想象一家巨大的中央厨房:灶台、烤炉、蒸柜分散在好几个房间,食材存放在距离各不相同的仓库里。一位聪明的总厨接单后,绝不会随便找个灶台就开火,而是先看清食材在哪个备料区,再把菜派给离食材最近的那口灶台。这种“让食材和灶台尽量靠近”的排兵布阵,就是后厨里的亲和性。今天我们要聊的“设备亲和性(Device Affinity)”,正是异构计算世界里那位总厨的看家本领——在CPU、GPU、NPU和内存构成的复杂厨房里,把任务、数据、设备之间的关系摆得明明白白,让每一道“AI大菜”少跑冤枉路、快出桌。我们将从概念、原因、类型、运行机制、性能、优化一路讲到工程实践和常见陷阱,把这张后厨动线图完整铺开。
对AI从业者而言,掌握设备亲和性,就是掌握异构算力调度的底层直觉。它决定你写的模型是“跑得快”还是“被搬运拖垮”,是编译器、Runtime和平台工程师都必须吃透的核心基本功。
可视化演示
展示异构计算设备亲和性:CPU亲和性掩码绑定、NUMA内存本地性优化,以及GPU MIG实例划分与共享。
本节配有交互动效(进入视口后自动加载,约 3KB)。
CPU亲和绑定 sched_setaffinity设置掩码 mask=CPU_SET 调度器只在掩码指定核心内运行任务。
基本概念
想象一家巨型中央厨房:灶台(GPU)、蒸柜(NPU)、冷柜(CPU内存)分布在不同的房间。总厨接单后从不随手找灶台,而是先看食材放在哪个备料区,再把菜派给离食材最近的那口灶台。这种“人和料尽量在一起”的安排,就是后厨里的亲和性。
在AI异构计算系统里,“食材”是张量数据,“灶台”是GPU/NPU等计算设备,“备料区”是设备显存,“仓库”是主机内存。任务运行前要把数据从仓库搬到灶台旁。如果任务与它依赖的数据被派到不同设备,就会产生跨设备搬运。设备亲和性(Device Affinity)的核心,就是让任务与数据、上下文、周边资源尽量落在同一个设备视野内,使数据在本地可见、可直接访问。
AI Runtime负责把高层算力请求落到物理设备。调度器会根据在线设备列表、每台设备的显存容量、拓扑距离等信息,给算子挑一个“最亲”的设备。这并非单个开关,而是一套自上而下的策略:设备选择、内存分配、Stream绑定、算子排列都要全局考虑。一条朴素公式概括了它的意义:
总厨的排班表上,一道菜的总时间等于炒菜加取料。食材在手边时搬运为零;食材在走廊尽头时,灶台再旺也只能干等。这就是设备亲和性要解决的第一性问题。
- 总时间$T_{\text{total}}$ 表示一个任务从启动到完成的端到端时间,也就是“客人等上菜”的时长。
- 计算时间$T_{\text{compute}}$ 是设备上真正做浮点运算的时间,由算法与硬件决定。
- 搬运时间$T_{\text{transfer}}$ 是数据在主机与设备之间搬来搬去的时间,亲和性优化的正是这一项。
产生原因
中央厨房的备料区与灶台之间,隔着一条仅供一辆餐车通过的走廊。走廊看着不长,但车流密集,运一桶高汤还好,运一整头牛就得耗上半天。这条走廊,就是主板上的PCIe总线。
AI计算中,GPU/NPU不能直接访问主机内存,必须先通过PCIe等链路把数据复制到自己的显存(Host-to-Device),算完再复制回去(Device-to-Host)。大模型训练中,每一轮迭代都搬运GB级别的梯度与中间结果,搬运开销与计算量同数量级,一点不能小看。
搬运贵在带宽有限。PCIe 4.0 x16的峰值带宽约32GB/s,而GPU自身显存带宽可达TB/s,两者差两个数量级。设备亲和性就是尽量少走上这条走廊、少搬大件货。搬运时间的数学模型非常直观:
主厨最怕的不是灶台不够,而是餐车在走廊上排长队。想快,要么减少取料次数,要么把料提前放灶台脚下——这正是亲和性存在的全部理由。
- 搬运时间$T_{\text{transfer}}$ 是一次数据复制所消耗的时间,单位通常是毫秒。
- 数据量$D$ 是需要复制的字节数,比如一批激活值或梯度张量。
- 链路带宽$B_{\text{link}}$ 是总线实际达到的有效带宽,表示每秒能搬多少字节。
类型划分
后厨里的“近”有很多种:专用炒锅放在炒灶上是“灶具亲和”;粤菜师傅的工位正对海鲜池是“人料亲和”;冷藏库出入口靠近备菜台是“路线亲和”。它们都说明一个道理:距离有多种维度,每种距离都影响效率。
设备亲和性同样分层次:资源亲和指计算设备之间的拓扑关系,比如某张GPU插在哪个PCIe槽位、能否直连相邻GPU;数据亲和指任务运行的设备恰好持有它需要的数据;执行亲和指线程或进程绑定到指定CPU核,避免被操作系统搬来搬去。
服务器中最常见的是NUMA亲和(Non-Uniform Memory Access,非一致内存访问)。一台服务器有多个NUMA节点,每个节点拥有本地内存。CPU访问本地内存快、访问远端内存慢,差距可达数倍。AI训练服务器中,GPU通常挂载在某个NUMA节点下,如果数据落在别的节点,GPU访问它就得绕路。调度时把线程、GPU和主机内存在NUMA层面对齐,就是典型的亲和对齐。平均访存时间可以这样估算:
主厨看一份菜谱,脑子里同时算料在哪、人在哪、锅在哪。调度器做的也是同一件事:把任务、数据、设备在拓扑图上摆到最近的位置,让命中率 $p$ 无限逼近1。
- 平均访存时间$T_{\text{avg}}$ 是任务访问数据的平均延迟,越小越好。
- 本地命中率$p$ 是数据命中本地内存或显存的比例,亲和性越高 $p$ 越大。
- 两种延迟$T_{\text{local}}$ 是本地访问延迟,$T_{\text{remote}}$ 是跨节点访问延迟,后者远大于前者。
运行机制
主厨有本排班表:每道菜在哪个灶台做、在哪个备料位取料,提前写死,烹饪中途绝不换灶。换灶意味着把半成品从一个工作台搬到另一个,锅要重新热,锅气全没了。
AI Runtime也有一本排班表。在CUDA Runtime或AscendCL中,每个线程都有“当前设备”(Current Device)概念。调用设备绑定API后,后续的显存分配、内核启动、Stream操作都自动发往该设备。Stream(流)是命令的传送带,任何Stream都隶属于创建它的设备,天然绑定。
如果任务要切换设备,线程必须先同步旧设备上的所有Stream,等待未完成的内核结束,再切换上下文,在新设备上重建Stream和内存句柄。切换成本毫秒级,但频繁切换会让大量时间耗在“交接”上。设备亲和性要求调度器为任务分配设备后保持“从一而终”:
好的排班表让师傅在同一口锅前坐满全场;糟糕的排班表让师傅端着半成品满后厨跑。设备亲和性提供的,正是那本“从一而终”的排班表。
- 切换总开销$T_{\text{switch}}$ 是一段时间内因设备切换损失的时间。
- 切换次数$N_{\text{switch}}$ 是任务在各设备间的跳转次数,调度策略直接决定它。
- 单次切换成本$T_{\text{context}}$ 是重建上下文的固定开销,$T_{\text{sync}}$ 是等待旧设备排空的同步时间。
性能影响
账本是主厨的信誉。每道菜从接单到上桌,前台都在计时。有些菜明明烹饪只要三分钟,却在走廊上耗了两分钟,账本上的总时间难看至极。
设备的账本就是端到端延迟与吞吐率。亲和性差时,数据搬运时间直接叠在每次计算前,让算子实际完成时间远超理论值。更糟的是,频繁小块搬运不仅带宽利用率低,还会被启动延迟主导,放大开销。数据显示,一次1KB的小传输耗时可能是大块传输的数百倍,而搬运有效带宽会急剧缩水:
小碎片搬运就像一趟趟只送一碟菜,每次上下楼,账本自然难看。主厨的经验是:要么一次端一大盘,要么干脆提前备好。亲和不只是少搬,还包括搬大块、搬连续。
- 有效带宽$B_{\text{eff}}$ 是扣除启动延迟后,用户实际感知的搬运速度。
- 数据量$D$ 是本次搬运的字节数,块越大、分摊的延迟越多。
- 启动延迟$T_{\text{latency}}$ 是建立一次传输的固定开销,无论多小都要付。
- 峰值带宽$B_{\text{peak}}$ 是链路理想限速,对应走廊的最高限速。
优化策略
主厨的工具箱里有四样东西:提前备菜(数据预置)、固定工位(静态绑定)、专用传菜电梯(P2P直连)、边炒边备(通信计算重叠)。这些招术不是魔法,而是对动线的极致规划。
对应到AI Runtime,静态绑定用绑核与环境变量把任务钉在特定设备;动态调度根据当前负载把任务分给“最近”的设备;数据预置把热点数据常驻显存;P2P直连在支持直通的设备间建起点对点通信,绕过主机内存;通信计算重叠则把搬运与计算在时间上错开,是最漂亮的一招。
在流水线并行训练中,第i层的梯度搬运可以与第i+1层的计算同时发生。理想情况下,总时间不再是搬运加计算,而是取两者最大值:
主厨装上传菜电梯后,备菜和炒菜同时进行,客人不再等“先送菜再下锅”。设备亲和性优化的最高境界,就是让人感觉不到搬运的存在。
- 流水线总时间$T_{\text{pipeline}}$ 是采用重叠后的总耗时。
- 计算时间$T_{\text{compute}}$ 是设备实际运算时间,与搬运并行进行。
- 搬运时间$T_{\text{transfer}}$ 是数据搬运时间,只要比计算短就不拖后腿。
- 取最大值两者并行时,总时间由较长一方决定,较短的一方被“藏”进长的一方。
实践案例
某天后厨接到婚宴大单:几十桌同时开席,多口灶台并行,每桌菜品还要节奏一致,不能这桌上完、那桌还在备料。主厨必须提前排好每桌对应的灶台与食材动线。
今天的大模型训练与在线推理,就是这张婚宴大单。多卡训练的同步梯度与AllReduce通信每轮几十GB;在线推理要在几十毫秒内完成一个请求的运算;多租户平台还要在数百设备中调度任务。设备亲和性在这些场景中不是优化项,而是能不能跑起来的先决条件。
大模型预训练常用张量并行加流水线并行,调度器会按机内拓扑,优先把同一并行组的卡放在同一台服务器,甚至同一交换机域,因为NVLink带宽远高于跨机网络。在线推理要求请求与模型权重、KV Cache在同一个设备上长期共建,避免每次请求都从主机拉权重。平台级调度器则把设备亲和作为打分函数,按数据位置、设备空闲度、拓扑距离为候选设备排序。并行效率的公式揭示了亲和的收益:
婚宴办下来,主厨最大的心得是:桌数越多,食材越要提前各就各位。多卡训练规模越大,设备亲和性的价值就越突出。
- 并行效率$E$ 表示增加设备后加速效果打了多少折扣,理想值是1。
- 实际加速比$S_{\text{speedup}}$ 是N个设备相对单设备的提速倍数。
- 设备数量$N_{\text{devices}}$ 是参与并行的设备总数。亲和差会导致通信等待变长,使加速比远小于设备数。
常见陷阱
经验再足的主厨也会翻车:以为食材在冷藏库,打开发现被保洁挪走了;以为传菜电梯直达就快,结果电梯要绕行十五层;硬把生食熟食放同一冰箱,互相串味。
设备亲和性在真实系统里同样有“想当然”的坑。以为调用某绑定接口后数据就自动在设备上,实际却发生了隐式拷贝;以为支持P2P的设备间通信一定比走主机快,实际跨NUMA或PCIe拓扑复杂时反而更慢;以为统一内存把CPU和GPU内存统一了,首次访问仍可能按页触发拷贝。
最典型的翻车是隐式拷贝:程序员只绑定了设备索引,数据却仍由普通API分配在主机内存,每次内核运行时Runtime都偷偷搬数据。这种隐藏搬运不报错、不警告,只会让性能悄然劣化。另一个是索引错觉:用环境变量屏蔽部分GPU后,物理设备号与逻辑设备号错位,调度器按逻辑号派任务,把任务派到了数据不在的物理设备,亲和策略形同虚设。隐藏代价可表达为:
主厨最后总结:别信记忆里的食材位置,开动前先亲眼确认料在哪、通路通不通。设备亲和性的工程落地,靠的是调度前就把数据位置和设备拓扑摸得清清楚楚。
- 隐藏惩罚$T_{\text{penalty}}$ 是隐式拷贝悄悄加入总耗时的时间。
- 意外搬运量$D_{\text{unexpected}}$ 是调度器没预料到、运行时却偷偷搬的数据量。
- 共享带宽$B_{\text{shared}}$ 是隐藏路径的带宽,它和显式搬运共用同一条走廊。
AI 应用场景
大模型多卡训练通信优化
阶段:模型训练 / 算力调度
解决的问题:传统的集合通信(AllReduce)若把并行组跨机分配,梯度同步要走网络,延迟和带宽均不可控。AI Runtime通过亲和调度,把并行组限制在同一台服务器内,让同步走NVLink等设备直连链路,大幅降低通信占比。
具体案例:某大模型预训练任务原本8卡并行组被分散在两台服务器,AllReduce走跨机RoCE网络,通信占比达35%。调度器改为按拓扑亲和分配,将8卡放入同一台8卡服务器,梯度同步走NVLink,通信占比降到12%,训练吞吐提升近三成。
在线推理服务p99延迟优化
阶段:模型推理 / 部署优化
解决的问题:请求到达时若模型权重不在设备显存,每次都要从主机内存拉取,且推理线程所在的CPU核若与GPU不在同一NUMA节点,DMA访问会绕远路。设备亲和性把权重、KV Cache常驻显存,并把线程绑定到GPU所在NUMA节点,直接压住长尾延迟。
具体案例:某LLM推理服务在亲和调度前p99延迟为320ms,主要开销来自每次请求的权重搬运与跨NUMA访问。优化后,模型权重常驻显存、线程绑定到对应NUMA节点CPU核,p99降至180ms,且吞吐提升约40%。
多租户异构算力平台调度
阶段:资源管理与任务调度
解决的问题:多团队共享数百台异构设备时,若随意分配设备,任务的数据集常散落在别的机器,造成大数据量跨机拉取。平台调度器记录每个用户最近使用过的设备与缓存数据集,将有数据在场的设备作为打分第一优先级,实现数据亲和。
具体案例:某智算平台引入“数据位置感知”评分后,缓存命中率从40%升至85%,任务平均排队等待时间减半,跨机数据集传输量下降了60%以上。
Python 演示
# -*- coding: utf-8 -*-"""模拟设备亲和性调度对总耗时的影响"""import random class Device: def __init__(self, name, bandwidth): self.name = name # 设备名 self.bandwidth = bandwidth # 拷贝带宽 class Job: def __init__(self, name, data_gb, data_device, compute_s): self.name = name # 任务名 self.data_gb = data_gb # 需要的数据量 self.data_device = data_device # 数据所在的设备号 self.compute_s = compute_s # 计算耗时 def transfer_cost(job, dev): return job.data_gb / dev.bandwidth def run_naive(jobs, devices): total = 0.0 for job in jobs: dev = devices[0] # 总是派给0号设备 if job.data_device != 0: total += transfer_cost(job, dev) total += job.compute_s return total def run_affinity(jobs, devices): total = 0.0 for job in jobs: dev = devices[job.data_device] # 数据在哪,任务就放哪 total += job.compute_s # 数据已在本地,搬运成本为0 return total def main(): random.seed(42) devices = [Device("gpu0", 20.0), Device("gpu1", 20.0), Device("gpu2", 20.0)] jobs = [] for i in range(30): data_gb = round(random.uniform(0.5, 4.0), 2) data_device = random.randint(0, 2) compute_s = round(random.uniform(0.2, 1.5), 2) jobs.append(Job("job%d" % i, data_gb, data_device, compute_s)) naive = run_naive(jobs, devices) affine = run_affinity(jobs, devices) print("naive total: %.2f s" % naive) print("affinity total: %.2f s" % affine) print("speedup: %.2f x" % (naive / affine)) if __name__ == "__main__": main()
dev = devices[0]朴素调度永远把任务丢给0号设备,不管数据在哪里。dev = devices[job.data_device]亲和调度根据数据所在设备号选择执行设备,这是核心差异。if job.data_device != 0: total += transfer_cost(job, dev)数据不在目标设备时要付出搬运时间。total += job.compute_s无论哪种调度,计算时间都必不可少。random.randint(0, 2)模拟数据随机散布在三台设备上,更接近真实异构场景。
常见误区
误区我觉得只要在代码里调用设备绑定接口,把当前设备设为GPU,数据就一定在GPU显存里了,不会再发生主机到设备的拷贝。
正解设备绑定只是给后续操作指了个方向,它不影响已分配内存的位置。如果数据是用普通主机内存API分配的,内核启动时Runtime仍会执行隐式拷贝。要实现真正的亲和,必须用设备侧内存分配接口,并显式把数据创建在目标设备上。
为何常见:API名称和“当前设备”的概念容易让人误以为绑定即是分配,忽略了内存归属是独立于设备上下文的一层。
误区GPU显存只要放得下模型,性能就没问题,设备亲和性根本不重要。
正解显存容量解决“能不能放下”,不解决“数据要不要搬”。训练时每轮迭代都有中间结果、梯度与主机交换,即使显存足够,亲和性差仍会产生大量搬运延迟。容量是空间问题,带宽是时间问题,两者不能混为一谈。
为何常见:大家习惯用容量衡量设备强弱,忽略了通信带宽这个独立的性能维度。
误区两张GPU都支持P2P,多卡通信时无脑开启P2P就行了,肯定比先拷到主机再拷过去快。
正解P2P快的前提是两张卡在同一PCIe拓扑的直连通道上。跨NUMA、跨PCIe交换机时,P2P可能绕远路,实际速度反而不如走主机内存中转。开启前必须检查peer access是否真正生效,并用基准测试验证。
为何常见:看到硬件规格写着“支持P2P”就认为一定加速,忽略了拓扑距离这个决定因素。
误区AI计算都用GPU,CPU的NUMA节点跟性能没什么关系,不用关心。
正解GPU驱动、DMA引擎和主机内存在NUMA拓扑上都有归属节点。GPU挂在某个NUMA节点下,如果host内存落在其他节点,PCIe访问和DMA都会绕远,数据搬运开销显著增加。绑核时也应优先选择GPU所在NUMA节点上的CPU核。
为何常见:注意力都集中在GPU上,忽略了GPU与CPU之间通过主机总线交互的客观路径。
面试题
Q1. 设备亲和性在异构调度中的核心目标是?
- A.把所有算子集中到同一设备上运行
- B.让任务和数据尽量靠近以减少搬运← 正确答案
- C.提高每个设备的峰值计算频率
- D.减少设备数量降低硬件采购成本
答案:B。设备亲和性的核心是减少跨设备数据搬运。A违背了异构设备分工原则,C、D与“亲和”无关,只有B抓住了“就近”的本质,正如主厨让食材靠近灶台。
Q2. 主机与GPU之间数据拷贝的主要瓶颈通常是?
- A.GPU显存带宽过高导致数据等待
- B.CPU核心频率不足以驱动拷贝引擎
- C.PCIe等链路带宽远低于显存带宽← 正确答案
- D.主机内存容量小导致频繁换页
答案:C。显存带宽高达TB/s,而PCIe链路只有几十GB/s,走廊比灶台窄得多,搬运速度受制于最窄环节。A、B、D都不是数据传输链路的真实瓶颈。
Q3. NUMA节点的“本地内存”指的是?
- A.距离当前CPU插座最近的那块物理内存← 正确答案
- B.与当前GPU共享同一散热器片的内存
- C.容量最大且价格最低的那部分内存
- D.虚拟内存中经常被访问的热点页面
答案:A。NUMA将CPU与内存分组,本地内存归同组CPU独占访问、延迟最低。B、C、D都不符合NUMA硬件的物理划分,只有A描述了“本地”的含义。
Q4. 下列措施中能提升设备亲和性的有?
- A.将任务绑定到数据所在设备的执行流← 正确答案
- B.把热点数据常驻设备显存避免反复搬运← 正确答案
- C.尽可能增加调用设备切换接口的次数
- D.按拓扑就近分配并行任务到同一服务器← 正确答案
答案:ABD。A让任务与数据在同一设备上执行,B通过常驻减少搬运,D按拓扑就近分配,都是亲和性的典型手段。C频繁切换设备恰恰破坏亲和,会带来巨大的同步与上下文开销。
Q5. 在CUDA Runtime中,Stream与设备的关系是?
- A.Stream可以跨设备并发执行同一批内核
- B.每个Stream独立地绑定一个逻辑设备编号
- C.Stream创建后隶属于当前操作所在设备← 正确答案
- D.Stream只负责主机端内存的异步拷贝
答案:C。Stream是命令的传送带,创建时被打上设备烙印,后续所有命令都发往该设备。A跨设备、B手动绑定、D只做主机拷贝,都与Stream的实际语义不符。
Q6. 有效带宽公式中,小数据块搬运效率低的主要原因是?
- A.峰值带宽随数据块变小而自动降低
- B.固定启动延迟在总耗时中占比变大← 正确答案
- C.数据块小导致缓存命中率明显下降
- D.传输协议要求更大的数据包头开销
答案:B。有效带宽的分母是启动延迟加传输时间,数据块越小时启动延迟占比越高,所以实测带宽很低。这也解释了为什么亲和调度会合并小搬运为大块搬运。
Q7. 大模型多卡训练中,以下做法能改善通信与计算效率的是?
- A.把AllReduce并行组限制在同一台服务器内← 正确答案
- B.用流水线方式重叠梯度搬运与下一层计算← 正确答案
- C.每次迭代前将模型权重一律重新载入显存
- D.选择支持设备直连的拓扑下发梯度集合通信← 正确答案
答案:ABD。A利用机内NVLink高带宽,B让搬运与计算并行,D走设备直连绕开主机内存,都能改善效率。C每次重新载入权重会增加大量搬运,违背亲和的初衷。
Q8. 使用统一内存(Unified Memory)时,隐式拷贝最可能的场景是?
- A.数据首次在GPU内核中被访问时触发按页迁移← 正确答案
- B.主机内存不足时自动扩展到硬盘交换分区
- C.GPU显存已满时自动压缩已缓存的数据页
- D.调度器把未使用数据主动回收并释放显存
答案:A。统一内存的典型行为是“按需分页迁移”,GPU首次访问某页数据时触发缺页搬运。B是swap,C是压缩,D是回收,都与隐式拷贝的触发机制无关。
Q9. 设备亲和性优化后,以下哪些指标通常会明显改善?
- A.端到端训练迭代时间显著缩短← 正确答案
- B.传输链路有效带宽利用率提升← 正确答案
- C.隐式拷贝的次数一定降为零
- D.跨设备数据搬运的延迟占比下降← 正确答案
答案:ABD。优化亲和能缩短迭代时间、提高带宽利用率、降低搬运延迟占比。但“隐式拷贝降为零”过于绝对,工程中仍可能存在个别路径触发隐藏拷贝,不能保证完全消除。
Q10. 推理阶段请求频繁在设备间迁移时,哪种做法对p99延迟最有利?
- A.每请求按当前最小负载选择任意空闲设备
- B.权重与缓存常驻设备且请求绑定固定归属卡← 正确答案
- C.将权重存主机内存由各卡按需拉取共享
- D.增加设备切换次数以均衡各卡的计算热点
答案:B。p99延迟关注最坏情况,A的负载均衡策略会让请求随机落在没有权重的设备上,C每次拉取权重,D切换次数增多。只有B让权重与KV Cache常驻并固定归属,确保每个请求的数据都在本地,长尾延迟最可控。