Reading Time: 10 minutes

Comida clave

  • El código Python puede ejecutarse en una computadora portátil y en un sistema HPC grande con la misma lógica central cuando el flujo de trabajo usa MPi4PY o DASK correctamente.
  • La reproducibilidad del medio ambiente es a menudo la parte más difícil del trabajo de HPC. Los entornos de Conda, los archivos de bloqueo y los scripts de trabajo ayudan a resolverlo.
  • El flujo de trabajo tiene tres etapas: prototipo local, paralelizar con MPPI4PY o DASK y enviar trabajos a través de SLURM, PBS u otro programador.
  • No vuelva a escribir toda la base de código para el clúster. Mantenga estable la lógica de simulación y envuélvala con un entorno reproducible y capas de ejecución en paralelo.

Escribes un script de simulación en tu computadora portátil. Lo prueba con pequeños conjuntos de datos. Funciona en minutos. Entonces necesitas miles de núcleos y cientos de gigabytes de RAM para ejecutarlo a escala.

¿Reescribes toda la base de código?

No. El mismo código de Python que se ejecuta en su computadora portátil puede ejecutarse en una supercomputadora con cambios mínimos. La clave no es reescribir. es envolver.

Necesita tres cosas: un entorno reproducible, un modelo de ejecución en paralelo y un script de envío de trabajos para el programador de clústeres.

Esta guía recorre el flujo de trabajo completo desde el prototipo local hasta la implementación de HPC de producción sin cambiar su lógica de simulación principal.

El flujo de trabajo de Python de HPC de tres etapas

La mayoría de los proyectos de HPC Python siguen el mismo patrón de tres etapas, independientemente del dominio científico.

Stage 1 — Local Prototyping:
  Laptop → Jupyter or IDE → NumPy / Pandas → serial execution

Stage 2 — Parallelization:
  Same code → mpi4py or Dask → parallel execution

Stage 3 — Cluster Deployment:
  Python script → Slurm / PBS job submission → distributed compute nodes

El objetivo es avanzar gradualmente a través de estas etapas. Comience con una versión local en funcionamiento. Agregue la ejecución en paralelo solo después de que la lógica sea correcta. Envíe al clúster solo después de que funcionen pequeñas pruebas paralelas.

Etapa 1: Establecer un entorno reproducible

Antes de escribir código paralelo, necesita un entorno de desarrollo confiable. Aquí es donde muchos flujos de trabajo científicos fallan primero.

Por qué la reproducibilidad es difícil para los clústeres de HPC

Los clústeres de HPC son sistemas compartidos. Muchos grupos de investigación utilizan la misma infraestructura y los paquetes de sistemas pueden cambiar con el tiempo. Una simulación que funciona hoy puede romperse seis meses después de las actualizaciones del módulo, los cambios del compilador o los cambios en la versión del paquete.

La solución es la gestión del medio ambiente. Conda se usa comúnmente porque aísla paquetes del sistema Python y puede administrar dependencias compiladas, así como paquetes de Python.

# Create a reproducible environment
conda create -n my-sim python=3.11 numpy mpi4py dask

# Lock dependencies for reproducibility
conda env export --no-builds --name my-sim > environment.yml

# Recreate the environment on another machine
conda env create -n my-sim -f environment.yml

Un solo archivo de entorno brinda a los colaboradores y futuros usuarios una forma clara de recrear la pila de software. Para una reproducibilidad más estricta, use archivos de bloqueo que anclan cada versión de paquete y de dependencia de compilación.

Conda vs Venv: Por qué Conda a menudo gana en HPC

El venv incorporado de Python funciona bien para paquetes de Python puro. Los flujos de trabajo de HPC a menudo dependen de bibliotecas científicas compiladas, tiempos de ejecución de MPI, HDF5, BLAS, CUDA y otros componentes a nivel de sistema.

Característica ven condación
Paquetes de Python puro
Dependencias a nivel de sistema como MPI y HDF5 No
Consistencia multiplataforma Limitado Fuerte
Paquetes de aceleración de GPU como CUPY o PyTorch CUDA Configuración manual Configuración más automatizada

Conda da reproducibilidad a través de más de la pila científica. Eso importa cuando se trabaja con MPI4PY porque el entorno de ejecución de MPI y Python deben ser compatibles tanto en la máquina local como en el clúster.

Etapa 2: Paralelización de su código Python

Una vez que la versión local funciona, el siguiente paso es la paralelización. En los flujos de trabajo de Python HPC, dos herramientas son especialmente comunes:

  1. MPI4PY para un control distribuido de grano fino en muchos nodos.
  2. Dask para paralelismo de nivel superior con menos cambios de código.

Por qué necesitas MPPI4PY

El bloqueo global del intérprete de Python limita la verdadera ejecución de Python de subprocesos múltiples dentro de un proceso. El módulo multiprocessing puede paralelizar en una máquina, pero no se escala naturalmente a través de varios nodos de cómputo.

MPI4PY proporciona enlaces de Python para MPI, la interfaz de paso de mensajes. MPI es una API estándar para computación en paralelo distribuida y se usa ampliamente en sistemas HPC.

Con MPI4PY, cada proceso ejecuta el mismo script pero recibe un rango único. Ese rango controla qué parte de la carga de trabajo maneja cada proceso.

Patrón básico de MPi4PY: Hola mundo

from mpi4py import MPI

comm = MPI.COMM_WORLD

rank = comm.Get_rank()
size = comm.Get_size()

print(f"Hello from process {rank} out of {size} processes")

Inicie el script con MPI:

mpiexec -n 16 python my_script.py

Esto inicia 16 procesos independientes de Python. Cada proceso ejecuta el mismo script, pero cada uno tiene un rank.

El modelo de datos: procesos independientes

Los procesos MPI no comparten memoria de forma predeterminada. Cada proceso tiene su propio espacio de memoria. Para intercambiar información, los procesos deben enviar, recibir, difundir, recopilar o reducir explícitamente datos.

import numpy as np
from mpi4py import MPI

comm = MPI.COMM_WORLD
rank = comm.Get_rank()

# Rank 0 creates the initial data.
if rank == 0:
    data = np.arange(10, dtype="i")
else:
    data = np.empty(10, dtype="i")

# Broadcast data from rank 0 to all ranks.
comm.Bcast(data, root=0)

print(f"Rank {rank} received data: {data}")

Este modelo de comunicación explícito es una de las razones por las que MPI escala bien. Cada rango posee su memoria local, y la comunicación solo ocurre cuando la solicita.

Distribución de la carga de trabajo: el patrón central

La mayoría de los flujos de trabajo científicos de MPI siguen el mismo patrón: divide el problema, calcula localmente y reduce los resultados.

from mpi4py import MPI
import numpy as np

comm = MPI.COMM_WORLD

size = comm.Get_size()
rank = comm.Get_rank()

# Total problem size
N = 10_000_000

# Calculate workload per rank
workloads = [N // size for _ in range(size)]

for i in range(N % size):
    workloads[i] += 1

my_start = sum(workloads[:rank])
my_end = my_start + workloads[rank]

# Each rank works on its own slice
my_data = np.random.rand(my_end - my_start)

# Local computation
local_result = np.sum(np.sin(my_data))

# Sum results across all ranks
send_buffer = np.array([local_result])
receive_buffer = np.zeros(1)

comm.Reduce(send_buffer, receive_buffer, op=MPI.SUM, root=0)

if rank == 0:
    print(f"Total computed across {size} ranks: {receive_buffer[0]}")

Este patrón se aplica a muchas tareas de simulación:

  • Simulaciones de Monte Carlo, donde cada rango procesa muestras independientes.
  • barridos de parámetros, donde cada rango prueba diferentes conjuntos de parámetros.
  • Descomposición de dominio, donde cada rango posee parte de la cuadrícula de simulación.
  • Dinámica molecular, donde clasifica las fuerzas de cómputo para diferentes subconjuntos de partículas.

La caja de herramientas de comunicación colectiva

MPI4PY proporciona operaciones colectivas que suelen ser más fáciles y eficientes que los mensajes de punto a punto personalizados.

Operación ¿Que hace? Cuándo usar
Bcast Un proceso envía los mismos datos a todos los rangos Compartir condiciones iniciales, constantes o valores de configuración
Scatter Distribuye piezas de una matriz entre rangos Descomposición de dominio o partición de carga de trabajo
Gather Recopila datos de todos los rangos Recopilación de salidas parciales
Reduce Agrega valores como Sum, Max o Min Sumar energías o recopilar métricas globales
Allreduce agrega valores y da el resultado a cada rango Sincronización del estado global en todos los procesos

Utilice la comunicación colectiva cuando sea posible. Por lo general, es más simple y eficiente que coordinar manualmente muchos envíos y recepción.

Cuándo usar Dask en su lugar

Dask se encuentra entre Python en serie y MPI. Es útil cuando la carga de trabajo es vergonzosamente paralela, como ejecutar la misma simulación con muchos conjuntos de parámetros o condiciones iniciales.

from dask.distributed import Client
from dask import delayed

client = Client(n_workers=16, threads_per_worker=4)

def my_simulation(params):
    # Your simulation logic here
    return params["a"] * params["b"]

parameter_list = [
    {"a": i, "b": 2.0}
    for i in range(100)
]

tasks = [
    delayed(my_simulation)(params)
    for params in parameter_list
]

final_results = client.compute(tasks, sync=True)

print(final_results[:5])

La ventaja de Dask es que le permite paralelizar muchos flujos de trabajo sin rediseñar la simulación en torno a rangos y comunicadores.

MPI4PY vs Dask: ¿Cuál elegir?

Criterio MPI4PY seguro
curva de aprendizaje más empinado porque debes entender las filas y los comunicadores Más suave porque utiliza patrones familiares de tareas de Python
control de grano fino excelente porque toda comunicación es explícita Limitado por la abstracción del gráfico de tareas
Eficiencia de memoria Alto porque cada rango almacena su propia porción moderado porque los trabajadores agregan gastos generales
Adecuado para Soludores de PDE a gran escala y simulaciones descompuestas de dominio Monte Carlo, barridos de parámetros y análisis de datos
Mejor escala Paralelismo denso en muchos nodos Escala débil en los recuentos de trabajadores moderados

Comience con Dask para crear prototipos y cargas de trabajo vergonzosamente paralelas. Muévase a MPI4PY cuando necesite un control estricto sobre la comunicación, la memoria y la escala en muchos nodos.

Etapa 3: Presentación al clúster

Después de las pruebas locales y la paralelización, el siguiente paso es la implementación de clústeres. La mayoría de los clústeres usan un programador de trabajos. Slurm es uno de los más comunes.

El guión de trabajo de Slurm

Un script de trabajo SLURM le dice al programador qué recursos necesita y cómo ejecutar el código.

#!/bin/bash
#SBATCH --job-name=my-sim
#SBATCH --nodes=32
#SBATCH --tasks-per-node=4
#SBATCH --cpus-per-task=1
#SBATCH --mem=8GB
#SBATCH --time=04:00:00
#SBATCH --output=sim_output.%j

# Load required modules
module load Python/3.11

# Activate conda environment
eval "$(conda shell.bash hook)"
conda activate my-sim

# Run the MPI job
srun python my_simulation.py

Los parámetros importantes incluyen:

  • --nodes: Número de nodos de cálculo.
  • --tasks-per-node: Número de tareas MPI por nodo.
  • --cpus-per-task: núcleos de CPU asignados a cada tarea.
  • --time: Límite de reloj de pared. Los trabajos generalmente se detienen si lo superan.
  • --output: patrón de archivo de salida. %j Inserta el ID de trabajo.

Ejecución en grupos sin slurm

No todos los grupos usan Slurm. Las alternativas comunes incluyen:

  • PBS o par, generalmente usando qsub.
  • LSF, generalmente usando bsub.
  • Cobalto u otros programadores específicos del sitio.

La sintaxis de envío de trabajos cambia, pero el código de Python generalmente no. MPI4PY y Dask son en su mayoría agnósticos del programador una vez que se lanzaron correctamente.

Modo interactivo frente a lote

Para la depuración, las ejecuciones interactivas son útiles:

srun --ntasks=4 --pty --time=02:00:00 python my_simulation.py

Para la producción, utilice el envío por lotes:

sbatch my_job_script.sh

Las sesiones interactivas son buenas para pruebas cortas. Los trabajos por lotes son mejores para tiradas largas, cargas de trabajo durante la noche y simulaciones de producción.

El flujo de trabajo completo: de la computadora portátil a la producción

La transición del prototipo local al despliegue de clúster puede ser gradual. La lógica de simulación central debe permanecer estable mientras cambia el contenedor de ejecución.

Paso 1: Desarrollar y probar localmente

# my_simulation.py
import numpy as np

def simulate(initial_condition, params):
    # Core simulation logic
    result = np.sin(initial_condition) * params["factor"]
    return np.sum(result)

# Local test
data = np.random.rand(1000)
result = simulate(data, {"factor": 1.5})

print(f"Local result: {result}")

Paso 2: agregar paralelización

# my_simulation_mpi.py
import numpy as np
from mpi4py import MPI

comm = MPI.COMM_WORLD

rank = comm.Get_rank()
size = comm.Get_size()

def simulate(initial_condition, params):
    # Core simulation logic stays the same
    result = np.sin(initial_condition) * params["factor"]
    return np.sum(result)

N = 10_000_000

workloads = [N // size for _ in range(size)]

for i in range(N % size):
    workloads[i] += 1

my_start = sum(workloads[:rank])
my_end = my_start + workloads[rank]

my_data = np.random.rand(my_end - my_start)

local_result = simulate(my_data, {"factor": 1.5})

send_buffer = np.array([local_result])
receive_buffer = np.zeros(1)

comm.Reduce(send_buffer, receive_buffer, op=MPI.SUM, root=0)

if rank == 0:
    print(f"Parallel result across {size} ranks: {receive_buffer[0]}")

Paso 3: Enviar al clúster

Guarde un script de trabajo como job_script.sh:

#!/bin/bash
#SBATCH --job-name=parallel-sim
#SBATCH --nodes=64
#SBATCH --tasks-per-node=8
#SBATCH --time=12:00:00

module load Python/3.11

eval "$(conda shell.bash hook)"
conda activate my-sim

srun python my_simulation_mpi.py

Envíelo:

sbatch job_script.sh

La lógica de simulación sigue siendo la misma. El contenedor cambia la forma en que se divide y se lanza la carga de trabajo.

Trampas comunes y cómo evitarlas

Escolar 1: Difundir demasiados datos

Difundir grandes matrices desde el rango 0 hasta cada rango puede convertirse en un cuello de botella de comunicación. Para simulaciones grandes, evite enviar más datos de los que necesita cada rango.

Las mejores opciones incluyen:

  • Use Scatter en lugar de Bcast cuando cada rango solo necesite un segmento.
  • Utilice la descomposición del dominio para que cada rango tenga una región de la simulación.
  • Para Monte Carlo, que cada rango genere sus propias muestras aleatorias en lugar de transmitirlas.

Pitfall 2: Sobreasignación de recursos de clúster

Solicitar más nodos que su código puede usar el tiempo de asignación de pérdidas y puede aumentar el retraso en la cola.

Comience con un pequeño trabajo de prueba, como de 4 a 8 nodos. Mide la velocidad. Escala solo después de que el código muestre una eficiencia paralela útil.

Escolar 3: Olvidar el gil en código mixto

Numpy, Scipy y Bibliotecas Compiladas C o Fortran a menudo liberan el bloqueo de intérprete global de Python durante operaciones numéricas pesadas. Los hilos de Python puro generalmente no proporcionan el mismo beneficio.

Si su flujo de trabajo mezcla subprocesos de Python con bibliotecas numéricas, la escala de prueba con cuidado en lugar de asumir más subprocesos le ayudará.

Escolar 4: Desajuste del medio ambiente en el clúster

Su computadora portátil puede usar Python 3.11 mientras que el módulo de clúster proporciona Python 3.9. MPI4PY, bibliotecas MPI y dependencias compiladas pueden romperse cuando las versiones no coinciden.

Utilice un entorno de Conda y documente la configuración exacta. Vuelva a crear y probar el entorno en el clúster antes de ejecutar trabajos grandes.

Guía de decisión: ¿Qué estrategia de paralelización?

Situación Enfoque recomendado
Pequeño conjunto de datos y pruebas lógicas locales Python en serie con numpy
Máquina única con múltiples núcleos Python multiprocessing o Dask LocalCluster
8-64 trabajadores y tareas vergonzosamente paralelas Dask con un lanzador de clústeres
64-1000 nodos y PDE descompuestos por el dominio MPI4PY con distribución explícita de carga de trabajo
Simulación de producción en más de 1000 nodos MPI4PY, secuencias de comandos de trabajos de programador y entorno de Conda bloqueado
Código de investigación sin reproducibilidad garantizada Dask, archivo de entorno de Conda y script de trabajo como punto de partida

Su herramienta de paralelización debe coincidir con la escala y la estructura del problema. Comience más pequeño de lo que cree que necesita, luego escalar después de medir el rendimiento.

Guías relacionadas

Para temas relacionados en flujos de trabajo de simulación científica:

Resumen y próximos pasos

Los flujos de trabajo de HPC Python se tratan de envolver, no de reescribir. La misma lógica de simulación que se ejecuta en una computadora portátil puede ejecutarse en muchos núcleos cuando construye la estructura de ejecución correcta.

El flujo de trabajo es:

  1. Bloquee el entorno con Conda para que el código se ejecute constantemente en todos los sistemas.
  2. Agregue paralelismo con MPI4PY para un control distribuido de grano fino o DASK para el paralelismo de tareas de alto nivel.
  3. Envíe trabajos a través de SLURM, PBS, LSF o el planificador utilizado por su clúster.

La parte más difícil es a menudo el entorno, no el código. Invierta en una configuración reproducible antes de tiempo, y las simulaciones paralelas se vuelven más fáciles de escalar desde una computadora portátil a una supercomputadora.

Próximos pasos

  1. Auditar la simulación actual. Si solo se ejecuta en una computadora portátil, comience creando un entorno de conda y pruebe una pequeña ejecución de múltiples núcleos.
  2. Medir antes de escalar. Ejecute un pequeño trabajo de clúster, mida la aceleración y solo luego escala al tamaño de producción.
  3. Documente la configuración. Guarde el archivo de entorno, el script de trabajo, el comando de inicio y la salida esperada.

Establecer un flujo de trabajo de Python de HPC requiere comprender los paquetes científicos de Python, la programación en paralelo y la infraestructura de clústeres. Si necesita ayuda con la paralelización de MPI, la programación de trabajos o la reproducibilidad del entorno, nuestro equipo puede admitir flujos de trabajo científicos escalables de Python, incluidas las simulaciones basadas en Fipy y los motores Monte Carlo personalizados.