Native JAX Training#
This page shows how to run a training loop that you write directly in
JAX on a cloud TPU or GPU. Read this page if you use jax.grad,
jax.pmap, or jax.sharding without Keras. Kinetic runs a JAX function
the same way that it runs a Keras function: you decorate the function
with @kinetic.run() and call it. The sections below cover the
JAX-specific details: the JAX runtime in the image, single-host
parallelism, multi-host slices, data, and outputs.
Before you start#
Complete Getting Started. You need an active profile and a cluster.
Make sure that the cluster has a node pool for the accelerator that you use.
kinetic pool listshows the pools of the cluster.kinetic pool add --accelerator tpu-v5litepod-8adds a pool for the first example. See Clusters and Node Pools.Do not add
jax,jaxlib, orlibtputo your dependency file. Kinetic installs them for you. See The JAX runtime in the image.
A first run#
Save this script and run it with python:
import kinetic
@kinetic.run(accelerator="tpu-v5litepod-8")
def jax_computation():
import jax
import jax.numpy as jnp
print(f"Devices: {jax.devices()}")
x = jnp.ones((1000, 1000))
result = jnp.dot(x, x)
return float(result[0, 0])
print(jax_computation()) # 1000.0
Kinetic sends the function to the cluster, streams the log lines to your terminal, and returns the value to the script. The first run takes 5 to 10 minutes, because Kinetic builds a container image with your dependencies. Later runs with the same dependency file start in less than 1 minute while a node still runs.
A training loop with jax.grad runs without a change to the loop:
@kinetic.run(accelerator="tpu-v5litepod-4")
def train():
import jax
import jax.numpy as jnp
def loss_fn(params, x, y):
pred = x @ params["w"] + params["b"]
return jnp.mean((pred - y) ** 2)
grad_fn = jax.grad(loss_fn)
key = jax.random.PRNGKey(0)
params = {"w": jax.random.normal(key, (10, 1)), "b": jnp.zeros(1)}
x = jax.random.normal(key, (512, 10))
y = x @ jnp.ones((10, 1)) + 0.1 * jax.random.normal(key, (512, 1))
lr = 0.01
for step in range(200):
grads = grad_fn(params, x, y)
params = {k: params[k] - lr * grads[k] for k in params}
if step % 50 == 0:
print(f"step {step}: loss={loss_fn(params, x, y):.4f}")
return float(loss_fn(params, x, y))
Put import jax and the imports of other heavy libraries inside the
decorated function. The import then runs on the pod, which has the JAX
build for the accelerator. Your local machine does not need JAX.
The JAX runtime in the image#
JAX needs a jaxlib build and an accelerator runtime that match the
hardware. The image that Kinetic builds contains JAX for your
accelerator:
Accelerator |
JAX package in the image |
|---|---|
TPU ( |
|
GPU ( |
|
CPU ( |
|
Do not pin jax, jaxlib, or libtpu in your dependency file.
Kinetic removes the entries for jax, jaxlib, libtpu, and
libtpu-nightly from the dependency list before the install. A pin in
your file therefore does not replace the installation in the image.
Kinetic logs a warning for each removed entry. To keep a line in
requirements.txt, append # kn:keep to the line. See
JAX and accelerator runtimes
for the filter rules and the override.
Inside the function, jax.devices() returns the devices of the pod:
tpu-v5litepod-8: 8 TPU devices on one host.tpu-v5litepod-4: 4 TPU devices on one host.gpu-l4: one GPU device.cpu: one CPU device.
Single-host parallelism#
A single-host slice, for example tpu-v5litepod-8, holds all of its
chips on one VM. jax.pmap or jax.sharding spreads the computation
across those chips. Kinetic needs no extra setting for this case.
@kinetic.run(accelerator="tpu-v5litepod-8")
def parallel_computation():
import jax
import jax.numpy as jnp
n_devices = jax.local_device_count()
print(f"Running on {n_devices} devices")
@jax.pmap
def parallel_matmul(x):
return jnp.dot(x, x.T)
data = jnp.ones((n_devices, 256, 256))
result = parallel_matmul(data)
return float(result[0, 0, 0])
On tpu-v5litepod-8, jax.local_device_count() is 8, and each pmap
replica runs on one chip.
Multi-host slices#
Some slices span more than one host. tpu-v5litepod-16 and tpu-v6e-16
each consist of four 4-chip VMs. Kinetic reads the host count from the
accelerator name and selects the pathways backend for you. You do not
set backend=. Kinetic runs one pod per host and sets the environment
variables for multi-controller JAX on every pod. JAX then starts one
process per host. jax.process_count() equals the host count,
jax.local_device_count() is the chip count of one host, and
jax.device_count() is the total. Collectives across hosts work, for
example jax.lax.psum inside pmap.
@kinetic.run(accelerator="tpu-v6e-16")
def train_distributed():
import jax
print(f"Host {jax.process_index()} of {jax.process_count()}")
print(f"Devices on this host: {jax.local_device_count()}")
print(f"Total devices: {jax.device_count()}")
# pmap and sharding work across hosts here.
...
On tpu-v6e-16, jax.process_count() is 4, jax.local_device_count()
is 4, and jax.device_count() is 16.
A multi-host job has two requirements:
The cluster needs a node pool for the multi-host accelerator, for example
kinetic pool add --accelerator tpu-v6e-16.The cluster needs the LeaderWorkerSet controller.
kinetic upinstalls it.
Note
The chip count alone does not tell you the host count. tpu-v5litepod-8
is one 8-chip VM, but tpu-v6e-8 is two 4-chip VMs and therefore a
multi-host job. The Hosts column on the
Accelerators page decides.
See Distributed Training for the log that you see, the return value, and the failure behavior of a multi-host job.
Data#
To pass a dataset into a remote JAX function, construct a
kinetic.Data(...) object at the call site in your local script.
Pass the object as an argument. Kinetic uploads or mounts the source and
gives the remote function a plain filesystem path. The decorated
function only sees a str path:
import kinetic
from kinetic import Data
@kinetic.run(accelerator="tpu-v5litepod-8")
def train(data_dir):
# `data_dir` is a local filesystem path on the remote pod.
import os
files = os.listdir(data_dir)
...
# Local directory:
train(Data("./my_dataset/"))
# Existing Cloud Storage prefix (the trailing slash marks a directory):
train(Data("gs://my-bucket/dataset/"))
# Large Cloud Storage dataset, read on demand through FUSE:
train(Data("gs://my-bucket/large/", fuse=True))
Data accepts a local path, a gs:// URI, or an hf:// URI for a
Hugging Face dataset. See Working with Data for the
choice between a downloaded copy, a FUSE mount, and direct gs://
access.
Outputs and checkpoints#
Kubernetes deletes the pod filesystem, including /tmp, when the pod
ends. Write every file that you want to keep under KINETIC_OUTPUT_DIR.
Kinetic sets that environment variable in the pod to a Cloud Storage
prefix that stays after the pod ends. Orbax writes to a gs:// path
directly, so pass that prefix to the CheckpointManager:
@kinetic.run(accelerator="tpu-v5litepod-8")
def train():
import os
import orbax.checkpoint as ocp
output_dir = os.environ["KINETIC_OUTPUT_DIR"]
mngr = ocp.CheckpointManager(
f"{output_dir}/checkpoints", ocp.StandardCheckpointer()
)
...
Add orbax-checkpoint to your dependency file for this example.
See Outputs and Checkpoints for a full Orbax example, the default location, and how to resume a job.