1. 课程导入与多核并行科学计算概览

科学计算的核心矛盾从未改变:我们希望在可接受的时间内求解更大规模、更高精度的问题。过去四十年,这一目标主要靠处理器主频提升来实现,程序无需改写就能自动变快。但当主频在 2005 年前后逼近功耗与散热墙,单核性能增长显著放缓,硬件厂商转向在单块芯片上集成多个核心。这意味着性能红利不再"免费":程序必须显式地划分任务、协调数据、管理同步,才能用上多出来的计算资源。本课程讨论的,正是如何为多核平台设计高效、正确、可扩展的科学计算算法。

学习目标

完成本课程后,你应当具备三方面能力。第一是分析能力:面对一个数值问题,能判断其并行性来自哪里——是数据可分(如网格逐点更新)、任务可分(如多个独立参数扫描),还是两者兼有;能估算并行加速比上界,识别串行瓶颈与通信开销。第二是实现能力:能用 OpenMP 共享内存模型、MPI 消息传递模型以及二者的混合模式,写出正确的并行代码,并处理数据竞争、伪共享、负载不均等典型问题。第三是评估能力:会用强扩展与弱扩展两把尺子衡量程序,能读懂性能剖析结果,定位是计算、访存还是同步拖了后腿。

多核处理器架构演进

理解硬件是设计算法的前提。现代多核处理器有几个关键特征值得记住。其一是缓存层次与非一致访存:每个核心有私有 L1/L2,多个核心共享 L3,而多路服务器上访问本地内存与远端内存的延迟可相差近一倍(NUMA 效应)。其二是SIMD 向量单元:一个核心每周期可对 4 个双精度数或 8 个单精度数做同一条运算,这要求数据在内存中连续且循环可向量化。其三是超线程与多核并存:逻辑核数多于物理核数,对访存密集的代码有一定掩盖延迟的作用,但对计算密集代码提升有限。其四是内存带宽成为共享资源:核心越多,每个核心分到的带宽越少,"算得快但喂不饱"是常见困境。

这些特征直接决定了算法设计取向。例如,一个在单核上表现优异的稠密矩阵乘法,若不做分块,多核并行后会迅速撞上带宽墙;而一个逐点更新的模板计算(stencil),若不考虑缓存行对齐,相邻线程写同一缓存行的不同元素会引发伪共享,性能可能不升反降。

多核平台上的并行科学计算:从问题到性能判据 并行性来源 数据并行:网格逐点更新 任务并行:参数扫描/多物理场 流水并行:时间步/阶段链 混合:区域分解 + 线程 先识别,再决定分解策略 硬件约束 私有 L1/L2 + 共享 L3 NUMA 远端访存延迟高 SIMD 宽度 4/8 双精度 共享内存带宽有限 约束决定算法取舍 编程模型 OpenMP:共享内存线程 MPI:分布式消息传递 混合 MPI + OpenMP 向量化与分块优化 模型匹配问题结构 性能判据与常见瓶颈 强扩展:固定问题规模,增加核数 → 加速比 S(p)=T₁/Tₚ,受 Amdahl 定律限制 弱扩展:问题规模随核数同比例增长 → 考察每核效率是否维持,受 Gustafson 视角启发 典型瓶颈 数据竞争 伪共享 负载不均 同步开销 带宽饱和 串行段占比 NUMA 远端访问 向量化失败 逐项排查, 对症下药 课程知识地图(主线) 并行基础 模型·指标·定律 共享内存 OpenMP·同步 分布式 MPI·通信 数值算法 矩阵·迭代·FFT 性能工程 剖析·调优 实战项目 端到端并行求解

应用场景与挑战

并行科学计算支撑着大量真实工作负载。计算流体力学用有限体积法在亿级网格上求解 Navier–Stokes 方程,每个时间步都要遍历全部单元;分子动力学对数十万原子做短程力计算,天然适合区域分解;计算化学中的稠密对角化与矩阵乘法是 BLAS 的主战场;地震成像、天气预报、结构有限元、机器学习中的大规模线性代数,也都依赖多核与集群。它们的共同点是:单次迭代计算量大、数据局部性强、存在明确的规则或不规则通信模式。

挑战同样鲜明。第一是正确性:并行程序的错误往往是非确定性的,一次运行通过不代表正确,浮点归约顺序改变也会带来结果差异,需要区分"可接受的数值差异"与"真正的逻辑错误"。第二是可扩展性:Amdahl 定律告诉我们,若程序有 5% 的串行部分,即使无限多核,加速比也不超过 20 倍。第三是数据局部性:并行化后每个核心的数据工作集变小,但总带宽不变,分块与数据布局往往比线程数更影响性能。第四是负载均衡:自适应网格、粒子模拟、稀疏矩阵的非零分布都不均匀,静态划分常导致部分核心空转。

一个可动手的起点示例

为了让概念落地,看一个最简单的例子:用梯形法计算 ∫₀¹ 4/(1+x²)dx,其真值为 π。串行版本把区间分成 N 份累加。并行时,最直接的做法是让每个线程负责一段连续子区间,各自求部分和,最后归约。下面给出 OpenMP 版本的核心片段(示例参数 N=10⁸,仅作教学演示,实际数值随机器与编译器而异):

double f(double x){ return 4.0/(1.0+x*x); }

double trap_parallel(long N){
    double h = 1.0 / N, sum = 0.0;
    #pragma omp parallel for reduction(+:sum) schedule(static)
    for (long i = 0; i < N; ++i) {
        double x = (i + 0.5) * h;   // 中点法,避免端点重复计数
        sum += f(x);
    }
    return sum * h;
}

这段代码值得注意的细节有三处。reduction(+:sum) 让每个线程在私有副本上累加,结束时再合并,避免了写共享变量的数据竞争;但归约顺序不确定,因此结果与串行版本可能有末位差异,这属于正常现象。schedule(static) 把迭代块静态分给各线程,适合每步计算量均匀的情形;若函数求值代价差异大,应改用 dynamic 或 guided。中点法取 (i+0.5)h 而非端点,既保证精度又让每个 i 的计算完全独立。运行后可用 OMP_NUM_THREADS=4 与单线程对比耗时,计算加速比 S=T₁/T₄,并观察它是否接近 4——若明显偏低,就要怀疑归约开销、内存带宽或线程创建成本。

判断并行程序是否"值得"的一个实用习惯:先测串行版本,再测 1 线程并行版本。若 1 线程并行版已明显慢于串行版,说明并行框架本身的开销或代码结构(如循环不可向量化)存在问题,此时增加核数只会放大问题。

课程知识地图与实战项目

本课程按"基础—模型—算法—性能—实战"推进。前半程建立并行性分析、加速比与扩展性度量、访存层次与向量化的共同语言;中段分别深入 OpenMP 共享内存编程(线程创建、同步原语、调度策略、伪共享规避)与 MPI 分布式编程(点对点、集合通信、区域分解、通信与计算重叠);随后进入数值算法层面,讨论稠密矩阵乘法分块、稀疏矩阵向量乘的负载均衡、迭代求解器的并行预条件、快速傅里叶变换的并行分解;最后落到性能工程与综合实战。

贯穿课程的实战项目是:并行求解二维泊松方程。给定单位正方形上的 Dirichlet 边值问题,用五点差分格式离散为稀疏线性系统,采用 Jacobi 或共轭梯度迭代求解。项目分阶段交付:先用 OpenMP 实现共享内存版本并测量强/弱扩展;再用 MPI 做一维或二维区域分解,加入 halo 交换;最后尝试混合模式并做性能剖析,提交一份包含加速比曲线、瓶颈分析与优化前后对比的报告。这个项目规模可控,却覆盖了分解、通信、同步、负载均衡、性能评估的完整链条,是理解本课程全部要点的最佳载体。

开始之前,建议准备好一台多核机器(4 核以上即可)、支持 OpenMP 的编译器(如 gcc 加 -fopenmp)以及 MPI 实现(如 OpenMPI)。不必等到"学完再做",从第一个 OpenMP 循环开始就动手测量,让每一次性能数字的变化成为你理解并行原理的最直接反馈。