common.title

Docs
Quantum Circuit
TYTAN CLOUD

QUANTUM GAMING


Desktop RAG

Overview
Terms of service

Privacy policy

Contact
Research

Sign in
Sign up
common.title

NVIDIA CUDA-Q チュートリアル&H100ベンチマーク11: QAOAでのMaxcut問題

Yuichiro Minato

2025/04/08 01:16

Max Cut問題とは、グラフのノードを2つのグループに分け、その間のエッジ数を最大化する分割を求める問題です。小さなグラフでは解きやすいものの、一般的にはNP困難です。

Max Cutは機械学習・回路設計・統計物理など多くの分野に応用されており、本チュートリアルで紹介するQAOAは、ポートフォリオ最適化やジョブスケジューリングなど、他の最適化問題にも応用可能です。

https://nvidia.github.io/cuda-quantum/latest/applications/python/qaoa.html

以前にもベンチマークを作りましたので、そちらの結果を流用しながら進めます。

https://blueqat.com/yuichiro_minato2/a693682f-57d7-4c5c-8203-9006bb7afe43

🔹 Max Cut問題とは?

グラフの頂点集合を2つのグループに分け、その間をつなぐ辺の数を最大化する分割を求める問題です。

  • 小規模なグラフでは解けますが、一般にはNP困難
  • 応用例:機械学習、回路設計、統計物理、ポートフォリオ最適化、ジョブスケジューリング など多数

🔹 グラフの扱い

  • グラフ G = (V, E):頂点集合 V、辺集合 E
  • 無向グラフ(辺 (i, j)(j, i) は同じ)
  • 自己ループ((i, i))なし
  • カット:頂点集合 V を2つに分けたとき、異なる集合に属する頂点間の辺の数

🔹 例とビット表現

  • 頂点の分割はビット列で表現(例:01100
  • ビットが 0 なら片方の集合、1 ならもう一方の集合
  • 複数の分割が同じ最大カット値(max cut value)になることもある
import numpy as np
import cudaq
from cudaq import spin
from typing import List

# We'll use the graph below to illustrate how QAOA can be used to
# solve a max cut problem

#       v1  0--------------0 v2
#           |              | \
#           |              |  \
#           |              |   \
#           |              |    \
#       v0  0--------------0 v3-- 0 v4
# The max cut solutions are 01011, 10100, 01010, 10101 .

# First we define the graph nodes (i.e., vertices) and edges as lists of integers so that they can be broadcast into
# a cudaq.kernel.
nodes: List[int] = [0, 1, 2, 3, 4]
edges = [[0, 1], [1, 2], [2, 3], [3, 0], [2, 4], [3, 4]]
edges_src: List[int] = [edges[i][0] for i in range(len(edges))]
edges_tgt: List[int] = [edges[i][1] for i in range(len(edges))]

QAOA(量子近似最適化アルゴリズム)は、変分量子回路と古典的最適化器を組み合わせたアルゴリズムです。目的は、コストハミルトニアンの期待値を最小化するパラメータを古典的に最適化することです。

🔹 特徴:

  • 各グラフの頂点ごとに1つの量子ビットを割り当てる
  • 回路は重ね合わせ状態から開始
  • 回路はレイヤー構造で構成され、レイヤー数が多いほど精度が高まる
  • 他の変分アルゴリズムと比べて、構造が問題に特化しているのが特徴

QAOAは、量子と古典の協調により、組合せ最適化問題を効率的に近似的に解くための手法です。

QAOAの各レイヤーは、**「問題カーネル」と「ミキサーカーネル」**の2つで構成されています。

  • 🔹 ミキサーカーネル:各量子ビットにパラメータ付きの回転ゲートを適用(図では緑色で表示)
  • 🔹 問題カーネルグラフの辺情報をエンコードし、制御Xゲートとパラメータ付き回転ゲートを使って構成(下の図で示される)

このように、各レイヤーがグラフ構造と探索のバランスを取る役割を果たしています。

# Problem parameters
# The number of qubits we'll need is the same as the number of vertices in our graph
qubit_count: int = len(nodes)

# We can set the layer count to be any positive integer.  Larger values will create deeper circuits
layer_count: int = 2

# Each layer of the QAOA kernel contains 2 parameters
parameter_count: int = 2 * layer_count


@cudaq.kernel
def qaoaProblem(qubit_0: cudaq.qubit, qubit_1: cudaq.qubit, alpha: float):
    """Build the QAOA gate sequence between two qubits that represent an edge of the graph
    Parameters
    ----------
    qubit_0: cudaq.qubit
        Qubit representing the first vertex of an edge
    qubit_1: cudaq.qubit
        Qubit representing the second vertex of an edge
    thetas: List[float]
        Free variable

    Returns
    -------
    cudaq.Kernel
        Subcircuit of the problem kernel for Max-Cut of the graph with a given edge
    """
    x.ctrl(qubit_0, qubit_1)
    rz(2.0 * alpha, qubit_1)
    x.ctrl(qubit_0, qubit_1)


# We now define the kernel_qaoa function which will be the QAOA circuit for our graph
# Since the QAOA circuit for max cut depends on the structure of the graph,
# we'll feed in global concrete variable values into the kernel_qaoa function for the qubit_count, layer_count, edges_src, edges_tgt.
# The types for these variables are restricted to Quake Values (e.g. qubit, int, List[int], ...)
# The thetas plaeholder will be our free parameters
@cudaq.kernel
def kernel_qaoa(qubit_count: int, layer_count: int, edges_src: List[int],
                edges_tgt: List[int], thetas: List[float]):
    """Build the QAOA circuit for max cut of the graph with given edges and nodes
    Parameters
    ----------
    qubit_count: int
        Number of qubits in the circuit, which is the same as the number of nodes in our graph
    layer_count : int
        Number of layers in the QAOA kernel
    edges_src: List[int]
        List of the first (source) node listed in each edge of the graph, when the edges of the graph are listed as pairs of nodes
    edges_tgt: List[int]
        List of the second (target) node listed in each edge of the graph, when the edges of the graph are listed as pairs of nodes
    thetas: List[float]
        Free variables to be optimized

    Returns
    -------
    cudaq.Kernel
        QAOA circuit for Max-Cut for max cut of the graph with given edges and nodes
    """
    # Let's allocate the qubits
    qreg = cudaq.qvector(qubit_count)
    # And then place the qubits in superposition
    h(qreg)

    # Each layer has two components: the problem kernel and the mixer
    for i in range(layer_count):
        # Add the problem kernel to each layer
        for edge in range(len(edges_src)):
            qubitu = edges_src[edge]
            qubitv = edges_tgt[edge]
            qaoaProblem(qreg[qubitu], qreg[qubitv], thetas[i])
        # Add the mixer kernel to each layer
        for j in range(qubit_count):
            rx(2.0 * thetas[i + layer_count], qreg[j])

QAOAはコスト関数を最小化する変分アルゴリズムであり、Max Cut問題にも適用可能です。

🔹 Max Cutへの適用

各頂点に2値の変数(0 または 1)を割り当てることで、グラフの**分割(カット)**を表現します。変数の割り当てが各頂点の所属する集合を決定し、それによりカットの構成が決まります。

ある変数の割り当てに対する**カット値(cut value)**は、以下のように計算されます:

C = \sum_{(i,j) \in E} \frac{1 - z_i z_j}{2}

ここで E はグラフの辺の集合、z_i, z_j はそれぞれ頂点 i, j に対応する変数です。


🔁 最大化 → 最小化への変換

QAOAは最小化アルゴリズムなので、Max Cutの最大化問題をコスト関数の最小化問題として次のように書き換えます:

\text{Maximize } C \quad \Leftrightarrow \quad \text{Minimize } -C

さらに、変数 z_iパウリZ演算子 Z_i に、定数 1恒等演算子 I に置き換えることで、量子力学的なハミルトニアンの形式に変換します:

H_C = \sum_{(i,j) \in E} \frac{1 - Z_i Z_j}{2}

このハミルトニアンの最小固有値を求めることが、QAOAにおけるMax Cutの目的になります。

この H_Ccudaq.spin を用いて実装可能です。

# The problem Hamiltonian
# Define a function to generate the Hamiltonian for a max cut problem using the graph
# with the given edges


def hamiltonian_max_cut(edges_src, edges_tgt):
    """Hamiltonian for finding the max cut for the graph with given edges and nodes

    Parameters
    ----------
    edges_src: List[int]
        List of the first (source) node listed in each edge of the graph, when the edges of the graph are listed as pairs of nodes
    edges_tgt: List[int]
        List of the second (target) node listed in each edge of the graph, when the edges of the graph are listed as pairs of nodes

    Returns
    -------
    cudaq.SpinOperator
        Hamiltonian for finding the max cut of the graph with given edges
    """

    hamiltonian = 0

    for edge in range(len(edges_src)):

        qubitu = edges_src[edge]
        qubitv = edges_tgt[edge]
        # Add a term to the Hamiltonian for the edge (u,v)
        hamiltonian += 0.5 * (spin.z(qubitu) * spin.z(qubitv) -
                              spin.i(qubitu) * spin.i(qubitv))

    return hamiltonian

QAOAアルゴリズムを開始するには、初期パラメータの設定と、使用する**古典的な最適化手法(最適化ルーチン)**を指定する必要があります。

# Specify the optimizer and its initial parameters.
cudaq.set_random_seed(13)
optimizer = cudaq.optimizers.NelderMead()
np.random.seed(13)
optimizer.initial_parameters = np.random.uniform(-np.pi / 8, np.pi / 8,
                                                 parameter_count)
print("Initial parameters = ", optimizer.initial_parameters)

QAOAの最適化ループを実装するには、observe プリミティブまたは vqe プリミティブのどちらかを使用できます

#cudaq.set_target('nvidia')
cudaq.set_target('qpp-cpu')

# Generate the Hamiltonian for our graph
hamiltonian = hamiltonian_max_cut(edges_src, edges_tgt)
print(hamiltonian)

# Define the objective, return `<state(params) | H | state(params)>`
# Note that in the `observe` call we list the kernel, the hamiltonian, and then the concrete global variable values of our kernel
# followed by the parameters to be optimized.


def objective(parameters):
    return cudaq.observe(kernel_qaoa, hamiltonian, qubit_count, layer_count,
                         edges_src, edges_tgt, parameters).expectation()


# Optimize!
optimal_expectation, optimal_parameters = optimizer.optimize(
    dimensions=parameter_count, function=objective)

# Alternatively we can use the vqe call (just comment out the above code and uncomment the code below)
# optimal_expectation, optimal_parameters = cudaq.vqe(
#    kernel=kernel_qaoa,
#    spin_operator=hamiltonian,
#    argument_mapper=lambda parameter_vector: (qubit_count, layer_count, edges_src, edges_tgt, parameter_vector),
#    optimizer=optimizer,
#    parameter_count=parameter_count)

print('optimal_expectation =', optimal_expectation)
print('Therefore, the max cut value is at least ', -1 * optimal_expectation)
print('optimal_parameters =', optimal_parameters)
# Sample the circuit using the optimized parameters
# Since our kernel has more than one argument, we need to list the values for each of these variables in order in the `sample` call.
counts = cudaq.sample(kernel_qaoa, qubit_count, layer_count, edges_src,
                      edges_tgt, optimal_parameters)
print(counts)

# Identify the most likely outcome from the sample
max_cut = max(counts, key=lambda x: counts[x])
print('The max cut is given by the partition: ',
      max(counts, key=lambda x: counts[x]))

ベンチマークは再掲します。
量子古典ハイブリッド計算で、問題設定によって速度も多少変わりますので、ジグザグになっています。
GPUはNVIDIA H100、CPUはGoogle Colabの標準CPU(EPYCではエラーでループが回らなかった)を利用しています。

image

© 2025, blueqat Inc. All rights reserved