大规模线性稀疏问题的求解挑战
电子设计自动化(EDA)、计算流体动力学(CFD)和先进优化工作流中大规模问题的求解已成为常态。随着芯片设计、制造和多物理场模拟复杂度的增长,这些工作负载对传统求解器提出了前所未有的可扩展性和性能要求。NVIDIA CUDA Direct Sparse Solver(cuDSS)使用户能够以最少的代码更改大规模运行稀疏求解器,为下一代工程和设计解锁突破性的速度和效率。
可以利用CPU/GPU混合内存模式运行原本无法装入单个GPU内存的更大问题,或者跨多个GPU甚至多个节点轻松扩展工作负载。本文讨论使用cuDSS求解大规模问题的用户策略。
快速入门
本文假设已经拥有使用cuDSS的有效代码。可能已经探索过GitHub上的入门示例(此处和此处),这些示例演示了在单个GPU上运行cuDSS以及使用Get和Set函数调整默认求解参数。这些示例涵盖了创建矩阵和主要cuDSS对象,以及执行cuDSS的三个核心阶段:分析、数值分解和求解。
得益于近期GPU世代内存容量的增加,即使是单个GPU也能处理相当大规模的稀疏问题。然而,当处理真正庞大的问题(超过1000万行和超过10亿个非零元素)时,有有效的策略可以使cuDSS快速高效运行。第一种方法仍然使用单个GPU,但引入了处理这些更大挑战的技术,无需重大代码更改。
重新思考数据类型:为什么INT64现在很重要
为cuDSS创建稠密或稀疏矩阵时,可能会使用两个函数之一:cudssMatrixCreateDn()或cudssMatrixCreateCsr(),甚至两者都用。从文档中,函数描述如下。
cudssMatrixCreateDn
cudssStatus_t cudssMatrixCreateDn(
cudssMatrix_t *matrix,
int64_t nrows,
int64_t ncols,
int64_t ld,
void *values,
cudaDataType_t valueType,
cudssLayout_t layout)
第二个函数cudssMatrixCreateCsr()如下所示。
cudssMatrixCreateCsr
cudssStatus_t cudssMatrixCreateCsr(
cudssMatrix_t *matrix,
int64_t nrows,
int64_t ncols,
int64_t nnz,
void *rowStart,
void *rowEnd,
void *colIndices,
void *values,
cudaDataType_t indexType,
cudaDataType_t valueType,
cudssMatrixType_t mtype,
cudssMatrixViewType_t mview,
cudssIndexBase_t indexBase)
在cuDSS 0.7.0之前的版本中,稀疏矩阵的索引只能使用32位整数。具体来说,rowStart、rowEnd和colIndices的基础数据类型只能是int,参数indexType只能是CUDA_R_32I。从cuDSS 0.7.0开始,可以通过使用int64类型的64位整数索引数组和CUDA_R_64I作为indexType来解决更大的问题。
注意: 输入矩阵的行数和列数限制在2^31以内(但使用64位索引时,输入矩阵可以拥有多得多的非零元素)。
混合内存模式——模糊CPU和GPU的界限
cuDSS混合内存模式旨在通过使用GPU和CPU内存来克服单个GPU在求解极大稀疏线性问题时遇到的内存限制。
然而,这需要权衡:CPU和GPU之间的数据传输需要时间,并受总线带宽限制。虽然能够处理更大的问题,但应预期由于这些传输会导致一定的性能损失。也就是说,得益于现代某机构驱动程序优化和快速的CPU/GPU互连(例如某机构Grace Blackwell节点中的互连),这种损失是可管理的——对于某些问题规模,混合内存性能的扩展令人印象深刻。
混合内存模式默认不启用,因此启用的第一步是调用函数cudssConfigSet()来设置CUDSS_CONFIG_HYBRID_MODE,这告诉cuDSS使用混合内存模式。注意此更改必须在调用cudssExecute()之前完成。
设备内存默认由cuDSS自动管理。它管理需要多少设备内存——最多可达整个GPU的容量。或者,用户可以通过设置用户定义的限制(范围从CUDSS_DATA_HYBRID_DEVICE_MEMORY_MIN的值到分析(符号分解)阶段后可用的设备内存)来指定较小的内存占用,该值可以通过某机构CUDA运行时API cudaMemGetInfo查询。需要注意的几个要点:
- 即使混合内存已开启,cuDSS首先尝试利用设备内存(如果可能则避免使用CPU内存)以达到最佳性能。
- 使用最大GPU内存时性能最佳(这将减少CPU和GPU之间的内存传输次数)
- 混合内存限制可以按设备设置(如下一个代码块所示)
示例代码指导如何获取最小设备内存需求并相应设置内存限制,从而精细控制内存占用。
...
/* 启用混合模式,其中因子存储在主机内存中。
注意:必须在第一次调用ANALYSIS步骤之前设置。*/
int hybrid_mode = 1;
CUDSS_CALL_AND_CHECK(cudssConfigSet(solverConfig, CUDSS_CONFIG_HYBRID_MODE,
&hybrid_mode,sizeof(hybrid_mode)), status,
"cudssConfigSet CUDSS_CONFIG_HYBRID_MODE");
/* 符号分解 */
...
/* (可选)用户可以查询混合内存模式下足够的设备内存最小量。
注意:默认情况下,如果需要,cuDSS会尝试使用所有可用的
设备内存 */
size_t sizeWritten;
int64_t device_memory_min;
CUDSS_CALL_AND_CHECK(cudssDataGet(handle, solverData,
CUDSS_DATA_HYBRID_DEVICE_MEMORY_MIN,
&device_memory_min, sizeof(device_memory_min),
&sizeWritten), status,
"cudssDataGet for\
CUDSS_DATA_HYBRID_DEVICE_MEMORY_MIN");
printf("cuDSS example: minimum amount of device memory\n"
"for the hybrid memory mode is %ld bytes\n",
device_memory_min);
/* (可选)用户可以指定有多少设备内存可用于
cuDSS
注意:默认情况下,如果需要,cuDSS会尝试使用所有可用的\
设备内存 */
int64_t hybrid_device_memory_limit = 40 * 1024 ; // 字节单位 = 40 KB
CUDSS_CALL_AND_CHECK(cudssConfigSet(solverConfig,
CUDSS_CONFIG_HYBRID_DEVICE_MEMORY_LIMIT,
&hybrid_device_memory_limit,
sizeof(hybrid_device_memory_limit)),
status,
"cudssConfigSet for\
CUDSS_CONFIG_HYBRID_DEVICE_MEMORY_LIMIT");
printf("cuDSS example: set the upper limit on device memory\n"
"for the hybrid memory mode to %ld bytes\n",
hybrid_device_memory_limit);
/* 分解 */
...
/* 求解 */
...
第一个cuDSS函数cudssConfigSet()在调用第一个分析步骤(符号分解)之前启用混合内存模式。然后使用cudssDataGet()查找混合内存模式下足够的设备内存最小量。函数调用cudssConfigSet()指定用于cuDSS的设备内存量。注意,有时自动内存管理会导致内存不足(OOM)错误。
对于集成的开发者,文档中的调试技巧非常有价值——阅读它们可以避免一些麻烦。
混合内存模式的性能取决于将数据从CPU移动到GPU的CPU/GPU内存带宽。为了说明这一点,下图1显示了使用cuDSS混合内存模式求解的矩阵大小从100万到1800万时分解和求解的加速比。基线是单个某机构B200 GPU节点。观察到的加速比比较了在Grace Blackwell节点与x86 Blackwell节点上执行的相同模型,反映了两节点之间的内存带宽比。
图1. GB200与B200的分解和求解阶段加速比(cuDSS混合内存模式使用最小所需设备内存: B200 + Grace (72核) – 480GB 对比 B200 + X86 CPU (112核))
通过INT64和混合内存模式cuDSS编码策略,可以容纳更大的问题规模,并且如果需要,可以使用节点上所有可能的内存。但仍然受限于单个GPU。下一个策略允许使用更多GPU来容纳更大的问题。这也允许通过使用更多GPU来更快地求解固定规模的问题。
倍增计算能力:多GPU模式(MG模式)
cuDSS多GPU模式(MG模式)允许开发者在单个节点内使用所有GPU,而无需开发者指定任何分布式通信层。cuDSS内部处理使用GPU所需的所有通信。在以下三种场景中很有帮助:
- 当问题太大,无法装入单个设备(无论是否使用混合内存)时。
- 当用户希望避免混合内存模式的性能损失时。
- 当用户专注于强扩展——使用更多GPU来更快地求解问题。
MG模式的亮点在于开发者无需指定通信层:不需要使用MPI、NCCL或其他通信层。cuDSS为您完成所有这些工作。
此外,由于CUDA在Windows节点上对MPI感知通信的限制,MG模式对于在Windows上运行的应用程序变得特别有价值。
下图2说明了在配备一个、两个和四个GPU的某机构DGX H200节点上求解大约3000万行矩阵所需的时间(以秒为单位),顶部图表是分解时间,底部图表是求解时间。初始计算在单个GPU上执行,随后使用MG模式在两个和四个GPU上运行。如图所示,与单个GPU相比,使用两个GPU求解模型显著减少了计算时间,尽管代价是增加了GPU资源使用。
图2. 在H200上使用一个、两个和四个GPU配置的分解和求解时间,使用Cadence的MCAE应用程序。该矩阵大约有3100万行和列,大约有10亿个非零元素。
此示例展示了如何利用MG模式。相关代码部分总结如下。注意这包括使用混合内存模式的代码。这很重要,因为如果使用混合内存,必须在所有将使用的设备上设置设备内存限制。
...
/* 创建cuDSS库句柄 */
cudssHandle_t handle;
/* 查询实际可用设备数量 */
int device_count = 0;
cuda_error = cudaGetDeviceCount(&device_count);
if (cuda_error != cudaSuccess || device_count <= 0) {
printf("ERROR: no GPU devices found\n");
fflush(0);
return -1;
}
/* device_indices可以设置为NULL。在这种情况下,cuDSS将使用设备
* 从0到(device_count - 1)
*/
int *device_indices = NULL;
device_indices = (int *)malloc(device_count * sizeof(int));
if (device_indices == NULL) {
printf("ERROR: failed to allocate host memory\n");
fflush(0);
return -1;
}
for (int i = 0; i < device_count; i++)
device_indices[i] = i;
...
/* 为多个设备初始化cudss句柄 */
CUDSS_CALL_AND_CHECK(cudssCreateMg(&handle, device_count, device_indices),
status, "cudssCreate");
...
/* 创建cuDSS求解器配置和数据对象 */
cudssConfig_t solverConfig;
cudssData_t solverData;
CUDSS_CALL_AND_CHECK(cudssConfigCreate(&solverConfig), status,
"cudssConfigCreate");
/* 向solverConfig传递相同的device_count和device_indices */
CUDSS_CALL_AND_CHECK(cudssConfigSet(solverConfig,
CUDSS_CONFIG_DEVICE_COUNT, &device_count,
sizeof(device_count)), status,
"cudssConfigSet for device_count");
CUDSS_CALL_AND_CHECK(cudssConfigSet(solverConfig,
CUDSS_CONFIG_DEVICE_INDICES, device_indices,
device_count * sizeof(int)), status,
"cudssConfigSet for device_count");
CUDSS_CALL_AND_CHECK(cudssDataCreate(handle, &solverData), status,
"cudssDataCreate");
...
/* 符号分解 */
CUDSS_CALL_AND_CHECK(cudssExecute(handle, CUDSS_PHASE_ANALYSIS,
solverConfig, solverData, A, x, b),
status, "cudssExecute for analysis");
...
/* 查询CUDSS_DATA_HYBRID_DEVICE_MEMORY_MIN应对每个设备
* 单独执行,在调用cudssDataGet之前先调用cudaSetDevice()。
* 获取CUDSS_DATA_MEMORY_ESTIMATES同理。
* 使用cudssConfigSet()设置CUDSS_CONFIG_HYBRID_DEVICE_MEMORY_LIMIT同理
*/
int default_device = 0;
cudaGetDevice(&default_device);
for (int dev_id = 0; dev_id < device_count; dev_id++) {
cudaSetDevice(device_indices[dev_id]);
int64_t hybrid_device_memory_limit = 0;
size_t sizeWritten;
CUDSS_CALL_AND_CHECK(cudssDataGet(handle, solverData,
CUDSS_DATA_HYBRID_DEVICE_MEMORY_MIN,
&hybrid_device_memory_limit,
sizeof(hybrid_device_memory_limit),
&sizeWritten),
status, "cudssDataGet for the memory estimates");
printf("dev_id = %d CUDSS_DATA_HYBRID_DEVICE_MEMORY_MIN %ld bytes\n",
device_indices[dev_id], hybrid_device_memory_limit);
}
/* cuDSS要求所有API调用在默认设备上进行,所以
* 重置设备上下文。
*/
cudaSetDevice(default_device);
/* 分解 */
CUDSS_CALL_AND_CHECK(cudssExecute(handle, CUDSS_PHASE_FACTORIZATION,
solverConfig, solverData, A, x, b),
status, "cudssExecute for factor");
/* 求解 */
CUDSS_CALL_AND_CHECK(cudssExecute(handle, CUDSS_PHASE_SOLVE, solverConfig,
solverData, A, x, b), status,
"cudssExecute for solve");
...
设置MG模式很简单。首先找到节点上的设备数量并使用全部,或者使用想要的特定设备数量。然后将设备索引设置为从设备0开始的设备数量(代码将使用前device_count个设备,如果需要可以更改为特定的设备编号)。可以轻松地通过命令行或文件输入设备数量和设备编号列表,使代码更灵活。
此后,通过调用cudssCreateMg()为多个设备初始化cuDSS句柄,开始特定的MG编码。但在调用求解阶段之前,还需要用设备信息初始化cuDSS配置。具体来说,使用cudssConfigCreate()创建cuDSS求解器配置对象后,应使用cudssConfigSet()为MG模式设置以下配置详情:
CUDSS_CONFIG_DEVICE_COUNT,使用数组device_count。CUDSS_CONFIG_DEVICE_INDICES,使用数组device_indices。
然后使用函数cudssDataCreate()为cuDSS创建solverData,并接下来执行分析阶段。
如果使用混合内存模式,在分解之前,可能需要为每个设备单独设置设备内存限制。如上代码所示。完成后,就可以分解矩阵并求解问题。
MG模式的一个亮点是不需要为GPU之间的通信编码。cuDSS为您完成所有这些工作。然而,使用MG模式目前存在一些限制。
- MG模式与多GPU多节点(MGMN)模式联合使用不受支持(下一节讨论MGMN)
- 分布式输入目前不受支持。
- 当使用
CUDSS_ALG_1或CUDSS_ALG_2进行重排序时,MG模式不受支持。 - MG模式不支持矩阵批处理。
- MG模式中的所有阶段都是同步的。
- 所有数据必须在调用
cudssExecute之前位于第一个设备(rank 0)上,然后cuDSS会根据需要将数据分发到其他设备。
更进一步:用于分布式能力的多GPU多节点(MGMN)模式
那么,如果一个节点不够,想要将计算扩展到多个节点呢?这就是MGMN模式的用武之地。这需要一个通信层,一旦添加,将允许使用节点内任何或所有GPU以及多个节点——没有限制。这使用户能够求解海量问题,或者使用更多GPU更快地求解问题。
cuDSS使用一种抽象——一个小的通信"垫片"层,可以定制为CUDA-aware Open MPI、某机构NCCL,甚至是自定义的通信层。
此MGMN示例的代码同时适用于Open MPI和NCCL。如果希望使用自己的通信层,有关于如何操作的说明。
为了说明通信层的使用方式,下面展示了示例中同时包含MPI和NCCL代码的ifdef代码块。编译期间定义了一些常量,这些常量对此示例很重要,但未在代码块中显示。它们是USE_MPI和USE_NCCL,定义了要使用的代码路径。
这个ifdef代码块对应示例代码中的520-535行(这些行号可能随后续版本变化,请仔细核对)。
#ifdef USE_MPI
#if USE_OPENMPI
if (strcmp(comm_backend_name,"openmpi") == 0) {
CUDSS_CALL_AND_CHECK(cudssDataSet(handle, solverData, CUDSS_DATA_COMM,
mpi_comm, sizeof(MPI_Comm*)),
status,
"cudssDataSet for OpenMPI comm");
}
#endif
#if USE_NCCL
if (strcmp(comm_backend_name,"nccl") == 0) {
CUDSS_CALL_AND_CHECK(cudssDataSet(handle, solverData, CUDSS_DATA_COMM,
nccl_comm, sizeof(ncclComm_t*)),
status,
"cudssDataSet for NCCL comm");
}
#endif
#endif
注意,定义MPI或NCCL的代码更改基本上是极简的。两者之间的代码差异很简单。可以以非常类似的方式使用自己的通信层。
一旦定义了通信器指针,通过代码中所示的CUDSS_DATA_COMM传递给cuDSS(如前一个代码片段所示),除非代码特别需要,否则不需要使用任何通信层函数。cuDSS在"幕后"使用定义的通信层,因此不需要为其编码。查看示例代码以了解如何使用多个节点。
关于实现自己的通信层,可以在cuDSS文档的高级主题部分找到很好的介绍性讨论。
通信层要求的高层概述如下:
- MGMN模式通过将所有通信特定的原语抽象到一个单独构建的小型垫片通信层来启用。
- 用户可以使用他们选择的通信后端(MPI、NCCL等)拥有自己的通信层实现。
- cuDSS中启用的MGMN执行不需要对不使用MGMN模式的应用程序进行任何更改。
- MGMN模式支持输入CSR矩阵、稠密右端项或解的1D行向分布(带重叠),使用
cudssMatrixSetDistributedRow1D()函数(参见下一段)。
cuDSS MGMN模式可选地接受预分布式输入,并可选择性地创建分布式输出。可以将A和B都放在rank 0设备上,在这种情况下,cuDSS将分发它们,或者可以使用cudssMatrixSetDistributedRow1d()函数告诉cuDSS数据在设备和节点上的分布方式。开发者必须确保数据位于正确节点和设备的正确位置。
良好性能的一个关键步骤是仔细选择CPU:GPU:NIC的绑定。这里不讨论,但在其他地方有文档记录。
MGMN模式目前存在一些限制:
- 当使用
CUDSS_ALG_1或CUDSS_ALG_2进行重排序时,MGMN模式不受支持。 - MGMN模式不支持矩阵批处理。
- MGMN模式中的所有阶段都是同步的。
要点总结
稀疏线性系统出现在许多学科中。受解决现实问题的需求推动,问题的整体规模正在快速增长。开发者必须找到既高效又快速解决这些问题的方法。某机构cuDSS提供了一个易于使用的库,用于使用某机构GPU求解日益庞大的问题。
有关可以与cuDSS一起使用的更多功能,建议阅读文档的高级功能部分。它们包含此处介绍的功能以及其他能力的更多信息,以帮助求解大规模稀疏线性问题。还有一个部分解释了在开发代码时如何与cuDSS一起进行日志记录。这是一个很好的资源,因为调试并行代码可能具有挑战性。cuDSS在执行代码时获取日志信息方面具有一些出色的功能。
订阅cuDSS客户页面以获取最新创新动态。FINISHED