Generated test suites¶
Subclass ModelSuite, describe how to build the model and its inputs, pick the checks,
and pytest collects a test per check, on every device.
import torch
from torch.nn import functional as F
from pytest_torch import ModelSuite
class TestTextClassifier(ModelSuite):
checks = ("contract", "compile", "export", "autocast", "training")
dynamic_batch = True
check_options = {
"inference_contract": {"dtypes": (torch.bfloat16,)},
"overfits_one_batch": {"steps": 300},
}
def make_model(self):
return TextClassifier()
def make_inputs(self, make_tensor):
ids = make_tensor(8, 12, dtype=torch.long, low=1, high=100)
return {"input_ids": ids, "attention_mask": torch.ones_like(ids)}
def loss(self, output, inputs):
return F.cross_entropy(output["logits"], inputs["input_ids"][:, 0] % 4)
test_models.py::TestTextClassifier::test_inference_contract[cpu] PASSED
test_models.py::TestTextClassifier::test_compile_parity[cpu] PASSED
test_models.py::TestTextClassifier::test_export_parity[cpu] PASSED
test_models.py::TestTextClassifier::test_autocast_parity[cpu] PASSED
test_models.py::TestTextClassifier::test_gradients_finite[cpu] PASSED
...
Checks¶
| Check | Generated tests | Runs |
|---|---|---|
contract (default) |
test_inference_contract |
check_inference_contract |
compile (default) |
test_compile_parity |
assert_compile_parity (eager backend) |
export (default) |
test_export_parity |
assert_export_parity |
torchscript |
test_torchscript_parity |
assert_torchscript_parity |
onnx |
test_onnx_parity |
assert_onnx_parity (skips without onnxruntime) |
aoti |
test_aoti_parity |
assert_aoti_parity (compiles C++) |
autocast |
test_autocast_parity |
assert_autocast_parity |
golden |
test_golden |
golden file __golden__/<ClassName>.pt |
output_spec |
test_output_spec |
assert_output_spec with the class's output_spec; B = batch size |
checkpoint |
test_checkpoint_roundtrip |
assert_checkpoint_roundtrip |
layouts |
test_layout_invariance |
assert_layout_invariant |
frozen |
test_frozen |
assert_frozen with frozen / frozen_parameters(); needs loss() |
training |
test_gradients_finite, test_all_params_receive_grad, test_loss_decreases, test_overfits_one_batch |
training sanity; needs loss() |
Checks you don't list don't exist on the class at all (no wall of skips). Compile, export and AOTInductor tests skip on platforms where dynamo is unsupported.
Options¶
check_options passes keyword arguments to the assertion behind a test, keyed by the test
name without test_:
check_options = {
"inference_contract": {"skip": ["device_parity"], "dtypes": (torch.float16,)},
"compile_parity": {"backend": "inductor"},
"all_params_receive_grad": {"ignore": ["pooler.*"]},
"overfits_one_batch": {"steps": 500, "lr": 3e-3},
}
dynamic_batch = True makes test_export_parity export with a dynamic batch dimension and
run a smaller batch through the same program (the example batch must be at least 4).
Mistakes such as an unknown check, an unknown option key, or "training" without a
loss() method fail at collection with a message saying what to fix.
One suite per config¶
Projects with many configs (Hydra / habitat-baselines style YAML files, one per experiment) can run every generated test once per config: each config builds a different model, and option combinations are where bugs hide.
from pathlib import Path
class TestNavPolicy(ModelSuite):
configs = sorted(Path("configs").rglob("*.yaml"))
# Shrink every config so tests stay fast (dotted keys into the loaded config).
config_overrides = {"habitat_baselines.rl.ddppo.hidden_size": 32}
checks = ("contract", "output_spec", "checkpoint", "frozen", "training")
def make_model(self):
return NavPolicy(self.config) # the current config, overrides applied
def make_inputs(self, make_tensor):
n = self.config["habitat"]["task"]["num_object_categories"]
return {"rgb": make_tensor(4, 6, 3, 32, 32), "objectgoal": make_tensor(4, 6, dtype=torch.long, high=n)}
def frozen_parameters(self): # what `train_encoder: false` freezes
return [] if self.config["habitat_baselines"]["rl"]["ddppo"]["train_encoder"] else ["visual_encoder.*"]
test_frozen[transformer_rope_frozen-cpu] PASSED
test_frozen[gru_finetune-cpu] SKIPPED (nothing is frozen in this configuration)
configsholds paths (.yaml/.ymlneed PyYAML,.json) or Python objects; test ids use the file name (config0, ... for objects). An empty list is an error, so a wrong glob can't silently run nothing.- Override
load_config(self, source)to compose with Hydra or OmegaConf instead, andconfig_id(cls, source)for other ids. Aconfig_overrideskey that doesn't exist in a config fails with the key's name. self.configis set for generated tests and for your own: request themodel_configfixture in a hand-written test method to run it once per config too.- With configs, golden files are named
<ClassName>[<config>].pt, one per config.
Customizing¶
Generated tests are ordinary methods: override one to change it, or add your own next to
them. make_model() runs after torch_seed, so weights are reproducible;
make_inputs(make_tensor) gets the seeded, device-aware tensor factory.