supercomputer
Examples

JAX MNIST CNN (1×L4)

Train a JAX + Flax CNN on MNIST in a couple of minutes.

Hardware: gpu_1x_l4. Save the files under Source files into a folder named jax-mnist, then follow the steps below.

Trains a small LeNet-style CNN on MNIST with JAX + Flax + Optax and writes the final test accuracy to a JSON artifact. This is the JAX equivalent of the PyTorch examples: a fast, self-contained workload that proves the JAX CUDA stack (jit, XLA, GPU) works end to end on supercomputer. Runs in a couple of minutes on a single L4.

No docker: jax[cuda12], flax, and optax are pip-installed via supercomputer.json, and training runs inside the session.

Files

  • supercomputer.json — pytorch/...-cuda12.8-devel base + pip: jax[cuda12] flax optax, SKU gpu_1x_l4. Pins the CA bundle env so runtime pip/HTTPS work on any host. (The PyTorch base is just a known-good CUDA/Python image; torch is unused — the jax[cuda12] wheel brings its own bundled CUDA libraries.)
  • train.py — downloads MNIST, trains the CNN, asserts test acc > 0.95, and writes /workspace/out/jax_mnist.json (final accuracy + per-epoch history).
  • run.sh — verifies JAX sees the GPU, then runs train.py.

Run

cd examples/jax-mnist
super create --sku gpu_1x_l4      # → s-xxxx
super run . --script run.sh --on s-xxxx
super download s-xxxx /workspace/out/jax_mnist.json ./jax_mnist.json
super stop s-xxxx

Notes

  • Tune epochs/batch via env: EPOCHS=5 BATCH=256 super run . --script run.sh --on s-xxxx.
  • MNIST is fetched from the public keras mirror; no HF token needed.

Source files

run.sh

#!/usr/bin/env bash
# Train a small CNN on MNIST with JAX/Flax INSIDE a supercomputer session — a smoke test
# that the JAX CUDA stack (jit/XLA/GPU) works on supercomputer. See README.md.
set -euo pipefail

# Fail loudly if JAX can't see the GPU (the whole point of this example).
python -c "import jax; d=jax.devices(); print('jax devices:', d); assert d[0].platform=='gpu', 'JAX is on CPU'"

exec python "$(dirname "$0")/train.py"

supercomputer.json

{
    "sku": "gpu_1x_l4",
    "image": {
        "base": "pytorch/pytorch:2.8.0-cuda12.8-cudnn9-devel",
        "apt": ["build-essential"],
        "pip": ["jax[cuda12]", "flax", "optax"],
        "env": {
            "PYTHONUNBUFFERED": "1",
            "SSL_CERT_FILE": "/etc/ssl/certs/ca-certificates.crt",
            "PIP_CERT": "/etc/ssl/certs/ca-certificates.crt",
            "REQUESTS_CA_BUNDLE": "/etc/ssl/certs/ca-certificates.crt"
        }
    }
}

train.py

"""Train a small CNN on MNIST with JAX/Flax to verify the JAX GPU stack.

Downloads MNIST as a single npz, trains a LeNet-style CNN for a few epochs with
Flax + Optax, then writes final test accuracy and per-epoch metrics to a JSON
artifact. A couple of minutes on one L4 — the point is to prove the JAX CUDA
path (jit, XLA, GPU) works end to end, not to chase accuracy.
"""

from __future__ import annotations

import json
import os
import time
import urllib.request
from pathlib import Path

import jax
import jax.numpy as jnp
import numpy as np
import optax
from flax import linen as nn

MNIST_URL = "https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz"
OUT = Path("/workspace/out/jax_mnist.json")
EPOCHS = int(os.environ.get("EPOCHS", "3"))
BATCH = int(os.environ.get("BATCH", "128"))
# Soak knob (stress tier): keep training whole epochs until this many seconds
# have elapsed; the artifact is written only after the loop. 0 = exactly EPOCHS.
STRESS_DURATION_S = int(os.environ.get("SUPERCOMPUTER_STRESS_DURATION_S", "0") or "0")


class CNN(nn.Module):
    """LeNet-style convnet: two conv/pool blocks then two dense layers."""

    @nn.compact
    def __call__(self, x):
        x = nn.relu(nn.Conv(32, (3, 3))(x))
        x = nn.avg_pool(x, (2, 2), (2, 2))
        x = nn.relu(nn.Conv(64, (3, 3))(x))
        x = nn.avg_pool(x, (2, 2), (2, 2))
        x = x.reshape((x.shape[0], -1))
        x = nn.relu(nn.Dense(128)(x))
        return nn.Dense(10)(x)


def load_mnist() -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
    """Fetch the keras MNIST npz and return (x_train, y_train, x_test, y_test)."""
    path = Path("/tmp/mnist.npz")
    if not path.exists():
        urllib.request.urlretrieve(MNIST_URL, path)
    with np.load(path) as d:
        x_train = d["x_train"].astype("float32")[..., None] / 255.0
        x_test = d["x_test"].astype("float32")[..., None] / 255.0
        return x_train, d["y_train"], x_test, d["y_test"]


def make_steps(model: CNN, optimizer: optax.GradientTransformation):
    """Build the jitted train step and accuracy fn closing over model/optimizer."""

    def loss_fn(params, x, y):
        logits = model.apply(params, x)
        return optax.softmax_cross_entropy_with_integer_labels(logits, y).mean()

    @jax.jit
    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)
        return optax.apply_updates(params, updates), opt_state, loss

    @jax.jit
    def accuracy(params, x, y):
        preds = jnp.argmax(model.apply(params, x), axis=-1)
        return (preds == y).mean()

    return train_step, accuracy


def main() -> None:
    x_train, y_train, x_test, y_test = load_mnist()
    model = CNN()
    params = model.init(jax.random.key(0), jnp.ones((1, 28, 28, 1)))
    optimizer = optax.adam(1e-3)
    opt_state = optimizer.init(params)
    train_step, accuracy = make_steps(model, optimizer)

    n = x_train.shape[0]
    history = []
    deadline = time.monotonic() + STRESS_DURATION_S
    epoch = 0
    while epoch < EPOCHS or time.monotonic() < deadline:
        perm = np.random.permutation(n)
        losses = []
        for i in range(0, n - BATCH + 1, BATCH):
            idx = perm[i:i + BATCH]
            params, opt_state, loss = train_step(
                params, opt_state, jnp.asarray(x_train[idx]), jnp.asarray(y_train[idx]))
            losses.append(float(loss))
        acc = float(accuracy(params, jnp.asarray(x_test), jnp.asarray(y_test)))
        history.append({"epoch": epoch, "train_loss": sum(losses) / len(losses), "test_acc": acc})
        print(f"epoch {epoch}: loss={history[-1]['train_loss']:.4f} test_acc={acc:.4f}", flush=True)
        epoch += 1

    final_acc = history[-1]["test_acc"]
    OUT.parent.mkdir(parents=True, exist_ok=True)
    OUT.write_text(json.dumps({
        "framework": "jax", "device": str(jax.devices()[0]),
        "epochs": len(history), "final_test_acc": final_acc, "history": history,
    }, indent=2))
    print(f"wrote {OUT} (final test acc {final_acc:.4f})", flush=True)
    assert final_acc > 0.95, f"accuracy too low for a healthy JAX run: {final_acc}"


if __name__ == "__main__":
    main()

On this page