Перейти до основного вмісту

Моделі програмування

Моделі програмування — це фундаментальні специфікації, які визначають, як структурується та виконується програмне забезпечення. Вони надають розробникам основу для вираження алгоритмів і організації коду, часто абстрагуючи низькорівневі деталі апаратного забезпечення або середовища виконання. Різні моделі підходять для різних типів задач і апаратних архітектур, пропонуючи різний ступінь абстракції та контролю.

У цьому уроці ми розглянемо квантові та класичні моделі програмування і подивимося, як їх можна поєднати для роботи алгоритмів у гетерогенних середовищах. Огляд дає Іскандар Сітдіков у наступному відео.

Модель програмування для QPU

Почнемо з моделі програмування для квантових комп'ютерів. Фундаментальна модель програмування, знайома майже всім квантовим розробникам, — це квантова схема (Circuit). Ми не будемо вдаватися в деталі моделі квантової схеми тут, оскільки вже є чудова лекція Джона Ватруса, яка пояснює це докладно. Зазначимо лише, що схема складається з набору ліній (так званих дротів), що представляють кубіти, гейтів, що представляють операції над квантовими станами, та набору вимірювань.

Діаграма квантової схеми, де кубіти зображені горизонтальними лініями, а квантові гейти — блоками або з'єднаннями між кубітами.

Ще одна важлива концепція моделі програмування для квантових обчислень — це так звані обчислювальні примітиви. Ці примітиви представляють деякі найпоширеніші завдання, які користувачі прагнуть виконати за допомогою квантового комп'ютера. Наразі доступні кілька примітивів, зокрема Executor. У цьому курсі ми зосередимося головним чином на примітивах Sampler і Estimator. Sampler дає тобі можливість вибирати стан, підготовлений твоєю квантовою схемою. Він показує, які стани обчислювального базису складають квантовий стан, підготовлений на твоїй квантовій схемі. Estimator дає змогу оцінювати очікуване значення спостережуваної величини для системи у стані, підготовленому твоєю квантовою схемою. Поширений контекст — оцінювання енергії системи в конкретному стані.

Модельна гістограма результатів від sampler. Деякі стани дуже ймовірно виміряти, інші — дуже малоймовірно.

Остання річ, про яку ми поговоримо в цьому розділі, — це транспіляція (transpilation). Транспіляція — це процес переписування заданої вхідної схеми відповідно до фізичних обмежень і архітектури набору інструкцій (ISA) конкретного квантового пристрою. Подібно до класичних компіляторів, це означає переведення абстрактних унітарних операцій у набір нативних гейтів цільового пристрою. Транспіляція також оптимізує інструкції схеми для ефективного виконання на шумних квантових комп'ютерах — процедура поступово змінює структуру схеми, застосовуючи кілька етапів оптимізації.

Діаграма транспіляції, що показує, як абстрактна схема відображається в схему архітектури набору інструкцій. Тобто схема переписується з використанням нативних гейтів і топології цільового апаратного забезпечення.

Перевір своє розуміння

Скільки кубітів у схемі нижче? Діаграма схеми з чотирма горизонтальними лініями та багатьма гейтами.

Відповідь:

Чотири.

Перевір своє розуміння

Припустимо, ти моделюєш електрони в молекулі. Тобі потрібно наближено знайти (a) енергію основного стану молекули та (b) які стани обчислювального базису домінують в основному стані молекули. У кожному випадку — який примітив ти б використав: Estimator чи Sampler?

Відповідь:

(a) Estimator (b) Sampler

Класичні моделі програмування

Існує багато моделей програмування для класичних комп'ютерів, але в цьому розділі ми зосередимося на двох найпопулярніших: паралельне програмування та робочі процеси задач (task workflows). Використовуючи ці дві моделі поряд із квантовими моделями програмування, можна виразити майже будь-який гібридний квантово-класичний робочий процес будь-якої складності.

Паралельне програмування

Паралельне програмування — це модель, яка ділить програму на підзадачі, що можуть виконуватися одночасно. Існує дві основні парадигми паралельного програмування:

  • Паралелізм із спільною пам'яттю (Open Multiprocessing, або OpenMP): використовується для завантаження кількох ядер на одному обчислювальному вузлі. Потоки виконання спільно використовують єдиний простір пам'яті.

  • Паралелізм із розподіленою пам'яттю (Message Passing Interface, або MPI): використовується для масштабування між кількома окремими обчислювальними вузлами. Кожен процес має власний ізольований простір пам'яті.

Тут ми зосередимося на моделі розподіленої пам'яті, оскільки вона є основою для багатовузлових суперкомп'ютерних обчислень і координації великих гетерогенних квантово-класичних задач.

Щоб ефективно працювати з моделями паралельного програмування з розподіленою пам'яттю, потрібно розуміти кілька понять:

  • Процес (Process) — незалежний екземпляр програми з власним простором пам'яті.
  • Ранг (Rank) — унікальний цілочисельний ідентифікатор, що призначається кожному процесу; використовується для ідентифікації відправника та одержувача під час комунікації (не обов'язково означає пріоритет).
  • Синхронізація (Synchronization) — механізм координації між різними рангами та процесами.
  • Єдина програма, різні дані (Single program, multiple data, SPMD) — абстрактна обчислювальна модель, де один екземпляр вихідного коду одночасно виконується на кількох процесах, кожен з яких обробляє різну підмножину загальних даних.
  • Передача повідомлень (Message passing) — парадигма комунікації в архітектурах із розподіленою пам'яттю, яка дозволяє незалежним процесам обмінюватися даними та проміжними результатами. Вона спирається на явні операції "відправлення" та "отримання" для координації виконання між різними обчислювальними вузлами.

Існує стандарт MPI, що реалізує цю парадигму передачі повідомлень для паралельних архітектур. MPI є функціональним втіленням усіх перерахованих концепцій, надаючи конкретні виклики бібліотек для управління процесами, присвоєння рангів, синхронізації та передачі повідомлень у рамках моделі SPMD. Зібравши всі ці концепції разом, можна сказати, що виконання паралельної програми відбувається таким чином:

  • Одна скомпільована програма (той самий бінарний файл) копіюється та запускається засобом запуску задач для створення кількох паралельних процесів на кількох вузлах.
  • Основний потік керування програмою визначається рангом процесу. Це принцип SPMD у дії: програма використовує умовну логіку (наприклад, if (rank == 0)), щоб гарантувати, що лише певні, розпаралелені секції коду виконуються робочими процесами, тоді як головний процес (зазвичай Rank 0) займається ініціалізацією та кінцевою агрегацією.
  • Комунікація між процесами відбувається через передачу повідомлень (за допомогою MPI), яка викликається щоразу, коли процесу потрібно обмінятися даними або проміжними результатами з іншим рангом.

Візуально це виглядатиме приблизно так:

Діаграма задачі, розподіленої між вузлами.

Спробуємо застосувати деякі концепції, які ми щойно вивчили, до коду.

Спочатку ми спробуємо запустити просту паралельну програму "hello world" за допомогою OpenMPI — реалізації протоколу MPI, стандарту передачі повідомлень у паралельному програмуванні. Тут ми використаємо Python-пакет mpi4py, який є Python-прив'язкою для стандарту 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")

~
~

Для запуску цієї програми ми використаємо два вузли, що вкажемо у скрипті подачі задачі.

$ 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

Потім запускаємо shell-скрипт.

$ sbatch mpi-hello-world.sh

Перевіримо журнали результатів задачі.

$ 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}

Тут ми використали два вузли, і процес на кожному вузлі тепер ідентифікується рангом — Rank 0 і Rank 1 — які використовуються для визначення потоку керування програмою.

Робочі процеси задач

Тепер поговоримо про модель програмування робочих процесів задач (Task workflow). Робочий процес задач абстрагує обчислення у вигляді направленого ациклічного графа (DAG). У цьому графі кожен вузол представляє конкретну задачу або роботу, а ребра (стрілки між вузлами) представляють залежності (за даними та порядком) між ними. Планувальник (scheduler) — це компонент, що розподіляє задачі по ресурсах і координує їх виконання.

Конкретним прикладом застосування моделі робочого процесу задач до квантових обчислень є фреймворк Qiskit patterns. Qiskit pattern — це загальна система, призначена для розбиття прикладних задач на послідовність етапів, особливо для квантових задач. Це дозволяє безперешкодно компонувати нові можливості, розроблені дослідниками IBM Quantum® (та іншими), і відкриває перспективу виконання квантових обчислювальних задач на потужній гетерогенній (CPU/GPU/QPU) обчислювальній інфраструктурі. Чотири кроки Qiskit pattern — це відображення (mapping), оптимізація (optimization), виконання (execution) та постобробка (post-processing), де всі задачі виконуються одна за одною в конвеєрі. Але з робочими процесами задач ми не обмежені лінійним порядком виконання та можемо виконувати задачі паралельно. Кожна задача в робочому процесі може бути цілим паралельним завданням самостійно. Отже, можна поєднувати ці моделі для опису алгоритмів довільної складності, а менеджер навантаження на кшталт Slurm все це оброблятиме.

Діаграма обчислювальних задач, організованих у робочий процес, у якому деякі процеси виконуються паралельно, а інші — послідовно.

Зображення вище ілюструє Qiskit pattern у дії. Робочий процес має графову структуру з чотирма етапами. Цю розгалужену структуру координує та виконує планувальник. На початковому етапі задача відображається у форму, придатну для квантового виконання (квантова схема). На наступному етапі ця квантова схема оптимізується для конкретного квантового апаратного забезпечення. Зображення показує це як паралельний процес, демонструючи, як кілька стратегій оптимізації можуть застосовуватися одночасно. Оптимізована квантова схема потім виконується на реальному квантовому апаратному забезпеченні. Це третій етап зображення, де планувальник працює з одним фіолетовим квантовим процесорним блоком. Нарешті, результати постобробляються класичними ресурсами.

Навіщо обидві?

То навіщо нам потрібні і паралельне програмування, і робочі процеси задач? За всіх розмов про квантовий паралелізм варто уточнити, що не все в квантових обчисленнях є паралельним.

Попередній урок про робочий процес SQD згадував деякі процеси, які не можна розпаралелити. Наприклад, нам потрібні результати багатьох квантових вимірювань, щоб спроєктувати нашу матрицю в підпростір tractable-розмірності. У свою чергу, нам потрібна діагоналізована матриця та пов'язані вектори стану, щоб перевірити самоузгодженість квантових вимірювань (використовуючи, наприклад, збереження заряду). Після всього цього нам потрібно вирішити, чи достатньо збіглася енергія основного стану для наших цілей. Ці кроки є обов'язково послідовними і вимагають перевірки умов збіжності та самоузгодженості перед продовженням.

Схема робочого процесу, специфічного для sample-based quantum diagonalization. Кроки включають варіаційну квантову схему, викорис�тання вимірювань для проєкції гамільтоніана у підпростір, потім класичний оптимізатор для оновлення варіаційних параметрів у схемі та повторення.

До цього робочого процесу ми повернемося детальніше та реалізуємо його в наступному розділі. Єдине, що потрібно запам'ятати з цього розділу, — це те, що робочі процеси задач є необхідними.

Практика програмування

Краса моделей програмування в тому, що їх можна поєднувати між собою. Знаючи квантові та класичні моделі програмування, можна описати гетерогенні обчислення довільної складності та виконати їх на апаратному забезпеченні. Попрактикуємося на невеликому прикладі комбінованого робочого процесу, який реалізує Qiskit pattern (відображення, оптимізація, виконання та постобробка) у рамках Slurm, з яким ми познайомилися в попередньому розділі. Кожна з чотирьох задач буде окремою задачею Slurm зі своїми ресурсами. Задача оптимізації використовуватиме MPI для паралельної оптимізації схем (лише для прикладу, як на зображенні вище). Задача виконання використовуватиме квантові ресурси та квантові моделі програмування (схему і sampler). Остання задача — постобробка — знову використовуватиме MPI паралельно з класичними ресурсами.

Відображення

Програма mapping.py призначена для побудови схеми PauliTwoDesign, яка часто використовується в літературі з квантового машинного навчання та квантових бенчмарків, з простою спостережуваною, що вимірює (n1)th(n-1)^\text{th} кубіт у напрямку ZZ для nn-кубітної системи з випадковими початковими параметрами. Кожен з цих артефактів (квантова схема, перетворена у файл qasm, спостережувана та параметри) зберігатиметься в окремому файлі у директорії даних і використовуватиметься як вхідні дані на етапі оптимізації.

Shell-скрипт цього етапу (mapping.sh) виглядає так:

#!/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

де визначаються назва задачі, формат виведення та кількість вузлів/задач/ЦП.

Оптимізація

Програма optimization.py починає з завантаження файлів з етапу відображення. Тут ти використовуватимеш QRMI для підключення квантових ресурсів до цієї програми.

qrmi = QRMI()
resources = qrmi.resources()
quantum_resource = resources[0]
...

Потім виконується легка оптимізація з параметром optimization_level=1 для транспіляції квантової схеми та застосування розміщення схеми до спостережуваної, після чого все зберігається у папці даних.

Shell-скрипт цього етапу (optimization.sh) виглядає так:

#!/bin/bash
#SBATCH --job-name=optimization
#SBATCH --output=output/optimization.out
#SBATCH --ntasks=4
#SBATCH --partition=classical

srun python3 /tmp/optimization.py

Тут --ntasks=4 запитує чотири класичні задачі у Slurm для паралельного процесу.

Виконання

Це ключовий квантовий етап, де оптимізована квантова схема з попереднього кроку запускається на QPU за допомогою Estimator. Для цього спочатку ми завантажимо три файли — транспільовану квантову схему, спостережувану та початкові параметри — а потім передамо їх до Estimator. Він обчислює оціночне значення спостережуваної та виводить його.

Скрипт execution.sh використовує плагін Slurm для роботи з квантовим ресурсом.

#!/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

Постобробка

Крок постобробки часто включає класичну діагоналізацію та перевірки самоузгодженості. Він може бути й ітеративним. Найкраще розглядати крок постобробки в наступному уроці, де фізичний контекст і мета ітеративних кроків стануть зрозумілішими.

Об'єднання всього разом

Ми можемо об'єднати всі ці задачі в один робочий процес за допомогою аргументу залежності для команди 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)

Й можна перевірити нашу чергу виконання 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)

Це був навчальний приклад для демонстрації поєднання моделей програмування. У наступному розділі ми розглянемо реальні алгоритми та продемонструємо моделі програмування і управління ресурсами на практичних робочих процесах.

Підсумок

У цьому уроці ми показали, як поєднати кілька класичних і квантових моделей програмування для побудови, управління та виконання повного чотириетапного робочого процесу. Ми почали з фундаментальних концепцій квантових схем та примітивів, потім дослідили класичні моделі, такі як паралельне програмування та робочі процеси задач. Об'єднавши всі концепції, ми побудували Qiskit pattern — відображення, оптимізація, виконання та постобробка — під управлінням менеджера навантаження Slurm з простою квантовою схемою та спостережуваною.

У наступному уроці ми використаємо цю основу для запуску вибіркових квантових алгоритмів і покажемо, як цей робочий процес можна застосувати для вирішення реальних задач.

Весь код і скрипти, використані в цьому розділі, доступні тобі у цьому репозиторії Github.