Przejdź do głównej treści

Porównaj ustawienia Transpilera

Szacowany czas użycia: poniżej jednej minuty na procesorze Eagle r3 (UWAGA: To jest tylko szacunek. Twój czas działania może się różnić.)

Tło

Aby zapewnić szybsze i bardziej wydajne wyniki, od 1 marca 2024 roku Circuit i obserwable muszą zostać przekształcone tak, aby używały wyłącznie instrukcji obsługiwanych przez QPU (quantum processing unit) przed przesłaniem do prymitywów Qiskit Runtime. Nazywamy je Circuit i obserwablami zgodnych z architekturą zestawu instrukcji (ISA). Jednym z popularnych sposobów na to jest użycie funkcji generate_preset_pass_manager Transpilera. Możesz jednak zdecydować się na bardziej ręczny proces.

Na przykład możesz chcieć skierować się na określony podzbiór Qubitów na konkretnym urządzeniu. Ten przewodnik testuje wydajność różnych ustawień Transpilera, przechodząc przez pełny proces tworzenia, transpilowania i przesyłania Circuit.

Wymagania

Przed rozpoczęciem upewnij się, że masz zainstalowane następujące elementy:

  • Qiskit SDK v1.2 lub nowszy, z obsługą wizualizacji
  • Qiskit Runtime v0.28 lub nowszy (pip install qiskit-ibm-runtime)

Konfiguracja

# 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,
)

Krok 1: Odwzorowanie klasycznych danych wejściowych na problem kwantowy

Utwórz mały Circuit, który Transpiler będzie próbował zoptymalizować. Ten przykład tworzy Circuit realizujący algorytm Grovera z wyrocznią oznaczającą stan 111. Następnie zasymuluj idealny rozkład (to, czego spodziewałbyś się zmierzyć, gdybyś uruchomił to na doskonałym komputerze kwantowym nieskończoną liczbę razy) w celu późniejszego porównania.

# 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

Krok 2: Optymalizacja problemu pod kątem wykonania na sprzęcie kwantowym

Następnie transpiluj Circuit dla QPU. Porównasz wydajność Transpilera z optimization_level ustawionym na 0 (najniższy) w porównaniu z 3 (najwyższy). Najniższy poziom optymalizacji wykonuje absolutne minimum potrzebne do uruchomienia Circuit na urządzeniu: odwzorowuje Qubity Circuit na Qubity urządzenia i dodaje bramki swap, aby umożliwić wszystkie operacje dwu-Qubitowe. Najwyższy poziom optymalizacji jest znacznie inteligentniejszy i stosuje wiele sztuczek w celu zmniejszenia całkowitej liczby Gate. Ponieważ Gate wieloQubitowe mają wysokie współczynniki błędów, a Qubity dekoherują z czasem, krótsze Circuit powinny dawać lepsze wyniki.

Poniższa komórka transpiluje qc dla obu wartości optimization_level, wyświetla liczbę dwuQubitowych Gate i dodaje transpilowane Circuit do listy. Niektóre algorytmy Transpilera są losowe, dlatego ustawia się ziarno dla powtarzalności.

# 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

Ponieważ bramki CNOT zazwyczaj mają wysoki współczynnik błędów, Circuit transpilowany z optimization_level=3 powinien działać znacznie lepiej.

Innym sposobem na poprawę wydajności jest dynamiczne odsprzęganie, polegające na stosowaniu sekwencji Gate do bezczynnych Qubitów. Niweluje to pewne niepożądane interakcje ze środowiskiem. Poniższa komórka dodaje dynamiczne odsprzęganie do Circuit transpilowanego z optimization_level=3 i dodaje go do listy.

# 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

Krok 3: Wykonanie przy użyciu prymitywów Qiskit

W tym momencie masz listę Circuit transpilowanych dla określonego QPU. Następnie utwórz instancję prymitywu Sampler i uruchom zadanie wsadowe za pomocą menedżera kontekstu (with ...:), który automatycznie otwiera i zamyka partię.

W ramach menedżera kontekstu próbkuj Circuit i zapisz wyniki w result.

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

Krok 4: Post-processing i zwrócenie wyniku w żądanym formacie klasycznym

Na koniec zwizualizuj wyniki z uruchomień na urządzeniu w porównaniu z idealnym rozkładem. Możesz zobaczyć, że wyniki z optimization_level=3 są bliższe idealnemu rozkładowi ze względu na mniejszą liczbę Gate, a optimization_level=3 + dd jest jeszcze bliżej dzięki dynamicznemu odsprzęganiu.

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

Możesz to potwierdzić, obliczając wierność Hellingera między każdym zestawem wyników a idealnym rozkładem (wyższe wartości są lepsze, a 1 oznacza doskonałą wierność).

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