使用 JAX 在 GKE 上通过 TPU 微调 LLM

本教程介绍如何使用 Google Kubernetes Engine (GKE) 上的张量处理单元 (TPU) 和 JAX 来对大语言模型 (LLM) 进行微调。借助微调,您可以调整基础模型(例如 Gemma 3),使其适合特定领域或任务。此过程通过使用您自己的专业数据集更新模型的参数,从而提高模型的精确度和准确度。

如果您在微调 AI/机器学习工作负载时需要利用托管式 Kubernetes 的精细控制、自定义、可伸缩性、弹性、可移植性和成本效益,那么本指南是一个很好的起点。

背景

通过在 GKE 上使用 TPU 和 Jax 来对 LLM 进行微调,您可以构建一个可用于生产用途的强大微调解决方案,具备托管式 Kubernetes 的所有优势。

Gemma

Gemma 是一组公开提供的轻量级生成式 AI/机器学习多模态模型(根据开放许可发布)。这些 AI 模型可以在应用、硬件、移动设备或托管服务中运行。Gemma 3 引入了多模态功能,支持视觉语言输入和文本输出。它可处理最多 128,000 个 token 的上下文窗口,并支持 140 多种语言。Gemma 3 还提供改进的数学、推理和聊天功能,包括结构化输出和函数调用。

您可以使用 Gemma 模型生成文本,也可以针对专门任务对这些模型进行调优。

如需了解详情,请参阅 Gemma 文档

TPU

TPU 是 Google 定制开发的应用专用集成电路 (ASIC),用于加速使用 TensorFlowPyTorchJAX 等框架构建的机器学习和 AI 模型。

使用 GKE 中的 TPU 之前,我们建议您完成以下学习路线:

  1. 了解 Cloud TPU 系统架构中的当前 TPU 版本可用性。
  2. 了解 GKE 中的 TPU

JAX

JAX 是一种高性能机器学习框架,旨在与 TPU 和 GPU 搭配使用。JAX 提供了一个用于构建和训练机器学习模型的 API。

如需了解详情,请参阅 JAX 代码库

目标

本教程介绍以下步骤:

  1. 根据模型特征创建一个具有推荐 TPU 拓扑的 GKE Autopilot 或 Standard 集群。 在本教程中,您将在单主机节点池上执行微调。
  2. 将数据添加到 Cloud Storage 存储桶,并通过 Cloud Storage FUSE 将其装载到容器。
  3. 在 GKE 上部署 LLM 微调作业。
  4. 监控微调作业并查看日志。

准备工作

  • In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  • Verify that billing is enabled for your Google Cloud project.

  • Enable the required API.

    Roles required to enable APIs

    To enable APIs, you need the serviceusage.services.enable permission. If you created the project, then you likely already have this permission through the Owner role (roles/owner). Otherwise, you can get this permission through the Service Usage Admin role (roles/serviceusage.serviceUsageAdmin). Learn how to grant roles.

    Enable the API

  • 确保您在项目中拥有以下一个或多个角色: roles/container.admin、roles/iam.serviceAccountAdmin、roles/storage.admin

    检查角色

    1. 在 Google Cloud 控制台中,前往 IAM 页面。

      转到 IAM
    2. 选择项目。
    3. 主账号列中,找到标识您或您所属群组的所有行。如需了解您属于哪些群组,请与您的管理员联系。

    4. 对于指定或包含您的所有行,请检查角色列以查看角色列表是否包含所需的角色。

    授予角色

    1. 在 Google Cloud 控制台中,前往 IAM 页面。

      转到 IAM
    2. 选择项目。
    3. 点击 授予访问权限
    4. 新的主账号字段中,输入您的用户标识符。 这通常是员工身份池中的用户的标识符。如需了解详情,请参阅在 IAM 政策中表示员工池用户,或与您的管理员联系。

    5. 点击选择角色,然后搜索相应角色。
    6. 如需授予其他角色,请点击 添加其他角色,然后添加其他各个角色。
    7. 点击 Save(保存)。
  • 确保您有足够的配额用于 16 个 TPU Trillium (v6e) 芯片。在本教程中,您将使用需要 16 个芯片和按需实例的节点池配置。
  • 确保您拥有 Docker 代码库。如果您没有,请在 Artifact Registry 中创建一个标准代码库

准备环境

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

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

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

  2. 设置默认环境变量:

    gcloud config set project PROJECT_ID
    gcloud config set billing/quota_project PROJECT_ID
    export PROJECT_ID=$(gcloud config get project)
    export CLUSTER_NAME=CLUSTER_NAME
    export REGION=CONTROL_PLANE_LOCATION
    export ZONE=ZONE
    export GCS_BUCKET_NAME=BUCKET_NAME
    

    替换以下值:

    • PROJECT_ID:您的 Google Cloud 项目 ID
    • CLUSTER_NAME:GKE 集群的名称。
    • CONTROL_PLANE_LOCATION:GKE 集群和 TPU 节点所在的 Compute Engine 区域。相应区域必须包含提供 TPU Trillium (v6e) 机器类型的可用区。
    • ZONE:所选 CONTROL_PLANE_LOCATION 区域内可使用 TPU Trillium (v6e) 机器类型的可用区。如需列出提供 TPU Trillium (v6e) TPU 的地区,请运行以下命令:

        gcloud compute accelerator-types list --filter="name~ct6e" --format="value(zone)"
      
    • BUCKET_NAME:包含训练数据的 Cloud Storage 存储桶的名称。

  3. 克隆示例代码库:

    git clone https://github.com/GoogleCloudPlatform/kubernetes-engine-samples.git
    cd kubernetes-engine-samples
    
  4. 导航到工作目录:

    cd ai-ml/llm-training-jax-tpu-gemma3
    

创建和配置 Google Cloud 资源

在本部分中,您将创建和配置 Google Cloud 资源。

创建 GKE 集群

您可以在 GKE Autopilot 或 Standard 集群中的 TPU 上对 LLM 进行微调。我们建议您使用 Autopilot 集群获得全托管式 Kubernetes 体验。如需选择最适合您的工作负载的 GKE 操作模式,请参阅选择 GKE 操作模式

Autopilot

创建一个使用 Workload Identity Federation for GKE并已启用 Cloud Storage FUSE 的 GKE Autopilot 集群。

gcloud container clusters create-auto ${CLUSTER_NAME} \
    --location=${REGION}

集群创建可能需要几分钟的时间。

标准

  1. 创建使用Workload Identity Federation for GKE并已启用 Cloud Storage FUSE 的区域级 GKE Standard 集群。

    gcloud container clusters create ${CLUSTER_NAME} \
        --enable-ip-alias \
        --addons GcsFuseCsiDriver \
        --machine-type=n2-standard-4 \
        --num-nodes=2 \
        --workload-pool=${PROJECT_ID}. \
        --location=${REGION}
    

    集群创建可能需要几分钟的时间。

  2. 创建单主机节点池:

    gcloud container node-pools create jax-tpu-nodepool \
        --cluster=${CLUSTER_NAME} \
        --machine-type=ct6e-standard-1t \
        --num-nodes=1 \
        --location=${REGION} \
        --node-locations=${ZONE} \
        --workload-metadata=GKE_METADATA
    

GKE 会创建一个具有 1x1 拓扑和一个节点的 TPU Trillium 节点池。--workload-metadata=GKE_METADATA 标志将节点池配置为使用 GKE 元数据服务器。

安装 JobSet

  1. 配置 kubectl 以与您的集群通信:

    gcloud container clusters get-credentials ${CLUSTER_NAME} --location=${REGION}
    
  2. 安装最新发布的 JobSet 版本:

    kubectl apply --server-side -f https://github.com/kubernetes-sigs/jobset/releases/download/JOBSET_VERSION/manifests.yaml
    

    JOBSET_VERSION 替换为 JobSet 的最新发布版本。例如 v0.11.0

  3. 验证 JobSet 安装:

    kubectl get pods -n jobset-system
    

    输出类似于以下内容:

    NAME                                         READY   STATUS    RESTARTS   AGE
    jobset-controller-manager-6c56668494-l4dhc   1/1     Running   0          4m45s
    

    如果 JobSet 正在等待资源,您可能需要添加更多节点。

配置 Cloud Storage FUSE

如需对 LLM 进行微调,您需要提供训练数据。在本教程中,您将使用 Hugging Face 中的 TinyStories 数据集。此数据集包含由 GPT-3.5 和 GPT-4 合成生成的短篇故事,这些故事使用有限的词汇。

本部分介绍如何配置 Cloud Storage FUSE 以从 Cloud Storage 存储桶中读取数据。

  1. 下载数据集:

    wget https://huggingface.co/datasets/roneneldan/TinyStories/resolve/main/TinyStories-train.txt?download=true -O TinyStories-train.txt
    
  2. 将数据上传到新的 Cloud Storage 存储桶:

    gcloud storage buckets create gs://${GCS_BUCKET_NAME} \
        --location=${REGION} \
        --enable-hierarchical-namespace \
        --uniform-bucket-level-access
    gcloud storage cp TinyStories-train.txt gs://${GCS_BUCKET_NAME}
    
  3. 如需允许工作负载通过 Cloud Storage FUSE 读取数据,请创建 Kubernetes 服务账号 (KSA) 并添加所需权限。运行 permissionsetup.sh 脚本:

    # Copyright 2026 Google LLC
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    
    #!/bin/bash
    
    # --- Configuration Variables ---
    # Kubernetes Service Account details
    export KSA_NAME="jaxserviceaccout"
    export NAMESPACE="default"
    
    # Google Cloud IAM Service Account details
    export GSA_NAME="<GSA_NAME>"
    # Automatically get the current project ID
    export PROJECT_ID=$(gcloud config get-value project)
    export  GSA_DESCRIPTION="GKE Service Account to read GCS bucket for ${KSA_NAME}"
    
    # GCS Bucket details
    export GCS_BUCKET_NAME="<GCS_BUCKET_NAME>" # <--- IMPORTANT: Update this to your bucket name
    
    # Derived Variables
    export GSA_EMAIL="${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
    export WI_MEMBER="serviceAccount:${PROJECT_ID}.svc.id.goog[${NAMESPACE}/${KSA_NAME}]"
    
    # --- Check if PROJECT_ID is set ---
    if [ -z "${PROJECT_ID}" ]; then
      echo "Error: PROJECT_ID is not set. Please set it using 'gcloud config set project YOUR_PROJECT_ID'"
      exit 1
    fi
    
    echo "--- Configuration ---"
    echo "KSA_NAME:      ${KSA_NAME}"
    echo "NAMESPACE:     ${NAMESPACE}"
    echo "GSA_NAME:      ${GSA_NAME}"
    echo "PROJECT_ID:    ${PROJECT_ID}"
    echo "GSA_EMAIL:     ${GSA_EMAIL}"
    echo "GCS_BUCKET_NAME:   ${GCS_BUCKET_NAME}"
    echo "WI_MEMBER:     ${WI_MEMBER}"
    echo "--------------------"
    read -p "Press enter to continue..."
    
    # --- Command Execution ---
    
    echo "[1/5] Creating Google Cloud IAM Service Account (GSA): ${GSA_NAME}"
    gcloud iam service-accounts create "${GSA_NAME}" \
        --project="${PROJECT_ID}" \
        --description="${GSA_DESCRIPTION}" \
        --display-name="${GSA_NAME}"
    
    echo "[2/5] Granting GSA '${GSA_EMAIL}' read access (roles/storage.objectViewer) to bucket 'gs://${GCS_BUCKET_NAME}'"
    gcloud storage buckets add-iam-policy-binding "gs://${GCS_BUCKET_NAME}" \
        --member="serviceAccount:${GSA_EMAIL}" \
        --role="roles/storage.objectViewer" \
        --project="${PROJECT_ID}"
    
    echo "[3/5] Creating Kubernetes Service Account (KSA): ${KSA_NAME} in namespace ${NAMESPACE}"
    kubectl create serviceaccount "${KSA_NAME}" --namespace "${NAMESPACE}"
    
    echo "[4/5] Allowing KSA to impersonate GSA (Workload Identity Binding): ${GSA_EMAIL}"
    gcloud iam service-accounts add-iam-policy-binding "${GSA_EMAIL}" \
        --role roles/iam.workloadIdentityUser \
        --member "${WI_MEMBER}" \
        --project="${PROJECT_ID}"
    
    echo "[5/5] Annotating KSA '${KSA_NAME}' to link with GSA '${GSA_EMAIL}'"
    kubectl annotate serviceaccount "${KSA_NAME}" \
        --namespace "${NAMESPACE}" \
        iam.gke.io/gcp-service-account="${GSA_EMAIL}"
    
    echo "--- Setup Complete ---"
    echo "Pods in namespace '${NAMESPACE}' using serviceAccount '${KSA_NAME}' can now authenticate as '${GSA_EMAIL}' and have read access to 'gs://${GCS_BUCKET_NAME}'."
    

    运行此脚本后,您的Google Cloud 项目和 GKE 集群中会配置以下资源:

    • 系统会在您的项目中创建一个名为 gcs-fuse-sa 的新 IAM 服务账号。
    • 创建的 Google Cloud 服务账号 (GSA) (gcs-fuse-sa) 会被授予 ${GCS_BUCKET_NAME} 指定的 Cloud Storage 存储桶的 roles/storage.objectViewer 角色。此权限允许 GSA 从存储桶中读取对象。
    • 系统会在 GKE 集群的 default 命名空间中创建一个名为 jaxserviceaccount 的新 KSA。
    • 更新 GSA 的 IAM 政策,以向 KSA 授予 roles/iam.workloadIdentityUser 角色。此权限允许 KSA 模拟 GSA。
    • KSA 已添加注释,可将其与 GSA 相关联。此注解会告知 GKE,KSA 应使用 Workload Identity 模拟哪个 GSA。

      现在,在 GKE 集群的 default 命名空间中运行的任何使用 jaxserviceaccount 服务账号的 Pod 都将能够以 gcs-fuse-sa GSA 的身份进行身份验证。这些 Pod 将拥有对存储在 gs://${GCS_BUCKET_NAME} 存储桶中的对象的读取权限,这对于微调作业使用 Cloud Storage FUSE 访问数据集至关重要。

创建微调脚本

在本部分中,您将探索对 Gemma 3 模型执行微调操作的训练脚本。此脚本使用 Gemma3Tokenizer

查看以下 Gemma3LLMTrain.py 微调脚本:

# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import grain.python as pygrain
import jax
import jax.numpy as jnp
import optax
import pandas as pd
import time
import argparse

from dataclasses import dataclass
from functools import partial
from gemma import gm
from flax.training import train_state
from jax.sharding import Mesh, PartitionSpec, NamedSharding

jax.distributed.initialize()
print("Global device count:", jax.device_count())
print("jax version:", jax.__version__)

tokenizer = gm.text.Gemma3Tokenizer()
num_epochs = 1
learning_rate = 2e-5

@dataclass
class TextDataset:
    data: list
    maxlen: int
    tokenizer: gm.text.Gemma3Tokenizer

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx: int):
        encoding = self.tokenizer.encode(self.data[idx])[:self.maxlen]  # Tokenize and truncate
        return encoding + [0] * (self.maxlen - len(encoding))  # Pad to maxlen

def load_and_preprocess_data(file_path, batch_size, maxlen, datacount, tokenizer):

    with open(file_path,