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.
Die folgenden externen Ressourcen könnten dir hilfreich sein:
- Die CPython-Dokumentation zum Schreiben von Erweiterungsmodulen.
- Die NumPy-Dokumentation zur Verwendung der C-API.
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.
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.tomldefiniert die standardmäßigen statischen Metadaten über das Python-Paket, das wir erstellen, einschließlich Name, Autor sowie Build- und Laufzeit-Abhängigkeiten.setup.pyenthält die minimale dynamische Konfiguration, die wir zum Erstellen unseres Erweiterungsmoduls benötigen.src/spectator_measures/__init__.pydefiniert die nutzerseitige Schnittstelle und stellt Code bereit, um mit Qiskits Python-seitigen Komponenten zu interagieren.src/spectator_measures/_coremodule.cdefiniert 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.
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
Die folgenden Ressourcen könnten dir hilfreich sein:
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-seitigenDAGCircuit.QkTarget *, entsprechend dem Python-seitigenTarget.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:
- Folgt einer definierten Signatur, um beliebige Python-Argumente entgegenzunehmen.
- Reserviert Speicher für aus den Python-Argumenten geparste native C-Objekte.
- 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.
- Delegiert an die native C-Geschäftslogik des vorherigen Abschnitts, die den DAG in-place mutiert.
- 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