mirror of
https://github.com/deepseek-ai/DeepInfer.git
synced 2026-10-30 07:47:10 +08:00
The config and sampling-parameter classes had zero test coverage despite
containing non-trivial validation logic that is exercised on every engine
startup. This PR adds two focused, CPU-only test files that cover all
_verify_args paths and several construction edge cases.
tests/test_sampling_params.py (47 tests)
Covers every validation rule in SamplingParams._verify_args:
- temperature: non-negative check, greedy-mode clamp (< SAMPLING_EPS
→ temperature=SAMPLING_EPS, top_p=1.0, top_k=-1).
- top_p: exclusive lower bound (> 0.0) and upper bound (<= 1.0).
- top_k: -1 (disabled) or [1, INT32_MAX]; zero and other negatives rejected.
- max_tokens: >= 1 and <= INT32_MAX; notably max_tokens=0 now raises
ValueError (the original code silently coerced 0 → 1 before validation).
- top_logprobs: [0, 1000].
- seq_seed: auto-generation, explicit value, uint64 masking, non-int raises.
- logprobs deprecation: logprobs=False emits no warning; logprobs=True
emits DeprecationWarning matching "logprobs.*deprecated".
- Optional fields: golden_token_ids, so_args, ignore_eos, first_n_token_no_eos,
eos_id, prompt_logprobs.
- Greedy mode: temperature=0 overrides top_k and top_p; non-zero preserves them.
tests/test_config.py (23 tests)
CacheConfig:
- Valid construction, boundary values (utilization=1.0), lookahead_tokens,
deferred-field initialisation (num_gpu_blocks etc. start as None).
- Rejects gpu_memory_utilization > 1.0 (existing check).
- Two tests marked xfail(strict=True) for the missing lower-bound guard
(0.0 and negative values currently pass silently). These will flip to
XPASS once PR fix/cache-config-validation is merged.
ParallelConfig:
- Auto-derivation of data_parallel_size when 0 is passed (min 1).
- world_size, data_parallel_rank, model_parallel_size, engram derivation.
- AssertionError when (cp * dp) is not divisible by ep.
- AssertionError when engine_rank is out of range.
SchedulerConfig:
- max_num_batched_tokens auto-default (>= max(max_model_len, 4096)).
- ValueError when explicit max_num_batched_tokens < max_model_len and
chunked_prefill=False; passes when chunked_prefill=True.
- ValueError for prefill_attempt_interval <= 0 and
prefill_any_attempt_interval <= 0.
- runtime_max_model_len >= max_model_len.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
259 lines
10 KiB
Python
259 lines
10 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
"""Unit tests for configuration classes: CacheConfig, ParallelConfig, SchedulerConfig.
|
|
|
|
These tests cover:
|
|
- CacheConfig: gpu_memory_utilization bounds (upper + lower), block_size
|
|
storage, deferred fields.
|
|
- ParallelConfig: _verify_args assertions, data_parallel_size auto-derive
|
|
when 0 is passed, world_size computation.
|
|
- SchedulerConfig: max_num_batched_tokens vs. max_model_len check,
|
|
prefill interval lower bounds.
|
|
|
|
No model weights, GPU, or CUDA device are required; all tests run on CPU.
|
|
|
|
NOTE: test_cache_config_rejects_zero_utilization and
|
|
test_cache_config_rejects_negative_utilization are currently marked xfail
|
|
because the lower-bound guard has not yet been added to CacheConfig._verify_args.
|
|
They will automatically flip to XPASS (and then be un-marked) once the companion
|
|
fix `fix/cache-config-validation` is merged.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from deepinfer.config import CacheConfig, ParallelConfig, SchedulerConfig
|
|
from deepinfer.engine.engine_type import EngineType
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CacheConfig
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestCacheConfig:
|
|
def test_valid_construction(self) -> None:
|
|
cfg = CacheConfig(block_size=16, gpu_memory_utilization=0.9)
|
|
assert cfg.block_size == 16
|
|
assert cfg.gpu_memory_utilization == pytest.approx(0.9)
|
|
|
|
def test_boundary_utilization_one(self) -> None:
|
|
"""Exactly 1.0 must be accepted (full GPU allocation)."""
|
|
cfg = CacheConfig(block_size=16, gpu_memory_utilization=1.0)
|
|
assert cfg.gpu_memory_utilization == pytest.approx(1.0)
|
|
|
|
def test_lookahead_tokens_default_zero(self) -> None:
|
|
cfg = CacheConfig(block_size=16, gpu_memory_utilization=0.5)
|
|
assert cfg.lookahead_tokens == 0
|
|
|
|
def test_lookahead_tokens_stored(self) -> None:
|
|
cfg = CacheConfig(block_size=16, gpu_memory_utilization=0.5, lookahead_tokens=4)
|
|
assert cfg.lookahead_tokens == 4
|
|
|
|
def test_deferred_fields_initially_none(self) -> None:
|
|
cfg = CacheConfig(block_size=16, gpu_memory_utilization=0.5)
|
|
assert cfg.num_gpu_blocks is None
|
|
assert cfg.num_state_seqs is None
|
|
assert cfg.bytes_of_state_per_seqs is None
|
|
assert cfg.bytes_per_block is None
|
|
|
|
def test_above_one_raises(self) -> None:
|
|
with pytest.raises(ValueError, match="GPU memory utilization"):
|
|
CacheConfig(block_size=16, gpu_memory_utilization=1.01)
|
|
|
|
def test_well_above_one_raises(self) -> None:
|
|
with pytest.raises(ValueError, match="GPU memory utilization"):
|
|
CacheConfig(block_size=16, gpu_memory_utilization=2.0)
|
|
|
|
@pytest.mark.xfail(
|
|
strict=True,
|
|
reason="Lower-bound guard missing in CacheConfig._verify_args; "
|
|
"fixed by PR fix/cache-config-validation.",
|
|
)
|
|
def test_zero_utilization_raises(self) -> None:
|
|
"""gpu_memory_utilization=0.0 should be rejected — reserving zero GPU
|
|
memory for KV blocks makes the engine non-functional and produces a
|
|
cryptic division-by-zero deep in the memory profiler."""
|
|
with pytest.raises(ValueError, match="GPU memory utilization"):
|
|
CacheConfig(block_size=16, gpu_memory_utilization=0.0)
|
|
|
|
@pytest.mark.xfail(
|
|
strict=True,
|
|
reason="Lower-bound guard missing in CacheConfig._verify_args; "
|
|
"fixed by PR fix/cache-config-validation.",
|
|
)
|
|
def test_negative_utilization_raises(self) -> None:
|
|
"""Negative gpu_memory_utilization is nonsensical and should be rejected
|
|
at construction time rather than surfacing as a cryptic failure later."""
|
|
with pytest.raises(ValueError, match="GPU memory utilization"):
|
|
CacheConfig(block_size=16, gpu_memory_utilization=-0.5)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ParallelConfig
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _parallel(
|
|
dp: int = 2,
|
|
cp: int = 1,
|
|
ep: int = 2,
|
|
engram: int = 0,
|
|
engine_rank: int = 0,
|
|
engine_group_rank: int = 0,
|
|
engine_type: EngineType = EngineType.NORMAL,
|
|
) -> ParallelConfig:
|
|
return ParallelConfig(
|
|
data_parallel_size=dp,
|
|
context_parallel_size=cp,
|
|
expert_parallel_size=ep,
|
|
engram_parallel_size=engram,
|
|
engine_rank=engine_rank,
|
|
engine_group_rank=engine_group_rank,
|
|
engine_type=engine_type,
|
|
)
|
|
|
|
|
|
class TestParallelConfig:
|
|
def test_valid_minimal(self) -> None:
|
|
cfg = _parallel(dp=2, cp=1, ep=2, engram=0)
|
|
assert cfg.data_parallel_size == 2
|
|
assert cfg.context_parallel_size == 1
|
|
assert cfg.expert_parallel_size == 2
|
|
|
|
def test_data_parallel_auto_derive_from_ep_and_cp(self) -> None:
|
|
"""When data_parallel_size=0, it should be derived as ep // cp."""
|
|
cfg = _parallel(dp=0, cp=2, ep=4, engram=0)
|
|
assert cfg.data_parallel_size == 2 # 4 // 2
|
|
|
|
def test_data_parallel_auto_derive_minimum_one(self) -> None:
|
|
"""Auto-derived data_parallel_size is always at least 1."""
|
|
# ep=1, cp=4 → integer division gives 0, but clamped to 1.
|
|
cfg = _parallel(dp=0, cp=4, ep=1, engram=0, engine_rank=0)
|
|
assert cfg.data_parallel_size == 1
|
|
|
|
def test_world_size_computed_correctly(self) -> None:
|
|
# world_size = max(cp * dp, engram_parallel_size)
|
|
cfg = _parallel(dp=4, cp=2, ep=4, engram=0)
|
|
# engram defaults to model_parallel_size * dp = 2 * 4 = 8
|
|
assert cfg.world_size == 8
|
|
|
|
def test_data_parallel_rank(self) -> None:
|
|
cfg = _parallel(dp=4, cp=1, ep=4, engram=0, engine_rank=3)
|
|
assert cfg.data_parallel_rank == 3 % 4
|
|
|
|
def test_model_parallel_size_equals_context_parallel(self) -> None:
|
|
cfg = _parallel(dp=2, cp=3, ep=6, engram=0)
|
|
assert cfg.model_parallel_size == 3
|
|
|
|
def test_engram_defaults_to_model_times_dp(self) -> None:
|
|
cfg = _parallel(dp=2, cp=2, ep=4, engram=0)
|
|
# engram_parallel_size = model_parallel_size * dp = 2 * 2 = 4
|
|
assert cfg.engram_parallel_size == 4
|
|
|
|
def test_invalid_ep_not_divisible_raises(self) -> None:
|
|
"""(cp * dp) must be divisible by ep."""
|
|
with pytest.raises(AssertionError):
|
|
_parallel(dp=2, cp=2, ep=3, engram=0) # 2*2=4 not divisible by 3
|
|
|
|
def test_engine_rank_out_of_range_raises(self) -> None:
|
|
"""engine_rank must be in [0, world_size // model_parallel_size)."""
|
|
with pytest.raises(AssertionError):
|
|
_parallel(dp=2, cp=1, ep=2, engram=0, engine_rank=999)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SchedulerConfig
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestSchedulerConfig:
|
|
def test_valid_basic(self) -> None:
|
|
cfg = SchedulerConfig(
|
|
max_num_batched_tokens=None,
|
|
max_num_seqs=64,
|
|
max_model_len=4096,
|
|
)
|
|
assert cfg.max_num_seqs == 64
|
|
assert cfg.max_model_len == 4096
|
|
|
|
def test_max_num_batched_tokens_defaults_to_max_model_len_or_4096(self) -> None:
|
|
cfg = SchedulerConfig(
|
|
max_num_batched_tokens=None,
|
|
max_num_seqs=4,
|
|
max_model_len=2048,
|
|
)
|
|
# _get_max_num_batched_tokens returns max(max_model_len, 4096, decode_tokens)
|
|
assert cfg.max_num_batched_tokens >= 4096
|
|
|
|
def test_explicit_max_num_batched_tokens(self) -> None:
|
|
cfg = SchedulerConfig(
|
|
max_num_batched_tokens=8192,
|
|
max_num_seqs=4,
|
|
max_model_len=4096,
|
|
)
|
|
assert cfg.max_num_batched_tokens == 8192
|
|
|
|
def test_too_small_batched_tokens_raises(self) -> None:
|
|
"""max_num_batched_tokens < max_model_len must be rejected when
|
|
chunked_prefill is disabled, because it would silently cap the
|
|
effective context window."""
|
|
with pytest.raises(ValueError, match="max_num_batched_tokens"):
|
|
SchedulerConfig(
|
|
max_num_batched_tokens=512,
|
|
max_num_seqs=4,
|
|
max_model_len=4096,
|
|
chunked_prefill=False,
|
|
)
|
|
|
|
def test_too_small_batched_tokens_ok_with_chunked_prefill(self) -> None:
|
|
"""With chunked_prefill=True the constraint is lifted."""
|
|
SchedulerConfig(
|
|
max_num_batched_tokens=512,
|
|
max_num_seqs=4,
|
|
max_model_len=4096,
|
|
chunked_prefill=True,
|
|
)
|
|
|
|
def test_prefill_attempt_interval_zero_raises(self) -> None:
|
|
with pytest.raises(ValueError, match="prefill_attempt_interval"):
|
|
SchedulerConfig(
|
|
max_num_batched_tokens=None,
|
|
max_num_seqs=4,
|
|
max_model_len=4096,
|
|
prefill_attempt_interval=0,
|
|
)
|
|
|
|
def test_prefill_attempt_interval_negative_raises(self) -> None:
|
|
with pytest.raises(ValueError, match="prefill_attempt_interval"):
|
|
SchedulerConfig(
|
|
max_num_batched_tokens=None,
|
|
max_num_seqs=4,
|
|
max_model_len=4096,
|
|
prefill_attempt_interval=-1,
|
|
)
|
|
|
|
def test_prefill_any_attempt_interval_zero_raises(self) -> None:
|
|
with pytest.raises(ValueError, match="prefill_any_attempt_interval"):
|
|
SchedulerConfig(
|
|
max_num_batched_tokens=None,
|
|
max_num_seqs=4,
|
|
max_model_len=4096,
|
|
prefill_any_attempt_interval=0,
|
|
)
|
|
|
|
def test_runtime_max_model_len_greater_than_max_model_len(self) -> None:
|
|
cfg = SchedulerConfig(
|
|
max_num_batched_tokens=None,
|
|
max_num_seqs=4,
|
|
max_model_len=4096,
|
|
)
|
|
assert cfg.runtime_max_model_len >= cfg.max_model_len
|
|
|
|
def test_stop_preempted_request_stored(self) -> None:
|
|
cfg = SchedulerConfig(
|
|
max_num_batched_tokens=None,
|
|
max_num_seqs=4,
|
|
max_model_len=4096,
|
|
stop_preempted_request=True,
|
|
)
|
|
assert cfg.stop_preempted_request is True
|