Zum Hauptinhalt springen

Qiskit in Python mit C erweitern

Die Qiskit-C-API kann innerhalb von Python-Erweiterungsmodulen genutzt werden. Du kannst leistungskritische Abschnitte deiner Qiskit-Erweiterungen in C schreiben, um sie zu beschleunigen, und sie dann sicher an deine Nutzer weitergeben.

Diese Anleitung führt dich durch den Prozess, ein vollständiges Erweiterungsmodul zu definieren, seinen Build-Prozess zu konfigurieren und es Python-Nutzern zugänglich zu machen. Das Paket bietet eine einfache Portierung von AddSpectatorMeasures aus den Qiskit-Addons nach C. Das ist ein echter benutzerdefinierter Pass mit einem echten Anwendungsfall in den Qiskit-Addons.

tipp

Die folgenden externen Ressourcen könnten dir hilfreich sein:

Die Qiskit-C-API wird für Python-Erweiterungsmodule auf eine sehr ähnliche Weise wie die NumPy-C-API bereitgestellt. Wenn du bereits ein NumPy-Erweiterungsmodul programmiert hast, wird dir der Qiskit-Prozess vertraut vorkommen.

warnung

Die Qiskit-C-API ist noch experimentell. Daher gibt es noch keine vollständig stabile Programmier- oder Binärschnittstelle, und es kann zwischen Minor-Versionen zu Breaking Changes kommen.

Zum Beispiel ist ein Erweiterungsmodul, das zur Build-Zeit Qiskit v2.4.0 verwendet, garantiert mit Qiskit v2.4.1 zur Laufzeit kompatibel, könnte aber beim Einsatz von Qiskit v2.5.0 zur Laufzeit nicht mehr funktionieren.

Voraussetzungen

Starte in einem leeren Verzeichnis.

Du musst die Standard-C-Compiler-Toolchain für deine Plattform verfügbar haben. Außerdem benötigst du eine Python-Version, die ihre C-API-Header enthält (das ist Standard).

Du solltest mit den einzelnen Funktionen und Objekten der Qiskit-C-API vertraut sein oder bereit sein, sie nachzuschlagen. Du solltest über grundlegende Kenntnisse der C-Programmierung verfügen.

Verzeichnisstruktur erstellen

Wir verwenden eine src-basierte Verzeichnisstruktur und ein einfaches setuptools-basiertes Build-System. Diese Anweisungen lassen sich leicht auf jedes Build-System anpassen, das Erweiterungsmodule erstellen kann.

Die finale Struktur sieht so aus:

extension-module
├── pyproject.toml
├── setup.py
└── src
└── spectator_measures
├── __init__.py
└── _coremodule.c

Zusammenfassend:

  • pyproject.toml definiert die standardmäßigen statischen Metadaten über das Python-Paket, das wir erstellen, einschließlich Name, Autor sowie Build- und Laufzeit-Abhängigkeiten.
  • setup.py enthält die minimale dynamische Konfiguration, die wir zum Erstellen unseres Erweiterungsmoduls benötigen.
  • src/spectator_measures/__init__.py definiert die nutzerseitige Schnittstelle und stellt Code bereit, um mit Qiskits Python-seitigen Komponenten zu interagieren.
  • src/spectator_measures/_coremodule.c definiert das C-Erweiterungsmodul, das den gesamten leistungskritischen Code unseres Pakets enthalten wird.

Wir werden jede Datei im Detail untersuchen und das Paket mit seinem Erweiterungsmodul aufbauen.

Paket-Metadaten definieren

Beginne mit der Definition der Datei pyproject.toml. Dies ist standard für ein setuptools-basiertes Projekt, wobei qiskit eine zusätzliche Anforderung im Array build-system.requires ist, zusätzlich zu setuptools.

pyproject.toml

[build-system]
requires = [
"setuptools",
"qiskit~=2.4.0",
]
build-backend = "setuptools.build_meta"

[project]
name = "spectator_measures"
authors = [
{ name = "Qiskit Developer" },
]
version = "0.0.1"
dependencies = [
"qiskit~=2.4.0",
]
# If you intend to release your package, you should
# also set the `license` information, and so on.

[tool.setuptools]
package-dir = {"" = "src"}

Ab Qiskit v2.4 ist die C-API außerhalb von Minor-Versionen noch nicht stabil (zum Beispiel ist die C-API für v2.4.0 kompatibel mit v2.4.1, aber nicht mit v2.5.0). In Zukunft beabsichtigen wir, diese Stabilität auf Major-Versionen auszuweiten. Setze vorerst die Laufzeitversion von Qiskit in project.dependencies so, dass sie der beim Build verwendeten Minor-Version entspricht.

In vielen reinen Python-setuptools-Projekten würde die Datei pyproject.toml ausreichen. Unser Modul benötigt jedoch während seines Build-Prozesses Zugriff auf die Qiskit-C-API-Header-Dateien. Ab v2.4 sind diese in den Qiskit-SDK-Python-Distributionen enthalten. Um das Verzeichnis zu finden, das sie enthält, führe qiskit.capi.get_include() aus. Das ergibt eine setup.py-Datei, die so aussieht:

setup.py

import qiskit
from setuptools import setup, Extension

core_ext = Extension(
# The fully qualified module name of the extension.
name="spectator_measures._core",
# The C source files needed for the extension. The file
# name is conventionally `<mod>module.c`, where `<mod>`
# is the module name (`_core`, in this case).
sources=["src/spectator_measures/_coremodule.c"],
# Directories containing additional header files used in
# the build process.
include_dirs=[qiskit.capi.get_include()],
)
setup(ext_modules=[core_ext])

Die meisten Paketinformationen sind in pyproject.toml definiert, und setuptools.setup() liest diese Datei ebenfalls.

tipp

Weitere Informationen zur Konfiguration von setuptools-basierten Projekten findest du im setuptools User Guide.

Python-seitigen Wrapper schreiben

Es ist technisch möglich, alles in einer Python-Erweiterung in C zu definieren. In der Praxis ist es einfacher, direkt in Python mit anderem Python-seitigen Code zu interagieren.

Dieses Paket definiert einen benutzerdefinierten Transpiler-Pass, der von der Python-seitigen qiskit.transpiler.TransformationPass-Klasse ableitet, aber eine Funktion aus dem C-Erweiterungsmodul für die gesamte Geschäftslogik verwendet. Das sieht so aus:

src/spectator_measures/__init__.py

from qiskit.transpiler import TransformationPass, Target
from . import _core

__version__ = "0.0.1"
__all__ = ["AddSpectatorMeasures"]

class AddSpectatorMeasures(TransformationPass):
def __init__(
self,
target: Target,
*,
include_unmeasured: bool = False,
creg_name: str | None = None,
add_barrier: bool = True
):
super().__init__()
self.target = target
self.include_unmeasured = include_unmeasured
self.creg_name = creg_name
self.add_barrier = add_barrier

def run(self, dag):
# Delegate to our C extension module.
_core.add_spectator_measures(
dag,
self.target,
include_unmeasured=self.include_unmeasured,
creg_name=self.creg_name,
add_barrier=self.add_barrier,
)
return dag

Die genauen Details dieses Passes sind für diese Anleitung unwesentlich. Wenn du interessiert bist, kannst du die AddSpectatorMeasures-API-Dokumentation in qiskit-addon-utils konsultieren. Diese Anleitung erstellt eine einfache Portierung dieses Passes, ohne Unterstützung für Kontrollfluss-Operationen.

C-Erweiterungsmodul schreiben

Dieser Abschnitt befasst sich mit der eigentlichen C-Erweiterung. Das ist die komplexeste Datei im Projekt, daher werden wir sie in Phasen aufteilen.

Header-Dateien konfigurieren

Beim Erstellen eines Python-Erweiterungsmoduls musst du Python.h vor jeder anderen Datei einbinden. Um die Qiskit-C-API in einem Erweiterungsmodul zu verwenden, musst du das Makro QISKIT_PYTHON_EXTENSION definieren, bevor du qiskit.h einbindest.

Unsere Includes sehen dann so aus:

src/spectator_measures/_coremodule.c

#define QISKIT_PYTHON_EXTENSION
#include <Python.h>
#include <qiskit.h>

#include <limits.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>

Reinen C-API-Code schreiben

Als Nächstes schreibe die gesamte Geschäftslogik als reinen Qiskit-C-API-Code. Diese Logik werden wir im folgenden Abschnitt für Python-Space verfügbar machen.

Dieser Abschnitt enthält ausschließlich reinen Qiskit-C-API-Code. Er verwendet die C-API-Typen:

  • QkDag *, entsprechend dem Python-seitigen DAGCircuit.
  • QkTarget *, entsprechend dem Python-seitigen Target.
  • QkNeighbors, ein nativer C-API-Typ zur Darstellung von Zwei-Qubit-Kopplungsbeschränkungen.
  • QkCircuitInstruction, ein nativer C-API-Typ zum Abfragen einzelner Instruktionen.

Die ersten beiden sind Teil unserer Interaktion mit Python-Space, aber wenn wir mit ihnen arbeiten, müssen wir nur die reine C-API berücksichtigen. In diesem Code gibt es keine Interaktion mit dem Python-Interpreter.

Beachte, dass alle Funktionen und Symbole, die in diesem Abschnitt definiert werden, mit static-Bindung deklariert sind. Das liegt daran, dass der Python-Interpreter nicht gegen dieses Erweiterungsmodul linkt; wir werden dem Interpreter die Details der verfügbaren Funktionen im nächsten Abschnitt bereitstellen.

Wir werden uns nicht bei den algorithmischen Details dieses Codes aufhalten; es ist lehrreich, einen bedeutungsvollen Transpiler-Pass für die Demonstration zu verwenden, aber die genaue Implementierung des Algorithmus ist für diese Anleitung nicht wichtig.

src/spectator_measures/_coremodule.c (appended)

/**
* The default name to use for `creg_name` if none is supplied.
*/
static char DEFAULT_CREG_NAME[] = "spec";

/**
* Is there a 2q link from the given qubit to any active qubit?
*/
static bool adjacent_to_active(QkNeighbors *adj, uint32_t qubit,
bool *active) {
for (uint32_t offset = adj->partition[qubit];
offset < adj->partition[qubit + 1]; offset++) {
if (active[adj->neighbors[offset]]) {
return true;
}
}
return false;
}

/**
* A transpiler pass that adds terminal measurements to all "spectator"
* qubits.
*/
static uint32_t add_spectator_measures(QkDag *dag,
const QkTarget *target,
bool include_unmeasured,
const char *creg_name,
bool add_barrier) {
uint32_t num_spectators = 0;
uint32_t num_qubits = qk_dag_num_qubits(dag);
uint32_t num_instructions = qk_dag_num_op_nodes(dag);
bool *active = calloc(num_qubits, sizeof(*active));
bool *is_additional_spectator =
calloc(num_qubits, sizeof(*is_additional_spectator));
uint32_t *spectators = malloc(num_qubits * sizeof(*spectators));
uint32_t *topological =
malloc(num_instructions * sizeof(*topological));
QkNeighbors neighbors;
QkCircuitInstruction instruction;

qk_neighbors_from_target(target, &neighbors);
qk_dag_topological_op_nodes(dag, topological);

for (uint32_t i = 0; i < num_instructions; i++) {
qk_dag_get_instruction(dag, topological[i], &instruction);
if (!strcmp(instruction.name, "barrier")) {
// Barriers don't count for the purposes of determining
// final measurements, either.
qk_circuit_instruction_clear(&instruction);
continue;
}
// If we're not adding measurements to "unmeasured" active
// qubits, then nothing counts as an additional "maybe
// spectator". If we are, then it's a maybe spectator if its
// last visited instruction was not a measure.
bool additional_spectator =
include_unmeasured && strcmp(instruction.name, "measure");
for (uint32_t *qarg = instruction.qubits;
qarg != instruction.qubits + instruction.num_qubits;
qarg++) {
active[*qarg] = true;
is_additional_spectator[*qarg] = additional_spectator;
}
qk_circuit_instruction_clear(&instruction);
}

for (uint32_t qubit = 0; qubit < num_qubits; qubit++) {
bool is_spectator =
!active[qubit] &&
adjacent_to_active(&neighbors, qubit, active);
is_spectator = is_spectator || is_additional_spectator[qubit];
if (is_spectator) {
spectators[num_spectators] = qubit;
num_spectators += 1;
}
}

if (num_spectators) {
uint32_t clbit = qk_dag_num_clbits(dag);
creg_name = creg_name ? creg_name : DEFAULT_CREG_NAME;
QkClassicalRegister *creg =
qk_classical_register_new(num_spectators, creg_name);
qk_dag_add_classical_register(dag, creg);
qk_classical_register_free(creg);
if (add_barrier) {
qk_dag_apply_barrier(dag, NULL, num_qubits, false);
}
for (uint32_t i = 0; i < num_spectators; i++) {
qk_dag_apply_measure(dag, spectators[i], clbit + i, false);
}
}

qk_neighbors_clear(&neighbors);
free(topological);
free(spectators);
free(is_additional_spectator);
free(active);
return num_spectators;
}

Python-Interaktionscode schreiben

Die gesamte Geschäftslogik ist nun in reinem C definiert. Als Nächstes muss sie sicher für Python zugänglich gemacht werden.

Definiere zunächst die einzige Funktion, die Python zugänglich gemacht wird. Diese muss einer definierten Signatur folgen, die ausschließlich Python-Typen verwendet und wie eine fn(self, *args, **kwargs)-Methode aussieht. Wir müssen einen PyObject * zurückgeben, die generische Form eines beliebigen Python-Objekts.

Die vollständige Funktion sieht so aus:

src/spectator_measures/_coremodule.c (appended)

static PyObject *py_add_spectator_measures(PyObject *self,
PyObject *args,
PyObject *kwargs) {
// Define space to hold the C-native handles we will parse out of the
// Python-space inputs.
QkDag *dag;
QkTarget *target;
const char *creg_name;
int include_unmeasured, add_barrier;

// This `kwlist` and `PyArg_Parse*` setup is standard Python C API
// programming for extension modules. We will examine the use of
// Qiskit C API functions within it afterwards.
static char *const kwlist[] = {
"dag", "target", "include_unmeasured",
"creg_name", "add_barrier", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&O&|pzp", kwlist,
qk_dag_convert_from_python, &dag,
qk_target_convert_from_python,
&target, &include_unmeasured,
&creg_name, &add_barrier)) {
// An error has occurred. The Python exception state will already
// be set, so we need to return the error indicator.
return NULL;
}

// Now we have C-native types, we can delegate to our C logic.
add_spectator_measures(dag, target, include_unmeasured, creg_name,
add_barrier);
Py_RETURN_NONE;
}

Kurz gesagt, die Funktion:

  1. Folgt einer definierten Signatur, um beliebige Python-Argumente entgegenzunehmen.
  2. Reserviert Speicher für aus den Python-Argumenten geparste native C-Objekte.
  3. Ruft eine Parse-Funktion auf, um die nativen C-Objekte zu extrahieren, konfiguriert mit der Liste der erwarteten Argumente, Schlüsselwortargumente und der zu verwendenden Konvertierungsfunktionen. Wenn dies fehlschlägt, gibt die Funktion den Fehler weiter.
  4. Delegiert an die native C-Geschäftslogik des vorherigen Abschnitts, die den DAG in-place mutiert.
  5. Gibt das Python-seitige None-Objekt zurück.

Die komplexeste Logik befindet sich vollständig innerhalb von PyArg_ParseTupleAndKeywords. Dies ist in der CPython-Dokumentation zum Parsen von Argumenten gut dokumentiert, die du für weitere Informationen konsultieren solltest.

Die Qiskit-C-API stellt mehrere Funktionen mit Namen wie qk_*_convert_from_python bereit, die als "Konverter"-Funktionen für die Verwendung mit PyArg_Parse*-Funktionen konzipiert sind. Diese entsprechen den O&-Schlüsseln im Formatstring; hier haben wir qk_dag_convert_from_python und qk_target_convert_from_python verwendet. Diese Funktionen leihen das native C-Objekt vom Python-Argument, aus dem sie abgeleitet werden. Das bedeutet, dass Mutationen in Python-Space propagiert werden, aber auch, dass du darauf achten solltest, deine Referenz auf das Python-Objekt, das sie unterstützt, nicht freizugeben, während du das Ergebnis verwendest. Das ist Standard in der Python-C-API-Programmierung.

Als Nächstes definieren wir die Informationen über dieses Modul und die darin enthaltene Funktion, damit wir sie Python-Space übergeben können:

src/spectator_measures/_coremodule.c (appended)

static PyMethodDef core_methods[] = {
// This entry is our function, cast to the correct type.
{"add_spectator_measures",
(PyCFunction)(void (*)(void))py_add_spectator_measures,
METH_VARARGS | METH_KEYWORDS, ""},
// A sentinel marking the end of the list.
{NULL, NULL, 0, NULL},
};
static struct PyModuleDef core_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "_core",
.m_methods = core_methods,
};

Diese Methodentabelle und Moduldefinitionsstruktur sind in der CPython-Dokumentation zur Modul-Initialisierung ausführlicher beschrieben.

Schließlich teile Python mit, wie das Modul initialisiert werden soll. Das ist die einzige Funktion in der C-Datei, die exportiert wird. Ihr Name muss genau dem Muster PyInit_<mod> entsprechen, wobei <mod> der (unqualifizierte) Modulname ist. In diesem Fall ist der voll qualifizierte Modulname spectator_measures._core, und der unqualifizierte Name ist _core, daher muss unsere Funktion PyInit__core heißen, mit dem doppelten Unterstrich.

src/spectator_measures/_coremodule.c (appended)

PyMODINIT_FUNC PyInit__core(void) {
// This line is critical to use the Qiskit C API. Your code will
// likely be immediately terminated by the operating system if you
// forget to do this.
if (qk_import() < 0) {
return NULL;
};
// The standard Python call to initialize a module.
return PyModuleDef_Init(&core_module);
}

Die Symbole PyMODINIT_FUNC und PyModuleDef_Init sind beide Standard-Python-C-API-Programmierung. Die Qiskit-spezifische Komponente ist qk_import(). Es ist wichtig, dass du diese Funktion während der Initialisierungsfunktion deines Moduls aufrufst; du wirst keine Qiskit-C-API-Funktionen aufrufen können, bis diese erfolgreich ausgeführt wurde.

Das Paket aus Python verwenden

Dies ist nun ein vollständiges Paket, einschließlich eines C-Erweiterungsmoduls. Da nur Standard-Tooling verwendet wurde und keine nicht-standardmäßigen Systembibliotheken zur Build-Zeit gelinkt werden, ist der Build-Prozess einfach.

Du kannst ein beliebiges PEP-517-kompatibles Build-Tool verwenden. Als minimales Beispiel kannst du den folgenden Befehl im Repository-Root ausführen, um das Paket zu installieren.

pip install .

Dadurch wird das C-Erweiterungsmodul kompiliert und das vollständige Python-Paket in deiner Umgebung installiert.

Ein Beispiel für die Verwendung dieses benutzerdefinierten Transpiler-Passes ist:

from qiskit import QuantumCircuit
from qiskit.transpiler import CouplingMap, Target
from spectator_measures import AddSpectatorMeasures

num_qubits = 10
qc = QuantumCircuit(num_qubits)
qc.x(0)
qc.x(5)

target = Target.from_configuration(
basis_gates=["x", "sx", "rz", "cx"],
num_qubits=num_qubits,
coupling_map=CouplingMap.from_line(num_qubits),
)
pass_ = AddSpectatorMeasures(target)
pass_(qc).draw()

Das Ergebnis davon ist:

        ┌───┐ ░
q_0: ┤ X ├─░──────────
└───┘ ░ ┌─┐
q_1: ──────░─┤M├──────
░ └╥┘
q_2: ──────░──╫───────
░ ║
q_3: ──────░──╫───────
░ ║ ┌─┐
q_4: ──────░──╫─┤M├───
┌───┐ ░ ║ └╥┘
q_5: ┤ X ├─░──╫──╫────
└───┘ ░ ║ ║ ┌─┐
q_6: ──────░──╫──╫─┤M├
░ ║ ║ └╥┘
q_7: ──────░──╫──╫──╫─
░ ║ ║ ║
q_8: ──────░──╫──╫──╫─
░ ║ ║ ║
q_9: ──────░──╫──╫──╫─
░ ║ ║ ║
spec: 3/═════════╩══╩══╩═
0 1 2