Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

nanoVLLM

手写 Triton PagedAttention decode kernel,与 PyTorch baseline 做可复现的 正确性与性能对比。目标是把 vLLM 的核心机制(分页 KV cache + online softmax) 实现到能讲透每一行的程度。

项目定位:学习与原型验证项目。只做 decode attention 算子层, 没有 scheduler、没有 serving、没有量化。不是生产级推理引擎。


与已有工作的关系(先说清楚,避免误会)

项目 作者 关系
vLLM UC Berkeley 等 PagedAttention 的原始论文与工业实现,本项目的思想来源
nano-vllm 俞星凯(DeepSeek) 名字相近但无代码关系。它是 ~1200 行 Python/PyTorch 的完整引擎复刻(含 scheduler/engine)
本项目 独立实现,聚焦点不同:只做 decode attention 的手写 Triton kernel,配三层对比 baseline + profiler

本项目没有 fork 任何仓库,代码从零编写。与 nano-vllm 的区别在于: 它复刻的是引擎架构,本项目做的是单算子的 kernel 级优化与量化对比


核心问题

decode 阶段每步只生成 1 个 token,却要对全部历史 KV 做 attention。 朴素做法有三笔随 context 放大的开销:

开销 朴素做法 本项目做法
显存 materialize [seq_len] 分数矩阵,O(N) online softmax 递推,中间状态只有 BLOCK_N,O(1)
访存 每步重新 gather 并拷贝 KV block table 间接寻址,直接读物理显存,零拷贝
Kernel launch gather / matmul / softmax / matmul 多次 launch 全部融合进一个 Triton kernel,一次 launch

online softmax 与标准 softmax 数学上完全等价(不是近似), 但把「必须看到全部分数才能归一化」变成了流式递推。


三层对比(为什么需要三层)

实现 做法 定位
paged_attention_ref Python 三重循环 正确性金标准
paged_attention_vectorized 高级索引 gather + einsum 公平 baseline:已向量化,但仍 materialize 分数矩阵
paged_attention_decode_triton 手写 Triton kernel,online softmax 本项目产出

关键对比是 vectorized vs triton。只和 Python 循环比是没有说服力的 —— 两边都向量化之后的差距,才真实反映 kernel 优化(online softmax + 单次 launch

  • 分块 gather)的收益。

快速开始

不需要 GPU:先验证算法与索引

pip install torch
python tests/test_kernel_logic_cpu.py

用 PyTorch 在 CPU 上逐行复刻 Triton kernel 的指针算术和 online softmax 递推,与标准 softmax 对拍。7 个 case 覆盖 GQA / MHA / MQA 和 4 种 context 边界。

这一步把「算法 / 索引错误」和「Triton 语法错误」解耦,排查范围小一个数量级。

需要 CUDA GPU:正确性 + 性能

环境:CUDA GPU、Python 3.10+、PyTorch ≥ 2.1(Linux 版自带 Triton)

# 1) 正确性验收
python tests/test_paged_attention.py

# 2) 单点三层对比
python bench/bench_decode.py

# 3) 扫描 context_len,出曲线数据
python bench/bench_decode.py --scan --csv results.csv

# 4) profiler trace(chrome://tracing 或 ui.perfetto.dev 打开)
python bench/bench_decode.py --profile --ctx 2048 --trace-out trace.json

# 5) 出图 + 打印 markdown 结果表
python bench/plot_results.py --csv results.csv --out-dir figs --gpu "RTX 3090"

完整的一步步操作流程见 RUNBOOK.md


目录结构

nanoVLLM/
├── nanoVLLM/ops/
│   ├── attention.py                # Triton PagedAttention decode kernel(核心)
│   └── paged_attention_ref.py      # PyTorch 参考实现 + 向量化 baseline
├── tests/
│   ├── test_kernel_logic_cpu.py    # CPU 逻辑验证(不需要 GPU)
│   └── test_paged_attention.py     # GPU 正确性验收(GQA/MHA/MQA + 边界长度)
├── bench/
│   ├── bench_decode.py             # 时间 / 显存 / profiler
│   └── plot_results.py             # CSV -> 曲线图 + markdown 表
└── RUNBOOK.md                      # 一步步操作手册

实现要点

数据布局

query:        [num_seqs, num_heads, head_dim]                    fp16
key_cache:    [num_blocks, block_size, num_kv_heads, head_dim]   fp16
value_cache:  同上
block_tables: [num_seqs, max_blocks_per_seq]                     int32
context_lens: [num_seqs]                                         int32

key_cache 的第 0 维是物理 block 编号,与序列无关 —— 它是全局显存池。 序列第 b 个逻辑 block 存在哪,由 block_tables[seq, b] 决定。 这和操作系统的虚拟内存页表同构,这就是 "paged" 的全部含义。

grid 设计与已知局限

grid = (num_heads, num_seqs),每个 program 负责一个 (序列, head) 组合, 串行遍历该序列的所有 block。

已知局限:program 总数 = num_heads × num_seqs。小 batch 时(如 8×8=64) 跑不满 RTX 3090 的 82 个 SM,occupancy 不足。生产级实现(vLLM 的 v2 kernel) 会在 context 维度再切 partition 做 split-K:先并行算局部 softmax,再二次归约。 本项目 v1 不做,但 benchmark 里提供了 --num-seqs 64 对照来量化这个影响。

五个处理过的边界情况

  1. GQA 映射kv_head = head // (num_heads // num_kv_heads)num_kv_heads == 1(MQA)时公式自动退化成立,无需分支。
  2. 尾部 maskcontext_len 通常不是 block_size 整数倍,最后一个 block 尾部是其他序列释放后的残留数据。除了把分数压成 -inftl.load 上也挂 mask=valid, other=0.0,防止残留的 inf/nan 污染整行乘加。
  3. -inf 相减产生 nanm_i 初值 -inf,第一轮 alpha = exp(-inf - m_new) = 0 是正确的。但若某 block 整块被 mask,m_new 仍为 -infexp(-inf - (-inf)) = nan 会污染全部结果。ctx_len >= 1 时不会发生; ctx_len == 0 时循环不执行,归一化前做了保护除。
  4. HEAD_DIM 必须是 2 的幂tl.arange 的编译期约束。
  5. 精度 — fp16 存储、fp32 累加。fp16 只有 10 位尾数,4096 token 累加 误差按 √N 放大,且 exp 极易溢出。m_i / l_i / acc 全用 fp32,只在写回时转 fp16。

测试条件

项目
GPU
Driver / CUDA / PyTorch / Triton
dtype fp16 存储,fp32 累加
num_seqs (batch) 8(另有 64 的对照组)
num_heads / num_kv_heads 8 / 4(GQA,与 Qwen3 同构)
head_dim 128
block_size 16
context_len 128 / 256 / 512 / 1024 / 2048 / 4096
预热 10 次
计时 重复 50 次取平均,前后 torch.cuda.synchronize()
显存 torch.cuda.max_memory_allocated() 峰值增量
数值容差 max abs diff < 2e-2(fp16 合理范围)

结果

context_len vectorized (ms) triton (ms) speedup vectorized 显存 (MB) triton 显存 (MB)
128
256
512
1024
2048
4096

与流式 ASR 的关系

流式 ASR 和 LLM decode 是同一个问题:变长序列 + 增量推理 + 历史状态复用

LLM 推理 流式 ASR
prefill 处理 prompt encoder 处理一个音频 chunk
decode 逐 token 生成 decoder 逐 token 输出文本
PagedAttention block 管理 chunk 级 KV cache 池
prefix caching 热词 / 上下文历史复用
continuous batching 多路音频流实时调度
context_lens 变长 mask 各路音频流长度不一

所以这套「block 管理 + online softmax decode kernel」可以直接迁移到流式 ASR 的 decoder 侧。差异点在于 ASR 对尾延迟(tail latency)更敏感, 所以适合固定 chunk 的确定性调度,而不适合引入时间抖动的投机解码。


路线

  • Triton PagedAttention decode kernel
  • CPU 逻辑验证(不依赖 GPU)
  • 三层对比 baseline + 正确性验收(GQA/MHA/MQA + 边界长度)
  • benchmark / profiler / 出图脚本
  • 在 RTX 3090 上跑出结果并填入上表
  • split-K partition 版 kernel,解决小 batch occupancy 不足
  • KV cache block manager + prefill/decode 引擎循环

参考

  • Kwon et al., Efficient Memory Management for Large Language Model Serving with PagedAttention, SOSP 2023
  • Dao et al., FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness, NeurIPS 2022
  • Milakov & Gimelshein, Online normalizer calculation for softmax, 2018

License

MIT

About

Hand-written Triton PagedAttention decode kernel with reproducible correctness & performance benchmarks against PyTorch baselines

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages