昇腾 910B3 的卡平时都是整卡调度跑大模型预训练没问题但一到推理或者小模型微调场景就头疼模型就占十几 GB 显存一张 64GB 的卡只用了四分之一剩下的全部闲置任务排队却越来越长。后来我们团队在 CubeStudio 平台上全面转用 vNPU 静态切分把一张 910B3 按显存切分成多块虚拟 NPU配合 K8s 的资源调度挂载给不同 Pod 使用实测下来一张卡稳定承载 3 到 4 个中等规模推理任务资源利用率直接翻倍。这篇就把完整的操作链路分享出来包括 npu-smi 静态切分、K8s 侧的资源声明和挂载、以及 PyTorch 环境下的最终验证主要面向正在做昇腾资源池化或者想把推理成本打下来的同学。1. 为什么要做 vNPU 切分一卡多用的核心逻辑1.1 整卡调度的资源浪费问题昇腾 910B3 在训练集群里通常按整卡调度好处是隔离干净、性能可控但带来的问题也很现实卡上有大量算力和显存被闲置。我在实际环境里观察过很多推理任务用到的显存只有 10GB 到 20GB而 910B3 单卡显存是 64GB 起步整卡跑推理意味着至少三分之二的显存是空的。模型推理和训练不一样推理是低延迟、高并发的场景瓶颈很多时候在显存容量和调度吞吐上不一定需要把整颗 NPU 的算力全占满。除了显存浪费还有排队问题。假设你有一个 8 卡节点8 个推理任务要跑如果是整卡调度一个任务占一张卡节点只能同时跑 8 个任务多出来的只能排队。但如果把一张 910B3 切成 4 个 vNPU8 卡节点就能同时承载 32 个推理任务排队压力大幅下降。对于不需要大显存的模型来说这几乎是零成本扩容。1.2 vNPU 静态切分与动态切分怎么选昇腾的 vNPU 切分有两种思路。动态切分是由调度平台在任务拉起时动态分配显存和资源适合快速实验、资源使用率波动大的场景优点是灵活缺点是资源边界不固定多任务并发时容易出现性能互相干扰而且整套调度链路依赖平台能力不是所有环境都支持。静态切分则是通过 npu-smi 命令把物理 NPU 划分成若干个固定规格的 vNPU 实例每个实例拥有独立的显存配额切分后长期有效。生产环境我更推荐静态切分原因主要有三点第一边界清晰每个 vNPU 的显存上限是硬隔离的一个任务把显存打爆不会影响同卡其他 vNPU第二调度简单K8s 侧只需要把 vNPU 当成一种可计数资源来分配第三可预期性好切分规格固定你可以在平台资源池里提前规划好整卡的分配拓扑不会出现任务跑着跑着显存不够又调不动的情况。1.3 静态切分适合什么场景不适合什么场景静态切分最适合的场景是中小模型推理、批量离线推理、多租户隔离开发环境。比如 GPT 系列大模型在 910B3 上做推理时如果开了 KV Cache 优化和量化单实例的显存需求会显著下降这时候就可以把一张卡切成几片给多个推理服务同时用。我见过比较典型的配置是把 64GB 显存切成 2 块 32GB 或者 4 块 16GB搭配合适的模型并行策略吞吐量比整卡跑批量推理要划算得多。但静态切分不适合超大模型训练尤其是需要张量并行和流水线并行的场景。这类任务往往希望独占整卡避免通信资源被抢占强行切分反而会影响性能和稳定性。另外如果某个任务偶尔需要突发大显存静态切分反而会限制它这种情况用动态切分或者整卡调度更合适。所以切分之前一定要先判断业务负载类型。2. 切分前的准备工作硬件确认、驱动检查和节点状态2.1 确认 NPU 型号和 vNPU 能力动手切分之前先在节点上确认一下硬件型号和驱动版本确保走的是受支持的路径。登录到目标节点执行npu-smi info输出信息里需要重点看 Product Name 是否为昇腾 910B3 系列同时查看 Driver Version 和 Firmware Version。不同驱动版本对 vNPU 切分的支持程度有差异如果驱动过老部分切分参数可能不生效建议先把驱动和固件升级到官方发布的稳定版本再操作。我踩过一次坑驱动版本太老npu-smi config 命令能执行成功但切出来的 vNPU 在容器里始终看不到排查下来才发现是固件不支持升级之后问题消失。确认 vNPU 能力可以直接查 vNPU 相关信息npu-smi info -t vnpu -i 0 -c 0如果命令有返回或者提示支持的 vNPU 信息说明当前硬件和驱动支持该特性如果返回不支持就需要先升级驱动或者确认硬件规格。2.2 确认切分前卡上无任务切分操作会修改 NPU 的资源划分状态如果卡上已经有任务在跑切分可能失败或者导致正在运行的任务异常退出。切分之前必须先检查卡的状态确认目标卡没有业务进程占用npu-smi info -t process通过这条命令可以看到当前节点上所有 NPU 上的进程占用情况。如果目标卡 ID 对应的进程列表不为空需要先把业务任务停掉或者等它跑完确保卡上完全空闲再切分。之前我在测试环境里没有做这一步直接对一个正在跑推理任务的卡执行切分结果容器里的推理进程报显存分配失败服务直接挂掉。生产环境千万别跳过这个检查。2.3 确认设备插件和运行环境如果是配合 K8s 使用节点上需要提前装好 Ascend 的设备插件也就是 Device Plugin这样 K8s 才能通过 Extended Resource 识别和调度 vNPU。检查设备插件是否正常可以用kubectl describe node node-name | grep -i ascend如果节点上能看到类似ascend.com/vnpu或者ascend.com/npu的资源项说明设备插件已经正常上报资源。如果没看到需要先排查 Device Plugin 的部署状态。还有一点容易被忽略插件版本和驱动版本要匹配否则插件可能能上报整卡资源但上报不了切分后的虚拟资源。另外节点上还要有昇腾的容器运行时依赖比如 Ascend Docker Runtime 的相关配置确保容器启动时能正确挂载/dev/davinci*设备以及注入 NPU 相关的环境变量。这一层没配好的话即使切分成功Pod 起来了也访问不到 NPU。3. npu-smi 静态切分实操切分、校验、还原一步不落3.1 查看当前切分状态和目标卡信息切分前先用命令确认当前卡的规格和显存总量以 910B3 常见配置为例单卡显存 64GB产品形态可能是 8 卡或者 16 卡的节点。使用npu-smi info记录下目标卡的卡号和对应的设备 ID。假设我们有一张 8 卡节点打算把第一个物理卡编号 0切分成两个 vNPU每个 vNPU 分 32GB 显存。切分的最小单位一般是 1GB但实际生产环境建议按照模型显存需求留出 10% 到 20% 的余量避免后期模型迭代或者数据量增长导致显存不够。3.2 执行静态切分的两种方式昇腾 NPU 的 vNPU 静态切分在 910B3 等型号上可以通过 npu-smi 命令完成配置。常见做法是采用配置文件方式先编写一个 XML 配置文件描述各个 vNPU 的显存分配再通过 npu-smi 加载。配置文件内容大概是vnpu_config physical_npu id0 virtual_npu id0 memory_cap32/ virtual_npu id1 memory_cap32/ /physical_npu /vnpu_config然后执行加载命令npu-smi config -t vnpu -c /path/to/vnpu_config.xml有些驱动版本也支持直接在命令行里写参数切分不用改配置文件。具体语法因版本而异但思路一致指定物理卡号、指定每个 vNPU 的显存大小、下发配置。执行完成后可以用npu-smi info -t vnpu查询切分结果是否生效。注意npu-smi config 命令通常需要 root 权限执行而且不同版本的参数名称可能不同。操作前先执行npu-smi config -h或查看官方文档确认当前版本的语法避免命令写错导致配置不生效。3.3 校验切分是否成功配置下发之后重新查询 vNPU 信息npu-smi info -t vnpu -i 0 -c 0正常情况下输出会列出这个物理卡被切分出的多个 vNPU并显示每个 vNPU 的显存配额和状态。同时再执行一遍npu-smi info主显示界面里物理卡 0 的显存总量应该仍然是 64GB但业务占用数据会按照 vNPU 维度展示。如果看到的是多个 vNPU 条目且显存总和等于物理卡总显存说明切分成功。我还习惯在切分后验证一下设备节点是否生成。昇腾的 NPU 设备节点在/dev/davinci*下切分后理应出现对应的虚拟设备节点如果设备节点没有生成需要重启节点或者重启 npu-smi 服务再检查一遍。3.4 切分结果的还原切分不是一劳永逸的业务调整或者要跑大模型训练时需要把 vNPU 还原成整卡。还原方式和切分类似把配置改成单卡独占模式即不分片然后重新加载npu-smi config -t vnpu -c /path/to/restore_config.xmlrestore_config.xml 内容就是把刚才的 32GB 32GB 改成整卡 64GB 独占vnpu_config physical_npu id0 virtual_npu id0 memory_cap64/ /physical_npu /vnpu_config还原操作同样要确保卡上没有正在运行的 vNPU 任务否则可能出现资源释放不完整的情况。还原后用npu-smi info -t vnpu确认 vNPU 列表已经变成单条记录再检查/dev/davinci*设备节点是否恢复为整卡设备。4. K8s 挂载 vNPU让容器调度到正确的虚拟设备4.1 Device Plugin 如何上报 vNPU 资源K8s 本身不认识昇腾 NPU 设备它能识别的是 Device Plugin 上报的扩展资源。昇腾设备插件会在启动时读取节点上的 NPU 状态把整卡和切分后的 vNPU 都上报为可调度资源。通常在节点上能看到类似ascend.com/vnpu的资源项表示该节点可用的 vNPU 数量。需要强调一点vNPU 资源上报的粒度是“块”不是“GB”。也就是说如果一张卡被切成 2 个 vNPU插件上报的数量就是 2K8s 调度器只负责把一个 Pod 调度到拥有足够 vNPU 数量的节点上至于这个 vNPU 具体是哪一块、显存多少由设备插件在容器启动时通过环境变量和设备节点注入来指定。4.2 Pod 或 Deployment 的资源配置写法在 K8s 中使用 vNPU 资源只需要在 Pod 的资源声明里加上 vNPU 的 limits例如apiVersion: v1 kind: Pod metadata: name: npu-vnpu-test spec: restartPolicy: OnFailure containers: - name: pytorch-test image: ascend-910b3-pytorch:v1 command: [/bin/sh, -c, python /workspace/test.py] resources: limits: ascend.com/vnpu: 1需要注意的是资源只能写到容器级别。如果同时需要 CPU 和内存也可以一并声明。设备插件在容器创建时会把 vNPU 对应的设备文件挂载进容器同时注入环境变量ASCEND_RT_VISIBLE_DEVICES这个变量决定容器内能看到哪几个 NPU 设备。如果节点上有多个 vNPUK8s 调度器会选择一个满足资源数量的节点设备插件再负责从该节点上挑选一个空闲的 vNPU 分配出去。从使用者的角度看你不需要关心分配的是第几个 vNPU只要在容器里读取环境变量即可。4.3 容器内如何确认分配到了哪个 vNPU容器启动后先检查环境变量echo $ASCEND_RT_VISIBLE_DEVICES正常输出会是一个类似0或者0:1的值。如果是0:1表示物理卡 0 的 vNPU 1 被分配到了当前容器。再用npu-smi info在容器内查看设备信息应该只能看到被分配的这一块 vNPU而不是整个物理卡。这种隔离机制保证了容器不会越权访问其他 vNPU 的显存和算力。4.4 多 Pod 并发调度验证挂载配置写完之后可以创建多个 Pod 同时申请 vNPU 资源来验证调度效果。比如在切分好的节点上一次性创建 4 个 Pod每个 Pod 申请 1 个 vNPU正常情况下 4 个 Pod 都会被调度到该节点上并且每个 Pod 内部看到的 vNPU 编号互不相同。这相当于验证了“一卡多用”的调度能力。如果创建的 Pod 数量超过节点上可用的 vNPU 总数多余的 Pod 会处于 Pending 状态这恰恰说明资源调度逻辑是生效的。我在实际测试中发现一个规律即便节点上有足够的 vNPU 剩余资源如果多个 Pod 同时请求设备插件偶尔会因为并发分配冲突导致个别 Pod 创建失败。这种情况通常是因为设备插件版本较旧。升级插件到新版本后并发分配冲突明显减少。生产环境建议把设备插件版本和驱动版本配套升级别只升级其中一个。5. PyTorch 验证从系统层面到算子层面确认一切正常5.1 容器内的 PyTorch 和 torch_npu 准备昇腾 NPU 上跑 PyTorch 需要安装 torch_npu这是昇腾适配 PyTorch 的插件包。环境准备阶段我们需要确保镜像里已经安装了匹配 PyTorch 版本的 torch_npu例如 PyTorch 2.1.0 对应某个 torch_npu 版本。镜像构建时还需要安装 CANN 工具包因为 torch_npu 底层依赖 CANN 的运行时和算子库。如果镜像里没有装可以在容器启动后手动安装。CANN 和 torch_npu 的安装包通常会作为昇腾基础镜像的一部分提供。构建自定义镜像时建议直接从官方昇腾 PyTorch 镜像拉取后在之上叠加业务代码这样最稳妥。5.2 简单 PyTorch 验证脚本设备可见性和张量计算先跑一个最基础的环境探活脚本确认 PyTorch 能看到 NPU 设备import torch import torch_npu print(PyTorch version:, torch.__version__) print(NPU device count:, torch.npu.device_count()) print(NPU device name:, torch.npu.get_device_name(0)) a torch.randn(1024, 1024, devicenpu) b torch.randn(1024, 1024, devicenpu) c torch.matmul(a, b) print(Matrix multiplication result device:, c.device) print(Matrix multiplication shape:, c.shape)这个脚本能完成两个验证第一个是确认容器内 NPU 设备可见device_count 应该返回 1第二个是确认基础算子能在 NPU 上跑通。如果torch.npu.device_count()返回 0大概率是设备节点没有正常挂载或者ASCEND_RT_VISIBLE_DEVICES环境变量为空需要回到 K8s 资源声明和设备插件侧排查。5.3 多容器并发验证资源隔离单容器验证只能证明 vNPU 可用不能证明隔离性。为了验证多个 vNPU 之间是否真的独立我会在同一个物理卡上同时起两个 Pod各自运行 PyTorch并在脚本里创建接近 vNPU 显存上限的矩阵分别在两个容器里执行显存占用高的算子观察是否出现互相干扰。例如 64GB 物理卡切成两个 32GB vNPU两个容器里分别执行 16GB 显存占用级别的计算正常情况下两个任务都能正常运行。如果隔离没有生效其中一个容器会报显存不足或者 OOM。还有一种验证方法在容器 A 里占满显存后去容器 B 里再创建大张量如果 B 不受影响地申请到显存说明显存隔离真实有效。这个实验我做过很多次静态切分的隔离效果在昇腾上是比较可靠的。5.4 真实推理场景的功能验证如果只是环境探活误判概率还是有点高。建议在最后一步跑一个真实模型推理比如加载一个 PyTorch 训练好的小模型在 vNPU 上跑推理并对比输出结果。示例如下import torch import torch_npu import torchvision.models as models model models.resnet18(pretrainedFalse) model model.to(npu) model.eval() dummy_input torch.randn(1, 3, 224, 224).to(npu) with torch.no_grad(): output model(dummy_input) print(Inference output shape:, output.shape) print(Inference NPU memory allocate OK)加上模型推理验证后整个链路就完整了vNPU 切分成功、K8s 调度挂载成功、PyTorch 算子执行成功、真实模型推理成功。这一套流程跑通说明 vNPU 可以从基础设施层面应用到业务层面。6. 踩坑记录切分和挂载过程中最常见的 6 个问题6.1 切分命令执行成功但 vNPU 查询不到这个现象我在论坛和客户环境里都见过。切分命令返回成功但npu-smi info -t vnpu查询不到任何 vNPU 条目容器里也看不到虚拟设备。排查路径固定先查驱动和固件版本是否匹配再看 npu-smi 服务是否需要重启最后查切分配置文件中显存总和是否超过物理卡总显存。如果显存之和写超了配置会静默失败建议把显存总和控制在物理卡显存的 95% 以内留出系统保留内存。6.2 K8s 调度成功但容器里看不到 NPU 设备Pod 能创建成功说明资源调度层面没问题但容器内npu-smi info报错或者 device_count 为 0通常是两个原因设备插件没有正确传递物理设备或者容器运行时没有挂载昇腾相关设备。检查kubectl describe pod pod-name里的环境变量和设备参数看看有没有ASCEND_RT_VISIBLE_DEVICES和/dev/davinci0挂载记录。如果容器运行时没有集成昇腾插件设备根本不会出现在容器里这时候要检查节点上的容器运行时配置。6.3 多个 vNPU 同时跑大任务出现性能波动静态切分虽然隔离了显存但算力隔离不一定能做到完全线性。多个 vNPU 同时跑计算密集型算子时会出现一定程度的算力抢占因为底层物理芯片的 AI Core 是共享的。这不是配置问题而是 vNPU 的工作机制。如果业务对性能波动敏感建议在分配时不要把一个物理卡的 vNPU 全部打满预留一些算力余量。6.4 切分后 npu-smi 显示的总显存变小有些版本在切分后npu-smi info主界面显示的显存总量不是物理卡完整的 64GB而是可用显存值。这会让初学者误以为切分把显存切丢了。实际上去看 vNPU 列表各 vNPU 显存加起来还是等于总显存。这种显示差异只是展示口径问题不需要处理。6.5 设备插件上报的 vNPU 数量和实际不符如果你把一张卡切成 4 个 vNPU但 K8s 节点上看到的ascend.com/vnpu数量还是 1说明设备插件在启动时读取的是旧状态。重启设备插件 Pod让它重新扫描节点上的 NPU 状态通常就能解决。注意设备插件 DaemonSet 的重启会影响节点上已有 NPU 任务建议在维护窗口执行。6.6 还原成整卡后K8s 还是只能调度到 vNPU切分配置还原后设备插件不一定能自动感知变化节点上可能仍显示旧的 vNPU 资源数量。这时候重启节点上的设备插件 Pod 或者重启整个节点刷新资源上报信息即可。遇到这个问题不要慌不是配置没还原成功而是缓存信息没有刷新。7. 把 vNPU 用好的三个经验静态切分只是第一步真正把一卡多用跑得稳还需要在资源和业务两个层面做规划。第一个经验是切分粒度不能拍脑袋要基于模型显存 profiling 结果。跑一次真实推理观察峰值显存然后按峰值显存的 1.2 倍去定 vNPU 大小这样既留了余量又不会浪费太多。第二个经验是 K8s 侧最好给不同的 vNPU 资源打上标签方便按业务类型分配比如离线批处理任务用大 vNPU在线小流量服务用小 vNPU。第三个经验是要监控vNPU 之间的算力竞争虽然存在但影响可以通过节点级监控观察到。别只顾着看 Pod 层日志要把 NPU 利用率、显存占用、算子耗时这些指标一起拉起来形成一套可观测体系。一套切分 挂载 验证流程走下来昇腾 910B3 的一卡多用并没有想象中复杂。核心思路就是把物理资源切成标准化的虚拟资源再交给调度系统去分配。静态切分虽然不如动态切分灵活但胜在稳定、可控、可预期生产环境跑起来很省心。每次我要给新环境做 NPU 资源池化第一件事永远是先把这张卡的 vNPU 拓扑画出来然后才敢让业务往里进。