Repository navigation
[DSV4] Tuned ASM FP8 decode with split plans; HCA on aiter's persistent V4-NM kernel - #2462
Merged
Merged
Conversation
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>
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>
…de 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>
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>
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>
4 tasks done
Contributor
🏷️ CI GuideRuns automatically on every eligible PR before approval:
Heavy model tests:
|
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>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: yihonglie <hyi@amd.com>
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>
3 of 4 tasks
…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>
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>
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>
yhl-amd
marked this pull request as ready for review
October 4, 2026 05:28
Contributor
Author
Local accuracy validation (GSM8K, 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 |
valarLip
approved these changes
Oct 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
DeepSeek-V4 native FP8 decode on the gfx950 ASM kernels, in two parts. This PR supersedes #2411: its commits are included here.
1. Adapt to the tuned AITER ASM FP8 decode, with caller-built split plans (formerly #2411)
Adapt DeepSeek-V4 native FP8 decode to the swapped gfx950 ASM decode kernel from [ASM] [MLA v4 nm] Swap gfx950 asm decode kernel aiter#5195.
Pass a KV split plan to the decode ASM kernel. Whoever builds a decode CSR builds its split plan next to it, on the host: the split count comes from AITER's
get_mla_v4_nm_num_kv_splits([ASM] [HIP] [MLA v4 nm] Split plan helper and persistent decode kernel with in-kernel split and merge aiter#6126) andsplit_indptris a row of a constant[16, T + 1]uniform table (v4_uniform_split_table) built once at init, so no forward writes a plan and every CUDA graph reads constant memory.window, CSAwindow + index_topk, HCAwindow + max_model_len // 128), sized for the padded row count.prepare_mtp_decode): SWA plan for the draft rows.DSparkIndexBuffers, its own table): built with the draft CSR at stage 0 and read by every stage (B*Trows,W + TKV).Each plan depends only on the row count and config, so CUDA graph capture and replay agree.
sparse_attn_v4_paged_decodetakes a singlesplit_plan; the model selects it in the same ratio branch askv_indices/kv_indptr.v4_decode_split_planis the only place that checks for AITER's helper. With an AITER build that lacks it, or a pick outside the table, plans areNoneand AITER keeps its own split pick.ASM-only: no FlyDSL/Triton decode implementation, route or kernel file is added or changed.
2. HCA layers on aiter's persistent V4-NM kernel
HCA layers (compress_ratio 128) now use
mla_decode_fwd_v4_nm_psinstead of the decode ASM + host split plan. The kernel plans the split from the real per-row KV lengths every call, so HCA's growing KV no longer runs with a split fixed at graph-capture time.atom/model_ops/v4_kernels/hca_persist.py: gates, one workspace per device, call wrapper. If the installed aiter lacks the API, everything stays on the ASM path (logged once).ATOM_V4_HCA_PERSIST(default 1), fp8 KV, ratio 128, 128 local heads, gfx950,ATOM_V4_HCA_PERSIST_MIN_ROWS(default 15) <= rows <= 32768, row-dense pools. Smaller batches and all SWA/CSA layers keep the ASM + split plan.DeepseekV4Attention.__init__, all paths incl. plugins) before KV sizing and graph capture; a capture without a workspace stays on the ASM path instead of allocating.paged_decode.pytakescompress_ratio=;deepseek_v4.pypasses it at the decode call site.docs/environment_variables.md.Also:
tests/test_deepseek_v4_transfer_regions.pystubs the three imports these changes add todeepseek_v4_attn(get_tensor_model_parallel_world_size,v4_decode_split_plan,hca_persist), which the CPU unit-test runner was failing on.Dependencies
Test plan
.github/scripts/run_unit_tests.sh, CPU torch, no aiter), merged with currentmain: 0 failurestests/test_v4_prefill_asm_decode_dispatch.py,tests/test_paged_attention_dispatch.py,tests/test_dspark.py: pass with and without the AITER helpertests/test_v4_hca_persist_dispatch.py(CPU, mocked) +tests/model_ops/test_v4_hca_persist_gpu.py(GPU: vs ASM path and reference, K=0 padded rows, eager N < T_pad, offset kv_indptr, CUDA-graph replay with changing lengths, capture without workspace)--cudagraph-mode FULL, real draft acceptance: graph capture, correct outputs, DSpark acceptance unchanged vs. the stock image🤖 Generated with Claude Code