Reading Time: 8 minutes

Schlüssel zum Mitnehmen

  • Python-Code kann auf einem Laptop und einem großen HPC-System mit der gleichen Kernlogik ausgeführt werden, wenn der Workflow MPI4Py oder Dask korrekt verwendet.
  • Die Reproduzierbarkeit der Umgebung ist oft der schwierigste Teil der HPC-Arbeit. Conda-Umgebungen, Lockfiles und Job-Skripte helfen dabei, es zu lösen.
  • Der Workflow besteht aus drei Phasen: Prototyp lokal, parallelisieren mit MPI4PY oder DASK und Senden von Jobs über SLURM, PBS oder einen anderen Scheduler.
  • Schreiben Sie nicht die gesamte Codebasis für den Cluster neu. Halten Sie die Simulationslogik stabil und wickeln Sie sie mit reproduzierbarer Umgebung und parallelen Ausführungsschichten ein.

Sie schreiben ein Simulationsskript auf Ihren Laptop. Sie testen es mit kleinen Datensätzen. Es läuft in Minuten. Dann benötigen Sie Tausende von Kernen und Hunderte von Gigabyte RAM, um es im Maßstab auszuführen.

Schreiben Sie die gesamte Codebasis um?

Nein. Der gleiche Python-Code, der auf Ihrem Laptop ausgeführt wird, kann auf einem Supercomputer mit minimalen Änderungen ausgeführt werden. Der Schlüssel ist nicht neu zu schreiben. es ist umwickelt.

Sie benötigen drei Dinge: eine reproduzierbare Umgebung, ein paralleles Ausführungsmodell und ein Auftragsübermittlungsskript für den Clusterplaner.

Dieser Leitfaden durchläuft den vollständigen Workflow vom lokalen Prototyp bis zur HPC-Produktion, ohne dass Sie Ihre Kernsimulationslogik ändern.

Der dreistufige HPC-Python-Workflow

Die meisten HPC-Python-Projekte folgen unabhängig vom wissenschaftlichen Bereich dem gleichen dreistufigen Muster.

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

Ziel ist es, diese Phasen schrittweise durchzugehen. Beginnen Sie mit einer funktionierenden lokalen Version. Fügen Sie die parallele Ausführung erst hinzu, wenn die Logik korrekt ist. Erst nach kleinen parallelen Tests an den Cluster senden.

Stufe 1: Aufbau einer reproduzierbaren Umgebung

Bevor Sie parallelen Code schreiben, benötigen Sie eine zuverlässige Entwicklungsumgebung. Hier scheitern viele wissenschaftliche Workflows zuerst.

Warum Reproduzierbarkeit für HPC-Cluster schwierig ist

HPC-Cluster sind gemeinsam genutzte Systeme. Viele Forschungsgruppen verwenden dieselbe Infrastruktur, und Systempakete können sich im Laufe der Zeit ändern. Eine heutige Simulation kann nach Modulaktualisierungen, Compileränderungen oder Paketversionsänderungen sechs Monate später abbrechen.

Die Lösung ist das Umweltmanagement. Conda wird häufig verwendet, weil es Pakete aus dem System Python isoliert und sowohl kompilierte Abhängigkeiten als auch Python-Pakete verwalten kann.

# 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

Eine einzelne Umgebungsdatei bietet Mitarbeitern und zukünftigen Benutzern eine klare Möglichkeit, den Software-Stack neu zu erstellen. Verwenden Sie für eine strengere Reproduzierbarkeit Lockfiles, die jede Paketversion anheften und eine Abhängigkeit aufbauen.

Conda vs Venv: Warum Conda oft auf HPC gewinnt

Pythons integrierte venv funktioniert gut für reine Python-Pakete. HPC-Workflows hängen häufig von kompilierten wissenschaftlichen Bibliotheken, MPI-Runtimes, HDF5, BLAS, CUDA und anderen Komponenten auf Systemebene ab.

Funktion Venv Konda
Reine Python-Pakete Ja Ja
Abhängigkeiten auf Systemebene wie MPI und HDF5 Nein Ja
Plattformübergreifende Konsistenz Beschränkt Stark
GPU-Beschleunigungspakete wie Cupy oder PyTorch CUDA Manuelle Einrichtung Mehr automatisierte Einrichtung

Conda bietet Reproduzierbarkeit über mehr wissenschaftliche Stapel. Das ist bei der Arbeit mit MPI4Py von Bedeutung, da die MPI-Runtime- und Python-Umgebung sowohl auf dem lokalen Computer als auch auf dem Cluster kompatibel sein muss.

Stufe 2: Parallelisierung Ihres Python-Codes

Sobald die lokale Version funktioniert, ist der nächste Schritt die Parallelisierung. In Python-HPC-Workflows sind zwei Tools besonders verbreitet:

  1. MPI4PY für eine feinkörnige verteilte Steuerung über viele Knoten.
  2. Dask für übergeordnete Parallelität mit weniger Codeänderungen.

Warum Sie mpi4py brauchen

Die globale Dolmetschersperre von Python begrenzt die wahre Multi-Thread-Python-Ausführung in einem Prozess. Das multiprocessing-Modul kann auf einer Maschine parallelisieren, skaliert jedoch nicht über mehrere Rechenknoten hinweg.

MPI4PY bietet Python-Bindungen für MPI, die Nachrichtenübermittlungsschnittstelle. MPI ist eine Standard-API für verteiltes Parallel-Computing und wird häufig auf HPC-Systemen verwendet.

Bei MPI4Py führt jeder Prozess das gleiche Skript aus, erhält jedoch einen eindeutigen Rang. Dieser Rang steuert, welchen Teil der Arbeitslast jeder Prozess verarbeitet.

Grundlegendes mpi4py-Muster: Hallo Welt

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")

Starten Sie das Skript mit MPI:

mpiexec -n 16 python my_script.py

Dies startet 16 unabhängige Python-Prozesse. Jeder Prozess führt das gleiche Skript aus, aber jeder hat ein anderes rank .

Das Datenmodell: unabhängige Prozesse

MPI-Prozesse teilen sich standardmäßig keinen Speicher. Jeder Prozess hat seinen eigenen Speicherplatz. Um Informationen auszutauschen, müssen Prozesse explizit Daten senden, empfangen, übertragen, sammeln oder verkleinern.

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}")

Dieses explizite Kommunikationsmodell ist ein Grund, warum MPI gut skaliert. Jeder Rang besitzt seinen lokalen Speicher, und die Kommunikation erfolgt nur, wenn Sie ihn anfordern.

Workload-Verteilung: Das Kernmuster

Die meisten wissenschaftlichen MPI-Workflows folgen demselben Muster: Teilen Sie das Problem, berechnen Sie lokal und reduzieren Sie die Ergebnisse.

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]}")

Dieses Muster gilt für viele Simulationsaufgaben:

  • Monte-Carlo-Simulationen, bei denen jeder Rang unabhängige Stichproben verarbeitet.
  • Parameter-Sweeps, bei denen jeder Rang verschiedene Parametersätze testet.
  • Domänenzerlegung, wobei jeder Rang einen Teil des Simulationsrasters besitzt.
  • Molekulare Dynamik, bei der die Rechenkräfte für verschiedene Teilchen-Teilmengen rangieren.

Die kollektive Kommunikations-Toolbox

MPI4Py bietet kollektive Vorgänge, die in der Regel einfacher und effizienter sind als benutzerdefinierte Punkt-zu-Punkt-Messaging.

Betrieb Was es tut Wann zu verwenden
Bcast Ein Prozess sendet die gleichen Daten an alle Ränge Teilen von Anfangsbedingungen, Konstanten oder Konfigurationswerten
Scatter Verteilt Teile eines Arrays über Ränge Domänenzerlegung oder Workload-Partitionierung
Gather Sammelt Daten aus allen Reihen Sammeln von Teilausgaben
Reduce Aggregiert Werte wie SUM, MAX oder MIN Energien summieren oder globale Metriken sammeln
Allreduce Aggregiert Werte und gibt jedem Rang das Ergebnis Synchronisieren des globalen Status über alle Prozesse

Verwenden Sie nach Möglichkeit kollektive Kommunikation. Es ist normalerweise einfacher und effizienter als die manuelle Koordinierung vieler Sends und Empfangen.

Wann sollte man das stattdessen verwenden?

Dask sitzt zwischen Serial Python und MPI. Dies ist nützlich, wenn die Arbeitslast peinlich parallel ist, z. B. die gleiche Simulation mit vielen Parametersätzen oder Anfangsbedingungen auszuführen.

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])

Der Vorteil von Dask ist, dass Sie viele Workflows parallelisieren können, ohne die Simulation um Ränge und Kommunikatoren neu zu gestalten.

mpi4py vs. Dask: Welche wählen?

Kriterium MPI4PY Dask
Lernkurve Steiler, weil Sie Ränge und Kommunikatoren verstehen müssen sanfter, weil es bekannte Python-Aufgabenmuster verwendet
Feinkornsteuerung Ausgezeichnet, weil jede Kommunikation explizit ist Eingeschränkt durch die Aufgabengrafik-Abstraktion
Speichereffizienz Hoch, weil jeder Rang sein eigenes Stück speichert gemäßigt, weil die Arbeitnehmer den Overhead erhöhen
Passend für Großformatige PDE-Solver und domänenzerlegte Simulationen Monte Carlo, Parameter-Sweeps und Datenanalyse
Beste Skala Dichte Parallelität über viele Knoten Schwache Skalierung über mäßige Arbeiterzählungen

Beginnen Sie mit DASK für Prototyping und peinlich parallele Workloads. Gehen Sie zu MPI4PY, wenn Sie über viele Knoten hinweg eine strenge Kontrolle über Kommunikation, Speicher und Skalierung benötigen.

Stufe 3: Einreichen des Clusters

Nach lokalem Testen und Parallelisierung ist der nächste Schritt die Clusterbereitstellung. Die meisten Cluster verwenden einen Jobplaner. Slurm ist eine der häufigsten.

Das Slurm-Job-Skript

Ein Slurm-Job-Skript teilt dem Scheduler mit, welche Ressourcen Sie benötigen und wie der Code ausgeführt wird.

#!/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

Wichtige Parameter sind:

  • --nodes: Anzahl der Rechenknoten.
  • --tasks-per-node: Anzahl der MPI-Aufgaben pro Knoten.
  • --cpus-per-task: CPU-Kernen, die jeder Aufgabe zugewiesen werden.
  • --time: Wanduhrgrenze. Jobs werden normalerweise gestoppt, wenn sie es überschreiten.
  • --output: Ausgabedateimuster. %j Fügt die Job-ID ein.

Laufen auf Clustern ohne Slurm

Nicht jeder Cluster verwendet Slurm. Häufige Alternativen sind:

  • PBS oder Drehmoment, normalerweise unter Verwendung von qsub .
  • LSF, normalerweise mit bsub .
  • Kobalt oder andere ortsspezifische Scheduler.

Die Syntax der Jobeinreichung ändert sich, der Python-Code jedoch normalerweise nicht. MPI4PY und DASK sind meist planer-agnostisch, sobald sie korrekt gestartet wurden.

Interaktiver vs. Batch-Modus

Zum Debuggen sind interaktive Läufe nützlich:

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

Verwenden Sie für die Produktion die Chargeneinreichung:

sbatch my_job_script.sh

Interaktive Sitzungen eignen sich gut für kurze Tests. Batch-Jobs sind besser für Langstrecken, Arbeitslasten über Nacht und Produktionssimulationen.

Der komplette Workflow: vom Laptop bis zur Produktion

Der Übergang vom lokalen Prototyp zur Clusterbereitstellung kann allmählich erfolgen. Die Kernsimulationslogik sollte stabil bleiben, während sich der Ausführungswrapper ändert.

Schritt 1: Entwickeln und testen Sie lokal

# 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}")

Schritt 2: Parallelisierung hinzufügen

# 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]}")

Schritt 3: An den Cluster senden

Speichern Sie ein Job-Skript wie 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

einreichen:

sbatch job_script.sh

Die Simulationslogik bleibt gleich. Der Wrapper ändert die Aufteilung und den Start der Arbeitslast.

Häufige Fallstricke und wie man sie vermeidet

Fallfall 1: Zu viele Daten übertragen

Das Senden großer Arrays von Rang 0 bis zu jedem Rang kann zu einem Kommunikationsengpass werden. Vermeiden Sie bei großen Simulationen, mehr Daten zu senden, als jeder Rang benötigt.

Bessere Optionen sind:

  • Verwenden Sie Scatter anstelle von Bcast, wenn jeder Rang nur eine Scheibe benötigt.
  • Verwenden Sie die Domänenzerlegung, damit jeder Rang eine Region der Simulation besitzt.
  • Lassen Sie für Monte Carlo jeden Rang seine eigenen Zufallsstichproben generieren, anstatt sie zu senden.

Fallstricke 2: Überalloverallcluster-Ressourcen

Wenn Sie mehr Knoten als Ihr Code anfordern, können Sie Zeit für die Zuweisung von Abfällen verwenden und die Warteschlangenverzögerung erhöhen.

Beginnen Sie mit einem kleinen Testjob, z. B. 4 bis 8 Knoten. Beschleunigung messen. Skalieren erst, nachdem der Code nützliche parallele Effizienz zeigt.

Fallfall 3: Vergessen der Gil in gemischtem Code

Numpy, Scipy und kompilierte C- oder Fortran-Bibliotheken geben häufig die Python-Global-Interpreter-Sperre während schwerer numerischer Operationen frei. Reine Python-Threads bieten normalerweise nicht den gleichen Nutzen.

Wenn Ihr Workflow Python-Threads mit numerischen Bibliotheken mischt, hilft die Testskalierung sorgfältig, anstatt mehr Threads anzunehmen.

Fallfall 4: Umgebungsfehler auf dem Cluster

Ihr Laptop kann Python 3.11 verwenden, während das Clustermodul Python 3.9 bereitstellt. MPI4PY, MPI-Bibliotheken und kompilierte Abhängigkeiten können brechen, wenn Versionen nicht übereinstimmen.

Verwenden Sie eine Conda-Umgebung und dokumentieren Sie die genaue Einrichtung. Erstellen und testen Sie die Umgebung auf dem Cluster, bevor Sie große Jobs ausführen.

Entscheidungsleitfaden: Welche Parallelisierungsstrategie?

Lage Empfohlener Ansatz
Kleiner Datensatz und lokale Logiktests Serienpython mit numpy
Einzelmaschine mit mehreren Kernen Python multiprocessing oder Dask LocalCluster
8–64 Arbeiter und peinlich parallele Aufgaben Dask mit einem Cluster-Launcher
64–1000 Knoten und domänenzerlegte PDEs mpi4py mit expliziter Workload-Verteilung
Produktionssimulation auf über 1000 Knoten MPI4PY, Scheduler-Job-Skripte und gesperrte Conda-Umgebung
Recherchecode ohne garantierte Reproduzierbarkeit Dask, Conda-Umgebungsdatei und Job-Skript als Ausgangspunkt

Ihr Parallelisierungstool sollte mit dem Maßstab und der Struktur des Problems übereinstimmen. Beginnen Sie kleiner als Sie denken und skalieren Sie nach der Messung der Leistung.

Verwandte Anleitungen

Zu verwandten Themen in wissenschaftlichen Simulations-Workflows:

Zusammenfassung und nächste Schritte

Bei HPC-Python-Workflows geht es um das Einpacken, nicht um das Umschreiben. Die gleiche Simulationslogik, die auf einem Laptop ausgeführt wird, kann auf vielen Kernen ausgeführt werden, wenn Sie die richtige Ausführungsstruktur erstellen.

Der Arbeitsablauf ist:

  1. Sperren Sie die Umgebung mit Conda, damit der Code konsistent über alle Systeme läuft.
  2. Fügen Sie Parallelität mit MPI4PY für eine feinkörnige verteilte Steuerung oder einen Stab für die Parallelität von Aufgaben hinzu.
  3. Senden Sie Jobs über Slurm, PBS, LSF oder den von Ihrem Cluster verwendeten Scheduler.

Der schwierigste Teil ist oft die Umgebung, nicht der Code. Investieren Sie frühzeitig in reproduzierbaren Setup, und parallele Simulationen lassen sich einfacher von Laptop zu Supercomputer skalieren.

Nächste Schritte

  1. Überprüfen Sie die aktuelle Simulation. Wenn es nur auf einem Laptop ausgeführt wird, erstellen Sie zunächst eine Conda-Umgebung und testen Sie einen kleinen Multi-Core-Lauf.
  2. Messen Sie vor der Skalierung. Führen Sie einen kleinen Cluster-Job aus, messen Sie die Beschleunigung und skalieren Sie erst dann auf die Produktionsgröße.
  3. Dokumentieren Sie die Einrichtung. Speichern Sie die Umgebungsdatei, das Auftragsskript, den Startbefehl und die erwartete Ausgabe.

Das Einrichten eines HPC-Python-Workflows erfordert das Verständnis von Python-Wissenschaftspaketen, paralleler Programmierung und Cluster-Infrastruktur. Wenn Sie Hilfe bei der Parallelisierung von MPI, der Jobplanung oder der Reproduzierbarkeit der Umgebung benötigen, kann unser Team skalierbare wissenschaftliche Python-Workflows unterstützen, einschließlich FIPY-basierter Simulationen und benutzerdefinierten Monte-Carlo-Motoren.