mirror of
https://github.com/pytorch/pytorch.git
synced 2026-10-28 18:56:51 +08:00
This reverts commit e506c0307c.
Reverted https://github.com/pytorch/pytorch/pull/200800 on behalf of https://github.com/meta-codesync due to Diff reverted internally ([comment](https://github.com/pytorch/pytorch/pull/200800#issuecomment-6459836373))
468 lines
17 KiB
Python
468 lines
17 KiB
Python
# Owner(s): ["module: dsl-native-ops"]
|
|
|
|
import unittest
|
|
|
|
import torch
|
|
import torch.backends.python_native as pn
|
|
from torch._native import flydsl_utils as fu
|
|
from torch._native.ops.topk.flydsl_impl import (
|
|
_REGISTER_KS as _impl_register_ks,
|
|
_SUPPORTED_ARCHES,
|
|
)
|
|
from torch.testing import make_tensor
|
|
from torch.testing._internal.common_cuda import TEST_CUDA
|
|
from torch.testing._internal.common_utils import (
|
|
instantiate_parametrized_tests,
|
|
parametrize,
|
|
run_tests,
|
|
skipIfNoFlyDSL,
|
|
TestCase,
|
|
)
|
|
|
|
|
|
_REGISTER_KS = tuple(sorted(_impl_register_ks))
|
|
# Radix sort-length padding edges; baseline (K, N) correctness lives in OpInfo.
|
|
_RADIX_PADDING_KS = (65, 129, 513)
|
|
# One K per radix gate row, plus the register gate, to cover each N ceiling.
|
|
_GATE_MAX_N_KS = (max(_REGISTER_KS), 64, 257, 384, 1024)
|
|
|
|
|
|
def _gate_n_bounds(k: int) -> tuple[int, int]:
|
|
from torch._native.ops.topk.flydsl_impl import _radix_n_range, _REGISTER_N_BOUNDS
|
|
|
|
if k in _REGISTER_KS:
|
|
return _REGISTER_N_BOUNDS
|
|
n_range = _radix_n_range(k)
|
|
if n_range is None:
|
|
raise AssertionError(f"missing radix gate for K={k}")
|
|
return n_range
|
|
|
|
|
|
def _test_n(k: int) -> int:
|
|
return _gate_n_bounds(k)[0]
|
|
|
|
|
|
def _test_n_max(k: int) -> int:
|
|
return _gate_n_bounds(k)[1]
|
|
|
|
|
|
def _special_cols(n: int) -> tuple[int, ...]:
|
|
return (0, n // 3 + 1, n // 2, 2 * n // 3 + 2, n - 1)
|
|
|
|
|
|
def _expected_kernel(k: int, n: int) -> str:
|
|
from torch._native.ops.topk.flydsl_impl import _kernel_for
|
|
|
|
kernel = _kernel_for(k, n)
|
|
if kernel is None:
|
|
raise AssertionError(f"K={k} N={n} is outside the FlyDSL gate")
|
|
return kernel
|
|
|
|
|
|
def _test_m(device_index: int | None = None) -> int:
|
|
from torch._native.ops.topk.flydsl_impl import _min_rows_for_full_wave
|
|
|
|
if device_index is None:
|
|
device_index = torch.cuda.current_device()
|
|
return max(256, _min_rows_for_full_wave(device_index))
|
|
|
|
|
|
@unittest.skipUnless(TEST_CUDA, "CUDA required")
|
|
@skipIfNoFlyDSL
|
|
@unittest.skipIf(
|
|
torch.version.hip is None
|
|
or not fu._is_supported_arch(
|
|
torch.cuda.current_device() if torch.cuda.is_available() else 0,
|
|
_SUPPORTED_ARCHES,
|
|
),
|
|
"FlyDSL TopK override requires gfx950",
|
|
)
|
|
class TestFlyDSLTopK(TestCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
from torch._native.ops.topk.flydsl_kernels import clear_topk_cache
|
|
|
|
clear_topk_cache()
|
|
|
|
def _make_input(self, *, shape):
|
|
torch.manual_seed(0)
|
|
return make_tensor(shape, device="cuda", dtype=torch.float32)
|
|
|
|
def _assert_no_flydsl_compiles(self) -> None:
|
|
from torch._native.ops.topk.flydsl_kernels import topk_cache_info
|
|
|
|
self.assertEqual(topk_cache_info().misses, 0)
|
|
|
|
def _assert_topk_matches_aten(self, x: torch.Tensor, k: int) -> None:
|
|
with pn.flydsl.disabled():
|
|
ref_v, _ = torch.topk(x, k, dim=-1)
|
|
got_v, got_i = torch.topk(x, k, dim=-1)
|
|
self.assertEqual(got_v, ref_v)
|
|
self.assertEqual(torch.gather(x, -1, got_i), got_v)
|
|
if k >= 2:
|
|
self.assertTrue(
|
|
(got_v[..., :-1] >= got_v[..., 1:]).all(),
|
|
"output is not descending",
|
|
)
|
|
|
|
@parametrize("k", _RADIX_PADDING_KS)
|
|
def test_radix_sort_length_padding(self, k: int):
|
|
torch.manual_seed(0)
|
|
x = make_tensor((_test_m(), _test_n(k)), device="cuda", dtype=torch.float32)
|
|
self._assert_topk_matches_aten(x, k)
|
|
|
|
def test_register_correctness_with_odd_rows(self):
|
|
# Register kernel packs two rows per CTA; odd M leaves a tail
|
|
# block where in_bounds is false for one row slot (row_safe falls back
|
|
# to row 0 for loads only; writes stay gated on in_bounds).
|
|
k = 8
|
|
n = _test_n(k)
|
|
self.assertEqual(_expected_kernel(k, n), "register")
|
|
rows = _test_m() | 1
|
|
x = make_tensor((rows, n), device="cuda", dtype=torch.float32)
|
|
self._assert_topk_matches_aten(x, k)
|
|
|
|
def test_3d_input(self):
|
|
from torch._native.ops.topk.flydsl_kernels import topk_cache_info
|
|
|
|
torch.manual_seed(3)
|
|
k = 704
|
|
n = _test_n(k)
|
|
rows = (_test_m() + 3) // 4
|
|
x = make_tensor((4, rows, n), device="cuda", dtype=torch.float32)
|
|
self._assert_topk_matches_aten(x, k)
|
|
got_v, _ = torch.topk(x, k, dim=-1)
|
|
self.assertEqual(topk_cache_info("radix").misses, 1)
|
|
self.assertEqual(got_v.shape, (4, rows, k))
|
|
|
|
def test_correctness_with_extreme_values(self):
|
|
k = 8
|
|
torch.manual_seed(2)
|
|
n = _test_n(k)
|
|
x = make_tensor((_test_m(), n), device="cuda", dtype=torch.float32)
|
|
for col, val in zip(
|
|
_special_cols(n), (float("inf"), float("-inf"), 1e38, -1e38, float("inf"))
|
|
):
|
|
x[:, col] = val
|
|
self._assert_topk_matches_aten(x, k)
|
|
|
|
def test_nan_count_and_finite_subsequence_match_aten(self):
|
|
k = 512
|
|
torch.manual_seed(10)
|
|
m = _test_m()
|
|
n = _test_n(k)
|
|
x = make_tensor((m, n), device="cuda", dtype=torch.float32)
|
|
x_bits = x.view(torch.int32)
|
|
cols = _special_cols(n)
|
|
x_bits[:, cols[0]] = 0x7FC12345
|
|
x_bits[:, cols[1]] = 0xFFC54321 - (1 << 32)
|
|
x[:, cols[2]] = float("inf")
|
|
x[:, cols[3]] = float("-inf")
|
|
x_bits[:, cols[4]] = 0x7FC12345
|
|
with pn.flydsl.disabled():
|
|
ref_v, _ = torch.topk(x, k, dim=-1)
|
|
got_v, got_i = torch.topk(x, k, dim=-1)
|
|
gathered = torch.gather(x, -1, got_i)
|
|
self.assertEqual(gathered.view(torch.int32), got_v.view(torch.int32))
|
|
self.assertEqual(got_v.isnan().sum(dim=-1), ref_v.isnan().sum(dim=-1))
|
|
ref_finite = ref_v.masked_select(~ref_v.isnan()).reshape(m, -1)
|
|
got_finite = got_v.masked_select(~got_v.isnan()).reshape(m, -1)
|
|
self.assertEqual(got_finite, ref_finite)
|
|
|
|
def test_out_variant(self):
|
|
k = 704
|
|
m = _test_m()
|
|
x = make_tensor((m, _test_n(k)), device="cuda", dtype=torch.float32)
|
|
with pn.flydsl.disabled():
|
|
ref_v, _ = torch.topk(x, k, dim=-1)
|
|
|
|
out_v = torch.empty(m, k, dtype=torch.float32, device="cuda")
|
|
out_i = torch.empty(m, k, dtype=torch.int64, device="cuda")
|
|
got_v, got_i = torch.topk(x, k, dim=-1, out=(out_v, out_i))
|
|
self.assertIs(got_v, out_v)
|
|
self.assertIs(got_i, out_i)
|
|
self.assertEqual(got_v, ref_v)
|
|
self.assertEqual(torch.gather(x, -1, got_i), got_v)
|
|
|
|
def test_cow_out_dispatches_and_materializes(self):
|
|
from torch._native.ops.topk.flydsl_kernels import topk_cache_info
|
|
|
|
k = 8
|
|
m = _test_m()
|
|
n = _test_n(k)
|
|
x = self._make_input(shape=(m, n))
|
|
with pn.flydsl.disabled():
|
|
ref_v, _ = torch.topk(x, k, dim=-1)
|
|
|
|
values_base = torch.full((m, k), -1.0, device="cuda")
|
|
indices_base = torch.full((m, k), -1, dtype=torch.int64, device="cuda")
|
|
values = values_base._lazy_clone()
|
|
indices = indices_base._lazy_clone()
|
|
self.assertTrue(torch._C._is_cow_tensor(values))
|
|
self.assertTrue(torch._C._is_cow_tensor(indices))
|
|
|
|
got_v, got_i = torch.topk(x, k, dim=-1, out=(values, indices))
|
|
|
|
self.assertEqual(topk_cache_info(_expected_kernel(k, n)).misses, 1)
|
|
self.assertFalse(torch._C._is_cow_tensor(values))
|
|
self.assertFalse(torch._C._is_cow_tensor(indices))
|
|
self.assertEqual(values_base, torch.full_like(values_base, -1.0))
|
|
self.assertEqual(indices_base, torch.full_like(indices_base, -1))
|
|
self.assertIs(got_v, values)
|
|
self.assertIs(got_i, indices)
|
|
self.assertEqual(got_v, ref_v)
|
|
self.assertEqual(torch.gather(x, -1, got_i), got_v)
|
|
|
|
def test_out_values_overlapping_input(self):
|
|
k = 8
|
|
m = 16 * _test_m()
|
|
n = _test_n(k)
|
|
x = self._make_input(shape=(m, n))
|
|
original = x.clone()
|
|
with pn.flydsl.disabled():
|
|
ref_v, _ = torch.topk(original, k, dim=-1)
|
|
|
|
values = x.flatten()[m * (n - k) :].view(m, k)
|
|
indices = torch.empty((m, k), device="cuda", dtype=torch.int64)
|
|
self.assertTrue(torch._C._overlaps(x, values))
|
|
|
|
got_v, got_i = torch.topk(x, k, dim=-1, out=(values, indices))
|
|
self.assertIs(got_v, values)
|
|
self.assertIs(got_i, indices)
|
|
self.assertEqual(got_v, ref_v)
|
|
self.assertEqual(torch.gather(original, -1, got_i), got_v)
|
|
|
|
def test_cow_input_dispatches_and_remains_cow(self):
|
|
k = 8
|
|
x = self._make_input(shape=(_test_m(), _test_n(k)))
|
|
with pn.flydsl.disabled():
|
|
ref_v, _ = torch.topk(x, k, dim=-1)
|
|
|
|
x = x._lazy_clone()
|
|
self.assertTrue(torch._C._is_cow_tensor(x))
|
|
data_ptr = x.const_data_ptr()
|
|
|
|
got_v, got_i = torch.topk(x, k, dim=-1)
|
|
|
|
self.assertTrue(torch._C._is_cow_tensor(x))
|
|
self.assertEqual(x.const_data_ptr(), data_ptr)
|
|
self.assertEqual(got_v, ref_v)
|
|
self.assertEqual(torch.gather(x, -1, got_i), got_v)
|
|
|
|
def test_topk_uses_cache(self):
|
|
n = _test_n(8)
|
|
m = _test_m()
|
|
x = self._make_input(shape=(m, n))
|
|
with pn.flydsl.disabled():
|
|
ref = torch.topk(x, 8, dim=-1)
|
|
|
|
got = torch.topk(x, 8, dim=-1)
|
|
self.assertEqual(got.values, ref.values)
|
|
self.assertEqual(got.indices, ref.indices)
|
|
|
|
x3 = self._make_input(shape=(2, (m + 1) // 2, n))
|
|
with pn.flydsl.disabled():
|
|
ref3 = torch.topk(x3, 8, dim=-1)
|
|
got3 = torch.topk(x3, 8, dim=-1)
|
|
self.assertEqual(got3.values, ref3.values)
|
|
self.assertEqual(got3.indices, ref3.indices)
|
|
|
|
from torch._native.ops.topk.flydsl_kernels import topk_cache_info
|
|
|
|
info = topk_cache_info("register")
|
|
self.assertEqual(info.misses, 1)
|
|
self.assertGreaterEqual(info.hits, 1)
|
|
self.assertEqual(info.currsize, 1)
|
|
|
|
@parametrize("k,n", ((32, 4096), (8, 1537), (64, 4096), (320, 8192)))
|
|
def test_unsupported_configuration_falls_through_without_compiling(
|
|
self, k: int, n: int
|
|
):
|
|
x = self._make_input(shape=(_test_m(), n))
|
|
torch.topk(x, k, dim=-1)
|
|
self._assert_no_flydsl_compiles()
|
|
|
|
@parametrize(
|
|
"case",
|
|
(
|
|
"dtype",
|
|
"smallest",
|
|
"unsorted",
|
|
"non_last_dim",
|
|
"noncontiguous",
|
|
"too_few_rows",
|
|
),
|
|
)
|
|
def test_unsupported_case_falls_through_without_compiling(self, case: str):
|
|
rows = _test_m()
|
|
if case == "too_few_rows":
|
|
from torch._native.ops.topk.flydsl_impl import _min_rows_for_full_wave
|
|
|
|
rows = _min_rows_for_full_wave(torch.cuda.current_device()) - 1
|
|
n = _test_n(8)
|
|
shape = (n, rows) if case == "noncontiguous" else (rows, n)
|
|
dtype = torch.float16 if case == "dtype" else torch.float32
|
|
x = make_tensor(shape, device="cuda", dtype=dtype)
|
|
if case == "noncontiguous":
|
|
x = x.transpose(0, 1)
|
|
dim = 0 if case == "non_last_dim" else -1
|
|
largest = case != "smallest"
|
|
sorted_ = case != "unsorted"
|
|
torch.topk(x, 8, dim=dim, largest=largest, sorted=sorted_)
|
|
self._assert_no_flydsl_compiles()
|
|
|
|
def test_radix_non_multiple_of_four_n(self):
|
|
torch.manual_seed(11)
|
|
x = make_tensor((_test_m(), 32769), device="cuda", dtype=torch.float32)
|
|
self._assert_topk_matches_aten(x, 512)
|
|
|
|
@parametrize(
|
|
"kernel,k,n",
|
|
(
|
|
("register", 8, 1024),
|
|
("radix", 512, 32768),
|
|
),
|
|
)
|
|
def test_misaligned_base_matches_aten(self, kernel: str, k: int, n: int):
|
|
from torch._native.ops.topk.flydsl_kernels import topk_cache_info
|
|
|
|
torch.manual_seed(12)
|
|
m = _test_m()
|
|
self.assertEqual(_expected_kernel(k, n), kernel)
|
|
buf = torch.randn(m * n + 1, device="cuda", dtype=torch.float32)
|
|
x = buf[1:].reshape(m, n)
|
|
if not x.is_contiguous():
|
|
raise AssertionError("expected contiguous misaligned-base view")
|
|
if x.data_ptr() % 16 == 0:
|
|
raise AssertionError("expected misaligned base pointer")
|
|
self._assert_topk_matches_aten(x, k)
|
|
self.assertEqual(topk_cache_info(kernel).misses, 1)
|
|
|
|
def test_noncontiguous_out_dispatches_and_matches_aten(self):
|
|
k = 8
|
|
m = _test_m()
|
|
x = self._make_input(shape=(m, _test_n(k)))
|
|
with pn.flydsl.disabled():
|
|
ref_v, _ = torch.topk(x, k, dim=-1)
|
|
|
|
values = torch.empty((m, 2 * k), device="cuda", dtype=torch.float32)[:, ::2]
|
|
indices = torch.empty((m, 2 * k), device="cuda", dtype=torch.int64)[:, ::2]
|
|
self.assertFalse(values.is_contiguous())
|
|
self.assertFalse(indices.is_contiguous())
|
|
|
|
got_v, got_i = torch.topk(x, k, dim=-1, out=(values, indices))
|
|
self.assertIs(got_v, values)
|
|
self.assertIs(got_i, indices)
|
|
self.assertEqual(got_v, ref_v)
|
|
self.assertEqual(torch.gather(x, -1, got_i), got_v)
|
|
|
|
@parametrize("k", (64, 257, 704, 1023))
|
|
def test_deterministic_mode_matches_aten_with_heavy_ties(self, k: int):
|
|
from torch._native.ops.topk.flydsl_kernels import topk_cache_info
|
|
|
|
torch.manual_seed(6)
|
|
n = _test_n(k)
|
|
self.assertEqual(_expected_kernel(k, n), "radix")
|
|
x = torch.randint(0, 4, (_test_m(), n), device="cuda", dtype=torch.float32)
|
|
|
|
prior = torch.are_deterministic_algorithms_enabled()
|
|
try:
|
|
torch.use_deterministic_algorithms(True)
|
|
with pn.flydsl.disabled():
|
|
ref_v, ref_i = torch.topk(x, k, dim=-1)
|
|
v1, i1 = torch.topk(x, k, dim=-1)
|
|
v2, i2 = torch.topk(x, k, dim=-1)
|
|
finally:
|
|
torch.use_deterministic_algorithms(prior)
|
|
|
|
self.assertEqual(topk_cache_info("radix").misses, 1)
|
|
self.assertEqual(v1, v2)
|
|
self.assertEqual(i1, i2)
|
|
self.assertEqual(v1, ref_v)
|
|
self.assertEqual(i1, ref_i)
|
|
|
|
@parametrize("k", _REGISTER_KS)
|
|
def test_register_tie_order_is_value_desc_index_asc(self, k: int):
|
|
torch.manual_seed(7)
|
|
n = _test_n(k)
|
|
self.assertEqual(_expected_kernel(k, n), "register")
|
|
x = torch.randint(0, 3, (_test_m(), n), device="cuda", dtype=torch.float32)
|
|
expected_i = torch.argsort(x, dim=-1, descending=True, stable=True)[:, :k]
|
|
|
|
prior = torch.are_deterministic_algorithms_enabled()
|
|
try:
|
|
torch.use_deterministic_algorithms(True)
|
|
v1, i1 = torch.topk(x, k, dim=-1)
|
|
v2, i2 = torch.topk(x, k, dim=-1)
|
|
finally:
|
|
torch.use_deterministic_algorithms(prior)
|
|
|
|
self.assertEqual(i1, i2)
|
|
self.assertEqual(v1, v2)
|
|
self.assertEqual(i1, expected_i)
|
|
self.assertEqual(torch.gather(x, -1, i1), v1)
|
|
|
|
@parametrize("k", _GATE_MAX_N_KS)
|
|
def test_correctness_at_gate_n_ceiling(self, k: int):
|
|
torch.manual_seed(12)
|
|
x = make_tensor((_test_m(), _test_n_max(k)), device="cuda", dtype=torch.float32)
|
|
self._assert_topk_matches_aten(x, k)
|
|
|
|
@parametrize("k", (8, 704))
|
|
def test_autograd_passes_through(self, k: int):
|
|
x = make_tensor(
|
|
(_test_m(), _test_n(k)),
|
|
device="cuda",
|
|
dtype=torch.float32,
|
|
requires_grad=True,
|
|
)
|
|
values, indices = torch.topk(x, k, dim=-1)
|
|
values.sum().backward()
|
|
self.assertIsNotNone(x.grad)
|
|
expected = torch.zeros_like(x)
|
|
expected.scatter_(-1, indices, 1.0)
|
|
self.assertEqual(x.grad, expected)
|
|
|
|
@unittest.skipIf(
|
|
torch.cuda.device_count() < 2,
|
|
"requires at least 2 visible CUDA devices",
|
|
)
|
|
def test_each_device_gets_its_own_specialization(self):
|
|
k = 704
|
|
if not all(fu._is_supported_arch(index, _SUPPORTED_ARCHES) for index in (0, 1)):
|
|
self.skipTest("requires two gfx950 devices")
|
|
|
|
old_device = torch.cuda.current_device()
|
|
try:
|
|
torch.cuda.set_device(0)
|
|
for index in (0, 1):
|
|
device = torch.device("cuda", index)
|
|
x = make_tensor(
|
|
(_test_m(index), _test_n(k)),
|
|
device=device,
|
|
dtype=torch.float32,
|
|
)
|
|
with pn.flydsl.disabled():
|
|
ref_v, _ = torch.topk(x, k, dim=-1)
|
|
got_v, got_i = torch.topk(x, k, dim=-1)
|
|
|
|
self.assertEqual(torch.cuda.current_device(), 0)
|
|
self.assertEqual(got_v.device, device)
|
|
self.assertEqual(got_v, ref_v)
|
|
self.assertEqual(torch.gather(x, -1, got_i), got_v)
|
|
|
|
from torch._native.ops.topk.flydsl_kernels import topk_cache_info
|
|
|
|
info = topk_cache_info()
|
|
self.assertEqual(info.misses, 2)
|
|
self.assertEqual(info.currsize, 2)
|
|
finally:
|
|
torch.cuda.set_device(old_device)
|
|
|
|
|
|
instantiate_parametrized_tests(TestFlyDSLTopK)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run_tests()
|