Review Test Refactoring
Review a test file (or a PR/diff) against the decoupling standards defined in
the refactor-test-decoupling skill. The review checks classification
correctness, naming conventions, API replacements, instantiation mechanisms,
and completeness.
How to Use This Skill
Choose Your Mode
This skill supports two review modes. Determine which applies:
| Input | Mode | What to Review |
|---|---|---|
A test file path (test/test_ops.py) |
Whole-file review | Audit the entire file against decoupling standards |
A PR URL, branch name, or git diff |
Diff-based review | Review only the changed portions |
For Whole-File Review (test file path)
Read the entire test file. Load the full file — you are auditing every class and test method, not just a diff.
Read
../../../reference/device_api_catalog.yaml. This catalog is the authoritative reference for whether a device API is Category A (hastorch.acceleratorequivalent), Category B (general cross-backend concept), or Category C (truly device-specific). Every API classification decision in the review must be grounded in this catalog. See../../../reference/classification_guide.mdfor lookup instructions.Run through the full checklist below, applying every check to every class and test method in the file. There is no "before" version to compare against — you are the auditor.
For Diff-Based Review (PR, branch, or diff)
Identify the changes to review. If the user provides a PR URL, fetch it with
gh. Otherwise, usegit diffagainst the base branch (usuallyorigin/main).Read
../../../reference/device_api_catalog.yaml(same as above).For each changed test file, run through the checklist below. Focus on the diff — you are reviewing what changed, not re-auditing the entire file. However, key checks like naming conventions, instantiation mechanisms, and import cleanliness should be verified for the file as a whole even in diff-based mode.
Reporting
Report findings organized by severity:
- Blocker: Test loss, wrong classification locking tests out of accelerators, broken instantiation (class won't run).
- Major: Wrong naming convention, wrong instantiation mechanism, stale imports that keep file classified as device_specific.
- Minor: Style issues, missed cleanup opportunities, suboptimal decorator ordering.
Reference: The Device API Catalog
../../../reference/device_api_catalog.yaml classifies every PyTorch device API into three categories. Always consult it when reviewing classifications — never rely on memory or heuristics.
| Category | Description | Strategy Implication |
|---|---|---|
| A | APIs with torch.accelerator equivalents |
NOT device-specific → device-agnostic |
| B | General cross-backend concepts, no wrapper yet | NOT device-specific → device-agnostic |
| C | Truly device-specific, no cross-device equivalent | device-specific only |
Rule: Only Category C APIs justify device-specific. If a test uses only Category A or B APIs, it must be device-agnostic with @onlyAccelerator.
Review Checklist
1. Classification Correctness
The single most impactful category of review finding. A wrong classification either locks tests out of accelerators they could run on (device-agnostic misclassified as device-specific) or causes test failures on accelerators that lack the required features (device-specific misclassified as device-agnostic).
1a. False-CUDA Detection (most common error)
For every test classified as device-specific (TestFooCUDA) or using
@onlyCUDA / device="cuda", ask:
Is this test verifying a truly device-specific feature (Category C in the report), or is it just using CUDA as a device for generic computation?
How to check: Look up each torch.cuda.* API the test uses in
../../../reference/device_api_catalog.yaml. If every API it uses is Category A or B,
the test is misclassified — it should be device-agnostic with @onlyAccelerator.
Red flags (signals the test is wrongly classified as CUDA-specific):
| Code Pattern | What It Means | Severity |
|---|---|---|
Test with generic ops (add, softmax, matmul, loss) still has @onlyCUDA or device="cuda" |
Should be device-agnostic with @onlyAccelerator |
Blocker |
.cuda() / .to("cuda") used instead of .to(device) |
Test hardcodes CUDA for no reason | Blocker |
torch.cuda.<api> call where the catalog shows torch.accelerator.<api> exists |
Category A — has cross-accelerator equivalent; replace with torch.accelerator.* |
Major |
torch.cuda.Stream / torch.cuda.Event used but test not marked as device-specific |
Category B — general concept; verify usage context, usually device-agnostic | Info |
TEST_CUDA import remains but no device-specific CUDA tests exist in the file |
Stale import keeps file classified as device_specific | Major |
Unnecessary @onlyAccelerator: If @onlyAccelerator was ADDED to a test that had no prior device restriction, verify the test genuinely requires an accelerator. If it works on CPU, the restriction should have been removed entirely.
1b. Over-generalization Detection
Conversely, check that tests using Category C APIs were NOT incorrectly
generalized to device-agnostic. Consult ../../../reference/device_api_catalog.yaml → category_c for the full per-backend lists. Key examples:
| Code Pattern | What It Means | Severity |
|---|---|---|
Test using any API from category_c.cuda in the catalog but placed in TestFooDevice with @onlyAccelerator |
Will fail on non-CUDA accelerators | Blocker |
Test using any API from category_c.mps in the catalog but placed in TestFooDevice |
Will fail on non-MPS accelerators | Blocker |
Test using any API from category_c.xpu in the catalog but placed in TestFooDevice |
Will fail on non-XPU accelerators | Blocker |
Dtype compatibility on MPS: Even when a test uses only generic ops (no
Category C APIs), it may still fail on non-CUDA accelerators if it uses dtypes
not supported by that backend. The most common case: complex128 and float64
are unsupported on MPS. When a test is generalized to device-agnostic (or already
uses @onlyAccelerator):
| Check | How to Verify |
|---|---|
complex128 or torch.complex128 used in @dtypes or as default dtype |
MPS does not support double-precision complex. Add @expectedFailureMPS or use @dtypesIfMPS to exclude complex128. |
float64 or torch.float64 used in @dtypes or as default dtype |
MPS does not support float64. Add @expectedFailureMPS or use @dtypesIfMPS to exclude float64. |
torch.long used with MPS convolution/indexing ops |
MPS has limited int64 support in some ops. Add a skip (@skipIfMPS) if needed. |
How to validate: For every test using @onlyAccelerator or the device
parameter, verify every dtype it exercises (from @dtypes, _default_dtype, or
inline tensor creation) against known MPS dtype limitations. The failure
signature is: "Cannot convert a MPS Tensor to float64 dtype" or similar dtype
conversion errors on MPS. In diff-based mode, pay special attention to tests
where @onlyCUDA was removed.
MPS coverage safety: When MPS coverage is broadened (new allow_mps=True or @onlyAccelerator replacing CUDA-only restriction), verify @skipIfMPS is present unless MPS was already covered via @dtypesIfMPS or @onlyMPS. Exception: @skipIfMPS is NOT required when the class is NOT instantiated for MPS — MPS variants are only created when the class's instantiate_device_type_tests call passes allow_mps=True. If no MPS variant exists, the test cannot run on MPS and the skip is unnecessary.
@onlyCPU to device-agnostic: Verify each @onlyCPU test was individually evaluated (not bulk-decided). Check that device param was added when @onlyCPU was removed.
1c. CPU-only Correctness
For tests in a CPU-only class (TestFoo without device suffix):
| Check | What to Look For |
|---|---|
No device parameter in method signature |
def test_foo(self) not def test_foo(self, device) |
| No device-dependent decorators | No @onlyCUDA, @onlyAccelerator, @skipCUDAIf, etc. |
No .to(device), .cuda(), device=device in test body |
All tensors are CPU |
| No cross-device tensor operations | Can't have CPU tensor op GPU tensor |
2. Naming Convention
Class renaming is OPTIONAL. The future hw_classification member on TestCase will handle strategy classification; class names are no longer the primary discriminator. The coder decides whether to rename based on external reference impact (see refactor-test-decoupling for the decision framework).
Do NOT flag a class name mismatch as an issue unless it is actively misleading (e.g., a CPU-only class named TestFooCUDA). A class using the original name with the correct strategy mechanism is valid.
For reference, the recommended naming convention (when coder chooses to rename):
| Strategy | Recommended Name | Acceptable Alternative |
|---|---|---|
| CPU-only | TestFoo (original name) |
Must NOT have device suffix |
| device-agnostic | TestFooDevice |
Original name is fine |
| device-specific | Original name (instantiate_device_type_tests appends the device suffix) |
Original name is fine |
2a. Cross-File Reference Integrity
If the coder kept the original class names, skip this check — no external references need updating. This is the primary benefit of not renaming.
When a class IS renamed (e.g., TestIndexing → TestIndexingDevice), the
old name may still be referenced in external configuration files. A rename
without updating these files causes CI breakage: dynamo expected-failure entries
stop matching, turning expected failures into unexpected failures.
Files to check for stale class name references:
| File/Directory | Example Reference | How to Verify |
|---|---|---|
test/dynamo_skips/ |
TestIndexing.test_invalid_sparse_coo_values_cpu |
find test/dynamo_skips/ -name "OldClassName*" (by filename, NOT grep — these are sentinel files, often 0 bytes) |
test/dynamo_expected_failures/ |
TestIndexingCPU.test_byte_mask_cpu |
find test/dynamo_expected_failures/ -name "OldClassName*" (by filename, NOT grep) |
test/inductor_expected_failures/ |
TestIndexing.test_foo |
find test/inductor_expected_failures/ -name "OldClassName*" (by filename, NOT grep) |
torch/testing/_internal/common_methods_invocations.py |
DecorateInfo(unittest.skip("..."), 'TestCommon', 'test_complex_half_reference_testing') |
Search for 'OldClassName' string in DecorateInfo(...) constructor calls |
.ci/pytorch/test_exclude_list.py |
Test name in skip list | grep -r "OldClassName\b" .ci/pytorch/ |
.ci/pytorch/*-trunk.yml |
Test name in CI config | grep -r "OldClassName\b" .ci/ |
How to fix: For each stale reference, update the class name to match the
new name. Verify which class actually owns each test — when a class is split
into multiple new classes (CPU-only + 2 + 3), tests may now live under
different class names (e.g., TestIndexing.test_foo might now be
TestIndexingDevice.test_foo or TestIndexingCPU.test_foo).
For common_methods_invocations.py specifically: DecorateInfo entries
use exact class name matching in is_active() — if cls_name='TestCommon'
but the test now lives in TestCommonDevice, the skip/xfail decorator is
silently dropped. To find broken entries:
python -c "
import torch
from torch.testing._internal.common_methods_invocations import op_db
from torch.testing._internal.opinfo.core import DecorateInfo
old_names = {'TestOldName1', 'TestOldName2'} # fill in renamed classes
count = 0
for op in op_db:
for d in op.decorators:
if isinstance(d, DecorateInfo) and d.cls_name in old_names:
print(f'{op.name}: cls_name={d.cls_name}, test_name={d.test_name}')
count += 1
print(f'Total: {count} stale DecorateInfo entries')
"
Then replace the old class name string literal with the new one in
torch/testing/_internal/common_methods_invocations.py.
Severity: Blocker — broken DecorateInfo entries cause tests to silently run when they should be skipped, or tests to be silently skipped when they should run.
3. Instantiation Mechanism
| Strategy | Expected Mechanism | Wrong Mechanism |
|---|---|---|
| CPU-only, no parametrization | Plain TestCase |
instantiate_device_type_tests |
CPU-only, with @parametrize/@dtypes |
@instantiate_parametrized_tests |
instantiate_device_type_tests |
CPU-only, with @ops |
instantiate_device_type_tests(...,> |
@instantiate_parametrized_tests |
| device-agnostic | instantiate_device_type_tests(TestFooDevice, globals()) |
@instantiate_parametrized_tests |
| device-specific | instantiate_device_type_tests(...,> |
Plain TestCase with setUp guard or @instantiate_parametrized_tests |
Critical: Check that instantiate_device_type_tests is never used for
CPU-only classes — it creates useless per-device variants.
Critical: Check that no class uses both instantiate_parametrized_tests
and instantiate_device_type_tests — double instantiation causes test name
collisions.
4. API Replacement Correctness
For device-agnostic tests, verify device-specific APIs were replaced with their
device-agnostic equivalents. Consult ../../../reference/device_api_catalog.yaml → category_a for the authoritative mapping. The catalog defines every torch.<device>.<api> → torch.accelerator.<api> replacement.
Key checks:
| Before (Wrong) | After (Correct) | Check |
|---|---|---|
@onlyCUDA |
@onlyAccelerator |
Not left as @onlyCUDA |
@unittest.skipIf(not TEST_CUDA, ...) |
@onlyAccelerator |
Not left as skip |
device="cuda" |
device parameter |
No hardcoded "cuda" |
.cuda() / .to("cuda") |
.to(device) |
No .cuda() calls |
For any torch.cuda.<api>() call remaining in a device-agnostic test, check the
catalog: if Category A, it should be torch.accelerator.<api>(). If Category B,
use the unified type (e.g., torch.Stream instead of torch.cuda.Stream). If
Category C, the test belongs in device-specific.
Return type compatibility: Verify return type compatibility for all torch.accelerator.* replacements, especially HIGH RISK APIs: current_device_index (returns int, compare against int), set_device_index (takes int arg), get_device_capability (return type differs across backends). Consult ../../../reference/device_api_catalog.yaml type annotations.
Remaining torch.cuda in device-agnostic classes: Scan each device-agnostic class for remaining torch.cuda.* calls — each must be either migrated to torch.accelerator.* or the test moved to device-specific.
5. Import Cleanup
| Check | How to Verify |
|---|---|
TEST_CUDA import removed if no device-specific CUDA tests remain |
grep "TEST_CUDA" in the file |
TEST_MPS import removed if no device-specific MPS tests remain |
grep "TEST_MPS" in the file |
TEST_XPU import removed if no device-specific XPU tests remain |
grep "TEST_XPU" in the file |
@onlyCUDA import removed if no device-specific CUDA tests remain |
grep "onlyCUDA" in the imports |
@onlyOn import removed if all uses were replaced |
grep "onlyOn" in the file |
@onlyNativeDeviceTypes removal |
@onlyNativeDeviceTypes / @onlyNativeDeviceTypesAnd are redundant on device-agnostic classes — REMOVE them. Before removing, verify dtype compatibility (float64/complex128/channels-last may be unsupported on MPS/MTIA) and add @skipIfMPS/@dtypesIfMPS if needed. |
| New imports are correct | onlyAccelerator from common_device_type, torch.accelerator if used |
6. Test Completeness
Whole-file review: Verify every test method in the file is properly placed in
an appropriate strategy class. No test should be in a class that doesn't match
its device dependency level (e.g., a test using only CPU ops should not be in a
TestFooCUDA class).
| Check | How to Verify |
|---|---|
Every def test_ belongs to the correct strategy class |
Cross-reference each test's API usage against the catalog and its enclosing class name |
| Device instantiation present for device-specific | instantiate_device_type_tests(...,> with a |
| No test logic unintentionally modified | If reviewing a diff, compare test bodies against the base version. If whole-file, flag tests that appear incomplete or have empty bodies |
| No duplicate test bodies across device-specific classes | If identical test bodies appear across device-specific classes, they belong in the device-agnostic shared class |
| No device-specific artifacts in device-agnostic classes | Scan for _cuda suffix in test method names, internal variable names like cuda_out, module-level helpers with if device_type == "<backend>" branches — clean these when the test is in a device-agnostic class |
Diff-based review: Additionally verify that every original test method is
accounted for (count def test_ in old vs new). A test "lost" in refactoring is
a regression.
7. Common Pitfalls
| Pitfall | Detection | Severity |
|---|---|---|
@onlyAccelerator used as class decorator |
@onlyAccelerator\nclass TestFoo — breaks instantiate_device_type_tests |
Blocker |
CPU-only class has device parameter |
def test_foo(self, device) in TestFoo (no Device suffix) |
Blocker |
skipIfXpu/skipIfCUDA from common_utils in device-agnostic class |
These skip ALL variants, not just the target device | Major |
GPU_TYPE/HAS_GPU from inductor_utils not converted |
Leftover inductor-specific device abstraction | Major |
Mixed device param and hardcoded "cuda" in same class |
Inconsistent; some tests use device param, others hardcode | Major |
instantiate_device_type_tests call references wrong class name |
Class name in globals() call doesn't match actual test class — class never instantiated |
Blocker |
except_for/only_for/allow_mps/allow_xpu args missing from instantiation |
Device allowlists not applied to current instantiate_device_type_tests call |
Major |
| Category A/B API treated as if it makes a test CUDA-specific | Test locked to CUDA unnecessarily; check the catalog | Major |
| Missing blacklist skip decorators | @skipXPU, @skipMPS, @skipMeta absent — these document known gaps. If the original file had them and they're now gone, that's a regression |
Blocker |
@onlyAccelerator used without dtype compatibility check |
Test runs on MPS/XPU but uses complex128 or float64 (unsupported on MPS). For every test using @onlyAccelerator, verify every dtype the test uses is supported on ALL target backends. If not, add @expectedFailureMPS, @dtypesIfMPS, or a skip decorator. |
Blocker |
| Test class name doesn't match OpInfo DecorateInfo references | DecorateInfo entries in common_methods_invocations.py use exact class name matching in is_active(). If the test class was RENAMED, verify DecorateInfo entries were updated (section 2a). If the coder kept the original name, this check is a no-op. |
Blocker |
| Flagging an original class name as "wrong" when the coder chose not to rename | Renaming is optional. If the coder kept the original name (e.g., TestFoo for a device-agnostic class), do NOT flag it unless the name is actively misleading (e.g., a CPU-only class named TestFooCUDA). The hw_classification member will handle classification. |
N/A — reviewer guidance |
@unittest.skipIf(not TEST_CUDA, ...) leftover in device-agnostic class |
Should be @onlyAccelerator |
Major |
@skipIfMPS/@skipXPU/@skipCUDAIf applied to method without device parameter |
These decorators check the device kwarg and silently fail if missing |
Blocker |
Missing hw_classification class attribute |
Every test class must have hw_classification = HardwareClassification.XXX. Missing attr causes test runner to skip or misroute tests. |
Blocker |
Incorrect hw_classification value |
Value must match the class mechanism: GENERIC for CPU-only, ACCELERATOR for device-agnostic, CUDA/MPS/XPU for device-specific per device, CPU for CPU-only-with-@ops. A wrong value (e.g., GENERIC on a device-agnostic class) breaks --hw-classification filtering. |
Blocker |
HardwareClassification not imported |
Must be imported from torch.testing._internal.common_utils and merged alphabetically into the existing import block. |
Blocker |
8. Decorator Ordering
For device-agnostic tests, decorators must be ordered correctly. The device
parameter is filled in by instantiate_device_type_tests, and other
parametrization decorators fill additional arguments:
# Correct: @dtypes closest to method, @onlyAccelerator above
@onlyAccelerator # outermost (skip if CPU)
@dtypes(torch.float32) # parametrization
def test_foo(self, device, dtype):
...
# Wrong — @onlyAccelerator below @dtypes may cause issues
@dtypes(torch.float32)
@onlyAccelerator
def test_foo(self, device, dtype): # incorrect
...
9. HardwareClassification Tag
Every test class must have a hw_classification class attribute matching its
strategy and instantiation mechanism. This is mandatory — the test runner
uses --hw-classification to filter test execution by hardware category.
| Class Mechanism | Expected hw_classification |
|---|---|
Plain TestCase or @instantiate_parametrized_tests, no device param |
HardwareClassification.GENERIC |
instantiate_device_type_tests(only_for="cpu") |
HardwareClassification.CPU |
instantiate_device_type_tests(except_for=...) |
HardwareClassification.ACCELERATOR |
instantiate_device_type_tests(only_for="cuda") |
HardwareClassification.CUDA |
instantiate_device_type_tests(only_for="mps") |
HardwareClassification.MPS |
instantiate_device_type_tests(only_for="xpu") |
HardwareClassification.XPU |
| Structural contract (mirrors the deterministic test linter): |
GENERIC: class NOT instantiated viainstantiate_device_type_tests; methods take nodevice/devices.ACCELERATOR: class instantiated viainstantiate_device_type_tests; every method takesdevice/devices; no@only*except@onlyAccelerator; the instantiate call uses noonly_for.CPU/CUDA/MPS/XPU: class instantiated viainstantiate_device_type_testswithonly_for=<device>; every method takesdevice/devices; the instantiate call uses noexcept_for.
How to verify:
| Check | How to Verify |
|---|---|
| Import present | grep "HardwareClassification" <file> — must be imported from torch.testing._internal.common_utils |
| Every class tagged | grep "hw_classification" <file> — count must equal number of TestCase subclasses |
| Value matches strategy | Cross-reference each class's mechanism (instantiate call + device params) against the table above |
| Import merged alphabetically | HardwareClassification must appear in the existing common_utils import block in alphabetical order |
Severity: Blocker — missing or incorrect hw_classification causes the test runner to skip or misroute tests.
Review Output Format
Structure your review as follows:
## Review: <test file path or PR/branch name>
### Summary
- Mode: whole-file / diff-based
- File(s) reviewed: N
- Classification: correct / N issues found
- Naming: correct / N issues found
- API replacements: correct / N issues found
- Completeness: all tests properly placed / N issues found
### Findings
#### Blockers (must fix)
- [ ] **<file:line>**: <issue description>
- Fix: <suggested fix>
#### Major (should fix)
- [ ] **<file:line>**: <issue description>
#### Minor (nice to have)
- [ ] **<file:line>**: <issue description>
### Verified Correct
- <list of things that are correct per the standards>
Reference
The refactoring standards this review checks against are defined in the
refactor-test-decoupling skill. Consult it for the full classification
decision tree, blacklist vs. whitelist rules, instantiation mechanism
comparison, and CPU-only/2/3 patterns.
../../../reference/device_api_catalog.yaml is the single authoritative source for API classification. It categorizes every device API as A (accelerator equivalent), B (general concept), or C (truly device-specific). All classification decisions in the review must be grounded in this catalog — never hard-code or guess which APIs belong to which category.