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 QuantumCircuit
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 CXGate
s:
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()
.
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()
.
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 ent_qc
, respectivamente -
Añadidos tres
CXGate
s más para intercambiar los qubits (físicos) 0y 1 -
Sustituimos nuestro
YGate
por unRZGate
y unXGate
-
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 CXGate
s. CXGate
s 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().coupling_map
para obtenerlo). Un vistazo rápido al mapa de acoplamiento de santiago
nos 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 CXGate
s 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.
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
CXGate
s 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()
.
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()
.
En la Figura 4-5, podemos ver que el paso basic_swap
ha añadido dos puertas de intercambio para llevar a cabo los CXGate
s, 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.
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.
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.
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.
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.
Nombre | Explicación |
---|---|
|
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. |
|
Este paso encuentra el grupo de qubits físicos mejor conectados y asigna los qubits del circuito a este grupo. |
|
Este paso utiliza información sobre las propiedades de ruido del dispositivo para elegir un diseño. |
|
Esta pasada utiliza el algoritmo SABRE para encontrar una disposición inicial que requiera el menor número posible de SWAPs. |
|
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 |
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
()):
(
f
'Step
{
i
}
:'
)
for
transpiler_pass
in
step
[
'passes'
]:
(
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.