大模型推理部署实战:从 GPU 显存管理到高并发服务化的全链路设计

cover

一、Token 吞吐与显存瓶颈:LLM 部署的工程困境

大模型推理部署的核心矛盾在于:GPU 显存是有限的,而模型参数和 KV Cache 的显存需求几乎是无上限的。一个 70B 参数的模型,仅模型权重就需要约 140GB 显存(FP16),加上 KV Cache 和运行时开销,单卡 A100 80GB 根本无法容纳。

更严峻的挑战来自并发。每个并发请求都需要独立的 KV Cache 空间来存储注意力机制的 Key 和 Value。以 Llama-2-70B 为例,单个请求的 KV Cache 在序列长度 2048 时约占 2GB 显存。10 个并发请求就需要 20GB 额外显存,直接挤占模型权重的存储空间。

推理延迟的尾部效应同样不可忽视。Prefill 阶段(首 Token 生成)需要处理整个 Prompt,计算密集;Decode 阶段(后续 Token 生成)每次只处理一个 Token,访存密集。两个阶段的资源瓶颈完全不同,混合调度会导致 GPU 利用率在 30%-90% 之间剧烈波动。

二、LLM 推理引擎的显存管理与调度机制

现代 LLM 推理引擎(vLLM、TGI、TensorRT-LLM)的核心创新在于显存管理和请求调度策略。理解这些机制是优化部署方案的工程基础。

flowchart TB
    A[推理请求到达] --> B[请求调度器]
    B --> C{Prefill/Decode 分离?}
    C -->|是| D[Prefill 实例]
    C -->|否| E[混合调度实例]
    D --> F[生成 KV Cache]
    F --> G[Transfer KV Cache]
    G --> H[Decode 实例]
    E --> I[连续批处理 Continuous Batching]

    H --> J[Token 输出]
    I --> J

    J --> K{序列结束?}
    K -->|是| L[释放 KV Cache]
    K -->|否| M[继续 Decode]
    M --> I

    subgraph PagedAttention 显存管理
        N[虚拟 KV Cache 页表] --> O[物理显存块池]
        O --> P[按需分配]
        O --> Q[共享 Copy-on-Write]
    end

    style B fill:#e74c3c,color:#fff
    style N fill:#3498db,color:#fff
    style I fill:#27ae60,color:#fff

PagedAttention 机制:vLLM 的核心创新,借鉴操作系统的虚拟内存分页思想。将 KV Cache 划分为固定大小的 Block(如 16 个 Token 对应一个 Block),维护虚拟 Block 到物理 Block 的页表映射。请求的 KV Cache 不需要连续的显存空间,按需分配物理 Block,序列结束后整块释放。这解决了传统方案中 KV Cache 必须预分配最大长度的显存浪费问题,显存利用率从 20%-40% 提升至 90% 以上。

Continuous Batching:传统 Static Batching 需要等待 Batch 中最长的序列完成后才能释放资源,短序列被迫等待。Continuous Batching 在每个 Decode Step 检查序列是否生成 EOS Token,已完成的序列立即移出 Batch,新请求立即加入。这使得 GPU 利用率不再受限于 Batch 中最慢的请求。

Prefill/Decode 分离调度:将推理拆分为 Prefill 和 Decode 两个独立阶段,分别调度到不同的 GPU 实例。Prefill 实例专注于计算密集的首 Token 生成,可以大 Batch 处理;Decode 实例专注于访存密集的 Token 生成,需要低延迟响应。分离后,两类实例可以独立扩缩容,资源利用率显著提升。

三、LLM 推理服务的高并发部署实现

3.1 基于 vLLM 的推理服务部署

"""
vLLM 推理服务启动配置
关键参数说明:
- gpu_memory_utilization: GPU 显存使用上限,预留空间给 CUDA 内核
- max_num_seqs: 最大并发序列数,受 KV Cache 显存约束
- enable_prefix_caching: 前缀缓存,相同 Prompt 前缀可复用 KV Cache
"""
from vllm import LLM, SamplingParams
from vllm.entrypoints.openai.api_server import run_server

# 推理引擎配置
ENGINE_ARGS = {
    "model": "/models/llama-2-70b-chat-hf",
    "tensor_parallel_size": 4,        # 4 卡张量并行
    "gpu_memory_utilization": 0.92,   # 显存使用上限 92%
    "max_num_seqs": 64,               # 最大并发序列数
    "max_model_len": 4096,            # 最大序列长度
    "enable_prefix_caching": True,    # 开启前缀缓存
    "block_size": 16,                 # PagedAttention Block 大小
    "swap_space": 16,                 # CPU 交换空间大小(GB)
    "dtype": "float16",              # 推理精度
}

# 采样参数模板
DEFAULT_SAMPLING = SamplingParams(
    temperature=0.7,
    top_p=0.9,
    max_tokens=2048,
    # 为什么设置 frequency_penalty:防止长文本生成中的重复循环
    frequency_penalty=0.1,
)

3.2 Kubernetes 上的 GPU 推理服务部署

# GPU 推理服务 Deployment
# 为什么使用 StatefulSet 而非 Deployment:
# 张量并行需要稳定的 Pod 网络标识,Pod 之间通过 RDMA 通信
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: llm-inference
  namespace: ai-inference
spec:
  replicas: 2  # 2 组 4 卡实例
  selector:
    matchLabels:
      app: llm-inference
  template:
    metadata:
      labels:
        app: llm-inference
    spec:
      # GPU 拓扑感知调度:确保 4 张卡在同一 NUMA 节点
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: gpu-topology
                    operator: In
                    values:
                      - "4x-a100-nvlink"
      containers:
        - name: vllm-server
          image: vllm/vllm-openai:latest
          command:
            - python
            - -m
            - vllm.entrypoints.openai.api_server
          args:
            - --model=/models/llama-2-70b-chat-hf
            - --tensor-parallel-size=4
            - --gpu-memory-utilization=0.92
            - --max-num-seqs=64
            - --enable-prefix-caching
          resources:
            limits:
              nvidia.com/gpu: 4
              memory: "128Gi"
              cpu: "16"
            requests:
              nvidia.com/gpu: 4
              memory: "64Gi"
              cpu: "8"
          ports:
            - containerPort: 8000
          # 健康检查:推理服务就绪需要加载模型(约 2 分钟)
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 120
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 180
            periodSeconds: 30
---
# HPA:基于 GPU 利用率和请求队列深度自动扩缩容
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: llm-inference-hpa
  namespace: ai-inference
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: StatefulSet
    name: llm-inference
  minReplicas: 2
  maxReplicas: 8
  metrics:
    # 自定义指标:GPU SM 利用率
    - type: Pods
      pods:
        metric:
          name: gpu_sm_utilization
        target:
          type: AverageValue
          averageValue: "70"
    # 自定义指标:推理队列深度
    - type: Pods
      pods:
        metric:
          name: inference_queue_depth
        target:
          type: AverageValue
          averageValue: "10"

3.3 推理请求网关与负载均衡

package gateway

import (
	"context"
	"sync/atomic"
)

// LLMGateway 大模型推理网关
// 核心职责:请求路由、负载均衡、流式响应转发
type LLMGateway struct {
	backends []*InferenceBackend
	// 轮询计数器,使用原子操作避免锁竞争
	rrCounter uint64
}

// InferenceBackend 推理后端实例
type InferenceBackend struct {
	Address       string
	ActiveConns   int64  // 当前活跃连接数
	MaxConns      int64  // 最大并发连接数
	AvgLatencyMs  int64  // 平均推理延迟
	QueueDepth    int64  // 推理队列深度
	IsHealthy     bool   // 健康状态
}

// SelectBackend 选择推理后端
// 为什么不用简单轮询:推理延迟差异大,轮询会导致长尾请求堆积
// 采用最少活跃连接 + 队列深度加权的策略
func (g *LLMGateway) SelectBackend() (*InferenceBackend, error) {
	var best *InferenceBackend
	minScore := int64(math.MaxInt64)

	for _, b := range g.backends {
		if !b.IsHealthy {
			continue
		}
		// 评分 = 活跃连接数 * 2 + 队列深度 * 3
		// 队列深度权重更高:排队中的请求对延迟影响更大
		score := atomic.LoadInt64(&b.ActiveConns)*2 +
			atomic.LoadInt64(&b.QueueDepth)*3
		if score < minScore {
			minScore = score
			best = b
		}
	}

	if best == nil {
		return nil, ErrNoHealthyBackend
	}
	return best, nil
}

// StreamInference 流式推理请求
// 使用 SSE 协议将 Token 逐个推送给客户端
func (g *LLMGateway) StreamInference(ctx context.Context,
	prompt string) (<-chan Token, error) {
	backend, err := g.SelectBackend()
	if err != nil {
		return nil, err
	}

	// 增加后端活跃连接计数
	atomic.AddInt64(&backend.ActiveConns, 1)
	defer atomic.AddInt64(&backend.ActiveConns, -1)

	tokenCh := make(chan Token, 64)
	go func() {
		defer close(tokenCh)
		// 调用后端推理服务的流式接口
		stream, err := backend.StreamComplete(ctx, prompt)
		if err != nil {
			tokenCh <- Token{Error: err}
			return
		}
		for token := range stream {
			select {
			case tokenCh <- token:
			case <-ctx.Done():
				// 客户端断开连接,停止接收
				return
			}
		}
	}()

	return tokenCh, nil
}

四、LLM 部署的代价:显存、延迟与成本的三角约束

显存与并发的零和博弈:GPU 显存是固定资源,模型权重、KV Cache 和运行时开销三者竞争。增加并发数意味着更多 KV Cache,挤占模型权重的显存空间。当显存不足时,推理引擎会触发 Swap(将 KV Cache 换出到 CPU 内存),延迟从毫秒级飙升至秒级。生产环境中,必须根据业务 QPS 和平均序列长度精确计算 KV Cache 需求,避免 Swap 触发。

张量并行的通信开销:4 卡张量并行时,每个 Decode Step 需要一次 All-Reduce 通信。NVLink 互联的通信延迟约 20-50us,PCIe 互联则高达 100-200us。对于 Decode 阶段(单步计算量小),通信开销占比可达 30%-50%。如果模型规模允许,优先选择单卡部署(如量化后的 70B 模型),避免通信开销。

前缀缓存的命中率代价:Prefix Caching 通过复用相同 Prompt 前缀的 KV Cache 来加速推理,但缓存管理本身有开销。Block 的哈希计算和页表查找在每个请求的 Prefill 阶段都需要执行。如果 Prompt 前缀的重复率低于 10%,缓存命中率低,管理开销反而拖慢推理速度。启用前需要分析业务流量的 Prompt 分布特征。

适用边界:上述方案适用于 7B-70B 规模模型的在线推理服务。对于 7B 以下的小模型,单卡部署即可满足大多数场景,无需复杂的并行策略。对于 175B 以上的超大模型,需要引入流水线并行和专家并行,架构复杂度显著提升。对于离线批处理场景,吞吐优先于延迟,应采用大 Batch + Static Batching 策略。

五、总结

LLM 推理部署的核心挑战是 GPU 显存管理和高并发调度。PagedAttention 解决了 KV Cache 的显存碎片问题,Continuous Batching 提升了 GPU 利用率,Prefill/Decode 分离优化了延迟分布。但这些优化都有其适用前提和代价。

落地路线建议:第一步,根据模型规模和 QPS 需求计算 GPU 资源需求,确定并行策略;第二步,部署 vLLM 推理引擎,配置 PagedAttention 和 Continuous Batching;第三步,实现推理网关的负载均衡和流式转发;第四步,配置基于 GPU 利用率的 HPA 自动扩缩容。每一步都需要在真实推理负载下验证参数,特别是显存利用率和 Swap 触发阈值。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐