주 콘텐츠로 건너뛰기

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

Qiskit v2.4 기준으로 C API는 마이너 버전 외에서는 아직 안정적이지 않습니다(예를 들어, v2.4.0의 C API는 v2.4.1과 호환되지만 v2.5.0과는 호환되지 않습니다). 향후 이 안정성을 메이저 버전으로 확장할 계획입니다. 지금은 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-utilsAddSpectatorMeasures 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_pythonqk_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_FUNCPyModuleDef_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