Snabba isolerade Docker-miljöer för AI-agenters RL-rollouts med gvx

Säkerhets- och infrastrukturflaskhalsen i RL-rollouts för kodningsagenter
Träning och utvärdering av mjukvaruutvecklingsagenter över benchmarks och ramverk för reinforcement learning (RL), såsom SWE-bench, SWE-ReX, R2E-Gym, SWE-Gym, Verl och OpenHands, kräver exekvering av tusentals samtidiga rollouts av ej betrodd, LLM-genererad kod. I verkliga mjukvaruutvecklingsuppgifter redigerar en agent inte bara textfiler: den kör docker build, startar databaser och mikrotjänster via docker compose, exekverar integrationstestsviter och inspekterar containerloggar.
Vid rollout-skala (128 till 1 024 samtidiga episoder per kluster) träffar befintlig containerinfrastruktur fyra systemiska säkerhets- och prestandaflaskhalsar:
- Host
dockerdLock Contention and State Leakage: Om hundratals samtidiga RL-arbetare riktas mot en enda värd-/var/run/docker.sockfår varje agent root-motsvarande åtkomst till värdens daemon, extraheringen av imagelager serialiseras,docker0-subnätsallokeringarna tar slut, feletAnother app is currently holding the xtables lockutlöses i värdensiptables, och föräldralösa containrar läcker mellan episoder. - Privileged DinD Container Escape Risk: Att köra standardcontainrar
docker:dindkräver--privileged(CAP_SYS_ADMIN), vilket exponerar råa blockenheter på värden, cgroup release agents och kärnmoduler för ej betrodda RL-rollouts. - Hardware Virtualization (
/dev/kvm) Constraints: MicroVM-runtimes (Firecracker,QEMU,Kata Containers) isolerar interna kärnor på ett rent sätt, men de kräver/dev/kvm. Vanliga Kubernetes-pods utan nästlad virtualisering, bare-metal-molninstanser där KVM är inaktiverat och befintligagVisor(runsc)-sandlådor kan inte starta KVM-microVMs. - Slow Episode Reset (
env.reset()) Latency: Att starta en nydocker:dind-miljö och hämta eller packa upp basimages för varje RL-episod tar 10 till 60 sekunder, vilket lämnar GPU-träningsarbetare overksamma medan de väntar på att miljöerna ska återställas.
gvx (rungv CLI och z.gfire.dev/gvx/pkg/gvx Go SDK) tillhandahåller en säker, lågansträngande och tokeneffektiv rollout-runtime som löser alla fyra flaskhalsarna. Varje rollout-miljö körs som en enda oprivilegierad, självlåst userspace-process (isolerad av Googles gVisor Sentry-kärna, pkg/hostlockdown och en netstack i minnet) vars hela /var/lib/docker-tillstånd finns inuti en enda copy-on-write-fil .ext4 (ext4lite).
Low-Effort, Token-Efficient Execution for Agents: När
docker buildellerdocker composefungerar direkt i varje episod (eller kan startas av agenten i en enda tre-kommandotur viarungv) slösar RL-rollouts inte kontextfönstrets tokens eller stegbudgetar på felsökning avservice docker start-fel medulimit,iptables-nft-fel ellerruncpivot_root-avvisningar. Varje token i trajektorian går till att lösa den faktiska mjukvaruutvecklingsuppgiften.
Zero-Privilege Security for Untrusted Agent Rollouts: Varje
gvx-episod låser ner sin egen värd-OS-process innan gästkod körs (PR_SET_NO_NEW_PRIVS+ BPFseccomppå Linux, Seatbeltdeny defaultpå macOS, Restricted Token + Job Object på Windows). Även om en RL-agent kördocker run --privilegedeller destruktivarm -rf /-kommandon inom sin episod, förblir dessa operationer strikt begränsade till den episodens userspace-Sentry-kärna och dess tillfälliga.ext4-volymfil.
Rolloutarkitektur: parallella oprivilegierade instanser + reflink-ögonblicksbilder
I gvx är varje rollout-arbetare en vanlig oprivilegierad OS-process med sin egen gVisor Sentry-kärna, sin egen userspace-netstack (10.0.2.15/24) och sin egen ext4lite-blockvolym (ep-N.ext4), medan den delar en skrivskyddad, innehållsadresserbar erofs-OCI-bildcache över värdens sidcache.

Varför ettfilsvolymer av ext4lite förändrar ekonomin för RL-återställning
Eftersom gvx:s ext4lite-filsystem lagrar en hel JBD2-journalförd ext4-volym (inklusive Dockers overlayfs-lagerkataloger, trusted.overlay.*-utökade attribut, whiteout-enhetsnoder och bildmetadata) inuti en enda vanlig värdfil, blir hanteringen av episodens livscykel en enda filoperation:
- 100-millisekunders
env.reset()viacp --reflink=always:- Du bygger en gång en fil
golden-docker.ext4som innehåller förhämtade basimages (python:3.11,postgres:16,redis:7eller uppgiftsspecifikaSWE-bench-basimages). - I början av varje RL-episod skapar
cp --reflink=always golden-docker.ext4 ep-0042.ext4(på XFS, Btrfs, ZFS eller macOS APFS) en oberoende Copy-on-Write-klon på under 100 millisekunder utan att kopiera underliggande diskblock. - Endast de 4 KiB-block som ändras under just den episoden tar upp ytterligare diskutrymme.
- Du bygger en gång en fil
- Nollrestsnedmontering (
env.close()):- Att avsluta
rungv-processen (sb.Close()i Go) och ta bortep-0042.ext4frigör 100 % av episodens processer, nätverkssocklar, monteringar och lagring på ~10 millisekunder. Ingenting monteras någonsin i värdkärnans monteringstabell eller registreras i värdens cgroups.
- Att avsluta
Steg-för-steg-arbetsflöde: Golden .ext4-ögonblicksbild och snabb episodåterställning
Du kan installera rungv på varje rollout-arbetarnod antingen genom att ladda ner den SLSA-attesterade förkompilerade binären från 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 genom att köra CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest.
Steg 1: Skapa den gyllene docker:dind-volymen en gång
1mkdir -p /srv/rl-snapshots /srv/rl-episodes
2
3# 1. Starta en seed-instans av docker:dind med stöd från 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. Förfyll basimages och byggcache inuti 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. Stoppa seed-instansen på ett kontrollerat sätt så att ext4lite spolar och komprimerar imagen
20kill -INT $SEED_PID
21wait $SEED_PID
Steg 2: Starta parallella RL-episoder på 100 millisekunder
Varje parallell rollout-arbetare klonar golden-docker.ext4 via reflink och startar en isolerad docker:dind-instans med alla images redan närvarande i /var/lib/docker:
1EPISODE_ID="ep-0042"
2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
3
4# Omedelbar 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# Starta isolerad oprivilegierad episod med 4 GiB RAM och 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# Inuti rollout-arbetaren är images omedelbart tillgängliga utan nätverkshämtning
22docker images
23
24# Episodnedmontering efter beräkning av belöning (~10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"
Programmatisk rollout-pool med Go SDK:n z.gfire.dev/gvx/pkg/gvx
Om din RL-miljöserver eller utvärderingsharness är skriven i Go (eller anropar en Go-rolloutdaemon via gRPC) kan du hantera parallella containerepisoder direkt 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-klona den gyllene ext4lite-volymen
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. Tillämpa en tidsgräns per episod i väggklocktid
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-tak
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}
Jämförelse av sandlåderuntimes för RL-agenter
| Mått / Krav | Delad värd-dockerd | Privilegierad docker:dind | Firecracker / KVM MicroVM | Standard gVisor (runsc) | gvx (rungv / pkg/gvx) |
|---|---|---|---|---|---|
Kräver värdens root eller --privileged | Ja (docker.sock = root) | Ja (--privileged) | Kräver /dev/kvm + TAP-konfiguration | Ja (eller userns + mount) | Nej (enda oprivilegierad process) |
Fungerar i oprivilegierade K8s-pods / befintlig runsc | Nej | Nej | Nej (ingen /dev/kvm) | Misslyckas (EBUSY vid nästlad seccomp) | Ja (automatisk futex-loop-reserv) |
Stöder inre docker build och docker compose | Delar värddaemonen | Ja | Ja | Misslyckas utan värd-DinD-justering | Ja (overlayfs på ext4lite) |
| Nätverksisolering per episod | Delat docker0 / xtables-lås | Separat namnrymd | Separat VM-NIC | Separat netns | Isolerad netstack i processen |
Latens för varm episodåterställning (env.reset()) | 5 s till 30 s (rensning av container/volym) | 10 s till 60 s (kall daemon) | ~150 ms (VM-ögonblicksbild) | 5 s till 20 s | ~100 ms (cp --reflink=always av .ext4) |
| Plattformsoberoende lokal utveckling (Windows / macOS / Linux) | Kräver Linux-VM | Kräver Linux-VM | Endast Linux KVM | Endast Linux | Inbyggt på Windows, macOS och Linux |
Vidare läsning i den här serien
- gvx: Kör Linux-containrar, Docker och Kubernetes i en enda oprivilegierad process: Komplett arkitekturgenomgång av
gvxpå Windows (whpx,systrap), macOS (hvf) och Linux (systrap). - Kör Docker, docker build och docker compose utan root eller användarnamnrymder: Snabb startguide i tre steg och felsökningsguide när
sudo,unshare(CLONE_NEWUSER),mount(2)och/dev/fuseär blockerade. - Kör Docker-in-Docker inuti gVisor och nästlade sandlådor med gvx: Djupdykning i varför
docker.iooch nästladrunscmisslyckas inutigVisor(runsc) och hurgvxfaller tillbaka till futex-loopen_RUN_SYSCALL_LOOPvidEBUSY. - Snabba isolerade Docker-miljöer för RL-rollouts av AI-agenter med gvx (detta inlägg)