Skip to content

[DSV4] Tuned ASM FP8 decode with split plans; HCA on aiter's persistent V4-NM kernel - #2462

Merged
yhl-amd merged 13 commits into
ROCm:mainfrom
yhl-amd:perf/dsv4-hca-persistent-decode
Oct 4, 2026
Merged

yhl-amd merged 13 commits into
ROCm:mainfrom
yhl-amd:perf/dsv4-hca-persistent-decode

Conversation

@yhl-amd

@yhl-amd yhl-amd commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

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) and split_indptr is 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.

    • V4 metadata builder, verify/decode: one plan per layer type (SWA window, CSA window + index_topk, HCA window + max_model_len // 128), sized for the padded row count.
    • V4 metadata builder, MTP draft (prepare_mtp_decode): SWA plan for the draft rows.
    • DSpark draft (DSparkIndexBuffers, its own table): built with the draft CSR at stage 0 and read by every stage (B*T rows, W + T KV).

    Each plan depends only on the row count and config, so CUDA graph capture and replay agree.

  • sparse_attn_v4_paged_decode takes a single split_plan; the model selects it in the same ratio branch as kv_indices / kv_indptr.

  • v4_decode_split_plan is the only place that checks for AITER's helper. With an AITER build that lacks it, or a pick outside the table, plans are None and 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_ps instead 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).
  • Gates: 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.
  • Workspace allocated at model load (HCA 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.py takes compress_ratio=; deepseek_v4.py passes it at the decode call site.
  • Env vars documented in docs/environment_variables.md.

Also: tests/test_deepseek_v4_transfer_regions.py stubs the three imports these changes add to deepseek_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

  • Non-GPU unit suite as CI runs it (.github/scripts/run_unit_tests.sh, CPU torch, no aiter), merged with current main: 0 failures
  • tests/test_v4_prefill_asm_decode_dispatch.py, tests/test_paged_attention_dispatch.py, tests/test_dspark.py: pass with and without the AITER helper
  • tests/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)
  • ruff / black on changed files
  • DeepSeek-V4-Pro-DSpark, FP8 KV, TP8/EP8/DPA8, --cudagraph-mode FULL, real draft acceptance: graph capture, correct outputs, DSpark acceptance unchanged vs. the stock image
  • GSM8K 5-shot (1319) on DeepSeek-V4-Pro-DSpark, TP8 + DP attention + EP8, real acceptance, together with the MegaMoE changes: 0.9606 strict / 0.9613 flexible (stock image 0.9530)
  • Full-config server smoke (V4-Pro-DSpark, TP8 + DP-attention, FULL graphs, dspark K6, real acceptance): graph capture OK on all ranks, persistent path hit eager + captured, 37*48=1776, arithmetic 480/480

🤖 Generated with Claude Code

yhl-amd and others added 7 commits September 25, 2026 22:48
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>
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

🏷️ CI Guide

Runs automatically on every eligible PR before approval:

  • ✅ Pre Checkin: Black, Ruff, catalog schema validation, non-GPU unit tests

Heavy model tests:

  • ✅ Run after the PR is approved and Pre Checkin passes
  • ✅ Run immediately when an approval review is submitted
  • ✅ Can be requested before approval with labels
Label Tests
ci:full Run all heavy PR model tests: native ATOM, vLLM, and SGLang
ci:atom Run native ATOM model accuracy tests
ci:vllm Run ATOM vLLM OOT model accuracy tests
ci:sglang Run ATOM SGLang model accuracy tests

Heavy jobs are skipped when the PR is not approved and no matching ci:* label is present.
Add labels via the sidebar or gh pr edit 2462 --add-label <label>

yhl-amd and others added 3 commits October 3, 2026 08:50
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>
@yhl-amd yhl-amd changed the title [DSV4] Run HCA fp8 decode through aiter's persistent V4-NM kernel [DSV4] Tuned ASM FP8 decode with split plans; HCA on aiter's persistent V4-NM kernel Oct 4, 2026
@yhl-amd yhl-amd mentioned this pull request Oct 4, 2026
3 of 4 tasks
yhl-amd and others added 3 commits October 3, 2026 23:47
…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
yhl-amd marked this pull request as ready for review October 4, 2026 05:28
@yhl-amd

yhl-amd commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor Author

Local accuracy validation (GSM8K, CI accuracy configs)

Setup. 8x MI355X. Image: rocm/atom-dev:latest @ sha256:f6940ab6…, the image the nightly CI used for ATOM main 5b228e9.

Config CI threshold CI nightly @5b228e925 Local baseline Local PR
DeepSeek-V4-Pro (TP8) 0.94 0.9515 / 0.9522 0.9484 / 0.9492 0.9477 / 0.9477
DeepSeek-V4-Pro MTP (MTP3) 0.94, accept 0.622 0.9530 / 0.9530 0.9583 / 0.9591, accept 65.92% 0.9507 / 0.9515, accept 65.97%
DeepSeek-V4-Pro DSpark (DPA) 0.93 0.9500 / 0.9500 0.9484 / 0.9500, accept 43.64% 0.9477 / 0.9484, accept 43.91%
DeepSeek-V4-Pro DEP RCCL (DPA + EP) 0.94 0.9560 / 0.9568 0.9522 / 0.9538 0.9575 / 0.9575

Each entry is flexible-extract / strict-match. Acceptance is accepted / total draft tokens summed over every [MTP Stats Interval] line of every rank. For DPA that sum is needed because each rank logs its own stats.

  • Every PR result is above the CI threshold.
  • PR − baseline ranges from −0.0076 to +0.0053 with no consistent direction. That is within the ±0.006 standard error of a single 1319-sample run. Acceptance is unchanged for MTP3 and slightly higher for DSpark.
  • DPA configs (DSpark, DEP RCCL): these run at 128 local heads. The HCA persistent kernel is enabled there; all 8 ranks log V4 HCA persistent decode: device N ready.
  • TP8 and MTP3: these run at 16 local heads. The persistent kernel stays off by design (it needs 128 heads), so these configs cover the split-plan path of the decode ASM kernel.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants