GoSuda

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

By Lemon Mint views ...

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:

  1. Host dockerd Lock Contention and State Leakage: Om hundratals samtidiga RL-arbetare riktas mot en enda värd-/var/run/docker.sock får varje agent root-motsvarande åtkomst till värdens daemon, extraheringen av imagelager serialiseras, docker0-subnätsallokeringarna tar slut, felet Another app is currently holding the xtables lock utlöses i värdens iptables, och föräldralösa containrar läcker mellan episoder.
  2. Privileged DinD Container Escape Risk: Att köra standardcontainrar docker:dind krä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.
  3. 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 befintliga gVisor (runsc)-sandlådor kan inte starta KVM-microVMs.
  4. Slow Episode Reset (env.reset()) Latency: Att starta en ny docker: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 build eller docker compose fungerar direkt i varje episod (eller kan startas av agenten i en enda tre-kommandotur via rungv) slösar RL-rollouts inte kontextfönstrets tokens eller stegbudgetar på felsökning av service docker start-fel med ulimit, iptables-nft-fel eller runc pivot_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 + BPF seccomp på Linux, Seatbelt deny default på macOS, Restricted Token + Job Object på Windows). Även om en RL-agent kör docker run --privileged eller destruktiva rm -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.

Parallell arkitektur för RL-rollouts av agenter med gvx

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:

  1. 100-millisekunders env.reset() via cp --reflink=always:
    • Du bygger en gång en fil golden-docker.ext4 som innehåller förhämtade basimages (python:3.11, postgres:16, redis:7 eller uppgiftsspecifika SWE-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.
  2. Nollrestsnedmontering (env.close()):
    • Att avsluta rungv-processen (sb.Close() i Go) och ta bort ep-0042.ext4 frigö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.

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 / KravDelad värd-dockerdPrivilegierad docker:dindFirecracker / KVM MicroVMStandard gVisor (runsc)gvx (rungv / pkg/gvx)
Kräver värdens root eller --privilegedJa (docker.sock = root)Ja (--privileged)Kräver /dev/kvm + TAP-konfigurationJa (eller userns + mount)Nej (enda oprivilegierad process)
Fungerar i oprivilegierade K8s-pods / befintlig runscNejNejNej (ingen /dev/kvm)Misslyckas (EBUSY vid nästlad seccomp)Ja (automatisk futex-loop-reserv)
Stöder inre docker build och docker composeDelar värddaemonenJaJaMisslyckas utan värd-DinD-justeringJa (overlayfs på ext4lite)
Nätverksisolering per episodDelat docker0 / xtables-låsSeparat namnrymdSeparat VM-NICSeparat netnsIsolerad 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-VMKräver Linux-VMEndast Linux KVMEndast LinuxInbyggt på Windows, macOS och Linux

Vidare läsning i den här serien

  1. gvx: Kör Linux-containrar, Docker och Kubernetes i en enda oprivilegierad process: Komplett arkitekturgenomgång av gvx på Windows (whpx, systrap), macOS (hvf) och Linux (systrap).
  2. 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.
  3. Kör Docker-in-Docker inuti gVisor och nästlade sandlådor med gvx: Djupdykning i varför docker.io och nästlad runsc misslyckas inuti gVisor (runsc) och hur gvx faller tillbaka till futex-loopen _RUN_SYSCALL_LOOP vid EBUSY.
  4. Snabba isolerade Docker-miljöer för RL-rollouts av AI-agenter med gvx (detta inlägg)