Skip to content

Commit 12128ad

Browse files
committed
E2E dynamic adaptive DSpark tests
Signed-off-by: Benjamin Chislett <bchislett@nvidia.com>
1 parent 623f749 commit 12128ad

2 files changed

Lines changed: 159 additions & 8 deletions

File tree

tests/v1/e2e/spec_decode/test_spec_decode.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,6 +1439,21 @@ def dspark_config():
14391439
)
14401440

14411441

1442+
@pytest.fixture
1443+
def dspark_adaptive_config(dspark_config):
1444+
speculative_config = dspark_config["speculative_config"] | {
1445+
"num_speculative_tokens": 7,
1446+
"num_speculative_tokens_per_batch_size": [(1, 1024, 5)],
1447+
"adaptive_verification": True,
1448+
}
1449+
speculative_config.pop("attention_backend")
1450+
return dspark_config | {
1451+
"attention_backend": "FLASH_ATTN",
1452+
"speculative_config": speculative_config,
1453+
"max_num_seqs": 1024,
1454+
}
1455+
1456+
14421457
@single_gpu_only
14431458
@large_gpu_mark(min_gb=24)
14441459
def test_dspark_correctness_and_acceptance_rate(dspark_config):
@@ -1471,8 +1486,43 @@ def test_dspark_correctness_and_acceptance_rate(dspark_config):
14711486
f"gsm8k_accuracy={gsm8k_accuracy:.3f}"
14721487
)
14731488

1474-
assert acceptance_rate >= 0.428 * 0.9
1475-
assert acceptance_len >= 3.994 * 0.9
1489+
assert acceptance_rate >= 0.428 * 0.95
1490+
assert acceptance_len >= 3.994 * 0.95
1491+
assert gsm8k_accuracy >= 0.801 * 0.9
1492+
1493+
del spec_llm
1494+
torch.accelerator.empty_cache()
1495+
cleanup_dist_env_and_memory()
1496+
1497+
1498+
@single_gpu_only
1499+
@large_gpu_mark(min_gb=24)
1500+
def test_dspark_adaptive_correctness_and_acceptance_length(dspark_adaptive_config):
1501+
"""Check adaptive verification quality with a larger draft window.
1502+
The drafter produces up to 7 candidates per request, while the target verifies an
1503+
average budget of 5 draft tokens per request.
1504+
"""
1505+
spec_llm = LLM(**dspark_adaptive_config)
1506+
1507+
results = evaluate_gsm8k_offline(spec_llm, temperature=1.0)
1508+
gsm8k_accuracy = results["accuracy"]
1509+
metrics = spec_llm.get_metrics()
1510+
acceptance_len = compute_acceptance_len(metrics)
1511+
1512+
# Check the actual low-level stats to ensure we're not over- or under-allocating.
1513+
name2metric = {metric.name: metric for metric in metrics}
1514+
num_drafts = name2metric["vllm:spec_decode_num_drafts"].value
1515+
num_draft_tokens = name2metric["vllm:spec_decode_num_draft_tokens"].value
1516+
1517+
print(
1518+
f"Adaptive DSpark acceptance_len={acceptance_len:.2f}, "
1519+
f"gsm8k_accuracy={gsm8k_accuracy:.3f}"
1520+
)
1521+
1522+
assert num_drafts > 0
1523+
assert num_draft_tokens < num_drafts * 7
1524+
assert 0.99 * (num_drafts * 5) <= num_draft_tokens <= 1.01 * (num_drafts * 5)
1525+
assert acceptance_len >= 3.62 * 0.95 # Measured on non-adaptive K=5 baseline
14761526
assert gsm8k_accuracy >= 0.801 * 0.9
14771527

14781528
del spec_llm

tests/v1/spec_decode/test_dynamic_sd.py

Lines changed: 107 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def _make_scheduler_with_dynamic_sd(
3535
max_num_seqs: int = 16,
3636
max_num_batched_tokens: int = 8192,
3737
runtime_num_speculative_tokens: int = 3,
38+
adaptive_verification: bool = False,
3839
) -> Scheduler:
3940
base_scheduler = create_scheduler(
4041
max_num_seqs=max_num_seqs,
@@ -45,6 +46,7 @@ def _make_scheduler_with_dynamic_sd(
4546
speculative_config = base_scheduler.vllm_config.speculative_config
4647
assert speculative_config is not None
4748
speculative_config.num_speculative_tokens_per_batch_size = schedule
49+
speculative_config.adaptive_verification = adaptive_verification
4850

4951
return Scheduler(
5052
vllm_config=base_scheduler.vllm_config,
@@ -183,9 +185,104 @@ def test_v2_scheduler_selects_aggregate_dynamic_sd_budget():
183185
assert output.num_spec_tokens_to_schedule == 4
184186

185187

186-
@pytest.mark.parametrize("adaptive_verification", [False, True])
187-
def test_scheduler_preserves_actual_grammar_mask_layout(
188+
@pytest.mark.parametrize("optimal_k", [0, 2])
189+
def test_adaptive_scheduler_caps_fallback_decodes_at_dynamic_k(optimal_k: int):
190+
scheduler = _make_scheduler_with_dynamic_sd(
191+
[(1, 2, optimal_k)],
192+
max_num_seqs=2,
193+
max_num_batched_tokens=32,
194+
runtime_num_speculative_tokens=4,
195+
adaptive_verification=True,
196+
)
197+
decode_req, prefill_req = create_requests(num_requests=2, num_tokens=1)
198+
scheduler.add_request(decode_req)
199+
prefill_output = scheduler.schedule()
200+
scheduler.update_from_output(
201+
prefill_output,
202+
ModelRunnerOutput(
203+
req_ids=[decode_req.request_id],
204+
req_id_to_index={decode_req.request_id: 0},
205+
sampled_token_ids=[[0]],
206+
logprobs=None,
207+
prompt_logprobs_dict={},
208+
pooler_output=[],
209+
),
210+
)
211+
scheduler.update_draft_token_ids(
212+
DraftTokenIds([decode_req.request_id], [[10, 11, 12, 13]])
213+
)
214+
215+
scheduler.add_request(prefill_req)
216+
output = scheduler.schedule()
217+
218+
expected_draft_tokens = [10, 11, 12, 13][:optimal_k]
219+
assert output.scheduled_spec_decode_tokens == (
220+
{decode_req.request_id: expected_draft_tokens} if expected_draft_tokens else {}
221+
)
222+
assert output.num_scheduled_tokens == {
223+
decode_req.request_id: 1 + optimal_k,
224+
prefill_req.request_id: 1,
225+
}
226+
assert output.total_num_scheduled_tokens == 2 + optimal_k
227+
assert output.num_spec_tokens_to_schedule == 2 * optimal_k
228+
229+
230+
def test_adaptive_scheduler_preserves_full_windows_for_worker_allocation():
231+
scheduler = _make_scheduler_with_dynamic_sd(
232+
[(1, 2, 2)],
233+
max_num_seqs=2,
234+
max_num_batched_tokens=32,
235+
runtime_num_speculative_tokens=4,
236+
adaptive_verification=True,
237+
)
238+
requests = create_requests(num_requests=2, num_tokens=1)
239+
for request in requests:
240+
scheduler.add_request(request)
241+
prefill_output = scheduler.schedule()
242+
scheduler.update_from_output(
243+
prefill_output,
244+
ModelRunnerOutput(
245+
req_ids=[request.request_id for request in requests],
246+
req_id_to_index={
247+
request.request_id: i for i, request in enumerate(requests)
248+
},
249+
sampled_token_ids=[[0], [0]],
250+
logprobs=None,
251+
prompt_logprobs_dict={},
252+
pooler_output=[],
253+
),
254+
)
255+
scheduler.update_draft_token_ids(
256+
DraftTokenIds(
257+
[request.request_id for request in requests],
258+
[[10, 11, 12, 13], [20, 21, 22, 23]],
259+
)
260+
)
261+
262+
output = scheduler.schedule()
263+
264+
assert output.scheduled_spec_decode_tokens == {
265+
requests[0].request_id: [10, 11, 12, 13],
266+
requests[1].request_id: [20, 21, 22, 23],
267+
}
268+
assert output.num_scheduled_tokens == {
269+
requests[0].request_id: 5,
270+
requests[1].request_id: 5,
271+
}
272+
assert output.total_num_scheduled_tokens == 10
273+
assert output.num_spec_tokens_to_schedule == 4
274+
275+
276+
@pytest.mark.parametrize(
277+
("adaptive_verification", "expected_spec_token_ids"),
278+
[
279+
(False, [10, 11, 12]),
280+
(True, [10, 11, 12, -1]),
281+
],
282+
)
283+
def test_scheduler_preserves_grammar_mask_window(
188284
adaptive_verification: bool,
285+
expected_spec_token_ids: list[int],
189286
):
190287
scheduler = create_scheduler(
191288
num_speculative_tokens=4,
@@ -194,6 +291,7 @@ def test_scheduler_preserves_actual_grammar_mask_layout(
194291
speculative_config = scheduler.vllm_config.speculative_config
195292
assert speculative_config is not None
196293
speculative_config.adaptive_verification = adaptive_verification
294+
scheduler.adaptive_verification = adaptive_verification
197295

198296
request = create_requests(num_requests=1, num_tokens=1)[0]
199297
scheduler.add_request(request)
@@ -220,21 +318,24 @@ def test_scheduler_preserves_actual_grammar_mask_layout(
220318
)
221319
)
222320

223-
assert request.spec_token_ids == [10, 11, 12]
321+
assert request.spec_token_ids == expected_spec_token_ids
224322
grammar.validate_tokens.assert_called_once_with([10, 11, 12, 13])
225323

226324
output = scheduler.schedule()
227-
assert output.scheduled_spec_decode_tokens[request.request_id] == [10, 11, 12]
325+
assert (
326+
output.scheduled_spec_decode_tokens[request.request_id]
327+
== expected_spec_token_ids
328+
)
228329

229-
bitmask = np.zeros((4, 1), dtype=np.int32)
330+
bitmask = np.zeros((len(expected_spec_token_ids) + 1, 1), dtype=np.int32)
230331
scheduler.structured_output_manager.grammar_bitmask = Mock(return_value=bitmask)
231332
grammar_output = scheduler.get_grammar_bitmask(output)
232333

233334
assert grammar_output is not None
234335
assert grammar_output.grammar_mask_stride == 5
235336
assert grammar_output.grammar_bitmask is bitmask
236337
assert scheduler.structured_output_manager.grammar_bitmask.call_args.args[2] == {
237-
request.request_id: [10, 11, 12]
338+
request.request_id: expected_spec_token_ids
238339
}
239340

240341

0 commit comments

Comments
 (0)