[Model] Accelerate Breeze-TTS-2 non-streaming inference (#7752)

This commit is contained in:
Yueqian Lin authored and GitHub committed 2026-09-22 17:10:15 -07:00
1 parent d2aa07159c
commit 3f473a358b
84 files changed
+8162 -5593

No files matched your search

+20
View File
@@ -80,6 +80,26 @@ steps:
- group: ":card_index_dividers: E2E Test"
depends_on: upload-merge-pipeline
steps:
- label: "TTS · Breeze-TTS-2 Voice Design"
timeout_in_minutes: 20
source_file_dependencies:
- tests/e2e/online_serving/test_breeze_tts_2.py
- tests/helpers/client.py
- tests/helpers/media.py
- vllm_omni/model_executor/models/breeze_tts_2/
- vllm_omni/model_executor/models/qwen3_tts/
- vllm_omni/model_executor/stage_input_processors/breeze_tts_2.py
- vllm_omni/entrypoints/openai/tts_adapters/breeze_tts_2.py
- vllm_omni/deploy/breeze_tts_2.yaml
- vllm_omni/deploy/breeze_tts_2_throughput.yaml
- vllm_omni/deploy/breeze_tts.yaml
- vllm_omni/deploy/breeze_tts_throughput.yaml
- vllm_omni/config/pipeline_registry.py
- vllm_omni/engine/arg_utils.py
commands:
- pytest -s -v tests/e2e/online_serving/test_breeze_tts_2.py -m 'advanced_model and tts' --run-level=advanced_model
mirror_hardwares: l4_1
- label: "TTS · Qwen3-TTS CustomVoice Test"
source_file_dependencies: tts_qwen3_tts_function
commands:
+19
View File
@@ -198,6 +198,25 @@ steps:
"
mirror_hardwares: l4_1
- label: "TTS · Breeze-TTS-2 Voice Design"
timeout_in_minutes: 20
source_file_dependencies:
- tests/e2e/online_serving/test_breeze_tts_2.py
- tests/helpers/client.py
- vllm_omni/model_executor/models/breeze_tts_2/
- vllm_omni/model_executor/models/qwen3_tts/
- vllm_omni/model_executor/stage_input_processors/breeze_tts_2.py
- vllm_omni/entrypoints/openai/tts_adapters/breeze_tts_2.py
- vllm_omni/deploy/breeze_tts_2.yaml
- vllm_omni/deploy/breeze_tts_2_throughput.yaml
- vllm_omni/deploy/breeze_tts.yaml
- vllm_omni/deploy/breeze_tts_throughput.yaml
- vllm_omni/config/pipeline_registry.py
- vllm_omni/engine/arg_utils.py
commands:
- pytest -s -v tests/e2e/online_serving/test_breeze_tts_2.py -m 'core_model and tts' --run-level=core_model
mirror_hardwares: l4_1
- label: "Diffusion · Wan22 Test"
source_file_dependencies: diffusion_wan22_function
commands:
@@ -0,0 +1,757 @@
{
"measurement_scope": "Historical measurements for PR7752, not a main-branch comparison or a measurement of subsequent review fixes.",
"environment": {
"gpu": "NVIDIA H100 80 GB",
"gpu_count": 1,
"python": "3.12",
"pytorch": "2.13.0+cu132",
"vllm": "0.29.0",
"transformers": "5.14.1",
"nsight_systems": "2026.5.1"
},
"model": {
"id": "BreezeBlue/Breeze-TTS-2",
"revision": "3e28c5151381a722f1d8661b4118c298caa77aa4"
},
"workload": {
"texts": [
"Hello, this is Breeze TTS 2 running on vLLM Omni.",
"你好,今天天气很好,我们一起出去走走吧。"
],
"alternating_texts": true,
"seed": 42,
"guidance_scale": 1.0,
"repetition_penalty": 1.1,
"top_p": 1.0,
"sampled": {
"temperature": 0.9,
"top_k": 50
},
"greedy": {
"temperature": 0.0,
"top_k": 0
},
"stream": false,
"response_format": "pcm",
"sample_rate": 24000,
"channels": 1,
"pcm_bits": 16
},
"cumulative_diagnostic": {
"baseline_sha": "5bda85c371e8c0c7728c2f7f5865d3ddd79b9d99",
"combined_sha": "37a04897cf8566c63e82eb13c27c064b9bcd4222",
"intermediate_variants": "Ordered patch subsets of the combined commit, not independent commits.",
"patch_sha256": {
"batched_eos": "2f107ea8e890e03cc138ef2f69e693e3474d3aaf0eb4051c00886cbaa9785b3d",
"greedy_graph": "4e783313260ddc2e36c8a74ba9d95dc5d78338443612ebf89854e31b8be6933b",
"batched_embedding": "90557e4ec73fdedcff51fd717d5e8c6477e71087fd97ac52534542234f5d1d3a"
},
"sequence_slots": 8,
"kv_gib": 2,
"codec_chunk_frames": 16,
"max_new_tokens": 48,
"temperature": 0.0,
"cohorts_per_variant": 1,
"nsight_injection_present": true,
"collection_enabled": false,
"warmup": "Two eight-request full-response warmups, then four full-response 64-request cohorts, each preceded by one concurrency-sized warmup.",
"variants": [
{
"variant": "baseline",
"makespan_s": 0.9672094583511353,
"requests": 8,
"concurrency": 8,
"audio_s": 30.72,
"failures": 0
},
{
"variant": "plus_batched_eos",
"makespan_s": 0.9280132218264043,
"requests": 8,
"concurrency": 8,
"audio_s": 30.72,
"failures": 0
},
{
"variant": "plus_greedy_graph",
"makespan_s": 0.8857334200292826,
"requests": 8,
"concurrency": 8,
"audio_s": 30.72,
"failures": 0
},
{
"variant": "plus_batched_embedding",
"makespan_s": 0.8435833756811917,
"requests": 8,
"concurrency": 8,
"audio_s": 30.72,
"failures": 0
}
],
"limits": [
"Single cumulative diagnostic samples; no repeat spread.",
"Nsight injection remains when collection is disabled.",
"The final variant used graph tracing configuration; prior variants used node tracing configuration.",
"Do not interpret these timings as isolated uninstrumented per-optimization gains."
]
},
"mechanical_attribution": {
"batched_eos": {
"stage0_stream_synchronization_calls": [
773,
438
],
"device_to_host_copies": [
841,
506
],
"capture_mode": "node"
},
"greedy_graph": {
"radix_sort_kernels": [
735,
0
],
"capture_mode": "node"
},
"combined_graph_capture": {
"captured_wall_ms": [
955.4114495404065,
855.6996420957148
],
"gpu_envelope_ms": [
926.633897,
826.396835
],
"activity_upper_ms": [
651.130806,
628.864685
],
"idle_lower_ms": [
275.50309100000004,
197.53215
],
"stage0_explicit_kernel_launch_count": [
7087,
6198
],
"stage0_graph_launch_count": [
156,
156
]
},
"limits": [
"Host API durations overlap GPU activity and are not additional idle time.",
"Graph envelopes hide internal gaps: activity is an upper bound and idle is a lower bound.",
"Stage 1 has eight versus nine graph launches across the graph-mode traces.",
"No separate final-capacity stage breakdown was measured."
]
},
"combined_uninstrumented_ab": {
"baseline_sha": "5bda85c371e8c0c7728c2f7f5865d3ddd79b9d99",
"head_sha": "37a04897cf8566c63e82eb13c27c064b9bcd4222",
"sequence_slots": 8,
"kv_gib": 2,
"codec_chunk_frames": 16,
"requests_per_group": 64,
"max_new_tokens": 384,
"baseline_rounds": 2,
"head_rounds": 1,
"profiler_configured": false,
"nsight_injection_present": false,
"warmup": "One excluded 64-request full-response wave for each temperature/concurrency family, plus an excluded 2*C-request full-response cohort before every group.",
"rows": [
{
"build": "plain-baseline",
"phase": "round-0",
"temperature": 0.0,
"concurrency": 8,
"requests": 64,
"makespan_s": 10.77213971177116,
"req_per_s": 5.941252314994074,
"aggregate_rtf": 0.030157166046391827,
"audio_s": 357.2,
"caps": 0,
"failures": 0,
"endpoint_trimmed_audio_s": 305.88,
"endpoint_trimmed_aggregate_rtf": 0.03521688149526337,
"quiet_tail_at_least_3s": 7,
"max_quiet_tail_s": 3.24
},
{
"build": "plain-baseline",
"phase": "round-0",
"temperature": 0.0,
"concurrency": 16,
"requests": 64,
"makespan_s": 11.076066234149039,
"req_per_s": 5.778224745774739,
"aggregate_rtf": 0.029092420240988227,
"audio_s": 380.72,
"caps": 2,
"failures": 0,
"endpoint_trimmed_audio_s": 315.48,
"endpoint_trimmed_aggregate_rtf": 0.035108616185333584,
"quiet_tail_at_least_3s": 6,
"max_quiet_tail_s": 26.04
},
{
"build": "plain-baseline",
"phase": "round-0",
"temperature": 0.9,
"concurrency": 8,
"requests": 64,
"makespan_s": 8.603743681218475,
"req_per_s": 7.438622345259851,
"aggregate_rtf": 0.03389435739528236,
"audio_s": 253.84,
"caps": 0,
"failures": 0,
"endpoint_trimmed_audio_s": 239.02,
"endpoint_trimmed_aggregate_rtf": 0.035995915325991445,
"quiet_tail_at_least_3s": 0,
"max_quiet_tail_s": 1.3
},
{
"build": "plain-baseline",
"phase": "round-0",
"temperature": 0.9,
"concurrency": 16,
"requests": 64,
"makespan_s": 8.66502623911947,
"req_per_s": 7.386013409984042,
"aggregate_rtf": 0.033143460216950235,
"audio_s": 261.44,
"caps": 0,
"failures": 0,
"endpoint_trimmed_audio_s": 242.14,
"endpoint_trimmed_aggregate_rtf": 0.03578519137325296,
"quiet_tail_at_least_3s": 0,
"max_quiet_tail_s": 1.3
},
{
"build": "plain-baseline",
"phase": "round-1",
"temperature": 0.0,
"concurrency": 8,
"requests": 64,
"makespan_s": 13.904579282738268,
"req_per_s": 4.602800178172402,
"aggregate_rtf": 0.0392784725501081,
"audio_s": 354.0,
"caps": 1,
"failures": 0,
"endpoint_trimmed_audio_s": 285.36,
"endpoint_trimmed_aggregate_rtf": 0.048726448285457906,
"quiet_tail_at_least_3s": 8,
"max_quiet_tail_s": 25.38
},
{
"build": "plain-baseline",
"phase": "round-1",
"temperature": 0.0,
"concurrency": 16,
"requests": 64,
"makespan_s": 10.060411099344492,
"req_per_s": 6.361569061941222,
"aggregate_rtf": 0.02935460754944121,
"audio_s": 342.72,
"caps": 0,
"failures": 0,
"endpoint_trimmed_audio_s": 289.04,
"endpoint_trimmed_aggregate_rtf": 0.03480629359031446,
"quiet_tail_at_least_3s": 10,
"max_quiet_tail_s": 3.16
},
{
"build": "plain-baseline",
"phase": "round-1",
"temperature": 0.9,
"concurrency": 8,
"requests": 64,
"makespan_s": 8.535738762002438,
"req_per_s": 7.4978864494894575,
"aggregate_rtf": 0.033363581777683074,
"audio_s": 255.84,
"caps": 0,
"failures": 0,
"endpoint_trimmed_audio_s": 237.9,
"endpoint_trimmed_aggregate_rtf": 0.03587952401009852,
"quiet_tail_at_least_3s": 0,
"max_quiet_tail_s": 2.12
},
{
"build": "plain-baseline",
"phase": "round-1",
"temperature": 0.9,
"concurrency": 16,
"requests": 64,
"makespan_s": 8.584469985216856,
"req_per_s": 7.4553234049642105,
"aggregate_rtf": 0.033355882752629996,
"audio_s": 257.36,
"caps": 0,
"failures": 0,
"endpoint_trimmed_audio_s": 242.57999999999998,
"endpoint_trimmed_aggregate_rtf": 0.035388201769382704,
"quiet_tail_at_least_3s": 0,
"max_quiet_tail_s": 1.94
},
{
"build": "plain-batched",
"phase": "round-0",
"temperature": 0.0,
"concurrency": 8,
"requests": 64,
"makespan_s": 12.983129982370883,
"req_per_s": 4.929473870083891,
"aggregate_rtf": 0.03320153943936907,
"audio_s": 391.04,
"caps": 2,
"failures": 0,
"endpoint_trimmed_audio_s": 312.72,
"endpoint_trimmed_aggregate_rtf": 0.041516788124747,
"quiet_tail_at_least_3s": 11,
"max_quiet_tail_s": 28.4
},
{
"build": "plain-batched",
"phase": "round-0",
"temperature": 0.0,
"concurrency": 16,
"requests": 64,
"makespan_s": 9.772029620129615,
"req_per_s": 6.549304749155183,
"aggregate_rtf": 0.027698496655696188,
"audio_s": 352.8,
"caps": 1,
"failures": 0,
"endpoint_trimmed_audio_s": 286.26,
"endpoint_trimmed_aggregate_rtf": 0.034136902187275955,
"quiet_tail_at_least_3s": 6,
"max_quiet_tail_s": 26.04
},
{
"build": "plain-batched",
"phase": "round-0",
"temperature": 0.9,
"concurrency": 8,
"requests": 64,
"makespan_s": 8.171920725144446,
"req_per_s": 7.831696140061215,
"aggregate_rtf": 0.03169376638669115,
"audio_s": 257.84,
"caps": 0,
"failures": 0,
"endpoint_trimmed_audio_s": 241.72,
"endpoint_trimmed_aggregate_rtf": 0.03380738344011437,
"quiet_tail_at_least_3s": 0,
"max_quiet_tail_s": 1.22
},
{
"build": "plain-batched",
"phase": "round-0",
"temperature": 0.9,
"concurrency": 16,
"requests": 64,
"makespan_s": 8.02551719499752,
"req_per_s": 7.974563937124526,
"aggregate_rtf": 0.030886380830501534,
"audio_s": 259.84000000000003,
"caps": 0,
"failures": 0,
"endpoint_trimmed_audio_s": 243.0,
"endpoint_trimmed_aggregate_rtf": 0.03302681973250008,
"quiet_tail_at_least_3s": 0,
"max_quiet_tail_s": 1.78
}
],
"limits": [
"Ranges compare the head sample with two baseline samples; they are not confidence intervals.",
"Full-response greedy output lengths and frame-limit incidence vary, so no general greedy throughput gain is claimed.",
"No transcription was run on these historical A/B outputs."
]
},
"capacity": {
"runtime_sha": "7bc0b3e3a2e41ef44e8efb30fd927e93bf6a48a7",
"main_base_sha": "6bda549ac23a667efbe44a54cf3f6d57e6e24da2",
"sequence_slots": [
8,
16,
32
],
"kv_gib": [
2,
4,
8
],
"codec_graph_buckets": [
[
1,
2,
4,
8
],
[
1,
2,
4,
8,
16
],
[
1,
2,
4,
8,
16,
32
]
],
"backbone_capture_sizes": [
1,
2,
4,
8,
32,
64,
128,
256
],
"codec_chunk_frames": 16,
"requests_per_group": 128,
"cohorts_per_point": 1,
"excluded_warmups_per_configuration": {
"cohorts": 4,
"requests_per_cohort": 64,
"covers": "Both client concurrency levels and both sampling settings"
},
"profiler_configured": false,
"nsight_injection_present": false,
"rows": [
{
"slots": 8,
"concurrency": 8,
"observed_peak_client_requests": 8,
"temperature": 0.0,
"max_new_tokens": 384,
"requests": 128,
"failures": 0,
"req_per_s": 6.7812081269297915,
"makespan_s": 18.87569259107113,
"aggregate_rtf": 0.02841355459879445,
"audio_s": 664.32,
"near_cap_count": 0,
"exact_cap_count": 0,
"fixed48_verified": null
},
{
"slots": 8,
"concurrency": 16,
"observed_peak_client_requests": 16,
"temperature": 0.0,
"max_new_tokens": 384,
"requests": 128,
"failures": 0,
"req_per_s": 5.8798058352164615,
"makespan_s": 21.76942633604631,
"aggregate_rtf": 0.029975526459636356,
"audio_s": 726.24,
"near_cap_count": 2,
"exact_cap_count": 2,
"fixed48_verified": null
},
{
"slots": 8,
"concurrency": 8,
"observed_peak_client_requests": 8,
"temperature": 0.9,
"max_new_tokens": 384,
"requests": 128,
"failures": 0,
"req_per_s": 7.72991106982811,
"makespan_s": 16.559052082709968,
"aggregate_rtf": 0.0320365502296664,
"audio_s": 516.88,
"near_cap_count": 0,
"exact_cap_count": 0,
"fixed48_verified": null
},
{
"slots": 8,
"concurrency": 16,
"observed_peak_client_requests": 16,
"temperature": 0.9,
"max_new_tokens": 384,
"requests": 128,
"failures": 0,
"req_per_s": 7.945057322591292,
"makespan_s": 16.110645248088986,
"aggregate_rtf": 0.030958196095482296,
"audio_s": 520.4,
"near_cap_count": 0,
"exact_cap_count": 0,
"fixed48_verified": null
},
{
"slots": 8,
"concurrency": 8,
"observed_peak_client_requests": 8,
"temperature": 0.0,
"max_new_tokens": 48,
"requests": 128,
"failures": 0,
"req_per_s": 9.850579526557848,
"makespan_s": 12.994159344118088,
"aggregate_rtf": 0.026436684863521503,
"audio_s": 491.52,
"near_cap_count": 128,
"exact_cap_count": 128,
"fixed48_verified": true
},
{
"slots": 8,
"concurrency": 16,
"observed_peak_client_requests": 16,
"temperature": 0.0,
"max_new_tokens": 48,
"requests": 128,
"failures": 0,
"req_per_s": 10.665146583658053,
"makespan_s": 12.00171033712104,
"aggregate_rtf": 0.02441754218978076,
"audio_s": 491.52,
"near_cap_count": 128,
"exact_cap_count": 128,
"fixed48_verified": true
},
{
"slots": 16,
"concurrency": 16,
"observed_peak_client_requests": 16,
"temperature": 0.0,
"max_new_tokens": 384,
"requests": 128,
"failures": 0,
"req_per_s": 7.754874623816668,
"makespan_s": 16.505747186020017,
"aggregate_rtf": 0.023979758231665527,
"audio_s": 688.32,
"near_cap_count": 1,
"exact_cap_count": 1,
"fixed48_verified": null
},
{
"slots": 16,
"concurrency": 32,
"observed_peak_client_requests": 32,
"temperature": 0.0,
"max_new_tokens": 384,
"requests": 128,
"failures": 0,
"req_per_s": 10.104109318018903,
"makespan_s": 12.668113138061017,
"aggregate_rtf": 0.018577125085143444,
"audio_s": 681.92,
"near_cap_count": 1,
"exact_cap_count": 1,
"fixed48_verified": null
},
{
"slots": 16,
"concurrency": 16,
"observed_peak_client_requests": 16,
"temperature": 0.9,
"max_new_tokens": 384,
"requests": 128,
"failures": 0,
"req_per_s": 10.920674745703012,
"makespan_s": 11.72088748915121,
"aggregate_rtf": 0.021831484669108944,
"audio_s": 536.88,
"near_cap_count": 0,
"exact_cap_count": 0,
"fixed48_verified": null
},
{
"slots": 16,
"concurrency": 32,
"observed_peak_client_requests": 32,
"temperature": 0.9,
"max_new_tokens": 384,
"requests": 128,
"failures": 0,
"req_per_s": 11.210975235811437,
"makespan_s": 11.417383172083646,
"aggregate_rtf": 0.02120613516360261,
"audio_s": 538.4,
"near_cap_count": 0,
"exact_cap_count": 0,
"fixed48_verified": null
},
{
"slots": 16,
"concurrency": 16,
"observed_peak_client_requests": 16,
"temperature": 0.0,
"max_new_tokens": 48,
"requests": 128,
"failures": 0,
"req_per_s": 15.14487085057202,
"makespan_s": 8.451706274878234,
"aggregate_rtf": 0.017195040435543284,
"audio_s": 491.52,
"near_cap_count": 128,
"exact_cap_count": 128,
"fixed48_verified": true
},
{
"slots": 16,
"concurrency": 32,
"observed_peak_client_requests": 32,
"temperature": 0.0,
"max_new_tokens": 48,
"requests": 128,
"failures": 0,
"req_per_s": 16.563937910502617,
"makespan_s": 7.727630995213985,
"aggregate_rtf": 0.015721905507840955,
"audio_s": 491.52,
"near_cap_count": 128,
"exact_cap_count": 128,
"fixed48_verified": true
},
{
"slots": 32,
"concurrency": 32,
"observed_peak_client_requests": 32,
"temperature": 0.0,
"max_new_tokens": 384,
"requests": 128,
"failures": 0,
"req_per_s": 15.246181432777554,
"makespan_s": 8.395544849336147,
"aggregate_rtf": 0.012390119317202106,
"audio_s": 677.6,
"near_cap_count": 0,
"exact_cap_count": 0,
"fixed48_verified": null
},
{
"slots": 32,
"concurrency": 64,
"observed_peak_client_requests": 64,
"temperature": 0.0,
"max_new_tokens": 384,
"requests": 128,
"failures": 0,
"req_per_s": 11.166074835908939,
"makespan_s": 11.463294119108468,
"aggregate_rtf": 0.015826284127331107,
"audio_s": 724.32,
"near_cap_count": 1,
"exact_cap_count": 1,
"fixed48_verified": null
},
{
"slots": 32,
"concurrency": 32,
"observed_peak_client_requests": 32,
"temperature": 0.9,
"max_new_tokens": 384,
"requests": 128,
"failures": 0,
"req_per_s": 17.17219888865503,
"makespan_s": 7.453908543102443,
"aggregate_rtf": 0.014336645143680651,
"audio_s": 519.92,
"near_cap_count": 0,
"exact_cap_count": 0,
"fixed48_verified": null
},
{
"slots": 32,
"concurrency": 64,
"observed_peak_client_requests": 64,
"temperature": 0.9,
"max_new_tokens": 384,
"requests": 128,
"failures": 0,
"req_per_s": 17.514343665613783,
"makespan_s": 7.3082955572754145,
"aggregate_rtf": 0.013677750331777613,
"audio_s": 534.32,
"near_cap_count": 0,
"exact_cap_count": 0,
"fixed48_verified": null
},
{
"slots": 32,
"concurrency": 32,
"observed_peak_client_requests": 32,
"temperature": 0.0,
"max_new_tokens": 48,
"requests": 128,
"failures": 0,
"req_per_s": 25.82908453652114,
"makespan_s": 4.955653763841838,
"aggregate_rtf": 0.010082303393232905,
"audio_s": 491.52,
"near_cap_count": 128,
"exact_cap_count": 128,
"fixed48_verified": true
},
{
"slots": 32,
"concurrency": 64,
"observed_peak_client_requests": 64,
"temperature": 0.0,
"max_new_tokens": 48,
"requests": 128,
"failures": 0,
"req_per_s": 28.81314277607874,
"makespan_s": 4.442417163401842,
"aggregate_rtf": 0.00903812085653044,
"audio_s": 491.52,
"near_cap_count": 128,
"exact_cap_count": 128,
"fixed48_verified": true
}
],
"limits": [
"This scales slots, KV reservation and codec graph buckets together.",
"One cohort per point; no repeated capacity samples or confidence intervals.",
"The 48-frame cohorts deliberately truncate speech and are excluded from full-utterance quality claims.",
"Peak device memory was not measured.",
"No capacity claim extends to cloning, CFG guidance above one, long prompts or other GPUs."
]
},
"quality": {
"full_response_pcm_checked": 1536,
"sampled_full_responses": 768,
"sampled_frame_limit_hits": 0,
"sampled_quiet_tails_at_least_three_seconds": 0,
"greedy_frame_limit_hits": 5,
"asr_selected_unique_clips": 47,
"asr_selected_sampled_clips": 24,
"sampled_asr": "All selected sampled transcripts matched after accounting for TTS 2 versus TTS2 spacing.",
"endpoint_method": "Trim leading/trailing quiet using 20 ms RMS windows at or below -40 dBFS; keep internal pauses and original makespans.",
"limits": [
"ASR selects median-duration and longest English/Chinese clips per group, deduplicated by WAV hash.",
"Selected ASR and endpoint measurements do not establish perceptual quality parity.",
"Capped greedy clips have extra ASR words with unreliable tail alignment.",
"No listening or MOS assessment was performed."
]
},
"metrics": {
"requests_per_second": "Completed requests divided by cohort wall time",
"aggregate_rtf": "Cohort wall time divided by total returned audio duration",
"timing_exclusions": [
"PCM validation",
"hashing",
"WAV file writes",
"post-timing audio analysis"
]
}
}
File diff suppressed because it is too large. Load diff
+11
View File
@@ -7,6 +7,17 @@
# is tracked here.
models:
BreezeBlue/Breeze-TTS-2:
supported_tasks: [default_voice, voice_design, voice_clone]
backend: openai-audio-speech
endpoint: /v1/audio/speech
task_extra_body:
default_voice: {}
voice_design:
task_type: VoiceDesign
voice_clone:
task_type: Base
IndexTeam/IndexTTS-2.5:
supported_tasks: [voice_clone]
backend: openai-audio-speech
@@ -42,7 +42,8 @@ Scheduler / current input batch
prefill / other spans single-token decode
preprocess preprocess or preprocess_decode_batch
| |
prompt embedding runner-managed talker_mtp
prompt embedding decode embeddings
| talker_mtp (if present)
\ /
model forward
```
@@ -96,9 +97,24 @@ computed per-row fields are not promised at that earlier boundary.
### Routing guarantee
With `has_preprocess=True`, the batched decode hook is eligible only when it is
callable, the runner has `talker_mtp`, the scheduled span is one token, and
callable, the scheduled span is one token, and
`_omni_is_prefill` is false. Otherwise the normal `preprocess` hook runs.
The hook's return format depends on whether the runner manages MTP:
| Runner capability | `preprocess_decode_batch` return tuple |
| --- | --- |
| Without `talker_mtp` | `(input_ids, inputs_embeds, updates)` |
| With `talker_mtp` | `(input_ids, inputs_embeds, hidden_states, text_step, updates)` |
Each tensor's batch dimension and each entry in the `updates` list correspond
to one physical request row, including separate CFG branches. Non-MTP batches
contain contiguous single-token decode spans; the runner flushes a batch before
processing any other span. It copies embeddings into its existing input buffer
and applies each update dictionary to the corresponding request. Returning the
original `input_ids` object lets it skip an unchanged token-ID copy. The existing
five-result MTP format remains supported.
Runner-managed MTP is likewise restricted to single-token decode rows. A prefill
row, including a one-token tail, is never added to that MTP batch: the runner
does not consume its `mtp_inputs`, generate codes for it, or overwrite its
@@ -130,6 +146,8 @@ formalizes that existing behavior. Prefer upgrading the matching vLLM and
vLLM-Omni release pair and consuming the explicit phase keys. Do not silently
fall back to `input_ids.numel() > 1` when the model requires chunking correctness.
For a custom or backported runtime, verify both metadata and MTP guards.
Older runners can restrict `preprocess_decode_batch` to MTP models; a non-MTP
model must retain its scalar `preprocess` implementation for those runtimes.
## Correctness and testing plan
+1 -1
View File
@@ -72,7 +72,7 @@ th {
| `AuKForConditionalGeneration` | AuK / AuK-Flash (instruction-driven speech generation and editing) | `tencent/AuK`, `tencent/AuK-Flash` (with `Qwen/Qwen2.5-Omni-3B`; assemble with `tools/prepare_auk_checkpoint.py`) | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/Tencent/AuK-H100.md) |
| `OmniVoiceModel` | OmniVoice | `k2-fsa/OmniVoice` | ✅︎ | | | ✅︎ | — |
| `VoxCPM2TalkerForConditionalGeneration` | VoxCPM2 | `openbmb/VoxCPM2` | ✅︎ | | | ✅︎ | — |
| `BreezeForConditionalGeneration` | Breeze-TTS-2 (TTS, voice clone, voice direction) | `BreezeBlue/Breeze-TTS-2` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/BreezeBlue/Breeze-TTS-2.md) |
| `BreezeForConditionalGeneration` | Breeze-TTS-2 (voice design, voice clone, voice direction, CFG) | `BreezeBlue/Breeze-TTS-2` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/BreezeBlue/Breeze-TTS-2.md) |
| `MammothModa2ForConditionalGeneration` | MammothModa2-Preview | `bytedance-research/MammothModa2-Preview` | ✅︎ | ✅︎ | | | — |
| `MammothModa2ForConditionalGeneration` | MammothModa2-Dev (AR-only image understanding) | `bytedance-research/MammothModa2-Dev` | ✅︎ | | | | — |
| `Flux2KleinPipeline` | FLUX.2-klein | `black-forest-labs/FLUX.2-klein-4B`, `black-forest-labs/FLUX.2-klein-9B` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

@@ -14,12 +14,10 @@ from pathlib import Path
import soundfile as sf
import torch
from transformers import AutoConfig
from transformers import AutoTokenizer
from vllm_omni import Omni
from vllm_omni.model_executor.models.breeze_tts_2.prompt_builder import (
BreezeTTS2PromptBuilder,
)
from vllm_omni.model_executor.models.breeze_tts_2.prompt import DEFAULT_INSTRUCTION, build_breeze_prompt
from vllm_omni.utils.tracking_parser import TrackingArgumentParser
REPO_ROOT = Path(__file__).resolve().parents[4]
@@ -95,8 +93,7 @@ def resolve_template(args) -> str:
def reference_payload(ref_audio_path: str | None) -> dict:
"""Load the reference clip into the payload keys the prompt builder reads.
``BreezeTTS2PromptBuilder`` consumes ``ref_audio`` (a waveform) plus
``ref_audio_sample_rate``; a bare path field is not read.
The prompt helper consumes a waveform and its sample rate.
"""
if ref_audio_path is None:
return {}
@@ -107,37 +104,18 @@ def reference_payload(ref_audio_path: str | None) -> dict:
def build_prompt(args) -> dict:
config = AutoConfig.from_pretrained(args.model, trust_remote_code=True)
reference_encoder = None
if args.ref_audio is not None:
from vllm_omni.model_executor.models.breeze_tts_2.audio_tokenizer import (
BreezeReferenceAudioTokenizer,
tokenizer = AutoTokenizer.from_pretrained(args.model)
payload = reference_payload(args.ref_audio)
reference = (payload["ref_audio"], payload["ref_audio_sample_rate"]) if payload else None
return build_breeze_prompt(
tokenizer,
args.text,
DEFAULT_INSTRUCTION if args.instruction is None else args.instruction,
speaker=args.voice,
ref_audio=reference,
ref_text=args.ref_text,
)
reference_encoder = BreezeReferenceAudioTokenizer.from_pretrained(
args.model,
num_codebooks=int(getattr(config, "num_codebooks", 16)),
device_map="cpu",
)
builder = BreezeTTS2PromptBuilder.from_pretrained(
args.model,
config,
reference_audio_encoder=reference_encoder,
)
payload = {
"text": args.text,
"speaker": args.voice,
**reference_payload(args.ref_audio),
}
if args.instruction:
payload["instruction"] = args.instruction
if args.ref_text:
payload["ref_text"] = args.ref_text
prompt = builder.build(payload, template=resolve_template(args))
if args.max_new_tokens is not None:
prompt["additional_information"]["breeze_max_new_frames"] = int(args.max_new_tokens)
return prompt
def extract_audio(multimodal_output: dict) -> torch.Tensor:
"""Extract the final complete audio tensor from multimodal output.
@@ -180,6 +158,11 @@ def main():
print(f"Prompt len : {len(prompt['prompt_token_ids'])} tokens")
t_start = time.perf_counter()
if args.max_new_tokens is not None:
sampling = [params.clone() for params in engine.resolve_sampling_params_list(None)]
sampling[0].max_tokens = args.max_new_tokens
outputs = engine.generate([prompt], sampling_params_list=sampling)
else:
outputs = engine.generate([prompt])
elapsed = time.perf_counter() - t_start
@@ -1061,7 +1061,7 @@ vllm serve BreezeBlue/Breeze-TTS-2 --omni --port 8091
./breeze_tts_2/run_server.sh
```
The deploy config at `vllm_omni/deploy/breeze_tts_2.yaml` auto-loads (async-chunk streaming, 8-frame inter-stage chunks).
The deploy config at `vllm_omni/deploy/breeze_tts_2.yaml` auto-loads (async-chunk streaming with 1, 2, 4, then 5-frame inter-stage chunks).
### Sending requests
+270 -173
View File
@@ -1,210 +1,307 @@
# Breeze-TTS-2
> Text-to-speech serving (plain / voice design / voice clone / voice direction)
[Breeze-TTS-2](https://huggingface.co/BreezeBlue/Breeze-TTS-2) generates 24 kHz
English and Chinese speech. The integration supports Voice Design from text
instructions, Voice Clone from a reference recording and transcript, and
Voice Direction combining reference conditioning with instructions.
## Summary
## Installation and serving
- Vendor: BreezeBlue
- Model: `BreezeBlue/Breeze-TTS-2` (`BreezeForConditionalGeneration`)
- Task: Text-to-speech with speaker tags, natural-language style instructions, single-reference voice cloning, and reference+instruction "voice direction"
- Mode: Online serving with the OpenAI-compatible `/v1/audio/speech` API (streaming and non-streaming); offline `Omni` example
- Hardware: 1x NVIDIA L20 48GB (CUDA)
- Maintainer: Community
## When to use this recipe
Use this recipe when you want a known-good starting point for serving
Breeze-TTS-2 with vLLM-Omni. Breeze-TTS-2 is a two-stage AR TTS pipeline:
| Stage | Components | Output |
| ----- | ---------- | ------ |
| 0 (talker, `LLM_AR`) | T5Gemma2 text encoder + Qwen3 backbone + 2052-way codebook-0 head + 12-layer depth decoder | 16-codebook codec frames |
| 1 (codec, `LLM_GENERATION`) | Bundled Qwen3-TTS audio tokenizer decoder (Mimi fallback) | 24 kHz mono waveform |
Four prompt modes are selected automatically from the request fields:
| Mode | Trigger | Fields |
| ---- | ------- | ------ |
| `tts_plain` | only `input` (default `voice=S0`) | `input`, `voice` |
| `tts_instruction` (voice design) | `input` + `instructions` | style/emotion/pace directives, not spoken |
| `ref_clone_tata` (clone) | `ref_audio` + `ref_text` + `input` | reference clip and its exact transcript |
| `ref_edit_tata` (voice direction) | reference trio + `instructions` | keep the reference timbre, direct the delivery |
Current scope: greedy sampling with `cfg_scale=1.0` and a default
`repetition_penalty=1.1` on generated codebook-0 tokens; CFG ≠ 1.0 and
`negative_prompt` are rejected with explicit client errors until companion
request support lands.
The [official inference example](https://github.com/breezeblue-ai/breeze-tts/blob/main/infer.py)
also uses repetition penalty `1.1`, but its
[runtime defaults](https://github.com/breezeblue-ai/breeze-tts/blob/main/breeze_infer/runtime.py)
sample both the backbone and depth decoder with temperature `0.9` and top-k
`50`. The [official voice-design examples](https://huggingface.co/BreezeBlue/Breeze-TTS-2#-voice-design)
use CFG `4`. Those sampling and guidance settings are outside the current
port's scope, so the examples below do not establish parity with the official
voice-design outputs.
## Supported model contract
| Item | Value |
| ---- | ----- |
| Tasks | Text-to-speech through `/v1/audio/speech` (online) and the offline `Omni` example |
| Modes | plain, voice design (`instructions`), voice clone (`ref_audio` + `ref_text`), voice direction (reference + `instructions`) |
| Languages | English and Chinese (model card) |
| Reference audio | exactly one clip per request with its exact transcript; encoded by the bundled Qwen3-TTS tokenizer to 16 codebooks at 12.5 Hz |
| Output | 24 kHz mono; `wav` or `pcm`; raw audio streaming (`stream_format="audio"`) or SSE `speech.audio.delta` (`stream_format="sse"`) |
| Length | prompt bounded by stage-0 `max_model_len=4096`; synthesis bounded by `max_new_tokens` codec frames (default 2048, about 164 s) |
| Sampling | greedy (`temperature=0`) with `cfg_scale=1.0` and `repetition_penalty=1.1`; other guidance values and `negative_prompt` are rejected |
| Deployment profile | `vllm_omni/deploy/breeze_tts_2.yaml`: two stages on one GPU, async-chunk streaming with 8-frame codec chunks, stage 0 `gpu_memory_utilization=0.80`, stage 1 `0.15` |
## References
- Issue: [#6656 [New Model]: Breeze TTS 2](https://github.com/vllm-project/vllm-omni/issues/6656)
- Related examples under `examples/`:
[`examples/online_serving/text_to_speech/breeze_tts_2/`](../../examples/online_serving/text_to_speech/breeze_tts_2/),
[`examples/offline_inference/text_to_speech/breeze_tts_2/`](../../examples/offline_inference/text_to_speech/breeze_tts_2/)
## Hardware
- Accelerator model and per-device memory: 1x NVIDIA L20 48GB
- Number of devices: 1 (both stages are placed on device 0)
- Device interconnect: not applicable (single device)
- Host memory: no special requirement; per-request codec state is small
- Qualification scope: functional serving of all four prompt modes, streaming PCM, and 4 concurrent mixed-mode requests (greedy, `cfg_scale=1.0`). No other accelerator is qualified by this recipe.
## Software environment
- OS: Linux
- Python: 3.12.12
- Driver / runtime: NVIDIA driver 580.82.07, CUDA 13.0 (PyTorch 2.13.0+cu130)
- vLLM version: 0.28.0
- vLLM-Omni version or commit: the commit that introduced this recipe (branch based on `main` @ `67ef0b64`)
- Transformers: 5.14.1
## Command
Start the server from the repository root. The deploy config
(`vllm_omni/deploy/breeze_tts_2.yaml`, async chunk streaming by default)
auto-loads from the checkpoint's `model_type`; pass it explicitly if you
customized it:
Follow the project's source installation guide for vLLM and vLLM-Omni.
The integration loads the released checkpoint directly. The original Breeze
repository and the `qwen-tts` package are not required. The common requirements
include `soxr` to reproduce reference-audio resampling.
```bash
vllm serve BreezeBlue/Breeze-TTS-2 \
--deploy-config vllm_omni/deploy/breeze_tts_2.yaml \
--omni --port 8091
vllm-omni serve BreezeBlue/Breeze-TTS-2 --omni --host 127.0.0.1 --port 8091
```
## Verification
The default configuration uses one CUDA GPU, BF16 generation and codec
computation, and FP32 reference encoding. Decoded waveform tensors are converted
to FP32 before response formatting. It targets GPUs with at least 16 GiB
available for the deployment; other workloads consume additional memory.
Plain synthesis (speaker tag `S0`..`S9`, default `S0`):
On WSL2, set vLLM's supported pinned-memory option before starting the server:
```bash
curl -X POST http://localhost:8091/v1/audio/speech \
-H "Content-Type: application/json" \
export VLLM_WSL2_ENABLE_PIN_MEMORY=1
```
Native Linux enables pinned memory by default. Startup warmup exercises plain
generation, CFG, reference conditioning and two-request batches. New text
lengths or batch sizes can require additional graph
capture or compilation; warm the workload before measuring steady performance.
## Voice Design
```bash
curl --fail http://127.0.0.1:8091/v1/audio/speech \
-H 'Content-Type: application/json' \
-d '{
"model": "BreezeBlue/Breeze-TTS-2",
"input": "Hello, this is Breeze TTS 2 running on vLLM-Omni.",
"voice": "S0",
"input": "你好,欢迎使用语音合成系统。",
"instructions": "年轻女性,声音温暖自然,使用标准普通话。",
"extra_params": {"guidance_scale": 4.0},
"response_format": "wav",
"sample_rate": 24000
}' --output breeze_plain.wav
"seed": 42,
"max_new_tokens": 250
}' --output breeze.wav
```
Voice design (instruction only, no reference audio):
The `voice` field selects a speaker tag, such as `S0` or `[S0]`; omitting it
uses `[S0]`. These are checkpoint prompt tags, not a catalog of named voices.
Omitting `instructions` uses `Speak clearly and naturally.`. An explicitly
empty instruction remains empty. Vocal-event tokens, including `(laughs)`
and `[笑]`, are passed to the checkpoint unchanged.
## Voice Clone and Voice Direction
Add `ref_audio` and its exact `ref_text` transcript. Reference audio accepts the
shared speech API's audio URLs and base64 data URLs, either as a string or
a one-item list. The shared resolver's
format, duration and local-file access restrictions apply.
```json
{
"model": "BreezeBlue/Breeze-TTS-2",
"input": "Please read this new sentence clearly and naturally.",
"ref_audio": "data:audio/wav;base64,<base64-encoded-reference>",
"ref_text": "The words actually spoken in the reference recording.",
"instructions": "Keep the reference voice. Speak gently and calmly.",
"extra_params": {"guidance_scale": 4.0},
"response_format": "wav",
"seed": 42
}
```
Use `guidance_scale: 1.0` with the default instruction for ordinary cloning.
Reference audio plus a delivery instruction and CFG provides Voice Direction.
When explicitly specified, `task_type` is `Base` for reference conditioning
and `VoiceDesign` without reference audio. The legacy `CustomVoice` value
also remains accepted.
Stereo references are averaged to mono and resampled to 24 kHz with soxr HQ.
Reference text is encoded independently from the target text. The reference
codec preserves full causal attention during offline encoding, as used by the
released Breeze runtime; newer Transformers sliding-window defaults would
change long references. Reference convolution uses full FP32 precision.
## Sampling and output
Supported `extra_params`:
| Parameter | Default | Meaning |
| --- | --- | --- |
| `guidance_scale` | 1.0 | Any finite positive value; values other than 1 use two CFG branches |
| `temperature` | 0.9 | Zero selects greedy generation |
| `top_k` | 50 | Zero or -1 disables top-k filtering |
| `top_p` | 1.0 | Nucleus sampling probability in (0, 1] |
| `repetition_penalty` | 1.1 | Positive penalty applied to the first codebook's generated history |
`cfg_scale` remains an alias for `guidance_scale`; `guidance_scale` takes
precedence when both are supplied.
CFG combines conditional and unconditional logits in the Qwen3 output head
and all 15 remaining depth codebooks. Both branches retain reference audio
and reference text; the unconditional branch removes only target instructions.
`max_new_tokens` limits generated audio frames, including EOS. Each frame is
80 ms; the default is 1500 tokens, subject to the remaining 2048-token context.
Both CFG branches stop at the same generation limit despite unequal prompts.
Each request owns its seed, generator, audio-code history and codec state.
Different GPU kernels and batch shapes can still change sampled outputs.
For streamed PCM, use `"stream": true`, `"stream_format": "audio"`, and
`"response_format": "pcm"`. Output is mono signed 16-bit little-endian PCM at
24 kHz. Complete WAV responses accumulate the same internal streaming output.
With the default deployment, the first chunks contain 1, 2, 4 and 5 codec
frames, then remain at 5 frames.
## Non-streaming throughput profile
For concurrent complete responses, select the throughput deployment from the
repository root.
```bash
curl -X POST http://localhost:8091/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"input": "(laugh) Welcome to our story time.",
"instructions": "A warm young woman, clear voice, lively delivery.",
"response_format": "wav"
}' --output breeze_design.wav
vllm-omni serve BreezeBlue/Breeze-TTS-2 --omni \
--deploy-config vllm_omni/deploy/breeze_tts_2_throughput.yaml \
--host 127.0.0.1 --port 8091
```
Voice cloning (`ref_audio` and `ref_text` must be provided together):
Request a complete WAV with `"stream": false` and omit `stream_format`.
Setting `"stream_format": "audio"` enables streaming even when `stream` is false.
```bash
curl -X POST http://localhost:8091/v1/audio/speech \
-H "Content-Type: application/json" \
curl --fail http://127.0.0.1:8091/v1/audio/speech \
-H 'Content-Type: application/json' \
-d '{
"input": "This is new text spoken in the cloned voice.",
"ref_audio": "file:///path/to/reference.wav",
"ref_text": "The exact transcript of the reference audio.",
"response_format": "wav"
}' --output breeze_clone.wav
"model": "BreezeBlue/Breeze-TTS-2",
"input": "Hello, welcome to the speech synthesis demonstration.",
"stream": false,
"response_format": "wav",
"seed": 42,
"max_new_tokens": 250
}' --output breeze-complete.wav
```
Voice direction (reference + instruction):
The profile was tested on one H100 80 GB. It sets `max_num_seqs: 32` on both
stages and reserves 8 GiB of stage-0 KV cache for thirty-two 2048-token contexts.
These are physical sequence slots: stage 0 can run up to thirty-two plain
requests (`guidance_scale: 1.0`) or sixteen
CFG requests, since each CFG request occupies two slots. Mixed workloads share
the same limit; additional requests queue. The default deployment's memory
target does not apply to this larger profile.
The internal codec uses fixed 16-frame chunks from the first chunk onward,
with a shorter final chunk when needed. `cfg_prefill_delay_tokens: 0` removes
the default eight-frame admission delay, so waiting requests can enter as soon
as scheduling and KV capacity permit. The stateful codec still requires
`async_chunk: true` for these complete HTTP responses. Warm the expected text
lengths and concurrency before measuring the profile.
The shared benchmark defaults to streaming. Override both `stream` and
`stream_format` to measure complete responses, and set the generation limit
explicitly in the request body:
```bash
curl -X POST http://localhost:8091/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"input": "We need to discuss what happened last night.",
"ref_audio": "file:///path/to/reference.wav",
"ref_text": "The exact transcript of the reference audio.",
"instructions": "Speak slowly with a restrained, serious tone.",
"response_format": "wav"
}' --output breeze_direction.wav
python benchmarks/tts/bench_tts.py \
--model BreezeBlue/Breeze-TTS-2 --task default_voice --locale en \
--dataset-path benchmarks/build_dataset/seed_tts_smoke \
--host 127.0.0.1 --port 8091 --concurrency 1 2 4 8 16 32 64 \
--num-prompts 128 --num-warmups 64 \
--output-dir results/breeze-nonstream -- \
--extra-body '{"stream":false,"stream_format":null,"response_format":"pcm","max_new_tokens":384,"seed":42,"extra_params":{"temperature":0.9,"top_k":50,"top_p":1,"repetition_penalty":1.1,"guidance_scale":1}}'
```
Streaming raw PCM (`stream_format="audio"`):
Use the same dataset and sampling settings when comparing deployment profiles.
Verify completed and failed request counts in each result. For complete
responses, compare throughput and RTF; first-response timing represents
completion latency. Run ASR separately: the benchmark's WER mode forces streaming.
## Pipeline and optimization
Stage 0 batches T5Gemma2 text encoding, optional reference-audio encoding,
vLLM's native Qwen3 backbone with paged KV caching, and Breeze's Llama depth
decoder. Text segments up to 128 tokens use padded CUDA Graphs with separate
attention masks. Longer segments use dynamically compiled encoder layers
without retaining a separate graph workspace for each input length.
The depth decoder uses fused QKV/MLP projections, compiled layer kernels,
fixed frame-local KV buffers and a CUDA Graph covering all 15 depth steps.
Graphs pad requests to power-of-two buckets, keeping CFG branches separate.
Compiled layers reuse kernels across batch sizes, avoiding a new compilation
for every shrinking batch. Only real requests advance their random state,
and returned tensors exclude padded rows.
Positive temperature, top-k and top-p are mutable GPU inputs to the captured
sampler. Changing those settings, or changing a positive CFG scale within the
paired path, reuses the graph. Greedy decoding uses a separate graph that skips
sorting and random sampling. Each request owns its random-number generator.
Reference encoding uses dynamic-shape compilation with CUDA Graphs disabled
for that path, so large reference-convolution workspaces remain reusable by
the allocator. Every convolution retains each recording's actual boundary;
the final downsampler repeats its last valid state for partial frames, matching
independent encoding. Static convolution metadata removes GPU-to-CPU padding
calculations. Startup compiles reference batches of one and two.
Waveform storage is aligned to 1920-sample frames, with that frame width
marked static for compilation. Actual sample lengths still govern every
convolution boundary. This avoids nested dynamic ceil/mod expressions that
otherwise make compiler tiling spend minutes simplifying reference shapes.
Stage 1 reuses Qwen3-TTS's stateful codec and its CUDA Graph decoder with the
checkpoint's `audio_tokenizer/` weights. Shared-memory chunks connect stages.
Only the generated-code decode path is captured: reference conditioning was
already consumed in stage 0, so the Qwen ICL reference-prefix graphs and their
large convolution workspaces are unnecessary for Breeze.
The Qwen3 backbone uses compiled piecewise prefill and full decode graphs.
Stage 0 uses synchronous scheduling to keep CFG branches at the same
generation step; stage 1 uses asynchronous scheduling. CFG admission reserves
enough KV pages for both branches' complete generation and preserves their
actual position IDs. Cancellation finishes both branches.
The default admission policy lets active requests produce eight audio frames
before adding another prefill. This gives playback a buffer before text and
reference encoding share the device with an existing stream. It increases a
queued request's first-audio latency; benchmark it separately from an idle server.
The default `max_num_seqs: 2` counts physical branches: it permits two plain
requests or one CFG request at a time. Additional CFG requests queue.
Prefix caching and chunked prefill are disabled because prompt token IDs
reserve positions for externally encoded embeddings. The deployment requires
`async_chunk: true`, including for complete HTTP responses.
The accelerated implementation retains the `breeze_tts_2` model package,
`breeze_tts_2` and `breeze_tts_2_codec` stage names, and default
`breeze_tts_2.yaml` deployment. The shorter `breeze_tts.yaml` and
`breeze_tts_throughput.yaml` deployment filenames remain compatibility aliases.
For a copied custom configuration, use the current `breeze_tts_2.yaml` as
the starting point: codec chunk controls use `codec_chunk_frames`,
`initial_codec_chunk_frames` and `codec_chunk_ramp`.
Defaults now use a 2048-token context and temperature 0.9; set temperature
to zero explicitly when comparing greedy generation with the earlier runtime.
The supported execution scope is one CUDA GPU, TP=1, PP=1 and unquantized
released weights. Named voices, speaker embeddings, dual CFG and multiple
reference recordings are not supported. The original public fast runtime
also does not expose dual CFG.
## Offline inference and validation
`vllm_omni.model_executor.models.breeze_tts_2.prompt.build_breeze_prompt`
accepts a checkpoint tokenizer, text, instructions, sampling parameters,
and optional `ref_audio=(waveform, sample_rate)` plus `ref_text`. Pass its
result to `Omni.generate()` using the default Breeze deployment. Waveform
arrays use soundfile's `(samples, channels)` convention.
CPU tests compare cached depth attention and CFG against Hugging Face Llama
and cover request-owned sampling, payload transport, chunk delivery and CFG
admission. GPU tests compare padded batched text graphs with independent
eager encoders and exercise depth graphs across CFG scales and batch shapes.
```bash
curl -N -X POST http://localhost:8091/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"input": "Streaming output from Breeze TTS 2.",
"stream": true,
"stream_format": "audio",
"response_format": "pcm",
"sample_rate": 24000
}' --output breeze_stream.pcm
pytest -v tests/model_executor/models/test_breeze_tts_2.py \
tests/model_executor/models/test_breeze_tts_2_registration.py \
tests/core/sched/test_omni_cfg_ar_scheduler.py -m 'core_model and cpu'
pytest -v tests/model_executor/models/test_breeze_tts_2_graphs.py -m 'core_model and cuda'
pytest -v tests/e2e/online_serving/test_breeze_tts_2.py \
-m 'core_model and tts' --run-level=core_model
pytest -v tests/e2e/online_serving/test_breeze_tts_2.py \
-m 'advanced_model and tts' --run-level=advanced_model
```
Expected results: finite, non-silent 24 kHz mono WAV files; the streaming example
returns raw PCM bytes. Use `stream_format="sse"` for `speech.audio.delta`
events containing base64-encoded audio. On 1x L20 (greedy, single request after
warm-up) a few seconds of speech completes in roughly 3.4–3.8 s wall time.
Serving tests use the shared client for Chinese CFG design, English PCM,
concurrent CFG requests, reference cloning and direction. Core tests use dummy
weights and eight-frame requests; advanced tests use real weights and the
shared content checks. These checks do not establish perceptual quality parity.
The Chinese content check uses Whisper large-v3 as its primary transcriber:
small misrecognizes homophones on the fixed test clip. The expected text and
shared similarity threshold are unchanged; there is no failed-check retry.
Ensure enough host memory and disk space for the large-v3 test model when
Whisper runs on CPU alongside the serving process.
## Notes
The English PCM test uses a 0 dB harmonic-to-noise floor because the original
eager runtime scores 0.56 dB on that same prompt and seed, below the shared
1 dB floor. Raw PCM is not transcribed by the shared helper. Separate matched
evaluations must retain audio and compare intelligibility, speaker similarity,
instruction effects, first audio, RTF and playback underruns.
- **Sample rate**: only `24000` is accepted; other values fail validation
before inference.
- **Speed / seeds / multi-reference**: `speed` must be `1.0`;
`task_type=VoiceDesign`, `ref_audio_2`, and speaker embeddings are not
supported yet.
- **CFG**: `guidance_scale`/`cfg_scale` must be `1.0`. Non-1.0 values and
`negative_prompt` return a client error.
- **Repetition penalty**: the default `1.1` matches upstream's penalty on
generated codebook-0 tokens and discourages repeated silence. Override it
with `extra_params: {"repetition_penalty": 1.0}` to disable it, or another
finite positive value. Text and reference audio are excluded from its history.
The legacy `tools/breeze_tts_2_upstream_golden.py` uses penalty `1.0`; set
the same serving override when comparing against those golden frames.
- **Length budget**: prompt length is bounded by stage 0 `max_model_len=4096`;
synthesis length by `max_new_tokens` (default 2048 frames ≈ 164 s at the
12.5 Hz codec frame rate) — set `max_new_tokens` for shorter caps.
- **Checkpoint layout**: the checkpoint must contain the `audio_tokenizer/`
subdirectory (bundled Qwen3-TTS codec) for the default streaming path; the
Mimi fallback is used only when that directory is absent and does not
support async-chunk streaming.
- **License**: the reference inference code is Apache 2.0; the checkpoints are
distributed under the separate BreezeBlue Research and Non-Commercial
License. Commercial use requires written authorization from BreezeBlue.
The shared serving benchmark supports default speech, Voice Design and
Voice Clone. For Voice Design:
## Supported features
```bash
python benchmarks/tts/bench_tts.py \
--model BreezeBlue/Breeze-TTS-2 --task voice_design --locale en \
--host 127.0.0.1 --port 8091 --concurrency 1 2 \
--num-prompts 4 --num-warmups 1 --request-seed 42 --output-len 250 \
--output-dir results/breeze
```
| Feature | Status | Notes |
| ------- | ------ | ----- |
| Streaming output | ✓ | `async_chunk: true`, 8-frame inter-stage chunks; raw audio or SSE `speech.audio.delta` |
| Voice cloning | ✓ | one reference clip plus transcript per request |
| Voice design / voice direction | ✓ | `instructions` field, with or without a reference |
| Classifier-free guidance | ✗ | `cfg_scale` must be `1.0`; `negative_prompt` rejected (follow-up) |
| Non-greedy sampling | ✗ | greedy decoding only |
| Prefix caching | ✗ | disabled in the deploy config |
| Tensor / pipeline parallelism | untested | single-GPU profile only |
| Quantization | ✗ | not supported in this release |
| CUDA graphs | not tuned | depth decoder and stage-1 codec run eagerly |
| Platforms | CUDA | qualified on 1x NVIDIA L20 |
For a local model path, add `--served-model-name /path/to/Breeze-TTS-2`.
For cloning, select `--task voice_clone --dataset-path /path/to/seed-tts-eval`
with the desired locale. Reference conditioning with instructions and other
CFG scales also require identical requests and sampling settings in the
reference runtime and this deployment.
The reference source and released weights have separate licenses. Consult
the [reference repository](https://github.com/breezeblue-ai/breeze-tts) and
checkpoint model card.
+2
View File
@@ -15,6 +15,8 @@ kernels==0.16.1
safetensors>=0.8.0
accelerate==1.12.0
soundfile>=0.13.1
# Breeze reference conditioning must use the released codec's soxr resampling.
soxr>=0.5.0.post1
cache-dit==1.5.0
tqdm>=4.66.0
torchsde>=0.2.6
+2 -2
View File
@@ -167,7 +167,7 @@ def test_inventory_matches_reviewed_snapshot_counts():
EnvironmentVariableCategory.PUBLIC_OMNI: 30,
EnvironmentVariableCategory.INHERITED_VLLM: 20,
EnvironmentVariableCategory.PLATFORM_EXTERNAL: 27,
EnvironmentVariableCategory.MODEL_SPECIFIC: 59,
EnvironmentVariableCategory.MODEL_SPECIFIC: 58,
EnvironmentVariableCategory.BENCHMARK_TRANSITIONAL: 21,
EnvironmentVariableCategory.INTERNAL: 2,
}
@@ -181,7 +181,7 @@ def test_inventory_matches_reviewed_snapshot_counts():
ModelEnvironmentVariableDisposition.PROMOTE: 31,
ModelEnvironmentVariableDisposition.REQUEST_SCOPE: 5,
ModelEnvironmentVariableDisposition.EXTERNAL: 0,
ModelEnvironmentVariableDisposition.INTERNALIZE: 16,
ModelEnvironmentVariableDisposition.INTERNALIZE: 15,
ModelEnvironmentVariableDisposition.DEPRECATE_REMOVE: 7,
}
@@ -0,0 +1,102 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import pytest
from vllm import SamplingParams
from vllm.v1.core.sched.output import SchedulerOutput
from vllm.v1.core.sched.request_queue import SchedulingPolicy, create_request_queue
from vllm.v1.request import Request, RequestStatus
from vllm_omni.core.sched.omni_ar_scheduler import OmniARScheduler
from vllm_omni.core.sched.omni_cfg_ar_scheduler import OmniCFGARScheduler
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _request(name: str, prompt_length: int) -> Request:
return Request(name, [0] * prompt_length, SamplingParams(max_tokens=100), None)
def _scheduler(monkeypatch, waiting, running=()):
scheduler = OmniCFGARScheduler.__new__(OmniCFGARScheduler)
scheduler.policy = SchedulingPolicy.FCFS
scheduler.waiting = create_request_queue(scheduler.policy)
for request in waiting:
scheduler.waiting.add_request(request)
scheduler.skipped_waiting = create_request_queue(scheduler.policy)
scheduler.running = list(running)
scheduler.requests = {request.request_id: request for request in [*waiting, *running]}
scheduler.max_model_len = 128
scheduler.max_num_running_reqs = 2
scheduler.max_num_scheduled_tokens = 256
scheduler.block_size = 16
scheduler._capacity = 16
scheduler._pairs = {"pair": {"cond": "cond", "uncond": "uncond"}}
scheduler._request_pairs = {"cond": "pair", "uncond": "pair"}
scheduler._prefill_min_output_tokens = 0
def advance(self, throttle_prefills=False):
self.running.extend(self.waiting)
self.waiting = create_request_queue(self.policy)
scheduled = {}
for request in self.running:
count = request.num_tokens - request.num_computed_tokens
request.num_computed_tokens += count
request.append_output_token_ids(0)
scheduled[request.request_id] = count
output = SchedulerOutput.make_empty()
output.num_scheduled_tokens = scheduled
output.total_num_scheduled_tokens = sum(scheduled.values())
return output
monkeypatch.setattr(OmniARScheduler, "schedule", advance)
return scheduler
def test_unequal_prompts_keep_real_positions_and_share_context_limit(monkeypatch):
cond, uncond = _request("cond", 80), _request("uncond", 30)
scheduler = _scheduler(monkeypatch, [cond, uncond])
output = scheduler.schedule()
assert output.num_scheduled_tokens == {"cond": 80, "uncond": 30}
assert (cond.max_tokens, uncond.max_tokens) == (48, 48)
for _ in range(4):
assert scheduler.schedule().num_scheduled_tokens == {"cond": 1, "uncond": 1}
assert cond.num_computed_tokens - uncond.num_computed_tokens == 50
def test_waiting_pair_cannot_be_split_or_starved_by_later_single(monkeypatch):
cond, uncond = _request("cond", 40), _request("uncond", 20)
running, later = _request("running", 10), _request("later", 10)
scheduler = _scheduler(monkeypatch, [cond, uncond, later], [running])
assert scheduler.schedule().num_scheduled_tokens == {"running": 10}
assert list(scheduler.waiting) == [cond, uncond, later]
scheduler.running.clear()
del scheduler.requests["running"]
assert scheduler.schedule().num_scheduled_tokens == {"cond": 40, "uncond": 20}
assert list(scheduler.waiting) == [later]
def test_cancel_either_branch_finishes_both_and_releases_pair(monkeypatch):
cond, uncond = _request("cond", 40), _request("uncond", 20)
scheduler = _scheduler(monkeypatch, [cond, uncond])
def finish(self, ids, status):
assert set(ids) == {"cond", "uncond"}
assert status == RequestStatus.FINISHED_ABORTED
finished = [self.requests.pop(rid) for rid in ids]
self.waiting.remove_requests(finished)
return finished
monkeypatch.setattr(OmniARScheduler, "finish_requests", finish)
assert len(scheduler.finish_requests("uncond", RequestStatus.FINISHED_ABORTED)) == 2
assert not scheduler._pairs and not scheduler._request_pairs and not scheduler.waiting
def test_new_prefill_waits_for_initial_streaming_audio(monkeypatch):
request = _request("plain", 20)
scheduler = _scheduler(monkeypatch, [], [request])
scheduler._prefill_min_output_tokens = 8
request.append_output_token_ids([0] * 7)
assert scheduler._should_defer_waiting_admission()
request.append_output_token_ids(0)
assert not scheduler._should_defer_waiting_admission()
+81 -73
View File
@@ -1,100 +1,108 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""
E2E Online tests for Breeze-TTS-2 with text input and audio output.
These tests verify the /v1/audio/speech endpoint works correctly with
actual model inference, not mocks. Breeze-TTS-2 plain mode needs only
``input`` and a speaker tag (``voice`` defaults to ``S0``); reference
cloning requires ``ref_audio`` + ``ref_text`` and is not exercised here.
"""
import os
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
"""Breeze design, reference conditioning and CFG through the shared speech client."""
import pytest
from tests.helpers.mark import hardware_test
from tests.helpers.media import get_asset_path
from tests.helpers.runtime import OmniServerParams
from tests.helpers.stage_config import get_deploy_config_path
MODEL = "BreezeBlue/Breeze-TTS-2"
DEFAULT_AUDIO_SPEECH_TIMEOUT_S = 300.0
MAX_CONCURRENT = 4
# ~0.5 s of 24 kHz mono PCM_16 in WAV (~24k payload + header).
_MIN_AUDIO_BYTES = 40_000
def get_prompt(prompt_type="text"):
"""English prompt for zero-shot TTS."""
prompts = {
"text": "The weather is nice today, perfect for a walk in the park.",
}
return prompts.get(prompt_type, prompts["text"])
tts_server_params = [
pytest.param(
OmniServerParams(
model=MODEL,
stage_config_path=get_deploy_config_path("breeze_tts_2.yaml"),
server_args=["--trust-remote-code", "--disable-log-stats"],
pytestmark = [
pytest.mark.core_model,
pytest.mark.advanced_model,
pytest.mark.tts,
pytest.mark.parametrize(
"omni_server",
[OmniServerParams(model=MODEL, stage_config_path=get_deploy_config_path("breeze_tts_2.yaml"))],
indirect=True,
),
id="breeze_tts_2",
)
]
@pytest.mark.slow
@pytest.mark.tts
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
def test_text_to_audio_001(omni_server, online_client) -> None:
"""
Test plain text-to-audio via OpenAI API.
Deploy Setting: breeze_tts_2.yaml
Input Modal: text
Output Modal: audio
Input Setting: stream=False
Datasets: few requests (max_num_seqs=8)
"""
request_config = {
def test_chinese_voice_design(omni_server, online_client, run_level) -> None:
online_client.send_audio_speech_request(
{
"model": omni_server.model,
"input": get_prompt(),
"stream": False,
"timeout": DEFAULT_AUDIO_SPEECH_TIMEOUT_S,
"input": "你好,欢迎使用语音合成系统。今天的天气很好。",
"instructions": "年轻女性,使用标准普通话,语速适中。",
"extra_params": {"guidance_scale": 4.0},
"transcript_language": "zh",
# Whisper-small transcribes "合成" as "和程" on this fixed clip.
# Use the stronger primary ASR without changing the expected text
# or similarity threshold, or retrying after a failed assertion.
"transcript_model": "large-v3",
"response_format": "wav",
"voice": "S0",
"sample_rate": 24000,
"min_audio_bytes": _MIN_AUDIO_BYTES,
"min_audio_bytes": 24000,
"seed": 42,
"max_new_tokens": 8 if run_level == "core_model" else 250,
}
online_client.send_audio_speech_request(request_config, request_num=MAX_CONCURRENT)
)
@pytest.mark.slow
@pytest.mark.tts
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
def test_text_to_audio_002(omni_server, online_client) -> None:
"""
Test streaming text-to-audio via OpenAI API (async-chunk SSE deltas).
Deploy Setting: breeze_tts_2.yaml
Input Modal: text
Output Modal: audio
Input Setting: stream=True
Datasets: single request
"""
request_config = {
def test_english_streaming(omni_server, online_client, run_level) -> None:
online_client.send_audio_speech_request(
{
"model": omni_server.model,
"input": get_prompt(),
"input": "Hello, this is a demonstration of natural speech synthesis.",
"instructions": "A warm, clear female voice speaking English at a moderate pace.",
"transcript_language": "en",
"response_format": "pcm",
"sample_rate": 24000,
"min_audio_bytes": 24000,
"stream": True,
"stream_format": "audio",
"timeout": DEFAULT_AUDIO_SPEECH_TIMEOUT_S,
"response_format": "wav",
"voice": "S0",
"sample_rate": 24000,
"min_audio_bytes": _MIN_AUDIO_BYTES,
# The reference eager runtime scores 0.56 dB on this same prompt
# (seed 42, CFG=1); the shared 1 dB floor rejects it as well.
"min_hnr_db": 0.0,
"seed": 42,
"max_new_tokens": 8 if run_level == "core_model" else 250,
}
online_client.send_audio_speech_request(request_config)
)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_two_concurrent_requests(omni_server, online_client, run_level) -> None:
online_client.send_audio_speech_request(
{
"model": omni_server.model,
"input": "Each request keeps its own audio generation state.",
"instructions": "A clear English voice.",
"extra_params": {"guidance_scale": 4.0},
"transcript_language": "en",
"response_format": "wav",
"sample_rate": 24000,
"min_audio_bytes": 24000,
"seed": 123,
"max_new_tokens": 8 if run_level == "core_model" else 250,
},
request_num=2,
)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("guidance_scale", [1.0, 4.0])
def test_reference_cloning_and_direction(omni_server, online_client, run_level, guidance_scale) -> None:
online_client.send_audio_speech_request(
{
"model": omni_server.model,
"input": "Please read this sentence clearly and naturally.",
"instructions": "Keep the reference voice. Speak gently and calmly.",
"ref_audio": get_asset_path("qwen3_tts/clone_2.wav", as_data_url=True),
"ref_text": "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you.",
"task_type": "Base",
"extra_params": {"guidance_scale": guidance_scale},
"transcript_language": "en",
"response_format": "wav",
"sample_rate": 24000,
"min_audio_bytes": 24000,
"seed": 42,
"max_new_tokens": 8 if run_level == "core_model" else 250,
}
)
@@ -0,0 +1,185 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""CPU regression coverage for Breeze's public speech request compatibility."""
from concurrent.futures import Executor
from dataclasses import dataclass
from unittest.mock import AsyncMock, Mock
import numpy as np
import pytest
from pydantic import ValidationError
from vllm import SamplingParams
from vllm_omni.entrypoints.openai.protocol.audio import OpenAICreateSpeechRequest
from vllm_omni.entrypoints.openai.tts_adapters.base import SpeechServingContext
from vllm_omni.entrypoints.openai.tts_adapters.breeze_tts_2 import BreezeTTS2Adapter
from vllm_omni.model_executor.models.breeze_tts_2.prompt import DEFAULT_INSTRUCTION
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
REFERENCE = "data:audio/wav;base64,cHVibGljLXRlc3QtZGF0YQ=="
@dataclass
class ModelConfigStub:
max_model_len: int
@dataclass
class EngineStub:
model_config: ModelConfigStub
@dataclass
class ServerStub:
_tts_executor: Executor | None
_validate_ref_audio_format: Mock
_resolve_ref_audio: AsyncMock
@pytest.fixture
def adapter():
engine = EngineStub(model_config=ModelConfigStub(max_model_len=32))
server = ServerStub(
_tts_executor=None,
_validate_ref_audio_format=Mock(return_value=None),
_resolve_ref_audio=AsyncMock(return_value=(np.ones(1920, dtype=np.float32), 24000, None)),
)
result = BreezeTTS2Adapter(SpeechServingContext(server=server, engine_client=engine))
result.tokenizer = Mock()
result.tokenizer.encode.side_effect = lambda text, **kwargs: list(text.encode("utf-8"))
return result
@pytest.mark.parametrize("voice,tag", [(None, "S0"), ("default", "S0"), ("", "S0"), ("S3", "S3"), ("[S2]", "S2")])
def test_speaker_tags_reach_the_conditioning_prompt(adapter, voice, tag):
request = OpenAICreateSpeechRequest(input="Hello.", voice=voice)
assert adapter.validate(request) is None
prompt = adapter._build_prompt(request, SamplingParams(), None)
target = bytes(prompt["additional_information"]["breeze_prompt"]["target_ids"]).decode()
assert target == f"[{tag}]<ins_bos>{DEFAULT_INSTRUCTION}<ins_eos>Hello."
def test_explicit_empty_instruction_remains_empty(adapter):
request = OpenAICreateSpeechRequest(input="Hello.", instructions="")
prompt = adapter._build_prompt(request, SamplingParams(), None)
target = bytes(prompt["additional_information"]["breeze_prompt"]["target_ids"]).decode()
assert target == "[S0]<ins_bos><ins_eos>Hello."
@pytest.mark.parametrize(
"extra,expected",
[({"cfg_scale": 4.0}, 4.0), ({"guidance_scale": 2.0, "cfg_scale": 4.0}, 2.0)],
)
def test_cfg_alias_and_explicit_guidance_precedence(adapter, extra, expected):
request = OpenAICreateSpeechRequest(input="Hello.", extra_params=extra)
assert adapter.validate(request) is None
prompt = adapter._build_prompt(request, SamplingParams(), None)
assert prompt["additional_information"]["breeze_prompt"]["guidance_scale"] == expected
assert prompt["additional_information"]["cfg_group"]["role"] == "cond"
@pytest.mark.parametrize("reference", [None, REFERENCE, [REFERENCE]])
def test_custom_voice_and_false_x_vector_are_compatible(adapter, reference):
request = OpenAICreateSpeechRequest(
input="Hello.",
task_type="CustomVoice",
x_vector_only_mode=False,
ref_audio=reference,
ref_text="Reference words." if reference is not None else None,
)
assert adapter.validate(request) is None
if reference is not None:
adapter.ctx.server._validate_ref_audio_format.assert_called_once_with(REFERENCE)
@pytest.mark.asyncio
async def test_single_reference_list_is_resolved_once_and_preserved_in_prompt(adapter):
request = OpenAICreateSpeechRequest(
input="New words.", voice="S1", ref_audio=[REFERENCE], ref_text="Reference words.", task_type="Base"
)
assert adapter.validate(request) is None
prepared = await adapter.build(request, [SamplingParams()], has_inline_ref_audio=True)
adapter.ctx.server._resolve_ref_audio.assert_awaited_once_with(REFERENCE)
info = prepared.prompt["additional_information"]
assert bytes(info["breeze_prompt"]["reference_ids"]).decode() == "[S1]Reference words."
assert info["breeze_prompt"]["reference_frames"] == 1
np.testing.assert_array_equal(info["reference_waveform"].numpy(), np.ones(1920, dtype=np.float32))
@pytest.mark.parametrize(
"kwargs,fragment",
[
({"input": " \n"}, "empty"),
({"speed": 1.2}, "speed"),
({"language": "English"}, "language"),
({"ref_audio_2": REFERENCE}, "one reference"),
({"speaker_embedding": [0.1, 0.2]}, "one reference"),
({"x_vector_only_mode": True}, "x_vector_only_mode"),
({"ref_audio": [REFERENCE, REFERENCE], "ref_text": "words"}, "exactly one"),
({"ref_audio": REFERENCE}, "transcript"),
({"ref_audio": REFERENCE, "ref_text": " "}, "transcript"),
({"ref_text": "words"}, "requires ref_audio"),
({"task_type": "Base"}, "VoiceDesign"),
({"ref_audio": REFERENCE, "ref_text": "words", "task_type": "VoiceDesign"}, "Base"),
({"extra_params": {"unknown": 1}}, "Unsupported"),
({"extra_params": {"temperature": True}}, "finite number"),
({"extra_params": {"temperature": float("nan")}}, "finite number"),
({"extra_params": {"cfg_scale": float("inf")}}, "finite number"),
({"extra_params": {"cfg_scale": 0}}, "positive"),
({"extra_params": {"guidance_scale": 0, "cfg_scale": 4}}, "positive"),
({"extra_params": {"repetition_penalty": 0}}, "positive"),
({"extra_params": {"temperature": -0.1}}, "temperature"),
({"extra_params": {"top_p": 0}}, "top_p"),
({"extra_params": {"top_p": 1.1}}, "top_p"),
({"extra_params": {"top_k": 1.5}}, "top_k"),
({"extra_params": {"top_k": -2}}, "top_k"),
],
)
def test_unsupported_or_invalid_inputs_fail_before_build(adapter, kwargs, fragment):
request = OpenAICreateSpeechRequest(**{"input": "Hello.", **kwargs})
error = adapter.validate(request)
assert error is not None and fragment in error
adapter.ctx.server._resolve_ref_audio.assert_not_awaited()
def test_empty_reference_list_is_rejected_by_request_schema():
with pytest.raises(ValidationError, match="list cannot be empty"):
OpenAICreateSpeechRequest(input="Hello.", ref_audio=[])
def test_reference_format_validation_error_is_preserved(adapter):
adapter.ctx.server._validate_ref_audio_format.return_value = "Unsupported reference format"
request = OpenAICreateSpeechRequest(input="Hello.", ref_audio=[REFERENCE], ref_text="words")
assert adapter.validate(request) == "Unsupported reference format"
@pytest.mark.parametrize("requested,expected", [(None, 9), (4, 4), (200, 9)])
def test_sampling_override_clones_defaults_and_caps_remaining_context(adapter, requested, expected):
defaults = [SamplingParams(max_tokens=20, top_k=50), SamplingParams(max_tokens=100)]
request = OpenAICreateSpeechRequest(
input="Hello.", max_new_tokens=requested, extra_params={"repetition_penalty": 1.7}
)
result = adapter.apply_sampling_overrides(defaults, request, {"prompt_token_ids": [0] * 23})
assert result is not defaults and result[0] is not defaults[0]
assert result[0].max_tokens == expected
assert result[0].repetition_penalty == 1.7
assert (defaults[0].max_tokens, defaults[0].repetition_penalty) == (20, 1.0)
assert result[1].max_tokens == defaults[1].max_tokens == 100
def test_sampling_override_normalizes_disabled_top_k_without_mutating_default(adapter):
default = SamplingParams(max_tokens=20)
default.top_k = 0
result = adapter.apply_sampling_overrides([default], OpenAICreateSpeechRequest(input="Hello."))
assert result[0].top_k == -1
assert default.top_k == 0
@pytest.mark.parametrize("prompt_length", [32, 33])
def test_filled_context_is_rejected_without_mutating_sampling_defaults(adapter, prompt_length):
default = SamplingParams(max_tokens=20)
request = OpenAICreateSpeechRequest(input="Hello.", max_new_tokens=4)
with pytest.raises(ValueError, match="fills the model context"):
adapter.apply_sampling_overrides([default], request, {"prompt_token_ids": [0] * prompt_length})
assert default.max_tokens == 20
@@ -16,6 +16,7 @@ from inspect import Signature, signature
from pathlib import Path
from types import SimpleNamespace
import anyio
import numpy as np
import pytest
import torch
@@ -5779,21 +5780,34 @@ class TestTTSAsyncOffloading:
@pytest.mark.asyncio
async def test_generate_audio_chunks_discards_ref_audio_artifact_warmup_on_close(self, qwen3_tts_server):
closed = asyncio.Event()
async def pcm_generator():
yield SimpleNamespace(
multimodal_output={
try:
yield OmniRequestOutput(
request_id="req-close",
final_output_type="audio",
_multimodal_output={
"audio": torch.zeros(16, dtype=torch.float32),
"sr": 24000,
}
},
)
await asyncio.sleep(0)
finally:
# Engine abort waits for stage acknowledgments. Retain an
# actual cancellation checkpoint to cover ASGI cancel scopes.
await anyio.sleep(0)
closed.set()
qwen3_tts_server._request_ref_audio_artifact_keys["req-close"] = ("artifact-close", False)
stream = qwen3_tts_server._generate_audio_chunks(pcm_generator(), "req-close")
engine_stream = pcm_generator()
stream = qwen3_tts_server._generate_audio_chunks(engine_stream, "req-close")
assert await anext(stream)
with anyio.CancelScope() as scope:
scope.cancel()
await stream.aclose()
assert closed.is_set()
assert "req-close" not in qwen3_tts_server._request_ref_audio_artifact_keys
assert ("artifact-close", False) not in qwen3_tts_server._ref_audio_model_artifact_ready
@@ -140,6 +140,7 @@ _PIPELINE_STAGES = [
"audio_vae",
"aura",
"breeze_tts_2",
"breeze_tts_2_codec",
"code2wav",
"cosyvoice3_code2wav",
"cosyvoice3_talker",
+82
View File
@@ -5,6 +5,7 @@ import asyncio
import re
from types import SimpleNamespace
import anyio
import pytest
from vllm.lora.request import LoRARequest
from vllm.sampling_params import RequestOutputKind, SamplingParams
@@ -545,6 +546,87 @@ def test_generate_cancellation_converges_after_engine_shutdown_starts(mocker):
asyncio.run(run_test())
@pytest.mark.cpu
def test_generate_waits_for_abort_inside_cancelled_asgi_scope():
async def run_test():
submitted = anyio.Event()
acknowledged = []
async def add_request(**kwargs):
submitted.set()
async def abort(request_ids, timeout=None):
assert timeout == async_omni_mod.ABORT_TIMEOUT_S
await anyio.sleep(0)
acknowledged.extend(request_ids)
omni = get_async_omni_instance(fake_add_request=add_request, fake_abort_request=abort)
async def collect():
async for _ in omni.generate(
prompt={"prompt": "prompt"},
request_id="cancel-asgi",
sampling_params_list=[SamplingParams()],
output_modalities=["image"],
):
pass
async with anyio.create_task_group() as group:
group.start_soon(collect)
await submitted.wait()
group.cancel_scope.cancel()
assert len(acknowledged) == 1 and acknowledged[0].startswith("cancel-asgi-")
assert omni.request_states == {}
asyncio.run(run_test())
@pytest.mark.cpu
def test_generate_bounds_abort_inside_cancelled_asgi_scope(monkeypatch):
monkeypatch.setattr(async_omni_mod, "ABORT_TIMEOUT_S", 0.05)
async def run_test():
submitted = anyio.Event()
abort_started = anyio.Event()
abort_finished = anyio.Event()
async def add_request(**kwargs):
submitted.set()
async def abort(request_ids, timeout=None):
assert len(request_ids) == 1
assert timeout == 0.05
abort_started.set()
try:
await asyncio.Future()
finally:
abort_finished.set()
omni = get_async_omni_instance(fake_add_request=add_request, fake_abort_request=abort)
async def collect():
async for _ in omni.generate(
prompt={"prompt": "prompt"},
request_id="cancel-asgi-hanging-abort",
sampling_params_list=[SamplingParams()],
output_modalities=["image"],
):
pass
async def cancel_request():
async with anyio.create_task_group() as group:
group.start_soon(collect)
await submitted.wait()
group.cancel_scope.cancel()
await asyncio.wait_for(cancel_request(), timeout=1.0)
assert abort_started.is_set()
assert abort_finished.is_set()
assert omni.request_states == {}
asyncio.run(run_test())
@pytest.mark.cpu
def test_generate_yields_streaming_diffusion_chunks_before_final():
"""AsyncOmni.generate yields every intermediate diffusion chunk before the final one."""
@@ -1158,6 +1158,7 @@ def test_async_omni_errored_property_alive():
)
assert omni.errored is False
assert omni.is_stopped is False
def test_async_omni_errored_property_dead_engine():
@@ -1168,6 +1169,7 @@ def test_async_omni_errored_property_dead_engine():
)
assert omni.errored is True
assert omni.is_stopped is True
def test_async_omni_errored_false_when_stage_dead():
+5 -1
View File
@@ -780,7 +780,11 @@ def _resolve_audio_transcript(
audio_bytes = getattr(response, "audio_bytes", None)
if not audio_bytes:
return None
return convert_audio_bytes_to_text(audio_bytes, language=request_config.get("transcript_language"))
return convert_audio_bytes_to_text(
audio_bytes,
model_size=request_config.get("transcript_model", "small"),
language=request_config.get("transcript_language"),
)
def assert_omni_response(response: Any, request_config: dict[str, Any], run_level):
+2
View File
@@ -1444,6 +1444,7 @@ class OnlineOmniClient:
- task_type, ref_text, ref_audio: TTS-specific extras (optional, passed via extra_body)
- min_audio_bytes: optional minimum ``len(audio_bytes)`` checked in ``assert_audio_speech_response``
- transcript_expected_text: local expected spoken text; defaults to ``input``
- transcript_model: primary Whisper model for content checks; defaults to ``small``
- timeout: request timeout in seconds (float, optional, default 120.0)
- stream: whether to use streaming API (bool, optional, default False)
@@ -1467,6 +1468,7 @@ class OnlineOmniClient:
"task_type",
"ref_text",
"ref_audio",
"extra_params",
"language",
"max_new_tokens",
"seed",
+21
View File
@@ -11,6 +11,7 @@ from tests.helpers.assertions import (
_resolve_audio_transcript,
assert_audio_speech_response,
)
from tests.helpers.client import OmniResponse
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@@ -75,6 +76,26 @@ def test_resolve_transcript_leaves_language_unset_by_default(monkeypatch):
assert captured["model_size"] == "small"
def test_resolve_transcript_uses_configured_primary_model(monkeypatch):
captured = _capture_transcribe(monkeypatch)
response = OmniResponse(audio_content=None, audio_bytes=b"fake-wav")
_resolve_audio_transcript(
response,
{
"input": "你好",
"response_format": "wav",
"transcript_language": "zh",
"transcript_model": "large-v3",
},
"advanced_model",
speech_api=True,
)
assert captured["model_size"] == "large-v3"
assert captured["language"] == "zh"
@pytest.mark.parametrize(
("request_config", "expected_text"),
[
Whitespace-only changes.
@@ -1,128 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from types import SimpleNamespace
import numpy as np
import pytest
import torch
from vllm_omni.model_executor.models.breeze_tts_2.audio_tokenizer import (
BreezeReferenceAudioTokenizer,
resolve_audio_tokenizer_path,
)
def _patch_hf_api(monkeypatch, snapshot_download):
"""Point the resolver's tagged HF helper at a fake API object."""
from vllm.transformers_utils import repo_utils
monkeypatch.setattr(repo_utils, "hf_api", lambda: SimpleNamespace(snapshot_download=snapshot_download))
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class _Tokenizer:
def encode(self, audio, sr=None):
assert isinstance(audio, np.ndarray)
assert sr == 16000
return {"audio_codes": [torch.arange(32).reshape(16, 2)]}
def test_reference_audio_tokenizer_normalizes_codebook_major_output():
adapter = BreezeReferenceAudioTokenizer(_Tokenizer())
codes = adapter.encode(np.zeros(32, dtype=np.float32), 16000)
assert tuple(codes.shape) == (2, 16)
assert codes.dtype == torch.int16
assert codes.device.type == "cpu"
def test_reference_audio_tokenizer_rejects_waveform_without_sample_rate():
adapter = BreezeReferenceAudioTokenizer(_Tokenizer())
try:
adapter.encode(np.zeros(32, dtype=np.float32))
except ValueError as exc:
assert "sample_rate is required" in str(exc)
else:
raise AssertionError("missing sample rate should be rejected")
def test_resolve_prefers_local_directory_without_hub_access(tmp_path, monkeypatch):
bundled = tmp_path / "audio_tokenizer"
bundled.mkdir()
def _no_hub(*_args, **_kwargs):
raise AssertionError("local directories must not touch the hub")
_patch_hf_api(monkeypatch, _no_hub)
assert resolve_audio_tokenizer_path(str(tmp_path)) == bundled
def test_resolve_local_directory_without_bundled_tokenizer_returns_none(tmp_path, monkeypatch):
def _no_hub(*_args, **_kwargs):
raise AssertionError("local directories must not touch the hub")
_patch_hf_api(monkeypatch, _no_hub)
assert resolve_audio_tokenizer_path(str(tmp_path)) is None
def test_resolve_repo_id_uses_hf_snapshot(tmp_path, monkeypatch):
snapshot_root = tmp_path / "snapshot"
(snapshot_root / "audio_tokenizer").mkdir(parents=True)
calls = []
def _fake_snapshot_download(repo_id, **kwargs):
calls.append((repo_id, kwargs))
if kwargs.get("local_files_only"):
raise FileNotFoundError("cold cache")
return str(snapshot_root)
_patch_hf_api(monkeypatch, _fake_snapshot_download)
resolved = resolve_audio_tokenizer_path("BreezeBlue/Breeze-TTS-2")
assert resolved == snapshot_root / "audio_tokenizer"
# The cold-cache probe runs first; the online fetch carries the pattern.
assert calls[0] == (
"BreezeBlue/Breeze-TTS-2",
{"allow_patterns": ["audio_tokenizer/*"], "local_files_only": True},
)
assert calls[1] == ("BreezeBlue/Breeze-TTS-2", {"allow_patterns": ["audio_tokenizer/*"]})
def test_resolve_repo_id_prefers_complete_cached_snapshot(tmp_path, monkeypatch):
snapshot_root = tmp_path / "snapshot"
(snapshot_root / "audio_tokenizer").mkdir(parents=True)
calls = []
def _fake_snapshot_download(repo_id, **kwargs):
calls.append((repo_id, kwargs))
assert kwargs.get("local_files_only"), "cached snapshot must not hit the network"
return str(snapshot_root)
_patch_hf_api(monkeypatch, _fake_snapshot_download)
resolved = resolve_audio_tokenizer_path("BreezeBlue/Breeze-TTS-2")
assert resolved == snapshot_root / "audio_tokenizer"
assert len(calls) == 1
def test_resolve_repo_id_without_bundled_tokenizer_returns_none(tmp_path, monkeypatch):
snapshot_root = tmp_path / "snapshot"
snapshot_root.mkdir()
def _fake_snapshot_download(repo_id, **kwargs):
if kwargs.get("local_files_only"):
raise FileNotFoundError("cold cache")
return str(snapshot_root)
_patch_hf_api(monkeypatch, _fake_snapshot_download)
assert resolve_audio_tokenizer_path("BreezeBlue/Breeze-TTS-2") is None
@@ -1,95 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from typing import Any
import pytest
import torch
from vllm_omni.model_executor.models.breeze_tts_2.modeling_breeze_tts_2_talker import (
BreezeTTS2TalkerForGeneration,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class _RequestHead:
def __call__(self, hidden: torch.Tensor) -> torch.Tensor:
# Hidden row 0 selects codec id 2; row 1 selects codec id 3.
code0 = 2 + int(hidden.reshape(-1)[0].item())
logits = torch.zeros((hidden.shape[0], 9), dtype=hidden.dtype)
logits[:, code0] = 1.0
return logits
def _model() -> BreezeTTS2TalkerForGeneration:
model = object.__new__(BreezeTTS2TalkerForGeneration)
model.num_codebooks = 4
model.codebook_vocab_size = 8
model.codebook_size = 8
model.codebook_eos_token_id = 0
model.audio_token_id = 100
model.audio_eos_token_id = 101
model.lm_head = _RequestHead()
model.logits_processor = lambda head, hidden: head(hidden)
model._codec_disallowed_mask = None
model._batch_state = None
model._batch_state_spans = None
model._async_chunk = True
model._golden_dump_dir = None
model._generate_depth_codes = lambda _hidden, code0: _frame(code0).unsqueeze(0)
return model
def _frame(code0: int) -> torch.Tensor:
return torch.tensor([code0, code0 + 1, code0 + 2, code0 + 3], dtype=torch.long)
def _step(model: BreezeTTS2TalkerForGeneration, infos: list[dict[str, Any]], hidden: torch.Tensor):
return model.make_omni_output(
hidden,
model_intermediate_buffer=infos,
request_token_spans=[(0, 1), (1, 2)],
)
def test_two_requests_accumulate_and_emit_independent_frames():
model = _model()
infos = [
{
"breeze_generated_frames": 1,
"breeze_max_new_frames": 2,
"breeze_audio_codes": _frame(2).reshape(1, 4),
},
{"breeze_generated_frames": 0, "breeze_max_new_frames": 3},
]
hidden = torch.tensor([[0.0], [1.0]], dtype=torch.float32)
first = _step(model, infos, hidden)
assert [tuple(item.shape) for item in first.multimodal_outputs["codes"]["audio"]] == [(1, 4), (1, 4)]
assert torch.equal(first.multimodal_outputs["codes"]["audio"][0][0], _frame(2))
assert torch.equal(first.multimodal_outputs["codes"]["audio"][1][0], _frame(3))
second = _step(model, infos, hidden)
# Request 0 reaches its two-frame budget and emits only a finish marker;
# request 1 still has budget and emits its next tail frame.
assert second.multimodal_outputs["codes"]["audio"][0].numel() == 0
assert torch.equal(second.multimodal_outputs["codes"]["audio"][1][0], _frame(3))
# Async streaming only maintains the per-frame tail; the cumulative
# buffer is deliberately not grown (request 0 keeps its pre-seeded
# entry untouched, request 1 never gains one).
assert infos[0]["breeze_audio_codes"].shape == (1, 4)
assert "breeze_audio_codes" not in infos[1]
assert infos[0]["breeze_force_eos"] is True
assert infos[1]["breeze_force_eos"] is False
def test_finished_requests_only_clear_model_owned_batch_mapping():
model = _model()
model._batch_state = [{"id": "a"}, {"id": "b"}]
model._batch_state_spans = [(0, 1), (1, 2)]
model.on_requests_finished(["a", "b"])
assert model._batch_state is None
assert model._batch_state_spans is None
@@ -1,68 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from typing import Any
import pytest
import torch
from torch import nn
from vllm_omni.model_executor.models.breeze_tts_2.modeling_breeze_tts_2_codec import (
BreezeTTS2MimiCodec,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class _BundledTokenizer:
def __init__(self) -> None:
self.model = nn.Linear(2, 2)
def named_parameters(self):
for name, parameter in self.model.named_parameters():
yield f"model.{name}", parameter
def decode(self, _encoded: Any) -> tuple[list[torch.Tensor], int]:
return [torch.zeros(4)], 24_000
def test_bundled_qwen_tokenizer_consumes_unused_root_fallback_weights(tmp_path):
bundled = tmp_path / "audio_tokenizer"
bundled.mkdir()
codec = object.__new__(BreezeTTS2MimiCodec)
codec.model_path = str(tmp_path)
codec.vllm_config = None
# load_weights reuses the path resolved once in __init__.
codec._tokenizer_path = bundled
codec._codec = None
codec._audio_tokenizer = _BundledTokenizer()
codec._loaded_local_weights = False
weights = [
("backbone_model.layers.0.norm.weight", torch.ones(2)),
("codec_model.decoder.layers.0.conv.weight", torch.ones(2)),
("lm_head.weight", torch.ones(2, 2)),
]
loaded = codec.load_weights(iter(weights))
assert "_audio_tokenizer.model.weight" in loaded
assert "_audio_tokenizer.model.bias" in loaded
# The bundled tokenizer was preloaded, so load_weights must not try to copy
# the root Mimi fallback into the nonexistent ``self._codec`` module.
assert codec._codec is None
def test_codec_warmup_with_single_token_returns_empty_audio():
codec = object.__new__(BreezeTTS2MimiCodec)
codec._num_codebooks = 4
codec._codebook_size = 8
codec._sample_rate = 24_000
codec._audio_tokenizer = None
codec._codec = None
codec._async_chunk = False
output = codec.forward(torch.tensor([1], dtype=torch.long), seq_token_counts=[1])
assert output.multimodal_outputs["model_outputs"][0].numel() == 0
assert output.multimodal_outputs["sr"][0].item() == 24_000
@@ -1,113 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import pytest
from transformers.models.qwen3 import Qwen3Config
from vllm_omni.model_executor.models.breeze_tts_2.configuration_breeze_tts_2 import (
BreezeTTS2Config,
BreezeTTS2DepthDecoderConfig,
BreezeTTS2TextEncoderConfig,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _config() -> BreezeTTS2Config:
return BreezeTTS2Config(
backbone_config={
"model_type": "qwen3",
"hidden_size": 32,
"intermediate_size": 64,
"num_hidden_layers": 2,
"num_attention_heads": 4,
"num_key_value_heads": 2,
"head_dim": 8,
"vocab_size": 128,
"max_position_embeddings": 512,
},
text_encoder_config={
"model_type": "t5gemma2_text",
"hidden_size": 24,
"intermediate_size": 48,
"num_hidden_layers": 6,
"num_attention_heads": 2,
"num_key_value_heads": 1,
"head_dim": 8,
"vocab_size": 256,
},
depth_decoder_config={
"model_type": "breeze_depth_decoder_model",
"hidden_size": 32,
"backbone_hidden_size": 32,
"audio_embed_size": 32,
"intermediate_size": 64,
"num_hidden_layers": 2,
"num_attention_heads": 4,
"num_key_value_heads": 2,
"head_dim": 8,
"num_codebooks": 16,
"vocab_size": 2051,
"rope_scaling": {
"rope_type": "llama3",
"factor": 32.0,
"low_freq_factor": 0.001953125,
"high_freq_factor": 0.0078125,
"original_max_position_embeddings": 16,
},
},
codec_config={"model_type": "mimi", "codebook_size": 2048, "num_quantizers": 32},
num_codebooks=16,
vocab_size=2051,
text_vocab_size=262158,
)
def test_nested_configs_and_text_config_use_qwen_backbone():
config = _config()
assert isinstance(config.backbone_config, Qwen3Config)
assert config.get_text_config() is config.backbone_config
assert config.get_text_config().vocab_size == 128
assert isinstance(config.text_encoder_config, BreezeTTS2TextEncoderConfig)
assert isinstance(config.depth_decoder_config, BreezeTTS2DepthDecoderConfig)
assert config.num_codebooks == config.depth_decoder_config.num_codebooks == 16
assert config.codec_config["codebook_size"] == 2048
def test_transformers_five_roundtrip_preserves_nested_configs_and_rope():
config = _config()
roundtrip = BreezeTTS2Config(**dict(config.to_dict()))
assert isinstance(roundtrip.backbone_config, Qwen3Config)
assert isinstance(roundtrip.text_encoder_config, BreezeTTS2TextEncoderConfig)
assert isinstance(roundtrip.depth_decoder_config, BreezeTTS2DepthDecoderConfig)
assert roundtrip.depth_decoder_config.rope_parameters["rope_theta"] == 500_000.0
def test_nested_autoconfig_registration_is_idempotent():
import importlib
module = importlib.import_module("vllm_omni.model_executor.models.breeze_tts_2.configuration_breeze_tts_2")
importlib.reload(module)
assert module.BreezeTTS2Config.model_type == "breeze"
def test_text_encoder_defaults_to_upstream_layer_pattern():
config = BreezeTTS2TextEncoderConfig(num_hidden_layers=6)
assert config.layer_types == [
"sliding_attention",
"sliding_attention",
"sliding_attention",
"sliding_attention",
"sliding_attention",
"full_attention",
]
def test_invalid_codebook_layout_is_rejected():
with pytest.raises(ValueError, match="positive"):
BreezeTTS2DepthDecoderConfig(num_codebooks=0)
@@ -1,157 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from types import SimpleNamespace
import pytest
from vllm import SamplingParams
from vllm_omni.entrypoints.openai.tts_adapters.breeze_tts_2 import BreezeTTS2Adapter
from vllm_omni.model_executor.models.breeze_tts_2.prompt_builder import (
BreezeTTS2PromptBuilder,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class _Tokenizer:
unk_token_id = -1
def convert_tokens_to_ids(self, token):
return {"<|AUDIO|>": 262144, "<|audio_eos|>": 262145}.get(token, self.unk_token_id)
def __call__(self, texts, *, add_special_tokens=True, padding=False):
if isinstance(texts, str):
texts = [texts]
ids = [[1000 + ord(char) % 1000 for char in text] for text in texts]
if add_special_tokens:
ids = [[2, *row, 1] for row in ids]
return {"input_ids": ids}
def decode(self, ids, *, skip_special_tokens=False):
del skip_special_tokens
return "".join(chr(int(token_id) - 1000) for token_id in ids if token_id not in (1, 2))
def test_instruction_prompt_remains_one_independent_text_segment():
builder = BreezeTTS2PromptBuilder(
_Tokenizer(),
SimpleNamespace(
audio_token_id=262144,
audio_eos_token_id=262145,
pad_token_id=0,
backbone_config=SimpleNamespace(vocab_size=100000),
num_codebooks=16,
codec_config={"codebook_size": 2048},
),
)
prompt = builder.build(
{
"template": "tts_instruction",
"speaker": "S2",
"instruction": "Use a bright and friendly delivery.",
"text": "Hello from Breeze.",
}
)
info = prompt["additional_information"]
assert info["template"] == "tts_instruction"
assert "input_values" not in info
assert bool(info["text_ids_mask"].all())
assert info["text_ids_len"].tolist() == [len(info["prompt_ids"])]
def _request(extra_params=None, **overrides):
base = dict(
input="Hello.",
ref_text=None,
ref_audio=None,
task_type=None,
speed=1.0,
max_new_tokens=None,
language=None,
speaker_embedding=None,
x_vector_only_mode=None,
extra_params=extra_params or {},
)
base.update(overrides)
return SimpleNamespace(**base)
def _adapter() -> BreezeTTS2Adapter:
adapter = object.__new__(BreezeTTS2Adapter)
adapter.ctx = SimpleNamespace(server=SimpleNamespace())
return adapter
def test_cfg_scale_one_is_explicitly_supported_without_companion_request():
adapter = _adapter()
assert adapter.validate(_request({"guidance_scale": 1.0})) is None
assert adapter.validate(_request({"cfg_scale": 1.0})) is None
def test_non_unit_cfg_is_rejected_until_negative_branch_exists():
adapter = _adapter()
assert "guidance_scale=1.0" in adapter.validate(_request({"guidance_scale": 2.0}))
assert "guidance_scale=1.0" in adapter.validate(_request({"cfg_scale": 0.5}))
assert "negative_prompt" in adapter.validate(_request({"negative_prompt": "noisy"}))
def test_unsupported_conditioning_fields_are_rejected_not_ignored():
adapter = _adapter()
assert "language" in adapter.validate(_request(language="en"))
assert "speaker_embedding" in adapter.validate(_request(speaker_embedding=[0.1, 0.2]))
assert "x_vector_only_mode" in adapter.validate(_request(x_vector_only_mode=True))
# Absent fields must keep the request valid.
assert adapter.validate(_request()) is None
assert adapter.validate(_request(x_vector_only_mode=False)) is None
def test_multiple_reference_clips_are_rejected_not_silently_truncated():
adapter = _adapter()
adapter.ctx.server._validate_ref_audio_format = lambda _uri: None
error = adapter.validate(_request(ref_audio=["a.wav", "b.wav"], ref_text="hello"))
assert "exactly one reference clip" in error
assert "2" in error
# A single-element list stays valid and reaches format validation.
assert adapter.validate(_request(ref_audio=["a.wav"], ref_text="hello")) is None
def test_non_greedy_sampling_overrides_are_rejected():
adapter = _adapter()
# Greedy values, including the deploy-config defaults, stay valid.
assert adapter.validate(_request({"temperature": 0.0, "top_p": 1.0, "top_k": -1})) is None
assert adapter.validate(_request({"temperature": 0})) is None
# The talker ignores the sampled code0, so any non-greedy override must be
# refused instead of silently producing greedy audio.
assert "greedy" in adapter.validate(_request({"temperature": 0.7}))
assert "greedy" in adapter.validate(_request({"top_p": 0.9}))
assert "greedy" in adapter.validate(_request({"top_k": 50}))
assert "temperature must be a number" in adapter.validate(_request({"temperature": "warm"}))
@pytest.mark.parametrize("value", [0, -1, float("nan"), float("inf"), "invalid"])
def test_invalid_repetition_penalties_are_rejected(value):
assert "repetition_penalty" in _adapter().validate(_request({"repetition_penalty": value}))
@pytest.mark.parametrize("value", [0.9, 1.0, 1.1, "1.2"])
@pytest.mark.parametrize("max_new_tokens", [None, 32])
def test_repetition_penalty_override_reaches_standard_sampling_metadata(value, max_new_tokens):
adapter = _adapter()
request = _request({"repetition_penalty": value}, max_new_tokens=max_new_tokens)
original = [SamplingParams(max_tokens=128, repetition_penalty=1.1)]
assert adapter.validate(request) is None
updated = adapter.apply_sampling_overrides(original, request)
assert updated[0].repetition_penalty == float(value)
assert updated[0].max_tokens == (128 if max_new_tokens is None else max_new_tokens)
assert original[0].repetition_penalty == 1.1
assert original[0].max_tokens == 128
@@ -1,152 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import pytest
import torch
from vllm_omni.model_executor.models.breeze_tts_2.configuration_breeze_tts_2 import (
BreezeTTS2DepthDecoderConfig,
BreezeTTS2TextEncoderConfig,
)
from vllm_omni.model_executor.models.breeze_tts_2.modeling_breeze_tts_2_depth import (
BreezeDepthDecoderForCausalLM,
)
from vllm_omni.model_executor.models.breeze_tts_2.modeling_breeze_tts_2_text import (
BreezeTTS2TextEncoder,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _depth_config() -> BreezeTTS2DepthDecoderConfig:
return BreezeTTS2DepthDecoderConfig(
hidden_size=16,
backbone_hidden_size=16,
audio_embed_size=16,
intermediate_size=32,
num_hidden_layers=1,
num_attention_heads=4,
num_key_value_heads=2,
head_dim=4,
max_position_embeddings=8,
num_codebooks=4,
vocab_size=8,
rope_scaling={
"rope_type": "llama3",
"rope_theta": 100.0,
"factor": 1.0,
"low_freq_factor": 0.5,
"high_freq_factor": 1.0,
"original_max_position_embeddings": 4,
},
)
def test_depth_decoder_position_specific_logits_and_offsets():
config = _depth_config()
decoder = BreezeDepthDecoderForCausalLM(config)
output = decoder(
input_ids=torch.tensor([[0, 2]]),
backbone_last_hidden_state=torch.randn(1, 16),
use_cache=False,
)
assert output.logits.shape == (1, 1, 8)
with torch.no_grad():
embedding = decoder.model.embed_tokens.weight
expected_first = embedding[2] @ decoder.model.inputs_embeds_projector.weight.T
# The first hidden row is replaced by the backbone state, so compare the
# second input row (codebook 0, id 2 + 0 * vocab_size).
actual_input = decoder.model.embed_tokens(torch.tensor([[2]]))[0, 0]
actual_projected = actual_input @ decoder.model.inputs_embeds_projector.weight.T
assert torch.allclose(expected_first, actual_projected)
def _text_config() -> BreezeTTS2TextEncoderConfig:
return BreezeTTS2TextEncoderConfig(
hidden_size=16,
intermediate_size=32,
num_hidden_layers=2,
num_attention_heads=2,
num_key_value_heads=1,
head_dim=8,
vocab_size=32,
layer_types=["sliding_attention", "full_attention"],
rope_parameters={
"sliding_attention": {"rope_type": "default", "rope_theta": 100.0},
"full_attention": {"rope_type": "default", "rope_theta": 100.0},
},
)
def test_text_encoder_masks_right_padding_independently(monkeypatch):
import vllm_omni.model_executor.models.breeze_tts_2.modeling_breeze_tts_2_text as text_module
class _ParallelEmbedding(torch.nn.Embedding):
def __init__(self, num_embeddings: int, embedding_dim: int):
super().__init__(num_embeddings, embedding_dim)
class _QKVLinear(torch.nn.Module):
def __init__(
self,
hidden_size: int,
head_size: int,
total_num_heads: int,
total_num_kv_heads: int,
**_: object,
):
super().__init__()
output_size = (total_num_heads + 2 * total_num_kv_heads) * head_size
self.linear = torch.nn.Linear(hidden_size, output_size, bias=False)
def forward(self, hidden_states: torch.Tensor) -> tuple[torch.Tensor, None]:
return self.linear(hidden_states), None
class _RowLinear(torch.nn.Module):
def __init__(self, input_size: int, output_size: int, **_: object):
super().__init__()
self.linear = torch.nn.Linear(input_size, output_size, bias=False)
def forward(self, hidden_states: torch.Tensor) -> tuple[torch.Tensor, None]:
return self.linear(hidden_states), None
class _MergedLinear(torch.nn.Module):
def __init__(self, input_size: int, output_sizes: list[int], **_: object):
super().__init__()
self.linear = torch.nn.Linear(input_size, sum(output_sizes), bias=False)
def forward(self, hidden_states: torch.Tensor) -> tuple[torch.Tensor, None]:
return self.linear(hidden_states), None
monkeypatch.setattr(text_module, "get_tensor_model_parallel_world_size", lambda: 1)
monkeypatch.setattr(text_module, "VocabParallelEmbedding", _ParallelEmbedding)
monkeypatch.setattr(text_module, "QKVParallelLinear", _QKVLinear)
monkeypatch.setattr(text_module, "RowParallelLinear", _RowLinear)
monkeypatch.setattr(text_module, "MergedColumnParallelLinear", _MergedLinear)
config = _text_config()
encoder = BreezeTTS2TextEncoder(config).eval()
ids = torch.tensor([[3, 4]])
padded_ids = torch.tensor([[3, 4, 0, 0]])
mask = torch.tensor([[True, True, False, False]])
with torch.inference_mode():
short = encoder(ids)
padded = encoder(padded_ids, attention_mask=mask)
assert short.shape == (1, 2, 16)
assert padded.shape == (1, 4, 16)
assert torch.allclose(short[0, :2], padded[0, :2], atol=1e-5)
def test_text_encoder_weight_loader_reports_fused_target_names():
encoder = object.__new__(BreezeTTS2TextEncoder)
parameter = torch.nn.Parameter(torch.zeros(2, 2))
parameter.weight_loader = lambda param, tensor, shard_id: param.data.copy_(tensor)
encoder.__dict__["named_parameters"] = lambda: [("layers.0.qkv_proj.weight", parameter)]
loaded = encoder.load_weights([("layers.0.q_proj.weight", torch.ones(2, 2))])
assert loaded == {"layers.0.qkv_proj.weight"}
assert torch.equal(parameter.detach(), torch.ones(2, 2))
@@ -1,28 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import pytest
from vllm.model_executor.models.registry import ModelRegistry
from vllm_omni.model_executor.models.registry import OmniModelRegistry
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_breeze_architectures_resolve_to_omni_classes():
assert "BreezeForConditionalGeneration" in OmniModelRegistry.get_supported_archs()
assert "BreezeTTS2MimiCodec" in OmniModelRegistry.get_supported_archs()
talker = OmniModelRegistry._try_load_model_cls("BreezeForConditionalGeneration")
codec = OmniModelRegistry._try_load_model_cls("BreezeTTS2MimiCodec")
assert talker.__name__ == "BreezeTTS2TalkerForGeneration"
assert codec.__name__ == "BreezeTTS2MimiCodec"
def test_plugin_registration_publishes_architectures_to_upstream_registry():
from vllm_omni.engine.arg_utils import register_omni_models_to_vllm
register_omni_models_to_vllm()
assert "BreezeForConditionalGeneration" in ModelRegistry.get_supported_archs()
assert "BreezeTTS2MimiCodec" in ModelRegistry.get_supported_archs()
@@ -1,36 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from pathlib import Path
import pytest
import yaml
from vllm_omni.config.pipeline_registry import OMNI_PIPELINES
from vllm_omni.model_executor.models.breeze_tts_2.pipeline import BREEZE_TTS_2_PIPELINE
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_breeze_pipeline_is_registered_with_talker_and_codec_stages():
assert OMNI_PIPELINES["breeze"] is BREEZE_TTS_2_PIPELINE
assert [stage.model_stage for stage in BREEZE_TTS_2_PIPELINE.stages] == [
"breeze_tts_2",
"breeze_tts_2_codec",
]
assert BREEZE_TTS_2_PIPELINE.stages[1].requires_full_payload_input is True
assert BREEZE_TTS_2_PIPELINE.stages[0].async_chunk_process_next_stage_input_func.endswith(
"talker2codec_async_chunk"
)
assert BREEZE_TTS_2_PIPELINE.stages[1].retains_state_across_chunks is True
def test_breeze_default_deploy_is_streaming():
deploy_path = Path(__file__).parents[4] / "vllm_omni" / "deploy" / "breeze_tts_2.yaml"
config = yaml.safe_load(deploy_path.read_text(encoding="utf-8"))
assert config["async_chunk"] is True
assert len(config["stages"]) == 2
# Stage 0 must prefill each prompt in one step: the talker builds a codec
# frame from the last hidden row of every scheduled request.
assert config["stages"][0]["enable_chunked_prefill"] is False
assert config["connectors"]["connector_of_shared_memory"]["extra"]["breeze_codec_chunk_frames"] == 8
@@ -1,189 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from types import SimpleNamespace
import pytest
import torch
from vllm_omni.model_executor.models.breeze_tts_2.prompt_builder import (
BreezeTTS2PromptBuilder,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class _Tokenizer:
pad_token_id = 0
eos_token_id = 1
unk_token_id = -1
_special = {
"<|AUDIO|>": 262144,
"<|audio_eos|>": 262145,
}
def convert_tokens_to_ids(self, token):
return self._special.get(token, self.unk_token_id)
def __call__(self, texts, *, add_special_tokens=True, padding=False):
if isinstance(texts, str):
texts = [texts]
ids = []
for text in texts:
values = [1000 + ord(char) % 1000 for char in text]
if add_special_tokens:
values = [2, *values, 1]
ids.append(values)
return {"input_ids": ids}
def decode(self, ids, *, skip_special_tokens=False):
del skip_special_tokens
rendered = []
for token_id in ids:
if token_id == 262144:
rendered.append("<|AUDIO|>")
elif token_id == 262145:
rendered.append("<|audio_eos|>")
elif token_id in (2, 1):
continue
else:
rendered.append(chr(int(token_id) - 1000))
return "".join(rendered)
class _RenderingTokenizer(_Tokenizer):
"""Tiny tokenizer that preserves Breeze placeholders after decode."""
def __call__(self, texts, *, add_special_tokens=True, padding=False):
if isinstance(texts, str):
texts = [texts]
ids = []
for text in texts:
row = []
cursor = 0
while cursor < len(text):
if text.startswith("<|AUDIO|>", cursor):
row.append(262144)
cursor += len("<|AUDIO|>")
elif text.startswith("<|audio_eos|>", cursor):
row.append(262145)
cursor += len("<|audio_eos|>")
else:
row.append(1000 + ord(text[cursor]) % 1000)
cursor += 1
if add_special_tokens:
row = [2, *row, 1]
ids.append(row)
return {"input_ids": ids}
class _ReferenceEncoder:
def __init__(self):
self.calls = []
def encode(self, audio, sample_rate=None):
self.calls.append((audio, sample_rate))
return torch.arange(32, dtype=torch.int16).reshape(2, 16)
def _config():
return SimpleNamespace(
audio_token_id=262144,
audio_eos_token_id=262145,
pad_token_id=0,
backbone_config=SimpleNamespace(vocab_size=100000),
num_codebooks=16,
codec_config={"codebook_size": 2048},
)
def test_builder_accepts_missing_codec_config():
config = _config()
config.codec_config = None
builder = BreezeTTS2PromptBuilder(_Tokenizer(), config)
assert builder.codebook_size == 2048
def test_build_instruction_prompt():
builder = BreezeTTS2PromptBuilder(_Tokenizer(), _config())
prompt = builder.build({"text": "hello", "instruction": "happy", "speaker": "S0"})
assert prompt["prompt_token_ids"]
info = prompt["additional_information"]
assert "input_values" not in info
assert set(prompt["prompt_token_ids"]) == {0}
assert info["prompt_ids"].numel() == len(prompt["prompt_token_ids"])
assert info["text_ids_mask"].dtype == torch.bool
assert info["text_ids_len"].tolist() == [len(prompt["prompt_token_ids"])]
assert bool(info["text_ids_mask"].all())
def test_build_reference_prompt_and_reuse_encoder():
encoder = _ReferenceEncoder()
builder = BreezeTTS2PromptBuilder(_RenderingTokenizer(), _config(), encoder)
prompt = builder.build(
{
"template": "ref_edit_tata",
"text": "target",
"instruction": "calm",
"ref_text": "reference",
"ref_audio": "ref.wav",
"ref_audio_sample_rate": 16000,
}
)
info = prompt["additional_information"]
codes = info["input_values"]
assert tuple(codes.shape) == (2, 16)
assert codes.dtype == torch.int16
assert set(prompt["prompt_token_ids"]) == {0}
assert info["prompt_ids"].tolist().count(262144) == 2
assert info["prompt_ids"].tolist().count(262145) == 1
assert int((~info["text_ids_mask"]).sum()) == 3
assert info["text_ids_len"].numel() == 2
assert encoder.calls == [("ref.wav", 16000)]
def test_build_reference_prompt_preserves_special_tokens_after_rendering():
encoder = _ReferenceEncoder()
builder = BreezeTTS2PromptBuilder(_RenderingTokenizer(), _config(), encoder)
prompt = builder.build(
{
"template": "ref_clone_tata",
"text": "target",
"ref_text": "reference",
"ref_audio": "ref.wav",
"ref_audio_sample_rate": 16000,
}
)
info = prompt["additional_information"]
prompt_ids = info["prompt_ids"].tolist()
assert prompt_ids.count(262144) == 2
assert prompt_ids.count(262145) == 1
assert int((~info["text_ids_mask"]).sum()) == 3
assert len(info["text_ids_len"]) == 2
assert sum(info["text_ids_len"].tolist()) == int(info["text_ids_mask"].sum())
def test_build_reference_prompt_accepts_preencoded_codes_mapping():
builder = BreezeTTS2PromptBuilder(_RenderingTokenizer(), _config())
codes = torch.arange(32, dtype=torch.int16).reshape(2, 16)
prompt = builder.build(
{
"template": "ref_clone_tata",
"text": "target",
"ref_text": "reference",
"codes": {"ref": codes},
}
)
info = prompt["additional_information"]
assert torch.equal(info["input_values"], codes)
assert info["ref_code_len"] == 2
@@ -1,187 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import importlib.util
from pathlib import Path
from types import SimpleNamespace
import numpy as np
import pytest
import soundfile as sf
import torch
from vllm_omni.model_executor.models.breeze_tts_2.prompt_builder import (
BreezeTTS2PromptBuilder,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
_EXAMPLE_PATH = (
Path(__file__).resolve().parents[4]
/ "examples"
/ "offline_inference"
/ "text_to_speech"
/ "breeze_tts_2"
/ "end2end.py"
)
class _Tokenizer:
unk_token_id = -1
def convert_tokens_to_ids(self, token):
return {
"<|AUDIO|>": 262144,
"<|audio_eos|>": 262145,
}.get(token, self.unk_token_id)
def __call__(self, texts, *, add_special_tokens=True, padding=False):
if isinstance(texts, str):
texts = [texts]
ids = [[1000 + ord(char) % 1000 for char in text] for text in texts]
for row_index, text in enumerate(texts):
if "<|AUDIO|>" in text or "<|audio_eos|>" in text:
cursor = 0
row = []
while cursor < len(text):
if text.startswith("<|AUDIO|>", cursor):
row.append(262144)
cursor += len("<|AUDIO|>")
elif text.startswith("<|audio_eos|>", cursor):
row.append(262145)
cursor += len("<|audio_eos|>")
else:
row.append(1000 + ord(text[cursor]) % 1000)
cursor += 1
ids[row_index] = row
if add_special_tokens:
ids = [[2, *row, 1] for row in ids]
return {"input_ids": ids}
def decode(self, ids, *, skip_special_tokens=False):
del skip_special_tokens
rendered = []
for token_id in ids:
if token_id == 262144:
rendered.append("<|AUDIO|>")
elif token_id == 262145:
rendered.append("<|audio_eos|>")
elif token_id in (1, 2):
continue
else:
rendered.append(chr(int(token_id) - 1000))
return "".join(rendered)
class _ReferenceEncoder:
def __init__(self) -> None:
self.calls: list[tuple[object, int | None]] = []
def encode(self, audio, sample_rate=None):
self.calls.append((audio, sample_rate))
return torch.arange(32, dtype=torch.int16).reshape(2, 16)
def _builder() -> tuple[BreezeTTS2PromptBuilder, _ReferenceEncoder]:
encoder = _ReferenceEncoder()
builder = BreezeTTS2PromptBuilder(
_Tokenizer(),
SimpleNamespace(
audio_token_id=262144,
audio_eos_token_id=262145,
pad_token_id=0,
backbone_config=SimpleNamespace(vocab_size=100000),
num_codebooks=16,
codec_config={"codebook_size": 2048},
),
encoder,
)
return builder, encoder
def test_reference_voice_uses_exact_transcript_and_audio_codes():
builder, encoder = _builder()
prompt = builder.build(
{
"template": "ref_clone_tata",
"speaker": "S1",
"ref_text": "exact reference transcript",
"ref_audio": "reference.wav",
"ref_audio_sample_rate": 24_000,
"text": "new target text",
}
)
info = prompt["additional_information"]
assert tuple(info["input_values"].shape) == (2, 16)
assert info["ref_code_len"] == 2
assert info["prompt_ids"].tolist().count(262144) == 2
assert info["text_ids_len"].numel() == 2
assert int(info["text_ids_mask"].sum()) == int(info["text_ids_len"].sum())
assert encoder.calls == [("reference.wav", 24_000)]
def test_reference_plus_instruction_composes_voice_direction_prompt():
builder, _ = _builder()
prompt = builder.build(
{
"template": "ref_edit_tata",
"speaker": "S0",
"ref_text": "exact reference transcript",
"ref_audio": "reference.wav",
"instruction": "speak calmly and warmly",
"text": "new target text",
}
)
info = prompt["additional_information"]
assert info["template"] == "ref_edit_tata"
assert tuple(info["input_values"].shape) == (2, 16)
assert info["prompt_ids"].tolist().count(262144) == 2
assert info["text_ids_len"].numel() == 2
def test_offline_example_reference_payload_feeds_prompt_builder(tmp_path):
"""Smoke: the offline example's payload keys flow into the real builder.
Loads ``end2end.py`` (voxtral-style importlib), reads a real stereo wav
through its ``reference_payload`` helper, and feeds the result into the
real ``BreezeTTS2PromptBuilder`` — guarding the example↔builder field
contract that previously broke via an unread ``ref_audio_path`` key.
"""
spec = importlib.util.spec_from_file_location("breeze_tts_2_end2end", _EXAMPLE_PATH)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
sample_rate = 16_000
t = np.arange(sample_rate, dtype=np.float32) / sample_rate
tone = np.sin(2 * np.pi * 220.0 * t)
ref_path = tmp_path / "reference.wav"
sf.write(str(ref_path), np.stack([tone, tone], axis=1), sample_rate)
payload = module.reference_payload(str(ref_path))
# The helper emits exactly the keys the prompt builder consumes.
assert set(payload) == {"ref_audio", "ref_audio_sample_rate"}
assert payload["ref_audio_sample_rate"] == sample_rate
assert payload["ref_audio"].ndim == 1 # stereo down-mixed to one clip
builder, encoder = _builder()
prompt = builder.build(
{
"template": "ref_clone_tata",
"speaker": "S0",
"ref_text": "exact reference transcript",
"text": "new target text",
**payload,
}
)
info = prompt["additional_information"]
assert tuple(info["input_values"].shape) == (2, 16)
assert info["ref_code_len"] == 2
assert info["prompt_ids"].tolist().count(262144) == 2
# The builder received the example's waveform and sample rate verbatim.
assert encoder.calls == [(payload["ref_audio"], sample_rate)]
@@ -1,122 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from typing import Any
import pytest
import torch
from vllm.v1.sample.logits_processor import LogitsProcessors
from vllm.v1.sample.metadata import SamplingMetadata
from tests.model_executor.models.breeze_tts_2.test_talker_state_machine import _depth_codes, _talker
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _model(first_score=11.0, second_score=10.5):
model = _talker()
model._async_chunk = True
logits = torch.full((1, 9), -100.0)
logits[0, 2:4] = torch.tensor([first_score, second_score])
model._main_head_logits = lambda hidden: logits.expand(hidden.shape[0], -1).clone()
model._generate_depth_codes = lambda hidden, code0: _depth_codes(model, hidden, code0)
return model
def _step(model, infos, penalties, *, no_penalties=False):
return model.make_omni_output(
torch.zeros(len(infos), 16),
model_intermediate_buffer=infos,
request_token_spans=[(i, i + 1) for i in range(len(infos))],
sampling_metadata=SamplingMetadata(
temperature=None,
all_greedy=True,
all_random=False,
top_p=None,
top_k=None,
generators={},
max_num_logprobs=None,
no_penalties=no_penalties,
frequency_penalties=torch.zeros(len(infos)),
presence_penalties=torch.zeros(len(infos)),
repetition_penalties=torch.tensor(penalties),
# Text/reference conditioning must never be counted as generated
# codec history, even if the scheduler ids overlap the codec ids.
prompt_token_ids=torch.tensor([[2, 2]] * len(infos)),
output_token_ids=[list(info.get("breeze_generated_code0_ids", [])) for info in infos],
allowed_token_ids_mask=None,
bad_words_token_ids={},
logitsprocs=LogitsProcessors(),
),
)
@pytest.mark.parametrize("scores", [(11.0, 10.5), (-1.0, -1.05)])
def test_penalty_applies_once_per_generated_token_and_respects_logit_sign(scores):
model = _model(*scores)
info = {"prompt_ids": torch.tensor([2]), "input_values": torch.tensor([[2, 3, 4, 5]])}
chosen = []
for _ in range(4):
output = _step(model, [info], [1.1])
chosen.append(output.multimodal_outputs["codes"]["audio"][0][0, 0].item())
assert chosen == [2, 3, 2, 2]
def test_request_history_and_penalty_are_independent():
model = _model()
first: dict[str, Any] = {}
second: dict[str, Any] = {}
_step(model, [first], [1.1])
output = _step(model, [first, second], [1.1, 1.1])
assert [codes[0, 0].item() for codes in output.multimodal_outputs["codes"]["audio"]] == [3, 2]
# Reverse the batch order; a request's history follows its state, while
# each penalty follows the current sampling-metadata row.
output = _step(model, [second, first], [1.1, 1.0])
assert [codes[0, 0].item() for codes in output.multimodal_outputs["codes"]["audio"]] == [3, 2]
def test_disabled_penalties_do_not_read_uninitialized_sampling_buffer():
model = _model()
info: dict[str, Any] = {}
for _ in range(2):
output = _step(model, [info], [float("nan")], no_penalties=True)
assert output.multimodal_outputs["codes"]["audio"][0][0, 0].item() == 2
def test_scheduler_logits_preserve_selected_frame_under_further_sampling_transforms():
model = _model()
info: dict[str, Any] = {}
_step(model, [info], [1.1])
output = _step(model, [info], [1.1])
selected = output.multimodal_outputs["codes"]["audio"][0][0, 0].item()
logits = model.compute_logits(torch.zeros(1, 16))
assert selected == 3
assert logits.argmax(dim=-1).item() == selected
assert torch.isfinite(logits).sum().item() == 1
assert logits[0, selected].item() == 0.0
# The scheduler may apply repetition again and rescale temperature. A
# finite zero for the selected token preserves a valid point mass.
for temperature in (0.1, 0.9, 2.0):
probabilities = torch.softmax(logits / 1.1 / temperature, dim=-1)
assert torch.isfinite(probabilities).all()
assert probabilities[0, selected].item() == 1.0
def test_repetition_penalty_can_select_eos_without_emitting_a_frame():
model = _model()
info: dict[str, Any] = {}
_step(model, [info], [1.1])
logits = torch.full((1, 9), -100.0)
logits[0, 2] = 11.0
logits[0, 8] = 10.5
model._main_head_logits = lambda hidden: logits.expand(hidden.shape[0], -1).clone()
output = _step(model, [info], [1.1])
assert output.multimodal_outputs["codes"]["audio"][0].numel() == 0
assert info["breeze_generated_frames"] == 1
assert model.compute_logits(torch.zeros(1, 16)).argmax(dim=-1).item() == 8
@@ -1,133 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from types import SimpleNamespace
import pytest
import torch
from vllm_omni.model_executor.stage_input_processors.breeze_tts_2 import (
talker2codec,
talker2codec_full_payload,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_talker2codec_flattens_codebook_major():
codes = torch.tensor([[1, 2], [3, 4], [5, 6]], dtype=torch.int16)
output = SimpleNamespace(multimodal_output={"codes": {"audio": codes}})
prompts = talker2codec([output])
assert prompts[0]["prompt_token_ids"] == [1, 3, 5, 2, 4, 6]
assert torch.equal(prompts[0]["multi_modal_data"]["codes"]["audio"], codes.transpose(0, 1))
def test_talker2codec_keeps_empty_request_terminal():
output = SimpleNamespace(multimodal_output={"codes": {}})
prompts = talker2codec([output])
assert prompts == [{"prompt_token_ids": []}]
def test_talker2codec_accepts_engine_core_output_wrapper():
codes = torch.tensor([[7, 8]], dtype=torch.int16)
output = SimpleNamespace(outputs=[SimpleNamespace(multimodal_output={"codes": {"audio": codes}})])
prompts = talker2codec([output])
assert prompts[0]["prompt_token_ids"] == [7, 8]
def test_talker2codec_unwraps_per_request_audio_container():
codes = torch.tensor([[7, 8], [9, 10]], dtype=torch.int16)
output = SimpleNamespace(multimodal_output={"codes": {"audio": [codes]}})
prompts = talker2codec([output])
assert prompts[0]["prompt_token_ids"] == [7, 9, 8, 10]
def test_talker2codec_accepts_flat_full_payload():
output = {"codes.audio": torch.tensor([1, 3, 2, 4], dtype=torch.int16)}
prompts = talker2codec([output])
assert prompts[0]["prompt_token_ids"] == [1, 3, 2, 4]
assert torch.equal(
prompts[0]["multi_modal_data"]["codes"]["audio"],
torch.tensor([1, 3, 2, 4], dtype=torch.int16),
)
def test_talker2codec_full_payload_transposes_accumulated_frames():
payload = talker2codec_full_payload(
transfer_manager=None,
pooling_output={"codes.audio": torch.tensor([[1, 2], [3, 4], [5, 6]])},
request=None,
)
assert payload is not None
assert torch.equal(
payload.codes.audio,
torch.tensor([1, 3, 5, 2, 4, 6]),
)
def test_talker2codec_full_payload_accepts_runner_pooling_output():
payload = talker2codec_full_payload(
pooling_output={"codes.audio": torch.tensor([[9, 8]], dtype=torch.int16)},
request=None,
)
assert payload is not None
assert torch.equal(payload.codes.audio, torch.tensor([9, 8]))
def test_full_payload_zero_frame_finish_sends_explicit_terminal_marker():
payload = talker2codec_full_payload(
pooling_output={"codes.audio": torch.empty(0, 16, dtype=torch.long)},
request=None,
is_finished=True,
)
assert payload is not None
assert payload.codes.audio.numel() == 0
assert payload.meta.code_flat_numel == 0
assert bool(payload.meta.finished.item()) is True
def test_full_payload_missing_codes_finish_sends_explicit_terminal_marker():
payload = talker2codec_full_payload(
pooling_output={},
request=None,
is_finished=True,
)
assert payload is not None
assert payload.codes.audio.numel() == 0
assert bool(payload.meta.finished.item()) is True
def test_full_payload_empty_without_finish_stays_silent():
payload = talker2codec_full_payload(
pooling_output={"codes.audio": torch.empty(0, 16, dtype=torch.long)},
request=None,
is_finished=False,
)
assert payload is None
def test_full_payload_carries_finished_flag_on_nonempty_frames():
payload = talker2codec_full_payload(
pooling_output={"codes.audio": torch.tensor([[1, 2], [3, 4]], dtype=torch.long)},
request=None,
is_finished=True,
)
assert payload is not None
assert payload.codes.audio.numel() == 4
assert bool(payload.meta.finished.item()) is True
@@ -1,243 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import copy
from collections import defaultdict
from types import SimpleNamespace
import pytest
import torch
from vllm_omni.model_executor.models.breeze_tts_2.modeling_breeze_tts_2_codec import (
BreezeTTS2MimiCodec,
)
from vllm_omni.model_executor.stage_input_processors.breeze_tts_2 import (
talker2codec_async_chunk,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class _TransferManager:
def __init__(self, chunk_frames: int = 4):
self.connector = SimpleNamespace(config={"breeze_codec_chunk_frames": chunk_frames})
self.code_prompt_token_ids: defaultdict[str, list[torch.Tensor]] = defaultdict(list)
class _Request:
def __init__(self, request_id: str, finished: bool = False):
self.external_req_id = request_id
self._finished = finished
def is_finished(self):
return self._finished
def test_async_processor_sends_only_unemitted_tail_and_flushes_finish():
manager = _TransferManager(chunk_frames=4)
request = _Request("req-1")
for code0 in range(3):
frame = torch.tensor([code0, code0 + 4, code0 + 8, code0 + 12], dtype=torch.long)
assert talker2codec_async_chunk(manager, {"codes": {"audio": frame}}, request) is None
payload = talker2codec_async_chunk(
manager,
{"codes": {"audio": torch.tensor([3, 7, 11, 15], dtype=torch.long)}},
request,
)
assert payload is not None
assert payload.codes.audio.tolist() == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
assert bool(payload.meta.finished.item()) is False
assert manager.code_prompt_token_ids["req-1"] == []
assert talker2codec_async_chunk(manager, {"codes": {"audio": torch.arange(4)}}, request) is None
payload = talker2codec_async_chunk(manager, None, _Request("req-1", finished=True))
assert payload.codes.audio.tolist() == [0, 1, 2, 3]
assert bool(payload.meta.finished.item()) is True
assert bool(payload.meta.stream_finished.item()) is True
assert payload.meta.codec_streaming is True
class _Decoder:
def __init__(self):
self.calls = []
def batched_chunked_decode(self, codes, lengths, caches=None, **kwargs):
self.calls.append((codes.shape, tuple(lengths), tuple(caches), kwargs))
return [torch.ones(length * 1920, dtype=torch.float32) for length in lengths]
def test_stateful_codec_batches_request_chunks_and_releases_finished_state():
decoder = _Decoder()
codec = object.__new__(BreezeTTS2MimiCodec)
codec._async_chunk = True
codec._num_codebooks = 4
codec._codebook_size = 8
codec._sample_rate = 24_000
codec._audio_tokenizer = SimpleNamespace(model=SimpleNamespace(decoder=decoder))
codec._decoder_state_cache = {}
input_ids = torch.tensor([0, 1, 2, 3, 4, 5, 6, 7], dtype=torch.long)
output = codec.forward(
input_ids,
runtime_additional_information=[
# ``stream_finished`` is the model-visible flag: the connector
# receiver strips ``meta.finished`` while merging runtime info.
{"meta": {"request_id": "live", "stream_finished": False}},
{"meta": {"request_id": "done", "stream_finished": True}},
],
seq_token_counts=[4, 4],
request_ids=["scheduler-live", "scheduler-done"],
)
assert decoder.calls[0][0] == (2, 4, 1)
assert [len(item) for item in output.multimodal_outputs["model_outputs"]] == [1920, 1920]
assert set(codec._decoder_state_cache) == {"scheduler-live"}
codec.on_requests_finished(["scheduler-live"])
assert codec._decoder_state_cache == {}
def test_stateful_codec_rejects_partial_frame_chunk():
decoder = _Decoder()
codec = object.__new__(BreezeTTS2MimiCodec)
codec._async_chunk = True
codec._num_codebooks = 4
codec._codebook_size = 8
codec._sample_rate = 24_000
codec._audio_tokenizer = SimpleNamespace(model=SimpleNamespace(decoder=decoder))
codec._decoder_state_cache = {}
# 5 codes cannot form a whole frame; a silent state reset would corrupt
# the rest of the stream, so the codec must fail loudly.
with pytest.raises(ValueError, match="not divisible"):
codec.forward(
torch.tensor([0, 1, 2, 3, 4], dtype=torch.long),
runtime_additional_information=[{"meta": {"request_id": "bad", "finished": False}}],
seq_token_counts=[5],
request_ids=["scheduler-bad"],
)
assert decoder.calls == []
@pytest.mark.parametrize("token_counts", [(4, 0), (0, 4), (0, 4, 0, 8, 0)])
def test_stateful_codec_terminal_marker_pops_state_without_decoding(token_counts):
decoder = _Decoder()
codec = object.__new__(BreezeTTS2MimiCodec)
codec._async_chunk = True
codec._num_codebooks = 4
codec._codebook_size = 8
codec._sample_rate = 24_000
codec._audio_tokenizer = SimpleNamespace(model=SimpleNamespace(decoder=decoder))
request_ids = [f"scheduler-{index}" for index in range(len(token_counts))]
codec._decoder_state_cache = {request_id: {} for request_id in request_ids}
output = codec.forward(
torch.arange(sum(token_counts), dtype=torch.long) % codec._codebook_size,
runtime_additional_information=[
# Plain ``finished`` covers the fallback for direct in-process
# callers that do not go through the connector receiver.
{"meta": {"finished": count == 0}}
for count in token_counts
],
seq_token_counts=token_counts,
request_ids=request_ids,
)
# Empty terminal markers release their state without consuming an output
# slot belonging to another request, regardless of their batch position.
assert len(decoder.calls) == 1
assert decoder.calls[0][0][0] == sum(count > 0 for count in token_counts)
assert [item.numel() for item in output.multimodal_outputs["model_outputs"]] == [
count // codec._num_codebooks * 1920 for count in token_counts
]
live_request_ids = [request_id for request_id, count in zip(request_ids, token_counts) if count]
assert list(codec._decoder_state_cache) == live_request_ids
assert all(codec._decoder_state_cache[request_id]["prefix_frames"] == 0 for request_id in live_request_ids)
@pytest.mark.parametrize("configured_chunk_frames", [None, 4])
def test_stateful_codec_advances_decoder_cache_after_sliding_window(monkeypatch, tmp_path, configured_chunk_frames):
from vllm_omni.model_executor.models.qwen3_tts.qwen3_tts_tokenizer import Qwen3TTSTokenizer
from vllm_omni.model_executor.models.qwen3_tts.tokenizer_12hz.configuration_qwen3_tts_tokenizer_v2 import (
Qwen3TTSTokenizerV2DecoderConfig,
)
from vllm_omni.model_executor.models.qwen3_tts.tokenizer_12hz.modeling_qwen3_tts_tokenizer_v2 import (
Qwen3TTSTokenizerV2Decoder,
)
torch.manual_seed(0)
config = Qwen3TTSTokenizerV2DecoderConfig(
codebook_size=32,
hidden_size=16,
latent_dim=16,
codebook_dim=16,
num_attention_heads=2,
num_key_value_heads=2,
intermediate_size=32,
num_hidden_layers=1,
num_quantizers=2,
decoder_dim=32,
upsample_rates=(2,),
upsampling_ratios=(2,),
sliding_window=72,
)
decoder = Qwen3TTSTokenizerV2Decoder(config).eval()
# Codebooks initialize to zeros before checkpoint loading; populate them
# so different codec IDs produce distinguishable cached frame histories.
with torch.no_grad():
for name, parameter in decoder.quantizer.named_parameters():
if name.endswith("embedding_sum"):
parameter.normal_()
tokenizer = SimpleNamespace(model=SimpleNamespace(decoder=decoder, get_output_sample_rate=lambda: 24_000))
monkeypatch.setattr(Qwen3TTSTokenizer, "from_pretrained", lambda *args, **kwargs: tokenizer)
connector_extra = {} if configured_chunk_frames is None else {"breeze_codec_chunk_frames": configured_chunk_frames}
chunk_frames = configured_chunk_frames or 8
codec = object.__new__(BreezeTTS2MimiCodec)
codec.vllm_config = SimpleNamespace(
model_config=SimpleNamespace(stage_connector_config={"extra": connector_extra}),
device_config=SimpleNamespace(device="cpu"),
)
codec._tokenizer_path = tmp_path / "audio_tokenizer"
codec._audio_tokenizer = None
codec._async_chunk = True
codec._num_codebooks = config.num_quantizers
codec._codebook_size = config.codebook_size
codec._decoder_state_cache = {}
codec.load_weights(iter(()))
# At least two chunks after the decoder's rolling window expose a stale
# cache even when the first padded chunk still produces plausible audio.
total_frames = config.sliding_window + 2 * chunk_frames
tail_frames = 3
codes = torch.randint(0, config.codebook_size, (1, config.num_quantizers, total_frames + tail_frames))
for end in range(chunk_frames, total_frames + 1, chunk_frames):
flat = codes[0, :, end - chunk_frames : end].reshape(-1)
codec.forward(
flat,
runtime_additional_information=[{"meta": {"stream_finished": False}}],
seq_token_counts=[flat.numel()],
request_ids=["scheduler-live"],
)
state = codec._decoder_state_cache["scheduler-live"]
assert state["suffix_frames"] == min(end - chunk_frames, config.sliding_window) + chunk_frames
with torch.inference_mode():
expected_quantized = decoder.quantizer.decode(codes[..., max(0, end - config.sliding_window) : end])
torch.testing.assert_close(state["suffix_quantized"], expected_quantized)
# A short terminal chunk must emit only its real frames, then release
# state. Its waveform should agree with the unpadded single-request path.
with torch.inference_mode():
expected_tail = decoder.chunked_decode(codes[..., -tail_frames:], caches=copy.deepcopy(state))
flat_tail = codes[0, :, -tail_frames:].reshape(-1)
output = codec.forward(
flat_tail,
runtime_additional_information=[{"meta": {"stream_finished": True}}],
seq_token_counts=[flat_tail.numel()],
request_ids=["scheduler-live"],
)
waveform = output.multimodal_outputs["model_outputs"][0]
assert waveform.numel() == tail_frames * decoder.total_upsample
torch.testing.assert_close(waveform, expected_tail.reshape(-1), atol=1e-5, rtol=1e-4)
assert codec._decoder_state_cache == {}
@@ -1,243 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from typing import Any
import pytest
import torch
from vllm_omni.model_executor.models.breeze_tts_2.modeling_breeze_tts_2_talker import (
BreezeTTS2TalkerForGeneration,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class _Head:
def __init__(self) -> None:
self.code0 = 2
def __call__(self, hidden: torch.Tensor) -> torch.Tensor:
logits = torch.zeros((hidden.shape[0], 9), dtype=hidden.dtype)
logits[:, self.code0] = 1.0
return logits
class _Embedding:
def __init__(self) -> None:
self.weight = torch.randn(4 * 8, 16)
def __call__(self, ids: torch.Tensor) -> torch.Tensor:
return self.weight[ids.reshape(-1)].reshape(*ids.shape, -1)
def _talker() -> BreezeTTS2TalkerForGeneration:
model = object.__new__(BreezeTTS2TalkerForGeneration)
model.num_codebooks = 4
model.codebook_vocab_size = 8
model.codebook_size = 8
model.hidden_size = 16
model.codebook_eos_token_id = 0
model.audio_token_id = 100
model.audio_eos_token_id = 101
# Bypass nn.Module registration; this fake is created with object.__new__.
model.__dict__["embed_audio_tokens"] = _Embedding()
model.audio_token_offsets = torch.arange(4) * 8
model.lm_head = _Head()
model.logits_processor = lambda head, hidden: head(hidden)
model._codec_disallowed_mask = None
model._batch_state = None
model._batch_state_spans = None
model._async_chunk = False
model._golden_dump_dir = None
return model
def _depth_codes(_model: Any, _hidden: torch.Tensor, code0: int) -> torch.Tensor:
if code0 >= 8:
return torch.empty(0, dtype=torch.long)
return torch.tensor([code0, code0 + 1, code0 + 2, code0 + 3], dtype=torch.long).unsqueeze(0)
def test_prefill_hidden_generates_and_stores_first_complete_frame():
model = _talker()
model._generate_depth_codes = lambda hidden, code0: _depth_codes(model, hidden, code0)
info = {"_omni_is_prefill": True, "breeze_generated_frames": 0, "breeze_max_new_frames": 3}
hidden = torch.arange(16, dtype=torch.float32).reshape(1, 16)
output = model.make_omni_output(
hidden,
model_intermediate_buffer=[info],
request_token_spans=[(0, 1)],
)
codes = output.multimodal_outputs["codes"]["audio"][0]
assert torch.equal(codes, torch.tensor([[2, 3, 4, 5]]))
assert torch.equal(info["breeze_current_frame"], codes[0])
assert info["breeze_generated_frames"] == 1
assert info["breeze_force_eos"] is False
def test_first_decode_consumes_full_frame_instead_of_scalar_eos_fallback():
model = _talker()
model._generate_depth_codes = lambda hidden, code0: _depth_codes(model, hidden, code0)
info = {"_omni_is_prefill": True, "breeze_generated_frames": 0, "breeze_max_new_frames": 3}
hidden = torch.arange(16, dtype=torch.float32).reshape(1, 16)
output = model.make_omni_output(
hidden,
model_intermediate_buffer=[info],
request_token_spans=[(0, 1)],
)
frame = output.multimodal_outputs["codes"]["audio"][0][0]
decode_info = dict(info)
decode_info.pop("_omni_is_prefill", None)
safe_id, decode_embeds, update = model.preprocess(
torch.tensor([2], dtype=torch.long),
None,
**decode_info,
)
assert safe_id.item() == 2
assert update == {}
expected = model.embed_input_ids(frame.reshape(1, 1, -1)).reshape(1, -1)
assert torch.equal(decode_embeds, expected)
def test_real_decode_rejects_missing_current_frame():
model = _talker()
info = {
"breeze_generated_frames": 1,
"breeze_audio_codes": torch.tensor([[2, 3, 4, 5]]),
}
try:
model.preprocess(torch.tensor([2], dtype=torch.long), None, **info)
except RuntimeError as exc:
assert "breeze_current_frame" in str(exc)
assert "codebooks 1..15" in str(exc)
else:
raise AssertionError("missing breeze_current_frame must fail explicitly")
def test_embed_input_ids_rejects_scalar_codec_id():
model = _talker()
try:
model.embed_input_ids(torch.tensor([[2]]))
except ValueError as exc:
assert "complete codec frames" in str(exc)
else:
raise AssertionError("scalar codec ids must not be embedded")
def test_decode_accumulates_frames_then_natural_eos_stops_without_fake_frame():
model = _talker()
model._generate_depth_codes = lambda hidden, code0: _depth_codes(model, hidden, code0)
info = {"breeze_generated_frames": 1, "breeze_max_new_frames": 3}
info["breeze_audio_codes"] = torch.tensor([[2, 3, 4, 5]])
model.lm_head.code0 = 3
hidden = torch.arange(16, dtype=torch.float32).reshape(1, 16)
output = model.make_omni_output(
hidden,
model_intermediate_buffer=[info],
request_token_spans=[(0, 1)],
)
assert output.multimodal_outputs["codes"]["audio"][0].shape == (2, 4)
# Main-head class 8 is the Breeze backbone EOS (one past vocab_size=8).
model.lm_head.code0 = 8
output = model.make_omni_output(
hidden,
model_intermediate_buffer=[info],
request_token_spans=[(0, 1)],
)
assert output.multimodal_outputs["codes"]["audio"][0].shape == (2, 4)
assert info["breeze_force_eos"] is True
assert info["breeze_force_eos"] is True
logits = model.compute_logits(hidden)
assert torch.equal(logits[0, :8], torch.full((8,), float("-inf")))
assert logits[0, 8].item() == 0.0
def test_frame_budget_forces_eos_on_followup_step():
model = _talker()
model._generate_depth_codes = lambda hidden, code0: _depth_codes(model, hidden, code0)
info = {"breeze_generated_frames": 2, "breeze_max_new_frames": 2}
info["breeze_audio_codes"] = torch.tensor([[1, 2, 3, 4], [5, 6, 7, 0]])
hidden = torch.arange(16, dtype=torch.float32).reshape(1, 16)
output = model.make_omni_output(
hidden,
model_intermediate_buffer=[info],
request_token_spans=[(0, 1)],
)
assert output.multimodal_outputs["codes"]["audio"][0].shape == (2, 4)
assert info["breeze_generated_frames"] == 2
assert info["breeze_force_eos"] is True
def test_async_chunk_mode_skips_full_accumulation():
model = _talker()
model._async_chunk = True
model._generate_depth_codes = lambda hidden, code0: _depth_codes(model, hidden, code0)
info = {"breeze_generated_frames": 0, "breeze_max_new_frames": 8}
hidden = torch.arange(16, dtype=torch.float32).reshape(1, 16)
for _ in range(3):
output = model.make_omni_output(
hidden,
model_intermediate_buffer=[info],
request_token_spans=[(0, 1)],
)
assert output.multimodal_outputs["codes"]["audio"][0].shape == (1, 4)
# Streaming keeps only the per-frame tail; the cumulative buffer (and its
# per-step torch.cat) must not exist without the opt-in golden dump.
assert "breeze_audio_codes" not in info
assert info["breeze_generated_frames"] == 3
assert torch.equal(info["breeze_current_frame"], torch.tensor([2, 3, 4, 5]))
def test_async_chunk_mode_keeps_accumulation_for_golden_dump(tmp_path):
model = _talker()
model._async_chunk = True
model._golden_dump_dir = str(tmp_path)
model._generate_depth_codes = lambda hidden, code0: _depth_codes(model, hidden, code0)
info = {"breeze_generated_frames": 0, "breeze_max_new_frames": 8}
hidden = torch.arange(16, dtype=torch.float32).reshape(1, 16)
for _ in range(3):
output = model.make_omni_output(
hidden,
model_intermediate_buffer=[info],
request_token_spans=[(0, 1)],
)
assert output.multimodal_outputs["codes"]["audio"][0].shape == (1, 4)
assert info["breeze_audio_codes"].shape == (3, 4)
assert torch.equal(info["breeze_audio_codes"][-1], torch.tensor([2, 3, 4, 5]))
def test_opt_in_golden_dump_writes_terminal_frames(tmp_path):
model = _talker()
model._golden_dump_dir = str(tmp_path)
info = {
"template": "tts_plain",
"prompt_ids": torch.tensor([1, 2, 3]),
"breeze_audio_codes": torch.tensor([[1, 2, 3, 4]]),
"breeze_generated_frames": 1,
"breeze_force_eos": True,
}
model._dump_golden_frames(info)
dump_path = next(tmp_path.glob("breeze_*.pt"))
payload = torch.load(dump_path, weights_only=False)
assert payload["template"] == "tts_plain"
assert torch.equal(payload["prompt_ids"], torch.tensor([1, 2, 3]))
assert torch.equal(payload["codes"], torch.tensor([[1, 2, 3, 4]]))
assert payload["finished"] is True
@@ -1,73 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from types import SimpleNamespace
import pytest
import torch
from torch import nn
from vllm_omni.model_executor.models.breeze_tts_2.modeling_breeze_tts_2_talker import (
BreezeTTS2TalkerForGeneration,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class _PaddedHead(nn.Module):
def __init__(self) -> None:
super().__init__()
self.weight = nn.Parameter(torch.zeros(2_112, 32), requires_grad=False)
class _DepthDecoder:
def __init__(self) -> None:
self.loaded: list[tuple[str, torch.Tensor]] = []
def load_weights(self, weights):
self.loaded.extend(weights)
return {"model.embed_tokens.weight"}
class _TalkerHost(nn.Module):
def __init__(self) -> None:
super().__init__()
self.config = SimpleNamespace(tie_codebooks_embeddings=True)
self.codebook_vocab_size = 2_051
self.hidden_size = 32
self.lm_head = _PaddedHead()
self.embed_audio_tokens = nn.Embedding(4 * 2_051, 32)
self.depth_decoder = _DepthDecoder()
def _padded_loader(parameter: nn.Parameter, weight: torch.Tensor) -> None:
assert parameter.shape != weight.shape
parameter[: weight.shape[0]].copy_(weight)
def test_lm_head_loader_accepts_vocab_padding():
host = _TalkerHost()
host.lm_head.weight.weight_loader = _padded_loader
checkpoint_weight = torch.full((2_052, 32), 1.5)
loaded = BreezeTTS2TalkerForGeneration.load_weights(
host,
iter([("lm_head.weight", checkpoint_weight)]),
)
assert "lm_head.weight" in loaded
assert torch.equal(host.lm_head.weight[:2_052], checkpoint_weight)
assert torch.count_nonzero(host.lm_head.weight[2_052:]).item() == 0
def test_tied_audio_embedding_reports_both_strict_loading_aliases():
host = _TalkerHost()
checkpoint_weight = torch.full((4 * 2_051, 32), 2.0)
loaded = BreezeTTS2TalkerForGeneration.load_weights(
host,
iter([("depth_decoder.model.embed_tokens.weight", checkpoint_weight)]),
)
assert "embed_audio_tokens.weight" in loaded
assert "depth_decoder.model.embed_tokens.weight" in loaded
@@ -0,0 +1,54 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Weight-free Breeze talker and request helpers shared by model tests."""
from dataclasses import dataclass
from unittest.mock import Mock
import torch
from vllm_omni.model_executor.models.breeze_tts_2.first_code_sampler import BreezeFirstCodeSampler
from vllm_omni.model_executor.models.breeze_tts_2.modeling_breeze import BreezeForConditionalGeneration
@dataclass
class TalkerConfig:
vocab_size: int
eos_token_id: int
@dataclass
class DepthStub:
generate_frames: Mock
def _small_talker():
model = BreezeForConditionalGeneration.__new__(BreezeForConditionalGeneration)
torch.nn.Module.__init__(model)
model._first_code_sampler = BreezeFirstCodeSampler()
model.config = TalkerConfig(vocab_size=8, eos_token_id=7)
model.num_codebooks, model.codebook_size, model.hidden_size = 3, 4, 2
model.lm_head = torch.nn.Linear(2, 8, bias=False)
with torch.no_grad():
model.lm_head.weight.zero_()
model.lm_head.weight[1] = torch.tensor([10.0, 0.0])
model.lm_head.weight[2] = torch.tensor([9.5, 0.0])
model.lm_head.weight[3] = torch.tensor([0.0, 10.0])
model.lm_head.weight[7] = torch.tensor([-10.0, -10.0])
model.depth_decoder = DepthStub(
generate_frames=Mock(side_effect=lambda hidden, first, **kwargs: first[:, None].repeat(1, 3))
)
return model
def _request_info(request_id):
return {
"global_request_id": [request_id],
"breeze_prompt": {"role": "cond", "guidance_scale": 1.0},
"breeze_sampling": {"temperature": 0.0, "top_k": 0, "top_p": 1.0, "repetition_penalty": 1.1},
"breeze_state": {
"generator": torch.Generator().manual_seed(42),
"history": torch.empty(1, 0, dtype=torch.long),
"current": torch.zeros(1, 3, dtype=torch.long),
},
}
@@ -23,7 +23,7 @@ class _FakeDecoder(nn.Module):
def __init__(self, total_upsample: int = _TOTAL_UPSAMPLE):
super().__init__()
self.total_upsample = total_upsample
self.decode_calls: list[dict[str, int | tuple[int, ...]]] = []
self.decode_calls: list[dict[str, object]] = []
self.batched_decode_calls: list[dict[str, object]] = []
self.decode_codes: list[torch.Tensor] = []
self.cudagraph_calls: list[dict[str, int | torch.device]] = []
@@ -91,8 +91,8 @@ class _FakeDecoder(nn.Module):
wav_len = frames * self.total_upsample + 6
wav = torch.arange(wav_len, dtype=torch.float32).view(1, 1, -1)
offsets = torch.arange(batch, dtype=torch.float32).view(batch, 1, 1) * 1000
batched_wav = wav.expand(batch, 1, wav_len) + offsets
return [batched_wav[row, :, : lengths[row] * self.total_upsample] for row in range(batch)]
batched_outputs = wav.expand(batch, 1, wav_len) + offsets
return [batched_outputs[row, :, : lengths[row] * self.total_upsample] for row in range(batch)]
def enable_cudagraph(self, **kwargs):
self.cudagraph_calls.append(kwargs)
@@ -577,6 +577,7 @@ def test_decode_chunking_override_is_passed_to_cudagraph():
_load_weights_noop(model)
assert model.decoder.cudagraph_calls[-1] == {
"capture_modes": ("icl", "xvec"),
"capture_batch_sizes": None,
"stateless_capture_sizes": None,
"device": torch.device("cuda"),
@@ -607,6 +608,13 @@ def test_cudagraph_batch_config_is_preserved():
assert call["capture_batch_sizes"] == [1, 2, 4, 8]
def test_cudagraph_captures_only_modes_used_by_the_model():
model = _make_model(async_chunk=True, device=torch.device("cuda"))
model.decoder_cudagraph_modes = ("xvec",)
_load_weights_noop(model)
assert model.decoder.cudagraph_calls[-1]["capture_modes"] == ("xvec",)
def test_stateless_cudagraph_capture_sizes_are_passed_from_config():
model = _make_model(
async_chunk=False,
@@ -0,0 +1,459 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Check depth attention against HF Llama and request-local sampling state."""
from collections import defaultdict
import numpy as np
import pytest
import torch
import torch.nn.functional as F
from transformers import LlamaConfig, MimiConfig
from transformers.models.llama.modeling_llama import LlamaModel
from transformers.models.mimi.modeling_mimi import MimiConv1d
from tests.model_executor.models.breeze_tts_2_helpers import _request_info, _small_talker
from vllm_omni.engine.serialization import deserialize_additional_information, serialize_additional_information
from vllm_omni.model_executor.models.breeze_tts_2.depth_decoder import (
BreezeDepthDecoder,
sample_graph_logits,
sample_logits,
)
from vllm_omni.model_executor.models.breeze_tts_2.prompt import CFG_UNCOND_SUFFIX, build_breeze_prompt
from vllm_omni.model_executor.models.breeze_tts_2.reference_encoder import BreezeReferenceConv
from vllm_omni.model_executor.stage_input_processors.breeze_tts_2 import expand_cfg_prompts, talker2code2wav_async_chunk
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@pytest.mark.parametrize("parameter", ["temperature", "top_p", "repetition_penalty"])
@pytest.mark.parametrize("value", [float("nan"), float("inf")])
def test_offline_prompt_rejects_nonfinite_sampling(mocker, parameter, value):
with pytest.raises(ValueError, match="sampling"):
build_breeze_prompt(mocker.Mock(), "Hello.", **{parameter: value})
@pytest.mark.parametrize("stride", [2, 3])
def test_reference_convolution_batch_preserves_partial_final_frames(stride):
torch.manual_seed(7)
config = MimiConfig(use_causal_conv=True, pad_mode="constant")
first = MimiConv1d(config, 1, 4, kernel_size=2 * stride, stride=stride)
last = MimiConv1d(config, 4, 4, kernel_size=4, stride=2, pad_mode="replicate", bias=False)
batched_first = BreezeReferenceConv(first, stride)
batched_last = BreezeReferenceConv(last, 2 * stride)
lengths = torch.tensor([37, 62])
batched_first.sample_lengths = batched_last.sample_lengths = lengths
waves = [torch.randn(1, 1, int(length)) for length in lengths]
padded = torch.cat([F.pad(wave, (0, 96 - wave.shape[-1])) for wave in waves])
actual = batched_last(batched_first(padded))
for row, waveform in enumerate(waves):
expected = last(first(waveform))
torch.testing.assert_close(actual[row : row + 1, :, : expected.shape[-1]], expected)
assert not actual[row, :, expected.shape[-1] :].count_nonzero()
def test_reference_conditioning_survives_transport_and_cfg_keeps_true_prompt_lengths(mocker) -> None:
tokenizer = mocker.Mock()
tokenizer.encode.side_effect = lambda text, **kwargs: list(text.encode("utf-8"))
waveform = np.zeros(1921, dtype=np.float32)
prompt = build_breeze_prompt(
tokenizer, "Hello.", "Speak happily.", ref_audio=(waveform, 24000), ref_text="Reference.", guidance_scale=4
)
companion = expand_cfg_prompts(prompt, None)[0].prompt
assert len(prompt["prompt_token_ids"]) > len(companion["prompt_token_ids"])
positive = deserialize_additional_information(serialize_additional_information(prompt["additional_information"]))
negative = deserialize_additional_information(serialize_additional_information(companion["additional_information"]))
assert positive["breeze_prompt"]["target_ids"] == list(b"[S0]<ins_bos>Speak happily.<ins_eos>Hello.")
assert negative["breeze_prompt"]["target_ids"] == list(b"[S0]Hello.")
assert positive["breeze_prompt"]["reference_ids"] == negative["breeze_prompt"]["reference_ids"]
assert positive["breeze_prompt"]["reference_frames"] == 2
assert positive["cfg_group"]["role"] == "cond"
assert negative["cfg_group"]["role"] == "uncond"
torch.testing.assert_close(positive["reference_waveform"], torch.from_numpy(waveform), atol=0, rtol=0)
torch.testing.assert_close(negative["reference_waveform"], positive["reference_waveform"], atol=0, rtol=0)
@pytest.fixture
def depth() -> BreezeDepthDecoder:
torch.manual_seed(42)
config = LlamaConfig(
hidden_size=32,
intermediate_size=64,
num_hidden_layers=2,
num_attention_heads=4,
num_key_value_heads=2,
head_dim=8,
vocab_size=35,
max_position_embeddings=16,
rope_theta=500000.0,
rope_scaling={
"rope_type": "llama3",
"factor": 32.0,
"low_freq_factor": 0.001953125,
"high_freq_factor": 0.0078125,
"original_max_position_embeddings": 16,
},
)
config.num_codebooks = 4
config.audio_embed_size = 48
config._attn_implementation = "eager"
model = BreezeDepthDecoder(config).eval()
torch.nn.init.normal_(model.codebooks_head, std=0.1)
return model
@pytest.fixture
def hf_depth(depth: BreezeDepthDecoder) -> LlamaModel:
reference = LlamaModel(depth.config).eval()
weights = {}
for name, parameter in depth.named_parameters():
if name.startswith("layers."):
if name.endswith("qkv.weight"):
layer = depth.layers[int(name.split(".")[1])]
for part, tensor in zip(
("q", "k", "v"), parameter.split([layer.q_size, layer.kv_size, layer.kv_size]), strict=True
):
weights[name.replace("qkv.weight", f"self_attn.{part}_proj.weight")] = tensor
elif name.endswith("gate_up.weight"):
for part, tensor in zip(("gate", "up"), parameter.chunk(2), strict=True):
weights[name.replace("gate_up.weight", f"mlp.{part}_proj.weight")] = tensor
else:
weights[name.replace(".o_proj.", ".self_attn.o_proj.").replace(".down_proj.", ".mlp.down_proj.")] = (
parameter
)
elif name == "norm.weight":
weights[name] = parameter
result = reference.load_state_dict(weights, strict=False)
assert result.missing_keys == ["embed_tokens.weight"]
assert result.unexpected_keys == []
return reference
@pytest.mark.parametrize("batch", [1, 2])
@torch.inference_mode()
def test_depth_cached_attention_matches_hf_full_sequence(
depth: BreezeDepthDecoder, hf_depth: LlamaModel, batch: int
) -> None:
"""Wrong cache positions or fused-QKV ordering change the reference hidden states."""
embeddings = torch.randn(batch, 4, depth.config.hidden_size)
expected = hf_depth(inputs_embeds=embeddings, use_cache=False).last_hidden_state
shape = (batch, 2, 4, 8)
caches = [(torch.zeros(shape), torch.zeros(shape)) for _ in depth.layers]
actual = []
for start, end in [(0, 2), (2, 3), (3, 4)]:
x = embeddings[:, start:end]
for layer, cache in zip(depth.layers, caches, strict=True):
x = layer(
x,
depth.positions[start:end],
cache,
depth.causal_mask[start:end, :4][None, None],
depth.rope_cos[start:end][None, None],
depth.rope_sin[start:end][None, None],
)
actual.append(depth.norm(x))
torch.testing.assert_close(torch.cat(actual, 1), expected, atol=2e-6, rtol=2e-5)
@pytest.mark.parametrize("guidance_scale", [0.5, 4.0])
@torch.inference_mode()
def test_cfg_depth_matches_hf_full_prefix_at_every_codebook(
depth: BreezeDepthDecoder, hf_depth: LlamaModel, guidance_scale: float
) -> None:
hidden = torch.randn(4, depth.config.audio_embed_size)
first = torch.tensor([7, 11])
prefix = torch.cat((hidden[:, None], depth.embed_tokens(first.repeat(2)[:, None])), dim=1)
expected = [first]
for codebook in range(depth.num_codebooks - 1):
states = hf_depth(inputs_embeds=depth.inputs_embeds_projector(prefix), use_cache=False).last_hidden_state
logits = F.linear(states[:, -1].float(), depth.codebooks_head[codebook].T)
cond, uncond = logits.chunk(2)
token = (uncond + guidance_scale * (cond - uncond))[:, : depth.vocab_size - 3].argmax(-1)
expected.append(token)
embed = depth.embed_tokens((token.repeat(2) + (codebook + 1) * depth.vocab_size)[:, None])
prefix = torch.cat((prefix, embed), dim=1)
actual = depth._generate_frame(
hidden, first, depth._allocate_cache(hidden), 0.0, 0, 1.0, torch.Generator(), guidance_scale=guidance_scale
)
torch.testing.assert_close(actual, torch.stack(expected, dim=-1), atol=0, rtol=0)
@torch.inference_mode()
def test_frame_generation_does_not_reuse_another_requests_cache(depth: BreezeDepthDecoder) -> None:
hidden = torch.randn(1, 48)
first = torch.tensor([7])
def generate(value: torch.Tensor) -> torch.Tensor:
return depth.generate_frame(
value, first, temperature=0.9, top_k=10, top_p=0.8, generator=torch.Generator().manual_seed(99)
)
expected = generate(hidden)
generate(hidden * -3)
torch.testing.assert_close(generate(hidden), expected, atol=0, rtol=0)
def test_top_k_keeps_tied_candidates_and_nucleus_keeps_crossing_token() -> None:
generator = torch.Generator().manual_seed(42)
logits = torch.tensor([[0.0, 0.0, -100.0]]).expand(128, -1)
sampled = sample_logits(logits, 1.0, 1, 1.0, generator)
assert set(sampled.tolist()) == {0, 1}
sampled = sample_logits(torch.tensor([[10.0, 0.0]]), 1.0, 0, 0.1, generator)
assert sampled.item() == 0
@pytest.mark.parametrize("parameters", [(0.0, 0, 1.0), (0.9, 50, 1.0), (0.7, 100, 0.8), (1.2, 0, 0.95)])
def test_graph_sampler_matches_scalar_filtering_with_identical_noise(parameters) -> None:
generator = torch.Generator().manual_seed(42)
logits = torch.randn(32, 2051, generator=generator)
logits[:, -3:] = -torch.inf
logits[:, 10:14] = 4.0
noise = torch.empty_like(logits).exponential_(generator=generator)
expected = sample_logits(logits, *parameters, generator, noise)
actual = sample_graph_logits(logits, torch.tensor(parameters), noise)
torch.testing.assert_close(actual, expected, atol=0, rtol=0)
@pytest.mark.parametrize("top_p", [0.25, 0.5, 0.75])
@pytest.mark.parametrize("top_k", [0, 2])
def test_graph_sampler_excludes_token_after_exact_nucleus_boundary(top_p, top_k) -> None:
# Equal probabilities make the CDF boundary exact in float32. Bias the
# shared noise toward the first token outside that nucleus so retaining
# one extra candidate deterministically changes the sampled token.
logits = torch.tensor([[0.0, 0.0, 0.0, 0.0, -torch.inf, -torch.inf, -torch.inf]])
noise = torch.ones_like(logits)
first_excluded = int(top_p * 4)
noise[:, first_excluded] = 0.001
parameters = (1.0, top_k, top_p)
expected = sample_logits(logits, *parameters, torch.Generator().manual_seed(42), noise)
actual = sample_graph_logits(logits, torch.tensor(parameters), noise)
assert expected.item() != first_excluded
torch.testing.assert_close(actual, expected, atol=0, rtol=0)
def test_codec_chunks_preserve_zero_frames_and_flush_each_request_once(mocker) -> None:
manager = mocker.Mock()
manager.connector.config = {"extra": {"codec_chunk_frames": 5, "initial_codec_chunk_frames": 5}}
manager.code_prompt_token_ids = defaultdict(list)
manager.request_payload = {}
first = mocker.Mock(external_req_id="first")
first.is_finished.return_value = False
second = mocker.Mock(external_req_id="second")
second.is_finished.return_value = False
zeros = {"codes": {"audio": torch.zeros(3, 16, dtype=torch.long)}}
assert talker2code2wav_async_chunk(manager, zeros, first) is None
assert talker2code2wav_async_chunk(manager, zeros, second) is None
codes = torch.arange(64).reshape(4, 16)
output = talker2code2wav_async_chunk(manager, {"codes": {"audio": codes}}, first)
expected = torch.cat((torch.zeros(3, 16, dtype=torch.long), codes))
torch.testing.assert_close(output.codes.audio.reshape(16, -1).T, expected)
final = talker2code2wav_async_chunk(manager, None, first, is_finished=True)
assert final.codes.audio.numel() == 0
assert final.meta.finished.item()
other = talker2code2wav_async_chunk(manager, None, second, is_finished=True)
torch.testing.assert_close(other.codes.audio, torch.zeros(48, dtype=torch.long))
def test_codec_chunk_ramp_keeps_frame_order_and_flushes_partial_tail(mocker) -> None:
manager = mocker.Mock()
manager.connector.config = {"extra": {"codec_chunk_frames": 5, "codec_chunk_ramp": [1, 2, 4, 5]}}
manager.code_prompt_token_ids = defaultdict(list)
manager.request_payload = {}
request = mocker.Mock(external_req_id="ramp")
request.is_finished.return_value = False
outputs = []
for index in range(15):
result = talker2code2wav_async_chunk(manager, {"codes": {"audio": torch.full((1, 16), index)}}, request)
if result is not None:
outputs.append(result.codes.audio.reshape(16, -1).T)
tail = talker2code2wav_async_chunk(manager, None, request, is_finished=True)
outputs.append(tail.codes.audio.reshape(16, -1).T)
assert [len(part) for part in outputs] == [1, 2, 4, 5, 3]
torch.testing.assert_close(torch.cat(outputs), torch.arange(15)[:, None].expand(-1, 16))
assert tail.meta.finished.item()
@torch.inference_mode()
def test_talker_history_and_generators_follow_requests_across_reordering_and_new_admission():
model = _small_talker()
first, second = _request_info("first"), _request_info("second")
# Unequal spans must select the last hidden row of each request.
model.make_omni_output(
torch.tensor([[0.0, 1.0], [0.0, 1.0], [1.0, 0.0], [0.0, 1.0]]),
model_intermediate_buffer=[first, second],
request_token_spans=[(0, 3), (3, 4)],
)
reordered = model.make_omni_output(
torch.tensor([[0.0, 1.0], [1.0, 0.0]]),
model_intermediate_buffer=[second, first],
request_token_spans=[(0, 1), (1, 2)],
)
assert [frame[0, 0].item() for frame in reordered.multimodal_outputs["codes"]["audio"]] == [3, 2]
assert first["breeze_state"]["history"].tolist() == [[1, 2]]
assert second["breeze_state"]["history"].tolist() == [[3, 3]]
generators = model.depth_decoder.generate_frames.call_args.kwargs["generators"]
assert generators[0] is second["breeze_state"]["generator"]
assert generators[1] is first["breeze_state"]["generator"]
newcomer = _request_info("newcomer")
admitted = model.make_omni_output(
torch.tensor([[1.0, 0.0], [1.0, 0.0]]),
model_intermediate_buffer=[newcomer, first],
request_token_spans=[(0, 1), (1, 2)],
)
assert [frame[0, 0].item() for frame in admitted.multimodal_outputs["codes"]["audio"]] == [1, 1]
assert newcomer["breeze_state"]["history"].tolist() == [[1]]
assert first["breeze_state"]["history"].tolist() == [[1, 2, 1]]
assert second["breeze_state"]["history"].tolist() == [[3, 3]]
@torch.inference_mode()
def test_talker_eos_does_not_shift_live_state_or_poison_the_next_batch():
model = _small_talker()
finished, live = _request_info("finished"), _request_info("live")
output = model.make_omni_output(
torch.tensor([[-1.0, -1.0], [0.0, 1.0]]),
model_intermediate_buffer=[finished, live],
request_token_spans=[(0, 1), (1, 2)],
)
assert output.multimodal_outputs["codes"]["audio"][0].numel() == 0
assert finished["breeze_state"]["history"].numel() == 0
assert live["breeze_state"]["current"].tolist() == [[3, 3, 3]]
assert model.compute_logits(torch.zeros(2, 2)).argmax(-1).tolist() == [7, 0]
model.make_omni_output(torch.tensor([[0.0, 1.0]]), model_intermediate_buffer=[live], request_token_spans=[(0, 1)])
assert live["breeze_state"]["history"].tolist() == [[3, 3]]
assert model.compute_logits(torch.zeros(1, 2)).argmax(-1).tolist() == [0]
@pytest.mark.parametrize("temperature", [0.0, 0.9])
@torch.inference_mode()
def test_talker_mixed_eos_and_cfg_matches_independent_request_state(temperature):
"""Batch EOS decisions must not mix CFG state or change a request's RNG stream."""
finished_companion = "finished_cfg" + CFG_UNCOND_SUFFIX
live_companion = "live_cfg" + CFG_UNCOND_SUFFIX
order = ["finished_cfg", "live_plain", finished_companion, "finished_plain", live_companion, "live_cfg"]
hidden_by_id = {
"finished_cfg": [-1.0, -1.0],
finished_companion: [-1.0, -1.0],
"finished_plain": [-1.0, -1.0],
"live_plain": [0.0, 1.0],
"live_cfg": [1.0, 0.0],
live_companion: [1.0, 0.0],
}
def make_infos():
infos = {request_id: _request_info(request_id) for request_id in order}
for index, (request_id, info) in enumerate(infos.items()):
info["breeze_sampling"].update(temperature=temperature, top_k=1)
if "cfg" not in request_id:
# Interleave distinct sampler groups while each request still
# matches its independently decoded state and RNG trajectory.
info["breeze_sampling"].update(temperature=temperature * 0.5, top_p=0.8)
info["breeze_state"]["generator"].manual_seed(42 + index)
info["breeze_state"]["history"] = torch.tensor([[3, 3]])
if "cfg" in request_id:
info["breeze_prompt"]["guidance_scale"] = 4.0
if request_id.endswith(CFG_UNCOND_SUFFIX):
info["breeze_prompt"]["role"] = "uncond"
return infos
def sample_depth(hidden, first, *, temperature, generators, **kwargs):
# Consume request-local RNG state in the downstream phase as well as
# codebook-0 sampling, without requiring depth CUDA graphs on CPU.
tail = (
torch.stack([torch.randint(0, 4, (2,), generator=generator) for generator in generators])
if temperature > 0
else first[:, None].expand(-1, 2)
)
return torch.cat((first[:, None], tail), dim=1)
batched, independent = _small_talker(), _small_talker()
batched.depth_decoder.generate_frames.side_effect = sample_depth
independent.depth_decoder.generate_frames.side_effect = sample_depth
batch_infos, independent_infos = make_infos(), make_infos()
initial_rng = {rid: info["breeze_state"]["generator"].get_state() for rid, info in batch_infos.items()}
saved_frames = []
# The second step drops both EOS requests and moves the live CFG companion
# ahead of its parent, exercising shrinking and reordered batches.
for step_ids in (order, [live_companion, "live_cfg", "live_plain"]):
expected_audio, expected_tokens = {}, {}
for request_id in step_ids:
if request_id.endswith(CFG_UNCOND_SUFFIX):
continue
pair = [request_id]
if independent_infos[request_id]["breeze_prompt"]["guidance_scale"] != 1.0:
pair.append(request_id + CFG_UNCOND_SUFFIX)
result = independent.make_omni_output(
torch.tensor([hidden_by_id[rid] for rid in pair]),
model_intermediate_buffer=[independent_infos[rid] for rid in pair],
request_token_spans=[(i, i + 1) for i in range(len(pair))],
)
tokens = independent.compute_logits(torch.zeros(len(pair), 2)).argmax(-1)
for index, rid in enumerate(pair):
expected_audio[rid] = result.multimodal_outputs["codes"]["audio"][index].clone()
expected_tokens[rid] = tokens[index]
result = batched.make_omni_output(
torch.tensor([hidden_by_id[rid] for rid in step_ids]),
model_intermediate_buffer=[batch_infos[rid] for rid in step_ids],
request_token_spans=[(i, i + 1) for i in range(len(step_ids))],
)
actual_tokens = batched.compute_logits(torch.zeros(len(step_ids), 2)).argmax(-1)
for index, rid in enumerate(step_ids):
actual = result.multimodal_outputs["codes"]["audio"][index]
torch.testing.assert_close(actual, expected_audio[rid], atol=0, rtol=0)
torch.testing.assert_close(actual_tokens[index], expected_tokens[rid], atol=0, rtol=0)
saved_frames.append((actual, actual.clone()))
for rid in order:
actual, expected = batch_infos[rid]["breeze_state"], independent_infos[rid]["breeze_state"]
for key in ("history", "current"):
torch.testing.assert_close(actual[key], expected[key], atol=0, rtol=0)
torch.testing.assert_close(actual["generator"].get_state(), expected["generator"].get_state())
# Unconditional companions never own sampling; an EOS request consumes
# only codebook-0 sampling, with no downstream depth draws.
for rid in (finished_companion, live_companion):
torch.testing.assert_close(batch_infos[rid]["breeze_state"]["generator"].get_state(), initial_rng[rid])
for rid in ("finished_cfg", "finished_plain"):
reference_generator = torch.Generator().set_state(initial_rng[rid])
sampling = batch_infos[rid]["breeze_sampling"]
sample_logits(
torch.zeros(1, batched.config.vocab_size),
sampling["temperature"],
sampling["top_k"],
sampling["top_p"],
reference_generator,
)
torch.testing.assert_close(
batch_infos[rid]["breeze_state"]["generator"].get_state(), reference_generator.get_state()
)
for rid in ("finished_cfg", finished_companion, "finished_plain", live_companion):
assert batch_infos[rid]["breeze_state"]["history"].tolist() == [[3, 3]]
assert batch_infos["live_cfg"]["breeze_state"]["history"].tolist() == [[3, 3, 1, 2]]
assert batch_infos["live_plain"]["breeze_state"]["history"].tolist() == [[3, 3, 3, 3]]
assert batch_infos["live_cfg"]["breeze_state"]["current"] is batch_infos[live_companion]["breeze_state"]["current"]
for original, snapshot in saved_frames:
torch.testing.assert_close(original, snapshot, atol=0, rtol=0)
@torch.inference_mode()
def test_talker_all_eos_preserves_history_and_skips_depth():
model = _small_talker()
infos = [_request_info("first"), _request_info("second")]
for info in infos:
info["breeze_state"]["history"] = torch.tensor([[1, 2]])
output = model.make_omni_output(
torch.tensor([[-1.0, -1.0], [-2.0, -2.0]]),
model_intermediate_buffer=infos,
request_token_spans=[(0, 1), (1, 2)],
)
model.depth_decoder.generate_frames.assert_not_called()
assert model.compute_logits(torch.zeros(2, 2)).argmax(-1).tolist() == [7, 7]
assert all(frame.numel() == 0 for frame in output.multimodal_outputs["codes"]["audio"])
for info in infos:
assert info["breeze_state"]["history"].tolist() == [[1, 2]]
assert info["breeze_state"]["current"].tolist() == [[0, 0, 0]]
@@ -0,0 +1,184 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Check only the caller's grouping seam, with the existing weight-free fixture."""
import pytest
import torch
import torch.nn.functional as F
from tests.model_executor.models.breeze_tts_2_helpers import _request_info, _small_talker
from vllm_omni.model_executor.models.breeze_tts_2.depth_decoder import sample_logits
from vllm_omni.model_executor.models.breeze_tts_2.prompt import CFG_UNCOND_SUFFIX
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@torch.inference_mode()
def test_first_code_groups_preserve_logits_request_order_cfg_and_eos(monkeypatch):
"""Grouping changes dispatch only; head/history arithmetic and routing stay exact."""
model = _small_talker()
cfg_a, cfg_b = "cfg_a" + CFG_UNCOND_SUFFIX, "cfg_b" + CFG_UNCOND_SUFFIX
order = ["plain_a", "plain_b", cfg_a, "finished_a", "cfg_a", "cfg_b", cfg_b, "plain_c"]
tuples = {"a": (0.9, 3, 1.0), "b": (0.7, 2, 0.8), "c": (0.0, 0, 1.0)}
chosen = {"plain_a": 1, "plain_b": 2, "finished_a": 7, "cfg_a": 3, "cfg_b": 0, "plain_c": 2}
hidden = {
"plain_a": [1.0, -0.5],
"plain_b": [-0.5, 1.0],
cfg_a: [0.25, 0.5],
"finished_a": [-1.0, -1.0],
"cfg_a": [0.75, -0.5],
"cfg_b": [0.5, 0.75],
cfg_b: [-0.25, 0.5],
"plain_c": [0.5, -0.75],
}
infos = {rid: _request_info(rid) for rid in order}
for index, (rid, info) in enumerate(infos.items()):
parent = rid.removesuffix(CFG_UNCOND_SUFFIX)
parameters = tuples[parent[-1]]
info["breeze_sampling"].update(
temperature=parameters[0],
top_k=parameters[1],
top_p=parameters[2],
repetition_penalty=2.0 if parent.startswith("cfg") else 1.25,
)
info["breeze_state"]["generator"].manual_seed(100 + index)
info["breeze_state"]["history"] = torch.tensor([[1, 3, 1]])
if parent.startswith("cfg"):
info["breeze_prompt"]["guidance_scale"] = 4.0 if parent == "cfg_a" else 2.0
if rid.endswith(CFG_UNCOND_SUFFIX):
info["breeze_prompt"]["role"] = "uncond"
original_states = {
rid: {key: value.clone() for key, value in info["breeze_state"].items() if key in ("history", "current")}
for rid, info in infos.items()
}
request_for_generator = {id(info["breeze_state"]["generator"]): rid for rid, info in infos.items()}
class RecordingSampler:
def __init__(self):
self.calls = []
def sample(self, logits, parameters, generators):
ids = [request_for_generator[id(generator)] for generator in generators]
self.calls.append((parameters, ids, [row.clone() for row in logits]))
return [torch.tensor([chosen[rid]], dtype=torch.long) for rid in ids]
recorder = RecordingSampler()
model._first_code_sampler = recorder
# Build the scalar pre-grouping reference before spying on actual head
# calls. Both positive and negative repeated logits exercise the penalty.
expected_logits = {}
for rid in chosen:
row = torch.tensor([hidden[rid]], dtype=torch.bfloat16).float()
scores = F.linear(row, model.lm_head.weight)
scale = infos[rid]["breeze_prompt"]["guidance_scale"]
if scale != 1.0:
uncond = F.linear(
torch.tensor([hidden[rid + CFG_UNCOND_SUFFIX]], dtype=torch.bfloat16).float(), model.lm_head.weight
)
scores = uncond + scale * (scores - uncond)
scores[:, model.codebook_size : model.config.eos_token_id] = -torch.inf
history = infos[rid]["breeze_state"]["history"]
selected = scores.gather(1, history)
penalty = infos[rid]["breeze_sampling"]["repetition_penalty"]
scores.scatter_(1, history, torch.where(selected < 0, selected * penalty, selected / penalty))
expected_logits[rid] = scores
actual_linear, head_inputs = F.linear, []
def scalar_head(inputs, weight, bias=None):
assert inputs.dtype == torch.float32 and inputs.shape == (1, model.hidden_size)
assert weight is model.lm_head.weight
head_inputs.append(inputs.clone())
return actual_linear(inputs, weight, bias)
# Prefill-like unequal spans must still select each physical row's last
# hidden state, including a CFG companion preceding its parent.
flat_hidden: list[list[float]] = []
spans: list[tuple[int, int]] = []
for index, rid in enumerate(order):
start = len(flat_hidden)
flat_hidden.extend([[-99.0, -99.0]] * (index % 3))
flat_hidden.append(hidden[rid])
spans.append((start, len(flat_hidden)))
values = torch.tensor(flat_hidden, dtype=torch.bfloat16)
with monkeypatch.context() as head_guard:
head_guard.setattr(F, "linear", scalar_head)
output = model.make_omni_output(
values, model_intermediate_buffer=[infos[rid] for rid in order], request_token_spans=spans
)
assert output.text_hidden_states is values
assert len(head_inputs) == len(chosen) + 2
assert [(parameters, ids) for parameters, ids, _ in recorder.calls] == [
(tuples["a"], ["plain_a", "finished_a", "cfg_a"]),
(tuples["b"], ["plain_b", "cfg_b"]),
(tuples["c"], ["plain_c"]),
]
for _, ids, rows in recorder.calls:
for rid, row in zip(ids, rows, strict=True):
assert row.dtype == torch.float32 and row.shape == (1, model.config.vocab_size)
torch.testing.assert_close(row, expected_logits[rid], atol=0, rtol=0)
# Depth group creation must follow original conditioned order, not the
# first-code sampler's grouped traversal (A,A,A,B,B,C).
live_order = ["plain_a", "plain_b", "cfg_a", "cfg_b", "plain_c"]
calls = model.depth_decoder.generate_frames.call_args_list
assert len(calls) == len(live_order)
for call, rid in zip(calls, live_order, strict=True):
depth_hidden, first = call.args
scale = infos[rid]["breeze_prompt"]["guidance_scale"]
expected_hidden = [hidden[rid]] + ([hidden[rid + CFG_UNCOND_SUFFIX]] if scale != 1 else [])
torch.testing.assert_close(depth_hidden, torch.tensor(expected_hidden, dtype=torch.bfloat16), atol=0, rtol=0)
assert first.tolist() == [chosen[rid]]
assert call.kwargs["generators"] == [infos[rid]["breeze_state"]["generator"]]
assert call.kwargs["guidance_scale"] == scale
assert tuple(call.kwargs[key] for key in ("temperature", "top_k", "top_p")) == tuples[rid[-1]]
assert model.compute_logits(torch.zeros(len(order), 2)).argmax(-1).tolist() == [0, 0, 0, 7, 0, 0, 0, 0]
for index, rid in enumerate(order):
state = infos[rid]["breeze_state"]
audio = output.multimodal_outputs["codes"]["audio"][index]
if rid in live_order:
assert state["history"].tolist() == [[1, 3, 1, chosen[rid]]]
assert audio.tolist() == [[chosen[rid]] * 3]
else:
assert audio.numel() == 0
torch.testing.assert_close(state["history"], original_states[rid]["history"], atol=0, rtol=0)
torch.testing.assert_close(
infos["finished_a"]["breeze_state"]["current"], original_states["finished_a"]["current"], atol=0, rtol=0
)
for parent, companion in (("cfg_a", cfg_a), ("cfg_b", cfg_b)):
assert infos[parent]["breeze_state"]["current"] is infos[companion]["breeze_state"]["current"]
@torch.inference_mode()
def test_first_code_shared_generator_preserves_order_across_parameter_groups(monkeypatch):
"""A shared RNG stream must retain A,B,A request order across tuple groups."""
model = _small_talker()
infos = [_request_info(rid) for rid in ("first_a", "middle_b", "last_a")]
parameters = [(0.9, 3, 1.0), (0.7, 2, 0.8), (0.9, 3, 1.0)]
actual_generator = torch.Generator().manual_seed(2**63 + 19)
expected_generator = torch.Generator().manual_seed(2**63 + 19)
hidden = torch.tensor([[0.2, 0.0], [0.0, 0.2], [0.1, 0.2]], dtype=torch.bfloat16)
expected = []
for index, (info, values) in enumerate(zip(infos, parameters, strict=True)):
info["breeze_sampling"].update(temperature=values[0], top_k=values[1], top_p=values[2])
info["breeze_state"]["generator"] = actual_generator
logits = F.linear(hidden[index : index + 1].float(), model.lm_head.weight)
logits[:, model.codebook_size : model.config.eos_token_id] = -torch.inf
expected.append(sample_logits(logits, *values, expected_generator))
assert all(token.item() != model.config.eos_token_id for token in expected)
sample, calls = model._first_code_sampler.sample, []
def record_sample(logits, settings, generators):
calls.append((settings, len(logits), [generator is actual_generator for generator in generators]))
return sample(logits, settings, generators)
monkeypatch.setattr(model._first_code_sampler, "sample", record_sample)
output = model.make_omni_output(
hidden, model_intermediate_buffer=infos, request_token_spans=[(0, 1), (1, 2), (2, 3)]
)
assert calls == [(values, 1, [True]) for values in parameters]
torch.testing.assert_close(actual_generator.get_state(), expected_generator.get_state(), atol=0, rtol=0)
for info, token, audio in zip(infos, expected, output.multimodal_outputs["codes"]["audio"], strict=True):
torch.testing.assert_close(audio, token[:, None].repeat(1, 3), atol=0, rtol=0)
torch.testing.assert_close(info["breeze_state"]["history"], token[:, None], atol=0, rtol=0)
assert info["breeze_state"]["generator"] is actual_generator
@@ -0,0 +1,307 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""First-code sampler token, RNG, graph-cache and fallback contracts."""
import pytest
import torch
from tests.helpers.mark import hardware_test
from vllm_omni.model_executor.models.breeze_tts_2.depth_decoder import sample_logits
from vllm_omni.model_executor.models.breeze_tts_2.first_code_sampler import BreezeFirstCodeSampler
pytestmark = [pytest.mark.core_model]
PARAMETERS = (0.9, 50, 1.0)
VOCAB = 2052
EOS = 2051
def _logits(count: int, *, device: str, seed: int = 100, vocab: int = VOCAB) -> list[torch.Tensor]:
source = torch.Generator(device="cpu").manual_seed(seed)
values = torch.randn((count, vocab), generator=source, dtype=torch.float32) * 3
if vocab == VOCAB:
values[:, 2048:EOS] = -torch.inf
return list(values.to(device).split(1))
def _generators(seeds: list[int], device: str) -> list[torch.Generator]:
return [torch.Generator(device=device).manual_seed(seed) for seed in seeds]
def _reference(rows, parameters, generators) -> list[torch.Tensor]:
return [sample_logits(row, *parameters, generator) for row, generator in zip(rows, generators, strict=True)]
def _assert_outputs(actual, expected, rows) -> None:
assert isinstance(actual, list) and len(actual) == len(expected)
for result, reference, row in zip(actual, expected, rows, strict=True):
assert result.shape == (1,) and result.dtype == torch.long and result.device == row.device
torch.testing.assert_close(result, reference, atol=0, rtol=0)
def _assert_states(actual, expected) -> None:
for left, right in zip(actual, expected, strict=True):
torch.testing.assert_close(left.get_state(), right.get_state(), atol=0, rtol=0)
@pytest.mark.cpu
def test_empty_and_cpu_sampling_preserve_eager_contract() -> None:
sampler = BreezeFirstCodeSampler()
assert sampler.sample([], PARAMETERS, []) == []
assert not sampler._graphs
rows = _logits(3, device="cpu")
before = [row.clone() for row in rows]
actual_generators = _generators([42, 2**63 + 19, 2**64 - 23], "cpu")
expected_generators = _generators([42, 2**63 + 19, 2**64 - 23], "cpu")
for parameters in (PARAMETERS, (0.7, 20, 0.8), (1.2, 0, 0.95)):
expected = _reference(rows, parameters, expected_generators)
_assert_outputs(sampler.sample(rows, parameters, actual_generators), expected, rows)
_assert_states(actual_generators, expected_generators)
for row, original in zip(rows, before, strict=True):
torch.testing.assert_close(row, original, atol=0, rtol=0)
assert not sampler._graphs
@pytest.mark.cpu
def test_greedy_never_samples_or_advances_rng(monkeypatch) -> None:
sampler = BreezeFirstCodeSampler()
rows = _logits(3, device="cpu")
generators = _generators([42, 43, 44], "cpu")
original_states = [generator.get_state().clone() for generator in generators]
def unexpected_multinomial(*_args, **_kwargs):
raise AssertionError("Greedy sampling must not call multinomial")
monkeypatch.setattr(torch, "multinomial", unexpected_multinomial)
for parameters in ((0.0, 0, 1.0), (0.0, 1, 0.2)):
_assert_outputs(sampler.sample(rows, parameters, generators), [row.argmax(-1) for row in rows], rows)
for generator, state in zip(generators, original_states, strict=True):
torch.testing.assert_close(generator.get_state(), state, atol=0, rtol=0)
assert not sampler._graphs
@pytest.mark.cpu
@pytest.mark.parametrize(
"invalid", ["extra_generator", "missing_generator", "empty_rows", "multirow", "mixed_vocab", "mixed_dtype"]
)
def test_invalid_batch_fails_before_advancing_any_generator(invalid: str) -> None:
sampler = BreezeFirstCodeSampler()
rows = _logits(2, device="cpu")
generators = _generators([42, 43], "cpu")
if invalid == "extra_generator":
rows = rows[:1]
elif invalid == "missing_generator":
generators = generators[:1]
elif invalid == "empty_rows":
rows = []
elif invalid == "multirow":
rows[1] = rows[1].expand(2, -1)
elif invalid == "mixed_vocab":
rows[1] = rows[1][:, :-1]
else:
rows[1] = rows[1].double()
states = [generator.get_state().clone() for generator in generators]
with pytest.raises(ValueError):
sampler.sample(rows, PARAMETERS, generators)
for generator, state in zip(generators, states, strict=True):
torch.testing.assert_close(generator.get_state(), state, atol=0, rtol=0)
assert not sampler._graphs
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@torch.inference_mode()
def test_cuda_replay_preserves_tokens_rng_and_owned_outputs(monkeypatch) -> None:
sampler = BreezeFirstCodeSampler()
seeds = list(range(42, 77)) + [2**63 + 19, 2**64 - 23, 101]
actual_generators = _generators(seeds, "cuda")
expected_generators = _generators(seeds, "cuda")
# Torch also accepts an unspecified CUDA index. Exercise it alongside the
# indexed generators created from serving request tensors.
indexed_device = torch.device("cuda", torch.accelerator.current_device_index())
for pool in (actual_generators, expected_generators):
pool[1] = torch.Generator(device=indexed_device).manual_seed(seeds[1])
orders = (
[0],
[2, 0, 1],
list(range(16)),
[36, *range(16)],
[35, *range(30)],
list(reversed(range(32))),
[35, 1, 2],
)
retained: list[tuple[torch.Tensor, torch.Tensor]] = []
global_state = torch.cuda.get_rng_state().clone()
for sweep in range(2):
if sweep:
for pool in (actual_generators, expected_generators):
pool[2] = torch.Generator(device="cuda").manual_seed(2**64 - 101)
for case, order in enumerate(orders):
rows = _logits(len(order), device="cuda", seed=1000 + sweep * 10 + case)
if sweep and case == 3:
for index, row in enumerate(rows):
row.fill_(-torch.inf)
row[0, EOS if index % 2 else 2] = 0
original = [row.clone() for row in rows]
live_actual = [actual_generators[index] for index in order]
live_expected = [expected_generators[index] for index in order]
expected = _reference(rows, PARAMETERS, live_expected)
actual = sampler.sample(rows, PARAMETERS, live_actual)
_assert_outputs(actual, expected, rows)
_assert_states(actual_generators, expected_generators)
retained.extend((value, reference.clone()) for value, reference in zip(actual, expected, strict=True))
for row, before in zip(rows, original, strict=True):
torch.testing.assert_close(row, before, atol=0, rtol=0)
# Preserve the subsequent 15 independent depth draws, including
# skipping them for EOS. All inactive/companion generators remain
# in the full-pool comparison above and below.
for index, token in enumerate(torch.cat(expected).cpu().tolist()):
if token == EOS:
continue
left = torch.empty((15, 2051), device="cuda")
right = torch.empty_like(left)
for codebook in range(15):
left[codebook].exponential_(generator=live_actual[index])
right[codebook].exponential_(generator=live_expected[index])
torch.testing.assert_close(left, right, atol=0, rtol=0)
_assert_states(actual_generators, expected_generators)
# B1/3/16/17/31/32 must use exactly buckets 1/4/16/32.
assert len(sampler._graphs) == 4
rows = _logits(3, device="cuda", seed=2026)
expected = _reference(rows, PARAMETERS, expected_generators[:3])
def unexpected_multinomial(*_args, **_kwargs):
raise AssertionError("Warmed graph replay must not call Python multinomial")
with monkeypatch.context() as replay_guard:
replay_guard.setattr(torch, "multinomial", unexpected_multinomial)
_assert_outputs(sampler.sample(rows, PARAMETERS, actual_generators[:3]), expected, rows)
_assert_outputs(
sampler.sample(rows, (0.0, 1, 0.2), actual_generators[:3]), [row.argmax(-1) for row in rows], rows
)
_assert_states(actual_generators, expected_generators)
assert len(sampler._graphs) == 4
for value, expected in retained:
torch.testing.assert_close(value, expected, atol=0, rtol=0)
torch.testing.assert_close(torch.cuda.get_rng_state(), global_state, atol=0, rtol=0)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@torch.inference_mode()
def test_cuda_parameters_vocab_and_missing_graph_api(monkeypatch) -> None:
sampler = BreezeFirstCodeSampler()
actual_generators = _generators([42, 43, 44], "cuda")
expected_generators = _generators([42, 43, 44], "cuda")
cases = ((0.9, 50, 1.0), (0.7, 100, 0.8), (1.2, 0, 0.95), (1.0, 1, 1.0), (1.0, 0, 0.5))
for index, parameters in enumerate(cases):
rows = _logits(3, device="cuda", seed=200 + index)
if index >= 3:
for row in rows:
row.fill_(-torch.inf)
row[0, :4] = 0 # top-k ties and exact top-p=0.5 boundary.
expected = _reference(rows, parameters, expected_generators)
_assert_outputs(sampler.sample(rows, parameters, actual_generators), expected, rows)
_assert_states(actual_generators, expected_generators)
assert len(sampler._graphs) == len(cases)
rows = _logits(3, device="cuda", vocab=17)
expected = _reference(rows, PARAMETERS, expected_generators)
_assert_outputs(sampler.sample(rows, PARAMETERS, actual_generators), expected, rows)
_assert_states(actual_generators, expected_generators)
assert len(sampler._graphs) == len(cases) + 1
class UnavailableGraph:
def __init__(self, *_args, **_kwargs):
raise AssertionError("Unsupported graph API must select the eager fallback")
fallback = BreezeFirstCodeSampler()
expected = _reference(rows, PARAMETERS, expected_generators)
with monkeypatch.context() as api_guard:
api_guard.setattr(torch.cuda, "CUDAGraph", UnavailableGraph)
_assert_outputs(fallback.sample(rows, PARAMETERS, actual_generators), expected, rows)
_assert_states(actual_generators, expected_generators)
assert not fallback._graphs
# Validate every row before the first eager/captured draw, including an
# invalid device that appears only after an otherwise valid CUDA row.
invalid = [rows[0], rows[1].cpu(), rows[2]]
with pytest.raises(ValueError):
sampler.sample(invalid, PARAMETERS, actual_generators)
_assert_states(actual_generators, expected_generators)
# A late invalid generator must likewise fail before row zero samples.
cpu_generator = torch.Generator(device="cpu").manual_seed(77)
cpu_reference = torch.Generator(device="cpu").manual_seed(77)
mixed_generators = [actual_generators[0], cpu_generator, actual_generators[2]]
with pytest.raises(ValueError):
sampler.sample(rows, PARAMETERS, mixed_generators)
_assert_states(mixed_generators, [expected_generators[0], cpu_reference, expected_generators[2]])
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@torch.inference_mode()
def test_cuda_shared_generator_uses_sequential_eager_fallback(monkeypatch) -> None:
sampler = BreezeFirstCodeSampler()
rows = _logits(3, device="cuda")
# The duplicate-generator guard must precede lookup of an existing graph.
sampler.sample(rows, PARAMETERS, _generators([100, 101, 102], "cuda"))
cached = dict(sampler._graphs)
assert len(cached) == 1
actual = torch.Generator(device="cuda").manual_seed(2**64 - 23)
reference = torch.Generator(device="cuda").manual_seed(2**64 - 23)
multinomial = torch.multinomial
calls = []
def eager_spy(*args, **kwargs):
calls.append(kwargs.get("generator"))
return multinomial(*args, **kwargs)
for order in ([0, 1, 2], [2, 0, 1]):
ordered = [rows[index] for index in order]
expected = _reference(ordered, PARAMETERS, [reference] * 3)
calls.clear()
with monkeypatch.context() as fallback_guard:
fallback_guard.setattr(torch, "multinomial", eager_spy)
_assert_outputs(sampler.sample(ordered, PARAMETERS, [actual] * 3), expected, ordered)
assert len(calls) == 3 and all(generator is actual for generator in calls)
_assert_states([actual], [reference])
assert set(sampler._graphs) == set(cached)
assert all(sampler._graphs[key] is value for key, value in cached.items())
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@torch.inference_mode()
def test_cuda_cache_limit_falls_back_without_evicting_warmed_graphs(monkeypatch) -> None:
sampler = BreezeFirstCodeSampler()
rows = _logits(1, device="cuda")
actual_generators = _generators([42], "cuda")
expected_generators = _generators([42], "cuda")
for top_k in range(1, 33):
parameters = (0.9, top_k, 1.0)
expected = _reference(rows, parameters, expected_generators)
_assert_outputs(sampler.sample(rows, parameters, actual_generators), expected, rows)
_assert_states(actual_generators, expected_generators)
cached = dict(sampler._graphs)
assert len(cached) == 32
parameters = (0.9, 33, 1.0)
expected = _reference(rows, parameters, expected_generators)
multinomial = torch.multinomial
calls = []
def eager_spy(*args, **kwargs):
calls.append(kwargs.get("generator"))
return multinomial(*args, **kwargs)
with monkeypatch.context() as fallback_guard:
fallback_guard.setattr(torch, "multinomial", eager_spy)
_assert_outputs(sampler.sample(rows, parameters, actual_generators), expected, rows)
assert len(calls) == 1 and calls[0] is actual_generators[0]
_assert_states(actual_generators, expected_generators)
assert set(sampler._graphs) == set(cached)
assert all(sampler._graphs[key] is value for key, value in cached.items())
expected = _reference(rows, (0.9, 1, 1.0), expected_generators)
def unexpected_multinomial(*_args, **_kwargs):
raise AssertionError("Cache exhaustion must retain existing captured entries")
with monkeypatch.context() as replay_guard:
replay_guard.setattr(torch, "multinomial", unexpected_multinomial)
_assert_outputs(sampler.sample(rows, (0.9, 1, 1.0), actual_generators), expected, rows)
_assert_states(actual_generators, expected_generators)
@@ -0,0 +1,405 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Check captured cache ownership and padded text masking against eager models."""
import pytest
import torch
from transformers import LlamaConfig, T5Gemma2TextConfig
from transformers.models.t5gemma2.modeling_t5gemma2 import T5Gemma2TextEncoder
from vllm.platforms import current_platform
from tests.helpers.mark import hardware_test
from vllm_omni.model_executor.models.breeze_tts_2.depth_decoder import BreezeDepthDecoder, sample_logits
from vllm_omni.model_executor.models.breeze_tts_2.modeling_breeze import BreezeForConditionalGeneration
from vllm_omni.model_executor.models.breeze_tts_2.text_encoder_graph import (
BreezeTextEncoderCompiled,
BreezeTextEncoderGraph,
)
pytestmark = [pytest.mark.core_model]
@pytest.fixture
def full_precision_matmul():
# Different padded shapes can select different TF32 kernels. Compare
# masking and replay semantics independently of that reduced precision.
original_precision = torch.get_float32_matmul_precision()
torch.set_float32_matmul_precision("highest")
try:
yield
finally:
torch.set_float32_matmul_precision(original_precision)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@torch.inference_mode()
def test_text_graph_masks_padding_and_local_attention_across_replays(full_precision_matmul) -> None:
torch.manual_seed(42)
config = T5Gemma2TextConfig(
vocab_size=64,
hidden_size=32,
intermediate_size=64,
num_hidden_layers=2,
num_attention_heads=4,
num_key_value_heads=2,
head_dim=8,
sliding_window=16,
layer_types=["sliding_attention", "full_attention"],
dropout_rate=0.0,
)
config._attn_implementation = "sdpa"
encoder = T5Gemma2TextEncoder(config).to("cuda").eval()
encoder.embed_tokens.eoi_token_index = 17
encoder.embed_tokens.eoi_embedding.fill_(0.4)
projection = torch.nn.Linear(32, 48, bias=False).to("cuda").eval()
graph = BreezeTextEncoderGraph(encoder, projection, 64)
for length in (31, 51, 17, 31):
prompt = torch.randint(0, 64, (1, length), device="cuda")
prompt[:, 3] = 17
expected = projection(encoder(input_ids=prompt).last_hidden_state)[0]
actual = graph.run(prompt)
torch.testing.assert_close(actual, expected, atol=2e-6, rtol=2e-5)
batched = BreezeTextEncoderGraph(encoder, projection, 64, batch_size=3)
prompts = [torch.randint(0, 64, (length,), device="cuda") for length in (31, 51, 17)]
outputs = batched.run_batch(prompts)
for prompt, actual in zip(prompts, outputs, strict=True):
expected = projection(encoder(input_ids=prompt[None]).last_hidden_state)[0]
torch.testing.assert_close(actual, expected, atol=2e-6, rtol=2e-5)
compiled = BreezeTextEncoderCompiled(encoder, projection)
long_prompts = [torch.randint(0, 64, (length,)) for length in (211, 173, 129)]
for batch in ([long_prompts[0]], long_prompts, [long_prompts[1]]):
outputs = compiled.run_batch(batch)
for prompt, actual in zip(batch, outputs, strict=True):
expected = projection(encoder(input_ids=prompt[None].to("cuda")).last_hidden_state)[0]
torch.testing.assert_close(actual, expected, atol=2e-6, rtol=2e-5)
def _make_depth_decoder(vocab_size: int = 35) -> BreezeDepthDecoder:
config = LlamaConfig(
hidden_size=32,
intermediate_size=64,
num_hidden_layers=2,
num_attention_heads=4,
num_key_value_heads=2,
head_dim=8,
vocab_size=vocab_size,
max_position_embeddings=16,
rope_theta=500000.0,
rope_scaling={
"rope_type": "llama3",
"factor": 32.0,
"low_freq_factor": 0.001953125,
"high_freq_factor": 0.0078125,
"original_max_position_embeddings": 16,
},
)
config.num_codebooks = 16
config.audio_embed_size = 48
depth = BreezeDepthDecoder(config).to("cuda").eval()
torch.nn.init.normal_(depth.codebooks_head, std=0.1)
return depth
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@torch.inference_mode()
def test_depth_graph_survives_interleaved_batches_and_workspace_allocations(full_precision_matmul, monkeypatch) -> None:
torch.manual_seed(42)
depth = _make_depth_decoder()
hidden = torch.randn(2, 48, device="cuda")
first = torch.tensor([7, 11], device="cuda")
generators = [torch.Generator(device="cuda").manual_seed(seed) for seed in (42, 99)]
expected = torch.cat(
[
depth.generate_frame(hidden[i : i + 1], first[i : i + 1], temperature=0, top_k=0, top_p=1, generator=g)
for i, g in enumerate(generators)
]
)
for batch in (1, 2, 1, 2):
# Unowned allocations made before graph capture would be recycled here.
pressure = [torch.full((batch, 2, depth.num_codebooks, 8), float("nan"), device="cuda") for _ in range(32)]
actual = depth.generate_frames(
hidden[:batch], first[:batch], temperature=0, top_k=0, top_p=1, generators=generators[:batch]
)
torch.testing.assert_close(actual, expected[:batch], atol=0, rtol=0)
del pressure
def sample(batch: int) -> torch.Tensor:
return depth.generate_frames(
hidden[:batch],
first[:batch],
temperature=0.9,
top_k=10,
top_p=0.8,
generators=[torch.Generator(device="cuda").manual_seed(seed) for seed in (42, 99)[:batch]],
)
sampled = sample(1)
sampled_graph = depth._graphs[(1, 1, False)]
greedy_graph = depth._graphs[(1, 1, True)]
assert sampled_graph is not greedy_graph
sample(2)
torch.testing.assert_close(sample(1), sampled, atol=0, rtol=0)
for temperature, top_k, top_p in ((0.0, 0, 1.0), (0.7, 20, 0.95), (0.0, 1, 0.2), (1.2, 0, 0.8)):
rng_before = generators[0].get_state()
actual = depth.generate_frames(
hidden[:1], first[:1], temperature=temperature, top_k=top_k, top_p=top_p, generators=generators[:1]
)
if temperature == 0:
assert depth._graphs[(1, 1, True)] is greedy_graph
torch.testing.assert_close(actual, expected[:1], atol=0, rtol=0)
torch.testing.assert_close(generators[0].get_state(), rng_before, atol=0, rtol=0)
else:
assert depth._graphs[(1, 1, False)] is sampled_graph
torch.testing.assert_close(sample(1), sampled, atol=0, rtol=0)
graph = None
for scale in (4.0, 0.5, 4.0):
expected_cfg = depth._generate_frame(
hidden, first[:1], depth._allocate_cache(hidden), 0, 0, 1, generators[0], guidance_scale=scale
)
actual_cfg = depth.generate_frames(
hidden, first[:1], temperature=0, top_k=0, top_p=1, generators=generators[:1], guidance_scale=scale
)
torch.testing.assert_close(actual_cfg, expected_cfg, atol=0, rtol=0)
if graph is not None:
assert depth._graphs[(2, 2, True)] is graph
graph = depth._graphs[(2, 2, True)]
sample(2)
# Use a still-cold sampled CFG entry to exercise platforms without
# captured generator support, while reusing the existing compiled model.
fallback_key = (2, 2, False)
assert fallback_key not in depth._graphs
reference_generator = torch.Generator(device="cuda").set_state(generators[0].get_state())
inactive_rng = generators[1].get_state()
exponential = torch.Tensor.exponential_
observed_generators = []
def record_eager_noise(tensor, *args, **kwargs):
observed_generators.append(kwargs.get("generator"))
return exponential(tensor, *args, **kwargs)
fallback_graph = None
retained = []
with monkeypatch.context() as platform_guard:
platform_guard.setattr(current_platform, "is_cuda", lambda: False)
for scale in (4.0, 0.5, 4.0):
expected = depth._generate_frame(
hidden,
first[:1],
depth._allocate_cache(hidden),
0.9,
10,
0.8,
reference_generator,
guidance_scale=scale,
)
observed_generators.clear()
with monkeypatch.context() as noise_spy:
noise_spy.setattr(torch.Tensor, "exponential_", record_eager_noise)
actual = depth.generate_frames(
hidden,
first[:1],
temperature=0.9,
top_k=10,
top_p=0.8,
generators=generators[:1],
guidance_scale=scale,
)
entry = depth._graphs[fallback_key]
assert entry.noise_generators is None
assert len(observed_generators) == depth.num_codebooks - 1
assert all(generator is generators[0] for generator in observed_generators)
if fallback_graph is not None:
assert entry is fallback_graph
fallback_graph = entry
torch.testing.assert_close(actual, expected, atol=0, rtol=0)
torch.testing.assert_close(generators[0].get_state(), reference_generator.get_state(), atol=0, rtol=0)
torch.testing.assert_close(generators[1].get_state(), inactive_rng, atol=0, rtol=0)
retained.append((actual, expected.clone()))
for actual, expected in retained:
torch.testing.assert_close(actual, expected, atol=0, rtol=0)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@torch.inference_mode()
def test_depth_graph_buckets_bound_shapes_and_preserve_requests(full_precision_matmul, monkeypatch) -> None:
def reject_eager_noise(*args, **kwargs):
raise AssertionError("Sampled depth replay must not issue eager RNG draws")
torch.manual_seed(17)
# Match the checkpoint's multinomial draw size, including reserved IDs.
depth = _make_depth_decoder(vocab_size=2051)
hidden = torch.randn(32, 48, device="cuda")
uncond = torch.randn_like(hidden)
first = torch.arange(32, device="cuda")
first_logits = torch.randn(32, depth.vocab_size, device="cuda")
first_logits[:, -3:] = -torch.inf
seeds = [42 + index for index in range(32)]
seeds[0], seeds[-1] = 2**63 + 17, 2**64 - 1
generators = [torch.Generator(device="cuda").manual_seed(seed) for seed in seeds]
reference_generators = [torch.Generator(device="cuda").manual_seed(seed) for seed in seeds]
expected_greedy = torch.cat(
[
depth.generate_frame(
hidden[index : index + 1],
first[index : index + 1],
temperature=0,
top_k=0,
top_p=1,
generator=reference_generators[index],
)
for index in range(32)
]
)
retained: list[tuple[torch.Tensor, torch.Tensor]] = []
for batch in (*range(1, 33), 17, 11, 3, 1):
rows = list(reversed(range(batch)))
actual = depth.generate_frames(
hidden[rows], first[rows], temperature=0, top_k=0, top_p=1, generators=[generators[row] for row in rows]
)
expected = expected_greedy[rows]
torch.testing.assert_close(actual, expected, atol=0, rtol=0)
retained.append((actual, expected.clone()))
assert set(depth._graphs) == {(batch, 1, True) for batch in (1, 2, 4, 8, 16, 32)}
for generator, reference in zip(generators, reference_generators, strict=True):
torch.testing.assert_close(generator.get_state(), reference.get_state(), atol=0, rtol=0)
retired_generators: list[tuple[torch.Generator, torch.Tensor]] = []
for temperature, guidance_scale in ((0.9, 1.0), (0.0, 4.0), (0.9, 4.0)):
batches: tuple[int, ...] = (3, 5, 11, 7, 3, 1)
if guidance_scale == 1:
batches = (16, 17, 31, 32, 17, 31, 16, *batches)
for step, batch in enumerate(batches):
if guidance_scale == 1 and step == 4:
# New requests reuse rows in the same 32-request graph. Their
# seeds must replace the previous owners, not just offsets.
for row, seed in ((0, 2**64 - 1), (16, 2**63 + 321)):
retired_generators.append((generators[row], generators[row].get_state()))
generators[row] = torch.Generator(device="cuda").manual_seed(seed)
reference_generators[row] = torch.Generator(device="cuda").manual_seed(seed)
rows = list(reversed(range(batch)))
batch_hidden = hidden[rows]
if guidance_scale != 1:
batch_hidden = torch.cat((batch_hidden, uncond[rows]))
expected, first_codes = [], []
for row in rows:
# The talker and depth decoder share each request's generator.
# Exercise a codebook-0 sample immediately before graph replay.
row_first = sample_logits(first_logits[row : row + 1], temperature, 10, 0.8, generators[row])
reference_first = sample_logits(
first_logits[row : row + 1], temperature, 10, 0.8, reference_generators[row]
)
torch.testing.assert_close(row_first, reference_first, atol=0, rtol=0)
first_codes.append(row_first)
row_hidden = hidden[row : row + 1]
if guidance_scale != 1:
row_hidden = torch.cat((row_hidden, uncond[row : row + 1]))
expected.append(
depth._generate_frame(
row_hidden,
reference_first,
depth._allocate_cache(row_hidden),
temperature,
10,
0.8,
reference_generators[row],
guidance_scale=guidance_scale if guidance_scale != 1 else None,
)
)
request_bucket = 1 << (batch - 1).bit_length()
branches = 2 if guidance_scale != 1 else 1
graph_key = (request_bucket * branches, branches, temperature == 0)
with monkeypatch.context() as rng_guard:
if temperature > 0 and graph_key in depth._graphs:
# A warmed replay must generate noise within the CUDA
# graph, without dispatching new RNG kernels from Python.
rng_guard.setattr(torch.Tensor, "exponential_", reject_eager_noise)
actual = depth.generate_frames(
batch_hidden,
torch.cat(first_codes),
temperature=temperature,
top_k=10,
top_p=0.8,
generators=[generators[row] for row in rows],
guidance_scale=guidance_scale,
)
expected_frames = torch.cat(expected)
torch.testing.assert_close(actual, expected_frames, atol=0, rtol=0)
retained.append((actual, expected_frames.clone()))
# Check the next codebook-0 sample as well as the depth result: a
# replay must return the advanced RNG stream to its live request.
for row in rows:
actual_next = sample_logits(first_logits[row : row + 1], temperature, 10, 0.8, generators[row])
expected_next = sample_logits(
first_logits[row : row + 1], temperature, 10, 0.8, reference_generators[row]
)
torch.testing.assert_close(actual_next, expected_next, atol=0, rtol=0)
# Padding must not draw RNG, and inactive request streams must not advance.
for generator, reference in zip(generators, reference_generators, strict=True):
torch.testing.assert_close(generator.get_state(), reference.get_state(), atol=0, rtol=0)
for generator, state in retired_generators:
torch.testing.assert_close(generator.get_state(), state, atol=0, rtol=0)
expected_keys = {(batch, 1, True) for batch in (1, 2, 4, 8, 16, 32)}
expected_keys.update((batch, 1, False) for batch in (1, 4, 8, 16, 32))
expected_keys.update((batch, 2, greedy) for batch in (2, 8, 16, 32) for greedy in (False, True))
assert set(depth._graphs) == expected_keys
for (batch_bucket, branches, greedy), entry in depth._graphs.items():
if greedy:
assert entry.noise_generators is None
else:
assert entry.noise_generators is not None
assert len(entry.noise_generators) == batch_bucket // branches
assert {id(generator) for generator in entry.noise_generators}.isdisjoint(
id(generator) for generator in generators
)
for actual, expected in retained:
torch.testing.assert_close(actual, expected, atol=0, rtol=0)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@torch.inference_mode()
def test_batched_decode_embedding_matches_scalar_bfloat16() -> None:
# Use the checkpoint's RVQ table dimensions without loading its backbone.
model = BreezeForConditionalGeneration.__new__(BreezeForConditionalGeneration)
torch.nn.Module.__init__(model)
model.num_codebooks, model.hidden_size = 16, 2048
model.register_buffer("offsets", torch.arange(16, device="cuda") * 2051)
model.depth_decoder = torch.nn.Module()
torch.manual_seed(17)
model.depth_decoder.embed_tokens = torch.nn.Embedding(16 * 2051, 2048, device="cuda", dtype=torch.bfloat16)
for seed in (17, 42, 99):
generator = torch.Generator(device="cuda").manual_seed(seed)
frames = torch.randint(0, 2051, (32, 16), device="cuda", generator=generator)
frames[1] = frames[0]
for batch in (1, 2, 3, 4, 5, 6, 7, 8, 16, 32, 7, 1):
# Reverse physical rows and revisit smaller batches after expansion.
current_frames = [row[None] for row in frames[:batch].flip(0)]
if batch > 1:
# CFG branches share their generated audio frame.
current_frames[-2] = current_frames[-1]
infos = [
{
"_omni_is_prefill": False,
"breeze_state": {"current": current},
"global_request_id": [str(index)],
"breeze_prompt": {},
"breeze_sampling": {},
}
for index, current in enumerate(current_frames)
]
ids = torch.zeros(batch, device="cuda", dtype=torch.long)
expected = torch.cat(
[
model.preprocess(input_ids=ids[index : index + 1], input_embeds=None, **info)[1]
for index, info in enumerate(infos)
]
)
output_ids, actual, updates = model.preprocess_decode_batch(input_ids=ids, req_infos=infos)
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
assert output_ids is ids
assert updates == [{} for _ in infos]
@@ -0,0 +1,119 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""CPU contracts for Breeze's decode-only embedding preprocessing."""
import pytest
import torch
from vllm_omni.model_executor.models.breeze_tts_2.modeling_breeze import BreezeForConditionalGeneration
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _embedding_talker(dtype):
model = BreezeForConditionalGeneration.__new__(BreezeForConditionalGeneration)
torch.nn.Module.__init__(model)
model.num_codebooks, model.hidden_size = 16, 8
model.register_buffer("offsets", torch.arange(model.num_codebooks) * 7)
model.depth_decoder = torch.nn.Module()
model.depth_decoder.embed_tokens = torch.nn.Embedding(16 * 7, 8, dtype=dtype)
with torch.no_grad():
model.depth_decoder.embed_tokens.weight.copy_(
torch.randn(16 * 7, 8, generator=torch.Generator().manual_seed(17))
)
return model
def _decode_info(request_id, current, seed):
return {
"global_request_id": [request_id],
"breeze_prompt": {"role": "cond", "guidance_scale": 1.0},
"breeze_sampling": {"temperature": 0.9, "top_k": 4, "top_p": 1.0, "repetition_penalty": 1.1},
"breeze_state": {
"current": current,
"history": torch.tensor([[1, 2, 3]], dtype=torch.long),
"generator": torch.Generator().manual_seed(seed),
},
"_omni_is_prefill": False,
}
@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16])
@torch.inference_mode()
def test_decode_batch_matches_scalar_after_reordering_shrinking_and_cfg(dtype):
model = _embedding_talker(dtype)
shared_frame = torch.arange(16, dtype=torch.long).reshape(1, 16) % 7
infos = [
_decode_info("unconditional", shared_frame, 1),
_decode_info("other", (shared_frame + 1) % 7, 2),
_decode_info("conditioned", shared_frame, 3),
_decode_info("last", (shared_frame + 3) % 7, 4),
]
infos[0]["breeze_prompt"] = {"role": "uncond", "guidance_scale": 4.0}
infos[2]["breeze_prompt"]["guidance_scale"] = 4.0
saved = []
for indices in ([0, 1, 2, 3], [2, 0, 1], [1]):
selected = [infos[index] for index in indices]
# Placeholder IDs deliberately differ from the RVQ frame IDs.
input_ids = torch.arange(len(selected), dtype=torch.long) + 100
expected = torch.cat(
[
model.preprocess(input_ids=input_ids[row : row + 1], input_embeds=None, **info)[1]
for row, info in enumerate(selected)
]
)
state_before = [
(
info["breeze_state"]["current"].clone(),
info["breeze_state"]["history"].clone(),
info["breeze_state"]["generator"].get_state(),
)
for info in infos
]
output_ids, embeds, updates = model.preprocess_decode_batch(input_ids=input_ids, req_infos=selected)
assert output_ids is input_ids
torch.testing.assert_close(embeds, expected, rtol=0, atol=0)
assert updates == [{} for _ in selected]
for info, (current, history, rng) in zip(infos, state_before, strict=True):
state = info["breeze_state"]
assert torch.equal(state["current"], current)
assert torch.equal(state["history"], history)
assert torch.equal(state["generator"].get_state(), rng)
saved.append((embeds, embeds.clone()))
# Simulate the next generated frame. CFG branches share the new frame.
next_frame = (infos[2]["breeze_state"]["current"] + 2) % 7
infos[0]["breeze_state"]["current"] = next_frame
infos[2]["breeze_state"]["current"] = next_frame
infos[1]["breeze_state"]["current"] = (infos[1]["breeze_state"]["current"] + 1) % 7
assert infos[0]["breeze_state"]["current"] is infos[2]["breeze_state"]["current"]
for output, snapshot in saved:
assert torch.equal(output, snapshot)
@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16])
def test_decode_batch_empty_preserves_embedding_shape_and_dtype(dtype):
model = _embedding_talker(dtype)
ids = torch.empty(0, dtype=torch.long)
output_ids, embeds, updates = model.preprocess_decode_batch(input_ids=ids, req_infos=[])
assert output_ids is ids
assert embeds.shape == (0, 8)
assert embeds.dtype == dtype
assert embeds.device == model.depth_decoder.embed_tokens.weight.device
assert updates == []
@pytest.mark.parametrize("invalid", ["count", "state", "frame_shape", "prefill"])
def test_decode_batch_rejects_invalid_request_state(invalid):
model = _embedding_talker(torch.float32)
info = _decode_info("request", torch.zeros(1, 16, dtype=torch.long), 42)
ids = torch.tensor([0])
if invalid == "count":
ids = torch.tensor([0, 1])
elif invalid == "state":
del info["breeze_state"]
elif invalid == "frame_shape":
info["breeze_state"]["current"] = torch.zeros(2, 16, dtype=torch.long)
else:
info["_omni_is_prefill"] = True
with pytest.raises(ValueError, match="Breeze decode"):
model.preprocess_decode_batch(input_ids=ids, req_infos=[info])
@@ -0,0 +1,50 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""The accelerated runtime preserves Breeze-TTS-2's model identifiers."""
from pathlib import Path
import pytest
import yaml
from vllm_omni.config.pipeline_registry import OMNI_PIPELINES
from vllm_omni.entrypoints.openai.tts_adapters import detect_tts_model_type, resolve_adapter
from vllm_omni.entrypoints.openai.tts_adapters.breeze_tts_2 import BreezeTTS2Adapter
from vllm_omni.model_executor.models.breeze_tts_2.pipeline import BREEZE_TTS_2_PIPELINE
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_breeze_tts_2_pipeline_keeps_checkpoint_and_stage_identifiers():
assert OMNI_PIPELINES["breeze"] is BREEZE_TTS_2_PIPELINE
assert BREEZE_TTS_2_PIPELINE.model_arch == "BreezeForConditionalGeneration"
assert BREEZE_TTS_2_PIPELINE.default_deploy_config_name == "breeze_tts_2.yaml"
assert [stage.model_stage for stage in BREEZE_TTS_2_PIPELINE.stages] == [
"breeze_tts_2",
"breeze_tts_2_codec",
]
def test_breeze_tts_2_adapter_remains_registered():
assert resolve_adapter("breeze_tts_2") is BreezeTTS2Adapter
@pytest.mark.parametrize(
"model_stage,model_arch",
[
("breeze_tts_2", None),
("breeze_tts_2_codec", None),
("breeze_tts_2", "BreezeForConditionalGeneration"),
(None, "BreezeForConditionalGeneration"),
],
)
def test_breeze_tts_2_detection_keeps_existing_identifiers(model_stage, model_arch):
assert detect_tts_model_type(model_stage, model_arch) == "breeze_tts_2"
@pytest.mark.parametrize("suffix", ["", "_throughput"])
def test_breeze_deploy_aliases_match_canonical_configuration(suffix):
deploy_dir = Path(__file__).parents[3] / "vllm_omni" / "deploy"
canonical = yaml.safe_load((deploy_dir / f"breeze_tts_2{suffix}.yaml").read_text())
alias = yaml.safe_load((deploy_dir / f"breeze_tts{suffix}.yaml").read_text())
assert canonical == alias
@@ -19,7 +19,7 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@pytest.fixture
def talker_sampling_params():
deploy_path = Path(__file__).parents[4] / "vllm_omni" / "deploy" / "breeze_tts_2.yaml"
deploy_path = Path(__file__).parents[3] / "vllm_omni" / "deploy" / "breeze_tts_2.yaml"
deploy = yaml.safe_load(deploy_path.read_text(encoding="utf-8"))
params = SamplingParams(
**merge_sampling_constraints(
+152
View File
@@ -3,6 +3,7 @@
from contextlib import contextmanager
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
import torch
@@ -1030,3 +1031,154 @@ def test_preprocess_one_token_chunked_prefill_tail_then_decode(monkeypatch, batc
assert route == ("batch" if batched_decode and not is_prefill else "normal")
torch.testing.assert_close(embeds, torch.full((1, 4), 42.0 if is_prefill else 43.0))
assert ("codes" in runner.model_intermediate_buffer["r"]) == (not is_prefill)
class DecodeOnlyPreprocessModel:
has_preprocess = True
def __init__(self, *, rewrite_ids=False, invalid_output=None):
self.rewrite_ids = rewrite_ids
self.invalid_output = invalid_output
self.calls = []
@staticmethod
def embed_input_ids(input_ids, **kwargs):
return input_ids.float().view(-1, 1).expand(-1, 4).clone()
def preprocess(self, input_ids, input_embeds, **info):
self.calls.append(("scalar", [info["request_id"]], [info["_omni_is_prefill"]]))
return input_ids, self.embed_input_ids(input_ids), {}
def preprocess_decode_batch(self, *, input_ids, req_infos):
self.calls.append(
("batch", [info["request_id"] for info in req_infos], [info["_omni_is_prefill"] for info in req_infos])
)
ids = input_ids + 100 if self.rewrite_ids else input_ids
embeds = self.embed_input_ids(ids) + 10
updates = [{"decode_marker": info["request_id"]} for info in req_infos]
if self.invalid_output == "embeddings":
embeds = embeds[:-1]
elif self.invalid_output == "ids":
ids = ids[:-1]
elif self.invalid_output == "updates":
updates = updates[:-1]
return ids, embeds, updates
def _without_mtp_buffers(runner):
runner.has_talker_mtp = False
for name in ("talker_mtp", "talker_mtp_input_ids", "talker_mtp_inputs_embeds", "last_talker_hidden", "text_step"):
delattr(runner, name)
@pytest.mark.parametrize("rewrite_ids", [False, True], ids=["original-ids", "replaced-ids"])
@pytest.mark.parametrize("order", ["interleaved", "reversed", "adjacent"])
def test_decode_batch_without_mtp_preserves_mixed_rows_and_buffers(monkeypatch, rewrite_ids, order):
rows = [
("cfg-uncond", 5, 5, 1),
("prefill-tail", 5, 4, 1),
("cfg-cond", 5, 6, 1),
("single-prompt", 1, 0, 1),
("multi-token-decode", 5, 5, 2),
]
if order == "reversed":
rows.reverse()
elif order == "adjacent":
rows = [rows[1], rows[0], rows[2], *rows[3:]]
runner, scheduled, _, _ = _make_phase_runner(monkeypatch, rows, batched_decode=False)
_without_mtp_buffers(runner)
model = DecodeOnlyPreprocessModel(rewrite_ids=rewrite_ids)
runner.model = model
total = scheduled.total_num_scheduled_tokens
runner.inputs_embeds = DummyBuffer(torch.full((total + 2, 4), -99.0))
ids_buffer = runner.input_ids.gpu
embeds_buffer = runner.inputs_embeds.gpu
output_ids, output_embeds, *_ = runner._preprocess(scheduled, total)
assert output_ids.data_ptr() == ids_buffer.data_ptr()
assert output_embeds.data_ptr() == embeds_buffer.data_ptr()
assert torch.equal(embeds_buffer[total:], torch.full((2, 4), -99.0))
seen = [
(route, rid, prefill) for route, ids, phases in model.calls for rid, prefill in zip(ids, phases, strict=True)
]
offset = 0
for (route, rid, prefill), (expected_id, prompt, computed, span) in zip(seen, rows, strict=True):
eligible = span == 1 and computed >= prompt
assert rid == expected_id
assert prefill == (computed < prompt)
assert route == ("batch" if eligible else "scalar")
expected_ids = torch.arange(offset, offset + span) + (100 if eligible and rewrite_ids else 0)
assert torch.equal(output_ids[offset : offset + span], expected_ids)
expected_embeds = expected_ids.float().view(-1, 1).expand(-1, 4) + (10 if eligible else 0)
torch.testing.assert_close(output_embeds[offset : offset + span], expected_embeds)
info = runner.model_intermediate_buffer[rid]
assert info.get("decode_marker") == (rid if eligible else None)
offset += span
def test_decode_batch_without_mtp_accepts_wrapper_input_ids_none(monkeypatch):
rows = [("first", 5, 5, 1), ("second", 5, 6, 1)]
runner, scheduled, _, _ = _make_phase_runner(monkeypatch, rows, batched_decode=False)
_without_mtp_buffers(runner)
model = DecodeOnlyPreprocessModel()
runner.model = model
runner.supports_mm_inputs = True
runner.maybe_get_ec_connector_output = _noop_forward_context
runner.encoder_cache = {}
runner._execute_mm_encoder = lambda output: None
runner._gather_mm_embeddings = lambda output: ([], None)
runner._extract_mm_kwargs = lambda output: {}
runner._prepare_mm_inputs = lambda size: (None, runner.inputs_embeds.gpu[:size])
ids, embeds, *_ = runner._preprocess(scheduled, 2)
assert ids.data_ptr() == runner.input_ids.gpu.data_ptr()
assert model.calls == [("batch", ["first", "second"], [False, False])]
torch.testing.assert_close(embeds, torch.tensor([[10.0] * 4, [11.0] * 4]))
@pytest.mark.parametrize(
"offsets",
[
pytest.param([0, 2, 3], id="gap"),
pytest.param([1, 0, 2], id="reversed"),
pytest.param([0, 0, 1], id="duplicate"),
pytest.param([-1, 0, 1], id="negative-contiguous-start"),
pytest.param([1, 2, 3], id="contiguous-out-of-bounds"),
],
)
def test_decode_batch_without_mtp_rejects_invalid_offsets_before_model_hook(monkeypatch, offsets):
rows = [("first", 5, 5, 1), ("second", 5, 6, 1)]
runner, scheduled, _, _ = _make_phase_runner(monkeypatch, rows, batched_decode=False)
_without_mtp_buffers(runner)
model = DecodeOnlyPreprocessModel(rewrite_ids=True)
runner.model = model
scalar_hook = Mock(wraps=model.preprocess)
batch_hook = Mock(wraps=model.preprocess_decode_batch)
monkeypatch.setattr(model, "preprocess", scalar_hook)
monkeypatch.setattr(model, "preprocess_decode_batch", batch_hook)
runner.query_start_loc.cpu = torch.tensor(offsets, dtype=torch.int32)
runner.inputs_embeds = DummyBuffer(torch.full((4, 4), -99.0))
ids_before = runner.input_ids.gpu.clone()
embeds_before = runner.inputs_embeds.gpu.clone()
with pytest.raises(
RuntimeError,
match="Non-MTP batched decode preprocessing requires contiguous in-bounds token offsets",
):
runner._preprocess(scheduled, scheduled.total_num_scheduled_tokens)
# These checks remain active when the runner is imported under python -O.
scalar_hook.assert_not_called()
batch_hook.assert_not_called()
torch.testing.assert_close(runner.input_ids.gpu, ids_before, rtol=0, atol=0)
torch.testing.assert_close(runner.inputs_embeds.gpu, embeds_before, rtol=0, atol=0)
@pytest.mark.parametrize("invalid", ["embeddings", "ids", "updates"])
def test_decode_batch_without_mtp_rejects_incomplete_outputs(monkeypatch, invalid):
rows = [("first", 5, 5, 1), ("second", 5, 6, 1)]
runner, scheduled, _, _ = _make_phase_runner(monkeypatch, rows, batched_decode=False)
_without_mtp_buffers(runner)
runner.model = DecodeOnlyPreprocessModel(invalid_output=invalid)
before = runner.inputs_embeds.gpu.clone()
with pytest.raises(ValueError, match="decode preprocessing"):
runner._preprocess(scheduled, 2)
assert torch.equal(runner.inputs_embeds.gpu, before)
@@ -194,7 +194,6 @@ _MODEL_INTERNALIZE = (
"MING_TTS_STAGE1_FINAL_LOG",
"MOSS_TTS_DEBUG_STOP",
"NEMOTRON_VOICECHAT_DEBUG_FUNCTION_TIMELINE",
"VLLM_OMNI_BREEZE_GOLDEN_DIR",
"VLLM_OMNI_QWEN_VISUAL_DEBUG_DIR",
"VLLM_OMNI_QWEN3_CODE2WAV_BATCH_STATS",
"VLLM_OMNI_QWEN3_CODE2WAV_BATCH_STATS_LOG_EVERY",
@@ -0,0 +1,155 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Atomic CFG admission for full-attention AR models with unequal prompts.
Each branch retains its real prompt length and position IDs. Admission reserves
capacity for the complete generation of both branches, so KV pressure cannot
preempt one branch while its partner continues. Reservations account for pages;
the ordinary paged attention manager still allocates pages as tokens arrive.
"""
from collections.abc import Iterable
from typing import Any
from vllm.v1.core.sched.output import SchedulerOutput
from vllm.v1.core.sched.request_queue import SchedulingPolicy, create_request_queue
from vllm.v1.request import Request, RequestStatus
from vllm_omni.core.sched.omni_ar_scheduler import OmniARScheduler
from vllm_omni.engine.serialization import deserialize_additional_information
class OmniCFGARScheduler(OmniARScheduler):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
if self.scheduler_config.async_scheduling or self.scheduler_config.enable_chunked_prefill:
raise ValueError("Atomic CFG scheduling requires synchronous scheduling and complete prefills")
if self.policy != SchedulingPolicy.FCFS or self.cache_config.enable_prefix_caching:
raise ValueError("Atomic CFG scheduling requires FCFS and disabled prefix caching")
if self.vllm_config.speculative_config is not None or self.connector is not None:
raise ValueError("Atomic CFG scheduling does not support speculative decoding or KV connectors")
if len(self.kv_cache_manager.kv_cache_config.kv_cache_groups) != 1:
raise ValueError("Atomic CFG reservations require one full-attention KV group")
self._capacity = self.kv_cache_manager.block_pool.num_gpu_blocks - 1 - self.kv_cache_manager.watermark_blocks
pair_pages = 2 * -(-self.max_model_len // self.block_size)
if self._capacity < pair_pages or self.max_num_running_reqs < 2:
raise ValueError("Atomic CFG requires KV capacity and max_num_seqs for two maximum-length branches")
if self.max_num_scheduled_tokens < 2 * self.max_model_len:
raise ValueError("Atomic CFG requires max_num_batched_tokens >= 2 * max_model_len")
self._pairs: dict[str, dict[str, str]] = {}
self._request_pairs: dict[str, str] = {}
self._prefill_min_output_tokens = int(self.vllm_config.additional_config.get("cfg_prefill_delay_tokens", 0))
if self._prefill_min_output_tokens < 0:
raise ValueError("cfg_prefill_delay_tokens cannot be negative")
def _should_defer_waiting_admission(self) -> bool:
# Let fresh streaming requests accumulate playback audio before
# another request's text/reference encoding occupies the same device.
return any(request.num_output_tokens < self._prefill_min_output_tokens for request in self.running)
def add_request(self, request: Request) -> None:
payload = getattr(request, "additional_information", None)
info = deserialize_additional_information(payload) if payload is not None else {}
group = info.get("cfg_group")
if group is not None:
role = group["role"]
suffix = group["uncond_suffix"]
if role not in ("cond", "uncond") or not suffix:
raise ValueError("CFG groups require cond/uncond roles and a companion suffix")
external_id = request.external_req_id
if role == "uncond":
if not external_id.endswith(suffix):
raise ValueError("CFG companion request ID does not match its suffix")
pair_id = external_id[: -len(suffix)]
else:
pair_id = external_id
pair = self._pairs.setdefault(pair_id, {})
if role in pair:
raise ValueError("Duplicate CFG branch")
pair[role] = request.request_id
self._request_pairs[request.request_id] = pair_id
super().add_request(request)
def _reserved_pages(self, request: Request) -> int:
length = min(self.max_model_len, request.num_prompt_tokens + request.max_tokens)
return -(-length // self.block_size)
def schedule(self, throttle_prefills: bool = False) -> SchedulerOutput:
self._drop_aborted_queued_requests()
self._prune_pairs()
original_waiting = self.waiting
admitted = create_request_queue(self.policy)
pending = {request.request_id: request for request in original_waiting}
pages = self._capacity - sum(self._reserved_pages(request) for request in self.running)
slots = self.max_num_running_reqs - len(self.running)
tokens = self.max_num_scheduled_tokens - len(self.running)
chosen: set[str] = set()
for request in original_waiting:
if request.request_id in chosen:
continue
pair_id = self._request_pairs.get(request.request_id)
if pair_id is None:
group = [request]
else:
pair = self._pairs[pair_id]
if len(pair) != 2 or any(rid not in pending for rid in pair.values()):
continue
group = [pending[pair[role]] for role in ("cond", "uncond")]
# Offline callers also need both unequal-length prompts to
# stop together when the longer branch reaches the context.
limit = min(min(member.max_tokens, self.max_model_len - member.num_prompt_tokens) for member in group)
for member in group:
member.max_tokens = limit
needed_pages = sum(self._reserved_pages(member) for member in group)
needed_tokens = sum(member.num_tokens - member.num_computed_tokens for member in group)
if len(group) > slots or needed_pages > pages or needed_tokens > tokens:
# Do not let later single-branch requests repeatedly occupy
# the spare slot and starve an earlier complete CFG pair.
break
for member in group:
admitted.add_request(member)
chosen.add(member.request_id)
pages -= needed_pages
tokens -= needed_tokens
slots -= len(group)
original_waiting.remove_requests([pending[rid] for rid in chosen])
self.waiting = admitted
try:
output = super().schedule(throttle_prefills)
finally:
original_waiting.prepend_requests(self.waiting)
self.waiting = original_waiting
for pair in self._pairs.values():
if len(pair) != 2:
continue
ids = list(pair.values())
scheduled = [rid in output.num_scheduled_tokens for rid in ids]
if any(scheduled) != all(scheduled):
raise RuntimeError("CFG atomic admission invariant violated: only one branch was scheduled")
if all(scheduled):
progress = [
self.requests[rid].num_computed_tokens - self.requests[rid].num_prompt_tokens for rid in ids
]
if progress[0] != progress[1]:
raise RuntimeError("CFG branches reached different generation steps")
return output
def finish_requests(self, request_ids: str | Iterable[str] | None, finished_status: RequestStatus) -> list[Request]:
if request_ids is None:
ids = None
else:
ids = {request_ids} if isinstance(request_ids, str) else set(request_ids)
for rid in tuple(ids):
pair_id = self._request_pairs.get(rid)
if pair_id is not None:
ids.update(self._pairs[pair_id].values())
finished = super().finish_requests(ids, finished_status)
self._prune_pairs()
return finished
def _prune_pairs(self) -> None:
for pair_id, pair in list(self._pairs.items()):
if all(rid not in self.requests for rid in pair.values()):
del self._pairs[pair_id]
for rid in pair.values():
self._request_pairs.pop(rid, None)
+62
View File
@@ -0,0 +1,62 @@
# Compatibility alias for breeze_tts_2.yaml; keep both profiles synchronized.
# Breeze-TTS-2, one GPU, voice design, cloning, and instruction guidance.
# Both streamed and complete HTTP responses use the stateful codec pipeline.
async_chunk: true
connectors:
shm:
name: SharedMemoryConnector
extra:
codec_streaming: true
codec_chunk_frames: 5
initial_codec_chunk_frames: 1
codec_chunk_ramp: [1, 2, 4, 5]
codec_left_context_frames: 0
decode_cudagraph_batch_sizes: [1, 2]
connector_get_sleep_s: 0.001
connector_get_max_wait_first_chunk: 1000
connector_get_max_wait: 300
stages:
- stage_id: 0
devices: "0"
max_num_seqs: 2
max_model_len: 2048
max_num_batched_tokens: 4096
gpu_memory_utilization: 0.65
kv_cache_memory_bytes: 536870912
enforce_eager: false
compilation_config:
mode: 3
cudagraph_mode: FULL_AND_PIECEWISE
cudagraph_capture_sizes: [1, 2, 32, 64, 128, 256]
enable_prefix_caching: false
enable_chunked_prefill: false
async_scheduling: false
engine_extras:
additional_config:
cfg_prefill_delay_tokens: 8
output_connectors:
to_stage_1: shm
default_sampling_params:
temperature: 0.9
top_p: 1.0
top_k: 50
repetition_penalty: 1.1
max_tokens: 1500
seed: 42
- stage_id: 1
devices: "0"
max_num_seqs: 2
max_model_len: 32768
max_num_batched_tokens: 32768
gpu_memory_utilization: 0.15
enforce_eager: false
enable_prefix_caching: false
async_scheduling: true
input_connectors:
from_stage_0: shm
default_sampling_params:
temperature: 0.0
top_p: 1.0
top_k: -1
repetition_penalty: 1.0
max_tokens: 32768
+40 -34
View File
@@ -1,55 +1,61 @@
# Breeze-TTS-2: talker -> stateful Qwen3-TTS codec decoder.
# Stage 0 batches codec frames and sends incremental codebook-major chunks.
# Breeze-TTS-2, one GPU, voice design, cloning, and instruction guidance.
# Both streamed and complete HTTP responses use the stateful codec pipeline.
async_chunk: true
trust_remote_code: true
connectors:
connector_of_shared_memory:
shm:
name: SharedMemoryConnector
extra:
codec_streaming: true
connector_get_sleep_s: 0.01
connector_get_max_wait_first_chunk: 3000
codec_chunk_frames: 5
initial_codec_chunk_frames: 1
codec_chunk_ramp: [1, 2, 4, 5]
codec_left_context_frames: 0
decode_cudagraph_batch_sizes: [1, 2]
connector_get_sleep_s: 0.001
connector_get_max_wait_first_chunk: 1000
connector_get_max_wait: 300
breeze_codec_chunk_frames: 8
stages:
- stage_id: 0
max_num_seqs: 8
gpu_memory_utilization: 0.80
enable_prefix_caching: false
# The talker derives one codec frame from the last hidden row of every
# scheduled request. A partially scheduled prompt would yield a frame from
# a mid-prompt hidden state, so keep each prefill in a single step.
enable_chunked_prefill: false
async_scheduling: true
devices: "0"
max_num_seqs: 2
max_model_len: 2048
max_num_batched_tokens: 4096
max_model_len: 4096
devices: "0"
gpu_memory_utilization: 0.65
kv_cache_memory_bytes: 536870912
enforce_eager: false
compilation_config:
mode: 3
cudagraph_mode: FULL_AND_PIECEWISE
cudagraph_capture_sizes: [1, 2, 32, 64, 128, 256]
enable_prefix_caching: false
enable_chunked_prefill: false
async_scheduling: false
engine_extras:
additional_config:
cfg_prefill_delay_tokens: 8
output_connectors:
to_stage_1: connector_of_shared_memory
to_stage_1: shm
default_sampling_params:
temperature: 0.0
temperature: 0.9
top_p: 1.0
top_k: -1
# Match upstream's penalty on generated codebook-0 history.
top_k: 50
repetition_penalty: 1.1
max_tokens: 2048
# min_tokens also masks the tokenizer's EOS 1 despite ignore_eos.
# Keep valid codec token 1 and an immediate backbone EOS available.
min_tokens: 0
max_tokens: 1500
seed: 42
- stage_id: 1
max_num_seqs: 8
devices: "0"
max_num_seqs: 2
max_model_len: 32768
max_num_batched_tokens: 32768
gpu_memory_utilization: 0.15
enforce_eager: true
enforce_eager: false
enable_prefix_caching: false
async_scheduling: true
max_num_batched_tokens: 65536
max_model_len: 65536
devices: "0"
input_connectors:
from_stage_0: shm
default_sampling_params:
temperature: 0.0
top_p: 1.0
top_k: -1
max_tokens: 65536
repetition_penalty: 1.0
max_tokens: 32768
@@ -0,0 +1,62 @@
# Breeze-TTS-2 non-streaming throughput profile, tested on one H100 80 GB.
# Both streamed and complete HTTP responses use the stateful codec pipeline.
async_chunk: true
connectors:
shm:
name: SharedMemoryConnector
extra:
codec_streaming: true
codec_chunk_frames: 16
initial_codec_chunk_frames: 16
codec_chunk_ramp: [16]
codec_left_context_frames: 0
decode_cudagraph_batch_sizes: [1, 2, 4, 8, 16, 32]
connector_get_sleep_s: 0.001
connector_get_max_wait_first_chunk: 1000
connector_get_max_wait: 300
stages:
- stage_id: 0
devices: "0"
max_num_seqs: 32
max_model_len: 2048
max_num_batched_tokens: 4096
gpu_memory_utilization: 0.65
# Reserve enough KV for thirty-two full-length requests.
kv_cache_memory_bytes: 8589934592
enforce_eager: false
compilation_config:
mode: 3
cudagraph_mode: FULL_AND_PIECEWISE
cudagraph_capture_sizes: [1, 2, 4, 8, 32, 64, 128, 256]
enable_prefix_caching: false
enable_chunked_prefill: false
async_scheduling: false
engine_extras:
additional_config:
cfg_prefill_delay_tokens: 0
output_connectors:
to_stage_1: shm
default_sampling_params:
temperature: 0.9
top_p: 1.0
top_k: 50
repetition_penalty: 1.1
max_tokens: 1500
seed: 42
- stage_id: 1
devices: "0"
max_num_seqs: 32
max_model_len: 32768
max_num_batched_tokens: 32768
gpu_memory_utilization: 0.15
enforce_eager: false
enable_prefix_caching: false
async_scheduling: true
input_connectors:
from_stage_0: shm
default_sampling_params:
temperature: 0.0
top_p: 1.0
top_k: -1
repetition_penalty: 1.0
max_tokens: 32768
@@ -0,0 +1,63 @@
# Compatibility alias for breeze_tts_2_throughput.yaml; keep both profiles synchronized.
# Breeze-TTS-2 non-streaming throughput profile, tested on one H100 80 GB.
# Both streamed and complete HTTP responses use the stateful codec pipeline.
async_chunk: true
connectors:
shm:
name: SharedMemoryConnector
extra:
codec_streaming: true
codec_chunk_frames: 16
initial_codec_chunk_frames: 16
codec_chunk_ramp: [16]
codec_left_context_frames: 0
decode_cudagraph_batch_sizes: [1, 2, 4, 8, 16, 32]
connector_get_sleep_s: 0.001
connector_get_max_wait_first_chunk: 1000
connector_get_max_wait: 300
stages:
- stage_id: 0
devices: "0"
max_num_seqs: 32
max_model_len: 2048
max_num_batched_tokens: 4096
gpu_memory_utilization: 0.65
# Reserve enough KV for thirty-two full-length requests.
kv_cache_memory_bytes: 8589934592
enforce_eager: false
compilation_config:
mode: 3
cudagraph_mode: FULL_AND_PIECEWISE
cudagraph_capture_sizes: [1, 2, 4, 8, 32, 64, 128, 256]
enable_prefix_caching: false
enable_chunked_prefill: false
async_scheduling: false
engine_extras:
additional_config:
cfg_prefill_delay_tokens: 0
output_connectors:
to_stage_1: shm
default_sampling_params:
temperature: 0.9
top_p: 1.0
top_k: 50
repetition_penalty: 1.1
max_tokens: 1500
seed: 42
- stage_id: 1
devices: "0"
max_num_seqs: 32
max_model_len: 32768
max_num_batched_tokens: 32768
gpu_memory_utilization: 0.15
enforce_eager: false
enable_prefix_caching: false
async_scheduling: true
input_connectors:
from_stage_0: shm
default_sampling_params:
temperature: 0.0
top_p: 1.0
top_k: -1
repetition_penalty: 1.0
max_tokens: 32768
+2 -4
View File
@@ -54,9 +54,7 @@ def _register_omni_hf_configs() -> None:
try:
from transformers import AutoConfig
from vllm_omni.model_executor.models.breeze_tts_2.configuration_breeze_tts_2 import (
BreezeTTS2Config,
)
from vllm_omni.model_executor.models.breeze_tts_2.configuration_breeze import BreezeConfig
from vllm_omni.model_executor.models.indextts2.configuration_indextts2 import (
IndexTTS2Config,
IndexTTS25Config,
@@ -95,6 +93,7 @@ def _register_omni_hf_configs() -> None:
_CONFIG_REGISTRY = None
for model_type, config_cls in [
("breeze", BreezeConfig),
("dense", MingDenseConfig),
("bailingmm", MingMoeConfig),
("indextts2", IndexTTS2Config),
@@ -108,7 +107,6 @@ def _register_omni_hf_configs() -> None:
("glm_tts", GLMTTSConfig),
("omnivoice", OmniVoiceConfig),
("voxcpm2", VoxCPM2Config),
("breeze", BreezeTTS2Config),
]:
try:
AutoConfig.register(model_type, config_cls)
+20 -7
View File
@@ -14,8 +14,9 @@ import asyncio
import time
import uuid
from collections.abc import AsyncGenerator, Iterable, Mapping, Sequence
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast
import anyio
from vllm import TokensPrompt
from vllm.engine.protocol import EngineClient, StreamingInput
from vllm.logger import init_logger
@@ -75,6 +76,8 @@ class AsyncOmni(AsyncOmniBase, EngineClient):
... print(output)
"""
engine: AsyncOmniEngine
def _create_engine(self, **engine_kwargs: Any) -> AsyncOmniEngine:
return AsyncOmniEngine(**engine_kwargs)
@@ -265,7 +268,7 @@ class AsyncOmni(AsyncOmniBase, EngineClient):
first_chunk_submitted = asyncio.get_running_loop().create_future()
input_stream_task = await self._add_streaming_input_request(
request_id=request_id,
input_stream=prompt,
input_stream=cast(AsyncGenerator, prompt),
sampling_params_list=req_sp_list,
final_stage_id=final_stage_id_for_e2e,
final_output_stage_ids=final_output_stage_ids,
@@ -285,7 +288,8 @@ class AsyncOmni(AsyncOmniBase, EngineClient):
lora_request=lora_request,
)
submit_ts = time.time()
req_state.metrics.stage_first_ts[0] = submit_ts
stage_first_ts = cast(list[float | None], req_state.metrics.stage_first_ts)
stage_first_ts[0] = submit_ts
req_start_ts[request_id] = submit_ts
if admitting:
await self._release_generate_admission()
@@ -310,6 +314,9 @@ class AsyncOmni(AsyncOmniBase, EngineClient):
except (asyncio.CancelledError, GeneratorExit):
self._record_request_failure_once(request_id, reason="client_disconnect")
# ASGI cancellation also affects subsequent awaits. Shield the
# bounded engine abort before removing the local request state.
with anyio.CancelScope(shield=True):
await self._abort_internal_requests(request_id, timeout=ABORT_TIMEOUT_S)
logger.info(f"[AsyncOmni] Request {request_id} aborted.")
raise
@@ -359,6 +366,7 @@ class AsyncOmni(AsyncOmniBase, EngineClient):
# only check thinker's sampling params now
stage0_params = sampling_params_list[0]
self._validate_streaming_input_sampling_params(stage0_params)
stage0_params = cast(SamplingParams, stage0_params)
req_state = self.request_states[request_id]
has_submitted_first_chunk = False
@@ -801,7 +809,8 @@ class AsyncOmni(AsyncOmniBase, EngineClient):
async with self._pause_cond:
return self._paused
async def start_profile(
# EngineClient exposes async operations; OmniBase implements the sync API.
async def start_profile( # type: ignore[override]
self,
profile_prefix: str | None = None,
stages: list[int] | None = None,
@@ -816,7 +825,7 @@ class AsyncOmni(AsyncOmniBase, EngineClient):
"""
return await self.collective_rpc(method="profile", args=(True, profile_prefix), stage_ids=stages)
async def stop_profile(self, stages: list[int] | None = None) -> list[Any]:
async def stop_profile(self, stages: list[int] | None = None) -> list[Any]: # type: ignore[override]
"""Stop profiling specified stages.
Uses vLLM-compatible profile(is_start=False) interface.
@@ -1103,10 +1112,14 @@ class AsyncOmni(AsyncOmniBase, EngineClient):
"""Get tokenizer for the comprehension stage."""
stage_index = self._get_comprehension_stage_index()
if stage_index is not None:
tokenizer = self.engine.output_processors[stage_index].tokenizer
processor = self.engine.output_processors[stage_index]
assert processor is not None
tokenizer = processor.tokenizer
if tokenizer is not None:
return tokenizer
return self.input_processor.tokenizer # type: ignore[return-value]
processor = self.input_processor
assert processor is not None
return processor.tokenizer
async def is_tracing_enabled(self) -> bool:
"""Check if tracing is enabled."""
+6 -1
View File
@@ -71,6 +71,9 @@ class AsyncEventResolver:
if tid is None and isinstance(ack, dict):
tid = ack.get("task_id")
if tid is None:
logger.warning("Received ACK without a task_id")
return
async with self._lock:
task_info = self._pending_tasks.get(tid)
@@ -382,6 +385,7 @@ class AsyncOmniBase(OmniBase):
if should_continue:
continue
assert req_state is not None
req_state.stage_id = stage_id
# Route to the per-request queue
@@ -563,7 +567,8 @@ class AsyncOmniBase(OmniBase):
# ==================== EngineClient Interface ====================
async def check_health(self) -> None:
# The async entrypoints expose the synchronous base health check as an awaitable.
async def check_health(self) -> None: # type: ignore[override]
"""Check engine health by verifying the Orchestrator process is alive."""
OmniBase.check_health(self)
@@ -22,6 +22,7 @@ class ClientRequestState:
self.final_stage_id: int | None = final_stage_id
self.queue = queue if queue is not None else asyncio.Queue()
self.metrics: OrchestratorAggregator | None = None
self.input_stream_task: asyncio.Task | None = None
# Request-scoped idempotency guard for Prometheus failure counters.
self.failure_recorded = False
# Wall-clock time at which the user's request arrived in the engine
@@ -1807,6 +1807,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
finally:
if not artifact_ready:
self._discard_ref_audio_artifact_warmup(request_id)
# Disconnects can arrive while suspended at yield. Closing the
# engine iterator must survive the cancelled ASGI scope.
close = getattr(generator, "aclose", None)
if close is not None:
with anyio.CancelScope(shield=True):
@@ -1,262 +1,188 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Breeze-TTS-2 serving adapter for synchronous full-payload generation."""
import asyncio
import copy
import math
from typing import TYPE_CHECKING, Any
import numpy as np
import pybase64 as base64
from transformers import AutoTokenizer, PreTrainedTokenizerBase
from vllm.inputs import TokensPrompt
from vllm.logger import init_logger
from vllm.sampling_params import SamplingParams
from vllm.utils.async_utils import make_async
from vllm_omni.entrypoints.openai.protocol.audio import OpenAICreateSpeechRequest
from vllm_omni.entrypoints.openai.tts_adapters import register_tts_adapter
from vllm_omni.entrypoints.openai.tts_adapters.base import (
ARTTSAdapter,
PreparedRequest,
SpeechServingContext,
apply_max_new_tokens,
conditioning_cache_salt,
)
from vllm_omni.model_executor.models.breeze_tts_2.audio_tokenizer import (
BreezeReferenceAudioTokenizer,
)
from vllm_omni.model_executor.models.breeze_tts_2.prompt_builder import (
BreezeTTS2PromptBuilder,
)
from vllm_omni.model_executor.models.breeze_tts_2.prompt import DEFAULT_INSTRUCTION, build_breeze_prompt
if TYPE_CHECKING:
from vllm_omni.entrypoints.openai.protocol.audio import OpenAICreateSpeechRequest
logger = init_logger(__name__)
@register_tts_adapter
class BreezeTTS2Adapter(ARTTSAdapter):
"""Build Breeze prompts before scheduler submission.
The adapter owns only request-facing concerns. Model workers still own the
stage-0 embeddings and stage-1 waveform decode.
"""
stage_keys = frozenset({"breeze_tts_2"})
model_archs = frozenset({"BreezeForConditionalGeneration"})
name = "breeze_tts_2"
# Breeze's architecture and stage key are unique, so this detector cannot
# collide with another adapter. Lower priority runs first; 9 keeps it
# ahead of the default-priority (100) detectors.
stage_keys = frozenset({"breeze_tts_2", "breeze_tts_2_codec"})
model_archs = frozenset({"BreezeForConditionalGeneration"})
detect_priority = 9
supported_output_sample_rates = frozenset({24000})
def _load_supported_speakers(self) -> set[str]:
# Breeze accepts arbitrary speaker tags such as S0/S1 in its prompt;
# there is no finite checkpoint speaker inventory to advertise.
return set()
def __init__(self, ctx: SpeechServingContext) -> None:
super().__init__(ctx)
self.tokenizer: PreTrainedTokenizerBase | None = None
self._build_async = make_async(self._build_prompt, executor=ctx.server._tts_executor)
def _load_codec_frame_rate(self) -> float | None:
assert self.ctx.engine_client is not None
config = self.ctx.engine_client.model_config.hf_config
codec = getattr(config, "codec_config", None)
if isinstance(codec, dict):
frame_rate = codec.get("_frame_rate")
if frame_rate is not None:
return float(frame_rate)
return 12.5
def _load_codec_frame_rate(self) -> float:
# Breeze-TTS-2 uses 1,920 waveform samples per frame at 24 kHz.
return 24000 / 1920
def validate(self, request: "OpenAICreateSpeechRequest") -> str | None:
if not request.input or not request.input.strip():
return "Breeze-TTS-2 input text cannot be empty"
if request.ref_text is not None and not request.ref_text.strip():
return "Breeze-TTS-2 ref_text cannot be empty"
if request.ref_text is not None and request.ref_audio is None:
return "Breeze-TTS-2 ref_text requires ref_audio"
if request.ref_audio is not None and (request.ref_text is None or not request.ref_text.strip()):
return "Breeze-TTS-2 ref_audio requires ref_text"
if isinstance(request.ref_audio, list) and len(request.ref_audio) != 1:
# The prompt path conditions on exactly one reference waveform;
# extra clips would be silently discarded otherwise.
return f"Breeze-TTS-2 supports exactly one reference clip, got {len(request.ref_audio)}"
if request.task_type == "Base" and request.ref_audio is None:
return "Breeze-TTS-2 Base task requires ref_audio and ref_text"
if request.task_type == "VoiceDesign":
return "Breeze-TTS-2 does not support task_type=VoiceDesign"
async def warmup(self) -> None:
"""Prime text, depth and reference paths through the ordinary runner."""
server = self.ctx.server
logger.info("Warming Breeze text graphs, CFG branches, reference encoder and streaming codec")
async def generate(text: str, request_id: str, **kwargs: object) -> bytes:
request = OpenAICreateSpeechRequest(
input=text, model=server.model_name, response_format="wav", seed=42, max_new_tokens=64, **kwargs
)
audio, _ = await server._generate_audio_bytes(request, request_id=request_id)
return audio
reference_text = "Welcome to this demonstration of clear and natural speech synthesis."
reference = await generate(reference_text, "breeze-warmup-single")
await generate(
"Hello.",
"breeze-warmup-cfg",
extra_params={"guidance_scale": 4.0, "temperature": 0.7, "top_k": 100, "top_p": 0.8},
)
await generate(
"Hello again.",
"breeze-warmup-reference",
ref_audio="data:audio/wav;base64," + base64.b64encode(reference).decode("ascii"),
ref_text=reference_text,
extra_params={"guidance_scale": 4.0},
)
await asyncio.gather(
generate(reference_text, "breeze-warmup-batch-a"),
generate("Thank you for listening to this clear and natural voice demonstration.", "breeze-warmup-batch-b"),
)
logger.info("Breeze speech warmup complete")
def validate(self, request: OpenAICreateSpeechRequest) -> str | None:
if not request.input.strip():
return "Input text cannot be empty"
if request.speed is not None and request.speed != 1.0:
return "Breeze-TTS-2 does not support speed adjustment"
return "Breeze does not support speed adjustment"
if request.language is not None:
return "Breeze-TTS-2 does not support 'language'; the prompt language follows the input text"
if request.speaker_embedding is not None:
return "Breeze-TTS-2 does not support 'speaker_embedding' (voice-cloning helper field)"
return "Breeze infers the language from the input text; the language field is not supported"
if request.ref_audio_2 is not None or request.speaker_embedding is not None:
return "Breeze accepts one reference recording and its transcript"
has_reference = request.ref_audio is not None
if has_reference:
reference = request.ref_audio
if isinstance(reference, list):
if len(reference) != 1:
return "Breeze supports exactly one reference recording"
reference = reference[0]
if not isinstance(reference, str) or not isinstance(request.ref_text, str) or not request.ref_text.strip():
return "Breeze voice cloning requires one ref_audio URL and a non-empty ref_text transcript"
error = self.ctx.server._validate_ref_audio_format(reference)
if error:
return error
elif request.ref_text is not None:
return "Breeze ref_text requires ref_audio"
expected_task = "Base" if has_reference else "VoiceDesign"
if request.task_type not in (None, "CustomVoice", expected_task):
return f"Breeze requires task_type='{expected_task}' for this conditioning"
if request.x_vector_only_mode:
return "Breeze-TTS-2 does not support 'x_vector_only_mode' (voice-cloning helper field)"
extra_params = request.extra_params or {}
guidance_scale = extra_params.get("guidance_scale", extra_params.get("cfg_scale", 1.0))
try:
unsupported_guidance = guidance_scale is not None and float(guidance_scale) != 1.0
except (TypeError, ValueError):
return "Breeze-TTS-2 guidance_scale must be a number"
if unsupported_guidance:
return "Breeze-TTS-2 currently supports only guidance_scale=1.0"
if extra_params.get("negative_prompt"):
return "Breeze-TTS-2 does not support negative_prompt/CFG yet"
# The talker selects codebook 0 by argmax inside make_omni_output and
# ignores the scheduler's sampled token, so non-greedy overrides would
# not change the audio while still letting the sampler draw an early
# EOS. Reject them until the talker consumes the sampled code.
greedy_error = _validate_greedy_sampling(extra_params)
if greedy_error is not None:
return greedy_error
repetition_penalty = extra_params.get("repetition_penalty")
if repetition_penalty is not None:
try:
repetition_penalty = float(repetition_penalty)
except (TypeError, ValueError):
return "Breeze-TTS-2 repetition_penalty must be finite and positive"
if not math.isfinite(repetition_penalty) or repetition_penalty <= 0:
return "Breeze-TTS-2 repetition_penalty must be finite and positive"
if request.max_new_tokens is not None:
if request.max_new_tokens < self.max_new_tokens_min:
return f"max_new_tokens must be at least {self.max_new_tokens_min}"
if request.max_new_tokens > self.max_new_tokens_max:
return f"max_new_tokens cannot exceed {self.max_new_tokens_max}"
if request.ref_audio is not None:
ref_audio = request.ref_audio[0] if isinstance(request.ref_audio, list) else request.ref_audio
return self.ctx.server._validate_ref_audio_format(ref_audio)
return "Breeze does not support x_vector_only_mode"
extra = request.extra_params or {}
supported = {"guidance_scale", "cfg_scale", "temperature", "top_k", "top_p", "repetition_penalty"}
if unknown := set(extra) - supported:
return f"Unsupported Breeze parameters: {sorted(unknown)}"
for key, value in extra.items():
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
return f"Breeze {key} must be a finite number"
if extra.get("guidance_scale", extra.get("cfg_scale", 1.0)) <= 0 or extra.get("repetition_penalty", 1.1) <= 0:
return "Breeze guidance_scale and repetition_penalty must be positive"
if extra.get("temperature", 0.9) < 0 or not 0 < extra.get("top_p", 1.0) <= 1:
return "Breeze requires temperature >= 0 and 0 < top_p <= 1"
top_k = extra.get("top_k", 50)
if not isinstance(top_k, int) or top_k < -1:
return "Breeze top_k must be -1, 0, or a positive integer"
return None
def _build_prompt(
self,
request: OpenAICreateSpeechRequest,
sampling: SamplingParams,
reference: tuple[np.ndarray, int] | None,
) -> TokensPrompt:
if self.tokenizer is None:
engine_client = self.ctx.engine_client
if engine_client is None:
raise RuntimeError("Breeze speech serving requires an engine client")
model = engine_client.model_config.model
self.tokenizer = AutoTokenizer.from_pretrained(model, config=engine_client.model_config.hf_config)
extra = request.extra_params or {}
return build_breeze_prompt(
self.tokenizer,
request.input,
DEFAULT_INSTRUCTION if request.instructions is None else request.instructions,
speaker=request.voice if request.voice and request.voice != "default" else "S0",
ref_audio=reference,
ref_text=request.ref_text,
guidance_scale=float(extra.get("guidance_scale", extra.get("cfg_scale", 1.0))),
temperature=float(extra.get("temperature", sampling.temperature)),
top_k=max(int(extra.get("top_k", sampling.top_k)), 0),
top_p=float(extra.get("top_p", sampling.top_p)),
repetition_penalty=float(extra.get("repetition_penalty", sampling.repetition_penalty)),
)
async def build(
self,
request: "OpenAICreateSpeechRequest",
sampling_params_list: list,
request: OpenAICreateSpeechRequest,
sampling_params_list: list[SamplingParams],
has_inline_ref_audio: bool,
) -> PreparedRequest:
del has_inline_ref_audio
server = self.ctx.server
builder = await self._get_builder()
template = self._resolve_template(request)
payload: dict[str, Any] = {
"text": request.input,
"template": template,
"speaker": request.voice or "S0",
}
if request.instructions:
payload["instruction"] = request.instructions
if request.ref_text:
payload["ref_text"] = request.ref_text
reference = None
if request.ref_audio is not None:
ref_audio = request.ref_audio[0] if isinstance(request.ref_audio, list) else request.ref_audio
wav_list, sr, cache_key = await server._resolve_ref_audio(ref_audio)
payload["ref_audio"] = np.asarray(wav_list, dtype=np.float32)
payload["ref_audio_sample_rate"] = int(sr)
else:
cache_key = None
prompt = await asyncio.to_thread(builder.build, payload, template=template)
# The AR scheduler's completion budget is measured in codebook-0
# frames for Breeze. Preserve it in the mutable request metadata so
# the talker can emit its own EOS before a scheduler length cutoff.
max_new_frames = request.max_new_tokens
if max_new_frames is None and sampling_params_list:
max_new_frames = getattr(sampling_params_list[0], "max_tokens", None)
if max_new_frames is not None:
prompt["additional_information"]["breeze_max_new_frames"] = int(max_new_frames)
tts_params = {
"template": [template],
"text": [request.input],
}
if cache_key:
tts_params["ref_audio_cache_key"] = cache_key
prompt["cache_salt"] = conditioning_cache_salt(request, tts_params)
return PreparedRequest(prompt=prompt, tts_params=tts_params, model_type=self.name)
async def _get_builder(self) -> BreezeTTS2PromptBuilder:
server = self.ctx.server
cached = getattr(server, "_breeze_tts_2_prompt_builder", None)
if cached is not None:
return cached
# Multiple speech requests can arrive while the first tokenizer load
# is still in progress. Serialize the one-time CPU initialization so
# model files are not opened and decoded repeatedly.
lock = getattr(server, "_breeze_tts_2_prompt_builder_lock", None)
if lock is None:
lock = asyncio.Lock()
server._breeze_tts_2_prompt_builder_lock = lock
async with lock:
cached = getattr(server, "_breeze_tts_2_prompt_builder", None)
if cached is not None:
return cached
model_path = server.engine_client.model_config.model
config = server.engine_client.model_config.hf_config
audio_tokenizer = await asyncio.to_thread(
BreezeReferenceAudioTokenizer.from_pretrained,
model_path,
num_codebooks=int(getattr(config, "num_codebooks", 16)),
codebook_size=self._codebook_size(config),
device_map="cpu",
)
builder = await asyncio.to_thread(
BreezeTTS2PromptBuilder.from_pretrained,
model_path,
config,
reference_audio_encoder=audio_tokenizer,
)
server._breeze_tts_2_prompt_builder = builder
return builder
@staticmethod
def _codebook_size(config: Any) -> int:
codec = getattr(config, "codec_config", None)
if isinstance(codec, dict):
return int(codec.get("codebook_size", 2048))
return int(getattr(codec, "codebook_size", 2048))
@staticmethod
def _resolve_template(request: "OpenAICreateSpeechRequest") -> str:
if request.ref_audio is not None:
return "ref_edit_tata" if request.instructions else "ref_clone_tata"
return "tts_instruction" if request.instructions else "tts_plain"
audio = request.ref_audio[0] if isinstance(request.ref_audio, list) else request.ref_audio
waveform, sample_rate, _ = await self.ctx.server._resolve_ref_audio(audio)
reference = (np.asarray(waveform, dtype=np.float32), sample_rate)
prompt = await self._build_async(request, sampling_params_list[0], reference)
return PreparedRequest(prompt=prompt, model_type=self.name)
def apply_sampling_overrides(
self,
sampling_params_list: list,
request: "OpenAICreateSpeechRequest",
prompt: dict[str, Any] | None = None,
sampling_params_list: list[SamplingParams],
request: OpenAICreateSpeechRequest,
prompt: dict | None = None,
request_id: str | None = None,
) -> list:
del prompt, request_id
sampling_params_list = apply_max_new_tokens(sampling_params_list, request)
repetition_penalty = (request.extra_params or {}).get("repetition_penalty")
if repetition_penalty is not None:
# The generic speech handler copies extra_params to extra_args;
# promote this value so the worker receives sampling metadata.
# Only this scalar changes; preserve the caller's params without
# copying nested state or the unchanged codec-stage parameters.
sampling_params_list = [copy.copy(sampling_params_list[0]), *sampling_params_list[1:]]
sampling_params_list[0].repetition_penalty = float(repetition_penalty)
return sampling_params_list
def _validate_greedy_sampling(extra_params: dict[str, Any]) -> str | None:
"""Return an error unless the sampling overrides keep decoding greedy."""
temperature = extra_params.get("temperature")
if temperature is not None:
try:
if float(temperature) != 0.0:
return "Breeze-TTS-2 currently supports only greedy decoding (temperature=0)"
except (TypeError, ValueError):
return "Breeze-TTS-2 temperature must be a number"
top_p = extra_params.get("top_p")
if top_p is not None:
try:
if float(top_p) != 1.0:
return "Breeze-TTS-2 currently supports only greedy decoding (top_p=1.0)"
except (TypeError, ValueError):
return "Breeze-TTS-2 top_p must be a number"
top_k = extra_params.get("top_k")
if top_k is not None:
try:
if int(top_k) not in (-1, 0, 1):
return "Breeze-TTS-2 currently supports only greedy decoding (top_k=-1)"
except (TypeError, ValueError):
return "Breeze-TTS-2 top_k must be an integer"
return None
__all__ = ["BreezeTTS2Adapter"]
) -> list[SamplingParams]:
params = apply_max_new_tokens(sampling_params_list, request)
params = list(params)
params[0] = params[0].clone()
if params[0].top_k == 0:
params[0].top_k = -1
extra = request.extra_params or {}
if "repetition_penalty" in extra:
params[0].repetition_penalty = extra["repetition_penalty"]
if prompt is not None:
engine_client = self.ctx.engine_client
if engine_client is None:
raise RuntimeError("Breeze speech serving requires an engine client")
model_config = engine_client.model_config
available = model_config.max_model_len - len(prompt["prompt_token_ids"])
if available <= 0:
raise ValueError("Breeze conditioning fills the model context; shorten the text or reference audio")
# Both CFG branches must stop after the same number of frames,
# including when the conditioned prompt reaches the context limit.
params[0].max_tokens = min(params[0].max_tokens, available)
return params
@@ -0,0 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from .code2wav import BreezeCode2Wav
from .modeling_breeze import BreezeForConditionalGeneration
__all__ = ["BreezeCode2Wav", "BreezeForConditionalGeneration"]
@@ -1,149 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Reference-audio encoding helpers for Breeze-TTS-2.
The Breeze prompt uses a Qwen3-TTS tokenizer only for reference audio. This
module deliberately does not contain text tokenization or waveform decoding;
the latter belongs to the pipeline's codec stage.
"""
from __future__ import annotations
from collections.abc import Mapping
from pathlib import Path
from typing import Any
import numpy as np
import torch
def resolve_audio_tokenizer_path(model_path: str) -> Path | None:
"""Resolve the bundled ``audio_tokenizer`` dir for a local path or HF repo id.
Local directories (dev checkouts) resolve directly: present returns the
subdirectory, absent returns ``None`` so callers can fall back. Repo ids
resolve through the vLLM-tagged HF helpers: an already-complete cached
snapshot is preferred (offline deployments keep working), and a partial
cache falls through to a targeted online fetch restricted to the bundled
tokenizer subtree. Returns ``None`` when the checkpoint carries no
``audio_tokenizer`` subdirectory.
"""
root = Path(model_path)
if root.is_dir():
bundled = root / "audio_tokenizer"
return bundled if bundled.is_dir() else None
from vllm.transformers_utils.repo_utils import hf_api
kwargs: dict[str, object] = {"allow_patterns": ["audio_tokenizer/*"]}
try:
cached = Path(hf_api().snapshot_download(model_path, local_files_only=True, **kwargs))
except Exception:
cached = None
if cached is not None and (cached / "audio_tokenizer").is_dir():
return cached / "audio_tokenizer"
online = Path(hf_api().snapshot_download(model_path, **kwargs))
bundled = online / "audio_tokenizer"
return bundled if bundled.is_dir() else None
class BreezeReferenceAudioTokenizer:
"""Small adapter that normalizes Qwen3-TTS encoder output to ``(T, Q)``.
The underlying tokenizer is created once per serving worker and is reused
for all requests. Keeping this wrapper independent from the
prompt builder makes it straightforward to inject a fake encoder in tests
and to replace the upstream Qwen3 implementation later.
"""
def __init__(self, tokenizer: Any, *, num_codebooks: int = 16, codebook_size: int = 2048) -> None:
self.tokenizer = tokenizer
self.num_codebooks = int(num_codebooks)
self.codebook_size = int(codebook_size)
if self.num_codebooks <= 0 or self.codebook_size <= 0:
raise ValueError("num_codebooks and codebook_size must be positive")
@classmethod
def from_pretrained(
cls,
model_path: str,
*,
num_codebooks: int = 16,
codebook_size: int = 2048,
**kwargs: Any,
) -> BreezeReferenceAudioTokenizer:
"""Load the bundled ``audio_tokenizer`` with the existing Qwen wrapper."""
from vllm_omni.model_executor.models.qwen3_tts.qwen3_tts_tokenizer import (
Qwen3TTSTokenizer,
)
resolved = resolve_audio_tokenizer_path(model_path)
if resolved is None:
raise FileNotFoundError(
"Breeze reference audio tokenizer was not found for "
f"{model_path} (expected an audio_tokenizer/ subdirectory)"
)
tokenizer = Qwen3TTSTokenizer.from_pretrained(str(resolved), **kwargs)
return cls(tokenizer, num_codebooks=num_codebooks, codebook_size=codebook_size)
@torch.inference_mode()
def encode(self, audio: Any, sample_rate: int | None = None) -> torch.Tensor:
"""Encode one waveform or audio source into ``(T, Q)`` codes."""
if isinstance(audio, torch.Tensor):
audio = audio.detach().cpu().numpy()
elif isinstance(audio, list) and audio and all(isinstance(x, (int, float)) for x in audio):
audio = np.asarray(audio, dtype=np.float32)
if isinstance(audio, np.ndarray) and audio.ndim > 1:
# Accept both common layouts: (samples, channels) and
# (channels, samples). Qwen3TTSTokenizer expects one 1-D clip.
channel_first = audio.shape[0] <= 8 and audio.shape[-1] > audio.shape[0]
audio = audio.mean(axis=0 if channel_first else -1)
if isinstance(audio, np.ndarray) and sample_rate is None:
raise ValueError("sample_rate is required when ref_audio is a numpy waveform")
encoded = self.tokenizer.encode(audio, sr=sample_rate)
return self._normalize_codes(encoded)
def _normalize_codes(self, encoded: Any) -> torch.Tensor:
codes = getattr(encoded, "audio_codes", None)
if codes is None and isinstance(encoded, Mapping):
codes = encoded.get("audio_codes")
if codes is None and isinstance(encoded, (tuple, list)):
codes = encoded[0] if encoded else None
if codes is None:
raise ValueError("reference audio tokenizer returned no audio_codes")
if isinstance(codes, (tuple, list)) and codes:
# Qwen3TTSTokenizer returns one tensor per input waveform.
if len(codes) != 1:
raise ValueError(f"only one reference waveform is supported, got {len(codes)}")
codes = codes[0]
codes = torch.as_tensor(codes)
if codes.ndim == 3:
if codes.shape[0] != 1:
raise ValueError(f"only one reference waveform is supported, got {tuple(codes.shape)}")
codes = codes[0]
if codes.ndim != 2:
raise ValueError(f"reference audio codes must be 2D, got {tuple(codes.shape)}")
if codes.shape[0] == 0:
raise ValueError("reference audio tokenizer returned no frames")
if codes.shape[-1] != self.num_codebooks:
if codes.shape[0] == self.num_codebooks:
codes = codes.transpose(0, 1)
else:
raise ValueError(
f"reference audio codes must have {self.num_codebooks} codebooks, got {tuple(codes.shape)}"
)
codes = codes.to(device="cpu", dtype=torch.long).contiguous()
if codes.numel() and (int(codes.min()) < 0 or int(codes.max()) >= self.codebook_size):
raise ValueError(
f"reference audio code outside [0, {self.codebook_size}): "
f"min={int(codes.min())}, max={int(codes.max())}"
)
# int16 is enough for Mimi/Qwen3-TTS codes and halves IPC/cache memory.
return codes.to(dtype=torch.int16)
__all__ = ["BreezeReferenceAudioTokenizer", "resolve_audio_tokenizer_path"]
@@ -0,0 +1,13 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from vllm_omni.model_executor.models.qwen3_tts.qwen3_tts_code2wav import Qwen3TTSCode2Wav
class BreezeCode2Wav(Qwen3TTSCode2Wav):
"""Breeze bundles the same Qwen3 codec under audio_tokenizer/."""
tokenizer_subfolder = "audio_tokenizer"
# Reference conditioning is consumed by the AR stage. The codec receives
# generated codes only, so it never decodes an ICL reference prefix.
decoder_cudagraph_modes = ("xvec",)
@@ -0,0 +1,47 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Configuration for the released Breeze-TTS-2 checkpoint."""
from transformers import LlamaConfig, PretrainedConfig, Qwen3Config
from transformers.models.t5gemma2.configuration_t5gemma2 import T5Gemma2TextConfig
class BreezeConfig(PretrainedConfig):
model_type = "breeze"
def __init__(
self,
backbone_config: dict | None = None,
depth_decoder_config: dict | None = None,
text_encoder_config: dict | None = None,
num_codebooks: int = 16,
audio_vocab_size: int = 2051,
audio_embed_size: int = 2048,
max_position_embeddings: int = 2048,
rope_scaling: dict | None = None,
rope_theta: float = 500000.0,
**kwargs: object,
) -> None:
self.max_position_embeddings = max_position_embeddings
self.rope_parameters = {"rope_theta": rope_theta, **(rope_scaling or {"rope_type": "default"})}
self.num_codebooks = num_codebooks
self.audio_vocab_size = audio_vocab_size
self.audio_embed_size = audio_embed_size
self.backbone_config = Qwen3Config(**(backbone_config or {}))
self.depth_decoder_config = LlamaConfig(**(depth_decoder_config or {}))
self.text_encoder_config = T5Gemma2TextConfig(**(text_encoder_config or {}))
# The scheduler samples continuation/EOS tokens. Text encoder IDs are
# carried in the prompt payload and are outside this vocabulary.
self.vocab_size = audio_vocab_size + 1
self.eos_token_id = audio_vocab_size
self.backbone_config.vocab_size = self.vocab_size
self.backbone_config.bos_token_id = 0
self.backbone_config.pad_token_id = 0
self.backbone_config.eos_token_id = self.eos_token_id
self.backbone_config.tie_word_embeddings = False
super().__init__(**kwargs)
self.vocab_size = audio_vocab_size + 1
self.eos_token_id = audio_vocab_size
def get_text_config(self, decoder: bool = False) -> Qwen3Config:
return self.backbone_config
@@ -1,366 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Configuration classes for Breeze-TTS-2."""
from __future__ import annotations
from typing import Any
from transformers import AutoConfig
from transformers.configuration_utils import PretrainedConfig
from transformers.models.qwen3 import Qwen3Config
class BreezeTTS2Config(PretrainedConfig):
"""Configuration for ``BreezeForConditionalGeneration``."""
model_type = "breeze"
def __init__(
self,
backbone_config: dict | Qwen3Config | None = None,
text_encoder_config: dict | BreezeTTS2TextEncoderConfig | None = None,
depth_decoder_config: dict | BreezeTTS2DepthDecoderConfig | None = None,
codec_config: dict | None = None,
architectures: list[str] | None = None,
attention_bias: bool = False,
attention_dropout: float = 0.0,
audio_embed_size: int = 2048,
audio_eos_token_id: int = 262145,
audio_num_codebooks: int = 16,
audio_token_id: int = 262144,
audio_vocab_size: int = 2051,
backbone_flavor: str = "llama-1B",
backbone_model_name_or_path: str | None = None,
backbone_model_type: str = "qwen3",
bos_token_id: int = 2,
codebook_eos_token_id: int = 0,
codebook_pad_token_id: int = 2050,
decoder_flavor: str = "llama-100M",
depth_header_loss_weight: float = 1.0,
dtype: str = "bfloat16",
eos_token_id: int = 1,
head_dim: int = 128,
hidden_act: str = "silu",
hidden_size: int = 2048,
initializer_range: float = 0.02,
intermediate_size: int = 6144,
max_position_embeddings: int = 2048,
mlp_bias: bool = False,
num_attention_heads: int = 16,
num_codebooks: int = 16,
num_hidden_layers: int = 28,
num_key_value_heads: int = 8,
pad_token_id: int = 0,
rms_norm_eps: float = 1e-5,
rope_scaling: dict | None = None,
rope_theta: float = 500_000.0,
text_encoder_bucket_max_length_ratio: float = 4.0,
text_encoder_lora_config: dict | None = None,
text_encoder_proj_type: str = "linear",
text_encoder_special_tokens_config: dict | None = None,
text_vocab_size: int = 262158,
tie_codebooks_embeddings: bool = True,
tie_word_embeddings: bool = False,
use_cache: bool = True,
vocab_size: int = 2051,
sampling_rate: int = 24000,
codec_model_name_or_path: str = "kyutai/mimi",
**kwargs: Any,
) -> None:
serialized_rope_parameters = kwargs.pop("rope_parameters", None)
if rope_scaling is None and serialized_rope_parameters is not None:
rope_scaling = serialized_rope_parameters
# Breeze stores the Qwen3 backbone in backbone_config. This follows the
# MOSS-TTS pattern and deliberately does not declare sub_configs.
if backbone_config is None:
backbone_config = {}
if isinstance(backbone_config, dict):
backbone_config = dict(backbone_config)
backbone_config.pop("model_type", None)
self.backbone_config = Qwen3Config(**backbone_config)
else:
self.backbone_config = backbone_config
if text_encoder_config is None:
text_encoder_config = {}
if isinstance(text_encoder_config, dict):
text_encoder_config = dict(text_encoder_config)
text_encoder_config.pop("model_type", None)
self.text_encoder_config = BreezeTTS2TextEncoderConfig(**text_encoder_config)
else:
self.text_encoder_config = text_encoder_config
if depth_decoder_config is None:
depth_decoder_config = {}
if isinstance(depth_decoder_config, dict):
depth_decoder_config = dict(depth_decoder_config)
depth_decoder_config.pop("model_type", None)
self.depth_decoder_config = BreezeTTS2DepthDecoderConfig(**depth_decoder_config)
else:
self.depth_decoder_config = depth_decoder_config
# Nested configs must exist before the parent constructor: Transformers
# 5.x may call get_text_config() during config validation.
super().__init__(
architectures=architectures,
bos_token_id=bos_token_id,
eos_token_id=eos_token_id,
pad_token_id=pad_token_id,
tie_word_embeddings=tie_word_embeddings,
**kwargs,
)
self.attention_bias = attention_bias
self.attention_dropout = attention_dropout
self.audio_embed_size = audio_embed_size
self.audio_eos_token_id = audio_eos_token_id
self.audio_num_codebooks = audio_num_codebooks
self.audio_token_id = audio_token_id
self.audio_vocab_size = audio_vocab_size
# Keep Mimi's nested config as a plain checkpoint dictionary. Stage 1
# owns codec construction; stage 0 only needs codebook_size here.
self.codec_config = codec_config
self.backbone_flavor = backbone_flavor
self.backbone_model_name_or_path = backbone_model_name_or_path
self.backbone_model_type = backbone_model_type
self.codebook_eos_token_id = codebook_eos_token_id
self.codebook_pad_token_id = codebook_pad_token_id
self.decoder_flavor = decoder_flavor
self.depth_header_loss_weight = depth_header_loss_weight
self.dtype = dtype
self.head_dim = head_dim
self.hidden_act = hidden_act
self.hidden_size = hidden_size
self.initializer_range = initializer_range
self.intermediate_size = intermediate_size
self.max_position_embeddings = max_position_embeddings
self.mlp_bias = mlp_bias
self.num_attention_heads = num_attention_heads
self.num_codebooks = num_codebooks
self.num_hidden_layers = num_hidden_layers
self.num_key_value_heads = num_key_value_heads
self.rms_norm_eps = rms_norm_eps
if rope_scaling is not None:
# Preserve the upstream legacy field while also satisfying the
# normalized ``rope_parameters`` validation added in Transformers 5.
rope_scaling = dict(rope_scaling)
rope_scaling.setdefault("rope_type", rope_scaling.get("type", "default"))
rope_scaling.setdefault("rope_theta", rope_theta)
self.rope_scaling = rope_scaling
self.rope_theta = rope_theta
self.text_encoder_bucket_max_length_ratio = text_encoder_bucket_max_length_ratio
self.text_encoder_lora_config = text_encoder_lora_config or {}
self.text_encoder_proj_type = text_encoder_proj_type
self.text_encoder_special_tokens_config = text_encoder_special_tokens_config or {}
self.text_vocab_size = text_vocab_size
self.tie_codebooks_embeddings = tie_codebooks_embeddings
self.use_cache = use_cache
self.vocab_size = vocab_size
self.sampling_rate = sampling_rate
self.codec_model_name_or_path = codec_model_name_or_path
if not getattr(self, "architectures", None):
self.architectures = ["BreezeForConditionalGeneration"]
def get_text_config(self, **_: Any) -> Qwen3Config:
return self.backbone_config
class BreezeTTS2TextEncoderConfig(PretrainedConfig):
"""Configuration for Breeze's T5Gemma2 text encoder."""
model_type = "t5gemma2_text"
def __init__(
self,
hidden_size: int = 1152,
intermediate_size: int = 6912,
num_hidden_layers: int = 26,
num_attention_heads: int = 4,
num_key_value_heads: int = 1,
head_dim: int = 256,
max_position_embeddings: int = 32768,
hidden_activation: str = "gelu_pytorch_tanh",
rms_norm_eps: float = 1e-6,
rope_theta: float = 10_000.0,
sliding_window: int = 512,
query_pre_attn_scalar: int = 256,
eoi_token_index: int = 256000,
vocab_size: int = 262158,
layer_types: list[str] | None = None,
rope_parameters: dict | None = None,
attention_bias: bool = False,
attention_dropout: float = 0.0,
dropout_rate: float = 0.0,
initializer_range: float = 0.02,
tie_word_embeddings: bool = True,
use_bidirectional_attention: bool = False,
use_cache: bool = True,
requires_grad: bool = False,
preferred_attn_implementation: str = "flash_attention_2",
dtype: str = "bfloat16",
is_encoder_decoder: bool = False,
bos_token_id: int = 2,
eos_token_id: int = 1,
pad_token_id: int = 0,
**kwargs: Any,
) -> None:
kwargs.pop("model_type", None)
super().__init__(
bos_token_id=bos_token_id,
eos_token_id=eos_token_id,
pad_token_id=pad_token_id,
is_encoder_decoder=is_encoder_decoder,
tie_word_embeddings=tie_word_embeddings,
**kwargs,
)
self.hidden_size = hidden_size
self.intermediate_size = intermediate_size
self.num_hidden_layers = num_hidden_layers
self.num_attention_heads = num_attention_heads
self.num_key_value_heads = num_key_value_heads
self.head_dim = head_dim
self.max_position_embeddings = max_position_embeddings
self.hidden_activation = hidden_activation
self.hidden_act = hidden_activation
self.rms_norm_eps = rms_norm_eps
self.rope_theta = rope_theta
self.sliding_window = sliding_window
self.query_pre_attn_scalar = query_pre_attn_scalar
self.eoi_token_index = eoi_token_index
self.vocab_size = vocab_size
if layer_types is None:
# T5Gemma2 uses sliding attention except every sixth layer.
layer_types = [
"full_attention" if (index + 1) % 6 == 0 else "sliding_attention" for index in range(num_hidden_layers)
]
self.layer_types = list(layer_types)
if rope_parameters is None:
rope_parameters = {
"full_attention": {
"factor": 8.0,
"rope_theta": 1_000_000.0,
"rope_type": "linear",
},
"sliding_attention": {
"rope_theta": 10_000.0,
"rope_type": "default",
},
}
self.rope_parameters = dict(rope_parameters)
self.attention_bias = attention_bias
self.attention_dropout = attention_dropout
self.dropout_rate = dropout_rate
self.initializer_range = initializer_range
self.use_bidirectional_attention = use_bidirectional_attention
self.use_cache = use_cache
self.requires_grad = requires_grad
self.preferred_attn_implementation = preferred_attn_implementation
self.dtype = dtype
class BreezeTTS2DepthDecoderConfig(PretrainedConfig):
"""Configuration for Breeze's per-frame depth decoder."""
model_type = "breeze_depth_decoder_model"
def __init__(
self,
hidden_size: int = 1024,
backbone_hidden_size: int = 2048,
audio_embed_size: int = 2048,
intermediate_size: int = 8192,
num_hidden_layers: int = 12,
num_attention_heads: int = 8,
num_key_value_heads: int = 2,
head_dim: int = 128,
max_position_embeddings: int = 33,
hidden_act: str = "silu",
rms_norm_eps: float = 1e-5,
rope_theta: float = 500_000.0,
rope_scaling: dict | None = None,
attention_bias: bool = False,
attention_dropout: float = 0.0,
mlp_bias: bool = False,
initializer_range: float = 0.02,
use_cache: bool = True,
num_codebooks: int = 16,
vocab_size: int = 2051,
codebook_loss_weights: list[int] | None = None,
dtype: str = "bfloat16",
tie_word_embeddings: bool = False,
pad_token_id: int | None = 0,
**kwargs: Any,
) -> None:
kwargs.pop("model_type", None)
# Transformers 5 serializes the normalized field as
# ``rope_parameters`` while the upstream Breeze checkpoint calls it
# ``rope_scaling``. Normalize before ``PretrainedConfig`` validates
# so ``to_dict() -> BreezeTTS2DepthDecoderConfig(**dict)`` round-trips.
serialized_rope_parameters = kwargs.pop("rope_parameters", None)
if rope_scaling is None:
rope_scaling = {
"factor": 32.0,
"high_freq_factor": 0.0078125,
"low_freq_factor": 0.001953125,
"original_max_position_embeddings": 16,
"rope_type": "llama3",
}
if codebook_loss_weights is None:
codebook_loss_weights = [3]
if num_codebooks <= 0 or vocab_size <= 0:
raise ValueError("num_codebooks and vocab_size must be positive")
rope_parameters = dict(serialized_rope_parameters or rope_scaling)
rope_parameters.setdefault("rope_type", rope_parameters.get("type", "default"))
rope_parameters.setdefault("rope_theta", rope_theta)
super().__init__(
vocab_size=vocab_size,
tie_word_embeddings=tie_word_embeddings,
pad_token_id=pad_token_id,
**kwargs,
)
# The vLLM depth decoder is a plain ``nn.Module``, so it does not get
# Transformers' PreTrainedModel default attention initialization.
self._attn_implementation = getattr(self, "_attn_implementation", None) or "eager"
self.hidden_size = hidden_size
self.backbone_hidden_size = backbone_hidden_size
self.audio_embed_size = audio_embed_size
self.intermediate_size = intermediate_size
self.num_hidden_layers = num_hidden_layers
self.num_attention_heads = num_attention_heads
self.num_key_value_heads = num_key_value_heads
self.head_dim = head_dim
self.max_position_embeddings = max_position_embeddings
self.hidden_act = hidden_act
self.rms_norm_eps = rms_norm_eps
self.rope_theta = rope_theta
self.rope_parameters = dict(getattr(self, "rope_parameters", None) or rope_parameters)
self.rope_parameters.setdefault("rope_theta", rope_theta)
# Transformers 4 exposes this as a plain attribute; Transformers 5
# implements ``rope_scaling`` as a property backed by rope_parameters.
if not hasattr(type(self), "rope_scaling"):
self.rope_scaling = self.rope_parameters
self.rope_type = rope_scaling.get("rope_type", "default")
self.attention_bias = attention_bias
self.attention_dropout = attention_dropout
self.mlp_bias = mlp_bias
self.initializer_range = initializer_range
self.use_cache = use_cache
self.num_codebooks = num_codebooks
self.vocab_size = vocab_size
self.codebook_loss_weights = list(codebook_loss_weights)
self.dtype = dtype
AutoConfig.register("breeze", BreezeTTS2Config, exist_ok=True)
__all__ = [
"BreezeTTS2Config",
"BreezeTTS2DepthDecoderConfig",
"BreezeTTS2TextEncoderConfig",
]
@@ -0,0 +1,409 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Breeze's within-frame transformer, with frame-local KV state.
Attention follows the reference BF16 matmul / FP32 softmax ordering. Each
frame overwrites its causal cache before reading it. Captured workspaces
belong to their graphs; generated frames and RNG state belong to requests.
"""
import torch
import torch.nn.functional as F
from torch import nn
from transformers import PretrainedConfig
from vllm.platforms import current_platform
from vllm_omni.platforms import current_omni_platform
def sample_logits(
logits: torch.Tensor,
temperature: float,
top_k: int,
top_p: float,
generator: torch.Generator,
noise: torch.Tensor | None = None,
) -> torch.Tensor:
"""Sample using scalar parameters, optionally with pre-drawn noise.
Keep the filtering contract aligned with ``sample_graph_logits``: scale
in FP32, retain top-k ties, and remove nucleus candidates only when the
cumulative mass before them is >= top_p. Temperature zero selects argmax
without drawing RNG. The parity tests cover ties and exact boundaries.
"""
if temperature == 0:
return logits.argmax(-1)
scores = logits.float() / temperature
if top_k > 0:
threshold = scores.topk(min(top_k, scores.shape[-1]), dim=-1).values[..., -1:]
scores = scores.masked_fill(scores < threshold, -torch.inf)
if top_p < 1:
values, indices = scores.sort(dim=-1, descending=True)
probabilities = values.softmax(-1)
cumulative = probabilities.cumsum(-1)
# Keep the first candidate crossing the nucleus boundary as in HF.
remove = cumulative - probabilities >= top_p
scores = scores.scatter(-1, indices, values.masked_fill(remove, -torch.inf))
probabilities = scores.softmax(-1)
if noise is not None:
return (probabilities / noise).argmax(-1)
return torch.multinomial(probabilities, 1, generator=generator).squeeze(-1)
def sample_graph_logits(logits: torch.Tensor, parameters: torch.Tensor, noise: torch.Tensor) -> torch.Tensor:
"""Sample with mutable GPU parameters and a request-owned RNG draw.
Sorting once supplies both the top-k threshold and the nucleus order.
Filtering keeps ties at the top-k threshold, and keeps the first token
crossing the nucleus boundary. The three reserved codec IDs remain in
the RNG workspace, with zero probability, as in the reference sampler.
Replay can change parameters, so selection uses tensor operations rather
than ``sample_logits``' Python branches on scalar parameters. Keep their
FP32 scaling and top-k/top-p boundaries aligned without sharing that
control flow. This path consumes supplied noise; its caller owns RNG
advancement and must avoid advancing request RNG for greedy decoding.
"""
temperature, top_k, top_p = parameters.unbind()
scaled = logits.float() / torch.where(temperature > 0, temperature, 1.0)
values, indices = scaled.sort(dim=-1, descending=True)
cutoff = torch.where(top_k > 0, top_k, logits.shape[-1]).long().clamp(1, logits.shape[-1]) - 1
threshold = values.gather(-1, cutoff.expand(values.shape[0], 1))
values = values.masked_fill(values < threshold, -torch.inf)
probabilities = values.softmax(-1)
cumulative = probabilities.cumsum(-1)
# Use the same boundary and arithmetic as the eager sampler, including
# an exactly representable cumulative probability equal to top_p.
remove = (cumulative - probabilities >= top_p) & (top_p < 1)
values = values.masked_fill(remove, -torch.inf)
# Argmax of p / Exp(1) is the single-sample multinomial algorithm.
selected = (values.softmax(-1) / noise.gather(-1, indices)).argmax(-1, keepdim=True)
sampled = indices.gather(-1, selected).squeeze(-1)
return torch.where(temperature > 0, sampled, logits.argmax(-1))
class BreezeRMSNorm(nn.Module):
def __init__(self, size: int, eps: float) -> None:
super().__init__()
self.weight = nn.Parameter(torch.ones(size))
self.eps = eps
def forward(self, x: torch.Tensor) -> torch.Tensor:
y = x.float()
y = y * torch.rsqrt(y.square().mean(-1, keepdim=True) + self.eps)
return self.weight * y.to(x.dtype)
class BreezeDepthLayer(nn.Module):
def __init__(self, config: PretrainedConfig) -> None:
super().__init__()
self.heads = config.num_attention_heads
self.kv_heads = config.num_key_value_heads
self.dim = config.head_dim
self.q_size = self.heads * self.dim
self.kv_size = self.kv_heads * self.dim
self.qkv = nn.Linear(config.hidden_size, self.q_size + 2 * self.kv_size, bias=False)
self.o_proj = nn.Linear(self.q_size, config.hidden_size, bias=False)
self.gate_up = nn.Linear(config.hidden_size, 2 * config.intermediate_size, bias=False)
self.down_proj = nn.Linear(config.intermediate_size, config.hidden_size, bias=False)
self.input_layernorm = BreezeRMSNorm(config.hidden_size, config.rms_norm_eps)
self.post_attention_layernorm = BreezeRMSNorm(config.hidden_size, config.rms_norm_eps)
def forward(
self,
x: torch.Tensor,
positions: torch.Tensor,
cache: tuple[torch.Tensor, torch.Tensor],
mask: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
) -> torch.Tensor:
batch, length, _ = x.shape
residual = x
q, k, v = self.qkv(self.input_layernorm(x)).split([self.q_size, self.kv_size, self.kv_size], -1)
q = q.view(batch, length, self.heads, self.dim).transpose(1, 2)
k = k.view(batch, length, self.kv_heads, self.dim).transpose(1, 2)
v = v.view(batch, length, self.kv_heads, self.dim).transpose(1, 2)
q1, q2 = q.chunk(2, -1)
k1, k2 = k.chunk(2, -1)
q = q * cos + torch.cat((-q2, q1), -1) * sin
k = k * cos + torch.cat((-k2, k1), -1) * sin
cache[0].index_copy_(2, positions, k)
cache[1].index_copy_(2, positions, v)
# Fold query heads sharing one KV head into its query dimension.
# This avoids materializing repeated keys/values for grouped-query
# attention while preserving BF16 matmul and FP32 softmax ordering.
grouped_q = q.reshape(batch, self.kv_heads, self.heads // self.kv_heads * length, self.dim)
scores = (
torch.matmul(grouped_q, cache[0].transpose(-1, -2)).reshape(batch, self.heads, length, cache[0].shape[2])
* self.dim**-0.5
)
scores = scores.masked_fill(~mask, torch.finfo(q.dtype).min)
probabilities = scores.softmax(-1, dtype=torch.float32).to(q.dtype)
grouped_probabilities = probabilities.reshape(batch, self.kv_heads, -1, cache[0].shape[2])
y = torch.matmul(grouped_probabilities, cache[1]).reshape(batch, self.heads, length, self.dim)
y = y.transpose(1, 2).reshape(batch, length, self.q_size)
x = residual + self.o_proj(y)
gate, up = self.gate_up(self.post_attention_layernorm(x)).chunk(2, -1)
return x + self.down_proj(F.silu(gate) * up)
class BreezeDepthDecoder(nn.Module):
def __init__(self, config: PretrainedConfig) -> None:
super().__init__()
self.config = config
self.num_codebooks = config.num_codebooks
self.vocab_size = config.vocab_size
self.embed_tokens = nn.Embedding(self.num_codebooks * self.vocab_size, config.audio_embed_size)
self.inputs_embeds_projector = nn.Linear(config.audio_embed_size, config.hidden_size, bias=False)
self.layers = nn.ModuleList([BreezeDepthLayer(config) for _ in range(config.num_hidden_layers)])
self.norm = BreezeRMSNorm(config.hidden_size, config.rms_norm_eps)
self.codebooks_head = nn.Parameter(
torch.empty(self.num_codebooks - 1, config.hidden_size, self.vocab_size, dtype=torch.float32)
)
rope = config.rope_parameters
inv = 1.0 / (rope["rope_theta"] ** (torch.arange(0, config.head_dim, 2).float() / config.head_dim))
wavelength = 2 * torch.pi / inv
low = rope["original_max_position_embeddings"] / rope["low_freq_factor"]
high = rope["original_max_position_embeddings"] / rope["high_freq_factor"]
smooth = (rope["original_max_position_embeddings"] / wavelength - rope["low_freq_factor"]) / (
rope["high_freq_factor"] - rope["low_freq_factor"]
)
scaled = (1 - smooth) * inv / rope["factor"] + smooth * inv
inv = torch.where(wavelength > low, inv / rope["factor"], torch.where(wavelength < high, inv, scaled))
angles = torch.outer(torch.arange(self.num_codebooks).float(), inv)
angles = torch.cat((angles, angles), -1)
self.register_buffer("rope_cos", angles.cos(), persistent=False)
self.register_buffer("rope_sin", angles.sin(), persistent=False)
self.register_buffer("positions", torch.arange(self.num_codebooks), persistent=False)
self.register_buffer(
"causal_mask",
torch.arange(self.num_codebooks)[None, :] <= torch.arange(self.num_codebooks)[:, None],
persistent=False,
)
self._graphs: dict[tuple[int, int, bool], BreezeDepthGraph] = {}
self._compiled_layer = None
self._compiled_sampler = None
def _allocate_cache(self, hidden: torch.Tensor) -> list[tuple[torch.Tensor, torch.Tensor]]:
shape = (hidden.shape[0], self.config.num_key_value_heads, self.num_codebooks, self.config.head_dim)
# Masked future positions still participate in the value matmul.
# Initialize them to finite values so zero probabilities cannot meet NaN.
return [(hidden.new_zeros(shape), hidden.new_zeros(shape)) for _ in self.layers]
def generate_frame(
self,
hidden: torch.Tensor,
first: torch.Tensor,
*,
temperature: float,
top_k: int,
top_p: float,
generator: torch.Generator,
) -> torch.Tensor:
return self._generate_frame(hidden, first, self._allocate_cache(hidden), temperature, top_k, top_p, generator)
def _generate_frame(
self,
hidden: torch.Tensor,
first: torch.Tensor,
caches: list[tuple[torch.Tensor, torch.Tensor]],
temperature: float,
top_k: int,
top_p: float,
generator: torch.Generator,
noise: torch.Tensor | None = None,
guidance_scale: float | torch.Tensor | None = None,
sampling_parameters: torch.Tensor | None = None,
greedy: bool = False,
) -> torch.Tensor:
batch = hidden.shape[0]
branches = 2 if guidance_scale is not None else 1
branch_first = first.repeat(branches)
x = torch.cat((hidden.unsqueeze(1), self.embed_tokens(branch_first.reshape(batch, 1))), 1)
frames = [first]
start = 0
for codebook in range(self.num_codebooks - 1):
x = self.inputs_embeds_projector(x)
end = start + x.shape[1]
cos = self.rope_cos[start:end].to(x.dtype)[None, None]
sin = self.rope_sin[start:end].to(x.dtype)[None, None]
for layer, cache in zip(self.layers, caches, strict=True):
args = (x, self.positions[start:end], cache, self.causal_mask[start:end][None, None], cos, sin)
if noise is None and not greedy:
x = layer(*args)
else:
x = self._compiled_layer(layer, *args)
logits = F.linear(self.norm(x[:, -1]).float(), self.codebooks_head[codebook].T.float())
if guidance_scale is not None:
cond, uncond = logits.chunk(2, dim=0)
logits = uncond + guidance_scale * (cond - uncond)
logits[:, self.vocab_size - 3 :] = -torch.inf
if greedy:
token = logits.argmax(-1)
elif noise is None:
token = sample_logits(logits, temperature, top_k, top_p, generator)
else:
token = self._compiled_sampler(logits, sampling_parameters, noise[:, codebook])
frames.append(token)
x = self.embed_tokens((token.repeat(branches) + (codebook + 1) * self.vocab_size).reshape(batch, 1))
start = end
return torch.stack(frames, -1)
def generate_frames(
self,
hidden: torch.Tensor,
first: torch.Tensor,
*,
temperature: float,
top_k: int,
top_p: float,
generators: list[torch.Generator],
guidance_scale: float = 1.0,
) -> torch.Tensor:
batch = hidden.shape[0]
branches = 2 if guidance_scale != 1.0 else 1
requests = first.shape[0]
if batch != requests * branches or len(generators) != requests:
raise ValueError("Depth generation requires one token and generator per request and aligned CFG branches")
if requests == 0:
return first.new_empty((0, self.num_codebooks))
# Buckets bound captured workspace shapes as live requests arrive and
# finish. Compiled layers share code across those batch dimensions.
request_bucket = 1 << (requests - 1).bit_length()
batch_bucket = request_bucket * branches
# Greedy decoding needs no sorting, probability filtering, or RNG
# workspace. Capture it separately from the mutable sampled path.
key = (batch_bucket, branches, temperature == 0)
parameters = (temperature, top_k, top_p)
entry = self._graphs.get(key)
if entry is None:
if self._compiled_layer is None:
self._compiled_layer = torch.compile(
BreezeDepthLayer.forward,
fullgraph=True,
dynamic=True,
options={"epilogue_fusion": False, "max_autotune": True},
)
self._compiled_sampler = torch.compile(sample_graph_logits, fullgraph=True, dynamic=False)
entry = BreezeDepthGraph(
self,
hidden.new_empty((batch_bucket, hidden.shape[-1])),
first.new_empty((request_bucket,)),
parameters,
guidance_scale,
)
self._graphs[key] = entry
if entry.parameter_values != parameters:
entry.parameters.copy_(entry.parameters.new_tensor(parameters))
entry.parameter_values = parameters
if batch == batch_bucket:
entry.hidden.copy_(hidden)
else:
# CFG graphs keep all conditioned rows before all unconditional
# rows. Pad each branch separately so matching rows stay paired.
for branch in range(branches):
entry.hidden[branch * request_bucket : branch * request_bucket + requests].copy_(
hidden[branch * requests : (branch + 1) * requests]
)
entry.first[:requests].copy_(first)
if entry.guidance_scale is not None and entry.guidance_value != guidance_scale:
entry.guidance_scale.fill_(guidance_scale)
entry.guidance_value = guidance_scale
if entry.noise_generators is not None:
# Graphs retain placeholder generator states. Copy each live
# request's seed and post-codebook-0 offset into its current row;
# replacing a generator state would invalidate the captured RNG.
for placeholder, generator in zip(entry.noise_generators, generators):
placeholder.manual_seed(generator.initial_seed())
placeholder.set_offset(generator.get_offset())
elif temperature > 0:
assert entry.noise is not None
for row, generator in enumerate(generators):
# Match the 15 independent exponential draws used by
# torch.multinomial(..., num_samples=1), preserving each
# request's RNG stream even when requests are batched.
for codebook in range(self.num_codebooks - 1):
entry.noise[row, codebook].exponential_(generator=generator)
entry.graph.replay()
if entry.noise_generators is not None:
# Replay advances host offsets without synchronizing the device.
# Padded rows have no request state to update.
for placeholder, generator in zip(entry.noise_generators, generators):
generator.set_offset(placeholder.get_offset())
# The tiny RVQ result outlives this replay in request state and the
# transfer queue. The graph owns and overwrites its output workspace.
return entry.output[:requests].clone()
class BreezeDepthGraph:
def __init__(
self,
model: BreezeDepthDecoder,
hidden: torch.Tensor,
first: torch.Tensor,
parameters: tuple[float, int, float],
guidance_scale: float = 1.0,
) -> None:
self.parameter_values = parameters
self.greedy = parameters[0] == 0
self.parameters = hidden.new_tensor(parameters, dtype=torch.float32)
self.guidance_value = guidance_scale
self.guidance_scale = hidden.new_tensor(guidance_scale, dtype=torch.float32) if guidance_scale != 1.0 else None
self.hidden = torch.zeros_like(hidden)
self.first = torch.zeros_like(first)
self.noise = (
None
if self.greedy
else hidden.new_ones((first.shape[0], model.num_codebooks - 1, model.vocab_size), dtype=torch.float32)
)
self.noise_generators: list[torch.Generator] | None = None
if not self.greedy and current_platform.is_cuda() and hasattr(torch.cuda.CUDAGraph, "register_generator_state"):
self.noise_generators = [torch.Generator(device=hidden.device) for _ in range(first.shape[0])]
# Materialize the distribution kernel before graph capture.
self._fill_noise()
# Captured kernels retain addresses, not the Python tensors allocated
# before capture. Keep every KV allocation alive with its graph.
self.caches = model._allocate_cache(hidden)
generator = torch.Generator(device=hidden.device)
for _ in range(3):
model._generate_frame(
self.hidden,
self.first,
self.caches,
*parameters,
generator,
self.noise,
self.guidance_scale,
self.parameters,
greedy=self.greedy,
)
current_omni_platform.synchronize()
self.graph = torch.cuda.CUDAGraph()
if self.noise_generators is not None:
for generator in self.noise_generators:
self.graph.register_generator_state(generator)
with torch.cuda.graph(
self.graph, pool=current_platform.get_global_graph_pool(), capture_error_mode="thread_local"
):
if self.noise_generators is not None:
self._fill_noise()
self.output = model._generate_frame(
self.hidden,
self.first,
self.caches,
*parameters,
generator,
self.noise,
self.guidance_scale,
self.parameters,
greedy=self.greedy,
)
def _fill_noise(self) -> None:
assert self.noise is not None and self.noise_generators is not None
for row, generator in enumerate(self.noise_generators):
# Preserve the shape and ordering of independent multinomial
# draws; one larger draw would consume a different RNG stream.
for codebook in range(self.noise.shape[1]):
self.noise[row, codebook].exponential_(generator=generator)
@@ -0,0 +1,102 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Capture first-code sampling while preserving each request's RNG stream."""
import torch
from vllm.platforms import current_platform
from vllm_omni.model_executor.models.breeze_tts_2.depth_decoder import sample_logits
from vllm_omni.platforms import current_omni_platform
class BreezeFirstCodeSampler:
def __init__(self) -> None:
self._graphs: dict[tuple[torch.device, int, int, tuple[float, int, float]], _SamplerGraph] = {}
def sample(
self,
logits: list[torch.Tensor],
parameters: tuple[float, int, float],
generators: list[torch.Generator],
) -> list[torch.Tensor]:
if len(logits) != len(generators):
raise ValueError("First-code sampling requires one generator per logit row")
if not logits:
return []
first = logits[0]
for row, generator in zip(logits, generators, strict=True):
if (
row.ndim != 2
or row.shape[0] != 1
or row.shape[1] == 0
or row.shape != first.shape
or row.dtype != torch.float32
or row.device != first.device
or generator.device.type != row.device.type
or (generator.device.index is not None and generator.device.index != row.device.index)
):
raise ValueError("First-code sampling requires aligned FP32 [1, vocab] rows and generators")
capture = (
parameters[0] > 0
and first.is_cuda
and current_platform.is_cuda()
and hasattr(torch.cuda.CUDAGraph, "register_generator_state")
and len({id(generator) for generator in generators}) == len(generators)
)
if capture:
bucket = 1 << (len(logits) - 1).bit_length()
key = (first.device, first.shape[1], bucket, parameters)
entry = self._graphs.get(key)
# Sampling parameters are static graph branches. Bound their cache
# without evicting buffers retained by existing captured graphs.
with torch.accelerator.device_index(first.device.index):
if entry is None and len(self._graphs) < 32:
entry = _SamplerGraph(first, bucket, parameters)
self._graphs[key] = entry
if entry is not None:
return entry.replay(logits, generators)
# Shared generators must retain sequential draw ordering, including
# when a matching graph is already cached for independent requests.
return [sample_logits(row, *parameters, generator) for row, generator in zip(logits, generators, strict=True)]
class _SamplerGraph:
def __init__(self, row: torch.Tensor, bucket: int, parameters: tuple[float, int, float]) -> None:
self.logits = row.new_zeros((bucket, row.shape[1]))
self.generators = [torch.Generator(device=row.device) for _ in range(bucket)]
def sample_rows() -> torch.Tensor:
# Keep the original one-row softmax, filtering, multinomial
# validation and RNG draw geometry; batched reductions can change
# seeded output trajectories even when probabilities are close.
return torch.cat(
[
sample_logits(self.logits[index : index + 1], *parameters, generator)
for index, generator in enumerate(self.generators)
]
)
for _ in range(3):
sample_rows()
current_omni_platform.synchronize()
self.graph = torch.cuda.CUDAGraph()
self.capture_stream = torch.cuda.Stream(device=row.device)
for generator in self.generators:
self.graph.register_generator_state(generator)
with torch.cuda.graph(self.graph, stream=self.capture_stream):
self.output = sample_rows()
def replay(self, logits: list[torch.Tensor], generators: list[torch.Generator]) -> list[torch.Tensor]:
live = len(logits)
self.logits[:live].copy_(torch.cat(logits))
# Captures retain the placeholder state objects. Update their values
# rather than replacing those objects with request-owned RNG states.
for placeholder, request in zip(self.generators[:live], generators, strict=True):
placeholder.manual_seed(request.initial_seed())
placeholder.set_offset(request.get_offset())
self.graph.replay()
for placeholder, request in zip(self.generators[:live], generators, strict=True):
request.set_offset(placeholder.get_offset())
# Padded rows have valid logits but no request state to advance. Clone
# the live results so later replays cannot overwrite a returned token.
return list(self.output[:live].clone().split(1))
@@ -0,0 +1,407 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Breeze-TTS-2: T5Gemma2 prompt encoding, paged Qwen3, and RVQ decoding."""
from collections.abc import Iterable
from typing import Any, TypedDict, cast
import torch
import torch.nn.functional as F
from torch import nn
from transformers.models.t5gemma2.modeling_t5gemma2 import T5Gemma2TextEncoder
from vllm.config import VllmConfig
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
from vllm.model_executor.models.qwen3 import Qwen3Model
from vllm.sequence import IntermediateTensors
from vllm.v1.sample.metadata import SamplingMetadata
from vllm.v1.utils import record_function_or_nullcontext
from vllm_omni.config.model import OmniModelConfig
from vllm_omni.model_executor.models.breeze_tts_2.configuration_breeze import BreezeConfig
from vllm_omni.model_executor.models.breeze_tts_2.depth_decoder import BreezeDepthDecoder
from vllm_omni.model_executor.models.breeze_tts_2.first_code_sampler import BreezeFirstCodeSampler
from vllm_omni.model_executor.models.breeze_tts_2.prompt import CFG_UNCOND_SUFFIX
from vllm_omni.model_executor.models.breeze_tts_2.reference_encoder import BreezeReferenceEncoder
from vllm_omni.model_executor.models.breeze_tts_2.text_encoder_graph import (
BreezeTextEncoderCompiled,
BreezeTextEncoderGraph,
)
from vllm_omni.model_executor.models.output_templates import OmniOutput
class BreezeSampling(TypedDict):
temperature: float
top_k: int
top_p: float
repetition_penalty: float
class BreezeState(TypedDict):
generator: torch.Generator
history: torch.Tensor
current: torch.Tensor
class BreezeForConditionalGeneration(nn.Module):
have_multimodal_outputs = True
has_preprocess = True
has_postprocess = False
requires_full_prefix_cached_hidden_states = False
omni_pooler_payload_include_hidden = False
use_async_omni_output = True
def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
super().__init__()
self.vllm_config = vllm_config
self.config = cast(BreezeConfig, vllm_config.model_config.hf_config)
parallel = vllm_config.parallel_config
if parallel.tensor_parallel_size != 1 or parallel.pipeline_parallel_size != 1:
raise ValueError("Breeze-TTS-2 currently supports TP=1 and PP=1")
if vllm_config.cache_config.enable_prefix_caching:
raise ValueError("Breeze prompt placeholders require enable_prefix_caching=False")
if vllm_config.scheduler_config.enable_chunked_prefill:
raise ValueError("Breeze text encoding requires enable_chunked_prefill=False")
if not cast(OmniModelConfig, vllm_config.model_config).async_chunk:
raise ValueError("Breeze requires async_chunk=True for its stateful codec stage")
if vllm_config.model_config.quantization is not None:
raise ValueError("The initial Breeze integration does not support quantization")
if self.config.backbone_model_type != "qwen3" or self.config.text_encoder_proj_type != "linear":
raise ValueError("This integration supports the Breeze-TTS-2 Qwen3/linear checkpoint")
if self.config.num_codebooks != 16 or self.config.audio_vocab_size != 2051:
raise ValueError("Breeze-TTS-2 requires 16 codebooks with 2048 audio codes and 3 reserved IDs")
self.num_codebooks = self.config.num_codebooks
self.codebook_size = self.config.audio_vocab_size - 3
self.hidden_size = self.config.backbone_config.hidden_size
backbone_config = vllm_config.with_hf_config(self.config.backbone_config, architectures=["Qwen3ForCausalLM"])
backbone_config.model_config.hf_text_config = backbone_config.model_config.hf_config
self.model = Qwen3Model(vllm_config=backbone_config, prefix=f"{prefix}.model".strip("."))
# Breeze replaces the backbone token embedding with summed RVQ embeddings.
self.model.embed_tokens = nn.Identity()
text_config = self.config.text_encoder_config
text_config._attn_implementation = "sdpa"
self.text_encoder = T5Gemma2TextEncoder(text_config)
self.text_encoder_proj = nn.Linear(text_config.hidden_size, self.hidden_size, bias=False)
self._long_text_encoder = BreezeTextEncoderCompiled(self.text_encoder, self.text_encoder_proj)
self.reference_encoder = BreezeReferenceEncoder(vllm_config)
self.depth_decoder = BreezeDepthDecoder(self.config.depth_decoder_config)
self._first_code_sampler = BreezeFirstCodeSampler()
self.lm_head = nn.Linear(self.hidden_size, self.config.vocab_size, bias=False, dtype=torch.float32)
self.register_buffer(
"offsets", torch.arange(self.num_codebooks) * self.config.audio_vocab_size, persistent=False
)
self.gpu_resident_buffer_keys = {
("breeze_state", "current"),
("breeze_state", "history"),
("breeze_prepared", "embeds"),
}
self._next_tokens: torch.Tensor | None = None
self._text_graphs: dict[tuple[int, int], BreezeTextEncoderGraph] = {}
@torch.inference_mode()
def preprocess_batch(
self, *, req_ids: list[str], model_intermediate_buffer: dict[str, dict[str, Any]], device: torch.device
) -> None:
pending = [
model_intermediate_buffer[rid] for rid in req_ids if "breeze_state" not in model_intermediate_buffer[rid]
]
if not pending:
return
segment_ids: dict[tuple[int, ...], None] = {}
references: dict[str, dict[str, Any]] = {}
reference_keys: dict[str, str] = {}
for info in pending:
conditioning = info["breeze_prompt"]
segment_ids[tuple(conditioning["target_ids"])] = None
if "reference_waveform" in info:
segment_ids[tuple(conditioning["reference_ids"])] = None
request_id = info["global_request_id"][0]
key = request_id.removesuffix(CFG_UNCOND_SUFFIX) if conditioning["role"] == "uncond" else request_id
reference_keys[request_id] = key
references.setdefault(key, info)
if not self._text_graphs:
# The serving warmup pays these captures before accepting speech
# requests. Cover the default two-row deployment's distinct text
# segments, including two independent reference recordings.
for batch in range(1, min(4, 2 * self.vllm_config.scheduler_config.max_num_seqs) + 1):
for size in (32, 64, 128):
self._text_graphs[(batch, size)] = BreezeTextEncoderGraph(
self.text_encoder, self.text_encoder_proj, size, batch_size=batch
)
self.reference_encoder.warmup(min(2, self.vllm_config.scheduler_config.max_num_seqs))
self._long_text_encoder.warmup(min(4, 2 * self.vllm_config.scheduler_config.max_num_seqs))
length = max(len(ids) for ids in segment_ids)
bucket = max(32, 1 << (length - 1).bit_length())
with record_function_or_nullcontext("breeze:text_encoder"):
prompts = [torch.tensor(ids, device="cpu", dtype=torch.long) for ids in segment_ids]
if bucket > 128:
encoded = self._long_text_encoder.run_batch(prompts)
# Large prefill activations must not raise the decoder graph
# pool's permanent high-water mark. Return free prefill
# workspace to the device before starting streamed decode.
torch.accelerator.empty_cache()
else:
graph_key = (len(segment_ids), bucket)
if graph_key not in self._text_graphs:
self._text_graphs[graph_key] = BreezeTextEncoderGraph(
self.text_encoder, self.text_encoder_proj, bucket, batch_size=len(segment_ids)
)
encoded = self._text_graphs[graph_key].run_batch(prompts)
segments = dict(zip(segment_ids, encoded, strict=True))
reference_embeds: dict[str, torch.Tensor] = {}
if references:
with record_function_or_nullcontext("breeze:reference_encoder"):
codes = self.reference_encoder.encode_batch(
[info["reference_waveform"] for info in references.values()],
[info["breeze_prompt"]["reference_frames"] for info in references.values()],
)
for key, code in zip(references, codes, strict=True):
# Audio EOS is a zero RVQ frame, distinct from generation EOS.
eos = code.new_full((1, self.num_codebooks), self.config.codebook_eos_token_id)
audio = self.depth_decoder.embed_tokens(torch.cat((code, eos)) + self.offsets).sum(1)
reference_ids = tuple(references[key]["breeze_prompt"]["reference_ids"])
reference_embeds[key] = torch.cat((segments[reference_ids], audio), dim=0)
for info in pending:
embeds = segments[tuple(info["breeze_prompt"]["target_ids"])]
request_id = info["global_request_id"][0]
if request_id in reference_keys:
embeds = torch.cat((reference_embeds[reference_keys[request_id]], embeds), dim=0)
info["breeze_prepared"] = {"embeds": embeds}
def embed_input_ids(self, input_ids: torch.Tensor, **_: object) -> torch.Tensor:
# vLLM's memory profiling does not have a request payload.
return self.lm_head.weight.new_zeros((input_ids.numel(), self.hidden_size), dtype=self.model.norm.weight.dtype)
def preprocess(
self,
input_ids: torch.Tensor,
input_embeds: torch.Tensor | None,
*,
breeze_prompt: dict[str, Any],
breeze_sampling: BreezeSampling,
global_request_id: list[str],
breeze_prepared: dict[str, torch.Tensor] | None = None,
breeze_state: BreezeState | None = None,
_omni_is_prefill: bool,
_omni_seed: int | None = None,
**_: object,
) -> tuple[torch.Tensor, torch.Tensor, dict[str, object]]:
if _omni_is_prefill:
if breeze_prepared is None:
raise RuntimeError("Breeze prefill requires the runner's batch preprocessing hook")
embeds = breeze_prepared.pop("embeds")
if embeds.shape[0] != input_ids.numel():
raise ValueError("Breeze prompt payload and scheduled token count differ")
generator = torch.Generator(device=input_ids.device)
if _omni_seed is None:
generator.seed()
else:
generator.manual_seed(_omni_seed)
state: BreezeState = {
"generator": generator,
"history": input_ids.new_empty((1, 0)),
"current": input_ids.new_empty((1, self.num_codebooks)),
}
return input_ids, embeds, {"breeze_state": state}
if breeze_state is None or input_ids.numel() != 1:
raise ValueError("Breeze decode requires initialized per-request state and one token")
embeds = self.depth_decoder.embed_tokens(breeze_state["current"] + self.offsets).sum(1)
return input_ids, embeds, {}
def preprocess_decode_batch(
self,
*,
input_ids: torch.Tensor,
req_infos: list[dict[str, Any]],
) -> tuple[torch.Tensor, torch.Tensor, list[dict[str, Any]]]:
"""Embed one current RVQ frame per physical decode row, including CFG."""
if input_ids.numel() != len(req_infos):
raise ValueError("Breeze decode requires one token per request")
frames: list[torch.Tensor] = []
for info in req_infos:
state = info.get("breeze_state")
if info.get("_omni_is_prefill") or state is None:
raise ValueError("Breeze decode requires initialized per-request state")
current = state["current"]
if current.shape != (1, self.num_codebooks):
raise ValueError("Breeze decode requires one RVQ frame per request")
frames.append(current)
if not frames:
embeds = self.depth_decoder.embed_tokens.weight.new_empty((0, self.hidden_size))
else:
current = torch.cat(frames, dim=0)
embeds = self.depth_decoder.embed_tokens(current + self.offsets).sum(1)
return input_ids, embeds, [{} for _ in req_infos]
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
intermediate_tensors: IntermediateTensors | None = None,
inputs_embeds: torch.Tensor | None = None,
**_: object,
) -> torch.Tensor | IntermediateTensors:
return self.model(None, positions, intermediate_tensors, inputs_embeds)
def compute_logits(
self, hidden_states: torch.Tensor, sampling_metadata: SamplingMetadata | None = None
) -> torch.Tensor:
logits = hidden_states.new_full((hidden_states.shape[0], self.config.vocab_size), -torch.inf)
if self._next_tokens is None:
logits[:, 0] = 0
else:
if self._next_tokens.numel() != hidden_states.shape[0]:
raise RuntimeError("Breeze sampled rows do not match the current request batch")
logits.scatter_(1, self._next_tokens[:, None], 0)
return logits
def make_omni_output(
self,
model_outputs: torch.Tensor,
*,
model_intermediate_buffer: list[dict[str, object]] | None = None,
request_token_spans: list[tuple[int, int]] | None = None,
**_: object,
) -> OmniOutput:
if not model_intermediate_buffer:
self._next_tokens = None
return OmniOutput(text_hidden_states=model_outputs, multimodal_outputs={})
if request_token_spans is None or len(request_token_spans) != len(model_intermediate_buffer):
raise RuntimeError("Breeze requires one hidden-state span per request")
self._next_tokens = torch.zeros(len(model_intermediate_buffer), device=model_outputs.device, dtype=torch.long)
output_codes = [
torch.empty((0, self.num_codebooks), device=model_outputs.device, dtype=torch.long)
for _ in model_intermediate_buffer
]
infos = model_intermediate_buffer
request_rows = {cast(list[str], info["global_request_id"])[0]: index for index, info in enumerate(infos)}
prepared_rows: list[tuple[list[int], torch.Tensor, torch.Tensor, tuple[float, int, float, float]]] = []
for index, (info, (start, end)) in enumerate(zip(infos, request_token_spans, strict=True)):
if not 0 <= start < end <= model_outputs.shape[0]:
raise RuntimeError("Invalid Breeze request span")
conditioning = cast(dict[str, Any], info["breeze_prompt"])
if conditioning["role"] == "uncond":
parent = cast(list[str], info["global_request_id"])[0].removesuffix(CFG_UNCOND_SUFFIX)
if parent not in request_rows:
raise RuntimeError("Breeze CFG companion was scheduled without its conditioned branch")
continue
hidden = model_outputs[end - 1 : end]
state = cast(BreezeState, info["breeze_state"])
sampling = cast(BreezeSampling, info["breeze_sampling"])
logits = F.linear(hidden.float(), self.lm_head.weight)
indices = [index]
guidance_scale = float(conditioning["guidance_scale"])
if guidance_scale != 1.0:
request_id = cast(list[str], info["global_request_id"])[0]
companion = request_rows.get(request_id + CFG_UNCOND_SUFFIX)
if companion is None:
raise RuntimeError("Breeze CFG request was scheduled without its unconditional branch")
if infos[companion]["breeze_sampling"] != sampling:
raise RuntimeError("Breeze CFG branches have different sampling parameters")
_, companion_end = request_token_spans[companion]
uncond = model_outputs[companion_end - 1 : companion_end]
uncond_logits = F.linear(uncond.float(), self.lm_head.weight)
logits = uncond_logits + guidance_scale * (logits - uncond_logits)
hidden = torch.cat((hidden, uncond))
indices.append(companion)
logits[:, self.codebook_size : self.config.eos_token_id] = -torch.inf
history = state["history"]
if history.numel():
selected = logits.gather(1, history)
penalty = sampling["repetition_penalty"]
logits.scatter_(1, history, torch.where(selected < 0, selected * penalty, selected / penalty))
parameters = (sampling["temperature"], sampling["top_k"], sampling["top_p"], guidance_scale)
prepared_rows.append((indices, hidden, logits, parameters))
generators = [cast(BreezeState, infos[row[0][0]]["breeze_state"])["generator"] for row in prepared_rows]
sampling_groups: dict[tuple[float, int, float], list[int]] = {}
for position, (_, _, _, (temperature, top_k, top_p, _)) in enumerate(prepared_rows):
sampling_groups.setdefault((temperature, top_k, top_p), []).append(position)
positions_by_group = list(sampling_groups.values())
if len({id(generator) for generator in generators}) != len(generators):
# Normally each request owns a generator. Preserve sequential draw
# ordering even if callers supply shared state across parameter groups.
positions_by_group = [[position] for position in range(len(prepared_rows))]
first_codes: dict[int, torch.Tensor] = {}
for positions in positions_by_group:
temperature, top_k, top_p, _ = prepared_rows[positions[0]][3]
first = self._first_code_sampler.sample(
[prepared_rows[position][2] for position in positions],
(temperature, top_k, top_p),
[generators[position] for position in positions],
)
first_codes.update(zip(positions, first, strict=True))
sampled_rows = [
(indices, hidden, first_codes[position], parameters)
for position, (indices, hidden, _, parameters) in enumerate(prepared_rows)
]
# Read back EOS once after all first-code samples. Restore request order
# before grouping depth work so its RNG and CFG ordering stay unchanged.
first_ids = torch.cat([row[2] for row in sampled_rows]).to("cpu").tolist() if sampled_rows else []
groups: dict[tuple[float, int, float, float], list[tuple[list[int], torch.Tensor, torch.Tensor]]] = {}
for (indices, hidden, first, parameters), first_id in zip(sampled_rows, first_ids, strict=True):
if first_id == self.config.eos_token_id:
self._next_tokens[indices] = self.config.eos_token_id
continue
state = cast(BreezeState, infos[indices[0]]["breeze_state"])
state["history"] = torch.cat((state["history"], first[:, None]), dim=1)
groups.setdefault(parameters, []).append((indices, hidden, first))
for (temperature, top_k, top_p, guidance_scale), rows in groups.items():
# Depth batches place every conditioned row before every
# unconditional row. Both branches consume the same sampled codes.
hidden = torch.stack([row[1] for row in rows]).transpose(0, 1).reshape(-1, self.hidden_size)
with record_function_or_nullcontext("breeze:depth_decoder"):
frames = self.depth_decoder.generate_frames(
hidden,
torch.cat([row[2] for row in rows]),
temperature=temperature,
top_k=top_k,
top_p=top_p,
generators=[cast(BreezeState, infos[row[0][0]]["breeze_state"])["generator"] for row in rows],
guidance_scale=guidance_scale,
)
for offset, (indices, _, _) in enumerate(rows):
frame = frames[offset : offset + 1]
for index in indices:
cast(BreezeState, infos[index]["breeze_state"])["current"] = frame
output_codes[indices[0]] = frame
return OmniOutput(text_hidden_states=model_outputs, multimodal_outputs={"codes": {"audio": output_codes}})
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
backbone: list[tuple[str, torch.Tensor]] = []
depth: dict[str, torch.Tensor] = {}
loaded: set[str] = set()
params = dict(self.named_parameters())
for name, value in weights:
if name.startswith(("backbone_model.layers.", "backbone_model.norm.")):
backbone.append((name.removeprefix("backbone_model."), value))
elif name.startswith("depth_decoder."):
depth[name] = value
elif name.startswith(("text_encoder.", "text_encoder_proj.", "lm_head.")):
default_weight_loader(params[name], value)
loaded.add(name)
elif not name.startswith(("codec_model.", "backbone_model.embed_tokens.", "embed_text_tokens.")):
raise ValueError(f"Unexpected Breeze checkpoint parameter: {name}")
loaded.update("model." + name for name in self.model.load_weights(backbone))
for target, parameter in self.depth_decoder.named_parameters():
if target == "codebooks_head":
value = depth["depth_decoder.codebooks_head.weight"]
elif target.endswith("qkv.weight"):
layer = target.split(".")[1]
value = torch.cat(
[depth[f"depth_decoder.model.layers.{layer}.self_attn.{p}_proj.weight"] for p in ("q", "k", "v")]
)
elif target.endswith("gate_up.weight"):
layer = target.split(".")[1]
value = torch.cat(
[depth[f"depth_decoder.model.layers.{layer}.mlp.{p}_proj.weight"] for p in ("gate", "up")]
)
else:
source = target.replace(".o_proj.", ".self_attn.o_proj.").replace(".down_proj.", ".mlp.down_proj.")
value = depth["depth_decoder.model." + source]
default_weight_loader(parameter, value)
loaded.add("depth_decoder." + target)
loaded.update("reference_encoder." + name for name in self.reference_encoder.load_weights(self.vllm_config))
missing = set(params) - loaded
if missing:
raise ValueError(f"Uninitialized Breeze parameters: {sorted(missing)}")
return loaded
@@ -1,425 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Breeze-TTS-2 stage-1 codec (codec codes -> waveform).
The talker emits a frame-major ``(T, 16)`` matrix. The stage input
processor flattens it codebook-major; this module restores the ``(T, 16)``
layout and delegates decoding to Breeze's bundled Qwen3-TTS tokenizer. A
Mimi fallback remains available for checkpoints that omit ``audio_tokenizer``.
"""
from __future__ import annotations
from collections.abc import Iterable, Mapping
from typing import Any
import torch
from torch import nn
from vllm.config import VllmConfig
from vllm_omni.model_executor.models.breeze_tts_2.audio_tokenizer import (
resolve_audio_tokenizer_path,
)
from vllm_omni.model_executor.models.output_templates import OmniOutput
def _split_flat_ids(ids: torch.Tensor, counts: list[int] | None) -> list[torch.Tensor]:
if counts is None or len(counts) <= 1:
return [ids]
boundaries = [0]
for count in counts:
boundaries.append(boundaries[-1] + int(count))
return [ids[boundaries[i] : boundaries[i + 1]] for i in range(len(counts))]
class BreezeTTS2MimiCodec(nn.Module):
"""Synchronous Breeze codec stage used by ``GenerationModelRunner``."""
input_modalities = "audio"
requires_raw_input_tokens = True
# Stateful chunk decoding needs the scheduler-side request id, which is
# also used by on_requests_finished to release decoder state.
requires_request_ids = True
have_multimodal_outputs = True
has_preprocess = False
has_postprocess = False
enable_update_additional_information = True
def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
super().__init__()
del prefix
self.vllm_config = vllm_config
self.config = vllm_config.model_config.hf_config
self.model_path = vllm_config.model_config.model
self._async_chunk = bool(getattr(vllm_config.model_config, "async_chunk", False))
# Resolve once: a local directory works directly, an HF repo id
# resolves to its cached snapshot. ``None`` means the checkpoint has
# no bundled tokenizer and the Mimi fallback applies.
self._tokenizer_path = resolve_audio_tokenizer_path(self.model_path)
if self._async_chunk and self._tokenizer_path is None:
raise RuntimeError(
"Breeze-TTS-2 async_chunk requires the bundled Qwen3-TTS audio_tokenizer; "
"the stateless Mimi fallback cannot preserve chunk decoder state"
)
self._audio_tokenizer = None
codec_config = getattr(self.config, "codec_config", None)
if isinstance(codec_config, Mapping):
codec_kwargs = dict(codec_config)
elif codec_config is not None:
codec_kwargs = codec_config.to_dict()
else:
codec_kwargs = {}
codec_kwargs.pop("model_type", None)
codec_kwargs.pop("architectures", None)
# Breeze checkpoints normally carry a Qwen3-TTS decoder in this
# subdirectory. Avoid constructing a second, unused Mimi network in
# that case; this saves both startup time and worker memory.
self._codec = None
if self._tokenizer_path is None:
# Import lazily: stage 0 does not need the Mimi implementation.
from transformers import MimiConfig, MimiModel
self._codec = MimiModel(MimiConfig(**codec_kwargs)).eval()
for parameter in self._codec.parameters():
parameter.requires_grad_(False)
self._num_codebooks = int(getattr(self.config, "num_codebooks", 16))
self._codebook_size = int(codec_kwargs.get("codebook_size", 2048))
self._sample_rate = int(codec_kwargs.get("sampling_rate", getattr(self.config, "sampling_rate", 24000)))
self._decoder_state_cache: dict[str, dict[str, Any]] = {}
def embed_input_ids(self, input_ids: torch.Tensor, **_: Any) -> torch.Tensor:
"""Return stable dummy embeddings; this stage never samples."""
if input_ids.numel() == 0:
return torch.empty((0, 1), device=input_ids.device, dtype=torch.float32)
return torch.zeros((input_ids.shape[0], 1), device=input_ids.device, dtype=torch.float32)
def compute_logits(
self,
hidden_states: torch.Tensor | OmniOutput,
sampling_metadata: Any = None,
) -> None:
del hidden_states, sampling_metadata
return None
@torch.inference_mode()
def forward(
self,
input_ids: torch.Tensor | None = None,
runtime_additional_information: list[dict[str, Any]] | None = None,
**kwargs: Any,
) -> OmniOutput:
empty = torch.empty(0, dtype=torch.float32)
sr = torch.tensor(self._sample_rate, dtype=torch.int32)
if input_ids is None or input_ids.numel() == 0:
return OmniOutput(
text_hidden_states=None,
multimodal_outputs={"model_outputs": [empty], "sr": [sr]},
)
# The generation runner's profiling/dummy invocation has no request ids
# or runtime payload. Real requests always provide both.
if runtime_additional_information is None and kwargs.get("request_ids") is None:
return OmniOutput(
text_hidden_states=None,
multimodal_outputs={"model_outputs": [empty], "sr": [sr]},
)
ids = input_ids.reshape(-1).to(dtype=torch.long)
counts = kwargs.get("seq_token_counts")
if counts is not None:
counts = [int(item) for item in counts]
requests = _split_flat_ids(ids, counts)
waveforms: list[torch.Tensor] = []
sample_rates: list[torch.Tensor] = []
if self._async_chunk:
return self._forward_stateful_chunks(
ids=ids,
requests=requests,
runtime_additional_information=runtime_additional_information,
empty=empty,
sr=sr,
**kwargs,
)
for index, request_ids in enumerate(requests):
runtime_info = (
runtime_additional_information[index]
if runtime_additional_information is not None and index < len(runtime_additional_information)
else None
)
flat = self._payload_codes(request_ids, runtime_info)
if flat.numel() == 0:
waveforms.append(empty.to(device=ids.device))
sample_rates.append(sr)
continue
if flat.numel() % self._num_codebooks != 0:
raise ValueError(
f"Breeze codec input length {int(flat.numel())} is not divisible by "
f"num_codebooks {self._num_codebooks}"
)
if int(flat.min()) < 0 or int(flat.max()) >= self._codebook_size:
raise ValueError(
f"Breeze codec id outside [0, {self._codebook_size}): min={int(flat.min())}, max={int(flat.max())}"
)
frames = flat.numel() // self._num_codebooks
codes = flat.reshape(self._num_codebooks, frames).unsqueeze(0)
waveform = self._decode_codes(codes)
if waveform.ndim == 3:
waveform = waveform[0, 0]
elif waveform.ndim == 2:
waveform = waveform[0]
if waveform.ndim != 1:
raise ValueError(f"Breeze Mimi waveform must be 1D, got {tuple(waveform.shape)}")
# Serving/output serialization consumes CPU tensors. Move only
# the final waveform off the worker device; codec inference itself
# remains on the accelerator.
waveforms.append(waveform.detach().to(device="cpu", dtype=torch.float32).contiguous())
sample_rates.append(sr)
return OmniOutput(
text_hidden_states=None,
multimodal_outputs={"model_outputs": waveforms, "sr": sample_rates},
)
def _forward_stateful_chunks(
self,
*,
ids: torch.Tensor,
requests: list[torch.Tensor],
runtime_additional_information: list[dict[str, Any]] | None,
empty: torch.Tensor,
sr: torch.Tensor,
**kwargs: Any,
) -> OmniOutput:
"""Decode incremental codebook-major chunks with per-request state."""
decoder = self._audio_tokenizer.model.decoder
scheduler_request_ids = kwargs.get("request_ids")
if scheduler_request_ids is None:
raise ValueError("Breeze async_chunk codec inputs require scheduler request_ids")
request_ids = [str(request_id) for request_id in scheduler_request_ids]
if len(request_ids) != len(requests):
raise ValueError(
f"Breeze codec request id count {len(request_ids)} does not match input request count {len(requests)}"
)
valid: list[tuple[str, torch.Tensor, bool]] = []
valid_indices: list[int] = []
for index, request_ids_tensor in enumerate(requests):
runtime_info = (
runtime_additional_information[index]
if runtime_additional_information is not None and index < len(runtime_additional_information)
else {}
)
state_id = request_ids[index]
# The connector receiver strips ``meta.finished`` while merging
# runtime metadata, so the stream-terminated signal arrives on
# the model-visible ``stream_finished`` field (the same contract
# CosyVoice3 / Audex use). Fall back to ``finished`` for direct
# in-process callers that still set only the transport flag.
finished = self._meta_bool(runtime_info, "stream_finished") or self._meta_bool(runtime_info, "finished")
flat = self._payload_codes(request_ids_tensor, runtime_info)
if flat.numel() == 0:
# Terminal marker: the stage-0 processor sends an empty
# payload with ``finished=True`` once generation ends.
self._decoder_state_cache.pop(state_id, None)
continue
if flat.numel() % self._num_codebooks != 0:
# A partial frame means the transport payload is corrupt.
# Silently resetting decoder state would corrupt the rest of
# the stream; fail loudly instead. The scheduler aborts the
# request and ``on_requests_finished`` releases the state.
raise ValueError(
f"Breeze codec chunk length {int(flat.numel())} is not divisible by "
f"num_codebooks {self._num_codebooks}"
)
if int(flat.min()) < 0 or int(flat.max()) >= self._codebook_size:
raise ValueError(
f"Breeze codec id outside [0, {self._codebook_size}): min={int(flat.min())}, max={int(flat.max())}"
)
frames = int(flat.numel()) // self._num_codebooks
valid.append((state_id, flat.reshape(self._num_codebooks, frames), finished))
valid_indices.append(index)
waveforms = [empty.to(device=ids.device)] * len(requests)
sample_rates = [sr] * len(requests)
if not valid:
return OmniOutput(
text_hidden_states=None,
multimodal_outputs={"model_outputs": waveforms, "sr": sample_rates},
)
states: list[dict[str, Any]] = []
max_frames = max(codes.shape[-1] for _, codes, _ in valid)
batched_codes = valid[0][1].new_zeros((len(valid), self._num_codebooks, max_frames))
for row, (state_id, row_codes, _) in enumerate(valid):
batched_codes[row, :, : row_codes.shape[-1]].copy_(row_codes)
state = self._decoder_state_cache.get(state_id)
if state is None:
state = {}
self._decoder_state_cache[state_id] = state
state.setdefault("prefix_frames", 0)
states.append(state)
decoded = decoder.batched_chunked_decode(
batched_codes,
[int(codes.shape[-1]) for _, codes, _ in valid],
caches=states,
chunk_size=300,
left_context_size=25,
)
if len(decoded) != len(valid):
raise RuntimeError(f"Breeze codec returned {len(decoded)} chunks for {len(valid)} requests")
for request_index, waveform in zip(valid_indices, decoded, strict=True):
if waveform.ndim == 2 and waveform.shape[0] == 1:
waveform = waveform[0]
elif waveform.ndim != 1:
waveform = waveform.reshape(-1)
waveforms[request_index] = waveform.detach().to(device="cpu", dtype=torch.float32).contiguous()
for state_id, _, finished in valid:
if finished:
self._decoder_state_cache.pop(state_id, None)
return OmniOutput(
text_hidden_states=None,
multimodal_outputs={"model_outputs": waveforms, "sr": sample_rates},
)
@staticmethod
def _meta_bool(info: Mapping[str, Any] | None, key: str) -> bool:
if not isinstance(info, Mapping):
return False
meta = info.get("meta", {})
if not isinstance(meta, Mapping):
return False
value = meta.get(key, False)
if isinstance(value, torch.Tensor):
return bool(value.reshape(-1)[0].item()) if value.numel() else False
return bool(value)
def on_requests_finished(self, finished_req_ids: Iterable[str]) -> None:
for request_id in finished_req_ids:
self._decoder_state_cache.pop(str(request_id), None)
def _decode_codes(self, codes: torch.Tensor) -> torch.Tensor:
"""Decode one ``(1, Q, T)`` sequence using Breeze's bundled codec."""
if self._audio_tokenizer is not None:
wavs, sample_rate = self._audio_tokenizer.decode({"audio_codes": [codes[0].transpose(0, 1).contiguous()]})
self._sample_rate = int(sample_rate)
if not wavs:
raise RuntimeError("Breeze Qwen3-TTS tokenizer returned no waveform")
return torch.as_tensor(wavs[0], device=codes.device, dtype=torch.float32).reshape(-1)
if self._codec is None:
raise RuntimeError("Breeze codec is not initialized; load_weights() was not completed")
decoded = self._codec.decode(codes)
waveform = getattr(decoded, "audio_values", decoded)
if isinstance(waveform, Mapping):
waveform = waveform.get("audio_values")
if waveform is None:
raise RuntimeError("MimiModel.decode returned no audio_values")
return torch.as_tensor(waveform, device=codes.device, dtype=torch.float32)
@staticmethod
def _payload_codes(input_ids: torch.Tensor, runtime_info: Mapping[str, Any] | None) -> torch.Tensor:
if isinstance(runtime_info, Mapping):
codes = runtime_info.get("codes")
if isinstance(codes, Mapping):
audio = codes.get("audio")
if audio is not None:
return torch.as_tensor(audio, device=input_ids.device, dtype=torch.long).reshape(-1)
# Full-payload transport may flatten nested OmniPayload keys at
# the wire boundary and restore them only after this hook runs.
audio = runtime_info.get("codes.audio")
if audio is not None:
return torch.as_tensor(audio, device=input_ids.device, dtype=torch.long).reshape(-1)
return input_ids
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
"""Load Breeze's bundled Qwen3-TTS codec, or fallback Mimi weights."""
if self._tokenizer_path is not None:
# The root checkpoint contains both the talker weights and a Mimi
# ``codec_model.*`` fallback. When the bundled Qwen3-TTS tokenizer
# is present, it owns decoding; consume the root iterator without
# copying the unused fallback into worker memory.
if self._audio_tokenizer is None:
from vllm_omni.model_executor.models.qwen3_tts.qwen3_tts_tokenizer import (
Qwen3TTSTokenizer,
)
device = self.vllm_config.device_config.device
self._audio_tokenizer = Qwen3TTSTokenizer.from_pretrained(
str(self._tokenizer_path),
device_map=str(device),
)
self._sample_rate = int(self._audio_tokenizer.model.get_output_sample_rate())
if self._async_chunk:
connector_config = getattr(self.vllm_config.model_config, "stage_connector_config", None)
extra = (
connector_config.get("extra", connector_config)
if isinstance(connector_config, Mapping)
else getattr(connector_config, "extra", None)
)
value = extra.get("breeze_codec_chunk_frames") if isinstance(extra, Mapping) else None
try:
chunk_frames = int(value) if value is not None else 8
except (TypeError, ValueError) as exc:
raise ValueError(f"Invalid Breeze codec_chunk_frames={value!r}") from exc
if chunk_frames <= 0:
raise ValueError(f"Breeze codec_chunk_frames must be positive, got {chunk_frames}")
# Qwen treats chunks shorter than its configured window as
# terminal tails and does not advance their rolling cache.
# Match the producer's chunk size instead of its default 25.
decoder = self._audio_tokenizer.model.decoder
decoder._initial_codec_chunk_frames = chunk_frames
decoder._incremental_chunk_frames = chunk_frames
decoder._incremental_chunk_ramp = []
for _ in weights:
pass
tokenizer_module = getattr(self._audio_tokenizer, "model", None)
named_parameters = getattr(tokenizer_module, "named_parameters", None)
if callable(named_parameters):
return {f"_audio_tokenizer.model.{name}" for name, _ in named_parameters()}
return set()
params = dict(self._codec.named_parameters()) if self._codec is not None else {}
loaded: set[str] = set()
for name, tensor in weights:
if name.startswith("codec_model."):
target_name = name[len("codec_model.") :]
else:
continue
parameter = params.get(target_name)
if parameter is None:
raise ValueError(f"Unknown Breeze Mimi weight: {target_name}")
loader = getattr(parameter, "weight_loader", None)
if loader is None:
parameter.data.copy_(tensor.to(device=parameter.device, dtype=parameter.dtype))
else:
loader(parameter, tensor)
loaded.add(target_name)
if loaded:
return {f"_codec.{name}" for name in loaded}
# Breeze inference uses the bundled Qwen3-TTS tokenizer for both
# reference encoding and generated-code decoding. Load it once per
# Stage-1 worker; unlike the request-side wrapper this instance is
# placed on the worker device and reused for the entire batch.
# A few Breeze distributions omit codec tensors and point to the
# standalone kyutai/mimi repository instead. Load it once, never per
# request, and fail loudly if it cannot be resolved.
codec_name = getattr(self.config, "codec_model_name_or_path", None) or "kyutai/mimi"
from transformers import AutoModel
self._codec = AutoModel.from_pretrained(codec_name).eval()
for parameter in self._codec.parameters():
parameter.requires_grad_(False)
self._sample_rate = int(getattr(self._codec.config, "sampling_rate", self._sample_rate))
self._codebook_size = int(
getattr(getattr(self._codec.config, "quantizer", None), "cardinality", self._codebook_size)
)
return {f"_codec.{name}" for name, _ in self._codec.named_parameters()}
__all__ = ["BreezeTTS2MimiCodec"]
@@ -1,523 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
from collections.abc import Callable
from inspect import signature
from typing import Any
import torch
from torch import nn
from transformers.activations import ACT2FN
from transformers.cache_utils import Cache, DynamicCache
from transformers.integrations import use_kernel_forward_from_hub
from transformers.masking_utils import create_causal_mask
from transformers.modeling_outputs import BaseModelOutputWithPast, CausalLMOutputWithPast
from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS, dynamic_rope_update
from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS
from transformers.processing_utils import Unpack
from transformers.utils import TransformersKwargs, logging
from transformers.utils.deprecation import deprecate_kwarg
logger = logging.get_logger(__name__)
def _create_depth_causal_mask(
*,
config: Any,
inputs_embeds: torch.Tensor,
attention_mask: torch.Tensor | None,
past_key_values: Cache | None,
position_ids: torch.LongTensor | None,
cache_position: torch.LongTensor,
) -> torch.Tensor | None:
"""Bridge Transformers 4.57 and 5.x ``create_causal_mask`` contracts."""
parameters = signature(create_causal_mask).parameters
kwargs: dict[str, Any] = {
"config": config,
"attention_mask": attention_mask,
"past_key_values": past_key_values,
"position_ids": position_ids,
}
if "inputs_embeds" in parameters:
kwargs["inputs_embeds"] = inputs_embeds
else:
kwargs["input_embeds"] = inputs_embeds
kwargs["cache_position"] = cache_position
return create_causal_mask(**kwargs)
class BreezeDepthDecoderModel(nn.Module):
def __init__(self, config):
super().__init__()
self.config = config
self.padding_idx = config.pad_token_id
self.vocab_size = config.vocab_size
if hasattr(config, "audio_embed_size") and config.audio_embed_size:
self.audio_embed_size = config.audio_embed_size
else:
self.audio_embed_size = config.backbone_hidden_size
self.embed_tokens = nn.Embedding((config.num_codebooks * config.vocab_size), self.audio_embed_size)
self.layers = nn.ModuleList(
[BreezeDecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)]
)
self.norm = BreezeRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.rotary_emb = BreezeRotaryEmbedding(config=config)
self.gradient_checkpointing = False
self.inputs_embeds_projector = nn.Linear(self.audio_embed_size, config.hidden_size, bias=False)
if config.backbone_hidden_size != self.audio_embed_size:
self.backbone_hidden_state_projector = nn.Linear(
config.backbone_hidden_size, self.audio_embed_size, bias=False
)
else:
self.backbone_hidden_state_projector = None
# Weights are loaded from the Breeze checkpoint by the parent talker.
# Avoid PreTrainedModel.post_init here because this module is a plain
# nn.Module used for eager per-frame inference.
def forward(
self,
input_ids: torch.LongTensor = None,
backbone_last_hidden_state: torch.FloatTensor | None = None,
attention_mask: torch.Tensor | None = None,
position_ids: torch.LongTensor | None = None,
past_key_values: Cache | None = None,
inputs_embeds: torch.FloatTensor | None = None,
use_cache: bool | None = None,
cache_position: torch.LongTensor | None = None,
**kwargs: Unpack[TransformersKwargs],
) -> tuple | BaseModelOutputWithPast:
r"""
backbone_last_hidden_state (`torch.FloatTensor` of shape `(batch_size, backbone_hidden_size)`, *optional*):
The last hidden state of the backbone model. This is required when
the first codebook token (generated by the backbone model) is
provided in the `input_ids` argument.
"""
if position_ids is not None and not torch.compiler.is_compiling():
logger.warning_once(
"Custom `position_ids` were provided but will be ignored. "
"Breeze depth decoder derives them from `cache_position`; "
"they must be identical across the batch."
)
position_ids = None
if (input_ids is None) ^ (inputs_embeds is not None):
raise ValueError("You must specify exactly one of input_ids or inputs_embeds.")
if use_cache and past_key_values is None:
past_key_values = DynamicCache(config=self.config)
if cache_position is None:
past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0
inputs_seq_length = inputs_embeds.shape[1] if inputs_embeds is not None else input_ids.shape[1]
device = inputs_embeds.device if inputs_embeds is not None else input_ids.device
cache_position = torch.arange(past_seen_tokens, past_seen_tokens + inputs_seq_length, device=device)
if inputs_embeds is None:
codebook_idxs = torch.clamp(cache_position - 1, min=0)
offset = codebook_idxs * self.vocab_size
inputs_embeds = self.embed_tokens(input_ids + offset)
input_ids_are_first_codebook = cache_position[0] == 0
if backbone_last_hidden_state is not None:
if self.backbone_hidden_state_projector is not None:
inputs_embeds[:, 0] = self.backbone_hidden_state_projector(backbone_last_hidden_state)
else:
inputs_embeds[:, 0] = backbone_last_hidden_state
else:
if not torch.compiler.is_compiling() and input_ids_are_first_codebook:
logger.warning(
"When the first codebook token is provided, "
"`backbone_last_hidden_state` is required for correct inference."
)
inputs_embeds = self.inputs_embeds_projector(inputs_embeds)
causal_mask = _create_depth_causal_mask(
config=self.config,
inputs_embeds=inputs_embeds,
attention_mask=attention_mask,
past_key_values=past_key_values,
position_ids=position_ids,
cache_position=cache_position,
)
hidden_states = inputs_embeds
# create position embeddings to be shared across the decoder layers
position_ids = cache_position.unsqueeze(0)
position_embeddings = self.rotary_emb(hidden_states, position_ids)
for decoder_layer in self.layers[: self.config.num_hidden_layers]:
hidden_states = decoder_layer(
hidden_states,
attention_mask=causal_mask,
position_ids=position_ids,
past_key_values=past_key_values,
use_cache=use_cache,
cache_position=cache_position,
position_embeddings=position_embeddings,
**kwargs,
)
hidden_states = self.norm(hidden_states)
return BaseModelOutputWithPast(
last_hidden_state=hidden_states,
past_key_values=past_key_values if use_cache else None,
)
@use_kernel_forward_from_hub("RMSNorm")
class BreezeRMSNorm(nn.Module):
def __init__(self, hidden_size, eps=1e-6):
"""
BreezeRMSNorm is equivalent to T5LayerNorm
"""
super().__init__()
self.weight = nn.Parameter(torch.ones(hidden_size))
self.variance_epsilon = eps
def forward(self, hidden_states):
input_dtype = hidden_states.dtype
hidden_states = hidden_states.to(torch.float32)
variance = hidden_states.pow(2).mean(-1, keepdim=True)
hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
return self.weight * hidden_states.to(input_dtype)
def extra_repr(self):
return f"{tuple(self.weight.shape)}, eps={self.variance_epsilon}"
class BreezeRotaryEmbedding(nn.Module):
inv_freq: torch.Tensor # fix linting for `register_buffer`
def __init__(self, config, device=None):
super().__init__()
# BC: "rope_type" was originally "type"
if hasattr(config, "rope_scaling") and isinstance(config.rope_scaling, dict):
self.rope_type = config.rope_scaling.get("rope_type", config.rope_scaling.get("type"))
else:
self.rope_type = "default"
self.max_seq_len_cached = config.max_position_embeddings
self.original_max_seq_len = config.max_position_embeddings
self.config = config
self.rope_init_fn = ROPE_INIT_FUNCTIONS[self.rope_type]
inv_freq, self.attention_scaling = self.rope_init_fn(self.config, device)
self.register_buffer("inv_freq", inv_freq, persistent=False)
self.original_inv_freq = self.inv_freq
@torch.no_grad()
@dynamic_rope_update # power user: used with advanced RoPE types (e.g. dynamic rope)
def forward(self, x, position_ids):
inv_freq_expanded = self.inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1).to(x.device)
position_ids_expanded = position_ids[:, None, :].float()
device_type = x.device.type if isinstance(x.device.type, str) and x.device.type != "mps" else "cpu"
with torch.autocast(device_type=device_type, enabled=False): # Force float32
freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(1, 2)
emb = torch.cat((freqs, freqs), dim=-1)
cos = emb.cos() * self.attention_scaling
sin = emb.sin() * self.attention_scaling
return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
class BreezeMLP(nn.Module):
def __init__(self, config):
super().__init__()
self.config = config
self.hidden_size = config.hidden_size
self.intermediate_size = config.intermediate_size
self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=config.mlp_bias)
self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=config.mlp_bias)
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=config.mlp_bias)
self.act_fn = ACT2FN[config.hidden_act]
def forward(self, x):
down_proj = self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x))
return down_proj
def rotate_half(x):
"""Rotates half the hidden dims of the input."""
x1 = x[..., : x.shape[-1] // 2]
x2 = x[..., x.shape[-1] // 2 :]
return torch.cat((-x2, x1), dim=-1)
def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1):
"""Applies Rotary Position Embedding to the query and key tensors.
Args:
q (`torch.Tensor`): The query tensor.
k (`torch.Tensor`): The key tensor.
cos (`torch.Tensor`): The cosine part of the rotary embedding.
sin (`torch.Tensor`): The sine part of the rotary embedding.
position_ids (`torch.Tensor`, *optional*):
Deprecated and unused.
unsqueeze_dim (`int`, *optional*, defaults to 1):
The 'unsqueeze_dim' argument specifies the dimension along which to unsqueeze cos[position_ids] and
sin[position_ids] so that they can be properly broadcasted to the dimensions of q and k. For example, note
that cos[position_ids] and sin[position_ids] have the shape [batch_size, seq_len, head_dim]. Then, if q and
k have the shape [batch_size, heads, seq_len, head_dim], then setting unsqueeze_dim=1 makes
cos[position_ids] and sin[position_ids] broadcastable to the shapes of q and k. Similarly, if q and k have
the shape [batch_size, seq_len, heads, head_dim], then set unsqueeze_dim=2.
Returns:
`tuple(torch.Tensor)` comprising of the query and key tensors rotated using the Rotary Position Embedding.
"""
cos = cos.unsqueeze(unsqueeze_dim)
sin = sin.unsqueeze(unsqueeze_dim)
q_embed = (q * cos) + (rotate_half(q) * sin)
k_embed = (k * cos) + (rotate_half(k) * sin)
return q_embed, k_embed
def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor:
"""
This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep). The hidden states go from (batch,
num_key_value_heads, seqlen, head_dim) to (batch, num_attention_heads, seqlen, head_dim)
"""
batch, num_key_value_heads, slen, head_dim = hidden_states.shape
if n_rep == 1:
return hidden_states
hidden_states = hidden_states[:, :, None, :, :].expand(batch, num_key_value_heads, n_rep, slen, head_dim)
return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim)
def eager_attention_forward(
module: nn.Module,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attention_mask: torch.Tensor | None,
scaling: float,
dropout: float = 0.0,
**kwargs: Unpack[TransformersKwargs],
):
key_states = repeat_kv(key, module.num_key_value_groups)
value_states = repeat_kv(value, module.num_key_value_groups)
attn_weights = torch.matmul(query, key_states.transpose(2, 3)) * scaling
if attention_mask is not None:
causal_mask = attention_mask[:, :, :, : key_states.shape[-2]]
attn_weights = attn_weights + causal_mask
attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query.dtype)
attn_weights = nn.functional.dropout(attn_weights, p=dropout, training=module.training)
attn_output = torch.matmul(attn_weights, value_states)
attn_output = attn_output.transpose(1, 2).contiguous()
return attn_output, attn_weights
class BreezeAttention(nn.Module):
"""Multi-headed attention from 'Attention Is All You Need' paper"""
def __init__(self, config, layer_idx: int):
super().__init__()
self.config = config
self.layer_idx = layer_idx
self.head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads)
self.num_key_value_groups = config.num_attention_heads // config.num_key_value_heads
self.scaling = self.head_dim**-0.5
self.attention_dropout = config.attention_dropout
self.is_causal = True
self.q_proj = nn.Linear(
config.hidden_size,
config.num_attention_heads * self.head_dim,
bias=config.attention_bias,
)
self.k_proj = nn.Linear(
config.hidden_size,
config.num_key_value_heads * self.head_dim,
bias=config.attention_bias,
)
self.v_proj = nn.Linear(
config.hidden_size,
config.num_key_value_heads * self.head_dim,
bias=config.attention_bias,
)
self.o_proj = nn.Linear(
config.num_attention_heads * self.head_dim,
config.hidden_size,
bias=config.attention_bias,
)
@deprecate_kwarg("past_key_value", new_name="past_key_values", version="4.58")
def forward(
self,
hidden_states: torch.Tensor,
position_embeddings: tuple[torch.Tensor, torch.Tensor],
attention_mask: torch.Tensor | None,
past_key_values: Cache | None = None,
cache_position: torch.LongTensor | None = None,
**kwargs: Unpack[TransformersKwargs],
) -> tuple[torch.Tensor, torch.Tensor]:
input_shape = hidden_states.shape[:-1]
hidden_shape = (*input_shape, -1, self.head_dim)
query_states = self.q_proj(hidden_states).view(hidden_shape).transpose(1, 2)
key_states = self.k_proj(hidden_states).view(hidden_shape).transpose(1, 2)
value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2)
cos, sin = position_embeddings
query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin)
if past_key_values is not None:
# sin and cos are specific to RoPE models; cache_position needed for the static cache
cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position}
key_states, value_states = past_key_values.update(key_states, value_states, self.layer_idx, cache_kwargs)
attention_interface: Callable = eager_attention_forward
if self.config._attn_implementation != "eager":
attention_interface = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation]
attn_output, attn_weights = attention_interface(
self,
query_states,
key_states,
value_states,
attention_mask,
dropout=0.0 if not self.training else self.attention_dropout,
scaling=self.scaling,
**kwargs,
)
attn_output = attn_output.reshape(*input_shape, -1).contiguous()
attn_output = self.o_proj(attn_output)
return attn_output, attn_weights
class BreezeDecoderLayer(nn.Module):
def __init__(self, config, layer_idx: int):
super().__init__()
self.hidden_size = config.hidden_size
self.self_attn = BreezeAttention(config=config, layer_idx=layer_idx)
self.mlp = BreezeMLP(config)
self.input_layernorm = BreezeRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.post_attention_layernorm = BreezeRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
@deprecate_kwarg("past_key_value", new_name="past_key_values", version="4.58")
def forward(
self,
hidden_states: torch.Tensor,
attention_mask: torch.Tensor | None = None,
position_ids: torch.LongTensor | None = None,
past_key_values: Cache | None = None,
use_cache: bool | None = False,
cache_position: torch.LongTensor | None = None,
position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, # necessary, but kept here for BC
**kwargs: Unpack[TransformersKwargs],
) -> torch.Tensor:
residual = hidden_states
hidden_states = self.input_layernorm(hidden_states)
# Self Attention
hidden_states, _ = self.self_attn(
hidden_states=hidden_states,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
use_cache=use_cache,
cache_position=cache_position,
position_embeddings=position_embeddings,
**kwargs,
)
hidden_states = residual + hidden_states
# Fully Connected
residual = hidden_states
hidden_states = self.post_attention_layernorm(hidden_states)
hidden_states = self.mlp(hidden_states)
hidden_states = residual + hidden_states
return hidden_states
class BreezeCodebooksHead(nn.Module):
"""Position-specific heads for codebooks 1..15."""
def __init__(self, hidden_size: int, num_codebooks: int, vocab_size: int) -> None:
super().__init__()
self.num_codebooks = int(num_codebooks)
self.weight = nn.Parameter(torch.empty(self.num_codebooks - 1, hidden_size, vocab_size))
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
# ``hidden_states`` contains the placeholder at position 0 followed
# by codebook-0..N tokens. Return logits for codebook-1..N.
depth_states = hidden_states[:, 1 : self.num_codebooks, :]
if depth_states.shape[1] > self.num_codebooks - 1:
raise ValueError(f"depth decoder received too many input positions: {hidden_states.shape[1]}")
return torch.einsum("bkh,khv->bkv", depth_states, self.weight[: depth_states.shape[1]])
class BreezeDepthDecoderForCausalLM(nn.Module):
"""Eager depth decoder used by the vLLM stage-0 talker."""
def __init__(self, config) -> None:
super().__init__()
self.config = config
self.model = BreezeDepthDecoderModel(config)
self.codebooks_head = BreezeCodebooksHead(
config.hidden_size,
config.num_codebooks,
config.vocab_size,
)
def forward(
self,
input_ids: torch.Tensor,
backbone_last_hidden_state: torch.Tensor,
*,
use_cache: bool = False,
**kwargs,
) -> CausalLMOutputWithPast:
outputs = self.model(
input_ids=input_ids,
backbone_last_hidden_state=backbone_last_hidden_state,
use_cache=use_cache,
**kwargs,
)
logits = self.codebooks_head(outputs.last_hidden_state)
return CausalLMOutputWithPast(
logits=logits,
past_key_values=outputs.past_key_values,
hidden_states=getattr(outputs, "hidden_states", None),
attentions=getattr(outputs, "attentions", None),
)
def load_weights(self, weights):
params_dict = dict(self.named_parameters())
loaded: set[str] = set()
for name, tensor in weights:
param = params_dict.get(name)
if param is None:
continue
if name == "codebooks_head.weight":
expected = (
int(self.config.num_codebooks) - 1,
int(self.config.hidden_size),
int(self.config.vocab_size),
)
actual = tuple(int(dim) for dim in tensor.shape)
if actual != expected:
raise ValueError(f"Breeze depth weight {name!r} has shape {actual}, expected {expected}")
loader = getattr(param, "weight_loader", None)
if loader is None:
param.data.copy_(tensor.to(device=param.device, dtype=param.dtype))
else:
loader(param, tensor)
loaded.add(name)
return loaded
__all__ = [
"BreezeCodebooksHead",
"BreezeDepthDecoderForCausalLM",
"BreezeDepthDecoderModel",
]
@@ -1,641 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Breeze-TTS-2 stage-0 talker for vLLM-Omni.
The scheduler samples Breeze codebook-0 tokens one at a time. The other 15
codebooks are kept in the request-local Omni buffer: after each backbone step,
the eager depth decoder completes the frame and the next backbone step
consumes the summed 16-codebook embedding. This preserves the upstream Breeze
generation order without importing HuggingFace GenerationMixin.
"""
from __future__ import annotations
import hashlib
import math
import os
from collections.abc import Iterable, Mapping, Sequence
from pathlib import Path
from typing import Any
import torch
from torch import nn
from torch.nn.utils.rnn import pad_sequence
from vllm.config import VllmConfig
from vllm.model_executor.layers.logits_processor import LogitsProcessor
from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
from vllm.model_executor.models.qwen3 import Qwen3Model
from vllm.sequence import IntermediateTensors
from vllm.v1.sample.metadata import SamplingMetadata
from vllm_omni.model_executor.models.breeze_tts_2.configuration_breeze_tts_2 import (
BreezeTTS2Config,
)
from vllm_omni.model_executor.models.breeze_tts_2.modeling_breeze_tts_2_depth import (
BreezeDepthDecoderForCausalLM,
)
from vllm_omni.model_executor.models.breeze_tts_2.modeling_breeze_tts_2_text import (
BreezeTTS2TextEncoder,
)
from vllm_omni.model_executor.models.output_templates import OmniOutput
def _prefixed(prefix: str, name: str) -> str:
return f"{prefix}.{name}" if prefix else name
def _normalize_request_infos(infos: Any) -> list[dict[str, Any]] | None:
"""Normalize runner metadata to one mutable dict per request."""
if not isinstance(infos, Sequence) or isinstance(infos, (str, bytes, bytearray)):
return None
if not all(isinstance(info, dict) for info in infos):
return None
return infos
def _iter_request_rows(
states: Sequence[dict[str, Any]],
spans: Sequence[tuple[int, int]] | None,
num_rows: int,
) -> Iterable[tuple[dict[str, Any], int, int]]:
"""Map request state to logits rows for full or sampled hidden layouts."""
if num_rows <= 0 or not states:
return
if spans is not None and len(spans) == len(states):
if all(0 <= int(start) <= int(end) <= num_rows for start, end in spans):
for state, (start, end) in zip(states, spans, strict=True):
if int(start) < int(end):
yield state, int(start), int(end)
return
# The runner passes full-sequence spans to make_omni_output but only
# one sampled row per request to compute_logits.
if num_rows == len(states):
for index, state in enumerate(states):
yield state, index, index + 1
return
if num_rows == len(states):
for index, state in enumerate(states):
yield state, index, index + 1
return
raise RuntimeError(
"Breeze talker cannot align request state with logits rows: "
f"states={len(states)}, rows={num_rows}, spans={spans}"
)
def _check_weight_shape(name: str, tensor: torch.Tensor, expected: tuple[int, ...]) -> None:
"""Fail early for Breeze-owned, non-sharded checkpoint tensors."""
actual = tuple(int(dim) for dim in tensor.shape)
if actual != expected:
raise ValueError(f"Breeze weight {name!r} has shape {actual}, expected {expected}")
class BreezeTTS2TalkerForGeneration(nn.Module):
"""Stage-0 Breeze talker: text/audio conditioning -> codec frames."""
have_multimodal_outputs = True
has_preprocess = True
has_postprocess = True
# Stage 0 emits a tiny per-frame codec payload. Let the AR runner snapshot
# it through the async-output path instead of the final-only client path.
use_async_omni_output = True
eager_omni_postprocess_before_async_output = True
omni_pooler_payload_include_hidden = False
input_modalities = "text"
gpu_resident_buffer_keys: set[tuple[str, str]] = {
("hidden_states", "last"),
}
def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
super().__init__()
self.vllm_config = vllm_config
self.config: BreezeTTS2Config = vllm_config.model_config.hf_config
self._async_chunk = bool(vllm_config.model_config.async_chunk)
self._golden_dump_dir = os.environ.get("VLLM_OMNI_BREEZE_GOLDEN_DIR")
backbone_config = self.config.backbone_config
backbone_vllm_config = vllm_config.with_hf_config(
backbone_config,
architectures=["Qwen3ForCausalLM"],
)
backbone_vllm_config.model_config.hf_text_config = backbone_config
self.model = Qwen3Model(
vllm_config=backbone_vllm_config,
prefix=_prefixed(prefix, "model"),
)
# Upstream's Qwen3 adapter keeps only decoder layers/norm/RoPE and
# replaces the standard text embedding with Breeze's summed audio/text
# tables. Breeze always supplies ``inputs_embeds`` through preprocess,
# so remove the unused Qwen embedding instead of allocating and then
# reporting an uninitialized 151936x2048 parameter at load time.
self.model.embed_tokens = nn.Identity()
self.hidden_size = int(backbone_config.hidden_size)
self.num_codebooks = int(self.config.num_codebooks)
self.codebook_vocab_size = int(self.config.vocab_size)
codec_config = self.config.codec_config
self.codebook_size = (
int(codec_config.get("codebook_size", self.codebook_vocab_size))
if isinstance(codec_config, Mapping)
else self.codebook_vocab_size
)
self.codebook_eos_token_id = int(self.config.codebook_eos_token_id)
self.audio_token_id = int(self.config.audio_token_id)
self.audio_eos_token_id = int(self.config.audio_eos_token_id)
self.text_pad_token_id = int(self.config.pad_token_id)
self.lm_head = ParallelLMHead(
self.codebook_vocab_size + 1,
self.hidden_size,
bias=False,
prefix=_prefixed(prefix, "lm_head"),
)
self.logits_processor = LogitsProcessor(self.codebook_vocab_size + 1)
# Upstream Breeze uses embedding(code_i + i * vocab_size) and sums
# over codebooks before entering the Qwen3 backbone.
self.embed_audio_tokens = nn.Embedding(
self.num_codebooks * self.codebook_vocab_size,
self.hidden_size,
)
# Keep Breeze's standalone text table even though the normal path
# uses T5Gemma2 outputs. It is part of the checkpoint contract and is
# also the correct fallback when a checkpoint omits text_encoder.
self.embed_text_tokens = nn.Embedding(int(self.config.text_vocab_size), self.hidden_size)
self.register_buffer(
"audio_token_offsets",
torch.arange(self.num_codebooks, dtype=torch.long) * self.codebook_vocab_size,
persistent=False,
)
self.text_encoder = BreezeTTS2TextEncoder(self.config.text_encoder_config)
self.text_encoder_proj = nn.Linear(
int(self.config.text_encoder_config.hidden_size),
self.hidden_size,
bias=False,
)
self.depth_decoder = BreezeDepthDecoderForCausalLM(self.config.depth_decoder_config)
if bool(self.config.tie_codebooks_embeddings):
# The upstream HF model ties these tables. Sharing the Parameter
# also prevents a tied checkpoint from leaving the depth input
# embedding randomly initialized when only one key is serialized.
self.depth_decoder.model.embed_tokens.weight = self.embed_audio_tokens.weight
self._codec_disallowed_mask: torch.Tensor | None = None
self._batch_state: list[dict[str, Any]] | None = None
self._batch_state_spans: Sequence[tuple[int, int]] | None = None
# ------------------------------------------------------------------
# Embedding and backbone hooks
# ------------------------------------------------------------------
def embed_input_ids(self, input_ids: torch.Tensor, **_: Any) -> torch.Tensor:
"""Embed complete generated codec frames; scalar ids are invalid."""
if input_ids.ndim < 1 or input_ids.shape[-1] != self.num_codebooks:
raise ValueError(
"Breeze embed_input_ids requires complete codec frames with shape "
f"(..., {self.num_codebooks}), got {tuple(input_ids.shape)}"
)
codes = input_ids.to(dtype=torch.long)
offsets = self.audio_token_offsets.to(device=codes.device)
ids = codes.clamp(0, self.codebook_vocab_size - 1) + offsets
return self.embed_audio_tokens(ids).sum(dim=-2)
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
intermediate_tensors: IntermediateTensors | None = None,
inputs_embeds: torch.Tensor | None = None,
**_: Any,
) -> torch.Tensor | IntermediateTensors:
return self.model(
input_ids=input_ids,
positions=positions,
intermediate_tensors=intermediate_tensors,
inputs_embeds=inputs_embeds,
)
# ------------------------------------------------------------------
# Prompt/text conditioning
# ------------------------------------------------------------------
def _encode_text_segments(
self,
input_ids: torch.Tensor,
text_mask: torch.Tensor,
text_lengths: torch.Tensor,
) -> torch.Tensor:
text_ids = input_ids[text_mask.to(device=input_ids.device, dtype=torch.bool)].to(torch.long)
lengths = [int(x) for x in text_lengths.reshape(-1).tolist() if int(x) > 0]
if int(text_ids.numel()) != sum(lengths):
raise ValueError(f"Breeze text mask/length mismatch: {text_ids.numel()} != {sum(lengths)}")
if not lengths:
return input_ids.new_empty((0, self.hidden_size), dtype=self.text_encoder.dtype)
segments = list(torch.split(text_ids, lengths))
padded = pad_sequence(segments, batch_first=True, padding_value=0)
attn = torch.zeros_like(padded, dtype=torch.bool)
for row, length in enumerate(lengths):
attn[row, :length] = True
hidden = self.text_encoder(padded, attention_mask=attn)
projected = self.text_encoder_proj(hidden)
return torch.cat([projected[row, :length] for row, length in enumerate(lengths)], dim=0)
def _build_prompt_embeddings(self, prompt_ids: torch.Tensor, info: Mapping[str, Any]) -> torch.Tensor:
device = next(self.parameters()).device
prompt_ids = prompt_ids.reshape(-1).to(device=device, dtype=torch.long)
text_mask = torch.as_tensor(info.get("text_ids_mask"), device=device, dtype=torch.bool).reshape(-1)
text_lengths = torch.as_tensor(info.get("text_ids_len"), device=device, dtype=torch.long)
if text_mask.numel() != prompt_ids.numel():
raise ValueError("Breeze text_ids_mask length does not match prompt length")
output = torch.zeros(
(prompt_ids.numel(), self.hidden_size),
device=device,
dtype=self.embed_text_tokens.weight.dtype,
)
text_embeds = self._encode_text_segments(prompt_ids, text_mask, text_lengths)
output[text_mask] = text_embeds.to(dtype=output.dtype)
audio_mask = prompt_ids == self.audio_token_id
input_values = info.get("input_values")
if bool(audio_mask.any()):
if input_values is None:
raise ValueError("Breeze audio placeholders require input_values")
codes = torch.as_tensor(input_values, device=device, dtype=torch.long)
if codes.ndim != 2 or codes.shape[-1] != self.num_codebooks:
raise ValueError(f"Breeze input_values must be (T, {self.num_codebooks})")
if int(codes.shape[0]) != int(audio_mask.sum()):
raise ValueError("Breeze reference code count does not match audio placeholders")
output[audio_mask] = self.embed_input_ids(codes).to(dtype=output.dtype)
eos_mask = prompt_ids == self.audio_eos_token_id
if bool(eos_mask.any()):
eos_codes = torch.full(
(1, self.num_codebooks),
self.codebook_eos_token_id,
dtype=torch.long,
device=device,
)
output[eos_mask] = self.embed_input_ids(eos_codes).expand(int(eos_mask.sum()), -1)
return output
def preprocess(
self,
input_ids: torch.Tensor,
input_embeds: torch.Tensor | None,
**info_dict: Any,
) -> tuple[torch.Tensor, torch.Tensor, dict[str, Any]]:
"""Build prefill embeddings and consume one completed frame per decode."""
del input_embeds
info = dict(info_dict)
span_len = int(input_ids.shape[0])
is_prefill = bool(info.get("_omni_is_prefill", span_len > 1))
if is_prefill:
full_ids = info.get("prompt_ids")
if not isinstance(full_ids, torch.Tensor):
raise RuntimeError("Breeze prefill is missing prompt_ids")
full_embeds = info.get("breeze_prompt_embeds")
if not isinstance(full_embeds, torch.Tensor):
full_embeds = self._build_prompt_embeddings(full_ids, info).detach().cpu().contiguous()
offset = int(info.get("breeze_prefill_offset", info.get("_omni_num_computed_tokens", 0)) or 0)
take = full_embeds[offset : offset + span_len].to(device=input_ids.device)
if take.shape[0] != span_len:
raise RuntimeError("Breeze prompt embedding slice is shorter than scheduled span")
return (
torch.full_like(input_ids, self.text_pad_token_id),
take,
{
"breeze_prompt_embeds": full_embeds,
"breeze_prefill_offset": offset + span_len,
"breeze_generated_frames": int(info.get("breeze_generated_frames", 0) or 0),
"breeze_max_new_frames": int(info.get("breeze_max_new_frames", -1)),
},
)
frame = info.get("breeze_current_frame")
if isinstance(frame, torch.Tensor) and frame.numel() == self.num_codebooks:
frame = frame.to(device=input_ids.device, dtype=torch.long).reshape(1, 1, -1)
embeds = self.embed_input_ids(frame).reshape(1, -1)
safe_id = frame[..., 0].reshape_as(input_ids).to(dtype=input_ids.dtype)
return safe_id, embeds, {}
has_breeze_state = any(
key in info
for key in (
"breeze_current_frame",
"breeze_audio_codes",
"breeze_generated_frames",
"breeze_prompt_embeds",
)
)
if has_breeze_state:
# A real decode must consume the full frame produced from the
# preceding backbone hidden state. A scalar fallback would silently
# fill codebooks 1..15 with EOS and corrupt AR state.
raise RuntimeError(
"Breeze decode is missing breeze_current_frame; refusing scalar fallback "
"because it would fill codebooks 1..15 with EOS"
)
# Runner-only dummy execution has no Breeze codec state. It must not
# invent a semantically invalid audio frame; a stable zero embedding is
# sufficient for shape/profiling warmup.
dummy_embeds = torch.zeros(
(*input_ids.shape, self.hidden_size),
device=input_ids.device,
dtype=self.embed_audio_tokens.weight.dtype,
)
return input_ids, dummy_embeds, {}
def postprocess(self, hidden_states: torch.Tensor, **_: Any) -> dict[str, Any]:
if hidden_states.numel() == 0:
return {}
return {"hidden_states": {"last": hidden_states[-1].detach()}}
# ------------------------------------------------------------------
# Main head, depth decoder and Omni output
# ------------------------------------------------------------------
def compute_logits(
self,
hidden_states: torch.Tensor | OmniOutput,
sampling_metadata: SamplingMetadata | None = None,
) -> torch.Tensor | None:
del sampling_metadata
if isinstance(hidden_states, OmniOutput):
hidden_states = hidden_states.text_hidden_states
if hidden_states is None:
return None
logits = self._main_head_logits(hidden_states)
# ``make_omni_output`` already selected codebook 0 and completed its
# frame. Return that same decision to the scheduler, including EOS.
# A finite zero survives the sampler's repetition/temperature passes
# without applying the repetition penalty twice or changing the frame.
# These logits describe deterministic codec selection, not model
# confidence; sampled-token logprobs are consequently zero.
states = self._batch_state or []
if states:
for state, row_start, row_end in _iter_request_rows(
states,
getattr(self, "_batch_state_spans", None),
int(logits.shape[0]),
):
selected_code0 = state.get("breeze_selected_code0")
if selected_code0 is not None:
row = logits[row_start:row_end]
row.fill_(float("-inf"))
row[:, int(selected_code0)] = 0.0
return logits
def _main_head_logits(self, hidden_states: torch.Tensor) -> torch.Tensor:
logits = self.logits_processor(self.lm_head, hidden_states)
if logits is None:
raise RuntimeError("Breeze main head returned no logits")
if self._codec_disallowed_mask is None or self._codec_disallowed_mask.device != logits.device:
mask = torch.zeros(logits.shape[-1], dtype=torch.bool, device=logits.device)
if self.codebook_size < self.codebook_vocab_size:
mask[self.codebook_size : self.codebook_vocab_size] = True
self._codec_disallowed_mask = mask
return logits.masked_fill(self._codec_disallowed_mask, float("-inf"))
@torch.no_grad()
def _generate_depth_codes(self, backbone_hidden: torch.Tensor, code0: int) -> torch.Tensor:
sequence = torch.tensor([[0, code0]], dtype=torch.long, device=backbone_hidden.device)
generated = [int(code0)]
for _ in range(self.num_codebooks - 1):
depth_out = self.depth_decoder(
input_ids=sequence,
backbone_last_hidden_state=backbone_hidden.reshape(1, -1),
use_cache=False,
)
logits = depth_out.logits[:, -1, :].clone()
if self.codebook_size < logits.shape[-1]:
logits[:, self.codebook_size :] = float("-inf")
next_code = int(torch.argmax(logits, dim=-1).item())
generated.append(next_code)
sequence = torch.cat([sequence, torch.tensor([[next_code]], device=sequence.device)], dim=1)
return torch.tensor(generated, dtype=torch.long, device=backbone_hidden.device).unsqueeze(0)
@staticmethod
def _accumulate_codes(info: dict[str, Any], frame: torch.Tensor) -> torch.Tensor:
"""Append one ``(1, Q)`` frame to the request's cumulative codes."""
accumulated = info.get("breeze_audio_codes")
accumulated = (
torch.cat([accumulated.to(frame.device), frame], dim=0)
if isinstance(accumulated, torch.Tensor) and accumulated.numel()
else frame
)
info["breeze_audio_codes"] = accumulated
return accumulated
def make_omni_output(self, model_outputs: torch.Tensor | OmniOutput, **kwargs: Any) -> OmniOutput:
if isinstance(model_outputs, OmniOutput):
self._batch_state = None
self._batch_state_spans = None
return model_outputs
hidden = model_outputs
infos = kwargs.get("model_intermediate_buffer", [])
spans = kwargs.get("request_token_spans")
info_items = _normalize_request_infos(infos)
if info_items is None:
self._batch_state = None
self._batch_state_spans = None
return OmniOutput(text_hidden_states=hidden, multimodal_outputs={})
# The runner invokes compute_logits after this method, so retain the
# same request ordering and spans for per-request EOS masking.
self._batch_state = info_items
self._batch_state_spans = spans
per_request_codes: list[torch.Tensor] = []
# The runner passes the same per-request sampling metadata to this
# hook and compute_logits, including offline SamplingParams overrides.
# When no_penalties is true vLLM does not initialize this GPU buffer.
sampling_metadata = kwargs.get("sampling_metadata")
repetition_penalties = None
if sampling_metadata is not None and not sampling_metadata.no_penalties:
repetition_penalties = sampling_metadata.repetition_penalties.tolist()
for index, info in enumerate(info_items):
if spans is not None and index < len(spans):
row_start = max(0, int(spans[index][0]))
row_end = min(int(spans[index][1]), int(hidden.shape[0]))
elif len(info_items) == 1:
row_start, row_end = 0, int(hidden.shape[0])
else:
raise RuntimeError("Breeze make_omni_output requires request_token_spans for batched decode")
if row_end <= row_start:
per_request_codes.append(hidden.new_empty((0, self.num_codebooks), dtype=torch.long))
continue
row_hidden = hidden[row_end - 1]
if self._golden_dump_dir and bool(info.get("_omni_is_prefill", False)):
self._dump_golden_prefill(info, row_hidden)
main_logits = self._main_head_logits(row_hidden.reshape(1, -1)).float()
penalty = float(repetition_penalties[index]) if repetition_penalties is not None else 1.0
if not math.isfinite(penalty) or penalty <= 0:
raise ValueError("Breeze repetition_penalty must be finite and positive")
# Match upstream: penalize each previously generated codebook-0
# id once, excluding the text prompt and reference audio codes.
history = info.setdefault("breeze_generated_code0_ids", [])
if penalty != 1.0 and history:
history_ids = torch.tensor(history, device=main_logits.device, dtype=torch.long)
scores = main_logits[0, history_ids]
main_logits[0, history_ids] = torch.where(scores < 0, scores * penalty, scores / penalty)
code0 = int(torch.argmax(main_logits, dim=-1).item())
generated_frames = int(info.get("breeze_generated_frames", 0) or 0)
max_new_frames = int(info.get("breeze_max_new_frames", -1))
terminal = code0 == self.codebook_vocab_size or (max_new_frames > 0 and generated_frames >= max_new_frames)
info["breeze_selected_code0"] = self.codebook_vocab_size if terminal else code0
if terminal:
info["breeze_force_eos"] = True
existing = info.get("breeze_audio_codes")
per_request_codes.append(
existing
if isinstance(existing, torch.Tensor) and existing.numel() and not self._async_chunk
else hidden.new_empty((0, self.num_codebooks), dtype=torch.long)
)
continue
if code0 < 0 or code0 >= self.codebook_size:
raise RuntimeError(f"Breeze main head produced invalid codec id {code0}")
frame = self._generate_depth_codes(row_hidden, code0)
history.append(code0)
info["breeze_current_frame"] = frame[0].detach()
info["breeze_generated_frames"] = generated_frames + 1
info["breeze_force_eos"] = False
if self._async_chunk:
# Streaming consumers only ever read the per-frame tail. The
# cumulative buffer exists for full-payload transport and the
# opt-in golden dump; maintaining it here would also cost an
# O(T^2) tensor-cat per request.
if self._golden_dump_dir:
self._accumulate_codes(info, frame)
per_request_codes.append(frame)
else:
per_request_codes.append(self._accumulate_codes(info, frame))
if self._golden_dump_dir:
for info in info_items:
self._dump_golden_frames(info)
return OmniOutput(text_hidden_states=hidden, multimodal_outputs={"codes": {"audio": per_request_codes}})
def _dump_golden_frames(self, info: dict[str, Any]) -> None:
"""Write terminal codec frames for opt-in golden-parity diagnostics."""
codes = info.get("breeze_audio_codes")
if not info.get("breeze_force_eos", False) or not isinstance(codes, torch.Tensor) or codes.numel() == 0:
return
prompt_ids = info.get("prompt_ids")
if not isinstance(prompt_ids, torch.Tensor):
raise RuntimeError("Breeze golden dump is missing prompt_ids")
digest = hashlib.sha256(prompt_ids.detach().cpu().numpy().tobytes()).hexdigest()[:16]
output_dir = Path(self._golden_dump_dir) # type: ignore[arg-type]
output_dir.mkdir(parents=True, exist_ok=True)
torch.save(
{
"template": info.get("template"),
"prompt_ids": prompt_ids.cpu(),
"codes": codes.detach().cpu().to(torch.long).contiguous(),
"generated_frames": int(info.get("breeze_generated_frames", 0) or 0),
"finished": True,
},
output_dir / f"breeze_{digest}.pt",
)
def _dump_golden_prefill(self, info: dict[str, Any], hidden: torch.Tensor) -> None:
"""Write the terminal-prompt hidden row for opt-in parity diagnostics."""
prompt_ids = info.get("prompt_ids")
if not isinstance(prompt_ids, torch.Tensor):
raise RuntimeError("Breeze golden dump is missing prompt_ids")
digest = hashlib.sha256(prompt_ids.detach().cpu().numpy().tobytes()).hexdigest()[:16]
output_dir = Path(self._golden_dump_dir) # type: ignore[arg-type]
output_dir.mkdir(parents=True, exist_ok=True)
torch.save(
{
"template": info.get("template"),
"prompt_ids": prompt_ids.cpu(),
"hidden": hidden.detach().reshape(-1).cpu().to(torch.float32).contiguous(),
},
output_dir / f"prefill_{digest}.pt",
)
def on_requests_finished(self, finished_req_ids: Iterable[str]) -> None:
"""Drop cross-step references retained for the finished AR requests."""
del finished_req_ids
# Codec/frame state lives in request-local additional_information and is
# freed with the Request. Only the short logits-to-state mapping is
# model-owned and must not leak into the next micro-batch.
self._batch_state = None
self._batch_state_spans = None
# ------------------------------------------------------------------
# Weight loading
# ------------------------------------------------------------------
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
loaded: set[str] = set()
params = dict(self.named_parameters())
backbone_weights: list[tuple[str, torch.Tensor]] = []
text_weights: list[tuple[str, torch.Tensor]] = []
depth_weights: list[tuple[str, torch.Tensor]] = []
for name, tensor in weights:
# This table belongs to Breeze's custom audio embedding module,
# although its checkpoint key is nested under backbone_model.
# Handle it before forwarding the remaining backbone weights to
# Qwen3Model.
if name == "backbone_model.embed_tokens.embed_audio_tokens.weight":
target = "embed_audio_tokens.weight"
param = params.get(target)
if param is None:
raise ValueError(f"Missing Breeze parameter: {target}")
_check_weight_shape(name, tensor, tuple(param.shape))
default_weight_loader(param, tensor)
loaded.add(target)
continue
if name.startswith("backbone_model."):
# Qwen3Model.load_weights() is called on the nested module and
# therefore expects names relative to that module (the same
# convention used by MOSS-TTS), not the outer ``model.``
# namespace used by ``named_parameters()``.
backbone_weights.append((name[len("backbone_model.") :], tensor))
continue
if name.startswith("text_encoder."):
text_name = name[len("text_encoder.") :]
text_weights.append((text_name, tensor))
continue
if name.startswith("depth_decoder."):
depth_weights.append((name[len("depth_decoder.") :], tensor))
continue
if name in {"lm_head.weight", "embed_text_tokens.weight"} or name.startswith("text_encoder_proj."):
target = name
else:
continue
param = params.get(target)
if param is None:
continue
# ParallelLMHead stores a TP-local shard while checkpoint tensors
# are global; validate against the global Breeze head shape.
expected_shape = (
(self.codebook_vocab_size + 1, self.hidden_size) if name == "lm_head.weight" else tuple(param.shape)
)
_check_weight_shape(name, tensor, expected_shape)
loader = getattr(param, "weight_loader", default_weight_loader)
loader(param, tensor)
loaded.add(target)
if backbone_weights:
loaded.update("model." + key for key in self.model.load_weights(backbone_weights))
if text_weights:
loaded.update("text_encoder." + key for key in self.text_encoder.load_weights(text_weights))
if depth_weights:
loaded.update("depth_decoder." + key for key in self.depth_decoder.load_weights(depth_weights))
if bool(getattr(self.config, "tie_codebooks_embeddings", False)):
# The checkpoint serializes only one side of the tied table.
# ``named_parameters(remove_duplicate=True)`` exposes it as the
# talker-owned ``embed_audio_tokens.weight``, so report both
# aliases to the strict weight tracker.
if "depth_decoder.model.embed_tokens.weight" in loaded:
loaded.add("embed_audio_tokens.weight")
if "embed_audio_tokens.weight" in loaded:
loaded.add("depth_decoder.model.embed_tokens.weight")
return loaded
__all__ = ["BreezeTTS2TalkerForGeneration"]
@@ -1,350 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Breeze T5Gemma2 text encoder.
The Breeze checkpoint contains a T5Gemma2 encoder, not the older T5Gemma
implementation used by some diffusion models. This module keeps the encoder
local to Breeze so checkpoint names and attention semantics stay explicit,
while using vLLM tensor-parallel layers for the expensive projections.
"""
from __future__ import annotations
from collections.abc import Iterable
from typing import Any
import torch
import torch.nn.functional as F
from torch import nn
from vllm.distributed import get_tensor_model_parallel_world_size
from vllm.model_executor.layers.activation import get_act_fn
from vllm.model_executor.layers.linear import (
MergedColumnParallelLinear,
QKVParallelLinear,
RowParallelLinear,
)
from vllm.model_executor.layers.vocab_parallel_embedding import VocabParallelEmbedding
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
class _RMSNorm(nn.Module):
"""T5Gemma2 RMSNorm, whose checkpoint stores a zero-centered weight."""
def __init__(self, dim: int, eps: float) -> None:
super().__init__()
self.weight = nn.Parameter(torch.zeros(dim))
self.eps = float(eps)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
normalized = hidden_states.float() * torch.rsqrt(
hidden_states.float().pow(2).mean(dim=-1, keepdim=True) + self.eps
)
return (normalized * (1.0 + self.weight.float())).to(hidden_states.dtype)
class _RotaryEmbedding(nn.Module):
def __init__(self, config: Any) -> None:
super().__init__()
self.config = config
for layer_type in dict.fromkeys(config.layer_types):
params = dict(config.rope_parameters.get(layer_type, {}))
base = float(params.get("rope_theta", 10_000.0))
factor = float(params.get("factor", 1.0))
dim = int(config.head_dim)
inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))
if params.get("rope_type") == "linear":
inv_freq = inv_freq / factor
elif params.get("rope_type", "default") != "default":
raise ValueError(f"Unsupported T5Gemma2 rope type: {params.get('rope_type')!r}")
self.register_buffer(f"{layer_type}_inv_freq", inv_freq, persistent=False)
def forward(
self,
hidden_states: torch.Tensor,
position_ids: torch.Tensor,
layer_type: str,
) -> tuple[torch.Tensor, torch.Tensor]:
inv_freq = getattr(self, f"{layer_type}_inv_freq")
expanded = inv_freq[None, :, None].to(device=hidden_states.device)
positions = position_ids[:, None, :].float()
freqs = (expanded.float() @ positions).transpose(1, 2)
emb = torch.cat((freqs, freqs), dim=-1)
return emb.cos().to(hidden_states.dtype), emb.sin().to(hidden_states.dtype)
def _rotate_half(value: torch.Tensor) -> torch.Tensor:
first, second = value[..., : value.shape[-1] // 2], value[..., value.shape[-1] // 2 :]
return torch.cat((-second, first), dim=-1)
def _apply_rotary(
query: torch.Tensor,
key: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
cos = cos.unsqueeze(1)
sin = sin.unsqueeze(1)
return (query * cos) + (_rotate_half(query) * sin), (key * cos) + (_rotate_half(key) * sin)
def _repeat_kv(hidden_states: torch.Tensor, repeats: int) -> torch.Tensor:
if repeats == 1:
return hidden_states
batch, heads, seq_len, head_dim = hidden_states.shape
return (
hidden_states[:, :, None, :, :]
.expand(batch, heads, repeats, seq_len, head_dim)
.reshape(batch, heads * repeats, seq_len, head_dim)
)
def _attention_mask(
padding_mask: torch.Tensor | None,
*,
batch_size: int,
seq_len: int,
layer_type: str,
sliding_window: int | None,
dtype: torch.dtype,
device: torch.device,
) -> torch.Tensor | None:
if layer_type == "full_attention" and padding_mask is None:
return None
valid = (
torch.ones((batch_size, seq_len), device=device, dtype=torch.bool)
if padding_mask is None
else padding_mask.to(device=device, dtype=torch.bool)
)
allowed = valid[:, None, None, :].expand(batch_size, 1, seq_len, seq_len)
if layer_type == "sliding_attention":
if sliding_window is None:
raise ValueError("T5Gemma2 sliding_attention requires sliding_window")
q_idx = torch.arange(seq_len, device=device)[:, None]
kv_idx = torch.arange(seq_len, device=device)[None, :]
left = (int(sliding_window) + 1) // 2
right = int(sliding_window) // 2 + 1
distance = q_idx - kv_idx
local = ((distance >= 0) & (distance < left)) | ((distance < 0) & (-distance < right))
allowed = allowed & local[None, None, :, :]
mask = torch.zeros((batch_size, 1, seq_len, seq_len), device=device, dtype=dtype)
mask.masked_fill_(~allowed, torch.finfo(dtype).min)
return mask
class _SelfAttention(nn.Module):
def __init__(self, config: Any) -> None:
super().__init__()
tp_size = get_tensor_model_parallel_world_size()
if config.num_attention_heads % tp_size != 0:
raise ValueError("T5Gemma2 attention heads must be divisible by tensor parallel size")
self.layer_type = config.layer_type
self.head_dim = int(config.head_dim)
self.num_heads = int(config.num_attention_heads) // tp_size
self.num_kv_heads = max(1, int(config.num_key_value_heads) // tp_size)
self.num_key_value_groups = self.num_heads // self.num_kv_heads
self.scaling = float(config.query_pre_attn_scalar) ** -0.5
self.qkv_proj = QKVParallelLinear(
hidden_size=config.hidden_size,
head_size=self.head_dim,
total_num_heads=config.num_attention_heads,
total_num_kv_heads=config.num_key_value_heads,
bias=config.attention_bias,
)
self.o_proj = RowParallelLinear(
input_size=config.num_attention_heads * self.head_dim,
output_size=config.hidden_size,
bias=config.attention_bias,
input_is_parallel=True,
)
self.q_norm = _RMSNorm(self.head_dim, config.rms_norm_eps)
self.k_norm = _RMSNorm(self.head_dim, config.rms_norm_eps)
self.sliding_window = config.sliding_window if self.layer_type == "sliding_attention" else None
def forward(
self,
hidden_states: torch.Tensor,
position_embeddings: tuple[torch.Tensor, torch.Tensor],
attention_mask: torch.Tensor | None,
) -> torch.Tensor:
qkv, _ = self.qkv_proj(hidden_states)
q_size = self.num_heads * self.head_dim
kv_size = self.num_kv_heads * self.head_dim
query, key, value = qkv.split((q_size, kv_size, kv_size), dim=-1)
shape = (*hidden_states.shape[:-1], -1, self.head_dim)
query = self.q_norm(query.view(shape).transpose(1, 2))
key = self.k_norm(key.view(shape).transpose(1, 2))
value = value.view(shape).transpose(1, 2)
query, key = _apply_rotary(query, key, *position_embeddings)
key = _repeat_kv(key, self.num_key_value_groups)
value = _repeat_kv(value, self.num_key_value_groups)
# SDPA scales by head_dim**-0.5; adjust Q to T5Gemma2's scalar.
query = query * (self.scaling / (self.head_dim**-0.5))
output = F.scaled_dot_product_attention(
query,
key,
value,
attn_mask=attention_mask,
dropout_p=0.0,
)
output = output.transpose(1, 2).contiguous().reshape(*hidden_states.shape[:-1], -1)
output, _ = self.o_proj(output)
return output
class _MLP(nn.Module):
def __init__(self, config: Any) -> None:
super().__init__()
self.gate_up_proj = MergedColumnParallelLinear(
input_size=config.hidden_size,
output_sizes=[config.intermediate_size, config.intermediate_size],
bias=False,
)
self.down_proj = RowParallelLinear(
input_size=config.intermediate_size,
output_size=config.hidden_size,
bias=False,
input_is_parallel=True,
)
self.act_fn = get_act_fn(config.hidden_activation)
self.dropout = nn.Dropout(float(config.dropout_rate))
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
gate_up, _ = self.gate_up_proj(hidden_states)
gate, up = gate_up.chunk(2, dim=-1)
output, _ = self.down_proj(self.dropout(self.act_fn(gate) * up))
return output
class _EncoderLayer(nn.Module):
def __init__(self, config: Any, layer_idx: int) -> None:
super().__init__()
layer_config = type("LayerConfig", (), dict(vars(config)))()
layer_config.layer_type = config.layer_types[layer_idx]
self.attention_type = layer_config.layer_type
self.pre_self_attn_layernorm = _RMSNorm(config.hidden_size, config.rms_norm_eps)
self.self_attn = _SelfAttention(layer_config)
self.post_self_attn_layernorm = _RMSNorm(config.hidden_size, config.rms_norm_eps)
self.pre_feedforward_layernorm = _RMSNorm(config.hidden_size, config.rms_norm_eps)
self.mlp = _MLP(config)
self.post_feedforward_layernorm = _RMSNorm(config.hidden_size, config.rms_norm_eps)
self.dropout = nn.Dropout(float(config.dropout_rate))
def forward(
self,
hidden_states: torch.Tensor,
position_embeddings: tuple[torch.Tensor, torch.Tensor],
attention_mask: torch.Tensor | None,
) -> torch.Tensor:
residual = hidden_states
hidden_states = self.pre_self_attn_layernorm(hidden_states)
hidden_states = self.self_attn(hidden_states, position_embeddings, attention_mask)
hidden_states = residual + self.dropout(self.post_self_attn_layernorm(hidden_states))
residual = hidden_states
hidden_states = self.pre_feedforward_layernorm(hidden_states)
hidden_states = self.mlp(hidden_states)
hidden_states = residual + self.dropout(self.post_feedforward_layernorm(hidden_states))
return hidden_states
class BreezeTTS2TextEncoder(nn.Module):
"""Frozen T5Gemma2 encoder returning ``(B, S, hidden_size)`` states."""
def __init__(self, config: Any) -> None:
super().__init__()
self.config = config
self.embed_tokens = VocabParallelEmbedding(config.vocab_size, config.hidden_size)
self.embed_scale = float(config.hidden_size**0.5)
self.eoi_token_index = int(getattr(config, "eoi_token_index", 256000))
# T5Gemma2 stores this learned replacement vector under the input
# embedding module (``embed_tokens.eoi_embedding``), so keep the same
# parameter path for checkpoint loading and tied-weight inspection.
self.embed_tokens.eoi_embedding = nn.Parameter(torch.zeros(config.hidden_size))
self.layers = nn.ModuleList([_EncoderLayer(config, i) for i in range(config.num_hidden_layers)])
self.norm = _RMSNorm(config.hidden_size, config.rms_norm_eps)
self.rotary_emb = _RotaryEmbedding(config)
self.dropout = nn.Dropout(float(config.dropout_rate))
@property
def dtype(self) -> torch.dtype:
return next(self.parameters()).dtype
def forward(
self,
input_ids: torch.Tensor,
attention_mask: torch.Tensor | None = None,
) -> torch.Tensor:
if input_ids.ndim != 2:
raise ValueError(f"Breeze text encoder expects 2D input_ids, got {tuple(input_ids.shape)}")
if attention_mask is not None and attention_mask.shape != input_ids.shape:
raise ValueError(
"Breeze text encoder attention_mask must match input_ids: "
f"{tuple(attention_mask.shape)} != {tuple(input_ids.shape)}"
)
hidden_states = self.embed_tokens(input_ids) * self.embed_scale
eoi_mask = input_ids == self.eoi_token_index
if bool(eoi_mask.any()):
hidden_states = torch.where(
eoi_mask.unsqueeze(-1),
self.embed_tokens.eoi_embedding.to(dtype=hidden_states.dtype),
hidden_states,
)
hidden_states = self.dropout(hidden_states)
positions = torch.arange(input_ids.shape[1], device=input_ids.device, dtype=torch.long).unsqueeze(0)
position_embeddings = {
layer_type: self.rotary_emb(hidden_states, positions, layer_type)
for layer_type in set(self.config.layer_types)
}
masks = {
layer_type: _attention_mask(
attention_mask,
batch_size=input_ids.shape[0],
seq_len=input_ids.shape[1],
layer_type=layer_type,
sliding_window=self.config.sliding_window,
dtype=hidden_states.dtype,
device=input_ids.device,
)
for layer_type in set(self.config.layer_types)
}
for layer in self.layers:
hidden_states = layer(
hidden_states,
position_embeddings[layer.attention_type],
masks[layer.attention_type],
)
return self.dropout(self.norm(hidden_states))
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
stacked_params_mapping = (
("qkv_proj", "q_proj", "q"),
("qkv_proj", "k_proj", "k"),
("qkv_proj", "v_proj", "v"),
("gate_up_proj", "gate_proj", 0),
("gate_up_proj", "up_proj", 1),
)
params = dict(self.named_parameters())
loaded: set[str] = set()
for name, tensor in weights:
target = name
shard_id: str | int | None = None
for packed_name, source_name, source_id in stacked_params_mapping:
marker = f".{source_name}."
if marker in name:
target = name.replace(marker, f".{packed_name}.")
shard_id = source_id
break
parameter = params.get(target)
if parameter is None:
continue
loader = getattr(parameter, "weight_loader", default_weight_loader)
if shard_id is None:
loader(parameter, tensor)
else:
loader(parameter, tensor, shard_id)
loaded.add(target)
return loaded
__all__ = ["BreezeTTS2TextEncoder"]
@@ -1,21 +1,12 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Synchronous Breeze-TTS-2 pipeline: talker -> Mimi codec."""
from vllm_omni.config.stage_config import (
PipelineConfig,
StageExecutionType,
StagePipelineConfig,
)
_PROC = "vllm_omni.model_executor.stage_input_processors.breeze_tts_2"
from vllm_omni.config.stage_config import PipelineConfig, StageExecutionType, StagePipelineConfig
BREEZE_TTS_2_PIPELINE = PipelineConfig(
model_type="breeze",
default_deploy_config_name="breeze_tts_2.yaml",
model_arch="BreezeForConditionalGeneration",
hf_architectures=("BreezeForConditionalGeneration",),
default_deploy_config_name="breeze_tts_2.yaml",
stages=(
StagePipelineConfig(
stage_id=0,
@@ -24,34 +15,24 @@ BREEZE_TTS_2_PIPELINE = PipelineConfig(
input_sources=(),
owns_tokenizer=True,
engine_output_type="latent",
async_chunk_process_next_stage_input_func=f"{_PROC}.talker2codec_async_chunk",
custom_process_next_stage_input_func=f"{_PROC}.talker2codec_full_payload",
sampling_constraints={
"detokenize": False,
# Text EOS 1 is a valid codec token; only the backbone's
# explicit codec EOS below may terminate audio generation.
"ignore_eos": True,
# The talker has already selected the emitted frame's token;
# min_tokens must not mask that token in the sampler.
"min_tokens": 0,
"stop_token_ids": [2051],
},
scheduler_cls="vllm_omni.core.sched.omni_cfg_ar_scheduler.OmniCFGARScheduler",
prompt_expand_func="vllm_omni.model_executor.stage_input_processors.breeze_tts_2.expand_cfg_prompts",
async_chunk_process_next_stage_input_func=(
"vllm_omni.model_executor.stage_input_processors.breeze_tts_2.talker2code2wav_async_chunk"
),
sampling_constraints={"detokenize": False, "stop_token_ids": [2051], "ignore_eos": True, "min_tokens": 0},
),
StagePipelineConfig(
stage_id=1,
model_stage="breeze_tts_2_codec",
model_arch="BreezeCode2Wav",
execution_type=StageExecutionType.LLM_GENERATION,
input_sources=(0,),
final_output=True,
final_output_type="audio",
engine_output_type="audio",
model_arch="BreezeTTS2MimiCodec",
sync_process_input_func=f"{_PROC}.talker2codec",
sampling_constraints={"detokenize": True},
requires_full_payload_input=True,
retains_state_across_chunks=True,
sampling_constraints={"detokenize": True},
),
),
)
__all__ = ["BREEZE_TTS_2_PIPELINE"]
@@ -0,0 +1,95 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Breeze's instruction and reference-audio templates for both public APIs."""
import math
from typing import Any
import numpy as np
import torch
from transformers import PreTrainedTokenizerBase
from vllm.inputs import TokensPrompt, tokens_input
from vllm.multimodal.audio import AudioResampler
DEFAULT_INSTRUCTION = "Speak clearly and naturally."
SAMPLE_RATE = 24000
ENCODE_DOWNSAMPLE_RATE = 1920
CFG_UNCOND_SUFFIX = "__cfg_uncond"
def build_breeze_prompt(
tokenizer: PreTrainedTokenizerBase,
text: str,
instructions: str = DEFAULT_INSTRUCTION,
*,
speaker: str = "S0",
ref_audio: tuple[np.ndarray, int] | None = None,
ref_text: str | None = None,
guidance_scale: float = 1.0,
temperature: float = 0.9,
top_k: int = 50,
top_p: float = 1.0,
repetition_penalty: float = 1.1,
) -> TokensPrompt:
if not text.strip():
raise ValueError("Breeze input text cannot be empty")
if not math.isfinite(guidance_scale) or guidance_scale <= 0:
raise ValueError("Breeze guidance_scale must be finite and positive")
if top_k == -1:
top_k = 0
if (
not all(math.isfinite(value) for value in (temperature, top_p, repetition_penalty))
or temperature < 0
or top_k < 0
or not 0 < top_p <= 1
or repetition_penalty <= 0
):
raise ValueError("Invalid Breeze sampling parameters")
if (ref_audio is None) != (ref_text is None):
raise ValueError("Breeze voice cloning requires both ref_audio and ref_text")
speaker_tag = speaker if speaker.startswith("[") and speaker.endswith("]") else f"[{speaker}]"
target = tokenizer.encode(f"{speaker_tag}<ins_bos>{instructions}<ins_eos>{text}", add_special_tokens=True)
conditioning: dict[str, Any] = {"target_ids": target, "guidance_scale": guidance_scale, "role": "cond"}
prefix_length = 0
if ref_audio is not None:
if not ref_text or not ref_text.strip():
raise ValueError("Breeze reference transcript cannot be empty")
waveform, sample_rate = ref_audio
waveform = np.asarray(waveform, dtype=np.float32)
if waveform.ndim == 2:
# Public waveform input follows soundfile: (samples, channels).
waveform = waveform.mean(axis=1)
if waveform.ndim != 1 or not waveform.size or not np.isfinite(waveform).all() or sample_rate <= 0:
raise ValueError("Breeze reference audio must contain finite samples at a positive sample rate")
if sample_rate != SAMPLE_RATE:
# Match the released tokenizer's librosa/soxr_hq resampling,
# including ceil-length handling for non-integral rate ratios.
length = math.ceil(waveform.size * SAMPLE_RATE / sample_rate)
waveform = AudioResampler(target_sr=SAMPLE_RATE, method="soxr").resample(waveform, orig_sr=sample_rate)
waveform = np.pad(waveform, (0, max(0, length - waveform.size)))[:length]
reference_ids = tokenizer.encode(f"{speaker_tag}{ref_text}", add_special_tokens=True)
frames = math.ceil(waveform.size / ENCODE_DOWNSAMPLE_RATE)
conditioning.update(
reference_ids=reference_ids,
reference_frames=frames,
)
prefix_length = len(reference_ids) + frames + 1
if guidance_scale != 1.0:
conditioning["negative_ids"] = tokenizer.encode(f"{speaker_tag}{text}", add_special_tokens=True)
prompt = tokens_input(prompt_token_ids=[0] * (prefix_length + len(target)))
prompt["additional_information"] = {
"breeze_prompt": conditioning,
"breeze_sampling": {
"temperature": temperature,
"top_k": top_k,
"top_p": top_p,
"repetition_penalty": repetition_penalty,
},
}
if ref_audio is not None:
# Tensor payloads belong at the transport root (or in a declared
# tensor namespace), rather than inside JSON-valued model metadata.
prompt["additional_information"]["reference_waveform"] = torch.from_numpy(np.ascontiguousarray(waveform))
if guidance_scale != 1.0:
prompt["additional_information"]["cfg_group"] = {"role": "cond", "uncond_suffix": CFG_UNCOND_SUFFIX}
return prompt
@@ -1,336 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Breeze-TTS-2 request-to-prompt conversion.
This module is intentionally limited to request preparation. It creates the
real prompt token ids needed by the scheduler and stores the text/audio masks
and reference codes in ``additional_information``. The stage-0 model later
turns those values into embeddings; this keeps tokenization independent from
the Qwen3/text-encoder implementation and avoids changing sequence length in
the model ``preprocess`` hook.
"""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any
import numpy as np
import torch
from transformers import AutoTokenizer
from vllm_omni.inputs.data import OmniTokensPrompt
from .audio_tokenizer import BreezeReferenceAudioTokenizer
AUDIO_TAG = "<|AUDIO|>"
AUDIO_EOS = "<|audio_eos|>"
INSTRUCTION_BOS = "<ins_bos>"
INSTRUCTION_EOS = "<ins_eos>"
@dataclass(frozen=True)
class _TextSegment:
text: str
@dataclass(frozen=True)
class _AudioSegment:
source: Any
sample_rate: int | None
class BreezeTTS2PromptBuilder:
"""Build one Breeze prompt without depending on model modules.
``tokenizer`` is a normal HuggingFace tokenizer. ``reference_audio_encoder``
is injected so the builder can use the serving worker's cached audio
encoder and never reload it per request. The returned ``OmniTokensPrompt``
contains scheduler token ids and compact CPU metadata for stage-0
``preprocess``.
"""
def __init__(
self,
tokenizer: Any,
config: Any,
reference_audio_encoder: BreezeReferenceAudioTokenizer | None = None,
) -> None:
self.tokenizer = tokenizer
self.audio_encoder = reference_audio_encoder
self.audio_token_id = int(config.audio_token_id)
self.audio_eos_token_id = int(config.audio_eos_token_id)
# The Breeze tokenizer vocabulary is larger than the Qwen3 backbone
# vocabulary. ``prompt_token_ids`` are scheduler bookkeeping only, so
# keep them in the backbone vocabulary and retain the real Breeze ids
# in ``additional_information['prompt_ids']`` for the talker.
self.scheduler_token_id = int(config.pad_token_id)
scheduler_vocab_size = int(config.backbone_config.vocab_size)
if self.scheduler_token_id >= scheduler_vocab_size:
raise ValueError(
"prompt_pad_token_id must be smaller than the backbone vocabulary: "
f"{self.scheduler_token_id} >= {scheduler_vocab_size}"
)
self.num_codebooks = int(config.num_codebooks)
codec_config = getattr(config, "codec_config", None)
if isinstance(codec_config, Mapping):
self.codebook_size = int(codec_config.get("codebook_size", 2048))
elif codec_config is not None:
self.codebook_size = int(getattr(codec_config, "codebook_size", 2048))
else:
# ``codec_config`` is optional in BreezeTTS2Config; a minimal
# checkpoint may omit it. Breeze's codec uses 2048-entry
# codebooks, the same fallback the talker and codec stages apply.
self.codebook_size = 2048
if self.num_codebooks <= 0 or self.codebook_size <= 0:
raise ValueError("num_codebooks and codec codebook_size must be positive")
self._validate_audio_special_tokens()
@classmethod
def from_pretrained(
cls,
model_path: str,
config: Any,
*,
reference_audio_encoder: BreezeReferenceAudioTokenizer | None = None,
tokenizer_kwargs: Mapping[str, Any] | None = None,
) -> BreezeTTS2PromptBuilder:
"""Load the text tokenizer once and construct a reusable builder."""
kwargs = dict(tokenizer_kwargs or {})
kwargs.setdefault("use_fast", True)
kwargs.setdefault("trust_remote_code", True)
tokenizer = AutoTokenizer.from_pretrained(model_path, **kwargs)
return cls(tokenizer, config, reference_audio_encoder)
def build(
self,
request: Mapping[str, Any],
*,
template: str | None = None,
) -> OmniTokensPrompt:
"""Convert one request to a vLLM-Omni token prompt.
Supported templates are ``tts_plain``, ``tts_instruction``,
``ref_clone_tata`` and ``ref_edit_tata``. When omitted, the template
is inferred from the presence of instruction/reference fields.
"""
template_name = template or request.get("template")
if not isinstance(template_name, str) or not template_name:
template_name = _infer_template(request)
segments = self._build_segments(template_name, request)
# Breeze's reference implementation first adds tokenizer special
# tokens to every text segment, decodes those segments back to text,
# inserts the audio placeholders, and tokenizes the complete string
# once. This matters at segment boundaries: tokenizing each raw text
# fragment independently can produce different boundary tokens.
rendered_segments: list[tuple[str, bool, int]] = []
reference_codes: list[torch.Tensor] = []
for segment in segments:
if isinstance(segment, _TextSegment):
ids = self._encode_one_text(segment.text, add_special_tokens=True)
if not ids:
raise ValueError("Breeze text tokenizer returned an empty segment")
rendered = str(self.tokenizer.decode(ids, skip_special_tokens=False))
rendered_segments.append((rendered, True, 0))
continue
codes = self._resolve_reference_codes(segment)
reference_codes.append(codes)
audio_text = AUDIO_TAG * int(codes.shape[0]) + AUDIO_EOS
rendered_segments.append((audio_text, False, int(codes.shape[0]) + 1))
prompt_ids: list[int] = []
text_mask: list[bool] = []
text_lengths: list[int] = []
rendered_prompt = "".join(item[0] for item in rendered_segments)
prompt_ids = self._encode_one_text(rendered_prompt, add_special_tokens=False)
offset = 0
for rendered, is_text, expected_len in rendered_segments:
segment_ids = self._encode_one_text(rendered, add_special_tokens=False)
actual_len = len(segment_ids)
if not is_text and actual_len != expected_len:
raise ValueError(
"Breeze audio placeholder token length changed after rendering: "
f"expected {expected_len}, got {actual_len}"
)
if not is_text and segment_ids != [
*([self.audio_token_id] * (expected_len - 1)),
self.audio_eos_token_id,
]:
raise ValueError("Breeze tokenizer did not preserve audio placeholder ids")
text_mask.extend([is_text] * actual_len)
if is_text:
text_lengths.append(actual_len)
offset += actual_len
if offset != len(prompt_ids) or len(text_mask) != len(prompt_ids):
raise RuntimeError("Breeze rendered prompt length accounting error")
if not prompt_ids:
raise ValueError("Breeze prompt is empty")
input_values = torch.cat(reference_codes, dim=0) if reference_codes else None
if input_values is not None and int(input_values.shape[0]) != prompt_ids.count(self.audio_token_id):
raise RuntimeError("reference code count does not match audio placeholder count")
additional_information: dict[str, Any] = {
# The model runner may invoke preprocess on a chunk of a long
# prompt. Keep one compact CPU copy so the talker can reconstruct
# the full embedding buffer before slicing that chunk.
"prompt_ids": torch.tensor(prompt_ids, dtype=torch.long),
"attention_mask": torch.ones(len(prompt_ids), dtype=torch.bool),
"text_ids_mask": torch.tensor(text_mask, dtype=torch.bool),
"text_ids_len": torch.tensor(text_lengths, dtype=torch.long),
"template": template_name,
}
if input_values is not None:
# ``input_values`` is the name used by the upstream Breeze model.
# Keep the tensor unbatched here: Omni transports one request at a
# time and the talker adds the batch dimension after collation.
additional_information["input_values"] = input_values
additional_information["ref_code_len"] = int(input_values.shape[0])
if self.scheduler_token_id < 0:
raise ValueError("prompt_pad_token_id must be non-negative")
return OmniTokensPrompt(
# Never expose Breeze's high text/audio ids to the Qwen3 scheduler
# or its token-range validation. The real ids remain available in
# additional_information and are consumed by ``preprocess``.
prompt_token_ids=[self.scheduler_token_id] * len(prompt_ids),
additional_information=additional_information,
)
def _build_segments(self, template: str, request: Mapping[str, Any]) -> list[_TextSegment | _AudioSegment]:
speaker = _speaker_prefix(request.get("speaker", "S0"))
text = _required_text(request, "text")
instruction = request.get("instruction")
ref_text = request.get("ref_text")
if template == "tts_plain":
return [_TextSegment(f"{speaker}{text}")]
if template == "tts_instruction":
if not isinstance(instruction, str) or not instruction:
raise ValueError("tts_instruction requires a non-empty instruction")
return [_TextSegment(f"{speaker}{INSTRUCTION_BOS}{instruction}{INSTRUCTION_EOS}{text}")]
if template in ("ref_clone_tata", "ref_edit_tata"):
if not isinstance(ref_text, str) or not ref_text:
raise ValueError(f"{template} requires a non-empty ref_text")
audio = self._audio_segment(request)
if template == "ref_clone_tata":
return [_TextSegment(f"{speaker}{ref_text}"), audio, _TextSegment(f"{speaker}{text}")]
if not isinstance(instruction, str) or not instruction:
raise ValueError("ref_edit_tata requires a non-empty instruction")
return [
_TextSegment(f"{speaker}{ref_text}"),
audio,
_TextSegment(f"{speaker}{INSTRUCTION_BOS}{instruction}{INSTRUCTION_EOS}{text}"),
]
raise ValueError(
f"unknown Breeze template {template!r}; expected tts_plain, tts_instruction, "
"ref_clone_tata or ref_edit_tata"
)
def _audio_segment(self, request: Mapping[str, Any]) -> _AudioSegment:
source = request.get("ref_audio")
if source is None:
source = request.get("ref_audio_codes")
if source is None:
raw_codes = request.get("codes")
source = raw_codes.get("ref") if isinstance(raw_codes, Mapping) else None
if source is None:
raise ValueError("reference template requires ref_audio or ref_audio_codes")
sample_rate = request.get("ref_audio_sample_rate")
return _AudioSegment(source, int(sample_rate) if sample_rate is not None else None)
def _resolve_reference_codes(self, segment: _AudioSegment) -> torch.Tensor:
source = segment.source
if _looks_like_codes(source, self.num_codebooks):
codes = torch.as_tensor(source)
return _normalize_codes(codes, self.num_codebooks, self.codebook_size)
if self.audio_encoder is None:
raise RuntimeError("reference audio was provided but no reference_audio_encoder is configured")
codes = self.audio_encoder.encode(source, segment.sample_rate)
return _normalize_codes(codes, self.num_codebooks, self.codebook_size)
def _encode_one_text(self, text: str, *, add_special_tokens: bool) -> list[int]:
encoded = self.tokenizer(text, add_special_tokens=add_special_tokens, padding=False)
ids = encoded["input_ids"] if isinstance(encoded, Mapping) else encoded.input_ids
if isinstance(ids, torch.Tensor):
ids = ids.tolist()
return _as_int_list(ids)
def _validate_audio_special_tokens(self) -> None:
convert = self.tokenizer.convert_tokens_to_ids
for token, expected in ((AUDIO_TAG, self.audio_token_id), (AUDIO_EOS, self.audio_eos_token_id)):
actual = convert(token)
unk = getattr(self.tokenizer, "unk_token_id", None)
if actual is None or (unk is not None and actual == unk) or int(actual) != expected:
raise ValueError(f"tokenizer id for {token!r} is {actual}, expected config value {expected}")
def _infer_template(request: Mapping[str, Any]) -> str:
has_ref = (
request.get("ref_audio") is not None
or request.get("ref_audio_codes") is not None
or bool(request.get("ref_text"))
)
has_instruction = bool(request.get("instruction"))
if has_ref and has_instruction:
return "ref_edit_tata"
if has_ref:
return "ref_clone_tata"
return "tts_instruction" if has_instruction else "tts_plain"
def _required_text(request: Mapping[str, Any], key: str) -> str:
value = request.get(key)
if not isinstance(value, str) or not value.strip():
raise ValueError(f"Breeze request requires non-empty {key}")
return value
def _speaker_prefix(value: Any) -> str:
if value in (None, ""):
return ""
value = str(value)
return value if value.startswith("[") and value.endswith("]") else f"[{value}]"
def _as_int_list(value: Any) -> list[int]:
if isinstance(value, torch.Tensor):
value = value.tolist()
elif isinstance(value, np.ndarray):
value = value.tolist()
if not isinstance(value, list):
raise ValueError("tokenizer input_ids must be a list")
if value and isinstance(value[0], (list, tuple, np.ndarray, torch.Tensor)):
if len(value) != 1:
raise ValueError("tokenizer input_ids contains multiple sequences")
return _as_int_list(value[0])
return [int(item) for item in value]
def _looks_like_codes(value: Any, num_codebooks: int) -> bool:
if isinstance(value, (torch.Tensor, np.ndarray)):
return value.ndim == 2 and value.shape[-1] == num_codebooks
return False
def _normalize_codes(codes: Any, num_codebooks: int, codebook_size: int) -> torch.Tensor:
codes = torch.as_tensor(codes)
if codes.ndim != 2:
raise ValueError(f"reference audio codes must be 2D, got {tuple(codes.shape)}")
if codes.shape[0] == 0:
raise ValueError("reference audio codes must contain at least one frame")
if codes.shape[-1] != num_codebooks:
raise ValueError(f"expected {num_codebooks} codebooks, got {tuple(codes.shape)}")
codes = codes.to(device="cpu", dtype=torch.long).contiguous()
if int(codes.min()) < 0 or int(codes.max()) >= codebook_size:
raise ValueError(f"reference code outside [0, {codebook_size})")
return codes.to(dtype=torch.int16)
__all__ = ["BreezeTTS2PromptBuilder"]
@@ -0,0 +1,165 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""The bundled Qwen3 codec encoder used by Breeze voice cloning."""
import torch
from torch import nn
from transformers.models.mimi.modeling_mimi import MimiConv1d
from vllm.config import VllmConfig
from vllm.model_executor.model_loader.default_loader import DefaultModelLoader
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
from vllm_omni.model_executor.models.qwen3_tts.tokenizer_12hz.configuration_qwen3_tts_tokenizer_v2 import (
Qwen3TTSTokenizerV2Config,
)
from vllm_omni.model_executor.models.qwen3_tts.tokenizer_12hz.modeling_qwen3_tts_tokenizer_v2 import (
Qwen3TTSTokenizerV2Encoder,
)
class BreezeReferenceEncoder(nn.Module):
def __init__(self, vllm_config: VllmConfig) -> None:
super().__init__()
config = Qwen3TTSTokenizerV2Config.from_pretrained(
vllm_config.model_config.model,
subfolder="audio_tokenizer",
revision=vllm_config.model_config.revision,
)
if config.input_sample_rate != 24000 or config.encode_downsample_rate != 1920:
raise ValueError("Breeze requires the bundled 24 kHz, 12.5 Hz Qwen3 codec")
config.encoder_config._attn_implementation = "sdpa"
# The released Breeze runtime encodes references in FP32. Quantizer
# boundaries affect speaker conditioning, so preserve that precision.
self.encoder = Qwen3TTSTokenizerV2Encoder._from_config(config.encoder_config, dtype=torch.float32).eval()
self._convolutions: list[BreezeReferenceConv] = []
hop = 1
for name, module in list(self.encoder.named_modules()):
if isinstance(module, MimiConv1d):
hop *= module.conv.stride[0]
convolution = BreezeReferenceConv(module, hop)
self._convolutions.append(convolution)
parent, _, attribute = name.rpartition(".")
self.encoder.get_submodule(parent).add_module(attribute, convolution)
if hop != config.encode_downsample_rate:
raise ValueError("Breeze reference convolution strides do not match the codec frame rate")
self.num_quantizers = config.encoder_valid_num_quantizers
# Reference convolutions have large temporary workspaces. Compile
# without a graph's permanently reserved workspace for every length.
self._compiled_encode = torch.compile(
self._encode_impl,
fullgraph=True,
dynamic=True,
options={"triton.cudagraphs": False, "epilogue_fusion": False},
)
def forward(self, waveform: torch.Tensor, frames: int) -> torch.Tensor:
return self.encode_batch([waveform], [frames])[0]
def encode_batch(self, waveforms: list[torch.Tensor], frame_counts: list[int]) -> list[torch.Tensor]:
device = next(self.encoder.parameters()).device
waveform = nn.utils.rnn.pad_sequence(waveforms, batch_first=True)
waveform = nn.functional.pad(waveform, (0, -waveform.shape[1] % 1920))
waveform_frames = waveform.reshape(len(waveforms), -1, 1920).to(device=device, dtype=torch.float32)
lengths = torch.tensor([waveform.numel() for waveform in waveforms], device=device, dtype=torch.long)
with torch.backends.cudnn.flags(allow_tf32=False):
encoded = self._compiled_encode(waveform_frames, lengths)
return [
encoded[row, :, :frames].transpose(0, 1).clone(memory_format=torch.contiguous_format)
for row, frames in enumerate(frame_counts)
]
def warmup(self, max_batch_size: int) -> None:
device = next(self.encoder.parameters()).device
with torch.backends.cudnn.flags(allow_tf32=False):
for batch in range(1, max_batch_size + 1):
waveform_frames = torch.zeros((batch, 32, 1920), device=device, dtype=torch.float32)
lengths = torch.full((batch,), 32 * 1920, device=device, dtype=torch.long)
# Initialize the codec's normalized codebooks before tracing.
self._encode_impl(waveform_frames, lengths)
self._compiled_encode(waveform_frames, lengths)
torch.accelerator.empty_cache()
def _encode_impl(self, waveform_frames: torch.Tensor, sample_lengths: torch.Tensor) -> torch.Tensor:
# Expose the complete 1920-sample stride product to the compiler.
# Nested ceil/mod expressions for arbitrary waveform lengths otherwise
# cause expensive symbolic simplification during kernel tiling. Every
# convolution below masks the true per-recording boundary, so aligning
# the storage to full frames preserves partial final-frame semantics.
torch._dynamo.mark_static(waveform_frames, 2)
waveform = waveform_frames.flatten(1)
# The released Breeze reference encoder uses full causal attention
# during offline encoding. Recent Transformers Mimi defaults to a
# sliding mask, which changes references longer than ten seconds.
# Supply the actual reference mask rather than changing the checkpoint
# configuration or relying on a Transformers-version default.
for convolution in self._convolutions:
convolution.sample_lengths = sample_lengths
hidden = self.encoder.encoder(waveform.unsqueeze(1)).transpose(1, 2)
length = hidden.shape[1]
causal_mask = torch.ones((length, length), device=hidden.device, dtype=torch.bool).tril()[None, None]
hidden = self.encoder.encoder_transformer(
hidden, attention_mask=causal_mask, use_cache=False, return_dict=True
).last_hidden_state
hidden = self.encoder.downsample(hidden.transpose(1, 2))
return self.encoder.quantizer.encode(hidden, self.num_quantizers).transpose(0, 1)
def load_weights(self, vllm_config: VllmConfig) -> set[str]:
loader = DefaultModelLoader(vllm_config.load_config)
source = DefaultModelLoader.Source(
model_or_path=vllm_config.model_config.model,
revision=vllm_config.model_config.revision,
subfolder="audio_tokenizer",
)
parameters = dict(self.named_parameters())
buffers = dict(self.named_buffers())
loaded: set[str] = set()
for name, value in loader._get_weights_iterator(source):
if name.startswith("decoder."):
continue
if name in parameters:
default_weight_loader(parameters[name], value)
elif name in buffers:
buffers[name].copy_(value)
else:
raise ValueError(f"Unexpected reference codec weight: {name}")
loaded.add(name)
required = set(parameters) | {name for name in buffers if ".quantizer." in name}
missing = required - loaded
if missing:
raise ValueError(f"Uninitialized reference codec weights or codebooks: {sorted(missing)}")
return loaded
class BreezeReferenceConv(nn.Module):
"""Offline Mimi convolution with padding derived from static shape metadata."""
def __init__(self, source: MimiConv1d, output_hop: int) -> None:
super().__init__()
if not source.causal or source.pad_mode not in ("constant", "replicate"):
raise ValueError("Breeze reference encoding requires causal zero/replicate-padded convolutions")
self.conv = source.conv
self.pad_mode = source.pad_mode
self.stride = self.conv.stride[0]
self.padding = (self.conv.kernel_size[0] - 1) * self.conv.dilation[0] + 1 - self.stride
self.output_hop = output_hop
self.sample_lengths: torch.Tensor | None = None
def forward(self, hidden: torch.Tensor) -> torch.Tensor:
# HF stores these integers in GPU tensors, introducing a device sync
# when F.pad converts them back to integers. Shapes determine padding.
if self.sample_lengths is None:
raise RuntimeError("Reference convolution requires the batch's input lengths")
if self.pad_mode == "replicate":
# Mimi's final downsampler repeats the actual last encoder state
# for a partial frame. Each row has its own boundary in a bucket.
input_hop = self.output_hop // self.stride
lengths = (self.sample_lengths + input_hop - 1) // input_hop
indices = torch.arange(hidden.shape[-1], device=hidden.device)[None, :]
indices = torch.minimum(indices, lengths[:, None] - 1)
hidden = hidden.gather(2, indices[:, None, :].expand(-1, hidden.shape[1], -1))
right = -hidden.shape[-1] % self.stride
hidden = nn.functional.pad(hidden, (self.padding, right), mode=self.pad_mode)
hidden = self.conv(hidden)
lengths = (self.sample_lengths + self.output_hop - 1) // self.output_hop
invalid = torch.arange(hidden.shape[-1], device=hidden.device)[None, :] >= lengths[:, None]
return hidden.masked_fill(invalid[:, None, :], 0)
@@ -0,0 +1,122 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Padded T5Gemma2 prefill graphs with explicit bidirectional masks."""
import torch
import torch.nn.functional as F
from torch import nn
from transformers.models.t5gemma2.modeling_t5gemma2 import (
T5Gemma2EncoderLayer,
T5Gemma2TextEncoder,
sliding_window_mask_function,
)
from vllm.platforms import current_platform
from vllm_omni.platforms import current_omni_platform
class BreezeTextEncoderCompiled:
"""Compiled prefill layers without retaining large activation graph pools."""
def __init__(self, encoder: T5Gemma2TextEncoder, projection: nn.Linear) -> None:
self.encoder = encoder
self.projection = projection
self._layer = torch.compile(
T5Gemma2EncoderLayer.forward,
fullgraph=True,
dynamic=True,
options={"triton.cudagraphs": False, "epilogue_fusion": False},
)
def run_batch(self, prompts: list[torch.Tensor]) -> list[torch.Tensor]:
encoder = self.encoder
device = self.projection.weight.device
dtype = self.projection.weight.dtype
lengths = [prompt.numel() for prompt in prompts]
ids = nn.utils.rnn.pad_sequence(prompts, batch_first=True).to(device)
positions = torch.arange(ids.shape[1], device=device)[None]
invalid = positions >= torch.tensor(lengths, device=device)[:, None]
mask = torch.zeros((len(prompts), 1, ids.shape[1], ids.shape[1]), device=device, dtype=dtype)
mask.masked_fill_(invalid[:, None, None, :], torch.finfo(dtype).min)
mask_function = sliding_window_mask_function(encoder.config.sliding_window, is_causal=False)
allowed = mask_function(0, 0, positions[:, :, None], positions[:, None, :])
local_mask = mask.masked_fill(~allowed[:, None], torch.finfo(dtype).min)
masks = {"full_attention": mask, "sliding_attention": local_mask}
embedding = encoder.embed_tokens
hidden = F.embedding(ids, embedding.weight) * embedding.embed_scale.to(dtype)
hidden = torch.where(
(ids == embedding.eoi_token_index).unsqueeze(-1), embedding.eoi_embedding.to(dtype), hidden
)
position_embeddings = {
kind: encoder.rotary_emb(hidden, positions, kind) for kind in set(encoder.config.layer_types)
}
for layer in encoder.layers:
kind = layer.attention_type
hidden = self._layer(layer, hidden, position_embeddings[kind], masks[kind], positions)
output = self.projection(encoder.norm(hidden))
return [output[row, :length].clone() for row, length in enumerate(lengths)]
def warmup(self, max_batch_size: int) -> None:
# Dynamo specializes size-one dimensions; a second batch exercises
# the general batch dimension used by paired and reference prompts.
for batch in sorted({1, max_batch_size}):
self.run_batch([torch.zeros(256, dtype=torch.long) for _ in range(batch)])
torch.accelerator.empty_cache()
class BreezeTextEncoderGraph:
def __init__(self, encoder: T5Gemma2TextEncoder, projection: nn.Linear, size: int, batch_size: int = 1) -> None:
device = projection.weight.device
dtype = projection.weight.dtype
self.ids = torch.zeros((batch_size, size), device=device, dtype=torch.long)
self.positions = torch.arange(size, device=device).unsqueeze(0)
self.full_mask = torch.zeros((batch_size, 1, size, size), device=device, dtype=dtype)
mask_function = sliding_window_mask_function(encoder.config.sliding_window, is_causal=False)
allowed = mask_function(0, 0, self.positions[:, :, None], self.positions[:, None, :])
self.local_mask_base = torch.zeros_like(self.full_mask).masked_fill_(~allowed[:, None], torch.finfo(dtype).min)
self.local_mask = self.local_mask_base.clone()
self.masks = {"full_attention": self.full_mask, "sliding_attention": self.local_mask}
for _ in range(3):
self._encode(encoder, projection)
current_omni_platform.synchronize()
self.graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(
self.graph, pool=current_platform.get_global_graph_pool(), capture_error_mode="thread_local"
):
self.output = self._encode(encoder, projection)
def _encode(self, encoder: T5Gemma2TextEncoder, projection: nn.Linear) -> torch.Tensor:
embedding = encoder.embed_tokens
hidden = F.embedding(self.ids, embedding.weight) * embedding.embed_scale.to(embedding.weight.dtype)
# HF's boolean-index assignment materializes a data-dependent index
# list. Express the same EOI substitution with a fixed-shape select.
hidden = torch.where(
(self.ids == embedding.eoi_token_index).unsqueeze(-1),
embedding.eoi_embedding.to(hidden.dtype),
hidden,
)
encoded = encoder(inputs_embeds=hidden, attention_mask=self.masks, position_ids=self.positions)
return projection(encoded.last_hidden_state)
def run(self, prompt: torch.Tensor) -> torch.Tensor:
return self.run_batch([prompt[0]])[0]
def run_batch(self, prompts: list[torch.Tensor]) -> list[torch.Tensor]:
if len(prompts) != self.ids.shape[0]:
raise ValueError("Breeze text batch differs from the captured graph size")
lengths = [prompt.numel() for prompt in prompts]
if not all(0 < length <= self.ids.shape[1] for length in lengths):
raise ValueError("Breeze text segment does not fit its graph bucket")
self.ids.zero_()
for row, (prompt, length) in enumerate(zip(prompts, lengths, strict=True)):
self.ids[row, :length].copy_(prompt)
invalid = self.positions >= torch.tensor(lengths, device=self.ids.device)[:, None]
invalid = invalid[:, None, None, :]
self.full_mask.zero_()
self.full_mask.masked_fill_(invalid, torch.finfo(self.full_mask.dtype).min)
self.local_mask.copy_(self.local_mask_base)
self.local_mask.masked_fill_(invalid, torch.finfo(self.local_mask.dtype).min)
self.graph.replay()
# Subsequent prefills and backbone/depth graphs reuse graph storage.
# The runner must receive independently owned prompt embeddings.
return [self.output[row, :length].clone() for row, length in enumerate(lengths)]
@@ -58,6 +58,8 @@ class Qwen3TTSCode2Wav(nn.Module):
via the SpeechTokenizer decoder directly (bypassing HF wrapper overhead)."""
input_modalities = "audio"
tokenizer_subfolder = "speech_tokenizer"
decoder_cudagraph_modes: tuple[str, ...] = ("icl", "xvec")
# Ask the model runner for the scheduler-side request IDs. Stateful
# decoder caches must use the same IDs delivered by on_requests_finished;
@@ -105,7 +107,7 @@ class Qwen3TTSCode2Wav(nn.Module):
# load_weights().
tok_config = Qwen3TTSTokenizerV2Config.from_pretrained(
self.model_path,
subfolder="speech_tokenizer",
subfolder=self.tokenizer_subfolder,
)
dec_config = tok_config.decoder_config
self.decoder = Qwen3TTSTokenizerV2Decoder._from_config(dec_config)
@@ -194,6 +196,7 @@ class Qwen3TTSCode2Wav(nn.Module):
)
self.decoder.enable_cudagraph(
capture_modes=self.decoder_cudagraph_modes,
capture_batch_sizes=decode_cudagraph_batch_sizes,
stateless_capture_sizes=decode_cudagraph_capture_sizes,
device=device,
@@ -566,7 +569,7 @@ class Qwen3TTSCode2Wav(nn.Module):
source = DefaultModelLoader.Source(
model_or_path=self.model_path,
revision=self.vllm_config.model_config.revision,
subfolder="speech_tokenizer",
subfolder=self.tokenizer_subfolder,
)
subfolder_weights = model_loader._get_weights_iterator(source)
loaded = AutoWeightsLoader(self).load_weights(
@@ -1,5 +1,6 @@
# Copyright 2026 The Alibaba Qwen team.
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""
CUDA Graph wrapper for Qwen3TTSTokenizerV2Decoder.
@@ -45,6 +46,7 @@ class CUDAGraphDecoderWrapper:
def __init__(
self,
decoder: torch.nn.Module,
capture_modes: tuple[str, ...] = ("icl", "xvec"),
capture_batch_sizes: list[int] | None = None,
stateless_capture_sizes: list[int] | None = None,
num_quantizers: int = 8,
@@ -57,6 +59,9 @@ class CUDAGraphDecoderWrapper:
decode_left_context: int = 25,
):
self.decoder = decoder
if not capture_modes or set(capture_modes) - {"icl", "xvec"}:
raise ValueError("capture_modes must contain icl and/or xvec")
self.capture_modes = capture_modes
self.capture_batch_sizes = sorted(set(capture_batch_sizes or [1]))
self.num_quantizers = num_quantizers
self.enabled = enabled
@@ -407,11 +412,13 @@ class CUDAGraphDecoderWrapper:
icl_capture_shapes = self._get_icl_capture_shapes()
for batch_size in self.capture_batch_sizes:
if "icl" in self.capture_modes:
try:
self._capture_icl_prefix(batch_size, device, dtype)
logger.info("Captured ICL prefix CUDA Graph for batch=%d", batch_size)
except Exception:
logger.warning("Failed to capture ICL prefix CUDA Graph for batch=%d", batch_size, exc_info=True)
if "xvec" in self.capture_modes:
try:
self._capture_xvec_prefix(batch_size, device, dtype)
logger.info("Captured xvec prefix CUDA Graph for batch=%d", batch_size)
@@ -425,7 +432,7 @@ class CUDAGraphDecoderWrapper:
self.icl_capture_sizes,
)
for batch_size, size in icl_capture_shapes:
for batch_size, size in icl_capture_shapes if "icl" in self.capture_modes else ():
try:
caches = self._make_dummy_icl_cache(batch_size, device, next(self.decoder.parameters()).dtype)
self._capture_combined_suffix("icl", batch_size, size, caches, device, dtype)
@@ -439,7 +446,7 @@ class CUDAGraphDecoderWrapper:
)
model_dtype = next(self.decoder.parameters()).dtype
for batch_size in self.capture_batch_sizes:
for batch_size in self.capture_batch_sizes if "xvec" in self.capture_modes else ():
for size, previous_frames in self._xvec_previous_frames_by_target.items():
try:
caches = self._make_dummy_xvec_cache(batch_size, device, model_dtype)
@@ -999,6 +999,7 @@ class Qwen3TTSTokenizerV2Decoder(Qwen3TTSTokenizerV2DecoderPreTrainedModel):
def enable_cudagraph(
self,
capture_modes: tuple[str, ...] = ("icl", "xvec"),
capture_batch_sizes: list[int] | None = None,
stateless_capture_sizes: list[int] | None = None,
device: torch.device | None = None,
@@ -1021,6 +1022,7 @@ class Qwen3TTSTokenizerV2Decoder(Qwen3TTSTokenizerV2DecoderPreTrainedModel):
self._cudagraph_wrapper = CUDAGraphDecoderWrapper(
decoder=self,
capture_modes=capture_modes,
capture_batch_sizes=capture_batch_sizes,
stateless_capture_sizes=stateless_capture_sizes,
num_quantizers=self.config.num_quantizers,
+2 -10
View File
@@ -9,6 +9,8 @@ from vllm.model_executor.models.registry import (
)
_OMNI_MODELS = {
"BreezeForConditionalGeneration": ("breeze_tts_2", "modeling_breeze", "BreezeForConditionalGeneration"),
"BreezeCode2Wav": ("breeze_tts_2", "code2wav", "BreezeCode2Wav"),
"Qwen2_5OmniForConditionalGeneration": (
"qwen2_5_omni",
"qwen2_5_omni",
@@ -476,16 +478,6 @@ _OMNI_MODELS = {
"acoustic",
"MiniMaxMusic3AcousticForConditionalGeneration",
),
"BreezeForConditionalGeneration": (
"breeze_tts_2",
"modeling_breeze_tts_2_talker",
"BreezeTTS2TalkerForGeneration",
),
"BreezeTTS2MimiCodec": (
"breeze_tts_2",
"modeling_breeze_tts_2_codec",
"BreezeTTS2MimiCodec",
),
}
@@ -1,244 +1,88 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Transfer raw Breeze RVQ frames to the stateful Qwen3 codec."""
"""Stage-0 to stage-1 input conversion for Breeze-TTS-2.
The prompt builder and the reference-audio tokenizer live under the Breeze
model directory. This module intentionally contains only the inter-stage
contract: flatten the generated ``(T, Q)`` codec matrix into the codebook-major
token sequence consumed by the stage-1 codec model.
"""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from typing import Any
from typing import TYPE_CHECKING, Any
import torch
from vllm.logger import init_logger
from vllm.v1.request import Request
from vllm_omni.data_entry_keys import CodesStruct, MetaStruct, OmniPayloadStruct
from vllm_omni.inputs.data import OmniTokensPrompt
from vllm_omni.data_entry_keys import CodesStruct, MetaStruct, OmniPayload, OmniPayloadStruct
from vllm_omni.model_executor.models.breeze_tts_2.prompt import CFG_UNCOND_SUFFIX
from vllm_omni.model_executor.stage_input_processors.bagel import ExpandedPrompt
logger = init_logger(__name__)
# ``BreezeTTS2TalkerForGeneration`` emits the complete sequence accumulated
# so far on every decode step. The generic full-payload accumulator normally
# concatenates rank-2 tensors, which would duplicate all earlier frames.
# Replace the latest complete sequence instead.
_FULL_PAYLOAD_REPLACE_KEYS: frozenset[str] = frozenset({"codes.audio"})
_DEFAULT_CODEC_CHUNK_FRAMES = 8
if TYPE_CHECKING:
from vllm_omni.distributed.omni_connectors.transfer_adapter.chunk_transfer_adapter import OmniChunkTransferAdapter
def _extract_audio_codes(output: Any) -> torch.Tensor | None:
"""Extract ``codes.audio`` from the supported Omni output shapes."""
# Depending on the engine path, source_outputs contains either the
# per-request output itself or an EngineCoreOutput with ``outputs[0]``.
if isinstance(output, Mapping):
multimodal = output.get("multimodal_output", output.get("multimodal_outputs"))
if multimodal is None and "codes" in output:
multimodal = output
if multimodal is None and "codes.audio" in output:
audio = output["codes.audio"]
if isinstance(audio, (list, tuple)):
if len(audio) != 1:
raise ValueError(f"expected one Breeze audio-code tensor, got {len(audio)}")
audio = audio[0]
audio = torch.as_tensor(audio)
if audio.ndim not in (1, 2):
raise ValueError(f"Breeze audio codes must be rank 1 or 2, got {tuple(audio.shape)}")
return audio.to(device="cpu", dtype=torch.long).contiguous()
else:
multimodal = getattr(output, "multimodal_output", None)
if multimodal is None:
multimodal = getattr(output, "multimodal_outputs", None)
if multimodal is None:
candidates = getattr(output, "outputs", None)
if isinstance(candidates, Sequence) and candidates:
return _extract_audio_codes(candidates[0])
if not isinstance(multimodal, Mapping):
return None
codes = multimodal.get("codes")
audio = codes.get("audio") if isinstance(codes, Mapping) else multimodal.get("codes.audio")
if audio is None:
return None
# ``OmniOutput`` stores one tensor per request under ``codes.audio``.
# ``process_engine_inputs`` invokes this function once per source output,
# so unwrap that single-item container before tensor conversion.
if isinstance(audio, (list, tuple)):
if len(audio) != 1:
raise ValueError(f"expected one Breeze audio-code tensor, got {len(audio)}")
audio = audio[0]
audio = torch.as_tensor(audio)
if audio.ndim == 3 and audio.shape[0] == 1:
audio = audio[0]
if audio.ndim not in (1, 2):
raise ValueError(
f"Breeze audio codes must have shape (T, Q) or codebook-major (Q*T,), got {tuple(audio.shape)}"
)
return audio.to(device="cpu", dtype=torch.long).contiguous()
def expand_cfg_prompts(prompt: dict[str, Any] | str, sampling_params: Any) -> list[ExpandedPrompt]:
if not isinstance(prompt, dict):
raise ValueError("Breeze requires build_breeze_prompt to encode its conditioning")
info = prompt["additional_information"]
conditioning = info["breeze_prompt"]
if conditioning["guidance_scale"] == 1.0:
return []
negative = conditioning["negative_ids"]
companion_conditioning = {**conditioning, "role": "uncond", "target_ids": negative}
companion_conditioning.pop("negative_ids")
length = len(prompt["prompt_token_ids"]) - len(conditioning["target_ids"]) + len(negative)
companion = {
"prompt_token_ids": [0] * length,
"additional_information": {
**info,
"breeze_prompt": companion_conditioning,
"cfg_group": {"role": "uncond", "uncond_suffix": CFG_UNCOND_SUFFIX},
},
}
return [ExpandedPrompt(prompt=companion, role="uncond", request_id_suffix=CFG_UNCOND_SUFFIX)]
def talker2codec(
source_outputs: Sequence[Any],
prompt: Any = None,
_requires_multimodal_data: bool = False,
) -> list[OmniTokensPrompt]:
"""Build synchronous stage-1 prompts from completed stage-0 outputs.
Breeze's Mimi decoder receives one flat codebook-major sequence. The
generated matrix is therefore transposed from ``(T, Q)`` to ``(Q, T)``
before flattening. No audio decoding, text tokenization, or streaming
state is handled here.
"""
del prompt, _requires_multimodal_data
results: list[OmniTokensPrompt] = []
for source_index, output in enumerate(source_outputs):
codes = _extract_audio_codes(output)
if codes is None or codes.numel() == 0:
logger.warning("Breeze stage output %d contains no audio codes", source_index)
results.append(OmniTokensPrompt(prompt_token_ids=[]))
continue
# Connector full-payload builders send a codebook-major flat tensor;
# direct in-process stage outputs carry frame-major ``(T, Q)``.
if codes.ndim == 1:
codebook_major_data = codes.contiguous()
codebook_major = codebook_major_data
else:
codebook_major_data = codes.transpose(0, 1).contiguous()
codebook_major = codebook_major_data.reshape(-1)
results.append(
OmniTokensPrompt(
prompt_token_ids=codebook_major.tolist(),
multi_modal_data={"codes": {"audio": codebook_major_data}},
)
)
return results
def talker2codec_full_payload(
transfer_manager: Any = None,
pooling_output: Mapping[str, Any] | None = None,
request: Any = None,
def talker2code2wav_async_chunk(
transfer_manager: "OmniChunkTransferAdapter",
multimodal_output: OmniPayload | None,
request: Request,
is_finished: bool = False,
) -> OmniPayloadStruct | None:
"""Build the final Stage-1 payload from accumulated talker codes.
``pooling_output`` is the AR runner's flattened per-request payload. Mimi
consumes codebook-major IDs, so the frame-major talker matrix is transposed
before flattening. A request that finishes without any codec frame still
gets an explicit empty payload with ``finished=True``: returning ``None``
would leave Stage 1 without a terminal marker, and the connector receiver
requeues marker-less empty chunks forever.
"""
del transfer_manager, request
audio = None
if isinstance(pooling_output, Mapping):
audio = pooling_output.get("codes.audio")
if audio is None:
nested = pooling_output.get("codes")
if isinstance(nested, Mapping):
audio = nested.get("audio")
codes = torch.empty(0, dtype=torch.long) if audio is None else torch.as_tensor(audio, dtype=torch.long)
if codes.numel() == 0:
if not is_finished:
return None
return OmniPayloadStruct(
codes=CodesStruct(audio=torch.empty(0, dtype=torch.long)),
meta=MetaStruct(
code_flat_numel=0,
finished=torch.tensor(True, dtype=torch.bool),
),
)
if codes.ndim == 2:
flat = codes.transpose(0, 1).contiguous().reshape(-1)
elif codes.ndim == 1:
flat = codes.contiguous()
else:
raise ValueError(f"Breeze audio codes must be rank 1 or 2, got {tuple(codes.shape)}")
return OmniPayloadStruct(
codes=CodesStruct(audio=flat),
meta=MetaStruct(
code_flat_numel=int(flat.numel()),
finished=torch.tensor(bool(is_finished), dtype=torch.bool),
),
)
def _extract_current_audio_frame(
multimodal_output: Mapping[str, Any] | None,
) -> torch.Tensor | None:
"""Extract the single ``(Q,)`` frame emitted by async Stage 0."""
if not isinstance(multimodal_output, Mapping):
return None
codes = multimodal_output.get("codes")
audio = codes.get("audio") if isinstance(codes, Mapping) else None
if audio is None:
return None
frame = torch.as_tensor(audio, dtype=torch.long).reshape(-1)
if frame.numel() == 0:
return None
return frame.cpu().contiguous()
def _codec_chunk_frames(transfer_manager: Any) -> int:
connector = getattr(transfer_manager, "connector", None)
# SharedMemoryConnector stores its ConnectorSpec.extra directly in
# ``connector.config``; there is no second ``extra`` wrapper at runtime.
config = getattr(connector, "config", {}) or {}
value = config.get("breeze_codec_chunk_frames")
try:
chunk_frames = int(value) if value is not None else _DEFAULT_CODEC_CHUNK_FRAMES
except (TypeError, ValueError) as exc:
raise ValueError(f"Invalid Breeze codec_chunk_frames={value!r}") from exc
if chunk_frames <= 0:
raise ValueError(f"Breeze codec_chunk_frames must be positive, got {chunk_frames}")
return chunk_frames
def talker2codec_async_chunk(
transfer_manager: Any,
multimodal_output: Mapping[str, Any] | None,
request: Any,
is_finished: bool = False,
) -> OmniPayloadStruct | None:
"""Batch Stage-0 frames and send only the un-emitted codebook-major tail."""
request_id = request.external_req_id
finished = bool(is_finished or request.is_finished())
frame = _extract_current_audio_frame(multimodal_output)
if frame is not None:
transfer_manager.code_prompt_token_ids[request_id].append(frame)
pending = transfer_manager.code_prompt_token_ids[request_id]
chunk_frames = _codec_chunk_frames(transfer_manager)
if not pending and not finished:
if request_id.endswith(CFG_UNCOND_SUFFIX):
return None
if len(pending) < chunk_frames and not finished:
return None
if pending:
# Stage 0 supplies frame-major ``(T, Q)`` rows. The Qwen3-TTS decoder
# consumes ``(Q, T)``, so transpose before flattening the emitted tail.
tail = torch.stack(pending, dim=0)
flat = tail.transpose(0, 1).contiguous().reshape(-1)
finished = is_finished or request.is_finished()
frames = transfer_manager.code_prompt_token_ids[request_id]
if multimodal_output is not None:
codes = multimodal_output.get("codes", {}).get("audio")
if codes is not None and codes.numel():
if codes.ndim != 2 or codes.shape[1] != 16:
raise ValueError("Breeze codec payload must have shape (frames, 16)")
rows = codes.to(device="cpu", dtype=torch.long)
if not bool(((rows >= 0) & (rows < 2048)).all()):
raise ValueError("Breeze codec payload contains a reserved or invalid code")
frames.extend(rows.unbind(0))
connector = transfer_manager.connector
if connector is None:
raise RuntimeError("Breeze streaming requires a stage connector")
config = connector.config
extra = config.get("extra", config)
state = transfer_manager.request_payload.setdefault(request_id, {"frames": 0, "chunks": 0})
emitted = state["frames"]
ramp = extra.get("codec_chunk_ramp", [])
if state["chunks"] < len(ramp):
chunk_size = int(ramp[state["chunks"]])
else:
flat = torch.empty(0, dtype=torch.long)
pending.clear()
chunk_size = int(
extra.get("initial_codec_chunk_frames", 1) if emitted == 0 else extra.get("codec_chunk_frames", 5)
)
if chunk_size <= 0:
raise ValueError("codec_chunk_frames must be positive")
pending = len(frames) - emitted
if not finished and pending < chunk_size:
return None
if pending < 0:
raise RuntimeError("Breeze codec transfer emitted more frames than generated")
# The shared codec retains its causal state; each frame is delivered once.
flat = torch.stack(frames[emitted:]).T.contiguous().reshape(-1) if pending else torch.empty(0, dtype=torch.long)
state["frames"] = len(frames)
state["chunks"] += 1
return OmniPayloadStruct(
codes=CodesStruct(audio=flat),
meta=MetaStruct(
request_id=request_id,
code_flat_numel=int(flat.numel()),
codec_streaming=True,
finished=torch.tensor(finished, dtype=torch.bool),
# The chunk adapter strips ``meta.finished`` before it reaches the
# stage-1 model; ``stream_finished`` survives and is what the
# codec uses to release per-request decoder state.
stream_finished=torch.tensor(finished, dtype=torch.bool),
),
meta=MetaStruct(request_id=request_id, left_context_size=0, finished=torch.tensor(finished)),
)
__all__ = ["talker2codec", "talker2codec_async_chunk", "talker2codec_full_payload"]
+38 -1
View File
@@ -1770,6 +1770,43 @@ class OmniGPUModelRunner(PrefixCacheRunnerMixin, GPUModelRunner):
req_ids_b = [item[0] for item in decode_batch_items]
start_offsets_b = [item[1] for item in decode_batch_items]
req_infos_b = [item[2] for item in decode_batch_items]
if not self.has_talker_mtp:
# Non-MTP hooks return (ids, embeds, per-request updates),
# matching scalar preprocess without the MTP-only tensors.
# Every non-decode row flushes the group, so these one-token
# spans are contiguous in the runner's capture buffers.
start = start_offsets_b[0]
end = start + len(req_ids_b)
if start < 0 or end > preprocess_input_ids.shape[0] or start_offsets_b != list(range(start, end)):
raise RuntimeError(
"Non-MTP batched decode preprocessing requires contiguous in-bounds token offsets"
)
ids_b = preprocess_input_ids[start:end]
req_input_ids, req_embeds, updates = batch_decode_preprocess(
input_ids=ids_b,
req_infos=req_infos_b,
)
if (
req_input_ids.numel() != len(req_ids_b)
or req_embeds.ndim != 2
or req_embeds.shape[0] != len(req_ids_b)
or len(updates) != len(req_ids_b)
):
raise ValueError("Batched decode preprocessing must return one output and update per request")
if inputs_embeds is None:
inputs_embeds = torch.empty(
(preprocess_input_ids.shape[0], req_embeds.shape[-1]),
device=req_embeds.device,
dtype=req_embeds.dtype,
)
inputs_embeds[start:end].copy_(req_embeds)
if req_input_ids is not ids_b:
preprocess_input_ids[start:end].copy_(req_input_ids.reshape(-1))
for req_id_b, update_dict_b in zip(req_ids_b, updates, strict=True):
self._update_intermediate_buffer(req_id_b, update_dict_b)
decode_batch_items.clear()
return
ids_b = torch.stack(
[preprocess_input_ids[offset : offset + 1].reshape(-1)[0] for offset in start_offsets_b]
)
@@ -1839,7 +1876,7 @@ class OmniGPUModelRunner(PrefixCacheRunnerMixin, GPUModelRunner):
# Seed, so a model that samples inside forward() can be
# reproducible: vLLM's own sampler seeding does not reach it.
req_infos["_omni_seed"] = getattr(sampling_params, "seed", None)
if callable(batch_decode_preprocess) and self.has_talker_mtp and span_len == 1 and not is_prefill:
if callable(batch_decode_preprocess) and span_len == 1 and not is_prefill:
decode_batch_items.append((req_id, s, req_infos))
continue