Otimização de transpilação com SABRE
Estimativa de uso: 1 minuto em um processador Heron r2 (NOTA: Esta é apenas uma estimativa. Seu tempo de execução pode variar.)
Objetivos de aprendizagem
Após concluir este tutorial, você deverá compreender:
- Como configurar os parâmetros do SABRE (
layout_trials,swap_trials,max_iterations) para melhorar a qualidade da transpilação - Os trade-offs entre o tempo de transpilação e a qualidade do circuito (profundidade e contagem de portas)
- Como personalizar a heurística de roteamento do SABRE (
basic,decay,lookahead) e comparar seu desempenho em hardware
Pré-requisitos
Sugerimos que você esteja familiarizado com os seguintes tópicos antes de iniciar este tutorial:
- Transpilar circuitos: visão geral da transpilação no Qiskit
- Estágios do Transpiler: estágios de layout e roteamento
- Configurar gerenciadores de passes predefinidos: personalizando níveis de otimização
Contexto
A transpilação converte circuitos quânticos em formas compatíveis com hardware quântico específico. Dois estágios principais são a escolha de um layout de qubit (mapeamento de qubits lógicos para qubits físicos) e o roteamento de portas (inserção de portas SWAP para que portas de múltiplos qubits respeitem a conectividade do dispositivo).
SABRE (SWAP-Based Bidirectional heuristic search algorithm) otimiza tanto o layout quanto o roteamento. É especialmente eficaz para circuitos de grande escala (100+ qubits) em dispositivos com mapas de acoplamento complexos, como os processadores IBM® Heron. O SABRE minimiza portas SWAP e reduz a profundidade do circuito, melhorando a fidelidade de execução. Melhorias recentes no algoritmo LightSABRE reduzem ainda mais os tempos de execução e contagens de portas.
Neste tutorial, você irá primeiro configurar o SabreLayout com diferentes parâmetros para otimizar um pequeno circuito GHZ e observar o impacto na fidelidade de execução. Em seguida, comparará as heurísticas de roteamento do SABRE em escala em hardware real.
Requisitos
Antes de iniciar este tutorial, certifique-se de ter o seguinte instalado:
- Qiskit SDK v2.0 ou posterior, com suporte a visualização
- Qiskit Runtime v0.22 ou posterior (
pip install qiskit-ibm-runtime) - Qiskit Aer (
pip install qiskit-aer)
Configuração
# 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
Exemplo em simulador de pequena escala
Nesta seção, um simulador com ruído baseado no modelo de ruído do backend real é usado para demonstrar como diferentes configurações do SabreLayout afetam tanto a qualidade da transpilação quanto a fidelidade de execução. Usar qiskit_aer com um modelo de ruído derivado de dados reais de calibração de hardware permite testar a transpilação sem consumir créditos de hardware.
Passo 1: Mapear entradas clássicas para um problema quântico
Construímos um circuito GHZ de topologia em estrela com 15 qubits. O primeiro qubit é o hub, com portas CNOT conectando-o diretamente a todos os outros qubits. Esta topologia cria um problema de layout desafiador porque não se mapeia trivialmente para o mapa de acoplamento do dispositivo.
Também definimos operadores ZZ para medir correlações de emaranhamento entre pares de qubits.

O SABRE é um algoritmo de propósito geral e não faz suposições sobre a estrutura do circuito. Para este circuito GHZ de topologia em estrela, um roteamento ótimo é na verdade conhecido: o passe StarPreRouting detecta sub-circuitos em estrela e os reescreve em uma cadeia linear que se mapeia diretamente em qualquer backend com um caminho linear suficientemente longo. Este tutorial se concentra no SABRE porque ele funciona para circuitos arbitrários, mas se você sabe que seu circuito tem uma estrutura especial clara, aplicar um passe especializado como StarPreRouting antes do roteamento pode superar qualquer busca 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]
Passo 2: Otimizar o problema para execução em hardware quântico
O gerenciador de passes predefinido padrão optimization_level=3 já usa SabreLayout, mas com padrões conservadores. Para explorar o impacto de configurações mais fortes, esse passe é substituído por um SabreLayout personalizado configurado para uma busca mais agressiva, enquanto todos os outros passes no estágio de layout são mantidos intactos. Como ponto de comparação separado, um quarto gerenciador de passes mantém o SabreLayout padrão, mas adiciona StarPreRouting ao estágio de init. O StarPreRouting é um passe ciente da estrutura que detecta sub-circuitos em estrela e os reescreve em uma cadeia linear antes do roteamento.
O fluxo de trabalho é:
- Inspecionar o gerenciador de passes padrão para ver onde o
SabreLayoutfica dentro do estágiolayout. - Substituir esse passe por uma instância personalizada de
SabreLayoutusandoPassManager.replace(index, passes=...), e construir a variantepm_starcompm.init += StarPreRouting(). - Executar os quatro gerenciadores de passes e comparar as métricas.
As quatro configurações são:
| Configuração | Descrição |
|---|---|
pm_1 (padrão) | Preset nível 3 padrão (SabreLayout com 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 | Preset padrão com StarPreRouting adicionado ao estágio de init |
Parâmetros-chave do SABRE:
layout_trials/swap_trials: Controlam quantos layouts candidatos e soluções de roteamento o SABRE explora. Aumentar o número de tentativas significa que o SABRE amostra um espaço de busca mais amplo, aumentando a chance de encontrar uma solução melhor.max_iterations: Controla quantos ciclos de refinamento de roteamento forward-backward o SABRE realiza em cada candidato. O SABRE melhora iterativamente o layout aprendendo com o feedback de roteamento, então quanto mais iterações, melhores as melhorias.
Ambos têm o custo de maior tempo de transpilação, mas os circuitos resultantes são mais curtos e usam menos portas, o que diretamente reduz decoerência e erros de portas em hardware real.
Passo 2a: Inspecionar o gerenciador de passes padrão. Um StagedPassManager é composto de estágios (init, layout, routing, translation, optimization, scheduling), cada um por si mesmo um PassManager. Chamar .draw() em um estágio renderiza seus passes como um grafo para que possamos ver onde o SabreLayout fica.
# 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()

No diagrama acima, o passe SabreLayout que queremos personalizar fica dentro do ConditionalController na posição [2] do estágio de layout. Esse controlador faz duas coisas:
- Ele condiciona o
SabreLayoutpara que só execute quando oVF2Layoutem [1] falhou em encontrar um mapeamento perfeito (caso contrário, o layout VF2 perfeito é mantido). - Ele precede o
SabreLayoutcom um passeBarrierBeforeFinalMeasurementsque protege as medições de serem reordenadas durante o roteamento interno do SabreLayout.
Se apenas fizermos replace(index=2, passes=sl_2), ambos os comportamentos serão descartados. Para mantê-los, reembrulhamos nosso SabreLayout personalizado no mesmo ConditionalController (com a mesma condição e a barreira protetora) antes de substituí-lo.
Passo 2b: Construir passes SabreLayout personalizados e substituir o padrão.
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()

A posição [2] é novamente um ConditionalController — idêntico em forma ao padrão, mas o SabreLayout interno é o nosso personalizado (com layout_trials=200, swap_trials=200 e max_iterations=8 para pm_3; pm_2 é idêntico exceto por max_iterations=4). A barreira protetora e o condicionamento _vf2_match_not_found são preservados, então a única diferença entre pm_2/pm_3 e pm_1 é a configuração do SABRE em si. O pm_star mantém o SabreLayout padrão e apenas adiciona StarPreRouting ao final do estágio de init.
Passo 2c: Executar cada gerenciador de passes e 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%
Os três gerenciadores de passes modificados produziram circuitos com menor profundidade 2Q do que o padrão. As configurações SABRE agressivas (pm_2 e pm_3) trocam maior tempo de transpilação por uma busca mais ampla, enquanto pm_star aproveita a estrutura em estrela do circuito e produz um resultado ainda mais raso sem pagar nenhum custo extra de transpilação. Os ganhos exatos variarão de execução para execução, mas a tendência geral é consistente: mais tentativas e iterações do SABRE permitem que a busca heurística explore um espaço mais amplo, e passes cientes da estrutura como StarPreRouting podem contornar completamente essa busca quando a forma do circuito corresponde.
Mesmo nessa escala pequena (15 qubits), a margem de melhoria é suficiente para que as três abordagens superem o padrão. Com circuitos maiores (100+ qubits), o espaço de busca cresce dramaticamente e os benefícios de tanto o aumento de tentativas quanto os passes cientes da estrutura se tornam muito mais pronunciados, como a seção de grande escala mostrará.
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()

Passo 3: Executar usando primitivas Qiskit
Executamos cada circuito transpilado 10 vezes usando o EstimatorV2 do Aer com um modelo de ruído derivado do backend real. Como os resultados de simulação com ruído variam entre execuções, fazer a média de múltiplas execuções fornece estimativas de fidelidade mais confiáveis e nos permite quantificar a incerteza estatística com barras de erro.
# 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
Como este é um circuito pequeno, os valores de fidelidade ficam relativamente próximos entre as quatro configurações. Os circuitos são curtos o suficiente para que o ruído de hardware não penalize severamente nem mesmo a versão menos otimizada. A fidelidade média acompanha amplamente a profundidade 2Q: pm_3 e pm_star, os dois circuitos mais rasos, alcançam as maiores fidelidades e estão essencialmente empatados dentro de suas barras de erro. pm_2 é um contraexemplo útil: embora sua profundidade 2Q seja menor do que a de pm_1, sua fidelidade média acaba sendo marginalmente menor também, o que serve de lembrete de que a relação profundidade-fidelidade é estatística e não determinística. Os qubits físicos específicos que um layout seleciona e a calibração desses qubits no momento de execução também importam.
Passo 4: Pós-processar e retornar resultado no formato clássico desejado
Em seguida, plote as correlações de emaranhamento em função da distância entre qubits, junto com a correlação média como uma métrica de fidelidade única. Em um caso ideal (sem ruído), todas as correlações seriam 1. Com ruído realista, cada porta adicional introduz erro e cada etapa de tempo adicional permite decoerência, portanto, um circuito transpilado com menor profundidade e menos portas (especialmente portas de dois qubits) deve preservar melhor o emaranhamento.
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()

Os resultados mostram uma conexão clara entre a qualidade da transpilação e a fidelidade de execução, com algumas ressalvas úteis:
pm_1(padrão): Referência. Com apenas 20 tentativas e quatro iterações, o SABRE tem espaço limitado para otimizar, resultando no circuito mais profundo entre os circuitos apenas-SABRE.pm_2(mais tentativas): Explorar dez vezes mais candidatos encontra um layout ligeiramente mais raso, mas a fidelidade média é aproximadamente estável (e pode até cair abaixo da referência dentro do ruído) porque o ganho de profundidade é pequeno nessa escala.pm_3(mais tentativas + mais iterações): Dobrarmax_iterationspara 8 dá ao SABRE mais ciclos de refinamento, produzindo o circuito mais raso apenas-SABRE e a maior fidelidade média na comparação.pm_star(padrão + StarPreRouting): AdicionaStarPreRoutingao estágio de init de um preset padrão. A reescrita ciente da estrutura colapsa a estrela em uma cadeia linear que o restante do transpiler mapeia no caminho linear do dispositivo, produzindo o circuito mais raso no geral (ligeiramente melhor do quepm_3) e empatando compm_3em fidelidade dentro das barras de erro. Faz isso com o mesmo tempo de transpilação do padrão, já que a reescrita é essencialmente gratuita em comparação com a busca estocástica do SABRE.
Observe que aumentar max_iterations nem sempre tem um impacto positivo. Neste caso, ajudou significativamente, mas para outros circuitos ou backends as iterações adicionais podem não gerar mais melhorias, ou podem até prejudigar ligeiramente o desempenho devido à superotimização de um mínimo local. Em geral, você deve aumentar layout_trials e swap_trials o máximo que seu orçamento de tempo permitir, já que mais tentativas sempre aumentam a chance de encontrar um layout melhor. Aumentar max_iterations vale a pena testar, mas deve ser validado para seu caso de uso específico. Passes especializados como StarPreRouting são semelhantes em espírito, mas mais dependentes do circuito: eles só ajudam quando o circuito realmente contém a estrutura que visam. O ganho é grande quando aplicável e zero caso contrário, mas eles custam essencialmente nada para tentar.
Exemplo de hardware em grande escala
Além de ajustar o número de tentativas, o SABRE suporta a personalização da heurística de roteamento. O SABRE oferece três heurísticas:
basic: Uma abordagem gulosa simples que seleciona o swap que minimiza a distância imediata à próxima porta.decay(padrão): Pondera dinamicamente os qubits com base em atividade recente, desencorajando swaps repetidos nos mesmos qubits.lookahead: Avalia custos de roteamento futuros olhando antecipadamente para portas próximas, potencialmente encontrando sequências de swap melhores.
Para usar uma heurística personalizada, crie um passe SabreSwap e conecte-o ao SabreLayout via parâmetro routing_pass.
Um quarto gerenciador de passes é adicionado à comparação: pm_star_hw, que mantém as configurações padrão de SabreLayout/SabreSwap, mas adiciona StarPreRouting ao estágio de init. Nessa escala (100 qubits), a busca SABRE é mais difícil, e a reescrita de uma estrela em cadeia linear se torna uma vitória clara porque um processador Heron tem caminhos lineares longos o suficiente para hospedar o circuito resultante.
Aqui comparamos as três heurísticas SABRE mais StarPreRouting em escala em um circuito GHZ de 100 qubits. Executamos múltiplas tentativas de layout com diferentes seeds para as configurações SABRE, selecionamos o melhor circuito transpilado de cada uma e os submetemos todos ao hardware real junto com o resultado do StarPreRouting.
Passos 1-4 comprimidos em um único bloco de código
Aqui o fluxo de trabalho completo é montado em maior escala. Ao usar SabreSwap como routing_pass para SabreLayout, apenas uma tentativa de layout é realizada por chamada, portanto, a célula de código a seguir faz um loop sobre seeds para explorar o espaço de layout.
Usamos o mesmo auxiliar wrap_sabre definido no Passo 2 de pequena escala (acima), e adicionamos um auxiliar análogo wrap_routing porque o estágio routing no índice [1] também é um ConditionalController([BarrierBeforeFinalMeasurements, routing_pass], ...) — substituí-lo diretamente descartaria igualmente a barreira protetora e o condicionamento _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álise
Os gráficos de dispersão mostram variabilidade significativa entre seeds para as três heurísticas SABRE, o que ressalta a importância de executar múltiplas tentativas de layout em vez de depender de uma única transpilação. A linha do StarPreRouting é essencialmente plana entre seeds porque a reescrita de uma estrela em uma cadeia linear é determinística dada a estrutura; o roteamento SABRE posterior tem então muito pouca liberdade em uma cadeia linear, de forma que a seed tem quase nenhum efeito na profundidade ou tamanho final.
A partir dos resultados de transpilação, tanto as heurísticas decay quanto lookahead superam consistentemente basic por uma larga margem. A heurística basic, embora rápida, usa uma estratégia gulosa simples que muitas vezes leva a circuitos substancialmente mais profundos. Para este circuito GHZ de topologia em estrela, lookahead tende a produzir a menor profundidade 2Q e contagem de portas entre as heurísticas SABRE, já que sua função de custo prospectivo é adequada para circuitos com padrões de conectividade de longo alcance. O StarPreRouting, no entanto, eclipsa todos os três por uma margem substancial: ao reescrever a estrela em uma cadeia linear antes do roteamento, ele curto-circuita completamente o problema de busca e entrega um circuito que o restante do transpiler pode mapear em um caminho linear com mínimas SWAPs adicionais.
Essa vantagem se reflete diretamente na fidelidade do hardware. Menor profundidade 2Q e contagem de portas nem sempre se traduzem um-para-um em maior fidelidade (os qubits físicos específicos que um layout usa e sua calibração no momento de execução também importam), mas quando a lacuna de profundidade é tão grande quanto a existente entre SABRE e StarPreRouting aqui, a abordagem ciente da estrutura vence decisivamente porque o circuito acumula muito menos decoerência e muito menos eventos de erro de dois qubits. O gráfico de barras de fidelidade mostra StarPreRouting substancialmente à frente mesmo da melhor heurística SABRE, enquanto basic fica bem abaixo do resto porque seus circuitos muito mais profundos acumulam o maior erro.
Principais conclusões:
- Entre as heurísticas SABRE,
decayelookaheadsão substancialmente melhores do quebasicpara circuitos não triviais. Prefira uma das duas para cargas de trabalho em produção. - A melhor heurística SABRE depende do seu circuito e hardware. Testar múltiplas heurísticas com múltiplas seeds é a estratégia mais confiável.
- Se você quiser explorar ainda mais layouts, aumente
swap_trials(elayout_trialsquando você não está fixando um passe de roteamento personalizado) em vez de distribuir o trabalho para nós remotos. Os passes SABRE já paralelizam tentativas entre threads locais, e o trabalho por tentativa é pequeno o suficiente para que a sobrecarga de distribuição tipicamente domine qualquer aceleração. - Quando o circuito tem uma estrutura especial conhecida, aplicar um passe ciente da estrutura como
StarPreRoutingantes do SABRE pode entregar uma melhoria de ordem de magnitude que nenhuma quantidade de ajuste do SABRE conseguirá igualar. Isso não substitui o SABRE:StarPreRoutingsó ajuda quando o circuito realmente contém sub-circuitos em estrela e o backend tem um caminho linear longo o suficiente. Vale a pena verificar a biblioteca de passes por correspondências sempre que você conhece a forma do seu circuito.
Próximos passos
Se você achou este trabalho interessante, pode se interessar pelo seguinte material:
- Referência da API
SabreLayout: documentação completa dos parâmetros - Artigo SABRE: o algoritmo SABRE original para layout e roteamento
- Artigo LightSABRE: as melhorias algorítmicas que potencializam a implementação SABRE atual do Qiskit
- Escrever um passe de transpiler personalizado: construa sua própria lógica de transpilação
- Plugins de transpiler: estenda o pipeline de transpilação do Qiskit com passes de terceiros
- Representação DAG: compreenda o grafo acíclico direcionado usado internamente pelo transpiler
Pesquisa do tutorial
Por favor, responda a esta breve pesquisa para fornecer feedback sobre este tutorial. Seus insights nos ajudarão a melhorar nossas ofertas de conteúdo e experiência do usuário.
Nota: Esta pesquisa é da IBM Quantum e abrange o conteúdo do tutorial (escrito pela IBM). A doQumentation fornece o website, traduções e execução de código — para feedback sobre esses aspectos, por favor abra uma issue no GitHub.