Оптимізація транспіляції за допомогою SABRE
Приблизна оцінка часу виконання: 1 хвилина на процесорі Heron r2 (ПРИМІТКА: Це лише оцінка. Твій час виконання може відрізнятися.)
Результати навчання
Після проходження цього посібника ти повинен(-на) розуміти:
- Як налаштовувати параметри SABRE (
layout_trials,swap_trials,max_iterations) для покращення якості транспіляції - Компроміси між часом виконання транспіляції та якістю схеми (глибиною та кількістю вентилів)
- Як налаштовувати евристику маршрутизації SABRE (
basic,decay,lookahead) і порівнювати їхню продуктивність на обладнанні
Передумови
Радимо ознайомитися з такими темами перед проходженням цього посібника:
- Транспіляція схем: огляд транспіляції в Qiskit
- Етапи транспілятора: етапи розташування та маршрутизації
- Налаштування попередньо встановлених менеджерів проходів: налаштування рівнів оптимізації
Передмова
Транспіляція перетворює квантові схеми у форми, сумісні з конкретним квантовим обладнанням. Два ключових етапи — вибір розташування кубітів (відображення логічних кубітів на фізичні кубіти) і маршрутизація вентилів (вставка вентилів SWAP, щоб багатокубітні вентилі відповідали зв'язності пристрою).
SABRE (двонаправлений евристичний алгоритм пошуку на основі SWAP) оптимізує як розташування, так і маршрутизацію. Він особливо ефективний для великомасштабних схем (понад 100 кубітів) на пристроях зі складними картами зв'язку, як-от процесори IBM® Heron. SABRE мінімізує вентилі SWAP і зменшує глибину схеми, покращуючи точність виконання. Останні покращення в алгоритмі LightSABRE додатково зменшують час виконання та кількість вентилів.
У цьому посібнику ти спочатку налаштуєш SabreLayout з різними параметрами для оптимізації невеликої схеми GHZ і спостерігатимеш вплив на точність виконання. Потім порівняєш евристики маршрутизації SABRE у великому масштабі на реальному обладнанні.
Вимоги
Перед початком цього посібника переконайся, що у тебе встановлено наступне:
- Qiskit SDK v2.0 або пізніше, з підтримкою візуалізації
- Qiskit Runtime v0.22 або пізніше (
pip install qiskit-ibm-runtime) - Qiskit Aer (
pip install qiskit-aer)
Налаштування
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import EstimatorOptions
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_aer.primitives import EstimatorV2 as AerEstimator
from qiskit.transpiler.passes import (
SabreLayout,
SabreSwap,
BarrierBeforeFinalMeasurements,
StarPreRouting,
)
from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.passmanager.flow_controllers import ConditionalController
import matplotlib.pyplot as plt
import numpy as np
import time
seed = 42
service = QiskitRuntimeService(
channel="ibm_cloud",
token="<YOUR_API_TOKEN>", # Replace with your actual API token
instance="<YOUR_INSTANCE_NAME>", # Replace with your instance name if needed
)
backend = service.least_busy(operational=True, simulator=False)
print(f"Using backend: {backend.name}")
Using backend: ibm_kingston
Приклад з невеликим симулятором
У цьому розділі використовується шумний симулятор на основі моделі шуму реального бекенду, щоб продемонструвати, як різні конфігурації SabreLayout впливають як на якість транспіляції, так і на точність виконання. Використання qiskit_aer з моделлю шуму, похідною від реальних калібрувальних даних обладнання, дозволяє тобі тестувати транспіляцію без витрати кредитів обладнання.
Крок 1: Відображення класичних входів на квантову задачу
Ми конструюємо схему GHZ із зірковою топологією з 15 кубітів. Перший кубіт є центром, а вентилі CNOT з'єднують його безпосередньо з кожним іншим кубітом. Ця топологія створює складну задачу розташування, оскільки вона не відображається тривіально на карту зв'язку пристрою.
Ми також визначаємо оператори ZZ для вимірювання кореляцій заплутування між парами кубітів.

SABRE — алгоритм загального призначення, який не робить жодних припущень щодо структури схеми. Для цієї схеми GHZ із зірковою топологією оптимальна маршрутизація насправді відома: прохід StarPreRouting виявляє зіркові під-схеми та переписує їх у лінійний ланцюг, який відображається безпосередньо на будь-який бекенд з достатньо довгим лінійним шляхом. Цей посібник зосереджується на SABRE, оскільки він працює для довільних схем, але якщо ти знаєш, що твоя схема має чітку спеціальну структуру, застосування спеціалізованого проходу на кшталт StarPreRouting перед маршрутизацією може перевершити будь-який евристичний пошук.
num_qubits_sim = 15
# Create star-topology GHZ circuit
qc_sim = QuantumCircuit(num_qubits_sim)
qc_sim.h(0)
for i in range(1, num_qubits_sim):
qc_sim.cx(0, i)
qc_sim.measure_all()
# ZZ operators: Z on qubit 0 and qubit i, identity elsewhere
operator_strings_sim = [
"Z" + "I" * i + "Z" + "I" * (num_qubits_sim - 2 - i)
for i in range(num_qubits_sim - 1)
]
operators_sim = [SparsePauliOp(op) for op in operator_strings_sim]
Крок 2: Оптимізація задачі для виконання на квантовому обладнанні
Стандартний менеджер проходів optimization_level=3 вже використовує SabreLayout, але з консервативними налаштуваннями за замовчуванням. Щоб дослідити вплив більш агресивних налаштувань, цей прохід замінюється на власний SabreLayout, налаштований для більш інтенсивного пошуку, тоді як усі інші проходи на етапі розташування залишаються незмінними. Як окрема точка порівняння, четвертий менеджер проходів зберігає стандартний SabreLayout, але додає StarPreRouting до етапу ініціалізації. StarPreRouting — це прохід зі знанням структури, який виявляє зіркові під-схеми та переписує їх у лінійний ланцюг перед маршрутизацією.
Робочий процес:
- Перевірка стандартного менеджера проходів, щоб побачити, де
SabreLayoutзнаходиться всередині етапуlayout. - Заміна цього проходу на власний екземпляр
SabreLayoutза допомогоюPassManager.replace(index, passes=...)і побудова варіантуpm_starза допомогоюpm.init += StarPreRouting(). - Запуск усіх чотирьох менеджерів проходів і порівняння метрик.
Чотири конфігурації:
| Конфігурація | Опис |
|---|---|
pm_1 (за замовчуванням) | Стандартний попередньо встановлений рівень 3 (SabreLayout з max_iterations=4, layout_trials=20, swap_trials=20) |
pm_2 | Власний SabreLayout (max_iterations=4, layout_trials=200, swap_trials=200) |
pm_3 | Власний SabreLayout (max_iterations=8, layout_trials=200, swap_trials=200) |
pm_star | Стандартний попередньо встановлений з StarPreRouting, доданим до етапу ініціалізації |
Ключові параметри SABRE:
layout_trials/swap_trials: Контролюють, скільки варіантів розташувань і рішень маршрутизації досліджує SABRE. Збільшення кількості спроб означає, що SABRE досліджує ширший простір пошуку, збільшуючи шанс знайти кращий розв'язок.max_iterations: Контролює, скільки циклів уточнення маршрутизації вперед-назад SABRE виконує для кожного кандидата. SABRE ітеративно покращує розташування, навчаючись зі зворотного зв'язку маршрутизації, тому більше ітерацій — кращі покращення.
Обидва параметри коштують більшого часу транспіляції, але отримані схеми є коротшими та використовують менше вентилів, що безпосередньо зменшує декогерентність і похибки вентилів на реальному обладнанні.
Крок 2а: Перевірка стандартного менеджера проходів. StagedPassManager складається з етапів (init, layout, routing, translation, optimization, scheduling), кожний з яких є PassManager. Виклик .draw() для етапу відображає його проходи у вигляді графу, щоб ми могли побачити, де знаходиться SabreLayout.
# Build the default pass manager (no modifications yet)
pm_1 = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=seed
)
# Visualize the layout stage to see where SabreLayout sits
pm_1.layout.draw()

На діаграмі вище прохід SabreLayout, який ми хочемо налаштувати, знаходиться всередині ConditionalController на позиції [2] етапу розташування. Цей контролер виконує дві речі:
- Він перешкоджає запуску
SabreLayout, якщоVF2Layoutна [1] не знайшов ідеального відображення (інакше зберігається ідеальне розташування VF2). - Він передує
SabreLayoutпроходомBarrierBeforeFinalMeasurements, який захищає вимірювання від переупорядкування під час внутрішньої маршрутизації SabreLayout.
Якщо ми просто виконаємо replace(index=2, passes=sl_2), обидві поведінки будуть видалені. Щоб їх зберегти, ми повторно загортаємо наш власний SabreLayout у той самий ConditionalController (з тією ж умовою та захисним бар'єром) перед заміною.
Крок 2б: Побудова власних проходів SabreLayout і заміна стандартного.
cmap = backend.coupling_map
# Custom SabreLayout passes with more aggressive search
sl_2 = SabreLayout(
coupling_map=cmap,
seed=seed,
max_iterations=4,
layout_trials=200,
swap_trials=200,
)
sl_3 = SabreLayout(
coupling_map=cmap,
seed=seed,
max_iterations=8,
layout_trials=200,
swap_trials=200,
)
# Same condition the preset uses: only run SabreLayout when VF2Layout did not
# find a perfect mapping. This preserves any perfect layout VF2 produced at [1].
def _vf2_match_not_found(property_set):
if property_set["layout"] is None:
return True
return (
property_set["VF2Layout_stop_reason"] is not None
and property_set["VF2Layout_stop_reason"]
is not VF2LayoutStopReason.SOLUTION_FOUND
)
def wrap_sabre(sabre_pass):
"""Re-wrap a SabreLayout in the original ConditionalController + barrier."""
return ConditionalController(
[
BarrierBeforeFinalMeasurements(
"qiskit.transpiler.internal.routing.protection.barrier"
),
sabre_pass,
],
condition=_vf2_match_not_found,
)
# Build two fresh pass managers and swap in the wrapped custom SabreLayout at index 2
pm_2 = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=seed
)
pm_3 = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=seed
)
pm_2.layout.replace(index=2, passes=wrap_sabre(sl_2))
pm_3.layout.replace(index=2, passes=wrap_sabre(sl_3))
# Build pm_star: default preset with StarPreRouting added to the init stage
pm_star = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=seed
)
pm_star.init += StarPreRouting()
# Visualize pm_3 after replacement (pm_2 has the same structure, only max_iterations differs)
pm_3.layout.draw()

Позиція [2] тепер знову є ConditionalController — ідентична за формою до стандартної, але внутрішній SabreLayout — це наш власний (з layout_trials=200, swap_trials=200 і max_iterations=8 для pm_3; pm_2 ідентичний окрім max_iterations=4). Захисний бар'єр і умова _vf2_match_not_found збережені, тому єдина різниця між pm_2/pm_3 і pm_1 — це конфігурація SABRE. pm_star зберігає стандартний SabreLayout і лише додає StarPreRouting у кінець етапу ініціалізації.
Крок 2в: Запуск кожного менеджера проходів і порівняння.
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%
Усі три модифіковані менеджери проходів виробили схеми з меншою глибиною 2Q, ніж стандартний. Агресивні конфігурації SABRE (pm_2 і pm_3) торгують більшим часом транспіляції на ширший пошук, тоді як pm_star використовує зіркову структуру схеми та отримує ще мілкіший результат без додаткових витрат на транспіляцію. Точні здобутки варіюватимуться від запуску до запуску, але загальна тенденція є незмінною: більше спроб і ітерацій SABRE дозволяють евристиці досліджувати ширший простір, а проходи зі знанням структури, на кшталт StarPreRouting, можуть повністю обійти цей пошук, коли форма схеми відповідає.
Навіть у цьому невеликому масштабі (15 кубітів), простір для покращення достатній, щоб усі три підходи перевершили стандартний. Зі збільшенням схем (понад 100 кубітів) простір пошуку різко зростає, і переваги як збільшення кількості спроб, так і проходів зі знанням структури стають набагато більш вираженими, як покаже розділ великого масштабу.
pm_names = list(results_sim.keys())
depths = [results_sim[n]["depth"] for n in pm_names]
sizes = [results_sim[n]["size"] for n in pm_names]
times = [results_sim[n]["time"] for n in pm_names]
colors = ["#404080", "#2a9d8f", "#a8d05e", "#e29bdd"]
x = np.arange(len(pm_names))
fig, axs = plt.subplots(1, 3, figsize=(14, 5))
# 2Q Depth
bars = axs[0].bar(x, depths, color=colors)
axs[0].set_ylabel("2Q Depth", fontsize=11)
axs[0].set_title("Two-Qubit Gate Depth", fontsize=13)
axs[0].set_ylim(0, max(depths) * 1.2)
for bar, val in zip(bars, depths):
axs[0].text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + max(depths) * 0.02,
str(val),
ha="center",
va="bottom",
fontsize=11,
fontweight="bold",
)
for i in range(1, len(depths)):
pct = (depths[0] - depths[i]) / depths[0] * 100
if pct != 0:
axs[0].text(
bars[i].get_x() + bars[i].get_width() / 2,
bars[i].get_height() / 2,
f"{pct:+.0f}%",
ha="center",
va="center",
fontsize=10,
color="white",
fontweight="bold",
)
# Size
bars = axs[1].bar(x, sizes, color=colors)
axs[1].set_ylabel("Gate Count", fontsize=11)
axs[1].set_title("Circuit Size", fontsize=13)
axs[1].set_ylim(0, max(sizes) * 1.2)
for bar, val in zip(bars, sizes):
axs[1].text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + max(sizes) * 0.02,
str(val),
ha="center",
va="bottom",
fontsize=11,
fontweight="bold",
)
for i in range(1, len(sizes)):
pct = (sizes[0] - sizes[i]) / sizes[0] * 100
if abs(pct) > 0.1:
axs[1].text(
bars[i].get_x() + bars[i].get_width() / 2,
bars[i].get_height() / 2,
f"{pct:+.0f}%",
ha="center",
va="center",
fontsize=10,
color="white",
fontweight="bold",
)
# Time
bars = axs[2].bar(x, times, color=colors)
axs[2].set_ylabel("Time (s)", fontsize=11)
axs[2].set_title("Transpilation Time", fontsize=13)
axs[2].set_ylim(0, max(times) * 1.3)
for bar, val in zip(bars, times):
axs[2].text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + max(times) * 0.03,
f"{val:.2f}s",
ha="center",
va="bottom",
fontsize=11,
fontweight="bold",
)
for ax in axs:
ax.set_xticks(x)
ax.set_xticklabels(pm_names, fontsize=8, rotation=15)
ax.grid(axis="y", linestyle="--", alpha=0.5)
plt.suptitle(
"Transpilation quality vs. configuration",
fontsize=14,
fontweight="bold",
y=1.02,
)
plt.tight_layout()
plt.show()

Крок 3: Виконання за допомогою примітивів Qiskit
Ми запускаємо кожну транспільовану схему 10 разів за допомогою Aer EstimatorV2 з моделлю шуму, похідною від реального бекенду. Оскільки результати шумної симуляції варіюються між запусками, усереднення по кількох запусках дає більш надійні оцінки точності та дозволяє нам кількісно оцінити статистичну невизначеність з допомогою планок похибок.
# 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
Оскільки це невелика схема, значення точності для всіх чотирьох конфігурацій знаходяться відносно близько. Схеми досить короткі, щоб апаратний шум суттєво не погіршував навіть найменш оптимізовану версію. Середня точність загалом корелює з глибиною 2Q: pm_3 і pm_star, дві наймілкіші схеми, досягають найвищої точності та фактично пов'язані в межах своїх планок похибок. pm_2 є корисним контрприкладом: незважаючи на меншу глибину 2Q, ніж у pm_1, його середня точність виявляється незначно нижчою, що нагадує нам, що зв'язок між глибиною і точністю є статистичним, а не детерміністичним. Конкретні кубіти, які вибирає розташування, та калібрування цих кубітів під час виконання також мають значення.
Крок 4: Постобробка та повернення результату у бажаному класичному форматі
Далі будуємо графік кореляцій заплутування як функцію відстані між кубітами, разом із середньою кореляцією як єдиною метрикою точності. В ідеальному (без шуму) випадку всі кореляції дорівнювали б 1. З реалістичним шумом кожен додатковий вентиль вносить помилку, а кожен додатковий часовий крок дозволяє декогерентності, тому транспільована схема з меншою глибиною та меншою кількістю вентилів (особливо двокубітних) повинна краще зберігати заплутування.
data_sim = list(range(1, len(operators_sim) + 1))
markers = ["o", "s", "^", "*"]
colors_line = ["#404080", "#2a9d8f", "#a8d05e", "#e29bdd"]
fig, (ax1, ax2) = plt.subplots(
1, 2, figsize=(14, 5), gridspec_kw={"width_ratios": [2.5, 1]}
)
# Left: correlations vs distance with error bars (mean +/- 1 std)
for (name, stats), marker, color in zip(
sim_stats.items(), markers, colors_line
):
ax1.errorbar(
data_sim,
stats["mean"],
yerr=stats["std"],
marker=marker,
label=name,
color=color,
linewidth=2,
capsize=3,
capthick=1,
elinewidth=1,
)
ax1.set_xlabel("Distance between qubits $i$", fontsize=11)
ax1.set_ylabel(r"$\langle Z_0 Z_i \rangle$", fontsize=11)
ax1.set_title(
"Entanglement correlations vs. qubit distance (avg. of 10 runs)",
fontsize=12,
)
ax1.legend(fontsize=9)
ax1.grid(alpha=0.3)
# Right: mean correlation bar chart with error bars
names = list(sim_stats.keys())
means = [sim_stats[n]["overall_mean"] for n in names]
stds = [sim_stats[n]["overall_std"] for n in names]
x_bar = np.arange(len(names))
bars = ax2.bar(
x_bar, means, yerr=stds, color=colors_line, capsize=5, ecolor="gray"
)
ax2.set_ylabel(r"Mean $\langle Z_0 Z_i \rangle$", fontsize=11)
ax2.set_title("Average fidelity", fontsize=13, pad=12)
y_range = max(means) - min(means) if max(means) != min(means) else 0.01
# Top of ylim accounts for the bar height + std error bar + headroom for the value label
y_top = max(m + s for m, s in zip(means, stds)) + y_range * 1.5
ax2.set_ylim(min(means) - y_range * 0.8, y_top)
for bar, val, std in zip(bars, means, stds):
ax2.text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + std + y_range * 0.15,
f"{val:.4f}",
ha="center",
va="bottom",
fontsize=10,
fontweight="bold",
)
# Annotate % change vs pm_1
baseline_mean = means[0]
for i in range(1, len(means)):
pct = (means[i] - baseline_mean) / baseline_mean * 100
if abs(pct) > 0.01:
mid_y = (means[i] + ax2.get_ylim()[0]) / 2
ax2.text(
bars[i].get_x() + bars[i].get_width() / 2,
mid_y,
f"{pct:+.1f}%",
ha="center",
va="center",
fontsize=10,
color="white",
fontweight="bold",
)
ax2.set_xticks(x_bar)
ax2.set_xticklabels(names, fontsize=8, rotation=15)
ax2.grid(axis="y", linestyle="--", alpha=0.5)
fig.tight_layout()
plt.show()

Результати показують чіткий зв'язок між якістю транспіляції та точністю виконання, з кількома корисними застереженнями:
pm_1(стандартний): Базовий рівень. Маючи лише 20 спроб і чотири ітерації, SABRE має обмежений простір для оптимізації, в результаті чого виходить найглибша серед схем лише на SABRE.pm_2(більше спроб): Дослідження вдесятеро більшої кількості кандидатів знаходить трохи мілкіше розташування, але середня точність приблизно однакова (і навіть може опуститися нижче базового рівня в межах шуму), оскільки виграш у глибині є невеликим у цьому масштабі.pm_3(більше спроб + більше ітерацій): Подвоєнняmax_iterationsдо 8 дає SABRE більше циклів уточнення, виробляючи найбількш мілку схему лише на SABRE та найвищу середню точність у порівнянні.pm_star(стандартний + StarPreRouting): ДодаєStarPreRoutingдо етапу ініціалізації інакше стандартного попередньо встановленого. Перезапис зі знанням структури згортає зірку в лінійний ланцюг, який решта транспілятора відображає на лінійний шлях пристрою, виробляючи найбількш мілку схему загалом (трохи краще, ніжpm_3) і відповідаючиpm_3за точністю в межах планок похибок. Він досягає цього з тим самим часом транспіляції, що й стандартний, оскільки перезапис є фактично безкоштовним порівняно з стохастичним пошуком SABRE.
Зверни увагу, що збільшення max_iterations не завжди має позитивний вплив. У цьому випадку це допомогло значно, але для інших схем або бекендів додаткові ітерації можуть не дати подальшого покращення або навіть злегка погіршити продуктивність через надмірну оптимізацію локального мінімуму. Загалом, ти повинен(-на) збільшувати layout_trials і swap_trials наскільки дозволяє твій часовий бюджет, оскільки більше спроб завжди збільшують шанс знайти краще розташування. Збільшення max_iterations варто тестувати, але слід перевіряти для конкретного випадку використання. Спеціалізовані проходи, на кшталт StarPreRouting, схожі за духом, але залежать від схеми: вони допомагають лише тоді, коли схема насправді містить структуру, яку вони виявляють. Виграш є великим, коли застосовується, і нульовим в іншому випадку, але вони коштують фактично нічого для спроби.
Приклад великого масштабу на обладнанні
Крім регулювання кількості спроб, SABRE підтримує налаштування евристики маршрутизації. SABRE пропонує три евристики:
basic: Простий жадібний підхід, що вибирає своп, що мінімізує безпосередню відстань до наступного вентилю.decay(за замовчуванням): Динамічно зважує кубіти на основі нещодавньої активності, не заохочуючи повторні свопи на одних і тих самих кубітах.lookahead: Оцінює майбутні витрати на маршрутизацію, заглядаючи вперед на наступні вентилі, потенційно знаходячи кращі послідовності свопів.
Щоб використовувати власну евристику, створи прохід SabreSwap і з'єднай його з SabreLayout через параметр routing_pass.
До порівняння додається четвертий менеджер проходів: pm_star_hw, який зберігає стандартні налаштування SabreLayout/SabreSwap, але додає StarPreRouting до етапу ініціалізації. У цьому масштабі (100 кубітів) пошук SABRE є складнішим, і перезапис із зірки в лінійний ланцюг стає очевидною перемогою, оскільки процесор Heron має лінійні шляхи достатньо довгі для отриманої схеми.
Тут ми порівнюємо всі три евристики SABRE плюс StarPreRouting у великому масштабі на схемі GHZ з 100 кубітів. Ми запускаємо кілька спроб розташування з різними зернами для конфігурацій SABRE, вибираємо найкращу транспільовану схему з кожної та надсилаємо їх усі на реальне обладнання поруч із результатом StarPreRouting.
Кроки 1-4 стиснуті в одному блоці коду
Тут повний робочий процес зібрано в більшому масштабі. При використанні SabreSwap як routing_pass для SabreLayout, за замовчуванням виконується лише одна спроба розташування, тому наступна комірка коду перебирає зерна для дослідження простору розташувань.
Ми використовуємо той самий допоміжний елемент wrap_sabre, визначений у кроці 2 невеликого масштабу (вище), і додаємо аналогічний допоміжний елемент wrap_routing, оскільки етап routing на індексі [1] також є ConditionalController([BarrierBeforeFinalMeasurements, routing_pass], ...) — заміна його безпосередньо аналогічно відкине захисний бар'єр і умову _swap_condition.
# -------------------------Step 1-------------------------
num_qubits = 100
# Create star-topology GHZ circuit
qc = QuantumCircuit(num_qubits)
qc.h(0)
for i in range(1, num_qubits):
qc.cx(0, i)
qc.measure_all()
# ZZ operators
operator_strings = [
"Z" + "I" * i + "Z" + "I" * (num_qubits - 2 - i)
for i in range(num_qubits - 1)
]
operators = [SparsePauliOp(op) for op in operator_strings]
# -------------------------Step 2-------------------------
num_seeds = 10
seed_list = [seed + i for i in range(num_seeds)]
swap_trials = 200
# The default routing[1] is a ConditionalController([barrier, routing_pass],
# condition=_swap_condition); we re-wrap so the new routing pass keeps the
# protective barrier and is skipped when routing isn't needed (matches the preset).
def _swap_condition(property_set):
return not property_set["routing_not_needed"]
def wrap_routing(routing_pass):
return ConditionalController(
[
BarrierBeforeFinalMeasurements(
"qiskit.transpiler.internal.routing.protection.barrier"
),
routing_pass,
],
condition=_swap_condition,
)
heuristic_results = {}
# Three SABRE heuristics, swept over seeds
for heuristic in ["basic", "decay", "lookahead"]:
trials = []
for s in seed_list:
sr = SabreSwap(
coupling_map=cmap, heuristic=heuristic, trials=swap_trials, seed=s
)
sl = SabreLayout(coupling_map=cmap, routing_pass=sr, seed=s)
pm = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=s
)
# Re-wrap each custom pass in its original ConditionalController + barrier
# (wrap_sabre is defined in the small-scale Step 2 cell above).
pm.layout.replace(index=2, passes=wrap_sabre(sl))
pm.routing.replace(index=1, passes=wrap_routing(sr))
t0 = time.time()
tqc = pm.run(qc)
elapsed = time.time() - t0
depth = tqc.depth(lambda x: x.operation.num_qubits == 2)
size = tqc.size()
trials.append(
{
"tqc": tqc,
"depth": depth,
"size": size,
"time": elapsed,
"seed": s,
}
)
heuristic_results[heuristic] = trials
# Default preset + StarPreRouting in init, also swept over seeds for a fair comparison
star_trials = []
for s in seed_list:
pm_star_hw = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=s
)
pm_star_hw.init += StarPreRouting()
t0 = time.time()
tqc = pm_star_hw.run(qc)
elapsed = time.time() - t0
depth = tqc.depth(lambda x: x.operation.num_qubits == 2)
size = tqc.size()
star_trials.append(
{
"tqc": tqc,
"depth": depth,
"size": size,
"time": elapsed,
"seed": s,
}
)
heuristic_results["StarPreRouting"] = star_trials
# Print summary for each entry
for label in ["basic", "decay", "lookahead", "StarPreRouting"]:
trials = heuristic_results[label]
depths = [t["depth"] for t in trials]
sizes = [t["size"] for t in trials]
best = min(trials, key=lambda t: t["depth"])
print(f"{label}:")
print(
f" 2Q depth: min: {min(depths)}, mean: {np.mean(depths):.1f}, std: {np.std(depths):.1f}"
)
print(
f" size : min: {min(sizes)}, mean: {np.mean(sizes):.1f}, std: {np.std(sizes):.1f}"
)
print(
f" best seed: {best['seed']} (2Q depth={best['depth']}, size={best['size']})"
)
basic:
2Q depth: min: 524, mean: 570.5, std: 39.9
size : min: 3819, mean: 4227.1, std: 360.6
best seed: 51 (2Q depth=524, size=3852)
decay:
2Q depth: min: 387, mean: 436.4, std: 41.7
size : min: 2687, mean: 3183.1, std: 459.3
best seed: 45 (2Q depth=387, size=2786)
lookahead:
2Q depth: min: 364, mean: 424.6, std: 36.5
size : min: 2335, mean: 3014.6, std: 388.1
best seed: 51 (2Q depth=364, size=2485)
StarPreRouting:
2Q depth: min: 196, mean: 196.0, std: 0.0
size : min: 1151, mean: 1151.0, std: 0.0
best seed: 42 (2Q depth=196, size=1151)
hw_colors = {
"basic": "#ff7f0e",
"decay": "#d62728",
"lookahead": "#1f77b4",
"StarPreRouting": "#2a9d8f",
}
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 5))
for label in ["basic", "decay", "lookahead", "StarPreRouting"]:
trials = heuristic_results[label]
depths = [t["depth"] for t in trials]
sizes = [t["size"] for t in trials]
seeds = [t["seed"] for t in trials]
color = hw_colors[label]
ax1.scatter(
seeds,
depths,
label=label,
color=color,
alpha=0.8,
edgecolor="k",
s=60,
)
ax1.axhline(np.mean(depths), color=color, linestyle="--", alpha=0.5)
ax2.scatter(
seeds,
sizes,
label=label,
color=color,
alpha=0.8,
edgecolor="k",
s=60,
)
ax2.axhline(np.mean(sizes), color=color, linestyle="--", alpha=0.5)
ax1.set_xlabel("Seed", fontsize=11)
ax1.set_ylabel("2Q Depth", fontsize=11)
ax1.set_title("Two-Qubit Gate Depth per Seed", fontsize=13)
ax1.legend(fontsize=10)
ax1.grid(alpha=0.3)
ax2.set_xlabel("Seed", fontsize=11)
ax2.set_ylabel("Gate Count", fontsize=11)
ax2.set_title("Circuit Size per Seed", fontsize=13)
ax2.legend(fontsize=10)
ax2.grid(alpha=0.3)
plt.suptitle(
"Transpilation variability across seeds: SABRE heuristics vs. StarPreRouting",
fontsize=14,
fontweight="bold",
y=1.02,
)
plt.tight_layout()
plt.show()
# Summary comparison
for label in ["basic", "decay", "lookahead", "StarPreRouting"]:
best = min(heuristic_results[label], key=lambda t: t["depth"])
print(
f"{label}: best 2Q depth={best['depth']}, size={best['size']} (seed={best['seed']})"
)

basic: best 2Q depth=524, size=3852 (seed=51)
decay: best 2Q depth=387, size=2786 (seed=45)
lookahead: best 2Q depth=364, size=2485 (seed=51)
StarPreRouting: best 2Q depth=196, size=1151 (seed=42)
# -------------------------Step 3: Execute on hardware-------------------------
best_circuits = {}
for label in ["basic", "decay", "lookahead", "StarPreRouting"]:
best_circuits[label] = min(
heuristic_results[label], key=lambda t: t["depth"]
)
b = best_circuits[label]
print(f"Best {label}: 2Q depth={b['depth']}, size={b['size']}")
options = EstimatorOptions()
options.resilience_level = 2
options.dynamical_decoupling.enable = True
options.dynamical_decoupling.sequence_type = "XY4"
estimator = Estimator(backend, options=options)
hw_jobs = {}
hw_ops = {}
for label, best in best_circuits.items():
hw_ops[label] = [op.apply_layout(best["tqc"].layout) for op in operators]
hw_jobs[label] = estimator.run([(best["tqc"], hw_ops[label])])
print(f"{label} job: {hw_jobs[label].job_id()}")
estimator.options.environment.job_tags = ["TUT_TOWS"]
hw_results = {}
for label, job in hw_jobs.items():
hw_results[label] = job.result()[0]
print(f"{label} job done")
Best basic: 2Q depth=524, size=3852
Best decay: 2Q depth=387, size=2786
Best lookahead: 2Q depth=364, size=2485
Best StarPreRouting: 2Q depth=196, size=1151
basic job: d81q5tnoha1c73bknprg
decay job: d81q5tugbeec73aktopg
lookahead job: d81q5to0bvlc73d1epe0
StarPreRouting job: d81q5u7tjchs73bn82hg
basic job done
decay job done
lookahead job done
StarPreRouting job done
# -------------------------Step 4: Post-process-------------------------
data = list(range(1, len(operators) + 1))
hw_markers = {
"basic": "D",
"decay": "o",
"lookahead": "s",
"StarPreRouting": "*",
}
hw_labels = ["basic", "decay", "lookahead", "StarPreRouting"]
fig, (ax1, ax2) = plt.subplots(
1, 2, figsize=(14, 5), gridspec_kw={"width_ratios": [2.5, 1]}
)
# Left: correlations vs distance
for label in hw_labels:
evs = list(hw_results[label].data.evs)
b = best_circuits[label]
ax1.plot(
data,
evs,
marker=hw_markers[label],
color=hw_colors[label],
linewidth=2,
label=f"{label} (2Q depth={b['depth']}, size={b['size']})",
markersize=5 if label == "StarPreRouting" else 4,
)
ax1.set_xlabel("Distance between qubits $i$", fontsize=11)
ax1.set_ylabel(r"$\langle Z_0 Z_i \rangle$", fontsize=11)
ax1.set_title(
"Entanglement correlations vs. qubit distance (hardware)", fontsize=12
)
ax1.legend(fontsize=9)
ax1.grid(alpha=0.3)
# Right: mean fidelity bar chart
hw_means = [np.mean(list(hw_results[label].data.evs)) for label in hw_labels]
hw_bar_colors = [hw_colors[label] for label in hw_labels]
x_bar = np.arange(len(hw_labels))
bars = ax2.bar(x_bar, hw_means, color=hw_bar_colors)
ax2.set_ylabel(r"Mean $\langle Z_0 Z_i \rangle$", fontsize=11)
ax2.set_title("Average fidelity", fontsize=13)
y_range = (
max(hw_means) - min(hw_means) if max(hw_means) != min(hw_means) else 0.01
)
ax2.set_ylim(min(hw_means) - y_range * 0.2, max(hw_means) + y_range * 0.15)
for bar, val in zip(bars, hw_means):
ax2.text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + y_range * 0.05,
f"{val:.4f}",
ha="center",
va="bottom",
fontsize=11,
fontweight="bold",
)
ax2.set_xticks(x_bar)
ax2.set_xticklabels(hw_labels, fontsize=9, rotation=15)
ax2.grid(axis="y", linestyle="--", alpha=0.5)
fig.tight_layout()
plt.show()
print("\nMean fidelity:")
for label, m in zip(hw_labels, hw_means):
print(f" {label}: {m:.4f}")

Mean fidelity:
basic: 0.0344
decay: 0.1298
lookahead: 0.1857
StarPreRouting: 0.3295
Аналіз
Графіки розсіювання показують значну мінливість між зернами для всіх трьох евристик SABRE, що підкреслює важливість запуску кількох спроб розташування, а не покладатися на одну транспіляцію. Лінія StarPreRouting є практично рівною між зернами, оскільки перезапис із зірки в лінійний ланцюг є детерміністичним з огляду на структуру; подальша маршрутизація SABRE тоді має дуже мало свободи на лінійному ланцюзі, тому зерно майже не впливає на кінцеву глибину або розмір.
З результатів транспіляції видно, що обидві евристики decay і lookahead стабільно перевершують basic з великим відривом. Евристика basic, хоча й швидка, використовує простий жадібний підхід, який часто призводить до суттєво глибших схем. Для цієї схеми GHZ із зірковою топологією lookahead зазвичай виробляє найменшу глибину 2Q та кількість вентилів серед евристик SABRE, оскільки її перспективна функція витрат добре підходить для схем з далекосяжними шаблонами зв'язку. Однак StarPreRouting значно перевершує всі три з суттєвим відривом: переписуючи зірку в лінійний ланцюг перед маршрутизацією, вона повністю обходить задачу пошуку та доставляє схему, яку решта транспілятора може відобразити на лінійний шлях з мінімальними додатковими свопами.
Ця перевага безпосередньо переходить у точність на обладнанні. Менша глибина 2Q та кількість вентилів не завжди перетворюються один-до-одного у вищу точність (конкретні фізичні кубіти, які вибирає розташування, та їхнє калібрування під час виконання також мають значення), але коли різниця в глибині є такою великою, як між SABRE та StarPreRouting тут, підхід зі знанням структури перемагає вирішально, оскільки схема накопичує набагато менше декогерентності та набагато менше подій помилок двокубітних вентилів. Стовпчаста діаграма точності показує, що StarPreRouting значно попереду навіть найкращої евристики SABRE, тоді як basic знаходиться значно нижче решти, оскільки її набагато глибші схеми накопичують найбільше помилок.
Ключові висновки:
- Серед евристик SABRE,
decayіlookaheadсуттєво кращі заbasicдля нетривіальних схем. Надавай перевагу одній із двох для виробничих навантажень. - Найкраща евристика SABRE залежить від твоєї схеми та обладнання. Тестування кількох евристик із кількома зернами — найнадійніша стратегія.
- Якщо ти хочеш дослідити ще більше розташувань, збільшуй
swap_trials(іlayout_trials, коли не закріплюєш власний прохід маршрутизації), а не розподіляй роботу на віддалені вузли. Проходи SABRE вже паралелізують спроби між локальними потоками, а робота для кожної спроби є достатньо невеликою, щоб накладні витрати на розподіл зазвичай переважали будь-яке прискорення. - Коли схема має відому спеціальну структуру, застосування проходу зі знанням структури, на кшталт
StarPreRouting, перед SABRE може забезпечити покращення на порядок, яке жодна кількість налаштувань SABRE не зможе відповідати. Це не заміна SABRE:StarPreRoutingдопомагає лише тоді, коли схема насправді містить зіркові під-схеми і бекенд має достатньо довгий лінійний шлях. Варто перевірити бібліотеку проходів на відповідність, коли ти знаєш форму своєї схеми.
Наступні кроки
Якщо тебе зацікавила ця робота, тебе можуть зацікавити такі матеріали:
- Довідник API
SabreLayout: повна документація параметрів - Стаття SABRE: оригінальний алгоритм SABRE для розташування та маршрутизації
- Стаття LightSABRE: алгоритмічні покращення, що забезпечують поточну реалізацію SABRE в Qiskit
- Написання власного проходу транспілятора: побудуй власну логіку транспіляції
- Плагіни транспілятора: розширюй конвеєр транспіляції Qiskit сторонніми проходами
- Представлення DAG: розумій направлений ациклічний граф, що використовується внутрішньо транспілятором
Опитування щодо посібника
Будь ласка, пройди це коротке опитування, щоб надати відгук про цей посібник. Твої думки допоможуть нам покращити наш контент і досвід користувача.
Примітка: Це опитування від IBM Quantum і стосується змісту посібника (написаного IBM). doQumentation надає вебсайт, переклади та виконання коду — для відгуків щодо них, будь ласка, відкрий запит на GitHub.