perf: add --prefill-only-disable-kv-cache to skip KV pool allocation (#23675)
This commit is contained in:
@@ -1123,6 +1123,116 @@ class MHATokenToKVPool(KVCache):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NoOpMHATokenToKVPool(MHATokenToKVPool):
|
||||||
|
"""KV cache pool that skips physical K/V buffer allocation.
|
||||||
|
|
||||||
|
Used in embedding-mode prefill-only workloads with the FA
|
||||||
|
fa_skip_kv_cache path, where no layer reads or writes KV cache because
|
||||||
|
attention uses raw K/V via flash_attn_varlen_func. Other prefill-only paths
|
||||||
|
such as scoring/MIS may benefit from the same idea later, but some still
|
||||||
|
stage K/V through paged cache today.
|
||||||
|
|
||||||
|
This class keeps the scheduler's view of pool capacity (self.size is
|
||||||
|
honored for admission) but allocates only (page_size, head_num, head_dim)
|
||||||
|
placeholder tensors per layer to satisfy any code paths that dereference
|
||||||
|
the buffers.
|
||||||
|
|
||||||
|
Callers MUST ensure no real set_kv_buffer/get_*_buffer calls happen against
|
||||||
|
this pool; those paths raise loudly so misuse is visible.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _create_buffers(self):
|
||||||
|
# Allocate minimal placeholder buffers. They exist purely so that code
|
||||||
|
# paths holding `k_buffer` / `v_buffer` references (pointer tables,
|
||||||
|
# layer-transfer counters, stride arithmetic) keep working without
|
||||||
|
# None-guards scattered across the codebase. Shape is
|
||||||
|
# [page_size, head_num, head_dim] per layer so that the unconditional
|
||||||
|
# `key_cache.view(-1, page_size, head_num, head_dim)` in the FA backend
|
||||||
|
# at the top of forward_extend succeeds regardless of --page-size.
|
||||||
|
# Total footprint is still on the order of KB vs GBs for a real pool.
|
||||||
|
with self.memory_saver_adapter.region(GPU_MEMORY_TYPE_KV_CACHE):
|
||||||
|
self.k_buffer = [
|
||||||
|
torch.zeros(
|
||||||
|
(self.page_size, self.head_num, self.head_dim),
|
||||||
|
dtype=self.store_dtype,
|
||||||
|
device=self.device,
|
||||||
|
)
|
||||||
|
for _ in range(self.layer_num)
|
||||||
|
]
|
||||||
|
self.v_buffer = [
|
||||||
|
torch.zeros(
|
||||||
|
(self.page_size, self.head_num, self.v_head_dim),
|
||||||
|
dtype=self.store_dtype,
|
||||||
|
device=self.device,
|
||||||
|
)
|
||||||
|
for _ in range(self.layer_num)
|
||||||
|
]
|
||||||
|
|
||||||
|
self.k_data_ptrs = torch.tensor(
|
||||||
|
[x.data_ptr() for x in self.k_buffer],
|
||||||
|
dtype=torch.uint64,
|
||||||
|
device=self.device,
|
||||||
|
)
|
||||||
|
self.v_data_ptrs = torch.tensor(
|
||||||
|
[x.data_ptr() for x in self.v_buffer],
|
||||||
|
dtype=torch.uint64,
|
||||||
|
device=self.device,
|
||||||
|
)
|
||||||
|
self.data_ptrs = torch.cat([self.k_data_ptrs, self.v_data_ptrs], dim=0)
|
||||||
|
self.data_strides = torch.tensor(
|
||||||
|
[
|
||||||
|
np.prod(x.shape[1:]) * x.dtype.itemsize
|
||||||
|
for x in self.k_buffer + self.v_buffer
|
||||||
|
],
|
||||||
|
device=self.device,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _finalize_allocation_log(self, num_tokens: int):
|
||||||
|
self.mem_usage = 0.0
|
||||||
|
placeholder_bytes = (
|
||||||
|
2
|
||||||
|
* self.layer_num
|
||||||
|
* self.page_size
|
||||||
|
* self.head_num
|
||||||
|
* max(self.head_dim, self.v_head_dim)
|
||||||
|
* self.store_dtype.itemsize
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"KV Cache skipped (no-op pool). Logical #tokens: {num_tokens}, "
|
||||||
|
f"physical K/V size: ~{placeholder_bytes / 1024:.1f} KB placeholder"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_kv_size_bytes(self):
|
||||||
|
# Report zero so downstream memory accounting matches reality.
|
||||||
|
return (0, 0)
|
||||||
|
|
||||||
|
def set_kv_buffer(self, *args, **kwargs):
|
||||||
|
raise RuntimeError(
|
||||||
|
"NoOpMHATokenToKVPool.set_kv_buffer was called. This pool is only "
|
||||||
|
"valid in prefill-only modes (e.g. --is-embedding, scoring) with "
|
||||||
|
"the FA backend's fa_skip_kv_cache path active; the attention "
|
||||||
|
"backend must never write to it. Check that the workload truly "
|
||||||
|
"performs no decode and that the FA backend's fa_skip_kv_cache "
|
||||||
|
"preconditions are met."
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_key_buffer(self, layer_id: int):
|
||||||
|
# Return the placeholder. The FA backend reads this before taking the
|
||||||
|
# fa_skip_kv_cache branch (which does not use it); the placeholder shape
|
||||||
|
# is (page_size, head_num, head_dim) so downstream .view() calls succeed.
|
||||||
|
return self.k_buffer[layer_id - self.start_layer]
|
||||||
|
|
||||||
|
def get_value_buffer(self, layer_id: int):
|
||||||
|
return self.v_buffer[layer_id - self.start_layer]
|
||||||
|
|
||||||
|
def get_kv_buffer(self, layer_id: int):
|
||||||
|
return self.get_key_buffer(layer_id), self.get_value_buffer(layer_id)
|
||||||
|
|
||||||
|
def move_kv_cache(self, tgt_loc: torch.Tensor, src_loc: torch.Tensor):
|
||||||
|
# no-op; embedding mode has no KV cache to move
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
class MHATokenToKVPoolFP4(MHATokenToKVPool):
|
class MHATokenToKVPoolFP4(MHATokenToKVPool):
|
||||||
|
|
||||||
def _create_buffers(self):
|
def _create_buffers(self):
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from sglang.srt.mem_cache.memory_pool import (
|
|||||||
MHATokenToKVPoolFP4,
|
MHATokenToKVPoolFP4,
|
||||||
MLATokenToKVPool,
|
MLATokenToKVPool,
|
||||||
MLATokenToKVPoolFP4,
|
MLATokenToKVPoolFP4,
|
||||||
|
NoOpMHATokenToKVPool,
|
||||||
NSATokenToKVPool,
|
NSATokenToKVPool,
|
||||||
ReqToTokenPool,
|
ReqToTokenPool,
|
||||||
)
|
)
|
||||||
@@ -196,6 +197,44 @@ class ModelRunnerKVCacheMixin:
|
|||||||
|
|
||||||
return MAMBA_CACHE_SIZE_MAX_RUNNING_REQUESTS_RATIO + additional_ratio
|
return MAMBA_CACHE_SIZE_MAX_RUNNING_REQUESTS_RATIO + additional_ratio
|
||||||
|
|
||||||
|
def _validate_prefill_only_disable_kv_cache_pool_family(
|
||||||
|
self: ModelRunner,
|
||||||
|
is_nsa_model: bool,
|
||||||
|
is_dsv4_model: bool,
|
||||||
|
current_platform,
|
||||||
|
):
|
||||||
|
if not self.server_args.prefill_only_disable_kv_cache or self.is_draft_worker:
|
||||||
|
return
|
||||||
|
|
||||||
|
unsupported_pool_family = None
|
||||||
|
if is_dsv4_model:
|
||||||
|
unsupported_pool_family = "DeepSeekV4TokenToKVPool"
|
||||||
|
elif current_platform.is_out_of_tree() and not self.mambaish_config:
|
||||||
|
unsupported_pool_family = "out-of-tree platform KV pool"
|
||||||
|
elif (
|
||||||
|
self.server_args.attention_backend == "ascend" and not self.mambaish_config
|
||||||
|
):
|
||||||
|
unsupported_pool_family = "NPU/Ascend KV pool"
|
||||||
|
elif self.use_mla_backend and is_nsa_model:
|
||||||
|
unsupported_pool_family = "NSA/MLA KV pool"
|
||||||
|
elif self.use_mla_backend and not self.mambaish_config:
|
||||||
|
unsupported_pool_family = "MLA KV pool"
|
||||||
|
elif self.is_hybrid_swa:
|
||||||
|
unsupported_pool_family = "SWA KV pool"
|
||||||
|
elif self.mambaish_config:
|
||||||
|
unsupported_pool_family = "hybrid linear/Mamba KV pool"
|
||||||
|
elif is_float4_e2m1fn_x2(self.kv_cache_dtype):
|
||||||
|
unsupported_pool_family = "FP4 MHA KV pool"
|
||||||
|
|
||||||
|
if unsupported_pool_family is not None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"--prefill-only-disable-kv-cache is not supported for "
|
||||||
|
f"{unsupported_pool_family}. Supported configurations today: plain MHA "
|
||||||
|
"models on CUDA with the FA (fa3/fa4) prefill backend, --is-embedding, "
|
||||||
|
"--chunked-prefill-size=-1, --disable-radix-cache, no context-parallel "
|
||||||
|
"attention, no HiSparse, and --kv-cache-dtype != fp4_e2m1."
|
||||||
|
)
|
||||||
|
|
||||||
def _init_pools(self: ModelRunner):
|
def _init_pools(self: ModelRunner):
|
||||||
"""Initialize the memory pools."""
|
"""Initialize the memory pools."""
|
||||||
max_num_reqs = self.max_running_requests
|
max_num_reqs = self.max_running_requests
|
||||||
@@ -292,6 +331,10 @@ class ModelRunnerKVCacheMixin:
|
|||||||
# Out-of-tree platform plugin system — used by elif below
|
# Out-of-tree platform plugin system — used by elif below
|
||||||
from sglang.srt.platforms import current_platform
|
from sglang.srt.platforms import current_platform
|
||||||
|
|
||||||
|
self._validate_prefill_only_disable_kv_cache_pool_family(
|
||||||
|
is_nsa_model, is_dsv4_model, current_platform
|
||||||
|
)
|
||||||
|
|
||||||
if is_dsv4_model:
|
if is_dsv4_model:
|
||||||
swa_page_size = self.page_size
|
swa_page_size = self.page_size
|
||||||
assert swa_page_size == 256, "In paged swa mode, page_size must be 256."
|
assert swa_page_size == 256, "In paged swa mode, page_size must be 256."
|
||||||
@@ -591,7 +634,12 @@ class ModelRunnerKVCacheMixin:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.token_to_kv_pool = MHATokenToKVPool(
|
pool_cls = (
|
||||||
|
NoOpMHATokenToKVPool
|
||||||
|
if self.server_args.prefill_only_disable_kv_cache
|
||||||
|
else MHATokenToKVPool
|
||||||
|
)
|
||||||
|
self.token_to_kv_pool = pool_cls(
|
||||||
self.max_total_num_tokens,
|
self.max_total_num_tokens,
|
||||||
page_size=self.page_size,
|
page_size=self.page_size,
|
||||||
dtype=self.kv_cache_dtype,
|
dtype=self.kv_cache_dtype,
|
||||||
@@ -719,6 +767,24 @@ class ModelRunnerKVCacheMixin:
|
|||||||
swa_allocator.full_to_swa_index_mapping
|
swa_allocator.full_to_swa_index_mapping
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Defensive check: the explicit validation above should reject known
|
||||||
|
# unsupported pool families before allocation. Keep this guard here so
|
||||||
|
# future pool-selection refactors fail at boot instead of on first use.
|
||||||
|
if (
|
||||||
|
self.server_args.prefill_only_disable_kv_cache
|
||||||
|
and not self.is_draft_worker
|
||||||
|
and not isinstance(self.token_to_kv_pool, NoOpMHATokenToKVPool)
|
||||||
|
):
|
||||||
|
raise RuntimeError(
|
||||||
|
"--prefill-only-disable-kv-cache expected NoOpMHATokenToKVPool but the "
|
||||||
|
f"runtime pool is {type(self.token_to_kv_pool).__name__}. This pool "
|
||||||
|
"family is not yet supported by --prefill-only-disable-kv-cache. "
|
||||||
|
"Supported configurations today: plain MHA models on CUDA with the FA "
|
||||||
|
"(fa3/fa4) prefill backend, --is-embedding, --chunked-prefill-size=-1, "
|
||||||
|
"--disable-radix-cache, no context-parallel attention, no HiSparse, "
|
||||||
|
"and --kv-cache-dtype != fp4_e2m1."
|
||||||
|
)
|
||||||
|
|
||||||
def _apply_token_constraints(self: ModelRunner, token_capacity: int) -> int:
|
def _apply_token_constraints(self: ModelRunner, token_capacity: int) -> int:
|
||||||
"""Apply external constraints to token capacity: user cap, PP sync.
|
"""Apply external constraints to token capacity: user cap, PP sync.
|
||||||
|
|
||||||
|
|||||||
@@ -365,6 +365,7 @@ class ServerArgs:
|
|||||||
trust_remote_code: bool = False
|
trust_remote_code: bool = False
|
||||||
context_length: Optional[int] = None
|
context_length: Optional[int] = None
|
||||||
is_embedding: bool = False
|
is_embedding: bool = False
|
||||||
|
prefill_only_disable_kv_cache: bool = False
|
||||||
enable_multimodal: Optional[bool] = None
|
enable_multimodal: Optional[bool] = None
|
||||||
revision: Optional[str] = None
|
revision: Optional[str] = None
|
||||||
model_impl: str = "auto"
|
model_impl: str = "auto"
|
||||||
@@ -846,6 +847,10 @@ class ServerArgs:
|
|||||||
# Validate PD disaggregation flags early (before dummy-model short-circuit).
|
# Validate PD disaggregation flags early (before dummy-model short-circuit).
|
||||||
self._handle_pd_disaggregation()
|
self._handle_pd_disaggregation()
|
||||||
|
|
||||||
|
# Validate --prefill-only-disable-kv-cache args early (before dummy-model
|
||||||
|
# short-circuit). The backend check is run later after backends settle.
|
||||||
|
self._validate_prefill_only_disable_kv_cache_args()
|
||||||
|
|
||||||
if self.model_path.lower() in ["none", "dummy"]:
|
if self.model_path.lower() in ["none", "dummy"]:
|
||||||
# Skip for dummy models
|
# Skip for dummy models
|
||||||
return
|
return
|
||||||
@@ -914,6 +919,13 @@ class ServerArgs:
|
|||||||
# the final attention backend and chunked_prefill_size are in effect.
|
# the final attention backend and chunked_prefill_size are in effect.
|
||||||
self._handle_multi_item_scoring()
|
self._handle_multi_item_scoring()
|
||||||
|
|
||||||
|
# Backend-dependent half of --prefill-only-disable-kv-cache validation.
|
||||||
|
# Must stay after _handle_attention_backend_compatibility() (above) and
|
||||||
|
# _handle_multi_item_scoring() so the resolved prefill backend is final;
|
||||||
|
# the flag/precondition half runs earlier in
|
||||||
|
# _validate_prefill_only_disable_kv_cache_args().
|
||||||
|
self._handle_prefill_only_disable_kv_cache()
|
||||||
|
|
||||||
# Handle Hicache settings.
|
# Handle Hicache settings.
|
||||||
self._handle_hicache()
|
self._handle_hicache()
|
||||||
|
|
||||||
@@ -3243,6 +3255,101 @@ class ServerArgs:
|
|||||||
"Pipeline parallelism is incompatible with overlap schedule."
|
"Pipeline parallelism is incompatible with overlap schedule."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _validate_prefill_only_disable_kv_cache_args(self):
|
||||||
|
"""Validate --prefill-only-disable-kv-cache flag/precondition constraints.
|
||||||
|
|
||||||
|
Runs before the dummy-model short-circuit so misuse is rejected even
|
||||||
|
for dummy models. Backend resolution is checked separately by
|
||||||
|
_handle_prefill_only_disable_kv_cache after backends settle.
|
||||||
|
"""
|
||||||
|
if not self.prefill_only_disable_kv_cache:
|
||||||
|
return
|
||||||
|
|
||||||
|
# This flag is intentionally scoped to embedding mode for now. Other
|
||||||
|
# prefill-only paths (for example scoring and MIS) can benefit from
|
||||||
|
# the same idea later, but some of them still stage K/V through the
|
||||||
|
# paged cache today.
|
||||||
|
if not self.is_embedding:
|
||||||
|
raise ValueError(
|
||||||
|
"--prefill-only-disable-kv-cache currently requires --is-embedding. "
|
||||||
|
"Other prefill-only workloads may be supported in a future change once "
|
||||||
|
"their attention paths stop reading or writing the paged KV cache."
|
||||||
|
)
|
||||||
|
if self.kv_cache_dtype == "fp4_e2m1":
|
||||||
|
raise ValueError(
|
||||||
|
"--prefill-only-disable-kv-cache does not currently support "
|
||||||
|
"--kv-cache-dtype=fp4_e2m1 because the FP4 pool uses a separate "
|
||||||
|
"allocation path."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Structural preconditions for the FA backend's fa_skip_kv_cache path,
|
||||||
|
# which is the only embedding path that doesn't read or write the pool:
|
||||||
|
# - chunked_prefill_size == -1 keeps a request in a single forward,
|
||||||
|
# so K/V never has to be reused across prefill chunks.
|
||||||
|
# - disable_radix_cache stops the prefix cache from indexing pool
|
||||||
|
# slots that no longer hold real data.
|
||||||
|
if self.chunked_prefill_size != -1:
|
||||||
|
raise ValueError(
|
||||||
|
"--prefill-only-disable-kv-cache requires --chunked-prefill-size=-1 so the FA "
|
||||||
|
"backend takes the fa_skip_kv_cache path; otherwise the pool would be touched "
|
||||||
|
"between prefill chunks."
|
||||||
|
)
|
||||||
|
if not self.disable_radix_cache:
|
||||||
|
raise ValueError(
|
||||||
|
"--prefill-only-disable-kv-cache requires --disable-radix-cache because the "
|
||||||
|
"radix cache indexes KV pool slots that no longer hold real data."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Context-parallel prefill stages K/V through cp_allgather_and_save_kv_cache,
|
||||||
|
# which writes to the pool via set_kv_buffer. NoOpMHATokenToKVPool intentionally
|
||||||
|
# raises on writes, so the engine would boot fine but fail on the first request.
|
||||||
|
if self.attn_cp_size > 1:
|
||||||
|
raise ValueError(
|
||||||
|
"--prefill-only-disable-kv-cache is incompatible with --attn-cp-size > 1: "
|
||||||
|
"the context-parallel attention path writes K/V to the pool via set_kv_buffer, "
|
||||||
|
"which the no-op pool intentionally rejects."
|
||||||
|
)
|
||||||
|
if self.enable_prefill_context_parallel:
|
||||||
|
raise ValueError(
|
||||||
|
"--prefill-only-disable-kv-cache is incompatible with "
|
||||||
|
"--enable-prefill-context-parallel: the prefill-CP path stages K/V through "
|
||||||
|
"the paged cache, which the no-op pool does not support."
|
||||||
|
)
|
||||||
|
|
||||||
|
# HiSparse selects a different pool class (HiSparseNSATokenToKVPool /
|
||||||
|
# HiSparseTokenToKVPoolAllocator) that is not the no-op pool.
|
||||||
|
if self.enable_hisparse:
|
||||||
|
raise ValueError(
|
||||||
|
"--prefill-only-disable-kv-cache is incompatible with --enable-hisparse: "
|
||||||
|
"HiSparse uses a dedicated pool family that is not the no-op MHA pool."
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_prefill_only_disable_kv_cache(self):
|
||||||
|
"""Validate --prefill-only-disable-kv-cache backend constraint.
|
||||||
|
|
||||||
|
Must run after _handle_attention_backend_compatibility() (which fills
|
||||||
|
the default attention_backend if unset) and _handle_multi_item_scoring()
|
||||||
|
(which may further mutate it). The assertion below guards against
|
||||||
|
accidental call-site reordering: if attention_backend is still None,
|
||||||
|
backends haven't settled yet and get_attention_backends() would return
|
||||||
|
a stale (None, None).
|
||||||
|
"""
|
||||||
|
if not self.prefill_only_disable_kv_cache:
|
||||||
|
return
|
||||||
|
|
||||||
|
assert self.attention_backend is not None, (
|
||||||
|
"_handle_prefill_only_disable_kv_cache must run after "
|
||||||
|
"_handle_attention_backend_compatibility() so the prefill backend is resolved."
|
||||||
|
)
|
||||||
|
|
||||||
|
prefill_backend, _ = self.get_attention_backends()
|
||||||
|
if prefill_backend not in ("fa3", "fa4"):
|
||||||
|
raise ValueError(
|
||||||
|
"--prefill-only-disable-kv-cache currently requires the FA prefill backend "
|
||||||
|
f"(fa3/fa4), but got prefill backend {prefill_backend!r}. Other prefill-only "
|
||||||
|
"workloads and backends may be supported in a future change."
|
||||||
|
)
|
||||||
|
|
||||||
def _handle_hicache(self):
|
def _handle_hicache(self):
|
||||||
"""Normalize hicache-related knobs into a valid runtime configuration.
|
"""Normalize hicache-related knobs into a valid runtime configuration.
|
||||||
|
|
||||||
@@ -4383,6 +4490,11 @@ class ServerArgs:
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Whether to use a CausalLM as an embedding model.",
|
help="Whether to use a CausalLM as an embedding model.",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--prefill-only-disable-kv-cache",
|
||||||
|
action="store_true",
|
||||||
|
help="Skip the physical KV cache allocation for embedding-mode prefill-only workloads. Currently only valid with --is-embedding, --chunked-prefill-size=-1, --disable-radix-cache, an FA prefill backend, and non-FP4 KV cache so the fa_skip_kv_cache path is active (no layer reads or writes the cache). Other prefill-only workloads such as scoring/MIS may benefit from this later once their attention paths stop using paged KV. Scheduler admission accounting is unchanged; per-layer K/V tensors are sized to (page_size, head_num, head_dim) placeholders so GPU memory is not wasted.",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--enable-multimodal",
|
"--enable-multimodal",
|
||||||
default=ServerArgs.enable_multimodal,
|
default=ServerArgs.enable_multimodal,
|
||||||
|
|||||||
@@ -515,5 +515,63 @@ class TestNgramExternalSamArgs(CustomTestCase):
|
|||||||
self.assertIn("external-corpus-max-tokens", str(context.exception))
|
self.assertIn("external-corpus-max-tokens", str(context.exception))
|
||||||
|
|
||||||
|
|
||||||
|
class TestPrefillOnlyDisableKvCache(unittest.TestCase):
|
||||||
|
"""Validation for --prefill-only-disable-kv-cache.
|
||||||
|
|
||||||
|
The flag wires NoOpMHATokenToKVPool, which is only safe when:
|
||||||
|
- the engine is in embedding mode (fa_skip_kv_cache active in FA backend),
|
||||||
|
- chunked_prefill_size == -1 (no inter-chunk K/V reuse),
|
||||||
|
- disable_radix_cache (radix cache otherwise indexes empty pool slots),
|
||||||
|
- no context-parallel attention (CP writes to the pool via set_kv_buffer),
|
||||||
|
- no HiSparse (uses a different pool family),
|
||||||
|
- kv_cache_dtype != fp4_e2m1 (FP4 pool is a separate allocation path).
|
||||||
|
All other configurations must be rejected at __post_init__ time so users
|
||||||
|
get a clear error before model load.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _base_kwargs(self, **overrides):
|
||||||
|
kwargs = dict(
|
||||||
|
model_path="dummy",
|
||||||
|
is_embedding=True,
|
||||||
|
chunked_prefill_size=-1,
|
||||||
|
disable_radix_cache=True,
|
||||||
|
prefill_only_disable_kv_cache=True,
|
||||||
|
)
|
||||||
|
kwargs.update(overrides)
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def test_valid_minimal_config_constructs(self):
|
||||||
|
sa = ServerArgs(**self._base_kwargs())
|
||||||
|
self.assertTrue(sa.prefill_only_disable_kv_cache)
|
||||||
|
|
||||||
|
def test_rejects_when_not_embedding(self):
|
||||||
|
with self.assertRaisesRegex(ValueError, "requires --is-embedding"):
|
||||||
|
ServerArgs(**self._base_kwargs(is_embedding=False))
|
||||||
|
|
||||||
|
def test_rejects_when_chunked_prefill_size_not_minus_one(self):
|
||||||
|
with self.assertRaisesRegex(ValueError, "--chunked-prefill-size=-1"):
|
||||||
|
ServerArgs(**self._base_kwargs(chunked_prefill_size=8192))
|
||||||
|
|
||||||
|
def test_rejects_when_radix_cache_enabled(self):
|
||||||
|
with self.assertRaisesRegex(ValueError, "--disable-radix-cache"):
|
||||||
|
ServerArgs(**self._base_kwargs(disable_radix_cache=False))
|
||||||
|
|
||||||
|
def test_rejects_attn_cp_size_greater_than_one(self):
|
||||||
|
with self.assertRaisesRegex(ValueError, "--attn-cp-size"):
|
||||||
|
ServerArgs(**self._base_kwargs(attn_cp_size=2, tp_size=2))
|
||||||
|
|
||||||
|
def test_rejects_prefill_context_parallel(self):
|
||||||
|
with self.assertRaisesRegex(ValueError, "--enable-prefill-context-parallel"):
|
||||||
|
ServerArgs(**self._base_kwargs(enable_prefill_context_parallel=True))
|
||||||
|
|
||||||
|
def test_rejects_hisparse(self):
|
||||||
|
with self.assertRaisesRegex(ValueError, "--enable-hisparse"):
|
||||||
|
ServerArgs(**self._base_kwargs(enable_hisparse=True))
|
||||||
|
|
||||||
|
def test_rejects_fp4_kv_cache(self):
|
||||||
|
with self.assertRaisesRegex(ValueError, "fp4_e2m1"):
|
||||||
|
ServerArgs(**self._base_kwargs(kv_cache_dtype="fp4_e2m1"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user