模型服务化:Triton、vLLM 与 TGI 推理引擎

训练好的权重只是半成品,对外提供能力要靠推理引擎。本文对比 Triton、vLLM 与 TGI 三种主流服务化方案,讲清模型仓库与配置文件、动态批处理与连续批处理、PagedAttention 显存管理、张量并行切分、压测指标,以及从单机到 Kubernetes 的容量规划与扩缩容实践。

一个训练好的权重文件,离「能被业务调用的服务」还差很远:要处理并发请求、要拼接与截断输入、要管理显存、要暴露指标、要能灰度与回滚。把模型变成服务的过程叫模型服务化(Model Serving),由一类专门的推理引擎(Inference Engine)承担。

选错引擎的代价非常直接。同一张 A100,用朴素 Flask 包一层接口,吞吐可能只有每秒几十个 token;换成 vLLM 的连续批处理,同样硬件能到上千。本文对比三种主流方案——NVIDIA Triton、vLLM、Hugging Face TGI,讲清各自的适用边界与生产部署要点。

推理引擎到底解决什么

把服务化拆开,引擎要做的事分三层:

  • 协议层:接收 HTTP/gRPC 请求,做鉴权、限流、超时控制、流式返回(Server-Sent Events)。业务方不应感知底层是 PyTorch 还是 ONNX。
  • 调度层:把并发请求攒成批次、排队、抢占、回收显存。这是引擎性能差异的核心来源,也是「动态批处理」与「连续批处理」的分水岭。
  • 执行层:把张量送进 GPU,选择 kernel、管理算子融合与并行切分(张量并行 / 流水并行),并处理不同精度(FP16 / BF16 / INT8 / FP8)。

一个常被忽略的事实是:推理瓶颈往往不在算力,而在显存带宽。生成式模型逐 token 解码时,每步都要把全部权重从显存读一遍。因此引擎的两条主线是——减少重复访存(KV Cache、前缀缓存)与提高批次密度(让 GPU 每步处理更多请求)。

还有一组必须提前想清楚的约束:吞吐与延迟此消彼长。批次越大,单步摊薄的开销越少,吞吐越高;但每个请求要等更久才被组批,尾延迟随之上升。引擎的所有调度旋钮,本质上都在这个权衡曲线上选点。而冷启动是另一条暗线:7B 模型加载动辄二三十秒,扩容时若不预热,流量高峰期的弹性扩容反而会引发一波超时。

批处理的数学:为什么连续批处理更快

设单个请求的解码步数为 L,单步耗时为 t(与批大小近似无关,因为瓶颈在显存带宽而非算力),批大小为 B。静态批处理的总时间是 L·t,产出 B·L 个 token,吞吐为 B/t。

问题出在 L 不一致时:一批里有一个请求要生成 1000 个 token,另一个只要 50 个,整批必须等前者跑完,后者占着的显存白白空转。GPU 利用率在批尾急剧下降——这就是「气泡(bubble)」。

连续批处理在每个解码步重新组批:完成的请求立刻退出,队列里的新请求马上补位。吞吐的近似模型变为:只要队列不空,每一步都是满批,于是有效吞吐趋近 B/t 的上界,而不是被最慢请求拖到 B/(L_max·t) 的均值。

# 连续批处理的极简调度循环
def step_loop(queue, running, max_batch, max_tokens):
    while queue or running:
        # 1. 回收已结束的序列
        running = [s for s in running if not s.finished]

        # 2. 用空闲显存预算补位新请求
        while queue and len(running) < max_batch:
            s = queue.pop(0)
            if s.prompt_len + s.max_new_tokens > max_tokens:
                s.truncate(max_tokens)          # 超长请求截断
            running.append(s)

        # 3. 对当前 running 做一次前向,每序列生成一个 token
        logits = model.forward_batch([s.tokens for s in running])
        for s, lg in zip(running, logits):
            s.append_token(sample(lg))          # 逐 token 追加

真实实现(vLLM 的 Scheduler)还要处理抢占、分块预填充(chunked prefill)与显存换出,但骨架就是这三步。

Triton Inference Server:多框架通用服务

Triton 是 NVIDIA 出品的通用推理服务器,最大的特点是框架无关:同一个进程里可以同时托管 PyTorch、TensorRT、ONNX Runtime、TensorFlow、Python 后端甚至自定义 C++ 后端。

模型仓库(Model Repository)

Triton 按固定目录结构加载模型,每个模型一个子目录,版本用数字目录区分:

model_repository/
├── text_embedding/
│   ├── config.pbtxt
│   ├── 1/
│   │   └── model.onnx
│   └── 2/
│       └── model.onnx
└── reranker/
    ├── config.pbtxt
    └── 1/
        └── model.plan

目录里的数字就是版本号,Triton 默认加载最新版本,并支持多版本共存与灰度切换。

config.pbtxt 关键参数

name: "text_embedding"
platform: "onnxruntime_onnx"
max_batch_size: 64

input [
  {
    name: "input_ids"
    data_type: TYPE_INT32
    dims: [ -1 ]
  }
]
output [
  {
    name: "embedding"
    data_type: TYPE_FP32
    dims: [ 768 ]
  }
]

dynamic_batching {
  preferred_batch_size: [ 8, 16, 32 ]
  max_queue_delay_microseconds: 5000
}

instance_group [
  {
    count: 2
    kind: KIND_GPU
    gpus: [ 0, 1 ]
  }
]

几个必须理解的字段:

参数作用调优方向
max_batch_size单次推理最大批大小受显存限制,过大易 OOM
preferred_batch_size优先攒到的批次档位与 kernel 高效档位对齐
max_queue_delay_microseconds攒批等待上限越大吞吐越高、延迟越差
instance_group.count每 GPU 上的模型实例数提升并发,代价是显存翻倍
kindKIND_GPU / KIND_CPU按算力需求选择

max_queue_delay_microseconds 是延迟与吞吐的直接旋钮。设成 5000(5ms)意味着请求最多等 5ms 攒批;对在线问答这种延迟敏感场景,通常设 1~5ms,而对离线批处理可以放到几十毫秒。

集成(Ensemble)与业务逻辑脚本

当服务需要「预处理 → 模型 → 后处理」多步串联时,Triton 提供 Ensemble 把多个模型串成一张有向图,数据在 Triton 内部以共享内存传递,不经过网络:

ensemble_scheduling {
  step [
    { model_name: "tokenizer"  model_version: -1
      input_map  { key: "text"       value: "text" }
      output_map { key: "input_ids"  value: "input_ids" } }
    { model_name: "text_embedding" model_version: -1
      input_map  { key: "input_ids"  value: "input_ids" }
      output_map { key: "embedding"  value: "embedding" } }
  ]
}

复杂的 Python 逻辑(如动态路由、外部特征拼接)则用 BLS(Business Logic Scripting) 后端,在 Python 里以进程内调用的方式触发其他模型,避免网络往返。

动态批处理与序列批处理

Triton 的 dynamic_batching 会把同一模型、不同请求的输入在批维度上拼接。对 Transformer 这类变长输入,用 sequence_batching 维护会话状态,避免每次重算上下文。这两者与 vLLM 的连续批处理并不是一回事——Triton 的批一旦形成就跑到结束,中途不会插入新请求。对生成式 LLM,Triton 通常配合 TensorRT-LLM 后端(inflight_batcher_llm)才能获得连续批处理能力。

vLLM:为 LLM 而生的引擎

vLLM 的核心创新是 PagedAttention:把 KV Cache 按固定大小的块(block)管理,像操作系统分页一样用页表映射逻辑序列到物理块。这解决了传统实现中「按最大长度预分配显存」造成的巨大浪费。

传统方案里,一个序列的 KV Cache 必须连续分配,长度按 max_model_len 预留。实际生成长度若只有一半,另一半显存就永远闲置,显存利用率常低至 20%~40%。分页后,块按需分配、用多少占多少,显存碎片几乎消失,实测可把显存利用率拉到 90% 以上,直接转化为更大的批大小与更高的吞吐。

连续批处理与抢占

vLLM 的调度器维护 waiting 与 running 两个队列,每个解码步尝试把 waiting 里的请求塞进 running。当显存不足以容纳所有 running 序列时,触发抢占(preempt):选一个序列换出,其 KV 块释放(recompute 策略)或搬到 CPU 内存(swap 策略)。

  • recompute:丢弃 KV,需要时重算前缀。适合序列短、算力富余的场景。
  • swap:把 KV 换到 CPU 内存(--swap-space 指定 GB 数),需要时换回。适合显存紧张但内存充裕的场景。

抢占次数是重要的健康指标:持续抢占说明显存不足、批密度被迫降低,应调低 --max-num-seqs 或升级显存。

启动与参数

python -m vllm.entrypoints.openai.api_server \
  --model Qwen/Qwen2.5-7B-Instruct \
  --tensor-parallel-size 2 \
  --gpu-memory-utilization 0.90 \
  --max-model-len 8192 \
  --max-num-seqs 256 \
  --enable-prefix-caching \
  --swap-space 8 \
  --port 8000

关键参数含义:

  • --tensor-parallel-size:把单层权重按列/行切到多卡,需要卡间高速互联(NVLink)。
  • --gpu-memory-utilization:允许 vLLM 占用的显存比例,KV Cache 用剩余显存;0.90 是常用起点。
  • --max-num-seqs:同时在飞的序列数上限,直接决定批密度。
  • --enable-prefix-caching:相同前缀(如系统提示词)复用 KV,多轮对话场景收益显著。

vLLM 对外暴露 OpenAI 兼容接口,客户端可直接用 openai SDK 指向本地地址,迁移成本极低。

量化与 LoRA 多租户

vLLM 支持 GPTQ、AWQ、FP8 等量化权重直接加载,也支持运行期挂载多个 LoRA 适配器,实现「一个基座服务多个微调租户」:

python -m vllm.entrypoints.openai.api_server \
  --model Qwen/Qwen2.5-7B-Instruct \
  --quantization awq \
  --enable-lora \
  --max-loras 4 --max-lora-rank 32 \
  --lora-modules tenant-a=/adapters/a tenant-b=/adapters/b

请求时通过 model 字段指定租户名,vLLM 在每步动态合并 LoRA 权重。这让「按客户定制」不必为每个客户单独起一个进程,显存利用率大幅提升。

TGI:Hugging Face 的生产化封装

TGI(Text Generation Inference)由 Hugging Face 维护,主打「开箱即用」:内置 token 流式输出、token 级水印、张量并行、量化加载与 OpenAI 兼容路由。

docker run --gpus all --shm-size 1g -p 8080:80 \
  -v $PWD/data:/data \
  ghcr.io/huggingface/text-generation-inference:latest \
  --model-id meta-llama/Llama-3.1-8B-Instruct \
  --num-shard 2 \
  --max-input-length 4096 \
  --max-total-tokens 8192 \
  --max-batch-prefill-tokens 8192

TGI 的批处理同样支持连续组批,--max-batch-prefill-tokens 控制预填充阶段(prefill)的总 token 预算,是防止长输入把显存打爆的重要护栏。相比 vLLM,TGI 的配置项更少、更偏约定优于配置,适合快速上线标准 LLM。

三者对比与选型

维度TritonvLLMTGI
定位通用多框架服务器LLM 专用引擎LLM 生产化封装
批处理动态批 / 序列批连续批处理连续批处理
显存管理手动配置PagedAttentionPagedAttention 变体
多模型托管原生支持单模型为主单模型
张量并行支持支持支持
流式输出支持支持原生优化
适合场景传统 CV/NLP 多模型自建 LLM 服务快速上线 HF 模型

选型经验:多模型混布、需要严格版本管理用 Triton;追求 LLM 吞吐与成本用 vLLM;想最快跑通 Hugging Face 模型用 TGI。三者并不互斥——常见做法是 Triton 托管嵌入与重排模型,vLLM 单独托管生成模型,前面再挂一层网关做路由。

性能基准与压测

上线前必须拿到本机的真实曲线,而不是看论文数字。推荐用固定并发、逐级加压的方式,记录 TTFT 与吞吐:

# 用 vLLM 自带的基准脚本测吞吐
python benchmarks/benchmark_throughput.py \
  --model Qwen/Qwen2.5-7B-Instruct \
  --dataset-name sharegpt \
  --num-prompts 1000 \
  --output-len 256

# 用 vLLM 的在线压测测延迟
python benchmarks/benchmark_serving.py \
  --model Qwen/Qwen2.5-7B-Instruct \
  --num-prompts 500 --request-rate 20 \
  --metric-percentiles 50,90,99

一份典型的结果(A100 80G,7B BF16):

并发吞吐 (tok/s)TTFT P50 (ms)TPOT P99 (ms)
1554022
83806525
3295018038
128140072095

读法:吞吐随并发上升,但 TTFT 在并发 32 之后快速劣化。业务若要求首字延迟 < 200ms,就要把单实例并发压在这个拐点之下,多起实例横向扩展,而不是继续加大 --max-num-seqs。

生产部署要点

健康检查与就绪探针

模型加载可能耗时数十秒到数分钟(大模型权重读盘 + 编译)。就绪探针必须等引擎真正可服务后再放流量,否则会有一波 5xx:

readinessProbe:
  httpGet:
    path: /health
    port: 8000
  initialDelaySeconds: 30
  periodSeconds: 10
  failureThreshold: 30

扩缩容信号

GPU 服务不能只看 CPU 使用率。合理信号是队列深度与KV Cache 利用率:

# vLLM 暴露的 Prometheus 指标
vllm:num_requests_waiting          # 排队请求数,持续 >0 说明要扩容
vllm:gpu_cache_usage_perc          # KV Cache 占用率
vllm:time_to_first_token_seconds   # TTFT,SLA 核心

按 num_requests_waiting 做 HPA 自定义指标,比按 GPU 利用率更贴合真实负载。GPU 扩缩容还要注意:缩容要慢、扩容要快——新 Pod 加载权重期间无法服务,缩得太急会在流量回升时踩空。

版本灰度与回滚

模型迭代频繁,必须支持不重启切换版本。Triton 通过模型控制 API 显式加载/卸载版本,天然支持多版本共存:

# 加载新版本 3,此时 2 与 3 同时在内存
curl -X POST localhost:8000/v2/repository/models/text_embedding/versions/3/load
# 灰度:把 10% 流量切到 v3
# 观察指标后,卸载旧版本
curl -X POST localhost:8000/v2/repository/models/text_embedding/versions/2/unload

vLLM/TGI 单进程只托管一个模型,版本切换靠蓝绿部署:新版本起一组新 Pod,就绪后把 Service 权重逐步切过去,旧 Pod 保留一段时间以便快速回滚。无论哪种方式,都要保留「一键回到上一版本」的能力——模型质量问题是概率性的,指标恶化常常滞后几小时才显现。

常见坑

  • 冷启动慢:权重放本地 NVMe 或做镜像预热,别每次从对象存储拉。
  • OOM 随机出现:gpu-memory-utilization 设太高,留 5%~10% 余量给临时激活。
  • 首 token 慢、后续快:预填充阶段计算密集,可调 max-batch-prefill-tokens 限制单批预填充量。
  • 流式返回被缓冲:反向代理(Nginx/Ingress)需关闭 response buffering,否则流式变成一次性返回。
  • 多实例争抢显存:同卡跑多个引擎时,各自按整卡比例估算会超配,需显式限制显存。

成本估算与硬件选型

服务化的最终约束是钱。一张 A100 80G 的云端按需价约 3~4 美元/小时,能否跑满决定了单位 token 成本。粗略估算:

场景卡型单卡吞吐每小时 token 数每百万 token 成本
7B 对话A10G 24G~900 tok/s3.2M~0.4 美元
7B 对话(量化)A10G 24G~1500 tok/s5.4M~0.25 美元
70B 对话2×A100 80G~700 tok/s2.5M~2.8 美元
嵌入模型T4 16G~5000 req/s—极低

从表里能读出两条选型原则:能用小模型解决就别上大模型(7B 与 70B 的成本差近十倍);嵌入与重排这类小模型别占大卡,用 T4/L4 甚至 CPU 就够。真实场景里,把 80% 的简单请求路由给小模型、只把难题交给大模型,是比任何单点优化都有效的降本手段。

引擎之外:网关与观测

引擎只负责单模型的推理。生产系统还需要一层**推理网关(Inference Gateway)**承担:

  • 多模型路由:按请求复杂度、租户、成本选择后端模型。
  • 语义缓存:命中相似历史请求直接返回,省下一次完整推理。
  • 限流与配额:按租户维度限制 QPS 与 token 消耗。
  • 降级与熔断:后端过载时切到更小模型或返回兜底答案。

观测上,除了引擎自身的 TTFT/TPOT/队列深度,还要在网关侧记录端到端延迟、缓存命中率、路由分布与错误码。只有把「引擎指标」与「业务指标」打通,才能在故障时快速定位是引擎排队还是网关慢。

小结

模型服务化的本质是在显存、算力与延迟之间做资源调度。Triton 提供通用性与多模型治理,vLLM 用 PagedAttention 与连续批处理把 LLM 吞吐拉满,TGI 用约定优于配置换上线速度。理解批处理与显存管理这两条主线,就能在选型时不迷信单点指标。引擎之上还有容量规划与弹性伸缩的问题,可结合 LLM 推理引擎 的横向对比与 模型部署 的工程实践一起看;若要在已有优化基础上继续压榨吞吐,LLM 推理优化 给出了从 KV Cache 到投机解码的完整路径。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「ai」更多文章

  1. 排序学习与搜索召回排序系统
  2. 数据版本控制与血缘:DVC 与 LakeFS
  3. 模型可解释性:SHAP、LIME 与注意力归因