gvxによるAI Agent RL Rolloutsのための高速な隔離Docker環境

コーディングエージェントのRLロールアウトにおけるセキュリティとインフラストラクチャのボトルネック
ベンチマークおよび強化学習(RL)フレームワーク(SWE-bench、SWE-ReX、R2E-Gym、SWE-Gym、Verl、OpenHands など)にまたがってソフトウェアエンジニアリング・エージェントを訓練および評価するには、信頼されていないLLM生成コードの数千件の同時ロールアウトを実行する必要がある。実世界のソフトウェアエンジニアリング・タスクでは、エージェントは単にテキストファイルを編集するだけではない。docker build を実行し、docker compose を介してデータベースやマイクロサービスを起動し、統合テストスイートを実行し、コンテナのログを検査する。
ロールアウトの規模(クラスタあたり128〜1,024の同時エピソード)では、既存のコンテナインフラストラクチャは、システム的なセキュリティおよびパフォーマンスのボトルネックに四つ直面する。
- ホストの
dockerdにおけるロック競合と状態漏洩: 数百の同時RLワーカーを単一のホスト/var/run/docker.sockに向けると、すべてのエージェントがホストデーモンに対してroot相当のアクセス権を得ることになり、イメージレイヤーの展開が直列化され、docker0のサブネット割り当てが枯渇し、ホストのiptablesでAnother app is currently holding the xtables lockエラーが発生し、エピソードをまたいで孤立したコンテナが漏洩する。 - 特権DinDコンテナのエスケープリスク: 標準の
docker:dindコンテナを実行するには--privileged(CAP_SYS_ADMIN)が必要であり、これにより生のホストブロックデバイス、cgroupのrelease agent、カーネルモジュールが信頼されていないRLロールアウトに露出する。 - ハードウェア仮想化(
/dev/kvm)の制約: MicroVMランタイム(Firecracker、QEMU、Kata Containers)は内部カーネルを明確に隔離するが、これらは/dev/kvmを必要とする。ネスト仮想化を伴わない標準のKubernetesポッド、KVMが無効化されたベアメタルのクラウドインスタンス、および既存のgVisor(runsc)サンドボックスでは、KVM microVMを起動できない。 - エピソードリセット(
env.reset())の遅延: RLエピソードごとに新しいdocker:dind環境を起動し、ベースイメージを取得または展開するには10〜60秒かかり、環境のリセットを待つ間GPUの訓練ワーカーがアイドル状態のままになる。
gvx(rungv CLIおよび z.gfire.dev/gvx/pkg/gvx Go SDK)は、上記の四つのボトルネックすべてを解決する安全で多層防御型のロールアウトランタイムを提供する。各ロールアウト環境は、単一の非特権かつ自己ロックされたユーザー空間プロセスとして実行される(GoogleのgVisorのSentryカーネル、pkg/hostlockdown、およびインメモリのnetstackによって隔離される)。その /var/lib/docker の状態全体は、**単一のコピーオンライトな .ext4 ファイル(ext4lite)**内に存在する。
信頼されていないエージェントのロールアウトのためのゼロ特権セキュリティ: すべての
gvxエピソードは、ゲストコードを実行する前に自身のホストOSプロセスをロックダウンする(LinuxではPR_SET_NO_NEW_PRIVSと BPFseccomp、macOSでは Seatbeltdeny default、Windowsでは Restricted Token と Job Object)。RLエージェントがエピソード内でdocker run --privilegedや破壊的なrm -rf /コマンドを実行したとしても、それらの操作は、そのエピソード固有のユーザー空間Sentryカーネルおよび一時的な.ext4ボリュームファイルの内部に厳密に閉じ込められたままとなる。
ロールアウトアーキテクチャ: 並列な非特権インスタンスとReflinkスナップショット
gvx では、各ロールアウトワーカーは、独自のgVisor Sentry カーネル、独自のユーザー空間 netstack(10.0.2.15/24)、および独自の ext4lite ブロックボリューム(ep-N.ext4)を備えた通常の非特権OSプロセスであり、読み取り専用のコンテンツアドレッサブルな erofs OCIイメージキャッシュをホストのページキャッシュ全体で共有する。

単一ファイルの ext4lite ボリュームがRLリセットの経済性を変える理由
gvx の ext4lite ファイルシステムは、JBD2ジャーナリングされた ext4 ボリューム全体(Dockerの overlayfs レイヤーディレクトリ、trusted.overlay.* 拡張属性、whiteoutデバイスノード、イメージメタデータを含む)を一つの通常のホストファイル内に格納するため、エピソードのライフサイクル管理は単一のファイル操作となる。
cp --reflink=alwaysによる100ミリ秒のenv.reset():- 事前に取得したベースイメージ(
python:3.11、postgres:16、redis:7、またはタスク固有のSWE-benchベースイメージ)を含むgolden-docker.ext4ファイルを一度だけ構築する。 - 各RLエピソードの開始時に、
cp --reflink=always golden-docker.ext4 ep-0042.ext4(XFS、Btrfs、ZFS、またはmacOS APFS上)を実行すると、基盤となるディスクブロックをコピーすることなく、100ミリ秒未満で独立したコピーオンライトのクローンが作成される。 - そのエピソードの間に変更された4 KiBブロックのみが、追加のディスク容量を消費する。
- 事前に取得したベースイメージ(
- 残留物のないティアダウン(
env.close()):rungvプロセスを終了し(Goではsb.Close())、ep-0042.ext4のリンクを解除すると、約10ミリ秒でそのエピソードのプロセス、ネットワークソケット、マウント、ストレージの100%が回収される。ホストカーネルのマウントテーブルに何かがマウントされることや、ホストのcgroupに登録されることは一切ない。
段階的ワークフロー: ゴールデン .ext4 スナップショットと高速エピソードリセット
各ロールアウトワーカーノードへの rungv のインストールは、GitHub Releases からSLSA認証済みのビルド済みバイナリをダウンロードする(gh attestation verify rungv-v0.2.0-linux-amd64.tar.gz --repo gfire-sigs/gvx --signer-workflow gfire-sigs/gvx/.github/workflows/release.yml --deny-self-hosted-runners)か、CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest を実行することで行える。
ステップ1: ゴールデン docker:dind ボリュームを一度だけ作成する
1mkdir -p /srv/rl-snapshots /srv/rl-episodes
2
3# 1. golden-docker.ext4 を基盤とするシードの docker:dind インスタンスを起動する
4rungv run \
5 -p unix:/srv/rl-snapshots/seed.sock:2375 \
6 --mount type=volume,src=/srv/rl-snapshots/golden-docker.ext4,dst=/var/lib/docker \
7 -e DOCKER_TLS_CERTDIR= \
8 docker:dind --tls=false &
9SEED_PID=$!
10
11# 2. golden-docker.ext4 内にベースイメージとビルドキャッシュを事前に投入する
12export DOCKER_HOST="unix:///srv/rl-snapshots/seed.sock"
13until docker info >/dev/null 2>&1; do sleep 0.5; done
14
15docker pull python:3.11-slim
16docker pull redis:7-alpine
17docker pull postgres:16-alpine
18
19# 3. ext4lite がイメージをフラッシュおよび圧縮できるよう、シードインスタンスを正常に停止する
20kill -INT $SEED_PID
21wait $SEED_PID
ステップ2: 100ミリ秒で並列RLエピソードを起動する
各並列ロールアウトワーカーは、reflinkによって golden-docker.ext4 を複製し、/var/lib/docker にすべてのイメージが既に存在している状態で、隔離された docker:dind インスタンスを起動する。
1EPISODE_ID="ep-0042"
2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
3
4# 即時のコピーオンライトクローン(XFS/Btrfs/APFS上で約100ms)
5cp --reflink=always \
6 /srv/rl-snapshots/golden-docker.ext4 \
7 "/srv/rl-episodes/${EPISODE_ID}.ext4"
8
9# 4 GiBのRAMと2つのCPUを備えた、隔離された非特権エピソードを起動する
10rungv run \
11 -m 4g --cpus 2 \
12 -p "unix:${SOCK}:2375" \
13 --mount "type=volume,src=/srv/rl-episodes/${EPISODE_ID}.ext4,dst=/var/lib/docker" \
14 -e DOCKER_TLS_CERTDIR= \
15 docker:dind --tls=false &
16EP_PID=$!
17
18export DOCKER_HOST="unix://${SOCK}"
19until docker info >/dev/null 2>&1; do sleep 0.2; done
20
21# ロールアウトワーカー内では、ネットワークからの取得なしにイメージがただちに利用可能である
22docker images
23
24# 報酬計算後のエピソードのティアダウン(約10ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"
z.gfire.dev/gvx/pkg/gvx Go SDKを用いたプログラムによるロールアウトプール
RL環境サーバーまたは評価ハーネスがGoで記述されている場合(あるいはgRPC経由でGoのロールアウトデーモンを呼び出す場合)、z.gfire.dev/gvx/pkg/gvx を通じて並列コンテナエピソードを直接管理できる。
1package main
2
3import (
4 "bytes"
5 "context"
6 "fmt"
7 "log"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "time"
12
13 "z.gfire.dev/gvx/pkg/gvx"
14)
15
16func runEpisode(ctx context.Context, goldenExt4, scratchDir string, epID int) error {
17 epExt4 := filepath.Join(scratchDir, fmt.Sprintf("ep-%04d.ext4", epID))
18 defer os.Remove(epExt4)
19
20 // 1. ゴールデンな ext4lite ボリュームをreflinkで複製する
21 cpCmd := exec.CommandContext(ctx, "cp", "--reflink=always", goldenExt4, epExt4)
22 if err := cpCmd.Run(); err != nil {
23 return fmt.Errorf("clone golden volume: %w", err)
24 }
25
26 // 2. エピソードごとの実時間タイムアウトを適用する
27 epCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
28 defer cancel()
29
30 var stdout, stderr bytes.Buffer
31 sb, err := gvx.New(epCtx, gvx.Config{
32 Platform: gvx.PlatformAuto,
33 NetworkMode: gvx.NetModeNAT,
34 NetRaw: true,
35 AllowPacketSocketWrite: true,
36 AllowSUID: true,
37 OCIImage: "docker:dind",
38 Entrypoint: []string{"sh", "-c"},
39 EntrypointSet: true,
40 Cmd: []string{
41 "dockerd-entrypoint.sh --tls=false >/var/log/dockerd.log 2>&1 & " +
42 "until docker info >/dev/null 2>&1; do sleep 0.2; done && " +
43 "docker run --rm python:3.11-slim python -c 'print(6 * 7)'",
44 },
45 Envv: []string{"DOCKER_TLS_CERTDIR="},
46 Mounts: []gvx.Mount{
47 {
48 Kind: gvx.MountVolume,
49 ImagePath: epExt4,
50 GuestPath: "/var/lib/docker",
51 },
52 },
53 Resources: gvx.Resources{
54 MemoryBytes: 4 << 30, // 4 GiBの上限
55 CPUMillis: 2000, // 2.0 CPU
56 },
57 Stdout: &stdout,
58 Stderr: &stderr,
59 })
60 if err != nil {
61 return fmt.Errorf("init episode %d: %w", epID, err)
62 }
63 defer sb.Close()
64
65 exitCode, err := sb.Run(epCtx)
66 if err != nil {
67 return fmt.Errorf("run episode %d: %w", epID, err)
68 }
69
70 log.Printf("episode %d finished: exit=%d stdout=%q", epID, exitCode, stdout.String())
71 return nil
72}
73
74func main() {
75 ctx := context.Background()
76 if err := runEpisode(ctx, "/srv/rl-snapshots/golden-docker.ext4", "/srv/rl-episodes", 1); err != nil {
77 log.Fatal(err)
78 }
79}
エージェントRLサンドボックスランタイムの比較
| 指標 / 要件 | 共有ホストの dockerd | 特権 docker:dind | Firecracker / KVM MicroVM | 標準の gVisor(runsc) | gvx(rungv / pkg/gvx) |
|---|---|---|---|---|---|
ホストの root または --privileged が必要か | はい(docker.sock = root) | はい(--privileged) | /dev/kvm と TAP の設定が必要 | はい(または userns + mount) | いいえ(単一の非特権プロセス) |
非特権のK8s Pod / 既存の runsc 内で動作するか | いいえ | いいえ | いいえ(/dev/kvm がない) | 失敗(ネストされたseccompで EBUSY) | はい(futexループへの自動フォールバック) |
内部での docker build と docker compose をサポートするか | ホストデーモンを共有 | はい | はい | ホスト側のDinDチューニングなしでは失敗 | はい(ext4lite 上の overlayfs) |
| エピソードごとのネットワーク隔離 | docker0 / xtables ロックを共有 | 別の名前空間 | 別のVM NIC | 別のnetns | プロセス内の隔離された netstack |
ウォーム状態でのエピソードリセット遅延(env.reset()) | 5秒〜30秒(コンテナ/ボリュームの削除) | 10秒〜60秒(コールドデーモン) | 約150ms(VMスナップショット) | 5秒〜20秒 | 約100ms(cp --reflink=always の .ext4) |
| クロスプラットフォームのローカル開発(Windows / macOS / Linux) | Linux VMが必要 | Linux VMが必要 | Linux KVMのみ | Linuxのみ | Windows、macOS、Linuxでネイティブに動作 |
このシリーズの関連記事
- gvx: 非特権プロセス一つでLinuxコンテナ、Docker、Kubernetesを実行する: Windows(
whpx、systrap)、macOS(hvf)、Linux(systrap)にわたるgvxのアーキテクチャの完全な解説。 - rootやユーザー名前空間なしでDocker、docker build、docker composeを実行する:
sudo、unshare(CLONE_NEWUSER)、mount(2)、/dev/fuseがブロックされている場合の、3ステップの簡易ブートストラップおよびトラブルシューティングガイド。 - gvxを用いてgVisorおよびネストされたサンドボックス内でDocker-in-Dockerを実行する:
docker.ioおよびネストされたrunscがgVisor(runsc)内で失敗する理由と、gvxがEBUSY発生時に_RUN_SYSCALL_LOOPfutexループへどのようにフォールバックするかについての詳細解説。 - gvxによるAIエージェントRLロールアウトのための高速な隔離Docker環境(本記事)