Laboratório GPT-2 · RoBERTa · Parte IV — Na sua máquina
14

Servindo modelos:
sua API, o TGI e o TEI

Da chamada de função ao servidor HTTP — e o que os servidores de produção fazem que o seu não faz, e por que isso importa só a partir de certa escala.

Apostila 14 de 15 Pré-requisito: apostilas 06 e 07

1 Por que servir por HTTP

Até aqui, cada script do laboratório carregava o modelo do zero. Para o gpt2-xl isso custa 7 segundos e 5,8 GB de RAM — toda vez.

Um servidor de inferência resolve isso invertendo a relação: o modelo fica carregado num processo que não morre, e qualquer coisa fala com ele por HTTP. Você ganha:

2 O nosso servidor

.venv\Scripts\python.exe server\app.py

  ->  http://localhost:8080/       painel
  ->  http://localhost:8080/docs   API interativa (Swagger)

São ~200 linhas em server/app.py, e ele expõe deliberadamente os mesmos endpoints do TGI e do TEI — de forma que o que você aprende aqui transfere direto para os servidores de produção.

Todos testados e funcionando nesta máquina.
EndpointOrigemO que faz
POST /generateTGIcompleta um texto; aceita os 5 modelos da família GPT
POST /generate_streamTGIo mesmo, token a token, via Server-Sent Events
POST /embedTEIvetor de 768 dimensões por texto (mean pooling normalizado)
POST /predictTEIclassificação — o detector RoBERTa devolvendo Fake/Real
GET /infoTGImodelos disponíveis e limites
GET /healthTGIchecagem de vida

Exemplos que funcionam agora

# gerar texto
curl -X POST http://localhost:8080/generate \
  -H "Content-Type: application/json" \
  -d '{"inputs": "The capital of France is",
       "model": "gpt2-medium",
       "parameters": {"max_new_tokens": 20, "do_sample": false}}'

# resposta:
# {"generated_text": " Paris, and the capital of France is Paris.",
#  "details": {"model": "gpt2-medium", "prompt_tokens": 5,
#              "generated_tokens": 15, "seconds": 2.649,
#              "tokens_per_second": 5.7}}

# classificar com o detector
curl -X POST http://localhost:8080/predict \
  -H "Content-Type: application/json" \
  -d '{"inputs": "I went to the market and the tomatoes were a disgrace."}'

# resposta:
# [[{"label": "Fake", "score": 0.309467},
#   {"label": "Real", "score": 0.690533}]]
O ciclo completo, em duas chamadas

Gere texto com /generate usando o gpt2-large, e passe o resultado para /predict. Foi assim que os números da apostila 12 foram produzidos: o detector devolveu P(Fake) = 0,9997.

Um decoder escreve, um encoder julga, os dois no mesmo processo, conversando por HTTP. É todo o curso rodando em duas requisições.

3 O que é o TGI, de verdade

O text-generation-inference é o servidor de produção do Hugging Face para modelos generativos. Escrito em Rust (o roteador) mais Python (o motor), ele voltou a ser Apache 2.0 em abril de 2024, depois de um período com licença restritiva.

Ele não faz nada de diferente conceitualmente do nosso servidor. Ele faz as mesmas coisas muito melhor, através de quatro técnicas que valem conhecer pelo nome:

TécnicaO problema que resolve
Continuous batchingNosso servidor atende uma requisição por vez. O TGI junta requisições de vários usuários num único lote — e, crucialmente, troca requisições dentro do lote conforme umas terminam e outras chegam, sem esperar o lote inteiro acabar. É o maior ganho de throughput dos quatro.
PagedAttentionO cache KV (apostila 04) cresce a cada token e tem tamanho imprevisível. Alocá-lo em blocos contíguos desperdiça memória enorme. O PagedAttention o gerencia em páginas, como memória virtual de sistema operacional. Veio do projeto vLLM.
Kernels fundidosFlashAttention e afins: implementações CUDA que calculam a atenção sem nunca materializar a matriz n×n na memória. Mais rápido e usa menos RAM. Exigem GPU NVIDIA.
Tensor parallelismDividir um modelo grande demais para uma GPU entre várias, com as camadas fatiadas.

Por que não usamos o TGI aqui

A avaliação honesta para a sua máquina

O TGI é distribuído como imagem Docker Linux e seu motor depende de kernels CUDA. O laboratório foi feito para uma máquina comum: Windows, sem GPU NVIDIA (uma placa AMD de entrada não tem suporte oficial a ROCm) e sem Docker nem WSL.

Fazer o TGI rodar aqui exigiria instalar WSL2 e Docker Desktop — algumas horas e ~15 GB de disco, dos quais você tem pouco sobrando. E, no fim, você teria o mesmo GPT-2 respondendo, dentro de um contêiner, sem GPU para usar os kernels que justificam o TGI.

As quatro técnicas acima só compensam a partir de dezenas de requisições simultâneas. Para uma pessoa aprendendo, com um cliente por vez, elas não fazem diferença nenhuma. Foi por isso que escrevemos os mesmos endpoints em 200 linhas de Python — e a troca aqui é a favor do aprendizado, porque você consegue ler este servidor inteiro.

Se um dia você precisar servir modelos em produção, com GPU NVIDIA e Linux: use o TGI ou o vLLM. Não escreva o seu.

4 O TEI e uma boa notícia sobre o RoBERTa

O text-embeddings-inference é o irmão do TGI para encoders, também Apache 2.0 desde abril de 2024. E ele traz um detalhe relevante para nós:

Ao contrário do que se costuma supor, o TEI não serve apenas embeddings. Ele tem um endpoint /predict para modelos de sequence classification — exatamente a categoria dos nossos roberta-*-openai-detector. Ou seja: o RoBERTa, que não roda no Ollama por falta de suporte a cabeça de classificação (como a apostila 00 explica), rodaria no TEI sem problema.

Isso vale como correção de uma simplificação comum: "encoders não têm servidor de inferência" é falso. O ecossistema tem ferramenta para os dois galhos da árvore — é só que a do galho encoder é menos famosa.

O TEI tem um backend em Rust puro (candle) que pode compilar para CPU, então ele é tecnicamente menos hostil ao Windows que o TGI. Mas o Windows não é plataforma suportada, e a compilação é uma aposta. Nosso /predict faz o mesmo trabalho hoje.

5 A tabela de decisão

Como escolher, na vida real.
SituaçãoUse
Aprendendo, um usuário, CPUeste servidor — legível, sem dependências pesadas
Protótipo, poucos usuárioseste servidor, ou transformers direto
Produção, decoder, GPU NVIDIA, LinuxvLLM ou TGI
Produção, embeddings ou classificaçãoTEI
Rodar LLM no desktop, sem programarOllama ou llama.cpp (mas não servem encoders)
Máxima velocidade em CPUllama.cpp com pesos quantizados em GGUF

6 Detalhes de implementação que valem ler

Streaming: por que ele importa

O gpt2-xl gera a 3,6 tokens/s nesta máquina. Uma resposta de 100 tokens leva ~28 segundos. Sem streaming, o usuário encara uma tela parada por 28 segundos. Com streaming, ele vê a primeira palavra em 300 ms e o texto crescendo.

O tempo total é idêntico. A percepção é completamente diferente — e é por isso que todo assistente de IA faz streaming. No nosso servidor, o TextIteratorStreamer roda a geração numa thread separada e vai emitindo eventos SSE.

Mean pooling: como sai o vetor do /embed

O encoder devolve um vetor por token. Para ter um vetor de frase, tiram-se a média sobre os tokens reais (ignorando padding) e normaliza-se para norma 1:

v = (h * mascara).sum(1) / mascara.sum(1)     # média só dos tokens reais
v = F.normalize(v, dim=-1)                    # norma 1

A normalização é o que permite comparar vetores por produto interno — que, para vetores de norma 1, é a similaridade de cosseno. É assim que bancos de dados vetoriais fazem busca rapidamente.

Uma nota: usamos mean pooling, não o <s>. Para modelos treinados especificamente para embeddings (Sentence-BERT e derivados), o mean pooling costuma ser melhor. Para classificação, usa-se o <s> — como o /predict faz.

7 No laboratório

# terminal 1 — deixe rodando
.venv\Scripts\python.exe server\app.py

# terminal 2
.venv\Scripts\python.exe lab\11_api.py
  1. Verifica a saúde e lista os modelos servidos.
  2. Compara os cinco geradores pela API, sem recarregar nada.
  3. Demonstra o streaming, imprimindo os tokens conforme chegam.
  4. Roda o ciclo completo: /generate → /predict, medindo quanto o detector acusa.
  5. Tenta fazer busca semântica com /embed — e falha. Leia a caixa abaixo antes de rodar; esse fracasso é a parte mais instrutiva do lab.
Por que a busca semântica não funciona com estes modelos

Montei um acervo de 6 frases e perguntei "Where is the famous iron tower located?". O sistema devolveu a frase sobre a floresta amazônica. E o mais revelador: todas as similaridades ficaram entre +0,99 e +1,00. Segundo esses vetores, a Torre Eiffel e a Amazônia são 99,8% parecidas.

Isso se chama anisotropia. Em vez de se espalharem pela esfera de 768 dimensões, os vetores colapsam num cone estreito — todos apontando quase para a mesma direção. O cosseno perde poder de discriminar.

Centralizar ajuda parcialmente. Subtraindo a média dos vetores antes de comparar, as notas voltam a se espalhar (de +0,99 para uma faixa de +0,54 a +0,79) e o sistema passa a acertar parte das perguntas. Mas ainda erra — porque isso trata o sintoma.

A causa são duas, empilhadas:

  1. Estes vetores vêm do roberta-base-openai-detector, fine-tunado para uma tarefa binária. O ajuste reorganizou as representações em torno do eixo "humano vs máquina" e descartou estrutura semântica que não servia para isso.
  2. Mesmo um RoBERTa puro produz embeddings de frase medianos. Ele nunca foi treinado para que a média dos seus tokens fosse um bom resumo — o objetivo dele era preencher lacunas (apostila 10).

Foi exatamente para resolver isso que criaram o Sentence-BERT (Reimers & Gurevych, 2019): pegar um BERT e treiná-lo com um objetivo de similaridade, usando pares de frases parecidas e diferentes. Modelos de embedding como E5, BGE e gte fazem alguma variante disso.

A lição: "é um encoder" não significa "serve para embeddings". O objetivo de treino determina para que as representações servem. A mecânica que o lab mostra (vetorizar → normalizar → produto interno → ordenar) está correta e é a de qualquer banco vetorial. Troque o modelo por um treinado para embeddings e funciona.

Exercício 14

  1. Abra http://localhost:8080/docs e dispare uma geração pelo formulário. Mude temperature e seed e observe o que acontece com a reprodutibilidade.
  2. Com seed fixo e do_sample=true, a saída se repete? E com seed nulo? Por quê?
  3. O /generate carrega o modelo na primeira chamada e o mantém em cache. Faça duas chamadas ao gpt2-xl e compare os tempos. Onde foram os 7 segundos?
  4. Difícil: nosso servidor atende uma requisição por vez. Descreva o que aconteceria com 20 clientes simultâneos, e explique, com suas palavras, como o continuous batching resolveria.
Referências

As fontes primárias dos conceitos deste capítulo. Os números do texto foram medidos no laboratório; as ideias vêm daqui.

Os termos técnicos têm aqui o sentido de aprendizado de máquina. O glossário do curso mostra esse sentido ao lado dos outros que as mesmas palavras têm na TI.

  1. Yu, G.-I. et al. (2022). Orca: A Distributed Serving System for Transformer-Based Generative Models. OSDI 2022. usenix.org
  2. Kwon, W. et al. (2023). Efficient Memory Management for Large Language Model Serving with PagedAttention. SOSP 2023. arXiv:2309.06180
  3. Hugging Face. Text Generation Inference (TGI). Repositório de código. github.com/huggingface/text-generation-inference
  4. Hugging Face. Text Embeddings Inference (TEI). Repositório de código. github.com/huggingface/text-embeddings-inference
  5. Ethayarajh, K. (2019). How Contextual are Contextualized Word Representations? EMNLP 2019. arXiv:1909.00512
  6. Reimers, N. e Gurevych, I. (2019). Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks. EMNLP 2019. arXiv:1908.10084

A apostila 15 é o mapa de referência: cada script, cada comando, cada arquivo — o guia para você voltar depois.