Skip to content

Commit 5716900

Browse files
[Spec Decode] Tighten confidence allocation
Keep draft and verifier costs as separate one-dimensional tables, compute current confidence ordering only on TP rank zero, reduce manager staging state, and retain two request-count CUDA graph tiers. Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>
1 parent 0fc06da commit 5716900

9 files changed

Lines changed: 175 additions & 264 deletions

File tree

tests/evals/gsm8k/configs/DeepSeek-V4-Flash-DSpark-confidence-TP4.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ server_args: >-
1818
--speculative-config '{"method":"dspark",
1919
"model":"deepseek-ai/DeepSeek-V4-Flash-DSpark",
2020
"attention_backend":"FLASH_ATTN","num_speculative_tokens":7,
21-
"draft_sample_method":"probabilistic","dspark_confidence_threshold":0.0,
22-
"dspark_budget_frac":0.5,"confidence_based_verification":"auto"}'
21+
"draft_sample_method":"probabilistic","dspark_sps_curve":"auto",
22+
"confidence_based_verification":"auto"}'

tests/test_config.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1576,11 +1576,9 @@ def test_dspark_confidence_config_validation():
15761576
config = SpeculativeConfig(
15771577
method="ngram",
15781578
num_speculative_tokens=1,
1579-
dspark_confidence_threshold=0.25,
15801579
dspark_budget_frac=0.5,
15811580
confidence_based_verification="mask",
15821581
)
1583-
assert config.dspark_confidence_threshold == 0.25
15841582
assert config.dspark_budget_frac == 0.5
15851583
assert config.confidence_based_verification == "mask"
15861584
with pytest.raises(ValidationError, match="confidence_based_verification"):
@@ -1594,8 +1592,6 @@ def test_dspark_confidence_config_validation():
15941592
@pytest.mark.parametrize(
15951593
("field", "value"),
15961594
[
1597-
("dspark_confidence_threshold", -0.1),
1598-
("dspark_confidence_threshold", 1.1),
15991595
("dspark_budget_frac", 0.0),
16001596
("dspark_budget_frac", 1.1),
16011597
],

tests/v1/spec_decode/test_dspark_confidence.py

Lines changed: 24 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from vllm.v1.worker.gpu.spec_decode.confidence import ( # noqa: E402
3333
MaskedConfidenceManager,
3434
VarlenConfidenceManager,
35-
build_sps_table_from_costs,
35+
build_cost_tables_from_curves,
3636
compute_prefix_survival,
3737
make_confidence_manager,
3838
select_draft_token_budget,
@@ -45,7 +45,6 @@
4545

4646
def _spec_config(**overrides: Any) -> SimpleNamespace:
4747
config = SimpleNamespace(
48-
dspark_confidence_threshold=0.0,
4948
dspark_budget_frac=1.0,
5049
dspark_sps_curve=None,
5150
dspark_online_sts=False,
@@ -62,6 +61,7 @@ def _logit(probs: np.ndarray) -> np.ndarray:
6261
@pytest.fixture(autouse=True)
6362
def _single_rank_tp_group(monkeypatch):
6463
group = SimpleNamespace(
64+
rank_in_group=0,
6565
broadcast=lambda tensor, src=0: tensor,
6666
broadcast_object=lambda value, src=0: value,
6767
)
@@ -95,9 +95,7 @@ def test_auto_verification_selects_supported_manager(cg_support, expected_type):
9595
manager = make_confidence_manager(
9696
"auto",
9797
attn_cg_support,
98-
max_num_tokens=8,
9998
req_states=req_states,
100-
device=device,
10199
speculative_config=_spec_config(),
102100
)
103101

@@ -107,7 +105,6 @@ def test_auto_verification_selects_supported_manager(cg_support, expected_type):
107105
def test_masked_verification_does_not_auto_profile_sps():
108106
device = torch.device("cuda")
109107
manager = MaskedConfidenceManager(
110-
max_num_tokens=8,
111108
req_states=RequestState(
112109
max_num_reqs=2,
113110
max_model_len=4,
@@ -116,12 +113,12 @@ def test_masked_verification_does_not_auto_profile_sps():
116113
vocab_size=16,
117114
device=device,
118115
),
119-
device=device,
120116
speculative_config=_spec_config(dspark_sps_curve="auto"),
121117
)
122118

123119
assert not manager.wants_auto_sps_curve
124-
assert manager.sps_table_np is None
120+
assert manager.draft_cost_ms is None
121+
assert manager.verify_and_sample_cost_ms is None
125122

126123

127124
def test_prepare_pos_seq_lens_clears_active_padding():
@@ -196,32 +193,16 @@ def test_varlen_indexer_indices_use_device_lengths():
196193
assert indices.cpu().tolist() == [0, 1, 1, 2, 3]
197194

198195

199-
def test_select_budget_uses_global_prefix_order():
200-
survival = compute_prefix_survival(
201-
_logit(
202-
np.array(
203-
[
204-
[0.90, 0.90, 0.90],
205-
[0.95, 0.10, 0.99],
206-
[0.70, 0.70, 0.70],
207-
]
208-
)
209-
)
210-
)
211-
budget = select_draft_token_budget(survival, 3, 3, 3, min_survival=0.75)
212-
assert budget == 3
213-
214-
215196
def test_select_budget_applies_fraction_globally():
216197
survival = compute_prefix_survival(_logit(np.array([[0.90, 0.80], [0.80, 0.80]])))
217198
budget = select_draft_token_budget(survival, 2, 2, 2, budget_frac=0.5)
218-
assert budget == 3
199+
assert budget == 2
219200

220201

221202
def test_select_budget_is_hard_cap_under_ties():
222203
survival = compute_prefix_survival(_logit(np.array([[0.90, 0.80], [0.90, 0.80]])))
223204
budget = select_draft_token_budget(survival, 2, 2, 2, budget_frac=0.25)
224-
assert budget == 2
205+
assert budget == 1
225206

226207

227208
def test_select_budget_is_hard_cap_for_saturated_scores():
@@ -232,7 +213,7 @@ def test_select_budget_is_hard_cap_for_saturated_scores():
232213
"""
233214
survival = compute_prefix_survival(np.full((4, 7), 40.0))
234215
budget = select_draft_token_budget(survival, 4, 4, 4, budget_frac=0.5)
235-
assert budget == int(4 * 7 * 0.5) + 1
216+
assert budget == int(4 * 7 * 0.5)
236217

237218

238219
def test_select_budget_never_admits_zero_survival():
@@ -256,46 +237,45 @@ def test_select_budget_uses_sps_argmax():
256237
# SPS drops sharply after B=4 so theta peaks at k=2:
257238
# k=0: 2.00*1.00, k=1: 2.90*0.95=2.755, k=2: 3.62*0.90=3.258,
258239
# k=3: 4.22*0.20=0.844, k=4: 4.52*0.10=0.452.
259-
sps_table = np.broadcast_to(
260-
np.array([1.0, 1.0, 1.0, 0.95, 0.90, 0.20, 0.10]),
261-
(3, 7),
262-
)
240+
rates = np.array([1.0, 1.0, 1.0, 0.95, 0.90, 0.20, 0.10])
263241
budget = select_draft_token_budget(
264242
survival,
265243
num_batch_requests=2,
266244
num_sampling_requests=2,
267245
num_required_target_tokens=2,
268-
sps_table=sps_table,
246+
draft_cost_ms=np.zeros(3),
247+
verify_and_sample_cost_ms=1000.0 / rates,
269248
)
270249
assert budget == 2
271250

272251

273252
def test_select_budget_counts_only_requests_that_sample():
274253
survival = np.array([[0.9, 0.8], [0.7, 0.6]])
275-
sps_table = np.ones((3, 9))
276-
sps_table[:, 5:] = 0.1
254+
rates = np.ones(9)
255+
rates[5:] = 0.1
277256

278257
budget = select_draft_token_budget(
279258
survival,
280259
num_batch_requests=2,
281260
num_sampling_requests=1,
282261
num_required_target_tokens=4,
283-
sps_table=sps_table,
262+
draft_cost_ms=np.zeros(3),
263+
verify_and_sample_cost_ms=1000.0 / rates,
284264
)
285265

286266
assert budget == 0
287267

288268

289-
def test_sps_table_combines_direct_draft_and_verification_costs():
269+
def test_cost_tables_keep_draft_and_verification_costs_separate():
290270
draft_curve = [(1, 0.2), (4, 0.8), (8, 1.6)]
291271
verify_curve = [(1, 2.0), (8, 3.0), (64, 5.0)]
292272

293-
table = build_sps_table_from_costs(
273+
draft_cost_ms, verify_and_sample_cost_ms = build_cost_tables_from_curves(
294274
draft_curve, verify_curve, max_num_reqs=8, max_batch_tokens=64
295275
)
296276

297-
assert 1000.0 / table[1, 8] == pytest.approx(3.2)
298-
assert 1000.0 / table[8, 8] == pytest.approx(4.6)
277+
assert draft_cost_ms[1] + verify_and_sample_cost_ms[8] == pytest.approx(3.2)
278+
assert draft_cost_ms[8] + verify_and_sample_cost_ms[8] == pytest.approx(4.6)
299279

300280

301281
def test_select_budget_temperature_desaturates_zeros():
@@ -312,9 +292,8 @@ def test_select_budget_temperature_desaturates_zeros():
312292
)
313293
# T=1: exact-zero survival past position 2 truncates both requests.
314294
assert budget_t1 == 4
315-
# T=80: sigmoid(-10) > 0, so the budget (int(10*0.9)+1 = 10 admissions,
316-
# capped at 5 per request) is fully spent.
317-
assert budget_t80 == 10
295+
# T=80: sigmoid(-10) > 0, so 90% of the candidates are admitted.
296+
assert budget_t80 == 9
318297

319298

320299
def test_online_sts_fits_order_preserving_temperatures():
@@ -362,6 +341,7 @@ def test_capacity_manager_assigns_scalar_budget_from_gpu_scores(monkeypatch):
362341
monkeypatch.setattr(
363342
"vllm.v1.worker.gpu.spec_decode.confidence.get_tp_group",
364343
lambda: SimpleNamespace(
344+
rank_in_group=0,
365345
broadcast=lambda tensor, src=0: broadcasts.append(tensor),
366346
broadcast_object=lambda value, src=0: value,
367347
),
@@ -377,9 +357,7 @@ def test_capacity_manager_assigns_scalar_budget_from_gpu_scores(monkeypatch):
377357
)
378358
req_states.req_id_to_index = {"req0": 0, "req1": 1}
379359
handler = VarlenConfidenceManager(
380-
max_num_tokens=16,
381360
req_states=req_states,
382-
device=device,
383361
speculative_config=_spec_config(),
384362
)
385363
input_batch: Any = SimpleNamespace(
@@ -433,9 +411,7 @@ def test_varlen_capacity_manager_compacts_verifier_batch():
433411
)
434412
req_states.req_id_to_index = {"req0": 0, "req1": 1}
435413
handler = VarlenConfidenceManager(
436-
max_num_tokens=16,
437414
req_states=req_states,
438-
device=device,
439415
speculative_config=_spec_config(dspark_budget_frac=0.34),
440416
)
441417
handler._confidence_logits[:2].copy_(
@@ -541,9 +517,7 @@ def test_masked_capacity_manager_marks_pruned_tokens_for_forward_and_sampler():
541517
)
542518
req_states.req_id_to_index = {"req0": 0, "req1": 1}
543519
handler = MaskedConfidenceManager(
544-
max_num_tokens=16,
545520
req_states=req_states,
546-
device=device,
547521
speculative_config=_spec_config(dspark_budget_frac=0.34),
548522
)
549523
handler._confidence_logits[:2].copy_(
@@ -679,7 +653,6 @@ def test_varlen_cudagraph_capture_adds_full_desc():
679653

680654
manager._init_candidates()
681655

682-
assert any(
683-
desc.max_req_tokens == manager.decode_query_len
684-
for desc in manager._capture_descs[CUDAGraphMode.FULL]
685-
)
656+
descs = manager._capture_descs[CUDAGraphMode.FULL]
657+
assert [desc.num_reqs for desc in descs] == [3, 5]
658+
assert all(desc.max_req_tokens == manager.decode_query_len for desc in descs)

vllm/config/speculative.py

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,6 @@ class SpeculativeConfig:
232232
synthetic_acceptance_rates. Only valid when rejection_sample_method is 'synthetic'.
233233
Mutually exclusive with synthetic_acceptance_rates."""
234234

235-
dspark_confidence_threshold: float = 0.0
236-
"""Minimum DSpark cumulative prefix-survival probability for keeping a
237-
per-request draft prefix. Set to 0.0 to use budget-based global top-k
238-
allocation."""
239-
240235
dspark_budget_frac: float = 1.0
241236
"""Fraction of the full per-request draft-token budget available to the
242237
DSpark global prefix allocator."""
@@ -1217,11 +1212,6 @@ def _verify_args(self) -> Self:
12171212
"are only valid with rejection_sample_method='synthetic'."
12181213
)
12191214

1220-
if not 0.0 <= self.dspark_confidence_threshold <= 1.0:
1221-
raise ValueError(
1222-
"dspark_confidence_threshold must be in [0, 1], got "
1223-
f"{self.dspark_confidence_threshold}."
1224-
)
12251215
if not 0.0 < self.dspark_budget_frac <= 1.0:
12261216
raise ValueError(
12271217
f"dspark_budget_frac must be in (0, 1], got {self.dspark_budget_frac}."
@@ -1254,17 +1244,10 @@ def _verify_args(self) -> Self:
12541244
if (
12551245
self.method == "dspark"
12561246
and self.confidence_based_verification in ("auto", "mask")
1257-
and (
1258-
self.dspark_confidence_threshold > 0.0
1259-
or self.dspark_budget_frac < 1.0
1260-
or self.dspark_sps_curve is not None
1261-
)
1247+
and (self.dspark_budget_frac < 1.0 or self.dspark_sps_curve is not None)
12621248
and "VLLM_MOE_SKIP_PADDING" not in os.environ
12631249
):
1264-
# The masked fallback keeps pruned verify rows in the batch as
1265-
# padding; pruning only saves work if MoE kernels skip those rows.
1266-
# Set here (frontend) so spawned workers inherit it before their
1267-
# env caches freeze. Set VLLM_MOE_SKIP_PADDING=0 to override.
1250+
# Set before spawning workers so masked rows can skip MoE work.
12681251
logger.info(
12691252
"Confidence-based verification: defaulting "
12701253
"VLLM_MOE_SKIP_PADDING=1 so MoE kernels can skip masked "

vllm/v1/worker/gpu/cudagraph_utils.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,7 @@ def decode_descs(
232232
if num_tokens > max_decode_tokens or num_tokens > max_cg_capture_size:
233233
return
234234
max_requests = min(num_tokens, self.max_num_reqs)
235-
request_counts = {
236-
(max_requests + 1) // 2,
237-
(3 * max_requests + 3) // 4,
238-
max_requests,
239-
}
235+
request_counts = {(max_requests + 1) // 2, max_requests}
240236
for num_reqs in sorted(request_counts):
241237
if num_reqs * self.decode_query_len < num_tokens:
242238
continue

vllm/v1/worker/gpu/model_runner.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -454,9 +454,7 @@ def initialize_kv_cache(self, kv_cache_config: KVCacheConfig) -> None:
454454
self.spec_decode_confidence_manager = make_confidence_manager(
455455
self.speculative_config.confidence_based_verification,
456456
attn_cg_support,
457-
self.max_num_tokens,
458457
self.req_states,
459-
self.device,
460458
self.speculative_config,
461459
)
462460
self.block_tables = BlockTables(
@@ -1048,7 +1046,6 @@ def prepare_inputs(
10481046
if self.spec_decode_confidence_manager is not None:
10491047
self.spec_decode_confidence_manager.trim_batch(input_batch, draft_tokens)
10501048
if self.use_dcp:
1051-
# Prepare dcp local seq_lens.
10521049
prepare_dcp_local_seq_lens(
10531050
self.input_buffers.dcp_local_seq_lens,
10541051
self.input_buffers.seq_lens,
@@ -1061,8 +1058,7 @@ def prepare_inputs(
10611058
: input_batch.num_reqs_after_padding
10621059
]
10631060
if uses_padding_mask:
1064-
# Mark trailing cudagraph-padding rows so kernels can skip work for
1065-
# them when supported.
1061+
# Mark trailing cudagraph padding.
10661062
self.input_buffers.is_padding[
10671063
input_batch.num_tokens : num_tokens_after_padding
10681064
].fill_(True)

0 commit comments

Comments
 (0)