GoSuda

Hurtige isolerede Docker-miljøer til AI-agent RL-rollouts med gvx

By Lemon Mint views ...

Flaskehalsen i sikkerhed og infrastruktur ved RL-rollouts for kodningsagenter

Træning og evaluering af softwareingeniøragenter på tværs af benchmarks og reinforcement learning (RL)-rammeværk såsom SWE-bench, SWE-ReX, R2E-Gym, SWE-Gym, Verl og OpenHands kræver eksekvering af tusindvis af samtidige rollouts af ikke-betroet, LLM-genereret kode. I reelle softwareingeniøropgaver redigerer en agent ikke blot tekstfiler: den kører docker build, starter databaser og mikrotjenester op via docker compose, udfører integrationstestsuiter og inspicerer containerlogs.

Ved rollout-skala (128 til 1.024 samtidige episoder pr. cluster) rammer eksisterende containerinfrastruktur fire systemiske flaskehalse inden for sikkerhed og ydeevne:

  1. Værts-dockerd-låsekonflikt og 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ærtsdaemonen, image-lagudpakning serialiseres, docker0-subnetallokeringer udtømmes, fejlen Another app is currently holding the xtables lock udløses i værtens iptables, og forældreløse containere lækker på tværs af episoder.
  2. Privilegeret DinD-containerflugtrisiko: Kørsel af standard docker:dind-containere kræver --privileged (CAP_SYS_ADMIN), hvilket eksponerer rå værts-blokenheder, cgroup-release-agenter og kernemoduler over for uigennemskuede RL-rollouts.
  3. Begrænsninger ved hardwarevirtualisering (/dev/kvm): MicroVM-runtimes (Firecracker, QEMU, Kata Containers) isolerer indre kerner rent, men de kræver /dev/kvm. Standard Kubernetes-pods uden indlejret virtualisering, bare-metal-cloudinstanser hvor KVM er deaktiveret, og eksisterende gVisor (runsc)-sandkasser kan ikke starte KVM-microVMs.
  4. Langsom episodenulstilling (env.reset())-latens: At starte et nyt docker:dind-miljø og hente eller udpakke basisimages for hver RL-episode tager 10 til 60 sekunder, hvilket lader GPU-træningsworkers stå inaktive, mens de venter på nulstilling af miljøerne.

gvx (rungv CLI og z.gfire.dev/gvx/pkg/gvx Go SDK) leverer en sikker, lavindsats og token-effektiv rollout-runtime, der løser alle fire flaskehalse. Hvert rollout-miljø kører som en enkelt uprivilegeret, selvlåst user-space-proces (isoleret af Googles gVisor Sentry-kerne, pkg/hostlockdown og et in-memory netstack), hvis samlede /var/lib/docker-tilstand befinder sig i en enkelt copy-on-write .ext4-fil (ext4lite).

Lavindsats, token-effektiv eksekvering for agenter: Når docker build eller docker compose fungerer direkte i hver episode (eller kan bootstrappes af agenten i en enkelt tre-kommando-tur via rungv), spilder RL-rollouts ikke kontekstvindue-tokens eller trinbudgetter på fejlfinding af service docker start-ulimit-fejl, iptables-nft-fejl eller runc pivot_root-afvisninger. Hvert token i trajektorien går til at løse den faktiske softwareingeniøropgave.

Nul-privilegie-sikkerhed for uigennemskuede agent-rollouts: Hver gvx-episode låser sin egen værts-OS-proces før kørsel af gæstekode (PR_SET_NO_NEW_PRIVS + BPF seccomp på Linux, Seatbelt deny default på macOS, Restricted Token + Job Object på Windows). Selv hvis en RL-agent kører docker run --privileged eller destruktive rm -rf /-kommandoer inde i sin episode, forbliver disse operationer strengt begrænset til den pågældende episodes user-space-Sentry-kerne og den 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 user-space netstack (10.0.2.15/24) og sin egen ext4lite-blokvolumen (ep-N.ext4), mens den deler et skrivebeskyttet, content-addressable erofs-OCI-imagecache på tværs af værtens page cache.

Parallel Agent RL Rollout Architecture with gvx

Hvorfor enkeltfils-ext4lite-volumener ændrer økonomien ved RL-nulstilling

Fordi gvx's ext4lite-filsystem lagrer en hel JBD2-journaleret ext4-volumen (inklusive Dockers overlayfs-lagmapper, trusted.overlay.*-udvidede attributter, whiteout-device-noder og image-metadata) i én almindelig værtsfil, bliver episodens livscyklusstyring til en enkelt filoperation:

  1. 100 millisekunders env.reset() via cp --reflink=always:
    • Du bygger én gang en golden-docker.ext4-fil, der indeholder forhåndshentede basisimages (python:3.11, postgres:16, redis:7 eller opgavespecifikke SWE-bench-basisimages).
    • 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 bliver ændret under den pågældende episode, optager yderligere diskplads.
  2. Nul-rest nedrivning (env.close()):
    • Afslutning af rungv-processen (sb.Close() i Go) og fjernelse af ep-0042.ext4 frigiver 100 % af episodens processer, netværkssockets, mounts og lager på ~10 millisekunder. Intet bliver nogensinde mountet i værtskernens mount-tabel eller registreret i værts-cgroups.

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 prebuilt-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 understøttet af 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 basisimages 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 belønningsberegning (~10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"

Programmatisk rollout-pulje med z.gfire.dev/gvx/pkg/gvx Go SDK

Hvis din RL-miljøserver eller 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 af den 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 vægtur-timeout pr. episode
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 grænse
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 for RL-sandkasser til agenter

Metrik / kravDelt værts-dockerdPrivilegeret docker:dindFirecracker / KVM MicroVMStandard gVisor (runsc)gvx (rungv / pkg/gvx)
Kræver værts-root eller --privilegedJa (docker.sock = root)Ja (--privileged)Kræver /dev/kvm + TAP-opsætningJa (eller userns + mount)Nej (enkelt uprivilegeret proces)
Virker i uprivilegerede K8s-pods / eksisterende runscNejNejNej (ingen /dev/kvm)Fejler (EBUSY ved nested seccomp)Ja (automatisk futex-loop-fallback)
Understøtter indre docker build og docker composeDeler værtsdaemonJaJaFejler uden værts-DinD-tuningJa (overlayfs på ext4lite)
Netværksisolation pr. episodeDelt docker0 / xtables-låsSeparat namespaceSeparat VM-NICSeparat netnsIsoleret 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-VMKræver Linux-VMKun Linux KVMKun LinuxNative på Windows, macOS og Linux

Yderligere læsning i denne serie

  1. gvx: Kørsel af Linux-containere, Docker og Kubernetes i én uprivilegeret proces: Komplet arkitekturgennemgang af gvx på tværs af Windows (whpx, systrap), macOS (hvf) og Linux (systrap).
  2. Kørsel af Docker, docker build og docker compose uden root eller user namespaces: Hurtig 3-trins bootstrap- og fejlfindingsguide, når sudo, unshare(CLONE_NEWUSER), mount(2) og /dev/fuse er blokeret.
  3. Kørsel af Docker-in-Docker inde i gVisor og indlejrede sandkasser med gvx: Dybdegående gennemgang af hvorfor docker.io og indlejret runsc fejler inde i gVisor (runsc), og hvordan gvx falder tilbage til _RUN_SYSCALL_LOOP-futex-løkken ved EBUSY.
  4. Hurtige isolerede Docker-miljøer til RL-rollouts af AI-agenter med gvx (dette indlæg)