Sari la conținutul principal

Optimizarea transpilării cu SABRE

Estimare utilizare: 1 minut pe un procesor Heron r2 (NOTĂ: Aceasta este doar o estimare. Timpul tău de execuție poate varia.)

Rezultate de învățare

După parcurgerea acestui tutorial, ar trebui să înțelegi:

  • Cum să configurezi parametrii SABRE (layout_trials, swap_trials, max_iterations) pentru a îmbunătăți calitatea transpilării
  • Compromisurile dintre timpul de transpilare și calitatea circuitului (adâncime și număr de porți)
  • Cum să personalizezi euristica de rutare SABRE (basic, decay, lookahead) și să compari performanța lor pe hardware

Condiții prealabile

Îți sugerăm să fii familiarizat cu următoarele subiecte înainte de a parcurge acest tutorial:

Context

Transpilarea convertește circuitele cuantice în forme compatibile cu hardware-ul cuantic specific. Două etape cheie sunt alegerea unui layout de qubiți (maparea qubiților logici pe qubiții fizici) și rutarea porților (inserarea porților SWAP astfel încât porțile multi-qubit să respecte conectivitatea dispozitivului).

SABRE (SWAP-Based Bidirectional heuristic search algorithm — algoritm euristic bidirecțional bazat pe SWAP) optimizează atât layout-ul, cât și rutarea. Este deosebit de eficient pentru circuite la scară largă (100+ qubiți) pe dispozitive cu hărți de cuplare complexe, precum procesoarele IBM® Heron. SABRE minimizează porțile SWAP și reduce adâncimea circuitului, îmbunătățind fidelitatea execuției. Îmbunătățirile recente din algoritmul LightSABRE reduc și mai mult timpii de execuție și numărul de porți.

În acest tutorial, vei configura mai întâi SabreLayout cu diferiți parametri pentru a optimiza un circuit GHZ mic și vei observa impactul asupra fidelității execuției. Apoi, vei compara euristicile de rutare ale SABRE la scară pe hardware real.

Cerințe

Înainte de a începe acest tutorial, asigură-te că ai instalate următoarele:

  • Qiskit SDK v2.0 sau mai recent, cu suport pentru vizualizare
  • Qiskit Runtime v0.22 sau mai recent (pip install qiskit-ibm-runtime)
  • Qiskit Aer (pip install qiskit-aer)

Configurare

# 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

Exemplu la scară mică cu simulator

În această secțiune, se utilizează un simulator cu zgomot bazat pe modelul de zgomot al backend-ului real pentru a demonstra cum diferite configurații SabreLayout afectează atât calitatea transpilării, cât și fidelitatea execuției. Utilizând qiskit_aer cu un model de zgomot derivat din datele reale de calibrare hardware îți permite să testezi transpilarea fără a consuma credite hardware.

Pasul 1: Mapează intrările clasice la o problemă cuantică

Construim un circuit GHZ cu topologie stea cu 15 qubiți. Primul qubit este hub-ul, cu porți CNOT care îl conectează direct la fiecare alt qubit. Această topologie creează o problemă dificilă de layout deoarece nu se mapează trivial pe harta de cuplare a dispozitivului.

De asemenea, definim operatori ZZ pentru a măsura corelațiile de entanglement Z0Zi\langle Z_0 Z_i \rangle între perechile de qubiți.

ghz_star_topology.png

Când cunoști structura circuitului

SABRE este un algoritm de uz general și nu face presupuneri despre structura circuitului. Pentru acest circuit GHZ cu topologie stea, o rutare optimă este de fapt cunoscută: pasul StarPreRouting detectează sub-circuitele stea și le rescrie într-un lanț liniar care se mapează direct pe orice backend cu un traseu liniar suficient de lung. Acest tutorial se concentrează pe SABRE deoarece funcționează pentru circuite arbitrare, dar dacă știi că circuitul tău are o structură specială clară, aplicarea unui pas specializat precum StarPreRouting înainte de rutare poate depăși orice căutare euristică.

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]

Pasul 2: Optimizează problema pentru execuția pe hardware cuantic

Managerul de pași preset implicit optimization_level=3 utilizează deja SabreLayout, dar cu setări implicite conservatoare. Pentru a explora impactul setărilor mai agresive, acel pas este înlocuit cu un SabreLayout personalizat configurat pentru o căutare mai agresivă, în timp ce toate celelalte pași din etapa de layout sunt lăsate neatinse. Ca un punct de comparație separat, un al patrulea manager de pași păstrează SabreLayout-ul implicit, dar adaugă StarPreRouting în etapa de inițializare. StarPreRouting este un pas conștient de structură care detectează sub-circuitele stea și le rescrie într-un lanț liniar înainte de rutare.

Fluxul de lucru este:

  1. Inspectează managerul de pași implicit pentru a vedea unde se află SabreLayout în etapa layout.
  2. Înlocuiește acel pas cu o instanță personalizată SabreLayout folosind PassManager.replace(index, passes=...) și construiește varianta pm_star cu pm.init += StarPreRouting().
  3. Rulează toți cei patru manageri de pași și compară metricile.

Cele patru configurații sunt:

ConfigurațieDescriere
pm_1 (implicit)Preset implicit nivel 3 (SabreLayout cu max_iterations=4, layout_trials=20, swap_trials=20)
pm_2SabreLayout personalizat (max_iterations=4, layout_trials=200, swap_trials=200)
pm_3SabreLayout personalizat (max_iterations=8, layout_trials=200, swap_trials=200)
pm_starPreset implicit cu StarPreRouting adăugat în etapa de inițializare

Parametrii cheie SABRE:

  • layout_trials / swap_trials: Controlează câte layout-uri candidate și soluții de rutare explorează SABRE. Creșterea numărului de încercări înseamnă că SABRE eșantionează un spațiu de căutare mai larg, crescând șansa de a găsi o soluție mai bună.
  • max_iterations: Controlează câte cicluri de rafinare a rutării înainte-înapoi efectuează SABRE pentru fiecare candidat. SABRE îmbunătățește iterativ layout-ul prin învățare din feedback-ul rutării, deci cu cât mai multe iterații, cu atât mai bune sunt îmbunătățirile.

Ambele au ca preț un timp mai lung de transpilare, dar circuitele rezultate sunt mai scurte și folosesc mai puține porți, ceea ce reduce direct decoerența și erorile de poartă pe hardware-ul real.

Pasul 2a: Inspectează managerul de pași implicit. Un StagedPassManager este compus din etape (init, layout, routing, translation, optimization, scheduling), fiecare reprezentând ea însăși un PassManager. Apelând .draw() pe o etapă redă pașii săi ca grafic, astfel încât să putem vedea unde se află 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

În diagrama de mai sus, pasul SabreLayout pe care dorim să-l personalizăm se află în ConditionalController la poziția [2] a etapei de layout. Acel controler face două lucruri:

  • Condiționează SabreLayout astfel încât să ruleze doar când VF2Layout la [1] nu a reușit să găsească o mapare perfectă (altfel se păstrează layout-ul perfect VF2).
  • Precedă SabreLayout cu un pas BarrierBeforeFinalMeasurements care protejează măsurătorile de reordonare în timpul rutării interne a SabreLayout.

Dacă doar replace(index=2, passes=sl_2), ambele comportamente sunt abandonate. Pentru a le păstra, re-învelim SabreLayout-ul nostru personalizat în același ConditionalController (cu aceeași condiție și bariera protectivă) înainte de a-l înlocui.

Pasul 2b: Construiește pașii personalizați SabreLayout și înlocuiește cel implicit.

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

Poziția [2] este din nou un ConditionalController — identic ca formă cu cel implicit, dar SabreLayout-ul interior este cel personalizat (cu layout_trials=200, swap_trials=200 și max_iterations=8 pentru pm_3; pm_2 este identic, cu excepția max_iterations=4). Bariera protectivă și condiționarea _vf2_match_not_found sunt păstrate, astfel că singura diferență dintre pm_2/pm_3 și pm_1 este configurația SABRE în sine. pm_star păstrează SabreLayout-ul implicit și adaugă doar StarPreRouting la sfârșitul etapei de inițializare.

Pasul 2c: Rulează fiecare manager de pași și compară.

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%

Toți cei trei manageri de pași modificați au produs circuite cu adâncime 2Q mai mică decât cea implicită. Configurațiile SABRE agresive (pm_2 și pm_3) sacrifică un timp mai lung de transpilare pentru o căutare mai largă, în timp ce pm_star valorifică structura stea a circuitului și produce un rezultat și mai puțin adânc fără a plăti niciun cost suplimentar de transpilare. Câștigurile exacte vor varia de la o rulare la alta, dar tendința generală este consecventă: mai multe încercări și iterații SABRE permit euristicii să exploreze un spațiu mai larg, iar pași conștienți de structură precum StarPreRouting pot ocoli complet această căutare când forma circuitului se potrivește.

Chiar și la această scară mică (15 qubiți), există suficient spațiu de îmbunătățire astfel încât toate cele trei abordări să depășească valoarea implicită. Cu circuite mai mari (100+ qubiți), spațiul de căutare crește dramatic, iar beneficiile atât ale numărului crescut de încercări, cât și ale pașilor conștienți de structură devin mult mai pronunțate, după cum va arăta secțiunea la scară largă.

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

Pasul 3: Execuție folosind primitivele Qiskit

Rulăm fiecare circuit transpilat de 10 ori folosind Aer EstimatorV2 cu un model de zgomot derivat din backend-ul real. Deoarece rezultatele simulării cu zgomot variază între rulări, medierea pe mai multe rulări oferă estimări mai fiabile de fidelitate și ne permite să cuantificăm incertitudinea statistică cu bare de eroare.

# 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

Deoarece acesta este un circuit mic, valorile de fidelitate se situează relativ aproape pentru toate cele patru configurații. Circuitele sunt suficient de scurte încât zgomotul hardware nu penalizează puternic nici măcar versiunea cel mai puțin optimizată. Fidelitatea medie urmărește în linii mari adâncimea 2Q: pm_3 și pm_star, cele două circuite mai puțin adânci, ating cele mai ridicate fidelități și sunt practic la egalitate în limitele barelor de eroare. pm_2 este un contra-exemplu util: deși adâncimea sa 2Q este mai mică decât cea a pm_1, fidelitatea sa medie ajunge să fie marginal mai mică, ceea ce amintește că legătura adâncime-fidelitate este statistică, nu deterministă. Qubiții specifici pe care îi selectează un layout și calibrarea acelor qubiți la momentul rulării contează și ele.

Pasul 4: Post-procesare și returnarea rezultatului în formatul clasic dorit

Urmează să reprezentăm grafic corelațiile de entanglement Z0Zi\langle Z_0 Z_i \rangle în funcție de distanța dintre qubiți, împreună cu corelația medie ca metrică unică de fidelitate. Într-un caz ideal (fără zgomot), toate corelațiile ar fi 1. Cu zgomot realist, fiecare poartă suplimentară introduce erori și fiecare pas de timp suplimentar permite decoerența, deci un circuit transpilat cu adâncime mai mică și mai puține porți (în special porți cu doi qubiți) ar trebui să păstreze mai bine entanglement-ul.

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

Rezultatele arată o conexiune clară între calitatea transpilării și fidelitatea execuției, cu câteva observații utile:

  • pm_1 (implicit): Valoare de referință. Cu doar 20 de încercări și patru iterații, SABRE are un spațiu limitat pentru optimizare, rezultând în cel mai adânc dintre circuitele SABRE-only.
  • pm_2 (mai multe încercări): Explorarea de zece ori mai mulți candidați găsește un layout ușor mai puțin adânc, dar fidelitatea medie este aproximativ plată (și poate chiar scădea sub valoarea de referință în limitele zgomotului) deoarece câștigul de adâncime este mic la această scară.
  • pm_3 (mai multe încercări + mai multe iterații): Dublarea max_iterations la 8 oferă SABRE mai multe cicluri de rafinare, producând cel mai puțin adânc circuit SABRE-only și cea mai ridicată fidelitate medie în comparație.
  • pm_star (implicit + StarPreRouting): Adaugă StarPreRouting în etapa de inițializare a unui preset altfel implicit. Rescrierea conștientă de structură prăbușește steaua într-un lanț liniar pe care restul transpilatorului îl mapează pe traseul liniar al dispozitivului, producând cel mai puțin adânc circuit în general (ușor mai bun decât pm_3) și egalând pm_3 la fidelitate în limitele barelor de eroare. Realizează acest lucru cu același timp de transpilare ca și cel implicit, deoarece rescrierea este practic gratuită comparativ cu căutarea stochastică a SABRE.

Reține că creșterea max_iterations nu are întotdeauna un impact pozitiv. În acest caz a ajutat semnificativ, dar pentru alte circuite sau backend-uri, iterațiile suplimentare pot să nu producă îmbunătățiri suplimentare sau pot chiar să afecteze ușor performanța din cauza supra-optimizării unui minim local. În general, ar trebui să crești layout_trials și swap_trials cât mai mult permite bugetul tău de timp, deoarece mai multe încercări măresc întotdeauna șansa de a găsi un layout mai bun. Creșterea max_iterations merită testată, dar ar trebui validată pentru cazul tău specific de utilizare. Pașii specializați precum StarPreRouting sunt similari ca idee, dar mai dependenți de circuit: ajută doar când circuitul conține cu adevărat structura pe care o vizează. Câștigul este mare când este aplicabil și zero altfel, dar costă practic nimic să-i încerci.

Exemplu la scară largă pe hardware

Pe lângă ajustarea numărului de încercări, SABRE suportă personalizarea euristicii de rutare. SABRE oferă trei euristici:

  • basic: O abordare lăcomă simplă care selectează swap-ul care minimizează distanța imediată față de următoarea poartă.
  • decay (implicit): Ponderează dinamic qubiții în funcție de activitatea recentă, descurajând swap-urile repetate pe aceiași qubiți.
  • lookahead: Evaluează costurile viitoare de rutare privind înainte la porțile următoare, găsind potențial secvențe de swap mai bune.

Pentru a folosi o euristică personalizată, creează un pas SabreSwap și conectează-l la SabreLayout prin parametrul routing_pass.

Un al patrulea manager de pași este adăugat la comparație: pm_star_hw, care păstrează setările implicite SabreLayout/SabreSwap dar adaugă StarPreRouting în etapa de inițializare. La această scară (100 de qubiți), căutarea SABRE este mai dificilă, iar rescrierea din stea într-un lanț liniar devine un câștig clar deoarece un procesor Heron are trasee liniare suficient de lungi pentru a găzdui circuitul rezultat.

Aici comparăm toate cele trei euristici SABRE plus StarPreRouting la scară pe un circuit GHZ cu 100 de qubiți. Rulăm mai multe încercări de layout cu semințe diferite pentru configurațiile SABRE, selectăm cel mai bun circuit transpilat din fiecare și le trimitem pe toate pe hardware real alături de rezultatul StarPreRouting.

Pașii 1-4 comprimați într-un singur bloc de cod

Aici fluxul de lucru complet este asamblat la o scară mai mare. Când se folosește SabreSwap ca routing_pass pentru SabreLayout, se efectuează o singură încercare de layout per apel, deci celula de cod următoare iterează pe semințe pentru a explora spațiul de layout.

Folosim același ajutor wrap_sabre definit în Pasul 2 la scară mică (mai sus), și adăugăm un ajutor analogic wrap_routing deoarece etapa routing la indexul [1] este de asemenea un ConditionalController([BarrierBeforeFinalMeasurements, routing_pass], ...) — înlocuirea lui neînvelit ar abandona similar bariera protectivă și condiționarea _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']})"
)

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

Analiză

Diagramele scatter arată o variabilitate semnificativă a semințelor pentru toate cele trei euristici SABRE, ceea ce subliniază importanța rulării mai multor încercări de layout în loc să ne bazăm pe o singură transpilare. Linia StarPreRouting este practic plată pe toate semințele deoarece rescrierea din stea într-un lanț liniar este deterministă dată structura; rutarea SABRE din aval are apoi foarte puțină libertate pe un lanț liniar, deci sămânța nu are aproape niciun efect asupra adâncimii sau dimensiunii finale.

Din rezultatele transpilării, atât euristica decay, cât și lookahead depășesc constant basic cu o marjă largă. Euristica basic, deși rapidă, folosește o strategie lăcomă simplă care duce adesea la circuite substanțial mai adânci. Pentru acest circuit GHZ cu topologie stea, lookahead tinde să producă cea mai mică adâncime 2Q și număr de porți dintre euristicile SABRE, deoarece funcția sa de cost orientată spre viitor este bine adaptată circuitelor cu tipare de conectivitate pe distanțe lungi. StarPreRouting, totuși, le depășește pe toate trei cu o marjă substanțială: prin rescrierea stelei într-un lanț liniar înainte de rutare, ocolește complet problema de căutare și livrează un circuit pe care restul transpilatorului îl poate mapa pe un traseu liniar cu SWAP-uri suplimentare minime.

Acel avantaj se transferă direct la fidelitatea hardware. Adâncimea 2Q mai mică și numărul de porți mai mic nu se traduc întotdeauna unu-la-unu la o fidelitate mai mare (qubiții fizici specifici pe care îi utilizează un layout și calibrarea lor la momentul rulării contează și ele), dar când diferența de adâncime este la fel de mare ca cea dintre SABRE și StarPreRouting aici, abordarea conștientă de structură câștigă decisiv deoarece circuitul acumulează mult mai puțină decoerență și mult mai puține evenimente de eroare a porților cu doi qubiți. Graficul de bare al fidelității arată StarPreRouting substanțial înaintea chiar și celei mai bune euristici SABRE, în timp ce basic se situează mult sub celelalte deoarece circuitele sale mult mai adânci acumulează cele mai multe erori.

Concluzii cheie:

  • Printre euristicile SABRE, decay și lookahead sunt substanțial mai bune decât basic pentru circuite non-triviale. Preferă una din cele două pentru sarcinile de producție.
  • Cea mai bună euristică SABRE depinde de circuit și hardware. Testarea mai multor euristici cu mai multe semințe este strategia cea mai fiabilă.
  • Dacă vrei să explorezi mai multe layout-uri, crește swap_trials (și layout_trials când nu fixezi un pas personalizat de rutare) mai degrabă decât să distribui munca pe noduri la distanță. Pașii SABRE paralelizează deja încercările pe thread-uri locale, iar munca per încercare este suficient de mică încât overhead-ul de distribuție domină de obicei orice câștig de viteză.
  • Când circuitul are o structură specială cunoscută, aplicarea unui pas conștient de structură precum StarPreRouting înainte de SABRE poate livra o îmbunătățire de un ordin de mărime pe care nicio cantitate de ajustare SABRE nu o va egala. Acesta nu este un înlocuitor pentru SABRE: StarPreRouting ajută doar când circuitul conține cu adevărat sub-circuite stea și backend-ul are un traseu liniar suficient de lung. Merită verificată biblioteca de pași pentru potriviri ori de câte ori cunoști forma circuitului tău.

Pași următori

Dacă ai găsit această lucrare interesantă, s-ar putea să fii interesat de următorul material:

Recomandări

Sondaj tutorial

Te rugăm să completezi acest scurt sondaj pentru a oferi feedback despre acest tutorial. Părerile tale ne vor ajuta să ne îmbunătățim oferta de conținut și experiența utilizatorului.

Notă: Acest sondaj este de la IBM Quantum și acoperă conținutul tutorialului (scris de IBM). doQumentation oferă site-ul web, traducerile și execuția codului — pentru feedback despre acestea, te rugăm să deschizi un issue pe GitHub.

Link la sondaj