Cython Best Practices, Conventions und Knowledge#

Dieses Dokument enthält Tipps zur Entwicklung von Cython-Code in scikit-learn.

Tipps für die Entwicklung mit Cython in scikit-learn#

Tipps zur Erleichterung der Entwicklung#

  • Zeit, die für das Lesen der Cython-Dokumentation aufgewendet wird, ist keine verlorene Zeit.

  • Wenn Sie beabsichtigen, OpenMP zu verwenden: Unter MacOS implementiert die Systemverteilung von clang OpenMP nicht. Sie können das Paket compilers installieren, das auf conda-forge verfügbar ist und eine Implementierung von OpenMP enthält.

  • Die Aktivierung von Prüfungen kann hilfreich sein. Zum Beispiel zur Aktivierung von Boundscheck verwenden Sie

    export SKLEARN_ENABLE_DEBUG_CYTHON_DIRECTIVES=1
    
  • Beginnen Sie in einem Notebook von Grund auf neu, um zu verstehen, wie Sie Cython verwenden und schnell Feedback zu Ihrer Arbeit erhalten. Wenn Sie OpenMP für Ihre Implementierungen in Ihrem Jupyter Notebook verwenden möchten, fügen Sie zusätzliche Compiler- und Linker-Argumente in der Cython-Magie hinzu.

    # For GCC and for clang
    %%cython --compile-args=-fopenmp --link-args=-fopenmp
    # For Microsoft's compilers
    %%cython --compile-args=/openmp --link-args=/openmp
    
  • Um C-Code (z. B. einen Segfault) zu debuggen, verwenden Sie gdb mit

    gdb --ex r --args python ./entrypoint_to_bug_reproducer.py
    
  • Um einen Wert an Ort und Stelle zum Debuggen im cdef (nogil) Kontext zu erhalten, verwenden Sie

    with gil:
        print(state_to_print)
    
  • Beachten Sie, dass Cython f-Strings mit Ausdrücken wie {var=} nicht parsen kann, z. B.

    print(f"{test_val=}")
    
  • Der scikit-learn-Codebestand enthält viele nicht-einheitliche (verschmolzene) Typdefinitionen (Neudefinitionen). Derzeit gibt es laufende Arbeiten, um dies im gesamten Codebestand zu vereinfachen und zu vereinheitlichen. Stellen Sie bis dahin sicher, dass Sie verstehen, welche konkreten Typen letztendlich verwendet werden.

  • Sie finden diese Alias-Funktion möglicherweise praktisch, um einzelne Cython-Erweiterungen zu kompilieren

    # You might want to add this alias to your shell script config.
    alias cythonX="cython -X language_level=3 -X boundscheck=False -X wraparound=False -X initializedcheck=False -X nonecheck=False -X cdivision=True"
    
    # This generates `source.c` as if you had recompiled scikit-learn entirely.
    cythonX --annotate source.pyx
    
  • Die Verwendung der Option --annotate mit diesem Flag ermöglicht die Generierung eines HTML-Berichts zur Code-Annotation. Dieser Bericht zeigt Interaktionen mit dem CPython-Interpreter Zeile für Zeile an. Interaktionen mit dem CPython-Interpreter sollten in den rechenintensiven Abschnitten der Algorithmen so weit wie möglich vermieden werden. Weitere Informationen finden Sie in diesem Abschnitt des Cython-Tutorials.

    # This generates a HTML report (`source.html`) for `source.c`.
    cythonX --annotate source.pyx
    

Tipps zur Leistung#

  • Verstehen Sie das GIL im Kontext von CPython (welche Probleme es löst, was seine Grenzen sind) und gewinnen Sie ein gutes Verständnis dafür, wann Cython in C-Code ohne Interaktion mit CPython übersetzt wird, wann nicht und wann es nicht möglich ist (z. B. bei Interaktionen mit Python-Objekten, einschließlich Funktionen). In diesem Zusammenhang bietet PEP073 einen guten Überblick und Kontext sowie Wege zur Entfernung.

  • Stellen Sie sicher, dass Sie Prüfungen deaktiviert haben.

  • Bevorzugen Sie immer Memoryviews anstelle von cnp.ndarray, wenn möglich: Memoryviews sind leichtgewichtig.

  • Vermeiden Sie Memoryview-Slicing: Memoryview-Slicing kann in einigen Fällen kostspielig oder irreführend sein und wir sollten es besser nicht verwenden, auch wenn die Handhabung weniger Dimensionen in einem bestimmten Kontext vorzuziehen wäre.

  • Dekorieren Sie finale Klassen oder Methoden mit @final (dies ermöglicht das Entfernen von virtuellen Tabellen, wenn nötig)

  • Inline-Methoden und Funktionen, wenn es sinnvoll ist

  • Im Zweifelsfall lesen Sie den generierten C- oder C++-Code, wenn Sie können: "Je weniger C-Befehle und Indirektionen für eine Zeile Cython-Code, desto besser" ist eine gute Faustregel.

  • nogil-Deklarationen sind nur Hinweise: Wenn Sie die cdef-Funktionen als nogil deklarieren, bedeutet dies, dass sie ohne das Halten des GIL aufgerufen werden können, aber es gibt das GIL nicht frei, wenn Sie sie aufrufen. Sie müssen dies selbst tun, entweder indem Sie nogil=True explizit an cython.parallel.prange übergeben oder indem Sie einen expliziten Kontextmanager verwenden.

    cdef inline void my_func(self) nogil:
    
        # Some logic interacting with CPython, e.g. allocating arrays via NumPy.
    
        with nogil:
            # The code here is run as if it were written in C.
    
        return 0
    

    Dieser Punkt basiert auf diesem Kommentar von Stéfan’s Benhel

  • Direkte Aufrufe von BLAS-Routinen sind über Schnittstellen möglich, die in sklearn.utils._cython_blas definiert sind.

Verwendung von OpenMP#

Da scikit-learn auch ohne OpenMP erstellt werden kann, ist es notwendig, jeden direkten Aufruf von OpenMP zu schützen.

Das Modul _openmp_helpers, verfügbar in sklearn/utils/_openmp_helpers.pyx, bietet geschützte Versionen der OpenMP-Routinen. Um OpenMP-Routinen zu verwenden, müssen sie aus diesem Modul cimportiert und nicht direkt aus der OpenMP-Bibliothek.

from sklearn.utils._openmp_helpers cimport omp_get_max_threads
max_threads = omp_get_max_threads()

Die parallele Schleife, prange, ist bereits durch Cython geschützt und kann direkt aus cython.parallel verwendet werden.

Typen#

Cython-Code erfordert die Verwendung expliziter Typen. Dies ist einer der Gründe für die Leistungssteigerung. Um Code-Duplizierung zu vermeiden, haben wir einen zentralen Ort für die am häufigsten verwendeten Typen in sklearn/utils/_typedefs.pxd. Idealerweise schauen Sie dort zuerst nach und cimportieren die benötigten Typen, zum Beispiel

from sklearn.utils._typedefs cimport float32, float64