三个递进实验,从理论到实践:
25 分钟
对应 PPT 第 6–7 页
给定以下模型配置,计算单个请求在指定 token 数下的 KV Cache 大小:
| 模型 | n_layers | n_kv_heads | head_dim | dtype |
|---|---|---|---|---|
| Llama-3-8B | 32 | 8 | 128 | FP16 |
| Qwen2.5-7B | 28 | 4 | 128 | FP16 |
| Llama-3-70B | 80 | 8 | 128 | FP16 |
KV Cache 公式: 2 × n_layers × n_kv_heads × head_dim × n_tokens × dtype_bytes
计算每个模型在以下场景的 KV Cache:
| 场景 | prompt tokens | output tokens | 总 tokens |
|---|---|---|---|
| A: 短问答 | 128 | 128 | 256 |
| B: 中等对话 | 512 | 512 | 1024 |
| C: 长文档 | 4096 | 2048 | 6144 |
问题:
对应 PPT 第 47–48 页 部分 5
source vllm-env/bin/activate
vllm serve Qwen/Qwen2.5-0.5B-Instruct \
--host 0.0.0.0 --port 8000 \
--max-model-len 2048 \
--gpu-memory-utilization 0.8 \
--max-num-seqs 32 &
观察启动日志:
# GPU blocks: XXX — 每个 Block 存 block_size=16 个 token 的 KV Cacheallocate_kv_cache() 公式curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2.5-0.5B-Instruct",
"messages": [
{"role": "system", "content": "你是一个有帮助的AI助手"},
{"role": "user", "content": "用三句话介绍GPU和CPU的区别"}
],
"max_tokens": 200,
"temperature": 0.7
}' | python3 -m json.tool | head -20
pip install aiohttp
wget -q https://raw.githubusercontent.com/vllm-project/vllm/main/benchmarks/benchmark_serving.py
python benchmark_serving.py \
--backend vllm \
--model Qwen/Qwen2.5-0.5B-Instruct \
--dataset-name random \
--random-input-len 128 \
--random-output-len 128 \
--num-prompts 100 \
--request-rate 4 2>&1 | tee benchmark_result.txt
记录关键指标: Throughput (tok/s), TTFT P50/P95, TPOT P50/P95。
对比题: 将 --request-rate 改为 1、8、16、32,观察 TTFT 和 TPOT 如何变化。找到系统饱和点 (Throughput 不再增长的那个 rate)。
对应 PPT 第 49–50 页 部分 5
pip install git+https://github.com/ForceInjection/nano-vllm.git
# 下载模型
huggingface-cli download --resume-download Qwen/Qwen3-0.6B \
--local-dir ~/huggingface/Qwen3-0.6B/ \
--local-dir-use-symlinks False
# 验证
python -c "from nanovllm import LLM; print('nano-vllm OK')"
from nanovllm import LLM, SamplingParams
llm = LLM(f'{__import__("os").environ["HOME"]}/huggingface/Qwen3-0.6B/',
enforce_eager=True)
sampling_params = SamplingParams(temperature=0.6, max_tokens=128)
prompts = [
'你好,请介绍你自己。',
'什么是GPU?',
]
outputs = llm.generate(prompts, sampling_params)
for i, output in enumerate(outputs):
print(f'--- Prompt {i+1} ---')
print(output['text'])
创建 trace_nanovllm.py:
"""nano-vllm 执行追踪 — 观察 Sequence 状态转换和 Block 分配"""
import os
from nanovllm import LLM, SamplingParams
# ====== Monkey-patch 添加日志 ======
from nanovllm.engine import sequence as seq_mod
from nanovllm.engine import block_manager as bm_mod
from nanovllm.engine import scheduler as sch_mod
_orig_sequence_init = seq_mod.Sequence.__init__
def _traced_init(self, token_ids, sampling_params=None):
_orig_sequence_init(self, token_ids, sampling_params)
print(f"[SEQ {self.seq_id}] CREATED | status=WAITING | "
f"num_tokens={self.num_tokens} | num_blocks={self.num_blocks}")
seq_mod.Sequence.__init__ = _traced_init
_orig_allocate = bm_mod.BlockManager.allocate
def _traced_allocate(self, seq, num_cached_blocks):
_orig_allocate(self, seq, num_cached_blocks)
print(f"[BLOCK] ALLOCATE seq={seq.seq_id} | "
f"cached_blocks={num_cached_blocks} | "
f"new_blocks={seq.num_blocks - num_cached_blocks} | "
f"block_table={seq.block_table} | "
f"free_blocks={len(self.free_block_ids)}")
bm_mod.BlockManager.allocate = _traced_allocate
_orig_deallocate = bm_mod.BlockManager.deallocate
def _traced_deallocate(self, seq):
print(f"[BLOCK] DEALLOCATE seq={seq.seq_id} | "
f"block_table={seq.block_table}")
_orig_deallocate(self, seq)
bm_mod.BlockManager.deallocate = _traced_deallocate
_orig_preempt = sch_mod.Scheduler.preempt
def _traced_preempt(self, seq):
print(f"[SCHED] PREEMPT seq={seq.seq_id} | reason=OOM (no free blocks)")
_orig_preempt(self, seq)
sch_mod.Scheduler.preempt = _traced_preempt
_orig_postprocess = sch_mod.Scheduler.postprocess
def _traced_postprocess(self, seqs, token_ids, is_prefill):
for seq, tok_id in zip(seqs, token_ids):
phase = "PREFILL" if is_prefill else "DECODE"
status = seq.status.name
print(f"[SCHED] STEP seq={seq.seq_id} | phase={phase} | "
f"token={tok_id} | num_tokens={seq.num_tokens} | "
f"cached={seq.num_cached_tokens} | status→{status}")
_orig_postprocess(self, seqs, token_ids, is_prefill)
sch_mod.Scheduler.postprocess = _traced_postprocess
# ====== 运行推理 ======
print("=" * 60)
print("nano-vllm 执行追踪开始")
print("=" * 60)
llm = LLM(
f"{os.environ['HOME']}/huggingface/Qwen3-0.6B/",
enforce_eager=True,
gpu_memory_utilization=0.6,
)
sampling_params = SamplingParams(temperature=0.6, max_tokens=64)
prompts = [
"你好,请介绍你自己。",
"什么是GPU? 请详细解释。",
]
outputs = llm.generate(prompts, sampling_params)
print("\n" + "=" * 60)
print("输出结果:")
for i, output in enumerate(outputs):
print(f"\n--- Prompt {i+1} ---")
print(output['text'][:200])
运行:
python trace_nanovllm.py 2>&1 | head -80
观察要点:
phase= 日志)# 停止 vLLM 服务
kill %1
2 × L × H_kv × D × T × Bengine/scheduler.py (93 行)engine/block_manager.py (121 行)engine/model_runner.py (~230 行)block_table[i] → 物理 Block ID → 间接寻址 → 物理不连续但逻辑连续完成后应能回答:
block_table 是什么数据结构?它的长度由什么决定?schedule() 中是如何区分的?can_allocate() 中如何检查?