GoSuda

Szybkie izolowane środowiska Docker dla rolloutów RL agentów AI z wykorzystaniem gvx

By Lemon Mint views ...

Wąskie gardło bezpieczeństwa i infrastruktury w rolloutach RL agentów kodujących

Trenowanie i ewaluacja agentów inżynierii oprogramowania na benchmarkach oraz w frameworkach uczenia przez wzmocnienie (RL), takich jak SWE-bench, SWE-ReX, R2E-Gym, SWE-Gym, Verl i OpenHands, wymaga wykonywania tysięcy równoległych rolloutów niezaufanego kodu generowanego przez LLM. W rzeczywistych zadaniach inżynierii oprogramowania agent nie tylko edytuje pliki tekstowe: uruchamia docker build, podnosi bazy danych i mikroserwisy za pomocą docker compose, wykonuje zestawy testów integracyjnych oraz sprawdza logi kontenerów.

Na skali rolloutów (od 128 do 1 024 równoległych epizodów na klaster) istniejąca infrastruktura kontenerowa napotyka cztery systemowe wąskie gardła bezpieczeństwa i wydajności:

  1. Rywalizacja o blokady demona dockerd hosta i wyciek stanu: Skierowanie setek równoległych workerów RL na jeden host /var/run/docker.sock daje każdemu agentowi dostęp równoważny root do demona hosta, szereguje wyodrębnianie warstw obrazów, wyczerpuje alokacje podsieci docker0, wywołuje błędy Another app is currently holding the xtables lock w iptables hosta oraz pozostawia osierocone kontenery między epizodami.
  2. Ryzyko ucieczki z kontenera w trybie uprzywilejowanym DinD: Uruchamianie standardowych kontenerów docker:dind wymaga --privileged (CAP_SYS_ADMIN), co eksponuje surowe urządzenia blokowe hosta, cgroup release agents i moduły jądra niezaufanym rolloutom RL.
  3. Ograniczenia wirtualizacji sprzętowej (/dev/kvm): MicroVM (Firecracker, QEMU, Kata Containers) czysto izolują wewnętrzne jądra, ale wymagają /dev/kvm. Standardowe pody Kubernetes bez zagnieżdżonej wirtualizacji, instancje bare-metal w chmurze, na których KVM jest wyłączone, oraz istniejące piaskownice gVisor (runsc) nie mogą uruchamiać microVM KVM.
  4. Wysokie opóźnienie resetu epizodu (env.reset()): Uruchamianie świeżego środowiska docker:dind oraz pobieranie lub rozpakowywanie obrazów bazowych dla każdego epizodu RL trwa od 10 do 60 sekund, pozostawiając bezczynne workery treningowe GPU w oczekiwaniu na resetowanie środowisk.

gvx (CLI rungv oraz SDK Go z.gfire.dev/gvx/pkg/gvx) zapewnia bezpieczne, niskonakładowe i oszczędne pod względem tokenów środowisko uruchomieniowe rolloutów, które rozwiązuje wszystkie cztery wąskie gardła. Każde środowisko rolloutu działa jako pojedynczy, nieuprzywilejowany, samoblokujący się proces w przestrzeni użytkownika (izolowany przez jądro Sentry gVisor firmy Google, pkg/hostlockdown oraz wewnętrzny netstack w pamięci), którego cały stan /var/lib/docker znajduje się w jednym pliku .ext4 typu copy-on-write (ext4lite).

Niskonakładowe, oszczędne pod względem tokenów wykonanie dla agentów: Gdy docker build lub docker compose działa od razu w każdym epizodzie (lub może zostać uruchomione przez agenta w jednej turze złożonej z 3 poleceń za pomocą rungv), rollouty RL nie marnują tokenów okna kontekstu ani budżetu kroków na rozwiązywanie problemów z service docker start, błędami ulimit, błędami iptables-nft czy odrzuceniami pivot_root przez runc. Każdy token trajektorii trafia do rozwiązywania rzeczywistego zadania inżynierii oprogramowania.

Bezpieczeństwo bez uprawnień dla niezaufanych rolloutów agentów: Każdy epizod gvx blokuje własny proces systemu operacyjnego hosta przed uruchomieniem kodu gościa (PR_SET_NO_NEW_PRIVS + BPF seccomp w Linuxie, Seatbelt deny default w macOS, Restricted Token + Job Object w Windows). Nawet jeśli agent RL wykona docker run --privileged lub destrukcyjne polecenia rm -rf / wewnątrz swojego epizodu, operacje te pozostają ściśle ograniczone do jądra Sentry w przestrzeni użytkownika tego epizodu oraz do jego efemerycznego pliku woluminu .ext4.


Architektura rolloutów: równoległe nieuprzywilejowane instancje + snapshoty reflink

W gvx każdy worker rolloutu jest zwykłym nieuprzywilejowanym procesem systemu operacyjnego, z własnym jądrem Sentry gVisor, własnym netstack w przestrzeni użytkownika (10.0.2.15/24) oraz własnym woluminem blokowym ext4lite (ep-N.ext4), przy czym współdzieli tylko do odczytu adresowalną zawartościowo pamięć podręczną obrazów OCI erofs w page cache hosta.

Równoległa architektura rolloutów RL agentów z gvx

Dlaczego woluminy ext4lite w jednym pliku zmieniają ekonomię resetów RL

Ponieważ filesystem ext4lite przechowuje cały wolumin ext4 z dziennikiem JBD2 (w tym katalogi warstw overlayfs Dockera, rozszerzone atrybuty trusted.overlay.*, węzły urządzeń whiteout oraz metadane obrazów) w jednym zwykłym pliku hosta, zarządzanie cyklem życia epizodu sprowadza się do jednej operacji na pliku:

  1. Reset env.reset() w 100 milisekund za pomocą cp --reflink=always:
    • Raz budujesz plik golden-docker.ext4 zawierający wstępnie pobrane obrazy bazowe (python:3.11, postgres:16, redis:7 lub specyficzne dla zadania obrazy bazowe SWE-bench).
    • Na początku każdego epizodu RL polecenie cp --reflink=always golden-docker.ext4 ep-0042.ext4 (na XFS, Btrfs, ZFS lub macOS APFS) tworzy niezależny klon Copy-on-Write w czasie krótszym niż 100 milisekund, bez kopiowania bazowych bloków dysku.
    • Dodatkową przestrzeń dyskową zajmują wyłącznie bloki 4 KiB zmodyfikowane w trakcie tego konkretnego epizodu.
  2. Zerowe pozostałości przy zamykaniu (env.close()):
    • Zakończenie procesu rungv (sb.Close() w Go) i usunięcie ep-0042.ext4 odzyskuje 100% procesów, gniazd sieciowych, montowań i pamięci masowej epizodu w około 10 milisekund. Nic nigdy nie jest montowane w tabeli montowań jądra hosta ani rejestrowane w cgroupach hosta.

Przewodnik krok po kroku: złoty snapshot .ext4 i szybki reset epizodu

Możesz zainstalować rungv na każdym węźle workera rolloutów, pobierając prekompilowany plik binarny z atestacją 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) lub uruchamiając CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest.

Krok 1: Utwórz złoty wolumin docker:dind jednorazowo

 1mkdir -p /srv/rl-snapshots /srv/rl-episodes
 2
 3# 1. Uruchom instancję seed docker:dind opartą 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. Wstępnie wypełnij obrazy bazowe i cache budowania wewnątrz 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. Łagodnie zatrzymaj instancję seed, aby ext4lite zapisał i skompaktował obraz
20kill -INT $SEED_PID
21wait $SEED_PID

Krok 2: Uruchom równoległe epizody RL w 100 milisekund

Każdy równoległy worker rolloutu klonuje golden-docker.ext4 przez reflink i uruchamia odizolowaną instancję docker:dind ze wszystkimi obrazami już obecnymi w /var/lib/docker:

 1EPISODE_ID="ep-0042"
 2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
 3
 4# Natychmiastowy 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# Uruchom odizolowany epizod bez uprawnień z 4 GiB RAM i 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# Wewnątrz workera rolloutu obrazy są od razu dostępne bez pobierania przez sieć
22docker images
23
24# Zakończenie epizodu po obliczeniu nagrody (~10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"

Programowalna pula rolloutów z użyciem SDK Go z.gfire.dev/gvx/pkg/gvx

Jeśli Twój serwer środowisk RL lub harness ewaluacyjny jest napisany w Go (albo wywołuje demona rolloutów w Go przez gRPC), możesz zarządzać równoległymi epizodami kontenerów bezpośrednio przez 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. Sklonuj przez reflink wolumin 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. Zastosuj limit czasu rzeczywistego dla epizodu
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}

Porównanie środowisk uruchomieniowych piaskownic RL agentów

Metryka / wymaganieWspółdzielony demon dockerd hostaUprzywilejowany docker:dindFirecracker / KVM MicroVMStandardowy gVisor (runsc)gvx (rungv / pkg/gvx)
Wymagany root hosta lub --privilegedTak (docker.sock = root)Tak (--privileged)Wymaga /dev/kvm + konfiguracji TAPTak (lub userns + mount)Nie (pojedynczy proces bez uprawnień)
Działa w nieuprzywilejowanych podach K8s / istniejącym runscNieNieNie (brak /dev/kvm)Nie (EBUSY przy zagnieżdżonym seccomp)Tak (automatyczny fallback na pętlę futex)
Obsługuje wewnętrzne docker build i docker composeWspółdzieli demona hostaTakTakNie bez strojenia DinD hostaTak (overlayfs na ext4lite)
Izolacja sieci per epizodWspółdzielony docker0 / blokada xtablesOddzielna przestrzeń nazwOddzielna karta NIC VMOddzielna przestrzeń nazw sieciowychIzolowany netstack w procesie
Opóźnienie resetu ciepłego epizodu (env.reset())5 s do 30 s (czyszczenie kontenera/woluminu)10 s do 60 s (zimny demon)~150 ms (snapshot VM)5 s do 20 s~100 ms (cp --reflink=always .ext4)
Lokalny development międzyplatformowy (Windows / macOS / Linux)Wymaga maszyny wirtualnej LinuxWymaga maszyny wirtualnej LinuxTylko Linux KVMTylko LinuxNatywnie na Windows, macOS i Linux

Dalsza lektura w tej serii

  1. gvx: uruchamianie kontenerów Linux, Docker i Kubernetes w jednym nieuprzywilejowanym procesie: Pełne omówienie architektury gvx w systemach Windows (whpx, systrap), macOS (hvf) i Linux (systrap).
  2. Uruchamianie Docker, docker build i docker compose bez roota ani przestrzeni użytkownika: Szybki 3-krokowy bootstrap oraz przewodnik rozwiązywania problemów, gdy sudo, unshare(CLONE_NEWUSER), mount(2) i /dev/fuse są zablokowane.
  3. Docker-in-Docker wewnątrz gVisor i zagnieżdżonych piaskownic z gvx: Szczegółowe omówienie tego, dlaczego docker.io i zagnieżdżony runsc zawodzą wewnątrz gVisor (runsc) oraz jak gvx przechodzi na pętlę futex _RUN_SYSCALL_LOOP przy EBUSY.
  4. Szybkie izolowane środowiska Docker dla rolloutów RL agentów AI z gvx (ten wpis)