4 min de leitura

Do Byte ao Tensor: A Anatomia Oculta dos Tokens em LLMs

Do Byte ao Tensor: A Anatomia Oculta dos Tokens em LLMs

Por que Strings Puras Quebram o Silício?

Para uma Unidade de Processamento Gráfico (GPU), texto não existe. Silício processa matrizes, floats e operações de multiplicação e acumulação (MACs) em paralelo através de milhares de núcleos de execução (Tensor Cores). Quando você envia a string "Aceleração de Hardware" para um LLM (Large Language Model), o hardware enfrenta um gargalo de computação e IO.

Antes da consolidação das técnicas modernas de tokenização, a engenharia de Processamento de Linguagem Natural (PLN) oscilava entre dois extremos ineficientes:

  1. Abordagem por Caractere Único: Tratar cada letra/byte como uma unidade analítica. Embora reduza o tamanho do vocabulário para poucas centenas de elementos (facilitando a alocação de memória para matrizes de projeção), o comprimento da sequência ($N$) explode. Como a complexidade do mecanismo de Self-Attention do Transformer escala quadraticamente em relação ao comprimento da sequência $O(N^2)$ , processar um parágrafo caractere por caractere consome toda a memória SRAM e HBM disponível para computar matrizes de atenção intermediárias.
  2. Abordagem por Palavra Inteira (Word-level): Mapear cada palavra única do dicionário para um ID. Isso gera um problema de dimensionalidade intratável. O vocabulário expande continuamente devido a conjugações, plurais e gírias. Matrizes de embedding gigantescas ocupariam gigabytes de VRAM preciosa apenas para fazer consultas estáticas de índices (lookups), deixando menos espaço para os pesos ativos do modelo durante a inferência.

A tokenização por subpalavras (subwords) surgiu para resolver este trade-off de engenharia: comprimir o texto original em fragmentos estatísticos reaproveitáveis, limitando o tamanho do vocabulário a um tamanho fixo (ex: $32.000$ a $128.000$ elementos) enquanto minimiza o comprimento da sequência final entregue aos blocos de computação.

O Mecanismo Interno: Algoritmos e Pipelines de Dados

O processo de conversão de texto bruto em tensores de entrada divide-se em duas etapas críticas: Codificação Estatística (Tokenização) e Projeção de Alta Dimensionalidade (Embedding).

graph TD
    A[Texto Bruto: 'Hardware'] --> B[Pre-Tokenizer: Divisão por espaços/regras]
    B --> C[BPE Encoder: Substituição por IDs do Vocabulário]
    C --> D[Vetor de IDs: 1D Tensor int32]
    D --> E[Embedding Matrix lookup: W_emb]
    E --> F[Vetor de Ativação: 2D Tensor float16/bfloat16]
    style A fill:#222,stroke:#555,stroke-width:2px
    style D fill:#333,stroke:#777,stroke-width:2px
    style F fill:#111,stroke:#444,stroke-width:2px

O Algoritmo Core: Byte-Pair Encoding (BPE)

Popularizado pela OpenAI com o GPT-2 (e refinado no tiktoken usando representações de bytes nativas), o BPE constrói o vocabulário de forma iterativa de baixo para cima (bottom-up).

  1. Inicializa o vocabulário com todos os bytes individuais encontrados no corpus de treinamento ($256$ símbolos básicos).
  2. Conta a frequência de todos os pares de símbolos adjacentes no texto.
  3. Mescla (merge) o par mais frequente em um novo símbolo único.
  4. Repete o processo até atingir o tamanho limite estipulado para o vocabulário ($V$).

Abaixo está uma implementação limpa e de baixo nível que demonstra a mecânica exata de contagem e fusão de pares que ocorre na CPU/GPU durante a fase de compilação do tokenizador:

import collections

def get_stats(ids: list[int]) -> dict[tuple[int, int], int]:
    """Varre a lista de IDs de bytes e conta a frequência de pares adjacentes."""
    counts = collections.defaultdict(int)
    for pair in zip(ids, ids[1:]):
        counts[pair] += 1
    return counts

def merge(ids: list[int], pair: tuple[int, int], idx: int) -> list[int]:
    """Substitui o par mais comum pelo novo ID gerado (fusão)."""
    new_ids = []
    i = 0
    while i < len(ids):
        if i < len(ids) - 1 and ids[i] == pair[0] and ids[i+1] == pair[1]:
            new_ids.append(idx)
            i += 2
        else:
            new_ids.append(ids[i])
            i += 1
    return new_ids

# Exemplo prático de processamento de bytes puros
texto_exemplo = "low lower lowest widest"
text_bytes = list(texto_exemplo.encode("utf-8"))

# Executa 3 iterações de fusão para otimização de dicionário
vocab_size = 256
num_merges = 3
for i in range(num_merges):
    stats = get_stats(text_bytes)
    if not stats:
        break
    top_pair = max(stats, key=stats.get)
    new_id = vocab_size + i
    text_bytes = merge(text_bytes, top_pair, new_id)
    print(f"Par fundido {top_pair} -> Novo ID: {new_id}")
    
    ```

Da Unidade Discreta ao Espaço Vetorial

Uma vez que o texto foi reduzido a um vetor unidimensional de inteiros de 32 bits (os IDs dos tokens), ele precisa ser convertido em uma representação matemática contínua. É aqui que entra a Matriz de Embedding ($W_{emb} \in \mathbb{R}^{V \times d_{model}}$), onde $V$ é o tamanho do vocabulário e $d_{model}$ é a dimensão oculta do modelo (ex: $4096$ no LLaMA-7B).

Cada linha dessa matriz corresponde a um token do vocabulário. A operação de "conversão" não passa de uma indexação em hardware (equivalente a uma multiplicação por um vetor one-hot):

$$x_i = W_{emb}[ID_{token}]$$

Onde $x_i \in \mathbb{R}^{d_{model}}$ é o vetor denso que entrará diretamente nos barramentos de execução das camadas de atenção.

O Impacto no Silício: Memória, Latência e Bugs de Borda

Compreender tokens não é apenas teoria algorítmica; é uma necessidade de otimização de infraestrutura de hardware.

Eficiência de Memória e Throughput de Cache

A eficiência de um tokenizador é medida pela sua taxa de compressão (número de bytes por token). Se um tokenizador precisa de 3 tokens para codificar a palavra "configuração", ele está desperdiçando preciosa largura de banda de memória HBM e aumentando o número de passos autoregressivos que a GPU precisa realizar.

A arquitetura do Transformer exige que, a cada novo token gerado, todo o histórico anterior seja lido da memória global para os caches locais (o chamado mecanismo de KV Cache). Menos eficiência na tokenização implica diretamente em:

  • Menor throughput de tokens por segundo.
  • Estouro precoce da janela de contexto alocada na VRAM.
  • Aumento de latência no tempo até o primeiro token (Time to First Token - TTFT).

Vulnerabilidades de Engenharia induzidas por Tokens

A abstração dos tokens cria anomalias matemáticas conhecidas na comunidade de engenharia reversa de LLMs. Como o modelo nunca vê caracteres individuais, palavras semanticamente idênticas com capitalizações ou espaços diferentes geram IDs completamente distintos, apontando para linhas totalmente diferentes na matriz $W_{emb}$.

O Caso do Token "SolidGoldMagikarp": Em modelos como o GPT-3, certas strings associadas a nomes de usuários do Reddit foram repetidas exaustivamente em datasets de raspagem web sem o devido tratamento. O BPE criou tokens dedicados para elas. No entanto, durante o ajuste fino (fine-tuning) e alinhamento do modelo, esses tokens raramente foram ativados. O resultado? O vetor na matriz de embedding ficou isolado, e ao enviar essa string específica, os pesos geravam ativações bizarras, quebrando o comportamento determinístico do modelo e induzindo alucinações catastróficas.

Ao projetar sistemas de IA de alta performance, a escolha do tokenizador dita o limite físico da eficiência do modelo. Otimizar a tokenização é o passo mais barato e frequentemente o mais negligenciado, para extrair o máximo de performance do hardware de inferência.