A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 5, 19:45 UTC
[Feat][Spec Decode] DFlash (#36847)
Signed-off-by: Benjamin Chislett <bchislett@nvidia.com>
Benjamin Chislett committed
Mar 30, 2026 at 15:03 UTC
494636b29d3b3a7b35020e4becb6c6995e200f9d
17 files changed
+1577
-107
tests/models/registry.py
+8
@@ -1163,6 +1163,14 @@ _SPECULATIVE_DECODING_EXAMPLE_MODELS = {
1163
# "JackFram/llama-160m",
1164
# speculative_model="ibm-ai-platform/llama-160m-accelerator"
1165
# ),
1166
+ # [DFlash]
1167
+ "DFlashDraftModel": _HfExamplesInfo(
1168
+ "Qwen/Qwen3.5-4B",
1169
+ speculative_model="z-lab/Qwen3.5-4B-DFlash",
1170
+ use_original_num_layers=True, # Need all layers since DFlash has >1 layer,
1171
+ max_model_len=8192, # Reduce max len to ensure test runs in low-VRAM CI env
1172
+ max_num_seqs=32,
1173
+ ),
1174
# [Eagle]
1175
"EagleDeepSeekMTPModel": _HfExamplesInfo(
1176
"eagle618/deepseek-v3-random",
tests/v1/e2e/spec_decode/test_spec_decode.py
+166
-6
@@ -7,6 +7,7 @@ from typing import Any
7
8
import pytest
9
import torch
10
+from tqdm import tqdm
11
12
from tests.evals.gsm8k.gsm8k_eval import _build_gsm8k_prompts, evaluate_gsm8k_offline
13
from tests.utils import (
@@ -1105,19 +1106,178 @@ def some_high_acceptance_metrics() -> dict:
1106
}
1107
1108
1108
-def compute_acceptance_rate(metrics: list[Metric]) -> float:
1109
+def compute_acceptance_rate(
1110
+ metrics: list[Metric], prev_metrics: list[Metric] | None = None
1111
+) -> float:
1112
name2metric = {metric.name: metric for metric in metrics}
1110
- n_draft_toks = name2metric["vllm:spec_decode_num_draft_tokens"].value # type: ignore
1113
+ n_draft_toks = name2metric["vllm:spec_decode_num_draft_tokens"].value
1114
if n_draft_toks == 0:
1115
return float("nan")
1113
- n_accepted_toks = name2metric["vllm:spec_decode_num_accepted_tokens"].value # type: ignore
1116
+ n_accepted_toks = name2metric["vllm:spec_decode_num_accepted_tokens"].value
1117
+ if prev_metrics is not None:
1118
+ prev_name2metric = {metric.name: metric for metric in prev_metrics}
1119
+ n_draft_toks -= prev_name2metric["vllm:spec_decode_num_draft_tokens"].value
1120
+ n_accepted_toks -= prev_name2metric[
1121
+ "vllm:spec_decode_num_accepted_tokens"
1122
+ ].value
1123
+ if n_draft_toks <= 0:
1124
+ return float("nan")
1125
return n_accepted_toks / n_draft_toks
1126
1127
1117
-def compute_acceptance_len(metrics: list[Metric]) -> float:
1128
+def compute_acceptance_len(
1129
+ metrics: list[Metric], prev_metrics: list[Metric] | None = None
1130
+) -> float:
1131
name2metric = {metric.name: metric for metric in metrics}
1119
- n_drafts = name2metric["vllm:spec_decode_num_drafts"].value # type: ignore
1120
- n_accepted_toks = name2metric["vllm:spec_decode_num_accepted_tokens"].value # type: ignore
1132
+ n_drafts = name2metric["vllm:spec_decode_num_drafts"].value
1133
+ n_accepted_toks = name2metric["vllm:spec_decode_num_accepted_tokens"].value
1134
if n_drafts == 0:
1135
return 1
1136
+ if prev_metrics is not None:
1137
+ prev_name2metric = {metric.name: metric for metric in prev_metrics}
1138
+ n_drafts -= prev_name2metric["vllm:spec_decode_num_drafts"].value
1139
+ n_accepted_toks -= prev_name2metric[
1140
+ "vllm:spec_decode_num_accepted_tokens"
1141
+ ].value
1142
+ if n_drafts <= 0:
1143
+ return 1
1144
return 1 + (n_accepted_toks / n_drafts)
1145
+
1146
+
1147
+# Datasets in the format used in DFlash validations
1148
+def load_and_process_dataset(data_name: str):
1149
+ from datasets import load_dataset
1150
+
1151
+ if data_name == "gsm8k":
1152
+ dataset = load_dataset("openai/gsm8k", "main", split="test")
1153
+ prompt_fmt = (
1154
+ "{question}\nPlease reason step by step,"
1155
+ " and put your final answer within \\boxed{{}}."
1156
+ )
1157
+ dataset = dataset.map(lambda x: {"turns": [prompt_fmt.format(**x)]})
1158
+ elif data_name == "mt-bench":
1159
+ dataset = load_dataset("HuggingFaceH4/mt_bench_prompts", split="train")
1160
+ dataset = dataset.map(lambda x: {"turns": x["prompt"]})
1161
+ elif data_name == "humaneval":
1162
+ dataset = load_dataset("openai/openai_humaneval", split="test")
1163
+ prompt_fmt = (
1164
+ "Write a solution to the following problem and make sure"
1165
+ " that it passes the tests:\n```python\n{prompt}\n```"
1166
+ )
1167
+ dataset = dataset.map(lambda x: {"turns": [prompt_fmt.format(**x)]})
1168
+
1169
+ return dataset
1170
+
1171
+
1172
+@pytest.fixture
1173
+def dflash_config():
1174
+ target_model = "Qwen/Qwen3-8B"
1175
+ draft_model = "z-lab/Qwen3-8B-DFlash-b16"
1176
+
1177
+ return dict(
1178
+ model=target_model,
1179
+ trust_remote_code=True,
1180
+ speculative_config={
1181
+ "method": "dflash",
1182
+ "model": draft_model,
1183
+ "num_speculative_tokens": 16,
1184
+ "max_model_len": 32768,
1185
+ },
1186
+ max_model_len=32768,
1187
+ max_num_seqs=128,
1188
+ gpu_memory_utilization=0.85,
1189
+ enforce_eager=False,
1190
+ disable_log_stats=False,
1191
+ )
1192
+
1193
+
1194
+def test_dflash_acceptance_rates(dflash_config):
1195
+ """
1196
+ E2E test for DFlash (block diffusion) speculative decoding.
1197
+ Runs acceptance rate validation on GSM8k, MT-Bench, and HumanEval
1198
+ comparing against baseline results from the paper (Table 1).
1199
+ See https://github.com/z-lab/dflash/blob/main/benchmark_sglang.py for methodology.
1200
+ """
1201
+ spec_llm = LLM(**dflash_config)
1202
+
1203
+ max_prompts_per_dataset = 200 # mt-bench has 80, humaneval has 164, truncates gsm8k
1204
+
1205
+ # All scores from Table 1 in https://arxiv.org/pdf/2602.06036
1206
+ expected_acceptance_lengths = {
1207
+ "mt-bench": 4.24,
1208
+ "humaneval": 6.50,
1209
+ "gsm8k": 6.54 * 0.95, # runs with a subset of prompts so extra wide tol here
1210
+ }
1211
+
1212
+ tokenizer = spec_llm.get_tokenizer()
1213
+ for dataset_name, expected_len in expected_acceptance_lengths.items():
1214
+ dataset = load_and_process_dataset(dataset_name)
1215
+ prev_metrics = None
1216
+ acceptance_lengths = []
1217
+ for i in tqdm(
1218
+ range(min(max_prompts_per_dataset, len(dataset))),
1219
+ desc=f"Processing {dataset_name}",
1220
+ ):
1221
+ user_content = dataset[i]["turns"][0]
1222
+ prompt_text = tokenizer.apply_chat_template(
1223
+ [{"role": "user", "content": user_content}],
1224
+ tokenize=False,
1225
+ add_generation_prompt=True,
1226
+ enable_thinking=False,
1227
+ )
1228
+
1229
+ # Temp=0, MaxTokens=2048 from the paper
1230
+ spec_llm.generate(
1231
+ [prompt_text],
1232
+ SamplingParams(temperature=0, max_tokens=2048),
1233
+ use_tqdm=False,
1234
+ )
1235
+ current_metrics = spec_llm.get_metrics()
1236
+ acceptance_len = compute_acceptance_len(current_metrics, prev_metrics)
1237
+ prev_metrics = current_metrics
1238
+ acceptance_lengths.append(acceptance_len)
1239
+
1240
+ mean_acceptance_length = sum(acceptance_lengths) / len(acceptance_lengths)
1241
+ expected_len = expected_len * 0.9
1242
+ print(
1243
+ f"DFlash acceptance_len for {dataset_name}: {mean_acceptance_length:.2f}"
1244
+ f" (expected at least {expected_len:.2f})"
1245
+ )
1246
+
1247
+ assert mean_acceptance_length >= expected_len, (
1248
+ f"DFlash acceptance_len for {dataset_name} is below expected threshold:"
1249
+ f"{mean_acceptance_length:.2f} < {expected_len:.2f}"
1250
+ )
1251
+
1252
+ del spec_llm
1253
+ torch.accelerator.empty_cache()
1254
+ cleanup_dist_env_and_memory()
1255
+
1256
+
1257
+def test_dflash_correctness(dflash_config):
1258
+ """
1259
+ E2E test for DFlash (block diffusion) speculative decoding.
1260
+ Ensures output correctness on GSM8k, with cudagraphs and batching on.
1261
+ """
1262
+ spec_llm = LLM(**dflash_config)
1263
+
1264
+ # Evaluate GSM8k accuracy (Qwen3-8B ref: ~87-92% on GSM8k)
1265
+ evaluate_llm_for_gsm8k(spec_llm, expected_accuracy_threshold=0.8)
1266
+
1267
+ current_metrics = spec_llm.get_metrics()
1268
+ acceptance_len = compute_acceptance_len(current_metrics)
1269
+
1270
+ # AR is thoroughly validated in test_dflash_acceptance_rates, in a manner consistent
1271
+ # with the DFlash paper. However, that test measures AL per-request and thus runs
1272
+ # with a batch size of 1. To ensure that AL does not collapse with large batch sizes
1273
+ # we enforce a baseline on the AL over the full lm-eval-style GSM8k test.
1274
+ expected_len = 3.5 # Measured is 3.9 to 4.0
1275
+ print(f"DFlash GSM8k correctness test got AL {acceptance_len}")
1276
+ assert acceptance_len >= expected_len, (
1277
+ "DFlash correctness check failed with"
1278
+ f" {acceptance_len=}, expected at least {expected_len}"
1279
+ )
1280
+
1281
+ del spec_llm
1282
+ torch.accelerator.empty_cache()
1283
+ cleanup_dist_env_and_memory()
tests/v1/spec_decode/test_eagle.py
+152
-3
@@ -27,6 +27,7 @@ from vllm.config.load import LoadConfig
27
from vllm.model_executor.models.llama import LlamaForCausalLM
28
from vllm.platforms import current_platform
29
from vllm.v1.attention.backends.registry import AttentionBackendEnum
30
+from vllm.v1.spec_decode.dflash import DFlashProposer
31
from vllm.v1.spec_decode.draft_model import DraftModelProposer
32
from vllm.v1.spec_decode.eagle import EagleProposer
33
from vllm.v1.spec_decode.metadata import SpecDecodeMetadata
@@ -36,6 +37,8 @@ model_dir = "meta-llama/Llama-3.1-8B-Instruct"
37
eagle_dir = "yuhuili/EAGLE-LLaMA3.1-Instruct-8B"
38
eagle3_dir = "yuhuili/EAGLE3-LLaMA3.1-Instruct-8B"
39
ar_draft_model_dir = "amd/PARD-Llama-3.2-1B" # Compatible with parallel and AR drafting
40
+dflash_target_dir = "Qwen/Qwen3-8B"
41
+dflash_dir = "z-lab/Qwen3-8B-DFlash-b16"
42
43
BLOCK_SIZE = 16
44
@@ -47,18 +50,29 @@ def _create_proposer(
50
speculative_token_tree: list[tuple[int, ...]] | None = None,
51
parallel_drafting: bool = False,
52
) -> EagleProposer:
50
- model_config = ModelConfig(model=model_dir, runner="generate", max_model_len=100)
51
-
53
# Method-dependent setup
54
if method == "eagle":
55
+ target_model_dir = model_dir
56
draft_model_dir = eagle_dir
57
elif method == "eagle3":
58
+ target_model_dir = model_dir
59
draft_model_dir = eagle3_dir
60
elif method == "draft_model":
61
+ target_model_dir = model_dir
62
draft_model_dir = ar_draft_model_dir
63
+ elif method == "dflash":
64
+ target_model_dir = dflash_target_dir
65
+ draft_model_dir = dflash_dir
66
else:
67
raise ValueError(f"Unknown method: {method}")
68
69
+ model_config = ModelConfig(
70
+ model=target_model_dir,
71
+ runner="generate",
72
+ max_model_len=100,
73
+ trust_remote_code=(method == "dflash"),
74
+ )
75
+
76
spec_token_tree_str = None
77
if speculative_token_tree is not None:
78
assert num_speculative_tokens == len(speculative_token_tree)
@@ -92,7 +106,9 @@ def _create_proposer(
106
attention_config=AttentionConfig(backend=attention_backend),
107
)
108
95
- if "eagle" in method:
109
+ if method == "dflash":
110
+ proposer = DFlashProposer(vllm_config=vllm_config, device=device)
111
+ elif "eagle" in method:
112
proposer = EagleProposer(vllm_config=vllm_config, device=device)
113
else:
114
proposer = DraftModelProposer(vllm_config=vllm_config, device=device)
@@ -1152,3 +1168,136 @@ def test_propose_tree(spec_token_tree):
1168
1169
# Verify that the draft tokens match our expectations.
1170
assert torch.equal(result, expected_tokens)
1171
+
1172
+
1173
+def test_set_inputs_first_pass_dflash():
1174
+ """
1175
+ Test for DFlash set_inputs_first_pass.
1176
+
1177
+ DFlash uses cross-attention: context tokens become K/V and only
1178
+ query tokens (bonus + mask) are Q. This tests the DFlash-specific
1179
+ input preparation where:
1180
+ - Context hidden states are stored by reference (no copy)
1181
+ - Query input_ids are [next_token, mask, mask, ...] per request
1182
+ - Context and query positions are written to separate buffers
1183
+ - token_indices_to_sample points to mask token positions only
1184
+ - A new CommonAttentionMetadata is returned with causal=False
1185
+
1186
+ Setup:
1187
+ - 3 requests with query_lens [3, 2, 4]
1188
+ - num_speculative_tokens = 3
1189
+ - num_query_per_req = 4 (1 bonus + 3 mask tokens)
1190
+ - next_token_ids: [100, 200, 300]
1191
+
1192
+ Expected output layout (query tokens only, 12 total):
1193
+ Request 0 (indices 0-3): [100, mask, mask, mask]
1194
+ Request 1 (indices 4-7): [200, mask, mask, mask]
1195
+ Request 2 (indices 8-11): [300, mask, mask, mask]
1196
+
1197
+ Expected positions layout (separate buffers):
1198
+ Context (_context_positions_buffer, 9 tokens): copied from target_positions
1199
+ Query (positions, 12 tokens):
1200
+ Request 0: last_pos=9, query=[10, 11, 12, 13]
1201
+ Request 1: last_pos=7, query=[8, 9, 10, 11]
1202
+ Request 2: last_pos=11, query=[12, 13, 14, 15]
1203
+ """
1204
+ device = torch.device(current_platform.device_type)
1205
+
1206
+ num_speculative_tokens = 3
1207
+ proposer = _create_proposer("dflash", num_speculative_tokens)
1208
+ mask_token_id = proposer.parallel_drafting_token_id
1209
+
1210
+ # Setup batch with 3 requests
1211
+ batch_spec = BatchSpec(
1212
+ seq_lens=[10, 8, 12],
1213
+ query_lens=[3, 2, 4],
1214
+ )
1215
+
1216
+ common_attn_metadata = create_common_attn_metadata(
1217
+ batch_spec,
1218
+ block_size=BLOCK_SIZE,
1219
+ device=device,
1220
+ arange_block_indices=True,
1221
+ )
1222
+
1223
+ # Input tensors
1224
+ # Request 0: tokens [10, 11, 12] at positions [7, 8, 9]
1225
+ # Request 1: tokens [20, 21] at positions [6, 7]
1226
+ # Request 2: tokens [30, 31, 32, 33] at positions [8, 9, 10, 11]
1227
+ target_token_ids = torch.tensor(
1228
+ [10, 11, 12, 20, 21, 30, 31, 32, 33], dtype=torch.int32, device=device
1229
+ )
1230
+ target_positions = torch.tensor(
1231
+ [7, 8, 9, 6, 7, 8, 9, 10, 11], dtype=torch.int64, device=device
1232
+ )
1233
+ target_hidden_states = torch.randn(
1234
+ 9, proposer.hidden_size, dtype=proposer.dtype, device=device
1235
+ )
1236
+ next_token_ids = torch.tensor([100, 200, 300], dtype=torch.int32, device=device)
1237
+
1238
+ num_tokens, token_indices_to_sample, output_cad = proposer.set_inputs_first_pass(
1239
+ target_token_ids=target_token_ids,
1240
+ next_token_ids=next_token_ids,
1241
+ target_positions=target_positions,
1242
+ target_hidden_states=target_hidden_states,
1243
+ token_indices_to_sample=None,
1244
+ cad=common_attn_metadata,
1245
+ num_rejected_tokens_gpu=None,
1246
+ )
1247
+
1248
+ num_query_per_req = 1 + num_speculative_tokens # 4
1249
+ num_context = 9
1250
+
1251
+ # num_tokens is the query-only count
1252
+ assert num_tokens == 3 * num_query_per_req # 12
1253
+
1254
+ # Verify input_ids (query tokens only)
1255
+ # Each request: [next_token, mask, mask, mask]
1256
+ M = mask_token_id
1257
+ expected_input_ids = torch.tensor(
1258
+ [100, M, M, M, 200, M, M, M, 300, M, M, M],
1259
+ dtype=torch.int32,
1260
+ device=device,
1261
+ )
1262
+ assert torch.equal(proposer.input_ids[:num_tokens], expected_input_ids)
1263
+
1264
+ # Verify context positions (separate buffer): copied from target_positions
1265
+ assert torch.equal(
1266
+ proposer._context_positions_buffer[:num_context], target_positions
1267
+ )
1268
+
1269
+ # Verify query positions (separate buffer, starts at index 0):
1270
+ # req0: last_pos=9, query=[10, 11, 12, 13]
1271
+ # req1: last_pos=7, query=[8, 9, 10, 11]
1272
+ # req2: last_pos=11, query=[12, 13, 14, 15]
1273
+ expected_query_positions = torch.tensor(
1274
+ [10, 11, 12, 13, 8, 9, 10, 11, 12, 13, 14, 15],
1275
+ dtype=torch.int64,
1276
+ device=device,
1277
+ )
1278
+ assert torch.equal(
1279
+ proposer.positions[:num_tokens],
1280
+ expected_query_positions,
1281
+ )
1282
+
1283
+ # Verify token_indices_to_sample (mask tokens only, skip bonus at offset 0)
1284
+ # req0: query indices 0-3, mask at 1,2,3
1285
+ # req1: query indices 4-7, mask at 5,6,7
1286
+ # req2: query indices 8-11, mask at 9,10,11
1287
+ expected_token_indices_to_sample = torch.tensor(
1288
+ [1, 2, 3, 5, 6, 7, 9, 10, 11], dtype=torch.int32, device=device
1289
+ )
1290
+ assert torch.equal(token_indices_to_sample, expected_token_indices_to_sample)
1291
+
1292
+ # Verify the new CAD has DFlash-specific properties
1293
+ assert output_cad.causal is False # DFlash requires non-causal attention
1294
+ assert output_cad.num_actual_tokens == num_tokens # query-only count
1295
+ assert output_cad.max_query_len == num_query_per_req
1296
+
1297
+ expected_query_start_loc = torch.tensor(
1298
+ [0, 4, 8, 12], dtype=torch.int32, device=device
1299
+ )
1300
+ assert torch.equal(output_cad.query_start_loc, expected_query_start_loc)
1301
+
1302
+ # Verify hidden states (stored by reference, not copied)
1303
+ assert proposer._dflash_hidden_states is target_hidden_states
vllm/config/speculative.py
+21
-6
@@ -47,8 +47,11 @@ MTPModelTypes = Literal[
47
"pangu_ultra_moe_mtp",
48
"step3p5_mtp",
49
]
50
-EagleModelTypes = Literal["eagle", "eagle3", "extract_hidden_states", MTPModelTypes]
50
NgramGPUTypes = Literal["ngram_gpu"]
51
+DFlashModelTypes = Literal["dflash"]
52
+EagleModelTypes = Literal[
53
+ "eagle", "eagle3", "extract_hidden_states", MTPModelTypes, DFlashModelTypes
54
+]
55
SpeculativeMethod = Literal[
56
"ngram",
57
"medusa",
@@ -206,7 +209,11 @@ class SpeculativeConfig:
209
factors: list[Any] = []
210
# Eagle3 and extract_hidden_states affect the computation graph because
211
# they return intermediate hidden states in addition to the final hidden state.
209
- uses_aux_hidden_states = self.method in ("eagle3", "extract_hidden_states")
212
+ uses_aux_hidden_states = self.method in (
213
+ "eagle3",
214
+ "extract_hidden_states",
215
+ "dflash",
216
+ )
217
factors.append(uses_aux_hidden_states)
218
219
# The specific layers used also affect the computation graph
@@ -490,7 +497,7 @@ class SpeculativeConfig:
497
)
498
499
# Automatically detect the method
493
- if self.method in ("eagle", "eagle3"):
500
+ if self.method in ("eagle", "eagle3", "dflash"):
501
pass
502
# examples:
503
# yuhuili/EAGLE-LLaMA3-Instruct-8B
@@ -500,6 +507,8 @@ class SpeculativeConfig:
507
self.method = "eagle"
508
elif "eagle3" in self.draft_model_config.model.lower():
509
self.method = "eagle3"
510
+ elif "dflash" in self.draft_model_config.model.lower():
511
+ self.method = "dflash"
512
elif self.draft_model_config.hf_config.model_type == "medusa":
513
self.method = "medusa"
514
elif self.draft_model_config.hf_config.model_type == "mlp_speculator":
@@ -532,7 +541,7 @@ class SpeculativeConfig:
541
)
542
543
# Replace hf_config for EAGLE draft_model
535
- if self.method in ("eagle", "eagle3"):
544
+ if self.method in ("eagle", "eagle3", "dflash"):
545
from vllm.transformers_utils.configs.eagle import EAGLEConfig
546
from vllm.transformers_utils.configs.speculators import (
547
SpeculatorsConfig,
@@ -552,6 +561,9 @@ class SpeculativeConfig:
561
self.draft_model_config.hf_config = eagle_config
562
self.update_arch_()
563
564
+ if self.method == "dflash":
565
+ self.parallel_drafting = True
566
+
567
if self.num_speculative_tokens is not None and hasattr(
568
self.draft_model_config.hf_config, "num_lookahead_tokens"
569
):
@@ -807,7 +819,7 @@ class SpeculativeConfig:
819
"kimi_k25",
820
]
821
if (
810
- self.method in ("eagle3", "extract_hidden_states")
822
+ self.method in ("eagle3", "extract_hidden_states", "dflash")
823
and self.target_model_config
824
and not any(
825
supported_model in self.target_model_config.hf_text_config.model_type
@@ -855,7 +867,10 @@ class SpeculativeConfig:
867
return slots_per_req
868
869
def use_eagle(self) -> bool:
858
- return self.method in ("eagle", "eagle3", "mtp")
870
+ return self.method in ("eagle", "eagle3", "mtp", "dflash")
871
+
872
+ def use_dflash(self) -> bool:
873
+ return self.method == "dflash"
874
875
def uses_draft_model(self) -> bool:
876
return self.method == "draft_model"
vllm/config/vllm.py
+20
@@ -1327,6 +1327,26 @@ class VllmConfig:
1327
max_num_batched_tokens - scheduled_token_delta
1328
)
1329
1330
+ if self.scheduler_config.max_num_scheduled_tokens <= 0:
1331
+ raise ValueError(
1332
+ "max_num_scheduled_tokens is set to"
1333
+ f" {self.scheduler_config.max_num_scheduled_tokens} based on"
1334
+ " the speculative decoding settings, which does not allow"
1335
+ " any tokens to be scheduled. Increase max_num_batched_tokens"
1336
+ " to accommodate the additional draft token slots, or decrease"
1337
+ " num_speculative_tokens or max_num_seqs."
1338
+ )
1339
+ if self.scheduler_config.max_num_scheduled_tokens < 8192:
1340
+ logger.warning_once(
1341
+ "max_num_scheduled_tokens is set to"
1342
+ f" {self.scheduler_config.max_num_scheduled_tokens} based on"
1343
+ " the speculative decoding settings. This may lead to suboptimal"
1344
+ " performance. Consider increasing max_num_batched_tokens to"
1345
+ " accommodate the additional draft token slots, or decrease"
1346
+ " num_speculative_tokens or max_num_seqs.",
1347
+ scope="local",
1348
+ )
1349
+
1350
max_num_scheduled_tokens = self.scheduler_config.max_num_scheduled_tokens
1351
if max_num_batched_tokens < max_num_scheduled_tokens + (
1352
self.speculative_config.max_num_new_slots_for_drafting
vllm/model_executor/models/qwen3.py
+1
@@ -285,6 +285,7 @@ class Qwen3ForCausalLM(
285
286
self.config = config
287
288
+ self.vllm_config = vllm_config
289
self.quant_config = quant_config
290
self.model = Qwen3Model(
291
vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
vllm/model_executor/models/qwen3_dflash.py
new
+619
@@ -0,0 +1,619 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from collections.abc import Iterable
5
+
6
+import torch
7
+import torch.nn.functional as F
8
+from torch import nn
9
+from transformers import Qwen3Config
10
+
11
+from vllm import _custom_ops as ops
12
+from vllm.compilation.decorators import support_torch_compile
13
+from vllm.config import CacheConfig, VllmConfig, get_current_vllm_config
14
+from vllm.distributed import get_tensor_model_parallel_world_size
15
+from vllm.logger import init_logger
16
+from vllm.model_executor.layers.attention import Attention
17
+from vllm.model_executor.layers.layernorm import RMSNorm
18
+from vllm.model_executor.layers.linear import (
19
+ QKVParallelLinear,
20
+ ReplicatedLinear,
21
+ RowParallelLinear,
22
+)
23
+from vllm.model_executor.layers.logits_processor import LogitsProcessor
24
+from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
25
+from vllm.model_executor.layers.rotary_embedding import get_rope
26
+from vllm.model_executor.layers.vocab_parallel_embedding import (
27
+ ParallelLMHead,
28
+ VocabParallelEmbedding,
29
+)
30
+from vllm.model_executor.model_loader.weight_utils import (
31
+ default_weight_loader,
32
+ maybe_remap_kv_scale_name,
33
+)
34
+from vllm.multimodal.inputs import NestedTensors
35
+from vllm.transformers_utils.config import set_default_rope_theta
36
+from vllm.v1.attention.backend import AttentionType
37
+
38
+from .qwen2 import Qwen2MLP as Qwen3MLP
39
+from .qwen3 import Qwen3ForCausalLM
40
+from .utils import (
41
+ AutoWeightsLoader,
42
+ get_draft_quant_config,
43
+ maybe_prefix,
44
+ process_eagle_weight,
45
+)
46
+
47
+logger = init_logger(__name__)
48
+
49
+
50
+class DFlashQwen3Attention(nn.Module):
51
+ """Attention for DFlash speculative decoding.
52
+
53
+ Context KVs are pre-inserted into the KV cache before the forward pass.
54
+ This layer handles only query tokens via standard attention.
55
+ Adapted from Qwen3Attention."""
56
+
57
+ def __init__(
58
+ self,
59
+ hidden_size: int,
60
+ num_heads: int,
61
+ num_kv_heads: int,
62
+ rope_parameters: dict,
63
+ max_position: int = 4096 * 32,
64
+ head_dim: int | None = None,
65
+ rms_norm_eps: float = 1e-06,
66
+ attention_bias: bool = False,
67
+ cache_config: CacheConfig | None = None,
68
+ quant_config: QuantizationConfig | None = None,
69
+ prefix: str = "",
70
+ attn_type: str = AttentionType.DECODER,
71
+ ) -> None:
72
+ super().__init__()
73
+ self.layer_name = prefix
74
+ self.hidden_size = hidden_size
75
+ tp_size = get_tensor_model_parallel_world_size()
76
+ self.total_num_heads = num_heads
77
+ assert self.total_num_heads % tp_size == 0
78
+ self.num_heads = self.total_num_heads // tp_size
79
+ self.total_num_kv_heads = num_kv_heads
80
+ if self.total_num_kv_heads >= tp_size:
81
+ assert self.total_num_kv_heads % tp_size == 0
82
+ else:
83
+ assert tp_size % self.total_num_kv_heads == 0
84
+ self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
85
+ self.head_dim = head_dim or hidden_size // self.total_num_heads
86
+ self.q_size = self.num_heads * self.head_dim
87
+ self.kv_size = self.num_kv_heads * self.head_dim
88
+ self.scaling = self.head_dim**-0.5
89
+
90
+ self.qkv_proj = QKVParallelLinear(
91
+ hidden_size,
92
+ self.head_dim,
93
+ self.total_num_heads,
94
+ self.total_num_kv_heads,
95
+ bias=attention_bias,
96
+ quant_config=quant_config,
97
+ prefix=f"{prefix}.qkv_proj",
98
+ )
99
+ self.o_proj = RowParallelLinear(
100
+ self.total_num_heads * self.head_dim,
101
+ hidden_size,
102
+ bias=attention_bias, # DFlash has o_proj bias when using attention bias
103
+ quant_config=quant_config,
104
+ prefix=f"{prefix}.o_proj",
105
+ )
106
+
107
+ self.rotary_emb = get_rope(
108
+ self.head_dim,
109
+ max_position=max_position,
110
+ rope_parameters=rope_parameters,
111
+ )
112
+ self.attn = Attention(
113
+ self.num_heads,
114
+ self.head_dim,
115
+ self.scaling,
116
+ num_kv_heads=self.num_kv_heads,
117
+ cache_config=cache_config,
118
+ quant_config=quant_config,
119
+ prefix=f"{prefix}.attn",
120
+ attn_type=attn_type,
121
+ )
122
+ self.q_norm = RMSNorm(self.head_dim, eps=rms_norm_eps)
123
+ self.k_norm = RMSNorm(self.head_dim, eps=rms_norm_eps)
124
+
125
+ def forward(
126
+ self,
127
+ positions: torch.Tensor,
128
+ hidden_states: torch.Tensor,
129
+ ) -> torch.Tensor:
130
+ """DFlash attention assumes that the KV cache is already populated
131
+ with the context K/V from the target model's hidden states. This forward op
132
+ computes attention for the query tokens only.
133
+ See also: precompute_and_store_context_kv"""
134
+ qkv = F.linear(hidden_states, self.qkv_proj.weight, self.qkv_proj.bias)
135
+ q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
136
+
137
+ # Per-head RMSNorm
138
+ q_shape, k_shape = q.shape, k.shape
139
+ q = self.q_norm(
140
+ q.view(*q_shape[:-1], q_shape[-1] // self.head_dim, self.head_dim)
141
+ ).view(q_shape)
142
+ k = self.k_norm(
143
+ k.view(*k_shape[:-1], k_shape[-1] // self.head_dim, self.head_dim)
144
+ ).view(k_shape)
145
+
146
+ q, k = self.rotary_emb(positions, q, k)
147
+
148
+ attn_output = self.attn(q, k, v)
149
+ output, _ = self.o_proj(attn_output)
150
+ return output
151
+
152
+
153
+class DFlashQwen3DecoderLayer(nn.Module):
154
+ def __init__(
155
+ self,
156
+ vllm_config: VllmConfig,
157
+ *,
158
+ config: Qwen3Config,
159
+ cache_config: CacheConfig | None = None,
160
+ quant_config: QuantizationConfig | None = None,
161
+ prefix: str = "",
162
+ ) -> None:
163
+ super().__init__()
164
+ self.hidden_size = config.hidden_size
165
+ set_default_rope_theta(config, default_theta=1000000)
166
+ attn_type = AttentionType.DECODER
167
+
168
+ self.self_attn = DFlashQwen3Attention(
169
+ hidden_size=self.hidden_size,
170
+ num_heads=config.num_attention_heads,
171
+ max_position=config.max_position_embeddings,
172
+ num_kv_heads=config.num_key_value_heads,
173
+ rms_norm_eps=config.rms_norm_eps,
174
+ attention_bias=getattr(config, "attention_bias", False),
175
+ head_dim=getattr(config, "head_dim", None),
176
+ cache_config=cache_config,
177
+ quant_config=quant_config,
178
+ rope_parameters=config.rope_parameters,
179
+ prefix=f"{prefix}.self_attn",
180
+ attn_type=attn_type,
181
+ )
182
+ self.mlp = Qwen3MLP(
183
+ hidden_size=self.hidden_size,
184
+ intermediate_size=config.intermediate_size,
185
+ hidden_act=config.hidden_act,
186
+ quant_config=quant_config,
187
+ prefix=f"{prefix}.mlp",
188
+ )
189
+ self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
190
+ self.post_attention_layernorm = RMSNorm(
191
+ config.hidden_size, eps=config.rms_norm_eps
192
+ )
193
+
194
+ def forward(
195
+ self,
196
+ positions: torch.Tensor,
197
+ hidden_states: torch.Tensor,
198
+ residual: torch.Tensor | None,
199
+ ) -> tuple[torch.Tensor, torch.Tensor]:
200
+ if residual is not None:
201
+ hidden_states, residual = self.input_layernorm(hidden_states, residual)
202
+ else:
203
+ residual = hidden_states
204
+ hidden_states = self.input_layernorm(hidden_states)
205
+
206
+ hidden_states = self.self_attn(
207
+ positions=positions,
208
+ hidden_states=hidden_states,
209
+ )
210
+
211
+ hidden_states, residual = self.post_attention_layernorm(hidden_states, residual)
212
+ hidden_states = self.mlp(hidden_states)
213
+ return hidden_states, residual
214
+
215
+
216
+@support_torch_compile
217
+class DFlashQwen3Model(nn.Module):
218
+ def __init__(
219
+ self,
220
+ *,
221
+ vllm_config: VllmConfig,
222
+ start_layer_id: int = 0,
223
+ prefix: str = "",
224
+ ) -> None:
225
+ super().__init__()
226
+ self.config = vllm_config.speculative_config.draft_model_config.hf_config
227
+ self.vocab_size = self.config.vocab_size
228
+ self.quant_config = get_draft_quant_config(vllm_config)
229
+
230
+ drafter_config = getattr(self.config, "eagle_config", {})
231
+ drafter_config.update(getattr(self.config, "dflash_config", {}))
232
+
233
+ if drafter_config is not None and "use_aux_hidden_state" in drafter_config:
234
+ self.use_aux_hidden_state = drafter_config["use_aux_hidden_state"]
235
+ else:
236
+ self.use_aux_hidden_state = True
237
+
238
+ current_vllm_config = get_current_vllm_config()
239
+
240
+ self.embed_tokens = VocabParallelEmbedding(
241
+ self.config.vocab_size,
242
+ self.config.hidden_size,
243
+ prefix=maybe_prefix(prefix, "embed_tokens"),
244
+ )
245
+
246
+ self.layers = nn.ModuleList(
247
+ [
248
+ DFlashQwen3DecoderLayer(
249
+ current_vllm_config,
250
+ prefix=maybe_prefix(prefix, f"layers.{layer_idx + start_layer_id}"),
251
+ config=self.config,
252
+ )
253
+ for layer_idx in range(self.config.num_hidden_layers)
254
+ ]
255
+ )
256
+ if self.use_aux_hidden_state:
257
+ num_features_to_use = self.config.num_hidden_layers
258
+ if "target_layer_ids" in drafter_config:
259
+ num_features_to_use = len(drafter_config["target_layer_ids"])
260
+ elif "layer_ids" in drafter_config:
261
+ num_features_to_use = len(drafter_config["layer_ids"])
262
+ if hasattr(self.config, "target_hidden_size"):
263
+ fc_input_size = self.config.target_hidden_size * num_features_to_use
264
+ else:
265
+ fc_input_size = self.config.hidden_size * num_features_to_use
266
+ self.fc = ReplicatedLinear(
267
+ input_size=fc_input_size,
268
+ output_size=self.config.hidden_size,
269
+ bias=False,
270
+ params_dtype=vllm_config.model_config.dtype,
271
+ quant_config=self.quant_config,
272
+ prefix=maybe_prefix(prefix, "fc"),
273
+ return_bias=False,
274
+ )
275
+ self.hidden_norm = RMSNorm(
276
+ self.config.hidden_size,
277
+ eps=self.config.rms_norm_eps,
278
+ )
279
+ self.norm = RMSNorm(
280
+ self.config.hidden_size,
281
+ eps=self.config.rms_norm_eps,
282
+ )
283
+
284
+ def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
285
+ return self.embed_tokens(input_ids)
286
+
287
+ def _build_fused_kv_buffers(self) -> None:
288
+ """Build fused weight buffers for precompute_and_store_context_kv.
289
+
290
+ Must be called after weights are loaded. Stacks the KV-projection
291
+ weights, K-norm weights, and RoPE parameters from every attention
292
+ layer so that precompute_and_store_context_kv can run one fused
293
+ GEMM for all layers at once. Also aliases the weight of the hidden_norm.
294
+ """
295
+ layers_attn = [layer.self_attn for layer in self.layers]
296
+ attn0 = layers_attn[0]
297
+ has_bias = attn0.qkv_proj.bias is not None
298
+
299
+ self._hidden_norm_weight = self.hidden_norm.weight.data
300
+
301
+ # KV projection weights: [num_layers * 2 * kv_size, hidden_size]
302
+ kv_weights = [a.qkv_proj.weight[a.q_size :] for a in layers_attn]
303
+ self._fused_kv_weight = torch.cat(kv_weights, dim=0)
304
+ if has_bias:
305
+ kv_biases = [a.qkv_proj.bias[a.q_size :] for a in layers_attn]
306
+ self._fused_kv_bias: torch.Tensor | None = torch.cat(kv_biases, dim=0)
307
+ else:
308
+ self._fused_kv_bias = None
309
+
310
+ # K-norm weights: list of [head_dim] tensors, one per layer.
311
+ self._k_norm_weights = [a.k_norm.weight.data for a in layers_attn]
312
+
313
+ # RoPE parameters
314
+ self._rope_head_size = attn0.rotary_emb.head_size
315
+ self._rope_cos_sin_cache = attn0.rotary_emb.cos_sin_cache
316
+ self._rope_is_neox = attn0.rotary_emb.is_neox_style
317
+ # Validation that RoPE params are the same across all layers
318
+ for attn in layers_attn[1:]:
319
+ assert (
320
+ attn.rotary_emb.head_size == self._rope_head_size
321
+ and attn.rotary_emb.is_neox_style == self._rope_is_neox
322
+ ), "All layers must have the same RoPE parameters for DFlash precomputation"
323
+
324
+ # Layer metadata
325
+ self._num_attn_layers = len(layers_attn)
326
+ self._kv_size = attn0.kv_size
327
+ self._head_dim = attn0.head_dim
328
+ self._num_kv_heads = attn0.num_kv_heads
329
+ self._rms_norm_eps = attn0.q_norm.variance_epsilon
330
+ # Validation that all layers have the same attention config
331
+ for attn in layers_attn[1:]:
332
+ assert (
333
+ attn.kv_size == self._kv_size
334
+ and attn.head_dim == self._head_dim
335
+ and attn.num_kv_heads == self._num_kv_heads
336
+ and attn.q_norm.variance_epsilon == self._rms_norm_eps
337
+ ), "All layers must have the same attn config for DFlash precomputation"
338
+
339
+ # References to inner Attention layers for direct cache writes
340
+ self._attn_layers = [layer.self_attn.attn for layer in self.layers]
341
+
342
+ def precompute_and_store_context_kv(
343
+ self,
344
+ context_states: torch.Tensor,
345
+ context_positions: torch.Tensor,
346
+ context_slot_mapping: torch.Tensor | None = None,
347
+ ) -> None:
348
+ """Precompute K/V for context states write them into each layer's KV cache.
349
+
350
+ Input context states are projected to K/V, normed, and have RoPE applied.
351
+ Since the context shape is different than the query shape, we can't rely on the
352
+ regular forward pass to apply torch.compile and CUDA graphs to this section.
353
+ As such, this function is optimized to minimize the number of torch ops present:
354
+ we use fused vLLM kernels for RMSNorm and RoPE, fuse the GEMM into one
355
+ large projection, and avoid cloning buffers (with .contiguous()) where possible.
356
+
357
+ When context_slot_mapping is None (e.g. during dummy_run) only
358
+ the computation runs, and no K/V is written to cache.
359
+ """
360
+ if not hasattr(self, "_num_attn_layers"):
361
+ logger.warning_once(
362
+ "DFlash buffer initialization was skipped. If dummy weights are not "
363
+ "in use, this may indicate an error in weight loading."
364
+ )
365
+ self._build_fused_kv_buffers()
366
+
367
+ num_ctx = context_states.shape[0]
368
+ L = self._num_attn_layers
369
+ kv = self._kv_size
370
+ hd = self._head_dim
371
+ nkv = self._num_kv_heads
372
+
373
+ # --- Fused KV projection (one GEMM for all layers) ---
374
+ normed_context_states = torch.empty_like(context_states)
375
+ ops.rms_norm(
376
+ normed_context_states,
377
+ context_states,
378
+ self._hidden_norm_weight,
379
+ self._rms_norm_eps,
380
+ )
381
+ all_kv_flat = F.linear(
382
+ normed_context_states, self._fused_kv_weight, self._fused_kv_bias
383
+ )
384
+ # Single contiguous copy that separates K/V and transposes to
385
+ # layer-major layout. Result: [2, L, num_ctx, nkv, hd] contiguous.
386
+ # Indexing dim-0 gives contiguous [L, num_ctx, nkv, hd] for K and V.
387
+ all_kv = (
388
+ all_kv_flat.view(num_ctx, L, 2, nkv, hd).permute(2, 1, 0, 3, 4).contiguous()
389
+ )
390
+ all_k = all_kv[0] # [L, num_ctx, nkv, hd], contiguous
391
+ all_v = all_kv[1] # [L, num_ctx, nkv, hd], contiguous
392
+
393
+ # --- Per-layer RMSNorm K (3D: [num_ctx, nkv, hd] per layer) ---
394
+ all_k_normed = torch.empty_like(all_k)
395
+ for i in range(L):
396
+ ops.rms_norm(
397
+ all_k_normed[i],
398
+ all_k[i],
399
+ self._k_norm_weights[i],
400
+ self._rms_norm_eps,
401
+ )
402
+
403
+ # --- Fused RoPE across all layers ---
404
+ # View as [L * num_ctx, kv] so RoPE sees one big batch (no copy).
405
+ # In-place RoPE: pass K as the "query" arg with key=None.
406
+ all_k_flat = all_k_normed.view(L * num_ctx, kv)
407
+ positions_repeated = context_positions.repeat(L)
408
+ cos_sin_cache = self._rope_cos_sin_cache
409
+ if cos_sin_cache.dtype != all_k_flat.dtype:
410
+ cos_sin_cache = cos_sin_cache.to(dtype=all_k_flat.dtype)
411
+ ops.rotary_embedding(
412
+ positions_repeated,
413
+ all_k_flat,
414
+ None,
415
+ self._rope_head_size,
416
+ cos_sin_cache,
417
+ self._rope_is_neox,
418
+ )
419
+
420
+ if context_slot_mapping is None:
421
+ return
422
+
423
+ # --- Per-layer cache insert ---
424
+ all_k_final = all_k_flat.view(L, num_ctx, nkv, hd)
425
+ for i in range(L):
426
+ attn = self._attn_layers[i]
427
+ kv_cache = attn.kv_cache
428
+ attn.impl.do_kv_cache_update(
429
+ attn,
430
+ all_k_final[i],
431
+ all_v[i],
432
+ kv_cache,
433
+ context_slot_mapping,
434
+ )
435
+
436
+ def forward(
437
+ self,
438
+ input_ids: torch.Tensor,
439
+ positions: torch.Tensor,
440
+ input_embeds: torch.Tensor | None = None,
441
+ ) -> torch.Tensor:
442
+ if input_embeds is None:
443
+ input_embeds = self.embed_input_ids(input_ids)
444
+
445
+ hidden_states = input_embeds
446
+
447
+ residual = None
448
+ for layer in self.layers:
449
+ hidden_states, residual = layer(
450
+ positions=positions,
451
+ hidden_states=hidden_states,
452
+ residual=residual,
453
+ )
454
+ hidden_states, _ = self.norm(hidden_states, residual)
455
+ return hidden_states
456
+
457
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
458
+ stacked_params_mapping = [
459
+ (".qkv_proj", ".q_proj", "q"),
460
+ (".qkv_proj", ".k_proj", "k"),
461
+ (".qkv_proj", ".v_proj", "v"),
462
+ (".gate_up_proj", ".gate_proj", 0),
463
+ (".gate_up_proj", ".up_proj", 1),
464
+ ]
465
+ params_dict = dict(self.named_parameters())
466
+ loaded_params: set[str] = set()
467
+ for name, loaded_weight in weights:
468
+ if "midlayer." in name:
469
+ name = name.replace("midlayer.", "layers.0.")
470
+ if self.quant_config is not None and (
471
+ scale_name := self.quant_config.get_cache_scale(name)
472
+ ):
473
+ param = params_dict[scale_name]
474
+ weight_loader = getattr(param, "weight_loader", default_weight_loader)
475
+ loaded_weight = (
476
+ loaded_weight if loaded_weight.dim() == 0 else loaded_weight[0]
477
+ )
478
+ weight_loader(param, loaded_weight)
479
+ loaded_params.add(scale_name)
480
+ continue
481
+ if "scale" in name:
482
+ name = maybe_remap_kv_scale_name(name, params_dict)
483
+ if name is None:
484
+ continue
485
+ for param_name, weight_name, shard_id in stacked_params_mapping:
486
+ if weight_name not in name:
487
+ continue
488
+ name = name.replace(weight_name, param_name)
489
+ param = params_dict[name]
490
+ weight_loader = param.weight_loader
491
+ weight_loader(param, loaded_weight, shard_id)
492
+ break
493
+ else:
494
+ param = params_dict[name]
495
+ weight_loader = getattr(param, "weight_loader", default_weight_loader)
496
+ weight_loader(param, loaded_weight)
497
+ loaded_params.add(name)
498
+ return loaded_params
499
+
500
+
501
+class DFlashQwen3ForCausalLM(Qwen3ForCausalLM):
502
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
503
+ nn.Module.__init__(self)
504
+ self.config = vllm_config.speculative_config.draft_model_config.hf_config
505
+ if getattr(self.config, "draft_vocab_size", None) is None:
506
+ self.config.draft_vocab_size = getattr(self.config, "vocab_size", None)
507
+ target_layer_num = vllm_config.model_config.get_num_layers(
508
+ vllm_config.parallel_config
509
+ )
510
+ self.config.target_layer_count = target_layer_num
511
+ self.model = DFlashQwen3Model(
512
+ vllm_config=vllm_config,
513
+ prefix="model",
514
+ start_layer_id=target_layer_num,
515
+ )
516
+
517
+ logit_scale = getattr(self.config, "logit_scale", 1.0)
518
+ self.lm_head = ParallelLMHead(
519
+ self.config.draft_vocab_size,
520
+ self.config.hidden_size,
521
+ prefix=maybe_prefix(prefix, "lm_head"),
522
+ )
523
+ self.logits_processor = LogitsProcessor(
524
+ self.config.draft_vocab_size, scale=logit_scale
525
+ )
526
+ self.draft_id_to_target_id = None
527
+
528
+ def embed_input_ids(
529
+ self,
530
+ input_ids: torch.Tensor,
531
+ multimodal_embeddings: NestedTensors | None = None,
532
+ is_multimodal: torch.Tensor | None = None,
533
+ ) -> torch.Tensor:
534
+ return self.model.embed_input_ids(input_ids)
535
+
536
+ def forward(
537
+ self,
538
+ input_ids: torch.Tensor,
539
+ positions: torch.Tensor,
540
+ inputs_embeds: torch.Tensor | None = None,
541
+ ) -> torch.Tensor:
542
+ return self.model(input_ids, positions, inputs_embeds)
543
+
544
+ def compute_logits(
545
+ self,
546
+ hidden_states: torch.Tensor,
547
+ ) -> torch.Tensor | None:
548
+ logits = self.logits_processor(self.lm_head, hidden_states)
549
+ if self.draft_id_to_target_id is None:
550
+ return logits
551
+
552
+ base = torch.arange(self.config.draft_vocab_size, device=logits.device)
553
+ targets = base + self.draft_id_to_target_id
554
+ logits_new = logits.new_full(
555
+ (logits.shape[0], self.config.vocab_size),
556
+ float("-inf"),
557
+ )
558
+ logits_new[:, targets] = logits
559
+ return logits_new
560
+
561
+ def precompute_and_store_context_kv(
562
+ self,
563
+ context_states: torch.Tensor,
564
+ context_positions: torch.Tensor,
565
+ context_slot_mapping: torch.Tensor | None = None,
566
+ ) -> None:
567
+ """Precompute projected + RoPE'd K/V and write to cache."""
568
+ self.model.precompute_and_store_context_kv(
569
+ context_states, context_positions, context_slot_mapping
570
+ )
571
+
572
+ def combine_hidden_states(
573
+ self,
574
+ hidden_states: torch.Tensor,
575
+ ) -> torch.Tensor:
576
+ if not self.model.use_aux_hidden_state:
577
+ return hidden_states
578
+ needs_squeeze = hidden_states.dim() == 1
579
+ if needs_squeeze:
580
+ hidden_states = hidden_states.unsqueeze(0)
581
+ result = self.model.fc(hidden_states)
582
+ if needs_squeeze:
583
+ result = result.squeeze(0)
584
+ return result
585
+
586
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
587
+ model_weights = {}
588
+ includes_draft_id_mapping = False
589
+ includes_embed_tokens = False
590
+ for name, loaded_weight in weights:
591
+ assert "mask_hidden" not in name, (
592
+ "DFlash should use mask_token_id to embed the padding hidden state"
593
+ )
594
+ if "t2d" in name:
595
+ continue
596
+ if "d2t" in name:
597
+ name = name.replace("d2t", "draft_id_to_target_id")
598
+ includes_draft_id_mapping = True
599
+ elif "lm_head" not in name:
600
+ name = "model." + name
601
+ if "embed_tokens" in name:
602
+ includes_embed_tokens = True
603
+ model_weights[name] = loaded_weight
604
+ process_eagle_weight(self, name)
605
+
606
+ skip_substrs = []
607
+ if not includes_draft_id_mapping:
608
+ skip_substrs.append("draft_id_to_target_id")
609
+ if not includes_embed_tokens:
610
+ skip_substrs.append("embed_tokens")
611
+ if not self.model.use_aux_hidden_state:
612
+ skip_substrs.append("fc.")
613
+ loader = AutoWeightsLoader(
614
+ self,
615
+ skip_prefixes=None,
616
+ skip_substrs=skip_substrs,
617
+ )
618
+ loader.load_weights(model_weights.items())
619
+ self.model._build_fused_kv_buffers()
vllm/model_executor/models/qwen3_next.py
+6
-8
@@ -56,6 +56,7 @@ from vllm.sequence import IntermediateTensors
56
from vllm.transformers_utils.configs.qwen3_next import Qwen3NextConfig
57
58
from .interfaces import (
59
+ EagleModelMixin,
60
HasInnerState,
61
IsHybrid,
62
MixtureOfExperts,
@@ -454,7 +455,7 @@ class Qwen3NextDecoderLayer(nn.Module):
455
456
457
@support_torch_compile
457
-class Qwen3NextModel(nn.Module):
458
+class Qwen3NextModel(nn.Module, EagleModelMixin):
459
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
460
super().__init__()
461
@@ -492,8 +493,6 @@ class Qwen3NextModel(nn.Module):
493
else:
494
self.norm = PPMissingLayer()
495
495
- self.aux_hidden_state_layers: tuple[int, ...] = ()
496
-
496
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
497
return self.embed_tokens(input_ids)
498
@@ -515,20 +514,19 @@ class Qwen3NextModel(nn.Module):
514
hidden_states = intermediate_tensors["hidden_states"]
515
residual = intermediate_tensors["residual"]
516
518
- aux_hidden_states = []
517
+ aux_hidden_states = self._maybe_add_hidden_state([], 0, hidden_states, residual)
518
for layer_idx, layer in enumerate(
519
islice(self.layers, self.start_layer, self.end_layer),
520
start=self.start_layer,
521
):
523
- if layer_idx in self.aux_hidden_state_layers:
524
- aux_hidden_states.append(
525
- hidden_states + residual if residual is not None else hidden_states
526
- )
522
hidden_states, residual = layer(
523
positions=positions,
524
hidden_states=hidden_states,
525
residual=residual,
526
)
527
+ self._maybe_add_hidden_state(
528
+ aux_hidden_states, layer_idx + 1, hidden_states, residual
529
+ )
530
531
if not get_pp_group().is_last_rank:
532
return IntermediateTensors(
vllm/model_executor/models/registry.py
+1
@@ -546,6 +546,7 @@ _SPECULATIVE_DECODING_MODELS = {
546
"EagleLlamaForCausalLM": ("llama_eagle", "EagleLlamaForCausalLM"),
547
"EagleLlama4ForCausalLM": ("llama4_eagle", "EagleLlama4ForCausalLM"),
548
"EagleMiniCPMForCausalLM": ("minicpm_eagle", "EagleMiniCPMForCausalLM"),
549
+ "DFlashDraftModel": ("qwen3_dflash", "DFlashQwen3ForCausalLM"),
550
"Eagle3LlamaForCausalLM": ("llama_eagle3", "Eagle3LlamaForCausalLM"),
551
"LlamaForCausalLMEagle3": ("llama_eagle3", "Eagle3LlamaForCausalLM"),
552
"Eagle3Qwen2_5vlForCausalLM": ("llama_eagle3", "Eagle3LlamaForCausalLM"),
vllm/transformers_utils/configs/eagle.py
+12
-1
@@ -62,9 +62,20 @@ class EAGLEConfig(PretrainedConfig):
62
else f"Eagle3{arch}"
63
for arch in self.model.architectures
64
]
65
+ elif method == "dflash":
66
+ assert self.model is not None, (
67
+ "model should not be None when method is dflash"
68
+ )
69
+ kwargs["architectures"] = [
70
+ arch
71
+ if arch.startswith("DFlash") or arch.endswith("DFlash")
72
+ else f"DFlash{arch}"
73
+ for arch in self.model.architectures
74
+ ]
75
else:
76
raise ValueError(
67
- f"Invalid method {method}. Supported methods are eagle and eagle3."
77
+ f"Invalid method {method}. Supported methods are "
78
+ "eagle, eagle3, and dflash."
79
)
80
81
super().__init__(**kwargs)
vllm/v1/attention/backend.py
+14
@@ -220,6 +220,17 @@ class AttentionBackend(ABC):
220
def supports_per_head_quant_scales(cls) -> bool:
221
return False
222
223
+ @classmethod
224
+ def supports_non_causal(cls) -> bool:
225
+ """Check if backend supports non-causal (bidirectional) attention
226
+ for decoder models.
227
+
228
+ Unlike ENCODER_ONLY attention type which implies a different
229
+ execution model, this refers to non-causal attention within the
230
+ standard paged-KV-cache decoder path.
231
+ """
232
+ return False
233
+
234
@classmethod
235
def supports_attn_type(cls, attn_type: str) -> bool:
236
"""Check if backend supports a given attention type.
@@ -261,6 +272,7 @@ class AttentionBackend(ABC):
272
use_per_head_quant_scales: bool,
273
device_capability: "DeviceCapability",
274
attn_type: str,
275
+ use_non_causal: bool = False,
276
) -> list[str]:
277
invalid_reasons = []
278
if not cls.supports_head_size(head_size):
@@ -293,6 +305,8 @@ class AttentionBackend(ABC):
305
invalid_reasons.append("compute capability not supported")
306
if not cls.supports_attn_type(attn_type):
307
invalid_reasons.append(f"attention type {attn_type} not supported")
308
+ if use_non_causal and not cls.supports_non_causal():
309
+ invalid_reasons.append("non-causal attention not supported")
310
combination_reason = cls.supports_combination(
311
head_size,
312
dtype,
vllm/v1/attention/backends/flash_attn.py
+4
@@ -101,6 +101,10 @@ class FlashAttentionBackend(AttentionBackend):
101
def get_name() -> str:
102
return "FLASH_ATTN"
103
104
+ @classmethod
105
+ def supports_non_causal(cls) -> bool:
106
+ return True
107
+
108
@classmethod
109
def supports_attn_type(cls, attn_type: str) -> bool:
110
"""FlashAttention supports all attention types."""
vllm/v1/attention/selector.py
+9
-1
@@ -29,6 +29,7 @@ class AttentionSelectorConfig(NamedTuple):
29
use_mm_prefix: bool = False
30
use_per_head_quant_scales: bool = False
31
attn_type: str = AttentionType.DECODER
32
+ use_non_causal: bool = False
33
34
def __repr__(self):
35
return (
@@ -41,7 +42,8 @@ class AttentionSelectorConfig(NamedTuple):
42
f"use_sparse={self.use_sparse}, "
43
f"use_mm_prefix={self.use_mm_prefix}, "
44
f"use_per_head_quant_scales={self.use_per_head_quant_scales}, "
44
- f"attn_type={self.attn_type})"
45
+ f"attn_type={self.attn_type}, "
46
+ f"use_non_causal={self.use_non_causal})"
47
)
48
49
@@ -76,6 +78,11 @@ def get_attn_backend(
78
else:
79
block_size = None
80
81
+ speculative_config = vllm_config.speculative_config
82
+ use_non_causal = (
83
+ speculative_config is not None and speculative_config.method == "dflash"
84
+ )
85
+
86
attn_selector_config = AttentionSelectorConfig(
87
head_size=head_size,
88
dtype=dtype,
@@ -87,6 +94,7 @@ def get_attn_backend(
94
use_mm_prefix=use_mm_prefix,
95
use_per_head_quant_scales=use_per_head_quant_scales,
96
attn_type=attn_type or AttentionType.DECODER,
97
+ use_non_causal=use_non_causal,
98
)
99
100
return _cached_get_attn_backend(
vllm/v1/spec_decode/dflash.py
new
+282
@@ -0,0 +1,282 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from typing import Any
5
+
6
+import torch
7
+from typing_extensions import override
8
+
9
+from vllm.config import VllmConfig
10
+from vllm.forward_context import set_forward_context
11
+from vllm.logger import init_logger
12
+from vllm.triton_utils import triton
13
+from vllm.v1.attention.backend import CommonAttentionMetadata
14
+from vllm.v1.spec_decode.eagle import SpecDecodeBaseProposer
15
+from vllm.v1.spec_decode.utils import copy_and_expand_dflash_inputs_kernel
16
+
17
+logger = init_logger(__name__)
18
+
19
+
20
+class DFlashProposer(SpecDecodeBaseProposer):
21
+ def __init__(
22
+ self,
23
+ vllm_config: VllmConfig,
24
+ device: torch.device,
25
+ runner=None,
26
+ ):
27
+ assert vllm_config.speculative_config is not None
28
+ assert vllm_config.speculative_config.method == "dflash"
29
+ super().__init__(
30
+ vllm_config=vllm_config,
31
+ device=device,
32
+ pass_hidden_states_to_model=True,
33
+ runner=runner,
34
+ )
35
+
36
+ # Only next_token_ids and mask tokens are query tokens, all other context is K/V
37
+ self.max_query_tokens = self.max_batch_size * (1 + self.num_speculative_tokens)
38
+ # Positions covers both context states + query states
39
+ self.max_positions = self.max_num_tokens + self.max_query_tokens
40
+
41
+ # Separate context buffers to keep query buffer addresses stable for CUDA graphs
42
+ self._context_slot_mapping_buffer = torch.zeros(
43
+ self.max_num_tokens,
44
+ dtype=torch.int64,
45
+ device=device,
46
+ )
47
+ self._slot_mapping_buffer = torch.zeros(
48
+ self.max_query_tokens,
49
+ dtype=torch.int64,
50
+ device=device,
51
+ )
52
+ self._context_positions_buffer = torch.zeros(
53
+ self.max_num_tokens,
54
+ dtype=torch.int64,
55
+ device=device,
56
+ )
57
+ self.positions = torch.zeros(
58
+ self.max_query_tokens,
59
+ dtype=torch.int64,
60
+ device=device,
61
+ )
62
+
63
+ self.arange = torch.arange(
64
+ self.max_positions + 1, device=device, dtype=torch.int32
65
+ )
66
+
67
+ # For DFlash we use the input embeddings to embed the mask token
68
+ self.parallel_drafting_hidden_state_tensor = None
69
+
70
+ @override
71
+ def _raise_if_multimodal(self):
72
+ # Override to allow multimodal inputs since DFlash supports Qwen3.5 models
73
+ # Support for multimodal inputs has not been tested.
74
+ pass
75
+
76
+ @override
77
+ def set_inputs_first_pass(
78
+ self,
79
+ target_token_ids: torch.Tensor,
80
+ next_token_ids: torch.Tensor,
81
+ target_positions: torch.Tensor,
82
+ target_hidden_states: torch.Tensor,
83
+ token_indices_to_sample: torch.Tensor | None,
84
+ cad: CommonAttentionMetadata,
85
+ num_rejected_tokens_gpu: torch.Tensor | None,
86
+ ) -> tuple[int, torch.Tensor, CommonAttentionMetadata]:
87
+ # DFlash cross-attention: context K/V from target hidden states,
88
+ # Q from query embeddings (bonus + mask tokens).
89
+ batch_size = cad.batch_size()
90
+ num_context = target_token_ids.shape[0]
91
+ num_query_per_req = 1 + self.num_speculative_tokens
92
+ num_query_total = batch_size * num_query_per_req
93
+
94
+ # Store for build_model_inputs_first_pass to use
95
+ self._dflash_num_context = num_context
96
+
97
+ # We don't need to copy into a buffer here since the context preprocessing
98
+ # does not run in a CUDA graph
99
+ self._dflash_hidden_states = target_hidden_states
100
+
101
+ token_indices_to_sample = torch.empty(
102
+ batch_size * self.num_speculative_tokens,
103
+ dtype=torch.int32,
104
+ device=self.device,
105
+ )
106
+
107
+ # Launch fused triton kernel for input_ids, positions, slot_mapping,
108
+ # and token_indices_to_sample
109
+ max_ctx_per_req = cad.max_query_len
110
+ max_tokens_per_req = max_ctx_per_req + num_query_per_req
111
+ BLOCK_SIZE = min(256, triton.next_power_of_2(max_tokens_per_req))
112
+ num_blocks = triton.cdiv(max_tokens_per_req, BLOCK_SIZE)
113
+ grid = (batch_size, num_blocks)
114
+
115
+ has_num_rejected = num_rejected_tokens_gpu is not None
116
+ copy_and_expand_dflash_inputs_kernel[grid](
117
+ # Inputs
118
+ next_token_ids_ptr=next_token_ids,
119
+ target_positions_ptr=target_positions,
120
+ # Outputs
121
+ out_input_ids_ptr=self.input_ids,
122
+ out_context_positions_ptr=self._context_positions_buffer,
123
+ out_query_positions_ptr=self.positions,
124
+ out_context_slot_mapping_ptr=self._context_slot_mapping_buffer,
125
+ out_query_slot_mapping_ptr=self._slot_mapping_buffer,
126
+ out_token_indices_ptr=token_indices_to_sample,
127
+ # Block table
128
+ block_table_ptr=cad.block_table_tensor,
129
+ block_table_stride=cad.block_table_tensor.stride(0),
130
+ # Metadata
131
+ query_start_loc_ptr=cad.query_start_loc,
132
+ num_rejected_tokens_ptr=(
133
+ num_rejected_tokens_gpu if has_num_rejected else 0
134
+ ),
135
+ # Scalars
136
+ parallel_drafting_token_id=self.parallel_drafting_token_id,
137
+ block_size=self.block_size,
138
+ num_query_per_req=num_query_per_req,
139
+ num_speculative_tokens=self.num_speculative_tokens,
140
+ total_input_tokens=num_context,
141
+ BLOCK_SIZE=BLOCK_SIZE,
142
+ HAS_NUM_REJECTED=has_num_rejected,
143
+ )
144
+
145
+ query_slot_mapping = self._slot_mapping_buffer[:num_query_total]
146
+ new_query_start_loc = self.arange[: batch_size + 1] * num_query_per_req
147
+
148
+ # In padded mode, cad.seq_lens includes rejected tokens. Subtract
149
+ # them so attention only sees the valid prefix of context states.
150
+ effective_seq_lens = cad.seq_lens
151
+ if has_num_rejected:
152
+ effective_seq_lens = effective_seq_lens - num_rejected_tokens_gpu
153
+
154
+ new_cad = CommonAttentionMetadata(
155
+ query_start_loc=new_query_start_loc,
156
+ seq_lens=effective_seq_lens + num_query_per_req,
157
+ query_start_loc_cpu=(
158
+ torch.from_numpy(self.token_arange_np[: batch_size + 1]).clone()
159
+ * num_query_per_req
160
+ ),
161
+ _seq_lens_cpu=None,
162
+ _num_computed_tokens_cpu=None,
163
+ num_reqs=cad.num_reqs,
164
+ num_actual_tokens=num_query_total,
165
+ max_query_len=num_query_per_req,
166
+ max_seq_len=cad.max_seq_len + num_query_per_req,
167
+ block_table_tensor=cad.block_table_tensor,
168
+ slot_mapping=query_slot_mapping,
169
+ causal=False, # Non-causal attention is required for DFlash
170
+ )
171
+
172
+ return num_query_total, token_indices_to_sample, new_cad
173
+
174
+ @override
175
+ @torch.inference_mode()
176
+ def dummy_run(
177
+ self,
178
+ num_tokens: int,
179
+ use_cudagraphs: bool = True,
180
+ is_graph_capturing: bool = False,
181
+ slot_mappings: dict[str, torch.Tensor] | None = None,
182
+ ) -> None:
183
+ """
184
+ Key differences to default dummy_run:
185
+ - Only one forward pass due to parallel drafting
186
+ - DFlash uses context states as unpadded metadata, so hidden_states will
187
+ use the unpadded num_tokens instead of num_input_tokens
188
+ - max_query_tokens is quite small, DFlash only sees spec tokens as queries
189
+ - Multimodal inputs are not currently supported
190
+ """
191
+ num_query_tokens = min(num_tokens, self.max_query_tokens)
192
+ cudagraph_runtime_mode, num_input_tokens, num_tokens_across_dp = (
193
+ self._determine_batch_execution_and_padding(
194
+ num_query_tokens, use_cudagraphs=use_cudagraphs
195
+ )
196
+ )
197
+
198
+ # Slot mapping sized to num_input_tokens (query only), matching
199
+ # the K/V tensor size from the model forward. Context KVs are
200
+ # pre-inserted separately and don't flow through the model.
201
+ if (
202
+ self._draft_attn_layer_names
203
+ and slot_mappings is not None
204
+ and next(iter(self._draft_attn_layer_names)) in slot_mappings
205
+ ):
206
+ slot_mapping_dict = self._get_slot_mapping(num_input_tokens)
207
+ else:
208
+ slot_mapping_dict = slot_mappings or {}
209
+
210
+ # Context and query positions use separate buffers; no copy needed.
211
+ context_positions = self._context_positions_buffer[:num_tokens]
212
+ # Context states will be passed directly to the precomputation without
213
+ # going through the buffer, since no CUDA graph is used for the precomputation.
214
+ # For the dummy run, we use the dummy buffer.
215
+ context_states = self.hidden_states[:num_tokens]
216
+
217
+ # Run the KV projection (GEMM + norms + RoPE) for memory profiling,
218
+ self.model.precompute_and_store_context_kv(context_states, context_positions)
219
+ with set_forward_context(
220
+ None,
221
+ self.vllm_config,
222
+ num_tokens=num_input_tokens,
223
+ num_tokens_across_dp=num_tokens_across_dp,
224
+ cudagraph_runtime_mode=cudagraph_runtime_mode,
225
+ slot_mapping=slot_mapping_dict,
226
+ ):
227
+ self.model(
228
+ input_ids=self.input_ids[:num_input_tokens],
229
+ positions=self._get_positions(num_input_tokens),
230
+ inputs_embeds=None,
231
+ )
232
+
233
+ @override
234
+ def build_model_inputs_first_pass(
235
+ self,
236
+ num_tokens: int,
237
+ num_input_tokens: int,
238
+ mm_embed_inputs: tuple[list[torch.Tensor], torch.Tensor] | None,
239
+ ) -> tuple[dict[str, Any], int]:
240
+ # Context and query positions/slots were written to separate
241
+ # buffers by the kernel — no copy needed.
242
+ num_context = self._dflash_num_context
243
+
244
+ # Pre-insert context KVs directly into cache
245
+ self.model.precompute_and_store_context_kv(
246
+ self._dflash_hidden_states, # Shape is already [num_context, hidden_size]
247
+ self._context_positions_buffer[:num_context],
248
+ self._context_slot_mapping_buffer[:num_context],
249
+ )
250
+ return (
251
+ dict(
252
+ input_ids=self.input_ids[:num_input_tokens],
253
+ positions=self._get_positions(num_input_tokens),
254
+ inputs_embeds=None,
255
+ ),
256
+ num_input_tokens,
257
+ )
258
+
259
+ @override
260
+ def build_per_layer_attn_metadata(
261
+ self, cad: CommonAttentionMetadata, draft_index: int = 0
262
+ ) -> dict[str, object]:
263
+ per_layer_attention_metadata = super().build_per_layer_attn_metadata(
264
+ cad, draft_index
265
+ )
266
+ for layer_name, attn_metadata in per_layer_attention_metadata.items():
267
+ assert getattr(attn_metadata, "causal", None) is False, (
268
+ f"Attention metadata for layer {layer_name} does not have"
269
+ " non-causal support, which is required for DFlash."
270
+ " Consider using a different attention backend, such as FlashAttention."
271
+ )
272
+ return per_layer_attention_metadata
273
+
274
+ @override
275
+ def _get_eagle3_use_aux_hidden_state_from_config(self):
276
+ use_aux_hidden_state = True
277
+ dflash_config = getattr(
278
+ self.draft_model_config.hf_config, "dflash_config", None
279
+ )
280
+ if dflash_config is not None:
281
+ use_aux_hidden_state = dflash_config.get("use_aux_hidden_state", True)
282
+ return use_aux_hidden_state
vllm/v1/spec_decode/eagle.py
+119
-72
@@ -2,7 +2,7 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import ast
4
from importlib.util import find_spec
5
-from typing import cast
5
+from typing import Any, cast
6
7
import numpy as np
8
import torch
@@ -23,6 +23,7 @@ from vllm.model_executor.models import supports_multimodal
23
from vllm.model_executor.models.deepseek_eagle3 import Eagle3DeepseekV2ForCausalLM
24
from vllm.model_executor.models.interfaces import SupportsMultiModal
25
from vllm.model_executor.models.llama_eagle3 import Eagle3LlamaForCausalLM
26
+from vllm.model_executor.models.qwen3_dflash import DFlashQwen3ForCausalLM
27
from vllm.multimodal import MULTIMODAL_REGISTRY
28
from vllm.platforms import current_platform
29
from vllm.triton_utils import triton
@@ -83,13 +84,15 @@ class SpecDecodeBaseProposer:
84
self.hidden_size = self.draft_model_config.get_hidden_size()
85
self.inputs_embeds_size = self.draft_model_config.get_inputs_embeds_size()
86
86
- # Unifying eagle, draft model, and parallel drafting support
87
+ # Unifying eagle, draft model, and parallel drafting support.
88
+ # DFlash always uses parallel drafting (all tokens in one pass),
89
+ # but has an additional slot for the next_token_id (does not shift like EAGLE)
90
self.parallel_drafting: bool = self.speculative_config.parallel_drafting
91
self.extra_slots_per_request = (
92
1 if not self.parallel_drafting else self.num_speculative_tokens
93
)
94
self.net_num_new_slots_per_request = self.extra_slots_per_request - (
92
- 1 if self.pass_hidden_states_to_model else 0
95
+ 1 if (self.pass_hidden_states_to_model and self.method != "dflash") else 0
96
)
97
self.needs_extra_input_slots = self.net_num_new_slots_per_request > 0
98
@@ -101,10 +104,14 @@ class SpecDecodeBaseProposer:
104
self.speculative_config.use_local_argmax_reduction
105
)
106
104
- max_batch_size = vllm_config.scheduler_config.max_num_seqs
107
+ self.max_batch_size = vllm_config.scheduler_config.max_num_seqs
108
self.max_num_tokens = vllm_config.scheduler_config.max_num_batched_tokens
109
self.token_arange_np = np.arange(self.max_num_tokens)
110
111
+ # Can be specialized by methods like DFlash to reduce the limit
112
+ self.max_query_tokens = self.max_num_tokens
113
+ self.max_positions = self.max_num_tokens
114
+
115
# Multi-modal data support
116
self.mm_registry = MULTIMODAL_REGISTRY
117
self.supports_mm_inputs = self.mm_registry.supports_multimodal_inputs(
@@ -146,18 +153,20 @@ class SpecDecodeBaseProposer:
153
# 1D-RoPE.
154
# See page 5 of https://arxiv.org/abs/2409.12191
155
self.mrope_positions = torch.zeros(
149
- (3, self.max_num_tokens + 1), dtype=torch.int64, device=device
156
+ (3, self.max_positions + 1), dtype=torch.int64, device=device
157
)
158
elif self.uses_xdrope_dim > 0 and self.draft_uses_xdrope_dim > 0:
159
self.xdrope_positions = torch.zeros(
153
- (self.uses_xdrope_dim, self.max_num_tokens + 1),
160
+ (self.uses_xdrope_dim, self.max_positions + 1),
161
dtype=torch.int64,
162
device=device,
163
)
164
else:
165
# RoPE need (max_num_tokens,)
166
self.positions = torch.zeros(
160
- self.max_num_tokens, dtype=torch.int64, device=device
167
+ self.max_positions,
168
+ dtype=torch.int64,
169
+ device=device,
170
)
171
self.hidden_states = torch.zeros(
172
(self.max_num_tokens, self.hidden_size), dtype=self.dtype, device=device
@@ -168,7 +177,7 @@ class SpecDecodeBaseProposer:
177
178
# We need +1 here because the arange is used to set query_start_loc,
179
# which has one more element than batch_size.
171
- max_num_slots_for_arange = max(max_batch_size + 1, self.max_num_tokens)
180
+ max_num_slots_for_arange = max(self.max_batch_size + 1, self.max_num_tokens)
181
self.arange = torch.arange(
182
max_num_slots_for_arange, device=device, dtype=torch.int32
183
)
@@ -200,7 +209,7 @@ class SpecDecodeBaseProposer:
209
)
210
211
self.backup_next_token_ids = CpuGpuBuffer(
203
- max_batch_size,
212
+ self.max_batch_size,
213
dtype=torch.int32,
214
pin_memory=is_pin_memory_available(),
215
device=device,
@@ -208,7 +217,9 @@ class SpecDecodeBaseProposer:
217
)
218
219
self._slot_mapping_buffer = torch.zeros(
211
- self.max_num_tokens, dtype=torch.int64, device=device
220
+ self.max_positions,
221
+ dtype=torch.int64,
222
+ device=device,
223
)
224
225
# Determine allowed attention backends once during initialization.
@@ -275,7 +286,7 @@ class SpecDecodeBaseProposer:
286
# Precompute draft position offsets in flattened tree.
287
self.tree_draft_pos_offsets = torch.arange(
288
1, len(self.tree_choices) + 1, device=device, dtype=torch.int32
278
- ).repeat(max_batch_size, 1)
289
+ ).repeat(self.max_batch_size, 1)
290
291
def _raise_if_padded_drafter_batch_disabled(self):
292
if self.speculative_config.disable_padded_drafter_batch:
@@ -305,14 +316,19 @@ class SpecDecodeBaseProposer:
316
# for those masked slots.
317
318
model_hf_config = self.draft_model_config.hf_config
308
- if hasattr(model_hf_config, "pard_token"):
319
+ # DFlash stores mask_token_id in dflash_config
320
+ dflash_config = getattr(model_hf_config, "dflash_config", None)
321
+ if dflash_config and "mask_token_id" in dflash_config:
322
+ self.parallel_drafting_token_id = dflash_config["mask_token_id"]
323
+ elif hasattr(model_hf_config, "pard_token"):
324
self.parallel_drafting_token_id = model_hf_config.pard_token
325
elif hasattr(model_hf_config, "ptd_token_id"):
326
self.parallel_drafting_token_id = model_hf_config.ptd_token_id
327
else:
328
raise ValueError(
329
"For parallel drafting, the draft model config must have "
315
- "`pard_token` or `ptd_token_id` specified in its config.json."
330
+ "`pard_token`, `ptd_token_id`, or "
331
+ "`dflash_config.mask_token_id` specified in its config.json."
332
)
333
334
if self.pass_hidden_states_to_model:
@@ -402,9 +418,14 @@ class SpecDecodeBaseProposer:
418
) -> torch.Tensor:
419
batch_size = common_attn_metadata.batch_size()
420
405
- if self.method == "eagle3":
421
+ if self.method in ("eagle3", "dflash"):
422
assert isinstance(
407
- self.model, (Eagle3LlamaForCausalLM, Eagle3DeepseekV2ForCausalLM)
423
+ self.model,
424
+ (
425
+ Eagle3LlamaForCausalLM,
426
+ Eagle3DeepseekV2ForCausalLM,
427
+ DFlashQwen3ForCausalLM,
428
+ ),
429
)
430
target_hidden_states = self.model.combine_hidden_states(
431
target_hidden_states
@@ -423,40 +444,17 @@ class SpecDecodeBaseProposer:
444
)
445
)
446
426
- per_layer_attn_metadata: dict[str, object] = {}
427
- for attn_group in self.draft_attn_groups:
428
- attn_metadata = attn_group.get_metadata_builder().build_for_drafting(
429
- common_attn_metadata=common_attn_metadata, draft_index=0
430
- )
431
- for layer_name in attn_group.layer_names:
432
- per_layer_attn_metadata[layer_name] = attn_metadata
447
+ per_layer_attn_metadata = self.build_per_layer_attn_metadata(
448
+ common_attn_metadata
449
+ )
450
451
cudagraph_runtime_mode, num_input_tokens, num_tokens_across_dp = (
452
self._determine_batch_execution_and_padding(num_tokens)
453
)
454
438
- if self.supports_mm_inputs:
439
- mm_embeds, is_mm_embed = mm_embed_inputs or (None, None)
440
-
441
- self.inputs_embeds[:num_tokens] = self.model.embed_input_ids(
442
- self.input_ids[:num_tokens],
443
- multimodal_embeddings=mm_embeds,
444
- is_multimodal=is_mm_embed,
445
- )
446
-
447
- input_ids = None
448
- inputs_embeds = self.inputs_embeds[:num_input_tokens]
449
- else:
450
- input_ids = self.input_ids[:num_input_tokens]
451
- inputs_embeds = None
452
-
453
- model_kwargs = {
454
- "input_ids": input_ids,
455
- "positions": self._get_positions(num_input_tokens),
456
- "inputs_embeds": inputs_embeds,
457
- }
458
- if self.pass_hidden_states_to_model:
459
- model_kwargs["hidden_states"] = self.hidden_states[:num_input_tokens]
455
+ model_kwargs, slot_mapping_size = self.build_model_inputs_first_pass(
456
+ num_tokens, num_input_tokens, mm_embed_inputs
457
+ )
458
459
with set_forward_context(
460
per_layer_attn_metadata,
@@ -465,7 +463,7 @@ class SpecDecodeBaseProposer:
463
num_tokens_across_dp=num_tokens_across_dp,
464
cudagraph_runtime_mode=cudagraph_runtime_mode,
465
slot_mapping=self._get_slot_mapping(
468
- num_input_tokens, common_attn_metadata.slot_mapping
466
+ slot_mapping_size, common_attn_metadata.slot_mapping
467
),
468
):
469
ret_hidden_states = self.model(**model_kwargs)
@@ -488,7 +486,10 @@ class SpecDecodeBaseProposer:
486
positions = self.positions[token_indices_to_sample]
487
hidden_states = hidden_states[token_indices_to_sample]
488
491
- if isinstance(attn_metadata, TreeAttentionMetadata):
489
+ if any(
490
+ isinstance(attn_metadata, TreeAttentionMetadata)
491
+ for attn_metadata in per_layer_attn_metadata.values()
492
+ ):
493
# Draft using tree attention - requires full logits for top-k
494
logits = self.model.compute_logits(sample_hidden_states)
495
draft_token_ids_list = self.propose_tree(
@@ -504,15 +505,16 @@ class SpecDecodeBaseProposer:
505
506
draft_token_ids = self._greedy_sample(sample_hidden_states)
507
507
- if self.allowed_attn_types is not None and not isinstance(
508
- attn_metadata, self.allowed_attn_types
509
- ):
510
- raise ValueError(
511
- f"Unsupported attention metadata type for speculative "
512
- "decoding with num_speculative_tokens > 1: "
513
- f"{type(attn_metadata)}. Supported types are: "
514
- f"{self.allowed_attn_types}"
515
- )
508
+ for attn_metadata in per_layer_attn_metadata.values():
509
+ if self.allowed_attn_types is not None and not isinstance(
510
+ attn_metadata, self.allowed_attn_types
511
+ ):
512
+ raise ValueError(
513
+ f"Unsupported attention metadata type for speculative "
514
+ "decoding with num_speculative_tokens > 1: "
515
+ f"{type(attn_metadata)}. Supported types are: "
516
+ f"{self.allowed_attn_types}"
517
+ )
518
519
# Generate the remaining draft tokens.
520
draft_token_ids_list = [draft_token_ids]
@@ -593,13 +595,9 @@ class SpecDecodeBaseProposer:
595
common_attn_metadata._num_computed_tokens_cpu += 1
596
597
# Rebuild attention metadata
596
- for attn_group in self.draft_attn_groups:
597
- attn_metadata = attn_group.get_metadata_builder().build_for_drafting(
598
- common_attn_metadata=common_attn_metadata,
599
- draft_index=token_index + 1,
600
- )
601
- for layer_name in attn_group.layer_names:
602
- per_layer_attn_metadata[layer_name] = attn_metadata
598
+ per_layer_attn_metadata = self.build_per_layer_attn_metadata(
599
+ common_attn_metadata, draft_index=token_index + 1
600
+ )
601
602
# copy inputs to buffer for cudagraph
603
self.input_ids[:batch_size] = input_ids
@@ -780,8 +778,51 @@ class SpecDecodeBaseProposer:
778
779
return total_num_output_tokens, token_indices_to_sample, new_cad
780
781
+ def build_model_inputs_first_pass(
782
+ self,
783
+ num_tokens: int,
784
+ num_input_tokens: int,
785
+ mm_embed_inputs: tuple[list[torch.Tensor], torch.Tensor] | None,
786
+ ) -> tuple[dict[str, Any], int]:
787
+ if self.supports_mm_inputs:
788
+ mm_embeds, is_mm_embed = mm_embed_inputs or (None, None)
789
+
790
+ self.inputs_embeds[:num_tokens] = self.model.embed_input_ids(
791
+ self.input_ids[:num_tokens],
792
+ multimodal_embeddings=mm_embeds,
793
+ is_multimodal=is_mm_embed,
794
+ )
795
+
796
+ input_ids = None
797
+ inputs_embeds = self.inputs_embeds[:num_input_tokens]
798
+ else:
799
+ input_ids = self.input_ids[:num_input_tokens]
800
+ inputs_embeds = None
801
+
802
+ model_kwargs = {
803
+ "input_ids": input_ids,
804
+ "positions": self._get_positions(num_input_tokens),
805
+ "inputs_embeds": inputs_embeds,
806
+ }
807
+ if self.pass_hidden_states_to_model:
808
+ model_kwargs["hidden_states"] = self.hidden_states[:num_input_tokens]
809
+
810
+ return model_kwargs, num_input_tokens
811
+
812
+ def build_per_layer_attn_metadata(
813
+ self, common_attn_metadata: CommonAttentionMetadata, draft_index: int = 0
814
+ ) -> dict[str, object]:
815
+ per_layer_attn_metadata: dict[str, object] = {}
816
+ for attn_group in self.draft_attn_groups:
817
+ attn_metadata = attn_group.get_metadata_builder().build_for_drafting(
818
+ common_attn_metadata=common_attn_metadata, draft_index=draft_index
819
+ )
820
+ for layer_name in attn_group.layer_names:
821
+ per_layer_attn_metadata[layer_name] = attn_metadata
822
+ return per_layer_attn_metadata
823
+
824
def model_returns_tuple(self) -> bool:
784
- return self.method not in ("mtp", "draft_model")
825
+ return self.method not in ("mtp", "draft_model", "dflash")
826
827
def prepare_next_token_ids_cpu(
828
self,
@@ -1310,15 +1351,20 @@ class SpecDecodeBaseProposer:
1351
self._maybe_share_embeddings(target_language_model)
1352
self._maybe_share_lm_head(target_language_model)
1353
1313
- if self.parallel_drafting and self.pass_hidden_states_to_model:
1314
- assert self.parallel_drafting_hidden_state_tensor is not None
1315
- self.parallel_drafting_hidden_state_tensor.copy_(
1316
- self.model.combine_hidden_states(
1317
- self.model.mask_hidden.view(3 * self.hidden_size)
1354
+ if (
1355
+ self.parallel_drafting
1356
+ and self.pass_hidden_states_to_model
1357
+ and self.parallel_drafting_hidden_state_tensor is not None
1358
+ ):
1359
+ flat_mask = self.model.mask_hidden.view(-1)
1360
+ if self.eagle3_use_aux_hidden_state:
1361
+ # EAGLE3: mask_hidden stores all aux hidden states,
1362
+ # project through combine_hidden_states
1363
+ self.parallel_drafting_hidden_state_tensor.copy_(
1364
+ self.model.combine_hidden_states(flat_mask)
1365
)
1319
- if self.eagle3_use_aux_hidden_state
1320
- else self.model.mask_hidden.view(self.hidden_size)
1321
- )
1366
+ else:
1367
+ self.parallel_drafting_hidden_state_tensor.copy_(flat_mask)
1368
1369
def _maybe_share_embeddings(self, target_language_model: nn.Module) -> None:
1370
"""
@@ -1493,8 +1539,9 @@ class SpecDecodeBaseProposer:
1539
) -> None:
1540
# FIXME: when using tree-based specdec, adjust number of forward-passes
1541
# according to the depth of the tree.
1542
+ only_one_forward_pass = is_graph_capturing or self.parallel_drafting
1543
for fwd_idx in range(
1497
- self.num_speculative_tokens if not is_graph_capturing else 1
1544
+ 1 if only_one_forward_pass else self.num_speculative_tokens
1545
):
1546
if fwd_idx <= 1:
1547
cudagraph_runtime_mode, num_input_tokens, num_tokens_across_dp = (
vllm/v1/spec_decode/utils.py
+108
@@ -441,6 +441,114 @@ def copy_and_expand_eagle_inputs_kernel(
441
)
442
443
444
+@triton.jit
445
+def copy_and_expand_dflash_inputs_kernel(
446
+ # Inputs
447
+ next_token_ids_ptr, # [num_reqs]
448
+ target_positions_ptr, # [num_context]
449
+ # Outputs
450
+ out_input_ids_ptr, # [num_query_total] (output)
451
+ out_context_positions_ptr, # [num_context] (output)
452
+ out_query_positions_ptr, # [num_query_total] (output)
453
+ out_context_slot_mapping_ptr, # [num_context] (output)
454
+ out_query_slot_mapping_ptr, # [num_query_total] (output)
455
+ out_token_indices_ptr, # [num_reqs * num_speculative_tokens] (output)
456
+ # Block table
457
+ block_table_ptr, # [max_reqs, max_blocks]
458
+ block_table_stride, # stride of block_table dim 0 (in elements)
459
+ # Metadata
460
+ query_start_loc_ptr, # [num_reqs + 1]
461
+ num_rejected_tokens_ptr, # [num_reqs] or null (0) when not padded
462
+ # Scalars
463
+ parallel_drafting_token_id, # tl.int32
464
+ block_size, # tl.int32
465
+ num_query_per_req, # tl.int32
466
+ num_speculative_tokens, # tl.int32
467
+ total_input_tokens, # tl.int32
468
+ BLOCK_SIZE: tl.constexpr,
469
+ HAS_NUM_REJECTED: tl.constexpr = False,
470
+):
471
+ """
472
+ Fused kernel for DFlash first-pass input setup.
473
+
474
+ Per request, this kernel:
475
+ 1. Copies context positions from target_positions to
476
+ out_context_positions.
477
+ 2. Computes query positions (last_target_pos + 1 + offset) and writes
478
+ them to out_query_positions.
479
+ 3. Writes input_ids for query tokens: [next_token, mask, mask, ...].
480
+ 4. Computes slot_mapping for context and query positions into separate
481
+ buffers via block_table lookup.
482
+ 5. Writes token_indices_to_sample for the mask (speculative) tokens.
483
+ """
484
+ req_idx = tl.program_id(axis=0)
485
+ block_idx = tl.program_id(axis=1)
486
+
487
+ # Load context token range for this request
488
+ ctx_start = tl.load(query_start_loc_ptr + req_idx)
489
+ ctx_end = tl.load(query_start_loc_ptr + req_idx + 1)
490
+ num_ctx = ctx_end - ctx_start
491
+ total_tokens = num_ctx + num_query_per_req
492
+
493
+ j = block_idx * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
494
+ in_bounds = j < total_tokens
495
+ is_ctx = j < num_ctx
496
+ is_query = (~is_ctx) & in_bounds
497
+ query_off = j - num_ctx # offset within query portion (0-indexed)
498
+
499
+ # --- Positions ---
500
+ # Context: load from target_positions
501
+ ctx_pos_idx = tl.minimum(ctx_start + j, total_input_tokens - 1)
502
+ ctx_pos = tl.load(target_positions_ptr + ctx_pos_idx, mask=is_ctx, other=0)
503
+
504
+ # Query: last_valid_pos + 1 + query_off
505
+ # In padded mode, ctx_end includes rejected tokens; use valid_ctx_end
506
+ # to find the last accepted context position.
507
+ if HAS_NUM_REJECTED:
508
+ num_rejected = tl.load(num_rejected_tokens_ptr + req_idx)
509
+ valid_ctx_end = ctx_end - num_rejected
510
+ else:
511
+ valid_ctx_end = ctx_end
512
+ last_pos = tl.load(target_positions_ptr + valid_ctx_end - 1)
513
+ query_pos = last_pos + 1 + query_off
514
+
515
+ positions = tl.where(is_ctx, ctx_pos, query_pos)
516
+
517
+ # Context and query positions go to separate buffers.
518
+ ctx_pos_out = ctx_start + j
519
+ tl.store(out_context_positions_ptr + ctx_pos_out, ctx_pos, mask=is_ctx)
520
+ query_out = req_idx * num_query_per_req + query_off
521
+ tl.store(out_query_positions_ptr + query_out, query_pos, mask=is_query)
522
+
523
+ # --- Slot mapping (block_table lookup for all positions) ---
524
+ block_num = positions // block_size
525
+ # # Clamp block_number to avoid OOB when position is at max
526
+ block_num = tl.minimum(block_num, block_table_stride - 1)
527
+ block_id = tl.load(
528
+ block_table_ptr + req_idx * block_table_stride + block_num,
529
+ mask=in_bounds,
530
+ other=0,
531
+ ).to(tl.int64)
532
+ slot = block_id * block_size + (positions % block_size)
533
+ tl.store(out_context_slot_mapping_ptr + ctx_pos_out, slot, mask=is_ctx)
534
+ tl.store(out_query_slot_mapping_ptr + query_out, slot, mask=is_query)
535
+
536
+ # --- Input IDs (query tokens only) ---
537
+ bonus_token = tl.load(next_token_ids_ptr + req_idx)
538
+ is_bonus = is_query & (query_off == 0)
539
+ input_id = tl.where(is_bonus, bonus_token, parallel_drafting_token_id)
540
+ tl.store(out_input_ids_ptr + query_out, input_id, mask=is_query)
541
+
542
+ # --- Token indices to sample (mask tokens, skip the bonus token) ---
543
+ is_sample = is_query & (query_off > 0)
544
+ sample_out_idx = req_idx * num_speculative_tokens + (query_off - 1)
545
+ tl.store(
546
+ out_token_indices_ptr + sample_out_idx,
547
+ query_out,
548
+ mask=is_sample,
549
+ )
550
+
551
+
552
@torch.compile(dynamic=True, backend=current_platform.simple_compile_backend)
553
def update_num_computed_tokens_for_batch_change(
554
num_computed_tokens: torch.Tensor,
vllm/v1/worker/gpu_model_runner.py
+35
-10
@@ -160,6 +160,7 @@ from vllm.v1.sample.logits_processor.interface import LogitsProcessor
160
from vllm.v1.sample.metadata import SamplingMetadata
161
from vllm.v1.sample.rejection_sampler import RejectionSampler
162
from vllm.v1.sample.sampler import Sampler
163
+from vllm.v1.spec_decode.dflash import DFlashProposer
164
from vllm.v1.spec_decode.draft_model import DraftModelProposer
165
from vllm.v1.spec_decode.eagle import EagleProposer
166
from vllm.v1.spec_decode.extract_hidden_states import ExtractHiddenStatesProposer
@@ -515,6 +516,7 @@ class GPUModelRunner(
516
| NgramProposerGPU
517
| SuffixDecodingProposer
518
| EagleProposer
519
+ | DFlashProposer
520
| DraftModelProposer
521
| MedusaProposer
522
| ExtractHiddenStatesProposer
@@ -546,6 +548,9 @@ class GPUModelRunner(
548
self._ngram_pinned_val_buf = torch.zeros(
549
self.max_num_reqs, dtype=torch.int32, pin_memory=True
550
)
551
+ elif self.speculative_config.use_dflash():
552
+ self.drafter = DFlashProposer(self.vllm_config, self.device, self)
553
+ self.use_aux_hidden_state_outputs = True
554
elif self.speculative_config.method == "suffix":
555
self.drafter = SuffixDecodingProposer(self.vllm_config)
556
elif self.speculative_config.use_eagle():
@@ -2289,7 +2294,7 @@ class GPUModelRunner(
2294
cm.slot_mapping = slot_mappings[kv_cache_gid]
2295
2296
if self.speculative_config and spec_decode_common_attn_metadata is None:
2292
- if isinstance(self.drafter, EagleProposer):
2297
+ if isinstance(self.drafter, (EagleProposer, DFlashProposer)):
2298
if self.drafter.kv_cache_gid == kv_cache_gid:
2299
spec_decode_common_attn_metadata = cm
2300
else:
@@ -4202,7 +4207,10 @@ class GPUModelRunner(
4207
# as inputs, and does not need to wait for bookkeeping to finish.
4208
assert isinstance(
4209
self.drafter,
4205
- EagleProposer | DraftModelProposer | ExtractHiddenStatesProposer,
4210
+ EagleProposer
4211
+ | DFlashProposer
4212
+ | DraftModelProposer
4213
+ | ExtractHiddenStatesProposer,
4214
)
4215
sampled_token_ids = sampler_output.sampled_token_ids
4216
if input_fits_in_drafter:
@@ -4589,8 +4597,14 @@ class GPUModelRunner(
4597
next_token_ids, valid_sampled_tokens_count
4598
)
4599
4592
- elif spec_config.use_eagle() or spec_config.uses_draft_model():
4593
- assert isinstance(self.drafter, EagleProposer | DraftModelProposer)
4600
+ elif (
4601
+ spec_config.use_eagle()
4602
+ or spec_config.use_dflash()
4603
+ or spec_config.uses_draft_model()
4604
+ ):
4605
+ assert isinstance(
4606
+ self.drafter, EagleProposer | DFlashProposer | DraftModelProposer
4607
+ )
4608
4609
if spec_config.disable_padded_drafter_batch:
4610
# When padded-batch is disabled, the sampled_token_ids should be
@@ -4889,10 +4903,13 @@ class GPUModelRunner(
4903
return None
4904
4905
hf_config = self.speculative_config.draft_model_config.hf_config
4892
- if not hasattr(hf_config, "eagle_aux_hidden_state_layer_ids"):
4893
- return None
4906
4895
- layer_ids = hf_config.eagle_aux_hidden_state_layer_ids
4907
+ layer_ids = getattr(hf_config, "eagle_aux_hidden_state_layer_ids", None)
4908
+ if not layer_ids:
4909
+ dflash_config = getattr(hf_config, "dflash_config", None)
4910
+ if dflash_config and isinstance(dflash_config, dict):
4911
+ layer_ids = dflash_config.get("target_layer_ids")
4912
+
4913
if layer_ids and isinstance(layer_ids, (list, tuple)):
4914
return tuple(layer_ids)
4915
@@ -5479,7 +5496,10 @@ class GPUModelRunner(
5496
):
5497
assert isinstance(
5498
self.drafter,
5482
- EagleProposer | DraftModelProposer | ExtractHiddenStatesProposer,
5499
+ EagleProposer
5500
+ | DFlashProposer
5501
+ | DraftModelProposer
5502
+ | ExtractHiddenStatesProposer,
5503
)
5504
assert self.speculative_config is not None
5505
# Eagle currently only supports PIECEWISE cudagraphs.
@@ -6236,7 +6256,9 @@ class GPUModelRunner(
6256
self.speculative_config.use_eagle()
6257
or self.speculative_config.uses_draft_model()
6258
):
6239
- assert isinstance(self.drafter, EagleProposer | DraftModelProposer)
6259
+ assert isinstance(
6260
+ self.drafter, EagleProposer | DFlashProposer | DraftModelProposer
6261
+ )
6262
self.drafter.initialize_attn_backend(kv_cache_config, kernel_block_sizes)
6263
6264
def _check_and_update_cudagraph_mode(
@@ -6420,7 +6442,10 @@ class GPUModelRunner(
6442
self.speculative_config.use_eagle()
6443
or self.speculative_config.uses_extract_hidden_states()
6444
):
6423
- assert isinstance(self.drafter, EagleProposer | ExtractHiddenStatesProposer)
6445
+ assert isinstance(
6446
+ self.drafter,
6447
+ EagleProposer | DFlashProposer | ExtractHiddenStatesProposer,
6448
+ )
6449
self.drafter.initialize_cudagraph_keys(cudagraph_mode)
6450
6451
def calculate_reorder_batch_threshold(self) -> None: