Skip to content

[Model] Complete MiniCPM-o 4.5 FP16 fused CFM dispatch - #9231

Open
natureofnature wants to merge 3 commits into
vllm-project:mainfrom
natureofnature:perf/minicpm-cfm-fp16-followup
Open

natureofnature wants to merge 3 commits into
vllm-project:mainfrom
natureofnature:perf/minicpm-cfm-fp16-followup

Conversation

@natureofnature

@natureofnature natureofnature commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Contributor intent

1. What outcome do you want to achieve?

Improve MiniCPM-o 4.5 serving throughput, time to first audio, and RTF while preserving output quality.

2. What is the highest-leverage variable for that outcome?

FP16 CFM execution cost, including fused-body selection and attention execution.

3. How will this work move that variable?

Keep FP16 CFM fusion enabled independently of TF32, and route its non-pooled attention through the existing FP16-capable tiled kernel. Measure the resulting serving throughput, latency and audio quality against post-#9218 main at the same precision.

4. Whose customer or user needs does this address, and what are those needs?

MiniCPM-o 4.5 serving users seeking lower latency and RTF, and higher throughput under concurrent requests.

Purpose

Follow-up to #9218. Two FP16 dispatch gaps remain:

  • With token2wav_allow_tf32: false or MINICPMO_CODE2WAV_TF32=off, CUDA disables the default fused CFM body even when token2wav_float16: true or cfm_float16: true selects FP16. The default now accounts for either FP16 flag; an explicit cfm_fused_body setting still takes precedence.
  • The non-pooled fused attention path requires FP32 Q/K/V, so FP16 falls back to SDPA even though cfm_attention already supports FP16. The existing SM80+ dispatch now accepts matching FP16 Q/K/V as well. No new attention backend or dependency is introduced.

The changes are confined to dispatch, regression coverage, and docs/design/minicpm_o45_mrv2_performance.md. FP32 policy, existing platform fallbacks, CFG and Euler step count are unchanged.

Test Result

Base: 0da1d831e07fadccc3ab51ef27b7c21623b693c2 (post-#9218 main). Original sweep: d4b7e7260d2583fe21f41336b30063b2ebf07927. Additional validation: current head 3b866f6a0cbc0291eac237ae369fafae29dae8e6, which adds tests and leaves the runtime unchanged.

Environment: one NVIDIA H800 80 GB; vLLM 0.32.0; PyTorch 2.13.0+cu130; CUDA 13; Triton 3.7.1; FlashInfer 0.7.0.post1 installed. MPS was enabled in every arm, with all three stage workers confirmed as clients.

Additional validation at the current head

C16 throughput and RTF gains reproduced across three fresh server starts. Quality noninferiority is not established for the PR: the C16 and C32 WER upper bounds exceed the predeclared margin. SIM and corrected UTMOS meet their margins at all three tested concurrencies. The WER confidence intervals include zero, so this does not establish a quality regression either.

C16 performance repeat

Same H800, MPS, official client, 64 timing inputs and deployment settings as the original sweep below. Each arm has three fresh server starts, each with 32 warmup requests, one discarded 64-request run, and four measured 64-request runs. All 2,304 included timed requests succeeded. Values are arithmetic means of 12 official per-run metrics per arm, including the per-run TTFA percentiles.

C16 arm Audio throughput (audio-s/s) TTFA p50 (ms) TTFA p95 (ms) Mean RTF
main · CFM FP32 / Triton (TF32 allowed) 28.168 744.61 1433.99 0.5581
main · CFM FP16 / SDPA 29.976 675.71 1297.26 0.5287
PR · CFM FP16 / Triton 33.753 710.89 1233.89 0.4704

Compared with main FP16, PR FP16 improves throughput 12.6% and reduces mean RTF 11.0%. TTFA p50 increases 5.2%, while p95 decreases 4.9%. The original sweep's 19.0% C16 p50 increase is smaller in this repeat, but the latency tradeoff remains. Across individual fresh starts, throughput is 28.930–30.524 audio-s/s for main FP16 and 33.570–33.878 for PR FP16.

The same-precision main FP16 versus PR FP16 comparison measures this PR's dispatch change. The FP32 control uses the existing Triton attention with TF32 allowed; main FP16 switches to SDPA, while PR FP16 uses Triton. The FP32-to-FP16 comparisons therefore describe the respective complete precision modes.

C16 means by fresh server start
C16 arm / fresh start Audio throughput (audio-s/s) TTFA p50 (ms) TTFA p95 (ms) Mean RTF
main · CFM FP32 / Triton (TF32 allowed) / 1 28.499 774.54 1442.48 0.5530
main · CFM FP32 / Triton (TF32 allowed) / 2 28.122 709.56 1394.91 0.5593
main · CFM FP32 / Triton (TF32 allowed) / 3 27.884 749.73 1464.58 0.5620
main · CFM FP16 / SDPA / 1 30.524 715.06 1281.84 0.5179
main · CFM FP16 / SDPA / 2 28.930 664.47 1345.19 0.5455
main · CFM FP16 / SDPA / 3 30.473 647.60 1264.75 0.5227
PR · CFM FP16 / Triton / 1 33.878 722.41 1235.02 0.4673
PR · CFM FP16 / Triton / 2 33.570 733.23 1282.64 0.4750
PR · CFM FP16 / Triton / 3 33.809 677.02 1184.00 0.4688

Held-out quality validation

256 additional English Seed-TTS prompts × C1/C16/C32 × three fresh server starts × three arms = 6,912 generated WAVs. These IDs exclude all 128 inputs previously used for evaluation or warmup. All included requests and WER/SIM/UTMOS scoring calls completed; all saved WAVs are nonempty and finite. The same paired prompt IDs are used throughout. WER is the official mean per-request WER, and SIM is the official WavLM embedding-cosine proxy.

C Arm Mean per-request WER WavLM SIM Corrected UTMOS
1 main · CFM FP32 / Triton (TF32 allowed) 5.727% 0.862257 4.1268
1 main · CFM FP16 / SDPA 5.749% 0.862239 4.1289
1 PR · CFM FP16 / Triton 5.742% 0.862266 4.1242
16 main · CFM FP32 / Triton (TF32 allowed) 6.170% 0.862548 4.1341
16 main · CFM FP16 / SDPA 5.878% 0.862408 4.1314
16 PR · CFM FP16 / Triton 6.091% 0.862136 4.1261
32 main · CFM FP32 / Triton (TF32 allowed) 5.728% 0.862130 4.1246
32 main · CFM FP16 / SDPA 5.512% 0.861871 4.1365
32 PR · CFM FP16 / Triton 5.583% 0.861765 4.1321

Average the three repeats within each prompt, then bootstrap the 256 paired prompts with 20,000 draws, seed 20261022. The predeclared one-sided 95% upper-bound margins are WER +0.20 percentage points, SIM drop 0.005, and UTMOS drop 0.05. All nine bounds must meet their margins for a comparison to pass. These are engineering screens, not perceptual-equivalence thresholds.

Each cell is mean degradation / one-sided 95% upper bound. Positive degradation means higher WER or lower SIM/UTMOS.

Comparison C WER (percentage points) SIM drop UTMOS drop All three bounds within margins?
PR FP16 vs main FP16 1 -0.006 / +0.087 -0.000027 / +0.000082 +0.0047 / +0.0124 Yes
PR FP16 vs main FP16 16 +0.213 / +0.935 +0.000273 / +0.000897 +0.0053 / +0.0144 No
PR FP16 vs main FP16 32 +0.071 / +0.327 +0.000106 / +0.000550 +0.0044 / +0.0145 No
main FP16 vs main FP32 1 +0.022 / +0.130 +0.000018 / +0.000131 -0.0020 / +0.0056 Yes
main FP16 vs main FP32 16 -0.292 / +0.139 +0.000140 / +0.000693 +0.0027 / +0.0125 Yes
main FP16 vs main FP32 32 -0.217 / +0.159 +0.000259 / +0.000706 -0.0120 / -0.0023 Yes
PR FP16 vs main FP32 1 +0.016 / +0.102 -0.000009 / +0.000110 +0.0027 / +0.0098 Yes
PR FP16 vs main FP32 16 -0.079 / +0.305 +0.000413 / +0.000884 +0.0080 / +0.0163 No
PR FP16 vs main FP32 32 -0.146 / +0.171 +0.000364 / +0.000840 -0.0076 / +0.0025 Yes

Main FP16 versus main FP32 meets all nine bounds. PR FP16 versus main FP16 does not: the WER upper bounds are +0.935 pp at C16 and +0.327 pp at C32. Their two-sided 95% intervals are [−0.435, +1.110] pp and [−0.223, +0.382] pp, respectively. PR FP16 versus main FP32 also remains inconclusive at C16, whose WER upper bound is +0.305 pp. No quality-equivalence or quality-improvement claim is made.

The largest C16 WER contribution comes from the three-word prompt “Not a thing.”, where all three arms receive large WER values and the PR scores higher than main FP16 in two repeats. That prompt is retained in every mean and confidence interval.

UTMOS correction: the pinned balacoon/utmos JIT internally casts its input to float32 and divides by 32768, consistent with its documented int16 input. The official evaluator passes already-normalized floats, applying the amplitude scaling twice. The unmodified official JSONs are retained, but their UTMOS values are not used for acceptance. All 6,912 saved WAVs were rescored offline with the same official 16 kHz resampler and pinned JIT, passing normalized float audio multiplied by 32768. A probe on four separate smoke-test WAVs agreed with documented int16 input within 0.0035 MOS. WER, SIM, generated audio and performance are unchanged. The thresholds were fixed before generation; this scorer correction is the only scoring-protocol amendment.

Real-asset serving with TF32 disabled

No explicit cfm_fused_body override is present in the deployment inheritance chain. Each case uses actual model assets and 16 official C4 requests with audio saving and quality-scoring execution:

Revision / precision TF32 disabled through Observed default fused body Successful requests
main / CFM-only FP16 token2wav_allow_tf32: false False 16/16
PR / CFM-only FP16 token2wav_allow_tf32: false True 16/16
PR / full-Flow FP16 MINICPMO_CODE2WAV_TF32=off True 16/16

These 48/48 serving checks validate actual default selection and successful audio generation. The full-Flow case is a serving smoke test; the 256-prompt quality campaign above uses CFM-only FP16.

One container shutdown interrupted an earlier FP32 start. Its partial results are retained separately, and the entire start was repeated. No completed prompt or run was removed based on its result.

Original official serving benchmark

Unmodified vllm bench serve --omni, MRv2 turn serving, 64 fixed English Seed-TTS requests with per-request reference audio. All three arms use CFM-only FP16, cfm_float16: true and token2wav_float16: false; encoder, speaker extraction and HiFT retain their existing precision. Thinker/Talker use BF16. All use 10 Euler steps, stage sequence limits 16/8/8, CFM microbatch 4 and graph budget 64.

The CFM attention paths compared are main: SDPA, PR: the existing Triton kernel, and FlashInfer: an experimental FA3 paged-prefill prototype using FlashInfer 0.7.0.post1, based on 50027779f66463f1765330bc7cb67659b1bffeb7. The FlashInfer implementation is not included in this PR. The reproduction commands below apply to main and PR; running the FlashInfer arm requires that local prototype.

Each concurrency has four measured runs per arm: two runs from each of two fresh server starts. Each server first receives 32 disjoint warmup requests; each tested concurrency gets a discarded 64-request run before the two measured runs. Cells below are arithmetic means of the official per-run metrics, including the per-run TTFA percentiles. Quality evaluation runs separately after timing.

C CFM attention Audio throughput (audio-s/s) TTFA p50 (ms) TTFA p95 (ms) Mean RTF
1 main · SDPA 9.101 269.27 313.48 0.1116
1 PR · Triton 9.227 266.20 309.07 0.1099
1 FlashInfer (experimental) 9.305 262.93 310.18 0.1090
4 main · SDPA 21.215 417.54 522.80 0.1933
4 PR · Triton 22.812 397.11 496.44 0.1797
4 FlashInfer (experimental) 23.222 394.59 497.17 0.1760
8 main · SDPA 27.265 525.84 748.97 0.2945
8 PR · Triton 28.786 559.35 714.14 0.2798
8 FlashInfer (experimental) 29.872 555.08 709.23 0.2730
16 main · SDPA 30.459 637.74 1256.79 0.5166
16 PR · Triton 34.777 759.01 1148.19 0.4540
16 FlashInfer (experimental) 34.013 653.38 1175.18 0.4708
32 main · SDPA 32.933 914.49 2489.77 0.9501
32 PR · Triton 35.754 861.92 2293.84 0.8732
32 FlashInfer (experimental) 35.756 883.58 2329.39 0.8676

MiniCPM-o FP16 CFM: original performance sweep and updated 256-prompt WER, WavLM SIM and corrected UTMOS validation

SVG version

Performance panels retain the original C1/C4/C8/C16/C32 sweep. Quality panels use the new 256-prompt C1/C16/C32 validation above, with three fresh server starts per arm, and compare main FP32, main FP16 and PR FP16. Shading shows the range of run means, not confidence intervals. FlashInfer was not rerun on this quality cohort. The WER panel and table reflect that PR-versus-main FP16 noninferiority is not established at C16/C32; SIM and corrected UTMOS meet their margins.

Relative to main, the PR improves C16 throughput 14.2% and reduces mean RTF 12.1%; at C32 the changes are +8.6% and −8.1%, respectively. TTFA is a tradeoff: C8 p50 increases 6.4%, and C16 p50 increases 19.0% (637.74 → 759.01 ms), while p95 improves at both. This is not a uniform latency improvement.

FlashInfer has higher measured throughput than Triton at C4/C8, lower throughput at C16, and nearly identical throughput at C32. Its C16 TTFA p50 is lower than Triton's (653.38 versus 759.01 ms). These are workload-specific tradeoffs; the FlashInfer results are retained for comparison without adding its implementation to this PR.

All 3,840 timed requests across the three arms succeeded. At C8, generated duration averages 242.08 s main / 232.74 s Triton / 231.38 s FlashInfer. The PR's request throughput improves 9.7% over main, compared with 5.6% for audio throughput. All runs are retained. This is a small fixed workload, not a production traffic estimate.

Attention cost attribution

An isolated CFM attention microbenchmark on the same H800 measures the cost removed by the dispatch change. Both calls use FP16, the existing non-pooled KV arena layout, 8 heads × 64 dimensions, and per-row valid-key masks; CFG uses twice the request batch. These are single-attention timings, not whole-stage timings or a measurement of CFM's share of end-to-end runtime.

Request batch Query frames Cached frames SDPA (µs) Existing Triton kernel (µs)
1 50 350 29.539 11.464
4 50 350 30.097 11.977
8 100 400 45.237 24.235

Timing uses five eager warmups, 16 calls captured per CUDA Graph, one graph warmup, then five CUDA-event measurements of 32 replays each; the table reports the median per-call time. Both implementations pass comparison with FP32 math SDPA at rtol=atol=2e-3.

In the serving A/B, both deployments explicitly select cfm_fused_body: true and allow TF32, so the separate TF32-disabled default fix does not change either measured execution path. The same-precision serving comparison therefore isolates the attention dispatch change; no additional performance gain is attributed to the default-selection fix. The throughput and RTF gains, and the C8/C16 TTFA p50 regressions, are reported above.

Initial 64-prompt quality screen

Separate official Whisper large-v3 WER and WavLM embedding-cosine SIM evaluation at C16 only, 64 requests per fresh server. WER is the mean per-request error rate. SIM is the benchmark's WavLM proxy, not UniSpeech SIM-o. Main and FlashInfer each have three quality runs; the PR has two. These initial results are retained in the table below; the figure now shows the separate 256-prompt validation above.

CFM attention / server start WER WavLM SIM
main · SDPA / 1 4.382% 0.858818
main · SDPA / 2 6.723% 0.859592
main · SDPA / 3 5.127% 0.858505
PR · Triton / 1 5.370% 0.859381
PR · Triton / 2 5.297% 0.859634
FlashInfer (experimental) / 1 5.266% 0.860028
FlashInfer (experimental) / 2 4.851% 0.859328
FlashInfer (experimental) / 3 7.169% 0.858467

Mean WER across starts is 5.411% main / 5.334% Triton / 5.762% FlashInfer; mean SIM is 0.858971 / 0.859507 / 0.859274, respectively. Main itself varies from 4.382% to 6.723% WER. These results do not establish a quality improvement or statistical equivalence. All 512 saved WAVs were nonempty and finite, with zero request, ASR or SIM failures. UTMOS, listening evaluation, V1/duplex E2E and NPU E2E were not evaluated in this initial screen.

Regression tests

  • 186 passed, 14 warnings in 3.03s at 3b866f6a0cbc0291eac237ae369fafae29dae8e6 on H800 for the three files below. GPU tests call the real tiled kernel and compare fused FP16 output/cache tensors with the original ragged body at rtol=atol=2e-3.
  • The FP16 attention dispatch assertion fails on the base implementation and passes on the PR.
  • The 56 new configuration cases give 8 failed, 48 passed with the base implementation: the eight failures are CUDA FP16 defaults with TF32 disabled.
  • Four new GPU cases exercise cfm_float16 and token2wav_float16 with TF32 disabled through either the stage configuration or MINICPMO_CODE2WAV_TF32=off. They run the real Code2Wav configuration and backend initialization, verify that fusion is enabled without the unsupported warning, and compare DiT outputs and both caches against the original body at rtol=atol=2e-3, with CUDA matmul and cuDNN TF32 both disabled. Only model-asset loading is substituted; _ragged_fused_body is not forced on. 4 passed on the PR; 4 failed on an isolated base checkout because fusion was not enabled.
  • The same checks confirm that CFM-only initialization converts the estimator to FP16 via estimator.half(); it does not leave the estimator weights FP32 and rely solely on autocast. Encoder parameters remain FP32 for cfm_float16 and become FP16 for token2wav_float16. These are initialization/DiT checks. Performance measurements use TF32 enabled; the additional real-asset serving checks above cover TF32 disabled.
  • All applicable local pre-commit hooks and git diff --check passed.

Test Plan

python -m pytest -q \
  tests/model_executor/models/minicpmo_4_5/test_code2wav_device_spk_flag.py \
  tests/model_executor/models/minicpmo_4_5/test_cfm_tiled_attention.py \
  tests/model_executor/models/minicpmo_4_5/test_dit_fused.py

pre-commit run --files \
  vllm_omni/model_executor/models/minicpmo_4_5/minicpmo_4_5_code2wav.py \
  vllm_omni/model_executor/models/minicpmo_4_5/dit_fused.py \
  tests/model_executor/models/minicpmo_4_5/test_code2wav_device_spk_flag.py \
  tests/model_executor/models/minicpmo_4_5/test_dit_fused.py \
  docs/design/minicpm_o45_mrv2_performance.md

git diff --check
Reproduction: pinned inputs and deployment

Set the checkout paths to the base and PR revisions above. Both tested checkouts have the same one-line vLLM 0.32.0 API adaptation in minicpmo_4_5_omni_llm.py: self.llm.get_input_embeddings(input_ids) → self.llm.embed_input_ids(input_ids). It is outside this PR. All 33 official benchmark Python files are identical across arms.

Download the pinned models and dataset subset before enabling offline mode:

export MAIN_CHECKOUT=/absolute/path/to/main-checkout
export PR_CHECKOUT=/absolute/path/to/pr-checkout
export BENCH_DIR=/absolute/path/to/benchmark
mkdir -p "$BENCH_DIR"
unset HF_HUB_OFFLINE HF_DATASETS_OFFLINE TRANSFORMERS_OFFLINE
export MODEL_PATH="$(hf download openbmb/MiniCPM-o-4_5 --revision 073dbbc8c5bc0af2d789e1ce12e7c17a6be746e1)"
export WHISPER_PATH="$(hf download openai/whisper-large-v3 --revision 06f233fe06e710322aca913c1bc4249a0d71fce1)"
export WAVLM_PATH="$(hf download microsoft/wavlm-base-plus --revision 4c66d4806a428f2e922ccfa1a962776e232d487b)"

python - <<'PY'
import os
import shutil
from pathlib import Path
from huggingface_hub import hf_hub_download

dataset = dict(repo_id="zhaochenyang20/seed-tts-eval", repo_type="dataset",
               revision="8f5e1aa2a35d42f42e940074c1983358b9491f89")
meta = Path(hf_hub_download(filename="en/meta.lst", **dataset))
rows = [row for row in meta.read_text().splitlines() if "|" in row]
# Zero-based rows in the pinned meta.lst, in the measured request order.
# First 64: evaluation; remaining 32: disjoint warmup.
indices = [
    316, 1068, 994, 934, 439, 221, 742, 368, 566, 717, 34, 1083,
    490, 1040, 525, 196, 461, 270, 230, 388, 454, 1015, 726, 1007,
    114, 631, 791, 447, 1023, 666, 720, 85, 798, 217, 529, 314,
    121, 697, 917, 643, 20, 132, 425, 235, 693, 656, 533, 847,
    282, 418, 467, 489, 810, 916, 77, 1073, 591, 827, 698, 663,
    478, 117, 273, 38, 542, 105, 799, 109, 252, 971, 373, 475,
    611, 755, 290, 1044, 198, 716, 927, 729, 346, 37, 266, 982,
    1043, 343, 952, 179, 875, 505, 200, 1017, 11, 479, 966, 756,
]
for split, selected in (("evaluation", indices[:64]), ("warmup", indices[64:])):
    dest = Path(os.environ["BENCH_DIR"]) / "inputs" / split / "en"
    dest.mkdir(parents=True, exist_ok=True)
    subset = [rows[i] for i in selected]
    (dest / "meta.lst").write_text("\n".join(subset) + "\n")
    for row in subset:
        relative = row.split("|")[2].strip()
        source = hf_hub_download(filename="en/" + relative, **dataset)
        target = dest / relative
        target.parent.mkdir(parents=True, exist_ok=True)
        shutil.copyfile(source, target)
PY

Start one server at a time. Set SERVER_CHECKOUT to the main or PR checkout; the same overlay and precision apply to both.

export CUDA_VISIBLE_DEVICES=0  # Select the same idle H800 for every run.
export SERVER_CHECKOUT="$MAIN_CHECKOUT"
export DEPLOY_CONFIG="$BENCH_DIR/deploy.yaml"
export HF_HUB_OFFLINE=1 HF_DATASETS_OFFLINE=1 TRANSFORMERS_OFFLINE=1
export VLLM_WORKER_MULTIPROC_METHOD=spawn TOKENIZERS_PARALLELISM=false
unset CUDA_MPS_PIPE_DIRECTORY CUDA_MPS_LOG_DIRECTORY CUDA_MPS_ACTIVE_THREAD_PERCENTAGE

cat > "$DEPLOY_CONFIG" <<YAML
base_config: ${SERVER_CHECKOUT}/vllm_omni/deploy/minicpmo_4_5_turn_mrv2.yaml
connectors:
  connector_of_shared_memory:
    extra:
      cfm_graph_precapture: true
      cfm_max_graphs: 64
      hift_graph_capture_batch_sizes:
      - 1
      - 2
      - 4
      - 8
      micro_batch_size: 4
      cfm_float16: true
      token2wav_float16: false
platforms:
  cuda:
    cuda_mps: true
    stages:
    - kv_cache_memory_bytes: 4294967296
      stage_id: 0
    - kv_cache_memory_bytes: 2147483648
      stage_id: 1
stages:
- limit_mm_per_prompt:
    audio: 0
    image: 0
    video: 0
  max_model_len: 4096
  max_num_batched_tokens: 4096
  max_num_seqs: 16
  stage_id: 0
- max_num_seqs: 8
  stage_id: 1
- max_num_seqs: 8
  stage_id: 2
YAML

PYTHONPATH="$SERVER_CHECKOUT" \
vllm serve "$MODEL_PATH" --omni --deploy-config "$DEPLOY_CONFIG" \
  --trust-remote-code --served-model-name crossref-test \
  --host 127.0.0.1 --port 18590

The overlay inherits cfm_fused_body: true and token2wav_allow_tf32: true from the MRv2 configs. Consequently, these performance results measure the attention dispatch change; the TF32-disabled default fix is covered by regression tests and the real-asset serving checks above. Stop each server and wait for its workers and MPS instance to exit before starting the next.

Official client commands and quality evaluation

Run in a second terminal with the same model/input paths, after /health returns 200. Use the main checkout for the client in every arm. The function only invokes the official CLI; it does not implement a separate benchmark client or metric calculation.

export PYTHONPATH="$MAIN_CHECKOUT"
export RESULT_DIR="$BENCH_DIR/results/main-a1"  # Use a unique directory for each fresh server.
export SEED_TTS_WER_EVAL=0 SEED_TTS_UTMOS_EVAL=0 SEED_TTS_SIM_EVAL=0

cat > "$BENCH_DIR/extra-body.json" <<'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, "repetition_penalty": 1.05, "min_tokens": 50, "max_tokens": 2048, "seed": 42, "detokenize": false},
    {"temperature": 0.0, "top_p": 1.0, "top_k": -1, "max_tokens": 65536, "detokenize": true}
  ]
}
JSON

bench() {
  local split=$1 count=$2 concurrency=$3 name=$4 ready_timeout=${5:-0}
  vllm bench serve --omni \
    --backend openai-chat-omni --base-url http://127.0.0.1:18590 \
    --endpoint /v1/chat/completions --model crossref-test \
    --tokenizer "$MODEL_PATH" --trust-remote-code \
    --dataset-name seed-tts --dataset-path "$BENCH_DIR/inputs/$split" \
    --seed-tts-locale en \
    --seed-tts-system-prompt 'Please read the following text out loud in English.' \
    --disable-shuffle --num-prompts "$count" --output-len 256 --seed 42 \
    --request-rate inf --max-concurrency "$concurrency" --num-warmups 0 \
    --ready-check-timeout-sec "$ready_timeout" --extra-body "$(< "$BENCH_DIR/extra-body.json")" \
    --percentile-metrics audio_ttfp,audio_rtf,audio_duration,e2el \
    --metric-percentiles 50,95 --disable-tqdm --save-result --save-detailed \
    --result-dir "$RESULT_DIR/c$concurrency" --result-filename "$name.json"
}

bench warmup 32 16 warmup 60
for c in ${CONCURRENCIES:-1 4 8 16 32}; do
  bench evaluation 64 "$c" discard
  bench evaluation 64 "$c" r1
  bench evaluation 64 "$c" r2
done

Use a fresh RESULT_DIR for every server start. The measured main starts used CONCURRENCIES="1 16", "1 4 8 16 32", and "4 8 32", respectively. Both PR starts used all five concurrencies. Thus every main/PR concurrency has two server starts and four timed runs; quality was evaluated once at C16 after each start's timing runs. The three FlashInfer starts used the same concurrency lists as the three main starts, with one separate C16 quality run after each.

Run quality evaluation using the same bench function, outside the timed loop:

export SEED_TTS_WER_EVAL=1 SEED_TTS_WER_SAVE_ITEMS=1 SEED_TTS_SIM_EVAL=1
export SEED_TTS_HF_WHISPER_MODEL="$WHISPER_PATH"
export SEED_TTS_WAVLM_MODEL="$WAVLM_PATH"
export SEED_TTS_EVAL_DEVICE=cuda:0
export SEED_TTS_WER_SAVE_AUDIO_DIR="$RESULT_DIR/audio-c16"
bench evaluation 64 16 quality
Additional validation: fixed inputs, commands and scoring

Use the same pinned model, Whisper, WavLM, deployment overlay, request body and bench function above. Use head 3b866f6a0cbc0291eac237ae369fafae29dae8e6 for the PR. Each arm gets three fresh server starts and a separate result directory per start. Start order: main FP16, PR FP16, main FP32, PR FP16, main FP32, main FP16, main FP32, main FP16, PR FP16. Set cfm_float16: false for main FP32 and true for both FP16 arms; keep token2wav_float16: false and TF32 allowed in all three. Main FP32 uses the existing Triton attention, main FP16 uses SDPA, and PR FP16 uses Triton.

Download the 256 additional inputs and pinned UTMOS checkpoint before enabling offline mode:

unset HF_HUB_OFFLINE HF_DATASETS_OFFLINE TRANSFORMERS_OFFLINE
export HF_HUB_CACHE="$BENCH_DIR/eval-hf-cache"
export UTMOS_PATH="$(hf download balacoon/utmos utmos.jit \
  --revision b44d848644aa5d89a6e4f180ea50452d8c162db2)"

python - <<'PY'
import hashlib
import os
import shutil
from pathlib import Path
from huggingface_hub import hf_hub_download

dataset = dict(repo_id="zhaochenyang20/seed-tts-eval", repo_type="dataset",
               revision="8f5e1aa2a35d42f42e940074c1983358b9491f89")
meta = Path(hf_hub_download(filename="en/meta.lst", **dataset))
rows = [s for s in meta.read_text().splitlines() if s and not s.startswith("#")]
# Zero-based rows in the pinned meta.lst, in measured request order.
indices = [353, 858, 946, 180, 251, 484, 247, 583, 1076, 625, 492, 524, 664, 220, 773, 118, 446, 383, 1038, 610, 769, 759, 840, 640, 499, 39, 953, 523, 1029, 909, 1059, 3, 943, 911, 836, 995, 629, 1055, 465, 823, 584, 1084, 317, 248, 907, 15, 876, 637, 1, 579, 792, 337, 417, 250, 162, 704, 31, 725, 1054, 882, 319, 239, 218, 241, 885, 904, 102, 582, 390, 568, 135, 825, 201, 997, 191, 918, 480, 127, 903, 472, 302, 255, 713, 277, 299, 639, 238, 795, 296, 259, 443, 458, 432, 92, 370, 262, 305, 192, 120, 433, 449, 660, 195, 377, 140, 93, 614, 748, 554, 681, 28, 189, 738, 413, 816, 242, 588, 690, 576, 881, 310, 607, 171, 841, 699, 1014, 1067, 158, 400, 148, 40, 289, 980, 460, 831, 468, 1071, 409, 837, 137, 362, 10, 6, 297, 1069, 291, 312, 978, 987, 867, 887, 857, 414, 936, 335, 949, 749, 928, 778, 981, 209, 757, 534, 275, 701, 325, 372, 76, 718, 495, 334, 834, 537, 807, 979, 322, 494, 386, 307, 202, 455, 731, 360, 642, 937, 924, 751, 824, 743, 9, 1080, 648, 380, 890, 26, 75, 802, 283, 985, 793, 1057, 57, 190, 165, 919, 84, 839, 744, 767, 512, 945, 1079, 164, 408, 212, 204, 547, 616, 219, 518, 709, 806, 17, 62, 436, 382, 89, 735, 404, 286, 763, 783, 168, 268, 1075, 931, 650, 722, 184, 765, 150, 815, 628, 464, 739, 680, 513, 771, 111, 869, 658, 864, 211, 678, 516, 596]
selected = [rows[i] for i in indices]
dest = Path(os.environ["BENCH_DIR"]) / "inputs/holdout/en"
dest.mkdir(parents=True, exist_ok=True)
(dest / "meta.lst").write_text("\n".join(selected) + "\n")
assert hashlib.sha256((dest / "meta.lst").read_bytes()).hexdigest() == \
    "90ae8153fe419d7afad93e04166cfede49043f0ae717c8561ea7ad1b6d393d62"
for row in selected:
    relative = row.split("|")[2]
    target = dest / relative
    if not target.exists():
        target.parent.mkdir(parents=True, exist_ok=True)
        shutil.copyfile(hf_hub_download(filename="en/" + relative, **dataset), target)
# The official evaluator resolves refs/main; pin that cache entry as well.
refs = Path(os.environ["HF_HUB_CACHE"]) / "models--balacoon--utmos/refs"
refs.mkdir(parents=True, exist_ok=True)
(refs / "main").write_text("b44d848644aa5d89a6e4f180ea50452d8c162db2")
assert hashlib.sha256(Path(os.environ["UTMOS_PATH"]).read_bytes()).hexdigest() == \
    "3080923f49ee69eb81ad875491eef94e533bf329d70e0cc37fa23745c464669d"
PY

export HF_HUB_OFFLINE=1 HF_DATASETS_OFFLINE=1 TRANSFORMERS_OFFLINE=1

For each fresh server, run the following with a unique RESULT_DIR. Quality scoring is disabled throughout timing:

export SEED_TTS_WER_EVAL=0 SEED_TTS_SIM_EVAL=0 SEED_TTS_UTMOS_EVAL=0
bench warmup 32 16 warmup 60
bench evaluation 64 16 discard
for r in 1 2 3 4; do bench evaluation 64 16 "r$r"; done

export SEED_TTS_WER_EVAL=1 SEED_TTS_WER_SAVE_ITEMS=1 SEED_TTS_SIM_EVAL=1
export SEED_TTS_UTMOS_EVAL=1 SEED_TTS_UTMOS_DEVICE=cuda:0
export SEED_TTS_HF_WHISPER_MODEL="$WHISPER_PATH"
export SEED_TTS_WAVLM_MODEL="$WAVLM_PATH" SEED_TTS_EVAL_DEVICE=cuda:0
for c in 1 16 32; do
  export SEED_TTS_WER_SAVE_AUDIO_DIR="$RESULT_DIR/audio-c$c"
  bench holdout 256 "$c" quality
done

Retain the original official JSONs. After serving finishes, correct UTMOS on those same saved WAVs using the official resampler and pinned JIT loader:

import json
import os
from pathlib import Path
import torch
from vllm_omni.benchmarks.data_modules.seed_tts_eval import _audio_path_to_f32_16k
from vllm_omni.benchmarks.data_modules.utmos_jit import load_utmos_jit

model = load_utmos_jit(os.environ["UTMOS_PATH"], "cuda:0")
for c in (1, 16, 32):
    result = Path(os.environ["RESULT_DIR"]) / f"c{c}/quality.json"
    items = json.loads(result.read_text())["seed_tts_wer_eval_items"]
    corrected = []
    for item in items:
        x = torch.from_numpy(_audio_path_to_f32_16k(item["audio_path"]))[None].cuda()
        with torch.inference_mode():
            score = model(x * 32768.0).reshape(-1)[0].item()
        corrected.append({"utterance_id": item["utterance_id"], "utmos": score})
    result.with_name("quality.utmos-corrected.json").write_text(json.dumps(corrected))

Statistical postprocessing is separate from the official client. For each concurrency and metric, align the 256 IDs, average the three repeats within each ID, then subtract reference from candidate. Multiply WER differences by 100 to obtain percentage points; negate SIM/UTMOS differences so positive always means degradation. Draw 20,000 bootstrap samples of 256 paired prompt indices with numpy.random.default_rng(20261022). The 95th percentile of the bootstrap mean is the one-sided 95% upper bound. Thresholds were fixed before generation: WER +0.20 percentage points, SIM drop 0.005, UTMOS drop 0.05. All nine bounds must meet their margins for a comparison to pass. No prompts are removed.

For the TF32-disabled serving checks, remove cfm_fused_body from the MRv2 YAML inheritance chain so initialization selects its default. Test main and PR with cfm_float16: true, token2wav_float16: false, token2wav_allow_tf32: false; separately test the PR with token2wav_float16: true, cfm_float16: false, and MINICPMO_CODE2WAV_TF32=off. Each gets a fresh server and bench evaluation 16 4 preflight 60 with audio saving and quality evaluation enabled. These 16-request checks establish successful serving and actual default selection, not quality noninferiority for full-Flow FP16.

Signed-off-by: natureofnature <wzliu@connect.hku.hk>
Signed-off-by: natureofnature <wzliu@connect.hku.hk>
@natureofnature natureofnature changed the title [Bugfix] Keep MiniCPM-o 4.5 FP16 fusion independent of TF32 [Model] Complete MiniCPM-o 4.5 FP16 fused CFM dispatch Oct 22, 2026
@natureofnature
natureofnature marked this pull request as ready for review October 22, 2026 06:59
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-22T06:59:46.277553Z
We couldn’t complete this review. Comment @codex review to try again.
d4b7e72 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@vllm-omni-review-bot

Copy link
Copy Markdown

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 amy-why-3459 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the dispatch change is small and the FP16 path through cfm_attention already existed in the kernel (FP16 dots with FP32 accumulation), so routing matching FP16 Q/K/V there looks right to me. The test that asserts the tiled kernel is actually hit under FP16 is a good addition.

One gap on the second half of the change (the TF32-disabled default), see inline.

Comment on lines +479 to +481
self._tf32_mode != "off"
or read_bool_flag(extra, "token2wav_float16", False)
or read_bool_flag(extra, "cfm_float16", False)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This flips the default to the fused body for token2wav_allow_tf32: false / MINICPMO_CODE2WAV_TF32=off + FP16, but per the PR description the serving runs all inherit token2wav_allow_tf32: true, and the new tests only assert the fused_body flag in the config dict. So the newly enabled combination is never exercised end to end.

That matters mostly for cfm_float16: true alone: there the estimator weights stay FP32 and FP16 comes from autocast (_cfm_only_float16), so whether BatchedToken2Wav really ends up with _ragged_fused_body=True, and whether any op in the fused body that isn't autocast-cast (modulation table, torch._addmm_activation in _mlp, conv taps) now runs as IEEE FP32 without TF32, isn't covered. Could you add one short check with TF32 off + cfm_float16: true (and one with token2wav_float16: true), e.g. extending test_fp16_fused_body_matches_fp16_ragged_body or a quick C16 run, confirming no "cfm_fused_body requested but unsupported here" warning and output parity with the original body?

@vllm-omni-review-bot

vllm-omni-review-bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Omni ReviewBot triage note

Automated triage of commit 3b866f6a0cbc produced:

  • Priority: high. Prompt maintainer attention is suggested.

These are automated triage suggestions only — the final decision belongs to the maintainers.

@vllm-omni-review-bot

vllm-omni-review-bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Omni ReviewBot: three questions on the performance claim

@natureofnature this PR reads as a performance or value claim:

  • claim: Compared with main FP16, PR FP16 improves throughput 12.6% and reduces mean RTF 11.0%.
  • claim: Relative to main, the PR improves C16 throughput 14.2% and reduces mean RTF 12.1%; at C32 the changes are +8.6% and −8.1%, respectively.
  • claim: The PR's request throughput improves 9.7% over main, compared with 5.6% for audio throughput.

Before the full evidence checklist, three short questions:

  1. Bottleneck — what is the current bottleneck, and which profile, trace or per-stage measurement shows it?
  2. Value — what does the change buy the user or the system (latency, throughput, memory, cost), and at which workload?
  3. A/B or ablation — is there a same-workload, same-head/config comparison that isolates each main claim on its own? For stacked optimizations, one number per item rather than a blended delta.

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.

Signed-off-by: natureofnature <wzliu@connect.hku.hk>

@amy-why-3459 amy-why-3459 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding test_tf32_disabled_fp16_default_initializes_fused_body, this covers what I asked about earlier: with TF32 off via either token2wav_allow_tf32: false or MINICPMO_CODE2WAV_TF32=off, both cfm_float16 and token2wav_float16 now go through the real MiniCPMO45Code2Wav / BatchedToken2Wav init, assert the fused body is selected without the unsupported warning, check the FP16 estimator / encoder dtypes, and compare fused vs. ragged outputs and caches under the real _cfm_autocast. The fused_calls check plus the TF32 asserts inside the wrapper make sure the fused path actually ran with TF32 disabled. No further concerns from me on the delta since d4b7e72.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants