This tutorial shows you how to run distributed PyTorch workloads and fine-tune a Gemma 4 model on multi-host Cloud TPU slices in Google Kubernetes Engine (GKE) by using Ray Core, Ray Train, PyTorch Fully Sharded Data Parallel (FSDP2), and TorchTPU.
This tutorial is intended for machine learning (ML) engineers, platform admins and operators, and data and AI specialists who want to orchestrate distributed PyTorch training workloads on multi-host TPU slices in Kubernetes. To learn more about common roles and example tasks referenced in Google Cloud content, see Common GKE user roles and tasks.
Before starting this tutorial, ensure that you're familiar with the following:
Background
This section describes the key technologies used in this tutorial.
TPUs
Tensor Processing Units (TPUs) are Google custom-designed accelerators for machine learning workloads. This tutorial supports both TPU Trillium (v6e) and Ironwood (TPU7x). In a multi-host TPU slice, multiple TPU hosts communicate directly over a high-speed Inter-Chip Interconnect (ICI) network, providing the aggregate High Bandwidth Memory (HBM) and low-latency collective communication required to train models that exceed the memory capacity of a single chip or TPU host.
TorchTPU
TorchTPU is an open-source PyTorch backend for Cloud TPU, developed in collaboration between Google and Meta. With TorchTPU, you can run existing PyTorch models on TPU hardware with minimal code changes and scale training and inference workloads from a single chip to multi-slice TPU pods through the Accelerated Linear Algebra (XLA) compiler. TorchTPU uses a one-process-per-device execution model over the TPU ICI network and supports standard PyTorch APIs, including eager execution, torch.compile, torch.distributed, DeviceMesh, DTensor, and Fully Sharded Data Parallel (FSDP2).
KubeRay and Ray on TPUs
KubeRay is a Kubernetes operator for deploying and managing Ray applications. Enabling the Ray on GKE add-on deploys the managed KubeRay operator and the KubeRay TPU webhook. When KubeRay creates worker Pods that request google.com/tpu resources, the webhook automatically configures the Pod hostnames, headless Service, and TPU environment variables required for multi-host coordination.
Ray detects the TPU topology on each node at startup and schedules distributed workloads across TPU slices. You can orchestrate distributed TorchTPU workloads at two levels:
- Ray Core provides topology-aware placement groups and actor scheduling for custom distributed execution loops, such as tensor-parallel computation or reinforcement learning (RL) pipelines.
- Ray Train provides managed distributed data-parallel and model-parallel training, including PyTorch Distributed Data Parallel (DDP), PyTorch FSDP2, and JAX single program, multiple data (SPMD), with built-in checkpointing and automatic failure recovery.
Objectives
This tutorial shows you how to do the following:
- Create a GKE cluster in Autopilot or Standard on TPU Trillium (v6e) or Ironwood (TPU7x).
- Build a container image that layers Ray and Hugging Face training libraries on top of the TorchTPU base image.
- Deploy a RayCluster custom resource to provision the head and TPU worker nodes.
- Run distributed tensor-parallel computation with PyTorch
DeviceMeshandDTensoracross a TPU slice by using Ray Core. - Fine-tune a Gemma 4 model with PyTorch FSDP2 across a multi-host TPU slice by using Ray Train.
Costs
In this tutorial, you use the following billable components of Google Cloud:
To generate a cost estimate based on your projected usage, use the pricing calculator.
When you finish the tasks that are described in this tutorial, you can avoid continued billing by deleting the resources that you created. For more information, see Clean up.
Before you begin
Before you start, make sure that you have performed the following tasks:
- Enable the Artifact Registry API and the Cloud Storage API and the Google Kubernetes Engine API. Enable APIs
- To use the Google Cloud CLI for this task,
install and then
initialize the
gcloud CLI. If you previously installed the gcloud CLI, get the latest
version by running the
gcloud components updatecommand. Earlier gcloud CLI versions might not support running the commands in this document.
- Verify that your project has sufficient quota for TPU Trillium (v6e) or Ironwood (TPU7x) capacity in your selected zone. For more information, see Cloud TPU quotas.
- Ensure that your account has the following IAM roles:
- Kubernetes Engine Admin (
roles/container.admin) - Artifact Registry Administrator (
roles/artifactregistry.admin) - Storage Admin (
roles/storage.admin)
- Kubernetes Engine Admin (
Prepare your environment
In this tutorial, you use Cloud Shell to manage resources hosted on Google Cloud. Cloud Shell comes preinstalled with the software that you need for this tutorial, including kubectl, docker, python3, and the gcloud CLI.
To set up your environment with Cloud Shell, follow these steps:
In the Google Cloud console, launch a Cloud Shell session by clicking
Activate Cloud Shell in the Google Cloud console. This action launches a session in the bottom pane of Google Cloud console.
Clone the
kubernetes-engine-samplesrepository and change to the sample directory for this tutorial:git clone https://github.com/GoogleCloudPlatform/kubernetes-engine-samples.git cd kubernetes-engine-samples/ai-ml/gke-ray/raytrain/torchtpuCreate and activate a local Python virtual environment with the Ray CLI installed:
python3 -m venv ~/ray-env source ~/ray-env/bin/activate pip install "ray[default]==2.58.0"To download the Gemma 4 model weights, acknowledge the Google license agreement on Hugging Face:
- Go to the Gemma 4 model page on Hugging Face.
- Sign in and accept the license terms by clicking Agree and access repository.
- In your Hugging Face account settings, generate a user access token with
Readpermissions.
Set the environment variables for your Google Cloud project, cluster, Cloud Storage bucket, and Hugging Face token:
export PROJECT_ID=PROJECT_ID export CLUSTER_NAME=CLUSTER_NAME export REGION=REGION export ZONE=ZONE export ARTIFACT_REGISTRY=ARTIFACT_REGISTRY export BUCKET_NAME=BUCKET_NAME export HF_TOKEN=HF_TOKEN export IMAGE_URI=${REGION}-docker.pkg.dev/${PROJECT_ID}/${ARTIFACT_REGISTRY}/ray-torchtpu:latest gcloud config set project ${PROJECT_ID}Replace the following:
PROJECT_ID: your Google Cloud project ID.CLUSTER_NAME: the name of your GKE cluster, such asray-torchtpu-cluster.REGION: the region where your Artifact Registry repository, Cloud Storage bucket, and TPU capacity are located.ZONE: the zone where your TPU capacity is available. For available regions and zones by TPU version, see TPU regions and zones.ARTIFACT_REGISTRY: the name of your Artifact Registry Docker repository, such asray-torchtpu.BUCKET_NAME: the name of your Cloud Storage bucket for storing Ray Train checkpoints, such asPROJECT_ID-ray-torchtpu.HF_TOKEN: your Hugging Face access token.
Create a GKE cluster
You can run distributed TorchTPU training workloads in a GKE Autopilot or Standard cluster with the managed Ray operator add-on enabled.
Create your GKE cluster and configure credentials for the
kubectlcommand-line tool:Autopilot
Create a regional GKE Autopilot cluster with the Ray operator add-on enabled:
gcloud container clusters create-auto ${CLUSTER_NAME} \ --project=${PROJECT_ID} \ --region=${REGION} \ --cluster-version=1.36 \ --enable-ray-operatorWhen you deploy the RayCluster custom resource in a later step, GKE Autopilot automatically provisions the CPU head node and the multi-host TPU slice that match your Pod resource requests and node selectors.
Configure the
kubectlcommand-line tool to communicate with your cluster:gcloud container clusters get-credentials ${CLUSTER_NAME} \ --project=${PROJECT_ID} \ --region=${REGION}
Standard
Create a zonal GKE Standard cluster with a CPU node pool to host the Ray head Pod, Image streaming, GKE Dataplane V2, and Workload Identity Federation for GKE enabled:
gcloud container clusters create ${CLUSTER_NAME} \ --project=${PROJECT_ID} \ --zone=${ZONE} \ --cluster-version=1.36 \ --machine-type=n2-standard-8 \ --num-nodes=1 \ --addons=RayOperator \ --enable-image-streaming \ --enable-dataplane-v2 \ --workload-pool=${PROJECT_ID}.svc.id.googConfigure the
kubectlcommand-line tool to communicate with your cluster:gcloud container clusters get-credentials ${CLUSTER_NAME} \ --project=${PROJECT_ID} \ --zone=${ZONE}Create a 16-device multi-host TPU slice node pool with a
4x4topology across four hosts on TPU Trillium (v6e) or a2x2x2topology across two hosts on Ironwood (TPU7x):Trillium (v6e)
gcloud container node-pools create tpu-multi-host-pool \ --project=${PROJECT_ID} \ --cluster=${CLUSTER_NAME} \ --zone=${ZONE} \ --machine-type=ct6e-standard-4t \ --tpu-topology=4x4 \ --num-nodes=4Ironwood (TPU7x)
gcloud container node-pools create tpu-multi-host-pool \ --project=${PROJECT_ID} \ --cluster=${CLUSTER_NAME} \ --zone=${ZONE} \ --machine-type=tpu7x-standard-4t \ --tpu-topology=2x2x2 \ --num-nodes=2
Create a Cloud Storage bucket to store shared Ray Train checkpoints across the multi-host TPU nodes and grant the
defaultKubernetes ServiceAccount resource access to the bucket:gcloud storage buckets create gs://${BUCKET_NAME} --location=${REGION} PROJECT_NUMBER=$(gcloud projects describe ${PROJECT_ID} --format="value(projectNumber)") gcloud storage buckets add-iam-policy-binding gs://${BUCKET_NAME} \ --member="principal://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${PROJECT_ID}.svc.id.goog/subject/ns/default/sa/default" \ --role="roles/storage.objectAdmin"Create a Kubernetes Secret with your Hugging Face access token so that Ray Pods can download the gated Gemma 4 weights:
kubectl create secret generic hf-secret \ --from-literal=hf_api_token=${HF_TOKEN}
Build the container image
Build a container image that installs Ray and the Hugging Face training libraries on top of the prebuilt TorchTPU base image, which includes TorchTPU and the libtpu library.
Create an Artifact Registry Docker repository and configure Docker authentication:
gcloud artifacts repositories create ${ARTIFACT_REGISTRY} \ --repository-format=docker \ --location=${REGION} gcloud auth configure-docker ${REGION}-docker.pkg.dev,us-docker.pkg.devInspect the
Dockerfilein the sample directory:Single-slice training with the Ray
TorchTrainerclass and per-chiplet scheduling on Ironwood (TPU7x) require Ray version 2.58.0 or later, and fine-tuning Gemma 4 requires thetransformerslibrary version 5.5.0 or later.Build and push the image to Artifact Registry:
docker build -t ${IMAGE_URI} . docker push ${IMAGE_URI}
Deploy the RayCluster custom resource
Deploy a RayCluster custom resource that provisions a CPU head Pod, an in-memory /dev/shm volume for the Ray shared-memory object store, and a multi-host TPU worker group.
Review the RayCluster manifest for your TPU version in the sample repository:
Trillium (v6e)
Ironwood (TPU7x)
On Ironwood (TPU7x), each physical chip contains two chiplets, providing eight logical TPU cores per four-chip host. The manifest sets the value of the
TPUfield to"8"in theresourcessection and sets theRAY_TPU_RESOURCE_PER_CHIPenvironment variable to"2"to advertise and schedule eight logicalTPUresources per host.Apply the manifest to create the RayCluster custom resource:
Trillium (v6e)
envsubst < ray-cluster.tpu-v6e-multihost.yaml | kubectl apply -f -Ironwood (TPU7x)
envsubst < ray-cluster.tpu-v7x-multihost.yaml | kubectl apply -f -Wait until the RayCluster head Pod and all TPU worker Pods reach the
Runningstate:kubectl get pods -l ray.io/cluster=ray-torchtpu-clusterThe output is similar to the following:
Trillium (v6e)
NAME READY STATUS RESTARTS AGE ray-torchtpu-cluster-head-pdpk6 1/1 Running 0 2m30s ray-torchtpu-cluster-v6e-16-worker-brwpc 1/1 Running 0 2m30s ray-torchtpu-cluster-v6e-16-worker-dx4w7 1/1 Running 0 2m30s ray-torchtpu-cluster-v6e-16-worker-h4d4w 1/1 Running 0 2m30s ray-torchtpu-cluster-v6e-16-worker-t2g24 1/1 Running 0 2m30sIronwood (TPU7x)
NAME READY STATUS RESTARTS AGE ray-torchtpu-cluster-head-k9x2m 1/1 Running 0 2m30s ray-torchtpu-cluster-tpu7x-16-worker-7cwpq 1/1 Running 0 2m30s ray-torchtpu-cluster-tpu7x-16-worker-m98r9 1/1 Running 0 2m30sInspect the environment variables on one of the worker Pods to confirm that the KubeRay TPU webhook injected the multi-host TPU coordination and TorchTPU topology variables:
kubectl get pods -l ray.io/node-type=worker \ -o=jsonpath='{range .items[0].spec.containers[0].env[*]}{.name}={.value}{"\n"}{end}' \ | grep -E "TPU_WORKER_ID|TPU_WORKER_HOSTNAMES|TPU_NAME|TORCH_TPU_TOPOLOGY|TORCH_TPU_SLICEBUILDER_ADDRESSES"The output is similar to the following:
Trillium (v6e)
TPU_WORKER_HOSTNAMES=v6e-16-0-0.ray-torchtpu-cluster-headless,v6e-16-0-1.ray-torchtpu-cluster-headless,v6e-16-0-2.ray-torchtpu-cluster-headless,v6e-16-0-3.ray-torchtpu-cluster-headless TORCH_TPU_TOPOLOGY=4,4,1 TORCH_TPU_SLICEBUILDER_ADDRESSES=v6e-16-0-0.ray-torchtpu-cluster-headless:8471,v6e-16-0-0.ray-torchtpu-cluster-headless:8472,v6e-16-0-0.ray-torchtpu-cluster-headless:8473,v6e-16-0-0.ray-torchtpu-cluster-headless:8474,v6e-16-0-1.ray-torchtpu-cluster-headless:8471,v6e-16-0-1.ray-torchtpu-cluster-headless:8472,v6e-16-0-1.ray-torchtpu-cluster-headless:8473,v6e-16-0-1.ray-torchtpu-cluster-headless:8474,v6e-16-0-2.ray-torchtpu-cluster-headless:8471,v6e-16-0-2.ray-torchtpu-cluster-headless:8472,v6e-16-0-2.ray-torchtpu-cluster-headless:8473,v6e-16-0-2.ray-torchtpu-cluster-headless:8474,v6e-16-0-3.ray-torchtpu-cluster-headless:8471,v6e-16-0-3.ray-torchtpu-cluster-headless:8472,v6e-16-0-3.ray-torchtpu-cluster-headless:8473,v6e-16-0-3.ray-torchtpu-cluster-headless:8474 TPU_WORKER_ID=3 TPU_NAME=v6e-16-0Ironwood (TPU7x)
TPU_WORKER_HOSTNAMES=tpu7x-16-0-0.ray-torchtpu-cluster-headless,tpu7x-16-0-1.ray-torchtpu-cluster-headless TORCH_TPU_TOPOLOGY=2,2,2,2 TORCH_TPU_SLICEBUILDER_ADDRESSES=tpu7x-16-0-0.ray-torchtpu-cluster-headless:8471,tpu7x-16-0-0.ray-torchtpu-cluster-headless:8472,tpu7x-16-0-0.ray-torchtpu-cluster-headless:8473,tpu7x-16-0-0.ray-torchtpu-cluster-headless:8474,tpu7x-16-0-0.ray-torchtpu-cluster-headless:8475,tpu7x-16-0-0.ray-torchtpu-cluster-headless:8476,tpu7x-16-0-0.ray-torchtpu-cluster-headless:8477,tpu7x-16-0-0.ray-torchtpu-cluster-headless:8478,tpu7x-16-0-1.ray-torchtpu-cluster-headless:8471,tpu7x-16-0-1.ray-torchtpu-cluster-headless:8472,tpu7x-16-0-1.ray-torchtpu-cluster-headless:8473,tpu7x-16-0-1.ray-torchtpu-cluster-headless:8474,tpu7x-16-0-1.ray-torchtpu-cluster-headless:8475,tpu7x-16-0-1.ray-torchtpu-cluster-headless:8476,tpu7x-16-0-1.ray-torchtpu-cluster-headless:8477,tpu7x-16-0-1.ray-torchtpu-cluster-headless:8478 TPU_WORKER_ID=1 TPU_NAME=tpu7x-16-0Establish a port-forwarding session to the Ray head service:
kubectl port-forward svc/ray-torchtpu-cluster-head-svc 8265:8265 >/dev/null 2>&1 &You can inspect the TPU worker nodes and their auto-detected
ray.io/tpu-*labels in the Cluster tab of the Ray Dashboard.
Run distributed tensor parallelism with Ray Core
Ray Core provides the slice_placement_group function in the ray.util.tpu module to reserve an intact multi-host TPU slice and the PlacementGroupSchedulingStrategy class to schedule one Ray actor per logical TPU device across the slice.
Review the
ray_core_collectives.pyscript in the sample repository:The script runs distributed tensor parallelism across the TPU slice in three steps:
- The
slice_placement_groupfunction reserves a 16-device TPU slice and schedules oneTorchTPUSliceWorkeractor on each TPU device. - Each actor computes its global rank from its host index and local device ID and then initializes the
tpu_distprocess group. - The
init_device_meshandparallelize_modulefunctions shard the feed-forward layers across all 16 TPU devices, verify the output against a single-device CPU reference, and run anall_reducecollective over ICI.
- The
Submit the Ray Core job for your TPU version:
Trillium (v6e)
ray job submit \ --address http://localhost:8265 \ --working-dir . \ --runtime-env-json '{"excludes": ["ray-env", ".git", "*.whl"]}' \ -- python3 ray_core_collectives.py --topology=4x4 --accelerator-type=TPU-V6EIronwood (TPU7x)
ray job submit \ --address http://localhost:8265 \ --working-dir . \ --runtime-env-json '{"excludes": ["ray-env", ".git", "*.whl"]}' \ -- python3 ray_core_collectives.py --topology=2x2x2 --accelerator-type=TPU-V7XThe output is similar to the following:
Trillium (v6e)
Rank 0 (worker_id=0, local_rank=0, ip=10.72.3.20): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 1 (worker_id=0, local_rank=1, ip=10.72.3.20): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 2 (worker_id=0, local_rank=2, ip=10.72.3.20): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 3 (worker_id=0, local_rank=3, ip=10.72.3.20): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 4 (worker_id=1, local_rank=0, ip=10.72.14.20): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 5 (worker_id=1, local_rank=1, ip=10.72.14.20): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 6 (worker_id=1, local_rank=2, ip=10.72.14.20): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 7 (worker_id=1, local_rank=3, ip=10.72.14.20): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 8 (worker_id=2, local_rank=0, ip=10.72.7.20): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 9 (worker_id=2, local_rank=1, ip=10.72.7.20): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 10 (worker_id=2, local_rank=2, ip=10.72.7.20): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 11 (worker_id=2, local_rank=3, ip=10.72.7.20): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 12 (worker_id=3, local_rank=0, ip=10.72.6.20): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 13 (worker_id=3, local_rank=1, ip=10.72.6.20): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 14 (worker_id=3, local_rank=2, ip=10.72.6.20): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 15 (worker_id=3, local_rank=3, ip=10.72.6.20): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] All 16 TorchTPU ranks verified DTensor TP parity (max_abs_diff=0.002470) and all_reduce sum = 136.0 ------------------------------------------ Job 'raysubmit_HFTkKrjSnfza3ZFD' succeeded ------------------------------------------Ironwood (TPU7x)
Rank 0 (worker_id=0, local_rank=0, ip=172.16.14.6): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 1 (worker_id=0, local_rank=1, ip=172.16.14.6): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 2 (worker_id=0, local_rank=2, ip=172.16.14.6): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 3 (worker_id=0, local_rank=3, ip=172.16.14.6): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 4 (worker_id=0, local_rank=4, ip=172.16.14.6): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 5 (worker_id=0, local_rank=5, ip=172.16.14.6): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 6 (worker_id=0, local_rank=6, ip=172.16.14.6): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 7 (worker_id=0, local_rank=7, ip=172.16.14.6): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 8 (worker_id=1, local_rank=0, ip=172.16.15.7): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 9 (worker_id=1, local_rank=1, ip=172.16.15.7): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 10 (worker_id=1, local_rank=2, ip=172.16.15.7): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 11 (worker_id=1, local_rank=3, ip=172.16.15.7): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 12 (worker_id=1, local_rank=4, ip=172.16.15.7): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 13 (worker_id=1, local_rank=5, ip=172.16.15.7): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 14 (worker_id=1, local_rank=6, ip=172.16.15.7): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] Rank 15 (worker_id=1, local_rank=7, ip=172.16.15.7): all_reduce_sum=[136.0, 136.0, 136.0, 136.0] All 16 TorchTPU ranks verified DTensor TP parity (max_abs_diff=0.002470) and all_reduce sum = 136.0 ------------------------------------------ Job 'raysubmit_MrJbAe3hvARBcBN2' succeeded ------------------------------------------
Fine-tune a Gemma 4 model with Ray Train and PyTorch FSDP2
The Ray Train TorchTrainer class automates TPU slice reservation, worker placement, and torch.distributed initialization across a multi-host TPU slice.
Review the
ray_train_llm_finetune.pyscript in the sample repository, which fine-tunes thegoogle/gemma-4-E2B-itmodel on thedatabricks/databricks-dolly-15kinstruction dataset:The script configures the
TorchTrainerclass with the following components:- The
ScalingConfigclass configures the distributed worker topology for the training job. Setting theuse_tpu=True,num_workers=16,topology, andaccelerator_typeparameters reserves a 16-device TPU slice and schedules one worker per TPU core. - The
TorchConfigclass configures thetorch.distributedprocess group across the workers. Setting thebackend="tpu_dist"parameter connects all 16 workers over the TPU ICI network before thetrain_funcfunction runs. - The
create_dataloaderfunction downloads and tokenizes the instruction dataset, batches the token sequences with a PyTorchDataLoaderobject, and calls theray.train.torch.prepare_data_loaderfunction so each worker receives a disjoint shard of each global batch on its assigned TPU device. - The
train_funcfunction loads the model inbfloat16precision and calls the PyTorch FSDP2fully_shardfunction on each feed-forward block and the root model to shard parameters, gradients, and optimizer states across all 16 TPU cores. - The
Checkpointclass andray.train.reportfunction gather the full model state dictionary on rank 0 at the end of each epoch and upload the checkpoint to your Cloud Storage bucket.
- The
Submit the
TorchTrainerFSDP2 fine-tuning job for your TPU version:Trillium (v6e)
ray job submit \ --address http://localhost:8265 \ --working-dir . \ --runtime-env-json '{"excludes": ["ray-env", ".git", "*.whl"]}' \ -- python3 ray_train_llm_finetune.py \ --topology=4x4 \ --accelerator-type=TPU-V6E \ --storage-path=gs://${BUCKET_NAME}/ray_train_torchtpuIronwood (TPU7x)
ray job submit \ --address http://localhost:8265 \ --working-dir . \ --runtime-env-json '{"excludes": ["ray-env", ".git", "*.whl"]}' \ -- python3 ray_train_llm_finetune.py \ --topology=2x2x2 \ --accelerator-type=TPU-V7X \ --storage-path=gs://${BUCKET_NAME}/ray_train_torchtpuThe output is similar to the following:
Trillium (v6e)
Starting Ray Train FSDP2 SFT job: model=google/gemma-4-E2B-it, topology=4x4, accelerator_type=TPU-V6E, num_workers=16 (TrainController pid=5407) Attempting to start training worker group of size 16 with the following resources: [{'TPU': 1, 'accelerator_type:TPU-V6E': 0.001}] * 16 (RayTrainWorker pid=19733, ip=10.72.3.20) Setting up process group for: env:// [rank=0, world_size=16] (RayTrainWorker pid=19733, ip=10.72.3.20) Epoch 1/3 | loss=3.1402 | epoch_time=28.17s | tokens/s=18613.5 (RayTrainWorker pid=19733, ip=10.72.3.20) Checkpoint successfully created at: Checkpoint(filesystem=gcs, path=my-project-ray-torchtpu/ray_train_torchtpu/torchtpu_gemma4_fsdp2_sft_1791864427/checkpoint_2026-10-13_04-08-10.762175) (RayTrainWorker pid=19733, ip=10.72.3.20) Epoch 2/3 | loss=2.3318 | epoch_time=26.07s | tokens/s=20112.8 (RayTrainWorker pid=19733, ip=10.72.3.20) Checkpoint successfully created at: Checkpoint(filesystem=gcs, path=my-project-ray-torchtpu/ray_train_torchtpu/torchtpu_gemma4_fsdp2_sft_1791864427/checkpoint_2026-10-13_04-10-02.548088) (RayTrainWorker pid=19733, ip=10.72.3.20) Epoch 3/3 | loss=2.2050 | epoch_time=25.52s | tokens/s=20545.2 (RayTrainWorker pid=19733, ip=10.72.3.20) Checkpoint successfully created at: Checkpoint(filesystem=gcs, path=my-project-ray-torchtpu/ray_train_torchtpu/torchtpu_gemma4_fsdp2_sft_1791864427/checkpoint_2026-10-13_04-11-48.701853) Training completed! Final metrics: {'epoch': 3, 'loss': 2.205, 'epoch_time_s': 25.52, 'tokens_per_s': 20545.2} Saved checkpoint: Checkpoint(filesystem=gcs, path=my-project-ray-torchtpu/ray_train_torchtpu/torchtpu_gemma4_fsdp2_sft_1791864427/checkpoint_2026-10-13_04-11-48.701853) ------------------------------------------ Job 'raysubmit_zbm4pnFZitA1YECA' succeeded ------------------------------------------Ironwood (TPU7x)
Starting Ray Train FSDP2 SFT job: model=google/gemma-4-E2B-it, topology=2x2x2, accelerator_type=TPU-V7X, num_workers=16 (TrainController pid=1162) Attempting to start training worker group of size 16 with the following resources: [{'TPU': 1, 'accelerator_type:TPU-V7X': 0.001}] * 16 (RayTrainWorker pid=257, ip=172.16.26.6) Setting up process group for: env:// [rank=0, world_size=16] (RayTrainWorker pid=257, ip=172.16.26.6) Epoch 1/3 | loss=3.1407 | epoch_time=26.28s | tokens/s=19953.3 (RayTrainWorker pid=257, ip=172.16.26.6) Checkpoint successfully created at: Checkpoint(filesystem=gcs, path=my-project-ray-torchtpu/ray_train_torchtpu/torchtpu_gemma4_fsdp2_sft_1791867004/checkpoint_2026-10-13_04-52-00.026930) (RayTrainWorker pid=257, ip=172.16.26.6) Epoch 2/3 | loss=2.3235 | epoch_time=16.68s | tokens/s=31429.9 (RayTrainWorker pid=257, ip=172.16.26.6) Checkpoint successfully created at: Checkpoint(filesystem=gcs, path=my-project-ray-torchtpu/ray_train_torchtpu/torchtpu_gemma4_fsdp2_sft_1791867004/checkpoint_2026-10-13_04-53-33.533341) (RayTrainWorker pid=257, ip=172.16.26.6) Epoch 3/3 | loss=2.1964 | epoch_time=16.98s | tokens/s=30884.6 (RayTrainWorker pid=257, ip=172.16.26.6) Checkpoint successfully created at: Checkpoint(filesystem=gcs, path=my-project-ray-torchtpu/ray_train_torchtpu/torchtpu_gemma4_fsdp2_sft_1791867004/checkpoint_2026-10-13_04-55-01.508219) Training completed! Final metrics: {'epoch': 3, 'loss': 2.1964, 'epoch_time_s': 16.98, 'tokens_per_s': 30884.6} Saved checkpoint: Checkpoint(filesystem=gcs, path=my-project-ray-torchtpu/ray_train_torchtpu/torchtpu_gemma4_fsdp2_sft_1791867004/checkpoint_2026-10-13_04-55-01.508219) ------------------------------------------ Job 'raysubmit_zSNnCXhHkwKiSmJh' succeeded ------------------------------------------The training output shows the following:
- Epoch 1 includes initial XLA graph compilation. Epochs 2 and 3 reuse the cached executables from
/dev/shm/torch_tpu_cache, reaching over 20,000 tokens per second on TPU Trillium (v6e) and 31,000 tokens per second on Ironwood (TPU7x). - The global cross-entropy loss synchronized across all 16 FSDP2 worker ranks decreases from 3.14 in Epoch 1 to 2.20 in Epoch 3 across the three fine-tuning epochs on the
databricks/databricks-dolly-15kinstruction dataset. - At the end of each epoch, rank 0 gathers the full model state dictionary and uploads the checkpoint to your Cloud Storage bucket, retaining the lowest-loss checkpoint as configured in the
CheckpointConfig(num_to_keep=1, checkpoint_score_attribute="loss", checkpoint_score_order="min")object.
- Epoch 1 includes initial XLA graph compilation. Epochs 2 and 3 reuse the cached executables from
Observe workload performance
To monitor TPU TensorCore utilization, HBM usage, and Ray cluster metrics in Cloud Monitoring, see Collect and view logs and metrics for Ray clusters on GKE and View TPU metrics on the Ray Dashboard.
Access the Ray Dashboard
To inspect the status of your Ray actors, placement groups, and training jobs in the built-in Ray Dashboard, follow these steps:
If port forwarding to the Ray head service is not already running, start it in the background:
kubectl port-forward svc/ray-torchtpu-cluster-head-svc 8265:8265 >/dev/null 2>&1 &In Cloud Shell, click
Web Preview and select Preview on port 8265, or open
http://localhost:8265in your local browser.Click the Jobs, Cluster, and Metrics tabs to inspect worker logs, placement group state, TPU node labels, and the TPU Utilization by Node graph.
Clean up
To avoid incurring charges to your Google Cloud account for the resources used in this tutorial, delete the RayCluster custom resource, the GKE cluster, the Cloud Storage bucket, and the Artifact Registry repository:
Stop the background port-forwarding process and delete the RayCluster custom resource and Kubernetes Secret:
pkill -f "kubectl port-forward" || true kubectl delete raycluster ray-torchtpu-cluster kubectl delete secret hf-secret --ignore-not-foundDelete the GKE cluster:
Autopilot
gcloud container clusters delete ${CLUSTER_NAME} \ --region=${REGION}Standard
gcloud container clusters delete ${CLUSTER_NAME} \ --zone=${ZONE}Delete the Cloud Storage bucket and its checkpoint contents:
gcloud storage rm --recursive gs://${BUCKET_NAME}Delete the Artifact Registry repository:
gcloud artifacts repositories delete ${ARTIFACT_REGISTRY} \ --location=${REGION}
What's next
- Learn more about PyTorch on Cloud TPU.
- Explore the TorchTPU documentation.
- Learn how to serve LLMs on multi-host TPUs with Ray and TorchTPU.
- Learn how to optimize AI training on TPUs with DWS and Kueue.
- Read About TPUs in GKE.
- Explore the Ray Train documentation.