Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 89 additions & 4 deletions tests/unit/higgs_audio_v3/test_higgs_audio_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ class FakeTalker:

t = FakeTalker()
t._sample_audio_codes = mod.HiggsAudioV3TalkerForConditionalGeneration._sample_audio_codes.__get__(t)
t._expand_audio_sampling_params = mod.HiggsAudioV3TalkerForConditionalGeneration._expand_audio_sampling_params
return t

def _make_batched_sampler_talker(self, num_rows=4):
Expand Down Expand Up @@ -300,16 +301,30 @@ class FakeTalker:
"_audio_seed_mask_from_step_input",
):
setattr(t, name, getattr(cls, name).__get__(t))
t._expand_audio_sampling_params = cls._expand_audio_sampling_params
t._device_cache_key = cls._device_cache_key
return t

@staticmethod
def _sampling_metadata(temperature=1.0, top_k=50, top_p=0.95):
return type(
"SamplingMetadata",
(),
{
"temperature": temperature,
"top_k": top_k,
"top_p": top_p,
"all_greedy": temperature is None,
},
)()

def test_sample_respects_mask(self):
"""Tokens masked to -inf must never be sampled."""
t = self._make_batched_sampler_talker(num_rows=1)
logits = torch.full((10, 1026), float("-inf"))
# Only token 500 is allowed for each row
logits[:, 500] = 0.0
result = t._sample_audio_codes(logits)
result = t._sample_audio_codes(logits, self._sampling_metadata(), num_codebooks=1)
assert result.shape == (10,)
assert (result == 500).all(), f"Expected all 500, got {result.tolist()}"

Expand All @@ -320,10 +335,76 @@ def test_sample_all_masked_falls_back_to_argmax(self):
# Row 0: all masked
# Row 1: only token 42 allowed
logits[1, 42] = 0.0
result = t._sample_audio_codes(logits)
result = t._sample_audio_codes(logits, self._sampling_metadata(), num_codebooks=1)
assert result.shape == (2,)
assert result[1].item() == 42

def test_top_k_minus_one_disables_top_k_filtering(self, monkeypatch):
t = self._make_minimal_talker()
seen_probs = []

def fake_multinomial(probs, num_samples):
seen_probs.append(probs.clone())
return probs.argmax(dim=-1, keepdim=True)

monkeypatch.setattr(torch, "multinomial", fake_multinomial)

logits = torch.tensor([[4.0, 3.0, 2.0, 1.0]])
metadata = self._sampling_metadata(
temperature=1.0,
top_k=-1,
top_p=1.0,
)

t._sample_audio_codes(logits, metadata, num_codebooks=1)

assert len(seen_probs) == 1
assert torch.count_nonzero(seen_probs[0][0]).item() == 4

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)
)
logits = torch.tensor([[0.0, 2.0], [3.0, 0.0]])

result = t._sample_audio_codes(
logits,
self._sampling_metadata(torch.tensor([float("nan"), float("inf")])),
num_codebooks=1,
)

assert len(seen) == 1
assert torch.isfinite(seen[0]).all()
assert result.tolist() == [1, 0]

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)
)
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(
temperature=torch.tensor([0.5, 1.0]),
top_k=torch.tensor([1, 4]),
top_p=torch.tensor([1.0, 0.8]),
)

temperatures, top_ks, top_ps = t._expand_audio_sampling_params(metadata, logits, num_codebooks=2)
t._sample_audio_codes(logits, metadata, num_codebooks=2)

torch.testing.assert_close(temperatures, torch.tensor([0.5, 0.5, 1.0, 1.0]))
assert top_ks.tolist() == [1, 1, 4, 4]
torch.testing.assert_close(top_ps, torch.tensor([1.0, 1.0, 0.8, 0.8]))
assert len(seen) == 1
torch.testing.assert_close(seen[0][0], seen[0][1])
torch.testing.assert_close(seen[0][2], seen[0][3])
assert torch.count_nonzero(seen[0][0]).item() == 1
assert torch.count_nonzero(seen[0][2]).item() > 1

def test_delay_masking_forces_boc_during_delay(self):
"""During delay phase, codebooks beyond delay_count must have only BOC allowed."""
from vllm_omni.model_executor.models.higgs_audio_v3.higgs_audio_v3_talker import BOC_ID
Expand Down Expand Up @@ -468,7 +549,9 @@ def test_direct_audio_sampler_does_not_override_done_tokens(self):
t._fast_audio_sampler_gpu_fallback_reason = lambda **kwargs: None
t._audio_codebook_logits_from_rows = lambda hidden, rows, all_rows=False: torch.zeros(2, 8, 1026)
t._apply_delay_pattern_masking_batched = lambda cb_logits, audio_rows, all_rows=False: None
t._sample_audio_codes = lambda logits_2d: torch.zeros(int(logits_2d.shape[0]), dtype=torch.long)
t._sample_audio_codes = lambda logits_2d, *args, **kwargs: torch.zeros(
int(logits_2d.shape[0]), dtype=torch.long
)
t._update_delay_state_batched = lambda *args, **kwargs: None
t.sample = mod.HiggsAudioV3TalkerForConditionalGeneration.sample.__get__(t)

Expand Down Expand Up @@ -499,7 +582,9 @@ def test_direct_audio_sampler_uses_gpu_tokens_for_active_rows(self):
t._fast_audio_sampler_gpu_fallback_reason = lambda **kwargs: None
t._audio_codebook_logits_from_rows = lambda hidden, rows, all_rows=False: torch.zeros(2, 8, 1026)
t._apply_delay_pattern_masking_batched = lambda cb_logits, audio_rows, all_rows=False: None
t._sample_audio_codes = lambda logits_2d: torch.zeros(int(logits_2d.shape[0]), dtype=torch.long)
t._sample_audio_codes = lambda logits_2d, *args, **kwargs: torch.zeros(
int(logits_2d.shape[0]), dtype=torch.long
)
t._update_delay_state_batched = lambda *args, **kwargs: None
t.sample = mod.HiggsAudioV3TalkerForConditionalGeneration.sample.__get__(t)

Expand Down
80 changes: 80 additions & 0 deletions tests/worker/test_gpu_ar_model_runner.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import asyncio
from collections.abc import Sequence
from types import SimpleNamespace

import numpy as np
import pytest
import torch
from vllm.sampling_params import SamplingParams
from vllm.v1.worker import gpu_input_batch
from vllm.v1.worker.gpu_input_batch import CachedRequestState, InputBatch

from vllm_omni.entrypoints.openai.protocol.audio import OpenAICreateSpeechRequest
from vllm_omni.entrypoints.openai.serving_speech import OmniOpenAIServingSpeech
from vllm_omni.entrypoints.openai.tts_adapters.base import PreparedRequest
from vllm_omni.outputs import OmniModelRunnerOutput
from vllm_omni.worker.gpu_ar_model_runner import (
ExecuteModelState,
Expand All @@ -25,6 +32,79 @@ def _make_runner(engine_output_type: str | None, downstream_req_ids: set[str]) -
return runner


def test_speech_extra_params_reach_model_sampler_as_sampling_metadata(monkeypatch):
requested = {"temperature": 0.7, "top_p": 0.8, "top_k": 17}
request = OpenAICreateSpeechRequest.model_validate(
{"input": "Hello", "extra_params": requested},
)

class Adapter:
def validate(self, request):
return None

async def build(self, request, sampling_params_list, has_inline_ref_audio):
return PreparedRequest(prompt={"prompt": request.input}, tts_params={}, model_type="higgs_audio_v3")

engine = SimpleNamespace(
errored=False,
default_sampling_params_list=[SamplingParams(temperature=1.0, top_p=0.95, top_k=50)],
generate=lambda **kwargs: kwargs["sampling_params_list"],
)
serving = object.__new__(OmniOpenAIServingSpeech)
serving.engine_client = engine
serving.model_config = SimpleNamespace(async_chunk=True)
serving._tts_model_type = "higgs_audio_v3"
serving._get_tts_adapter = lambda: Adapter()
serving._track_ref_audio_artifact_warmup = lambda *args, **kwargs: None

_, stage_sampling_params, _ = asyncio.run(serving._prepare_speech_generation(request, request_id="speech-test"))
stage0_params = stage_sampling_params[0]

monkeypatch.setattr(gpu_input_batch, "PIN_MEMORY", False)
input_batch = InputBatch(
max_num_reqs=1,
max_model_len=8,
max_num_batched_tokens=8,
device=torch.device("cpu"),
vocab_size=1024,
block_sizes=[1],
kernel_block_sizes=[1],
)
input_batch.add_request(
CachedRequestState(
req_id="speech-test",
prompt_token_ids=[1],
mm_features=[],
sampling_params=stage0_params,
generator=None,
block_ids=([],),
num_computed_tokens=0,
output_token_ids=[],
)
)
input_batch.sampling_metadata = input_batch._make_sampling_metadata()

received = []
runner = object.__new__(GPUARModelRunner)
runner.input_batch = input_batch
runner.model = SimpleNamespace(
prefer_model_sampler=True,
skips_model_sampler_output_token_history=True,
sample=lambda logits, metadata: received.append(metadata) or "model-sampler",
)
runner.sampler = SimpleNamespace()
logits = torch.zeros((1, 4))

output = runner._sample(logits, spec_decode_metadata=None)

assert output == "model-sampler"
assert len(received) == 1
sampling_metadata = received[0]
torch.testing.assert_close(sampling_metadata.temperature, torch.tensor([requested["temperature"]]))
torch.testing.assert_close(sampling_metadata.top_p, torch.tensor([requested["top_p"]]))
assert sampling_metadata.top_k.tolist() == [requested["top_k"]]


def test_resolve_pooler_payload_req_ids_audio_terminal_stage_keeps_payload():
runner = _make_runner(engine_output_type="audio", downstream_req_ids=set())

Expand Down
3 changes: 3 additions & 0 deletions vllm_omni/entrypoints/openai/serving_speech.py
Original file line number Diff line number Diff line change
Expand Up @@ -3183,6 +3183,9 @@ async def _prepare_speech_generation(
import copy

sampling_params_list = copy.deepcopy(sampling_params_list)
for name in ("temperature", "top_p", "top_k"):
if (value := request.extra_params.get(name)) is not None:
setattr(sampling_params_list[0], name, value)
if sampling_params_list[0].extra_args is None:
sampling_params_list[0].extra_args = {}
sampling_params_list[0].extra_args.update(request.extra_params)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
from vllm.model_executor.models.qwen3 import Qwen3Model
from vllm.platforms import current_platform
from vllm.v1.outputs import SamplerOutput
from vllm.v1.sample.metadata import SamplingMetadata
from vllm.v1.sample.ops.topk_topp_sampler import apply_top_k_top_p

from vllm_omni.model_executor.models.output_templates import OmniOutput
from vllm_omni.transformers_utils.configs.higgs_audio_v3 import (
Expand Down Expand Up @@ -983,7 +985,11 @@ def run_stock_sampler() -> Any:

# Sample per-codebook
cb_logits_2d = cb_logits.reshape(-1, cb_logits.shape[-1])
codes_2d = self._sample_audio_codes(cb_logits_2d)
codes_2d = self._sample_audio_codes(
cb_logits_2d,
sampling_metadata,
int(cb_logits.shape[1]),
)
codes_flat = codes_2d.view(cb_logits.shape[0], cb_logits.shape[1]).to(torch.long)

self._update_delay_state_batched(
Expand Down Expand Up @@ -1337,23 +1343,42 @@ def _update_delay_state_batched(
self._postprocess_audio_rows = num_rows
self._postprocess_audio_active_rows = 0

def _sample_audio_codes(self, logits_2d: torch.Tensor) -> torch.Tensor:
@staticmethod
def _expand_audio_sampling_params(
sampling_metadata: SamplingMetadata, logits_2d: torch.Tensor, num_codebooks: int
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
batch_size, remainder = divmod(int(logits_2d.shape[0]), num_codebooks)
if remainder:
raise ValueError(f"audio codebook logits rows must be divisible by {num_codebooks}")

def expand(value, default, dtype):
if value is None:
value = torch.full((batch_size,), default, device=logits_2d.device, dtype=dtype)
else:
value = torch.as_tensor(value, device=logits_2d.device, dtype=dtype).expand(batch_size)
return value.repeat_interleave(num_codebooks)

return (
expand(sampling_metadata.temperature, 0.0 if sampling_metadata.all_greedy else 1.0, logits_2d.dtype),
expand(sampling_metadata.top_k, logits_2d.shape[-1], torch.long),
expand(sampling_metadata.top_p, 1.0, logits_2d.dtype),
)

def _sample_audio_codes(
self, logits_2d: torch.Tensor, sampling_metadata: SamplingMetadata, num_codebooks: int = 8
) -> torch.Tensor:
"""Replicate upstream sampling: temperature → top-k → top-p → multinomial."""
x = logits_2d.float()
top_k = 50
top_p = 0.95
if 0 < top_k < x.shape[-1]:
kth = x.topk(top_k, dim=-1).values[..., -1:]
x = x.masked_fill(x < kth, float("-inf"))
if 0.0 < top_p < 1.0:
sorted_logits, sorted_idx = x.sort(dim=-1, descending=True)
cumprobs = sorted_logits.softmax(dim=-1).cumsum(dim=-1)
sorted_mask = cumprobs > top_p
sorted_mask[..., 1:] = sorted_mask[..., :-1].clone()
sorted_mask[..., 0] = False
mask = torch.zeros_like(x, dtype=torch.bool)
mask.scatter_(-1, sorted_idx, sorted_mask)
x = x.masked_fill(mask, float("-inf"))
temperatures, top_ks, top_ps = self._expand_audio_sampling_params(sampling_metadata, x, num_codebooks)
top_ks = torch.where(top_ks <= 0, torch.full_like(top_ks, x.shape[-1]), top_ks)
top_ks = top_ks.clamp(max=x.shape[-1])
top_ps = top_ps.clamp(min=0.0, max=1.0)
temperatures = torch.where(torch.isfinite(temperatures), temperatures, torch.ones_like(temperatures))
top_ps = torch.where(torch.isfinite(top_ps), top_ps, torch.ones_like(top_ps))
greedy = temperatures <= 0
safe_temperatures = torch.where(greedy, torch.ones_like(temperatures), temperatures)
x.div_(safe_temperatures.unsqueeze(-1))
x = apply_top_k_top_p(x, top_ks, top_ps)
# Detect all-masked rows BEFORE softmax (softmax of all-inf yields NaN).
has_finite = torch.isfinite(x).any(dim=-1)
all_masked = ~has_finite
Expand All @@ -1362,7 +1387,8 @@ def _sample_audio_codes(self, logits_2d: torch.Tensor) -> torch.Tensor:
safe_x = torch.where(all_masked.unsqueeze(-1), torch.zeros_like(x), x)
probs = safe_x.softmax(dim=-1)
sampled = torch.multinomial(probs, num_samples=1).squeeze(-1)
return torch.where(all_masked, fallback, sampled)
sampled = torch.where(all_masked, fallback, sampled)
return torch.where(greedy, logits_2d.argmax(dim=-1), sampled)

# ------------------------------------------------------------------ omni output
def make_omni_output(self, model_outputs: torch.Tensor | OmniOutput, **kwargs: Any) -> OmniOutput:
Expand Down
Loading