企业级AI模型部署革命:基于昇腾NPU硬切分与Kubernetes的单卡多模型共存实战
在构建企业级AI应用平台时,我们常常面临一个核心矛盾:一方面,大型语言模型(LLM)需要消耗海量的计算资源,动辄占用整张甚至多张高性能计算卡;另一方面,embedding、reranker、语音识别等“小”模型虽然单个资源需求不大,但种类繁多,如果每个模型都独占一张卡,硬件成本将急剧攀升,资源利用率却低得可怜。传统的“一模型一卡”部署模式,在追求极致性价比和灵活性的今天,已经显得捉襟见肘。
想象一下这样的场景:你手头有一台搭载了昇腾Atlas 800I A2推理服务器的设备,需要同时部署一个用于文档检索的embedding模型(如bge-m3)、一个用于结果精排的reranker模型(如bge-reranker-v2-m3),或许还有一个语音识别模型(如SenseVoiceSmall)。按照老办法,你可能需要三张卡。但现实是,这些“小”模型在推理时,显存和算力占用远未达到单卡的饱和状态。大量的计算资源就在这种“占着茅坑不拉屎”的状态下被白白浪费了。
有没有一种技术,能像切蛋糕一样,将一张物理NPU卡的算力和显存精准地划分成多个独立的虚拟部分,分别分配给不同的模型使用,并且保证它们之间互不干扰?答案是肯定的。昇腾NPU提供的虚拟化硬切分(vNPU) 能力,结合Kubernetes强大的资源调度与隔离特性,正是解决这一难题的“黄金组合”。本文将深入探讨如何利用这套技术栈,在单张Atlas 800I A2上,实现embedding与reranker模型的高效、稳定、生产级共存部署。这不仅是一次技术实践,更是对AI基础设施资源利用范式的一次革新。
1. 核心理念:从资源独占走向智能共享
在深入实操之前,我们必须理解支撑此次部署变革的两大技术支柱:昇腾vNPU硬切分和Kubernetes容器化编排。它们的结合,并非简单的功能叠加,而是产生了“1+1>2”的协同效应。
昇腾NPU虚拟化硬切分 并非简单的软件时分复用。它是一种在硬件驱动层实现的、基于物理隔离的资源划分能力。你可以将其理解为在一套物理公寓(NPU卡)内,建造出多个拥有独立产权、独立水电表、隔音墙完备的套房(vNPU)。每个vNPU拥有独占的AI Core、显存(Memory)及其他计算单元(如AI CPU、VPC等)。这意味着:
- 真实的资源保障:每个vNPU分配到的AI Core和显存是物理上预留的,不会被其他vNPU上的任务抢占,确保了模型推理的延迟稳定性和性能可预测性。
- 强隔离性:一个vNPU上的进程崩溃、内存泄漏或异常高负载,不会影响到同一张物理卡上其他vNPU的运行,极大地提升了系统的整体可靠性。
- 灵活的规格定义:昇腾提供了多种预定义的切分模板(如
vir10_3c_32g、vir05_1c_16g),用户可以根据模型的实际资源需求,像选择云主机规格一样,为不同模型分配合适的vNPU。
而 Kubernetes 作为容器编排的事实标准,为这些vNPU的“套房”提供了完美的“物业管理”和“租客调度”服务:
- 资源抽象与调度:Kubernetes可以将vNPU作为一种可扩展的硬件资源(类似于
nvidia.com/gpu)进行管理和调度。它负责将包含特定模型的Pod(容器组)精准地调度到拥有目标vNPU的节点上。 - 生命周期与健康管理:通过
Deployment、StatefulSet等资源对象,Kubernetes确保了模型服务的高可用性。结合Liveness Probe、Readiness Probe和Startup Probe,它能自动监控模型服务的健康状态,并在异常时执行重启或迁移,这是单纯使用Docker的--restart参数无法比拟的。 - 统一的配置与网络:通过
ConfigMap、Secret管理模型配置和密钥,通过Service为内部和外部访问提供稳定的网络端点,使得整个部署架构清晰、易于维护。
当硬切分提供了底层的、稳固的资源“隔间”,Kubernetes则在其之上构建了自动化、智能化的运维“大厦”。两者结合,最终实现的目标是:让多个AI模型像微服务一样,安全、高效、弹性地共享同一块高性能计算硬件。
2. 环境准备:从硬件检查到K8s配置
实战开始。假设我们拥有一台Atlas 800I A2(32G或64G版本)服务器,并已安装好标准的Kubernetes集群(如K3s、KubeSphere等)。我们的目标是将这张卡的一部分资源切分出来,同时部署bge-m3和bge-reranker-v2-m3模型。
2.1 NPU状态检查与切分规划
首先,我们需要登录到服务器,检查NPU的当前状态,并规划切分方案。
-
查看所有NPU信息:使用
npu-smi工具,这是管理昇腾设备的瑞士军刀。npu-smi info这条命令会列出所有物理NPU卡的状态,包括卡ID、芯片ID、健康状况、功耗、温度以及当前是否有进程运行。我们需要找到一张空闲的、状态健康的卡作为切分对象。输出中类似
Processes: None的标识意味着该卡未被占用。 -
查询可用的切分模板:不同的NPU型号和显存规格支持的切分模板不同。我们需要根据模型需求选择合适的模板。
npu-smi info -t template-info以Atlas 800I A2 (64G)为例,常见的模板可能包括:
vir10_3c_32g: 分配10个AI Core和32GB显存。vir05_1c_16g: 分配5个AI Core和16GB显存。
根据社区实践和模型大小,bge-m3和bge-reranker-v2-m3这类模型在推理时显存占用通常在几个GB到十几GB之间。为了最大化利用资源并留有余量,我们可以选择
vir10_3c_32g模板进行1切2,即一张物理卡切分出两个vNPU,每个vNPU拥有10个AI Core和32GB显存(注意:这是逻辑分配,实际物理显存为64G,1切2后每个vNPU分得一半的物理资源)。 -
记录目标卡信息:假设我们选定卡ID (
device_id) 为6,芯片ID (chip_id) 为0的NPU进行切分。
2.2 执行NPU硬切分
确定了目标和方案后,开始执行切分命令。
-
创建vNPU:使用以下命令,在指定的物理卡上按照模板创建虚拟NPU。
npu-smi set -t create-vnpu -i 6 -c 0 -f vir10_3c_32g-i 6: 指定设备ID为6。-c 0: 指定芯片ID为0。-f vir10_3c_32g: 指定使用vir10_3c_32g模板进行切分。
执行成功后,系统会提示vNPU创建完成。
-
查询创建的vNPU信息:创建后,我们需要获取生成的vNPU的ID,这是后续挂载到容器的关键。
npu-smi info -t info-vnpu -i 6 -c 0输出会显示该物理卡上所有的vNPU列表。例如,你可能会看到两个vNPU,它们的ID可能是
100和101(或212、213等)。请务必记下这两个ID,假设我们得到vnpu_id为100和101。
注意:vNPU一旦创建,其对应的物理NPU部分资源就被锁定,无法再以整卡形式挂载给其他容器使用。在销毁vNPU之前,这部分资源是专属于这两个虚拟实例的。
2.3 Kubernetes集群配置
为了让Kubernetes能够识别和调度vNPU,我们需要在集群节点上进行一些配置。这里主要涉及将vNPU设备挂载到容器运行时。
方案选择:原生Docker挂载 vs Ascend Docker Runtime
对于vLLM(通过mis-tei或mindie镜像)部署embedding和reranker模型,通常推荐使用原生Docker挂载方式,更为直接和通用。
-
配置Kubernetes节点:我们需要确保Kubernetes工作节点(即运行NPU的服务器)的Docker或Containerd配置能够访问到vNPU设备。这通常意味着需要将vNPU的设备文件(如
/dev/vdavinci100)暴露给容器运行时。 -
创建RuntimeClass(可选但推荐):在Kubernetes中,可以创建一个
RuntimeClass资源,专门用于运行需要vNPU的Pod。这比在每个Pod中单独配置要清晰。# vnpu-runtimeclass.yaml apiVersion: node.k8s.io/v1 kind: RuntimeClass metadata: name: ascend-vnpu handler: runc # 根据实际容器运行时填写,可能是 `runc` 或 `crun`应用此配置:
kubectl apply -f vnpu-runtimeclass.yaml。 -
准备模型存储:无论是bge-m3还是bge-reranker-v2-m3,都需要提前将模型文件下载到服务器的一个持久化目录中,例如
/home/ai-models。在K8s中,我们将通过hostPath卷的方式将这个目录挂载到Pod内的容器中。
至此,底层硬件切分和集群基础配置已完成。我们得到两个虚拟的、隔离的计算单元(vNPU 100和101),并准备好了K8s的运行环境。接下来,就是为这两个“房客”部署各自的“服务”(模型)。
3. 模型部署实战:为vNPU定制K8s资源清单
我们将分别为bge-m3和bge-reranker-v2-m3创建Kubernetes的Deployment和Service。这里的关键在于Pod的配置,如何将特定的vNPU安全地、隔离地分配给对应的模型容器。
3.1 部署bge-m3 Embedding模型
首先创建embedding模型的部署文件。我们假设使用一个支持昇腾NPU和vLLM的镜像,例如swr.cn-south-1.myhuaweicloud.com/ascendhub/mis-tei:latest(具体版本需根据CANN和模型要求选择)。
# bge-m3-embedding-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: bge-m3-embedding
namespace: ai-models
spec:
replicas: 1 # 生产环境可根据需要调整,但vNPU是独占资源,通常一个vNPU一个副本
selector:
matchLabels:
app: bge-m3-embedding
template:
metadata:
labels:
app: bge-m3-embedding
spec:
runtimeClassName: ascend-vnpu # 使用我们创建的RuntimeClass
nodeSelector:
kubernetes.io/hostname: your-npu-node-name # 指定运行在拥有NPU的节点上
containers:
- name: embedding-server
image: swr.cn-south-1.myhuaweicloud.com/ascendhub/mis-tei:7.1.RC1
command: ["python3"]
args:
- "-m"
- "vllm.entrypoints.openai.api_server"
- "--model"
- "/models/bge-m3" # 容器内模型路径
- "--served-model-name"
- "bge-m3"
- "--task"
- "embedding" # 关键参数,指定为embedding任务
- "--host"
- "0.0.0.0"
- "--port"
- "8001"
- "--trust-remote-code"
- "--enable-prefix-caching" # 启用前缀缓存,优化性能
# 针对NPU的可选性能参数
- "--gpu-memory-utilization"
- "0.8"
securityContext:
privileged: true # 通常需要特权模式来访问NPU设备
volumeMounts:
- name: model-storage
mountPath: /models
- name: npu-devices
mountPath: /dev/vdavinci100 # 挂载vNPU 100的设备文件
resources:
limits:
# 这里可以象征性地设置,实际资源受vNPU规格限制
memory: "32Gi"
cpu: "4"
requests:
memory: "16Gi"
cpu: "2"
startupProbe:
httpGet:
path: /health
port: 8001
initialDelaySeconds: 60 # 模型加载可能需要时间
periodSeconds: 10
failureThreshold: 10
livenessProbe:
httpGet:
path: /health
port: 8001
initialDelaySeconds: 90
periodSeconds: 20
readinessProbe:
httpGet:
path: /health
port: 8001
initialDelaySeconds: 30
periodSeconds: 5
volumes:
- name: model-storage
hostPath:
path: /home/ai-models
type: Directory
- name: npu-devices
hostPath:
path: /dev/vdavinci100 # 主机上的vNPU 100设备
type: CharDevice
---
# bge-m3-embedding-service.yaml
apiVersion: v1
kind: Service
metadata:
name: bge-m3-embedding-service
namespace: ai-models
spec:
selector:
app: bge-m3-embedding
ports:
- port: 8001
targetPort: 8001
nodePort: 30001 # 如果需要从集群外部访问,使用NodePort
type: NodePort
关键点解析:
volumeMounts和volumes:我们通过hostPath卷,不仅挂载了模型存储目录,更重要的是挂载了特定的vNPU设备文件 (/dev/vdavinci100)。这确保了该Pod中的容器独占并使用vNPU 100。securityContext.privileged: true:访问底层NPU设备通常需要特权模式。在生产环境中,应评估更细粒度的权限配置(如使用Device Plugins),但初期为简化部署可先使用特权模式。startupProbe:对于AI模型,加载权重到显存的过程可能较长,startupProbe的initialDelaySeconds和failureThreshold需要设置得足够大,避免在模型加载完成前就被判定为启动失败。task embedding:这是vLLM启动embedding模型服务的关键参数,必须正确指定。
3.2 部署bge-reranker-v2-m3 Reranker模型
reranker模型的部署YAML与embedding模型高度相似,主要区别在于:
- 使用另一个vNPU设备(例如
/dev/vdavinci101)。 - 启动命令中不需要
--task参数(或者根据vLLM版本,可能需要指定为其他值,某些版本reranker部署方式与LLM相同)。 - 模型名称和路径更改。
# bge-reranker-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: bge-reranker-v2-m3
namespace: ai-models
spec:
replicas: 1
selector:
matchLabels:
app: bge-reranker-v2-m3
template:
metadata:
labels:
app: bge-reranker-v2-m3
spec:
runtimeClassName: ascend-vnpu
nodeSelector:
kubernetes.io/hostname: your-npu-node-name
containers:
- name: reranker-server
image: swr.cn-south-1.myhuaweicloud.com/ascendhub/mis-tei:7.1.RC1
command: ["python3"]
args:
- "-m"
- "vllm.entrypoints.openai.api_server"
- "--model"
- "/models/bge-reranker-v2-m3"
- "--served-model-name"
- "bge-reranker-v2-m3"
# 注意:reranker模型部署通常不指定 --task,或需查阅具体模型文档
- "--host"
- "0.0.0.0"
- "--port"
- "8002"
- "--trust-remote-code"
- "--enable-prefix-caching"
- "--gpu-memory-utilization"
- "0.8"
securityContext:
privileged: true
volumeMounts:
- name: model-storage
mountPath: /models
- name: npu-devices
mountPath: /dev/vdavinci101 # 挂载vNPU 101
resources:
limits:
memory: "32Gi"
cpu: "4"
startupProbe:
httpGet:
path: /health
port: 8002
initialDelaySeconds: 60
periodSeconds: 10
failureThreshold: 10
livenessProbe:
httpGet:
path: /health
port: 8002
initialDelaySeconds: 90
periodSeconds: 20
readinessProbe:
httpGet:
path: /health
port: 8002
initialDelaySeconds: 30
periodSeconds: 5
volumes:
- name: model-storage
hostPath:
path: /home/ai-models
type: Directory
- name: npu-devices
hostPath:
path: /dev/vdavinci101 # 主机上的vNPU 101设备
type: CharDevice
---
# bge-reranker-service.yaml
apiVersion: v1
kind: Service
metadata:
name: bge-reranker-service
namespace: ai-models
spec:
selector:
app: bge-reranker-v2-m3
ports:
- port: 8002
targetPort: 8002
nodePort: 30002
type: NodePort
将以上YAML文件中的your-npu-node-name替换为实际的Kubernetes节点名称,然后依次应用:
kubectl create namespace ai-models
kubectl apply -f bge-m3-embedding-deployment.yaml
kubectl apply -f bge-m3-embedding-service.yaml
kubectl apply -f bge-reranker-deployment.yaml
kubectl apply -f bge-reranker-service.yaml
使用kubectl get pods -n ai-models -w观察Pod状态,直到两个Pod都进入Running状态,并且READY为1/1。
4. 生产级调优与运维监控
部署成功只是第一步。要让这套系统在生产环境中稳定运行,还需要关注性能调优、监控和故障处理。
4.1 性能调优参数实践
vLLM和底层NPU驱动提供了丰富的参数来调节推理性能。以下是一些针对昇腾NPU和vLLM的实用调优经验:
| 参数 | 作用 | 建议值(针对bge-m3/reranker) | 说明 |
|---|---|---|---|
--gpu-memory-utilization | 控制vLLM可使用的显存比例 | 0.8 | 设置为小于1,为系统和其他进程预留空间,避免OOM。 |
--max-num-seqs | 最大并发处理的序列数 | 128 | 影响吞吐量。值越大,吞吐潜力越高,但可能增加延迟和显存占用。需压测确定。 |
--max-num-batched-tokens | 批处理的最大token数 | 256 或 512 | 同样影响吞吐和延迟的平衡。小模型可以设小一些。 |
--enforce-eager | 禁用图优化,以显存换速度 | 视情况启用 | 如果出现OOM,可以尝试启用此选项,它会减少图编译占用的显存,但可能降低推理速度。 |
--pipeline-parallel-size | 流水线并行大小 | 1 | 对于单卡vNPU部署的小模型,保持为1。 |
在Deployment的args中,可以这样添加:
args:
- "-m"
- "vllm.entrypoints.openai.api_server"
- "--model"
- "/models/bge-m3"
# ... 其他参数
- "--max-num-seqs"
- "128"
- "--max-num-batched-tokens"
- "256"
- "--gpu-memory-utilization"
- "0.85"
4.2 监控与日志收集
清晰的监控是运维的眼睛。我们需要监控两个层面:
-
vNPU硬件监控:虽然Pod内部看到的只是虚拟卡,但物理卡的健康状态至关重要。可以在宿主机上部署
npu-smi的定时采集任务,将NPU的温度、功耗、利用率、内存使用率、ECC错误等信息推送到Prometheus。- 利用率监控:观察每个vNPU上模型的AI Core利用率,判断负载是否均衡,是否存在瓶颈。
- 显存监控:确保模型运行时的显存占用稳定在分配额度内,没有泄漏。
-
模型服务应用监控:
- Kubernetes原生监控:利用
kubectl top pod查看Pod的CPU和内存使用情况。通过Service的NodePort,我们可以从集群外部访问模型的/metrics端点(如果vLLM开启的话),获取请求延迟(P50, P99)、吞吐量(Tokens/s)、错误率等关键指标,并接入Grafana面板。 - 日志集中收集:使用Fluentd、Fluent Bit或Filebeat等工具,将Pod内vLLM输出的日志(特别是错误日志和慢查询日志)收集到Elasticsearch或Loki中,便于问题排查。
- Kubernetes原生监控:利用
4.3 健康检查与故障自愈
我们在Deployment中已经配置了三种Probe,它们协同工作:
- Startup Probe:模型加载慢,用它来“耐心等待”启动完成。
- Readiness Probe:告诉Kubernetes和Service,Pod何时准备好接收流量。如果检查失败,Pod会从Service的负载均衡池中移除。
- Liveness Probe:判断Pod是否“活着”。如果连续失败,Kubernetes会重启Pod。
一个常见的坑是:模型服务进程还在,但NPU设备驱动层出现异常,导致推理请求全部失败。此时HTTP端口可能仍是通的,/health检查能通过,但业务接口报错。针对这种情况,一个更健壮的做法是:
- 编写一个自定义的健康检查脚本,该脚本不仅检查HTTP端口,还尝试执行一次极简的推理请求(例如embedding一个固定短句)。
- 将这个脚本放入Pod的另一个“sidecar”容器中,或者作为主容器
livenessProbe中exec类型的检查命令。
livenessProbe:
exec:
command:
- /bin/sh
- -c
- |
# 检查端口是否监听
nc -z localhost 8001 || exit 1
# 执行一个简单的curl请求,检查模型是否正常响应
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" -d '{"model":"bge-m3", "input":"test"}' http://localhost:8001/v1/embeddings)
if [ "$RESPONSE" -ne 200 ]; then exit 1; fi
initialDelaySeconds: 120
periodSeconds: 30
4.4 资源清理与vNPU生命周期管理
当不再需要某个模型服务,或需要调整切分方案时,需要妥善清理。
-
删除Kubernetes资源:
kubectl delete -f bge-m3-embedding-deployment.yaml kubectl delete -f bge-m3-embedding-service.yaml # 等待Pod完全终止 -
销毁vNPU:确认对应的Pod已删除,且没有其他进程在使用该vNPU后,在宿主机上执行:
npu-smi set -t destroy-vnpu -i 6 -c 0 -v 100 npu-smi set -t destroy-vnpu -i 6 -c 0 -v 101销毁后,物理卡6的资源将恢复为整卡可用状态。
长期运行建议:对于生产环境,可以考虑编写一个简单的Operator或使用Kubernetes的Device Plugin框架来管理vNPU的生命周期,实现vNPU资源的动态创建、绑定和回收,使其更贴合云原生的自动化理念。不过,这需要更深入的开发工作,初期手动管理结合清晰的运维文档也是可行的。
通过以上四个章节的拆解,我们从理念、准备、实战到运维,完整地走通了一条利用昇腾NPU硬切分和Kubernetes实现单卡多模型部署的道路。这套方案将宝贵的硬件资源利用率提升到了新的高度,同时借助K8s获得了企业级应用所需的可维护性、可观测性和弹性。在实际项目中,我遇到过因startupProbe配置过短导致模型反复重启的坑,也见证了通过调整--max-num-batched-tokens将吞吐提升近一倍的喜悦。技术细节的打磨永无止境,但方向已然清晰:让算力如水,随需而动,高效共享。
398



被折叠的 条评论
为什么被折叠?



