[{"data":1,"prerenderedAt":315},["ShallowReactive",2],{"post-/blog/tokenizer-c":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"tags":11,"body":15,"_type":309,"_id":310,"_source":311,"_file":312,"_stem":313,"_extension":314},"/blog/tokenizer-c","blog",false,"","Por que o tokenizer do meu GPT-2 caseiro demorava 5 minutos, e como C resolveu em 3 segundos","Voltando para as raizes codando em C","2026-03-18",[12,13,14],"AI","Python","C",{"type":16,"children":17,"toc":301},"root",[18,26,33,39,44,49,55,60,65,79,100,106,125,130,151,157,170,175,180,246,251,257,270,283,296],{"type":19,"tag":20,"props":21,"children":23},"element","h1",{"id":22},"por-que-o-tokenizer-do-meu-gpt-2-caseiro-demorava-5-minutos-e-como-c-resolveu-em-3-segundos",[24],{"type":25,"value":8},"text",{"type":19,"tag":27,"props":28,"children":30},"h2",{"id":29},"tokenizer-o-tradutor-que-ninguém-presta-atenção",[31],{"type":25,"value":32},"Tokenizer: o tradutor que ninguém presta atenção",{"type":19,"tag":34,"props":35,"children":36},"p",{},[37],{"type":25,"value":38},"Modelos de linguagem não leem texto. Leem números. Alguém precisa fazer essa conversão, e esse alguém é o tokenizer.",{"type":19,"tag":34,"props":40,"children":41},{},[42],{"type":25,"value":43},"O GPT-2 usa BPE (Byte Pair Encoding). Começa com os 256 bytes possíveis e vai fundindo os pares mais frequentes do corpus até montar um vocabulário. Depois de uns milhares de merges, o vocabulário captura pedaços úteis da língua: sílabas, palavras comuns, sufixos. O tamanho do vocabulário importa porque define a resolução com que o modelo enxerga o texto. Se o tokenizer fragmenta demais, o modelo gasta capacidade juntando pedaços. Se comprime bem, sobra capacidade pra entender o que está escrito.",{"type":19,"tag":34,"props":45,"children":46},{},[47],{"type":25,"value":48},"No Gepeto-2 (meu mini GPT-2 educacional), implementei o BPE do zero em Python. Byte-level, mesma regex de pré-tokenização do GPT-2 original, vocabulário de 8192 tokens. Funciona. Só que demora.",{"type":19,"tag":27,"props":50,"children":52},{"id":51},"o-problema-na-prática",[53],{"type":25,"value":54},"O problema na prática",{"type":19,"tag":34,"props":56,"children":57},{},[58],{"type":25,"value":59},"Antes de treinar o modelo, preciso encodar o corpus inteiro: pegar todos os textos do JSONL e converter em sequências de token IDs. O corpus tem ~1800 artigos da Wikipedia, ~27 milhões de caracteres.",{"type":19,"tag":34,"props":61,"children":62},{},[63],{"type":25,"value":64},"100 artigos levavam 326 segundos em Python. O corpus inteiro levaria mais de uma hora. E toda vez que eu quisesse trocar o corpus (e eu pretendo treinar com vários), ia ter que esperar de novo.",{"type":19,"tag":34,"props":66,"children":67},{},[68,70,77],{"type":25,"value":69},"O gargalo tem nome: ",{"type":19,"tag":71,"props":72,"children":74},"code",{"className":73},[],[75],{"type":25,"value":76},"_apply_merges",{"type":25,"value":78},". Pra cada palavra do texto, o tokenizer aplica ~7900 merges em sequência. Cada merge varre a lista de tokens procurando pares adjacentes pra fundir. Loop dentro de loop, multiplicado por centenas de milhares de palavras.",{"type":19,"tag":34,"props":80,"children":81},{},[82,84,90,92,98],{"type":25,"value":83},"O que acontece por baixo do Python nesse loop é meio doloroso de pensar. Cada ",{"type":19,"tag":71,"props":85,"children":87},{"className":86},[],[88],{"type":25,"value":89},"tokens[j]",{"type":25,"value":91}," não é um acesso à memória. É uma indireção via ponteiro pra um objeto ",{"type":19,"tag":71,"props":93,"children":95},{"className":94},[],[96],{"type":25,"value":97},"int",{"type":25,"value":99}," que mora no heap. Cada iteração passa por resolução de referências, type checking dinâmico, e o garbage collector tá ali rondando. São milhões de iterações, e cada uma carrega todo esse overhead.",{"type":19,"tag":27,"props":101,"children":103},{"id":102},"o-que-muda-em-c",[104],{"type":25,"value":105},"O que muda em C",{"type":19,"tag":34,"props":107,"children":108},{},[109,111,116,118,123],{"type":25,"value":110},"Em C, o array de tokens é um bloco contíguo de ",{"type":19,"tag":71,"props":112,"children":114},{"className":113},[],[115],{"type":25,"value":97},{"type":25,"value":117},". Acessar ",{"type":19,"tag":71,"props":119,"children":121},{"className":120},[],[122],{"type":25,"value":89},{"type":25,"value":124}," é somar um offset num ponteiro. Acabou. Sem interpretador, sem type checking em runtime, sem GC, sem objetos. O compilador ainda otimiza por cima: desenrola loops, aloca registradores, dá hints de branch prediction. Nada disso existe no mundo Python.",{"type":19,"tag":34,"props":126,"children":127},{},[128],{"type":25,"value":129},"Eu gosto de Python. É minha linguagem do dia a dia. Mas pra um loop apertado varrendo arrays de inteiros milhões de vezes, a diferença entre interpretado e compilado aparece. E não é sutíl.",{"type":19,"tag":34,"props":131,"children":132},{},[133,135,141,143,149],{"type":25,"value":134},"A implementação em C ficou com umas 80 linhas. Duas funções: ",{"type":19,"tag":71,"props":136,"children":138},{"className":137},[],[139],{"type":25,"value":140},"apply_merges",{"type":25,"value":142}," (um chunk) e ",{"type":19,"tag":71,"props":144,"children":146},{"className":145},[],[147],{"type":25,"value":148},"apply_merges_batch",{"type":25,"value":150}," (vários de uma vez, pra amortizar o custo das chamadas via ctypes). O algoritmo é o mesmo do Python, linha por linha. A diferença é toda na execução.",{"type":19,"tag":27,"props":152,"children":154},{"id":153},"como-ficou",[155],{"type":25,"value":156},"Como ficou",{"type":19,"tag":34,"props":158,"children":159},{},[160,162,168],{"type":25,"value":161},"Criei um módulo C e um wrapper Python com ctypes. O ",{"type":19,"tag":71,"props":163,"children":165},{"className":164},[],[166],{"type":25,"value":167},".c",{"type":25,"value":169}," compila sozinho na primeira importação. Se o gcc não estiver disponível, volta pro Python puro sem quebrar nada.",{"type":19,"tag":34,"props":171,"children":172},{},[173],{"type":25,"value":174},"Quando chamo o tokenizer na hora de treinar, se o backend C carregou, usa ele. Senão, usa a implementação Python de sempre. O resto do tokenizer (regex, special tokens, decode) continua em Python porque não são gargalo.",{"type":19,"tag":34,"props":176,"children":177},{},[178],{"type":25,"value":179},"Benchmark com 100 artigos (1.5M chars):",{"type":19,"tag":181,"props":182,"children":183},"table",{},[184,208],{"type":19,"tag":185,"props":186,"children":187},"thead",{},[188],{"type":19,"tag":189,"props":190,"children":191},"tr",{},[192,198,203],{"type":19,"tag":193,"props":194,"children":195},"th",{},[196],{"type":25,"value":197},"Backend",{"type":19,"tag":193,"props":199,"children":200},{},[201],{"type":25,"value":202},"Tempo",{"type":19,"tag":193,"props":204,"children":205},{},[206],{"type":25,"value":207},"Tokens",{"type":19,"tag":209,"props":210,"children":211},"tbody",{},[212,230],{"type":19,"tag":189,"props":213,"children":214},{},[215,220,225],{"type":19,"tag":216,"props":217,"children":218},"td",{},[219],{"type":25,"value":13},{"type":19,"tag":216,"props":221,"children":222},{},[223],{"type":25,"value":224},"326.2s",{"type":19,"tag":216,"props":226,"children":227},{},[228],{"type":25,"value":229},"344,620",{"type":19,"tag":189,"props":231,"children":232},{},[233,237,242],{"type":19,"tag":216,"props":234,"children":235},{},[236],{"type":25,"value":14},{"type":19,"tag":216,"props":238,"children":239},{},[240],{"type":25,"value":241},"3.6s",{"type":19,"tag":216,"props":243,"children":244},{},[245],{"type":25,"value":229},{"type":19,"tag":34,"props":247,"children":248},{},[249],{"type":25,"value":250},"89.5x mais rápido. Mesma saída, token por token. O corpus inteiro (~27M chars) agora encoda em cerca de 1 minuto.",{"type":19,"tag":27,"props":252,"children":254},{"id":253},"valeu-o-trabalho",[255],{"type":25,"value":256},"Valeu o trabalho?",{"type":19,"tag":34,"props":258,"children":259},{},[260,262,268],{"type":25,"value":261},"Sim e não. Eu já tinha um cache em disco: encoda uma vez, salva como ",{"type":19,"tag":71,"props":263,"children":265},{"className":264},[],[266],{"type":25,"value":267},".pt",{"type":25,"value":269},", e depois carrega direto. Isso resolve se você roda o mesmo corpus sempre. Mas eu quero testar corpus diferentes, e pra cada corpus novo, o primeiro encoding levava mais de uma hora. Agora leva 1 minuto.",{"type":19,"tag":34,"props":271,"children":272},{},[273,275,281],{"type":25,"value":274},"Escrever o módulo C não foi um projeto grande. Foram umas 80 linhas de C e umas 80 de wrapper. O algoritmo já existia em Python, era traduzir pra ",{"type":19,"tag":71,"props":276,"children":278},{"className":277},[],[279],{"type":25,"value":280},"int*",{"type":25,"value":282}," e expor via ctypes.",{"type":19,"tag":34,"props":284,"children":285},{},[286,288,294],{"type":25,"value":287},"Se o corpus crescer pra GBs, dá pra jogar OpenMP. Os chunks são independentes, então um ",{"type":19,"tag":71,"props":289,"children":291},{"className":290},[],[292],{"type":25,"value":293},"#pragma omp parallel for",{"type":25,"value":295}," no batch cairia sem atrito. Mas 89x single-threaded tá mais que suficiente por enquanto.",{"type":19,"tag":34,"props":297,"children":298},{},[299],{"type":25,"value":300},"No fim, o que me fez pensar foi perceber que \"encodar o texto\" não é uma etapa burocrática. São milhões de caracteres passando por milhares de merges, e cada merge é um loop sobre a sequência inteira. Em Python, cada iteração desse loop carrega o peso do interpretador. Em C, é uma soma de ponteiro. Multiplicado por uns bilhões de iterações, isso vira a diferença entre 5 minutos e 3 segundos.",{"title":7,"searchDepth":302,"depth":302,"links":303},2,[304,305,306,307,308],{"id":29,"depth":302,"text":32},{"id":51,"depth":302,"text":54},{"id":102,"depth":302,"text":105},{"id":153,"depth":302,"text":156},{"id":253,"depth":302,"text":256},"markdown","content:blog:tokenizer-c.md","content","blog/tokenizer-c.md","blog/tokenizer-c","md",1773882640319]