Optimización de transpilación con SABRE
Estimación de uso: 1 minuto en un procesador Heron r2 (NOTA: Esto es solo una estimación. Tu tiempo de ejecución puede variar.)
Resultados de aprendizaje
Después de completar este tutorial, deberías comprender:
- Cómo configurar los parámetros de SABRE (
layout_trials,swap_trials,max_iterations) para mejorar la calidad de la transpilación - Las compensaciones entre el tiempo de transpilación y la calidad del circuito (profundidad y conteo de compuertas)
- Cómo personalizar la heurística de enrutamiento SABRE (
basic,decay,lookahead) y comparar su rendimiento en hardware
Requisitos previos
Te sugerimos que estés familiarizado con los siguientes temas antes de completar este tutorial:
- Transpilación de circuitos: descripción general de la transpilación en Qiskit
- Etapas del transpilador: etapas de disposición y enrutamiento
- Configurar gestores de pases preestablecidos: personalización de niveles de optimización
Antecedentes
La transpilación convierte circuitos cuánticos en formas compatibles con hardware cuántico específico. Dos etapas clave son elegir una disposición de qubits (mapeo de qubits lógicos a qubits físicos) y el enrutamiento de compuertas (inserción de compuertas SWAP para que las compuertas multi-qubit respeten la conectividad del dispositivo).
SABRE (SWAP-Based Bidirectional heuristic search algorithm) optimiza tanto la disposición como el enrutamiento. Es especialmente eficaz para circuitos a gran escala (100+ qubits) en dispositivos con mapas de acoplamiento complejos, como los procesadores IBM® Heron. SABRE minimiza las compuertas SWAP y reduce la profundidad del circuito, mejorando la fidelidad de ejecución. Las mejoras recientes en el algoritmo LightSABRE reducen aún más los tiempos de ejecución y los conteos de compuertas.
En este tutorial, primero configurarás SabreLayout con diferentes parámetros para optimizar un pequeño circuito GHZ y observarás el impacto en la fidelidad de ejecución. Luego, compararás las heurísticas de enrutamiento de SABRE a escala en hardware real.
Requisitos
Antes de comenzar este tutorial, asegúrate de tener instalado lo siguiente:
- Qiskit SDK v2.0 o posterior, con soporte de visualización
- Qiskit Runtime v0.22 o posterior (
pip install qiskit-ibm-runtime) - Qiskit Aer (
pip install qiskit-aer)
Configuración
# 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
Ejemplo a pequeña escala con simulador
En esta sección, se utiliza un simulador ruidoso basado en el modelo de ruido del backend real para demostrar cómo diferentes configuraciones de SabreLayout afectan tanto la calidad de la transpilación como la fidelidad de ejecución. Usar qiskit_aer con un modelo de ruido derivado de datos de calibración de hardware real te permite probar la transpilación sin consumir créditos de hardware.
Paso 1: Mapear entradas clásicas a un problema cuántico
Construimos un circuito GHZ de topología en estrella con 15 qubits. El primer qubit es el centro, con compuertas CNOT que lo conectan directamente a todos los demás qubits. Esta topología crea un problema de disposición desafiante porque no se mapea trivialmente al mapa de acoplamiento del dispositivo.
También definimos operadores ZZ para medir las correlaciones de entrelazamiento entre pares de qubits.

SABRE es un algoritmo de propósito general y no hace suposiciones sobre la estructura del circuito. Para este circuito GHZ de topología en estrella, en realidad se conoce un enrutamiento óptimo: el pase StarPreRouting detecta sub-circuitos en estrella y los reescribe en una cadena lineal que se mapea directamente a cualquier backend con una ruta lineal suficientemente larga. Este tutorial se centra en SABRE porque funciona para circuitos arbitrarios, pero si sabes que tu circuito tiene una estructura especial clara, aplicar un pase especializado como StarPreRouting antes del enrutamiento puede superar a cualquier búsqueda heurística.
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]
Paso 2: Optimizar el problema para la ejecución en hardware cuántico
El gestor de pases preestablecido predeterminado con optimization_level=3 ya usa SabreLayout, pero con valores predeterminados conservadores. Para explorar el impacto de configuraciones más agresivas, ese pase se reemplaza por un SabreLayout personalizado configurado para una búsqueda más intensiva, mientras que todos los demás pases en la etapa de disposición se dejan intactos. Como punto de comparación adicional, un cuarto gestor de pases mantiene el SabreLayout predeterminado pero añade StarPreRouting a la etapa de inicialización. StarPreRouting es un pase consciente de la estructura que detecta sub-circuitos en estrella y los reescribe en una cadena lineal antes del enrutamiento.
El flujo de trabajo es:
- Inspeccionar el gestor de pases predeterminado para ver dónde se ubica
SabreLayoutdentro de la etapalayout. - Reemplazar ese pase con una instancia personalizada de
SabreLayoutusandoPassManager.replace(index, passes=...), y construir la variantepm_starconpm.init += StarPreRouting(). - Ejecutar los cuatro gestores de pases y comparar las métricas.
Las cuatro configuraciones son:
| Config | Descripción |
|---|---|
pm_1 (predeterminado) | Preestablecido de nivel 3 predeterminado (SabreLayout con max_iterations=4, layout_trials=20, swap_trials=20) |
pm_2 | SabreLayout personalizado (max_iterations=4, layout_trials=200, swap_trials=200) |
pm_3 | SabreLayout personalizado (max_iterations=8, layout_trials=200, swap_trials=200) |
pm_star | Preestablecido predeterminado con StarPreRouting añadido a la etapa de inicialización |
Parámetros clave de SABRE:
layout_trials/swap_trials: Controlan cuántas disposiciones candidatas y soluciones de enrutamiento explora SABRE. Aumentar el número de pruebas significa que SABRE muestrea un espacio de búsqueda más amplio, incrementando la probabilidad de encontrar una mejor solución.max_iterations: Controla cuántos ciclos de refinamiento de enrutamiento hacia adelante y hacia atrás realiza SABRE en cada candidato. SABRE mejora iterativamente la disposición aprendiendo de la retroalimentación del enrutamiento, por lo que más iteraciones conllevan mejores mejoras.
Ambos tienen el costo de un mayor tiempo de transpilación, pero los circuitos resultantes son más cortos y usan menos compuertas, lo que directamente reduce la decoherencia y los errores de compuertas en hardware real.
Paso 2a: Inspeccionar el gestor de pases predeterminado. Un StagedPassManager está compuesto de etapas (init, layout, routing, translation, optimization, scheduling), cada una a su vez un PassManager. Llamar a .draw() en una etapa renderiza sus pases como un grafo para que podamos ver dónde se ubica 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()

En el diagrama anterior, el pase SabreLayout que queremos personalizar vive dentro del ConditionalController en la posición [2] de la etapa de disposición. Ese controlador hace dos cosas:
- Condiciona
SabreLayoutpara que solo se ejecute cuandoVF2Layouten [1] no encontró un mapeo perfecto (de lo contrario, se mantiene la disposición perfecta de VF2). - Precede
SabreLayoutcon un paseBarrierBeforeFinalMeasurementsque protege las mediciones de ser reordenadas durante el enrutamiento interno de SabreLayout.
Si simplemente hacemos replace(index=2, passes=sl_2), ambos comportamientos se eliminan. Para mantenerlos, volvemos a envolver nuestro SabreLayout personalizado en el mismo ConditionalController (con la misma condición y la barrera protectora) antes de intercambiarlo.
Paso 2b: Construir pases SabreLayout personalizados y reemplazar el predeterminado.
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()

La posición [2] es nuevamente un ConditionalController — idéntico en estructura al predeterminado, pero el SabreLayout interior es nuestro personalizado (con layout_trials=200, swap_trials=200, y max_iterations=8 para pm_3; pm_2 es idéntico salvo por max_iterations=4). La barrera protectora y la condición _vf2_match_not_found se preservan, por lo que la única diferencia entre pm_2/pm_3 y pm_1 es la configuración de SABRE en sí. pm_star mantiene el SabreLayout predeterminado y solo añade StarPreRouting al final de la etapa de inicialización.
Paso 2c: Ejecutar cada gestor de pases y comparar.
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%
Los tres gestores de pases modificados produjeron circuitos con menor profundidad 2Q que el predeterminado. Las configuraciones agresivas de SABRE (pm_2 y pm_3) intercambian un mayor tiempo de transpilación por una búsqueda más amplia, mientras que pm_star aprovecha la estructura en estrella del circuito y produce un resultado aún más superficial sin pagar ningún costo adicional de transpilación. Las ganancias exactas variarán de ejecución en ejecución, pero la tendencia general es consistente: más pruebas e iteraciones de SABRE permiten que la búsqueda heurística explore un espacio más amplio, y los pases conscientes de la estructura como StarPreRouting pueden eludir esa búsqueda por completo cuando la forma del circuito coincide.
Incluso a esta pequeña escala (15 qubits), el margen de mejora es suficiente para que los tres enfoques superen al predeterminado. Con circuitos más grandes (100+ qubits), el espacio de búsqueda crece drásticamente y los beneficios tanto de más pruebas como de los pases conscientes de la estructura se vuelven mucho más pronunciados, como mostrará la sección a gran escala.
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()

Paso 3: Ejecutar usando primitivas de Qiskit
Ejecutamos cada circuito transpilado 10 veces usando el EstimatorV2 de Aer con un modelo de ruido derivado del backend real. Dado que los resultados de simulación ruidosa varían entre ejecuciones, promediar sobre múltiples ejecuciones proporciona estimaciones de fidelidad más confiables y nos permite cuantificar la incertidumbre estadística con barras de error.
# 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
Dado que este es un circuito pequeño, los valores de fidelidad caen relativamente cerca entre las cuatro configuraciones. Los circuitos son lo suficientemente cortos como para que el ruido del hardware no penalice gravemente ni siquiera la versión menos optimizada. La fidelidad media sigue en general la profundidad 2Q: pm_3 y pm_star, los dos circuitos más superficiales, alcanzan las fidelidades más altas y están esencialmente empatados dentro de sus barras de error. pm_2 es un contra-ejemplo útil: aunque su profundidad 2Q es menor que la de pm_1, su fidelidad media resulta marginalmente inferior también, lo que recuerda que el vínculo profundidad-fidelidad es estadístico y no determinista. Los qubits específicos que selecciona una disposición y la calibración de esos qubits en el momento de la ejecución también importan.
Paso 4: Post-procesar y devolver el resultado en el formato clásico deseado
A continuación, grafica las correlaciones de entrelazamiento como función de la distancia entre qubits, junto con la correlación media como métrica única de fidelidad. En un caso ideal (sin ruido), todas las correlaciones serían 1. Con ruido realista, cada compuerta adicional introduce error y cada paso de tiempo adicional permite la decoherencia, por lo que un circuito transpilado con menor profundidad y menos compuertas (especialmente compuertas de dos qubits) debería preservar mejor el entrelazamiento.
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()

Los resultados muestran una conexión clara entre la calidad de la transpilación y la fidelidad de ejecución, con algunas advertencias útiles:
pm_1(predeterminado): Línea base. Con solo 20 pruebas y cuatro iteraciones, SABRE tiene poco margen para optimizar, lo que resulta en el circuito más profundo de los que usan solo SABRE.pm_2(más pruebas): Explorar diez veces más candidatos encuentra una disposición ligeramente más superficial, pero la fidelidad media es aproximadamente plana (e incluso puede caer por debajo de la línea base dentro del ruido) porque la ganancia en profundidad es pequeña a esta escala.pm_3(más pruebas + más iteraciones): Duplicarmax_iterationsa 8 le da a SABRE más ciclos de refinamiento, produciendo el circuito SABRE-solo más superficial y la fidelidad media más alta en la comparación.pm_star(predeterminado + StarPreRouting): AñadeStarPreRoutinga la etapa de inicialización de un preestablecido predeterminado. La reescritura consciente de la estructura colapsa la estrella en una cadena lineal que el resto del transpilador mapea a la ruta lineal del dispositivo, produciendo el circuito más superficial en general (ligeramente mejor quepm_3) y coincidiendo conpm_3en fidelidad dentro de las barras de error. Lo logra con el mismo tiempo de transpilación que el predeterminado, ya que la reescritura es esencialmente gratuita en comparación con la búsqueda estocástica de SABRE.
Ten en cuenta que aumentar max_iterations no siempre tiene un impacto positivo. En este caso ayudó significativamente, pero para otros circuitos o backends las iteraciones adicionales pueden no producir mejoras adicionales, o incluso pueden perjudicar ligeramente el rendimiento debido a la sobreoptimización de un mínimo local. En general, deberías aumentar layout_trials y swap_trials tanto como tu presupuesto de tiempo lo permita, ya que más pruebas siempre incrementan la probabilidad de encontrar una mejor disposición. Aumentar max_iterations vale la pena probarlo pero debería validarse para tu caso de uso específico. Los pases especializados como StarPreRouting son similares en espíritu pero más dependientes del circuito: solo ayudan cuando el circuito contiene realmente la estructura que apuntan. La ganancia es grande cuando es aplicable y nula en caso contrario, pero cuestan esencialmente nada probar.
Ejemplo de hardware a gran escala
Además de ajustar el número de pruebas, SABRE admite la personalización de la heurística de enrutamiento. SABRE ofrece tres heurísticas:
basic: Un enfoque codicioso simple que selecciona el swap que minimiza la distancia inmediata a la siguiente compuerta.decay(predeterminado): Pondera dinámicamente los qubits según la actividad reciente, desalentando los swaps repetidos en los mismos qubits.lookahead: Evalúa los costos de enrutamiento futuros mirando hacia adelante en las próximas compuertas, encontrando potencialmente mejores secuencias de swaps.
Para usar una heurística personalizada, crea un pase SabreSwap y conéctalo a SabreLayout mediante el parámetro routing_pass.
Se añade un cuarto gestor de pases a la comparación: pm_star_hw, que mantiene la configuración predeterminada de SabreLayout/SabreSwap pero añade StarPreRouting a la etapa de inicialización. A esta escala (100 qubits) la búsqueda SABRE es más difícil, y la reescritura de una estrella a una cadena lineal se convierte en una clara ventaja porque un procesador Heron tiene rutas lineales suficientemente largas para alojar el circuito resultante.
Aquí comparamos las tres heurísticas SABRE más StarPreRouting a escala en un circuito GHZ de 100 qubits. Ejecutamos múltiples pruebas de disposición con diferentes semillas para las configuraciones SABRE, seleccionamos el mejor circuito transpilado de cada una, y los enviamos todos al hardware real junto con el resultado de StarPreRouting.
Pasos 1-4 comprimidos en un único bloque de código
Aquí se ensambla el flujo de trabajo completo a mayor escala. Al usar SabreSwap como routing_pass para SabreLayout, solo se realiza una prueba de disposición por llamada, por lo que el siguiente bloque de código itera sobre semillas para explorar el espacio de disposición.
Usamos el mismo helper wrap_sabre definido en el Paso 2 a pequeña escala (arriba), y añadimos un helper análogo wrap_routing porque la etapa routing en el índice [1] también es un ConditionalController([BarrierBeforeFinalMeasurements, routing_pass], ...) — reemplazarlo sin envolver eliminaría igualmente la barrera protectora y la condició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
Análisis
Los diagramas de dispersión muestran una variabilidad significativa entre semillas para las tres heurísticas SABRE, lo que subraya la importancia de ejecutar múltiples pruebas de disposición en lugar de depender de una sola transpilación. La línea de StarPreRouting es esencialmente plana entre semillas porque la reescritura de una estrella a una cadena lineal es determinista dada la estructura; el enrutamiento SABRE posterior tiene entonces muy poca libertad en una cadena lineal, por lo que la semilla apenas afecta la profundidad o el tamaño final.
A partir de los resultados de transpilación, tanto las heurísticas decay como lookahead superan consistentemente a basic por un amplio margen. La heurística basic, aunque rápida, usa una estrategia codiciosa simple que a menudo produce circuitos sustancialmente más profundos. Para este circuito GHZ de topología en estrella, lookahead tiende a producir la menor profundidad 2Q y el menor conteo de compuertas entre las heurísticas SABRE, ya que su función de costo prospectiva se adapta bien a circuitos con patrones de conectividad de largo alcance. StarPreRouting, sin embargo, supera a las tres por un margen sustancial: al reescribir la estrella en una cadena lineal antes del enrutamiento, elude completamente el problema de búsqueda y entrega un circuito que el resto del transpilador puede mapear a una ruta lineal con un mínimo de SWAPs adicionales.
Esa ventaja se traslada directamente a la fidelidad en hardware. Una menor profundidad 2Q y un menor conteo de compuertas no siempre se traducen uno a uno en mayor fidelidad (los qubits físicos específicos que usa una disposición y su calibración en el momento de la ejecución también importan), pero cuando la brecha de profundidad es tan grande como la que existe entre SABRE y StarPreRouting aquí, el enfoque consciente de la estructura gana de forma decisiva porque el circuito acumula mucha menos decoherencia y muchos menos eventos de error de dos qubits. El gráfico de barras de fidelidad muestra StarPreRouting sustancialmente por delante incluso de la mejor heurística SABRE, mientras que basic se ubica muy por debajo del resto porque sus circuitos mucho más profundos acumulan el mayor error.
Conclusiones clave:
- Entre las heurísticas SABRE,
decayylookaheadson sustancialmente mejores quebasicpara circuitos no triviales. Prefiere una de las dos para cargas de trabajo en producción. - La mejor heurística SABRE depende de tu circuito y hardware. Probar múltiples heurísticas con múltiples semillas es la estrategia más confiable.
- Si quieres explorar aún más disposiciones, aumenta
swap_trials(ylayout_trialscuando no estás fijando un pase de enrutamiento personalizado) en lugar de distribuir el trabajo a nodos remotos. Los pases SABRE ya paralelizan las pruebas a través de hilos locales, y el trabajo por prueba es suficientemente pequeño como para que la sobrecarga de distribución típicamente domine cualquier aceleración. - Cuando el circuito tiene una estructura especial conocida, aplicar un pase consciente de la estructura como
StarPreRoutingantes de SABRE puede brindar una mejora de un orden de magnitud que ninguna cantidad de ajuste de SABRE igualará. Esto no es un reemplazo de SABRE:StarPreRoutingsolo ayuda cuando el circuito realmente contiene sub-circuitos en estrella y el backend tiene una ruta lineal suficientemente larga. Vale la pena revisar la biblioteca de pases para encontrar coincidencias siempre que conozcas la forma de tu circuito.
Próximos pasos
Si este trabajo te resultó interesante, puede que te interese el siguiente material:
- Referencia de API de
SabreLayout: documentación completa de parámetros - Artículo de SABRE: el algoritmo SABRE original para disposición y enrutamiento
- Artículo de LightSABRE: las mejoras algorítmicas que potencian la implementación actual de SABRE en Qiskit
- Escribir un pase de transpilador personalizado: construye tu propia lógica de transpilación
- Plugins del transpilador: extiende la tubería de transpilación de Qiskit con pases de terceros
- Representación DAG: comprende el grafo acíclico dirigido utilizado internamente por el transpilador
Encuesta del tutorial
Por favor, completa esta breve encuesta para proporcionar comentarios sobre este tutorial. Tus opiniones nos ayudarán a mejorar nuestro contenido y la experiencia del usuario.
Nota: Esta encuesta es proporcionada por IBM Quantum y cubre el contenido del tutorial (escrito por IBM). doQumentation proporciona el sitio web, las traducciones y la ejecución de código — para comentarios sobre esos aspectos, por favor abre un issue en GitHub.