メインコンテンツへスキップ

トランスパイラー設定の比較

使用時間の目安:Eagle r3プロセッサーで1分未満(注意:これはあくまで目安です。実際の実行時間は異なる場合があります。)

背景

より高速かつ効率的な結果を得るために、2024年3月1日以降、Qiskit Runtimeプリミティブに送信する前に、回路とオブザーバブルをQPU(量子処理ユニット)がサポートする命令のみを使用するよう変換する必要があります。これを命令セットアーキテクチャ(ISA)回路およびオブザーバブルと呼びます。この変換を行う一般的な方法のひとつは、トランスパイラーのgenerate_preset_pass_manager関数を使用することです。ただし、より手動のプロセスを選択することもできます。

たとえば、特定デバイス上の特定のQubitサブセットをターゲットにしたい場合があります。このウォークスルーでは、回路の作成・トランスパイル・送信の全プロセスを実行することで、異なるトランスパイラー設定のパフォーマンスをテストします。

要件

始める前に、以下がインストールされていることを確認してください:

  • 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:古典的な入力を量子問題にマッピングする

トランスパイラーが最適化を試みるための小さな回路を作成します。この例では、状態111をマークするオラクルを用いてGroverのアルゴリズムを実行する回路を作成します。次に、後で比較するために理想的な分布(完全な量子コンピューターで無限回実行した場合に期待される測定値)をシミュレーションします。

# 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")

Output of the previous code cell

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

plot_histogram(ideal_distribution)

Output of the previous code cell

ステップ2:量子ハードウェア実行のために問題を最適化する

次に、QPU向けに回路をトランスパイルします。optimization_level0(最低)に設定した場合と3(最高)に設定した場合のトランスパイラーのパフォーマンスを比較します。最低の最適化レベルは、デバイス上で回路を実行するために必要な最低限の処理を行います。具体的には、回路のQubitをデバイスのQubitにマッピングし、すべての2Qubit演算を可能にするためにSWAPゲートを追加します。最高の最適化レベルはより高度で、全体的なゲート数を削減するためにさまざまな工夫を活用します。マルチQubitゲートはエラーレートが高く、Qubitは時間とともにデコヒーレンスするため、短い回路の方がより良い結果をもたらすはずです。

次のセルでは、optimization_levelの両方の値についてqcをトランスパイルし、2Qubitゲートの数を出力し、トランスパイルされた回路をリストに追加します。トランスパイラーのアルゴリズムの一部はランダム化されているため、再現性のためにシードを設定します。

# 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でトランスパイルされた回路の方がはるかに優れたパフォーマンスを発揮するはずです。

パフォーマンスをさらに向上させる別の方法として、ダイナミックデカップリングがあります。これは、アイドル状態のQubitにゲートのシーケンスを適用することで、環境との望ましくない相互作用の一部を打ち消します。次のセルでは、optimization_level=3でトランスパイルされた回路にダイナミックデカップリングを追加し、リストに追加します。

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

Output of the previous code cell

ステップ3:Qiskitプリミティブを使用して実行する

この時点で、指定されたQPU向けにトランスパイルされた回路のリストが揃っています。次に、サンプラープリミティブのインスタンスを作成し、コンテキストマネージャー(with ...:)を使用してバッチジョブを開始します。コンテキストマネージャーはバッチを自動的に開いて閉じます。

コンテキストマネージャー内で、回路をサンプリングし、結果を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:後処理を行い、希望する古典的フォーマットで結果を返す

最後に、デバイス実行の結果を理想的な分布と比較してプロットします。ゲート数が少ないため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",
],
)

Output of the previous code cell

各結果セットと理想的な分布の間のHellinger忠実度を計算することで、これを確認できます(値が高いほど良く、1は完全な忠実度を表します)。

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