编程 vLLM vs SGLang 深度横评:当 PagedAttention 遇上 RadixAttention——LLM 推理框架选型完全指南

2026-08-18 11:46:32 +0800 CST views 5

vLLM vs SGLang 深度横评:当 PagedAttention 遇上 RadixAttention——LLM 推理框架选型完全指南

引言

如果你做过 LLM 推理服务化,一定会遇到这两个灵魂拷问:

  1. 显存不够用——一个 LLaMA-7B 单次请求的 KV Cache 就占用约 16GB 显存,8 卡 A100 都撑不住几个并发;
  2. 长上下文跑不动——128K token 的文档摘要请求进来,显存直接爆掉,TTFT(Time To First Token,首字延迟)飙到几十秒。

传统的 HuggingFace pipeline 方案在这两个问题上几乎束手无策:KV Cache 按完整序列预分配,不管用不用都占着显存;不同长度的请求无法合并,GPU 空转等待;prefix(系统提示词)每次都重新计算……

正是在这个背景下,vLLMSGLang 这两个推理框架崛起为业界标杆。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(内存带宽密集型)任务——真正卡住的是显存带宽,而不是算力。

这两个阶段的特性差异,直接导致了不同的优化策略:

阶段瓶颈类型核心优化方向
PrefillCompute-Bound张量并行、算子融合、FlashAttention
DecodeMemory-BoundKV 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 的循环),调度器会:

  1. 检查已完成 Decode 的请求 → 从批中移除,释放显存
  2. 检查新到达的 Prefill 请求 → 插入批中,立即处理
  3. 继续处理仍在 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 装饰器背后做了大量编译时优化:

  1. Prompt 模板预编译:Prompt 结构被提取为模板,实际请求只传输变量值,网络传输量大幅减少
  2. Tokenization 共享:相同模板的请求共享 tokenization 结果
  3. KV Cache 复用:基数树自动管理复用,无需用户干预
  4. 正则约束生成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 场景对比总览

维度vLLMSGLang
核心技术创新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,2001,050vLLM+14%
长文本生成(avg 1024 tokens output)850780vLLM+9%
高并发短请求(64 并发,avg 64 tokens)8,5009,200SGLang+8%
RAG(50% 相同检索 context)4201,050SGLang+150%
多轮对话(5轮,平均 prefix 重复 40%)380620SGLang+63%
固定 system prompt + 多用户1,1001,650SGLang+50%
结构化 JSON 生成620890SGLang+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 tokens0.080.07SGLang
2,048 tokens0.450.42SGLang
8,192 tokens1.81.9vLLM
32,768 tokens8.27.1SGLang
131,072 tokens38.528.3SGLang

分析:

  • 短 prompt(<8K):两者相近,SGLang 略有优势
  • 长 prompt(>32K):SGLang 显著领先,长度越长差距越大
  • 原因:SGLang 的 RadixAttention 减少了重复计算,且对长序列有更激进的优化

4.4 显存效率对比

指标vLLMSGLang
LLaMA-7B 显存占用~14GB(权重)~14GB(权重)
单请求 KV Cache(2K seq)~1GB~1GB
100 并发时显存效率~70%(PagedAttention 节省)~80%(RadixAttention 共享额外节省)
RAG 场景显存峰值(50 并发,相同检索)52GB/64GB38GB/64GB
Long-context 显存控制依赖 block_sizeSWA+CSA+HCA 分层管理

SGLang 在 RAG 和多轮对话场景的显存效率优势来自基数树的天然共享——相同的检索 context 无论被多少个请求引用,物理上只存一份 KV Cache。

4.5 生态与工具链

生态维度vLLMSGLang
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 给开发者的建议

  1. 不要二选一:大厂往往同时运行多个推理集群,不同场景用不同框架是常态
  2. 从 vLLM 开始:如果你是第一次部署 LLM 推理,从 vLLM 入手,社区资料丰富,踩坑有据可查
  3. 当你遇到这些信号时考虑切换到 SGLang
    • RAG 场景延迟居高不下
    • 多轮对话场景 prefix 重复计算严重
    • 需要强制 JSON Schema 输出
    • 需要处理 128K+ 超长上下文
  4. 持续关注:两个框架的迭代速度都很快,每季度都有大版本更新,建议定期评估
  5. 性能压测优先:不要相信官方跑分,用你的模型、你的数据、你的流量模式做压测

总结

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,具体数值因模型、硬件、流量模式而异,建议在真实环境中压测后决策。

推荐文章

JavaScript中的常用浏览器API
2024-11-18 23:23:16 +0800 CST
Elasticsearch 监控和警报
2024-11-19 10:02:29 +0800 CST
全栈工程师的技术栈
2024-11-19 10:13:20 +0800 CST
Golang 随机公平库 satmihir/fair
2024-11-19 03:28:37 +0800 CST
程序员茄子在线接单