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.
| Caso | Reparto | Notas |
|---|---|---|
| 8–64 workers CPU | spawn(W)[w] / bloques | NumPy o saltos; margen L ≥ consumo |
| Miles de hilos GPU | Philox llave = w | Sin estado compartido ni comunicación |
random clásico | Random(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
- 1π con 8 workers.
Cada uno estima con su stream; el promedio de los 8 es el π final con error ÷√8.
- 2Búsqueda de bug en worker 5.
Se re-corre solo w = 5 con su stream: el resto ni se toca.
- 3GPU 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.
W workers, ¿cuántos únicos?
pares con solape / C(W,2)
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
- Con vecinas, W = 16 y L = 40: ¿qué % de valores son únicos? ¿De qué sirve paralelizar así?
- Con bloques y L = 120, W = 16: ¿hay solape? ¿Por qué el bloque debe ser ≥ L?
- Si los parciales de 8 workers dan idéntico hasta el 4º decimal, ¿precisión o gemelos?
Ver respuestas sugeridas
- 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.
- Sí en los bordes: 16 bloques de 16 con L = 120 se desbordan y pisan al vecino. Bloque ≥ L con margen o nada.
- 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.