GoSuda

Raske isolerte Docker-miljøer for AI-agent RL-rollouts med gvx

By Lemon Mint views ...

Flaskehalsen for sikkerhet og infrastruktur i RL-rollouts for kodeagenter

Trening og evaluering av programvareagenter på tvers av benchmarks og forsterkende læringsrammeverk (RL) som SWE-bench, SWE-ReX, R2E-Gym, SWE-Gym, Verl og OpenHands krever kjøring av tusenvis av samtidige rollouts av ikke-betrodd, LLM-generert kode. I reelle programvareutviklingsoppgaver redigerer en agent ikke bare tekstfiler: den kjører docker build, starter databaser og mikrotjenester via docker compose, kjører integrasjonstestsuiter og inspiserer containerlogger.

Ved rollout-skala (128 til 1,024 samtidige episoder per klynge) treffer eksisterende containerinfrastruktur fire systemiske flaskehalser knyttet til sikkerhet og ytelse:

  1. Låsekonkurranse og tilstandslekkasje i vertens dockerd: Når hundrevis av samtidige RL-arbeidere peker mot en enkelt /var/run/docker.sock på verten, får hver agent tilgang tilsvarende root til vertsdemonen. Dette serialiserer utpakking av image-lag, tømmer subnettallokeringene til docker0, utløser feilmeldingen Another app is currently holding the xtables lock i vertens iptables, og etterlater foreldreløse containere på tvers av episoder.
  2. Risiko for containerrømming via privilegert DinD: Standard docker:dind-containere krever --privileged (CAP_SYS_ADMIN), noe som eksponerer rå blokkenheter på verten, cgroup-release-agenter og kjernemoduler for ikke-betrodde RL-rollouts.
  3. Begrensninger knyttet til maskinvarevirtualisering (/dev/kvm): MicroVM-kjøretider (Firecracker, QEMU, Kata Containers) isolerer indre kjerner på en ren måte, men de krever /dev/kvm. Standard Kubernetes-pods uten nøstet virtualisering, bare-metal-skyinstanser der KVM er deaktivert, og eksisterende gVisor-sandkasser (runsc) kan ikke starte KVM-microVM-er.
  4. Treg tilbakestilling av episoden (env.reset()): Å starte et nytt docker:dind-miljø og hente eller pakke ut basisimages for hver RL-episode tar 10 til 60 sekunder, og lar GPU-treningsarbeidere stå ledige mens de venter på at miljøene tilbakestilles.

gvx (rungv-CLI og z.gfire.dev/gvx/pkg/gvx Go SDK) gir en sikker, lavinnsats og tokeneffektiv rollout-kjøretid som løser alle fire flaskehalsene. Hvert rollout-miljø kjøres som en enkelt prosess i brukerrommet uten privilegier, selvlåst (isolert av Googles gVisor Sentry-kjerne, pkg/hostlockdown og et netstack i minnet), og hele /var/lib/docker-tilstanden ligger inne i en enkelt copy-on-write .ext4-fil (ext4lite).

Lavinnsats og tokeneffektiv kjøring for agenter: Når docker build eller docker compose fungerer rett fra start i hver episode (eller kan startes av agenten i én tur på tre kommandoer via rungv), bruker ikke RL-rollouts kontekstvindutokens eller stegbudsjett på feilsøking av service docker start-feil med ulimit, iptables-nft-feil eller avvisninger av runc pivot_root. Hvert token i trajektorien går til å løse den faktiske programvareutviklingsoppgaven.

Null privilegier for ikke-betrodde agent-rollouts: Hver gvx-episode låser sin egen vertsprosess før gjestekode kjøres (PR_SET_NO_NEW_PRIVS + BPF seccomp på Linux, Seatbelt deny default på macOS, Restricted Token + Job Object på Windows). Selv om en RL-agent kjører docker run --privileged eller destruktive rm -rf /-kommandoer inne i episoden, forblir disse operasjonene strengt begrenset til den episodens Sentry-kjerne i brukerrommet og dens midlertidige .ext4-volumfil.


Rollout-arkitektur: Parallelle instanser uten privilegier + reflink-snapshots

I gvx er hver rollout-arbeider en vanlig OS-prosess uten privilegier, med egen gVisor Sentry-kjerne, eget netstack i brukerrommet (10.0.2.15/24) og eget ext4lite-blokkvolum (ep-N.ext4). Alle arbeiderne deler en skrivebeskyttet, innholdsadressert erofs-cache for OCI-images via vertens sidecache.

Parallell arkitektur for RL-rollouts med agenter og gvx

Hvorfor enkeltfils ext4lite-volumer endrer økonomien i RL-tilbakestilling

Ettersom gvxs ext4lite-filsystem lagrer et helt JBD2-journalført ext4-volum (inkludert Dockers overlayfs-lagkataloger, trusted.overlay.*-utvidede attributter, whiteout-enhetsnoder og image-metadata) i én vanlig vertsfil, blir livssyklushåndtering av episoder en enkelt filoperasjon:

  1. 100 millisekunders env.reset() via cp --reflink=always:
    • Du bygger én gang en golden-docker.ext4-fil som inneholder forhåndshentede basisimages (python:3.11, postgres:16, redis:7, eller SWE-bench-basisimages spesifikke for oppgaven).
    • Ved starten av hver RL-episode lager cp --reflink=always golden-docker.ext4 ep-0042.ext4 (på XFS, Btrfs, ZFS eller macOS APFS) en uavhengig copy-on-write-klone på under 100 millisekunder, uten å kopiere underliggende disk-blokker.
    • Bare de 4 KiB-blokkene som endres under den spesifikke episoden, bruker ekstra diskplass.
  2. Rengjøring uten rester (env.close()):
    • Å avslutte rungv-prosessen (sb.Close() i Go) og fjerne lenken til ep-0042.ext4 frigjør 100 % av episodens prosesser, nettverkssokler, monteringer og lagring på ca. 10 millisekunder. Ingenting monteres noen gang i vertskjernens monteringstabell eller registreres i vertens cgroups.

Trinn-for-trinn-arbeidsflyt: Golden .ext4-snapshot og rask tilbakestilling av episoder

Du kan installere rungv på hver rollout-arbeidernode enten ved å laste ned den SLSA-attesterte forhåndsbygde binærfilen 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 å kjøre CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest.

Trinn 1: Opprett det golden docker:dind-volumet én gang

 1mkdir -p /srv/rl-snapshots /srv/rl-episodes
 2
 3# 1. Start en seed-instans av docker:dind støttet av 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. Forhåndsfyll basisimages og build-cache inne 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. Stopp seed-instansen på en kontrollert måte, slik at ext4lite tømmer og komprimerer imaget
20kill -INT $SEED_PID
21wait $SEED_PID

Trinn 2: Start parallelle RL-episoder på 100 millisekunder

Hver parallelle rollout-arbeider kloner golden-docker.ext4 via reflink og starter en isolert docker:dind-instans der alle images allerede finnes i /var/lib/docker:

 1EPISODE_ID="ep-0042"
 2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
 3
 4# Øyeblikkelig copy-on-write-klone (~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 isolert episode uten privilegier 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# Inne i rollout-arbeideren er images umiddelbart tilgjengelige uten nedlasting fra nettverket
22docker images
23
24# Avslutning av episoden etter beregning av belønning (~10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"

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

Hvis RL-miljøserveren eller evalueringsharnessen din er skrevet i Go (eller kaller en Go-rollout-daemon over gRPC), kan du styre parallelle containerepisoder direkte gjennom 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-klone det golden ext4lite-volumet
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. Bruk en tidsgrense for klokketid per 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-grense
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 av sandkjøretider for RL-agenter

Metrikk / kravDelt vert-dockerdPrivilegert docker:dindFirecracker / KVM MicroVMStandard gVisor (runsc)gvx (rungv / pkg/gvx)
Krever root på verten eller --privilegedJa (docker.sock = root)Ja (--privileged)Krever /dev/kvm + TAP-oppsettJa (eller userns + mount)Nei (én prosess uten privilegier)
Fungerer i uprivilegerte K8s-pods / eksisterende runscNeiNeiNei (ingen /dev/kvm)Feiler (EBUSY ved nøstet seccomp)Ja (automatisk fallback til futex-løkke)
Støtter indre docker build og docker composeDeler vertsdemonenJaJaFeiler uten vertsbasert DinD-justeringJa (overlayfs på ext4lite)
Nettverksisolasjon per episodeDelt docker0 / xtables-låsEgen navneområdeEgen VM-nettverkskortEgen netnsIsolert netstack i prosessen
Latens for tilbakestilling av varm episode (env.reset())5 s til 30 s (container/volum-opprydding)10 s til 60 s (kald demon)~150 ms (VM-snapshot)5 s til 20 s~100 ms (cp --reflink=always av .ext4)
Lokal utvikling på tvers av plattformer (Windows / macOS / Linux)Krever Linux-VMKrever Linux-VMKun Linux KVMKun LinuxInnebygd på Windows, macOS og Linux

Mer lesestoff i denne serien

  1. gvx: Kjøre Linux-containere, Docker og Kubernetes i én prosess uten privilegier: Fullstendig arkitekturgjennomgang av gvx på Windows (whpx, systrap), macOS (hvf) og Linux (systrap).
  2. Kjøre Docker, docker build og docker compose uten root eller brukernavnerom: Rask bootstrap i tre trinn og feilsøkingsveiledning når sudo, unshare(CLONE_NEWUSER), mount(2) og /dev/fuse er blokkert.
  3. Kjøre Docker-in-Docker inne i gVisor og nøstede sandkasser med gvx: Dypdykk i hvorfor docker.io og nøstet runsc feiler inne i gVisor (runsc), og hvordan gvx faller tilbake til _RUN_SYSCALL_LOOP-futex-løkken ved EBUSY.
  4. Raske isolerte Docker-miljøer for RL-rollouts av AI-agenter med gvx (dette innlegget)