GoSuda

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

By Lemon Mint views ...

Flaskhalsen i säkerhet och infrastruktur för RL-rollouts med kodagenter

Träning och utvärdering av mjukvaruutvecklingsagenter över benchmarks och ramverk för förstärkningsinlärning (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 uppgifter inom mjukvaruutveckling 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 flaskhalsar för säkerhet och prestanda:

  1. Låskonkurrens i värdens dockerd och tillståndsläckage: När hundratals samtidiga RL-arbetare riktas mot en enda /var/run/docker.sock på värden får varje agent root-motsvarande åtkomst till värdens daemon, extraktionen av bildlager serialiseras, adressutrymmet i subnätet docker0 tar slut, felen Another app is currently holding the xtables lock utlöses i värdens iptables och föräldralösa containrar läcker mellan episoderna.
  2. Risk för containerrymning via privilegierad DinD: Att köra standardcontainrar med 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. Begränsningar för hårdvarubaserad virtualisering (/dev/kvm): MicroVM-runtimes (Firecracker, QEMU, Kata Containers) isolerar inre kärnor på ett rent sätt, men de kräver /dev/kvm. Standardpods i Kubernetes utan nästlad virtualisering, bare-metal-molninstanser där KVM är inaktiverat och befintliga gVisor (runsc)-sandlådor kan inte starta KVM-microVMs.
  4. Långsam latens vid episodåterställning (env.reset()): Att starta en ny docker:dind-miljö och hämta eller packa upp basbilder för varje RL-episod tar 10 till 60 sekunder, vilket lämnar GPU-träningsarbetare overksamma medan de väntar på miljöåterställningar.

gvx (rungv CLI och z.gfire.dev/gvx/pkg/gvx Go SDK) tillhandahåller en säker rollout-runtime med försvar på flera nivåer som löser alla fyra flaskhalsarna. Varje rollout-miljö körs som en enda oprivilegierad, självlåst process i användarutrymmet (isolerad av Googles gVisor-kärna Sentry, pkg/hostlockdown och ett minnesbaserat netstack) vars hela tillstånd i /var/lib/docker finns inuti en enda kopiera-vid-skrivning-fil .ext4 (ext4lite).

Nollprivilegiesäkerhet för ej betrodda agent-rollouts: Varje gvx-episod låser ned sin egen värdprocess i operativsystemet 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 episodens kärna i användarutrymmet (Sentry) och dess tillfälliga volymfil .ext4.


Rollout-arkitektur: Parallella oprivilegierade instanser + reflink-ögonblicksbilder

I gvx är varje rollout-arbetare en vanlig oprivilegierad OS-process med sin egen gVisor-kärna Sentry, sitt eget netstack i användarutrymmet (10.0.2.15/24) och sin egen blockvolym ext4lite (ep-N.ext4). Samtidigt delar arbetarna en skrivskyddad, innehållsadresserbar OCI-bildcache i formatet erofs över värdens sidcache.

Parallell RL-rollout-arkitektur för agenter med gvx

Varför volymer i form av enskilda ext4lite-filer förändrar ekonomin för RL-återställning

Eftersom ext4lite-filsystemet i gvx lagrar en hel JBD2-journalförd ext4-volym (inklusive Dockers lagerkataloger för overlayfs, utökade attribut trusted.overlay.*, whiteout-enhetsnoder och bildmetadata) i 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 basbilder (python:3.11, postgres:16, redis:7 eller uppgiftsspecifika basbilder för SWE-bench).
    • 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 klon med kopiera-vid-skrivning 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. Nedtagning utan kvarlämnade spår (env.close()):
    • Att avsluta processen rungv (sb.Close() i Go) och ta bort ep-0042.ext4 frigör 100 % av episodens processer, nätverkssockets, monteringar och lagring på ungefär 10 millisekunder. Ingenting monteras någonsin i värdkärnans monteringstabell eller registreras i värdens cgroups.

Steg-för-steg-arbetsflöde: Gyllene ögonblicksbild .ext4 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 volymen docker:dind en gång

 1mkdir -p /srv/rl-snapshots /srv/rl-episodes
 2
 3# 1. Starta en startinstans 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 basbilder 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 startinstansen på ett kontrollerat sätt så att ext4lite spolar och kompakterar avbildningen
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 bilder redan närvarande i /var/lib/docker:

 1EPISODE_ID="ep-0042"
 2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
 3
 4# Omedelbar klon med kopiera-vid-skrivning (~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 finns bilderna omedelbart tillgängliga utan nätverkshämtning
22docker images
23
24# Nedtagning av episoden efter beräkning av belöningen (~10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"

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

Om din RL-miljöserver eller utvärderingsharness är skriven i Go (eller anropar en Go-daemon för rollouts över 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 (väggklocka)
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 runtimes för sandlådor för RL-agenter

Mätvärde / 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 (en enda oprivilegierad process)
Fungerar i oprivilegierade K8s-pods / befintlig runscNejNejNej (ingen /dev/kvm)Misslyckas (EBUSY vid nästlad seccomp)Ja (automatisk reservlösning med futex-loop)
Stöder inre docker build och docker composeDelar värddaemonJaJaMisslyckas utan DinD-justering på värdenJa (overlayfs på ext4lite)
Nätverksisolering per episodDelat docker0 / xtables-låsSeparat namnrymdSeparat VM-nätverkskortSeparat netnsIsolerat netstack i processen
Latens för återställning av varm episod (env.reset())5 s till 30 s (beskärning av container/volym)10 s till 60 s (kall daemon)~150 ms (VM-ögonblicksbild)5 s till 20 s~100 ms (cp --reflink=always .ext4)
Plattformsoberoende lokal utveckling (Windows / macOS / Linux)Kräver Linux-VMKräver Linux-VMEndast Linux KVMEndast LinuxNative på Windows, macOS och Linux

Fler artiklar i denna serie

  1. gvx: Köra Linux-containrar, Docker och Kubernetes i en enda oprivilegierad process: Fullständig arkitekturgenomgång av gvx på Windows (whpx, systrap), macOS (hvf) och Linux (systrap).
  2. Köra Docker, docker build och docker compose utan root eller användarnamnrymder: Snabb tregegsuppstart och felsökningsguide när sudo, unshare(CLONE_NEWUSER), mount(2) och /dev/fuse är blockerade.
  3. Köra Docker-in-Docker inuti gVisor och nästlade sandlådor med gvx: Fördjupning i varför docker.io och nästlad runsc misslyckas inuti gVisor (runsc) och hur gvx faller tillbaka på futex-loopen _RUN_SYSCALL_LOOP vid EBUSY.
  4. Snabba isolerade Docker-miljöer för RL-rollouts med AI-agenter med gvx (detta inlägg)