Skip to content

从零部署一个推理服务

本页速览 用 Llama-2-7B 走通从 PyTorch 直接推理、vLLM 替换到 AWQ 量化 + EAGLE 投机解码 + Triton 部署的三版渐进路线,每版给出可执行命令、观测指标、目录结构与性能对比,是推理部署方向最浓缩的端到端动手项目。

从零部署一个推理服务

读十篇 benchmark 报告,不如完整跑通一次部署。本文带你用最经典的 Llama-2-7B 走一遍推理服务从"能跑"到"跑得快"再到"跑得稳"的全流程——不是"调包出结果",而是把每一版为什么这么做、收益在哪、代价是什么讲清楚。

很多初学者学了一堆推理优化技术之后,仍然不知道一个生产级推理服务到底长什么样:v1 怎么从训练框架直接搬过来?vLLM 替换 transformers 真的能拿到几倍?量化与投机解码叠加会不会互相打架?本文用一条主线(Llama-2-7B 对话服务)把完整部署流程从头走到底。你得到的不是"这个模型的 tokens/s 数字",而是一套可以复用到任何 LLM 部署的优化骨架。

完整项目分为三版(v1 → v2 → v3),先看整体地图:

                起点:HuggingFace 上的 Llama-2-7B 权重


        ┌─────────────────────────────────────────┐
        │ v1: PyTorch + Transformers 直接推理      │
        │   - transformers from_pregressive       │
        │   - bf16、单 batch、无 KV cache 复用     │
        │   - 观测延迟 / 显存基线                   │
        └──────────────────┬──────────────────────┘

        ┌─────────────────────────────────────────┐
        │ v2: vLLM 替换 transformers              │
        │   - PagedAttention + 连续批处理           │
        │   - OpenAI 兼容 API 服务                 │
        │   - 吞吐 5–10×,延迟 p99 下降             │
        └──────────────────┬──────────────────────┘

        ┌─────────────────────────────────────────┐
        │ v3: AWQ INT4 + EAGLE-3 + Triton         │
        │   - 4bit 量化压缩权重                    │
        │   - 投机解码小 batch 极致延迟             │
        │   - Triton 多模型多版本统一编排           │
        └─────────────────────────────────────────┘

三版对应三种典型工程问题:v1 解决"能不能跑",v2 解决"吞吐够不够",v3 解决"成本与延迟能不能再砍一截"。它们也正是工业界推理优化的标准升级路径,详见 推理路径全景批处理与请求调度

前置知识

本文假设你已了解 LLM 推理的基本概念:什么是 延迟、吞吐与并发显存层次与带宽墙模型量化基础模型服务化与编排。如果还需要补,先读 什么是推理加速总体架构解剖 再回来。

一、项目选择:为什么是 Llama-2-7B

第一个问题是:练手项目选什么模型? 选模型有三个标准,缺一不可。

标准为什么违背的后果
单卡放得下(≤ 14B 左右)一张 A100/L40/4090 能跑完整流程,不用纠结张量并行多卡分布式直接把流程复杂度放大 10 倍,学不到主线
生态支持广(vLLM/TRT-LLM/llama.cpp 都有)能跨引擎对比,对比是学习的捷径选了冷门模型,没引擎适配,半数优化手段无法验证
有公开对话版(chat 版可用)能直接做真实业务(问答服务),不是只跑 generate 示例仅有 base 版的话,输出质量差,没法做端到端体验

按这个标准,三个候选对比:

模型参数量单卡显存(bf16 + KV)生态支持适合度
Llama-2-7B-Chat7B≈ 16 GB★★★ vLLM/TRT-LLM/llama.cpp/Ollama 全覆盖★★★ 首选
Qwen2.5-7B-Instruct7B≈ 16 GB★★★ 国产模型生态最齐★★★ 中文场景首选
Mistral-7B-Instruct-v0.37B≈ 16 GB★★☆ vLLM/TRT-LLM 支持★★☆ 英文场景

本文选 Llama-2-7B-Chat,理由三条:

  1. 生态标杆:vLLM、TensorRT-LLM、llama.cpp 三个主流引擎都把它作为一级支持对象,对比公平。
  2. 显存可控:bf16 推理约 14 GB 权重 + 2 GB KV cache,单张 A100 (40 GB) 或 RTX 4090 (24 GB) 都能跑完三版实验。
  3. 公开可下载:Meta 官方权重在 HuggingFace 公开(需申请),无需特殊权限。

先定义"成功",再开始写代码

把问题翻译成部署任务时,必须回答三个问题:

  1. 任务类型:在线对话(流式生成、低延迟)还是离线批量(高吞吐)?本文以在线对话为主线,因为它最典型、约束最严苛。
  2. SLA 定义:首字延迟(TTFT)< 500ms、单请求生成延迟(TPOT)< 50ms/token、p99 端到端 < 4s、单卡 QPS > 20。
  3. 基线:v1 的 PyTorch 直接推理就是基线,任何后续优化必须显著超过它,否则优化没意义。

SLA 的定义先于代码写下,这是 部署设计原则 的基本功。

二、环境搭建

1. 版本要求

本文代码基于以下版本(2024–2025 年稳定版):

软件版本用途
Python3.10+语言本身
torch2.3+ / CUDA 12.1训练框架与 GPU 后端
transformers4.44+HuggingFace 模型加载
vllm0.6+v2/v3 的主力推理引擎
autoawq0.2.6+AWQ 4bit 量化
tritonserver24.08+Triton 推理服务
nvidia-smidriver ≥ 535GPU 监控

2. 创建虚拟环境

永远不要把依赖装进系统 Python——推理优化对版本极其敏感,CUDA/cudnn/torch 版本错一位都可能性能掉 30%:

bash
mkdir llama-deploy && cd llama-deploy

python -m venv .venv
source .venv/bin/activate        # macOS / Linux
# .venv\Scripts\activate        # Windows PowerShell

# v1 用:transformers + torch
pip install torch==2.3.1 transformers==4.44.2 accelerate sentencepiece

# v2 用:vLLM(自带 PagedAttention)
pip install vllm==0.6.3

# v3 用:AWQ 量化 + Triton client
pip install autoawq==0.2.6 tritonclient[all]==2.44.0

pip freeze > requirements.txt

为什么 vLLM 要单列一个 install

vLLM 的 wheels 包含预编译的 CUDA kernel(PagedAttention、FlashAttention),对 CUDA 版本与 torch 版本严格匹配。混装到 transformers 环境里经常冲突。生产实践里,v1 与 v2 通常拆成两个独立 venv 或两个独立 Docker 镜像——这也是为什么 Triton 后来会成为"统一编排层"的原因,详见 Triton 推理服务

3. GPU 与显存自检

bash
nvidia-smi --query-gpu=name,memory.total,memory.free,driver_version,compute_cap --format=csv
# name, memory.total [MiB], memory.free [MiB], driver_version, compute_cap
# NVIDIA A100 80GB, 81920, 78000, 535.104.05, 8.0

compute_cap 必须 ≥ 8.0(Ampere 及以上)才能完整跑通本文的 bf16 + PagedAttention + FlashAttention 路径。T4/V100 这类老卡也能跑,但性能数据不在同一档。

三、v1:PyTorch + Transformers 直接推理

v1 的目标只有一句:让模型生成出像样的回答。它故意不用任何"花活",是后续所有优化的对照基线。

1. 加载模型

python
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

MODEL = "meta-llama/Llama-2-7b-chat-hf"

tokenizer = AutoTokenizer.from_pretrained(MODEL, use_fast=True)
model = AutoModelForCausalLM.from_pretrained(
    MODEL,
    torch_dtype=torch.bfloat16,   # bf16 比 fp16 数值更稳,比 fp32 省一半显存
    device_map="auto",            # 自动把权重搬到 GPU
)
model.eval()

prompt = "用三句话解释 PagedAttention。"
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")

device_map="auto" 是 HuggingFace accelerate 的能力——把模型层按显存预算分到可见的 GPU/CPU。单卡场景它就是"全搬到 GPU"。

2. 跑一次推理

python
with torch.inference_mode():
    out = model.generate(
        **inputs,
        max_new_tokens=256,
        do_sample=True,
        temperature=0.6,
        top_p=0.9,
        pad_token_id=tokenizer.eos_token_id,
    )
text = tokenizer.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
print(text)

3. 观测基线指标

v1 的重点不是"跑通",而是测出基线:单请求延迟、显存峰值、tokens/s。这是后续所有优化的对照物。

python
import time, torch

def bench_one(prompt, max_new_tokens=256):
    torch.cuda.reset_peak_memory_stats()
    inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
    # warmup(首次推理会触发 kernel 编译,不能进基线)
    _ = model.generate(**inputs, max_new_tokens=8, do_sample=False)

    torch.cuda.synchronize()
    t0 = time.perf_counter()
    out = model.generate(**inputs, max_new_tokens=max_new_tokens,
                         do_sample=True, temperature=0.6, top_p=0.9,
                         pad_token_id=tokenizer.eos_token_id)
    torch.cuda.synchronize()
    t1 = time.perf_counter()

    peak = torch.cuda.max_memory_allocated() / 1e9   # GB
    n_tok = out.shape[1] - inputs["input_ids"].shape[1]
    tps = n_tok / (t1 - t0)
    print(f"输出 {n_tok} tokens / {(t1-t0)*1000:.0f} ms = {tps:.1f} tok/s | 峰值 {peak:.1f} GB")
    return {"tokens": n_tok, "ms": (t1-t0)*1000, "tps": tps, "peak_gb": peak}

bench_one("写一段 200 字关于秋天的散文。")

典型 A100 上的 v1 基线:

指标数值说明
单请求生成 tokens256与 max_new_tokens 对齐
端到端延迟≈ 9–11 s包含 prefill + decode
生成阶段 tokens/s≈ 25–30 tok/s单 batch、无 PagedAttention
显存峰值≈ 14.5 GB7B 权重 14 GB + KV cache + 临时
GPU 利用率30–50%decode 阶段严重 memory-bound

这个数字就是基线。记下来,v2/v3 都要和它比。延迟与 GPU 利用率为什么这么低?详见 Roofline 模型与算力分析显存层次与带宽墙

v1 暴露的两个核心问题

  1. 吞吐极低:30 tok/s 意味着单卡只能服务 30 个并发用户各 1 token/s,远不够 SaaS 标准。
  2. GPU 利用率低:30–50% 说明算力闲置。原因是 HuggingFace generate 默认逐 token 解码、batch=1、KV cache 复用机制原始,每一步都受限于显存带宽而非算力——这就是 批处理与请求调度 要解决的核心问题。

四、v2:vLLM 替换 transformers

v2 的目标是用 vLLM 把吞吐拉起来。vLLM 的两个核心机制——PagedAttention 与 continuous batching——是过去两年推理优化最大的工程突破,详见 vLLM 与 PagedAttention

1. 用 CLI 启动 OpenAI 兼容服务

vLLM 自带 vllm serve CLI,几行就能拉起一个 OpenAI 协议的 HTTP 服务:

bash
# 单卡启动 Llama-2-7B-Chat,OpenAI 兼容 API
vllm serve meta-llama/Llama-2-7b-chat-hf \
    --dtype bfloat16 \
    --max-model-len 4096 \
    --max-num-seqs 256 \
    --gpu-memory-utilization 0.9 \
    --enforce-eager \
    --port 8000

关键参数含义(详见 调参与性能调优):

参数含义v1 对应物
--max-model-len模型最大上下文长度transformers 的 max_new_tokens 上限
--max-num-seqs同时在飞的最大请求数v1 没有,相当于 batch=1
--gpu-memory-utilizationKV cache 占总显存比例v1 无主动管理
--enforce-eager关闭 CUDA Graph(调试用)-

2. 客户端测试

python
from openai import OpenAI
import time, asyncio

client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")

def bench_one_vllm(prompt, max_tokens=256):
    t0 = time.perf_counter()
    resp = client.completions.create(
        model="meta-llama/Llama-2-7b-chat-hf",
        prompt=prompt,
        max_tokens=max_tokens,
        temperature=0.6,
        top_p=0.9,
    )
    t1 = time.perf_counter()
    n_tok = resp.usage.completion_tokens
    print(f"输出 {n_tok} tokens / {(t1-t0)*1000:.0f} ms = {n_tok/(t1-t0):.1f} tok/s")
    return {"tokens": n_tok, "ms": (t1-t0)*1000, "tps": n_tok/(t1-t0)}

bench_one_vllm("写一段 200 字关于秋天的散文。")

3. 并发压测

v1 单请求就慢,vLLM 的真正价值在多请求并发。下面是 32 个并发同时打:

python
import asyncio
from openai import AsyncOpenAI

aclient = AsyncOpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")

async def one(i):
    t0 = time.perf_counter()
    r = await aclient.completions.create(
        model="meta-llama/Llama-2-7b-chat-hf",
        prompt=f"用三句话解释 PagedAttention。请求编号 {i}。",
        max_tokens=128,
    )
    return r.usage.completion_tokens, time.perf_counter() - t0

async def main(n=32):
    t0 = time.perf_counter()
    results = await asyncio.gather(*[one(i) for i in range(n)])
    total_toks = sum(r[0] for r in results)
    total_time = time.perf_counter() - t0
    print(f"{n} 请求 / {total_time*1000:.0f} ms = {total_toks/total_time:.0f} tok/s 吞吐")

asyncio.run(main(32))

4. v1 → v2 性能对比

A100 40GB 上的典型数据:

指标v1 (transformers)v2 (vLLM)收益
单请求 TPOT (ms/tok)35–4018–22
单请求 TTFT (ms)800200
32 并发吞吐 (tok/s)≈ 30(串行排队的等效)800–120030–40×
显存峰值14.5 GB36 GB(含 KV pool)上去了,但换来吞吐
GPU 利用率30–50%80–95%显著拉满

为什么 vLLM 单请求就快了一倍

单请求 vLLM 也比 transformers 快,原因不在 PagedAttention(那是多请求机制),而在:

  1. 优化的 attention kernel:vLLM 默认用 FlashAttention2,访存远低于 naive 实现;
  2. CUDA Graph:把整步 decode 编译成图,省掉 kernel launch 开销;
  3. 更紧凑的 KV cache 布局:即使单请求也比 transformers 的 list-of-tensor 快。

详见 算子融合与自定义核vLLM 与 PagedAttention

五、v3:AWQ 量化 + EAGLE 投机解码 + Triton

v2 已经把吞吐拉到 1000+ tok/s,下一步是"砍成本 + 砍延迟"——这就是 v3 的两个武器:量化投机解码

1. AWQ 4bit 量化

AWQ(Activation-aware Weight Quantization)把权重压到 INT4,激活仍 FP16 计算——是 weight-only 量化的代表方案,详见 模型量化基础权重量化与混合精度

python
# 第一步:用 autoawq 把模型量化成 INT4
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer

model_path = "meta-llama/Llama-2-7b-chat-hf"
quant_path = "Llama-2-7b-chat-hf-awq"
quant_config = { "zero_point": True, "q_group_size": 128, "w_bit": 4, "version": "GEMM" }

model = AutoAWQForCausalLM.from_pretrained(model_path)
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
model.quantize(tokenizer, quant_config=quant_config)
model.save_quantized(quant_path)
bash
# 第二步:vLLM 直接加载 AWQ 量化模型
vllm serve ./Llama-2-7b-chat-hf-awq \
    --quantization awq \
    --dtype float16 \
    --max-model-len 4096 \
    --max-num-seqs 256 \
    --gpu-memory-utilization 0.9 \
    --port 8000

AWQ 后的预期变化:

指标v2 (bf16)v3a (AWQ INT4)变化
权重显存14 GB4 GB3.5× 压缩
单请求 TPOT20 ms14 ms1.4× 加速(memory-bound 受益)
32 并发吞吐1000 tok/s1700 tok/s1.7×
模型质量 (MMLU)45.344.8-0.5(基本无损)

量化前必做的事

跑评测集验证精度。AWQ/GPTQ 这类 PTQ 量化是 trade-off,不是免费午餐——某些模型(激活异常值多的)量化后精度掉得很惨。最小验证集:跑一遍 MMLU 或你业务的真实评测。详见 常见陷阱与反模式 的"量化前后精度没对齐"。

2. EAGLE-3 投机解码

投机解码(speculative decoding)用一个小模型猜测若干 token,再用大模型一次验证——小 batch 下能显著降低 TPOT。EAGLE-3 是 2024 年效果最好的方案之一,详见 投机解码与 Medusa/EAGLE

bash
# vLLM 启动时加上 EAGLE-3 draft model
vllm serve ./Llama-2-7b-chat-hf-awq \
    --quantization awq \
    --speculative-model "lmsys/Falcon-EAGLE-3-7B" \
    --num-speculative-tokens 5 \
    --speculative-draft-tensor-parallel-size 1 \
    --max-model-len 4096 \
    --max-num-seqs 16 \
    --port 8000

--num-speculative-tokens 5 表示小模型一次猜 5 个,大模型一次验证——若全猜对,单步可生成 6 个 token。

投机解码只对小 batch 有效

--max-num-seqs 16 故意调小了。大 batch(≥ 64)下投机解码往往是反向收益:因为大模型本身已被 batch 喂满算力,投机解码徒增显存与计算。这是最常见的反模式之一,详见 常见陷阱与反模式 的"投机解码在大 batch 用"。

3. Triton 部署

生产环境通常不止一个模型、不止一个版本——你需要一个统一编排层。Triton Inference Server 就是 NVIDIA 的标准答案,详见 Triton 推理服务

最简 Triton 配置(部署 vLLM 后端 + AWQ 模型):

model_repository/
└── llama2-7b-awq/
    ├── config.pbtxt
    └── 1/
        └── model.py       # Python backend 调 vLLM

config.pbtxt

name: "llama2-7b-awq"
backend: "python"
max_batch_size: 32
input [
  { name: "prompt", data_type: TYPE_STRING, dims: [ -1 ] }
]
output [
  { name: "text", data_type: TYPE_STRING, dims: [ -1 ] }
]
dynamic batching {
  preferred_batch_size: [ 4, 8, 16, 32 ]
  max_queue_delay_microseconds: 50000
}

启动 Triton:

bash
tritonserver --model-repository ./model_repository \
             --http-port 8000 \
             --grpc-port 8001 \
             --metrics-port 8002

Triton 的价值在 v3 才真正显现:多模型共存(LLM + embedder + reranker)、多版本灰度(v2 与 v3 同时跑、按权重分流)、统一 metrics 端口(/metrics 暴露 Prometheus 指标)。这是 模型服务化与编排 的核心实践。

六、三版性能对比表

把三版的指标汇总到一张表(A100 40GB,单卡,Llama-2-7B-Chat,prompt 200 tokens / output 256 tokens / 32 并发):

指标v1 PyTorchv2 vLLM bf16v3 vLLM AWQ+EAGLE+Triton相对 v1
单请求 TTFT (ms)8002001206.7×
单请求 TPOT (ms/tok)3820103.8×
单请求端到端 (s)10.55.32.73.9×
32 并发吞吐 (tok/s)≈ 301100190063×
显存峰值 (GB)14.53622省了 14 GB
GPU 利用率30–50%80–95%85–95%拉满
模型质量 (MMLU)45.345.344.8-0.5
部署复杂度-

性能数字的解读纪律

  1. 绝对数字随硬件、prompt、并发变化:本文数字是 A100 40GB 的参考值,RTX 4090 上 v1/v2 的比例类似但绝对值不同。复现请用 推理基准测试实践 的方法。
  2. v3 的代价是复杂度:多了一个量化模型、一个 draft 模型、一个 Triton 编排层——线上事故时排查链路更长。是否值得上 v3,看你的 SLA 与成本账,详见 部署设计原则
  3. 质量不能只看 MMLU:业务评测(你的真实 prompt 分布上的准确率)才是最终裁判,详见 常见陷阱与反模式

七、项目目录结构

一个可维护的推理部署项目,文件组织要遵循"模型/代码/配置分离":

llama-deploy/
├── README.md                    # 部署指南、性能基准、回滚流程
├── requirements.txt             # 锁定版本
├── configs/
│   ├── v1_transformers.yaml     # 三版各自的配置
│   ├── v2_vllm.yaml
│   └── v3_vllm_awq_eagle.yaml
├── models/
│   ├── llama2-7b-chat/          # 原始权重(只读)
│   ├── llama2-7b-chat-awq/      # 量化权重
│   └── falcon-eagle-3-7b/       # 投机解码 draft model
├── src/
│   ├── __init__.py
│   ├── client.py                # 统一客户端(OpenAI / Triton)
│   ├── bench.py                 # 基准测试脚本
│   └── quantize.py              # AWQ 量化脚本
├── deploy/
│   ├── triton/
│   │   ├── model_repository/
│   │   └── start_triton.sh
│   └── docker/
│       ├── vllm.Dockerfile      # vLLM 镜像
│       └── triton.Dockerfile
├── scripts/
│   ├── bench_v1.sh              # 跑 v1 基准
│   ├── bench_v2.sh
│   └── bench_v3.sh
└── reports/
    └── bench_2024-08-22.md      # 三版性能对比报告
文件/目录职责为什么这么放
configs/三版配置集中切版本只改启动配置,不动代码
models/权重与训练框架解耦量化产物与原权重分离,回滚就是切配置
src/bench.py统一基准测试三版用同一套测试脚本,对比才公平
deploy/部署相关隔离Docker/Triton 配置不污染主代码

一个可复现部署的黄金检验:删掉 models/reports/,在新机器上跑 scripts/bench_v2.sh 能不能复现 v2 的数字? 如果能,工程化及格了。

八、常见坑

坑 1:v1 的 GPU 利用率当唯一指标

v1 跑出来 GPU 利用率 30%,看着"很闲",新手直接下结论"再开两个并发就解决了"。但 v1 的瓶颈是显存带宽不是算力——多开并发反而让显存带宽更挤,吞吐反而下降。这是 Roofline 模型与算力分析 的典型 memory-bound 场景。要先上 PagedAttention 把 KV cache 管好,再谈加并发。

坑 2:v2 的 gpu-memory-utilization 设太高

vLLM 默认 0.9(占 90% 显存做 KV pool)。如果同一张卡上还跑着 embedder 或 reranker,0.9 会让其他模型 OOM。生产环境推荐 0.7–0.8,留出余量,详见 调参与性能调优

坑 3:v3 的 AWQ 模型没跑业务评测

MMLU 只测通用能力。如果你的业务是代码生成、法律问答、医学摘要,必须在业务评测集上验证。曾经有团队上线 AWQ 代码模型后 JSON 输出格式直接坏掉(INT4 把特定 token 的 logits 量化偏移了),MMLU 几乎无变化但业务全崩。详见 常见陷阱与反模式

坑 4:v3 的 EAGLE draft model 显存没算进预算

EAGLE-3 7B 在 bf16 下自己就要 14 GB——你以为是"小模型"其实跟主模型一样大。部署时显存预算必须算上 draft model,否则启动时 OOM。

九、进阶方向

三版跑完只是起点,把它扩展成真正拿得出手的项目,可以走四个方向:

  1. 加分布式:把单卡 vLLM 升级到张量并行(TP=2/4)或流水线并行(PP),跨多卡服务大模型,详见 分布式推理(TP/PP)
  2. 加监控:上 Prometheus + Grafana,监控 QPS、p99、KV cache 命中率、GPU 温度——没有监控的部署等于裸奔。
  3. 加路由:用小模型处理简单请求、大模型处理难请求,降低平均成本,详见 作品集项目 的多模型路由项目。
  4. 加 RAG:把 embedder + 向量库 + reranker + LLM 串成一条 RAG 服务,做端到端业务,详见 作品集项目

如果你想用更细粒度的方式学每一步优化,从 v0 到 v5 的渐进式教程见 渐进式教程:三版跑起来。如果你想把它做成能在面试时讲清楚的项目,扩展路线见 作品集项目能力对标:简历该突出什么

十、延伸阅读

参考资料