Signed-off-by: Shenglei Fu <sfu@confluent.io> Co-authored-by: Claude <noreply@anthropic.com>
148 lines
6.2 KiB
Python
148 lines
6.2 KiB
Python
import pytest
|
|
import torch
|
|
|
|
from vllm_omni.utils import mm_outputs as mm_outputs_mod
|
|
from vllm_omni.utils.mm_outputs import build_mm_cpu
|
|
|
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
|
|
|
TOTAL_TOKENS = 5
|
|
NUM_TOKEN_ALIGNED_TENSORS = 3 # codes.audio, meta.ref_code_len, meta.codec_streaming
|
|
|
|
|
|
def make_talker_passthrough_payload(num_requests: int, device: str = "cpu") -> dict:
|
|
"""The passthrough set qwen3-tts actually hands to build_mm_cpu each decode
|
|
step (qwen3_tts_talker.make_omni_output): a nested codes/meta dict whose
|
|
``codes.ref`` holds one tensor per request, alongside 1D int token-aligned
|
|
metadata. Under prefix caching only ``codes.audio`` is 2D and token-aligned,
|
|
so it is the sole cache key and this whole structure is passthrough.
|
|
"""
|
|
return {
|
|
"codes": {
|
|
"audio": torch.rand(TOTAL_TOKENS, 4, device=device),
|
|
"ref": [torch.arange(idx + 1, dtype=torch.long, device=device) for idx in range(num_requests)],
|
|
},
|
|
"meta": {
|
|
"ref_code_len": torch.full((TOTAL_TOKENS,), 7, dtype=torch.int32, device=device),
|
|
"codec_streaming": torch.ones(TOTAL_TOKENS, dtype=torch.int8, device=device),
|
|
},
|
|
}
|
|
|
|
|
|
def iter_tensors(value):
|
|
"""Yield every tensor leaf, recursing the same dict/list nesting _to_cpu does."""
|
|
if isinstance(value, torch.Tensor):
|
|
yield value
|
|
elif isinstance(value, dict):
|
|
for item in value.values():
|
|
yield from iter_tensors(item)
|
|
elif isinstance(value, list):
|
|
for item in value:
|
|
yield from iter_tensors(item)
|
|
|
|
|
|
@pytest.mark.parametrize("num_requests", [1, 3])
|
|
def test_build_mm_cpu_d2h_count_grows_per_request(num_requests, mocker):
|
|
"""Pins the pre-W2 cost: build_mm_cpu issues one per-tensor copy for each
|
|
token-aligned payload tensor plus one more for every request's ``codes.ref``
|
|
entry, so the transfer count grows linearly with batch size. That growth is
|
|
what RFC #4855 W2 sets out to batch away.
|
|
|
|
Counting tensor leaves rather than dict keys is deliberate: a flat dict of
|
|
K tensors makes ``call_count == K`` true by construction, since build_mm_cpu
|
|
is a loop over items. Only the per-request list can move this number.
|
|
"""
|
|
spy = mocker.spy(mm_outputs_mod, "_to_cpu")
|
|
|
|
result = build_mm_cpu(multimodal_outputs=make_talker_passthrough_payload(num_requests))
|
|
|
|
tensor_copies = [call for call in spy.call_args_list if isinstance(call.args[0], torch.Tensor)]
|
|
assert len(tensor_copies) == NUM_TOKEN_ALIGNED_TENSORS + num_requests
|
|
assert len(result["codes"]["ref"]) == num_requests
|
|
|
|
|
|
def test_build_mm_cpu_preserves_int_dtypes_of_talker_metadata():
|
|
"""ref_code_len is int32 and codec_streaming int8 at the source; a W2 that
|
|
stages passthrough through a float buffer would corrupt both silently."""
|
|
result = build_mm_cpu(multimodal_outputs=make_talker_passthrough_payload(3))
|
|
|
|
assert result["meta"]["ref_code_len"].dtype == torch.int32
|
|
assert result["meta"]["codec_streaming"].dtype == torch.int8
|
|
assert all(ref.dtype == torch.long for ref in result["codes"]["ref"])
|
|
|
|
|
|
def test_build_mm_cpu_returns_cpu_tensors_for_every_nested_leaf():
|
|
"""The output contract: every tensor leaf, including those inside the
|
|
``codes.ref`` list, comes back on CPU.
|
|
|
|
On this lane the inputs are already CPU, so this documents the contract
|
|
rather than proving it -- ``.to("cpu")`` is a no-op here. The GPU-gated
|
|
sibling below is the one that can actually fail.
|
|
"""
|
|
result = build_mm_cpu(multimodal_outputs=make_talker_passthrough_payload(3))
|
|
|
|
leaves = list(iter_tensors(result))
|
|
assert len(leaves) == NUM_TOKEN_ALIGNED_TENSORS + 3
|
|
assert all(leaf.device.type == "cpu" for leaf in leaves)
|
|
|
|
|
|
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires a CUDA device")
|
|
def test_build_mm_cpu_moves_a_gpu_payload_off_the_device():
|
|
"""D2H is the entire purpose of build_mm_cpu. RFC #4855 W2 lists "skip the
|
|
CPU conversion when the consumer is the same-GPU next stage" as one of its
|
|
three options; that option leaves every tensor on the device and no CPU-lane
|
|
test can observe it, because there ``.to("cpu")`` is a silent no-op.
|
|
"""
|
|
payload = make_talker_passthrough_payload(3, device="cuda")
|
|
source_leaves = list(iter_tensors(payload))
|
|
assert all(leaf.device.type == "cuda" for leaf in source_leaves)
|
|
|
|
result = build_mm_cpu(multimodal_outputs=payload)
|
|
|
|
leaves = list(iter_tensors(result))
|
|
assert len(leaves) == len(source_leaves)
|
|
assert all(leaf.device.type == "cpu" for leaf in leaves)
|
|
|
|
|
|
def test_build_mm_cpu_empty_input_is_a_noop(mocker):
|
|
spy = mocker.spy(mm_outputs_mod, "_to_cpu")
|
|
|
|
assert build_mm_cpu(multimodal_outputs={}) == {}
|
|
spy.assert_not_called()
|
|
|
|
|
|
def test_build_mm_cpu_preserves_nested_and_non_tensor_payload_semantics():
|
|
"""_to_cpu must recurse into nested dict/list structures, force
|
|
non-contiguous tensors to become contiguous, detach from autograd,
|
|
and leave dtype/shape/values unchanged; non-tensor leaves (scalar,
|
|
string) pass through untouched. Any W2 implementation (cat, deferral,
|
|
or a new materializer) must preserve this exact semantics."""
|
|
non_contig = torch.arange(9, dtype=torch.float32).reshape(3, 3).t()
|
|
assert not non_contig.is_contiguous()
|
|
grad_tensor = torch.tensor([5.0, 6.0], requires_grad=True)
|
|
|
|
payload = {
|
|
"nested": {"inner_tensor": non_contig, "inner_scalar": "keep-me"},
|
|
"list_of_tensors": [torch.tensor([1.0, 2.0]), "marker", torch.tensor([3.0, 4.0])],
|
|
"grad_key": grad_tensor,
|
|
"scalar": 42,
|
|
}
|
|
result = build_mm_cpu(multimodal_outputs=payload)
|
|
|
|
inner = result["nested"]["inner_tensor"]
|
|
assert inner.is_contiguous()
|
|
assert inner.dtype == non_contig.dtype
|
|
assert inner.shape == non_contig.shape
|
|
assert torch.equal(inner, non_contig)
|
|
assert result["nested"]["inner_scalar"] == "keep-me"
|
|
|
|
lst = result["list_of_tensors"]
|
|
assert torch.equal(lst[0], torch.tensor([1.0, 2.0]))
|
|
assert lst[1] == "marker"
|
|
assert torch.equal(lst[2], torch.tensor([3.0, 4.0]))
|
|
|
|
assert result["grad_key"].requires_grad is False
|
|
assert torch.equal(result["grad_key"], grad_tensor.detach())
|
|
|
|
assert result["scalar"] == 42
|