把 GPU 等待压到 10 秒内:GKE Agent Sandbox 如何加速大规模 Agentic RL

2026-09-30 17 预计阅读时间: 1 分钟
来源: cloud.google.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:12 分钟

Agentic RL 的性能瓶颈不一定在 GPU。模型可能很快生成了一段代码,但在同步训练循环中,GPU 还要等待成千上万个 CPU 沙箱拉取镜像、完成调度并执行第一条命令。只要批次里有一个沙箱迟迟没有就绪,整个训练步骤就可能停在那里。

面向这类负载,GKE Agent Sandbox 将预热池、镜像流式加载、受控补池和 Pod 原地复用组合起来。公开基准显示,在最高 18,312 个并发沙箱的测试中,平均首次命令时间从 44~85 秒降至 1.1~8.8 秒,最差等待时间则从 7.5 分钟压到 10 秒以内。

Agentic RL 为什么会把 Kubernetes 推到极限

典型的 Agentic RL 循环可以简化为:GPU 上的策略模型生成动作,CPU 沙箱执行代码或工具调用,系统收集结果并计算奖励,然后进入下一轮训练。

这个过程有三个不同于普通在线服务的特征。

同步批次由最慢的沙箱决定

假设一个训练步骤同时启动 4,000 条轨迹。即使 3,999 个沙箱在 5 秒内就绪,只要剩下一个因为冷镜像、节点磁盘或调度队列等待了几分钟,训练仍可能无法继续。

因此需要关注的不是单纯的平均启动时间,而是:

  • Time to First Command(TTFC),即从申请沙箱到第一条命令开始执行的时间;
  • P95、P99 和最大 TTFC;
  • 同步批次整体的完成时间;
  • 等待沙箱期间的 GPU/TPU 空闲比例。

镜像数量大,而且每个镜像都可能不同

传统容器平台通常围绕少量、反复复用的基础镜像优化。但 SWE-bench、R2E-Gym 一类代码任务可能让每个仓库对应一个独立 OCI 镜像,而且单个镜像可能超过 1.2 GB。

来源中的高压力测试包含 4,578 个 R2E 镜像。每个任务执行 4 条 rollout,就会产生 18,312 个沙箱请求。镜像集合远大于节点本地磁盘时,依赖普通拉取和缓存会带来 I/O 竞争以及明显的长尾。

突发创建 Pod 会冲击控制面

RL rollout 往往不是平滑到达,而是在训练步骤边界集中爆发。数万个短命 Pod 同时创建、调度、终止和回收,会让 API Server、etcd、调度器与垃圾回收逻辑同时承压,还可能放大节点健康误判。

这意味着简单地增加 GPU,或者把 Kubernetes 控制面规格调大,并不能从根本上解决问题。系统必须减少关键路径上的工作量,同时减少每轮训练制造的 Kubernetes 对象数量。

四个机制如何缩短关键路径

GKE Agent Sandbox 针对 RL 的实现并不是单一的“更快启动”,而是把多个动作移出训练关键路径。

1. 用 SandboxWarmPool 提前准备环境

预热池维护已经初始化并通过健康检查的沙箱。rollout 到来时,编排层领取现成环境,而不是临时创建所有资源。

代价是提前消耗一部分 CPU、内存和后台集群时间,但对 GPU 密集型训练而言,这通常是合理交换:空闲 CPU 的成本通常低于等待中的加速器。

2. 让镜像 hydration 在 rollout 之前完成

高基数镜像集合不可能全部常驻节点磁盘。GKE Image Streaming 与预热池配合后,可以提前规划镜像在节点上的放置,并避免把完整镜像下载放在首次命令的关键路径上。

这项优化尤其影响最大 TTFC。平均值看起来尚可时,少数冷镜像仍可能让整个同步批次多等几分钟。

3. 对预热池补充过程限速

如果几千个沙箱被同时领取,控制器不能立刻用同样规模的 Pod 创建请求补满预热池,否则会形成 API Server 的“惊群”。Agent Sandbox Controller v1.0.0 引入补池速率控制,把控制面请求维持在可预测范围内。

这里的原则很实用:业务侧并发可以很高,但控制面变更速率必须有界。

4. 原地回收 Pod,而不是每条轨迹都重建

SDK 可以保留 Pod,在两轮 episode 之间执行仓库重置和 checkout,再把它交给下一条轨迹。来源中的 4,578 镜像、每个镜像 4 条 rollout 测试里,Pod 创建数从 18,312 降到 5,869,控制面生命周期抖动减少约 3.1 倍。

原地复用也有边界。重置逻辑必须清理工作目录、临时文件、后台进程、网络状态和可能泄漏的凭据。对不可信代码,还应继续使用 gVisor 等隔离层;“复用 Pod”不能等同于“复用未清理的执行状态”。

在自己的集群先测出 TTFC 基线

下面是一个通用 Kubernetes 冒烟测试,不依赖 Agent Sandbox 专用 CRD,可用于观察当前集群在突发任务下的首次命令时间。它不复现官方完整基准,但能帮助团队建立可比较的基线。

先保存为 ttfc-job.yaml。运行前把 parallelism 和 completions 调整到集群能安全承受的范围;如需测试真实镜像压力,请将 python:3.12-slim 替换成具有代表性的任务镜像。

apiVersion: batch/v1
kind: Job
metadata:
  name: ttfc-smoke
spec:
  completions: 50
  parallelism: 50
  completionMode: Indexed
  ttlSecondsAfterFinished: 600
  template:
    metadata:
      labels:
        benchmark: ttfc-smoke
    spec:
      restartPolicy: Never
      containers:
        - name: task
          image: python:3.12-slim
          imagePullPolicy: IfNotPresent
          command:
            - python
            - -c
            - |
              import time
              print(time.time(), flush=True)
              time.sleep(20)

执行任务:

kubectl apply -f ttfc-job.yaml
kubectl wait --for=condition=complete job/ttfc-smoke --timeout=10m

再保存下面的脚本为 measure_ttfc.py。它以 Pod 创建时间为起点,以容器输出第一行时间戳为终点,计算平均值、P95 和最大 TTFC。

#!/usr/bin/env python3
import datetime as dt
import json
import math
import subprocess


def kubectl(*args: str) -> str:
    return subprocess.check_output(
        ["kubectl", *args], text=True, stderr=subprocess.STDOUT
    ).strip()


def parse_time(value: str) -> float:
    return dt.datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp()


data = json.loads(
    kubectl("get", "pods", "-l", "job-name=ttfc-smoke", "-o", "json")
)
measurements = []

for pod in data["items"]:
    name = pod["metadata"]["name"]
    created = parse_time(pod["metadata"]["creationTimestamp"])
    first_line = kubectl("logs", name).splitlines()[0]
    first_command = float(first_line)
    measurements.append((name, first_command - created))

values = sorted(value for _, value in measurements)
if not values:
    raise SystemExit("No completed pods found")

p95_index = max(0, math.ceil(len(values) * 0.95) - 1)
print(f"pods={len(values)}")
print(f"average_ttfc={sum(values) / len(values):.2f}s")
print(f"p95_ttfc={values[p95_index]:.2f}s")
print(f"max_ttfc={values[-1]:.2f}s")
print("slowest pods:")
for name, value in sorted(measurements, key=lambda item: item[1], reverse=True)[:5]:
    print(f"  {name}: {value:.2f}s")

运行并清理:

python3 measure_ttfc.py
kubectl delete job ttfc-smoke

为了让对比有效,应分别测试冷缓存、暖缓存和高镜像基数场景,并记录节点数量、节点规格、镜像大小、并发数与镜像拉取策略。不要只运行一次;长尾问题通常需要多轮突发测试才会出现。

接入时不要只看“启动快了多少”

GKE Agent Sandbox 的公开测试使用了 10 节点 gVisor 沙箱池,并覆盖 500 镜像的 SWE-bench 环境以及 4,578 镜像的 R2E 语料。测试结果很有参考价值,但不能直接替代团队自己的容量验证。

上线前建议检查以下项目:

  • 关键路径指标:同时记录平均、P95、P99 和最大 TTFC,而不是只报告均值。
  • 加速器利用率:确认 TTFC 改善确实转化为 GPU/TPU 空闲时间下降。
  • 预热池容量:根据 rollout 突发规模设置池大小,同时限制补池速率。
  • 镜像策略:统计唯一镜像数、镜像体积和节点磁盘工作集,避免把缓存命中率想得过于乐观。
  • 复用安全性:验证 git reset、工作目录清理、进程终止和凭据轮换是否完整。
  • 失败记账:每个失败沙箱都应被记录并标记为可重试,不能静默丢弃。遗漏 rollout 不只是少了一条样本,还可能给奖励分布引入偏差。
  • 恢复能力:根据任务时长评估快照、挂起和恢复功能,避免节点故障导致整批轨迹重跑。

这套方案最值得借鉴的地方,不只是某个 45 倍数字,而是它重新划分了 RL 基础设施的关键路径:镜像准备提前做,控制面写入限速做,Pod 生命周期尽量复用,而失败必须显式进入训练数据治理。对于并行 rollout 已经达到数千甚至数万规模的团队,这些决策往往比继续增加加速器更能提升研究吞吐量。


相关推荐