Files
sglang/test/registered/unit/managers/test_prefill_scheduler_admission_bench.py
laoyao0822 3a43727216 Bound CP prefill batching by estimated temp memory
CP shared-KV bs>1 batching was only bounded by request count, extend tokens, and cached tokens. That left temporary GPU buffers such as MLA/index materialization, remap metadata, logits windows, and transfer descriptors implicit, and raw extend-token limits could exceed the active chunked-prefill budget.\n\nThis adds an explicit max-buffer-size admission gate with a CPU-only stream-aware estimator, wires it through PrefillAdder/Scheduler, performs a startup CUDA smoke allocation when configured, and reports the estimate in the scheduler admission benchmark. When chunked prefill is active, the effective CP extend-token limit is capped by the current chunk budget so the CP path does not advertise unreachable batch capacity or lift max-prefill-tokens too far.\n\nConstraint: Admission estimation must stay CPU-only on the scheduler hot path; CUDA allocation is limited to startup smoke checking.\nConstraint: Single oversized requests must still be allowed to run alone to avoid scheduler deadlock.\nRejected: Rely only on --max-prefill-tokens | it does not reliably bound the first oversized request and does not model cache-hit/load-back pressure.\nRejected: Let CP extend limit exceed chunked-prefill size | it creates an unreachable effective capacity and misleading budget lift.\nConfidence: medium\nScope-risk: moderate\nDirective: If bs>1 L1 prefetch is enabled later, update CPSharedKVPrefillBufferEstimatorContext.bs_gt1_l1_prefetch_enabled and include the live prefetch dense buffers in overlap windows.\nTested: local py_compile for touched files\nTested: local PYTHONPATH=python pytest -q test/registered/unit/managers/test_cp_shared_kv_prefill_buffer_estimator.py (4 passed)\nTested: remote cjy-glm5-new targeted pytest for new server_args, PrefillAdder, estimator, and benchmark cases (10 passed)\nTested: remote cjy-glm5-new PYTHONPATH=python pytest -q test/registered/unit/managers/test_cp_shared_kv_prefill_buffer_estimator.py test/registered/unit/managers/test_prefill_adder.py test/registered/unit/managers/test_prefill_scheduler_admission_bench.py (29 passed before chunk cap, then test_prefill_adder.py 21 passed after chunk cap)\nNot-tested: full server_args suite because existing TestPrepareServerArgs tries to reach HuggingFace and fails under container DNS/network\nNot-tested: GLM5 ETE smoke with --cp-shared-kv-prefill-max-buffer-size
2026-06-11 01:33:28 +08:00

175 lines
6.4 KiB
Python

import importlib.util
import sys
import unittest
from pathlib import Path
_REPO_ROOT = Path(__file__).resolve().parents[4]
sys.path.insert(0, str(_REPO_ROOT / "python"))
_BENCH_PATH = (
_REPO_ROOT
/ "benchmark"
/ "hicache"
/ "bench_prefill_scheduler_admission.py"
)
def _load_bench_module():
spec = importlib.util.spec_from_file_location(
"bench_prefill_scheduler_admission", _BENCH_PATH
)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
class TestPrefillSchedulerAdmissionBench(unittest.TestCase):
def test_l1_l2_and_extend_tokens_are_reported_with_real_prefill_semantics(self):
bench = _load_bench_module()
cfg = bench.SchedulerBenchConfig(
page_size=64,
available_tokens=10000,
evictable_tokens=0,
max_prefill_tokens=1024,
enable_cp_shared_kv_prefill_bs_gt1=True,
cp_shared_kv_prefill_max_batch_requests=8,
cp_shared_kv_prefill_max_total_extend_tokens=4096,
)
trace = bench.run_scheduler_admission_trace(
[
bench.RequestSpec(
rid="hit-l1-l2",
l1_cached_tokens=128,
l2_cached_tokens=128,
extend_tokens=64,
max_new_tokens=1,
)
],
cfg,
)
self.assertEqual(len(trace.ticks), 1)
tick = trace.ticks[0]
self.assertEqual([req.rid for req in tick.accepted], ["hit-l1-l2"])
accepted = tick.accepted[0]
self.assertEqual(accepted.initial_extend_tokens, 192)
self.assertEqual(accepted.effective_extend_tokens, 64)
self.assertEqual(accepted.l1_cached_tokens, 128)
self.assertEqual(accepted.l2_cached_tokens, 128)
self.assertEqual(accepted.loaded_l2_tokens, 128)
self.assertEqual(tick.log_hit_tokens, 256)
self.assertEqual(tick.log_input_tokens, 64)
def test_cp_total_extend_limit_controls_batching_not_generic_max_prefill_tokens(self):
bench = _load_bench_module()
cfg = bench.SchedulerBenchConfig(
page_size=64,
available_tokens=10000,
evictable_tokens=0,
max_prefill_tokens=192,
enable_cp_shared_kv_prefill_bs_gt1=True,
cp_shared_kv_prefill_max_batch_requests=8,
cp_shared_kv_prefill_max_total_extend_tokens=256,
)
trace = bench.run_scheduler_admission_trace(
[
bench.RequestSpec("a", 0, 0, 128, max_new_tokens=1),
bench.RequestSpec("b", 0, 0, 128, max_new_tokens=1),
],
cfg,
)
self.assertEqual(len(trace.ticks), 1)
self.assertEqual([req.rid for req in trace.ticks[0].accepted], ["a", "b"])
self.assertEqual(trace.ticks[0].cp_total_extend_tokens, 256)
def test_l2_load_back_consumes_l1_capacity_and_can_stop_later_requests(self):
bench = _load_bench_module()
cfg = bench.SchedulerBenchConfig(
page_size=64,
available_tokens=320,
evictable_tokens=0,
max_prefill_tokens=4096,
enable_cp_shared_kv_prefill_bs_gt1=True,
cp_shared_kv_prefill_max_batch_requests=8,
cp_shared_kv_prefill_max_total_extend_tokens=4096,
)
trace = bench.run_scheduler_admission_trace(
[
bench.RequestSpec("l2-heavy", 0, 128, 64, max_new_tokens=1),
bench.RequestSpec("next", 0, 0, 64, max_new_tokens=1),
],
cfg,
)
self.assertEqual([req.rid for req in trace.ticks[0].accepted], ["l2-heavy"])
self.assertEqual(trace.ticks[0].stopped_on_rid, "next")
self.assertEqual(trace.ticks[0].stopped_result, "NO_TOKEN")
self.assertEqual(trace.ticks[0].allocator_available_after_tick, 192)
self.assertEqual(trace.ticks[0].load_back_events[0].loaded_tokens, 128)
def test_total_cached_limit_is_observable_in_scheduler_trace(self):
bench = _load_bench_module()
cfg = bench.SchedulerBenchConfig(
page_size=64,
available_tokens=10000,
evictable_tokens=0,
max_prefill_tokens=4096,
enable_cp_shared_kv_prefill_bs_gt1=True,
cp_shared_kv_prefill_max_batch_requests=8,
cp_shared_kv_prefill_max_total_extend_tokens=4096,
cp_shared_kv_prefill_max_total_cached_tokens=4096,
)
trace = bench.run_scheduler_admission_trace(
[
bench.RequestSpec("a", 4096, 0, 64, max_new_tokens=1),
bench.RequestSpec("b", 4096, 0, 64, max_new_tokens=1),
],
cfg,
)
self.assertEqual([req.rid for req in trace.ticks[0].accepted], ["a"])
self.assertEqual(trace.ticks[0].stopped_on_rid, "b")
self.assertEqual(trace.ticks[0].stopped_result, "OTHER")
self.assertEqual(trace.ticks[0].cp_total_cached_tokens, 4096)
def test_max_buffer_size_is_observable_in_scheduler_trace(self):
bench = _load_bench_module()
cfg = bench.SchedulerBenchConfig(
page_size=64,
available_tokens=10000,
evictable_tokens=0,
max_prefill_tokens=4096,
enable_cp_shared_kv_prefill_bs_gt1=True,
cp_shared_kv_prefill_max_batch_requests=8,
cp_shared_kv_prefill_max_total_extend_tokens=4096,
cp_shared_kv_prefill_max_total_cached_tokens=4096,
cp_shared_kv_prefill_max_buffer_size=1,
kv_cache_dim=1,
kv_dtype_bytes=2,
index_head_dim=8,
vocab_size=16,
tp_size=1,
)
trace = bench.run_scheduler_admission_trace(
[
bench.RequestSpec("a", 0, 0, 64, max_new_tokens=1),
bench.RequestSpec("b", 0, 0, 64, max_new_tokens=1),
],
cfg,
)
tick = trace.ticks[0]
self.assertEqual([req.rid for req in tick.accepted], ["a"])
self.assertEqual(tick.stopped_on_rid, "b")
self.assertEqual(tick.stopped_result, "OTHER")
self.assertGreater(tick.cp_estimated_peak_buffer_bytes, 1)
self.assertIn("layer_forward_peak_bytes", tick.cp_buffer_breakdown)
if __name__ == "__main__":
unittest.main()