- Cargo workspace with xserv-cuda crate - CUDA FFI bindings (cudart: memory, stream, device, error) - GpuBuffer RAII wrapper with H2D/D2H/D2D copy - CudaStream wrapper with RAII Drop - CachingAllocator with size-bucketed free lists - PinnedBuffer for page-locked host memory - Device info query via cudaDeviceGetAttribute - Vector-add CUDA kernel smoke test - Integration test suite (11 tests) - build.rs: cc crate compiles .cu for SM 12.0 - sync-and-build.sh for remote build on dash5 - Roadmap doc (docs/00-roadmap.md) and Phase 0+1 design doc Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
58 KiB
xserv — LLM Inference Engine Roadmap
从零用 Rust + CUDA 构建一个完整的 LLM 推理引擎,目标是深入理解 LLM Serving 全栈技术。
设计决策
| 决策项 | 选择 | 备注 |
|---|---|---|
| 抽象层级 | Level 0.5 | 自写 CUDA kernel + cuBLAS 可切换,便于 benchmark 对比 |
| 硬件 | 8×RTX 5090 (Blackwell, CC 12.0, 32GB GDDR7) | 纯 PCIe Gen5 x16 互联,无 NVLink (详见下方硬件拓扑) |
| 语言 | Rust + CUDA (C/C++) | Rust FFI 调用 CUDA |
| 起步模型 | GPT-2 124M → Qwen3-7B | 从简单到实用 |
| 精度 | BF16/FP16 | 后期扩展 FP8 |
| Tensor | 自己实现 | 完整学习 tensor 抽象设计 |
| Tokenizer | 自己实现 BPE | 学习分词机制 |
| 权重格式 | safetensors | Rust 友好,零拷贝 mmap |
| Async Runtime | tokio | 成熟稳定,不引入性能问题 |
| API | OpenAI 兼容 | /v1/chat/completions,SSE streaming |
| 时间线 | 不限 | 学习为主,每步验证 |
硬件拓扑 (dash5, 已确认 2026-05-21)
GPU: 8× NVIDIA GeForce RTX 5090, 32607 MiB, Compute Capability 12.0
CUDA Toolkit: 12.9 (安装于 /usr/local/cuda-12.9,需将 bin/ 加入 PATH)
PCIe: Gen 5 x16 (理论单向 ~64 GB/s,空闲时降频至 Gen 1)
互联拓扑 (nvidia-smi topo -m):
GPU0 GPU1 GPU2 GPU3 GPU4 GPU5 GPU6 GPU7
GPU0 X PHB PHB PHB NODE NODE NODE NODE
GPU1 PHB X PHB PHB NODE NODE NODE NODE
GPU2 PHB PHB X PHB NODE NODE NODE NODE
GPU3 PHB PHB PHB X NODE NODE NODE NODE
GPU4 NODE NODE NODE NODE X PHB PHB PHB
GPU5 NODE NODE NODE NODE PHB X PHB PHB
GPU6 NODE NODE NODE NODE PHB PHB X PHB
GPU7 NODE NODE NODE NODE PHB PHB PHB X
PHB = 同一 PCIe Host Bridge(同组,延迟低)
NODE = 跨 PCIe Host Bridge(跨组,延迟较高)
分组: GPU 0-3 为一组, GPU 4-7 为一组。组内 PHB 互联,跨组 NODE 互联。
对设计的影响:
- 无 NVLink: AllReduce 带宽受限于 PCIe (~64 GB/s vs NVLink ~450 GB/s)
- TP 策略: 当前阶段目标 TP=1/2/4,在同组内 (0-3 或 4-7) 执行,全 PHB 互联
- 跨组并行 (TP=8, PP 等): 留待后续扩展
- CPU Affinity: GPU 0-3 亲和 CPU 0-127, GPU 4-7 亲和 CPU 0-186(NUMA 0-1)
项目结构
xserv/
├── Cargo.toml # workspace root
├── csrc/ # CUDA 源文件 (.cu / .cuh)
│ ├── gemm/ # GEMM kernels (naive, tiled, tensor core)
│ ├── attention/ # Attention kernels (naive, flash, paged)
│ ├── normalization/ # LayerNorm, RMSNorm
│ ├── activation/ # GELU, SiLU
│ ├── embedding/ # Embedding lookup, RoPE
│ ├── reduce/ # Softmax, argmax, sampling
│ └── quantize/ # FP8/INT8 kernels
├── crates/
│ ├── xserv-cuda/ # Phase 1: CUDA FFI, context, stream, allocator
│ ├── xserv-tensor/ # Phase 2: Tensor type, ops dispatch, DType
│ ├── xserv-kernels/ # Phase 3-5: kernel registry (custom + cuBLAS)
│ ├── xserv-tokenizer/ # Phase 7: BPE tokenizer
│ ├── xserv-model/ # Phase 6,8,10: model def + weight loading
│ ├── xserv-runtime/ # Phase 9,11,12: KV cache, paging, scheduler
│ ├── xserv-engine/ # Phase 13: inference engine orchestration
│ ├── xserv-api/ # Phase 13: HTTP server + OpenAI compat
│ ├── xserv-speculative/ # Phase 16: speculative decoding
│ └── xserv-distributed/ # Phase 17: tensor parallelism, NCCL
├── tests/ # integration tests
├── benches/ # criterion benchmarks
├── tools/ # 辅助脚本 (PyTorch reference output 生成等)
└── docs/ # 每个 phase 的设计文档
Phase 依赖图
Phase 0: 项目脚手架 + 环境验证
│
Phase 1: CUDA FFI 基础设施
│
Phase 2: Tensor 抽象层
│
Phase 3: GEMM (naive → tiled → tensor core → cuBLAS)
│
Phase 4: Transformer Kernels (Norm, Activation, Embedding, RoPE, Softmax)
│
Phase 5: Attention Kernel (naive MHA)
│
Phase 6: 模型加载 (safetensors + HF config)
│ │
│ Phase 7: BPE Tokenizer (可与 Phase 6 并行)
│ │
Phase 8: GPT-2 完整推理 ◄──────────── 里程碑 ① CLI 文本生成
│
Phase 9: KV Cache + Autoregressive Generation
│
Phase 10: Qwen3-7B 支持 ◄─────────── 里程碑 ② 7B 模型推理
│
Phase 11: Paged Attention + KV Cache Manager
│
Phase 12: Continuous Batching + Request Scheduler
│
Phase 13: HTTP API + SSE Streaming ◄── 里程碑 ③ 端到端 API 可用
│
Phase 14: Flash Attention v2
│
Phase 15: 性能优化 ◄──────────────── 里程碑 ④ 50% vLLM throughput
│
Phase 16: Speculative Decoding
│
Phase 17: Tensor Parallelism (TP=1/2/4) ◄── 里程碑 ⑤ 多卡推理
│
Phase 18: 量化 (FP8 / INT8)
│
Phase 19: Multimodal ◄────────────── 里程碑 ⑥ 视觉问答
Phase 0: 项目脚手架 + 环境验证
目标: 搭建 Cargo workspace,验证 CUDA 工具链,确保开发环境就绪。
技术要点:
- Cargo workspace 配置,所有 crate 共享依赖版本
build.rs中用cccrate 编译.cu文件的 pipeline- 验证 CUDA toolkit 版本: 已确认 CUDA 12.9 (
/usr/local/cuda-12.9) - 验证 GPU compute capability: 已确认 CC 12.0 (Blackwell)
- 确认
nvcc在 PATH 中 (需要export PATH=/usr/local/cuda-12.9/bin:$PATH) 运行已确认: 纯 PCIe Gen5, 无 NVLinknvidia-smi topo -m确认互联拓扑
外部依赖: cc crate(编译 CUDA)
测试验收:
cargo build通过- 一个最小的
.cukernel(向量加法)能从 Rust 调用并返回正确结果 - 输出 GPU 信息(名称、显存、compute capability)
设计文档: docs/01-cuda-ffi.md(与 Phase 1 合并)
Phase 1: CUDA FFI 基础设施
Crate: xserv-cuda
目标: 封装 CUDA Runtime API,提供安全的 Rust 抽象层。
模块划分
xserv-cuda/src/
├── lib.rs
├── error.rs # CudaError 类型, cudaGetLastError 封装
├── device.rs # Device 枚举, 设备查询 (属性/数量/当前设备)
├── context.rs # CUDA context 管理
├── stream.rs # CudaStream (异步操作流)
├── memory.rs # GPU 内存分配/释放/拷贝 (H2D, D2H, D2D)
├── allocator.rs # Caching Allocator (显存池)
└── module.rs # cuModuleLoad (加载 PTX/cubin, 可选)
关键技术点
-
FFI 绑定策略:
- 手写
extern "C"绑定核心 CUDA Runtime API(~30 个函数) - 不用 bindgen,保持可控和可读
- 需要绑定的 API:
cudaMalloc,cudaFree,cudaMemcpy,cudaMemcpyAsync,cudaStreamCreate,cudaStreamSynchronize,cudaGetDeviceProperties,cudaSetDevice,cudaDeviceSynchronize,cudaGetLastError等
- 手写
-
GpuBuffer 抽象:
pub struct GpuBuffer { ptr: *mut c_void, size_bytes: usize, device: usize, } impl Drop for GpuBuffer { fn drop(&mut self) { /* cudaFree or return to allocator */ } }Droptrait 自动释放,防止 GPU 内存泄漏- 不实现
Clone(显式copy_from代替)
-
Caching Allocator:
- 维护 free list(按大小分桶,桶边界: 512B, 1KB, 2KB, ..., 1GB)
alloc(size): 在对应桶中找 >= size 的 free block,miss 时cudaMallocfree(ptr, size): 不调cudaFree,放回 free listtrim(): 真正释放所有 free blocks(OOM 恢复时用)- 这是性能关键组件——频繁
cudaMalloc/cudaFree会严重影响 throughput - 参考: PyTorch 的
CUDACachingAllocator设计
-
Stream 管理:
- 每个 stream 是独立的 GPU 执行队列
- Kernel launch 和 memcpy 是异步的(提交到 stream 后立即返回)
stream.synchronize()等待该 stream 上所有操作完成- 后续用于 overlap compute 和 memory transfer
-
Error Handling:
#[derive(Debug)] pub enum CudaError { OutOfMemory, InvalidDevice, LaunchFailure, Raw { code: i32, message: String }, } // 所有 CUDA 调用包装为 Result pub(crate) fn check(code: cudaError_t) -> Result<(), CudaError>;
测试验收
- 分配 1GB GPU 内存,H2D 拷贝一个大数组,D2H 拷回,验证数据一致
- Caching allocator: alloc → free → re-alloc same size,第二次不触发
cudaMalloc(通过内部计数验证) - 多 stream 并发拷贝两个数组,验证结果正确
- 设备查询: 打印 GPU name, total memory, compute capability, SM count
- Benchmark: caching allocator vs 裸
cudaMalloc的分配延迟对比(100 次 alloc/free 循环)
Phase 2: Tensor 抽象层
Crate: xserv-tensor
目标: 实现核心 Tensor 类型,支持 CPU/GPU 存储、多种数据类型、视图操作。
核心数据结构
// --- 数据类型 ---
#[derive(Clone, Copy, PartialEq)]
pub enum DType {
F32,
F16,
BF16,
// 后期: U8, I8, F8E4M3, F8E5M2
}
impl DType {
pub fn size_bytes(&self) -> usize; // F32=4, F16=2, BF16=2
}
// --- 设备 ---
#[derive(Clone, Copy, PartialEq)]
pub enum Device {
Cpu,
Cuda(usize), // device ordinal
}
// --- 存储 ---
// 引用计数,支持 view(多个 Tensor 共享同一 Storage)
pub struct Storage(Arc<StorageInner>);
enum StorageInner {
Cpu { data: Vec<u8> },
Cuda { buffer: GpuBuffer, device: usize },
}
// --- Tensor ---
pub struct Tensor {
storage: Storage,
shape: SmallVec<[usize; 4]>, // 维度(大多数 tensor <= 4D)
strides: SmallVec<[usize; 4]>, // 步长(以元素为单位)
offset: usize, // storage 中的起始偏移(元素数)
dtype: DType,
device: Device,
}
关键技术点
-
Strided Layout:
- 支持
transpose,slice,permute等操作不拷贝数据,只改 strides/offset is_contiguous(): strides 从右到左依次为 1, shape[-1], shape[-1]*shape[-2], ...- 非 contiguous tensor 在送入 CUDA kernel 前需要
contiguous()拷贝为连续布局 - 例:
[3,4]tensor 的 strides =[4, 1];transpose 后 shape=[4,3], strides=[1, 4]
- 支持
-
BF16/F16 在 Rust 中的表示:
- 使用
halfcrate 的bf16和f16类型 - GPU kernel 中使用
__nv_bfloat16/__half - Tensor 内部存储为 raw bytes,通过 DType dispatch 解释
- 使用
-
设备间拷贝:
impl Tensor { pub fn to(&self, device: Device) -> Tensor; // CPU↔GPU 拷贝 pub fn to_dtype(&self, dtype: DType) -> Tensor; // 类型转换 } -
基础操作(此阶段实现):
- 创建:
zeros,ones,from_slice,rand,full,arange - 形状:
reshape,view,transpose,squeeze,unsqueeze,contiguous - 逐元素 (CPU + GPU kernel):
add,mul,sub,div - 广播 (Broadcasting): NumPy 语义,维度从尾部对齐
- 归约:
sum,max,mean(沿指定轴)
- 创建:
-
Op Dispatch 机制:
// 根据 device 和 dtype dispatch 到不同实现 pub fn add(a: &Tensor, b: &Tensor) -> Tensor { match (a.device(), b.device()) { (Device::Cpu, Device::Cpu) => cpu_ops::add(a, b), (Device::Cuda(_), Device::Cuda(_)) => cuda_ops::add(a, b), _ => panic!("device mismatch"), } }
测试验收
- 创建 tensor, reshape, transpose, slice,验证 shape/strides 计算正确
- 广播加法:
[3,1] + [1,4]→[3,4],与 numpy 结果对比 - CPU ↔ GPU 拷贝往返,数据一致
- BF16 tensor 的基础运算精度验证(与 FP32 结果对比 relative error)
- View 共享存储: 修改 view 的数据,原 tensor 也应变化
- Benchmark: GPU 逐元素 kernel vs CPU 的加速比(大数组)
Phase 3: GEMM — 矩阵乘法
Crate: xserv-kernels
CUDA 源码: csrc/gemm/
目标: 实现 GEMM 的多个版本,从 naive 到 tensor core,同时封装 cuBLAS,建立 benchmark 对比框架。
这是 CUDA kernel 编程的第一个"修罗场",会深刻理解 GPU 编程的核心概念。
实现路线(4 个递进版本)
Version 1: Naive GEMM
- 每个 thread 计算输出矩阵 C 的一个元素:
C[i][j] = sum(A[i][k] * B[k][j]) - grid 维度:
(M/BLOCK, N/BLOCK), block 维度:(BLOCK, BLOCK) - 学到: grid/block 维度规划, global memory access pattern
- 问题: global memory 访问完全没有局部性,bandwidth 利用率极低
- 预期性能: ~1-2% cuBLAS
Version 2: Tiled GEMM (shared memory)
- 将 A, B 分成 TILE×TILE 的小块,加载到 shared memory
- 每个 thread block 计算 C 的一个 TILE×TILE 输出块
- 内层循环沿 K 维度滑动 tile
- 学到: shared memory 使用,
__syncthreads(), bank conflict, memory coalescing - 关键: A 的 tile 要按行加载(coalesced),B 的 tile 按列访问需要注意 bank conflict
- 预期性能: ~10-20% cuBLAS
Version 3: Register Tiling + 向量化
- 每个 thread 计算多个输出元素(如 4×4 或 8×8)
- 使用寄存器存储中间结果,减少 shared memory 访问
- 向量化加载:
float4一次读 128 bit - 学到: register pressure, ILP (Instruction-Level Parallelism), occupancy vs. ILP tradeoff
- 预期性能: ~30-50% cuBLAS
Version 4: Tensor Core GEMM (WMMA)
- 使用 CUDA WMMA API 调用 Tensor Core
- BF16 输入, FP32 累加
- 每次 wmma::mma_sync 计算 16×16×16 矩阵乘
- 学到: WMMA fragment layout, Tensor Core 编程模型, warp-level 协作
- 关键: 5090 Blackwell (CC 12.0) 的 Tensor Core 支持 BF16 和 FP8
- 预期性能: ~60-80% cuBLAS
cuBLAS 封装
// 需要封装的 cuBLAS API
extern "C" {
fn cublasCreate_v2(handle: *mut cublasHandle_t) -> cublasStatus_t;
fn cublasSetStream_v2(handle: cublasHandle_t, stream: cudaStream_t) -> cublasStatus_t;
fn cublasGemmEx(
handle: cublasHandle_t,
transa: cublasOperation_t, transb: cublasOperation_t,
m: i32, n: i32, k: i32,
alpha: *const c_void,
A: *const c_void, Atype: cudaDataType, lda: i32,
B: *const c_void, Btype: cudaDataType, ldb: i32,
beta: *const c_void,
C: *mut c_void, Ctype: cudaDataType, ldc: i32,
computeType: cublasComputeType_t,
algo: cublasGemmAlgo_t,
) -> cublasStatus_t;
}
支持: BF16×BF16→BF16 (compute=FP32), FP16×FP16→FP16, FP32×FP32→FP32
Kernel Registry(运行时可切换 backend)
#[derive(Clone, Copy)]
pub enum GemmBackend {
Naive,
Tiled,
RegisterTiled,
TensorCore,
CuBlas,
}
pub fn matmul(a: &Tensor, b: &Tensor, backend: GemmBackend) -> Tensor;
// 全局默认 backend(可配置)
pub fn set_default_gemm_backend(backend: GemmBackend);
测试验收
- 正确性: 所有 5 个 backend 的输出与 cuBLAS 对比,max absolute error < 1e-3 (BF16)
- Benchmark 表格(用
criterioncrate):
| Backend | M=N=K=1024 | M=N=K=4096 | % of cuBLAS |
|---|---|---|---|
| Naive | ms | ms | % |
| Tiled | ms | ms | % |
| RegisterTiled | ms | ms | % |
| TensorCore | ms | ms | % |
| cuBLAS | ms | ms | 100% |
- Profile: 用
nsys/ncu分析 naive vs tiled 的 memory throughput 差异 - 非方阵测试: M=1, N=4096, K=4096 (decode 阶段的典型 shape)
Phase 4: Transformer 核心 Kernels
Crate: xserv-kernels
CUDA 源码: csrc/normalization/, csrc/activation/, csrc/embedding/, csrc/reduce/
目标: 实现 Transformer 所需的所有非 Attention 算子,每个都有自定义 CUDA kernel。
Kernel 清单
| Kernel | 用途 | CUDA 文件 | 核心优化点 |
|---|---|---|---|
| LayerNorm | GPT-2 | normalization/layernorm.cu |
Online Welford 算法, warp reduce, 向量化加载 |
| RMSNorm | Qwen3/LLaMA | normalization/rmsnorm.cu |
比 LayerNorm 简单(无 mean), rsqrt |
| GELU | GPT-2 激活 | activation/gelu.cu |
tanh 近似 vs 精确, 向量化 |
| SiLU (Swish) | Qwen3 激活 | activation/silu.cu |
x * sigmoid(x), 逐元素 |
| SwiGLU | Qwen3 FFN | activation/swiglu.cu |
SiLU(gate) * up, fused 逐元素 |
| Embedding | token→vector | embedding/embedding.cu |
Gather 操作, coalesced access |
| RoPE | Qwen3 位置编码 | embedding/rope.cu |
复数旋转, precompute freq |
| Softmax | Attention 内 | reduce/softmax.cu |
Online safe softmax, 数值稳定 |
| Argmax | Greedy sampling | reduce/argmax.cu |
Parallel reduction |
| TopK | TopK sampling | reduce/topk.cu |
Bitonic sort 或 radix select |
关键学习主题
Reduction Pattern(核心中的核心):
LayerNorm, RMSNorm, Softmax 都涉及对某个维度求和/求最大值。GPU reduction 是分层的:
Thread-level: 每个 thread 处理多个元素,本地累加
↓
Warp-level: __shfl_down_sync() 在 warp (32 threads) 内规约
↓
Block-level: shared memory 存各 warp 的结果,再规约
↓
Grid-level: (如果需要) atomic 或两遍 kernel
对于 Norm/Softmax,通常 hidden_dim <= 8192,一个 block 就够,不需要 grid-level reduction。
向量化内存访问:
float4(128-bit) 一次加载 4 个 float 或 8 个 bf16__nv_bfloat162一次处理 2 个 bf16- 提升 memory throughput,减少 load/store 指令数
每个 kernel 都实现两个版本:
- Custom CUDA kernel(自己写,深入理解)
- Reference 实现(简单的 Python/numpy,生成 reference output 用于验证)
测试验收
- 每个 kernel 的输出与 PyTorch 参考实现对比
- 写一个
tools/generate_reference.py脚本,为每个 op 生成 reference input/output,保存为.npy - Rust 测试中加载
.npy对比
- 写一个
- 数值精度: BF16 下 max relative error < 1e-2, FP32 下 < 1e-5
- RoPE: 验证旋转后的向量与 HF transformers 的
apply_rotary_pos_emb结果一致 - Softmax: 验证
sum(output, dim=-1) == 1.0,验证 numerical stability(大值输入不 overflow) - Benchmark: 每个 kernel 与 PyTorch 对应操作的延迟对比
Phase 5: Attention Kernel (Naive 版)
Crate: xserv-kernels
CUDA 源码: csrc/attention/naive_attention.cu
目标: 实现标准 Multi-Head Attention,不做 Flash/Paged 优化。理解 attention 机制的计算基础。
计算流程
Input: Q [B, H, S, D], K [B, H, S, D], V [B, H, S, D]
其中 B=batch, H=num_heads, S=seq_len, D=head_dim
1. scores = Q @ K^T / sqrt(D) → [B, H, S, S]
2. scores = scores + causal_mask → 上三角置为 -inf
3. weights = softmax(scores, dim=-1) → [B, H, S, S]
4. output = weights @ V → [B, H, S, D]
实现方式
方式一: 组合式(先跑通)
- 用 Phase 3 的 GEMM (Q@K^T) + Phase 4 的 Softmax + GEMM (weights@V)
- 简单但 materialize 了 S×S 矩阵,内存 O(S²)
方式二: Fused kernel(理解 Flash Attention 的前置)
- 一个 kernel 完成整个 attention
- 仍然 materialize S×S(不做 tiling),但减少 kernel launch 和 global memory 读写次数
Causal Mask
- 不显式构造 mask 矩阵(浪费内存)
- 在 softmax 前对
scores[i][j]wherej > i写-inf(-1e9for BF16) - 编译期条件判断:
if (col > row) score = -inf;
GQA 预备
- 本阶段实现标准 MHA:
num_kv_heads == num_heads - Phase 10 扩展为 GQA:
num_kv_heads < num_heads - GQA 时 K/V 需要 repeat: 每个 KV head 服务
num_heads / num_kv_heads个 Q head - 实际实现: 不真正 repeat 数据,在 kernel 中用
kv_head_idx = q_head_idx / num_groups索引
测试验收
- 随机 Q, K, V,输出与 PyTorch
F.scaled_dot_product_attention(is_causal=True)对比 - 验证 causal mask: attention weight 矩阵的上三角全为 0
- Benchmark 表(记录为 Flash Attention 的 baseline):
| Seq Length | Latency (ms) | GPU Memory (MB) |
|---|---|---|
| 128 | ||
| 512 | ||
| 2048 | ||
| 4096 | ||
| 8192 | OOM? |
- 输出 attention weights 可视化(小规模,验证 causal pattern)
Phase 6: 模型加载
Crate: xserv-model
目标: 从 HuggingFace safetensors 文件加载模型权重到 GPU Tensor。
核心组件
1. safetensors 解析
- 使用
safetensorscrate 读取文件 - 文件结构: 8 bytes header_size + JSON header + raw tensor data
- 支持 mmap 零拷贝读取
- 支持 sharded 文件:
model-00001-of-00003.safetensors- 通过
model.safetensors.index.json查找 tensor → file 的映射
- 通过
2. HF Config 解析
#[derive(Deserialize)]
pub struct ModelConfig {
pub architectures: Vec<String>,
pub hidden_size: usize,
pub intermediate_size: usize,
pub num_attention_heads: usize,
pub num_key_value_heads: usize, // GQA: 可能 < num_attention_heads
pub num_hidden_layers: usize,
pub vocab_size: usize,
pub max_position_embeddings: usize,
pub rms_norm_eps: f64, // Qwen3 用
pub rope_theta: f64, // RoPE base frequency
pub tie_word_embeddings: bool,
// ... 其他字段按模型按需添加
}
3. 权重映射
HuggingFace 命名规范 (以 Qwen3 为例):
model.embed_tokens.weight → embedding
model.layers.{i}.self_attn.q_proj.weight → layer[i].attn.q_proj
model.layers.{i}.self_attn.k_proj.weight → layer[i].attn.k_proj
model.layers.{i}.self_attn.v_proj.weight → layer[i].attn.v_proj
model.layers.{i}.self_attn.o_proj.weight → layer[i].attn.o_proj
model.layers.{i}.mlp.gate_proj.weight → layer[i].mlp.gate
model.layers.{i}.mlp.up_proj.weight → layer[i].mlp.up
model.layers.{i}.mlp.down_proj.weight → layer[i].mlp.down
model.layers.{i}.input_layernorm.weight → layer[i].attn_norm
model.layers.{i}.post_attention_layernorm.weight → layer[i].ffn_norm
model.norm.weight → final_norm
lm_head.weight → lm_head
4. 加载流程
safetensors file (disk)
→ mmap (host memory, 零拷贝)
→ dtype check/cast (如 FP32 → BF16)
→ H2D copy → GPU Tensor
→ 按 layer 组织成模型结构
外部依赖
safetensorscrateserde+serde_json(解析 config.json)memmap2(mmap 支持,safetensors crate 可能内置)
测试验收
- 加载 GPT-2 124M (
openai-community/gpt2),打印所有 tensor name, shape, dtype - 抽查几个 tensor 的前 10 个值,与 PyTorch
from_pretrained对比 - 加载 Qwen3-7B sharded 权重,验证所有 tensor 都成功加载
- 性能: 测量 7B 模型权重加载时间 (mmap → GPU 全流程)
- 错误处理: 缺少 tensor、dtype 不匹配、文件不存在等情况
Phase 7: BPE Tokenizer
Crate: xserv-tokenizer
目标: 从零实现 Byte-Pair Encoding tokenizer,兼容 HuggingFace tokenizer.json 格式。
BPE 算法核心
编码 (encode)
输入: "Hello world"
→ pre-tokenize (regex split): ["Hello", " world"]
→ 每个词转为 byte 序列: [72, 101, 108, 108, 111], [32, 119, 111, ...]
→ 初始 token 序列: 每个 byte 是一个 token
→ 反复合并:
1. 找当前序列中优先级最高的 byte-pair (从 merges 表查)
2. 合并该 pair
3. 重复直到无可合并
→ 输出: token IDs
解码 (decode)
token IDs → 查 vocab 得到 byte 序列 → 拼接 → UTF-8 decode → 文本
需要处理的细节
-
Pre-tokenization:
- GPT-2 regex:
's|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+ - Qwen3 可能使用不同的 regex pattern(从
tokenizer.json的pre_tokenizer字段读取) - 用
regexcrate 实现
- GPT-2 regex:
-
tokenizer.json 解析:
{ "model": { "type": "BPE", "vocab": {"Hello": 0, "world": 1, ...}, "merges": ["H e", "He l", "Hel lo", ...] }, "added_tokens": [...], "pre_tokenizer": {...}, "post_processor": {...} } -
Special Tokens:
<|endoftext|>(GPT-2 EOS)<|im_start|>,<|im_end|>(Qwen3 ChatML)<|endoftext|>(Qwen3 EOS)- Special tokens 不参与 BPE merge,直接映射到 ID
-
Chat Template (Qwen3 格式):
<|im_start|>system You are a helpful assistant.<|im_end|> <|im_start|>user Hello<|im_end|> <|im_start|>assistant -
性能优化:
- Merge rules 用
HashMap<(TokenId, TokenId), MergePriority>预索引 - 对于长文本,考虑 priority queue 加速 pair 查找
- Merge rules 用
测试验收
- 加载 GPT-2 tokenizer,encode + decode 一批测试文本,与 Python
AutoTokenizer逐 token 对比 - 加载 Qwen3 tokenizer,同样逐 token 对比
- 边界情况: 空字符串、纯 emoji (🎉🔥)、中英混合、超长文本 (1MB)
- Chat template: 给定 messages 列表,生成与 HF
apply_chat_template一致的 token 序列 - Benchmark: encode 1MB 文本的延迟
Phase 8: GPT-2 完整推理 — 里程碑 ①
Crate: xserv-model
目标: 将所有组件串联,实现 GPT-2 的完整推理 pipeline。这是第一次看到模型"说话"。
模型结构
pub struct GPT2 {
config: GPT2Config,
wte: Tensor, // token embedding [vocab_size, hidden_size]
wpe: Tensor, // position embedding [max_seq_len, hidden_size]
layers: Vec<GPT2Block>,
ln_f: LayerNorm, // final layer norm
// lm_head 与 wte 共享权重 (tied embeddings)
}
pub struct GPT2Block {
ln_1: LayerNorm,
attn: GPT2Attention, // MHA: q_proj, k_proj, v_proj, o_proj
ln_2: LayerNorm,
mlp: GPT2MLP, // fc1 (4H) → GELU → fc2 (H)
}
Forward Pass 流程
tokens [B, S]
→ wte[tokens] + wpe[0..S] → hidden [B, S, 768]
→ for each layer:
→ residual = hidden
→ hidden = ln_1(hidden)
→ hidden = attention(hidden) # Q, K, V 从 hidden 线性变换
→ hidden = hidden + residual # residual connection
→ residual = hidden
→ hidden = ln_2(hidden)
→ hidden = mlp(hidden) # Linear→GELU→Linear
→ hidden = hidden + residual
→ hidden = ln_f(hidden)
→ logits = hidden @ wte.T → [B, S, vocab_size]
→ next_token = sample(logits[:, -1, :]) # 只取最后一个 position
Sampling 策略
pub struct SamplingParams {
pub temperature: f32, // default 1.0
pub top_k: usize, // default 50
pub top_p: f32, // default 1.0 (disabled)
pub max_tokens: usize, // default 256
pub repetition_penalty: f32, // default 1.0 (disabled)
}
实现:
- Greedy:
argmax(logits) - Temperature:
logits = logits / temperature→ softmax → sample - Top-K: 保留 top-k logits,其余置为 -inf → softmax → sample
- Top-P (Nucleus): 按概率降序排列,累加到概率 >= p → 截断 → 重新 normalize → sample
- 以上可以组合: temperature → top-k → top-p → sample
CLI 交互
$ cargo run --release --bin xserv-cli -- --model openai-community/gpt2
xserv> The future of AI is
GPT-2> The future of AI is not just about the technology, but about the people
who are building it. The question is whether we can...
xserv> Once upon a time
GPT-2> Once upon a time, there was a young man who lived in a small village...
测试验收
- 加载
openai-community/gpt2,prefill "The future of AI is" - Prefill logits 与 PyTorch 对比: top-5 token IDs 和对应 logit 值一致
- Greedy decode 50 tokens,结果应该是连贯英文
- Temperature/TopK/TopP sampling: 生成多次结果应有变化
- CLI 交互模式可用
Phase 9: KV Cache + Autoregressive 优化
Crate: xserv-runtime
目标: 实现 KV Cache,将 decode 从 O(S²) 降到 O(S) per step。
核心概念: Prefill vs Decode
Prefill(首次处理 prompt):
- 输入: 完整 prompt
[B, S, D] - 计算: 所有 token 的 Q, K, V
- Attention:
Q[B,H,S,D] @ K[B,H,S,D]^T→ 完整 S×S 矩阵 - 输出: 缓存 K, V 到 KV cache
- 特点: Compute-bound(大矩阵乘法)
Decode(逐 token 生成):
- 输入: 上一步生成的 1 个 token
[B, 1, D] - 计算: 只计算新 token 的 Q, K, V
- K_new, V_new append 到 cache
- Attention:
Q[B,H,1,D] @ K_cache[B,H,S+1,D]^T→[B,H,1,S+1] - 特点: Memory-bound(Q 只有 1 行,瓶颈在读 K/V cache)
KV Cache 设计(简单版,非 paged)
pub struct KVCache {
// 每层一对 K/V tensor
// shape: [batch_size, num_kv_heads, max_seq_len, head_dim]
k_caches: Vec<Tensor>, // 索引 = layer_idx
v_caches: Vec<Tensor>,
seq_len: usize, // 当前已填充的长度
max_seq_len: usize, // 预分配的最大长度
}
impl KVCache {
// prefill 时: 写入 [0..prompt_len] 的 K, V
pub fn fill(&mut self, layer: usize, k: &Tensor, v: &Tensor);
// decode 时: 在 seq_len 位置写入新的 K, V,返回完整 cache
pub fn append(&mut self, layer: usize, k: &Tensor, v: &Tensor) -> (&Tensor, &Tensor);
}
Decode Attention Kernel
与 prefill attention 不同,decode 时 Q 只有 1 行:
Q [B, H, 1, D] × K_cache [B, H, S, D]^T → scores [B, H, 1, S]
→ softmax → weights [B, H, 1, S]
weights × V_cache [B, H, S, D] → output [B, H, 1, D]
优化方向:
- 每个 warp 处理一个 head
- 沿 S 维度做 parallel reduction (dot product + online softmax)
- 重点优化 memory bandwidth(K/V cache 的读取是瓶颈)
测试验收
- 对比有/无 KV cache 的生成结果 → 必须完全一致(bit-exact for greedy)
- Benchmark decode 延迟:
| Seq Length | Without Cache (ms/token) | With Cache (ms/token) | Speedup |
|---|---|---|---|
| 128 | |||
| 512 | |||
| 2048 |
- 显存占用: KV cache 的实际显存与理论值 (
2 * num_layers * num_kv_heads * seq_len * head_dim * sizeof(bf16)) 对比 - GPT-2 decode throughput (tokens/s) 记录为 baseline
Phase 10: Qwen3-7B 支持 — 里程碑 ②
Crate: xserv-model
目标: 扩展模型定义以支持 Qwen3-7B,验证输出正确性。
架构对比
| 特性 | GPT-2 (124M) | Qwen3-7B |
|---|---|---|
| Normalization | LayerNorm (pre-LN) | RMSNorm (pre-LN) |
| Position Encoding | Learned absolute (wpe) | RoPE (无单独参数) |
| Attention | MHA (12 heads, 12 KV heads) | GQA (如 32 Q heads, 8 KV heads) |
| Activation | GELU | SwiGLU (SiLU gate) |
| FFN | Linear(H→4H) → GELU → Linear(4H→H) | gate_proj + up_proj → SiLU gate → down_proj |
| Vocab Size | 50,257 | ~152,000 |
| Hidden Size | 768 | 3,584 (7B) |
| Layers | 12 | 28 |
| Tied Embeddings | Yes | No |
需要新增/修改的组件
1. GQA (Grouped Query Attention)
num_heads = 32, num_kv_heads = 8
每个 KV head 服务 32/8 = 4 个 Q head
Q: [B, 32, S, 128]
K: [B, 8, S, 128] ← 只有 8 个 KV heads
V: [B, 8, S, 128]
Attention 时:
kv_head_idx = q_head_idx / (num_heads / num_kv_heads)
不需要真正 repeat K/V 数据,kernel 中做索引映射
2. RoPE (Rotary Position Embedding)
对 Q, K 的每对相邻元素做旋转:
Q' = Q * cos(θ) + rotate_half(Q) * sin(θ)
K' = K * cos(θ) + rotate_half(K) * sin(θ)
其中 θ_i = pos / (rope_theta^(2i/d))
预计算: freqs = 1.0 / (rope_theta^(2i/d)) for i in 0..d/2
运行时: cos_cache[pos][i] = cos(pos * freqs[i])
3. SwiGLU FFN
x → gate_proj(x) → SiLU → ⊙ up_proj(x) → down_proj → output
三个 Linear:
gate_proj: [hidden_size, intermediate_size]
up_proj: [hidden_size, intermediate_size]
down_proj: [intermediate_size, hidden_size]
模型结构
pub struct Qwen3Model {
config: Qwen3Config,
embed_tokens: Tensor, // [vocab_size, hidden_size]
layers: Vec<Qwen3DecoderLayer>,
norm: RMSNorm, // final RMSNorm
lm_head: Tensor, // [vocab_size, hidden_size] (not tied)
}
pub struct Qwen3DecoderLayer {
input_layernorm: RMSNorm,
self_attn: Qwen3Attention, // GQA with RoPE
post_attention_layernorm: RMSNorm,
mlp: Qwen3MLP, // SwiGLU
}
显存预算 (BF16, 单卡 5090 32GB)
模型权重: 7B × 2B = ~14 GB
KV cache: 28 layers × 2(KV) × 8 heads × 4096 tokens × 128 dim × 2B ≈ 4.5 GB
Activation (单请求): ~1 GB
────────────────────────
总计: ~19.5 GB (单请求),剩余 ~12 GB 可用于更多并发
测试验收
- 加载 Qwen3-7B 权重到单张 5090,打印模型结构和参数量
- Prefill logits 与 HF transformers 对比: 输入 "你好" → top-5 logits 一致
- 英文生成: "What is the capital of France?" → 生成合理回答
- 中文生成: "请介绍一下量子计算" → 生成通顺中文
- 多轮对话:
验证 chat template 格式正确
<|im_start|>user\nHello<|im_end|>\n<|im_start|>assistant\n - 单请求性能 baseline: prefill latency (ms), decode throughput (tokens/s)
Phase 11: Paged Attention + KV Cache Manager
Crate: xserv-runtime
CUDA 源码: csrc/attention/paged_attention.cu
目标: 实现 vLLM 的核心创新 — PagedAttention,解决 KV cache 内存碎片化问题。
问题
Phase 9 的简单 KV cache 为每个请求预分配 max_seq_len 的连续内存:
- 请求只用了 100 tokens 但占了 4096 tokens 的空间 → 内存利用率低
- 连续分配导致碎片化 → 并发请求数受限
PagedAttention 设计
核心思想: 像操作系统的虚拟内存一样,将 KV cache 分成固定大小的 page (block)。
pub const BLOCK_SIZE: usize = 16; // 每个 block 存 16 个 token 的 KV
// 物理 KV cache: 预分配的大块 GPU 内存
// k_cache shape: [num_physical_blocks, num_kv_heads, block_size, head_dim]
// v_cache shape: [num_physical_blocks, num_kv_heads, block_size, head_dim]
pub struct BlockAllocator {
free_blocks: Vec<usize>, // 空闲物理 block ID 列表
num_total_blocks: usize,
ref_counts: Vec<usize>, // 每个 block 的引用计数 (CoW 用)
}
pub struct BlockTable {
// logical_block_idx → physical_block_idx
// 例: 一个 seq_len=50 的请求有 4 个 block (50/16=3.125, 向上取整)
blocks: Vec<usize>,
}
pub struct PagedKVCacheManager {
k_cache: Tensor, // 所有物理 blocks
v_cache: Tensor,
allocator: BlockAllocator,
block_tables: HashMap<SeqId, BlockTable>,
}
Paged Attention Kernel
与普通 attention 的区别: K/V 不是连续存储,需要通过 block table 间接寻址。
输入:
Q: [num_seqs, num_heads, head_dim] (decode 时每个 seq 只有 1 个 query)
k_cache: [num_blocks, num_kv_heads, block_size, head_dim] (物理存储)
v_cache: [num_blocks, num_kv_heads, block_size, head_dim]
block_tables: [num_seqs, max_num_blocks] (间接寻址表)
seq_lens: [num_seqs] (每个 seq 的实际长度)
每个 thread block 处理:
1 个 seq 的 1 个 attention head
遍历该 seq 的所有 logical blocks
对每个 block: 查 block_table 得到 physical_block_id → 读取 K/V
online softmax 累加
输出: [num_seqs, num_heads, head_dim]
Copy-on-Write (高级,可选)
- 多个 sequence 共享相同 prefix 的 KV blocks(beam search, prompt caching)
- 写入时: 如果 ref_count > 1,先复制该 block 再修改
- 这阶段先不实现,标记为后续优化
测试验收
- 正确性: paged attention 输出与 Phase 9 简单 KV cache 完全一致
- 内存效率对比:
| 场景 | Naive KV Cache | Paged KV Cache |
|---|---|---|
| 1 req, seq=100 | 分配 4096 tokens | 分配 7 blocks (112 tokens) |
| 10 req, seq=100-500 | 10×4096 | 按需分配 |
| 最大并发数 (32GB) | N 个 | M 个 (M >> N) |
- Block allocator: alloc/free 循环,无内存泄漏
- Benchmark: paged attention kernel vs naive decode attention 延迟对比
Phase 12: Continuous Batching + Request Scheduler
Crate: xserv-runtime
目标: 实现 iteration-level 调度,支持请求的动态加入和退出。
Static Batching vs Continuous Batching
Static (朴素):
Batch 1: [req1, req2, req3] → 等 req1, req2, req3 全部完成
Batch 2: [req4, req5, req6] → ...
问题: req1 完成了但 req3 还在生成 → GPU 空转
Continuous (Orca 论文):
Iteration 1: [req1, req2, req3] → req1 完成!
Iteration 2: [req2, req3, req4] → req4 动态加入
Iteration 3: [req2, req3, req4] → req3 完成!
Iteration 4: [req2, req4, req5] → req5 动态加入
核心数据结构
#[derive(Clone, Copy, PartialEq)]
pub enum SequenceStatus {
Waiting, // 在等待队列中
Prefilling, // 正在做 prefill
Decoding, // 正在做 decode
Finished, // 已完成 (EOS / max_len / stop string)
Preempted, // 被抢占(显存不够,KV cache 被换出)
}
pub struct Sequence {
pub id: SeqId,
pub prompt_tokens: Vec<u32>,
pub generated_tokens: Vec<u32>,
pub status: SequenceStatus,
pub sampling_params: SamplingParams,
pub block_table: BlockTable,
pub arrival_time: Instant,
// 用于 streaming 输出
pub output_sender: tokio::sync::mpsc::Sender<GeneratedToken>,
}
pub struct Scheduler {
waiting: VecDeque<Sequence>,
running: Vec<Sequence>,
max_num_seqs: usize, // 最大并发 batch size
max_num_tokens: usize, // 单次 iteration 最大 token 数
block_manager: PagedKVCacheManager,
}
调度循环 (Engine 主循环)
loop {
// Step 1: 回收已完成的 sequence
// - 释放其 KV cache blocks
// - 从 running 移除
// Step 2: 检查能否 admit 新请求
// - 条件: running.len() < max_num_seqs
// && 有足够的 free blocks 给新请求的 prompt
// - FCFS 从 waiting 取
// Step 3: 划分 prefill / decode
// - 新加入的 sequence: prefill (处理完整 prompt)
// - 已在 running 的: decode (生成 1 个 token)
// Step 4: 组装 batch input
// - Prefill: 各 seq 的 prompt tokens, 需要 padding 或 ragged batch
// - Decode: 各 seq 的最后一个 token
// Step 5: Forward pass
// - prefill 和 decode 可以混合在一个 forward 中
// - 或者分开处理 (先 prefill, 再 decode)
// Step 6: Sampling
// - 对每个 seq 的 logits 进行采样
// Step 7: 更新状态
// - 将新 token append 到 sequence
// - 检查是否完成 (EOS / max_len)
// - 通过 channel 发送新 token 给 API 层
}
Preemption(显存不足时的抢占)
当显存不足以 admit 新请求时:
- Swap: 将低优先级 seq 的 KV cache 从 GPU 换到 CPU(复杂,后续再做)
- Recompute: 丢弃低优先级 seq 的 KV cache,后续重新 prefill(简单,先实现这个)
测试验收
- 模拟 10 个请求在不同时间到达,所有请求都得到正确的生成结果
- 短请求完成后,新请求立即加入 batch(观察 log)
- Throughput 对比:
| 方式 | 20 请求总耗时 | Token/s |
|---|---|---|
| 串行 (batch=1) | ||
| Static batch=4 | ||
| Continuous batch |
- 压力测试: 100 个并发请求,全部正确完成,无 hang/crash
Phase 13: HTTP API + SSE Streaming — 里程碑 ③
Crate: xserv-engine, xserv-api
目标: 提供 OpenAI 兼容的 HTTP API,支持 SSE streaming。第一个端到端可用的里程碑。
技术栈
- HTTP:
axum(Rust async web framework) - Async:
tokio - JSON:
serde_json - SSE:
axum内置 SSE 支持 (axum::response::sse)
API 端点
POST /v1/chat/completions # 主要端点 (ChatML 格式)
POST /v1/completions # 纯文本补全
GET /v1/models # 列出可用模型
GET /health # 健康检查
请求/响应格式 (OpenAI 兼容)
Chat Completion Request:
{
"model": "qwen3-7b",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is 1+1?"}
],
"stream": true,
"temperature": 0.7,
"top_p": 0.9,
"max_tokens": 256,
"stop": ["\n\n"]
}
SSE Streaming Response:
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1234567890,"model":"qwen3-7b","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1234567890,"model":"qwen3-7b","choices":[{"index":0,"delta":{"content":"The"},"finish_reason":null}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1234567890,"model":"qwen3-7b","choices":[{"index":0,"delta":{"content":" answer"},"finish_reason":null}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1234567890,"model":"qwen3-7b","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
Non-streaming Response:
{
"id": "chatcmpl-xxx",
"object": "chat.completion",
"created": 1234567890,
"model": "qwen3-7b",
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": "The answer is 2."},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 25,
"completion_tokens": 8,
"total_tokens": 33
}
}
架构分层
Client (curl / Python OpenAI SDK)
│
▼
┌─────────────────────────────────────┐
│ xserv-api (axum HTTP server) │
│ - 解析请求, 验证参数 │
│ - apply chat template │
│ - 将请求提交给 engine │
│ - 从 channel 接收 token, 编码为 SSE│
└────────────┬────────────────────────┘
│ InferenceRequest (通过 channel)
▼
┌─────────────────────────────────────┐
│ xserv-engine (推理引擎) │
│ - 独立的 OS thread (非 async) │
│ - 运行 scheduler 调度循环 │
│ - 管理 model + KV cache │
│ - 每生成一个 token, 通过 channel │
│ 发送给 API 层 │
└─────────────────────────────────────┘
关键设计决策:
- Engine 跑在独立 OS thread(避免 GPU 同步操作 block tokio runtime)
- API ↔ Engine 通过
tokio::sync::mpscchannel 通信 - 每个请求有独立的
mpsc::Sender/Receiver用于 token streaming
测试验收
-
curl测试:curl http://localhost:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"qwen3-7b","messages":[{"role":"user","content":"Hello"}],"stream":true}'看到 SSE 逐 token 输出
-
Python OpenAI SDK 测试:
from openai import OpenAI client = OpenAI(base_url="http://localhost:8080/v1", api_key="unused") for chunk in client.chat.completions.create( model="qwen3-7b", messages=[{"role": "user", "content": "What is 1+1?"}], stream=True ): print(chunk.choices[0].delta.content or "", end="", flush=True) -
非 streaming 模式也能正常工作
-
并发 10 个请求,全部正确完成
-
多轮对话: 连续发两轮消息(第二轮包含 history),验证上下文连贯
-
/v1/models返回已加载的模型列表 -
错误处理: 无效参数返回 400, 模型不存在返回 404
Phase 14: Flash Attention v2
Crate: xserv-kernels
CUDA 源码: csrc/attention/flash_attention.cu
目标: 实现 Flash Attention v2 的 CUDA kernel,大幅降低 attention 的显存占用并提升速度。
核心思想
标准 attention 的问题:
S = Q @ K^T ← 需要 O(S²) 显存存储 S×S 矩阵
P = softmax(S) ← 需要完整的 S×S 才能做 softmax
O = P @ V
Flash Attention 的解法:
- 不 materialize S×S 矩阵
- 将 Q, K, V 分成 tiles,在 SRAM (shared memory) 中计算
- 使用 online softmax trick: 边算边更新 running max 和 running sum
算法 (Forward Pass)
Br, Bc = tile sizes for Q and K/V respectively
for each Q tile (q_start..q_start+Br):
load Q_tile [Br, D] to shared memory
initialize: O_tile = 0, l = 0, m = -inf // running sum and max
for each K,V tile (kv_start..kv_start+Bc):
load K_tile [Bc, D], V_tile [Bc, D] to shared memory
// Compute attention scores for this tile pair
S_tile = Q_tile @ K_tile^T // [Br, Bc], in registers/SRAM
// Apply causal mask (skip if kv_start > q_start + Br)
if causal: mask upper triangle of S_tile
// Online softmax update
m_new = max(m, rowmax(S_tile)) // new running max
P_tile = exp(S_tile - m_new) // safe exp
l_new = exp(m - m_new) * l + rowsum(P_tile) // update running sum
O_tile = diag(exp(m - m_new)) * O_tile + P_tile @ V_tile
m = m_new
l = l_new
O_tile = O_tile / l // final normalization
write O_tile [Br, D] to global memory (HBM)
实现要点
-
Tile 大小选择:
- 受限于 shared memory (5090 Blackwell CC 12.0: 需要实测确认 per-SM shared memory 上限)
- 需要同时存 Q_tile, K_tile, V_tile, S_tile
- 典型值: Br=Bc=128 for D=128, BF16
-
Causal mask 优化:
- 如果 K/V tile 完全在 Q tile 的"未来"(kv_start > q_end)→ 跳过整个 tile
- 减少约 50% 的计算量
-
BF16 精度:
- S_tile, P_tile 的计算在 FP32 中进行(累加精度)
- Q, K, V 的加载用 BF16(节省 bandwidth)
- 最终 O 转回 BF16 写出
-
与 Paged Attention 的结合:
- Flash Attention 的 K/V tile 遍历逻辑需要适配间接寻址
- 每个 tile 查 block_table 得到物理地址
- 这是 "Flash-Decoding" / "FlashInfer" 的核心
测试验收
- 正确性: 与 Phase 5 naive attention 对比, max error < 1e-2 (BF16)
- 显存: Flash Attention 不随 S 平方增长
| Seq Length | Naive VRAM | Flash VRAM | Naive Time | Flash Time |
|---|---|---|---|---|
| 512 | MB | MB | ms | ms |
| 2048 | MB | MB | ms | ms |
| 8192 | OOM? | MB | OOM? | ms |
| 32768 | OOM | MB | OOM | ms |
- 集成到 Qwen3-7B,端到端 decode latency 对比
- Profile:
ncu分析 compute utilization, memory throughput
Phase 15: 性能优化 — 里程碑 ④
目标: 系统性 profiling + 优化,向 50% vLLM throughput 目标冲刺。
优化方向
1. Kernel Fusion
减少 memory-bound kernel 之间的 global memory 读写:
- Residual Add + RMSNorm: 一次读 hidden + residual,写出 normed output
- SiLU + Elementwise Mul (SwiGLU 内部): 一次读 gate + up, 写出 SiLU(gate)*up
- Bias Add + Activation: Linear 的 bias + 激活函数合并
原则: 两个逐元素 kernel 之间如果有 global memory 读写,就值得融合。
2. CUDA Graphs
- Decode 阶段每步的 kernel 序列是固定的(shape 不变)
- 用
cudaStreamBeginCapture/cudaStreamEndCapture捕获一次 - 后续用
cudaGraphLaunch重放(消除 kernel launch overhead) - 注意: batch size 变化时需要重新 capture
3. Memory 优化
- 权重预加载: 启动时加载到 GPU,推理路径上零分配
- Activation reuse: 同一层的中间结果用完立即释放/复用
- Pinned memory: H2D/D2H 用
cudaMallocHost(pinned)提升拷贝带宽
4. Compute 优化
- 确保所有 GEMM 走 Tensor Core (BF16)
- Decode attention: 优化 memory bandwidth 利用率
- Prefill: chunked processing(控制峰值显存,允许更大 batch)
5. Scheduling 优化
- Prefill-Decode disaggregation: prefill 和 decode 分开 batch
- 原因: prefill 是 compute-bound, decode 是 memory-bound, 混合导致两边都不优
- Dynamic batch size: 根据当前 running seqs 的 seq_len 动态调整
Profiling 工具使用
# 整体 timeline (哪个 kernel 最耗时)
nsys profile --stats=true ./target/release/xserv-server
# 单个 kernel 分析 (occupancy, memory throughput)
ncu --target-processes all --set full ./target/release/xserv-server
# 自定义 Rust 计时
# 在 engine 循环中记录每个 phase 的耗时
测试验收
- 安装 vLLM,同一台机器跑 Qwen3-7B
- Benchmark 对比:
| Metric | vLLM | xserv | Ratio |
|---|---|---|---|
| Prefill latency (ms, 128 tokens) | |||
| Decode throughput (tokens/s, batch=1) | |||
| Decode throughput (tokens/s, batch=16) | |||
| Max concurrent requests (32GB) |
- 目标: xserv throughput >= 50% vLLM
- Profiling 报告: 每个组件的耗时占比 pie chart
- 无功能回归: 所有之前的集成测试通过
Phase 16: Speculative Decoding
Crate: xserv-speculative
目标: 用小模型(draft model)加速大模型(target model)的 decode 阶段。
算法
γ = 4 (speculative tokens 数量)
1. Draft model 自回归生成 γ 个 token: t1, t2, t3, t4
2. Target model 一次 forward 处理这 γ+1 个 position
(等价于一次 prefill: [last_accepted_token, t1, t2, t3, t4])
得到 γ+1 个 logits
3. Rejection sampling:
for i in 0..γ:
p = target_prob[i][t_{i+1}] // target model 给 draft token 的概率
q = draft_prob[i][t_{i+1}] // draft model 给该 token 的概率
if random() < min(1, p/q):
accept t_{i+1}
else:
reject, resample from adjusted distribution
break
4. 至少接受 1 个 token, 期望接受 ~γ×acceptance_rate 个
关键点
- 无损: rejection sampling 保证输出分布与纯 target model 一致
- 加速条件: draft model 足够快且与 target 分布接近
- Draft model 选择: Qwen3-0.5B / Qwen3-1.5B 作为 Qwen3-7B 的 draft
KV Cache 处理
- Draft model 有自己的 KV cache
- Target model 验证时,accepted tokens 的 KV 可以复用(不用重算)
- Rejected 位置之后的 KV 需要丢弃
测试验收
- 验证: speculative decode 100 条不同 prompt,输出分布与标准 decode 无统计差异
- Acceptance rate 统计 (期望 60-80% per token)
- 端到端加速:
| Method | Tokens/s (batch=1) | Speedup |
|---|---|---|
| Standard decode | 1.0x | |
| Speculative (γ=4) | ~2-3x |
Phase 17: Tensor Parallelism (TP=1/2/4) — 里程碑 ⑤
Crate: xserv-distributed
目标: 实现 Tensor Parallelism,支持 TP=2 和 TP=4(同组 GPU 内),跑通多卡推理。PP 及更大规模并行留待后续扩展。
通信后端: NCCL
NVIDIA Collective Communication Library,提供高效的 multi-GPU 通信原语。
需要封装的操作:
// NCCL FFI
ncclAllReduce(sendbuff, recvbuff, count, datatype, op, comm, stream)
ncclAllGather(sendbuff, recvbuff, count, datatype, comm, stream)
ncclBroadcast(sendbuff, recvbuff, count, datatype, root, comm, stream)
Tensor Parallelism 策略 (Megatron-LM 风格)
每层只需要 2 次 AllReduce:
Attention 部分
Column Parallel: Q/K/V proj 按 head 维度切分
GPU 0: q_proj[:, :hidden/TP], k_proj[:, :hidden/TP], v_proj[:, :hidden/TP]
GPU 1: q_proj[:, hidden/TP:], k_proj[:, hidden/TP:], v_proj[:, hidden/TP:]
→ 每卡计算自己那部分 heads 的 attention → 无需通信
Row Parallel: o_proj 按行切分
每卡计算部分输出 → AllReduce 求和 → 得到完整 output
FFN (SwiGLU) 部分
Column Parallel: gate_proj, up_proj 按列切分
每卡计算部分 intermediate features → 无需通信
Row Parallel: down_proj 按行切分
每卡计算部分输出 → AllReduce 求和 → 得到完整 output
其他
- Embedding: vocab 按行切分,AllGather 拼接
- RMSNorm: 每卡独立计算(输入是 AllReduce 后的完整 tensor)
- lm_head: 按列切分,AllGather 拼接 logits
权重分片
启动时:
- Rank 0 加载完整权重
- 按 TP 策略切分
- AllGather 或 Scatter 分发到各卡
或者:
- 每个 rank 独立加载,只读取属于自己的那部分(更高效)
互联拓扑 (已确认)
纯 PCIe Gen5 x16, 无 NVLink。GPU 分两组: 0-3 (PHB) 和 4-7 (PHB),跨组走 NODE。
TP 部署策略 (当前阶段目标: TP=1/2/4):
- TP=2: 同组内任意两卡 (如 GPU0+GPU1),PCIe PHB 延迟最低
- TP=4: 同组 4 卡 (GPU 0-3 或 GPU 4-7),全 PHB 互联
- PCIe Gen5 x16 带宽: 理论 ~64 GB/s 单向,实测 AllReduce 有效带宽约 40-50 GB/s
- 后续扩展: TP=8 (跨组) 和 Pipeline Parallelism 留待后续阶段
测试验收
- TP=2: Qwen3-7B 输出与单卡 (TP=1) 完全一致
- TP=4: 每卡权重显存占用约 1/4
- Scaling benchmark (同组 GPU 0-3):
| TP Size | Prefill (tokens/s) | Decode (tokens/s) | Scaling Efficiency |
|---|---|---|---|
| 1 | 100% | ||
| 2 | % | ||
| 4 | % |
- AllReduce latency 测量 (不同消息大小,同组 PHB 互联)
Phase 18: 量化 (FP8 / INT8)
Crate: xserv-kernels (新增量化 kernel), xserv-model (量化加载)
CUDA 源码: csrc/quantize/
目标: 降低模型显存占用和提升计算吞吐。
量化方式
1. Weight-Only INT8
- 只量化权重,activation 保持 BF16
- Per-channel scale: 每个输出 channel 一个
scale和zero_point - GEMM: INT8 × BF16 (dequantize on-the-fly)
- 适用于 memory-bound 场景(decode)
2. FP8 (E4M3 / E5M2)
- 5090 Blackwell (CC 12.0) 原生支持 FP8 Tensor Core
- 权重和 activation 都量化为 FP8
- Dynamic scaling: 每个 tensor 运行时计算
amax,确定 scale factor - GEMM: FP8 × FP8 → BF16/FP32 accumulate
- 适用于 compute-bound 场景(prefill)
3. GPTQ / AWQ (高级, 可选)
- INT4 weight quantization
- 需要 calibration data
- 更复杂但压缩率更高
FP8 实现要点
pub enum DType {
// ... existing
F8E4M3, // 1 sign + 4 exponent + 3 mantissa (范围小,精度高)
F8E5M2, // 1 sign + 5 exponent + 2 mantissa (范围大,精度低)
}
Dynamic scaling:
scale = amax(tensor) / fp8_max // fp8_max = 448.0 for E4M3
tensor_fp8 = cast_to_fp8(tensor / scale)
// GEMM 后: output = output * scale_A * scale_B
测试验收
- 精度:
| Quantization | Perplexity (WikiText-2) | vs BF16 |
|---|---|---|
| BF16 (baseline) | X.XX | — |
| FP8 E4M3 | X.XX | +0.XX |
| INT8 weight-only | X.XX | +0.XX |
- 显存: FP8 权重占用约 BF16 的一半 (~7 GB for 7B model)
- 性能: FP8 GEMM throughput vs BF16 GEMM
Phase 19: Multimodal — 里程碑 ⑥
Crate: xserv-model (新增 vision encoder)
目标: 支持 vision-language 模型,接受图片+文本输入。
目标模型
Qwen-VL 系列(或类似 architecture)。典型结构:
Image → ViT Encoder → Visual Tokens → Projector → LLM Input
Text → Tokenizer → Text Tokens ────────────→ LLM Input
需要新增的组件
1. ViT (Vision Transformer) Encoder
- Patch Embedding: 将图片切成 14×14 patches, 每个 patch 线性投影
- 输入: [B, 3, H, W] → 输出: [B, num_patches, hidden_dim]
- ViT Blocks: 标准 Transformer Encoder blocks
- Multi-Head Self-Attention (无 causal mask)
- FFN
- LayerNorm
- 参数量: 通常 300M-600M
2. Visual-Language Projector
- MLP: 将 ViT 输出维度映射到 LLM embedding 维度
- 可能包含 pooling / resampling (减少 visual token 数量)
3. 图片预处理
- Resize to target resolution (e.g., 448×448)
- Normalize: ImageNet mean/std
- 使用
imagecrate 在 CPU 上完成
4. 输入拼接
[<image_token>] × num_visual_tokens + [text tokens]
- 在 embedding 层面拼接
- LLM 处理混合 sequence
API 扩展
{
"model": "qwen-vl",
"messages": [{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}},
{"type": "text", "text": "What is in this image?"}
]
}]
}
外部依赖
imagecrate (图片加载和预处理)base64crate (解码 base64 图片)
测试验收
- 加载 Qwen-VL 模型,输入一张猫的图片 + "What is in this image?"
- 生成合理的图片描述
- 与 HF transformers 输出对比
- API 端到端: HTTP POST 含 base64 图片 → streaming 文字回答
- 纯文本请求不受影响(向后兼容)
里程碑总结
| 里程碑 | Phase | 验收标准 |
|---|---|---|
| ① GPT-2 推理 | 8 | CLI 输入 prompt, GPT-2 生成连贯文本, logits 与 PyTorch 一致 |
| ② Qwen3-7B 推理 | 10 | 7B 模型中英文对话, 多轮 chat template 正确 |
| ③ E2E API | 13 | HTTP streaming API, Python OpenAI SDK 可调用, 10 并发正确 |
| ④ 性能达标 | 15 | throughput >= 50% vLLM, profiling 报告完成 |
| ⑤ 多卡推理 | 17 | TP=2/4 同组 GPU 推理正确, scaling benchmark 完成 |
| ⑥ 多模态 | 19 | 图片输入 → 文字回答, API 端到端 |
外部依赖清单
| Crate | 用途 | 引入 Phase |
|---|---|---|
cc |
build.rs 编译 .cu 文件 | 0 |
half |
f16 / bf16 Rust 类型 | 2 |
smallvec |
Tensor shape / strides (栈分配) | 2 |
safetensors |
权重文件解析 | 6 |
serde + serde_json |
JSON 序列化 | 6 |
memmap2 |
文件 mmap (safetensors 可能内置) | 6 |
regex |
BPE pre-tokenization | 7 |
rand |
Sampling (随机数) | 8 |
tokio |
Async runtime | 13 |
axum |
HTTP server | 13 |
criterion |
Benchmark framework | 3+ |
image |
图片加载 (multimodal) | 19 |
base64 |
Base64 decode (multimodal API) | 19 |
不使用: candle, burn, tch, tokenizers, cudarc — 核心组件全部自己实现。