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

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

A temperatura divide os logits antes da softmax — e isso derruba a ideia de controle de criatividade. Mesmo em greedy, duas chamadas podem divergir.

Nesta página

Aqui está a mesma solicitação enviada ao mesmo modelo cinco vezes. Mesmos pesos, mesmo prompt, mesma máquina, mesmo seed aleatório. 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 quebrou. Cada token na última linha foi extraído legitimamente da própria distribuição de probabilidade do modelo sobre seu vocabulário de 151.936 entradas. O número que mudou se chama temperatura, é descrito na maioria das documentações como um controle de criatividade, e essa descrição está errada de um jeito 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 a gastá-lo de verdade. A caixa de ponto flutuante do Capítulo 2 terminou com uma instrução — lembre-se disso quando o Capítulo 17 perguntar por que o mesmo prompt, modelo e seed podem produzir tokens diferentes. E a caixa de mixture-of-experts do Capítulo 9 prometeu um catálogo de quatro causas de não determinismo. As três aparecem abaixo.

A única linha da qual o capítulo inteiro depende

Link para a seção: A única linha da qual o capítulo inteiro depende

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 vem da física estatística, onde o mesmo parâmetro controla o quão fortemente uma distribuição de Boltzmann se concentra em seus estados de baixa energia1 — e ela 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 é o mecanismo inteiro, e vale duas linhas de álgebra para ver por que ela não poderia estar em nenhum outro lugar. Suponha que você tentasse aplicar a temperatura às probabilidades em vez disso — escalá-las por 1/T1/T e renormalizar. Você 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 se cancela. Escalar probabilidades não faz absolutamente nada; a distribuição volta inalterada. A temperatura só tem efeito porque atua no expoente, onde dividir por TT antes de exponenciar é o mesmo que elevar cada probabilidade à potência 1/T1/T — uma remodelagem não linear que altera as razões entre as entradas, não sua escala comum.

A partir dessa posição, os dois limites seguem sem nenhum trabalho adicional. Quando T0T \to 0, o maior logit dispara em relação ao restante e pp colapsa no único token de maior pontuação: greedy decoding. Quando TT cresce, todo zi/Tz_i/T tende a zero, toda exponencial tende a 1, e a distribuição se achata rumo a uma distribuição uniforme sobre todo o vocabulário. Exatamente em T=0T = 0 a fórmula divide por zero, então toda implementação trata esse caso separadamente como o máximo aritmético — inclusive 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. Existe uma segunda coisa, sem relação, chamada temperatura em machine learning: temperature scaling, um método de calibração que ajusta um valor em um 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 isso; este capítulo nunca quer.

Aqui está essa distribuição, com a aritmética na sua frente. Os logits são fixos e plausíveis, então os números no texto abaixo podem ser conferidos com o que você 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 dividem 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, em temperatura 1, sem corte. ␣Paris detém 96,90 % da massa; ␣banana, no fim da lista com logit de 2.6-2.6, recebe 0,00 %. Leve a temperatura para 0 e um token sobrevive com 100 %. Leve para 2 e ␣Paris cai para 69,81 %, enquanto ␣banana sobe para 0,17 % — o token rejeitado pelo modelo, recebendo probabilidade real por causa de um botão que o leitor girou.

Temperatura não é um controle de criatividade

Link para a seção: Temperatura não é um controle de criatividade

O número ␣banana é o argumento inteiro em miniatura: aumentar a temperatura não pode dar a um modelo uma ideia que ele não teve. Os logits já foram calculados, o ranking já está fixo, e a temperatura o preserva exatamente — nenhum calor jamais move um token com pontuação menor acima de outro com pontuação maior. Tudo que ela faz é redistribuir massa para baixo no ranking que o próprio modelo produziu. Temperatura alta não deixa um modelo mais inventivo; ela o torna mais propenso a emitir os tokens que ele pontuou como ruins.

Em um vocabulário real, isso deixa de ser curiosidade e vira o motivo pelo qual saídas em alta temperatura são inutilizáveis. Medido em Qwen/Qwen2.5-0.5B-Instruct, uma forward pass, o prompt acima, contando quantos tokens são necessários para acumular uma determinada parcela 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 dividem os 90 % superiores da massa de probabilidade. Isso não é um espaço criativo mais amplo. É um modelo que recebeu uma instrução aritmética 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 a solicitação pediu.

A faixa útil é estreita e depende da tarefa, não do gosto. Em uma pergunta factual, a resposta é um token, e qualquer calor acima de cerca de 1.2 injeta erro sem benefício. Em uma pergunta aberta, realmente há mais de uma boa continuação, e algum calor compra variedade que ainda permanece 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..."

Em 1.3, o modelo inventou um nome próprio e uma frase que não faz parse. A faixa entre "idêntico toda vez" e "incoerente" fica aproximadamente de 0.6 a 1.1 para este modelo nesta tarefa, e o conselho honesto é que você a encontre medindo na sua tarefa, não copiando um número de um post de blog.

Por que o texto mais provável é um texto ruim

Link para a seção: Por que o texto mais provável é um texto ruim

Há uma pergunta óbvia escondida em tudo isso: se o modelo tem uma distribuição de probabilidade e um token é o mais provável, por que não escolhê-lo sempre? Greedy decoding é gratuito, reproduzí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 na mesma saída. Isso é degeneração de texto neural, nomeada e explicada por Holtzman et al. no artigo que introduziu o top-p.3 O modelo não está quebrado; maximizar a probabilidade da sequência é simplesmente o objetivo errado para texto aberto. A escrita humana não é a sequência de palavras mais provável — ela carrega surpresa, sua probabilidade por token oscila, cai e se recupera — enquanto o caminho de máxima probabilidade é um ponto fixo que, uma vez alcançado, não tem motivo para sair.

É por isso que a amostragem existe. E também, esta é a parte que costuma ficar de fora, não é uma lei universal. O Capítulo 12 mediu 24 de 24 acertos em problemas de palavras de duas etapas com greedy decoding simples, e a amostragem em temperatura 0.8 derrubou isso para 81 %; self-consistency então gastou seis vezes mais tokens para voltar aonde greedy já estava. As duas coisas são verdadeiras ao mesmo tempo:

Geração aberta. Não há uma única continuação correta, então a mais provável é uma armadilha — ela entra em loop, e 87,6 % dela é copiada de si mesma. Faça amostragem.

Tarefas com uma resposta correta. Há uma única continuação correta, então escolher qualquer outra coisa é escolher um erro. Os 100 % do Capítulo 12 viraram 81 % exatamente por esse motivo. Não faça amostragem.

A maioria dos prompts de produção é do segundo tipo e é configurada como o primeiro, porque a temperatura ficou no valor usado pelo código de exemplo.

Duas formas de cortar, e só uma delas se adapta

Link para a seção: Duas formas de cortar, e só uma delas se adapta

Amostragem da distribuição completa não é o que alguém realmente faz, porque a cauda é enorme e cheia de absurdo. Algo precisa ser cortado. Há duas respostas clássicas, e elas diferem em um aspecto que decide tudo.

Top-k mantém um número fixo de candidatos. Ordene por probabilidade, mantenha os primeiros kk, descarte o restante, renormalize.4 Top-p, também chamado de nucleus sampling, mantém uma quantidade fixa de massa: pegue tokens em ordem decrescente até que sua probabilidade acumulada atinja 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, mas não é, porque os dois prompts que você envia no mesmo minuto têm formatos de distribuição completamente diferentes. Ambos abaixo são o mesmo modelo em 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 em nove milésimos de por cento, porque a regra conta vagas, não evidência. No prompt de história, o mesmo k=40k = 40 joga fora 21 % da massa que o modelo genuinamente atribuiu, porque o núcleo real ali tem 467 tokens de largura.

Top-p faz exatamente um número cumprir os dois trabalhos. Defina p=0.9p = 0.9 e ele mantém 1 token no primeiro prompt e 467 no segundo, porque está fazendo uma pergunta sobre a distribuição em vez de impor uma contagem a ela. 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 dividem 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 em 0.90 com a temperatura em 1.5: três dos dez tokens sobrevivem e dividem a massa, ␣Paris renormalizado para 91,10 %. Agora mova apenas a temperatura. Em 0.7, o mesmo 0.90 deixa um sobrevivente — um núcleo tão estreito é greedy decoding usando outro nome. Em 2.0, ele deixa cinco. O corte nunca se moveu; o formato por baixo dele sim.

Esse widget também resolve uma concepção equivocada que vale nomear, porque ela custa dinheiro real. Em uma distribuição confiante, top_p = 0.9 não é "um pouco de variedade". É greedy. Em temperatura 1, o token líder aqui detém 96,90 %, que já passa de 0.9, então o núcleo tem um token de largura e nada mais poderá ser escolhido. Equipes definem top_p como 0.9 acreditando que afrouxaram alguma coisa e depois se perguntam por que todas as respostas são idênticas.

Defina top-k em vez disso e a falha oposta fica 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 dividem 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 em 5, sem top-p. Cinco tokens sobrevivem em todas as temperaturas, porque cinco foi o que se pediu. Em temperatura 1, como mostrado, os quatro candidatos abaixo de ␣Paris valem 2,79 % juntos. Caia para 0.7 e esses 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 na solicitação diz qual deles você está recebendo.

As penalidades, com as fórmulas, porque confundi-las é endêmico

Link para a seção: As penalidades, com as fórmulas, porque confundi-las é endêmico

Três mecanismos diferentes circulam sob nomes parecidos, 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 de qualquer token que tenha aparecido. Aparecer uma vez e aparecer quarenta vezes são penalizados de modo idêntico. É um interruptor, não um botão graduado.

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

Subtraia proporcionalmente à contagem. Um token usado quatro vezes é penalizado quatro vezes mais forte que um token usado uma vez, e a pressão se acumula conforme 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 Ela divide em vez de subtrair, com o caso de sinal necessário porque dividir um logit negativo o tornaria maior. Sua força, portanto, depende da magnitude do logit, o que significa que o mesmo ρ\rho atinge de modo diferente pontos diferentes da mesma frase.

A mesma continuação degenerada de antes, com cada uma aplicada. "Etapas alteradas" conta quantas das 120 etapas de geração escolheram um token diferente daquele que o modelo sem penalidade teria escolhido. A execução aqui tem 120 etapas contra 140 no bloco acima, por isso a linha de base sem penalidade mostra 85,5 % em vez de 87,6 %:

configuração4-grams repetidosetapas alteradas
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 aparecem. Presence em 0.5 mudou três decisões em 120 e cortou a repetição em um quarto — o loop era sustentado por um punhado de tokens. Frequency em 0.5 mudou quatro vezes mais decisões e teve um efeito muito maior, porque o multiplicador da contagem continua crescendo enquanto a constante de presence não cresce. E a penalidade CTRL no valor amplamente copiado de 1.2 reescreveu 35 de 120 decisões, o que não é um empurrão; é outro modelo.

Esse último número prepara a falha sobre a qual ninguém avisa.

O que as penalidades fazem com texto que deveria se repetir

Link para a seção: O que as penalidades fazem com texto que deveria se repetir

Código repete. Tabelas repetem. Listas repetem. Saída estruturada repete por definição — isso é a estrutura. Uma penalidade não consegue distinguir um modelo preso em loop de um modelo emitindo corretamente a quarta linha de uma tabela, porque ambos parecem um token aparecendo de novo.

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

tarefanadafrequency 0.5repetition 1.2
tabela markdown, 6 linhas0 / 56 etapas alteradas0 / 562 / 62
função Python0 / 930 / 9310 / 110
lista com marcadores, 1 a 120 / 500 / 500 / 50

A frequency penalty em 0.5 acabou sendo inofensiva nas três, o que é um resultado útil e levemente surpreendente, e diz algo preciso: como nenhuma decisão mudou, os tokens estruturais devem ter vencido suas posições por mais do que a penalidade subtraiu, mesmo depois de aparecerem cinco e seis vezes. A penalidade CTRL, que divide, de fato os desloca, e aqui está o que ela produziu:

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

O alinhamento desmorona: 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 fechamento é exatamente o tipo de repetição que a penalidade existe para quebrar. Cosmético, e custou seis tokens extras. 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 penalidade empurrou o modelo para fora de total — já usado na docstring — para total_sum, preencheu a saída com comentários inventados para gastar seu orçamento em tokens não usados, e então entrou em um range de três argumentos com um passo. O comentário diz incrementing by 2 each time, o que está errado para uma soma de quadrados de 1 a nn. Uma repetition penalty produziu código incorreto a partir de um prompt que era respondido corretamente sem ela.

A regra que segue é curta: penalidades são para prosa aberta, e devem ficar desligadas para código, saída estruturada, dados tabulares e qualquer coisa com schema. O Capítulo 18 é exatamente sobre essa segunda categoria.

A ordem de aplicação, e por que ela muda a resposta

Link para a seção: A ordem de aplicação, e por que ela muda a resposta

Toda implementação real aplica isso em uma sequência específica:

penalidades → temperatura → top-k → top-p → amostragem

Isso 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 qualquer distribuição que recebe, e a temperatura muda 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 puramente de qual etapa roda primeiro. Se você já se perguntou por que aumentar a temperatura "não faz nada" em um provedor e destrói a saída em outro com os mesmos dois números, esta tabela é uma resposta plausível.

Penalizar antes ou depois da temperatura. Subtrair uma penalidade α\alpha e depois dividir por TT dá uma penalidade efetiva de α/T\alpha/T; dividir primeiro e depois subtrair dá α\alpha. Com uma presence penalty de 1.0 aplicada ao token líder:

temperaturapenaliza, depois temperatempera, depois penaliza
0.599,858 %99,948 %
1.089,839 %89,839 %
2.010,783 %6,830 %

Idênticas em T=1T = 1, como devem ser. Separadas por um fator de 1,58 em T=2T = 2. "Presence penalty 1.0" não é uma quantidade bem definida de penalidade a menos que você também saiba onde a temperatura é aplicada, e nenhuma API documenta isso.

Mostrar detalhes

Opcional: o pipeline inteiro, na ordem acima.

Dezesseis linhas, e tudo neste capítulo está nelas. É o mesmo cálculo que o widget realiza, em 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 acumulada excluindo o token atual, que é o que faz o núcleo incluir o token que cruza o limiar em vez de parar imediatamente antes dele. Erre isso por um e top_p = 0.9 vira silenciosamente um corte um pouco mais apertado que o de todas as outras implementações.

Este é um dos poucos lugares na segunda metade do curso em que Python é a linguagem certa, e o motivo é estrutural, não estilístico: cada linha acima precisa do vetor completo de logits nas suas mãos, e por uma API HTTP esse vetor não existe. Você pode enviar temperature e top_p para um provedor; você não pode implementá-los, e não pode ver o que eles fizeram.

Cada provedor aceita um subconjunto diferente desses controles, com faixas diferentes, e ignora o resto em silêncio. Isso não é uma reclamação abstrata. Qualquer aplicação que oferece escolha de modelo precisa escrever as diferenças em algum lugar, e o arquivo onde faz isso é um mapa da incompatibilidade. Aqui está o que um catálogo desse tipo declara para um único parâmetro nas nove fontes de texto que ele suporta:

faixa de temperatura declaradafontes
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 em um provedor e o máximo de calor permitido em outro, e metade do catálogo não consegue expressar o valor que a outra metade trata como neutro-mais-um-pouco. Os outros botões são igualmente desiguais: 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 penalidade; Anthropic aceita topK, topP e sequências de parada, e nenhuma penalidade; e exatamente uma das nove — Mistral — aceita um seed. Enviar um parâmetro que um provedor não implementa geralmente não produz erro nenhum: a solicitação é bem-sucedida, o botão não faz nada, e você conclui que a configuração não tem efeito.

E repare no que esse arquivo é: uma alegação sobre a API de outra pessoa, escrita em um dia específico, que nada verifica depois. Um catálogo que diz 0 a 1 para um provedor que agora aceita 0 a 2 vai limitar silenciosamente toda solicitação.

Mais dois controles pertencem à mesma família. logprobs, quando oferecido, retorna as log-probabilidades do token escolhido e muitas vezes as principais alternativas — a única janela que você tem para a distribuição de que este capítulo trata, e a base de toda heurística de confiança construída sobre um modelo fechado. E maximum tokens mais sequências de parada encerram a geração sem referência à 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 sua resposta foi cortada no meio da frase por um orçamento, não finalizada pelo modelo.

Defina um seed e a amostragem se torna reproduzível. Essa parte é real, e é fácil 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 um seed, diferente entre seeds, exatamente como anunciado. Então o que o seed fixa é o sorteio aleatório na última linha daquela função sample — qual token é escolhido dada uma distribuição.

O que ele não fixa é a distribuição. E é aí que está o problema, porque o vetor de logits que seu modelo produz não é um objeto matemático; é o resultado de bilhões de somas em ponto flutuante, e essas somas têm uma ordem.

O Capítulo 2 deixou este experimento pronto. 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

Veja a última linha. O número de chunks muda a resposta. Isso 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, ele está fazendo precisamente isso. E um servidor divide de acordo com quantas solicitações está atendendo.

Aqui está esse efeito no próprio modelo. O mesmo prompt, a mesma forward pass, a única diferença sendo quantas outras solicitações 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

Rodando sozinho, o modelo é perfeitamente determinístico — vinte passes, idênticos até o bit. Coloque o prompt idêntico em um batch com solicitações sem relação e 97 % dos seus logits mudam. Nada na sua solicitação mudou. A solicitação de outra pessoa chegou.

Agora a parte honesta, porque isso costuma ser contado como se fosse o fim da história. Uma mudança de 2.5×1052.5 \times 10^{-5} só altera a saída se dois tokens candidatos estiverem dentro dessa distância um do outro. Em 717 etapas de geração ao longo de doze prompts, a menor lacuna entre os dois maiores logits foi 2.5×1032.5 \times 10^{-3} — cem vezes maior que a perturbação — e nenhuma etapa ficou perto o suficiente para virar. Então, neste modelo, em float32, em um laptop, o batching moveu todo logit e não mudou nenhum token.

Isso é uma descrição de condições favoráveis, não uma garantia, e uma mudança nessas 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 delas degrada bastante. A tabela do Capítulo 2 diz por quê: bfloat16 mantém 7 bits de mantissa, então perto de uma magnitude de logit de 16 os valores representáveis ficam separados por 0,125 — 16,0, depois 16,125, depois 16,25 — e o arredondamento pode mover um logit em até 0,0625. Enquanto isso, 4,7 % das etapas de geração medidas acima tinham uma lacuna entre os dois primeiros abaixo de 0,1. Essa é toda a diferença entre os dois experimentos: em float32 a perturbação era cem vezes menor que a decisão mais próxima, e em bfloat16 ela tem o mesmo tamanho. Inferência em produção roda em 16 bits, em hardware com kernels fundidos e ordens de redução que ninguém promete manter. Se "o ruído numérico é desprezível" é uma pergunta sobre precisão e hardware, não sobre o modelo.

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

A adição em ponto flutuante não é associativa

Link para a seção: A adição em ponto flutuante não é associativa

A caixa do Capítulo 2. A ordem de uma soma muda seu valor, então qualquer mudança em como uma redução é dividida muda os logits. Este é o substrato; as outras três são formas de mudar a ordem.

Dynamic batching agrupa sua solicitação com as de estranhos

Link para a seção: Dynamic batching agrupa sua solicitação com as de estranhos

Continuous batching, do Capítulo 13, é o motivo pelo qual a inferência é acessível — e significa que o formato das matrizes pelas quais seus tokens fluem depende do tráfego. Medido acima: 147.321 logits se moveram porque o tamanho do batch mudou.

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

Uma string de versão como -latest é um ponteiro, e ponteiros são redirecionados. Provedores também atualizam a pilha de serving sob um identificador de versão fixo. Nenhum dos dois é anunciado na granularidade que permitiria correlacionar isso com a mudança da sua própria saída.

O parâmetro seed da OpenAI é honesto sobre isso da única forma possível: ele vem junto com 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 como o que é — um provedor dizendo que controla as quatro causas acima, que você não controla nenhuma delas, e que a única coisa que ele pode oferecer é dizer depois do fato que algo se moveu.

Tudo aqui foi sobre um botão e suas consequências. Dê um passo para trás e o problema mais difícil aparece: o objeto que estivemos ajustando é uma distribuição de probabilidade, e distribuições de probabilidade não têm uma interface.

Uma function call tem. Uma linha de banco de dados tem. Um handler POST que espera um corpo JSON com três campos obrigatórios tem uma, e rejeitará qualquer outra coisa. Entre o modelo e todo outro componente do seu sistema existe um contrato sobre o qual um dos lados não consegue fazer promessas: o modelo produzirá algo, extraído de uma distribuição que você 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 retries. Se um token quebraria a estrutura exigida, você não o amostra torcendo para dar certo — você define seu logit como -\infty antes que a softmax sequer o veja. Constrained decoding é uma máscara sobre o mesmo vetor que passamos este capítulo remodelando, e transforma "responda em JSON, por favor" de uma solicitação em uma garantia.

O Capítulo 18 é esse contrato: tool calling, JSON Schema, saídas estruturadas, e o que é necessário para tornar um sistema determinístico seguro para construir sobre um probabilístico.


Todas as medições deste capítulo vêm de Qwen/Qwen2.5-0.5B-Instruct em CPU, float32 salvo indicação contrária, com a amostragem implementada como escrito na seção opcional, não delegada a uma biblioteca. Elas vêm de um modelo pequeno, e os valores específicos são dele; os mecanismos não. How to generate text with different decoding methods, de Von Platen (Hugging Face, 2020), é o artigo contra o qual este é medido e continua sendo a melhor introdução curta ao mesmo material. Para a seção de determinismo: as notas de reprodutibilidade do PyTorch descrevem o que um seed fixa e não fixa em uma única máquina, a documentação da OpenAI sobre seed e system_fingerprint descreve o que um provedor pode e não pode prometer, e a discussão de 2025 da Thinking Machines sobre kernels invariantes a batch é o relato público mais claro de por que corrigir isso no 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 em uma 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), seção 2, é onde o mesmo parâmetro reaparece em deep learning moderno — como uma forma de expor a distribuição completa de um professor, que são os 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 isso com a temperatura deste capítulo. Temperature scaling ajusta um único valor em um conjunto de validação para que a confiança do modelo corresponda à sua precisão; é um método de calibração post-hoc aplicado às saídas de um classificador. Temperature sampling é um controle em tempo de execução sobre como um gerador escolhe tokens. Mesma fórmula, finalidade diferente, e nenhum valor compartilhado.

  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 em 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 a amostragem top-k.

  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 seção 4.1 é a repetition penalty original — a que divide.

Pronto para deixar a LIA escolher por você?

Crie com todos os modelos de IA em um só lugar — comece grátis hoje.