feat: Add DeepSeek-V4 CSA attention kernels - #13
aws-zifan-he wants to merge 7 commits into
Conversation
…n across 8 cores in a single trainium 3 device
| @@ -0,0 +1,103 @@ | |||
| # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | |||
There was a problem hiding this comment.
[General comment] Can you add profiles with the PR. Decode and Prefill Profiles with few seqlen and some target sharding configs.
| @@ -0,0 +1,2100 @@ | |||
| # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | |||
There was a problem hiding this comment.
[General comment] Needs optimization, vectorize loops, reduce hbm round trips and fuse ops. Would be helpful to have profiles.
| # bf16->f16 round-to-nearest-even cast), and every downstream MAC is unchanged. | ||
| kv_chunks = [None] * num_k_chunks | ||
| for c_idx in nl.affine_range(num_k_chunks): | ||
| kv_bf = nl.ndarray((COMP_CHUNK, head_dim), dtype=nl.bfloat16, buffer=nl.sbuf) |
There was a problem hiding this comment.
can intialize outside the loop
There was a problem hiding this comment.
There is no latency change when hoisting this buffer initialization
|
Thank you for your contribution to the NKI Library! Please review our contribution guidelines if you haven’t already, and ensure your change follows our documented best practices. We are routing your PR to an appropriate reviewer and will follow up once it’s been assigned. |
Machine formatting pass (ruff format + isort) over the CSA kernels: reflow multi-line nisa calls to one argument per line, normalise slice and operator spacing, drop the unused torch import in csa_tp_all_reduce, and remove trailing blank lines at EOF. Also trims the long iteration-log comments that recorded measurements from development rather than explaining the code. No functional change -- every edit is formatting or comment prose.
Covers CSADecodeAttentionBlockNKI / CSAPrefillAttentionBlockNKI composition, which the per-kernel integration tests do not reach: the blocks interleave torch projections with NKI launches and span tensor-parallel ranks, so they fall outside what the kernel test framework traces.
| @@ -0,0 +1,1682 @@ | |||
| # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | |||
There was a problem hiding this comment.
This looks like a torch code that run as part of the kernels run which should not live here. These should be written in nki if you want to add it here.
There was a problem hiding this comment.
What would be the right place to put this file? Having an example/ folder and put the launcher there?
There was a problem hiding this comment.
Updated. Remain torch reference due to performance regression when switch to NKI.
| @@ -0,0 +1,293 @@ | |||
| # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | |||
… csa Syncs CR-300450903 revision 5. - nki_prefill_sparse_attn_kernel / nki_prefill_topk_kernel: an O(k) gathered prefill attention plus its top-k, selected at trace time on T_c. The sparse kernel is flat in context length while the dense-plus-mask path grows with it, so the two cross just under T_c = 4096. - Sequence-parallel prefill: sparse_prefill_q_range shards the scored second half by query range across ranks instead of by head, with compress_sharded and prefill_second_half_attention doing the sharded compressor and attention. - nki_tp_all_gather_kernel / tp_all_gather_rows: the row all-gather that reassembles the sequence-parallel prefill output across ranks. - Integration tests for both new prefill kernels.
Syncs the re-uploaded CR-300450903 revision 5 snapshot. nki_rms_rope_kernel takes heads / in_head_major / out_head_major, so one launch covers a multi-head q or de-RoPE tensor in either head-major ([heads * S, head_dim]) or query-major ([S, heads * head_dim]) layout, chosen independently per side. cos/sin are now indexed by position alone, so the caller no longer repeats the rotation table per head. csa_block passes the unrepeated tables and takes the core's native head-major output. Also trims the development narratives from csa_tp_all_reduce (compiler error codes and the failure modes behind the [2]-grid launch and the name= allocation), and drops an individual's name from a csa_block docstring -- the sequence-parallel layout rationale stands on its own.
03b1d7e to
d1bd880
Compare
…e rms+rope grid Syncs CR-300450903 revision 6. - The sparse prefill second half now dispatches through nki_prefill_topk_kernel with the indexer score kernel launched on the 2-core grid, which it already shards by program_id; a [1] launch left the second core idle. - The indexer's compressor rotates its result by an orthonormal Sylvester Hadamard, matching the model, with the torch reference and a rotate=1 test case to grade it. - nki_rms_rope_kernel goes SPMD over the (head, position-tile) space, splitting on heads when they divide the grid and on position tiles otherwise; a 1-core grid takes the same path unchanged. - The grouped low-rank output projection stays in XLA deliberately: it is already close to the tensor engine's achievable limit with no idle engine to claim. The profiling annotations in the CR's comments are kept qualitative here -- the absolute per-region latencies, MFU percentages and per-engine utilisation figures are omitted, as is the individual's name the earlier docstring carried.
…ed q_b prefill kernel Syncs CR-300450903 revision 7. - nki_indexer_qproj_rope_had_gemv folds the RoPE rotation and the orthonormal Hadamard into the indexer query-projection GEMV, so the decode indexer query needs one launch instead of a projection followed by two host-side rotations. - nki_qb_rms_rope_kernel runs the prefill wq_b projection together with its RMS and RoPE tail, fed by _pack_qb_weight, which pre-permutes the weight so each head's contraction axis lands on SBUF partitions and one head is one contiguous DMA. The pack runs on a parameter, so it constant-folds at trace time. - Drops the host-side _build_mask_from_scores path and trims module docstrings. Sensitive-content deltas from the CR: the product tier name is dropped from the CSAConfigFull docstring and from a test comment, in both cases leaving the shape itself described plainly.
302abc6 to
2cbd299
Compare
Summary
This change adds the whole DeepSeek-V4 CSA attention block as NKI kernels: it takes the raw hidden state of a new token and returns the projected block output, running on one Trainium3 chip as 4 tensor-parallel ranks of 2 logical NeuronCores each (whole single device). One
@nki.jitlaunch covers the lightning-indexer scoring, the top-k selection, the sparse attention and the output inverse RoPE, so neither the score row nor the selected-index array ever returns to the host. Over various sequence length, the kernel achieves 4x-170x speedup over the naive NKI baseline.Highlights
How the algorithm works
Standard attention compares each new token against every past token, so one decode step costs
O(S)and the KV cache grows with the conversation. At a 128K context it is the cache, not the weights, that limits how many users one device can serve. CSA replaces the full comparison with a selection: the model keeps a compressed KV cache, a small network scores every compressed position, and the attention reads only the 1024 highest-scoring positions plus a local window of 128. Neither count changes with the context length, so the attention's cost is constant.The 128 query heads are head-parallel across the 4 ranks, so each rank owns 32 query heads and 4 output-projection groups; one
torch_neuronx.traceof one rank emits one NEFF holding the projections, the sparse attention and the cross-rankall_reduce. Decode issues three launches per step:nki_qkv_rms_rope_kernel[1]nki_indexer_qproj_gemv[2]nki_indexer_score_topk_gather_2core[2]The evaluated model is the production DeepSeek-V4-Pro-Max configuration.
Parallelism
An important question when deploying a kernel to multiple cores is the dimension of distribution: how should we split the work across cores? Since we know that communication between chips is more expensive than communication inside a chip, our strategy is splitting works that does not require synchronizations across chip, and splitting works that need synchronization between two NeuronCores inside a chip. Hence:
The top-k on the GPSIMD engine
The rotational TopK in NKI Library employs max8 and nc_find_index8 to perform repetitive Top-8 operations and generalize to arbitrary TopK. This is very inefficient and bottleneck the vector engine to proceed the attention computations. Here comes to the key component of our optimization: we move the selection itself onto GPSIMD and fuse the TopK with multi-query index score computation in the indexer for further execution overlapping.
Another optimization we did along the way is reduce the DMA of KV cache. In DeepSeek V4, K and V are the same embedding. Instead of fetching the KV entries where one is transposed and one is not, we read from HBM once for K and V and transpose in SBUF.
After these updates, TopK selection is no longer the bottleneck. From 8K to 128K, the context grows 16x and the latency grows 1.24x, due to the scaling of computing indexer scores. The kernel achieves an overall 4.75x~16x speedup over the previous step.
DMA Traffic Shaping
The
nisa.dma_copyAPI carries apriority. This priority defines how you want to allocate the DMA bandwidth when multiple DMA operations are executed. A lower priority number means a higher priority. In the CSA kernel, we tune the priority based on the criticality and efficient of each DMA access to balance the latency.swdgegather; the indexer cache slice; the score store-back that the barrier waits on; the snake reformat; the q-projection weight bursts; the RMS tile assemblyK^TandV, which are contiguous and efficient; the RoPE cos and sin of the projection kernelPerformance
Whole-block decode latency (ms), BF16. Each row is a development step, so the table also shows what each mechanism was worth.
Launching a full block
The block is one PyTorch module. It owns its projection weights and the attention core, computes rank
tp_rank's shard, and appends the cross-rank all-reduce as its final op, so the traced block returns the full output:configis one rank's view of the model, fromshard_for_tp(CSAConfigFull(seq_len=...), tp_size): it divides bothn_headsando_groupsbytp_size, which leavesgroup_in = n_heads * head_dim / o_groupsat the full model's value — whatwo_aexpects, since the productionColumnParallelLinearis built from the global counts. A rank is then an ordinary block of its own size, constructed withtp_size=1. Passingreplica_ranks=Noneinstead returns the rank-local partial and leaves the caller to sum them.csa_block.pydrives this end to end against the CPU golden, in either of two modes:# The real topology: one rank per worker, all-reduce merged into each NEFF. torchrun --nproc_per_node=4 \ -m nkilib.experimental.deepseek_v4_csa.csa_block --phase decode --seq-len 32768Under
torchruneach worker pins itself to its own two physical NeuronCores, so the 4 ranks occupy 8 cores and run concurrently. The driver reads the base index fromNEURON_RT_VISIBLE_CORESorNEURON_RT_NUM_CORES.By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.