fix: gate _grouped_mm dispatch patch on HIP < 7.13

AMD fixed the gfx1200 null HIP kernel in ROCm 7.13 (torch 2.11+).
Users on the new wheel now get the real GPU _grouped_mm kernel for
MoE workloads instead of the Python mm fallback.

Changes:
- worker.py: add _hip_ver_at_least() helper; wrap full _grouped_mm
  patch in `if not _hip_ver_at_least(7, 13):` with else branch that
  logs the skip reason; update section-1f comment to document the fix
- test_rocm_support.py: add 5 tests covering the helper definition,
  the (7, 13) gate expression, the else branch, the skip log message,
  and the AMD-format version string parsing (.split(".")[:2])

Verified: torch==2.11.0+rocm7.13.0 — 3D batch and grouped (offs)
variants both succeed; null crash only present on rocm7.12 and earlier.
This commit is contained in:
LeoBorcherding 2026-05-15 14:52:14 -05:00
commit b33a90ee68
2 changed files with 135 additions and 72 deletions

View file

@ -1291,7 +1291,7 @@ def run_training_process(
pass
# ── 1f. Windows ROCm runtime patches ──
# torch._grouped_mm has a null HIP kernel on gfx1200 (ROCm 7.12 Windows),
# torch._grouped_mm has a null HIP kernel on gfx1200 (ROCm 7.12 Windows),
# causing 0xC0000005 (access violation) during training.
#
# Root cause: the JitDecomp autograd decomposition system (NOT torch.compile)
@ -1300,9 +1300,12 @@ def run_training_process(
# JitDecomp, so we must also override the CUDA dispatch key for _grouped_mm
# with a safe Python fallback.
#
# Verified on torch==2.10.0+rocm7.12.0:
# torch.library.Library("aten","IMPL").impl("_grouped_mm", fn, "CUDA")
# correctly overrides the HIP kernel and the call succeeds.
# Fixed in AMD's wheel: torch==2.11.0+rocm7.13.0 — the 3-D batch and grouped
# (with offs) variants of _grouped_mm now have working HIP kernels on gfx1200.
# We gate the dispatch override on HIP < 7.13 so users on the fixed wheel get
# the real GPU kernel rather than our Python fallback.
#
# Verified: null on torch==2.10.0+rocm7.12.0; fixed on torch==2.11.0+rocm7.13.0.
#
# Schema: _grouped_mm(Tensor self, Tensor mat2, Tensor? offs=None,
# Tensor? bias=None, ScalarType? out_dtype=None) -> Tensor
@ -1362,76 +1365,101 @@ def run_training_process(
_bnb_rocm_ver,
)
# Patch _grouped_mm CUDA dispatch with a safe Python mm fallback.
try:
import warnings as _warnings
_gm_lib = _torch_for_rocm.library.Library("aten", "IMPL")
def _grouped_mm_safe_impl(
self, mat2, offs = None, bias = None, out_dtype = None
):
"""Safe fallback for _grouped_mm on gfx1200 (null HIP kernel)."""
_t = _torch_for_rocm
if offs is None:
# Simple case: plain matrix multiply.
result = _t.mm(self.contiguous(), mat2.contiguous())
else:
# Grouped case: offs[i] is the exclusive end-row of group i
# in `self`; mat2 may be 3-D (num_groups, K, N) or 2-D.
offs_list = offs.tolist()
pieces = []
prev = 0
for idx, end in enumerate(offs_list):
end = int(end)
a_part = self[prev:end].contiguous()
if mat2.dim() == 3:
b_part = mat2[idx].contiguous()
else:
b_part = mat2.contiguous()
pieces.append(_t.mm(a_part, b_part))
prev = end
# Include any trailing rows not covered by offs
if prev < self.shape[0]:
a_tail = self[prev:].contiguous()
b_tail = (
mat2[-1].contiguous()
if mat2.dim() == 3
else mat2.contiguous()
)
pieces.append(_t.mm(a_tail, b_tail))
result = (
_t.cat(pieces, dim = 0)
if pieces
else _t.zeros(
0,
mat2.shape[-1],
device = self.device,
dtype = self.dtype,
)
)
if bias is not None:
result = result + bias
if out_dtype is not None:
result = result.to(out_dtype)
elif result.dtype != self.dtype:
result = result.to(self.dtype)
return result
with _warnings.catch_warnings():
_warnings.simplefilter("ignore")
_gm_lib.impl("_grouped_mm", _grouped_mm_safe_impl, "CUDA")
_WINDOWS_ROCM_GROUPED_MM_LIB = _gm_lib # prevent GC
logger.info(
"Windows ROCm: patched _grouped_mm CUDA dispatch "
"(null HIP kernel on gfx1200 bypassed with safe mm fallback)"
# Parse HIP version for the kernel-fix gate below.
# torch.version.hip can be "7.13.99004", "7.2.0", etc.
# We only need major.minor for the comparison.
def _hip_ver_at_least(major: int, minor: int) -> bool:
_hip_str = getattr(
getattr(_torch_for_rocm, "version", None), "hip", None
)
except Exception as _patch_exc:
logger.warning(
"Windows ROCm: could not patch _grouped_mm — "
"training may crash with 0xC0000005: %s",
_patch_exc,
if not _hip_str:
return False
try:
_parts = [int(x) for x in str(_hip_str).split(".")[:2]]
return (_parts[0], _parts[1]) >= (major, minor)
except (ValueError, IndexError):
return False
# _grouped_mm HIP kernel was null on gfx1200 in ROCm ≤ 7.12,
# causing 0xC0000005. AMD fixed it in ROCm 7.13 (torch 2.11+).
# Only install the Python fallback on the affected versions so users
# on 7.13+ get the real GPU kernel for MoE workloads.
if not _hip_ver_at_least(7, 13):
try:
import warnings as _warnings
_gm_lib = _torch_for_rocm.library.Library("aten", "IMPL")
def _grouped_mm_safe_impl(
self, mat2, offs = None, bias = None, out_dtype = None
):
"""Python mm fallback for _grouped_mm on gfx1200 (null HIP kernel, ROCm ≤ 7.12)."""
_t = _torch_for_rocm
if offs is None:
# Simple case: plain matrix multiply.
result = _t.mm(self.contiguous(), mat2.contiguous())
else:
# Grouped case: offs[i] is the exclusive end-row of
# group i in `self`; mat2 may be 3-D or 2-D.
offs_list = offs.tolist()
pieces = []
prev = 0
for idx, end in enumerate(offs_list):
end = int(end)
a_part = self[prev:end].contiguous()
if mat2.dim() == 3:
b_part = mat2[idx].contiguous()
else:
b_part = mat2.contiguous()
pieces.append(_t.mm(a_part, b_part))
prev = end
# Include any trailing rows not covered by offs
if prev < self.shape[0]:
a_tail = self[prev:].contiguous()
b_tail = (
mat2[-1].contiguous()
if mat2.dim() == 3
else mat2.contiguous()
)
pieces.append(_t.mm(a_tail, b_tail))
result = (
_t.cat(pieces, dim = 0)
if pieces
else _t.zeros(
0,
mat2.shape[-1],
device = self.device,
dtype = self.dtype,
)
)
if bias is not None:
result = result + bias
if out_dtype is not None:
result = result.to(out_dtype)
elif result.dtype != self.dtype:
result = result.to(self.dtype)
return result
with _warnings.catch_warnings():
_warnings.simplefilter("ignore")
_gm_lib.impl("_grouped_mm", _grouped_mm_safe_impl, "CUDA")
_WINDOWS_ROCM_GROUPED_MM_LIB = _gm_lib # prevent GC
logger.info(
"Windows ROCm: patched _grouped_mm CUDA dispatch "
"(null HIP kernel on gfx1200, ROCm ≤ 7.12 — "
"bypassed with Python mm fallback)"
)
except Exception as _patch_exc:
logger.warning(
"Windows ROCm: could not patch _grouped_mm — "
"training may crash with 0xC0000005: %s",
_patch_exc,
)
else:
logger.info(
"Windows ROCm: HIP >= 7.13 — _grouped_mm kernel is functional, "
"skipping Python fallback (AMD fixed gfx1200 null kernel in ROCm 7.13)"
)
# ── 2. Now import ML libraries (fresh in this clean process) ──

View file

@ -1884,6 +1884,41 @@ class TestWorkerWindowsRocmPatches:
# Must gate on HIP version — code uses getattr chain: "version" and "hip"
assert '"version"' in source and '"hip"' in source
def test_hip_ver_at_least_helper_defined(self):
"""_hip_ver_at_least helper must be defined inside the Windows ROCm block."""
source = _WORKER_PATH.read_text(encoding = "utf-8")
assert "def _hip_ver_at_least(major: int, minor: int)" in source
def test_grouped_mm_patch_gated_on_hip_lt_713(self):
"""_grouped_mm patch must be skipped on HIP >= 7.13 (AMD fixed the bug in ROCm 7.13)."""
source = _WORKER_PATH.read_text(encoding = "utf-8")
# The guard must call _hip_ver_at_least with exactly (7, 13)
assert "_hip_ver_at_least(7, 13)" in source
# The patch must be inside the `if not` branch (negated guard)
assert "if not _hip_ver_at_least(7, 13):" in source
def test_grouped_mm_hip_713_skip_message_present(self):
"""worker.py must log a message when skipping the patch on HIP >= 7.13."""
source = _WORKER_PATH.read_text(encoding = "utf-8")
assert "HIP >= 7.13" in source
assert "7.13" in source
def test_grouped_mm_patch_else_branch_present(self):
"""An else branch must follow the _hip_ver_at_least gate (skip path for 7.13+)."""
source = _WORKER_PATH.read_text(encoding = "utf-8")
# There must be an else: after the if not _hip_ver_at_least(7, 13): block
gate_idx = source.find("if not _hip_ver_at_least(7, 13):")
assert gate_idx != -1, "Version gate not found in worker.py"
# The else: branch must appear after the gate
else_idx = source.find("else:", gate_idx)
assert else_idx != -1, "else: branch after _hip_ver_at_least gate not found"
def test_hip_ver_at_least_handles_amd_version_format(self):
"""_hip_ver_at_least must split on '.' and compare only major.minor (handles '7.13.99004')."""
source = _WORKER_PATH.read_text(encoding = "utf-8")
# Must split the version string and take the first two parts
assert 'split(".")[:2]' in source or ".split('.')[:2]" in source
# =============================================================================
# TEST: install_python_stack.py -- _ROCM_TORCH_PKG_SPECS mapping