Zum Hauptinhalt springen

Transpiler-Einstellungen vergleichen

Nutzungsschätzung: unter einer Minute auf einem Eagle r3-Prozessor (HINWEIS: Dies ist nur eine Schätzung. Deine Laufzeit kann variieren.)

Hintergrund

Um schnellere und effizientere Ergebnisse zu gewährleisten, müssen Schaltkreise und Observables ab dem 1. März 2024 so transformiert werden, dass sie nur Anweisungen verwenden, die von der QPU (Quantum Processing Unit) unterstützt werden, bevor sie an die Qiskit Runtime Primitives übermittelt werden. Wir nennen diese Instruction Set Architecture (ISA)-Schaltkreise und -Observables. Eine gängige Methode hierfür ist die Verwendung der generate_preset_pass_manager-Funktion des Transpilers. Du kannst jedoch auch einem manuelleren Prozess folgen.

Beispielsweise möchtest du möglicherweise eine bestimmte Teilmenge von Qubits auf einem bestimmten Gerät ansprechen. Dieser Walkthrough testet die Leistung verschiedener Transpiler-Einstellungen, indem der vollständige Prozess der Erstellung, Transpilierung und Einreichung von Schaltkreisen durchgeführt wird.

Voraussetzungen

Stelle vor Beginn sicher, dass du Folgendes installiert hast:

  • Qiskit SDK v1.2 oder höher, mit Visualisierungs-Unterstützung
  • Qiskit Runtime v0.28 oder höher (pip install qiskit-ibm-runtime)

Setup

# Added by doQumentation — required packages for this notebook
!pip install -q qiskit qiskit-ibm-runtime
# Create circuit to test transpiler on
from qiskit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.circuit.library import GroverOperator, Diagonal

# Use Statevector object to calculate the ideal output
from qiskit.quantum_info import Statevector
from qiskit.visualization import plot_histogram
from qiskit.transpiler import PassManager

from qiskit.circuit.library import XGate
from qiskit.quantum_info import hellinger_fidelity

# Qiskit Runtime
from qiskit_ibm_runtime import (
QiskitRuntimeService,
Batch,
SamplerV2 as Sampler,
)
from qiskit_ibm_runtime.transpiler.passes.scheduling import (
ASAPScheduleAnalysis,
PadDynamicalDecoupling,
)

Schritt 1: Klassische Eingaben auf ein Quantenproblem abbilden

Erstelle einen kleinen Schaltkreis, den der Transpiler versuchen soll zu optimieren. Dieses Beispiel erstellt einen Schaltkreis, der Grovers Algorithmus mit einem Orakel durchführt, das den Zustand 111 markiert. Simuliere als Nächstes die ideale Verteilung (was du erwarten würdest zu messen, wenn du dies auf einem perfekten Quantencomputer unendlich oft ausführen würdest) zum späteren Vergleich.

# To run on hardware, select the backend with the fewest number of jobs in the queue
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
backend.name
'ibm_brisbanse'
oracle = Diagonal([1] * 7 + [-1])
qc = QuantumCircuit(3)
qc.h([0, 1, 2])
qc = qc.compose(GroverOperator(oracle))

qc.draw(output="mpl", style="iqp")

Output of the previous code cell

ideal_distribution = Statevector.from_instruction(qc).probabilities_dict()

plot_histogram(ideal_distribution)

Output of the previous code cell

Schritt 2: Problem für Quanten-Hardware-Ausführung optimieren

Transpiliere als Nächstes die Schaltkreise für die QPU. Du wirst die Leistung des Transpilers mit optimization_level auf 0 (niedrigste) gegen 3 (höchste) vergleichen. Das niedrigste Optimierungslevel macht das Minimum, das erforderlich ist, um den Schaltkreis auf dem Gerät zum Laufen zu bringen; es ordnet die Schaltkreis-Qubits den Geräte-Qubits zu und fügt Swap-Gates hinzu, um alle Zwei-Qubit-Operationen zu ermöglichen. Das höchste Optimierungslevel ist viel intelligenter und verwendet viele Tricks, um die Gesamtanzahl der Gates zu reduzieren. Da Multi-Qubit-Gates hohe Fehlerraten haben und Qubits im Laufe der Zeit dekohärieren, sollten die kürzeren Schaltkreise bessere Ergebnisse liefern.

Die folgende Zelle transpiliert qc für beide Werte von optimization_level, gibt die Anzahl der Zwei-Qubit-Gates aus und fügt die transpilierten Schaltkreise einer Liste hinzu. Einige der Algorithmen des Transpilers sind randomisiert, daher wird ein Seed für Reproduzierbarkeit festgelegt.

# Need to add measurements to the circuit
qc.measure_all()

# Find the correct two-qubit gate
twoQ_gates = set(["ecr", "cz", "cx"])
for gate in backend.basis_gates:
if gate in twoQ_gates:
twoQ_gate = gate

circuits = []
for optimization_level in [0, 3]:
pm = generate_preset_pass_manager(
optimization_level, backend=backend, seed_transpiler=0
)
t_qc = pm.run(qc)
print(
f"Two-qubit gates (optimization_level={optimization_level}): ",
t_qc.count_ops()[twoQ_gate],
)
circuits.append(t_qc)
Two-qubit gates (optimization_level=0):  21
Two-qubit gates (optimization_level=3): 14

Da CNOTs normalerweise eine hohe Fehlerrate haben, sollte der mit optimization_level=3 transpilierte Schaltkreis viel besser abschneiden.

Eine weitere Möglichkeit, die Leistung zu verbessern, ist durch Dynamic Decoupling, indem eine Sequenz von Gates auf unbeschäftigte Qubits angewendet wird. Dies hebt einige unerwünschte Wechselwirkungen mit der Umgebung auf. Die folgende Zelle fügt Dynamic Decoupling zu dem mit optimization_level=3 transpilierten Schaltkreis hinzu und fügt ihn der Liste hinzu.

# Get gate durations so the transpiler knows how long each operation takes
durations = backend.target.durations()

# This is the sequence we'll apply to idling qubits
dd_sequence = [XGate(), XGate()]

# Run scheduling and dynamic decoupling passes on circuit
pm = PassManager(
[
ASAPScheduleAnalysis(durations),
PadDynamicalDecoupling(durations, dd_sequence),
]
)
circ_dd = pm.run(circuits[1])

# Add this new circuit to our list
circuits.append(circ_dd)
circ_dd.draw(output="mpl", style="iqp", idle_wires=False)

Output of the previous code cell

Schritt 3: Ausführung mit Qiskit Primitives

An diesem Punkt hast du eine Liste von Schaltkreisen, die für die angegebene QPU transpiliert wurden. Erstelle als Nächstes eine Instanz des Sampler-Primitive und starte einen Batch-Job mit dem Context Manager (with ...:), der den Batch automatisch öffnet und schließt.

Innerhalb des Context Managers sammelst du die Schaltkreise und speicherst die Ergebnisse in result.

with Batch(backend=backend):
sampler = Sampler()
job = sampler.run(
[(circuit) for circuit in circuits], # sample all three circuits
shots=8000,
)
result = job.result()

Schritt 4: Nachbearbeitung und Rückgabe des Ergebnisses im gewünschten klassischen Format

Stelle schließlich die Ergebnisse von den Geräteläufen gegen die ideale Verteilung dar. Du kannst sehen, dass die Ergebnisse mit optimization_level=3 aufgrund der geringeren Gate-Anzahl näher an der idealen Verteilung liegen, und optimization_level=3 + dd ist aufgrund des Dynamic Decoupling noch näher.

binary_prob = [
{
k: v / res.data.meas.num_shots
for k, v in res.data.meas.get_counts().items()
}
for res in result
]
plot_histogram(
binary_prob + [ideal_distribution],
bar_labels=False,
legend=[
"optimization_level=0",
"optimization_level=3",
"optimization_level=3 + dd",
"ideal distribution",
],
)

Output of the previous code cell

Du kannst dies bestätigen, indem du die Hellinger-Fidelity zwischen jedem Ergebnissatz und der idealen Verteilung berechnest (höher ist besser, und 1 ist perfekte Fidelity).

for prob in binary_prob:
print(f"{hellinger_fidelity(prob, ideal_distribution):.3f}")
0.848
0.945
0.990