GoSuda

Ambientes Docker Isolados e Rápidos para Rollouts de RL de Agentes de IA com gvx

By Lemon Mint views ...

O Gargalo de Segurança e Infraestrutura nos Rollouts de RL de Agentes de Programação

O treinamento e a avaliação de agentes de engenharia de software em benchmarks e frameworks de aprendizado por reforço (RL), como SWE-bench, SWE-ReX, R2E-Gym, SWE-Gym, Verl e OpenHands, exigem a execução de milhares de rollouts concorrentes de código não confiável gerado por LLMs. Em tarefas reais de engenharia de software, um agente não se limita a editar arquivos de texto: ele executa docker build, inicia bancos de dados e microsserviços via docker compose, executa suítes de testes de integração e inspeciona logs de contêineres.

Em escala de rollout (128 a 1.024 episódios concorrentes por cluster), a infraestrutura de contêineres existente enfrenta quatro gargalos sistêmicos de segurança e desempenho:

  1. Contenção de Lock e Vazamento de Estado no dockerd do Host: Apontar centenas de workers de RL concorrentes para um único /var/run/docker.sock do host concede a cada agente acesso equivalente ao de root ao daemon do host, serializa a extração de camadas de imagens, esgota as alocações de sub-rede do docker0, dispara erros Another app is currently holding the xtables lock no iptables do host e vaza contêineres órfãos entre episódios.
  2. Risco de Escape de Contêiner via DinD Privilegiado: Executar contêineres docker:dind padrão exige --privileged (CAP_SYS_ADMIN), o que expõe dispositivos de bloco brutos do host, release agents de cgroup e módulos do kernel a rollouts de RL não confiáveis.
  3. Restrições de Virtualização por Hardware (/dev/kvm): Runtimes de microVM (Firecracker, QEMU, Kata Containers) isolam adequadamente os kernels internos, mas exigem /dev/kvm. Pods Kubernetes padrão sem virtualização aninhada, instâncias de nuvem bare-metal com KVM desabilitado e sandboxes gVisor (runsc) existentes não conseguem iniciar microVMs KVM.
  4. Latência Elevada no Reset de Episódio (env.reset()): Iniciar um novo ambiente docker:dind e baixar ou desempacotar imagens base para cada episódio de RL leva de 10 a 60 segundos, deixando os workers de treinamento em GPU ociosos enquanto aguardam os resets do ambiente.

gvx (CLI rungv e SDK em Go z.gfire.dev/gvx/pkg/gvx) fornece um runtime de rollout seguro, com defesa em profundidade, que resolve os quatro gargalos. Cada ambiente de rollout é executado como um único processo de espaço de usuário, não privilegiado e com auto-bloqueio (isolado pelo kernel Sentry do gVisor do Google, por pkg/hostlockdown e por um netstack em memória), cujo estado completo de /var/lib/docker reside dentro de um único arquivo .ext4 com Copy-on-Write (ext4lite).

Segurança Zero-Privilege para Rollouts de Agentes Não Confiáveis: Cada episódio gvx bloqueia o próprio processo do sistema operacional hospedeiro antes de executar código convidado (PR_SET_NO_NEW_PRIVS + seccomp BPF no Linux, Seatbelt deny default no macOS, Restricted Token + Job Object no Windows). Mesmo que um agente de RL execute docker run --privileged ou comandos destrutivos rm -rf / dentro de seu episódio, essas operações permanecem estritamente confinadas ao kernel Sentry de espaço de usuário desse episódio e ao seu arquivo de volume .ext4 efêmero.


Arquitetura de Rollout: Instâncias Paralelas Não Privilegiadas + Snapshots Reflink

No gvx, cada worker de rollout é um processo comum do sistema operacional, sem privilégios, com seu próprio kernel Sentry do gVisor, seu próprio netstack de espaço de usuário (10.0.2.15/24) e seu próprio volume de bloco ext4lite (ep-N.ext4), enquanto compartilha um cache de imagens OCI erofs somente leitura, endereçado por conteúdo, por meio do page cache do host.

Arquitetura Paralela de Rollout de Agentes RL com gvx

Por Que Volumes ext4lite de Arquivo Único Mudam a Economia do Reset em RL

Como o sistema de arquivos ext4lite do gvx armazena um volume ext4 completo com journal JBD2 (incluindo os diretórios de camadas do overlayfs do Docker, atributos estendidos trusted.overlay.*, nós de dispositivo whiteout e metadados de imagem) dentro de um único arquivo regular do host, o gerenciamento do ciclo de vida do episódio se reduz a uma única operação de arquivo:

  1. env.reset() de 100 Milissegundos via cp --reflink=always:
    • Você constrói um arquivo golden-docker.ext4 uma única vez, contendo imagens base pré-baixadas (python:3.11, postgres:16, redis:7 ou imagens base específicas de tarefas do SWE-bench).
    • No início de cada episódio de RL, cp --reflink=always golden-docker.ext4 ep-0042.ext4 (em XFS, Btrfs, ZFS ou macOS APFS) cria um clone independente com Copy-on-Write em menos de 100 milissegundos, sem copiar os blocos de disco subjacentes.
    • Apenas os blocos de 4 KiB modificados durante aquele episódio específico consomem espaço adicional em disco.
  2. Teardown Sem Resíduos (env.close()):
    • Encerrar o processo rungv (sb.Close() em Go) e remover o arquivo ep-0042.ext4 (unlink) recupera 100% dos processos, sockets de rede, montagens e armazenamento do episódio em cerca de 10 milissegundos. Nada é montado na tabela de montagens do kernel do host nem registrado em cgroups do host.

Fluxo de Trabalho Passo a Passo: Snapshot .ext4 Dourado e Reset Rápido de Episódio

Você pode instalar o rungv em cada nó worker de rollout, baixando o binário pré-compilado com atestação SLSA em 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) ou executando CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest.

Passo 1: Criar o Volume docker:dind Dourado Uma Única Vez

 1mkdir -p /srv/rl-snapshots /srv/rl-episodes
 2
 3# 1. Inicia uma instância semente docker:dind apoiada em 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é-popula imagens base e cache 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. Interrompe graciosamente a instância semente para que o ext4lite descarregue (flush) e compacte a imagem
20kill -INT $SEED_PID
21wait $SEED_PID

Passo 2: Gerar Episódios Paralelos de RL em 100 Milissegundos

Cada worker de rollout paralelo clona golden-docker.ext4 via reflink e inicializa uma instância docker:dind isolada, com todas as imagens já presentes em /var/lib/docker:

 1EPISODE_ID="ep-0042"
 2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
 3
 4# Clone instantâneo com Copy-on-Write (~100 ms em XFS/Btrfs/APFS)
 5cp --reflink=always \
 6  /srv/rl-snapshots/golden-docker.ext4 \
 7  "/srv/rl-episodes/${EPISODE_ID}.ext4"
 8
 9# Inicia episódio isolado e não privilegiado com 4 GiB de RAM e 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 do worker de rollout, as imagens ficam imediatamente disponíveis, sem pull pela rede
22docker images
23
24# Teardown do episódio após o cálculo da recompensa (~10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"

Pool de Rollouts Programático Usando o SDK em Go z.gfire.dev/gvx/pkg/gvx

Se o seu servidor de ambientes de RL ou o harness de avaliação for escrito em Go (ou chamar um daemon de rollout em Go via gRPC), você pode gerenciar episódios de contêineres paralelos diretamente pelo 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 o volume ext4lite dourado
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 timeout de tempo de parede por episódio
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, // Limite 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}

Comparação de Runtimes de Sandbox para Agentes de RL

Métrica / Requisitodockerd Compartilhado do Hostdocker:dind PrivilegiadoFirecracker / KVM MicroVMgVisor Padrão (runsc)gvx (rungv / pkg/gvx)
root do host ou --privileged obrigatórioSim (docker.sock = root)Sim (--privileged)Requer /dev/kvm + configuração de TAPSim (ou userns + mount)Não (processo único sem privilégios)
Funciona em Pods K8s sem privilégios / runsc existenteNãoNãoNão (sem /dev/kvm)Falha (EBUSY em seccomp aninhado)Sim (fallback automático para o loop de futex)
Suporta docker build e docker compose internosCompartilha o daemon do hostSimSimFalha sem ajuste de DinD no hostSim (overlayfs sobre ext4lite)
Isolamento de rede por episódiodocker0 / lock xtables compartilhadoNamespace separadoNIC de VM separadanetns separadonetstack isolado no processo
Latência de reset de episódio quente (env.reset())5 s a 30 s (prune de contêiner/volume)10 s a 60 s (daemon frio)~150 ms (snapshot de VM)5 s a 20 s~100 ms (cp --reflink=always de .ext4)
Desenvolvimento local multiplataforma (Windows / macOS / Linux)Requer VM LinuxRequer VM LinuxSomente KVM LinuxSomente LinuxNativo em Windows, macOS e Linux

Leitura Complementar Nesta Série

  1. gvx: Executando Contêineres Linux, Docker e Kubernetes em um Único Processo Não Privilegiado: Análise arquitetural completa do gvx no Windows (whpx, systrap), macOS (hvf) e Linux (systrap).
  2. Executando Docker, docker build e docker compose Sem Root ou User Namespaces: Guia rápido de inicialização em 3 passos e de solução de problemas quando sudo, unshare(CLONE_NEWUSER), mount(2) e /dev/fuse estão bloqueados.
  3. Executando Docker-in-Docker Dentro do gVisor e de Sandboxes Aninhados com gvx: Análise detalhada de por que docker.io e runsc aninhado falham dentro do gVisor (runsc) e de como o gvx recorre ao loop de futex _RUN_SYSCALL_LOOP em caso de EBUSY.
  4. Ambientes Docker Rápidos e Isolados para Rollouts de RL de Agentes de IA com gvx (este post)