Antevisão · Latent Rehearsal
08

A bancada

Servir, radiografar e treinar pelo navegador — e as duas medições que decidiram como o servidor foi escrito.

Apostila 08 Pré-requisito: apostila 14 do curso GPT-2

1 O que a bancada é

Uma página local, sem framework, sem build e sem CDN — funciona offline. Cinco abas sobre a mesma API em FastAPI:

As três primeiras vieram do curso anterior; as duas últimas nasceram com o projeto Antevisão.
AbaServe para
Playgroundgerar com o gpt2-xl ou com os nossos modelos, com streaming e controles de decodificação
Raio-Xtokenização, distribuição do próximo token, logit lens, mapa de atenção
Detectoros dois RoBERTa lado a lado
Modelosum cartão por treino salvo, com a curva e as medidas de geometria
Laboratóriorodar qualquer script do projeto, com log ao vivo

O seletor do Playground tem dois grupos: OpenAI (só o gpt2-xl) e Nossos (todos os checkpoints). Os outros quatro modelos da HuggingFace continuam baixados e continuam servindo os labs — só saíram do seletor, para ele não virar uma lista de coisas que ninguém escolhe.

2 Onde o tempo de uma requisição vai

Antes de otimizar qualquer coisa, medimos. gpt2, prompt de 23 tokens, 32 tokens gerados, 8 threads:

Decomposição de uma requisição completa.
EtapaTempo% do total
tokenizar0,1 ms0,0%
prefill (23 tokens de uma vez)120,1 ms6,7%
decodificar (32 tokens)1 666,6 ms93,3%
destokenizar0,0 ms0,0%
93% em decodificação serial

O prefill processa o prompt inteiro em uma passada — paralelo, barato por token. A decodificação é serial por definição: cada token precisa do anterior. Otimizar tokenização seria perder tempo com 0,0% do problema.

O que o KV cache paga

ConfiguraçãoTempotok/s
com cache1,86 s17,2
sem cache3,02 s10,6

1,62× — menos que os "10×" que se costuma ouvir, porque com 32 tokens o contexto ainda é curto e o ganho é quadrático no comprimento. O cache só é possível porque a máscara é causal: o passado nunca muda, então K e V dele podem ser guardados. Num encoder como o RoBERTa isso não existe.

A vazão que o nosso servidor joga fora

Mesmo modelo, requisições em lote.
Lotetok/s totalGanho
130,31,00×
263,72,10×
16140,84,65×

Cada token gerado lê todos os pesos do modelo da memória. Com lote 1, essa leitura serve uma requisição; com lote 16, dezesseis — pelo mesmo custo. É throughput quase de graça, e é a razão de existir do continuous batching do TGI e do vLLM.

Nosso servidor atende uma por vez. Para uma bancada de estudo está certo; para produção, é o primeiro gargalo a atacar. Está documentado como limitação consciente, não como descuido.

A parede do GPT-2, medida

contexto  512  → 29,6 ms/token
contexto 1000  → 34,6 ms/token
contexto 1024  → PAREDE: posição 1024 não existe na wpe (ela tem 1024 linhas: 0 a 1023)

O erro que quebrou o script de medição na primeira execução é literalmente a limitação arquitetural do GPT-2 — e é exatamente o que o RoPE elimina (apostila 02).

3 O cache com orçamento

A primeira versão guardava cada modelo carregado em memória, para sempre. Os sete modelos em float32 dão ~13 GB — acima da memória virtual de uma máquina de desenvolvimento comum —, e o sistema operacional derruba o processo quando ela enche:

OSError: the paging file is too small for this
operation to complete. (os error 1455)

Havia um segundo bug junto, mais sutil: a chave do cache incluía se o modelo tinha sido carregado com mapa de atenção ou não. Abrir o Playground e depois o Raio-X no mesmo modelo colocava duas cópias dele na memória. Ninguém percebia — só a RAM sumindo.

A correção foi um cache com orçamento: uma LRU com teto (LAB_CACHE_GB) que descarta o modelo menos usado ao estourar, e GET /info mostra o estado. Testado:

gpt2        → 0,46 GB
+ medium    → 1,79 GB
+ large     → 4,67 GB
+ gpt2-xl   → 5,80 GB   ← despejou os três menores
+ det-large → 1,32 GB   ← despejou o xl

E attn_implementation="eager" virou sempre — uma entrada por modelo, e é o que o Raio-X precisa de qualquer forma.

4 A aba Laboratório

Os nove scripts do projeto viraram formulários, gerados a partir de um catálogo no servidor. O log chega linha a linha por Server-Sent Events enquanto roda; fechar a aba não mata o processo, e voltar mostra o log desde o início.

Parar é um Ctrl+C de verdade

O botão envia CTRL_BREAK_EVENT, não kill. O treino recebe o sinal, salva o checkpoint e sai limpo — rodar de novo com a mesma rodada continua de onde parou. Um kill perderia o progresso desde a última avaliação.

Segurança: nada de "rode este comando"

Uma porta aberta que executa comando arbitrário é uma porta aberta que executa comando arbitrário, mesmo em localhost. O desenho é uma lista fechada: cada script declara seus argumentos com tipo, mínimo e máximo, e o argv é montado por código — nunca concatenado numa string de shell.

montar('treinar', {'rodada': 'a; rm -rf /'})
  -> rodada: use apenas letras, numeros, - _ . e virgula

montar('treinar', {'passos': 999999})
  -> passos: maximo 200000

5 A aba Modelos

Um cartão por treino salvo em model/runs/, com a curva desenhada em SVG puro — perda de treino em cinza, validação em verde, anisotropia em roxo tracejado. Os distintivos são lidos do nome da pasta: moderno, GPT-2 fiel, λ=1.

Embaixo, um campo que mede bits/caractere em todos os modelos de uma vez e destaca o melhor. É a métrica honesta entre tokenizadores diferentes — perplexidade é medida por token, e comparar PPL entre vocabulários não significa nada.

6 A API

Compatível com TGI e TEI, mais os endpoints que nenhum servidor de produção expõe.
EndpointOrigemFaz
POST /generateTGIcompleta texto (HF e nossos)
POST /generate_streamTGItoken a token (SSE)
POST /embedTEIvetores
POST /predictTEIclassificação Fake/Real
POST /attentionnossomapa n×n + perfil das cabeças
POST /logit_lensnossoprevisão camada a camada
GET /modelosnossonossos treinos, com curva e métricas
POST /nosso/perplexidadenossoloss, PPL e bits/caractere
POST /lab/rodarnossodispara um script
GET /lab/log/{id}nossolog ao vivo (SSE)

O parâmetro compass em /generate é a extensão que a apostila 05 descreve: {"alvo": "…", "alpha": 0, "beta": 8}.

Servir os nossos modelos é outro código

Os modelos da HuggingFace vêm com generate(), cache, streamer e um ecossistema inteiro. O nosso é um nn.Module de 400 linhas. Em vez de fingir que são iguais, a diferença mora em server/ours.py, num lugar só, e o resto do servidor lida com uma interface única.

A geração decodifica incrementalmente e envia só o que cresceu — BPE em nível de byte pode precisar de dois tokens para formar um caractere, e decodificar token a token isolado produziria lixo na tela.

Resultado: o nosso mini gera a 127 tok/s contra ~7 do gpt2. Duzentas vezes menor, dezoito vezes mais rápido.

Perguntas em aberto

A apostila 09 mostra a arquitetura inteira, alinhada ao modelo de mundo.
As 04 e 05 — a linhagem do colapso e o leme — esperam os experimentos fecharem: escrever antes seria inventar número.