欢迎光临
我们一直在努力

AI 推理服务的多模型共存:同集群部署不同版本的策略

AI 推理服务的多模型共存:同集群部署不同版本的策略

一、集群里同时跑了 vLLM 0.4、TGI 1.3、TensorRT-LLM 三个版本,资源分配一团糟

AI 推理服务的版本管理比传统微服务复杂得多。传统微服务部署的是"同一个 Docker 镜像的不同版本"——v1、v2、v3,管理起来是标准的 K8s Deployment 滚动更新。AI 推理服务不一样——不同模型(GPT-4、Llama-3、Claude)、不同推理引擎(vLLM、TGI、TensorRT-LLM)、不同量化版本(FP16、INT8、INT4)可能同时运行在同一个集群中。

问题不在于"能不能共存"——K8s 天然支持多 Deployment。问题在于"资源怎么分配"。AI 推理是 GPU 密集型负载,GPU 是不可压缩资源——同一张 GPU 卡不能同时被两个推理进程使用(除非用 MIG 切分)。所以多模型共存的核心挑战是 GPU 资源的调度和隔离。

策略的三个层次:水平切分(不同模型跑在不同 GPU 上)、垂直切分(同一 GPU 上用 MIG 隔离不同模型)、调度混合(GPU 和 CPU 混合部署,轻量推理用 CPU 兜底)。

二、底层机制与原理剖析

多模型共存的三层策略:

水平切分(不同 GPU):最简单的策略。每个模型独占一张 GPU 卡。优点:隔离性最好(一个模型出问题不影响其他模型)、推理性能不受干扰。缺点:资源利用率低——Llama-3-70B 占 70GB 显存,剩下 10GB 白白浪费。

MIG 垂直切分(Mult-Instance GPU):A100/H100 支持 MIG(多实例 GPU),把一张 GPU 显存切分成多个独立实例。例如:一张 80GB 的 A100 切成 5GB + 10GB + 30GB + 35GB 四个实例。每个实例有独立的内存和缓存,互不干扰。优点:显存利用率高。缺点:只支持特定 GPU 型号(A100、A30、H100)、切分后的实例互相隔离无法共享计算力。

Time-Slicing(时间片共享):没有真正的显存隔离,多个推理进程轮流占用 GPU 计算单元。优点:实现简单、不依赖 GPU 型号。缺点:切换开销(每次切换需要重新加载模型权重)、显存竞争可能导致 OOM。

三、生产级代码实现

# k8s/vllm-llama3-70b.yaml
# vLLM + Llama-3-70B 部署配置
# 设计要点:
# 1. 独占 GPU(不与其他模型共享)——稳定优先
# 2. nodeSelector 指定 GPU 型号——不同模型对 GPU 架构有要求
# 3. 资源 Requests = Limits —— GPU 资源保证性

apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-llama3-70b
namespace: ai-inference
labels:
app: vllm-llama3-70b
model: llama-3-70b
engine: vllm
version: v0.4.2
annotations:
# 标记模型信息供路由发现
model.workbuddy.tech/name: "llama-3-70b"
model.workbuddy.tech/engine: "vllm"
model.workbuddy.tech/quantization: "fp16"
prometheus.io/scrape: "true"
spec:
replicas: 1 # GPU 推理通常 1 副本(多副本需要多张 GPU)
selector:
matchLabels:
app: vllm-llama3-70b
template:
metadata:
labels:
app: vllm-llama3-70b
spec:
# GPU 节点亲和性
nodeSelector:
nvidia.com/gpu.product: "NVIDIA-A100-SXM4-80GB"
tolerations:
– key: "nvidia.com/gpu"
operator: "Exists"
effect: "NoSchedule"

containers:
– name: vllm
image: vllm/vllm-openai:v0.4.2
args:
– "–model"
– "meta-llama/Meta-Llama-3-70B-Instruct"
– "–host"
– "0.0.0.0"
– "–port"
– "8000"
– "–tensor-parallel-size"
– "1" # 单 GPU:TP=1;如果跨 GPU 推理设为 2
– "–gpu-memory-utilization"
– "0.90" # 留 10% 给 CUDA context
– "–max-model-len"
– "8192" # 上下文长度
env:
– name: CUDA_VISIBLE_DEVICES
value: "0"
– name: NVIDIA_VISIBLE_DEVICES
value: "0"
ports:
– containerPort: 8000
name: http
resources:
requests:
nvidia.com/gpu: 1
memory: "80Gi" # 模型 + KV Cache 的显存需求
cpu: "8"
limits:
nvidia.com/gpu: 1
memory: "80Gi"
cpu: "16"
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 60 # 模型加载需要时间
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 120
periodSeconds: 30
volumeMounts:
– name: model-storage
mountPath: /models
readOnly: true

volumes:
– name: model-storage
persistentVolumeClaim:
claimName: pvc-llama-models

# k8s/mig-tgi-whisper.yaml
# 使用 MIG 后的 TGI + Whisper 部署
# MIG 实例通过 nvidia.com/mig-3g.20gb 这种扩展资源申请

apiVersion: apps/v1
kind: Deployment
metadata:
name: tgi-whisper-large
namespace: ai-inference
labels:
app: tgi-whisper-large
model: whisper-large-v3
engine: tgi
spec:
replicas: 1
selector:
matchLabels:
app: tgi-whisper-large
template:
spec:
nodeSelector:
nvidia.com/gpu.product: "NVIDIA-A100-SXM4-80GB"
containers:
– name: tgi
image: ghcr.io/huggingface/text-generation-inference:1.3
args:
– "–model-id"
– "openai/whisper-large-v3"
– "–num-shard"
– "1"
– "–max-total-tokens"
– "4096"
resources:
requests:
nvidia.com/mig-3g.20gb: 1 # 申请 20GB MIG 实例
memory: "24Gi"
limits:
nvidia.com/mig-3g.20gb: 1
memory: "24Gi"

# model_router.py
"""
多模型路由器:根据请求中的 model 参数,将请求路由到正确的推理引擎实例

设计要点:
1. 基于 K8s Service + label selector 实现服务发现
2. 支持模型→引擎→实例的映射
3. 熔断:如果某个模型实例全部不可用,返回 503 而非无限等待
"""

import os
import logging
import threading
from typing import Dict, Optional, List
from dataclasses import dataclass, field
from urllib.request import Request, urlopen
from urllib.error import URLError
import json

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@dataclass
class ModelEndpoint:
"""推理引擎实例的端点信息"""
url: str
model: str
engine: str
version: str
weight: int = 1 # 流量权重
priority: int = 0 # 优先级(数字越小越高)
healthy: bool = True # 健康状态

# 延迟统计
avg_latency_ms: float = 0
error_count: int = 0

class ModelRouter:
"""
模型路由器

为什么自己实现而不是用 Istio / Envoy?
模型路由需求的特殊性:
– 需要按 model name 路由(不是 Host header)
– 需要感知模型列表的动态变化(新模型加入、旧模型下线)
– 有些模型有多个实例(负载均衡),有些只有一个(主备)
"""

def __init__(self, k8s_namespace: str = "ai-inference"):
self.namespace = k8s_namespace
self._endpoints: Dict[str, List[ModelEndpoint]] = {}
self._lock = threading.RLock()

# 静态模型映射(K8s Service discovery 的简化版)
# 生产环境应从 K8s API 动态发现
self._static_endpoints = {
"llama-3-70b": [
ModelEndpoint(
url="http://vllm-llama3-70b.ai-inference.svc:8000",
model="llama-3-70b",
engine="vllm",
version="0.4.2",
priority=0,
),
],
"gpt-4-mini": [
ModelEndpoint(
url="http://vllm-gpt4-mini-mig.ai-inference.svc:8000",
model="gpt-4-mini",
engine="vllm",
version="0.4.2",
priority=0,
),
],
"qwen-72b": [
ModelEndpoint(
url="http://trt-qwen-72b.ai-inference.svc:8000",
model="qwen-72b",
engine="tensorrt-llm",
version="0.8.0",
priority=1,
),
],
"whisper-large": [
ModelEndpoint(
url="http://tgi-whisper.ai-inference.svc:8000",
model="whisper-large-v3",
engine="tgi",
version="1.3",
priority=0,
),
],
}

self._endpoints = dict(self._static_endpoints)

# 启动后台健康检查
self._start_health_checks()

def resolve(self, model_name: str) -> Optional[ModelEndpoint]:
"""
为指定模型解析一个可用的端点

策略:
1. 精确匹配 model name
2. 从可用实例中选择优先级最高 + 健康的实例
3. 如果有多个同等优先级实例,按权重随机选择
"""
with self._lock:
endpoints = self._endpoints.get(model_name, [])
# 过滤健康实例
healthy = [ep for ep in endpoints if ep.healthy]
if not healthy:
logger.warning("No healthy endpoint for model: %s", model_name)
return None

# 按优先级排序
healthy.sort(key=lambda ep: ep.priority)

# 选择最高优先级的实例
# (如果同一优先级有多个,按权重选择——简化实现直接轮询)
top_priority = healthy[0].priority
candidates = [ep for ep in healthy if ep.priority == top_priority]
# 简单的加权随机(生产环境用更精细的算法)
import random
return random.choices(
candidates,
weights=[ep.weight for ep in candidates],
k=1,
)[0]

def mark_unhealthy(self, model_name: str, endpoint_url: str):
"""标记某个端点为不健康(在请求失败后调用)"""
with self._lock:
endpoints = self._endpoints.get(model_name, [])
for ep in endpoints:
if ep.url == endpoint_url:
ep.healthy = False
ep.error_count += 1
logger.warning("Marked %s unhealthy: %s", model_name, endpoint_url)

def _start_health_checks(self):
"""启动后台健康检查线程"""
self._health_thread = threading.Thread(
target=self._health_check_loop, daemon=True
)
self._health_thread.start()

def _health_check_loop(self):
"""周期性的健康检查——恢复标记为 unhealthy 的端点"""
import time

while True:
time.sleep(15) # 每 15 秒检查一次
with self._lock:
for model, endpoints in self._endpoints.items():
for ep in endpoints:
if not ep.healthy:
if self._check_endpoint(ep):
ep.healthy = True
ep.error_count = 0
logger.info("Recovered %s/%s: %s", model, ep.engine, ep.url)

@staticmethod
def _check_endpoint(ep: ModelEndpoint) -> bool:
"""检查单个端点是否健康"""
try:
req = Request(f"{ep.url}/health", method="GET")
with urlopen(req, timeout=5) as resp:
return resp.status == 200
except Exception:
return False

# —————————————————————————
# 使用示例:在 API Gateway 中集成路由器
# —————————————————————————
if __name__ == "__main__":
router = ModelRouter()

# 模拟请求:用户指定了模型
requests = [
{"model": "llama-3-70b", "prompt": "Hello"},
{"model": "qwen-72b", "prompt": "你好"},
{"model": "nonexistent-model", "prompt": "test"}, # 不存在的模型
]

for req in requests:
endpoint = router.resolve(req["model"])
if endpoint:
print(f"[{req['model']}] → {endpoint.url} ({endpoint.engine})")
else:
print(f"[{req['model']}] → 无可用实例 (503)")

四、边界分析与架构权衡

MIG 的限制:

  • MIG 切分后每个实例的显存和缓存是隔离的——如果一个大模型需要 50GB 显存但 MIG 实例只有 30GB,它就跑不了
  • MIG 实例间的通信效率不如统一显存——跨 MIG 实例的 tensor 传输需要走 PCIe
  • 不是所有 GPU 都支持 MIG——A100、A30、H100 支持,V100、T4 不支持

多模型共存的内存风险:

  • K8s 的 requests: nvidia.com/gpu: 1 只是保证容器能访问 GPU——不保证显存分配。两个容器各请求 1 个 GPU 但运行在同一张卡上,可能同时 OOM
  • 解决方案:使用 GPU Operator 的 Time-Slicing 配置 + 显存限制(–gpu-memory-utilization)

生产环境建议:

  • 核心模型(高流量、高优先级):独占 GPU,保证性能稳定
  • 非核心模型:MIG 或 Time-Slicing 共享 GPU
  • 轻量模型(如 embedding、分类器):CPU 推理(ONNX Runtime),节省 GPU 资源
  • 制定 GPU"配额"制度:每个团队/业务线有 GPU 时间的配额上限

五、总结

AI 推理服务的多模型共存,核心矛盾是 GPU 资源如何分配。水平切分简单但利用率低,MIG 垂直切分利用率高但灵活度受限,Time-Slicing 灵活但隔离性差。建议的核心模型独占 GPU(稳),非核心模型共享 GPU(省),轻量模型用 CPU 推理(更省)。路由器(Model Router)是这一切的入口——让用户在请求时指定模型名,路由层负责将请求转发到正确的推理实例,用户不需要知道底层是哪个引擎、哪个 GPU 在跑。

赞(0)
未经允许不得转载:171主机测评 » AI 推理服务的多模型共存:同集群部署不同版本的策略
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址