PyTorch Cheatsheet

Autograd

Use this PyTorch reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Core Concepts

PyTorch's autograd builds a dynamic computation graph as operations execute. Each tensor tracks its history; calling .backward() traverses the graph in reverse and accumulates gradients into .grad.

x = torch.tensor(3.0, requires_grad=True)
y = x ** 2 + 2 * x + 1   # y = (x+1)^2
y.backward()              # dy/dx = 2x + 2 = 8 at x=3
print(x.grad)             # tensor(8.)

requires_grad

# Set at creation
x = torch.ones(3, requires_grad=True)
x = torch.tensor([1.0], requires_grad=True)

# Toggle after creation
x.requires_grad_(True)    # in-place
x.requires_grad_(False)

# Leaf tensors created by the user have requires_grad=False by default
# unless you set it. Intermediate (non-leaf) tensors always track grad.

print(x.is_leaf)          # True if user-created or detached
print(x.grad_fn)          # None for leaf tensors

backward()

loss = model(x)           # scalar output
loss.backward()           # computes dloss/d(all_leaves_with_requires_grad)

# Non-scalar: must supply gradient argument
t = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
out = t ** 2
out.backward(torch.ones_like(out))  # equivalent to out.sum().backward()

# Retain graph for multiple backward passes
loss.backward(retain_graph=True)    # graph not freed after first call

# Accumulate grads by calling backward() multiple times
for batch in batches:
    loss = compute_loss(batch)
    loss.backward()          # grads accumulate in .grad
optimizer.step()
optimizer.zero_grad()

Gradient Accumulation and Zeroing

optimizer.zero_grad()           # zero before each forward pass (typical)
optimizer.zero_grad(set_to_none=True)  # sets .grad = None instead of 0.0
                                       # slightly faster, safe in most cases

# Manual zero
for p in model.parameters():
    if p.grad is not None:
        p.grad.zero_()

Accessing Gradients

x.grad             # accumulated gradient tensor (None until backward called)
x.grad.detach()    # gradient values without autograd tracking (never .data)
x.grad.zero_()     # manual zero

# Higher-order gradients: create_graph=True keeps grad graph alive
grad_x = torch.autograd.grad(y, x, create_graph=True)[0]
grad2_x = torch.autograd.grad(grad_x, x)[0]   # second derivative

torch.no_grad and torch.inference_mode

# Disables grad tracking — no graph built, fastest for inference
with torch.no_grad():
    output = model(x)

# Decorator form
@torch.no_grad()
def predict(x):
    return model(x)

# inference_mode — stricter, even faster (views cannot be used in grad ctx)
with torch.inference_mode():
    output = model(x)

@torch.inference_mode()
def evaluate(x):
    return model(x)

inference_mode is preferred over no_grad for pure inference — it's faster and prevents accidental grad leakage.

Detaching from the Graph

y = x.detach()              # view of x's data, no grad tracking
y = x.detach().clone()      # copy, fully independent

# Common pattern: extracting a value for logging
loss_val = loss.item()      # Python float — safest
loss_cpu = loss.detach().cpu()

torch.autograd.grad

# Compute specific gradients explicitly
grads = torch.autograd.grad(
    outputs=loss,
    inputs=[x, y],
    grad_outputs=None,      # assumes 1.0 for scalars
    create_graph=False,
    retain_graph=False,
    allow_unused=False,     # raise if input not in graph
)
dx, dy = grads

Jacobian and Hessian

from torch.autograd.functional import jacobian, hessian

J = jacobian(func, inputs)       # full Jacobian matrix
H = hessian(func, inputs)        # full Hessian matrix

# Efficient Jacobian-vector product (JVP)
_, jvp = torch.autograd.functional.jvp(func, inputs, v)

# Vector-Jacobian product (VJP) — what backward() does internally
_, vjp = torch.autograd.functional.vjp(func, inputs, v)

Custom Autograd Functions

class MySigmoid(torch.autograd.Function):
    @staticmethod
    def forward(ctx, x):
        s = torch.sigmoid(x)
        ctx.save_for_backward(s)   # save tensors needed in backward
        return s

    @staticmethod
    def backward(ctx, grad_output):
        (s,) = ctx.saved_tensors
        return grad_output * s * (1 - s)   # chain rule

# Usage
y = MySigmoid.apply(x)

ctx.save_for_backward is the only safe way to pass tensors from forward to backward. Storing them as Python attributes on ctx can cause memory leaks.

Gradient Checkpointing

from torch.utils.checkpoint import checkpoint

# Trades compute for memory: recomputes activations during backward
out = checkpoint(layer, x)
out = checkpoint(lambda x: layer1(layer2(x)), x)

# For sequential models
out = checkpoint_sequential(layers, segments=4, input=x)

Gradient Clipping

# Clip by norm (most common — prevents exploding gradients)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

# Clip by value
torch.nn.utils.clip_grad_value_(model.parameters(), clip_value=0.5)

# Check total norm without clipping
total_norm = torch.nn.utils.clip_grad_norm_(
    model.parameters(), max_norm=float('inf')
)

Hooks

# Tensor hook — fires during backward, receives gradient
handle = x.register_hook(lambda grad: grad * 2)  # double the gradient
handle.remove()     # unregister

# Module forward hook
def fwd_hook(module, input, output):
    print(output.shape)
handle = layer.register_forward_hook(fwd_hook)

# Module backward hook (receives grad_input and grad_output)
def bwd_hook(module, grad_input, grad_output):
    ...
handle = layer.register_full_backward_hook(bwd_hook)
handle.remove()

Anomaly Detection

# Raises informative errors for NaN/Inf in gradients — slow, use for debugging only
with torch.autograd.detect_anomaly():
    output = model(x)
    loss = criterion(output, y)
    loss.backward()

Profiling Autograd

with torch.autograd.profiler.profile(use_cuda=True) as prof:
    output = model(x)
    loss = criterion(output, y)
    loss.backward()

print(prof.key_averages().table(sort_by='cpu_time_total', row_limit=10))

Common Gotchas

In-place operations on leaf tensors with requires_grad=True raise errors. Use out-of-place alternatives.

Double backward: if you need loss.backward() twice (e.g., meta-learning), use retain_graph=True on the first call.

.grad accumulates — it is not reset between backward calls. Always call optimizer.zero_grad() (or zero_grad(set_to_none=True)) before the forward pass.

Wrapping operations in torch.no_grad() inside the training loop for things like metric computation avoids unnecessary graph memory.