Comparar la configuración del transpilador
Estimación de uso: menos de un minuto en un procesador Eagle r3 (NOTA: Esto es solo una estimación). El tiempo de ejecución puede variar.
En segundo plano
Para garantizar resultados más rápidos y eficientes, a partir del 1 de marzo de 2024, los circuitos y observables deberán transformarse para utilizar únicamente instrucciones compatibles con la QPU (unidad de procesamiento cuántico) antes de enviarse a las primitivas Qiskit Runtime. A estos circuitos y observables los denominamos arquitectura de conjunto de instrucciones (ISA). Una forma habitual de hacerlo es utilizar la función generate_preset_pass_manager del transpilador. Sin embargo, es posible que prefiera seguir un proceso más manual.
Por ejemplo, es posible que desee dirigirse a un subconjunto específico de qubits en un dispositivo específico. Este tutorial prueba el rendimiento de diferentes configuraciones del transpilador completando el proceso completo de creación, transpilación y envío de circuitos.
Requisitos
Antes de comenzar, asegúrese de tener instalado lo siguiente:
- Qiskit SDK v1.2 o posterior, con soporte de visualización
- Qiskit Runtime v0.28 o posterior (
pip install qiskit-ibm-runtime)
Configuración
# 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,
)Paso 1: Asignar entradas clásicas a un problema cuántico
Crea un pequeño circuito para que el transpilador intente optimizarlo. Este ejemplo crea un circuito que ejecuta el algoritmo de Grover con un oráculo que marca el estado 111. A continuación, simule la distribución ideal (lo que esperaría medir si ejecutara esto en un ordenador cuántico perfecto un número infinito de veces) para compararlo más adelante.
# 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.nameOutput:
'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:
ideal_distribution = Statevector.from_instruction(qc).probabilities_dict()
plot_histogram(ideal_distribution)Output:
Paso 2: Optimizar el problema para la ejecución en hardware cuántico
A continuación, transpile los circuitos para la QPU. Compararás el rendimiento del transpilador con optimization_level establecido en 0 (el más bajo) frente a 3 (el más alto). El nivel de optimización más bajo hace lo mínimo necesario para que el circuito funcione en el dispositivo; asigna los qubits del circuito a los qubits del dispositivo y añade puertas de intercambio para permitir todas las operaciones de dos qubits. El nivel de optimización más alto es mucho más inteligente y utiliza muchos trucos para reducir el recuento total de puertas. Dado que las puertas de múltiples qubits tienen altas tasas de error y los qubits se descoheren con el tiempo, los circuitos más cortos deberían dar mejores resultados.
La siguiente celda transpilada qc para ambos valores de optimization_level, imprime el número de puertas de dos qubits y añade los circuitos transpilados a una lista. Algunos de los algoritmos del transpilador son aleatorios, por lo que establece una semilla para garantizar la reproducibilidad.
# 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)Output:
Two-qubit gates (optimization_level=0): 21
Two-qubit gates (optimization_level=3): 14
Dado que los CNOT suelen tener una alta tasa de error, el circuito transpilado con optimization_level=3 debería funcionar mucho mejor.
Otra forma de mejorar el rendimiento es mediante el desacoplamiento dinámico, aplicando una secuencia de puertas a los qubits inactivos. Esto elimina algunas interacciones no deseadas con el entorno. La siguiente celda añade desacoplamiento dinámico al circuito transpilado con optimization_level=3 y lo añade a la lista.
# 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:
Paso 3: Ejecutar utilizando Qiskit primitives
En este punto, dispone de una lista de circuitos transpilados para la QPU especificada. A continuación, cree una instancia de la primitiva del muestreador e inicie un trabajo por lotes utilizando el gestor de contexto (with ...:), que abre y cierra automáticamente el lote.
Dentro del gestor de contexto, muestree los circuitos y almacene los resultados en result.
with Batch(backend=backend):
sampler = Sampler()
job = sampler.run(
[(circuit) for circuit in circuits], # sample all three circuits
shots=8000,
)
result = job.result()Paso 4: Procesamiento posterior y devolución del resultado en el formato clásico deseado
Por último, represente gráficamente los resultados obtenidos con el dispositivo en comparación con la distribución ideal. Se puede observar que los resultados con optimization_level=3 se acercan más a la distribución ideal debido al menor número de puertas, y optimization_level=3 + dd se acerca aún más debido al desacoplamiento dinámico.
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:
Puedes confirmarlo calculando la fidelidad de Hellinger entre cada conjunto de resultados y la distribución ideal (cuanto más alta, mejor, y 1 es la fidelidad perfecta).
for prob in binary_prob:
print(f"{hellinger_fidelity(prob, ideal_distribution):.3f}")Output:
0.848
0.945
0.990