LLM推理优化与部署

量化、加速与生产环境部署

Posted by 冯宇 on October 27, 2024

训练一个大模型可能需要数百万美元,但让它在生产环境中高效运行,同样是一个价值百万的问题。

LLM 推理优化的核心矛盾是:模型越来越大(70B, 175B),用户对延迟的容忍度却越来越低(希望秒级响应)。如何在有限的硬件资源下,让大模型跑得更快、成本更低?

本文将系统介绍 LLM 推理优化的核心技术:量化、KV-Cache、Flash Attention、以及 vLLM/TensorRT-LLM 等工业级部署方案。


一、推理性能瓶颈分析

1.1 LLM 推理的两个阶段

Prefill 阶段(处理输入 prompt):

  • 并行计算所有 token 的 attention
  • 计算密集型(Compute-bound)
  • 延迟:~100ms(取决于 prompt 长度)

Decode 阶段(逐 token 生成):

  • 串行生成,每次只生成 1 个 token
  • 显存带宽密集型(Memory-bound)
  • 延迟:~50ms/token

关键指标

指标 定义 目标
TTFT (Time To First Token) 首 token 延迟 < 500ms
TPS (Tokens Per Second) 生成速度 > 20 tokens/s
Throughput 每秒处理请求数 最大化
Latency 端到端延迟 最小化

1.2 性能瓶颈来源

# 一个 7B 模型的推理开销分析
model_params = 7_000_000_000  # 70亿参数

# FP16 存储(每个参数 2 字节)
model_size_gb = model_params * 2 / (1024**3)  # ~13 GB

# 推理时需要额外显存:
# 1. KV-Cache(存储历史 token 的 key/value)
# 2. 激活值(中间计算结果)
# 3. 临时缓冲区

# 假设生成 2048 token,batch_size=1
num_layers = 32
hidden_size = 4096
num_heads = 32
seq_len = 2048

kv_cache_size = 2 * num_layers * seq_len * hidden_size * 2 / (1024**3)
# 2(key+value)* 32层 * 2048 tokens * 4096维 * 2字节 ≈ 2 GB

total_memory = model_size_gb + kv_cache_size  # ~15 GB

瓶颈总结

  1. 显存占用大:模型参数 + KV-Cache + 激活值
  2. 显存带宽:Decode 阶段需要频繁读取权重
  3. 计算效率:Attention 的 $O(n^2)$ 复杂度

二、量化技术:让模型”瘦身”

2.1 量化原理

核心思想:用低精度(INT8/INT4)替代高精度(FP16/FP32)。

\[x_q = \text{round}\left(\frac{x - \text{zero\_point}}{\text{scale}}\right)\]

量化效果

精度 每参数字节数 模型大小(7B) 相对精度损失
FP32 4 28 GB 0% (基准)
FP16 2 14 GB < 0.1%
INT8 1 7 GB < 0.5%
INT4 0.5 3.5 GB 1-2%

2.2 量化方法对比

PTQ(Post-Training Quantization):训练后量化 QAT(Quantization-Aware Training):量化感知训练

from transformers import AutoModelForCausalLM
import torch

# ===== 方法1: bitsandbytes(最简单)=====
from transformers import BitsAndBytesConfig

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",  # Normal Float 4-bit
    bnb_4bit_use_double_quant=True,  # 双重量化
    bnb_4bit_compute_dtype=torch.bfloat16
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-7b-hf",
    quantization_config=bnb_config,
    device_map="auto"
)

# 现在模型只占用 ~4GB 显存!

GPTQ(高级量化)

from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig

# 量化配置
quantize_config = BaseQuantizeConfig(
    bits=4,  # 4-bit 量化
    group_size=128,  # 分组大小
    desc_act=False  # 是否量化激活值
)

# 加载模型
model = AutoGPTQForCausalLM.from_pretrained(
    "meta-llama/Llama-2-7b-hf",
    quantize_config=quantize_config
)

# 使用校准数据量化
model.quantize(calibration_dataset)

# 保存量化模型
model.save_quantized("./llama-2-7b-gptq-4bit")

# 推理
quantized_model = AutoGPTQForCausalLM.from_quantized(
    "./llama-2-7b-gptq-4bit",
    device="cuda:0"
)

AWQ(Activation-aware Weight Quantization)

from awq import AutoAWQForCausalLM

model = AutoAWQForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf")

# AWQ 量化(保护重要权重)
model.quantize(
    tokenizer,
    quant_config={"zero_point": True, "q_group_size": 128}
)

model.save_quantized("./llama-2-7b-awq-4bit")

性能对比

基准: LLaMA-7B FP16, NVIDIA A100

┌──────────┬───────────┬─────────┬──────────┐
│ 方法     │ 显存占用  │ 速度    │ 精度损失 │
├──────────┼───────────┼─────────┼──────────┤
│ FP16     │ 14 GB     │ 1.0x    │ 0%       │
│ INT8     │ 7 GB      │ 1.5x    │ 0.5%     │
│ GPTQ-4b  │ 4 GB      │ 2.0x    │ 1.5%     │
│ AWQ-4b   │ 4 GB      │ 2.5x    │ 1.0%     │
│ bitsandbytes-4b │ 4 GB │ 1.3x  │ 2.0%     │
└──────────┴───────────┴─────────┴──────────┘

三、KV-Cache 优化

3.1 KV-Cache 原理

问题:生成每个新 token 时,都需要重新计算所有历史 token 的 attention。

解决:缓存历史 token 的 Key 和 Value。

class AttentionWithKVCache(nn.Module):
    def __init__(self):
        super().__init__()
        self.kv_cache = None
        
    def forward(self, x, use_cache=False):
        batch_size, seq_len, hidden_size = x.shape
        
        # 计算 Q, K, V
        q = self.q_proj(x)
        k = self.k_proj(x)
        v = self.v_proj(x)
        
        if use_cache:
            if self.kv_cache is not None:
                # 使用缓存的 K, V
                k_cached, v_cached = self.kv_cache
                k = torch.cat([k_cached, k], dim=1)
                v = torch.cat([v_cached, v], dim=1)
            
            # 更新缓存
            self.kv_cache = (k, v)
        
        # Attention 计算
        attn_output = self.attention(q, k, v)
        return attn_output

3.2 PagedAttention(vLLM 核心技术)

问题:传统 KV-Cache 需要连续显存,导致碎片化。

解决:借鉴操作系统的虚拟内存机制,分页管理 KV-Cache。

# vLLM 的核心优势
from vllm import LLM, SamplingParams

llm = LLM(
    model="meta-llama/Llama-2-7b-hf",
    tensor_parallel_size=1,
    gpu_memory_utilization=0.9  # 使用 90% GPU 显存
)

prompts = [
    "Write a story about a robot",
    "Explain quantum computing"
] * 100  # 200 个请求

sampling_params = SamplingParams(
    temperature=0.8,
    top_p=0.95,
    max_tokens=512
)

# vLLM 自动做批处理和 KV-Cache 管理
outputs = llm.generate(prompts, sampling_params)

# 吞吐量提升 10-20x!

PagedAttention 原理

传统方法:
Request 1: [████████████████████] 2048 tokens (浪费显存)
Request 2: [████]                 256 tokens (碎片化)
Request 3: [████████]             512 tokens

PagedAttention:
逻辑地址 → 物理页面
Request 1: [Page0][Page1][Page2]...
Request 2: [Page5]
Request 3: [Page6][Page7]

✅ 显存利用率提升 ~4x
✅ 支持动态批处理

四、Attention 加速

4.1 Flash Attention

问题:标准 Attention 需要存储 $O(N^2)$ 的注意力矩阵。

解决:分块计算,避免存储完整矩阵。

# 标准 Attention(显存占用大)
def standard_attention(Q, K, V):
    # Q, K, V: [batch, num_heads, seq_len, head_dim]
    scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(head_dim)
    # scores: [batch, num_heads, seq_len, seq_len] ← 显存瓶颈!
    attn_weights = F.softmax(scores, dim=-1)
    output = torch.matmul(attn_weights, V)
    return output

# Flash Attention(显存友好)
from flash_attn import flash_attn_func

def flash_attention(Q, K, V):
    # 分块计算,只存储最终结果
    output = flash_attn_func(Q, K, V, causal=True)
    return output

# 性能提升:
# - 速度: 2-4x
# - 显存: 减少 5-20x

使用 Flash Attention 2

from transformers import AutoModelForCausalLM

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-7b-hf",
    torch_dtype=torch.float16,
    attn_implementation="flash_attention_2",  # 启用 Flash Attention 2
    device_map="auto"
)

# 自动加速,无需修改代码!

4.2 Multi-Query Attention (MQA)

思想:多个 Query 头共享一个 Key/Value 头。

# 标准 Multi-Head Attention
num_heads = 32
kv_size = num_heads * hidden_size  # 每层需要 32 组 KV

# Multi-Query Attention
num_query_heads = 32
num_kv_heads = 1  # 所有 Query 共享 1 组 KV
kv_size = num_kv_heads * hidden_size  # 只需 1 组 KV

# KV-Cache 减少 32x!

代码实现

class MultiQueryAttention(nn.Module):
    def __init__(self, hidden_size, num_heads):
        super().__init__()
        self.num_heads = num_heads
        self.head_dim = hidden_size // num_heads
        
        # Q 有多个头
        self.q_proj = nn.Linear(hidden_size, hidden_size)
        
        # K, V 只有一个头
        self.k_proj = nn.Linear(hidden_size, self.head_dim)
        self.v_proj = nn.Linear(hidden_size, self.head_dim)
        
    def forward(self, x):
        batch_size, seq_len, _ = x.shape
        
        # Q: [batch, num_heads, seq_len, head_dim]
        q = self.q_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim)
        q = q.transpose(1, 2)
        
        # K, V: [batch, 1, seq_len, head_dim]
        k = self.k_proj(x).unsqueeze(1)
        v = self.v_proj(x).unsqueeze(1)
        
        # K, V 广播到所有头
        k = k.expand(-1, self.num_heads, -1, -1)
        v = v.expand(-1, self.num_heads, -1, -1)
        
        # 标准 attention
        scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim)
        attn = F.softmax(scores, dim=-1)
        output = torch.matmul(attn, v)
        
        return output

4.3 Grouped-Query Attention (GQA)

折中方案:将 Query 分组,每组共享一个 KV 头。

MHA (Multi-Head):     32 Query → 32 KV
GQA (Grouped-Query):  32 Query → 8 KV (每 4 个 Query 共享 1 个 KV)
MQA (Multi-Query):    32 Query → 1 KV

五、推理引擎对比

5.1 vLLM

特点

  • ✅ PagedAttention(显存利用率最高)
  • ✅ Continuous Batching(动态批处理)
  • ✅ 易用性极高
from vllm import LLM, SamplingParams

# 初始化
llm = LLM(
    model="meta-llama/Llama-2-7b-chat-hf",
    tensor_parallel_size=2,  # 2 卡张量并行
    dtype="float16",
    max_model_len=4096
)

# 批量推理
prompts = ["Hello"] * 1000
outputs = llm.generate(prompts, SamplingParams(temperature=0.8, max_tokens=100))

# 吞吐量:~2000 tokens/s(单卡 A100)

OpenAI 兼容 API 服务

# 启动服务器
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-2-7b-chat-hf \
    --port 8000 \
    --tensor-parallel-size 2

# 客户端调用
curl http://localhost:8000/v1/completions \
    -H "Content-Type: application/json" \
    -d '{
        "model": "meta-llama/Llama-2-7b-chat-hf",
        "prompt": "Hello, how are you?",
        "max_tokens": 100
    }'

5.2 TensorRT-LLM

特点

  • ✅ NVIDIA 官方,优化极致
  • ✅ 支持多种量化(INT8/INT4/FP8)
  • ❌ 配置复杂,需要编译
import tensorrt_llm
from tensorrt_llm.runtime import ModelRunner

# 构建引擎(需要预先编译)
# 1. 导出模型
# 2. 量化(可选)
# 3. 构建 TensorRT 引擎

# 加载引擎
runner = ModelRunner.from_dir("./llama-7b-engine")

# 推理
input_ids = tokenizer.encode("Hello, world!")
outputs = runner.generate(
    input_ids,
    max_new_tokens=100,
    temperature=0.8
)

# 吞吐量:~3000 tokens/s(单卡 A100,INT8)

5.3 Text Generation Inference (TGI)

特点

  • ✅ Hugging Face 官方
  • ✅ 支持流式输出
  • ✅ 内置监控
# Docker 部署
docker run --gpus all --shm-size 1g -p 8080:80 \
    ghcr.io/huggingface/text-generation-inference:latest \
    --model-id meta-llama/Llama-2-7b-chat-hf \
    --num-shard 2 \
    --max-batch-total-tokens 32768

# 推理
curl http://localhost:8080/generate \
    -X POST \
    -d '{"inputs":"Hello","parameters":{"max_new_tokens":20}}' \
    -H 'Content-Type: application/json'

5.4 性能对比

模型: LLaMA-7B, 硬件: NVIDIA A100 40GB

┌─────────────┬──────────────┬─────────────┬────────────┐
│ 引擎        │ 吞吐量(tok/s)│ 首Token延迟 │ 显存占用   │
├─────────────┼──────────────┼─────────────┼────────────┤
│ Transformers│ 200          │ 150ms       │ 16 GB      │
│ vLLM        │ 2000         │ 100ms       │ 10 GB      │
│ TRT-LLM     │ 3000         │ 80ms        │ 8 GB       │
│ TGI         │ 1500         │ 120ms       │ 12 GB      │
└─────────────┴──────────────┴─────────────┴────────────┘

六、分布式推理

6.1 张量并行(Tensor Parallelism)

原理:将模型的每一层切分到多个 GPU。

# 使用 vLLM 的张量并行
llm = LLM(
    model="meta-llama/Llama-2-70b-hf",
    tensor_parallel_size=4,  # 4 卡并行
    dtype="float16"
)

# Attention 层的切分:
# GPU 0: heads 0-7
# GPU 1: heads 8-15
# GPU 2: heads 16-23
# GPU 3: heads 24-31

6.2 流水线并行(Pipeline Parallelism)

原理:将模型的不同层分配到不同 GPU。

from transformers import pipeline

# DeepSpeed Pipeline
pipe = pipeline(
    "text-generation",
    model="meta-llama/Llama-2-70b-hf",
    device_map="auto",  # 自动分配层到 GPU
    max_length=200
)

# 层分配示例(8 卡):
# GPU 0-1: Layers 0-9
# GPU 2-3: Layers 10-19
# GPU 4-5: Layers 20-29
# GPU 6-7: Layers 30-39

6.3 Ray Serve 负载均衡

from ray import serve
from transformers import AutoModelForCausalLM, AutoTokenizer

@serve.deployment(num_replicas=4, ray_actor_options={"num_gpus": 1})
class LLMDeployment:
    def __init__(self):
        self.model = AutoModelForCausalLM.from_pretrained("gpt2")
        self.tokenizer = AutoTokenizer.from_pretrained("gpt2")
        
    def __call__(self, request):
        prompt = request.query_params["prompt"]
        inputs = self.tokenizer(prompt, return_tensors="pt")
        outputs = self.model.generate(**inputs, max_length=100)
        return self.tokenizer.decode(outputs[0])

# 部署
serve.run(LLMDeployment.bind())

# 现在有 4 个副本自动负载均衡

七、生产部署最佳实践

7.1 完整部署架构

用户请求
   ↓
[Nginx/负载均衡器]
   ↓
[API Gateway]
   ↓
[请求队列 (Redis/RabbitMQ)]
   ↓
[推理服务集群]
  ├─ vLLM Instance 1 (GPU 0-1)
  ├─ vLLM Instance 2 (GPU 2-3)
  └─ vLLM Instance 3 (GPU 4-5)
   ↓
[响应缓存 (Redis)]
   ↓
[监控 (Prometheus + Grafana)]

7.2 Docker 部署示例

# Dockerfile
FROM nvidia/cuda:11.8.0-runtime-ubuntu22.04

# 安装依赖
RUN pip install vllm fastapi uvicorn

# 复制模型(或从 HF Hub 下载)
COPY ./model /model

# 启动脚本
CMD python -m vllm.entrypoints.openai.api_server \
    --model /model \
    --port 8000 \
    --tensor-parallel-size $GPU_COUNT
# docker-compose.yml
version: '3.8'

services:
  llm-inference:
    image: vllm-inference:latest
    runtime: nvidia
    environment:
      - GPU_COUNT=2
      - CUDA_VISIBLE_DEVICES=0,1
    ports:
      - "8000:8000"
    volumes:
      - ./models:/model
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 2
              capabilities: [gpu]

7.3 Kubernetes 部署

apiVersion: apps/v1
kind: Deployment
metadata:
  name: llm-inference
spec:
  replicas: 3
  selector:
    matchLabels:
      app: llm-inference
  template:
    metadata:
      labels:
        app: llm-inference
    spec:
      containers:
      - name: vllm
        image: vllm/vllm-openai:latest
        args:
          - --model
          - meta-llama/Llama-2-7b-chat-hf
          - --tensor-parallel-size
          - "2"
        resources:
          limits:
            nvidia.com/gpu: 2
        ports:
        - containerPort: 8000
---
apiVersion: v1
kind: Service
metadata:
  name: llm-service
spec:
  selector:
    app: llm-inference
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8000
  type: LoadBalancer

7.4 监控与告警

from prometheus_client import Counter, Histogram, start_http_server
import time

# 定义指标
request_count = Counter('llm_requests_total', 'Total requests')
request_duration = Histogram('llm_request_duration_seconds', 'Request duration')
tokens_generated = Counter('llm_tokens_generated_total', 'Total tokens generated')

def monitored_inference(prompt):
    start = time.time()
    request_count.inc()
    
    try:
        # 推理
        output = model.generate(prompt)
        tokens_generated.inc(len(output))
        
        # 记录延迟
        duration = time.time() - start
        request_duration.observe(duration)
        
        return output
    except Exception as e:
        error_count.inc()
        raise

# 启动 Prometheus 服务器
start_http_server(8001)

八、成本优化策略

8.1 Spot Instance 使用

# AWS Spot Instance 部署
# 成本降低 70%,但可能被中断

import boto3

ec2 = boto3.client('ec2')

# 请求 Spot Instance
response = ec2.request_spot_instances(
    InstanceCount=4,
    Type='persistent',
    LaunchSpecification={
        'ImageId': 'ami-xxx',  # 包含 vLLM 的 AMI
        'InstanceType': 'g5.12xlarge',  # 4x A10G
        'KeyName': 'my-key',
        'SecurityGroups': ['llm-sg']
    },
    SpotPrice='2.50'  # 最高出价 $2.50/小时(按需价格 $5/小时)
)

8.2 动态批处理

class DynamicBatcher:
    def __init__(self, max_batch_size=32, max_wait_ms=100):
        self.max_batch_size = max_batch_size
        self.max_wait_ms = max_wait_ms
        self.queue = []
        
    async def add_request(self, request):
        self.queue.append(request)
        
        # 条件满足时触发批处理
        if len(self.queue) >= self.max_batch_size:
            return await self.process_batch()
        
        # 或等待超时
        await asyncio.sleep(self.max_wait_ms / 1000)
        if self.queue:
            return await self.process_batch()
    
    async def process_batch(self):
        batch = self.queue[:self.max_batch_size]
        self.queue = self.queue[self.max_batch_size:]
        
        # 批量推理(吞吐量提升 10x)
        results = model.generate(batch)
        return results

8.3 请求缓存

import redis
import hashlib

cache = redis.Redis(host='localhost', port=6379, db=0)

def cached_inference(prompt, **kwargs):
    # 生成缓存键
    cache_key = hashlib.md5(f"{prompt}{kwargs}".encode()).hexdigest()
    
    # 检查缓存
    cached_result = cache.get(cache_key)
    if cached_result:
        return cached_result.decode()
    
    # 推理
    result = model.generate(prompt, **kwargs)
    
    # 存入缓存(TTL 1小时)
    cache.setex(cache_key, 3600, result)
    
    return result

# 缓存命中率 30-50% 时,成本降低显著

九、总结

推理优化核心技术

  1. 量化:INT8/INT4 减少显存和计算量
    • 推荐:GPTQ(精度)或 AWQ(速度)
  2. KV-Cache:缓存历史 token 的 attention
    • 推荐:vLLM 的 PagedAttention
  3. Flash Attention:分块计算减少显存
    • 推荐:直接用 Flash Attention 2
  4. 推理引擎
    • 易用性:vLLM
    • 极致性能:TensorRT-LLM
    • 开箱即用:TGI
  5. 分布式
    • 张量并行:适合大模型(70B+)
    • 流水线并行:适合长序列

部署建议

场景 推荐方案
快速原型 Transformers + GPU
生产服务(小模型) vLLM + Docker
生产服务(大模型) vLLM + K8s + 张量并行
极致性能 TensorRT-LLM + 量化
成本优先 vLLM + Spot Instance + 缓存

参考资源

论文

工具

博客


💬 交流与讨论

⚠️ 尚未完成 Giscus 配置。请在 _config.yml 中设置 repo_idcategory_id 后重新部署,即可启用升级后的评论系统。

配置完成后,评论区将自动支持 Markdown 代码高亮与 LaTeX 数学公式渲染,访客回复会同步到 GitHub Discussions,并具备通知功能。