Skip to content

Commit 223455b

Browse files
authored
Let the CUDA backend autotune matmuls on GPUs with fewer than 68 SMs (pytorch#23352)
## What is wrong today The CUDA backend compiles a model with AOTInductor and asks it to autotune every matrix multiply and convolution with Triton only. It does not allow the ATen fallback, because that would pull libtorch operators into the compiled library. Inductor only offers its Triton matmul and convolution templates on GPUs it considers big enough: 68 or more streaming multiprocessors (SMs). On a smaller GPU the Triton templates are skipped. A matmul then has no choices left, because nothing else is allowed: ``` NoValidChoicesError: No choices to select. Provided reason: No choices exist for backend. target: aten.addmm.default ``` A convolution falls back to ATen instead, which this backend then rejects as a missing fallback kernel (`aoti_torch_cuda_convolution`). So on any CUDA GPU with fewer than 68 SMs, even a model with a single `nn.Linear` cannot be lowered with `CudaPartitioner`. Today the only way around it is to patch `torch._inductor.utils.is_big_gpu` in your own export script. ## What this change does While the backend's own AOTInductor compile runs, inductor treats the GPU as big enough, so it considers the Triton templates it would otherwise skip. The autotuner then picks the fastest Triton choice as usual. The override follows the pattern the file already uses for its other inductor tweaks: - It is a context manager, `_allow_triton_templates_on_small_gpus`, entered in `get_extra_aoti_compile_context_manager` next to the existing ones. - It keeps the original `is_big_gpu` in a closure and puts it back when the compile ends, even if the compile fails. - It answers only on the thread doing the compile. Other threads get the original answer. - It is skipped on ROCm, where `is_big_gpu` checks the GPU architecture instead of the SM count. GPUs with 68 or more SMs already pass the check, so nothing changes for them. The Triton-only backend list stays as it is. ## What was tested - New unit tests in `test_cuda_export.py`, which need no small GPU: with `is_big_gpu` reporting a small GPU, Inductor's own template check says yes inside the backend's compile context and no outside it; the override is skipped on ROCm; the original function and the thread's state are back after a failed compile; a nested compile keeps the override until the outer one ends; and another thread gets the original answer. Each one fails when the part it covers is removed. - On a CUDA GPU with 20 SMs (Linux aarch64), a module with one `nn.Linear` lowered with `CudaPartitioner`, with no patch in the caller: before, `NoValidChoicesError`; after, the `.pte` and `.ptd` files are written. - On the same GPU, `test_cuda_export.py`, `test_cuda_partitioner.py` and `test_cuda_weight_metadata.py`: before, 6 export tests fail (linear, a multi-operation module and SDPA with Triton kernels off with `NoValidChoicesError`; conv1d, conv2d and a ResNet block with the missing convolution kernel); after, they pass. A lowered `nn.Linear` and `Conv2d` also give the same results as eager PyTorch.
1 parent 8a08ecd commit 223455b

2 files changed

Lines changed: 117 additions & 2 deletions

File tree

‎backends/cuda/cuda_backend.py‎

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,40 @@ def _has_rmask(_self) -> bool:
113113
IndexingOptions.has_rmask = orig_has_rmask
114114

115115

116+
_SMALL_GPU_GUARD = threading.local()
117+
118+
119+
@contextlib.contextmanager
120+
def _allow_triton_templates_on_small_gpus():
121+
"""Let Inductor use Triton templates on GPUs below its 68-SM floor. This
122+
backend autotunes with Triton only, so otherwise a matmul or convolution
123+
has no Triton choices left.
124+
125+
Only threads inside this context get the override; other threads get the
126+
original answer.
127+
128+
TODO: remove once Inductor skips the floor when ATen is not an allowed
129+
backend (https://github.com/pytorch/pytorch/issues/141690).
130+
"""
131+
from torch._inductor import utils as inductor_utils
132+
133+
orig_is_big_gpu = inductor_utils.is_big_gpu
134+
135+
def _is_big_gpu(index_or_device=0) -> bool:
136+
return getattr(_SMALL_GPU_GUARD, "active", False) or orig_is_big_gpu(
137+
index_or_device
138+
)
139+
140+
prev_active = getattr(_SMALL_GPU_GUARD, "active", False)
141+
inductor_utils.is_big_gpu = _is_big_gpu
142+
_SMALL_GPU_GUARD.active = True
143+
try:
144+
yield
145+
finally:
146+
_SMALL_GPU_GUARD.active = prev_active
147+
inductor_utils.is_big_gpu = orig_is_big_gpu
148+
149+
116150
def _full_zeros_preserving_strides(x: torch.Tensor, device) -> torch.Tensor:
117151
"""Allocate a zero-filled tensor matching ``x``'s size/stride/dtype on ``device``.
118152
@@ -904,8 +938,10 @@ def get_extra_aoti_compile_context_manager(
904938
compilation for the CUDA backend. Each manager is documented at
905939
its own `enter_context` call site below.
906940
907-
The optional shared-memory target applies to every CUDA compilation.
908-
The low-memory export monkey-patch (CPU clones for mutated buffers)
941+
The optional shared-memory target applies to every CUDA compilation,
942+
and so does the small-GPU override that lets Inductor use Triton
943+
templates below 68 SMs (skipped on ROCm). The low-memory export
944+
monkey-patch (CPU clones for mutated buffers)
909945
is gated on the ``low_memory_mode`` compile spec — only models that
910946
explicitly opt in (currently Qwen3.5 MoE) get it. Other models go
911947
through the unmodified AOTI codepath, which avoids regressions in
@@ -932,6 +968,9 @@ def _combined():
932968
# `ReplaceEdgeOpWithTritonOpPass` are unaffected.
933969
stack.enter_context(torch.nn.attention.sdpa_kernel([SDPBackend.MATH]))
934970
stack.enter_context(target_smem_context())
971+
# On ROCm, is_big_gpu gates on architecture, not SM count.
972+
if torch.version.hip is None:
973+
stack.enter_context(_allow_triton_templates_on_small_gpus())
935974
if low_memory_mode == "ON":
936975
# Force AOTI's mutated-buffer clones onto CPU during
937976
# compile so we stay under tight GPU memory caps (e.g.

‎backends/cuda/tests/test_cuda_export.py‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import contextlib
88
import os
9+
import threading
910
import unittest
1011
from typing import Tuple
1112
from unittest import mock
@@ -133,6 +134,81 @@ def test_target_smem_context_only_patches_exact_triton_limit(self):
133134

134135
self.assertIs(triton_compiler.max_shared_mem, local_max_shared_mem)
135136

137+
def test_triton_templates_allowed_on_small_gpus_during_compile(self):
138+
from torch._inductor import utils as inductor_utils
139+
from torch._inductor.ir import FixedLayout
140+
141+
layout = FixedLayout(torch.device("cuda", 0), torch.float32, [64, 64])
142+
with mock.patch.object(
143+
inductor_utils, "is_big_gpu", return_value=False
144+
) as is_big_gpu, mock.patch.object(torch.version, "hip", None):
145+
self.assertFalse(
146+
inductor_utils._use_template_for_gpu(layout, [torch.float32])
147+
)
148+
with CudaBackend.get_extra_aoti_compile_context_manager([]):
149+
self.assertTrue(
150+
inductor_utils._use_template_for_gpu(layout, [torch.float32])
151+
)
152+
self.assertIs(inductor_utils.is_big_gpu, is_big_gpu)
153+
154+
def test_small_gpu_override_is_skipped_on_rocm(self):
155+
from torch._inductor import utils as inductor_utils
156+
157+
with mock.patch.object(
158+
inductor_utils, "is_big_gpu", return_value=False
159+
) as is_big_gpu, mock.patch.object(torch.version, "hip", "6.4.0"):
160+
with CudaBackend.get_extra_aoti_compile_context_manager([]):
161+
self.assertIs(inductor_utils.is_big_gpu, is_big_gpu)
162+
163+
def test_small_gpu_override_is_restored_when_a_compile_fails(self):
164+
from executorch.backends.cuda.cuda_backend import (
165+
_allow_triton_templates_on_small_gpus,
166+
)
167+
from torch._inductor import utils as inductor_utils
168+
169+
with mock.patch.object(
170+
inductor_utils, "is_big_gpu", return_value=False
171+
) as is_big_gpu:
172+
with self.assertRaises(RuntimeError):
173+
with _allow_triton_templates_on_small_gpus():
174+
raise RuntimeError("compile failed")
175+
self.assertIs(inductor_utils.is_big_gpu, is_big_gpu)
176+
with _allow_triton_templates_on_small_gpus():
177+
held = inductor_utils.is_big_gpu
178+
self.assertFalse(held(0))
179+
180+
def test_small_gpu_override_survives_a_nested_compile(self):
181+
from executorch.backends.cuda.cuda_backend import (
182+
_allow_triton_templates_on_small_gpus,
183+
)
184+
from torch._inductor import utils as inductor_utils
185+
186+
with mock.patch.object(
187+
inductor_utils, "is_big_gpu", return_value=False
188+
) as is_big_gpu:
189+
with _allow_triton_templates_on_small_gpus():
190+
with _allow_triton_templates_on_small_gpus():
191+
pass
192+
self.assertTrue(inductor_utils.is_big_gpu(0))
193+
self.assertIs(inductor_utils.is_big_gpu, is_big_gpu)
194+
195+
def test_small_gpu_override_answers_only_its_own_thread(self):
196+
from executorch.backends.cuda.cuda_backend import (
197+
_allow_triton_templates_on_small_gpus,
198+
)
199+
from torch._inductor import utils as inductor_utils
200+
201+
seen = {}
202+
with mock.patch.object(inductor_utils, "is_big_gpu", return_value=False):
203+
with _allow_triton_templates_on_small_gpus():
204+
other = threading.Thread(
205+
target=lambda: seen.update(other=inductor_utils.is_big_gpu(0))
206+
)
207+
other.start()
208+
other.join(timeout=10)
209+
seen["inside"] = inductor_utils.is_big_gpu(0)
210+
self.assertEqual({"inside": True, "other": False}, seen)
211+
136212
def test_cuda_include_ptx_compile_spec(self):
137213
with mock.patch.object(
138214
CudaBackend, "_setup_cuda_environment_for_fatbin", return_value=True

0 commit comments

Comments
 (0)