想象你经营着一家定制工坊,门口排着长长的订单队列。工坊里有三位手艺迥异的师傅:老C(CPU)是全能手艺人,开料、雕刻、抛光样样会,但做事一次只能埋头几件,速度上不来;老G(GPU)是流水线大师,遇到一千个一模一样的圆珠打磨任务,能拉出一条上千人的传送带同时开工,快得惊人,可你要是让他雕一朵不对称的花,他反而抓瞎;老N(NPU)是AI雕塑专家,对神经网络模型里层层叠加的矩阵造型术研究到了极致,功耗低、效率高,但离开AI手艺,他几乎不碰别的活。 你的职责,就是当那位“总调度”:每来一张订单(计算任务),判断该给谁做(设备选型)、排到几点(执行时序)、把什么材料送到谁的工位(数据搬移)、几个师傅同时闲着时怎么让工坊产出最大化(负载均衡)。这一整套“分单-派活-送货-催工期”的运营艺术,正是异构计算调度的核心命题。本篇文章,我们就以这家“异构工坊”为镜,把 CPU/GPU/NPU 调度的门道一项项拆开讲给你听。
学懂异构计算调度,你将掌握 AI Runtime 系统的核心枢纽,理解主流框架性能差异的根源,是走向编译器、系统软件和 AI 基础架构方向的关键一跃。
可视化演示
展示CPU/GPU/NPU异构调度:任务按设备特性调度、负载占比分配、异构加速对比单核性能,通过交互理解异构计算资源的调度机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
设备调度:按特性分配 任务按设备特性调度 NPU适合专用算子 J→{CPU,GPU,NPU} 分配到最合适设备
异构计算概览
工坊刚开张时,你可能觉得“谁的活儿谁干就行”。可真拿到订单你会发现,一张“给一万个圆珠打磨抛光”的订单,让全能手艺人老C干三天三夜也赶不完;而一张“精雕一只花纹复杂的椅子腿”的订单,让流水线老G来做,他会摆摆手说做不了。硬件生而不同,这就是“异构”的由来。
在计算世界里,CPU 是全能型选手:核心少、主频高、擅长处理分支和复杂控制逻辑;GPU 是众核并行狂魔:成百上千个核心,适合做数据高度并行的矩阵运算;NPU 则是 AI 专用加速器:把卷积、矩阵乘、激活函数这些深度学习基本操作固化成计算引擎,精度要求放宽但极快极省电。三者共存一台机器,就构成了“异构计算系统”。
异构计算系统要高效运转,关键在“调度”:任务分给谁、什么时候分、数据怎么搬、谁先谁后,这些决策都发生在 AI Runtime 系统里。调度得好,工坊产能翻倍;调度不好,设备闲置、搬运拥堵、总时间翻倍。延迟、吞吐、能耗都是调度的核心指标。为了量化一次任务的整体代价,我们常用下面这个时间模型:
认识了工坊里的三位师傅,你才不会把打磨订单错派给木匠。接下来,我们推门走进“调度室”,看看总调度手边的排单系统和工具台长什么样。
- 调度决策耗时 $T_{dispatch}$总调度看订单、想对策、拍板派活的时间,任务越复杂、设备越多,这个时间越长。
- 数据搬运耗时 $T_{transfer}$把原料从仓库搬到对应工位的时间,料越大、通道越窄,搬运越慢。
- 硬件计算耗时 $T_{compute}$工匠真正开干的时间,取决于活儿的总量和师傅的手速。
调度器架构
工坊开工后,你这个总调度不可能一直站在门口发号施令。你需要一间“调度室”:前台接单(任务提交)、排期看板(任务队列)、师傅档案簿(设备资源表)、以及发号指令的传令台(执行引擎)。整个调度室,就是 Runtime 调度器的缩影。
AI Runtime 系统是连接上层框架(PyTorch / TensorFlow)和底层硬件的桥梁。用户编写模型代码时并不关心设备,框架把计算表示成一张计算图,图里的每个节点就是一个“算子及其参数”。Runtime 调度器拿到计算图后,把它们封装成一个个“任务”(也常被称为 kernel / 算子实例),放进任务队列等待派发。
调度器的核心模块包括:任务队列(FIFO 或优先级队列)、设备池(记录每个设备的算力、显存、状态)、调度策略模块(决定下一个任务给谁)和执行引擎(真正把 kernel 提交到设备驱动、管理 Stream 和事件同步)。当一个任务被派给 GPU,执行引擎会调用 CUDA / OpenCL 等驱动 API 将 kernel launch 到设备上,这个过程涉及上下文切换和内核启动开销,调度器必须精打细算。一次调度的完整开销可表示为:
调度室搭好了,接单、看板、设备档案、传令台一应俱全。可总调度每天还得面对同一个灵魂拷问:到底按什么规则派单?这正是下一节的“调度策略”。
- 排队等待 $T_{wait}$任务在队列里排队的时间,队列越拥挤、前面的任务越重,等待越久。
- 派单决策 $T_{dispatch}$调度策略模块根据设备负载、数据位置等信息选择目标设备的时间。
- 内核启动 $T_{launch}$执行引擎把 kernel 提交到驱动并启动硬件执行的时间,通常受 API 调用开销限制。
调度策略
同样是派单,有的总调度喜欢“前一天晚上就把明天的活排好”,有的则“现场看谁闲着就派给谁”。前者是静态调度,后者是动态调度。选择哪种风格,直接决定了工坊应对突发订单的能力。
静态调度(Static Scheduling)在编译期或启动前完成:遍历计算图,测量每个算子在目标设备上的预估耗时、内存占用,然后一次性排定执行顺序。它的优点是运行开销小、确定性强;缺点是一旦运行时的输入 shape、设备负载与预估不符,排好的计划就可能不再最优,甚至引发显存不足。
动态调度(Dynamic Scheduling)在运行期实时决策:每个算子在执行前根据当前设备的空闲率、数据所在位置、优先级等进行判断,选择最快的路径。这天然适合在线推理、多租户等负载波动大的场景。负载均衡策略是动态调度的核心——但请注意,负载均衡不等于任务数量均分,而要权衡设备算力(FLOPS)、带宽、内存、能耗,通常用带权重的贪心选择或最小负载优先算法。设备利用率这个指标常用来衡量负载均衡的好坏:
派单规则定了,原料运送也不能掉链子。订单派给了流水线大师老G,可料还在仓库没搬过去,老G也只能干瞪眼。下一节我们看总调度如何打通“原料运输线”。
- 忙时 $t_i^{busy}$第 i 台设备在时间窗内真正做计算的时间,等待搬运和同步空闲都不算。
- 设备总数 $N$可参与调度的设备数量,例如 1 块 CPU + 1 块 GPU + 1 块 NPU,则 N=3。
- 时间窗 $T_{window}$考察利用率的时间长度,通常取一个性能分析周期。
数据搬运优化
工坊里最让师傅们头疼的,往往不是活难,而是“料没到”。CPU 的“原料仓库”是主存(DDR),GPU 和 NPU 的仓库是各自的显存(HBM),中间只靠一条细细的 PCIe 总线连接。料在仓库搬一趟的时间,可能比师傅干活的时间还长。
异构计算中,数据搬运开销是性能的头号杀手。一个数据搬运周期的耗时可近似为数据量除以总线带宽。以 PCIe 4.0 x16 为例,理论带宽约 32GB/s,而 GPU 内部 HBM 带宽可达 TB/s 级,相差数十倍。如果每次 kernel 执行都先把数据从主存拷入显存、算完再传回,整体吞吐会被严重拉低。
优化手段可归为三大类:第一,减少搬运次数——算子融合(fusing),把多个算子合成一个 kernel,中间结果留在显存里不再回主存;第二,零拷贝 / 统一寻址——通过统一虚拟地址(UVM)、设备直传(P2P)、GPUDirect RDMA 等技术让数据在设备间直通,不经过 CPU 内存中转;第三,计算与通信重叠——用多流(Streaming)将数据预取和计算放到不同流水线上并发执行,使总耗时从加法关系变成取最大值:
原料运输线打通了,工坊速度快了不少。但订单千变万化——有些订单是“先雕花、再打磨、再组装”的连环工序,总调度得把一整条流程拆成一个个算子,动态编排给合适的师傅。这就是“算子调度实践”。
- 计算耗时 $T_{compute}$设备在当前 kernel 上做计算的时间。
- 搬运耗时 $T_{transfer}$与当前计算并发进行的数据预取或回传时间。
- 取最大 $\max$重叠执行时,较长的那一项决定整体耗时,另一项被完全“藏”在它的背后。
算子调度实践
现实中的订单往往不是孤立的活,而是整条流水线:比如一张“AI 雕塑”订单,要经历“切料→粗磨→雕刻→精修→上色”五道工序。对应到计算世界,一个深度学习模型就是一张计算图,图中每个节点(Conv、MatMul、ReLU、Softmax)都是一个算子。
算子调度的第一步是“算子映射”:Runtime 为每个算子选择目标设备上可执行的 kernel 实现。例如矩阵乘在 GPU 上可调用 cuBLAS 库,在 NPU 上可调用 CANN 的 MatMul 算子;某些算子在某设备上没有实现,就需要“回退”到 CPU 执行,或用其他算子组合代替。第二步是“图优化”:在算子执行前,调度器会对计算图做改造——算子融合(如 Conv+BN+ReLU 合并成一个 kernel)、常量折叠(把固定的计算提前算好)、内存复用规划(显存池)等。
第三步是“多流执行”:把没有依赖关系的算子放入不同 Stream,利用设备的多队列并发执行。最终执行时间取决于“关键路径”——即最慢的那条流。硬件适配则要求 Runtime 对不同厂商的 AI 芯片提供统一算子接口,屏蔽底层差异。多流执行的整体耗时可以写作:
掌握了算子级调度,你已经是位经验丰富的总调度了。不过要让工坊营收真正翻倍,还得学会“体检”——找出到底哪个环节在偷懒、哪个环节在堵车。这就是性能调优。
- 流 $s$一条 CUDA Stream(或对应 NPU 的 Task Stream)代表一个逻辑执行序列,不同流之间可以并行。
- 算子耗时 $T_k$流中第 k 个算子的执行时间。
- 流维度的最大值整张图要等所有流都完成,所以总耗时取决于最慢的流。
性能调优实战
总觉得工坊产能不对劲,但说不清是哪个师傅慢、哪条运输线堵,还是派单规则有漏洞。跟经营工坊一样,异构系统的性能调优第一步永远是“先测量、后动手”,而不是盲目改参数。
常用的性能分析方法包括:借助 profiling 工具(NVIDIA Nsight、昇腾 Profiler、Perf)记录每个 kernel 的执行时间、搬移时间、空闲等待时间;通过时间线(timeline)视图,可以直观找出 GPU 利用率的“空洞”在哪里。定位瓶颈的常用手法是分类讨论:计算密集型问题(设备利用率高但耗时仍长)→ 增大并行度或优化算法;搬运密集型问题(大量时间花在数据传输上)→ 算子融合和流水线重叠;调度开销问题(kernel 很小但数量很多)→ 合并小 kernel、减少启动次数。
系统性调优手段可归纳为:提高并行度(增大 batch、多卡并行)、提高数据局部性(缓存友好、减少跨设备访问)、异步化(异步推理、流水线并行)。Amdahl 定律告诉我们,即使把可并行部分做到极致,整体加速比仍然受串行部分瓶颈制约——所以不要忽略调度器本身的开销和同步等待。加速比公式如下:
从认识三位工匠,到搭起调度室,再到练就整套派单、运输、编排、体检的本领——你这家异构工坊终于开始高效运转。剩下要做的,就是把理论搬到真实战场,用代码和工具打磨成工程利器。
- 串行比例 $p$程序中可并行部分占整体时间的比例,那么不可并行部分就是 $1-p$。
- 并行度 $m$并行执行时使用的设备或核心数量,m 越大,并行部分耗时越低。
- 总加速比 $S$优化后的耗时相对原耗时的倍数,由串行部分决定天花板。
AI 应用场景
深度学习训练框架的多流调度
阶段:模型训练阶段的计算编排
解决的问题:训练模型时,CPU 负责数据预处理,GPU/NPU 负责张量计算。传统同步执行会让加速器频繁空转等待数据。Runtime 调度器把数据增强、前向传播、反向传播算子放入不同流,按依赖关系动态派发,并用数据预取和梯度累积填充设备空闲时间。
具体案例:PyTorch 在 GPU 上训练 CNN 时,DataLoader 通过多进程预取数据,调度器将 Forward/Backward 算子下发到 GPU 流,CPU 同步执行数据增强;若使用昇腾 NPU,CANN 的 Runtime 同样会做算子调度与流同步。这种编排通常能让训练吞吐相比朴素逐算子同步执行提升 2-5 倍。
大模型推理服务的动态路由
阶段:在线推理服务部署与请求调度
解决的问题:在线 LLM 服务中,prefill 阶段计算密集,适合 NPU/GPU;decode 阶段访存密集且延迟敏感。如果所有请求固定走一条路径,会互相拖慢,且设备利用率不均。
具体案例:vLLM 等推理引擎实现 continuous batching,Runtime 调度器根据当前请求的 KV Cache 占用和设备负载,动态把请求组装成 batch 分配到不同设备;CPU 专门负责 tokenizer、采样与后处理,NPU 做矩阵运算,GPU 处理并行度高的解码计算。端到端吞吐相比静态 batch 方案提升约 2-3 倍。
Python 演示
"""异构计算调度模拟器模拟标量/矩阵/神经网络三类任务在 CPU/GPU/NPU 上的调度与执行。""" class Task: def __init__(self, tid, kind, workload): self.tid = tid self.kind = kind self.workload = workload self.device = None self.remain = workload class Device: def __init__(self, name, speed, kinds): self.name = name self.speed = speed self.kinds = kinds self.running = None def can_run(self, kind): return kind in self.kinds def start(self, task): self.running = task task.device = self.name def tick(self, dt): if self.running: self.running.remain -= self.speed * dt if self.running.remain <= 0: self.running.remain = 0 self.running = None def idle(self): return self.running is None class Scheduler: def __init__(self, devices): self.devices = devices self.queue = [] def submit(self, task): self.queue.append(task) def dispatch(self): for task in list(self.queue): for device in self.devices: if device.can_run(task.kind) and device.idle(): device.start(task) self.queue.remove(task) break def run(self, dt=0.1): t = 0.0 while self.queue or any(not d.idle() for d in self.devices): self.dispatch() for device in self.devices: device.tick(dt) t += dt return t def main(): cpu = Device("CPU", 1.0, {"scalar"}) gpu = Device("GPU", 8.0, {"matrix", "scalar"}) npu = Device("NPU", 16.0, {"neural", "matrix"}) scheduler = Scheduler([cpu, gpu, npu]) tasks = [ Task(0, "scalar", 4.0), Task(1, "matrix", 40.0), Task(2, "neural", 32.0), Task(3, "matrix", 24.0), Task(4, "scalar", 2.0), Task(5, "neural", 8.0), ] for task in tasks: scheduler.submit(task) total = scheduler.run() print(f"总耗时: {total:.1f} 秒") for task in tasks: print(f"任务{task.tid}({task.kind}) -> {task.device}") if __name__ == "__main__": main()
cpu = Device("CPU", 1.0, {"scalar"})定义设备算力和支持的任务类型,CPU 只跑标量任务。if device.can_run(task.kind) and device.idle():调度核心判断:设备既支持该类型又空闲才派活。task.remain -= self.speed * dt每个模拟步按设备算力扣减任务剩余量,算力越快完成越快。device.start(task)把任务分配给设备,并记录任务落在了哪个设备上。while self.queue or any(not d.idle() for d in self.devices):只要队列非空或有设备还在忙,调度就继续推进。total = scheduler.run()启动整个调度模拟,返回所有任务完成的总时间。
常见误区
误区AI 计算不就是 GPU 吗?NPU 和 GPU 差不多,反正都是做并行计算的加速芯片,选哪个都行。
正解GPU 为图形渲染和大规模并行而设计,擅长矩阵乘法等通用并行计算,但通用计算仍要遵循 SIMT 编程模型;NPU 则把深度学习的卷积、矩阵乘、激活等算子直接固化为专用计算引擎,在低精度、高能效比上有优势,但通用性远不如 GPU。选型要结合算子类型、精度要求和部署场景,不能一概而论。
为何常见:两者都常被当作“AI 加速器”出现在宣传中,且都能跑神经网络,容易让人忽略架构差异。
误区把所有任务平均分给 CPU、GPU 和 NPU,每个设备各干各的,这样总该叫负载均衡了吧?
正解负载均衡的目标是让各设备利用率趋近一致,而不是让任务数量相等。一个矩阵乘任务在 CPU 上要 10 秒、在 GPU 上只要 1 秒,平均分配只会让 CPU 成为瓶颈。正确做法是按设备算力加权分配,并根据运行时的利用率数据动态调整,让快设备承担更多任务。
为何常见:日常生活中的“平均分”最直观公平,但忽略了异构设备在能力上的巨大差异。
误区现在都支持统一虚拟内存了,数据会自动搬来搬去,零拷贝早就由硬件实现了,用户不用管。
正解统一虚拟地址(如 CUDA UVM)只是在软件层屏蔽了地址空间差异,底层仍会发生按页迁移,缺页时同样有巨大开销。真正的零拷贝需要主动优化:用 cudaMemPrefetchAsync 预先将数据放到目标设备,用 P2P 让 GPU 之间直传,再用多流把搬运和计算重叠,才能把搬运开销“藏”起来。
为何常见:厂商强调“统一内存”的易用性,让初学者误以为数据位置和搬移开销从此不再是问题。
误区调用 kernel 的 API 立刻返回了,说明 GPU 已经开始算了,异步操作不用再管同步。
正解异步只是把 launch 指令提交到驱动队列后立即返回,kernel 可能还排在队列里未执行。如果后续算子依赖前一个算子的结果,必须通过 Stream 事件或 cudaStreamSynchronize 做同步;否则会出现数据未就绪就执行,导致错误或隐性的性能等待。
为何常见:看到 API 立即返回,误以为硬件已经执行完毕;实际“提交”和“执行完成”是两件完全不同的事。
面试题
Q1. 在异构计算系统中,最适合执行大规模矩阵乘法任务的计算设备是?
- A.CPU中央处理器
- B.GPU图形处理器← 正确答案
- C.NPU神经网络器
- D.固态硬盘存储
答案:B。矩阵乘法是典型的大规模并行密计算,GPU 拥有数千个核心,是执行这类任务的主流选择。CPU 核心少更擅长复杂控制流,NPU 更偏 AI 模型整体加速但通用性不如 GPU,固态硬盘则根本不是计算设备。
Q2. NPU 最适合加速哪一类负载?
- A.网页渲染请求
- B.磁盘文件读写
- C.神经网络推理← 正确答案
- D.操作系统核心
答案:C。NPU(神经网络处理器)把卷积、矩阵乘、激活等算子固化为专用引擎,天然为深度学习的训练和推理优化。网页渲染更依赖 GPU 的并行管线,磁盘读写是存储控制器的职责,操作系统核心主要运行在 CPU 上。
Q3. 以下哪项是调度器的主要职责?
- A.编译高级代码
- B.分配计算任务← 正确答案
- C.设计神经网络
- D.训练数据标注
答案:B。调度器负责把计算图中封装的算子任务分配到合适的设备上执行,包括排队、决策和 kernel 启动。编译高级代码是编译器的工作,设计网络结构是算法工程师的工作,数据标注更是完全不同的环节。
Q4. 静态调度的核心特点是什么?
- A.运行期实时决策
- B.编译期预先决定← 正确答案
- C.依赖人工干预
- D.无需任何信息
答案:B。静态调度在编译期或程序启动前,根据计算图、算子预估耗时和内存占用一次性排定执行顺序,运行开销小、可确定性强。运行期实时决策是动态调度的特征,人工干预和无需信息都不符合静态调度的工程实质。
Q5. 数据搬运开销大的根本原因是什么?
- A.设备算力太低
- B.总线带宽有限← 正确答案
- C.任务数量太多
- D.编译器效率差
答案:B。CPU 主存与 GPU/NPU 显存之间通过 PCIe 等总线相连,总线带宽远低于设备内部的存储带宽,导致搬运大数据成为瓶颈。算力低、任务多、编译器效率差都不是数据搬移慢的根本原因。
Q6. 使用多个 CUDA Stream 的主要目的是?
- A.降低编程难度
- B.实现任务并发← 正确答案
- C.减少代码体积
- D.提升网络带宽
答案:B。多个 Stream 可以让无依赖关系的 kernel 和数据拷贝在不同队列上并行执行,从而实现计算与通信重叠、提高设备利用率。编程难度不会因此下降,代码体积和网络带宽与 Stream 数量没有直接关系。
Q7. 以下哪项属于负载均衡的关键目标?
- A.任务数量完全相等
- B.设备利用率接近← 正确答案
- C.所有设备全速运行
- D.单设备算力最大化
答案:B。负载均衡追求各设备利用率尽量一致,这样系统总吞吐最高。任务数量相等可能让快设备吃得太少、慢设备排队;所有设备全速运行只是美好愿望,实际受依赖关系约束;单设备算力最大化是硬件设计目标,与调度均衡无关。
Q8. Amdahl 定律说明的核心结论是?
- A.并行核心越多越好
- B.串行部分限制加速← 正确答案
- C.内存越大性能越高
- D.编译优化可达十倍
答案:B。Amdahl 定律指出加速比上限由串行部分比例决定,即使并行部分无限加速,总体加速比也有天花板。并行核心并非越多越好,内存容量和编译优化都不在 Amdahl 定律的讨论范围内。
Q9. 计算与通信重叠后,总耗时近似等于?
- A.计算时间加通信时间
- B.计算和通信的较大者← 正确答案
- C.通信时间的二倍
- D.计算时间的两倍
答案:B。当数据搬运和计算分别位于不同流水线时,两者并发进行,整体时间由较长的一方决定,即取 max(计算,搬运)。相加关系是未重叠时的情形,二倍的说法没有依据。
Q10. 常见的零拷贝优化手段包括哪些?
- A.统一虚拟地址空间← 正确答案
- B.使用缓存一致协议
- C.编程语言运行时
- D.设备间直连传输← 正确答案
答案:AD。统一虚拟地址(如 CUDA UVM)可减少显式拷贝,设备间直连传输(P2P、GPUDirect RDMA)让数据不经 CPU 中转,两者是典型零拷贝手段。缓存一致性协议属于硬件内存模型范畴,编程语言运行时与零拷贝无直接关系。
Q11. 动态调度相比静态调度的优势包括?
- A.可感知负载变化← 正确答案
- B.无需编译期分析← 正确答案
- C.降低运行时开销
- D.支持突发任务到达← 正确答案
答案:ABD。动态调度在运行期实时决策,能感知设备负载和任务突发,且不需要完整的编译期 profiling 分析。但它的劣势恰是运行时开销更大,C 选项把劣势说成优势,属于黑话陷阱。
Q12. 关于算子融合的意义,下列说法正确的是?
- A.减少 kernel 启动次数← 正确答案
- B.消除中间结果回传← 正确答案
- C.一定降低计算精度
- D.缩短数据搬运总量← 正确答案
答案:ABD。算子融合把多个算子合成一个 kernel,减少了启动次数,中间结果留在片上显存而不回主存,从而缩短搬移总量。融合本身不改变精度,C 说法错误。