Repository navigation
[ASM] [HIP] [MLA v4 nm] Split plan helper and persistent decode kernel with in-kernel split and merge - #6126
Conversation
The gfx950 v4 nm auto split pick (ignore_total_kv=1) maximizes CU occupancy only, so it prefers whole multiples of cu_num: for gqa=128 single-token decode it picks 13-16 splits (several workgroup waves) for batches whose KV already fits one wave, paying extra partial writes and a wider stage-2 merge for no added parallelism. - On gfx950, pick num_kv_splits by minimizing a fitted latency model: workgroup waves x per-wave KV time, plus the partial-write/stage-2 cost of splitting. Other archs keep the existing pick. - Add kv_len_hint: callers that pass capacity-sized kv_page_indices (e.g. CUDA-graph decode) cannot be planned from total_kv / num_seqs, so they pass the per-seq KV length to plan for. The pick depends only on shapes and this int, so it stays fixed across graph capture and replay. - Fix the stage-2 merge for a caller-supplied split_indptr with one split per seq on a grid > 1: it took the in-place BF16 copy (keyed on split_indptr[-1] == num_seqs) although the kernel wrote FP32 partials, returning NaN. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com>
Planning inside mla_decode_fwd_v4_nm needs a per-seq KV length, and the only one the wrapper can see, kv_page_indices.shape[0] / num_seqs, is meaningless for CUDA-graph callers whose index buffers are capacity-sized. - Add get_mla_v4_nm_split_plan(num_seqs, num_heads, kv_len, split_indptr=): returns the split count (cost model on gfx950, occupancy pick elsewhere) and fills a caller-owned persistent split_indptr in place. Callers build it in their metadata step and pass both to the wrapper. - Drop kv_len_hint; the wrapper's own auto pick is unchanged from main. - Tests: plan fill, accuracy through the wrapper, and CUDA-graph replay with different KV lengths under a captured plan. _run_varlen_point now returns its checkAllclose error so the new tests gate on it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com>
- get_mla_v4_nm_split_plan returns MlaV4NmSplitPlan(num_kv_splits, split_indptr). The field names are the wrapper's kwargs, so callers pass a plan on as mla_decode_fwd_v4_nm(..., **plan._asdict()). - Latency-model coefficients live in a per-arch table; archs without an entry keep the occupancy-only pick. Only gfx950 is fitted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com>
…erge A persistent variant of the v4 nm fp8 decode kernel (gqa=128, one query token per row). Each call is one launch of grid (2P, 1, 1): the kernel plans the KV split over P partitions from kv_indptr, runs the attention and merges the split partials itself, so callers need no host split plan and no stage-2 merge, and the launch is CUDA-graph safe with varying KV lengths. - hsa/gfx950/mla_v4: mla_a8w8_qh64_qseqlen1_gqaratio64_nm_ps.co, symbol _ZN5aiter39mla_a8w8_qh64_qseqlen1_gqaratio64_nm_psE, registered as the ps=1 row of mla_v4_asm.csv. - csrc: mla_decode_v4_ps_asm in module_mla_v4_asm, with its own documented kernarg struct (same 21 x 16 B layout, repurposed slots marked), shape / dtype / contiguity / workspace checks and its own kernel cache. - aiter.mla: MlaV4NmPsWorkspace, get_mla_v4_nm_ps_workspace(device, num_partitions=128) and mla_decode_fwd_v4_nm_ps(...). One workspace serves calls that are ordered on the GPU; allocate it outside capture. - Tests: accuracy and LSE against the fp8-dequant reference (uniform, ragged, K=0 rows, N up to 3000, K up to 8320), kv_indptr with offset and extra entries, workspace reuse across batch sizes, partition counts, CUDA-graph replay with changing KV lengths, agreement with mla_decode_fwd_v4_nm, argument checks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com>
mla_a8w8_qh64_qseqlen1_gqaratio64_nm_ps.co is now reproducible from sources in the repo: nm_ps_src/gen.py patches the disassembly of the ROCm#5195 decode kernel (asm/rt.s) into the persistent kernel, splices in the in-kernel split planner and last-arriver merge (splice/*.hip, compiled with hipcc -S), renames the kernel symbol, assembles for gfx950 and can check the sha256 against the shipped .co. README.md documents provenance, rebuild steps and the kernarg layout. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com>
The persistent code object never reads kernarg slot 0xb8, yet the span workspace [P, 2] was a field of MlaV4NmPsWorkspace, a required argument of mla_decode_v4_ps_asm and size-checked in C++. Remove it from the workspace, the ctypes signature and the C++ entry point, and pass nullptr at 0xb8. Document the three regions of the cnt counter buffer next to its size. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com>
…rnel Run mla_decode_fwd_v4_nm_ps in a subprocess at large N (up to the 32768 row limit) with every input and output placed at the very end of its own 2 MiB-rounded allocation under the non-caching allocator, so an access one element past q, qrope, the KV pools, kv_indptr (exactly N+1 entries), kv_page_indices, sink, out or lse faults the GPU. Covers the lse and no-lse paths and checks the merge counters return to zero. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com>
…rator Rename the spliced planner / combine kernels and their labels (t8pre -> ps_plan, t8fin -> ps_merge, T8_* -> PS_*) and regenerate the code object. .text, .rodata (kernel descriptor) and .note (metadata) are byte-identical to the previous build; only local symbol names in .symtab change. Update the expected sha256. README fixes: the row limit is N <= 32768 (out / q byte offsets wrap above it; the cnt / arange sizes come from a 65536-row layout), cnt is int32 with its three regions spelled out, and slot 0xb8 is passed as 0. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com>
🏷️ CI GuideRuns automatically on every PR:
Extended tests (opt-in via labels):
One backend per PR: PR title tags & labels: |
Drop hsa/gfx950/mla_v4/nm_ps_src (assembly, patch scripts and HIP splice sources). Nothing at build or run time reads it; the kernel ships as mla_a8w8_qh64_qseqlen1_gqaratio64_nm_ps.co, unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com>
Resolve op_tests/test_mla_v4_nm.py: _run_varlen_point takes main's _gated_allclose (ROCm#5905), which now also returns the mismatch fraction so the split-plan tests can keep asserting on it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com>
dict literals for the workspace kwargs and the test inputs; one with-statement for the capture-time rejection check. No behaviour change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com>
…ew fixes - The metadata builder no longer builds the HCA split plan for forwards whose HCA decode the persistent kernel takes (it plans on the GPU from the real kv_indptr). The decision uses only init-time state (workspace allocated before any capture) and the row gate shared with dispatch (hca_persist.rows_ok), so capture and every replay agree; a call the kernel then declines gets aiter's own split pick. - _prepare_hca_persist delegates to hca_persist.prepare_if_usable and returns whether HCA can take the kernel. - DeepseekV4Attention passes its own kv_fp8 flag to prepare_if_usable. - Docs: the workspace is allocated at model load, not by the builder; the split-plan helper reference points at ROCm/aiter#6126. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com>
get_mla_v4_nm_num_kv_splits(num_seqs, num_heads, kv_len) returns the split count the plan helper picks, with no device work. The uniform split_indptr for it is [0, s, 2s, ...], and any prefix of a longer one is valid, so a caller can keep one constant buffer per split count and slice it instead of writing a split_indptr before every call. get_mla_v4_nm_split_plan now builds on it; its behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com>
Nothing calls the plan helper or MlaV4NmSplitPlan any more: callers take the count from get_mla_v4_nm_num_kv_splits and pair it with the uniform split_indptr [0, s, 2s, ...]. The tests build that pair with a local helper, and the mla_decode_fwd_v4_nm docstring points at the count helper. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com>
Local accuracy validation with the companion ROCm/ATOM#2462 (GSM8K, ATOM CI accuracy configs)Setup. 8x MI355X. Image:
Each entry is flexible-extract / strict-match. Acceptance is accepted / total draft tokens summed over every
🤖 Generated with Claude Code |
…nt V4-NM kernel (#2462) * perf(dsv4): adapt tuned AITER ASM FP8 decode * style: use functools cache for device arch * perf(dsv4): steer AITER decode split pick with kv_len_hint AITER's V4 NM wrapper can now pick num_kv_splits from a latency model when told the per-token KV length to plan for (`kv_len_hint`). ATOM's decode kv_indices buffers are capacity-sized, so AITER cannot derive that length itself; the builder publishes the worst case per layer type and the decode call forwards it: - CSA: window + index_topk - HCA: window + max_model_len // 128 Both depend only on config, so the pick stays fixed across CUDA graph capture and replay. With an AITER build that predates `kv_len_hint` (detected from the wrapper signature), CSA keeps the batch split table and HCA keeps AITER's own auto pick. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com> * perf(dsv4): pass AITER decode split plans through attention metadata Replace kv_len_hint with an explicit split plan. The V4 metadata builder calls AITER's get_mla_v4_nm_split_plan once per decode step for each sparse layer type and stores (num_kv_splits, split_indptr) on the attention metadata; the fp8 ASM decode forwards both to mla_decode_fwd_v4_nm. - Plans are sized for the padded row count and the longest kv_len of the layer type (SWA: window, CSA: window + index_topk, HCA: window + max_model_len // 128), so capture and replay agree. - split_indptr lives in persistent forward_vars buffers (per ubatch prefix) that the builder rewrites in place before each forward; graphs of different sizes share them. - MTP draft steps clear the SWA plan (it is sized for the verify rows) and fall back to AITER's own pick. - AITER builds without get_mla_v4_nm_split_plan keep the CSA batch table. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com> * refactor(dsv4): one split-plan path, owned by whoever builds the decode CSR - v4_decode_split_plan (paged_decode) is the single place that knows about aiter's get_mla_v4_nm_split_plan; it returns None on older aiter, which leaves the split pick to aiter. - Each owner of a decode CSR builds its plan next to it, in its own persistent split_indptr buffer: the V4 metadata builder (SWA/CSA/HCA for verify, SWA for the MTP draft) and DSpark's DSparkIndexBuffers (built with the CSR at stage 0, read by every stage). - sparse_attn_v4_paged_decode takes one split_plan argument; the model picks it in the same ratio branch as kv_indices / kv_indptr. - Drop the legacy CSA batch table and the query_group / kv_kind parameters it needed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com> * [DSV4] Run HCA fp8 decode through aiter's persistent V4-NM kernel HCA (compress_ratio 128) decode calls now use aiter.mla.mla_decode_fwd_v4_nm_ps: one launch that plans the KV split on the GPU from kv_indptr and merges the partials in-kernel, so no host-side split plan and no stage-2 merge. - atom/model_ops/v4_kernels/hca_persist.py: gates, per-device workspace (get_mla_v4_nm_ps_workspace) and the call. Taken only for fp8 KV, 128 local heads, gfx950, row-dense KV pools and ATOM_V4_HCA_PERSIST_MIN_ROWS <= rows <= 32768; everything else stays on the decode ASM + split plan. An aiter without the kernel stays on the ASM path and logs once. - The V4 metadata builder allocates the workspace at init, before warmup, KV sizing and graph capture. All HCA decode calls run on the forward's compute stream, so one workspace per device is enough. - sparse_attn_v4_paged_decode takes the caller's compress_ratio (passed from DeepseekV4Attention's decode call site). - ATOM_V4_HCA_PERSIST (default 1) and ATOM_V4_HCA_PERSIST_MIN_ROWS (default 15), documented in docs/environment_variables.md. - Tests: mocked dispatch rules, and a gfx950 test against the ASM path and the torch reference, including padded rows, an offset kv_indptr and CUDA-graph replay with changing KV lengths. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com> * [DSV4] Allocate the persistent HCA workspace at model load on every path Only the native V4 metadata builder called hca_persist.prepare(). The vLLM and SGLang plugins reach the same persistent HCA decode branch without that builder, so their workspace was allocated lazily on the first qualifying call: after KV sizing, and as a hard failure if that call was being captured into a CUDA graph. - Every HCA DeepseekV4Attention layer now calls hca_persist.prepare_if_usable() at construction (switch on, fp8 KV, 128 local heads, gfx950), so model load allocates the workspace in native ATOM and in both plugins, before KV sizing and graph capture. The native builder call stays (idempotent) and shares the eligibility check. - The dispatch gate asks hca_persist.workspace_ready(): a call that finds no workspace allocates it when eager, and stays on the ASM path under capture instead of raising. - Tests: CPU dispatch tests for the capture fallback and the layer-init hook; GPU test capturing a graph with no workspace (ASM result checked against the reference). Signed-off-by: yihonglie <hyi@amd.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * [DSV4] Satisfy ruff in the persistent HCA changes Fold hca_persist into the existing v4_kernels import, drop the E402 noqa directives ruff no longer enables, use dict literals in the GPU test, and underscore an unused unpacked tensor. No behaviour change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com> * [DSV4] Fix the non-GPU unit tests for the split-plan and HCA imports test_deepseek_v4_transfer_regions loads the real deepseek_v4_attn against a fixed set of import stubs, which did not cover what the decode changes added: aiter.dist.parallel_state.get_tensor_model_parallel_world_size, v4_kernels.v4_decode_split_plan and v4_kernels.hca_persist. Under the CPU runner the module then failed to import (41 errors); with test_pp collected first, its narrower parallel_state stub was picked up instead. Supply the three in the test's own stub set (hca_persist reports itself unusable, as it is without aiter), and use a dict literal in the prefill dispatch test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com> * [DSV4] Skip the HCA split plan where the persistent kernel runs; review fixes - The metadata builder no longer builds the HCA split plan for forwards whose HCA decode the persistent kernel takes (it plans on the GPU from the real kv_indptr). The decision uses only init-time state (workspace allocated before any capture) and the row gate shared with dispatch (hca_persist.rows_ok), so capture and every replay agree; a call the kernel then declines gets aiter's own split pick. - _prepare_hca_persist delegates to hca_persist.prepare_if_usable and returns whether HCA can take the kernel. - DeepseekV4Attention passes its own kv_fp8 flag to prepare_if_usable. - Docs: the workspace is allocated at model load, not by the builder; the split-plan helper reference points at ROCm/aiter#6126. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com> * [DSV4] Build decode split plans on the host from one constant table Every fp8 decode split plan is now (s, split_table[s - 1]): aiter's host-only split count (get_mla_v4_nm_num_kv_splits) and a row of a [16, T + 1] uniform split_indptr table built once at init (v4_uniform_split_table). No forward writes a split_indptr any more, and CUDA graphs of every size read constant memory, so a graph can no longer see another size's plan. - The builder's per-layer-type, per-ubatch and MTP-draft split_indptr buffers are replaced by the one shared table; DSparkIndexBuffers keeps its own table sized for its draft rows. - With planning free, the HCA plan is built again for every forward (the persistent kernel ignores it), so the builder no longer tracks whether HCA takes that kernel. - A split count beyond the table, or rows beyond it, leaves the pick to aiter. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com> * [DSV4] Describe split plans as plain tuples in the docstrings aiter no longer has MlaV4NmSplitPlan; ATOM's plans were already (num_kv_splits, split_indptr) tuples from v4_decode_split_plan. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com> --------- Signed-off-by: yihonglie <hyi@amd.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Summary
Two parts for the gfx950 V4-NM fp8 decode (
mla_decode_fwd_v4_nm), used by DeepSeek-V4 decode in ATOM. This PR supersedes #5890: its commits are included here.1. Host-only split count for callers, from a cost model (formerly #5890)
get_mla_v4_nm_num_kv_splits(num_seqs, num_heads, kv_len) -> int. Split count formla_decode_fwd_v4_nm, computed on the host with no device work. Callers pass it with the uniformsplit_indptr[0, s, 2s, ...]; any prefix of a longer one is valid, so a caller can keep one constant buffer per split count and slice it (ATOM does), and CUDA graphs read constant memory.num_seqs,num_headsand the caller'skv_len, never onkv_page_indices, whose length is meaningless for capacity-sized graph buffers.mla_decode_fwd_v4_nmis the same as main.split_indptrthat gives every seq one split on a grid > 1, the merge took the in-place BF16 copy (keyed onsplit_indptr[-1] == num_seqs) even though the kernel wrote FP32 partials, returning NaN. The v4 nm merge now always reduces the FP32 partials.Why callers should not rely on the old auto pick: it maximizes CU occupancy only, so for gqa=128 single-token decode it picks 13-16 splits (several workgroup waves) for batches whose KV already fits one wave, adding partial writes and stage-2 work without adding parallelism.
2. Persistent decode kernel with in-kernel split and merge
A persistent variant of the kernel (
ps=1row inmla_v4_asm.csv). One launch per call: a fixed grid of2*Pworkgroups walks per-partition work descriptors, the split plan is computed inside the kernel from the realkv_indptrevery call, and split rows are merged by the last-arriving workgroup. Rows that are not split write the final bf16 output directly. Because nothing about the split depends on host-side shapes, it is CUDA-graph safe with varying KV lengths.hsa/gfx950/mla_v4/mla_a8w8_qh64_qseqlen1_gqaratio64_nm_ps.co+ CSV row (ps=1,lse=1).csrc/py_itfs_cu/asm_mla_v4.cu:mla_decode_v4_ps_asmentry (documented packed kernarg struct, shape / workspace checks).aiter/mla.py:get_mla_v4_nm_ps_workspace(device, num_partitions=128)andmla_decode_fwd_v4_nm_ps(q_packed, q_rope, kv_packed, kv_rope, kv_indptr, kv_page_indices, sink, workspace, out=None, return_lse=False, lse=None).N <= 32768, row-dense KV pools, rows with K=0 left unwritten (same as the non-persistent kernel). Calls sharing a workspace must be ordered on the GPU; allocate the workspace outside graph capture.Merged with current
main; theop_tests/test_mla_v4_nm.pyconflict with #5905 is resolved (the varlen check uses_gated_allclose, which now also returns the mismatch fraction).Consumer: ROCm/ATOM#2462 (split plan for the decode ASM path, HCA layers on the persistent kernel).
Test plan
op_tests/test_mla_v4_nm.pyon gfx950 (MI355X), after mergingmain, fresh JIT build: 47 passedtest_v4_nm_split_planner_picks,test_v4_nm_num_kv_splits_uses_cost_model,test_v4_nm_split_plan_accuracy,test_v4_nm_split_plan_cudagraph_replay(captured once, replayed with different KV lengths),test_v4_nm_one_split_per_seq_on_wide_gridkv_indptr, workspace reuse across batch sizes, CUDA-graph replay with changing KV lengths, agreement withmla_decode_fwd_v4_nm, guard-page out-of-bounds checks, argument rejection🤖 Generated with Claude Code