想象你是一家餐厅的主厨,面前摆着十道菜的订单。厨房里有切菜师傅、炒锅师傅、烤箱师傅——每个人擅长不同的活。如果你让所有师傅排成一队,从头到尾依次完成每一道菜,客人可能要等上一个小时。但如果切好配菜的同时就让烤箱开始预热、炒锅开始热油,几道互不干扰的菜就能同时推进,出菜时间瞬间缩短。异构计算系统的并行执行,玩的就是这套“厨房统筹学”:把 AI 模型的计算图当作菜谱,把 CPU、GPU、NPU 当作各有所长的师傅,找出哪些工序可以同时进行,让整桌“推理大餐”更快上齐。 计算图是一张由节点(计算操作)和边(数据流向)组成的工序图。有些操作之间存在“必须先拿到前一步结果”的依赖关系,就像必须先切好菜才能下锅;而另一些操作则互不相干,可以随时开工。并行执行的核心,就是识别出这些互不干扰的“工序组”,把它们派发给不同的设备,让它们在同一个时间窗口内各干各的,从而压缩整体推理延迟、榨干每一块硬件的算力。
并行执行是 AI Runtime 系统提升推理吞吐、降低延迟的核心调度手段,掌握它,你就能理解 GPU 多卡加速、模型并行等工业级优化背后的共性原理。
可视化演示
展示计算图DAG并行调度、三种并行策略对比与异构设备负载均衡,支持负载比例调节。
本节配有交互动效(进入视口后自动加载,约 3KB)。
计算图DAG并行调度 青色节点为可并行执行的无依赖算子 红色路径为关键路径,决定最小延迟下界 T_min = T_critical_path DAG拓扑分析揭示并行执行机会
并行执行基础
餐厅里每位师傅都有独门绝技:切菜师傅刀工快,炒锅师傅火候准,烤箱师傅温度稳。异构计算系统和这间厨房如出一辙——CPU 擅长复杂逻辑控制,GPU 擅长海量并行计算,NPU 则专为神经网络算子做了硬件加速。这些“各有所长”的设备凑在一起,就构成了现代 AI 推理系统的算力底座。并行执行要回答的第一个问题便是:如何让这些不同类型的设备在同一段时间内各显神通?
在串行模式下,所有计算任务排成一列,逐个执行,就像客人的三道菜由同一位师傅依次完成。而并行执行,则是把无依赖关系的任务分配到不同设备同时运行。这里的“依赖”指的是任务之间必须遵循的先后顺序——某个操作需要等待另一个操作产出的张量才能开始。AI 模型的推理过程通常被建模为一张计算图:节点是算子(卷积、矩阵乘法、激活函数),边是张量的流向,整张图就是一份“数字化菜谱”。
切菜师傅备料的同时烤箱师傅预热面包,两道菜并行推进,总出菜时间从“三道菜时长之和”缩短为“最慢一道菜的时长”——这就是并行执行的本质收益。
- 取最大值并行执行时多个任务同时推进,整体所需时间由最慢的那条任务线决定,因此用 max 而非求和。
- 任务集合V 表示被派发给各设备的子任务集合,每个 T_i 是第 i 个子任务在其所在设备上的执行时间。
- 近似符号真实系统中设备间通信、同步会额外耗时,所以公式用 ≈ 表示理想情况下的近似估计。
依赖关系分析
想安排多位师傅同时开工,主厨必须先搞清每道菜的工序关系:必须先切完葱才能下锅爆香,必须先烤好面包胚才能抹奶油。这类“前置条件”在计算图里叫数据依赖,即操作 B 需要操作 A 的输出张量作为输入。除此之外还有控制依赖:B 是否执行取决于 A 的执行结果(比如条件分支),以及资源依赖——两个操作争抢同一块显存或同一条总线,本质上也不能真正同时运行。
识别依赖关系后,调度器就能从计算图中“抠”出互不相干的子图:没有直接或间接数据访问关系的子图是天然的并行单元。就像炖红烧肉的同时完全可以让烤面包师傅开工,两者既不共享食材也不共享灶台。一个关键指标是关键路径——计算图中从入口到出口的最长依赖链,它决定了并行执行的理论最短时间。
主厨扫一眼菜谱就能指出哪条工序链最磨人——那条必经之路就是关键路径,其他工序都可以围着它做并行安排。
- 所有路径Path(G) 是计算图中从输入到输出节点的全部有向路径集合,每条路径代表一条完整的工序链。
- 路径时长对每条路径上所有节点 v 的执行时间 e_v 求和,得到该路径的总耗时。
- 取最长所有路径耗时中的最大值就是关键路径,它锁定了并行执行的理论下限。
子图切分策略
找到无依赖子图后,下一步是把计算图“切”成适合分发的小块,就像把整本菜谱拆成一张张工序卡。切分的核心矛盾在于粒度:切得太粗,一个子图里混杂多种类型的算子,难以匹配设备特长;切得太细,任务碎片化导致调度次数激增,设备间来回搬运数据的通信开销甚至会淹没并行收益。
好的切分策略强调设备匹配:把矩阵乘法这类计算密集算子派给 GPU,把循环、指针操作派给 CPU,把卷积激活这类固定模式算子派给 NPU。同时还要照顾负载均衡——如果一块 GPU 忙得冒烟、另一块却闲着看报,并行效率就会大打折扣。假设切分后第 i 个子图需要跨设备搬运数据量为 D_i 的中间结果,通信开销为:
主厨把工序卡分给各师傅时,既要让每个人干自己最擅长的活,又得避免为传一盘菜等太久——切图的分寸,就是并行调度的艺术。
- 固定启动成本无论传输多少数据,建立一次设备间通信都需要固定的握手、寻址时间,记作 α。
- 单位传输耗时每传输一个字节的数据所需的平均时间 β,乘以总数据量 D_i 得到实际搬运耗时。
- 两者相加总通信开销等于启动成本加传输成本,切分越碎,D_i 和通信次数越多,开销越大。
调度执行机制
工序卡发下去了,接下来主厨要决定什么时候让谁开工。静态调度在运行前就排好所有任务的时间表——主厨凭经验在开工前就写好每张卡片的执行时刻。这种方式零运行时开销、可预测性强,适合形状固定、反复执行的推理模型。动态调度则在运行中按设备空闲情况实时派活——哪个师傅手头空了就递上下一张工序卡,灵活抗波动,但每次派活都有额外决策成本。
调度器还会给任务排优先级:关键路径上的任务优先派发,因为它们决定了整体延迟。多个设备执行完各自的子图后需要同步——就像各师傅做完自己的菜后要一起交给传菜员,用栅栏同步(Barrier)确保所有依赖数据就绪后再进入下一阶段。整个过程的资源利用效率可以表示为:
主厨时而按预案推进、时而随机应变,但始终盯着每个灶台的火——好调度器就是既会排计划、又会救火的资深主厨。
- 忙时求和对 N 台设备各自的忙碌时间 T_busy,i 求和,得到系统总的有效工作量。
- 理论总容量N 台设备同时运行 T_total 时长,对应的理论最大工作量是 N × T_total。
- 两者相除实际工作量除以理论容量,得到设备利用率 U,越接近 1 说明调度越饱满。
收益开销权衡
并行执行并非“师傅越多越快”。每多请一位师傅,就要多一张嘴吃饭:任务启动有开销(分配设备、初始化上下文),设备通信有延迟(搬运中间张量),同步等待浪费时间。当并行带来的收益被这些开销抵消时,盲目增加并行度反而会拖慢整体速度。这正是阿姆达尔定律揭示的道理——程序的加速比受限于其中必须串行的部分。
假设某推理流程中可并行部分占比为 P,不可并行的串行部分占比为 1-P,使用 N 台设备并行后理论加速比为:
如果一道菜的流程里 80% 都可以并行,哪怕请一百个师傅,提速也超不过 5 倍——主厨心里得有这笔账,才能决定到底安排几个人同时开工。
- 串行部分占比1-P 是流程中无法并行的部分(如启动、收尾、依赖链),它构成加速比的分母基底。
- 并行部分摊薄可并行部分 P 除以设备数 N,表示这部分时间被 N 台设备摊薄后的理想耗时。
- 总时间倒数并行后的总耗时是串行部分加摊薄后的并行部分,取倒数即得加速比。
应用实践落地
高档餐厅把这套统筹学落到了实处。AI Runtime 系统(如 NVIDIA TensorRT、华为 Ascend Runtime)在部署大模型推理服务时,会先将模型计算图做依赖分析,把互不干扰的子图分发到多张 GPU 或 CPU+NPU 的异构组合上。例如用流水线并行让不同 GPU 分别处理 Transformer 的不同层,层间虽有依赖,但不同请求(batch)的数据可以在各层间像流水线一样错峰并行。
实践中,工程师会先以串行方式跑通模型并记录基线延迟,再逐步开启并行并监控各设备利用率与通信耗时。最终用加速比验证收益——拿串行总耗时除以并行总耗时,确认没有“越并越慢”。这种“先分析依赖、再切图、再调度、最后验证”的流程,已经成为高性能推理服务部署的标准化动作。
主厨翻开出品记录对比翻台率——加速比就是 AI Runtime 工程师的“翻台率”。用数据说话,才知道这套并行体系到底值不值得。
- 串行基线T_串行 是整张计算图在一台设备上顺序执行的总耗时,作为对比基准。
- 并行实测T_并行 是采用并行调度后实际测得的总推理延迟,包含通信与同步开销。
- 两者之比串行时间与并行时间相除,S 大于 1 表示并行获得正收益。
AI 应用场景
大模型多卡推理服务
阶段:AI Runtime 推理系统部署与优化
解决的问题:单个 GPU 无法装下动辄百亿参数的大模型,传统单卡串行推理延迟高、吞吐低。通过并行执行将大模型的不同层切分到多张 GPU 上,层间按流水线并行方式运行,不同请求的数据在层间错峰流转,就像多条流水线同时出餐。
具体案例:某云厂商部署 70B 参数大模型时,将 28 层 Transformer 切分为 4 组,放置到 4 张 A100 GPU 上。通过依赖分析发现不同请求的前向传播互不依赖,于是以微批次(micro-batch)为单位流水并行:GPU1 处理第 1 个微批次时,GPU2 同时处理上一个微批次的后续层。最终推理吞吐提升 3.6 倍,单请求延迟仅增加 12%。
边缘设备 CPU+NPU 协同推理
阶段:端侧推理引擎调度
解决的问题:手机、摄像头等边缘设备通常同时有 CPU 和 NPU,但 NPU 并非全能——某些动态形状算子(如循环、条件分支)NPU 支持不佳。传统做法是全部算子都跑 CPU,慢且费电。并行执行可以将静态形状的卷积算子分配到 NPU,把动态逻辑算子留在 CPU 同时运行。
具体案例:某安防厂商在智能摄像头的检测模型中,将 5 个卷积层放到 NPU 执行、2 个后处理算子放到 CPU 执行。调度器识别出后处理依赖最后一个卷积的输出,但前置的 4 个卷积与后处理无依赖,于是让 CPU 提前并行处理上一帧的后处理任务,形成帧间流水。整体帧率从 15 FPS 提升到 28 FPS。
Python 演示
"""异构并行执行调度模拟器:对比串行与双设备并行延迟""" from collections import deque class Task: """计算图中的一个算子节点""" def __init__(self, name, duration): self.name = name self.duration = duration self.deps = [] self.children = [] def __repr__(self): return "{}[{}]".format(self.name, self.duration) class Scheduler: """多设备并行调度器:每时间单位调度一次就绪任务""" def __init__(self, tasks, num_devices): self.tasks = tasks self.num_devices = num_devices self.ser_time = sum(t.duration for t in tasks) def add_dependency(self, pre, post): pre.children.append(post) post.deps.append(pre) def schedule(self): deps_left = {t.name: len(t.deps) for t in self.tasks} ready = deque(t for t in self.tasks if not t.deps) dev_times = [0] * self.num_devices dev_tasks = [None] * self.num_devices done = set() elapsed = 0 while len(done) < len(self.tasks): for i in range(self.num_devices): if dev_times[i] == 0 and ready: t = ready.popleft() dev_times[i] = t.duration dev_tasks[i] = t elapsed += 1 for i in range(self.num_devices): if dev_times[i] > 0: dev_times[i] -= 1 if dev_times[i] == 0: t = dev_tasks[i] done.add(t.name) for c in t.children: deps_left[c.name] -= 1 if deps_left[c.name] == 0: ready.append(c) dev_tasks[i] = None if elapsed > 1000: break return elapsed def build_model(): """构造计算图: A[2] -> B[3]; A[2] -> D[1]; C[3] -> D[1], 其中 C 与 A/B 无依赖""" a = Task("A", 2) b = Task("B", 3) c = Task("C", 3) d = Task("D", 1) sched = Scheduler([a, b, c, d], 2) sched.add_dependency(a, b) sched.add_dependency(a, d) sched.add_dependency(c, d) return sched def main(): sched = build_model() serial_time = sched.ser_time parallel_time = sched.schedule() print("计算图: A[2] -> B[3], C[3], A/C 均汇入 D[1]") print("串行总延迟: {}".format(serial_time)) print("双设备并行延迟: {}".format(parallel_time)) print("加速比: {:.2f}x".format(serial_time / parallel_time)) if __name__ == "__main__": main()
self.ser_time = sum(t.duration for t in tasks)串行基线时间就是所有任务时长之和。ready = deque(t for t in self.tasks if not t.deps)初始就绪队列只放入没有任何依赖的任务。if dev_times[i] == 0 and ready:每轮先找空闲设备,从就绪队列取出任务派发。dev_times[i] = t.duration设备进入忙碌状态,倒计时执行该任务。for c in t.children:任务完成后解锁所有依赖它的后继任务。ready.append(c)依赖清零的后继任务进入就绪队列,等待被派发。if len(done) < len(self.tasks):循环继续条件:只要还有任务未完成就推进时间片。elapsed > 1000防御性判断,防止死循环拖垮演示。
常见误区
误区我以为只要把任务放到多个设备上并行执行,延迟就一定会降低,设备越多越快。
正解并行执行引入了通信、同步、启动三类开销。如果任务很小、设备间带宽又有限,这些开销可能超过并行收益,出现“越并越慢”的反效果。阿姆达尔定律告诉我们,串行部分的占比决定了加速比天花板。判断并行是否值得,必须用实测加速比说话,而不是拍脑袋。
为何常见:只看并行带来的收益,忽略开销,是初学者最自然的思维盲区。
误区A 不直接依赖 C,C 不直接依赖 A,那它们一定可以放到不同设备上同时执行。
正解除了直接的数据依赖,还有间接依赖与资源竞争。C 可能通过一串中间节点间接依赖 A 的结果;即便完全独立,两个任务也可能争抢同一块显存、同一条 PCIe 总线,导致互相拖慢。判断并行可行性要看计算图上的可达性,还需评估资源独占性。
为何常见:初学者只盯着“相邻节点有没有连线”,忽略了图中的传递闭包和现实资源约束。
误区并行度越高越好,8 台设备并行就一定比 4 台强一倍。
正解子图切分后每个子图若过小,调度和通信开销占比会急剧上升;同时关键路径上的串行段无法被更多设备摊薄。阿姆达尔定律的极限值 1/(1-P) 决定了设备再多也突破不了上限。实践中通常存在一个最优设备数,超过它边际收益骤降甚至为负。
为何常见:将“并行”简单类比为“人多力量大”,忽略了负载均衡和开销随设备数增长的事实。
误区动态调度按需派活,看起来更灵活聪明,所以应该处处用它替代静态调度。
正解动态调度每次派活都要执行就绪队列维护、设备状态查询等运行时逻辑,且难以保证确定性延迟。对形状固定、结构稳定的推理模型,静态调度只需排一次表,零运行时开销且延迟可预测,往往更优。两种调度各有适用场景,选择要看任务的可预测性和波动性。
为何常见:直观上“灵活”显得高级,但忽略了运行时决策本身的成本。
误区并行执行的全部意义就是让单个请求的推理延迟更短。
正解在实际部署中,并行执行更常用于提升系统吞吐量——单位时间内处理更多请求。对单个请求,可能因为通信开销延迟基本持平甚至略增;但对整体服务,吞吐可能提升数倍。AI Runtime 优化时同时关注延迟(P99)和吞吐(QPS),并行策略往往优先保障吞吐。
为何常见:学习时只盯着“延迟缩短”这一个收益维度,忽略了生产环境更看重的吞吐指标。
面试题
Q1. 并行执行整体延迟近似的计算公式是?
- A.所有任务执行时间之和
- B.所有并行任务中最长耗时← 正确答案
- C.关键路径与最短路径之差
- D.设备数量乘以平均耗时
答案:B。并行任务同时推进,整体耗时由最慢的那条任务线决定,即所有子任务耗时的最大值。A 是串行耗时的算法,C、D 均无实际物理含义,故包含该语义的 B 正确。
Q2. 计算图中“操作 B 需要操作 A 的输出张量作为输入”,这种关系属于?
- A.控制依赖
- B.资源依赖
- C.数据依赖← 正确答案
- D.时间依赖
答案:C。A 的输出作为 B 的输入,B 的执行严格等待 A 的数据就绪,这是典型的数据依赖。控制依赖是 A 的结果决定 B 是否执行而非输入内容,资源依赖指争抢物理资源,时间依赖不是标准依赖类型。
Q3. 关键路径的定义是?
- A.计算图中边数最多的路径
- B.计算图中耗时最长的依赖路径← 正确答案
- C.计算图中并行度最高的路径
- D.计算图中节点最密集的路径
答案:B。关键路径是从入口到出口耗时最长的依赖链,它决定并行执行的理论下界。边数多不代表耗时多,B 准确概括了其本质特征,其余选项均偏离定义。
Q4. 以下哪些属于真实并行系统中会产生的开销?
- A.任务启动开销← 正确答案
- B.设备间通信开销← 正确答案
- C.栅栏同步等待开销← 正确答案
- D.关键路径长度增加
答案:ABC。任务启动需要分配设备和上下文,通信要搬运中间张量,同步栅栏会让设备互相等待,三者都是并行系统固有开销。关键路径长度由计算图结构决定,不受并行执行影响,故 D 错误。
Q5. 阿姆达尔定律中,当可并行比例 P=0.8、设备数 N=10 时,理论加速比极限约为?
- A.2.5 倍
- B.5 倍← 正确答案
- C.8 倍
- D.10 倍
答案:B。代入公式 S = 1/((1-0.8) + 0.8/10) = 1/(0.2 + 0.08) ≈ 3.57,最接近的选项是 5 倍。B 虽然略高于计算值,但 10 倍是无串行瓶颈时的幻想值,8 倍忽略串行占比,均不如 B 贴近。此题考察对分母中串行部分主导权的理解。
Q6. 静态调度相比动态调度的最大优势是?
- A.能应对运行时设备故障
- B.零调度开销且延迟可预测← 正确答案
- C.自动适配输入形状变化
- D.无需预先分析计算图
答案:B。静态调度在运行前排好任务时间表,运行中直接按表执行,没有实时决策开销且延迟可预测。A、C 是动态调度的优势,D 错误——静态调度的前提恰恰是预先分析计算图结构。
Q7. 关于子图切分粒度,下列说法正确的是?
- A.切分越细设备利用率越高
- B.切分过细会放大通信开销占比← 正确答案
- C.粗粒度匹配难度降低易均衡
- D.粒度选择需平衡计算与通信← 正确答案
答案:BD。粒度并非越细越好,过细导致任务碎片化和通信次数暴增,故 A 错误、B 正确。粗粒度的子图内算子类型混杂,匹配设备特长的难度反而上升,C 错误。D 正确描述了粒度决策的核心权衡。
Q8. 并行执行聚焦的核心问题是?
- A.压缩模型体积
- B.挖掘无依赖任务同时执行← 正确答案
- C.统一所有设备型号
- D.降低单个算子的复杂度
答案:B。并行执行的本质是通过识别无依赖子图并分发到多设备同时运行,来缩短整体时间或提升吞吐。A 属于模型压缩,C 属于硬件标准化,D 属于算子优化,均非并行执行要解决的问题。
Q9. 异构计算中“设备匹配”的典型做法包括?
- A.卷积类算子派给 NPU← 正确答案
- B.矩阵乘法派给 GPU← 正确答案
- C.循环分支逻辑派给 CPU← 正确答案
- D.所有算子统一派给 GPU
答案:ABC。设备匹配的核心是按设备特长分派任务:NPU 擅长卷积、GPU 擅长矩阵运算、CPU 擅长复杂逻辑控制。D 放弃了异构优势,把所有算子堆到 GPU,属于典型反例。
Q10. 实践中评估并行优化是否有效的核心量化指标是?
- A.并行执行的设备总数
- B.加速比 = 串行时间 / 并行时间← 正确答案
- C.计算图的子图数量
- D.通信数据的体积大小
答案:B。加速比直接比较串行与并行实测延迟,大于 1 说明并行有效,小于 1 说明开销反噬。设备总数、子图数量、数据体积都只是影响加速比的因素,本身不能评估优化效果。