Saltar para o conteúdo
17/30Capítulo 17 de 30

Temperatura, top-p e o determinismo que não tem

A temperatura divide os logits antes da softmax — e isso desmonta a ideia de um botão de criatividade.

Nesta página

Eis o mesmo pedido enviado cinco vezes ao mesmo modelo. Os mesmos pesos, o mesmo prompt, a mesma máquina, a mesma seed aleatória. A única coisa que muda é um número.

TEXT
prompt: "Q: What is the capital of France?\nA:"

T = 0.0   " Paris\nWhat is the question and does the answer answer it? The
           question is: What is the capital of France?..."

T = 0.7   " Paris\nWhat is the question: Which city is the capital of
           France?..."

T = 1.0   " Paris\nWhat is a good geographical qualifier for describing
           Paris concerning its location?\nA: Near the Mediterranean Sea..."

T = 1.5   " Paris\nWhat clue from premise allows we to conclude that Godwin
           was &, He chose Healing Crimson Colour No:white flour Pure..."

T = 2.0   "安全感金华.ITEMT]]];\naims assume parental.st-importe.valtermination
           Screens قطر_Zeroหมายเลข-zA ('$ספטמבר..."

Nada se estragou. Todos os token na última linha foram extraídos legitimamente da distribuição de probabilidades do próprio modelo sobre o seu vocabulário de 151.936 entradas. O número que mudou chama-se temperatura, é descrito na maioria da documentação como um seletor de criatividade, e essa descrição está errada de uma forma que este capítulo pode demonstrar, não apenas afirmar.

Este é também o capítulo em que três promessas anteriores vencem. O Capítulo 4 definiu o logit e nunca chegou realmente a gastá-lo. A caixa sobre vírgula flutuante do Capítulo 2 terminou com uma instrução — lembre-se disto quando o Capítulo 17 perguntar porque é que o mesmo prompt, modelo e seed podem produzir token diferentes. E a caixa sobre mixture-of-experts do Capítulo 9 prometeu um catálogo de quatro causas de não determinismo. As três chegam abaixo.

O Capítulo 4 apresentou o logit como uma pontuação real não normalizada, uma por classe. O Capítulo 8 fez um modelo de linguagem produzir uma por entrada do vocabulário. A softmax transforma esse vetor z\mathbf{z} em probabilidades:

pi=ezijezjp_i = \frac{e^{z_i}}{\sum_j e^{z_j}}

A temperatura entra aqui — o nome é emprestado da física estatística, onde o mesmo parâmetro controla a nitidez com que uma distribuição de Boltzmann se concentra nos seus estados de baixa energia1 — e divide os logits antes da exponencial:

pi(T)=ezi/Tjezj/Tp_i(T) = \frac{e^{z_i/T}}{\sum_j e^{z_j/T}}

Essa posição é todo o mecanismo, e vale duas linhas de álgebra para perceber porque não poderia estar em mais lado nenhum. Suponha que tentava aplicar a temperatura às probabilidades — escalá-las por 1/T1/T e renormalizar. Obteria

pi/Tjpj/T=pijpj=pi\frac{p_i/T}{\sum_j p_j/T} = \frac{p_i}{\sum_j p_j} = p_i

A constante cancela. Escalar probabilidades não faz absolutamente nada; a distribuição volta inalterada. A temperatura só tem efeito porque atua sobre o expoente, onde dividir por TT antes de exponenciar é o mesmo que elevar cada probabilidade à potência 1/T1/T — uma remodelação não linear que altera os rácios entre entradas, não a sua escala comum.

Dessa posição, ambos os limites seguem sem mais trabalho. Quando T0T \to 0, o maior logit afasta-se do resto e pp colapsa no único token com maior pontuação: greedy decoding. Quando TT cresce, cada zi/Tz_i/T tende para zero, cada exponencial tende para 1, e a distribuição achata-se em direção a uma uniforme sobre todo o vocabulário. Exatamente em T=0T = 0, a fórmula divide por zero, por isso todas as implementações tratam esse caso à parte como o máximo aritmético — incluindo o widget abaixo, que muda para argmax em T0.001T \le 0.001.

Um aviso, porque a colisão de nomes causa confusão real. Há uma segunda coisa, não relacionada, chamada temperatura em machine learning: temperature scaling, um método de calibração que ajusta um valor num conjunto de validação para que a confiança de um classificador corresponda à sua precisão.2 Mesma fórmula, nada a ver com geração. Artigos que dizem «temperatura» muitas vezes querem dizer essa; este capítulo nunca quer.

Eis essa distribuição, com a aritmética à sua frente. Os logits são fixos e plausíveis, por isso os números no texto abaixo podem ser verificados contra o que vê:

  • ␣Paris96.9%
  • ␣the1.3%
  • ␣located0.8%
  • ␣a0.5%
  • ␣Lyon0.2%
  • ␣called0.1%
  • ␣home0.1%
  • ␣Marseille0.0%
  • ␣not0.0%
  • ␣banana0.0%

10 de 10 tokens sobrevivem ao corte e partilham a probabilidade.

Ver os dados em tabela
TokenlogitApós a temperaturaApós o corte
␣Paris⁨9.4⁩96.90%96.90%
␣the⁨5.1⁩1.31%1.31%
␣located⁨4.6⁩0.80%0.80%
␣a⁨4.1⁩0.48%0.48%
␣Lyon⁨3.2⁩0.20%0.20%
␣called⁨2.9⁩0.15%0.15%
␣home⁨2.4⁩0.09%0.09%
␣Marseille⁨1.8⁩0.05%0.05%
␣not⁨1.1⁩0.02%0.02%
␣banana⁨-2.6⁩0.00%0.00%
Amostragem: temperatura, top-p e top-k

Dez continuações candidatas de The capital of France is, à temperatura 1 sem cortes. ␣Paris detém 96,90 % da massa; ␣banana, no fundo com um logit de 2.6-2.6, recebe 0,00 %. Deslize a temperatura para 0 e sobrevive um token com 100 %. Deslize-a para 2 e ␣Paris cai para 69,81 %, enquanto ␣banana sobe para 0,17 % — o token rejeitado pelo modelo, a receber probabilidade real por um botão que o leitor rodou.

O número ␣banana é todo o argumento em miniatura: aumentar a temperatura não consegue dar a um modelo uma ideia que ele não teve. Os logits já estão calculados, a ordenação já está fixa, e a temperatura preserva-a exatamente — nenhum calor faz alguma vez um token com pontuação inferior passar acima de outro com pontuação superior. Tudo o que faz é redistribuir massa para baixo na ordenação que o próprio modelo produziu. Alta temperatura não torna um modelo mais inventivo; torna-o mais propenso a emitir os tokens que pontuou como maus.

Num vocabulário real, isto deixa de ser uma curiosidade e torna-se a razão pela qual output de alta temperatura é inutilizável. Medido em Qwen/Qwen2.5-0.5B-Instruct, uma forward pass, o prompt acima, contando quantos tokens são necessários para acumular uma dada fração da massa de probabilidade:

temperaturaprobabilidade top-1entropiatokens com 80 %90 %95 %99 %
0,599,98 %0,00 nats1111
0,799,65 %0,03 nats1111
1,096,01 %0,30 nats11114
1,288,20 %0,88 nats1213252
1,562,83 %3,07 nats293532.67226.787
2,016,62 %8,19 nats13.51632.96655.231101.205

Leia a última linha devagar. Em T=2T = 2, numa pergunta com exatamente uma resposta correta, 32.966 tokens diferentes partilham os 90 % superiores da massa de probabilidade. Isto não é um espaço criativo mais amplo. É um modelo a que a aritmética disse para tratar uma partícula coreana e um identificador C++ como opções vivas para a palavra depois de A:. O lixo no bloco de abertura é a consequência direta, e não é um bug no modelo nem na biblioteca — é o que o pedido pediu.

O intervalo útil é estreito e depende da tarefa, não do gosto. Numa pergunta factual, a resposta é um token e qualquer calor acima de cerca de 1,2 injeta erro sem benefício. Numa pergunta em aberto, há de facto mais do que uma boa continuação, e algum calor compra variedade que continua fluente:

TEXT
"Write a two-sentence story about a lighthouse."

T = 0.0  "The lighthouse stood tall and proud, its beacon illuminating the
          night sky above. A lone sailor, his eyes fixed on the distant
          horizon..."

T = 0.7  "In the quiet, stormy waters of the sea, a lighthouse stood
          sentinel over the horizon, its golden dome casting a warm glow
          on the fog-shrouded streets below..."

T = 1.0  "In the quiet night, a lone lighthouse stood sentinel over the
          sea, its shining beacon a beacon of hope and solace for sailors
          and fishermen across the vast and endless ocean..."

T = 1.3  "In the gentle sunlight, now reflecting upon the opening of Jack's
          lighthouse, Jim Trahan, a small-time individual difficult to
          define in paperwork, wondered about a career where simplicity
          reigns..."

A 1,3, o modelo inventou um nome próprio e uma frase que não se analisa sintaticamente. A faixa entre «idêntico sempre» e «incoerente» fica aproximadamente entre 0,6 e 1,1 para este modelo nesta tarefa, e o conselho honesto é encontrá-la medindo na sua tarefa, não copiando um número de um artigo de blog.

Há uma pergunta óbvia escondida sob tudo isto: se o modelo tem uma distribuição de probabilidades e um token é o mais provável, porque não escolhê-lo sempre? Greedy decoding é grátis, reprodutível e não precisa de parâmetros.

Porque o resultado é isto:

TEXT
prompt: "In a shocking finding, scientists discovered a herd of unicorns
         living in a remote valley."

greedy: " The unicorns were so rare that they were not even recognized by
         the local people. The unicorns were so rare that they were not
         even recognized by the local people. The unicorns were so rare
         that they were not even recognized by the local people. ..."

         repeated 4-grams: 87.6 %

Oito frases, uma frase. Quase nove em cada dez janelas de quatro tokens já tinham aparecido antes no mesmo output. Isto é degeneração de texto neural, nomeada e explicada por Holtzman et al. no artigo que introduziu top-p.3 O modelo não está estragado; maximizar a probabilidade da sequência é simplesmente o objetivo errado para texto em aberto. A escrita humana não é a sequência de palavras mais provável — carrega surpresa, com a sua probabilidade por token a vaguear, descer e recuperar — enquanto o caminho de probabilidade máxima é um ponto fixo que, uma vez alcançado, não tem razão para sair.

É por isso que a amostragem existe. É também, e esta é a parte que fica de fora, não uma lei universal. O Capítulo 12 mediu 24 de 24 respostas corretas em problemas de palavras de dois passos com greedy decoding simples, e a amostragem a temperatura 0,8 fez isso descer para 81 %; a self-consistency depois gastou seis vezes os tokens para regressar ao ponto onde o greedy já estava. Ambas as coisas são verdade ao mesmo tempo:

Geração em aberto. Não há uma única continuação certa, por isso a mais provável é uma armadilha — entra em loop, e 87,6 % dela é copiada de si própria. Faça amostragem.

Tarefas com uma resposta certa. uma única continuação correta, por isso extrair qualquer outra coisa é extrair um erro. Os 100 % do Capítulo 12 tornaram-se 81 % exatamente por esta razão. Não faça amostragem.

A maioria dos prompts de produção são do segundo tipo e são configurados como o primeiro, porque a temperatura ficou no valor que o código de exemplo usava.

Amostrar a partir da distribuição completa não é o que alguém faz realmente, porque a cauda é enorme e cheia de disparates. Alguma coisa tem de ser cortada. Há duas respostas clássicas e diferem num aspeto que decide tudo.

Top-k mantém um número fixo de candidatos. Ordene por probabilidade, mantenha os primeiros kk, descarte o resto, renormalize.4 Top-p, também chamado nucleus sampling, mantém uma quantidade fixa de massa: pegue em tokens por ordem descendente até a sua probabilidade cumulativa atingir pp, e pare.3 Formalmente, o núcleo é o menor conjunto VpV_p com

iVppip\sum_{i \in V_p} p_i \ge p

A diferença parece cosmética e não é, porque os dois prompts que envia no mesmo minuto têm formas de distribuição completamente diferentes. Ambos são o mesmo modelo à temperatura 1:

Q: What is the capital of France?\nA:Once upon a time,
probabilidade top-196,01 %25,39 %
tokens com 90 % da massa1467
top-k = 40 mantém99,61 % da massa78,87 % da massa
massa nas posições 2 a 403,61 %53,48 %
token na posição 40␣Av, 0,0093 %␣Dr, 0,128 %

Um kk fixo, duas falhas em direções opostas. No prompt factual, k=40k = 40 admite 39 tokens que, juntos, valem 3,6 % — deixa passar lixo, incluindo um candidato a nove milésimos de um por cento, porque a regra conta lugares e não evidência. No prompt de história, o mesmo k=40k = 40 deita fora 21 % da massa que o modelo atribuiu genuinamente, porque o núcleo real ali tem 467 tokens de largura.

Top-p faz um único número cumprir ambas as funções. Defina p=0.9p = 0.9 e ele mantém 1 token no primeiro prompt e 467 no segundo, porque faz uma pergunta sobre a distribuição em vez de lhe impor uma contagem. Veja essa adaptação diretamente — mesmo corte, quatro temperaturas:

  • ␣Paris91.1%
  • ␣the5.2%
  • ␣located3.7%
  • ␣a0.0%
  • ␣Lyon0.0%
  • ␣called0.0%
  • ␣home0.0%
  • ␣Marseille0.0%
  • ␣not0.0%
  • ␣banana0.0%

3 de 10 tokens sobrevivem ao corte e partilham a probabilidade.

Ver os dados em tabela
TokenlogitApós a temperaturaApós o corte
␣Paris⁨9.4⁩85.03%91.10%
␣the⁨5.1⁩4.84%5.18%
␣located⁨4.6⁩3.47%3.71%
␣a⁨4.1⁩2.48%
␣Lyon⁨3.2⁩1.36%
␣called⁨2.9⁩1.12%
␣home⁨2.4⁩0.80%
␣Marseille⁨1.8⁩0.54%
␣not⁨1.1⁩0.34%
␣banana⁨-2.6⁩0.03%
Amostragem: temperatura, top-p e top-k

Top-p a 0,90 com a temperatura a 1,5: três dos dez tokens sobrevivem e partilham a massa, ␣Paris renormalizado para 91,10 %. Agora mova apenas a temperatura. A 0,7, o mesmo 0,90 deixa um sobrevivente — um núcleo tão estreito é greedy decoding com outro nome. A 2,0, deixa cinco. O corte nunca se moveu; a forma por baixo dele é que mudou.

Esse widget também resolve uma ideia errada que vale a pena nomear, porque custa dinheiro real. Numa distribuição confiante, top_p = 0.9 não é «um pouco de variedade». É greedy. À temperatura 1, o token líder aqui detém 96,90 %, que já está acima de 0,9, por isso o núcleo tem um token de largura e nada mais pode alguma vez ser extraído. Equipas definem top_p como 0,9 acreditando que afrouxaram alguma coisa e depois perguntam-se porque todas as respostas são idênticas.

Defina top-k em vez disso e a falha oposta é igualmente visível:

  • ␣Paris97.2%
  • ␣the1.3%
  • ␣located0.8%
  • ␣a0.5%
  • ␣Lyon0.2%
  • ␣called0.0%
  • ␣home0.0%
  • ␣Marseille0.0%
  • ␣not0.0%
  • ␣banana0.0%

5 de 10 tokens sobrevivem ao corte e partilham a probabilidade.

Ver os dados em tabela
TokenlogitApós a temperaturaApós o corte
␣Paris⁨9.4⁩96.90%97.20%
␣the⁨5.1⁩1.31%1.32%
␣located⁨4.6⁩0.80%0.80%
␣a⁨4.1⁩0.48%0.49%
␣Lyon⁨3.2⁩0.20%0.20%
␣called⁨2.9⁩0.15%
␣home⁨2.4⁩0.09%
␣Marseille⁨1.8⁩0.05%
␣not⁨1.1⁩0.02%
␣banana⁨-2.6⁩0.00%
Amostragem: temperatura, top-p e top-k

Top-k a 5, sem top-p. Cinco tokens sobrevivem a todas as temperaturas, porque cinco foi o que foi pedido. À temperatura 1, como mostrado, os quatro candidatos abaixo de ␣Paris valem 2,79 % entre si. Baixe para 0,7 e os mesmos quatro valem 0,38 % — o corte é teatro, e o modelo é efetivamente greedy. Suba para 2,0 e eles valem 22,54 %. Mesma configuração, mesma contagem de sobreviventes, três comportamentos completamente diferentes, e nada no pedido lhe diz qual está a receber.

As penalizações, com as fórmulas, porque confundi-las é endémico

Ligação para a secção: As penalizações, com as fórmulas, porque confundi-las é endémico

Três mecanismos diferentes circulam com nomes semelhantes, fazem coisas diferentes, e a diferença é mensurável. Seja cic_i o número de vezes que o token ii já apareceu.

ziziα1[ci>0]z_i \leftarrow z_i - \alpha \cdot \mathbb{1}[c_i > 0]

Subtraia uma constante a qualquer token que já tenha aparecido. Aparecer uma vez e aparecer quarenta vezes são penalizados de forma idêntica. É um interruptor, não um seletor.

ziziβciz_i \leftarrow z_i - \beta \, c_i

Subtraia proporcionalmente à contagem. Um token usado quatro vezes é penalizado quatro vezes mais do que um token usado uma vez, e a pressão acumula-se à medida que o texto cresce.

zi{zi/ρif zi>0ziρif zi0z_i \leftarrow \begin{cases} z_i / \rho & \text{if } z_i > 0 \\ z_i \cdot \rho & \text{if } z_i \le 0 \end{cases}

A original, do artigo CTRL.7 Divide em vez de subtrair, com o caso do sinal necessário porque dividir um logit negativo torná-lo-ia maior. A sua força depende, portanto, da magnitude do logit, o que significa que o mesmo ρ\rho atinge de forma diferente em pontos diferentes da mesma frase.

A mesma continuação degenerada de antes, com cada uma aplicada. «Passos alterados» conta quantos dos 120 passos de geração escolheram um token diferente daquele que o modelo sem penalização teria escolhido. A execução tem aqui 120 passos contra 140 no bloco acima, razão pela qual a baseline sem penalização lê 85,5 % em vez de 87,6 %:

configuração4-grams repetidospassos alterados
nada85,5 %0 / 120
presence 0,565,0 %3 / 120
presence 1,03,4 %11 / 120
frequency 0,56,0 %12 / 120
frequency 1,00,0 %20 / 120
repetition 1,2 (CTRL)0,0 %35 / 120

Três coisas emergem. Presence a 0,5 mudou três decisões em 120 e cortou a repetição em um quarto — o loop era mantido por um punhado de tokens. Frequency a 0,5 mudou quatro vezes mais decisões para um efeito muito maior, porque o multiplicador da contagem continua a crescer enquanto a constante de presence não cresce. E a penalização CTRL no valor muito copiado de 1,2 reescreveu 35 de 120 decisões, o que não é um empurrão; é um modelo diferente.

Esse último número prepara a falha de que ninguém avisa.

Código repete. Tabelas repetem. Listas repetem. Output estruturado repete por definição — é isso que é estrutura. Uma penalização não consegue distinguir um modelo preso num loop de um modelo que emite corretamente a quarta linha de uma tabela, porque ambos parecem um token a aparecer outra vez.

As mesmas três tarefas, geradas de três formas:

tarefanadafrequency 0,5repetition 1,2
tabela markdown, 6 linhas0 / 56 passos alterados0 / 562 / 62
função Python0 / 930 / 9310 / 110
lista com marcadores, 1 a 120 / 500 / 500 / 50

A frequency penalty a 0,5 acabou por ser inofensiva nas três, o que é um resultado útil e ligeiramente surpreendente, e diz algo preciso: como nenhuma decisão mudou, os tokens estruturais devem ter ganho as suas posições por mais do que a penalização subtraiu, mesmo depois de aparecerem cinco e seis vezes. A penalização CTRL, que divide, desloca-os, e eis o que produziu:

TEXT
repetition 1.2, markdown table:
  | n | 2^n |
  | --- | --- |
  | 0 | 1      |
  | 1 | 2       |
  | 2 | 4       |

O alinhamento desfaz-se: a quantidade de preenchimento dentro de cada célula muda de linha para linha, porque a sequência de espaços antes da barra vertical de fecho é exatamente o tipo de repetição que a penalização existe para quebrar. Cosmético, e custou seis tokens extra. O caso Python não é cosmético:

TEXT
nothing / frequency 0.5:
      total = 0
      for i in range(1, n + 1):
          total += i ** 2
      return total

repetition 1.2:
      # Initialize total_sum with 0
      total_sum = 0
      # Loop through numbers from 1 to n, incrementing by 2 each time
      for i in range(1, n + 1,

A penalização empurrou o modelo para fora de total — já usado na docstring — para total_sum, preencheu o output com comentários inventados para gastar o seu orçamento em tokens não usados, e depois entrou num range de três argumentos com um passo. O comentário diz incrementing by 2 each time, o que é errado para uma soma de quadrados de 1 a nn. Uma repetition penalty produziu código incorreto a partir de um prompt que foi respondido corretamente sem ela.

A regra que se segue é curta: as penalizações são para prosa em aberto, e devem estar desligadas para código, output estruturado, dados tabulares e tudo o que tenha um schema. O Capítulo 18 trata exatamente dessa segunda categoria.

Todas as implementações reais aplicam isto numa sequência específica:

penalizações → temperatura → top-k → top-p → amostrar

Isto não é contabilidade arbitrária, e trocar duas etapas produz distribuições genuinamente diferentes. Duas medições, ambas no prompt factual.

Cortar antes ou depois da temperatura. O núcleo é calculado sobre a distribuição que lhe é entregue, e a temperatura altera radicalmente essa distribuição:

top-p 0,9 depois da temperaturatop-p 0,9 antes da temperatura
T=1.0T = 1.01 token1 token
T=1.5T = 1.5353 tokens1 token
T=2.0T = 2.032.966 tokens1 token

Em T=2T = 2, a mesma configuração nominal produz um conjunto candidato de 32.966 ou de 1, dependendo apenas de que etapa corre primeiro. Se alguma vez se perguntou porque aumentar a temperatura «não faz nada» num fornecedor e destrói o output noutro com os mesmos dois números, esta tabela é uma resposta plausível.

Penalizar antes ou depois da temperatura. Subtrair uma penalização α\alpha e depois dividir por TT dá uma penalização efetiva de α/T\alpha/T; dividir primeiro e depois subtrair dá α\alpha. Com uma presence penalty de 1,0 aplicada ao token líder:

temperaturapenalizar, depois temperartemperar, depois penalizar
0,599,858 %99,948 %
1,089,839 %89,839 %
2,010,783 %6,830 %

Idênticas em T=1T = 1, como têm de ser. Separadas por um fator de 1,58 em T=2T = 2. «Presence penalty 1.0» não é uma quantidade de penalização bem definida a menos que também saiba onde a temperatura é aplicada, e nenhuma API documenta isto.

Mostrar detalhes

Opcional: todo o pipeline, pela ordem acima.

Dezasseis linhas, e tudo neste capítulo está nelas. É o mesmo cálculo que o widget executa, sobre um vetor real de logits em vez de dez números fixos.

sample.pyPYTHON
def sample(logits, counts, presence=0.0, frequency=0.0,
           temperature=1.0, top_k=0, top_p=1.0, generator=None):
    z = logits.clone()

    idx = torch.tensor(list(counts))                       # 1. penalties
    if len(idx):
        z[idx] -= presence
        z[idx] -= frequency * torch.tensor([float(c) for c in counts.values()])

    if temperature <= 0:                                   # 2. temperature
        return int(z.argmax())                             #    T=0 is argmax
    p = torch.softmax(z / temperature, -1)

    p, order = p.sort(descending=True)
    if top_k:                                              # 3. top-k
        p[top_k:] = 0
    p = p * ((p.cumsum(0) - p) < top_p)                     # 4. top-p

    p = p / p.sum()                                        # 5. renormalise
    return int(order[torch.multinomial(p, 1, generator=generator)])

O cumsum(0) - p na linha de top-p é a massa cumulativa excluindo o token atual, que é o que faz o núcleo incluir o token que atravessa o limiar em vez de parar imediatamente antes. Erre isso por um e top_p = 0.9 torna-se silenciosamente um corte ligeiramente mais apertado do que em todas as outras implementações.

Este é um dos poucos lugares na segunda metade do curso em que Python é a linguagem certa, e a razão é estrutural, não estilística: cada linha acima precisa do vetor completo de logits nas suas mãos, e sobre uma API HTTP esse vetor não existe. Pode enviar temperature e top_p a um fornecedor; não consegue implementá-los, e não consegue ver o que fizeram.

Cada fornecedor aceita um subconjunto diferente destes controlos, com intervalos diferentes, e ignora o resto em silêncio. Isto não é uma queixa abstrata. Qualquer aplicação que ofereça uma escolha de modelo tem de escrever as diferenças algures, e o ficheiro onde o faz é um mapa da incompatibilidade. Eis o que um desses catálogos declara para um único parâmetro nas nove fontes de texto que suporta:

intervalo de temperatura declaradofontes
0 a 1Anthropic, Google, Meta, Cerebras, PaLM
0 a 1,5Mistral
0 a 2OpenAI, DeepSeek, xAI

A palavra é a mesma; a escala não é. Uma «temperatura de 1» é a distribuição não modificada num caso e o calor máximo permitido noutro, e metade do catálogo não consegue expressar o valor que a outra metade trata como neutro-mais-um-pouco. O resto dos controlos é igualmente desigual: as entradas OpenAI, DeepSeek e xAI aceitam presence e frequency penalties e nenhum topK; as entradas Google, Meta, Cerebras e PaLM aceitam topK e nenhuma penalização; a Anthropic aceita topK, topP e sequências de paragem e nenhuma penalização; e exatamente uma das nove — Mistral — aceita uma seed. Enviar um parâmetro que um fornecedor não implementa geralmente não produz erro nenhum: o pedido tem sucesso, o botão não faz nada, e conclui que a configuração não tem efeito.

E repare no que tal ficheiro é: uma alegação sobre a API de outra pessoa, escrita num dia específico, que nada verifica depois. Um catálogo que diz 0 a 1 para um fornecedor que agora aceita 0 a 2 limitará silenciosamente todos os pedidos.

Mais dois controlos pertencem à mesma família. logprobs, quando disponibilizado, devolve as log-probabilidades do token escolhido e muitas vezes as principais alternativas — a única janela que obtém para a distribuição de que este capítulo trata, e a base de todas as heurísticas de confiança construídas sobre um modelo fechado. E máximo de tokens mais sequências de paragem terminam a geração sem referência nenhuma à probabilidade: um limite rígido e uma correspondência de string. Ambos aparecem como o finish_reason do Capítulo 14, onde length significa que a sua resposta foi cortada a meio de uma frase por um orçamento, não terminada pelo modelo.

Defina uma seed e a amostragem torna-se reprodutível. Essa parte é real, e é fácil de verificar:

TEXT
seed = 1234  " Paris\nWhat is a good geographical qualifier for describing
               Paris concerning its location?\nA: Near the Mediterranean Sea"
seed = 1234  " Paris\nWhat is a good geographical qualifier for describing
               Paris concerning its location?\nA: Near the Mediterranean Sea"
seed = 7     " Paris is the capital of France. The appellation of Paris is
               \"Île de Paris\"."
seed = 7     " Paris is the capital of France. The appellation of Paris is
               \"Île de Paris\"."

Idêntico byte a byte dentro de uma seed, diferente entre seeds, exatamente como anunciado. Portanto, o que a seed fixa é o sorteio aleatório na última linha dessa função sample — que token é escolhido dada uma distribuição.

O que ela não fixa é a distribuição. E é aí que está o problema, porque o vetor de logits que o seu modelo produz não é um objeto matemático; é o output de milhares de milhões de somas de vírgula flutuante, e essas têm uma ordem.

O Capítulo 2 deixou esta experiência pronta. O mesmo milhão de números float32, somados em agrupamentos diferentes:

TEXT
sequential          998.564270020    error vs float64: 6.393e-03
pairwise (numpy)    998.570556641    error vs float64: 1.061e-04
in 4 chunks         998.570495605    error vs float64: 1.672e-04
in 8 chunks         998.570556641    error vs float64: 1.061e-04
in 16 chunks        998.570678711    error vs float64: 1.594e-05

sequential == pairwise?  False
4 chunks == 8 chunks?    False

Olhe para a última linha. O número de chunks muda a resposta. Isto não é uma curiosidade sobre numpy; é o mecanismo, porque quando um servidor de inferência divide uma redução por mais ou menos unidades paralelas, está a fazer precisamente isto. E um servidor divide de acordo com quantos pedidos está a servir.

Eis esse efeito no próprio modelo. O mesmo prompt, a mesma forward pass, sendo a única diferença quantos outros pedidos por acaso estavam no batch:

TEXT
20 identical forward passes, batch of 1:  20 / 20 bit-for-bit identical

the same prompt inside a batch of  2:  147,321 of 151,936 logits differ
the same prompt inside a batch of  4:  146,515 of 151,936 logits differ
the same prompt inside a batch of  8:  146,515 of 151,936 logits differ
the same prompt inside a batch of 16:  147,321 of 151,936 logits differ

largest change to any logit: 2.5e-05

A correr sozinho, o modelo é perfeitamente determinístico — vinte passagens, idênticas bit a bit. Coloque o prompt idêntico num batch com pedidos não relacionados e 97 % dos seus logits mudam. Nada no seu pedido mudou. Chegou o pedido de outra pessoa.

Agora a parte honesta, porque isto costuma ser contado como se fosse o fim da história. Uma alteração de 2.5×1052.5 \times 10^{-5} só altera o output se dois tokens candidatos estiverem a essa distância um do outro. Ao longo de 717 passos de geração em doze prompts, o menor intervalo entre os dois logits superiores foi 2.5×1032.5 \times 10^{-3} — cem vezes maior do que a perturbação — e nenhum passo esteve perto o suficiente para virar. Portanto, neste modelo, em float32, num portátil, o batching moveu todos os logits e não mudou nenhum token.

Isto é uma descrição de condições favoráveis, não uma garantia, e uma alteração a essas condições basta:

TEXT
same weights, same prompts, greedy decoding, no seed involved
float32 vs bfloat16:   6 of 8 answers diverge
                       first divergence at step 23, on average

  float32: "...it is scattered and dispersed into different colors,
            including blue. The blue light is scattered more than other
            colors, so it appears to come from the sky."

  bfloat16: "...it is scattered and scattered, causing the colors of the
             sun to be scattered and scattered, creating the appearance
             of a blue color."

Seis de oito respostas divergem, e uma degrada-se bastante. A tabela do Capítulo 2 diz porquê: bfloat16 mantém 7 bits de mantissa, por isso, perto de uma magnitude de logit de 16, os valores representáveis estão separados por 0,125 — 16,0, depois 16,125, depois 16,25 — e o arredondamento pode mover um logit até 0,0625. Entretanto, 4,7 % dos passos de geração medidos acima tinham um intervalo top-two inferior a 0,1. Essa é toda a diferença entre as duas experiências: em float32 a perturbação era cem vezes menor do que a decisão mais próxima, e em bfloat16 tem o mesmo tamanho. A inferência de produção corre em 16 bits, em hardware com kernels fundidos e ordens de redução que ninguém promete manter. Se «o ruído numérico é negligenciável» é uma pergunta sobre precisão e hardware, não sobre o modelo.

Portanto, as quatro causas, catalogadas como o Capítulo 9 prometeu:

A caixa do Capítulo 2. A ordem de uma soma altera o seu valor, por isso qualquer alteração na forma como uma redução é dividida altera os logits. Este é o substrato; as outras três são formas de alterar a ordem.

Dynamic batching agrupa o seu pedido com os de desconhecidos

Ligação para a secção: Dynamic batching agrupa o seu pedido com os de desconhecidos

Continuous batching, do Capítulo 13, é a razão pela qual a inferência é acessível — e significa que a forma das matrizes por onde os seus tokens passam depende do tráfego. Medido acima: 147.321 logits moveram-se porque o tamanho do batch mudou.

A caixa do Capítulo 9 já o disse. O router faz uma escolha discreta por token e por camada, sujeita a limites de capacidade por especialista calculados sobre o batch. Um token que teria ido sozinho para o especialista 7 vai para o especialista 12 acompanhado. Isto não é uma diferença de arredondamento; é um conjunto diferente de pesos.

Uma string de versão como -latest é um ponteiro, e ponteiros são redirecionados. Os fornecedores também atualizam a stack de serving sob um identificador de versão fixo. Nada disto é anunciado com a granularidade que lhe permitiria correlacioná-lo com a mudança do seu próprio output.

O parâmetro seed da OpenAI é honesto sobre isto da única forma que pode ser: vem acompanhado de um campo system_fingerprint que identifica a configuração de backend, e a documentação afirma que o determinismo é best-effort e que uma fingerprint alterada significa que os resultados podem diferir. Leia isso pelo que é — um fornecedor a dizer-lhe que controla todas as quatro causas acima, que o utilizador não controla nenhuma, e que a única coisa que pode oferecer é dizer-lhe depois do facto que algo mudou.

Tudo aqui foi sobre um botão e as suas consequências. Recuando um nível, aparece o problema mais difícil: o objeto que estivemos a afinar é uma distribuição de probabilidades, e distribuições de probabilidades não têm interface.

Uma function call tem. Uma linha de base de dados tem. Um handler POST que espera um corpo JSON com três campos obrigatórios tem, e rejeitará tudo o resto. Entre o modelo e todos os outros componentes do seu sistema há um contrato sobre o qual um dos lados não consegue fazer promessas: o modelo produzirá algo, extraído de uma distribuição que moldou mas não fixou, e o código do outro lado precisa de um valor de tipo conhecido ou lança erro.

A ponte entre esses dois mundos é construída com o material deste capítulo, não com parsing e retentativas. Se um token quebraria a estrutura necessária, não o amostra e espera — define o seu logit como -\infty antes de a softmax sequer o ver. Constrained decoding é uma máscara sobre o mesmo vetor que passámos este capítulo a remodelar, e transforma «responda em JSON, por favor» de um pedido numa garantia.

O Capítulo 18 é esse contrato: tool calling, JSON Schema, outputs estruturados, e o que é preciso para tornar um sistema determinístico seguro para construir sobre um sistema probabilístico.


Todas as medições neste capítulo vêm de Qwen/Qwen2.5-0.5B-Instruct em CPU, float32 salvo indicação em contrário, com a amostragem implementada como escrito na secção opcional, em vez de delegada a uma biblioteca. São de um modelo pequeno, e os valores específicos são dele; os mecanismos não são. How to generate text with different decoding methods, de Von Platen (Hugging Face, 2020), é o artigo contra o qual este é medido e continua a ser a melhor introdução curta ao mesmo material. Para a secção sobre determinismo: as notas de reprodutibilidade do PyTorch descrevem o que uma seed fixa e não fixa numa única máquina, a documentação da OpenAI sobre seed e system_fingerprint descreve o que um fornecedor pode e não pode prometer, e a discussão de 2025 da Thinking Machines sobre kernels invariantes ao batch é o relato público mais claro de porque corrigir isto ao nível do servidor de inferência é possível, mas não é gratuito.

  1. Ackley, D. H., Hinton, G. E. e Sejnowski, T. J. A Learning Algorithm for Boltzmann Machines. Cognitive Science 9(1), pp. 147–169 (1985), onde a temperatura numa softmax vem da física estatística. Hinton, G., Vinyals, O. e Dean, J., Distilling the Knowledge in a Neural Network, arXiv:1503.02531 (2015), secção 2, é onde o mesmo parâmetro reaparece no deep learning moderno — como forma de expor a distribuição completa de um professor, que são as soft labels do Capítulo 13, não a amostragem deste capítulo.

  2. Guo, C., Pleiss, G., Sun, Y. e Weinberger, K. Q. On Calibration of Modern Neural Networks. arXiv:1706.04599 (2017). Não confunda isto com a temperatura deste capítulo. Temperature scaling ajusta um único valor num conjunto de validação para que a confiança do modelo corresponda à sua precisão; é um método de calibração post-hoc aplicado aos outputs de um classificador. Temperature sampling é um controlo em runtime sobre como um gerador extrai tokens. Mesma fórmula, propósito diferente, e nenhum valor partilhado.

  3. Holtzman, A., Buys, J., Du, L., Forbes, M. e Choi, Y. The Curious Case of Neural Text Degeneration. arXiv:1904.09751 (2019). Introduz nucleus sampling e a medição de que decoding baseado em maximização produz texto cujo perfil de probabilidade não se parece nada com texto humano. 2

  4. Fan, A., Lewis, M. e Dauphin, Y. Hierarchical Neural Story Generation. arXiv:1805.04833 (2018). O artigo que popularizou top-k sampling.

  5. Nguyen, M. et al. Turning Up the Heat: Min-p Sampling for Creative and Coherent LLM Outputs. arXiv:2407.01082 (2024).

  6. Meister, C., Pimentel, T., Wiher, G. e Cotterell, R. Locally Typical Sampling. arXiv:2202.00666 (2022).

  7. Keskar, N. S., McCann, B., Varshney, L. R., Xiong, C. e Socher, R. CTRL: A Conditional Transformer Language Model for Controllable Generation. arXiv:1909.05858 (2019). A secção 4.1 é a repetition penalty original — a que divide.

Pronto para deixar a LIA escolher?

Construa com todos os modelos de IA num só sítio — comece grátis hoje.