🪐

CUDA-Q で遊んでみる (5) — 動的に CUDA-Q カーネルを構築する

に公開

目的

CUDA-Q で遊んでみる (2) — GPU で Grover のアルゴリズムを回してみる で静的に実装した CUDA-Q カーネルを cudaq.make_kernel で構築して、少しでも pythonic に書いてみたいというもの。

実装する

まずは必要なパッケージのインストール

以下を使うことにする。

!pip install -qU cudaq "cupy-cuda12x==13.6.0"

アスキーアートの回路図がちゃんと表示されるようにする

ユーティリティを定義する。

from IPython.display import HTML, display

def show_fixed(text, font="Consolas, Roboto Mono, monospace", size=13):
    text = text.expandtabs(4)   # タブをスペースに変換
    esc = (text.replace("&", "&")
               .replace("<", "&lt;")
               .replace(">", "&gt;"))
    html = f'<pre style="font-family:{font}; font-size:{size}px; white-space:pre; font-variant-ligatures:none;">{esc}</pre>'
    display(HTML(html))

必要なパッケージをインポートする

import numpy as np
import cudaq

CUDA-Q カーネルを動的に定義する

9. Just-in-Time Kernel Creation を参考にして、以下のような感じ実装すると良いと思う。静的なカーネル実装では z.ctrl としていた部分は kernel.cz とする必用がありそうで、引数も少し異なることには注意が必要である。それ以外は基本的には似たような形になる。

def embed_solution(kernel: cudaq.PyKernel, qubits: cudaq.qview, solution: list[int]):
    n_qubits = qubits.size()
    for i, v in enumerate(solution):
        if v == 0:
            kernel.x(qubits[i])
    # MCZ start
    ctrls = [qubits[i] for i in range(n_qubits - 1)]
    last = qubits[n_qubits - 1]
    kernel.cz(ctrls, last)
    # MCZ end
    for i, v in enumerate(solution):
        if v == 0:
            kernel.x(qubits[i])


def oracle(kernel: cudaq.PyKernel, qubits: cudaq.qview, solutions: list[int]):
    n_qubits = qubits.size()
    for i in range(len(solutions) // n_qubits):
        embed_solution(kernel, qubits, solutions[n_qubits*i:n_qubits*(i+1)])


def diffuser(kernel: cudaq.PyKernel, qubits: cudaq.qview):
    n_qubits = qubits.size()
    kernel.h(qubits)
    kernel.x(qubits)
    # MCZ start
    ctrls = [qubits[i] for i in range(n_qubits - 1)]
    last = qubits[n_qubits - 1]
    kernel.cz(ctrls, last)
    # MCZ end
    kernel.x(qubits)
    kernel.h(qubits)


def make_grover(num_qubits: int, counts: int, solutions: list[int]):
    kernel = cudaq.make_kernel()
    qubits = kernel.qalloc(num_qubits)

    kernel.h(qubits)
    for _ in range(counts):
        oracle(kernel, qubits, solutions)
        diffuser(kernel, qubits)

    kernel.mz(qubits)

    return kernel


try:
    solutions = [0, 1, 1, 0, 1]
    grover = make_grover(5, 1, solutions)
except Exception as e:
    print(e)

19 量子ビットで実験する

solutions = [
    [1,0,0,1,1,0,1,1,0,0,1,0,0,1,1,0,1,0,0],
    [1,1,1,0,1,1,1,0,0,1,0,1,0,0,1,1,0,1,1]
]
num_qubits = len(solutions[0])

N = 2**num_qubits
angle = np.arcsin(np.sqrt(len(solutions) / N))
counts = int((np.pi/2 - angle) / (2*angle) + 0.5)

print(f'{num_qubits=} {counts=}')

num_qubits=19 counts=402

サンプリングを行って結果を確認する

%%time

try:
    solutions_ = np.array(solutions).flatten().tolist()
    grover = make_grover(num_qubits, counts, solutions_)
    if False:
        show_fixed(str(cudaq.draw(grover)))
    result = cudaq.sample(grover)
    result: dict[str, int] = {k: v for k, v in result.items()}
    print(list(sorted(result.items(), key=lambda k_v: -k_v[1]))[:10])
except Exception as e:
    print(e)

[('1110111001010011011', 505), ('1001101100100110100', 495)]
CPU times: user 1min 40s, sys: 213 ms, total: 1min 40s
Wall time: 1min 43s

どうやら、静的に CUDA-Q カーネルを実装する場合と比較すると、どこかにオーバーヘッドがかかっているようで遅い。

量子ビット数と経過時間の関係を見る

折角なので量子ビット数を色々と変更して経過時間を観察してみた。CPU も参考までに添えたが、メインは GPU (cuStateVec) で静的にカーネルを構築する場合と動的にカーネルを構築する場合である。

見にくいので対数グラフにして見る。

  • Qiskit で遊んでみる (8) — Qiskit Aer GPU を思い出すと、CPU でやるなら CUDA-Q ではなく、Qiskit を用いたほうが良いかもしれない。
  • 動的カーネル構築の場合も Qiskit Aer 越しに cuStateVec を使ったほうが速いかもしれないが、まだ自分が cudaq.make_kernel を使いこなせていない可能性があるので何とも言えない。
  • 静的カーネル構築は上記らとは明らかに異なっていて、爆速の傾向が見られる。静的カーネル構築の書き方をマスターするのが一番効率的ということかもしれない。

まとめ

CUDA-Q カーネルの動的な構築について確認してみた。実は pythonic に書けてこれが最強では?と思っていたが、書き方がまずいのか GPU をフルに活かせない結果に終わってしまった。

引き続き調査を行い、静的なカーネル構築との溝が埋められそうならチャレンジしてみたいと思う。

参考文献

GitHubで編集を提案

Discussion