Saltar al contenido
17/30Capítulo 17 de 30

Temperatura, Top-p y el determinismo que no tienes

La temperatura divide los logits antes de la softmax: por eso no es un dial de creatividad. Incluso la misma llamada greedy puede variar.

En esta página

Aquí está la misma petición enviada al mismo modelo cinco veces. Mismos pesos, mismo prompt, misma máquina, misma semilla aleatoria. Lo único que cambia es un 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 ('$ספטמבר..."

No se ha roto nada. Cada token de la última línea se extrajo legítimamente de la propia distribución de probabilidad del modelo sobre su vocabulario de 151.936 entradas. El número que cambió se llama temperatura, en la mayoría de la documentación se describe como un dial de creatividad, y esa descripción es errónea de una forma que este capítulo puede demostrar, no solo afirmar.

Este también es el capítulo en el que vencen tres promesas anteriores. El capítulo 4 definió el logit y nunca llegó a gastarlo del todo. La caja de coma flotante del capítulo 2 terminaba con una instrucción: recuerda esto cuando el capítulo 17 pregunte por qué el mismo prompt, modelo y semilla pueden producir tokens distintos. Y la caja de mixture-of-experts del capítulo 9 prometía un catálogo de cuatro causas de no determinismo. Las tres llegan abajo.

La única línea de la que cuelga todo el capítulo

Enlace a la sección: La única línea de la que cuelga todo el capítulo

El capítulo 4 introdujo el logit como una puntuación real no normalizada, una por clase. El capítulo 8 hizo que un modelo de lenguaje produjera una por entrada del vocabulario. La softmax convierte ese vector z\mathbf{z} en probabilidades:

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

La temperatura entra aquí —el nombre se toma de la física estadística, donde el mismo parámetro controla lo bruscamente que una distribución de Boltzmann se concentra en sus estados de baja energía1— y divide los logits antes de la exponencial:

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

Esa ubicación es todo el mecanismo, y merece la pena verlo con dos líneas de álgebra para entender por qué no podría estar en ningún otro sitio. Supón que intentaras aplicar la temperatura a las probabilidades en su lugar: escalarlas por 1/T1/T y renormalizar. Obtendrías

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

La constante se cancela. Escalar probabilidades no hace absolutamente nada; la distribución vuelve sin cambios. La temperatura solo tiene efecto porque actúa sobre el exponente, donde dividir por TT antes de exponenciar equivale a elevar cada probabilidad a la potencia 1/T1/T: una remodelación no lineal que cambia las proporciones entre entradas, no su escala común.

A partir de esa ubicación, ambos límites se deducen sin más trabajo. Cuando T0T \to 0 el logit más grande se escapa del resto y pp colapsa sobre el único token con mayor puntuación: greedy decoding. Cuando TT crece, cada zi/Tz_i/T tiende a cero, cada exponencial tiende a 1 y la distribución se aplana hacia una uniforme sobre todo el vocabulario. Exactamente en T=0T = 0 la fórmula divide por cero, así que toda implementación lo trata como un caso especial y usa el máximo aritmético, incluido el widget de abajo, que cambia a argmax en T0.001T \le 0.001.

Una advertencia, porque la colisión de nombres causa confusión real. Hay una segunda cosa no relacionada llamada temperatura en machine learning: temperature scaling, un método de calibración que ajusta un valor en un conjunto de validación para que la confianza de un clasificador coincida con su precisión.2 Misma fórmula, nada que ver con la generación. Cuando los papers dicen «temperature», a menudo se refieren a esa; este capítulo nunca lo hace.

Aquí está esa distribución, con la aritmética delante de ti. Los logits son fijos y plausibles, así que los números del texto de abajo pueden comprobarse contra lo que ves:

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

10 de 10 tokens sobreviven al corte y comparten la probabilidad.

Ver los datos en una tabla
TokenlogitTras la temperaturaTras el 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%
Muestreo: temperatura, top-p y top-k

Diez continuaciones candidatas de The capital of France is, con temperatura 1 y sin cortes. ␣Paris contiene el 96,90 % de la masa; ␣banana, al final con un logit de 2.6-2.6, obtiene 0,00 %. Desliza la temperatura a 0 y sobrevive un token con el 100 %. Deslízala a 2 y ␣Paris cae al 69,81 %, mientras ␣banana sube al 0,17 %: el token rechazado por el modelo, al que un mando que el lector ha movido le ha dado probabilidad real.

El número ␣banana es todo el argumento en miniatura: subir la temperatura no puede darle a un modelo una idea que no tenía. Los logits ya están calculados, el ranking ya está fijado y la temperatura lo conserva exactamente: por mucho calor que apliques, un token con menor puntuación nunca pasa por encima de uno con mayor puntuación. Lo único que hace es redistribuir masa hacia abajo por el ranking que el propio modelo produjo. Una temperatura alta no hace que un modelo sea más inventivo; hace más probable que emita los tokens que puntuó como malos.

En un vocabulario real, esto deja de ser una curiosidad y se convierte en la razón por la que la salida a alta temperatura es inutilizable. Medido en Qwen/Qwen2.5-0.5B-Instruct, una pasada forward, el prompt anterior, contando cuántos tokens hacen falta para acumular una proporción dada de la masa de probabilidad:

temperaturaprobabilidad top-1entropíatokens que contienen el 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

Lee la última fila despacio. En T=2T = 2, para una pregunta con exactamente una respuesta correcta, 32.966 tokens distintos comparten el 90 % superior de la masa de probabilidad. Eso no es un espacio creativo más amplio. Es un modelo al que la aritmética le ha dicho que trate una partícula coreana y un identificador de C++ como opciones vivas para la palabra posterior a A:. La basura del bloque inicial es la consecuencia directa, y no es un bug del modelo ni de la biblioteca: es lo que pidió la solicitud.

El rango útil es estrecho y depende de la tarea, no del gusto. En una pregunta factual, la respuesta es un token y cualquier calor por encima de aproximadamente 1,2 inyecta error a cambio de nada. En una pregunta abierta sí hay más de una buena continuación, y algo de calor compra variedad que sigue siendo fluida:

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 el modelo ha inventado un nombre propio y una frase que no se parsea. La franja entre «idéntico cada vez» e «incoherente» está aproximadamente entre 0,6 y 1,1 para este modelo en esta tarea, y el consejo honesto es que la encuentres midiendo en tu tarea, no copiando un número de una entrada de blog.

Hay una pregunta obvia escondida bajo todo esto: si el modelo tiene una distribución de probabilidad y un token es el más probable, ¿por qué no cogerlo siempre? El greedy decoding es gratis, reproducible y no necesita parámetros.

Porque el resultado es este:

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 %

Ocho frases, una frase. Casi nueve de cada diez ventanas de cuatro tokens ya habían aparecido antes en la misma salida. Esto es degeneración de texto neural, nombrada y explicada por Holtzman et al. en el paper que introdujo top-p.3 El modelo no está roto; maximizar la probabilidad de la secuencia es sencillamente el objetivo equivocado para texto abierto. La escritura humana no es la secuencia de palabras más probable: lleva sorpresa, su probabilidad por token deambula, baja y se recupera, mientras que la ruta de máxima probabilidad es un punto fijo que, una vez alcanzado, no tiene motivo para abandonarse.

Por eso existe el muestreo. También, y esta es la parte que se omite, no es una ley universal. El capítulo 12 midió 24 de 24 correctas en problemas verbales de dos pasos con greedy decoding puro, y muestrear a temperatura 0,8 bajó eso al 81 %; luego la autoconsistencia gastó seis veces más tokens para volver a donde greedy ya estaba. Ambas cosas son ciertas a la vez:

Generación abierta. No hay una única continuación correcta, así que la más probable es una trampa: hace bucles, y el 87,6 % se copia de sí misma. Muestrea.

Tareas con una sola respuesta correcta. Sí hay una única continuación correcta, así que extraer cualquier otra cosa es extraer un error. El 100 % del capítulo 12 se convirtió en 81 % exactamente por este motivo. No muestrees.

La mayoría de prompts de producción son del segundo tipo y se configuran como el primero, porque la temperatura se dejó en lo que usaba el código de ejemplo.

Muestrear desde la distribución completa no es lo que nadie hace en realidad, porque la cola es enorme y está llena de sinsentidos. Hay que cortar algo. Hay dos respuestas clásicas y se diferencian en un aspecto que lo decide todo.

Top-k conserva un número fijo de candidatos. Ordena por probabilidad, conserva los primeros kk, descarta el resto y renormaliza.4 Top-p, también llamado nucleus sampling, conserva una cantidad fija de masa: toma tokens en orden descendente hasta que su probabilidad acumulada alcanza pp, y se detiene.3 Formalmente, el núcleo es el conjunto más pequeño VpV_p con

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

La diferencia suena cosmética y no lo es, porque los dos prompts que envías en el mismo minuto tienen formas de distribución completamente distintas. Ambos son el mismo modelo a temperatura 1:

Q: What is the capital of France?\nA:Once upon a time,
probabilidad top-196.01 %25.39 %
tokens que contienen el 90 % de la masa1467
top-k = 40 conserva99.61 % de la masa78.87 % de la masa
masa en los rangos 2 a 403.61 %53.48 %
token en el rango 40␣Av, 0.0093 %␣Dr, 0.128 %

Un kk fijo, dos fallos en direcciones opuestas. En el prompt factual, k=40k = 40 admite 39 tokens que entre todos valen un 3,6 %: deja pasar basura, incluido un candidato con nueve milésimas de punto porcentual, porque la regla cuenta huecos y no evidencia. En el prompt de historia, el mismo k=40k = 40 tira el 21 % de la masa que el modelo realmente asignó, porque el núcleo real ahí tiene 467 tokens de ancho.

Top-p hace que exactamente un número cumpla ambos trabajos. Fija p=0.9p = 0.9 y conserva 1 token en el primer prompt y 467 en el segundo, porque hace una pregunta sobre la distribución en vez de imponerle un recuento. Observa esa adaptación directamente: mismo corte, cuatro 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 sobreviven al corte y comparten la probabilidad.

Ver los datos en una tabla
TokenlogitTras la temperaturaTras el 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%
Muestreo: temperatura, top-p y top-k

Top-p a 0,90 con la temperatura en 1,5: sobreviven tres de los diez tokens y comparten la masa, con ␣Paris renormalizado al 91,10 %. Ahora mueve solo la temperatura. A 0,7, el mismo 0,90 deja un superviviente: un núcleo tan estrecho es greedy decoding con otro nombre. A 2,0 deja cinco. El corte no se ha movido; la forma que hay debajo sí.

Ese widget también zanja una confusión que merece nombre, porque cuesta dinero de verdad. En una distribución segura, top_p = 0.9 no es «un poco de variedad». Es greedy. A temperatura 1, el token líder aquí contiene el 96,90 %, que ya está por encima de 0,9, así que el núcleo tiene un token de ancho y nunca se podrá extraer nada más. Los equipos fijan top_p en 0,9 creyendo que han aflojado algo y luego se preguntan por qué cada respuesta es idéntica.

Configura top-k en su lugar y el fallo opuesto es igual de visible:

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

5 de 10 tokens sobreviven al corte y comparten la probabilidad.

Ver los datos en una tabla
TokenlogitTras la temperaturaTras el 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%
Muestreo: temperatura, top-p y top-k

Top-k a 5, sin top-p. Sobreviven cinco tokens a cada temperatura, porque se pidieron cinco. A temperatura 1, como se muestra, los cuatro candidatos por debajo de ␣Paris valen un 2,79 % entre todos. Baja a 0,7 y esos mismos cuatro valen 0,38 %: el corte es teatro, y el modelo es efectivamente greedy. Súbela a 2,0 y valen 22,54 %. Misma configuración, mismo número de supervivientes, tres comportamientos completamente distintos, y nada en la solicitud te dice cuál estás obteniendo.

Las penalizaciones, con las fórmulas, porque confundirlas es endémico

Enlace a la sección: Las penalizaciones, con las fórmulas, porque confundirlas es endémico

Tres mecanismos distintos circulan con nombres parecidos, hacen cosas distintas y la diferencia es medible. Sea cic_i el número de veces que el token ii ya ha aparecido.

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

Resta una constante a cualquier token que haya aparecido alguna vez. Aparecer una vez y aparecer cuarenta veces se penalizan de forma idéntica. Es un interruptor, no un dial.

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

Resta en proporción al recuento. Un token usado cuatro veces se penaliza cuatro veces más que un token usado una vez, y la presión se acumula a medida que crece el texto.

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}

La original, del paper de CTRL.7 Divide en vez de restar, con el caso de signo necesario porque dividir un logit negativo lo haría más grande. Por tanto, su fuerza depende de la magnitud del logit, lo que significa que el mismo ρ\rho golpea de forma distinta en distintos puntos de la misma frase.

La misma continuación degenerada de antes, con cada una aplicada. «Pasos alterados» cuenta cuántos de los 120 pasos de generación eligieron un token distinto del que habría elegido el modelo sin penalización. Aquí la ejecución es de 120 pasos frente a los 140 del bloque anterior, por eso la línea base sin penalización marca 85,5 % en vez de 87,6 %:

configuración4-gramas repetidospasos alterados
nada85.5 %0 / 120
presencia 0.565.0 %3 / 120
presencia 1.03.4 %11 / 120
frecuencia 0.56.0 %12 / 120
frecuencia 1.00.0 %20 / 120
repetición 1.2 (CTRL)0.0 %35 / 120

De ahí salen tres cosas. Presencia a 0,5 cambió tres decisiones de 120 y redujo la repetición una cuarta parte: el bucle se sostenía con un puñado de tokens. Frecuencia a 0,5 cambió cuatro veces más decisiones y tuvo un efecto mucho mayor, porque el multiplicador de recuento sigue creciendo mientras que la constante de presencia no. Y la penalización CTRL al valor ampliamente copiado de 1,2 reescribió 35 de 120 decisiones, lo cual no es un empujoncito: es otro modelo.

Ese último número prepara el fallo del que nadie avisa.

Qué hacen las penalizaciones al texto que se supone que debe repetirse

Enlace a la sección: Qué hacen las penalizaciones al texto que se supone que debe repetirse

El código se repite. Las tablas se repiten. Las listas se repiten. La salida estructurada se repite por definición: eso es la estructura. Una penalización no puede distinguir entre un modelo atrapado en un bucle y un modelo que emite correctamente la cuarta fila de una tabla, porque ambas cosas parecen un token que aparece otra vez.

Las mismas tres tareas, generadas de tres maneras:

tareanadafrecuencia 0.5repetición 1.2
tabla markdown, 6 filas0 / 56 pasos alterados0 / 562 / 62
función Python0 / 930 / 9310 / 110
lista con viñetas, 1 a 120 / 500 / 500 / 50

La penalización de frecuencia a 0,5 resultó ser inocua en las tres, lo cual es un resultado útil y ligeramente sorprendente, y dice algo preciso: como no cambió ninguna decisión, los tokens estructurales debían de estar ganando sus posiciones por más de lo que restaba la penalización, incluso después de aparecer cinco y seis veces. La penalización CTRL, que divide en su lugar, sí los desaloja, y esto es lo que produjo:

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

La alineación se desmorona: la cantidad de relleno dentro de cada celda cambia de una fila a otra, porque la secuencia de espacios antes de la barra vertical de cierre es exactamente el tipo de repetición que la penalización existe para romper. Es cosmético, y costó seis tokens extra. El caso de Python no es 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,

La penalización empujó al modelo fuera de total —ya usado en el docstring— hacia total_sum, rellenó la salida con comentarios inventados para gastar su presupuesto en tokens no usados y luego se metió en un range de tres argumentos con stride. El comentario dice incrementando de 2 en 2 cada vez, lo cual es incorrecto para una suma de cuadrados de 1 a nn. Una penalización de repetición produjo código incorrecto a partir de un prompt que se respondía correctamente sin ella.

La regla que se deriva es corta: las penalizaciones son para prosa abierta, y deben estar desactivadas para código, salida estructurada, datos tabulares y cualquier cosa con un schema. El capítulo 18 trata exactamente de esa segunda categoría.

El orden de aplicación, y por qué cambia la respuesta

Enlace a la sección: El orden de aplicación, y por qué cambia la respuesta

Toda implementación real aplica esto en una secuencia concreta:

penalizaciones → temperatura → top-k → top-p → muestra

No es contabilidad arbitraria, y cambiar dos etapas de sitio produce distribuciones realmente distintas. Dos mediciones, ambas sobre el prompt factual.

Cortar antes o después de la temperatura. El núcleo se calcula sobre la distribución que recibe, y la temperatura cambia esa distribución radicalmente:

top-p 0.9 después de la temperaturatop-p 0.9 antes de la temperatura
T=1.0T = 1.01 token1 token
T=1.5T = 1.5353 tokens1 token
T=2.0T = 2.032,966 tokens1 token

En T=2T = 2 la misma configuración nominal produce un conjunto de candidatos de 32.966 o de 1, dependiendo únicamente de qué etapa se ejecute primero. Si alguna vez te has preguntado por qué subir la temperatura «no hace nada» en un proveedor y destruye la salida en otro con los mismos dos números, esta tabla es una respuesta plausible.

Penalizar antes o después de la temperatura. Restar una penalización α\alpha y luego dividir por TT da una penalización efectiva de α/T\alpha/T; dividir primero y luego restar da α\alpha. Con una penalización de presencia de 1,0 aplicada al token líder:

temperaturapenalizar, luego temperartemperar, luego penalizar
0.599.858 %99.948 %
1.089.839 %89.839 %
2.010.783 %6.830 %

Idénticas en T=1T = 1, como deben ser. Separadas por un factor de 1,58 en T=2T = 2. «Penalización de presencia 1,0» no es una cantidad de penalización bien definida salvo que también sepas dónde se aplica la temperatura, y ninguna API lo documenta.

Mostrar detalles

Opcional: todo el pipeline, en el orden anterior.

Dieciséis líneas, y todo lo de este capítulo está en ellas. Es el mismo cálculo que realiza el widget, sobre un vector de logit real en vez de diez números fijos.

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)])

El cumsum(0) - p de la línea top-p es la masa acumulada excluyendo el token actual, que es lo que hace que el núcleo incluya el token que cruza el umbral en vez de detenerse justo antes. Equivócate por uno ahí y top_p = 0.9 se convierte en silencio en un corte ligeramente más estricto que el de cualquier otra implementación.

Este es uno de los pocos lugares de la segunda mitad del curso donde Python es el lenguaje correcto, y la razón es estructural más que estilística: cada línea anterior necesita tener en tus manos el vector completo de logits, y sobre una API HTTP ese vector no existe. Puedes enviar temperature y top_p a un proveedor; no puedes implementarlos, y no puedes ver qué hicieron.

Cada proveedor acepta un subconjunto distinto de estos controles, con rangos distintos, e ignora el resto en silencio. No es una queja abstracta. Cualquier aplicación que ofrezca una elección de modelo tiene que anotar las diferencias en alguna parte, y el archivo donde lo hace es un mapa de la incompatibilidad. Esto es lo que uno de esos catálogos declara para un único parámetro en las nueve fuentes de texto que admite:

rango de temperatura declaradofuentes
0 a 1Anthropic, Google, Meta, Cerebras, PaLM
0 a 1.5Mistral
0 a 2OpenAI, DeepSeek, xAI

La palabra es la misma; la escala no. Una «temperatura de 1» es la distribución sin modificar en uno y el calor máximo permitido en otro, y la mitad del catálogo no puede expresar el valor que la otra mitad trata como neutral-más-un-poco. El resto de mandos son igual de desiguales: las entradas de OpenAI, DeepSeek y xAI aceptan penalizaciones de presencia y frecuencia y ningún topK; las de Google, Meta, Cerebras y PaLM aceptan topK y ninguna penalización; Anthropic acepta topK, topP y secuencias de parada y ninguna penalización; y exactamente uno de los nueve —Mistral— acepta una semilla. Enviar un parámetro que un proveedor no implementa generalmente no produce ningún error: la solicitud tiene éxito, el mando no hace nada y tú concluyes que la configuración no tiene efecto.

Y fíjate en lo que es un archivo así: una afirmación sobre la API de otra persona, escrita un día concreto, que nada verifica después. Un catálogo que diga 0 a 1 para un proveedor que ahora acepta 0 a 2 limitará silenciosamente cada solicitud.

Dos controles más pertenecen a la misma familia. logprobs, cuando se ofrece, devuelve las log-probabilidades del token elegido y a menudo las pocas alternativas principales: la única ventana que obtienes hacia la distribución de la que trata este capítulo, y la base de toda heurística de confianza construida sobre un modelo cerrado. Y máximo de tokens más secuencias de parada terminan la generación sin referencia alguna a la probabilidad: un límite duro y una coincidencia de cadena. Ambos aparecen como el finish_reason del capítulo 14, donde length significa que tu respuesta se cortó a media frase por un presupuesto, no que el modelo la terminó.

Fija una semilla y el muestreo se vuelve reproducible. Esa parte es real, y es fácil verificarla:

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 una semilla, distinto entre semillas, exactamente como se anuncia. Así que lo que fija la semilla es la extracción aleatoria de la última línea de esa función sample: qué token se elige dada una distribución.

Lo que no fija es la distribución. Y ahí está el problema, porque el vector de logits que produce tu modelo no es un objeto matemático; es la salida de miles de millones de sumas en coma flotante, y esas sumas tienen un orden.

El capítulo 2 dejó este experimento preparado. El mismo millón de números float32, sumados en agrupaciones distintas:

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

Mira la última línea. El número de chunks cambia la respuesta. No es una curiosidad sobre numpy; es el mecanismo, porque cuando un servidor de inferencia divide una reducción entre más o menos unidades paralelas, está haciendo precisamente esto. Y un servidor divide según cuántas solicitudes esté atendiendo.

Aquí está ese efecto sobre el propio modelo. El mismo prompt, la misma pasada forward, con la única diferencia de cuántas otras solicitudes estaban casualmente en el 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

Ejecutado solo, el modelo es perfectamente determinista: veinte pasadas, idénticas hasta el bit. Pon el prompt idéntico en un batch con solicitudes no relacionadas y el 97 % de sus logits cambian. Nada de tu solicitud cambió. Llegó la solicitud de otra persona.

Ahora la parte honesta, porque esto suele contarse como si fuera el final de la historia. Un cambio de 2.5×1052.5 \times 10^{-5} solo altera la salida si dos tokens candidatos estaban a esa distancia entre sí. En 717 pasos de generación a lo largo de doce prompts, la menor brecha entre los dos logits principales fue 2.5×1032.5 \times 10^{-3} —cien veces mayor que la perturbación— y ningún paso estuvo lo bastante cerca como para volcarse. Así que en este modelo, en float32, en un portátil, el batching movió todos los logits y no cambió ningún token.

Eso describe condiciones favorables, no una garantía, y basta con un cambio en esas condiciones:

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 ocho respuestas divergen, y una de ellas se degrada mucho. La tabla del capítulo 2 dice por qué: bfloat16 conserva 7 bits de mantisa, así que cerca de una magnitud de logit de 16 los valores representables están separados por 0,125: 16,0, luego 16,125, luego 16,25; y el redondeo puede mover un logit hasta 0,0625. Mientras tanto, el 4,7 % de los pasos de generación medidos arriba tenían una brecha entre los dos primeros por debajo de 0,1. Esa es toda la diferencia entre los dos experimentos: en float32 la perturbación era cien veces menor que la decisión más cercana, y en bfloat16 tiene el mismo tamaño. La inferencia de producción se ejecuta en 16 bits, sobre hardware con kernels fusionados y órdenes de reducción que nadie promete mantener. Si «el ruido numérico es despreciable» es una pregunta sobre precisión y hardware, no sobre el modelo.

Así que, las cuatro causas, catalogadas como prometió el capítulo 9:

La caja del capítulo 2. El orden de una suma cambia su valor, así que cualquier cambio en cómo se divide una reducción cambia los logits. Este es el sustrato; las otras tres son formas de cambiar el orden.

El batching dinámico agrupa tu solicitud con las de desconocidos

Enlace a la sección: El batching dinámico agrupa tu solicitud con las de desconocidos

El batching continuo, del capítulo 13, es la razón por la que la inferencia es asequible, y significa que la forma de las matrices por las que fluyen tus tokens depende del tráfico. Medido arriba: 147.321 logits se movieron porque cambió el tamaño del batch.

El routing de mixture-of-experts depende del batch

Enlace a la sección: El routing de mixture-of-experts depende del batch

La caja del capítulo 9 ya lo decía. El router toma una decisión discreta por token y por capa, sujeta a límites de capacidad por experto calculados sobre el batch. Un token que solo habría ido al experto 7 va al experto 12 acompañado. Esto no es una diferencia de redondeo; es un conjunto distinto de pesos.

Una cadena de versión como -latest es un puntero, y los punteros se redirigen. Los proveedores también actualizan la pila de servicio bajo un identificador de versión fijo. Ninguna de las dos cosas se anuncia con la granularidad que te permitiría correlacionarla con el cambio de tu propia salida.

El parámetro seed de OpenAI es honesto sobre esto de la única forma en que puede serlo: se entrega junto con un campo system_fingerprint que identifica la configuración del backend, y la documentación afirma que el determinismo es best-effort y que un fingerprint cambiado significa que los resultados pueden diferir. Léelo como lo que es: un proveedor diciéndote que controla las cuatro causas anteriores, que tú no controlas ninguna, y que lo único que puede ofrecer es decirte a posteriori que algo se movió.

Todo aquí ha tratado de un mando y sus consecuencias. Da un paso atrás y aparece el problema más difícil: el objeto que hemos estado ajustando es una distribución de probabilidad, y las distribuciones de probabilidad no tienen interfaz.

Una function call sí la tiene. Una fila de base de datos sí la tiene. Un handler POST que espera un cuerpo JSON con tres campos obligatorios sí la tiene, y rechazará cualquier otra cosa. Entre el modelo y cada otro componente de tu sistema hay un contrato sobre el que una parte no puede prometer nada: el modelo producirá algo, extraído de una distribución que has moldeado pero no fijado, y el código del otro lado necesita un valor de un tipo conocido o falla.

El puente entre esos dos mundos se construye con el material de este capítulo, no con parsing y reintentos. Si un token rompería la estructura requerida, no lo muestreas y esperas: pones su logit en -\infty antes de que la softmax lo vea. El constrained decoding es una máscara sobre el mismo vector que hemos pasado este capítulo remodelando, y convierte «por favor, responde en JSON» de una petición en una garantía.

El capítulo 18 es ese contrato: tool calling, JSON Schema, salidas estructuradas y qué hace falta para que sea seguro construir un sistema determinista encima de uno probabilístico.


Todas las mediciones de este capítulo proceden de Qwen/Qwen2.5-0.5B-Instruct en CPU, float32 salvo que se indique lo contrario, con el muestreo implementado tal como se escribe en la sección opcional en vez de delegarlo a una biblioteca. Es un modelo pequeño, y los valores concretos son suyos; los mecanismos no. How to generate text with different decoding methods, de Von Platen (Hugging Face, 2020), es el artículo contra el que se mide este y sigue siendo la mejor introducción breve al mismo material. Para la sección de determinismo: las notas de reproducibilidad de PyTorch describen lo que una semilla arregla y no arregla en una sola máquina, la documentación de OpenAI sobre seed y system_fingerprint describe lo que un proveedor puede y no puede prometer, y la discusión de Thinking Machines de 2025 sobre kernels invariantes al batch es el relato público más claro de por qué arreglar esto a nivel de servidor de inferencia es posible, pero no gratis.

  1. Ackley, D. H., Hinton, G. E. y Sejnowski, T. J. A Learning Algorithm for Boltzmann Machines. Cognitive Science 9(1), pp. 147–169 (1985), donde la temperatura en una softmax procede de la física estadística. Hinton, G., Vinyals, O. y Dean, J., Distilling the Knowledge in a Neural Network, arXiv:1503.02531 (2015), sección 2, es donde el mismo parámetro reaparece en el deep learning moderno: como una forma de exponer la distribución completa de un teacher, que son las soft labels del capítulo 13, no el muestreo de este capítulo.

  2. Guo, C., Pleiss, G., Sun, Y. y Weinberger, K. Q. On Calibration of Modern Neural Networks. arXiv:1706.04599 (2017). No lo confundas con la temperatura de este capítulo. Temperature scaling ajusta un único valor en un conjunto de validación para que la confianza del modelo coincida con su precisión; es un método de calibración post-hoc aplicado a las salidas de un clasificador. Temperature sampling es un control en tiempo de ejecución sobre cómo un generador extrae tokens. Misma fórmula, propósito distinto y ningún valor compartido.

  3. Holtzman, A., Buys, J., Du, L., Forbes, M. y Choi, Y. The Curious Case of Neural Text Degeneration. arXiv:1904.09751 (2019). Introduce nucleus sampling y la medición de que el decoding basado en maximización produce texto cuyo perfil de probabilidad no se parece en nada al texto humano. 2

  4. Fan, A., Lewis, M. y Dauphin, Y. Hierarchical Neural Story Generation. arXiv:1805.04833 (2018). El paper que popularizó el muestreo 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. y Cotterell, R. Locally Typical Sampling. arXiv:2202.00666 (2022).

  7. Keskar, N. S., McCann, B., Varshney, L. R., Xiong, C. y Socher, R. CTRL: A Conditional Transformer Language Model for Controllable Generation. arXiv:1909.05858 (2019). La sección 4.1 es la penalización de repetición original: la que divide.

¿Listo para dejar que elija LIA?

Crea con todos los modelos de IA en un mismo sitio. Empieza gratis hoy.