Zum Inhalt springen

GridSearchCV beschleunigen: Erst Parallelität und RAM messen, dann Hardware aufrüsten

Ein scikit-learn-Notebook läuft trotz zusätzlicher CPU-Kerne kaum schneller? Das muss kein Hardwarefehler sein. Bei der Modellsuche konkurrieren Trainingsläufe um RAM, während numerische Bibliotheken eigene Threads starten können.

n_jobs=-1 allein ist deshalb noch keine Ressourcenplanung. Ausgangspunkt sollte ein reproduzierbarer Versuch sein: dieselben Daten, dieselben Validierungsfalten und ein kleiner, unveränderter Suchraum. Damit lässt sich unterscheiden, ob CPU-Zeit, RAM oder die Organisation der Berechnung bremst.

Wer mit der Suchanfrage „server hardware kaufen“ Plattformen vergleicht, sollte dafür ein Lastprofil mitbringen: maximaler Speicherbedarf eines Trainingslaufs, sinnvoll nutzbare Parallelität und gewünschte Laufzeit des gesamten Versuchs. Eine hohe Kernzahl allein beantwortet keine dieser drei Fragen.

1. Aus einem kleinen Raster werden viele Trainingsläufe

Angenommen, für einen Random Forest werden drei Werte für die maximale Baumtiefe und zwei Werte für die minimale Blattgröße geprüft. Das sind sechs Parameterkombinationen. Mit fünf Validierungsfalten entstehen 6 × 5 = 30 Modellanpassungen. Ist refit=True, folgt anschließend eine weitere Anpassung mit der ausgewählten Konfiguration auf allen übergebenen Trainingsdaten. Diese Unterscheidung ist wichtig: Eine schnelle einzelne Modellanpassung bedeutet noch keinen kurzen Suchlauf.

Ein hypothetisches Beispiel: Dauert jede der 30 Anpassungen drei Minuten, ergeben sich seriell 90 Minuten reine Anpassungszeit. Mit vier Workern wären acht Durchgänge nötig, davon der letzte mit zwei Aufgaben: idealisiert 24 Minuten. In Wirklichkeit sind die Aufgaben unterschiedlich lang; Prozessstart, Datenübergabe und Bewertung kosten zusätzlich Zeit. Auch der Refit kommt hinzu. Das ist eine Modellrechnung, kein Benchmark.

Vor einer großen Suche lohnt ein Probelauf mit wenigen Kombinationen. Fachlich plausible Parameterbereiche gehören zur Ressourcenplanung: Eine schnellere CPU ersetzt keinen sinnvoll begrenzten Suchraum.

2. Äußere Prozesse und innere Threads auseinanderhalten

Die scikit-learn-Dokumentation unterscheidet Parallelität über joblib, OpenMP und BLAS.

n_jobs steuert die joblib-Ebene, nicht pauschal sämtliche Threads des Programms. Werden beispielsweise acht Aufgaben parallel ausgeführt und fordert jede intern acht Threads an, können rechnerisch 64 Threads um die CPU konkurrieren. Das erzeugt nicht automatisch mehr Rechenleistung.

Allerdings wäre auch die Behauptung falsch, scikit-learn vervielfache Threads grundsätzlich ungebremst: Das standardmäßig verwendete joblib-Backend loky begrenzt innere Threadpools normalerweise. Explizite Umgebungsvariablen und andere Backends können das Verhalten verändern. Deshalb gehören Backend, Thread-Einstellungen und Bibliotheksversionen ins Versuchsprotokoll. Bei verschachtelten Aufrufen sollte zunächst eine Ebene parallel arbeiten, während die andere begrenzt bleibt.

3. Speicherbedarf nicht aus der Dateigröße ableiten

Eine komprimierte Datei auf der SSD ist kein Maß für den RAM-Bedarf. Für eine dichte numerische Matrix lässt sich wenigstens der reine Datenbereich berechnen: Eine Million Zeilen mit 100 Merkmalen in float64 benötigen 1.000.000 × 100 × 8 = 800.000.000 Byte, also rund 763 MiB. In float32 wäre es die Hälfte. Das beschreibt nur die Elemente der Matrix, nicht das gesamte Python-Programm.

NumPy weist diesen Datenbereich über X.nbytes aus. Zielvariable, Zwischenprodukte und Modell kommen hinzu. Vor einem Datentypwechsel muss geprüft werden, ob Präzision und Algorithmus dazu passen. Verarbeitungsschritte können neue Arrays erzeugen. Der Spitzenbedarf eines vollständigen Fits ist deshalb aussagekräftiger als die Größe des Ausgangsarrays.

Für eine erste Abschätzung kann man mit ausdrücklich gemessenen Zusatzbedarfen arbeiten. Angenommen, das System samt Ausgangsdaten belegt 6 GiB und jeder aktive Fit verursacht im Probelauf zusätzlich etwa 3 GiB privaten Speicher. Vier Fits ergäben rechnerisch 18 GiB, acht bereits 30 GiB. Das ist ein Planungsmodell, keine allgemeine Speicherformel für scikit-learn. Gemeinsam genutzte Speicherseiten dürfen dabei nicht mehrfach gezählt werden.

Joblib kann große NumPy-Arrays per Memory-Mapping zwischen Prozessen teilen. Das beseitigt aber nicht den Bedarf für neu erzeugte Trainingsausschnitte, Transformationen oder Modelle. Relevant ist auch der temporäre Speicherort: Laut joblib-Dokumentation kommen unter Linux unter anderem /dev/shm und das System-Temp-Verzeichnis infrage. Ein knapp bemessenes temporäres Dateisystem kann deshalb den Versuch stoppen, obwohl auf einem anderen Laufwerk viel Platz frei ist.

4. Mit begrenzter Parallelität anfangen

Das Muster setzt numerische Trainingsdaten X_train und eine binäre Zielvariable y_train mit genügend Fällen beider Klassen für fünf Falten voraus. Ein separates Testset bleibt außerhalb der Suche. Die Beobachtungen werden hier als unabhängig angenommen.

from joblib import parallel_config

from sklearn.ensemble import RandomForestClassifier

from sklearn.impute import SimpleImputer

from sklearn.model_selection import GridSearchCV, StratifiedKFold

from sklearn.pipeline import Pipeline

 

pipeline = Pipeline([

    ("imputer", SimpleImputer(strategy="median")),

    ("model", RandomForestClassifier(n_jobs=1, random_state=42)),

])

grid = {

    "model__max_depth": [10, 20, None],

    "model__min_samples_leaf": [1, 5],

}

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

search = GridSearchCV(

    pipeline, grid, cv=cv, scoring="roc_auc",

    n_jobs=4, pre_dispatch=4, refit=True, error_score="raise",

)

with parallel_config(backend="loky", inner_max_num_threads=1):

    search.fit(X_train, y_train)

Hier parallelisiert die Suche vier Aufgaben; der Random Forest baut seine Bäume innerhalb einer Aufgabe nicht zusätzlich über mehrere joblib-Worker auf. pre_dispatch=4 begrenzt die vorab disponierten Aufgaben, ist aber kein RAM-Limit. inner_max_num_threads=1 begrenzt unterstützte native Threadpools in den loky-Workern. Der gesonderte Refit im Hauptprozess ist bei der Messung ebenfalls zu berücksichtigen.

Fehlende Werte werden innerhalb der Pipeline ersetzt. So wird der Median in jeder Falte nur aus deren Trainingsanteil gelernt. Würde man ihn vor der Kreuzvalidierung aus sämtlichen Daten berechnen, könnten Informationen aus dem Validierungsanteil einfließen. Für wiederholte Messungen bleiben Datenaufteilung und Zufallszustände gleich. Bei Zeitreihen oder Gruppen muss stattdessen eine fachlich passende Aufteilung gewählt werden.

5. Drei Messgrößen reichen für den ersten Vergleich

Vergleiche denselben Suchlauf mit einem, zwei und vier Workern, bei ausreichend RAM auch mit acht. Wiederhole jede Variante unter ähnlichen Bedingungen. Prozessstart beim ersten Aufruf, konkurrierende Anwendungen und bereits geladene Daten beeinflussen den Vergleich.

  • Gesamtlaufzeit: Zeit vom Beginn bis zum Ende von fit, einschließlich Bewertung und Refit.
  • Speicherspitze: Bedarf des gesamten Prozessverbunds, nicht nur des Notebook-Kernels; gemeinsam genutzte Seiten korrekt berücksichtigen.
  • Modellqualität: unveränderte Bewertungsmetrik und identische Validierungsaufteilung, damit eine Beschleunigung nicht durch ein anderes Experiment erkauft wird.

Ein hypothetisches Ergebnis von 40 Minuten mit einem und 24 Minuten mit vier Workern entspricht einem Speedup von 40 / 24, also etwa 1,67. Bringen acht Worker danach nur noch 23 Minuten, aber deutlich mehr Speicherbedarf, ist die höhere Parallelität kaum attraktiv. Für einen gemeinsam genutzten Rechner kann die kleinere Einstellung sogar die verlässlichere Wahl sein.

Wann sich zusätzliche Hardware tatsächlich begründen lässt

Mehr RAM ist plausibel, wenn die gewünschte Zahl paralleler Fits wiederholt an der gemessenen Speicherspitze scheitert. Mehr CPU-Ressourcen sind plausibel, wenn zusätzliche Worker im Versuch noch einen deutlichen Zeitgewinn bringen und weder Speicher noch Datenzugriff begrenzen. Eine schnellere SSD ersetzt wiederum keinen fehlenden RAM und macht einen bereits vollständig im Speicher arbeitenden Algorithmus nicht automatisch schneller.

Die belastbare Anforderung lautet dann: sechs unveränderte Kandidaten mit fünf Falten innerhalb eines definierten Zeitfensters, bei dokumentierter Speicherspitze und reproduzierbarer Modellqualität. So werden Rechner sinnvoll vergleichbar.

Image
Niklas Lang

Seit 2020 bin ich als Machine Learning Engineer und Softwareentwickler tätig und beschäftige mich leidenschaftlich mit der Welt der Daten, Algorithmen und Softwareentwicklung. Neben meiner Arbeit in der Praxis unterrichte ich an mehreren deutschen Hochschulen, darunter die IU International University of Applied Sciences und die Duale Hochschule Baden-Württemberg, in den Bereichen Data Science, Mathematik und Business Analytics.

Mein Ziel ist es, komplexe Themen wie Statistik und maschinelles Lernen so aufzubereiten, dass sie nicht nur verständlich, sondern auch spannend und greifbar werden. Dabei kombiniere ich praktische Erfahrungen aus der Industrie mit fundierten theoretischen Grundlagen, um meine Studierenden bestmöglich auf die Herausforderungen der Datenwelt vorzubereiten.

Cookie Consent mit Real Cookie Banner