Files
pytorch/test/python_native/test_kernel_general.py
Simon Layton fa62a650d0 [_native][8/8] Serve the ordered sum/prod overrides with the shared kernel's inner-tree order (#195062)
Human Notes:

We finally get there!

Cut over the existing inner-tree implementations to the new stack.

Codex:

Route the existing ordered `aten::sum` and `aten::prod` overrides through the
shared fixed-DAG kernel. Eligibility, TensorIterator canonicalization,
keepdim, and direct/out overload handling remain in the adapter. Unsupported
dtypes and geometry continue to fall through to ATen.

Nonzero input row strides and unit-stride outputs use the shared kernel.
Noncompact rows use a dynamic leading stride; compactness and alignment key
the plan, with underaligned rows selecting the bit-neutral unstaged path.
Strided outputs retain the legacy same-order implementation.

H100 uses a targeted native-suite list; B200's existing dynamic discovery
covers the same suites without scheduling them twice. Labeling covers both
implementation and test-only changes. End-to-end tests assert both reference
bits and that the ordered route actually ran.

Test Plan:

```
spin quicklint
PYTORCH_SUM_INNER_TREE=1 python test/python_native/test_sum_cutedsl.py
python test/python_native/test_sum_inner_tree_plan.py
python test/python_native/test_inner_tree_order.py
```

Authored with assistance from an AI coding agent.
Pull Request resolved: https://github.com/pytorch/pytorch/pull/195062
Approved by: https://github.com/ngimel
2026-10-15 23:11:50 +00:00

114 lines
4.9 KiB
Python

# Owner(s): ["module: dsl-native-ops"]
# Smoke-test K0 compilation and dispatch on a middle-dimension reduction, which forces
# the general path. Override OpInfo suites provide full numerical coverage.
import pathlib
import re
import sys
import unittest
import torch
from torch.testing._internal.common_cuda import SM90OrLater, TEST_CUDA
from torch.testing._internal.common_utils import run_tests, TEST_CUTEDSL, TestCase
# Guard before importing cutlass-dependent kernels to avoid collection errors.
if not TEST_CUTEDSL:
sys.stderr.write("CuTeDSL not available\n")
if __name__ == "__main__":
sys.exit(0)
raise unittest.SkipTest("CuTeDSL not available")
import cutlass
from torch._native.ops import reductions
from torch._native.ops.reductions import (
kernel_general as kg,
kernel_xcta as xc,
traits as T,
)
@unittest.skipUnless(TEST_CUDA, "CUDA required")
@unittest.skipUnless(SM90OrLater, "Hopper+ required")
class TestKernelGeneral(TestCase):
def test_reduce_dim_general_path(self):
x = torch.randn(8, 16, 32, device="cuda")
out = kg.reduce_dim(
T.SumOps(acc=cutlass.Float32), "smoke", x, [1], torch.float32
)
torch.testing.assert_close(out, x.sum(dim=1), atol=1e-2, rtol=1e-2)
def test_two_stage_row_ragged_split(self):
# A prime row requires ragged stage-1 chunks that stop at row boundaries.
x = torch.randn(8, 65537, device="cuda")
(out,) = kg._two_stage_row(
T.SumOps(acc=cutlass.Float32), "smoke_rag", x, [torch.float32], 1
)
torch.testing.assert_close(out, x.sum(dim=1), atol=1e-1, rtol=1e-3)
def test_two_stage_row_index_is_global(self):
# Chunk-local reductions must report global columns and preserve first-wins ties.
x = torch.zeros(8, 65537, device="cuda")
x[:, 40000] = 1.0
x[:, 50000] = 1.0 # tie with the above -> the lower column must win
(idx,) = kg._two_stage_row(
T.ArgMaxOps(acc=cutlass.Float32), "smoke_ragidx", x, [torch.int32], 1
)
self.assertEqual(idx, torch.full((8,), 40000, device="cuda", dtype=torch.int32))
def test_family_has_exactly_one_cute_kernel(self):
# Assert every axis still shares one kernel. Glob files and match qualified or
# annotated decorators so new drivers and spellings cannot evade the check.
# Runtime introspection cannot distinguish @cute.kernel from @cute.jit wrappers.
root = pathlib.Path(reductions.__file__).parent
deco = re.compile(r"@(?:\w+\.)*cute\.kernel\b")
found = [
f"{path.name}:{i}"
for path in sorted(root.glob("*.py"))
for i, line in enumerate(path.read_text().splitlines(), 1)
if deco.match(line.strip())
]
self.assertEqual(
len(found), 1, f"expected one kernel in the family, got {found}"
)
self.assertTrue(found[0].startswith("tile.py"), f"the body moved: {found}")
def test_internal_invariants_raise(self):
# Each invariant must raise explicitly because python -O strips asserts. Exercise every
# check on its documented invalid input.
trait = T.SumOps(acc=cutlass.Float32)
# reduce-all needs a flat view, so a transposed input has to be refused, not reshaped.
xt = torch.randn(64, 128, device="cuda").t()
with self.assertRaisesRegex(AssertionError, "contiguous CUDA input"):
kg._reduce_all(trait, "inv", xt, [torch.float32], 1, 128, 4)
# The general path needs a CUDA input; a CPU tensor is refused, not silently run.
with self.assertRaisesRegex(AssertionError, "need a CUDA input"):
kg._reduce(trait, "inv", torch.randn(8, 8), [1], [torch.float32], 1)
# The magic-division decode is exact only below 2^31.
with self.assertRaisesRegex(AssertionError, "count and num_o"):
kg.ReduceBlock(
trait, count=2**31, num_o=1, red_pairs=((2**31, 1),), kept_pairs=()
)
# A missing reduced run would index vals[-1]; empty kept runs remain valid.
with self.assertRaisesRegex(AssertionError, "at least one reduced run"):
kg.ReduceBlock(trait, count=1, num_o=1, red_pairs=(), kept_pairs=())
# The reshaping cross-CTA path also requires contiguous CUDA input.
with self.assertRaisesRegex(AssertionError, "CUDA"):
xc.reduce_row_xcta(trait, "inv_xcta", xt, torch.float32)
def test_no_suppressed_asserts_survive(self):
# Reject suppressed asserts directly because lint itself can be silenced.
root = pathlib.Path(reductions.__file__).parent
offenders = [
f"{path.name}:{i}"
for path in sorted(root.glob("*.py"))
for i, line in enumerate(path.read_text().splitlines(), 1)
if "noqa: S101" in line
]
self.assertEqual(offenders, [])
if __name__ == "__main__":
run_tests()