Tutorial: Multi-host RL Training with TPUs and SkyRL on GKE

SkyRL is a scalable and flexible reinforcement learning framework designed to handle the complex memory and compute patterns required for large language model (LLM) alignment. It is optimized for distributed environments, enabling efficient training by decoupling data, model weights, and the training engine.

Image reference: https://docs.skyrl.ai/docs/tinker/architecture

SkyRL provides a backend implementation for the Tinker API using JAX, allowing you to scale LLM post-training on TPUs or GPUs . The integration contains three layers:

  1. API Layer: FastAPI HTTP server that accepts Tinker API requests, stores them in a database, and returns future IDs for async polling.

  2. Engine Layer: Background subprocess that polls the database, batches pending requests, and dispatches them to the backend.

  3. Backend Layer: Translates Tinker operations into training and inference calls, managing Ray workers, JAX, FSDP2/Megatron training, and vLLM inference

See SkyRL architecture documentation for more details.

Why JAX and TPUs?

SkyRL utilizes JAX as its distributed backend for training and sampling during reinforcement learning. Because JAX is hardware-agnostic and offers primitives that scale seamlessly from local development to clusters of thousands of chips, SkyRL can extend model training across both GPUs and TPUs with little to no changes.

Furthermore, SkyRL leverages JAX capabilities such as JIT compilation to achieve high-throughput execution. This significantly boosts performance, particularly on TPUs, which are natively optimized for JAX-based workloads.

What We Are Building

In this tutorial, we will perform multi-host RL training (FSDP and tensor parallelism) on Qwen3-0.6B using SkyRL on GKE and Trillium TPUs.

Prerequisites

Step 1: Configure access to the GKE cluster

Get cluster credentials to access the GKE cluster:

export PROJECT_ID=<YOUR_PROJECT_ID>
export CLUSTER_REGION=<YOUR_REGION>
export CLUSTER_NAME=<YOUR_CLUSTER_NAME>

gcloud container clusters get-credentials $CLUSTER_NAME --region $CLUSTER_REGION --project-id $PROJECT_ID

Step 2: Install the JobSet controller

In this tutorial, we will use the JobSet API to orchestrate parallel execution of SkyRL workers.

First, install the JobSet CRDs and controller:

VERSION=v0.11.1
kubectl apply --server-side -f https://github.com/kubernetes-sigs/jobset/releases/download/$VERSION/manifests.yaml

Verify installation by confirming that the JobSet controller is running:

kubectl -n jobset-system get pods

Step 3: Deploy JobSet

In this tutorial, we will create a JobSet resource that will orchestrate and deploy each SkyRL worker on a 4x4 TPU v6e slice. Worker 0 is configured to start the SkyRL Tinker API server and engine.

Create the JobSet manifest:

# jobset.yaml
apiVersion: jobset.x-k8s.io/v1alpha2
kind: JobSet
metadata:
  name: skyrl-tx-job
spec:
  failurePolicy:
    maxRestarts: 0
  replicatedJobs:
    - name: skyrl-workers
      replicas: 1
      template:
        spec:
          parallelism: 4
          completions: 4
          backoffLimit: 0
          template:
            spec:
              affinity:
                podAffinity:
                  requiredDuringSchedulingIgnoredDuringExecution:
                  - labelSelector:
                      matchExpressions:
                      - key: jobset.sigs.k8s.io/jobset-name
                        operator: In
                        values:
                        - skyrl-tx-job
                    topologyKey: cloud.google.com/gke-nodepool 
              subdomain: test
              restartPolicy: Never
              nodeSelector:
                cloud.google.com/gke-tpu-accelerator: tpu-v6e-slice
                cloud.google.com/gke-tpu-topology: 4x4
              containers:
              - name: worker
                image: us-docker.pkg.dev/cloud-tpu-images/jax-ai-image/tpu:latest
                securityContext:
                  privileged: false
                command:
                - bash
                - -c
                - |
                  set -ex

                  git clone https://github.com/NovaSky-AI/SkyRL.git
                  cd SkyRL

                  if [ "$JOB_COMPLETION_INDEX" -eq "0" ]; then
                    uv run --extra tpu --extra tinker --extra jax -m skyrl.tinker.api \
                    --base-model "Qwen/Qwen3-0.6B" \
                    --session-timeout-sec 86400 \
                    --backend-config "{\"train_micro_batch_size\": 8, \"sample_max_num_sequences\": 256, \"tensor_parallel_size\": 4, \"fully_sharded_data_parallel_size\": 4, \"num_processes\": 4, \"coordinator_address\": \"skyrl-tx-job-skyrl-workers-0-0.skyrl-tx-job:7777\"}"
                  else
                    sleep 60
                    uv run --extra tpu --extra tinker --extra jax -m skyrl.backends.jax \
                    --coordinator-address skyrl-tx-job-skyrl-workers-0-0.skyrl-tx-job:7777 \
                    --num-processes 4 \
                    --process-id "$JOB_COMPLETION_INDEX"
                  fi
                resources:
                  requests:
                    google.com/tpu: 4
                  limits:
                    google.com/tpu: 4

Apply the resource with kubectl:

kubectl apply -f jobset.yaml

Step 4: Verify the Tinker API server and engine

Retrieve logs from the first replicated job running the Tinker API server and engine:

kubectl logs -f -l batch.kubernetes.io/job-completion-index=0,job-name=skyrl-tx-job-skyrl-workers-0

Verify engine and workers have started from the output:

Installed 144 packages in 650ms
2026-03-26 17:18:44,231 - INFO - uvicorn.error: Started server process [401]
2026-03-26 17:18:44,233 - INFO - uvicorn.error: Waiting for application startup.
2026-03-26 17:18:45,877 - INFO - skyrl: Using internal engine for inference
2026-03-26 17:18:45,878 - DEBUG - skyrl: Detected API server uv run flags:
['--extra', 'tpu', '--extra', 'tinker', '--extra', 'jax']
2026-03-26 17:18:45,884 - INFO - skyrl: Started background engine with PID 404:
uv run --extra tpu --extra tinker --extra jax --extra tinker --extra jax -m
skyrl.tinker.engine --base-model Qwen/Qwen3-0.6B --backend jax --backend-config
{"train_micro_batch_size": 8, "sample_max_num_sequences": 256,
"tensor_parallel_size": 4, "fully_sharded_data_parallel_size": 4,
"num_processes": 4, "coordinator_address":
"skyrl-tx-job-skyrl-workers-0-0.skyrl-tx-job:7777"} --checkpoints-base
/tmp/skyrl_checkpoints --database-url
sqlite:////jax-ai-image/SkyRL/skyrl/tinker/tinker.db
--external-inference-api-key EMPTY --external-inference-lora-base
/tmp/lora_models --session-cleanup-interval-sec 60 --session-timeout-sec 300
2026-03-26 17:18:45,886 - INFO - uvicorn.error: Application startup complete.
2026-03-26 17:18:45,888 - INFO - uvicorn.error: Uvicorn running on
http://0.0.0.0:8000 (Press CTRL+C to quit)
warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning.
WARNING:absl:Tensorflow library not found, tensorflow.io.gfile operations will use native shim calls. GCS paths (i.e. 'gs://...') cannot be accessed.
2026-03-26 17:20:32,331 - INFO - skyrl: JAX distributed initialized:
process_id=0 (4 total), local devices: 4, total devices: 16
Fetching 11 files: 100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 11/11 [00:06<00:00,  1.83it/s]

Step 5: Run the RL training loop

In this example, we apply RL to enhance the model’s mathematical problem-solving capabilities. This walkthrough utilizes the math RL example from the tinker cookbook examples. Execute the following command to initiate the RL training process:

POD=$(kubectl get pod -l batch.kubernetes.io/job-completion-index=0,job-name=skyrl-tx-job-skyrl-workers-0 -o custom-columns=":metadata.name" --no-headers)

kubectl exec -ti $POD -- sh -c 'cd SkyRL && export TINKER_API_KEY=tml-dummy && uv run --with "tinker-cookbook[math-rl] @ git+https://github.com/thinking-machines-lab/tinker-cookbook.git@nightly" python -m tinker_cookbook.recipes.math_rl.train base_url=http://localhost:8000 model_name="Qwen/Qwen3-0.6B" group_size=4 groups_per_batch=100 learning_rate=1e-4 max_tokens=512 save_every=0'

You can observe the model’s progress across multiple rollouts, each assigned a reward based on its outcome. For instance, the following output represents a high-reward trajectory where the model successfully solved the math problem.

****** trajectory idx=2, reward=1 ******
Per-step metrics:
  Step 0:
    format: 1.0
    correct: 1.0
---- datum ----
<|im_start|>user
What is 4 + 5?<|im_end|>
<|im_start|>assistant
9<|im_end|>
<|im_start|>user
What is 56 + 72?<|im_end|>
<|im_start|>assistant
<think>
Alright, the user is asking 56 plus 72. Let me add those numbers. 56 plus 70 is 126, and then add the remaining 2, so 128. Yep, that seems right. To make sure, I can break it down: 50 + 70 is 120, and 6 + 2 is 8, so 120 + 8 = 128. Final answer is 128.
</think>

128<|im_end|>

You can view the training metrics to view the model’s progress as it learns to solve Math problems. The metrics should be included in the output after each training step like the following:

                                Step 18
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Metric                                                  ┃ Value      ┃
┑━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
β”‚ env/all/ac_tokens_per_turn                              β”‚ 484.094727 β”‚
β”‚ env/all/by_group/frac_all_bad                           β”‚ 0.453125   β”‚
β”‚ env/all/by_group/frac_all_good                          β”‚ 0.046875   β”‚
β”‚ env/all/by_group/frac_mixed                             β”‚ 0.500000   β”‚
β”‚ env/all/correct                                         β”‚ 0.268555   β”‚
β”‚ env/all/format                                          β”‚ 0.212891   β”‚
β”‚ env/all/ob_tokens_per_turn                              β”‚ 217.890625 β”‚
β”‚ env/all/reward/total                                    β”‚ 0.189844   β”‚
β”‚ env/all/total_ac_tokens                                 β”‚ 495713     β”‚
β”‚ env/all/total_episodes                                  β”‚ 1024       β”‚
β”‚ env/all/total_ob_tokens                                 β”‚ 223120     β”‚
β”‚ env/all/total_turns                                     β”‚ 1024       β”‚
β”‚ env/all/turns_per_episode                               β”‚ 1.000000   β”‚
β”‚ optim/entropy                                           β”‚ 0.431664   β”‚
β”‚ optim/kl_sample_train_v1                                β”‚ 0.003004   β”‚
β”‚ optim/kl_sample_train_v2                                β”‚ 0.003044   β”‚
β”‚ optim/lr                                                β”‚ 0.000020   β”‚
β”‚ progress/batch                                          β”‚ 18         β”‚
β”‚ progress/done_frac                                      β”‚ 0.101064   β”‚
β”‚ skyrl.ai/grad_norm                                      β”‚ 0.003937   β”‚
β”‚ skyrl.ai/learning_rate                                  β”‚ 0.000020   β”‚
β”‚ time/assemble_training_data                             β”‚ 0.725428   β”‚
β”‚ time/compute_full_batch_metrics_and_get_sampling_client β”‚ 2.225906   β”‚
β”‚ time/compute_group_rewards:max                          β”‚ 0.000005   β”‚
β”‚ time/compute_group_rewards:mean                         β”‚ 0.000003   β”‚
β”‚ time/compute_kl_sample_train                            β”‚ 0.125626   β”‚
β”‚ time/do_group_rollout_and_filter_constant_reward:max    β”‚ 98.730395  β”‚
β”‚ time/do_group_rollout_and_filter_constant_reward:mean   β”‚ 82.161999  β”‚
β”‚ time/do_train_step_and_get_sampling_client              β”‚ 66.614592  β”‚
β”‚ time/env_initial_observation:max                        β”‚ 0.399646   β”‚
β”‚ time/env_initial_observation:mean                       β”‚ 0.003795   β”‚
β”‚ time/env_step:max                                       β”‚ 0.008772   β”‚
β”‚ time/env_step:mean                                      β”‚ 0.000565   β”‚
β”‚ time/policy_sample:max                                  β”‚ 96.301080  β”‚
β”‚ time/policy_sample:mean                                 β”‚ 74.323960  β”‚
β”‚ time/prepare_minibatch                                  β”‚ 0.740415   β”‚
β”‚ time/sampling                                           β”‚ 98.864469  β”‚
β”‚ time/save_checkpoint                                    β”‚ 2.100218   β”‚
β”‚ time/save_checkpoint_and_get_sampling_client            β”‚ 2.100240   β”‚
β”‚ time/total                                              β”‚ 165.533009 β”‚
β”‚ time/train_step                                         β”‚ 63.602819  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

4 Likes