大模型部署实战:从 GPU 显存管理到弹性推理服务的工程化落地

一、推理部署的"最后一公里":当模型跑不起来

某企业训练了一个 7B 参数的业务模型,准备部署到生产环境。运维团队申请了 4 张 A100 GPU,但模型加载后显存占用就达到 48GB,4 张卡只剩 2 张可用。更糟糕的是,并发请求达到 10 QPS 时,推理延迟从 200ms 飙升到 5s,GPU 利用率却只有 30%——大量时间花在请求排队和内存拷贝上,GPU 计算单元处于空闲等待状态。

大模型部署的核心矛盾是:GPU 显存是稀缺资源,但推理吞吐量受限于显存管理效率。模型参数、KV Cache、请求批处理,三者竞争同一块显存。如何在这三者之间合理分配显存,直接决定了推理服务的吞吐量和延迟。

本文将围绕大模型推理部署的三个核心问题展开:显存管理、推理引擎选型、弹性伸缩策略。

二、推理部署的架构分层与显存分配

大模型推理服务的架构可以分为三层:模型服务层、推理引擎层、基础设施层。每一层都有不同的显存管理策略和性能优化手段。

graph TB
    subgraph "模型服务层"
        API["API Gateway<br/>限流 / 路由 / 认证"]
        Batcher["请求批处理器<br/>动态 Batch 合并<br/>提升 GPU 利用率"]
    end

    subgraph "推理引擎层"
        VLLM["vLLM<br/>PagedAttention<br/>虚拟内存管理 KV Cache"]
        TGI["TGI<br/>Flash Attention<br/>连续批处理"]
        Triton["NVIDIA Triton<br/>多模型调度<br/>GPU 共享"]
    end

    subgraph "基础设施层"
        GPU["GPU 集群<br/>A100 / H100"]
        K8s["Kubernetes<br/>GPU 调度 / 弹性伸缩"]
        Storage["模型存储<br/>S3 / NFS<br/>模型分片加载"]
    end

    subgraph "显存分配"
        Weights["模型权重<br/>7B 模型 ≈ 14GB<br/>FP16 精度"]
        KVCache["KV Cache<br/>每个请求 ≈ 1-2GB<br/>上下文长度决定"]
        Activation["激活值 / 临时缓冲<br/>批处理时动态分配"]
    end

    API --> Batcher
    Batcher --> VLLM
    Batcher --> TGI
    VLLM --> GPU
    TGI --> GPU
    Triton --> GPU
    GPU --> Weights
    GPU --> KVCache
    GPU --> Activation
    K8s --> GPU
    Storage --> VLLM
    Storage --> TGI

    style KVCache fill:#ffcdd2
    style Weights fill:#c8e6c9
    style Activation fill:#fff9c4

显存分配的三方博弈

以 7B 模型(FP16 精度)为例,模型权重占用约 14GB。一张 A100(80GB)扣除权重后剩余 66GB。如果每个请求的 KV Cache 占用 1.5GB(2048 Token 上下文),则最多支持 44 个并发请求。但实际中还需要预留激活值和临时缓冲,可用并发数通常在 20-30 之间。

KV Cache 是显存消耗的大头。上下文越长,KV Cache 越大。4K 上下文的 KV Cache 是 2K 上下文的两倍。这意味着长上下文场景下,并发能力会显著下降。

PagedAttention:vLLM 的显存管理创新

vLLM 的 PagedAttention 借鉴了操作系统的虚拟内存分页机制。传统推理引擎为每个请求预分配一块连续的 KV Cache 显存,但实际使用中 KV Cache 的长度随生成过程增长,预分配要么浪费(分配过多),要么溢出(分配不足)。

PagedAttention 将 KV Cache 划分为固定大小的 Block(类似内存页),按需分配。不同请求的 Block 可以不连续,通过页表映射。这消除了显存碎片,将 KV Cache 的显存利用率从 60%-70% 提升到 95% 以上。

三、基于 vLLM + Kubernetes 的弹性推理服务实现

vLLM 推理服务部署

# vLLM 推理服务启动脚本
# 关键参数说明:
#   --gpu-memory-utilization: GPU 显存使用率上限,默认 0.9
#   --max-model-len: 最大上下文长度,直接影响 KV Cache 大小
#   --max-num-seqs: 最大并发序列数,控制吞吐上限
#   --enable-prefix-caching: 前缀缓存,复用相同 Prompt 前缀的 KV Cache

from vllm import LLM, SamplingParams
from vllm.entrypoints.openai.api_server import run_server

# 启动 OpenAI 兼容的推理服务
# 生产环境建议通过 CLI 启动,便于参数管理
"""
python -m vllm.entrypoints.openai.api_server \
    --model /models/qwen2-7b \
    --served-model-name qwen2-7b \
    --gpu-memory-utilization 0.85 \
    --max-model-len 4096 \
    --max-num-seqs 64 \
    --enable-prefix-caching \
    --port 8000 \
    --host 0.0.0.0
"""

Kubernetes GPU 弹性伸缩

# 推理服务 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: llm-inference
  namespace: ai-serving
spec:
  replicas: 2
  selector:
    matchLabels:
      app: llm-inference
  template:
    metadata:
      labels:
        app: llm-inference
    spec:
      containers:
        - name: vllm
          image: vllm/vllm-openai:latest
          resources:
            limits:
              nvidia.com/gpu: 1  # 每个 Pod 占用 1 张 GPU
            requests:
              nvidia.com/gpu: 1
          env:
            - name: MODEL_PATH
              value: "/models/qwen2-7b"
          ports:
            - containerPort: 8000
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 120  # 模型加载需要时间
            periodSeconds: 10
          volumeMounts:
            - name: model-storage
              mountPath: /models
      volumes:
        - name: model-storage
          persistentVolumeClaim:
            claimName: model-pvc
---
# 基于 GPU 利用率和请求队列长度的 HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: llm-inference-hpa
  namespace: ai-serving
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: llm-inference
  minReplicas: 2
  maxReplicas: 8
  metrics:
    # 指标一:GPU 利用率超过 70% 时扩容
    - type: Pods
      pods:
        metric:
          name: gpu_utilization
        target:
          type: AverageValue
          averageValue: "70"
    # 指标二:请求队列长度超过 10 时扩容
    - type: Pods
      pods:
        metric:
          name: vllm_num_requests_waiting
        target:
          type: AverageValue
          averageValue: "10"
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
        - type: Pods
          value: 2
          periodSeconds: 120
    scaleDown:
      stabilizationWindowSeconds: 300  # 缩容冷却期 5 分钟
      policies:
        - type: Pods
          value: 1
          periodSeconds: 120

Java 后端调用推理服务

@Service
public class LocalInferenceService {

    private final RestTemplate restTemplate;
    private final MeterRegistry meterRegistry;

    @Value("${llm.inference.url}")
    private String inferenceUrl;

    /**
     * 调用本地推理服务
     * 兼容 OpenAI API 格式,便于与 Spring AI 集成
     */
    public InferenceResponse infer(InferenceRequest request) {
        String url = inferenceUrl + "/v1/completions";

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        Map<String, Object> body = Map.of(
            "model", request.getModel(),
            "prompt", request.getPrompt(),
            "max_tokens", request.getMaxTokens(),
            "temperature", request.getTemperature(),
            "top_p", 0.9
        );

        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);

        try {
            long start = System.nanoTime();
            ResponseEntity<Map> response = restTemplate.exchange(
                url, HttpMethod.POST, entity, Map.class);
            long durationMs = (System.nanoTime() - start) / 1_000_000;

            meterRegistry.timer("llm.local.inference.duration")
                .record(durationMs, TimeUnit.MILLISECONDS);

            Map responseBody = response.getBody();
            Map usage = (Map) responseBody.get("usage");

            // 记录 Token 用量
            meterRegistry.counter("llm.local.tokens.input")
                .increment(((Number) usage.get("prompt_tokens")).longValue());
            meterRegistry.counter("llm.local.tokens.output")
                .increment(((Number) usage.get("completion_tokens")).longValue());

            return InferenceResponse.from(responseBody);

        } catch (ResourceAccessException e) {
            // 推理服务超时,触发降级
            meterRegistry.counter("llm.local.inference.timeout").increment();
            throw new InferenceTimeoutException("本地推理服务超时", e);
        }
    }
}

四、推理部署的架构权衡

云 API vs 本地部署的成本拐点

维度 云 API(OpenAI/Claude) 本地部署(vLLM + GPU)
初始成本 GPU 服务器 + 运维
单次调用成本 固定(按 Token 计费) 随调用量摊薄
数据隐私 数据经过第三方 数据不出机房
模型选择 供应商限定 完全自主
弹性伸缩 供应商负责 自建 HPA

成本拐点通常在日均调用量超过 100 万次 Token 时。低于此阈值,云 API 的总成本更低;高于此阈值,本地部署的边际成本趋近于零,总成本更低。但本地部署的运维成本(GPU 故障、驱动升级、模型更新)不可忽视。

量化精度与推理质量的平衡

模型量化(INT8、INT4)可以显著降低显存占用和推理延迟。7B 模型从 FP16 量化到 INT4 后,显存占用从 14GB 降到 4GB,推理速度提升 2-3 倍。但量化会带来精度损失,在代码生成、数学推理等对精度敏感的任务上,INT4 量化可能导致输出质量显著下降。

选择标准是:对话类任务可以接受 INT4 量化,代码和数学任务建议至少 INT8 量化,关键业务场景保持 FP16。

冷启动与模型加载时间

GPU 推理服务的冷启动时间通常在 30-120 秒(模型加载 + 预热)。在弹性伸缩场景下,流量突增时新 Pod 的冷启动延迟可能导致请求排队超时。解决方案包括:保持最低副本数(minReplicas >= 2)、模型预热(启动后发送预热请求)、模型分片加载(将模型权重分散存储在多张 GPU 上并行加载)。

多模型共存的 GPU 共享

当需要同时部署多个模型时,GPU 资源分配成为难题。NVIDIA MPS(Multi-Process Service)和 Triton Inference Server 的 GPU 共享功能,允许多个模型共享同一张 GPU。但共享意味着资源竞争,一个模型的推理峰值可能影响另一个模型的延迟。在延迟敏感场景下,每个模型独占 GPU 是更稳妥的选择。

五、总结

大模型推理部署的核心挑战是显存管理。模型权重、KV Cache、激活值三方竞争有限的 GPU 显存,合理的分配策略直接决定吞吐量和延迟。vLLM 的 PagedAttention 通过虚拟内存分页机制,将 KV Cache 的显存利用率提升到 95% 以上,是当前最成熟的推理引擎之一。

本地部署与云 API 的选择,取决于调用量、隐私要求和运维能力。日均 100 万 Token 是成本拐点的粗略参考。量化精度与推理质量需要根据任务类型权衡,对话类任务可接受 INT4,代码和数学任务建议 INT8 以上。

落地路线建议:第一阶段使用云 API 验证业务可行性,积累 Token 用量基线数据;第二阶段在单机环境部署 vLLM,验证本地推理的延迟和吞吐;第三阶段迁移到 Kubernetes + GPU HPA,实现弹性伸缩;第四阶段根据成本数据决定云 API 与本地部署的混合比例,建立成本-延迟-质量的三维决策模型。

Logo

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

更多推荐