mirror of
https://github.com/pytorch/pytorch.git
synced 2026-10-28 18:56:51 +08:00
Add a FlyDSL-based fused RMSNorm forward kernel as a native op override, on top
of the backend in the PR below.
### Supported cases
fp16, bf16 and fp32 tensors on gfx950, contiguous input and weight, 1-D `normalized_shape`, and
`weight` of shape `(N,)` with matching dtype and device. N-D inputs are logically flattened to
`(M, N)`. `eps` is passed through at runtime and does not participate in the cache key. The
kernel returns the forward result and the FP32 `rstd` that ATen's backward consumes.
### When FlyDSL acceleration kicks in
gfx950, non-copy-on-write contiguous input and weight, one normalized dimension, non-negative
`eps`, and:
| N | minimum rows M |
|---|---:|
| `4096 <= N < 8192` | 8192 |
| `8192 <= N < 16384` | 4096 |
| `16384 <= N <= 114688` | 2048 |
### Test plan:
- lintrunner on all changed files
- python test/python_native/test_flydsl_rmsnorm_fwd.py
- OPINFO_RESTRICT_TO_DSL=flydsl python test/test_ops.py -k rms_norm
### Performance methodology
Latency is measured with CUDA events after 10 warmup iterations and over 50 timed iterations. The
ATen baseline runs with FlyDSL disabled; the FlyDSL measurement invokes the same operator with the
override enabled. Every row below was confirmed to actually dispatch, by checking the compile
cache call count advanced, and its output and `rstd` were compared against the ATen baseline.
Each row is a single run. Repeating the smallest shape ten times spread its speedup over
1.16x-1.26x.
```python
def bench(fn, iters=50, warmup=10):
for _ in range(warmup):
fn()
torch.cuda.synchronize()
start = torch.cuda.Event(enable_timing=True)
end = torch.cuda.Event(enable_timing=True)
start.record()
for _ in range(iters):
fn()
end.record()
torch.cuda.synchronize()
return start.elapsed_time(end) / iters
fwd = lambda: torch.ops.aten._fused_rms_norm(x, [N], weight, eps)
with pn.flydsl.disabled():
aten_ms = bench(fwd)
flydsl_ms = bench(fwd)
speedup = aten_ms / flydsl_ms
```
### Performance
GPU: AMD 256-CU MI355X (gfx950). FlyDSL 0.3.0.
| dtype | M | N | ATen (us) | FlyDSL (us) | speedup |
|:---|---:|---:|---:|---:|---:|
| fp16 | 8192 | 4096 | 26.7 | 22.9 | 1.16x |
| fp16 | 4096 | 8192 | 30.1 | 22.6 | 1.33x |
| fp16 | 2048 | 16384 | 31.0 | 23.1 | 1.34x |
| fp16 | 2048 | 65536 | 173.2 | 112.3 | 1.54x |
| fp16 | 2048 | 114688 | 303.2 | 258.8 | 1.17x |
| fp16 | 8192 | 4097 | 96.7 | 26.4 | 3.66x |
| fp16 | 4096 | 8193 | 82.7 | 26.0 | 3.18x |
| fp16 | 2048 | 16385 | 91.9 | 25.9 | 3.55x |
| bf16 | 8192 | 4096 | 26.7 | 22.2 | 1.20x |
| bf16 | 2048 | 16384 | 30.9 | 22.4 | 1.38x |
| bf16 | 8192 | 4097 | 96.4 | 26.4 | 3.65x |
| bf16 | 2048 | 16385 | 73.7 | 25.8 | 2.86x |
| fp32 | 8192 | 4096 | 56.3 | 40.1 | 1.41x |
| fp32 | 4096 | 8192 | 58.5 | 40.7 | 1.44x |
| fp32 | 2048 | 16384 | 57.7 | 44.1 | 1.31x |
| fp32 | 2048 | 32768 | 154.1 | 105.5 | 1.46x |
| fp32 | 2048 | 65536 | 321.8 | 210.3 | 1.53x |
| fp32 | 2048 | 114688 | 564.7 | 483.8 | 1.17x |
| fp32 | 8192 | 4097 | 91.8 | 41.6 | 2.20x |
| fp32 | 4096 | 8193 | 82.5 | 42.3 | 1.95x |
| fp32 | 2048 | 16385 | 78.0 | 43.9 | 1.78x |
| fp32 | 2048 | 32769 | 212.7 | 105.0 | 2.03x |
Aligned shapes improve by 1.2x to 1.5x, with both ends of the band -- `N` = 4096 and
`N` = 114688 -- sitting near 1.2x. Off-by-one `N` improves by up to 3.7x.
Unsupported shapes retain ATen behavior.
Co-authored-by: lirui927 <ruili@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Pull Request resolved: https://github.com/pytorch/pytorch/pull/191447
Approved by: https://github.com/jgong5, https://github.com/slayton58
Co-authored-by: lirui927 <Rui.Li@amd.com>
222 lines
7.9 KiB
Python
222 lines
7.9 KiB
Python
# Owner(s): ["module: dsl-native-ops"]
|
|
|
|
import os
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
import torch
|
|
from torch._native import flydsl_utils
|
|
from torch._vendor.packaging.version import Version
|
|
from torch.testing._internal.common_utils import (
|
|
instantiate_parametrized_tests,
|
|
parametrize,
|
|
run_tests,
|
|
TestCase,
|
|
)
|
|
|
|
|
|
_QUERY_RAISES = object()
|
|
|
|
|
|
class TestFlyDSLArchResolution(TestCase):
|
|
"""What the kernel gets compiled for.
|
|
|
|
Compiling for the wrong arch is silent -- a wave64 reduction on a wave32
|
|
device produces wrong numbers rather than an error -- so each source of the
|
|
answer is pinned here.
|
|
"""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
flydsl_utils._resolve_rocm_arch.cache_clear()
|
|
flydsl_utils._get_flydsl_device_arch.cache_clear()
|
|
self.addCleanup(flydsl_utils._resolve_rocm_arch.cache_clear)
|
|
self.addCleanup(flydsl_utils._get_flydsl_device_arch.cache_clear)
|
|
|
|
def _resolve(self, *, flydsl_arch="", hsa="", props=_QUERY_RAISES):
|
|
"""Resolve with the environment and the device query both controlled.
|
|
|
|
``props=None`` means no CUDA device; the default means the query itself
|
|
raises.
|
|
"""
|
|
env = {"FLYDSL_GPU_ARCH": flydsl_arch, "HSA_OVERRIDE_GFX_VERSION": hsa}
|
|
query = (
|
|
RuntimeError("no device") if props is _QUERY_RAISES else None,
|
|
props,
|
|
)
|
|
with (
|
|
patch.dict(os.environ, env),
|
|
patch.object(torch.cuda, "is_available", return_value=props is not None),
|
|
patch.object(
|
|
torch.cuda,
|
|
"get_device_properties",
|
|
side_effect=query[0],
|
|
return_value=query[1],
|
|
),
|
|
):
|
|
return flydsl_utils._resolve_rocm_arch(0)
|
|
|
|
def test_explicit_arch_env_wins_and_is_stripped(self):
|
|
# Set on both, so this also pins the precedence over the HSA override.
|
|
self.assertEqual(
|
|
self._resolve(flydsl_arch="gfx950:sramecc+", hsa="9.0.10"), "gfx950"
|
|
)
|
|
|
|
def test_hsa_override_gfx_form_is_stripped(self):
|
|
self.assertEqual(self._resolve(hsa="gfx950:sramecc+"), "gfx950")
|
|
|
|
def test_hsa_override_stepping_is_hexadecimal(self):
|
|
# 9.0.10 is gfx90a, not gfx9010 -- the one rule here that is easy to
|
|
# get wrong by reading the format as decimal.
|
|
self.assertEqual(self._resolve(hsa="9.0.10"), "gfx90a")
|
|
|
|
@parametrize("hsa", ("9.0", "9.0.x", "not-a-version"))
|
|
def test_unusable_hsa_override_falls_back_to_the_device(self, hsa):
|
|
props = SimpleNamespace(gcnArchName="gfx942:xnack-")
|
|
self.assertEqual(self._resolve(hsa=hsa, props=props), "gfx942")
|
|
|
|
def test_device_properties_are_stripped(self):
|
|
props = SimpleNamespace(gcnArchName="gfx950:sramecc+:xnack-")
|
|
self.assertEqual(self._resolve(props=props), "gfx950")
|
|
|
|
def test_no_cuda_device_returns_none(self):
|
|
self.assertIsNone(self._resolve(props=None))
|
|
|
|
def test_device_query_failure_returns_none(self):
|
|
# get_device_properties raising must decline rather than propagate:
|
|
# this runs inside the dispatcher predicate.
|
|
self.assertIsNone(self._resolve())
|
|
|
|
def test_missing_gcn_arch_name_returns_none(self):
|
|
self.assertIsNone(self._resolve(props=SimpleNamespace()))
|
|
|
|
def test_resolution_is_cached_per_device(self):
|
|
props = SimpleNamespace(gcnArchName="gfx942")
|
|
self.assertEqual(self._resolve(props=props), "gfx942")
|
|
self.assertEqual(self._resolve(flydsl_arch="gfx950"), "gfx942")
|
|
flydsl_utils._resolve_rocm_arch.cache_clear()
|
|
self.assertEqual(self._resolve(flydsl_arch="gfx950"), "gfx950")
|
|
|
|
|
|
class TestFlyDSLSharedPredicates(TestCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
flydsl_utils._is_supported_arch.cache_clear()
|
|
self.addCleanup(flydsl_utils._is_supported_arch.cache_clear)
|
|
|
|
@parametrize(
|
|
"arch,supported_arches,expected",
|
|
(
|
|
("gfx950", ("gfx950",), True),
|
|
("gfx942", ("gfx950",), False),
|
|
("gfx942", ("gfx942", "gfx950"), True),
|
|
(None, ("gfx950",), False),
|
|
),
|
|
)
|
|
def test_is_supported_arch(self, arch, supported_arches, expected):
|
|
with patch.object(flydsl_utils, "_resolve_rocm_arch", return_value=arch):
|
|
self.assertEqual(
|
|
flydsl_utils._is_supported_arch(0, supported_arches), expected
|
|
)
|
|
|
|
def test_is_supported_arch_is_cached(self):
|
|
with patch.object(
|
|
flydsl_utils,
|
|
"_resolve_rocm_arch",
|
|
side_effect=("gfx950", "gfx942"),
|
|
) as resolve:
|
|
self.assertTrue(flydsl_utils._is_supported_arch(0, ("gfx950",)))
|
|
self.assertTrue(flydsl_utils._is_supported_arch(0, ("gfx950",)))
|
|
self.assertEqual(resolve.call_count, 1)
|
|
|
|
@parametrize(
|
|
"rows_m,n,itemsize,expected",
|
|
(
|
|
(2048, 114688, 4, True),
|
|
(16383, 65536, 4, True),
|
|
(16384, 65536, 4, False),
|
|
(16385, 65536, 4, False),
|
|
(1 << 31, 1, 1, False),
|
|
(1, 1 << 31, 1, False),
|
|
),
|
|
)
|
|
def test_fits_int32_buffer_span(self, rows_m, n, itemsize, expected):
|
|
self.assertEqual(
|
|
flydsl_utils._fits_int32_buffer_span(rows_m, n, itemsize), expected
|
|
)
|
|
|
|
|
|
class TestFlyDSLVersionGate(TestCase):
|
|
"""The gate that decides whether FlyDSL overrides register at all.
|
|
|
|
Every case has to clear the cache twice: ``_version_is_ok`` is
|
|
``functools.cache``d, so a stale verdict would leak into the next case and
|
|
a real verdict from this machine's install would leak into the first.
|
|
"""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
flydsl_utils._version_is_ok.cache_clear()
|
|
self.addCleanup(flydsl_utils._version_is_ok.cache_clear)
|
|
|
|
def _with_version(self, version):
|
|
return patch.object(
|
|
flydsl_utils,
|
|
"_check_runtime_available",
|
|
return_value=(True, version),
|
|
)
|
|
|
|
@parametrize("version", ("0.3.0", "0.3.1", "0.3.5", "0.3.0.post1"))
|
|
def test_stable_supported_releases_are_accepted(self, version):
|
|
with self._with_version(Version(version)):
|
|
self.assertTrue(flydsl_utils._version_is_ok())
|
|
|
|
@parametrize(
|
|
"version",
|
|
("0.2.9", "0.3.0.dev765", "0.3.0rc1", "0.4.0", "1.3.0"),
|
|
)
|
|
def test_other_versions_are_rejected(self, version):
|
|
with self._with_version(Version(version)):
|
|
self.assertFalse(flydsl_utils._version_is_ok())
|
|
|
|
def test_missing_version_is_rejected(self):
|
|
with (
|
|
self._with_version(None),
|
|
self.assertLogs("torch._native.flydsl_utils", level="INFO") as logs,
|
|
):
|
|
self.assertFalse(flydsl_utils._version_is_ok())
|
|
|
|
self.assertIn("version None is not supported", "\n".join(logs.output))
|
|
|
|
def test_unsupported_version_reports_the_version(self):
|
|
with (
|
|
self._with_version(Version("0.4.0")),
|
|
self.assertLogs("torch._native.flydsl_utils", level="INFO") as logs,
|
|
):
|
|
self.assertFalse(flydsl_utils._version_is_ok())
|
|
|
|
self.assertIn("0.4.0 is not supported", "\n".join(logs.output))
|
|
|
|
def test_skip_check_overrides_unsupported_version(self):
|
|
with (
|
|
self._with_version(Version("0.4.0")),
|
|
patch.object(flydsl_utils, "check_native_version_skip", return_value=True),
|
|
):
|
|
self.assertTrue(flydsl_utils._version_is_ok())
|
|
|
|
def test_skip_check_overrides_missing_version(self):
|
|
with (
|
|
self._with_version(None),
|
|
patch.object(flydsl_utils, "check_native_version_skip", return_value=True),
|
|
):
|
|
self.assertTrue(flydsl_utils._version_is_ok())
|
|
|
|
|
|
instantiate_parametrized_tests(TestFlyDSLArchResolution)
|
|
instantiate_parametrized_tests(TestFlyDSLSharedPredicates)
|
|
instantiate_parametrized_tests(TestFlyDSLVersionGate)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run_tests()
|