[Perf] Optimize Higgs Audio v3 serving (#4204)
Signed-off-by: Sy03 <1370724210@qq.com> Signed-off-by: Yueqian Lin <linyueqian@outlook.com> Co-authored-by: Yueqian Lin <linyueqian@outlook.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
21 files changed
+1949
-707
No files matched your search
@@ -568,6 +568,55 @@ steps:
|
||||
path: /mnt/hf-cache
|
||||
type: DirectoryOrCreate
|
||||
|
||||
- label: ":full_moon: TTS · Higgs-Audio-V3 · Perf Test"
|
||||
key: nightly-tts-performance-higgs-audio-v3
|
||||
depends_on: upload-nightly-pipeline
|
||||
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test" || build.pull_request.labels includes "tts-test"
|
||||
timeout_in_minutes: 180
|
||||
commands:
|
||||
- export BENCHMARK_DIR=tests/dfx/perf/results
|
||||
- export VLLM_USE_DEEP_GEMM="0"
|
||||
- export VLLM_MOE_USE_DEEP_GEMM="0"
|
||||
- |
|
||||
set +e
|
||||
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_higgs_audio_v3.json
|
||||
EXIT=$$?
|
||||
buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
|
||||
exit $$EXIT
|
||||
agents:
|
||||
queue: "mithril-h100-pool"
|
||||
plugins:
|
||||
- kubernetes:
|
||||
podSpec:
|
||||
containers:
|
||||
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
resources:
|
||||
limits:
|
||||
nvidia.com/gpu: 1
|
||||
volumeMounts:
|
||||
- name: devshm
|
||||
mountPath: /dev/shm
|
||||
- name: hf-cache
|
||||
mountPath: /root/.cache/huggingface
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /root/.cache/huggingface
|
||||
- name: HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
key: token
|
||||
nodeSelector:
|
||||
node.kubernetes.io/instance-type: gpu-h100-sxm
|
||||
volumes:
|
||||
- name: devshm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
- name: hf-cache
|
||||
hostPath:
|
||||
path: /mnt/hf-cache
|
||||
type: DirectoryOrCreate
|
||||
|
||||
# Diffusion X2I suite: x2i / x2a / x2t and related non-video paths; x2v is only in "Diffusion X2V Model Test" below.
|
||||
- group: ":card_index_dividers: Diffusion X2I(&A&T) Model Test"
|
||||
key: nightly-diffusion-x2iat-group
|
||||
@@ -1463,6 +1512,7 @@ steps:
|
||||
- nightly-omni-performance-vllm-text
|
||||
- nightly-omni-performance-multi-replicas
|
||||
- nightly-tts-performance
|
||||
- nightly-tts-performance-higgs-audio-v3
|
||||
- nightly-diffusion-x2iat-performance-qwen-image
|
||||
- nightly-diffusion-x2iat-performance-qwen-image-edit
|
||||
- nightly-diffusion-x2iat-performance-qwen-image-edit-2511
|
||||
@@ -1476,6 +1526,7 @@ steps:
|
||||
- export DEFAULT_INPUT_DIR=tests/dfx/perf/results
|
||||
- export DEFAULT_OUTPUT_DIR=tests/dfx/perf/results
|
||||
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-tts-performance
|
||||
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-tts-performance-higgs-audio-v3
|
||||
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-omni-performance-no-async-chunk
|
||||
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-omni-performance-async-chunk
|
||||
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-omni-performance-vllm-text
|
||||
|
||||
@@ -33,6 +33,15 @@ models:
|
||||
voice_clone:
|
||||
task_type: Base
|
||||
|
||||
bosonai/higgs-audio-v3-tts-4b:
|
||||
supported_tasks: [default_voice, voice_clone]
|
||||
backend: openai-audio-speech
|
||||
endpoint: /v1/audio/speech
|
||||
trust_remote_code: true
|
||||
task_extra_body:
|
||||
default_voice: {}
|
||||
voice_clone: {}
|
||||
|
||||
openbmb/VoxCPM2:
|
||||
supported_tasks: [voice_clone, default_voice]
|
||||
trust_remote_code: true
|
||||
|
||||
@@ -52,6 +52,7 @@ vllm-omni serve bosonai/higgs-audio-v3-tts-4b \
|
||||
|
||||
The default deploy config `vllm_omni/deploy/higgs_multimodal_qwen3.yaml` is
|
||||
loaded automatically by model registry (HF `model_type=higgs_multimodal_qwen3`).
|
||||
It matches the high-throughput profile.
|
||||
|
||||
**Offline batch inference:**
|
||||
|
||||
@@ -103,8 +104,28 @@ python examples/online_serving/text_to_speech/higgs_audio_v3/batch_speech_client
|
||||
- Voice cloning: `ref_audio` accepts WAV/FLAC/MP3; `ref_text` is optional but improves fidelity.
|
||||
- Deploy config: `vllm_omni/deploy/higgs_multimodal_qwen3.yaml` (auto-discovered from `model_type`).
|
||||
- `max_num_seqs=16` for both stages.
|
||||
- `enforce_eager=true` for both stages (CUDA graph available for stage 0 but no throughput gain at batch>1).
|
||||
- Stage 0 and Stage 1 default to the same device (`0`) for single-GPU serving.
|
||||
- Stage 0 intentionally keeps `enforce_eager=true`. This preserves the Higgs-specific local MLP CUDA graph path, which is the current high-throughput default.
|
||||
- Stage 1 remains `enforce_eager=true` for the codec decoder.
|
||||
- Deploy profiles:
|
||||
- High throughput: `vllm_omni/deploy/higgs_multimodal_qwen3_high_throughput.yaml`.
|
||||
Use this for medium/high concurrency. The auto-discovered
|
||||
`higgs_multimodal_qwen3.yaml` is kept as a compatibility/default alias for
|
||||
this profile.
|
||||
- Low latency: `vllm_omni/deploy/higgs_multimodal_qwen3_low_latency.yaml`.
|
||||
Use this for low-concurrency serving (for example c1-c4). It sets Stage 0
|
||||
`enforce_eager=false` and enables vLLM `FULL_DECODE_ONLY` CUDA graph through
|
||||
YAML `compilation_config`; no environment variable is required.
|
||||
- Profile details: `vllm_omni/deploy/README_higgs_audio_v3.md`.
|
||||
- Performance note:
|
||||
- Do not switch the auto-discovered default Stage 0 profile to vLLM
|
||||
`FULL_DECODE_ONLY` CUDA graph without an end-to-end throughput and
|
||||
audio-quality revalidation. On the H20 SeedTTS c16/full-dataset benchmark,
|
||||
the eager Stage 0 path with Higgs local MLP CUDA graph reproduced
|
||||
~35 audio_s/s (`1088/1088` OK per run, three runs around 134-136s). A
|
||||
separate H20 low-concurrency smoke after the FULL_DECODE audio-feedback
|
||||
capture fix showed lower c1/c4 latency, so the FULL_DECODE profile is kept
|
||||
as an explicit low-latency option rather than the throughput default.
|
||||
- Known limitations:
|
||||
- CUDA graph for stage 0 helps batch=1 (-13% RTF) but not batch>1 due to model-owned sampler running outside graph.
|
||||
- Stage 1 (code2wav) must use `enforce_eager=true` (`@torch.inference_mode` incompatible with graph capture).
|
||||
- Async streaming (chunk-based) not yet implemented; sync-only pipeline.
|
||||
- Stage 0 full-decode CUDA graph is experimental; sampler, delay-state updates, staging, and request postprocess remain outside the graph.
|
||||
@@ -0,0 +1,74 @@
|
||||
[
|
||||
{
|
||||
"test_name": "test_higgs_audio_v3",
|
||||
"server_params": {
|
||||
"model": "bosonai/higgs-audio-v3-tts-4b",
|
||||
"stage_overrides": {
|
||||
"0": {
|
||||
"gpu_memory_utilization": 0.3
|
||||
},
|
||||
"1": {
|
||||
"gpu_memory_utilization": 0.2
|
||||
}
|
||||
},
|
||||
"extra_cli_args": [
|
||||
"--deploy-config",
|
||||
"vllm_omni/deploy/higgs_multimodal_qwen3.yaml",
|
||||
"--trust-remote-code"
|
||||
]
|
||||
},
|
||||
"benchmark_params": [
|
||||
{
|
||||
"task": "default_voice",
|
||||
"eval_phase": "latency",
|
||||
"dataset_name": "seed-tts-text",
|
||||
"backend": "openai-audio-speech",
|
||||
"endpoint": "/v1/audio/speech",
|
||||
"dataset_path": "benchmarks/build_dataset/seed_tts_smoke",
|
||||
"num_prompts": [
|
||||
20
|
||||
],
|
||||
"max_concurrency": [
|
||||
1
|
||||
],
|
||||
"seed_tts_locale": "en",
|
||||
"percentile-metrics": "ttft,e2el,audio_rtf,audio_ttfp,audio_duration",
|
||||
"baseline": {
|
||||
"median_audio_ttfp_ms": [
|
||||
3000
|
||||
],
|
||||
"median_audio_rtf": [
|
||||
0.5
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"task": "default_voice",
|
||||
"eval_phase": "throughput",
|
||||
"dataset_name": "seed-tts-text",
|
||||
"backend": "openai-audio-speech",
|
||||
"endpoint": "/v1/audio/speech",
|
||||
"dataset_path": "benchmarks/build_dataset/seed_tts_smoke",
|
||||
"num_prompts": [
|
||||
500
|
||||
],
|
||||
"max_concurrency": [
|
||||
16
|
||||
],
|
||||
"seed_tts_locale": "en",
|
||||
"percentile-metrics": "ttft,e2el,audio_rtf,audio_ttfp,audio_duration",
|
||||
"baseline": {
|
||||
"median_audio_ttfp_ms": [
|
||||
5000
|
||||
],
|
||||
"median_audio_rtf": [
|
||||
1.2
|
||||
],
|
||||
"audio_throughput": [
|
||||
30
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -145,7 +145,7 @@ def test_shm_connector_flow(mocker: MockerFixture):
|
||||
Using real SharedMemoryConnector (inline mode for simplicity).
|
||||
"""
|
||||
# 1. Setup Connector
|
||||
config = {"shm_threshold_bytes": 1024} # Large threshold to use inline
|
||||
config = {"shm_threshold_bytes": 1024, "inline_small_payloads": True}
|
||||
connector = SharedMemoryConnector(config)
|
||||
connectors_map = {("0", "1"): connector}
|
||||
|
||||
|
||||
@@ -75,18 +75,21 @@ def test_create_unknown_connector():
|
||||
|
||||
@pytest.fixture
|
||||
def shm_connector():
|
||||
config = {"shm_threshold_bytes": 100} # Small threshold for testing
|
||||
config = {"shm_threshold_bytes": 100, "inline_small_payloads": True}
|
||||
return SharedMemoryConnector(config)
|
||||
|
||||
|
||||
def test_put_get_inline(shm_connector):
|
||||
"""Test transfer for small data (currently always uses SHM)."""
|
||||
"""Test inline transfer for small data."""
|
||||
data = {"small": "data"}
|
||||
|
||||
success, size, metadata = shm_connector.put("stage_0", "stage_1", "req_1", data)
|
||||
assert success is True
|
||||
assert "shm" in metadata
|
||||
assert "inline_bytes" in metadata
|
||||
assert "shm" not in metadata
|
||||
assert "size" in metadata
|
||||
assert shm_connector._metrics["inline_writes"] == 1
|
||||
assert shm_connector._metrics["shm_writes"] == 0
|
||||
|
||||
# Retrieve
|
||||
retrieved_data, ret_size = shm_connector.get("stage_0", "stage_1", "req_1", metadata)
|
||||
|
||||
@@ -13,7 +13,7 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
@pytest.fixture()
|
||||
def connector():
|
||||
c = SharedMemoryConnector({"shm_threshold_bytes": 64})
|
||||
c = SharedMemoryConnector({"shm_threshold_bytes": 0})
|
||||
yield c
|
||||
c.close()
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ AC-5 (delay pattern), AC-7 (stage processor), and AC-10 (registry)
|
||||
without requiring the actual checkpoint or GPU.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
@@ -232,7 +236,7 @@ class TestDelayPatternBehavior:
|
||||
|
||||
|
||||
class TestSamplerMethods:
|
||||
"""Test the actual _sample_audio_codes and _apply_delay_pattern_masking methods."""
|
||||
"""Test the actual sampler and batched delay masking methods."""
|
||||
|
||||
def _make_minimal_talker(self):
|
||||
"""Create a minimal talker-like object with sampler/masking methods."""
|
||||
@@ -241,18 +245,65 @@ class TestSamplerMethods:
|
||||
class FakeTalker:
|
||||
num_codebooks = 8
|
||||
codebook_size = 1026
|
||||
_audio_state = {}
|
||||
|
||||
t = FakeTalker()
|
||||
t._sample_audio_codes = mod.HiggsAudioV3TalkerForConditionalGeneration._sample_audio_codes.__get__(t)
|
||||
t._apply_delay_pattern_masking = (
|
||||
mod.HiggsAudioV3TalkerForConditionalGeneration._apply_delay_pattern_masking.__get__(t)
|
||||
return t
|
||||
|
||||
def _make_batched_sampler_talker(self, num_rows=4):
|
||||
"""Create a fake talker with GPU-resident sampler state helpers."""
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3 import higgs_audio_v3_talker as mod
|
||||
|
||||
class FakeTalker:
|
||||
num_codebooks = 8
|
||||
codebook_size = 1026
|
||||
|
||||
t = FakeTalker()
|
||||
t._decode_last_codes = torch.arange(num_rows * t.num_codebooks, dtype=torch.long).view(
|
||||
num_rows, t.num_codebooks
|
||||
)
|
||||
t._decode_has_codes = torch.tensor([True, False, True, True])[:num_rows].clone()
|
||||
t._decode_delay_count = torch.tensor([0, 2, 7, 4], dtype=torch.int32)[:num_rows].clone()
|
||||
t._decode_eoc_countdown = torch.tensor([-1, -1, 3, 1], dtype=torch.int32)[:num_rows].clone()
|
||||
t._decode_generation_done = torch.tensor([False, False, False, True])[:num_rows].clone()
|
||||
t._decode_active_audio_count = 0
|
||||
t._codebook_index_cache = {}
|
||||
t._row_index_cache = {}
|
||||
t._boc_frame_cache = {}
|
||||
t._last_audio_codes_buffer = None
|
||||
t._last_audio_host_staging = None
|
||||
t._last_audio_gpu_staging = None
|
||||
t._last_audio_staging_event = None
|
||||
t._audio_staging_events = []
|
||||
t._audio_staging_event_cursor = 0
|
||||
t._last_audio_codes = None
|
||||
t._last_audio_code_valid = []
|
||||
t._postprocess_cursor = 0
|
||||
t._postprocess_audio_rows = 0
|
||||
t._postprocess_audio_active_rows = 0
|
||||
|
||||
cls = mod.HiggsAudioV3TalkerForConditionalGeneration
|
||||
for name in (
|
||||
"_sample_audio_codes",
|
||||
"_ensure_decode_state_capacity",
|
||||
"_get_row_indices",
|
||||
"_get_codebook_indices",
|
||||
"_get_boc_frames",
|
||||
"_get_audio_codes_buffer",
|
||||
"_get_audio_gpu_staging_buffer",
|
||||
"_get_audio_host_staging_buffer",
|
||||
"_apply_delay_pattern_masking_batched",
|
||||
"_update_delay_state_batched",
|
||||
"_prefill_row_mask",
|
||||
"_audio_seed_mask_from_step_input",
|
||||
):
|
||||
setattr(t, name, getattr(cls, name).__get__(t))
|
||||
t._device_cache_key = cls._device_cache_key
|
||||
return t
|
||||
|
||||
def test_sample_respects_mask(self):
|
||||
"""Tokens masked to -inf must never be sampled."""
|
||||
t = self._make_minimal_talker()
|
||||
t = self._make_batched_sampler_talker(num_rows=1)
|
||||
logits = torch.full((10, 1026), float("-inf"))
|
||||
# Only token 500 is allowed for each row
|
||||
logits[:, 500] = 0.0
|
||||
@@ -262,7 +313,7 @@ class TestSamplerMethods:
|
||||
|
||||
def test_sample_all_masked_falls_back_to_argmax(self):
|
||||
"""All-masked row should fall back to argmax (least-negative logit)."""
|
||||
t = self._make_minimal_talker()
|
||||
t = self._make_batched_sampler_talker(num_rows=1)
|
||||
logits = torch.full((2, 1026), float("-inf"))
|
||||
# Row 0: all masked
|
||||
# Row 1: only token 42 allowed
|
||||
@@ -275,11 +326,13 @@ class TestSamplerMethods:
|
||||
"""During delay phase, codebooks beyond delay_count must have only BOC allowed."""
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.higgs_audio_v3_talker import BOC_ID
|
||||
|
||||
t = self._make_minimal_talker()
|
||||
t = self._make_batched_sampler_talker(num_rows=1)
|
||||
# Simulate delay_count=2 for batch row 0
|
||||
t._audio_state[0] = {"num_delay": 2, "num_remaining_delays": None}
|
||||
t._ensure_decode_state_capacity(1, torch.device("cpu"))
|
||||
t._decode_delay_count[0] = 2
|
||||
t._decode_eoc_countdown[0] = -1
|
||||
cb_logits = torch.zeros(1, 8, 1026) # [1 audio row, 8 codebooks, 1026 vocab]
|
||||
t._apply_delay_pattern_masking(cb_logits, [0])
|
||||
t._apply_delay_pattern_masking_batched(cb_logits, torch.tensor([0], dtype=torch.long))
|
||||
# CBs 3-7 should have only BOC allowed (everything else -inf)
|
||||
for q in range(3, 8):
|
||||
row = cb_logits[0, q]
|
||||
@@ -292,10 +345,12 @@ class TestSamplerMethods:
|
||||
"""Active codebooks during delay should have BOC disallowed."""
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.higgs_audio_v3_talker import BOC_ID
|
||||
|
||||
t = self._make_minimal_talker()
|
||||
t._audio_state[0] = {"num_delay": 3, "num_remaining_delays": None}
|
||||
t = self._make_batched_sampler_talker(num_rows=1)
|
||||
t._ensure_decode_state_capacity(1, torch.device("cpu"))
|
||||
t._decode_delay_count[0] = 3
|
||||
t._decode_eoc_countdown[0] = -1
|
||||
cb_logits = torch.zeros(1, 8, 1026)
|
||||
t._apply_delay_pattern_masking(cb_logits, [0])
|
||||
t._apply_delay_pattern_masking_batched(cb_logits, torch.tensor([0], dtype=torch.long))
|
||||
# CBs 0-3 are active: BOC should be -inf
|
||||
for q in range(4):
|
||||
assert cb_logits[0, q, BOC_ID].item() == float("-inf")
|
||||
@@ -304,10 +359,12 @@ class TestSamplerMethods:
|
||||
"""Only codebook 0 should allow EOC during normal generation."""
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.higgs_audio_v3_talker import EOC_ID
|
||||
|
||||
t = self._make_minimal_talker()
|
||||
t._audio_state[0] = {"num_delay": 8, "num_remaining_delays": None}
|
||||
t = self._make_batched_sampler_talker(num_rows=1)
|
||||
t._ensure_decode_state_capacity(1, torch.device("cpu"))
|
||||
t._decode_delay_count[0] = 8
|
||||
t._decode_eoc_countdown[0] = -1
|
||||
cb_logits = torch.zeros(1, 8, 1026)
|
||||
t._apply_delay_pattern_masking(cb_logits, [0])
|
||||
t._apply_delay_pattern_masking_batched(cb_logits, torch.tensor([0], dtype=torch.long))
|
||||
# CB0 should keep EOC
|
||||
assert cb_logits[0, 0, EOC_ID].item() != float("-inf")
|
||||
# CB1-7 should have EOC masked
|
||||
@@ -321,11 +378,13 @@ class TestSamplerMethods:
|
||||
EOC_ID,
|
||||
)
|
||||
|
||||
t = self._make_minimal_talker()
|
||||
t = self._make_batched_sampler_talker(num_rows=1)
|
||||
# Ramp-down with 4 remaining delays: lock CBs 0-3 to EOC
|
||||
t._audio_state[0] = {"num_delay": 8, "num_remaining_delays": 4}
|
||||
t._ensure_decode_state_capacity(1, torch.device("cpu"))
|
||||
t._decode_delay_count[0] = 8
|
||||
t._decode_eoc_countdown[0] = 4
|
||||
cb_logits = torch.zeros(1, 8, 1026)
|
||||
t._apply_delay_pattern_masking(cb_logits, [0])
|
||||
t._apply_delay_pattern_masking_batched(cb_logits, torch.tensor([0], dtype=torch.long))
|
||||
# CBs 0-3 locked: only EOC allowed
|
||||
for q in range(4):
|
||||
row = cb_logits[0, q]
|
||||
@@ -337,6 +396,195 @@ class TestSamplerMethods:
|
||||
assert cb_logits[0, q, BOC_ID].item() == float("-inf")
|
||||
assert cb_logits[0, q, EOC_ID].item() == float("-inf")
|
||||
|
||||
def test_batched_delay_masking_all_rows_matches_sparse_path(self):
|
||||
"""The all-row fast path must preserve sparse path masking semantics."""
|
||||
torch.manual_seed(0)
|
||||
sparse = self._make_batched_sampler_talker()
|
||||
all_rows = self._make_batched_sampler_talker()
|
||||
rows = torch.arange(4, dtype=torch.long)
|
||||
logits_sparse = torch.randn(4, 8, 1026)
|
||||
logits_all_rows = logits_sparse.clone()
|
||||
|
||||
sparse._apply_delay_pattern_masking_batched(logits_sparse, rows, all_rows=False)
|
||||
all_rows._apply_delay_pattern_masking_batched(logits_all_rows, rows, all_rows=True)
|
||||
|
||||
assert torch.equal(logits_all_rows, logits_sparse)
|
||||
|
||||
def test_delay_state_update_all_rows_matches_sparse_path(self):
|
||||
"""The all-row fast path must update state and staging like index_copy_."""
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.higgs_audio_v3_talker import (
|
||||
EOC_ID,
|
||||
)
|
||||
|
||||
sparse = self._make_batched_sampler_talker()
|
||||
all_rows = self._make_batched_sampler_talker()
|
||||
rows = torch.arange(4, dtype=torch.long)
|
||||
codes = torch.tensor(
|
||||
[
|
||||
[10, 11, 12, 13, 14, 15, 16, 17],
|
||||
[20, 21, 22, 23, 24, 25, 26, 27],
|
||||
[30, 31, EOC_ID, 33, 34, 35, 36, 37],
|
||||
[40, 41, 42, 43, 44, 45, 46, 47],
|
||||
],
|
||||
dtype=torch.long,
|
||||
)
|
||||
code_row_mask = torch.tensor([True, False, True, True])
|
||||
|
||||
sparse._update_delay_state_batched(
|
||||
codes.clone(), rows, 4, torch.device("cpu"), code_row_mask=code_row_mask, all_rows=False
|
||||
)
|
||||
all_rows._update_delay_state_batched(
|
||||
codes.clone(), rows, 4, torch.device("cpu"), code_row_mask=code_row_mask, all_rows=True
|
||||
)
|
||||
|
||||
assert torch.equal(all_rows._decode_delay_count, sparse._decode_delay_count)
|
||||
assert torch.equal(all_rows._decode_eoc_countdown, sparse._decode_eoc_countdown)
|
||||
assert torch.equal(all_rows._decode_generation_done, sparse._decode_generation_done)
|
||||
assert torch.equal(all_rows._decode_last_codes, sparse._decode_last_codes)
|
||||
assert torch.equal(all_rows._decode_has_codes, sparse._decode_has_codes)
|
||||
assert all_rows._last_audio_codes is None
|
||||
assert sparse._last_audio_codes is None
|
||||
assert torch.equal(all_rows._last_audio_host_staging, sparse._last_audio_host_staging)
|
||||
|
||||
def test_direct_audio_sampler_does_not_override_done_tokens(self):
|
||||
"""Done rows are represented directly in GPU sampled tokens."""
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3 import higgs_audio_v3_talker as mod
|
||||
|
||||
t = self._make_batched_sampler_talker(num_rows=2)
|
||||
t._resolve_token_ids = lambda: None
|
||||
t._audio_continuation_id = 99999
|
||||
t._eos_token_id = 151671
|
||||
t._last_logits_hidden = torch.zeros(2, 16)
|
||||
t._last_step_input_ids = torch.tensor([99999, 99999])
|
||||
t._last_step_query_start_loc = None
|
||||
t._decode_has_codes = torch.tensor([True, True])
|
||||
t._decode_generation_done = torch.tensor([False, True])
|
||||
t._decode_delay_count = torch.zeros(2, dtype=torch.long)
|
||||
t._decode_eoc_countdown = torch.full((2,), -1, dtype=torch.long)
|
||||
t._fast_audio_direct_rows = 2
|
||||
t._last_audio_done_flags = None
|
||||
t._fast_audio_sampler_gpu_fallback_reason = lambda **kwargs: None
|
||||
t._audio_codebook_logits_from_rows = lambda hidden, rows, all_rows=False: torch.zeros(2, 8, 1026)
|
||||
t._apply_delay_pattern_masking_batched = lambda cb_logits, audio_rows, all_rows=False: None
|
||||
t._sample_audio_codes = lambda logits_2d: torch.zeros(int(logits_2d.shape[0]), dtype=torch.long)
|
||||
t._update_delay_state_batched = lambda *args, **kwargs: None
|
||||
t.sample = mod.HiggsAudioV3TalkerForConditionalGeneration.sample.__get__(t)
|
||||
|
||||
sampler_output = t.sample(torch.zeros(2, 200000), sampling_metadata=object())
|
||||
|
||||
assert not getattr(
|
||||
mod.HiggsAudioV3TalkerForConditionalGeneration, "supports_sampled_token_ids_cpu_override", False
|
||||
)
|
||||
assert sampler_output.sampled_token_ids.tolist() == [[99999], [151671]]
|
||||
|
||||
def test_direct_audio_sampler_uses_gpu_tokens_for_active_rows(self):
|
||||
"""Direct audio sampling leaves scheduler-visible tokens to the GPU tensor."""
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3 import higgs_audio_v3_talker as mod
|
||||
|
||||
t = self._make_batched_sampler_talker(num_rows=2)
|
||||
t._resolve_token_ids = lambda: None
|
||||
t._audio_continuation_id = 99999
|
||||
t._eos_token_id = 151671
|
||||
t._last_logits_hidden = torch.zeros(2, 16)
|
||||
t._last_step_input_ids = torch.tensor([99999, 99999])
|
||||
t._last_step_query_start_loc = None
|
||||
t._decode_has_codes = torch.tensor([True, True])
|
||||
t._decode_generation_done = torch.tensor([False, False])
|
||||
t._decode_delay_count = torch.zeros(2, dtype=torch.long)
|
||||
t._decode_eoc_countdown = torch.full((2,), -1, dtype=torch.long)
|
||||
t._fast_audio_direct_rows = 2
|
||||
t._last_audio_done_flags = [0, 0]
|
||||
t._fast_audio_sampler_gpu_fallback_reason = lambda **kwargs: None
|
||||
t._audio_codebook_logits_from_rows = lambda hidden, rows, all_rows=False: torch.zeros(2, 8, 1026)
|
||||
t._apply_delay_pattern_masking_batched = lambda cb_logits, audio_rows, all_rows=False: None
|
||||
t._sample_audio_codes = lambda logits_2d: torch.zeros(int(logits_2d.shape[0]), dtype=torch.long)
|
||||
t._update_delay_state_batched = lambda *args, **kwargs: None
|
||||
t.sample = mod.HiggsAudioV3TalkerForConditionalGeneration.sample.__get__(t)
|
||||
|
||||
sampler_output = t.sample(torch.zeros(2, 200000), sampling_metadata=object())
|
||||
|
||||
assert sampler_output.sampled_token_ids.tolist() == [[99999], [99999]]
|
||||
|
||||
def test_terminal_rampdown_frame_is_emitted_before_done(self):
|
||||
"""The final ramp-down frame must be emitted, not replaced by a done marker."""
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.higgs_audio_v3_talker import EOC_ID
|
||||
|
||||
t = self._make_batched_sampler_talker(num_rows=1)
|
||||
t._decode_delay_count[0] = 8
|
||||
t._decode_eoc_countdown[0] = 1
|
||||
t._decode_generation_done[0] = False
|
||||
t._decode_has_codes[0] = True
|
||||
codes = torch.tensor([[10, 11, 12, 13, 14, 15, 16, 17]], dtype=torch.long)
|
||||
|
||||
t._update_delay_state_batched(
|
||||
codes,
|
||||
torch.tensor([0], dtype=torch.long),
|
||||
1,
|
||||
torch.device("cpu"),
|
||||
code_row_mask=torch.tensor([True]),
|
||||
all_rows=True,
|
||||
)
|
||||
|
||||
staged = t._last_audio_host_staging[0]
|
||||
assert staged[t.num_codebooks].item() == 1 # valid
|
||||
assert staged[t.num_codebooks + 1].item() == 1 # done
|
||||
assert not (staged[: t.num_codebooks] == -1).any()
|
||||
assert staged[: t.num_codebooks - 1].eq(EOC_ID).all()
|
||||
assert t._decode_generation_done[0].item() is True
|
||||
assert t._decode_has_codes[0].item() is False
|
||||
|
||||
def test_decode_state_growth_preserves_inflight_rows(self):
|
||||
"""Growing decode buffers must copy old rows and initialize new rows inactive."""
|
||||
t = self._make_batched_sampler_talker(num_rows=2)
|
||||
old_codes = t._decode_last_codes.clone()
|
||||
old_has = t._decode_has_codes.clone()
|
||||
old_delay = t._decode_delay_count.clone()
|
||||
old_rem = t._decode_eoc_countdown.clone()
|
||||
old_done = t._decode_generation_done.clone()
|
||||
|
||||
t._ensure_decode_state_capacity(5, torch.device("cpu"))
|
||||
|
||||
assert t._decode_last_codes.shape[0] >= 5
|
||||
assert torch.equal(t._decode_last_codes[:2], old_codes)
|
||||
assert torch.equal(t._decode_has_codes[:2], old_has)
|
||||
assert torch.equal(t._decode_delay_count[:2], old_delay)
|
||||
assert torch.equal(t._decode_eoc_countdown[:2], old_rem)
|
||||
assert torch.equal(t._decode_generation_done[:2], old_done)
|
||||
assert not t._decode_has_codes[2:5].any()
|
||||
assert not t._decode_generation_done[2:5].any()
|
||||
assert t._decode_eoc_countdown[2:5].eq(-1).all()
|
||||
|
||||
def test_mixed_batch_prefill_mask_targets_request_rows(self):
|
||||
"""Mixed prefill/decode must reset only prefill request rows."""
|
||||
t = self._make_batched_sampler_talker(num_rows=3)
|
||||
t._last_step_query_start_loc = torch.tensor([0, 1, 4, 5])
|
||||
|
||||
mask = t._prefill_row_mask(3, torch.device("cpu"))
|
||||
|
||||
assert torch.equal(mask, torch.tensor([False, True, False]))
|
||||
|
||||
def test_mixed_batch_seed_mask_maps_token_tails_to_request_rows(self):
|
||||
"""Seed detection should return one flag per request, not per token."""
|
||||
t = self._make_batched_sampler_talker(num_rows=3)
|
||||
t._audio_continuation_id = 99999
|
||||
t._last_step_query_start_loc = torch.tensor([0, 1, 4, 5])
|
||||
t._last_step_input_ids = torch.tensor([99999, 11, 12, 13, 99999])
|
||||
|
||||
mask = t._audio_seed_mask_from_step_input(3, torch.device("cpu"))
|
||||
|
||||
assert torch.equal(mask, torch.tensor([True, False, True]))
|
||||
|
||||
def test_mixed_batch_seed_mask_allows_prefill_tail_audio_token(self):
|
||||
"""A prefill span ending in <|audio|> starts audio-code generation."""
|
||||
t = self._make_batched_sampler_talker(num_rows=3)
|
||||
t._audio_continuation_id = 99999
|
||||
t._last_step_query_start_loc = torch.tensor([0, 1, 4, 5])
|
||||
t._last_step_input_ids = torch.tensor([99999, 11, 12, 99999, 99999])
|
||||
|
||||
mask = t._audio_seed_mask_from_step_input(3, torch.device("cpu"))
|
||||
|
||||
assert torch.equal(mask, torch.tensor([True, True, True]))
|
||||
|
||||
|
||||
# ---- AC-6: Feedback Method Tests ----
|
||||
|
||||
@@ -348,10 +596,14 @@ class TestFeedbackMethods:
|
||||
|
||||
class FakeTalker:
|
||||
_last_audio_codes = torch.tensor([[100, 200, 300, 400, 500, 600, 700, 800]])
|
||||
_last_audio_code_valid = [True]
|
||||
_last_audio_host_staging = None
|
||||
_last_audio_staging_event = None
|
||||
_postprocess_cursor = 0
|
||||
|
||||
t = FakeTalker()
|
||||
t.postprocess = mod.HiggsAudioV3TalkerForConditionalGeneration.postprocess.__get__(t)
|
||||
t._postprocess_impl = mod.HiggsAudioV3TalkerForConditionalGeneration._postprocess_impl.__get__(t)
|
||||
result = t.postprocess(torch.zeros(1, 64))
|
||||
assert "codes" in result
|
||||
assert "audio" in result["codes"]
|
||||
@@ -363,10 +615,14 @@ class TestFeedbackMethods:
|
||||
|
||||
class FakeTalker:
|
||||
_last_audio_codes = torch.tensor([[-1, -1, -1, -1, -1, -1, -1, -1]])
|
||||
_last_audio_code_valid = [False]
|
||||
_last_audio_host_staging = None
|
||||
_last_audio_staging_event = None
|
||||
_postprocess_cursor = 0
|
||||
|
||||
t = FakeTalker()
|
||||
t.postprocess = mod.HiggsAudioV3TalkerForConditionalGeneration.postprocess.__get__(t)
|
||||
t._postprocess_impl = mod.HiggsAudioV3TalkerForConditionalGeneration._postprocess_impl.__get__(t)
|
||||
result = t.postprocess(torch.zeros(1, 64))
|
||||
assert result == {}
|
||||
|
||||
@@ -378,10 +634,14 @@ class TestFeedbackMethods:
|
||||
_last_audio_codes = torch.tensor(
|
||||
[[100, 200, 300, 400, 500, 600, 700, 800], [-1, -1, -1, -1, -1, -1, -1, -1]]
|
||||
)
|
||||
_last_audio_code_valid = [True, False]
|
||||
_last_audio_host_staging = None
|
||||
_last_audio_staging_event = None
|
||||
_postprocess_cursor = 0
|
||||
|
||||
t = FakeTalker()
|
||||
t.postprocess = mod.HiggsAudioV3TalkerForConditionalGeneration.postprocess.__get__(t)
|
||||
t._postprocess_impl = mod.HiggsAudioV3TalkerForConditionalGeneration._postprocess_impl.__get__(t)
|
||||
r1 = t.postprocess(torch.zeros(1, 64))
|
||||
assert "codes" in r1
|
||||
r2 = t.postprocess(torch.zeros(1, 64))
|
||||
@@ -405,14 +665,23 @@ class TestAudioFeedback:
|
||||
codebook_size = 1026
|
||||
_audio_continuation_id = 99999 # fake audio token
|
||||
_last_step_query_start_loc = None
|
||||
_audio_state = {}
|
||||
_decode_active_audio_count = 0
|
||||
_decode_last_codes = torch.zeros(4, 8, dtype=torch.long)
|
||||
_decode_has_codes = torch.zeros(4, dtype=torch.bool)
|
||||
_use_external_decode_cudagraph = False
|
||||
multimodal_embedding = embed
|
||||
model = type("M", (), {"embed_tokens": lambda self, ids: torch.zeros(ids.shape[0], 16)})()
|
||||
|
||||
t = FakeTalker()
|
||||
t._decode_last_codes = torch.zeros(256, 8, dtype=torch.long)
|
||||
t._decode_has_codes = torch.zeros(256, dtype=torch.bool)
|
||||
t._apply_audio_feedback = mod.HiggsAudioV3TalkerForConditionalGeneration._apply_audio_feedback.__get__(t)
|
||||
t._is_single_token_decode_step = (
|
||||
mod.HiggsAudioV3TalkerForConditionalGeneration._is_single_token_decode_step.__get__(t)
|
||||
)
|
||||
t._step_request_count = mod.HiggsAudioV3TalkerForConditionalGeneration._step_request_count.__get__(t)
|
||||
t._decode_request_token_positions = (
|
||||
mod.HiggsAudioV3TalkerForConditionalGeneration._decode_request_token_positions.__get__(t)
|
||||
)
|
||||
t._ensure_decode_state_capacity = lambda min_bs, device: None
|
||||
return t
|
||||
|
||||
def test_text_positions_unchanged(self):
|
||||
@@ -428,9 +697,11 @@ class TestAudioFeedback:
|
||||
"""Audio position with state should have its embedding replaced."""
|
||||
t = self._make_feedback_talker()
|
||||
audio_id = t._audio_continuation_id
|
||||
t._audio_state[1] = {"last_codes": torch.zeros(8, dtype=torch.long)}
|
||||
t._decode_last_codes[1] = torch.zeros(8, dtype=torch.long)
|
||||
t._decode_has_codes[1] = True
|
||||
t._decode_active_audio_count = 1
|
||||
t._ensure_decode_state_capacity = lambda min_bs, device: None
|
||||
t._last_step_query_start_loc = torch.tensor([0, 1, 2, 3])
|
||||
input_ids = torch.tensor([1, audio_id, 3])
|
||||
hidden = torch.zeros(3, 16)
|
||||
result = t._apply_audio_feedback(hidden, input_ids)
|
||||
@@ -444,12 +715,80 @@ class TestAudioFeedback:
|
||||
"""Audio position without state should not be replaced."""
|
||||
t = self._make_feedback_talker()
|
||||
audio_id = t._audio_continuation_id
|
||||
# No state for row 0 — _decode_has_codes[0] defaults to False
|
||||
# No state for row 0
|
||||
input_ids = torch.tensor([audio_id])
|
||||
hidden = torch.zeros(1, 16)
|
||||
result = t._apply_audio_feedback(hidden, input_ids)
|
||||
assert torch.equal(result, hidden)
|
||||
|
||||
def test_prefill_span_does_not_receive_audio_feedback(self):
|
||||
"""A prefill span can have numel == hidden rows but must not be treated as decode."""
|
||||
t = self._make_feedback_talker()
|
||||
t._last_step_input_ids = torch.tensor([1, 2, 3, 4])
|
||||
t._last_step_query_start_loc = torch.tensor([0, 4])
|
||||
t._decode_last_codes[0] = torch.zeros(8, dtype=torch.long)
|
||||
t._decode_has_codes[0] = True
|
||||
t._decode_active_audio_count = 1
|
||||
hidden = torch.zeros(4, 16)
|
||||
|
||||
result = t._apply_audio_feedback(hidden, t._last_step_input_ids)
|
||||
|
||||
assert torch.equal(result, hidden)
|
||||
|
||||
def test_multi_request_decode_receives_audio_feedback(self):
|
||||
"""Concurrent decode has numel > 1 but each request span is exactly one token."""
|
||||
t = self._make_feedback_talker()
|
||||
t._last_step_input_ids = torch.tensor([10, 11, 12, 13])
|
||||
t._last_step_query_start_loc = torch.tensor([0, 1, 2, 3, 4])
|
||||
t._decode_last_codes[2] = torch.zeros(8, dtype=torch.long)
|
||||
t._decode_has_codes[2] = True
|
||||
t._decode_active_audio_count = 1
|
||||
hidden = torch.zeros(4, 16)
|
||||
|
||||
result = t._apply_audio_feedback(hidden, t._last_step_input_ids)
|
||||
|
||||
assert result[2].abs().sum() > 0
|
||||
assert result[0].abs().sum() == 0
|
||||
assert result[1].abs().sum() == 0
|
||||
assert result[3].abs().sum() == 0
|
||||
|
||||
def test_external_decode_graph_capture_keeps_feedback_ops_without_query_start_loc(self):
|
||||
"""FULL_DECODE capture has no request qsl but still needs dense decode feedback ops."""
|
||||
t = self._make_feedback_talker()
|
||||
t._use_external_decode_cudagraph = True
|
||||
t._last_step_input_ids = torch.tensor([10, 11, 12, 13])
|
||||
t._last_step_query_start_loc = None
|
||||
t._decode_last_codes[2] = torch.zeros(8, dtype=torch.long)
|
||||
t._decode_has_codes[2] = True
|
||||
hidden = torch.zeros(4, 16)
|
||||
|
||||
result = t._apply_audio_feedback(hidden, t._last_step_input_ids)
|
||||
|
||||
assert result[2].abs().sum() > 0
|
||||
assert result[0].abs().sum() == 0
|
||||
assert result[1].abs().sum() == 0
|
||||
assert result[3].abs().sum() == 0
|
||||
|
||||
def test_mixed_prefill_decode_receives_feedback_only_on_decode_rows(self):
|
||||
"""Mixed batches should skip prefill spans but keep audio feedback for decode spans."""
|
||||
t = self._make_feedback_talker()
|
||||
t._last_step_input_ids = torch.tensor([99999, 21, 22, 23, 99999])
|
||||
t._last_step_query_start_loc = torch.tensor([0, 1, 4, 5])
|
||||
t._decode_last_codes[0] = torch.zeros(8, dtype=torch.long)
|
||||
t._decode_last_codes[2] = torch.zeros(8, dtype=torch.long)
|
||||
t._decode_has_codes[0] = True
|
||||
t._decode_has_codes[2] = True
|
||||
t._decode_active_audio_count = 2
|
||||
hidden = torch.zeros(5, 16)
|
||||
|
||||
result = t._apply_audio_feedback(hidden, t._last_step_input_ids)
|
||||
|
||||
assert result[0].abs().sum() > 0
|
||||
assert result[4].abs().sum() > 0
|
||||
assert result[1].abs().sum() == 0
|
||||
assert result[2].abs().sum() == 0
|
||||
assert result[3].abs().sum() == 0
|
||||
|
||||
|
||||
# ---- AC-8: Codec Strictness ----
|
||||
|
||||
@@ -775,6 +1114,23 @@ class TestStageInputProcessor:
|
||||
with pytest.raises(ValueError, match="Not enough frames"):
|
||||
_revert_delay_pattern(codes)
|
||||
|
||||
def test_talker2code2wav_skips_too_few_frames_without_crashing(self):
|
||||
from vllm_omni.model_executor.stage_input_processors.higgs_audio_v3 import (
|
||||
talker2code2wav,
|
||||
)
|
||||
|
||||
class Output:
|
||||
multimodal_output = {"codes": {"audio": torch.zeros(5, 8, dtype=torch.long)}}
|
||||
|
||||
class TalkerOutput:
|
||||
finished = True
|
||||
outputs = [Output()]
|
||||
|
||||
result = talker2code2wav([TalkerOutput()])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["prompt_token_ids"] == []
|
||||
|
||||
def test_filter_real_code_frames(self):
|
||||
from vllm_omni.model_executor.stage_input_processors.higgs_audio_v3 import (
|
||||
_filter_real_code_frames,
|
||||
@@ -826,7 +1182,13 @@ class TestRegistry:
|
||||
|
||||
# __file__ = tests/unit/higgs_audio_v3/test_*.py → 4 dirnames to repo root
|
||||
repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
||||
yaml_path = os.path.join(repo_root, "vllm_omni", "deploy", "higgs_multimodal_qwen3.yaml")
|
||||
deploy_dir = os.path.join(repo_root, "vllm_omni", "deploy")
|
||||
for name in (
|
||||
"higgs_multimodal_qwen3.yaml",
|
||||
"higgs_multimodal_qwen3_high_throughput.yaml",
|
||||
"higgs_multimodal_qwen3_low_latency.yaml",
|
||||
):
|
||||
yaml_path = os.path.join(deploy_dir, name)
|
||||
assert os.path.isfile(yaml_path), f"Deploy YAML not found at {yaml_path}"
|
||||
|
||||
|
||||
@@ -914,298 +1276,89 @@ class TestPromptBuilder:
|
||||
assert 151704 not in ids # <|ref_text|>
|
||||
|
||||
|
||||
# ---- Regression: ramp-down done-before-emit off-by-one ----
|
||||
class TestVoiceCloneReferenceCache:
|
||||
def test_audio_tokenizer_dir_env_accepts_parent_or_subdir(self, tmp_path, monkeypatch):
|
||||
from vllm_omni.model_executor.models.higgs_audio_v2 import higgs_audio_v2_tokenizer as tok
|
||||
|
||||
parent = tmp_path / "OmniVoice"
|
||||
subdir = parent / "audio_tokenizer"
|
||||
subdir.mkdir(parents=True)
|
||||
(subdir / "config.json").write_text("{}", encoding="utf-8")
|
||||
|
||||
def _simulate_delay_state_machine(
|
||||
sampled_codes_per_step: list[torch.Tensor],
|
||||
) -> list[torch.Tensor]:
|
||||
"""Reproduce the delay-pattern state machine from sample().
|
||||
monkeypatch.setenv("HIGGS_AUDIO_TOKENIZER_PATH", str(parent))
|
||||
assert tok._resolve_audio_tokenizer_dir() == str(subdir)
|
||||
|
||||
Feeds ``sampled_codes_per_step`` (each [Q]) through the exact same
|
||||
logic as HiggsAudioV3TalkerForConditionalGeneration.sample() and
|
||||
returns the list of emitted frames (non -1 codes).
|
||||
"""
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.higgs_audio_v3_talker import (
|
||||
BOC_ID,
|
||||
EOC_ID,
|
||||
monkeypatch.setenv("HIGGS_AUDIO_TOKENIZER_PATH", str(subdir))
|
||||
assert tok._resolve_audio_tokenizer_dir() == str(subdir)
|
||||
|
||||
def test_higgs_v3_ref_code_cache_returns_clone(self):
|
||||
from vllm_omni.entrypoints.openai.serving_speech import OmniOpenAIServingSpeech
|
||||
|
||||
serving = object.__new__(OmniOpenAIServingSpeech)
|
||||
serving._higgs_audio_v3_ref_code_cache = OrderedDict()
|
||||
serving._higgs_audio_v3_ref_code_cache_bytes = 0
|
||||
serving._higgs_audio_v3_ref_code_inflight = {}
|
||||
|
||||
codes = torch.arange(16, dtype=torch.long).reshape(2, 8)
|
||||
serving._put_higgs_audio_v3_ref_codes("ref-a", codes)
|
||||
|
||||
cached = serving._get_higgs_audio_v3_ref_codes("ref-a")
|
||||
assert cached is not None
|
||||
cached.fill_(0)
|
||||
|
||||
cached_again = serving._get_higgs_audio_v3_ref_codes("ref-a")
|
||||
assert cached_again is not None
|
||||
assert torch.equal(cached_again, codes)
|
||||
|
||||
def test_higgs_v3_ref_code_inflight_deduplicates_concurrent_encode(self):
|
||||
from vllm_omni.entrypoints.openai.serving_speech import OmniOpenAIServingSpeech
|
||||
|
||||
serving = object.__new__(OmniOpenAIServingSpeech)
|
||||
serving._higgs_audio_v3_ref_code_cache = OrderedDict()
|
||||
serving._higgs_audio_v3_ref_code_cache_bytes = 0
|
||||
serving._higgs_audio_v3_ref_code_inflight = {}
|
||||
calls = 0
|
||||
|
||||
def encode_reference_audio(_wav, _sr):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
time.sleep(0.05)
|
||||
return torch.arange(16, dtype=torch.long).reshape(2, 8)
|
||||
|
||||
def apply_delay_pattern(codes):
|
||||
return codes + 1
|
||||
|
||||
async def run():
|
||||
return await asyncio.gather(
|
||||
*[
|
||||
serving._resolve_higgs_audio_v3_ref_codes(
|
||||
"ref-a",
|
||||
object(),
|
||||
24000,
|
||||
encode_reference_audio,
|
||||
apply_delay_pattern,
|
||||
)
|
||||
for _ in range(3)
|
||||
]
|
||||
)
|
||||
|
||||
num_codebooks = 8
|
||||
bos = BOC_ID
|
||||
eos_stream = EOC_ID
|
||||
results = asyncio.run(run())
|
||||
assert calls == 1
|
||||
assert sum(int(inflight_wait) for _, _, inflight_wait in results) == 2
|
||||
for codes, cache_hit, _ in results:
|
||||
assert cache_hit is False
|
||||
assert torch.equal(codes, torch.arange(16, dtype=torch.long).reshape(2, 8) + 1)
|
||||
|
||||
state: dict = {
|
||||
"num_delay": 0,
|
||||
"num_remaining_delays": None,
|
||||
"audio_out_ids": None,
|
||||
"last_codes": torch.full((num_codebooks,), bos, dtype=torch.long),
|
||||
"should_terminate": False,
|
||||
}
|
||||
|
||||
emitted: list[torch.Tensor] = []
|
||||
for step_codes in sampled_codes_per_step:
|
||||
if state["should_terminate"]:
|
||||
break
|
||||
|
||||
num_delay = state["num_delay"]
|
||||
num_remaining_delays = state["num_remaining_delays"]
|
||||
this_codes = step_codes.clone()
|
||||
|
||||
if num_delay + 1 < num_codebooks:
|
||||
this_codes[num_delay + 1 :] = bos
|
||||
num_delay += 1
|
||||
|
||||
if num_remaining_delays is not None:
|
||||
this_codes[: num_codebooks - num_remaining_delays] = eos_stream
|
||||
num_remaining_delays -= 1
|
||||
else:
|
||||
eos_positions = (this_codes == eos_stream).nonzero(as_tuple=False).reshape(-1)
|
||||
if eos_positions.numel() > 0:
|
||||
last_eos_idx = int(eos_positions[-1].item())
|
||||
this_codes[: last_eos_idx + 1] = eos_stream
|
||||
num_remaining_delays = num_codebooks - last_eos_idx - 1
|
||||
|
||||
if num_remaining_delays is not None and num_remaining_delays <= 0:
|
||||
state["num_delay"] = 0
|
||||
state["num_remaining_delays"] = None
|
||||
state["should_terminate"] = True
|
||||
continue
|
||||
|
||||
state["num_delay"] = num_delay
|
||||
state["num_remaining_delays"] = num_remaining_delays
|
||||
state["last_codes"] = this_codes.clone()
|
||||
emitted.append(this_codes)
|
||||
|
||||
return emitted
|
||||
|
||||
|
||||
class TestRampDownOffByOne:
|
||||
"""Regression tests for the done-before-emit off-by-one bug.
|
||||
|
||||
When EOC fires early, the ramp-down must emit enough frames so that
|
||||
Stage1's _revert_delay_pattern receives T >= Q = 8.
|
||||
"""
|
||||
|
||||
def test_eoc_at_step0_terminates(self):
|
||||
"""EOC on cb0 at step 0 terminates; T < Q is handled by Stage1 defensive try/except."""
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.higgs_audio_v3_talker import EOC_ID
|
||||
|
||||
Q = 8
|
||||
steps = []
|
||||
for s in range(20):
|
||||
codes = torch.randint(0, 1024, (Q,))
|
||||
if s == 0:
|
||||
codes[0] = EOC_ID
|
||||
steps.append(codes)
|
||||
|
||||
emitted = _simulate_delay_state_machine(steps)
|
||||
assert len(emitted) == Q - 1, f"EOC at step 0: expected {Q - 1} emitted frames, got {len(emitted)}"
|
||||
|
||||
def test_eoc_at_step1_emits_enough_frames(self):
|
||||
"""EOC on cb0 at step 1 must produce >= Q emitted frames."""
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.higgs_audio_v3_talker import EOC_ID
|
||||
|
||||
Q = 8
|
||||
steps = []
|
||||
for s in range(20):
|
||||
codes = torch.randint(0, 1024, (Q,))
|
||||
if s == 1:
|
||||
codes[0] = EOC_ID
|
||||
steps.append(codes)
|
||||
|
||||
emitted = _simulate_delay_state_machine(steps)
|
||||
assert len(emitted) >= Q, f"EOC at step 1: expected >= {Q} emitted frames, got {len(emitted)}"
|
||||
|
||||
def test_eoc_after_full_rampup_emits_enough(self):
|
||||
"""EOC after all codebooks are active produces T >> Q."""
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.higgs_audio_v3_talker import EOC_ID
|
||||
|
||||
Q = 8
|
||||
eoc_step = 10
|
||||
steps = []
|
||||
for s in range(30):
|
||||
codes = torch.randint(0, 1024, (Q,))
|
||||
if s == eoc_step:
|
||||
codes[0] = EOC_ID
|
||||
steps.append(codes)
|
||||
|
||||
emitted = _simulate_delay_state_machine(steps)
|
||||
assert len(emitted) >= Q, f"EOC at step {eoc_step}: expected >= {Q} emitted frames, got {len(emitted)}"
|
||||
|
||||
def test_last_rampdown_frame_not_all_negative(self):
|
||||
"""The last emitted ramp-down frame must contain real codes, not -1."""
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.higgs_audio_v3_talker import EOC_ID
|
||||
|
||||
Q = 8
|
||||
steps = []
|
||||
for s in range(20):
|
||||
codes = torch.randint(0, 1024, (Q,))
|
||||
if s == 1:
|
||||
codes[0] = EOC_ID
|
||||
steps.append(codes)
|
||||
|
||||
emitted = _simulate_delay_state_machine(steps)
|
||||
last = emitted[-1]
|
||||
assert (last >= 0).all(), f"Last ramp-down frame contains negative values: {last.tolist()}"
|
||||
|
||||
def test_emitted_frames_revertible_by_stage1(self):
|
||||
"""Emitted frames from EOC-at-step-1 must be revertible by _revert_delay_pattern."""
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.higgs_audio_v3_talker import EOC_ID
|
||||
from vllm_omni.model_executor.stage_input_processors.higgs_audio_v3 import (
|
||||
_revert_delay_pattern,
|
||||
cached, cache_hit, inflight_wait = asyncio.run(
|
||||
serving._resolve_higgs_audio_v3_ref_codes(
|
||||
"ref-a",
|
||||
object(),
|
||||
24000,
|
||||
encode_reference_audio,
|
||||
apply_delay_pattern,
|
||||
)
|
||||
|
||||
Q = 8
|
||||
steps = []
|
||||
for s in range(20):
|
||||
codes = torch.randint(0, 1024, (Q,))
|
||||
if s == 1:
|
||||
codes[0] = EOC_ID
|
||||
steps.append(codes)
|
||||
|
||||
emitted = _simulate_delay_state_machine(steps)
|
||||
# Stack to [T, Q], transpose to [Q, T]
|
||||
codes_qt = torch.stack(emitted, dim=0).t().contiguous()
|
||||
assert codes_qt.shape[0] == Q
|
||||
assert codes_qt.shape[1] >= Q
|
||||
|
||||
# Must not raise
|
||||
reverted = _revert_delay_pattern(codes_qt)
|
||||
assert reverted.shape[0] == Q
|
||||
assert reverted.shape[1] == codes_qt.shape[1] - Q + 1
|
||||
|
||||
|
||||
class TestStage1DefensiveHandling:
|
||||
"""Stage1 must not crash the server when receiving T < Q frames."""
|
||||
|
||||
def test_revert_delay_pattern_raises_on_insufficient_frames(self):
|
||||
"""_revert_delay_pattern correctly raises ValueError for T < Q."""
|
||||
from vllm_omni.model_executor.stage_input_processors.higgs_audio_v3 import (
|
||||
_revert_delay_pattern,
|
||||
)
|
||||
|
||||
codes = torch.zeros(8, 7) # T=7 < Q=8
|
||||
with pytest.raises(ValueError, match="Not enough frames"):
|
||||
_revert_delay_pattern(codes)
|
||||
|
||||
def test_talker2code2wav_survives_insufficient_frames(self):
|
||||
"""talker2code2wav must not crash when audio_codes has T < Q frames."""
|
||||
from vllm_omni.model_executor.stage_input_processors.higgs_audio_v3 import (
|
||||
talker2code2wav,
|
||||
)
|
||||
|
||||
# Build a fake talker output with only 7 frames (T=7 < Q=8)
|
||||
class FakeOutput:
|
||||
def __init__(self):
|
||||
self.multimodal_output = {
|
||||
"codes": {"audio": torch.zeros(7, 8, dtype=torch.long)},
|
||||
}
|
||||
|
||||
class FakeTalkerOutput:
|
||||
finished = True
|
||||
outputs = [FakeOutput()]
|
||||
|
||||
# Must not raise — should return gracefully
|
||||
result = talker2code2wav([FakeTalkerOutput()])
|
||||
assert len(result) == 1
|
||||
# Result is an OmniTokensPrompt with empty prompt_token_ids
|
||||
out = result[0]
|
||||
token_ids = out.prompt_token_ids if hasattr(out, "prompt_token_ids") else out.get("prompt_token_ids", [])
|
||||
assert token_ids == []
|
||||
|
||||
|
||||
# ---- Regression: buffer growth + prefill guard ----
|
||||
|
||||
|
||||
class TestBufferGrowthPreservesState:
|
||||
"""Bug #1: buffer growth in _apply_audio_feedback must not wipe existing state."""
|
||||
|
||||
def _make_feedback_talker_with_buffers(self):
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3 import higgs_audio_v3_talker as mod
|
||||
|
||||
embed = mod.HiggsFusedMultiTextEmbedding(num_codebooks=8, vocab_size=1026, hidden_size=16)
|
||||
torch.nn.init.ones_(embed.weight)
|
||||
|
||||
class FakeTalker:
|
||||
num_codebooks = 8
|
||||
codebook_size = 1026
|
||||
multimodal_embedding = embed
|
||||
|
||||
t = FakeTalker()
|
||||
t._decode_last_codes = torch.zeros(4, 8, dtype=torch.long)
|
||||
t._decode_has_codes = torch.zeros(4, dtype=torch.bool)
|
||||
t._apply_audio_feedback = mod.HiggsAudioV3TalkerForConditionalGeneration._apply_audio_feedback.__get__(t)
|
||||
return t
|
||||
|
||||
def test_buffer_growth_preserves_existing_codes(self):
|
||||
"""Growing the buffer must copy old _decode_last_codes data."""
|
||||
t = self._make_feedback_talker_with_buffers()
|
||||
# Set state for slot 2
|
||||
t._decode_last_codes[2] = torch.tensor([100, 200, 300, 400, 500, 600, 700, 800])
|
||||
t._decode_has_codes[2] = True
|
||||
|
||||
# Trigger growth by calling with bs=8 > current capacity 4
|
||||
hidden = torch.randn(8, 16)
|
||||
t._apply_audio_feedback(hidden, torch.zeros(8, dtype=torch.long))
|
||||
|
||||
# After growth, slot 2 data must still be present
|
||||
assert t._decode_has_codes[2].item() is True
|
||||
assert t._decode_last_codes[2, 0].item() == 100
|
||||
|
||||
def test_buffer_growth_new_slots_are_inactive(self):
|
||||
"""New slots from buffer growth must have has_codes=False."""
|
||||
t = self._make_feedback_talker_with_buffers()
|
||||
t._decode_has_codes[1] = True
|
||||
|
||||
hidden = torch.randn(8, 16)
|
||||
t._apply_audio_feedback(hidden, torch.zeros(8, dtype=torch.long))
|
||||
|
||||
# Slots 4-7 (new) must be inactive
|
||||
assert not t._decode_has_codes[4].item()
|
||||
assert not t._decode_has_codes[7].item()
|
||||
# Slot 1 (old) must still be active
|
||||
assert t._decode_has_codes[1].item()
|
||||
|
||||
|
||||
class TestPrefillGuard:
|
||||
"""Bug #2: _apply_audio_feedback must NOT run during prefill.
|
||||
|
||||
The prefill detection uses attn_metadata.max_query_len (== 1 for decode,
|
||||
> 1 for prefill). This correctly handles concurrent decode (N requests,
|
||||
each contributing 1 token → numel=N but max_query_len=1).
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _is_prefill_via_max_query_len(max_query_len: int | None, input_ids: torch.Tensor) -> bool:
|
||||
"""Mirror the actual detection logic in forward()."""
|
||||
if max_query_len is not None:
|
||||
return int(max_query_len) > 1
|
||||
return input_ids is not None and int(input_ids.numel()) > 1
|
||||
|
||||
def test_prefill_detected_via_max_query_len(self):
|
||||
"""Prefill: max_query_len > 1 → is_prefill=True."""
|
||||
input_ids = torch.tensor([1, 2, 3, 4, 5])
|
||||
assert self._is_prefill_via_max_query_len(5, input_ids) is True
|
||||
|
||||
def test_single_decode_detected_via_max_query_len(self):
|
||||
"""Single-request decode: max_query_len=1 → is_prefill=False."""
|
||||
input_ids = torch.tensor([42])
|
||||
assert self._is_prefill_via_max_query_len(1, input_ids) is False
|
||||
|
||||
def test_concurrent_decode_not_misclassified(self):
|
||||
"""Multi-request decode: numel=4 but max_query_len=1 → is_prefill=False.
|
||||
|
||||
This is the regression caught by reviewer: with c=4 concurrent decode
|
||||
requests, input_ids.numel()==4 but each request contributes exactly 1
|
||||
token, so max_query_len==1 and audio feedback must still apply.
|
||||
"""
|
||||
input_ids = torch.tensor([42, 43, 44, 45]) # 4 concurrent decode
|
||||
assert self._is_prefill_via_max_query_len(1, input_ids) is False
|
||||
|
||||
def test_fallback_when_max_query_len_unavailable(self):
|
||||
"""When attn_metadata is unavailable, fall back to numel heuristic."""
|
||||
prefill_ids = torch.tensor([1, 2, 3])
|
||||
assert self._is_prefill_via_max_query_len(None, prefill_ids) is True
|
||||
decode_ids = torch.tensor([42])
|
||||
assert self._is_prefill_via_max_query_len(None, decode_ids) is False
|
||||
assert calls == 1
|
||||
assert cache_hit is True
|
||||
assert inflight_wait is False
|
||||
assert torch.equal(cached, torch.arange(16, dtype=torch.long).reshape(2, 8) + 1)
|
||||
@@ -0,0 +1,53 @@
|
||||
# Higgs-Audio V3 Deploy Profiles
|
||||
|
||||
Higgs-Audio V3 has two Stage 0 graph profiles. They are intentionally separate
|
||||
because the graph paths are mutually exclusive.
|
||||
|
||||
## High Throughput
|
||||
|
||||
Use `higgs_multimodal_qwen3_high_throughput.yaml` for medium/high concurrency
|
||||
serving. This profile keeps Stage 0 `enforce_eager: true`, which preserves the
|
||||
Higgs-specific local MLP CUDA graph path. It is the default production profile
|
||||
for throughput-oriented serving.
|
||||
|
||||
`higgs_multimodal_qwen3.yaml` is kept as the auto-discovered default deploy
|
||||
config for `model_type=higgs_multimodal_qwen3`, and matches the high-throughput
|
||||
profile.
|
||||
|
||||
```bash
|
||||
vllm-omni serve bosonai/higgs-audio-v3-tts-4b \
|
||||
--omni --trust-remote-code \
|
||||
--deploy-config vllm_omni/deploy/higgs_multimodal_qwen3_high_throughput.yaml
|
||||
```
|
||||
|
||||
## Low Latency
|
||||
|
||||
Use `higgs_multimodal_qwen3_low_latency.yaml` for low-concurrency serving
|
||||
(for example c1-c4) where Stage 0 decode launch overhead dominates. This profile
|
||||
sets Stage 0 `enforce_eager: false` and explicitly enables vLLM
|
||||
`FULL_DECODE_ONLY` CUDA graph:
|
||||
|
||||
```yaml
|
||||
compilation_config:
|
||||
cudagraph_capture_sizes: [1, 2, 4, 8, 16]
|
||||
cudagraph_mode: FULL_DECODE_ONLY
|
||||
cudagraph_num_of_warmups: 1
|
||||
```
|
||||
|
||||
FULL_DECODE is controlled by deploy configuration, not by an environment
|
||||
variable. When this external decode graph is active, the Higgs talker disables
|
||||
the local MLP CUDA graph automatically.
|
||||
|
||||
```bash
|
||||
vllm-omni serve bosonai/higgs-audio-v3-tts-4b \
|
||||
--omni --trust-remote-code \
|
||||
--deploy-config vllm_omni/deploy/higgs_multimodal_qwen3_low_latency.yaml
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Stage 1 remains `enforce_eager: true` in both profiles.
|
||||
- Keep `VLLM_USE_DEEP_GEMM=0` and `VLLM_MOE_USE_DEEP_GEMM=0` for this model
|
||||
unless DeepGEMM support is revalidated.
|
||||
- Revalidate end-to-end throughput and audio quality before changing the default
|
||||
auto-discovered config.
|
||||
@@ -1,5 +1,11 @@
|
||||
# Higgs-audio v3 deploy: Stage 0 (talker) -> Stage 1 (code2wav) via shared memory.
|
||||
#
|
||||
# Auto-discovered default for model_type=higgs_multimodal_qwen3. This file is
|
||||
# the high-throughput profile and intentionally keeps Stage 0 enforce_eager=true
|
||||
# so the Higgs-specific local MLP CUDA graph path remains active. For low
|
||||
# concurrency latency-sensitive serving, use
|
||||
# higgs_multimodal_qwen3_low_latency.yaml explicitly.
|
||||
#
|
||||
# Plain TTS only (no voice cloning). async_chunk streams codec chunks from
|
||||
# Stage 0 to Stage 1 as they are produced (TTFA approaches one AR loop's
|
||||
# worth of work). Stage 1 honors meta.left_context_size + meta.right_holdback_size
|
||||
@@ -32,6 +38,7 @@ stages:
|
||||
max_num_seqs: 16
|
||||
gpu_memory_utilization: 0.6
|
||||
enforce_eager: true
|
||||
attention_backend: FLASHINFER
|
||||
trust_remote_code: true
|
||||
# Stage-0 prefix caching is on by default. The talker's HS opt-out and
|
||||
# deferred ``codes.audio`` mm-output write (see higgs_audio_v3_talker.py)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# Higgs-audio v3 deploy, high-throughput profile.
|
||||
#
|
||||
# This is the default production profile for medium/high concurrency serving.
|
||||
# Stage 0 keeps enforce_eager=true so Higgs' local MLP CUDA graph remains active.
|
||||
# Do not enable vLLM FULL_DECODE graph here: it replaces the local MLP graph
|
||||
# path instead of stacking on top of it.
|
||||
#
|
||||
# Plain TTS only (no voice cloning). async_chunk streams codec chunks from
|
||||
# Stage 0 to Stage 1 as they are produced (TTFA approaches one AR loop's
|
||||
# worth of work). Stage 1 honors meta.left_context_size + meta.right_holdback_size
|
||||
# so the per-chunk codec windows stitch into a coherent PCM stream.
|
||||
async_chunk: true
|
||||
|
||||
connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
# Stream PCM bytes back to the client as Stage 1 emits each chunk.
|
||||
codec_streaming: true
|
||||
# Streaming chunk shape for talker2code2wav_async_chunk:
|
||||
# codec_chunk_frames steady-state new frames per emit
|
||||
# codec_left_context_frames sliding past context to avoid leading-edge artifacts
|
||||
# codec_right_holdback_frames trailing frames held back so the codec
|
||||
# has future context for emit-region tail samples
|
||||
# initial_codec_chunk_frames smaller first chunk to minimize TTFA
|
||||
codec_chunk_frames: 25
|
||||
codec_left_context_frames: 25
|
||||
codec_right_holdback_frames: 4
|
||||
initial_codec_chunk_frames: 1
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 5000
|
||||
connector_get_max_wait: 500
|
||||
|
||||
stages:
|
||||
- stage_id: 0
|
||||
max_num_seqs: 64
|
||||
gpu_memory_utilization: 0.6
|
||||
enforce_eager: true
|
||||
attention_backend: FLASHINFER
|
||||
trust_remote_code: true
|
||||
# Stage-0 prefix caching is on by default. The talker's HS opt-out and
|
||||
# deferred ``codes.audio`` mm-output write (see higgs_audio_v3_talker.py)
|
||||
# keep per-step bookkeeping negligible; any shared text prefix across
|
||||
# concurrent requests is a net throughput win. Stage 1 (codec decoder)
|
||||
# has no KV cache and stays disabled.
|
||||
enable_prefix_caching: true
|
||||
async_scheduling: false
|
||||
max_num_batched_tokens: 4096
|
||||
max_model_len: 8192
|
||||
devices: "0"
|
||||
output_connectors:
|
||||
to_stage_1: connector_of_shared_memory
|
||||
default_sampling_params:
|
||||
temperature: 1.0
|
||||
top_p: 0.95
|
||||
top_k: 50
|
||||
max_tokens: 2048
|
||||
seed: 42
|
||||
repetition_penalty: 1.0
|
||||
|
||||
- stage_id: 1
|
||||
max_num_seqs: 16
|
||||
gpu_memory_utilization: 0.25
|
||||
enforce_eager: true
|
||||
trust_remote_code: true
|
||||
enable_prefix_caching: false
|
||||
async_scheduling: false
|
||||
max_num_batched_tokens: 65536
|
||||
max_model_len: 65536
|
||||
devices: "0"
|
||||
input_connectors:
|
||||
from_stage_0: connector_of_shared_memory
|
||||
default_sampling_params:
|
||||
temperature: 0.0
|
||||
top_p: 1.0
|
||||
top_k: -1
|
||||
max_tokens: 65536
|
||||
seed: 42
|
||||
repetition_penalty: 1.0
|
||||
@@ -0,0 +1,85 @@
|
||||
# Higgs-audio v3 deploy, low-latency profile.
|
||||
#
|
||||
# Use this profile for low-concurrency serving (for example c1-c4) where Stage 0
|
||||
# decode launch overhead dominates. Stage 0 enables vLLM FULL_DECODE_ONLY CUDA
|
||||
# graph through YAML configuration:
|
||||
#
|
||||
# enforce_eager: false
|
||||
# compilation_config.cudagraph_mode: FULL_DECODE_ONLY
|
||||
#
|
||||
# This is not controlled by an environment variable. The Higgs talker disables
|
||||
# its local MLP CUDA graph automatically when the external decode graph is active,
|
||||
# because the two graph paths are mutually exclusive.
|
||||
#
|
||||
# Plain TTS only (no voice cloning). async_chunk streams codec chunks from
|
||||
# Stage 0 to Stage 1 as they are produced (TTFA approaches one AR loop's
|
||||
# worth of work). Stage 1 honors meta.left_context_size + meta.right_holdback_size
|
||||
# so the per-chunk codec windows stitch into a coherent PCM stream.
|
||||
async_chunk: true
|
||||
|
||||
connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
# Stream PCM bytes back to the client as Stage 1 emits each chunk.
|
||||
codec_streaming: true
|
||||
# Streaming chunk shape for talker2code2wav_async_chunk:
|
||||
# codec_chunk_frames steady-state new frames per emit
|
||||
# codec_left_context_frames sliding past context to avoid leading-edge artifacts
|
||||
# codec_right_holdback_frames trailing frames held back so the codec
|
||||
# has future context for emit-region tail samples
|
||||
# initial_codec_chunk_frames smaller first chunk to minimize TTFA
|
||||
codec_chunk_frames: 25
|
||||
codec_left_context_frames: 25
|
||||
codec_right_holdback_frames: 4
|
||||
initial_codec_chunk_frames: 1
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 5000
|
||||
connector_get_max_wait: 500
|
||||
|
||||
stages:
|
||||
- stage_id: 0
|
||||
max_num_seqs: 16
|
||||
gpu_memory_utilization: 0.6
|
||||
enforce_eager: false
|
||||
attention_backend: FLASHINFER
|
||||
trust_remote_code: true
|
||||
enable_prefix_caching: true
|
||||
async_scheduling: false
|
||||
compilation_config:
|
||||
cudagraph_capture_sizes: [1, 2, 4, 8, 16]
|
||||
cudagraph_mode: FULL_DECODE_ONLY
|
||||
cudagraph_num_of_warmups: 1
|
||||
max_num_batched_tokens: 4096
|
||||
max_model_len: 8192
|
||||
devices: "0"
|
||||
output_connectors:
|
||||
to_stage_1: connector_of_shared_memory
|
||||
default_sampling_params:
|
||||
temperature: 1.0
|
||||
top_p: 0.95
|
||||
top_k: 50
|
||||
max_tokens: 2048
|
||||
seed: 42
|
||||
repetition_penalty: 1.0
|
||||
|
||||
- stage_id: 1
|
||||
max_num_seqs: 16
|
||||
gpu_memory_utilization: 0.25
|
||||
enforce_eager: true
|
||||
trust_remote_code: true
|
||||
enable_prefix_caching: false
|
||||
async_scheduling: false
|
||||
max_num_batched_tokens: 65536
|
||||
max_model_len: 65536
|
||||
devices: "0"
|
||||
input_connectors:
|
||||
from_stage_0: connector_of_shared_memory
|
||||
default_sampling_params:
|
||||
temperature: 0.0
|
||||
top_p: 1.0
|
||||
top_k: -1
|
||||
max_tokens: 65536
|
||||
seed: 42
|
||||
repetition_penalty: 1.0
|
||||
@@ -29,6 +29,7 @@ class SharedMemoryConnector(OmniConnectorBase):
|
||||
self.stage_id = config.get("stage_id", -1)
|
||||
self.device = config.get("device", "cuda:0")
|
||||
self.threshold = int(config.get("shm_threshold_bytes", 65536))
|
||||
self.inline_small_payloads = bool(config.get("inline_small_payloads", False))
|
||||
self._pending_keys: set[str] = set()
|
||||
self._metrics = {
|
||||
"puts": 0,
|
||||
@@ -53,9 +54,11 @@ class SharedMemoryConnector(OmniConnectorBase):
|
||||
payload = self.serialize_obj(data)
|
||||
size = len(payload)
|
||||
|
||||
# Currently, we always use SHM.
|
||||
if True:
|
||||
# Use Shared Memory
|
||||
# The legacy async-chunk adapter transfers only the connector key
|
||||
# across stages; it cannot deliver inline metadata to the receiver.
|
||||
# Keep key-addressed SHM as the default and only inline payloads
|
||||
# when the caller explicitly enables the metadata-aware path.
|
||||
if size >= self.threshold or not self.inline_small_payloads:
|
||||
lock_file = f"/dev/shm/shm_{put_key}_lockfile.lock"
|
||||
with open(lock_file, "wb+") as lockf:
|
||||
fcntl.flock(lockf, fcntl.LOCK_EX)
|
||||
@@ -67,9 +70,8 @@ class SharedMemoryConnector(OmniConnectorBase):
|
||||
self._pending_keys.add(put_key)
|
||||
self._metrics["shm_writes"] += 1
|
||||
else:
|
||||
# Inline - pass bytes directly to avoid double serialization of the object
|
||||
# We already serialized it to check size, so we pass the bytes.
|
||||
# The Queue will pickle these bytes (fast), avoiding re-serializing the complex object.
|
||||
# Inline small payloads to avoid the mmap + file-lock overhead
|
||||
# that dominates codec chunk transfers.
|
||||
metadata = {"inline_bytes": payload, "size": size}
|
||||
self._metrics["inline_writes"] += 1
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ _MING_TTS_MODEL_STAGES = {"ming_tts"}
|
||||
_MOSS_TTS_MODEL_STAGES = {"moss_tts_nano"}
|
||||
_MOSS_TTS_FULL_MODEL_STAGES = {"moss_tts", "moss_tts_codec"}
|
||||
_HIGGS_AUDIO_V2_TTS_MODEL_STAGES = {"higgs_audio_v2"}
|
||||
_HIGGS_AUDIO_V3_TTS_MODEL_STAGES = {"higgs_audio_v3"}
|
||||
_HIGGS_V3_TTS_MODEL_STAGES = {"higgs_audio_v3"}
|
||||
_GLM_TTS_MODEL_STAGES = {"glm_tts"}
|
||||
_TTS_MODEL_STAGES: set[str] = (
|
||||
_VOXTRAL_TTS_MODEL_STAGES
|
||||
@@ -87,7 +87,7 @@ _TTS_MODEL_STAGES: set[str] = (
|
||||
| _COSYVOICE3_TTS_MODEL_STAGES
|
||||
| _OMNIVOICE_TTS_MODEL_STAGES
|
||||
| _HIGGS_AUDIO_V2_TTS_MODEL_STAGES
|
||||
| _HIGGS_AUDIO_V3_TTS_MODEL_STAGES
|
||||
| _HIGGS_V3_TTS_MODEL_STAGES
|
||||
| _COVO_AUDIO_MODEL_STAGES
|
||||
| _VOXCPM2_TTS_MODEL_STAGES
|
||||
| _MING_TTS_MODEL_STAGES
|
||||
@@ -123,6 +123,8 @@ _REF_AUDIO_MIN_DURATION = 1.0 # seconds
|
||||
_REF_AUDIO_MAX_DURATION = 30.0 # seconds
|
||||
_REF_AUDIO_RESOLVE_CACHE_MAX_ENTRIES = 256
|
||||
_REF_AUDIO_RESOLVE_CACHE_MAX_BYTES = 256 * 1024 * 1024
|
||||
_HIGGS_V3_REF_CODE_CACHE_MAX_ENTRIES = 256
|
||||
_HIGGS_V3_REF_CODE_CACHE_MAX_BYTES = 64 * 1024 * 1024
|
||||
_QWEN3_TTS_REF_AUDIO_CACHE_KEY = "_qwen3_tts_ref_audio_cache_key"
|
||||
_TTS_MAX_INSTRUCTIONS_LENGTH = 500
|
||||
_TTS_MAX_NEW_TOKENS_MIN = 1
|
||||
@@ -288,6 +290,9 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
self._ref_audio_resolve_cache_max_bytes = _REF_AUDIO_RESOLVE_CACHE_MAX_BYTES
|
||||
self._ref_audio_model_artifact_ready: set[str] = set()
|
||||
self._request_ref_audio_artifact_keys: dict[str, str] = {}
|
||||
self._higgs_audio_v3_ref_code_cache: OrderedDict[str, tuple[torch.Tensor, int]] = OrderedDict()
|
||||
self._higgs_audio_v3_ref_code_cache_bytes = 0
|
||||
self._higgs_audio_v3_ref_code_inflight: dict[str, asyncio.Task[torch.Tensor]] = {}
|
||||
self._speaker_cache = get_speaker_cache()
|
||||
self._last_upload_ts = 0
|
||||
self._upload_lock = asyncio.Lock()
|
||||
@@ -646,7 +651,7 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
return "moss_tts"
|
||||
if model_stage in _HIGGS_AUDIO_V2_TTS_MODEL_STAGES:
|
||||
return "higgs_audio_v2"
|
||||
if model_stage in _HIGGS_AUDIO_V3_TTS_MODEL_STAGES:
|
||||
if model_stage in _HIGGS_V3_TTS_MODEL_STAGES:
|
||||
return "higgs_audio_v3"
|
||||
if model_stage in _GLM_TTS_MODEL_STAGES:
|
||||
return "glm_tts"
|
||||
@@ -2018,17 +2023,18 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
apply_delay_pattern,
|
||||
encode_reference_audio,
|
||||
)
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.ref_audio_cache import (
|
||||
cached_encode_reference_audio,
|
||||
)
|
||||
|
||||
wav_list, sr = await self._resolve_ref_audio(request.ref_audio)
|
||||
artifact_key = self._get_resolved_ref_audio_artifact_key(request.ref_audio)
|
||||
wav = np.asarray(wav_list, dtype=np.float32)
|
||||
# Cache the codec encode by content hash so identical ref_audio
|
||||
# payloads skip the encode and reuse the same [num_frames,
|
||||
# num_codebooks] codes tensor across requests.
|
||||
ref_codes_raw = await asyncio.to_thread(cached_encode_reference_audio, wav, int(sr), encode_reference_audio)
|
||||
ref_codes_delayed = apply_delay_pattern(ref_codes_raw)
|
||||
ref_codes_delayed, cache_hit, inflight_wait = await self._resolve_higgs_audio_v3_ref_codes(
|
||||
artifact_key,
|
||||
wav,
|
||||
int(sr),
|
||||
encode_reference_audio,
|
||||
apply_delay_pattern,
|
||||
)
|
||||
del cache_hit, inflight_wait
|
||||
|
||||
prompt_ids = adapter.build_prompt(
|
||||
request.input,
|
||||
@@ -2044,6 +2050,68 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
}
|
||||
return prompt
|
||||
|
||||
async def _resolve_higgs_audio_v3_ref_codes(
|
||||
self,
|
||||
artifact_key: str | None,
|
||||
wav: np.ndarray,
|
||||
sr: int,
|
||||
encode_reference_audio,
|
||||
apply_delay_pattern,
|
||||
) -> tuple[torch.Tensor, bool, bool]:
|
||||
ref_codes_delayed = self._get_higgs_audio_v3_ref_codes(artifact_key)
|
||||
if ref_codes_delayed is not None:
|
||||
return ref_codes_delayed, True, False
|
||||
if not artifact_key:
|
||||
ref_codes_raw = await asyncio.to_thread(encode_reference_audio, wav, sr)
|
||||
return apply_delay_pattern(ref_codes_raw), False, False
|
||||
|
||||
task = self._higgs_audio_v3_ref_code_inflight.get(artifact_key)
|
||||
if task is not None:
|
||||
return (await task).clone(), False, True
|
||||
|
||||
async def _encode_and_cache() -> torch.Tensor:
|
||||
ref_codes_raw = await asyncio.to_thread(encode_reference_audio, wav, sr)
|
||||
delayed = apply_delay_pattern(ref_codes_raw)
|
||||
self._put_higgs_audio_v3_ref_codes(artifact_key, delayed)
|
||||
cached = self._get_higgs_audio_v3_ref_codes(artifact_key)
|
||||
return cached if cached is not None else delayed.detach().to("cpu", dtype=torch.long).contiguous()
|
||||
|
||||
task = asyncio.create_task(_encode_and_cache())
|
||||
self._higgs_audio_v3_ref_code_inflight[artifact_key] = task
|
||||
try:
|
||||
return (await task).clone(), False, False
|
||||
finally:
|
||||
if self._higgs_audio_v3_ref_code_inflight.get(artifact_key) is task:
|
||||
self._higgs_audio_v3_ref_code_inflight.pop(artifact_key, None)
|
||||
|
||||
def _get_higgs_audio_v3_ref_codes(self, artifact_key: str | None) -> torch.Tensor | None:
|
||||
if not artifact_key:
|
||||
return None
|
||||
cached = self._higgs_audio_v3_ref_code_cache.get(artifact_key)
|
||||
if cached is None:
|
||||
return None
|
||||
self._higgs_audio_v3_ref_code_cache.move_to_end(artifact_key)
|
||||
return cached[0].clone()
|
||||
|
||||
def _put_higgs_audio_v3_ref_codes(self, artifact_key: str, codes: torch.Tensor) -> None:
|
||||
if _HIGGS_V3_REF_CODE_CACHE_MAX_ENTRIES <= 0 or _HIGGS_V3_REF_CODE_CACHE_MAX_BYTES <= 0 or not artifact_key:
|
||||
return
|
||||
cached_codes = codes.detach().to("cpu", dtype=torch.long).contiguous()
|
||||
size = int(cached_codes.numel() * cached_codes.element_size())
|
||||
if size > _HIGGS_V3_REF_CODE_CACHE_MAX_BYTES:
|
||||
return
|
||||
previous = self._higgs_audio_v3_ref_code_cache.pop(artifact_key, None)
|
||||
if previous is not None:
|
||||
self._higgs_audio_v3_ref_code_cache_bytes -= previous[1]
|
||||
self._higgs_audio_v3_ref_code_cache[artifact_key] = (cached_codes, size)
|
||||
self._higgs_audio_v3_ref_code_cache_bytes += size
|
||||
while len(self._higgs_audio_v3_ref_code_cache) > _HIGGS_V3_REF_CODE_CACHE_MAX_ENTRIES:
|
||||
_, (_, old_size) = self._higgs_audio_v3_ref_code_cache.popitem(last=False)
|
||||
self._higgs_audio_v3_ref_code_cache_bytes -= old_size
|
||||
while self._higgs_audio_v3_ref_code_cache_bytes > _HIGGS_V3_REF_CODE_CACHE_MAX_BYTES:
|
||||
_, (_, old_size) = self._higgs_audio_v3_ref_code_cache.popitem(last=False)
|
||||
self._higgs_audio_v3_ref_code_cache_bytes -= old_size
|
||||
|
||||
async def _resolve_higgs_audio_v3_adapter(self):
|
||||
"""Lazy-load the tokenizer adapter for higgs_audio_v3."""
|
||||
cached = getattr(self, "_higgs_audio_v3_adapter", None)
|
||||
|
||||
@@ -162,6 +162,54 @@ def _build_delay_pattern(codes: torch.Tensor) -> torch.Tensor:
|
||||
_ENCODER_CACHE: Any | None = None
|
||||
_K2_OMNIVOICE_REPO = "k2-fsa/OmniVoice"
|
||||
_K2_OMNIVOICE_SUBDIR = "audio_tokenizer"
|
||||
_AUDIO_TOKENIZER_PATH_ENVS = (
|
||||
"HIGGS_AUDIO_TOKENIZER_PATH",
|
||||
"HIGGS_AUDIO_V2_TOKENIZER_PATH",
|
||||
)
|
||||
|
||||
|
||||
def _normalize_audio_tokenizer_dir(path: str) -> str | None:
|
||||
if not path:
|
||||
return None
|
||||
expanded = os.path.abspath(os.path.expanduser(path))
|
||||
if os.path.isfile(os.path.join(expanded, "config.json")):
|
||||
return expanded
|
||||
nested = os.path.join(expanded, _K2_OMNIVOICE_SUBDIR)
|
||||
if os.path.isfile(os.path.join(nested, "config.json")):
|
||||
return nested
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_audio_tokenizer_dir() -> str | None:
|
||||
"""Resolve the Higgs codec encoder dir without requiring online HF access.
|
||||
|
||||
Voice cloning runs in the API server process. Production H20 jobs are often
|
||||
offline, so prefer explicit local directories and already-populated HF cache
|
||||
before falling back to ``snapshot_download``.
|
||||
"""
|
||||
for env_name in _AUDIO_TOKENIZER_PATH_ENVS:
|
||||
candidate = _normalize_audio_tokenizer_dir(os.getenv(env_name, ""))
|
||||
if candidate is not None:
|
||||
return candidate
|
||||
|
||||
from huggingface_hub import try_to_load_from_cache
|
||||
|
||||
cached_config = try_to_load_from_cache(
|
||||
repo_id=_K2_OMNIVOICE_REPO,
|
||||
filename=f"{_K2_OMNIVOICE_SUBDIR}/config.json",
|
||||
)
|
||||
if isinstance(cached_config, str) and os.path.isfile(cached_config):
|
||||
return os.path.dirname(cached_config)
|
||||
|
||||
from huggingface_hub.constants import HF_HUB_CACHE
|
||||
|
||||
safe = _K2_OMNIVOICE_REPO.replace("/", "--")
|
||||
snapshots_dir = os.path.join(HF_HUB_CACHE, f"models--{safe}", "snapshots")
|
||||
if os.path.isdir(snapshots_dir):
|
||||
for rev in os.listdir(snapshots_dir):
|
||||
candidate = _normalize_audio_tokenizer_dir(os.path.join(snapshots_dir, rev))
|
||||
if candidate is not None:
|
||||
return candidate
|
||||
|
||||
|
||||
def _load_audio_tokenizer():
|
||||
@@ -180,9 +228,12 @@ def _load_audio_tokenizer():
|
||||
|
||||
Only the ``audio_tokenizer/`` subdirectory (~806 MB) is downloaded.
|
||||
"""
|
||||
from huggingface_hub import snapshot_download
|
||||
from transformers import HiggsAudioV2TokenizerModel
|
||||
|
||||
audio_tokenizer_dir = _resolve_audio_tokenizer_dir()
|
||||
if audio_tokenizer_dir is None:
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
repo_path = snapshot_download(
|
||||
_K2_OMNIVOICE_REPO,
|
||||
allow_patterns=[f"{_K2_OMNIVOICE_SUBDIR}/*"],
|
||||
|
||||
@@ -93,8 +93,13 @@ class HiggsAudioV3Config(PretrainedConfig):
|
||||
tts_token_id: int | None = None,
|
||||
text_token_id: int | None = None,
|
||||
audio_continuation_id: int | None = None,
|
||||
enable_flashinfer_api_unwrap: bool = True,
|
||||
enable_mlp_cudagraph: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
# Legacy perf knob removed: Higgs v3 scheduler tokens now come from
|
||||
# SamplerOutput.sampled_token_ids only.
|
||||
kwargs.pop("enable_cpu_token_override", None)
|
||||
self.audio_token_id = audio_token_id
|
||||
self.mel_per_sample = mel_per_sample
|
||||
|
||||
@@ -124,6 +129,8 @@ class HiggsAudioV3Config(PretrainedConfig):
|
||||
self.tts_token_id = tts_token_id
|
||||
self.text_token_id = text_token_id
|
||||
self.audio_continuation_id = audio_continuation_id
|
||||
self.enable_flashinfer_api_unwrap = bool(enable_flashinfer_api_unwrap)
|
||||
self.enable_mlp_cudagraph = bool(enable_mlp_cudagraph)
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ __all__ = [
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
# Prefix under which codec weights are stored in the v3 checkpoint.
|
||||
_CODEC_PREFIX = "tied.embedding.modality_embeddings.0.model."
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -32,6 +32,7 @@ from vllm_omni.data_entry_keys import (
|
||||
OmniPayload,
|
||||
OmniPayloadStruct,
|
||||
)
|
||||
from vllm_omni.inputs.data import OmniTokensPrompt
|
||||
|
||||
__all__ = ["talker2code2wav", "talker2code2wav_async_chunk"]
|
||||
|
||||
@@ -48,6 +49,15 @@ _DEFAULT_CODEC_LEFT_CONTEXT_FRAMES = 25
|
||||
_DEFAULT_CODEC_RIGHT_HOLDBACK_FRAMES = 4
|
||||
|
||||
|
||||
def _empty_code2wav_prompt() -> Any:
|
||||
return OmniTokensPrompt(
|
||||
prompt_token_ids=[],
|
||||
multi_modal_data=None,
|
||||
mm_processor_kwargs=None,
|
||||
additional_information=None,
|
||||
)
|
||||
|
||||
|
||||
def _revert_delay_pattern(audio_codes_qt: torch.Tensor) -> torch.Tensor:
|
||||
"""Reverse the MusicGen-style delay pattern.
|
||||
|
||||
@@ -91,9 +101,8 @@ def talker2code2wav(
|
||||
_requires_multimodal_data: bool = False,
|
||||
) -> list[Any]:
|
||||
"""Sync: collect all talker codes, revert delay pattern, filter, pass to code2wav."""
|
||||
from vllm_omni.inputs.data import OmniTokensPrompt
|
||||
|
||||
code2wav_inputs: list[OmniTokensPrompt] = []
|
||||
code2wav_inputs: list[Any] = []
|
||||
for talker_output in source_outputs:
|
||||
if not talker_output.finished:
|
||||
continue
|
||||
@@ -103,14 +112,7 @@ def talker2code2wav(
|
||||
|
||||
audio_codes = mm_codes.get("audio")
|
||||
if audio_codes is None or not isinstance(audio_codes, torch.Tensor) or audio_codes.numel() == 0:
|
||||
code2wav_inputs.append(
|
||||
OmniTokensPrompt(
|
||||
prompt_token_ids=[],
|
||||
multi_modal_data=None,
|
||||
mm_processor_kwargs=None,
|
||||
additional_information=None,
|
||||
)
|
||||
)
|
||||
code2wav_inputs.append(_empty_code2wav_prompt())
|
||||
continue
|
||||
|
||||
audio_codes = audio_codes.to(torch.long)
|
||||
@@ -137,22 +139,9 @@ def talker2code2wav(
|
||||
# Step 1: Revert delay pattern
|
||||
try:
|
||||
codes_qt = _revert_delay_pattern(codes_qt)
|
||||
except ValueError:
|
||||
logger.warning(
|
||||
"Skipping request: insufficient frames for delay pattern "
|
||||
"reversal (T=%d < Q=%d). This is a request-level error, "
|
||||
"not a server-fatal condition.",
|
||||
codes_qt.shape[1],
|
||||
codes_qt.shape[0],
|
||||
)
|
||||
code2wav_inputs.append(
|
||||
OmniTokensPrompt(
|
||||
prompt_token_ids=[],
|
||||
multi_modal_data=None,
|
||||
mm_processor_kwargs=None,
|
||||
additional_information=None,
|
||||
)
|
||||
)
|
||||
except ValueError as exc:
|
||||
logger.warning("Skipping invalid Higgs Audio v3 code sequence for Stage 1: %s", exc)
|
||||
code2wav_inputs.append(_empty_code2wav_prompt())
|
||||
continue
|
||||
|
||||
# Step 2: Replace out-of-range codes (BOC=1024, EOC=1025, -1) with 0.
|
||||
@@ -172,14 +161,7 @@ def talker2code2wav(
|
||||
codes_qt = codes_qt[:, :-1]
|
||||
|
||||
if codes_qt.numel() == 0:
|
||||
code2wav_inputs.append(
|
||||
OmniTokensPrompt(
|
||||
prompt_token_ids=[],
|
||||
multi_modal_data=None,
|
||||
mm_processor_kwargs=None,
|
||||
additional_information=None,
|
||||
)
|
||||
)
|
||||
code2wav_inputs.append(_empty_code2wav_prompt())
|
||||
continue
|
||||
|
||||
# Code2Wav expects codebook-major flat: [Q * num_frames]
|
||||
|
||||
@@ -163,6 +163,8 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
|
||||
return output_token_ids
|
||||
|
||||
def _sampling_metadata_for_model_sampler(self, sampling_metadata):
|
||||
if getattr(self.model, "skips_model_sampler_output_token_history", False):
|
||||
return sampling_metadata
|
||||
output_token_ids = self._build_model_sampler_output_token_ids()
|
||||
if output_token_ids == sampling_metadata.output_token_ids:
|
||||
return sampling_metadata
|
||||
@@ -568,7 +570,6 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
|
||||
use_cascade_attn=cascade_attn_prefix_lens is not None,
|
||||
num_encoder_reqs=len(scheduler_output.scheduled_encoder_inputs),
|
||||
)
|
||||
|
||||
num_tokens_padded = batch_desc.num_tokens
|
||||
num_reqs_padded = batch_desc.num_reqs if batch_desc.num_reqs is not None else num_reqs
|
||||
ubatch_slices, ubatch_slices_padded = maybe_create_ubatch_slices(
|
||||
|
||||
@@ -57,6 +57,16 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
# The Omni tensor prefix cache will be allocated
|
||||
# when we initialize the metadata builders if enabled
|
||||
self.omni_prefix_cache = None
|
||||
self._sampled_token_ids_cpu_override = None
|
||||
self._omni_query_start_loc_model_kwarg = False
|
||||
|
||||
def _to_list(self, sampled_token_ids: torch.Tensor) -> list[list[int]]:
|
||||
override_fn = self._sampled_token_ids_cpu_override
|
||||
if callable(override_fn):
|
||||
sampled = override_fn(sampled_token_ids)
|
||||
if sampled is not None:
|
||||
return sampled
|
||||
return super()._to_list(sampled_token_ids)
|
||||
|
||||
def _omni_routed_experts_d2h(self, scheduler_output) -> None:
|
||||
"""Issue routed-experts D2H copy matching upstream GPUModelRunner pattern.
|
||||
@@ -105,14 +115,13 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
)
|
||||
|
||||
def initialize_metadata_builders(self, kv_cache_config, kernel_block_sizes):
|
||||
"""Override to fix scheduler_metadata buffer size for FA3 + CUDA graph.
|
||||
"""Initialize metadata builders and keep FA3 graph metadata buffers sized.
|
||||
|
||||
The upstream FlashAttentionMetadataBuilder pre-allocates
|
||||
scheduler_metadata with (max_num_seqs + 1) entries, but FA3's
|
||||
get_scheduler_metadata() can return up to
|
||||
(max_num_seqs * max_num_splits + 1) entries, causing a RuntimeError
|
||||
during CUDA graph capture. After calling the parent implementation
|
||||
we resize any too-small buffers.
|
||||
FlashAttentionMetadataBuilder can pre-allocate scheduler_metadata for
|
||||
only max_num_seqs + 1 entries while FA3 with split scheduling may need
|
||||
max_num_seqs * max_num_splits + 1 entries during CUDA graph capture.
|
||||
This runner is shared across Omni models, so preserve the existing
|
||||
workaround for non-Higgs models that still use FA3.
|
||||
"""
|
||||
super().initialize_metadata_builders(kv_cache_config, kernel_block_sizes)
|
||||
|
||||
@@ -143,6 +152,14 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
@instrument(span_name="Loading (GPU)")
|
||||
def load_model(self, *args, **kwargs) -> None:
|
||||
super().load_model(*args, **kwargs)
|
||||
model = getattr(self, "model", None)
|
||||
override_fn = None
|
||||
if bool(getattr(model, "supports_sampled_token_ids_cpu_override", False)):
|
||||
candidate = getattr(model, "consume_sampled_token_ids_cpu_override", None)
|
||||
if callable(candidate):
|
||||
override_fn = candidate
|
||||
self._sampled_token_ids_cpu_override = override_fn
|
||||
self._omni_query_start_loc_model_kwarg = bool(getattr(model, "supports_omni_query_start_loc", False))
|
||||
self._maybe_enable_output_token_ids_for_model_sampler()
|
||||
self._init_talker_mtp()
|
||||
self._prewarm_attention_capture_workspaces()
|
||||
@@ -1295,6 +1312,13 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
if self._omni_query_start_loc_model_kwarg:
|
||||
try:
|
||||
num_reqs = len(self.input_batch.req_ids)
|
||||
model_kwargs_extra["omni_query_start_loc"] = self.query_start_loc.gpu[: num_reqs + 1]
|
||||
except Exception as e:
|
||||
logger.debug("[OMNI] Failed to attach query_start_loc: %s", e)
|
||||
|
||||
if getattr(self.model_config, "has_sampling_extra_args", False):
|
||||
extra_args_list: list[dict] = []
|
||||
for req_id in self.input_batch.req_ids:
|
||||
@@ -1320,10 +1344,14 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
# execute the custom postprocess function
|
||||
# TODO(Peiqi): do we have a more elegant way to do this?
|
||||
if hasattr(self.model, "has_postprocess") and self.model.has_postprocess:
|
||||
postprocess_uses_hidden_states = getattr(self.model, "postprocess_uses_hidden_states", True)
|
||||
postprocess_uses_multimodal_outputs = getattr(self.model, "postprocess_uses_multimodal_outputs", True)
|
||||
postprocess_uses_req_infos = getattr(self.model, "postprocess_uses_req_infos", True)
|
||||
for req_index, req_id in enumerate(self.input_batch.req_ids):
|
||||
if req_ids_filter is not None and req_id not in req_ids_filter:
|
||||
continue
|
||||
req_infos = self.model_intermediate_buffer.get(req_id, {})
|
||||
req_infos = self.model_intermediate_buffer.get(req_id, {}) if postprocess_uses_req_infos else {}
|
||||
if postprocess_uses_hidden_states:
|
||||
if combined_hidden_states:
|
||||
# Combined hidden states contains all hidden states for every request
|
||||
hidden_states_slice = combined_hidden_states[req_id]
|
||||
@@ -1333,8 +1361,12 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
s, e = start_offset, start_offset + sched_tokens
|
||||
# only consider to store data into update dict.
|
||||
hidden_states_slice = hidden_states[s:e]
|
||||
else:
|
||||
hidden_states_slice = hidden_states
|
||||
|
||||
if combined_multimodal_outputs:
|
||||
if not postprocess_uses_multimodal_outputs:
|
||||
mm_out = None
|
||||
elif combined_multimodal_outputs:
|
||||
# NOTE this is a bit ugly, but the mm data is structured as a list of
|
||||
# keys mapping to request IDs, and if enabled, we will always have all
|
||||
# request IDs in every subdict, including for cache misses.
|
||||
@@ -1807,6 +1839,14 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
):
|
||||
"""Inject omni-specific kwargs into forward and cache model output"""
|
||||
model_kwargs_extra = self._build_model_kwargs_extra()
|
||||
update_decode_metadata = getattr(self.model, "update_decode_step_metadata", None)
|
||||
if getattr(self.model, "supports_omni_decode_step_metadata", False) and callable(update_decode_metadata):
|
||||
update_decode_metadata(
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
inputs_embeds=inputs_embeds,
|
||||
omni_query_start_loc=model_kwargs_extra.get("omni_query_start_loc"),
|
||||
)
|
||||
|
||||
model_output = super()._model_forward(
|
||||
input_ids=input_ids,
|
||||
|
||||
Reference in new issue
Block a user