SABREによるトランスパイル最適化
使用時間の目安: Heron r2プロセッサで1分(注意: これはあくまで目安です。実際の実行時間は異なる場合があります。)
学習内容
このチュートリアルを通じて、以下の内容を理解できるようになります:
- SABREパラメータ(
layout_trials、swap_trials、max_iterations)を設定してトランスパイル品質を向上させる方法 - トランスパイル実行時間と回路品質(深さとゲート数)のトレードオフ
- SABREルーティングヒューリスティック(
basic、decay、lookahead)をカスタマイズしてハードウェア上での性能を比較する方法
前提条件
このチュートリアルを始める前に、以下のトピックに精通していることをお勧めします:
- 回路のトランスパイル: Qiskitにおけるトランスパイルの概要
- Transpilerステージ: レイアウトおよびルーティングステージ
- プリセットパスマネージャーの設定: 最適化レベルのカスタマイズ
背景
トランスパイルは量子回路を特定の量子ハードウェアと互換性のある形式に変換します。2つの主要な段階として、量子ビットレイアウト(論理量子ビットを物理量子ビットにマッピングすること)とゲートルーティング(デバイスの接続性に従ってマルチ量子ビットゲートが実行できるようにSWAPゲートを挿入すること)があります。
SABRE(SWAP-Based Bidirectional heuristic search algorithm)は、レイアウトとルーティングの両方を最適化します。IBM® Heronプロセッサのような複雑なカップリングマップを持つデバイスでの大規模回路(100量子ビット以上)に特に効果的です。SABREはSWAPゲートを最小化し、回路の深さを削減することで、実行の忠実度を向上させます。LightSABREアルゴリズムの最近の改良により、実行時間とゲート数がさらに削減されています。
このチュートリアルでは、まず異なるパラメータでSabreLayoutを設定して小規模なGHZ回路を最適化し、実行の忠実度への影響を観察します。次に、実際のハードウェア上でSABREのルーティングヒューリスティックを大規模で比較します。
要件
このチュートリアルを始める前に、以下がインストールされていることを確認してください:
- Qiskit SDK v2.0以降、visualizationサポート付き
- Qiskit Runtime v0.22以降(
pip install qiskit-ibm-runtime) - Qiskit Aer(
pip install qiskit-aer)
セットアップ
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime
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}")
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: 量子ハードウェア実行のための問題の最適化
デフォルトのoptimization_level=3プリセットパスマネージャーはすでにSabreLayoutを使用していますが、保守的なデフォルト設定です。より強力な設定の影響を調べるため、そのパスをより積極的な探索に設定したカスタムSabreLayoutに置き換え、レイアウトステージの他のすべてのパスはそのままにします。比較のための4つ目のパスマネージャーは、デフォルトのSabreLayoutを維持しつつ、initステージにStarPreRoutingを追加します。StarPreRoutingは、スター型サブ回路を検出してルーティング前に線形チェーンに書き換える構造対応パスです。
ワークフローは以下の通りです:
- 検査: デフォルトのパスマネージャーで
SabreLayoutがlayoutステージのどこに位置するかを確認します。 - 置換:
PassManager.replace(index, passes=...)を使用してそのパスをカスタムSabreLayoutインスタンスに置き換え、pm.init += StarPreRouting()でpm_starバリアントを構築します。 - 実行: 4つのパスマネージャーすべてを実行してメトリクスを比較します。
4つの設定は以下の通りです:
| 設定 | 説明 |
|---|---|
pm_1(デフォルト) | レベル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をinitステージに追加したデフォルトプリセット |
SABREの主要パラメータ:
layout_trials/swap_trials: SABREが探索する候補レイアウトとルーティング解の数を制御します。試行回数を増やすことでSABREはより広い探索空間をサンプリングし、より良い解を見つける確率が高まります。max_iterations: SABREが各候補に対して実行する順方向・逆方向ルーティング改良サイクルの回数を制御します。SABREはルーティングフィードバックから学習することでレイアウトを反復的に改善するため、イテレーション数が多いほど改善効果が高まります。
どちらもトランスパイル時間が長くなるというコストが伴いますが、結果として得られる回路はより短くゲート数が少なくなり、実際のハードウェアでのデコヒーレンスとゲートエラーが直接削減されます。
ステップ2a: デフォルトのパスマネージャーを検査する。 StagedPassManagerはステージ(init、layout、routing、translation、optimization、scheduling)から構成され、各ステージ自体がPassManagerです。ステージで.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()

上の図では、カスタマイズしたいSabreLayoutパスが、レイアウトステージの位置**[2]**にあるConditionalControllerの中に位置しています。そのコントローラーは2つの役割を果たしています:
VF2Layout(位置[1])が完全なマッピングを見つけられなかった場合にのみSabreLayoutを実行するようにゲートします(完全なVF2レイアウトが見つかった場合はそのまま保持されます)。SabreLayoutの内部ルーティング中に測定が並べ替えられるのを防ぐBarrierBeforeFinalMeasurementsパスをSabreLayoutの前に配置します。
単純にreplace(index=2, passes=sl_2)とすると、両方の動作が失われます。これらを保持するために、同じConditionalController(同じ条件と保護バリアを含む)でカスタムSabreLayoutを再ラップしてから置換します。
ステップ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()

位置**[2]**は再びConditionalControllerになっています。形状はデフォルトと同一ですが、内部のSabreLayoutがカスタムのもの(pm_3ではlayout_trials=200、swap_trials=200、max_iterations=8、pm_2はmax_iterations=4を除いて同一)になっています。保護バリアと_vf2_match_not_foundゲートは保持されているため、pm_2/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}%")
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深さが低い回路を生成しました。積極的なSABRE設定(pm_2とpm_3)はより広い探索のためにトランスパイル時間を犠牲にし、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()

ステップ3: Qiskitプリミティブを使用して実行する
実際のバックエンドのノイズモデルから導出した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}"
)
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とpm_star(最も浅い2つの回路)が最高の忠実度を達成しており、エラーバー内では実質的に同等です。pm_2は有用な反例です: 2Q深さはpm_1より低いにもかかわらず、平均忠実度はわずかに低くなっており、深さと忠実度の関係が決定論的ではなく統計的であることを示しています。レイアウトが選択する特定の量子ビットとその実行時のキャリブレーションも重要です。
ステップ4: 後処理を行い、望ましい古典形式で結果を返す
次に、量子ビット距離の関数としてエンタングルメント相関をプロットし、単一の忠実度メトリクスとして平均相関を示します。理想的な(ノイズなしの)場合、すべての相関は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()

結果はトランスパイル品質と実行の忠実度の間に明確な関連を示しており、いくつかの有用な留意点があります:
pm_1(デフォルト): ベースライン。試行回数が20、イテレーション数が4のみで、SABREの最適化余地が限られており、SABRE単独では最も深い回路になります。pm_2(試行数増加): 10倍多くの候補を探索することでわずかに浅いレイアウトが見つかりますが、このスケールでは深さの改善が小さいため、平均忠実度はほぼ横ばい(ノイズ内でベースラインを下回る場合もあります)です。pm_3(試行数増加+イテレーション数増加):max_iterationsを8に倍増させることでSABREの改良サイクルが増え、SABRE単独では最も浅い回路が生成され、比較の中で最も高い平均忠実度を達成します。pm_star(デフォルト+StarPreRouting):StarPreRoutingをそれ以外はデフォルトプリセットのinitステージに追加します。構造対応の書き換えによりスターが線形チェーンに変換され、Transpilerの残りの部分がデバイスの線形パスにマッピングし、全体的に最も浅い回路(pm_3よりわずかに良い)を生成し、忠実度でもpm_3とエラーバー内で同等です。書き換えはSABREの確率的探索と比較して本質的に無料のため、デフォルトと同じトランスパイル時間でこれを実現します。
max_iterationsを増やすことが常にプラスの影響をもたらすとは限りません。この場合は大きな効果がありましたが、他の回路やバックエンドでは追加のイテレーションがさらなる改善をもたらさない場合や、局所最適解への過剰最適化により性能がわずかに低下する場合もあります。一般的に、layout_trialsとswap_trialsは時間的余裕の範囲内でできるだけ多く増やすべきです。試行回数が多いほど常により良いレイアウトを見つける確率が高まります。max_iterationsを増やすことも試す価値はありますが、具体的なユースケースで検証する必要があります。StarPreRoutingのような特化パスも精神的には似ていますが、より回路依存的です: 対象の構造が回路に実際に含まれている場合のみ効果があります。適用可能な場合の効果は大きく、そうでない場合はゼロですが、試すコストは本質的にゼロです。
大規模ハードウェアの例
試行回数の調整に加えて、SABREはルーティングヒューリスティックのカスタマイズにも対応しています。SABREは3つのヒューリスティックを提供しています:
basic: 次のゲートへの即時距離を最小化するスワップを選択するシンプルな貪欲アプローチです。decay(デフォルト): 最近の活動に基づいて量子ビットを動的に重み付けし、同じ量子ビットへの繰り返しスワップを抑制します。lookahead: 将来のゲートを先読みしてルーティングコストを評価し、より良いスワップシーケンスを見つける可能性があります。
カスタムヒューリスティックを使用するには、SabreSwapパスを作成し、routing_passパラメータ経由でSabreLayoutに接続します。
比較に4つ目のパスマネージャーとしてpm_star_hwを追加します。これはデフォルトのSabreLayout/SabreSwap設定を維持しつつ、initステージにStarPreRoutingを追加します。このスケール(100量子ビット)ではSABRE探索がより困難になり、Heronプロセッサには結果の回路を収容するのに十分な長い線形パスがあるため、スターを線形チェーンに書き換えることが明確な優位性をもたらします。
ここでは、100量子ビットのGHZ回路で3つのSABREヒューリスティックとStarPreRoutingをすべて大規模で比較します。SABRE設定について複数のシードでレイアウトトライアルを実行し、それぞれから最良のトランスパイル済み回路を選択して、StarPreRoutingの結果と共にすべて実際のハードウェアに送信します。
ステップ1〜4を単一のコードブロックにまとめる
ここでは完全なワークフローをより大規模でまとめています。SabreLayoutのrouting_passとしてSabreSwapを使用する場合、呼び出しごとに1つのレイアウトトライアルのみが実行されます。そのため、以下のコードセルでは複数のシードをループしてレイアウト空間を探索します。
小規模のステップ2で定義したwrap_sabreヘルパー(上記参照)を使用し、類似したwrap_routingヘルパーも追加します。routingステージのインデックス[1]もConditionalController([BarrierBeforeFinalMeasurements, routing_pass], ...)であり、そのままの置換では保護バリアと_swap_conditionゲートが失われるためです。
# -------------------------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']})"
)
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']})"
)

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

Mean fidelity:
basic: 0.0344
decay: 0.1298
lookahead: 0.1857
StarPreRouting: 0.3295
分析
散布図はすべての3つのSABREヒューリスティックでシード間に大きなばらつきを示しており、単一のトランスパイルに頼るのではなく複数のレイアウトトライアルを実行することの重要性を強調しています。StarPreRoutingの線はシード間でほぼ平坦です。スターを線形チェーンに書き換える操作は構造から決定論的であり、その後のSABREルーティングは線形チェーン上で自由度がほとんどないため、シードが最終的な深さやサイズにほとんど影響しないためです。
トランスパイル結果から、decayとlookaheadの両ヒューリスティックはbasicを大きく上回っています。basicヒューリスティックは高速ですが、シンプルな貪欲戦略を使用するため、しばしば大幅に深い回路につながります。このスター型トポロジのGHZ回路では、lookaheadはSABREヒューリスティックの中で最低の2Q深さとゲート数を生成する傾向があります。その先読みコスト関数が長距離の接続パターンを持つ回路に適しているためです。しかしStarPreRoutingはすべての3つを大幅に上回っています: ルーティング前にスターを線形チェーンに書き換えることで探索問題を完全に回避し、最小限の追加SWAPで線形パスにマッピングできる回路をTranspilerの残りに提供します。
この優位性はハードウェアの忠実度にもそのまま反映されます。2Q深さとゲート数の削減は必ずしも忠実度の向上に1対1で対応するわけではありません(レイアウトが使用する特定の物理量子ビットとその実行時のキャリブレーションも重要です)が、SABREとStarPreRoutingの間の深さの差がここまで大きい場合、構造対応アプローチは回路に蓄積されるデコヒーレンスと2量子ビットエラーイベントがはるかに少ないため、決定的に優れています。忠実度の棒グラフはStarPreRoutingが最良のSABREヒューリスティックを大幅に上回り、basicは最も深い回路で最も多くのエラーが蓄積されるため最下位に位置することを示しています。
主要な結論:
- SABREヒューリスティックの中では、
decayとlookaheadは非自明な回路に対してbasicより大幅に優れています。本番ワークロードにはこのいずれかを使用することをお勧めします。 - 最適なSABREヒューリスティックは回路とハードウェアに依存します。複数のヒューリスティックと複数のシードでテストすることが最も信頼性の高い戦略です。
- さらに多くのレイアウトを探索したい場合は、リモートノードにワークを分散させるのではなく、
swap_trials(およびカスタムルーティングパスを固定していない場合はlayout_trialsも)を増やしてください。SABREパスはすでにローカルスレッド間でトライアルを並列化しており、トライアルごとの作業量は小さいため、分散オーバーヘッドが速度向上を上回ることがほとんどです。 - 回路に特定の特殊構造がある場合、
StarPreRoutingのような構造対応パスをSABREの前に適用することで、どれだけのSABREチューニングでも到達できない桁違いの改善が得られる可能性があります。これはSABREの代替ではありません:StarPreRoutingは回路に実際にスター型サブ回路が含まれており、バックエンドが十分に長い線形パスを持つ場合にのみ効果があります。回路の形状が分かっている場合はパスライブラリで一致を探す価値があります。
次のステップ
この内容が興味深いと感じた方は、以下の資料をご参照ください:
SabreLayoutAPIリファレンス: 完全なパラメータドキュメント- SABREの論文: レイアウトとルーティングのための元のSABREアルゴリズム
- LightSABREの論文: QiskitのSABRE現実装を支えるアルゴリズム上の改良
- カスタムTranspilerパスの作成: 独自のトランスパイルロジックを構築する
- Transpilerプラグイン: サードパーティパスでQiskitのトランスパイルパイプラインを拡張する
- DAG表現: Transpilerが内部で使用する有向非巡回グラフを理解する
チュートリアルアンケート
このチュートリアルに関するフィードバックを提供するために、こちらの短いアンケートにご協力ください。皆様のご意見は、コンテンツの提供内容やユーザーエクスペリエンスの改善に役立てさせていただきます。
注意: このアンケートはIBM Quantumが提供するものであり、チュートリアルコンテンツ(IBMが作成)に関するものです。doQumentationはウェブサイト、翻訳、およびコード実行を提供しています — これらに関するフィードバックはGitHubイシューを開くことでお知らせください。