Skip to content

Commit 116822b

Browse files
committed
fix(mocker): reject prompts above max model length
Signed-off-by: Yongming Ding <yongmingd@nvidia.com>
1 parent 4cba833 commit 116822b

11 files changed

Lines changed: 324 additions & 26 deletions

File tree

components/src/dynamo/mocker/args.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,13 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
211211
help="Token block size for KV cache blocks. When unset, the default "
212212
"depends on engine: vLLM 64, SGLang 1, TRTLLM 32.",
213213
)
214+
parser.add_argument(
215+
"--max-model-len",
216+
type=int,
217+
default=None,
218+
help="Reject vLLM prompts longer than this many tokens. When omitted, "
219+
"derive the limit from --model-path when available; otherwise disabled.",
220+
)
214221
parser.add_argument(
215222
"--max-num-seqs",
216223
type=int,

components/src/dynamo/mocker/config.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from dynamo.common.utils.topology import apply_topology_config
1515
from dynamo.llm import ModelRuntimeConfig
1616
from dynamo.mocker import MockEngineArgs, ReasoningConfig, SglangArgs, TrtllmArgs
17+
from dynamo.mocker.utils.kv_cache import get_model_max_context_length
1718

1819
_DEFAULT_NUM_GPU_BLOCKS = 16384
1920
_DEFAULT_MAX_NUM_SEQS = 256
@@ -183,14 +184,20 @@ def _resolve_raw_engine_args(
183184
*,
184185
fallback_model_path: str | None = None,
185186
) -> dict:
187+
engine_type = raw.get("engine_type") or "vllm"
188+
model_path = raw.get("aic_model_path") or fallback_model_path
189+
if raw.get("max_model_len") is None and engine_type == "vllm" and model_path:
190+
max_model_len = get_model_max_context_length(model_path)
191+
if max_model_len is not None:
192+
raw["max_model_len"] = max_model_len
193+
186194
if raw.get("num_gpu_blocks") is not None:
187195
return raw
188196

189197
aic_backend = raw.get("aic_backend")
190198
if aic_backend is None:
191199
return raw
192200

193-
engine_type = raw.get("engine_type") or "vllm"
194201
sglang = raw.get("sglang")
195202
sglang_page_size = sglang.get("page_size") if isinstance(sglang, dict) else None
196203
raw["num_gpu_blocks"] = _estimate_aic_num_gpu_blocks(
@@ -201,7 +208,7 @@ def _resolve_raw_engine_args(
201208
aic_system=raw.get("aic_system"),
202209
aic_backend_version=raw.get("aic_backend_version"),
203210
aic_tp_size=raw.get("aic_tp_size"),
204-
aic_model_path=raw.get("aic_model_path") or fallback_model_path,
211+
aic_model_path=model_path,
205212
aic_moe_tp_size=raw.get("aic_moe_tp_size"),
206213
aic_moe_ep_size=raw.get("aic_moe_ep_size"),
207214
aic_attention_dp_size=raw.get("aic_attention_dp_size"),
@@ -243,6 +250,10 @@ def build_mocker_engine_args(args: argparse.Namespace) -> MockEngineArgs:
243250
aic_moe_ep_size = getattr(args, "aic_moe_ep_size", None)
244251
aic_attention_dp_size = getattr(args, "aic_attention_dp_size", None)
245252
engine_type = getattr(args, "engine_type", None) or "vllm"
253+
max_model_len = getattr(args, "max_model_len", None)
254+
model_path = getattr(args, "model_path", None)
255+
if max_model_len is None and engine_type == "vllm" and model_path:
256+
max_model_len = get_model_max_context_length(model_path)
246257
num_gpu_blocks = _resolve_num_gpu_blocks(
247258
explicit_num_gpu_blocks=getattr(args, "num_gpu_blocks", None),
248259
engine_type=engine_type,
@@ -267,6 +278,7 @@ def build_mocker_engine_args(args: argparse.Namespace) -> MockEngineArgs:
267278
engine_type=engine_type,
268279
num_gpu_blocks=num_gpu_blocks,
269280
block_size=getattr(args, "block_size", 0) or 0,
281+
max_model_len=max_model_len,
270282
max_num_seqs=getattr(args, "max_num_seqs", _DEFAULT_MAX_NUM_SEQS),
271283
max_num_batched_tokens=getattr(
272284
args, "max_num_batched_tokens", _DEFAULT_MAX_NUM_BATCHED_TOKENS
@@ -349,8 +361,7 @@ def build_runtime_config(
349361
engine_args: MockEngineArgs,
350362
) -> tuple[int, ModelRuntimeConfig]:
351363
rc = ModelRuntimeConfig()
352-
# Mocker does not enforce a model context limit.
353-
rc.context_length = 0
364+
rc.context_length = engine_args.max_model_len or 0
354365
rc.total_kv_blocks = engine_args.num_gpu_blocks
355366
rc.max_num_seqs = engine_args.max_num_seqs
356367
if rc.max_num_seqs is None:

components/src/dynamo/mocker/tests/unit/test_config.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def make_args(**overrides):
3535
"engine_type": "vllm",
3636
"num_gpu_blocks": None,
3737
"block_size": None,
38+
"max_model_len": None,
3839
"max_num_seqs": 256,
3940
"max_num_batched_tokens": 8192,
4041
"enable_prefix_caching": True,
@@ -397,6 +398,127 @@ def test_mocker_cli_accepts_mtp_configuration():
397398
assert args.aic_mtp_seed == 99
398399

399400

401+
def test_mocker_cli_accepts_max_model_len():
402+
args = parse_args(["--max-model-len", "32768"])
403+
404+
engine_args = CONFIG.build_mocker_engine_args(args)
405+
_, runtime_config = CONFIG.build_runtime_config(engine_args)
406+
407+
assert engine_args.max_model_len == 32768
408+
assert runtime_config.context_length == 32768
409+
410+
411+
def test_model_max_context_length_uses_shared_model_info(monkeypatch):
412+
import dynamo.mocker.utils.kv_cache as kv_cache
413+
414+
monkeypatch.setattr(
415+
kv_cache,
416+
"get_model_info",
417+
lambda _model_path: SimpleNamespace(max_context_length=204800),
418+
)
419+
420+
assert kv_cache.get_model_max_context_length("/models/mock") == 204800
421+
422+
423+
def test_model_max_context_length_returns_none_on_failure(monkeypatch):
424+
import dynamo.mocker.utils.kv_cache as kv_cache
425+
426+
def fail(_model_path):
427+
raise ValueError("bad model config")
428+
429+
monkeypatch.setattr(kv_cache, "get_model_info", fail)
430+
431+
assert kv_cache.get_model_max_context_length("/models/mock") is None
432+
433+
434+
def test_build_mocker_engine_args_derives_max_model_len(monkeypatch):
435+
monkeypatch.setattr(
436+
CONFIG, "get_model_max_context_length", lambda _model_path: 204800
437+
)
438+
439+
engine_args = CONFIG.build_mocker_engine_args(
440+
make_args(model_path="/models/mock", num_gpu_blocks=4096)
441+
)
442+
443+
assert engine_args.max_model_len == 204800
444+
445+
446+
def test_build_mocker_engine_args_preserves_explicit_max_model_len(monkeypatch):
447+
def fail(_model_path):
448+
raise AssertionError("model context lookup should not be called")
449+
450+
monkeypatch.setattr(CONFIG, "get_model_max_context_length", fail)
451+
452+
engine_args = CONFIG.build_mocker_engine_args(
453+
make_args(
454+
model_path="/models/mock",
455+
max_model_len=32768,
456+
num_gpu_blocks=4096,
457+
)
458+
)
459+
460+
assert engine_args.max_model_len == 32768
461+
462+
463+
def test_replay_engine_args_derives_max_model_len(monkeypatch):
464+
import dynamo.replay.main as replay_main
465+
466+
monkeypatch.setattr(
467+
replay_main, "get_model_max_context_length", lambda _model_path: 204800
468+
)
469+
470+
engine_args = replay_main._load_engine_args(
471+
json.dumps(
472+
{
473+
"num_gpu_blocks": 4096,
474+
"aic_model_path": "/models/mock",
475+
}
476+
)
477+
)
478+
479+
assert engine_args.max_model_len == 204800
480+
481+
482+
def test_replay_engine_args_keeps_max_model_len_unset_on_lookup_failure(monkeypatch):
483+
import dynamo.replay.main as replay_main
484+
485+
monkeypatch.setattr(
486+
replay_main, "get_model_max_context_length", lambda _model_path: None
487+
)
488+
489+
engine_args = replay_main._load_engine_args(
490+
json.dumps(
491+
{
492+
"num_gpu_blocks": 4096,
493+
"aic_model_path": "/models/mock",
494+
}
495+
)
496+
)
497+
498+
assert engine_args.max_model_len is None
499+
500+
501+
def test_replay_engine_args_preserves_explicit_max_model_len(monkeypatch):
502+
import dynamo.replay.main as replay_main
503+
504+
def fail(_model_path):
505+
raise AssertionError("model context lookup should not be called")
506+
507+
monkeypatch.setattr(replay_main, "get_model_max_context_length", fail)
508+
509+
engine_args = replay_main._load_engine_args(
510+
json.dumps(
511+
{
512+
"num_gpu_blocks": 4096,
513+
"max_model_len": 32768,
514+
"aic_model_path": "/models/mock",
515+
}
516+
)
517+
)
518+
519+
assert engine_args.max_model_len == 32768
520+
521+
400522
def test_replay_engine_args_compute_kv_bytes_for_g3_before_validation(monkeypatch):
401523
import dynamo.replay.main as replay_main
402524

components/src/dynamo/mocker/utils/kv_cache.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@
3535
DEFAULT_KV_TRANSFER_BANDWIDTH_GBPS = 64.0
3636

3737

38+
def get_model_max_context_length(model_path: str) -> int | None:
39+
"""Best-effort model context length from the shared model metadata loader."""
40+
try:
41+
max_context_length = get_model_info(model_path).max_context_length
42+
if max_context_length is None or max_context_length <= 0:
43+
return None
44+
return max_context_length
45+
except Exception as e:
46+
logger.warning("Could not determine max context length from model config: %s", e)
47+
return None
48+
49+
3850
def _normalize_dtype_str(dtype) -> str:
3951
"""Normalize a dtype to a plain string like 'float16'.
4052

components/src/dynamo/replay/main.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@
2929
)
3030
from dynamo.llm import AicPerfConfig, KvRouterConfig
3131
from dynamo.mocker import MockEngineArgs
32-
from dynamo.mocker.utils.kv_cache import compute_kv_bytes_per_token
32+
from dynamo.mocker.utils.kv_cache import (
33+
compute_kv_bytes_per_token,
34+
get_model_max_context_length,
35+
)
3336
from dynamo.replay import run_synthetic_trace_replay, run_trace_replay
3437
from dynamo.replay.reporting import format_report_table, write_report_json
3538

@@ -197,6 +200,21 @@ def _resolve_kv_bytes_per_token(raw: dict) -> None:
197200
raw["kv_bytes_per_token"] = kv_bytes_per_token
198201

199202

203+
def _resolve_max_model_len(raw: dict) -> None:
204+
if raw.get("max_model_len") is not None:
205+
return
206+
if (raw.get("engine_type") or "vllm") != "vllm":
207+
return
208+
209+
model_path = raw.get("aic_model_path")
210+
if not model_path:
211+
return
212+
213+
max_model_len = get_model_max_context_length(model_path)
214+
if max_model_len is not None:
215+
raw["max_model_len"] = max_model_len
216+
217+
200218
def _load_engine_args(raw_args: str | None):
201219
if raw_args is None:
202220
return None
@@ -229,6 +247,7 @@ def _load_engine_args(raw_args: str | None):
229247
raw["planner_profile_data"] = str(profile_data_result.npz_path)
230248
else:
231249
del raw["planner_profile_data"]
250+
_resolve_max_model_len(raw)
232251
_resolve_aic_num_gpu_blocks(raw)
233252
_resolve_kv_bytes_per_token(raw)
234253
return MockEngineArgs.from_json(json.dumps(raw))
@@ -295,7 +314,8 @@ def _engine_caps(args: MockEngineArgs) -> EngineCapabilities:
295314
num_gpu=1,
296315
max_num_batched_tokens=args.max_num_batched_tokens,
297316
max_num_seqs=args.max_num_seqs,
298-
context_length=max_kv_tokens if max_kv_tokens > 0 else None,
317+
context_length=args.max_model_len
318+
or (max_kv_tokens if max_kv_tokens > 0 else None),
299319
max_kv_tokens=max_kv_tokens if max_kv_tokens > 0 else None,
300320
speculative_nextn=args.aic_nextn,
301321
)

lib/bindings/python/rust/llm/replay.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ impl MockEngineArgs {
171171
#[pymethods]
172172
impl MockEngineArgs {
173173
#[new]
174-
#[pyo3(signature = (engine_type="vllm", num_gpu_blocks=None, block_size=0, max_num_seqs=Some(256), max_num_batched_tokens=Some(8192), enable_prefix_caching=true, enable_chunked_prefill=true, speedup_ratio=1.0, decode_speedup_ratio=1.0, dp_size=1, startup_time=None, worker_type="aggregated", planner_profile_data=None, aic_backend=None, aic_system=None, aic_backend_version=None, aic_tp_size=None, aic_model_path=None, aic_moe_tp_size=None, aic_moe_ep_size=None, aic_attention_dp_size=None, aic_nextn=None, aic_nextn_accept_rates=None, aic_mtp_seed=42, aic_gemm_dtype=None, aic_moe_dtype=None, aic_fmha_dtype=None, aic_kv_cache_dtype=None, aic_comm_dtype=None, gpu_memory_utilization=None, mem_fraction_static=None, free_gpu_memory_fraction=None, enable_local_indexer=false, bootstrap_port=None, handoff_session_timeout_ms=300000, kv_bytes_per_token=None, kv_transfer_bandwidth=None, kv_transfer_timing_mode="full_prompt", reasoning=None, response_replay_trace_path=None, zmq_kv_events_port=None, zmq_replay_port=None, preemption_mode="lifo", router_queue_policy=None, sglang=None, trtllm=None, num_g2_blocks=None, num_g3_blocks=None, offload_batch_size=None, bandwidth_g1_to_g2_gbps=None, bandwidth_g2_to_g1_gbps=None, bandwidth_g2_to_g3_gbps=None, bandwidth_g3_to_g2_gbps=None, enable_g4_storage=false, bandwidth_g2_to_g4_gbps=None, bandwidth_g4_to_g2_gbps=None))]
174+
#[pyo3(signature = (engine_type="vllm", num_gpu_blocks=None, block_size=0, max_num_seqs=Some(256), max_num_batched_tokens=Some(8192), enable_prefix_caching=true, enable_chunked_prefill=true, speedup_ratio=1.0, decode_speedup_ratio=1.0, dp_size=1, startup_time=None, worker_type="aggregated", planner_profile_data=None, aic_backend=None, aic_system=None, aic_backend_version=None, aic_tp_size=None, aic_model_path=None, aic_moe_tp_size=None, aic_moe_ep_size=None, aic_attention_dp_size=None, aic_nextn=None, aic_nextn_accept_rates=None, aic_mtp_seed=42, aic_gemm_dtype=None, aic_moe_dtype=None, aic_fmha_dtype=None, aic_kv_cache_dtype=None, aic_comm_dtype=None, gpu_memory_utilization=None, mem_fraction_static=None, free_gpu_memory_fraction=None, enable_local_indexer=false, bootstrap_port=None, handoff_session_timeout_ms=300000, kv_bytes_per_token=None, kv_transfer_bandwidth=None, kv_transfer_timing_mode="full_prompt", reasoning=None, response_replay_trace_path=None, zmq_kv_events_port=None, zmq_replay_port=None, preemption_mode="lifo", router_queue_policy=None, sglang=None, trtllm=None, num_g2_blocks=None, num_g3_blocks=None, offload_batch_size=None, bandwidth_g1_to_g2_gbps=None, bandwidth_g2_to_g1_gbps=None, bandwidth_g2_to_g3_gbps=None, bandwidth_g3_to_g2_gbps=None, enable_g4_storage=false, bandwidth_g2_to_g4_gbps=None, bandwidth_g4_to_g2_gbps=None, max_model_len=None))]
175175
#[allow(clippy::too_many_arguments)]
176176
fn new(
177177
engine_type: &str,
@@ -230,6 +230,7 @@ impl MockEngineArgs {
230230
enable_g4_storage: bool,
231231
bandwidth_g2_to_g4_gbps: Option<f64>,
232232
bandwidth_g4_to_g2_gbps: Option<f64>,
233+
max_model_len: Option<usize>,
233234
) -> PyResult<Self> {
234235
let engine_type = parse_mocker_engine_type(engine_type)?;
235236
let worker_type = parse_worker_type(worker_type)?;
@@ -248,6 +249,7 @@ impl MockEngineArgs {
248249
let mut builder = RsMockEngineArgs::builder()
249250
.engine_type(engine_type)
250251
.block_size(block_size)
252+
.max_model_len(max_model_len)
251253
.max_num_seqs(max_num_seqs)
252254
.max_num_batched_tokens(max_num_batched_tokens)
253255
.enable_prefix_caching(enable_prefix_caching)
@@ -365,6 +367,11 @@ impl MockEngineArgs {
365367
self.inner.num_gpu_blocks
366368
}
367369

370+
#[getter]
371+
fn max_model_len(&self) -> Option<usize> {
372+
self.inner.max_model_len
373+
}
374+
368375
#[getter]
369376
fn max_num_seqs(&self) -> Option<usize> {
370377
self.inner.max_num_seqs

lib/bindings/python/src/dynamo/_core.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1958,6 +1958,7 @@ class MockEngineArgs:
19581958
enable_g4_storage: bool = False,
19591959
bandwidth_g2_to_g4_gbps: Optional[float] = None,
19601960
bandwidth_g4_to_g2_gbps: Optional[float] = None,
1961+
max_model_len: Optional[int] = None,
19611962
) -> None:
19621963
...
19631964

@@ -1976,6 +1977,9 @@ class MockEngineArgs:
19761977
@num_gpu_blocks.setter
19771978
def num_gpu_blocks(self, value: int) -> None: ...
19781979

1980+
@property
1981+
def max_model_len(self) -> Optional[int]: ...
1982+
19791983
@property
19801984
def max_num_seqs(self) -> Optional[int]: ...
19811985

0 commit comments

Comments
 (0)