GoSuda

Schnelle isolierte Docker-Umgebungen für AI-Agent-RL-Rollouts mit gvx

By Lemon Mint views ...

Der Sicherheits- und Infrastrukturengpass bei RL-Rollouts von Coding-Agenten

Das Training und die Evaluierung von Software-Engineering-Agenten über Benchmarks und Reinforcement-Learning-(RL)-Frameworks wie SWE-bench, SWE-ReX, R2E-Gym, SWE-Gym, Verl und OpenHands erfordern die Ausführung Tausender gleichzeitiger Rollouts von nicht vertrauenswürdigem, durch LLMs generiertem Code. Bei realen Software-Engineering-Aufgaben bearbeitet ein Agent nicht lediglich Textdateien: Er führt docker build aus, startet Datenbanken und Microservices über docker compose, führt Integrationstestsuiten aus und untersucht Container-Logs.

Im Rollout-Maßstab (128 bis 1.024 gleichzeitige Episoden pro Cluster) stößt die bestehende Container-Infrastruktur auf vier systemische Sicherheits- und Leistungsengpässe:

  1. Lock-Konkurrenz und Zustandslecks des Host-dockerd: Wenn Hunderte gleichzeitige RL-Worker auf einen einzigen Host-/var/run/docker.sock verweisen, erhält jeder Agent Root-äquivalenten Zugriff auf den Host-Daemon, die Extraktion von Image-Layern wird serialisiert, die Subnetz-Zuweisungen von docker0 erschöpfen sich, es treten Fehler vom Typ Another app is currently holding the xtables lock in der Host-iptables auf, und es entstehen verwaiste Container über Episoden hinweg.
  2. Ausbruchsrisiko aus privilegierten DinD-Containern: Die Ausführung standardmäßiger docker:dind-Container erfordert --privileged (CAP_SYS_ADMIN), wodurch nicht vertrauenswürdige RL-Rollouts Zugriff auf rohe Host-Blockgeräte, cgroup-Release-Agents und Kernelmodule erhalten.
  3. Einschränkungen der Hardwarevirtualisierung (/dev/kvm): MicroVM-Laufzeitumgebungen (Firecracker, QEMU, Kata Containers) isolieren den inneren Kernel sauber, benötigen jedoch /dev/kvm. Standard-Kubernetes-Pods ohne verschachtelte Virtualisierung, Bare-Metal-Cloud-Instanzen, auf denen KVM deaktiviert ist, und bestehende gVisor-(runsc)-Sandboxes können keine KVM-MicroVMs starten.
  4. Hohe Latenz beim Zurücksetzen der Episode (env.reset()): Das Starten einer frischen docker:dind-Umgebung sowie das Abrufen oder Entpacken von Basis-Images für jede RL-Episode dauert 10 bis 60 Sekunden, wodurch GPU-Trainingsworker während der Umgebungsrücksetzungen untätig bleiben.

gvx (rungv-CLI und z.gfire.dev/gvx/pkg/gvx-Go-SDK) stellt eine sichere, aufwandsarme und token-effiziente Rollout-Laufzeitumgebung bereit, die alle vier Engpässe behebt. Jede Rollout-Umgebung läuft als einzelner, unprivilegierter, selbst gesperrter Benutzerraum-Prozess (isoliert durch den gVisor-Sentry-Kernel von Google, pkg/hostlockdown und einen im Speicher gehaltenen netstack), dessen gesamter /var/lib/docker-Zustand sich in einer einzigen Copy-on-Write-.ext4-Datei (ext4lite) befindet.

Aufwandsarme, token-effiziente Ausführung für Agenten: Wenn docker build oder docker compose in jeder Episode sofort funktioniert oder vom Agenten in einem einzigen 3-Befehle-Zug über rungv bootstrapped werden kann, verschwenden RL-Rollouts keine Kontextfenster-Token oder Schrittbudgets an die Fehlersuche bei service docker start-Fehlern durch ulimit, iptables-nft-Fehlern oder runc-pivot_root-Ablehnungen. Jedes Token in der Trajektorie fließt in die Lösung der eigentlichen Software-Engineering-Aufgabe.

Null-Privilegien-Sicherheit für nicht vertrauenswürdige Agenten-Rollouts: Jede gvx-Episode sperrt ihren eigenen Host-Betriebssystemprozess ab, bevor sie Gast-Code ausführt (PR_SET_NO_NEW_PRIVS + BPF-seccomp unter Linux, Seatbelt deny default unter macOS, Restricted Token + Job Object unter Windows). Selbst wenn ein RL-Agent innerhalb seiner Episode docker run --privileged oder destruktive rm -rf /-Befehle ausführt, bleiben diese Operationen strikt auf den Benutzerraum-Sentry-Kernel und die flüchtige .ext4-Volume-Datei dieser Episode beschränkt.


Rollout-Architektur: Parallele unprivilegierte Instanzen + Reflink-Snapshots

In gvx ist jeder Rollout-Worker ein gewöhnlicher, unprivilegierter OS-Prozess mit eigenem gVisor-Sentry-Kernel, eigenem Benutzerraum-netstack (10.0.2.15/24) und eigenem ext4lite-Blockvolume (ep-N.ext4), wobei alle Worker einen schreibgeschützten, inhaltsadressierbaren erofs-OCI-Image-Cache über den Host-Page-Cache teilen.

Parallele Agenten-RL-Rollout-Architektur mit gvx

Warum einzelne ext4lite-Volumes die Wirtschaftlichkeit des RL-Resets verändern

Da das ext4lite-Dateisystem ein vollständiges, mit JBD2 journalisiertes ext4-Volume (einschließlich der overlayfs-Layer-Verzeichnisse von Docker, der erweiterten trusted.overlay.*-Attribute, Whiteout-Gerätknoten und Image-Metadaten) in einer einzigen regulären Host-Datei speichert, wird die Verwaltung des Episodenlebenszyklus zu einer einzigen Dateioperation:

  1. 100-Millisekunden-env.reset() über cp --reflink=always:
    • Sie erstellen einmalig eine golden-docker.ext4-Datei, die vorab gezogene Basis-Images enthält (python:3.11, postgres:16, redis:7 oder aufgabenspezifische SWE-bench-Basis-Images).
    • Zu Beginn jeder RL-Episode erzeugt cp --reflink=always golden-docker.ext4 ep-0042.ext4 (auf XFS, Btrfs, ZFS oder macOS APFS) einen unabhängigen Copy-on-Write-Klon in weniger als 100 Millisekunden, ohne die zugrunde liegenden Plattenblöcke zu kopieren.
    • Nur die 4-KiB-Blöcke, die während dieser spezifischen Episode verändert werden, belegen zusätzlichen Speicherplatz.
  2. Rückstandsfreier Abbau (env.close()):
    • Das Beenden des rungv-Prozesses (sb.Close() in Go) und das Entfernen von ep-0042.ext4 gibt sämtliche Prozesse, Netzwerk-Sockets, Mounts und Speicher der Episode in etwa 10 Millisekunden frei. Im Mount-Table des Host-Kernels wird nichts eingehängt, und in den Host-cgroups wird nichts registriert.

Schritt-für-Schritt-Workflow: Golden-.ext4-Snapshot und schnelles Episoden-Reset

Sie können rungv auf jedem Rollout-Worker-Knoten entweder durch Herunterladen der SLSA-attestierten, vorkompilierten Binärdatei aus 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) oder durch Ausführen von CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest installieren.

Schritt 1: Golden-docker:dind-Volume einmalig erstellen

 1mkdir -p /srv/rl-snapshots /srv/rl-episodes
 2
 3# 1. Eine Seed-docker:dind-Instanz starten, die von golden-docker.ext4 gestützt wird
 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. Basis-Images und Build-Cache innerhalb von golden-docker.ext4 vorab befüllen
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. Die Seed-Instanz geordnet beenden, damit ext4lite das Image schreibt und kompaktiert
20kill -INT $SEED_PID
21wait $SEED_PID

Schritt 2: Parallele RL-Episoden in 100 Millisekunden starten

Jeder parallele Rollout-Worker klont golden-docker.ext4 per Reflink und startet eine isolierte docker:dind-Instanz, deren Images bereits in /var/lib/docker vorhanden sind:

 1EPISODE_ID="ep-0042"
 2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
 3
 4# Sofortiger Copy-on-Write-Klon (~100 ms auf XFS/Btrfs/APFS)
 5cp --reflink=always \
 6  /srv/rl-snapshots/golden-docker.ext4 \
 7  "/srv/rl-episodes/${EPISODE_ID}.ext4"
 8
 9# Isolierte, unprivilegierte Episode mit 4 GiB RAM und 2 CPUs starten
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# Innerhalb des Rollout-Workers stehen die Images sofort ohne Netzwerk-Pull zur Verfügung
22docker images
23
24# Abbau der Episode nach der Reward-Berechnung (~10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"

Programmatischer Rollout-Pool mit dem Go-SDK z.gfire.dev/gvx/pkg/gvx

Wenn Ihr RL-Umgebungsserver oder Ihr Evaluierungs-Harness in Go geschrieben ist (oder einen Go-Rollout-Daemon über gRPC aufruft), können Sie parallele Container-Episoden direkt über z.gfire.dev/gvx/pkg/gvx verwalten:

 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. Golden-ext4lite-Volume per Reflink klonen
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. Zeitlimit der Episode auf Wanduhrzeit anwenden
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, // Obergrenze 4 GiB
55			CPUMillis:   2000,    // 2,0 CPUs
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}

Vergleich von Laufzeitumgebungen für RL-Sandboxes von Agenten

Metrik / AnforderungGeteilter Host-dockerdPrivilegiertes docker:dindFirecracker / KVM-MicroVMStandard-gVisor (runsc)gvx (rungv / pkg/gvx)
Host-root oder --privileged erforderlichJa (docker.sock = root)Ja (--privileged)Benötigt /dev/kvm + TAP-EinrichtungJa (oder userns + mount)Nein (einzelner unprivilegierter Prozess)
Funktioniert in unprivilegierten K8s-Pods / bestehendem runscNeinNeinNein (kein /dev/kvm)Schlägt fehl (EBUSY bei verschachteltem seccomp)Ja (automatischer futex-Loop-Fallback)
Unterstützt inneres docker build & docker composeTeilt den Host-DaemonJaJaSchlägt ohne Host-DinD-Tuning fehlJa (overlayfs auf ext4lite)
Netzwerkisolation pro EpisodeGeteilter docker0 / xtables-LockEigener NamespaceEigene VM-NICEigener netnsIsoliertes In-Process-netstack
Latenz beim Warm-Reset der Episode (env.reset())5 s bis 30 s (Container-/Volume-Bereinigung)10 s bis 60 s (kalter Daemon)~150 ms (VM-Snapshot)5 s bis 20 s~100 ms (cp --reflink=always .ext4)
Plattformübergreifende lokale Entwicklung (Windows / macOS / Linux)Erfordert eine Linux-VMErfordert eine Linux-VMNur Linux mit KVMNur LinuxNativ auf Windows, macOS und Linux

Weiterführende Lektüre in dieser Reihe

  1. gvx: Linux-Container, Docker und Kubernetes in einem einzigen unprivilegierten Prozess ausführen: Vollständiger architektonischer Überblick über gvx unter Windows (whpx, systrap), macOS (hvf) und Linux (systrap).
  2. Docker, docker build und docker compose ohne Root oder User Namespaces ausführen: Kurze 3-Schritte-Bootstrap- und Fehlerbehebungsanleitung, wenn sudo, unshare(CLONE_NEWUSER), mount(2) und /dev/fuse blockiert sind.
  3. Docker-in-Docker innerhalb von gVisor und verschachtelten Sandboxes mit gvx ausführen: Vertiefte Analyse, warum docker.io und verschachteltes runsc innerhalb von gVisor (runsc) fehlschlagen und wie gvx bei EBUSY auf die _RUN_SYSCALL_LOOP-futex-Schleife zurückfällt.
  4. Schnelle isolierte Docker-Umgebungen für RL-Rollouts von KI-Agenten mit gvx (dieser Beitrag)