[Bugfix][Benchmark] Check readiness before tokenizer init in rust vllm-bench - #51863
Conversation
Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Tyler Michael Smith <tlrmchlsmth@gmail.com>
esmeetu
left a comment
There was a problem hiding this comment.
Ran an AI-assisted review (Claude Code) of this PR; findings below as inline comments, most severe first.
TL;DR: The PR moves the ready check earlier but leaves two real gaps: (1) the unretried /v1/models fetch still runs before the readiness wait in both orchestrators, so the startup race this PR targets still reproduces whenever --model is omitted; and (2) replacing the dataset-derived probe with a synthetic one ("test", output_len: 1) makes the check fail forever against healthy servers with --extra-body min_tokens or --skip-tokenizer-init, while also passing trivially when the real request shape (multimodal / token-id prompts) would be rejected — converting an upfront abort into a full run that saves completed=0 garbage results. Secondary findings cover the now-stale readiness probe, serialized boot-wait vs dataset generation, rerank payload knowledge duplicated in ready_checker, and missing test coverage for the new helper.
| (id, Some(name)) | ||
| }; | ||
|
|
||
| run_initial_ready_check(config, &client, &model_id, &model_name).await?; |
There was a problem hiding this comment.
[correctness — confirmed] Model auto-resolution still hits the server before this ready check, so the readiness timeout never applies when --model is omitted.
When --model is omitted while the server is still starting (the exact scenario this PR targets), get_first_model_from_server (line 352) sends a single unretried GET /v1/models (request.send().await?) before run_initial_ready_check is reached, so the benchmark exits immediately with a connection-refused error instead of waiting --ready-check-timeout-sec. Same gap in run_multi_turn_benchmark (multi_turn.rs:77 via get_first_model).
Consider moving the ready check above model resolution (probing a health-ish endpoint first), or retrying the /v1/models fetch under the same timeout.
| prompt: Arc::from("test"), | ||
| api_url: config.api_url.clone(), | ||
| prompt_len: 1, | ||
| output_len: 1, |
There was a problem hiding this comment.
[correctness — confirmed] The synthetic probe hardcodes output_len: 1 (rendered as max_tokens: 1) while still merging the user's extra_body, so --extra-body '{"min_tokens": 32}' (a common way to force output length) makes every probe fail with 400 min_tokens must be less than or equal to max_tokens against a perfectly healthy server. wait_for_endpoint treats each 400 as "not ready" and retries every 5s, so the run stalls for the full --ready-check-timeout-sec and then aborts with EndpointTimeout.
The removed code used the first dataset request's expected_output_len as max_tokens (normally >= min_tokens), so this configuration previously worked.
| ]) | ||
| }); | ||
| let test_input = RequestFuncInput { | ||
| prompt: Arc::from("test"), |
There was a problem hiding this comment.
[correctness — confirmed] The probe sends a text prompt ("test") without prompt_token_ids, which breaks the ready check against servers running with --skip-tokenizer-init: such servers reject string prompts on /v1/completions with a 400 on every attempt, so the check retries until --ready-check-timeout-sec expires and aborts with EndpointTimeout.
The removed single-turn ready check cloned first.prompt_token_ids into the probe, so this setup previously worked (e.g. random dataset with token-id prompts, which sets prompt="" and relies on prompt_token_ids).
| Arc::<str>::from("test document"), | ||
| ]) | ||
| }); | ||
| let test_input = RequestFuncInput { |
There was a problem hiding this comment.
[correctness — confirmed] The ready check no longer exercises the real benchmark request shape — multi_modal_content, chat_messages_json, and prompt_token_ids from the first dataset request are all dropped. Shape incompatibilities that previously aborted upfront now produce a full benchmark run with 0 completed requests.
Example: a multimodal dataset (VisionArena via openai-chat) against a model that rejects image input. Previously every probe got the server's 400 and the run aborted early with the underlying error in BenchError::EndpointTimeout. Now the text-only probe succeeds immediately, all N multimodal requests fail during the run, and — since metrics/calculator.rs has no all-requests-failed guard — the user waits out the whole run and gets a saved JSON result with completed=0 and zero/NaN metrics. Same applies to token-id probes if the server rejects token-array inputs.
Deriving the probe from the first dataset request (as before) while keeping the new early placement would need the dataset ready first — which conflicts with the goal here — but the probe could at least carry the request-shape-relevant fields once the dataset exists, e.g. by keeping a second shape-check before the timed run (see the staleness comment on benchmark.rs).
| }; | ||
|
|
||
| run_initial_ready_check(config, &client, &model_id, &model_name).await?; | ||
|
|
There was a problem hiding this comment.
[correctness — plausible] With the probe moved before tokenizer download and dataset generation (which can take minutes for HF tokenizer fetch / ShareGPT / 100k-prompt random datasets), readiness is stale by the time the timed run starts:
- reqwest's default
pool_idle_timeout(90s, not overridden by this client builder) evicts the probe's connection in the interim, so for datasets that skip the/tokenizesample verification and with defaultnum_warmups=0, the first benchmark request absorbs a fresh TCP/TLS handshake — inflating TTFT max/percentiles on small runs. - A server that crashes or restarts during dataset preparation — previously caught by the just-before-run probe — now surfaces as failed requests mid-benchmark instead of an upfront abort.
Same applies to multi_turn.rs:86.
| (id, Some(name)) | ||
| }; | ||
|
|
||
| run_initial_ready_check(config, &client, &model_id, &model_name).await?; |
There was a problem hiding this comment.
[efficiency] This serializes the server-boot wait before tokenizer load and dataset generation instead of overlapping them. On main, dataset generation (minutes for large random/HF datasets) ran while the server booted, and the ready check at the end consumed only the remaining boot time; now wall-clock startup is boot_time + generation_time instead of roughly max of the two.
A cheaper shape that keeps the fix: spawn the ready check as a concurrent task here and await it before the first server-dependent step (server-side tokenizer fallback, /tokenize verification, warmup).
| return Ok(()); | ||
| } | ||
|
|
||
| let prompt_list = (config.backend == BackendKind::VllmRerank).then(|| { |
There was a problem hiding this comment.
[simplification] This rerank special case duplicates the pooling backend's payload contract (prompt_list = [query, doc1, ...], enforced in backends/pooling.rs::build_payload) at the ready-check layer. If another structured-input backend is added or rerank's minimum-input requirement changes, this probe silently becomes invalid and every run against a healthy server spins until the full --ready-check-timeout-sec, then dies with EndpointTimeout.
A probe-input constructor owned by the backend layer (e.g. a method on BackendKind/Backend in backends/mod.rs next to the payload builders) would keep the knowledge in one place.
| use crate::error::{BenchError, Result}; | ||
|
|
||
| /// Wait for the configured backend before tokenizer and dataset initialization. | ||
| pub async fn run_initial_ready_check( |
There was a problem hiding this comment.
[test-coverage] run_initial_ready_check ships with no test coverage for its skip conditions or the rerank special case. The guard (return Ok on dry_run or ready_check_timeout_sec == 0) and the VllmRerank two-element prompt_list construction are pure, unit-testable logic. A future edit that inverts the dry_run guard would make --dry-run start issuing network requests (breaking the "dry runs stay offline" invariant documented in benchmark.rs) with nothing failing in CI.
| use crate::config::BenchConfig; | ||
| use crate::error::{BenchError, Result}; | ||
|
|
||
| /// Wait for the configured backend before tokenizer and dataset initialization. |
There was a problem hiding this comment.
[conventions] The move dropped the original inline comments — // Ready check (benchmark.rs) and // Ready check with a simple single request (multi_turn.rs) — contrary to rust/AGENTS.md: "When refactoring or reconstructing code, always preserve the original comments and documentation VERBATIM, if applicable." The note that the multi-turn probe is intentionally "a simple single request" is lost.
Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Tyler Michael Smith <tlrmchlsmth@gmail.com>
|
/ci run |
|
✅ Triggered Buildkite CI #84519 for commit |
…m-bench (vllm-project#51863) Signed-off-by: Tyler Michael Smith <tlrmchlsmth@gmail.com> Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Wenhua Cheng <wenhua.cheng@intel.com>
…m-bench (vllm-project#51863) Signed-off-by: Tyler Michael Smith <tlrmchlsmth@gmail.com> Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Zhu, Zufang <zufang.zhu@intel.com>
…m-bench (vllm-project#51863) Signed-off-by: Tyler Michael Smith <tlrmchlsmth@gmail.com> Co-authored-by: OpenAI Codex <codex@openai.com>
Summary
Apply
--ready-check-timeout-secto the startup-dependent requests that can run before the benchmark readiness probe:/v1/modelsdiscovery when--modelis omitted/tokenizeprobe when tokenizer loading falls back to the serverThis keeps the readiness request representative of the benchmark payload, including token IDs, multimodal content, and pooling inputs. Local tokenizer loading and dataset generation can still overlap server startup.
Duplicate work
Searched open PRs for
ready-check-timeout, tokenizer readiness, and server-side tokenization readiness. No duplicate fix was found.Testing
cargo test -p vllm-bench— 150 passed, 10 ignoredcargo clippy -p vllm-bench --all-targets -- -D warningscargo fmt --all -- --checkpre-commit runAI assistance
AI assistance was used to implement and test this change. I reviewed every changed line and can explain and maintain the implementation.