주 콘텐츠로 건너뛰기

Transpiler 설정 비교

예상 사용 시간: Eagle r3 프로세서 기준 1분 이내 (참고: 이는 예상치이며, 실제 실행 시간은 다를 수 있습니다.)

배경

더 빠르고 효율적인 결과를 보장하기 위해, 2024년 3월 1일부터 Circuit과 옵저버블은 Qiskit Runtime 프리미티브에 제출하기 전에 QPU(양자 처리 장치)가 지원하는 명령어만 사용하도록 변환되어야 합니다. 이를 명령어 집합 아키텍처(ISA) Circuit 및 옵저버블이라고 합니다. 이를 수행하는 일반적인 방법 중 하나는 Transpiler의 generate_preset_pass_manager 함수를 사용하는 것입니다. 그러나 더 수동적인 방법을 따를 수도 있습니다.

예를 들어, 특정 장치의 특정 Qubit 서브셋을 대상으로 지정하고 싶을 수 있습니다. 이 연습에서는 Circuit을 생성하고, transpile하고, 제출하는 전체 과정을 완료하여 다양한 Transpiler 설정의 성능을 테스트합니다.

요구사항

시작하기 전에 다음이 설치되어 있는지 확인하세요:

  • Qiskit SDK v1.2 이상, 시각화 지원 포함
  • Qiskit Runtime v0.28 이상 (pip install qiskit-ibm-runtime)

설정

# Added by doQumentation — required packages for this notebook
!pip install -q qiskit qiskit-ibm-runtime
# Create circuit to test transpiler on
from qiskit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.circuit.library import GroverOperator, Diagonal

# Use Statevector object to calculate the ideal output
from qiskit.quantum_info import Statevector
from qiskit.visualization import plot_histogram
from qiskit.transpiler import PassManager

from qiskit.circuit.library import XGate
from qiskit.quantum_info import hellinger_fidelity

# Qiskit Runtime
from qiskit_ibm_runtime import (
QiskitRuntimeService,
Batch,
SamplerV2 as Sampler,
)
from qiskit_ibm_runtime.transpiler.passes.scheduling import (
ASAPScheduleAnalysis,
PadDynamicalDecoupling,
)

1단계: 고전적 입력을 양자 문제로 매핑

Transpiler가 최적화를 시도할 작은 Circuit을 생성합니다. 이 예제에서는 상태 111을 표시하는 오라클을 사용하여 Grover 알고리즘을 수행하는 Circuit을 생성합니다. 다음으로, 나중에 비교하기 위해 이상적인 분포(완벽한 양자 컴퓨터에서 무한히 실행했을 때 측정할 것으로 예상되는 값)를 시뮬레이션합니다.

# To run on hardware, select the backend with the fewest number of jobs in the queue
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
backend.name
'ibm_brisbanse'
oracle = Diagonal([1] * 7 + [-1])
qc = QuantumCircuit(3)
qc.h([0, 1, 2])
qc = qc.compose(GroverOperator(oracle))

qc.draw(output="mpl", style="iqp")

이전 코드 셀의 출력

ideal_distribution = Statevector.from_instruction(qc).probabilities_dict()

plot_histogram(ideal_distribution)

이전 코드 셀의 출력

2단계: 양자 하드웨어 실행을 위한 문제 최적화

다음으로, QPU를 위해 Circuit을 transpile합니다. optimization_level0(최저)으로 설정한 경우와 3(최고)으로 설정한 경우의 Transpiler 성능을 비교합니다. 최저 최적화 수준은 Circuit을 장치에서 실행하는 데 필요한 최소한의 작업만 수행합니다. 즉, Circuit Qubit을 장치 Qubit에 매핑하고 모든 2-Qubit 연산을 허용하도록 swap Gate를 추가합니다. 최고 최적화 수준은 훨씬 더 스마트하며 전체 Gate 수를 줄이기 위해 다양한 기법을 사용합니다. 다중 Qubit Gate는 높은 오류율을 가지고 Qubit은 시간이 지남에 따라 디코히어하기 때문에, 더 짧은 Circuit이 더 나은 결과를 제공합니다.

다음 셀은 두 optimization_level 값에 대해 qc를 transpile하고, 2-Qubit Gate 수를 출력하며, transpile된 Circuit을 목록에 추가합니다. Transpiler의 일부 알고리즘은 무작위성을 사용하므로, 재현성을 위해 시드를 설정합니다.

# Need to add measurements to the circuit
qc.measure_all()

# Find the correct two-qubit gate
twoQ_gates = set(["ecr", "cz", "cx"])
for gate in backend.basis_gates:
if gate in twoQ_gates:
twoQ_gate = gate

circuits = []
for optimization_level in [0, 3]:
pm = generate_preset_pass_manager(
optimization_level, backend=backend, seed_transpiler=0
)
t_qc = pm.run(qc)
print(
f"Two-qubit gates (optimization_level={optimization_level}): ",
t_qc.count_ops()[twoQ_gate],
)
circuits.append(t_qc)
Two-qubit gates (optimization_level=0):  21
Two-qubit gates (optimization_level=3): 14

CNOT은 일반적으로 높은 오류율을 가지므로, optimization_level=3으로 transpile된 Circuit이 훨씬 더 나은 성능을 보일 것입니다.

성능을 향상시키는 또 다른 방법은 유휴 Qubit에 Gate 시퀀스를 적용하는 동적 디커플링을 통한 것입니다. 이는 환경과의 원치 않는 상호작용 일부를 상쇄합니다. 다음 셀은 optimization_level=3으로 transpile된 Circuit에 동적 디커플링을 추가하고 목록에 추가합니다.

# Get gate durations so the transpiler knows how long each operation takes
durations = backend.target.durations()

# This is the sequence we'll apply to idling qubits
dd_sequence = [XGate(), XGate()]

# Run scheduling and dynamic decoupling passes on circuit
pm = PassManager(
[
ASAPScheduleAnalysis(durations),
PadDynamicalDecoupling(durations, dd_sequence),
]
)
circ_dd = pm.run(circuits[1])

# Add this new circuit to our list
circuits.append(circ_dd)
circ_dd.draw(output="mpl", style="iqp", idle_wires=False)

이전 코드 셀의 출력

3단계: Qiskit 프리미티브를 사용한 실행

이 시점에서 지정된 QPU를 위해 transpile된 Circuit 목록이 있습니다. 다음으로, Sampler 프리미티브 인스턴스를 생성하고 배치 작업을 시작합니다. 컨텍스트 매니저(with ...:)를 사용하면 배치가 자동으로 열리고 닫힙니다.

컨텍스트 매니저 내에서 Circuit을 샘플링하고 결과를 result에 저장합니다.

with Batch(backend=backend):
sampler = Sampler()
job = sampler.run(
[(circuit) for circuit in circuits], # sample all three circuits
shots=8000,
)
result = job.result()

4단계: 후처리 및 원하는 고전적 형식으로 결과 반환

마지막으로, 장치 실행 결과를 이상적인 분포와 비교하여 플롯합니다. Gate 수가 적기 때문에 optimization_level=3의 결과가 이상적인 분포에 더 가깝고, 동적 디커플링으로 인해 optimization_level=3 + dd는 더욱 가까운 것을 확인할 수 있습니다.

binary_prob = [
{
k: v / res.data.meas.num_shots
for k, v in res.data.meas.get_counts().items()
}
for res in result
]
plot_histogram(
binary_prob + [ideal_distribution],
bar_labels=False,
legend=[
"optimization_level=0",
"optimization_level=3",
"optimization_level=3 + dd",
"ideal distribution",
],
)

이전 코드 셀의 출력

각 결과 세트와 이상적인 분포 사이의 Hellinger 충실도를 계산하여 이를 확인할 수 있습니다(높을수록 좋으며, 1이 완벽한 충실도입니다).

for prob in binary_prob:
print(f"{hellinger_fidelity(prob, ideal_distribution):.3f}")
0.848
0.945
0.990