Refactor Test Decoupling
Refactor PyTorch test files so tests focus on core functional logic and are decoupled from specific hardware accelerators.
Naming Convention
Class renaming is OPTIONAL. The future hw_classification member on TestCase (not yet landed) will handle strategy classification, so class names are no longer the primary classification mechanism. The agent should decide whether to rename based on external reference impact.
Recommended Names (when renaming)
| Strategy | Recommended Class Name | Instantiation | Example |
|---|---|---|---|
| CPU-only | TestFoo (keep original name) |
@instantiate_parametrized_tests or plain TestCase |
TestBinaryUfuncs |
| device-agnostic | TestFooDevice |
instantiate_device_type_tests() |
TestBinaryUfuncsDevice |
| device-specific | TestFoo (original name — instantiate_device_type_tests appends the device) |
instantiate_device_type_tests(only_for="<device>") |
TestBinaryUfuncsCUDA |
Renaming Decision
When to rename:
- The original class name has few or no external references (DecorateInfo entries, dynamo_skips/, dynamo_expected_failures/)
- The rename improves clarity (e.g.,
TestFoo→TestFooDevicemakes the strategy obvious)
When to keep the original name:
- The class has many external references that would need updating
- Renaming would risk silently breaking CI (stale DecorateInfo, dynamo skip files)
- The original name is already clear enough
How to decide:
- Check for DecorateInfo references:
grep "cls_name.*OldName" torch/testing/_internal/common_methods_invocations.py - Check for dynamo skip/expected-failure files:
find test/dynamo_skips/ test/dynamo_expected_failures/ -name "OldName*" - If zero or very few external refs → rename is safe
- If many external refs → keep the original name (avoids breaking cross-file references)
instantiate_device_type_tests removes the generic class from scope and replaces it with per-device variants (TestFooDeviceCPU, TestFooDeviceCUDA, etc.). instantiate_parametrized_tests keeps the class discoverable.
Device-specific mechanism: Device-specific classes use instantiate_device_type_tests(<Class>, globals(),> with a device parameter on each test method — they do NOT use a plain TestCase with a setUp guard. Because device-specific classes use instantiate_device_type_tests, @dtypes/@dtypesIfCUDA/@dtypesIfCPU decorators resolve correctly.
Classification
Every test falls into one of three categories. Classification is hierarchical: device-specific > device-agnostic > CPU-only.
| Category | Definition | Mechanism |
|---|---|---|
| CPU-only | No device usage; CPU only | instantiate_parametrized_tests() or plain TestCase |
| device-agnostic | Uses a device but only generic accelerator APIs | instantiate_device_type_tests() |
| device-specific | Requires a particular accelerator's unique features | instantiate_device_type_tests(only_for="<device>") |
Device API Categories (consult ../../../reference/device_api_catalog.yaml)
| Category | Examples | Strategy |
|---|---|---|
A — has torch.accelerator equivalent |
empty_cache, synchronize, CUDAGraph, memory_allocated, current_device |
device-agnostic |
| B — general concept, no wrapper yet | Stream, Event, manual_seed, get_device_properties |
device-agnostic |
| C — truly device-specific, no cross-device equivalent | NCCL, NVTX, cuDNN, GDS, Jiterator, Metal shaders, SYCL handles | device-specific |
Only Category C makes a test device-specific. If you can replace "cuda" with "mps" or "xpu" and the test still makes logical sense, it's device-agnostic.
Blacklist vs. Whitelist Decorators
| Decorator Type | Examples | Principle | Action |
|---|---|---|---|
| Blacklist (explicit skips) | @skipXPU, @skipCUDAIf, @skipMPS, @skipMeta |
Documents a known gap — intentional and informed | KEEP as-is |
| Whitelist (restrictive) | @onlyCUDA, @onlyOn(["cuda","xpu"]), @unittest.skipIf(not TEST_CUDA, ...) |
Artificially restricts — usually historical accident | ENLARGE to @onlyAccelerator |
| Whitelist (restrictive) | @onlyCPU |
Artificially restricts — usually historical accident | REMOVE — make test device-agnostic (add device param, pass device=device). MUST evaluate each @onlyCPU test individually. Default to device-agnostic unless the test genuinely tests CPU-only dispatch behavior. |
@onlyNativeDeviceTypes / @onlyNativeDeviceTypesAnd are redundant on device-agnostic classes — device instantiation already scopes to the right devices. REMOVE them (the test linter flags them on ACCELERATOR classes).
Decision Tree
Does the test reference a device?
├─ NO → CPU-only
├─ YES → What device APIs?
│ ├─ Generic only (torch.device(device), make_tensor(..., device=device)) → device-agnostic
│ ├─ Category A or B APIs → device-agnostic
│ ├─ Category C APIs → device-specific
│ └─ Hard to tell → Leave as-is
What decorators?
├─ Blacklist (@skipXPU, @skipCUDAIf, @skipMPS, @skipMeta) → KEEP
├─ Whitelist (@onlyCUDA, @onlyOn, @unittest.skipIf(not TEST_CUDA, ...)) → ENLARGE to @onlyAccelerator
> **Note:** An `if device_type == "<backend>"` conditional in the test body does NOT make a test device-specific — only Category C API calls do.
False-CUDA Patterns (→ device-agnostic, NOT device-specific)
These almost always indicate device-agnostic:
| Pattern | Why Not CUDA-Specific | Action |
|---|---|---|
@onlyCUDA on standard ops (add, softmax, matmul) |
The op works on any accelerator | @onlyAccelerator + device param |
.cuda() / .to("cuda") on tensors |
Just device placement | .to(device) |
device="cuda" in tensor creation |
Any device would work | device param |
@unittest.skipIf(not TEST_CUDA, ...) |
Proxy for "needs accelerator" | @onlyAccelerator |
Test name contains _cuda |
Naming, not functional | Remove suffix |
Caveats:
- Do NOT enlarge
@onlyCUDA→@onlyAcceleratorif the test had no prior device restriction — remove the restriction entirely instead. - Keep
@onlyCUDAif the test relies on backend-specific behavioral guarantees (NaN handling, determinism, precision, rounding modes).
Strategy: CPU-only
Zero device dependency. CPU tensors only, no device parameter.
Pattern A — Plain TestCase (no parametrization):
from torch.testing._internal.common_utils import HardwareClassification
class TestFoo(TestCase):
hw_classification = HardwareClassification.GENERIC
def test_basic_addition(self):
a = torch.randn(3, 3)
b = torch.randn(3, 3)
self.assertEqual(a + b, torch.add(a, b))
Pattern B — @instantiate_parametrized_tests (has @parametrize/@ops/@dtypes):
from torch.testing._internal.common_utils import HardwareClassification
@instantiate_parametrized_tests
class TestFoo(TestCase):
hw_classification = HardwareClassification.GENERIC
@parametrize("dtype", [torch.float32, torch.float64])
def test_dtype_behavior(self, dtype):
t = torch.randn(3, 3, dtype=dtype)
self.assertEqual(t.softmax(0).sum(0), torch.ones(3, dtype=dtype))
Why not instantiate_device_type_tests? It creates per-device variants (TestFooCPU, TestFooCUDA, etc.) — wasteful when all variants do the same CPU-only work.
Steps:
- Extract test methods into a standalone class. Keep the original name (no device suffix) — CPU-only classes should never have device suffixes.
- Remove
deviceparameter from signatures; hardcode"cpu"or omit device args - Remove device decorators and device imports (
TEST_CUDA,TEST_MPS, etc.) - Add
@instantiate_parametrized_testsif the class has parametrized decorators - Tag with
hw_classification: Addhw_classification = HardwareClassification.GENERICas the first class attribute. ImportHardwareClassificationfromtorch.testing._internal.common_utils(merge alphabetically into the existingcommon_utilsimport block). If the class usesinstantiate_device_type_tests(only_for="cpu")for@ops, useHardwareClassification.CPUinstead.
Strategy: Device-Agnostic
Tests that use a device parameter but only need generic accelerator APIs. This is the highest-impact refactoring — it unlocks tests for all accelerators at once.
Canonical Before/After
Before (false-CUDA):
from torch.testing._internal.common_cuda import TEST_CUDA
class TestFoo(TestCase):
@unittest.skipIf(not TEST_CUDA, "no CUDA")
def test_softmax_cuda(self):
t = torch.randn(3, 3, device="cuda")
result = t.softmax(0)
self.assertEqual(result.sum(0), torch.ones(3, device="cuda"))
@onlyCUDA
@skipXPU # XPU doesn't support this op yet
def test_matmul_cuda(self, device):
a = torch.randn(3, 3, device=device)
b = torch.randn(3, 3, device=device)
self.assertEqual(a @ b, torch.matmul(a, b))
After (device-agnostic):
from torch.testing._internal.common_device_type import (
instantiate_device_type_tests, onlyAccelerator,
)
from torch.testing._internal.common_utils import HardwareClassification
class TestFooDevice(TestCase):
hw_classification = HardwareClassification.ACCELERATOR
@onlyAccelerator
def test_softmax(self, device):
t = torch.randn(3, 3, device=device)
result = t.softmax(0)
self.assertEqual(result.sum(0), torch.ones(3, device=device))
@onlyAccelerator
@skipXPU # Still here — known gap
def test_matmul(self, device):
a = torch.randn(3, 3, device=device)
b = torch.randn(3, 3, device=device)
self.assertEqual(a @ b, torch.matmul(a, b))
instantiate_device_type_tests(TestFooDevice, globals())
Steps
- Scrutinize every CUDA reference. Ask: "CUDA as device or CUDA as feature?" Most are the former → device-agnostic.
- Create the device-agnostic class inheriting from
TestCase. Decide whether to rename (see "Renaming Decision" above). If renaming, useTestFooDevice; otherwise keep the original name. - Add
deviceparameter as first arg afterselfon each test method. - Replace hardcoded device strings:
"cuda"→deviceparam,.cuda()→.to(device). - Enlarge whitelist, keep blacklist:
@onlyCUDA→@onlyAccelerator,@unittest.skipIf(not TEST_CUDA, ...)→@onlyAccelerator. Keep@skipXPU,@skipCUDAIf,@skipMPS,@skipMetaas-is; remove@onlyNativeDeviceTypes/@onlyNativeDeviceTypesAnd(redundant). - Replace device-specific APIs:
torch.cuda.is_available()→torch.accelerator.is_available(), Category A APIs →torch.accelerator.*equivalents (see catalog). - Register:
instantiate_device_type_tests(<ClassName>, globals())at module level. - Tag with
hw_classification: Addhw_classification = HardwareClassification.ACCELERATORas the first class attribute. ImportHardwareClassificationfromtorch.testing._internal.common_utils(merge alphabetically into the existingcommon_utilsimport block). - Remove stale imports:
TEST_CUDA,TEST_MPSonly if no longer referenced.
Key Rules
@onlyAcceleratoris a method decorator, NOT a class decorator. Applied to a class, it replaces the class with a function andinstantiate_device_type_testsfails.- Use device-type-aware skips in device-agnostic classes:
skipXPUIf(True, msg)/skipCUDAIf(condition, msg)fromcommon_device_type(notcommon_utils) — these checkself.device_typeand only skip the specific device variant. - Category A APIs (
empty_cache,synchronize,CUDAGraph,memory_*) havetorch.accelerator.*equivalents — they do NOT make a test CUDA-specific. - Category B APIs (
Stream,Event) are general concepts on all backends — they do NOT make a test CUDA-specific.
Strategy: Device-Specific
Tests requiring a particular accelerator's unique (Category C) features.
Device-specific classes use instantiate_device_type_tests(only_for="<device>"). Every test method takes a device parameter. Do NOT use a plain TestCase with a setUp guard — the test linter rejects it. Naming: keep the original class name — instantiate_device_type_tests appends the device name to generate the variant (e.g. TestFoo → TestFooCUDA). Do NOT pre-suffix the name with the device (TestFooOnCUDA + only_for="cuda" → TestFooOnCUDACUDA).
from torch.testing._internal.common_device_type import instantiate_device_type_tests
from torch.testing._internal.common_utils import HardwareClassification
class TestFoo(TestCase):
hw_classification = HardwareClassification.CUDA
def test_cuda_stream(self, device):
s = torch.cuda.Stream()
...
instantiate_device_type_tests(TestFoo, globals(),
Because device-specific classes use instantiate_device_type_tests, @dtypes/@dtypesIfCUDA/@dtypesIfCPU/@parametrize decorators resolve correctly (they receive the device-type context from instantiation).
Steps:
- Confirm the test genuinely uses Category C APIs.
- Extract into the device-specific class. Decide whether to rename (see "Renaming Decision" above). Prefer the original name —
hw_classificationis the discriminator. - Add a
deviceparameter as first arg afterselfon each test method. Keep Category C API calls (torch.cuda.*, etc.) as-is. - Register:
instantiate_device_type_tests(<ClassName>, globals(),> at module level. - Tag with
hw_classification: Addhw_classification = HardwareClassification.CUDA(orMPS,XPUper device) as the first class attribute. ImportHardwareClassificationfromtorch.testing._internal.common_utils(merge alphabetically into the existingcommon_utilsimport block).
Combined Workflow
Step 1: Audit
Classify every test method. Create a table:
| Test Method | Device Usage | Category | Target Strategy |
|---|---|---|---|
test_basic_add |
None | unrelated | CPU-only |
test_softmax_cuda |
Generic only | agnostic | device-agnostic |
test_cuda_stream |
CUDA-specific | specific | device-specific |
Step 2: Split
Create up to three classes following the naming convention and patterns above.
Step 3: Clean up
- Remove stale
TEST_CUDA/TEST_MPSimports andcopy_tests()calls - Remove
deviceparameter from CPU-only tests - Keep blacklist skips (
@skipXPU,@skipMPS,@skipMeta,@skipCUDAIf)
Step 4: Update external references after class renames
If you kept the original class names, skip this step — no external references need updating. This is the primary benefit of not renaming.
When a class IS renamed (e.g., TestCommon → TestCommonDevice), external references to the old class name will silently stop matching. This causes previously-skipped tests to run and fail, or expected failures to become unguarded.
Three locations to check:
(a) DecorateInfo in common_methods_invocations.py — DecorateInfo entries use exact cls_name comparison:
python -c "
from torch.testing._internal.common_methods_invocations import op_db
from torch.testing._internal.opinfo.core import DecorateInfo
old = {'TestOldName1', 'TestOldName2'}
for op in op_db:
for d in op.decorators:
if isinstance(d, DecorateInfo) and d.cls_name in old:
print(f'{op.name}: cls_name={d.cls_name}, test_name={d.test_name}')
"
Fix: Search-and-replace the old class name in common_methods_invocations.py. For class splits, verify which new class owns each test method first.
(b) test/dynamo_skips/ — filenames are ClassName.test_method_name. When a class is renamed, old filenames no longer match and skipped tests may start running:
# Find stale entries after renaming TestFoo -> TestFooDevice
ls test/dynamo_skips/TestFoo.* 2>/dev/null
Fix: Rename files to use the new class name: mv test/dynamo_skips/TestFoo.test_x test/dynamo_skips/TestFooDevice.test_x
(c) test/dynamo_expected_failures/ — same filename convention as dynamo_skips:
# Find stale entries after renaming TestFoo -> TestFooDevice
ls test/dynamo_expected_failures/TestFoo.* 2>/dev/null
Fix: Same as (b) — rename files to match the new class name.
Step 5: Verify
- Test count:
grep -c "def test_" test/test_file.py— must match original - Class structure:
grep "^class " test/test_file.py— verify naming and instantiation - DecorateInfo: Step 4(a) check script produces zero output
- dynamo_skips: Step 4(b) check produces no stale entries
- dynamo_expected_failures: Step 4(c) check produces no stale entries
- Syntax:
python -c "import py_compile; py_compile.compile('test/test_file.py', doraise=True)"
Instantiation Mechanism Comparison
| Mechanism | Creates Device Variants? | Generic Class Discoverable? | hw_classification | Use When |
|---|---|---|---|---|
Plain TestCase |
No | Yes | GENERIC |
No parametrization needed |
instantiate_parametrized_tests() |
No | Yes | GENERIC |
Tests with @parametrize/@ops/@dtypes, no device dependency |
instantiate_device_type_tests() |
Yes (CPU, CUDA, MPS, ...) | No (removed from scope) | ACCELERATOR |
Tests with a device parameter, works on any accelerator |
instantiate_device_type_tests(only_for="<device>") |
Yes (single device) | No (removed from scope) | CUDA / MPS / XPU |
device-specific classes — Category C APIs |
Common Pitfalls
| Pitfall | Fix |
|---|---|
Removing blacklist skips (@skipXPU, @skipCUDAIf, @skipMPS, @skipMeta) |
Keep as-is — they document known gaps |
Treating Cat A/B APIs as CUDA-specific (empty_cache, synchronize, CUDAGraph, Stream, Event, memory_*) |
These are device-agnostic — consult device_api_catalog.yaml |
@onlyAccelerator as class decorator |
Use as method decorator only — on a class it replaces the class with a function |
Using skipIfXpu/skipIfCUDA from common_utils in device-agnostic classes |
Use common_device_type equivalents (skipXPUIf, skipCUDAIf) — they check self.device_type and only skip the target variant |
Naming CPU-only class with device suffix (e.g., TestFooCPU) |
Keep original name without suffix (TestFoo) — CPU-only has no device dependency. CPU-only classes should never have device suffixes. |
| Renaming a class without checking external reference impact | Before renaming, check DecorateInfo entries and dynamo_skips/dynamo_expected_failures for references to the old name. If there are many external refs, consider keeping the original name to avoid silent breakage. |
| Moving cross-device tests (CPU+GPU) to CPU-only | Tests using both CPU and GPU tensors still need a GPU — keep in device-agnostic |
| Renaming class without updating DecorateInfo | Search common_methods_invocations.py for old class name and update |
| Renaming class without updating dynamo_skips/ | Search test/dynamo_skips/ for filenames starting with old class name and rename to new class name |
| Renaming class without updating dynamo_expected_failures/ | Search test/dynamo_expected_failures/ for filenames starting with old class name and rename to new class name |
Using instantiate_device_type_tests for CPU-only tests |
Creates wasteful per-device variants doing the same CPU work — use instantiate_parametrized_tests |
Using @instantiate_parametrized_tests for device-specific classes |
Device-specific classes use instantiate_device_type_tests (which provides the device-type context that @dtypesIfCUDA/@dtypes/@parametrize need). Do not use @instantiate_parametrized_tests for device-specific classes |
Mixing device param and hardcoded "cuda" in same class |
Pick one strategy per class |
Including device suffix in device-specific class name when using instantiate_device_type_tests(...,> produces doubled names |
Keep the original class name — instantiate_device_type_tests appends the device suffix itself |
| Derive device type from existing data — never add new parameters | The device parameter from instantiate_device_type_tests or tensor.device.type from any tensor in scope already provides the device type. Adding explicit device_type/device parameters to functions that already receive tensors or have access to the test's device kwarg is redundant and breaks conventions (especially autograd.Function.forward()). |
| Mixed-device tests | When a test deliberately creates tensors on different devices (CPU + accelerator) for cross-device error handling: keep CPU tensors as explicit CPU, use device param for accelerator tensors, scope with @onlyAccelerator. Do NOT move to CPU-only or blindly convert all tensors to device. |
Missing hw_classification attribute |
Every refactored test class must have hw_classification = HardwareClassification.XXX as the first class attribute. Import HardwareClassification from torch.testing._internal.common_utils (merge alphabetically). CPU-only→GENERIC (or CPU for only_for="cpu"), device-agnostic→ACCELERATOR, device-specific→CUDA/MPS/XPU per device. |