揭秘大模型黑科技:为什么 vLLM 跑得又快又省显存?靠的就是这俩神器(PagedAttention、Continuous Batching)
vLLM 通过两大核心技术 PagedAttention 和 Continuous Batching 实现了高效推理。PagedAttention 借鉴操作系统分页机制,将 KV Cache 切分为小块动态管理,大幅提升显存利用率;Continuous Batching 则实现动态请求调度,让 GPU 保持满载状态。二者协同工作,既解决了显存浪费问题,又优化了计算吞吐量,使大模型推理效率显著提升。
揭秘大模型黑科技:为什么 vLLM 跑得又快又省显存?靠的就是这俩神器
目录
-
- 1.1 KV Cache 的显存瓶颈
- 1.2 传统推理的计算浪费
- 1.3 核心问题总结
-
- 2.1 传统 KV Cache 管理的痛点
- 2.2 PagedAttention 的设计思想
- 2.3 具体实现原理
- 2.4 显存优化效果分析
-
- 3.1 静态批处理的局限性
- 3.2 Continuous Batching 的工作机制
- 3.3 调度算法实现
- 3.4 吞吐量提升分析
-
- 4.1 协同工作原理
- 4.2 性能优化案例
- 4.3 实际部署经验
1. 为什么要搞这些骚操作?
大语言模型(LLM)推理为什么慢?为什么显存总是不够用?别急,今天带你揭秘 vLLM 的两大王牌技术, PagedAttention 和 Continuous Batching。它们一个管显存,一个管算力,搭配起来简直无敌!
1.1 KV Cache 的显存瓶颈
在大模型推理时,最大瓶颈之一就是 KV Cache(注意力的 Key-Value 缓存)。
让我们先理解一下 Transformer 的 Self-Attention 机制:
import torch
import torch.nn.functional as F
def self_attention(Q, K, V):
"""
标准的 Self-Attention 计算
Q, K, V: (batch_size, seq_len, d_model)
"""
d_k = Q.size(-1)
# 计算注意力分数: Q @ K^T / sqrt(d_k)
scores = torch.matmul(Q, K.transpose(-2, -1)) / torch.sqrt(torch.tensor(d_k, dtype=torch.float32))
# Softmax 归一化
attention_weights = F.softmax(scores, dim=-1)
# 加权求和得到输出
output = torch.matmul(attention_weights, V)
return output, attention_weights
注意力计算的数学公式为:
Attention ( Q , K , V ) = softmax ( Q K T d k ) V \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V Attention(Q,K,V)=softmax(dkQKT)V
问题来了:在自回归生成时,每生成一个新 token,都需要:
- 访问前面所有 token 的 K 和 V,不能丢弃
- 上下文长度为 L L L,每个 token 的 K、V 维度为 d d d,层数为 N N N
- 则 KV Cache 显存占用为: 2 × N × L × d × precision 2 \times N \times L \times d \times \text{precision} 2×N×L×d×precision
举个例子:
- 模型:LLaMA-13B(40 层,hidden_dim=5120)
- 序列长度:2048
- 精度:FP16(2 bytes)
- KV Cache 显存 = 2 × 40 × 2048 × 5120 × 2 2 \times 40 \times 2048 \times 5120 \times 2 2×40×2048×5120×2 ≈ 1.6GB(仅一个序列!)
# KV Cache 显存计算示例
def calculate_kv_cache_memory(num_layers, seq_len, hidden_dim, precision_bytes=2):
"""
计算 KV Cache 的显存占用
Args:
num_layers: Transformer 层数
seq_len: 序列长度
hidden_dim: 隐藏层维度
precision_bytes: 数值精度字节数(FP16=2, FP32=4)
Returns:
显存占用(GB)
"""
# KV 两个矩阵,所以乘以 2
memory_bytes = 2 * num_layers * seq_len * hidden_dim * precision_bytes
memory_gb = memory_bytes / (1024 ** 3)
return memory_gb
# LLaMA-13B 示例
llama_13b_kv_memory = calculate_kv_cache_memory(
num_layers=40,
seq_len=2048,
hidden_dim=5120
)
print(f"LLaMA-13B 单序列 KV Cache: {llama_13b_kv_memory:.2f} GB")
# 如果 batch_size=32
print(f"Batch=32 时 KV Cache: {llama_13b_kv_memory * 32:.2f} GB")
1.2 传统推理的计算浪费
传统的推理框架存在严重的资源浪费问题:
显存层面:
- 预分配大块连续内存:为了支持最大序列长度,提前分配固定大小的连续显存
- Padding 浪费:短序列也占用完整的显存空间
- 碎片化严重:请求结束后,释放的内存块可能无法被有效利用
- 低利用率:实际使用率常常只有 20%-40%
计算层面:
- 有的请求 prompt 长,有的短
- 有的正在生成,有的刚开始
- GPU 一会儿闲着,一会儿又爆满
- 静态批处理导致新请求必须等待
# 传统静态批处理的伪代码
class TraditionalBatchInference:
def __init__(self, max_batch_size=8, max_seq_len=2048):
self.max_batch_size = max_batch_size
self.max_seq_len = max_seq_len
# 预分配固定大小的 KV Cache(严重浪费!)
self.kv_cache = self.allocate_kv_cache(max_batch_size, max_seq_len)
def process_batch(self, requests):
"""
静态批处理:必须等凑够一批才能开始
"""
batch = []
# 等待凑够 max_batch_size 个请求
while len(batch) < self.max_batch_size:
if requests:
batch.append(requests.pop(0))
else:
time.sleep(0.01) # 空转等待,GPU 闲置!
# 所有请求一起跑,必须等最长的那个完成
results = self.inference(batch)
return results
1.3 核心问题总结
问题总结:
- 显存浪费:预分配、padding、碎片化
- GPU 不满载:静态批处理,空闲时间长
- 响应延迟高:新请求必须排队等待
这就是 PagedAttention + Continuous Batching 要解决的核心痛点。
2. PagedAttention:显存的分页大师
2.1 传统 KV Cache 管理的痛点
在传统实现中,KV Cache 的管理类似于"买房":
请求 A(实际需要 1000 tokens):分配 2048 空间 → 浪费 1048
请求 B(实际需要 500 tokens): 分配 2048 空间 → 浪费 1548
请求 C(实际需要 1800 tokens):分配 2048 空间 → 浪费 248
总利用率 = (1000+500+1800)/(2048*3) ≈ 53.8%
更糟糕的是,当请求结束后,释放的大块内存可能无法容纳新的长请求,导致外部碎片化。
2.2 PagedAttention 的设计思想
PagedAttention 的灵感来自操作系统的虚拟内存分页机制。它的核心思路是:
- 分页存储:把 KV Cache 切成一个个小块(page),而不是大段连续内存
- 页表管理:像内存管理一样,记录每个请求的 KV page 分布
- 动态分配/释放:谁需要分配就给 page,用完立即回收
- 非连续存储:逻辑上连续,物理上可以分散
传统方式(连续内存):
请求 A: [████████████████████████████] 2048 slots
PagedAttention(分页存储):
请求 A: [Page0:64] → [Page5:64] → [Page12:64] → ... → [Page30:64]
物理上分散,逻辑上连续
2.3 具体实现原理
数据结构设计:
import numpy as np
from typing import List, Dict
class PagedKVCache:
"""
基于分页的 KV Cache 管理器
"""
def __init__(self, num_layers, page_size=64, num_pages=1000, hidden_dim=5120):
"""
Args:
num_layers: Transformer 层数
page_size: 每个 page 存储的 token 数量
num_pages: 总 page 数量
hidden_dim: 隐藏层维度
"""
self.num_layers = num_layers
self.page_size = page_size
self.num_pages = num_pages
self.hidden_dim = hidden_dim
# 物理内存池:所有 page 的实际存储
# shape: (num_layers, num_pages, page_size, hidden_dim)
self.physical_memory = np.zeros(
(num_layers, num_pages, page_size, hidden_dim),
dtype=np.float16
)
# 空闲 page 列表
self.free_pages = list(range(num_pages))
# 页表:记录每个请求使用的 page
# key: request_id, value: List[page_id]
self.page_table: Dict[str, List[int]] = {}
def allocate_pages(self, request_id: str, num_tokens: int) -> List[int]:
"""
为请求分配 page
Args:
request_id: 请求 ID
num_tokens: 需要存储的 token 数量
Returns:
分配的 page ID 列表
"""
# 计算需要多少个 page
num_pages_needed = (num_tokens + self.page_size - 1) // self.page_size
if len(self.free_pages) < num_pages_needed:
raise MemoryError("No enough free pages!")
# 分配 page
allocated_pages = []
for _ in range(num_pages_needed):
page_id = self.free_pages.pop(0)
allocated_pages.append(page_id)
# 更新页表
self.page_table[request_id] = allocated_pages
print(f"Request {request_id}: allocated {num_pages_needed} pages")
print(f"Page IDs: {allocated_pages}")
return allocated_pages
def write_kv(self, request_id: str, layer_id: int, kv_data: np.ndarray):
"""
将 KV 数据写入 page
Args:
request_id: 请求 ID
layer_id: 层 ID
kv_data: KV 数据,shape: (seq_len, hidden_dim)
"""
pages = self.page_table[request_id]
seq_len = kv_data.shape[0]
# 按 page 切分写入
for i, page_id in enumerate(pages):
start_idx = i * self.page_size
end_idx = min((i + 1) * self.page_size, seq_len)
if start_idx < seq_len:
page_data = kv_data[start_idx:end_idx]
self.physical_memory[layer_id, page_id, :len(page_data)] = page_data
def read_kv(self, request_id: str, layer_id: int) -> np.ndarray:
"""
读取请求的 KV 数据
Args:
request_id: 请求 ID
layer_id: 层 ID
Returns:
KV 数据,shape: (seq_len, hidden_dim)
"""
pages = self.page_table[request_id]
# 拼接所有 page 的数据
kv_data = []
for page_id in pages:
page_data = self.physical_memory[layer_id, page_id]
kv_data.append(page_data)
return np.concatenate(kv_data, axis=0)
def free_pages(self, request_id: str):
"""
释放请求占用的 page
"""
if request_id in self.page_table:
pages = self.page_table.pop(request_id)
self.free_pages.extend(pages)
print(f"Request {request_id}: freed {len(pages)} pages")
def get_memory_utilization(self) -> float:
"""
计算显存利用率
"""
used_pages = self.num_pages - len(self.free_pages)
return used_pages / self.num_pages
# 使用示例
cache_manager = PagedKVCache(
num_layers=40,
page_size=64,
num_pages=1000,
hidden_dim=5120
)
# 三个不同长度的请求
cache_manager.allocate_pages("req_001", num_tokens=1000) # 需要 16 pages
cache_manager.allocate_pages("req_002", num_tokens=500) # 需要 8 pages
cache_manager.allocate_pages("req_003", num_tokens=1800) # 需要 29 pages
print(f"\n显存利用率: {cache_manager.get_memory_utilization():.2%}")
PagedAttention 的注意力计算:
在分页存储的情况下,注意力计算需要特殊处理:
def paged_attention(query, page_table, kv_cache, page_size=64):
"""
基于分页的注意力计算
Args:
query: 当前 token 的 query,shape: (hidden_dim,)
page_table: 页表,List[page_id]
kv_cache: 物理内存,shape: (num_pages, page_size, hidden_dim)
page_size: 每个 page 的大小
Returns:
注意力输出
"""
d_k = query.shape[-1]
all_keys = []
all_values = []
# 遍历页表,收集所有 K 和 V
for page_id in page_table:
# 从物理内存读取 page 数据
page_k = kv_cache[page_id, :, :d_k] # Key
page_v = kv_cache[page_id, :, d_k:] # Value
all_keys.append(page_k)
all_values.append(page_v)
# 拼接所有 page 的 K 和 V
keys = np.concatenate(all_keys, axis=0) # shape: (total_seq_len, d_k)
values = np.concatenate(all_values, axis=0) # shape: (total_seq_len, d_v)
# 标准注意力计算
scores = np.dot(query, keys.T) / np.sqrt(d_k)
attention_weights = softmax(scores)
output = np.dot(attention_weights, values)
return output
def softmax(x):
"""数值稳定的 Softmax"""
exp_x = np.exp(x - np.max(x))
return exp_x / np.sum(exp_x)
2.4 显存优化效果分析
对比实验(LLaMA-13B, Batch=32):
方案 | 平均序列长度 | 显存占用 | 利用率 |
---|---|---|---|
传统连续存储 | 1200 | 51.2 GB | 58.6% |
PagedAttention | 1200 | 35.8 GB | 95.2% |
节省显存 | - | 30.1% | +36.6% |
核心优势:
- 零 Padding 浪费:按实际需求分配
- 无碎片化:page 可灵活组合
- 支持超长上下文:分页拼接,理论无上限
- 多请求隔离:各自独立的页表
一句话总结:PagedAttention = KV Cache 变成"分页内存"
3. Continuous Batching:算力的流水线工人
3.1 静态批处理的局限性
传统推理框架使用的是静态批处理(Static Batching):
# 静态批处理的问题示例
class StaticBatchScheduler:
def __init__(self, batch_size=8):
self.batch_size = batch_size
self.waiting_queue = []
def add_request(self, request):
"""新请求只能排队等待"""
self.waiting_queue.append(request)
print(f"Request {request.id} 进入等待队列,当前队列长度: {len(self.waiting_queue)}")
def run_batch(self):
"""必须凑够 batch_size 才能开始"""
if len(self.waiting_queue) < self.batch_size:
print(f"等待中...还需要 {self.batch_size - len(self.waiting_queue)} 个请求")
return None
# 取出一批请求
batch = self.waiting_queue[:self.batch_size]
self.waiting_queue = self.waiting_queue[self.batch_size:]
# 开始推理
print(f"开始处理 batch,包含 {len(batch)} 个请求")
results = self.inference(batch)
# 问题:必须等最慢的请求完成
# 即使其他请求已经生成完毕,也要等待
return results
核心问题:
- 等待凑批:GPU 空转,新请求延迟高
- 同步完成:快的等慢的,短序列浪费算力
- 固定批次:batch 大小不灵活,难以适应负载变化
时间线示意:
传统静态批处理:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━→ 时间
[等待凑批...] [Batch 1 运行................] [等待凑批...] [Batch 2...]
↑ ↑
Req1-8 一起跑 Req9-16 才能开始
问题:Req9 必须等 Req1-8 全部完成!
3.2 Continuous Batching 的工作机制
Continuous Batching 直接换了个玩法,实现了真正的"流水线"调度:
- 随时插入:正在跑的 batch 里可以动态加新请求
- 动态退出:谁生成完了,直接踢出 batch,空出的位置立即给新请求
- 流水线式调度:老请求继续解码,新请求同时进场
- 迭代级调度:每生成一个 token,就重新调度一次
Continuous Batching:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━→ 时间
[Req1][Req2][Req3]
[Req4][Req5][Req6] ← Req1 完成,Req4 立即插入
[Req7][Req8] ← Req2 完成,Req7 立即插入
[Req9] ← Req3 完成,Req9 立即插入
优势:GPU 永不空闲,新请求响应快!
3.3 调度算法实现
import time
from collections import deque
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class Request:
"""推理请求"""
id: str
prompt: str
max_tokens: int
generated_tokens: int = 0
status: str = "waiting" # waiting, running, completed
start_time: Optional[float] = None
end_time: Optional[float] = None
class ContinuousBatchScheduler:
"""
连续批处理调度器
"""
def __init__(self, max_batch_size=32, max_seq_len=2048):
self.max_batch_size = max_batch_size
self.max_seq_len = max_seq_len
# 当前正在运行的 batch
self.running_batch: List[Request] = []
# 等待队列
self.waiting_queue = deque()
# 完成的请求
self.completed_requests: List[Request] = []
def add_request(self, request: Request):
"""
添加新请求(可以随时添加)
"""
request.status = "waiting"
self.waiting_queue.append(request)
print(f"[时间 {time.time():.2f}] Request {request.id} 加入队列")
def schedule_step(self):
"""
调度步骤:每生成一个 token 后执行一次
这是 Continuous Batching 的核心!
"""
# 1. 检查当前 batch 中完成的请求
self._remove_completed_requests()
# 2. 从等待队列中添加新请求到 batch
self._add_new_requests_to_batch()
# 3. 执行推理(生成一个 token)
if self.running_batch:
self._inference_step()
def _remove_completed_requests(self):
"""
移除已完成的请求
"""
completed = []
still_running = []
for req in self.running_batch:
if req.generated_tokens >= req.max_tokens:
# 请求完成
req.status = "completed"
req.end_time = time.time()
completed.append(req)
print(f"[时间 {time.time():.2f}] Request {req.id} 完成,释放位置")
else:
still_running.append(req)
self.running_batch = still_running
self.completed_requests.extend(completed)
def _add_new_requests_to_batch(self):
"""
将等待队列中的请求添加到 batch
"""
# 计算还能容纳多少请求
available_slots = self.max_batch_size - len(self.running_batch)
# 从等待队列取出请求
while available_slots > 0 and self.waiting_queue:
new_req = self.waiting_queue.popleft()
new_req.status = "running"
new_req.start_time = time.time()
self.running_batch.append(new_req)
print(f"[时间 {time.time():.2f}] Request {new_req.id} 进入 batch(当前 batch 大小: {len(self.running_batch)})")
available_slots -= 1
def _inference_step(self):
"""
执行推理:为 batch 中的每个请求生成一个 token
"""
print(f"\n=== 推理步骤 ===")
print(f"当前 batch 大小: {len(self.running_batch)}")
print(f"活跃请求: {[req.id for req in self.running_batch]}")
# 模拟推理过程
for req in self.running_batch:
req.generated_tokens += 1
print(f" Request {req.id}: {req.generated_tokens}/{req.max_tokens} tokens")
# 在实际实现中,这里会调用模型前向传播
# output_tokens = model.forward(input_ids, kv_cache)
def run_until_complete(self):
"""
运行直到所有请求完成
"""
step = 0
while self.running_batch or self.waiting_queue:
step += 1
print(f"\n{'='*50}")
print(f"Step {step}")
print(f"{'='*50}")
self.schedule_step()
time.sleep(0.1) # 模拟推理时间
print(f"\n所有请求完成!")
self._print_statistics()
def _print_statistics(self):
"""
打印统计信息
"""
print(f"\n{'='*50}")
print("统计信息")
print(f"{'='*50}")
total_requests = len(self.completed_requests)
avg_latency = sum(
req.end_time - req.start_time
for req in self.completed_requests
) / total_requests if total_requests > 0 else 0
print(f"完成请求数: {total_requests}")
print(f"平均延迟: {avg_latency:.2f} 秒")
# 使用示例
scheduler = ContinuousBatchScheduler(max_batch_size=4)
# 模拟请求陆续到达
requests = [
Request(id="req_001", prompt="Hello", max_tokens=5),
Request(id="req_002", prompt="Hi", max_tokens=10),
Request(id="req_003", prompt="How are you", max_tokens=3),
Request(id="req_004", prompt="Good morning", max_tokens=8),
Request(id="req_005", prompt="Nice day", max_tokens=4),
]
# 添加前3个请求
for req in requests[:3]:
scheduler.add_request(req)
# 开始调度(模拟运行几步后添加新请求)
for i in range(3):
scheduler.schedule_step()
time.sleep(0.1)
# 添加后2个请求(动态插入!)
for req in requests[3:]:
scheduler.add_request(req)
# 运行直到完成
scheduler.run_until_complete()
3.4 吞吐量提升分析
理论分析:
假设有 N N N 个请求,生成长度分别为 L 1 , L 2 , . . . , L N L_1, L_2, ..., L_N L1,L2,...,LN:
静态批处理的总时间:
T static = N B × max ( L 1 , L 2 , . . . , L B ) + N B × max ( L B + 1 , . . . , L 2 B ) + . . . T_{\text{static}} = \frac{N}{B} \times \max(L_1, L_2, ..., L_B) + \frac{N}{B} \times \max(L_{B+1}, ..., L_{2B}) + ... Tstatic=BN×max(L1,L2,...,LB)+BN×max(LB+1,...,L2B)+...
其中 B B B 是批次大小。
Continuous Batching 的总时间:
T continuous = max ( L 1 , L 2 , . . . , L N ) T_{\text{continuous}} = \max(L_1, L_2, ..., L_N) Tcontinuous=max(L1,L2,...,LN)
吞吐量提升比:
Speedup = T static T continuous = ∑ i = 1 N / B max ( batch i ) max ( L 1 , . . . , L N ) \text{Speedup} = \frac{T_{\text{static}}}{T_{\text{continuous}}} = \frac{\sum_{i=1}^{N/B} \max(\text{batch}_i)}{\max(L_1, ..., L_N)} Speedup=TcontinuousTstatic=max(L1,...,LN)∑i=1N/Bmax(batchi)
实际测试数据(LLaMA-7B, A100-40GB):
指标 | 静态批处理 | Continuous Batching | 提升 |
---|---|---|---|
吞吐量 (tokens/s) | 1,250 | 2,840 | 2.27× |
平均延迟 (ms) | 380 | 145 | 2.62× |
GPU 利用率 | 65% | 92% | +27% |
请求排队时间 (ms) | 220 | 12 | 18.3× |
核心优势:
- GPU 一直满负荷干活:没有等待凑批的空闲期
- 新请求不用等:下单秒上车,首 token 延迟低
- 高并发轻松扛:动态调度,自适应负载
- 短请求不拖累:完成即走,不等长请求
一句话总结:Continuous Batching = 请求像流水线一样连续进出
4. 双剑合璧:算力 + 显存的完美配合
4.1 协同工作原理
光有 Continuous Batching 行不行?不行!
因为请求随时进出,序列长度不一,KV Cache 会变得又长又乱。如果还用传统的连续内存管理,会导致:
- 频繁的内存分配/释放
- 严重的碎片化
- 无法预测内存需求
这时 PagedAttention 就上场了!
分工协作:
- Continuous Batching 负责算力调度,保证 GPU 不闲着
- PagedAttention 负责显存调度,保证 KV Cache 灵活高效
class vLLMEngine:
"""
vLLM 引擎:整合 PagedAttention + Continuous Batching
"""
def __init__(self, model, max_batch_size=32, page_size=64, num_pages=1000):
self.model = model
# Continuous Batching 调度器
self.scheduler = ContinuousBatchScheduler(max_batch_size=max_batch_size)
# PagedAttention KV Cache 管理器
self.kv_cache = PagedKVCache(
num_layers=model.num_layers,
page_size=page_size,
num_pages=num_pages,
hidden_dim=model.hidden_dim
)
# 请求 → Page 映射
self.request_pages = {}
def add_request(self, request: Request):
"""
添加新请求
"""
# 1. Continuous Batching: 加入调度队列
self.scheduler.add_request(request)
# 2. PagedAttention: 预分配初始 page(用于 prompt)
prompt_len = len(request.prompt.split())
pages = self.kv_cache.allocate_pages(request.id, prompt_len)
self.request_pages[request.id] = pages
print(f"Request {request.id} 添加成功")
print(f" - 调度状态: {request.status}")
print(f" - 分配页数: {len(pages)}")
def generation_step(self):
"""
生成步骤:协同工作的核心
"""
# 1. Continuous Batching: 调度当前 batch
self.scheduler.schedule_step()
if not self.scheduler.running_batch:
return
# 2. 为 batch 中的每个请求准备 KV Cache
batch_kv_cache = []
for req in self.scheduler.running_batch:
# PagedAttention: 读取该请求的 KV
pages = self.request_pages[req.id]
kv = self.kv_cache.read_kv(req.id, layer_id=0) # 简化示例
batch_kv_cache.append(kv)
# 3. 模型前向传播(使用分页的 KV Cache)
# output = self.model.forward(batch_input, batch_kv_cache)
# 4. 生成新 token 后,更新 KV Cache
for req in self.scheduler.running_batch:
req.generated_tokens += 1
# PagedAttention: 如果需要更多空间,动态分配新 page
current_len = req.generated_tokens + len(req.prompt.split())
required_pages = (current_len + self.kv_cache.page_size - 1) // self.kv_cache.page_size
current_pages = len(self.request_pages[req.id])
if required_pages > current_pages:
# 需要分配新 page
new_page = self.kv_cache.free_pages.pop(0)
self.request_pages[req.id].append(new_page)
print(f" Request {req.id}: 分配新 page {new_page}")
# 5. Continuous Batching: 移除完成的请求
completed = [req for req in self.scheduler.running_batch
if req.generated_tokens >= req.max_tokens]
for req in completed:
# PagedAttention: 释放 page
self.kv_cache.free_pages(req.id)
del self.request_pages[req.id]
print(f"Request {req.id} 完成,释放资源")
def get_system_stats(self):
"""
获取系统统计信息
"""
return {
"running_requests": len(self.scheduler.running_batch),
"waiting_requests": len(self.scheduler.waiting_queue),
"memory_utilization": self.kv_cache.get_memory_utilization(),
"active_pages": len([p for p in self.request_pages.values()])
}
协同工作流程图:
┌─────────────────────────────────────────────────────────────┐
│ vLLM Engine │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────┐ ┌─────────────────────┐ │
│ │ Continuous Batching │ │ PagedAttention │ │
│ │ (算力调度) │◄────►│ (显存调度) │ │
│ └──────────────────────┘ └─────────────────────┘ │
│ │ │ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────┐ ┌─────────────────────┐ │
│ │ Running Batch │ │ Page Table │ │
│ │ [Req1, Req2, ...] │ │ Req1 → [P0,P5,P9] │ │
│ └──────────────────────┘ │ Req2 → [P1,P3] │ │
│ │ └─────────────────────┘ │
│ │ │ │
│ └──────────┬───────────────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Model Forward │ │
│ │ (GPU Computation) │ │
│ └─────────────────────┘ │
│ │ │
│ ▼ │
│ [ Generate Tokens ] │
└─────────────────────────────────────────────────────────────┘
4.2 性能优化案例
场景:在线服务,并发请求,序列长度不一
import random
# 模拟实际场景的性能测试
def benchmark_vllm():
"""
性能基准测试
"""
# 模拟 100 个请求,长度从 50 到 2000 不等
requests = []
for i in range(100):
req = Request(
id=f"req_{i:03d}",
prompt=f"prompt_{i}",
max_tokens=random.randint(50, 2000)
)
requests.append(req)
# 统计信息
stats = {
"total_requests": len(requests),
"total_tokens": sum(req.max_tokens for req in requests),
"avg_length": sum(req.max_tokens for req in requests) / len(requests),
"max_length": max(req.max_tokens for req in requests),
"min_length": min(req.max_tokens for req in requests)
}
print("="*60)
print("vLLM 性能测试")
print("="*60)
print(f"总请求数: {stats['total_requests']}")
print(f"总 token 数: {stats['total_tokens']}")
print(f"平均长度: {stats['avg_length']:.1f}")
print(f"最大长度: {stats['max_length']}")
print(f"最小长度: {stats['min_length']}")
print("="*60)
# 传统静态批处理的预估时间
batch_size = 8
num_batches = (len(requests) + batch_size - 1) // batch_size
# 假设每个 token 生成时间 = 1ms
static_time = 0
for i in range(num_batches):
batch = requests[i*batch_size:(i+1)*batch_size]
batch_time = max(req.max_tokens for req in batch)
static_time += batch_time
# Continuous Batching 的预估时间
# 理想情况:GPU 一直满载,时间 = 最长序列
continuous_time = max(req.max_tokens for req in requests)
speedup = static_time / continuous_time
print(f"\n传统静态批处理预估时间: {static_time} ms")
print(f"Continuous Batching 预估时间: {continuous_time} ms")
print(f"理论加速比: {speedup:.2f}×")
# 显存占用对比
# 传统方式:预分配最大长度
max_len = 2048
traditional_memory = batch_size * max_len * 2 # K + V
# PagedAttention:按实际长度
page_size = 64
total_pages_needed = sum(
(req.max_tokens + page_size - 1) // page_size
for req in requests[:batch_size]
)
paged_memory = total_pages_needed * page_size * 2
memory_saving = (traditional_memory - paged_memory) / traditional_memory
print(f"\n传统方式显存占用: {traditional_memory} units")
print(f"PagedAttention 显存占用: {paged_memory} units")
print(f"显存节省: {memory_saving:.1%}")
# 运行测试
benchmark_vllm()
实际生产环境数据(GPT-3 规模模型):
指标 | 传统方案 | vLLM | 提升 |
---|---|---|---|
吞吐量 | 1,200 req/s | 3,600 req/s | 3.0× |
P50 延迟 | 320 ms | 110 ms | 2.9× |
P99 延迟 | 1,200 ms | 380 ms | 3.2× |
显存利用率 | 48% | 91% | +43% |
GPU 利用率 | 62% | 94% | +32% |
成本($/1M tokens) | $2.5 | $0.85 | 节省 66% |
4.3 实际部署经验
配置建议:
# vLLM 推荐配置(基于 A100-40GB)
config = {
# PagedAttention 参数
"block_size": 16, # 每个 page/block 的大小
"gpu_memory_utilization": 0.9, # GPU 显存利用率目标
"max_num_seqs": 256, # 最大并发序列数
# Continuous Batching 参数
"max_num_batched_tokens": 2048, # 每个 batch 的最大 token 数
"max_model_len": 4096, # 模型支持的最大序列长度
# 调度策略
"scheduling_policy": "fcfs", # First-Come-First-Serve
"enable_prefix_caching": True, # 启用前缀缓存
# 性能优化
"swap_space": 4, # CPU swap 空间(GB)
"use_cuda_graph": True, # 使用 CUDA Graph 优化
}
最佳实践:
-
Page Size 选择:
- 太小:管理开销大
- 太大:浪费增加
- 推荐:16-64(根据模型大小调整)
-
Batch Size 调整:
- 根据显存动态调整
- 监控 GPU 利用率
- 避免 OOM
-
预热策略:
# 系统预热,编译 CUDA kernels
def warmup_vllm(engine, warmup_requests=10):
"""
系统预热
"""
print("开始预热...")
for i in range(warmup_requests):
dummy_req = Request(
id=f"warmup_{i}",
prompt="warmup",
max_tokens=100
)
engine.add_request(dummy_req)
# 运行几个迭代
for _ in range(50):
engine.generation_step()
print("预热完成!")
搭配效果总结:
- 吞吐量爆炸:GPU 永不打烊
- 显存利用率极高:无碎片、少 padding
- 支持超长上下文 + 海量并发:分页管理 + 动态调度
- 成本大幅降低:更少的 GPU 完成更多工作
5. 总结
5.1 核心技术回顾
vLLM 的两大核心技术各司其职,完美配合:
技术 | 作用层 | 核心创新 | 解决问题 |
---|---|---|---|
PagedAttention | 显存管理 | 分页存储 KV Cache | 显存碎片化、浪费、利用率低 |
Continuous Batching | 计算调度 | 动态 batch 管理 | GPU 空闲、排队延迟、吞吐量低 |
关键公式总结:
KV Cache 显存占用:
Memory KV = 2 × N layers × L seq × d model × precision \text{Memory}_{\text{KV}} = 2 \times N_{\text{layers}} \times L_{\text{seq}} \times d_{\text{model}} \times \text{precision} MemoryKV=2×Nlayers×Lseq×dmodel×precision
PagedAttention 利用率:
Utilization = ∑ actual_tokens total_pages × page_size \text{Utilization} = \frac{\sum \text{actual\_tokens}}{\text{total\_pages} \times \text{page\_size}} Utilization=total_pages×page_size∑actual_tokens
Continuous Batching 加速比:
Speedup = ∑ i max ( batch i ) max ( all_sequences ) \text{Speedup} = \frac{\sum_{i} \max(\text{batch}_i)}{\max(\text{all\_sequences})} Speedup=max(all_sequences)∑imax(batchi)
5.2 性能提升总结
综合性能对比(LLaMA-13B, A100-40GB):
┌────────────────────────────────────────────────┐
│ 性能指标对比 │
├────────────────┬──────────┬──────────┬─────────┤
│ 指标 │ 传统方案 │ vLLM │ 提升 │
├────────────────┼──────────┼──────────┼─────────┤
│ 吞吐量 (tok/s) │ 1,250 │ 3,800 │ 3.04× │
│ 显存利用率 │ 52% │ 93% │ +41% │
│ GPU 利用率 │ 64% │ 95% │ +31% │
│ P50 延迟 (ms) │ 340 │ 98 │ 3.47× │
│ 最大 batch │ 16 │ 128 │ 8× │
│ 成本效率 │ 1.0× │ 3.2× │ 3.2× │
└────────────────┴──────────┴──────────┴─────────┘
5.3 适用场景
最适合 vLLM 的场景:
- 在线服务:高并发、低延迟要求
- 长文本生成:文章、报告、代码
- 多轮对话:聊天机器人、客服系统
- 批量推理:大规模数据处理
不适合的场景:
- 单请求、低并发:优势不明显
- 极短序列(<50 tokens):管理开销相对较大
更多推荐
所有评论(0)