Skip to main content
rule

pytorch — Cursor rule: what it does, how to install

Enforces clean PyTorch patterns across Python files in Cursor. Instructs the assistant to separate modules, place tensors on devices explicitly, and disable gradients in inference.

Install into Cursor · Manually: put the files in place All methods and platforms ↓
.cursor/rules/pytorch.mdc
Open on GitHub

What pytorch does

The rule targets all **/*.py files and stops the assistant from writing monolithic training scripts. It requires splitting the workflow into distinct modules for data loading, model architecture, training, and evaluation. In data pipelines, it sets num_workers=4 and pin_memory=True inside the DataLoader.

Device management must remain explicit. The rule forces .to(device) calls for models and batches instead of relying on default CPU placement. During model evaluation, wrapping inference in torch.no_grad() is strictly required.

In-place operations like .add_() are forbidden. They corrupt the PyTorch autograd computation graph. Such mutations are permitted only when profiling proves a critical performance bottleneck.

What it helps with

  • Split monolithic ML scripts into modular components
  • Prevent inference memory leaks by applying torch.no_grad()
  • Enforce explicit tensor allocation on GPU via .to(device)
  • Protect the autograd computation graph against in-place mutations

Example prompts for the agent

  • Create a modular PyTorch training pipeline with separate data loaders and model definition
  • Implement an evaluation loop for my PyTorch CNN using explicit device placement

How to install pytorch

Manually: put the files in place

.cursor/rules/pytorch.mdc

Frontmatter: description, globs, alwaysApply

Step by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules

Questions about pytorch

How do I install the pytorch rule in Cursor?

Save the rule file to .cursor/rules/pytorch.mdc in your project root.

What PyTorch bugs does this rule prevent?

It eliminates silent CPU fallback, stops in-place mutations that break autograd, and prevents gradient computation during evaluation.

Which files trigger this rule in Cursor?

Cursor applies the rule automatically to all Python files matching the **/*.py glob pattern.

Source text

pytorch.mdc · CC0-1.0
---
description: Definitive guidelines for writing clean, performant, and maintainable PyTorch code, emphasizing modern best practices, explicit device management, and efficient training patterns.
globs: **/*.py
---
# PyTorch Best Practices

This guide outlines the definitive best practices for developing with PyTorch, ensuring your code is readable, performant, and production-ready. We prioritize usability, explicit control, and modern tooling.

## 1. Code Organization and Structure

Structure your PyTorch projects for clarity, testability, and scalability. Encapsulate logical blocks into distinct functions or classes.

### 1.1. Modularize Your Codebase

Separate data loading, model definition, training, and evaluation into dedicated modules or functions. This makes components reusable and testable.

❌ BAD: Monolithic script
```python
# train.py
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset

# ... data loading, model definition, training loop all in one file ...

class MyModel(nn.Module):
    # ...
    pass

def main():
    # Data loading
    train_data = TensorDataset(...)
    train_loader = DataLoader(train_data, batch_size=32)

    # Model, optimizer, loss
    model = MyModel()
    optimizer = torch.optim.Adam(model.parameters())
    criterion = nn.CrossEntropyLoss()

    # Training loop
    for epoch in range(10):
        for batch_idx, (data, target) in enumerate(train_loader):
            # ... training logic ...
            pass

if __name__ == "__main__":
    main()
```

✅ GOOD: Modularized structure
```python
# src/data.py
import torch
from torch.utils.data import DataLoader, TensorDataset

def get_dataloaders(batch_size: int) -> tuple[DataLoader, DataLoader]:
    # Example: Create synthetic data
    X = torch.randn(1000, 784)
    y = torch.randint(0, 10, (1000,))
    train_dataset = TensorDataset(X, y)
    val_dataset = TensorDataset(X[:100], y[:100]) # Smaller val set
    train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4, pin_memory=True)
    val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, num_workers=4, pin_memory=True)
    return train_loader, val_loader

# src/model.py
import torch.nn as nn

class SimpleCNN(nn.Module):
    def __init__(self, num_classes: int = 10):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(1, 32, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Conv2d(32, 64, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(64 * 7 * 7, 128), # Assuming 28x28 input, adjust for other sizes
            nn.ReLU(),
            nn.Linear(128, num_classes)
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x = self.features(x)
        x = self.classifier(x)
        return x

# src/train.py
import torch
import torch.nn as nn
from torch.optim import Adam
from src.model import SimpleCNN
from src.data import get_dataloaders

def train_epoch(model: nn.Module, loader: DataLoader, optimizer: Adam, criterion: nn.Module, device: torch.device) -> float:
    model.train()
    total_loss = 0.0
    for data, target in loader:
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
    return total_loss / len(loader)

def evaluate_model(model: nn.Module, loader: DataLoader, device: torch.device) -> float:
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for data, target in loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            _, predicted = torch.max(output.data, 1)
            total += target.size(0)
            correct += (predicted == target).sum().item()
    return 100 * correct / total

# main.py
import torch
from src.model import SimpleCNN
from src.data import get_dataloaders
from src.train import train_epoch, evaluate_model

def run_experiment(epochs: int = 10, batch_size: int = 64, lr: float = 1e-3):
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    train_loader, val_loader = get_dataloaders(batch_size)
    model = SimpleCNN().to(device)
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    criterion = torch.nn.CrossEntropyLoss()

    for epoch in range(epochs):
        train_loss = train_epoch(model, train_loader, optimizer, criterion, device)
        val_accuracy = evaluate_model(model, val_loader, device)
        print(f"Epoch {epoch+1}: Train Loss = {train_loss:.4f}, Val Acc = {val_accuracy:.2f}%")

    torch.save(model.state_dict(), "final_model.pth")

if __name__ == "__main__":
    run_experiment()
```

## 2. Common Patterns and Anti-patterns

Adopt patterns that enhance clarity and performance, and avoid common pitfalls.

### 2.1. Explicit Device Placement

Always explicitly move tensors and models to the correct device (`cpu` or `cuda`). Never rely on implicit device handling.

❌ BAD: Implicit device assumption
```python
model = MyModel() # Model is on CPU by default
data = torch.randn(1, 3, 224, 224) # Data is on CPU
output = model(data) # This will run on CPU, even if CUDA is available
```

✅ GOOD: Explicit device placement
```python
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = MyModel().to(device)
data = torch.randn(1, 3, 224, 224).to(device)
output = model(data) # Model and data are on the same device
```

### 2.2. Disable Gradient Calculation for Inference

Use `torch.no_grad()` for validation and inference to save memory and speed up computation.

❌ BAD: Calculating unnecessary gradients
```python
model.eval()
for data, target in val_loader:
    output = model(data) # Gradients are still computed and stored
    loss = criterion(output, target)
    # ...
```

✅ GOOD: Disabling gradients
```python
model.eval()
with torch.no_grad():
    for data, target in val_loader:
        output = model(data) # No gradients computed
        loss = criterion(output, target)
        # ...
```

### 2.3. Avoid In-Place Operations (Unless Profiled)

In-place operations (`.add_()`, `.mul_()`, etc.) can break PyTorch's autograd engine and make debugging difficult. Prefer out-of-place operations unless profiling explicitly shows an in-place operation is a critical bottleneck for memory or speed.

❌ BAD: In-place operation
```python
x = torch.randn(5, requires_grad=True)
y = x * 2
x.add_(1) # Modifies x in-place, can cause autograd issues
```

✅ GOOD: Out-of-place operation
```python
x = torch.randn(5, requires_grad=True)
y = x * 2
x = x + 1 # Creates a new tensor, preserving autograd graph
```

## 3. Performance Considerations

Optimize your PyTorch code for speed and memory efficiency.

### 3.1. Leverage `torch.compile`

Use `torch.compile` to significantly accelerate your models by tracing and optimizing the computation graph. Apply it to your `nn.Module` instances.

❌ BAD: Running model in eager mode only
```python
model = MyModel().to(device)
# Training loop
for data, target in loader:
    output = model(data)
    # ...
```

✅ GOOD: Compiling the model
```python
model = MyModel().to(device)
compiled_model = torch.compile(model) # Compile once
# Training loop
for data, target in loader:
    output = compiled_model(data) # Use the compiled model
    # ...
```

### 3.2. Mixed Precision Training with `torch.cuda.amp`

For CUDA devices, use automatic mixed precision (AMP) to halve memory bandwidth and speed up computation without significant accuracy loss.

❌ BAD: Full precision training
```python
optimizer = torch.optim.Adam(model.parameters())
for data, target in loader:
    output = model(data)
    loss = criterion(output, target)
    loss.backward()
    optimizer.step()
```

✅ GOOD: Mixed precision training
```python
from torch.cuda.amp import autocast, GradScaler

scaler = GradScaler()
optimizer = torch.optim.Adam(model.parameters())

for data, target in loader:
    with autocast(): # Operations within this context run in mixed precision
        output = model(data)
        loss = criterion(output, target)

    scaler.scale(loss).backward() # Scale gradients
    scaler.step(optimizer) # Update optimizer
    scaler.update() # Update scaler for next iteration
```

### 3.3. Optimize `DataLoader`

Configure `DataLoader` for efficient asynchronous data loading.

❌ BAD: Default `DataLoader` settings
```python
DataLoader(dataset, batch_size=32) # num_workers=0, pin_memory=False
```

✅ GOOD: Optimized `DataLoader`
```python
DataLoader(
    dataset,
    batch_size=32,
    shuffle=True,
    num_workers=min(os.cpu_count(), 8), # Tune based on CPU cores
    pin_memory=True, # Faster host-to-GPU transfer
    persistent_workers=True # Keep workers alive between epochs
)
```

## 4. Common Pitfalls and Gotchas

Be aware of these common issues to avoid frustrating debugging sessions.

### 4.1. Device Mismatches

Ensure all tensors involved in an operation are on the same device. This is the most frequent error.

❌ BAD: Device mismatch
```python
model = MyModel().cuda()
input_tensor = torch.randn(1, 3, 224, 224).cpu() # Input on CPU
output = model(input_tensor) # ERROR: Expected all tensors to be on the same device
```

✅ GOOD: Consistent device placement
```python
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = MyModel().to(device)
input_tensor = torch.randn(1, 3, 224, 224).to(device)
output = model(input_tensor) # All good
```

### 4.2. Multiprocessing Start Method

When using multiprocessing with CUDA, always set the start method to `"spawn"` or `"forkserver"` to avoid "poison fork" issues.

❌ BAD: Default `fork` start method with CUDA
```python
# This might implicitly use 'fork' on Linux, leading to CUDA errors
import torch.multiprocessing as mp
mp.set_start_method('fork') # Explicitly setting bad practice
```

✅ GOOD: Safe multiprocessing start method
```python
import torch.multiprocessing as mp
try:
    mp.set_start_method('spawn', force=True) # Recommended for cross-platform safety
except RuntimeError:
    pass # Already set
```

## 5. Type Hints

Use type hints extensively for improved readability, maintainability, and static analysis.

❌ BAD: Untyped functions
```python
def train_step(model, data, target, optimizer, criterion):
    # ...
    pass
```

✅ GOOD: Type-hinted functions
```python
import torch
import torch.nn as nn
from torch.optim import Optimizer
from torch.utils.data import DataLoader

def train_step(
    model: nn.Module,
    data: torch.Tensor,
    target: torch.Tensor,
    optimizer: Optimizer,
    criterion: nn.Module
) -> float:
    # ...
    return loss.item()
```

## 6. Virtual Environments

Always use virtual environments (`venv` or `conda`) to manage dependencies and ensure reproducible setups.

❌ BAD: Global package installation
```bash
pip install torch torchvision # Pollutes global Python environment
```

✅ GOOD: Virtual environment setup
```bash
# Using venv
python -m venv .venv
source .venv/bin/activate
pip install torch torchvision

# Using conda
conda create -n my_env python=3.10
conda activate my_env
conda install pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia
```

## 7. Packaging

For reusable components or libraries, package your code properly using `setuptools` or `poetry`.

❌ BAD: Just a collection of scripts
```
my_project/
├── train.py
├── model.py
└── data.py
```

✅ GOOD: Packaged structure
```
my_project/
├── pyproject.toml # Or setup.py
├── src/
│   └── my_project_lib/
│       ├── __init__.py
│       ├── model.py
│       ├── data.py
│       └── train_utils.py
├── scripts/
│   └── run_training.py # Entry point that imports from src/
└── tests/
    ├── test_model.py
    └── test_data.py
```

## 8. Testing Approaches

Implement unit tests for individual modules and integration tests for training loops.

### 8.1. Unit Tests for Modules

Test `nn.Module` definitions, data transformations, and utility functions independently.

❌ BAD: No tests, or only end-to-end testing
```python
# No dedicated tests for SimpleCNN
```

✅ GOOD: Unit test for a model
```python
# tests/test_model.py
import unittest
import torch
from src.model import SimpleCNN

class TestSimpleCNN(unittest.TestCase):
    def test_forward_pass(self):
        model = SimpleCNN(num_classes=10)
        input_tensor = torch.randn(1, 1, 28, 28) # Batch, Channels, H, W
        output = model(input_tensor)
        self.assertEqual(output.shape, (1, 10))

    def test_output_range(self):
        model = SimpleCNN(num_classes=2)
        input_tensor = torch.randn(1, 1, 28, 28)
        output = model(input_tensor)
        # Check if output is float
        self.assertTrue(output.dtype == torch.float32)

if __name__ == '__main__':
    unittest.main()
```

### 8.2. Integration Tests for Training Loops

Use small, mock datasets to quickly verify the training loop's functionality without long training times.

❌ BAD: Only testing with full datasets
```python
# Training script only runs with full data, slow to verify changes
```

✅ GOOD: Integration test with mock data
```python
# tests/test_training.py
import unittest
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
from src.model import SimpleCNN
from src.train import train_epoch, evaluate_model

class TestTrainingLoop(unittest.TestCase):
    def test_training_run(self):
        device = torch.device("cpu") # Use CPU for faster testing
        model = SimpleCNN(num_classes=2).to(device)
        optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
        criterion = nn.CrossEntropyLoss()

        # Create a small mock dataset
        mock_X = torch.randn(10, 1, 28, 28)
        mock_y = torch.randint(0, 2, (10,))
        mock_dataset = TensorDataset(mock_X, mock_y)
        mock_loader = DataLoader(mock_dataset, batch_size=2)

        initial_loss = train_epoch(model, mock_loader, optimizer, criterion, device)
        # Run a second epoch to ensure loss decreases (basic sanity check)
        second_epoch_loss = train_epoch(model, mock_loader, optimizer, criterion, device)

        self.assertLess(second_epoch_loss, initial_loss * 1.1) # Expect some decrease or stability
        accuracy = evaluate_model(model, mock_loader, device)
        self.assertGreaterEqual(accuracy, 0.0) # Basic check that accuracy is calculated

if __name__ == '__main__':
    unittest.main()
```

© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc

Similar IDE rules

All IDE rules for AI and ML →

From the same set240

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor