1. 引言
随着计算机应用对算力需求的指数级增长及计算场景的多元化发展,处理器经过不断的革新。虽然处理器的计算性能不断提升,但是受纳米级集成电路工艺的物理的限制,单核处理器的主频提升已接近瓶颈,性能优化空间逐渐缩小。在这个背景下,通过多核并行计算架构[1]来提升计算性能,已成为当前计算技术研究的核心方向。
面对高分辨率气候建模、超大规模深度学习模型训练等复杂计算场景,并行计算技术[2]通过多节点任务协同与硬件的加速能力,已经成为突破单核处理器计算性能瓶颈的关键方法。其中,基于MPI (消息传递接口)的分布式内存并行架构[3],以及依靠GPU (图形处理单元)的众核并行加速技术[4],不仅通过细粒度任务划分与流水线调度实现了计算资源的高效利用,更在异构计算框架下为科学计算与工业智能化提供了高效的解决方案。
在电磁场、流体力学、结构力学等许多工程问题和物理问题都涉及到大规模的线性方程组的求解[5]。大规模稠密线性方程组矩阵的求解成为了计算模拟中的关键步骤[6]。高斯消元法[7]和LU分解[8]作为两种经典的直接求解方法,因其较为稳定且易于实现的特点,被广泛应用于线性方程组的求解中。对于稠密矩阵,通常使用LU分解,LU分解即将一个线性方程组的系数矩阵分解为一个单位下三角矩阵和一个上三角矩阵,再利用前向回代和后向回代求出解向量x,这种方法非常适用于稠密矩阵的情况。基于MPI或GPU的并行计算技术的应用,为解大规模的线性方程组提供了有效的解决方案。将LU分解过程中的计算任务分配给多个处理单元并行执行,可以大大的缩短计算时间并提高资源的利用率。基于GPU的并行计算依靠其强大的并行处理能力,在处理大规模稠密线性方程组时,能够大幅提升矩阵分解过程的计算效率。
CUDA (Compute Unified Device Architecture)是由NVIDIA公司推出的一种并行计算平台和编程模型[9],开发者能够利用GPU进行并行计算。2006年发布到现在为止,CUDA已成为高性能计算领域的一个重要工具,广泛的应用于图像处理、机器学习、科学计算、金融建模等多个领域。因为GPU比传统的CPU具有更高的并行处理能力,所以在计算大量的数据时,利用GPU能够大大提高计算效率。
本文提出一种基于CUDA的LU分解并行算法,将矩阵划分为动态优化调整的线程块网格,利用共享内存的高速缓存来加速LU分解,该设计直接通过GPU显存来进行数据的交互,避免MPI的各个进程之间的通信延迟,从而提高了线性方程组的求解效率。
2. 稠密线性方程组的并行计算简述
在计算科学与应用数学交叉领域,并行计算作为一种新型的计算模式,它是一种通过同时使用多核处理器、GPU以及分布式计算集群的等多个计算资源,来加快求解大规模的复杂问题的新型计算方式,其核心在于将大规模问题分解为可并行的子任务,利用计算资源的空间并行性来加速问题求解的效率。
线性方程组是由多个线性方程所组成的,其目标是求解一组未知数的值,使得这组数满足每一个线性方程,而这类线性方程组的系数矩阵通常为是稠密矩阵,即非零元素占比超过90%的系数矩阵。
3. LU分解法原理
计算求解
的线性方程组,假设A的顺序主子式都不为零,且本文中仅讨论当A为方阵时的情景,那么A可以进行LU分解,即
,其中L为单位下三角矩阵,U为上三角矩阵,
由LU分解法可知,先计算U的第一行,可得
;再计算L的第一列,可得
,依次循环,则计算U的第r行为
;接着计算L的第r列
,即A的LU分解可以按照行列,行列,,依次进行,直至分解完成,得到一个单位下三角矩阵L和一个上三角矩阵U。对于线性方程组
,利用LU分解法得到L和U后,根据L和U的特性,可利用
,先向前回代求解
求出y,再向后回代求解
,最后求出解向量x。
4. CUDA编程模型
在CUDA架构中,引入了主机(Host)和设备(Device)这两个概念,其中,CPU及系统内存作为Host,负责任务调度、控制流程以及串行计算,而GPU及其显存作为Device,专门用于并行计算和数据处理。在程序执行过程中,CUDA在单指令多数据(SIMD, Single Instruction Multiple Data)模型的基础上进行了优化,采用单指令多线程(SIMT, Single Instruction Multiple Thread)模型,使得同一指令序列能够生成多个CUDA线程,并将一个线程束中的CUDA线程映射到GPU不同的CUDA核心,独立处理不同的数据。相比于SIMD,SIMT提供了更高的灵活性,能够自动适应不同的执行宽度。
4.1. 线程模型
CUDA程序的线程模型由线程块层和线程层两个并行逻辑层组成。在程序执行前,数据需要通过特定接口传输到设备端内存,随后通过核函数启动大量GPU线程并行处理数据。在执行过程中,线程块被映射到流式多处理器,线程被映射到流处理器。不同的线程块可以被映射到同一流式多处理器上并行处理,而每个线程块又会被划分为多个线程束,每个线程束由线程块中连续的32个线程组成。线程块是GPU上最小的并行执行单元,包含多个线程,一个线程块内的所有线程可以在一片共享内存区域内进行数据通信,降低数据访问的延迟。网格是由多个线程块组成的,用来协调这些线程块的执行,可以用来处理更大的计算任务。每个线程块负责处理任务的一部分,从而实现高效的并行。核函数是在GPU上运行的CUDA函数,需要使用_global_进行声明。CUDA函数的并行执行逻辑包括粗粒度并行和细粒度并行两个层次。其中,粗粒度并行发生在网格内的线程块之间,即多个线程块可独立并行执行计算任务,提高整体并发性。而细粒度并行则发生在线程块内部的不同线程之间,各线程能够同时执行计算,从而进一步优化计算效率并提升性能。
4.2. 存储模型
CUDA的内存层次结构包含了多种内存类型,分别是全局内存、共享内存、常量内存和纹理内存。全局内存是GPU中最大的内存,但是访问速度慢,容易影响计算性能;共享内存是线程块内的高速缓存,虽然容量较小但是可以实现数据共享,访问速度快,大大提高计算性能。常量内存具有专门的缓存,在所有流式多处理器之间共享,主要用于存储不变的数据,当多个线程访问同一个常量值时,GPU会利用广播机制,一次读取就能满足所有线程的需求,避免冗余访问。纹理内存具有独立的缓存系统,支持二维、三维访问模式,能够有效提高数据局部性并减少内存延迟。
5. CUDA并行求解算法设计
在CUDA并行求解算法设计中,主要利用GPU强大的并行计算能力来加速LU分解过程,特别是在矩阵进行LU分解时的行列消元过程中利用GPU的并行计算能力,最后求出解向量x。我们将分解任务划分为多个线程块,每个线程块负责矩阵的一个小部分,充分利用GPU的并行架构和共享内存的数据共享和高速缓存,来降低内存访问的延迟和突破计算性能瓶颈。为了有效地解决大规模线性方程组,本文提出了一种基于CUDA的并行LU分解方法。在传统的LU分解中,每次行列计算都需要访问前面的计算结果,因此行列计算过程的数据依赖性非常强,将此任务并行化可以大大的减少计算时间。为了提高数值计算的稳定性,我们在LU分解中加入了部分选主元技术。在每一步消元之前,我们选择当前列中绝对值最大的元素作为主元,并交换两行元素,以防止计算过程中的数值不稳定问题。不可避免的,当加入部分选主元技术以后,由于寻找主元以及交换两行等操作,会增加通信开销。但是,LU分解在处理各种不同的矩阵,甚至是在处理接近奇异的矩阵时,部分选主元技术可以使得使用LU分解来分解这个矩阵时更加可靠稳定。
5.1. 并行策略
观察LU分解过程,采取迭代并行策略:由于LU分解对U的第r行和L的第r列进行分解时,该行的待分解元素所需数据只有该行主元及左侧所有元素以及待分解元素所在列的上方所有元素,该列的待分解元素所需数据只有该列主元上方所有元素以及待分解元素所在行的左侧所有元素,所以同行和同列的元素进行分解时并无数据依赖,由此可以并行分解U的第r行和L的第r列,即先并行分解U的第一行和L的第一列,再并行分解U的第二行和L的第二列,依次分解直到完成,过程如图1所示。
Figure 1. The process of LU decomposition
图1. LU分解过程
在计算时,首先将第一步对第一行和第一列并行求解,第二步再对剩余子块并行更新,更新后在继续对第二行和第二列并行求解,求解后再对剩余子块并行更新,直到整个矩阵分解完成后,利用前向回代和后向回代求出解向量。并行LU分解中,系数矩阵A的分解过程可以通过一个线程计算一个矩阵元素的方式来并行化。在CUDA中,我们使用二维网格和线程块的方法,每个线程块处理矩阵的一部分元素,线程块内的线程则负责计算该部分矩阵中的元素,利用共享内存来减少数据通信时间。这种方式利用了GPU的并行计算能力,将大规模的矩阵分解任务分解为多个小规模的任务,分别由不同的线程块和线程处理。每个线程块负责处理矩阵的一个子块,线程块内的线程则并行计算该子块中的元素,来实现高效的并行。而且二维网格的结构正好能够与矩阵的二维布局一一映射,使得数据访问更加高效,从而降低内存访问延迟。在本研究中,我们采用行循环划分(row-cyclic partitioning)策略,其中每个线程计算矩阵A中的一个元素,以最大化并行度。采用二维线程块(Block)和二维网格(Grid)进行任务分配,每个线程 (threadIdx.x, threadIdx.y)计算一个A[i] [j]。网格大小gridDim = (N/BlockSize, N/BlockSize),线程块大小blockDim = (BlockSize,BlockSize),使得每个BlockSize × BlockSize线程块负责矩阵的一个子块,提高GPU计算效率。为了减少全局内存访问,使用共享内存pivotRow存储主元行A [k],由threadIdx.y == 0线程加载,并使用共享内存pivotElement存储A [k] [k],由threadIdx.x == 0&&threadIdx.y == 0线程加载。在每轮k迭代中,所有线程首先加载pivotRow和pivotElement,然后执行__syncthreads()确保数据一致,再并行计算A [i] [j]更新,并在计算后再次同步,以保证数据正确性。
以8 × 8的矩阵为例,使用2 × 2的线程块,线程块按4 × 4大小划分整个矩阵,TB (x, y)代表线程块blockIdx.x = x,blockIdx.y = y,每个TB (x, y)负责计算4 × 4的矩阵子块。每个4 × 4的线程块由Block (4, 4)组成,每个线程(threadIdx.x, threadIdx.y)计算A [i] [j],以第一个线程块TB (0, 0)中的线程展开,第一步对矩阵第一行和第一列进行并行分解,第二步对剩余的矩阵进行并行更新元素,接着再对矩阵第二行第二列并行分解,再对剩余矩阵进行更新,一直到整个矩阵分解完成,如图2所示。
Figure 2. Thread block and thread partitioning
图2. 线程块和线程划分
5.2. 线程块和网格配置
在进行LU分解时,矩阵的每一行和每一列需要进行交替的计算。我们根据矩阵规模的大小和GPU硬件参数来动态调整线程块尺寸,以此来利用GPU的并行性和计算资源利用率。线程块内的每个线程可以通过共享内存和线程同步机制来工作,而网格中的线程块则独立工作,共同完成任务。对于一个
的矩阵,可以使用一个
的线程块配置,这样每个线程块包含
个线程,适合大多数中等规模大小的矩阵。如果矩阵规模较大(
时),系统自动切换至
粗粒度的分块方式,单个线程块包含了1024个线程,高效利用GPU的计算能力,如果矩阵规模较小(
时),系统则选用
的精细分块策略,通过共享内存实现数据共享,减少全局内存访问频次,即随着矩阵规模大小的变化,线程块的大小可以进行相应的调整来确保高效的利用计算资源,提高计算效率。
5.3. 通信优化策略
在CUDA并行LU分解中,通信开销主要来源于GPU全局内存访问和CPU与GPU之间的数据传输。为了降低通信开销,本文采取了以下优化措施:由于全局内存访问延迟较高,通常需要数百个时钟周期才能完成一次读取或写入,而共享内存(Shared Memory)访问延迟仅需几十个时钟周期。因此,直接从全局内存读取数据进行计算会导致大量的延迟,降低并行计算的效率。在LU分解过程中,每一轮迭代需要用到当前主元行(PivotRow)和主元列(PivotColumn)。如果所有线程直接从全局内存读取这些数据,访问延迟较高。因此,我们可以使用共享内存缓存主元行和主元列,让同一个线程块内的所有线程共享这些数据,从而减少全局内存的访问次数。采用一个线程块中的部分线程负责将主元行、主元列加载到共享内存,__shared__ float pivotRow [N];__shared__ float pivotCol [N];加载完成后同步(__syncthreads()),然后其他线程直接从共享内存读取数据进行计算,从而避免多次重复访问全局内存。这样,每个线程块内部的线程可以直接从共享内存中获取数据,而不是反复访问全局内存,从而大幅降低通信开销。
6. 实验结果与分析
6.1. 实验平台
为了验证基于CUDA的并行LU分解算法的有效性,本文在实验环境GPU:NVIDIA GeForce RTX 4060,显存:8 GB GDDR6,计算能力:8.0,最高处理频率:2.4 Ghz,CPU:AMD Ryzen9 7945HX,主频为2.5 Ghz,内存:64 GB,系统环境:Windows 10,IDE:Visual Studio 2019,CUDA Toolkit:CUDA 11.2上进行了实验,对比了并行算法与CPU单线程串行算法的性能差异,并分析了不同矩阵规模大小对加速效果的影响。
6.2. 实验结果与分析
加速比是衡量并行计算效率的核心指标,它的定义为:
,其中
是串行算法的执行时间,
是并行算法的执行时间包括了通信时间。实验选取了随机稠密矩阵作为测试数据,矩阵规模从
到
,统计了LU分解求解线性方程组的计算时间,单位为毫秒以及计算加速比。实验中的串行LU求解耗时与并行LU求解耗时所得数据均为随机稠密矩阵规模大小不变的情况下,随机运行10次所得数据的平均值。实验数据对比如表1、表2所示。
Table 1. Serial vs. parallel execution time comparison (CUDA)
表1. 串行与并行的求解时间对比(CUDA)
系数矩阵大小 |
串行LU求解耗时(ms) |
CUDA并行(ms) |
加速比 |
64 × 64 |
1.37 |
2.40 |
0.57 |
128 × 128 |
11.03 |
8.23 |
1.34 |
256 × 256 |
87.04 |
30.35 |
2.86 |
512 × 512 |
700.55 |
116.79 |
5.99 |
1024 × 1024 |
5509.17 |
424.53 |
12.97 |
2048 × 2048 |
45376.68 |
1335.51 |
33.97 |
Table 2. Serial vs. parallel execution time comparison (OpenMP)
表2. 串行与并行的求解时间对比(OpenMP)
系数矩阵大小 |
串行LU求解耗时(ms) |
OpenMP并行 (ms) |
加速比 |
64 × 64 |
1.37 |
1.82 |
0.75 |
128 × 128 |
11.03 |
9.34 |
1.18 |
256 × 256 |
87.04 |
45.53 |
1.91 |
512 × 512 |
700.55 |
194.46 |
3.60 |
1024 × 1024 |
5509.17 |
1202.15 |
4.58 |
2048 × 2048 |
45376.68 |
8534.28 |
5.31 |
表1展示了在CUDA中,表2展示了在OpenMP中,不同规模大小下矩阵串行算法(CPU)与并行算法(GPU)的运行时间对比。实验结果表明,随着矩阵规模的增大,CUDA并行算法的加速效果明显优于OpenMP并行算法。例如,对于
的矩阵,串行算法耗时约45.3秒,CUDA并行算法仅需1.3秒,加速比约达到34倍,但是OpenMP并行算法需要7.5秒;对于较小规模的矩阵如
,CUDA并行算法加速比相对较低,约为3倍,但仍优于OpenMP并行算法。而当矩阵规模为
时,反而串行求解时间比CUDA并行求解时间短,此时加速比小于1,仅达到0.57,造成这个结果可能是线程同步、通信、负载均衡以及内存访问模式尚未达到标准,导致并行效率差,求解速度不如传统CPU计算。
7. 结论
本文通过设计基于CUDA的并行化LU分解算法,提出迭代并行与动态分块策略,优化GPU内存访问,实现LU分解的混合并行分解,在
规模以上的矩阵中,打破了传统串行算法的计算性能瓶颈。实验表明,在单GPU环境下,
的矩阵求解加速比达34倍,验证了算法在大规模问题中的高效求解能力,成功解决了大规模稠密线性方程组求解效率低下的问题。