
智算中心的机器越堆越多但真正让平台团队头疼的往往不是买卡而是怎么把手里这些不同品牌、不同架构的GPU和NPU管起来用起来。如果你也在做类似的事或者正准备搭一套异构算力管理平台这篇内容应该能帮你少走不少弯路。我从一个平台开发者的视角结合我自己在做的东西把“不同品牌GPU与NPU统一管理”这个事拆开讲讲。里面会有架构设计思路、实际踩坑记录、调度方案选型的对比也有一些可以直接拿过去用的配置示例和排查技巧。适合正在做智算平台、AI训练平台、或者准备把异构设备纳入统一调度体系的开发同学参考。1. 异构算力管理的痛点与整体设计思路先说个我自己的感受异构算力管理这件事难点不在某个单独的设备怎么用而在怎么让一堆彼此差异巨大的设备跑在同一个平台体系里还不出乱子。1.1 异构设备多了之后先踩到的是哪些坑GPU和NPU在硬件形态、驱动接口、显存管理、算子栈上都不一样。比如英伟达的GPU有CUDA生态AMD的GPU有ROCm华为昇腾NPU有CANN还有各类做推理加速的NPU厂商每家都有自己的运行时。最直接的麻烦是上层业务代码不可能为每种设备各写一套底层平台也不可能为每种设备各维护一套独立的调度和监控系统。我当初接手这个事的时候平台里有三个品牌的GPU和两种NPU每类设备的接入方式都不同。有的走PCIe直通有的需要厂商提供的用户态驱动有的设备不支持SR-IOV有的支持MIG但版本受限。早期的做法是每种设备单独用一套脚本加定时任务去管显存不够就人工调设备掉卡了也是人工重启。机器数量少还能扛一旦上了规模光设备发现和状态同步就能把人累死。后来决定要做一个统一的算力管理底座目标很明确业务方只需要声明要多少算力、什么类型的设备平台自动完成设备分配、环境注入、监控接入和故障回收。异构统一管理这件事本质上是把设备差异尽量隔离在下层向上层暴露一套相对一致的资源语义。这个思路和操作系统屏蔽硬件细节是一个道理上层应用不关心你用的是SATA还是NVMe硬盘只关心文件读写这个接口。算力平台也一样业务不关心你底层是A100还是昇腾910B只关心能不能申请到卡、能不能正常跑起来、性能够不够。1.2 统一管理的分层抽象物理层、资源层、调度层我倾向于把整个体系分成五层来做从左到右依次是物理设备层、驱动运行时层、资源抽象层、调度编排层、业务接入层。物理设备层就是那些真实的GPU和NPU板卡包括PCIe、NVLink、RoCE网络这些互联设施。这一层要做的是设备发现、固件管理、健康检查出了问题要能第一时间定位到具体物理位置。驱动运行时层是把厂商提供的驱动、运行时库、工具链统一封装成标准接口尽量让上层感知不到具体厂商差异。资源抽象层负责把设备资源量化成可调度的对象比如一张卡对应多少个单位的算力、多少显存、多少带宽这些都是调度器要看的核心指标。调度编排层负责把任务和资源做匹配要考虑队列优先级、亲和性、抢占策略这些。业务接入层就是用户提交作业的入口可以是Kubeflow、SLURM、甚至是一个简单的Web界面但底层都走同一套资源申请逻辑。这五层每一层都有各自的难点。物理层的难点在设备种类多、兼容性杂尤其是NPU各家设计差异很大。驱动层最大的坑是版本冲突一个节点上装了多套驱动很容易把系统库搞乱。资源抽象层的难点是不同设备的算力怎么归一化一张A100和一张昇腾910B能简单等价吗显然不能但平台总得给用户一个直观的参考。我在这套体系里做的一个关键设计是引入“设备模板Device Profile”概念。每种设备都有一份模板里面记录了设备名称、显存大小、算力参考值以某款主流GPU为基准折算、支持的特性比如是否支持显存切分、是否支持MIG、默认驱动版本等。这样调度器在匹配资源时不是直接拿设备型号做字符串匹配而是拿模板里的属性做条件匹配灵活很多。1.3 为什么现成的调度框架不够用还要自己做一层很多人一上来就问我K8s不是有Device Plugin机制吗直接用不就好了K8s的Device Plugin确实能解决设备上报问题但它只解决“设备能不能被调度”的问题解决不了“业务怎么选设备”和“异构资源怎么共享”的问题。举个例子。K8s默认的调度器看到的是扩展资源Extended Resource比如nvidia.com/gpu这个资源名它只关心数值上够不够不关心你调度到哪张卡上会影响数据搬移开销。如果你有NVLink拓扑和多机RoCE互联默认调度器完全无视这些信息结果就是任务可能被分配到跨NUMA的设备上性能下降一截还找不到原因。另一个问题是K8s默认粒度过粗要么给整张卡要么不给不支持一张卡上多个进程共享显存利用率上不去。很多推理场景一张卡只用了2G显存剩下30G全浪费这在生产环境是没法接受的。所以我们自己开发调度层的时候并不打算重造一套资源管理轮子而是在K8s基础上做增强。具体来说是在调度器旁边加了一个算力调度组件负责处理设备拓扑感知、共享调度、抢占和优先级K8s继续负责Pod生命周期和基础调度。这样既复用K8s的成熟能力又能解决异构算力特有的问题。2. 异构设备的接入与统一标识把设备接入平台其实是个脏活累活尤其设备种类一多你会发现很多意外情况。这个章节我讲得细一点因为能不能把基础打牢直接关系到上层调度和监控的效果。2.1 驱动与运行时让GPU和NPU先“说同一种话”驱动安装是第一步。英伟达的驱动安装相对标准化apt装或者runfile装都有人用。AMD的ROCm对内核版本更敏感装完还得验证一下用户态工具链是否正常。NPU就更麻烦一点有的只支持特定内核版本有的是把驱动放在容器镜像里需要init容器先加载。我建议在节点接入平台之前做一个自动化巡检脚本把设备的PCIe信息、固件版本、驱动版本、用户态工具链版本全部采集到一个统一的设备台账里。一个很重要的细节是驱动版本与容器内CUDA版本的匹配。K8s里跑AI任务基本都走容器但容器里的CUDA版本未必和宿主机的驱动兼容。英伟达官方给过一张兼容矩阵表比如某几个CUDA版本需要的最小驱动版本是多少。昇腾那边也有类似的版本适配要求CANN版本和驱动版本必须配对配错了直接启动失败日志还特别隐晦。我建议在节点的设备插件里加一个“环境预检”逻辑容器启动前先检查驱动版本是否满足请求不满足就直接拒掉这样比跑到一半再炸要好得多。2.2 设备插件与拓扑感知让K8s“看见”每张卡K8s Device Plugin的工作方式是每个节点上的插件进程向kubelet汇报自己支持哪些设备kubelet把这些设备作为扩展资源记录下来。调度器在调度Pod时检查节点上扩展资源的可分配数量够就调度上去然后kubelet在容器启动时把设备列表通过环境变量传给容器运行时。这套流程本身不复杂但异构场景下有两个坑。第一个坑是裸设备列表的更新时机。设备可能被占用了、被释放了、掉卡了插件每次对账都要把最新状态同步给kubelet如果同步不实时会出现超额分配或者资源泄漏。我会让插件定期扫描设备状态并且在每个设备状态变化时主动发消息通知kubelet。第二个坑是设备设备和拓扑信息没有关联。K8s只需知道节点有多少“张”卡不关心这些卡在哪个PCIe switch上、是否共享同一个NVSwitch、和网卡的位置关系如何。但这些信息对高性能分布式训练至关重要。为了补上这个缺口我参考NVIDIA的拓扑感知调度思路在节点上做了一个Topology Manager。它通过读取PCIe拓扑、NVLink信息和网卡NUMA节点信息生成一张节点级的“设备拓扑图”然后把这个拓扑图作为节点标注吐给调度器。调度器在分配多卡任务时优先选择拓扑上彼此互联带宽高的卡比如同一NVSwitch下的四张卡尽量避免跨PCIe Switch通信。2.3 用标签和扩展资源统一业务视角设备接入之后要让业务方能清晰描述自己的需求。我建议不要直接用nvidia.com/gpu这种厂商相关资源名作为用户接口而是定义一套中性的资源语义比如“算力单元”和“显存单元”然后把物理设备映射到这些抽象资源上。这样用户不用关心底层是哪个厂商的设备只需要说“我要多少算力、多少显存、是否需要多卡通信”。同时在K8s节点上打几组标准标签。第一组是设备类型标签比如acceleratorGPU或acceleratorNPU还有具体型号比如gpu-modelA100-NVL-40G。第二组是设备能力标签比如支持显存切分、支持MIG、支持RDMA、支持RoCE等。第三组是归属标签例如属于哪个资源池、哪个部门、哪个项目。调度器的策略可以直接基于标签做匹配比如“训练任务必须调度到支持RDMA的GPU节点上”“推理任务优先调度到NPU节点上”。这些标签最终会体现在K8s的NodeSelector或节点亲和性配置里。统一标识还有一个重要用途是成本核算。不同异构设备的采购价差很大如果只按“一张卡”来计量成本中心根本对不上账。我们把每一类设备定义了一个“算力系数”比如一张昇腾910B折算成多少个标准算力单元一张A800折算成多少个然后成本按算力单元去核算。这样业务方申请资源时能直观看出自己用掉的是贵资源还是便宜资源。3. 统一调度与任务编排实战调度的核心不是把任务放到某个节点上就算完事而是要在满足约束的前提下让资源的利用率更高让任务跑得更快更稳。这一节重点讲我这边调度器的实现思路和一些实操细节。3.1 三层调度策略设计我把调度策略拆成三层全局调度、节点调度、设备调度。全局调度层做的事情是选择节点需要综合考虑节点上可用资源总量、当前队列里的任务优先级、节点的健康状态、节点的亲和性约束。这一层可以复用K8s默认调度器的NodeFilter功能但我会额外加一些自定义过滤逻辑比如排除处于“维护模式”的节点、排除GPU故障率过高的节点等。节点调度层处理的是同一个节点内部的任务放置问题。这一步要看任务请求几卡、是否要共享卡、每卡要多少显存。如果多卡任务是模型并行训练还要尽量把它们放在同一个拓扑域内减少数据搬移开销。设备调度层是最后一步真正决定这个任务具体用哪几张卡以及卡上的显存怎么分配。这三层策略的执行顺序不能乱而且每一层的决策结果要记录下来方便后面做故障定位和调度性能分析。我在这套系统里给每个调度请求生成一个“调度轨迹ID”从进入调度队列开始到最后分配完成每步都打日志排查“任务为什么被调度到某张卡上”这类问题时会特别有用。3.2 GPU与NPU混合训练的调度示例异构调度的典型场景就是混合训练比如一个大模型的主参数放在GPU上训练某些子模块在NPU上做推理或向量检索。还有一种情况是GPU集群快满了临时把一部分小任务调度到NPU上去后续再迁移回来。这类场景在调度器里要特别关注两个点一个是设备之间的通信路径GPU和NPU如果在同一个节点上还好说但如果跨节点就要考虑是否走RDMA以及网卡的带宽是否够用另一个是设备模板的匹配NPU的算力参考值低于GPU的话调度器要把任务做拆分或降配。我举一个实际的调度配置示例。假设有一个训练任务请求1张GPU和2张NPU调度器的匹配逻辑大致是这样apiVersion: scheduling.example.io/v1 kind: DeviceRequestTemplate metadata: name: mixed-train-template spec: devices: - type: GPU model: A100-NVL-40G count: 1 shared: false needs: [RDMA] - type: NPU model: Ascend-910B count: 2 shared: false needs: [RDMA] topologyPolicy: placement: sameNode preferred: sameSwitch这里的配置我把它拉成了模板形式调度器在处理时第一步筛选同时装有GPU和NPU的节点第二步看这些节点上有没有满足条件的空闲卡第三步检查节点间网络是否满足RDMA要求。如果第一步没有节点满足就降级为“同交换机不同节点”的拓扑策略并给任务打上一个“跨节点通信”的标注方便用户知道数据链路会有额外开销。实际运行中我们发现混合训练的性能瓶颈往往不在GPU或NPU本身而在跨设备的数据交换层。GPU和NPU之间的数据格式不一定一致转换过程会消耗大量CPU和内存带宽。所以在调度配置里我还加了一个“设备亲和性权重”参数如果任务里包含需要高频交换数据的设备调度器会尽量让它们落在同一个节点或同一个NUMA域。3.3 共享、切分与超卖算力利用率的进一步挖掘一个不太好看的数据是很多推理任务在用GPU的时候显存占用率不到50%算力利用率更低。所以如果平台总把整张卡分给一个任务资源的浪费是很惊人的。我们做了三层优化来提升利用率。第一层是显存共享。对支持MIG或vGPU的GPU设备我们可以把单卡切分成多个实例每个实例拥有独立的显存和算力隔离。对设备支持不太好的场景我们做的是时间片共享多个进程共用一张卡通过调度器控制时间片分配。这里要特别注意任务隔离问题。如果两个任务共用一张卡其中一个跑满算力另一个性能就会明显抖动。为避免这种情况我会把共享任务按照“算力需求高但显存需求低”和“显存需求高但算力需求低”做配对尽量错峰。第二层是显存的动态分配。不是所有任务都会一直占用整张卡的显存但驱动一般会预留整卡显存。我们做了一层显存代理在用户态把任务实际内存使用量上报给调度器空闲显存可以预分配给其他任务用。这里要设计一个显存水位回推机制防止突然大量申请导致现有任务OOM。第三层是超卖但这里要非常谨慎。超卖确实能提升资源利用率但一旦任务峰值同时出现节点负载会瞬间拉满甚至OOM。生产场景下我一般建议按1.2到1.5倍做超卖同时开启节点级的内存流控和GPU算力配额限制保证出问题的时候影响面可控。4. 监控、可观测性与故障排查统一管理的最后一个硬骨头是监控。每种设备都有自己的监控工具和技术栈比如GPU常用DCGM昇腾有自带的管理工具但它们的指标格式、采集方式、告警字段各不相同。如果不能把它们归一化运维手里光是各种命令行查状态的工具就有好几套。4.1 设备监控采集的标准化我这边在设计监控系统时没有直接对接厂商的监控API而是在各节点部署了一个异构采集代理。这个代理负责把厂商提供的工具输出统一转换成标准的Prometheus指标格式同时把设备健康状态同步到K8s自定义资源里。这样上层Grafana、告警系统只认一套数据模型不用关心底层是哪个厂商的设备。指标采集要分两层。第一层是设备级指标包括利用率、显存用量、温度、功率、PCIe错误计数、NVLink带宽等这些指标用来判断硬件是否健康。第二层是进程级指标包括每个容器使用了哪些设备、显存占用多少、算力使用多少这些数据用来做任务级别的分析和计费。进程级指标要特别小心因为同一张卡可能被多个容器共享采集时要通过卡上的进程信息和容器ID做关联否则归因会乱。我在采集层加了一个“指标质量门禁”机制每个指标都带元数据标注采集方式、采集周期、可能误差。这样下游在画图或告警时能明确知道某个指标是估算值还是精确值避免被一个不准确的指标误导。4.2 任务级监控与失败重试机制设备监控只能告诉你设备是否健康但用户更关心的是任务跑得怎么样。因此我们又做了一层任务级监控。每个训练或推理任务在启动时监控系统会把任务的容器ID、使用设备列表、启动时间等关联起来形成任务维度的数据视图。用户在界面上能看到这个任务当前的算力利用率曲线、显存变化曲线、迭代耗时趋势等。真正坑人的是设备掉卡问题。训练大模型跑到一半某张卡掉了如果没有检查点续训机制前面几天的算力时间就全白费了。我们在调度器里做了一个“重调度检查点联动”机制任务失败时先判断是否要自动重试如果有检查点就通过环境变量把检查点路径注入新容器自动从最近的一个保存点续跑。这里有个细节重试前要把出问题的节点做新的健康检查如果确认是硬件故障就立刻把该节点上的设备从资源池里摘除避免下次再调度上去。4.3 常见故障速查与排查技巧这些是我们在实际运维过程中遇到频次比较高的问题整理成一个速查表可以直接照着排查。故障现象可能原因排查方法处理建议容器启动报CUDA版本不匹配驱动版本与容器内CUDA版本不兼容检查驱动版本和容器内nvidia-smi输出更新驱动或替换兼容的基础镜像NPU设备在容器内不可见设备插件未将设备映射进容器CANN环境变量未注入检查Device Plugin日志和容器内device列表确认设备插件环境变量注入逻辑多卡任务性能远低于预期未考虑NVLink/PCIe拓扑跨Switch通信用nvidia-smi topo查看拓扑检查调度亲和性优化调度器拓扑策略增加亲和性约束GPU显存泄漏长时间运行后OOM推理框架缓存未及时释放观察显存曲线定位泄漏进程配上限流和定期重启策略节点偶发掉卡但物理设备正常PMC/驱动误报或固件Bug查看dmesg和厂商事件日志升级固件或调整驱动参数异构设备混跑时任务互相干扰共享卡上算力争抢查看进程级指标分析时间片分配对低优先级任务做算力配额限制排查这类问题有个通用思路先看节点层再看驱动层最后看业务层。节点层看电源、散热、PCIe链路是否异常驱动层看驱动日志、事件上报、环境变量是否有误业务层看框架版本、CUDA依赖、算子实现。不管什么问题建议先把所有日志落到一个统一的日志平台否则设备和节点一多靠ssh到每台机器上翻日志根本查不动。5. 一些实操心得和后续还可以做的事文章写到这主体内容基本讲完了。最后分享几个我在实际跑这段平台时的个人感受。第一点异构算力管理不是一次性把事情做完就算完。设备的型号会更新厂商的驱动和用户态工具链会更新K8s本身也在快速迭代平台要保持一套平滑升级的机制。我建议分层升级先升级设备插件和采集代理再升级调度组件最后批量升级节点设备驱动避免一次性全量变更导致风险不可控。第二点尽可能做一些设备灰度验证。新到一批卡、换一个新驱动版本不要直接全量接入生产环境。用一批测试节点先跑几天压力测试把错误率、温度、性能衰减曲线跑出来确认稳定后再纳入正式资源池。这个习惯能帮你省掉大把救火时间。第三点文档和台账一定不要省。异构环境的设备太多型号、序列号、驱动版本、固件版本、所在节点、所属项目这些信息记录在一张能检索的表里排查问题的时候能少花很多时间。哪怕只是简单的Google Sheet或者Wiki页面都比没有强。后续我觉得还可以做的方向是自动化的性能基准库建设。现在异构设备越来越多不同设备在不同模型、不同算子上的表现差别很大如果有一套相对完整的能力基准库调度器在决策时就可以根据任务类型自动推荐最合适的设备型号。这个做好了整个平台的算力分配会更智能也算是从“能统一管理”迈向“管得好、用得好”的关键一步。