数值积分和微分方程这两类问题,在计算科学里属于“天天见”的硬骨头。我这些年不管是做流体仿真、电磁场计算,还是帮别人优化算法,最后都会撞到同一个问题上:CPU单核跑得太慢,多核又受限于内存带宽,等一个大规模参数扫描跑完,人已经不想再看结果了。后来我把一批数值积分和微分方程的求解任务迁移到GPU上,同样规模的算题时间从“按天算”变成“按小时甚至按分钟算”,这个体验确实让人回不去。这篇文章就围绕“怎么用GPU把数值积分和微分方程真正跑起来”展开,讲原理、讲代码、讲坑,也讲讲哪些场景其实不适合硬上GPU。
这篇文章适合这么几类人看:正在做科学计算、数值仿真,被性能逼到想换方案的研究生和工程师;搞机器学习但需要自己写数值求解器、又对CUDA不熟的人;还有纯粹想了解GPU除了训练神经网络还能干嘛的爱好者。默认你有基础的C/Python能力,不要求你写过CUDA,我会把关键代码和思路都拆开讲。
1. 为什么数值积分和微分方程天然适合GPU
1.1 并行性到底藏在哪
很多人一提到GPU,第一反应是“那是用来训练神经网络的”。这话对,但只对了一半。GPU擅长的是大规模的规则并行计算,而数值积分和一大类微分方程求解,恰恰是规则并行的典型代表。
先说数值积分。最简单的矩形法或者梯形法,本质上就是把积分区间切成N小段,每一小段单独算个近似面积,最后全部加起来。这个“单独算”的过程,每一段之间互不依赖,天然就是可并行的。你只需要把N分得足够大,然后把每一段丢给不同的计算单元,最后做一次归约求和。这个归约操作在GPU上是家常便饭。
再说微分方程。很多人第一反应是微分方程的时间推进是串行的——必须算出第k步,才能算第k+1步,这个认识没错。但注意,串行的是时间步,不是空间自由度。考虑一个N维常微分方程组(比如分子动力学里的几万个粒子、电路仿真里的节点电压),在每一个时间步内,所有自由度的更新公式往往是一样的,而且各自由度之间的耦合只在求导阶段发生。这时候并行度就藏在“维度”里。
对偏微分方程来说,显式格式下的并行性更明显。比如一维热传导方程,用显式前向欧拉格式,某一格点下一时刻的值,只依赖于当前时刻这个点以及左右邻居的值。这种stencil(模板)计算模式,让每个网格点都成为一个独立的任务,GPU天然适配。
1.2 什么样的场景才值得搬上GPU
虽然上面说了很多并行性,但并不是所有数值问题都值得上GPU。我见过不少同学把2×2的矩阵求逆也拿到GPU上去“加速”,结果除了把显存占上、把代码变复杂,什么都没得到。
值得上GPU的题型有这几种特征:
- 问题规模足够大。至少要几千个独立任务以上。积分区间切几万段、网格点数上百万、微分方程组维度成百上千,这些都行。
- 计算模式是“单指令多数据”。每个数据单元执行同样的计算公式,只是数据不同。数值积分就是典型,有限差分也是一个套路。
- 数据局部性可控。计算过程中,每个线程主要访问自己的那部分数据,偶尔从邻居读点东西,不需要到处乱跳。
- 长时间扫参或蒙特卡洛需要大量重复计算。比如给随机微分方程做路径采样,几千条路径彼此独立,每条路径内部的求解还是那套公式,这种题型GPU收益极大。
反过来,那些强串行、规模小、或者每步都要做稀疏矩阵分解的问题,比如经典的隐式求解带刚性约束的大规模稀疏系统,GPU并不是首选。这类问题主要瓶颈是通信和稀疏访存,GPU再快也会被内存访问拖死。后面第3节我会专门说说这个坑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数值积分的GPU实现:从核函数到归约
2.1 选哪种数值积分方法
数值积分的方法选型,直接决定并行版本的写法。我在工程里最常用的就是三种:梯形法、Simpson法、Gauss-Legendre求积。
梯形法和Simpson法属于等距采样方法,实现最简单。把所有采样点均匀铺开,每个线程负责几个点,最后归约。这类方法收敛阶数固定,梯形法是二阶,Simpson法是四阶,要求被积函数不能太变态,如果有间断或者奇异点,等距采样会浪费大量点。
Gauss-Legendre求积是更“硬核”的选择,同样数量的采样点,代数精度高得多。但它的采样点不均匀,靠近积分区间两端更密,做并行划分时不太均匀,且权重需要查表。我的建议是:练手和通用场景先用梯形法或Simpson法跑通流程,跑顺了再换Gauss-Legendre提升精度。
还有一个方向是自适应积分。哪种呢?就是在误差大的区域细分、误差小的区域粗分,这样能大幅减少采样点。但注意,自适应细分在GPU上并不好写,因为它需要不断的动态划分和任务分配,动态负载均衡在GPU上会增加不少开销。我一般不把自适应积分直接写在kernel里,而是分成两步:先在CPU上做递归细分得到分段区间,再把每个细分区间丢给GPU做并行积分。这样既保留了自适应的精度优势,又没牺牲GPU的吞吐。
2.2 一个可以直接改用的梯形积分CUDA核函数
下面这个是我经常用来跑通流程的示例:用梯形法计算
[
I = \int_0^\pi f(x),dx,\quad f(x) = \sin x \cdot e^{-0.05x}
]
关键点在于:核函数里不直接传全部采样点数组,而是每个线程按自己的编号和步长,用stride循环连续处理多个采样点,这样能保证合并访存,效率更高。归约部分用共享内存数组完成块内求和,最后每个block只把部分和写回全局数组,再由CPU做最后的加总。
cuda复制#include <stdio.h>
#include <math.h>
__device__ double my_func(double x) {
// 被积函数,实际工程中可能是某个复杂的物理模型
return sin(x) * exp(-0.05 * x);
}
// 梯形法积分核函数
// n:采样点数量,dx:步长,x_start:积分下限,partial:每个block的部分和
__global__ void trapezoid_kernel(double x_start, double dx,
