Thomas Piacentini
← Atelier

Fiche

JAX against PyTorch

Texte original · anglais

Eager tape autograd against traced, compiled pure functions. The same MNIST MLP in both, with APIs and gotchas.

Side by side

FactorPyTorchJAX
AutodiffTape-based autogradFunctional transforms
ExecutionEager (C++ kernels)Traced + JIT compiled
Modelnn.ModulePure function: f(params, x) -> y
TPU supportCommunity-maintained (torch_xla)First-class
GPU supportBest-in-class (native CUDA)Good (CUDA via XLA)
DebuggingEasy (eager, line-by-line)Hard (tracing + compilation)
Speed (MNIST MLP)~0.5s/epoch~0.3s/epoch
Multi-deviceFSDPjax.pmap(f)
Sources

Same task, two APIs

TaskPyTorchJAX
Get gradientsloss.backward()jax.grad(loss_fn)(params, x, y)
Clear gradientsoptimizer.zero_grad()Not needed. Grads are return values
Update paramsoptimizer.step()optax.apply_updates(params, updates)
Adamtorch.optim.Adam(model.parameters(), lr=1e-3)optax.adam(learning_rate=1e-3)
Batch over examplesfor x in batch:jax.vmap(f)
Hold paramsmodel.parameters()Immutable pytree of arrays
Seed randomnesstorch.manual_seedjax.random.PRNGKey(42)
Save weightstorch.save(model.state_dict(), "model.pt")checkpointer.save('/tmp/model', params)
Sources

Two execution models

ops.gradtracemodel(x)runs now, eagerprocessTaperecords each opstoreloss.backward()replays tape in reverseoptimizer.step()mutates paramsPure functionf(params, x) -> yjax.gradreturns a grad functionjax.jittraces on first callXLAfused machine codegpu
Nodes and connections
NodeRoleKind
model(x)runs now, eagerprocess
Taperecords each opstore
loss.backward()replays tape in reverse
optimizer.step()mutates params
Pure functionf(params, x) -> y
jax.gradreturns a grad function
jax.jittraces on first call
XLAfused machine codegpu
FromToConnection
model(x)Tapeops
Tapeloss.backward()
loss.backward()optimizer.step().grad
Pure functionjax.grad
jax.gradjax.jit
jax.jitXLAtrace
Sources

One JAX training step

  1. init_params random.split(key, 3) gives three keys. He scale is jnp.sqrt(2.0 / 784).
  2. optimizer.init opt_state = optimizer.init(params) holds the Adam state.
  3. jax.value_and_grad returns the loss and the grads in one pass.
  4. optimizer.update turns the grads into updates and a new opt_state.
  5. optax.apply_updates returns new params. Nothing is mutated.
  6. First call JAX traces train_step and compiles it to XLA. The first epoch is slow.
  7. Later calls skip Python. Epochs 2-10 are fast. 10 epochs give ~97% test accuracy.
Sources

PyTorch: MNIST MLP loop

model = MNISTModel().to(device)784, 256, 128, 10
criterion = nn.CrossEntropyLoss()takes raw logits
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(10):
model.train()dropout on
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()grads accumulate
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()replays the tape
optimizer.step()
torch.save(model.state_dict(), "mnist_mlp.pt")~97.8% test accuracy
Sources

JAX: MNIST MLP loop

optimizer = optax.adam(learning_rate=1e-3)
@jax.jitcompile to XLA
def train_step(params, opt_state, x, y):
loss, grads = jax.value_and_grad(loss_fn)(params, x, y)
updates, opt_state = optimizer.update(grads, opt_state, params)
params = optax.apply_updates(params, updates)
return params, opt_state, lossnew state out
key = random.PRNGKey(0)
params = init_params(key)nested dict pytree
opt_state = optimizer.init(params)
for epoch in range(n_epochs):
key, subkey = random.split(key)new key each epoch
perm = random.permutation(subkey, len(X_train))
params, opt_state, loss = train_step(params, opt_state, xb, yb)batch_size 128
Sources

JAX transforms

TaskCode
Differentiate a functionjax.grad(f)
Second derivativejax.grad(jax.grad(f))
Batch a one-example functionjax.vmap(predict, in_axes=(None, 0))
Per-example gradientsjax.vmap(jax.grad(loss_fn), in_axes=(None, 0, 0))
Split the batch over devicesjax.pmap(train_step, axis_name='devices')
Update every leafjax.tree.map(lambda p, g: p - lr * g, params, grads)
Set one elementa = a.at[0].set(5)
Clip, then Adamoptax.chain(optax.clip_by_global_norm(1.0), optax.adam(1e-3))
Sources

Install and run

CommandWhat it does
pip install jax jaxlib optax flaxInstalls JAX, Optax and Flax
pip install jax[cuda12]Installs JAX with GPU support
pip install jax[tpu] -f <url>Installs JAX for TPU on Google Cloud
XLA_PYTHON_CLIENT_PREALLOCATE=falseStops the 75% GPU memory preallocation
jax.debug.print()Prints inside a jitted function
fast_fn(x).block_until_ready()Waits for the result before you stop a timer
torch.load("model.pt", weights_only=True)Loads a saved state dict
Sources

Gotchas

SymptomCauseFix
Expected all tensors to be on the same deviceModel and batch on different devicesMove both with .to(device)
Wrong gradients and no errorSoftmax output into CrossEntropyLossPass raw logits
First step is slowJIT compiles on the first callWarm up before you benchmark
if x > 0 fails inside JITControl flow depends on a traced valueUse jax.lax.cond
Python loop inside JIT is slowLoop over JAX arraysUse jax.lax.scan or jax.lax.fori_loop
a[0] = 5 failsJAX arrays are immutableWrite a = a.at[0].set(5)
Sources

Use X when

  • Use PyTorch unless you have a specific reason to use JAX.
  • Use PyTorch to debug line by line. print(), pdb and breakpoint() work.
  • Use PyTorch to prototype fast and for the HuggingFace ecosystem.
  • Use JAX when you have TPU access.
  • Use JAX for per-example gradients.
  • Use JAX for multi-device training at massive scale.
Sources

Questions

Why does JAX need pure functions?
JIT compilation requires them. Same inputs give the same outputs.
Why is there no zero_grad in JAX?
jax.grad returns grads as values. Tensors store no graph.
Why save state_dict(), not the model?
The model object uses pickle. Pickle breaks when you refactor.
Why torch.no_grad() in eval?
It disables autograd. Memory use goes down.
What maps from the mini framework?
Sequential, Adam, zero_grad, backward, step, train, eval.
Sources
Original source
---
id: jax-vs-pytorch
title: JAX against PyTorch
code: JP
subtitle: Eager tape autograd against traced, compiled pure functions. The same MNIST MLP in both, with APIs and gotchas.
hue: 280
sources: phases/03-deep-learning-core/10-mini-framework, phases/03-deep-learning-core/11-intro-to-pytorch, phases/03-deep-learning-core/12-intro-to-jax
source_hash: 74354f7a9889b59d
---

## Side by side
type: comparison
from: phases/03-deep-learning-core/11-intro-to-pytorch/docs/en.md, phases/03-deep-learning-core/12-intro-to-jax/docs/en.md

| Factor | PyTorch | JAX |
|---|---|---|
| Autodiff | Tape-based autograd | Functional transforms |
| Execution | Eager (C++ kernels) | Traced + JIT compiled |
| Model | `nn.Module` | Pure function: `f(params, x) -> y` |
| TPU support | Community-maintained (torch_xla) | First-class |
| GPU support | Best-in-class (native CUDA) | Good (CUDA via XLA) |
| Debugging | Easy (eager, line-by-line) | Hard (tracing + compilation) |
| Speed (MNIST MLP) | ~0.5s/epoch | ~0.3s/epoch |
| Multi-device | `FSDP` | `jax.pmap(f)` |

## Same task, two APIs
type: comparison
from: phases/03-deep-learning-core/11-intro-to-pytorch/docs/en.md, phases/03-deep-learning-core/12-intro-to-jax/docs/en.md

| Task | PyTorch | JAX |
|---|---|---|
| Get gradients | `loss.backward()` | `jax.grad(loss_fn)(params, x, y)` |
| Clear gradients | `optimizer.zero_grad()` | Not needed. Grads are return values |
| Update params | `optimizer.step()` | `optax.apply_updates(params, updates)` |
| Adam | `torch.optim.Adam(model.parameters(), lr=1e-3)` | `optax.adam(learning_rate=1e-3)` |
| Batch over examples | `for x in batch:` | `jax.vmap(f)` |
| Hold params | `model.parameters()` | Immutable pytree of arrays |
| Seed randomness | `torch.manual_seed` | `jax.random.PRNGKey(42)` |
| Save weights | `torch.save(model.state_dict(), "model.pt")` | `checkpointer.save('/tmp/model', params)` |

## Two execution models
type: architecture
from: phases/03-deep-learning-core/11-intro-to-pytorch/docs/en.md, phases/03-deep-learning-core/12-intro-to-jax/docs/en.md

```arch
node | call | 1,1 | model(x) | runs now, eager | process
node | tape | 2,1 | Tape | records each op | store
node | bwd | 3,1 | loss.backward() | replays tape in reverse
node | step | 4,1 | optimizer.step() | mutates params
node | fn | 1,2 | Pure function | f(params, x) -> y
node | grad | 2,2 | jax.grad | returns a grad function
node | jit | 3,2 | jax.jit | traces on first call
node | xla | 4,2 | XLA | fused machine code | gpu
edge | call | tape | ops
edge | tape | bwd |
edge | bwd | step | .grad
edge | fn | grad |
edge | grad | jit |
edge | jit | xla | trace
```

## One JAX training step
type: flow
from: phases/03-deep-learning-core/12-intro-to-jax/docs/en.md, phases/03-deep-learning-core/12-intro-to-jax/code/jax_intro.py

1. **init_params**: `random.split(key, 3)` gives three keys. He scale is `jnp.sqrt(2.0 / 784)`.
2. **optimizer.init**: `opt_state = optimizer.init(params)` holds the Adam state.
3. **jax.value_and_grad**: returns the loss and the grads in one pass.
4. **optimizer.update**: turns the grads into updates and a new `opt_state`.
5. **optax.apply_updates**: returns new params. Nothing is mutated.
6. **First call**: JAX traces `train_step` and compiles it to XLA. The first epoch is slow.
7. **Later calls**: skip Python. Epochs 2-10 are fast. 10 epochs give ~97% test accuracy.

## PyTorch: MNIST MLP loop
type: annotated-code
lang: python
from: phases/03-deep-learning-core/11-intro-to-pytorch/docs/en.md, phases/03-deep-learning-core/11-intro-to-pytorch/code/pytorch_intro.py

```python
model = MNISTModel().to(device)                  # 784, 256, 128, 10
criterion = nn.CrossEntropyLoss()                # takes raw logits
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(10):
    model.train()                                # dropout on
    for images, labels in train_loader:
        images, labels = images.to(device), labels.to(device)
        optimizer.zero_grad()                    # grads accumulate
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()                          # replays the tape
        optimizer.step()
torch.save(model.state_dict(), "mnist_mlp.pt")   # ~97.8% test accuracy
```

## JAX: MNIST MLP loop
type: annotated-code
lang: python
from: phases/03-deep-learning-core/12-intro-to-jax/code/jax_intro.py, phases/03-deep-learning-core/12-intro-to-jax/docs/en.md

```python
optimizer = optax.adam(learning_rate=1e-3)
@jax.jit                                           # compile to XLA
def train_step(params, opt_state, x, y):
    loss, grads = jax.value_and_grad(loss_fn)(params, x, y)
    updates, opt_state = optimizer.update(grads, opt_state, params)
    params = optax.apply_updates(params, updates)
    return params, opt_state, loss                 # new state out
key = random.PRNGKey(0)
params = init_params(key)                          # nested dict pytree
opt_state = optimizer.init(params)
for epoch in range(n_epochs):
    key, subkey = random.split(key)                # new key each epoch
    perm = random.permutation(subkey, len(X_train))
    params, opt_state, loss = train_step(params, opt_state, xb, yb)  # batch_size 128
```

## JAX transforms
type: task-table
span: 2
from: phases/03-deep-learning-core/12-intro-to-jax/docs/en.md

| Task | Code |
|---|---|
| Differentiate a function | `jax.grad(f)` |
| Second derivative | `jax.grad(jax.grad(f))` |
| Batch a one-example function | `jax.vmap(predict, in_axes=(None, 0))` |
| Per-example gradients | `jax.vmap(jax.grad(loss_fn), in_axes=(None, 0, 0))` |
| Split the batch over devices | `jax.pmap(train_step, axis_name='devices')` |
| Update every leaf | `jax.tree.map(lambda p, g: p - lr * g, params, grads)` |
| Set one element | `a = a.at[0].set(5)` |
| Clip, then Adam | `optax.chain(optax.clip_by_global_norm(1.0), optax.adam(1e-3))` |

## Install and run
type: command-table
from: phases/03-deep-learning-core/12-intro-to-jax/docs/en.md, phases/03-deep-learning-core/12-intro-to-jax/code/jax_intro.py, phases/03-deep-learning-core/11-intro-to-pytorch/docs/en.md

| Command | What it does |
|---|---|
| `pip install jax jaxlib optax flax` | Installs JAX, Optax and Flax |
| `pip install jax[cuda12]` | Installs JAX with GPU support |
| `pip install jax[tpu] -f <url>` | Installs JAX for TPU on Google Cloud |
| `XLA_PYTHON_CLIENT_PREALLOCATE=false` | Stops the 75% GPU memory preallocation |
| `jax.debug.print()` | Prints inside a jitted function |
| `fast_fn(x).block_until_ready()` | Waits for the result before you stop a timer |
| `torch.load("model.pt", weights_only=True)` | Loads a saved state dict |

## Gotchas
type: troubleshooting
from: phases/03-deep-learning-core/11-intro-to-pytorch/docs/en.md, phases/03-deep-learning-core/12-intro-to-jax/docs/en.md

| Symptom | Cause | Fix |
|---|---|---|
| `Expected all tensors to be on the same device` | Model and batch on different devices | Move both with `.to(device)` |
| Wrong gradients and no error | Softmax output into `CrossEntropyLoss` | Pass raw logits |
| First step is slow | JIT compiles on the first call | Warm up before you benchmark |
| `if x > 0` fails inside JIT | Control flow depends on a traced value | Use `jax.lax.cond` |
| Python loop inside JIT is slow | Loop over JAX arrays | Use `jax.lax.scan` or `jax.lax.fori_loop` |
| `a[0] = 5` fails | JAX arrays are immutable | Write `a = a.at[0].set(5)` |

## Use X when
type: tips
from: phases/03-deep-learning-core/12-intro-to-jax/docs/en.md, phases/03-deep-learning-core/11-intro-to-pytorch/docs/en.md

- Use PyTorch unless you have a specific reason to use JAX.
- Use PyTorch to debug line by line. `print()`, `pdb` and `breakpoint()` work.
- Use PyTorch to prototype fast and for the HuggingFace ecosystem.
- Use JAX when you have TPU access.
- Use JAX for per-example gradients.
- Use JAX for multi-device training at massive scale.

## Questions
type: qa
from: phases/03-deep-learning-core/10-mini-framework/docs/en.md, phases/03-deep-learning-core/11-intro-to-pytorch/docs/en.md, phases/03-deep-learning-core/12-intro-to-jax/docs/en.md

- **Why does JAX need pure functions?** JIT compilation requires them. Same inputs give the same outputs.
- **Why is there no `zero_grad` in JAX?** `jax.grad` returns grads as values. Tensors store no graph.
- **Why save `state_dict()`, not the model?** The model object uses pickle. Pickle breaks when you refactor.
- **Why `torch.no_grad()` in eval?** It disables autograd. Memory use goes down.
- **What maps from the mini framework?** `Sequential`, `Adam`, `zero_grad`, `backward`, `step`, `train`, `eval`.
Code et livrables 1
Ce que vous en retenez

Enregistré dans votre carnet sur cet appareil.

Sources et limites

Les repères de lecture sont déclaratifs. Les résultats de tests sont enregistrés séparément ; ils valent pour les cas exécutés.

AI Engineering from Scratch · 3f9715c · MIT

Télécharger ce fichier ↓