Skip to content

Add: dynamic decode batch across the DeepSeek-V4 Flash dspark tree - #910

Merged
zhangqi-chen merged 5 commits into
hw-native-sys:mainfrom
zhangqi-chen:feat/dspark-decode-dynamic-batch
Aug 7, 2026
Merged

zhangqi-chen merged 5 commits into
hw-native-sys:mainfrom
zhangqi-chen:feat/dspark-decode-dynamic-batch

Conversation

@zhangqi-chen

Copy link
Copy Markdown
Collaborator
  • Serve any decode batch from one compiled program across the dspark
    decode tree, from rope_interleave up to decode_swa / decode_hca /
    decode_csa. Each kernel derives its token count with pl.tensor.dim and
    drives every loop bound from it; each test entry gains -b/--batch and
    its fixture is parameterized.

  • Size the decode kernels for one card rather than one DP rank.
    DECODE_BATCH is the per-DP-rank request count and TP is not
    implemented, so a single card carries the whole rank; the fourteen
    non-MoE decode kernels and the shared decode/prefill leaves now bound
    their batch axis with DECODE_BATCH // TP. expert_routed keeps
    DECODE_BATCH. The compile-time bound is 16, so the runtime contract is
    B in {4, 8, 12, 16}, i.e. T = B * S in {8, 16, 24, 32}.

  • Ceil the cube-tile grids that previously floored. qkv_proj_rope and
    decode_compressor_ratio128 computed their block count as t // TILE, so
    a token count that was not a whole multiple of the 16-row tile left the
    last block unprojected.

  • Row-block every matmul whose M tile was the static token count.
    decode_indexer and the proj_a of all three sparse_attn kernels asked
    for a tile taller than the source tensor, or taller than the written
    region of a T_PAD-strided scratch, and produced zeros. Those matmuls
    fan ceil(t / 16) blocks at the 16-row cube floor.

  • Take token-major flat tensors in the compressors and the indexer. A
    rank-increasing pl.reshape feeding an @pl.jit.inline parameter fails
    metadata inference once the axis is dynamic, so the token axis stays
    flat end to end and b_dim comes from a per-request block table. This
    also matches the order vLLM hands over slot mappings.

  • Make decode_indexer's score parameter pl.InOut. It is written by one
    scope and read back by the topk scope, and pypto emits add_output for
    a pl.Out parameter, so the orchestration missed the read-after-write
    edge.

Deployment note: the kernels do not support a decode batch below 4, so
the 4-token ACL-graph capture bucket must be pinned out with
cudagraph_capture_sizes. On that bucket the tile grids run zero blocks
and return uninitialized memory rather than failing.

Serve any vLLM ACL-graph decode bucket from one compiled program. The A3
single-node deployment (TP4 DP4, --max-num-seqs 64, MTP=1, FULL_DECODE_ONLY,
FLASHCOMM1) captures token counts [4, 8, 16, 24, ..., 128], so the runtime
contract is B a multiple of 4 up to 64, i.e. T = B*S a multiple of 8 up to 128.
DECODE_BATCH stays the compile-time upper bound that sizes all scratch.

Every kernel from rope_interleave up to decode_swa / decode_hca / decode_csa
now derives its token count with pl.tensor.dim and drives loop bounds from it;
each test entry gains -b/--batch and its fixture is parameterized.

Three classes of latent bug surfaced, all invisible at B = 64:

- Floor-divided cube-tile grids dropped the tail token block. qkv_proj_rope and
  decode_compressor_ratio128 computed the block count as t // TILE, so at
  T = 40 the last 8 tokens were never projected. Now ceil.

- A matmul whose M tile is the static token count is only valid while
  bs == T_PAD. decode_indexer, and the proj_a of all three sparse_attn
  kernels, asked for a tile taller than the source tensor (or than the written
  region of a T_PAD-strided scratch) and silently produced zeros. Those
  matmuls are row-blocked at the 16-row cube floor.

- A rank-increasing pl.reshape feeding an @pl.jit.inline parameter fails
  metadata inference once the axis is dynamic. The compressors and the indexer
  therefore take token-major flat tensors, matching how vLLM hands over slot
  mappings; b_dim now comes from a per-request block table.

Also fixes an orchestration race the batch work exposed: decode_indexer's
`score` is written by one scope and read back by topk, but pypto emits
add_output for a pl.Out parameter, so the RAW edge was missing. It is pl.InOut
now.

Validated on a2a3 at B = 64 and at least one value of the B = 4 (mod 8) class
(12/20/32/60), which is the class that exercises the partial 16-row tile.

DEPLOYMENT NOTE: the T = 4 capture bucket must be pinned out, e.g.
--compilation-config '{"cudagraph_mode":"FULL_DECODE_ONLY",
"cudagraph_capture_sizes":[8,16,...,128]}'. Without it a 1-2 request batch
lands on a bucket the kernels do not support and returns uninitialized memory
rather than failing.
Comments state what, not why: drop the "divides by a compile-time constant"
rationale from the seven spmd index decodes, collapse the two-line pad-zero and
row-block notes to one line each, and reword the flat-ABI notes to name the
constraint instead of retelling the diagnosis.

Header groups follow the model-config / tiling convention: decode_indexer had
the dynamic-variable group under the `# model config` label,
decode_sparse_attn_swa ended up with two `# Dynamic shape variables.` labels,
and rope_interleave had no group labels at all.

Split the one over-long statement introduced by the pass: the hca rope_cs
gather stores are now one op per line.

Also drop seven pre-existing dead locals the pass surfaced -- five stale reads
in the goldens plus a duplicated pl.tensor.dim in the swa entry. The two
`as qk_tid` captures stay: pl.spmd only accepts `deps=` in the capture form, so
dropping the name fails to compile. They are renamed `_qk_tid` instead.
DECODE_BATCH is the per-DP-rank request count. TP is not implemented, so a
single card carries the whole rank and every decode kernel sized its batch
axis 4x too large. Use DECODE_BATCH // TP for the compile-time bound in the
fourteen non-MoE decode kernels; expert_routed keeps DECODE_BATCH.

decode_mtp_verify took T from DECODE_TOKENS rather than B * S, which no longer
agreed with the per-card B; it derives T from B now and the assert it guarded
is redundant.

B_MAX is 16, so the dynamic batch contract narrows to B in {4, 8, 12, 16}
(T in {8, 16, 24, 32}).
rmsnorm, hc_pre, hc_post, qkv_proj_rope and mtp_projection serve both paths
from one T_DYN kernel, so their decode test point and their decode-side tile
asserts still read the per-DP-rank DECODE_BATCH. Match the fused kernels and
use DECODE_BATCH // TP; the prefill mode is untouched.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 96c5b97b-0c9c-436a-80a8-46b1dedf2669

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The DeepSeek V4 decode paths now use tensor-parallel-adjusted batch capacity, flattened token-major tensors, runtime dynamic dimensions, padded token workspaces, and configurable batch fixtures. Decode CLIs validate runtime batch values.

Changes

DeepSeek V4 dynamic decode

Layer / File(s) Summary
Batch sizing and shared interfaces
models/deepseek_v4_flash_dspark/config.py, models/deepseek_v4_flash_dspark/{hc_head,hc_pre,hc_post,mtp_projection,qkv_proj_rope,rmsnorm,rope_interleave}.py
Decode batch sizing uses DECODE_BATCH // TP. Shared RoPE handling supports dynamic rows with four-row processing.
Indexer and compressor paths
models/deepseek_v4_flash_dspark/{decode_compressor_ratio4,decode_compressor_ratio128,decode_indexer,decode_indexer_compressor,decode_csa,decode_hca}.py
Indexer and compressor interfaces use flattened dynamic token dimensions. State, cache, projection, normalization, fixture, golden-reference, and CLI logic use runtime batch sizes.
Sparse attention paths
models/deepseek_v4_flash_dspark/{decode_sparse_attn_csa,decode_sparse_attn_hca,decode_sparse_attn_swa}.py, models/deepseek_v4_flash_dspark/decode_swa.py
Sparse attention uses runtime token counts, padded workspaces, blocked RoPE and bias generation, row-blocked projections, dynamic output shapes, and batch-aware fixtures.
Metadata and verification
models/deepseek_v4_flash_dspark/{decode_metadata,decode_mtp_verify}.py
Metadata and MTP verification derive per-rank batch and token counts from tensor parallelism.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

Suggested labels: enhancement

Poem

A rabbit flattens tokens in flight,
TP keeps each rank’s batch size right.
Dynamic rows hop through every cache,
RoPE blocks bloom in tiled dispatch.
“Four at a time!” the rabbit sings,
While runtime shapes grow useful wings.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: dynamic decode-batch support across the DeepSeek-V4 Flash dspark tree.
Description check ✅ Passed The description directly explains dynamic batch support, tensor-parallel sizing, tiling fixes, flattened tensors, and test changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
models/deepseek_v4_flash_dspark/decode_indexer.py (1)

748-761: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The --batch default is not checked against its own validation rule. All seven harnesses default -b/--batch to B = DECODE_BATCH // TP and then reject any value below 4 or not a multiple of 4. If a configuration yields B < 4 or B % 4 != 0, running the harness with no -b flag fails with a parser error. The validation logic and the help text are also copied verbatim across all seven files.

  • models/deepseek_v4_flash_dspark/decode_indexer.py#L748-L761: extract the argument definition and the range check into one shared helper in models/deepseek_v4_flash_dspark/utils.py, and assert at import time that B satisfies the rule.
  • models/deepseek_v4_flash_dspark/decode_indexer_compressor.py#L642-L658: replace the local definition and check with the shared helper.
  • models/deepseek_v4_flash_dspark/decode_csa.py#L920-L939: replace the local definition and check with the shared helper.
  • models/deepseek_v4_flash_dspark/decode_hca.py#L691-L708: replace the local definition and check with the shared helper.
  • models/deepseek_v4_flash_dspark/decode_sparse_attn_csa.py#L807-L827: replace the local definition and check with the shared helper.
  • models/deepseek_v4_flash_dspark/decode_sparse_attn_hca.py#L806-L827: replace the local definition and check with the shared helper.
  • models/deepseek_v4_flash_dspark/decode_sparse_attn_swa.py#L684-L701: replace the local definition and check with the shared helper.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@models/deepseek_v4_flash_dspark/decode_indexer.py` around lines 748 - 761,
The --batch default must satisfy the same validation rule applied to user input.
In models/deepseek_v4_flash_dspark/utils.py, add a shared helper that defines
the -b/--batch argument and performs the range and multiple-of-4 check, and
assert at import time that B is at least 4 and divisible by 4. Replace the local
argument definition and validation in decode_indexer.py (748-761),
decode_indexer_compressor.py (642-658), decode_csa.py (920-939), decode_hca.py
(691-708), decode_sparse_attn_csa.py (807-827), decode_sparse_attn_hca.py
(806-827), and decode_sparse_attn_swa.py (684-701) with this helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@models/deepseek_v4_flash_dspark/config.py`:
- Line 242: Clarify the DECODE_BATCH contract in the configuration and its
consumers: either define separate total and per-card values, or update local
decode-kernel calculations to derive the intended per-card batch consistently,
including the MoE path using RECV_MAX. Ensure comments and all usages agree on
whether DECODE_BATCH is DP-sized or local per-card.

In `@models/deepseek_v4_flash_dspark/decode_compressor_ratio128.py`:
- Around line 109-110: Update the cos and sin tensor shape annotations in
compressor_ratio128() to use the static B capacity instead of B_DYN, matching
decode_compressor_ratio4.py and the buffers written by rope_interleave().

In `@models/deepseek_v4_flash_dspark/decode_hca.py`:
- Around line 149-152: Update the block-count calculations near t_dim, b_dim,
and the topk_blocks/wb_blocks assignments to use ceiling division for both
HCA_TOPK_TOKEN_TILE and HCA_WB_TOKEN_TILE. Preserve the existing topk_t < t_dim
bounds guard and add the equivalent write_t < t_dim guard in the writeback loop
so tail tokens are initialized and written safely.

In `@models/deepseek_v4_flash_dspark/decode_indexer.py`:
- Around line 389-390: Update the cos and sin parameter annotations in the
relevant decode-indexer function to use B_DYN instead of B, matching their
bind_dynamic declarations and the batch-shaped fixture output. Keep the existing
second dimension ROPE_HEAD_DIM // 2 and dynamic bindings unchanged.

In `@models/deepseek_v4_flash_dspark/decode_sparse_attn_hca.py`:
- Around line 142-156: Token-tile grids currently floor-divide t_dim and drop
tail tokens; update
models/deepseek_v4_flash_dspark/decode_sparse_attn_hca.py:142-156 to use ceil
grids for valid_blocks and act_t_blks and guard per-token writes. Apply the same
fix in models/deepseek_v4_flash_dspark/decode_hca.py:149-152 for topk_blocks and
wb_blocks, adding the write_t < t_dim writeback guard; in
models/deepseek_v4_flash_dspark/decode_csa.py:169-171 for wb_blocks and its
writeback loop at line 240; and in
models/deepseek_v4_flash_dspark/decode_sparse_attn_swa.py:394-398 for act_t_blks
and proj_b_act stores. Preserve existing topk guards and ensure sparse_bias rows
and tail-token activations are initialized and written.

In `@models/deepseek_v4_flash_dspark/qkv_proj_rope.py`:
- Line 119: Update workspace allocations around t_matmul in
models/deepseek_v4_flash_dspark/qkv_proj_rope.py: use the static decode capacity
for create_tensor shapes instead of runtime-derived t_dim or t_matmul, while
restricting computation through valid_shape or active-token loop bounds. Apply
the same static-capacity workspace change to the affected create_tensor calls in
models/deepseek_v4_flash_dspark/decode_swa.py lines 113-140.

---

Nitpick comments:
In `@models/deepseek_v4_flash_dspark/decode_indexer.py`:
- Around line 748-761: The --batch default must satisfy the same validation rule
applied to user input. In models/deepseek_v4_flash_dspark/utils.py, add a shared
helper that defines the -b/--batch argument and performs the range and
multiple-of-4 check, and assert at import time that B is at least 4 and
divisible by 4. Replace the local argument definition and validation in
decode_indexer.py (748-761), decode_indexer_compressor.py (642-658),
decode_csa.py (920-939), decode_hca.py (691-708), decode_sparse_attn_csa.py
(807-827), decode_sparse_attn_hca.py (806-827), and decode_sparse_attn_swa.py
(684-701) with this helper.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 573a1996-ed67-4d77-ae06-ccc865808012

📥 Commits

Reviewing files that changed from the base of the PR and between 222c9cf and d40947b.

📒 Files selected for processing (20)
  • models/deepseek_v4_flash_dspark/config.py
  • models/deepseek_v4_flash_dspark/decode_compressor_ratio128.py
  • models/deepseek_v4_flash_dspark/decode_compressor_ratio4.py
  • models/deepseek_v4_flash_dspark/decode_csa.py
  • models/deepseek_v4_flash_dspark/decode_hca.py
  • models/deepseek_v4_flash_dspark/decode_indexer.py
  • models/deepseek_v4_flash_dspark/decode_indexer_compressor.py
  • models/deepseek_v4_flash_dspark/decode_metadata.py
  • models/deepseek_v4_flash_dspark/decode_mtp_verify.py
  • models/deepseek_v4_flash_dspark/decode_sparse_attn_csa.py
  • models/deepseek_v4_flash_dspark/decode_sparse_attn_hca.py
  • models/deepseek_v4_flash_dspark/decode_sparse_attn_swa.py
  • models/deepseek_v4_flash_dspark/decode_swa.py
  • models/deepseek_v4_flash_dspark/hc_head.py
  • models/deepseek_v4_flash_dspark/hc_post.py
  • models/deepseek_v4_flash_dspark/hc_pre.py
  • models/deepseek_v4_flash_dspark/mtp_projection.py
  • models/deepseek_v4_flash_dspark/qkv_proj_rope.py
  • models/deepseek_v4_flash_dspark/rmsnorm.py
  • models/deepseek_v4_flash_dspark/rope_interleave.py

Comment thread models/deepseek_v4_flash_dspark/config.py
Comment thread models/deepseek_v4_flash_dspark/decode_compressor_ratio128.py Outdated
Comment thread models/deepseek_v4_flash_dspark/decode_hca.py
Comment thread models/deepseek_v4_flash_dspark/decode_indexer.py Outdated
Comment thread models/deepseek_v4_flash_dspark/decode_sparse_attn_hca.py
Comment thread models/deepseek_v4_flash_dspark/qkv_proj_rope.py
compressor_ratio128 declared its interleaved cos/sin as [B_DYN, ...], but
those buffers are the B_MAX-sized scratch rope_interleave writes, which is
how compressor_ratio4 and indexer_compressor already declare them.

indexer_test declared its half-width cos/sin with the static B while binding
dimension 0 to B_DYN, so the annotation disagreed with both the bind and the
fixture.
@zhangqi-chen
zhangqi-chen merged commit 008a709 into hw-native-sys:main Aug 7, 2026
19 of 34 checks passed
@zhangqi-chen
zhangqi-chen deleted the feat/dspark-decode-dynamic-batch branch August 7, 2026 01:01
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.

1 participant