Skip to content
Back to skills

Pytorch Patterns

ASecurity

Idiomatic PyTorch for robust, reproducible, memory-conscious training pipelines: device-agnostic placement, full seed control, explicit tensor shape tracking, clean nn.Module construction, weight initialisation, correct train/eval mode discipline, the standard training and validation loops, efficient Dataset/DataLoader configuration, variable-length collation, resumable checkpointing, and the performance levers (mixed precision, gradient checkpointing, torch.compile). Front-loads the autograd...

  • 2 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 2, 2026
ai-agentspythonrustgodebuggingbackendsecurityperformance

Works with

  • cli

Security analysis

A100/100

Scanned September 2, 2026

npx -y skills add Canhada-Labs/ceo-orchestration --skill pytorch-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Pytorch Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Pytorch Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/canhada-labs-pytorch-patterns/badge)](https://www.skillsdirectory.com/skills/canhada-labs-pytorch-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: pytorch-patterns
description: >
  Idiomatic PyTorch for robust, reproducible, memory-conscious training
  pipelines: device-agnostic placement, full seed control, explicit tensor
  shape tracking, clean nn.Module construction, weight initialisation,
  correct train/eval mode discipline, the standard training and validation
  loops, efficient Dataset/DataLoader configuration, variable-length
  collation, resumable checkpointing, and the performance levers (mixed
  precision, gradient checkpointing, torch.compile). Front-loads the
  autograd- and correctness-breaking anti-patterns: forgetting eval() at
  validation, in-place ops severing the graph, .item() before backward,
  moving the model to GPU inside the loop, and the pickle-RCE risk of loading
  checkpoints without weights_only. Use when writing or reviewing models,
  training scripts, or data pipelines, or when tuning GPU memory and speed.
version: 1.0.0
metadata:
  activation_triggers:
    - "torch|pytorch"
    - "nn\\.Module"
    - "DataLoader|Dataset"
    - "optimizer|zero_grad|backward"
    - "autocast|GradScaler|amp"
    - "torch\\.compile|checkpoint"
    - "cuda|state_dict|weights_only"
  paths:
    - "**/*.py"
  risk_class: low
  domain: data-ml
source: affaan-m/ecc@81af4076 skills/pytorch-patterns/
license: MIT
---

# PyTorch Patterns

Idiomatic PyTorch for training code that is portable across hardware,
reproducible run-to-run, and honest about GPU memory. The through-line: keep
the model device-agnostic, control every source of randomness, track tensor
shapes explicitly, and never let a convenience call quietly detach the
autograd graph.

## When to Activate

- Writing a new model, training script, or data pipeline.
- Reviewing deep-learning code for correctness or efficiency.
- Debugging a training loop, a data loader, or a loss that will not move.
- Tuning GPU memory footprint or training throughput.
- Setting up an experiment that must be reproducible.

## Version Note

The AMP and compilation surfaces have moved. This skill uses the current
`torch.amp` namespace (`torch.amp.autocast("cuda", ...)`,
`torch.amp.GradScaler("cuda")`) rather than the older `torch.cuda.amp`, and
`torch.compile` (available from 2.0). If you are on an older build, the
concepts hold but the import paths differ — check `torch.__version__`.

Type hints below use `typing.Optional[...]` / `typing.Tuple[...]` rather than
PEP 604 `X | None`, so the snippets stay valid on Python 3.9. Add
`from __future__ import annotations` at the top of a module if you prefer the
`|` spelling on 3.9.

## Core Principles

### 1. Device-agnostic placement

Resolve the device once, then move the model and every batch onto it. Never
hardcode `.cuda()` — it turns "no GPU available" into a crash instead of a
CPU fallback.

```python
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = MyModel().to(device)
# per batch:
data = data.to(device)

# Avoid: hard-fails on any CPU-only machine
model = MyModel().cuda()
```

### 2. Reproducibility first

Seed every RNG your pipeline touches — PyTorch (CPU and CUDA), NumPy, and
Python's `random`. For bit-reproducible runs also force deterministic cuDNN,
accepting the throughput cost.

```python
def set_seed(seed: int = 42) -> None:
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    np.random.seed(seed)
    random.seed(seed)
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False  # benchmark=True is faster but non-deterministic
```

`cudnn.benchmark = True` auto-tunes convolution algorithms and is faster for
fixed input sizes — but it makes runs non-deterministic. Pick one; do not
expect both.

### 3. Explicit shape management

Annotate the shape after each transform in `forward`. A one-line comment per
step turns a silent `view`/`reshape` mismatch into something you can read.

```python
def forward(self, x: torch.Tensor) -> torch.Tensor:
    # x: (N, C, H, W)
    x = self.conv1(x)            # (N, 32, H, W)
    x = self.pool(x)             # (N, 32, H//2, W//2)
    x = x.view(x.size(0), -1)    # (N, 32 * H//2 * W//2)
    return self.fc(x)            # (N, num_classes)
```

Prefer `reshape` over `view` when a tensor may be non-contiguous (e.g. after
a `permute` or `transpose`); `view` raises on non-contiguous memory.

## Model Architecture

### Build submodules in `__init__`, not `forward`

Every learnable layer must be constructed once in `__init__` so its
parameters register with the module. Constructing a layer (or its weights)
inside `forward` creates fresh, untrained parameters on every call and they
never appear in `model.parameters()`.

```python
class ImageClassifier(nn.Module):
    def __init__(self, num_classes: int, dropout: float = 0.5) -> None:
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2),
        )
        self.classifier = nn.Sequential(
            nn.Dropout(dropout),
            nn.Linear(64 * 16 * 16, num_classes),
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x = self.features(x)
        x = x.view(x.size(0), -1)
        return self.classifier(x)
```

### Weight initialisation

Initialise explicitly rather than trusting layer defaults, especially for
deep nets. `model.apply(fn)` walks every submodule.

```python
def init_weights(module: nn.Module) -> None:
    if isinstance(module, nn.Linear):
        nn.init.kaiming_normal_(module.weight, mode="fan_out", nonlinearity="relu")
        if module.bias is not None:
            nn.init.zeros_(module.bias)
    elif isinstance(module, nn.Conv2d):
        nn.init.kaiming_normal_(module.weight, mode="fan_out", nonlinearity="relu")
    elif isinstance(module, nn.BatchNorm2d):
        nn.init.ones_(module.weight)
        nn.init.zeros_(module.bias)

model.apply(init_weights)
```

## Training Loops

### Train step

Set train mode, clear grads with `set_to_none=True` (frees the buffers and is
marginally faster), run the forward under autocast when using mixed
precision, clip gradients, and step. Accumulate the loss as a Python float
with `.item()` for logging only.

```python
from typing import Optional

def train_one_epoch(
    model: nn.Module,
    dataloader: DataLoader,
    optimizer: torch.optim.Optimizer,
    criterion: nn.Module,
    device: torch.device,
    scaler: Optional[torch.amp.GradScaler] = None,
) -> float:
    model.train()  # dropout on, BatchNorm updates running stats
    total_loss = 0.0

    for data, target in dataloader:
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad(set_to_none=True)

        with torch.amp.autocast("cuda", enabled=scaler is not None):
            output = model(data)
            loss = criterion(output, target)

        if scaler is not None:
            scaler.scale(loss).backward()
            scaler.unscale_(optimizer)  # unscale before clipping
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
            scaler.step(optimizer)
            scaler.update()
        else:
            loss.backward()
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
            optimizer.step()

        total_loss += loss.item()

    return total_loss / len(dataloader)
```

Note the ordering under AMP: `scaler.scale(loss).backward()`, then
`scaler.unscale_(optimizer)` **before** `clip_grad_norm_`, then
`scaler.step` / `scaler.update`. Clipping scaled gradients clips the wrong
magnitude.

### Validation step

Set eval mode and disable gradients. `@torch.no_grad()` as a decorator is
cleaner than wrapping the body and covers early returns.

```python
from typing import Tuple

@torch.no_grad()
def evaluate(
    model: nn.Module,
    dataloader: DataLoader,
    criterion: nn.Module,
    device: torch.device,
) -> Tuple[float, float]:
    model.eval()  # dropout off, BatchNorm uses running stats
    total_loss, correct, total = 0.0, 0, 0

    for data, target in dataloader:
        data, target = data.to(device), target.to(device)
        output = model(data)
        total_loss += criterion(output, target).item()
        correct += (output.argmax(1) == target).sum().item()
        total += target.size(0)

    return total_loss / len(dataloader), correct / total
```

`model.eval()` and `torch.no_grad()` are independent and you need both:
`eval()` switches layer behaviour (dropout, BatchNorm), `no_grad()` stops
building the autograd graph. Forgetting `eval()` is the most common silent
validation bug — see Anti-Patterns.

## Data Pipeline

### Custom Dataset

A `Dataset` needs `__len__` and `__getitem__`. Keep per-item work light and
push heavy transforms into the loader's workers.

```python
from typing import Optional, Tuple

class ImageDataset(Dataset):
    def __init__(
        self,
        image_dir: str,
        labels: dict,
        transform: Optional[transforms.Compose] = None,
    ) -> None:
        self.image_paths = list(Path(image_dir).glob("*.jpg"))
        self.labels = labels
        self.transform = transform

    def __len__(self) -> int:
        return len(self.image_paths)

    def __getitem__(self, idx: int) -> Tuple[torch.Tensor, int]:
        img = Image.open(self.image_paths[idx]).convert("RGB")
        label = self.labels[self.image_paths[idx].stem]
        if self.transform:
            img = self.transform(img)
        return img, label
```

### DataLoader configuration

The defaults (`num_workers=0`, no pinned memory) leave the GPU starved. Load
in parallel workers, pin host memory for a faster host→device copy, and keep
workers alive across epochs.

```python
dataloader = DataLoader(
    dataset,
    batch_size=32,
    shuffle=True,             # training only; never shuffle eval
    num_workers=4,            # parallel loading
    pin_memory=True,          # faster host -> GPU copy
    persistent_workers=True,  # don't respawn workers each epoch
    drop_last=True,           # stable batch size (matters for BatchNorm)
)
```

### Variable-length collation

For sequences, pad to the batch's max length in a `collate_fn`.

```python
from typing import List, Tuple

def collate_fn(
    batch: List[Tuple[torch.Tensor, int]],
) -> Tuple[torch.Tensor, torch.Tensor]:
    sequences, labels = zip(*batch)
    padded = nn.utils.rnn.pad_sequence(sequences, batch_first=True, padding_value=0)
    return padded, torch.tensor(labels)

dataloader = DataLoader(dataset, batch_size=32, collate_fn=collate_fn)
```

## Checkpointing

Save the full training state — model, optimizer, epoch, metrics — so a run
can resume, not just weights. On load, pass `weights_only=True` (see the
security note) and map to CPU first so a checkpoint saved on GPU loads on any
machine.

```python
def save_checkpoint(model, optimizer, epoch: int, loss: float, path: str) -> None:
    torch.save(
        {
            "epoch": epoch,
            "model_state_dict": model.state_dict(),
            "optimizer_state_dict": optimizer.state_dict(),
            "loss": loss,
        },
        path,
    )

def load_checkpoint(path: str, model, optimizer=None) -> dict:
    checkpoint = torch.load(path, map_location="cpu", weights_only=True)
    model.load_state_dict(checkpoint["model_state_dict"])
    if optimizer is not None:
        optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
    return checkpoint
```

> Security: `torch.load` historically unpickled arbitrary objects, so a
> malicious checkpoint could execute code on load. Always pass
> `weights_only=True` for anything you did not produce yourself, and treat
> third-party checkpoints as untrusted input. Save with `state_dict()`, not
> the whole model object — pickling a full `nn.Module` binds the file to your
> exact class layout and is far more fragile.

## Performance Levers

### Mixed precision (AMP)

Run the forward and loss in lower precision under `autocast`, and use a
`GradScaler` so small gradients do not underflow to zero. Often ~2x
throughput and roughly half the activation memory on supported GPUs.

```python
scaler = torch.amp.GradScaler("cuda")
for data, target in dataloader:
    with torch.amp.autocast("cuda"):
        output = model(data)
        loss = criterion(output, target)
    scaler.scale(loss).backward()
    scaler.step(optimizer)
    scaler.update()
    optimizer.zero_grad(set_to_none=True)
```

### Gradient checkpointing (trade compute for memory)

Recompute intermediate activations during the backward pass instead of
holding them — lets a larger model fit at the cost of extra forward compute.

```python
from torch.utils.checkpoint import checkpoint

class LargeModel(nn.Module):
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x = checkpoint(self.block1, x, use_reentrant=False)
        x = checkpoint(self.block2, x, use_reentrant=False)
        return self.head(x)
```

Pass `use_reentrant=False` — the reentrant path is legacy and interacts badly
with some inputs.

### torch.compile (2.0+)

JIT-compile the model into fused kernels. Wrap once after moving to device.

```python
model = torch.compile(MyModel().to(device), mode="reduce-overhead")
# modes: "default" (safe) · "reduce-overhead" · "max-autotune" (slowest to compile)
```

The first step or two pays a compilation cost; steady-state is faster.

## Quick Reference

| Idiom | Purpose |
|---|---|
| `model.train()` / `model.eval()` | Switch layer behaviour before the loop |
| `torch.no_grad()` / `@torch.no_grad()` | Stop building the graph for inference |
| `optimizer.zero_grad(set_to_none=True)` | Cheaper, more thorough grad clearing |
| `.to(device)` | Device-agnostic placement |
| `torch.amp.autocast` + `GradScaler` | Mixed precision without gradient underflow |
| `pin_memory=True` | Faster host→GPU transfer |
| `torch.compile` | Kernel fusion for speed (2.0+) |
| `weights_only=True` | Safe checkpoint loading |
| `torch.manual_seed` (+ NumPy, random) | Reproducible runs |
| `torch.utils.checkpoint` | Fit larger models by recomputing activations |

## Anti-Patterns

```python
# 1) Forgetting eval() — dropout still fires, BatchNorm uses batch stats
model.train()
with torch.no_grad():
    output = model(val_data)          # WRONG: metrics are noisy and wrong
# Right:
model.eval()
with torch.no_grad():
    output = model(val_data)

# 2) In-place ops that sever autograd
x = F.relu(x, inplace=True)           # can corrupt the graph
x += residual                          # in-place add on a graph tensor
# Right:
x = F.relu(x)
x = x + residual

# 3) Moving the model to GPU inside the loop
for data, target in dataloader:
    model = model.cuda()               # re-copies the model every iteration
# Right: move once, before the loop
model = model.to(device)
for data, target in dataloader:
    data, target = data.to(device), target.to(device)

# 4) .item() before backward — detaches from the graph
loss = criterion(output, target).item()
loss.backward()                        # error: a float has no backward()
# Right: keep the tensor; call .item() only to log
loss = criterion(output, target)
loss.backward()
print(f"loss={loss.item():.4f}")

# 5) Saving the whole model object
torch.save(model, "model.pt")          # fragile, tied to class layout
# Right: save the state_dict
torch.save(model.state_dict(), "model.pt")
```

When memory or speed is the question, measure before guessing: profile with
`torch.profiler` and read the allocator with `torch.cuda.memory_summary()`.

## Changelog

- 1.0.0 — Initial clean-room authoring. Covers device-agnostic placement,
  full seed control, explicit shape tracking, module construction and weight
  init, train/eval discipline, the standard train and validation loops,
  Dataset/DataLoader configuration and variable-length collation, resumable
  checkpointing with the `weights_only` security note, the performance levers
  (AMP, gradient checkpointing, `torch.compile`), and the five autograd- and
  correctness-breaking anti-patterns. Type hints written for Python 3.9
  compatibility (`typing.Optional`/`Tuple`, current `torch.amp` namespace).
Skill-Import-Attestation: reviewed-by=AE9B236FDAF0462874060C6BCFCFACF00335DC74; sha256=9bd2169e09e8cfc7677ffbbfd5e7186d1782fa8be257d42eb69a17d2d44f4778

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.