C.W.K.
Stream
Lesson 04 of 05 · published

The Complete Training Loop

~9 min · training, jax, tutorial

Level 0Curious
0 XP0/73 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete

Let's put everything together into a complete, production-style training loop with Flax NNX and Optax. This is the pattern you'll use (or adapt) for real projects.

from flax import nnx
import jax
import jax.numpy as jnp
import optax

# --- Model Definition ---
class Classifier(nnx.Module):
    def __init__(self, rngs: nnx.Rngs):
        self.conv1 = nnx.Conv(1, 32, kernel_size=(3, 3), rngs=rngs)
        self.conv2 = nnx.Conv(32, 64, kernel_size=(3, 3), rngs=rngs)
        self.linear1 = nnx.Linear(64 * 5 * 5, 256, rngs=rngs)
        self.linear2 = nnx.Linear(256, 10, rngs=rngs)
        self.dropout = nnx.Dropout(rate=0.5, rngs=rngs)

    def __call__(self, x):
        x = nnx.relu(self.conv1(x))
        x = nnx.max_pool(x, window_shape=(2, 2), strides=(2, 2))
        x = nnx.relu(self.conv2(x))
        x = nnx.max_pool(x, window_shape=(2, 2), strides=(2, 2))
        x = x.reshape(x.shape[0], -1)
        x = nnx.relu(self.linear1(x))
        x = self.dropout(x)
        return self.linear2(x)

# --- Setup ---
model = Classifier(rngs=nnx.Rngs(0))

schedule = optax.warmup_cosine_decay_schedule(
    init_value=0.0,
    peak_value=1e-3,
    warmup_steps=500,
    decay_steps=10000,
)

tx = optax.chain(
    optax.clip_by_global_norm(1.0),
    optax.adamw(learning_rate=schedule),
)
optimizer = nnx.Optimizer(model, tx)

# --- Training Step ---
@nnx.jit
def train_step(model, optimizer, x, y):
    def loss_fn(model):
        logits = model(x)
        loss = optax.softmax_cross_entropy_with_integer_labels(logits, y)
        return jnp.mean(loss)

    loss, grads = nnx.value_and_grad(loss_fn)(model)
    optimizer.update(grads)
    return loss

# --- Evaluation Step ---
@nnx.jit
def eval_step(model, x, y):
    logits = model(x)
    predictions = jnp.argmax(logits, axis=-1)
    accuracy = jnp.mean(predictions == y)
    return accuracy

# --- Training Loop ---
num_epochs = 10
for epoch in range(num_epochs):
    # Training
    train_loss = 0.0
    num_batches = 0
    for x_batch, y_batch in train_loader:
        loss = train_step(model, optimizer, x_batch, y_batch)
        train_loss += loss
        num_batches += 1

    avg_loss = train_loss / num_batches

    # Evaluation
    total_acc = 0.0
    eval_batches = 0
    for x_val, y_val in val_loader:
        acc = eval_step(model, x_val, y_val)
        total_acc += acc
        eval_batches += 1

    avg_acc = total_acc / eval_batches
    print(f"Epoch {epoch}: loss={avg_loss:.4f}, val_acc={avg_acc:.4f}")

💡 Why This Matters

This is a real training loop, not pseudocode. The key things to notice: (1) the entire train_step is JIT-compiled, so the forward pass, loss, backward pass, and optimizer update all run as one fused computation on the accelerator; (2) the model and optimizer are NNX objects that get mutated in-place by nnx.jit; (3) evaluation uses the same model object but in a separate JIT-compiled function.

Code

from flax import nnx
import jax
import jax.numpy as jnp
import optax

# --- Model Definition ---
class Classifier(nnx.Module):
    def __init__(self, rngs: nnx.Rngs):
        self.conv1 = nnx.Conv(1, 32, kernel_size=(3, 3), rngs=rngs)
        self.conv2 = nnx.Conv(32, 64, kernel_size=(3, 3), rngs=rngs)
        self.linear1 = nnx.Linear(64 * 5 * 5, 256, rngs=rngs)
        self.linear2 = nnx.Linear(256, 10, rngs=rngs)
        self.dropout = nnx.Dropout(rate=0.5, rngs=rngs)

    def __call__(self, x):
        x = nnx.relu(self.conv1(x))
        x = nnx.max_pool(x, window_shape=(2, 2), strides=(2, 2))
        x = nnx.relu(self.conv2(x))
        x = nnx.max_pool(x, window_shape=(2, 2), strides=(2, 2))
        x = x.reshape(x.shape[0], -1)
        x = nnx.relu(self.linear1(x))
        x = self.dropout(x)
        return self.linear2(x)

# --- Setup ---
model = Classifier(rngs=nnx.Rngs(0))

schedule = optax.warmup_cosine_decay_schedule(
    init_value=0.0,
    peak_value=1e-3,
    warmup_steps=500,
    decay_steps=10000,
)

tx = optax.chain(
    optax.clip_by_global_norm(1.0),
    optax.adamw(learning_rate=schedule),
)
optimizer = nnx.Optimizer(model, tx)

# --- Training Step ---
@nnx.jit
def train_step(model, optimizer, x, y):
    def loss_fn(model):
        logits = model(x)
        loss = optax.softmax_cross_entropy_with_integer_labels(logits, y)
        return jnp.mean(loss)

    loss, grads = nnx.value_and_grad(loss_fn)(model)
    optimizer.update(grads)
    return loss

# --- Evaluation Step ---
@nnx.jit
def eval_step(model, x, y):
    logits = model(x)
    predictions = jnp.argmax(logits, axis=-1)
    accuracy = jnp.mean(predictions == y)
    return accuracy

# --- Training Loop ---
num_epochs = 10
for epoch in range(num_epochs):
    # Training
    train_loss = 0.0
    num_batches = 0
    for x_batch, y_batch in train_loader:
        loss = train_step(model, optimizer, x_batch, y_batch)
        train_loss += loss
        num_batches += 1

    avg_loss = train_loss / num_batches

    # Evaluation
    total_acc = 0.0
    eval_batches = 0
    for x_val, y_val in val_loader:
        acc = eval_step(model, x_val, y_val)
        total_acc += acc
        eval_batches += 1

    avg_acc = total_acc / eval_batches
    print(f"Epoch {epoch}: loss={avg_loss:.4f}, val_acc={avg_acc:.4f}")

External links

Exercise

Combine: model (Flax NNX or Equinox), loss, optax optimizer, jit'd train_step, simple data loader, 100-step loop. Print training loss every 10 steps. Save the final params. This is your minimum-viable JAX trainer template.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.