Optimalizace transpilace pomocí SABRE
Odhadovaná doba využití: 1 minuta na procesoru Heron r2 (POZNÁMKA: Jedná se pouze o odhad. Skutečná doba běhu se může lišit.)
Výsledky učení
Po absolvování tohoto tutoriálu bys měl/a rozumět:
- Jak konfigurovat parametry SABRE (
layout_trials,swap_trials,max_iterations) pro zlepšení kvality transpilace - Kompromisům mezi dobou transpilace a kvalitou obvodu (hloubka a počet hradel)
- Jak přizpůsobit heuristiku směrování SABRE (
basic,decay,lookahead) a porovnat jejich výkon na hardwaru
Předpoklady
Doporučujeme, abys před absolvováním tohoto tutoriálu znal/a následující témata:
- Transpilace obvodů: přehled transpilace v Qiskit
- Fáze transpilátoru: fáze rozmístění a směrování
- Konfigurace přednastavených správců průchodů: přizpůsobení úrovní optimalizace
Pozadí
Transpilace převádí kvantové obvody do forem kompatibilních s konkrétním kvantovým hardwarem. Dvěma klíčovými fázemi jsou volba rozmístění qubitů (mapování logických qubitů na fyzické qubity) a směrování hradel (vkládání SWAP hradel tak, aby vícequbitová hradla respektovala konektivitu zařízení).
SABRE (SWAP-Based Bidirectional heuristic search algorithm) optimalizuje obě fáze — rozmístění i směrování. Je zvláště účinný pro rozsáhlé obvody (100+ qubitů) na zařízeních se složitými coupling mapami, jako jsou IBM® Heron procesory. SABRE minimalizuje SWAP hradla a snižuje hloubku obvodu, čímž zlepšuje věrnost spuštění. Nedávná vylepšení v algoritmu LightSABRE dále snižují doby běhu a počty hradel.
V tomto tutoriálu nejprve nakonfiguruješ SabreLayout s různými parametry pro optimalizaci malého GHZ obvodu a pozorování dopadu na věrnost spuštění. Poté porovnáš heuristiky směrování SABRE ve velkém měřítku na reálném hardwaru.
Požadavky
Před zahájením tohoto tutoriálu se ujisti, že máš nainstalováno:
- Qiskit SDK v2.0 nebo novější, s podporou vizualizace
- Qiskit Runtime v0.22 nebo novější (
pip install qiskit-ibm-runtime) - Qiskit Aer (
pip install qiskit-aer)
Nastavení
# 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
Příklad v malém měřítku se simulátorem
V této části je použit hlučný simulátor založený na šumovém modelu reálného backendu, aby demonstroval, jak různé konfigurace SabreLayout ovlivňují jak kvalitu transpilace, tak věrnost spuštění. Použití qiskit_aer se šumovým modelem odvozeným z reálných kalibračních dat hardwaru ti umožňuje testovat transpilaci bez spotřebování kreditů hardwaru.
Krok 1: Mapování klasických vstupů na kvantový problém
Zkonstruujeme GHZ obvod s hvězdicovou topologií s 15 qubity. První qubit je uzel, přičemž CNOT hradla ho propojují přímo s každým dalším qubitem. Tato topologie vytváří náročný problém rozmístění, protože se netriviálně mapuje na coupling mapu zařízení.
Také definujeme operátory ZZ pro měření korelací provázání napříč páry qubitů.

SABRE je algoritmus pro obecné použití a nepředpokládá žádnou strukturu obvodu. Pro tento GHZ obvod s hvězdicovou topologií je ve skutečnosti známo optimální směrování: průchod StarPreRouting detekuje hvězdicové podobvody a přepisuje je do lineárního řetězu, který se přímo mapuje na jakýkoli backend s dostatečně dlouhou lineární cestou. Tento tutoriál se zaměřuje na SABRE, protože funguje pro libovolné obvody, ale pokud víš, že tvůj obvod má jasnou speciální strukturu, aplikace specializovaného průchodu jako StarPreRouting před směrováním může překonat jakékoli heuristické prohledávání.
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]
Krok 2: Optimalizace problému pro spuštění na kvantovém hardwaru
Výchozí přednastavený správce průchodů s optimization_level=3 již používá SabreLayout, ale s konzervativními výchozími hodnotami. Pro zkoumání dopadu silnějšího nastavení je tento průchod nahrazen vlastním SabreLayout nakonfigurovaným pro agresivnější prohledávání, přičemž všechny ostatní průchody ve fázi rozmístění zůstávají nezměněny. Jako samostatný bod porovnání čtvrtý správce průchodů zachovává výchozí SabreLayout, ale přidává StarPreRouting do inicializační fáze. StarPreRouting je strukturálně uvědomělý průchod, který detekuje hvězdicové podobvody a přepisuje je do lineárního řetězu před směrováním.
Postup je:
- Prozkoumat výchozí správce průchodů, aby bylo vidět, kde
SabreLayoutsídlí uvnitř fázelayout. - Nahradit tento průchod vlastní instancí
SabreLayoutpomocíPassManager.replace(index, passes=...)a sestavit variantupm_starpomocípm.init += StarPreRouting(). - Spustit všechny čtyři správce průchodů a porovnat metriky.
Čtyři konfigurace jsou:
| Konfigurace | Popis |
|---|---|
pm_1 (výchozí) | Výchozí úroveň-3 přednastavení (SabreLayout s max_iterations=4, layout_trials=20, swap_trials=20) |
pm_2 | Vlastní SabreLayout (max_iterations=4, layout_trials=200, swap_trials=200) |
pm_3 | Vlastní SabreLayout (max_iterations=8, layout_trials=200, swap_trials=200) |
pm_star | Výchozí přednastavení s StarPreRouting přidaným do inicializační fáze |
Klíčové parametry SABRE:
layout_trials/swap_trials: Řídí, kolik kandidátních rozmístění a směrovacích řešení SABRE prozkoumá. Zvýšení počtu pokusů znamená, že SABRE vzorkuje širší prostor prohledávání, čímž zvyšuje šanci na nalezení lepšího řešení.max_iterations: Řídí, kolik cyklů zdokonalování směrování vpřed-vzad SABRE provede pro každého kandidáta. SABRE iterativně zlepšuje rozmístění tím, že se učí ze zpětné vazby směrování, takže čím více iterací, tím lepší zlepšení.
Obojí přichází za cenu delší doby transpilace, ale výsledné obvody jsou kratší a používají méně hradel, což přímo snižuje dekoherenci a chyby hradel na reálném hardwaru.
Krok 2a: Prozkoumat výchozí správce průchodů. StagedPassManager se skládá z fází (init, layout, routing, translation, optimization, scheduling), přičemž každá je sama PassManager. Volání .draw() na fázi vykreslí její průchody jako graf, takže vidíme, kde SabreLayout sídlí.
# 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()

Ve výše uvedeném diagramu průchod SabreLayout, který chceme přizpůsobit, sídlí uvnitř ConditionalController na pozici [2] fáze rozmístění. Tento kontroler dělá dvě věci:
- Podmíní
SabreLayouttak, aby běžel pouze tehdy, kdyžVF2Layoutna [1] nenašel perfektní mapování (jinak je zachováno perfektní rozmístění VF2). - Předchází
SabreLayoutprůchodemBarrierBeforeFinalMeasurements, který chrání měření před přeuspořádáním během interního směrování SabreLayout.
Kdybychom pouze provedli replace(index=2, passes=sl_2), obě chování by se ztratila. Abychom je zachovali, znovu zabalíme náš vlastní SabreLayout do stejného ConditionalController (se stejnou podmínkou a ochrannou bariérou) před jeho výměnou.
Krok 2b: Sestavit vlastní průchody SabreLayout a nahradit výchozí.
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()

Pozice [2] je nyní opět ConditionalController — identický tvarově s výchozím, ale vnitřní SabreLayout je naše vlastní (s layout_trials=200, swap_trials=200 a max_iterations=8 pro pm_3; pm_2 je identický kromě max_iterations=4). Ochranná bariéra a podmínění _vf2_match_not_found jsou zachovány, takže jediným rozdílem mezi pm_2/pm_3 a pm_1 je samotná konfigurace SABRE. pm_star zachovává výchozí SabreLayout a pouze přidává StarPreRouting na konec inicializační fáze.
Krok 2c: Spustit každý správce průchodů a porovnat.
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%
Všechny tři upravené správce průchodů produkovaly obvody s nižší 2Q hloubkou než výchozí. Agresivní konfigurace SABRE (pm_2 a pm_3) obchodují delší dobu transpilace za širší prohledávání, zatímco pm_star využívá hvězdicovou strukturu obvodu a produkuje ještě mělčí výsledek bez jakýchkoli dodatečných nákladů na transpilaci. Přesné zisky se budou v jednotlivých bězích lišit, ale obecný trend je konzistentní: více pokusů a iterací SABRE umožňuje heuristice prohledat širší prostor a strukturálně uvědomělé průchody jako StarPreRouting mohou toto prohledávání zcela obejít, když tvar obvodu odpovídá.
Dokonce i v tomto malém měřítku (15 qubitů) je prostor pro zlepšení dostatečně velký, aby všechny tři přístupy překonaly výchozí nastavení. U větších obvodů (100+ qubitů) prostor prohledávání dramaticky roste a výhody jak zvýšených pokusů, tak strukturálně uvědomělých průchodů se stávají mnohem výraznějšími, jak ukáže sekce o velkém měřítku.
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()

Krok 3: Spuštění pomocí primitiv Qiskit
Každý transpilovaný obvod spustíme 10krát pomocí Aer EstimatorV2 se šumovým modelem odvozeným z reálného backendu. Protože výsledky hlučné simulace se mezi běhy liší, průměrování přes více běhů dává spolehlivější odhady věrnosti a umožňuje nám kvantifikovat statistickou nejistotu pomocí chybových úseček.
# 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
Protože se jedná o malý obvod, hodnoty věrnosti jsou ve všech čtyřech konfiguracích relativně blízko. Obvody jsou dostatečně krátké, takže hardwarový šum výrazně nepenalizuje ani nejméně optimalizovanou verzi. Průměrná věrnost obecně sleduje 2Q hloubku: pm_3 a pm_star, dva nejmělčí obvody, dosahují nejvyšší věrnosti a jsou v podstatě na stejné úrovni v rámci svých chybových úseček. pm_2 je užitečným protipříkladem: přestože jeho 2Q hloubka je nižší než pm_1, jeho průměrná věrnost je mírně nižší, což připomíná, že spojení hloubka-věrnost je statistické, nikoli deterministické. Záleží také na konkrétních qubitech, které rozmístění vybere, a na kalibraci těchto qubitů v době běhu.
Krok 4: Následné zpracování a vrácení výsledku v požadovaném klasickém formátu
Dále vykresli korelace provázání jako funkci vzdálenosti qubitů spolu s průměrnou korelací jako jedinou metrikou věrnosti. V ideálním (bezhlučném) případě by všechny korelace byly 1. S realistickým šumem každé další hradlo zavádí chybu a každý další časový krok umožňuje dekoherenci, takže transpilovaný obvod s nižší hloubkou a méně hradly (zejména dvouqubitovými) by měl lépe zachovávat provázání.
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()

Výsledky ukazují jasnou souvislost mezi kvalitou transpilace a věrností spuštění, s několika užitečnými upozorněními:
pm_1(výchozí): Základní linie. S pouhými 20 pokusy a čtyřmi iteracemi má SABRE omezený prostor pro optimalizaci, výsledkem jsou nejhlubší obvody mezi SABRE-only obvody.pm_2(více pokusů): Prozkoumání desetkrát více kandidátů nalezne mírně mělčí rozmístění, ale průměrná věrnost je přibližně stejná (a může dokonce klesnout pod základní linii v rámci šumu), protože zisk hloubky je v tomto měřítku malý.pm_3(více pokusů + více iterací): Zdvojnásobenímax_iterationsna 8 dává SABRE více cyklů zdokonalování, produkcí nejmělčího SABRE-only obvodu a nejvyšší průměrné věrnosti v porovnání.pm_star(výchozí + StarPreRouting): PřidáváStarPreRoutingdo inicializační fáze jinak výchozího přednastavení. Strukturálně uvědomělý přepis zredukuje hvězdu do lineárního řetězu, který zbytek transpilátoru mapuje na lineární cestu zařízení, čímž produkuje celkově nejmělčí obvod (mírně lepší nežpm_3) a shoduje se spm_3na věrnosti v rámci chybových úseček. Dosahuje toho se stejnou dobou transpilace jako výchozí, protože přepis je v podstatě zdarma ve srovnání se stochastickým prohledáváním SABRE.
Všimni si, že zvyšování max_iterations nemá vždy pozitivní dopad. V tomto případě výrazně pomohlo, ale pro jiné obvody nebo backendy nemusí další iterace přinést další zlepšení, nebo mohou dokonce mírně snížit výkon kvůli nadměrné optimalizaci lokálního minima. Obecně bys měl/a zvyšovat layout_trials a swap_trials co nejvíce, jak dovoluje tvůj časový rozpočet, protože více pokusů vždy zvyšuje šanci na nalezení lepšího rozmístění. Zvyšování max_iterations stojí za otestování, ale mělo by být validováno pro tvůj konkrétní případ použití. Specializované průchody jako StarPreRouting jsou podobné duchem, ale více závislé na obvodu: pomáhají pouze tehdy, když obvod skutečně obsahuje strukturu, na kterou cílí. Zisk je velký, když je to aplikovatelné, a jinak nulový, ale v podstatě nestojí nic je vyzkoušet.
Příklad ve velkém měřítku na hardwaru
Kromě úpravy počtu pokusů SABRE podporuje přizpůsobení heuristiky směrování. SABRE nabízí tři heuristiky:
basic: Jednoduchý hladový přístup, který vybírá swap minimalizující okamžitou vzdálenost k dalšímu hradlu.decay(výchozí): Dynamicky váží qubity na základě nedávné aktivity, odrazuje od opakovaných swapů na stejných qubitech.lookahead: Vyhodnocuje budoucí náklady na směrování pohledem dopředu na nadcházející hradla, potenciálně nalézaje lepší sekvence swapů.
Chceš-li použít vlastní heuristiku, vytvoř průchod SabreSwap a připoj ho k SabreLayout prostřednictvím parametru routing_pass.
Čtvrtý správce průchodů je přidán do porovnání: pm_star_hw, který zachovává výchozí nastavení SabreLayout/SabreSwap, ale přidává StarPreRouting do inicializační fáze. V tomto měřítku (100 qubitů) je prohledávání SABRE obtížnější a přepis z hvězdy do lineárního řetězu se stává jasnou výhrou, protože Heron procesor má dostatečně dlouhé lineární cesty pro hostování výsledného obvodu.
Zde porovnáme všechny tři heuristiky SABRE plus StarPreRouting ve velkém měřítku na 100-qubitovém GHZ obvodu. Spustíme více pokusů o rozmístění s různými seedy pro konfigurace SABRE, vybereme nejlepší transpilovaný obvod z každého a všechny je předložíme reálnému hardwaru spolu s výsledkem StarPreRouting.
Kroky 1-4 zkomprimované do jednoho bloku kódu
Zde je celý postup sestavený ve větším měřítku. Při použití SabreSwap jako routing_pass pro SabreLayout se na jedno volání provede pouze jeden pokus o rozmístění, takže následující buňka kódu iteruje přes seedy pro prozkoumání prostoru rozmístění.
Používáme stejný pomocník wrap_sabre definovaný v malém měřítku v Kroku 2 (výše) a přidáváme analogický pomocník wrap_routing, protože fáze routing na indexu [1] je také ConditionalController([BarrierBeforeFinalMeasurements, routing_pass], ...) — jeho přímé nahrazení by podobně zrušilo ochrannou bariéru a podmínění _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
Analýza
Bodové grafy ukazují výraznou variabilitu napříč seedy pro všechny tři heuristiky SABRE, což zdůrazňuje důležitost spouštění více pokusů o rozmístění namísto spoléhání se na jedinou transpilaci. Linie StarPreRouting je v podstatě plochá napříč seedy, protože přepis z hvězdy do lineárního řetězu je deterministický vzhledem ke struktuře; následné směrování SABRE má pak na lineárním řetězu velmi malou volnost, takže seed má téměř žádný vliv na konečnou hloubku nebo velikost.
Z výsledků transpilace heuristiky decay i lookahead konzistentně výrazně překonávají basic. Heuristika basic, ačkoli je rychlá, používá jednoduchou hladovou strategii, která často vede k podstatně hlubším obvodům. Pro tento GHZ obvod s hvězdicovou topologií má lookahead tendenci produkovat nejnižší 2Q hloubku a počet hradel mezi heuristikami SABRE, protože jeho nákladová funkce hledící dopředu je vhodná pro obvody s konektivitou na dlouhé vzdálenosti. StarPreRouting však všechny tři výrazně překoná: přepsáním hvězdy do lineárního řetězu před směrováním zcela obchází vyhledávací problém a doručuje obvod, který zbytek transpilátoru dokáže namapovat na lineární cestu s minimálním počtem dalších SWAP.
Tato výhoda se přímo přenáší na věrnost hardwaru. Nižší 2Q hloubka a počet hradel se ne vždy přímo promítají do vyšší věrnosti (záleží také na konkrétních fyzických qubitech, které rozmístění použije, a na jejich kalibraci v době běhu), ale když je hloubkový rozdíl tak velký jako mezi SABRE a StarPreRouting zde, strukturálně uvědomělý přístup vítězí rozhodně, protože obvod akumuluje mnohem méně dekoherence a mnohem méně dvouqubitových chybových událostí. Graf věrnosti ukazuje StarPreRouting výrazně před i nejlepší heuristikou SABRE, zatímco basic leží výrazně pod ostatními, protože jeho mnohem hlubší obvody akumulují nejvíce chyb.
Klíčové závěry:
- Mezi heuristikami SABRE jsou
decayalookaheadvýrazně lepší nežbasicpro netriviální obvody. Pro produkční pracovní zátěže preferuj jednu ze dvou. - Nejlepší heuristika SABRE závisí na tvém obvodu a hardwaru. Testování více heuristik s více seedy je nejspolehlivější strategií.
- Pokud chceš prozkoumat ještě více rozmístění, zvyš
swap_trials(alayout_trials, když nepřipisuješ vlastní průchod směrování) namísto rozdávání práce na vzdálené uzly. Průchody SABRE již paralelizují pokusy přes lokální vlákna a práce na jeden pokus je dostatečně malá, že distribuční režie obvykle dominuje jakémukoli zrychlení. - Když má obvod známou speciální strukturu, aplikace strukturálně uvědomělého průchodu jako
StarPreRoutingpřed SABRE může přinést zlepšení o řád, které žádné množství ladění SABRE nebude schopno dosáhnout. Toto není náhrada za SABRE:StarPreRoutingpomáhá pouze tehdy, když obvod skutečně obsahuje hvězdicové podobvody a backend má dostatečně dlouhou lineární cestu. Stojí za to zkontrolovat knihovnu průchodů pro shody, kdykoli znáš tvar svého obvodu.
Další kroky
Pokud tě tato práce zaujala, možná tě budou zajímat následující materiály:
- Referenční dokumentace API
SabreLayout: úplná dokumentace parametrů - Článek SABRE: původní algoritmus SABRE pro rozmístění a směrování
- Článek LightSABRE: algoritmická vylepšení, která pohání aktuální implementaci SABRE v Qiskit
- Napsat vlastní průchod transpilátoru: sestavit vlastní logiku transpilace
- Pluginy transpilátoru: rozšíření transpilačního kanálu Qiskit o průchody třetích stran
- Reprezentace DAG: pochopení orientovaného acyklického grafu používaného interně transpilátorem
Průzkum tutoriálu
Prosím, vyplň tento krátký průzkum a poskytni nám zpětnou vazbu k tomuto tutoriálu. Tvoje poznatky nám pomohou zlepšit naši nabídku obsahu a uživatelský zážitek.
Poznámka: Tento průzkum je od IBM Quantum a týká se obsahu tutoriálu (napsaného IBM). doQumentation poskytuje webové stránky, překlady a spouštění kódu — pro zpětnou vazbu k těmto položkám prosím otevři GitHub issue.