Modelos de programación
Los modelos de programación son especificaciones fundamentales que definen cómo se estructura y ejecuta el software. Proporcionan a los desarrolladores un framework para expresar algoritmos y organizar código, abstrayendo a menudo los detalles del hardware subyacente o del entorno de ejecución. Diferentes modelos son adecuados para distintos tipos de problemas y arquitecturas de hardware, ofreciendo diferentes niveles de abstracción y control.
En esta lección, cubriremos los modelos de programación cuánticos y clásicos y mostraremos cómo podemos combinarlos para ejecutar algoritmos en entornos heterogéneos. Iskandar Sitdikov nos ofrece una visión general en el siguiente video.
Modelo de programación para QPU
Comenzamos con el modelo de programación para computadores cuánticos. El modelo de programación fundamental, familiar para prácticamente todos los desarrolladores cuánticos, es el circuito cuántico. No entraremos aquí en los detalles del modelo de circuito cuántico, ya que tenemos una excelente lección de John Watrous que lo explica en detalle. Solo mencionaremos que un circuito consiste en una serie de líneas (llamadas cables) que representan qubits, puertas que representan operaciones sobre estados cuánticos, y una serie de mediciones.
Otro concepto importante del modelo de programación para la computación cuántica son los llamados primitivos computacionales. Estos primitivos representan algunas de las tareas más comunes que los usuarios desean realizar con un computador cuántico. Actualmente hay varios primitivos disponibles, incluyendo Executor. En este curso nos enfocamos principalmente en los primitivos Sampler y Estimator. Sampler te da la capacidad de muestrear un estado preparado por tu circuito cuántico. Te indica qué estados de la base computacional componen el estado cuántico preparado en tu circuito cuántico. Estimator te permite estimar el valor esperado de un observable para un sistema en el estado preparado por tu circuito cuántico. Un caso de uso frecuente es la estimación de la energía de un sistema en un estado determinado.
Lo último de lo que hablaremos en esta sección es la transpilación. La transpilación es el proceso de reescribir un circuito de entrada dado para que cumpla con las restricciones físicas y la Arquitectura del Conjunto de Instrucciones (ISA) de un dispositivo cuántico específico. Similar a los compiladores clásicos, esto significa traducir operaciones unitarias abstractas al conjunto de puertas nativas que el dispositivo objetivo puede ejecutar. Además, optimiza las instrucciones del circuito para una ejecución eficiente en computadores cuánticos ruidosos, modificando la estructura del circuito gradualmente a través de múltiples fases de optimización.
Verifica tu comprensión
¿Cuántos qubits contiene el siguiente circuito?

Respuesta:
Cuatro.
Verifica tu comprensión
Supón que estás modelando los electrones en una molécula. Quieres (a) aproximar la energía del estado fundamental de la molécula y (b) descubrir qué estados de la base computacional dominan más en el estado fundamental de la molécula. ¿Qué primitivo usarías en cada caso: Estimator o Sampler?
Respuesta:
(a) Estimator (b) Sampler
Modelos de programación clásicos
Existen muchos modelos de programación para computadores clásicos, pero en esta sección nos enfocamos en dos de los más populares: la programación paralela y los flujos de trabajo de tareas. Con estos dos modelos junto con los modelos de programación cuántica, se puede describir prácticamente cualquier flujo de trabajo híbrido cuántico-clásico de cualquier complejidad.
Programación paralela
La programación paralela es un modelo que divide un programa en subproblemas que pueden ejecutarse simultáneamente. Hay dos paradigmas principales de programación paralela:
-
Memoria compartida (Open Multiprocessing, u OpenMP): Se utiliza para aprovechar múltiples núcleos dentro de un solo nodo de cómputo. Los hilos de ejecución comparten un espacio de memoria común.
-
Memoria distribuida (Message Passing Interface, o MPI): Se utiliza para escalar a través de múltiples nodos de cómputo separados. Cada proceso tiene su propio espacio de memoria aislado.
Aquí nos enfocamos en el modelo de memoria distribuida, ya que es esencial para la supercomputación multinodo y la coordinación de trabajos heterogéneos cuántico-clásicos a gran escala.
Hay algunos conceptos que debemos entender para trabajar en modelos de programación paralela con memoria distribuida:
- Proceso – Una instancia independiente del programa con su propio espacio de memoria.
- Rango – Un identificador entero único asignado a cada proceso, utilizado específicamente para identificar al emisor y receptor en la comunicación (no necesariamente un "rango" en el sentido de priorización).
- Sincronización – Un mecanismo de coordinación entre diferentes rangos y procesos.
- Single Program, Multiple Data (SPMD) – Un modelo de cómputo abstracto en el que una sola instancia de código fuente se ejecuta simultáneamente en múltiples procesos, cada uno trabajando en un subconjunto diferente de los datos totales.
- Paso de mensajes – El paradigma de comunicación en arquitecturas de memoria distribuida que permite a procesos independientes intercambiar datos y resultados intermedios. Se basa en operaciones explícitas de "enviar" y "recibir" para coordinar la ejecución entre diferentes nodos de cómputo.
Existe un estándar llamado MPI que implementa este paradigma de paso de mensajes para arquitecturas paralelas. MPI es la materialización funcional de todos estos conceptos y proporciona las llamadas de biblioteca específicas necesarias para gestionar procesos, asignar rangos, facilitar la sincronización y permitir el paso de mensajes bajo el modelo SPMD. Reuniendo todos estos conceptos, la ejecución de un programa paralelo se puede describir de la siguiente manera:
- Un solo programa compilado (el mismo binario) es copiado y ejecutado por un lanzador de trabajos para crear múltiples procesos paralelos en múltiples nodos.
- El flujo de control principal del programa se determina por el rango del proceso. Este es el principio SPMD en acción: el programa utiliza lógica condicional (por ejemplo,
if (rank == 0)) para asegurar que solo ciertas secciones paralelizadas del código sean ejecutadas por los procesos trabajadores, mientras que un proceso maestro (a menudo Rank 0) maneja la inicialización y la agregación final. - La comunicación entre procesos ocurre a través del paso de mensajes (con MPI), que se invoca siempre que un proceso necesita intercambiar datos o resultados intermedios con otro rango.
Visualmente, esto se ve aproximadamente así:
Intentemos ahora traducir algunos de los conceptos recién aprendidos a código.
Primero, intentemos ejecutar un programa paralelo sencillo "Hello World" con OpenMPI, una implementación del protocolo MPI, un estándar para el paso de mensajes en la programación paralela. Aquí usamos el paquete Python mpi4py, que proporciona una interfaz Python para el estándar Message Passing Interface (MPI).
$ vim mpi-hello-world.py
from mpi4py import MPI
import sys
comm = MPI.COMM_WORLD
rank = comm.Get_rank()
size = comm.Get_size()
sys.stdout.write(f"[Rank {rank}] Hello from process {rank} of {size}!\n")
if rank == 0:
data = {'answer': 42, 'pi': 3.14}
sys.stdout.write(f"[Rank {rank}] Sending: {data}\n")
comm.send(data, dest=1, tag=42)
elif rank == 1:
data = comm.recv(source=0, tag=42)
sys.stdout.write(f"[Rank {rank}] Received: {data}\n")
~
~
Usaremos dos nodos para ejecutar este programa, lo cual especificamos en nuestro script de envío.
$ vim mpi-hello-world.sh
#!/bin/bash
#
#SBATCH --job-name=mpi-hello-world
#SBATCH --output=mpi-hello-world.out
#SBATCH --nodes=2
#SBATCH --ntasks-per-node=1
#SBATCH --cpus-per-task=1
#SBATCH --partition=normal
/usr/lib64/openmpi/bin/mpirun python /data/ch3/parallel/mpi-hello-world.py
Luego ejecutamos el script de shell.
$ sbatch mpi-hello-world.sh
Podemos verificar los registros de resultados del trabajo.
$ cat mpi-hello-world.out | grep Rank
[Rank 1] Hello from process 1 of 2!
[Rank 0] Hello from process 0 of 2!
[Rank 0] Sending: {'answer': 42, 'pi': 3.14}
[Rank 1] Received: {'answer': 42, 'pi': 3.14}
Aquí hemos usado dos nodos, y el proceso en cada nodo se identifica ahora por un rango —Rank 0 y Rank 1— que se utilizan para dirigir el flujo de control del programa.
Flujos de trabajo de tareas
Pasemos ahora al modelo de programación de flujos de trabajo de tareas. Un flujo de trabajo de tareas abstrae los cálculos en un grafo acíclico dirigido (DAG). En este grafo, cada nodo representa una tarea o trabajo específico, y las aristas (las flechas que conectan los nodos) representan las dependencias (de datos y de orden) entre ellos. Un scheduler es el componente que asigna tareas a los recursos y orquesta la ejecución.
Un ejemplo concreto de un modelo de flujo de trabajo de tareas aplicado a la computación cuántica es el framework Qiskit Patterns. Un Qiskit Pattern es un framework general que descompone problemas específicos de dominio en una secuencia de fases, particularmente para tareas cuánticas. Esto permite la composición fluida de nuevas capacidades desarrolladas por investigadores de IBM Quantum® (y otros), y posibilita un futuro en el que las tareas de cómputo cuántico se ejecuten en infraestructura de cómputo heterogénea (CPU/GPU/QPU) de alto rendimiento. Los cuatro pasos de un Qiskit Pattern son Mapping, Optimización, Ejecución y Posprocesamiento, donde todas las tareas se ejecutan secuencialmente en un pipeline. Sin embargo, con los flujos de trabajo de tareas, no estamos limitados a un orden de ejecución lineal y podemos ejecutar tareas en paralelo. Cada tarea de un flujo de trabajo puede ser en sí misma un trabajo paralelo completo. Así, se pueden combinar estos modelos de cualquier manera para describir algoritmos de cualquier complejidad, y un gestor de cargas de trabajo como Slurm se encarga de la gestión.
La imagen anterior muestra el Qiskit Pattern en acción. El flujo de trabajo tiene una estructura de grafo con cuatro fases. Esta estructura ramificada es orquestada y ejecutada por el scheduler. El problema se convierte en una forma ejecutable cuánticamente (circuito cuántico) en la fase inicial. En la siguiente fase, este circuito cuántico se optimiza para el hardware cuántico específico. La imagen muestra esto como un proceso paralelo, lo que ilustra cómo se podrían aplicar múltiples estrategias de optimización simultáneamente. El circuito cuántico optimizado se ejecuta entonces en el hardware cuántico real. Esta es la tercera fase de la imagen, donde el scheduler trabaja con una unidad de procesamiento cuántico morada. Finalmente, los resultados son posprocesados por recursos clásicos.
¿Por qué ambos?
Entonces, ¿por qué necesitamos tanto la programación paralela como los flujos de trabajo de tareas? Con toda la discusión sobre el paralelismo cuántico, vale la pena aclarar que en la computación cuántica no todo se ejecuta en paralelo.
La lección anterior sobre el flujo de trabajo SQD menciónó algunos procesos que no pueden paralelizarse. Por ejemplo, necesitamos los resultados de muchas mediciones cuánticas para proyectar nuestra matriz en un subespacio de dimensión manejable. Para ello, a su vez necesitamos la matriz diagonalizada y los vectores de estado asociados para verificar la autoconsistencia de las mediciones cuánticas (por ejemplo, mediante la conservación de la carga). Después, debemos decidir si la energía del estado fundamental ha convergido suficientemente para nuestros fines. Estos pasos son necesariamente secuenciales y requieren probar condiciones de convergencia y autoconsistencia antes de poder continuar.
Este flujo de trabajo se tratará con más detalle en la próxima sección y se implementará. Lo único que necesitas llevarte de esta sección es que los flujos de trabajo de tareas son necesarios.
Práctica de programación
Lo hermoso de los modelos de programación es que todos se pueden combinar. Con conocimientos sobre modelos de programación cuánticos y clásicos, puedes describir y ejecutar en hardware una computación heterogénea de cualquier complejidad. Practiquemos esto con un pequeño ejemplo de un flujo de trabajo combinado que implementa el Qiskit Pattern (Map, Optimize, Execute y Post-Process) dentro de Slurm, que aprendimos en el capítulo anterior. Cada una de las cuatro tareas es un trabajo Slurm separado con sus propios recursos. La tarea de optimización utiliza MPI para optimizar circuitos en paralelo (solo como ejemplo, como en la imagen anterior). La tarea de ejecución utiliza recursos cuánticos y modelos de programación cuánticos (Circuito y Sampler). La última tarea —posprocesamiento— utiliza nuevamente MPI en paralelo con recursos clásicos.
Mapping
El programa mapping.py está diseñado para crear un circuito PauliTwoDesign, frecuentemente utilizado en la literatura de aprendizaje automático cuántico y en la literatura de benchmarking cuántico, con un observable simple que mide el qubit en la dirección de un sistema de qubits con parámetros iniciales aleatorios. Cada uno de estos elementos (el circuito cuántico convertido a un archivo QASM, el observable y los parámetros) se guarda en un archivo separado en el directorio de datos y se utiliza como entrada en la fase de optimización.
El script de shell de esta fase (mapping.sh) es
#!/bin/bash
#
#SBATCH --job-name=mapping
#SBATCH --output=mapping.out
#SBATCH --nodes=1
#SBATCH --ntasks-per-node=1
#SBATCH --cpus-per-task=1
#SBATCH --partition=normal
srun python /data/ch3/workflows/mapping.py
y define el nombre del trabajo, el formato de salida y el número de nodos/tareas/CPU.
Optimización
El programa optimization.py comienza cargando archivos de la fase de mapping. Aquí se utiliza QRMI para incorporar recursos cuánticos en este programa.
qrmi = QRMI()
resources = qrmi.resources()
quantum_resource = resources[0]
...
Luego realiza una optimización ligera estableciendo optimization_level=1 para transpilar el circuito cuántico y aplicar el diseño del circuito al observable, y los guarda en la carpeta de datos.
El script de shell de esta fase (optimization.sh) es
#!/bin/bash
#SBATCH --job-name=optimization
#SBATCH --output=output/optimization.out
#SBATCH --ntasks=4
#SBATCH --partition=classical
srun python3 /tmp/optimization.py
Aquí, --ntasks=4 solicita cuatro tareas clásicas de Slurm para un proceso paralelo.
Ejecución
Esta es la fase cuántica central, donde el circuito cuántico optimizado del paso anterior es ejecutado por el Estimator en la QPU. Para ello, primero cargamos tres archivos —el circuito cuántico transpilado, el observable y los parámetros iniciales— y luego los pasamos al Estimator. Este proporciona el valor estimado del observable y lo muestra.
El script execution.sh utiliza un plugin de Slurm para usar un recurso cuántico.
#!/bin/bash
#
#SBATCH --job-name=execution
#SBATCH --output=execution.out
#SBATCH --nodes=1
#SBATCH --ntasks-per-node=1
#SBATCH --cpus-per-task=1
#SBATCH --partition=quantum
#SBATCH --gres=qpu:1
srun python /data/ch3/workflows/execution.py
Posprocesamiento
El paso de posprocesamiento a menudo incluye diagonalización clásica y verificaciones de autoconsistencia. También puede ser iterativo. Tiene más sentido examinar el paso de posprocesamiento en la próxima lección, donde el contexto físico y el propósito de los pasos iterativos quedarán claros.
Combinándolo todo
Podemos integrar todas estas tareas en un flujo de trabajo utilizando el argumento de dependencia del comando sbatch:
$ MAPPING_JOB=$(sbatch --parsable mapping.sh)
$ OPTIMIZE_JOB=$(sbatch --parsable --dependency=afterok:$MAPPING_JOB optimization.sh)
$ EXECUTE_JOB=$(sbatch --parsable --dependency=afterok:$OPTIMIZE_JOB execute.sh)
Y podemos verificar nuestra cola de ejecución de Slurm.
$ squeue
# JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
# 3 classical mapping admin PD 0:00 1 (None)
# 4 classical optimiza admin PD 0:00 1 (Dependency)
# 5 quantum execute admin PD 0:00 1 (Dependency)
Este fue un ejemplo de juguete para demostrar la combinación de modelos de programación. En el próximo capítulo, examinaremos algoritmos reales y demostraremos modelos de programación y gestión de recursos en flujos de trabajo útiles.
Resumen
En esta lección, hemos mostrado cómo combinar múltiples modelos de programación clásicos y cuánticos para crear, gestionar y ejecutar un flujo de trabajo completo de cuatro etapas. Comenzamos con los conceptos fundamentales de circuitos cuánticos y primitivos, luego exploramos modelos clásicos como la programación paralela y los flujos de trabajo de tareas. Combinando todos los conceptos, construimos un Qiskit Pattern —Map, Optimize, Execute y Post-Process— orquestado por el gestor de cargas de trabajo Slurm con un circuito cuántico simple y un observable.
En la próxima lección, utilizaremos este framework para ejecutar algoritmos cuánticos basados en muestreo y mostrar cómo este flujo de trabajo puede aplicarse a problemas significativos.
Todo el código y los scripts de este capítulo están disponibles para ti en este repositorio de Github.