LLM 推理部署与优化:vLLM PagedAttention、量化技术与生产级服务搭建

LLM 推理部署全链路指南:vLLM(PagedAttention)vs TGI vs llama.cpp 选型与性能对比、GGUF/AWQ/GPTQ/FP8 量化技术详解、动态批处理与连续批处理、长上下文优化(KV Cache / FlashAttention)。附带 FastAPI + vLLM 完整服务端代码、Docker Compose 生产部署模板。

目录

  1. 推理引擎选型矩阵
  2. vLLM:PagedAttention 革命
  3. TGI 与 llama.cpp
  4. 量化技术详解
  5. 批处理策略:静态 vs 动态 vs 连续
  6. 长上下文优化
  7. 分布式推理:张量并行与流水线并行
  8. FastAPI + vLLM 生产服务
  9. Docker Compose 生产部署

1. 推理引擎选型矩阵

维度vLLMTGI (HuggingFace)llama.cppTensorRT-LLM
PagedAttention✅ 首创✅ (部分)
连续批处理
量化支持AWQ/GPTQ/FP8GPTQ/AWQGGUF 全系列仅 NVIDIA
多 GPU✅ TP + PP✅ TP❌ 单卡✅ TP + PP
Speculative Decode
Prefix Caching
并发吞吐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
易用性⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
适用平台NVIDIA/AMDNVIDIACPU/GPU/Apple仅 NVIDIA
HTTP APIOpenAI 兼容OpenAI 兼容自定义自定义

选型建议

  • 生产级高吞吐(NVIDIA) → vLLM(社区最大,功能最全)
  • HuggingFace 生态深度集成 → TGI(与 Hub 无缝集成)
  • 本地/边缘/CPU → llama.cpp(跨平台最完善)
  • 极致性能(NVIDIA) → TensorRT-LLM(需复杂编译)

2. vLLM:PagedAttention 革命

2.1 PagedAttention 核心原理

传统推理为每个请求分配连续的 KV Cache,导致严重内存碎片。PagedAttention 借鉴操作系统虚拟内存:

  • KV Cache 分块存储(block,典型 16 tokens)
  • 逻辑块 → 物理块的映射表
  • 支持内存共享(copy-on-write,适合 beam search)
  • 支持 prefix caching(共享系统提示)

内存效率提升

  • 传统:内存浪费 60-80%(内部碎片 + 外部碎片)
  • PagedAttention:浪费 < 10%

2.2 vLLM 快速启动

# 安装
pip install vllm

# 启动 OpenAI 兼容 API 服务
python -m vllm.entrypoints.openai.api_server \
    --model Qwen/Qwen2.5-7B-Instruct \
    --tensor-parallel-size 1 \
    --max-model-len 8192 \
    --dtype bfloat16 \
    --gpu-memory-utilization 0.9

# 客户端调用(与 OpenAI API 100% 兼容)
curl http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "Qwen/Qwen2.5-7B-Instruct",
    "messages": [{"role": "user", "content": "Hello!"}],
    "max_tokens": 100
  }'

2.3 Python 内嵌推理

from vllm import LLM, SamplingParams

# 加载模型
llm = LLM(
    model="Qwen/Qwen2.5-7B-Instruct",
    tensor_parallel_size=2,          # 双卡张量并行
    dtype="bfloat16",
    max_model_len=8192,
    gpu_memory_utilization=0.9,
)

# 采样参数
sampling_params = SamplingParams(
    temperature=0.7,
    top_p=0.9,
    max_tokens=512,
)

# 批量推理
prompts = [
    "Explain quantum computing in simple terms.",
    "Write a Python function to calculate factorial.",
    "What are the benefits of microservices?",
]

outputs = llm.generate(prompts, sampling_params)
for output in outputs:
    print(f"Prompt: {output.prompt}\nGenerated: {output.outputs[0].text}\n")

2.4 vLLM 高级配置

from vllm import LLM, SamplingParams
from vllm.lora.request import LoRARequest

# LoRA 适配器热加载(生产多租户场景)
llm = LLM(
    model="meta-llama/Llama-2-7b-hf",
    enable_lora=True,
    max_loras=4,               # 同时加载的 LoRA 数量
    max_lora_rank=64,
)

sampling = SamplingParams(temperature=0.8)

# 使用不同 LoRA 适配器
output = llm.generate(
    "What is machine learning?",
    sampling,
    lora_request=LoRARequest("math-lora", 1, "/path/to/math-lora"),
)

# 量化加载
quant_llm = LLM(
    model="TheBloke/Llama-2-7B-AWQ",
    quantization="awq",
    dtype="auto",
)

3. TGI 与 llama.cpp

3.1 TGI(Text Generation Inference)

# Docker 启动
docker run --gpus all -p 8080:80 \
  -v $PWD/data:/data \
  ghcr.io/huggingface/text-generation-inference:2.0 \
  --model-id Qwen/Qwen2.5-7B-Instruct \
  --quantize awq

# 调用
curl http://localhost:8080/generate \
  -X POST -H "Content-Type: application/json" \
  -d '{"inputs":"What is AI?","parameters":{"max_new_tokens":100}}'

TGI 特色功能:

  • Safetensor 格式:更快的模型加载
  • 引水流(Watermarks):检测 AI 生成文本
  • Marian NMT 集成:翻译任务优化

3.2 llama.cpp

# 启动 server
./server -m model-Q4_K_M.gguf \
  -c 4096 -n 512 --host 0.0.0.0 --port 8080 \
  --parallel 4                    # 并发槽数

# 批处理推理
python -c "
from llama_cpp import Llama
llm = Llama('model-Q4_K_M.gguf', n_ctx=4096, n_threads=8)
out = llm('Q: What is RAG?\nA:', max_tokens=200)
print(out['choices'][0]['text'])
"

4. 量化技术详解

4.1 量化方法对比

方法精度大小(7B)速度损失质量损失适用
FP1616-bit14GB基准开发、高精度
BF1616-bit14GB基准Ampere+ GPU
FP8 (E4M3)8-bit7GB1.1x< 1%Hopper (H100)
INT88-bit7GB1.2x1-2%通用
GPTQ 4-bit4-bit4GB1.5x2-3%GPU 生产
AWQ 4-bit4-bit4GB1.5x1-2%GPU 生产(推荐)
GGUF Q4_K_M4-bit4GB0.3x (CPU)2-3%CPU/边缘
GGUF Q5_K_M5-bit5GB0.3x (CPU)1%CPU 高质量
GGUF Q8_08-bit7.5GB0.5x (CPU)< 0.5%CPU 接近无损

4.2 AWQ 量化(推荐 GPU 部署)

from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer

model_path = "Qwen/Qwen2.5-7B-Instruct"
quant_path = "qwen-7b-awq"
quant_config = {"zero_point": True, "q_group_size": 128, "w_bit": 4}

# 加载并量化
model = AutoAWQForCausalLM.from_pretrained(model_path)
tokenizer = AutoTokenizer.from_pretrained(model_path)
model.quantize(tokenizer, quant_config=quant_config)

# 保存
model.save_quantized(quant_path)
tokenizer.save_pretrained(quant_path)

# vLLM 加载 AWQ
from vllm import LLM
llm = LLM(model=quant_path, quantization="awq")

4.3 GPTQ 量化

from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig

quantize_config = BaseQuantizeConfig(
    bits=4,
    group_size=128,
    desc_act=False,  # True 质量更好但速度更慢
)

model = AutoGPTQForCausalLM.from_pretrained(
    "Qwen/Qwen2.5-7B-Instruct",
    quantize_config,
)
model.quantize(examples, batch_size=1)
model.save_quantized("qwen-7b-gptq")

4.4 FP8 量化(H100 专属)

from transformers import AutoModelForCausalLM

# 仅支持 Hopper 架构 GPU
model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.1-8B",
    torch_dtype=torch.float8_e4m3fn,
    device_map="auto",
)

5. 批处理策略:静态 vs 动态 vs 连续

策略机制优点缺点吞吐
静态批处理固定 batch,等全部完成简单受最长序列拖累
动态批处理请求累积 N 秒或 batch 满减少等待仍存在气泡
连续批处理(vLLM/TGI)Token 级调度,有新请求随时插入几乎无气泡实现复杂⭐⭐⭐⭐⭐

5.1 连续批处理可视化

时间轴 →
静态:  [AAAAAA][BBBB    ][CC      ]  B 和 C 被 A 拖累
连续:  [ABABAC][BABCCA  ][BBC    ]   Token 级调度,GPU 始终饱和

5.2 vLLM 调度配置

from vllm import LLM

llm = LLM(
    model="Qwen/Qwen2.5-7B-Instruct",
    max_num_seqs=256,          # 最大并发序列数
    max_num_batched_tokens=4096,  # 每批最大 token 数
    scheduling_policy="fcfs",  # fcfs / priority
)

6. 长上下文优化

6.1 KV Cache 管理

KV Cache 是推理显存的「大头」:

KV Cache 显存 = 2 * layers * batch * seq_len * hidden_dim * dtype_size

以 Llama-2-7B 为例:
- layers = 32, hidden_dim = 4096, dtype = fp16 (2 bytes)
- batch=1, seq_len=4096: 2 * 32 * 1 * 4096 * 4096 * 2 = 2.1 GB
- batch=1, seq_len=128K: 67 GB

优化策略

技术原理效果
KV Cache 量化Cache 存 INT8/FP8显存 ↓ 50%
FlashAttention-2融合 kernel,减少 HBM 读写速度 ↑ 2-4x
Sliding Window Attention只关注最近 N 个 token支持超长序列
StreamingLLM保留 attention sink + 最近 token无限长文本
Prefix Caching共享系统提示的 KV多轮对话共享

6.2 FlashAttention 配置

# vLLM 默认启用 FlashAttention
llm = LLM(
    model="Qwen/Qwen2.5-7B-Instruct",
    max_model_len=32768,       # 32K 上下文
    rope_scaling={"type": "dynamic", "factor": 2.0},  # 位置编码扩展
)

# 手动控制 attention 后端
import os
os.environ["VLLM_ATTENTION_BACKEND"] = "FLASH_ATTN"  # FLASH_ATTN / XFORMERS / FLASHINFER

7. 分布式推理:张量并行与流水线并行

7.1 张量并行(Tensor Parallelism)

将单层的矩阵运算拆分到多 GPU:

# 2 GPU 张量并行
python -m vllm.entrypoints.openai.api_server \
    --model Qwen/Qwen2.5-72B-Instruct \
    --tensor-parallel-size 2

7.2 流水线并行(Pipeline Parallelism)

将不同层分配到不同 GPU:

# 4 GPU:2 路张量并行 × 2 路流水线并行
python -m vllm.entrypoints.openai.api_server \
    --model Qwen/Qwen2.5-72B-Instruct \
    --tensor-parallel-size 2 \
    --pipeline-parallel-size 2

7.3 显存计算

def estimate_inference_memory(
    params_b: float,      # 参数量(B)
    dtype_bytes: float,   # 数据类型字节数(fp16=2, int8=1, int4=0.5)
    batch: int = 1,
    seq_len: int = 2048,
    layers: int = 32,
    hidden_dim: int = 4096,
    kv_cache_ratio: float = 0.8,  # KV cache 占比
) -> dict:
    """估算推理显存需求。"""
    # 模型权重
    model_mem = params_b * 1e9 * dtype_bytes / (1024**3)

    # KV Cache
    kv_cache = 2 * layers * batch * seq_len * hidden_dim * 2 / (1024**3)

    # 激活值(粗略估计)
    activation = batch * seq_len * hidden_dim * 4 / (1024**3)

    # 开销系数
    overhead = 1.2
    total = (model_mem + kv_cache + activation) * overhead

    return {
        "model_weights_gb": round(model_mem, 2),
        "kv_cache_gb": round(kv_cache, 2),
        "activation_gb": round(activation, 2),
        "total_gb": round(total, 2),
    }

# Llama-2-7B FP16 @ 4K 上下文
print(estimate_inference_memory(7, 2, seq_len=4096))
# {'model_weights_gb': 13.02, 'kv_cache_gb': 2.0, 'activation_gb': 0.12, 'total_gb': 18.17}

8. FastAPI + vLLM 生产服务

from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from vllm import LLM, SamplingParams, AsyncLLMEngine
from vllm.sampling_params import SamplingParams
from vllm.utils import random_uuid
import asyncio
import json

app = FastAPI(title="LLM Inference Service")

class ChatRequest(BaseModel):
    messages: list[dict]
    model: str = "default"
    temperature: float = Field(default=0.7, ge=0.0, le=2.0)
    top_p: float = Field(default=0.9, ge=0.0, le=1.0)
    max_tokens: int = Field(default=512, ge=1, le=8192)
    stream: bool = False
    stop: list[str] = []

class ChatResponse(BaseModel):
    content: str
    usage: dict
    model: str

class InferenceEngine:
    """vLLM 推理引擎封装。"""
    def __init__(self, model_path: str):
        self.engine = AsyncLLMEngine.from_engine_args(
            model_path,
            tensor_parallel_size=1,
            dtype="bfloat16",
            max_model_len=8192,
        )

    async def generate(
        self,
        prompt: str,
        sampling_params: SamplingParams,
        request_id: str = None,
    ):
        request_id = request_id or random_uuid()
        generator = self.engine.generate(prompt, sampling_params, request_id)

        final_output = None
        async for output in generator:
            final_output = output

        return final_output

    async def stream_generate(
        self,
        prompt: str,
        sampling_params: SamplingParams,
        request_id: str = None,
    ):
        request_id = request_id or random_uuid()
        generator = self.engine.generate(prompt, sampling_params, request_id)

        async for output in generator:
            yield output.outputs[0].text

# 使用更简单的同步方式(vLLM 的 LLM 类)
engine = LLM(
    model="Qwen/Qwen2.5-7B-Instruct",
    tensor_parallel_size=1,
    dtype="bfloat16",
    max_model_len=8192,
    gpu_memory_utilization=0.9,
)

async def format_messages(messages: list[dict]) -> str:
    """将消息列表格式化为模型输入提示。"""
    # 简单 format(实际使用 tokenizer.apply_chat_template)
    formatted = ""
    for m in messages:
        if m["role"] == "system":
            formatted += f"System: {m['content']}\n"
        elif m["role"] == "user":
            formatted += f"User: {m['content']}\n"
        elif m["role"] == "assistant":
            formatted += f"Assistant: {m['content']}\n"
    formatted += "Assistant: "
    return formatted

@app.post("/v1/chat/completions", response_model=ChatResponse)
async def chat_completions(req: ChatRequest):
    prompt = await format_messages(req.messages)

    sampling = SamplingParams(
        temperature=req.temperature,
        top_p=req.top_p,
        max_tokens=req.max_tokens,
        stop=req.stop,
    )

    if req.stream:
        async def stream_generator():
            outputs = engine.generate([prompt], sampling)
            for output in outputs[0].outputs:
                data = json.dumps({"text": output.text})
                yield f"data: {data}\n\n"
            yield "data: [DONE]\n\n"

        return StreamingResponse(stream_generator(), media_type="text/event-stream")

    outputs = engine.generate([prompt], sampling)
    generated = outputs[0].outputs[0].text

    return ChatResponse(
        content=generated,
        usage={
            "prompt_tokens": len(outputs[0].prompt_token_ids),
            "completion_tokens": len(outputs[0].outputs[0].token_ids),
            "total_tokens": len(outputs[0].prompt_token_ids) + len(outputs[0].outputs[0].token_ids),
        },
        model=req.model,
    )

@app.get("/health")
async def health():
    return {"status": "healthy", "model": "Qwen/Qwen2.5-7B-Instruct"}

@app.get("/metrics")
async def metrics():
    """Prometheus 风格指标。"""
    return {
        "gpu_utilization": 0.85,
        "requests_total": 12345,
        "avg_latency_ms": 120,
    }

9. Docker Compose 生产部署

# docker-compose.yml
version: '3.8'

services:
  vllm:
    image: vllm/vllm-openai:latest
    runtime: nvidia
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    volumes:
      - ./models:/models
      - ./hf_cache:/root/.cache/huggingface
    environment:
      - HF_HOME=/root/.cache/huggingface
      - CUDA_VISIBLE_DEVICES=0,1
    command: >
      --model /models/Qwen2.5-7B-Instruct
      --tensor-parallel-size 2
      --max-model-len 8192
      --dtype bfloat16
      --gpu-memory-utilization 0.9
      --max-num-seqs 256
      --enable-prefix-caching
      --port 8000
    ports:
      - "8000:8000"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

  api:
    build: ./api
    ports:
      - "8080:8080"
    environment:
      - VLLM_BASE_URL=http://vllm:8000
      - API_KEY=${API_KEY}
    depends_on:
      vllm:
        condition: service_healthy
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - api
    restart: unless-stopped

volumes:
  redis_data:

9.1 Nginx 配置

upstream llm_backend {
    least_conn;
    server api1:8080;
    server api2:8080;
}

server {
    listen 80;
    server_name llm-api.example.com;

    location /v1/ {
        proxy_pass http://llm_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        # SSE 支持
        proxy_buffering off;
        proxy_cache off;
        proxy_read_timeout 300s;
    }

    location /metrics {
        allow 10.0.0.0/8;
        deny all;
        proxy_pass http://llm_backend;
    }
}

9.2 部署检查清单

  • 模型权重已下载到本地卷(避免启动时下载)
  • GPU 驱动与 CUDA 版本匹配
  • 显存利用率预留 10% 缓冲(防止 OOM)
  • 健康检查配置合理(模型加载时间可达数分钟)
  • API Key 认证 / Rate Limiting
  • 日志收集(结构化 JSON 日志)
  • 监控:GPU 利用率、显存、吞吐量、延迟 P99

交叉链接:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页