Repository navigation
[Model] Batch MiniCPM-o 4.5 cold reference CFM setup - #9221
natureofnature wants to merge 4 commits into
Conversation
Signed-off-by: natureofnature <wzliu@connect.hku.hk>
Signed-off-by: natureofnature <wzliu@connect.hku.hk>
Signed-off-by: natureofnature <wzliu@connect.hku.hk>
|
This PR appears to belong to: docs/design/module/ar_runtime.md, docs/design/module/model_integration.md. Module owners: @tzhouam @fake0fan @Gaohan123 Routing: @tzhouam via module of the changed files, CODEOWNERS; @fake0fan via module of the changed files; @Gaohan123 via module of the changed files @natureofnature, please review your own changes and leave a short self-review comment describing what you checked. PRs without author self-review may not be assigned a reviewer. Please take a look when you have a chance. If you would like an automated review, mention @vllm-omni-review-bot in a comment. |
amy-why-3459
left a comment
There was a problem hiding this comment.
Thanks for splitting this out from #9159 and for the clear tradeoff write-up. I went through setup_references / _create_initial_states in batched_token2wav.py, the _prepare_decode_batch change, and the Whole-Euler replay/_entry paths they now hit. Cache ordering, hit retention and the per-row isolation look right to me: the initial solve has no att_cache/prompt_lengths, so replay returns a stacked out_att and _split_flow_cache copies each row into its own exact-size buffer. That keeps the cached prompt state un-growable in place, which setup_batch relies on.
Two things on the graph side that might also bear on the C2 / C32 TTFA numbers:
-
The batched cold solve runs at offset 0 with
graph_batchfrom_plan_groups(B)(2/4/8) and a query width taken from the reference's mel length.precapture_whole_euleronly covers decode offsetsrange(prompt_len, steady + 1), so these setup graphs are captured lazily on the serving path. They also draw from the samemax_graphsbudget (default 32) as the decode graphs. Each distinct (group size, aligned prompt width) is a new entry, so with varied reference lengths this can take budget from decode shapes, or find it already full. Could you check the capture/hit stats from the sweep? It might be worth either precapturing the setup widths for the precaptured prompt or keeping them out of the decode budget. -
When the batched replay is refused (budget full is memoized in
_unsupported, so it stays refused for that key), the fallback discards the B per-reference_encode_initial_promptoutputs and re-runs the encoder and speaker projection per reference (inline below). Once a key is unsupported, every later cold group of that shape pays the encoder twice. Checking eligibility first, or passing the already-encoded rows into the scalar path, would avoid that.
Minor: the transient peak during a cold group is now the stacked out_att for 2B CFG rows plus the per-row copies made in _split_flow_cache (and the extra per-row clone() of the other three caches on top of the batch clone that _split_flow_cache already does). That fits the +2.6 GiB observed at C16. It may be worth a line in the design doc next to the microbatch bound, since it scales with micro_batch_size.
| # A full graph budget or failed capture can refuse this batch even | ||
| # while M=1 graphs remain usable. Preserve each reference's solver. | ||
| assert isinstance(features, list) | ||
| return [self._create_initial_states(feature, 1)[0] for feature in features] |
There was a problem hiding this comment.
On refusal, the per-reference encoder/speaker outputs computed above for the whole group are dropped and recomputed by each _create_initial_states(feature, 1). Since an over-budget key is added to _unsupported and stays refused, this double encode repeats for every later cold group of the same shape. Could the scalar fallback reuse encoded[i] (or could eligibility be checked before encoding)?
There was a problem hiding this comment.
Fixed in 532e9ca. The scalar retry now receives each reference's existing encoded_prompt, so it reuses the M=1 encoder and speaker-projection outputs rather than recomputing them. This also covers later groups hitting a memoized refusal.
The encoding-count regressions fail on the parent and pass with this change, covering replay refusal, wrapper disablement, and a full graph budget. Real H800 full-budget checks at B=3 and B=8 also pass cache/PCM comparison against independent scalar streams at atol=1e-5, rtol=1.3e-6.
| pending = [features[row] for row in indices] | ||
| # Capture can disable Whole-Euler during an earlier solve. | ||
| if len(pending) > 1 and self._whole_euler_ragged_active(): | ||
| created = self._create_initial_states(pending, len(pending)) |
There was a problem hiding this comment.
This is the first place offset-0 Whole-Euler graphs with graph_batch > 1 get captured, lazily during serving, since precapture_whole_euler only warms decode offsets. Each (group size, prompt query width) uses a slot of the shared max_graphs budget. Do the sweep logs show how many setup captures happened, and whether decode shapes were pushed out to eager as a result?
There was a problem hiding this comment.
Checked the original sweep logs at 41cb8686: both baseline visits reached 25/32 graphs and both candidate visits reached 28/32. In candidate B1, an M=2 offset-zero setup graph was captured during the second measured C2 run; host warmup plus capture took 1.051 seconds. No graph-budget saturation was observed. Those log snapshots are not whole-run fallback totals, so they do not establish a zero-fallback claim for the entire sweep.
Addressed in 532e9ca: eligible non-slot precapture warms decode/tail shapes first, followed by setup shapes in ascending batch order, within the unchanged shared budget. The real-weight H800 check warms 24 decode graphs plus four setup graphs (M=1/2/4/8). Tight-budget tests retain decode priority, and refused batches reuse their existing encodings for scalar retry. Unseen reference widths still use the existing lazy-capture/fallback path.
On this revision, the official serving regression completed 384/384 requests across C2/C32, plus 16 warmup requests. All 28 Whole-Euler captures occurred during warmup, with none logged in the six subsequent runs. This addresses the observed capture mechanism; it does not establish that every earlier latency regression is resolved.
Also added the transient-memory explanation to the design document: the stacked CFG output and independent per-reference cache copies coexist during setup, with the group size bounded by the existing microbatch limit.
Omni ReviewBot triage noteAutomated triage of commit
These are automated triage suggestions only — the final decision belongs to the maintainers. |
Omni ReviewBot: three questions on the performance claim@natureofnature this PR reads as a performance or value claim:
Before the full evidence checklist, three short questions:
When you answer, the evidence that settles it is: base and head SHA, hardware, model, workload, warm-up and repeat count, mean or percentiles with their spread, and a correctness/quality-equivalence signal; an end-to-end claim also needs stage attribution. |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
Signed-off-by: natureofnature <wzliu@connect.hku.hk>
|
Following up on the performance-evidence questions: Scope and attribution. This change targets the separate initial CFM solve performed for each cold reference. It groups compatible misses while keeping reference extraction, encoder initialization, and speaker projection at M=1. It does not add FP16 execution or change steady-state decode batching. The call-count/cache tests verify that mechanism, and the serving comparison measures its end-to-end effect. A complete per-stage timing profile was not collected, so these results do not establish that cold setup is the dominant service bottleneck. Controlled comparison. Base Value and limits. At C8/C16/C32, audio throughput improves 7.90%/6.64%/8.50%, and mean completion RTF decreases 5.61%/7.42%/7.93%. C1 is approximately flat; C2 throughput falls 2.27%, and C32 mean TTFA rises 4.86%. The earlier C16 memory study also shows an approximately 2.6 GiB increase in sampled peak device usage. This is a concurrent-throughput tradeoff, not a uniform latency or memory improvement. The current head, |
|
@amy-why-3459 PTAL |
amy-why-3459
left a comment
There was a problem hiding this comment.
Thanks for the follow-up. Checked the delta in 532e9ca:
- Refused batched replay now hands each reference's existing
encoded_promptto the scalar retry, so the M=1 encoder/speaker outputs are reused instead of recomputed, including for later groups that hit the memoized_unsupportedkey. The encoding-count tests cover the refusal, disabled-wrapper and full-budget paths. - Offset-0 setup graphs are now precaptured after the decode/tail keys, M=1 first, within the same
max_graphsbudget, and the setup width goes through the same_capture_query_widthas replay, so the warmed keys match what serving hits. The doc note on the transient per-reference cache copies covers the memory point.
Both earlier points look addressed; no further comments on this delta. As you note, reference widths not seen during precapture still go through the lazy capture/fallback path, and the full A/B sweep plus WER are from earlier heads.
natureofnature
left a comment
There was a problem hiding this comment.
Reviewed the six-file change through fresh-reference grouping, CFG/cache row splitting, scalar fallback and decode-first precapture. The existing encodings now survive repeated graph refusals, and cached references retain independent storage and caller order. Both earlier graph findings are addressed.
The reported current-head 279-test suite, H800 mixed-length/continuation and full-budget cache/PCM comparisons, and C2/C32 serving regression cover the changed mechanisms. The earlier A/B sweep and WER remain evidence for their stated revisions; I have not rerun these tests. Repository CI still has pending checks and an AMD failure whose cause I could not inspect.
【Conclusion: Found no blocking issues in commit 532e9ca.】
【Action recommended: None.】
Contributor intent
1. What outcome do you want to achieve?
Improve MiniCPM-o 4.5 serving throughput and latency on a single GPU while checking speech accuracy.
2. What is the highest-leverage variable for that outcome?
The cost of Code2Wav/CFM inference, measured through audio throughput, first-audio latency and RTF under concurrent serving.
3. How will this work move that variable?
Batch compatible cold-reference CFM initialization while preserving each reference's conditioning and cache state, then measure it with the official serving benchmark.
4. Whose customer or user needs does this address, and what are those needs?
MiniCPM-o 4.5 users serving concurrent speech requests on one GPU need higher throughput, predictable latency and checked audio quality.
Purpose
#9159 batches cross-reference decoding. Cold reference initialization still performs one initial CFM solve per reference. This patch groups compatible setup-cache misses and runs their initial solve together, bounded by the existing microbatch and graph batch limits.
Reference extraction, encoder initialization and speaker projection remain M=1. Cached references retain independent storage and conditioning; setup-cache hits survive eviction while a group is being prepared, and publication preserves the caller's reference order.
The optimization uses the existing CUDA fused FP32/TF32 ragged Whole-Euler path with zero frame padding. Other execution policies retain scalar setup. If a batched graph replay is refused, setup reuses the already-computed M=1 encoder/speaker outputs and retries each reference's CFM solve, allowing an existing M=1 graph to remain usable. This avoids duplicate encoding and silently switching a fused scalar solve to an unfused batched fallback when the graph budget is full.
For eligible non-slot execution, the existing reference-driven precapture now warms offset-zero setup graphs after decode/tail shapes, M=1 first, within the unchanged shared graph budget. Warmup still needs to run before measurement. Independent per-reference cache copies are retained; the documentation explains their transient-memory tradeoff.
No deployment defaults or new configuration knobs are introduced. The execution contract is updated in
docs/design/minicpm_o45_mrv2_performance.md. This is separate from #9213's FP16 fused-body work.Performance limitations: the six-point sweep at
41cb8686improves throughput at C4–C32, while C2 regresses slightly and C32 first-audio latency increases. C1 is approximately flat in that sweep; the earlier three-point experiment's approximately 5% C1 regression remains unexplained. The review follow-up addresses repeated fallback encoding and lazy setup capture; its serving regression does not replace the earlier ABBA comparison or establish that all latency regressions are resolved.Test Plan
vLLM Version: 0.32.0; PyTorch 2.13.0+cu130; Transformers 5.19.0; datasets 3.6.0.
vLLM-Omni Commit: baseline
bea9b9e9ecb980d3cbdbf0ee061a42f325b83e75; current review fix and regression evidence at532e9ca1145cef8b7cdd3f5334cff20c6ad9c0ac. The complete six-point serving sweep and unfused fallback check were measured at41cb868615e8480e5d75f6b9dfc9732ccac3eddc. Historical three-point timing was collected at03fe9c65f462a9dd3cbe87b685fa3137921124ff; separate WER and C1-only control atda8df8a98bebb1f589e211f49aa0c75c1a5219f1.The
41cb8686sweep includes the precision/body eligibility gate and scalar fallback on refused graph replay. Timing uses fused FP32/TF32. The earlier quality evidence remains tied to its measured revision; WER evaluation is disabled in the timing sweep and follow-up serving regression.One H800 80 GB; MPS enabled in both arms; MRV2 turn serving; Thinker/Talker BF16; Code2Wav FP32/TF32; stage sequence limits 8/8/8; CFM microbatch 8. The same compatibility adjustment was applied outside this PR to both serving checkouts for vLLM 0.32:
self.llm.get_input_embeddings(input_ids)→self.llm.embed_input_ids(input_ids)inminicpmo_4_5_omni_llm.py.The unchanged baseline
vllm bench serveclient drives both servers. Input is a fixed ordered 64-request English Seed-TTS subset. Each server receives 16 warmup requests at C16, reusing the first 16 evaluation requests. Then C1, C2, C4, C8, C16 and C32 each have one discarded 64-request run and two measured 64-request runs. Server order is A1 → B1 → B2 → A2, giving four measurements per arm/concurrency. Client concurrency increases to 32 while stage sequence limits remain 8/8/8. The historical sweep used C1/C8/C16; its results are retained separately below.Serving configuration and official benchmark commands
Set
SERVER_CHECKOUTto the baseline or candidate checkout,BASE_CHECKOUTto the baseline, andMODEL_PATHto the localopenbmb/MiniCPM-o-4_5snapshot. Use the same model and Seed-TTS rows for both arms.DATASETis the prepared Seed-TTS root containingen/meta.lstand its reference WAVs; use the 64 IDs listed below in that order.WARMUP_DATASETcontains its first 16 rows with the same reference paths.RUNTIME_ENVis the serving/timing Python environment andEVAL_ENVis the separate environment with the optional Seed-TTS WER dependencies. The software versions above describe the serving environment.Save the following as
extra-body.json:{ "modalities": ["text", "audio"], "chat_template_kwargs": {"enable_thinking": false, "use_tts_template": true}, "sampling_params_list": [ {"temperature": 0.0, "top_p": 1.0, "top_k": -1, "max_tokens": 256, "seed": 42, "detokenize": true, "repetition_penalty": 1.0}, {"temperature": 0.8, "top_p": 0.85, "top_k": 25, "max_tokens": 2048, "min_tokens": 50, "seed": 42, "detokenize": false, "repetition_penalty": 1.05}, {"temperature": 0.0, "top_p": 1.0, "top_k": -1, "max_tokens": 65536, "detokenize": true} ] }In a second terminal after server readiness, use a new
RESULT_DIRfor each server start:For the separate quality run, start a fresh server for each arm, repeat the same 16-request C16 warmup, then enable the existing Seed-TTS WER evaluator and save generated audio:
WHISPER_PATHis the localopenai/whisper-large-v3snapshot. Quality-run latency is excluded from the performance table. The C1-only diagnostic instead warms 16 requests at C1, then runs only C1 with one discarded and two measured runs per fresh server, A → B.Fixed ordered Seed-TTS evaluation IDs (warmup reuses the first 16)
Test Result
Review follow-up at
532e9ca1atol=1e-5, rtol=1.3e-6; the largest PCM absolute difference in these checks is6.44e-6. These are real-weight component checks, not a new WER evaluation.vllm bench serveserving regression: C2 and C32, one discarded run plus two measured 64-request runs at each concurrency. All 384/384 requests passed, plus 16/16 disjoint warmup requests. All 28 Whole-Euler captures completed during warmup; none occurred in the six subsequent runs, and no service errors were logged.Candidate-only means from the two measured runs (same command/configuration below; this is not a new A/B comparison):
Concurrency curves at
41cb8686: 1, 2, 4, 8, 16, 32Fresh complete ABBA sweep at
41cb8686, using the protocol above: 48 measured runs, 3,072/3,072 successful requests. Each point is the mean of four runs per arm; shading shows the observed min/max, not a confidence interval. All six concurrency levels were measured in this sweep.Run-level measurements (CSV) · Exact summaries and ranges · Measurement protocol
TTFA is the official client's audio TTFP; RTF is its completion-based audio RTF. The horizontal axis uses log2 spacing for the six measured concurrency levels. The result artifacts live on a separate branch, keeping them out of the implementation diff.
This timing sweep does not replace the separate WER run or establish the cause of the earlier C1 regression. The historical study is retained below with its original revisions and limitations.
Historical three-point study and C1 limitation (timing at 03fe9c6)
All 24 measured performance runs completed 64/64 requests. Values are the mean across four runs; brackets show the observed min/max for audio throughput and first-audio latency. TTFA below is the official client's audio TTFP; RTF is its completion-based audio RTF.
C16: audio throughput +4.93%, mean TTFA −11.29%, mean RTF −4.65%. Mean per-run p95 TTFA changes from 1502.44 to 1494.93 ms, so the mean-latency improvement should not be read as a comparable p95 improvement. C8 throughput improvement is small and its run ranges overlap.
C1 limitation: the main sweep shows a 5.13% throughput regression. A separate C1-only warmup/control gives throughput 8.340→8.350 audio-s/s, mean TTFA 314.03→312.01 ms and RTF 0.12227→0.12178 (two measured runs per arm). Scalar setup microbenchmarks are also approximately equal. These controls do not establish the cause or resolve the original mixed-load regression; warmup/concurrency history and experiment order differ.
Memory: sampled device usage every 2 seconds, not allocator peak. At C16, mean per-run observed peak is 48,725→51,338 MiB; the highest candidate sample is 52,902 MiB. This is a memory tradeoff, not a memory reduction. Two initial baseline C1 intervals lack samples; the other 22 measured intervals are covered. Generated audio duration is identical at C1 (230.4 s/run) and varies slightly with concurrent sampling at C8/C16; request throughput is included alongside audio throughput for that reason.
Separate C8 quality run (candidate da8df8a):
WER did not worsen in this sample. The baseline is above 0.05; the candidate is below it. This is one 64-request comparison, not a general quality guarantee. SIM and UTMOS were not measured.
Correctness regression coverage at
532e9ca1: 279 passed, 14 warnings in 64.90s. It covers mixed shapes, graph batch limits, cache hits/LRU eviction, duplicate references and continuing streams, independent cache ownership, failed setup publication, disabled/unsupported execution policies, graph replay refusal without re-encoding, and decode-first precapture under tight budgets.Real-H800 component checks use native setup APIs and fixed reference features, including unequal prompt lengths, cache ownership, and six continuation chunks extending beyond the 100-frame history suffix. Normal fused execution and full-graph-budget fallback pass numerical comparison at
atol=1e-5, rtol=1.3e-6; earlier TF32 opt-out and explicit unfused checks retain scalar setup. These component checks are separate from WER. Changed-file pre-commit passes except for two inherited mypy missing-annotation errors in the untouched key-mask tests; both errors were reproduced on baselinebea9b9e9. NPU E2E, perceptual quality, and current-head WER remeasurement have not been run.Codex assisted implementation, tests, review and PR drafting.