Hurtige isolerede Docker-miljøer til AI Agent RL Rollouts med gvx

Flaskehalsen for sikkerhed og infrastruktur i RL-rollouts for kodningsagenter
Træning og evaluering af softwareingeniøragenter på tværs af benchmarks og reinforcement learning (RL)-frameworks såsom SWE-bench, SWE-ReX, R2E-Gym, SWE-Gym, Verl og OpenHands kræver udførelse af tusindvis af samtidige rollouts af ikke-betroet, LLM-genereret kode. I virkelige softwareudviklingsopgaver redigerer en agent ikke blot tekstfiler: den kører docker build, starter databaser og mikroservices via docker compose, eksekverer integrationstestsuiter og inspicerer containerlogs.
Ved rollout-skala (128 til 1.024 samtidige episoder pr. cluster) rammer eksisterende containerinfrastruktur fire systemiske sikkerheds- og ydelsesflaskehalse:
- Lågekonflikt i værtens
dockerdog tilstandslækage: Når hundredvis af samtidige RL-workers peger på en enkelt værts/var/run/docker.sock, får hver agent root-ækvivalent adgang til værtsdæmonen, udpakning af image-lag serialiseres,docker0-subnetallokeringer udtømmes, der opstår fejl medAnother app is currently holding the xtables locki værtensiptables, og forældreløse containere lækker på tværs af episoder. - Risiko for containerflugt ved privilegeret DinD: Kørsel af standard
docker:dind-containere kræver--privileged(CAP_SYS_ADMIN), hvilket eksponerer rå værts-blokenheder, cgroup release agents og kernemoduler over for ikke-betroede RL-rollouts. - Begrænsninger ved hardwarevirtualisering (
/dev/kvm): MicroVM-runtimes (Firecracker,QEMU,Kata Containers) isolerer den indre kerne rent, men de kræver/dev/kvm. Standard Kubernetes-pods uden nested virtualisering, bare-metal-cloudinstanser hvor KVM er deaktiveret, og eksisterendegVisor(runsc)-sandboxes kan ikke starte KVM-microVM'er. - Høj latens ved episodenulstilling (
env.reset()): Start af et nytdocker:dind-miljø og hentning eller udpakning af basisimager for hver RL-episode tager 10 til 60 sekunder, hvilket lader GPU-træningsworkers stå inaktive mens de venter på nulstilling af miljøet.
gvx (rungv CLI og z.gfire.dev/gvx/pkg/gvx Go SDK) leverer en sikker rollout-runtime med defense-in-depth, der løser alle fire flaskehalse. Hvert rollout-miljø kører som en enkelt uprivilegeret, selvlåsende brugerrumsproces (isoleret af Googles gVisor Sentry-kerne, pkg/hostlockdown og et in-memory netstack), hvis samlede /var/lib/docker-tilstand befinder sig inde i en enkelt copy-on-write .ext4-fil (ext4lite).
Nul-privilegium-sikkerhed for ikke-betroede agent-rollouts: Hver
gvx-episode låser sin egen værts-OS-proces ned, før gæstekode køres (PR_SET_NO_NEW_PRIVS+ BPFseccomppå Linux, Seatbeltdeny defaultpå macOS, Restricted Token + Job Object på Windows). Selv hvis en RL-agent udførerdocker run --privilegedeller destruktiverm -rf /-kommandoer inde i sin episode, forbliver disse operationer strengt begrænset til den pågældende episodes brugerrums-Sentry-kerne og dens midlertidige.ext4-volumenfil.
Rollout-arkitektur: Parallelle uprivilegerede instanser + reflink-snapshots
I gvx er hver rollout-worker en almindelig uprivilegeret OS-proces med sin egen gVisor Sentry-kerne, sit eget brugerrums-netstack (10.0.2.15/24) og sit eget ext4lite-blokvolumen (ep-N.ext4), mens den deler en skrivebeskyttet, content-addressable erofs OCI-image-cache på tværs af værtens page cache.

Hvorfor enkeltfils-ext4lite-volumener ændrer økonomien ved RL-nulstillinger
Fordi gvx's ext4lite-filsystem gemmer et helt JBD2-journaliseret ext4-volumen (inklusive Dockers overlayfs-lagmapper, trusted.overlay.*-udvidede attributter, whiteout-enhedsnoder og image-metadata) i én almindelig værtsfil, bliver episodens livscyklusstyring en enkelt filoperation:
- 100-millisekunders
env.reset()viacp --reflink=always:- Du bygger én gang en
golden-docker.ext4-fil, der indeholder forhåndshentede basisimager (python:3.11,postgres:16,redis:7eller opgavespecifikkeSWE-bench-basisimager). - Ved starten af hver RL-episode opretter
cp --reflink=always golden-docker.ext4 ep-0042.ext4(på XFS, Btrfs, ZFS eller macOS APFS) en uafhængig Copy-on-Write-klon på under 100 millisekunder uden at kopiere de underliggende diskblokke. - Kun de 4 KiB-blokke, der er ændret under den pågældende episode, bruger yderligere diskplads.
- Du bygger én gang en
- Nedrivning uden rester (
env.close()):- Afslutning af
rungv-processen (sb.Close()i Go) og fjernelse afep-0042.ext4frigiver 100% af episodens processer, netværkssockets, mounts og lager på ~10 millisekunder. Intet bliver nogensinde monteret i værtskernens mount-tabel eller registreret i værts-cgroups.
- Afslutning af
Trin-for-trin-arbejdsgang: Gyldent .ext4-snapshot og hurtig episodenulstilling
Du kan installere rungv på hver rollout-workernode enten ved at downloade den SLSA-attesterede præbyggede binær fra 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) eller ved at køre CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest.
Trin 1: Opret det gyldne docker:dind-volumen én gang
1mkdir -p /srv/rl-snapshots /srv/rl-episodes
2
3# 1. Start en seed-docker:dind-instans, der er baseret på 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. Forudfyld basisimager og build-cache inde i 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. Stop seed-instansen elegant, så ext4lite skriver og komprimerer imaget
20kill -INT $SEED_PID
21wait $SEED_PID
Trin 2: Start parallelle RL-episoder på 100 millisekunder
Hver parallel rollout-worker kloner golden-docker.ext4 via reflink og starter en isoleret docker:dind-instans med alle images allerede til stede i /var/lib/docker:
1EPISODE_ID="ep-0042"
2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
3
4# Øjeblikkelig Copy-on-Write-klon (~100 ms på XFS/Btrfs/APFS)
5cp --reflink=always \
6 /srv/rl-snapshots/golden-docker.ext4 \
7 "/srv/rl-episodes/${EPISODE_ID}.ext4"
8
9# Start isoleret, uprivilegeret episode med 4 GiB RAM og 2 CPU'er
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# Inde i rollout-workeren er images straks tilgængelige uden netværkshentning
22docker images
23
24# Nedlukning af episoden efter beregning af belønning (~10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"
Programmatisk rollout-pool med z.gfire.dev/gvx/pkg/gvx Go SDK
Hvis dit RL-miljøserver eller din evalueringsharness er skrevet i Go (eller kalder en Go rollout-daemon over gRPC), kan du styre parallelle containerepisoder direkte via 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. Reflink-klon det gyldne ext4lite-volumen
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. Anvend en timeout pr. episode baseret på vægurstid
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 loft
55 CPUMillis: 2000, // 2.0 CPU'er
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}
Sammenligning af runtimes til RL-sandboxes for agenter
| Metrik / krav | Delt værts-dockerd | Privilegeret docker:dind | Firecracker / KVM MicroVM | Standard gVisor (runsc) | gvx (rungv / pkg/gvx) |
|---|---|---|---|---|---|
Værts-root eller --privileged påkrævet | Ja (docker.sock = root) | Ja (--privileged) | Kræver /dev/kvm + TAP-opsætning | Ja (eller userns + mount) | Nej (enkelt uprivilegeret proces) |
Virker i uprivilegerede K8s-Pods / eksisterende runsc | Nej | Nej | Nej (ingen /dev/kvm) | Fejler (EBUSY ved nested seccomp) | Ja (automatisk futex-loop-fallback) |
Understøtter indre docker build og docker compose | Deler værtsdæmonen | Ja | Ja | Fejler uden værts-DinD-tuning | Ja (overlayfs på ext4lite) |
| Netværksisolation pr. episode | Delt docker0 / xtables-lås | Separat namespace | Separat VM-NIC | Separat netns | Isoleret in-process netstack |
Latens ved varm episodenulstilling (env.reset()) | 5 s til 30 s (container/volumen-oprydning) | 10 s til 60 s (kold daemon) | ~150 ms (VM-snapshot) | 5 s til 20 s | ~100 ms (cp --reflink=always .ext4) |
| Lokal udvikling på tværs af platforme (Windows / macOS / Linux) | Kræver Linux-VM | Kræver Linux-VM | Kun Linux KVM | Kun Linux | Native på Windows, macOS og Linux |
Yderligere læsning i denne serie
- gvx: Kørsel af Linux-containere, Docker og Kubernetes i én uprivilegeret proces: Komplet arkitekturgennemgang af
gvxpå tværs af Windows (whpx,systrap), macOS (hvf) og Linux (systrap). - Kørsel af Docker, docker build og docker compose uden root eller user namespaces: Hurtig bootstrap i 3 trin og fejlfindingsguide, når
sudo,unshare(CLONE_NEWUSER),mount(2)og/dev/fuseer blokeret. - Kørsel af Docker-in-Docker inde i gVisor og nested sandboxes med gvx: Dybdegående gennemgang af, hvorfor
docker.ioog nestedrunscfejler inde igVisor(runsc), og hvordangvxfalder tilbage til_RUN_SYSCALL_LOOP-futex-loopet vedEBUSY. - Hurtige isolerede Docker-miljøer til AI-agent-RL-rollouts med gvx (denne artikel)