Skip to content

[None][perf] DFlash performance optimizations - #13995

Closed
ziyixiong-nv wants to merge 7 commits into
NVIDIA:mainfrom
ziyixiong-nv:dev-fxiong-dflash-perf-opt
Closed

[None][perf] DFlash performance optimizations#13995
ziyixiong-nv wants to merge 7 commits into
NVIDIA:mainfrom
ziyixiong-nv:dev-fxiong-dflash-perf-opt

Conversation

@ziyixiong-nv

@ziyixiong-nv ziyixiong-nv commented May 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Performance optimizations for the DFlash speculative-decoding worker on the PyTorch backend. The headline change is dropping the K-1 mask-filler tokens that DFlash was forwarding through every target layer; the rest are supporting fusions that close the remaining gap on the draft side.

Qwen3-8B on B200, TP=4, MTBench:

Config DFlash (this branch) Eagle3
bs=64 mdl=3 5064 TPS/gpu 4363 (+16.1%)
bs=64 mdl=7 4756 TPS/gpu 4548 (+4.6%)

Acceptance length is byte-for-byte preserved vs. the pre-branch baseline.

Commits

  • d517eed DFlash: drop K-1 filler tokens from target gen input — reduce tokens_per_gen_step from 2K to K+1, remove the paired slicing/padding. +14.5% TPS/gpu at bs=64 mdl=3 (main large-batch win).
  • 59ea073 DFlash: fuse cross-layer k_norm and avoid mask-mul alloc.
  • f609129 DFlash: use fused qk-norm+rope on non-YaRN draft.
  • 785cdee DFlash: fuse query-input build in pure Torch + fused embedding lookup.
  • 5c45f60 DFlash: match RoPE between context and query for YaRN (bug fix).
  • cbdb9f4 DFlash: migrate draft path to per-layer cached K/V pool.
  • 4d73942 DFlash: Qwen3.5 fixes (cherry-pick).

Test plan

  • tests/unittest/_torch/speculative/hw_agnostic/test_dflash.py::test_dflash_qwen3_8b (both overlap-scheduler variants) — pass
  • tests/unittest/_torch/speculative/hw_agnostic/test_dflash.py::test_dflash_qwen3_5_4b (both variants) — pass
  • tests/integration/defs/accuracy/test_llm_api_pytorch.py::TestQwen3_8B::test_dflash (GSM8K) — pass
  • /bot run

Summary by CodeRabbit

  • New Features

    • Added comprehensive test coverage for DFlash speculative decoding on Qwen3 and Qwen3.5 models with GSM8K evaluation
  • Improvements

    • Optimized DFlash context K/V precomputation and caching for improved memory efficiency
    • Refined draft token generation strategy and position ID handling for speculative decoding

Review Change Stack

Cherry-pick of NVIDIA#13782:
- Fix mrope position_ids in DFlash
- Fix GDN verify buffer size for DFlash
- Fix GDN/Mamba2 state updates in DFlash
- Add tests

Original-author: Anurag Mukkara <134339030+amukkara@users.noreply.github.com>
Signed-off-by: ziyixiong-nv <219238287+ziyixiong-nv@users.noreply.github.com>
Replace per-layer context K/V recomputation on the padded target_hidden
with a persistent per-layer cache, and drop the legacy recompute path
entirely.

- Build _ctx_k_buf / _ctx_v_buf of shape
  [max_batch, L, max_ctx+block_size, nkv, hd] at worker init. Each
  decode iter runs one fused GEMM (stacked across drafter layers) on
  the newly accepted K+1 tokens and scatters into the per-slot buffer;
  subsequent iters read the cached tensors directly via
  flash_attn_with_kvcache's cache_batch_idx — no gather, no re-project.
- Use fused q_norm + k_norm + RoPE per layer in dflash_forward via
  torch.ops.trtllm.fused_qk_norm_rope.
- Use fused cross-layer RoPE in precompute_context_kv via
  flashinfer_apply_rope_with_cos_sin_cache_inplace.

Drop the old fallback that recomputed context K/V per-layer from
target_hidden each step: every DFlash checkpoint has
_build_fused_kv_buffers, so the fast path is always taken. Removed
dflash_forward's target_hidden / context_positions params, the
use_ctx_cache branching, the per-layer recomputation, and the
DFlashForCausalLM._kv_buf_k / _kv_buf_v / _cache_seqlens /
_block_offsets plus DFlashWorker._ctx_buf / _ctx_pos_buf / _proj_dim
attributes that only served the fallback.

Qwen3-8B, 8xB200, TP=8, MTBench, mdl=3, tok/s per user:
         DFlash(new)  DFlash(old)  Eagle3(best)
  bs=1       977.2       701.1        909.4
  bs=2       881.1       661.3        799.5
  bs=4       801.9       608.8        748.3
  bs=8       729.7       529.3        691.4
  bs=16      592.3       317.5        564.4
  bs=32      495.2       224.0        482.3
  bs=64      452.5       156.0        438.6

Memory: +160 MB per TP rank for the ctx K/V buffers on Qwen3-8B.

Signed-off-by: ziyixiong-nv <219238287+ziyixiong-nv@users.noreply.github.com>
fused_qk_norm_rope in dflash_forward rebuilt YaRN frequencies from
the HuggingFace formulation (honors truncate=false), while
precompute_context_kv rotated context K through the DeepSeek-V2
style cache (always truncated).

On gpt-oss-120b (truncate=false) the two paths produced different
cos/sin for the same position, so context K and query Q / noise K
were rotated inconsistently. Attention scores decayed to noise and
AR collapsed from 0.35 to 0.012.

Replace fused_qk_norm_rope with explicit q_norm + k_norm followed
by flashinfer_apply_rope_with_cos_sin_cache_inplace reading the
same cache as precompute_context_kv. Both paths now see identical
cos/sin for any rope scaling.

Qwen3-8B (no rope_scaling) unaffected.

gpt-oss-120b recovers and beats Eagle3:
  mtbench  bs=1  mdl=3:  tps 357 -> 975,  AR 0.012 -> 0.695  (Eagle3: 822, 0.437)
  mtbench  bs=1  mdl=5:  tps     -> 1174, AR       -> 0.582
  codeEdit bs=1  mdl=3:  tps     -> 915,  AR       -> 0.636  (Eagle3: 888, 0.470)
  codeEdit bs=16 mdl=3:  tps     -> 422,  AR       -> 0.646  (Eagle3: 441, 0.390)

Signed-off-by: ziyixiong-nv <219238287+ziyixiong-nv@users.noreply.github.com>
…edding lookup

Replaces the ~10 scattered small PyTorch ops (expand + clone + gather +
scatter + arange broadcasts) that ran sequentially on the host per draft
step with:

1. A few broadcast-add expressions for query_positions / ctx_positions.
2. A single embed_tokens call over [bonus..., mask_token_id], cutting
   the per-step vocab-sharded embedding gather+all-reduce from
   O(num_gens * block_size) to O(num_gens + 1) tokens.

Routes through embed_tokens.forward (not .weight[...]) so TP-sharded
vocabularies are handled correctly (each rank only owns a slice of the
embedding table and must all-reduce).

Verified on Qwen3-8B TP=8 B200 / MTBench, bs=64 mdl=8: AL and output
length bit-identical to the old pure-Torch baseline; +1.4% tps/gpu,
+2.4% tps/user.

Signed-off-by: ziyixiong-nv <219238287+ziyixiong-nv@users.noreply.github.com>
The YaRN RoPE fix disabled the fused q_norm + k_norm + RoPE kernel.
For drafters with no rope_scaling and partial_rotary_factor=1.0 the
fused kernel and the flashinfer fallback produce identical
frequencies, so the fused kernel is safe and saves two launches per
layer.

Guard picks the fused kernel only when rope_params.scale_type is
None (or RotaryScalingType.none) and partial_rotary_factor == 1.0.
YaRN / long-rope / partial-rotary drafters keep the safe fallback
path.

AR unchanged:
  Qwen3-8B (no rope_scaling):  bs=1 mdl=3  AR=0.702, tps ~960-975
  gpt-oss-120b (YaRN):         bs=1 mdl=3  AR=0.695, tps ~981

Signed-off-by: ziyixiong-nv <219238287+ziyixiong-nv@users.noreply.github.com>
Two AR-preserving cleanups that target the large-batch draft path.

- precompute_context_kv: replace the Python for-loop of L per-layer
  k_norm calls with a single F.rms_norm over [N, L, nkv, hd] followed
  by a broadcast multiply with the stacked per-layer weights. Stacks
  k_norm weights once in _build_fused_kv_buffers; asserts all layers
  share variance_epsilon (reports the observed values on mismatch so
  the error is self-diagnosable).

- prepare_1st_drafter_inputs: turn 'k_new = k_new * mask_bc' into
  'k_new.mul_(mask_bc)' (and same for v_new). Avoids allocating a
  new [num_gens*(K+1), L, nkv, hd] tensor each step.

Qwen3-8B, 8xB200, TP=8, MTBench:
  bs=1  mdl=10: tps 1571 -> 1582, AR 0.4414 -> 0.4416
  bs=64 mdl=3 : tps ~452 -> ~442 (2 runs: 434, 450; within run-to-run
                noise), AR 0.709 -> 0.711

Neither change touches the math; AR diffs are at fp rounding level.
Perf impact is small at this scale (kernel-launch savings are a tiny
fraction of bs=64 step time), but the code is cleaner.

Signed-off-by: ziyixiong-nv <219238287+ziyixiong-nv@users.noreply.github.com>
DFlash was forwarding 2K tokens per gen request through the full target
model (K+1 accepted tokens plus K-1 mask fillers), but only the K+1
prefix is consumed by acceptance and the context-store projection.  The
K-1 fillers hit every target layer and allreduce for no benefit, and at
large batch that wasted target work is the dominant step-time gap
against Eagle3.

Reduce tokens_per_gen_step to K+1: target now runs exactly the tokens
the algorithm needs, and the DFlash worker drops the paired slicing,
padding, and reshape that only existed to bridge the 2K/K+1 mismatch.
The draft model's block_size and mask-query generation are untouched,
so acceptance length is preserved byte-for-byte.

Qwen3-8B on B200, TP=4, MTBench:
  bs=64 mdl=3:  4422 -> 5064 TPS/gpu (+14.5%),  +16.1% vs Eagle3 4363
  bs=64 mdl=7:        4756 TPS/gpu,              +4.6% vs Eagle3 4548
  bs=32 mdl=7:  3377 -> 4162 TPS/gpu (+23.3%)
  bs=32 mdl=3:  3503 -> 3806 TPS/gpu  (+8.6%)
AL unchanged across all configs.

Tests: test_dflash_qwen3_8b, test_dflash_qwen3_5_4b (both overlap-
scheduler variants), TestQwen3_8B::test_dflash (GSM8K) all pass.

Signed-off-by: ziyixiong-nv <219238287+ziyixiong-nv@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: dd99746a-d70f-4f39-ac54-0835a0a8d580

📥 Commits

Reviewing files that changed from the base of the PR and between 9547230 and d517eed.

📒 Files selected for processing (11)
  • tensorrt_llm/_torch/models/modeling_speculative.py
  • tensorrt_llm/_torch/modules/mamba/gdn_mixer.py
  • tensorrt_llm/_torch/pyexecutor/mamba_cache_manager.py
  • tensorrt_llm/_torch/pyexecutor/model_engine.py
  • tensorrt_llm/_torch/speculative/dflash.py
  • tensorrt_llm/llmapi/llm_args.py
  • tests/integration/defs/accuracy/references/gsm8k.yaml
  • tests/integration/defs/accuracy/test_llm_api_pytorch.py
  • tests/integration/test_lists/qa/llm_function_core.txt
  • tests/integration/test_lists/test-db/l0_h100.yml
  • tests/unittest/_torch/speculative/hw_agnostic/test_dflash.py

📝 Walkthrough

Walkthrough

This PR refactors DFlash speculative decoding from a 2K-token-pair model to a K+1 model (K draft tokens plus 1 bonus token). It adds precomputed context K/V caching, flashinfer-compatible RoPE support, and fixes position ID handling for 3D MRoPE tensors while extending validation tests for Qwen models.

Changes

DFlash K+1 Refactor

Layer / File(s) Summary
Configuration and K+1 Token Semantics
tensorrt_llm/llmapi/llm_args.py
DFlashDecodingConfig.tokens_per_gen_step returns max_draft_len + 1 instead of 2 * max_draft_len, defining the K+1 contract.
Mamba Verification Path for K+1
tensorrt_llm/_torch/modules/mamba/gdn_mixer.py
forward_decode() and forward_extend() verification paths now compute draft_token_num using max_total_draft_tokens + 1 (K+1 model) for correct tensor reshaping.
DFlash Model Architecture and Precomputation
tensorrt_llm/_torch/models/modeling_speculative.py
New precompute_context_kv() method builds fused per-layer K/V projection weights and returns fp32 cos/sin cache for flashinfer. dflash_forward() signature updated to consume precomputed per-slot ctx_k_cache/ctx_v_cache buffers and selects among three RoPE paths (fused QK-norm+RoPE, flashinfer, PyTorch fallback).
Cache Manager and Position Handling Integration
tensorrt_llm/_torch/pyexecutor/mamba_cache_manager.py, tensorrt_llm/_torch/pyexecutor/model_engine.py
MixedMambaHybridCacheManager passes tokens_per_gen_step - 1 to draft token counting. PyTorchModelEngine preprocesses/postprocesses 3D MRoPE position_ids tensors by branching on ndim for correct offset application.
DFlash Worker K+1 Orchestration
tensorrt_llm/_torch/speculative/dflash.py
DFlashWorker allocates fixed per-slot K/V buffer pools, updates _draft_tokens_per_req to K+1 semantics, stores precomputed context K/V during prefill/acceptance with write masks, and refactors prepare_1st_drafter_inputs() to use K+1 position IDs and embeddings (bonus + mask token). Draft forward call signature updated to pass precomputed K/V caches and indexing instead of target_hidden/context_positions.
Test and Validation Coverage
tests/unittest/_torch/speculative/hw_agnostic/test_dflash.py, tests/integration/defs/accuracy/test_llm_api_pytorch.py, tests/integration/defs/accuracy/references/gsm8k.yaml, tests/integration/test_lists/qa/llm_function_core.txt, tests/integration/test_lists/test-db/l0_h100.yml
Added parametrized unit tests for Qwen3-8B and Qwen3.5-4B with per-model GPU memory gates and draft acceptance assertions. Shared test helpers construct CUDA-graph configs and run generation. Integration test for Qwen3.5-4B DFlash on GSM8K benchmark with accuracy reference (80.5) and test suite registrations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • NVIDIA/TensorRT-LLM#12794: Directly related DFlash refactor that introduced the initial DFlashForCausalLM and dflash_forward implementation; this PR extends that foundation with precomputed K/V caching and flashinfer RoPE support.

Suggested reviewers

  • mikeiovine
  • syuoni
  • xinhe-nv
  • niukuo
  • lucaslie
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

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