GoSuda

Entornos Docker Aislados y Rápidos para Rollouts de RL de Agentes de IA con gvx

By Lemon Mint views ...

El cuello de botella de seguridad e infraestructura en los rollouts de RL de agentes de programación

Entrenar y evaluar agentes de ingeniería de software en benchmarks y frameworks de aprendizaje por refuerzo (RL) como SWE-bench, SWE-ReX, R2E-Gym, SWE-Gym, Verl y OpenHands requiere ejecutar miles de rollouts concurrentes de código no confiable generado por LLM. En tareas reales de ingeniería de software, un agente no se limita a editar archivos de texto: ejecuta docker build, levanta bases de datos y microservicios mediante docker compose, ejecuta suites de pruebas de integración e inspecciona los logs de los contenedores.

A escala de rollout (de 128 a 1,024 episodios concurrentes por clúster), la infraestructura de contenedores existente enfrenta cuatro cuellos de botella sistémicos de seguridad y rendimiento:

  1. Contención de bloqueos y fuga de estado en el dockerd del host: Apuntar cientos de workers de RL concurrentes a un único /var/run/docker.sock del host otorga a cada agente acceso equivalente a root sobre el daemon del host, serializa la extracción de capas de imágenes, agota las asignaciones de subred de docker0, provoca errores Another app is currently holding the xtables lock en el iptables del host y deja contenedores huérfanos filtrados entre episodios.
  2. Riesgo de escape de contenedor por DinD privilegiado: Ejecutar contenedores docker:dind estándar requiere --privileged (CAP_SYS_ADMIN), lo que expone a los rollouts de RL no confiables los dispositivos de bloque crudos del host, los release agents de cgroups y los módulos del kernel.
  3. Restricciones de virtualización por hardware (/dev/kvm): Los runtimes de microVM (Firecracker, QEMU, Kata Containers) aíslan limpiamente los kernels internos, pero requieren /dev/kvm. Los pods estándar de Kubernetes sin virtualización anidada, las instancias de nube bare-metal donde KVM está deshabilitado y los sandboxes gVisor (runsc) existentes no pueden lanzar microVMs KVM.
  4. Latencia lenta de env.reset() al reiniciar episodios: Iniciar un entorno docker:dind nuevo y descargar o desempaquetar imágenes base para cada episodio de RL tarda entre 10 y 60 segundos, dejando inactivos a los workers de entrenamiento en GPU mientras esperan los reinicios del entorno.

gvx (CLI rungv y SDK de Go z.gfire.dev/gvx/pkg/gvx) proporciona un runtime de rollouts seguro y con defensa en profundidad que resuelve los cuatro cuellos de botella. Cada entorno de rollout se ejecuta como un único proceso de espacio de usuario, sin privilegios y con autobloqueo (aislado por el kernel Sentry de gVisor de Google, pkg/hostlockdown y un netstack en memoria), cuyo estado completo de /var/lib/docker reside dentro de un único archivo .ext4 con copy-on-write (ext4lite).

Seguridad sin privilegios para rollouts de agentes no confiables: Cada episodio de gvx bloquea su propio proceso del sistema operativo anfitrión antes de ejecutar código invitado (PR_SET_NO_NEW_PRIVS + seccomp BPF en Linux, Seatbelt deny default en macOS, Restricted Token + Job Object en Windows). Incluso si un agente de RL ejecuta docker run --privileged o comandos destructivos rm -rf / dentro de su episodio, esas operaciones permanecen estrictamente confinadas al kernel Sentry de espacio de usuario de ese episodio y a su archivo de volumen .ext4 efímero.


Arquitectura de rollouts: instancias paralelas sin privilegios + snapshots por reflink

En gvx, cada worker de rollout es un proceso ordinario del sistema operativo sin privilegios, con su propio kernel Sentry de gVisor, su propio netstack de espacio de usuario (10.0.2.15/24) y su propio volumen de bloques ext4lite (ep-N.ext4), mientras comparte, a través de la caché de páginas del host, una caché de imágenes OCI erofs de solo lectura y direccionable por contenido.

Arquitectura de rollouts paralelos de agentes de RL con gvx

Por qué los volúmenes ext4lite de un solo archivo cambian la economía del reinicio en RL

Dado que el sistema de archivos ext4lite de gvx almacena un volumen ext4 completo con journaling JBD2 (incluidos los directorios de capas de overlayfs de Docker, los atributos extendidos trusted.overlay.*, los nodos de dispositivo whiteout y los metadatos de imágenes) dentro de un único archivo regular del host, la gestión del ciclo de vida de los episodios se convierte en una única operación sobre archivos:

  1. env.reset() de 100 milisegundos mediante cp --reflink=always:
    • Construyes una vez un archivo golden-docker.ext4 que contiene imágenes base preextraídas (python:3.11, postgres:16, redis:7 o imágenes base específicas de tareas de SWE-bench).
    • Al inicio de cada episodio de RL, cp --reflink=always golden-docker.ext4 ep-0042.ext4 (en XFS, Btrfs, ZFS o macOS APFS) crea un clon independiente con copy-on-write en menos de 100 milisegundos, sin copiar los bloques de disco subyacentes.
    • Solo los bloques de 4 KiB modificados durante ese episodio específico consumen espacio adicional en disco.
  2. Desmontaje sin residuos (env.close()):
    • Terminar el proceso rungv (sb.Close() en Go) y desvincular ep-0042.ext4 recupera el 100 % de los procesos, sockets de red, montajes y almacenamiento del episodio en ~10 milisegundos. Nada se monta jamás en la tabla de montajes del kernel anfitrión ni se registra en los cgroups del host.

Flujo de trabajo paso a paso: snapshot dorado .ext4 y reinicio rápido de episodios

Puedes instalar rungv en cada nodo worker de rollouts descargando el binario precompilado con atestación SLSA desde 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) o ejecutando CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest.

Paso 1: crear una vez el volumen dorado docker:dind

 1mkdir -p /srv/rl-snapshots /srv/rl-episodes
 2
 3# 1. Inicia una instancia semilla de docker:dind respaldada por 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. Precarga las imágenes base y la caché de build dentro de 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. Detén de forma ordenada la instancia semilla para que ext4lite vacíe y compacte la imagen
20kill -INT $SEED_PID
21wait $SEED_PID

Paso 2: lanzar episodios paralelos de RL en 100 milisegundos

Cada worker de rollout paralelo clona golden-docker.ext4 mediante reflink y arranca una instancia docker:dind aislada con todas las imágenes ya presentes en /var/lib/docker:

 1EPISODE_ID="ep-0042"
 2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
 3
 4# Clon instantáneo con copy-on-write (~100 ms en XFS/Btrfs/APFS)
 5cp --reflink=always \
 6  /srv/rl-snapshots/golden-docker.ext4 \
 7  "/srv/rl-episodes/${EPISODE_ID}.ext4"
 8
 9# Lanza un episodio aislado sin privilegios con 4 GiB de RAM y 2 CPUs
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# Dentro del worker de rollout, las imágenes están disponibles de inmediato sin descarga por red
22docker images
23
24# Desmontaje del episodio tras el cálculo de la recompensa (~10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"

Pool programático de rollouts usando el SDK de Go z.gfire.dev/gvx/pkg/gvx

Si tu servidor de entornos de RL o tu harness de evaluación está escrito en Go (o llama a un daemon de rollouts en Go mediante gRPC), puedes gestionar episodios de contenedores paralelos directamente a través de 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. Clona por reflink el volumen 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. Aplica un timeout de reloj de pared por episodio
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, // Límite de 4 GiB
55			CPUMillis:   2000,    // 2.0 CPUs
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}

Comparación de runtimes de sandbox para agentes de RL

Métrica / Requisitodockerd compartido del hostdocker:dind privilegiadoFirecracker / microVM KVMgVisor estándar (runsc)gvx (rungv / pkg/gvx)
Requiere root del host o --privilegedSí (docker.sock = root)Sí (--privileged)Necesita /dev/kvm + config. TAPSí (o userns + mount)No (un único proceso sin privilegios)
Funciona dentro de Pods de K8s sin privilegios / runsc existenteNoNoNo (sin /dev/kvm)Falla (EBUSY en seccomp anidado)Sí (bucle de futex como fallback automático)
Soporta docker build y docker compose internosComparte el daemon del hostSíSíFalla sin ajuste de DinD en el hostSí (overlayfs sobre ext4lite)
Aislamiento de red por episodiodocker0 / bloqueo xtables compartidoNamespace separadoNIC de VM separadanetns separadonetstack aislado en el proceso
Latencia de reinicio de episodio en caliente (env.reset())5 s a 30 s (poda de contenedores/volúmenes)10 s a 60 s (daemon frío)~150 ms (snapshot de VM)5 s a 20 s~100 ms (cp --reflink=always de .ext4)
Desarrollo local multiplataforma (Windows / macOS / Linux)Requiere VM LinuxRequiere VM LinuxSolo KVM de LinuxSolo LinuxNativo en Windows, macOS y Linux

Más lecturas en esta serie

  1. gvx: Ejecución de contenedores Linux, Docker y Kubernetes en un solo proceso sin privilegios: Recorrido arquitectónico completo de gvx en Windows (whpx, systrap), macOS (hvf) y Linux (systrap).
  2. Ejecutar Docker, docker build y docker compose sin root ni user namespaces: Guía rápida de arranque en 3 pasos y de resolución de problemas cuando sudo, unshare(CLONE_NEWUSER), mount(2) y /dev/fuse están bloqueados.
  3. Ejecutar Docker-in-Docker dentro de gVisor y sandboxes anidados con gvx: Análisis detallado de por qué docker.io y runsc anidado fallan dentro de gVisor (runsc) y de cómo gvx recurre al bucle de futex _RUN_SYSCALL_LOOP ante EBUSY.
  4. Entornos Docker aislados y rápidos para rollouts de RL de agentes de IA con gvx (esta entrada)