ข้ามไปยังเนื้อหาหลัก

ขยาย Qiskit ใน Python ด้วย C

Qiskit C API สามารถใช้งานภายใน Python extension module ได้ คุณสามารถเขียนส่วนที่สำคัญต่อ performance ของ Qiskit extension ของคุณด้วย C เพื่อเร่งความเร็ว แล้ว แจกจ่ายสิ่งเหล่านี้ให้กับผู้ใช้ของคุณได้อย่างปลอดภัย

คู่มือนี้จะพาคุณไปตามกระบวนการกำหนด extension module ที่สมบูรณ์ การกำหนดค่า build process ของมัน และการเปิดเผยให้ผู้ใช้ Python ใช้งานได้ แพ็กเกจนี้นำเสนอ port อย่างง่ายของ AddSpectatorMeasures จาก Qiskit addons ไปยัง C นี่คือ custom pass จริง ที่มี use case จริงใน Qiskit addons

เคล็ดลับ

คุณอาจพบว่าแหล่งข้อมูลภายนอกต่อไปนี้มีประโยชน์:

Qiskit C API ถูกเปิดเผยสำหรับ Python extension module ในลักษณะที่คล้ายกับ NumPy C API มาก หากคุณเคยเขียน NumPy extension มาก่อน คุณจะพบว่ากระบวนการของ Qiskit คุ้นเคย

คำเตือน

Qiskit C API ยังอยู่ในระยะทดลอง ดังนั้นจึงยังไม่มี programming หรือ binary interface ที่เสถียรอย่างสมบูรณ์ และอาจมีการเปลี่ยนแปลงที่ทำให้เกิด breaking ระหว่าง minor version

ตัวอย่างเช่น extension module ที่ใช้ Qiskit v2.4.0 ในตอน build รับประกันว่าจะทำงานได้กับ Qiskit v2.4.1 ในตอน runtime แต่อาจ break เมื่อใช้ Qiskit v2.5.0 ในตอน runtime

ข้อกำหนด

เริ่มต้นจากไดเรกทอรีที่สะอาด

คุณต้องมี C compiler toolchain มาตรฐานพร้อมใช้งานสำหรับ platform ของคุณ และคุณต้องมี Python เวอร์ชันที่รวม C API header ของมันด้วย (ซึ่งเป็นมาตรฐาน)

คุณควรคุ้นเคยกับ หรือพร้อมที่จะค้นหา ฟังก์ชันและ object แต่ละตัวที่มีอยู่ ใน Qiskit C API คุณควรมีความคุ้นเคยกับการเขียนโปรแกรม C อยู่บ้าง

สร้างโครงสร้างไดเรกทอรี

เราจะใช้โครงสร้างไดเรกทอรีแบบ src และ build system แบบ setuptools อย่างง่าย คำแนะนำ เหล่านี้ควรปรับเข้ากับ build system ใดก็ตามที่สามารถ build extension module ได้ง่าย

โครงสร้างสุดท้ายจะมีลักษณะดังนี้:

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

โดยสรุป:

  • pyproject.toml กำหนด metadata เชิงสถิตมาตรฐานเกี่ยวกับ Python package ที่เรากำลังสร้าง รวมถึงชื่อ ผู้เขียน และ dependency ในตอน build และ run
  • setup.py มี dynamic configuration ขั้นต่ำที่เราต้องใช้ในการ build extension module ของเรา
  • src/spectator_measures/__init__.py กำหนด interface ที่ผู้ใช้เห็น และให้โค้ดบางส่วนเพื่อ เชื่อมต่อกับ component ฝั่ง Python ของ Qiskit
  • src/spectator_measures/_coremodule.c กำหนด C extension module ซึ่งจะมีโค้ด ที่สำคัญต่อ performance ทั้งหมดของแพ็กเกจของเรา

เราจะตรวจสอบแต่ละไฟล์อย่างละเอียด โดยสร้างแพ็กเกจขึ้นมาพร้อมกับ extension module ของมัน

กำหนด metadata ของแพ็กเกจ

เริ่มต้นด้วยการกำหนดไฟล์ pyproject.toml นี่เป็นมาตรฐานสำหรับโปรเจกต์แบบ setuptools แม้ว่า qiskit จะเป็น requirement เพิ่มเติมใน array build-system.requires นอกเหนือจาก 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"}

ตั้งแต่ Qiskit v2.4 เป็นต้นมา C API ยังไม่เสถียรนอกเหนือจาก minor version (ตัวอย่างเช่น C API สำหรับ v2.4.0 จะ เข้ากันได้กับ v2.4.1 แต่ไม่เข้ากับ v2.5.0) ในอนาคต เราตั้งใจจะขยาย ความเสถียรนี้ไปยัง major version สำหรับตอนนี้ ให้ตั้งค่า runtime version ของ Qiskit ใน project.dependencies ให้ตรงกับ minor version ที่ใช้ในตอน build

ในโปรเจกต์แบบ setuptools ที่เป็น pure-Python หลายตัว การมีไฟล์ pyproject.toml ก็เพียงพอแล้ว อย่างไรก็ตาม module ของเราต้องการเข้าถึงไฟล์ header ของ Qiskit C API ระหว่าง build process เริ่มตั้งแต่ v2.4 เป็นต้นไป สิ่งเหล่านี้รวมอยู่ใน Qiskit SDK Python distribution หากต้องการค้นหาไดเรกทอรีที่มีพวกมัน ให้รัน qiskit.capi.get_include() ผลลัพธ์คือไฟล์ setup.py ที่มีลักษณะดังนี้:

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

ข้อมูลส่วนใหญ่ของแพ็กเกจถูกกำหนดไว้ใน pyproject.toml และ setuptools.setup() จะ อ่านไฟล์นั้นด้วยเช่นกัน

เคล็ดลับ

ดู setuptools User Guide สำหรับข้อมูล เพิ่มเติมเกี่ยวกับการกำหนดค่าโปรเจกต์แบบ setuptools

เขียน wrapper ฝั่ง Python

ในทางเทคนิคแล้วสามารถกำหนดทุกอย่างใน Python extension จาก C ได้ ในทางปฏิบัติ มัน ง่ายกว่าที่จะโต้ตอบกับโค้ดฝั่ง Python อื่นๆ จาก Python เอง

แพ็กเกจนี้กำหนด custom transpiler pass ที่สืบทอดมาจากคลาส qiskit.transpiler.TransformationPass ฝั่ง Python แต่ใช้ฟังก์ชันจาก C extension module สำหรับ business logic ทั้งหมดของมัน มีลักษณะดังนี้:

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

รายละเอียดที่แน่ชัดของ pass นี้ไม่สำคัญสำหรับคู่มือนี้ หากคุณสนใจ คุณสามารถ ดู เอกสาร API ของ AddSpectatorMeasures ใน qiskit-addon-utils ได้ คู่มือนี้สร้าง port อย่างง่ายของ pass นั้น โดยไม่รองรับ control-flow operation

เขียน C extension module

เคล็ดลับ

ส่วนนี้เกี่ยวข้องกับ C extension จริง นี่เป็นไฟล์ที่ซับซ้อนที่สุดใน โปรเจกต์ ดังนั้นเราจะแบ่งมันออกเป็นขั้นตอน

กำหนดค่าไฟล์ header

เมื่อ build Python extension module คุณต้อง include Python.h ก่อนไฟล์อื่นใด หากต้องการใช้ Qiskit C API ใน extension module คุณต้องกำหนด macro QISKIT_PYTHON_EXTENSION ก่อนการ include qiskit.h

include ของเราจึงมีลักษณะดังนี้:

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>

เขียนโค้ด pure C API

ต่อไป เขียน business logic ทั้งหมดเป็นโค้ด pure Qiskit C API เราจะเปิดเผย logic นี้ให้ Python space ในส่วนถัดไป

ส่วนนี้มีเฉพาะโค้ด pure Qiskit C API เท่านั้น มันใช้ type ของ C API:

  • QkDag * ซึ่งสอดคล้องกับ DAGCircuit ฝั่ง Python
  • QkTarget * ซึ่งสอดคล้องกับ Target ฝั่ง Python
  • QkNeighbors ซึ่งเป็น native C API type ที่แทน two-qubit coupling constraint
  • QkCircuitInstruction ซึ่งเป็น native C API type สำหรับ query แต่ละ instruction

สองตัวแรกเป็นส่วนหนึ่งของการโต้ตอบกับ Python space ของเรา แต่เมื่อทำงานกับพวกมัน เรา ต้องพิจารณาเฉพาะ pure C API เท่านั้น ไม่มีการโต้ตอบกับ Python interpreter ในโค้ดนี้

โปรดทราบว่าฟังก์ชันและ symbol ทั้งหมดที่กำหนดในส่วนนี้ถูกประกาศด้วย static linkage นี่เป็นเพราะ Python interpreter จะไม่ link กับ extension module นี้ เราจะให้ interpreter ทราบรายละเอียดของฟังก์ชันที่มีในส่วนถัดไป

เราจะไม่ลงลึกในรายละเอียดเชิง algorithm ของโค้ดนี้ การใช้ transpiler pass ที่มีความหมาย สำหรับการสาธิตนั้นเป็นประโยชน์ แต่ implementation ที่แม่นยำของ algorithm ไม่ สำคัญต่อคู่มือนี้

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

ตอนนี้ business logic ทั้งหมดถูกกำหนดเป็น pure C แล้ว ต่อไป มันต้องถูกเปิดเผยให้ Python อย่างปลอดภัย

ในการเริ่มต้น ให้กำหนดฟังก์ชันเดียวที่จะถูกเปิดเผยให้ Python สิ่งนี้ต้อง เป็นไปตาม signature ที่กำหนด ซึ่งล้วนเป็น Python type ที่มีลักษณะเหมือน method fn(self, *args, **kwargs) เราต้องคืนค่า PyObject * ซึ่งเป็นรูปแบบทั่วไปของ Python object ใดๆ

ฟังก์ชันที่สมบูรณ์มีลักษณะดังนี้:

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;
}

โดยสรุป ฟังก์ชันนี้:

  1. เป็นไปตาม signature ที่กำหนดเพื่อรับ Python argument โดยพลการ
  2. กำหนด space เพื่อเก็บ C-native object ที่ parse ออกมาจาก Python argument
  3. เรียกฟังก์ชัน parse เพื่อแยก C-native object ออกมา โดยกำหนดค่าด้วยรายการ argument ที่คาดหวัง keyword argument และฟังก์ชันที่ใช้ในการแปลงพวกมัน หากล้มเหลว ฟังก์ชันจะ propagate error
  4. มอบหมายให้ business logic แบบ C-native ของส่วนก่อนหน้า ซึ่ง mutate DAG ในตำแหน่งเดิม
  5. คืนค่า object None ฝั่ง Python

logic ที่ซับซ้อนที่สุดทั้งหมดอยู่ภายใน PyArg_ParseTupleAndKeywords สิ่งนี้มีเอกสารอย่างดีใน เอกสาร CPython เกี่ยวกับการ parse argument ซึ่งคุณ ควรดูเพื่อข้อมูลเพิ่มเติม

Qiskit C API ให้ฟังก์ชันหลายตัวที่มีชื่อเช่น qk_*_convert_from_python ซึ่งถูก ออกแบบเป็นฟังก์ชัน "converter" สำหรับใช้กับฟังก์ชัน PyArg_Parse* สิ่งเหล่านี้สอดคล้องกับ key O& ใน format string ที่นี่ เราใช้ qk_dag_convert_from_python และ qk_target_convert_from_python ฟังก์ชันเหล่านี้ ยืม C-native object จาก Python argument ที่พวกมันได้มา หมายความว่าการ mutate จะ propagate ไปยัง Python space แต่ยังหมายความว่า คุณควรระวังไม่ปล่อย reference ของคุณไปยัง Python object ที่ค้ำพวกมันไว้ ขณะที่ใช้งาน ผลลัพธ์ นี่เป็นมาตรฐานสำหรับการเขียนโปรแกรม Python C API

ต่อไป เรากำหนดข้อมูลเกี่ยวกับ module นี้และฟังก์ชันที่มันมี เพื่อให้เราสามารถส่งไปยัง Python space ได้:

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,
};

method table และ module-definition structure นี้ถูกอธิบายอย่างละเอียดมากขึ้นใน เอกสาร CPython เกี่ยวกับการ initialize module

สุดท้าย บอก Python ว่าจะ initialize module อย่างไร นี่เป็นฟังก์ชันเดียวในไฟล์ C ที่ถูก export ชื่อของมัน ต้อง ตรงกับ pattern PyInit_<mod> พอดี โดยที่ <mod> คือชื่อ module (แบบไม่ qualified) ในกรณีนี้ ชื่อ module แบบ fully qualified คือ spectator_measures._core และชื่อแบบไม่ qualified คือ _core ดังนั้น ฟังก์ชันของเราต้องชื่อ PyInit__core โดยมี underscore สองตัว

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

symbol PyMODINIT_FUNC และ PyModuleDef_Init ล้วนเป็นการเขียนโปรแกรม Python C API มาตรฐาน component ที่เป็น Qiskit-specific คือ qk_import() เป็นสิ่งสำคัญที่คุณต้องเรียกฟังก์ชันนี้ระหว่าง ฟังก์ชัน initialize ของ module ของคุณ คุณจะไม่สามารถเรียกฟังก์ชัน Qiskit C API ใดๆ ได้จนกว่าจะดำเนินการนี้สำเร็จ

ใช้แพ็กเกจจาก Python

ตอนนี้นี่คือแพ็กเกจที่สมบูรณ์ รวมถึง C extension module เนื่องจากใช้เฉพาะ tool มาตรฐาน และไม่มี system library ที่ไม่เป็นมาตรฐานถูก link ระหว่าง build time build process จึงง่าย

คุณสามารถใช้ build tool ที่เข้ากันได้กับ PEP-517 ใดก็ได้ เป็นตัวอย่างขั้นต่ำ คุณสามารถรันคำสั่งต่อไปนี้ใน repository root เพื่อติดตั้งแพ็กเกจ

pip install .

สิ่งนี้ compile C extension module และ ติดตั้ง Python package ที่สมบูรณ์ในสภาพแวดล้อมของคุณ

ตัวอย่างการใช้ custom transpiler pass นี้คือ:

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

ผลลัพธ์ของสิ่งนี้คือ:

┌───┐ ░
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