DeepInfer/tests/test_config.py
akash6murali 2afdb3d2d8 test: add unit tests for SamplingParams, CacheConfig, ParallelConfig, SchedulerConfig
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>
2026-10-20 14:28:11 -04:00

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