はじめに
前回の記事でOQTOPUSのローカル環境(oqtopus-cliでクラウド層を構築し、quri-parts-oqtopusから接続する構成)を組んだので、今回はその環境を使って「マルチプログラミングジョブ」を実際に試してみた話を書く。
環境
前回記事と同じく、Windows + WSL2 + Docker Desktop 上に oqtopus-cli で構築した cloud-local テンプレートの環境が起動している状態から始める。
quri-parts-oqtopus のバージョンは以下の通り。
$ pip show quri-parts-oqtopus
Name: quri-parts-oqtopus
Version: 1.1.4
デバイス一覧を見ると、ローカル環境には qulacs(16qubitシミュレータ)や SVSim(39qubitシミュレータ)、モック的なQPUデバイスがいくつか登録されている。今回は qulacs を使う。
from quri_parts_oqtopus.backend.devices.device import OqtopusDeviceBackend
backend = OqtopusDeviceBackend()
for d in backend._client.list_devices():
print(d.raw.device_id, d.raw.device_type, d.raw.n_qubits)
01927422-... QPU 64
Kawasaki QPU 64
qulacs simulator 16
SC QPU 64
SVSim simulator 39
マルチプログラミングジョブとは
複数の(比較的qubit数の少ない)量子回路を1つの物理デバイス上にまとめて配置し、1回のジョブとして同時に実行する仕組み。実機のqubit数に対して1つの回路が使うqubit数が少ない場合、デバイスを分割して複数ユーザー/複数回路を相乗りさせることでスループットを上げよう、というモチベーション。
ソースを読むと、sample()/sample_qasm() の中に実装が入っている。
# quri_parts_oqtopus/backend/jobs/sampling.py (抜粋)
if job_type is None:
if isinstance(program, list):
job_type = "multi_manual"
else:
job_type = "sampling"
program = [program]
回路をlistで渡すだけで、自動的に job_type="multi_manual" になる。ジョブ種別としては oqtopus_client.rest.models.jobs_job_type.JobsJobType に SAMPLING / ESTIMATION / MULTI_MANUAL / SSE の4種類が定義されていて、MULTI_MANUAL がまさにこれ。名前が "manual" なのは、おそらく「ユーザーが自分で複数回路を1本のジョブにまとめて送る」方式という意味だろう。
試してみる
2qubitのBell状態と3qubitのGHZ状態、2種類の回路を用意する。
from quri_parts.circuit import QuantumCircuit
from quri_parts_oqtopus.backend import OqtopusSamplingBackend
SHOTS = 2000
# 回路A: 2qubit Bell状態
circuit_a = QuantumCircuit(2)
circuit_a.add_H_gate(0)
circuit_a.add_CNOT_gate(0, 1)
# 回路B: 3qubit GHZ状態
circuit_b = QuantumCircuit(3)
circuit_b.add_H_gate(0)
circuit_b.add_CNOT_gate(0, 1)
circuit_b.add_CNOT_gate(1, 2)
backend = OqtopusSamplingBackend()
まずは比較のためそれぞれ単独で実行する。
job_a = backend.sample(circuit_a, device_id="qulacs", shots=SHOTS)
print(job_a.result().counts)
# => {3: 1011, 0: 989}
job_b = backend.sample(circuit_b, device_id="qulacs", shots=SHOTS)
print(job_b.result().counts)
# => {0: 1020, 7: 980}
理論通り、Bell状態は 00/11がほぼ半々(0と3)、GHZ状態は 000/111がほぼ半々(0と7)になっている。
次に、この2つの回路をリストにしてまとめて投げる。
job_multi = backend.sample([circuit_a, circuit_b], device_id="qulacs", shots=SHOTS)
result = job_multi.result()
print(result.counts)
# => {3: 487, 28: 547, 0: 505, 31: 461}
print(result.divided_counts)
# => {0: Counter({0: 1052, 3: 948}), 1: Counter({7: 1008, 0: 992})}
結果を読み解く
counts(合成分布)のキーは、回路ごとの測定結果のビット列を連結した値になっている。今回は「回路A(2bit) | 回路B(3bit) << 2」という形で、リストの先頭(回路A)が下位ビット、後続(回路B)が上位ビットに配置されているようだ。
| 合成値 | 2進数(5bit) | 回路A(下位2bit) | 回路B(上位3bit) |
|---|---|---|---|
| 0 | 00000 |
0 (00) |
0 (000) |
| 3 | 00011 |
3 (11) |
0 (000) |
| 28 | 11100 |
0 (00) |
7 (111) |
| 31 | 11111 |
3 (11) |
7 (111) |
回路Aと回路Bはそれぞれ独立に測定されているので、4通りの組み合わせがほぼ均等(2000shots/4 ≈ 500)に出現している。これは「2つの回路をまとめて実行しているが、測定結果としては独立」ということを裏付けている。
そして divided_counts を見ると、インデックス0が回路A、インデックス1が回路B(渡したリストの順序と対応)に対応しており、それぞれの分布は単独実行時の結果({3: 1011, 0: 989} / {0: 1020, 7: 980})とほぼ一致した。つまり「まとめて実行しても、個々の回路の測定結果はちゃんと分解して取り出せる」ことを確認できた。並べて比較するとこの通り。
物理qubitへの実際のマッピングを見る
ここまでは「回路ごとの測定結果」の話だったが、そもそも2つの回路はデバイス上のどの物理qubitに割り当てられたのかも気になったので調べてみた。
job.job_info を覗くと、transpile_result の中に virtual_physical_mapping というキーがあり、ここに仮想qubit(回路上のqubit番号)と物理qubit(デバイス上の実際のqubit)の対応表が入っている。
info = job.job_info
print(info["transpile_result"]["virtual_physical_mapping"])
"qubit_mapping": {"0": 13, "1": 15, "2": 14, "3": 12, "4": 9},
"bit_mapping": {"0": 0, "1": 1, "2": 2, "3": 3, "4": 4}
}
combined_program(2回路を1本のOpenQASMにまとめたもの)を見ると分かるが、仮想qubitの0,1が回路A(Bell)、2,3,4が回路B(GHZ)に対応している。つまり:
| 仮想qubit | 所属回路 | 物理qubit |
|---|---|---|
| 0 | 回路A q0 | 13 |
| 1 | 回路A q1 | 15 |
| 2 | 回路B q0 | 14 |
| 3 | 回路B q1 | 12 |
| 4 | 回路B q2 | 9 |
これをqulacsデバイス(16qubit)のトポロジー(list_devices()のdevice_infoに含まれるqubitの座標・カプラー接続情報)の上に重ねて図示すると、以下のようになる。
回路A(青)は物理qubit 13-15、回路B(オレンジ)は物理qubit 9-12-14 という、デバイス上で互いに重ならない領域に配置されている。ちゃんと2つの回路が異なる物理qubit領域に「相乗り」していることが視覚的に確認できた。
まとめ
-
quri-parts-oqtopusのsample()に回路のリストを渡すだけで、マルチプログラミングジョブ(job_type="multi_manual")が試せる - 合成された
countsは回路ごとのビット列を連結したものになっており、divided_countsで個々の回路の分布に分解できる -
virtual_physical_mappingとtranspiled_programを読めば、2つの回路が実際にデバイス上のどの物理qubitへ、互いに重ならないように配置されたかまで追跡できる
補足
- ソースコードを読む限り、estimatorについてはマルチプログラミング未対応のようでした。

