GoSuda

Rýchle izolované Docker prostredia pre RL rollouty AI agentov s gvx

By Lemon Mint views ...

Problém bezpečnosti a infraštruktúry pri RL rolloutoch kódovacích agentov

Trénovanie a hodnotenie softvérových inžinierskych agentov naprieč benchmarkmi a frameworkmi pre reinforcement learning (RL), ako sú SWE-bench, SWE-ReX, R2E-Gym, SWE-Gym, Verl a OpenHands, vyžaduje spúšťanie tisícov súbežných rolloutov nedôveryhodného kódu generovaného LLM. V reálnych úlohách softvérového inžinierstva agent nielen upravuje textové súbory: spúšťa docker build, pomocou docker compose spúšťa databázy a mikroservisy, vykonáva integračné testovacie sady a kontroluje logy kontajnerov.

Pri rozsahu rolloutov (128 až 1 024 súbežných epizód na klaster) existujúca kontajnerová infraštruktúra narazí na štyri systémové problémy v oblasti bezpečnosti a výkonu:

  1. Zámková kolízia a únik stavu v hostiteľskom dockerd: Nasmerovanie stoviek súbežných RL workerov na jediný hostiteľský /var/run/docker.sock dáva každému agentovi prístup ekvivalentný rootovi k hostiteľskému démonu, serializuje extrakciu vrstiev obrazov, vyčerpá adresné priestory podsiete docker0, spôsobuje chyby Another app is currently holding the xtables lock v hostiteľskom iptables a necháva osirelé kontajnery naprieč epizódami.
  2. Riziko úniku z kontajnera pri privilegovanom DinD: Spúšťanie štandardných kontajnerov docker:dind vyžaduje --privileged (CAP_SYS_ADMIN), čo nechráneným RL rolloutom sprístupňuje surové hostiteľské blokové zariadenia, cgroup release agenty a kernelové moduly.
  3. Obmedzenia hardvérovej virtualizácie (/dev/kvm): MicroVM runtime (Firecracker, QEMU, Kata Containers) čisto izolujú vnútorné jadro, ale vyžadujú /dev/kvm. Štandardné Kubernetes pody bez vnorenej virtualizácie, bare-metal cloudové inštancie, kde je KVM vypnuté, a existujúce sandboxy gVisor (runsc) nemôžu spúšťať KVM microVM.
  4. Vysoká latencia resetu epizódy (env.reset()): Spustenie nového prostredia docker:dind a sťahovanie alebo rozbaľovanie základných obrazov pre každú RL epizódu trvá 10 až 60 sekúnd, čo necháva GPU trénovacie workery nečinné počas čakania na reset prostredia.

gvx (CLI rungv a Go SDK z.gfire.dev/gvx/pkg/gvx) poskytuje bezpečné, nenáročné a token-efektívne runtime prostredie pre rollouty, ktoré rieši všetky štyri problémy. Každé prostredie rolloutu beží ako jediný neprivilegovaný, samozamykajúci sa proces v používateľskom priestore (izolovaný Google gVisor jadrom Sentry, balíkom pkg/hostlockdown a netstack v pamäti), pričom celý stav /var/lib/docker sa nachádza v jedinom súbore .ext4 s copy-on-write (ext4lite).

Nenáročné a token-efektívne vykonávanie pre agentov: Ak docker build alebo docker compose funguje hneď po inštalácii v každej epizóde (alebo ho agent dokáže inicializovať v jedinom trojpríkazovom kroku pomocou rungv), RL rollouty neplytvajú tokenmi kontextového okna ani krokmi rozpočtu na riešenie zlyhaní service docker start, ulimit, chýb iptables-nft alebo odmietnutí runc pivot_root. Každý token v trajektórii sa venuje riešeniu skutočnej úlohy softvérového inžinierstva.

Bezpečnosť bez privilégií pre nedôveryhodné rollouty agentov: Každá epizóda gvx uzamkne vlastný hostiteľský OS proces skôr, než spustí hosťovský kód (PR_SET_NO_NEW_PRIVS + BPF seccomp na Linuxe, Seatbelt deny default na macOS, Restricted Token + Job Object na Windows). Aj keď RL agent v rámci svojej epizódy spustí docker run --privileged alebo deštruktívne príkazy rm -rf /, tieto operácie zostanú prísne obmedzené na jadro Sentry v používateľskom priestore danej epizódy a na jej dočasný súbor zväzku .ext4.


Architektúra rolloutu: paralelné neprivilegované inštancie + reflink snímky

V gvx je každý rollout worker bežným neprivilegovaným procesom operačného systému s vlastným jadrom gVisor Sentry, vlastným netstack v používateľskom priestore (10.0.2.15/24) a vlastným blokovým zväzkom ext4lite (ep-N.ext4), pričom zdieľa len na čítanie určenú cache obrazov OCI erofs adresovateľnú obsahom v page cache hostiteľa.

Architektúra paralelných RL rolloutov agentov s gvx

Prečo zväzky ext4lite v jednom súbore menia ekonomiku resetov RL

Keďže súborový systém ext4lite ukladá celý zväzok ext4 s žurnálom JBD2 (vrátane adresárov vrstiev overlayfs Dockeru, rozšírených atribútov trusted.overlay.*, whiteout zariadení a metadát obrazov) do jedného bežného súboru hostiteľa, správa životného cyklu epizódy sa stáva jedinou operáciou so súborom:

  1. 100-milisekundový env.reset() pomocou cp --reflink=always:
    • Súbor golden-docker.ext4 vytvoríte raz; obsahuje vopred stiahnuté základné obrazy (python:3.11, postgres:16, redis:7 alebo základné obrazy SWE-bench špecifické pre úlohu).
    • Na začiatku každej RL epizódy cp --reflink=always golden-docker.ext4 ep-0042.ext4 (na XFS, Btrfs, ZFS alebo macOS APFS) vytvorí nezávislý klon s copy-on-write za menej než 100 milisekúnd bez kopírovania podkladových diskových blokov.
    • Dodatočné miesto na disku spotrebujú iba bloky po 4 KiB, ktoré sa zmenili počas danej epizódy.
  2. Upratanie bez zvyškov (env.close()):
    • Ukončenie procesu rungv (sb.Close() v Go) a odstránenie odkazu na ep-0042.ext4 uvoľní 100 % procesov, sieťových soketov, pripojení a úložiska danej epizódy za približne 10 milisekúnd. Nič sa nikdy nepripojí do tabuľky pripojení hostiteľského jadra ani sa nezaregistruje v hostiteľských cgroup.

Postup krok za krokom: zlatá snímka .ext4 a rýchly reset epizódy

rungv môžete nainštalovať na každý uzol rollout workera buď stiahnutím prekompilovaného binárneho súboru s atestáciou SLSA z 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), alebo spustením CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest.

Krok 1: Vytvorte zlatý zväzok docker:dind iba raz

 1mkdir -p /srv/rl-snapshots /srv/rl-episodes
 2
 3# 1. Spusťte seed inštanciu docker:dind podloženú súborom 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. Predpripravte základné obrazy a cache zostavovania vnútri 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. Korektne zastavte seed inštanciu, aby ext4lite zapísal dáta a kompaktoval obraz
20kill -INT $SEED_PID
21wait $SEED_PID

Krok 2: Spustite paralelné RL epizódy za 100 milisekúnd

Každý paralelný rollout worker naklonuje golden-docker.ext4 pomocou reflinku a spustí izolovanú inštanciu docker:dind, v ktorej sú všetky obrazy už prítomné v /var/lib/docker:

 1EPISODE_ID="ep-0042"
 2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
 3
 4# Okamžitý klon s copy-on-write (~100 ms na XFS/Btrfs/APFS)
 5cp --reflink=always \
 6  /srv/rl-snapshots/golden-docker.ext4 \
 7  "/srv/rl-episodes/${EPISODE_ID}.ext4"
 8
 9# Spustite izolovanú neprivilegovanú epizódu so 4 GiB RAM a 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# Vnútri rollout workera sú obrazy okamžite dostupné bez sťahovania zo siete
22docker images
23
24# Ukončenie epizódy po výpočte odmeny (~10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"

Programovo riadený pool rolloutov pomocou Go SDK z.gfire.dev/gvx/pkg/gvx

Ak je váš RL environment server alebo evaluačný harness napísaný v Go (alebo volá rollout démona v Go cez gRPC), môžete paralelné kontajnerové epizódy spravovať priamo cez 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 klon zlatého zväzku 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. Nastavenie časového limitu epizódy podľa nástenných hodín
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 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}

Porovnanie runtime prostredí pre RL sandboxy agentov

Metrika / požiadavkaZdieľaný hostiteľský dockerdPrivilegovaný docker:dindFirecracker / KVM MicroVMŠtandardný gVisor (runsc)gvx (rungv / pkg/gvx)
Vyžaduje root na hostiteľovi alebo --privilegedÁno (docker.sock = root)Áno (--privileged)Potrebuje /dev/kvm + nastavenie TAPÁno (alebo userns + mount)Nie (jediný neprivilegovaný proces)
Funguje v neprivilegovaných K8s Podoch / existujúcom runscNieNieNie (bez /dev/kvm)Zlyhá (EBUSY pri vnorenom seccomp)Áno (automatický záložný futex loop)
Podporuje vnútorné docker build a docker composeZdieľa hostiteľský démonÁnoÁnoZlyhá bez ladenia hostiteľského DinDÁno (overlayfs na ext4lite)
Izolácia siete pre každú epizóduZdieľaný docker0 / zámok xtablesSamostatný namespaceSamostatná NIC VMSamostatný netnsIzolovaný netstack v procese
Latencia resetu teplej epizódy (env.reset())5 s až 30 s (prune kontajnerov/zväzkov)10 s až 60 s (studený démon)~150 ms (snímka VM)5 s až 20 s~100 ms (cp --reflink=always .ext4)
Lokálny vývoj na viacerých platformách (Windows / macOS / Linux)Vyžaduje Linux VMVyžaduje Linux VMIba Linux KVMIba LinuxNatívne na Windows, macOS a Linuxe

Ďalšie čítanie v tejto sérii

  1. gvx: Spúšťanie Linux kontajnerov, Dockeru a Kubernetes v jedinom neprivilegovanom procese: Kompletný architektonický prehľad gvx naprieč Windows (whpx, systrap), macOS (hvf) a Linuxom (systrap).
  2. Spúšťanie Dockeru, docker build a docker compose bez roota alebo používateľských namespace: Rýchly trojkrokový návod na inicializáciu a riešenie problémov, keď sú blokované sudo, unshare(CLONE_NEWUSER), mount(2) a /dev/fuse.
  3. Docker-in-Docker vnútri gVisor a vnorených sandboxov s gvx: Hĺbkový pohľad na to, prečo docker.io a vnorený runsc zlyhávajú vnútri gVisor (runsc) a ako gvx prechádza na futex loop _RUN_SYSCALL_LOOP pri EBUSY.
  4. Rýchle izolované Docker prostredia pre RL rollouty AI agentov s gvx (tento príspevok)