Método de Monte Carlo · Tema 41

Paralelización de Monte Carlo

Réplicas independientes = núcleos felices: partir N, sembrar por worker y sumar.

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.

Reparto de 100 000 en 4 workers.
WorkernSemillaDevuelve
125 000101suma₁, n₁
225 000102suma₂, n₂
325 000103suma₃, n₃
425 000104suma₄, n₄

03 · Rendimiento

Medir T₁→Paralelizar→Speedup
  1. 1
    Medir T₁.

    Tiempo con 1 núcleo para N. Base del speedup S = T₁/T_k.

  2. 2
    Estimar overhead.

    Crear procesos + juntar: fijo ~0,5 s. Con N chico domina.

  3. 3
    Elegir 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. 1
    π con 8 núcleos.

    100 000 → 8×12 500. De 8 s a ~1,2 s.

  2. 2
    8 Q en 8 núcleos.

    Una Q por worker con sus semillas: embudo paralelo.

  3. 3
    R = 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.

EXPERIMENTO 41

Núcleos que sí rinden

S = T₁ / T_k · E = S/k

Los resultados numéricos aparecen debajo.
Speedup S—
Eficiencia—
Tiempo T_k—
Lectura—

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

  1. Con N = 20 000 y k = 8, ¿S ronda 4 o 8?
  2. Con N = 800 000, ¿S se acerca a 8?
  3. ¿Por qué no pedir k = 32 en una PC de 8?
Ver respuestas sugeridas
  1. ~4: el overhead pesa. Paralelizar poco N no paga.
  2. Sí: ~7,2 (90%). Bloques grandes diluyen el fijo.
  3. 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.