GoSuda

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

By Lemon Mint views ...

コーディングエージェントのRLロールアウトにおけるセキュリティとインフラストラクチャのボトルネック

ベンチマークおよび強化学習(RL)フレームワーク(SWE-bench、SWE-ReX、R2E-Gym、SWE-Gym、Verl、OpenHands など)にまたがってソフトウェアエンジニアリング・エージェントを訓練および評価するには、信頼されていないLLM生成コードの数千件の同時ロールアウトを実行する必要がある。実世界のソフトウェアエンジニアリング・タスクでは、エージェントは単にテキストファイルを編集するだけではない。docker build を実行し、docker compose を介してデータベースやマイクロサービスを起動し、統合テストスイートを実行し、コンテナのログを検査する。

ロールアウトの規模(クラスタあたり128〜1,024の同時エピソード)では、既存のコンテナインフラストラクチャは、システム的なセキュリティおよびパフォーマンスのボトルネックに四つ直面する。

  1. ホストの dockerd におけるロック競合と状態漏洩: 数百の同時RLワーカーを単一のホスト /var/run/docker.sock に向けると、すべてのエージェントがホストデーモンに対してroot相当のアクセス権を得ることになり、イメージレイヤーの展開が直列化され、docker0 のサブネット割り当てが枯渇し、ホストの iptables で Another app is currently holding the xtables lock エラーが発生し、エピソードをまたいで孤立したコンテナが漏洩する。
  2. 特権DinDコンテナのエスケープリスク: 標準の docker:dind コンテナを実行するには --privileged(CAP_SYS_ADMIN)が必要であり、これにより生のホストブロックデバイス、cgroupのrelease agent、カーネルモジュールが信頼されていないRLロールアウトに露出する。
  3. ハードウェア仮想化(/dev/kvm)の制約: MicroVMランタイム(Firecracker、QEMU、Kata Containers)は内部カーネルを明確に隔離するが、これらは /dev/kvm を必要とする。ネスト仮想化を伴わない標準のKubernetesポッド、KVMが無効化されたベアメタルのクラウドインスタンス、および既存の gVisor(runsc)サンドボックスでは、KVM microVMを起動できない。
  4. エピソードリセット(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 と BPF seccomp、macOSでは Seatbelt deny 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イメージキャッシュをホストのページキャッシュ全体で共有する。

gvxによる並列エージェントRLロールアウトアーキテクチャ

単一ファイルの ext4lite ボリュームがRLリセットの経済性を変える理由

gvx の ext4lite ファイルシステムは、JBD2ジャーナリングされた ext4 ボリューム全体(Dockerの overlayfs レイヤーディレクトリ、trusted.overlay.* 拡張属性、whiteoutデバイスノード、イメージメタデータを含む)を一つの通常のホストファイル内に格納するため、エピソードのライフサイクル管理は単一のファイル操作となる。

  1. 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ブロックのみが、追加のディスク容量を消費する。
  2. 残留物のないティアダウン(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:dindFirecracker / 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でネイティブに動作

このシリーズの関連記事

  1. gvx: 非特権プロセス一つでLinuxコンテナ、Docker、Kubernetesを実行する: Windows(whpx、systrap)、macOS(hvf)、Linux(systrap)にわたる gvx のアーキテクチャの完全な解説。
  2. rootやユーザー名前空間なしでDocker、docker build、docker composeを実行する: sudo、unshare(CLONE_NEWUSER)、mount(2)、/dev/fuse がブロックされている場合の、3ステップの簡易ブートストラップおよびトラブルシューティングガイド。
  3. gvxを用いてgVisorおよびネストされたサンドボックス内でDocker-in-Dockerを実行する: docker.io およびネストされた runsc が gVisor(runsc)内で失敗する理由と、gvx が EBUSY 発生時に _RUN_SYSCALL_LOOP futexループへどのようにフォールバックするかについての詳細解説。
  4. gvxによるAIエージェントRLロールアウトのための高速な隔離Docker環境(本記事)