주 콘텐츠로 건너뛰기

유틸리티 규모 실험 I

참고

Tamiya Onodera (2024년 7월 5일)

원본 강의의 PDF를 다운로드하세요. 일부 코드 스니펫은 정적 이미지이므로 더 이상 사용되지 않을 수 있습니다.

이 실험을 실행하는 데 필요한 QPU 시간은 약 45초입니다.

1. Utility paper 소개

이 레슨에서는 Nature Vol 618, 2023년 6월 15일에 게재된 비공식적으로 "utility paper"라고 부르는 논문에 등장하는 유틸리티 규모의 Circuit을 실행합니다. 이 논문은 2D 횡방향 자기장 이징 모델의 시간 진화를 다룹니다. 구체적으로는 다음 해밀토니안의 시간 동역학을 고려합니다.

H=HZZ+HX=J(i,j)ZiZj+hiXiH = H_{ZZ} + H_X = - J \sum_{(i,j)} Z_i Z_j + h \sum_{i} X_i

여기서 J>0J > 0i<ji < j인 최근접 스핀 간의 결합 상수이고, hh는 전체 횡방향 자기장입니다. 논문에서는 1차 Trotter 분해를 사용하여 초기 상태에서의 스핀 동역학을 시뮬레이션합니다.

exp(iHZZδt)=(i,j)exp(iJδtZiZj)=(i,j)RZiZj(2Jδt)exp(iHXδt)=iexp(ihδtXi)=iRXi(2hδt)\begin{aligned} \exp(-i H_{ZZ} \delta t) &= \prod_{(i,j)} \exp (i J \delta t Z_i Z_j) = \prod_{(i,j)} \mathrm{R}_{Z_i Z_j} ( - 2 J \delta t) \\ \exp(-i H_X \delta t) &= \prod_{i} \exp (-i h \delta t X_i ) = \prod_{i} \mathrm{R}_{X_i} ( 2 h \delta t) \end{aligned}

여기서 진화 시간 TTT/δtT / \delta t개의 Trotter 단계로 이산화되며, RZiZj(θJ)\mathrm{R}_{Z_i Z_j}(\theta_J)RXi(θh)\mathrm{R}_{X_i}(\theta_h)는 각각 ZZZZXX 회전 Gate입니다.

실험은 IBM Quantum® Eagle 프로세서(heavy-hex 연결성을 가진 127-Qubit 장치)에서 수행되었으며, 모든 Qubit에 XX 상호작용을 적용하고 커플링 맵의 모든 에지에 ZZZZ 상호작용을 적용합니다. 단, "데이터 의존성"으로 인해 모든 ZZZZ 상호작용을 동시에 적용할 수는 없습니다. 따라서 커플링 맵에 색을 칠하여(color the coupling map) 레이어별로 그룹화합니다. 같은 레이어에 속한 상호작용은 동일한 색이 지정되며, 병렬로 적용할 수 있습니다.

또한 실험의 단순화를 위해 θJ=π/2\theta_J=-\pi /2인 경우에 집중합니다.

이 논문의 주요 기여는 상태벡터 시뮬레이션의 한계를 넘어서는 규모의 양자 Circuit을 구성하고, 잡음이 있는 양자 컴퓨터에서 실행하여 신뢰할 수 있는 결과를 성공적으로 추출했다는 점입니다. 즉, 잡음이 있는 양자 컴퓨터의 유용성(utility)을 입증했습니다. 이를 위해 확률적 오류 증폭(PEA)을 사용한 제로 잡음 외삽법(ZNE)을 적용하여 잡음 장치의 오류를 완화했습니다.

이후로 우리는 이러한 실험과 Circuit을 "유틸리티 규모(utility-scale)"라고 부릅니다.

1.1 목표

이 레슨의 목표는 유틸리티 규모의 Circuit을 구성하고 Eagle 프로세서에서 실행하는 것입니다. 현재 PEA는 Qiskit의 실험적 기능이며, ZNE와 PEA를 함께 적용하는 데 상당한 시간이 소요되기 때문에, 이 노트북에서 신뢰할 수 있는 결과를 추출하는 것은 범위를 벗어납니다.

구체적으로는 논문의 Figure 4b에 해당하는 Circuit을 구성하고 실행한 후, 자신의 "미완화(unmitigated)" 데이터 포인트를 직접 그려봅니다. 이 Circuit은 Z62\langle Z_{62} \rangle을 관측값으로 하는 127-Qubit ×\times 60-레이어(20 Trotter 단계) Circuit입니다. image.png 어렵게 느껴지시나요?   걱정하지 마세요. 이 코스의 마지막 세 레슨이 디딤돌 역할을 합니다. 먼저 소규모 실험부터 시작합니다. 가짜 장치에서 Z13\langle Z_{13} \rangle을 관측값으로 하는 27-Qubit ×\times 6-레이어(2 Trotter 단계) Circuit을 구성하고 실행하겠습니다.

소개는 이것으로 충분합니다. 유틸리티 규모의 모험을 시작해봅시다!

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime rustworkx
import qiskit

qiskit.__version__
'2.0.2'
#!pip install qiskit_ibm_runtime
#!pip install qiskit_aer
import matplotlib.pyplot as plt
import numpy as np
import rustworkx as rx

from qiskit import QuantumCircuit, transpile
from qiskit.circuit import Parameter
from qiskit.circuit.library import YGate
from qiskit.quantum_info import SparsePauliOp
from qiskit_ibm_runtime import (
QiskitRuntimeService,
fake_provider,
EstimatorV2 as Estimator,
)
from qiskit_aer import AerSimulator
service = QiskitRuntimeService()

2. 준비

2.1 RZZ(-π\pi / 2) 구성

먼저, 일반적인 경우에 RZZ Gate는 두 개의 CXCX Gate가 필요하다는 점을 확인합니다.

from qiskit.circuit.library import RZZGate

θ_h = Parameter("$\\theta_h$")
qc1 = QuantumCircuit(2)
qc1.append(RZZGate(θ_h), [0, 1])
qc1.decompose(reps=1).draw("mpl")

Output of the previous code cell

앞서 언급했듯이, 이 실험에서는 특정 각도인 -π\pi / 2로 고정된 RZZ Gate에 집중합니다. 논문에서 보여주듯이, 이 경우 단 하나의 CXCX Gate만으로 구현할 수 있습니다.

qc2 = QuantumCircuit(2)

qc2.sdg([0, 1])
qc2.append(YGate().power(1 / 2), [1])
qc2.cx(0, 1)
qc2.append(YGate().power(1 / 2).adjoint(), [1])

qc2.draw("mpl")

Output of the previous code cell

나중에 참조하기 위해 이 Circuit으로 Gate를 정의합니다.

rzz = qc2.to_gate(label="RZZ")

새로 정의한 rzz를 간단히 사용해봅시다.

qc3 = QuantumCircuit(3)
qc3.append(rzz, [0, 1])
qc3.append(rzz, [0, 2])
display(qc3.draw("mpl"))
# display(qc.decompose(reps=1).draw("mpl"))

Output of the previous code cell

더 활용하기 전에, -pi/2에 대한 qc1(RZZ Gate)과 새로 정의한 rzz 또는 qc2 Gate의 논리적 동등성을 검증해봅시다:

from qiskit.quantum_info import Operator

op1 = Operator(qc1.assign_parameters([-np.pi / 2]))
op2 = Operator(qc2)

op1.equiv(op2)
True

2.2 커플링 맵에 색 칠하기

Backend의 커플링 맵에 색을 칠하는 방법을 살펴봅시다. 이 작업은 ZZZZ 상호작용을 레이어별로 그룹화하는 데 필요합니다.

먼저 Backend의 커플링 맵을 시각화해봅시다. 현재 모든 IBM Quantum 장치의 커플링 맵은 heavy-hexagonal 구조임을 참고하세요.

backend = service.least_busy(operational=True, simulator=False)

backend.coupling_map.draw()

Output of the previous code cell

커플링 맵에 색을 칠하기 위해 그래프 및 복잡한 네트워크를 처리하는 Python 패키지인 rustworkx를 사용합니다. rustworkx는 여러 가지 색칠 알고리즘을 제공하는데, 모두 휴리스틱 방식이므로 최소 색칠을 보장하지는 않습니다.

다만 heavy-hex 그래프는 이분 그래프이므로, 이러한 그래프에서 최소 색칠을 찾을 수 있는 graph_bipartite_edge_color를 사용합니다.

def color_coupling_map(backend):
graph = backend.coupling_map.graph
undirected_graph = graph.to_undirected(multigraph=False)
edge_color_map = rx.graph_bipartite_edge_color(undirected_graph)
if edge_color_map is None:
edge_color_map = rx.graph_greedy_edge_color(undirected_graph)
# build a map from color to a list of edges
edge_index_map = undirected_graph.edge_index_map()
color_edges_map = {color: [] for color in edge_color_map.values()}
for edge_index, color in edge_color_map.items():
color_edges_map[color].append(
(edge_index_map[edge_index][0], edge_index_map[edge_index][1])
)
return edge_color_map, color_edges_map

Heavy-hexagonal 그래프는 세 가지 색으로 칠해집니다. 위의 커플링 맵에 대해 이를 확인해봅시다.

edge_color_map, color_edges_map = color_coupling_map(backend)
print(
f"{backend.name}, {backend.num_qubits}-qubit device, {len(color_edges_map.keys())} colors assigned."
)
ibm_strasbourg, 127-qubit device, 3 colors assigned.

맞습니다!

재미 삼아, rustworkx 시각화 기능을 사용하여 커플링 맵에 얻은 색을 칠해봅시다.

color_str_map = {0: "green", 1: "red", 2: "blue"}

undirected_graph = backend.coupling_map.graph.to_undirected(multigraph=False)
for i in undirected_graph.edge_indices():
undirected_graph.get_edge_data_by_index(i)["color"] = color_str_map[
edge_color_map[i]
]

rx.visualization.graphviz_draw(
undirected_graph, method="neato", edge_attr_fn=lambda edge: {"color": edge["color"]}
)

Output of the previous code cell

3. 2D 이징 모델의 Trotterized 시간 진화 풀기

2D 이징 모델의 시간 진화에 대한 utility paper의 Circuit을 구성하는 루틴을 정의해봅시다. 이 루틴은 Backend, Trotter 단계 수를 나타내는 정수, 그리고 배리어 삽입을 제어하는 불리언의 세 가지 매개변수를 받습니다.

def get_utility_circuit(backend, num_steps: int, barrier: bool = False):
num_qubits = backend.num_qubits
_, color_edges_map = color_coupling_map(backend)
θ_h = Parameter("$\\theta_h$")
qc = QuantumCircuit(num_qubits)

for i in range(num_steps):
qc.rx(θ_h, range(num_qubits))

for _, edge_list in color_edges_map.items():
for edge in edge_list:
qc.append(rzz, edge)

if barrier:
qc.barrier()
return qc

구성된 Circuit에 대해 Qubit 매핑과 라우팅이 이미 수동으로 수행되었음을 참고하세요. 따라서 나중에 Circuit을 Transpile할 때, Transpiler에게 Qubit 매핑과 라우팅을 맡기면 안 됩니다(맡겨서는 안 됩니다). 곧 확인하겠지만, 최적화 레벨 1과 레이아웃 방법 "trivial"을 사용하여 호출합니다.

다음으로 빠른 확인을 위해 구성된 Circuit의 정보를 얻는 간단한 루틴을 정의합니다.

def get_circuit_info(qc: QuantumCircuit, reps: int = 0):
qc0 = qc.decompose(reps=reps)
return (
f"{qc0.num_qubits} qubits × {qc0.depth(lambda x: x.operation.num_qubits == 2)} layers ({qc0.depth()}-depth)"
+ ", "
+ f"""Gate breakdown: {", ".join([f"{k.upper()} {v}" for k, v in qc0.count_ops().items()])}"""
)

이 루틴들을 실제로 사용해봅시다. 27 Qubit ×\times 15 레이어(5 Trotter 단계)의 Circuit이 표시되어야 합니다. 가짜 장치에는 28개의 에지가 있으므로, 28*5개의 얽힘 Gate가 있어야 합니다.

backend = fake_provider.FakeTorontoV2()
num_steps = 5
qc = get_utility_circuit(backend, num_steps, True)

display(qc.draw(output="mpl", fold=-1))
print(get_circuit_info(qc, reps=0))
print(get_circuit_info(qc, reps=1))

Output of the previous code cell

27 qubits × 15 layers (20-depth),  Gate breakdown: CIRCUIT-165 140, RX 135, BARRIER 5
27 qubits × 15 layers (60-depth), Gate breakdown: SDG 280, UNITARY 280, CX 140, R 135, BARRIER 5

4. 27-Qubit 버전의 문제를 풀어봅니다.

이제 유틸리티 실험의 소규모 버전을 시연해 보겠습니다. Z13\langle Z_{13} \rangle을 관측값으로 하는 27-Qubit ×\times 6-레이어(2 Trotter 단계) Circuit을 만들고, AerSimulator와 가상 디바이스 양쪽에서 실행해 봅니다.

물론, 우리는 네 단계로 구성된 워크플로우인 "Qiskit 패턴"을 따릅니다. 이 패턴은 매핑(Map), 최적화(Optimize), 실행(Execute), 후처리(Post-Process)로 이루어져 있습니다. 구체적으로는 다음과 같습니다.

  • 고전적 입력값을 양자 계산으로 매핑합니다.
  • 양자 계산을 위해 Circuit을 최적화합니다.
  • Primitive를 사용하여 Circuit을 실행합니다.
  • 결과를 고전적 형식으로 후처리하여 반환합니다.

아래에서는 소규모 실험용 Circuit을 생성하는 매핑 단계를 진행합니다. 그런 다음 AerSimulator를 위한 최적화 및 실행 단계와, 가상 디바이스를 위한 최적화 및 실행 단계를 각각 수행합니다. 마지막으로 결과를 시각화하는 후처리 단계를 진행합니다.

4.1 1단계: 매핑(Map)

backend = fake_provider.FakeTorontoV2()  # a 27 qubit fake device.
num_steps = 2
qc = get_utility_circuit(backend, num_steps)
obs = SparsePauliOp.from_sparse_list(
[("Z", [13], 1)], num_qubits=backend.num_qubits
) # Falcon
angles = [
0,
0.1,
0.2,
0.3,
0.4,
0.5,
0.6,
0.7,
0.8,
1.0,
np.pi / 2,
] # We try 11 angles for theta_h.

4.2 2단계 및 3단계: 최적화 및 실행 (시뮬레이터)

backend_sim = AerSimulator()
transpiled_qc_sim = transpile(
qc, backend_sim, optimization_level=1, layout_method="trivial"
)
transpiled_obs_sim = obs.apply_layout(layout=transpiled_qc_sim.layout)

print(get_circuit_info(qc, reps=1))
print(get_circuit_info(transpiled_qc_sim, reps=1))
27 qubits × 6 layers (23-depth),  Gate breakdown: SDG 112, UNITARY 112, CX 56, R 54
27 qubits × 6 layers (16-depth), Gate breakdown: U3 80, CX 56, R 54, U1 32, U 28

어느 사용자가 2.3 GHz 쿼드코어 Intel Core i7 프로세서와 32GB 3LPDDR4X RAM이 장착된 MacBook Pro(macOS 14.5 구동)에서 다음 셀을 실행한 결과, 실제 실행 시간(wall time)은 161ms였습니다. 각 노트북마다 결과는 조금씩 다를 수 있습니다.

%%time
params = [[p] for p in angles]
estimator = Estimator(mode=backend_sim)
pub = (transpiled_qc_sim, transpiled_obs_sim, params)
result_sim = estimator.run([pub]).result()
CPU times: user 231 ms, sys: 186 ms, total: 417 ms
Wall time: 111 ms

4.3 2단계 및 3단계: 최적화 및 실행 (가상 디바이스)

backend_fake = fake_provider.FakeTorontoV2()
transpiled_qc_fake = transpile(
qc, backend_fake, optimization_level=1, layout_method="trivial"
)
transpiled_obs_fake = obs.apply_layout(layout=transpiled_qc_fake.layout)

print(get_circuit_info(qc, reps=1))
print(get_circuit_info(transpiled_qc_fake, reps=1))
27 qubits × 6 layers (23-depth),  Gate breakdown: SDG 112, UNITARY 112, CX 56, R 54
27 qubits × 6 layers (49-depth), Gate breakdown: SDG 324, U1 274, H 162, CX 56, U3 14

동일한 환경에서 같은 사용자가 다음 셀을 실행했을 때, 실제 실행 시간(Wall Time)은 2분 19초였습니다. 가상 디바이스에서 Circuit을 실행하면 잡음 시뮬레이션이 수행되므로, 정확한 시뮬레이션보다 훨씬 더 많은 시간이 걸립니다. 가상 디바이스에서는 27-Qubit ×\times 9-레이어(3 Trotter 단계)와 같이 더 큰 Circuit을 실행하지 않는 것을 권장합니다.

%%time
params = [[p] for p in angles]
estimator = Estimator(mode=backend_fake)
pub = (transpiled_qc_fake, transpiled_obs_fake, params)
result_fake = estimator.run([pub]).result()
CPU times: user 4min 42s, sys: 9.35 s, total: 4min 51s
Wall time: 38.3 s

4.4 4단계: 후처리(Post-process)

정확한 시뮬레이션과 잡음 시뮬레이션의 결과를 시각화합니다. FakeToronto에서 잡음이 얼마나 심각한 영향을 미치는지 확인할 수 있습니다.

plt.plot(angles, result_fake[0].data.evs, "o", label="Fake Device")
plt.plot(angles, result_sim[0].data.evs, "o", label="AerSimulator")
plt.xlabel("$\\mathrm{R_x}$ angle $\\theta_h$")
plt.title("$\\langle Z_{13} \\rangle$")
plt.legend()
plt.show()

Output of the previous code cell

5. 127-Qubit 버전의 문제를 풀어봅니다.

처음에 언급했던 유틸리티 규모의 실험을 실행하는 것이 여러분의 목표입니다. Z62\langle Z_{62} \rangle을 관측값으로 하는 127-Qubit, 60-레이어(20 Trotter 단계) Circuit을 만들고 실행해 보세요. 27-Qubit 버전의 코드를 참고하여 직접 도전해 보시길 권장합니다. 하지만 풀이는 여기에 제공되어 있습니다.

풀이:

5.1 1단계: 매핑(Map)

# backend_map = service.backend("ibm_brisbane")
backend_map = service.least_busy(operational=True, simulator=False)

num_steps = 20
qc = get_utility_circuit(backend_map, num_steps)
obs = SparsePauliOp.from_sparse_list(
[("Z", [62], 1)], num_qubits=backend_map.num_qubits
) # Eagle
angles = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 1.0, np.pi / 2]

5.2 2단계 및 3단계: 최적화 및 실행

Eagle 프로세서의 커플링 맵에는 144개의 엣지가 있다는 점에 유의하세요.

# backend = service.backend("ibm_brisbane")
backend = backend_map

transpiled_qc = transpile(qc, backend, optimization_level=1, layout_method="trivial")
transpiled_obs = obs.apply_layout(layout=transpiled_qc.layout)

print(get_circuit_info(qc, reps=1))
print(get_circuit_info(transpiled_qc))
156 qubits × 60 layers (221-depth),  Gate breakdown: SDG 7040, UNITARY 7040, CX 3520, R 3120
156 qubits × 60 layers (201-depth), Gate breakdown: RZ 11933, SX 6240, CZ 3520
params = [[p] for p in angles]
estimator = Estimator(mode=backend)
pub = (transpiled_qc, transpiled_obs, params)
job = estimator.run([pub])

job_id = job.job_id()
print(f"job id={job_id}")
job id=d1479n6qf56g0081sxa0

5.3 후처리(Post-process)

유틸리티 논문의 Figure 4b에 있는 "보정된(mitigated)" 데이터 포인트 값을 제공합니다. 여러분의 결과와 함께 이 값들을 시각화해 보세요.

result_paper = [
1.0171,
1.0044,
0.9563,
0.9602,
0.8394,
0.8120,
0.5466,
0.4556,
0.1953,
0.0141,
0.0117,
]

# REPLACE WITH YOUR OWN JOB ID
job = service.job(job_id)

plt.plot(angles, job.result()[0].data.evs, "o", label=f"{job.backend().name}")
plt.plot(angles, result_paper, "o", label="Utility Paper")
plt.xlabel("$\\mathrm{R_x}$ angle $\\theta_h$")
plt.title("$\\langle Z_{62} \\rangle$")
plt.legend()
plt.show()

Output of the previous code cell

여러분의 결과가 Figure 4b의 "보정되지 않은(unmitigated)" 데이터 포인트와 비슷한가요? 실험 당시의 디바이스 종류와 상태에 따라 결과는 크게 다를 수 있습니다. 결과 자체에 대해서는 걱정하지 않아도 됩니다. 중요한 것은 코딩을 올바르게 수행했는지 여부입니다. 그렇게 했다면 축하합니다. 여러분은 유틸리티 시대의 출발선에 선 것입니다.

유틸리티 논문에서처럼, 전 세계의 과학자들은 잡음이 있는 환경에서도 의미 있는 결과를 추출하기 위해 엄청난 창의성을 발휘해 왔습니다. 이 집단적 노력의 최종 목표는 양자 우위(quantum advantage)입니다. 즉, 양자 컴퓨터가 산업에서 유용한 일부 문제를 고전 컴퓨터보다 더 빠르게, 더 높은 정확도로, 또는 더 저렴하게 해결할 수 있는 상태입니다. 이것은 단 하나의 사건으로 이루어지는 것이 아니라, 양자 결과를 고전적으로 재현하는 데 점점 더 많은 시간이 걸리다가, 어느 순간 그 격차가 결정적으로 중요해지는 시대로 진입하는 것입니다. 양자 우위에 이르는 길에서 한 가지 분명한 것은, 유틸리티 규모의 실험을 통해서만 그곳에 도달할 수 있다는 것입니다. 이 코스가 도전과 즐거움으로 가득한 이 탐구에 여러분이 합류하는 계기가 된다면, 더할 나위 없이 기쁠 것입니다.

참고 문헌