Números Pseudoaleatorios · Tema 38

Simulaciones paralelas y semillas

Un worker, un stream: repartir azar entre procesos sin solapes, con el índice como única verdad.

01 · Punto de partida

Ocho workers, un solo ciclo

Partís tu Monte Carlo en 8 workers y cada uno siembra con la hora: los que arrancan en el mismo milisegundo quedan gemelos y tu “×8 réplicas” son ×4 duplicadas. O siembran 1…8 en un LCG y sus streams se solapan (Tema 37). El paralelismo multiplica la velocidad pero también multiplica las formas de pisarse.

La regla de oro: el stream del worker w es función determinista de (madre, w), nunca del reloj ni del azar del momento.

  • ¿Por qué la hora por worker genera gemelos?
  • ¿Qué tres repartos garantizan disjunción?
  • ¿Cómo agrego resultados parciales sin sesgo?
  • ¿Qué cambia en GPU con miles de hilos?

02 · Definición

Tres repartos honestos

◈

Bloques

Worker w arranca en w·L: necesita salto rápido o L generoso. Simple y visual.

⇄

Salto (leapfrog)

Worker w toma 1 de cada W valores (w, w+W, …): entrelaza sin solape. Exige avance de a W.

▣

Parametrización

Cada worker su carril: incremento (PCG), llave (Philox) o hijo spawn (NumPy). La vía moderna.

Qué reparto según tu plataforma.
CasoRepartoNotas
8–64 workers CPUspawn(W)[w] / bloquesNumPy o saltos; margen L ≥ consumo
Miles de hilos GPUPhilox llave = wSin estado compartido ni comunicación
random clásicoRandom(hash(madre, w))Práctico; documentar esquema

03 · Agregación correcta

Promediar parciales sin mentir

Cada worker estima con su stream (media, proporción, percentil) y el coordinador promedia ponderado por N: con igual N por worker, promedio simple. La varianza entre workers estima el error: si dos workers dan idéntico, son gemelos, no precisión.

Gemelos por reloj
Workers nacidos en el mismo ms comparten semilla y stream: sus “réplicas independientes” son copias. Síntoma: varianza entre workers ≈ 0.
Orden no determinista
Los workers terminan en cualquier orden: agregar por índice w (no por llegada) mantiene la trazabilidad semilla→resultado.
Reproducibilidad paralela
Misma madre + mismo W + mismo reparto por índice = mismos parciales aunque cambie el hardware o el orden de llegada (Tema 36).

Bien repartido

Índice → stream

W parciales independientes, varianza honesta, re-corrible por worker. Escala sin sorpresas.

Mal repartido

Reloj o 1…W

Gemelos y solapes: la varianza colapsa y la media finge precisión. Más workers, mismo azar.

04 · Ejemplos

Tres repartos reales

Madre + W→stream(w)→Promedio ponderado
  1. 1
    π con 8 workers.

    Cada uno estima con su stream; el promedio de los 8 es el π final con error ÷√8.

  2. 2
    Búsqueda de bug en worker 5.

    Se re-corre solo w = 5 con su stream: el resto ni se toca.

  3. 3
    GPU con 10⁵ hilos.

    Hilo t = Philox(llave = t): sin solape posible por construcción, sin memoria compartida.

05 · Implementación en Python

Emular workers con índice

Sin NumPy ni procesos: cada “worker” es un Random(hash(madre, w)) que estima π en su cuarto de círculo. El promedio de parciales converge; con reloj colapsaría.

Python en tu navegador. Subí W a 16 y el error cae ÷√2 sin tocar el esquema.

import hashlib
import random

def worker_pi(w, madre=38, n=2000):
    h = hashlib.sha256(f"{madre}:{w}".encode()).digest()
    rng = random.Random(int.from_bytes(h[:8], "big"))
    dentro = sum(1 for _ in range(n) if rng.random() ** 2 + rng.random() ** 2 < 1)
    return 4 * dentro / n


W = 8
parciales = [worker_pi(w) for w in range(W)]
print([round(p, 4) for p in parciales])
print("pi global:", round(sum(parciales) / W, 4))

Ocho parciales ≈ 3,14 ± 0,03 y un global más cerca de π que cualquiera: ÷√8 en acción.

Gemelos por reloj, en cámara lenta

import random

def worker_reloj(w, n=500):
    rng = random.Random(1718440000)  # mismo segundo para todos
    return [rng.random() for _ in range(n)][0]


print([round(worker_reloj(w), 6) for w in range(4)])

06 · Exploración

Laboratorio: gemelos al acecho

W workers con L valores cada uno sobre LCG m = 256: con semillas 1…W (vecinas) los streams se pisan; con bloques de 256/W se separan. El panel cuenta pares de workers con solape.

EXPERIMENTO 38

W workers, ¿cuántos únicos?

pares con solape / C(W,2)

Los resultados numéricos aparecen debajo.
Pares con solape—
% valores únicos—
Período—
Veredicto—

Vecinas con W = 8 y L = 40: 9 de 28 pares se pisan.

Cada fila es un worker, cada punto un valor: columnas violeta = el mismo valor en varios workers a la vez.

Preguntas para explorar

  1. Con vecinas, W = 16 y L = 40: ¿qué % de valores son únicos? ¿De qué sirve paralelizar así?
  2. Con bloques y L = 120, W = 16: ¿hay solape? ¿Por qué el bloque debe ser ≥ L?
  3. Si los parciales de 8 workers dan idéntico hasta el 4º decimal, ¿precisión o gemelos?
Ver respuestas sugeridas
  1. Solo ~37 % de valores únicos con 35/120 pares pisándose (y con L = 120 caen a ~13 %): más de la mitad del cómputo es azar repetido. Paralelizar sin esquema es gastar CPU en copias.
  2. Sí en los bordes: 16 bloques de 16 con L = 120 se desbordan y pisan al vecino. Bloque ≥ L con margen o nada.
  3. Gemelos: con streams honestos la dispersión entre workers es visible (÷√N cada uno). Varianza ≈ 0 = alarma.

07 · Comprensión

Confusiones frecuentes

«Cada worker con time() está bien»

Los que nacen en el mismo tick son gemelos idénticos. El reloj ordena eventos, no independiza streams.

«Semillas 1…W son distintas, listo»

Distinto número ≠ distinto tramo (Tema 37): en ciclos chicos son ventanas vecinas casi iguales. Esquema o solape.

«Promedio los parciales como vengan»

Con distinto N por worker hay que ponderar por N; y agregar por índice w, no por orden de llegada, para trazar cada parcial a su stream.

«Más workers siempre apuran»

Solo si el trabajo por worker supera el costo de crear streams y juntar parciales. Con N chico, 64 workers son puro overhead.

08 · Práctica guiada

Ejercicios con Python

Ejercicio 1: π en 8 workers

Repetí la estimación del bloque anterior con W = 4 y W = 16: ¿cómo escala el error del global?

import hashlib
import random

def worker_pi(w, madre=38, n=2000):
    h = hashlib.sha256(f"{madre}:{w}".encode()).digest()
    rng = random.Random(int.from_bytes(h[:8], "big"))
    dentro = sum(1 for _ in range(n) if rng.random() ** 2 + rng.random() ** 2 < 1)
    return 4 * dentro / n


for w in [4, 16]:
    ps = [worker_pi(i) for i in range(w)]
    print(w, round(sum(ps) / w, 4))
Ver solución razonada

Ambos ≈ 3,14, con el de 16 más cerca típicamente (÷2 de error). Si un W diera exacto lo mismo que otro, sospechá gemelos.

Ejercicio 2: contar gemelos

Con el juguete m = 256 y semillas 1…8 con L = 40, contá pares de workers con algún valor en común.

def run(semilla, n, a=13, c=7, m=256):
    x = semilla % m
    sal = []
    for _ in range(n):
        x = (a * x + c) % m
        sal.append(x)
    return set(sal)


streams = [run(s, 40) for s in range(1, 9)]
pares = sum(1 for i in range(8) for j in range(i + 1, 8) if streams[i] & streams[j])
print(pares, "de 28")
Ver solución

9 de 28 pares comparten valores con L = 40 (con L = 120 serían casi todos). Con bloques de 32 no habría ninguno: ventanas separadas del mismo ciclo.

Ejercicio 3: re-correr al worker 5

Mostrá que el parcial del worker 5 se reproduce solo, sin tocar a los demás.

Ver una posible respuesta
import hashlib
import random

def parcial(w, madre=38, n=2000):
    h = hashlib.sha256(f"{madre}:{w}".encode()).digest()
    rng = random.Random(int.from_bytes(h[:8], "big"))
    return sum(rng.random() for _ in range(n))


print(parcial(5) == parcial(5))

True: el índice w identifica el stream para siempre. Así se depura en paralelo sin re-correr el mundo.

09 · Síntesis

Ideas para recordar

  • stream(w) = f(madre, w): bloques, leapfrog o llave; reloj nunca.
  • Semillas vecinas o por tiempo generan gemelos y solapes.
  • Agregar por índice con ponderación por N; varianza ≈ 0 = alarma.
  • GPU = counter-based por hilo; CPU = spawn/bloques.
  • Misma madre + W + esquema = mismos parciales en cualquier hardware.

En el próximo bloque entra el mundo físico del cómputo: precisión de punto flotante, cómo guarda decimales tu máquina.