2. 科学计算任务特征分析
分块与负载均衡的前提,是先看清任务本身“长什么样”。同样是科学计算,有的任务几乎全部时间花在浮点运算上,有的任务大部分时间在搬运数据;有的任务可以切成上千个小块并行跑,有的任务一旦切开就要付出高昂的通信代价。如果对这些特征判断失误,分块策略和均衡手段往往会南辕北辙。本节围绕四个维度展开:计算密集与数据密集的区分、任务粒度与并行度、依赖关系建模,以及负载不均衡的常见来源。
一、计算密集型与数据密集型:先看时间花在哪里
区分两类任务最直接的办法是做“时间账”:统计一个任务中浮点运算耗时与内存/IO 访问耗时的比例,再结合算术强度(每字节数据参与的浮点运算次数,FLOP/Byte)判断瓶颈位置。计算密集型任务的算术强度高,CPU 流水线和向量单元是主要资源,例如稠密矩阵乘法、有限元刚度矩阵组装、蒙特卡洛粒子输运;数据密集型任务的算术强度低,性能受限于内存带宽、缓存命中率或磁盘/网络吞吐,例如大规模稀疏矩阵向量乘、逐元素数组运算、海量观测数据的读写与筛选。
这一区分直接决定分块目标。对计算密集型任务,分块应尽量让每块的计算量均衡,并保证块内数据能驻留缓存、便于向量化;对数据密集型任务,分块的首要目标是提高数据局部性、减少跨节点搬运,块大小往往由缓存行、页大小或通信带宽决定,而不是由计算量决定。实践中可以用一个简单判据:若把数据量减半而计算量不变,运行时间几乎不变,则偏计算密集;若运行时间随数据量近似线性增长,则偏数据密集。
二、任务粒度与并行度:切多细才合适
粒度指单个任务块所包含的工作量,并行度指可同时执行的块数量。二者相互制约:块越小,并行度越高、负载越容易摊平,但调度开销、通信次数和边界处理成本随之上升;块越大,单块效率高、通信少,但可并行的块数减少,容易出现“块数少于核数”的欠并行。设总工作量为 W,单块开销为 c,通信与调度开销为 o,则并行执行时间近似为 W/p + o·p(p 为块数),存在一个使总时间最小的块数区间。
工程上常用的做法是先保证“块数不少于可用执行单元数的若干倍”(例如 4~8 倍),给动态调度留出摊平空间,再通过实测调整块大小。以 64 核集群上处理 108 个网格单元为例,若每块 105 个单元,则共 1000 块,约为核数的 15 倍,既能保持较高并行度,又不至于让单块小到被调度开销淹没。这个倍数不是定论,需结合单块实际耗时测量确定。
三、依赖关系建模:决定哪些块能真正并行
任务之间的依赖决定了并行的“天花板”。把每个任务块视为节点、把“块 A 的输出是块 B 的输入”视为有向边,就得到一张有向无环图(DAG)。图中最长路径即关键路径,它给出了在无限资源下的最短完成时间下界;任何分块方案都不可能突破这个下界。因此,分块时首先要识别关键路径上的串行段,把优化资源优先投向那里,而不是盲目增加并行块数。
依赖还分强弱。以时间步进仿真为例,第 n+1 步依赖第 n 步的结果,这是强依赖,只能通过区域分解把空间切成子域、在子域边界交换“幽灵层”数据来实现并行;而同一时间步内不同子域之间只有边界数据交换,属于弱依赖,可用非阻塞通信与计算重叠。再如参数扫描类任务,各参数组合之间完全独立,属于无依赖任务,可直接按组合切块并任意调度。建模时建议显式写出依赖边及其数据量,因为通信代价往往由边上的数据量而非节点计算量决定。
四、负载不均衡的常见来源
即使块数足够多,实际运行仍常出现“大部分单元早早空闲、少数单元长时间忙碌”的长尾现象。常见来源有四类:
- 算法异构性:同一程序内不同分支的计算量差异巨大,例如自适应网格中加密区域的单元计算量是粗网格的数倍,按单元数均分就会严重失衡。
- 数据倾斜:键值分布不均或稀疏结构不规则,某些块的非零元、粒子数或记录数远高于平均值,导致处理时间成倍增加。
- 硬件差异:集群中节点代际不同、主频与内存带宽不一致,或存在网络拥塞、共享缓存争用,使相同工作量的实际耗时不同。
- 调度与启动抖动:块启动时间不一、IO 等待、垃圾回收或系统噪声,造成小块任务的实际开销被放大。
识别不均衡的直接手段是采集每块的开始/结束时间戳,计算各执行单元的忙碌时长与空闲时长,画出类似上图中的耗时条形。若最大块耗时是最小块的数倍,且长尾块集中在特定数据区域或特定节点,就能定位到倾斜或硬件差异;若长尾随机出现,则更可能是调度抖动。针对算法异构性,可按“预估工作量”而非“元素个数”切分,例如用加密层级加权;针对数据倾斜,可采用动态任务队列让空闲单元主动领取下一块;针对硬件差异,可给慢节点分配更小的块或更少的块。判断均衡是否达标的实用指标是并行效率:E = T1 / (p·Tp),若 E 明显低于 0.7,就应回到上述四个维度逐一排查,而不是继续加大块数。