Servir, radiografar e treinar pelo navegador — e as duas medições que decidiram como o servidor foi escrito.
Uma página local, sem framework, sem build e sem CDN — funciona offline. Cinco abas sobre a mesma API em FastAPI:
| Aba | Serve para |
|---|---|
| Playground | gerar com o gpt2-xl ou com os nossos modelos, com streaming e controles de decodificação |
| Raio-X | tokenização, distribuição do próximo token, logit lens, mapa de atenção |
| Detector | os dois RoBERTa lado a lado |
| Modelos | um cartão por treino salvo, com a curva e as medidas de geometria |
| Laboratório | rodar 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.
Antes de otimizar qualquer coisa, medimos. gpt2, prompt de 23 tokens, 32 tokens gerados, 8 threads:
| Etapa | Tempo | % do total |
|---|---|---|
| tokenizar | 0,1 ms | 0,0% |
| prefill (23 tokens de uma vez) | 120,1 ms | 6,7% |
| decodificar (32 tokens) | 1 666,6 ms | 93,3% |
| destokenizar | 0,0 ms | 0,0% |
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.
| Configuração | Tempo | tok/s |
|---|---|---|
| com cache | 1,86 s | 17,2 |
| sem cache | 3,02 s | 10,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
| Lote | tok/s total | Ganho |
|---|---|---|
| 1 | 30,3 | 1,00× |
| 2 | 63,7 | 2,10× |
| 16 | 140,8 | 4,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.
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).
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.
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.
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.
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
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.
| Endpoint | Origem | Faz |
|---|---|---|
POST /generate | TGI | completa texto (HF e nossos) |
POST /generate_stream | TGI | token a token (SSE) |
POST /embed | TEI | vetores |
POST /predict | TEI | classificação Fake/Real |
POST /attention | nosso | mapa n×n + perfil das cabeças |
POST /logit_lens | nosso | previsão camada a camada |
GET /modelos | nosso | nossos treinos, com curva e métricas |
POST /nosso/perplexidade | nosso | loss, PPL e bits/caractere |
POST /lab/rodar | nosso | dispara um script |
GET /lab/log/{id} | nosso | log ao vivo (SSE) |
O parâmetro compass em /generate é a extensão que a apostila 05 descreve: {"alvo": "…", "alpha": 0, "beta": 8}.
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.
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.