Otimização da transpilação com o SABRE
Estimativa de tempo de execução: 1 minuto em um processador Heron r2 (NOTA: Trata-se apenas de uma estimativa. (O tempo de execução pode variar.)
Resultados do aprendizado
Depois de concluir este tutorial, você deverá compreender:
- Como configurar os parâmetros do SABRE (
layout_trials,swap_trials,max_iterations) para melhorar a qualidade da transpilagem - O equilíbrio entre o tempo de execução da transpilagem e a qualidade do circuito (profundidade e número 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 seguir com este tutorial:
- Circuitos de transpilação : visão geral da transpilação no Qiskit
- Etapas do transpiler : etapas de layout e roteamento
- Configurar gerenciadores de passagens predefinidos : personalização dos níveis de otimização
Segundo plano
A transpilação converte circuitos quânticos em formatos compatíveis com um hardware quântico específico. Duas etapas fundamentais são a escolha do layout dos qubits (mapeamento dos qubits lógicos para os qubits físicos) e o roteamento das portas (inserção de portas SWAP para que as portas de múltiplos qubits respeitem a conectividade do dispositivo).
O SABRE ( algoritmo de busca heurística bidirecional baseado em SWAP ) otimiza tanto o layout quanto o roteamento. É especialmente eficaz para circuitos de grande escala (mais de 100 qubits) em dispositivos com mapas de acoplamento complexos, como os processadores Heron d IBM®. O SABRE minimiza as portas SWAP e reduz a profundidade do circuito, melhorando a fidelidade de execução. As melhorias recentes no algoritmo “ LightSABRE ” reduzem ainda mais os tempos de execução e o número de portas lógicas.
Neste tutorial, você irá, em primeiro lugar, configurar SabreLayout o programa com diferentes parâmetros para otimizar um pequeno circuito GHZ e observar o impacto na fidelidade da execução. Em seguida, você comparará as heurísticas de roteamento do SABRE em grande escala em hardware real.
Requisitos
Antes de iniciar este tutorial, verifique se você tem os seguintes itens instalados:
- Qiskit SDK v2.0 ou versão posterior, com suporte à visualização
- Qiskit Runtime v0.22 ou posterior (
pip install qiskit-ibm-runtime) - Qiskit Aer (
pip install qiskit-aer)
Instalação
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}")Output:
Using backend: ibm_kingston
Exemplo de simulador em pequena escala
Nesta seção, utiliza-se um simulador com ruído baseado no modelo de ruído do backend real para demonstrar como diferentes SabreLayout configurações afetam tanto a qualidade da transpilagem quanto a fidelidade da execução. O uso de qiskit_aer com um modelo de ruído derivado de dados reais de calibração de hardware permite que você teste 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 com topologia em estrela com 15 qubits. O primeiro qubit é o nó central, com portas CNOT conectando-o diretamente a todos os outros qubits. Essa topologia cria um problema complexo de layout, pois não se corresponde de forma simples ao mapa de acoplamento do dispositivo.
Também definimos ZZ operadores para medir as correlações de entrelaçamento entre pares de qubits.
O SABRE é um algoritmo de uso geral e não faz suposições sobre a estrutura do circuito. Para esse circuito GHZ com topologia em estrela, já se conhece, de fato, um roteamento ideal: o StarPreRouting pass detecta os subcircuitos em estrela e os reescreve em uma cadeia linear que se mapeia diretamente para 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 possui uma estrutura especial bem definida, aplicar uma etapa especializada como StarPreRouting essa 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]Etapa 2: Otimizar o problema para execução em hardware quântico
SabreLayoutO gerenciador de passagens predefinido optimization_level=3 já utiliza isso, mas com valores padrão conservadores. Para analisar o impacto de configurações mais rigorosas, essa etapa é substituída por uma configurada sob SabreLayout medida para uma busca mais agressiva, enquanto todas as outras etapas da fase de layout permanecem inalteradas. Como ponto de comparação adicional, um quarto gerenciador de passagens mantém a configuração padrão SabreLayout , mas adiciona StarPreRouting à fase de inicialização. StarPreRouting é uma etapa que leva em conta a estrutura, que detecta subcircuitos em forma de estrela e os reescreve em uma cadeia linear antes do roteamento.
O fluxo de trabalho é o seguinte:
- Inspecione o gerenciador de passagens padrão para verificar onde
SabreLayoutele se encontra dentro dolayoutpalco. - Substitua essa passagem por uma instância personalizada
SabreLayoutusandoPassManager.replace(index, passes=...), e crie apm_starvariante compm.init += StarPreRouting(). - Execute todos os quatro gerenciadores de passagem e compare as métricas.
As quatro configurações são:
Configuração | Descrição |
|---|---|
pm_1 (padrão) | Predefinição padrão “ level-3 ” (SabreLayout com max_iterations=4, layout_trials=20, swap_trials=20) |
pm_2 | Personalizado SabreLayout (max_iterations=4, layout_trials=200, swap_trials=200) |
pm_3 | Personalizado SabreLayout (max_iterations=8, layout_trials=200, swap_trials=200) |
pm_star | Configuração padrão com StarPreRouting adicionada à fase de inicialização |
Parâmetros principais do SABRE:
layout_trials/swap_trials: Controla quantos layouts e soluções de roteamento possíveis o SABRE analisa. Aumentar o número de tentativas significa que o SABRE analisa um espaço de busca mais amplo, aumentando a chance de encontrar uma solução melhor.max_iterations: Determina quantos ciclos de refinamento de roteamento para frente e para trás o SABRE realiza em cada candidato. O SABRE aprimora o layout de forma iterativa, aprendendo com o feedback do roteamento; portanto, quanto mais iterações, melhores são as melhorias.
Ambas as opções implicam um tempo de transpilação mais longo, mas os circuitos resultantes são mais curtos e utilizam menos portas, o que reduz diretamente a decoerência e os erros de porta em hardware real.
Etapa 2a: Verifique o gerenciador de senhas padrão. A StagedPassManager é composta por estágios (init, layout, routing, translation, optimization, scheduling), sendo cada um deles um PassManager. A chamada .draw() de uma função no palco representa suas passagens como um gráfico, para que possamos ver onde SabreLayout ela está localizada.
# 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:
No diagrama acima, a SabreLayout passagem que queremos personalizar está localizada ConditionalController na posição [2] do estágio de layout. Esse controlador faz duas coisas:
- Ele funciona como um gate
SabreLayout, de modo que só é executado quandoVF2Layouto [1] não consegue encontrar um mapeamento perfeito (caso contrário, o layout perfeito VF2 é mantido). - Isso é precedido
SabreLayoutpor umaBarrierBeforeFinalMeasurementsetapa que evita que as medições sejam reordenadas durante o roteamento interno d SabreLayout's.
Se simplesmente replace(index=2, passes=sl_2), ambos os comportamentos são descartados. Para preservá-los, voltamos a embrulhar nosso produto personalizado SabreLayout no mesmo ConditionalController material (nas mesmas condições e com a barreira protetora) antes de colocá-lo no lugar.
Etapa 2b: : Crie passes personalizados SabreLayout e substitua os 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()Output:
A posição [2] agora é um ConditionalController novamente — de formato idêntico ao padrão, mas o interno SabreLayout é 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 _vf2_match_not_found sistema de portas são mantidos; portanto, a única diferença entre pm_2/pm_3 e pm_1 é a própria configuração do SABRE. pm_star mantém a configuração padrão SabreLayout e apenas adiciona StarPreRouting no final da fase de inicialização.
Etapa 2c: Execute cada gerenciador de passagem e compare os resultados.
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}%")Output:
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%
Todos os três gerenciadores de passagem modificados produziram circuitos com profundidade de 2Q menor do que a padrão. As configurações agressivas do SABRE (pm_2 e pm_3) sacrificam um tempo de transpilação mais longo em troca de uma busca mais ampla, enquanto pm_star aproveita a estrutura em estrela do circuito e produz um resultado ainda mais superficial sem incorrer em nenhum custo extra de transpilação. Os ganhos exatos variam de execução para execução, mas a tendência geral é consistente: mais ensaios e iterações do SABRE permitem que a busca heurística explore um espaço mais amplo, e passagens que levam em conta a estrutura, como StarPreRouting , podem contornar totalmente essa busca quando a forma do circuito coincide.
Mesmo nessa escala reduzida (15 qubits), a margem para melhorias é suficiente para que todas as três abordagens superem o resultado padrão. Com circuitos maiores (mais de 100 qubits), o espaço de busca cresce drasticamente e os benefícios tanto do aumento do número de tentativas quanto das passagens que levam em conta a estrutura tornam-se muito mais evidentes, como será mostrado na seção sobre grande 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()Output:
Passo 3: Execute usando Qiskit primitives
Executamos cada circuito transpilado 10 vezes usando o Aer EstimatorV2 com um modelo de ruído derivado do backend real. Como os resultados das simulações, que apresentam ruído, variam entre as execuções, calcular a média de várias execuções proporciona estimativas de fidelidade mais confiáveis e nos permite quantificar a incerteza estatística por meio de 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}"
)Output:
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 se trata de um circuito pequeno, os valores de fidelidade ficam relativamente próximos nas quatro configurações. Os circuitos são curtos o suficiente para que o ruído do hardware não prejudique significativamente nem mesmo a versão menos otimizada. A fidelidade média acompanha, de maneira geral, a profundidade d 2Q : pm_3 e pm_star, os dois circuitos mais rasos, alcançam as maiores fidelidades e estão praticamente empatados dentro de suas barras de erro. pm_2 é um contra-exemplo útil: embora sua profundidade de 2Q seja menor do que pm_1a de 's, sua fidelidade média acaba sendo ligeiramente menor também, o que nos lembra que a relação entre profundidade e fidelidade é estatística, e não determinística. Os qubits específicos selecionados por um layout e a calibração desses qubits durante a execução também são importantes.
Etapa 4: Pós-processamento e retorno do resultado no formato clássico desejado
Em seguida, represente graficamente as correlações de entrelaçamento em função da distância entre os qubits, juntamente com a correlação média como uma única métrica de fidelidade. Em um caso ideal (sem ruído), todas as correlações seriam 1. Devido ao ruído realista, cada porta adicional introduz erros e cada passo de tempo adicional permite a decoerência; portanto, um circuito transpilado com menor profundidade e menos portas (especialmente portas de dois qubits) deve preservar melhor o entrelaçamento.
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:
Os resultados mostram uma clara relação entre a qualidade da transpilagem e a fidelidade da execução, com algumas ressalvas importantes:
pm_1(padrão) : Linha de base. Com apenas 20 tentativas e quatro iterações, o SABRE tem margem limitada para otimização, o que resulta no circuito mais profundo entre aqueles gerados exclusivamente pelo SABRE.pm_2(mais testes) : Ao explorar dez vezes mais candidatos, obtém-se um layout ligeiramente menos profundo, mas a fidelidade média permanece praticamente estável (e pode até cair abaixo da linha de base devido ao ruído), pois o ganho de profundidade é pequeno nessa escala.pm_3(mais testes + mais iterações) : Ao dobrarmax_iterationso número para 8, o SABRE passa a ter mais ciclos de refinamento, produzindo o circuito mais superficial criado exclusivamente pelo SABRE e a maior fidelidade média na comparação.pm_star(padrão + StarPreRouting ) : AdicionaStarPreRoutingà fase de inicialização de uma predefinição que, de outra forma, seria padrão. A reescrita com reconhecimento de estrutura reduz o símbolo “*” a uma cadeia linear que o restante do transpiler mapeia para o caminho linear do dispositivo, produzindo o circuito mais raso no geral (ligeiramente melhor do quepm_3) e igualandopm_3em termos de fidelidade dentro das barras de erro. Isso é feito com o mesmo tempo de transpilagem que o 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 traz um impacto positivo. Nesse caso, isso ajudou significativamente, mas, para outros circuitos ou backends, as iterações adicionais podem não trazer melhorias adicionais ou podem até prejudicar ligeiramente o desempenho devido à otimização excessiva de um mínimo local. Em geral, você deve aumentar layout_trials e swap_trials tanto quanto seu tempo disponível permitir, já que um maior número de tentativas sempre aumenta a chance de encontrar um layout melhor. Vale a pena testar o aumento max_iterations , mas ele deve ser validado para o seu caso de uso específico. Passos especializados como StarPreRouting são semelhantes em princípio, mas dependem mais do circuito: eles só ajudam quando o circuito realmente contém a estrutura à qual se destinam. O ganho é grande quando aplicável e é zero nos demais casos, mas não custa praticamente nada tentar.
Exemplo de hardware em grande escala
Além de ajustar o número de tentativas, o SABRE permite personalizar a heurística de roteamento. O SABRE oferece três heurísticas:
basic: Uma abordagem gananciosa simples que seleciona a troca que minimiza a distância imediata até o próximo portão.decay(padrão): Atribui pesos dinamicamente aos qubits com base na atividade recente, evitando trocas repetidas nos mesmos qubits.lookahead: Avalia os custos futuros de roteamento, analisando antecipadamente os próximos pontos de passagem, a fim de identificar possíveis sequências de troca mais vantajosas.
Para usar uma heurística personalizada, crie uma SabreSwap passagem e conecte-a a SabreLayout por meio do routing_pass parâmetro.
SabreLayoutUm quarto gerenciador de passagens é adicionado à comparação: pm_star_hw, que mantém as configurações padrão,SabreSwap mas adiciona StarPreRouting à fase de inicialização. Nessa escala (100 qubits), a busca SABRE se torna mais complexa, e a reescrita de uma estrutura em estrela para uma cadeia linear se torna uma vantagem clara, pois um processador Heron possui caminhos lineares suficientemente longos para acomodar o circuito resultante.
Aqui, comparamos as três heurísticas do SABRE, além de StarPreRouting em escala, em um circuito GHZ de 100 qubits. Realizamos vários testes de layout com diferentes sementes para as configurações do SABRE, selecionamos o melhor circuito transpilado de cada um deles e os submetemos, todos, a hardware real juntamente com StarPreRouting o resultado.
Etapas 1 a 4 agrupadas em um único bloco de código
Aqui, todo o fluxo de trabalho é organizado em uma escala maior. Ao usar SabreSwap como para routing_pass SabreLayout, é realizada apenas uma tentativa de layout por chamada; portanto, a célula de código a seguir percorre as sementes em um loop para explorar o espaço de layout.
Usamos o mesmo wrap_sabre auxiliar definido na Etapa 2 em pequena escala (acima) e adicionamos um auxiliar análogo wrap_routing , pois o routing estágio no índice [1] também é um ConditionalController([BarrierBeforeFinalMeasurements, routing_pass], ...) — substituí-lo sem mais nada eliminaria, da mesma forma, a barreira protetora e o _swap_condition controle de acesso.
# -------------------------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']})"
)Output:
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:
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")Output:
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:
Mean fidelity:
basic: 0.0344
decay: 0.1298
lookahead: 0.1857
StarPreRouting: 0.3295
Análise
Os gráficos de dispersão mostram uma variabilidade significativa entre as sementes para todas as três heurísticas do SABRE, o que ressalta a importância de realizar vários testes de layout, em vez de se basear em uma única transpilação. A StarPreRouting linha permanece essencialmente plana em todas as sementes, pois a reescrita de uma estrutura em estrela para uma cadeia linear é determinística, dada a estrutura; o roteamento SABRE posterior, portanto, tem muito pouca liberdade em uma cadeia linear, de modo que a semente quase não tem efeito sobre a profundidade ou o tamanho finais.
A partir dos resultados da transpilagem, tanto a decay heurística quanto lookahead a superam consistentemente a basic por uma ampla margem. A basic heurística, embora rápida, utiliza uma estratégia gananciosa simples que muitas vezes resulta em circuitos substancialmente mais profundos. Para esse circuito GHZ com topologia em estrela, lookahead tende a produzir a menor profundidade de 2Q e e o menor número de portas entre as heurísticas SABRE, uma vez que sua função de custo prospectiva é bem adequada para circuitos com padrões de conectividade de longo alcance. StarPreRouting, No entanto, supera os três por uma margem substancial: ao reescrever a estrela em uma cadeia linear antes do roteamento, ele contorna completamente o problema de busca e gera um circuito que o restante do transpiler pode mapear para um caminho linear com um número mínimo de SWAPs adicionais.
Essa vantagem se reflete diretamente na fidelidade do hardware. Uma menor profundidade de “ 2Q ” e um número menor de portas nem sempre se traduzem diretamente em maior fidelidade (os qubits físicos específicos que um layout utiliza e sua calibração em tempo de execução também são importantes), mas quando a diferença de profundidade é tão grande quanto a existente entre o SABRE e StarPreRouting este caso, a abordagem que leva em conta a estrutura se destaca de forma decisiva, pois 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 que está substancialmente à frente até mesmo da melhor heurística do SABRE, enquanto basic fica bem abaixo dos demais, pois seus circuitos, muito mais complexos, acumulam o maior número de erros.
Principais conclusões:
- Entre as heurísticas do SABRE,
decayelookaheadsão substancialmente melhores do quebasicpara circuitos não triviais. Opte por uma das duas opções para cargas de trabalho de produção. - A melhor heurística do SABRE depende do seu circuito e do seu hardware. Testar várias heurísticas com várias sementes é a estratégia mais confiável.
- Se você quiser explorar ainda mais layouts, aumente
swap_trials(elayout_trialsquando não estiver definindo uma passagem de roteamento personalizada), em vez de distribuir o trabalho para nós remotos. Os passes do SABRE já paralelizam os testes entre threads locais, e o trabalho por teste é pequeno o suficiente para que a sobrecarga da distribuição normalmente supere qualquer ganho de velocidade. - Quando o circuito possui uma estrutura especial conhecida, a aplicação de uma etapa que leve em conta essa estrutura — como
StarPreRoutingantes do SABRE — pode proporcionar uma melhoria de uma ordem de magnitude que nenhum ajuste no SABRE será capaz de igualar. Isso não substitui o SABRE:StarPreRoutingsó ajuda quando o circuito realmente contém subcircuitos em estrela e o backend possui um caminho linear suficientemente longo. Vale a pena verificar se há correspondências na biblioteca de passagens sempre que você souber a forma do seu circuito.
Próximas etapas
Se você achou este trabalho interessante, talvez se interesse pelo material a seguir:
SabreLayoutReferência da API : documentação completa dos parâmetros- Artigo sobre o SABRE : o algoritmo SABRE original para layout e roteamento
- LightSABRE artigo : as melhorias algorítmicas que sustentam a atual implementação do SABRE no Qiskit
- Escreva uma etapa personalizada do transpiler : crie sua própria lógica de transpilagem
- Plug-ins de transpilação : ampliem o pipeline de transpilação do Qiskit com etapas de terceiros
- Representação DAG : compreender o grafo acíclico direcionado utilizado internamente pelo transpiler
Pesquisa tutorial
Responda a esta breve pesquisa para fornecer feedback sobre este tutorial. Suas percepções nos ajudarão a melhorar nossas ofertas de conteúdo e a experiência do usuário.