אופטימיזציית טרנספילציה עם SABRE
הערכת שימוש: דקה אחת על מעבד Heron r2 (הערה: זוהי הערכה בלבד. זמן הריצה שלך עשוי להשתנות.)
תוצאות לימוד
לאחר עבור מדריך זה, תבינו:
- כיצד להגדיר פרמטרי SABRE (
layout_trials,swap_trials,max_iterations) לשיפור איכות הטרנספילציה - את הפשרות בין זמן ריצת הטרנספילציה לאיכות המעגל (עומק וספירת Gates)
- כיצד להתאים אישית את ההיוריסטיקה של ניתוב SABRE (
basic,decay,lookahead) ולהשוות את ביצועיהן על חומרה
דרישות מוקדמות
אנחנו ממליצים שתכירו את הנושאים הבאים לפני עבור מדריך זה:
- טרנספילציה של מעגלים: סקירה כללית של טרנספילציה ב-Qiskit
- שלבי הטרנספילר: שלבי פריסה וניתוב
- הגדרת מנהלי מעבר מוכנים מראש: התאמה אישית של רמות אופטימיזציה
רקע
טרנספילציה ממירה מעגלים קוונטיים לצורות תואמות לחומרה קוונטית ספציפית. שני שלבים מרכזיים הם בחירת פריסת qubit (מיפוי qubits לוגיים ל-qubits פיזיים) וניתוב Gate (הוספת Gates מסוג SWAP כך שפעולות רב-Qubit יכבדו את קישוריות המכשיר).
SABRE (אלגוריתם חיפוש היוריסטי דו-כיווני מבוסס SWAP) מייעל גם פריסה וגם ניתוב. הוא יעיל במיוחד עבור מעגלים בקנה מידה גדול (100+ qubits) על מכשירים עם מפות חיבור מורכבות, כמו מעבדי IBM® Heron. SABRE ממזעז Gates מסוג SWAP ומקצר את עומק המעגל, ומשפר את נאמנות הביצוע. שיפורים עדכניים באלגוריתם LightSABRE מפחיתים עוד יותר את זמני הריצה ואת ספירת ה-Gates.
במדריך זה, תגדירו תחילה את SabreLayout עם פרמטרים שונים לאופטימיזציה של מעגל GHZ קטן ותצפו בהשפעה על נאמנות הביצוע. לאחר מכן, תשוו את ההיוריסטיקות לניתוב של SABRE בקנה מידה על חומרה אמיתית.
דרישות
לפני תחילת מדריך זה, ודאו שהדברים הבאים מותקנים:
- Qiskit SDK גרסה v2.0 ומעלה, עם תמיכת ויזואליזציה
- Qiskit Runtime גרסה v0.22 ומעלה (
pip install qiskit-ibm-runtime) - Qiskit Aer (
pip install qiskit-aer)
הגדרה
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import EstimatorOptions
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_aer.primitives import EstimatorV2 as AerEstimator
from qiskit.transpiler.passes import (
SabreLayout,
SabreSwap,
BarrierBeforeFinalMeasurements,
StarPreRouting,
)
from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.passmanager.flow_controllers import ConditionalController
import matplotlib.pyplot as plt
import numpy as np
import time
seed = 42
service = QiskitRuntimeService(
channel="ibm_cloud",
token="<YOUR_API_TOKEN>", # Replace with your actual API token
instance="<YOUR_INSTANCE_NAME>", # Replace with your instance name if needed
)
backend = service.least_busy(operational=True, simulator=False)
print(f"Using backend: {backend.name}")
Using backend: ibm_kingston
דוגמה בקנה מידה קטן עם סימולטור
בחלק זה, סימולטור רועש מבוסס מודל הרעש של ה-Backend האמיתי משמש להדגמת כיצד הגדרות שונות של SabreLayout משפיעות הן על איכות הטרנספילציה והן על נאמנות הביצוע. שימוש ב-qiskit_aer עם מודל רעש שנגזר מנתוני כיול חומרה ממשית מאפשר לכם לבדוק את הטרנספילציה מבלי לצרוך קרדיטים של חומרה.
שלב 1: מיפוי קלטים קלאסיים לבעיה קוונטית
אנחנו בונים מעגל GHZ בטופולוגיית כוכב עם 15 qubits. ה-Qubit הראשון הוא הציר, עם Gates מסוג CNOT המחברים אותו ישירות לכל Qubit אחר. טופולוגיה זו יוצרת בעיית פריסה מאתגרת מכיוון שאינה ממופה בצורה טריוויאלית למפת החיבור של המכשיר.
אנחנו גם מגדירים אופרטורי ZZ למדידת מתאמי שזירה בין זוגות qubits.

SABRE הוא אלגוריתם למטרות כלליות ואינו מניח דבר לגבי מבנה המעגל. עבור מעגל GHZ בטופולוגיית כוכב זה, ניתוב אופטימלי ידוע בפועל: המעבר StarPreRouting מזהה תת-מעגלים בצורת כוכב וכותב אותם מחדש לשרשרת לינארית הממופה ישירות על כל Backend בעל נתיב לינארי ארוך מספיק. מדריך זה מתמקד ב-SABRE מכיוון שהוא עובד עבור מעגלים שרירותיים, אבל אם אתם יודעים שלמעגל שלכם יש מבנה מיוחד ברור, יישום מעבר מיוחד כמו StarPreRouting לפני הניתוב יכול להיות עדיף על כל חיפוש היוריסטי.
num_qubits_sim = 15
# Create star-topology GHZ circuit
qc_sim = QuantumCircuit(num_qubits_sim)
qc_sim.h(0)
for i in range(1, num_qubits_sim):
qc_sim.cx(0, i)
qc_sim.measure_all()
# ZZ operators: Z on qubit 0 and qubit i, identity elsewhere
operator_strings_sim = [
"Z" + "I" * i + "Z" + "I" * (num_qubits_sim - 2 - i)
for i in range(num_qubits_sim - 1)
]
operators_sim = [SparsePauliOp(op) for op in operator_strings_sim]
שלב 2: אופטימיזציה של הבעיה לביצוע על חומרה קוונטית
מנהל המעבר המוכן מראש עם ברירת מחדל optimization_level=3 כבר משתמש ב-SabreLayout, אך עם ברירות מחדל שמרניות. כדי לחקור את ההשפעה של הגדרות חזקות יותר, מעבר זה מוחלף במופע SabreLayout מותאם אישית שמוגדר לחיפוש אגרסיבי יותר, בעוד שכל שאר המעברים בשלב הפריסה נשארים ללא שינוי. כנקודת השוואה נפרדת, מנהל מעבר רביעי שומר על SabreLayout ברירת המחדל אך מוסיף StarPreRouting לשלב ה-init. StarPreRouting הוא מעבר מודע-למבנה שמזהה תת-מעגלים בצורת כוכב וכותב אותם מחדש לשרשרת לינארית לפני הניתוב.
תהליך העבודה הוא:
- בחינה של מנהל המעבר הדיפולטי לראות היכן
SabreLayoutיושב בתוך שלב ה-layout. - החלפה של אותו מעבר במופע
SabreLayoutמותאם אישית באמצעותPassManager.replace(index, passes=...), ובנייה של גרסתpm_starעםpm.init += StarPreRouting(). - הרצה של כל ארבעת מנהלי המעברים והשוואת מדדים.
ארבע ההגדרות הן:
| הגדרה | תיאור |
|---|---|
pm_1 (ברירת מחדל) | רמה 3 מוכנה מראש (SabreLayout עם max_iterations=4, layout_trials=20, swap_trials=20) |
pm_2 | SabreLayout מותאם אישית (max_iterations=4, layout_trials=200, swap_trials=200) |
pm_3 | SabreLayout מותאם אישית (max_iterations=8, layout_trials=200, swap_trials=200) |
pm_star | רמה מוכנה מראש עם StarPreRouting שנוסף לשלב ה-init |
פרמטרי SABRE מרכזיים:
layout_trials/swap_trials: שולטים בכמה פריסות מועמדות ופתרונות ניתוב SABRE חוקר. הגדלת מספר הניסויים פירושה ש-SABRE דוגם מרחב חיפוש רחב יותר, ומגדיל את הסיכוי למצוא פתרון טוב יותר.max_iterations: שולט בכמה מחזורי שכלול ניתוב קדימה-אחורה SABRE מבצע על כל מועמד. SABRE משפר את הפריסה בצורה איטרטיבית תוך למידה ממשוב הניתוב, לכן יותר איטרציות מובילות לשיפורים טובים יותר.
שניהם באים במחיר של זמן טרנספילציה ארוך יותר, אך המעגלים שנוצרים קצרים יותר ומשתמשים בפחות Gates, מה שמפחית ישירות את הדה-קוהרנציה ושגיאות ה-Gate על חומרה אמיתית.
שלב 2א: בחינת מנהל המעבר הדיפולטי. StagedPassManager מורכב משלבים (init, layout, routing, translation, optimization, scheduling), כשכל אחד הוא עצמו PassManager. קריאה ל-.draw() על שלב מציגה את המעברים שלו כגרף כדי שנוכל לראות היכן SabreLayout יושב.
# Build the default pass manager (no modifications yet)
pm_1 = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=seed
)
# Visualize the layout stage to see where SabreLayout sits
pm_1.layout.draw()

בדיאגרמה למעלה, המעבר SabreLayout שאנחנו רוצים להתאים אישית יושב בתוך ה-ConditionalController במיקום [2] של שלב הפריסה. בקר זה עושה שני דברים:
- הוא מגדר את
SabreLayoutכך שירוץ רק כאשרVF2Layoutב-[1] לא הצליח למצוא מיפוי מושלם (אחרת הפריסה המושלמת של VF2 נשמרת). - הוא מקדים את
SabreLayoutבמעברBarrierBeforeFinalMeasurementsשמגן על מדידות מפני סידור מחדש במהלך הניתוב הפנימי של SabreLayout.
אם פשוט נעשה replace(index=2, passes=sl_2), שני ההתנהגויות האלה יאבדו. כדי לשמור אותן, אנחנו עוטפים מחדש את ה-SabreLayout המותאם שלנו באותו ConditionalController (עם אותה תנאי והמחסום המגן) לפני שמחליפים אותו.
שלב 2ב: בניית מעברי SabreLayout מותאמים אישית והחלפת ברירת המחדל.
cmap = backend.coupling_map
# Custom SabreLayout passes with more aggressive search
sl_2 = SabreLayout(
coupling_map=cmap,
seed=seed,
max_iterations=4,
layout_trials=200,
swap_trials=200,
)
sl_3 = SabreLayout(
coupling_map=cmap,
seed=seed,
max_iterations=8,
layout_trials=200,
swap_trials=200,
)
# Same condition the preset uses: only run SabreLayout when VF2Layout did not
# find a perfect mapping. This preserves any perfect layout VF2 produced at [1].
def _vf2_match_not_found(property_set):
if property_set["layout"] is None:
return True
return (
property_set["VF2Layout_stop_reason"] is not None
and property_set["VF2Layout_stop_reason"]
is not VF2LayoutStopReason.SOLUTION_FOUND
)
def wrap_sabre(sabre_pass):
"""Re-wrap a SabreLayout in the original ConditionalController + barrier."""
return ConditionalController(
[
BarrierBeforeFinalMeasurements(
"qiskit.transpiler.internal.routing.protection.barrier"
),
sabre_pass,
],
condition=_vf2_match_not_found,
)
# Build two fresh pass managers and swap in the wrapped custom SabreLayout at index 2
pm_2 = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=seed
)
pm_3 = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=seed
)
pm_2.layout.replace(index=2, passes=wrap_sabre(sl_2))
pm_3.layout.replace(index=2, passes=wrap_sabre(sl_3))
# Build pm_star: default preset with StarPreRouting added to the init stage
pm_star = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=seed
)
pm_star.init += StarPreRouting()
# Visualize pm_3 after replacement (pm_2 has the same structure, only max_iterations differs)
pm_3.layout.draw()

מיקום [2] הוא עכשיו שוב ConditionalController — זהה בצורה לברירת המחדל, אך ה-SabreLayout הפנימי הוא המותאם אישית שלנו (עם layout_trials=200, swap_trials=200, ו-max_iterations=8 עבור pm_3; pm_2 זהה פרט ל-max_iterations=4). המחסום המגן והמסכה _vf2_match_not_found נשמרים, כך שההבדל היחיד בין pm_2/pm_3 לבין pm_1 הוא תצורת SABRE עצמה. pm_star שומר על SabreLayout ברירת המחדל ורק מוסיף StarPreRouting בסוף שלב ה-init.
שלב 2ג: הרצת כל מנהל מעבר והשוואה.
results_sim = {}
for name, pm in [
("pm_1 (4,20,20)", pm_1),
("pm_2 (4,200,200)", pm_2),
("pm_3 (8,200,200)", pm_3),
("pm_star (default + StarPreRouting)", pm_star),
]:
t0 = time.time()
tqc = pm.run(qc_sim)
elapsed = time.time() - t0
depth = tqc.depth(lambda x: x.operation.num_qubits == 2)
size = tqc.size()
ops_mapped = [op.apply_layout(tqc.layout) for op in operators_sim]
results_sim[name] = {
"tqc": tqc,
"ops": ops_mapped,
"depth": depth,
"size": size,
"time": elapsed,
}
print(f"{name}: 2Q Depth {depth}, Size {size}, Time {elapsed:.2f}s")
# Print improvement relative to default (pm_1)
baseline = results_sim["pm_1 (4,20,20)"]
print("\nImprovement vs. default (pm_1):")
for name in [
"pm_2 (4,200,200)",
"pm_3 (8,200,200)",
"pm_star (default + StarPreRouting)",
]:
r = results_sim[name]
depth_pct = (baseline["depth"] - r["depth"]) / baseline["depth"] * 100
size_pct = (baseline["size"] - r["size"]) / baseline["size"] * 100
print(f" {name}: 2Q depth {depth_pct:+.1f}%, size {size_pct:+.1f}%")
pm_1 (4,20,20): 2Q Depth 38, Size 183, Time 0.01s
pm_2 (4,200,200): 2Q Depth 36, Size 183, Time 0.15s
pm_3 (8,200,200): 2Q Depth 30, Size 158, Time 0.16s
pm_star (default + StarPreRouting): 2Q Depth 26, Size 160, Time 0.01s
Improvement vs. default (pm_1):
pm_2 (4,200,200): 2Q depth +5.3%, size +0.0%
pm_3 (8,200,200): 2Q depth +21.1%, size +13.7%
pm_star (default + StarPreRouting): 2Q depth +31.6%, size +12.6%
כל שלושת מנהלי המעברים המשונים ייצרו מעגלים עם עומק 2Q נמוך יותר מברירת המחדל. תצורות SABRE האגרסיביות (pm_2 ו-pm_3) מחליפות זמן טרנספילציה ארוך יותר בחיפוש רחב יותר, בעוד ש-pm_star מנצל את מבנה הכוכב של המעגל ומייצר תוצאה רדודה אף יותר מבלי לשלם שום עלות טרנספילציה נוספת. הרווחים המדויקים ישתנו מהרצה להרצה, אך המגמה הכללית עקבית: יותר ניסויים ואיטרציות SABRE מאפשרות לחיפוש ההיוריסטי לחקור מרחב רחב יותר, ומעברים מודעי-מבנה כמו StarPreRouting יכולים לעקוף לגמרי את החיפוש הזה כאשר צורת המעגל מתאימה.
אפילו בקנה מידה קטן זה (15 qubits), הפוטנציאל לשיפור גדול מספיק כך ששלושת הגישות עוקפות את ברירת המחדל. עם מעגלים גדולים יותר (100+ qubits), מרחב החיפוש גדל דרמטית והיתרונות של גם הגדלת ניסויים וגם מעברים מודעי-מבנה הופכים להרבה יותר בולטים, כפי שיוצג בחלק הקנה מידה גדול.
pm_names = list(results_sim.keys())
depths = [results_sim[n]["depth"] for n in pm_names]
sizes = [results_sim[n]["size"] for n in pm_names]
times = [results_sim[n]["time"] for n in pm_names]
colors = ["#404080", "#2a9d8f", "#a8d05e", "#e29bdd"]
x = np.arange(len(pm_names))
fig, axs = plt.subplots(1, 3, figsize=(14, 5))
# 2Q Depth
bars = axs[0].bar(x, depths, color=colors)
axs[0].set_ylabel("2Q Depth", fontsize=11)
axs[0].set_title("Two-Qubit Gate Depth", fontsize=13)
axs[0].set_ylim(0, max(depths) * 1.2)
for bar, val in zip(bars, depths):
axs[0].text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + max(depths) * 0.02,
str(val),
ha="center",
va="bottom",
fontsize=11,
fontweight="bold",
)
for i in range(1, len(depths)):
pct = (depths[0] - depths[i]) / depths[0] * 100
if pct != 0:
axs[0].text(
bars[i].get_x() + bars[i].get_width() / 2,
bars[i].get_height() / 2,
f"{pct:+.0f}%",
ha="center",
va="center",
fontsize=10,
color="white",
fontweight="bold",
)
# Size
bars = axs[1].bar(x, sizes, color=colors)
axs[1].set_ylabel("Gate Count", fontsize=11)
axs[1].set_title("Circuit Size", fontsize=13)
axs[1].set_ylim(0, max(sizes) * 1.2)
for bar, val in zip(bars, sizes):
axs[1].text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + max(sizes) * 0.02,
str(val),
ha="center",
va="bottom",
fontsize=11,
fontweight="bold",
)
for i in range(1, len(sizes)):
pct = (sizes[0] - sizes[i]) / sizes[0] * 100
if abs(pct) > 0.1:
axs[1].text(
bars[i].get_x() + bars[i].get_width() / 2,
bars[i].get_height() / 2,
f"{pct:+.0f}%",
ha="center",
va="center",
fontsize=10,
color="white",
fontweight="bold",
)
# Time
bars = axs[2].bar(x, times, color=colors)
axs[2].set_ylabel("Time (s)", fontsize=11)
axs[2].set_title("Transpilation Time", fontsize=13)
axs[2].set_ylim(0, max(times) * 1.3)
for bar, val in zip(bars, times):
axs[2].text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + max(times) * 0.03,
f"{val:.2f}s",
ha="center",
va="bottom",
fontsize=11,
fontweight="bold",
)
for ax in axs:
ax.set_xticks(x)
ax.set_xticklabels(pm_names, fontsize=8, rotation=15)
ax.grid(axis="y", linestyle="--", alpha=0.5)
plt.suptitle(
"Transpilation quality vs. configuration",
fontsize=14,
fontweight="bold",
y=1.02,
)
plt.tight_layout()
plt.show()

שלב 3: ביצוע באמצעות Qiskit primitives
אנחנו מריצים כל מעגל מטורנספל 10 פעמים באמצעות EstimatorV2 של Aer עם מודל רעש שנגזר מה-Backend האמיתי. מאחר שתוצאות הסימולציה הרועשת משתנות בין הרצות, ממוצע על פני ריצות מרובות מניב הערכות נאמנות אמינות יותר ומאפשר לכמת את אי-הוודאות הסטטיסטית עם פסי שגיאה.
# Create a noisy estimator from the real backend's noise model
noisy_estimator = AerEstimator.from_backend(backend)
num_runs = 10
# sim_all_runs[name] = list of arrays, one per run
sim_all_runs = {name: [] for name in results_sim}
for run in range(num_runs):
for name, r in results_sim.items():
job = noisy_estimator.run([(r["tqc"], r["ops"])])
evs = list(job.result()[0].data.evs)
sim_all_runs[name].append(evs)
print(f"Run {run + 1}/{num_runs} done")
# Compute mean and std across runs for each config
sim_stats = {}
for name in results_sim:
all_evs = np.array(sim_all_runs[name]) # shape (num_runs, num_operators)
sim_stats[name] = {
"mean": np.mean(all_evs, axis=0),
"std": np.std(all_evs, axis=0),
"overall_mean": np.mean(all_evs),
"overall_std": np.std(
np.mean(all_evs, axis=1)
), # std of per-run averages
}
print(
f"{name}: mean fidelity = {sim_stats[name]['overall_mean']:.4f} +/- {sim_stats[name]['overall_std']:.4f}"
)
Run 1/10 done
Run 2/10 done
Run 3/10 done
Run 4/10 done
Run 5/10 done
Run 6/10 done
Run 7/10 done
Run 8/10 done
Run 9/10 done
Run 10/10 done
pm_1 (4,20,20): mean fidelity = 0.9510 +/- 0.0094
pm_2 (4,200,200): mean fidelity = 0.9513 +/- 0.0043
pm_3 (8,200,200): mean fidelity = 0.9540 +/- 0.0065
pm_star (default + StarPreRouting): mean fidelity = 0.9547 +/- 0.0072
מאחר שמדובר במעגל קטן, ערכי הנאמנות קרובים יחסית בין כל ארבע ההגדרות. המעגלים קצרים מספיק כך שרעש החומרה אינו מעניש בכבדות אפילו את הגרסה פחות מאופטמת. ממוצע הנאמנות עוקב בגסות אחרי עומק ה-2Q: pm_3 ו-pm_star, שני המעגלים הרדודים ביותר, משיגים את הנאמנויות הגבוהות ביותר וקשורים בעצם בתוך פסי השגיאה שלהם. pm_2 הוא דוגמה-נגד שימושית: למרות שעומק ה-2Q שלו נמוך מזה של pm_1, ממוצע הנאמנות שלו בסופו של דבר נמוך מעט גם הוא, מה שמזכיר לנו שהקשר עומק-נאמנות הוא סטטיסטי ולא דטרמיניסטי. ה-qubits הפיזיים שפריסה בוחרת וכיול אותם qubits בזמן הריצה משנים גם הם.
שלב 4: עיבוד לאחר ואיחזור תוצאה בפורמט קלאסי רצוי
בשלב הבא, נציג את מתאמי השזירה כפונקציה של מרחק ה-Qubit, יחד עם מתאם הממוצע כמדד נאמנות יחיד. במקרה אידיאלי (ללא רעש), כל המתאמים יהיו 1. עם רעש ריאלי, כל Gate נוסף מכניס שגיאה וכל שלב זמן נוסף מאפשר דה-קוהרנציה, לכן מעגל מטורנספל עם עומק נמוך יותר ופחות Gates (במיוחד Gates דו-Qubit) אמור לשמר שזירה טוב יותר.
data_sim = list(range(1, len(operators_sim) + 1))
markers = ["o", "s", "^", "*"]
colors_line = ["#404080", "#2a9d8f", "#a8d05e", "#e29bdd"]
fig, (ax1, ax2) = plt.subplots(
1, 2, figsize=(14, 5), gridspec_kw={"width_ratios": [2.5, 1]}
)
# Left: correlations vs distance with error bars (mean +/- 1 std)
for (name, stats), marker, color in zip(
sim_stats.items(), markers, colors_line
):
ax1.errorbar(
data_sim,
stats["mean"],
yerr=stats["std"],
marker=marker,
label=name,
color=color,
linewidth=2,
capsize=3,
capthick=1,
elinewidth=1,
)
ax1.set_xlabel("Distance between qubits $i$", fontsize=11)
ax1.set_ylabel(r"$\langle Z_0 Z_i \rangle$", fontsize=11)
ax1.set_title(
"Entanglement correlations vs. qubit distance (avg. of 10 runs)",
fontsize=12,
)
ax1.legend(fontsize=9)
ax1.grid(alpha=0.3)
# Right: mean correlation bar chart with error bars
names = list(sim_stats.keys())
means = [sim_stats[n]["overall_mean"] for n in names]
stds = [sim_stats[n]["overall_std"] for n in names]
x_bar = np.arange(len(names))
bars = ax2.bar(
x_bar, means, yerr=stds, color=colors_line, capsize=5, ecolor="gray"
)
ax2.set_ylabel(r"Mean $\langle Z_0 Z_i \rangle$", fontsize=11)
ax2.set_title("Average fidelity", fontsize=13, pad=12)
y_range = max(means) - min(means) if max(means) != min(means) else 0.01
# Top of ylim accounts for the bar height + std error bar + headroom for the value label
y_top = max(m + s for m, s in zip(means, stds)) + y_range * 1.5
ax2.set_ylim(min(means) - y_range * 0.8, y_top)
for bar, val, std in zip(bars, means, stds):
ax2.text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + std + y_range * 0.15,
f"{val:.4f}",
ha="center",
va="bottom",
fontsize=10,
fontweight="bold",
)
# Annotate % change vs pm_1
baseline_mean = means[0]
for i in range(1, len(means)):
pct = (means[i] - baseline_mean) / baseline_mean * 100
if abs(pct) > 0.01:
mid_y = (means[i] + ax2.get_ylim()[0]) / 2
ax2.text(
bars[i].get_x() + bars[i].get_width() / 2,
mid_y,
f"{pct:+.1f}%",
ha="center",
va="center",
fontsize=10,
color="white",
fontweight="bold",
)
ax2.set_xticks(x_bar)
ax2.set_xticklabels(names, fontsize=8, rotation=15)
ax2.grid(axis="y", linestyle="--", alpha=0.5)
fig.tight_layout()
plt.show()

התוצאות מראות קשר ברור בין איכות הטרנספילציה לנאמנות הביצוע, עם כמה הסתייגויות שימושיות:
pm_1(ברירת מחדל): קו בסיס. עם רק 20 ניסויים וארבע איטרציות, ל-SABRE יש מקום מוגבל לאופטימיזציה, וזה מוביל לעמוק ביותר מבין מעגלי SABRE בלבד.pm_2(יותר ניסויים): חקר פי עשרה יותר מועמדים מוצא פריסה מעט רדודה יותר, אך ממוצע הנאמנות בערך שטוח (ואפילו עלול לצנוח מתחת לקו הבסיס בתוך רעש) מכיוון שהרווח בעומק קטן בקנה מידה זה.pm_3(יותר ניסויים + יותר איטרציות): הכפלתmax_iterationsל-8 מעניקה ל-SABRE יותר מחזורי שכלול, ומייצרת את המעגל הרדוד ביותר בין מעגלי SABRE בלבד ואת ממוצע הנאמנות הגבוה ביותר בהשוואה.pm_star(ברירת מחדל + StarPreRouting): מוסיףStarPreRoutingלשלב ה-init של פרסט ברירת מחדל אחרת. שכתוב המודע-למבנה מכווץ את הכוכב לשרשרת לינארית שהמשך הטרנספילר ממפה על הנתיב הלינארי של המכשיר, ומייצר את המעגל הרדוד ביותר בכלל (מעט טוב יותר מ-pm_3) ומשתווה ל-pm_3בנאמנות בתוך פסי שגיאה. הוא מגיע לכך עם אותו זמן טרנספילציה כמו ברירת המחדל, מכיוון שהשכתוב זול בעצם בהשוואה לחיפוש הסטוכסטי של SABRE.
שימו לב שהגדלת max_iterations לא תמיד משפיעה לחיוב. במקרה זה היא עזרה משמעותית, אך עבור מעגלים או Backends אחרים האיטרציות הנוספות עשויות שלא להניב שיפור נוסף, ואפילו עלולות לפגוע מעט בביצועים בשל אופטימיזציית יתר של מינימום מקומי. בכלל, כדאי להגדיל את layout_trials ו-swap_trials ככל שתקציב הזמן שלכם מאפשר, מאחר שיותר ניסויים תמיד מגדילים את הסיכוי למצוא פריסה טובה יותר. הגדלת max_iterations שווה בדיקה אך צריכה לעבור אימות עבור מקרה השימוש הספציפי שלכם. מעברים מיוחדים כמו StarPreRouting דומים ברוחם אך תלויים יותר במעגל: הם עוזרים רק כאשר המעגל מכיל בפועל את המבנה שהם מכוונים אליו. הרווח גדול כשרלוונטי ואפס אחרת, אבל הם עולים כמעט כלום לנסות.
דוגמה בקנה מידה גדול על חומרה
בנוסף להתאמת מספר הניסויים, SABRE תומך בהתאמה אישית של ההיוריסטיקה לניתוב. SABRE מציע שלוש היוריסטיקות:
basic: גישה חמדנית פשוטה שבוחרת את ה-SWAP שממזערת את המרחק המיידי ל-Gate הבא.decay(ברירת מחדל): משקלת qubits דינמית בהתבסס על פעילות אחרונה, מה שמרתיע החלפות חוזרות על אותם qubits.lookahead: מעריך עלויות ניתוב עתידיות על ידי הסתכלות קדימה על Gates קרובים, ועשוי למצוא רצפי החלפה טובים יותר.
כדי להשתמש בהיוריסטיקה מותאמת אישית, יוצרים מעבר SabreSwap ומחברים אותו ל-SabreLayout דרך הפרמטר routing_pass.
מנהל מעבר רביעי מתווסף להשוואה: pm_star_hw, ששומר על הגדרות SabreLayout/SabreSwap ברירת המחדל אך מוסיף StarPreRouting לשלב ה-init. בקנה מידה זה (100 qubits) חיפוש SABRE קשה יותר, והשכתוב מכוכב לשרשרת לינארית הופך לניצחון ברור מכיוון שלמעבד Heron יש נתיבים לינאריים ארוכים מספיק לאירוח המעגל שנוצר.
כאן אנחנו משווים את כל שלוש ההיוריסטיקות של SABRE בתוספת StarPreRouting בקנה מידה על מעגל GHZ בן 100 qubits. אנחנו מריצים ניסויי פריסה מרובים עם Seeds שונים עבור תצורות SABRE, בוחרים את המעגל המטורנספל הטוב ביותר מכל אחת, ומגישים את כולם לחומרה אמיתית לצד תוצאת StarPreRouting.
שלבים 1-4 מוכנסים לתוך בלוק קוד יחיד
כאן תהליך העבודה המלא מורכב בקנה מידה גדול יותר. בשימוש ב-SabreSwap כ-routing_pass עבור SabreLayout, מתבצע רק ניסיון פריסה אחד לכל קריאה, לכן תא הקוד הבא עובר על Seeds לחקר מרחב הפריסה.
אנחנו משתמשים באותו עזר wrap_sabre שהוגדר בשלב 2 הקנה-מידה-הקטן (למעלה), ומוסיפים עזר מקביל wrap_routing מכיוון שמעבר ה-routing באינדקס [1] הוא גם ConditionalController([BarrierBeforeFinalMeasurements, routing_pass], ...) — החלפתו ישירה תפיל באופן דומה את המחסום המגן ואת מסכת _swap_condition.
# -------------------------Step 1-------------------------
num_qubits = 100
# Create star-topology GHZ circuit
qc = QuantumCircuit(num_qubits)
qc.h(0)
for i in range(1, num_qubits):
qc.cx(0, i)
qc.measure_all()
# ZZ operators
operator_strings = [
"Z" + "I" * i + "Z" + "I" * (num_qubits - 2 - i)
for i in range(num_qubits - 1)
]
operators = [SparsePauliOp(op) for op in operator_strings]
# -------------------------Step 2-------------------------
num_seeds = 10
seed_list = [seed + i for i in range(num_seeds)]
swap_trials = 200
# The default routing[1] is a ConditionalController([barrier, routing_pass],
# condition=_swap_condition); we re-wrap so the new routing pass keeps the
# protective barrier and is skipped when routing isn't needed (matches the preset).
def _swap_condition(property_set):
return not property_set["routing_not_needed"]
def wrap_routing(routing_pass):
return ConditionalController(
[
BarrierBeforeFinalMeasurements(
"qiskit.transpiler.internal.routing.protection.barrier"
),
routing_pass,
],
condition=_swap_condition,
)
heuristic_results = {}
# Three SABRE heuristics, swept over seeds
for heuristic in ["basic", "decay", "lookahead"]:
trials = []
for s in seed_list:
sr = SabreSwap(
coupling_map=cmap, heuristic=heuristic, trials=swap_trials, seed=s
)
sl = SabreLayout(coupling_map=cmap, routing_pass=sr, seed=s)
pm = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=s
)
# Re-wrap each custom pass in its original ConditionalController + barrier
# (wrap_sabre is defined in the small-scale Step 2 cell above).
pm.layout.replace(index=2, passes=wrap_sabre(sl))
pm.routing.replace(index=1, passes=wrap_routing(sr))
t0 = time.time()
tqc = pm.run(qc)
elapsed = time.time() - t0
depth = tqc.depth(lambda x: x.operation.num_qubits == 2)
size = tqc.size()
trials.append(
{
"tqc": tqc,
"depth": depth,
"size": size,
"time": elapsed,
"seed": s,
}
)
heuristic_results[heuristic] = trials
# Default preset + StarPreRouting in init, also swept over seeds for a fair comparison
star_trials = []
for s in seed_list:
pm_star_hw = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=s
)
pm_star_hw.init += StarPreRouting()
t0 = time.time()
tqc = pm_star_hw.run(qc)
elapsed = time.time() - t0
depth = tqc.depth(lambda x: x.operation.num_qubits == 2)
size = tqc.size()
star_trials.append(
{
"tqc": tqc,
"depth": depth,
"size": size,
"time": elapsed,
"seed": s,
}
)
heuristic_results["StarPreRouting"] = star_trials
# Print summary for each entry
for label in ["basic", "decay", "lookahead", "StarPreRouting"]:
trials = heuristic_results[label]
depths = [t["depth"] for t in trials]
sizes = [t["size"] for t in trials]
best = min(trials, key=lambda t: t["depth"])
print(f"{label}:")
print(
f" 2Q depth: min: {min(depths)}, mean: {np.mean(depths):.1f}, std: {np.std(depths):.1f}"
)
print(
f" size : min: {min(sizes)}, mean: {np.mean(sizes):.1f}, std: {np.std(sizes):.1f}"
)
print(
f" best seed: {best['seed']} (2Q depth={best['depth']}, size={best['size']})"
)
basic:
2Q depth: min: 524, mean: 570.5, std: 39.9
size : min: 3819, mean: 4227.1, std: 360.6
best seed: 51 (2Q depth=524, size=3852)
decay:
2Q depth: min: 387, mean: 436.4, std: 41.7
size : min: 2687, mean: 3183.1, std: 459.3
best seed: 45 (2Q depth=387, size=2786)
lookahead:
2Q depth: min: 364, mean: 424.6, std: 36.5
size : min: 2335, mean: 3014.6, std: 388.1
best seed: 51 (2Q depth=364, size=2485)
StarPreRouting:
2Q depth: min: 196, mean: 196.0, std: 0.0
size : min: 1151, mean: 1151.0, std: 0.0
best seed: 42 (2Q depth=196, size=1151)
hw_colors = {
"basic": "#ff7f0e",
"decay": "#d62728",
"lookahead": "#1f77b4",
"StarPreRouting": "#2a9d8f",
}
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 5))
for label in ["basic", "decay", "lookahead", "StarPreRouting"]:
trials = heuristic_results[label]
depths = [t["depth"] for t in trials]
sizes = [t["size"] for t in trials]
seeds = [t["seed"] for t in trials]
color = hw_colors[label]
ax1.scatter(
seeds,
depths,
label=label,
color=color,
alpha=0.8,
edgecolor="k",
s=60,
)
ax1.axhline(np.mean(depths), color=color, linestyle="--", alpha=0.5)
ax2.scatter(
seeds,
sizes,
label=label,
color=color,
alpha=0.8,
edgecolor="k",
s=60,
)
ax2.axhline(np.mean(sizes), color=color, linestyle="--", alpha=0.5)
ax1.set_xlabel("Seed", fontsize=11)
ax1.set_ylabel("2Q Depth", fontsize=11)
ax1.set_title("Two-Qubit Gate Depth per Seed", fontsize=13)
ax1.legend(fontsize=10)
ax1.grid(alpha=0.3)
ax2.set_xlabel("Seed", fontsize=11)
ax2.set_ylabel("Gate Count", fontsize=11)
ax2.set_title("Circuit Size per Seed", fontsize=13)
ax2.legend(fontsize=10)
ax2.grid(alpha=0.3)
plt.suptitle(
"Transpilation variability across seeds: SABRE heuristics vs. StarPreRouting",
fontsize=14,
fontweight="bold",
y=1.02,
)
plt.tight_layout()
plt.show()
# Summary comparison
for label in ["basic", "decay", "lookahead", "StarPreRouting"]:
best = min(heuristic_results[label], key=lambda t: t["depth"])
print(
f"{label}: best 2Q depth={best['depth']}, size={best['size']} (seed={best['seed']})"
)

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

Mean fidelity:
basic: 0.0344
decay: 0.1298
lookahead: 0.1857
StarPreRouting: 0.3295
ניתוח
גרפי הפיזור מציגים שונות משמעותית בין Seeds עבור כל שלוש ההיוריסטיקות של SABRE, מה שמדגיש את החשיבות של הרצת ניסויי פריסה מרובים במקום להסתמך על טרנספילציה אחת. קו ה-StarPreRouting בעצם שטוח בין Seeds מכיוון שהשכתוב מכוכב לשרשרת לינארית הוא דטרמיניסטי נתון המבנה; לניתוב SABRE שבא אחריו יש אז כמעט אין חופש על שרשרת לינארית, כך שה-Seed כמעט ולא משפיע על העומק או הגודל הסופי.
מתוצאות הטרנספילציה, גם ה-decay וגם ה-lookahead עולים בהרבה על basic. ההיוריסטיקה basic, למרות שמהירה, משתמשת באסטרטגיה חמדנית פשוטה שלעתים קרובות מובילה למעגלים עמוקים משמעותית יותר. עבור מעגל GHZ בטופולוגיית כוכב זה, lookahead נוטה לייצר את עומק ה-2Q ואת ספירת ה-Gates הנמוכים ביותר בין היוריסטיקות SABRE, מכיוון שפונקציית העלות המסתכלת קדימה שלו מתאימה היטב למעגלים עם דפוסי קישוריות ארוכי טווח. StarPreRouting, לעומת זאת, גדול בהרבה על שלושתם בפער משמעותי: על ידי שכתוב הכוכב לשרשרת לינארית לפני הניתוב, הוא מקצר לגמרי את בעיית החיפוש ומספק מעגל שהמשך הטרנספילר יכול למפות על נתיב לינארי עם מינימום SWAPs נוספים.
היתרון הזה עובר ישירות לנאמנות החומרה. עומק 2Q וספירת Gates נמוכים יותר לא תמיד מתורגמים אחד-לאחד לנאמנות גבוהה יותר (ה-qubits הפיזיים שפריסה משתמשת בהם וכיולם בזמן הריצה גם משנים), אבל כאשר פער העומק גדול כמו זה שבין SABRE ל-StarPreRouting כאן, הגישה המודעת-למבנה מנצחת בצורה מכרעת מכיוון שהמעגל צובר הרבה פחות דה-קוהרנציה ואירועי שגיאת Gate דו-Qubit. תרשים עמודות הנאמנות מציג את StarPreRouting קדימה משמעותית אפילו מהיוריסטיקת SABRE הטובה ביותר, בעוד basic יושב הרחק מתחת לשאר מפני שמעגליה העמוקים הרבה יותר צוברים את הכי הרבה שגיאה.
נקודות מפתח:
- בין היוריסטיקות SABRE,
decayו-lookaheadטובות משמעותית מ-basicעבור מעגלים לא-טריוויאליים. העדיפו אחת משתיהן לעומסי עבודה ייצוריים. - ההיוריסטיקה הטובה ביותר של SABRE תלויה במעגל ובחומרה שלכם. בדיקת היוריסטיקות מרובות עם Seeds מרובים היא האסטרטגיה האמינה ביותר.
- אם ברצונכם לחקור עוד יותר פריסות, הגדילו את
swap_trials(ואתlayout_trialsכאשר אינכם מצמידים מעבר ניתוב מותאם אישית) במקום לפזר את העבודה לצמתים מרוחקים. מעברי SABRE כבר מקבילים ניסויים על פני threads מקומיים, ועבודת כל ניסיון קטנה מספיק שתקורת הפצה בדרך כלל שולטת על כל הזדרזות. - כאשר למעגל יש מבנה מיוחד ידוע, יישום מעבר מודע-מבנה כמו
StarPreRoutingלפני SABRE יכול לספק שיפור מסדר גודל שאין כמות של כיוון SABRE תתאים לו. זה אינו תחליף ל-SABRE:StarPreRoutingעוזר רק כאשר המעגל מכיל בפועל תת-מעגלים בצורת כוכב ול-Backend יש נתיב לינארי ארוך מספיק. שווה לבדוק את ספריית המעברים לגבי התאמות בכל פעם שאתם יודעים את צורת המעגל שלכם.
צעדים הבאים
אם מצאתם את העבודה הזאת מעניינת, ייתכן שתתעניינו בחומר הבא:
- עיון ב-API של
SabreLayout: תיעוד פרמטרים מלא - מאמר SABRE: אלגוריתם SABRE המקורי לפריסה וניתוב
- מאמר LightSABRE: שיפורי האלגוריתם שמניעים את מימוש SABRE הנוכחי של Qiskit
- כתיבת מעבר טרנספילר מותאם אישית: בניית לוגיקת טרנספילציה משלכם
- תוספי Transpiler: הרחבת צינור הטרנספילציה של Qiskit עם מעברים של צד שלישי
- ייצוג DAG: הבנת הגרף האציקלי המכוון המשמש פנימית את הטרנספילר
סקר מדריך
אנא מלאו את הסקר הקצר הזה כדי לספק משוב על מדריך זה. התובנות שלכם יעזרו לנו לשפר את הצעות התוכן וחווית המשתמש.
הערה: סקר זה הוא של IBM Quantum ומכסה את תוכן המדריך (שנכתב על ידי IBM). doQumentation מספקת את האתר, התרגומים ויישום הקוד — לגבי משוב על אלה, אנא פתחו בעיית GitHub.