Skip to content

Commit 1e8070e

Browse files
[Spec Decode] Tighten confidence graph allocation
Compute current confidence ordering only on TP rank zero, retain two request-count CUDA graph tiers, and use automatic SPS in the confidence evaluation config. Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>
1 parent 0fc06da commit 1e8070e

9 files changed

Lines changed: 53 additions & 102 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: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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
)
@@ -196,22 +196,6 @@ def test_varlen_indexer_indices_use_device_lengths():
196196
assert indices.cpu().tolist() == [0, 1, 1, 2, 3]
197197

198198

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-
215199
def test_select_budget_applies_fraction_globally():
216200
survival = compute_prefix_survival(_logit(np.array([[0.90, 0.80], [0.80, 0.80]])))
217201
budget = select_draft_token_budget(survival, 2, 2, 2, budget_frac=0.5)
@@ -362,6 +346,7 @@ def test_capacity_manager_assigns_scalar_budget_from_gpu_scores(monkeypatch):
362346
monkeypatch.setattr(
363347
"vllm.v1.worker.gpu.spec_decode.confidence.get_tp_group",
364348
lambda: SimpleNamespace(
349+
rank_in_group=0,
365350
broadcast=lambda tensor, src=0: broadcasts.append(tensor),
366351
broadcast_object=lambda value, src=0: value,
367352
),
@@ -679,7 +664,6 @@ def test_varlen_cudagraph_capture_adds_full_desc():
679664

680665
manager._init_candidates()
681666

682-
assert any(
683-
desc.max_req_tokens == manager.decode_query_len
684-
for desc in manager._capture_descs[CUDAGraphMode.FULL]
685-
)
667+
descs = manager._capture_descs[CUDAGraphMode.FULL]
668+
assert [desc.num_reqs for desc in descs] == [3, 5]
669+
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 & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,7 +1048,6 @@ def prepare_inputs(
10481048
if self.spec_decode_confidence_manager is not None:
10491049
self.spec_decode_confidence_manager.trim_batch(input_batch, draft_tokens)
10501050
if self.use_dcp:
1051-
# Prepare dcp local seq_lens.
10521051
prepare_dcp_local_seq_lens(
10531052
self.input_buffers.dcp_local_seq_lens,
10541053
self.input_buffers.seq_lens,
@@ -1061,8 +1060,7 @@ def prepare_inputs(
10611060
: input_batch.num_reqs_after_padding
10621061
]
10631062
if uses_padding_mask:
1064-
# Mark trailing cudagraph-padding rows so kernels can skip work for
1065-
# them when supported.
1063+
# Mark trailing cudagraph padding.
10661064
self.input_buffers.is_padding[
10671065
input_batch.num_tokens : num_tokens_after_padding
10681066
].fill_(True)

vllm/v1/worker/gpu/spec_decode/confidence.py

Lines changed: 40 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,6 @@ def select_draft_token_budget(
222222
num_required_target_tokens: int,
223223
budget_frac: float = 1.0,
224224
sps_table: np.ndarray | None = None,
225-
min_survival: float = 0.0,
226225
) -> int:
227226
"""Choose the scalar draft budget from stale prefix scores.
228227
@@ -232,9 +231,6 @@ def select_draft_token_budget(
232231
"""
233232
flat = survival.reshape(-1)
234233
scores = flat[np.isfinite(flat)]
235-
if min_survival > 0.0:
236-
return int((scores >= min_survival).sum())
237-
238234
scores = np.sort(scores)[::-1]
239235
total = scores.size
240236
max_admissions = min(int(total * budget_frac) + 1, total)
@@ -314,7 +310,6 @@ def __init__(
314310
self.copy_stream = torch.cuda.Stream(device)
315311
self.varlen_spec_decode = False
316312

317-
self.min_survival_probability = speculative_config.dspark_confidence_threshold
318313
self.capacity_budget_frac = speculative_config.dspark_budget_frac
319314
sps_curve = speculative_config.dspark_sps_curve
320315
self.wants_auto_sps_curve = sps_curve == "auto"
@@ -740,7 +735,6 @@ def _plan_draft_token_budget(
740735
num_required_target_tokens=int(num_required_target_tokens_per_req.sum()),
741736
budget_frac=self.capacity_budget_frac,
742737
sps_table=self.sps_table_np,
743-
min_survival=self.min_survival_probability,
744738
)
745739
budget = min(budget, int(valid_draft_tokens_per_req.sum()))
746740
return int(get_tp_group().broadcast_object(budget, src=0))
@@ -764,45 +758,47 @@ def _assign_draft_token_budget(
764758
capacities.clamp_(max=self.forced_capacity)
765759
return capacities
766760

767-
temperatures: np.ndarray | float = 1.0
768-
if self.online_sts is not None:
769-
temperatures = self.online_sts.temperatures
770-
self._temperatures_np[:] = temperatures
771-
self._temperatures.copy_(self._temperatures_cpu, non_blocking=True)
772-
_compute_prefix_survival_kernel[(num_reqs,)](
773-
self._confidence_logits,
774-
self._confidence_logits.stride(0),
775-
input_batch.idx_mapping,
776-
self._valid_draft_tokens,
777-
self._temperatures,
778-
self._survival,
779-
NUM_STEPS=self.num_speculative_steps,
780-
)
781-
num_candidates = num_reqs * self.num_speculative_steps
782-
torch.sort(
783-
self._survival[:num_reqs].flatten(),
784-
descending=True,
785-
stable=True,
786-
out=(
787-
self._sorted_survival[:num_candidates],
788-
self._sorted_indices[:num_candidates],
789-
),
790-
)
791-
capacities.zero_()
792-
if draft_token_budget > 0:
793-
admitted = self._sorted_indices[:draft_token_budget]
794-
torch.div(
795-
admitted,
796-
self.num_speculative_steps,
797-
rounding_mode="floor",
798-
out=admitted,
761+
tp_group = get_tp_group()
762+
if tp_group.rank_in_group == 0:
763+
temperatures: np.ndarray | float = 1.0
764+
if self.online_sts is not None:
765+
temperatures = self.online_sts.temperatures
766+
self._temperatures_np[:] = temperatures
767+
self._temperatures.copy_(self._temperatures_cpu, non_blocking=True)
768+
_compute_prefix_survival_kernel[(num_reqs,)](
769+
self._confidence_logits,
770+
self._confidence_logits.stride(0),
771+
input_batch.idx_mapping,
772+
self._valid_draft_tokens,
773+
self._temperatures,
774+
self._survival,
775+
NUM_STEPS=self.num_speculative_steps,
799776
)
800-
capacities.scatter_add_(
801-
0,
802-
admitted,
803-
self._capacity_ones[:draft_token_budget],
777+
num_candidates = num_reqs * self.num_speculative_steps
778+
torch.sort(
779+
self._survival[:num_reqs].flatten(),
780+
descending=True,
781+
stable=True,
782+
out=(
783+
self._sorted_survival[:num_candidates],
784+
self._sorted_indices[:num_candidates],
785+
),
804786
)
805-
get_tp_group().broadcast(capacities, src=0)
787+
capacities.zero_()
788+
if draft_token_budget > 0:
789+
admitted = self._sorted_indices[:draft_token_budget]
790+
torch.div(
791+
admitted,
792+
self.num_speculative_steps,
793+
rounding_mode="floor",
794+
out=admitted,
795+
)
796+
capacities.scatter_add_(
797+
0,
798+
admitted,
799+
self._capacity_ones[:draft_token_budget],
800+
)
801+
tp_group.broadcast(capacities, src=0)
806802
return capacities
807803

808804
def warmup(self, input_buffers: "InputBuffers") -> None:
@@ -1116,7 +1112,7 @@ def __init__(self, *args: Any, **kwargs: Any):
11161112
self.wants_auto_sps_curve = False
11171113
logger.info_once(
11181114
"DSpark auto SPS profiling requires compact verification; "
1119-
"masked verification will use the configured threshold or budget."
1115+
"masked verification will use the configured budget."
11201116
)
11211117

11221118
def _prepare_forward_skip_mask(

vllm/v1/worker/gpu/spec_decode/dspark/speculator.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,7 @@ def __init__(self, vllm_config: VllmConfig, device: torch.device):
8080
self.use_confidence_based_verification = (
8181
self.speculative_config.confidence_based_verification not in ("none", "off")
8282
and (
83-
self.speculative_config.dspark_confidence_threshold > 0.0
84-
or self.speculative_config.dspark_budget_frac < 1.0
83+
self.speculative_config.dspark_budget_frac < 1.0
8584
or sps_curve is not None
8685
)
8786
)

vllm/v1/worker/gpu/spec_decode/rejection_sampler.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,7 @@ def __init__(
5353
spec_config.method == "dspark"
5454
and spec_config.confidence_based_verification not in ("none", "off")
5555
and (
56-
spec_config.dspark_confidence_threshold > 0.0
57-
or spec_config.dspark_budget_frac < 1.0
56+
spec_config.dspark_budget_frac < 1.0
5857
or spec_config.dspark_sps_curve is not None
5958
)
6059
)

0 commit comments

Comments
 (0)