Medii Docker izolate și rapide pentru Rollouts RL ale agenților AI cu gvx

Blocajul de securitate și infrastructură în rollout-urile RL ale agenților de programare
Antrenarea și evaluarea agenților de inginerie software pe benchmark-uri și framework-uri de reinforcement learning (RL), precum SWE-bench, SWE-ReX, R2E-Gym, SWE-Gym, Verl și OpenHands, necesită executarea a mii de rollout-uri concurente de cod nesigur, generat de LLM-uri. În sarcinile reale de inginerie software, un agent nu se mulțumește să editeze fișiere text: el rulează docker build, pornește baze de date și microservicii prin docker compose, execută suite de teste de integrare și inspectează jurnalele containerelor.
La scară de rollout (128 până la 1.024 de episoade concurente per cluster), infrastructura de containere existentă se lovește de patru blocaje sistemice de securitate și performanță:
- Contencia de blocare a
dockerdde pe gazdă și scurgerea de stare: Îndreptarea a sute de workeri RL concurenți către un singur/var/run/docker.sockde pe gazdă oferă fiecărui agent acces echivalent cu cel de root la daemonul gazdei, serializează extragerea straturilor de imagini, epuizează alocările de subrețea aledocker0, declanșează eroriAnother app is currently holding the xtables lockîniptables-ul gazdei și lasă containere orfane între episoade. - Riscul de evadare din containerele DinD privilegiate: Rularea containerelor standard
docker:dindnecesită--privileged(CAP_SYS_ADMIN), ceea ce expune dispozitivele de bloc brute ale gazdei, agenții de eliberare cgroup și modulele kernel în rollout-urile RL nesigure. - Constrângerile virtualizării hardware (
/dev/kvm): Runtime-urile MicroVM (Firecracker,QEMU,Kata Containers) izolează curat kernelurile interne, dar necesită/dev/kvm. Pod-urile Kubernetes standard fără virtualizare imbricată, instanțele cloud bare-metal unde KVM este dezactivat și sandbox-urilegVisor(runsc) existente nu pot lansa microVM-uri KVM. - Latența lentă a resetării episodului (
env.reset()): Pornirea unui mediudocker:dindnou și extragerea sau dezarhivarea imaginilor de bază pentru fiecare episod RL durează între 10 și 60 de secunde, lăsând workerii de antrenare GPU inactivi în timp ce așteaptă resetarea mediilor.
gvx (CLI-ul rungv și SDK-ul Go z.gfire.dev/gvx/pkg/gvx) oferă un runtime de rollout sigur, cu apărare în profunzime, care rezolvă toate cele patru blocaje. Fiecare mediu de rollout rulează ca un singur proces în spațiul utilizatorului, fără privilegii și auto-blocat (izolat de kernelul Sentry al gVisor de la Google, de pkg/hostlockdown și de un netstack în memorie), a cărui stare întreagă /var/lib/docker se află într-un singur fișier .ext4 copy-on-write (ext4lite).
Securitate fără privilegii pentru rollout-urile nesigure ale agenților: Fiecare episod
gvxîși blochează propriul proces al sistemului de operare gazdă înainte de a rula codul oaspete (PR_SET_NO_NEW_PRIVS+seccompBPF pe Linux,deny defaultSeatbelt pe macOS, Restricted Token + Job Object pe Windows). Chiar dacă un agent RL executădocker run --privilegedsau comenzi distructiverm -rf /în interiorul episodului său, aceste operațiuni rămân strict confinate la kernelul Sentry în spațiul utilizatorului al episodului respectiv și la fișierul de volum.ext4efemer.
Arhitectura rollout-ului: instanțe paralele fără privilegii + snapshot-uri reflink
În gvx, fiecare worker de rollout este un proces obișnuit de sistem de operare, fără privilegii, cu propriul kernel gVisor Sentry, propriul netstack în spațiul utilizatorului (10.0.2.15/24) și propriul volum de bloc ext4lite (ep-N.ext4), iar toți partajează un cache de imagini OCI erofs, read-only și adresat prin conținut, prin page cache-ul gazdei.

De ce volumele ext4lite dintr-un singur fișier schimbă economia resetării RL
Deoarece sistemul de fișiere ext4lite al gvx stochează un volum ext4 complet cu jurnal JBD2 (inclusiv directoarele straturilor overlayfs ale Docker, atributele extinse trusted.overlay.*, nodurile de dispozitiv whiteout și metadatele imaginilor) într-un singur fișier obișnuit de pe gazdă, gestionarea ciclului de viață al episodului devine o singură operație pe fișier:
env.reset()de 100 de milisecunde princp --reflink=always:- Construiești o dată fișierul
golden-docker.ext4, care conține imaginile de bază preîncărcate (python:3.11,postgres:16,redis:7sau imaginile de bază specifice sarcinilorSWE-bench). - La începutul fiecărui episod RL,
cp --reflink=always golden-docker.ext4 ep-0042.ext4(pe XFS, Btrfs, ZFS sau macOS APFS) creează în sub 100 de milisecunde o clonă copy-on-write independentă, fără a copia blocurile de disc de bază. - Doar blocurile de 4 KiB modificate în timpul acelui episod specific consumă spațiu suplimentar pe disc.
- Construiești o dată fișierul
- Dezafectare fără reziduuri (
env.close()):- Terminarea procesului
rungv(sb.Close()în Go) și ștergerea legăturiiep-0042.ext4recuperează 100% din procesele, socket-urile de rețea, montările și stocarea episodului în ~10 milisecunde. Nimic nu este montat niciodată în tabela de montări a kernelului gazdă și nu este înregistrat în cgroup-urile gazdei.
- Terminarea procesului
Flux de lucru pas cu pas: snapshot .ext4 golden și resetare rapidă a episodului
Poți instala rungv pe fiecare nod worker de rollout fie descărcând binarul prebuilt atestat SLSA din 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), fie rulând CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest.
Pasul 1: Creează volumul golden docker:dind o singură dată
1mkdir -p /srv/rl-snapshots /srv/rl-episodes
2
3# 1. Pornește o instanță seed docker:dind susținută de 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. Preîncarcă imaginile de bază și cache-ul de build în 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. Oprește elegant instanța seed, astfel încât ext4lite să scrie datele pe disc și să compacteze imaginea
20kill -INT $SEED_PID
21wait $SEED_PID
Pasul 2: Lansează episoadele RL paralele în 100 de milisecunde
Fiecare worker paralel de rollout clonează golden-docker.ext4 prin reflink și pornește o instanță docker:dind izolată, cu toate imaginile deja prezente în /var/lib/docker:
1EPISODE_ID="ep-0042"
2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
3
4# Clonă Copy-on-Write instantanee (~100 ms pe XFS/Btrfs/APFS)
5cp --reflink=always \
6 /srv/rl-snapshots/golden-docker.ext4 \
7 "/srv/rl-episodes/${EPISODE_ID}.ext4"
8
9# Lansează un episod izolat, fără privilegii, cu 4 GiB RAM și 2 CPU-uri
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# În interiorul worker-ului de rollout, imaginile sunt disponibile imediat, fără pull din rețea
22docker images
23
24# Dezafectarea episodului după calcularea recompensei (~10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"
Pool programatic de rollout-uri folosind SDK-ul Go z.gfire.dev/gvx/pkg/gvx
Dacă serverul de mediu RL sau harness-ul de evaluare este scris în Go (sau apelează un daemon de rollout în Go prin gRPC), poți gestiona episoade de containere paralele direct prin 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. Clonează prin reflink volumul ext4lite golden
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. Aplică un timeout de ceas (wall-clock) per episod
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, // Limită de 4 GiB
55 CPUMillis: 2000, // 2.0 CPU-uri
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}
Comparație a runtime-urilor de sandbox pentru agenți RL
| Metrică / Cerință | dockerd partajat de pe gazdă | docker:dind privilegiat | Firecracker / KVM MicroVM | gVisor standard (runsc) | gvx (rungv / pkg/gvx) |
|---|---|---|---|---|---|
Necesită root pe gazdă sau --privileged | Da (docker.sock = root) | Da (--privileged) | Necesită /dev/kvm + configurare TAP | Da (sau userns + mount) | Nu (un singur proces fără privilegii) |
Funcționează în Pod-uri K8s fără privilegii / runsc existent | Nu | Nu | Nu (fără /dev/kvm) | Eșuează (EBUSY la seccomp imbricat) | Da (fallback automat pe bucla futex) |
Suportă docker build și docker compose în interior | Partajează daemonul gazdei | Da | Da | Eșuează fără reglaj DinD pe gazdă | Da (overlayfs pe ext4lite) |
| Izolarea rețelei per episod | docker0 / lock xtables partajat | Namespace separat | NIC de VM separat | Netns separat | Izolat într-un netstack din proces |
Latența resetării episodului (env.reset()) la cald | 5 s până la 30 s (curățarea containerelor/volumelor) | 10 s până la 60 s (daemon rece) | ~150 ms (snapshot de VM) | 5 s până la 20 s | ~100 ms (cp --reflink=always .ext4) |
| Dezvoltare locală multiplatformă (Windows / macOS / Linux) | Necesită VM Linux | Necesită VM Linux | Doar Linux KVM | Doar Linux | Nativ pe Windows, macOS și Linux |
Lecturi suplimentare din această serie
- gvx: Rularea containerelor Linux, Docker și Kubernetes într-un singur proces fără privilegii: Prezentare arhitecturală completă a
gvxpe Windows (whpx,systrap), macOS (hvf) și Linux (systrap). - Rularea Docker, docker build și docker compose fără root sau user namespaces: Ghid rapid de inițializare în 3 pași și de depanare, atunci când
sudo,unshare(CLONE_NEWUSER),mount(2)și/dev/fusesunt blocate. - Rularea Docker-in-Docker în interiorul gVisor și al sandbox-urilor imbricate cu gvx: Analiză detaliată a motivelor pentru care
docker.ioșirunscimbricat eșuează în interiorulgVisor(runsc) și a modului în caregvxtrece pe bucla de syscall-uri futex_RUN_SYSCALL_LOOPlaEBUSY. - Medii Docker izolate și rapide pentru rollout-urile RL ale agenților AI cu gvx (acest articol)