[Model] Enable Higgs Audio v3 MRV2 streaming and fix mixed-prefill capture bounds (#8226)
Signed-off-by: Sy03 <1370724210@qq.com>
This commit is contained in:
35 files changed
+3231
-90
No files matched your search
@@ -205,6 +205,16 @@ steps:
|
||||
- pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_minicpmo_4_5_ready.json -m 'core_model and cuda'
|
||||
mirror_hardwares: h100_1
|
||||
|
||||
- label: "TTS · Higgs v3 Full Response Test"
|
||||
source_file_dependencies:
|
||||
- tests/e2e/online_serving/test_higgs_audio_v3.py
|
||||
- vllm_omni/model_executor/models/higgs_audio_v3/
|
||||
- vllm_omni/model_executor/stage_input_processors/higgs_audio_v3.py
|
||||
- vllm_omni/deploy/higgs_multimodal_qwen3.yaml
|
||||
commands:
|
||||
- timeout 20m pytest -s -v tests/e2e/online_serving/test_higgs_audio_v3.py::test_full_response_v1_preserves_audio -m 'core_model and cuda' --run-level 'core_model'
|
||||
mirror_hardwares: h100_1
|
||||
|
||||
- label: "TTS · Qwen3-TTS CustomVoice Test"
|
||||
source_file_dependencies: tts_qwen3_tts_function
|
||||
commands:
|
||||
|
||||
@@ -315,3 +315,34 @@ class TestHiggsAudioV3OnlineVoiceClone:
|
||||
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
@pytest.mark.parametrize(
|
||||
"omni_server",
|
||||
[
|
||||
OmniServerParams(
|
||||
model=MODEL,
|
||||
stage_config_path=STAGE_CONFIG,
|
||||
server_args=[*SERVER_ARGS, "--no-async-chunk"],
|
||||
env_dict=SERVER_ENV,
|
||||
)
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
def test_full_response_v1_preserves_audio(omni_server, online_client) -> None:
|
||||
"""Non-chunk V1 must retain codec rows through the terminal talker step."""
|
||||
online_client.send_audio_speech_request(
|
||||
{
|
||||
"model": omni_server.model,
|
||||
"input": "The quick brown fox jumps over the lazy dog.",
|
||||
"stream": False,
|
||||
"response_format": "wav",
|
||||
"min_audio_bytes": _MIN_AUDIO_BYTES,
|
||||
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
|
||||
},
|
||||
request_num=3,
|
||||
)
|
||||
@@ -0,0 +1,78 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled", [False, True])
|
||||
def test_model_metadata_reset(enabled):
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.higgs_audio_v3_talker import (
|
||||
HiggsAudioV3TalkerForConditionalGeneration as C,
|
||||
)
|
||||
|
||||
t = C.__new__(C)
|
||||
torch.nn.Module.__init__(t)
|
||||
t.config = SimpleNamespace(audio_async_prompt_mode=enabled)
|
||||
t._set_last_step_query_start_loc = lambda x: None
|
||||
t._sync_decode_state_with_batch = lambda x: None
|
||||
t.update_decode_step_metadata(audio_prompt_mode_rows=2)
|
||||
assert t._step_audio_mode_rows == (2 if enabled else 0)
|
||||
t.update_decode_step_metadata()
|
||||
assert t._step_audio_mode_rows == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_state", [False, True])
|
||||
def test_actual_state_guard_and_eos(bad_state):
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
import vllm_omni.model_executor.models.higgs_audio_v3.higgs_audio_v3_talker as mod
|
||||
|
||||
root = Path(mod.__file__).parents[4]
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"helper_fixture", root / "tests/model_executor/models/higgs_audio_v3/test_higgs_audio_v3.py"
|
||||
)
|
||||
assert spec is not None and spec.loader is not None
|
||||
helpers = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(helpers)
|
||||
t = helpers.TestSamplerMethods()._make_batched_sampler_talker(2)
|
||||
t._restore_terminal_audio_rows = (
|
||||
mod.HiggsAudioV3TalkerForConditionalGeneration._restore_terminal_audio_rows.__get__(t)
|
||||
)
|
||||
t.config = SimpleNamespace(audio_async_prompt_mode=True, audio_full_sample_graph=False)
|
||||
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, 12345 if bad_state else 151671])
|
||||
t._last_step_query_start_loc = None
|
||||
t._decode_has_codes = torch.tensor([True, False])
|
||||
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 = 0
|
||||
t._step_audio_tail_rows = 0
|
||||
t._step_audio_mode_rows = 2
|
||||
t._fast_audio_sampler_gpu_fallback_reason = lambda **kw: 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 *a, **kw: None
|
||||
t._sample_audio_codes = lambda logits, *a, **kw: torch.zeros(logits.shape[0], dtype=torch.long)
|
||||
seen_codes = {}
|
||||
t._update_delay_state_batched = lambda *a, **kw: seen_codes.update(kw)
|
||||
|
||||
def call():
|
||||
return mod.HiggsAudioV3TalkerForConditionalGeneration.sample(
|
||||
t, torch.zeros(2, 200000), SimpleNamespace(no_penalties=True)
|
||||
)
|
||||
|
||||
if bad_state:
|
||||
with pytest.raises(RuntimeError, match="tail mismatch"):
|
||||
call()
|
||||
else:
|
||||
assert call().sampled_token_ids.tolist() == [[99999], [151671]]
|
||||
assert seen_codes["code_row_mask"].tolist() == [True, False]
|
||||
@@ -0,0 +1,90 @@
|
||||
# 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.higgs_audio_v3.higgs_audio_v3_talker import (
|
||||
HiggsAudioV3TalkerForConditionalGeneration as C,
|
||||
)
|
||||
from vllm_omni.worker.gpu_ar_model_runner import GPUARModelRunner
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
def test_two_inflight_steps_keep_owned_codes_and_masks():
|
||||
t = C.__new__(C)
|
||||
torch.nn.Module.__init__(t)
|
||||
t.num_codebooks = 2
|
||||
t.use_async_omni_output = True
|
||||
first = torch.tensor([[10, 11, 1, 0], [20, 21, 0, 0], [30, 31, 1, 1], [40, 41, 1, 0]])
|
||||
t._finish_audio_staging(first, 4, torch.device("cpu"), 4)
|
||||
first.zero_() # Next graph replay overwrites original GPU storage.
|
||||
a = t.post_sample_multimodal_outputs(
|
||||
req_ids=["b", "a", "c", "invalid"], invalid_req_indices=[3], multimodal_outputs=None
|
||||
)
|
||||
second = torch.tensor([[50, 51, 1, 0]])
|
||||
t._finish_audio_staging(second, 1, torch.device("cpu"), 1)
|
||||
b = t.post_sample_multimodal_outputs(req_ids=["new"], invalid_req_indices=[], multimodal_outputs=None)
|
||||
second.zero_()
|
||||
assert t.post_sample_multimodal_outputs(req_ids=[], invalid_req_indices=[], multimodal_outputs=None) is None
|
||||
# Finish callbacks out of order. They cannot read current model state.
|
||||
rb = t.finalize_multimodal_outputs_from_cpu_snapshot(b)["codes"]["audio"]
|
||||
ra = t.finalize_multimodal_outputs_from_cpu_snapshot(a)["codes"]["audio"]
|
||||
assert [x.tolist() for x in ra] == [[[10, 11]], [], [[30, 31]], []]
|
||||
assert rb[0].tolist() == [[50, 51]]
|
||||
a["_higgs_audio_snapshot"].zero_()
|
||||
assert ra[0].tolist() == [[10, 11]]
|
||||
assert t._fast_audio_direct_rows == 0
|
||||
assert t.finalize_multimodal_outputs_from_cpu_snapshot(None) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled", [False, True])
|
||||
def test_whole_output_explicit_opt_in(enabled):
|
||||
r = object.__new__(GPUARModelRunner)
|
||||
r.use_async_scheduling = True
|
||||
r.speculative_config = None
|
||||
r.model_config = SimpleNamespace(async_chunk=False, enable_return_routed_experts=False)
|
||||
r.model = SimpleNamespace(use_async_omni_output=True, supports_async_whole_payload=enabled, has_postprocess=False)
|
||||
assert r._should_use_async_omni_output() == enabled
|
||||
r.use_async_scheduling = False
|
||||
assert not r._should_use_async_omni_output()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("async_output", [False, True])
|
||||
def test_terminal_and_invalid_rows_preserve_full_response_codes(async_output):
|
||||
from vllm_omni.distributed.omni_connectors.model_runner.omni_connector_payload_transport import (
|
||||
_OmniConnectorPayloadTransportMixin,
|
||||
)
|
||||
from vllm_omni.model_executor.stage_input_processors.higgs_audio_v3 import talker2code2wav_full_payload
|
||||
|
||||
model = C.__new__(C)
|
||||
torch.nn.Module.__init__(model)
|
||||
model.num_codebooks = 8
|
||||
model.use_async_omni_output = async_output
|
||||
model._last_audio_host_staging = None
|
||||
model._last_audio_staging_event = None
|
||||
transport = _OmniConnectorPayloadTransportMixin()
|
||||
transport._pending_full_payload_send = {}
|
||||
transport._full_payload_replace_keys_cached = frozenset()
|
||||
expected = torch.arange(80, dtype=torch.int32).reshape(10, 8)
|
||||
# Terminal and invalid rows must append zero rows, never replace earlier codes.
|
||||
for codes, valid, done, invalid in [
|
||||
*[(row, 1, 0, []) for row in expected],
|
||||
(torch.zeros(8), 0, 1, []),
|
||||
(torch.ones(8), 1, 0, [0]),
|
||||
]:
|
||||
staging = torch.cat((codes.to(torch.int32), torch.tensor([valid, done], dtype=torch.int32))).reshape(1, 10)
|
||||
model._finish_audio_staging(staging, 1, torch.device("cpu"), 1)
|
||||
payload = model.post_sample_multimodal_outputs(
|
||||
req_ids=["request"], invalid_req_indices=invalid, multimodal_outputs=None
|
||||
)
|
||||
payload = model.finalize_multimodal_outputs_from_cpu_snapshot(payload)
|
||||
audio = payload["codes"]["audio"][0]
|
||||
assert audio.shape == (int(bool(valid) and not invalid), 8)
|
||||
transport.accumulate_full_payload_output("request", {"codes.audio": audio}, None)
|
||||
full, _ = transport._materialize_full_payload_entry(transport._pending_full_payload_send["request"])
|
||||
torch.testing.assert_close(full["codes.audio"], expected)
|
||||
assert talker2code2wav_full_payload(None, full, None)["codes"]["audio"].numel() == 16
|
||||
@@ -0,0 +1,82 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Codec batching must preserve per-request order and convolution boundaries."""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.higgs_audio_v3_code2wav import HiggsAudioV3Code2Wav
|
||||
from vllm_omni.transformers_utils.configs.higgs_audio_v3 import HiggsAudioV3Config
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def codec(monkeypatch):
|
||||
model = HiggsAudioV3Code2Wav(config=HiggsAudioV3Config())
|
||||
calls = []
|
||||
|
||||
def decode(codes):
|
||||
calls.append(tuple(codes.shape))
|
||||
if (codes < 0).any():
|
||||
raise ValueError("invalid codec row")
|
||||
# A time-dependent decoder makes both trim boundaries observable.
|
||||
pcm = codes[:, :1].float().repeat_interleave(model.hop_length, dim=-1)
|
||||
return pcm
|
||||
|
||||
monkeypatch.setattr(model, "decode_codes", decode)
|
||||
return model, calls
|
||||
|
||||
|
||||
def test_ragged_codec_batch_preserves_request_order_and_trims(codec):
|
||||
model, calls = codec
|
||||
codes = [
|
||||
torch.arange(6).expand(8, -1),
|
||||
torch.arange(10, 14).expand(8, -1),
|
||||
torch.arange(20, 26).expand(8, -1),
|
||||
]
|
||||
output = model(
|
||||
input_ids=torch.cat([row.reshape(-1) for row in codes]),
|
||||
seq_token_counts=[row.numel() for row in codes],
|
||||
runtime_additional_information=[
|
||||
{"meta": {"left_context_size": 1, "right_holdback_size": 1}},
|
||||
{"meta": {"left_context_size": 0, "right_holdback_size": 1}},
|
||||
{"meta": {"left_context_size": 1, "right_holdback_size": 1}},
|
||||
],
|
||||
)
|
||||
assert calls == [(2, 8, 6), (1, 8, 4)]
|
||||
for actual, values in zip(
|
||||
output.multimodal_outputs["model_outputs"], ([1, 2, 3, 4], [10, 11, 12], [21, 22, 23, 24])
|
||||
):
|
||||
expected = torch.tensor(values, dtype=torch.float32).repeat_interleave(model.hop_length)
|
||||
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_length,bad_value", [(0, 0), (7, 1), (24, -1)])
|
||||
def test_invalid_codec_request_does_not_discard_valid_peer(codec, bad_length, bad_value):
|
||||
model, _ = codec
|
||||
valid = torch.arange(3).expand(8, -1).reshape(-1)
|
||||
bad = torch.full((bad_length,), bad_value, dtype=torch.long)
|
||||
output = model(input_ids=torch.cat([bad, valid]), seq_token_counts=[bad.numel(), valid.numel()])
|
||||
audio = output.multimodal_outputs["model_outputs"]
|
||||
assert len(audio) == 2
|
||||
assert audio[0].numel() == 0
|
||||
expected = torch.arange(3, dtype=torch.float32).repeat_interleave(model.hop_length)
|
||||
torch.testing.assert_close(audio[1], expected, rtol=0, atol=0)
|
||||
|
||||
|
||||
def test_native_payload_overrides_control_slots_and_empty_terminal(codec):
|
||||
model, calls = codec
|
||||
codes = torch.arange(6).expand(8, -1).reshape(-1)
|
||||
output = model(
|
||||
input_ids=torch.zeros(2, dtype=torch.long),
|
||||
seq_token_counts=[1, 1],
|
||||
runtime_additional_information=[
|
||||
{"codes": {"audio": codes}, "meta": {"left_context_size": 1, "right_holdback_size": 1}},
|
||||
{"codes": {"audio": torch.empty(0, dtype=torch.long)}, "meta": {"finished": True}},
|
||||
],
|
||||
)
|
||||
assert calls == [(1, 8, 6)]
|
||||
wavs = output.multimodal_outputs["model_outputs"]
|
||||
torch.testing.assert_close(wavs[0], torch.arange(1, 5).float().repeat_interleave(model.hop_length))
|
||||
assert wavs[1].numel() == 0
|
||||
@@ -0,0 +1,65 @@
|
||||
# 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.higgs_audio_v3.full_sample_graph import run_dense_sample
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.higgs_audio_v3_code2wav import (
|
||||
HiggsAudioV3Code2WavForConditionalGeneration as Codec,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
def test_matched_streaming_graphs_cover_every_exact_batch():
|
||||
model = SimpleNamespace(
|
||||
config=SimpleNamespace(
|
||||
codec_graph_single_max_frames=33,
|
||||
codec_graph_batch_sizes=list(range(1, 65)),
|
||||
codec_graph_frame_sizes=[8, 33],
|
||||
)
|
||||
)
|
||||
shapes = Codec._decode_graph_shapes(model)
|
||||
assert len(shapes) == 159
|
||||
assert all((b, f) in shapes for b in range(1, 65) for f in (8, 33))
|
||||
assert all((1, f) in shapes for f in range(1, 34))
|
||||
assert (2, 32) not in shapes
|
||||
|
||||
|
||||
def test_default_graph_shapes_are_preserved():
|
||||
shapes = Codec._decode_graph_shapes(SimpleNamespace(config=SimpleNamespace()))
|
||||
assert len(shapes) == 162
|
||||
assert (1, 150) in shapes and (16, 54) in shapes
|
||||
|
||||
|
||||
def test_invalid_graph_shape_rejected():
|
||||
with pytest.raises(ValueError):
|
||||
Codec._decode_graph_shapes(SimpleNamespace(config=SimpleNamespace(codec_graph_batch_sizes=[0])))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("use_v2", [False, True])
|
||||
@pytest.mark.parametrize("sampler", [None, False, True])
|
||||
@pytest.mark.parametrize("codec", [None, False, True])
|
||||
def test_graph_defaults_follow_runner_and_preserve_explicit_overrides(use_v2, sampler, codec):
|
||||
from vllm_omni.transformers_utils.configs.higgs_audio_v3 import HiggsAudioV3Config
|
||||
|
||||
config = HiggsAudioV3Config(audio_full_sample_graph=sampler, codec_cuda_graph=codec)
|
||||
assert config.resolve_graph_defaults(use_v2_model_runner=use_v2) == (
|
||||
use_v2 if sampler is None else sampler,
|
||||
use_v2 if codec is None else codec,
|
||||
)
|
||||
# Resolving a stage must not bake its defaults into shared model config.
|
||||
assert config.audio_full_sample_graph is sampler
|
||||
assert config.codec_cuda_graph is codec
|
||||
|
||||
|
||||
def test_sampler_cache_miss_does_not_capture_or_advance_rng():
|
||||
model = SimpleNamespace(_dense_sample_graphs={})
|
||||
torch.manual_seed(7)
|
||||
before = torch.random.get_rng_state()
|
||||
assert run_dense_sample(model, torch.zeros(2, 4), None, None) is None
|
||||
assert torch.equal(before, torch.random.get_rng_state())
|
||||
assert not model._dense_sample_graphs
|
||||
@@ -0,0 +1,175 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""The fused DAC Snake / decoder forward is bit-identical to transformers' DacDecoder."""
|
||||
|
||||
import copy
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
pytestmark = [pytest.mark.core_model]
|
||||
|
||||
requires_cuda = pytest.mark.skipif(not current_omni_platform.is_cuda(), reason="requires CUDA")
|
||||
|
||||
_DAC_CONFIG = {
|
||||
"codebook_dim": 8,
|
||||
"codebook_size": 1024,
|
||||
"decoder_hidden_size": 256,
|
||||
"downsampling_ratios": [8, 5, 4, 2, 3],
|
||||
"encoder_hidden_size": 16,
|
||||
"hidden_size": 64,
|
||||
"hop_length": 960,
|
||||
"n_codebooks": 9,
|
||||
"sampling_rate": 16000,
|
||||
"upsampling_ratios": [8, 5, 4, 2, 3],
|
||||
}
|
||||
|
||||
|
||||
def _dac_decoder_pair(device, dtype):
|
||||
from transformers import DacConfig, DacModel
|
||||
|
||||
from vllm_omni.model_executor.models.higgs_audio_v2.higgs_audio_decoder import (
|
||||
_fuse_dac_decoder,
|
||||
adjust_conv_transpose_output_padding,
|
||||
)
|
||||
|
||||
torch.manual_seed(0)
|
||||
reference = DacModel(DacConfig(**_DAC_CONFIG)).decoder
|
||||
adjust_conv_transpose_output_padding(reference)
|
||||
with torch.no_grad():
|
||||
for name, param in reference.named_parameters():
|
||||
if name.endswith("alpha"):
|
||||
param.copy_(torch.rand_like(param) * 2 + 0.05)
|
||||
elif name.endswith("bias"):
|
||||
param.normal_(0.0, 0.1)
|
||||
reference = reference.to(device, dtype).eval()
|
||||
fused = copy.deepcopy(reference)
|
||||
assert _fuse_dac_decoder(fused) == 1 + 7 * len(reference.block)
|
||||
fused.load_state_dict(reference.state_dict())
|
||||
return reference, fused
|
||||
|
||||
|
||||
@requires_cuda
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float32])
|
||||
@pytest.mark.parametrize("shape", [(1, 64, 33), (3, 1024, 8), (2, 96, 5000)])
|
||||
@torch.inference_mode()
|
||||
def test_fused_snake_matches_transformers_snake(dtype, shape):
|
||||
from transformers.models.dac.modeling_dac import Snake1d
|
||||
|
||||
from vllm_omni.model_executor.models.higgs_audio_v2.higgs_audio_decoder import _FusedDacSnake1d
|
||||
|
||||
torch.manual_seed(0)
|
||||
reference = Snake1d(shape[1]).to("cuda", dtype)
|
||||
reference.alpha.copy_(torch.rand_like(reference.alpha) * 4 + 0.05)
|
||||
fused = _FusedDacSnake1d(shape[1]).to("cuda", dtype)
|
||||
fused.load_state_dict(reference.state_dict())
|
||||
x = (torch.randn(shape, device="cuda") * 3).to(dtype)
|
||||
residual = (torch.randn(shape, device="cuda") * 3).to(dtype)
|
||||
bias = (torch.randn(shape[1], device="cuda") * 3).to(dtype)
|
||||
assert torch.equal(fused(x), reference(x))
|
||||
|
||||
total, out = fused.fused(x, bias, residual, write_sum=True)
|
||||
expected_total = residual + (x + bias.reshape(1, -1, 1))
|
||||
assert torch.equal(total, expected_total)
|
||||
assert torch.equal(out, reference(expected_total))
|
||||
assert fused.fused(x, bias)[0] is None
|
||||
assert torch.equal(fused.fused(x, bias)[1], reference(x + bias.reshape(1, -1, 1)))
|
||||
|
||||
|
||||
@requires_cuda
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float32])
|
||||
@pytest.mark.parametrize("batch,frames", [(1, 33), (4, 8)])
|
||||
def test_fused_dac_decoder_matches_transformers_decoder(dtype, batch, frames):
|
||||
reference, fused = _dac_decoder_pair("cuda", dtype)
|
||||
x = torch.randn(batch, _DAC_CONFIG["hidden_size"], frames, device="cuda").to(dtype)
|
||||
with torch.inference_mode():
|
||||
assert torch.equal(fused(x), reference(x))
|
||||
|
||||
# Reloaded weights must not reuse the cached Snake reciprocals.
|
||||
with torch.no_grad():
|
||||
for module in (reference, fused):
|
||||
module.snake1.alpha.mul_(0.5)
|
||||
with torch.inference_mode():
|
||||
assert torch.equal(fused(x), reference(x))
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_fuse_dac_decoder_keeps_state_dict_and_falls_back_on_cpu():
|
||||
from transformers.models.dac.modeling_dac import Snake1d
|
||||
|
||||
from vllm_omni.model_executor.models.higgs_audio_v2.higgs_audio_decoder import _FusedDacSnake1d
|
||||
|
||||
reference, fused = _dac_decoder_pair("cpu", torch.float32)
|
||||
assert not any(isinstance(module, Snake1d) for module in fused.modules())
|
||||
assert set(fused.state_dict()) == set(reference.state_dict())
|
||||
x = torch.randn(2, _DAC_CONFIG["hidden_size"], 5)
|
||||
with torch.no_grad():
|
||||
assert torch.equal(fused(x), reference(x))
|
||||
|
||||
decoder = nn.Sequential(Snake1d(8), nn.Conv1d(8, 8, 3))
|
||||
from vllm_omni.model_executor.models.higgs_audio_v2.higgs_audio_decoder import _fuse_dac_decoder
|
||||
|
||||
assert _fuse_dac_decoder(decoder) == 1
|
||||
assert isinstance(decoder[0], _FusedDacSnake1d)
|
||||
assert "forward" not in vars(decoder)
|
||||
|
||||
|
||||
@requires_cuda
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float32])
|
||||
@torch.inference_mode()
|
||||
def test_fused_decoder_graph_replays_changed_inputs(dtype):
|
||||
reference, fused = _dac_decoder_pair("cuda", dtype)
|
||||
x = torch.randn(2, _DAC_CONFIG["hidden_size"], 8, device="cuda").to(dtype)
|
||||
stream = torch.cuda.Stream()
|
||||
stream.wait_stream(torch.cuda.current_stream())
|
||||
with torch.cuda.stream(stream):
|
||||
for _ in range(3):
|
||||
fused(x)
|
||||
torch.cuda.current_stream().wait_stream(stream)
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph, stream=stream):
|
||||
output = fused(x)
|
||||
for _ in range(2):
|
||||
x.normal_()
|
||||
graph.replay()
|
||||
assert torch.equal(output, reference(x))
|
||||
|
||||
|
||||
@requires_cuda
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@torch.inference_mode()
|
||||
def test_fused_snake_uses_eager_on_non_nvidia_platform(monkeypatch):
|
||||
from transformers.models.dac.modeling_dac import Snake1d
|
||||
|
||||
from vllm_omni.model_executor.models.higgs_audio_v2.higgs_audio_decoder import _FusedDacSnake1d
|
||||
|
||||
reference = Snake1d(8).cuda()
|
||||
fused = _FusedDacSnake1d(8).cuda()
|
||||
fused.load_state_dict(reference.state_dict())
|
||||
x = torch.randn(2, 8, 33, device="cuda")
|
||||
monkeypatch.setattr(current_omni_platform, "is_cuda", lambda: False)
|
||||
assert not fused.can_fuse(x)
|
||||
assert torch.equal(fused(x), reference(x))
|
||||
|
||||
|
||||
@requires_cuda
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@torch.inference_mode()
|
||||
def test_fused_snake_preserves_float64_precision():
|
||||
from transformers.models.dac.modeling_dac import Snake1d
|
||||
|
||||
from vllm_omni.model_executor.models.higgs_audio_v2.higgs_audio_decoder import _FusedDacSnake1d
|
||||
|
||||
reference = Snake1d(8).to("cuda", torch.float64)
|
||||
fused = _FusedDacSnake1d(8).to("cuda", torch.float64)
|
||||
fused.load_state_dict(reference.state_dict())
|
||||
x = torch.randn(2, 8, 33, device="cuda", dtype=torch.float64)
|
||||
assert not fused.can_fuse(x)
|
||||
assert torch.equal(fused(x), reference(x))
|
||||
@@ -24,6 +24,27 @@ from vllm_omni.entrypoints.openai.tts_adapters.higgs_audio_v3 import HiggsAudioV
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
def test_external_decode_enters_backbone_compile_boundary():
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.higgs_audio_v3_talker import (
|
||||
HiggsAudioV3TalkerForConditionalGeneration,
|
||||
)
|
||||
|
||||
class Backbone(torch.nn.Module):
|
||||
def forward(self, input_ids, positions, inputs_embeds):
|
||||
assert input_ids is None
|
||||
return inputs_embeds + positions[:, None]
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
pytest.fail("External decode bypassed the Qwen3Model compilation boundary")
|
||||
|
||||
talker = SimpleNamespace(_use_external_decode_cudagraph=True, model=Backbone())
|
||||
hidden = torch.randn(2, 4)
|
||||
positions = torch.tensor([11, 27])
|
||||
actual = HiggsAudioV3TalkerForConditionalGeneration._run_qwen3_layers_eager(talker, positions, hidden)
|
||||
torch.testing.assert_close(actual, hidden + positions[:, None])
|
||||
|
||||
|
||||
# ---- AC-1: Configuration ----
|
||||
|
||||
|
||||
@@ -284,7 +305,7 @@ class TestSamplerMethods:
|
||||
"""Create a minimal talker-like object with sampler/masking methods."""
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3 import higgs_audio_v3_talker as mod
|
||||
|
||||
class FakeTalker:
|
||||
class FakeTalker(SimpleNamespace):
|
||||
num_codebooks = 8
|
||||
codebook_size = 1026
|
||||
|
||||
@@ -297,9 +318,10 @@ class TestSamplerMethods:
|
||||
"""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:
|
||||
class FakeTalker(SimpleNamespace):
|
||||
num_codebooks = 8
|
||||
codebook_size = 1026
|
||||
config = type("Config", (), {"audio_full_sample_graph": False})()
|
||||
|
||||
t = FakeTalker()
|
||||
t._decode_last_codes = torch.arange(num_rows * t.num_codebooks, dtype=torch.long).view(
|
||||
@@ -337,6 +359,7 @@ class TestSamplerMethods:
|
||||
"_get_audio_host_staging_buffer",
|
||||
"_apply_delay_pattern_masking_batched",
|
||||
"_update_delay_state_batched",
|
||||
"_finish_audio_staging",
|
||||
"_prefill_row_mask",
|
||||
"_audio_seed_mask_from_step_input",
|
||||
):
|
||||
@@ -426,9 +449,12 @@ class TestSamplerMethods:
|
||||
def test_sampling_metadata_invalid_temperature_is_sanitized(self, monkeypatch):
|
||||
t = self._make_minimal_talker()
|
||||
seen = []
|
||||
monkeypatch.setattr(
|
||||
torch, "multinomial", lambda probs, num_samples: seen.append(probs) or probs.argmax(-1, True)
|
||||
)
|
||||
|
||||
def sample(probs, num_samples):
|
||||
seen.append(probs)
|
||||
return probs.argmax(-1, True)
|
||||
|
||||
monkeypatch.setattr(torch, "multinomial", sample)
|
||||
logits = torch.tensor([[0.0, 2.0], [3.0, 0.0]])
|
||||
|
||||
result = t._sample_audio_codes(
|
||||
@@ -444,9 +470,12 @@ class TestSamplerMethods:
|
||||
def test_sampling_metadata_is_expanded_per_request_and_vq(self, monkeypatch):
|
||||
t = self._make_minimal_talker()
|
||||
seen = []
|
||||
monkeypatch.setattr(
|
||||
torch, "multinomial", lambda probs, num_samples: seen.append(probs) or probs.argmax(-1, True)
|
||||
)
|
||||
|
||||
def sample(probs, num_samples):
|
||||
seen.append(probs)
|
||||
return probs.argmax(-1, True)
|
||||
|
||||
monkeypatch.setattr(torch, "multinomial", sample)
|
||||
cb_logits = torch.tensor([[[4.0, 3.0, 2.0, 1.0]] * 2] * 2) # [2 requests, 2 VQs, 4 vocab]
|
||||
logits = cb_logits.reshape(-1, cb_logits.shape[-1])
|
||||
metadata = self._sampling_metadata(
|
||||
@@ -840,7 +869,7 @@ class TestFeedbackMethods:
|
||||
"""postprocess() should return codes from _last_audio_codes."""
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3 import higgs_audio_v3_talker as mod
|
||||
|
||||
class FakeTalker:
|
||||
class FakeTalker(SimpleNamespace):
|
||||
_last_audio_codes = torch.tensor([[100, 200, 300, 400, 500, 600, 700, 800]])
|
||||
_last_audio_code_valid = [True]
|
||||
_last_audio_host_staging = None
|
||||
@@ -859,7 +888,7 @@ class TestFeedbackMethods:
|
||||
"""postprocess() should skip rows with -1 (no audio)."""
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3 import higgs_audio_v3_talker as mod
|
||||
|
||||
class FakeTalker:
|
||||
class FakeTalker(SimpleNamespace):
|
||||
_last_audio_codes = torch.tensor([[-1, -1, -1, -1, -1, -1, -1, -1]])
|
||||
_last_audio_code_valid = [False]
|
||||
_last_audio_host_staging = None
|
||||
@@ -876,7 +905,7 @@ class TestFeedbackMethods:
|
||||
"""postprocess() should advance cursor by 1 per call."""
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3 import higgs_audio_v3_talker as mod
|
||||
|
||||
class FakeTalker:
|
||||
class FakeTalker(SimpleNamespace):
|
||||
_last_audio_codes = torch.tensor(
|
||||
[[100, 200, 300, 400, 500, 600, 700, 800], [-1, -1, -1, -1, -1, -1, -1, -1]]
|
||||
)
|
||||
@@ -901,7 +930,7 @@ class TestReferenceAudioSubstitution:
|
||||
def _make_talker(self, query_start_loc):
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3 import higgs_audio_v3_talker as mod
|
||||
|
||||
class FakeTalker:
|
||||
class FakeTalker(SimpleNamespace):
|
||||
_last_step_query_start_loc = query_start_loc
|
||||
_apply_ref_audio_substitution = mod.HiggsAudioV3TalkerForConditionalGeneration._apply_ref_audio_substitution
|
||||
|
||||
@@ -981,7 +1010,7 @@ class TestAudioFeedback:
|
||||
embed = mod.HiggsFusedMultiTextEmbedding(num_codebooks=8, vocab_size=1026, hidden_size=16)
|
||||
torch.nn.init.ones_(embed.weight)
|
||||
|
||||
class FakeTalker:
|
||||
class FakeTalker(SimpleNamespace):
|
||||
num_codebooks = 8
|
||||
codebook_size = 1026
|
||||
_audio_continuation_id = 99999 # fake audio token
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
from vllm.sampling_params import SamplingParams
|
||||
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.higgs_audio_v3_talker import (
|
||||
HiggsAudioV3TalkerForConditionalGeneration,
|
||||
)
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.model_state import HiggsModelState
|
||||
from vllm_omni.worker_v2.model_states.intermediate_buffer import OmniIntermediateBuffer
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
def make_state():
|
||||
state = object.__new__(HiggsModelState)
|
||||
state.sampling_slots = None
|
||||
state.request_slots = {}
|
||||
state.device = torch.device("cpu")
|
||||
state.rope_state = state.prompt_embeds_state = state._static_inputs_embeds = None
|
||||
state.requests = {}
|
||||
state.generators = {}
|
||||
state.metadata_key = state.metadata = None
|
||||
state.intermediate_buffer = OmniIntermediateBuffer(4)
|
||||
state.model = SimpleNamespace(
|
||||
on_requests_finished=Mock(),
|
||||
_resolve_token_ids=Mock(),
|
||||
_audio_continuation_id=99,
|
||||
update_decode_step_metadata=Mock(),
|
||||
)
|
||||
return state
|
||||
|
||||
|
||||
def request(name, seed, **kwargs):
|
||||
return SimpleNamespace(
|
||||
req_id=name, prompt_token_ids=[1, 2, 99], mm_features=[], sampling_params=SamplingParams(seed=seed, **kwargs)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("slots", [False, True])
|
||||
def test_sampling_follows_request_identity_and_reinitializes_reused_slots(slots):
|
||||
s = make_state()
|
||||
if slots:
|
||||
s.sampling_slots = {
|
||||
name: torch.zeros(4, dtype=torch.long if name == "top_k" else torch.float32)
|
||||
for name in ("temperature", "top_p", "top_k", "zeros", "ones")
|
||||
}
|
||||
s.sampling_slots["ones"].fill_(1)
|
||||
s.add_request(0, request("a", 42, temperature=0.5))
|
||||
s.add_request(2, request("b", 99, temperature=0.9))
|
||||
a = s.generators["a"]
|
||||
b = s.generators["b"]
|
||||
first = s.sampling_metadata(["a", "b"])
|
||||
assert first.generators == {0: a, 1: b}
|
||||
torch.testing.assert_close(first.temperature, torch.tensor([0.5, 0.9]))
|
||||
swapped = s.sampling_metadata(["b", "a"])
|
||||
assert swapped.generators == {0: b, 1: a}
|
||||
torch.testing.assert_close(swapped.temperature, torch.tensor([0.9, 0.5]))
|
||||
s.remove_request("a")
|
||||
s.add_request(0, request("new", 7))
|
||||
assert "a" not in s.generators and "a" not in s.requests
|
||||
s.model.on_requests_finished.assert_called_once_with({"a"})
|
||||
assert s.sampling_metadata(["new"]).generators[0].initial_seed() == 7
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"kwargs",
|
||||
[
|
||||
{"repetition_penalty": 1.2},
|
||||
{"logprobs": 1},
|
||||
{"min_p": 0.1},
|
||||
{"frequency_penalty": 0.2},
|
||||
{"prompt_logprobs": 1},
|
||||
{"min_tokens": 1},
|
||||
],
|
||||
)
|
||||
def test_unsupported_sampling_is_rejected_before_admission(kwargs):
|
||||
s = make_state()
|
||||
with pytest.raises(ValueError, match="temperature/top-k/top-p"):
|
||||
s.add_request(0, request("a", 42, **kwargs))
|
||||
assert not s.requests
|
||||
|
||||
|
||||
def test_partial_prefill_not_classified_as_audio_and_slot_order_is_authoritative():
|
||||
s = make_state()
|
||||
s.add_request(0, request("a", 42))
|
||||
s.add_request(2, request("b", 99))
|
||||
batch = SimpleNamespace(
|
||||
req_ids=["b", "a"],
|
||||
idx_mapping_np=np.array([2, 0]),
|
||||
num_scheduled_tokens=np.array([1, 1]),
|
||||
positions=torch.tensor([2, 1]),
|
||||
query_start_loc=torch.tensor([0, 1, 2]),
|
||||
num_reqs=2,
|
||||
num_tokens=2,
|
||||
)
|
||||
states = SimpleNamespace(num_computed_prefill_tokens=np.array([1, 0, 2, 0]))
|
||||
s.run_preprocess(batch, {"input_ids": torch.tensor([99, 2])}, states)
|
||||
assert s.model.update_decode_step_metadata.call_args.kwargs["audio_prompt_mode_rows"] == 0
|
||||
states.num_computed_prefill_tokens[0] = 2
|
||||
s.run_preprocess(batch, {"input_ids": torch.tensor([99, 99])}, states)
|
||||
assert s.model.update_decode_step_metadata.call_args.kwargs["audio_prompt_mode_rows"] == 2
|
||||
|
||||
|
||||
def test_finalizer_filters_unsampled_prefill_and_keeps_owned_audio():
|
||||
s = make_state()
|
||||
s.model = object.__new__(HiggsAudioV3TalkerForConditionalGeneration)
|
||||
torch.nn.Module.__init__(s.model)
|
||||
s.model.num_codebooks = 2
|
||||
snapshot = torch.tensor([[11, 12, 1, 0], [21, 22, 1, 0], [31, 32, 0, 1]])
|
||||
payload = {"_higgs_audio_snapshot": snapshot, "_higgs_invalid_rows": ()}
|
||||
result = s.finalize_audio_snapshot(payload, [1, 0, 1])
|
||||
snapshot.zero_()
|
||||
assert [x.tolist() for x in result["codes"]["audio"]] == [[[11, 12]], [], []]
|
||||
assert payload["_higgs_invalid_rows"] == ()
|
||||
|
||||
|
||||
def test_upstream_warmup_retains_generic_text_sampling():
|
||||
s = make_state()
|
||||
s.add_request(0, request("_warmup_0_", 0, repetition_penalty=1.2, logprobs=1))
|
||||
batch = SimpleNamespace(req_ids=["_warmup_0_"])
|
||||
sampler, rejection = s.custom_sampler(Mock())
|
||||
expected = (object(), torch.ones(1), torch.zeros(1))
|
||||
standard = Mock(return_value=expected)
|
||||
output = sampler.sample_step(None, batch, None, None, standard)
|
||||
assert output.sampler_output is expected[0] and output.multimodal_outputs is None
|
||||
assert rejection is None
|
||||
standard.assert_called_once_with(None, batch, None)
|
||||
s.remove_request("_warmup_0_")
|
||||
assert not s.requests
|
||||
|
||||
|
||||
def test_static_embeddings_exclude_padding_and_preserve_storage():
|
||||
s = make_state()
|
||||
embed = torch.nn.Embedding(100, 3)
|
||||
s.model.model = SimpleNamespace(embed_tokens=embed)
|
||||
s.model._apply_audio_feedback = Mock(side_effect=lambda x, ids: x + 2)
|
||||
s.model._apply_ref_audio_substitution = Mock(side_effect=lambda x, *args: x + 5)
|
||||
s._static_inputs_embeds = torch.full((8, 3), 999.0)
|
||||
ptr = s._static_inputs_embeds.data_ptr()
|
||||
ids = torch.tensor([1, 2, 3, 0])
|
||||
batch = SimpleNamespace(num_tokens=3, num_reqs=3, num_tokens_after_padding=4, has_prefill=False)
|
||||
s._prepare_static_embeddings(batch, {"input_ids": ids})
|
||||
assert s._static_inputs_embeds.data_ptr() == ptr
|
||||
torch.testing.assert_close(s._static_inputs_embeds[:3], embed(ids[:3]) + 2)
|
||||
assert s._static_inputs_embeds[3].count_nonzero() == 0
|
||||
assert s.model._apply_audio_feedback.call_args.args[1].numel() == 3
|
||||
s.model._apply_ref_audio_substitution.assert_not_called()
|
||||
batch.has_prefill = True
|
||||
batch.positions = torch.arange(4)
|
||||
s._prepare_static_embeddings(batch, {"input_ids": ids, "model_intermediate_buffer": [{}]})
|
||||
torch.testing.assert_close(s._static_inputs_embeds[:3], embed(ids[:3]) + 7)
|
||||
|
||||
|
||||
def test_mixed_feedback_updates_only_single_token_spans_with_codes():
|
||||
s = make_state()
|
||||
s.model._ensure_decode_state_capacity = Mock()
|
||||
s.model._decode_last_codes = torch.tensor([[1, 2], [3, 4], [5, 6], [7, 8]])
|
||||
s.model._decode_has_codes = torch.tensor([True, True, False, True])
|
||||
s.model.multimodal_embedding = lambda codes: codes.sum(1)[:, None].expand(-1, 3)
|
||||
batch = SimpleNamespace(num_tokens=7, num_reqs=4, query_start_loc=torch.tensor([0, 1, 4, 5, 7]))
|
||||
embeds = torch.arange(21).reshape(7, 3).float()
|
||||
output = s._apply_audio_feedback(embeds, torch.ones(7).long(), batch)
|
||||
expected = embeds.clone()
|
||||
expected[0] = 3
|
||||
torch.testing.assert_close(output, expected)
|
||||
assert embeds[0].tolist() == [0, 1, 2]
|
||||
|
||||
|
||||
def test_direct_payload_owns_rows_and_filters_partial_prefill():
|
||||
s = make_state()
|
||||
s.direct_payload = True
|
||||
s.model.num_codebooks = 2
|
||||
snapshot = torch.tensor([[11, 12, 1, 0], [21, 22, 1, 0], [31, 32, 0, 1]])
|
||||
out = s.finalize_audio_snapshot({"_higgs_audio_snapshot": snapshot}, [1, 0, 1])
|
||||
snapshot.zero_()
|
||||
assert out.inter_stage[0]["codes.audio"].tolist() == [[11, 12]]
|
||||
assert out.inter_stage[1:] == [None, None]
|
||||
|
||||
|
||||
def test_registered_audio_sampler_preserves_rows_counts_and_owned_output(mocker):
|
||||
state = make_state()
|
||||
metadata, payload = object(), {"_higgs_audio_snapshot": torch.ones(2, 4)}
|
||||
state.sampling_metadata = Mock(return_value=metadata)
|
||||
state.model.compute_logits = Mock(return_value=torch.ones(2, 8))
|
||||
state.model.sample = Mock(
|
||||
return_value=SimpleNamespace(sampled_token_ids=torch.tensor([[9], [8]]), logprobs_tensors=None)
|
||||
)
|
||||
state.model.post_sample_multimodal_outputs = Mock(return_value=payload)
|
||||
count, rejected = torch.tensor([1, 0]), torch.tensor([0, 0])
|
||||
mocker.patch(
|
||||
"vllm_omni.model_executor.models.higgs_audio_v3.model_state.get_num_sampled_and_rejected",
|
||||
return_value=(count, rejected),
|
||||
)
|
||||
batch = SimpleNamespace(
|
||||
req_ids=["b", "a"],
|
||||
num_reqs=2,
|
||||
num_draft_tokens=0,
|
||||
idx_mapping=torch.tensor([3, 1]),
|
||||
logits_indices=torch.tensor([2, 0]),
|
||||
seq_lens=None,
|
||||
cu_num_logits=None,
|
||||
)
|
||||
hidden = torch.arange(12).reshape(3, 4).float()
|
||||
standard = Mock(side_effect=AssertionError("must not sample twice"))
|
||||
sampler, rejection = state.custom_sampler(Mock())
|
||||
result = sampler.sample_step(hidden, batch, SimpleNamespace(prefill_len=SimpleNamespace(gpu=None)), None, standard)
|
||||
state.sampling_metadata.assert_called_once()
|
||||
torch.testing.assert_close(state.model.compute_logits.call_args.args[0], hidden[[2, 0]])
|
||||
assert state.model.compute_logits.call_args.args[1] is metadata
|
||||
assert result.num_sampled is count and result.num_rejected is rejected
|
||||
assert result.multimodal_outputs is payload and result.owns_multimodal_outputs
|
||||
assert not result.include_hidden_states and result.finalize_multimodal == state.finalize_audio_snapshot
|
||||
assert rejection is None
|
||||
standard.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("draft,grammar", [(1, None), (0, object())])
|
||||
def test_registered_audio_sampler_rejects_unsupported_modes(draft, grammar):
|
||||
sampler, _ = make_state().custom_sampler(Mock())
|
||||
batch = SimpleNamespace(req_ids=["real-request"], num_draft_tokens=draft)
|
||||
with pytest.raises(ValueError, match="grammar or speculative"):
|
||||
sampler.sample_step(None, batch, None, grammar, Mock())
|
||||
@@ -0,0 +1,46 @@
|
||||
# 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.higgs_audio_v3 import (
|
||||
talker2code2wav,
|
||||
talker2code2wav_full_payload,
|
||||
talker2code2wav_token_only,
|
||||
)
|
||||
from vllm_omni.outputs.mm_outputs import MultimodalPayload
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("wrapped", [False, True])
|
||||
@pytest.mark.parametrize("rows", [0, 4, 8, 9, 30])
|
||||
@pytest.mark.parametrize("offset", [0, -16, 1016])
|
||||
def test_native_full_payload_preserves_legacy_dedelay_and_tail(rows, offset, wrapped):
|
||||
audio = torch.arange(rows * 8).reshape(rows, 8) + offset
|
||||
payload = {"codes": {"audio": audio}}
|
||||
if wrapped:
|
||||
payload = MultimodalPayload(metadata=payload)
|
||||
out = SimpleNamespace(finished=True, outputs=[SimpleNamespace(multimodal_output=payload)])
|
||||
legacy = talker2code2wav([out])[0]["prompt_token_ids"]
|
||||
native = talker2code2wav_full_payload(None, {"codes.audio": audio}, None)
|
||||
assert native["codes"]["audio"].tolist() == legacy
|
||||
# Independent expected layout: book q reads delayed rows q + frame.
|
||||
frames = max(rows - 7, 0) if rows >= 8 else 0
|
||||
kept = frames - 1 if frames >= 2 else frames
|
||||
expected = []
|
||||
for q in range(8):
|
||||
for frame in range(kept):
|
||||
value = (q + frame) * 8 + q + offset
|
||||
expected.append(value if 0 <= value < 1024 else 0)
|
||||
assert legacy == expected
|
||||
assert native["meta"]["finished"]
|
||||
assert talker2code2wav_token_only([out])[0]["prompt_token_ids"] == legacy
|
||||
|
||||
|
||||
def test_native_control_slot_without_audio_payload():
|
||||
out = SimpleNamespace(finished=True, outputs=[SimpleNamespace(multimodal_output=None)])
|
||||
assert talker2code2wav_token_only([out])[0]["prompt_token_ids"] == [0]
|
||||
@@ -0,0 +1,96 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Fused Higgs sampling kernels must be bit-identical to the eager code they replace."""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.skipif(not current_omni_platform.is_cuda(), reason="requires CUDA")]
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("numel", [8 * 1026, 1000, 300000])
|
||||
def test_batched_generator_noise_matches_per_request_exponential(numel):
|
||||
from vllm_omni.utils.seeded_exponential import fill_exponential_rows
|
||||
|
||||
device = torch.device("cuda")
|
||||
seeds = [42, 42, None, 2**63 + 5]
|
||||
|
||||
def generators():
|
||||
gens = [None if s is None else torch.Generator(device=device).manual_seed(s) for s in seeds]
|
||||
resumed = gens[1]
|
||||
assert resumed is not None
|
||||
resumed.set_offset(resumed.get_offset() + 4) # a request that already sampled
|
||||
return gens
|
||||
|
||||
default = torch.cuda.default_generators[device.index or 0]
|
||||
start = default.get_offset()
|
||||
expected_gens = generators()
|
||||
expected = torch.empty(len(seeds), numel, device=device)
|
||||
for row, generator in enumerate(expected_gens):
|
||||
expected[row].exponential_(generator=generator)
|
||||
expected_default = default.get_offset()
|
||||
|
||||
default.set_offset(start)
|
||||
actual_gens = generators()
|
||||
actual = fill_exponential_rows(torch.empty_like(expected), actual_gens)
|
||||
assert torch.equal(actual, expected)
|
||||
assert default.get_offset() == expected_default
|
||||
for got, want in zip(actual_gens, expected_gens):
|
||||
if want is not None:
|
||||
assert got.get_offset() == want.get_offset()
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_fused_decode_embeddings_match_feedback_and_zero_padding():
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.step_kernels import decode_embeddings
|
||||
|
||||
torch.manual_seed(0)
|
||||
vocab, hidden, books, audio_vocab = 1000, 96, 8, 17
|
||||
text = torch.randn(vocab, hidden, device="cuda", dtype=torch.bfloat16)
|
||||
audio = torch.randn(books * audio_vocab, hidden, device="cuda", dtype=torch.bfloat16)
|
||||
ids = torch.randint(0, vocab, (5,), device="cuda")
|
||||
ids[0] = -1
|
||||
codes = torch.randint(0, audio_vocab, (8, books), device="cuda")
|
||||
has = torch.tensor([True, False, True, False, True, True, False, False], device="cuda")
|
||||
offsets = torch.arange(books, device="cuda") * audio_vocab
|
||||
expected = torch.where(
|
||||
has[:5, None], F.embedding(codes[:5] + offsets, audio).sum(-2), F.embedding(ids.clamp_min(0), text)
|
||||
)
|
||||
out = torch.full((8, hidden), 3.0, device="cuda", dtype=torch.bfloat16)
|
||||
decode_embeddings(out, ids, text, audio, codes, has, 7, audio_vocab)
|
||||
assert torch.equal(out[:5], expected)
|
||||
assert torch.count_nonzero(out[5:7]) == 0
|
||||
assert torch.all(out[7] == 3)
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("prompt_mode", [False, True])
|
||||
def test_step_tail_guard_matches_eager_restore_reset_and_validation(prompt_mode):
|
||||
from vllm_omni.model_executor.models.higgs_audio_v3.step_kernels import step_tail_guard
|
||||
|
||||
audio, eos = 7, 9
|
||||
# Rows: decode audio, decode EOS, 3-token prefill ending in audio, decode audio.
|
||||
ids = torch.tensor([audio, eos, 1, 2, audio, audio], device="cuda")
|
||||
qsl = torch.tensor([0, 1, 2, 5, 6], device="cuda", dtype=torch.int32)
|
||||
has = torch.tensor([True, True, True, False], device="cuda")
|
||||
done = torch.tensor([False, False, True, False], device="cuda")
|
||||
delay = torch.tensor([3, 4, 5, 6], device="cuda")
|
||||
eoc = torch.tensor([1, 2, 3, 4], device="cuda")
|
||||
error = torch.zeros(1, device="cuda", dtype=torch.int32)
|
||||
step_tail_guard(ids, qsl, has, done, delay, eoc, error, audio_id=audio, eos_id=eos, prompt_mode=prompt_mode)
|
||||
prefill = torch.tensor([False, False, True, False], device="cuda")
|
||||
tail = ids[(qsl[1:] - 1).long()]
|
||||
want_done = torch.tensor([False, False, True, False], device="cuda")
|
||||
if prompt_mode:
|
||||
want_done |= tail == eos
|
||||
want_done &= ~prefill
|
||||
assert torch.equal(done, want_done)
|
||||
assert torch.equal(has, torch.tensor([True, True, False, False], device="cuda"))
|
||||
assert delay.tolist() == [3, 4, 0, 6] and eoc.tolist() == [1, 2, -1, 4]
|
||||
# The EOS row is valid only as a terminal prompt-mode row.
|
||||
assert int(error.item()) == (0 if prompt_mode else 1)
|
||||
@@ -0,0 +1,41 @@
|
||||
# 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.higgs_audio_v3.higgs_audio_v3_talker import (
|
||||
HiggsAudioV3TalkerForConditionalGeneration,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fallback", [None, "prefill", "logprobs", "allowed", "bad_words", "penalty", "inactive"])
|
||||
def test_text_head_only_skipped_for_direct_audio_decode(fallback):
|
||||
hidden = torch.randn(3, 4)
|
||||
calls = []
|
||||
|
||||
def logits_processor(head, h):
|
||||
calls.append(h)
|
||||
return torch.ones(3, 7)
|
||||
|
||||
model = SimpleNamespace(
|
||||
_audio_continuation_id=5,
|
||||
_last_step_input_ids=torch.ones(4 if fallback == "prefill" else 3, dtype=torch.long),
|
||||
_fast_audio_direct_rows=0 if fallback == "inactive" else 3,
|
||||
_text_vocab_size=7,
|
||||
lm_head=None,
|
||||
logits_processor=logits_processor,
|
||||
)
|
||||
metadata = SimpleNamespace(
|
||||
no_penalties=fallback != "penalty",
|
||||
max_num_logprobs=1 if fallback == "logprobs" else None,
|
||||
allowed_token_ids_mask=torch.ones(3, 7) if fallback == "allowed" else None,
|
||||
bad_words_token_ids={0: [1]} if fallback == "bad_words" else {},
|
||||
)
|
||||
logits = HiggsAudioV3TalkerForConditionalGeneration.compute_logits(model, hidden, metadata)
|
||||
assert logits.shape == (3, 7)
|
||||
assert model._last_logits_hidden is hidden
|
||||
assert len(calls) == (fallback is not None)
|
||||
@@ -215,6 +215,8 @@ class _MiniDiT(nn.Module):
|
||||
cnn_cache_buffer[b_idx] = x[:, -2:, :].transpose(1, 2).contiguous()
|
||||
dt = x.shape[1]
|
||||
att_cache_buffer[b_idx][:, :, :dt, :] = x.unsqueeze(1)
|
||||
if att_b is not None:
|
||||
att_cache_buffer[b_idx][:, :, dt:, :] = att_b
|
||||
x = self.final_layer(x)
|
||||
x = x.transpose(1, 2)
|
||||
return x
|
||||
@@ -248,6 +250,9 @@ def test_cfm_graph_replay_matches_eager_for_uncached_and_cached_shapes(
|
||||
with torch.inference_mode():
|
||||
for _, chunk_size, old_att_len in ((2, 10, 0), (2, 10, 5)):
|
||||
inputs = _cfm_inputs(2, chunk_size, old_att_len)
|
||||
# A cache output must overwrite every row, including the old tail.
|
||||
inputs[4].fill_(float("nan"))
|
||||
inputs[5].fill_(float("nan"))
|
||||
|
||||
eager_inputs = tuple(v.clone() for v in inputs)
|
||||
with torch.no_grad():
|
||||
@@ -268,6 +273,8 @@ def test_cfm_graph_replay_matches_eager_for_uncached_and_cached_shapes(
|
||||
torch.testing.assert_close(graph_result, eager_result, rtol=1e-4, atol=1e-5)
|
||||
torch.testing.assert_close(graph_cnn, eager_inputs[4], rtol=1e-4, atol=1e-5)
|
||||
torch.testing.assert_close(graph_att, eager_inputs[5], rtol=1e-4, atol=1e-5)
|
||||
assert torch.isfinite(graph_att).all()
|
||||
torch.testing.assert_close(graph_att[:, :, :, chunk_size:, :], inputs[3])
|
||||
|
||||
wrapper._flush()
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""The batched Higgs async-chunk builder must emit exactly what the per-request one does."""
|
||||
|
||||
from collections import defaultdict
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.model_executor.stage_input_processors.higgs_audio_v3 import (
|
||||
talker2code2wav_async_chunk,
|
||||
talker2code2wav_async_chunk_batch,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
def _manager():
|
||||
extra = {
|
||||
"codec_chunk_frames": 5,
|
||||
"codec_left_context_frames": 3,
|
||||
"codec_right_holdback_frames": 1,
|
||||
"initial_codec_chunk_frames": 2,
|
||||
}
|
||||
return SimpleNamespace(connector=SimpleNamespace(config={"extra": extra}), code_prompt_token_ids=defaultdict(list))
|
||||
|
||||
|
||||
class _Request:
|
||||
def __init__(self, rid, finish_step):
|
||||
self.external_req_id = rid
|
||||
self.finish_step = finish_step
|
||||
self.step = 0
|
||||
|
||||
def is_finished(self):
|
||||
return self.step >= self.finish_step
|
||||
|
||||
|
||||
def _payload_items(payload):
|
||||
if payload is None:
|
||||
return None
|
||||
meta = payload.meta
|
||||
return (
|
||||
payload.codes.audio.tolist(),
|
||||
None if meta is None else (meta.left_context_size, meta.right_holdback_size, bool(meta.finished)),
|
||||
)
|
||||
|
||||
|
||||
def test_batch_async_chunk_matches_per_request_builder():
|
||||
torch.manual_seed(0)
|
||||
single, batched = _manager(), _manager()
|
||||
finish = [23, 9, 31]
|
||||
requests_single = [_Request(f"r{i}", f) for i, f in enumerate(finish)]
|
||||
requests_batch = [_Request(f"r{i}", f) for i, f in enumerate(finish)]
|
||||
for step in range(32):
|
||||
outputs: list[dict | None] = []
|
||||
flags: list[bool] = []
|
||||
for i, request in enumerate(requests_single):
|
||||
if step > request.finish_step:
|
||||
outputs.append(None)
|
||||
elif step == 7 and i == 2:
|
||||
outputs.append(None) # a step without an emitted row
|
||||
else:
|
||||
outputs.append({"codes": {"audio": torch.randint(0, 1026, (1, 8))}})
|
||||
flags.append(step == request.finish_step)
|
||||
for request in requests_single + requests_batch:
|
||||
request.step = step
|
||||
active = [i for i, request in enumerate(requests_single) if step <= request.finish_step]
|
||||
want = [
|
||||
_payload_items(talker2code2wav_async_chunk(single, outputs[i], requests_single[i], flags[i]))
|
||||
for i in active
|
||||
]
|
||||
got = talker2code2wav_async_chunk_batch(
|
||||
batched,
|
||||
[outputs[i] for i in active],
|
||||
[requests_batch[i] for i in active],
|
||||
[flags[i] for i in active],
|
||||
)
|
||||
assert [_payload_items(p) for p in got] == want
|
||||
assert single.higgs_v3_emitted_frames == batched.higgs_v3_emitted_frames == {}
|
||||
@@ -0,0 +1,96 @@
|
||||
# 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.v1.worker.gpu_model_runner import GPUModelRunner
|
||||
|
||||
from vllm_omni.worker.gpu_model_runner import OmniGPUModelRunner
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fail", [False, True])
|
||||
def test_runner_closes_metadata_scope_on_return_and_error(monkeypatch, fail):
|
||||
events = []
|
||||
|
||||
def begin(**kw):
|
||||
events.append("begin")
|
||||
|
||||
def end():
|
||||
events.append("end")
|
||||
|
||||
def forward(*args, **kw):
|
||||
assert events == ["begin"]
|
||||
events.extend(["warmup", "capture"])
|
||||
if fail:
|
||||
raise RuntimeError("capture failed")
|
||||
return torch.ones(2)
|
||||
|
||||
r = object.__new__(OmniGPUModelRunner)
|
||||
r.model = SimpleNamespace(
|
||||
supports_omni_decode_step_metadata=True, update_decode_step_metadata=begin, finish_decode_step_forward=end
|
||||
)
|
||||
r.input_batch = SimpleNamespace(req_ids=["a", "b"])
|
||||
r._build_model_kwargs_extra = lambda: {}
|
||||
monkeypatch.setattr(GPUModelRunner, "_model_forward", forward)
|
||||
if fail:
|
||||
with pytest.raises(RuntimeError, match="capture failed"):
|
||||
r._model_forward()
|
||||
else:
|
||||
torch.testing.assert_close(r._model_forward(), torch.ones(2))
|
||||
assert events == ["begin", "warmup", "capture", "end"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("requires_tails", [False, True])
|
||||
@pytest.mark.parametrize("async_scheduling", [False, True])
|
||||
def test_cpu_tail_metadata_requires_model_capability(monkeypatch, requires_tails, async_scheduling):
|
||||
observed = {}
|
||||
runner = object.__new__(OmniGPUModelRunner)
|
||||
runner.model = SimpleNamespace(
|
||||
supports_omni_decode_step_metadata=True,
|
||||
requires_cpu_input_tail_ids=requires_tails,
|
||||
update_decode_step_metadata=lambda **kwargs: observed.update(kwargs),
|
||||
)
|
||||
ids = torch.tensor([10, 11, 12])
|
||||
runner.use_async_scheduling = async_scheduling
|
||||
runner.input_ids = SimpleNamespace(gpu=ids, cpu=ids)
|
||||
runner.query_start_loc = SimpleNamespace(cpu=torch.tensor([0, 2, 3]))
|
||||
runner.input_batch = SimpleNamespace(req_ids=["a", "b"])
|
||||
runner._build_model_kwargs_extra = lambda: {}
|
||||
monkeypatch.setattr(GPUModelRunner, "_model_forward", lambda *args, **kwargs: torch.ones(2))
|
||||
runner._model_forward(input_ids=ids)
|
||||
expected = [11, 12] if requires_tails and not async_scheduling else None
|
||||
assert observed["cpu_input_tail_ids"] == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("generation", [False, True])
|
||||
@pytest.mark.parametrize("enabled,fail", [(False, False), (True, False), (True, True)])
|
||||
def test_auxiliary_capture_finishes_before_worker_readiness(monkeypatch, generation, enabled, fail):
|
||||
from vllm.v1.worker.gpu_worker import Worker
|
||||
|
||||
from vllm_omni.worker.base import OmniGPUWorkerBase
|
||||
from vllm_omni.worker.gpu_generation_worker import GPUGenerationWorker
|
||||
|
||||
events: list[str] = []
|
||||
|
||||
def capture():
|
||||
assert events == ["warmup"]
|
||||
events.append("capture")
|
||||
if fail:
|
||||
raise RuntimeError("auxiliary capture failed")
|
||||
|
||||
monkeypatch.setattr(Worker, "compile_or_warm_up_model", lambda _: events.append("warmup"))
|
||||
worker = object.__new__(GPUGenerationWorker if generation else OmniGPUWorkerBase)
|
||||
worker.use_v2_model_runner = generation
|
||||
model = SimpleNamespace(capture_auxiliary_graphs=capture) if enabled else SimpleNamespace()
|
||||
worker.model_runner = SimpleNamespace(model=model, profile_run=lambda: events.append("warmup"))
|
||||
if fail:
|
||||
with pytest.raises(RuntimeError, match="auxiliary capture failed"):
|
||||
worker.compile_or_warm_up_model()
|
||||
else:
|
||||
worker.compile_or_warm_up_model()
|
||||
events.append("ready")
|
||||
assert events == ["warmup"] + (["capture"] if enabled else []) + ([] if fail else ["ready"])
|
||||
@@ -0,0 +1,102 @@
|
||||
# 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.config import VllmConfig, set_current_vllm_config
|
||||
from vllm.config.compilation import CUDAGraphMode
|
||||
from vllm.v1.attention.backends.flash_attn import FlashAttentionBackend, FlashAttentionImpl
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheConfig, KVCacheGroupSpec
|
||||
from vllm.v1.worker.gpu.input_batch import InputBatch, InputBuffers
|
||||
from vllm.v1.worker.utils import AttentionGroup
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from vllm_omni.worker_v2.model_states.omni_model_state import OmniModelState
|
||||
|
||||
pytestmark = [pytest.mark.core_model]
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
def test_full_graph_prefill_uses_real_flash_attention_backend():
|
||||
if not torch.cuda.is_available() or torch.cuda.get_device_capability()[0] < 9:
|
||||
pytest.skip("requires FA3 on Hopper or newer")
|
||||
_check_prefill()
|
||||
|
||||
|
||||
def _check_prefill():
|
||||
torch.manual_seed(17)
|
||||
device = torch.device("cuda:0")
|
||||
c = VllmConfig()
|
||||
c.model_config = SimpleNamespace(
|
||||
get_num_attention_heads=lambda p: 32,
|
||||
rswa_window=None,
|
||||
is_diffusion=False,
|
||||
is_mm_prefix_lm=False,
|
||||
hf_config=SimpleNamespace(model_type="qwen3"),
|
||||
)
|
||||
c.scheduler_config.max_num_seqs = 64
|
||||
c.compilation_config.cudagraph_mode = CUDAGraphMode.FULL
|
||||
c.compilation_config.max_cudagraph_capture_size = 256
|
||||
c.attention_config.flash_attn_version = 3
|
||||
spec = FullAttentionSpec(block_size=16, num_kv_heads=8, head_size=128, dtype=torch.bfloat16)
|
||||
kvconfig = KVCacheConfig(num_blocks=1024, kv_cache_tensors=[], kv_cache_groups=[KVCacheGroupSpec(["attn"], spec)])
|
||||
state = OmniModelState.__new__(OmniModelState)
|
||||
state.max_model_len = 256
|
||||
state.supports_mm_inputs = False
|
||||
with set_current_vllm_config(c):
|
||||
group = AttentionGroup(FlashAttentionBackend, ["attn"], spec, 0)
|
||||
group.create_metadata_builders(c, device)
|
||||
impl = FlashAttentionImpl(32, 128, 128**-0.5, 8, None, None, "auto")
|
||||
assert impl.vllm_flash_attn_version == 3
|
||||
buffers = InputBuffers(64, 256, device)
|
||||
batch = InputBatch.make_dummy(64, 256, buffers)
|
||||
tables = torch.arange(1024, dtype=torch.int32, device=device).reshape(64, 16)
|
||||
slots = torch.zeros((1, 256), dtype=torch.int64, device=device)
|
||||
cache = torch.randn(1024, 8, 16, 256, device=device, dtype=torch.bfloat16)
|
||||
q = torch.randn(256, 32, 128, device=device, dtype=torch.bfloat16)
|
||||
k = v = torch.empty(256, 8, 128, device=device, dtype=torch.bfloat16)
|
||||
layer = SimpleNamespace(
|
||||
_q_scale=torch.ones((), device=device),
|
||||
_k_scale=torch.ones((), device=device),
|
||||
_v_scale=torch.ones((), device=device),
|
||||
)
|
||||
batch.query_start_loc.copy_(torch.arange(65, device=device, dtype=torch.int32) * 4)
|
||||
batch.seq_lens.fill_(4)
|
||||
prepare = OmniModelState.prepare_attn
|
||||
md = prepare(state, batch, CUDAGraphMode.NONE, (tables,), slots, [[group]], kvconfig, for_capture=True)["attn"]
|
||||
out = torch.full_like(q, float("nan"))
|
||||
stream = torch.cuda.Stream()
|
||||
stream.wait_stream(torch.cuda.current_stream())
|
||||
with torch.cuda.stream(stream):
|
||||
for _ in range(3):
|
||||
impl.forward(layer, q, k, v, cache, md, out)
|
||||
stream.synchronize()
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph, stream=stream):
|
||||
impl.forward(layer, q, k, v, cache, md, out)
|
||||
# Same captured 64-request shape: one 212-token prompt, 44 single-token
|
||||
# requests, then zero-length padded rows. No backend is mocked.
|
||||
lengths = np.array([212] + [1] * 44 + [0] * 19, dtype=np.int32)
|
||||
batch.query_start_loc.copy_(torch.tensor(np.r_[0, np.cumsum(lengths)], device=device, dtype=torch.int32))
|
||||
batch.seq_lens.copy_(torch.tensor(lengths, device=device))
|
||||
from dataclasses import replace
|
||||
|
||||
runtime = replace(
|
||||
batch,
|
||||
max_query_len=212,
|
||||
num_scheduled_tokens=lengths,
|
||||
query_start_loc_np=np.r_[0, np.cumsum(lengths)],
|
||||
seq_lens_cpu_upper_bound=torch.tensor(lengths),
|
||||
)
|
||||
eager_md = state.prepare_attn(runtime, CUDAGraphMode.NONE, (tables,), slots, [[group]], kvconfig)["attn"]
|
||||
expected = torch.empty_like(q)
|
||||
impl.forward(layer, q, k, v, cache, eager_md, expected)
|
||||
state.prepare_attn(runtime, CUDAGraphMode.NONE, (tables,), slots, [[group]], kvconfig)
|
||||
out.fill_(float("nan"))
|
||||
graph.replay()
|
||||
torch.accelerator.synchronize()
|
||||
assert md.max_query_len == 256
|
||||
torch.testing.assert_close(out[:212], expected[:212], atol=0.002, rtol=0.002)
|
||||
@@ -0,0 +1,169 @@
|
||||
# Higgs v3 Model Runner V2 streaming profile, validated on one H200.
|
||||
# Select an available physical GPU with CUDA_VISIBLE_DEVICES before launch.
|
||||
# First audio: 8 frames (320 ms); following chunks: 25 frames (1 s).
|
||||
# Codec graph windows are exact: 8 first frames, 25 + 8 left context.
|
||||
# AR and codec capacities are 64; tested client concurrencies are 32 and 64.
|
||||
# Keep model weights and sampling parameters identical for comparisons.
|
||||
# Server flags used in measurements: --disable-log-stats --disable-uvicorn-access-log.
|
||||
# Auxiliary graphs are captured after KV sizing and are outside the utilization
|
||||
# budget. Leave at least 10 GiB free for codec graph pools on this H200 profile;
|
||||
# inspect both allocated and reserved capture logs before raising stage budgets.
|
||||
# Sampler graphs cover tensor-valued temperature/top_k/top_p. Disabling top-k
|
||||
# or top-p for an entire batch (None metadata) uses eager sampling.
|
||||
async_chunk: true
|
||||
connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
codec_streaming: true
|
||||
codec_chunk_frames: 25
|
||||
codec_left_context_frames: 8
|
||||
codec_right_holdback_frames: 0
|
||||
initial_codec_chunk_frames: 8
|
||||
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: false
|
||||
attention_backend: FLASH_ATTN
|
||||
trust_remote_code: true
|
||||
enable_prefix_caching: false
|
||||
async_scheduling: true
|
||||
compilation_config:
|
||||
cudagraph_capture_sizes:
|
||||
- 1
|
||||
- 2
|
||||
- 4
|
||||
- 8
|
||||
- 16
|
||||
- 24
|
||||
- 32
|
||||
- 40
|
||||
- 48
|
||||
- 56
|
||||
- 64
|
||||
- 96
|
||||
- 128
|
||||
- 192
|
||||
- 256
|
||||
- 384
|
||||
- 512
|
||||
cudagraph_mode: FULL
|
||||
cudagraph_num_of_warmups: 1
|
||||
max_cudagraph_capture_size: 512
|
||||
cudagraph_copy_inputs: true
|
||||
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
|
||||
hf_overrides:
|
||||
audio_async_prompt_mode: true
|
||||
audio_async_payload: true
|
||||
audio_mrv2_static_inputs: true
|
||||
audio_mrv2_slot_parameters: true
|
||||
audio_mrv2_direct_payload: true
|
||||
audio_mrv2_sample_graph_buckets: true
|
||||
audio_mrv2_async_state_indices: true
|
||||
audio_mrv2_fused_state: true
|
||||
- stage_id: 1
|
||||
max_num_seqs: 64
|
||||
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
|
||||
hf_overrides:
|
||||
codec_graph_single_max_frames: 33
|
||||
codec_graph_batch_sizes:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
- 8
|
||||
- 9
|
||||
- 10
|
||||
- 11
|
||||
- 12
|
||||
- 13
|
||||
- 14
|
||||
- 15
|
||||
- 16
|
||||
- 17
|
||||
- 18
|
||||
- 19
|
||||
- 20
|
||||
- 21
|
||||
- 22
|
||||
- 23
|
||||
- 24
|
||||
- 25
|
||||
- 26
|
||||
- 27
|
||||
- 28
|
||||
- 29
|
||||
- 30
|
||||
- 31
|
||||
- 32
|
||||
- 33
|
||||
- 34
|
||||
- 35
|
||||
- 36
|
||||
- 37
|
||||
- 38
|
||||
- 39
|
||||
- 40
|
||||
- 41
|
||||
- 42
|
||||
- 43
|
||||
- 44
|
||||
- 45
|
||||
- 46
|
||||
- 47
|
||||
- 48
|
||||
- 49
|
||||
- 50
|
||||
- 51
|
||||
- 52
|
||||
- 53
|
||||
- 54
|
||||
- 55
|
||||
- 56
|
||||
- 57
|
||||
- 58
|
||||
- 59
|
||||
- 60
|
||||
- 61
|
||||
- 62
|
||||
- 63
|
||||
- 64
|
||||
codec_graph_frame_sizes:
|
||||
- 8
|
||||
- 33
|
||||
cuda_mps: true
|
||||
model_runner: v2
|
||||
+6
-5
@@ -1280,11 +1280,12 @@ class _OmniConnectorPayloadTransportMixin(_OmniConnectorRuntimeMixin):
|
||||
payload_kwarg: pooling_output,
|
||||
"request": request,
|
||||
}
|
||||
supports_is_finished = getattr(
|
||||
self,
|
||||
"_custom_process_supports_is_finished",
|
||||
self._custom_process_supports_is_finished_kwarg(),
|
||||
)
|
||||
# The signature check is cached at initialization; never re-inspect
|
||||
# the hook for every request of every model step.
|
||||
supports_is_finished = getattr(self, "_custom_process_supports_is_finished", None)
|
||||
if supports_is_finished is None:
|
||||
supports_is_finished = self._custom_process_supports_is_finished_kwarg()
|
||||
self._custom_process_supports_is_finished = supports_is_finished
|
||||
is_finished_fn = getattr(request, "is_finished", None)
|
||||
if callable(is_finished_fn):
|
||||
try:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""HiggsAudio codec decoder kernel for higgs-audio v2.
|
||||
|
||||
This module hosts the parameter-side building blocks for the higgs-audio-v2
|
||||
@@ -16,11 +16,14 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import types
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
__all__ = [
|
||||
"HiggsAudioVQLayer",
|
||||
"HiggsAudioRVQ",
|
||||
@@ -99,6 +102,138 @@ class _Snake1d(nn.Module):
|
||||
return _snake(x, self.alpha)
|
||||
|
||||
|
||||
class _FusedDacSnake1d(nn.Module):
|
||||
"""Drop-in for ``transformers`` ``DacModel`` Snake1d with a fused CUDA kernel.
|
||||
|
||||
Same ``alpha`` parameter, so checkpoints load unchanged. Inference on CUDA
|
||||
runs one bit-identical kernel instead of about seven elementwise launches;
|
||||
everything else keeps the transformers expression.
|
||||
"""
|
||||
|
||||
def __init__(self, channels: int):
|
||||
super().__init__()
|
||||
self.alpha = nn.Parameter(torch.ones(1, channels, 1))
|
||||
self._inverse: tuple[tuple[int, int], torch.Tensor] | None = None
|
||||
|
||||
def can_fuse(self, x: torch.Tensor) -> bool:
|
||||
# The kernel uses NVIDIA PTX and float32 intermediates. ROCm tensors
|
||||
# also report is_cuda; retain the eager path there and for float64.
|
||||
return (
|
||||
current_omni_platform.is_cuda()
|
||||
and x.is_cuda
|
||||
and x.dim() == 3
|
||||
and not torch.is_grad_enabled()
|
||||
and x.dtype in (torch.float16, torch.bfloat16, torch.float32)
|
||||
and x.dtype == self.alpha.dtype
|
||||
)
|
||||
|
||||
def fused(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
bias: torch.Tensor | None = None,
|
||||
residual: torch.Tensor | None = None,
|
||||
write_sum: bool = False,
|
||||
) -> tuple[torch.Tensor | None, torch.Tensor]:
|
||||
"""Snake of ``residual + (x + bias)`` in one kernel; see ``fused_snake1d``."""
|
||||
from .snake_kernel import fused_snake1d
|
||||
|
||||
alpha = self.alpha.detach()
|
||||
try:
|
||||
key = (alpha.data_ptr(), alpha._version)
|
||||
except RuntimeError: # inference tensors carry no version counter
|
||||
key = None
|
||||
if key is not None and self._inverse is not None and self._inverse[0] == key:
|
||||
inverse = self._inverse[1]
|
||||
else:
|
||||
# The eager (alpha + 1e-9).reciprocal(), computed once per weight
|
||||
# update in the same dtype instead of on every call.
|
||||
inverse = (alpha + 1e-9).reciprocal().reshape(-1).contiguous()
|
||||
self._inverse = None if key is None else (key, inverse)
|
||||
return fused_snake1d(x, alpha.reshape(-1), inverse, bias=bias, residual=residual, write_sum=write_sum)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
if self.can_fuse(x):
|
||||
return self.fused(x)[1]
|
||||
shape = x.shape
|
||||
x = x.reshape(shape[0], shape[1], -1)
|
||||
x = x + (self.alpha + 1e-9).reciprocal() * torch.sin(self.alpha * x).pow(2)
|
||||
return x.reshape(shape)
|
||||
|
||||
|
||||
def _conv_without_bias(conv: nn.Module, x: torch.Tensor) -> torch.Tensor:
|
||||
# cuDNN convolutions add the bias as a separate kernel anyway, so leaving
|
||||
# it to the next fused Snake changes no bits.
|
||||
if isinstance(conv, nn.ConvTranspose1d):
|
||||
return nn.functional.conv_transpose1d(
|
||||
x, conv.weight, None, conv.stride, conv.padding, conv.output_padding, conv.groups, conv.dilation
|
||||
)
|
||||
return conv._conv_forward(x, conv.weight, None)
|
||||
|
||||
|
||||
def _fused_dac_decoder_forward(decoder: nn.Module, hidden_state: torch.Tensor) -> torch.Tensor:
|
||||
"""transformers ``DacDecoder.forward`` with conv biases and residual adds
|
||||
folded into the Snake kernels (5 elementwise passes per residual unit -> 2).
|
||||
|
||||
A value is carried as ``residual + (raw + bias)`` until the next Snake
|
||||
consumes it, and only materialized where a residual connection needs it.
|
||||
"""
|
||||
if not decoder.snake1.can_fuse(hidden_state):
|
||||
return type(decoder).forward(decoder, hidden_state)
|
||||
raw, bias, residual = _conv_without_bias(decoder.conv1, hidden_state), decoder.conv1.bias, None
|
||||
for block in decoder.block:
|
||||
_, act = block.snake1.fused(raw, bias, residual)
|
||||
raw, bias, residual = _conv_without_bias(block.conv_t1, act), block.conv_t1.bias, None
|
||||
for unit in (block.res_unit1, block.res_unit2, block.res_unit3):
|
||||
residual, act = unit.snake1.fused(raw, bias, residual, write_sum=True)
|
||||
_, act = unit.snake2.fused(_conv_without_bias(unit.conv1, act), unit.conv1.bias)
|
||||
raw, bias = _conv_without_bias(unit.conv2, act), unit.conv2.bias
|
||||
_, act = decoder.snake1.fused(raw, bias, residual)
|
||||
return decoder.tanh(decoder.conv2(act))
|
||||
|
||||
|
||||
def _fuse_dac_decoder(decoder: nn.Module) -> int:
|
||||
"""Swap transformers DAC Snake1d modules for fused ones (before weights
|
||||
load) and, when the decoder has the stock layout, fuse its forward."""
|
||||
try:
|
||||
from transformers.models.dac.modeling_dac import DacDecoder
|
||||
from transformers.models.dac.modeling_dac import Snake1d as DacSnake1d
|
||||
except ImportError:
|
||||
return 0
|
||||
replaced = 0
|
||||
for module in list(decoder.modules()):
|
||||
for name, child in list(module.named_children()):
|
||||
if isinstance(child, DacSnake1d):
|
||||
fused = _FusedDacSnake1d(int(child.alpha.shape[1]))
|
||||
fused.to(device=child.alpha.device, dtype=child.alpha.dtype)
|
||||
setattr(module, name, fused)
|
||||
replaced += 1
|
||||
if type(decoder) is DacDecoder and _has_fusable_dac_layout(decoder):
|
||||
decoder.forward = types.MethodType(_fused_dac_decoder_forward, decoder)
|
||||
return replaced
|
||||
|
||||
|
||||
def _has_fusable_dac_layout(decoder: nn.Module) -> bool:
|
||||
convs = [decoder.conv1]
|
||||
for block in decoder.block:
|
||||
units = (block.res_unit1, block.res_unit2, block.res_unit3)
|
||||
convs.append(block.conv_t1)
|
||||
for unit in units:
|
||||
conv1, conv2 = unit.conv1, unit.conv2
|
||||
# Length-preserving convs, so the residual add needs no crop.
|
||||
if conv1.stride[0] != 1 or 2 * conv1.padding[0] != conv1.dilation[0] * (conv1.kernel_size[0] - 1):
|
||||
return False
|
||||
if conv2.stride[0] != 1 or conv2.kernel_size[0] != 1 or conv2.padding[0] != 0:
|
||||
return False
|
||||
convs += [conv1, conv2]
|
||||
if not all(isinstance(m, _FusedDacSnake1d) for m in (block.snake1, *(u.snake1 for u in units))):
|
||||
return False
|
||||
if not all(isinstance(u.snake2, _FusedDacSnake1d) for u in units):
|
||||
return False
|
||||
return isinstance(decoder.snake1, _FusedDacSnake1d) and all(
|
||||
conv.bias is not None and conv.padding_mode == "zeros" for conv in convs
|
||||
)
|
||||
|
||||
|
||||
def _wn_conv1d(*args: Any, **kwargs: Any) -> nn.Module:
|
||||
from torch.nn.utils import weight_norm
|
||||
|
||||
@@ -248,6 +383,7 @@ def build_higgs_audio_acoustic_decoder(
|
||||
dac_cfg = DacConfig(**tokenizer_config["acoustic_model_config"])
|
||||
dac_model = DacModel(dac_cfg)
|
||||
decoder = dac_model.decoder.to(device)
|
||||
_fuse_dac_decoder(decoder)
|
||||
adjust_conv_transpose_output_padding(decoder)
|
||||
if hasattr(decoder, "tanh"):
|
||||
decoder.tanh = nn.Identity()
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# ruff: noqa: N803
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Fused DAC Snake activation: ``x + 1 / (alpha + 1e-9) * sin(alpha * x) ** 2``.
|
||||
|
||||
The eager expression launches about seven elementwise kernels per call, each
|
||||
reading and writing the whole activation; in the codec's CUDA graphs they are
|
||||
most of its GPU time. This kernel reads ``x`` once and rounds every
|
||||
intermediate to the tensor dtype exactly where the eager ops do, so outputs are
|
||||
bit-identical to the unfused expression. It can also absorb the preceding conv
|
||||
bias add and residual add, which are otherwise two more full passes each.
|
||||
"""
|
||||
|
||||
import torch
|
||||
from vllm.triton_utils import tl, tldevice, triton
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _mul_rn(a, b):
|
||||
# Explicit round-to-nearest ops are never contracted into FMA, so each
|
||||
# product/sum rounds exactly where the separate eager kernels round.
|
||||
return tl.inline_asm_elementwise("mul.rn.f32 $0, $1, $2;", "=r,r,r", [a, b], dtype=tl.float32, is_pure=True, pack=1)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _add_rn(a, b):
|
||||
return tl.inline_asm_elementwise("add.rn.f32 $0, $1, $2;", "=r,r,r", [a, b], dtype=tl.float32, is_pure=True, pack=1)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _dac_snake_kernel(
|
||||
x_ptr,
|
||||
bias_ptr,
|
||||
residual_ptr,
|
||||
alpha_ptr,
|
||||
inverse_ptr,
|
||||
sum_ptr,
|
||||
out_ptr,
|
||||
stride_b,
|
||||
stride_c,
|
||||
t_len,
|
||||
HAS_BIAS: tl.constexpr,
|
||||
HAS_RESIDUAL: tl.constexpr,
|
||||
WRITE_SUM: tl.constexpr,
|
||||
BLOCK_T: tl.constexpr,
|
||||
):
|
||||
batch = tl.program_id(0).to(tl.int64)
|
||||
channel = tl.program_id(1).to(tl.int64)
|
||||
t = tl.program_id(2) * BLOCK_T + tl.arange(0, BLOCK_T)
|
||||
mask = t < t_len
|
||||
offset = batch * stride_b + channel * stride_c + t
|
||||
x = tl.load(x_ptr + offset, mask=mask, other=0.0)
|
||||
dtype = x.dtype
|
||||
xf = x.to(tl.float32)
|
||||
if HAS_BIAS:
|
||||
# The conv bias add that cuDNN convolutions run as a separate kernel.
|
||||
xf = _add_rn(xf, tl.load(bias_ptr + channel).to(tl.float32)).to(dtype).to(tl.float32)
|
||||
if HAS_RESIDUAL:
|
||||
residual = tl.load(residual_ptr + offset, mask=mask, other=0.0).to(tl.float32)
|
||||
xf = _add_rn(residual, xf).to(dtype).to(tl.float32)
|
||||
if WRITE_SUM:
|
||||
tl.store(sum_ptr + offset, xf.to(dtype), mask=mask)
|
||||
alpha = tl.load(alpha_ptr + channel).to(tl.float32)
|
||||
inverse = tl.load(inverse_ptr + channel).to(tl.float32)
|
||||
scaled = _mul_rn(alpha, xf).to(dtype).to(tl.float32)
|
||||
# libdevice sinf, as the eager CUDA kernel (tl.sin is the approximation).
|
||||
sine = tldevice.sin(scaled).to(dtype).to(tl.float32)
|
||||
square = _mul_rn(sine, sine).to(dtype).to(tl.float32)
|
||||
term = _mul_rn(inverse, square).to(dtype).to(tl.float32)
|
||||
tl.store(out_ptr + offset, _add_rn(xf, term).to(dtype), mask=mask)
|
||||
|
||||
|
||||
def fused_snake1d(
|
||||
x: torch.Tensor,
|
||||
alpha: torch.Tensor,
|
||||
inverse: torch.Tensor,
|
||||
*,
|
||||
bias: torch.Tensor | None = None,
|
||||
residual: torch.Tensor | None = None,
|
||||
write_sum: bool = False,
|
||||
) -> tuple[torch.Tensor | None, torch.Tensor]:
|
||||
"""Snake of ``residual + (x + bias)``, rounded as the separate eager ops.
|
||||
|
||||
x, residual: (B, C, T); alpha, inverse, bias: (C,), all in x's dtype.
|
||||
Returns ``(sum, snake(sum))``; ``sum`` is only materialized when
|
||||
``write_sum`` is set (for the next residual connection), else None.
|
||||
"""
|
||||
x = x.contiguous()
|
||||
if residual is not None:
|
||||
residual = residual.contiguous()
|
||||
batch, channels, t_len = x.shape
|
||||
out = torch.empty_like(x)
|
||||
total = torch.empty_like(x) if write_sum else None
|
||||
if x.numel() == 0:
|
||||
return total, out
|
||||
block = min(triton.next_power_of_2(t_len), 2048)
|
||||
_dac_snake_kernel[(batch, channels, triton.cdiv(t_len, block))](
|
||||
x,
|
||||
x if bias is None else bias,
|
||||
x if residual is None else residual,
|
||||
alpha,
|
||||
inverse,
|
||||
out if total is None else total,
|
||||
out,
|
||||
x.stride(0),
|
||||
x.stride(1),
|
||||
t_len,
|
||||
HAS_BIAS=bias is not None,
|
||||
HAS_RESIDUAL=residual is not None,
|
||||
WRITE_SUM=write_sum,
|
||||
BLOCK_T=block,
|
||||
)
|
||||
return total, out
|
||||
@@ -0,0 +1,332 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""Dense decode sampler graphs captured and parity-checked during startup.
|
||||
|
||||
Capture compares eager and captured state transitions with identical noise.
|
||||
Serving only replays warmed shapes and preserves request torch RNG draws. CPU payload staging
|
||||
remains outside capture. This module does not handle mixed prefill batches.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import time
|
||||
|
||||
import torch
|
||||
from vllm.logger import init_logger
|
||||
from vllm.v1.outputs import SamplerOutput
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
_STATE = (
|
||||
"_decode_last_codes",
|
||||
"_decode_has_codes",
|
||||
"_decode_delay_count",
|
||||
"_decode_eoc_countdown",
|
||||
"_decode_generation_done",
|
||||
)
|
||||
|
||||
|
||||
_BUCKETS = (1, 2, 4, 8, 16, 24, 32, 40, 48, 56, 64, 96, 128)
|
||||
_FIELDS = ("temperature", "top_k", "top_p")
|
||||
|
||||
|
||||
def _graph_key(model, batch, hidden, force_audio_inputs, metadata):
|
||||
return (
|
||||
batch,
|
||||
hidden.dtype,
|
||||
hidden.device,
|
||||
bool(force_audio_inputs),
|
||||
bool(metadata.all_greedy),
|
||||
tuple(getattr(metadata, f) is None for f in _FIELDS),
|
||||
tuple(getattr(model, name).data_ptr() for name in _STATE),
|
||||
)
|
||||
|
||||
|
||||
def _replay_captured(model, hidden, metadata, force_audio_inputs):
|
||||
"""Replay an already captured (bucket) graph writing inputs in place.
|
||||
|
||||
The capture path below pads through temporaries on every call. Once a
|
||||
graph exists, only the live rows are written into its static inputs:
|
||||
padded rows keep finite values from earlier steps and own no request
|
||||
state, forced-audio inputs are constants, sampling parameters are copied
|
||||
only when the cached metadata changes, and request noise is drawn directly
|
||||
into the graph's noise input.
|
||||
"""
|
||||
cache = getattr(model, "_dense_sample_graphs", None)
|
||||
if not cache:
|
||||
return None
|
||||
batch = hidden.shape[0]
|
||||
bucket = batch
|
||||
if getattr(model.config, "audio_mrv2_sample_graph_buckets", False):
|
||||
bucket = next((n for n in _BUCKETS if n >= batch), batch)
|
||||
model._ensure_decode_state_capacity(bucket, hidden.device)
|
||||
key = _graph_key(model, bucket, hidden, force_audio_inputs, metadata)
|
||||
entry = cache.get(key)
|
||||
if entry is None:
|
||||
return None
|
||||
graph, static_hidden, static_input_ids, static_noise, static_metadata, output, staging, keepalive = entry
|
||||
books = model.num_codebooks
|
||||
static_hidden[:batch].copy_(hidden)
|
||||
if not force_audio_inputs:
|
||||
static_input_ids[:batch].copy_(model._last_step_input_ids)
|
||||
if batch < bucket:
|
||||
static_input_ids[batch:].fill_(model._audio_continuation_id)
|
||||
if metadata.generators:
|
||||
from vllm_omni.utils.seeded_exponential import fill_exponential_rows
|
||||
|
||||
fill_exponential_rows(static_noise[: batch * books], [metadata.generators.get(i) for i in range(batch)])
|
||||
else:
|
||||
static_noise[: batch * books].exponential_()
|
||||
sources = getattr(model, "_dense_sample_metadata_sources", None)
|
||||
if sources is None:
|
||||
sources = model._dense_sample_metadata_sources = {}
|
||||
if sources.get(key) is not metadata:
|
||||
for name in _FIELDS:
|
||||
value = getattr(metadata, name)
|
||||
if value is not None:
|
||||
getattr(static_metadata, name)[:batch].copy_(value)
|
||||
sources[key] = metadata
|
||||
graph.replay()
|
||||
model._finish_audio_staging(staging[:batch], batch, hidden.device, batch)
|
||||
return SamplerOutput(sampled_token_ids=output.sampled_token_ids[:batch].clone(), logprobs_tensors=None)
|
||||
|
||||
|
||||
def run_dense_sample(model, hidden, logits, metadata, force_audio_inputs=False, active_batch=None):
|
||||
batch = hidden.shape[0]
|
||||
if active_batch is None:
|
||||
replayed = _replay_captured(model, hidden, metadata, force_audio_inputs)
|
||||
if replayed is not None:
|
||||
return replayed
|
||||
# Serving never captures a new shape. Unsupported shapes/metadata use the
|
||||
# regular sampler; startup owns capture and its parity checks.
|
||||
if not getattr(model, "_warming_sample_graphs", False):
|
||||
return None
|
||||
if active_batch is None and getattr(model.config, "audio_mrv2_sample_graph_buckets", False):
|
||||
bucket = next((n for n in (1, 2, 4, 8, 16, 24, 32, 40, 48, 56, 64, 96, 128) if n >= batch), batch)
|
||||
if bucket != batch:
|
||||
# Dummy rows own no request state. The batch-state gather replaces
|
||||
# these rows before any later request can use them.
|
||||
model._ensure_decode_state_capacity(bucket, hidden.device)
|
||||
padded_hidden = hidden.new_zeros((bucket, hidden.shape[1]))
|
||||
padded_hidden[:batch].copy_(hidden)
|
||||
padded_metadata = copy.copy(metadata)
|
||||
for name in ("temperature", "top_k", "top_p"):
|
||||
value = getattr(metadata, name)
|
||||
if value is not None:
|
||||
padded = value.new_ones(bucket)
|
||||
padded[:batch].copy_(value)
|
||||
setattr(padded_metadata, name, padded)
|
||||
original_ids = model._last_step_input_ids
|
||||
padded_ids = original_ids.new_full((bucket,), model._audio_continuation_id)
|
||||
if not force_audio_inputs:
|
||||
padded_ids[:batch].copy_(original_ids)
|
||||
model._last_step_input_ids = padded_ids
|
||||
try:
|
||||
output = run_dense_sample(
|
||||
model,
|
||||
padded_hidden,
|
||||
logits.new_empty((bucket, logits.shape[1])),
|
||||
padded_metadata,
|
||||
force_audio_inputs=force_audio_inputs,
|
||||
active_batch=batch,
|
||||
)
|
||||
finally:
|
||||
model._last_step_input_ids = original_ids
|
||||
return SamplerOutput(sampled_token_ids=output.sampled_token_ids[:batch], logprobs_tensors=None)
|
||||
actual = batch if active_batch is None else active_batch
|
||||
effective_ids = (
|
||||
torch.full((batch,), model._audio_continuation_id, dtype=model._last_step_input_ids.dtype, device=hidden.device)
|
||||
if force_audio_inputs
|
||||
else model._last_step_input_ids
|
||||
)
|
||||
books = model.num_codebooks
|
||||
vocab = model.modality_head.vocab_size
|
||||
noise = torch.empty((batch * books, vocab), dtype=torch.float32, device=hidden.device)
|
||||
if actual < batch:
|
||||
noise[actual * books :].fill_(1)
|
||||
if metadata.generators:
|
||||
from vllm_omni.utils.seeded_exponential import fill_exponential_rows
|
||||
|
||||
fill_exponential_rows(noise[: actual * books], [metadata.generators.get(i) for i in range(actual)])
|
||||
else:
|
||||
noise[: actual * books].exponential_()
|
||||
fields = _FIELDS
|
||||
# force_audio_inputs controls capture-time Python branches (including
|
||||
# the mixed-tail assertion). Never reuse that graph for real EOS inputs.
|
||||
key = _graph_key(model, batch, hidden, force_audio_inputs, metadata)
|
||||
cache = getattr(model, "_dense_sample_graphs", None)
|
||||
if cache is None:
|
||||
cache = model._dense_sample_graphs = {}
|
||||
entry = cache.get(key)
|
||||
if entry is None:
|
||||
static_hidden = hidden.clone()
|
||||
static_input_ids = effective_ids.clone()
|
||||
static_noise = noise.clone()
|
||||
static_metadata = copy.copy(metadata)
|
||||
for name in fields:
|
||||
value = getattr(metadata, name)
|
||||
setattr(static_metadata, name, None if value is None else value.clone())
|
||||
# Ensure the reusable staging exists before capture. Each graph keeps
|
||||
# its own reference if a later larger batch grows the model buffer.
|
||||
staging = model._get_audio_gpu_staging_buffer(batch, hidden.device)
|
||||
state = {name: getattr(model, name).clone() for name in _STATE}
|
||||
original_ids = model._last_step_input_ids
|
||||
original_qsl = model._last_step_query_start_loc
|
||||
original_mode_rows = getattr(model, "_step_audio_mode_rows", 0)
|
||||
original_tail_rows = getattr(model, "_step_audio_tail_rows", 0)
|
||||
original_direct_rows = getattr(model, "_fast_audio_direct_rows", 0)
|
||||
# Dense decode has one token per row. Own this input instead of
|
||||
# capturing a view of the runner's resizable staging buffer.
|
||||
static_qsl = torch.arange(batch + 1, dtype=torch.int32, device=hidden.device)
|
||||
|
||||
def restore():
|
||||
for name, value in state.items():
|
||||
getattr(model, name).copy_(value)
|
||||
|
||||
try:
|
||||
model._in_audio_sample_graph = True
|
||||
# Outer eligibility and real-input validation already ran. The
|
||||
# owned graph inputs represent exactly one audio-state row each.
|
||||
# Normalize capture-time host dispatch; never capture an outer
|
||||
# mixed-batch assertion or a stock-sampler branch into this graph.
|
||||
model._step_audio_mode_rows = 0
|
||||
model._step_audio_tail_rows = 0
|
||||
model._fast_audio_direct_rows = batch
|
||||
model._audio_graph_noise = static_noise
|
||||
model._last_step_input_ids = static_input_ids
|
||||
model._last_step_query_start_loc = static_qsl
|
||||
model._last_logits_hidden = static_hidden
|
||||
reference = model.sample(logits, static_metadata)
|
||||
expected_ids = reference.sampled_token_ids.clone()
|
||||
expected_staging = staging.clone()
|
||||
expected_state = {name: getattr(model, name).clone() for name in _STATE}
|
||||
restore()
|
||||
model._last_logits_hidden = static_hidden
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
output = model.sample(logits, static_metadata)
|
||||
# CUDA capture records work; it does not execute the transition.
|
||||
# Validate only after replay from the preserved initial state.
|
||||
restore()
|
||||
graph.replay()
|
||||
torch.testing.assert_close(output.sampled_token_ids, expected_ids, atol=0, rtol=0)
|
||||
torch.testing.assert_close(staging, expected_staging, atol=0, rtol=0)
|
||||
for name, expected in expected_state.items():
|
||||
torch.testing.assert_close(getattr(model, name), expected, atol=0, rtol=0)
|
||||
restore()
|
||||
logger.debug("Higgs sample graph parity checked for batch=%d", batch)
|
||||
finally:
|
||||
model._in_audio_sample_graph = False
|
||||
model._step_audio_mode_rows = original_mode_rows
|
||||
model._step_audio_tail_rows = original_tail_rows
|
||||
model._fast_audio_direct_rows = original_direct_rows
|
||||
model._audio_graph_noise = None
|
||||
model._last_step_input_ids = original_ids
|
||||
model._last_step_query_start_loc = original_qsl
|
||||
model._last_logits_hidden = None
|
||||
# Graphs reference storage, not Python owners. Model caches and state
|
||||
# can grow after capture; keep their original allocations alive.
|
||||
keepalive = [getattr(model, name) for name in _STATE]
|
||||
for name in ("_row_index_cache", "_codebook_index_cache", "_boc_frame_cache"):
|
||||
keepalive.extend(getattr(model, name).values())
|
||||
keepalive.append(static_qsl)
|
||||
entry = (graph, static_hidden, static_input_ids, static_noise, static_metadata, output, staging, keepalive)
|
||||
cache[key] = entry
|
||||
graph, static_hidden, static_input_ids, static_noise, static_metadata, output, staging, keepalive = entry
|
||||
static_hidden.copy_(hidden)
|
||||
static_input_ids.copy_(effective_ids)
|
||||
static_noise.copy_(noise)
|
||||
for name in fields:
|
||||
value = getattr(metadata, name)
|
||||
if value is not None:
|
||||
getattr(static_metadata, name).copy_(value)
|
||||
graph.replay()
|
||||
model._finish_audio_staging(staging[:actual], actual, hidden.device, actual)
|
||||
return SamplerOutput(sampled_token_ids=output.sampled_token_ids.clone(), logprobs_tensors=None)
|
||||
|
||||
|
||||
@torch.inference_mode()
|
||||
def capture_sample_graphs(model, max_num_reqs):
|
||||
"""Warm the MRv2 sampler without consuming request RNG or decode state."""
|
||||
if not model._sample_graph_enabled or getattr(model, "_sample_graphs_ready", False):
|
||||
return
|
||||
from vllm.v1.sample.logits_processor import LogitsProcessors
|
||||
from vllm.v1.sample.metadata import SamplingMetadata
|
||||
|
||||
device = model.model.embed_tokens.weight.device
|
||||
if device.type != "cuda":
|
||||
return
|
||||
start = time.perf_counter()
|
||||
memory_before = torch.accelerator.memory_allocated(device)
|
||||
reserved_before = torch.accelerator.memory_reserved(device)
|
||||
model._resolve_token_ids()
|
||||
buckets = (
|
||||
sorted({next((b for b in _BUCKETS if b >= n), n) for n in range(1, max_num_reqs + 1)})
|
||||
if getattr(model.config, "audio_mrv2_sample_graph_buckets", False)
|
||||
else range(1, max_num_reqs + 1)
|
||||
)
|
||||
model._ensure_decode_state_capacity(max(buckets), device)
|
||||
model._get_audio_gpu_staging_buffer(max(buckets), device)
|
||||
saved_state = {name: getattr(model, name).clone() for name in _STATE}
|
||||
fields = (
|
||||
"_last_step_input_ids",
|
||||
"_last_step_query_start_loc",
|
||||
"_last_logits_hidden",
|
||||
"_step_audio_mode_rows",
|
||||
"_step_audio_tail_rows",
|
||||
"_fast_audio_direct_rows",
|
||||
)
|
||||
saved = {name: getattr(model, name, None) for name in fields}
|
||||
try:
|
||||
model._warming_sample_graphs = True
|
||||
with torch.random.fork_rng(devices=[device.index]):
|
||||
for batch in buckets:
|
||||
hidden = model.model.embed_tokens.weight.new_zeros((batch, model.model.embed_tokens.weight.shape[1]))
|
||||
logits = hidden.new_empty((batch, model.config.text_config.vocab_size))
|
||||
model._last_step_input_ids = torch.full(
|
||||
(batch,), model._audio_continuation_id, device=device, dtype=torch.long
|
||||
)
|
||||
model._last_step_query_start_loc = torch.arange(batch + 1, device=device, dtype=torch.int32)
|
||||
ones = torch.ones(batch, device=device)
|
||||
zeros = torch.zeros_like(ones)
|
||||
for greedy in (False, True):
|
||||
metadata = SamplingMetadata(
|
||||
temperature=zeros if greedy else ones,
|
||||
all_greedy=greedy,
|
||||
all_random=not greedy,
|
||||
top_p=ones,
|
||||
top_k=torch.full((batch,), -1, device=device, dtype=torch.long),
|
||||
generators={},
|
||||
max_num_logprobs=None,
|
||||
no_penalties=True,
|
||||
prompt_token_ids=None,
|
||||
frequency_penalties=zeros,
|
||||
presence_penalties=zeros,
|
||||
repetition_penalties=ones,
|
||||
output_token_ids=[[] for _ in range(batch)],
|
||||
allowed_token_ids_mask=None,
|
||||
bad_words_token_ids={},
|
||||
logitsprocs=LogitsProcessors(),
|
||||
)
|
||||
for forced in (False, True):
|
||||
run_dense_sample(model, hidden, logits, metadata, force_audio_inputs=forced)
|
||||
finally:
|
||||
for name, value in saved_state.items():
|
||||
getattr(model, name).copy_(value)
|
||||
for name, value in saved.items():
|
||||
setattr(model, name, value)
|
||||
model._warming_sample_graphs = False
|
||||
model._clear_last_audio_outputs()
|
||||
model._async_audio_gpu_staging = None
|
||||
model._dense_sample_metadata_sources = {}
|
||||
torch.accelerator.synchronize()
|
||||
model._sample_graphs_ready = True
|
||||
logger.info(
|
||||
"Higgs sampler captured %d CUDA graphs in %.2f s "
|
||||
"(allocated delta %.1f MiB, reserved delta %.1f MiB, reserved total %.1f MiB)",
|
||||
len(model._dense_sample_graphs),
|
||||
time.perf_counter() - start,
|
||||
(torch.accelerator.memory_allocated(device) - memory_before) / 2**20,
|
||||
(torch.accelerator.memory_reserved(device) - reserved_before) / 2**20,
|
||||
torch.accelerator.memory_reserved(device) / 2**20,
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Stage 1 (codec decoder) for higgs-audio v3.
|
||||
|
||||
Reuses higgs-audio-v2's RVQ + DAC codec decoder but loads weights from the
|
||||
@@ -10,6 +10,7 @@ prefix) rather than from a standalone tokenizer repo.
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
@@ -57,6 +58,9 @@ class HiggsAudioV3Code2Wav(nn.Module):
|
||||
|
||||
input_modalities = "audio"
|
||||
|
||||
supports_native_payload_input = True
|
||||
batched_gpu_staging_keys = {("codes", "audio")}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: HiggsAudioV3Config | None = None,
|
||||
@@ -81,6 +85,11 @@ class HiggsAudioV3Code2Wav(nn.Module):
|
||||
self._model_path = None
|
||||
self.vllm_config = None
|
||||
|
||||
_, self._codec_graph_enabled = self.config.resolve_graph_defaults(
|
||||
use_v2_model_runner=bool(
|
||||
getattr(getattr(self.vllm_config, "model_config", None), "use_v2_model_runner", False)
|
||||
)
|
||||
)
|
||||
self.sample_rate: int = int(self.config.sample_rate)
|
||||
self.num_codebooks: int = int(self.config.num_codebooks)
|
||||
self.num_real_codes: int = int(self.config.num_real_codes)
|
||||
@@ -97,6 +106,8 @@ class HiggsAudioV3Code2Wav(nn.Module):
|
||||
self.quantizer: HiggsAudioRVQ | None = None
|
||||
self.fc2: nn.Linear | None = None
|
||||
self.acoustic_decoder: nn.Module | None = None
|
||||
self._decode_graphs: dict[tuple[int, int], tuple[Any, torch.Tensor, torch.Tensor]] = {}
|
||||
self._decode_graphs_initialized = False
|
||||
self._loaded: bool = False
|
||||
# Do NOT eagerly load from standalone tokenizer here.
|
||||
# load_weights() will try bundled V3 codec first, then fall back.
|
||||
@@ -452,6 +463,30 @@ class HiggsAudioV3Code2Wav(nn.Module):
|
||||
self._ensure_codec_loaded()
|
||||
|
||||
codes = self._validate_codes(audio_codes)
|
||||
if codes.is_cuda and self._codec_graph_enabled:
|
||||
batch, _, frames = codes.shape
|
||||
entry = self._decode_graphs.get((batch, frames))
|
||||
if entry is not None:
|
||||
graph, static_codes, static_audio = entry
|
||||
static_codes[:batch].copy_(codes)
|
||||
graph.replay()
|
||||
# The next group may replay the same graph. Own the output
|
||||
# before reusing its storage or shared graph memory pool.
|
||||
return static_audio[:batch].clone()
|
||||
return self._decode_codes_impl(codes)
|
||||
|
||||
@torch.inference_mode()
|
||||
def capture_auxiliary_graphs(self) -> None:
|
||||
"""Capture exact codec shapes before the generation worker is ready."""
|
||||
if not self._codec_graph_enabled or self._decode_graphs_initialized:
|
||||
return
|
||||
if not self._loaded:
|
||||
self._ensure_codec_loaded()
|
||||
device = self.fc2.weight.device
|
||||
if device.type == "cuda":
|
||||
self._capture_decode_graphs(device)
|
||||
|
||||
def _decode_codes_impl(self, codes: torch.Tensor) -> torch.Tensor:
|
||||
rvq_codes = codes.transpose(0, 1).long()
|
||||
quantized = self.quantizer.decode(rvq_codes)
|
||||
quantized = quantized.to(dtype=self.fc2.weight.dtype)
|
||||
@@ -464,6 +499,55 @@ class HiggsAudioV3Code2Wav(nn.Module):
|
||||
audio = audio.unsqueeze(1)
|
||||
return audio.to(codes.device)
|
||||
|
||||
def _decode_graph_shapes(self) -> list[tuple[int, int]]:
|
||||
single_max = int(getattr(self.config, "codec_graph_single_max_frames", 150))
|
||||
batches = getattr(self.config, "codec_graph_batch_sizes", (2, 4, 8, 16))
|
||||
frames = getattr(self.config, "codec_graph_frame_sizes", (5, 29, 54))
|
||||
if single_max < 1 or any(int(n) < 1 for n in (*batches, *frames)):
|
||||
raise ValueError("Higgs codec graph dimensions must be positive")
|
||||
return sorted({(1, n) for n in range(1, single_max + 1)} | {(int(b), int(f)) for b in batches for f in frames})
|
||||
|
||||
@torch.inference_mode()
|
||||
def _capture_decode_graphs(self, device: torch.device) -> None:
|
||||
# Never pad time: convolution boundary conditions must stay exact.
|
||||
# Batch size also stays exact: padding can change convolution
|
||||
# algorithms and rounding. Uncovered shapes retain the eager path.
|
||||
start = time.perf_counter()
|
||||
memory_before = torch.accelerator.memory_allocated(device)
|
||||
reserved_before = torch.accelerator.memory_reserved(device)
|
||||
shapes = self._decode_graph_shapes()
|
||||
previous_device = current_omni_platform.current_device()
|
||||
current_omni_platform.set_device(device)
|
||||
try:
|
||||
stream = torch.cuda.Stream(device=device)
|
||||
stream.wait_stream(torch.cuda.current_stream(device))
|
||||
pool = torch.cuda.graph_pool_handle()
|
||||
for batch, frames in shapes:
|
||||
codes = torch.zeros((batch, self.num_codebooks, frames), device=device, dtype=torch.long)
|
||||
stream.wait_stream(torch.cuda.current_stream(device))
|
||||
with torch.cuda.stream(stream):
|
||||
for _ in range(3):
|
||||
self._decode_codes_impl(codes)
|
||||
stream.synchronize()
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph, pool=pool, stream=stream):
|
||||
audio = self._decode_codes_impl(codes)
|
||||
self._decode_graphs[batch, frames] = (graph, codes, audio)
|
||||
torch.cuda.current_stream(device).wait_stream(stream)
|
||||
finally:
|
||||
current_omni_platform.set_device(torch.device(device.type, previous_device))
|
||||
self._decode_graphs_initialized = True
|
||||
torch.accelerator.synchronize()
|
||||
logger.info(
|
||||
"Higgs codec captured %d exact-frame CUDA graphs in %.2f s "
|
||||
"(allocated delta %.1f MiB, reserved delta %.1f MiB, reserved total %.1f MiB)",
|
||||
len(self._decode_graphs),
|
||||
time.perf_counter() - start,
|
||||
(torch.accelerator.memory_allocated(device) - memory_before) / 2**20,
|
||||
(torch.accelerator.memory_reserved(device) - reserved_before) / 2**20,
|
||||
torch.accelerator.memory_reserved(device) / 2**20,
|
||||
)
|
||||
|
||||
@torch.inference_mode()
|
||||
def forward_chunk(
|
||||
self,
|
||||
@@ -518,6 +602,18 @@ class HiggsAudioV3Code2Wav(nn.Module):
|
||||
ids = input_ids.reshape(-1).to(dtype=torch.long)
|
||||
request_ids_list = self._split_request_ids(ids, kwargs.get("seq_token_counts"))
|
||||
|
||||
# Native MRV2 uses token placeholders for scheduling. The connector
|
||||
# payload owns the actual codebook-major codec sequence, including an
|
||||
# empty terminal payload which must not decode a placeholder token.
|
||||
if runtime_additional_information is not None:
|
||||
for i, info in enumerate(runtime_additional_information):
|
||||
if i >= len(request_ids_list):
|
||||
break
|
||||
codes = info.get("codes", {}) if isinstance(info, dict) else {}
|
||||
audio = codes.get("audio") if isinstance(codes, dict) else None
|
||||
if isinstance(audio, torch.Tensor):
|
||||
request_ids_list[i] = audio.reshape(-1).to(device=ids.device, dtype=torch.long)
|
||||
|
||||
left_context_size = [0] * len(request_ids_list)
|
||||
right_holdback_size = [0] * len(request_ids_list)
|
||||
if runtime_additional_information is not None:
|
||||
@@ -530,11 +626,14 @@ class HiggsAudioV3Code2Wav(nn.Module):
|
||||
if "right_holdback_size" in meta:
|
||||
right_holdback_size[i] = int(meta["right_holdback_size"])
|
||||
|
||||
wavs: list[torch.Tensor] = []
|
||||
wavs: list[torch.Tensor] = [empty] * len(request_ids_list)
|
||||
# Requests with identical codec windows are independent along B. Keep
|
||||
# time lengths and overlap trims exact rather than padding the temporal
|
||||
# axis, which could change the convolution boundary conditions.
|
||||
groups: dict[tuple[int, int, int], list[tuple[int, torch.Tensor]]] = {}
|
||||
for i, req_ids in enumerate(request_ids_list):
|
||||
n = int(req_ids.numel())
|
||||
if n == 0:
|
||||
wavs.append(empty)
|
||||
continue
|
||||
if n % self.num_codebooks != 0:
|
||||
logger.warning(
|
||||
@@ -542,23 +641,43 @@ class HiggsAudioV3Code2Wav(nn.Module):
|
||||
n,
|
||||
self.num_codebooks,
|
||||
)
|
||||
wavs.append(empty)
|
||||
continue
|
||||
frames = n // self.num_codebooks
|
||||
codes_qf = req_ids.reshape(self.num_codebooks, frames)
|
||||
codes_bqf = codes_qf.unsqueeze(0)
|
||||
key = (frames, left_context_size[i], right_holdback_size[i])
|
||||
groups.setdefault(key, []).append((i, req_ids.reshape(self.num_codebooks, frames)))
|
||||
|
||||
for (_, left, right), group in groups.items():
|
||||
codes_bqf = group[0][1].unsqueeze(0) if len(group) == 1 else torch.stack([codes for _, codes in group])
|
||||
try:
|
||||
pcm = self.forward_chunk(
|
||||
codes_bqf,
|
||||
left_context_size=left_context_size[i],
|
||||
right_holdback_size=right_holdback_size[i],
|
||||
left_context_size=left,
|
||||
right_holdback_size=right,
|
||||
hop_length=self.hop_length,
|
||||
)
|
||||
except ValueError as exc:
|
||||
logger.warning("HiggsAudioV3Code2Wav: decode skipped (%s)", exc)
|
||||
wavs.append(empty)
|
||||
if len(group) == 1:
|
||||
logger.warning("HiggsAudioV3Code2Wav: decode skipped (%s)", exc)
|
||||
continue
|
||||
# An invalid row must not discard valid peers. This slow path
|
||||
# retains the previous per-request validation/error behavior.
|
||||
for index, codes in group:
|
||||
try:
|
||||
row_pcm = self.forward_chunk(
|
||||
codes.unsqueeze(0),
|
||||
left_context_size=left,
|
||||
right_holdback_size=right,
|
||||
hop_length=self.hop_length,
|
||||
)
|
||||
wavs[index] = row_pcm[0, 0].to(torch.float32)
|
||||
except ValueError as row_exc:
|
||||
logger.warning("HiggsAudioV3Code2Wav: decode skipped (%s)", row_exc)
|
||||
continue
|
||||
wavs.append(pcm.squeeze(0).squeeze(0).to(torch.float32).cpu())
|
||||
pcm = pcm.to(torch.float32)
|
||||
for row, (index, _) in enumerate(group):
|
||||
# The generation runner owns the batched pinned D2H copy and
|
||||
# its completion event; avoid blocking once per request here.
|
||||
wavs[index] = pcm[row, 0]
|
||||
|
||||
return OmniOutput(
|
||||
text_hidden_states=None,
|
||||
|
||||
@@ -108,12 +108,26 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
prefer_model_sampler: bool = True
|
||||
# Tell the runner to call postprocess() to emit per-step audio codes.
|
||||
have_multimodal_outputs: bool = True
|
||||
has_postprocess: bool = True
|
||||
has_postprocess: bool = False
|
||||
skips_model_sampler_output_token_history: bool = True
|
||||
postprocess_uses_hidden_states: bool = False
|
||||
postprocess_uses_multimodal_outputs: bool = False
|
||||
postprocess_uses_req_infos: bool = False
|
||||
supports_omni_query_start_loc: bool = True
|
||||
requires_cpu_input_tail_ids: bool = True
|
||||
# The codec stage consumes audio codes, never backbone hidden states.
|
||||
omni_pooler_payload_include_hidden: bool = False
|
||||
|
||||
def capture_auxiliary_graphs(self) -> None:
|
||||
from .full_sample_graph import capture_sample_graphs
|
||||
|
||||
scheduler = self.vllm_config.scheduler_config
|
||||
capture_sample_graphs(self, scheduler.max_num_seqs)
|
||||
|
||||
def create_omni_model_state(self, vllm_config, encoder_cache, device):
|
||||
from .model_state import HiggsModelState
|
||||
|
||||
return HiggsModelState(vllm_config, self, encoder_cache, device)
|
||||
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
@@ -122,6 +136,17 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
self.config = hf_config
|
||||
else:
|
||||
self.config = HiggsAudioV3Config(**hf_config.to_dict())
|
||||
self._sample_graph_enabled, _ = self.config.resolve_graph_defaults(
|
||||
use_v2_model_runner=bool(getattr(vllm_config.model_config, "use_v2_model_runner", False))
|
||||
)
|
||||
self.use_async_omni_output = getattr(self.config, "audio_async_payload", False) is True
|
||||
self.supports_async_whole_payload = self.use_async_omni_output
|
||||
if self.use_async_omni_output:
|
||||
if not vllm_config.scheduler_config.async_scheduling:
|
||||
raise ValueError("audio_async_payload requires async scheduling")
|
||||
if getattr(self.config, "audio_async_prompt_mode", False) is not True:
|
||||
raise ValueError("audio_async_payload requires audio prompt mode classification")
|
||||
self._async_audio_gpu_staging = None
|
||||
model_config = getattr(vllm_config, "model_config", None)
|
||||
compilation_config = getattr(vllm_config, "compilation_config", None)
|
||||
cudagraph_mode = getattr(compilation_config, "cudagraph_mode", None)
|
||||
@@ -345,10 +370,26 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
omni_query_start_loc: torch.Tensor | None = None,
|
||||
req_ids: list[str] | None = None,
|
||||
cpu_input_tail_ids: list[int] | None = None,
|
||||
audio_prompt_mode_rows: int = 0,
|
||||
**_: Any,
|
||||
) -> None:
|
||||
"""Update per-step metadata before runner forward or CUDA graph replay."""
|
||||
_ = positions, inputs_embeds
|
||||
self._step_audio_mode_rows = (
|
||||
audio_prompt_mode_rows if getattr(self.config, "audio_async_prompt_mode", False) is True else 0
|
||||
)
|
||||
self._step_audio_tail_rows = 0
|
||||
if cpu_input_tail_ids:
|
||||
self._resolve_token_ids()
|
||||
if (
|
||||
cpu_input_tail_ids
|
||||
and self._audio_continuation_id is not None
|
||||
and req_ids is not None
|
||||
and len(cpu_input_tail_ids) == len(req_ids)
|
||||
and all(token == self._audio_continuation_id for token in cpu_input_tail_ids)
|
||||
):
|
||||
self._step_audio_tail_rows = len(cpu_input_tail_ids)
|
||||
if input_ids is not None:
|
||||
self._last_step_input_ids = input_ids
|
||||
if self._use_external_decode_cudagraph and input_ids.is_cuda and input_ids.ndim >= 1:
|
||||
@@ -359,6 +400,9 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
if req_ids is not None:
|
||||
self._sync_decode_state_with_batch(req_ids)
|
||||
|
||||
def finish_decode_step_forward(self) -> None:
|
||||
self._decode_step_metadata_from_runner = False
|
||||
|
||||
def _ensure_state_pool_capacity(self, num_rows: int, device: torch.device) -> None:
|
||||
if not hasattr(self, "_state_pool_indices"):
|
||||
self._state_pool_indices: dict[str, int] = {}
|
||||
@@ -387,17 +431,53 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
self._state_pool_eoc_countdown = resize(self._state_pool_eoc_countdown, -1)
|
||||
self._state_pool_generation_done = resize(self._state_pool_generation_done, False)
|
||||
|
||||
def _state_indices(self, rows: list[int], device: torch.device) -> torch.Tensor:
|
||||
# A fresh pinned allocation is owned by PyTorch's host allocator until
|
||||
# its asynchronous copy completes; never overwrite a shared CPU staging
|
||||
# buffer while the previous decode step may still be reading it.
|
||||
if device.type == "cuda" and getattr(self.config, "audio_mrv2_async_state_indices", False):
|
||||
return torch.tensor(rows, dtype=torch.long, pin_memory=True).to(device, non_blocking=True)
|
||||
return torch.tensor(rows, dtype=torch.long, device=device)
|
||||
|
||||
def _sync_decode_state_with_batch(self, req_ids: list[str]) -> None:
|
||||
device = self._decode_has_codes.device
|
||||
self._ensure_state_pool_capacity(len(req_ids), device)
|
||||
previous_req_ids = getattr(self, "_last_batch_req_ids", [])
|
||||
|
||||
if (
|
||||
device.type == "cuda"
|
||||
and getattr(self.config, "audio_mrv2_fused_state", False)
|
||||
and (previous_req_ids != req_ids or any(r not in self._state_pool_indices for r in req_ids))
|
||||
):
|
||||
from .mrv2_state_kernels import sync_state
|
||||
|
||||
previous_rows = [row for row, req_id in enumerate(previous_req_ids) if req_id in self._state_pool_indices]
|
||||
pool_rows = [self._state_pool_indices[previous_req_ids[row]] for row in previous_rows]
|
||||
current_rows = []
|
||||
for req_id in req_ids:
|
||||
fresh = req_id not in self._state_pool_indices
|
||||
if fresh:
|
||||
slot = (
|
||||
self._free_state_pool_indices.pop()
|
||||
if self._free_state_pool_indices
|
||||
else len(self._state_pool_indices)
|
||||
)
|
||||
self._ensure_state_pool_capacity(slot + 1, device)
|
||||
self._state_pool_indices[req_id] = slot
|
||||
slot = self._state_pool_indices[req_id]
|
||||
current_rows.append(-slot - 1 if fresh else slot)
|
||||
self._ensure_decode_state_capacity(len(req_ids), device)
|
||||
if req_ids or previous_rows:
|
||||
sync_state(self, previous_rows, pool_rows, current_rows)
|
||||
self._last_batch_req_ids = list(req_ids)
|
||||
return
|
||||
|
||||
if previous_req_ids != req_ids or any(req_id not in self._state_pool_indices for req_id in req_ids):
|
||||
previous_rows = [row for row, req_id in enumerate(previous_req_ids) if req_id in self._state_pool_indices]
|
||||
if previous_rows:
|
||||
pool_rows = [self._state_pool_indices[previous_req_ids[row]] for row in previous_rows]
|
||||
src = torch.tensor(previous_rows, dtype=torch.long, device=device)
|
||||
dst = torch.tensor(pool_rows, dtype=torch.long, device=device)
|
||||
src = self._state_indices(previous_rows, device)
|
||||
dst = self._state_indices(pool_rows, device)
|
||||
self._state_pool_has_codes[dst] = self._decode_has_codes[src]
|
||||
self._state_pool_last_codes[dst] = self._decode_last_codes[src]
|
||||
self._state_pool_delay_count[dst] = self._decode_delay_count[src]
|
||||
@@ -420,10 +500,9 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
self._state_pool_generation_done[pool_row] = False
|
||||
|
||||
self._ensure_decode_state_capacity(len(req_ids), device)
|
||||
pool_rows = torch.tensor(
|
||||
pool_rows = self._state_indices(
|
||||
[self._state_pool_indices[req_id] for req_id in req_ids],
|
||||
dtype=torch.long,
|
||||
device=device,
|
||||
device,
|
||||
)
|
||||
self._decode_has_codes[: len(req_ids)] = self._state_pool_has_codes[pool_rows]
|
||||
self._decode_last_codes[: len(req_ids)] = self._state_pool_last_codes[pool_rows]
|
||||
@@ -450,8 +529,9 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
**kwargs: Any,
|
||||
) -> torch.Tensor:
|
||||
# A runner invocation may call forward repeatedly while warming and
|
||||
# capturing a graph. Keep its metadata authoritative for that scope.
|
||||
metadata_from_runner = self._decode_step_metadata_from_runner
|
||||
self._decode_step_metadata_from_runner = False
|
||||
|
||||
info_dicts = kwargs.get("model_intermediate_buffer")
|
||||
if info_dicts is None:
|
||||
@@ -501,7 +581,7 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
and inputs_embeds is None
|
||||
and not self._is_single_token_decode_step(int(hidden_states.shape[0]))
|
||||
)
|
||||
if is_prefill and info_dicts:
|
||||
if is_prefill and info_dicts and inputs_embeds is None:
|
||||
# Voice clone: replace marked prompt positions with ref audio embeddings.
|
||||
hidden_states = self._apply_ref_audio_substitution(hidden_states, input_ids, positions, info_dicts)
|
||||
|
||||
@@ -515,6 +595,11 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
return self._run_qwen3_layers_eager(positions, hidden_states)
|
||||
|
||||
def _run_qwen3_layers_eager(self, positions: torch.Tensor, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
if self._use_external_decode_cudagraph:
|
||||
# Enter Qwen3Model's compilation boundary before the runner
|
||||
# captures the decode graph. Audio feedback stays outside it.
|
||||
return self.model(input_ids=None, positions=positions, inputs_embeds=hidden_states)
|
||||
|
||||
residual: torch.Tensor | None = None
|
||||
for layer in self.model.layers:
|
||||
hidden_states, residual = layer(positions, hidden_states, residual)
|
||||
@@ -619,6 +704,25 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
|
||||
def compute_logits(self, hidden_states: torch.Tensor, sampling_metadata: Any = None) -> torch.Tensor:
|
||||
self._last_logits_hidden = hidden_states
|
||||
ids = getattr(self, "_last_step_input_ids", None)
|
||||
rows = int(hidden_states.shape[0])
|
||||
if (
|
||||
self._audio_continuation_id is not None
|
||||
and sampling_metadata is not None
|
||||
and getattr(sampling_metadata, "no_penalties", False)
|
||||
and (
|
||||
(isinstance(ids, torch.Tensor) and ids.numel() == rows and self._fast_audio_direct_rows == rows)
|
||||
or getattr(self, "_step_audio_tail_rows", 0) == rows
|
||||
or getattr(self, "_step_audio_mode_rows", 0) == rows
|
||||
)
|
||||
and getattr(sampling_metadata, "max_num_logprobs", None) is None
|
||||
and getattr(sampling_metadata, "allowed_token_ids_mask", None) is None
|
||||
and not getattr(sampling_metadata, "bad_words_token_ids", None)
|
||||
):
|
||||
# sample() already selects continuation/EOS directly in this
|
||||
# state, using the audio head rather than the text head. Keep
|
||||
# the normal vocabulary shape for runner-side processors.
|
||||
return hidden_states.new_empty((rows, self._text_vocab_size), dtype=torch.float32)
|
||||
return self.logits_processor(self.lm_head, hidden_states)
|
||||
|
||||
def embed_input_ids(self, input_ids: torch.Tensor, **_: Any) -> torch.Tensor:
|
||||
@@ -656,7 +760,9 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
``audio_placeholder_positions`` ([T] absolute prompt positions). Legacy
|
||||
callers that still submit -100 token IDs remain supported.
|
||||
"""
|
||||
if not info_dicts:
|
||||
if not info_dicts or not any(
|
||||
isinstance(info, dict) and info.get("audio_input_ids") is not None for info in info_dicts
|
||||
):
|
||||
return hidden_states
|
||||
|
||||
flat_ids = input_ids.reshape(-1)
|
||||
@@ -788,10 +894,6 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
if self._decode_active_audio_count == 0 and not external_decode_graph:
|
||||
return hidden_states
|
||||
|
||||
req_rows, token_positions = self._decode_request_token_positions(num_tokens, hidden_states.device)
|
||||
if int(req_rows.numel()) == 0:
|
||||
return hidden_states
|
||||
|
||||
num_reqs = self._step_request_count(num_tokens)
|
||||
self._ensure_decode_state_capacity(num_reqs, hidden_states.device)
|
||||
|
||||
@@ -800,6 +902,9 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
has_codes = self._decode_has_codes[:num_tokens].unsqueeze(-1)
|
||||
current_hidden = hidden_states
|
||||
else:
|
||||
req_rows, token_positions = self._decode_request_token_positions(num_tokens, hidden_states.device)
|
||||
if int(req_rows.numel()) == 0:
|
||||
return hidden_states
|
||||
codes_slice = self._decode_last_codes.index_select(0, req_rows)
|
||||
has_codes = self._decode_has_codes.index_select(0, req_rows).unsqueeze(-1)
|
||||
current_hidden = hidden_states.index_select(0, token_positions)
|
||||
@@ -932,6 +1037,44 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
return None
|
||||
return tail_ids == int(audio_id)
|
||||
|
||||
def _step_tail_guard(self, num_rows: int, *, prompt_mode: bool, decode_only: bool) -> None:
|
||||
from .step_kernels import step_tail_guard
|
||||
|
||||
error = getattr(self, "_decode_tail_guard_error", None)
|
||||
device = self._decode_has_codes.device
|
||||
if error is None or error.device != device:
|
||||
error = self._decode_tail_guard_error = torch.zeros(1, dtype=torch.int32, device=device)
|
||||
step_tail_guard(
|
||||
self._last_step_input_ids.reshape(-1),
|
||||
None if decode_only else self._last_step_query_start_loc,
|
||||
self._decode_has_codes[:num_rows],
|
||||
self._decode_generation_done[:num_rows],
|
||||
self._decode_delay_count[:num_rows],
|
||||
self._decode_eoc_countdown[:num_rows],
|
||||
error,
|
||||
audio_id=int(self._audio_continuation_id),
|
||||
eos_id=int(self._eos_token_id),
|
||||
prompt_mode=prompt_mode,
|
||||
)
|
||||
torch._assert_async(error[0] == 0, "CPU/GPU Higgs graph audio tail mismatch")
|
||||
|
||||
def _restore_terminal_audio_rows(self, num_rows):
|
||||
# Async scheduling can execute one final in-flight EOS row after
|
||||
# request retirement has released its state slot. EOS is terminal:
|
||||
# never resurrect that row as a new audio stream with a forced input.
|
||||
eos_id = self._eos_token_id
|
||||
if eos_id is None:
|
||||
return
|
||||
ids = self._last_step_input_ids.reshape(-1)
|
||||
qsl = self._last_step_query_start_loc
|
||||
if isinstance(qsl, torch.Tensor) and qsl.numel() == num_rows + 1:
|
||||
tail = ids.index_select(0, (qsl[1:].long() - 1).clamp_min(0))
|
||||
elif ids.numel() == num_rows:
|
||||
tail = ids
|
||||
else:
|
||||
raise RuntimeError("Cannot validate terminal audio rows")
|
||||
self._decode_generation_done[:num_rows].logical_or_(tail == eos_id)
|
||||
|
||||
def _fast_audio_sampler_gpu_fallback_reason(
|
||||
self,
|
||||
*,
|
||||
@@ -997,6 +1140,7 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
and accumulate per-request state.
|
||||
"""
|
||||
self._resolve_token_ids()
|
||||
self._async_audio_gpu_staging = None
|
||||
|
||||
audio_id = self._audio_continuation_id
|
||||
|
||||
@@ -1020,6 +1164,56 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
self._ensure_decode_state_capacity(num_rows, hidden.device)
|
||||
ids = getattr(self, "_last_step_input_ids", None)
|
||||
decode_only = isinstance(ids, torch.Tensor) and int(ids.numel()) == num_rows
|
||||
prompt_mode_rows = getattr(self, "_step_audio_mode_rows", 0) == num_rows
|
||||
graph_forced_audio = getattr(self, "_step_audio_tail_rows", 0) == num_rows or prompt_mode_rows
|
||||
use_sample_graph = bool(
|
||||
getattr(self, "_sample_graph_enabled", False)
|
||||
and not getattr(self, "_in_audio_sample_graph", False)
|
||||
and hidden.is_cuda
|
||||
and (graph_forced_audio or (decode_only and self._fast_audio_direct_rows == num_rows))
|
||||
and getattr(sampling_metadata, "no_penalties", False)
|
||||
and self._fast_audio_sampler_gpu_fallback_reason(
|
||||
logits=logits, sampling_metadata=sampling_metadata, num_rows=num_rows
|
||||
)
|
||||
is None
|
||||
)
|
||||
qsl = self._last_step_query_start_loc
|
||||
fused_guard = (
|
||||
use_sample_graph
|
||||
and graph_forced_audio
|
||||
and self._eos_token_id is not None
|
||||
and (decode_only or (isinstance(qsl, torch.Tensor) and int(qsl.numel()) == num_rows + 1))
|
||||
)
|
||||
if fused_guard:
|
||||
# One launch restores terminal EOS rows, resets new prefill rows
|
||||
# and validates the audio tail (~20 small eager kernels before).
|
||||
self._step_tail_guard(num_rows, prompt_mode=prompt_mode_rows, decode_only=decode_only)
|
||||
elif prompt_mode_rows:
|
||||
self._restore_terminal_audio_rows(num_rows)
|
||||
if use_sample_graph:
|
||||
from .full_sample_graph import run_dense_sample
|
||||
|
||||
if not fused_guard and graph_forced_audio:
|
||||
actual_audio = self._audio_seed_mask_from_step_input(num_rows, hidden.device)
|
||||
if getattr(self, "_step_audio_mode_rows", 0) == num_rows:
|
||||
# Reset new prefill rows before accepting old active state.
|
||||
if not decode_only:
|
||||
self._reset_decode_state_rows(
|
||||
self._prefill_row_mask(num_rows, hidden.device), num_rows, hidden.device
|
||||
)
|
||||
actual_audio = (
|
||||
actual_audio | self._decode_has_codes[:num_rows] | self._decode_generation_done[:num_rows]
|
||||
)
|
||||
torch._assert_async(actual_audio.all(), "CPU/GPU Higgs graph audio tail mismatch")
|
||||
if not decode_only:
|
||||
if not fused_guard:
|
||||
pfmask = self._prefill_row_mask(num_rows, hidden.device)
|
||||
self._reset_decode_state_rows(pfmask, num_rows, hidden.device)
|
||||
self._fast_audio_direct_rows = num_rows
|
||||
sampled = run_dense_sample(self, hidden, logits, sampling_metadata, force_audio_inputs=graph_forced_audio)
|
||||
if sampled is not None:
|
||||
return sampled
|
||||
self._last_logits_hidden = None
|
||||
if not decode_only:
|
||||
self._fast_audio_direct_rows = 0
|
||||
pfmask = self._prefill_row_mask(num_rows, hidden.device)
|
||||
@@ -1042,6 +1236,17 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
|
||||
if fast_fallback_reason is None:
|
||||
direct_audio_batch = decode_only and self._fast_audio_direct_rows == num_rows
|
||||
mixed_direct = (
|
||||
getattr(self, "_step_audio_tail_rows", 0) == num_rows
|
||||
or getattr(self, "_step_audio_mode_rows", 0) == num_rows
|
||||
) and getattr(sampling_metadata, "no_penalties", False)
|
||||
if mixed_direct:
|
||||
# Guard the CPU classification against runner/device drift.
|
||||
torch._assert_async(
|
||||
(audio_mask if getattr(self, "_step_audio_mode_rows", 0) == num_rows else prev_audio_mask).all(),
|
||||
"CPU/GPU Higgs audio tail mismatch",
|
||||
)
|
||||
direct_audio_batch = True
|
||||
|
||||
if direct_audio_batch:
|
||||
self._fast_audio_direct_rows = num_rows
|
||||
@@ -1194,6 +1399,62 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
self._boc_frame_cache[key] = buf
|
||||
return buf[:num_rows]
|
||||
|
||||
def post_sample_multimodal_outputs(self, *, req_ids, invalid_req_indices, multimodal_outputs):
|
||||
"""Export current-step codes by current batch row, with owned CPU storage.
|
||||
|
||||
Forward-time payloads refer to previous request-buffer rows, which may
|
||||
be reordered or overwritten when the sampler reuses its host staging.
|
||||
Export only after sampling, before the runner takes its output snapshot.
|
||||
"""
|
||||
if getattr(self, "use_async_omni_output", False):
|
||||
staging = self._async_audio_gpu_staging
|
||||
self._async_audio_gpu_staging = None
|
||||
self._fast_audio_direct_rows = 0
|
||||
if staging is None:
|
||||
return None
|
||||
if staging.shape[0] != len(req_ids):
|
||||
raise RuntimeError("Higgs async snapshot row mismatch")
|
||||
return {"_higgs_audio_snapshot": staging, "_higgs_invalid_rows": tuple(invalid_req_indices)}
|
||||
staging = self._last_audio_host_staging
|
||||
if staging is None:
|
||||
self._fast_audio_direct_rows = 0
|
||||
return None
|
||||
if staging.shape[0] != len(req_ids):
|
||||
raise RuntimeError("Higgs audio staging rows do not match the sampled request batch")
|
||||
event = self._last_audio_staging_event
|
||||
if event is not None:
|
||||
event.synchronize()
|
||||
self._last_audio_staging_event = None
|
||||
codes = staging[:, : self.num_codebooks].clone()
|
||||
valid = staging[:, self.num_codebooks].tolist()
|
||||
done = staging[:, self.num_codebooks + 1].tolist()
|
||||
invalid = set(invalid_req_indices)
|
||||
self._fast_audio_direct_rows = len(req_ids) if not invalid and all(v or d for v, d in zip(valid, done)) else 0
|
||||
# Keep the codec-row rank on terminal/invalid steps: a 1-D empty
|
||||
# value would replace previously accumulated full-response rows.
|
||||
empty = codes.new_empty((0, self.num_codebooks))
|
||||
rows = [codes[i : i + 1] if valid[i] and i not in invalid else empty for i in range(len(req_ids))]
|
||||
return {"codes": {"audio": rows}}
|
||||
|
||||
def finalize_multimodal_outputs_from_cpu_snapshot(self, payload):
|
||||
"""Pure per-step conversion; never reads mutable decode/request state."""
|
||||
if not payload or "_higgs_audio_snapshot" not in payload:
|
||||
return payload
|
||||
staging = payload["_higgs_audio_snapshot"]
|
||||
if staging.device.type != "cpu":
|
||||
raise RuntimeError("Higgs snapshot finalized before D2H")
|
||||
codes = staging[:, : self.num_codebooks].clone()
|
||||
valid = staging[:, self.num_codebooks].tolist()
|
||||
invalid = set(payload["_higgs_invalid_rows"])
|
||||
# Keep the codec-row rank on terminal/invalid steps: a 1-D empty
|
||||
# value would replace previously accumulated full-response rows.
|
||||
empty = codes.new_empty((0, self.num_codebooks))
|
||||
return {
|
||||
"codes": {
|
||||
"audio": [codes[i : i + 1] if valid[i] and i not in invalid else empty for i in range(staging.shape[0])]
|
||||
}
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ postprocess
|
||||
def postprocess(
|
||||
self,
|
||||
@@ -1450,6 +1711,20 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
staging[:, num_codebooks].copy_(valid_full.to(torch.int32))
|
||||
staging[:, num_codebooks + 1].copy_(done_full.to(torch.int32))
|
||||
|
||||
self._finish_audio_staging(staging, num_rows, device, num_rows if all_rows else int(rows.numel()))
|
||||
|
||||
def _finish_audio_staging(self, staging, num_rows, device, active_rows):
|
||||
if getattr(self, "_in_audio_sample_graph", False):
|
||||
return
|
||||
if getattr(self, "use_async_omni_output", False):
|
||||
# Own this step's GPU values before the next graph replay. The
|
||||
# runner copies the snapshot on its output stream and waits there.
|
||||
self._async_audio_gpu_staging = staging[:num_rows].clone()
|
||||
self._last_audio_host_staging = None
|
||||
self._last_audio_staging_event = None
|
||||
self._last_audio_codes = None
|
||||
self._decode_active_audio_count = active_rows
|
||||
return
|
||||
host = self._get_audio_host_staging_buffer(num_rows)
|
||||
host.copy_(staging, non_blocking=True)
|
||||
if device.type == "cuda":
|
||||
@@ -1459,7 +1734,7 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
else:
|
||||
self._last_audio_staging_event = None
|
||||
|
||||
self._decode_active_audio_count = num_rows if all_rows else int(rows.numel())
|
||||
self._decode_active_audio_count = active_rows
|
||||
# Host staging is the authoritative postprocess path. Keeping another
|
||||
# full GPU codes buffer alive adds fill/copy work on the hot path.
|
||||
self._last_audio_codes = None
|
||||
@@ -1514,7 +1789,24 @@ class HiggsAudioV3TalkerForConditionalGeneration(nn.Module):
|
||||
fallback = x.argmax(dim=-1)
|
||||
safe_x = torch.where(all_masked.unsqueeze(-1), torch.zeros_like(x), x)
|
||||
probs = safe_x.softmax(dim=-1)
|
||||
if sampling_metadata.generators:
|
||||
if getattr(self, "_audio_graph_noise", None) is not None:
|
||||
torch._assert_async(torch.isfinite(probs).all(), "Audio sampling probabilities must be finite")
|
||||
sampled = probs.div(self._audio_graph_noise).argmax(dim=-1)
|
||||
elif probs.is_cuda:
|
||||
# Match torch.multinomial's single-sample exponential race, but
|
||||
# validate the whole batch asynchronously. The original per-request
|
||||
# calls each copy validation scalars to the host before drawing.
|
||||
torch._assert_async(torch.isfinite(probs).all(), "Audio sampling probabilities must be finite")
|
||||
q = torch.empty_like(probs)
|
||||
if sampling_metadata.generators:
|
||||
from vllm_omni.utils.seeded_exponential import fill_exponential_rows
|
||||
|
||||
rows = int(probs.shape[0]) // num_codebooks
|
||||
fill_exponential_rows(q, [sampling_metadata.generators.get(i) for i in range(rows)])
|
||||
else:
|
||||
q.exponential_()
|
||||
sampled = probs.div(q).argmax(dim=-1)
|
||||
elif sampling_metadata.generators:
|
||||
sampled = torch.cat(
|
||||
[
|
||||
torch.multinomial(
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Higgs delay-pattern sampling and owned audio output on Model Runner V2."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from vllm.logger import init_logger
|
||||
from vllm.v1.sample.logits_processor import LogitsProcessors
|
||||
from vllm.v1.sample.metadata import SamplingMetadata
|
||||
from vllm.v1.worker.gpu.input_batch import get_num_sampled_and_rejected
|
||||
from vllm.v1.worker.gpu.sample.output import SamplerOutput
|
||||
|
||||
from vllm_omni.worker_v2.model_states.omni_model_state import OmniModelState
|
||||
from vllm_omni.worker_v2.omni_sampler import OmniSampler, OmniSamplingOutput
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class HiggsModelState(OmniModelState):
|
||||
def __init__(self, vllm_config, model, encoder_cache, device):
|
||||
super().__init__(vllm_config, model, encoder_cache, device)
|
||||
model._use_external_decode_cudagraph = True
|
||||
# Fix decode-state addresses before any sampling graph can capture them.
|
||||
model._ensure_decode_state_capacity(max(self.max_num_tokens, 128), device)
|
||||
logger.info("Higgs MRV2 custom sampler and owned audio snapshots enabled")
|
||||
if getattr(model.config, "audio_mrv2_static_inputs", False):
|
||||
self._static_inputs_embeds = torch.zeros(
|
||||
(self.max_num_tokens, model.model.embed_tokens.weight.shape[1]),
|
||||
dtype=self.dtype,
|
||||
device=device,
|
||||
)
|
||||
self.direct_payload = bool(getattr(model.config, "audio_mrv2_direct_payload", False))
|
||||
self.sampling_slots = None
|
||||
self.request_slots = {}
|
||||
if getattr(model.config, "audio_mrv2_slot_parameters", False):
|
||||
capacity = self.scheduler_config.max_num_seqs
|
||||
self.sampling_slots = {
|
||||
"temperature": torch.zeros(capacity, device=device),
|
||||
"top_p": torch.ones(capacity, device=device),
|
||||
"top_k": torch.full((capacity,), -1, dtype=torch.long, device=device),
|
||||
"zeros": torch.zeros(capacity, device=device),
|
||||
"ones": torch.ones(capacity, device=device),
|
||||
}
|
||||
self.requests = {}
|
||||
self.generators = {}
|
||||
self._ensure_slot_capacity(self.scheduler_config.max_num_seqs)
|
||||
self.metadata_key = None
|
||||
self.metadata = None
|
||||
logger.info(
|
||||
"Higgs MRV2 options: static=%s slot_parameters=%s direct_payload=%s",
|
||||
self._static_inputs_embeds is not None,
|
||||
self.sampling_slots is not None,
|
||||
self.direct_payload,
|
||||
)
|
||||
if not model.use_async_omni_output:
|
||||
raise ValueError("Higgs MRV2 requires audio_async_payload; use higgs_multimodal_qwen3_mrv2_h200.yaml")
|
||||
|
||||
def _ensure_slot_capacity(self, slots):
|
||||
"""Per-slot prompt facts for vectorized step classification."""
|
||||
current = getattr(self, "prompt_len_np", None)
|
||||
if current is not None and len(current) >= slots:
|
||||
return
|
||||
size = max(slots, 2 * (0 if current is None else len(current)))
|
||||
old_len, old_tail = current, getattr(self, "prompt_audio_tail_np", None)
|
||||
self.prompt_len_np = np.zeros(size, dtype=np.int64)
|
||||
self.prompt_audio_tail_np = np.zeros(size, dtype=bool)
|
||||
if current is not None:
|
||||
self.prompt_len_np[: len(old_len)] = old_len
|
||||
self.prompt_audio_tail_np[: len(old_tail)] = old_tail
|
||||
|
||||
def add_request(self, req_index, new_req_data):
|
||||
params = new_req_data.sampling_params
|
||||
is_warmup = str(new_req_data.req_id).startswith("_warmup_")
|
||||
if not is_warmup and (
|
||||
params.frequency_penalty != 0
|
||||
or params.presence_penalty != 0
|
||||
or params.repetition_penalty != 1
|
||||
or params.logprobs is not None
|
||||
or params.prompt_logprobs is not None
|
||||
or params.min_tokens != 0
|
||||
or params.allowed_token_ids
|
||||
or params.bad_words
|
||||
or params.logit_bias
|
||||
or params.min_p != 0
|
||||
):
|
||||
raise ValueError("Higgs MRV2 currently supports temperature/top-k/top-p sampling without text penalties")
|
||||
super().add_request(req_index, new_req_data)
|
||||
self.requests[new_req_data.req_id] = new_req_data
|
||||
self.request_slots[new_req_data.req_id] = req_index
|
||||
if self.sampling_slots is not None:
|
||||
for name in ("temperature", "top_p", "top_k"):
|
||||
self.sampling_slots[name][req_index : req_index + 1].fill_(getattr(params, name))
|
||||
self.metadata_key = None
|
||||
prompt = new_req_data.prompt_token_ids or []
|
||||
self._ensure_slot_capacity(req_index + 1)
|
||||
self.prompt_len_np[req_index] = len(prompt)
|
||||
self.model._resolve_token_ids()
|
||||
self.prompt_audio_tail_np[req_index] = bool(prompt) and prompt[-1] == self.model._audio_continuation_id
|
||||
if params.seed is not None:
|
||||
self.generators[new_req_data.req_id] = torch.Generator(device=self.device).manual_seed(params.seed)
|
||||
|
||||
def remove_request(self, req_index_or_id):
|
||||
index = self._resolve_req_index(req_index_or_id)
|
||||
if index is not None:
|
||||
req_id = self.intermediate_buffer.buffers[index].get("req_id")
|
||||
if req_id is not None:
|
||||
self.model.on_requests_finished({req_id})
|
||||
self.requests.pop(req_id, None)
|
||||
self.request_slots.pop(req_id, None)
|
||||
self.generators.pop(req_id, None)
|
||||
self.metadata_key = None
|
||||
super().remove_request(req_index_or_id)
|
||||
|
||||
def run_preprocess(self, input_batch, model_inputs, req_states=None, mtp_batch_descriptor_dispatcher=None):
|
||||
model = self.model
|
||||
model._resolve_token_ids()
|
||||
# Every row must have an audio-continuation prompt whose prefill ends
|
||||
# within this step; one vectorized check replaces a per-row loop.
|
||||
n = input_batch.num_reqs
|
||||
slots = input_batch.idx_mapping_np[:n]
|
||||
end = req_states.num_computed_prefill_tokens[slots] + input_batch.num_scheduled_tokens[:n]
|
||||
audio_mode = bool(np.all(self.prompt_audio_tail_np[slots] & (end >= self.prompt_len_np[slots])))
|
||||
model.update_decode_step_metadata(
|
||||
input_ids=model_inputs["input_ids"][: input_batch.num_tokens],
|
||||
positions=input_batch.positions,
|
||||
omni_query_start_loc=input_batch.query_start_loc[: input_batch.num_reqs + 1],
|
||||
req_ids=input_batch.req_ids,
|
||||
audio_prompt_mode_rows=input_batch.num_reqs if audio_mode else 0,
|
||||
)
|
||||
|
||||
if self._static_inputs_embeds is not None:
|
||||
self._prepare_static_embeddings(input_batch, model_inputs)
|
||||
|
||||
def _prepare_static_embeddings(self, input_batch, model_inputs):
|
||||
# Feedback and reference substitution are request-dependent and belong
|
||||
# outside the captured backbone. Use only actual tokens: graph padding
|
||||
# must not change the one-token-per-request decode classification.
|
||||
n = input_batch.num_tokens
|
||||
ids = model_inputs["input_ids"][:n]
|
||||
if self._fused_decode_embeddings(input_batch, ids):
|
||||
return
|
||||
safe_ids = torch.where(ids < 0, torch.zeros_like(ids), ids)
|
||||
embeds = self.model.model.embed_tokens(safe_ids)
|
||||
info = model_inputs.get("model_intermediate_buffer")
|
||||
if input_batch.has_prefill and info:
|
||||
embeds = self.model._apply_ref_audio_substitution(
|
||||
embeds,
|
||||
ids,
|
||||
input_batch.positions[:n],
|
||||
info,
|
||||
)
|
||||
embeds = self._apply_audio_feedback(embeds, ids, input_batch)
|
||||
self._static_inputs_embeds[:n].copy_(embeds)
|
||||
padded = input_batch.num_tokens_after_padding
|
||||
self._static_inputs_embeds[n:padded].zero_()
|
||||
|
||||
def _fused_decode_embeddings(self, input_batch, ids) -> bool:
|
||||
"""Pure decode steps: token/feedback embeddings and padding in one launch."""
|
||||
model = self.model
|
||||
if (
|
||||
input_batch.has_prefill
|
||||
or input_batch.num_tokens != input_batch.num_reqs
|
||||
or not ids.is_cuda
|
||||
or not getattr(model, "_use_external_decode_cudagraph", False)
|
||||
):
|
||||
return False
|
||||
text = getattr(model.model.embed_tokens, "weight", None)
|
||||
audio = getattr(model.multimodal_embedding, "weight", None)
|
||||
if text is None or audio is None or text.dtype != self._static_inputs_embeds.dtype:
|
||||
return False
|
||||
from .step_kernels import decode_embeddings
|
||||
|
||||
n = int(ids.numel())
|
||||
model._ensure_decode_state_capacity(n, ids.device)
|
||||
decode_embeddings(
|
||||
self._static_inputs_embeds,
|
||||
ids,
|
||||
text,
|
||||
audio,
|
||||
model._decode_last_codes,
|
||||
model._decode_has_codes,
|
||||
int(input_batch.num_tokens_after_padding),
|
||||
int(model.multimodal_embedding.vocab_size),
|
||||
)
|
||||
return True
|
||||
|
||||
def _apply_audio_feedback(self, embeds, ids, input_batch):
|
||||
if input_batch.num_tokens == input_batch.num_reqs:
|
||||
return self.model._apply_audio_feedback(embeds, ids)
|
||||
# Every scheduled request has a nonempty span. Gather its first token
|
||||
# for all rows, then mask on-device. Boolean indexing would first ask
|
||||
# the GPU how many rows matched, synchronizing every mixed prefill.
|
||||
n = input_batch.num_reqs
|
||||
qsl = input_batch.query_start_loc[: n + 1]
|
||||
starts = qsl[:-1].long()
|
||||
model = self.model
|
||||
model._ensure_decode_state_capacity(n, embeds.device)
|
||||
audio = model.multimodal_embedding(model._decode_last_codes[:n]).to(embeds.dtype)
|
||||
use_audio = model._decode_has_codes[:n] & ((qsl[1:] - qsl[:-1]) == 1)
|
||||
current = embeds.index_select(0, starts)
|
||||
result = embeds.clone()
|
||||
result.index_copy_(0, starts, torch.where(use_audio[:, None], audio, current))
|
||||
return result
|
||||
|
||||
def run_postprocess(self, hidden_states, input_batch):
|
||||
self.model.finish_decode_step_forward()
|
||||
|
||||
def postprocess_model_output(self, model_output, input_batch, req_states):
|
||||
return model_output, None
|
||||
|
||||
def sampling_metadata(self, req_ids, slots=None):
|
||||
key = tuple(req_ids)
|
||||
if self.metadata_key != key:
|
||||
params = [self.requests[r].sampling_params for r in req_ids]
|
||||
n = len(params)
|
||||
if self.sampling_slots is not None:
|
||||
if slots is None:
|
||||
slots = torch.tensor([self.request_slots[r] for r in req_ids], device=self.device)
|
||||
temperature, top_p, top_k = (
|
||||
self.sampling_slots[name].index_select(0, slots) for name in ("temperature", "top_p", "top_k")
|
||||
)
|
||||
zeros = self.sampling_slots["zeros"][:n]
|
||||
ones = self.sampling_slots["ones"][:n]
|
||||
else:
|
||||
temperature = torch.tensor([p.temperature for p in params], device=self.device)
|
||||
top_p = torch.tensor([p.top_p for p in params], device=self.device)
|
||||
top_k = torch.tensor([p.top_k for p in params], device=self.device)
|
||||
zeros = torch.zeros(n, device=self.device)
|
||||
ones = torch.ones(n, device=self.device)
|
||||
self.metadata = SamplingMetadata(
|
||||
temperature=temperature,
|
||||
all_greedy=all(p.temperature == 0 for p in params),
|
||||
all_random=all(p.temperature != 0 for p in params),
|
||||
top_p=top_p,
|
||||
top_k=top_k,
|
||||
generators={i: self.generators[r] for i, r in enumerate(req_ids) if r in self.generators},
|
||||
max_num_logprobs=None,
|
||||
no_penalties=True,
|
||||
prompt_token_ids=None,
|
||||
frequency_penalties=zeros,
|
||||
presence_penalties=zeros,
|
||||
repetition_penalties=ones,
|
||||
output_token_ids=[[] for _ in params],
|
||||
allowed_token_ids_mask=None,
|
||||
bad_words_token_ids={},
|
||||
logitsprocs=LogitsProcessors(),
|
||||
)
|
||||
self.metadata_key = key
|
||||
return self.metadata
|
||||
|
||||
def custom_sampler(self, sampler):
|
||||
return HiggsSampler(sampler, self), None
|
||||
|
||||
def finalize_audio_snapshot(self, payload: dict[str, Any], num_sampled: list[int]):
|
||||
if payload and "_higgs_audio_snapshot" in payload and getattr(self, "direct_payload", False):
|
||||
from vllm_omni.worker_v2.output_snapshot import RequestOutputSnapshot
|
||||
|
||||
snapshot = payload["_higgs_audio_snapshot"]
|
||||
if snapshot.device.type != "cpu":
|
||||
raise RuntimeError("Higgs snapshot finalized before D2H")
|
||||
codes = snapshot[:, : self.model.num_codebooks].clone()
|
||||
valid = snapshot[:, self.model.num_codebooks].tolist()
|
||||
invalid = set(payload.get("_higgs_invalid_rows", ()))
|
||||
# One split creates every row view; per-row slicing cost one
|
||||
# dispatcher call per request on the output-materialization path.
|
||||
rows = torch.split(codes, 1)
|
||||
return RequestOutputSnapshot(
|
||||
[
|
||||
{"codes.audio": rows[i]} if valid[i] and n and i not in invalid else None
|
||||
for i, n in enumerate(num_sampled)
|
||||
]
|
||||
)
|
||||
if payload and "_higgs_audio_snapshot" in payload:
|
||||
payload = dict(payload)
|
||||
payload["_higgs_invalid_rows"] = tuple(i for i, n in enumerate(num_sampled) if n == 0)
|
||||
return self.model.finalize_multimodal_outputs_from_cpu_snapshot(payload)
|
||||
|
||||
|
||||
class HiggsSampler(OmniSampler):
|
||||
"""Higgs audio sampling registered through the upstream sampler factory."""
|
||||
|
||||
def __init__(self, base_sampler, state: HiggsModelState):
|
||||
super().__init__(base_sampler)
|
||||
self.state = state
|
||||
|
||||
def sample_step(self, hidden_states, input_batch, req_states, grammar_output, standard_sample):
|
||||
# Upstream warmup exercises its text sampler, including grammar and
|
||||
# penalties, with synthetic non-audio prompts. Keep that path intact.
|
||||
if all(str(r).startswith("_warmup_") for r in input_batch.req_ids):
|
||||
return super().sample_step(hidden_states, input_batch, req_states, grammar_output, standard_sample)
|
||||
if grammar_output is not None or input_batch.num_draft_tokens:
|
||||
raise ValueError("Higgs MRV2 does not support grammar or speculative sampling")
|
||||
metadata = self.state.sampling_metadata(input_batch.req_ids, input_batch.idx_mapping[: input_batch.num_reqs])
|
||||
hidden = hidden_states[input_batch.logits_indices]
|
||||
logits = self.state.model.compute_logits(hidden, metadata)
|
||||
sampled = self.state.model.sample(logits, metadata)
|
||||
count, rejected = get_num_sampled_and_rejected(
|
||||
torch.ones(input_batch.num_reqs, dtype=torch.int32, device=hidden.device),
|
||||
input_batch.seq_lens,
|
||||
input_batch.cu_num_logits,
|
||||
input_batch.idx_mapping,
|
||||
req_states.prefill_len.gpu,
|
||||
)
|
||||
output = SamplerOutput(sampled.sampled_token_ids, sampled.logprobs_tensors, None, count, rejected)
|
||||
payload = self.state.model.post_sample_multimodal_outputs(
|
||||
req_ids=input_batch.req_ids,
|
||||
invalid_req_indices=[],
|
||||
multimodal_outputs=None,
|
||||
)
|
||||
return OmniSamplingOutput(
|
||||
output,
|
||||
count,
|
||||
rejected,
|
||||
payload,
|
||||
include_hidden_states=False,
|
||||
owns_multimodal_outputs=True,
|
||||
finalize_multimodal=self.state.finalize_audio_snapshot,
|
||||
)
|
||||
@@ -0,0 +1,68 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Fused movement of model-owned delay state when a request batch changes."""
|
||||
|
||||
import torch
|
||||
from vllm.triton_utils import tl
|
||||
from vllm.triton_utils import triton as tr
|
||||
|
||||
|
||||
@tr.jit(do_not_specialize=["count"])
|
||||
def _move(
|
||||
indices,
|
||||
source_codes,
|
||||
source_has,
|
||||
source_delay,
|
||||
source_eoc,
|
||||
source_done,
|
||||
dest_codes,
|
||||
dest_has,
|
||||
dest_delay,
|
||||
dest_eoc,
|
||||
dest_done,
|
||||
count,
|
||||
books: tl.constexpr,
|
||||
gather: tl.constexpr,
|
||||
block: tl.constexpr,
|
||||
):
|
||||
row = tl.program_id(0)
|
||||
col = tl.arange(0, block)
|
||||
if gather:
|
||||
encoded = tl.load(indices + row)
|
||||
fresh = encoded < 0
|
||||
src = tl.where(fresh, -encoded - 1, encoded)
|
||||
dst = row
|
||||
else:
|
||||
src = tl.load(indices + row)
|
||||
dst = tl.load(indices + count + row)
|
||||
fresh = False
|
||||
codes = tl.load(source_codes + src * books + col, col < books, 0)
|
||||
has = tl.load(source_has + src)
|
||||
delay = tl.load(source_delay + src)
|
||||
eoc = tl.load(source_eoc + src)
|
||||
done = tl.load(source_done + src)
|
||||
tl.store(dest_codes + dst * books + col, tl.where(fresh, 0, codes), col < books)
|
||||
tl.store(dest_has + dst, tl.where(fresh, 0, has))
|
||||
tl.store(dest_delay + dst, tl.where(fresh, 0, delay))
|
||||
tl.store(dest_eoc + dst, tl.where(fresh, -1, eoc))
|
||||
tl.store(dest_done + dst, tl.where(fresh, 0, done))
|
||||
|
||||
|
||||
def sync_state(model, previous_rows, pool_rows, current_rows):
|
||||
# One owned pinned upload, ordered on the decode stream. Separate scatter
|
||||
# and gather kernels provide a global ordering point for moved pool rows.
|
||||
device = model._decode_has_codes.device
|
||||
packed = torch.tensor(previous_rows + pool_rows + current_rows, dtype=torch.int64, pin_memory=True).to(
|
||||
device, non_blocking=True
|
||||
)
|
||||
names = ("last_codes", "has_codes", "delay_count", "eoc_countdown", "generation_done")
|
||||
decode = [getattr(model, "_decode_" + name) for name in names]
|
||||
pool = [getattr(model, "_state_pool_" + name) for name in names]
|
||||
n = len(previous_rows)
|
||||
block = tr.next_power_of_2(model.num_codebooks)
|
||||
if n:
|
||||
_move[(n,)](packed, *decode, *pool, n, model.num_codebooks, False, block)
|
||||
if current_rows:
|
||||
_move[(len(current_rows),)](
|
||||
packed[2 * n :], *pool, *decode, len(current_rows), model.num_codebooks, True, block
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""higgs-audio v3 pipeline: Talker (text -> 8-codebook codec) -> Code2Wav (codec -> 24 kHz PCM).
|
||||
|
||||
Two delivery modes are wired here:
|
||||
@@ -19,6 +19,9 @@ from vllm_omni.config.stage_config import (
|
||||
StageExecutionType,
|
||||
StagePipelineConfig,
|
||||
)
|
||||
from vllm_omni.outputs.output_modality import TensorAccumulationStrategy, register_key_accumulation_strategy
|
||||
|
||||
register_key_accumulation_strategy("codes.audio", TensorAccumulationStrategy.CONCAT_DIM0)
|
||||
|
||||
_PROC = "vllm_omni.model_executor.stage_input_processors.higgs_audio_v3"
|
||||
|
||||
@@ -34,6 +37,8 @@ HIGGS_AUDIO_V3_PIPELINE = PipelineConfig(
|
||||
execution_type=StageExecutionType.LLM_AR,
|
||||
input_sources=(),
|
||||
owns_tokenizer=True,
|
||||
supports_native_mrv2_data_plane=True,
|
||||
custom_process_next_stage_input_func=f"{_PROC}.talker2code2wav_full_payload",
|
||||
engine_output_type="latent",
|
||||
# stop_token_ids: the model-owned sampler forces eos at ramp-down
|
||||
# completion. Safety stops from the actual V3 checkpoint:
|
||||
@@ -54,7 +59,9 @@ HIGGS_AUDIO_V3_PIPELINE = PipelineConfig(
|
||||
final_output_type="audio",
|
||||
engine_output_type="audio",
|
||||
model_arch="HiggsAudioV3Code2WavForConditionalGeneration",
|
||||
sync_process_input_func=f"{_PROC}.talker2code2wav",
|
||||
sync_process_input_func=f"{_PROC}.talker2code2wav_token_only",
|
||||
supports_native_mrv2_data_plane=True,
|
||||
requires_full_payload_input=True,
|
||||
sampling_constraints={"detokenize": True},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
# ruff: noqa: N803
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Fused per-step Higgs kernels for the MRV2 decode loop.
|
||||
|
||||
Each replaces a chain of small eager launches that ran on every model step
|
||||
(terminal-row restore, prefill reset and audio-tail validation before dense
|
||||
sampling; token and feedback embeddings on pure decode steps).
|
||||
"""
|
||||
|
||||
import torch
|
||||
from vllm.triton_utils import tl, triton
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _step_tail_guard_kernel(
|
||||
ids_ptr,
|
||||
qsl_ptr,
|
||||
has_codes_ptr,
|
||||
done_ptr,
|
||||
delay_ptr,
|
||||
eoc_ptr,
|
||||
error_ptr,
|
||||
num_rows,
|
||||
audio_id,
|
||||
eos_id,
|
||||
HAS_QSL: tl.constexpr,
|
||||
PROMPT_MODE: tl.constexpr,
|
||||
BLOCK: tl.constexpr,
|
||||
):
|
||||
rows = tl.arange(0, BLOCK)
|
||||
mask = rows < num_rows
|
||||
if HAS_QSL:
|
||||
start = tl.load(qsl_ptr + rows, mask=mask, other=0)
|
||||
end = tl.load(qsl_ptr + rows + 1, mask=mask, other=1)
|
||||
valid = end > 0
|
||||
tail = tl.load(ids_ptr + tl.maximum(end - 1, 0), mask=mask, other=audio_id)
|
||||
prefill = mask & ((end - start) > 1)
|
||||
else:
|
||||
valid = mask
|
||||
tail = tl.load(ids_ptr + rows, mask=mask, other=audio_id)
|
||||
prefill = rows < 0
|
||||
done = tl.load(done_ptr + rows, mask=mask, other=0) != 0
|
||||
has = tl.load(has_codes_ptr + rows, mask=mask, other=0) != 0
|
||||
if PROMPT_MODE:
|
||||
# An in-flight EOS row after retirement is terminal, never a new stream.
|
||||
done = done | (tail == eos_id)
|
||||
# New prefill rows start a fresh delay-pattern stream.
|
||||
done = done & ~prefill
|
||||
has = has & ~prefill
|
||||
tl.store(done_ptr + rows, done, mask=mask)
|
||||
tl.store(has_codes_ptr + rows, has, mask=mask)
|
||||
tl.store(delay_ptr + rows, 0, mask=prefill)
|
||||
tl.store(eoc_ptr + rows, -1, mask=prefill)
|
||||
ok = (tail == audio_id) & valid
|
||||
if PROMPT_MODE:
|
||||
ok = ok | has | done
|
||||
tl.store(error_ptr, tl.max((mask & ~ok).to(tl.int32), axis=0))
|
||||
|
||||
|
||||
def step_tail_guard(
|
||||
ids: torch.Tensor,
|
||||
qsl: torch.Tensor | None,
|
||||
has_codes: torch.Tensor,
|
||||
done: torch.Tensor,
|
||||
delay: torch.Tensor,
|
||||
eoc: torch.Tensor,
|
||||
error: torch.Tensor,
|
||||
*,
|
||||
audio_id: int,
|
||||
eos_id: int,
|
||||
prompt_mode: bool,
|
||||
) -> None:
|
||||
"""Fused terminal-row restore, prefill reset and audio-tail validation.
|
||||
|
||||
One launch replaces the index/compare/reset chains that ran before every
|
||||
dense sample, for pure decode and mixed prefill steps alike; ``error``
|
||||
becomes nonzero if any row is not an audio row. ``qsl`` (query start
|
||||
locations) selects each row's last token; ``None`` means one token per row.
|
||||
"""
|
||||
rows = int(has_codes.numel())
|
||||
_step_tail_guard_kernel[(1,)](
|
||||
ids,
|
||||
ids if qsl is None else qsl,
|
||||
has_codes,
|
||||
done,
|
||||
delay,
|
||||
eoc,
|
||||
error,
|
||||
rows,
|
||||
audio_id,
|
||||
eos_id,
|
||||
HAS_QSL=qsl is not None,
|
||||
PROMPT_MODE=prompt_mode,
|
||||
BLOCK=max(16, triton.next_power_of_2(rows)),
|
||||
)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _decode_embeddings_kernel(
|
||||
out_ptr,
|
||||
ids_ptr,
|
||||
text_weight_ptr,
|
||||
audio_weight_ptr,
|
||||
codes_ptr,
|
||||
has_codes_ptr,
|
||||
num_rows,
|
||||
hidden,
|
||||
audio_vocab,
|
||||
BOOKS: tl.constexpr,
|
||||
BLOCK: tl.constexpr,
|
||||
):
|
||||
row = tl.program_id(0)
|
||||
cols = tl.program_id(1) * BLOCK + tl.arange(0, BLOCK)
|
||||
mask = cols < hidden
|
||||
out = out_ptr + row.to(tl.int64) * hidden + cols
|
||||
if row >= num_rows:
|
||||
# Graph padding rows own no request and must stay zero.
|
||||
tl.store(out, tl.zeros((BLOCK,), dtype=out_ptr.dtype.element_ty), mask=mask)
|
||||
return
|
||||
if tl.load(has_codes_ptr + row) != 0:
|
||||
# Audio feedback: sum of the previous frame's codebook embeddings,
|
||||
# accumulated in float32 and rounded once like ``embedding(...).sum()``.
|
||||
acc = tl.zeros((BLOCK,), dtype=tl.float32)
|
||||
for book in range(BOOKS):
|
||||
code = tl.load(codes_ptr + row * BOOKS + book).to(tl.int64) + book * audio_vocab
|
||||
acc += tl.load(audio_weight_ptr + code * hidden + cols, mask=mask, other=0.0).to(tl.float32)
|
||||
tl.store(out, acc.to(out_ptr.dtype.element_ty), mask=mask)
|
||||
else:
|
||||
token = tl.maximum(tl.load(ids_ptr + row), 0).to(tl.int64)
|
||||
tl.store(out, tl.load(text_weight_ptr + token * hidden + cols, mask=mask), mask=mask)
|
||||
|
||||
|
||||
def decode_embeddings(
|
||||
out: torch.Tensor,
|
||||
ids: torch.Tensor,
|
||||
text_weight: torch.Tensor,
|
||||
audio_weight: torch.Tensor,
|
||||
codes: torch.Tensor,
|
||||
has_codes: torch.Tensor,
|
||||
padded_rows: int,
|
||||
audio_vocab: int,
|
||||
) -> None:
|
||||
"""One-token-per-row input embeddings with audio feedback, into ``out[:padded_rows]``."""
|
||||
rows, hidden = int(ids.numel()), int(out.shape[1])
|
||||
books = int(codes.shape[1])
|
||||
block = 1024
|
||||
_decode_embeddings_kernel[(padded_rows, triton.cdiv(hidden, block))](
|
||||
out, ids, text_weight, audio_weight, codes, has_codes, rows, hidden, audio_vocab, BOOKS=books, BLOCK=block
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Stage-input processor for higgs-audio v3: Talker -> Code2Wav.
|
||||
|
||||
Two adapters:
|
||||
@@ -21,6 +21,7 @@ to avoid corrupting valid tail content.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
@@ -95,6 +96,18 @@ def _filter_real_code_frames(audio_codes_qt: torch.Tensor) -> torch.Tensor:
|
||||
return frames[valid].t().contiguous()
|
||||
|
||||
|
||||
def _whole_utterance_codec_codes(audio_rows: torch.Tensor) -> torch.Tensor:
|
||||
"""Convert complete delayed [T, Q] rows to flat codebook-major CPU codes."""
|
||||
codes = _revert_delay_pattern(audio_rows.to(device="cpu", dtype=torch.long).t().contiguous())
|
||||
# Substitute special/padding codes after de-delay. Clamping would map EOC
|
||||
# to a valid codec entry and introduce audible artifacts.
|
||||
codes = torch.where((codes < 0) | (codes >= _NUM_REAL_CODES), 0, codes)
|
||||
# Preserve the existing removal of the residual ramp-down frame.
|
||||
if codes.shape[-1] >= 2:
|
||||
codes = codes[:, :-1]
|
||||
return codes.reshape(-1).contiguous()
|
||||
|
||||
|
||||
def talker2code2wav(
|
||||
source_outputs: list[Any],
|
||||
prompt: Any = None,
|
||||
@@ -133,43 +146,16 @@ def talker2code2wav(
|
||||
f"got {audio_codes.shape[1]}. Audio codes shape: {tuple(audio_codes.shape)}"
|
||||
)
|
||||
|
||||
# Transpose to [Q, T] for delay pattern reversal
|
||||
codes_qt = audio_codes.transpose(0, 1).contiguous().cpu()
|
||||
|
||||
# Step 1: Revert delay pattern
|
||||
try:
|
||||
codes_qt = _revert_delay_pattern(codes_qt)
|
||||
codec_codes = _whole_utterance_codec_codes(audio_codes)
|
||||
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.
|
||||
# Must use torch.where, NOT clamp: clamp(max=1023) turns 1025→1023
|
||||
# which is a valid codec code and decodes to audio artifacts.
|
||||
# Matches sglang's: torch.where(codes >= codec_vocab, 0, codes)
|
||||
codes_qt = torch.where(
|
||||
(codes_qt >= _NUM_REAL_CODES) | (codes_qt < 0),
|
||||
torch.zeros_like(codes_qt),
|
||||
codes_qt,
|
||||
)
|
||||
|
||||
# Step 3: Trim the last frame. After de-delay, the final frame
|
||||
# contains residual ramp-down codes (EOC→0 substituted) that
|
||||
# decode to a brief noise artifact at the end of the audio.
|
||||
if codes_qt.shape[-1] >= 2:
|
||||
codes_qt = codes_qt[:, :-1]
|
||||
|
||||
if codes_qt.numel() == 0:
|
||||
code2wav_inputs.append(_empty_code2wav_prompt())
|
||||
continue
|
||||
|
||||
# Code2Wav expects codebook-major flat: [Q * num_frames]
|
||||
codec_codes = codes_qt.reshape(-1).tolist()
|
||||
|
||||
code2wav_inputs.append(
|
||||
OmniTokensPrompt(
|
||||
prompt_token_ids=codec_codes,
|
||||
prompt_token_ids=codec_codes.tolist(),
|
||||
multi_modal_data=None,
|
||||
mm_processor_kwargs=None,
|
||||
additional_information=None,
|
||||
@@ -261,6 +247,13 @@ def talker2code2wav_async_chunk(
|
||||
elif not finished:
|
||||
return None
|
||||
|
||||
return _flush_async_chunk(
|
||||
transfer_manager, request_id, finished, _async_chunk_config(transfer_manager), emitted_frames
|
||||
)
|
||||
|
||||
|
||||
def _async_chunk_config(transfer_manager: Any) -> tuple[int, int, int, int]:
|
||||
"""Validated (chunk, left context, right holdback, initial chunk) frames."""
|
||||
connector = getattr(transfer_manager, "connector", None)
|
||||
raw_cfg = getattr(connector, "config", {}) or {}
|
||||
cfg = raw_cfg.get("extra", raw_cfg) if isinstance(raw_cfg, dict) else {}
|
||||
@@ -281,7 +274,18 @@ def talker2code2wav_async_chunk(
|
||||
f"codec_right_holdback_frames={right_holdback_size_config}, "
|
||||
f"initial_codec_chunk_frames={configured_initial_chunk_size}"
|
||||
)
|
||||
return chunk_size, left_context_size_config, right_holdback_size_config, configured_initial_chunk_size
|
||||
|
||||
|
||||
def _flush_async_chunk(
|
||||
transfer_manager: Any,
|
||||
request_id: str,
|
||||
finished: bool,
|
||||
config: tuple[int, int, int, int],
|
||||
emitted_frames: dict[str, int],
|
||||
) -> OmniPayloadStruct | None:
|
||||
"""Emit the next sliding-window codec chunk once enough rows accumulated."""
|
||||
chunk_size, left_context_size_config, right_holdback_size_config, configured_initial_chunk_size = config
|
||||
n_rows = len(transfer_manager.code_prompt_token_ids[request_id])
|
||||
de_delayed_total = max(0, n_rows - (_NUM_CODEBOOKS - 1))
|
||||
emitted = int(emitted_frames.get(request_id, 0))
|
||||
@@ -385,3 +389,83 @@ def talker2code2wav_async_chunk(
|
||||
codes=CodesStruct(audio=codec_codes),
|
||||
meta=meta,
|
||||
)
|
||||
|
||||
|
||||
def talker2code2wav_async_chunk_batch(
|
||||
transfer_manager: Any,
|
||||
pooling_outputs: list[OmniPayload | None],
|
||||
requests: list[Any],
|
||||
is_finished: list[bool],
|
||||
) -> list[OmniPayloadStruct | None]:
|
||||
"""One talker step of async-chunk payloads with one host conversion per row.
|
||||
|
||||
Same accumulation and flush semantics as ``talker2code2wav_async_chunk``;
|
||||
the chunk config is resolved once per step and each emitted ``[1, Q]``
|
||||
CPU row is converted with a single ``tolist`` instead of several small
|
||||
tensor operations per request.
|
||||
"""
|
||||
if not (len(pooling_outputs) == len(requests) == len(is_finished)):
|
||||
raise ValueError("batch codec inputs must have identical lengths")
|
||||
emitted_frames = getattr(transfer_manager, "higgs_v3_emitted_frames", None)
|
||||
if emitted_frames is None:
|
||||
emitted_frames = {}
|
||||
transfer_manager.higgs_v3_emitted_frames = emitted_frames
|
||||
config = None
|
||||
rows_by_request = transfer_manager.code_prompt_token_ids
|
||||
payloads: list[OmniPayloadStruct | None] = []
|
||||
for pooling_output, request, finished_flag in zip(pooling_outputs, requests, is_finished):
|
||||
request_id = request.external_req_id
|
||||
finished = bool(finished_flag or request.is_finished())
|
||||
if isinstance(pooling_output, dict):
|
||||
codes = pooling_output.get("codes")
|
||||
audio = codes.get("audio") if isinstance(codes, dict) else None
|
||||
if isinstance(audio, torch.Tensor) and audio.numel():
|
||||
if audio.ndim == 2 and audio.device.type == "cpu" and not audio.is_floating_point():
|
||||
row = audio.tolist()[-1]
|
||||
if len(row) != _NUM_CODEBOOKS:
|
||||
raise ValueError(
|
||||
f"talker emit row has {len(row)} codebooks; expected {_NUM_CODEBOOKS} "
|
||||
"for higgs_audio_v3 async_chunk."
|
||||
)
|
||||
else:
|
||||
row = _extract_last_step_row(pooling_output)
|
||||
row = None if row is None else row.cpu().tolist()
|
||||
if row is not None:
|
||||
rows_by_request[request_id].append(row)
|
||||
elif not finished:
|
||||
payloads.append(None)
|
||||
continue
|
||||
if config is None:
|
||||
config = _async_chunk_config(transfer_manager)
|
||||
payloads.append(_flush_async_chunk(transfer_manager, request_id, finished, config, emitted_frames))
|
||||
return payloads
|
||||
|
||||
|
||||
def talker2code2wav_token_only(source_outputs, prompt=None, _requires_multimodal_data=False):
|
||||
"""Control slots for the native full-payload consumer."""
|
||||
result = []
|
||||
for output in source_outputs:
|
||||
if not output.finished:
|
||||
continue
|
||||
mm = getattr(output.outputs[0], "multimodal_output", None)
|
||||
if isinstance(mm, Mapping) and isinstance(mm.get("codes", {}).get("audio"), torch.Tensor):
|
||||
result.extend(talker2code2wav([output], prompt, _requires_multimodal_data))
|
||||
else:
|
||||
result.append(OmniTokensPrompt(prompt_token_ids=[0]))
|
||||
return result
|
||||
|
||||
|
||||
def talker2code2wav_full_payload(transfer_manager, pooling_output, request):
|
||||
"""De-delay the accumulated raw rows once before native codec delivery."""
|
||||
del transfer_manager, request
|
||||
audio = pooling_output.get("codes.audio") if isinstance(pooling_output, dict) else None
|
||||
if audio is None and isinstance(pooling_output, dict):
|
||||
audio = pooling_output.get("codes", {}).get("audio")
|
||||
codes = torch.empty(0, dtype=torch.long)
|
||||
if isinstance(audio, torch.Tensor) and audio.numel():
|
||||
if audio.numel() % _NUM_CODEBOOKS:
|
||||
raise ValueError("Higgs payload must contain complete codebook rows")
|
||||
rows = audio.reshape(-1, _NUM_CODEBOOKS).to(device="cpu", dtype=torch.long)
|
||||
if rows.shape[0] >= _NUM_CODEBOOKS:
|
||||
codes = _whole_utterance_codec_codes(rows)
|
||||
return {"codes": {"audio": codes}, "meta": {"finished": torch.tensor(True)}}
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Configuration class for higgs-audio v3 (HiggsMultimodalQwen3) in vllm-omni.
|
||||
|
||||
``HiggsAudioV3Config.from_pretrained(model_path)`` returns a config with
|
||||
@@ -95,6 +95,8 @@ class HiggsAudioV3Config(PretrainedConfig):
|
||||
audio_continuation_id: int | None = None,
|
||||
enable_flashinfer_api_unwrap: bool = True,
|
||||
enable_mlp_cudagraph: bool = True,
|
||||
audio_full_sample_graph: bool | None = None,
|
||||
codec_cuda_graph: bool | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
# Legacy perf knob removed: Higgs v3 scheduler tokens now come from
|
||||
@@ -131,9 +133,18 @@ class HiggsAudioV3Config(PretrainedConfig):
|
||||
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)
|
||||
self.audio_full_sample_graph = audio_full_sample_graph
|
||||
self.codec_cuda_graph = codec_cuda_graph
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def resolve_graph_defaults(self, *, use_v2_model_runner: bool) -> tuple[bool, bool]:
|
||||
"""Resolve sampler/codec graphs without mutating shared checkpoint config."""
|
||||
return (
|
||||
use_v2_model_runner if self.audio_full_sample_graph is None else self.audio_full_sample_graph,
|
||||
use_v2_model_runner if self.codec_cuda_graph is None else self.codec_cuda_graph,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_pretrained(cls, pretrained_model_name_or_path: str, **kwargs: Any) -> HiggsAudioV3Config:
|
||||
"""Load config and resolve special token IDs from the checkpoint tokenizer.
|
||||
|
||||
@@ -13,6 +13,7 @@ from contextlib import AbstractContextManager, nullcontext
|
||||
import torch
|
||||
from vllm.logger import init_logger
|
||||
from vllm.utils.mem_utils import format_gib, memory_profiling
|
||||
from vllm.v1.worker.gpu_worker import CompilationTimes
|
||||
from vllm.v1.worker.gpu_worker import Worker as GPUWorker
|
||||
|
||||
from vllm_omni.diffusion.data import (
|
||||
@@ -39,6 +40,17 @@ class OmniGPUWorkerBase(GPUWorker):
|
||||
for custom trace naming, background gzip, and trace path collection.
|
||||
"""
|
||||
|
||||
def _capture_auxiliary_graphs(self) -> None:
|
||||
"""Let opt-in models warm valid inputs before the worker becomes ready."""
|
||||
capture = getattr(self.model_runner.model, "capture_auxiliary_graphs", None)
|
||||
if callable(capture):
|
||||
capture()
|
||||
|
||||
def compile_or_warm_up_model(self) -> CompilationTimes:
|
||||
result = super().compile_or_warm_up_model()
|
||||
self._capture_auxiliary_graphs()
|
||||
return result
|
||||
|
||||
def load_model(self, *args, **kwargs):
|
||||
with self._maybe_get_memory_pool_context("weights"):
|
||||
res = super().load_model(*args, **kwargs)
|
||||
@@ -225,7 +237,7 @@ class OmniGPUWorkerBase(GPUWorker):
|
||||
logger.info(f"[LLM Worker {self.rank}] Wake-up complete.")
|
||||
return True
|
||||
|
||||
def handle_sleep_task(self, task: OmniSleepTask) -> OmniACK:
|
||||
def handle_sleep_task(self, task: OmniSleepTask) -> OmniACK | None:
|
||||
"Handle deterministic Sleep command from the main process"
|
||||
try:
|
||||
if isinstance(task, dict):
|
||||
@@ -279,7 +291,7 @@ class OmniGPUWorkerBase(GPUWorker):
|
||||
pass
|
||||
return OmniACK(task_id=task.task_id, status="ERROR", error_msg=str(e))
|
||||
|
||||
def handle_wake_task(self, task: OmniWakeTask) -> OmniACK:
|
||||
def handle_wake_task(self, task: OmniWakeTask) -> OmniACK | None:
|
||||
"Handle deterministic Wakeup command from the main process"
|
||||
try:
|
||||
if isinstance(task, dict):
|
||||
|
||||
@@ -1655,7 +1655,9 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin, Duplex
|
||||
model_config = getattr(self, "model_config", None)
|
||||
if model_config is None:
|
||||
model_config = getattr(getattr(self, "vllm_config", None), "model_config", None)
|
||||
if not bool(getattr(model_config, "async_chunk", False)):
|
||||
if not bool(getattr(model_config, "async_chunk", False)) and not self._model_omni_flag(
|
||||
getattr(self, "model", None), "supports_async_whole_payload"
|
||||
):
|
||||
return False
|
||||
if bool(getattr(model_config, "enable_return_routed_experts", False)):
|
||||
return False
|
||||
@@ -2110,6 +2112,14 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin, Duplex
|
||||
scheduler_output.total_num_scheduled_tokens,
|
||||
)
|
||||
|
||||
post_sample_mm = getattr(getattr(self, "model", None), "post_sample_multimodal_outputs", None)
|
||||
if callable(post_sample_mm):
|
||||
multimodal_outputs = post_sample_mm(
|
||||
req_ids=req_ids_output_copy,
|
||||
invalid_req_indices=invalid_req_indices,
|
||||
multimodal_outputs=multimodal_outputs,
|
||||
)
|
||||
|
||||
multimodal_outputs = self._run_post_sample_talker_mtp(
|
||||
req_ids=req_ids_output_copy,
|
||||
valid_sampled_token_ids=valid_sampled_token_ids,
|
||||
@@ -2184,12 +2194,18 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin, Duplex
|
||||
if output_tensor_snapshot.async_payload is not None:
|
||||
with record_function_or_nullcontext("omni_async_output:wait_cpu_payload"):
|
||||
output_tensor_snapshot.async_payload.wait()
|
||||
mm_snapshot = output_tensor_snapshot.multimodal_outputs
|
||||
finalize_snapshot = getattr(
|
||||
getattr(self, "model", None), "finalize_multimodal_outputs_from_cpu_snapshot", None
|
||||
)
|
||||
if callable(finalize_snapshot):
|
||||
mm_snapshot = finalize_snapshot(mm_snapshot)
|
||||
with record_function_or_nullcontext("omni_output_builder:total"):
|
||||
output = self._build_omni_model_runner_output_from_snapshot(
|
||||
scheduler_output=scheduler_output_snapshot,
|
||||
hidden_states=output_tensor_snapshot.hidden_states,
|
||||
staged_hidden_states_cpu=output_tensor_snapshot.staged_hidden_states_cpu,
|
||||
multimodal_outputs=output_tensor_snapshot.multimodal_outputs,
|
||||
multimodal_outputs=mm_snapshot,
|
||||
req_ids_output_copy=req_ids_output_snapshot,
|
||||
req_id_to_index_output_copy=req_id_to_index_output_snapshot,
|
||||
valid_sampled_token_ids=valid_sampled_token_ids_snapshot,
|
||||
|
||||
@@ -126,4 +126,5 @@ class GPUGenerationWorker(OmniWorkerMixin, OmniGPUWorkerBase):
|
||||
|
||||
start = time.perf_counter()
|
||||
self.model_runner.profile_run()
|
||||
self._capture_auxiliary_graphs()
|
||||
return CompilationTimes(language_model=time.perf_counter() - start, encoder=0.0)
|
||||
@@ -2148,7 +2148,19 @@ class OmniGPUModelRunner(PrefixCacheRunnerMixin, GPUModelRunner):
|
||||
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):
|
||||
cpu_input_tail_ids = None
|
||||
if (
|
||||
getattr(self.model, "requires_cpu_input_tail_ids", False)
|
||||
and not self.use_async_scheduling
|
||||
and input_ids is not None
|
||||
and input_ids.data_ptr() == self.input_ids.gpu.data_ptr()
|
||||
):
|
||||
count = len(self.input_batch.req_ids)
|
||||
tails = self.query_start_loc.cpu[1 : count + 1].to(torch.long) - 1
|
||||
if count and bool((tails >= 0).all()) and bool((tails < input_ids.numel()).all()):
|
||||
cpu_input_tail_ids = self.input_ids.cpu.index_select(0, tails).tolist()
|
||||
update_decode_metadata(
|
||||
cpu_input_tail_ids=cpu_input_tail_ids,
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
inputs_embeds=inputs_embeds,
|
||||
@@ -2156,14 +2168,19 @@ class OmniGPUModelRunner(PrefixCacheRunnerMixin, GPUModelRunner):
|
||||
req_ids=self.input_batch.req_ids,
|
||||
)
|
||||
|
||||
model_output = super()._model_forward(
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
intermediate_tensors=intermediate_tensors,
|
||||
inputs_embeds=inputs_embeds,
|
||||
**model_kwargs,
|
||||
**model_kwargs_extra,
|
||||
)
|
||||
try:
|
||||
model_output = super()._model_forward(
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
intermediate_tensors=intermediate_tensors,
|
||||
inputs_embeds=inputs_embeds,
|
||||
**model_kwargs,
|
||||
**model_kwargs_extra,
|
||||
)
|
||||
finally:
|
||||
finish_decode_step = getattr(self.model, "finish_decode_step_forward", None)
|
||||
if callable(finish_decode_step):
|
||||
finish_decode_step()
|
||||
# CUDAGraphWrapper's weak_ref_tensors preserves the fields but turns
|
||||
# NamedTuple outputs into plain tuples. Restore the Omni envelope
|
||||
# before a model adapter or extraction discards its multimodal fields.
|
||||
|
||||
@@ -31,6 +31,9 @@ def init_omni_model_state(
|
||||
capabilities; otherwise
|
||||
delegates to the upstream v2 factory.
|
||||
"""
|
||||
factory = getattr(model, "create_omni_model_state", None)
|
||||
if factory is not None:
|
||||
return factory(vllm_config, encoder_cache, device)
|
||||
uses_omni_lifecycle = any(
|
||||
getattr(model, flag, False) is True for flag in ("has_preprocess", "has_postprocess", "have_multimodal_outputs")
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@ import inspect
|
||||
import threading
|
||||
import types
|
||||
from collections.abc import Callable
|
||||
from dataclasses import replace
|
||||
from typing import Any, cast
|
||||
|
||||
import numpy as np
|
||||
@@ -26,10 +27,12 @@ from vllm.config import VllmConfig
|
||||
from vllm.config.compilation import CUDAGraphMode
|
||||
from vllm.logger import init_logger
|
||||
from vllm.v1.core.sched.output import NewRequestData
|
||||
from vllm.v1.kv_cache_interface import KVCacheConfig
|
||||
from vllm.v1.worker.gpu.input_batch import InputBatch
|
||||
from vllm.v1.worker.gpu.mm.encoder_cache import EncoderCache
|
||||
from vllm.v1.worker.gpu.model_states.default import DefaultModelState
|
||||
from vllm.v1.worker.gpu.states import RequestState
|
||||
from vllm.v1.worker.utils import AttentionGroup
|
||||
|
||||
from vllm_omni.model_executor.models.output_templates import OmniOutput, OwnedBatchTensor
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
@@ -413,6 +416,36 @@ class OmniModelState(DefaultModelState):
|
||||
# Input preparation
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def prepare_attn(
|
||||
self,
|
||||
input_batch: InputBatch,
|
||||
cudagraph_mode: CUDAGraphMode,
|
||||
block_tables: tuple[torch.Tensor, ...],
|
||||
slot_mappings: torch.Tensor,
|
||||
attn_groups: list[list[AttentionGroup]],
|
||||
kv_cache_config: KVCacheConfig,
|
||||
for_capture: bool = False,
|
||||
ubatch_idx: int = 0,
|
||||
) -> dict[str, Any]:
|
||||
if for_capture and input_batch.max_query_len is None:
|
||||
# vLLM 0.30 distributes dummy tokens evenly across requests. For
|
||||
# an unconstrained FULL graph that split is not a query-length
|
||||
# bound: replay may put the entire token bucket in one request.
|
||||
# Attention launch parameters are fixed at capture, so use the
|
||||
# bucket's worst-case query length, not the dummy per-row length.
|
||||
# Keep explicit bounds (e.g. varlen decode) and runtime metadata.
|
||||
input_batch = replace(input_batch, max_query_len=input_batch.num_tokens)
|
||||
return super().prepare_attn(
|
||||
input_batch,
|
||||
cudagraph_mode,
|
||||
block_tables,
|
||||
slot_mappings,
|
||||
attn_groups,
|
||||
kv_cache_config,
|
||||
for_capture=for_capture,
|
||||
ubatch_idx=ubatch_idx,
|
||||
)
|
||||
|
||||
def prepare_inputs_embeds(
|
||||
self,
|
||||
scheduled_encoder_inputs: dict[str, list[int]],
|
||||
|
||||
Reference in new issue
Block a user