多租户AI推理部署的核心矛盾在于:GPU显存和算力属于稀缺资源,不同客户对延迟、吞吐和成本的要求差异极大。如果把所有租户的请求打到同一个推理进程里,轻则互相拖慢响应,重则触发显存溢出导致服务整体不可用。更麻烦的是,没有清晰的资源边界,计费就变成一笔糊涂账。这篇文章会从隔离粒度、配额控制和账单生成三个层面展开,给出可直接落地的部署方案。

从进程级到实例级的隔离策略
最粗糙的做法是共享一个推理进程,依靠请求头中的租户标识做逻辑隔离。这种方式部署简单,但无法限制单个租户的显存占用,模型显存一旦被大请求挤满,所有租户都会受到影响。更推荐的做法是每个租户独占一个推理实例,通过独立的GPU显存分配和网络命名空间实现强隔离。
以vLLM为例,可以在Kubernetes中为每个租户创建一个Deployment,并通过环境变量控制显存上限。下面是一个使用Triton推理服务器的多实例部署示例,每个租户绑定固定的GPU显存比例:
apiVersion: apps/v1
kind: Deployment
metadata:
name: triton-tenant-a
namespace: tenant-a
spec:
replicas: 1
template:
spec:
containers:
- name: triton-server
image: nvcr.io/nvidia/tritonserver:24.01-py3
args:
- tritonserver
- --model-repository=/models
- --grpc-port=8001
resources:
limits:
nvidia.com/gpu: 1
memory: 16Gi
env:
- name: CUDA_VISIBLE_DEVICES
value: "0"
- name: TRITON_MEMORY_LIMIT
value: "0.6"
这里CUDA_VISIBLE_DEVICES把租户A限制在0号GPU,TRITON_MEMORY_LIMIT设置为0.6意味着最多使用该卡60%的显存。租户B可以部署到同一块GPU的剩余容量,或者独立使用另一块卡。这种方式的好处是显存边界清晰,一个租户的推理任务不会导致另一个租户的显存溢出。
如果租户数量很多,为每个租户维护独立Deployment会带来运维负担。此时可以采用模型多实例加动态路由的方案,例如在Triton中为同一模型配置多个实例组,每个实例组绑定不同的内存池,再通过自定义调度器把请求路由到对应租户的实例组。不过这种方案的隔离强度弱于独立的Pod,适合对成本更敏感、租户规模较小的内部团队。
基于Kubernetes的资源配额与优先级控制
仅靠实例级隔离还不够,还需要在集群层面防止租户过度申请资源。Kubernetes的ResourceQuota和LimitRange可以限制每个命名空间内的总GPU数量、CPU和内存请求。建议给每个租户创建独立的命名空间,并设置配额对象:
apiVersion: v1
kind: ResourceQuota
metadata:
name: tenant-a-quota
namespace: tenant-a
spec:
hard:
requests.nvidia.com/gpu: "2"
limits.nvidia.com/gpu: "2"
requests.cpu: "16"
requests.memory: 32Gi
persistentvolumeclaims: "5"
上面的配额限制租户A最多申请2块GPU、16核CPU和32GiB内存。当租户A试图创建第三个使用GPU的Pod时,API Server会直接拒绝。配合LimitRange,还可以设定单个Pod的最小和最大资源申请,避免某个租户用一堆小Pod恶意占满配额。
对于共享GPU场景,例如一块GPU要同时服务两个租户,配额只能控制GPU卡数,没法限制显存占用比例。这时需要结合调度器扩展或者使用NVIDIA的MIG(多实例GPU)技术。在A100/H100等数据中心GPU上,MIG可以把一块物理GPU切成多个独立的硬件分区,每个分区有独立的显存和计算单元,租户之间几乎完全隔离。部署时可以在Pod的GPU请求中指定MIG实例类型:
resources:
limits:
nvidia.com/mig-1g.10gb: 1
通过MIG,租户A拿到1g.10gb的切片,租户B拿到另一个切片,它们连显存地址空间都是物理隔离的,安全性最好。缺点是MIG会牺牲一部分峰值算力,而且支持的卡型有限。如果集群中混有多种GPU,建议在节点标签中标记卡型和MIG能力,通过nodeSelector或nodeAffinity把租户调度到合适的硬件上。
独立计费的两种落地模式
计费模型通常分为按调用次数和按资源占用时长两种。按调用次数适合API化的推理服务,比如每次文本生成或图像识别固定计费;按资源时长则适合长期运行的批量推理任务。无论哪种方式,都需要从监控系统中提取准确的租户维度指标。
如果推理引擎支持Prometheus指标暴露,可以用tenant标签来区分每个租户的请求数和显存使用。例如vLLM自带vllm:num_requests_total指标,通过在启动参数中注入租户标识,可以让每个实例的指标带上tenant标签。下面的PromQL可以计算出某个租户在过去一小时内的推理总次数:
sum(increase(vllm:num_requests_total{tenant="tenant-a"}[1h])) by (tenant)
按调用次数计费时,只需要把这个数值乘以单价即可。如果还需要统计每次调用的平均显存占用和耗时,可以拉取vllm:gpu_cache_usage_perc和vllm:time_to_first_token_seconds等指标,生成更精细的SLA报告。计费系统可以定期从Prometheus查询这些数据,写入账单数据库。
按GPU时长计费则更适合短租或任务型场景。可以在Pod的annotation中记录租户ID和单位价格,当Pod被调度到GPU节点后,通过NVIDIA DCGM导出GPU利用率,计算该租户实际占用的GPU时间。一个简化的计费逻辑如下:
def calculate_tenant_bill(tenant_id, start_time, end_time):
gpu_usage = query_dcgm_metrics(tenant_id, start_time, end_time)
total_gpu_seconds = sum(sample.value for sample in gpu_usage)
price_per_gpu_hour = 12.5 # 每GPU小时单价
return total_gpu_seconds / 3600 * price_per_gpu_hour
这段代码从DCGM的指标库中取出租户在指定时间段内的GPU利用率采样点,累加得到GPU占用秒数,再换算成小时并乘以单价。实际生产环境中还需要处理GPU利用率低于阈值时的折扣、空闲时间的摊销方式等策略,但整体思路是清晰的:资源监控数据是可计费的基础。
独立计费还涉及账单的对账和导出。建议把每个时间段内的原始监控数据按租户聚合后保存到对象存储,避免直接依赖实时监控系统做历史账单。这样当客户对费用有疑问时,可以随时回溯原始数据,也便于审计和财务核算。
部署实践中容易踩的坑
第一个坑是显存碎片化。即使通过MIG或内存限制做了隔离,频繁地加载和卸载大模型也会让GPU显存出现碎片,导致无法分配连续的大块内存。解决办法是在推理服务启动时预先分配好KV Cache,并关闭动态扩容,让显存占用保持稳定。
第二个坑是冷启动延迟。如果为每个租户独立部署实例,当租户没有流量时实例被缩容,下一次请求到来时需要重新加载模型权重,可能需要几十秒甚至几分钟。对于对延迟敏感的推理服务,建议保留一个最小副本数,或者使用模型预热的机制把权重常驻在GPU显存中。
第三个坑是网络隔离与安全。多租户环境里,推理服务往往需要和租户自己的存储或数据库通信。如果网络策略配置不当,租户A的Pod可能会访问到租户B的私有服务。务必在每个命名空间中应用NetworkPolicy,只允许必要的出口流量,并在入口方向限制来源IP或标签。
最后还要注意计费数据的时间对齐问题。监控系统的时间戳可能存在秒级偏差,如果账单按自然小时切割,会与Pod运行的精确时间产生误差。建议在计费系统中使用UTC时间戳统一处理,并且保留至少一位小数的精度,避免因四舍五入造成客户投诉。