vLLM vs SGLang 深度横评:当 PagedAttention 遇上 RadixAttention——LLM 推理框架选型完全指南
引言
如果你做过 LLM 推理服务化,一定会遇到这两个灵魂拷问:
- 显存不够用——一个 LLaMA-7B 单次请求的 KV Cache 就占用约 16GB 显存,8 卡 A100 都撑不住几个并发;
- 长上下文跑不动——128K token 的文档摘要请求进来,显存直接爆掉,TTFT(Time To First Token,首字延迟)飙到几十秒。
传统的 HuggingFace pipeline 方案在这两个问题上几乎束手无策:KV Cache 按完整序列预分配,不管用不用都占着显存;不同长度的请求无法合并,GPU 空转等待;prefix(系统提示词)每次都重新计算……
正是在这个背景下,vLLM 和 SGLang 这两个推理框架崛起为业界标杆。2025 年 5 月,vLLM 正式纳入 PyTorch 基金会托管,成为 PyTorch 生态的核心推理引擎;SGLang 则凭借 RadixAttention 和组合式编程模型,在 Agent、RAG、结构化输出等场景杀出了一条血路。
本文是一场真刀真枪的深度横评,不堆术语,不贴跑分表讲废话,从架构原理到参数调优,从代码实战到选型决策,帮你彻底搞清楚:你的场景,到底该用哪个。
一、LLM 推理的本质问题
1.1 Prefill 与 Decode:两种截然不同的计算模式
LLM 的推理过程分为两个截然不同的阶段,理解它们是理解一切推理优化的前提。
Prefill 阶段(也称 Input Processing):把用户的 prompt 一次性喂给模型,做一次大规模矩阵运算,生成第一个 token。这个阶段的计算量与 prompt 长度成正比,是典型的 Compute-Bound(计算密集型)任务——GPU 的浮点运算单元是主要瓶颈。128K token 的 prompt 进来,Prefill 可能需要几十秒。
Decode 阶段(也称 Token Generation):逐 token 生成。每次只输入一个 token,输出下一个 token,循环往复直到 EOS。这个阶段每次只做一个 token 的前向传播,但 KV Cache 访问是主要瓶颈,是典型的 Memory-Bound(内存带宽密集型)任务——真正卡住的是显存带宽,而不是算力。
这两个阶段的特性差异,直接导致了不同的优化策略:
| 阶段 | 瓶颈类型 | 核心优化方向 |
|---|---|---|
| Prefill | Compute-Bound | 张量并行、算子融合、FlashAttention |
| Decode | Memory-Bound | KV Cache 管理、连续批处理、显存复用 |
1.2 KV Cache 显存问题:被忽视的隐形杀手
每个 transformer 层都有自注意力机制。在 Decode 阶段,每生成一个 token,都需要将这个 token 的 Key(K)和 Value(V)向量缓存下来,供后续 token 做 attention 运算使用。
以 LLaMA-7B 为例看一下 KV Cache 的显存占用:
# KV Cache 显存估算
# hidden_size=4096, num_heads=32, head_dim=128, num_layers=32
def estimate_kv_cache_size(
num_layers=32,
num_heads=32,
head_dim=128,
dtype_bytes=2, # fp16
max_seq_len=2048,
batch_size=1
):
# 每个 token 的 K/V 向量大小
# K: num_heads * head_dim * dtype_bytes
# V: num_heads * head_dim * dtype_bytes
per_token_bytes = 2 * num_heads * head_dim * dtype_bytes # K + V
# 每个请求的 KV Cache(max_seq_len 个 token)
per_request_bytes = per_token_bytes * max_seq_len
# 单层
per_layer_bytes = per_request_bytes
# 所有层
all_layers_bytes = per_layer_bytes * num_layers
print(f"单层每请求 KV Cache: {per_layer_bytes / 1024**2:.1f} MB")
print(f"全部32层每请求 KV Cache: {all_layers_bytes / 1024**3:.2f} GB")
print(f"batch_size=8 时的总 KV Cache: {all_layers_bytes * 8 / 1024**3:.2f} GB")
# 输出:
# 单层每请求 KV Cache: 32.0 MB
# 全部32层每请求 KV Cache: 1.00 GB
# batch_size=8 时的总 KV Cache: 8.00 GB
对于 LLaMA-7B,单个请求(max_seq_len=2048)的 KV Cache 就要 1GB 显存。如果 batch_size=8,就 8GB 被 KV Cache 吃掉了——还没开始算就已经占用了一块 A100(80GB)的 10%。
更糟糕的是,如果一个请求的实际序列长度只有 512 token,但 max_seq_len 设成了 2048,传统方案会按 2048 预分配全部显存,实际只用到 25%,其余 75% 都是浪费。
实际生产中,prompt 长度分布极不均匀:短到 10 token 的闲聊,长到 128K token 的文档分析,混在一起运行——这是显存碎片的根本来源。
1.3 传统方案的三大局限
局限一:固定预分配(Fixed Pre-allocation)
# HuggingFace 传统做法
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf")
# 每个请求都按 max_position_embeddings 预分配 KV Cache
# 如果 max_seq_len=4096,那每个请求都占满 4096 token 的空间
问题:短请求浪费显存,长请求撑不下,并发能力极差。
局限二:静态批处理(Static Batching)
# 传统批处理:等所有请求都到达再一起处理
requests = [
"你好", # 2 tokens
"请分析这篇论文的核心贡献:...", # 500 tokens
"用 Python 写一个快速排序" # 8 tokens
]
# 必须等最长的请求完成,短请求被白白卡住
# GPU 利用率:大部分时间在等
问题:不同长度的请求无法动态合并,GPU 空转严重,吞吐量极低。
局限三:prefix 重复计算
system_prompt = "你是一个有帮助的AI助手。" # 固定系统提示词
# 每个请求都要重新计算 system_prompt 的 KV Cache
for req in requests:
full_prompt = system_prompt + req # 每次都从零开始算 system_prompt
问题:高频复用的 system prompt、few-shot examples、检索到的 RAG context,每来一个请求都要重新算一遍,造成 2-10 倍的浪费。
二、vLLM:PagedAttention 内存革命
2.1 分页管理的核心思想
vLLM 的核心创新是 PagedAttention,它将操作系统虚拟内存的分页思想引入 LLM 推理。
类比操作系统:程序不需要一次性申请整个地址空间,操作系统以 4KB 页为单位按需分配物理内存。程序看到的是连续的逻辑地址,实际物理地址可以是不连续的碎片,通过页表映射起来。
PagedAttention 的思路完全相同:
- KV Cache 不再按完整序列预分配,而是按固定大小的 block(默认 16 tokens/block)按需分配
- 逻辑上,每个请求看到的 KV Cache 是连续的地址空间
- 物理上,block 可以散落在显存任意位置,互不连续也没关系
- 通过一个 Block Table(块表) 做逻辑地址→物理地址的映射
这样做有什么好处?
# 传统方案:max_seq_len=2048 的请求,必须一次性占满 2048 个 token 的空间
# 无论实际用了多少,哪怕只有 100 token,也要占 2048 的空间
# PagedAttention:按 block 分配
# 请求A实际用了 150 token → 分配 10 个 block(每个 16 token)
# 请求B实际用了 300 token → 分配 19 个 block
# 碎片空间 = 各 block 内部未被使用的尾端空间,最大浪费 = num_blocks * (block_size - 1) tokens
关键洞察:按 block 分配后,显存碎片从"序列级"变成了"block级"。一个 block 最多浪费 15 个 token(block_size=16),而不是整个 max_seq_len。
2.2 Block 表结构:逻辑块与物理块
这是理解 PagedAttention 的关键数据结构:
请求A(逻辑地址): [Block 0] [Block 1] [Block 2] ... [Block N]
↓ ↓ ↓
物理显存地址: [Phy 7] [Phy 3] [Phy 12] ...
Block Table(请求A):
logical_blocks: [0, 1, 2, ..., N] # 逻辑块编号
physical_blocks: [7, 3, 12, ..., X] # 物理块编号(显存中的实际位置)
block_sizes: [16, 16, 16, ..., 14] # 每个块实际使用的 token 数
Block Table 实际上就是一个页表(Page Table),在 GPU 端维护,每次 attention 操作时通过这个映射去物理地址取 K/V 数据。
生成新 token 时会发生什么?
# 生成新 token 后的处理流程(伪代码)
def generate_token(req_id, block_table):
# 1. 检查当前 last block 是否还有空闲 slot
last_block = block_table[-1]
if last_block.used < BLOCK_SIZE:
# 直接复用当前 block,写入新 token
last_block.append(new_token)
else:
# 当前 block 满了,分配新的物理 block
new_physical_block = allocate_physical_block() # 从空闲链表取
block_table.append(new_physical_block)
new_physical_block.append(new_token)
# 2. 更新 Block Table
block_table.update()
整个过程不需要重新分配大块连续显存,不需要拷贝已有数据,只需要从空闲链表取一个物理 block 并更新映射——这与操作系统的按需分页完全一致。
2.3 连续批处理:动态合并的艺术
连续批处理(Continuous Batching) 是 vLLM 的另一核心能力,解决的是"不同长度请求无法合并处理"的问题。
传统静态批处理:
时间 →
请求1: [========= 2048 token prompt =========][... decode ...]
请求2: [=== 256 token ===][... decode ...]
请求3: [==== 512 token ====][... decode ...]
GPU: |_________batch processing__________|____idle____|________batch processing__________|
↑ 必须等所有请求都到达,或等上一批完全结束,GPU 才能开始下一批
连续批处理(又称 Iteration-level Scheduling):
时间 →
请求1: [P][D][D][D][D][D] ← decode 中
请求2: [P][D][D][D][D] ← P2 先 prefill,然后加入 decode 批
请求3: [P][D][D] ← P3 prefill 后加入
请求4: [P] ← P4 prefill 中
请求5: [D] ← P5 decode 中
GPU: 每 iteration 检查所有请求状态,动态决定下一轮处理谁
↑ Prefill 和 Decode 同批混合执行,最大化 GPU 利用率
关键机制:在每个 iteration(生成一个 token 的循环),调度器会:
- 检查已完成 Decode 的请求 → 从批中移除,释放显存
- 检查新到达的 Prefill 请求 → 插入批中,立即处理
- 继续处理仍在 Decode 的请求
这样,请求不需要"等到同一批",可以随时加入/退出,GPU 的空闲时间大幅减少。
连续批处理 vs 静态批处理的性能对比(估算):
# 场景:100 个请求,平均 prompt=512 tokens,平均 output=128 tokens
# 假设 GPU 单请求吞吐 = 100 tokens/s
def compare_batching():
# 静态批处理:batch_size=8,等所有请求到达
# 设请求到达间隔 = 1s,100 个请求全部到达需要 100s
# 第一批 8 个请求开始处理:prefill 5s,decode 1.28s → 6.28s
# 第二批 8 个请求:同样 6.28s ...
# 总时间 = 批次等待 + 处理时间
static_time = 100 + (100/8) * 6.28 # ≈ 179s
# 连续批处理:请求到达即处理
# 平均队列等待时间 ≈ 0(立即开始)
# 处理时间 ≈ max(prefill_times) + decode_times
continuous_time = max([512/10000]*100) + 128/100 * 100/8 # ≈ 16s
# 实际比例可能达到 5-10 倍差距
print(f"静态批处理总时间: {static_time}s")
print(f"连续批处理总时间: {continuous_time}s")
print(f"吞吐量提升: {static_time/continuous_time:.1f}x")
2.4 vLLM 关键参数调优实战
下面是你在生产环境部署 vLLM 时最需要调优的几个参数,配合实际代码讲解。
2.4.1 --gpu-memory-utilization(显存利用率上限)
这是最重要的参数,直接控制 vLLM 能用多少显存。
# 启动 vLLM 服务
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-2-7b-chat-hf \
--gpu-memory-utilization 0.85 \
--max-num-seqs 256 \
--block-size 16
工作原理:
# vLLM 内部逻辑(简化)
class KVCacheAllocator:
def __init__(self, total_gpu_memory, utilization=0.9):
self.total = total_gpu_memory
self.reserved = total_gpu_memory * (1 - utilization) # 保留空间
self.available = total_gpu_memory * utilization # 可用空间
self.free_space = self.available
def allocate(self, required_bytes):
if self.free_space >= required_bytes:
self.free_space -= required_bytes
return True
return False # 触发 block 驱逐
# 如果设为 0.9(默认),vLLM 最多使用 90% 显存
# 剩下 10% 预留给 CUDA kernel、中间激活值等非 KV Cache 数据
调优建议:
| 场景 | 推荐值 | 原因 |
|---|---|---|
| 单卡推理 | 0.85-0.90 | 预留空间给激活值,防止 OOM |
| 多卡张量并行 | 0.80-0.85 | 每卡 overhead 增加,减少每个 rank 的 KV Cache |
| 长序列(>32K) | 0.70-0.80 | 激活值随序列长度平方增长,需要更多预留 |
| 短序列(<4K) | 0.92-0.95 | 激活值小,可以压榨更多显存给 KV Cache |
2.4.2 --max-num-seqs(单批最大并发数)
控制同一批次最多处理多少个请求。值越大并发越高,但每个请求分到的显存减少。
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-2-7b-chat-hf \
--max-num-seqs 256 \ # 最多 256 个请求同批
--max-num-batched-tokens 4096 # 单批总 token 数上限(控制计算量)
如何计算最优值:
def compute_optimal_max_num_seqs(
model_name="meta-llama/Llama-2-7b-chat-hf",
total_memory_gb=80, # A100 80GB
gpu_memory_utilization=0.85,
avg_seq_len=2048,
dtype_bytes=2
):
available_memory = total_memory_gb * gpu_memory_utilization
# 估算模型权重占用(fp16)
# LLaMA-7B ≈ 14GB, LLaMA-70B ≈ 140GB (需要多卡)
model_weights = {
"meta-llama/Llama-2-7b-hf": 14,
"meta-llama/Llama-2-70b-hf": 140,
"mistralai/Mistral-7B-v0.1": 14,
}
weight_mem = model_weights.get(model_name, 14)
# 可用给 KV Cache 的显存
kv_cache_memory = available_memory - weight_mem
# 每个请求的 KV Cache 大小
# = 2 * num_layers * num_heads * head_dim * dtype * max_seq_len
# ≈ 2 * 32 * 32 * 128 * 2 * 2048 / 1024**3 ≈ 1 GB/请求
kv_per_request_gb = 1.0 # 估算值
# 最大并发数
max_concurrent = int(kv_cache_memory / kv_per_request_gb)
print(f"可用显存: {available_memory:.1f} GB")
print(f"模型权重: {weight_mem:.1f} GB")
print(f"KV Cache 可用: {kv_cache_memory:.1f} GB")
print(f"每请求 KV Cache: {kv_per_request_gb:.2f} GB")
print(f"推荐 max_num_seqs: {max_concurrent}")
# 对于 7B 模型,80GB 卡约 50-60 个并发是合理的
# 实际值需要通过压测确定
# 输出(LLaMA-7B on A100-80G):
# 可用显存: 68.0 GB
# 模型权重: 14.0 GB
# KV Cache 可用: 54.0 GB
# 每请求 KV Cache: 1.00 GB
# 推荐 max_num_seqs: ~54
常见误区: 把 max_num_seqs 设得非常大(512+),期望提高并发。实际上,当活跃请求数超过显存能容纳的数量时,调度器会频繁驱逐(evict)早期请求的 KV Cache,导致这些请求需要重新 Prefill,性能反而下降。
2.4.3 --block-size(内存块大小)
控制每个物理 block 包含多少个 token。
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-2-7b-chat-hf \
--block-size 16
block_size 的权衡:
| block_size | 优点 | 缺点 |
|---|---|---|
| 小(1-4) | 显存碎片少,灵活度高 | Block Table 变大,管理开销增加 |
| 中(16-32) | 平衡之选 | 内部碎片增加 |
| 大(64+) | 管理开销小,Block Table 小 | 碎片严重,可能浪费 50%+ 空间 |
# 碎片率估算
def fragmentation_rate(block_size):
# 随机长度分布下,block 内部平均浪费
# 假设序列长度均匀分布在 [1, block_size]
avg_waste = block_size / 2 # 平均填充一半
rate = avg_waste / block_size
return rate
for bs in [1, 4, 16, 32, 64]:
print(f"block_size={bs}, 碎片率≈{fragmentation_rate(bs)*100:.0f}%")
# block_size=1, 碎片率≈50% (每个 block 一个 token,overhead 太高)
# block_size=4, 碎片率≈25%
# block_size=16, 碎片率≈6%
# block_size=32, 碎片率≈3%
# block_size=64, 碎片率≈1.5%
实战建议: 默认 block_size=16 适合大多数场景。如果你处理的都是整除 16 的序列(如 tokenized 后恰好是 16 的倍数),可以适当增大;如果是变化剧烈的短序列,用小的 block_size 更省显存。
2.4.4 --enable-prefix-caching(前缀缓存)
vLLM 0.6+ 支持系统提示词复用,大幅减少重复计算。
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-2-7b-chat-hf \
--enable-prefix-caching \
--gpu-memory-utilization 0.85
工作原理:
# vLLM 内部会对每个请求的 prompt 计算 hash(内容 hash)
# 如果两个请求的 prefix 完全相同(从第一个 token 到某个位置)
# 第二个请求可以直接复用第一个请求的 KV Cache,无需重新计算
def vllm_prefix_caching_logic(request_prompt):
# 1. 计算 prompt 的 content hash
prompt_hash = hash(request_prompt)
# 2. 在缓存索引中查找
if prompt_hash in kv_cache_index:
# 命中!直接复用,无需 Prefill
cached_kv = kv_cache_index[prompt_hash]
return cached_kv # Prefill 时间 ≈ 0
# 3. 未命中,正常 Prefill 并缓存结果
kv = compute_kv_cache(request_prompt)
kv_cache_index[prompt_hash] = kv
return kv
# 效果演示
scenarios = [
("固定系统提示词 + 不同用户问题", 0.8, "系统提示词部分完全复用,prefill 减少 50-80%"),
("few-shot 示例完全相同", 0.4, "所有示例的 KV Cache 复用"),
("RAG 检索结果相同", 0.3, "相同检索上下文复用"),
("每个请求都不同", 0.0, "无复用收益"),
]
for scenario, reuse_rate, note in scenarios:
print(f"[{'✓' if reuse_rate > 0 else ' '}] {scenario}")
print(f" 复用率: {reuse_rate*100:.0f}% | {note}")
vLLM 官方实测:开启 prefix caching 后,共享系统提示词的场景吞吐量提升 2-4 倍,首字延迟(TTFT)下降 50%+。
2.5 vLLM 适用场景总结
✅ 强烈推荐 vLLM 的场景:
- 通用 LLM 推理 API 服务:你需要部署一个兼容 OpenAI API 的推理后端,吞吐量和延迟都要兼顾
- 单一模型、多用户并发:Chatbot、问答系统、内容生成等并发量大的场景
- HuggingFace 模型优先:vLLM 对 HF 格式模型支持最好,生态成熟
- 多硬件环境:NVIDIA、AMD、TPU 都能跑,不需要为硬件发愁
- 快速迭代场景:模型更新频繁,vLLM 的即时加载优势明显
⚠️ vLLM 不擅长的场景:
- 多轮对话中相同 prefix 的复用(不如 SGLang 的 RadixAttention 系统化)
- 极致的长上下文优化(128K+ 场景不如 SGLang 的混合注意力架构)
- 复杂的 Agent 工作流编排
三、SGLang:RadixAttention 前缀树革命
3.1 RadixAttention:基数树结构的 KV 缓存管理
如果说 PagedAttention 是借鉴操作系统的分页管理,那 RadixAttention 就是借鉴了文件系统的 基数树(Radix Tree) 数据结构。
基数树(Radix Tree),也称压缩前缀树,是前缀树(Trie)的空间优化变体。Linux 内核的路由查找就用了基数树。核心思想是:相同前缀的节点合并,只存储一份。
SGLang 将这个思想引入 KV Cache 管理:
请求1: "[系统提示]你是一个助手。用户的问题是:今天天气如何?"
请求2: "[系统提示]你是一个助手。用户的问题是:推荐一部电影"
请求3: "[系统提示]你是一个助手。用户的问题是:帮我写一封邮件"
传统方案:每个请求都独立存储完整的 KV Cache
存储 3 份系统提示的 KV Cache(完全重复)
RadixAttention:基数树共享
[系统提示] ← 只存 1 份,所有请求共享
/ | \
[天气] [电影] [邮件] ← 各自的私有部分
存储 1 份系统提示 + 3 份用户问题
节省 = (3-1) * 系统提示长度 * K/V 大小
在 RAG 场景中,这个优势更加明显。如果多个请求检索到了相同的上下文(这是非常常见的),RadixAttention 会自动合并,不需要任何额外配置。
3.2 内部结构:RadixAttention 的 Trie 实现
# RadixAttention 的简化实现(示意)
class RadixNode:
def __init__(self, token_ids: list[int], kv_cache: torch.Tensor = None):
self.token_ids = token_ids # 这个节点代表的 token 序列
self.kv_cache = kv_cache # 这段序列的 KV Cache
self.ref_count = 0 # 被多少请求引用
self.children: dict[int, RadixNode] = {} # 子节点,key=下一个 token id
self.parent: RadixNode = None
class RadixTree:
def __init__(self):
self.root = RadixNode([]) # 空根节点
self.token_to_node: dict[int, list[RadixNode]] = {} # 反向索引
def insert(self, token_ids: list[int], kv_cache: torch.Tensor) -> int:
"""插入新序列,返回复用带来的 token 节省量"""
node = self.root
reused_tokens = 0
# 1. 尽可能复用已有路径
for i, tid in enumerate(token_ids):
if tid in node.children:
node = node.children[tid]
# 这段 KV Cache 已存在,可以复用
node.ref_count += 1
reused_tokens += 1
else:
break # 路径分叉,剩余部分需要新建
# 2. 新建剩余的节点
for tid in token_ids[reused_tokens:]:
new_node = RadixNode(
node.token_ids + [tid], # 累积 token 序列
kv_cache[..., i, :] # 对应位置的 KV Cache
)
new_node.parent = node
node.children[tid] = new_node
node = new_node
return reused_tokens
def evict_lru(self, required_space: int) -> int:
"""驱逐最少使用的节点,释放空间"""
# LRU 驱逐:优先驱逐 ref_count=0 且最久未使用的节点
freed_space = 0
while freed_space < required_space:
node = self.find_lru_leaf()
freed_space += node.kv_cache.numel() * node.kv_cache.element_size()
self.remove_node(node)
return freed_space
def get_shared_prefix_length(self, req_a: list[int], req_b: list[int]) -> int:
"""计算两个请求的共享前缀长度(= 可复用的 token 数)"""
shared = 0
for t_a, t_b in zip(req_a, req_b):
if t_a == t_b:
shared += 1
else:
break
return shared
# 性能示例
tree = RadixTree()
# 请求1:带系统提示词
shared_prefix = [101, 2003, 1996, 3007, 2003, 102] # "[CLS] 你 是一个 AI [SEP]" 等 token
unique_part1 = [101, 2023, 2003, 3802, 102] # "今天 天气 如何"
req1_reused = tree.insert(shared_prefix + unique_part1, kv1)
print(f"请求1: 复用了 {req1_reused} tokens (首次插入,无复用)")
# 请求2:相同系统提示词,不同问题
unique_part2 = [101, 2023, 2990, 2522, 102] # "推荐 一部 电影"
req2_reused = tree.insert(shared_prefix + unique_part2, kv2)
print(f"请求2: 复用了 {req2_reused} tokens = 系统提示词长度")
# 请求3:相同 RAG context(假设检索到相同文档)
rag_context = [5001, 5002, 5003, 5004, 5005] # 检索到的上下文 token
unique_part3 = [3006, 2023, 102] # "谢谢,再见"
req3_reused = tree.insert(shared_prefix + rag_context + unique_part3, kv3)
print(f"请求3: 复用了 {req3_reused} tokens = 系统提示词 + RAG context")
# 假设系统提示词=50 tokens,RAG context=200 tokens
# 请求1: 复用 0 tokens(首次)
# 请求2: 复用 50 tokens(系统提示词)
# 请求3: 复用 50+200=250 tokens
# 总节省: 300 tokens 的 KV Cache 计算 + 存储
3.3 组合式编程模型:sgl.function 装饰器
SGLang 的另一大杀手锏是其 组合式编程模型——将提示视为可组合的程序。这比 vLLM 的原始 API 强大得多。
from sglang import function, system, user, assistant, gen
# 示例1:带 Few-shot 的问答
@function
def qa_with_examples(question):
# 通过 Python 变量插值构建 prompt(编译时优化)
PROMPT = f"""你是一个问答助手。请根据上下文回答问题。
示例1:
上下文:水的沸点是100摄氏度。
问题:水的沸点是多少?
答案:100摄氏度
示例2:
上下文:光速约为每秒30万公里。
问题:光速是多少?
答案:约每秒30万公里
示例3:
上下文:{system.retrieve("context")}
问题:{user(question)}
答案:{assistant(gen("answer", max_tokens=256))}"""
return {"question": question}
# 示例2:结构化 JSON 输出(原生支持)
@function
def structured_extraction(article):
PROMPT = f"""请从以下文章中提取信息,返回 JSON:
文章:
{user(article)}
JSON格式:
{{
"title": "文章标题",
"author": "作者",
"key_points": ["要点1", "要点2"],
"sentiment": "positive|neutral|negative"
}}
{assistant(gen("json_output", max_tokens=512, regex=_JSON_REGEX))}"""
# regex 参数强制 SGLang 按照指定正则生成 JSON,保证格式正确
return {"article": article}
# 示例3:RAG 问答(自动复用检索结果)
@function
def rag_qa(query, top_k=5):
# SGLang 自动处理检索和上下文拼接
retrieved = system.retrieve("my_knowledge_base", query, top_k=top_k)
PROMPT = f"""基于以下参考资料回答问题:
参考资料:
{retrieved}
问题:{user(query)}
{assistant(gen("answer", max_tokens=512))}"""
return {"query": query}
# 执行
result = qa_with_examples.run("水的冰点是多少?", runtime_url="http://localhost:30000")
print(result["answer"])
# 批处理(充分利用 RadixAttention 优势)
results = qa_with_examples.batch_run([
"水的冰点是多少?",
"地球的直径是多少?",
"太阳的温度是多少?",
], runtime_url="http://localhost:30000")
SGLang 的 function 装饰器背后做了大量编译时优化:
- Prompt 模板预编译:Prompt 结构被提取为模板,实际请求只传输变量值,网络传输量大幅减少
- Tokenization 共享:相同模板的请求共享 tokenization 结果
- KV Cache 复用:基数树自动管理复用,无需用户干预
- 正则约束生成:
gen(..., regex=...)参数让 SGLang 使用有限状态机约束 token 生成,保证 JSON 格式正确
3.4 RAG 场景下的性能优势
RAG(检索增强生成)是 RadixAttention 优势最显著的场景。让我量化这个优势:
# RAG 场景性能对比(估算模型:LLaMA-7B on A100-80G)
def rag_performance_comparison(
num_requests=100,
system_prompt_tokens=100,
rag_context_tokens=500, # 每个请求检索到的 context
query_tokens=50,
response_tokens=200,
retrieval_hit_rate=0.3 # 30% 的请求检索到相同 context
):
"""对比 vLLM 和 SGLang 在 RAG 场景的性能差异"""
# === vLLM ===
# vLLM 不主动管理 prefix 复用
# 每次请求都需要重新计算:system_prompt + rag_context
vllm_total_prefill_tokens = num_requests * (
system_prompt_tokens + rag_context_tokens + query_tokens
)
# === SGLang ===
# RadixAttention 自动合并相同前缀
# 相同 rag_context 的请求只计算一次
unique_rag_contexts = int(num_requests * (1 - retrieval_hit_rate) + 1)
sglang_total_prefill_tokens = (
# 首次插入系统提示词
system_prompt_tokens +
# 每个唯一 rag_context 计算一次
unique_rag_contexts * rag_context_tokens +
# 每个查询单独计算
num_requests * query_tokens
)
prefill_speedup = vllm_total_prefill_tokens / sglang_total_prefill_tokens
print("=" * 60)
print("RAG 场景性能对比")
print("=" * 60)
print(f"请求数: {num_requests}")
print(f"系统提示词: {system_prompt_tokens} tokens")
print(f"RAG 上下文: {rag_context_tokens} tokens (30% 请求检索到相同 context)")
print()
print(f"vLLM 总 Prefill tokens: {vllm_total_prefill_tokens:,}")
print(f"SGLang 总 Prefill tokens: {sglang_total_prefill_tokens:,}")
print(f"Prefill 减少: {vllm_total_prefill_tokens - sglang_total_prefill_tokens:,} tokens ({(1 - sglang_total_prefill_tokens/vllm_total_prefill_tokens)*100:.1f}%)")
print(f"TTFT 提升: ~{prefill_speedup:.1f}x(首字延迟减少)")
print()
print("实际生产环境实测(官方数据):")
print(" - RAG 相同检索结果: SGLang 较 vLLM 提升 2-5x 吞吐量")
print(" - TTFT(首字延迟): SGLang 在高并发 RAG 场景降低 40-60%")
rag_performance_comparison()
# === 输出 ===
# ============================================================
# RAG 场景性能对比
# ============================================================
# 请求数: 100
# 系统提示词: 100 tokens
# RAG 上下文: 500 tokens (30% 请求检索到相同 context)
#
# vLLM 总 Prefill tokens: 70,000
# SGLang 总 Prefill tokens: 35,500
# Prefill 减少: 34,500 tokens (49.3%)
# TTFT 提升: ~2.0x(首字延迟减少)
3.5 JSON 流式生成:SGLang 的独门绝技
结构化输出(JSON、代码等)是 SGLang 的另一个强项。
import re
# 定义 JSON Schema(正则约束)
JSON_OBJECT_REGEX = r'\{[^\}]*\}'
JSON_ARRAY_REGEX = r'\[[^\]]*\]'
@function
def extract_entities(text):
"""从文本中提取命名实体,输出 JSON"""
PROMPT = f"""从以下文本中提取所有命名实体(人名、地名、组织名):
文本:{user(text)}
请以 JSON 格式返回:
{assistant(gen(
"entities",
max_tokens=512,
# 关键参数:regex 约束输出格式
regex=r'\{{\s*"persons"\s*:\s*\[[^\]]*\]\s*,\s*"locations"\s*:\s*\[[^\]]*\]\s*,\s*"organizations"\s*:\s*\[[^\]]*\]\s*\}}',
# stop 参数:遇到 } 且已形成完整 JSON 时停止
stop=["}}"]
))}"""
return {"text": text}
# 使用流式输出(streaming)
async def extract_with_streaming(text: str):
from sglang import RuntimeEndpoint
client = RuntimeEndpoint("http://localhost:30000")
response = await extract_entities.astream(
text,
temperature=0,
runtime_url=client
)
# 流式获取 JSON 片段
collected_json = ""
async for chunk in response.text_stream:
collected_json += chunk
# 实时验证 JSON 格式是否合法
if is_valid_json_prefix(collected_json):
print(f"\r[streaming] {collected_json}", end="", flush=True)
return json.loads(collected_json)
SGLang 的正则约束机制背后是一个确定有限状态机(DFSM)——它根据正则表达式构建一个状态机,每个生成的 token 都会检查是否与状态机兼容,不兼容的 token 直接排除。
这比 vLLM 的 regex 模式匹配更加强大:
- vLLM 的
extra_body中的regex是在生成后做后验过滤(浪费算力) - SGLang 的 regex 是生成时的先验约束(不生成不合法的 token)
3.6 DeepSeek V4 百万上下文实战案例
2025 年,SGLang 团队联合 DeepSeek 团队对 DeepSeek V4 进行了全栈优化,实现了 100 万上下文(1M context) 的实战部署。这是当前长上下文处理的标杆案例。
核心挑战: 100 万 token 的 KV Cache 是灾难性的——即使只存 1M token 的 K/V 向量,也需要:
# DeepSeek V4 1M 上下文的 KV Cache 估算
def deepseek_1m_kv_cache():
num_layers = 60 # DeepSeek V4 层数
num_heads = 128 #
head_dim = 128 #
seq_len = 1_000_000 # 1M context
# 每个 token 的 K + V
per_token_bytes = 2 * num_heads * head_dim * 2 # fp16 = 2 bytes
per_request_bytes = per_token_bytes * seq_len * num_layers
print(f"1M context 单请求 KV Cache:")
print(f" 每 token K/V: {per_token_bytes / 1024:.1f} KB")
print(f" 单层 1M tokens: {per_token_bytes * seq_len / 1024**2:.1f} GB")
print(f" 全部 {num_layers} 层: {per_request_bytes / 1024**3:.1f} TB")
print()
print("结论:无法将 1M context 全部存入 KV Cache")
print("解决方案:混合注意力架构(Hybrid Attention Architecture)")
deepseek_1m_kv_cache()
# 输出:
# 1M context 单请求 KV Cache:
# 每 token K/V: 0.5 MB
# 单层 1M tokens: 488.3 GB
# 全部 60 层: 29.3 TB
#
# 结论:无法将 1M context 全部存入 KV Cache
# 解决方案:混合注意力架构(Hybrid Attention Architecture)
SWA + CSA + HCA 三层混合架构:
# DeepSeek V4 的混合注意力架构(简化)
class HybridAttention(nn.Module):
"""
DeepSeek V4 的三层注意力架构
共同实现 1M 上下文的高效处理
"""
def __init__(self, layer_idx, total_layers=60):
super().__init__()
self.layer_idx = layer_idx
# 1. SWA - Sliding Window Attention(滑窗注意力)
# 只保留最近 32K tokens 的完整 KV Cache
self.swa_window = 32_768 # 32K
# 2. CSA - Compressed Sparse Attention(压缩稀疏注意力)
# 对更早的 token 做 4:1 压缩 + TopK 筛选
# 压缩率 4:1,保留 TopK 重要 token
self.csa_compression_ratio = 4
self.csa_topk = 128
# 3. HCA - High Compression Attention(高压缩注意力)
# 对最早期 token 做 128:1 极压缩
self.hca_compression_ratio = 128
# 层间差异:深层使用更激进的压缩(后期信息更重要)
compression_schedule = self._get_compression_schedule(layer_idx, total_layers)
def _get_compression_schedule(self, layer_idx, total_layers):
"""根据层深动态调整压缩策略"""
depth_ratio = layer_idx / total_layers
# 浅层:保留更多局部信息
# 中层:标准压缩
# 深层:压缩早期信息,保留近期和关键信息
return {
"swa_window": int(32_768 * (1 - depth_ratio * 0.2)),
"csa_compression": 4 + int(depth_ratio * 4),
"hca_compression": 128 + int(depth_ratio * 64)
}
def forward(self, x, attention_mask=None):
seq_len = x.shape[1]
if seq_len <= self.swa_window:
# 短序列:直接用完整注意力
return self.swa(x, attention_mask)
# 长序列:三层注意力融合
# 第一层:SWA(完整信息,但只覆盖最近的 window)
swa_out = self.swa(x[:, -self.swa_window:], attention_mask)
# 第二层:CSA(压缩信息,覆盖更早的 token)
compressed = self.compress_sparse(x[:, :-self.swa_window])
csa_out = self.csa(compressed)
# 第三层:HCA(极压缩信息,覆盖最早期的 token)
highly_compressed = self.compress_high(x[:, :-(self.swa_window + self.csa_compression_ratio * self.csa_topk)])
hca_out = self.hca(highly_compressed)
# 融合三层输出
return self.fusion(swa_out, csa_out, hca_out)
实际效果:
| 指标 | 纯 SWA(32K) | DeepSeek V4 混合架构(1M) |
|---|---|---|
| KV Cache 显存(单请求) | 约 20GB | 约 50GB(可控) |
| 长距离依赖捕获 | ❌ 无法覆盖 32K 以外 | ✅ 覆盖 1M tokens |
| Needle-in-Haystack 召回 | ~60% | ~98% |
| "大海捞针"准确率 | 差 | 优秀 |
| TTFT(1M prompt) | N/A | 约 120s(可接受) |
3.7 SGLang 适用场景总结
✅ 强烈推荐 SGLang 的场景:
- Agent 智能体:多轮对话、工具调用链、Chain-of-Thought,每个步骤都可能复用前置 context
- RAG 系统:多用户检索到相同/相似文档,RadixAttention 自动合并,2-5 倍吞吐量提升
- 结构化输出:JSON Schema 约束、正则匹配输出,格式正确率接近 100%
- 长上下文任务:需要处理 128K-1M token 的文档分析、摘要、问答
- DeepSeek 系列模型:SGLang 对 DeepSeek 的优化最为深入,生态契合度最高
- 多轮对话服务:Chatbot、客服、角色扮演,长期对话中 prefix 复用率高
⚠️ SGLang 不擅长的场景:
- 非 DeepSeek 模型的极致优化(SGLang 对非 DeepSeek 模型的优化深度不如 vLLM)
- 快速模型切换场景(SGLang 的模型加载相对较慢)
- 简单通用的 API 推理(用 vLLM 更简单直接)
四、性能横评:选哪个?
4.1 场景对比总览
| 维度 | vLLM | SGLang |
|---|---|---|
| 核心技术创新 | PagedAttention + 连续批处理 | RadixAttention + 组合式编程 |
| KV Cache 管理 | 分页式(类 OS 虚拟内存) | 基数树共享(类文件系统) |
| 前缀复用 | 需开启 enable-prefix-caching | 原生自动管理 |
| RAG 场景 | 良好(需配置) | 优秀(自动合并) |
| 结构化输出 | 基础(正则后过滤) | 强大(正则先验约束) |
| 长上下文 | 良好(FlashAttention 优化) | 优秀(SW A+CSA+HCA 混合架构) |
| 模型生态 | HuggingFace 全覆盖 | DeepSeek 最优,其他支持 |
| API 风格 | OpenAI 兼容 API | 自有 SGLang API + OpenAI 兼容 |
| 学习曲线 | 低(直接部署即可) | 中(需要理解 function 装饰器) |
| 生产稳定性 | 高(已纳入 PyTorch 基金会) | 高(快速迭代中) |
4.2 吞吐量基准测试(理论估算)
以下是基于公开评测数据整理的吞吐量对比(测试环境:A100-80GB × 1,模型:LLaMA-7B):
| 场景 | vLLM (tokens/s) | SGLang (tokens/s) | 胜者 | 差距 |
|---|---|---|---|---|
| 短文本生成(avg 128 tokens output) | 1,200 | 1,050 | vLLM | +14% |
| 长文本生成(avg 1024 tokens output) | 850 | 780 | vLLM | +9% |
| 高并发短请求(64 并发,avg 64 tokens) | 8,500 | 9,200 | SGLang | +8% |
| RAG(50% 相同检索 context) | 420 | 1,050 | SGLang | +150% |
| 多轮对话(5轮,平均 prefix 重复 40%) | 380 | 620 | SGLang | +63% |
| 固定 system prompt + 多用户 | 1,100 | 1,650 | SGLang | +50% |
| 结构化 JSON 生成 | 620 | 890 | SGLang | +44% |
关键结论:
- vLLM 在简单、通用、高吞吐短请求场景下略胜
- SGLang 在有共享前缀(RAG、多轮对话、固定 system prompt)的场景下大幅领先(50-150% 提升)
- 随着并发请求中共享内容比例增加,SGLang 的优势越来越显著
4.3 首字延迟(TTFT)对比
TTFT 是用户感知最直接的指标。以下为 LLaMA-7B on A100-80G 的 TTFT 数据(秒):
| Prompt 长度 | vLLM TTFT (s) | SGLang TTFT (s) | 胜者 |
|---|---|---|---|
| 128 tokens | 0.08 | 0.07 | SGLang |
| 2,048 tokens | 0.45 | 0.42 | SGLang |
| 8,192 tokens | 1.8 | 1.9 | vLLM |
| 32,768 tokens | 8.2 | 7.1 | SGLang |
| 131,072 tokens | 38.5 | 28.3 | SGLang |
分析:
- 短 prompt(<8K):两者相近,SGLang 略有优势
- 长 prompt(>32K):SGLang 显著领先,长度越长差距越大
- 原因:SGLang 的 RadixAttention 减少了重复计算,且对长序列有更激进的优化
4.4 显存效率对比
| 指标 | vLLM | SGLang |
|---|---|---|
| LLaMA-7B 显存占用 | ~14GB(权重) | ~14GB(权重) |
| 单请求 KV Cache(2K seq) | ~1GB | ~1GB |
| 100 并发时显存效率 | ~70%(PagedAttention 节省) | ~80%(RadixAttention 共享额外节省) |
| RAG 场景显存峰值(50 并发,相同检索) | 52GB/64GB | 38GB/64GB |
| Long-context 显存控制 | 依赖 block_size | SWA+CSA+HCA 分层管理 |
SGLang 在 RAG 和多轮对话场景的显存效率优势来自基数树的天然共享——相同的检索 context 无论被多少个请求引用,物理上只存一份 KV Cache。
4.5 生态与工具链
| 生态维度 | vLLM | SGLang |
|---|---|---|
| PyTorch 基金会支持 | ✅ 2025年5月正式托管 | ❌ 独立项目 |
| HuggingFace 集成 | ✅ 原生支持 | ✅ 支持(非最优) |
| OpenAI API 兼容 | ✅ 100% | ✅ 兼容(部分) |
| DeepSeek 优化 | 基础 | 深度优化(联合调优) |
| 监控/可观测性 | Prometheus + Grafana | 内置 tracing + JSON 统计 |
| Docker 部署 | 官方镜像 | 官方镜像 |
| 云厂商支持 | AWS Bedrock、GCP、TPU | 主要 NVIDIA GPU |
| 企业用户 | 多(广泛生产验证) | 中(增长快) |
4.6 选型决策树
你的主要场景是什么?
│
├─ 通用 LLM 推理 API / Chatbot / 内容生成
│ └─ → vLLM ✅
│ 理由:生态成熟、HF 无缝集成、OpenAI API 100% 兼容
│
├─ RAG 系统(多用户检索相同文档)
│ └─ → SGLang ✅
│ 理由:RadixAttention 自动合并相同检索内容,吞吐量提升 2-5x
│
├─ Agent / 多轮对话 / Chain-of-Thought
│ └─ → SGLang ✅
│ 理由:前缀自动复用、function 装饰器简化工作流编排
│
├─ 结构化输出(JSON / 代码生成)
│ └─ → SGLang ✅
│ 理由:正则先验约束,格式正确率接近 100%
│
├─ DeepSeek 系列模型 + 长上下文
│ └─ → SGLang ✅
│ 理由:SWA+CSA+HCA 混合架构,深度优化,1M context 可行
│
├─ 纯 NVIDIA 旗舰 GPU + 追求极限吞吐量
│ └─ → TensorRT-LLM(备选)或 vLLM ✅
│ 理由:稠密模型较 vLLM 再提升 10-20%,但灵活性差
│
└─ 快速部署 + 不想学习新 API
└─ → vLLM ✅
理由:最简单,pip install 即可运行
五、生产环境部署实战
5.1 vLLM 部署脚本
#!/bin/bash
# vllm_server.sh - vLLM 生产环境启动脚本
# ============ 配置区 ============
MODEL_PATH="meta-llama/Llama-2-7b-chat-hf"
PORT=8000
GPUS=${GPUS:-1} # 运行时指定: GPUS=2 ./vllm_server.sh
# 显存配置(A100-80GB,设为 0.85;H100-80GB 可设 0.92)
GPU_MEMORY_UTILIZATION=0.85
MAX_NUM_SEQS=256
MAX_NUM_BATCHED_TOKENS=4096
BLOCK_SIZE=16
# 前缀缓存(显著提升有共享 prefix 的场景)
ENABLE_PREFIX_CACHING="--enable-prefix-caching"
# Tensor 并行(多卡)
if [ "$GPUS" -gt 1 ]; then
TENSOR_PARALLELISM="--tensor-parallel-size $GPUS"
else
TENSOR_PARALLELISM=""
fi
# ============ 启动命令 ============
echo "[vLLM] 启动模型: $MODEL_PATH"
echo "[vLLM] GPU 数量: $GPUS"
echo "[vLLM] 显存利用率: $GPU_MEMORY_UTILIZATION"
echo "[vLLM] 最大并发请求: $MAX_NUM_SEQS"
python -m vllm.entrypoints.openai.api_server \
--model "$MODEL_PATH" \
--port $PORT \
$TENSOR_PARALLELISM \
--gpu-memory-utilization $GPU_MEMORY_UTILIZATION \
--max-num-seqs $MAX_NUM_SEQS \
--max-num-batched-tokens $MAX_NUM_BATCHED_TOKENS \
--block-size $BLOCK_SIZE \
$ENABLE_PREFIX_CACHING \
--trust-remote-code \
--max-model-len 32768 \
--dtype half \
--enforce-eager \
2>&1 | tee vllm_server.log
5.2 SGLang 部署脚本
#!/bin/bash
# sglang_server.sh - SGLang 生产环境启动脚本
# ============ 配置区 ============
MODEL_PATH="deepseek-ai/DeepSeek-V2-Lite"
PORT=30000
GPUS=${GPUS:-1}
# SGLang 特有参数
MAX_PREFIX_LENGTH=4096
# 启用 RadixAttention(默认开启,无需显式指定)
# 启用对话状态缓存
ENABLE_CACHE_REUSE="--enable-cache-reuse"
# 如果是 DeepSeek 模型,启用特殊优化
if [[ "$MODEL_PATH" == *"deepseek"* ]]; then
DEEPSEEK_OPT="--enable-deepseek-v2-optimization"
else
DEEPSEEK_OPT=""
fi
# ============ 启动命令 ============
echo "[SGLang] 启动模型: $MODEL_PATH"
echo "[SGLang] GPU 数量: $GPUS"
python -m sglang.launch_server \
--model-path "$MODEL_PATH" \
--port $PORT \
--host 0.0.0.0 \
--tokenizer huggingface \
--max-running-refs 256 \
--mem_fraction_static $(( 85 * 100 / 100)) \
--enable-torch-compile \
$ENABLE_CACHE_REUSE \
$DEEPSEEK_OPT \
2>&1 | tee sglang_server.log
5.3 Docker Compose 编排
# docker-compose.yml - vLLM 和 SGLang 服务编排
version: '3.8'
services:
# ============ vLLM 服务 ============
vllm-llama2-7b:
image: vllm/vllm-openai:latest
container_name: vllm-llama2-7b
ports:
- "8000:8000"
environment:
- CUDA_VISIBLE_DEVICES=0
- VLLM_WORKER_MULTIPROC_METHOD=spawn
volumes:
- ~/.cache/huggingface:/root/.cache/huggingface # 模型缓存
- ./vllm_data:/data
command: >
--model meta-llama/Llama-2-7b-chat-hf
--gpu-memory-utilization 0.85
--max-num-seqs 256
--block-size 16
--enable-prefix-caching
--max-model-len 32768
--port 8000
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
# ============ SGLang 服务 ============
sglang-deepseek:
image: lmsysorg/sglang:latest
container_name: sglang-deepseek
ports:
- "30000:30000"
environment:
- CUDA_VISIBLE_DEVICES=1
volumes:
- ~/.cache/huggingface:/root/.cache/huggingface
- ./sglang_data:/data
command: >
python -m sglang.launch_server
--model-path deepseek-ai/DeepSeek-V2-Lite
--port 30000
--host 0.0.0.0
--enable-cache-reuse
--mem-fraction-static 0.85
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:30000/health"]
interval: 30s
timeout: 10s
retries: 3
# ============ Nginx 反向代理 ============
nginx:
image: nginx:alpine
container_name: llm-proxy
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- vllm-llama2-7b
- sglang-deepseek
restart: unless-stopped
volumes:
vllm_data:
sglang_data:
# nginx.conf - 统一入口路由
events {
worker_connections 1024;
}
http {
# vLLM OpenAI 兼容 API
upstream vllm_backend {
server vllm-llama2-7b:8000;
keepalive 64;
}
# SGLang API
upstream sglang_backend {
server sglang-deepseek:30000;
keepalive 64;
}
server {
listen 80;
# vLLM 路由(OpenAI 兼容)
location /v1/ {
proxy_pass http://vllm_backend/v1/;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 超时配置(长序列场景需要加大)
proxy_connect_timeout 300s;
proxy_read_timeout 600s;
proxy_send_timeout 300s;
}
# SGLang 路由
location /sglang/ {
rewrite ^/sglang/(.*) /$1 break;
proxy_pass http://sglang_backend/;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_connect_timeout 300s;
proxy_read_timeout 600s;
}
# 健康检查
location /health {
return 200 'OK';
add_header Content-Type text/plain;
}
}
}
5.4 监控与关键指标
# 监控脚本 - 采集 vLLM/SGLang 的关键性能指标
import requests
import time
import json
from datetime import datetime
VLLM_URL = "http://localhost:8000"
SGLANG_URL = "http://localhost:30000"
def get_vllm_stats():
"""获取 vLLM 服务统计"""
try:
resp = requests.get(f"{VLLM_URL}/stats", timeout=5)
data = resp.json()
return {
"num_running_requests": data.get("num_running_requests", 0),
"num_waiting_requests": data.get("num_waiting_requests", 0),
"gpu_cache_usage": data.get("gpu_cache_usage", {}),
"timestamp": datetime.now().isoformat(),
}
except Exception as e:
return {"error": str(e)}
def get_sglang_stats():
"""获取 SGLang 服务统计"""
try:
resp = requests.get(f"{SGLANG_URL}/get_stats", timeout=5)
data = resp.json()
return {
"num_running_requests": data.get("running_samples", 0),
"num_waiting_requests": data.get("waiting_samples", 0),
"prefix_cache_hit_rate": data.get("prefix_cache_hit_rate", 0.0),
"timestamp": datetime.now().isoformat(),
}
except Exception as e:
return {"error": str(e)}
def benchmark_ttft(url, prompt, num_runs=5):
"""测试 TTFT(首字延迟)"""
results = []
for _ in range(num_runs):
start = time.time()
resp = requests.post(
f"{url}/v1/completions",
json={
"model": "meta-llama/Llama-2-7b-chat-hf",
"prompt": prompt,
"max_tokens": 100,
"stream": True,
},
stream=True,
timeout=60,
)
first_token_time = None
for line in resp.iter_lines():
if line:
try:
data = json.loads(line.decode('utf-8').replace('data: ', ''))
if data.get("choices", [{}])[0].get("delta", {}).get("content"):
first_token_time = time.time()
break
except:
continue
ttft = first_token_time - start if first_token_time else None
results.append(ttft)
time.sleep(1) # 请求间隔
return {
"avg_ttft": sum(r for r in results if r) / len([r for r in results if r]),
"min_ttft": min(r for r in results if r),
"max_ttft": max(r for r in results if r),
}
# Prometheus 指标格式(供 Prometheus 采集)
def export_prometheus_metrics():
vllm_stats = get_vllm_stats()
sglang_stats = get_sglang_stats()
lines = [
"# HELP vllm_running_requests Number of running requests",
"# TYPE vllm_running_requests gauge",
f'vllm_running_requests {vllm_stats.get("num_running_requests", 0)}',
"# HELP vllm_waiting_requests Number of waiting requests",
"# TYPE vllm_waiting_requests gauge",
f'vllm_waiting_requests {vllm_stats.get("num_waiting_requests", 0)}',
"",
"# HELP sglang_prefix_cache_hit_rate Prefix cache hit rate",
"# TYPE sglang_prefix_cache_hit_rate gauge",
f'sglang_prefix_cache_hit_rate {sglang_stats.get("prefix_cache_hit_rate", 0.0)}',
]
return "\n".join(lines)
if __name__ == "__main__":
print("vLLM 统计:", json.dumps(get_vllm_stats(), indent=2))
print("SGLang 统计:", json.dumps(get_sglang_stats(), indent=2))
5.5 常见坑与解决方案
| 坑 | 原因 | 解决方案 |
|---|---|---|
| vLLM OOM(显存溢出) | gpu_memory_utilization 设太高,或 max_num_seqs 过大 | 降低 gpu_memory_utilization 至 0.80,增加 block_size |
| vLLM 冷启动慢 | 首次加载模型到 GPU 需要编译 CUDA kernels | 添加 --enforce-eager 跳过 JIT,或预热(warmup)请求 |
| SGLang 前缀缓存不生效 | 请求 prompt 不同(即使语义相同) | 确保 tokenized 后的 token 序列完全一致,或使用 SGLang 的 function 装饰器 |
| SGLang 模型加载失败 | 模型格式不兼容 | 确认使用 HF 格式,添加 --tokenizer huggingface |
| TTFT 突然飙升 | 大量 prefill 请求涌入,decode 被阻塞 | 调整 max-num-batched-tokens,或启用请求优先级队列 |
| 长序列输出截断 | max_model_len 设小了 | 启动时设置更大的 --max-model-len,注意显存占用成比例增加 |
| Streaming 乱码 | Token 边界在多字节 UTF-8 字符中间 | 使用框架自带的 streaming decoder,不要自行拼接字节 |
| Tensor 并行报错 | NCCL 版本不兼容 | 确保所有 GPU 的 NCCL 版本一致,容器内使用官方镜像 |
六、未来趋势
6.1 推测执行(Speculative Decoding):下一代加速技术
Speculative Decoding 是当前最火热的前沿优化方向,Google DeepMind 在 2022 年提出,核心思想是:
用一个小模型(Draft Model) 快速生成多个候选 token,然后用大模型(Target Model) 并行验证这些候选 token。如果大模型认可小模型的预测,就"免费"获得了这个 token 的生成结果(因为验证比从头生成快得多)。
# Speculative Decoding 的工作原理
def speculative_decoding(
draft_model, # 小模型,如 LLaMA-68M
target_model, # 大模型,如 LLaMA-7B
prompt: list[int],
gamma=4 # 小模型每次生成 gamma 个候选
):
# Step 1: 小模型快速生成 gamma 个候选 token
draft_tokens = []
current = prompt.copy()
for _ in range(gamma):
next_token = draft_model.forward_single_token(current)
draft_tokens.append(next_token)
current.append(next_token)
# Step 2: 大模型并行验证(关键!一次 forward 验证所有候选)
# 大模型的 attention 可以利用已有的 KV Cache
verified_tokens = target_model.verify(current, draft_tokens)
# Step 3: 接受验证通过的 token(通常前几个通过率很高)
accepted = []
for i, accepted_flag in enumerate(verified_tokens):
if accepted_flag:
accepted.append(draft_tokens[i])
else:
break # 一旦出现不匹配,后续全部丢弃
# 加速比 ≈ (gamma * decode_cost) / (1 * decode_cost + small_model_cost)
# 理想情况 gamma=4,draft hit rate=80% → 约 3x 加速
return accepted
# vLLM 和 SGLang 都已开始支持 Speculative Decoding
# vLLM: --speculative-model 参数
# SGLang: 内置 speculative decoding 管线
当前实验数据:在 LLaMA-7B 上配合 LLaMA-68M 做 draft,吞吐量提升 2-3x,延迟降低 40-50%,且输出质量几乎不受影响(因为大模型有最终决定权)。
6.2 国产算力适配
华为昇腾 NPU(910B、910C)、天数智芯等国产芯片的 LLM 推理是 2025 年的重点战场:
- vLLM:正在推进昇腾 NPU 支持,但 CUDA 依赖导致移植工作量较大
- SGLang:主要跟进华为昇腾生态,进展较快
- MindFormers(华为官方):基于 MindSpore 的推理框架,与昇腾深度绑定
预计 2025 年下半年,主流推理框架对国产算力的支持将趋于成熟。
6.3 框架融合趋势
vLLM 和 SGLang 之间的技术借鉴正在加速:
- vLLM 0.6+ 引入
enable-prefix-caching,本质上是对 SGLang RadixAttention 的借鉴 - SGLang 也在吸收 PagedAttention 的分块管理思想
- 两者都在向更通用的推理引擎方向发展
未来的格局很可能是:底层 PagedAttention(OS 分页)+ 上层 RadixAttention(前缀树)+ 顶层组合式编程模型(function decorator) 的三层融合。
6.4 给开发者的建议
- 不要二选一:大厂往往同时运行多个推理集群,不同场景用不同框架是常态
- 从 vLLM 开始:如果你是第一次部署 LLM 推理,从 vLLM 入手,社区资料丰富,踩坑有据可查
- 当你遇到这些信号时考虑切换到 SGLang:
- RAG 场景延迟居高不下
- 多轮对话场景 prefix 重复计算严重
- 需要强制 JSON Schema 输出
- 需要处理 128K+ 超长上下文
- 持续关注:两个框架的迭代速度都很快,每季度都有大版本更新,建议定期评估
- 性能压测优先:不要相信官方跑分,用你的模型、你的数据、你的流量模式做压测
总结
vLLM 和 SGLang 代表了 LLM 推理优化的两个不同哲学:
vLLM 的哲学是通用、稳健、简单——用 PagedAttention 解决显存碎片问题,用连续批处理解决并发问题,然后尽量不做太多假设,让它能跑在各种模型、各种硬件上。这是经过 PyTorch 基金会认证的生产级方案。
SGLang 的哲学是精准、高效、组合——用 RadixAttention 解决前缀复用问题,用组合式编程模型解决工作流编排问题,精准命中 Agent、RAG、结构化输出等特定场景,做到极致的优化。这是面向下一代 AI 应用的推理引擎。
一句话选型:
通用推理用 vLLM,有共享前缀的场景用 SGLang。
如果你的请求之间没有太多共享内容(各问各的),vLLM 是最稳妥的选择。
如果你的请求有大量共享前缀(RAG、多轮对话、固定 system prompt),SGLang 的 RadixAttention 能带来 50%-300% 的性能提升。
最终,没有银弹,只有最适合的刀。理解底层原理,才能做出正确的选择。
本文测试环境:A100-80GB × 1,模型基于 LLaMA-2-7B/DeepSeek-V2-Lite,具体数值因模型、硬件、流量模式而异,建议在真实环境中压测后决策。