GoSuda

Rychlá izolovaná prostředí Docker pro rollouty RL agentů umělé inteligence s gvx

By Lemon Mint views ...

Bezpečnostní a infrastrukturní úzké hrdlo v rolloutech RL pro kódující agenty

Trénování a vyhodnocování softwarových inženýrských agentů napříč benchmarky a frameworky pro posilované učení (reinforcement learning, RL), jako jsou SWE-bench, SWE-ReX, R2E-Gym, SWE-Gym, Verl a OpenHands, vyžaduje spouštění tisíců souběžných rolloutů nedůvěryhodného kódu generovaného LLM. V reálných softwarových inženýrských úlohách agent nejen upravuje textové soubory: spouští docker build, zprovozňuje databáze a mikroslužby pomocí docker compose, provádí sady integračních testů a kontroluje logy kontejnerů.

Na úrovni rolloutů v rozsahu (128 až 1 024 souběžných epizod na jeden cluster) narází stávající kontejnerová infrastruktura na čtyři systémová bezpečnostní a výkonnostní úzká hrdla:

  1. Zamykání a únik stavu u hostitelského dockerd: Nasměrování stovek souběžných RL workerů na jediný hostitelský /var/run/docker.sock dává každému agentovi přístup ekvivalentní rootovi k hostitelskému démonu, serializuje extrakci vrstev obrazů, vyčerpává adresy podsítě docker0, vyvolává chyby Another app is currently holding the xtables lock v hostitelském iptables a zanechává osiřelé kontejnery napříč epizodami.
  2. Riziko úniku z kontejneru u privilegovaného DinD: Spouštění standardních kontejnerů docker:dind vyžaduje --privileged (CAP_SYS_ADMIN), což nedůvěryhodným RL rolloutům zpřístupňuje surová hostitelská bloková zařízení, agenty cgroup release a kernelové moduly.
  3. Omezení hardwarové virtualizace (/dev/kvm): MicroVM runtimes (Firecracker, QEMU, Kata Containers) čistě izolují vnitřní kernely, ale vyžadují /dev/kvm. Standardní Kubernetes pody bez vnořené virtualizace, bare-metal cloudové instance, na kterých je KVM vypnuto, a stávající sandboxy gVisor (runsc) nemohou spouštět KVM microVM.
  4. Pomalý reset epizody (env.reset()): Spuštění nového prostředí docker:dind a stahování nebo rozbalování základních obrazů pro každou RL epizodu trvá 10 až 60 sekund, takže GPU trénovací workery zůstávají nečinné a čekají na reset prostředí.

gvx (CLI rungv a Go SDK z.gfire.dev/gvx/pkg/gvx) poskytuje bezpečné, nenáročné a na tokeny úsporné runtime pro rollouty, které řeší všechna čtyři úzká hrdla. Každé prostředí rolloutu běží jako jediný neprivilegovaný, samozamykající se uživatelský proces (izolovaný jádrem Sentry z gVisor od Googlu, balíčkem pkg/hostlockdown a netstackem v paměti), jehož celý stav /var/lib/docker je uložen v jediném souboru .ext4 s copy-on-write (ext4lite).

Nenáročné, na tokeny úsporné provádění pro agenty: Pokud docker build nebo docker compose funguje po vybalení v každé epizodě (nebo si je agent dokáže inicializovat v jediném tříkrokovém tahu pomocí rungv), RL rollouty neplýtvají tokeny kontextového okna ani rozpočtem kroků na odstraňování potíží s selháními service docker start, ulimit, iptables-nft nebo odmítnutími runc pivot_root. Každý token v trajektorii slouží k řešení skutečné softwarové inženýrské úlohy.

Bezprivilegiová bezpečnost pro nedůvěryhodné rollouty agentů: Každá epizoda gvx uzamkne svůj vlastní hostitelský OS proces dříve, než spustí hostující kód (PR_SET_NO_NEW_PRIVS + BPF seccomp na Linuxu, Seatbelt deny default na macOS, Restricted Token + Job Object na Windows). I kdyby RL agent uvnitř své epizody spustil docker run --privileged nebo destruktivní příkazy rm -rf /, tyto operace zůstávají přísně omezeny na uživatelské jádro Sentry dané epizody a její dočasný svazkový soubor .ext4.


Architektura rolloutu: paralelní neprivilegované instance + reflink snímky

V gvx je každý rollout worker běžným neprivilegovaným OS procesem se svým vlastním jádrem gVisor Sentry, vlastním uživatelským netstackem (10.0.2.15/24) a vlastním blokovým svazkem ext4lite (ep-N.ext4), přičemž sdílí pouze pro čtení určenou, obsahově adresovatelnou cache obrazů erofs OCI v rámci stránkové cache hostitele.

Architektura paralelního rolloutu RL agentů s gvx

Proč jednosouborové svazky ext4lite mění ekonomiku resetů RL

Protože souborový systém ext4lite ukládá celý svazek ext4 s žurnálem JBD2 (včetně adresářů vrstev overlayfs Dockeru, rozšířených atributů trusted.overlay.*, whiteout device nodes a metadat obrazů) do jediného běžného souboru na hostiteli, správa životního cyklu epizody se stává jedinou operací nad souborem:

  1. Reset env.reset() za 100 milisekund pomocí cp --reflink=always:
    • Jednou vytvoříte soubor golden-docker.ext4 obsahující předem stažené základní obrazy (python:3.11, postgres:16, redis:7 nebo základní obrazy SWE-bench specifické pro danou úlohu).
    • Na začátku každé RL epizody příkaz cp --reflink=always golden-docker.ext4 ep-0042.ext4 (na XFS, Btrfs, ZFS nebo macOS APFS) vytvoří nezávislý klon Copy-on-Write za méně než 100 milisekund, aniž by kopíroval podkladové disková bloky.
    • Dodatečné místo na disku spotřebují pouze 4KiB bloky, které byly během dané epizody změněny.
  2. Bezezbytkové ukončení (env.close()):
    • Ukončení procesu rungv (sb.Close() v Go) a odpojení souboru ep-0042.ext4 uvolní během přibližně 10 milisekund 100 % procesů, síťových socketů, připojení a úložiště dané epizody. Nic se nikdy nepřipojí do tabulky připojení hostitelského jádra ani se nezaregistruje v hostitelských cgroup.

Postup krok za krokem: zlatý snímek .ext4 a rychlý reset epizody

rungv můžete nainstalovat na každý uzel rollout workeru buď stažením předkompilované binárky s attestací 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), nebo spuštěním CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest.

Krok 1: Vytvoření zlatého svazku docker:dind pouze jednou

 1mkdir -p /srv/rl-snapshots /srv/rl-episodes
 2
 3# 1. Spusťte inicializační instanci docker:dind založenou na 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. Předem naplňte základní obrazy a build cache uvnitř 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. Korektně zastavte inicializační instanci, aby ext4lite zapsal a zkompaktoval obraz
20kill -INT $SEED_PID
21wait $SEED_PID

Krok 2: Spuštění paralelních RL epizod za 100 milisekund

Každý paralelní rollout worker klonuje golden-docker.ext4 pomocí reflinku a spouští izolovanou instanci docker:dind, ve které jsou již všechny obrazy přítomné v /var/lib/docker:

 1EPISODE_ID="ep-0042"
 2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
 3
 4# Okamžitý klon 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# Spuštění izolované neprivilegované epizody se 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# Uvnitř rollout workeru jsou obrazy okamžitě dostupné bez stahování ze sítě
22docker images
23
24# Ukončení epizody po výpočtu odměny (~10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"

Programovatelný pool rolloutů pomocí Go SDK z.gfire.dev/gvx/pkg/gvx

Pokud je váš RL environment server nebo evaluační harness napsán v Go (nebo volá rollout daemon v Go přes gRPC), můžete paralelní kontejnerové epizody spravovat přímo prostřednictvím 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 svazku 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. Nastavení časového limitu epizody podle hodinového času
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}

Srovnání runtime sandboxů pro RL agenty

Metrika / požadavekSdílený hostitelský dockerdPrivilegovaný docker:dindFirecracker / KVM MicroVMStandardní gVisor (runsc)gvx (rungv / pkg/gvx)
Vyžadován hostitelský root nebo --privilegedAno (docker.sock = root)Ano (--privileged)Vyžaduje /dev/kvm + nastavení TAPAno (nebo userns + mount)Ne (jediný neprivilegovaný proces)
Funguje v neprivilegovaných K8s Podech / stávajícím runscNeNeNe (bez /dev/kvm)Selhává (EBUSY u vnořeného seccomp)Ano (automatická záložní futex smyčka)
Podporuje vnitřní docker build a docker composeSdílí hostitelský démonAnoAnoSelhává bez ladění hostitelského DinDAno (overlayfs na ext4lite)
Izolace sítě pro jednotlivou epizoduSdílený docker0 / zámek xtablesOddělený namespaceOddělená síťová karta VMOddělený netnsIzolovaný netstack v rámci procesu
Latence teplého resetu epizody (env.reset())5 s až 30 s (prune kontejnerů/svazků)10 s až 60 s (studený démon)~150 ms (snímek VM)5 s až 20 s~100 ms (cp --reflink=always .ext4)
Multiplatformní lokální vývoj (Windows / macOS / Linux)Vyžaduje VM s LinuxemVyžaduje VM s LinuxemPouze Linux KVMPouze LinuxNativně na Windows, macOS a Linuxu

Další čtení v této sérii

  1. gvx: Spouštění Linuxových kontejnerů, Dockeru a Kubernetes v jediném neprivilegovaném procesu: Kompletní architektonický přehled gvx na Windows (whpx, systrap), macOS (hvf) a Linuxu (systrap).
  2. Spouštění Dockeru, docker build a docker compose bez rootu nebo uživatelských namespaces: Rychlý tříkrokový návod pro inicializaci a řešení potíží, pokud jsou blokovány sudo, unshare(CLONE_NEWUSER), mount(2) a /dev/fuse.
  3. Spouštění Docker-in-Docker uvnitř gVisor a vnořených sandboxů s gvx: Podrobný rozbor toho, proč docker.io a vnořený runsc selhávají uvnitř gVisor (runsc), a jak gvx přechází na futex smyčku _RUN_SYSCALL_LOOP při EBUSY.
  4. Rychlá izolovaná Docker prostředí pro RL rollouty AI agentů s gvx (tento příspěvek)