使用 Ray 和 TorchTPU 在多主机 TPU 上微调模型

本教程介绍了如何使用 Ray Core、Ray Train、PyTorch Fully Sharded Data Parallel (FSDP2) 和 TorchTPU,在 Google Kubernetes Engine (GKE) 中的多主机 Cloud TPU 切片上运行分布式 PyTorch 工作负载并微调 Gemma 4 模型。

本教程适用于机器学习 (ML) 工程师、平台管理员和运维人员,以及希望在 Kubernetes 中的多主机 TPU 切片上编排分布式 PyTorch 训练工作负载的数据和 AI 专家。如需详细了解我们在 Google Cloud 内容中提及的常见角色和示例任务,请参阅常见的 GKE 用户角色和任务。

在开始本教程之前,请确保您熟悉以下内容:

背景

本部分介绍本教程中使用的关键技术。

TPU

张量处理单元 (TPU) 是 Google 为机器学习工作负载定制设计的加速器。本教程同时支持 TPU Trillium (v6e) 和 Ironwood (TPU7x)。在多主机 TPU 切片中,多个 TPU 主机通过高速芯片间互连 (ICI) 网络直接通信,提供训练模型所需的聚合高带宽内存 (HBM) 和低延迟集体通信,这些模型超出了单个芯片或 TPU 主机的内存容量。

TorchTPU

TorchTPU 是适用于 Cloud TPU 的开源 PyTorch 后端,由 Google 和 Meta 合作开发。借助 TorchTPU,您只需对代码进行极少量的修改,即可在 TPU 硬件上运行现有的 PyTorch 模型,并通过加速线性代数 (XLA) 编译器将训练和推理工作负载从单芯片扩展到多切片 TPU Pod。TorchTPU 在 TPU ICI 网络上使用“每个设备一个进程”的执行模型,并支持标准 PyTorch API,包括即刻执行、torch.compile、torch.distributed、DeviceMesh、DTensor 和完全分片数据并行 (FSDP2)。

在 TPU 上运行 KubeRay 和 Ray

KubeRay 是一个 Kubernetes Operator,用于部署和管理 Ray 应用。启用 Ray on GKE 插件会部署托管式 KubeRay Operator 和 KubeRay TPU Webhook。当 KubeRay 创建请求 google.com/tpu 资源的 worker Pod 时,Webhook 会自动配置多主机协调所需的 Pod 主机名、无头 Service 和 TPU 环境变量。

Ray 会在启动时检测每个节点上的 TPU 拓扑,并在 TPU 切片之间调度分布式工作负载。您可以在两个级别编排分布式 TorchTPU 工作负载:

  • Ray Core 为自定义分布式执行循环(例如张量并行计算或强化学习 [RL] 流水线)提供拓扑感知型放置组和 actor 调度。
  • Ray Train 提供托管式分布式数据并行和模型并行训练,包括 PyTorch 分布式数据并行 (DDP)、PyTorch FSDP2 和 JAX 单程序多数据 (SPMD),并具有内置检查点和自动故障恢复功能。

目标

本教程介绍了如何执行以下操作:

  1. 在 TPU Trillium (v6e) 或 Ironwood (TPU7x) 上创建 Autopilot 或 Standard 模式的 GKE 集群。
  2. 构建一个容器映像,在 TorchTPU 基础映像之上叠加 Ray 和 Hugging Face 训练库。
  3. 部署 RayCluster 自定义资源,以预配头节点和 TPU 工作器节点。
  4. 使用 Ray Core 在 TPU 切片上通过 PyTorch DeviceMesh 和 DTensor 运行分布式张量并行计算。
  5. 使用 Ray Train,通过 PyTorch FSDP2 在多主机 TPU 切片上微调 Gemma 4 模型。

费用

在本教程中,您将使用 Google Cloud的以下收费组件:

您可使用价格计算器根据您的预计使用量来估算费用。

完成本教程中描述的任务后,您可以通过删除所创建的资源来避免继续计费。如需了解详情,请参阅清理。

准备工作

在开始之前,请确保您已执行以下任务:

  • 启用 Artifact Registry API、Cloud Storage API 和 Google Kubernetes Engine API。
  • 启用 API
  • 如需使用 Google Cloud CLI 执行此任务,请安装并初始化 gcloud CLI。如果您之前安装了 gcloud CLI,请通过运行 gcloud components update 命令来获取最新版本。较早版本的 gcloud CLI 可能不支持运行本文档中的命令。

准备环境

在本教程中,您将使用 Cloud Shell 来管理 Google Cloud上托管的资源。Cloud Shell 预安装了本教程所需的软件,包括 kubectl、docker、python3 和 gcloud CLI。

如需使用 Cloud Shell 设置您的环境,请按照以下步骤操作:

  1. 在 Google Cloud 控制台中,点击Google Cloud 控制台中的 Cloud Shell 激活图标。 激活 Cloud Shell 以启动 Cloud Shell 会话。此操作会在 Google Cloud 控制台的底部窗格中启动会话。

  2. 克隆 kubernetes-engine-samples 代码库并切换到本教程的示例目录:

    git clone https://github.com/GoogleCloudPlatform/kubernetes-engine-samples.git
    cd kubernetes-engine-samples/ai-ml/gke-ray/raytrain/torchtpu
    
  3. 创建并激活安装了 Ray CLI 的本地 Python 虚拟环境:

    python3 -m venv ~/ray-env
    source ~/ray-env/bin/activate
    pip install "ray[default]==2.58.0"
    
  4. 如需下载 Gemma 4 模型权重,请在 Hugging Face 上确认 Google 许可协议:

    1. 前往 Hugging Face 上的 Gemma 4 模型页面。
    2. 登录并点击同意并访问代码库,接受许可条款。
    3. 在 Hugging Face 账号设置中,生成具有 Read 权限的用户访问令牌。
  5. 为您的 Google Cloud 项目、集群、Cloud Storage 存储桶和 Hugging Face 令牌设置环境变量:

    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}
    

    替换以下内容:

    • PROJECT_ID:您的 Google Cloud 项目 ID。
    • CLUSTER_NAME:GKE 集群的名称,例如 ray-torchtpu-cluster。
    • REGION:Artifact Registry 代码库、Cloud Storage 存储桶和 TPU 容量所在的区域。
    • ZONE:您的 TPU 容量所在的可用区。如需了解按 TPU 版本划分的可用区域和可用区,请参阅 TPU 区域和可用区。
    • ARTIFACT_REGISTRY:Artifact Registry Docker 仓库的名称,例如 ray-torchtpu。
    • BUCKET_NAME:用于存储 Ray Train 检查点的 Cloud Storage 存储桶的名称,例如 PROJECT_ID-ray-torchtpu。
    • HF_TOKEN:您的 Hugging Face 访问令牌。

创建 GKE 集群

您可以在启用了托管式 Ray Operator 插件的 GKE Autopilot 或 Standard 集群中运行分布式 TorchTPU 训练工作负载。

  1. 创建 GKE 集群并为 kubectl 命令行工具配置凭据:

    Autopilot

    1. 创建启用了 Ray Operator 插件的区域级 GKE Autopilot 集群:

      gcloud container clusters create-auto ${CLUSTER_NAME} \
          --project=${PROJECT_ID} \
          --region=${REGION} \
          --cluster-version=1.36 \
          --enable-ray-operator
      

      在后面的步骤中部署 RayCluster 自定义资源时,GKE Autopilot 会自动预配与 Pod 资源请求和节点选择器匹配的 CPU 头节点和多主机 TPU 切片。

    2. 配置 kubectl 命令行工具以与您的集群通信:

      gcloud container clusters get-credentials ${CLUSTER_NAME} \
          --project=${PROJECT_ID} \
          --region=${REGION}
      

    标准

    1. 创建一个可用区级 GKE Standard 集群,该集群具有一个 CPU 节点池,用于托管 Ray 头部 Pod,并启用了映像流式传输、GKE Dataplane V2 和 Workload Identity Federation for GKE:

      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}.
      
    2. 配置 kubectl 命令行工具以与您的集群通信:

      gcloud container clusters get-credentials ${CLUSTER_NAME} \
          --project=${PROJECT_ID} \
          --zone=${ZONE}
      
    3. 在 TPU Trillium (v6e) 上创建一个包含 16 个设备的多主机 TPU 切片节点池,该节点池具有跨 4 个主机的 4x4 拓扑;或在 Ironwood (TPU7x) 上创建一个包含 16 个设备的多主机 TPU 切片节点池,该节点池具有跨 2 个主机的 2x2x2 拓扑:

      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=4
      

      Ironwood (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
      
  2. 创建一个 Cloud Storage 存储桶,用于存储多主机 TPU 节点之间共享的 Ray Train 检查点,并向 default Kubernetes ServiceAccount 资源授予对该存储桶的访问权限:

    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}./subject/ns/default/sa/default" \
        --role="roles/storage.objectAdmin"
    
  3. 使用您的 Hugging Face 访问令牌创建一个 Kubernetes Secret,以便 Ray Pod 可以下载受限的 Gemma 4 权重:

    kubectl create secret generic hf-secret \
        --from-literal=hf_api_token=${HF_TOKEN}
    

构建容器映像

构建一个容器映像,在预构建的 TorchTPU 基础映像(包含 TorchTPU 和 libtpu 库)之上安装 Ray 和 Hugging Face 训练库。

  1. 创建 Artifact Registry Docker 代码库并配置 Docker 身份验证:

    gcloud artifacts repositories create ${ARTIFACT_REGISTRY} \
        --repository-format=docker \
        --location=${REGION}
    
    gcloud auth configure-docker ${REGION}-docker.pkg.dev,us-docker.pkg.dev
    
  2. 检查示例目录中的 Dockerfile:

    FROM us-docker.pkg.dev/ml-oss-artifacts-published/torch-tpu-public-release-container/torch-tpu:latest
    
    # Install wget for KubeRay health probes, Ray (Core, Dashboard, and Train), and Hugging Face libraries for fine-tuning
    RUN apt-get update && apt-get install -y --no-install-recommends wget \
        && rm -rf /var/lib/apt/lists/* \
        && pip install --no-cache-dir \
        "ray[default,train]==2.58.0" \
        "transformers>=5.5.0" \
        datasets

    在 Ironwood (TPU7x) 上使用 Ray TorchTrainer 类进行单切片训练和按小芯片调度需要 Ray 2.58.0 或更高版本,而微调 Gemma 4 需要 transformers 库 5.5.0 或更高版本。

  3. 构建映像并将其推送到 Artifact Registry:

    docker build -t ${IMAGE_URI} .
    docker push ${IMAGE_URI}
    

部署 RayCluster 自定义资源

部署 RayCluster 自定义资源,该资源会预配一个 CPU 头 Pod、一个用于 Ray 共享内存对象存储区的内存中 /dev/shm 卷,以及一个多主机 TPU 工作进程组。

  1. 在示例代码库中查看适用于您的 TPU 版本的 RayCluster 清单:

    Trillium (v6e)

    apiVersion: ray.io/v1
    kind: RayCluster
    metadata:
      name: ray-torchtpu-cluster
    spec:
      headGroupSpec:
        rayStartParams:
          num-cpus: "0"
        template:
          spec:
            containers:
            - name: ray-head
              image: ${IMAGE_URI}
              env:
              - name: HF_TOKEN
                valueFrom:
                  secretKeyRef:
                    name: hf-secret
                    key: hf_api_token
              ports:
              - containerPort: 6379
                name: gcs
              - containerPort: 8265
                name: dashboard
              - containerPort: 10001
                name: client
              resources:
                limits:
                  cpu: "4"
                  memory: "16Gi"
                requests:
                  cpu: "4"
                  memory: "16Gi"
      workerGroupSpecs:
      - groupName: v6e-16
        replicas: 1
        numOfHosts: 4
        template:
          spec:
            containers:
            - name: ray-worker
              image: ${IMAGE_URI}
              env:
              - name: HF_TOKEN
                valueFrom:
                  secretKeyRef:
                    name: hf-secret
                    key: hf_api_token
              resources:
                limits:
                  cpu: "64"
                  memory: "200Gi"
                  google.com/tpu: "4"
                requests:
                  cpu: "64"
                  memory: "200Gi"
                  google.com/tpu: "4"
              volumeMounts:
              - mountPath: /dev/shm
                name: dshm
            volumes:
            - name: dshm
              emptyDir:
                medium: Memory
                sizeLimit: 100Gi
            nodeSelector:
              cloud.google.com/gke-tpu-accelerator: tpu-v6e-slice
              cloud.google.com/gke-tpu-topology: 4x4

    Ironwood (TPU7x)

    apiVersion: ray.io/v1
    kind: RayCluster
    metadata:
      name: ray-torchtpu-cluster
    spec:
      headGroupSpec:
        rayStartParams:
          num-cpus: "0"
        template:
          spec:
            containers:
            - name: ray-head
              image: ${IMAGE_URI}
              env:
              - name: HF_TOKEN
                valueFrom:
                  secretKeyRef:
                    name: hf-secret
                    key: hf_api_token
              - name: RAY_TPU_RESOURCE_PER_CHIP
                value: "2"
              ports:
              - containerPort: 6379
                name: gcs
              - containerPort: 8265
                name: dashboard
              - containerPort: 10001
                name: client
              resources:
                limits:
                  cpu: "4"
                  memory: "16Gi"
                requests:
                  cpu: "4"
                  memory: "16Gi"
      workerGroupSpecs:
      - groupName: tpu7x-16
        replicas: 1
        numOfHosts: 2
        resources:
          # Advertises 8 logical TPU cores (2 chiplets per physical chip) per host.
          TPU: "8"
        template:
          spec:
            containers:
            - name: ray-worker
              image: ${IMAGE_URI}
              env:
              - name: HF_TOKEN
                valueFrom:
                  secretKeyRef:
                    name: hf-secret
                    key: hf_api_token
              - name: RAY_TPU_RESOURCE_PER_CHIP
                value: "2"
              resources:
                limits:
                  cpu: "64"
                  memory: "250Gi"
                  google.com/tpu: "4"
                requests:
                  cpu: "64"
                  memory: "250Gi"
                  google.com/tpu: "4"
              volumeMounts:
              - mountPath: /dev/shm
                name: dshm
            volumes:
            - name: dshm
              emptyDir:
                medium: Memory
                sizeLimit: 128Gi
            nodeSelector:
              cloud.google.com/gke-tpu-accelerator: tpu7x
              cloud.google.com/gke-tpu-topology: 2x2x2

    在 Ironwood (TPU7x) 上,每个物理芯片包含两个芯粒,每个四芯片主机提供八个逻辑 TPU 核心。清单会将 resources 部分中的 TPU 字段值设置为 "8",并将 RAY_TPU_RESOURCE_PER_CHIP 环境变量设置为 "2",以便在每台主机上通告和调度 8 个逻辑 TPU 资源。

  2. 应用清单以创建 RayCluster 自定义资源:

    Trillium (v6e)

    envsubst < ray-cluster.tpu-v6e-multihost.yaml | kubectl apply -f -
    

    Ironwood (TPU7x)

    envsubst < ray-cluster.tpu-v7x-multihost.yaml | kubectl apply -f -
    
  3. 等待 RayCluster 头 Pod 和所有 TPU 工作进程 Pod 达到 Running 状态:

    kubectl get pods -l ray.io/cluster=ray-torchtpu-cluster
    

    输出类似于以下内容:

    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          2m30s
    

    Ironwood (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          2m30s
    
  4. 检查其中一个工作器 Pod 上的环境变量,确认 KubeRay TPU 网络钩子已注入多主机 TPU 协调和 TorchTPU 拓扑变量:

    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"
    

    输出类似于以下内容:

    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-0
    

    Ironwood (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-0
    
  5. 建立到 Ray 头服务的端口转发会话:

    kubectl port-forward svc/ray-torchtpu-cluster-head-svc 8265:8265 >/dev/null 2>&1 &
    

    您可以在 Ray 信息中心的集群标签页中检查 TPU 工作器节点及其自动检测到的 ray.io/tpu-* 标签。

使用 Ray Core 运行分布式张量并行处理

Ray Core 在 ray.util.tpu 模块中提供了 slice_placement_group 函数,用于预留完整的多主机 TPU 切片;还提供了 PlacementGroupSchedulingStrategy 类,用于在切片中的每个逻辑 TPU 设备上调度一个 Ray actor。

  1. 查看示例代码库中的 ray_core_collectives.py 脚本:

    import argparse
    import os
    
    import ray
    from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
    from ray.util.tpu import (
        get_tpu_version_from_type,
        slice_placement_group,
    )
    import torch
    import torch.distributed as dist
    from torch.distributed.device_mesh import init_device_mesh
    from torch.distributed.tensor.parallel import (
        ColwiseParallel,
        RowwiseParallel,
        parallelize_module,
    )
    import torch.nn as nn
    import torch_tpu  # noqa: F401
    
    RANDOM_SEED = 42
    
    
    class FeedForward(nn.Module):
        """Two-layer feed-forward block for tensor-parallel sharding across a TPU slice."""
    
        def __init__(self, hidden_dim: int):
            super().__init__()
            self.w1 = nn.Linear(hidden_dim, 4 * hidden_dim, bias=False)
            self.act = nn.SiLU()
            self.w2 = nn.Linear(4 * hidden_dim, hidden_dim, bias=False)
    
        def forward(self, x: torch.Tensor) -> torch.Tensor:
            return x + self.w2(self.act(self.w1(x)))
    
    
    @ray.remote
    class TorchTPUSliceWorker:
        """Ray actor bound to a single logical TPU device within a slice placement group."""
    
        def __init__(self, devices_per_host: int):
            self.node_ip = ray.util.get_node_ip_address()
            hosts = os.environ["TPU_WORKER_HOSTNAMES"].split(",")
            self.worker_id = int(os.environ.get("TPU_WORKER_ID", "0"))
            self.local_rank = int(
                ray.get_runtime_context().get_accelerator_ids()["TPU"][0]
            )
            self.rank = self.worker_id * devices_per_host + self.local_rank
            # Set per-process PyTorch env:// variables from the webhook's host-level
            # TPU metadata and Ray's assigned local device ID (TorchTrainer sets these
            # automatically in the Ray Train section below).
            os.environ.update(
                {
                    "MASTER_ADDR": hosts[0],
                    "MASTER_PORT": "29500",
                    "WORLD_SIZE": str(len(hosts) * devices_per_host),
                    "RANK": str(self.rank),
                    "LOCAL_RANK": str(self.local_rank),
                    "LOCAL_WORLD_SIZE": str(devices_per_host),
                }
            )
    
        def run_tensor_parallel_step(
            self,
            batch_size: int = 64,
            hidden_dim: int = 128,
        ) -> dict:
            dist.init_process_group(backend="tpu_dist")
            world_size = dist.get_world_size()
            device = torch.device("tpu")
    
            # 1. Compute single-device reference output on CPU with identical seed.
            torch.manual_seed(RANDOM_SEED)
            ref_model = FeedForward(hidden_dim).eval()
            inputs_cpu = torch.randn(batch_size, hidden_dim)
            with torch.no_grad():
                ref_output = ref_model(inputs_cpu)
    
            # 2. Shard the model across all TPU devices in the slice with DTensor TP.
            torch.manual_seed(RANDOM_SEED)
            tp_model = FeedForward(hidden_dim).to(device).eval()
            mesh = init_device_mesh("tpu", (world_size,))
            tp_model = parallelize_module(
                tp_model,
                mesh,
                {"w1": ColwiseParallel(), "w2": RowwiseParallel()},
            )
    
            with torch.no_grad():
                tp_output = tp_model(inputs_cpu.to(device)).cpu()
    
            torch.testing.assert_close(tp_output, ref_output, rtol=1e-3, atol=1e-2)
            max_abs_diff = float((tp_output - ref_output).abs().max().item())
    
            # 3. Verify direct ICI all_reduce across all ranks in the slice.
            rank_tensor = torch.full(
                (4,), float(self.rank + 1), dtype=torch.float32, device=device
            )
            dist.all_reduce(rank_tensor, op=dist.ReduceOp.SUM)
            all_reduce_sum = rank_tensor.cpu().tolist()
    
            dist.destroy_process_group()
            return {
                "rank": self.rank,
                "local_rank": self.local_rank,
                "worker_id": self.worker_id,
                "node_ip": self.node_ip,
                "all_reduce_sum": all_reduce_sum,
                "max_abs_diff": max_abs_diff,
            }
    
    
    def parse_args() -> argparse.Namespace:
        parser = argparse.ArgumentParser(
            description="Run distributed TorchTPU tensor parallelism across a Ray TPU slice."
        )
        parser.add_argument(
            "--topology",
            type=str,
            default="4x4",
            help="TPU physical topology.",
        )
        parser.add_argument(
            "--accelerator-type",
            type=str,
            default="TPU-V6E",
            help="Ray TPU accelerator type (TPU-V6E, TPU-V7X, etc.).",
        )
        return parser.parse_args()
    
    
    def main() -> None:
        args = parse_args()
        ray.init()
    
        slice_handle = slice_placement_group(
            topology=args.topology,
            accelerator_version=get_tpu_version_from_type(args.accelerator_type),
            num_slices=1,
            resources_per_bundle={"TPU": 1},
        )
        ray.get(slice_handle.placement_group.ready())
    
        devices_per_host = slice_handle.num_bundles // slice_handle.num_hosts
        workers = [
            TorchTPUSliceWorker.options(
                resources={"TPU": 1},
                scheduling_strategy=PlacementGroupSchedulingStrategy(
                    placement_group=slice_handle.placement_group,
                    placement_group_bundle_index=bundle_idx,
                ),
            ).remote(devices_per_host)
            for bundle_idx in range(slice_handle.num_bundles)
        ]
        results = sorted(
            ray.get([w.run_tensor_parallel_step.remote() for w in workers]),
            key=lambda r: r["rank"],
        )
    
        expected_sum = sum(range(1, slice_handle.num_bundles + 1))
        for r in results:
            print(
                f"Rank {r['rank']:2d} (worker_id={r['worker_id']}, "
                f"local_rank={r['local_rank']}, ip={r['node_ip']}): "
                f"all_reduce_sum={r['all_reduce_sum']}"
            )
            assert r["all_reduce_sum"] == [float(expected_sum)] * 4
    
        print(
            f"All {len(results)} TorchTPU ranks verified DTensor TP parity "
            f"(max_abs_diff={results[0]['max_abs_diff']:.6f}) and "
            f"all_reduce sum = {expected_sum:.1f}"
        )
        for w in workers:
            ray.kill(w)
        slice_handle.shutdown()
    
    
    if __name__ == "__main__":
        main()

    该脚本分三步在 TPU 切片上运行分布式张量并行处理:

    1. slice_placement_group 函数会预留一个 16 设备 TPU 切片,并在每个 TPU 设备上调度一个 TorchTPUSliceWorker actor。
    2. 每个 actor 都会根据其主机索引和本地设备 ID 计算其全局排名,然后初始化 tpu_dist 进程组。
    3. init_device_mesh 和 parallelize_module 函数会将前馈层分片到所有 16 个 TPU 设备上,根据单设备 CPU 参考值验证输出,并通过 ICI 运行 all_reduce 集合。
  2. 提交适用于您的 TPU 版本的 Ray Core 作业:

    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-V6E
    

    Ironwood (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-V7X
    

    输出类似于以下内容:

    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
    ------------------------------------------
    

使用 Ray Train 和 PyTorch FSDP2 微调 Gemma 4 模型

Ray Train TorchTrainer 类可自动执行 TPU 切片预留、工作器放置以及跨多主机 TPU 切片的 torch.distributed 初始化。

  1. 查看示例代码库中的 ray_train_llm_finetune.py 脚本,该脚本使用 databricks/databricks-dolly-15k 指令数据集对 google/gemma-4-E2B-it 模型进行微调:

    import argparse
    import os
    import tempfile
    import time
    
    from datasets import load_dataset
    import ray
    import ray.train
    from ray.train import (
        Checkpoint,
        CheckpointConfig,
        RunConfig,
        ScalingConfig,
    )
    from ray.train.torch import TorchConfig, TorchTrainer
    import torch
    from torch.distributed import ReduceOp, all_reduce
    from torch.distributed.checkpoint.state_dict import (
        StateDictOptions,
        get_model_state_dict,
    )
    from torch.distributed.device_mesh import init_device_mesh
    from torch.distributed.fsdp import MixedPrecisionPolicy, fully_shard
    from torch.utils.data import DataLoader
    import torch_tpu  # noqa: F401
    from transformers import (
        AutoModelForCausalLM,
        AutoTokenizer,
        DataCollatorForLanguageModeling,
    )
    
    
    def create_dataloader(
        tokenizer: AutoTokenizer,
        batch_size: int,
        dataset_split: str = "train[:1024]",
        max_seq_len: int = 512,
    ) -> DataLoader:
        """Loads and tokenizes the Dolly 15k dataset for causal LM fine-tuning."""
        dataset = load_dataset(
            "databricks/databricks-dolly-15k", split=dataset_split
        )
        tokenized = dataset.map(
            lambda x: tokenizer(
                f"Instruction: {x['instruction']}\nResponse: {x['response']}",
                truncation=True,
                max_length=max_seq_len,
                padding="max_length",
            ),
            remove_columns=dataset.column_names,
        )
        dataloader = DataLoader(
            tokenized,
            batch_size=batch_size,
            shuffle=True,
            drop_last=True,
            collate_fn=DataCollatorForLanguageModeling(
                tokenizer=tokenizer, mlm=False
            ),
        )
        return ray.train.torch.prepare_data_loader(dataloader)
    
    
    def train_func(config: dict) -> None:
        """Per-worker distributed FSDP2 training loop executed on each logical TPU core."""
        ctx = ray.train.get_context()
        world_rank = ctx.get_world_rank()
        world_size = ctx.get_world_size()
        device = ray.train.torch.get_device()
    
        model_id = config["model_id"]
        dataset_split = config["dataset_split"]
        batch_size = config["batch_size_per_device"]
        num_epochs = config["num_epochs"]
        max_seq_len = 512
    
        tokenizer = AutoTokenizer.from_pretrained(model_id)
        dataloader = create_dataloader(
            tokenizer=tokenizer,
            batch_size=batch_size,
            dataset_split=dataset_split,
            max_seq_len=max_seq_len,
        )
    
        torch.manual_seed(42)
        model = AutoModelForCausalLM.from_pretrained(
            model_id,
            dtype=torch.bfloat16,
            attn_implementation="eager",
            vision_config=None,
            audio_config=None,
        ).to(device)
        model.config.get_text_config().use_cache = False
    
        mesh = init_device_mesh("tpu", (world_size,))
        mp_policy = MixedPrecisionPolicy(
            param_dtype=torch.bfloat16,
            reduce_dtype=torch.float32,
        )
        text_model = model.model.language_model
        text_model.embed_tokens_per_layer.requires_grad_(False)
        for layer in text_model.layers:
            fully_shard(layer.mlp, mesh=mesh, mp_policy=mp_policy)
        fully_shard(
            model,
            mesh=mesh,
            mp_policy=mp_policy,
            ignored_params=set(text_model.embed_tokens_per_layer.parameters()),
        )
    
        optimizer = torch.optim.AdamW(
            model.parameters(), lr=2e-5, weight_decay=0.01
        )
        global_batch_size = batch_size * world_size
    
        for epoch in range(num_epochs):
            dataloader.sampler.set_epoch(epoch)
            model.train()
            epoch_start = time.perf_counter()
            running_loss = torch.zeros(1, device=device, dtype=torch.float32)
    
            for batch in dataloader:
                optimizer.zero_grad()
                outputs = model(
                    input_ids=batch["input_ids"],
                    labels=batch["labels"],
                )
                outputs.loss.backward()
                optimizer.step()
                running_loss += outputs.loss.detach().float()
    
            all_reduce(running_loss, op=ReduceOp.AVG)
            avg_loss = running_loss.item() / len(dataloader)
            epoch_time = time.perf_counter() - epoch_start
            tokens_per_sec = (
                len(dataloader) * global_batch_size * max_seq_len
            ) / epoch_time
    
            metrics = {
                "epoch": epoch + 1,
                "loss": round(avg_loss, 4),
                "epoch_time_s": round(epoch_time, 2),
                "tokens_per_s": round(tokens_per_sec, 1),
            }
            if world_rank == 0:
                print(
                    f"Epoch {epoch + 1}/{num_epochs} | loss={metrics['loss']:.4f} | "
                    f"epoch_time={metrics['epoch_time_s']:.2f}s | "
                    f"tokens/s={metrics['tokens_per_s']:.1f}"
                )
    
            full_state_dict = get_model_state_dict(
                model,
                options=StateDictOptions(
                    full_state_dict=True,
                    cpu_offload=True,
                    ignore_frozen_params=True,
                ),
            )
            if world_rank == 0:
                with tempfile.TemporaryDirectory() as tmpdir:
                    torch.save(full_state_dict, os.path.join(tmpdir, "model.pt"))
                    ray.train.report(
                        metrics, checkpoint=Checkpoint.from_directory(tmpdir)
                    )
            else:
                ray.train.report(metrics)
    
    
    def parse_args() -> argparse.Namespace:
        parser = argparse.ArgumentParser(
            description="Fine-tune a Hugging Face CausalLM on TPUs with Ray Train and TorchTPU FSDP2."
        )
        parser.add_argument(
            "--model-id",
            type=str,
            default="google/gemma-4-E2B-it",
            help="Hugging Face model ID to fine-tune.",
        )
        parser.add_argument(
            "--topology",
            type=str,
            default="4x4",
            help="TPU slice topology.",
        )
        parser.add_argument(
            "--accelerator-type",
            type=str,
            default="TPU-V6E",
            help="Ray TPU accelerator type (TPU-V6E, TPU-V7X, etc.).",
        )
        parser.add_argument(
            "--num-workers",
            type=int,
            default=16,
            help="Total TPU worker processes across the slice.",
        )
        parser.add_argument(
            "--dataset-split",
            type=str,
            default="train[:1024]",
            help="Hugging Face dataset split to load.",
        )
        parser.add_argument(
            "--num-epochs",
            type=int,
            default=3,
            help="Number of fine-tuning epochs.",
        )
        parser.add_argument(
            "--batch-size-per-device",
            type=int,
            default=4,
            help="Per-device training batch size.",
        )
        parser.add_argument(
            "--storage-path",
            type=str,
            default=os.environ.get("STORAGE_PATH"),
            required="STORAGE_PATH" not in os.environ,
            help="Cloud Storage or shared file system path for checkpoints.",
        )
        return parser.parse_args()
    
    
    def main() -> None:
        args = parse_args()
        ray.init()
        run_name = f"torchtpu_gemma4_fsdp2_sft_{int(time.time())}"
    
        print(
            f"Starting Ray Train FSDP2 SFT job: model={args.model_id}, "
            f"topology={args.topology}, accelerator_type={args.accelerator_type}, "
            f"num_workers={args.num_workers}"
        )
    
        trainer = TorchTrainer(
            train_loop_per_worker=train_func,
            train_loop_config={
                "model_id": args.model_id,
                "dataset_split": args.dataset_split,
                "num_epochs": args.num_epochs,
                "batch_size_per_device": args.batch_size_per_device,
            },
            torch_config=TorchConfig(backend="tpu_dist"),
            scaling_config=ScalingConfig(
                use_tpu=True,
                num_workers=args.num_workers,
                topology=args.topology,
                accelerator_type=args.accelerator_type,
                resources_per_worker={"TPU": 1},
            ),
            run_config=RunConfig(
                name=run_name,
                storage_path=args.storage_path,
                checkpoint_config=CheckpointConfig(
                    num_to_keep=1,
                    checkpoint_score_attribute="loss",
                    checkpoint_score_order="min",
                ),
            ),
        )
    
        result = trainer.fit()
        print(f"Training completed! Final metrics: {result.metrics}")
        print(f"Saved checkpoint: {result.checkpoint}")
    
    
    if __name__ == "__main__":
        main()

    该脚本使用以下组件配置 TorchTrainer 类:

    • ScalingConfig 类用于为训练作业配置分布式工作器拓扑。设置 use_tpu=True、num_workers=16、topology 和 accelerator_type 参数会预留一个 16 设备 TPU 切片,并为每个 TPU 核心安排一个工作器。
    • TorchConfig 类在工作器之间配置 torch.distributed 进程组。设置 backend="tpu_dist" 参数会在 train_func 函数运行之前通过 TPU ICI 网络连接所有 16 个工作器。
    • create_dataloader 函数会下载指令数据集并对其进行分词,使用 PyTorch DataLoader 对象对词元序列进行批处理,并调用 ray.train.torch.prepare_data_loader 函数,以便每个工作器在其分配的 TPU 设备上接收每个全局批次的不相交分片。
    • train_func 函数会以 bfloat16 精度加载模型,并在每个前馈块和根模型上调用 PyTorch FSDP2 fully_shard 函数,以在所有 16 个 TPU 核心之间对参数、梯度和优化器状态进行分片。
    • Checkpoint 类和 ray.train.report 函数会在每个周期结束时收集 rank 0 上的完整模型状态字典,并将检查点上传到 Cloud Storage 存储桶。
  2. 提交适用于您的 TPU 版本的 TorchTrainer FSDP2 微调作业:

    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_torchtpu
    

    Ironwood (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_torchtpu
    

    输出类似于以下内容:

    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
    ------------------------------------------
    

    训练输出如下所示:

    • 第 1 个周期包括初始 XLA 图编译。周期 2 和 3 重用 /dev/shm/torch_tpu_cache 中缓存的可执行文件,在 TPU Trillium (v6e) 上达到每秒超过 20,000 个 token,在 Ironwood (TPU7x) 上达到每秒 31,000 个 token。
    • 在 databricks/databricks-dolly-15k 指令数据集上进行三个微调周期后,在所有 16 个 FSDP2 工作器等级之间同步的全局交叉熵损失从第 1 个周期的 3.14 降至第 3 个周期的 2.20。
    • 在每个周期结束时,rank 0 会收集完整的模型状态字典,并将检查点上传到您的 Cloud Storage 存储桶,同时保留 CheckpointConfig(num_to_keep=1, checkpoint_score_attribute="loss", checkpoint_score_order="min") 对象中配置的损失最低的检查点。

观察工作负载性能

如需在 Cloud Monitoring 中监控 TPU TensorCore 利用率、HBM 用量和 Ray 集群指标,请参阅收集和查看 GKE 上的 Ray 集群的日志和指标以及在 Ray 信息中心内查看 TPU 指标。

访问 Ray 信息中心

如需在内置 Ray 信息中心内检查 Ray actor、放置组和训练作业的状态,请按照以下步骤操作:

  1. 如果到 Ray 头服务的端口转发尚未运行,请在后台启动:

    kubectl port-forward svc/ray-torchtpu-cluster-head-svc 8265:8265 >/dev/null 2>&1 &
    
  2. 在 Cloud Shell 中,点击 “网页预览”图标。 网页预览,然后选择在端口 8265 上预览,或在本地浏览器中打开 http://localhost:8265。

  3. 点击作业、集群和指标标签页,检查工作器日志、展示位置组状态、TPU 节点标签和按节点划分的 TPU 利用率图表。

清理

为避免因本教程中使用的资源导致您的 Google Cloud 账号产生费用,请删除 RayCluster 自定义资源、GKE 集群、Cloud Storage 存储桶和 Artifact Registry 代码库:

  1. 停止后台端口转发进程,并删除 RayCluster 自定义资源和 Kubernetes Secret:

    pkill -f "kubectl port-forward" || true
    kubectl delete raycluster ray-torchtpu-cluster
    kubectl delete secret hf-secret --ignore-not-found
    
  2. 删除 GKE 集群:

    Autopilot

    gcloud container clusters delete ${CLUSTER_NAME} \
        --region=${REGION}
    

    标准

    gcloud container clusters delete ${CLUSTER_NAME} \
        --zone=${ZONE}
    
  3. 删除 Cloud Storage 存储桶及其检查点内容:

    gcloud storage rm --recursive gs://${BUCKET_NAME}
    
  4. 删除 Artifact Registry 代码库:

    gcloud artifacts repositories delete ${ARTIFACT_REGISTRY} \
        --location=${REGION}
    

后续步骤