Skip to content

dspark: draft-pass cleanup (-10% draft time) + upstream rejection-utils consolidation with opt-in block verification - #71

Closed
voipmonitor wants to merge 5 commits into
dev/eldritch-enlightenmentfrom
fable/dspark-block-verification-20260703
Closed

dspark: draft-pass cleanup (-10% draft time) + upstream rejection-utils consolidation with opt-in block verification#71
voipmonitor wants to merge 5 commits into
dev/eldritch-enlightenmentfrom
fable/dspark-block-verification-20260703

Conversation

@voipmonitor

@voipmonitor voipmonitor commented Jul 3, 2026

Copy link
Copy Markdown

Two commits from the DSpark speedup/consolidation campaign (full context: dspark-upstream-consolidation.md §6–8).

1. 16140bcb — draft-pass cleanup + timing instrumentation (the measured win)

Three graph-captured reductions in the DSpark draft forward:

  • confidence-head skip unless VLLM_DSPARK_CONFIDENCE=1 — the serving speculator discards it; it's an lm-head-sized projection computed every draft step
  • per-step topk_idxs sharing across the three draft layers (layer-invariant; built once, shared via a module dict cleared at forward_spec entry; capture-safe)
  • persistent [b, window+block] staging buffer replacing the per-layer window gather + full-width torch.cat (one window-wide copy and two allocations per layer per step gone)

Measured with the new CUDA-event instrumentation (VLLM_DSPARK_TIMING=1, rolling prepare/graph-forward ms — the only way to time inside FULL cudagraphs; measurement-only: its per-step host sync collapses cc64): draft graph-forward 2.01 → 1.81 ms/step, prepare 0.33 → 0.31 ms, 30k coherence clean (CJK 0).

2. 5a72086 — upstream rejection-utils consolidation + opt-in block verification

Wholesale adoption of upstream's refactored rejection_sampler_utils.py (post-vllm-project#46781/vllm-project#47093/vllm-project#47383: block-verification kernels, int64-safe offsets) with our extras re-applied on top: fp64 acceptance uniforms under USE_FP64 (upstream is fp32-only) and the active-row-guarded gumbel_block_argmax signature. Adds tl_rand32 to gumbel.py, "block" to RejectionSampleMethod, and a host-side all-greedy skip (the block prep kernels cost ~5–7 % of a step and are inert at temp 0).

Honest verdict on block verification itself: synthetic kernel harness (drives rejection_sample() directly with controlled draft divergence — in rtx6kpro code/tiny-decode/synth_rejection_test.py) shows temp-0 equivalence exact, distribution preservation, and +7–47 % accepted length on Gaussian-noise drafts; but on the real DSpark draft, E2E acceptance is unchanged (12×800-token probes ×3 per config @temp 0.7: block 54.8 % mean vs standard 56.5 %, spread ±3 — DSpark's errors are bimodal, no ratio slack to pool). Left opt-in, not default. The consolidation value stands regardless: one utils file aligned with upstream + our hardening, and the synth harness as a permanent regression tool.

Measurements around this branch (DS4-Flash-DSpark TP2 A8, k=5)

cc1 197.3 / cc64 1,925 aggregate / prefill 12,972/12,875/12,067 (full B12X @f416b75). For the record, the paired Lucifer-CUTLASS comparison (cc64 +21 % — the dynamic w4a8 batch-M MoE gap) is in the wiki §8.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for a new speculative decoding rejection mode: block.
    • Improved GPU execution paths for some MoE and linear operations, with faster specialized handling for eligible workloads.
    • Added optional timing output for DSpark speculative decoding to help diagnose performance.
  • Bug Fixes

    • Reduced repeated work during DSpark attention and speculative decoding by reusing cached results where possible.
    • Improved rejection sampling behavior for all-greedy batches.

voipmonitor and others added 5 commits July 2, 2026 09:36
… FP8 kernel

The b12x MXFP8 dense GEMM wins below M~2048 on DSV4-Flash TP2 shapes
(0.56-0.96x vs DeepGEMM) but loses at prefill M (1.05-1.21x at M=4096-8192).
Keep the b12x pack for decode/small batches and additionally run the
DeepGEMM weight processing on the original checkpoint params (replaced in
place, no extra memory); token batches >= VLLM_B12X_FP8_LINEAR_DG_PREFILL_MIN_TOKENS
(default 2049) route to DeepGemmFp8BlockScaledMMKernel.apply_weights inside
the opaque custom op. Kill switch: VLLM_B12X_FP8_LINEAR_DG_PREFILL=0.

Full-B12X DS4 TP2 A8 prefill with this + the b12x tiled-quant fix:
13,556-13,684 / 13,034-13,175 / 12,037-12,151 tok/s @ 8k/64k/128k
(vs 12,468/11,971/11,133 before; Lucifer 13,442/12,622/11,716; pure
DeepGEMM-linear hybrid 13,811/13,201/12,182). Decode cc1 unchanged
(ITL 7.715ms).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two Triton kernels (FC1+SiLU staged in fp32 gmem, FC2 with router-weighted
fp32-atomic scatter) that read the b12x N256/K128 in-place-repacked FP4
expert weights and e8m0 sfb grids directly via their verified inverse bit
mappings (b12x tests/test_w4a8_rp_inverse_mapping.py), with BF16
activations (no input quantization) and fp32 accumulation.

Numerics: cos vs fp32 oracle 0.999999 (better than the w4a8 dynamic
kernel's 0.9990, which quantizes activations); cos vs the dynamic kernel
0.999. Isolated graph-replay at DS4-Flash TP2 shapes (E=256 K=4096 N=1024
topk=6): 28.7 us/layer at M=1 vs 34.8 us for the dynamic grouped kernel
(plus ~3 us of wrapper fills/copies this path also bypasses).

Enabled via VLLM_B12X_W4A8_MX_TINY_DECODE=1; engages only for
quant_mode=w4a8_mx, silu, w31 layout, M==1, shape multiples of 256;
everything else falls through to the b12x dynamic path. First E2E serve
measured 138.7 tok/s DS4 TP2 A8 decode cc1 (from 133.7 baseline); a
restart-to-restart variance question is still open (see rtx6kpro wiki).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sem='relaxed' on the fp32 atomic_add scatters read neutral in the isolated
graph-replay microbench (28.7 vs 28.8 us/layer, L2-warm identical routing)
but cost ~7% E2E decode in real serving (131.0-131.3 vs 138.7-138.8 tok/s,
DS4 TP2 A8 cc1). Default semantics restored; E2E reproducible at 138.7+.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…utils (opt-in), keep fp64 acceptance hardening

Wholesale adoption of upstream's rejection_sampler_utils.py (post-vllm-project#46781/
vllm-project#47093/vllm-project#47383: block-verification kernels, refactored logsumexp helpers,
int64-safe offsets) with our extras re-applied on top: fp64 acceptance
uniforms under USE_FP64 (upstream is fp32-only) and the active-row-guarded
gumbel_block_argmax signature. Adds tl_rand32 to gumbel.py, 'block' to
RejectionSampleMethod, and wires use_block_verification through
RejectionSampler with a host-side all-greedy skip (the block prep kernels
cost ~5-7% of a step and are inert at temp=0).

Validation (synthetic kernel harness, no server):
- temp=0: block == standard exactly (valid-prefix comparison)
- distribution preservation: TVD vs target within sampling bounds
- accepted length on Gaussian-noise drafts: +7% to +47% over standard
  across temp in {0.6,0.8,1.0} x draft quality

E2E on DS4-Flash-DSpark TP2 (12x800-token probes x3, temp 0.7):
- block 54.0/56.4/54.2%, standard 54.1/56.0/59.3% -> the real-draft gain
  is within run-to-run noise (DSpark's errors are bimodal, not the smooth
  ratio spectrum the synthetic model rewards). Left OPT-IN, not default:
  rejection_sample_method='block'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three graph-captured reductions in the DSpark draft forward, validated by
in-kernel-graph CUDA-event timing (VLLM_DSPARK_TIMING=1, prints rolling
prepare/graph-forward ms every 200 steps):
- skip the confidence head unless VLLM_DSPARK_CONFIDENCE=1 (the serving
  speculator discards it; it is an lm-head-sized projection)
- build the layer-invariant dspark topk_idxs once per draft step (shared
  across the three layers via a per-step module dict, cleared at
  forward_spec entry; capture-safe)
- replace the per-layer window gather + full-width torch.cat with a
  persistent [b, window+block] staging buffer (index_select into the
  window slice + small block copy; kills one window-wide copy and two
  allocations per layer per step)

Measured on DS4-Flash-DSpark TP2 A8 (600+ steps): draft graph-forward
2.01 -> 1.81 ms/step (-10%), prepare 0.33 -> 0.31 ms; 30k coherence clean.
Also from this campaign, measured-and-falsified for the record: k-sweep
(k=5 beats k=3/k=2: 251 vs 218 vs 192 tok/s greedy probes - verify time
scales weakly with M, so shorter blocks just lose accepted tokens) and
A16-vs-A8 MoE for DSpark (244.5 vs 241.3, tie within trajectory noise).

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

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a "block" rejection sampling method with new Triton kernels for residual-mass and cumulative log-probability computation across the rejection sampler stack. It also independently adds DeepGEMM prefill routing for b12x linear layers, a tiny-decode MoE Triton path, DSpark attention buffer reuse/caching with timing instrumentation, and a Gumbel random-value helper.

Changes

Block Verification Rejection Sampling

Layer / File(s) Summary
Config and sampler wiring
vllm/config/speculative.py, vllm/v1/worker/gpu/spec_decode/rejection_sampler.py
Adds "block" to RejectionSampleMethod; RejectionSampler derives use_block_verification and disables it for all-greedy batches before passing it to rejection_sample.
New residual-mass and logprob kernels
vllm/v1/worker/gpu/spec_decode/rejection_sampler_utils.py
Adds _compute_max_and_sumexp, _compute_global_residual_mass, _compute_global_target_argmax, _compute_global_logprobs_and_logsumexp, _compute_cumulative_log_p_kernel, _compute_local_residual_mass_kernel.
Rejection/resample acceptance logic
vllm/v1/worker/gpu/spec_decode/rejection_sampler_utils.py
Extends _rejection_kernel and _resample_kernel with block-verification flags to compute acceptance thresholds and rescale log-probabilities.
rejection_sample API and wiring
vllm/v1/worker/gpu/spec_decode/rejection_sampler_utils.py
Adds use_block_verification parameter, changes draft_logits=None handling, and wires new kernels/arguments through launches.

B12x DeepGEMM Prefill Routing

Layer / File(s) Summary
DeepGEMM prefill kernel init and routing
vllm/model_executor/kernels/linear/scaled_mm/b12x.py
Adds env-configured token threshold, initializes DeepGemmFp8BlockScaledMMKernel when supported, and routes large-batch prefill through it.

B12x Tiny-Decode MoE Path

Layer / File(s) Summary
Tiny-decode Triton kernels and buffer cache
vllm/model_executor/layers/fused_moe/b12x_tiny_decode.py
Implements FP4 decode helper, _tiny_fc1_kernel, _tiny_fc2_kernel, and a device-local buffer cache.
Entrypoint and wiring
vllm/model_executor/layers/fused_moe/b12x_tiny_decode.py, vllm/model_executor/layers/fused_moe/b12x_moe.py
Adds maybe_run_tiny_w4a8mx_moe with eligibility checks and launches, called as an early-exit fast path from _run_b12x_moe_fp4.

DSpark Attention Caching and Timing

Layer / File(s) Summary
Shared state, KV buffer reuse, confidence gating
vllm/models/deepseek_v4/nvidia/dspark.py
Reworks KV staging with a persistent buffer, caches topk_idxs across layers, gates confidence head via env flag, clears state per step.
Speculator timing instrumentation
vllm/v1/worker/gpu/spec_decode/dspark/speculator.py
Adds optional CUDA event timing around prepare/graph-forward/forward phases in propose.

Gumbel Random Value Helper

Layer / File(s) Summary
tl_rand32 helper
vllm/v1/worker/gpu/sample/gumbel.py
Adds Triton helper generating fp32 random values with optional zero-clamping.

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

Sequence Diagram(s)

sequenceDiagram
    participant RejectionSampler
    participant rejection_sample
    participant CumulativeLogPKernel as _compute_cumulative_log_p_kernel
    participant ResidualMassKernel as _compute_local_residual_mass_kernel
    participant RejectionKernel as _rejection_kernel
    participant ResampleKernel as _resample_kernel

    RejectionSampler->>RejectionSampler: derive use_block_verification (batch temps)
    RejectionSampler->>rejection_sample: call with use_block_verification
    alt use_block_verification enabled
        rejection_sample->>CumulativeLogPKernel: launch (compute cumulative_log_p)
        rejection_sample->>ResidualMassKernel: launch (compute local_residual_mass)
    end
    rejection_sample->>RejectionKernel: launch (cumulative_log_p, residual_mass, flags)
    RejectionKernel-->>rejection_sample: accepted_length, rejected_idx
    rejection_sample->>ResampleKernel: launch (cumulative_log_p, USE_BLOCK_VERIFICATION)
    ResampleKernel-->>rejection_sample: resampled tokens
    rejection_sample-->>RejectionSampler: sampled output tokens
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main change areas: DSpark draft-pass cleanup and rejection-utils consolidation with opt-in block verification.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fable/dspark-block-verification-20260703

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

🧹 Nitpick comments (2)
vllm/model_executor/kernels/linear/scaled_mm/b12x.py (1)

120-124: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cache the parsed threshold instead of re-parsing every forward call.

_b12x_dg_prefill_min_tokens() is invoked from _apply_b12x_fp8_block_scaled_linear on every forward call (per layer, per token batch). Doing an os.getenv + int() parse each time is unnecessary overhead in a hot path, and a malformed env value (int(raw) with no try/except) will raise on the very first forward call rather than at start-up.

♻️ Proposed fix using functools.lru_cache
+from functools import lru_cache
+
+
+@lru_cache(maxsize=1)
 def _b12x_dg_prefill_min_tokens() -> int:
     raw = os.getenv("VLLM_B12X_FP8_LINEAR_DG_PREFILL_MIN_TOKENS")
     if raw is None:
         return _B12X_DG_PREFILL_DEFAULT_MIN_TOKENS
     return int(raw)
🤖 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 `@vllm/model_executor/kernels/linear/scaled_mm/b12x.py` around lines 120 - 124,
The _b12x_dg_prefill_min_tokens() helper is parsing the environment variable on
every _apply_b12x_fp8_block_scaled_linear forward call, which is unnecessary in
this hot path and can fail late on a bad value. Cache the parsed result in
_b12x_dg_prefill_min_tokens() (for example with a memoized helper) so the env
lookup and int conversion happen only once, and keep the default fallback
behavior for missing values.
vllm/v1/worker/gpu/spec_decode/dspark/speculator.py (1)

525-545: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: de-duplicate the timing report blocks.

The graph-forward (Line 534, every 200 steps) and eager-forward (Line 579, every 50 steps) paths carry nearly identical accumulate-and-print logic with different intervals and labels. Extracting a small _report_timing(phase_label, interval) helper would remove the duplication and make the interval discrepancy intentional rather than incidental. Behavior is otherwise correct and fully env-gated.

Also applies to: 573-587

🤖 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 `@vllm/v1/worker/gpu/spec_decode/dspark/speculator.py` around lines 525 - 545,
The timing accumulation and stderr print logic is duplicated in the speculative
decode paths, making the graph-forward and eager-forward reports harder to
maintain. Extract the shared report/update code from the speculator forward flow
into a small helper such as _report_timing in Speculator, and have both the
run_fullgraph path and the eager-forward path call it with their respective
labels/intervals so the different cadence stays explicit.
🤖 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 `@vllm/model_executor/kernels/linear/scaled_mm/b12x.py`:
- Around line 133-141: The DeepGEMM prefill branch in b12x routes directly to
dg_kernel.apply_weights even though that path uses x.view(-1, x.shape[-1]) and
can fail on non-contiguous tensors. Update the dispatch logic in the b12x
scaled_mm path to add a contiguity check before calling dg_kernel.apply_weights,
and keep non-contiguous inputs on the existing b12x fallback path that already
normalizes them. Use the existing dg_kernel and apply_weights symbols to place
the guard in the prefill-regime branch.

In `@vllm/models/deepseek_v4/nvidia/dspark.py`:
- Line 1082: The condition in dspark.py exceeds the 88-character line limit and
should be wrapped. Reformat the long if statement in the shared/capturing check
so it spans multiple lines while preserving the same logic, using the nearby
conditional in the DeepSeek V4 NVidia code path to locate it.
- Around line 1058-1092: The tensor-debug layer-parts dump path in the dspark KV
staging logic still references cache_window even though that symbol is out of
scope after switching to the persistent all_kv buffer. Update the debug payload
in the code around the all_kv / topk_idxs construction to use the actual window
slice from all_kv (for example the first window_size columns) or remove that
field entirely so _dspark_dump_layer_parts_enabled() cannot raise NameError when
VLLM_DSPARK_TENSOR_DEBUG_LAYER_PARTS is enabled.

---

Nitpick comments:
In `@vllm/model_executor/kernels/linear/scaled_mm/b12x.py`:
- Around line 120-124: The _b12x_dg_prefill_min_tokens() helper is parsing the
environment variable on every _apply_b12x_fp8_block_scaled_linear forward call,
which is unnecessary in this hot path and can fail late on a bad value. Cache
the parsed result in _b12x_dg_prefill_min_tokens() (for example with a memoized
helper) so the env lookup and int conversion happen only once, and keep the
default fallback behavior for missing values.

In `@vllm/v1/worker/gpu/spec_decode/dspark/speculator.py`:
- Around line 525-545: The timing accumulation and stderr print logic is
duplicated in the speculative decode paths, making the graph-forward and
eager-forward reports harder to maintain. Extract the shared report/update code
from the speculator forward flow into a small helper such as _report_timing in
Speculator, and have both the run_fullgraph path and the eager-forward path call
it with their respective labels/intervals so the different cadence stays
explicit.
🪄 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: 0eeb706b-f148-4a1a-a71a-713c30837303

📥 Commits

Reviewing files that changed from the base of the PR and between 3f65c52 and 16140bc.

📒 Files selected for processing (9)
  • vllm/config/speculative.py
  • vllm/model_executor/kernels/linear/scaled_mm/b12x.py
  • vllm/model_executor/layers/fused_moe/b12x_moe.py
  • vllm/model_executor/layers/fused_moe/b12x_tiny_decode.py
  • vllm/models/deepseek_v4/nvidia/dspark.py
  • vllm/v1/worker/gpu/sample/gumbel.py
  • vllm/v1/worker/gpu/spec_decode/dspark/speculator.py
  • vllm/v1/worker/gpu/spec_decode/rejection_sampler.py
  • vllm/v1/worker/gpu/spec_decode/rejection_sampler_utils.py

Comment on lines +133 to +141
dg_kernel = getattr(layer, "b12x_dg_prefill_kernel", None)
if (
dg_kernel is not None
and x.numel() // x.shape[-1] >= _b12x_dg_prefill_min_tokens()
):
# Prefill-regime token counts run the DeepGEMM path on the
# dg-processed copy of the weights; decode/small batches stay on the
# b12x MXFP8 dense GEMM, which wins below the crossover.
return dg_kernel.apply_weights(layer, x, bias)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and nearby symbols first.
ast-grep outline vllm/model_executor/kernels/linear/scaled_mm/b12x.py --view expanded || true

printf '\n--- file excerpt ---\n'
sed -n '1,380p' vllm/model_executor/kernels/linear/scaled_mm/b12x.py

printf '\n--- search related kernels / apply_weights ---\n'
rg -n "class .*BlockScaledMMLinearKernel|def apply_weights|view\\(-1, x\\.shape\\[-1\\]\\)|torch\\.compiler\\.is_compiling|b12x_dg_prefill_kernel|dg_prefill" vllm/model_executor/kernels/linear -S

Repository: local-inference-lab/vllm

Length of output: 20708


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- BlockScaledMMLinearKernel.py ---\n'
sed -n '1,220p' vllm/model_executor/kernels/linear/scaled_mm/BlockScaledMMLinearKernel.py

printf '\n--- deep_gemm.py ---\n'
sed -n '1,240p' vllm/model_executor/kernels/linear/scaled_mm/deep_gemm.py

printf '\n--- mxfp8/b12x.py compile-time path ---\n'
sed -n '280,380p' vllm/model_executor/kernels/linear/mxfp8/b12x.py

printf '\n--- any DG-specific x normalization ---\n'
rg -n "contiguous\\(|reshape\\(|view\\(-1, x\\.shape\\[-1\\]\\)|apply_weights\\(layer, x, bias\\)|b12x_dg_prefill_kernel" vllm/model_executor/kernels/linear/scaled_mm -S

Repository: local-inference-lab/vllm

Length of output: 253


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- BlockScaledMMLinearKernel.py ---'
sed -n '1,220p' vllm/model_executor/kernels/linear/scaled_mm/BlockScaledMMLinearKernel.py

echo
echo '--- deep_gemm.py ---'
sed -n '1,240p' vllm/model_executor/kernels/linear/scaled_mm/deep_gemm.py

echo
echo '--- mxfp8/b12x.py compile-time path ---'
sed -n '280,380p' vllm/model_executor/kernels/linear/mxfp8/b12x.py

echo
echo '--- DG-specific x normalization search ---'
rg -n "contiguous\\(|reshape\\(|view\\(-1, x\\.shape\\[-1\\]\\)|apply_weights\\(layer, x, bias\\)|b12x_dg_prefill_kernel" vllm/model_executor/kernels/linear/scaled_mm -S

Repository: local-inference-lab/vllm

Length of output: 18800


Add a contiguity guard before routing to DeepGEMM prefill.
dg_kernel.apply_weights() ultimately does x.view(-1, x.shape[-1]), so this branch can crash on non-contiguous inputs while the b12x fallback already normalizes them.

Proposed fix
-        return dg_kernel.apply_weights(layer, x, bias)
+        return dg_kernel.apply_weights(layer, x.contiguous(), bias)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
dg_kernel = getattr(layer, "b12x_dg_prefill_kernel", None)
if (
dg_kernel is not None
and x.numel() // x.shape[-1] >= _b12x_dg_prefill_min_tokens()
):
# Prefill-regime token counts run the DeepGEMM path on the
# dg-processed copy of the weights; decode/small batches stay on the
# b12x MXFP8 dense GEMM, which wins below the crossover.
return dg_kernel.apply_weights(layer, x, bias)
dg_kernel = getattr(layer, "b12x_dg_prefill_kernel", None)
if (
dg_kernel is not None
and x.numel() // x.shape[-1] >= _b12x_dg_prefill_min_tokens()
):
# Prefill-regime token counts run the DeepGEMM path on the
# dg-processed copy of the weights; decode/small batches stay on the
# b12x MXFP8 dense GEMM, which wins below the crossover.
return dg_kernel.apply_weights(layer, x.contiguous(), bias)
🤖 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 `@vllm/model_executor/kernels/linear/scaled_mm/b12x.py` around lines 133 - 141,
The DeepGEMM prefill branch in b12x routes directly to dg_kernel.apply_weights
even though that path uses x.view(-1, x.shape[-1]) and can fail on
non-contiguous tensors. Update the dispatch logic in the b12x scaled_mm path to
add a contiguity check before calling dg_kernel.apply_weights, and keep
non-contiguous inputs on the existing b12x fallback path that already normalizes
them. Use the existing dg_kernel and apply_weights symbols to place the guard in
the prefill-regime branch.

Comment on lines +1058 to +1092
# Persistent [b, window+block] staging buffer: one gather for the
# window + a small copy for the block KV, replacing the per-layer
# gather + full-width torch.cat (saves one window-wide copy and two
# allocations per layer per step).
if (
getattr(self, "_dspark_allkv_buf", None) is None
or self._dspark_allkv_buf.shape[0] < batch_size
):
self._dspark_allkv_buf = torch.empty(
batch_size,
window_size + block_size,
kv.shape[-1],
dtype=kv.dtype,
device=kv.device,
)
all_kv = self._dspark_allkv_buf[:batch_size]
torch.index_select(
self.dspark_kv_cache, 0, cache_rows, out=all_kv[:, :window_size]
)
all_kv[:, window_size:].copy_(kv)
# topk_idxs is layer-invariant: computed once per draft step by the
# first dspark layer and shared via the shared-state dict.
shared = _dspark_layer_shared_state
step_key = (positions.data_ptr(), batch_size, block_size)
if shared.get("key") != step_key or shared.get("capturing") is not torch.cuda.is_current_stream_capturing():
shared["key"] = step_key
shared["capturing"] = torch.cuda.is_current_stream_capturing()
shared["topk"] = _build_dspark_topk_idxs(
window_size=window_size,
batch_size=batch_size,
block_size=block_size,
positions=positions.view(batch_size),
device=x.device,
)
topk_idxs = shared["topk"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="vllm/models/deepseek_v4/nvidia/dspark.py"

echo "== context around lines 1035-1125 =="
sed -n '1035,1125p' "$file" | cat -n

echo
echo "== all references to cache_window in file =="
rg -n "\bcache_window\b" "$file"

echo
echo "== debug dump helper references =="
rg -n "_dspark_dump_layer_parts_enabled|tensor_debug|layer_parts" "$file"

Repository: local-inference-lab/vllm

Length of output: 7167


cache_window still breaks the tensor-debug dump path.
_dspark_dump_layer_parts_enabled() can still reach "cache_window": cache_window, but that name is no longer in scope. Enabling VLLM_DSPARK_TENSOR_DEBUG_LAYER_PARTS=1 will raise NameError; point it at all_kv[:, :window_size] or remove the field.

🤖 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 `@vllm/models/deepseek_v4/nvidia/dspark.py` around lines 1058 - 1092, The
tensor-debug layer-parts dump path in the dspark KV staging logic still
references cache_window even though that symbol is out of scope after switching
to the persistent all_kv buffer. Update the debug payload in the code around the
all_kv / topk_idxs construction to use the actual window slice from all_kv (for
example the first window_size columns) or remove that field entirely so
_dspark_dump_layer_parts_enabled() cannot raise NameError when
VLLM_DSPARK_TENSOR_DEBUG_LAYER_PARTS is enabled.

# first dspark layer and shared via the shared-state dict.
shared = _dspark_layer_shared_state
step_key = (positions.data_ptr(), batch_size, block_size)
if shared.get("key") != step_key or shared.get("capturing") is not torch.cuda.is_current_stream_capturing():

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Line exceeds the 88-character limit.

This condition is ~116 chars; wrap it. As per coding guidelines, "Python code must follow an 88-character line length limit."

✂️ Proposed wrap
-        if shared.get("key") != step_key or shared.get("capturing") is not torch.cuda.is_current_stream_capturing():
+        capturing = torch.cuda.is_current_stream_capturing()
+        if shared.get("key") != step_key or shared.get("capturing") is not capturing:
             shared["key"] = step_key
-            shared["capturing"] = torch.cuda.is_current_stream_capturing()
+            shared["capturing"] = capturing
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if shared.get("key") != step_key or shared.get("capturing") is not torch.cuda.is_current_stream_capturing():
capturing = torch.cuda.is_current_stream_capturing()
if shared.get("key") != step_key or shared.get("capturing") is not capturing:
shared["key"] = step_key
shared["capturing"] = capturing
🤖 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 `@vllm/models/deepseek_v4/nvidia/dspark.py` at line 1082, The condition in
dspark.py exceeds the 88-character line limit and should be wrapped. Reformat
the long if statement in the shared/capturing check so it spans multiple lines
while preserving the same logic, using the nearby conditional in the DeepSeek V4
NVidia code path to locate it.

Source: Coding guidelines

@voipmonitor

Copy link
Copy Markdown
Author

Closing this old dev/eldritch-enlightenment PR as superseded by the current Fathomless Firmament DSpark stack.

The intended changes were audited before closing:

  • Block verification and its int64 fixes are already present upstream in the FF base (#46781 / #47383), with additional sampling hardening in the FF branch. The DSpark E2E measurements in this PR showed no acceptance benefit, so block remains opt-in and we do not need this older wholesale port.
  • The confidence-head skip is already handled structurally in the FF implementation: confidence is computed only when capacity scheduling uses it.
  • The old shared topk_idxs and persistent all_kv staging changes target the removed _dspark_attention implementation. FF uses paged sparse-SWA metadata/cache groups, so these hunks cannot be carried over directly.
  • VLLM_DSPARK_TIMING synchronizes the host every measured step and is diagnostic only, not production code.
  • This stacked PR also contains inherited perf(linear): route prefill-sized batches to DeepGEMM inside the b12x FP8 linear kernel #69/feat(moe): rp-native Triton tiny-decode path for b12x w4a8_mx (M=1) — DS4 A8 decode 133.7 → 138.7 tok/s #70 commits; they are intentionally not being treated as part of the DSpark cleanup.

No production change from this PR is missing from the new FF submission. Any optimization for the current paged sparse-SWA implementation should be developed and measured separately against dev/fathomless-firmament in the new non-draft PR.

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