cusvaer

从 cuQuantum Appliance 22.11 开始,我们包含了 cusvaer。cusvaer 被设计为 Qiskit 后端求解器,并针对分布式状态向量模拟进行了优化。

功能

  • 分布式模拟

    cusvaer 将状态向量模拟分布到多个 GPU 和 CPU 设备。

  • CPU 内存作为外部存储以容纳状态向量。

    cusvaer 不仅能够在 GPU 上分配状态向量,还可以在 CPU 内存中分配,以容纳更大尺寸的状态向量。

  • 为了获得最佳性能的 2 的幂配置

    GPU 的数量以及进程和节点的数量应始终为 2 的幂。此设计是为了获得最佳性能而选择的。

  • 随附经过验证的库集

    cusvaer 随附最新的 cuStateVec 和一组经过验证的 MPI 库。性能已在 NVIDIA SuperPOD 上得到验证。

分布式状态向量模拟

分布式状态向量模拟的常用方法是将状态向量等分切片成多个子状态向量并将它们分发到进程。分布式状态向量模拟的一些示例也在 多 GPU 计算 中进行了描述。在 cusvaer 中,这种等分切片的状态向量称为子状态向量。

当前版本支持两种分布式模拟配置,(1)多进程模拟和(2)多 GPU 单进程模拟。

  1. 多进程模拟

对于此配置,cusvaer 使用“每个进程单个设备”模型。子状态向量放置在 GPU 中[*]_。每个进程拥有一个 GPU,并且一个子状态向量分配给分配给该进程的 GPU。因此,进程数始终与 GPU 数相同。所有进程中的子状态向量大小相同。

当对状态向量进行切片时,子状态向量的大小由电路中的量子比特数和用于分布的 GPU 数计算得出。

示例:可以使用 32 个 DGX A100 和以下配置计算 40 量子比特 c128 模拟。

  • 状态向量大小:16 TiB (= 16 [字节/元素] * (1 << 40 [量子比特]))

  • 每个节点的 GPU 数量:8

  • 节点数量:32

  • 每个 GPU 中子状态向量的大小:64 [GiB/GPU] = 16 TiB / (32 [节点] x 8 [GPU/节点])

cusvaer 通过使用给定量子电路的量子比特数和进程数来计算子状态向量的大小。因此,通过指定适当的进程数,用户可以将状态向量模拟分布到进程和节点。进程数应为 2 的幂。

对于具有多个 GPU 的节点,GPU 通过使用以下用于配置亲和性的公式分配给进程。

device_id = mpi_rank % number_of_processes_in_node
  1. 多 GPU 单进程模拟

对于此配置,cusvaer 将使用“单个进程与多个 GPU”模型。所有 GPU 预计都安装在单个服务器或工作站中。GPUDirect P2P 是一项要求,因此,所有 GPU 都通过 NVLink/NVSwitch 和/或带有单个 PCIe 交换机的 PCIe 总线连接。

通过将 GPU 设备 ID 指定给 cusvaer_device_ids,模拟器将使用指定的 GPU 来分配状态向量。

使用 CPU 和 GPU 内存分配状态向量

从 24.11 开始,cusvaer 支持使用 CPU 内存来分配状态向量的一部分。如果 GPU 内存的容量不足以容纳大型状态向量,则将额外利用 CPU 内存来保存状态向量的一部分。

默认情况下,如果状态向量不适合 GPU 内存,则将自动使用 CPU 内存。并且,用户还可以使用选项 cusvaer_max_cpu_memory_mbcusvaer_max_gpu_memory_mb 显式指定可用于状态向量分配的内存容量。这些选项给出了可用于状态向量分配的可用内存容量的上限。

默认情况下,这些选项为 None,这意味着 cusvaer 将通过使用系统中安装的物理内存来计算最大内存容量,如下所示

  • cusvaer_max_cpu_memory_mb 设置为 [物理 CPU 内存量] - 4 GiB。

  • cusvaer_max_gpu_memory_mb 设置为 [物理 GPU 内存量] - 2 MiB。

对于最大 GPU 内存消耗,状态向量大小是 2 的幂。如果 GPU 具有 80 GiB 内存,则状态向量的最大大小为 64 GiB。当添加一个量子比特时,状态向量大小将加倍。因此,将使用 64 GiB 的 CPU 内存。CPU 内存大小为 [状态向量大小] - [GPU 内存使用量]。当在 GPU 上向 64 GiB 状态向量添加两个量子比特时,状态向量大小将为 256 GiB。因此,CPU 内存使用量将为 192 GiB。对于添加三个量子比特,CPU 内存使用量将为 448 GiB,总共 512 GiB 状态向量。

在当前实现中,CPU 内存最多可以添加 三个量子比特

注意

用户有责任保持用于状态向量分配的空闲 CPU 和 GPU 内存。如果所需的 CPU 和/或 GPU 内存量不可用,cusvaer 将引发内存不足错误。

此功能为“单进程单 GPU”模拟和“多进程模拟”提供。“单进程多 GPU”配置不支持 CPU 内存利用率。

使用 cusvaer

cusvaer 提供一个类 cusvaer.backends.StatevectorSimulator。此类具有与 Qiskit Aer StatevectorSimulator 相同的接口。

以下是使用 cusvaer 的代码示例。

from qiskit import QuantumCircuit, transpile
from cusvaer.backends import StatevectorSimulator

def create_ghz_circuit(n_qubits):
    ghz = QuantumCircuit(n_qubits)
    ghz.h(0)
    for qubit in range(n_qubits - 1):
        ghz.cx(qubit, qubit + 1)
    ghz.measure_all()
    return ghz

circuit = create_ghz_circuit(20)

simulator = StatevectorSimulator()
simulator.set_options(precision='double')
circuit = transpile(circuit, simulator)
job = simulator.run(circuit, shots=1024)
result = job.result()

if result.mpi_rank == 0:
    print(result.get_counts())

将获得以下控制台输出

$ python ghz_cusvaer.py
{'00000000000000000000': 480, '11111111111111111111': 544}

要运行分布式模拟,用户需要使用 MPI 或适当的库来指定进程数。在下面的示例中,20 量子比特状态向量模拟分布到两个进程,每个进程都有 19 量子比特子状态向量。进程数应为 2 的幂。

$ mpirun -n 2 ghz_cusvaer.py

注意

GHZ 电路在此用作示例,这并不意味着演示性能。众所周知,当模拟分布到多个 GPU 和/或多个节点时,GHZ 电路太浅而无法显示良好的扩展性。

注意

从 24.11 开始,cusvaer 将在多进程模拟中尽可能使用 CUDA 虚拟内存管理功能进行状态向量分配。为了在 cuQuantum Appliance 中允许此功能,可能需要通过选项 --cap-add=SYS_PTRACE 向容器提供额外的进程间通信能力,如下所示

docker run --cap-add=SYS_PTRACE --gpus all -it --rm "$image_name"

MPI 库

cusvaer 提供对 Open MPIMPICH 的内置支持。

下面显示的版本已验证或预计可以工作。

  • Open MPI

    • 已验证:v4.1.4 / UCX v1.13.1

    • 预计可以工作:v3.0.x、v3.1.x、v4.0.x、v4.1.x

  • MPICH

    • 已验证:v4.0.2

cuQuantum Appliance 随附使用 PMI 启用的 Open MPI v4.1.4 和 UCX v1.13.1 二进制文件。构建配置旨在与 Slurm 配合使用。

两个选项 cusvaer_comm_plugin_typecusvaer_comm_plugin_soname 用于选择 MPI 库。有关详细信息,请参阅 CommPlugin

对于使用其他 MPI 库,用户需要编译外部插件。请参阅 cuQuantum GitHub

cusvaer 选项

cusvaer 提供 Qiskit Aer 常用选项和 cusvaer 特有选项的子集。

Qiskit Aer 提供的常用选项

选项

描述

shots

非负整数

拍摄次数

precision

"single""double"

状态向量元素的精度

fusion_enable

TrueFalse

启用/禁用门融合

fusion_max_qubit

正整数

用于门融合的最大量子比特数

fusion_threshold

正整数

启用门融合的阈值

memory

TrueFalse

如果指定 True,则经典寄存器值将保存到结果中

seed_simulator

整数

随机数生成器的种子

cusvaer 选项

选项

描述

cusvaer_comm_plugin_type

cusvaer.CommPluginType

选择用于进程间通信的 CommPlugin

cusvaer_comm_plugin_soname

字符串

用于进程间通信的共享库名称

cusvaer_global_index_bits

正整数列表

网络结构

cusvaer_p2p_device_bits

非负整数

cusvaer_data_transfer_buffer_bits

正整数

用于在传输数据期间临时缓冲区的的大小

cusvaer_host_threads

正整数

用于在主机上处理电路的主机线程数

cusvaer_diagonal_fusion_max_qubit

大于或等于 -1 的整数

用于对角门融合的最大量子比特数

cusvaer_device_ids

设备 ID 列表,整数形式

cusvaer_max_cpu_memory_mb

零或正整数

CPU 内存的最大容量(以 MiB 为单位),用于状态向量分配

cusvaer_max_gpu_memory_mb

正整数

状态向量分配的最大 GPU 内存容量,单位为 MiB

cusvaer_migration_level

零或正整数

插入到 cusvaer_global_index_bits 的迁移索引位的位置

设备选择

cusvaer_device_ids

cusvaer_device_ids 指定用于模拟的 GPU 设备 ID。

对于单进程和单 GPU 的模拟,此选项应仅包含一个代表 GPU 的整数。如果指定 None,将使用当前 GPU。

对于单进程多 GPU 的模拟,此选项应包含代表一系列设备 ID 的整数列表。设备 ID 的数量应为 2 的幂次方。

对于多进程模拟,每个进程使用单个 GPU,此选项应包含一个代表设备 ID 的整数。如果指定 None,将根据进程的 rank 自动选择 GPU。None 是默认值,建议用于多进程模拟。

多进程模拟

CommPlugin 选项

cusvaer 动态链接到一个库,该库通过使用 CommPlugin 在运行时处理进程间通信。CommPlugin 是一个小模块,通常封装 MPI 库。CommPlugin 通过 cusvaer_comm_plugin_typecusvaer_comm_plugin_soname 选项选择。

cusvaer_comm_plugin_type

cusvaer_comm_plugin_type 选项用于选择 CommPlugin。值类型为 cusvaer.CommPluginType,默认值为 cusvaer.CommPluginType.MPI_AUTO

# declared in cusvaer
from enum import IntEnum

class CommPluginType(IntEnum):
    SELF        = 0       # Single process
    EXTERNAL    = 1       # Use external plugin
    MPI_AUTO    = 0x100   # Automatically select Open MPI or MPICH
    MPI_OPENMPI = 0x101   # Use Open MPI
    MPI_MPICH   = 0x102   # Use MPICH

SELF

用于单进程模拟的 CommPlugin。SELF CommPlugin 没有任何外部依赖项。

MPI_AUTO, MPI_OPENMPI, MPI_MPICH

通过指定 MPI_OPENMPIMPI_MPICH,将动态加载指定的库并用于进程间数据传输。MPI_AUTO 会自动选择其中一个。使用这些选项,cusvaer 内部将使用 MPI_COMM_WORLD 作为通信器。

EXTERNAL

使用封装进程间通信库的外部自定义插件的选项。此选项用于使用用户偏好的库。有关详细信息,请参阅 cuQuantum GitHub

如果指定了 CommPluginType.SELF 以外的值,则在应用程序生命周期内,CommPluginType 的值应保持不变。这反映了一个进程在应用程序生命周期内只能使用一个 MPI 库。

comm_plugin_soname

comm_plugin_soname 选项指定共享库的名称。当为 cusvaer_comm_plugin_type 指定 MPI_AUTOMPI_OPENMPIMPI_MPICH 时,应指定搜索路径中相应的 MPI 库名称。默认值为 "libmpi.so"

如果指定了 EXTERNALcomm_plugin_soname 将包含自定义 CommPlugin 的名称。

指定设备网络结构

两个选项 cusvaer_global_index_bitscusvaer_p2p_device_bits 可用于指定节点间和节点内网络结构。通过指定这些选项,cusvaer 调度数据传输以利用更快的通信路径来加速模拟。

cusvaer_global_index_bits

cusvaer_global_index_bits 是一个正整数列表,表示节点间网络结构。

假设集群中有 8 个节点具有更快的通信网络,并且运行 32 节点模拟,则 cusvaer_global_index_bits 的值为 [3, 2]。第一个 3 是 log2(8),表示具有快速通信的 8 个节点,对应于状态向量中的 3 个量子比特。第二个 2 表示 32 个节点中的 4 个 8 节点组。global_index_bits 元素的总和为 5,这意味着节点数为 32 = 2^5。

cusvaer_global_index_bits 中的最后一个元素可以省略。

cusvaer_p2p_device_bits

cusvaer_p2p_device_bits 选项用于指定可以使用 GPUDirect P2P 进行通信的 GPU 数量。

对于像 DGX A100 这样的 8 GPU 节点,该数字为 log2(8) = 3。

cusvaer_p2p_device_bits 的值通常与 cusvaer_global_index_bits 的第一个元素相同,因为 GPUDirect P2P 网络通常是集群中最快的。

cusvaer_data_transfer_buffer_bits

cusvaer_data_transfer_buffer_bits 指定用于节点间数据传输的缓冲区大小。进程的每个 rank 分配 (1 << cusvaer_data_transfer_buffer_bits) 字节的设备内存。默认设置为 26 (64 MiB)。允许的最小值为 24 (16 MiB)。

根据系统,将 cusvaer_data_transfer_buffer_bits 设置为更大的值可以加速节点间数据传输。

CPU 内存利用率

cusvaer_max_cpu_memory_mb

cusvaer_max_cpu_memory_mb 选项指定用于状态向量分配的最大 CPU 内存容量。单位为 MiB。选项值可以是 None(默认)、0 或正整数。

如果指定 None,则通过使用 CPU 的物理内存大小来计算最大 CPU 内存容量。

如果指定 0,则禁用 CPU 内存利用率。状态向量将仅在 GPU 上分配。

如果指定正整数,则该值将用作分配状态向量的最大 CPU 内存大小。该值不应超过 CPU 物理内存量。

cusvaer_max_gpu_memory_mb

cusvaer_max_gpu_memory_mb 选项指定用于状态向量分配的最大 GPU 内存容量。单位为 MiB。选项值可以是 None 或正整数。默认值为 None

如果指定 None,则通过使用 GPU 的物理内存大小来计算最大 GPU 内存容量。

如果指定正整数,则该值将用作分配状态向量的最大 GPU 内存大小。该值不应超过 GPU 物理内存量。

cusvaer_migration_level

通过使用 CPU 内存扩展状态向量是为了插入迁移索引位,这些迁移索引位代表在 CPU 和 GPU 上分配的量子比特。如果插入一个迁移索引位,状态向量大小将加倍。

cusvaer_migration_level 选项指定将迁移索引位的数量插入到 cusvaer_global_index_bits 的位置。

cusvaer_global_index_bits 中的索引位数量从更快到更慢的网络连接排序,以获得最佳性能。此外,CPU 和 GPU 之间的状态向量迁移会在它们之间传输状态向量元素,这会影响模拟的性能,具体取决于 CPU-GPU 数据传输的带宽。通过选择合适的位置将迁移索引位的数量插入到 cusvaer_global_index_bits,可以优化模拟性能。

默认值为 None,这意味着迁移索引位的数量插入到 cusvaer_global_index_bits 的末尾。默认设置假设 CPU 和 GPU 之间的数据传输带宽比 GPU 间连接(如 NVLink、IB 等)慢。

其他选项

cusvaer_host_threads

cusvaer_host_threads 指定用于电路处理的 CPU 线程数。默认值设置为 8,这是一个很好的值。性能对此选项不敏感。

cusvaer_diagonal_fusion_max_qubit

cusvaer_diagonal_fusion_max_qubit 指定每个融合对角门允许的最大量子比特数。默认值设置为 -1,融合大小将自动调整以获得更好的性能。如果为 0,则禁用对角门的门融合。

自定义指令

set_state_simple(state)

此指令设置状态向量。state 参数是 numpy ndarray。如果 state 参数的 dtype 与状态向量精度的 dtype 不同,则在将其设置为状态向量之前,给定的 ndarray 将转换为状态向量的类型。

当运行单进程模拟时,state 参数将设置其数组元素为状态向量。state 参数值的大小与状态向量的大小相同。

如果模拟分布到多个进程,则此指令设置分配给进程的设备中的子状态向量。state 参数的大小应与子状态向量的大小相同。

与 Qiskit Aer 提供的 set_statevector() 指令不同,set_state_simple() 不检查 state 参数的绝对值。

save_state_simple()

此指令保存分配给进程的 GPU 中的状态向量。保存的状态向量作为 numpy ndarray 在结果对象中返回。

当运行单进程模拟时,保存的 ndarray 是状态向量。如果模拟分布到多个进程,则保存的 ndarray 是进程拥有的子状态向量。

注意

以上自定义指令是初步的,可能会发生更改。

cusvaer 选项配置示例

import cusvaer

options = {
  'cusvaer_comm_plugin_type': cusvaer_comm_plugin_type=cusvaer.CommPluginType.MPI_AUTO,  # automatically select Open MPI or MPICH
  'cusvaer_comm_plugin_soname': 'libmpi.so',  # MPI library name is libmpi.so
  'cusvaer_global_index_bits': [3, 2],  # 8 devices per node, 4 nodes
  'cusvaer_p2p_device_bits': 3,         # 8 GPUs in one node
  'precision': 'double'         # use complex128
}
simulator = cusvaer.backends.StatevectorSimulator()
simulator.set_options(**options)

异常

cusvaer 使用 qiskit.providers.basicaer.BasicAerError class 引发异常。

与 mpi4py 的互操作性

mpi4py 是一个封装 MPI 的 Python 包。通过在运行模拟之前导入 mpi4py,cusvaer 可以与 mpi4py 互操作。

from qiskit import QuantumCircuit, transpile
from cusvaer.backends import StatevectorSimulator
# import mpi4py here to call MPI_Init()
from mpi4py import MPI

def create_ghz_circuit(n_qubits):
    ghz = QuantumCircuit(n_qubits)
    ghz.h(0)
    for qubit in range(n_qubits - 1):
        ghz.cx(qubit, qubit + 1)
    ghz.measure_all()
    return ghz

print(f"mpi4py: rank: {MPI.COMM_WORLD.Get_rank()}, size: {MPI.COMM_WORLD.Get_size()}")

circuit = create_ghz_circuit(n_qubits)

# Create StatevectorSimulator instead of using Aer.get_backend()
simulator = StatevectorSimulator()
circuit = transpile(circuit, simulator)
job = simulator.run(circuit)
result = job.result()

print(f"Result: rank: {result.mpi_rank}, size: {result.num_mpi_processes}")

cusvaer 在执行第一次模拟之前立即调用 MPI_Init(),并在 Python 调用在 atexit 中注册的函数时调用 MPI_Finalize()

cusvaer 在调用 MPI_Init() 之前检查 MPI 是否已初始化。如果 MPI_Init() 在第一次模拟之前已被调用,则会跳过 MPI_Init()MPI_Finalize() 调用。

对于 mpi4py,MPI_Init() 在加载 mpi4py 时调用。因此,通过在调用 simulator.run() 之前导入 mpi4py,mpi4py 和 cusvaer 可以共存。