Skip to content

Add: DeepSeek V4 prefill support up to 8192 tokens - #893

Merged
zhangqi-chen merged 1 commit into
hw-native-sys:mainfrom
MaxwellF1:codex/deepseek-v4-prefill-8192
Aug 13, 2026
Merged

Add: DeepSeek V4 prefill support up to 8192 tokens#893
zhangqi-chen merged 1 commit into
hw-native-sys:mainfrom
MaxwellF1:codex/deepseek-v4-prefill-8192

Conversation

@MaxwellF1

@MaxwellF1 MaxwellF1 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor
  • prefill_fwd takes a whole request: the request-scoped tensors carry a
    dynamic token extent, and the compiled body walks them in fixed-T tiles.
    The 43-layer body still compiles for exactly T rows, and each tile binds its
    activations to its own scope. The parameter list keeps the same names,
    order, count and dtypes, and the tile count comes from the submitted extent.
  • The paged caches stay on device across tiles. KV, compressed KV, indexer KV
    and the three compressor states are InOut and addressed by slot mapping, so
    each tile writes its own pages and later tiles read the accumulated history.
    The body clears its MoE signal windows on exit. A tile past the end of a
    rank's request runs one fully masked sentinel row.
  • The MoE round declares the producer-to-waiter dependencies it relies on:
    dispatch_push -> dispatch_wait, combine -> combine_wait and
    dispatch_push -> combine_wait, and dispatch_gather is no longer
    pre-resolved. A request now runs up to 64 tiles, so the number of
    all-to-all rounds per dispatch grows with the prompt; without those edges a
    blocking waiter can hold a core group while the local notifier still has
    blocks parked in that core's pending slot, which deadlocks EP8 prefill.
  • One tile reaches 8192 tokens of history. The Indexer scores up to 2048
    compressed candidates, sorts them in full and emits the model-configured
    top-k of 512; sparse attention covers 128 sliding-window rows plus those 512,
    padded to 640.
  • The packed CSA capacity bound compares against the Indexer's top-k rather
    than the raw candidate count.

Callers size the request tensors to the prompt and submit one task:

x_hc = torch.empty(ranks, prompt_tokens, HC_MULT, D)   # was prefill_seq rows

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds configurable DeepSeek-V4 Flash prefill profiles, protocol-dependent MoE signaling, token-aware state mapping, and serving8k layer and finalization modules.

Changes

DeepSeek V4 Flash prefill

Layer / File(s) Summary
Prefill profiles and indexer contracts
models/deepseek/v4-flash/config.py, models/deepseek/v4-flash/prefill_context_profile.py, models/deepseek/v4-flash/prefill_indexer.py, models/deepseek/v4-flash/prefill_attention_csa.py, models/deepseek/v4-flash/prefill_sparse_attn.py
The PR derives capacity limits from selectable profiles. Indexer score emission, sorting, top-k validation, active-range checks, and sparse-attention dimensions use the profile values.
Protocol-dependent MoE signaling
models/deepseek/v4-flash/moe.py
MoE dispatch, payload exchange, combine, clearing, early resolution, and host signal windows support legacy_monotonic and epoch_isolated protocols.
Prefill state and signal integration
models/deepseek/v4-flash/prefill_fwd.py
Prefill kernels use expanded MoE signal tensors. Shared metadata uses token-aware block-table mappings, bounds checks, and inactive-row masking.
Serving8k layer execution
models/deepseek/v4-flash/prefill_layer_serving8k.py
The new serving8k layer path selects attention implementations, slices packed tensors, runs MoE, clears signal epochs, and builds host tensor specifications.
Serving8k finalization
models/deepseek/v4-flash/prefill_finalize_serving8k.py
The new finalization path applies the HC head and RMSNorm, coordinates distributed buffers, and computes selected LM-head logits.

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

Possibly related issues

  • hw-native-sys/pypto-serving#123: The profile-based token-capacity checks address the fixed prefill kernel chunk-size objective described by the issue.

Possibly related PRs

Suggested labels: enhancement

Poem

A rabbit tunes the prefill stream,
With epochs hopping through the dream.
Scores sort wide, then settle neat,
Packed serving layers find their beat.
HC heads bloom and logits fly—
The burrow builds for 8k high.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.53% 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
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.
Title check ✅ Passed The title clearly summarizes the main change: DeepSeek V4 prefill support for prompts up to 8192 tokens.
Description check ✅ Passed The description directly explains the changes to chunked prefill, 8192-token support, attention sizing, MoE synchronization, and serving integration.

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: 1

🧹 Nitpick comments (3)
models/deepseek/v4-flash/moe.py (1)

75-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a bound assertion for the epoch slot index.

In the epoch_isolated protocol, dispatch and combine index the signal windows at moe_epoch - 1. The window has MOE_SIGNAL_SLOTS = M.num_hidden_layers rows. Nothing here states that every caller's maximum moe_epoch fits that row count. The packed graph drives epochs up to LAST_MOE_EPOCH, derived from FWD_NUM_LAYERS. If those two layer counts ever diverge, the notify and wait offsets leave the window.

Add an import-time assertion that ties the slot count to the maximum epoch the module supports.

🤖 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/moe.py` around lines 75 - 76, Add an import-time
assertion near MOE_SIGNAL_SLOTS and the epoch configuration to ensure the
epoch-isolated slot count is at least LAST_MOE_EPOCH, preventing dispatch and
combine indexing at moe_epoch - 1 from exceeding the signal window; preserve the
existing non-isolated configuration.
models/deepseek/v4-flash/prefill_layer_serving8k.py (1)

60-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extend the post-import check to MOE_TOKENS.

Line 53 mutates config.MOE_TOKENS before importing moe. That mutation only takes effect if moe was not already imported in this process. The check on Lines 60-68 re-validates the profile fingerprint and the signal protocol after import, but it does not inspect the token count. A moe module imported earlier with the decode token count therefore passes this guard while carrying the wrong T.

Add moe_module.T (or the equivalent token constant) to the stale-import check.

🤖 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/prefill_layer_serving8k.py` around lines 60 - 68,
Extend the post-import validation condition in the prefill profile guard to also
verify that moe_module.T matches the expected prefill MOE token count from
config.MOE_TOKENS. Raise the existing RuntimeError when the imported module
carries a stale decode token count, while preserving the current fingerprint and
signal-protocol checks.
models/deepseek/v4-flash/prefill_attention_csa.py (1)

610-611: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Bound the sparse-row estimate by the profile constant.

max_sparse_rows uses IDX_TOPK, the raw model top-k. The kernel selects at most PREFILL_MAX_COMPRESSED compressed rows, which equals IDX_TOPK only for the non-legacy profiles. For the legacy profile the effective bound is smaller, so this estimate is pessimistic. The comment on Line 1014 already describes the suffix in terms of the profile cap, so the two now use different constants.

The current check still passes inside every profile's RAW_TOKEN_CAP, so this is a consistency improvement rather than a defect.

♻️ Proposed alignment with the profile bound
     max_visible_cmp = (context_len + q_len) // COMPRESS_RATIO
-    max_sparse_rows = WIN + min(max_visible_cmp, IDX_TOPK)
+    max_sparse_rows = WIN + min(max_visible_cmp, PREFILL_MAX_COMPRESSED)
🤖 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/prefill_attention_csa.py` around lines 610 - 611,
Update the max_sparse_rows calculation near max_visible_cmp to use the
profile-specific PREFILL_MAX_COMPRESSED bound instead of raw IDX_TOPK, keeping
the WIN offset and visible-row minimum logic unchanged. Align this estimate with
the profile-cap suffix calculation described near the existing Line 1014
comment.
🤖 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/prefill_layer_serving8k.py`:
- Around line 640-646: Update the L3 prefill layer flow so each call to
l3_prefill_layer uses a distinct per-layer moe_epoch, ensuring epoch_isolated
selects a separate signal row for every layer. Pass the prior layer’s epoch to
clear_moe_signal_epoch when clearing, and if epochs are reused, add the required
cross-rank barrier before clearing.

---

Nitpick comments:
In `@models/deepseek/v4-flash/moe.py`:
- Around line 75-76: Add an import-time assertion near MOE_SIGNAL_SLOTS and the
epoch configuration to ensure the epoch-isolated slot count is at least
LAST_MOE_EPOCH, preventing dispatch and combine indexing at moe_epoch - 1 from
exceeding the signal window; preserve the existing non-isolated configuration.

In `@models/deepseek/v4-flash/prefill_attention_csa.py`:
- Around line 610-611: Update the max_sparse_rows calculation near
max_visible_cmp to use the profile-specific PREFILL_MAX_COMPRESSED bound instead
of raw IDX_TOPK, keeping the WIN offset and visible-row minimum logic unchanged.
Align this estimate with the profile-cap suffix calculation described near the
existing Line 1014 comment.

In `@models/deepseek/v4-flash/prefill_layer_serving8k.py`:
- Around line 60-68: Extend the post-import validation condition in the prefill
profile guard to also verify that moe_module.T matches the expected prefill MOE
token count from config.MOE_TOKENS. Raise the existing RuntimeError when the
imported module carries a stale decode token count, while preserving the current
fingerprint and signal-protocol checks.
🪄 Autofix (Beta)

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: 4f823b03-47ee-4876-a80e-e95633555c9a

📥 Commits

Reviewing files that changed from the base of the PR and between 2388850 and 969ff08.

📒 Files selected for processing (9)
  • models/deepseek/v4-flash/config.py
  • models/deepseek/v4-flash/moe.py
  • models/deepseek/v4-flash/prefill_attention_csa.py
  • models/deepseek/v4-flash/prefill_context_profile.py
  • models/deepseek/v4-flash/prefill_finalize_serving8k.py
  • models/deepseek/v4-flash/prefill_fwd.py
  • models/deepseek/v4-flash/prefill_indexer.py
  • models/deepseek/v4-flash/prefill_layer_serving8k.py
  • models/deepseek/v4-flash/prefill_sparse_attn.py

Comment thread models/deepseek/v4-flash/prefill_layer_serving8k.py Outdated
@MaxwellF1
MaxwellF1 force-pushed the codex/deepseek-v4-prefill-8192 branch 3 times, most recently from b89e7d5 to 67f84c1 Compare August 6, 2026 08:44
- prefill_fwd takes a whole request: the request-scoped tensors carry a
  dynamic token extent, and the compiled body walks them in fixed-T tiles.
  The 43-layer body still compiles for exactly T rows, and each tile binds its
  activations to its own scope. The parameter list keeps the same names,
  order, count and dtypes, and the tile count comes from the submitted extent.
- The paged caches stay on device across tiles. KV, compressed KV, indexer KV
  and the three compressor states are InOut and addressed by slot mapping, so
  each tile writes its own pages and later tiles read the accumulated history.
  The body clears its MoE signal windows on exit. A tile past the end of a
  rank's request runs one fully masked sentinel row.
- The MoE round declares the producer-to-waiter dependencies it relies on:
  dispatch_push -> dispatch_wait, combine -> combine_wait and
  dispatch_push -> combine_wait, and dispatch_gather is no longer
  pre-resolved. A request now runs up to 64 tiles, so the number of
  all-to-all rounds per dispatch grows with the prompt; without those edges a
  blocking waiter can hold a core group while the local notifier still has
  blocks parked in that core's pending slot, which deadlocks EP8 prefill.
- One tile reaches 8192 tokens of history. The Indexer scores up to 2048
  compressed candidates, sorts them in full and emits the model-configured
  top-k of 512; sparse attention covers 128 sliding-window rows plus those 512,
  padded to 640.
- The packed CSA capacity bound compares against the Indexer's top-k rather
  than the raw candidate count.

Callers size the request tensors to the prompt and submit one task:

    x_hc = torch.empty(ranks, prompt_tokens, HC_MULT, D)   # was prefill_seq rows
@MaxwellF1
MaxwellF1 force-pushed the codex/deepseek-v4-prefill-8192 branch from 67f84c1 to 51fd55a Compare August 12, 2026 14:42
@MaxwellF1

Copy link
Copy Markdown
Contributor Author

EP8 prefill on cards 0,2,4,6,8,10,12,14, both variants interleaved on the same
cards: without the dependency edges 5/5 runs end in S1:running-stalled
(orch_error_code=8 TENSOR_WAIT_TIMEOUT, running=2 ready=0 waiting=440); with
them 3/3 pass. Raising PTO2_RING_DEP_POOL / PTO2_RING_TASK_WINDOW /
PTO2_RING_HEAP from 16384 / 16384 / 1 GiB to the 8-card CI values
131072 / 131072 / 2 GiB does not help the unfixed version (2/2 still stalled),
so this is not a ring-capacity limit. The same stall shows up intermittently at
EP4 without the edges (2 of 6 runs) and never with them.

No measurable cost. PYPTO_BENCH=1, 100 rounds / 5 warmup, both variants
interleaved on the same cards, 3 runs each, in us:

case without edges with edges delta
EP2 decode 40052 40148 +0.2%
EP4 decode 37611 37795 +0.5%
EP8 decode 38128 38182 +0.1%
EP2 prefill 90618 91033 +0.5%
EP4 prefill 93415 93541 +0.1%

Tile scaling at EP2, 128 / 256 / 512 tokens: 115627 / 223677 / 453552 us, i.e.
1.00 / 1.94 / 3.92 for 1 / 2 / 4 tiles.

Goldens on this branch rebased onto main: moe.py, prefill_layer.py at EP2,
EP4 and EP8, plus prefill_indexer.py, prefill_sparse_attn.py and
prefill_csa.py — all pass. End-to-end prefill_fwd numerics need
pypto-serving; prefill_fwd.py and decode_fwd.py are the only two entries in
this directory with golden_fn=None.

@zhangqi-chen
zhangqi-chen merged commit b7ee679 into hw-native-sys:main Aug 13, 2026
9 of 11 checks passed
zhangqi-chen added a commit that referenced this pull request Aug 18, 2026
The dspark directory forked from deepseek_v4_flash_mtp before #893 and
#953 landed, so it still carries the MoE handshake, split-K and indexer
sort behaviour those two fixed. This ports their dspark-applicable
parts; the stacked YaRN RoPE tables from #953 are not included, since
they live in the decode_fwd / prefill_fwd routing dspark does not have
yet.

- Declare dispatch_push -> dispatch_wait and drop the pre-resolve on
  dispatch_gather, so a blocking waiter cannot hold a core group while
  the local notifier still has blocks parked in that core's pending
  slot. Without those edges EP8 prefill deadlocks once a request runs
  enough all-to-all rounds, and dspark runs EP16.
- Publish the MoE combine arrivals from a combine_wait scope gated on
  the whole scatter grid and on dispatch_push, instead of folding one
  notify into each scatter block, so the wait expects moe_epoch rather
  than moe_epoch * N_LOCAL. The combine scatter, its wait and
  shared_routed lose allow_early_resolve, so the cross-rank handshake
  cannot reserve the AIV cores the scatter itself needs. moe.py is now
  identical to the mtp file apart from the EP naming and the
  16-experts-per-rank split.
- Replace the FP32 AtomicAdd accumulation in hc_pre's linear and RMS
  split-K paths with disjoint per-split partial buffers reduced in
  ascending K order, in both the fused and the separate implementation,
  and retire the zero-seed phase the atomics required. AtomicAdd sums
  the partials in task-completion order, which is neither fixed nor
  associative, so the same greedy request can diverge and then compound
  autoregressively over a long decode. The golden follows the same
  ascending order.
- Add the block_len=1024 merge stage to the prefill indexer top-k when
  INDEXER_SCORE_CAP exceeds 256. dspark sizes the cap as
  2 * T / COMPRESS_RATIO, so it is exactly 256 at the checked-in
  PREFILL_SEQ=512 and larger for any longer chunk; past 256 the 64/256
  stages leave the score prefix partially ordered and the top-k
  silently picks the wrong keys. The CP score path already carries this
  stage for its 1024-wide cap.
- Bound the packed sparse CSA rows by what the indexer actually emits,
  WIN + min(max_visible_cmp, IDX_TOPK), so build_tensor_specs stops
  refusing prompts that fit.

The hc_pre reduction costs the determinism it buys: on a2a3, 100 rounds
after 5 warmup, the 512-row prefill case goes from 167.0 to 199.5 us
minimum over two runs, while the 128-row decode case is unchanged.
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