iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

autograd

torch.autograd tracks operations on tensors with requires_grad=True and builds a dynamic computation graph. loss.backward() walks it backwards; gradients land in .grad.

Compute, no_grad, hooks, manual graph

EXAMPLE
import torch
import torch.nn as nn

# 1) Manual gradient — the core idea
x = torch.tensor(2.0, requires_grad=True)
y = x ** 3 + 4 * x          # y = 16 when x=2
y.backward()                 # dy/dx = 3x^2 + 4 = 16 when x=2
print(x.grad)                # tensor(16.)

# 2) Multiple variables
a = torch.tensor(1.0, requires_grad=True)
b = torch.tensor(2.0, requires_grad=True)
z = a ** 2 + 3 * a * b + b ** 3
z.backward()
print(a.grad)                # 2a + 3b = 8
print(b.grad)                # 3a + 3b^2 = 15

# 3) Vector gradients — backward requires a SCALAR
v = torch.randn(3, requires_grad=True)
out = v * v                # vector output
# out.backward()           # ERROR — need a scalar
out.sum().backward()         # OR pass grad of out
print(v.grad)

# 4) Disable autograd — no_grad / inference_mode
with torch.no_grad():
    pred = model(x)          # no graph built, faster, less memory

with torch.inference_mode():  # even cheaper than no_grad on PyTorch 1.9+
    pred = model(x)

# 5) Detach — break the graph
p = some_tensor.detach()     # same data, but no grad tracking
target = (input * 2).detach()  # don't backprop into target

# 6) In a model — every forward pass builds a fresh graph
model = nn.Linear(10, 1)
loss_fn = nn.MSELoss()
opt = torch.optim.SGD(model.parameters(), lr=0.01)

for x, y in batches:
    opt.zero_grad(set_to_none=True)   # fresh gradients
    pred = model(x)
    loss = loss_fn(pred, y)
    loss.backward()                    # graph walked; gradients populated
    opt.step()                         # apply gradients

# 7) zero_grad — why
# Gradients ACCUMULATE in .grad — without zeroing, every batch's gradient adds on top.
# set_to_none=True is faster + safer (None means 'no contribution').

# 8) Gradient accumulation — train with effective batch > GPU memory
opt.zero_grad(set_to_none=True)
for i, (x, y) in enumerate(batches):
    pred = model(x)
    loss = loss_fn(pred, y) / accum_steps   # normalise
    loss.backward()
    if (i + 1) % accum_steps == 0:
        opt.step()
        opt.zero_grad(set_to_none=True)

# 9) Gradient clipping — stop exploding gradients (common with RNNs)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

# 10) Higher-order derivatives — create_graph=True
x = torch.tensor(2.0, requires_grad=True)
y = x ** 3
grad_x = torch.autograd.grad(y, x, create_graph=True)[0]   # 3x^2 = 12
grad_grad_x = torch.autograd.grad(grad_x, x)[0]            # 6x = 12

# 11) Hooks — peek at intermediate gradients
def inspect_grad(grad):
    print('intermediate grad:', grad.norm())

z.register_hook(inspect_grad)

# Or whole-tensor hooks on Modules
def hook(module, grad_input, grad_output):
    print(module.__class__.__name__, [g.norm() if g is not None else None for g in grad_output])

for m in model.modules():
    m.register_full_backward_hook(hook)

# 12) Custom autograd Function — write your own gradient
class MyReLU(torch.autograd.Function):
    @staticmethod
    def forward(ctx, x):
        ctx.save_for_backward(x)
        return x.clamp(min=0)
    @staticmethod
    def backward(ctx, grad_output):
        x, = ctx.saved_tensors
        return grad_output * (x > 0).float()

y = MyReLU.apply(x)

# 13) Mixed precision — gradient scaling for fp16
scaler = torch.amp.GradScaler('cuda')
for x, y in batches:
    opt.zero_grad(set_to_none=True)
    with torch.amp.autocast('cuda', dtype=torch.float16):
        pred = model(x)
        loss = loss_fn(pred, y)
    scaler.scale(loss).backward()
    scaler.step(opt)
    scaler.update()

# 14) Common gotchas
# - Forgetting to zero gradients → loss explodes mysteriously
# - Using .item() too early breaks the graph
# - Doing `with torch.no_grad():` around the training step disables gradient calc → no learning
# - Tensor on the wrong device → silent zero gradients (move with .to(device) consistently)
# - Modifying a tensor with `requires_grad=True` in-place → autograd error or wrong gradient

# 15) Debug aids
# torch.autograd.set_detect_anomaly(True)   # slow, but pinpoints NaN/inf in backward
# torch.autograd.gradcheck(fn, (inputs,))   # verify a custom function's gradient

Why it matters

Autograd builds a fresh graph on every forward pass. The training-loop rhythm — zero_grad → forward → loss → backward → step — is the whole framework once you understand that.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
import torch
x = torch.tensor(3.0, requires_grad=True)
y = x ** 2 + 2 * x
y.backward()
print(x.grad)   # 2x + 2 = 8
Try it Yourself »

Exercise

Compute gradients of y wrt inputs.

y. ()

Test yourself

Q1. Enable gradient tracking on a tensor with…
Q2. Compute gradients with…
Q3. Disable autograd in eval with…

Discussion

Loading…