NIVIDIA 高性能计算CUDA笔记 (五) cuSOLVER库的简介与矩阵特征值分解示例

NIVIDIA 高性能计算CUDA笔记 (五)

cuSOLVER库的简介与矩阵特征值分解示例

​ cuSOLVER是NVIDIA提供基于cuBLAS和cuSPARSE库的GPU加速的线性代数操作算子库,其专注于稠密和稀疏矩阵的高级线性代数运算,,包括矩阵分解和求解方程等内容。其里面包括了三个独立库:\(cuSOlverDN、cuSolver、cuSolverRF\) 三部分。其特点为:

  • 高性能:利用GPU并行计算能力,显著加速线性代数运算;
  • 兼容性:支持单精度(float)、双精度(double)、复数等数据类型;
  • 易用性:提供类似LAPACK的API,便于传统CPU代码迁移到GPU;
  • 与cuBLAS集成:底层依赖cuBLAS进行基本线性代数运算;

​ cuSolver的典型应用场景:(1)科学计算(如计算流体力学、结构分析);(2)机器学习(如PCA、线性回归);(3)信号处理;(4)计算视觉等。 cuSOLVER与cuBLAS的区别:

  • cuBLAS主要提供基础的线性代数运算(如矩阵乘法,向量操作);
  • cuSolver专注于高级线性代数求解(如方程组求解,矩阵分解);

1.cuSolver库主要功能API说明

​ cuSOLVER主要分为三个部分及部分通用函数接口:

  • **cuSolverDN (Dense Linear Algebra)**用于稠密矩阵的运算,cuSolverSP库主要设计用于求解稠密线性方程组:

\[\mathbf{Ax}=\mathbf{b} \tag{1} \]

其中系数矩阵、右侧向量和解向量:\(\mathbf{A}\in \mathbf{R}{n\times{n}},\mathbf{b}\in\mathbf{R}{n},\mathbf{x}\in{\mathbf{R}_{n}}\)。

​ \(cuSolverDN\) 库提供了\(QR\)分解和\(LU\) 分解,以处理一般的矩阵,该矩阵可能是非对称的。对于对称或Hermit矩阵提供了Cholesky分解。对于对称不定的矩阵,提供了LDL分解。

其主要功能包括:

功能 关键函数(前缀为cuSolverDN) 说明
矩阵分解 [s/d/c/z]getrf LU分解
[s/d/c/z]potrf Cholesky分解
[s/d/c/z]getrs QR分解
线性方程组求解 [s/d/c/z]getrs 用LU分解结果求解
[s/d/c/z]ports 用Cholesky分解后求解
[s/d/c/z]gels 最小二乘问题(超定或欠定方程组)
特征值问题 [s/d]syevd 对称矩阵特征值分解(分治算法)
[c/z]heevd 埃尔米特矩阵特征值分解
奇异值分解 [s/d/c/z]gesvd 奇异值分解(SVD)
句柄操作 cusolverDnCreate() 创建句柄
cusolverDnDestroy() 销毁句柄
  • cuSolverSP(Sparse Linear Algebra) 用于稀疏矩阵运算,为稀疏矩阵提供了一个新的工具箱,用于解决稀疏线性系统,包括\(QR\)分解,由于不是所有稀疏模式都有性能良好的分解算法模式,所以\(cuSolver\)还提供一些CPU执行的方法。

    cuSolver库主要设计用于求解稀疏线性系统

    \[\mathbf{A}\mathbf{x}=\mathbf{b} \tag{2} \]

    以及稀疏最小二乘问题:

    \[\mathbf{x}=argmin||\mathbf{A}\mathbf{x}-\mathbf{b}|| \tag{3} \]

    其中\(\mathbf{A}\in{R^{m\times{n}}}\)稀疏矩阵,右侧输出向量\(b\in{R^{m}}\) 和解向量\(x\in{R^{n}}\)。其核心算法是基于稀疏矩阵的编码分解。矩阵以\(CSR\) 格式被接受。如果矩阵是对称的或Hermit矩阵,因此用户必须提供完整的矩阵,即填充缺失的下部分或上部。如果矩阵是对称正定的,用户只需要求解,则需要Cholesky分解可行,用户只需要提供的下三角部分。除了线性和最小二乘求解器,cuSolver库还提供了一个幂乘法的简单特征求解器。

    功能 关键函数(前缀为cusolverSP) 说明
    稀疏线性方程求解(CPU分析) csrlsvlu 使用LU分解求解(非对称矩阵)
    csrlsvqr 使用QR分分解求解(任意矩阵)
    csrlsvchol 使用Cholesky分解求解(对称正定)
    稀疏最小二乘法问题 csrlsqvqr 稀疏最小二乘法(QR分解)
    稀疏矩阵特征值分解 [s/d]csreigvsi 稀疏对称矩阵特征值(迭代)
    句柄管理 cusolverSpCreate() 创建句柄
    cusolverSpDestroy() 销毁句柄
  • cuSolverRF(Refactorization) 用于矩阵分解,加速解决一系列具有相同稀疏模式但不同数值的线性方程组。

    \(cuSolverRF\) 库旨在通过快速重分解加速线性系统集合的求解,当给定相同稀疏度模式的新系数时

    \[\mathbf{A}_i\mathbf{x}_i=f_i \tag{4} \]

    其中给出了一组系数矩阵、右侧的观测向量和解向量:\(\mathbf{A}_i\in{\mathbf{R}^{n\times{n}}},\mathbf{f}_i\in\mathbf{R}^n,\mathbf{x}_i\in{R^n}(i=1,...,k)\)

    功能 关键函数(前缀为cusolverRf) 说明
    重分解求解 cusolverRfCreate() 创建句柄
    cusolverRfSetup[Host/Device]() 设置矩阵
    cusolverRfAnalyze() 分析矩阵的结构
    cusolverRfRefactor() 数值重分解
    cusolverRfSolve() 线性方程矩阵求解
    cusolverRfDestroy() 销毁句柄
  • 通用接口及辅助功能主要接口

功能 关键函数的接口 说明
错误处理 cusolverGetStatusString() 获取错误信息
设备管理 cusolverGetProperty() 获取库版本号
内存管理 cusolverDn/SpSetStream() 设置CUDA流
cusolverDn/SpGetStream() 获取CUDA流
缓存管理 cusolverDn/SpSetWorkspace() 设置工作空间
cusolverDn/Sp[Device/Host]BufferSize() 计算所需缓冲区大小

注意: 数据类型前缀说明:

数据类型的前缀 数据类型的说明
\(s\) 单精度浮点(float)
\(d\) 双精度浮点(double)
\(c\) 单精度复数(cuComplex)
z 双精度复数(cuDoubleComplex)

主要数据结构:

句柄数据结构 功能描述
cusolverDnHandle_t cuSolverDN句柄(上下文)
cusolverSpHandle_t cuSolverSP 句柄 (上下文)
cusolverRfHandle_t cuSolverRF 句柄 (上下文)
csr[lu/qr/chol]Info_t 稀疏分解信息结构体

2.cusolver的特征值分解示例

cuSolver的特征值分解的流程:

  1. 创建\(cuSolverDN\)句柄;
  2. 在设备上分配内存,并将矩阵A和数据传输到设备;
  3. 准备workspace,并计算所需workspace大小;
  4. 调用syevd函数进行计算;
  5. 将结果(特征值、特征向量)传输回主机;
  6. 释放设备内存和句柄;

注意: 矩阵A是**列优先存储(**和Fortran一样,即列主序)。在C/C++中我们通常按行主序存储,因此需要注意转换。

下面以双精度实数对称矩阵为例,使用cusolverDnDsyevd函数。

函数 矩阵类型 算法 特点 适用场景
syevd 实对称/复埃尔米特 分治法 稳定,内存占用较大 中小型矩阵,需要高精度
syevdx 实对称/复埃尔米特 二分法+逆迭代 可计算特征值子集 只需要部分特征值
syevj 实对称/复埃尔米特 Jacobi法 高精度,可并行性好 需要高精度特征向量
syevjBatched 实对称/复埃尔米特 Jacobi法 批量处理 多个小矩阵同时计算
gesvd 一般矩阵 SVD分解 计算奇异值和向量 奇异值分解问题

2.1实数对称矩阵的特征值分解

C++ 复制代码
#include <stdio.h>
#include <stdlib.h>
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <cusolverDn.h>

int main()
{
	cusolverDnHandle_t cusolverH = NULL;
	const int m = 3;
	const int lda = m;
	float *A = (float*)malloc(lda*m * sizeof(float));
	A[0] = 3.5;
	A[1] = 0.5;
	A[2] = 0;
	A[3] = 0.5;
	A[4] = 3.5;
	A[5] = 0;
	A[6] = 0;
	A[7] = 0;
	A[8] = 2;
	float W[m]; // eigenvalues最终保存结果
	int info_gpu = 0;//计算状态保存
	// 步骤1:声明句柄
	cusolverDnCreate(&cusolverH);
	// 步骤2:分配显存空间
	float *d_A = NULL; cudaMalloc((void**)&d_A, sizeof(float) * lda * m);//声明Hermite矩阵(与计算后的特征向量为同一空间)
	float *d_W = NULL; cudaMalloc((void**)&d_W, sizeof(float) * m);//声明特征值存储空间
	int *devInfo = NULL; cudaMalloc((void**)&devInfo, sizeof(int));//声明计算结果状态空间
	cudaMemcpy(d_A, A, sizeof(float) * lda * m, cudaMemcpyHostToDevice);//数据拷贝
	// 步骤3:申请计算缓存空间,并在显存中申请该空间
	float *d_work = NULL;
	int lwork = 0;
	cusolverEigMode_t jobz = CUSOLVER_EIG_MODE_VECTOR; // compute eigenvalues and eigenvectors.
	cublasFillMode_t uplo = CUBLAS_FILL_MODE_LOWER;
	cusolverDnSsyevd_bufferSize(cusolverH, jobz, uplo, m, d_A, lda, d_W, &lwork);//计算evd计算所需存储空间,保存到lwork中
	cudaMalloc((void**)&d_work, sizeof(float)*lwork);
	// 步骤4:特征分解
	cusolverDnSsyevd(cusolverH, jobz, uplo, m, d_A, lda, d_W, d_work, lwork, devInfo);
	cudaDeviceSynchronize();
	//步骤5:数据读回
	cudaMemcpy(A, d_A, sizeof(float)*lda*m, cudaMemcpyDeviceToHost);
	cudaMemcpy(W, d_W, sizeof(float)*m, cudaMemcpyDeviceToHost);
	cudaMemcpy(&info_gpu, devInfo, sizeof(int), cudaMemcpyDeviceToHost);

	printf("%d\n", info_gpu);
	printf("eigenvalue = (matlab base-1), ascending order\n");
	for (int i = 0; i < m; i++) {
		printf("W[%d] = %E\n", i + 1, W[i]);
	}
	for (size_t i = 0; i < m; i++)
	{
		for (size_t j = 0; j < m; j++)
		{
			printf("%.4f\n", A[i + j*m]);
		}
	}

    return 0;
}

2.2 复数Hermit矩阵的双精度特征分解的案例

C++ 复制代码
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>
#include <stdlib.h>
#include <cusolverDn.h>

int main()
{
	cusolverDnHandle_t cusolverH = NULL;
	const int m = 4;
	const int lda = m;

	cuDoubleComplex *A = (cuDoubleComplex*)malloc(lda*m*sizeof(cuDoubleComplex));
	A[0] = make_cuDoubleComplex(1.9501e2,0);
	A[1] = make_cuDoubleComplex(0.2049e2, 0.1811e2);
	A[2] = make_cuDoubleComplex(0.5217e2, 0.3123e2);
	A[3] = make_cuDoubleComplex(0.2681e2, 0.3998e2);
	A[4] = make_cuDoubleComplex(0.2049e2, -0.1811e2);
	A[5] = make_cuDoubleComplex(1.8272e2, 0);
	A[6] = make_cuDoubleComplex(0.5115e2, -0.0987e2);
	A[7] = make_cuDoubleComplex(0.4155e2, -0.0435e2);
	A[8] = make_cuDoubleComplex(0.5217e2, -0.3123e2);
	A[9] = make_cuDoubleComplex(0.5115e2, 0.0987e2);
	A[10] = make_cuDoubleComplex(2.3984e2, 0);
	A[11] = make_cuDoubleComplex(0.4549e2, -0.0510e2);
	A[12] = make_cuDoubleComplex(0.2681e2, -0.3998e2);
	A[13] = make_cuDoubleComplex(0.4155e2, 0.0435e2);
	A[14] = make_cuDoubleComplex(0.4549e2, 0.0510e2);
	A[15] = make_cuDoubleComplex(2.2332e2, 0);

	//1.9501 + 0.0000i   0.2049 - 0.1811i   0.5217 - 0.3123i   0.2681 - 0.3998i
	//	0.2049 + 0.1811i   1.8272 + 0.0000i   0.5115 + 0.0987i   0.4155 + 0.0435i
	//	0.5217 + 0.3123i   0.5115 - 0.0987i   2.3984 + 0.0000i   0.4549 + 0.0510i
	//	0.2681 + 0.3998i   0.4155 - 0.0435i   0.4549 - 0.0510i   2.2332 + 0.0000i

	double W[m]; // eigenvalues最终保存结果
	int info_gpu = 0;//计算状态保存
	// step 1: create cusolver/cublas handle
	cusolverDnCreate(&cusolverH);
	// step 2: copy A and B to device
	cuDoubleComplex *d_A = NULL; cudaMalloc((void**)&d_A, sizeof(cuDoubleComplex) * lda * m);//声明Hermite矩阵(与计算后的特征向量为同一空间)
	double *d_W = NULL; cudaMalloc((void**)&d_W, sizeof(double) * m);//声明特征值存储空间
	int *devInfo = NULL;cudaMalloc((void**)&devInfo, sizeof(int));//声明计算结果状态空间

	cudaMemcpy(d_A, A, sizeof(cuDoubleComplex) * lda * m, cudaMemcpyHostToDevice);//数据拷贝
	// step 3: query working space of syevd
	cuDoubleComplex *d_work = NULL;
	int lwork = 0;
	cusolverEigMode_t jobz = CUSOLVER_EIG_MODE_VECTOR; // compute eigenvalues and eigenvectors.
	cublasFillMode_t uplo = CUBLAS_FILL_MODE_LOWER;
	cusolverDnZheevd_bufferSize(cusolverH, jobz, uplo, m, d_A, lda, d_W, &lwork);//计算evd计算所需存储空间,保存到lwork中
	cudaMalloc((void**)&d_work, sizeof(cuDoubleComplex)*lwork);

	// step 4: compute spectrum
	cusolverDnZheevd(cusolverH, jobz, uplo, m, d_A, lda, d_W, d_work, lwork, devInfo);
	cudaDeviceSynchronize();
	cudaMemcpy(A, d_A, sizeof(cuDoubleComplex)*lda*m,cudaMemcpyDeviceToHost);
	cudaMemcpy(W, d_W, sizeof(double)*m, cudaMemcpyDeviceToHost);
	cudaMemcpy(&info_gpu, devInfo, sizeof(int), cudaMemcpyDeviceToHost);

	printf("%d\n", info_gpu);
	printf("eigenvalue = (matlab base-1), ascending order\n");
	for (int i = 0; i < m; i++) {
		printf("W[%d] = %E\n", i + 1, W[i]);
	}
	for (size_t i = 0; i < 4; i++)
	{
		for (size_t j = 0; j < 4; j++)
		{
			printf("%.4f + %.4f j\n", A[i * 4 + j].x, A[i * 4 + j].y);
		}
	}
	cudaFree(d_A);
	cudaFree(d_W);
	cudaFree(devInfo);
	cudaFree(d_work);
	cusolverDnDestroy(cusolverH);

    return 0;
}

注意事项:

  1. 存储顺序 :cuSolver 使用列优先存储(Fortran风格)
  2. 矩阵对称性:确保输入矩阵满足对称性要求
  3. 工作空间:必须先查询正确的工作空间大小
  4. 错误检查 :总是检查 d_info 和函数返回值
  5. 异步执行:大多数 cuSolver 函数是同步的,会阻塞直到完成
  6. 流管理 :可以使用 cusolverDnSetStream() 将计算绑定到特定 CUDA 流

3.参考资料