Repository navigation
[Feature][TTS] Port static chunk ramp to Moss-TTS streaming codec decode - #8124
Conversation
MOSS-TTS Local streaming has a chunk-size cliff: initial_codec_chunk_frames=1 emits an 80ms first chunk, then jumps straight to 15-frame (1.2s) steady chunks. Under C8 concurrency the client playback buffer never builds up (measured: 0% streaming continuity, ~0.37s mean underrun per request). Port the static chunk ramp (Phase 1 of the Qwen3-TTS ramp design, vllm-project#5152) to the MOSS-TTS raw async-chunk path: - moss_tts.py: opt-in codec_chunk_ramp ladder replaces the IC/steady threshold with a 2-way selection (ladder indexed by the connector-owned ramp_chunk_count; ramp-off path byte-identical to original). - modeling_moss_tts_codec.py: _connector_list helper; ramp sizes join _streaming_graph_frame_sizes so every ladder (B,T) graph is captured during NPUGraph warmup (zero runtime captures; count stays 35/35). - moss_tts_local.yaml: commented opt-in example. - tests: 6 CPU UTs (ladder sequence, finish flush, steady fallback, string config, backward compat). 910B2C, C8x40, 9 rounds (R1 cold discarded, median R2-R9): continuity 0% -> 60% (best round 100%), underrun 0.37s -> 0.10s (-74%), TTFP ~neutral, RTF 0.22 -> 0.26 (+18%, still far below 1.0). Signed-off-by: Wallbreazzz <110282866+Wallbreazzz@users.noreply.github.com>
|
This PR appears to belong to: docs/design/module/model_integration.md, docs/design/module/ar_runtime.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 @Wallbreazzz, 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. |
|
Author self-review — re-read the full diff before submitting; checked:
Known trade-offs (intentional scope):
@vllm-omni-review-bot please run an automated review. |
linyueqian
left a comment
There was a problem hiding this comment.
Thanks for porting the static ramp, the approach is clean: the ramp-off path is byte-identical, the ladder reuses parse_chunk_ramp / ramp_chunk_size on the processor side, and adding the ladder sizes to _streaming_graph_frame_sizes so every (B, T) is captured at warmup is the right call. One initialization defect inline. Two notes on the evidence: the numbers were measured on v0.28.0 (eb11446b) plus #7008 and #7280, so please record the exact ramp revision you benchmarked and whether it matches this head; and since this is a latency/UX change with an RTF cost (+18% in your table), a CUDA run of the same C8 voice-clone streaming comparison on current main would help show the continuity and underrun gains carry over beyond the 910B2C. Static read of the four files against merge-base 3b02e533; no PR code executed.
| raw = extra_cfg[name] | ||
| if isinstance(raw, str): | ||
| return [int(x.strip()) for x in raw.split(",")] | ||
| return [int(x) for x in raw] |
There was a problem hiding this comment.
[important] This parser disagrees with the one the stage input processor uses. With codec_chunk_ramp: left empty (YAML null), the key is present, so this line iterates None and raises TypeError in the codec constructor, and Stage 1 never starts, while parse_chunk_ramp in chunk_size_utils.py treats None or an invalid value as "ramp disabled" and only warns. Malformed strings and one-entry ladders diverge the same way (the processor disables the ramp, the codec raises or warms up sizes nobody emits). Deriving the warmup sizes from parse_chunk_ramp(extra_cfg, steady=...) keeps both sides on one validated ladder; a codec-init test for an explicit null and an invalid value would lock that in.
There was a problem hiding this comment.
Fixed in 2a4826d: _connector_list removed; the warmup sizes now derive via _resolve_streaming_graph_frame_sizes() → parse_chunk_ramp, the same helper the processor uses, so null / invalid / single-entry values disable the ramp on both sides with a warning instead of raising in codec __init__. Pinned by 5 new CPU tests (explicit null, malformed string, single-entry, non-positive, valid list + comma-separated string). Extracted as a module-level pure function rather than an init-level test because the decoder __init__ loads the ~1.6B audio tokenizer.
Omni ReviewBot routing recordAssigned Strict under experiment |
vllm-omni-review-bot
left a comment
There was a problem hiding this comment.
Omni ReviewBot review
PR description
Moss-TTS Local streaming still used a 1-frame first codec packet and then jumped straight to 15-frame (1.2 s) chunks. This PR adds an opt-in codec_chunk_ramp ladder on the raw async-chunk processor so emit sizes can step 2 → 4 → 8 → 15, reusing the existing parse_chunk_ramp / ramp_chunk_size helpers, while the codec warmup set also includes those ladder T values so NPUGraph capture covers every steady (B, T) the ramp will emit. With the key absent, threshold selection stays on the original initial/steady path and the graph set stays {initial, steady}. The commented Local YAML example is documentation only; default deployments do not enable the ramp.
Change flow
flowchart TD
A["[EXISTING] Stage-0 raw codec rows"]:::existing --> B["[CHANGED] talker2codec_raw_async_chunk threshold"]:::changed
C["[EXISTING] parse_chunk_ramp / ramp_chunk_size"]:::existing --> B
D["[CHANGED] moss_tts_local.yaml codec_chunk_ramp example"]:::changed --> B
B --> E["[CHANGED] MossTTSCodecDecoder graph T set"]:::changed
E --> F["[EXISTING] Streaming NPUGraph warmup"]:::existing
G["[NEW] test_moss_tts_chunk_ramp.py"]:::new --> B
classDef existing fill:#e5e7eb,stroke:#6b7280,color:#111827
classDef changed fill:#fef3c7,stroke:#d97706,color:#451a03,stroke-width:2px
classDef new fill:#dcfce7,stroke:#16a34a,color:#052e16,stroke-width:2px
classDef removed fill:#fee2e2,stroke:#dc2626,color:#450a0a,stroke-width:2px
Findings
- [P1] Codec ramp parser crashes on null/invalid values the processor treats as off —
vllm_omni/model_executor/models/moss_tts/modeling_moss_tts_codec.py:683
_connector_listtreats a presentcodec_chunk_rampkey as an iterable of ints. If deploy YAML setscodec_chunk_ramp:(YAMLnull),name in extra_cfgis true and[int(x) for x in raw]iteratesNoneduringMossTTSCodecDecoder.__init__, so Stage 1 never starts. A malformed string hitsint(...)on line 682 the same way. The processor side already callsparse_chunk_ramp(cfg, steady=chunk_frames), which the existing helper contract treats as ramp-disabled (warn only) forNone/ invalid / one-entry values — so the two consumers can disagree on the same extra map. Derive_streaming_graph_frame_sizesfromparse_chunk_ramp(extra_cfg, steady=self._stream_chunk_frames)(then union with initial/steady) and add a codec-init test for explicitnulland an invalid value. Default absent-key path is unchanged.
Existing thread: #8124 (comment)
Omni ReviewBot: finding feedback[p1] Codec ramp parser crashes on null/invalid values the processor treats as off —
This finding appears in the bot's COMMENT review, but GitHub could not place it as an inline diff comment. If you are the PR author and disagree, react 👎 to this comment. The disagreement will be shown to the maintainer; it does not approve or merge the PR. |
Replace the codec-side _connector_list parser with a module-level _resolve_streaming_graph_frame_sizes() built on parse_chunk_ramp, so the codec warmup set and the stage input processor share one validated codec_chunk_ramp: null / malformed / single-entry values now disable the ramp on both sides (warn only) instead of raising TypeError in MossTTSCodecDecoder.__init__ (Stage 1 would fail to start). Add 5 CPU tests pinning the shared semantics (absent key, explicit YAML null, invalid string / single-entry / non-positive values, valid list and comma-separated string forms). Signed-off-by: Wallbreazzz <110282866+Wallbreazzz@users.noreply.github.com>
|
@linyueqian Thank you for the review — all three points addressed (fixup
|
linyueqian
left a comment
There was a problem hiding this comment.
Thanks, the fix looks good. The codec now derives its warmup sizes through the same parse_chunk_ramp as the processor, so null or invalid ladders disable the ramp on both sides, and the new tests pin that. The revision note answers my other question. A CUDA run would still be nice to have, but it is not a blocker.
One optional follow-up: the codec's codec_chunk_frames default is 0 while the processor's default is 15. A ramp without an explicit codec_chunk_frames then logs a misleading ramp[-1] != 0 warning and skips warming up the 15-frame graph. Aligning the defaults would avoid that.
…tests Index the MOSS codec_chunk_ramp ladder by the connector's segment-local ramp_chunk_count again, as merged in vllm-project#8124. The rebase kept this branch's put_req_chunk index instead, so the ladder never advanced for callers that follow the connector contract and test_moss_tts_chunk_ramp.py failed. The branch's own ramp tests now advance both counters, as the connector does. Make two reference encoder tests independent of the host they run on: - test_processor_prepare_resamples_to_the_tokenizer_rate required bit-exact equality between resampling a mono clip and resampling its two-channel copy. CPU convolution kernels without AVX2 round a one-row batch differently from a two-row one (about 1e-6), so compare with a float32 tolerance. - test_client_requests_in_flight_use_their_own_connections started both requests together and expected one per worker, but a free worker may take both as a single batch, which left the barrier one party short. Send the second request once a worker holds the first. Signed-off-by: Yueqian Lin <linyueqian@outlook.com>
…S-TTS Rebased onto latest main after vllm-project#8124 (static chunk ramp) and vllm-project#8213 (MOSS Local 1.5 MRV2 serving) were merged. The static ramp portion of the original PR is now redundant — vllm-project#8124 already landed it using parse_chunk_ramp / ramp_chunk_size. This rebased commit keeps only the two features not yet on main: 1. Adaptive chunk controller (codec_chunk_adaptive): - moss_tts.py: Add adaptive branch to talker2codec_raw_async_chunk. When codec_chunk_adaptive is set, AdaptiveChunkController + compute_adaptive_emit from chunk_size_utils.py (introduced by Qwen3-TTS vllm-project#6001) replace the static ramp/IC-steady threshold. Chunk 0 uses IC/steady (same as original); chunk 1+ uses the controller's buffer-feedback target_size. Adaptive takes precedence over static ramp when both are configured. The ramp threshold override is moved after the adaptive branch so it only applies when adaptive is off. _adaptive_states cleanup is owned by the adapter (chunk_transfer_adapter already pops it on segment-finish and abort on main). - moss_tts_local_v1.yaml: Document codec_chunk_adaptive (commented). 2. MOSS-TTS seed fallback (tts_local_seed): - tts_adapters/moss_tts.py: In apply_sampling_overrides, propagate the deploy config's default_sampling_params.seed to tts_local_seed (matching Qwen3-TTS pattern). Shallow-copies stage 0 params so shared defaults stay immutable. Tests: - test_moss_tts_async_chunk.py: 7 new adaptive tests (chunk 0 IC threshold, finish flush, empty-finish sentinel, adaptive-over-ramp precedence, independent requests, states lifecycle). Existing static-ramp tests unchanged. - test_serving_speech.py: MOSS seed-fallback test via _prepare_speech_generation (entrypoints-level, not model_executor). All 23 tests pass (16 existing + 7 new). Signed-off-by: jingchengtian <tjc1995@126.com>
Purpose
Moss-TTS streaming decode has a structural chunk-size cliff:
initial_codec_chunk_frames: 1emits a tiny first chunk (80 ms audio), then jumps directly to steady 15-frame chunks (1.2 s). Under C8 concurrency this causes audible underruns right after the first packet: the client's playback buffer never builds up before the pipeline settles into 1.2 s delivery quanta (measured: 0% streaming continuity, ~0.37 s mean underrun per request).This PR ports the static chunk ramp (Phase 1 of the Qwen3-TTS ramp design, #5152) to the Moss-TTS raw async-chunk path: an opt-in
codec_chunk_ramp: [2, 4, 8, 15]ladder replaces the cliff with gradual steps, reusingparse_chunk_ramp/ramp_chunk_sizefromchunk_size_utils.pyunchanged.The adaptive ramp (#6001 Phase 2 controller) is intentionally not part of this PR; it will be proposed separately for Moss-TTS once stage-1 chunk batching interacts cleanly with controller-chosen sizes.
Changes
moss_tts.py(stage input processor): threshold selection becomes 2-way — static ramp ladder (indexed by the connector-maintainedramp_chunk_count) whencodec_chunk_rampis configured, otherwise the original IC/steady logic, byte-identical.modeling_moss_tts_codec.py: add module-level_resolve_streaming_graph_frame_sizes()built on the sameparse_chunk_ramphelper the processor uses (plus a small_connector_extra_cfgaccessor), so both consumers share one validated ladder:codec_chunk_ramp:left null in YAML, a malformed string, or a single-entry list disables the ramp on both sides (warn only) — no divergence, no raise at codec init. Ramp sizes join_streaming_graph_frame_sizesso every ladder(B, T)combination is captured during NPUGraph warmup — no runtime captures, no eager fallback.moss_tts_local.yaml: documented, commented-outcodec_chunk_rampexample.tests/model_executor/stage_input_processors/test_moss_tts_chunk_ramp.py(11 cases: ladder sequence, finished flush, steady fallback past ladder, string-form config, IC/steady backward compat, empty input; codec-side resolver — absent key, explicit YAML null, invalid string / single-entry / non-positive values, valid list and comma-separated string).Opt-in: with
codec_chunk_rampabsent, the processor takes the original code path and the codec warmup set is unchanged ({initial, steady}). Zero impact on existing deployments.Chunk-size effect (steady = 15):
Test Plan
pytest tests/model_executor/stage_input_processors/test_moss_tts_chunk_ramp.py -v— 11 passed (run on the NPU host, python 3.12 / pytest 9.1.1)ruff format --check+ruff checkcleanvllm bench serve). Primary campaign: 6 consecutive rounds on the post-review head (content-identical to this PR, see revision note), round 1 (cold start) discarded, median of rounds 2-6 reported. An earlier 9-round campaign on the pre-review head (identical runtime behavior — the fixup is a parsing-only refactor) showed the same distributions: RTF median 0.26 (0.22-0.30), continuity median 60% (35-100%)(B,T) = {1,2,4,8,16,32,64} x {1,2,4,8,15}= 35 graphs; capture count stayed at exactly 35 through all 9 rounds of the pre-review campaign (zero runtime captures, zero graph-limit warnings, verified from service log); the fixup does not change the warmup set for a valid ladder (sameparse_chunk_rampresult), and the post-review startup log shows the same 35-graph warmupcodec_chunk_rampcommented out, the processor takes the byte-identical original code path (UTTestBackwardCompat) and the codec warmup set stays at the original{1,15}x batch buckets (14 graphs, as measured in ramp-off rounds on 2026-09-23)Test Result
Environment: Ascend 910B2C, CANN 9.1.0, PyTorch 2.10.0, TorchNPU 2.10.0.post4, vLLM v0.28.0, vllm-omni = v0.28.0 + #7008 + #7280
Benchmark: C8 concurrency, voice_clone task, 40 requests per round, identical text/model/hardware across arms; only variable =
codec_chunk_ramp. Baseline measured 2026-09-23 on the same server/benchmark (ramp off; the feature is opt-in and the ramp-off code path is unchanged).Exact revision benchmarked (per review request): the primary campaign ran on a tree content-identical to this head — vLLM-Omni
v0.28.0(eb11446b) +a769d935(#7008) +ae3880f0(#7280) as the git-committed base, plus this PR's patches deployed by an anchor-patch tool and blob-hash verified against head. Our NPU stack (vllm-ascend) is pinned to vLLM 0.28.0, so the main-based head itself was not re-benchmarked. An earlier 9-round campaign ran on the pre-review head; the fixup (2a4826d) is a parsing-only refactor (identical behavior for a valid ladder — 11 CPU UTs; ramp-off path untouched), and both campaigns agree within round variance.Key findings:
(B,T)graphs are captured during warmup; the capture count stayed at exactly 35 across all rounds of the pre-review campaign (verified from service log), and the fixup does not change the warmup set for a valid ladder (sameparse_chunk_rampresult)Related