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 em Rollouts de RL para 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: Direcionar centenas de workers de RL concorrentes para um único /var/run/docker.sock do host concede a cada agente acesso equivalente ao de root no daemon do host, serializa a extração de camadas de imagens, esgota as alocações de sub-rede do docker0, provoca erros Another app is currently holding the xtables lock no iptables do host e faz com que contêineres órfãos vazem 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 do 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 bare-metal em nuvem 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 descompactar imagens base a 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 Go z.gfire.dev/gvx/pkg/gvx) oferece um runtime de rollout seguro, de baixo esforço e eficiente em tokens, que resolve os quatro gargalos. Cada ambiente de rollout é executado como um único processo de espaço de usuário, sem privilégios e com bloqueio próprio (isolado pelo kernel Sentry do gVisor do Google, pelo pkg/hostlockdown e por um netstack em memória), cujo estado completo de /var/lib/docker reside em um único arquivo .ext4 com copy-on-write (ext4lite).

Execução de Baixo Esforço e Eficiente em Tokens para Agentes: Quando docker build ou docker compose funciona imediatamente em cada episódio (ou pode ser inicializado pelo agente em um único turno de 3 comandos via rungv), os rollouts de RL não desperdiçam tokens da janela de contexto nem orçamento de passos com a solução de problemas como falhas de service docker start ulimit, erros de iptables-nft ou rejeições de pivot_root do runc. Cada token da trajetória é direcionado à solução da tarefa real de engenharia de software.

Segurança Zero-Privilégio para Rollouts de Agentes Não Confiáveis: Cada episódio do 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, deny default do Seatbelt no macOS, Restricted Token + Job Object no Windows). Mesmo que um agente de RL execute docker run --privileged ou comandos destrutivos rm -rf / dentro do 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 SO, 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 Rollouts de RL de Agentes com gvx

Por que Volumes ext4lite de Arquivo Único Mudam a Economia do Reset de 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 torna 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 APFS no macOS) cria um clone independente com copy-on-write em menos de 100 milissegundos, sem copiar os blocos de disco subjacentes.
    • Somente os blocos de 4 KiB modificados durante aquele episódio específico consomem espaço adicional em disco.
  2. Desmontagem sem Resíduos (env.close()):
    • Encerrar o processo rungv (sb.Close() em Go) e remover ep-0042.ext4 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 de docker:dind apoiada 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. Pré-popula as imagens base e o 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 grave e compacte a imagem
20kill -INT $SEED_PID
21wait $SEED_PID

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

Cada worker de rollout paralelo clona golden-docker.ext4 via reflink e inicia 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 o 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# Desmontagem 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 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 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 o 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 / MicroVM KVMgVisor 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 compartilhadosNamespace separadoNIC de VM separadaNetns separadonetstack isolado no processo
Latência de reset de episódio quente (env.reset())5s a 30s (poda de contêiner/volume)10s a 60s (daemon frio)~150 ms (snapshot de VM)5s a 20s~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: Walkthrough arquitetural completo 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 bootstrap 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 o 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 Isolados e Rápidos para Rollouts de RL de Agentes de IA com gvx (este post)