주 콘텐츠로 건너뛰기

C로 Python에서 Qiskit 확장하기

Qiskit C API는 Python 확장 모듈 내에서 사용할 수 있습니다. 성능이 중요한 Qiskit 확장 섹션을 C로 작성하여 가속화하고, 사용자에게 안전하게 배포할 수 있습니다.

이 가이드에서는 완전한 확장 모듈을 정의하고, 빌드 프로세스를 구성하고, Python 사용자에게 노출하는 전체 과정을 안내합니다. 이 패키지는 Qiskit 애드온의 AddSpectatorMeasures를 C로 간단히 포팅한 것입니다. 이것은 Qiskit 애드온에서 실제 사용 사례를 가진 실제 커스텀 패스입니다.

팁

다음 외부 리소스가 도움이 될 수 있습니다:

Qiskit C API는 NumPy C API와 매우 유사한 방식으로 Python 확장 모듈에 노출됩니다. 이전에 NumPy 확장 모듈을 프로그래밍한 적이 있다면 Qiskit 방식이 익숙하게 느껴질 것입니다.

경고

Qiskit C API는 아직 실험적인 단계입니다. 따라서 완전히 안정적인 프로그래밍 또는 바이너리 인터페이스가 아직 확정되지 않았으며, 마이너 버전 간에 호환성이 깨지는 변경이 발생할 수 있습니다.

예를 들어, 빌드 시 Qiskit v2.4.0을 사용하는 확장 모듈은 런타임에 Qiskit v2.4.1과 함께 동작하는 것이 보장되지만, 런타임에 Qiskit v2.5.0을 사용할 경우 작동하지 않을 수 있습니다.

요구 사항​

다음을 설치하려면 설치 주제의 지침을 완료하세요:

  • 플랫폼용 표준 C 컴파일러 툴체인

  • C API 헤더를 포함하는 Python 버전.

또한 Qiskit C API에서 사용 가능한 함수와 객체를 숙지하거나, 필요할 때 찾아볼 준비가 되어 있어야 하며, C 프로그래밍에 어느 정도 익숙해야 합니다.

빈 디렉토리에서 시작합니다.

디렉토리 구조 만들기​

src 기반 디렉토리 구조와 간단한 setuptools 기반 빌드 시스템을 사용합니다. 이 지침은 확장 모듈을 빌드할 수 있는 모든 빌드 시스템에 쉽게 적용할 수 있습니다.

최종 구조는 다음과 같습니다:

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

요약하면:

  • pyproject.toml은 생성 중인 Python 패키지에 대한 표준 정적 메타데이터를 정의하며, 이름, 저자, 빌드 및 런타임 종속성을 포함합니다.

  • setup.py는 확장 모듈을 빌드하는 데 필요한 최소한의 동적 구성을 포함합니다.

  • src/spectator_measures/__init__.py는 사용자 대면 인터페이스를 정의하고 Qiskit의 Python 공간 구성 요소와 인터페이스하는 코드를 제공합니다.

  • src/spectator_measures/_coremodule.c는 C 확장 모듈을 정의하며, 패키지의 모든 성능이 중요한 코드를 포함합니다.

각 파일을 자세히 살펴보고 확장 모듈과 함께 패키지를 구축해 나갑니다.

패키지 메타데이터 정의하기​

pyproject.toml 파일을 정의하는 것으로 시작합니다. 이는 setuptools 기반 프로젝트의 표준 방식이지만, setuptools 외에 build-system.requires 배열에 qiskit이 추가 요구 사항으로 포함됩니다.

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

project.dependencies에서 Qiskit의 런타임 버전을 빌드 시 사용된 마이너 버전과 일치하도록 설정하세요.

많은 순수 Python setuptools 기반 프로젝트에서는 pyproject.toml 파일만으로 충분합니다. 그러나 우리 모듈은 빌드 프로세스 중에 Qiskit C API 헤더 파일에 접근해야 합니다. v2.4부터 이 파일들은 Qiskit SDK Python 배포판에 포함됩니다. 이들이 있는 디렉토리를 찾으려면 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 기반 프로젝트 구성에 대한 자세한 내용은 setuptools 사용자 가이드를 참조하세요.

Python 공간 래퍼 작성하기​

C에서 Python 확장의 모든 것을 정의하는 것이 기술적으로 가능합니다. 실제로는 Python 자체에서 다른 Python 공간 코드와 상호작용하는 것이 더 쉽습니다.

이 패키지는 Python 공간의 qiskit.transpiler.TransformationPass 클래스에서 파생된 커스텀 트랜스파일러 패스를 정의하지만, 모든 비즈니스 로직에는 C 확장 모듈의 함수를 사용합니다. 다음과 같습니다:

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

이 패스의 정확한 세부 사항은 이 가이드에서 중요하지 않습니다. 관심이 있다면 qiskit-addon-utils의 AddSpectatorMeasures API 문서를 참조하세요. 이 가이드는 제어 흐름 작업에 대한 지원 없이 해당 패스의 간단한 포팅을 제작합니다.

C 확장 모듈 작성하기​

이 섹션은 실제 C 확장 모듈에 관한 것입니다. 이것은 프로젝트에서 가장 복잡한 파일이므로 여러 단계로 나누어 설명합니다.

헤더 파일 구성하기​

Python 확장 모듈을 빌드할 때, 다른 파일보다 먼저 Python.h를 포함해야 합니다. 확장 모듈에서 Qiskit C API를 사용하려면, qiskit.h를 포함하기 전에 QISKIT_PYTHON_EXTENSION 매크로를 정의해야 합니다.

그러면 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>

순수 C API 코드 작성하기​

다음으로, 모든 비즈니스 로직을 순수 Qiskit C API 코드로 작성합니다. 이 로직은 다음 섹션에서 Python 공간에 노출합니다.

이 섹션은 순수 Qiskit C API 코드만 포함합니다. 다음 C API 타입을 사용합니다:

  • QkDag * — Python 공간의 DAGCircuit에 해당합니다.

  • QkTarget * — Python 공간의 Target에 해당합니다.

  • QkNeighbors — 2-큐비트 결합 제약 조건을 나타내는 네이티브 C API 타입입니다.

  • QkCircuitInstruction — 개별 명령어를 쿼리하기 위한 네이티브 C API 타입입니다.

처음 두 가지는 Python 공간과의 상호작용의 일부를 형성하지만, 이들을 사용할 때는 순수 C API만 고려하면 됩니다. 이 코드에는 Python 인터프리터와의 상호작용이 없습니다.

이 섹션에 정의된 모든 함수와 심볼은 static 링키지로 선언됩니다. 이는 Python 인터프리터가 이 확장 모듈에 링크하지 않기 때문입니다. 다음 섹션에서 인터프리터에 사용 가능한 함수의 세부 정보를 제공합니다.

이 코드의 알고리즘적 세부 사항에 대해서는 자세히 다루지 않습니다. 시연을 위해 의미 있는 트랜스파일러 패스를 사용하는 것이 유익하지만, 알고리즘의 정확한 구현은 이 가이드에서 중요하지 않습니다.

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 상호작용 코드 작성하기​

이제 모든 비즈니스 로직이 순수 C로 정의되었습니다. 다음으로, 이를 Python에 안전하게 노출해야 합니다.

먼저 Python에 노출될 유일한 함수를 정의합니다. 이 함수는 정의된 시그니처를 따라야 하며, fn(self, *args, **kwargs) 메서드처럼 보이는 순수 Python 타입으로 구성됩니다. PyObject *를 반환해야 하며, 이는 Python 객체의 일반적인 형태입니다.

완전한 함수는 다음과 같습니다:

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. 임의의 Python 인수를 수락하는 정의된 시그니처를 따릅니다.

  2. Python 인수에서 파싱할 C 네이티브 객체를 저장할 공간을 정의합니다.

  3. 예상 인수 목록, 키워드 인수, 변환에 사용할 함수로 구성된 파싱 함수를 호출하여 C 네이티브 객체를 추출합니다. 실패하면 함수가 오류를 전파합니다.

  4. 이전 섹션의 C 네이티브 비즈니스 로직에 위임하여 DAG를 제자리에서 변환합니다.

  5. Python 공간의 None 객체를 반환합니다.

가장 복잡한 로직은 모두 PyArg_ParseTupleAndKeywords 내부에 있습니다. 이는 인수 파싱에 관한 CPython 문서에 잘 설명되어 있으니 추가 정보를 위해 참조하세요.

Qiskit C API는 PyArg_Parse* 함수와 함께 사용하도록 설계된 "변환기" 함수로서 qk_*_convert_from_python이라는 이름의 여러 함수를 제공합니다. 이는 형식 문자열의 O& 키에 해당합니다. 여기서는 qk_dag_convert_from_python과 qk_target_convert_from_python을 사용했습니다. 이 함수들은 파생된 Python 인수에서 C 네이티브 객체를 빌린 것입니다. 이는 변경 사항이 Python 공간으로 전파된다는 것을 의미하지만, 결과를 사용하는 동안 이를 뒷받침하는 Python 객체에 대한 참조를 해제하지 않도록 주의해야 합니다. 이는 표준 Python C API 프로그래밍입니다.

다음으로, 이 모듈과 그것이 포함하는 함수에 대한 정보를 정의하여 Python 공간에 전달합니다:

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

이 메서드 테이블과 모듈 정의 구조체에 대한 자세한 내용은 모듈의 메서드 테이블 및 초기화 함수에 관한 CPython 문서를 참조하세요.

마지막으로, Python에 모듈 초기화 방법을 알려줍니다. 이것은 C 파일에서 내보내지는 유일한 함수입니다. 이름은 PyInit_<mod> 패턴과 정확히 일치해야 하며, 여기서 <mod>는 (비정규화된) 모듈 이름입니다. 이 경우, 완전히 정규화된 모듈 이름은 spectator_measures._core이고 비정규화된 이름은 _core이므로, 함수 이름은 이중 밑줄을 사용하여 PyInit__core여야 합니다.

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

PyMODINIT_FUNC와 PyModuleDef_Init 심볼은 모두 표준 Python C API 프로그래밍입니다. Qiskit 특유의 구성 요소는 qk_import()입니다. 모듈의 초기화 함수 중에 이 함수를 호출하는 것이 중요합니다. 이것이 성공적으로 실행되기 전까지는 Qiskit C API 함수를 호출할 수 없습니다.

Python에서 패키지 사용하기​

이것은 이제 C 확장 모듈을 포함하는 완전한 패키지입니다. 표준 도구만 사용되었고 빌드 시 비표준 시스템 라이브러리가 링크되지 않으므로 빌드 프로세스가 간단합니다.

PEP-517 호환 빌드 도구를 사용할 수 있습니다. 최소한의 예시로, 저장소 루트에서 다음 명령을 실행하여 패키지를 설치할 수 있습니다.

pip install .

이렇게 하면 C 확장 모듈이 컴파일되고 완전한 Python 패키지가 환경에 설치됩니다.

이 커스텀 트랜스파일러 패스의 사용 예는 다음과 같습니다:

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