Skip to content

[TRTLLM-16005][feat] Store DFlash draft cross-attention context paged in the target KV cache manager - #16150

Closed
chungen04 wants to merge 10 commits into
NVIDIA:mainfrom
chungen04:dflash-hybrid-context
Closed

[TRTLLM-16005][feat] Store DFlash draft cross-attention context paged in the target KV cache manager#16150
chungen04 wants to merge 10 commits into
NVIDIA:mainfrom
chungen04:dflash-hybrid-context

Conversation

@chungen04

@chungen04 chungen04 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai summary

Description

Closes #16005

In #16005, it was described that current design for DFlash cross attention allocates large GPU buffer and can limit the admitting requests during serving. Reflecting vLLM's design, the key was that the hidden state from target injected as KV cache in the drafter should be allocated paged rather than a contiguous worst-case buffer.

This PR stores the draft context inside the target KV cache manager. The draft layers register as spec layers of the target manager, so the context lives in ordinary paged blocks and its memory scales with live requests. The projected context K/V is written with norm and RoPE already applied, and keeps the store path CUDA-graph safe.

A kernel (tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py) performs the paged cross-attention read. The per-token cost of the draft layers is added to the KV size estimation so the scheduler budgets for it. The feature is gated behind a new use_hybrid_context flag on DFlashDecodingConfig and defaults to off (can be set to default). The kernel uses a flash-decoding style split-KV path for small batches, where the context is partitioned across several CTAs that emit partial softmax states. Split ranges derive from the device-resident context lengths, so the fixed grid stays CUDA-graph safe. Both paths read 128 context tokens.

Isolated kernel timings at the DFlash decode shape (per TP 2 rank, 16 query heads, 4 KV heads, head_dim 128, 16 queries, context 8192) compare as follows against reading the dense buffer with flash_attn_with_kvcache.

batch dense flash_attn this kernel
1 0.077 ms 0.063 ms
8 0.213 ms 0.077 ms
32 0.406 ms 0.252 ms
64 0.727 ms 0.503 ms

Two compatibility fixes ride along:

  • When the draft head_dim differs from the target's, the draft spec layers are registered in target-head_dim units with the head count scaled so the byte size stays exact, and the worker views the pool buffers back to draft geometry with a zero-copy reshape.
  • When the target is a hybrid-Mamba model, the KV size estimation now resolves the manager class from the draft config instead of passing the plain attention draft config into the target's Mamba manager class.

Results

  • Baseline: The "baseline" is the v1.3.0rc20 release. "hybrid" is this implementation, rebased on v1.3.0rc20.
  • Memory: Measured on 2× B300 per server, TP 2, max_batch_size 64 and free_gpu_memory_fraction 0.9.
model dense ctx buffer (baseline) KV pool baseline KV pool hybrid
Qwen3-8B, max_seq_len 40960 26 GB per rank 209.8 GB (2.50 M tokens) 234.0 GB (2.79 M tokens)
Qwen3.5-122B-A10B, max_seq_len 131072 100 GB per rank 18.7 GB (0.66 M tokens) 111.4 GB (4.43 M tokens)
  • Throughput: Measured with aiperf. The workload is zai-org/LongBench , sized with the model tokenizer to about 16k and 32k prompt tokens, decoded greedily with natural stopping capped at 1024 new tokens.

Testing results on serving models with different context and concurrency:

  • Qwen3-8B: The test was primarily justifying the overhead of the paged implementation. A slight speedup was from the isolated kernel timings, as shown in table above.
tier concurrency baseline tok/s hybrid tok/s delta
16k 1 334.5 336.8 +0.7%
16k 8 1063.8 1128.0 +6.0%
16k 32 1881.0 1791.0 −4.8%
16k 64 2239.3 2387.4 +6.6%
32k 1 254.2 251.2 −1.2%
32k 8 647.7 721.9 +11.5%
32k 32 742.4 786.4 +5.9%
32k 64 892.9 928.2 +4.0%

Observing the Prometheus counters, metrics related to speculative decoding shows the correctness of the implementation reflecting the original:

model side drafted accepted rate per-position rate
Qwen3-8B baseline 627435 113793 0.1814 0.368, 0.126, 0.050
Qwen3-8B hybrid 626547 114463 0.1827 0.371, 0.127, 0.050
  • Qwen3.5-122B-A10B: It is clear that the admittance of requests (and therefore throughput) is bounded at high concurrency due to the insufficiency of space for target KV Cache.
tier concurrency baseline tok/s hybrid tok/s delta
16k 1 141.8 150.2 +6.0%
16k 8 470.0 511.5 +8.8%
16k 32 877.7 892.5 +1.7%
16k 64 1138.2 1201.0 +5.5%
32k 1 99.2 108.6 +9.5%
32k 8 327.2 367.2 +12.2%
32k 32 530.1 578.1 +9.1%
32k 64 544.4 712.1 +30.8%
  • Minimax-M2.5: Since the model is larger, the serving also hits the target KV Cache limitation at small concurrency, but it is currently unavailable upstream, as blocked by [None][feat] Minimax Eagle #11661 .

The paged path reads only live context pages while the dense path strides over the worst-case per-request buffer, so the advantage grows with context length and batch size on top of the memory savings.

Limitations

  • On targets with an fp8 KV cache the draft context inherits the pool dtype and is currently cast at store time, and without per-layer scales.
  • The per-layer context scatter store is a small Python loop that could be fused into one kernel.
  • Prefix-cache restore of the draft context, which the paged design enables and the dense buffer cannot support, is implemented but not yet benchmarked. All experiments above disabled prefix caching. (Incorrect implementation of context restore may cause collapse in acceptance, see [Bug]: DFlash/DSpark draft acceptance collapses with automatic prefix caching enabled vllm-project/vllm#47930 .)
  • A cleaner long-term direction is per-layer head_dim support in the KV cache manager, which KVCacheManagerV2 already accepts but the Mamba-hybrid manager path does not.

Test coverage

  • End-to-end correctness was validated by bit-identical greedy generations between the baseline and hybrid servers on Qwen3-8B.
  • Unit tests for the kernel and the estimation path are to be added.

PR Checklist

Please review the following before submitting your PR:

  • PR description clearly explains what and why. If using CodeRabbit's summary, please make sure it makes sense.

  • PR Follows TRT-LLM CODING GUIDELINES to the best of your knowledge.

  • Test cases are provided for new code paths (see test instructions)

  • If PR introduces API changes, an appropriate PR label is added - either api-compatible or api-breaking. For api-breaking, include BREAKING in the PR title.

  • Any new dependencies have been scanned for license and vulnerabilities

  • CODEOWNERS updated if ownership changes

  • Documentation updated as needed

  • Update tava architecture diagram if there is a significant design change in PR.

  • The reviewers assigned automatically/manually are appropriate for the PR.

  • Please check this after reviewing the above items as appropriate for this PR.

GitHub Bot Help

To see a list of available CI bot commands, please comment /bot help.

@chungen04
chungen04 requested review from a team as code owners July 8, 2026 22:47
@chungen04
chungen04 marked this pull request as draft July 8, 2026 22:47
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds an optional hybrid-context mode to DFlash speculative decoding that stores draft cross-attention K/V in the target model's paged KV-cache pools instead of dedicated dense buffers. It includes a new Triton paged cross-attention kernel, KV-cache sizing/head-mapping updates, DFlashWorker pool-write wiring, and a config flag.

Changes

DFlash Hybrid Context Storage

Layer / File(s) Summary
Hybrid context config flag
tensorrt_llm/llmapi/llm_args.py
DFlashDecodingConfig gains use_hybrid_context and an internal _num_draft_layers attribute; the validator disables separate draft KV cache when hybrid context is enabled.
Draft layer counting
tensorrt_llm/_torch/speculative/utils.py
get_num_spec_layers returns the configured draft-layer count for DFlash hybrid context.
KV-cache sizing and head-dimension mapping
tensorrt_llm/_torch/pyexecutor/_util.py
Detects hybrid DFlash context, resolves draft-layer counts, folds draft KV cost into the target budget, and converts draft KV head counts into target head-dimension units.
Triton paged cross-attention kernel
tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py
New module implements single-pass and split/merge Triton kernels for paged KV cross-attention with a dense noise K/V suffix, plus a PyTorch reference implementation.
DFlashWorker hybrid pool storage
tensorrt_llm/_torch/speculative/dflash.py
Initializes hybrid-context state, maps pool geometry, computes page/block indices, writes projected K/V into pool pages, and returns hybrid buffer handles.
dflash_forward integration
tensorrt_llm/_torch/models/modeling_speculative.py
Adds hybrid KV buffer/page-index parameters and branches attention computation between the new paged kernel and the existing flash-attn path.

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

Suggested reviewers: hnover-nv, jieli-matrix, yechank-nvidia, dongxuy04, kaiyux

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address the core requirements in #16005, including paged storage, scheduler sizing, prefix-cache restore, and geometry/head-dim compatibility.
Out of Scope Changes check ✅ Passed No clearly unrelated code changes are introduced beyond the requested DFlash hybrid-context feature and compatibility fixes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly states the main change: paging DFlash draft cross-attention context in the target KV cache manager.
Description check ✅ Passed The description follows the template and includes description, test coverage, and checklist content with a substantive summary of the change.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py (1)

357-391: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add return annotations and a public entrypoint docstring.

The new non-JIT Python functions should be fully annotated, and dflash_ctx_paged_attention is imported outside this file, so prefer a Google-style docstring over inline argument comments. As per coding guidelines, “Always annotate functions” and “Use Google style docstrings for classes and functions in Python.” Based on learnings, Python 3.10+ type syntax is acceptable here.

Also applies to: 450-458

🤖 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 `@tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py` around lines 357 -
391, The new Python helpers are missing explicit return annotations and the
public entrypoint lacks a proper docstring. Add Python 3.10+ type hints,
including return types, to the non-JIT helpers like _get_partial_bufs and
_num_splits, and replace the inline argument comments on
dflash_ctx_paged_attention with a Google-style docstring that describes its
arguments and return value. Keep the function names and signatures easy to
locate in the speculative attention module, and apply the same
documentation/annotation treatment to the related block around the later context
attention helper.

Sources: Coding guidelines, Learnings

🤖 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 `@tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py`:
- Around line 357-366: The `_get_partial_bufs` helper in `dflash_hybrid_attn`
uses the ambiguous single-letter buffer name `l`, which triggers Ruff E741.
Rename that local buffer to a clearer name like `logsumexp` or `lse`, and update
the returned tuple and `_PARTIAL_BUFS` assignment so the new identifier is used
consistently wherever the partial buffers are created.
- Around line 72-78: The Triton kernel setup in dflash_hybrid_attn uses
tl.arange spans that must be powers of two, but the current validation only
checks for a minimum size, so non-power-of-two values for D or R will still
compile-fail. Update the shape validation and launch path around the tl.arange
calls and the speculative attention kernel entry points to either reject
non-power-of-two head_dim / Q*group sizes early or pad them to the next power of
two before invoking the Triton kernels.

---

Nitpick comments:
In `@tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py`:
- Around line 357-391: The new Python helpers are missing explicit return
annotations and the public entrypoint lacks a proper docstring. Add Python 3.10+
type hints, including return types, to the non-JIT helpers like
_get_partial_bufs and _num_splits, and replace the inline argument comments on
dflash_ctx_paged_attention with a Google-style docstring that describes its
arguments and return value. Keep the function names and signatures easy to
locate in the speculative attention module, and apply the same
documentation/annotation treatment to the related block around the later context
attention helper.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: b8f5735c-2def-4f0e-8aa4-6f7737f54632

📥 Commits

Reviewing files that changed from the base of the PR and between 4cd00bb and 8d0363d.

📒 Files selected for processing (6)
  • tensorrt_llm/_torch/models/modeling_speculative.py
  • tensorrt_llm/_torch/pyexecutor/_util.py
  • tensorrt_llm/_torch/speculative/dflash.py
  • tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py
  • tensorrt_llm/_torch/speculative/utils.py
  • tensorrt_llm/llmapi/llm_args.py

Comment on lines +72 to +78
d = tl.arange(0, D)
tok = tl.arange(0, BLOCK_N)
for n0 in range(start, end, BLOCK_N):
idx = n0 + tok
valid = idx < end
# Per-token page gather: page ids come from the block table row.
page = tl.load(blk_row_ptr + idx // TPB, mask=valid, other=0).to(tl.int64)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect whether tests/configs cover non-power-of-two DFlash tile shapes.
rg -n --type=py \
  'dflash_ctx_paged_attention|dflash_ctx_paged_attention_ref|max_draft_len|head_dim|num_key_value_heads|num_attention_heads' \
  tensorrt_llm tests

Repository: NVIDIA/TensorRT-LLM

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and inspect the shape guards and tl.arange uses.
ast-grep outline tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py --view expanded

echo '---'
nl -ba tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py | sed -n '50,170p'

echo '---'
nl -ba tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py | sed -n '330,430p'

echo '---'
# Find any explicit power-of-two padding or next_power_of_2 usage in this module.
rg -n 'next_power_of_2|power of two|power-of-two|tl\.arange|BLOCK_|R =|D =' tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py

Repository: NVIDIA/TensorRT-LLM

Length of output: 654


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== relevant lines =='
sed -n '340,430p' tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py

echo '== tl.arange uses =='
grep -n 'tl.arange\|next_power_of_2\|power' tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py

echo '== shape/guard references in module =='
grep -n 'Q \* group\|D >=\|R >=\|BLOCK_\|tokens_per_block\|num_splits\|partial' tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py

Repository: NVIDIA/TensorRT-LLM

Length of output: 5385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== top-of-file context =='
sed -n '1,60p' tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py

echo '== surrounding kernels with tl.arange =='
sed -n '60,180p' tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py
echo '---'
sed -n '180,340p' tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py

echo '== call sites / tests using dflash_ctx_paged_attention =='
rg -n 'dflash_ctx_paged_attention\(' tensorrt_llm tests

Repository: NVIDIA/TensorRT-LLM

Length of output: 11827


🌐 Web query:

Triton tl.arange power of two endpoint requirements documentation

💡 Result:

In Triton, the tl.arange(start, end) function has specific requirements regarding its arguments and the resulting range [1]. Key requirements include: 1. Power of Two Requirement: The difference between the end and start arguments (the range) must be a power of two [1]. Specifically, if the value (end - start) is not a power of two, Triton will raise a ValueError [1]. Note that official documentation snippets may sometimes generalize or misstate this as a requirement for the start and end values themselves being powers of two [2], but the functional implementation specifically enforces that the range (end - start) must be a power of two [1]. 2. Constexpr Requirement: Both the start and end arguments must be tl.constexpr [1]. If you pass runtime values to tl.arange, the kernel compilation will fail with a ValueError [3][4][1]. Because tl.arange requires constexpr arguments, any block size or range parameter used within it must also be defined as tl.constexpr within the kernel signature [5]. 3. Integer Constraints: The arguments must fit within a 32-bit integer, and the end value must be strictly greater than the start value [1]. For best practices, ensure that any block size or range values used in indexing are marked as tl.constexpr in your Triton kernel signature to avoid compilation errors [5]. Top results: [2] https://triton-lang.org/main/python-api/generated/triton.language.arange.html [1] https://github.com/triton-lang/triton/blob/505bdb74/python/triton/language/semantic.py

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== call site context =='
sed -n '1350,1405p' tensorrt_llm/_torch/models/modeling_speculative.py

echo '== any draft/block size constraints near the call site =='
grep -n 'max_draft_len\|draft\|block_size\|Q =' -n tensorrt_llm/_torch/models/modeling_speculative.py | head -n 50

echo '== any explicit power-of-two checks in dflash module =='
grep -n 'power\|pow2\|next_power_of_2\|assert .*16' tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py

Repository: NVIDIA/TensorRT-LLM

Length of output: 6433


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path("tensorrt_llm/_torch/models/modeling_speculative.py")
text = path.read_text()
for needle in ["dflash_ctx_paged_attention(", "max_draft_len", "draft", "block_size", "speculative"]:
    idx = text.find(needle)
    print(f"NEEDLE {needle!r}: {idx}")
PY

Repository: NVIDIA/TensorRT-LLM

Length of output: 306


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== DFlash-related config classes / model usage =='
rg -n 'DFlash|dflash|head_dim|num_attention_heads|num_key_value_heads|qk_nope_head_dim|qk_rope_head_dim|v_head_dim' tensorrt_llm/_torch/models tensorrt_llm/_torch/speculative

echo '== specific DFlash draft model definitions =='
rg -n 'class .*DFlash|def .*DFlash|DFlashDraftModel|dflash_ctx_paged_attention' tensorrt_llm/_torch/models tensorrt_llm/_torch/speculative

echo '== head_dim constraints in tests for DFlash / hybrid speculative paths =='
rg -n 'DFlash|hybrid_block_idx|cache_seqlens|draft model|head_dim.*(64|72|128)|qk_nope_head_dim|v_head_dim' tests/unittest/_torch tests/unittest/others

Repository: NVIDIA/TensorRT-LLM

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== DFlash-specific definitions and constraints =='
rg -n 'DFlashDraftModel|hybrid_k_bufs|dflash_ctx_paged_attention|head_dim|num_attention_heads|num_key_value_heads|qk_nope_head_dim|v_head_dim' \
  tensorrt_llm/_torch/models tensorrt_llm/_torch/speculative | head -n 200

echo '== nearby DFlash model class / init context =='
sed -n '820,920p' tensorrt_llm/_torch/models/modeling_speculative.py

echo '== any explicit size constraints in DFlash/speculative docs =='
rg -n 'power of two|pow2|next_power_of_2|supported head_dim|supported.*head_dim|DFlash' \
  tensorrt_llm/_torch/models tensorrt_llm/_torch/speculative | head -n 200

Repository: NVIDIA/TensorRT-LLM

Length of output: 34992


Pad the tl.arange spans to powers of two.

tl.arange(0, D) and tl.arange(0, R) require a power-of-two span, but this path only checks >= 16. Non-power-of-two head_dim or Q * group will fail kernel compilation, so pad or reject those shapes before launching the Triton kernels.

🤖 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 `@tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py` around lines 72 - 78,
The Triton kernel setup in dflash_hybrid_attn uses tl.arange spans that must be
powers of two, but the current validation only checks for a minimum size, so
non-power-of-two values for D or R will still compile-fail. Update the shape
validation and launch path around the tl.arange calls and the speculative
attention kernel entry points to either reject non-power-of-two head_dim /
Q*group sizes early or pad them to the next power of two before invoking the
Triton kernels.

Comment on lines +357 to +366
def _get_partial_bufs(B, NKV, S, R, D, device):
key = (B, NKV, S, R, D, device)
bufs = _PARTIAL_BUFS.get(key)
if bufs is None:
acc = torch.empty(B * NKV * S * R * D, dtype=torch.float32, device=device)
m = torch.empty(B * NKV * S * R, dtype=torch.float32, device=device)
l = torch.empty(B * NKV * S * R, dtype=torch.float32, device=device)
bufs = (acc, m, l)
_PARTIAL_BUFS[key] = bufs
return bufs

Copy link
Copy Markdown
Contributor

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

Rename the ambiguous l buffer.

Ruff flags Line 363 with E741. Rename it to avoid blocking lint.

Proposed fix
-        l = torch.empty(B * NKV * S * R, dtype=torch.float32, device=device)
-        bufs = (acc, m, l)
+        l_buf = torch.empty(B * NKV * S * R, dtype=torch.float32, device=device)
+        bufs = (acc, m, l_buf)
📝 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
def _get_partial_bufs(B, NKV, S, R, D, device):
key = (B, NKV, S, R, D, device)
bufs = _PARTIAL_BUFS.get(key)
if bufs is None:
acc = torch.empty(B * NKV * S * R * D, dtype=torch.float32, device=device)
m = torch.empty(B * NKV * S * R, dtype=torch.float32, device=device)
l = torch.empty(B * NKV * S * R, dtype=torch.float32, device=device)
bufs = (acc, m, l)
_PARTIAL_BUFS[key] = bufs
return bufs
def _get_partial_bufs(B, NKV, S, R, D, device):
key = (B, NKV, S, R, D, device)
bufs = _PARTIAL_BUFS.get(key)
if bufs is None:
acc = torch.empty(B * NKV * S * R * D, dtype=torch.float32, device=device)
m = torch.empty(B * NKV * S * R, dtype=torch.float32, device=device)
l_buf = torch.empty(B * NKV * S * R, dtype=torch.float32, device=device)
bufs = (acc, m, l_buf)
_PARTIAL_BUFS[key] = bufs
return bufs
🧰 Tools
🪛 Ruff (0.15.20)

[error] 363-363: Ambiguous variable name: l

(E741)

🤖 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 `@tensorrt_llm/_torch/speculative/dflash_hybrid_attn.py` around lines 357 -
366, The `_get_partial_bufs` helper in `dflash_hybrid_attn` uses the ambiguous
single-letter buffer name `l`, which triggers Ruff E741. Rename that local
buffer to a clearer name like `logsumexp` or `lse`, and update the returned
tuple and `_PARTIAL_BUFS` assignment so the new identifier is used consistently
wherever the partial buffers are created.

Source: Linters/SAST tools

@chungen04 chungen04 changed the title [TRTLLM-16005][feat] DFlash: store draft cross-attention context in the target KV cache manager [TRTLLM-16005][feat] DFlash: store draft cross-attention context paged in the target KV cache manager Jul 8, 2026
@chungen04 chungen04 changed the title [TRTLLM-16005][feat] DFlash: store draft cross-attention context paged in the target KV cache manager [TRTLLM-16005][feat] Store DFlash draft cross-attention context paged in the target KV cache manager Jul 8, 2026
chungen04 and others added 8 commits July 9, 2026 20:20
…ontext

Add DFlashDecodingConfig.use_hybrid_context (prototype). When enabled, the
draft cross-attention context layers are registered as spec layers of the
target KV cache manager: get_num_spec_layers reports the draft layer count
and KvCacheCreator budgets the extra per-token cost on the last PP rank,
instead of allocating a dense per-request context buffer.

Signed-off-by: chungen04 <cho322@gatech.edu>
Read the draft context K/V directly from the target KV cache manager's
paged pool. Existing kernels do not fit: sequence lengths are
device-resident (acceptance-dependent, updated inside CUDA graphs), pages
are target-pool sized rather than 256-token aligned, and the dense noise
suffix must be fused into the same softmax. Online-softmax with bf16
tensor-core dots and fp32 accumulation; supports fp8 context storage.
Includes a torch reference implementation for testing.

Signed-off-by: chungen04 <cho322@gatech.edu>
…ache manager

When use_hybrid_context is enabled, the DFlash worker scatters the
projected context K/V into the draft spec layers of the target manager's
paged pool (position-aligned, so prefix-cache hits restore context), and
the drafter reads it back with the paged cross-attention kernel instead of
flash_attn_with_kvcache over a dense per-request buffer. Dense-path
behavior is unchanged when the flag is off.

Signed-off-by: chungen04 <cho322@gatech.edu>
The single-pass kernel launched (batch, num_kv_heads) CTAs and marched
one 32-token page per tl.dot iteration; at small batch that is ~4 CTAs
on a 148-SM GPU and cost ~+1 ms/step vs the dense-buffer flash_attn
path (measured -24% output throughput at concurrency 1, Qwen3-8B TP2).

Rework it flash-decoding style:

- Small batch: split-KV. The context is partitioned across S CTAs that
  emit partial softmax states; a merge kernel folds the partials plus
  the dense noise suffix. S derives from the launch batch size (static
  under CUDA graph capture); per-split ranges derive from the
  device-resident ctx_len, so the fixed grid stays graph-safe. Partial
  buffers are cached per shape for stable capture addresses.
- Large batch: keep the single-pass path (no partial round-trip).
- Both paths tile BLOCK_N=128 context tokens (4 pages) per tl.dot with
  a per-token page-id gather instead of one page per iteration.

Kernel time at B=1 (ctx 8192, 4 KV heads/rank): 0.284 ms -> 0.063 ms,
now faster than dense flash_attn_with_kvcache (0.077 ms). End-to-end
aiperf sweep (Qwen3-8B TP2, ISL 8000/OSL 512): hybrid ctx goes from
-24%..-7% vs the dense baseline to +2% (c=1), +8% (c=8), +4..5%
(c=16..64), with bit-identical greedy outputs.

Signed-off-by: chungen04 <cho322@gatech.edu>
The split count S was a tl.constexpr derived from the launch batch size.
Mixed context+generation iterations run eagerly at their actual
(unpadded) batch size, so serving traffic kept discovering new S values
and paying a blocking Triton JIT compile mid-request: p90 ITL jumped
3-4x and TTFT tails reached 13-17 s on the first sweep levels after a
server start, disappearing once the in-process compile cache warmed
(which made it look like measurement noise).

S only enters index arithmetic and the merge loop trip count, so pass
it as a runtime argument: each kernel compiles exactly once and no JIT
ever runs on the serving path. Kernel parity and timings unchanged.

Signed-off-by: chungen04 <cho322@gatech.edu>
…from the target

The KV cache manager has a single head_dim per pool, so draft spec
layers inherited the target's head_dim and the context store failed
with a shape mismatch whenever the two differ (e.g. a 128-head_dim
draft on a 256-head_dim target). Register draft spec layers in
target-head_dim units (heads scaled so bytes stay exact) and view the
pool buffers back to draft geometry in the worker; pure view, no copy.
Raises a clear error when the draft KV row is not expressible in
target head_dim units.

Signed-off-by: chungen04 <cho322@gatech.edu>
…a targets

The hybrid-ctx cost path passed the draft's plain-attention config into
the target's Mamba-hybrid cache manager class, raising 'Qwen3Config is
not a supported hybrid Mamba config' at startup on GDN/Mamba-hybrid
targets (e.g. Qwen3.5). Resolve the manager class from the draft config
instead, matching the external-drafter path.

Signed-off-by: chungen04 <cho322@gatech.edu>
Signed-off-by: chungen04 <b09901027@ntu.edu.tw>
@chungen04
chungen04 force-pushed the dflash-hybrid-context branch from af69173 to 5df8b51 Compare July 9, 2026 20:31
…e paged attention kernel

tl.arange requires a power-of-two span, but the query-row count
R = block_size x GQA group was used directly, so draft lengths not of
the form 2^k - 1 (e.g. max_draft_len=5 -> R=24) or non-power-of-two
GQA ratios failed Triton compilation at warmup. Every published DFlash
draft happens to be power-of-two clean, which is why this never fired
in benchmarks.

Pad rows to R_PAD = next_power_of_2(R) with masked query loads and
masked output stores; padded rows carry zeros through the softmax
pipeline and are discarded at the store, so results are exact. The
partial buffers of the split path are R_PAD-strided. head_dim and
tokens_per_block keep explicit power-of-two checks with actionable
errors (no known model needs padding there). Power-of-two shapes
compile with the masks folded away, so the existing fast path is
unchanged; kernel parity verified for R=24, R=20 (group 5), and R=6
against the torch reference, including CUDA-graph replay.

Signed-off-by: chungen04 <cho322@gatech.edu>
@chungen04
chungen04 marked this pull request as ready for review July 9, 2026 21:08
@chungen04

chungen04 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Hi reviewers, this PR solves an issue on DFlash with higher serving concurrency. Please take a look, thank you!

  • The feature was currently gated by a flag. Can also remove it (set it as default) if the design was justified better, and will avoid API compatibility issues.
  • No tests were currently added.
  • Please also check [None][feat] Minimax Eagle #11661 .

cc @JunyiXu-nv @zheyuf @lfr-0531 @yechank-nvidia

Signed-off-by: chungen04 <b09901027@ntu.edu.tw>

@JunyiXu-nv JunyiXu-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please add unit tests for the new kernel and feature. Thanks!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

These newly added fields should also be added to the stability tests' yaml list.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

You've shipped a torch reference implementation specifically for parity testing, but nothing in this PR actually calls it. This kernel is exactly the kind of code that needs parity tests: 630 lines, two execution paths (single-pass vs split+merge), fp8, and several padding edge cases. Could you add a few cases to tests/unittest/_torch/speculative/hw_agnostic/test_dflash.py? At minimum: ctx_len=0, ctx_len not page-aligned, empty splits when S>1, and a shape like Q=6 where R_PAD padding kicks in. Otherwise this ref function is dead code.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is a new user-visible knob (prototype or not), but not any mention of it under docs/. Please add a short section to the DFlash part of the speculative-decoding docs covering the prerequisites

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This new hybrid branch is nearly line-for-line identical to the separate-draft branch at L431-437. Can we fold them into a single "draft layers need to be charged" condition?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

set_max_total_draft_tokens now also flips _allow_separate_draft_kv_cache off as a side gig, so the name no longer matches what it does. Please rename.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

check spec_dec_mode.is_dflash() first and then still do getattr(sc, 'use_hybrid_context', False) — but is_dflash() already guarantees this is a DFlashDecodingConfig, so the field always exists. Just use sc.use_hybrid_context directly.

@chungen04

Copy link
Copy Markdown
Contributor Author

closing as duplicate with #18343

@chungen04 chungen04 closed this Sep 10, 2026
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.

[Feature]: Paged / KV-manager-resident storage for the DFlash draft context, replacing the dense buffer

3 participants