GoSuda

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

By Lemon Mint views ...

Goulot d'étranglement de sécurité et d'infrastructure dans les rollouts RL de agents de codage

L'entraînement et l'évaluation d'agents d'ingénierie logicielle à travers 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 LLM. Dans les tâches réelles d'ingénierie logicielle, un agent ne se contente pas de modifier des fichiers texte : il exécute docker build, lance des bases de données et des microservices via docker compose, exécute 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 des verrous de 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éseaux de docker0, déclenche les erreurs Another app is currently holding the xtables lock dans les iptables de l'hôte, et laisse des conteneurs orphelins fuir d'un épisode à l'autre.
  2. Risque d'évasion de conteneur via DinD privilégié : L'exécution de conteneurs docker:dind standard nécessite --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 les noyaux internes, mais ils nécessitent /dev/kvm. Les pods Kubernetes standard sans virtualisation imbriquée, les instances cloud bare-metal où KVM est désactivé et les sandboxes gVisor (runsc) existantes ne peuvent pas lancer de microVMs KVM.
  4. Latence élevée de réinitialisation d'épisode (env.reset()) : Démarrer un nouvel environnement docker:dind et 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 en attendant la réinitialisation des environnements.

gvx (CLI rungv et SDK Go z.gfire.dev/gvx/pkg/gvx) fournit un runtime de rollout sécurisé, peu coûteux en effort et économe en tokens, qui résout les quatre goulots d'étranglement. Chaque environnement de rollout s'exécute comme un unique processus en espace 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).

Exécution peu coûteuse en effort et économe en tokens pour les agents : Lorsque docker build ou docker compose fonctionne immédiatement dans chaque épisode (ou peut être initialisé par l'agent en un seul tour de 3 commandes via rungv), les rollouts RL ne gaspillent ni tokens de fenêtre de contexte ni budgets d'étapes à résoudre les échecs de service docker start liés à ulimit, les erreurs iptables-nft ou les rejets pivot_root de runc. Chaque token de la trajectoire est consacré à la résolution de la tâche réelle d'ingénierie logicielle.

Sécurité sans privilège pour les rollouts d'agents non fiables : Chaque épisode gvx verrouille son propre processus OS 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 OS ordinaire et 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 et adressé par contenu, via le page cache de l'hôte.

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

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

Parce que 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 seul fichier ordinaire de l'hôte, la gestion du cycle de vie d'un épisode se réduit à une seule opération sur fichier :

  1. Réinitialisation env.reset() en 100 millisecondes via cp --reflink=always :
    • Vous construisez une fois un fichier golden-docker.ext4 contenant des images de base pré-téléchargées (python:3.11, postgres:16, redis:7, ou des images de base SWE-bench propres à la tâche).
    • Au début de chaque épisode RL, cp --reflink=always golden-docker.ext4 ep-0042.ext4 (sur XFS, Btrfs, ZFS ou macOS APFS) crée en moins de 100 millisecondes un clone indépendant en copy-on-write, 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 ep-0042.ext4 récupère 100 % des processus, sockets réseau, montages 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.

Flux de travail 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 le volume docker:dind doré une seule fois

 1mkdir -p /srv/rl-snapshots /srv/rl-episodes
 2
 3# 1. Démarrer une instance docker:dind de départ 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 de départ 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 via 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 instantané en copy-on-write (~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 un épisode isolé et 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 directement des épisodes de conteneurs parallèles 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 horloge murale 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 sans privilège)
Fonctionne dans des Pods K8s non privilégiés / runsc existantNonNonNon (pas de /dev/kvm)Échoue (EBUSY sur seccomp imbriqué)Oui (repli automatique sur boucle futex)
Prend en charge docker build et docker compose internesPartage le démon hôteOuiOuiÉchoue sans réglage DinD hôteOui (overlayfs sur ext4lite)
Isolation réseau par épisodedocker0 / verrou xtables partagésEspace de noms séparéNIC de VM séparéenetns séparénetstack isolé dans le processus
Latence de réinitialisation à 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 sur Windows, macOS et Linux

Lectures complémentaires de cette série

  1. gvx : exécuter des conteneurs Linux, Docker et Kubernetes dans un seul processus non privilégié : Présentation architecturale complète 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 rapide de démarrage en 3 étapes et de dépannage lorsque sudo, unshare(CLONE_NEWUSER), mount(2) et /dev/fuse sont bloqués.
  3. Exécuter Docker-in-Docker dans gVisor et des sandboxes imbriquées avec gvx : Analyse approfondie des raisons pour lesquelles docker.io et runsc imbriqué échouent dans 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)