Rychlá izolovaná prostředí Docker pro rollouty AI agentů při učení s posilováním pomocí gvx

Úzké hrdlo zabezpečení a infrastruktury v RL rolloutech kódujících agentů
Trénování a vyhodnocování agentů pro softwarové inženýrství napříč benchmarky a frameworky pro reinforcement learning (RL), jako jsou SWE-bench, SWE-ReX, R2E-Gym, SWE-Gym, Verl a OpenHands, vyžaduje spouštění tisíců souběžných rolloutů nedůvěryhodného kódu generovaného LLM. V reálných úlohách softwarového inženýrství agent nejen upravuje textové soubory: spouští docker build, zprovozňuje databáze a mikroslužby pomocí docker compose, provádí sady integračních testů a kontroluje logy kontejnerů.
Při škále rolloutů (128 až 1 024 souběžných epizod na jeden cluster) narází stávající kontejnerová infrastruktura na čtyři systémová bezpečnostní a výkonnostní úzká hrdla:
- Zámková kontence hostitelského
dockerda únik stavu: Nasměrování stovek souběžných RL workerů na jediný host/var/run/docker.sockdává každému agentovi přístup ekvivalentní rootovi k démonu hostitele, serializuje extrakci vrstev obrazů, vyčerpává podsítědocker0, vyvolává chybyAnother app is currently holding the xtables lockviptableshostitele a napříč epizodami zanechává osiřelé kontejnery. - Únik z kontejneru u privilegovaného DinD: Spouštění standardních kontejnerů
docker:dindvyžaduje--privileged(CAP_SYS_ADMIN), což nechráněným RL rolloutům zpřístupňuje surová bloková zařízení hostitele, cgroup release agenty a jaderné moduly. - Omezení hardwarové virtualizace (
/dev/kvm): MicroVM runtime (Firecracker,QEMU,Kata Containers) čistě izolují vnitřní jádra, ale vyžadují/dev/kvm. Standardní pody Kubernetes bez vnořené virtualizace, bare-metal cloudové instance, kde je KVM vypnuto, a stávající sandboxygVisor(runsc) nemohou spouštět microVM KVM. - Pomalý reset epizody (
env.reset()): Spuštění nového prostředídocker:dinda stahování nebo rozbalování základních obrazů pro každou RL epizodu trvá 10 až 60 sekund, takže GPU trénovací workery nečinně čekají na resety prostředí.
gvx (CLI rungv a Go SDK z.gfire.dev/gvx/pkg/gvx) poskytuje bezpečný rollout runtime s obrannou hloubkou, který řeší všechna čtyři úzká hrdla. Každé prostředí rolloutu běží jako jediný neprivilegovaný, samozamykající se uživatelský proces (izolovaný jaderem gVisor Sentry od Googlu, balíčkem pkg/hostlockdown a netstack v paměti), jehož celý stav /var/lib/docker se nachází uvnitř jediného copy-on-write souboru .ext4 (ext4lite).
Bezpečnost bez privilegií pro nedůvěryhodné rollouty agentů: Každá epizoda
gvxuzamkne svůj vlastní hostitelský proces OS dříve, než spustí hostující kód (PR_SET_NO_NEW_PRIVS+ BPFseccompna Linuxu, Seatbeltdeny defaultna macOS, Restricted Token + Job Object na Windows). I když RL agent spustídocker run --privilegednebo destruktivní příkazyrm -rf /uvnitř své epizody, tyto operace zůstávají přísně omezeny na uživatelské jádroSentrydané epizody a její efemérní svazek souboru.ext4.
Architektura rolloutů: paralelní neprivilegované instance + reflink snapshoty
V gvx je každý rollout worker běžný neprivilegovaný proces OS se svým vlastním jádrem gVisor Sentry, vlastním uživatelským netstack (10.0.2.15/24) a vlastním blokovým svazkem ext4lite (ep-N.ext4), přičemž sdílí jen pro čtení určenou, obsahově adresovatelnou cache obrazů erofs OCI v page cache hostitele.

Proč svazky ext4lite v jediném souboru mění ekonomiku resetů RL
Protože filesystém ext4lite ukládá celý svazek ext4 s žurnálem JBD2 (včetně adresářů vrstev overlayfs Dockeru, rozšířených atributů trusted.overlay.*, whiteout device nodes a metadat obrazů) uvnitř jediného běžného souboru hostitele, správa životního cyklu epizody se stává jedinou operací nad souborem:
- 100milisekundový
env.reset()pomocícp --reflink=always:- Jednou sestavíte soubor
golden-docker.ext4, který obsahuje předem stažené základní obrazy (python:3.11,postgres:16,redis:7nebo obrazy základů specifické pro úlohySWE-bench). - Na začátku každé RL epizody příkaz
cp --reflink=always golden-docker.ext4 ep-0042.ext4(na XFS, Btrfs, ZFS nebo macOS APFS) vytvoří nezávislý klon Copy-on-Write za méně než 100 milisekund, aniž by kopíroval podkladové bloky disku. - Dodatečné místo na disku zabírají pouze 4 KiB bloky, které byly během dané epizody změněny.
- Jednou sestavíte soubor
- Úklid bez zbytků (
env.close()):- Ukončení procesu
rungv(sb.Close()v Go) a odlinkováníep-0042.ext4uvolní 100 % procesů, síťových socketů, připojení a úložiště epizody za ~10 milisekund. Nic se nikdy nepřipojí do mount table jádra hostitele ani se nezaregistruje v cgroup hostitele.
- Ukončení procesu
Postup krok za krokem: zlatý snapshot .ext4 a rychlý reset epizody
Příkaz rungv můžete nainstalovat na každý uzel rollout workeru buď stažením prebuilt binárky s atestací SLSA z GitHub Releases (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), nebo spuštěním CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest.
Krok 1: Jednorázově vytvořte zlatý svazek docker:dind
1mkdir -p /srv/rl-snapshots /srv/rl-episodes
2
3# 1. Spusťte seed instanci docker:dind založenou na golden-docker.ext4
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. Předpřipravte základní obrazy a cache sestavení uvnitř 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. Ukončete seed instanci elegantně, aby ext4lite provedl flush a kompaktaci obrazu
20kill -INT $SEED_PID
21wait $SEED_PID
Krok 2: Spusťte paralelní RL epizody během 100 milisekund
Každý paralelní rollout worker klonuje golden-docker.ext4 pomocí reflinku a spustí izolovanou instanci docker:dind, ve které jsou všechny obrazy již přítomny v /var/lib/docker:
1EPISODE_ID="ep-0042"
2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
3
4# Okamžitý klon Copy-on-Write (~100 ms na XFS/Btrfs/APFS)
5cp --reflink=always \
6 /srv/rl-snapshots/golden-docker.ext4 \
7 "/srv/rl-episodes/${EPISODE_ID}.ext4"
8
9# Spuštění izolované neprivilegované epizody se 4 GiB RAM a 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# Uvnitř rollout workeru jsou obrazy okamžitě dostupné bez stahování ze sítě
22docker images
23
24# Ukončení epizody po výpočtu odměny (~10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"
Programatický pool rolloutů pomocí Go SDK z.gfire.dev/gvx/pkg/gvx
Pokud je váš RL environment server nebo evaluační harness napsán v Go (nebo volá rollout daemon v Go přes gRPC), můžete paralelní epizody kontejnerů spravovat přímo prostřednictvím 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. Klonování golden ext4lite svazku pomocí reflinku
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. Nastavení časového limitu epizody podle nástěnných hodin
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, // limit 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}
Srovnání běhových prostředí pro sandboxy RL agentů
| Metrika / požadavek | Sdílený hostitelský dockerd | Privilegovaný docker:dind | Firecracker / KVM MicroVM | Standardní gVisor (runsc) | gvx (rungv / pkg/gvx) |
|---|---|---|---|---|---|
Vyžaduje root hostitele nebo --privileged | Ano (docker.sock = root) | Ano (--privileged) | Potřebuje /dev/kvm + nastavení TAP | Ano (nebo userns + mount) | Ne (jediný neprivilegovaný proces) |
Funguje uvnitř neprivilegovaných K8s Podů / stávajícího runsc | Ne | Ne | Ne (bez /dev/kvm) | Selhává (EBUSY u vnořeného seccomp) | Ano (automatický záložní futex loop) |
Podporuje vnitřní docker build a docker compose | Sdílí démona hostitele | Ano | Ano | Selhává bez ladění hostitelského DinD | Ano (overlayfs na ext4lite) |
| Síťová izolace na úrovni epizody | Sdílený docker0 / zámek xtables | Oddělený namespace | Oddělená NIC VM | Oddělený netns | Izolovaný netstack v procesu |
Latence resetu teplé epizody (env.reset()) | 5 s až 30 s (prune kontejnerů/svazků) | 10 s až 60 s (studený démon) | ~150 ms (snapshot VM) | 5 s až 20 s | ~100 ms (cp --reflink=always .ext4) |
| Lokální vývoj napříč platformami (Windows / macOS / Linux) | Vyžaduje VM s Linuxem | Vyžaduje VM s Linuxem | Pouze Linux KVM | Pouze Linux | Nativně na Windows, macOS a Linuxu |
Další čtení v této sérii
- gvx: Spouštění kontejnerů Linuxu, Dockeru a Kubernetes v jediném neprivilegovaném procesu: Kompletní architektonický průvodce
gvxnapříč Windows (whpx,systrap), macOS (hvf) a Linuxem (systrap). - Spouštění Dockeru, docker build a docker compose bez rootu nebo user namespaces: Rychlý třikrokový návod pro bootstrap a řešení potíží, když jsou blokovány
sudo,unshare(CLONE_NEWUSER),mount(2)a/dev/fuse. - Docker-in-Docker uvnitř gVisor a vnořených sandboxů s gvx: Hloubková analýza toho, proč
docker.ioa vnořenýrunscselhávají uvnitřgVisor(runsc) a jakgvxpřechází na záložní futex loop_RUN_SYSCALL_LOOPpřiEBUSY. - Rychlá izolovaná Docker prostředí pro RL rollouty AI agentů s gvx (tento příspěvek)