Skip to content

refactor: shared DSA indexer forward and top-k schedule, sparse prefill decided once, DCP kernel cleanup - #2776

Merged
valarLip merged 3 commits into
mainfrom
refactor-dsa-indexer-sparse-mla
Oct 28, 2026
Merged

valarLip merged 3 commits into
mainfrom
refactor-dsa-indexer-sparse-mla

Conversation

@valarLip

@valarLip valarLip commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Structure

  • Model registry (support_model_arch_dict, mono_decode_installers) moves from model_runner.py to atom/models/registry.py; docs and skills point there.
  • The DSA indexer op sparse_attn_indexer moves from models/deepseek_v2.py to model_ops/indexer/indexer_forward.py.
  • model_ops/indexer/schedule.py is the one classification of which layers reuse an earlier layer's top-k. The model, the MLA layer (which layer owns a selection and its work plan) and the PP runner (which stage hands its selection on) all read it; the runner's hand-copied version is gone.
  • The MLA metadata builder decides sparse prefill once a step (attn_metadata.run_sparse); MLA, the indexer, PCP and the sglang bridge follow it instead of each re-deriving max_seqlen_k > index_topk.
  • flatten_dsa_config publishes a nested dsa_config as the flat index_* fields; index planes are allocated only for the layers it names.
  • GDN causal-conv metadata is built from host-side sequence lengths (no device-to-host copy).
  • atom.model_ops.indexer loads its names on first use, so its kernel-free submodules (schedule) import without AITER.
  • Kimi-K3 image request inputs move to atom/models/kimi_k3/image_processing.py, so building them does not import the vision model or AITER.

Indexer and sparse MLA

  • NoPE indexer support: index_rope_dim, a fused FP8 query-quant and key-cache writer (model_ops/indexer/nope.py), and keys/head weights projected by the owning attention. The unpooled glm5_next path now uses the shared indexer.
  • A single-rank FP8 prefill subtracts request bases inside the slot conversion instead of rewriting the top-k tensor.
  • Sparse-prefill work buffers are sized for prefill's own gathered head width (it can differ from decode's).
  • A BF16 sparse-prefill row stays in one work unit, so its result does not depend on batch composition.
  • Small BF16 persistent DCP prefills split 128 heads into two independent groups (attentions/sparse_mla_head_groups.py); the builder owns one buffer set and binds it to the layers that own a selection, and only when no layer shares top-k.
  • BF16 ASM prefill pads 17..127 query heads to the 128 the kernel accepts.
  • Stable top-k whenever tensor parallelism is on.

DCP

  • The prefill query gather fuses rank-to-token reordering with head padding.
  • zero_nonfinite_rows checks 32 rows per program and exits before touching output when all are finite; the prefill indptr prefix sum is one launch; the decode candidate exchange uses the custom all-gather.
  • Ownership and slot mapping are shared helpers for the count and compact kernels.
  • Fixes: the local logits width covers partial interleave groups; the prefill key gather ignores padded block-table rows.

Loading

  • A layer the checkpoint stores in a plain float dtype is treated as unquantized, decided from the safetensors headers (no tensor is loaded).
  • Loading warns when a checkpoint float tensor is converted to another float dtype.

Kimi-K3

  • KDA control parameters, short convolutions and the output norm are FP32; e_score_correction_bias is FP32.
  • When the config names DSA layers, those layers fuse the indexer key and head-weight projections into the q_a/kv_a projection (K3FusedIndexerProjection).
  • The BF16 output gate uses the fused sigmoid-mul; BF16 gated norm and attention residual keep one reduction layout at every batch size.
  • Vision projections run on the aiter BF16 GEMM.

Other

  • MoE: pure tensor parallelism routes pad rows like real rows for now (TODO in _select_logical); a -1 route faults the TP a16w4 MoE path.
  • The multimodal chat endpoint returns prompt_token_ids when requested.

Test plan

  • ruff / black on every changed file
  • Targeted unit and GPU tests (807 passed; the two test_row_view_* cases failed only because aiter could not compile module_gemm_a8w8_blockscale_bpreshuffle in that test environment): index top-k schedule, NoPE writer and FP8 MQA, sparse MLA prefill (batch-composition independence, head groups), DCP topk / merge / prefill query gather / LSE mask, MLA index cache, glm5_next kpool, FP4 indexer, DeepSeek-V4.1 indexer and paged scoring, Kimi-K3 activations and norm batch invariance
  • Full CI-scope unit tests (tests/ minus tests/plugin and test_openai_server.py); the only failures should be ones that already fail on main

@github-actions

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 2776 --add-label <label>

…ll decided once, DCP kernel cleanup

Structure
- The model registry (support_model_arch_dict, mono_decode_installers) moves
  from model_runner.py to atom/models/registry.py.
- The DSA indexer op (sparse_attn_indexer) moves from models/deepseek_v2.py to
  model_ops/indexer/indexer_forward.py.
- model_ops/indexer/schedule.py is the one classification of which layers
  reuse an earlier layer's top-k; the model, the MLA layer and the PP runner
  all read it.
- The MLA metadata builder decides sparse prefill once a step
  (attn_metadata.run_sparse); MLA, the indexer, PCP and the sglang bridge
  follow it.
- flatten_dsa_config publishes a nested dsa_config as the flat index_* fields;
  index planes are allocated only for the layers it names.
- GDN causal-conv metadata is built from host-side sequence lengths.
- atom.model_ops.indexer loads its names on first use, so its kernel-free
  submodules import without AITER.
- Kimi-K3 image request inputs move to atom/models/kimi_k3/image_processing.py,
  so building them does not import the vision model or AITER.

Indexer and sparse MLA
- NoPE indexer: index_rope_dim, a fused FP8 query-quant and key-cache writer
  (model_ops/indexer/nope.py), and keys/head weights projected by the owning
  attention; the unpooled glm5_next path uses the shared indexer.
- A single-rank FP8 prefill subtracts request bases inside the slot
  conversion instead of rewriting the top-k tensor.
- Sparse-prefill work buffers are sized for prefill's own gathered head
  width; a BF16 sparse-prefill row stays in one work unit, so its result does
  not depend on batch composition; small BF16 persistent DCP prefills split
  128 heads into two groups with one shared buffer set bound by the builder.
- BF16 ASM prefill pads 17..127 query heads to the 128 the kernel accepts.
- Stable top-k whenever tensor parallelism is on.

DCP
- The prefill query gather fuses rank-to-token reordering with head padding.
- zero_nonfinite_rows checks 32 rows per program; the prefill indptr prefix
  sum is one launch; the decode candidate exchange uses the custom
  all-gather; ownership and slot mapping are shared helpers.
- The local logits width covers partial interleave groups, and the prefill
  key gather ignores padded block-table rows.

Loading
- A layer the checkpoint stores in a plain float dtype is treated as
  unquantized, decided from the safetensors headers.
- Loading warns when a checkpoint float tensor is converted to another float
  dtype.

Kimi-K3
- KDA control parameters, short convolutions and the output norm are FP32;
  e_score_correction_bias is FP32.
- When the config names DSA layers, those layers fuse the indexer key and
  head-weight projections into the q_a/kv_a projection
  (K3FusedIndexerProjection).
- The BF16 output gate uses the fused sigmoid-mul; BF16 gated norm and
  attention residual keep one reduction layout at every batch size.
- Vision projections run on the aiter BF16 GEMM.

Other
- MoE: pure tensor parallelism routes pad rows like real rows for now; a -1
  route faults the TP a16w4 MoE path.
- The multimodal chat endpoint returns prompt_token_ids when requested.
@valarLip
valarLip force-pushed the refactor-dsa-indexer-sparse-mla branch from edf3501 to 29f449e Compare October 28, 2026 06:43
The test loads aiter_mla.py with each import replaced; the head-group and
top-k schedule modules it now imports had no stand-in, so the real ones were
loaded under the stubbed triton and failed.
fused_sigmoid_mul_maybe_quant(quant=False) called AITER's
fused_sigmoid_mul directly. Traced by Dynamo, that wrapper's shape checks
and mask choice fixed the token count to the one seen while compiling, so
a compiled Kimi-K3 broke its graph at the MLA layer and the following
layer frame failed with a ConstraintViolationError on hidden_states.

The BF16 path is now the guarded op sigmoid_mul, like the FP8 group-quant
path: its layout checks run at call time and the graph holds one opaque
node. Numerics and the out-of-place contract are unchanged.
@valarLip
valarLip merged commit d1c7bb2 into main Oct 28, 2026
38 of 41 checks passed
@valarLip
valarLip deleted the refactor-dsa-indexer-sparse-mla branch October 28, 2026 10:37
charlieguo1106 added a commit that referenced this pull request Oct 29, 2026
Since #2776, pure TP routes pad rows like real rows (mask_pad_rows=
layer.use_ep) and passes fused_moe no scheduled_tokens_gpu. The HERD
branch of select_experts kept the default mask, so its Top-(K+1)
candidates still carried -1 pad rows while fused_moe got no
num_local_tokens. aiter's HERD drop leaves -1 rows out only with that
count, so pure-TP HERD decode broke.

The HERD branch now takes the same mask_pad_rows as _select_logical:
pure TP routes its pad rows as real candidates, and a later change to
that policy applies to HERD as well.
gbyu-amd added a commit that referenced this pull request Nov 2, 2026
#2776 gated pad-row masking on EP (`mask_pad_rows=layer.use_ep`) after a
suspected "-1 route faults the TP a16w4 MoE". The fault does not reproduce:
the TP regression from #2725 was the sort's pad sentinel (real token count)
sitting inside the a16w4 gemm2's `token_id < M` bound (padded M), which piled
every pad slot into one row. Routing pad rows to -1 is skipped by the sort, so
restore it for TP and drop the switch.

Kimi-K3 TP8, aiter bd4ff1c70: bs100 decode device time 44.56 -> 42.86 ms/step
(-3.8%), c128 TPOT unchanged (102.46 -> 102.41 ms, full batches have no pad
rows), GSM8K 0.9666, DSpark GSM8K 0.9651 / acceptance 51.57%, no fault.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JiaoliangYu added a commit that referenced this pull request Nov 3, 2026
#2776 gated pad-row masking on EP (`mask_pad_rows=layer.use_ep`) after a
suspected "-1 route faults the TP a16w4 MoE". The fault does not reproduce:
the TP regression from #2725 was the sort's pad sentinel (real token count)
sitting inside the a16w4 gemm2's `token_id < M` bound (padded M), which piled
every pad slot into one row. Routing pad rows to -1 is skipped by the sort, so
restore it for TP and drop the switch.

Kimi-K3 TP8, aiter bd4ff1c70: bs100 decode device time 44.56 -> 42.86 ms/step
(-3.8%), c128 TPOT unchanged (102.46 -> 102.41 ms, full batches have no pad
rows), GSM8K 0.9666, DSpark GSM8K 0.9651 / acceptance 51.57%, no fault.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: JiaoliangYu <Jiaoliang.Yu@amd.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant