Como SIMD transformou 146 segundos de tokenização em menos de 1 segundo
🇬🇧 Read it in English
Quando você manda um prompt pra uma LLM, o modelo não lê o seu texto. Antes de qualquer coisa, um tokenizador quebra a sua frase em pedaços e troca cada pedaço por um número. O que o modelo recebe é isso: uma lista de inteiros. Nunca as letras.
Pra um prompt só, esse passo é instantâneo. Você nem percebe que aconteceu.
Só que você quase nunca tokeniza uma coisa só. Pensa num RAG: você pega os documentos da sua empresa e, antes de indexar, tokeniza cada um. Ou um fine-tune: tokeniza o dataset inteiro antes de treinar. Ou uma avaliação rodando sobre milhões de exemplos. Aí aquele passo invisível vira hora de máquina.
E ninguém cronometra ele. A gente aceita a lentidão da tokenização como se fosse lei da física - “é o preço de trabalhar com LLM em escala”. Não é. Quando eu fui medir, tinha 200x parados ali, esperando alguém olhar.
Esse 200x mora num lugar específico: pipeline que tokeniza em massa e grava os ids no disco. Se o seu caso é o prompt de um usuário em produção, a conta muda. E entre as duas pontas tem o TTFT, o tempo até o primeiro token, onde a tokenização reaparece. A gente chega em todas.
Primeiro: quanto do tempo é tokenização?
Antes de vender solução, vale medir o problema. Porque otimizar o que não pesa é perda de tempo - a gente volta nisso lá no final.
Montei o cenário mais simples possível: 281.664 documentos de texto real, 863 MB do dataset fineweb. Ler do disco, passar pelo tokenizador do Qwen3-8B, e escrever os token ids. Só isso, nenhum modelo rodando no meio.
O prepare.py do nanoGPT faz exatamente isso: tokeniza o corpus uma vez e grava os ids num .bin que o treino lê depois. Tokenizar de novo a cada época seria desperdício.
Depois cronometrei cada etapa. O resultado:
| etapa | tempo |
|---|---|
| ler 863 MB do disco | 0,8 s |
| tokenizar | 146 s |
| escrever 760 MB de ids | 0,1 s |
A tokenização era 99,4% do tempo. Não era um gargalo no meio de outros. Era a pipeline inteira. Ler e escrever quase um gigabyte cada era ruído perto dela.
Isso muda o jogo. Quando um passo come 99% do tempo, otimizar ele deixa de ser refino e vira a prioridade número um.
POR ISSO: matar a tokenização acelera a pipeline inteira
Aqui entra o Gigatoken, um tokenizador que o Marcel Roed escreveu em Rust. A promessa dele é agressiva: até ~1000x mais rápido que o tokenizers da HuggingFace.
Número redondo demais, desconfiei. Então rodei o meu próprio benchmark - mesmo corpus, mesmo tokenizador, na minha máquina (um MacBook M4 Max). Só a tokenização:
| ferramenta | tempo | throughput |
|---|---|---|
HuggingFace tokenizers |
146 s | 0,01 GB/s |
| Gigatoken | 0,74 s | ~1,2 GB/s |
Cerca de 200x na minha máquina. Não é o 1000x do README deles, mas o 1000x foi num servidor de 144 núcleos - quanto mais núcleo, mais a diferença abre. No meu laptop, 200x já basta pra doer.
E a pipeline inteira, com I/O e tudo? Caiu de 147 segundos pra menos de 2. 88x mais rápido no total. Dois minutos e meio de espera viraram menos de dois segundos, no mesmo corpus e com o mesmo resultado.
NOTA: o número mais importante dessas tabelas não é o tempo. É que os dois produziram exatamente os mesmos 190.429.497 tokens. Byte por byte. Um é 200x mais rápido fazendo o trabalho idêntico. Isso não é atalho, é engenharia.
Isso não é otimização prematura
Uma objeção apareceu na minha cabeça antes de aparecer na sua: trocar de tokenizador pra raspar segundos não é o retrato da otimização prematura?
Era o que eu pensava, e é por isso que nunca tinha cronometrado esse passo. O tokenizers da HuggingFace ainda ajuda a não olhar: é escrito em Rust, é multithreaded, mantido por gente séria. Qualquer pessoa razoável bate o olho e diz “isso está otimizado”. E ainda tinha 200x na mesa.
Otimização prematura seria eu ter ido mexer no I/O que gastava 0,8 segundo. Atacar o passo que come 99% do tempo é o óbvio, e só parece ousado porque ninguém olhou o relógio antes.
O que existe, e ninguém batizou, é o problema inverso: otimização tardia, quando você aceita um gargalo como intocável sem nunca ter cronometrado ele. “Tokenização é lenta” era isso. Uma parede que todo mundo via, até alguém enxergar uma porta.
“Mas você não escolheu o tokenizador mais lento de propósito?”
Boa pergunta. Se eu testasse só o Qwen, você teria todo o direito de desconfiar. Então testei mais.
Com o GPT-2, o clássico: a HuggingFace levou 129 segundos, o Gigatoken levou 0,22. Os 193.502.159 tokens, de novo, idênticos. Trocar de tokenizador dentro da HuggingFace não fecha o abismo - abre.
Mas o teste que importa mesmo é contra o tiktoken, da OpenAI. É o tokenizador com fama de rápido, o que roda por trás do GPT-4. E ele é rápido: fez o mesmo corpus em 12 segundos, uns 10x mais veloz que a HuggingFace.
Só que o Gigatoken fez em 0,22 segundo. 57x mais rápido que o tokenizador rápido, com resultado idêntico token a token.
E o mais interessante: a lentidão da HuggingFace não é “tokenizar é lento”. O tiktoken faz a mesma tokenização 10x mais rápido só trocando um mecanismo interno. O gargalo tem nome específico, e não é o BPE.
Como? SIMD na parte que ninguém olhava
O nome do gargalo é pretokenization.
Antes do algoritmo de tokenização de verdade (o BPE), tem uma etapa boba: quebrar o texto em pré-pedaços. Quase todo mundo faz isso com expressão regular. E regex varrendo gigabytes de texto é lento - ela lê meio que caractere por caractere, procurando o padrão.
O que o Marcel fez foi trocar o regex por uma implementação que faz a mesma coisa com SIMD. E é aqui que a mágica desmonta.
Imagina que você precisa achar todas as vírgulas num texto de mil páginas. Você pode ler palavra por palavra, procurando. Ou pode abrir o texto em 64 colunas paralelas e perguntar de uma vez: “tem vírgula em algum destes 64 caracteres agora?”. Mesmo resultado. Tempo bem diferente.
SIMD é isso. Single Instruction, Multiple Data. Uma instrução, vários dados. O processador pega 16, 32, 64 bytes e faz a mesma operação em todos num único ciclo.
NOTA: se você quiser puxar esse fio, o SIMD é um eixo de concorrência inteiro que a maioria de nós ignora - não é thread nem processo, é a própria ALU do processador trabalhando em lote. O Paul Butcher dedica um capítulo a isso no Seven Concurrency Models in Seven Weeks (o data parallelism), e o Mitchell Hashimoto tem um ensaio ótimo defendendo que todo dev deveria conhecer. Mas pro que interessa aqui, basta saber: é essa técnica que transforma 146 segundos em 0,7.
A parte honesta: 200x no passo não é 200x na sua vida
Agora o aviso que separa este post de um panfleto de vendas.
Eu disse que a tokenização era 99% do tempo. Repara: 99% do tempo DAQUELE teste. Isso vale pra qualquer pipeline onde nenhum modelo roda junto: você lê texto, tokeniza e grava os ids. Foi só isso que eu medi, e ali a tokenização domina sozinha.
No momento em que um modelo entra na mesma pipeline, a conta muda. Num RAG você tokeniza e depois joga cada pedaço num modelo de embedding - esse forward pass é trabalho de verdade, e a fatia da tokenização encolhe.
E se você tá tokenizando o prompt de um usuário antes de chamar um modelo em produção, o cenário vira de vez. Na geração em si, a tokenização é uma migalha perto do forward pass - 200x ali não muda quase nada, porque ela nunca foi o problema.
A exceção fina fica no tempo até o primeiro token, o TTFT. O próprio autor do Gigatoken mostrou, na discussão no Hacker News, que em modelos menores dá pra raspar 5-10% do TTFT. É real - mas é outro universo perto do 88x do pré-processamento. O tamanho do prêmio depende de onde a tokenização entra na sua conta. Isso é a Lei de Amdahl.
O ganho total de otimizar uma parte é limitado por quanto aquela parte pesava no todo. Otimizar 99% do tempo transforma tudo. Otimizar 1% não muda nada, por mais espetacular que seja o 200x.
Eu aprendi isso na marra. Esse ano fiz a rinha de backend e, pela primeira vez na vida, escrevi um kernel com SIMD na mão - a aplicação fazia muita conta repetitiva, o candidato perfeito. A matemática ficou bem mais rápida. E o ganho final foi decepcionante.
Porque a matemática nunca foi o meu gargalo. O tempo estava em tudo que rodava em volta dela. Eu tinha acelerado brilhantemente um pedaço que não pesava. A mesma lei que faz o Gigatoken parecer mágica me deu uma rasteira na rinha - só que na direção contrária.
O que fica
O trabalho de verdade não é “instalar o Gigatoken”. É medir onde o seu tempo mora antes de sair otimizando.
Se você faz pré-processamento de dados pra LLM e nunca cronometrou a tokenização, cronometre. Se o seu caminho for parecido com o do teste - ler texto, tokenizar, gravar os ids - ela pode estar comendo quase tudo. E se estiver, tem um 200x ali:
pip install gigatoken
A API dele imita a dos tokenizadores que já existem, então a troca em si é uma linha. Quanto desse ganho aparece na sua ponta é outra conversa, e depende de quanto do seu tempo estava na tokenização. Só o cronômetro responde isso.
Mas se a tokenização for migalha na sua pipeline, guarde o Gigatoken pro dia certo e vá caçar o gargalo real. Porque no fim é sempre a mesma história: o tempo mora em algum lugar específico, e quase nunca é onde a gente acha.
Por hoje é só.