Optimisasi transpilasi dengan SABRE
Estimasi penggunaan: 1 menit pada prosesor Heron r2 (CATATAN: Ini hanya estimasi. Waktu aktual kamu bisa berbeda.)
Hasil pembelajaran
Setelah menyelesaikan tutorial ini, kamu akan memahami:
- Cara mengkonfigurasi parameter SABRE (
layout_trials,swap_trials,max_iterations) untuk meningkatkan kualitas transpilasi - Trade-off antara waktu transpilasi dan kualitas sirkuit (kedalaman dan jumlah gate)
- Cara menyesuaikan heuristik routing SABRE (
basic,decay,lookahead) dan membandingkan performanya pada perangkat keras
Prasyarat
Kami menyarankan kamu sudah familiar dengan topik-topik berikut sebelum mengikuti tutorial ini:
- Transpile circuits: ikhtisar transpilasi di Qiskit
- Transpiler stages: tahap layout dan routing
- Configure preset pass managers: menyesuaikan optimization level
Latar Belakang
Transpilasi mengubah Circuit kuantum ke bentuk yang kompatibel dengan perangkat keras kuantum tertentu. Dua tahap utamanya adalah memilih qubit layout (memetakan qubit logis ke qubit fisik) dan gate routing (menyisipkan gate SWAP agar gate multi-qubit mematuhi konektivitas perangkat).
SABRE (SWAP-Based Bidirectional heuristic search algorithm) mengoptimalkan layout maupun routing. Alat ini sangat efektif untuk sirkuit berskala besar (100+ qubit) pada perangkat dengan coupling map yang kompleks, seperti prosesor IBM® Heron. SABRE meminimalkan gate SWAP dan mengurangi kedalaman sirkuit, sehingga meningkatkan fidelitas eksekusi. Peningkatan terbaru dalam algoritma LightSABRE semakin mengurangi runtime dan jumlah gate.
Dalam tutorial ini, kamu pertama-tama akan mengkonfigurasi SabreLayout dengan parameter berbeda untuk mengoptimalkan sirkuit GHZ kecil dan mengamati dampaknya pada fidelitas eksekusi. Kemudian, kamu akan membandingkan heuristik routing SABRE pada skala besar di perangkat keras nyata.
Persyaratan
Sebelum memulai tutorial ini, pastikan kamu sudah menginstal hal-hal berikut:
- Qiskit SDK v2.0 atau lebih baru, dengan dukungan visualisasi
- Qiskit Runtime v0.22 atau lebih baru (
pip install qiskit-ibm-runtime) - Qiskit Aer (
pip install qiskit-aer)
Pengaturan
# 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
Contoh simulator skala kecil
Di bagian ini, sebuah simulator berisik berdasarkan noise model Backend nyata digunakan untuk mendemonstrasikan bagaimana berbagai konfigurasi SabreLayout memengaruhi kualitas transpilasi dan fidelitas eksekusi. Menggunakan qiskit_aer dengan noise model yang diturunkan dari data kalibrasi perangkat keras nyata memungkinkan kamu menguji transpilasi tanpa mengonsumsi kredit perangkat keras.
Langkah 1: Petakan input klasik ke masalah kuantum
Kita membangun sirkuit GHZ topologi bintang dengan 15 qubit. Qubit pertama berperan sebagai hub, dengan gate CNOT yang menghubungkannya langsung ke setiap qubit lain. Topologi ini menciptakan masalah layout yang menantang karena tidak terpetakan secara mudah ke coupling map perangkat.
Kita juga mendefinisikan operator ZZ untuk mengukur korelasi jeratan di setiap pasangan qubit.

SABRE adalah algoritma serbaguna dan tidak membuat asumsi apa pun tentang struktur sirkuit. Untuk sirkuit GHZ topologi bintang ini, sebenarnya sudah diketahui routing yang optimal: pass StarPreRouting mendeteksi sub-sirkuit bintang dan menulis ulang menjadi rantai linear yang dapat dipetakan langsung ke Backend mana pun yang memiliki jalur linear cukup panjang. Tutorial ini berfokus pada SABRE karena bekerja untuk sirkuit arbitrer, tetapi jika kamu tahu sirkuitmu memiliki struktur khusus yang jelas, menerapkan pass khusus seperti StarPreRouting sebelum routing dapat mengungguli pencarian heuristik apa pun.
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]
Langkah 2: Optimalkan masalah untuk eksekusi perangkat keras kuantum
Pass manager preset optimization_level=3 default sudah menggunakan SabreLayout, tetapi dengan pengaturan konservatif. Untuk mengeksplorasi dampak pengaturan yang lebih kuat, pass tersebut diganti dengan SabreLayout kustom yang dikonfigurasi untuk pencarian yang lebih agresif, sementara setiap pass lain di tahap layout dibiarkan tidak berubah. Sebagai titik perbandingan keempat, sebuah pass manager keempat mempertahankan SabreLayout default tetapi menambahkan StarPreRouting ke tahap init. StarPreRouting adalah pass yang sadar struktur yang mendeteksi sub-sirkuit bintang dan menulis ulang menjadi rantai linear sebelum routing.
Alur kerjanya adalah:
- Periksa pass manager default untuk melihat di mana
SabreLayoutberada di dalam tahaplayout. - Ganti pass tersebut dengan instance
SabreLayoutkustom menggunakanPassManager.replace(index, passes=...), dan buat varianpm_stardenganpm.init += StarPreRouting(). - Jalankan keempat pass manager dan bandingkan metriknya.
Keempat konfigurasi tersebut adalah:
| Konfigurasi | Deskripsi |
|---|---|
pm_1 (default) | Preset level-3 default (SabreLayout dengan max_iterations=4, layout_trials=20, swap_trials=20) |
pm_2 | SabreLayout kustom (max_iterations=4, layout_trials=200, swap_trials=200) |
pm_3 | SabreLayout kustom (max_iterations=8, layout_trials=200, swap_trials=200) |
pm_star | Preset default dengan StarPreRouting ditambahkan ke tahap init |
Parameter SABRE utama:
layout_trials/swap_trials: Mengontrol berapa banyak kandidat layout dan solusi routing yang dieksplorasi SABRE. Meningkatkan jumlah trials berarti SABRE menjelajahi ruang pencarian yang lebih luas, meningkatkan peluang menemukan solusi yang lebih baik.max_iterations: Mengontrol berapa banyak siklus penyempurnaan routing maju-mundur yang dilakukan SABRE pada setiap kandidat. SABRE secara iteratif meningkatkan layout dengan belajar dari umpan balik routing, sehingga semakin banyak iterasi, semakin baik peningkatannya.
Keduanya datang dengan biaya waktu transpilasi yang lebih lama, tetapi sirkuit yang dihasilkan lebih pendek dan menggunakan lebih sedikit gate, yang secara langsung mengurangi dekoherensi dan kesalahan gate pada perangkat keras nyata.
Langkah 2a: Periksa pass manager default. Sebuah StagedPassManager terdiri dari tahap-tahap (init, layout, routing, translation, optimization, scheduling), masing-masing merupakan PassManager sendiri. Memanggil .draw() pada sebuah tahap merender pass-nya sebagai grafik sehingga kita bisa melihat di mana SabreLayout berada.
# 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()

Dalam diagram di atas, pass SabreLayout yang ingin kita sesuaikan berada di dalam ConditionalController pada posisi [2] dari tahap layout. Controller tersebut melakukan dua hal:
- Menggerbangkan
SabreLayoutsehingga hanya berjalan ketikaVF2Layoutpada [1] gagal menemukan pemetaan sempurna (jika tidak, layout VF2 yang sempurna dipertahankan). - Mendahului
SabreLayoutdengan passBarrierBeforeFinalMeasurementsyang melindungi pengukuran agar tidak diurutkan ulang selama routing internal SabreLayout.
Jika kita hanya replace(index=2, passes=sl_2), kedua perilaku itu akan hilang. Untuk mempertahankannya, kita membungkus kembali SabreLayout kustom kita dalam ConditionalController yang sama (dengan kondisi dan barrier pelindung yang sama) sebelum menggantinya.
Langkah 2b: Buat pass SabreLayout kustom dan ganti yang default.
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()

Posisi [2] kini kembali berupa ConditionalController — identik dalam bentuk dengan yang default, tetapi SabreLayout di dalamnya adalah yang kustom kita (dengan layout_trials=200, swap_trials=200, dan max_iterations=8 untuk pm_3; pm_2 identik kecuali max_iterations=4). Barrier pelindung dan gating _vf2_match_not_found tetap terjaga, sehingga satu-satunya perbedaan antara pm_2/pm_3 dan pm_1 adalah konfigurasi SABRE itu sendiri. pm_star mempertahankan SabreLayout default dan hanya menambahkan StarPreRouting di akhir tahap init.
Langkah 2c: Jalankan setiap pass manager dan bandingkan.
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%
Ketiga pass manager yang dimodifikasi menghasilkan sirkuit dengan kedalaman 2Q yang lebih rendah dibanding yang default. Konfigurasi SABRE agresif (pm_2 dan pm_3) menukar waktu transpilasi yang lebih lama untuk pencarian yang lebih luas, sementara pm_star memanfaatkan struktur bintang sirkuit dan menghasilkan hasil yang lebih dangkal tanpa biaya transpilasi ekstra apa pun. Perolehan yang tepat akan bervariasi dari satu run ke run lainnya, tetapi tren umum konsisten: lebih banyak trials dan iterasi SABRE memungkinkan pencarian heuristik di ruang yang lebih luas, dan pass sadar struktur seperti StarPreRouting dapat melewati pencarian itu sepenuhnya ketika bentuk sirkuit cocok.
Bahkan pada skala kecil ini (15 qubit), ruang untuk peningkatan cukup sehingga ketiga pendekatan mengalahkan yang default. Dengan sirkuit yang lebih besar (100+ qubit), ruang pencarian tumbuh secara dramatis dan manfaat dari peningkatan trials dan pass sadar struktur menjadi jauh lebih menonjol, seperti yang akan ditunjukkan di bagian skala besar.
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()

Langkah 3: Eksekusi menggunakan primitif Qiskit
Kita menjalankan setiap Circuit yang ditranspilasi 10 kali menggunakan Aer EstimatorV2 dengan noise model yang diturunkan dari Backend nyata. Karena hasil simulasi berisik bervariasi antar run, rata-rata dari banyak run memberikan estimasi fidelitas yang lebih andal dan memungkinkan kita mengkuantifikasi ketidakpastian statistik dengan error bar.
# 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
Karena ini adalah sirkuit kecil, nilai fidelitas relatif dekat di semua empat konfigurasi. Sirkuitnya cukup pendek sehingga noise perangkat keras tidak memberikan penalti berat bahkan pada versi yang paling tidak dioptimalkan. Rata-rata fidelitas secara umum mengikuti kedalaman 2Q: pm_3 dan pm_star, dua sirkuit paling dangkal, mencapai fidelitas tertinggi dan pada dasarnya setara dalam batas error bar mereka. pm_2 adalah contoh yang berguna: meskipun kedalaman 2Q-nya lebih rendah dari pm_1, rata-rata fidelitasnya justru sedikit lebih rendah juga, yang mengingatkan kita bahwa hubungan kedalaman-ke-fidelitas bersifat statistik, bukan deterministik. Qubit fisik tertentu yang dipilih oleh layout dan kalibrasi qubit-qubit tersebut pada saat runtime juga berpengaruh.
Langkah 4: Pasca-proses dan kembalikan hasil dalam format klasik yang diinginkan
Selanjutnya, plot korelasi jeratan sebagai fungsi jarak qubit, bersama dengan korelasi rata-rata sebagai metrik fidelitas tunggal. Dalam kasus ideal (tanpa noise), semua korelasi seharusnya bernilai 1. Dengan noise nyata, setiap gate tambahan memperkenalkan kesalahan dan setiap langkah waktu tambahan memungkinkan dekoherensi, sehingga Circuit yang ditranspilasi dengan kedalaman lebih rendah dan lebih sedikit gate (terutama gate dua-qubit) seharusnya mempertahankan jeratan lebih baik.
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()

Hasilnya menunjukkan hubungan yang jelas antara kualitas transpilasi dan fidelitas eksekusi, dengan beberapa catatan penting:
pm_1(default): Baseline. Dengan hanya 20 trials dan empat iterasi, SABRE memiliki ruang terbatas untuk mengoptimalkan, menghasilkan sirkuit SABRE-only yang paling dalam.pm_2(lebih banyak trials): Mengeksplorasi sepuluh kali lebih banyak kandidat menemukan layout yang sedikit lebih dangkal, tetapi rata-rata fidelitas hampir sama (dan bahkan bisa turun di bawah baseline dalam noise) karena keuntungan kedalaman kecil pada skala ini.pm_3(lebih banyak trials + lebih banyak iterasi): Menggandakanmax_iterationsmenjadi 8 memberi SABRE lebih banyak siklus penyempurnaan, menghasilkan sirkuit SABRE-only paling dangkal dan rata-rata fidelitas tertinggi dalam perbandingan.pm_star(default + StarPreRouting): MenambahkanStarPreRoutingke tahap init dari preset default yang sebaliknya sama. Penulisan ulang sadar struktur ini menciutkan bintang menjadi rantai linear yang dipetakan oleh sisa transpiler ke jalur linear perangkat, menghasilkan sirkuit paling dangkal secara keseluruhan (sedikit lebih baik daripm_3) dan menyamaipm_3pada fidelitas dalam batas error bar. Ini dilakukan dengan waktu transpilasi yang sama seperti default, karena penulisan ulang ini pada dasarnya gratis dibandingkan pencarian stokastik SABRE.
Perlu diperhatikan bahwa meningkatkan max_iterations tidak selalu memberikan dampak positif. Dalam kasus ini membantu secara signifikan, tetapi untuk sirkuit atau Backend lain, iterasi tambahan mungkin tidak menghasilkan peningkatan lebih lanjut, atau bahkan sedikit menurunkan performa karena over-optimisasi terhadap minimum lokal. Secara umum, kamu harus meningkatkan layout_trials dan swap_trials sebanyak mungkin sesuai anggaran waktu yang dimiliki, karena lebih banyak trials selalu meningkatkan peluang menemukan layout yang lebih baik. Meningkatkan max_iterations layak dicoba tetapi harus divalidasi untuk kasus penggunaan spesifikmu. Pass khusus seperti StarPreRouting serupa semangatnya tetapi lebih bergantung pada sirkuit: mereka hanya membantu ketika sirkuit benar-benar mengandung struktur yang menjadi targetnya. Perolehannya besar ketika berlaku dan nol sebaliknya, tetapi biayanya hampir nol untuk dicoba.
Contoh perangkat keras skala besar
Selain menyesuaikan jumlah trials, SABRE mendukung kustomisasi heuristik routing. SABRE menawarkan tiga heuristik:
basic: Pendekatan greedy sederhana yang memilih swap yang meminimalkan jarak langsung ke gate berikutnya.decay(default): Memberikan bobot dinamis pada qubit berdasarkan aktivitas terkini, mencegah swap berulang pada qubit yang sama.lookahead: Mengevaluasi biaya routing masa depan dengan melihat ke depan pada gate yang akan datang, berpotensi menemukan urutan swap yang lebih baik.
Untuk menggunakan heuristik kustom, buat pass SabreSwap dan hubungkan ke SabreLayout melalui parameter routing_pass.
Pass manager keempat ditambahkan ke perbandingan: pm_star_hw, yang mempertahankan pengaturan SabreLayout/SabreSwap default tetapi menambahkan StarPreRouting ke tahap init. Pada skala ini (100 qubit) pencarian SABRE lebih sulit, dan penulisan ulang dari bintang menjadi rantai linear menjadi kemenangan yang jelas karena prosesor Heron memiliki jalur linear yang cukup panjang untuk menampung sirkuit yang dihasilkan.
Di sini kita membandingkan ketiga heuristik SABRE ditambah StarPreRouting pada skala besar pada sirkuit GHZ 100 qubit. Kita menjalankan beberapa layout trials dengan seed berbeda untuk konfigurasi SABRE, memilih sirkuit terbaik yang ditranspilasi dari masing-masing, dan mengirimkan semuanya ke perangkat keras nyata bersama hasil StarPreRouting.
Langkah 1-4 diringkas dalam satu blok kode
Di sini alur kerja lengkap digabungkan pada skala yang lebih besar. Saat menggunakan SabreSwap sebagai routing_pass untuk SabreLayout, hanya satu layout trial yang dilakukan per panggilan, sehingga kode berikut melakukan loop melalui seed untuk mengeksplorasi ruang layout.
Kita menggunakan helper wrap_sabre yang sama yang didefinisikan di Langkah 2 skala kecil (di atas), dan menambahkan helper wrap_routing yang analog karena tahap routing pada indeks [1] juga merupakan ConditionalController([BarrierBeforeFinalMeasurements, routing_pass], ...) — menggantinya secara langsung juga akan menghilangkan barrier pelindung dan gating _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
Analisis
Scatter plot menunjukkan variabilitas yang signifikan di seluruh seed untuk ketiga heuristik SABRE, yang menekankan pentingnya menjalankan beberapa layout trials daripada mengandalkan satu transpilasi saja. Garis StarPreRouting pada dasarnya datar di seluruh seed karena penulisan ulang dari bintang menjadi rantai linear bersifat deterministik setelah strukturnya ada; routing SABRE downstream kemudian memiliki sangat sedikit kebebasan pada rantai linear, sehingga seed hampir tidak berpengaruh pada kedalaman atau ukuran akhir.
Dari hasil transpilasi, heuristik decay dan lookahead secara konsisten mengungguli basic dengan selisih yang besar. Heuristik basic, meskipun cepat, menggunakan strategi greedy sederhana yang sering menghasilkan sirkuit yang jauh lebih dalam. Untuk sirkuit GHZ topologi bintang ini, lookahead cenderung menghasilkan kedalaman 2Q dan jumlah gate terendah di antara heuristik SABRE, karena fungsi biayanya yang berorientasi ke depan cocok untuk sirkuit dengan pola konektivitas jarak jauh. StarPreRouting, bagaimanapun, jauh melampaui ketiganya dengan selisih yang substansial: dengan menulis ulang bintang menjadi rantai linear sebelum routing, ia melewati masalah pencarian sepenuhnya dan menghasilkan sirkuit yang dapat dipetakan oleh sisa transpiler ke jalur linear dengan SWAP tambahan yang minimal.
Keunggulan itu langsung terlihat dalam fidelitas perangkat keras. Kedalaman 2Q dan jumlah gate yang lebih rendah tidak selalu diterjemahkan satu-per-satu ke fidelitas yang lebih tinggi (qubit fisik tertentu yang digunakan layout dan kalibrasinya pada saat runtime juga berpengaruh), tetapi ketika celah kedalaman sebesar celah antara SABRE dan StarPreRouting di sini, pendekatan sadar struktur menang secara tegas karena sirkuit mengakumulasi jauh lebih sedikit dekoherensi dan jauh lebih sedikit kejadian kesalahan dua-qubit. Diagram batang fidelitas menunjukkan StarPreRouting jauh di depan bahkan heuristik SABRE terbaik, sementara basic duduk jauh di bawah yang lain karena sirkuitnya yang jauh lebih dalam mengakumulasi kesalahan paling banyak.
Poin-poin utama:
- Di antara heuristik SABRE,
decaydanlookaheadjauh lebih baik daripadabasicuntuk sirkuit yang non-trivial. Lebih baik gunakan salah satu dari keduanya untuk beban kerja produksi. - Heuristik SABRE terbaik bergantung pada sirkuit dan perangkat keras kamu. Menguji beberapa heuristik dengan beberapa seed adalah strategi yang paling andal.
- Jika kamu ingin mengeksplorasi lebih banyak layout, tingkatkan
swap_trials(danlayout_trialsketika kamu tidak menetapkan routing pass kustom) daripada mendistribusikan pekerjaan ke node remote. Pass SABRE sudah memparalelkan trials di thread lokal, dan pekerjaan per trial cukup kecil sehingga overhead distribusi biasanya mendominasi kecepatan apa pun. - Ketika sirkuit memiliki struktur khusus yang diketahui, menerapkan pass sadar struktur seperti
StarPreRoutingsebelum SABRE dapat memberikan peningkatan satu orde besaran yang tidak akan tertandingi oleh penyetelan SABRE sebanyak apa pun. Ini bukan pengganti SABRE:StarPreRoutinghanya membantu ketika sirkuit benar-benar mengandung sub-sirkuit bintang dan Backend memiliki jalur linear yang cukup panjang. Layak untuk memeriksa pustaka pass untuk kecocokan kapan pun kamu mengetahui bentuk sirkuitmu.
Langkah berikutnya
Jika kamu merasa tertarik dengan materi ini, mungkin kamu juga tertarik dengan materi berikut:
- Referensi API
SabreLayout: dokumentasi parameter lengkap - Makalah SABRE: algoritma SABRE asli untuk layout dan routing
- Makalah LightSABRE: peningkatan algoritmik yang mendukung implementasi SABRE Qiskit saat ini
- Tulis transpiler pass kustom: buat logika transpilasi sendiri
- Plugin Transpiler: perluas pipeline transpilasi Qiskit dengan pass pihak ketiga
- Representasi DAG: pahami directed acyclic graph yang digunakan secara internal oleh transpiler
Survei tutorial
Tolong isi survei singkat ini untuk memberikan umpan balik tentang tutorial ini. Masukan kamu akan membantu kami meningkatkan konten dan pengalaman pengguna.
Catatan: Survei ini dikelola oleh IBM Quantum dan mencakup konten tutorial (ditulis oleh IBM). doQumentation menyediakan website, terjemahan, dan eksekusi kode — untuk umpan balik tentang hal tersebut, silakan buka isu GitHub.