GoSuda

Ambienti Docker isolati e rapidi per i rollout di RL degli agenti AI con gvx

By Lemon Mint views ...

Il collo di bottiglia di sicurezza e infrastruttura nei rollout RL degli agenti di programmazione

L'addestramento e la valutazione di agenti per l'ingegneria del software su benchmark e framework di reinforcement learning (RL) come SWE-bench, SWE-ReX, R2E-Gym, SWE-Gym, Verl e OpenHands richiedono l'esecuzione di migliaia di rollout concorrenti di codice non attendibile generato da LLM. Nei compiti reali di ingegneria del software, un agente non si limita a modificare file di testo: esegue docker build, avvia database e microservizi tramite docker compose, esegue suite di test di integrazione e ispeziona i log dei container.

Su scala di rollout (da 128 a 1.024 episodi concorrenti per cluster), l'infrastruttura di container esistente incontra quattro colli di bottiglia sistemici di sicurezza e prestazioni:

  1. Contesa di lock sul dockerd dell'host e perdita di stato: Indirizzare centinaia di worker RL concorrenti verso un singolo /var/run/docker.sock dell'host concede a ogni agente un accesso equivalente a root al daemon dell'host, serializza l'estrazione dei layer delle immagini, esaurisce le allocazioni della subnet di docker0, provoca errori Another app is currently holding the xtables lock in iptables dell'host e lascia container orfani tra un episodio e l'altro.
  2. Rischio di evasione dal container tramite DinD privilegiato: Eseguire container docker:dind standard richiede --privileged (CAP_SYS_ADMIN), che espone ai rollout RL non attendibili i block device grezzi dell'host, i release agent di cgroup e i moduli del kernel.
  3. Vincoli della virtualizzazione hardware (/dev/kvm): I runtime MicroVM (Firecracker, QEMU, Kata Containers) isolano in modo pulito i kernel interni, ma richiedono /dev/kvm. I pod Kubernetes standard senza virtualizzazione annidata, le istanze cloud bare-metal in cui KVM è disabilitato e le sandbox gVisor (runsc) esistenti non possono avviare microVM KVM.
  4. Latenza elevata del reset dell'episodio (env.reset()): Avviare un nuovo ambiente docker:dind e scaricare o decomprimere le immagini di base per ogni episodio RL richiede da 10 a 60 secondi, lasciando inattivi i worker di addestramento GPU in attesa del reset degli ambienti.

gvx (CLI rungv e SDK Go z.gfire.dev/gvx/pkg/gvx) fornisce un runtime di rollout sicuro e con difesa in profondità che risolve tutti e quattro i colli di bottiglia. Ogni ambiente di rollout viene eseguito come un unico processo user-space non privilegiato e auto-bloccato (isolato dal kernel Sentry di gVisor di Google, da pkg/hostlockdown e da un netstack in memoria) il cui intero stato di /var/lib/docker risiede all'interno di un unico file .ext4 copy-on-write (ext4lite).

Sicurezza a privilegi zero per i rollout di agenti non attendibili: Ogni episodio gvx blocca il proprio processo del sistema operativo host prima di eseguire codice guest (PR_SET_NO_NEW_PRIVS + seccomp BPF su Linux, Seatbelt deny default su macOS, Restricted Token + Job Object su Windows). Anche se un agente RL esegue docker run --privileged o comandi distruttivi rm -rf / all'interno del proprio episodio, tali operazioni restano rigorosamente confinate al kernel Sentry user-space di quell'episodio e al suo file volume .ext4 effimero.


Architettura di rollout: istanze parallele non privilegiate + snapshot reflink

In gvx, ogni worker di rollout è un normale processo del sistema operativo non privilegiato, con il proprio kernel gVisor Sentry, il proprio netstack user-space (10.0.2.15/24) e il proprio volume a blocchi ext4lite (ep-N.ext4), condividendo al tempo stesso una cache di immagini OCI erofs in sola lettura, indirizzata per contenuto, tramite la page cache dell'host.

Architettura di rollout RL parallela per agenti con gvx

Perché i volumi ext4lite a file singolo cambiano l'economia del reset RL

Poiché il filesystem ext4lite di gvx memorizza un intero volume ext4 con journal JBD2 (incluse le directory dei layer overlayfs di Docker, gli attributi estesi trusted.overlay.*, i nodi device whiteout e i metadati delle immagini) all'interno di un unico file regolare dell'host, la gestione del ciclo di vita dell'episodio diventa un'unica operazione su file:

  1. env.reset() in 100 millisecondi tramite cp --reflink=always:
    • Si costruisce una sola volta un file golden-docker.ext4 contenente le immagini di base preinstallate (python:3.11, postgres:16, redis:7 o immagini di base specifiche per SWE-bench).
    • All'inizio di ogni episodio RL, cp --reflink=always golden-docker.ext4 ep-0042.ext4 (su XFS, Btrfs, ZFS o macOS APFS) crea un clone Copy-on-Write indipendente in meno di 100 millisecondi, senza copiare i blocchi del disco sottostante.
    • Solo i blocchi da 4 KiB modificati durante quello specifico episodio occupano spazio aggiuntivo su disco.
  2. Smantellamento senza residui (env.close()):
    • Terminare il processo rungv (sb.Close() in Go) ed eliminare ep-0042.ext4 recupera il 100% dei processi, dei socket di rete, dei mount e dello storage dell'episodio in circa 10 millisecondi. Nulla viene mai montato nella mount table del kernel dell'host né registrato nei cgroup dell'host.

Flusso di lavoro passo passo: snapshot .ext4 golden e reset rapido dell'episodio

È possibile installare rungv su ciascun nodo worker di rollout scaricando il binario precompilato con attestazione SLSA dalle 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) oppure eseguendo CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest.

Passo 1: creare una sola volta il volume docker:dind golden

 1mkdir -p /srv/rl-snapshots /srv/rl-episodes
 2
 3# 1. Avvia un'istanza seed docker:dind basata su 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. Prepopola le immagini di base e la cache di build all'interno di 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. Arresta con garbo l'istanza seed affinché ext4lite svuoti e compatti l'immagine
20kill -INT $SEED_PID
21wait $SEED_PID

Passo 2: avviare episodi RL paralleli in 100 millisecondi

Ogni worker di rollout parallelo clona golden-docker.ext4 tramite reflink e avvia un'istanza docker:dind isolata, con tutte le immagini già presenti in /var/lib/docker:

 1EPISODE_ID="ep-0042"
 2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
 3
 4# Clone istantaneo Copy-on-Write (~100 ms su XFS/Btrfs/APFS)
 5cp --reflink=always \
 6  /srv/rl-snapshots/golden-docker.ext4 \
 7  "/srv/rl-episodes/${EPISODE_ID}.ext4"
 8
 9# Avvia un episodio isolato non privilegiato con 4 GiB di RAM e 2 CPU
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# Nel worker di rollout le immagini sono immediatamente disponibili, senza pull dalla rete
22docker images
23
24# Smantellamento dell'episodio dopo il calcolo della ricompensa (~10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"

Pool di rollout programmatico tramite l'SDK Go z.gfire.dev/gvx/pkg/gvx

Se il server dell'ambiente RL o l'harness di valutazione è scritto in Go (oppure chiama un daemon di rollout Go tramite gRPC), è possibile gestire episodi di container paralleli direttamente tramite 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. Clona tramite reflink il volume golden ext4lite
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. Applica il timeout di wall-clock per episodio
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, // limite di 4 GiB
55			CPUMillis:   2000,    // 2.0 CPU
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}

Confronto dei runtime sandbox per agenti RL

Metrica / Requisitodockerd host condivisodocker:dind privilegiatoFirecracker / KVM MicroVMgVisor standard (runsc)gvx (rungv / pkg/gvx)
root sull'host o --privileged richiestoSì (docker.sock = root)Sì (--privileged)Richiede /dev/kvm + setup TAPSì (o userns + mount)No (singolo processo non privilegiato)
Funziona in pod K8s non privilegiati / runsc esistentiNoNoNo (nessun /dev/kvm)Fallisce (EBUSY su seccomp annidato)Sì (fallback automatico al loop futex)
Supporta docker build e docker compose interniCondivide il daemon dell'hostSìSìFallisce senza tuning di DinD sull'hostSì (overlayfs su ext4lite)
Isolamento di rete per episodiodocker0 / lock xtables condivisiNamespace separatoNIC di VM separataNetns separatonetstack isolato nel processo
Latenza di reset dell'episodio a caldo (env.reset())5 s a 30 s (prune di container/volumi)10 s a 60 s (daemon a freddo)~150 ms (snapshot di VM)5 s a 20 s~100 ms (cp --reflink=always di .ext4)
Sviluppo locale multipiattaforma (Windows / macOS / Linux)Richiede VM LinuxRichiede VM LinuxSolo Linux KVMSolo LinuxNativo su Windows, macOS e Linux

Altri articoli di questa serie

  1. gvx: Esecuzione di container Linux, Docker e Kubernetes in un unico processo non privilegiato: Walkthrough architetturale completo di gvx su Windows (whpx, systrap), macOS (hvf) e Linux (systrap).
  2. Esecuzione di Docker, docker build e docker compose senza root né user namespace: Guida rapida di bootstrap in 3 passi e risoluzione dei problemi quando sudo, unshare(CLONE_NEWUSER), mount(2) e /dev/fuse sono bloccati.
  3. Esecuzione di Docker-in-Docker all'interno di gVisor e di sandbox annidate con gvx: Analisi dettagliata di perché docker.io e runsc annidato falliscono all'interno di gVisor (runsc) e di come gvx ricorre al loop futex _RUN_SYSCALL_LOOP in caso di EBUSY.
  4. Ambienti Docker isolati e veloci per i rollout RL degli agenti AI con gvx (questo articolo)