GoSuda

Environnements Docker isolés et rapides pour les déploiements RL d'agents IA avec gvx

By Lemon Mint views ...

Le goulot d'étranglement de sécurité et d'infrastructure dans les rollouts RL des agents de codage

L'entraînement et l'évaluation d'agents d'ingénierie logicielle sur des benchmarks et des frameworks d'apprentissage par renforcement (RL) tels que SWE-bench, SWE-ReX, R2E-Gym, SWE-Gym, Verl et OpenHands exigent l'exécution de milliers de rollouts concurrents de code non fiable généré par des LLM. Dans les tâches d'ingénierie logicielle réelles, un agent ne se contente pas de modifier des fichiers texte : il exécute docker build, démarre des bases de données et des microservices via docker compose, lance des suites de tests d'intégration et inspecte les journaux des conteneurs.

À l'échelle des rollouts (128 à 1 024 épisodes concurrents par cluster), l'infrastructure de conteneurs existante se heurte à quatre goulots d'étranglement systémiques en matière de sécurité et de performance :

  1. Contention de verrou sur le dockerd hôte et fuite d'état : faire pointer des centaines de workers RL concurrents vers un unique /var/run/docker.sock de l'hôte accorde à chaque agent un accès équivalent à root sur le démon hôte, sérialise l'extraction des couches d'images, épuise les allocations de sous-réseau de docker0, déclenche des erreurs Another app is currently holding the xtables lock dans l'iptables de l'hôte, et laisse des conteneurs orphelins fuir entre les épisodes.
  2. Risque d'évasion de conteneur via DinD privilégié : l'exécution de conteneurs docker:dind standard requiert --privileged (CAP_SYS_ADMIN), ce qui expose aux rollouts RL non fiables les périphériques blocs bruts de l'hôte, les release agents de cgroups et les modules noyau.
  3. Contraintes de virtualisation matérielle (/dev/kvm) : les runtimes microVM (Firecracker, QEMU, Kata Containers) isolent proprement le noyau interne, mais ils requièrent /dev/kvm. Les pods Kubernetes standard sans virtualisation imbriquée, les instances cloud bare-metal où KVM est désactivé, et les sandboxes gVisor existantes (runsc) ne peuvent pas lancer de microVM KVM.
  4. Latence de la réinitialisation d'épisode (env.reset()) : démarrer un environnement docker:dind neuf, puis télécharger ou décompresser les images de base pour chaque épisode RL, prend 10 à 60 secondes, laissant les workers d'entraînement GPU inactifs pendant l'attente de la réinitialisation des environnements.

gvx (CLI rungv et SDK Go z.gfire.dev/gvx/pkg/gvx) fournit un runtime de rollout sécurisé et protégé en profondeur, qui résout les quatre goulots d'étranglement. Chaque environnement de rollout s'exécute comme un unique processus utilisateur non privilégié et auto-verrouillé (isolé par le noyau Sentry de gVisor de Google, par pkg/hostlockdown et par un netstack en mémoire), dont l'intégralité de l'état /var/lib/docker réside dans un unique fichier .ext4 en copy-on-write (ext4lite).

Sécurité zéro privilège pour les rollouts d'agents non fiables : chaque épisode gvx verrouille son propre processus système hôte avant d'exécuter le code invité (PR_SET_NO_NEW_PRIVS + seccomp BPF sous Linux, deny default Seatbelt sous macOS, Restricted Token + Job Object sous Windows). Même si un agent RL exécute docker run --privileged ou des commandes destructrices rm -rf / au sein de son épisode, ces opérations restent strictement confinées au noyau Sentry en espace utilisateur de cet épisode et à son fichier de volume .ext4 éphémère.


Architecture de rollout : instances parallèles non privilégiées + snapshots reflink

Dans gvx, chaque worker de rollout est un processus système ordinaire non privilégié, doté de son propre noyau gVisor Sentry, de son propre netstack en espace utilisateur (10.0.2.15/24) et de son propre volume bloc ext4lite (ep-N.ext4), tout en partageant un cache d'images OCI erofs en lecture seule, adressé par contenu, à travers le cache de pages de l'hôte.

Architecture de rollout RL parallèle d'agents avec gvx

Pourquoi les volumes ext4lite à fichier unique changent l'économie de la réinitialisation RL

Comme le système de fichiers ext4lite de gvx stocke un volume ext4 complet avec journal JBD2 (incluant les répertoires de couches overlayfs de Docker, les attributs étendus trusted.overlay.*, les nœuds de périphériques whiteout et les métadonnées d'images) dans un unique fichier régulier de l'hôte, la gestion du cycle de vie d'un épisode se réduit à une seule opération sur fichier :

  1. env.reset() de 100 millisecondes via cp --reflink=always :
    • Vous construisez une fois un fichier golden-docker.ext4 contenant des images de base préchargées (python:3.11, postgres:16, redis:7, ou les images de base spécifiques aux tâches SWE-bench).
    • Au démarrage de chaque épisode RL, cp --reflink=always golden-docker.ext4 ep-0042.ext4 (sur XFS, Btrfs, ZFS ou macOS APFS) crée un clone copy-on-write indépendant en moins de 100 millisecondes, sans copier les blocs disque sous-jacents.
    • Seuls les blocs de 4 KiB modifiés pendant cet épisode précis consomment de l'espace disque supplémentaire.
  2. Démantèlement sans résidu (env.close()) :
    • Terminer le processus rungv (sb.Close() en Go) et supprimer le lien de ep-0042.ext4 récupère 100 % des processus, sockets réseau, points de montage et stockage de l'épisode en environ 10 millisecondes. Rien n'est jamais monté dans la table de montage du noyau hôte ni enregistré dans les cgroups de l'hôte.

Procédure pas à pas : snapshot .ext4 doré et réinitialisation rapide d'épisode

Vous pouvez installer rungv sur chaque nœud worker de rollout soit en téléchargeant le binaire précompilé attesté SLSA depuis 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), soit en exécutant CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest.

Étape 1 : Créer une seule fois le volume docker:dind doré

 1mkdir -p /srv/rl-snapshots /srv/rl-episodes
 2
 3# 1. Démarrer une instance docker:dind d'amorçage adossée à 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. Préremplir les images de base et le cache de build dans 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. Arrêter proprement l'instance d'amorçage afin qu'ext4lite vide ses tampons et compacte l'image
20kill -INT $SEED_PID
21wait $SEED_PID

Étape 2 : Lancer des épisodes RL parallèles en 100 millisecondes

Chaque worker de rollout parallèle clone golden-docker.ext4 par reflink et démarre une instance docker:dind isolée dont toutes les images sont déjà présentes dans /var/lib/docker :

 1EPISODE_ID="ep-0042"
 2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
 3
 4# Clone copy-on-write instantané (~100 ms sur XFS/Btrfs/APFS)
 5cp --reflink=always \
 6  /srv/rl-snapshots/golden-docker.ext4 \
 7  "/srv/rl-episodes/${EPISODE_ID}.ext4"
 8
 9# Lancer l'épisode isolé non privilégié avec 4 GiB de RAM et 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# Dans le worker de rollout, les images sont immédiatement disponibles sans téléchargement réseau
22docker images
23
24# Démantèlement de l'épisode après le calcul de la récompense (~10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"

Pool de rollouts programmatique avec le SDK Go z.gfire.dev/gvx/pkg/gvx

Si votre serveur d'environnements RL ou votre harnais d'évaluation est écrit en Go (ou appelle un démon de rollout Go via gRPC), vous pouvez gérer des épisodes de conteneurs parallèles directement via 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. Cloner par reflink le volume ext4lite doré
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. Appliquer un délai d'expiration en temps réel par épisode
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, // Plafond de 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}

Comparaison des runtimes de sandbox pour agents RL

Métrique / Exigencedockerd hôte partagédocker:dind privilégiéFirecracker / microVM KVMgVisor standard (runsc)gvx (rungv / pkg/gvx)
root de l'hôte ou --privileged requisOui (docker.sock = root)Oui (--privileged)Nécessite /dev/kvm + config TAPOui (ou userns + mount)Non (processus unique non privilégié)
Fonctionne dans des Pods K8s non privilégiés / runsc existantNonNonNon (pas de /dev/kvm)Échec (EBUSY sur seccomp imbriqué)Oui (repli automatique sur la boucle futex)
Prend en charge docker build et docker compose internesPartage le démon hôteOuiOuiÉchec sans réglage DinD hôteOui (overlayfs sur ext4lite)
Isolation réseau par épisodedocker0 / verrou xtables partagéEspace de noms séparéNIC de VM séparéeNetns séparéIsolé dans un netstack interne au processus
Latence de réinitialisation d'épisode à chaud (env.reset())5 s à 30 s (purge conteneur/volume)10 s à 60 s (démon à froid)~150 ms (snapshot de VM)5 s à 20 s~100 ms (cp --reflink=always de .ext4)
Développement local multiplateforme (Windows / macOS / Linux)Nécessite une VM LinuxNécessite une VM LinuxKVM Linux uniquementLinux uniquementNatif sous Windows, macOS et Linux

Autres lectures dans cette série

  1. gvx : exécuter des conteneurs Linux, Docker et Kubernetes dans un seul processus non privilégié : parcours architectural complet de gvx sous Windows (whpx, systrap), macOS (hvf) et Linux (systrap).
  2. Exécuter Docker, docker build et docker compose sans root ni espaces de noms utilisateur : guide d'amorçage en 3 étapes et de dépannage rapide lorsque sudo, unshare(CLONE_NEWUSER), mount(2) et /dev/fuse sont bloqués.
  3. Exécuter Docker-in-Docker dans gVisor et dans des sandboxes imbriqués avec gvx : analyse approfondie des raisons pour lesquelles docker.io et runsc imbriqué échouent à l'intérieur de gVisor (runsc), et de la manière dont gvx bascule sur la boucle futex _RUN_SYSCALL_LOOP en cas d'EBUSY.
  4. Environnements Docker isolés et rapides pour les rollouts RL d'agents IA avec gvx (cet article)