1. はじめに
このラボでは、GKE Standard と GKE Agent Sandbox(gVisor)で、trl ライブラリの Group Relative Policy Optimization(GRPO)アルゴリズムを使用して、高性能の分散型強化学習(RL)トレーニング ループを構築、プロビジョニング、実行する方法について詳しく説明します。
このチュートリアルの目的は、RL トレーニング ループ中に信頼できない LLM 生成コードを安全に評価する方法を示すことです。これは、オーケストレーション プレーン(Ray)と実行プレーン(GKE Agent Sandbox)を分離することで実現します。
RL コード評価の技術的課題
強化学習を使用して LLM エージェントをトレーニングする場合(単体テストで出力を評価してコードを記述するモデルをトレーニングする場合など)、トレーニング ループで信頼できない LLM 生成の Python スクリプトを数千個並行して実行する必要があります。これには重大な課題があります。
- Pod Churn Bottleneck: 従来の評価フレームワークでは、タスクごとに新しい Docker コンテナが起動されます。RL トレーニング ループ中に数百の並列ロールアウトに対してこれを動的に行うと、Kubernetes コントロール プレーンに大きな負荷がかかります。レイテンシにより、高頻度の RL トレーニングは不可能になります。
- セキュリティ リスク: 標準のコンテナ ランタイム内で LLM 生成の任意のコードを実行すると、ホスト OS カーネルが共有されます。1 つのエスケープの脆弱性により、ノードが侵害される可能性があります。
- IAM トークンの盗難: Kubernetes Pod 内で実行されている LLM 生成コードが、クラウド プロバイダのメタデータ サーバーにクエリを実行して、ノードの IAM サービス アカウント トークンを盗む可能性があります。
解決策: オーケストレーションと実行の分離
このアーキテクチャでは、オーケストレーションと実行が分離されています。
- オーケストレーター(Ray): 分散 Ray クラスタが RL トレーニング ループを管理し、ロールアウト生成を分散します。
- 実行プレーン(GKE Agent Sandbox): Ray ワーカーは、Kubernetes Pod を動的に作成するのではなく、専用の Sandbox Router に単純な HTTP 呼び出しを行います。ルーターは、gVisor(GKE Sandbox)で実行されている分離された事前ウォーミング済みのコンテナをワーカーに即座に割り当てます。
- 1 秒未満のレイテンシ: サンドボックスはマネージド
SandboxWarmPoolで事前ウォームアップされ、高速 HTTP ゲートウェイ経由で管理されるため、環境の作成は 200 ミリ秒未満に短縮され、Kubernetes コントロール プレーンを完全にバイパスします。
ラボの目標
この Codelab では、以下について学びます。
- RL ループで信頼できないコードを評価するためのアーキテクチャ上の課題とソリューション。
- 効率的なロールアウトのためのカスタム サンドボックス イメージをビルドする方法。
- GKE Agent Sandbox と SandboxWarmPool を構成して使用する方法。
- IAM トークンの盗難を防ぐためにサンドボックスを安全に分離する方法。
- Ray を使用してオーケストレーションと実行を分離し、SweBench と TRL で基本的な RL トレーニング ジョブを実行する方法。
2. クラスタの作成と前提条件
続行する前に、高パフォーマンスの GPU ノードプールと、トレーニング ワークロードを管理するためにインストールされた Ray Operator を含む GKE クラスタが必要です。
前提条件
この Codelab では、次のツールがインストールされ、構成されていることを前提としています。
- Google Cloud SDK(
gcloud) - Docker(カスタム イメージをローカルでビルドする場合に必要)
kubectl
環境変数
まず、この Codelab 全体で使用する環境変数を設定します。次のコマンドでは、適切なデフォルト値を使用していますが、必要に応じて特定の Google Cloud 環境に合わせて変更できます。
export PROJECT_ID=$(gcloud config get-value project)
export REGION="us-west3"
export ZONE="us-west3-a"
export REPO_NAME="rl-sandbox-repo"
カスタム コンテナ イメージを保持する Artifact Registry リポジトリを作成します。
gcloud artifacts repositories create $REPO_NAME \
--repository-format=docker \
--location=$REGION \
--description="Repository for RL Sandbox images"
クラスタ構成
AI ワークロード向けに最適化された GKE クラスタ(GPUDirect RDMA ネットワーク配線を含む)のプロビジョニングに関する完全なチュートリアルについては、公式ドキュメントの GKE AI Hypercompute カスタム クラスタを作成するをご覧ください。
重要な前提条件: クラスタまたは特定の実行ノードプールを作成するときは、--enable-agent-sandbox フラグと --sandbox type=gvisor フラグを渡して、Sandbox ウォームプールの必要なカスタム リソース定義(CRD)をインストールしてください。
クラスタ、GPU、Ray Operator が実行されていることを前提として、以下では実行プレーンを構成して RL ループを実行する方法について説明します。
3. カスタム イメージをビルドする
高パフォーマンスの RL を実行するうえで重要なのは、依存関係をイメージに組み込むことです。モデルを実行する GPU ワーカー用のイメージと、信頼できない評価コードを実行する分離されたサンドボックス用のイメージの 2 つの異なるイメージが必要です。
1. GPU ワーカー イメージをビルドする
Ray GPU ワーカーは、言語モデルを実行してトレーニング ループをオーケストレートするためにライブラリを必要とします。このイメージは公式の vLLM イメージの上に構築されているため、最新の GPU をサポートし、PyTorch/CUDA がプリインストールされています。
次のコマンドを実行して Dockerfile.gpu_worker を作成します。
cat << 'EOF' > Dockerfile.gpu_worker
# ==============================================================================
# Base Image: Use the official vLLM production image.
# This image comes pre-baked with PyTorch 2.11, CUDA 13.0, and vLLM.
# It supports sm_100 Blackwell GPUs natively!
# ==============================================================================
FROM vllm/vllm-openai:latest
USER root
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
numactl \
libnuma-dev \
wget \
ca-certificates \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install Ray, TRL, and Sandbox tools
# TRL does not require compiling flash_attn from source.
RUN pip install --no-cache-dir \
"ray[default]==2.55.1" \
"numpy<2.0" \
gymnasium>=0.28.1 \
k8s-agent-sandbox>=0.4.6 \
trl transformers packaging ninja cachetools accelerate datasets peft
EOF
イメージをビルドして Artifact Registry リポジトリに push します。
export WORKER_REPO="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/ray-gpu-worker:v1"
docker build -f Dockerfile.gpu_worker -t $WORKER_REPO .
docker push $WORKER_REPO
注: このガイドでは、ローカルの docker コマンドを使用してイメージをビルドします。イメージをリモートでビルドする場合は、代わりに Cloud Build(gcloud builds submit など)を使用できます。
2. CPU ヘッダー画像をビルドする
Ray ヘッドノードはクラスタをオーケストレートするだけで、重い GPU トレーニング モデルは実行しません。標準の CPU ノードで大規模なイメージ プル ボトルネック(通常は 15 GB 以上)が発生しないように、ヘッドノード用に軽量の CPU 専用イメージをビルドします。このイメージには Ray と必要な Python ライブラリが含まれていますが、CUDA や vLLM などの重い GPU ライブラリは含まれていません。
次のコマンドを実行して Dockerfile.head を作成します。
cat << 'EOF' > Dockerfile.head
# ==============================================================================
# Base Image: Use the official Python slim image for the exact patch version.
# This aligns the Python version (3.12.13) with the GPU worker node.
# ==============================================================================
FROM python:3.12.13-slim
USER root
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
wget \
ca-certificates \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install Ray, TRL, and Sandbox tools (CPU versions where applicable)
# We install torch CPU first to avoid pulling the 2GB+ CUDA torch package.
RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu && \
pip install --no-cache-dir \
"ray[default]==2.55.1" \
"numpy<2.0" \
gymnasium>=0.28.1 \
k8s-agent-sandbox>=0.4.6 \
trl transformers packaging ninja cachetools accelerate datasets peft
# Create a 'ray' user to run the container securely and match Ray conventions
RUN useradd -ms /bin/bash ray
USER ray
WORKDIR /home/ray
EOF
イメージをビルドして push します。
export HEAD_REPO="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/ray-head:v1"
docker build -f Dockerfile.head -t $HEAD_REPO .
docker push $HEAD_REPO
3. サンドボックス イメージをビルドする
サンドボックスには、評価するタスクの特定の依存関係が必要です。これにより、ランタイム インストールが瞬時に行われます。この Codelab では、SWE-bench の django/django リポジトリの問題を使用します。リポジトリを事前にクローンし、Python 環境を事前にビルドして、モデル スクリプトが RL ループでダウンロードに時間を費やさないようにします。
次のコマンドを実行して Dockerfile.sandbox を作成します。
cat << 'EOF' > Dockerfile.sandbox
# Use a stable Debian-based Miniconda image
FROM condaforge/miniforge3:latest
# 1. Install essential system libraries (including sqlite3 for Django tests)
RUN apt-get update && apt-get install -y \
git \
build-essential \
libsqlite3-dev \
&& rm -rf /var/lib/apt/lists/*
# 2. Set up the /workspace directory and grant ownership to the pre-existing non-root 'ubuntu' user (UID 1000)
RUN mkdir -p /workspace \
&& chown -R 1000:1000 /workspace
# 3. Switch to the non-root user
USER ubuntu
WORKDIR /workspace
# 4. Pre-configure Git globally so the agent can run git commands
RUN git config --global user.email "agent@gke-sandbox.local" \
&& git config --global user.name "Agent"
# 5. Pre-clone the repository as the non-root user
RUN git clone https://github.com/django/django.git .
# 6. Pre-build Conda environments and pre-cache common dependencies
# We do NOT run "pip install -e ." here to avoid Python version conflicts with the main branch.
# Instead, we pre-install the heavy dependencies so that runtime installation is instantaneous.
RUN conda create -y -n django-py39 python=3.9 \
&& conda run -n django-py39 pip install --no-cache-dir asgiref sqlparse tzdata pytest pytest-django
RUN conda create -y -n django-py310 python=3.10 \
&& conda run -n django-py310 pip install --no-cache-dir asgiref sqlparse tzdata pytest pytest-django
# --- Add Agent Server ---
# We use a multi-stage build to copy the agent server from the official python-runtime-sandbox image
COPY --from=registry.k8s.io/agent-sandbox/python-runtime-sandbox:v0.1.0 /app /opt/sandbox-agent
USER root
RUN chown -R 1000:1000 /opt/sandbox-agent \
&& /opt/conda/bin/pip install --no-cache-dir -r /opt/sandbox-agent/requirements.txt \
&& sed -i 's|"/app"|"/workspace"|g' /opt/sandbox-agent/main.py
USER ubuntu
# ------------------------
# Prepend the django-py39 conda environment bin to PATH for commands executed inside the container
ENV PATH=/home/ubuntu/.conda/envs/django-py39/bin:$PATH
# Keep the container alive and run the agent server using the system Python
CMD ["/opt/conda/bin/python3", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8888", "--log-level", "trace", "--app-dir", "/opt/sandbox-agent"]
EOF
イメージをビルドして push します。
export SANDBOX_REPO="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/django-sandbox:v1"
docker build -f Dockerfile.sandbox -t $SANDBOX_REPO .
docker push $SANDBOX_REPO
4. オーケストレーションと実行を構成する
オーケストレーション用の Ray クラスタと実行用の Sandbox リソースをデプロイします。
1. Ray クラスタの構成
RayCluster カスタム リソースをデプロイします。クラスタで使用可能なリソース(メモリ、CPU、GPU のタイプなど)は異なる場合があります。resources のリクエストと上限を適宜調整します。
次のコマンドを実行して raycluster.yaml を作成します。これは、cat << EOF を使用して、環境変数をマニフェストに自動的に置き換えます。
cat << EOF > raycluster.yaml
apiVersion: ray.io/v1
kind: RayCluster
metadata:
name: grpo-cluster
namespace: default
spec:
rayVersion: "2.55.1"
headGroupSpec:
rayStartParams:
dashboard-host: "0.0.0.0"
template:
spec:
containers:
- name: ray-head
image: ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/ray-head:v1
ports:
- containerPort: 6379
name: gcs-server
- containerPort: 8265
name: dashboard
- containerPort: 10001
name: client
resources:
limits:
cpu: "2"
memory: "8Gi"
requests:
cpu: "2"
memory: "8Gi"
workerGroupSpecs:
- groupName: gpu-group
replicas: 1
minReplicas: 1
maxReplicas: 1
rayStartParams: {}
template:
spec:
containers:
- name: ray-worker
image: ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/ray-gpu-worker:v1
resources:
limits:
cpu: "12"
memory: "120Gi"
nvidia.com/gpu: "1"
requests:
cpu: "12"
memory: "120Gi"
nvidia.com/gpu: "1"
EOF
適用します。
kubectl apply -f raycluster.yaml
クラスタが作成され、実行されていることを確認します(数分かかることがあります)。
kubectl get raycluster
予想される出力:
NAME DESIRED WORKERS AVAILABLE WORKERS CPUS MEMORY GPUS STATUS AGE rl-cluster 1 1 ready 2m
2. SandboxRouter の構成
SandboxRouter は高速 HTTP ゲートウェイとして機能し、Ray ワーカーからのリクエストを処理して、使用可能な gVisor Pod に瞬時にブリッジします。これにより、低速の Kubernetes API サーバー Pod のライフサイクルをバイパスします。
次のコマンドを実行して sandbox_router.yaml を作成します。
cat << 'EOF' > sandbox_router.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: sandbox-claim-manager
rules:
- apiGroups: ["extensions.agents.x-k8s.io"]
resources: ["sandboxclaims"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["agents.x-k8s.io"]
resources: ["sandboxes"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: sandbox-claim-manager-binding
namespace: default
subjects:
- kind: ServiceAccount
name: default
namespace: default
roleRef:
kind: Role
name: sandbox-claim-manager
apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: Service
metadata:
name: sandbox-router
namespace: default
spec:
type: ClusterIP
selector:
app: sandbox-router
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sandbox-router-deployment
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: sandbox-router
template:
metadata:
labels:
app: sandbox-router
spec:
containers:
- name: router
image: us-central1-docker.pkg.dev/k8s-staging-images/agent-sandbox/sandbox-router:latest-main
ports:
- containerPort: 8080
env:
- name: ALLOW_UNAUTHENTICATED_ROUTER
value: "true"
EOF
適用します。
kubectl apply -f sandbox_router.yaml
デプロイが実行されていることを確認します。
kubectl get deployment sandbox-router-deployment
予想される出力:
NAME READY UP-TO-DATE AVAILABLE AGE sandbox-router-deployment 2/2 2 2 1m
3. SandboxTemplate と WarmPool の構成
GKE Agent Sandbox では、Sandbox Router を使用して、分離された事前ウォーム コンテナを即座に割り当てることができます。Pod を準備状態に保つために、SandboxTemplate と SandboxWarmPool を定義します。
次のコマンドを実行して、環境変数を使用して sandbox_warmpool.yaml を作成します。
cat << EOF > sandbox_warmpool.yaml
apiVersion: extensions.agents.x-k8s.io/v1alpha1
kind: SandboxTemplate
metadata:
name: swe-bench-django
namespace: default
spec:
podTemplate:
spec:
runtimeClassName: gvisor
securityContext:
runAsNonRoot: true
runAsUser: 1000
nodeSelector:
sandbox.gke.io/runtime: gvisor
tolerations:
- key: sandbox.gke.io/runtime
operator: Equal
value: gvisor
effect: NoSchedule
containers:
- name: sandbox
image: ${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/django-sandbox:v1
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
resources:
requests:
cpu: "2"
memory: "4Gi"
limits:
cpu: "2"
memory: "4Gi"
---
apiVersion: extensions.agents.x-k8s.io/v1alpha1
kind: SandboxWarmPool
metadata:
name: swe-bench-django-warmpool
namespace: default
spec:
replicas: 10
sandboxTemplateRef:
name: swe-bench-django
EOF
適用します。
kubectl apply -f sandbox_warmpool.yaml
SandboxWarmPool が初期化されていることを確認します。
kubectl get sandboxwarmpool
予想される出力:
NAME READY AGE swe-bench-django-warmpool 10 1m
4. セキュリティ分離
NetworkPolicy はサンドボックスを厳密に分離し、GCP メタデータ サーバーへの下り(外向き)を禁止することで、IAM トークンの盗難を防ぎます。
次のコマンドを実行して network_policy.yaml を作成します。
cat << 'EOF' > network_policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: block-metadata-egress
namespace: default
spec:
podSelector:
matchLabels:
sandbox.gke.io/runtime: gvisor
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32
EOF
ポリシーを適用します。
kubectl apply -f network_policy.yaml
NetworkPolicy が作成されたことを確認します。
kubectl get networkpolicy
予想される出力:
NAME POD-SELECTOR AGE block-metadata-egress sandbox.gke.io/runtime=gvisor 1m
5. SweBench と TRL を使用した基本的な RL ジョブ
クラスタとサンドボックスの準備ができたら、GRPO トレーニング ループを実行できます。trl ライブラリを使用して GRPO アルゴリズムをオーケストレートし、ray リモート関数を使用して分離されたサンドボックス内で生成されたコードを評価します。
この Codelab の実行を高速化するため、1 つの Django の問題に絞り込みます。次のルーティング ロジックは、リポジトリごとに異なるウォームプールを選択する方法を示しています。これは、SWE-bench データセット全体に拡張する場合に便利です。
トレーニング スクリプト
次のコマンドを実行して train_trl.py を作成します。
cat << 'EOF' > train_trl.py
import ray
from k8s_agent_sandbox import SandboxClient
from k8s_agent_sandbox.models import SandboxDirectConnectionConfig
from trl import GRPOConfig, GRPOTrainer
from transformers import AutoModelForCausalLM, AutoTokenizer
from datasets import load_dataset
import urllib.request
import re
ray.init(ignore_reinit_error=True)
# 1. Define the Ray remote evaluation function
@ray.remote
def evaluate_rollout(code, prompt_data):
client = SandboxClient(connection_config=SandboxDirectConnectionConfig(api_url="http://sandbox-router.default.svc.cluster.local:8080"))
# Claim a pre-warmed sandbox instantly based on the repo
repo = prompt_data.get("repo")
# In a full system, you'd route to different warmpools based on repo
# Here we default to django for our single task
sandbox = client.create_sandbox(
template="swe-bench-django",
warmpool="swe-bench-django-warmpool",
sandbox_ready_timeout=600
)
try:
# Check if the code is correctly formatted
bash_match = re.search(r"```bash\n(.*?)\n```", code, re.DOTALL)
if not bash_match:
return 0.0
script = bash_match.group(1)
# In a real environment, we would apply the base commit and install here
# For simplicity, we just execute the script
import shlex
script_cmd = f"bash -c {shlex.quote(script)}"
result = sandbox.commands.run(script_cmd, timeout=60)
# Calculate continuous reward based on test passage ratio
if result.exit_code == 0:
return 1.0
# Very simple heuristic reward
return 0.1
finally:
# Clean up and release the sandbox back to the pool
client.delete_sandbox(sandbox.claim_name)
# 2. Define the Reward Function for TRL
def sandbox_reward_func(prompts, completions, **kwargs):
# Dispatch evaluation to Ray cluster
futures = [
evaluate_rollout.remote(completion, {
"repo": kwargs.get('repo', [])[i] if 'repo' in kwargs else None,
"base_commit": kwargs.get('base_commit', [])[i] if 'base_commit' in kwargs else None
}) for i, completion in enumerate(completions)
]
# Block and wait for all sandbox evaluations to complete
rewards = ray.get(futures)
return rewards
# 3. Setup GRPO Trainer
@ray.remote(num_gpus=1, num_cpus=8)
def train():
# Load dataset
dataset = load_dataset("princeton-nlp/SWE-bench_Lite", split="test")
# Filter to our selected target issue
dataset = dataset.filter(lambda x: x["instance_id"] == "django__django-15388")
def format_dataset(example):
files = re.findall(r'^\+\+\+ b/(.+)$', example["patch"], re.MULTILINE)
target_file = files[0] if files else ""
file_content = ""
if target_file:
try:
github_repo = example["repo"]
url = f"https://raw.githubusercontent.com/{github_repo}/{example['base_commit']}/{target_file}"
with urllib.request.urlopen(url) as response:
file_content = response.read().decode('utf-8')
except Exception as e:
pass
prompt = f"""You are an expert software engineer.
You are given a GitHub issue and the content of the file that contains the bug.
Write an executable bash script that will modify the target file to fix the bug (e.g. using cat << 'EOF' > {target_file} or inline python edits).
Wrap your bash script in ```bash ... ``` tags. Do not output raw python code directly.
Target File: {target_file}
Original File Content:
```python
{file_content}
```
Issue:
{example['problem_statement']}
"""
return {
"prompt": prompt,
"repo": example["repo"],
"instance_id": example["instance_id"],
"base_commit": example["base_commit"],
}
dataset = dataset.map(format_dataset)
model_name = "Qwen/Qwen2.5-Coder-1.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name)
training_args = GRPOConfig(
output_dir="outputs",
learning_rate=5e-6,
max_steps=50,
per_device_train_batch_size=1,
gradient_accumulation_steps=4,
num_generations=4,
)
trainer = GRPOTrainer(
model=model_name,
processing_class=tokenizer,
reward_funcs=[sandbox_reward_func],
args=training_args,
train_dataset=dataset,
)
print("Starting GRPO training with GKE Agent Sandboxes...")
trainer.train()
def main():
print("Submitting training job to GPU worker...")
ray.get(train.remote())
if __name__ == "__main__":
main()
EOF
クラスタにジョブを送信する
まず、Ray Head ダッシュボードにポート転送し、ローカルマシンからトレーニング ジョブを送信します。
kubectl port-forward service/grpo-cluster-head-svc 8265:8265 &
ray job submit \
--address http://localhost:8265 \
--runtime-env-json '{"working_dir": "."}' \
-- python train_trl.py
実行をモニタリングする
実行の進行状況をモニタリングできます。
- Ray ダッシュボード: ブラウザで
http://localhost:8265を開きます。 - サンドボックスの要求: GKE が gVisor の下でサンドボックスを動的に要求して解放する様子を確認します。
watch -n 1 "kubectl get sandboxclaims,sandboxes,pods"
6. まとめ
おめでとうございます!GKE Agent Sandbox を使用して、GKE Standard で高パフォーマンスの分散 RL トレーニング ループを安全に構成して実行しました。