Skip to content

feat(dflash): K=γ verify model orchestration + EAGLE ctx-accumulator contiguity fix#292

Open
Sujimoshi wants to merge 8 commits into
Avarok-Cybersecurity:mainfrom
Sujimoshi:feat/dflash-verify-orch
Open

feat(dflash): K=γ verify model orchestration + EAGLE ctx-accumulator contiguity fix#292
Sujimoshi wants to merge 8 commits into
Avarok-Cybersecurity:mainfrom
Sujimoshi:feat/dflash-verify-orch

Conversation

@Sujimoshi

Copy link
Copy Markdown
Contributor

Summary

Next cluster from the #218 split ("DFlash K=γ correctness + throughput optimizations"). Wires the SSM-layer K=γ verify kernels landed in #290 into the model's verify/decode/append orchestration layer.

Stacked on #290 (which stacks on #258) — merge order: #258, #290, then this.

Contents (crates/spark-model/src/model/trait_impl/):

  • verify_d.rs, verify_d_kgamma_layers.rs (new), verify_d_kgamma_attn_meta.rs (new) — K=γ verify step implementation using the WY16 GDN kernel and batched phase-1 conv from feat(dflash): SSM-layer K=γ verify kernels — WY16 GDN, batched phase-1 conv, opt-in F32 GDN output #290.
  • dflash_append.rs (new) — EAGLE ctx-slot append dispatchers: dflash_accept_append, dflash_eagle_accept_append, dflash_eagle_kgamma_append, plus dflash_decode_ctx_append_dispatch.
  • decode_a.rs — calls dflash_decode_ctx_append_dispatch unconditionally at the end of every decode() step (both CUDA-graph-replay and non-graph paths), fixing a real bug: the EAGLE ctx accumulator (ctx_hidden_acc) previously only advanced from the DFlash verify/propose path, so when DFlash gating turned off for a stretch (e.g. inside a <think> block) and resumed, the drafter's first post-resume propose() conditioned on stale/non-contiguous history.
  • verify_a.rs, verify_b.rs, prefill_b/batch_kernel.rs, prefill_c.rs, mod.rs — supporting wiring (new trait methods, batch-kernel/prefill hookups).
  • crates/spark-model/src/layers/dflash_head.rs / propose.rs — minimal addition: a skip_next_decode_append one-shot flag on DflashProposerState, set by the new dflash_append.rs dispatchers when they've already appended the relevant ctx row(s) before propose() runs, and consumed by propose_drafts to avoid double-appending. Scoped narrowly — the rest of dflash_head/propose.rs (the "dflash_head core" cluster: last_num_accepted, forward_block changes, etc.) is left for a later PR in the stack.

Deviations from #218 as-authored

Test plan

  • cargo check -p spark-model / cargo check --workspace
  • cargo fmt --all -- --check
  • cargo clippy --workspace --tests (crate-wide #![deny(clippy::all)])
  • file-size-cap (≤500 LoC/file)
  • GPU validation on GB10 (Qwen3.6-27B-NVFP4 + DFlash draft, T=0, greedy vs dflash):

🤖 Generated with Claude Code

Sujimoshi and others added 8 commits July 5, 2026 01:18
…4 lm_head

Split out of Avarok-Cybersecurity#218 (DFlash speculative decoding for Qwen3.6-27B-NVFP4).
Adds the model-level plumbing DFlash's draft/verify path needs: trait
default no-op hooks (prefill_phase1_verify, prefill_gdn_wy16) for future
verify-path PRs, a row-major K-row dflash_hidden_save capture buffer
(dflash_hidden_save_rows, try_dflash_capture_draft/_all) so every
accepted verify position gets its own target hidden instead of only
rows 0/1, ctx_slot_positions on DflashProposerState to track true
sequence positions across thinking-token gaps, an FP32 GDN output
sibling buffer (gdn_buf_out_f32 / ATLAS_DFLASH_VERIFY_GDN_F32 prototype)
and ssm_verify_h_tmp scratch, a dense_gemm_pipelined kernel for the
draft head lm_head, and a RoPE fix for the drafter (config.json has
rope_scaling=null; the prior YaRN-interpolated inv_freq corrupted
positions and caused 0% accept rate — replaced with the standard
1/theta^(2j/dim) formula).

Conflicts against main (which has evolved independently since Avarok-Cybersecurity#218
branched, most notably via Avarok-Cybersecurity#229's NVFP4 MMQ FFN + FlashInfer-GDN work)
were resolved by keeping main's own additions (default_loops module,
VARLEN-FLA batched methods, NVFP4 lm_head) intact and layering the
DFlash-specific fields/methods alongside them, rather than reapplying
the original patch wholesale. impl_a1.rs's misc-scratch block was
extracted into impl_a1_init::build_misc_scratch (shared with the new
gdn_buf_out_f32/ssm_verify_h_tmp allocations) to keep the constructor
readable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ot drafter's

Found while GPU-testing this PR end-to-end on GB10: the DFlash lm_head
NVFP4 fast path (w4a16_gemm) used the drafter's own (uncapped)
vocab_size as the GEMM's N, but lm_head_nvfp4 is quantized against the
target model's tokenizer-capped vocab_size, which can be smaller. On a
checkpoint where the tokenizer caps vocab from 248320 to 248077, this
read ~243 rows past the end of the packed weight/scale buffers,
producing CUDA_ERROR_ILLEGAL_ADDRESS on the first DFlash proposal and
poisoning the CUDA context for the rest of the process.

Threads the target's capped config.vocab_size through
build_model -> BlockDiffusionDraftHead::from_weights as a new
lm_head_nvfp4_vocab_size field, and fixes both the GEMM call and the
subsequent argmax loop (which shares the same row stride and had the
same latent bug, though it wouldn't itself crash — just read the wrong
row data).

Re-verified end-to-end on GB10 with the same drafter checkpoint and
target checkpoint that reproduced the original crash: 3 consecutive
requests including the one that previously triggered the CUDA-700
poisoning now all complete cleanly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Soft-load dense_gemm_bf16_pipelined via try_kernel (unused in this PR,
  shouldn't hard-fail model load)
- Make drafter RoPE config-driven (standard vs YaRN) based on
  config.json's rope_scaling field instead of hardcoding standard-only
- Guard ctx_slot_positions push against max_ctx_len to stay in lockstep
  with ctx_len clamping; downgrade expected-case "ctx_len update skipped"
  log from warn to trace
- Remove LM_HEAD_GEMM_LOGGED debug-session leftover (static + one-shot
  GEMM pointer dump)
- Gate gdn_buf_out_f32 (FP32 GDN verify buffer) allocation behind
  ATLAS_DFLASH_VERIFY_GDN_F32 env var instead of always allocating

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The two new DFlash verify-scaffolding hooks pushed transformer_layer.rs
to 535 LoC, tripping the file-size-cap CI gate. Condensed verbose
doc comments (no logic changes) back to 492 LoC.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…1 conv, opt-in F32 GDN output

Extracted from Avarok-Cybersecurity#218 (DFlash speculative decoding), stacked on Avarok-Cybersecurity#258
(model glue). SSM/GDN layer-level pieces of the K=γ verify path:

- gated_delta_rule_wy16.cu: fused K=16 WY-chunkwise GDN verify kernel
  (+ F32-output variant) writing final h_state and Hi_0..Hi_14
  intermediates in one pass; wired via prefill_gdn_wy16.
- causal_conv1d_update_prefill_inter: batched phase-1 conv that also
  writes per-token conv_state intermediates for partial-accept rollback;
  wired via prefill_phase1_verify.
- ATLAS_DFLASH_VERIFY_QKVZ_FIX (default ON): route the M=1 verify QKVZ
  projection through the same w4a16_gemv kernel serial decode uses
  (~2.29% rel_diff otherwise).
- ATLAS_DFLASH_VERIFY_OUTPROJ_FIX (default ON): per-row w4a16_gemv
  out_proj for verify-window calls (≤17 tokens, native-NVFP4 builds
  only) to keep batched verify numerically aligned with serial decode
  (~2.26% rel_diff otherwise). Normal prefill chunks keep the shared
  GEMM dispatch.
- ATLAS_DFLASH_VERIFY_GDN_F32 (opt-in): F32 GDN output in the split4 and
  wy16 verify branches, removing a BF16-truncation source.
- decode_batched sequential K∉{2,3,4,17} fallback extracted to
  conv_gdn_sequential.rs; prof!-instrumented decode GDN/norm launches.
- mod.rs: TransformerLayer impl moved to trait_transformer_impl.rs
  (500-LoC cap).

Deviations from Avarok-Cybersecurity#218 as-authored, due to main's independent evolution:
qkvz/out_proj projection ladders were replaced on main by shared
dispatch helpers (prefill_qkvz_proj / prefill_out_proj_dispatch) that
now carry the CUTLASS-NVFP4 and FP8 branches, so the verify fixes are
layered as guarded pre-branches instead of reapplying Avarok-Cybersecurity#218's inline
ladders; the out_proj fix is additionally gated to ≤17-token calls
because phase3 is now also the hot batched-prefill path. NVTX
instrumentation is left to Avarok-Cybersecurity#255. 218's trait_prefill_gdn_batched.rs and
qkvz_projection.rs extractions were dropped — main since made its own
supersets of both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…contiguity fix

Extracted from Avarok-Cybersecurity#218 (DFlash speculative decoding), stacked on Avarok-Cybersecurity#290
(SSM-layer verify kernels). Model-level orchestration of the K=γ
verify path plus a correctness fix for the drafter's context
accumulator:

- verify_d_kgamma_layers.rs / verify_d_kgamma_attn_meta.rs: per-layer
  K=γ forward loop and batched attention metadata, extracted from
  verify_d.rs to keep it under the 500-LoC cap.
- dflash_append.rs: EAGLE ctx-slot append dispatchers
  (dflash_accept_append / dflash_eagle_accept_append /
  dflash_eagle_kgamma_append) plus dflash_decode_ctx_append_dispatch,
  called unconditionally at the end of every decode() step so the
  drafter's ctx accumulator stays contiguous even across stretches
  where DFlash itself is gated off (e.g. inside a <think> block).
  Wires the accompanying `skip_next_decode_append` one-shot flag on
  `DflashProposerState` (new field) so propose()'s own internal
  decode-append doesn't double-append a row already appended by one
  of the eagle dispatchers.
- verify_a.rs / verify_b.rs / verify_d.rs: wiring for the above plus
  the K=γ dispatch path (decode_verify_graphed_kgamma_dispatch).

Deviations from Avarok-Cybersecurity#218 as-authored, due to main's independent evolution:
NVTX instrumentation (verify_step, per-layer/per-phase ranges,
d2h_argmax_readback) stripped throughout — left to Avarok-Cybersecurity#255, not yet
merged into this stack. prefill_b/batch_kernel.rs and prefill_c.rs
had their own `output_f32`/`output_f32_written` GdnPrefillBuffers
fields already (from Avarok-Cybersecurity#290's F32-GDN-output work); Avarok-Cybersecurity#218's patch tried
to add the same fields again verbatim — kept the existing (real, in
prefill_c.rs's case non-null) values and dropped the duplicates.

Test plan: cargo check/fmt/clippy/LoC-cap all green on this branch
tip. GPU validation on GB10 (Qwen3.6-27B-NVFP4 + DFlash draft, T=0)
pending — will report accept-rate/divergence vs the Avarok-Cybersecurity#290 baseline
(byte 634) and whether this fixes the previously-observed 0/16
accept collapse on the forced K=γ path (ATLAS_DFLASH_DRAFT_CAP
override), since that collapse predates this ctx-accumulator fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@SeedSource

Copy link
Copy Markdown
Collaborator

[BETA] Automated PR review — SeedSource AI pipeline

Advisory only, not a merge gate. AI-generated (internal risk-triage, Atlas serve gate, GLM-5.2 review), attributed per Atlas convention.

Triage

  • Risk: med - serve-test-required (triage advisory): True - path-heuristic (authority): True
  • Rationale: NVFP4 lm_head vocab sizing fix and GDN verify buffers added for DFlash drafter.
  • Serving-path files: crates/spark-model/src/factory/build.rs, crates/spark-model/src/layer.rs, crates/spark-model/src/layer/transformer_layer.rs, crates/spark-model/src/layers/dflash_head.rs, crates/spark-model/src/layers/dflash_head/forward_block.rs, crates/spark-model/src/layers/dflash_head/from_weights.rs

Serve gate

  • Build: 17s - Qwen3.6-35B-A3B-FP8 on GB10 node1
  • Top-k KL vs main: 0.0008 (no shift vs main) - decode delta -0.3%
  • Coherence probe: coherence=pass ping=10/10 (note: our test_coherence.py is flaky here; KL is the reliable serve signal)

GLM-5.2 code review

The diff is truncated mid-file (layers/dflash_head.rs), so the actual implementation of prefill_phase1_verify and prefill_gdn_wy16 overrides is not visible — cannot fully assess correctness of the K=γ verify path. The trait additions look structurally sound with safe Ok(false) defaults.

Concerns:

  1. target_lm_head_nvfp4_vocab_for_dflash: The comment claims config.vocab_size is the capped value, but this is asserted without evidence in the diff. If cap_vocab_size_to_tokenizer mutates config.vocab_size in place, fine; if it stores the cap elsewhere, this passes the uncapped value — the exact OOB the comment warns about. Needs verification.

  2. output_f32_written: Cell<bool>: Cell interior mutability in a Send + Sync trait object context is a smell. If GdnPrefillBuffers is shared across threads (batched multi-stream), this is a data race waiting to happen. Confirm buffers are per-stream/per-call.

  3. Doc comment removals: The # Arguments blocks were collapsed into prose. Information loss is minor but the per-argument semantics (e.g., seq_len = "starting length before these tokens") are now harder to scan.

Risk: Medium — the vocab-size fix addresses a real OOB, but the truncation hides whether the consumer side uses it correctly. Request full diff for dflash_head.rs and the SSM layer override.

Pipeline: build+serve on GB10, GLM-5.2 via NVIDIA NIM, internal risk-triage classifier. External diff treated as untrusted input. Public repo.

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.

3 participants