Skip to content

Commit ec45adf

Browse files
authored
feat(mocker): add explicit max_model_len support to vLLM mocker (#11069)
Signed-off-by: Yongming Ding <yongmingd@nvidia.com>
1 parent 8b63aaa commit ec45adf

18 files changed

Lines changed: 648 additions & 71 deletions

File tree

components/src/dynamo/mocker/args.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@
1818
logger = logging.getLogger(__name__)
1919

2020

21+
def positive_int(value: str) -> int:
22+
try:
23+
parsed = int(value)
24+
except ValueError as error:
25+
raise argparse.ArgumentTypeError(str(error)) from error
26+
if parsed <= 0:
27+
raise argparse.ArgumentTypeError(f"must be positive, got {parsed}")
28+
return parsed
29+
30+
2131
def non_negative_int(value: str) -> int:
2232
try:
2333
parsed = int(value)
@@ -211,6 +221,13 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
211221
help="Token block size for KV cache blocks. When unset, the default "
212222
"depends on engine: vLLM 64, SGLang 1, TRTLLM 32.",
213223
)
224+
parser.add_argument(
225+
"--max-model-len",
226+
type=positive_int,
227+
default=None,
228+
help="Maximum vLLM sequence length, including prompt and generated tokens. "
229+
"When omitted, no model-length limit is enforced.",
230+
)
214231
parser.add_argument(
215232
"--max-num-seqs",
216233
type=int,

components/src/dynamo/mocker/config.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ def build_mocker_engine_args(args: argparse.Namespace) -> MockEngineArgs:
243243
aic_moe_ep_size = getattr(args, "aic_moe_ep_size", None)
244244
aic_attention_dp_size = getattr(args, "aic_attention_dp_size", None)
245245
engine_type = getattr(args, "engine_type", None) or "vllm"
246+
max_model_len = getattr(args, "max_model_len", None)
246247
num_gpu_blocks = _resolve_num_gpu_blocks(
247248
explicit_num_gpu_blocks=getattr(args, "num_gpu_blocks", None),
248249
engine_type=engine_type,
@@ -267,6 +268,7 @@ def build_mocker_engine_args(args: argparse.Namespace) -> MockEngineArgs:
267268
engine_type=engine_type,
268269
num_gpu_blocks=num_gpu_blocks,
269270
block_size=getattr(args, "block_size", 0) or 0,
271+
max_model_len=max_model_len,
270272
max_num_seqs=getattr(args, "max_num_seqs", _DEFAULT_MAX_NUM_SEQS),
271273
max_num_batched_tokens=getattr(
272274
args, "max_num_batched_tokens", _DEFAULT_MAX_NUM_BATCHED_TOKENS
@@ -349,8 +351,7 @@ def build_runtime_config(
349351
engine_args: MockEngineArgs,
350352
) -> tuple[int, ModelRuntimeConfig]:
351353
rc = ModelRuntimeConfig()
352-
# Mocker does not enforce a model context limit.
353-
rc.context_length = 0
354+
rc.context_length = engine_args.max_model_len or 0
354355
rc.total_kv_blocks = engine_args.num_gpu_blocks
355356
rc.max_num_seqs = engine_args.max_num_seqs
356357
if rc.max_num_seqs is None:

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

Lines changed: 68 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,73 @@ 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+
@pytest.mark.parametrize("value", ["0", "-1"])
412+
def test_mocker_cli_rejects_non_positive_max_model_len(value):
413+
with pytest.raises(SystemExit):
414+
parse_args(["--max-model-len", value])
415+
416+
417+
def test_build_mocker_engine_args_keeps_max_model_len_explicit_only():
418+
engine_args = CONFIG.build_mocker_engine_args(
419+
make_args(model_path="/models/mock", num_gpu_blocks=4096)
420+
)
421+
422+
assert engine_args.max_model_len is None
423+
424+
425+
def test_build_mocker_engine_args_preserves_explicit_max_model_len():
426+
engine_args = CONFIG.build_mocker_engine_args(
427+
make_args(
428+
model_path="/models/mock",
429+
max_model_len=32768,
430+
num_gpu_blocks=4096,
431+
)
432+
)
433+
434+
assert engine_args.max_model_len == 32768
435+
436+
437+
def test_replay_engine_args_keeps_max_model_len_explicit_only():
438+
import dynamo.replay.main as replay_main
439+
440+
engine_args = replay_main._load_engine_args(
441+
json.dumps(
442+
{
443+
"num_gpu_blocks": 4096,
444+
"aic_model_path": "/models/mock",
445+
}
446+
)
447+
)
448+
449+
assert engine_args.max_model_len is None
450+
451+
452+
def test_replay_engine_args_preserves_explicit_max_model_len():
453+
import dynamo.replay.main as replay_main
454+
455+
engine_args = replay_main._load_engine_args(
456+
json.dumps(
457+
{
458+
"num_gpu_blocks": 4096,
459+
"max_model_len": 32768,
460+
"aic_model_path": "/models/mock",
461+
}
462+
)
463+
)
464+
465+
assert engine_args.max_model_len == 32768
466+
467+
400468
def test_replay_engine_args_compute_kv_bytes_for_g3_before_validation(monkeypatch):
401469
import dynamo.replay.main as replay_main
402470

components/src/dynamo/replay/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ def _engine_caps(args: MockEngineArgs) -> EngineCapabilities:
295295
num_gpu=1,
296296
max_num_batched_tokens=args.max_num_batched_tokens,
297297
max_num_seqs=args.max_num_seqs,
298-
context_length=max_kv_tokens if max_kv_tokens > 0 else None,
298+
context_length=args.max_model_len,
299299
max_kv_tokens=max_kv_tokens if max_kv_tokens > 0 else None,
300300
speculative_nextn=args.aic_nextn,
301301
)

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
@@ -1965,6 +1965,7 @@ class MockEngineArgs:
19651965
enable_g4_storage: bool = False,
19661966
bandwidth_g2_to_g4_gbps: Optional[float] = None,
19671967
bandwidth_g4_to_g2_gbps: Optional[float] = None,
1968+
max_model_len: Optional[int] = None,
19681969
) -> None:
19691970
...
19701971

@@ -1983,6 +1984,9 @@ class MockEngineArgs:
19831984
@num_gpu_blocks.setter
19841985
def num_gpu_blocks(self, value: int) -> None: ...
19851986

1987+
@property
1988+
def max_model_len(self) -> Optional[int]: ...
1989+
19861990
@property
19871991
def max_num_seqs(self) -> Optional[int]: ...
19881992

lib/llm/src/mocker.rs

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,12 @@ impl AsyncEngine<SingleIn<PreprocessedRequest>, ManyOut<LLMEngineOutput>, Error>
791791
let max_output_tokens = planned_output_token_ids
792792
.as_ref()
793793
.map_or(requested_max_output_tokens, Vec::len);
794+
let effective_max_output_tokens =
795+
self.engine_args
796+
.max_model_len
797+
.map_or(max_output_tokens, |max_model_len| {
798+
max_output_tokens.min(max_model_len.saturating_sub(request.token_ids.len()))
799+
});
794800
let native_timing = self
795801
.native_metrics
796802
.request_timing(&request.model, dp_rank, is_prefill, request_start)
@@ -1013,14 +1019,14 @@ impl AsyncEngine<SingleIn<PreprocessedRequest>, ManyOut<LLMEngineOutput>, Error>
10131019
break;
10141020
};
10151021

1016-
// A terminally rejected request never ran (its footprint
1017-
// exceeds the KV pool): emit no token and do not complete the
1018-
// bootstrap roomsurface the rejection and end the stream
1019-
// before any token/prefill bookkeeping.
1022+
// A terminally rejected request never ran because it violated
1023+
// a worker admission limit. Emit no token and do not complete
1024+
// the bootstrap room; surface the rejection before any
1025+
// token/prefill bookkeeping.
10201026
if signal.rejected {
10211027
handoff_cancel.cancel();
10221028
let _ = stream_tx.send(LLMEngineOutput::error(
1023-
"request rejected: KV footprint exceeds pool capacity".to_string(),
1029+
"request rejected: request exceeds worker admission limits".to_string(),
10241030
));
10251031
break;
10261032
}
@@ -1043,7 +1049,7 @@ impl AsyncEngine<SingleIn<PreprocessedRequest>, ManyOut<LLMEngineOutput>, Error>
10431049
..Default::default()
10441050
};
10451051

1046-
if signal.completed && token_count < max_output_tokens {
1052+
if signal.completed && token_count < effective_max_output_tokens {
10471053
let _ = stream_tx.send(LLMEngineOutput::error("Completion signal received before max tokens reached".to_string()));
10481054
break;
10491055
}
@@ -1239,6 +1245,22 @@ mod tests {
12391245
.unwrap()
12401246
}
12411247

1248+
fn decode_request(prompt_tokens: usize, max_tokens: u32) -> PreprocessedRequest {
1249+
PreprocessedRequest::builder()
1250+
.model("mock".to_string())
1251+
.token_ids(vec![1; prompt_tokens])
1252+
.stop_conditions(StopConditions {
1253+
max_tokens: Some(max_tokens),
1254+
..Default::default()
1255+
})
1256+
.sampling_options(SamplingOptions::default())
1257+
.output_options(OutputOptions::default())
1258+
.eos_token_ids(vec![])
1259+
.annotations(vec![])
1260+
.build()
1261+
.unwrap()
1262+
}
1263+
12421264
#[tokio::test(start_paused = true)]
12431265
async fn no_bootstrap_prefill_delays_terminal_finish_once() {
12441266
let args = MockEngineArgs::builder()
@@ -1284,6 +1306,43 @@ mod tests {
12841306
assert!(stream.next().await.is_none());
12851307
}
12861308

1309+
#[tokio::test]
1310+
async fn context_capped_completion_maps_to_length() {
1311+
let args = MockEngineArgs::builder()
1312+
.max_model_len(Some(4))
1313+
.build()
1314+
.unwrap();
1315+
let engine = MockEngine::new(args);
1316+
let (request_tx, mut request_rx) = tokio::sync::mpsc::unbounded_channel();
1317+
engine.request_senders.set(vec![request_tx]).unwrap();
1318+
1319+
let mut stream = engine
1320+
.generate(SingleIn::new(decode_request(3, 4)))
1321+
.await
1322+
.unwrap();
1323+
let request = request_rx.recv().await.unwrap();
1324+
assert_eq!(request.max_output_tokens, 4);
1325+
let request_id = request.uuid.unwrap();
1326+
engine
1327+
.active_requests
1328+
.get(&request_id)
1329+
.unwrap()
1330+
.send(OutputSignal {
1331+
uuid: request_id,
1332+
token_id: Some(42),
1333+
completed: true,
1334+
rejected: false,
1335+
handoff_delay_ms: None,
1336+
})
1337+
.unwrap();
1338+
1339+
let token = stream.next().await.unwrap();
1340+
assert_eq!(token.token_ids.len(), 1);
1341+
assert!(token.finish_reason.is_none());
1342+
assert_eq!(stream.next().await.unwrap(), LLMEngineOutput::length());
1343+
assert!(stream.next().await.is_none());
1344+
}
1345+
12871346
#[test]
12881347
fn unbounded_sequence_limit_uses_finite_multi_handoff_capacity() {
12891348
let args = MockEngineArgs::builder()

0 commit comments

Comments
 (0)