fix: preserve Torch seeded sampling on ROCm (#8341)

Signed-off-by: andyluo7 <andy.luo@amd.com>
This commit is contained in:
andyluo7 authored and GitHub committed 2026-10-04 16:03:21 +13:00
1 parent c150600896
commit 09d704907f
2 files changed
+136 -5

No files matched your search

@@ -0,0 +1,116 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from unittest.mock import Mock
import pytest
import torch
from tests.helpers.mark import hardware_test
from vllm_omni.utils import seeded_exponential
pytestmark = [pytest.mark.core_model]
@pytest.mark.cpu
@pytest.mark.parametrize("is_nvidia", [False, True], ids=["rocm", "cuda"])
def test_cuda_tensor_requires_nvidia_distribution_contract(monkeypatch, is_nvidia: bool) -> None:
monkeypatch.setattr(seeded_exponential.current_platform, "is_cuda", lambda: is_nvidia)
# HIP tensors expose the same Torch device predicate as NVIDIA tensors.
q = Mock(spec=torch.Tensor)
q.is_cuda = True
q.dtype = torch.float32
q.dim.return_value = 2
q.is_contiguous.return_value = True
generators = {0: object(), 1: object()}
assert seeded_exponential.batched_seeded_exponential_supported(q, generators) is is_nvidia
@pytest.mark.cpu
def test_shared_generator_keeps_ordered_torch_draws(monkeypatch) -> None:
monkeypatch.setattr(seeded_exponential.current_platform, "is_cuda", lambda: True)
q = Mock(spec=torch.Tensor)
q.is_cuda = True
q.dtype = torch.float32
q.dim.return_value = 2
q.is_contiguous.return_value = True
shared = object()
assert not seeded_exponential.batched_seeded_exponential_supported(q, {0: shared, 1: shared})
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@pytest.mark.parametrize("seeded_rows", [(0, 1, 2), (0, 2)], ids=["all_seeded", "mixed"])
@pytest.mark.parametrize("use_fp64_gumbel", [False, True])
def test_torch_fallback_preserves_samples_and_generator_state(monkeypatch, seeded_rows, use_fp64_gumbel) -> None:
from vllm.v1.sample.ops import topk_topp_sampler as sampler_ops
import vllm_omni.patch # noqa: F401
original = sampler_ops.random_sample.__wrapped__
monkeypatch.setattr(seeded_exponential.current_platform, "is_cuda", lambda: False)
def reject_kernel(*args, **kwargs):
raise AssertionError("the CUDA distribution kernel must not run on the fallback path")
monkeypatch.setattr(seeded_exponential, "fill_exponential_rows", reject_kernel)
probs = torch.arange(1, 258, device="cuda", dtype=torch.float32).expand(3, -1).contiguous()
probs /= probs.sum(dim=-1, keepdim=True)
expected_gens = {row: torch.Generator(device="cuda").manual_seed(42 + row) for row in seeded_rows}
actual_gens = {row: torch.Generator(device="cuda").manual_seed(42 + row) for row in seeded_rows}
expected_default = torch.cuda.get_rng_state()
actual_default = expected_default.clone()
try:
for _ in range(5):
torch.cuda.set_rng_state(expected_default)
# FP32 sampling divides probabilities in place. Give both paths
# the same untouched input on every step.
expected = original(probs.clone(), expected_gens, use_fp64_gumbel)
expected_default = torch.cuda.get_rng_state()
torch.cuda.set_rng_state(actual_default)
actual = sampler_ops.random_sample(probs.clone(), actual_gens, use_fp64_gumbel)
actual_default = torch.cuda.get_rng_state()
assert torch.equal(actual, expected)
assert torch.equal(actual_default, expected_default)
for row in seeded_rows:
assert torch.equal(actual_gens[row].get_state(), expected_gens[row].get_state())
finally:
torch.cuda.set_rng_state(expected_default)
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@pytest.mark.parametrize("rows", [None, [0, 2, 4]], ids=["grouped_codebooks", "selected_rows"])
def test_direct_fill_fallback_preserves_ordered_draws(monkeypatch, rows) -> None:
monkeypatch.setattr(seeded_exponential.current_platform, "is_cuda", lambda: False)
def reject_kernel(*args, **kwargs):
raise AssertionError("the NVIDIA kernel must not run for direct fallback callers")
monkeypatch.setattr(seeded_exponential, "torch_exponential_policy", reject_kernel)
expected = torch.ones(6, 257, device="cuda")
actual = expected.clone()
expected_gen = torch.Generator(device="cuda").manual_seed(71)
actual_gen = torch.Generator(device="cuda").manual_seed(71)
# A shared seeded generator and an unseeded request make loop order and
# default-state advancement observable, including grouped codebooks.
expected_gens = [expected_gen, None, expected_gen]
actual_gens = [actual_gen, None, actual_gen]
original_default = torch.cuda.get_rng_state()
expected_default = original_default.clone()
actual_default = original_default.clone()
try:
for _ in range(5):
torch.cuda.set_rng_state(expected_default)
targets = expected.reshape(3, -1) if rows is None else [expected[row] for row in rows]
for target, generator in zip(targets, expected_gens):
target.exponential_(generator=generator)
expected_default = torch.cuda.get_rng_state()
torch.cuda.set_rng_state(actual_default)
assert seeded_exponential.fill_exponential_rows(actual, actual_gens, rows) is actual
actual_default = torch.cuda.get_rng_state()
assert torch.equal(actual, expected)
assert torch.equal(actual_gen.get_state(), expected_gen.get_state())
assert torch.equal(actual_default, expected_default)
finally:
torch.cuda.set_rng_state(original_default)
+20 -5
View File
@@ -12,6 +12,7 @@ every value and every generator state afterwards matches the per-request loop.
""" """
import torch import torch
from vllm.platforms import current_platform
from vllm.triton_utils import tl, triton from vllm.triton_utils import tl, triton
@@ -73,10 +74,18 @@ def torch_exponential_policy(numel: int, device: torch.device) -> tuple[int, int
def fill_exponential_rows(out: torch.Tensor, generators: list, rows: list[int] | None = None) -> torch.Tensor: def fill_exponential_rows(out: torch.Tensor, generators: list, rows: list[int] | None = None) -> torch.Tensor:
"""Draw ``out[rows[i]].exponential_(generator=generators[i])`` for all ``i`` in one launch. """Draw ``out[rows[i]].exponential_(generator=generators[i])`` for all ``i`` in one launch.
``out`` is a contiguous float32 CUDA matrix; ``rows`` defaults to ``out`` is a contiguous float32 matrix. Without ``rows``, each generator
``range(len(generators))``. ``None`` generators use the default CUDA owns an equal contiguous slice of the flattened output (which may span
generator in order. Each generator is advanced exactly as torch would. several codebook rows). ``None`` generators use the default generator in
order. NVIDIA uses the batched CUDA kernel; other platforms retain
ordered Torch draws, including direct Higgs Audio callers.
""" """
if not current_platform.is_cuda():
targets = out.reshape(len(generators), -1) if rows is None else [out[row] for row in rows]
for target, generator in zip(targets, generators):
target.exponential_(generator=generator)
return out
numel = int(out.shape[-1]) if rows is not None else int(out.numel()) // len(generators) numel = int(out.shape[-1]) if rows is not None else int(out.numel()) // len(generators)
device = out.device device = out.device
threads, increment = torch_exponential_policy(numel, device) threads, increment = torch_exponential_policy(numel, device)
@@ -109,9 +118,15 @@ def fill_exponential_rows(out: torch.Tensor, generators: list, rows: list[int] |
def batched_seeded_exponential_supported(q: torch.Tensor, generators: dict) -> bool: def batched_seeded_exponential_supported(q: torch.Tensor, generators: dict) -> bool:
"""Whether ``fill_exponential_rows`` reproduces the per-row loop for ``q``.""" """Whether the NVIDIA CUDA kernel reproduces the per-row loop for ``q``.
ROCm tensors also report ``is_cuda``, but HIP uses a different distribution
implementation and cannot compile the CUDA libdevice transform above.
Keep those draws on Torch's original per-row path, including RNG state.
"""
return ( return (
len(generators) > 1 current_platform.is_cuda()
and len(generators) > 1
and q.is_cuda and q.is_cuda
and q.dtype == torch.float32 and q.dtype == torch.float32
and q.dim() == 2 and q.dim() == 2