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

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:
- Contesa di lock sul
dockerddell'host e perdita di stato: Indirizzare centinaia di worker RL concorrenti verso un singolo/var/run/docker.sockdell'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 didocker0, provoca erroriAnother app is currently holding the xtables lockiniptablesdell'host e lascia container orfani tra un episodio e l'altro. - Rischio di evasione dal container tramite DinD privilegiato: Eseguire container
docker:dindstandard 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. - 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 sandboxgVisor(runsc) esistenti non possono avviare microVM KVM. - Latenza elevata del reset dell'episodio (
env.reset()): Avviare un nuovo ambientedocker:dinde 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
gvxblocca il proprio processo del sistema operativo host prima di eseguire codice guest (PR_SET_NO_NEW_PRIVS+seccompBPF su Linux, Seatbeltdeny defaultsu macOS, Restricted Token + Job Object su Windows). Anche se un agente RL eseguedocker run --privilegedo comandi distruttivirm -rf /all'interno del proprio episodio, tali operazioni restano rigorosamente confinate al kernelSentryuser-space di quell'episodio e al suo file volume.ext4effimero.
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.

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:
env.reset()in 100 millisecondi tramitecp --reflink=always:- Si costruisce una sola volta un file
golden-docker.ext4contenente le immagini di base preinstallate (python:3.11,postgres:16,redis:7o immagini di base specifiche perSWE-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.
- Si costruisce una sola volta un file
- Smantellamento senza residui (
env.close()):- Terminare il processo
rungv(sb.Close()in Go) ed eliminareep-0042.ext4recupera 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.
- Terminare il processo
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 / Requisito | dockerd host condiviso | docker:dind privilegiato | Firecracker / KVM MicroVM | gVisor standard (runsc) | gvx (rungv / pkg/gvx) |
|---|---|---|---|---|---|
root sull'host o --privileged richiesto | Sì (docker.sock = root) | Sì (--privileged) | Richiede /dev/kvm + setup TAP | Sì (o userns + mount) | No (singolo processo non privilegiato) |
Funziona in pod K8s non privilegiati / runsc esistenti | No | No | No (nessun /dev/kvm) | Fallisce (EBUSY su seccomp annidato) | Sì (fallback automatico al loop futex) |
Supporta docker build e docker compose interni | Condivide il daemon dell'host | Sì | Sì | Fallisce senza tuning di DinD sull'host | Sì (overlayfs su ext4lite) |
| Isolamento di rete per episodio | docker0 / lock xtables condivisi | Namespace separato | NIC di VM separata | Netns separato | netstack 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 Linux | Richiede VM Linux | Solo Linux KVM | Solo Linux | Nativo su Windows, macOS e Linux |
Altri articoli di questa serie
- gvx: Esecuzione di container Linux, Docker e Kubernetes in un unico processo non privilegiato: Walkthrough architetturale completo di
gvxsu Windows (whpx,systrap), macOS (hvf) e Linux (systrap). - 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/fusesono bloccati. - Esecuzione di Docker-in-Docker all'interno di gVisor e di sandbox annidate con gvx: Analisi dettagliata di perché
docker.ioerunscannidato falliscono all'interno digVisor(runsc) e di comegvxricorre al loop futex_RUN_SYSCALL_LOOPin caso diEBUSY. - Ambienti Docker isolati e veloci per i rollout RL degli agenti AI con gvx (questo articolo)