Fix decode attention for Neuron SDK 2.32 and validate on Inf2 - #133
varuntej07 wants to merge 4 commits into
Conversation
contributed/decode_attention.py no longer runs. Its checks call nki.simulate_kernel, which Neuron SDK 2.32 / NKI 0.6.0 removed along with nki.baremetal and nki.benchmark, so `python contributed/decode_attention.py` fails with AttributeError on a current install. The file was merged in aws-neuron#129 against the older API, and aws-neuron#131's move to 2.32 updated the tutorials but not contributed/. Port the harness: - nki.simulate_kernel -> nki.simulate for the CPU path. - nki.baremetal -> a plain kernel(*args) call, which on 0.6.0 is the on-device path: @nki.jit "compiles and executes standalone, without a framework" when handed numpy arrays. - Add --backend, defaulting to the device when /dev/neuron0 exists and to simulation otherwise, so the file does the right thing on either host. - Factor input construction into _make_mha_inputs / _make_gqa_inputs so both kernels run on either backend from one code path. Record why neuronxcc.nki is not a fallback: nki.baremetal, nki.benchmark and nki.simulate_kernel still exist there, but they cannot drive a kernel decorated with the current top-level @nki.jit and raise AttributeError: 'Kernel' object has no attribute 'grid', because they expect the older TraceKernel. Gate bf16 behind BF16_SUPPORTED. bf16 inputs do not compile on NeuronCore-v2: nl.matmul infers its PSUM destination dtype from its operands, and the gen2 tensor engine rejects a non-fp32 matmul destination with "nc_matmul dst dtype must be float32 on gen2, got bfloat16". Fixing it means explicit fp32 PSUM tiles via nisa.nc_matmul at the matmul sites and dtype=nl.float32 at the transpose sites, which is a kernel change rather than a harness change, so it is left for a follow-up. gen3 accepts a bf16 destination, which is why review on Trn2 did not surface this. Add _quantize so a low-precision run compares against the reference computed from the same narrowed values widened back to fp32, keeping NumPy's own low-precision arithmetic out of the measured error. No kernel logic changes: decode_attention_fwd, decode_attention_gqa_fwd and both NumPy references are unchanged apart from comments. Tested on inf2.xlarge (NeuronCore-v2), NKI 0.6.0, fp32, both backends: MHA max|diff| 7.30e-07, GQA (seqlen_kv=512, group=4) max|diff| 1.46e-06. This is the first hardware validation of these kernels on Inferentia2; aws-neuron#129 was reviewed on Trainium2.
Timing a plain kernel(*args) call on NKI 0.6.0 measures the compiler, not the
kernel: nki/framework/compiled.py passes enable_cache=False to
compile_kernel_to_nir, so the standalone path re-runs the frontend on every
invocation (~1.5 s per call on Inf2, against ~67 us of kernel time at
seqlen_kv=512). nki.benchmark, which older samples used, was removed in 2.32
along with simulate_kernel and baremetal.
contributed/decode_attention_benchmark.py compiles once through the parser
frontend, the same frontend @nki.jit itself uses, and replays the NEFF:
with nki_ir_context() as context:
result = ParserFrontend().compile(context, kernel, inputs=...)
compiled = CompiledKernel.from_frontend(result, compile_opts)
res = compiled.benchmark(warmup=..., iterations=..., **tensors)
Two sweeps, both against the hypothesis that decode attention is memory-bound:
KV heads halved with query heads fixed, and seqlen_kv swept with head counts
fixed. benchmark() returns the real outputs, so every timed row is also checked
against the NumPy reference; a fast wrong kernel is not worth reporting.
Measured on inf2.xlarge, fp32, d=128, 5 warmup + 50 iterations. The kernel
reaches 4.7% of the 410 GB/s per-core HBM peak at best, and estimated_utilization
puts Vector at 41.5% against DMA at 5.5%. It is Vector-bound, not
bandwidth-bound, which makes softmax partition occupancy the target for
split-KV rather than the loads.
This lives in its own file because it needs a device. The numeric checks in
decode_attention.py still run anywhere via nki.simulate.
Closes the one thread left open on aws-neuron#129, where @aws-mattmcm asked whether new content should use range instead of affine_range and the decision was left to the kernel author. On NKI 0.6.0 the answer is yes, and more strongly than the question implies. nl.affine_range, nl.sequential_range and nl.static_range are all deprecated ("will be removed in future releases") and their docstrings all say to prefer range() directly. nl.range is not a separate API either: it is the same function object as nl.affine_range. In both backends the implementations are identical: # nki/_backends/mlir_tracer/__init__.py, and the simulator backend def affine_range(start, stop, step): return range(start, stop, step) def sequential_range(start, stop, step): return range(start, stop, step) def static_range(start, stop, step): return range(start, stop, step) So the iterator name conveys nothing to the compiler on 0.6.0; the earlier affine-vs-sequential distinction is gone. Both loops become plain range, with the dependency reasoning kept in comments where it still belongs: the KV-head loop's iterations are genuinely independent, and the KV-tile loop genuinely carries (m, l, acc) and relies on being emitted in program order. Re-validated on inf2.xlarge after the change, fp32, both backends: MHA max|diff| 7.30e-07, GQA (seqlen_kv=512, group=4) max|diff| 1.46e-06, unchanged from before it.
|
Hi @varuntej07, can we split this into two PRs? This one with the decode_attention.py port and range change, as it is self-contained, correct, and uses public APIs. The solution for decode_attention_benchmark.py relies on APIs that are not part of the public contract and this will likely break again with the next release. |
Per review: decode_attention_benchmark.py reaches for compiler internals that are not part of the public contract, so it does not belong alongside a change that is otherwise public-API only. Specifically it depended on nki_ir_context pulled out of compile_to_bir's __globals__, the private CompiledKernel._PEAK_HBM_BW, and the nki.compiler.frontend / nki.compiler.ncc_driver submodules, none of which are in nki's top-level exports. That is exactly the surface that moved under this file between 2.29 and 2.32 in the first place. Drops the benchmark, the two .gitignore entries for its NEFF artifacts, and the docstring line pointing at it. What remains is the SDK 2.32 harness port and the range change, unchanged and still validated on Inf2.
|
Thanks, that's fair and I've split it. This PR is now just the Before I open a second PR, I'd rather ask what you'd actually accept, because I couldn't find a supported route. There's no public timing API in 0.6.0, and Is there a supported way to get latency for a Separately, the measurement did turn up something worth recording regardless of whether the tool itself lands. Decode attention is usually assumed memory-bound, but on Inf2 this kernel is Vector-bound: it reaches 4.7% of the 410 GB/s per-core HBM peak at best, |
Description of changes:
contributed/decode_attention.py, merged in #129, no longer runs on a current install. Its correctness checks callnki.simulate_kernel, which Neuron SDK 2.32 / NKI 0.6.0 removed along withnki.baremetalandnki.benchmark. #131 moved the repo to 2.32 and rewrote theattention_fwd_performancetutorial, but did not touchcontributed/, so the break is latent in what is already onmain:Updated per review to contain only the port and the
rangechange. The device benchmark that was originally in this PR has been split out.The kernels themselves are unchanged.
decode_attention_fwd,decode_attention_gqa_fwdand both NumPy references differ only in the loop iterators and comments described below.1. Harness ported to the 0.6.0 API
nki.simulate_kernel(kernel, *args)nki.simulate(kernel)(*args)nki.baremetal()(kernel)(*args)kernel(*args)The second row is per
@nki.jit's own docstring, "compiles and executes standalone kernel, without a framework": a plain call with NumPy arrays is the device path on 0.6.0, which is whybaremetalwas removed rather than renamed.Adds
--backend {simulate,baremetal}, defaulting to the device when/dev/neuron0exists, sopython contributed/decode_attention.pydoes the right thing on a laptop or on an instance.Worth flagging for anyone porting another
contributed/kernel:simulate_kernel/baremetal/benchmarkdo still import from the deprecatedneuronxcc.nkinamespace, which makes them look like a working fallback. They are not. They cannot drive a kernel decorated with the current top-level@nki.jitand fail withAttributeError: 'Kernel' object has no attribute 'grid', because they expect the olderTraceKernel.2.
nl.affine_range-> builtinrange(closes the thread on #129)@aws-mattmcm asked on #129 whether new content should use
rangeinstead ofaffine_range, and @mrkcath-aws merged as-is and left it to me. On 0.6.0 the answer is yes.nl.affine_range,nl.sequential_rangeandnl.static_rangeare all deprecated, all three docstrings say to preferrange()directly, and their implementations are identical in both the tracer and the simulator backends:nl.rangeis not the migration target either: it is the same function object asnl.affine_rangeand carries the same deprecation. So both loops become plainrange, and the dependency reasoning moves into comments, since the iterator name no longer records it. The KV-head loop's iterations are genuinely independent (each gets its own softmax state and touches disjoint slices ofqandout), and the KV-tile loop genuinely carries(m, l, acc)and relies on in-order unrolling.Validation
Run on
inf2.xlarge(NeuronCore-v2, gen2), NKI 0.6.0, fp32, before and after therangechange, with identical results:Also clean under
python -W error::DeprecationWarning.This is the first validation of these kernels on Inferentia2; #129 was reviewed on Trainium2. That distinction caught one real thing, now gated rather than silently broken: bf16 inputs do not compile on gen2, because
nl.matmulhas nodtypeparameter and infers its PSUM destination dtype from the operands, and the gen2 tensor engine rejects a non-fp32 matmul destination:trn2 is gen3 and appears to permit it, which is why a Trn2 review would not surface it. The file now skips bf16 with an explanatory note on gen2 instead of failing to compile. Fixing it properly means explicit fp32 PSUM tiles plus
nisa.nc_matmulat all four matmul sites, which changes the kernels, so it belongs in its own PR.Per the README,
contributed/is experimental, and the baremetal / p99 / E2E requirements inCONTRIBUTING.mdsit under "Requirements for Kernels Targetingsrc/reference/", so I have followed the in-filecheck_correct()/main()house style of the othercontributed/kernels rather than adding atest_*.py.By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.