Capítulo 4. Utilizar el Transpilador

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Hemos estado utilizando la clase QuantumCircuit para representar programas cuánticos, y el propósito de los programas cuánticos es ejecutarlos en dispositivos reales y obtener resultados de ellos. Al programar, normalmente no nos preocupamos de los detalles específicos de los dispositivos y, en su lugar, utilizamos operaciones de alto nivel. Pero la mayoría de los dispositivos (y algunos simuladores) sólo pueden llevar a cabo un pequeño conjunto de operaciones y sólo pueden realizar compuertas multiqubit entre determinados qubits. Esto significa que tenemos que transpilar nuestro circuito para el dispositivo específico en el que lo estamos ejecutando.

El proceso de transpilación implica convertir las operaciones del circuito a las que admite el dispositivo e intercambiar qubits (mediante puertas de intercambio) dentro del circuito para superar la conectividad limitada de los qubits. El transpilador de Qiskit realiza este trabajo, así como algunas optimizaciones para reducir el número de compuertas del circuito en la medida de lo posible.

Inicio rápido con Transpile

En esta sección, te mostraremos cómo utilizar el transpilador para preparar tu circuito para el dispositivo. Daremos una breve visión general de la lógica del transpilador y de cómo podemos obtener los mejores resultados de él.

El único argumento necesario para transpile es el Quantum​Cir⁠cuit que queremos transpilar, pero si queremos que transpile haga algo interesante, tendremos que decirle qué queremos que haga. La forma más sencilla de hacer que tu circuito funcione en un dispositivo es simplemente pasarle a transpile el objeto backend y dejar que coja las propiedades que necesite. transpile devuelve un nuevo objeto QuantumCircuit compatible con el backend. El siguiente fragmento de código muestra cómo es el uso más sencillo del transpilador:

from qiskit import transpile
transpiled_circuit = transpile(circuit, backend)

Por ejemplo, aquí creamos un QuantumCircuit sencillo con un qubit, un YGate y dos CXGates:

from qiskit import QuantumCircuit
qc = QuantumCircuit(3)
qc.y(0)
for t in range(2): qc.cx(0,t+1)
qc.draw()

La Figura 4-1 muestra la salida de qc.draw().

Figura 4-1. Circuito simple con una puerta Y y dos puertas CX

En el siguiente fragmento de código, decidimos que queremos ejecutar qc en el backend simulado FakeSantiago (un backend simulado contiene las propiedades y los modelos de ruido de un sistema real y utiliza el AerSimulator para simular ese sistema). Podemos ver en la salida (mostrada después del código) que FakeSantiago no entiende la operación YGate:

from qiskit.test.mock import FakeSantiago
santiago = FakeSantiago()
santiago.configuration().basis_gates

['id', 'rz', 'sx', 'x', 'cx', 'reset']

Así que qc necesitará que transpiles antes de ejecutarse. En el siguiente fragmento de código, veremos lo que hace el transpilador cuando le damos qc y le decimos que transpile para santiago:

t_qc = transpile(qc, santiago)
t_qc.draw()

La Figura 4-2 muestra la salida de t_qc.draw().

Figura 4-2. Resultado de transpilar un circuito simple

Podemos ver en la Figura 4-2 que el transpilador ha hecho lo siguiente:

  • Asigna los qubits (virtuales) 0, 1 y 2 en qc a los qubits (físicos) 2, 1 y 0 en t_qc, respectivamente

  • Añadidos tres CXGates más para intercambiar los qubits (físicos) 0y 1

  • Sustituimos nuestro YGate por un RZGate y un XGate

  • Se han añadido dos qubits más (ya que santiago tiene cinco qubits)

La mayor parte de esto parece bastante razonable, excepto la adición de todos esos CXGates. CXGates suelen ser operaciones bastante caras, por lo que queremos evitarlas en la medida de lo posible. Entonces, ¿por qué ha hecho esto el transpilador? En algunos sistemas cuánticos, incluido santiago, no todos los qubits pueden comunicarse directamente entre sí.

Podemos comprobar qué qubits pueden hablar entre sí a través del mapa de acoplamiento de ese sistema (ejecuta backend.configuration()​.cou⁠pling_map para obtenerlo). Un vistazo rápido al mapa de acoplamiento de santiagonos muestra que el qubit físico 2 no puede hablar con el qubit físico 0, por lo que tenemos que añadir un intercambio en alguna parte.

Aquí tienes el resultado de santiago.configuration().coupling_map:

[[0, 1], [1, 0], [1, 2], [2, 1], [2, 3],
 [3, 2], [3, 4], [4, 3]]

Al llamar a transpile, si fijamos initial_layout=[1,0,2], podemos cambiar la forma en que qc mapea al backend y evitar intercambios innecesarios. Aquí, el índice de cada elemento de la lista representa el qubit virtual (en qc), y el valor en ese índice representa el qubit físico. Esta disposición mejorada anula la suposición del transpilador, y no necesita insertar ningún CXGates extra. El siguiente fragmento de código lo demuestra:

t_qc = transpile(qc, santiago, initial_layout=
                                        [1,0,2])
t_qc.draw()

La Figura 4-3 muestra la salida de t_qc.draw() en el fragmento de código anterior.

Como santiago sólo tiene cinco qubits, fue relativamente fácil encontrar una buena disposición para este circuito en este dispositivo. Para combinaciones circuito/dispositivo mayores, querremos hacerlo algorítmicamente. Una opción es configurar optimization_level=2 para pedir al transpilador que utilice un algoritmo más inteligente (pero más caro) para seleccionar un trazado mejor.

Figura 4-3. Resultado de transpilar un circuito simple con un trazado inicial más inteligente

La función transpile acepta cuatro ajustes posibles paraoptimization_level:

optimization_level=0

El transpilador simplemente hace el mínimo absoluto necesario para que el circuito funcione en el backend. El diseño inicial mantiene iguales los índices de los qubits físicos y virtuales, añade los intercambios necesarios y convierte todas las puertas en puertas de base.

optimization_level=1

Este es el valor por defecto. El transpilador toma decisiones más inteligentes. Por ejemplo, si tenemos menos qubits virtuales que físicos, el transpilador elige el subconjunto de qubits físicos mejor conectados y mapea los qubits virtuales a éstos. El transpilador también combina/elimina secuencias de puertas cuando es posible (por ejemplo, dos CXGates que se anulan mutuamente).

optimization_level=2

El transpilador buscará una disposición inicial que no necesite ningún intercambio para ejecutar el circuito o, en su defecto, buscará el subconjunto de qubits mejor conectado. Al igual que en el nivel 1, el transpilador también intenta colapsar y anular puertas siempre que sea posible.

optimization_level=3

Éste es el valor más alto que podemos fijar. El transpilador utilizará algoritmos más inteligentes para anular las puertas, además de las medidas tomadas con optimization_level=2.

Pases Transpiler

Dependiendo de tu caso de uso, el transpilador suele ser invisible. Funciones como execute lo llaman automáticamente, y gracias al transpilador, normalmente podemos ignorar el dispositivo específico en el que estamos trabajando cuando creamos circuitos. A pesar de este bajo perfil, el transpilador puede tener un efecto enorme en el rendimiento de un circuito. En esta sección, examinaremos las decisiones que toma el transpilador y veremos cómo cambiar su comportamiento cuando lo necesitemos.

El PassManager

Construimos una rutina de transpilación a partir de un montón de "pases" más pequeños. Cada pase es un programa que realiza una pequeña tarea (por ejemplo, decidir la disposición inicial o insertar puertas de intercambio), y utilizamos un objeto PassManager para organizar nuestra secuencia de pases. En esta sección, mostraremos un ejemplo sencillo de utilizando el pase BasicSwap.

En primer lugar, necesitamos un circuito cuántico para transpilar. El siguiente fragmento de código crea un circuito sencillo que utilizaremos como ejemplo:

from qiskit import QuantumCircuit
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 2)
qc.cx(2, 1)
qc.draw()

La Figura 4-4 muestra la salida de qc.draw().

Figura 4-4. Circuito simple con dos puertas CX

A continuación, tenemos que importar y construir el PassManager y los pases que queremos utilizar. El constructor BasicSwap pide el mapa de acoplamiento del dispositivo en el que queremos ejecutar nuestro circuito. En el siguiente fragmento de código, fingiremos que queremos ejecutarlo en un dispositivo en el que el qubit 0 no puede interactuar con el qubit 2 (pero el qubit 1 puede interactuar con ambos). El constructor PassManager pide los pases que queremos aplicar a nuestro circuito, que en este caso es sólo el pase basic_swap que creamos en la línea anterior:

from qiskit.transpiler import PassManager,
                                   CouplingMap
from qiskit.transpiler.passes import BasicSwap

coupling_map = CouplingMap([[0,1], [1,2]])
basic_swap_pass = BasicSwap(coupling_map)
pm = PassManager(basic_swap_pass)

Ahora que hemos creado nuestro procedimiento de transpilación, podemos aplicarlo al circuito utilizando el siguiente fragmento de código:

routed_qc = pm.run(qc)
routed_qc.draw()

La Figura 4-5 muestra la salida de routed_qc.draw().

Figura 4-5. Circuito simple que contiene dos puertas CX y dos intercambios necesarios para ejecutarse en hardware

En la Figura 4-5, podemos ver que el paso basic_swap ha añadido dos puertas de intercambio para llevar a cabo los CXGates, aunque observa que no ha devuelto los qubits a su orden original.

Compilación/Traducción de Pases

Para que un circuito funcione en un dispositivo, necesitamos para convertir todas las operaciones de nuestro circuito en instrucciones compatibles con el dispositivo. Esto puede implicar dividir las puertas de alto nivel en puertas de bajo nivel (una forma de compilación) o traducir un conjunto de puertas de bajo nivel a otro. La Figura 4-6 muestra cómo el transpilador puede descomponer una puerta X multicontrolada en puertas más pequeñas.

Figura 4-6. Ejemplo de una puerta X multicontrolada descompuesta en puertas H, fase y CX

En el momento de escribir esto, Qiskit tiene dos formas de averiguar cómo dividir una puerta en puertas más pequeñas. La primera es a través del atributo definition de la puerta. Si está definido, este atributo contiene un QuantumCircuit igual a esa puerta. Los pases Decompose y Unroller utilizan esta definición para expandir circuitos. El pase Decompose amplía el circuito sólo un nivel; es decir, no intentará descomponer las definiciones con las que hemos sustituido cada puerta. El método .decompose() de la clase QuantumCircuit utiliza el pase Decompose. El pase Unroller es similar, pero seguirá descomponiendo las definiciones de cada puerta recursivamente hasta que el circuito sólo contenga las puertas base que especifiquemos al construirlo.

La segunda forma de descomponer las puertas es consultando una biblioteca EquivalenceLibrary. Esta biblioteca puede almacenar muchos circuitos de definición para cada instrucción, lo que permite a los pases elegir cómo descomponer cada circuito. Esto tiene la ventaja de no estar atado a un conjunto específico de puertas base. El constructor BasisTranslator necesita un EquivalenceLibrary y una lista de etiquetas de nombres de puertas. Si el circuito contiene puertas que no están en la biblioteca de equivalencias, no tenemos más remedio que utilizar las definiciones incorporadas de esas puertas. El paso UnrollCustomDefinitions mira en EquivalenceLibrary, y si cada puerta no tiene una entrada en la biblioteca, desenrolla esa puerta utilizando su atributo .definition. En las rutinas preestablecidas del transpilador (que veremos más adelante en este capítulo), normalmente veremos el pase UnrollCustomDefinitions inmediatamente antes del pase BasisTranslator.

Pases de ruta

Algunos dispositivos pueden realizar puertas multiqubit sólo entre subconjuntos específicos de qubits. El hardware de IBM tiende a permitir sólo una puerta multiqubits (la CXGate) y puede realizar estas puertas sólo entre pares específicos de qubits. Llamamos mapa de acoplamiento a una lista de cada par de posibles interacciones de dos qubits. Vimos un ejemplo de esto en "El PassManager". En ese ejemplo, superamos esta limitación utilizando puertas de intercambio para desplazar los qubits en el mapa de acoplamiento. La Figura 4-7 muestra un ejemplo de mapa de acoplamiento.

Figura 4-7. Dibujo de un mapa de acoplamiento: [[0, 1], [1, 2], [2, 3], [3, 1]]

Qiskit tiene unos cuantos algoritmos para añadir estas pasadas de intercambio. La Tabla 4-1 enumera cada uno de los pases de intercambio disponibles, con una breve descripción del pase.

Tabla 4-1. Intercambio de pases de transpilador disponibles en Qiskit
Nombre Explicación

BasicSwap

Esta pasada realiza el menor trabajo computacional necesario para que el circuito funcione en el backend.

LookaheadSwap

A diferencia de BasicSwap, este paso utiliza un algoritmo más inteligente para reducir el número de puertas de intercambio. Hace una búsqueda del mejor primero entre todas las combinaciones potenciales de intercambios.

StochasticSwap

Este es el pase de intercambio utilizado en los gestores de pases preestablecidos. Este pase no es determinista, por lo que podría no producir el mismo circuito cada vez.

SabreSwap

Esta pasada utiliza el algoritmo SABRE (Búsqueda heurística bidireccional basada en SWAP) para intentar reducir el número de intercambios necesarios.

BIPMapping

Este pase resuelve al mismo tiempo la disposición inicial y los intercambios. El pase mapea estos problemas a un problema BIP (Programación Binaria Entera), que resuelve utilizando programas externos (docplex y CPLEX) que tendrás que instalar. Además, este pase no funciona bien con mapas de acoplamiento grandes (>~ 10 qubits).

Pases de optimización

El transpilador actúa en parte como un compilador de , y como la mayoría de los compiladores, también incluye algunos pases de optimización. El mayor problema de los ordenadores cuánticos modernos es el ruido, y el objetivo de estos pases de optimización es reducir al máximo el ruido en el circuito de salida. La mayoría de estos pases de optimización intentan reducir el ruido y el tiempo de ejecución minimizando el número de compuertas.

Las optimizaciones más sencillas buscan secuencias de puertas que no tengan ningún efecto, por lo que podemos eliminarlas con seguridad. Por ejemplo, dos CXGates seguidas no tendrían ningún efecto sobre la matriz unitaria del circuito, por lo que el pase CXCancellation las elimina. Del mismo modo, el pase RemoveDiagonalGatesBeforeMeasure hace lo que anuncia y elimina cualquier puerta con diagonales unitarias inmediatamente antes de una medición (ya que no cambiarán las mediciones en la base de cálculo). El pase OptimizeSwapBeforeMeasure elimina las compuertas SWAP inmediatamente antes de una medición y reasigna las mediciones al registro clásico para conservar la cadena de bits de salida.

Qiskit también tiene pases de optimización más inteligentes que intentan sustituir grupos de compuertas por grupos de compuertas más pequeños o más eficientes. Por ejemplo, podemos recopilar fácilmente secuencias de puertas de un solo qubit y sustituirlas por una sola UGate, que luego podemos descomponer en un conjunto eficiente de puertas base. Los pases Optimize1qGates y Optimize1qGatesDecomposition hacen esto para diferentes conjuntos de puertas iniciales. También podemos hacer lo mismo con las puertas de dos qubits; Collect2qBlocks y ConsolidateBlocks encuentran secuencias de puertas de dos qubits y las compilan en una matriz unitaria de dos qubits. El paso UnitarySynthesis puede descomponerla en las puertas básicas que elijamos.

Por ejemplo, la Figura 4-8 muestra dos circuitos con unitarios idénticos pero diferente número de puertas.

Figura 4-8. Ejemplo del mismo circuito tras pasar por dos procesos de transpilación diferentes

Pases de selección del diseño inicial

Al igual que con el trazado, también tenemos que elegir cómo mapear inicialmente nuestros qubits del circuito virtual a los qubits del dispositivo físico. La Tabla 4-2 enumera algunos algoritmos de selección de trazado que ofrece Qiskit.

Tabla 4-2. Pases iniciales del transpilador de diseño disponibles en Qiskit
Nombre Explicación

Trivial​Lay⁠out

Este paso simplemente asigna qubits de circuito a qubits físicos a través de sus índices. Por ejemplo, el qubit de circuito con índice 3 se asignará al qubit de dispositivo con índice 3.

Dense​Lay⁠out

Este paso encuentra el grupo de qubits físicos mejor conectados y asigna los qubits del circuito a este grupo.

Noise​Adap⁠tive​Layout

Este paso utiliza información sobre las propiedades de ruido del dispositivo para elegir un diseño.

Sabre​Lay⁠out

Esta pasada utiliza el algoritmo SABRE para encontrar una disposición inicial que requiera el menor número posible de SWAPs.

CSPLayout

Este paso convierte la selección de la disposición en un problema de satisfacción de restricciones (CSP). A continuación, utiliza el módulo constraint RecursiveBacktrackingSolver para intentar encontrar la mejor disposición.

Gestores de Pases Preestablecidos

Cuando antes utilizábamos la función de alto nivel transpile, no nos preocupábamos de los pases individuales y en su lugar establecíamos el parámetro optimization_level. Este parámetro indica al transpilador que utilice uno de los cuatro gestores de pases predefinidos. Qiskit construye estos gestores de pases predefinidos mediante funciones que toman ajustes de configuración y devuelven un objeto PassManager. Ahora que entendemos algunos pases, podemos echar un vistazo a lo que hacen las distintas rutinas de transpilación.

A continuación se muestra el código que utilizamos para extraer los pases de una sencilla rutina de transpilación, por si quieres reproducirlo:

from qiskit.transpiler import (PassManagerConfig,
                                      CouplingMap)
from qiskit.transpiler.preset_passmanagers import\
                              level_0_pass_manager
from qiskit.test.mock import FakeSantiago

sys_conf = FakeSantiago().configuration()
pm_conf = PassManagerConfig(
    basis_gates=sys_conf.basis_gates,
    coupling_map=CouplingMap(sys_conf.coupling_map))

for i, step in enumerate(
    level_0_pass_manager(pm_conf).passes()):
    print(f'Step {i}:')
    for transpiler_pass in step['passes']:
        print(f'  {transpiler_pass.name()}')

No hemos tratado algunos de los siguientes pases en este capítulo porque son pases de análisis que no afectan al circuito o porque son pases de limpieza para los que no tenemos elección de algoritmo. Es poco probable que estos pases tengan un efecto negativo evitable en el rendimiento de nuestros circuitos. Tampoco hemos cubierto algunos pases a nivel de pulso que están fuera del alcance de este capítulo:

Step 0:
  SetLayout
Step 1:
  TrivialLayout
Step 2:
  FullAncillaAllocation
  EnlargeWithAncilla
  ApplyLayout
Step 3:
  Unroll3qOrMore
Step 4:
  CheckMap
Step 5:
  BarrierBeforeFinalMeasurements
  StochasticSwap
Step 6:
  UnrollCustomDefinitions
  BasisTranslator
Step 7:
  TimeUnitConversion
Step 8:
  ValidatePulseGates
  AlignMeasures

Recuerda que optimization_level=0 hace lo mínimo necesario para que el circuito funcione en el dispositivo. En particular, podemos ver que utiliza TrivialLayout para elegir un trazado inicial, luego expande el circuito para que tenga el mismo número de qubits que el dispositivo. A continuación, el transpilador desenrolla el circuito a puertas de uno y dos qubits y utiliza StochasticSwap para el enrutamiento . Por último, desenrolla todo lo posible y traduce el circuito a las puertas base del dispositivo.

Para optimization_level=3, en cambio, el PassManager contiene los siguientes pases:

Step 0:
  Unroll3qOrMore
Step 1:
  RemoveResetInZeroState
  OptimizeSwapBeforeMeasure
  RemoveDiagonalGatesBeforeMeasure
Step 2:
  SetLayout
Step 3:
  TrivialLayout
  Layout2qDistance
Step 4:
  CSPLayout
Step 5:
  DenseLayout
Step 6:
  FullAncillaAllocation
  EnlargeWithAncilla
  ApplyLayout
Step 7:
  CheckMap
Step 8:
  BarrierBeforeFinalMeasurements
  StochasticSwap
Step 9:
  UnrollCustomDefinitions
  BasisTranslator
Step 10:
  RemoveResetInZeroState
Step 11:
  Depth
  FixedPoint
  Collect2qBlocks
  ConsolidateBlocks
  UnitarySynthesis
  Optimize1qGatesDecomposition
  CommutativeCancellation
  UnrollCustomDefinitions
  BasisTranslator
Step 12:
  TimeUnitConversion
Step 13:
  ValidatePulseGates
  AlignMeasures

Este PassManager es bastante diferente. Después de desenrollar a puertas de uno y dos qubits, ya podemos ver algunos pases de optimización en el Paso 1 eliminando puertas innecesarias. A continuación, el transpilador prueba algunos enfoques diferentes de selección de diseño. En primer lugar, comprueba si el TrivialLayout es óptimo (es decir, si no necesita insertar ningún SWAP para ejecutarse en el dispositivo). Si no lo es, el transpilador intenta encontrar una disposición utilizando CSPLayout. Si CSPLayout no encuentra una solución, entonces el transpilador utiliza el algoritmo DenseLayout. A continuación (Paso 6), el transpilador añade qubits extra (si es necesario) para que los circuitos tengan el mismo número de qubits que el dispositivo. A continuación, utiliza el algoritmo StochasticSwap para hacer posibles todas las puertas de dos qubits en el mapa de acoplamiento del dispositivo. Una vez solucionado el enrutamiento, el transpilador traduce el circuito a las puertas base del dispositivo antes de intentar algunas optimizaciones finales en el Paso 11.

Mirando los pases de optimization_level=3, podemos ver que el transpilador es un programa muy sofisticado que puede tener una gran influencia en el comportamiento de tus circuitos. Afortunadamente, ahora entiendes los problemas que debe resolver el transpilador y algunos de los algoritmos que utiliza para resolverlos.

Get Guía de bolsillo Qiskit now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.