🪐

CUDA-Q で遊んでみる (1) — とりあえず Bell 状態を作ってみる

に公開

目的

NVIDIA、CUDA-Q プラットフォームで、世界中の量子コンピューティング センターを加速

2024 年 5 月 13 日、ドイツ ハンブルグ — ISC — NVIDIA は本日、オープンソースの NVIDIA CUDA-Q™ プラットフォームによって、世界中の国立スーパーコンピューティング センターの量子コンピューティングに関する取り組みを加速させると発表しました。

というのがリリースされて随分経っているので、ちょっと触ってみようかなというもの。

チュートリアルから入る

インストール

Quick Start を見る。

pip install cudaq

で良いとのことだが、色々やりたいのでとりあえず以下のようにする。Google Colab を使うことにする。

cudaq qbraid qiskit 'qiskit[visualization] qiskit-aer'

回路可視化用の対策

等幅フォントでレンダリングしないとアスキーアートの回路が崩れるので対策する。Windows 環境なら Consolas を使えば良いので以下のようなユーティリティを用意する。

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))

ターゲットを指定する

CUDA-Q Simulation Backends に解説があるが、ターゲットを CPU とか GPU とか指定できる。

一例として「OpenMP CPU-only」なら qpp-cpu、「Single-GPU」で cuStateVec を使うなら nvidia、「Single-GPU」で cuTensorNet を使うなら tensornet、テンソルネットワークの中でも MPS(行列積状態)を使うなら tensornet-mps が指定できるらしい。何も指定しないならデフォルト選択に従って nvidiaqpp-cpu が選ばれるようだ。

とりあえず今回は qpp-cpu にしておく。

import cudaq

cudaq.set_target('qpp-cpu')

Bell 状態を作る

Validate your Installation を参考にすれば良い。Hadamard ゲートを適用して、制御 X ゲートを適用して、最後に Z 基底で測定する形である。

後でエラーになるのが面倒くさいので、カーネル・・・というか量子回路の部分だけど事前にコンパイルしてエラーを確認すると良さそう。

qubit_count = 2

@cudaq.kernel
def kernel():
    qubits = cudaq.qvector(qubit_count)
    h(qubits[0])
    for i in range(1, qubit_count):
        x.ctrl(qubits[0], qubits[i])
    mz(qubits)

try:
    kernel.compile()
except Exception as e:
    print(e)

もし未知のゲート abc を使ったりすると、以下のように分かる。

ここでさぼってしまうと後で AssertionError が起きたりして Colab のセッションを再起動するしかなくなったりするので面倒くさい。

Colab が警告しているが?

そもそも全てのゲートについて「not defined」で警告が出る。これは何だ?という部分ではある。

よく見ると、関数の上に @cudaq.kernel というデコレータがある。詳細は Appendix に回すとして、kernel_decorator.py をざっくり見ると、「デコレーション対象の関数を入力として受け取り、オプションでその AST 表現を MLIR 経由で実行可能コードへローダウンします。このデコレータは完全な JIT コンパイルモードを有効化し、関数を MLIR 表現へローダウンします。」といったことが書かれている。

つまり、@cudaq.kernel をつけた関数は CUDA-Q 用の「Python function AST」として扱われ、JIT コンパイルで中間表現に変換され、実行可能コードとなり、CUDA-Q のバックエンドで実行される、という作りらしい。

ということで、Python を用いた CUDA-Q 用の DSL (Domain-Specific Language) のような状態なので、警告が出てしまう。仕方がない。

回路の可視化

さて、カーネルを定義したので、量子回路を可視化したい。

以下のように等幅フォントを使うようにして可視化すると、いつもの絵が出て来る。勿論最初からブラウザのフォント指定を変更しておけば、ユーティリティをかます必要はない。

show_fixed(str(cudaq.draw(kernel)))

サンプリング

以下のようにして、\frac{1}{\sqrt{2}}(|00\rangle + |11\rangle) が確認できる。

result = cudaq.sample(kernel)
print(result)

{ 00:484 11:516 }

実は mz(qubits) をカーネルに書かなくても cudaq.sample すれば全量子ビットに対するサンプリングの結果が得られる。もし特定の量子ビットだけを測定したい場合には、以下のようにすれば良い。

mz(qubits[0])

Qiskit を使いたい

なるほど CUDA-Q の基本的な使い方は分かったが、慣れていないとちょっと難しい。ここでふと arXiv:2507.12480 LLM-Powered Quantum Code Transpilation という論文を眺めると、以下のようにめちゃくちゃナイスなことが書いてある。

Transpiling quantum code from one QSDK (e.g., IBM’s Qiskit) to another (e.g., Google’s Cirq) is critical for achieving cross-platform interoperability and hardware abstraction in quantum computing workflows.
...
In another study, Arulandu [6] developed a Python-based transpiler designed to convert OpenQASM 3.0 programs into CUDA-Q kernels. Building on existing OpenQASM parsing tools, the transpiler supports a substantial subset of the OpenQASM 3.0 specification, including features such as custom gates, control and adjoint modifiers, and binary expressions.
...
[6] A. C. Arulandu, “Transpiling openqasm 3.0 programs to cuda-q kernels.” https://arulandu.com/assets/pdf/cs252-qasm-cudaq-transpiler.pdf, 2024.Accessed: 2025-05-16.

A. C. Arulandu 氏の論文を見ると、qBraid への貢献が示唆されており、具体的には OpenQASM 3.0 to CUDA-Q Transpiler #857 で pull-request 対応をしたとのことである。これこそが冒頭でわざわざ qbraidqiskit をセットアップした理由である。実際に試してみよう。

まずは試す

単刀直入には from qbraid.transpiler.conversions import openqasm3_to_cudaq を使えば良いのだが、雑に Qiskit で回路を書いて変換すると例外が出る。QuantumCircuit.measure_all を使うと barrier が入るが、これに対応していないのである。

Barrier を含まない量子回路を記述する

一番簡単なのは以下のように勝手に barrier を差し込ませないようにすることである。

from qiskit import QuantumCircuit, qasm3

qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])

ここではもっと大袈裟な手を使ってみる。以下のように PassManager を使って barrier を消してしまうという手だ。実際にはやりすぎなので、上記の方法で実装するのが正しいと思う。

OpenQASM 3.0 の出力は以下のようになる:

OPENQASM 3.0;
include "stdgates.inc";
bit[2] meas;
qubit[2] q;
h q[0];
cx q[0], q[1];
meas[0] = measure q[0];
meas[1] = measure q[1];

OpenQASM 3.0 を経由した CUDA-Q カーネルへの変換

再掲になるが以下のようにして変換できる。show_fixed(str(cudaq.draw(kernel))) をすれば上の方で掲載した回路図がアスキーアートで出て来る。サンプリングしても同様の結果を得られる。

from qbraid.transpiler.conversions import openqasm3_to_cudaq

try:
    kernel = openqasm3_to_cudaq(qasm3.dumps(qc))
except Exception as e:
    print(e)

CUDA-Q カーネルから OpenQASM 2.0 を得る

逆に CUDA-Q カーネルから OpenQASM を得ることもできる。API の実装を確認したところ、現時点では OpenQASM 2.0 への変換になるらしい。何となくそれっぽい内容なのは分かる。

print(cudaq.translate(kernel, format="openqasm2"))
// Code generated by NVIDIA's nvq++ compiler
OPENQASM 2.0;

include "qelib1.inc";

gate nvqpp__mlirgen____nvqppBuilderKernel_OY2IYXXW60 q0 {
  h q0;
}

gate nvqpp__mlirgen____nvqppBuilderKernel_TREV53PON9 q0 {
  x q0;
}

qreg var0[2];
h var0[0];
cx var0[0], var0[1];
creg var3[1];
measure var0[0] -> var3[0];
creg var4[1];
measure var0[1] -> var4[0];

Qiskit で可視化してみる

以下で OpenQASM 2.0 をロードして QuantumCircuit にしてみる。可視化してみるとなるほどという感じ。

qc2 = QuantumCircuit.from_qasm_str(cudaq.translate(kernel, format="openqasm2"))
qc2.draw("mpl", style="clifford")

まとめ

とりあえずチュートリアルを軽く眺めた程度であれこれ試してみた。CUDA-Q カーネルをいきなり書くのはちょっと難しいなと思ったので、慣れた Qiskit からの変換も試してみた。

大体基本は分かった気がするので、他にも調べていきたい。

参考文献

Appendix

@cudaq.kernel に少しだけ迫る

「デコレーション対象の関数を入力として受け取り、オプションでその AST 表現を MLIR 経由で実行可能コードへローダウンします。このデコレータは完全な JIT コンパイルモードを有効化し、関数を MLIR 表現へローダウンします。」ということが書かれている kernel_decorator.py であるが、興味のある部分のみを抜粋すると以下のようになっている。要するに JIT コンパイラ付きのガワを被せるような仕組みになっていることが分かる。

[kernel_decorator.py]

# This file implements the decorator mechanism needed to
# JIT compile CUDA-Q kernels. It exposes the cudaq.kernel()
# decorator which hooks us into the JIT compilation infrastructure
# which maps the AST representation to an MLIR representation and ultimately
# executable code.


class PyKernelDecorator(object):
    """
    The `PyKernelDecorator` serves as a standard Python decorator that takes 
    the decorated function as input and optionally lowers its  AST 
    representation to executable code via MLIR. This decorator enables full JIT
    compilation mode, where the function is lowered to an MLIR representation.

    This decorator exposes a call overload that executes the code via the 
    MLIR `ExecutionEngine` for the MLIR mode. 
    """

    def __init__(self,
                 function,
                 verbose=False,
                 module=None,
                 kernelName=None,
                 funcSrc=None,
                 signature=None,
                 location=None,
                 overrideGlobalScopedVars=None):

        is_deserializing = isinstance(function, str)

        ...

    def compile(self):
        """
        Compile the Python function AST to MLIR. This is a no-op 
        if the kernel is already compiled. 
        """

        ...

    def __call__(self, *args):
        """
        Invoke the CUDA-Q kernel. JIT compilation of the kernel AST to MLIR 
        will occur here if it has not already occurred, except when the target
        requires custom handling.
        """

        ...
        # Compile, no-op if the module is not None
        self.compile()
        ...
        if self.returnType == None:
            cudaq_runtime.pyAltLaunchKernel(self.name,
                                            self.module,
                                            *processedArgs,
                                            callable_names=callableNames)
        else:
            result = cudaq_runtime.pyAltLaunchKernelR(
                self.name,
                self.module,
                mlirTypeFromPyType(self.returnType, self.module.context),
                *processedArgs,
                callable_names=callableNames)
            return result


def kernel(function=None, **kwargs):
    if function:
        return PyKernelDecorator(function)
    else:

        def wrapper(function):
            return PyKernelDecorator(function, **kwargs)

        return wrapper
GitHubで編集を提案

Discussion