多 GPU 域分解:halo 边界交换、1D/2D 切分与通信计算重叠
更新时间:2026-09-25。上一篇 多 GPU 互连:NVLink、NVSwitch 与节点间网络 解决了「卡与卡之间链路多快」的问题,但链路再宽,程序不按它的脾气切分数据也跑不满。高性能计算(HPC,High Performance Computing)领域用了三十年的答案是域分解(domain decomposition):把计算网格或矩阵像切蛋糕一样按卡切块,每张卡只拥有并计算自己那一块,块与块之间每步交换薄薄一层边界。本篇讲清这套范式的全部关键量——halo 有多厚、条切和块切的通信面差多少、弱扩展与强扩展的数字为何分道扬镳、以及怎样用 CUDA stream 把通信藏进计算里。它和数据并行训练共享同一套集合通信底座,可对照 训练集合通信:allreduce 与 all-gather;训练侧的另一种切法见 数据、张量与流水线并行。
本文要回答的问题
- 多 GPU 域分解的基本规则是什么?「owner computes」(属主计算)和 halo(幽灵单元格)分别指什么,halo 厚度由谁决定?
- 1D 条切和 2D 块切在边界单元格数量、邻居数、扩展性上差多少?为什么说切分维度直接决定通信面随卡数增长的指数?
- 通信/计算比怎么估算?弱扩展(weak scaling)和强扩展(strong scaling)的效率曲线为何走向相反,典型规模下加速比是多少?
- 怎样用 CUDA stream、异步发送和 neighbor exchange(邻居交换)把 halo 交换与内部计算重叠,实际能藏掉多少通信?
- 残差、总质量这类全局量为什么要收敛到 allreduce?它每步的代价和点对点交换相比如何?
一、切块、属主计算与幽灵单元格
域分解的规则只有三条。第一,把离散后的计算域(二维网格、三维网格或分块矩阵)划成 P 个互不重叠的子域,P 等于 GPU 数。第二,owner computes 原则:每个单元格、每个矩阵元素有唯一属主,只有属主卡写它,任何卡都不重复计算别人的部分——这保证了并行工作量恰好等于串行工作量,没有算法性冗余。第三,当一个单元格的更新依赖邻居时(流体求解、热传导、波动方程的 stencil(模板)计算都如此),属主卡必须先从邻居卡拿到边界外侧的数据副本。
这份副本就是 halo,也叫幽灵单元格(ghost cell):它在本卡显存里有存储、被本卡读,但属主是邻居卡,每一步(或每若干步)必须刷新。halo 厚度 w 等于 stencil 的作用半径:五点差分(中心加上下左右)只需 1 层,四阶 WENO 格式要 3 层,多层时间子步进可能临时扩到 4 层以上。halo 越厚,每步交换的数据量线性增大,这是高精度格式在多卡上的隐性税。
矩阵问题的同构说法顺带钉死:稀疏矩阵向量乘(SpMV)和迭代求解器里,按行块把矩阵分到各卡,每卡拥有连续的若干行和对应行的解向量分量;本行的非零元一旦指到其他卡持有的列,那些 x 分量就是 halo,先交换再做乘加。所以「切块 + halo」不依赖网格形态,它就是「按数据属主分发并行、按依赖关系补副本」这一通用规则在空间问题上的具象。
把交换量写成通用公式便于估算。d 维规则网格上,子域各向边长为 h₁…h_d,则每步每卡的 halo 交换量约为 V = 2w·Σ_i(∏_{j≠i} h_j)个单元格:每个切分面有正反两侧,面面积等于其余维度边长之积。三维 N³ 网格对号入座——slab 切分约 2N²w,pencil 约 4N(N/√P)w,立方块约 6(N/P^(1/3))²w,再乘每单元格字节数(FP64 为 8)即通信字节。若用更宽的 halo 换多步自治、每 k 步才交换一次,每步摊销交换量除以 k,这是以显存换通信的典型权衡。图与粒子数据没有规则面:图按点集切块后,halo 就是割边(edge cut)两端的远端顶点,交换量等于割边数,这正是图分割器死命压低割边的原因;分子动力学、SPH 一类粒子模拟按空间单元切块,粒子越过子域边界就连同邻接信息迁移到新属主,halo 变成每步动态变化的粒子包。
一个时间步因此分成固定的三段:打包本卡边界层发给各邻居 → 等待邻居的 halo 到达 → 在完整的「自有区域 + halo」上推进一个 stencil 步。第一段和第三段是计算与点对点通信,没有任何全局同步;真正的全局动作只在需要残差、通量积分等全局标量时才出现(见第六节)。下图画出两个相邻子域之间的 halo 关系。
二、1D 条切与 2D 块切:通信面是切法决定的
以二维 N×N 网格、P 张卡、halo 厚度 w 为例,这是整个域分解里最值得算账的一节。
1D 条切(沿一个方向切成竖条):每卡分得 N × N/P 的长条,只有左右两个邻居。单步交换的边界单元格为 2Nw(左右各一条,每条 N 行 w 列),本卡自有单元格 N²/P,通信/计算的单元格之比约为 2wP/N——随卡数 P 线性增长。
2D 块切(纵横各切,排成 √P × √P 的卡阵):每卡分得 (N/√P)² 的方块,最多有上下左右四个邻居。交换量为 4(N/√P)w,与自有量之比约为 4w√P/N——只随 P 的平方根增长。卡数从 64 涨到 4096(64 倍)时,条切的通信面涨 64 倍,块切只涨 8 倍。三维问题同理可推:按 slab(一维切)通信面 ∝ P,按 pencil(二维切)∝ P^(1/2),按立方体(三维切)∝ P^(1/3)。这就是大规模 CFD/气候模拟几乎一律用多维分解的原因,也是 MPI rank 排布成笛卡尔拓扑(MPI_Dims_create、MPI_Cart_create)的理由。
| 切分方式(2D 网格,P 卡) | 每卡子域形状 | 邻居数 | 单步 halo 单元格 | 通信/计算比 | 扩展性上限 |
|---|---|---|---|---|---|
| 1D 条切 | N × N/P 长条 | 2 | 2Nw | 2wP/N(∝ P) | 数百卡后通信面失控 |
| 2D 块切 | (N/√P)² 方块 | 4 | 4(N/√P)w | 4w√P/N(∝ √P) | 数千卡仍可控,主流选择 |
| 非均衡手工切 | 形状不规则 | 不定 | 不定,常含长尾 | 取决于最慢卡 | 负载不均即退化 |
用具体数字收个口。N = 16384、w = 1、FP64(每数 8 字节)、P = 64:条切每步交换 2×16384 个双精度数,约 256 KB;块切交换 4×2048 个,约 64 KB。单看数据量都不大,真正咬人的是后面要讲的消息条数、延迟与强扩展下的相对占比。
三、通信/计算比:弱扩展与强扩展的分叉
把一个时间步的时间写成 T = t_comp + t_comm,其中点对点消息的经典成本模型是 t_msg = α + n/β:α 是延迟(启动开销,含 kernel 发起、NVLink/网络握手),n/β 是 n 字节除以有效带宽。块切的另一个好处在这里显现:四个邻居的四条消息可以全部同时在飞,延迟 α 只付一次(并行曝光),条切也只有两条,但数据量更大。
两种扩展回答不同的问题。强扩展固定总问题规模、只加卡,追求更快出结果:每卡工作量按 1/P 缩小,而通信面按 P 的分数幂缩小,通信/计算比必然恶化,最终撞到「halo 比内部还多」的墙。弱扩展固定每卡工作量、问题规模随 P 同比增大,追求算更大的题:多维块切下每卡的边界面积几乎不变,通信/计算比恒定,这是 HPC 能扩展到上万卡的根本路径。
典型规模上的常见实测区间(2D/3D stencil,NVLink 内加 IB 跨节点,高阶格式取 w=2~3):
| 场景 | 64 卡 | 512 卡 | 1024 卡 |
|---|---|---|---|
| 弱扩展并行效率(每卡网格固定,如 3D 512³/卡) | 90% ~ 94% | 84% ~ 88% | 78% ~ 83% |
| 强扩展并行效率(固定 3D 2048³ 网格) | 85% ~ 90% | 40% ~ 55% | 20% ~ 30% |
| 强扩展实际加速比(对照 1 卡) | ~55 倍 | ~240 倍 | ~260 倍(已近饱和) |
强扩展那一行在 512 卡后坍塌,正是第三维块切也压不住 P^(1/3)的信号:此时每卡每步算了了几个内部层、却要交换六面 halo,α 开销也因消息数固定但计算变短而相对放大。工程对策是接受「强扩展有最优卡数」,超配的算力转去做弱扩展(多算工况、集成时间更长)或同时并行多个算例。
两条定律给这对曲线提供了理论口径。Amdahl 定律固定问题规模:S(P) = 1/(s + (1−s)/P),s 是不随 P 缩减的串行比例(通信、同步、 halo 交换都算在内),P→∞ 时加速比封顶在 1/s——强扩展撞的就是这堵墙;而且每卡计算缩小后,原本不起眼的通信被「推」进 s,墙还会越变越厚。Gustafson 定律换了个问法:既然有 P 张卡,就愿意算 P 倍大的问题,S(P) = P − s(P−1),串行部分随规模同比摊薄,加速比近似线性——弱扩展成立,是因为扩大的子域把边界通信稀释回固定比例。Amdahl 讲「同一道题分多人做」,省时间;Gustafson 讲「人多就做更大的题」,保吞吐。实操上先测出强扩展效率跌破 80% 的拐点卡数,拐点以内追速度,超出的算力全部按弱扩展投入。
四、用 CUDA stream 把 halo 交换藏进内部计算
通信/计算比再难看,只要交换能和计算同时发生,墙上时间里就可以不认这笔账。标准做法分四步,全部异步:
- 在通信 stream 上先投递接收(CUDA-aware MPI 的
MPI_Irecv,或 NCCL 的 ncclRecv),再用一个轻量 pack kernel 把本卡各边界层连续打包到 bounce buffer,发起MPI_Isend;GPU 间同节点走 NVLink P2P(cudaDeviceEnablePeerAccess),不经主机内存绕行,硬件细节见 多 GPU 互连:NVLink、NVSwitch。 - 在计算 stream 上立刻启动内部区域 kernel:只更新距边界 w 层以远的单元格,这部分不依赖任何 halo,通常占整块 70% ~ 90% 的工作量。
- 内部 kernel 跑完前后,在通信 stream 上
MPI_Wait确认 halo 到齐(用 CUDA event 做跨 stream 依赖)。 - 启动边界条带 kernel,更新剩余单元格,进入下一步。
时间线如下图:通信延迟被完全包进内部计算的窗口里,只有当通信比内部计算还长时才会露出一段等待。
NCCL 在这套编排中的位置需要单独说明。域分解的邻居收发用 ncclSend/ncclRecv 而非集合原语:同一步投递给多个邻居的调用会被 NCCL 纳入它的 channel 与 ring 调度,按链路拓扑选路——节点内优先 NVLink,跨节点走 IB 上的 GPUDirect RDMA。编排上守住两条:每步固定复用同一条通信 stream 和同一个 NCCL communicator,让内部 ring 与注册缓冲持续复用、不反复建链;残差 allreduce 不必堵在步边界,可以和下一步的内部 kernel 并发投递,真正要判收敛时(往往已是若干步后)再取结果,这等于把集合通信也藏进「内部计算」窗口。
工程上有三个高频坑。其一,先等 halo 再开算的朴素写法让 GPU 在每个 α 周期里空转,多卡越多亏得越明显;重叠改完,强扩展效率有时能抬升 15 ~ 20 个百分点。其二,pack 步骤不可省却常被遗忘:边界层在二维子域里内存不连续,直接发送会把 strides 信息塞进协议或发出大量碎消息;一次 pack kernel 的成本远小于多发的消息。其三,寄存器与共享内存占用别让两个 kernel 真并发时互相挤占 SM——多数实现让内部 kernel 占满算力、通信只走 DMA 引擎与 NVLink,二者天然不争计算资源,这正是重叠几乎免费的物理前提。
五、邻居交换之外:负载均衡与通信域
真实网格很少像代数例子那样方方正正。非结构网格(有限元、气动外形绕流)按空间用图分割器(METIS、ParMETIS)切块,优化目标是各块单元格数相等且切面边数最少;此时每卡邻居数可能有十几个,halo 散落在不规则的节点列表上,打包/查表成本上升,但「属主计算 + 切面交换」的框架不变。还要预留负载再平衡机制:自适应加密(AMR)让某些卡凭空长出大量单元格,隔若干步重切一次胜过全程拖后腿。
负载不均的直接后果是 straggler(掉队者):邻居交换的等待和周期性 allreduce 让最快的卡空等最慢的卡,整步时间由尾部决定。AMR 局部加密、激波扫过细化区、粒子在引力下聚团,都能让部分卡的工作量数倍于均值。应对按代价从低到高有三档:每若干步统计各卡单元格数,用递归坐标对分(RCB)、Hilbert 空间填充曲线一类几何剖分器低成本重切,保持子域紧致;再重一些用 ParMETIS、Zoltan 做带迁移权重的图再平衡,以上一轮划分为热启动,只迁移割口附近的少量单元格;细粒度不规则任务退到 work stealing——卡做完本地工作后按邻居优先级拉取任务,但任务粒度必须大到足以摊薄拉取的延迟。
切块形状还要顺着硬件拓扑排。节点内 8 张卡由 NVLink/NVSwitch 全互连,跨节点只有 IB(常见 200~400 Gbps,带宽约为 NVLink 的四分之一到八分之一),所以应让交换量大的邻居落在节点内:排布笛卡尔拓扑时先取节点大小这个因子,使每个子域跨节点的切面尽量不超过两面,三维问题常用「节点内两维、节点间一维」的维度分配;没有 NVSwitch 的 PCIe 树机器,还要把邻居对钉在同一 PCIe switch 下。子域形状本身不变,只重排 rank 到物理 GPU 的映射,跨节点流量常能再降三到五成。
通信域也要收窄。跑在多租户集群上时,用 MPI communicator 或 NCCL communicator 把全局通信限制在本任务的卡集合内;GPU 与网卡的亲和(GPU-NIC affinity)要对齐,跨 PCIe switch 绕行会让有效带宽腰斩。这些是系统层细节,但同一份域分解代码,拓扑摆得对不对,弱扩展效率可以差出十个百分点。
六、全局量收敛到 allreduce
每个 stencil 步是纯局部的,但迭代求解器需要全局标量判断收敛:残差范数 ‖b−Ax‖、总质量、升阻力积分、时间步长的全局 CFL 上限。做法是每卡先在自己子域(含边界,按不重不漏的属主口径)算出局部部分和,再做一次跨全部 P 卡的 allreduce:所有卡同时得到同一个全局值,再各自比较收敛阈值。
allreduce 的成本结构和邻居交换完全不同:它是全局集合通信,ring 算法的通信量约 2(P−1)/P·n(n 是标量或小向量,这里通常只有几个到几十个双精度数),延迟按 2(P−1) 跳计。因此它的数据量可以忽略,成本主要是延迟与全局同步——所有卡必须在此碰头,最慢的卡拖住全体。实践中三条准则:能每几步算一次残差就不要每步算;多个全局量(残差、各物理量通量)打包成一次 allreduce;检查完收敛后立刻释放,不要在同步点附近再放无关计算。allreduce 的 ring/tree 算法细节及带宽利用推导见 训练集合通信:allreduce 与 all-gather。
值得点破的是两种多卡范式的同构性:HPC 域分解是「空间切分 + halo 邻居交换 + 偶发 allreduce」,大模型的数据并行是「batch 切分 + 每步梯度 allreduce」。前者通信以点对点邻居为主、集合通信为辅;后者每步都是全局归约。理解了通信面随切分指数增长这条主线,两套系统的优化语言(块切、重叠、限制同步范围)就是相通的,训练侧更激进的切法(张量并行、流水线并行)可对照 数据、张量与流水线并行。
这一篇在知识体系里的位置
- 硬件底座:多 GPU 互连:NVLink、NVSwitch 与节点间网络——halo 交换实际跑在的 P2P 链路与跨节点网络;
- 集合通信:训练集合通信:allreduce 与 all-gather——全局量归约的 ring/tree 实现与带宽账;
- 并行范式对照:数据、张量与流水线并行——训练侧按 batch、层内、层间三种切法与本篇的空间切分互为镜像;
- 单卡性能上限:Roofline 模型:用算术强度定位 GPU 性能瓶颈——多卡扩展之前,单卡 kernel 先要不趴在带宽墙下。
总结
多 GPU 域分解是 HPC 多卡并行的基本范式:网格按卡切块、属主唯一计算,stencil 每步先交换 halo(幽灵单元格,厚度等于模板半径)再推进;通信面由切分维度决定——2D 网格 1D 条切的通信/计算比为 2wP/N、随卡数线性增长,2D 块切为 4w√P/N、只随平方根增长,三维类推(P、P^(1/2)、P^(1/3)),所以大规模题一律多维块切;弱扩展固定每卡工作量、通信面恒定,典型效率 64 卡约 92%、1024 卡仍有八成,强扩展固定总规模时 halos 相对膨胀,512 卡后效率跌到四五成;工程上用 CUDA stream 先发起异步邻居交换、立刻计算不依赖 halo 的内部区域、再汇合算边界条带,可把通信几乎完全藏进计算窗口;残差等全局标量靠一次延迟主导的 allreduce 收敛,应少做、打包做、同步点附近不放杂活——点对点邻居交换为主、全局归约为辅,正是它与数据并行训练每步全归约的根本区别。