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
from vllm.platforms import current_platform
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:
"""Draw ``out[rows[i]].exponential_(generator=generators[i])`` for all ``i`` in one launch.
``out`` is a contiguous float32 CUDA matrix; ``rows`` defaults to
``range(len(generators))``. ``None`` generators use the default CUDA
generator in order. Each generator is advanced exactly as torch would.
``out`` is a contiguous float32 matrix. Without ``rows``, each generator
owns an equal contiguous slice of the flattened output (which may span
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)
device = out.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:
"""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 (
len(generators) > 1
current_platform.is_cuda()
and len(generators) > 1
and q.is_cuda
and q.dtype == torch.float32
and q.dim() == 2