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

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 :
- Contention de verrou sur le
dockerdhôte et fuite d'état : faire pointer des centaines de workers RL concurrents vers un unique/var/run/docker.sockde 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 dedocker0, déclenche des erreursAnother app is currently holding the xtables lockdans l'iptablesde l'hôte, et laisse des conteneurs orphelins fuir entre les épisodes. - Risque d'évasion de conteneur via DinD privilégié : l'exécution de conteneurs
docker:dindstandard 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. - 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 sandboxesgVisorexistantes (runsc) ne peuvent pas lancer de microVM KVM. - Latence de la réinitialisation d'épisode (
env.reset()) : démarrer un environnementdocker:dindneuf, 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
gvxverrouille son propre processus système hôte avant d'exécuter le code invité (PR_SET_NO_NEW_PRIVS+seccompBPF sous Linux,deny defaultSeatbelt sous macOS, Restricted Token + Job Object sous Windows). Même si un agent RL exécutedocker run --privilegedou des commandes destructricesrm -rf /au sein de son épisode, ces opérations restent strictement confinées au noyauSentryen 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.

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 :
env.reset()de 100 millisecondes viacp --reflink=always:- Vous construisez une fois un fichier
golden-docker.ext4contenant des images de base préchargées (python:3.11,postgres:16,redis:7, ou les images de base spécifiques aux tâchesSWE-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.
- Vous construisez une fois un fichier
- Démantèlement sans résidu (
env.close()) :- Terminer le processus
rungv(sb.Close()en Go) et supprimer le lien deep-0042.ext4ré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.
- Terminer le processus
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 / Exigence | dockerd hôte partagé | docker:dind privilégié | Firecracker / microVM KVM | gVisor standard (runsc) | gvx (rungv / pkg/gvx) |
|---|---|---|---|---|---|
root de l'hôte ou --privileged requis | Oui (docker.sock = root) | Oui (--privileged) | Nécessite /dev/kvm + config TAP | Oui (ou userns + mount) | Non (processus unique non privilégié) |
Fonctionne dans des Pods K8s non privilégiés / runsc existant | Non | Non | Non (pas de /dev/kvm) | Échec (EBUSY sur seccomp imbriqué) | Oui (repli automatique sur la boucle futex) |
Prend en charge docker build et docker compose internes | Partage le démon hôte | Oui | Oui | Échec sans réglage DinD hôte | Oui (overlayfs sur ext4lite) |
| Isolation réseau par épisode | docker0 / verrou xtables partagé | Espace de noms séparé | NIC de VM séparée | Netns 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 Linux | Nécessite une VM Linux | KVM Linux uniquement | Linux uniquement | Natif sous Windows, macOS et Linux |
Autres lectures dans cette série
- gvx : exécuter des conteneurs Linux, Docker et Kubernetes dans un seul processus non privilégié : parcours architectural complet de
gvxsous Windows (whpx,systrap), macOS (hvf) et Linux (systrap). - 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/fusesont bloqués. - Exécuter Docker-in-Docker dans gVisor et dans des sandboxes imbriqués avec gvx : analyse approfondie des raisons pour lesquelles
docker.ioetrunscimbriqué échouent à l'intérieur degVisor(runsc), et de la manière dontgvxbascule sur la boucle futex_RUN_SYSCALL_LOOPen cas d'EBUSY. - Environnements Docker isolés et rapides pour les rollouts RL d'agents IA avec gvx (cet article)