ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

并行计算:突破物理墙,从多核到GPU的演进之路

并行计算:突破物理墙,从多核到GPU的演进之路 1. “频率墙”和“功耗墙”并行计算是怎么被现实逼出来的任何学过并行计算的人第一课通常不是代码而是物理。我从2008年开始接触高性能计算当时导师第一句话就是“你们有没有想过CPU为什么不再往上提高主频了”这个问题放到今天依然很有分量而且比当年更紧迫。看近二十年的数据非常直观2004年前后主流CPU主频已经冲到3.8GHz左右实验室里甚至能看到4GHz以上的样品而到了现在大多数服务器CPU的主频依然稳定在3.5GHz上下。不是芯片工程师偷懒而是从物理层面根本走不动了。并行计算这个看起来是“术”的话题真正的地基恰好就是这条物理曲线。1.1 主频越快发热为什么越失控芯片功耗和频率的关系学术推导很复杂但直觉上可以理解成一个简单模型频率每升高一点芯片内部的动态功耗就按电压平方乘以频率的关系往上走。也就是说在电压不能无脑降低的前提下频率和功耗几乎是跟着走的而频率提升带来的性能收益会越来越小功耗却按超线性速度增长散热面积却不会同步变大。结果就是CPU很快变成一个局部热点想散热就得堆成本堆了又失去商业价值。一个更直白的讲法主频是“天花板”散热是“砖头”。你想把主频再抬0.1GHz需要付出的散热成本可能是翻倍的这种边际效益很快归零。半导体行业花了无数精力做低功耗工艺、finFET、先进封装本质上都是在同这条曲线斗争。1.2 三条“墙”一起拦住单核性能学术界谈性能限制时经常引用三条墙我在这里用一张表说清楚墙的名字本质后果频率墙主频提升逼近物理极限单核性能不再随主频线性增长功耗墙功耗与电压平方成比例高主频带来不可接受的散热压力内存墙内存访问延迟远落后于CPU计算速度CPU大量时间在等待数据内存墙是最容易被普通开发者低估的。CPU从主存读一个数据延迟大概在几十到一百纳秒而CPU内部一条指令的执行可能只需要0.3纳秒。若干条访存指令堵在路上处理器的流水线整个被卡住计算单元只能空转。我见过很多“优化了半天算法却根本没快”的案例最后定位下来问题不是CPU算得慢而是数据放在内存里取不过来。1.3 三条路摆在设计者面前深流水线、超线程、还是多核面对这三条墙处理器设计者当时有几种选择。第一把流水线做得更深用更多的级数换更高主频。这条路在2020年初期的Pentium 4上被推到了极致同样也撞了墙——流水线越深分支预测失败和缓存未命中造成的惩罚越严重功耗还成倍增加。第二引入超线程技术把一个物理核包装成两个逻辑核在硬件层面隐藏访存延迟。这个思路依然有效但它提供的并行度有限。第三干脆在同一块芯片上放更多完整的CPU核这个方向最终成了整个行业的共同选择。所以并行计算不是某个学者拍脑袋想的“高级特性”而是处理器行业在物理约束下共同找出的一条出路。而这条路从根本上把“如何并行”从少数人的专业问题变成了所有软件开发者的日常问题。2. 并行思想的“史前时代”从人工计算小组到第一台并行机很多人以为并行计算是电子计算机出现之后才有了概念其实不是。在真正电子计算机出现之前并行思想已经在很多领域以“人肉”形式存在了。理解这段历史对今天设计多核系统、分布式架构依然有启发。2.1 巴贝奇与分析机里的“分工想象”查尔斯·巴贝奇在十九世纪设计分析机的时候虽然没有完整造出机器但他在纸面上规划过一套非常接近现代并行的计算流程。他设想把复杂计算任务拆分成多个操作由不同计算单元协同处理这实际上就是一种任务级并行的雏形。他的助手艾达·洛夫莱斯更进一步意识到机器不只可以处理数字还能处理符号。这个判断把计算从“算账”上升到了“通用信息处理”也顺带让并行思想从单纯的算术分工扩展成了更广义的“拆分任务、各自处理、再组合结果”。今天你看到的所有分治算法、任务队列、MapReduce本质上都和巴贝奇图纸上的思路同构。区别只是当时的“计算单元”可能是齿轮今天的“计算单元”可能是CPU核心或者一台服务器。2.2 人工计算团队并行算法的最早承载者在计算机真正普及之前很多科研项目靠的是成队的“计算员”。我读过一些科学计算史的资料那个年代计算员的日常工作就是面对一堆表格有人负责求和有人负责算对数表有人复核结果。整个流程是一个低配版的人肉流水线数据在人与人之间传递误差靠多轮冗余校验。这段历史经常被忽略但它对后来计算机体系结构的影响非常直接。早期计算机的指令集和存储器设计很大程度上是在模仿人工分工的步骤把复杂问题切成子任务把子任务映射到不同“工人”上最后汇总。今天所有并行编程模型里“任务分解”“负载均衡”“结果归约”这三个词放在人工计算团队里同样成立。2.3 ILLIAC IV一场早期SIMD的昂贵实验进入电子时代后最值得一提的实验机型之一是ILLIAC IV。它于1970年代左右在伊利诺伊大学设计是一个很有雄心的阵列机计划用64个处理器同时执行同一条指令、处理不同数据。这种设计理念在当时非常超前但工程实现上遇到了远远超出预期的困难最终项目延期数年、成本暴涨实际性能也没有完全达到目标。不过它留下了一笔极有价值的遗产向整个行业验证了SIMD架构的潜力也让后来者避开了不少坑。现代GPU内部的大规模并行执行引擎基本思路就和ILLIAC IV一脉相承。所以我不太赞成用“失败”来定义这个项目它更像是一块昂贵的敲门砖。2.4 这段历史对今天的架构师有什么用了解并行思想起源价值不在于背几个名字而在于认识到“并行不是新增特性而是计算的基础属性”。你今天在分布式系统里反复使用的“分而治之”和1950年代计算员之间的分工模式是同构的。后面所有架构设计从多核缓存一致性到云原生调度本质上都是在不同物理尺度上重新发明“分工与协作”。这也是我每次带新人时都会先花时间过一遍历史的原因如果只教技术概念很容易陷入“学会了MESI但不知道为什么要费这么大劲”的状态。有了历史脉络很多设计决策就顺理成章了。3. 并行计算不是“核多了就行”程序内部其实藏着四个层次的并行“并行计算不就是开多线程嘛。”这是我常听到的话。真去做架构设计之后会发现并行几乎存在于计算系统的每一层从最底层的指令到最上层的作业至少可以分成四个层次。理解这四个层次才能有效分析一段程序到底能提速多少、瓶颈在哪。3.1 指令级并行编译器与硬件偷偷替你做的并行在一个现代CPU内部一条指令的执行会被拆成取指、译码、执行、访存、写回等多个阶段。芯片用流水线让这些阶段重叠起来你写的是串行代码CPU内部却可能同时有十几条指令在“飞”。更复杂的是乱序执行处理器会分析指令间的数据依赖把互相之间没有依赖关系的指令打乱顺序执行最后再按原始语义提交结果。这个过程对程序员完全透明代价是芯片内部需要巨大的重排缓冲区和换名寄存器文件。我有时候和做编译器优化的朋友开玩笑你精心调整的循环顺序可能早就被CPU的乱序窗口给“看透改完”了。所以在讨论并行优化前应该先知道底层已经帮你做了多少。3.2 数据级并行SIMD与GPU的哲学很多数值计算的场景里多个数据样本执行的是完全相同的一种操作。比如图像处理时给整片像素统一加一个亮度偏移或者矩阵乘法中对每个元素做同样的乘加。SIMD指令集SSE、AVX这类可以在一个时钟周期内批量处理多个数据。GPU走的是同一路径只是把规模做到了极致几千个轻量级核心共享同一套指令流处理不同的像素或矩阵块。数据级并行有一个明显信号热点代码的循环体里没有复杂的循环依赖基本都是对数组的独立运算。如果你发现代码写得像“按模板套公式”那大概率就有数据级并行的优化空间。3.3 线程级并行多核时代程序员的真正战场到了多核时代需要程序员显式参与的主要是线程级并行。一个进程内创建多个线程每个线程跑在不同的核心上共享进程的地址空间。难点往往不在“创建线程”而在三件事怎么把任务切分得足够均衡、怎么让线程之间的通信尽量少、怎么避免伪共享之类的隐藏性能杀手。伪共享是我在实际项目里经常碰到的问题两个线程各自修改不同的变量但这两个变量偏偏落在同一个缓存行上。由于缓存一致性协议是按缓存行维护的任何一个线程写自己的变量都会导致整个缓存行在其他核上失效结果两个线程互相拖累性能比串行还差。这种问题不亲自踩过一次很难在代码评审中一眼看出来。3.4 作业级并行从单机到分布式集群单机的并行资源总归有限解决不了更大规模的问题时下一步是把多台机器组织起来。作业级并行看的不是线程在CPU上的调度而是整个计算任务如何分布到多台物理机上。典型的例子是MapReduce它把一个巨大任务拆成成百上千个小作业分散到集群里同时跑再汇总结果。这四个层次从上到下越往下越依赖硬件和编译器越往上越需要程序员亲自设计。成熟的并行架构设计往往是在四层之间来回权衡。比如你要优化一个深度学习训练任务可能先用GPU做数据级并行再用多卡做作业级并行同时还要注意CPU端的数据预处理有没有压满所有核。没有一个层次的“优化好”可以替代全局考虑。4. Flynn分类法给所有并行架构画一张坐标系如果不建立一套统一的分类语言讨论并行架构会变得非常混乱。1972年Michael Flynn提出了一套极简的并行计算机分类法到今天依然是理解各种并行系统的第一把钥匙。4.1 SISD、SIMD、MISD、MIMD四种组合的直观理解Flynn分类法只用两个维度就概括了并行系统指令流数量和的数据流数量。于是得到四种组合分类含义现实代表SISD单指令流单数据流传统单核CPUSIMD单指令流多数据流GPU、AVX指令、向量机MISD多指令流单数据流容错系统中的冗余执行MIMD多指令流多数据流多核CPU、大规模集群世界上绝大多数计算机属于SISD或MIMD。GPU是SIMD的典型代表MISD则很罕见一般只在故障容错场景里用多套处理器同时处理同一份数据再对比结果比如航空航天和关键安全系统。4.2 用Flynn分类法看清GPU与CPU的本质差异很多人觉得GPU是“显卡”和并行计算关联不大。其实GPU在Flynn分类法里属于极其典型的SIMD大系统。现代GPU更准确地说是SIMT风格——单指令多线程每个线程有自己的寄存器和执行上下文但大量线程共享同一套取指和执行单元。这种架构极其适合图形处理和深度学习的矩阵运算因为这些任务的瓶颈不是复杂逻辑分支而是规模巨大的重复计算。对比之下CPU是MIMD的代表非常适合任务级并行乃至串行逻辑复杂的代码。所以现在AI芯片设计经常讲“CPUGPU/NPU异构”本质就是让MIMD负责控制流、SIMD负责数据流二者配合。如果一开始就用Flynn分类法来理解这两种处理器就不会再纠结“GPU能不能完全替代CPU”这类问题。4.3 分类法的局限一把尺子不是一套图纸Flynn分类法足够简洁但也因此牺牲了区分度。它没有考虑存储层次、网络拓扑和通信结构而这些恰好在现代并行架构中占了极大的决策权重。一台MIMD机器可能是共享内存的多核CPU也可能是分布式集群两者的编程模型、性能特征、故障模型差异巨大但在Flynn分类法里它们完全一样。所以我的建议是把Flynn分类法当成“先定位再深入”的起点。看到一个新的并行系统先用Flynn给出骨架上的判断然后立刻进入两个更深层面的分析一个是内存和缓存如何组织另一个是任务如何调度和通信。这两点才是并行架构设计真正拉开差距的地方。5. 多核架构设计的“三座大山”缓存一致性、内存模型与同步聊完分类法我们进入并行架构设计中最硬核的部分。多核CPU之所以难不只是因为要从单核改成多核而是因为引入多核后整个内存模型和程序并发行为都必须重新设计。这里有三座绕不过去的大山。5.1 缓存一致性MESI协议和它背后的状态机现代CPU中每个核都有自己私有的L1、L2缓存。同一份内存数据可能被多个核各自缓存一份。如果核A修改了数值核B还在用旧值就会出现经典的“缓存不一致”问题。解决它的根基是缓存一致性协议MESI就是其中最著名的一个。MESI用四种状态来标记缓存行Modified已修改、Exclusive独占、Shared共享、Invalid无效。核与核之间通过总线或片上网络广播状态转换消息保证每一个缓存行在任何时刻要么是唯一的独占状态要么是全局共享的只读状态。实现细节远比教科书图中的状态转换复杂因为真实系统里缓存行是并发的、事务是乱序的还有中断和DMA要把状态机打断。很多芯片设计团队在验证MESI协议上花费的时间比设计执行单元都多。需要特别提醒的是MESI解决的是“同一个地址的多副本一致”问题它不等于“你写代码看到的执行顺序就是程序顺序”。这两个问题经常被混在一起但它们是独立的两回事。5.2 内存模型编译器重排指令带来的那些坑你以为代码里写的先后顺序就是CPU实际执行的顺序不一定。为了提高性能编译器和CPU都会重新排列指令序列。在单线程下这种重排不影响最终结果一旦多线程共享数据风险就来了。最典型的例子是线程A先写data再写flag线程B看到flag为真就以为data也已经写完。但实际上由于重排线程B可能读到旧data整个逻辑瞬间崩溃。为了解决这个问题语言和硬件层面引入内存模型。Java的JMM、C11的std::memory_order、Rust的原子类型本质上都是让你告诉编译器在这个点上哪些重排是被允许的哪些是不可接受的。只有在极少数需要极致性能的关键路径上才需要手动插入内存屏障或使用release-acquire语义日常开发优先用锁、原子变量和语言提供的高级同步工具会更安全。5.3 同步原语从自旋锁到无锁编程的取舍有共享就有并发的读写有并发读写就得有同步机制。最简单的同步工具是互斥锁但高性能场景下锁竞争会导致严重性能下降甚至出现“锁抖动”。我遇到过这样一个项目一个看似非常简单的全局计数器因为所有线程都去抢占同一把锁导致最终吞吐量比单线程还低。后来把计数器改成每个线程一个本地副本线程各自累加最后再汇总合并性能立刻恢复正常。这种问题背后是同步开销的基本原理。锁本身不是问题问题是临界区过长、锁粒度过粗、多个锁互相嵌套。自旋锁适合临界区极短的场景读写锁适合读多写少无锁队列则适合对延迟极其敏感的高频交易或网络包处理。选型的判断标准永远只有一个你到底在保护什么、你的并发模型里有多少竞争。这三座大山每座都值得花大量时间去深入。理解了它们才会明白“核越多不一定越快”的真正原因——一旦缓存同步和锁竞争成为瓶颈加核就成了加负。6. 从MPP到云原生分布式并行架构设计的一条演进主线单机多核的资源终归有限超大规模计算最终要跨机器。到了这一层并行架构设计开始进入分布式系统领域。这一章我从硬件形态讲到软件调度把分布式并行的演进主线捋一遍。6.1 SMP、NUMA、MPP硬件形态差异决定了编程难度同样是多处理器系统硬件组织方式差异很大不能一概而论SMP对称多处理所有CPU共享同一块内存总线访问任意内存地址的延迟基本一致编程简单但总线带宽容易成为瓶颈。NUMA非均匀内存访问每个CPU有自己更靠近的内存访问本地内存快、远程内存慢性能优化需要关心数据放置。MPP大规模并行处理每台机器独立内存和操作系统通过高速网络互联强调扩展性和容错但编程模型更重。做架构设计时第一步就要判断自己面对的是哪一种形态。如果你在NUMA机器上把线程绑定错了节点一次远程内存访问可能比本地访问慢一截这个差距在高频访问场景下会直接体现在延迟指标上。6.2 集群、网格与云边界越来越模糊的趋势传统教科书把集群定义为紧耦合的固定资源集合网格强调跨机构共享异构资源云则讲究资源池化、按需分配。今天在容器和Kubernetes这一层调度平台底下这三者的边界已经很模糊。你用云上的容器集群跑Spark作业调度器看到的却是一个“虚拟集群”。硬件到底是谁的、在哪里越来越不重要重要的是资源抽象后能不能按需弹缩。对并行架构设计来说这其实是个好消息很多过去需要专门搭建的MPP环境现在可以用云原生方式临时创建出来用完后释放。并行计算从“大科学装置”变成了“按需购买的计算能力”。6.3 无共享架构与容错思维分布式并行设计的两个支柱分布式并行架构里最核心的架构原则是Shared-Nothing也就是无共享。每个节点尽量独立持有自己的数据避免跨节点通信。理由很直白网络IO比本地内存IO慢好几个数量级一次跨节点取数据可能就足以拖垮整个任务的性能。所以绝大多数分布式数据库和计算框架都会想尽办法把计算推给数据而不是把数据拉过来算。容错是另一个支柱。机器多了之后故障不再是“万一”而是“必然”。任何并行任务在设计时就要考虑某个节点执行到一半挂了怎么办任务可以重新调度吗中间结果有没有持久化这也是MapReduce一类框架能够成功的要点——它把容错和并行一起打包给你你不需要自己维护节点状态。从MPP到现在云原生软件层在变硬件形态在变但“减少通信、接受故障、把计算推近数据”这三个底层原则一直没有变。7. 并行编程模型的代际跃迁从MPI、OpenMP到CUDA和MapReduce对绝大多数程序员来说并行计算的第一接触点是编程模型而不是芯片内部细节。在这个层面过去三十年发生了几次明显的范式转移每一次都让并行计算的准入门槛低了一大截。7.1 两种心智模型共享内存与消息传递并行编程模型大体分成两类。一类是共享内存模型以OpenMP为代表线程之间通过共享变量直接通信写起来很自然但局限于单机多核。另一类是消息传递模型以MPI为代表进程间没有共享地址空间只能通过显式的send/receive来交换数据写起来繁琐却几乎可以扩展到任意规模。这个选择的本质是在“易用性”和“可扩展性”之间做取舍。你不能指望MPI像OpenMP那样几行指令就能并行但它能承受上万节点规模的天气预报模拟。反之如果你的任务只在一台机器上跑硬上MPI只会把简单问题复杂化。7.2 OpenMP最小示例与默认调度策略的陷阱为了说明共享内存模型的方便程度这里放一个极简OpenMP例子#include omp.h #include stdio.h int main() { #pragma omp parallel for num_threads(4) for (int i 0; i 100; i) { printf(thread %d handling item %d\n, omp_get_thread_num(), i); } return 0; }代码本身很短核心就是那一行编译制导指令。编译器会把100次循环划分给4个线程去执行。但很多人第一次用OpenMP踩的坑在于调度策略默认的静态调度是平均切分任务如果各迭代的实际计算量很不均匀就会出现一部分线程忙死、一部分线程摸鱼。这时候需要改成dynamic调度让空闲线程去队列里取下一个任务。选错调度策略四核理论上四倍加速实际可能只有1.2倍。7.3 CUDAGPU并行从实验室走向大众的分水岭如果说OpenMP降低了单机并行的门槛那CUDA把GPU并行的门槛也拉到了普通工程师够得到的高度。GPU和CPU最大差异在线程数量级CPU一颗也就几十个线程上下文GPU可以同时管理几万个轻量级线程。CUDA的核心编程模型包括把数据拷贝到显存编写在GPU上运行的kernel函数再按网格和线程块的方式组织大规模并行任务。我第一次跑通CUDA程序的感受至今很清晰一段100万次循环的向量加法CPU版本跑了好几百毫秒GPU版本压到了几毫秒。虽然其中有编译优化的因素但量级上的冲击是实打实的。不过也要提醒CUDA不是“把循环塞进kernel就行”。内存拷贝的开销、线程块大小的选择、bank conflict这类硬件特性都会极大影响实际收益。GPU并行是一门需要动手调优的手艺不是看几天文档就能掌握的。7.4 MapReduce与Spark让“不擅长并行”的人也能并行MapReduce真正的革命性不在于性能而在于把并行、容错、调度、数据分区全部封装起来让程序员只需要实现map和reduce两个函数。这个抽象极其成功因为它把“如何并行”从程序员手里收走只留下“业务逻辑”。Spark则进一步把中间结果保留在内存减少反复落盘的损耗成为大数据领域的事实标准。这条演进路径背后的逻辑非常清晰每一代编程模型都在让“并行”这件事变得更不显眼。OpenMP把多线程包装成编译指令CUDA把你的计算抽象成网格和块MapReduce把整个集群抽象成一个函数式接口。未来新的并行编程模型大概率也会沿着这个方向继续走下去。8. 发展历程中的几个关键时刻以及并行计算的下一站把镜头拉远并行计算的发展并不是匀速推进的它有几个非常明显的拐点。理解了这些拐点就能理解为什么今天的架构设计长成这个样子。8.1 多核处理器集体转向一个改变软件生态的产业决策2000年代中期全球主流芯片厂商不约而同地把产品路线从“更高主频”转向“更多核心”。这个决定背后是物理约束也是商业选择。它带来一个深远影响所有软件开发者都必须重新审视自己的代码因为过去“升级处理器就能让程序变快”的日子结束了想快就得自己写并行。并行计算从少数高性能计算专家的专属领域变成了全体开发者的必备技能。回头看这是整个软件生态向并发和分布式演化最重要的一个分水岭。8.2 GPU通用化与深度学习浪潮CUDA发布之后GPU从图像加速器变成通用并行处理器。这本来只是一个硬件开放事件但紧接着深度学习爆发了神经网络训练的核心是大量矩阵乘法和卷积运算这正是GPU最擅长的事情。可以说过去十年AI发展的底层算力支撑就是并行计算而AI的需求反过来又倒逼GPU架构持续进化从单纯的计算单元走向带张量核心、甚至专门为Transformer优化的新形态。这条正反馈链条不会很快结束。8.3 异构计算与存算一体下一站的可能方向单靠CPU或单靠GPU已经很难满足所有场景。下一步的大主题大概率是“异构”CPU负责控制逻辑GPU/NPU负责高吞吐计算FPGA负责时延敏感或定制逻辑ASIC负责最固定的专用负载。系统层则需要有一套调度框架能把不同计算单元组织成统一资源池。另一个值得关注的方向是存算一体思路是在存储设备内部就近完成计算直接绕开内存墙。这个方向在AI推理场景已经有原型产品但要大规模进入通用计算还有很长的路要走。对架构师来说提前接触这些方向能帮你建立对技术迭代的感知避免等标准成熟后再从零学起。8.4 量子计算机带来的“不一样并行”严格来说量子计算提供的计算优势不能直接等同于经典并行量子比特的叠加态和纠缠态并不是“多个线程跑同一段代码”那么简单。但它确实在提醒我们计算模型本身是可以被重新定义的。现在的并行架构设计建立在“经典比特和布尔逻辑”这个基础上一旦基础模型改变很多设计会整个翻转。这一步还很远但它让我重新理解了并行计算的本质边界并行不是某个层级的优化技巧而是从物理世界到软件抽象一路贯穿的基础属性。无论未来芯片用什么材料、计算机用什么模型只要任务可以拆散、结果可以合并“并行”就会以新的形态继续存在。回顾这一路走来的经验我的真实感受是并行计算从入门到精通最忌讳的就是“只学一层”。如果一开始就扎进CUDA优化很容易忽略缓存一致性这类底层问题反过来只研究硬件原理又不理解MapReduce为什么能流行。最好的路径是先建立四层并行的大局观再根据自己方向逐层深入。把这篇文章里的内容消化透再去看具体的多核编程、GPU优化或分布式框架你会发现“并行”其实是一以贯之的一条主线而不是一堆零散知识点。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进