SABRE を用いたトランスパイレーションの最適化
推定実行時間:Heron r2 プロセッサで 1 分(注:これはあくまで推定値です。 (実行時間は異なる場合があります。)
学習成果
このチュートリアルを学習し終える頃には、以下の点を理解できるようになるはずです:
- トランスパイラリングの品質を向上させるためのSABREパラメータ(
layout_trials,swap_trials,max_iterations)の設定方法 - トランスパイレーションの実行時間と回路の品質(深さおよびゲート数)とのトレードオフ
- SABREルーティングヒューリスティック(
basic,decay,lookahead)をカスタマイズする方法と、ハードウェア上での性能比較
前提条件
このチュートリアルを進める前に、以下のトピックについて理解しておいていただくことをお勧めします:
- 回路のトランスパイル :Qiskitにおけるトランスパイルの概要
- トランスパイラの各段階 :レイアウト段階と配線段階
- プリセットのパスマネージャーの設定 :最適化レベルをカスタマイズする
背景
トランスパイレーションとは、量子回路を特定の量子ハードウェアと互換性のある形式に変換することです。 重要な段階は2つあり、 1 つは量子ビットのレイアウトの決定(論理量子ビットを物理量子ビットにマッピングすること)、 もう 1つはゲートの配線(マルチ量子ビットゲートがデバイスの接続性を考慮するようにSWAPゲートを挿入すること)である。
SABRE ( SWAPベースの双方向ヒューリスティック探索アルゴリズム )は、レイアウトと配線の両方を最適化します。 これは、 IBM® のHeronプロセッサのように、複雑な結合マップを持つデバイス上での大規模回路(100クビット以上)において特に効果的です。 SABREはSWAPゲートの数を最小限に抑え、回路の深さを低減することで、実行の忠実度を向上させます。 LightSABRE アルゴリズムにおける最近の改良により、実行時間とゲート数がさらに削減されました。
このチュートリアルでは、まずさまざまなパラメータを設定 SabreLayout して、小さなGHZ回路を最適化し、実行の忠実度に与える影響を確認します。 次に、実際のハードウェア上で、SABREのルーティングヒューリスティックを大規模に比較検証します。
要件
このチュートリアルを始める前に、以下のものがインストールされていることを確認してください:
- Qiskit SDK v2.0 またはそれ以降のバージョンで、 可視化機能をサポートしているもの
- Qiskit Runtime v0.22 またはそれ以降 (
pip install qiskit-ibm-runtime) - Qiskit Aer (
pip install qiskit-aer)
セットアップ
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import EstimatorOptions
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_aer.primitives import EstimatorV2 as AerEstimator
from qiskit.transpiler.passes import (
SabreLayout,
SabreSwap,
BarrierBeforeFinalMeasurements,
StarPreRouting,
)
from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.passmanager.flow_controllers import ConditionalController
import matplotlib.pyplot as plt
import numpy as np
import time
seed = 42
service = QiskitRuntimeService(
channel="ibm_cloud",
token="<YOUR_API_TOKEN>", # Replace with your actual API token
instance="<YOUR_INSTANCE_NAME>", # Replace with your instance name if needed
)
backend = service.least_busy(operational=True, simulator=False)
print(f"Using backend: {backend.name}")Output:
Using backend: ibm_kingston
小規模シミュレータの例
このセクションでは、実際のバックエンドのノイズモデルに基づいたノイズ付きシミュレータを用いて、さまざまな SabreLayout 設定がトランスパイルの品質と実行の忠実度にどのような影響を与えるかを示します。 実際のハードウェア校正データから導出されたノイズモデルを併用 qiskit_aer することで、ハードウェアクレジットを消費することなく、トランスパイレーションのテストを行うことができます。
ステップ1:古典的な入力を量子問題にマッピングする
15キュービットからなるスター型トポロジーのGHZ回路を構築する。 最初の量子ビットがハブとなり、CNOTゲートを介して他のすべての量子ビットと直接接続されています。 このトポロジーは、デバイスの結合マップに単純に写像されないため、レイアウト上の難しい問題を引き起こします。
また、量子ビットのペア間のエンタングルメント相関 を測定するための演算子も定義 ZZ する。
SABREは汎用アルゴリズムであり、回路構造についていかなる仮定も行いません。 このスタートポロジーのGHZ回路については、実際に最適な配線方法が知られている。すなわち、この StarPreRouting パスはスター型のサブ回路を検出し、それらを直線的なチェーンに書き換える。このチェーンは、十分に長い直線経路を持つ任意のバックエンドに直接マッピングされる。 このチュートリアルでは、任意の回路に対応していることからSABREに焦点を当てていますが、回路に明確な特殊な構造があることが分かっている場合は、配線前に前述のような StarPreRouting 特化された処理を適用することで、どのようなヒューリスティック探索よりも優れた性能を発揮できる場合があります。
num_qubits_sim = 15
# Create star-topology GHZ circuit
qc_sim = QuantumCircuit(num_qubits_sim)
qc_sim.h(0)
for i in range(1, num_qubits_sim):
qc_sim.cx(0, i)
qc_sim.measure_all()
# ZZ operators: Z on qubit 0 and qubit i, identity elsewhere
operator_strings_sim = [
"Z" + "I" * i + "Z" + "I" * (num_qubits_sim - 2 - i)
for i in range(num_qubits_sim - 1)
]
operators_sim = [SparsePauliOp(op) for op in operator_strings_sim]ステップ2:量子ハードウェア実行に向けた問題の最適化
SabreLayoutデフォルト optimization_level=3 のプリセット・パス・マネージャーはすでにこれを使用していますが、デフォルト設定は控えめになっています。 より強力な設定が及ぼす影響を調査するため、そのパスは、より積極的な検索が行われるようカスタマイズ SabreLayout された設定に置き換えられ、レイアウト段階のその他のパスはすべて変更されません。 別の比較対象として、4番目のパス・マネージャーはデフォルト SabreLayout の設定を維持しつつ、初期化段階に を追加 StarPreRouting します。 StarPreRouting これは、 構造を意識したパスであり、スター型サブ回路を検出し、配線を行う前にそれらを線形チェーンに書き換えるものである。
ワークフローは以下の通りです:
- デフォルトのパスマネージャーを調べて、がステージ内の
layoutどこSabreLayoutに位置しているかを確認してください。 pm.init += StarPreRouting()``PassManager.replace(index, passes=...)そのパスを、を使用してカスタムSabreLayoutインスタンスに置き換え、を使用してバリアントをpm_star構築します。- 4つのパスマネージャーをすべて実行し、メトリクスを比較してください。
4つの構成は以下の通りです:
構成 | 説明 |
|---|---|
pm_1 (デフォルト) | level-3 のデフォルトプリセット(SabreLayout , max_iterations=4 layout_trials=20, swap_trials=20) |
pm_2 | カスタム SabreLayout (max_iterations=4, layout_trials=200, swap_trials=200) |
pm_3 | カスタム SabreLayout (max_iterations=8, layout_trials=200, swap_trials=200) |
pm_star | 初期化段階に が StarPreRouting 追加されたデフォルトのプリセット |
SABREの主なパラメータ:
layout_trials/swap_trials: SABRE が検討するレイアウト候補およびルーティング案の数を制御します。 試行回数を増やすことで、SABREはより広範な探索空間を網羅し、より優れた解が見つかる可能性が高まります。max_iterations: SABREが各候補に対して実行する、順方向・逆方向のルーティング精緻化サイクルの回数を制御します。 SABREは配線フィードバックから学習することでレイアウトを反復的に改善するため、反復回数が多ければ多いほど、改善効果が高まります。
どちらもトランスパイレーションに時間がかかるという代償を伴いますが、その結果得られる回路はより短くなり、使用するゲート数も少なくなるため、実際のハードウェア上でのデコヒーレンスやゲートエラーを直接的に低減することができます。
ステップ 2a: デフォルトのパスマネージャーを確認します。 PassManagerA StagedPassManager は、各々がそれ自体が a である段階 (init, layout, routing, translation, optimization, scheduling) から構成される。 ステージ上で呼び出す .draw() と、そのパスがグラフとして表示されるため、どこ SabreLayout にあるのかを確認できます。
# Build the default pass manager (no modifications yet)
pm_1 = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=seed
)
# Visualize the layout stage to see where SabreLayout sits
pm_1.layout.draw()Output:
上の図では、カスタマイズしたいパスは SabreLayout 、レイアウトステージの **[2番目の]**位置にある 内にあります。 ConditionalController そのコントローラーには、次の2つの機能があります:
- ゲート
SabreLayout処理が行われるため、 [1] が完全なマッピングを見つけられなかった場合にVF2Layoutのみ実行されます(それ以外の場合は、完全な VF2 のレイアウトが維持されます)。 - これに先立ち
SabreLayout、 SabreLayout's の内部ルーティング中に測定値の順序が入れ替わらないようにするパスが実行されますBarrierBeforeFinalMeasurements。
replace(index=2, passes=sl_2)単に すれば、どちらの挙動も無効になります。 それらを保持するために、交換する前に、当社の特注 SabreLayout 品を同じ ConditionalController 包装材(同じ状態と保護層を保ったまま)で再度包み直します。
ステップ 2b: :カスタム SabreLayout パスを構築し、デフォルトのパスを置き換えます。
cmap = backend.coupling_map
# Custom SabreLayout passes with more aggressive search
sl_2 = SabreLayout(
coupling_map=cmap,
seed=seed,
max_iterations=4,
layout_trials=200,
swap_trials=200,
)
sl_3 = SabreLayout(
coupling_map=cmap,
seed=seed,
max_iterations=8,
layout_trials=200,
swap_trials=200,
)
# Same condition the preset uses: only run SabreLayout when VF2Layout did not
# find a perfect mapping. This preserves any perfect layout VF2 produced at [1].
def _vf2_match_not_found(property_set):
if property_set["layout"] is None:
return True
return (
property_set["VF2Layout_stop_reason"] is not None
and property_set["VF2Layout_stop_reason"]
is not VF2LayoutStopReason.SOLUTION_FOUND
)
def wrap_sabre(sabre_pass):
"""Re-wrap a SabreLayout in the original ConditionalController + barrier."""
return ConditionalController(
[
BarrierBeforeFinalMeasurements(
"qiskit.transpiler.internal.routing.protection.barrier"
),
sabre_pass,
],
condition=_vf2_match_not_found,
)
# Build two fresh pass managers and swap in the wrapped custom SabreLayout at index 2
pm_2 = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=seed
)
pm_3 = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=seed
)
pm_2.layout.replace(index=2, passes=wrap_sabre(sl_2))
pm_3.layout.replace(index=2, passes=wrap_sabre(sl_3))
# Build pm_star: default preset with StarPreRouting added to the init stage
pm_star = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=seed
)
pm_star.init += StarPreRouting()
# Visualize pm_3 after replacement (pm_2 has the same structure, only max_iterations differs)
pm_3.layout.draw()Output:
max_iterations=4``pm_3位置 [2] は再び になりました ConditionalController 。形状はデフォルトのものと同じですが、内部の SabreLayout はカスタムのものになっています(、 swap_trials=200``layout_trials=200、および max_iterations=8 が に設定されており、 pm_2 は を除いて同一です)。 pm_2保護バリアとゲーティングは _vf2_match_not_found 維持されているため、/pm_3 と pm_1 の唯一の違いは、SABREの構成そのものです。 pm_star デフォルト SabreLayout の設定を維持し、initステージの最後にのみを追加 StarPreRouting します。
ステップ 2c: 各パスマネージャーを実行し、結果を比較する。
results_sim = {}
for name, pm in [
("pm_1 (4,20,20)", pm_1),
("pm_2 (4,200,200)", pm_2),
("pm_3 (8,200,200)", pm_3),
("pm_star (default + StarPreRouting)", pm_star),
]:
t0 = time.time()
tqc = pm.run(qc_sim)
elapsed = time.time() - t0
depth = tqc.depth(lambda x: x.operation.num_qubits == 2)
size = tqc.size()
ops_mapped = [op.apply_layout(tqc.layout) for op in operators_sim]
results_sim[name] = {
"tqc": tqc,
"ops": ops_mapped,
"depth": depth,
"size": size,
"time": elapsed,
}
print(f"{name}: 2Q Depth {depth}, Size {size}, Time {elapsed:.2f}s")
# Print improvement relative to default (pm_1)
baseline = results_sim["pm_1 (4,20,20)"]
print("\nImprovement vs. default (pm_1):")
for name in [
"pm_2 (4,200,200)",
"pm_3 (8,200,200)",
"pm_star (default + StarPreRouting)",
]:
r = results_sim[name]
depth_pct = (baseline["depth"] - r["depth"]) / baseline["depth"] * 100
size_pct = (baseline["size"] - r["size"]) / baseline["size"] * 100
print(f" {name}: 2Q depth {depth_pct:+.1f}%, size {size_pct:+.1f}%")Output:
pm_1 (4,20,20): 2Q Depth 38, Size 183, Time 0.01s
pm_2 (4,200,200): 2Q Depth 36, Size 183, Time 0.15s
pm_3 (8,200,200): 2Q Depth 30, Size 158, Time 0.16s
pm_star (default + StarPreRouting): 2Q Depth 26, Size 160, Time 0.01s
Improvement vs. default (pm_1):
pm_2 (4,200,200): 2Q depth +5.3%, size +0.0%
pm_3 (8,200,200): 2Q depth +21.1%, size +13.7%
pm_star (default + StarPreRouting): 2Q depth +31.6%, size +12.6%
3つの修正済みパスマネージャーはすべて、デフォルトよりも 2Q の深さが浅い回路を生成した。 pm_3攻撃的なSABRE構成(pm_2 および)は、トランスピレーション時間を長くする代わりに探索範囲を広げていますが、は pm_star 回路のスター構造を活用し、追加のトランスピレーションコストを一切かけずに、さらに浅い検索結果を生み出します。 具体的な改善効果は実行ごとに異なりますが、全体的な傾向は一貫しています。すなわち、SABREの試行回数や反復回数を増やすことで、ヒューリスティック探索の対象範囲を広げることができ、また、回路の形状が一致する場合は、のような StarPreRouting 構造認識型のパスによって、その探索を完全に回避することができます。
この小規模なスケール(15キュービット)であっても、改善の余地は十分にあり、3つのアプローチすべてがデフォルトの性能を上回っている。 大規模な回路(100クビット以上)では、探索空間が飛躍的に拡大し、大規模回路のセクションで後述するように、試行回数の増加と構造を考慮したパスの両方の利点がより顕著になります。
pm_names = list(results_sim.keys())
depths = [results_sim[n]["depth"] for n in pm_names]
sizes = [results_sim[n]["size"] for n in pm_names]
times = [results_sim[n]["time"] for n in pm_names]
colors = ["#404080", "#2a9d8f", "#a8d05e", "#e29bdd"]
x = np.arange(len(pm_names))
fig, axs = plt.subplots(1, 3, figsize=(14, 5))
# 2Q Depth
bars = axs[0].bar(x, depths, color=colors)
axs[0].set_ylabel("2Q Depth", fontsize=11)
axs[0].set_title("Two-Qubit Gate Depth", fontsize=13)
axs[0].set_ylim(0, max(depths) * 1.2)
for bar, val in zip(bars, depths):
axs[0].text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + max(depths) * 0.02,
str(val),
ha="center",
va="bottom",
fontsize=11,
fontweight="bold",
)
for i in range(1, len(depths)):
pct = (depths[0] - depths[i]) / depths[0] * 100
if pct != 0:
axs[0].text(
bars[i].get_x() + bars[i].get_width() / 2,
bars[i].get_height() / 2,
f"{pct:+.0f}%",
ha="center",
va="center",
fontsize=10,
color="white",
fontweight="bold",
)
# Size
bars = axs[1].bar(x, sizes, color=colors)
axs[1].set_ylabel("Gate Count", fontsize=11)
axs[1].set_title("Circuit Size", fontsize=13)
axs[1].set_ylim(0, max(sizes) * 1.2)
for bar, val in zip(bars, sizes):
axs[1].text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + max(sizes) * 0.02,
str(val),
ha="center",
va="bottom",
fontsize=11,
fontweight="bold",
)
for i in range(1, len(sizes)):
pct = (sizes[0] - sizes[i]) / sizes[0] * 100
if abs(pct) > 0.1:
axs[1].text(
bars[i].get_x() + bars[i].get_width() / 2,
bars[i].get_height() / 2,
f"{pct:+.0f}%",
ha="center",
va="center",
fontsize=10,
color="white",
fontweight="bold",
)
# Time
bars = axs[2].bar(x, times, color=colors)
axs[2].set_ylabel("Time (s)", fontsize=11)
axs[2].set_title("Transpilation Time", fontsize=13)
axs[2].set_ylim(0, max(times) * 1.3)
for bar, val in zip(bars, times):
axs[2].text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + max(times) * 0.03,
f"{val:.2f}s",
ha="center",
va="bottom",
fontsize=11,
fontweight="bold",
)
for ax in axs:
ax.set_xticks(x)
ax.set_xticklabels(pm_names, fontsize=8, rotation=15)
ax.grid(axis="y", linestyle="--", alpha=0.5)
plt.suptitle(
"Transpilation quality vs. configuration",
fontsize=14,
fontweight="bold",
y=1.02,
)
plt.tight_layout()
plt.show()Output:
ステップ3: Qiskit primitivesを使用して実行する
各トランスパイルされた回路について、実際のバックエンドから導出したノイズモデルを用いて、Aer EstimatorV2 を使用して 10回実行します。 シミュレーション結果にはランごとにばらつきがあるため、複数のランの結果を平均化することで、より信頼性の高い精度推定値が得られ、誤差棒を用いて統計的な不確実性を定量化することが可能になります。
# Create a noisy estimator from the real backend's noise model
noisy_estimator = AerEstimator.from_backend(backend)
num_runs = 10
# sim_all_runs[name] = list of arrays, one per run
sim_all_runs = {name: [] for name in results_sim}
for run in range(num_runs):
for name, r in results_sim.items():
job = noisy_estimator.run([(r["tqc"], r["ops"])])
evs = list(job.result()[0].data.evs)
sim_all_runs[name].append(evs)
print(f"Run {run + 1}/{num_runs} done")
# Compute mean and std across runs for each config
sim_stats = {}
for name in results_sim:
all_evs = np.array(sim_all_runs[name]) # shape (num_runs, num_operators)
sim_stats[name] = {
"mean": np.mean(all_evs, axis=0),
"std": np.std(all_evs, axis=0),
"overall_mean": np.mean(all_evs),
"overall_std": np.std(
np.mean(all_evs, axis=1)
), # std of per-run averages
}
print(
f"{name}: mean fidelity = {sim_stats[name]['overall_mean']:.4f} +/- {sim_stats[name]['overall_std']:.4f}"
)Output:
Run 1/10 done
Run 2/10 done
Run 3/10 done
Run 4/10 done
Run 5/10 done
Run 6/10 done
Run 7/10 done
Run 8/10 done
Run 9/10 done
Run 10/10 done
pm_1 (4,20,20): mean fidelity = 0.9510 +/- 0.0094
pm_2 (4,200,200): mean fidelity = 0.9513 +/- 0.0043
pm_3 (8,200,200): mean fidelity = 0.9540 +/- 0.0065
pm_star (default + StarPreRouting): mean fidelity = 0.9547 +/- 0.0072
これは小規模な回路であるため、4つの構成すべてにおいて、忠実度の値は比較的近い範囲に収まっています。 回路が十分に短いため、最適化が最も不十分なバージョンであっても、ハードウェアノイズによる影響はそれほど大きくありません。 平均フィデリティは、おおむね 2Q の深さと相関している。 pm_3 最も浅い2つの回路である pm_star と は、最も高いフィデリティを達成しており、誤差範囲内では実質的に同値である。 pm_1``pm_2 は有用な反例である。 2Q の深さは よりも浅いにもかかわらず、その平均フィデリティもわずかに低くなっており、これは深さとフィデリティの関係が決定論的ではなく統計的なものであることを改めて示している。 レイアウトで選択される具体的な量子ビットや、実行時のそれらの量子ビットのキャリブレーションも重要である。
ステップ4:後処理を行い、結果を希望の古典形式で返す
次に、量子ビット間の距離の関数として、エンタングルメント相関 をプロットするとともに、単一の忠実度指標としての平均相関も併せてプロットする。 理想的な(ノイズのない)場合、すべての相関は1となる。 現実的なノイズが存在する場合、ゲートが1つ追加されるごとに誤差が生じ、時間ステップが1つ進むごとにデコヒーレンスが起こるため、深さが浅く、ゲート数(特に2量子ビットゲート)が少ないトランスパイルされた回路ほど、量子もつれをよりよく維持できるはずである。
data_sim = list(range(1, len(operators_sim) + 1))
markers = ["o", "s", "^", "*"]
colors_line = ["#404080", "#2a9d8f", "#a8d05e", "#e29bdd"]
fig, (ax1, ax2) = plt.subplots(
1, 2, figsize=(14, 5), gridspec_kw={"width_ratios": [2.5, 1]}
)
# Left: correlations vs distance with error bars (mean +/- 1 std)
for (name, stats), marker, color in zip(
sim_stats.items(), markers, colors_line
):
ax1.errorbar(
data_sim,
stats["mean"],
yerr=stats["std"],
marker=marker,
label=name,
color=color,
linewidth=2,
capsize=3,
capthick=1,
elinewidth=1,
)
ax1.set_xlabel("Distance between qubits $i$", fontsize=11)
ax1.set_ylabel(r"$\langle Z_0 Z_i \rangle$", fontsize=11)
ax1.set_title(
"Entanglement correlations vs. qubit distance (avg. of 10 runs)",
fontsize=12,
)
ax1.legend(fontsize=9)
ax1.grid(alpha=0.3)
# Right: mean correlation bar chart with error bars
names = list(sim_stats.keys())
means = [sim_stats[n]["overall_mean"] for n in names]
stds = [sim_stats[n]["overall_std"] for n in names]
x_bar = np.arange(len(names))
bars = ax2.bar(
x_bar, means, yerr=stds, color=colors_line, capsize=5, ecolor="gray"
)
ax2.set_ylabel(r"Mean $\langle Z_0 Z_i \rangle$", fontsize=11)
ax2.set_title("Average fidelity", fontsize=13, pad=12)
y_range = max(means) - min(means) if max(means) != min(means) else 0.01
# Top of ylim accounts for the bar height + std error bar + headroom for the value label
y_top = max(m + s for m, s in zip(means, stds)) + y_range * 1.5
ax2.set_ylim(min(means) - y_range * 0.8, y_top)
for bar, val, std in zip(bars, means, stds):
ax2.text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + std + y_range * 0.15,
f"{val:.4f}",
ha="center",
va="bottom",
fontsize=10,
fontweight="bold",
)
# Annotate % change vs pm_1
baseline_mean = means[0]
for i in range(1, len(means)):
pct = (means[i] - baseline_mean) / baseline_mean * 100
if abs(pct) > 0.01:
mid_y = (means[i] + ax2.get_ylim()[0]) / 2
ax2.text(
bars[i].get_x() + bars[i].get_width() / 2,
mid_y,
f"{pct:+.1f}%",
ha="center",
va="center",
fontsize=10,
color="white",
fontweight="bold",
)
ax2.set_xticks(x_bar)
ax2.set_xticklabels(names, fontsize=8, rotation=15)
ax2.grid(axis="y", linestyle="--", alpha=0.5)
fig.tight_layout()
plt.show()Output:
結果からは、トランスパイラによる変換の品質と実行の忠実度との間に明確な関連性が示されていますが、いくつかの留意点があります:
pm_1(デフォルト) : ベースライン。 SABREは試行回数がわずか20回、反復回数が4回にとどまるため、最適化の余地が限られており、その結果、SABREのみを用いた回路としては最も深い回路が得られた。pm_2(さらなる実験) :候補の数を10倍に増やして探索すると、レイアウトの深さはわずかに浅くなるが、この規模では深さの増加幅が小さいため、平均忠実度はほぼ横ばいとなる(ノイズの範囲内ではベースラインを下回ることもある)。pm_3(試行回数と反復回数の増加) :試行回数を2max_iterations倍の8回にすると、SABREの精緻化サイクルが増え、比較対象の中で最も単純なSABRE単独回路と、最も高い平均忠実度が得られる。pm_star(デフォルト + StarPreRouting ) : デフォルトのプリセットの初期化段階に を追加StarPreRoutingします。pm_3構造を意識した書き換えにより、スター構造は線形チェーンに簡略化され、トランパイラの残りの部分がこれをデバイスの線形パスにマッピングすることで、全体として最も浅い回路(よりわずかに優れている)が生成され、誤差範囲内では と同等の忠実度を実現pm_3している。 これは、SABREの確率的探索と比較すると、書き換え処理は実質的にコストがかからないため、デフォルト設定と同じトランスパイル時間で実行されます。
なお、を増加 max_iterations させても、必ずしも良い結果が得られるとは限りません。 このケースでは大きな効果がありましたが、他の回路やバックエンドの場合、反復回数を増やしてもそれ以上の改善が見られないばかりか、局所的な最小値での過剰最適化により、パフォーマンスがわずかに低下する可能性もあります。 一般的に、時間的余裕が許す限り、と swap_trials をできるだけ増やすべきです layout_trials 。試行回数が多ければ多いほど、より良いレイアウトが見つかる可能性が高まるからです。 この増加 max_iterations についてはテストする価値がありますが、具体的な使用ケースに合わせて検証を行う必要があります。 のような StarPreRouting 特殊なパスは、その趣旨は似ているものの、回路に依存する度合いがより高い。つまり、対象とする構造が実際にその回路に含まれている場合にのみ有効である。 適用可能な場合はゲインが大きくなり、そうでない場合はゼロになりますが、試してみるのに実質的なコストはかかりません。
大規模なハードウェアの例
SABREでは、試験数の調整に加え、 ルーティングヒューリスティックのカスタマイズもサポートしています。 SABREは3つのヒューリスティックを提供しています:
basic: 次のゲートまでの直線距離を最小化するスワップを選択する、単純な貪欲法。decay(デフォルト): 直近のアクティビティに基づいて量子ビットに動的に重み付けを行い、同じ量子ビットでの繰り返しのスワップを抑制します。lookahead: 今後のゲートを先読みして将来のルーティングコストを評価し、より良いスワップ順序を見出す可能性があります。
カスタムヒューリスティックを使用するには、パスを作成し SabreSwap 、パラメータを routing_pass 介して に SabreLayout 接続します。
SabreLayout``pm_star_hw比較対象として4つ目のパスマネージャーが追加されました。これはデフォルトの設定を維持しつつ、SabreSwap 初期化段階に を追加 StarPreRouting するものです。 この規模(100キュービット)では、SABRE検索はより困難になり、スター構造から線形チェーンへの書き換えが明らかに有利になります。これは、ヘロンプロセッサには、結果として得られる回路を収容するのに十分な長さの線形パスがあるからです。
ここでは、3つのSABREヒューリスティックすべてを、 StarPreRouting 100キュービットのGHZ回路上で大規模に比較する。 SABRE構成について、異なるシード値を用いて複数のレイアウト試行を行い、それぞれから最適なトランスパイル済み回路を選択し、それらすべてを結果 StarPreRouting とともに実ハードウェアに送信します。
手順 1~4 を 1 つのコードブロックにまとめました
ここでは、ワークフロー全体をより大きなスケールでまとめられています。 SabreLayoutを routing_pass の として使用 SabreSwap する場合、1回の呼び出しにつき1回のレイアウト試行のみが行われるため、以下のコードセルでは、シードを順に試行してレイアウト空間を探索しています。
(上記の)小規模なステップ2で定義されたのと同じ wrap_sabre ヘルパーを使用し、 [インデ]ックス1のステージも routing 同様にであるため、同様の wrap_routing ヘルパーを追加します。これをそのまま置き換えると、同様に保護バリアとゲーティングが _swap_condition``ConditionalController([BarrierBeforeFinalMeasurements, routing_pass], ...) 失われてしまうからです。
# -------------------------Step 1-------------------------
num_qubits = 100
# Create star-topology GHZ circuit
qc = QuantumCircuit(num_qubits)
qc.h(0)
for i in range(1, num_qubits):
qc.cx(0, i)
qc.measure_all()
# ZZ operators
operator_strings = [
"Z" + "I" * i + "Z" + "I" * (num_qubits - 2 - i)
for i in range(num_qubits - 1)
]
operators = [SparsePauliOp(op) for op in operator_strings]# -------------------------Step 2-------------------------
num_seeds = 10
seed_list = [seed + i for i in range(num_seeds)]
swap_trials = 200
# The default routing[1] is a ConditionalController([barrier, routing_pass],
# condition=_swap_condition); we re-wrap so the new routing pass keeps the
# protective barrier and is skipped when routing isn't needed (matches the preset).
def _swap_condition(property_set):
return not property_set["routing_not_needed"]
def wrap_routing(routing_pass):
return ConditionalController(
[
BarrierBeforeFinalMeasurements(
"qiskit.transpiler.internal.routing.protection.barrier"
),
routing_pass,
],
condition=_swap_condition,
)
heuristic_results = {}
# Three SABRE heuristics, swept over seeds
for heuristic in ["basic", "decay", "lookahead"]:
trials = []
for s in seed_list:
sr = SabreSwap(
coupling_map=cmap, heuristic=heuristic, trials=swap_trials, seed=s
)
sl = SabreLayout(coupling_map=cmap, routing_pass=sr, seed=s)
pm = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=s
)
# Re-wrap each custom pass in its original ConditionalController + barrier
# (wrap_sabre is defined in the small-scale Step 2 cell above).
pm.layout.replace(index=2, passes=wrap_sabre(sl))
pm.routing.replace(index=1, passes=wrap_routing(sr))
t0 = time.time()
tqc = pm.run(qc)
elapsed = time.time() - t0
depth = tqc.depth(lambda x: x.operation.num_qubits == 2)
size = tqc.size()
trials.append(
{
"tqc": tqc,
"depth": depth,
"size": size,
"time": elapsed,
"seed": s,
}
)
heuristic_results[heuristic] = trials
# Default preset + StarPreRouting in init, also swept over seeds for a fair comparison
star_trials = []
for s in seed_list:
pm_star_hw = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=s
)
pm_star_hw.init += StarPreRouting()
t0 = time.time()
tqc = pm_star_hw.run(qc)
elapsed = time.time() - t0
depth = tqc.depth(lambda x: x.operation.num_qubits == 2)
size = tqc.size()
star_trials.append(
{
"tqc": tqc,
"depth": depth,
"size": size,
"time": elapsed,
"seed": s,
}
)
heuristic_results["StarPreRouting"] = star_trials
# Print summary for each entry
for label in ["basic", "decay", "lookahead", "StarPreRouting"]:
trials = heuristic_results[label]
depths = [t["depth"] for t in trials]
sizes = [t["size"] for t in trials]
best = min(trials, key=lambda t: t["depth"])
print(f"{label}:")
print(
f" 2Q depth: min: {min(depths)}, mean: {np.mean(depths):.1f}, std: {np.std(depths):.1f}"
)
print(
f" size : min: {min(sizes)}, mean: {np.mean(sizes):.1f}, std: {np.std(sizes):.1f}"
)
print(
f" best seed: {best['seed']} (2Q depth={best['depth']}, size={best['size']})"
)Output:
basic:
2Q depth: min: 524, mean: 570.5, std: 39.9
size : min: 3819, mean: 4227.1, std: 360.6
best seed: 51 (2Q depth=524, size=3852)
decay:
2Q depth: min: 387, mean: 436.4, std: 41.7
size : min: 2687, mean: 3183.1, std: 459.3
best seed: 45 (2Q depth=387, size=2786)
lookahead:
2Q depth: min: 364, mean: 424.6, std: 36.5
size : min: 2335, mean: 3014.6, std: 388.1
best seed: 51 (2Q depth=364, size=2485)
StarPreRouting:
2Q depth: min: 196, mean: 196.0, std: 0.0
size : min: 1151, mean: 1151.0, std: 0.0
best seed: 42 (2Q depth=196, size=1151)
hw_colors = {
"basic": "#ff7f0e",
"decay": "#d62728",
"lookahead": "#1f77b4",
"StarPreRouting": "#2a9d8f",
}
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 5))
for label in ["basic", "decay", "lookahead", "StarPreRouting"]:
trials = heuristic_results[label]
depths = [t["depth"] for t in trials]
sizes = [t["size"] for t in trials]
seeds = [t["seed"] for t in trials]
color = hw_colors[label]
ax1.scatter(
seeds,
depths,
label=label,
color=color,
alpha=0.8,
edgecolor="k",
s=60,
)
ax1.axhline(np.mean(depths), color=color, linestyle="--", alpha=0.5)
ax2.scatter(
seeds,
sizes,
label=label,
color=color,
alpha=0.8,
edgecolor="k",
s=60,
)
ax2.axhline(np.mean(sizes), color=color, linestyle="--", alpha=0.5)
ax1.set_xlabel("Seed", fontsize=11)
ax1.set_ylabel("2Q Depth", fontsize=11)
ax1.set_title("Two-Qubit Gate Depth per Seed", fontsize=13)
ax1.legend(fontsize=10)
ax1.grid(alpha=0.3)
ax2.set_xlabel("Seed", fontsize=11)
ax2.set_ylabel("Gate Count", fontsize=11)
ax2.set_title("Circuit Size per Seed", fontsize=13)
ax2.legend(fontsize=10)
ax2.grid(alpha=0.3)
plt.suptitle(
"Transpilation variability across seeds: SABRE heuristics vs. StarPreRouting",
fontsize=14,
fontweight="bold",
y=1.02,
)
plt.tight_layout()
plt.show()
# Summary comparison
for label in ["basic", "decay", "lookahead", "StarPreRouting"]:
best = min(heuristic_results[label], key=lambda t: t["depth"])
print(
f"{label}: best 2Q depth={best['depth']}, size={best['size']} (seed={best['seed']})"
)Output:
basic: best 2Q depth=524, size=3852 (seed=51)
decay: best 2Q depth=387, size=2786 (seed=45)
lookahead: best 2Q depth=364, size=2485 (seed=51)
StarPreRouting: best 2Q depth=196, size=1151 (seed=42)
# -------------------------Step 3: Execute on hardware-------------------------
best_circuits = {}
for label in ["basic", "decay", "lookahead", "StarPreRouting"]:
best_circuits[label] = min(
heuristic_results[label], key=lambda t: t["depth"]
)
b = best_circuits[label]
print(f"Best {label}: 2Q depth={b['depth']}, size={b['size']}")
options = EstimatorOptions()
options.resilience_level = 2
options.dynamical_decoupling.enable = True
options.dynamical_decoupling.sequence_type = "XY4"
estimator = Estimator(backend, options=options)
hw_jobs = {}
hw_ops = {}
for label, best in best_circuits.items():
hw_ops[label] = [op.apply_layout(best["tqc"].layout) for op in operators]
hw_jobs[label] = estimator.run([(best["tqc"], hw_ops[label])])
print(f"{label} job: {hw_jobs[label].job_id()}")
estimator.options.environment.job_tags = ["TUT_TOWS"]
hw_results = {}
for label, job in hw_jobs.items():
hw_results[label] = job.result()[0]
print(f"{label} job done")Output:
Best basic: 2Q depth=524, size=3852
Best decay: 2Q depth=387, size=2786
Best lookahead: 2Q depth=364, size=2485
Best StarPreRouting: 2Q depth=196, size=1151
basic job: d81q5tnoha1c73bknprg
decay job: d81q5tugbeec73aktopg
lookahead job: d81q5to0bvlc73d1epe0
StarPreRouting job: d81q5u7tjchs73bn82hg
basic job done
decay job done
lookahead job done
StarPreRouting job done
# -------------------------Step 4: Post-process-------------------------
data = list(range(1, len(operators) + 1))
hw_markers = {
"basic": "D",
"decay": "o",
"lookahead": "s",
"StarPreRouting": "*",
}
hw_labels = ["basic", "decay", "lookahead", "StarPreRouting"]
fig, (ax1, ax2) = plt.subplots(
1, 2, figsize=(14, 5), gridspec_kw={"width_ratios": [2.5, 1]}
)
# Left: correlations vs distance
for label in hw_labels:
evs = list(hw_results[label].data.evs)
b = best_circuits[label]
ax1.plot(
data,
evs,
marker=hw_markers[label],
color=hw_colors[label],
linewidth=2,
label=f"{label} (2Q depth={b['depth']}, size={b['size']})",
markersize=5 if label == "StarPreRouting" else 4,
)
ax1.set_xlabel("Distance between qubits $i$", fontsize=11)
ax1.set_ylabel(r"$\langle Z_0 Z_i \rangle$", fontsize=11)
ax1.set_title(
"Entanglement correlations vs. qubit distance (hardware)", fontsize=12
)
ax1.legend(fontsize=9)
ax1.grid(alpha=0.3)
# Right: mean fidelity bar chart
hw_means = [np.mean(list(hw_results[label].data.evs)) for label in hw_labels]
hw_bar_colors = [hw_colors[label] for label in hw_labels]
x_bar = np.arange(len(hw_labels))
bars = ax2.bar(x_bar, hw_means, color=hw_bar_colors)
ax2.set_ylabel(r"Mean $\langle Z_0 Z_i \rangle$", fontsize=11)
ax2.set_title("Average fidelity", fontsize=13)
y_range = (
max(hw_means) - min(hw_means) if max(hw_means) != min(hw_means) else 0.01
)
ax2.set_ylim(min(hw_means) - y_range * 0.2, max(hw_means) + y_range * 0.15)
for bar, val in zip(bars, hw_means):
ax2.text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + y_range * 0.05,
f"{val:.4f}",
ha="center",
va="bottom",
fontsize=11,
fontweight="bold",
)
ax2.set_xticks(x_bar)
ax2.set_xticklabels(hw_labels, fontsize=9, rotation=15)
ax2.grid(axis="y", linestyle="--", alpha=0.5)
fig.tight_layout()
plt.show()
print("\nMean fidelity:")
for label, m in zip(hw_labels, hw_means):
print(f" {label}: {m:.4f}")Output:
Mean fidelity:
basic: 0.0344
decay: 0.1298
lookahead: 0.1857
StarPreRouting: 0.3295
解析
散布図からは、3つのSABREヒューリスティックすべてにおいて、種ごとに大きなばらつきが見られ、単一のトランスパイレーションに頼るのではなく、複数のレイアウト試行を実行することの重要性が浮き彫りになっている。 この StarPreRouting ラインは、シードを問わず実質的に横ばいとなっています。これは、構造が与えられれば、スター構造から線形チェーンへの書き換えが決定論的であるためです。また、下流のSABREルーティングでは線形チェーン上での自由度が極めて低いため、シードは最終的な深さやサイズにほとんど影響を与えません。
トランスパイラの結果から、 および lookahead のヒューリスティックは decay 、いずれも を常に大幅な差で上回 basic っていることがわかる。 この basic ヒューリスティックは高速である一方、単純な貪欲法を採用しているため、回路が著しく複雑になることがよくあります。 StarPreRoutingこのスター型トポロジーのGHZ回路については、 lookahead その先見的なコスト関数が長距離の接続パターンを持つ回路に非常に適しているため、SABREヒューリスティックの中でも 2Q の深さとゲート数が最も小さくなる傾向がある。, しかし、この手法はこれら3つをすべて大幅に上回っている。ルーティングの前にスター構造を線形チェーンに書き換えることで、検索問題を完全に回避し、トランパイラの残りの部分が最小限の追加SWAPで線形パスにマッピングできる回路を生成する。
その利点は、ハードウェアの忠実度にもそのまま反映されています。 2Q の深さやゲート数が少ないからといって、必ずしも忠実度が1対1で向上するわけではありません(レイアウトで使用される具体的な物理量子ビットや、実行時のキャリブレーションも重要だからです)。しかし、SABREとこ StarPreRouting ことの間の深さの差がこれほど大きい場合、構造を意識したアプローチが圧倒的に優れています。なぜなら、この回路ではデコヒーレンスの蓄積がはるかに少なく、2量子ビットエラーの発生もはるかに少ないからです。 忠実度の棒グラフを見ると StarPreRouting 、は最高のSABREヒューリスティックよりも大幅に上回っているのに対し basic 、は回路がはるかに複雑であるため、最も多くの誤差が蓄積され、他の手法を大きく下回っている。
主なポイント:
- SABREのヒューリスティック法のうち、
decayおよびlookaheadは、非自明な回路において、よりもbasic著しく優れた性能を示す。 本番環境のワークロードには、この2つのうちいずれかを選択してください。 - 最適なSABREヒューリスティックは、回路やハードウェアによって異なります。 複数のシードを用いて複数のヒューリスティックをテストすることが、最も信頼性の高い戦略である。
- さらに多くのレイアウトを検討したい場合は、リモートノードに作業を分散させるのではなく、(および
layout_trials、カスタムルーティングパスをピン留めしていない場合は)を増やすswap_trialsようにしてください。 SABREパスでは、すでにローカルスレッド間でトライアルが並列化されており、トライアルごとの処理量はごくわずかであるため、通常、分散処理にかかるオーバーヘッドが速度向上効果を上回ってしまいます。 - 回路に既知の特殊な構造がある場合、SABREの実行前に構造を意識したパスを適用することで
StarPreRouting、SABREのチューニングをいくら行っても達成できないほどの、桁違いの性能向上が得られる。 これはSABREの代替となるものではありません。StarPreRouting回路に実際にスター型サブ回路が含まれており、バックエンドに十分な長さの直線経路がある場合にのみ役立ちます。 回路の形状が分かっている場合は、passライブラリで一致するものが無いか確認してみる価値があります。
次のステップ
この作品に興味を持たれた方は、以下の資料もご参照ください:
SabreLayoutAPIリファレンス :パラメータに関する完全なドキュメント- SABRE論文 :レイアウトおよび配線のためのオリジナルのSABREアルゴリズム
- LightSABRE 論文 :Qiskitの現在のSABRE実装を支えるアルゴリズムの改善点
- カスタムトランスパイラーパスの作成 :独自のトランスパイルロジックを構築する
- トランスパイラープラグイン :サードパーティ製のパスを使用してQiskitのトランスパイレーションパイプラインを拡張する
- DAGの表現 :トランスパイラーが内部で使用する有向非循環グラフ(DAG)を理解する
チュートリアル調査
このチュートリアルに関するご意見・ご感想をお寄せください。 あなたの洞察は、私たちのコンテンツの提供とユーザーエクスペリエンスを向上させるのに役立ちます。