大模型推理服务部署:从模型加载到弹性扩缩容的工程实践

cover

一、大模型推理部署的三大工程瓶颈:显存、延迟与冷启动

将大语言模型从实验环境推向生产服务,需要跨越三道工程瓶颈。第一道是显存瓶颈:一个 7B 参数模型在 FP16 精度下需要约 14GB 显存,加上 KV Cache 和运行时开销,单卡 A100(80GB)最多同时服务 2-3 个并发请求。第二道是延迟瓶颈:自回归解码的特性决定了 Token 逐个生成,首 Token 延迟(TTFT)和每 Token 生成延迟(TPOT)直接影响用户体验。第三道是冷启动瓶颈:模型从磁盘加载到 GPU 显存需要 30-60 秒,Pod 扩容后无法立即承接流量。

这三个瓶颈不是孤立的,而是相互制约的。增大 Batch Size 可以提高吞吐量,但会增加延迟;模型量化可以降低显存占用,但会损失精度;预留 GPU 实例可以消除冷启动,但会大幅增加成本。如何在三者之间找到最优平衡点,是大模型推理服务部署的核心命题。

二、推理服务架构:模型服务层、调度层与网关层的协同

生产级大模型推理服务通常分为三层:模型服务层负责模型加载与推理执行,调度层负责请求路由与负载均衡,网关层负责协议转换与流量管理。

flowchart TD
    CLIENT[客户端请求] --> GW[API 网关层<br/>协议转换 / 限流 / 认证]

    subgraph 调度层
        ROUTER[请求路由器]
        LB[负载均衡器<br/>基于队列深度调度]
        QUEUE[请求队列<br/>优先级排序]
    end

    GW --> ROUTER
    ROUTER --> LB
    LB --> QUEUE

    subgraph 模型服务层
        subgraph Pod1[推理 Pod 1]
            ENGINE1[vLLM 推理引擎]
            GPU1[GPU 显存<br/>模型权重 + KV Cache]
        end
        subgraph Pod2[推理 Pod 2]
            ENGINE2[vLLM 推理引擎]
            GPU2[GPU 显存<br/>模型权重 + KV Cache]
        end
        subgraph Pod3[推理 Pod 3 - 冷备]
            ENGINE3[vLLM 推理引擎<br/>模型已预加载]
            GPU3[GPU 显存<br/>待激活]
        end
    end

    QUEUE -->|活跃请求| ENGINE1
    QUEUE -->|活跃请求| ENGINE2
    QUEUE -->|溢出请求| ENGINE3

    subgraph 监控与扩缩容
        METRICS[指标采集<br/>队列长度 / GPU利用率 / TTFT]
        HPA[HPA 控制器<br/>基于指标驱动扩缩容]
    end

    ENGINE1 --> METRICS
    ENGINE2 --> METRICS
    METRICS --> HPA
    HPA -->|扩容| Pod3

    style GW fill:#e74c3c,color:#fff
    style QUEUE fill:#e67e22,color:#fff
    style ENGINE1 fill:#3498db,color:#fff
    style ENGINE2 fill:#3498db,color:#fff
    style ENGINE3 fill:#95a5a6,color:#fff
    style HPA fill:#27ae60,color:#fff

模型服务层:采用 vLLM 作为推理引擎,核心优势是 PagedAttention 机制。传统推理引擎为每个请求预分配固定大小的 KV Cache,导致大量显存碎片。PagedAttention 借鉴操作系统的虚拟内存分页机制,将 KV Cache 划分为固定大小的 Block,按需分配和回收,显存利用率从 60% 提升到 90% 以上。

调度层:请求路由器根据模型版本和用户特征选择目标服务实例,负载均衡器基于队列深度(而非简单的轮询)分配请求。队列深度调度确保请求被分配到负载最轻的实例,避免某些实例过载而其他实例空闲。

网关层:将 OpenAI 兼容的 HTTP API 转换为模型服务层的 gRPC 调用,同时负责限流、认证和流式响应的 SSE 转换。

三、生产级部署实现

3.1 vLLM 推理服务的 Kubernetes 部署

# vLLM 推理服务 Deployment
# 关键设计:使用 GPU 亲和性调度,确保 Pod 调度到有 GPU 的节点
# preStop 钩子确保优雅关闭,避免正在推理的请求被中断
apiVersion: apps/v1
kind: Deployment
metadata:
  name: llm-inference-server
  namespace: ai-production
spec:
  replicas: 2
  selector:
    matchLabels:
      app: llm-inference
  template:
    metadata:
      labels:
        app: llm-inference
        version: v1
    spec:
      # GPU 亲和性:优先调度到 A100 节点
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: nvidia.com/gpu.product
                    operator: In
                    values:
                      - NVIDIA-A100-SXM4-80GB
      containers:
        - name: vllm-server
          image: vllm/vllm-openai:latest
          resources:
            limits:
              nvidia.com/gpu: 1
            requests:
              nvidia.com/gpu: 1
              cpu: "4"
              memory: "16Gi"
          # vLLM 启动参数
          # --max-model-len 控制最大上下文长度,直接影响 KV Cache 显存占用
          # --gpu-memory-utilization 设为 0.9,预留 10% 给 CUDA 内核开销
          # --enable-prefix-caching 开启前缀缓存,相同 prompt 前缀可复用 KV Cache
          command:
            - python
            - -m
            - vllm.entrypoints.openai.api_server
            - --model
            - /models/qwen2-7b-instruct
            - --served-model-name
            - qwen2-7b
            - --max-model-len
            - "8192"
            - --gpu-memory-utilization
            - "0.9"
            - --enable-prefix-caching
            - --host
            - "0.0.0.0"
            - --port
            - "8000"
          ports:
            - containerPort: 8000
          # 就绪探针:确认模型加载完成后再接收流量
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 60
            periodSeconds: 10
          # 优雅关闭:等待推理中的请求完成
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 30"]
      terminationGracePeriodSeconds: 60

3.2 基于队列深度的智能负载均衡

"""
基于队列深度的负载均衡器
核心思路:将请求路由到队列深度最浅的推理实例
而非简单的轮询,避免热点实例过载
"""
import asyncio
import aiohttp
from dataclasses import dataclass, field
from typing import List, Optional


@dataclass
class InferenceInstance:
    """推理服务实例"""
    url: str
    # 当前队列中的请求数
    queue_depth: int = 0
    # 最大并发请求数,由 GPU 显存和模型大小决定
    max_concurrent: int = 10
    # 是否健康
    healthy: bool = True

    @property
    def available_capacity(self) -> int:
        """剩余可用容量"""
        return max(0, self.max_concurrent - self.queue_depth)


class QueueDepthLoadBalancer:
    """基于队列深度的负载均衡器"""

    def __init__(self, instances: List[InferenceInstance]):
        self.instances = instances
        self._lock = asyncio.Lock()

    async def select_instance(
        self, prefer_instance: Optional[str] = None
    ) -> Optional[InferenceInstance]:
        """
        选择最优实例
        优先选择指定实例(亲和性调度),否则选队列最浅的
        """
        async with self._lock:
            # 如果有亲和性要求,优先使用指定实例
            if prefer_instance:
                for inst in self.instances:
                    if (inst.url == prefer_instance
                            and inst.healthy
                            and inst.available_capacity > 0):
                        inst.queue_depth += 1
                        return inst

            # 按可用容量降序排列,选择容量最大的实例
            # 这样设计是因为推理请求耗时长,队列积压会快速恶化延迟
            healthy_instances = [
                inst for inst in self.instances
                if inst.healthy and inst.available_capacity > 0
            ]

            if not healthy_instances:
                return None

            # 选择可用容量最大的实例
            selected = max(
                healthy_instances,
                key=lambda x: x.available_capacity
            )
            selected.queue_depth += 1
            return selected

    async def release_instance(self, url: str):
        """请求完成后释放实例的队列计数"""
        async with self._lock:
            for inst in self.instances:
                if inst.url == url:
                    inst.queue_depth = max(
                        0, inst.queue_depth - 1
                    )
                    break

    async def health_check(self):
        """定期健康检查,标记不健康实例"""
        while True:
            for inst in self.instances:
                try:
                    async with aiohttp.ClientSession() as session:
                        async with session.get(
                            f"{inst.url}/health",
                            timeout=aiohttp.ClientTimeout(total=5)
                        ) as resp:
                            inst.healthy = resp.status == 200
                except Exception:
                    inst.healthy = False
            await asyncio.sleep(10)

3.3 模型预加载消除冷启动

"""
模型预加载服务
核心思路:在 Pod 启动时就加载模型到 GPU 显存
但暂不注册到负载均衡器,作为温备实例
当 HPA 触发扩容时,温备实例可秒级上线
"""
import subprocess
import logging
import time

logger = logging.getLogger(__name__)


class ModelPreloader:
    """模型预加载管理器"""

    def __init__(self, model_path: str, preload_count: int = 1):
        self.model_path = model_path
        self.preload_count = preload_count

    def preload_model_to_gpu(self) -> bool:
        """
        将模型权重从磁盘加载到 GPU 显存
        使用 CUDA pinned memory 加速加载
        加载完成后模型常驻显存,等待推理请求
        """
        try:
            start_time = time.time()
            # 通过 vLLM 的离线加载接口预加载模型
            # --load-format 使用 safetensors,比 PyTorch 格式加载更快
            result = subprocess.run(
                [
                    "python", "-c",
                    f"""
import vllm
from vllm import LLM
# 预加载模型到 GPU,不启动服务
llm = LLM(
    model="{self.model_path}",
    load_format="safetensors",
    gpu_memory_utilization=0.9,
    enforce_eager=True
)
print("MODEL_LOADED")
"""
                ],
                capture_output=True,
                text=True,
                timeout=300
            )

            if "MODEL_LOADED" in result.stdout:
                elapsed = time.time() - start_time
                logger.info(
                    f"模型预加载完成, 耗时={elapsed:.1f}s"
                )
                return True
            else:
                logger.error(
                    f"模型预加载失败: {result.stderr}"
                )
                return False

        except subprocess.TimeoutExpired:
            logger.error("模型预加载超时")
            return False
        except Exception as e:
            logger.error(f"模型预加载异常: {e}")
            return False

四、推理服务部署的代价:GPU 成本、显存碎片与扩缩容滞后

GPU 成本:A100 GPU 的云上单价约为 20-30 元/小时,一个 7B 模型至少需要 1 张 A100。如果按峰值并发预留 GPU 实例,资源利用率通常不到 30%。通过分时复用(不同时段部署不同模型)和 Spot 实例可以降低成本,但增加了调度复杂度。

显存碎片:即使使用 PagedAttention,长上下文请求(如 8K Token)的 KV Cache 仍会占用大量显存 Block。当长短请求混合时,长请求的 KV Cache 占用导致短请求排队等待。解决方案是按上下文长度分池调度,将长请求和短请求路由到不同的实例。

扩缩容滞后:从 HPA 触发扩容到新 Pod 就绪(模型加载完成),通常需要 60-120 秒。在这段时间内,突增的流量只能由现有实例承担,可能导致队列积压和延迟飙升。温备实例可以将上线时间缩短到 5-10 秒,但需要额外预留 GPU 资源。

五、总结

大模型推理服务的部署核心是在显存、延迟和成本三者之间找到平衡。vLLM 的 PagedAttention 机制通过分页管理 KV Cache 显著提升显存利用率;基于队列深度的负载均衡避免热点实例过载;模型预加载和温备实例缓解冷启动问题。但 GPU 成本高、扩缩容滞后等根本性约束仍然存在,需要通过请求调度优化和弹性策略来缓解。

落地路线建议:第一步,使用 vLLM 部署推理服务,开启 PagedAttention 和前缀缓存;第二步,实现基于队列深度的负载均衡,替代简单的轮询策略;第三步,配置 Kubernetes HPA,基于 GPU 利用率和队列长度指标驱动扩缩容;第四步,部署温备实例池,将扩容响应时间从分钟级缩短到秒级;第五步,建立 TTFT/TPOT 监控看板,持续优化 Batch Size 和并发参数。

Logo

免费领 200 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐