Programovací modely
Programovací modely jsou základní specifikace, které definují, jak je software strukturován a spouštěn. Poskytují vývojářům rámec pro vyjádření algoritmů a organizaci kódu, přičemž často abstrahují nízkoúrovňové detaily základního hardwaru nebo prostředí spouštění. Různé modely jsou vhodné pro různé typy problémů a hardwarové architektury a nabízejí různé úrovně abstrakce a kontroly.
V této lekci si projdeme kvantové a klasické programovací modely a uvidíme, jak je můžeme kombinovat pro provoz algoritmů v heterogenních prostředích. Iskandar Sitdikov nám v následujícím videu podá přehled.
Programovací model pro QPU
Začneme programovacím modelem pro kvantové počítače. Základním programovacím modelem, který je povědomý téměř všem kvantovým vývojářům, je kvantový Circuit. Nebudeme zde zacházet do podrobností modelu kvantového Circuit, protože již máme skvělou přednášku Johna Watrouse, která to vysvětluje podrobně. Zmíníme pouze, že Circuit se skládá ze sady čar (nazývaných dráty), které představují Qubit, Gate, které představují operace na kvantových stavech, a sady měření.
Dalším důležitým konceptem programovacího modelu pro kvantové výpočty je to, čemu říkáme výpočetní primitiva. Tato primitiva představují některé z nejběžnějších úloh, které uživatelé chtějí s kvantovým počítačem provádět. V současné době je k dispozici několik primitiv, včetně Executor. V tomto kurzu se zaměříme především na primitiva Sampler a Estimator. Sampler ti umožňuje vzorkovat stav připravený tvým kvantovým Circuit. Říká ti, které výpočetní bázové stavy tvoří kvantový stav připravený na tvém kvantovém Circuit. Estimator ti umožňuje odhadnout střední hodnotu pozorovatelné veličiny pro systém ve stavu připraveném tvým kvantovým Circuit. Běžným kontextem je odhadování energie systému v konkrétním stavu.
Poslední věcí, o které budeme v této části mluvit, je transpilace. Transpilace je proces přepisu daného vstupního Circuit tak, aby odpovídal fyzickým omezením a architektuře instrukční sady (ISA) konkrétního kvantového zařízení. Podobně jako klasické kompilátory to znamená převod abstraktních unitárních operací na nativní sadu Gate, kterou může cílové zařízení spustit. Také optimalizuje instrukce Circuit pro efektivní spuštění na hlučných kvantových počítačích, přičemž rutina postupně mění strukturu Circuit použitím několika optimalizačních fází.
Ověř si své znalosti
Kolik Qubit je v níže uvedeném Circuit?

Odpověď:
Čtyři.
Ověř si své znalosti
Předpokládejme, že modeluješ elektrony v molekule. Chceš aproximovat (a) energii základního stavu molekuly a (b) které výpočetní bázové stavy jsou v základním stavu molekuly nejdominantnější. V každém případě bys použil/a primitivum Estimator nebo Sampler?
Odpověď:
(a) Estimator (b) Sampler
Klasické programovací modely
Existuje mnoho programovacích modelů pro klasické počítače, ale v této části se zaměříme na dva nejpopulárnější: paralelní programování a task workflow. Pomocí těchto dvou modelů společně s kvantovými programovacími modely lze vyjádřit téměř jakýkoli hybridní kvantově-klasický výpočetní postup libovolné složitosti.
Paralelní programování
Paralelní programování je model, který rozděluje program na dílčí úlohy, jež lze provádět současně. Existují dvě hlavní paradigmata paralelního programování:
-
Paralelismus se sdílenou pamětí (Open Multiprocessing, neboli OpenMP): Slouží k využití více jader v rámci jednoho výpočetního uzlu. Vlákna sdílejí jediný paměťový prostor.
-
Paralelismus s distribuovanou pamětí (Message Passing Interface, neboli MPI): Slouží ke škálování přes více oddělených výpočetních uzlů. Každý proces má vlastní izolovaný paměťový prostor.
Zde se zaměříme na model s distribuovanou pamětí, protože je nezbytný pro víceuzlový superpočítačový provoz a koordinaci rozsáhlých heterogenních kvantově-klasických úloh.
Abychom mohli pracovat s modely paralelního programování s distribuovanou pamětí, potřebujeme pochopit několik klíčových pojmů:
- Proces – Nezávislá instance programu s vlastním paměťovým prostorem.
- Rank – Jedinečný celočíselný identifikátor přiřazený každému procesu, který se používá k identifikaci odesílatele a příjemce při komunikaci (nejde nutně o „rank" ve smyslu priority).
- Synchronizace – Mechanismus koordinace mezi různými ranky a procesy.
- Single program, multiple data (SPMD) – Abstraktní výpočetní model, ve kterém jedna instance zdrojového kódu běží současně na více procesech, přičemž každý zpracovává jinou část celkových dat.
- Předávání zpráv – Komunikační paradigma používané v architekturách s distribuovanou pamětí, které umožňuje nezávislým procesům vyměňovat si data a mezivýsledky. Opírá se o explicitní operace „send" a „receive" pro koordinaci provádění mezi různými výpočetními uzly.
Existuje standard MPI, který implementuje toto paradigma předávání zpráv pro paralelní architektury. MPI je funkčním ztělesněním všech výše uvedených konceptů a poskytuje konkrétní volání knihovny potřebná ke správě procesů, přidělování ranků, usnadnění synchronizace a umožnění předávání zpráv v rámci modelu SPMD. Když všechny tyto koncepty shrneme, lze říci, že prov ádění paralelního programu probíhá následovně:
- Jeden zkompilovaný program (stejný binární soubor) je zkopírován a spuštěn pomocí spouštěče úloh, který vytvoří několik paralelních procesů napříč více uzly.
- Hlavní řídicí tok programu je určen rankem procesu. To je princip SPMD v praxi: program používá podmíněnou logiku (například
if (rank == 0)), aby zajistil, že pouze určité, paralelizované části kódu jsou prováděny pracovními procesy, zatímco hlavní proces (obvykle Rank 0) zajišťuje inicializaci a závěrečnou agregaci. - Komunikace mezi procesy probíhá prostřednictvím předávání zpráv (pomocí MPI), které se volá vždy, když proces potřebuje vyměnit data nebo mezivýsledky s jiným rankem.
Vizuálně to vypadá přibližně takto:
Zkusme nyní na kódu aplikovat některé z právě probraných konceptů.
Nejprve se pokusíme spustit jednoduchý paralelní program „hello world" pomocí OpenMPI, což je implementace protokolu MPI – standardu pro předávání zpráv v paralelním programování. Použijeme Python balíček mpi4py, což je Python binding pro standard 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")
~
~
K spuštění tohoto programu použijeme dva uzly, které zadáme v našem skriptu pro odesílání úlohy.
$ 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
Poté spusť shell skript.
$ sbatch mpi-hello-world.sh
Můžeme zkontrolovat protokoly výsledků úlohy.
$ 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}
Zde jsme použili dva uzly a proces na každém uzlu je nyní identifikován rankem – Rank 0 a Rank 1 – které slouží k rozhodování o řídicím toku programu.
Task workflow
Nyní si proberme programovací model task workflow. Task workflow abstrahuje výpočet do orientovaného acyklického grafu (DAG). V tomto grafu každý uzel představuje konkrétní úlohu nebo job a hrany (šipky spojující uzly) představují závislosti (datové a pořadové) mezi nimi. Plánovač je komponenta, která mapuje úlohy na zdroje a řídí jejich provádění.
Konkrétním příkladem modelu task workflow aplikovaného na kvantové výpočty je framework Qiskit patterns. Qiskit pattern je obecný framework navržený k rozložení problémů specifických pro danou doménu do posloupnosti fází, zejména pro kvantové úlohy. Umožňuje bezproblémovou skládatelnost nových schopností vyvinutých výzkumníky IBM Quantum® (a dalšími) a otevírá cestu k budoucnosti, v níž jsou kvantové výpočetní úlohy prováděny výkonnou heterogenní (CPU/GPU/QPU) výpočetní infrastrukturou. Čtyři kroky Qiskit patternu jsou mapování, optimalizace, provádění a post-processing, přičemž všechny úlohy jsou prováděny jedna po druhé v pipeline. Ale s task workflow nejsme vázáni na lineární pořadí provádění a můžeme úlohy provádět paralelně. Každá úloha workflow může být celou paralelní úlohou sama o sobě. Takže lze tyto modely libovolně kombinovat k popisu libovolně složitých algoritmů a správce zátěže jako Slurm se o ně postará.
Obrázek výše ilustruje Qiskit pattern v akci. Workflow má grafovou strukturu se čtyřmi fázemi. Tuto větvící se strukturu orchestruje a provádí plánovač. Problém je v počáteční fázi mapován do kvantově spustitelné podoby (kvantový obvod). V další fázi je tento kvantový obvod optimalizován pro konkrétní kvantový hardware. Obrázek to zobrazuje jako paralelní proces, což demonstruje, jak lze současně aplikovat více optimalizačních strategií. Optimalizovaný kvantový obvod je poté spuštěn na skutečném kvantovém hardwaru. To je třetí fáze obrázku, kde plánovač pracuje s jednou fialovou kvantovou procesorovou jednotkou. Nakonec jsou výsledky post-processovány klasickými zdroji.
Proč obojí?
Proč tedy potřebujeme jak paralelní programování, tak task workflow? Při vší té řeči o kvantovém paralelismu stojí za to objasnit, že ne vše je v kvantových výpočtech paralelní.
Předchozí lekce o workflow SQD zmínila některé procesy, které nelze paralelizovat. Například potřebujeme výsledky mnoha kvantových měření, abychom mohli promítnout naši matici do podprostoru zvládnutelné dimenze. Poté potřebujeme diagonalizovanou matici a příslušné stavové vektory, abychom mohli zkontrolovat sebekonzistenci kvantových měření (pomocí například zachování náboje). Po tom všem musíme rozhodnout, zda energie základního stavu dostatečně konvergovala pro naše účely. Tyto kroky jsou nutně sekvenční a vyžadují testování podmínek konvergence a sebekonzistence před pokračováním.
Tento workflow bude podrobněji přezkoumán a implementován v další části. Jediné, co si z této části potřebuješ odnést, je to, že task workflow jsou nezbytné.
Programovací praxe
Krása programovacích modelů spočívá v tom, že je můžeš libovolně kombinovat. Znáš-li kvantové i klasické programovací modely, dokážeš popsat heterogenní výpočet libovolné složitosti a spustit ho na hardwaru. Pojďme to procvičit na malém příkladu kombinovaného workflow, který implementuje vzor Qiskit (mapování, optimalizace, spuštění a post-processing) ve Slurmem, jak jsme se naučili v předchozí kapitole. Každý ze čtyř úkolů bude samostatnou Slurm úlohou s vlastními prostředky. Úloha optimalizace bude používat MPI k paralelní optimalizaci obvodů (pouze pro účely příkladu, jak je znázorněno na obrázku výše). Úloha spuštění využije kvantové prostředky a kvantové programovací modely (obvod a Sampler). Poslední úloha – post-processing – bude opět používat MPI paralelně s klasickými prostředky.
Mapování
Program mapping.py je navržen tak, aby sestavil obvod PauliTwoDesign, který se hojně používá v literatuře o kvantovém strojovém učení a kvantových benchmarcích, spolu s jednoduchým observablem, který měří qubit ve směru systému s qubity a s náhodnými počátečními parametry. Každý z těchto výstupů (kvantový obvod převedený do souboru qasm, observable a parametry) bude uložen do samostatného souboru v datovém adresáři a bude použit jako vstup ve fázi optimalizace.
Shellový skript této fáze (mapping.sh) je
#!/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
který definuje název úlohy, formát výstupu a počet uzlů/úkolů/CPU.
Optimalizace
Program optimization.py začíná načtením souborů z fáze mapování. Zde použiješ QRMI k začlenění kvantových prostředků do tohoto programu.
qrmi = QRMI()
resources = qrmi.resources()
quantum_resource = resources[0]
...
Poté provede lehkou optimalizaci nastavením optimization_level=1 pro transpilaci kvantového obvodu a aplikaci rozvržení obvodu na observable, které pak uloží do datové složky.
Shellový skript této fáze (optimization.sh) je
#!/bin/bash
#SBATCH --job-name=optimization
#SBATCH --output=output/optimization.out
#SBATCH --ntasks=4
#SBATCH --partition=classical
srun python3 /tmp/optimization.py
Zde --ntasks=4 vyžaduje od Slurmem čtyři klasické úlohy pro paralelní zpracování.
Spuštění
Toto je klíčová kvantová fáze, v níž je optimalizovaný kvantový obvod z předchozího kroku spuštěn na QPU prostřednictvím Estimatoru. K tomu nejprve načteme tři soubory – transpilovaný kvantový obvod, observable a počáteční parametry – a předáme je Estimatoru. Ten vrátí odhadovanou hodnotu observablu a vypíše ji.
Skript execution.sh využívá plugin Slurmem pro přístup ke kvantovému prostředku.
#!/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
Post-processing
Krok post-processingu často zahrnuje klasickou diagonalizaci a kontroly konzistence. Může být také iterativní. Nejužitečnější je zabývat se krokem post-processingu v příští lekci, kde jsou fyzický kontext a účel iterativních kroků jasné.
Vše dohromady
Všechny tyto úlohy můžeme zřetězit do workflow pomocí argumentu dependency příkazu 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)
A můžeme zkontrolovat frontu spouštění Slurmem.
$ 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)
Byl to ukázkový příklad demonstrující směs programovacích modelů. V příští kapitole se podíváme na algoritmy z praxe a ukážeme si programovací modely a správu prostředků na užitečných workflow.
Shrnutí
V této lekci jsme si ukázali, jak kombinovat více klasických a kvantových programovacích modelů k sestavení, správě a spuštění kompletního čtyřfázového workflow. Začali jsme základními koncepty kvantových obvodů a primitiv, poté jsme prozkoumali klasické modely jako paralelní programování a task workflow. Kombinací všech konceptů jsme sestavili vzor Qiskit – mapování, optimalizace, spuštění a post-processing – orchestrovaný správcem zátěže Slurm s jednoduchým kvantovým obvodem a observablem.
V příští lekci použijeme tento framework ke spouštění kvantových algoritmů založených na vzorkování a ukážeme, jak lze toto workflow uplatnit při řešení smysluplných problémů.
Veškerý kód a skripty použité v této kapitole jsou ti k dispozici v tomto repozitáři Github.