Lumaktaw sa pangunahing nilalaman

Pag-optimize ng transpilation gamit ang SABRE

Tinatayang paggamit: 1 minuto sa isang Heron r2 processor (PAUNAWA: Ito ay isang pagtatantya lamang. Maaaring mag-iba ang iyong runtime.)

Mga layunin sa pagkatuto

Pagkatapos dumaan sa tutorial na ito, dapat mong maunawaan ang:

  • Paano i-configure ang mga parameter ng SABRE (layout_trials, swap_trials, max_iterations) upang mapabuti ang kalidad ng transpilation
  • Ang mga trade-off sa pagitan ng runtime ng transpilation at kalidad ng circuit (lalim at bilang ng gate)
  • Paano i-customize ang heuristic ng routing ng SABRE (basic, decay, lookahead) at ihambing ang kanilang performance sa hardware

Mga Kinakailangan

Iminumungkahi namin na pamilyar ka sa mga sumusunod na paksa bago dumaan sa tutorial na ito:

Pinagmulan

Kino-convert ng transpilation ang mga quantum circuit sa mga anyo na katugma sa partikular na quantum hardware. Dalawang pangunahing yugto ang pagpili ng qubit layout (pagmamapa ng mga lohikal na qubit sa mga pisikal na qubit) at gate routing (pagpasok ng mga SWAP gate upang ang mga multi-qubit gate ay sumunod sa koneksyon ng device).

SABRE (SWAP-Based Bidirectional heuristic search algorithm) ay nag-o-optimize ng parehong layout at routing. Ito ay espesyal na epektibo para sa mga malakihang circuit (100+ qubit) sa mga device na may kumplikadong coupling map, tulad ng mga IBM® Heron processor. Pinipigilan ng SABRE ang mga SWAP gate at binabawasan ang lalim ng circuit, na nagpapabuti ng fidelity ng pagpapatupad. Ang mga kamakailang pagpapabuti sa algoritmo ng LightSABRE ay higit pang nagpapababa ng mga runtime at bilang ng gate.

Sa tutorial na ito, una kang mag-co-configure ng SabreLayout gamit ang iba't ibang parameter upang ma-optimize ang isang maliit na GHZ circuit at obserbahan ang epekto sa fidelity ng pagpapatupad. Pagkatapos, ihahambing mo ang mga heuristic ng routing ng SABRE sa malakihang antas sa tunay na hardware.

Mga Pangangailangan

Bago simulan ang tutorial na ito, siguraduhing naka-install ang mga sumusunod:

  • Qiskit SDK v2.0 o mas bago, na may suporta sa visualization
  • Qiskit Runtime v0.22 o mas bago (pip install qiskit-ibm-runtime)
  • Qiskit Aer (pip install qiskit-aer)

Setup

# 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

Halimbawa sa maliit na antas gamit ang simulator

Sa seksyon na ito, ginagamit ang isang maingay na simulator batay sa noise model ng tunay na backend upang ipakita kung paano nakakaapekto ang iba't ibang configuration ng SabreLayout sa parehong kalidad ng transpilation at fidelity ng pagpapatupad. Ang paggamit ng qiskit_aer na may noise model na nagmula sa aktwal na calibration data ng hardware ay nagbibigay-daan sa iyo na subukan ang transpilation nang hindi gumagamit ng mga hardware credit.

Hakbang 1: I-map ang mga classical input sa isang quantum problem

Nagtatayo kami ng star-topology GHZ circuit na may 15 qubit. Ang unang qubit ang hub, na may mga CNOT gate na direktang nakakonekta dito sa bawat ibang qubit. Ang topology na ito ay lumilikha ng mapaghamong problema sa layout dahil hindi ito direktang namamapa sa coupling map ng device.

Nagde-define din kami ng mga ZZ operator upang sukatin ang mga correlation ng entanglement na Z0Zi\langle Z_0 Z_i \rangle sa mga pares ng qubit.

ghz_star_topology.png

Kapag alam mo ang istraktura ng circuit

Ang SABRE ay isang pangkalahatang-layunin na algoritmo at walang mga pagpapalagay tungkol sa istraktura ng circuit. Para sa star-topology GHZ circuit na ito, kilala talaga ang isang optimal na routing: ang StarPreRouting pass ay nakaka-detect ng mga star sub-circuit at nire-rewrite ang mga ito sa isang linear na chain na direktang namamapa sa anumang backend na may sapat na mahabang linear na landas. Nakatutok ang tutorial na ito sa SABRE dahil gumagana ito para sa mga arbitrary na circuit, ngunit kung alam mong may malinaw na espesyal na istraktura ang iyong circuit, ang pag-apply ng isang espesyalisadong pass tulad ng StarPreRouting bago ang routing ay maaaring mas mahusay kaysa sa anumang heuristic na paghahanap.

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]

Hakbang 2: I-optimize ang problema para sa quantum hardware execution

Ang default na optimization_level=3 preset pass manager ay gumagamit na ng SabreLayout, ngunit may mga konserbatibong default. Upang tuklasin ang epekto ng mas matibay na mga setting, pinapalitan ang pass na iyon ng isang custom na SabreLayout na naka-configure para sa mas agresibong paghahanap, habang ang bawat ibang pass sa yugto ng layout ay naiwan nang hindi nabago. Bilang isang hiwalay na punto ng paghahambing, isang ikaapat na pass manager ang nagpapanatili ng default na SabreLayout ngunit nagdadagdag ng StarPreRouting sa init stage. Ang StarPreRouting ay isang structure-aware na pass na nakaka-detect ng mga star sub-circuit at nire-rewrite ang mga ito sa isang linear na chain bago ang routing.

Ang workflow ay:

  1. Suriin ang default na pass manager upang makita kung saan nakatayo ang SabreLayout sa loob ng yugto ng layout.
  2. Palitan ang pass na iyon ng isang custom na instance ng SabreLayout gamit ang PassManager.replace(index, passes=...), at buuin ang variant na pm_star gamit ang pm.init += StarPreRouting().
  3. Patakbuhin ang lahat ng apat na pass manager at ihambing ang mga sukatan.

Ang apat na configuration ay:

ConfigPaglalarawan
pm_1 (default)Default level-3 preset (SabreLayout na may max_iterations=4, layout_trials=20, swap_trials=20)
pm_2Custom na SabreLayout (max_iterations=4, layout_trials=200, swap_trials=200)
pm_3Custom na SabreLayout (max_iterations=8, layout_trials=200, swap_trials=200)
pm_starDefault preset na may StarPreRouting na idinagdag sa init stage

Mga pangunahing parameter ng SABRE:

  • layout_trials / swap_trials: Kinokontrol kung gaano karaming kandidatong layout at solusyon sa routing ang ine-explore ng SABRE. Ang pagdaragdag ng bilang ng mga trial ay nangangahulugang ang SABRE ay nag-sa-sample ng mas malawak na espasyo ng paghahanap, na nagpapataas ng pagkakataon na mahanap ang isang mas magandang solusyon.
  • max_iterations: Kinokontrol kung gaano karaming forward-backward routing refinement cycle ang isinasagawa ng SABRE sa bawat kandidato. Ang SABRE ay paulit-ulit na nagpapabuti ng layout sa pamamagitan ng pag-aaral mula sa feedback ng routing, kaya ang mas maraming iteration, mas magaling ang mga pagpapabuti.

Parehong may kasamang mas matagal na oras ng transpilation, ngunit ang mga nagresultang circuit ay mas maikli at gumagamit ng mas kaunting gate, na direktang nagpapababa ng decoherence at mga pagkakamali sa gate sa tunay na hardware.

Hakbang 2a: Suriin ang default na pass manager. Isang StagedPassManager ay binubuo ng mga yugto (init, layout, routing, translation, optimization, scheduling), bawat isa ay isang PassManager mismo. Ang pagtatawag ng .draw() sa isang yugto ay nagrederita ng mga pass nito bilang isang graph upang makita natin kung saan nakatayo ang 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 of the previous code cell

Sa diagram sa itaas, ang SabreLayout pass na nais naming i-customize ay nasa loob ng ConditionalController sa posisyon [2] ng yugto ng layout. Ang controller na iyon ay gumagawa ng dalawang bagay:

  • Inilalagay nito ang kondisyon sa SabreLayout upang ito ay tumakbo lamang kapag ang VF2Layout sa [1] ay nabigo na makahanap ng perpektong mapping (kung hindi, ang perpektong VF2 layout ay pinapanatili).
  • Nangunguna ito ng SabreLayout gamit ang isang BarrierBeforeFinalMeasurements pass na nagpoprotekta sa mga pagsukat mula sa pag-reorder sa panahon ng internal na routing ng SabreLayout.

Kung replace(index=2, passes=sl_2) lang ang gagawin, parehong kinakaltas ang mga gawi na iyon. Upang mapanatili ang mga ito, bini-wrap muli namin ang aming custom na SabreLayout sa parehong ConditionalController (na may parehong kondisyon at ang protektibong barrier) bago ito palitan.

Hakbang 2b: Buuin ang mga custom na SabreLayout pass at palitan ang 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()

Output of the previous code cell

Ang posisyon [2] ay isang ConditionalController na muli — magkaparehong anyo ng default, ngunit ang panloob na SabreLayout ay ang aming custom na isa (na may layout_trials=200, swap_trials=200, at max_iterations=8 para sa pm_3; ang pm_2 ay magkapareho maliban sa max_iterations=4). Ang protektibong barrier at ang _vf2_match_not_found na gating ay napanatili, kaya ang tanging pagkakaiba sa pagitan ng pm_2/pm_3 at pm_1 ay ang configuration ng SABRE mismo. Pinapanatili ng pm_star ang default na SabreLayout at nagdadagdag lamang ng StarPreRouting sa dulo ng init stage.

Hakbang 2c: Patakbuhin ang bawat pass manager at ihambing.

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%

Ang lahat ng tatlong binagong pass manager ay nagtuloy ng mga circuit na may mas mababang 2Q depth kaysa sa default. Ang mga agresibong SABRE configuration (pm_2 at pm_3) ay nagpalit ng mas matagal na oras ng transpilation para sa mas malawak na paghahanap, habang ang pm_star ay gumagamit ng star na istraktura ng circuit at nagbubunga ng mas mababaw na resulta nang hindi nagbabayad ng anumang karagdagang gastos sa transpilation. Ang mga eksaktong kita ay magbabago mula sa run hanggang sa run, ngunit ang pangkalahatang trend ay pare-pareho: ang mas maraming trial at iteration ng SABRE ay nagbibigay-daan sa heuristic na mag-search ng mas malawak na espasyo, at ang mga structure-aware na pass tulad ng StarPreRouting ay maaaring laktawan ang paghahanap na iyon nang buo kapag ang anyo ng circuit ay nagtutugma.

Kahit sa maliit na antas na ito (15 qubit), ang silid para sa pagpapabuti ay sapat na upang ang lahat ng tatlong pamamaraan ay mas mahusay kaysa sa default. Sa mga mas malalaking circuit (100+ qubit), ang espasyo ng paghahanap ay lumalaki nang malaki at ang mga benepisyo ng parehong pagtaas ng mga trial at mga structure-aware na pass ay nagiging mas kapansin-pansin, tulad ng ipapakita ng seksyon ng malakihang antas.

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 of the previous code cell

Hakbang 3: Isagawa gamit ang Qiskit primitives

Pinapatakbo namin ang bawat transpiled circuit nang 10 beses gamit ang Aer EstimatorV2 na may noise model na nagmula sa tunay na backend. Dahil nagbabago ang mga resulta ng maingay na simulation sa pagitan ng mga run, ang pag-average sa maraming run ay nagbibigay ng mas maaasahang pagtatantya ng fidelity at nagbibigay-daan sa amin na quantify ang statistical na kawalan ng katiyakan gamit ang mga 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

Dahil ito ay isang maliit na circuit, ang mga halaga ng fidelity ay medyo malapit sa isa't isa sa lahat ng apat na configuration. Ang mga circuit ay sapat na maikli upang ang ingay ng hardware ay hindi masyadong pinaparusahan kahit ang pinaka-hindi na-optimize na bersyon. Ang mean fidelity ay malawakang sumusubaybay sa 2Q depth: ang pm_3 at pm_star, ang dalawang pinakamababaw na circuit, ay nakakamit ng pinakamataas na fidelity at mahalagang magkasabay sa loob ng kanilang mga error bar. Ang pm_2 ay isang kapaki-pakinabang na counter-example: kahit ang 2Q depth nito ay mas mababa kaysa sa pm_1, ang mean fidelity nito ay medyo mas mababa rin, na isang paalala na ang link ng depth-sa-fidelity ay statistical kaysa sa deterministic. Ang mga partikular na qubit na pinili ng isang layout at ang calibration ng mga qubit na iyon sa oras ng run ay mahalaga rin.

Hakbang 4: Post-process at ibalik ang resulta sa nais na classical na format

Susunod, i-plot ang mga correlation ng entanglement na Z0Zi\langle Z_0 Z_i \rangle bilang function ng distansya ng qubit, kasama ang mean correlation bilang isang solong sukatan ng fidelity. Sa isang ideal (walang ingay) na kaso, ang lahat ng correlation ay magiging 1. Sa realistikong ingay, ang bawat karagdagang gate ay nagpapakilala ng pagkakamali at ang bawat karagdagang time step ay nagpapahintulot ng decoherence, kaya ang isang transpiled circuit na may mas mababang lalim at mas kaunting gate (lalo na ang mga two-qubit gate) ay dapat mas mahusay na mapanatili ang entanglement.

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 of the previous code cell

Ang mga resulta ay nagpapakita ng malinaw na koneksyon sa pagitan ng kalidad ng transpilation at fidelity ng pagpapatupad, na may ilang kapaki-pakinabang na paalala:

  • pm_1 (default): Baseline. Sa 20 trial at apat na iteration lamang, ang SABRE ay may limitadong silid para sa pag-optimize, na nagbubunga ng pinakamalalim sa mga SABRE-only na circuit.
  • pm_2 (mas maraming trial): Ang pag-explore ng sampung beses na mas maraming kandidato ay nakakahanap ng bahagyang mas mababaw na layout, ngunit ang mean fidelity ay humigit-kumulang patag (at maaaring pababa ng baseline sa loob ng ingay) dahil maliit ang kita sa lalim sa antas na ito.
  • pm_3 (mas maraming trial + mas maraming iteration): Ang pagdoble ng max_iterations sa 8 ay nagbibigay sa SABRE ng mas maraming refinement cycle, na nagbubunga ng pinakamababaw na SABRE-only na circuit at pinakamataas na mean fidelity sa paghahambing.
  • pm_star (default + StarPreRouting): Nagdadagdag ng StarPreRouting sa init stage ng isang kung hindi man ay default na preset. Ang structure-aware na rewrite ay nagtatali ng star sa isang linear na chain na inimamapa ng natitirang transpiler sa linear na landas ng device, na nagbubunga ng pinakamababaw na circuit nang buo (bahagyang mas mahusay kaysa sa pm_3) at nagtutugma sa pm_3 sa fidelity sa loob ng mga error bar. Ginagawa nito ito gamit ang parehong oras ng transpilation ng default, dahil ang rewrite ay mahalagang libre kumpara sa stochastic na paghahanap ng SABRE.

Tandaan na ang pagdaragdag ng max_iterations ay hindi palaging may positibong epekto. Sa kasong ito ito ay malaki ang naitulong, ngunit para sa iba pang mga circuit o backend ang mga karagdagang iteration ay maaaring hindi na magbunga ng karagdagang pagpapabuti, o maaaring bahagyang makasama pa sa performance dahil sa over-optimization ng isang local minimum. Sa pangkalahatan, dapat mong palakasin ang layout_trials at swap_trials nang kasing dami ng nagbibigay-daan ang iyong time budget, dahil ang mas maraming trial ay palaging nagpapataas ng pagkakataon na makahanap ng mas magandang layout. Ang pagdaragdag ng max_iterations ay sulit na subukan ngunit dapat mapatunayan para sa iyong partikular na use case. Ang mga espesyalisadong pass tulad ng StarPreRouting ay magkaparehong espiritu ngunit mas circuit-dependent: tumutulong lamang ang mga ito kapag ang circuit ay talagang naglalaman ng istraktura na tina-target nila. Ang kita ay malaki kapag angkop at zero kung hindi, ngunit mahalagang wala silang gastos na subukan.

Halimbawa ng malakihang antas sa hardware

Bukod sa pag-aayos ng bilang ng trial, sinusuportahan ng SABRE ang pag-customize ng routing heuristic. Nag-aalok ang SABRE ng tatlong heuristic:

  • basic: Isang simpleng greedy na pamamaraan na pumipili ng swap na nagpapaliit ng agarang distansya sa susunod na gate.
  • decay (default): Dinamikong nagbibigay-timbang sa mga qubit batay sa kamakailang aktibidad, na hindi hinihikayat ang paulit-ulit na mga swap sa parehong mga qubit.
  • lookahead: Sinusuri ang mga gastos sa routing sa hinaharap sa pamamagitan ng pagtingin sa mga paparating na gate, na posibleng nakakahanap ng mas mahusay na mga sequence ng swap.

Upang gumamit ng custom na heuristic, lumikha ng SabreSwap pass at ikonekta ito sa SabreLayout sa pamamagitan ng parameter na routing_pass.

Isang ikaapat na pass manager ang idinaragdag sa paghahambing: ang pm_star_hw, na nagpapanatili ng mga default na setting ng SabreLayout/SabreSwap ngunit nagdadagdag ng StarPreRouting sa init stage. Sa antas na ito (100 qubit) ang paghahanap ng SABRE ay mas mahirap, at ang rewrite mula sa isang star sa isang linear na chain ay nagiging malinaw na panalo dahil ang isang Heron processor ay may mga linear na landas na sapat na mahaba upang i-host ang nagresultang circuit.

Dito namin inihahambing ang lahat ng tatlong heuristic ng SABRE kasama ang StarPreRouting sa malakihang antas sa isang 100-qubit GHZ circuit. Nagpapatakbo kami ng maraming layout trial na may iba't ibang seed para sa mga SABRE configuration, pinipili ang pinakamahusay na transpiled circuit mula sa bawat isa, at isinusumite ang lahat ng ito sa tunay na hardware kasama ang resulta ng StarPreRouting.

Mga Hakbang 1-4 na naka-compress sa isang solong code block

Dito, ang buong workflow ay pinagsama sa mas malaking antas. Kapag gumagamit ng SabreSwap bilang routing_pass para sa SabreLayout, isang layout trial lamang ang ginagawa bawat tawag, kaya ang sumusunod na code cell ay naglo-loop sa mga seed upang i-explore ang espasyo ng layout.

Ginagamit namin ang parehong wrap_sabre helper na tinukoy sa maliit na antas na Hakbang 2 (sa itaas), at nagdadagdag ng katulad na wrap_routing helper dahil ang yugto ng routing sa index [1] ay isang ConditionalController([BarrierBeforeFinalMeasurements, routing_pass], ...) rin — ang pagpapalit nito nang hayagan ay katulad na magtatanggal ng protektibong barrier at ang _swap_condition gating.

# -------------------------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']})"
)

Output of the previous code cell

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

Output of the previous code cell

Mean fidelity:
basic: 0.0344
decay: 0.1298
lookahead: 0.1857
StarPreRouting: 0.3295

Pagsusuri

Ang mga scatter plot ay nagpapakita ng makabuluhang pagkakaiba-iba sa mga seed para sa lahat ng tatlong SABRE heuristic, na nagbibigay-diin sa kahalagahan ng pagpapatakbo ng maraming layout trial kaysa sa pag-asa sa isang solong transpilation. Ang linya ng StarPreRouting ay mahalagang patag sa mga seed dahil ang rewrite mula sa isang star sa isang linear na chain ay deterministic na may ibinigay na istraktura; ang downstream na SABRE routing ay nagtataglay ng napakaliit na kalayaan sa isang linear na chain, kaya ang seed ay halos walang epekto sa huling lalim o laki.

Mula sa mga resulta ng transpilation, parehong ang decay at lookahead na heuristic ay palaging mas mahusay kaysa sa basic ng malaking margin. Ang basic na heuristic, kahit mabilis, ay gumagamit ng isang simpleng greedy na estratehiya na kadalasang nagbubunga ng mas malalim na circuit. Para sa star-topology GHZ circuit na ito, ang lookahead ay may posibilidad na magbunga ng pinakamababang 2Q depth at bilang ng gate sa mga SABRE heuristic, dahil ang cost function na nakatuon sa hinaharap nito ay naaangkop sa mga circuit na may mga pattern ng koneksyon na malayo. Ang StarPreRouting, gayunpaman, ay nagtatalo sa lahat ng tatlong ng malaking margin: sa pamamagitan ng pag-rewrite ng star sa isang linear na chain bago ang routing, nini-short-circuit nito ang problema ng paghahanap nang buo at naghahatid ng isang circuit na maimamapa ng natitirang transpiler sa isang linear na landas na may kaunting karagdagang SWAP.

Ang kalamangan na iyon ay direktang napupunta sa fidelity ng hardware. Ang mas mababang 2Q depth at bilang ng gate ay hindi palaging nagsasalin ng isa-sa-isa sa mas mataas na fidelity (ang mga partikular na pisikal na qubit na ginagamit ng isang layout at ang kanilang calibration sa oras ng run ay mahalaga rin), ngunit kapag ang agwat ng lalim ay kasingmalaki ng nasa pagitan ng SABRE at StarPreRouting dito, ang structure-aware na pamamaraan ay mapagpasyang nananalunan dahil ang circuit ay nag-iipon ng mas kaunting decoherence at mas kaunting two-qubit na mga kaganapan ng pagkakamali. Ang fidelity bar chart ay nagpapakita ng StarPreRouting na malayo sa harapan kahit ng pinakamahusay na SABRE heuristic, habang ang basic ay nananatili sa ilalim ng natitirang bahagi dahil ang mas malalim na mga circuit nito ay nag-iipon ng pinakamaraming pagkakamali.

Mga pangunahing konklusyon:

  • Sa mga SABRE heuristic, ang decay at lookahead ay mas mahusay kaysa sa basic para sa mga non-trivial na circuit. Mas gusto ang isa sa dalawa para sa mga production workload.
  • Ang pinakamahusay na SABRE heuristic ay depende sa iyong circuit at hardware. Ang pagsubok ng maraming heuristic na may maraming seed ang pinaka-maaasahang estratehiya.
  • Kung nais mong mag-explore ng mas maraming layout, palakasin ang swap_trials (at layout_trials kapag hindi ka nagpi-pin ng custom na routing pass) kaysa sa pag-fan out ng trabaho sa mga remote node. Ang mga SABRE pass ay nagpa-parallelize na ng mga trial sa mga lokal na thread, at ang bawat-trial na trabaho ay maliit sapat na ang overhead ng pamamahagi ay karaniwang mas mataas kaysa sa anumang speedup.
  • Kapag ang circuit ay may kilalang espesyal na istraktura, ang pag-apply ng isang structure-aware na pass tulad ng StarPreRouting bago ang SABRE ay maaaring maghatid ng order-of-magnitude na pagpapabuti na walang dami ng pag-tune ng SABRE ang matutugma. Hindi ito kapalit ng SABRE: ang StarPreRouting ay tumutulong lamang kapag ang circuit ay talagang naglalaman ng mga star sub-circuit at ang backend ay may sapat na mahabang linear na landas. Sulit itong suriin sa aklatan ng pass para sa mga tugma tuwing alam mo ang anyo ng iyong circuit.

Mga susunod na hakbang

Kung natagpuan mong kawili-wili ang gawaing ito, maaari kang maging interesado sa mga sumusunod na materyal:

Mga Rekomendasyon

Survey ng tutorial

Mangyaring sagutan ang maikling survey na ito upang magbigay ng feedback sa tutorial na ito. Ang inyong mga pananaw ay makakatulong sa amin na mapabuti ang aming mga alok ng nilalaman at karanasan ng gumagamit.

Paunawa: Ang survey na ito ay mula sa IBM Quantum at sumasaklaw sa nilalaman ng tutorial (na sinulat ng IBM). Nagbibigay ang doQumentation ng website, mga pagsasalin, at pagpapatupad ng code — para sa feedback sa mga iyon, mangyaring mag-open ng GitHub issue.

Link to survey