01 · Punto de partida
El paralelismo gratis de Monte Carlo
Cada réplica es independiente: 1 000 000 / 8 núcleos = 125 000 por núcleo, sin hablarse. Al final se suman conteos y se promedia. Speedup casi lineal, el sueño del cómputo.
El único pecado: repetir la misma semilla en cada worker y promediar 8 veces lo mismo.
- ¿En cuántos bloques parto N?
- ¿Qué semilla lleva cada worker?
- ¿Cómo agrego conteos sin sesgar?
- ¿Cuánto overhead pago?
02 · Definición
Partir, sembrar, agregar
Partir
N = n₁ + … + n_k. Bloques iguales por defecto; desiguales si los núcleos difieren.
Sembrar
Una semilla por worker: 101…108 o SeedSequence. Nunca la misma.
Agregar
Sumar sumas y conteos: media = Σsuma/Σn. El error sale del total.
| Worker | n | Semilla | Devuelve |
|---|---|---|---|
| 1 | 25 000 | 101 | suma₁, n₁ |
| 2 | 25 000 | 102 | suma₂, n₂ |
| 3 | 25 000 | 103 | suma₃, n₃ |
| 4 | 25 000 | 104 | suma₄, n₄ |
03 · Rendimiento
- 1Medir T₁.
Tiempo con 1 núcleo para N. Base del speedup S = T₁/T_k.
- 2Estimar overhead.
Crear procesos + juntar: fijo ~0,5 s. Con N chico domina.
- 3Elegir k.
k = núcleos físicos. Más procesos que núcleos no acelera.
- Speedup ideal
- S = k. Monte Carlo lo roza porque el 99% es paralelizable.
- Eficiencia
- S/k: 90%+ es sano. Menos avisa overhead o bloques desiguales.
- Bloques grandes
- Miles por worker: el overhead se diluye. Cientos no pagan el lanzamiento.
04 · Ejemplos
- 1π con 8 núcleos.
100 000 → 8×12 500. De 8 s a ~1,2 s.
- 28 Q en 8 núcleos.
Una Q por worker con sus semillas: embudo paralelo.
- 3R = 20 réplicas.
Una réplica por worker en 20 tandas: validación paralela.
05 · Representación en Python
Pool con semillas por worker
Python en tu máquina. En el navegador no hay procesos: el patrón vale igual para hilos o jobs.
from multiprocessing import Pool
def worker(args):
n, semilla = args
import random
random.seed(semilla)
dentro = sum(1 for _ in range(n) if random.random()**2 + random.random()**2 <= 1)
return dentro, n
if __name__ == "__main__":
N, k = 100000, 4
tareas = [(N//k, 101+i) for i in range(k)]
with Pool(k) as pool:
partes = pool.map(worker, tareas)
dentro = sum(d for d, n in partes)
print(4*dentro/N)
# Agregar bien: sumas y conteos, no promedios sueltos
partes = [(19625, 25000), (19680, 25000)] # (dentro, n) por worker
dentro = sum(d for d, n in partes)
N = sum(n for d, n in partes)
print(4*dentro/N)
06 · Exploración
Laboratorio: speedup casi lineal
Mové núcleos k y N. La gris es el ideal S = k; la verde la real con overhead. Con N grande se pegan; con N chico se despegan.
Núcleos que sí rinden
S = T₁ / T_k · E = S/k
Mové k y N para planificar.
Overhead fijo 0,4 s + 0,02 ms por réplica. Con N = 10 000 el overhead domina; con N = 500 000 desaparece.
Preguntas para explorar
- Con N = 20 000 y k = 8, ¿S ronda 4 o 8?
- Con N = 800 000, ¿S se acerca a 8?
- ¿Por qué no pedir k = 32 en una PC de 8?
Ver respuestas sugeridas
- ~4: el overhead pesa. Paralelizar poco N no paga.
- Sí: ~7,2 (90%). Bloques grandes diluyen el fijo.
- Se pelean por 8 núcleos: más procesos, mismo silicio, más overhead.
07 · Comprensión
Confusiones frecuentes
«Misma semilla en cada worker»
Promediar 8 veces lo mismo: N efectivo = N/8. El bug silencioso clásico.
«Promedio de promedios»
Solo vale con bloques iguales. Con desiguales, ponderar por n.
«Paralelizar N = 500»
El overhead supera el ahorro. Paralelizar desde miles por worker.
«Más procesos = más rápido»
Hasta los núcleos físicos. Después, contienda y caída.
08 · Práctica guiada
Ejercicios con Python
Ejercicio 1: agregar bien
Dos workers devuelven (19625, 25000) y (19680, 25000). Agregá.
partes = [(19625, 25000), (19680, 25000)]
print(4*sum(d for d, n in partes)/sum(n for d, n in partes))
Ver solución razonada
3,1444: sumas sobre total, no promedio de 3,14 y 3,15 (aquí coincide por n iguales, pero el hábito salva).
Ejercicio 2: speedup a mano
T₁ = 8 s, T₄ = 2,4 s. Calculá S y E.
print("S:", 8/2.4, "E:", 8/2.4/4)
Ver solución
S ≈ 3,33, E ≈ 83%: sano. Menos de 70% avisa overhead.
Ejercicio 3: semillas por worker
Generá 4 semillas derivadas de 42 sin repetir.
Ver una posible respuesta
base = 42
print([base*100+1+i for i in range(4)])
# mejor: random.SeedSequence(42).spawn(4)
Distintas y trazables a la base 42: reproducibilidad paralela.
09 · Síntesis
Ideas para recordar
- Partir N en k, semilla por worker, agregar sumas/n.
- Speedup casi lineal en MC: 99% paralelizable.
- Bloques de miles: el overhead se diluye.
- k = núcleos físicos; más procesos no acelera.
- Nunca la misma semilla ni promedio de promedios desiguales.
En el próximo tema leeremos sin autoengaño: la interpretación de resultados.