blob: d340da6ebff97495447d943fc8018a089029d04a [file] [view] [edit]
# Contributing to Tensor Shape Support
Pyrefly's tensor shape tracking is designed so most PyTorch coverage can be
extended by editing stubs and tests, without changing Pyrefly's Rust internals.
This page explains the main mechanisms and how to validate changes.
Most external contributions should be stub-only or example/test-only changes.
Kernel changes are possible, but they are a narrower workflow for changes to
Pyrefly's shape machinery or the `shape_extensions` runtime package.
## Architecture Overview
Shape tracking uses three complementary mechanisms:
1. **Fixture stubs**: `.pyi` files with shape-generic type signatures. These
cover modules like `nn.Linear`, `nn.Conv2d`, and functions like `torch.mm`.
2. **Shape DSL functions**: shape transforms written in a small Python subset in
`tensor-shapes/pyrefly-torch-stubs/torch-stubs/_shapes.pyi`, decorated with
`@shape_dsl_function`, and attached to stubs with `@uses_shape_dsl(...)`.
These cover operations with computed shape logic like `reshape`, `cat`,
pooling, convolution, and interpolation.
3. **Special handlers**: Pyrefly implementation logic for constructs that need
deeper type system integration, like `nn.Sequential` chaining, `.shape`,
`.size()`, `assert_shape`, and decorator interpretation.
The first two mechanisms live in `tensor-shapes/` and are the normal way to add
or improve shape coverage. Special handlers require Pyrefly implementation
changes and should be treated as kernel work.
## Fixture Stubs
### Where They Live
```text
tensor-shapes/pyrefly-torch-stubs/torch-stubs/
|-- __init__.pyi
|-- _shapes.pyi
|-- nn/
| |-- __init__.pyi # nn.Linear, nn.Conv2d, nn.LSTM, etc.
| `-- functional.pyi # F.relu, F.softmax, F.conv2d, etc.
|-- distributions/
| `-- ... # torch.distributions
`-- ...
```
The tensor-shape test runner passes `tensor-shapes/` as a Pyrefly search path,
so these stubs override the normal `torch` stubs during validation.
### How Stubs Work
A fixture stub provides a shape-generic type signature. For example,
`nn.Linear`:
```python
class Linear[N, M](Module):
def __init__(
self,
in_features: SymInt[N],
out_features: SymInt[M],
bias: bool = True,
) -> None: ...
def forward[*Xs](self, input: Tensor[*Xs, N]) -> Tensor[*Xs, M]: ...
```
The constructor captures input and output dimensions as type parameters. The
`forward` method uses those parameters plus a variadic `*Xs` for batch
dimensions.
### Writing a New Stub
1. Identify the shape signature: input dimensions, output dimensions, and how
they relate.
2. Use `SymInt[X]` for parameters that determine tensor dimensions. Non-shape
parameters like `bias` and `dropout` stay as their original types.
3. Write the method or function signature expressing the shape transform. Use
`*Xs` or `*Bs` for batch dimensions that pass through unchanged.
4. Add the stub to the appropriate `.pyi` file in `tensor-shapes/pyrefly-torch-stubs/torch-stubs`.
5. Add or update focused tests under `tensor-shapes/pyrefly-torch-stubs/test/`.
### Example: Adding a New Module
Suppose you want to add `nn.GroupNorm`, which preserves spatial dimensions:
```python
class GroupNorm[NumGroups, NumChannels](Module):
def __init__(
self,
num_groups: SymInt[NumGroups],
num_channels: SymInt[NumChannels],
eps: float = 1e-5,
affine: bool = True,
) -> None: ...
def forward[*S](self, input: Tensor[*S]) -> Tensor[*S]: ...
```
Since `GroupNorm` does not change shape, the forward signature is simply
`Tensor[*S] -> Tensor[*S]`.
## Shape DSL Functions
Use the DSL when a plain signature cannot express the output shape.
### Where They Live
DSL functions live in:
```text
tensor-shapes/pyrefly-torch-stubs/torch-stubs/_shapes.pyi
```
Stubs attach a DSL function with `@uses_shape_dsl(...)`. For example, a stub may
declare a broad return type like `Tensor` and let the DSL refine the result shape
at call sites:
```python
from shape_extensions import uses_shape_dsl
from torch._shapes import reshape_ir
@uses_shape_dsl(reshape_ir)
def reshape(self: Tensor, shape: tuple[int, ...]) -> Tensor: ...
```
### The DSL Subset
The DSL is intentionally small. It supports common shape computation patterns,
including:
- `ShapedArray(shape=[...])` to construct result shapes
- `self.shape` and other shaped-array argument shapes
- Lists, slices, indexing, and comprehensions
- Arithmetic such as `+`, `-`, `*`, `//`, `%`, and `**`
- `if` / `else`
- Helper calls to other `@shape_dsl_function` functions
- DSL helpers from `shape_extensions.dsl`, such as `prod`, `sum`, `Unknown`,
and `Error`
Keep DSL functions simple and algebraic. They are analyzed by Pyrefly; they are
not normal runtime implementations of PyTorch operations.
### Example: `torch.cat`
```python
@shape_dsl_function
def cat_ir(tensors: list[ShapedArray], dim: int = 0) -> ShapedArray:
first = tensors[0]
d = normalize_dim(len(first.shape), dim)
return ShapedArray(
shape=[
sum([t.shape[i] for t in tensors]) if i == d else dim_val
for i, dim_val in enumerate(first.shape)
]
)
```
This sums shapes along the concatenation dimension and preserves all others.
### Adding a New DSL Function
1. Write the shape transform in `tensor-shapes/pyrefly-torch-stubs/torch-stubs/_shapes.pyi`.
2. Decorate it with `@shape_dsl_function`.
3. Attach it to the public stub with `@uses_shape_dsl(...)`.
4. Add positive tests that assert the computed shape.
5. Add negative tests with `# E:` expectations if the DSL should reject invalid
shapes or report shape errors.
## Ported Models
### Where They Live
```text
tensor-shapes/pyrefly-torch-stubs/examples/
```
Each file is a fully annotated port of a real-world PyTorch model with
`assert_type` checkpoints and smoke tests.
### Adding a New Model
1. Choose a model from [TorchBench](https://github.com/pytorch/benchmark) or
another source.
2. Port it using the
[tutorials](https://pyrefly.org/en/docs/tensor-shapes-tutorial-basics/) or
the [agent skill](https://pyrefly.org/en/docs/tensor-shapes-ai-porting/).
3. Add `assert_type` or `assert_shape` checkpoints after shape-changing
operations.
4. Add smoke tests at the bottom of the file when runtime execution is useful.
5. Run `verify_port.sh` to check for common quality issues.
### `verify_port.sh`
This script checks a ported model for common issues:
```bash
tensor-shapes/skills/add-shape-types-to-torch-model/verify_port.sh tensor-shapes/pyrefly-torch-stubs/examples/<model>.py
```
It reports:
| Metric | Description |
|--------|-------------|
| `ig` | `type: ignore` count |
| `bs` | Bare `Tensor` in signatures |
| `bv` | Bare `Tensor` in variable annotations |
| `sh` | Shaped `assert_type` count |
| `ba` | Bare `assert_type` count |
| `sm` | Smoke test count |
## Testing Stub and Example Changes
For most contributions, the important validation is the tensor-shape Pyrefly
runner. It checks the focused tests, negative expectations, jaxtyping examples,
and the example corpus using the shape-aware stubs.
Build Pyrefly first, then run:
```bash
cargo build
python3 tensor-shapes/pyrefly-torch-stubs/run_pyrefly.py
```
If your build uses a custom target directory, `run_pyrefly.py` respects
`CARGO_TARGET_DIR`. You can also pass the binary explicitly:
```bash
python3 tensor-shapes/pyrefly-torch-stubs/run_pyrefly.py --pyrefly /path/to/pyrefly
```
Run a single suite while iterating:
```bash
python3 tensor-shapes/pyrefly-torch-stubs/run_pyrefly.py --suite torch-positive
python3 tensor-shapes/pyrefly-torch-stubs/run_pyrefly.py --suite torch-negative
python3 tensor-shapes/pyrefly-torch-stubs/run_pyrefly.py --suite torch-examples
```
Use `--nocapture` when you want the full Pyrefly output on success. By default,
the runner prints a compact `PASS ...` line and only dumps checker output on
failure.
In an internal Buck checkout, the equivalent static validation targets are:
```bash
buck test tensor-shapes/pyrefly-torch-stubs/test:tensor_shapes_all_test
buck test tensor-shapes/pyrefly-torch-stubs/test:tensor_shapes_error_test
buck test tensor-shapes/pyrefly-torch-stubs/test:tensor_shapes_jaxtyping_test
buck test tensor-shapes/pyrefly-torch-stubs/test:tensor_shapes_jaxtyping_error_test
buck test tensor-shapes/pyrefly-torch-stubs/examples:torch_examples_test
```
The project-level `test.py` runner keeps tensor-shape validation separate from
the default Pyrefly test loop. To run just these validations through `test.py`:
```bash
python3 test.py --no-fmt --no-lint --no-test --tensor-shapes --no-conformance --no-jsonschema
```
## Runtime Tests
Runtime tests validate that the annotation helpers and runnable example models
behave correctly in Python, not just in Pyrefly's static checker.
The tests live in:
```text
tensor-shapes/pyrefly-torch-stubs/test/runtime_tests/
```
Run them from a Python 3.12+ virtualenv with `torch` installed:
```bash
python3.12 -m venv .tensor-shapes-venv
. .tensor-shapes-venv/bin/activate
python -m pip install --upgrade pip
python -m pip install torch
python tensor-shapes/pyrefly-torch-stubs/run_runtime_tests.py
```
Run one suite while iterating:
```bash
python tensor-shapes/pyrefly-torch-stubs/run_runtime_tests.py --suite annotation
python tensor-shapes/pyrefly-torch-stubs/run_runtime_tests.py --suite model
```
The runtime runner sets up import paths for `shape_extensions` and the runnable
example modules. In an internal Buck checkout, the existing runtime targets are:
```bash
buck test tensor-shapes/pyrefly-torch-stubs/test:annotation_runtime_test
buck test tensor-shapes/pyrefly-torch-stubs/test:model_runtime_test
```
## Kernel Tests
Most contributors should not need this section. Use these tests when you change
Pyrefly's tensor-shape kernel rather than only stubs or examples. Kernel changes
include:
- `shape_extensions` primitives or decorators
- `assert_shape` type-checker behavior
- `@shape_dsl_function` parsing, validation, or evaluation
- `@uses_shape_dsl` handling
- special handlers in Pyrefly's Rust source
The focused Pyrefly unit tests live in:
```text
pyrefly/lib/test/shape_dsl.rs
```
Run them with Cargo:
```bash
cargo test shape_dsl
```
In an internal Buck checkout:
```bash
buck test pyrefly:pyrefly_library -- shape_dsl
```
Kernel tests are intentionally much smaller than the stub/example suites. They
cover the core primitives and invariants; the tensor-shape stub tests stress
the DSL through realistic PyTorch signatures.
## Pre-Commit Checks
Before handing off changes, run formatting and linting:
```bash
./test.py --no-test --no-tensor-shapes --no-conformance --no-jsonschema
```
Also run the relevant tensor-shape checks for the files you touched:
- Stub/test/example changes: `python3 tensor-shapes/pyrefly-torch-stubs/run_pyrefly.py`
- Runtime helper or runnable model changes:
`python tensor-shapes/pyrefly-torch-stubs/run_runtime_tests.py`
- Kernel changes: `cargo test shape_dsl` or the Buck equivalent above