ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

多租户NPU隔离:PASID、IOMMU与硬件强制隔离实践

多租户NPU隔离:PASID、IOMMU与硬件强制隔离实践

多租户 NPU 隔离:从 PASID、IOMMU 到硬件强制隔离的完整实践

做 AI 基础设施的同学,这两年一定对“多租户 NPU 隔离”这个词不陌生。算力池化、AI 云、训练推理混部,哪个场景都绕不开它。我在实际落地一套 NPU 集群的时候,最大的体会是:软件层的隔离方案看起来很美好,一压测就原形毕露。真正扛住多租户场景的,还得是 PASID、IOMMU 这一层硬件机制,以及从设备侧到内存侧完整的强制隔离设计。

这篇文章我不打算写成手册式文档,而是按我实际趟坑、选型、调试的路径,讲清楚多租户 NPU 隔离背后的核心机制:PASID 怎么为每个租户打标,IOMMU 怎么做地址隔离和 DMA 重映射,硬件强制隔离又强在哪里、有什么代价。适合正在做 NPU 驱动、异构算力虚拟化、AI 平台资源隔离的开发者参考。凡是涉及具体型号和参数的地方,我尽量给出通用依据和实测数据,方便你迁移到自己手头的环境。

1. 多租户 NPU 隔离到底难在哪

很多人第一次接触 NPU 虚拟化,会下意识觉得:不就是给租户分算力吗?按比例切计算核心、限制内存配额,听起来不复杂。但真正上手之后会发现,NPU 和 CPU 虚拟化完全不是一个量级的问题。NPU 是典型的 DMA 密集型设备,计算核心自己会发起大量访存请求,同时内部还有多级缓存、片上 SRAM、甚至多个计算核并行访问。一旦多个租户共享同一块 NPU,哪怕计算任务调度得再好,只要地址空间没有隔离,A 租户的 DMA 就可能读到 B 租户的训练数据。

这里先说个结论:多租户隔离的核心,不是“能不能分”,而是“分了之后,内存访问能不能强制隔离、错误能不能限定在单一租户边界内”。

1.1 软件隔离为什么在 NPU 场景下失效

有人会问:我在 NPU 驱动里给每个租户维护一份独立的地址映射,软件层把物理地址算好,再把结果告诉设备,这样不行吗?

行,但只是看起来行。问题出在三个层面。

第一,NPU 的计算核心不是简单的“取指令-执行-访存”模型,它是高度并行的数据流引擎。一个计算任务里动辄上千个并发线程同时访问内存,驱动不可能实时校验每一条访存请求的合法性。只要有一条 DMA 请求的地址映射算错,或者边界条件漏判,数据就可能跨租户泄露。

第二,软件层的权限校验依赖 CPU 执行路径。只要路径上有 bug、有竞态、有被绕过的方式,隔离就不可信。安全界有个基本共识:凡是能在硬件层面强制执行的边界,就不要依赖软件自觉。

第三,性能损耗。软件层每次访存都做一次页表和权限校验,这个开销在 CPU 侧还能接受,在 NPU 这种高带宽访存场景下会直接拖垮吞吐。

所以现实是:NPU 多租户隔离必须靠硬件机制兜底。PASID 和 IOMMU 就是两个最关键的硬件基础设施。

1.2 隔离的核心层次

要理解完整的隔离方案,我习惯把它拆成三个层次。

第一层是身份隔离。每个租户的任务在设备侧要有唯一身份标识,设备知道“这条访存请求属于谁”。这一层靠 PASID。

第二层是地址隔离。身份确认后,设备发起的 DMA 请求要经过地址转换,把设备虚拟地址映射到真实的物理地址,并且保证两个租户的物理地址空间互不重叠。这一层靠 IOMMU。

第三层是资源隔离。不只是内存,NPU 的计算核心、缓存、带宽也要按租户划分,A 租户的计算洪峰不能把 B 租户的任务卡死。这一层靠硬件资源分区和配额机制。

这三层环环相扣。没有 PASID,IOMMU 不知道要按哪套页表做转换;没有 IOMMU,PASID 只是给设备看的标签;没有资源隔离,前面两层做得再好,租户体验还是会崩。下面逐层展开。

2. PASID:给 DMA 请求办一张“通行证”

PASID 的全称是 Process Address Space ID,直译就是“进程地址空间 ID”。它的基本原理是:在 PCIe 协议层面新增一个能力,让设备在发起 DMA 请求时携带一个 ID 字段,用来标识这次访存属于哪个进程地址空间。这样,多个进程可以共用同一个物理设备,设备侧也知道每条请求该走哪种页表翻译。

我第一次看 PASID 相关 spec 的时候,脑子里冒出来的类比是机房的访客卡。一个大型数据中心不可能让所有访客随意进出,每个访客领一张卡,刷卡后闸机才知道放行到哪一层。PASID 就是这个访客卡,IOMMU 就是闸机。

2.1 PASID 如何参与一次 NPU 访存

拿一次典型的 NPU 推理任务举例。驱动在初始化阶段,会为每个租户上下文申请一个 PASID。申请成功后,驱动把这个 PASID 和租户对应的页表关联起来,再通过命令队列把任务下发到 NPU。

NPU 的 DMA 引擎开始干活时,每一个读请求或写请求都会在总线事务层打上 PASID 标签。请求经过 PCIe 链路到达 IOMMU 之后,IOMMU 先读 PASID,再按 PASID 找到关联的地址转换页表,完成设备虚拟地址到物理地址的映射,最后才把请求放行到内存控制器。

整个过程有几个细节值得注意:

  • PASID 是一个 20 位的 ID。只从数值上限看,一个设备最多支持一百多万个地址空间,实际硬件会少得多,但多租户场景绰绰有余。
  • PASID 是设备级能力,不是系统级能力。硬件设备需要实现 PASID 扩展能力,host 侧还需要在 PCIe 配置空间里正确使能。
  • PASID 和进程是一对多的。一个物理进程可以拥有多个 PASID,也可以为每个租户虚拟出独立地址空间,再用 PASID 区分。

2.2 PASID 分配、释放与复用

实际编程中,PASID 的分配通常走内核统一接口。我记得早期有同学自己用位图管理 PASID,后来发现设备重启、热插拔、驱动异常恢复等场景下很容易出现 ID 泄漏。正确的做法是让内核的 PASID 子系统统一分配,驱动只负责申请和注册。

我在调一个模拟项目的时候踩过一个坑:某个租户任务异常退出,驱动释放了 PASID,但设备侧的命令队列里还残留着引用该 PASID 的未完成请求。等这个 PASID 被新租户复用,新任务刚启动,残留请求就带着旧地址访问了内存,直接导致数据错乱。后面加了一个“PASID 静默期”机制,释放后延迟一段时间再允许复用,同时设备侧 flush 所有队列,问题才消停。

提示:PASID 复用问题在实际项目里非常隐蔽。无论驱动怎么设计,都要考虑“设备侧还有没有遗留请求引用这个 ID”的场景。

PASID 本身只是一张通行证,能不能让租户真正隔离开,关键看 IOMMU 认不认、怎么认。

3. IOMMU:把 PASID 变成真正的隔离

IOMMU 的作用通俗讲,就是给设备的所有 DMA 访问装一个“门卫 + 翻译官”。门卫检查身份(PASID),翻译官把设备虚拟地址转换成物理地址。没有 IOMMU 的时候,设备想访问哪个物理地址就访问哪个物理地址,相当于门卫不在岗。有了 IOMMU,地址翻译和权限校验全部在硬件里强制完成。

3.1 一次内存访问的完整转换过程

我画一条完整链路方便理解:

NRU 计算核发起访存 -> 带上设备虚拟地址和 PASID -> 经过片上互联到达 DMA 引擎 -> PCIe 请求发出 -> 到达 IOMMU -> IOMMU 按 PASID 找到页表 -> 翻译设备虚拟地址 -> 检查权限位 -> 访问物理内存 -> 返回数据。

这个过程里最容易忽略的是权限检查。IOMMU 页表里不只是地址映射关系,还包含读、写、执行权限位。如果某个租户只被授予读权限,它发起写请求时 IOMMU 会直接拒绝并上报错误。这种错误不是软件层的异常,而是硬件层面的拒绝,无法被绕过。

IOMMU 的翻译缓存(一般叫 IOTLB)对性能影响很大。因为 NPU 访存频率极高,每次访存都走一次完整页表遍历显然不现实。IOTLB 会缓存最近使用的地址转换结果,命中缓存时翻译开销几乎可以忽略。但缓存也带来一个问题:多租户场景下缓存会被频繁冲刷,尤其是上下文切换、PASID 切换时,IOTLB 失效会导致访存性能明显抖动。

我在实测多租户并发场景时发现,如果不做缓存优化,租户任务切换时 NPU 的访存延迟会飙到平时的 2 到 3 倍。后面通过调整页表层级、增大连续映射区域、减少不必要的 IOTLB 失效,延迟才降回来。这也是 IOMMU 调优里最容易被低估的环节。

3.2 怎么确定“给谁用哪套页表”

IOMMU 本身不知道哪个 PASID 对应哪个租户,它需要驱动程序把关联关系注册进来。这个注册动作通常发生在上下文创建阶段:

  1. 租户申请资源,驱动为它创建独立的地址空间。
  2. 驱动向内核申请 PASID。
  3. 驱动把 PASID 与地址空间的页表关联,绑定 IOMMU 域。
  4. 驱动下发任务时,在命令中携带 PASID。

这里有个容易混淆的概念:IOMMU 域。域可以理解为一块独立的地址映射空间,一个域里可以包含多个设备。多租户场景下,最直接的做法是一个租户一个域,每个域一套页表,再通过 PASID 关联到具体地址空间。这样做有两个好处:隔离彻底、错误不会跨租户扩散;缺点是域数量多,IOMMU 的资源开销和页表开销也会线性增长。

注意:域并不是越多越好。我见过一些方案为了“极致隔离”,把每个队列都放到独立域里,最终导致页表数量爆炸、内存开销反超收益。根据经验,按“租户”而不是按“任务”划分域,是成本和隔离性之间比较平衡的选择。

3.3 中断也是隔离的一部分

隔离不仅限于内存访问,中断同样属于边界。多租户场景下,设备产生的中断必须能正确路由到对应租户的控制面。如果一个租户的任务触发设备错误中断,却把错误信息广播到了所有租户,那信息泄露就已经发生了。

中断隔离在 IOMMU 层面的实现通常依赖中断重映射能力。设备上报中断时携带新的标识信息,中断控制器根据标识把中断投递到正确的接收方。这套机制的作用平时看不出来,一旦有租户任务触发异常,正确的中断隔离可以避免整个设备被误杀。

4. 硬件强制隔离:从软约束到不可绕过

PASID 和 IOMMU 解决了“设备访问内存时身份可识别、地址可翻译、权限可校验”,但离真正的硬件强制隔离还差一口气。什么叫硬件强制隔离?我的定义是:即使驱动代码写错了、租户任务恶意构造请求了,隔离边界依然不能破。

要做到这一点,光靠 PASID + IOMMU 还不够,需要物理层面把资源也切开。

4.1 第二层翻译:设备内部的地址隔离

现代 NPU 内部通常有一级或两级地址翻译单元。第一级是设备内部的私有转换,第二级才是 IOMMU。两级翻译的好处是:设备内部的每个子引擎(比如独立的计算核、视频编解码单元、DMA 通道)可以有自己的地址转换逻辑,放在 IOMMU 之前先把请求过滤一遍。

我理解两级翻译的架构时,用过一张办公室门禁图来类比。IOMMU 是园区大门,只看一个人进不进得了园区;设备内部翻译是楼栋门禁,再看这个人能不能进这栋楼。两道关卡信息不同——园区大门认工牌,楼栋门禁认楼层权限——任何一道拦下来,请求都进不去。

两级翻译的实际价值在于:即使 IOMMU 配置错误,设备内部的地址转换也可能把请求限制在安全边界内;反过来,即使设备内部转换被绕过,IOMMU 也能兜底。互相冗余,安全性显著提升。

4.2 资源分区的硬件边界

内存隔离之外,NPU 的计算资源也要物理划分。这里主要指计算核心分区和缓存/带宽分区。

计算核心分区的思路是硬件静态切分。比如一块 NPU 有 8 个计算核,可以切出 2+2+4 给三个租户,每个租户的计算请求只会被路由到自己的核上。这种切分在硬件层面完成,不可动态跨越,隔离性最强,代价是灵活性差。

缓存和带宽分区则更复杂。还是拿访客卡类比:计算核分区相当于把楼层分给你,但电梯、走廊是共享的——NPU 的片上缓存和内存带宽也类似。如果 A 租户的任务疯狂刷缓存,B 租户的任务会因为缓存命中率下降而明显变慢。因此,真正的硬件隔离方案需要在缓存分区和带宽配额上做限制。

我实测过一个场景:两个租户共享同一块 NPU,带宽配额分别设成 50% 和 50%。当 A 租户把任务并发调满时,B 租户的平均推理延迟从 5ms 涨到了 18ms;开启带宽配额限制后,B 租户延迟稳定在 6-7ms。这个数据说明,带宽配额是真实有效的,但也需要接受一点:A 租户的峰值吞吐会受限,这是公平性换来的代价。

4.3 验证硬件隔离的正确姿势

配置完隔离之后,怎么确认它真的有效?我推荐一套验证动作:

  1. 先跑正常多租户任务,观察算力分配是否符合预期。
  2. 故意让 A 租户发起非法 DMA 请求(构造越界地址),检查 IOMMU 是否拦截、错误是否只上报给 A。
  3. 检查 A 租户的物理内存页是否可能被 B 租户读取。在驱动里加一段 dump 逻辑,尝试读取越界地址,看 IOMMU 是否拒绝。
  4. 压测带宽,确认两个租户的吞吐曲线是否符合配额。

除了功能验证,还要做“故障注入”。我常用的做法是:在一个租户的任务里人为制造设备超时,看设备驱动能否只重置这个租户的上下文,而其他租户任务不受影响。如果一次故障导致整块 NPU 重启,说明隔离没有真正落地,还得回去查资源分区和错误路由。

5. 完整落地实践:从零搭一套多租户 NPU 隔离

前面讲了原理和设计,这一节直接讲落地路径。我假设你手头有一块支持 PASID 和 IOMMU 的 NPU,运行环境是 Linux 系统,目标是把这块 NPU 按租户切分,每个租户拿到独立地址空间和计算资源。

5.1 前置检查:你的硬件到底支不支持

很多项目翻车不是设计问题,是硬件能力不够。落地之前先确认三个关键能力:

  • 设备是否支持 PASID。查设备能力寄存器,确认 PASID 相关字段是否置位。如果不支持,后续方案全部要改。
  • IOMMU 是否已开启。Linux 里检查内核启动参数,确认 IOMMU 驱动的 DMA 重映射已经使能。没开的话,PASID 只是标签,翻译不会生效。
  • 设备驱动是否实现了必要的回调。驱动需要向内核注册 PASID 的分配、绑定和释放,也需要为每个域建立页表。

提示:测试环境里如果发现关掉 IOMMU 设备工作正常、打开 IOMMU 后 DMA 报错,大概率是驱动没适配设备发起的请求格式。先别急着怀疑硬件,查驱动和 IOMMU 的交互逻辑。

5.2 配置流程和关键参数

我的落地步骤大致如下:

第一步,使能 IOMMU 并确认模式。内核启动参数里加上 IOMMU 开启选项,确认 IOMMU 处于翻译模式,而不是直通模式。直通模式下所有 DMA 直接放行,隔离等于没有。

第二步,确认设备绑定。把 NPU 设备绑定到支持 PASID 的驱动,而不是通用驱动。这一步决定了后续接口是否可用。

第三步,驱动里创建地址空间。为每个租户创建独立地址空间对象,分配 PASID,关联 IOMMU 域。关键参数有三个:域数量、页表层级、缓存策略。域数量按租户计;页表层级根据内存范围选,小内存用少层级以降低访问开销;缓存策略主要看任务切换频率。

第四步,下发任务时携带 PASID。确保设备的命令描述符里正确填入了 PASID 字段。这一步最容易出问题的地方是字段偏移或格式错误,导致 IOMMU 收到请求后找不到对应域。

第五步,验证配置。检查 IOMMU 事件日志、设备错误状态、租户任务的实际访存路径,确认没有意外放行或误拦截。

我在实际配置里发现一个关键点:PASID 使能之后,设备驱动和 IOMMU 驱动之间的bind时机非常讲究。PASID 分配早了,IOMMU 域还没建好,访问会失败;分配晚了,任务已经准备好,又要等。最终我是通过把“PASID 分配”放在“域建立之后、任务下发之前”来解决的,这个顺序建议你直接抄作业。

5.3 验证内容清单

配置完成后,建议按这个清单逐项验证:

  1. 地址空间隔离:租户 A 和租户 B 各分配一段连续物理内存,互相尝试越界读写,确认 IOMMU 拒绝。
  2. 计算资源隔离:同时跑两个推理任务,确认任务被调度到各自的硬件分区,没有跨分区抢占。
  3. 带宽配额:测满带宽,观察两个租户的实际吞吐是否符合配额比例。
  4. 错误隔离:人为构造非法请求,确认设备错误只影响发起方租户。
  5. 性能回归:对比开启隔离前后的基线性能,确认隔离带来的退化在可接受范围。

我整理了一张常见配置组合对照表,方便你根据场景快速定参数:

场景域划分粒度页表层级缓存策略资源分区方式
多租户 AI 云按租户2~3 级缓存优先静态切分
单租户多任务按任务组2 级均衡动态调度
训练推理混部按优先级3 级隔离优先带宽配额
测试验证环境按测试用例1~2 级关闭缓存不限制

5.4 性能隔离的观察维度

很多人衡量 NPU 隔离只看算力分配,忽视了内存和带宽。实际的性能隔离要同时看三个指标:算力利用率、访存带宽占用、端到端延迟。

算力利用率看的是计算核心有没有被独占;访存带宽占用看的是共享内存带宽有没有被某个租户打满;端到端延迟则是综合结果。我的经验是,一旦开启硬件隔离,算力分配基本能达到预期的 95% 以上,带宽配额则需要精细调参。

有一次我给某训练任务开了 70% 的带宽配额,结果推理任务的 P99 延迟反而比不开配额时还低。原因是训练任务的突发访问被限制后,推理任务的缓存命中率大幅上升,整体表现反而更好。这个现象让我意识到:配额不只是限制,也是保障。

6. 常见问题与排查实录

多租户隔离的方案落地过程中,我遇到的绝大多数问题都可以归为三类:配置不对、资源不够、边界不清。下面把典型问题整理成速查表,再单独讲几个印象深刻的排障过程。

6.1 常见问题速查表

问题现象可能原因排查方向
启动后 DMA 一直报错IOMMU 未开启或设备未绑定正确驱动检查内核启动参数和驱动绑定关系
多租户任务互相干扰带宽配额未配置或缓存共享检查资源分区配置项和实际配额
任务切换时延迟抖高IOTLB 频繁失效调整页表层级、增加连续映射
某个租户死掉导致整卡重启错误隔离没生效查中断路由和错误上报机制
性能比单租户下降明显页表层级过多或缓存策略不当降低页表层级、关闭不必要翻译
PASID 分配异常出错释放后立即被复用增加静默期、flush 设备队列
越界访问竟然没有报错IOMMU 配置成透传模式检查域绑定和页表权限位

6.2 实测中踩过的一个深坑

有一次压测,两个租户任务同时跑,理论上互不干扰,结果 B 租户的任务频繁超时。刚开始怀疑带宽配额没生效,查配置没问题。后来一步步排查,发现是 A 租户的缓存清理动作太频繁,导致 B 租户所在的硬件分区共享缓存被反复冲刷。

这个问题的根源是缓存分区没有跟着租户粒度走,而是用了全局统一的缓存策略。解决方式是把缓存策略改成按域区分,A 租户的清理动作只影响它自己的缓存区域。踩过这个坑之后,我每次配置隔离都会先确认“缓存分区”和“地址空间隔离”是不是对齐的,否则地址隔离做得再好,性能隔离一样是空话。

还有一次我遇到设备错误中断风暴,排查了很久才发现是错误上报没有做过滤。某个租户的任务触发了非致命错误,设备按默认行为把所有租户的中断都触发了一遍,直接造成 IOMMU 事件队列被打满。修正方案是开启中断过滤,只把错误投递到对应租户。

这类问题有一个共通点:设备默认行为往往以“可用性”优先,不会自动考虑多租户场景的隔离语义。你需要把隔离要求显式告诉设备驱动和 IOMMU 配置,而不是期望硬件默认帮你做好。

7. 最后的实操心得

如果只让我总结一条经验,那就是:多租户 NPU 隔离,不能只靠单一机制,一定要把身份、地址、资源三层都覆盖到。PASID 解决身份,IOMMU 解决地址,硬件分区和配额解决资源,三层叠加才称得上真正的硬件强制隔离。

我个人在实际操作中的体会是,先把 IOMMU 调通、把 PASID 挂上,拿非法 DMA 请求把隔离边界试一遍,再去做资源配额和性能优化。顺序反了的话,性能调得再好,边界一破全盘崩溃,回来返工的成本更高。

最后再分享一个小技巧:调试隔离问题时,IOMMU 事件日志是最好的线索来源。打开事件日志记录,故意构造一次越界访问,日志会告诉你请求是哪个设备、哪个 PASID、哪个地址被拦了。如果你看到事件日志里 PA SID 和实际租户对不上,说明驱动里哪个环节写错了,顺着日志回查定位很快。多租户隔离这条路没有捷径,但把日志用好、把边界测透,走起来会顺很多。

RELATED READING

延伸阅读

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