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
737 lines
28 KiB
Python
Executable File
737 lines
28 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
"""Offline benchmark/model for SGLang prefill scheduler admission.
|
|
|
|
This script answers a narrow question: given an ordered waiting queue with
|
|
per-request L1 cache hit, L2/HiCache hit, and compute extend lengths, what would
|
|
PrefillAdder admit into a prefill batch and which budget stops the scan?
|
|
|
|
It intentionally reuses the production PrefillAdder admission logic instead of
|
|
reimplementing the scheduler. CUDA/model execution is not required. L2 load
|
|
back is modeled by a fake tree cache that consumes fake L1/device allocator
|
|
capacity and records each load-back event.
|
|
|
|
Examples:
|
|
|
|
PYTHONPATH=python python benchmark/hicache/bench_prefill_scheduler_admission.py \
|
|
--synthetic-grid --l1-cached-tokens 0,4096 --l2-cached-tokens 0,4096 \
|
|
--extend-tokens 256,1024,4096 --available-tokens 200000 \
|
|
--max-prefill-tokens 16384 --cp-max-total-extend-tokens 65536 \
|
|
--output text
|
|
|
|
cat requests.jsonl
|
|
{"rid":"r0","l1_cached_tokens":40320,"l2_cached_tokens":0,"extend_tokens":128}
|
|
{"rid":"r1","l1_cached_tokens":0,"l2_cached_tokens":32768,"extend_tokens":512}
|
|
|
|
PYTHONPATH=python python benchmark/hicache/bench_prefill_scheduler_admission.py \
|
|
--requests-jsonl requests.jsonl --output json
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import math
|
|
import sys
|
|
import time
|
|
import types
|
|
from dataclasses import asdict, dataclass, field
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from typing import Iterable, Optional
|
|
|
|
import torch
|
|
|
|
|
|
_SGL_KERNEL_LIBRARIES = []
|
|
|
|
|
|
def _install_sgl_kernel_stubs() -> None:
|
|
"""Install minimal sgl_kernel stubs for CPU-only scheduler imports."""
|
|
|
|
if "sgl_kernel" not in sys.modules:
|
|
sys.modules["sgl_kernel"] = types.ModuleType("sgl_kernel")
|
|
sys.modules["sgl_kernel"].__file__ = "sgl_kernel_stub.py"
|
|
sys.modules["sgl_kernel"].__path__ = []
|
|
|
|
if not hasattr(sys.modules["sgl_kernel"], "__getattr__"):
|
|
|
|
def _sgl_kernel_getattr(name):
|
|
if name.startswith("__"):
|
|
raise AttributeError(name)
|
|
fn = lambda *args, **kwargs: None
|
|
setattr(sys.modules["sgl_kernel"], name, fn)
|
|
return fn
|
|
|
|
sys.modules["sgl_kernel"].__getattr__ = _sgl_kernel_getattr
|
|
|
|
if "sgl_kernel.kvcacheio" not in sys.modules:
|
|
sys.modules["sgl_kernel.kvcacheio"] = types.ModuleType("sgl_kernel.kvcacheio")
|
|
|
|
for name in (
|
|
"sgl_per_token_group_quant_8bit",
|
|
"sgl_per_token_group_quant_fp8",
|
|
"sgl_per_token_quant_fp8",
|
|
"fp8_blockwise_scaled_mm",
|
|
"fp8_scaled_mm",
|
|
"silu_and_mul",
|
|
):
|
|
if not hasattr(sys.modules["sgl_kernel"], name):
|
|
setattr(sys.modules["sgl_kernel"], name, lambda *args, **kwargs: None)
|
|
|
|
if "sgl_kernel.quantization" not in sys.modules:
|
|
quantization_stub = types.ModuleType("sgl_kernel.quantization")
|
|
for name in (
|
|
"ggml_dequantize",
|
|
"ggml_moe_a8",
|
|
"ggml_moe_a8_vec",
|
|
"ggml_moe_get_block_size",
|
|
"ggml_mul_mat_a8",
|
|
"ggml_mul_mat_vec_a8",
|
|
):
|
|
setattr(quantization_stub, name, lambda *args, **kwargs: None)
|
|
sys.modules["sgl_kernel.quantization"] = quantization_stub
|
|
|
|
sgl_kernel_lib = torch.library.Library("sgl_kernel", "FRAGMENT")
|
|
_SGL_KERNEL_LIBRARIES.append(sgl_kernel_lib)
|
|
for schema in (
|
|
"sgl_per_token_group_quant_8bit(Tensor input, Tensor(a!) output_q, Tensor(b!) output_s, int group_size, float eps, float fp8_min, float fp8_max, bool scale_ue8m0) -> ()",
|
|
"sgl_per_token_group_quant_fp8(Tensor input, Tensor(a!) output_q, Tensor(b!) output_s, int group_size, float eps, float fp8_min, float fp8_max, bool scale_ue8m0) -> ()",
|
|
"sgl_per_token_quant_fp8(Tensor input, Tensor(a!) output_q, Tensor(b!) output_s) -> ()",
|
|
"fp8_scaled_mm(Tensor mat_a, Tensor mat_b, Tensor scales_a, Tensor scales_b, ScalarType out_dtype, Tensor? bias=None) -> Tensor",
|
|
"fp8_blockwise_scaled_mm(Tensor mat_a, Tensor mat_b, Tensor scales_a, Tensor scales_b, ScalarType out_dtype) -> Tensor",
|
|
):
|
|
try:
|
|
sgl_kernel_lib.define(schema)
|
|
except RuntimeError as exc:
|
|
if "already" not in str(exc).lower() and "duplicate" not in str(exc).lower():
|
|
raise
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RequestSpec:
|
|
rid: str
|
|
l1_cached_tokens: int
|
|
l2_cached_tokens: int
|
|
extend_tokens: int
|
|
max_new_tokens: int = 1
|
|
output_tokens: int = 0
|
|
|
|
def __post_init__(self) -> None:
|
|
for field_name in (
|
|
"l1_cached_tokens",
|
|
"l2_cached_tokens",
|
|
"extend_tokens",
|
|
"max_new_tokens",
|
|
"output_tokens",
|
|
):
|
|
value = getattr(self, field_name)
|
|
if value < 0:
|
|
raise ValueError(f"{field_name} must be non-negative, got {value}")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SchedulerBenchConfig:
|
|
page_size: int = 64
|
|
available_tokens: int = 1_000_000
|
|
evictable_tokens: int = 0
|
|
max_prefill_tokens: int = 16_384
|
|
chunked_prefill_size: Optional[int] = None
|
|
mixed_with_decode_tokens: int = 0
|
|
new_token_ratio: float = 1.0
|
|
enable_cp_context: bool = True
|
|
enable_cp_shared_kv_prefill_bs_gt1: bool = True
|
|
cp_shared_kv_prefill_max_batch_requests: Optional[int] = None
|
|
cp_shared_kv_prefill_max_total_extend_tokens: Optional[int] = None
|
|
cp_shared_kv_prefill_max_total_cached_tokens: Optional[int] = None
|
|
cp_shared_kv_prefill_max_buffer_size: Optional[int] = None
|
|
kv_cache_dim: int = 656
|
|
kv_dtype_bytes: int = 1
|
|
index_head_dim: int = 128
|
|
vocab_size: int = 128_000
|
|
tp_size: int = 8
|
|
enable_bs_gt1_prefetch_estimate: bool = False
|
|
max_ticks: int = 1
|
|
consume_l2_load_back_capacity: bool = True
|
|
|
|
def __post_init__(self) -> None:
|
|
if self.page_size <= 0:
|
|
raise ValueError(f"page_size must be positive, got {self.page_size}")
|
|
if self.available_tokens < 0:
|
|
raise ValueError("available_tokens must be non-negative")
|
|
if self.evictable_tokens < 0:
|
|
raise ValueError("evictable_tokens must be non-negative")
|
|
if self.max_prefill_tokens < 0:
|
|
raise ValueError("max_prefill_tokens must be non-negative")
|
|
if self.max_ticks <= 0:
|
|
raise ValueError("max_ticks must be positive")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LoadBackEvent:
|
|
rid: str
|
|
requested_tokens: int
|
|
paged_tokens: int
|
|
loaded_tokens: int
|
|
mem_quota: Optional[int]
|
|
available_before: int
|
|
available_after: int
|
|
skipped_reason: Optional[str] = None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AcceptedRequest:
|
|
rid: str
|
|
l1_cached_tokens: int
|
|
l2_cached_tokens: int
|
|
loaded_l2_tokens: int
|
|
compute_extend_tokens: int
|
|
initial_extend_tokens: int
|
|
effective_extend_tokens: int
|
|
max_new_tokens: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TickResult:
|
|
tick: int
|
|
accepted: list[AcceptedRequest]
|
|
stopped_on_rid: Optional[str]
|
|
stopped_result: Optional[str]
|
|
rem_input_tokens_after_tick: int
|
|
rem_total_tokens_after_tick: float
|
|
cur_rem_tokens_after_tick: float
|
|
cp_total_extend_tokens: int
|
|
cp_total_cached_tokens: int
|
|
log_hit_tokens: int
|
|
log_input_tokens: int
|
|
allocator_available_after_tick: int
|
|
load_back_events: list[LoadBackEvent]
|
|
cp_estimated_peak_buffer_bytes: int
|
|
cp_buffer_breakdown: dict[str, int]
|
|
duration_us: float
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TraceResult:
|
|
config: SchedulerBenchConfig
|
|
request_count: int
|
|
ticks: list[TickResult]
|
|
remaining_rids: list[str]
|
|
blocked: bool
|
|
|
|
|
|
class FakeTokenAllocator:
|
|
def __init__(self, available_tokens: int, cfg: SchedulerBenchConfig):
|
|
self.available_tokens = int(available_tokens)
|
|
self.kvcache = SimpleNamespace(
|
|
kv_cache_dim=cfg.kv_cache_dim,
|
|
store_dtype=SimpleNamespace(itemsize=cfg.kv_dtype_bytes),
|
|
index_head_dim=cfg.index_head_dim,
|
|
quant_block_size=128,
|
|
index_k_with_scale_buffer_dtype=SimpleNamespace(itemsize=1),
|
|
)
|
|
|
|
def available_size(self) -> int:
|
|
return self.available_tokens
|
|
|
|
def full_available_size(self) -> int:
|
|
return self.available_tokens
|
|
|
|
def swa_available_size(self) -> int:
|
|
return self.available_tokens
|
|
|
|
def consume(self, tokens: int) -> bool:
|
|
if tokens < 0:
|
|
raise ValueError(f"tokens must be non-negative, got {tokens}")
|
|
if tokens > self.available_tokens:
|
|
return False
|
|
self.available_tokens -= tokens
|
|
return True
|
|
|
|
def get_kvcache(self):
|
|
return self.kvcache
|
|
|
|
|
|
class FakeTreeCache:
|
|
def __init__(
|
|
self,
|
|
*,
|
|
allocator: FakeTokenAllocator,
|
|
page_size: int,
|
|
evictable_tokens: int,
|
|
consume_l2_load_back_capacity: bool,
|
|
):
|
|
self.allocator = allocator
|
|
self.page_size = int(page_size)
|
|
self._evictable_tokens = int(evictable_tokens)
|
|
self.consume_l2_load_back_capacity = bool(consume_l2_load_back_capacity)
|
|
self.disable = False
|
|
self.load_back_events: list[LoadBackEvent] = []
|
|
|
|
def supports_mamba(self) -> bool:
|
|
return False
|
|
|
|
def supports_swa(self) -> bool:
|
|
return False
|
|
|
|
def is_tree_cache(self) -> bool:
|
|
return True
|
|
|
|
def full_evictable_size(self) -> int:
|
|
return self._evictable_tokens
|
|
|
|
def swa_evictable_size(self) -> int:
|
|
return self._evictable_tokens
|
|
|
|
def evictable_size(self) -> int:
|
|
return self._evictable_tokens
|
|
|
|
def inc_lock_ref(self, _node):
|
|
from sglang.srt.mem_cache.base_prefix_cache import IncLockRefResult
|
|
|
|
return IncLockRefResult()
|
|
|
|
def dec_lock_ref(self, _node, *_args, **_kwargs):
|
|
from sglang.srt.mem_cache.base_prefix_cache import DecLockRefResult
|
|
|
|
return DecLockRefResult()
|
|
|
|
def init_load_back(self, params):
|
|
rid = getattr(params.last_host_node, "rid", "unknown")
|
|
requested = int(params.host_hit_length)
|
|
if requested <= 0:
|
|
return torch.empty((0,), dtype=torch.int64), params.last_host_node
|
|
|
|
paged = _ceil_to_page(requested, self.page_size)
|
|
before = self.allocator.available_size()
|
|
skipped_reason: Optional[str] = None
|
|
loaded = requested
|
|
|
|
if params.mem_quota is not None and paged > int(params.mem_quota):
|
|
skipped_reason = "over_mem_quota"
|
|
loaded = 0
|
|
elif self.consume_l2_load_back_capacity and not self.allocator.consume(paged):
|
|
skipped_reason = "allocator_capacity"
|
|
loaded = 0
|
|
|
|
after = self.allocator.available_size()
|
|
self.load_back_events.append(
|
|
LoadBackEvent(
|
|
rid=rid,
|
|
requested_tokens=requested,
|
|
paged_tokens=paged,
|
|
loaded_tokens=loaded,
|
|
mem_quota=params.mem_quota,
|
|
available_before=before,
|
|
available_after=after,
|
|
skipped_reason=skipped_reason,
|
|
)
|
|
)
|
|
|
|
if loaded <= 0:
|
|
return torch.empty((0,), dtype=torch.int64), params.last_host_node
|
|
return torch.arange(loaded, dtype=torch.int64), params.last_host_node
|
|
|
|
|
|
class FakeRunningBatch:
|
|
reqs: list = []
|
|
batch_is_full: bool = False
|
|
|
|
def release_req(self, _req):
|
|
return None
|
|
|
|
def filter_batch(self, *_args, **_kwargs):
|
|
return None
|
|
|
|
def is_empty(self) -> bool:
|
|
return not self.reqs
|
|
|
|
def batch_size(self) -> int:
|
|
return len(self.reqs)
|
|
|
|
|
|
class _FakeReq:
|
|
def __init__(self, spec: RequestSpec):
|
|
self.rid = spec.rid
|
|
self.priority = 0
|
|
self.output_ids = [0] * spec.output_tokens
|
|
self.sampling_params = SimpleNamespace(
|
|
max_new_tokens=spec.max_new_tokens,
|
|
ignore_eos=False,
|
|
)
|
|
self.time_stats = SimpleNamespace(wait_queue_entry_time=0.0)
|
|
self.host_hit_length = spec.l2_cached_tokens
|
|
self.prefix_indices = torch.arange(spec.l1_cached_tokens, dtype=torch.int64)
|
|
self.fill_ids = list(
|
|
range(spec.l1_cached_tokens + spec.l2_cached_tokens + spec.extend_tokens)
|
|
)
|
|
self.extend_input_len = spec.l2_cached_tokens + spec.extend_tokens
|
|
self.extend_logprob_start_len = 0
|
|
self.last_node = SimpleNamespace(rid=spec.rid)
|
|
self.last_host_node = SimpleNamespace(rid=spec.rid)
|
|
self.cache_protected_len = 0
|
|
|
|
def set_extend_input_len(self, value: int) -> None:
|
|
self.extend_input_len = int(value)
|
|
|
|
def finished(self) -> bool:
|
|
return False
|
|
|
|
|
|
def _ceil_to_page(tokens: int, page_size: int) -> int:
|
|
if tokens <= 0:
|
|
return 0
|
|
return int(math.ceil(tokens / float(page_size)) * page_size)
|
|
|
|
|
|
def _configure_scheduler_globals(enable_cp_context: bool) -> None:
|
|
_install_sgl_kernel_stubs()
|
|
from sglang.srt.server_args import ServerArgs, set_global_server_args_for_scheduler
|
|
|
|
if enable_cp_context:
|
|
set_global_server_args_for_scheduler(
|
|
ServerArgs(
|
|
model_path="dummy",
|
|
enable_nsa_prefill_context_parallel=True,
|
|
nsa_prefill_cp_mode="in-seq-split",
|
|
)
|
|
)
|
|
else:
|
|
set_global_server_args_for_scheduler(ServerArgs(model_path="dummy"))
|
|
|
|
|
|
def _make_prefill_adder(cfg: SchedulerBenchConfig, tree_cache: FakeTreeCache, allocator: FakeTokenAllocator):
|
|
_install_sgl_kernel_stubs()
|
|
from sglang.srt.managers.cp_shared_kv_prefill_buffer_estimator import (
|
|
CPSharedKVPrefillBufferEstimatorContext,
|
|
)
|
|
from sglang.srt.managers.schedule_policy import PrefillAdder
|
|
|
|
return PrefillAdder(
|
|
page_size=cfg.page_size,
|
|
tree_cache=tree_cache,
|
|
token_to_kv_pool_allocator=allocator,
|
|
running_batch=FakeRunningBatch(),
|
|
new_token_ratio=cfg.new_token_ratio,
|
|
rem_input_tokens=cfg.max_prefill_tokens,
|
|
rem_chunk_tokens=cfg.chunked_prefill_size,
|
|
mixed_with_decode_tokens=cfg.mixed_with_decode_tokens,
|
|
priority_scheduling_preemption_threshold=0,
|
|
enable_cp_shared_kv_prefill_bs_gt1=cfg.enable_cp_shared_kv_prefill_bs_gt1,
|
|
cp_shared_kv_prefill_max_batch_requests=cfg.cp_shared_kv_prefill_max_batch_requests,
|
|
cp_shared_kv_prefill_max_total_extend_tokens=cfg.cp_shared_kv_prefill_max_total_extend_tokens,
|
|
cp_shared_kv_prefill_max_total_cached_tokens=cfg.cp_shared_kv_prefill_max_total_cached_tokens,
|
|
cp_shared_kv_prefill_max_buffer_size=cfg.cp_shared_kv_prefill_max_buffer_size,
|
|
cp_shared_kv_prefill_buffer_estimator_context=(
|
|
CPSharedKVPrefillBufferEstimatorContext(
|
|
kvcache=allocator.get_kvcache(),
|
|
model_config=SimpleNamespace(vocab_size=cfg.vocab_size),
|
|
tp_size=cfg.tp_size,
|
|
page_size=cfg.page_size,
|
|
logprob_chunk_enabled=False,
|
|
logprob_chunk_size=2048,
|
|
bs_gt1_l1_prefetch_enabled=cfg.enable_bs_gt1_prefetch_estimate,
|
|
)
|
|
),
|
|
)
|
|
|
|
|
|
def _result_name(result) -> str:
|
|
return getattr(result, "name", str(result))
|
|
|
|
|
|
def _accepted_request(spec: RequestSpec, req: _FakeReq, loaded_l2_tokens: int) -> AcceptedRequest:
|
|
return AcceptedRequest(
|
|
rid=spec.rid,
|
|
l1_cached_tokens=spec.l1_cached_tokens,
|
|
l2_cached_tokens=spec.l2_cached_tokens,
|
|
loaded_l2_tokens=loaded_l2_tokens,
|
|
compute_extend_tokens=spec.extend_tokens,
|
|
initial_extend_tokens=spec.l2_cached_tokens + spec.extend_tokens,
|
|
effective_extend_tokens=int(req.extend_input_len),
|
|
max_new_tokens=spec.max_new_tokens,
|
|
)
|
|
|
|
|
|
def run_scheduler_admission_trace(
|
|
requests: list[RequestSpec], cfg: SchedulerBenchConfig
|
|
) -> TraceResult:
|
|
_configure_scheduler_globals(cfg.enable_cp_context)
|
|
from sglang.srt.managers.schedule_policy import AddReqResult
|
|
|
|
pending = list(requests)
|
|
ticks: list[TickResult] = []
|
|
allocator = FakeTokenAllocator(cfg.available_tokens, cfg)
|
|
tree_cache = FakeTreeCache(
|
|
allocator=allocator,
|
|
page_size=cfg.page_size,
|
|
evictable_tokens=cfg.evictable_tokens,
|
|
consume_l2_load_back_capacity=cfg.consume_l2_load_back_capacity,
|
|
)
|
|
|
|
for tick_idx in range(cfg.max_ticks):
|
|
if not pending:
|
|
break
|
|
|
|
adder = _make_prefill_adder(cfg, tree_cache, allocator)
|
|
req_by_obj: dict[object, tuple[RequestSpec, _FakeReq]] = {}
|
|
stopped_on_rid: Optional[str] = None
|
|
stopped_result: Optional[str] = None
|
|
load_event_start = len(tree_cache.load_back_events)
|
|
start = time.perf_counter()
|
|
|
|
for spec in pending:
|
|
req = _FakeReq(spec)
|
|
before_events = len(tree_cache.load_back_events)
|
|
result = adder.add_one_req(
|
|
req,
|
|
has_chunked_req=False,
|
|
truncation_align_size=None,
|
|
)
|
|
after_events = len(tree_cache.load_back_events)
|
|
|
|
if req in adder.can_run_list:
|
|
req_by_obj[req] = (spec, req)
|
|
|
|
if result != AddReqResult.CONTINUE:
|
|
stopped_on_rid = spec.rid
|
|
stopped_result = _result_name(result)
|
|
# If the stopping request was still accepted, keep it in the
|
|
# batch just like the real scheduler does before breaking.
|
|
if req in adder.can_run_list:
|
|
req_by_obj[req] = (spec, req)
|
|
break
|
|
|
|
# Keep loop variables observable under debugger without changing
|
|
# behavior; this also makes the loadback event span explicit.
|
|
_ = before_events, after_events
|
|
|
|
duration_us = (time.perf_counter() - start) * 1_000_000.0
|
|
loaded_by_rid: dict[str, int] = {}
|
|
for event in tree_cache.load_back_events[load_event_start:]:
|
|
loaded_by_rid[event.rid] = loaded_by_rid.get(event.rid, 0) + event.loaded_tokens
|
|
|
|
accepted = [
|
|
_accepted_request(spec, req, loaded_by_rid.get(spec.rid, 0))
|
|
for req in adder.can_run_list
|
|
for spec, req in [req_by_obj[req]]
|
|
]
|
|
accepted_rids = {req.rid for req in accepted}
|
|
pending = [spec for spec in pending if spec.rid not in accepted_rids]
|
|
|
|
peak_estimate = adder.cp_shared_kv_prefill_last_buffer_estimate
|
|
peak_breakdown = (
|
|
{
|
|
"layer_forward_peak_bytes": peak_estimate.layer_forward_peak_bytes,
|
|
"logits_window_peak_bytes": peak_estimate.logits_window_peak_bytes,
|
|
"load_back_window_peak_bytes": peak_estimate.load_back_window_peak_bytes,
|
|
"materialize_peak_bytes": peak_estimate.materialize_peak_bytes,
|
|
"prefetch_peak_bytes": peak_estimate.prefetch_peak_bytes,
|
|
"logits_peak_bytes": peak_estimate.logits_peak_bytes,
|
|
"remap_peak_bytes": peak_estimate.remap_peak_bytes,
|
|
"transfer_descriptor_peak_bytes": peak_estimate.transfer_descriptor_peak_bytes,
|
|
"backup_descriptor_peak_bytes": peak_estimate.backup_descriptor_peak_bytes,
|
|
}
|
|
if peak_estimate is not None
|
|
else {}
|
|
)
|
|
ticks.append(
|
|
TickResult(
|
|
tick=tick_idx,
|
|
accepted=accepted,
|
|
stopped_on_rid=stopped_on_rid,
|
|
stopped_result=stopped_result,
|
|
rem_input_tokens_after_tick=int(adder.rem_input_tokens),
|
|
rem_total_tokens_after_tick=float(adder.rem_total_tokens),
|
|
cur_rem_tokens_after_tick=float(adder.cur_rem_tokens),
|
|
cp_total_extend_tokens=int(adder.cp_shared_kv_prefill_total_extend_tokens),
|
|
cp_total_cached_tokens=int(adder.cp_shared_kv_prefill_total_cached_tokens),
|
|
log_hit_tokens=int(adder.log_hit_tokens),
|
|
log_input_tokens=int(adder.log_input_tokens),
|
|
allocator_available_after_tick=allocator.available_size(),
|
|
load_back_events=list(tree_cache.load_back_events[load_event_start:]),
|
|
cp_estimated_peak_buffer_bytes=int(
|
|
adder.cp_shared_kv_prefill_estimated_peak_buffer_bytes
|
|
),
|
|
cp_buffer_breakdown=peak_breakdown,
|
|
duration_us=duration_us,
|
|
)
|
|
)
|
|
|
|
if not accepted:
|
|
break
|
|
|
|
return TraceResult(
|
|
config=cfg,
|
|
request_count=len(requests),
|
|
ticks=ticks,
|
|
remaining_rids=[spec.rid for spec in pending],
|
|
blocked=bool(pending),
|
|
)
|
|
|
|
|
|
def _parse_int_list(value: str | Iterable[int]) -> list[int]:
|
|
if isinstance(value, str):
|
|
return [int(item.strip()) for item in value.split(",") if item.strip()]
|
|
return [int(item) for item in value]
|
|
|
|
|
|
def _load_requests_jsonl(path: Path) -> list[RequestSpec]:
|
|
requests: list[RequestSpec] = []
|
|
with path.open("r", encoding="utf-8") as f:
|
|
for line_no, line in enumerate(f, start=1):
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
data = json.loads(line)
|
|
try:
|
|
requests.append(RequestSpec(**data))
|
|
except TypeError as exc:
|
|
raise ValueError(f"invalid request at {path}:{line_no}: {data}") from exc
|
|
return requests
|
|
|
|
|
|
def _build_synthetic_requests(args) -> list[RequestSpec]:
|
|
requests: list[RequestSpec] = []
|
|
rid = 0
|
|
for l1 in _parse_int_list(args.l1_cached_tokens):
|
|
for l2 in _parse_int_list(args.l2_cached_tokens):
|
|
for extend in _parse_int_list(args.extend_tokens):
|
|
for _ in range(args.repeat_per_case):
|
|
requests.append(
|
|
RequestSpec(
|
|
rid=f"r{rid}_l1{l1}_l2{l2}_e{extend}",
|
|
l1_cached_tokens=l1,
|
|
l2_cached_tokens=l2,
|
|
extend_tokens=extend,
|
|
max_new_tokens=args.max_new_tokens,
|
|
)
|
|
)
|
|
rid += 1
|
|
return requests
|
|
|
|
|
|
def _trace_to_dict(trace: TraceResult) -> dict:
|
|
return asdict(trace)
|
|
|
|
|
|
def _print_text(trace: TraceResult) -> None:
|
|
print(
|
|
"config "
|
|
f"page_size={trace.config.page_size} available={trace.config.available_tokens} "
|
|
f"evictable={trace.config.evictable_tokens} max_prefill={trace.config.max_prefill_tokens} "
|
|
f"cp_extend_limit={trace.config.cp_shared_kv_prefill_max_total_extend_tokens} "
|
|
f"cp_cached_limit={trace.config.cp_shared_kv_prefill_max_total_cached_tokens} "
|
|
f"cp_buffer_limit={trace.config.cp_shared_kv_prefill_max_buffer_size}"
|
|
)
|
|
for tick in trace.ticks:
|
|
accepted = ",".join(
|
|
f"{req.rid}(l1={req.l1_cached_tokens},l2={req.l2_cached_tokens},"
|
|
f"loaded={req.loaded_l2_tokens},extend={req.effective_extend_tokens})"
|
|
for req in tick.accepted
|
|
)
|
|
print(
|
|
f"tick={tick.tick} bs={len(tick.accepted)} accepted=[{accepted}] "
|
|
f"stop={tick.stopped_on_rid}:{tick.stopped_result} "
|
|
f"cp_extend={tick.cp_total_extend_tokens} cp_cached={tick.cp_total_cached_tokens} "
|
|
f"cp_peak_buffer={tick.cp_estimated_peak_buffer_bytes} "
|
|
f"log_hit={tick.log_hit_tokens} "
|
|
f"log_input={tick.log_input_tokens} rem_input={tick.rem_input_tokens_after_tick} "
|
|
f"rem_total={tick.rem_total_tokens_after_tick:.1f} "
|
|
f"cur_rem={tick.cur_rem_tokens_after_tick:.1f} "
|
|
f"allocator_available={tick.allocator_available_after_tick} "
|
|
f"duration_us={tick.duration_us:.1f}"
|
|
)
|
|
for event in tick.load_back_events:
|
|
print(
|
|
f" load_back rid={event.rid} requested={event.requested_tokens} "
|
|
f"paged={event.paged_tokens} loaded={event.loaded_tokens} "
|
|
f"quota={event.mem_quota} avail={event.available_before}->{event.available_after} "
|
|
f"skip={event.skipped_reason}"
|
|
)
|
|
if trace.remaining_rids:
|
|
print(f"remaining={','.join(trace.remaining_rids)} blocked={trace.blocked}")
|
|
|
|
|
|
def build_arg_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
source = parser.add_mutually_exclusive_group(required=True)
|
|
source.add_argument("--requests-jsonl", type=Path)
|
|
source.add_argument("--synthetic-grid", action="store_true")
|
|
parser.add_argument("--l1-cached-tokens", default="0,4096,32768")
|
|
parser.add_argument("--l2-cached-tokens", default="0,4096,32768")
|
|
parser.add_argument("--extend-tokens", default="128,512,2048,8192")
|
|
parser.add_argument("--repeat-per-case", type=int, default=1)
|
|
parser.add_argument("--max-new-tokens", type=int, default=1)
|
|
parser.add_argument("--page-size", type=int, default=64)
|
|
parser.add_argument("--available-tokens", type=int, default=1_000_000)
|
|
parser.add_argument("--evictable-tokens", type=int, default=0)
|
|
parser.add_argument("--max-prefill-tokens", type=int, default=16_384)
|
|
parser.add_argument("--chunked-prefill-size", type=int, default=None)
|
|
parser.add_argument("--mixed-with-decode-tokens", type=int, default=0)
|
|
parser.add_argument("--max-ticks", type=int, default=1)
|
|
parser.add_argument("--disable-cp-context", action="store_true")
|
|
parser.add_argument("--disable-cp-bs-gt1", action="store_true")
|
|
parser.add_argument("--cp-max-batch-requests", type=int, default=8)
|
|
parser.add_argument("--cp-max-total-extend-tokens", type=int, default=65_536)
|
|
parser.add_argument("--cp-max-total-cached-tokens", type=int, default=None)
|
|
parser.add_argument(
|
|
"--cp-max-buffer-size",
|
|
type=float,
|
|
default=None,
|
|
help="CP shared-KV estimated temp-buffer gate in decimal GB.",
|
|
)
|
|
parser.add_argument("--kv-cache-dim", type=int, default=656)
|
|
parser.add_argument("--kv-dtype-bytes", type=int, default=1)
|
|
parser.add_argument("--index-head-dim", type=int, default=128)
|
|
parser.add_argument("--vocab-size", type=int, default=128_000)
|
|
parser.add_argument("--tp-size", type=int, default=8)
|
|
parser.add_argument("--enable-bs-gt1-prefetch-estimate", action="store_true")
|
|
parser.add_argument("--no-consume-l2-load-back-capacity", action="store_true")
|
|
parser.add_argument("--output", choices=("text", "json"), default="text")
|
|
return parser
|
|
|
|
|
|
def main(argv: Optional[list[str]] = None) -> int:
|
|
args = build_arg_parser().parse_args(argv)
|
|
if args.requests_jsonl is not None:
|
|
requests = _load_requests_jsonl(args.requests_jsonl)
|
|
else:
|
|
requests = _build_synthetic_requests(args)
|
|
|
|
cfg = SchedulerBenchConfig(
|
|
page_size=args.page_size,
|
|
available_tokens=args.available_tokens,
|
|
evictable_tokens=args.evictable_tokens,
|
|
max_prefill_tokens=args.max_prefill_tokens,
|
|
chunked_prefill_size=args.chunked_prefill_size,
|
|
mixed_with_decode_tokens=args.mixed_with_decode_tokens,
|
|
enable_cp_context=not args.disable_cp_context,
|
|
enable_cp_shared_kv_prefill_bs_gt1=not args.disable_cp_bs_gt1,
|
|
cp_shared_kv_prefill_max_batch_requests=args.cp_max_batch_requests,
|
|
cp_shared_kv_prefill_max_total_extend_tokens=args.cp_max_total_extend_tokens,
|
|
cp_shared_kv_prefill_max_total_cached_tokens=args.cp_max_total_cached_tokens,
|
|
cp_shared_kv_prefill_max_buffer_size=(
|
|
None
|
|
if args.cp_max_buffer_size is None
|
|
else int(args.cp_max_buffer_size * 1e9)
|
|
),
|
|
kv_cache_dim=args.kv_cache_dim,
|
|
kv_dtype_bytes=args.kv_dtype_bytes,
|
|
index_head_dim=args.index_head_dim,
|
|
vocab_size=args.vocab_size,
|
|
tp_size=args.tp_size,
|
|
enable_bs_gt1_prefetch_estimate=args.enable_bs_gt1_prefetch_estimate,
|
|
max_ticks=args.max_ticks,
|
|
consume_l2_load_back_capacity=not args.no_consume_l2_load_back_capacity,
|
|
)
|
|
trace = run_scheduler_admission_trace(requests, cfg)
|
|
if args.output == "json":
|
|
print(json.dumps(_trace_to_dict(trace), indent=2, sort_keys=True))
|
|
else:
|
|
_print_text(trace)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|