Números Pseudoaleatorios · Tema 17

Generadores modernos

Mersenne Twister, PCG y Philox: estado grande, salida permutada y contadores paralelos. Por qué hoy casi nunca programás un LCG crudo.

01 · Punto de partida

El LCG ya no alcanza

Con LCG y xorshift aprendiste lo esencial, pero la práctica moderna los jubiló para uso general: períodos de 2³¹ o 2³² se agotan en segundos, las ternas caen en planos (Marsaglia) y los tests duros como BigCrush los rompen. Una simulación seria consume miles de millones de valores: necesita período gigante, calidad probada y flujos paralelos.

La respuesta vino en tres oleadas: Mersenne Twister (1997), el caballo de batalla de Python; PCG (2014), pequeño, rápido y con streams; y Philox / counter-based (2011), pensado para GPU y paralelismo masivo.

  • ¿Por qué un período de 2³² es hoy un juguete?
  • ¿Qué ganás con un estado de 624 palabras frente a un solo entero?
  • ¿Qué es el scrambling de salida y por qué lo usan PCG y Twister?
  • ¿Cómo generás 8 flujos independientes sin 8 generadores distintos?

02 · Definición

Tres diseños, tres filosofías

◈

MT19937

624 uint32 de estado, período 2¹⁹⁹³⁷−1 (primo de Mersenne). Recurre con twist y templa la salida. El random de Python.

⇄

PCG32 / PCG64

LCG de 64/128 bits + permutación XSH-RR. Período 2⁶⁴, estado mínimo, 2⁶³ streams vía incremento impar. Default de NumPy.

▣

Philox

Counter-based: salida = F(contador, llave) con rondas tipo cifrado. Salto O(1), ideal GPU/paralelo. Período 2¹²⁸.

Modernos frente a clásicos.
AspectoLCG / Xorshift32MT19937PCG / Philox
Período≤ 2³²2¹⁹⁹³⁷−12⁶⁴ a 2¹²⁸
Estado1 entero624 enteros1–4 enteros + llave
Streams paralelosNo (partir a mano)Difícil (jump costoso)Sí (nativo)
PythonDidácticorandom.Randomnumpy default_rng / Philox

03 · Cómo funcionan

Estado, tempering y contadores

Los tres rompen la linealidad visible con una permutación final. Sin ella verías la estructura del estado a simple vista (planos del LCG, linealidad del xorshift crudo del Tema 16).

Twist + tempering (MT)
Cada 624 pasos mezcla todo el vector (twist); cada salida pasa por 4 XOR-shifts con máscaras que la “peinan”. Equidistribución en 623 dimensiones.
Permutación XSH-RR (PCG)
Avanza un LCG de 64 bits y emite solo 32 bits rotados por los bits altos: xorshifted = ((s >> 18) ^ s) >> 27; rot = s >> 59. Barato y pasa BigCrush.
Rondas de cifrado (Philox)
No hay estado que avance: la salida n es una función invertible del contador n y la llave. Pedir n + 10⁹ cuesta lo mismo que pedir n + 1.

Estado que camina

MT y PCG

Secuencial: el valor n+1 depende del estado n. Rápidos en CPU, pero saltar N pasos exige avanzar o usar jump-ahead especial.

Contador que cifra

Philox

Aleatorio-accsible: cada índice es independiente. 8 hilos piden F(0..7, key) sin hablarse. El precio es más cómputo por valor.

04 · Ejemplos

Quién usa qué

Semilla→Estado / llave→u en [0,1)
  1. 1
    Python random = MT.

    random.seed(42) inicia 624 palabras. Ideal para docencia y reproducibilidad clásica; estado pesado de guardar.

  2. 2
    NumPy default_rng = PCG64.

    Estado chico, streams con SeedSequence.spawn. Lo que deberías usar hoy en simulaciones nuevas.

  3. 3
    JAX / GPU = Philox / ThreeFry.

    Cada réplica pide su bloque del contador con su llave: paralelismo sin solapes ni comunicación.

05 · Implementación en Python

Los tres en tu navegador

Python trae MT de fábrica. PCG y Philox los ilustramos en puro Python (fieles al espíritu, sin NumPy) para que el bloque corra en Pyodide sin instalar nada.

Python en tu navegador. Cambiá la semilla y compará: MT repite exacto, PCG cambia de stream con otro incremento, el contador salta gratis.

import random

random.seed(12345)
print([round(random.random(), 6) for _ in range(5)])
print("estado MT: %d enteros" % len(random.getstate()[1]))
print("periodo: 2**19937 - 1")

MT con semilla 12345: cinco uniformes estables y un estado de 624 enteros. Guardar ese estado pesa; PCG guarda 2.

PCG32 mínimo y contador tipo Philox

MASK64 = (1 << 64) - 1
MULT = 6364136223846793005

def pcg32(semilla, seq=1, n=5):
    inc = ((seq << 1) | 1) & MASK64
    state = 0
    def paso():
        nonlocal state
        old = state
        state = (old * MULT + inc) & MASK64
        xs = ((old >> 18) ^ old) >> 27
        rot = old >> 59
        return ((xs >> rot) | (xs << ((-rot) & 31))) & 0xFFFFFFFF
    paso()
    state = (state + semilla) & MASK64
    paso()
    return [paso() / 2**32 for _ in range(n)]


def philox_like(contador, llave):
    z = (contador + llave + 0x9E3779B97F4A7C15) & MASK64
    z = ((z ^ (z >> 30)) * 0xBF58476D1CE4E5B9) & MASK64
    z = ((z ^ (z >> 27)) * 0x94D049BB133111EB) & MASK64
    return ((z ^ (z >> 31)) >> 32) / 2**32


print([round(v, 6) for v in pcg32(12345)])
print([round(philox_like(i, 999), 6) for i in range(5)])
print(round(philox_like(10**9, 999), 6), "<- salto a mil millones sin iterar")

06 · Exploración

Laboratorio: cuatro generadores cara a cara

Elegí generador y semilla. La curva es u(n); el panel muestra el primer valor, cuántos distintos hay en la ventana y el período teórico. Probá semilla 0 y saltos: el LCG testigo cicla visible, los modernos no.

EXPERIMENTO 17

Período y paralelismo

LCG · MT · PCG · Philox

Los resultados numéricos aparecen debajo.
Primer u—
Distintos—
Período teórico—
Lectura—

MT con semilla 12345: cobertura sin orden visible.

En esta ventana corta todos cubren; la diferencia real es el período, el tamaño de estado y el salto paralelo. Philox puede pedir el índice mil millones sin iterar.

Preguntas para explorar

  1. Cambiá de MT a LCG con la misma semilla: ¿se parecen los primeros valores? ¿Qué dice de la portabilidad entre generadores?
  2. Poné N = 240 en PCG y en LCG: ¿ves ciclos en alguno? ¿Por qué la ventana es demasiado corta para MT/PCG/Philox?
  3. Si necesitaras 8 hilos paralelos, ¿por qué Philox/PCG con streams es más seguro que partir un LCG a mano?
Ver respuestas sugeridas
  1. No se parecen en nada: cada familia es otro algoritmo. La reproducibilidad exige fijar generador + semilla + versión (Tema 36).
  2. No ves ciclos: 240 « 2⁶⁴ « 2¹⁹⁹³⁷. El LCG chico didáctico sí ciclaría con módulo pequeño; el testigo de 2³¹ tampoco se agota aquí.
  3. Partir a mano arriesga solapes; PCG da streams por incremento y Philox por llave/contador con garantía matemática de no solape.

07 · Comprensión

Confusiones frecuentes

«MT es lo mejor porque su período es el más largo»

Período largo no implica mejor calidad ni velocidad: MT tiene estado enorme, siembra lenta y falla algunos tests lineales. PCG con 2⁶⁴ sobra para casi todo y es más liviano.

«PCG es solo un LCG, entonces es malo»

El motor es un LCG grande, pero la permutación de salida rompe la estructura visible. Pasa BigCrushCrush con nota alta; el LCG crudo no.

«Philox desperdicia cómputo por valor»

Hace más cuentas por número, pero gana salto O(1) y paralelismo sin comunicación: en GPU y simulaciones masivas termina más rápido en tiempo mural.

«Si es moderno sirve para criptografía»

No: MT, PCG y Philox son predecibles con observación suficiente. Seguridad exige CSPRNG del sistema operativo (Tema 18).

08 · Práctica guiada

Ejercicios con Python

Ejercicio 1: el peso del estado MT

Sembrá random con 42, pedí un valor, guardá el estado con getstate y restaurá con setstate: el siguiente valor debe repetirse exacto.

import random

random.seed(42)
print(round(random.random(), 6))
est = random.getstate()
a = random.random()
random.setstate(est)
b = random.random()
print(a == b, round(a, 6))
Ver solución razonada

Debe dar True: el estado captura las 624 palabras + índice. Por eso reproducir exige guardar más que la semilla si partiste la secuencia.

Ejercicio 2: streams PCG sin solape

Generá 5 valores con pcg32(7, seq=1) y con seq=2 (código de la sección 05). ¿Comparten valores?

# reutiliza pcg32() de la seccion 05
print([round(v, 6) for v in pcg32(7, seq=1)])
print([round(v, 6) for v in pcg32(7, seq=2)])
Ver solución

No comparten: distinto incremento impar implica distinto stream de período 2⁶⁴. Así se asigna un stream por componente (Tema 37).

Ejercicio 3: salto gratis del contador

Pedí philox_like(0, 999), philox_like(1, 999) y philox_like(10**12, 999). ¿Cuánto costaría llegar al índice 10¹² con un LCG?

Ver una posible respuesta
# reutiliza philox_like() de la seccion 05
print(round(philox_like(0, 999), 6))
print(round(philox_like(1, 999), 6))
print(round(philox_like(10**12, 999), 6))

El contador lo da en O(1); el LCG exigiría 10¹² iteraciones o un salto algebraico especial. Esa es la ventaja para paralelo (Tema 38).

09 · Síntesis

Ideas para recordar

  • MT19937: estado 624, período 2¹⁹⁹³⁷−1, el random de Python; pesado pero ubicuo.
  • PCG: LCG grande + XSH-RR, período 2⁶⁴, streams por incremento; default moderno (NumPy).
  • Philox: contador + llave con rondas, salto O(1) y paralelismo nativo.
  • Reproducibilidad = generador + semilla + versión; el período solo no hace calidad.
  • Ninguno es criptográfico: seguridad y velocidad son objetivos distintos.

En el próximo tema separaremos aguas: generadores criptográficos frente a generadores para simulación, velocidad contra imprevisibilidad.