一、为什么要在 CUDA 层面从零手写 LLM 推理引擎?
如果你是一个对大模型推理底层原理感兴趣的后端工程师,大概率有过这样的困惑:用 vLLM 部署很快,用 llama.cpp 量化推理也很方便,但你对"每一个 token 到底在 GPU 上经历了什么"依然是一头雾水。框架帮你封装了太多东西------算子怎么调度、内存怎么管理、kernel 怎么 launch,这些全被藏在 C++ 模板和 CUDA 核函数里面,你只能看到输入输出,看不到中间过程。
这个项目做的事情很简单:不依赖 PyTorch、不依赖 Transformers、不依赖 vLLM、不依赖 llama.cpp,纯 C++/CUDA 手写一个大语言模型的完整推理路径。当前主要面向 DeepSeek-R1-Distill-Qwen-7B 的单 batch 推理,目标是在 A100/A800 上跑通从权重加载到 token 生成的全链路,并且每一步的耗时都能精确到 kernel 级别。
说白了,这就是一个"可拆开的推理引擎"------你想看 RMSNorm 怎么算,就去看 RMSNorm 的 kernel;想调 RoPE 的位置编码,就直接改 RoPE 的索引计算;想优化 GQA Attention 的内存访问模式,就盯着 attention score 的 shared memory 布局调。没有框架的"保护伞",每一个优化点都清清楚楚。
二、核心技术知识点
在深入代码之前,先聊聊这个项目里涉及的几个关键技术点。这些不是教科书式的定义,而是从"写 kernel 时会碰到什么坑"的角度来讲。
2.1 RMSNorm:比 LayerNorm 更轻的归一化
Transformer 里每层前面都要做归一化。传统的 LayerNorm 要同时算均值和方差,还要学两个参数(gamma 和 beta)。RMSNorm(Root Mean Square Layer Normalization)做了两个简化:
- 去掉均值中心化,直接用均方根(RMS)做缩放
- 去掉 beta 偏置,只保留 gamma 缩放参数
公式很简单:
RMS(x) = sqrt( mean(x^2) + eps )
output = x / RMS(x) * gamma
在 CUDA 里实现 RMSNorm kernel,核心就是一个并行归约(parallel reduction)算均值,然后每个 thread 处理一个元素做缩放。因为不需要存中间均值,显存访问比 LayerNorm 少一次,速度更快。对于 7B 模型来说,每层都要做两次 RMSNorm(attention 前 + MLP 前),28 层就是 56 次,省掉的显存带宽是实打实的。
2.2 RoPE:旋转位置编码的数学直觉
RoPE(Rotary Position Embedding)是目前大模型最常用的位置编码方案。它的核心思想很优雅:不往 token embedding 里加位置信号,而是对 Q 和 K 的每个 head 做旋转变换,让注意力计算天然感知位置。
具体做法是把每个 head 的 dim 两两配对,对每一对 (x_{2i}, x_{2i+1}) 做一个二维旋转矩阵:
[cos(mθ) -sin(mθ)] [x_{2i}]
[sin(mθ) cos(mθ)] [x_{2i+1}]
其中 m 是位置索引,θ 是根据维度位置计算的频率。这样做的妙处在于:Q 和 K 都做 RoPE 后,点积 QK^T 天然包含了位置差信息,不需要额外计算。
在 CUDA kernel 里实现 RoPE,关键是要让两个 thread 合作处理一对维度(一个处理偶数位,一个处理奇数位),或者用一个 thread 处理多个配对。对于 head_dim=64 的情况,每个 head 需要 32 次旋转操作。
2.3 GQA:用少的 KV head 换速度
Grouped-Query Attention(GQA)是 DeepSeek-R1-Distill-Qwen-7B 用的注意力变体。标准 Multi-Head Attention 有 H 个 Q head、H 个 K head、H 个 V head;GQA 把 K 和 V 的 head 数减少到 G 个(G < H),Q 还是 H 个。
以 DeepSeek-R1-Distill-Qwen-7B 为例:28 个 Q head,但只有 4 个 KV head。这意味着:
- K 和 V 的投影矩阵变小了(只有 4 个而不是 28 个)
- KV Cache 的显存占用大幅降低(4/28 ≈ 14%)
- 但 attention score 计算时需要把 K 和 V 广播到 28 个 head
广播的方式很简单:第 i 个 KV head 的结果复制 H/G 份,分别对应到对应的 Q head。这个"复制"操作在 CUDA 里就是让多个 thread block 读同一块 KV Cache 内存,不存在额外的显存带宽开销。
2.4 SwiGLU:比 FFN 更有效的前馈网络
DeepSeek-R1-Distill-Qwen-7B 的前馈网络用的是 SwiGLU(Swish-Gated Linear Unit),结构如下:
forward(x):
x_proj = x @ W1 (hidden_size -> intermediate_size)
z = x @ W3 (hidden_size -> intermediate_size)
gate = silu(x_proj) * z
output = gate @ W2 (intermediate_size -> hidden_size)
对比传统的 FFN(就是两次线性变换夹一个激活函数),SwiGLU 多了一个"门控"路径(W3),用 silu 激活后再和主路径相乘。效果上是多了大约 1/3 的计算量,但收敛速度和最终效果明显更好。
在 CUDA 里实现 SwiGLU,最大的优化点在于:W1 和 W3 的矩阵乘法可以合并成一个 kernel,因为它们输入都是 x,可以一次性把 W1 和 W3 的权重拼在一起做一次大 GEMM,然后 split 出两个路径。这样减少了读 x 的次数(从 2 次变成 1 次),对内存带宽受限的 decode 阶段特别重要。
2.5 KV Cache:decode 阶段的生命线
自回归生成(decode)的核心瓶颈是什么?是每次生成一个新 token 都要重新计算前面所有 token 的 KV。KV Cache 的做法就是:把前面算好的 K 和 V 存起来,下次只需要算当前 token 的 Q、K、V,然后拿当前 Q 去和所有历史 KV 做 attention。
在实现上,KV Cache 是一个预分配的 GPU 缓冲区:
kv_cache[layer][0] = K_cache [num_kv_heads, head_dim, max_seq_len]
kv_cache[layer][1] = V_cache [num_kv_heads, head_dim, max_seq_len]
每次 decode 一步,只需要更新当前位置的 K 和 V(一个 scatter 操作),然后做 attention 时 read 整个 kv_cache。对于 max_seq=800 的场景,28 层 × 2 × 4 heads × 64 dim × 800 positions × 2 bytes(half) = 约 2.8GB 显存。
2.6 Decode 阶段的性能瓶颈分析
理解 decode 阶段的瓶颈分布很重要。对于一个 7B 模型的单步 decode:
| 算子 | 大致计算量占比 | 瓶颈类型 |
|---|---|---|
| RMSNorm (pre-attention) | ~2% | 内存带宽 |
| QKV 投影 | ~15% | 计算/带宽混合 |
| RoPE | ~3% | 内存带宽 |
| GQA Attention | ~25% | 计算/带宽混合 |
| RMSNorm (pre-MLP) | ~2% | 内存带宽 |
| SwiGLU MLP (W1, W3, W2) | ~53% | 计算密集 |
| Output投影 | ~5% | 计算/带宽混合 |
可以看到,MLP 路径(SwiGLU)占了超过一半的计算量,这是优化的第一优先级。其次是 Attention 路径。而 RMSNorm 和 RoPE 虽然占比小,但它们是"白嫖"操作------混在 GEMM 里一起算,不单独占时间。
三、架构设计思路
3.1 整体架构
这个推理引擎采用了"C++ 动态库 + Python 胶水"的混合架构:
┌─────────────────────────────────────────────────────┐
│ Python 层 │
│ python_infer.py (tokenizer + 命令行参数 + 计时) │
│ llm_cuda_python_tokenizer_v2/ (tokenizer 实现) │
├─────────────────────────────────────────────────────┤
│ CUDA 动态库 (libllm_cuda.so) │
│ llm_cuda_lib.cu (模型前向传播 + 所有 CUDA kernel) │
│ - safetensors 权重加载 │
│ - RMSNorm kernel │
│ - RoPE kernel │
│ - GQA Attention kernel │
│ - SwiGLU MLP kernel │
│ - KV Cache 管理 │
│ - decode loop 控制逻辑 │
├─────────────────────────────────────────────────────┤
│ HuggingFace safetensors 权重文件 │
│ (model-00001-of-XXXXX.safetensors + model.safetensors.index.json) │
└─────────────────────────────────────────────────────┘
设计哲学:Python 只负责 tokenizer 和 IO(模型路径、prompt 输入、结果输出),所有计算密集型操作全部下沉到 CUDA 动态库里。这样既保留了 Python 生态的便利(tokenizer 用现成的),又保证了推理性能不被 Python GIL 拖慢。
3.2 模型加载管线
项目支持 HuggingFace safetensors 格式权重加载。safetensors 是一种零反序列化风险的模型格式,权重数据以纯二进制块存储在文件末尾,头部只存元数据(形状、dtype、偏移量)。加载流程:
1. 读取 model.safetensors.index.json 获取权重文件映射
2. 按 key 名打开对应的 .safetensors 文件
3. 解析头部获取 tensor 的 shape/dtype/offset/size
4. cudaMalloc + cudaMemcpyAsync 将权重从 host 搬到 device
5. 存入 C++ 侧的模型参数结构体
3.3 编译系统
项目用 Makefile 管理 CUDA 编译:
make -f Makefile.cuda_lib lib A=sm_80
其中 A=sm_80 指定目标 GPU 架构(sm_80 = A100/A800/RTX 4090 系列)。编译产出 build/libllm_cuda.so 动态库,Python 侧通过 ctypes 或 pybind11 加载这个库调用推理入口。
四、核心代码实现原理
下面进入最硬核的部分------逐个拆解关键 CUDA kernel 的实现思路。虽然拿不到仓库的完整源码,但根据模型架构参数和项目描述,可以还原出高度接近真实实现的代码。
4.1 RMSNorm CUDA Kernel
RMSNorm 的实现核心是两步:先并行归约算均值,再每个元素做缩放。
cpp
__global__ void rms_norm_kernel(
const half* __restrict__ input, // [batch, seq_len, hidden_size]
const half* __restrict__ gamma, // [hidden_size]
half* __restrict__ output, // [batch, seq_len, hidden_size]
const int hidden_size,
const float eps)
{
// 每个 block 处理一个 token 的 hidden state
extern __shared__ float sdata[];
int tid = threadIdx.x;
int gid = blockIdx.x * blockDim.x + tid;
// 加载数据到 shared memory 做归约
float sum = 0.0f;
for (int i = tid; i < hidden_size; i += blockDim.x) {
float x = __half2float(input[blockIdx.x * hidden_size + i]);
sum += x * x;
}
// 块内归约
sdata[tid] = sum;
__syncthreads();
for (int s = blockDim.x / 2; s > 0; s >>= 1) {
if (tid < s) {
sdata[tid] += sdata[tid + s];
}
__syncthreads();
}
// 线程 0 写回均值
if (tid == 0) {
float rms = sqrtf(sdata[0] / hidden_size + eps);
// 广播 rms 给所有线程做缩放
__shared__ float r_rms;
if (threadIdx.x == 0) r_rms = rms;
__syncthreads();
float g = __half2float(gamma[blockIdx.x]);
for (int i = 0; i < hidden_size; i += blockDim.x) {
float x = __half2float(input[blockIdx.x * hidden_size + i]);
float out = (x / r_rms) * g;
output[blockIdx.x * hidden_size + i] = __float2half(out);
}
}
}
实际工程中,更常见的优化是把 RMSNorm 和后面的 GEMM 融合(fusion)成一个 kernel,避免中间结果的写回显存再重读。这就是项目提到"多轮优化"的核心手段之一。
4.2 RoPE Kernel
RoPE 需要对 Q 和 K 的每对维度做旋转。高效实现的关键是让两个 thread 协作处理一对维度:
cpp
__global__ void rope_kernel(
const half* __restrict__ q, // [batch, seq_len, n_heads, head_dim]
const half* __restrict__ k, // [batch, seq_len, n_kv_heads, head_dim]
half* __restrict__ q_out, // 同上
half* __restrict__ k_out,
const int seq_len,
const int n_heads,
const int n_kv_heads,
const int head_dim,
const float base) // RoPE theta, 模型中为 1000000.0
{
// 每个 thread 处理一个位置的一个维度对
int position = blockIdx.y; // seq_len 维度
int head_idx = blockIdx.x; // head 维度 (q: n_heads, k: n_kv_heads)
int pair_idx = threadIdx.x; // head_dim / 2 对
int dim_pair_start = pair_idx * 2;
// 计算频率: theta_i = base^(-2i/d_model)
float theta_inv = powf(base, -((float)dim_pair_start) / head_dim);
// 加载原始值
float q0 = __half2float(q[...]);
float q1 = __half2float(q[...]);
// 旋转
float cos_val = cosf(position * theta_inv);
float sin_val = sinf(position * theta_inv);
// 写回
q_out[...] = __float2half(q0 * cos_val - q1 * sin_val);
q_out[...] = __float2half(q0 * sin_val + q1 * cos_val);
}
对于 DeepSeek-R1-Distill-Qwen-7B 的 head_dim=64,每个 head 需要 32 个 thread 合作完成。实际实现中会用 __cosf/__sinf 快速近似,并用预计算的频率表避免重复 pow 计算。
4.3 GQA Attention Kernel
GQA Attention 是整个引擎里最复杂的 kernel。它的计算流程是:
1. Q = x @ Wq (B, S, H*head_dim) -> (B, S, H, head_dim)
2. K = x @ Wk (B, S, G*head_dim) -> (B, S, G, head_dim)
3. V = x @ Wv (B, S, G*head_dim) -> (B, S, G, head_dim)
4. 对 Q, K 做 RoPE
5. 更新 KV Cache(把当前 K, V 写入缓存)
6. Attention: scores = Q @ K_cache^T / sqrt(d_k)
7. softmax(scores) @ V_cache
8. 合并 heads: (B, S, H, head_dim) -> (B, S, H*head_dim)
9. 输出投影: @ Wo
GQA 的特殊之处在于步骤 6:K_cache 只有 G 个 head,但 Q 有 H 个 head。需要将 K_cache 广播 H/G 份。在 CUDA 里,这个广播不是真的复制数据,而是让 H/G 个 Q head 的 thread block 读同一块 K_cache 内存:
cpp
// 简化的 GQA attention 核心逻辑
__global__ void gqa_attention_kernel(
const half* __restrict__ Q, // [batch, n_heads, seq_q, head_dim]
const half* __restrict__ K_cache, // [batch, n_kv_heads, cache_len, head_dim]
const half* __restrict__ V_cache, // [batch, n_kv_heads, cache_len, head_dim]
half* __restrict__ output, // [batch, n_heads, seq_q, head_dim]
const int batch,
const int n_heads,
const int n_kv_heads,
const int seq_q,
const int cache_len,
const int head_dim,
const float scale)
{
// block: (head_idx, q_pos) | tile: (head_dim, BLOCK_K)
int head_idx = blockIdx.x;
int q_pos = blockIdx.y;
int kv_head_idx = head_idx / (n_heads / n_kv_heads); // GQA 映射
// 每个 thread 处理 head_dim/BLOCK_SIZE 个维度
// 用 shared memory 做 tiled attention 减少全局内存访问
__shared__ half sQ[BLOCK_SIZE][HEAD_TILE];
__shared__ half sK[BLOCK_SIZE][HEAD_TILE];
__shared__ half sV[BLOCK_SIZE][HEAD_TILE];
// ... tiled matrix multiply + attention softmax ...
// 关键优化点:
// 1. Q tile 加载一次,复用整个 K/V tile 的计算
// 2. K_cache 和 V_cache 的 kv_head_idx 映射让多个 Q head 共享读取
// 3. attention score 用 log-sum-exp trick 做数值稳定 softmax
}
4.4 SwiGLU MLP Kernel
SwiGLU 的前向计算包含三次 GEMM(W1, W3, W2)和一次 silu 激活。优化策略是把 W1 和 W3 合并:
cpp
// 伪代码描述计算流程:
// input: [batch, seq_len, hidden_size=3584]
// W1: [hidden_size, intermediate_size=18944]
// W3: [hidden_size, intermediate_size=18944]
// W2: [intermediate_size, hidden_size=3584]
// 合并 W1 和 W3 的 GEMM:
// output_proj = [x @ W1, x @ W3] -> [batch, seq_len, 2*intermediate_size]
// 然后 split 出 gate 和 z
// gate = silu(output_proj[:, :intermediate_size])
// z = output_proj[:, intermediate_size:]
// result = gate * z @ W2
// CUDA 实现要点:
// 1. 用 cuBLAS 的 gemm 做 W1 和 W3 的合并计算(或者手写 tiled GEMM)
// 2. silu(x) = x * sigmoid(x) = x / (1 + exp(-x))
// 3. W2 的 GEMM 是最后一个线性层,输出维度回到 hidden_size
对于 7B 模型,intermediate_size=18944,hidden_size=3584。单次 GEMM (3584 x 18944) 需要约 480M 次乘加运算。在 A100 上这大约占用 10-15ms。
4.5 KV Cache 管理
KV Cache 的内存管理和更新是 decode 阶段的核心操作:
cpp
// 模型参数结构体(简化版)
struct ModelParams {
// 权重(都在 GPU 上)
half* wte; // vocab_size x hidden_size
half* w1[28]; // SwiGLU W1: hidden x intermediate
half* w3[28]; // SwiGLU W3: hidden x intermediate
half* w2[28]; // SwiGLU W2: intermediate x hidden
half* wi[28]; // Attention Wq
half* wk[28]; // Attention Wk
half* wv[28]; // Attention Wv
half* wo[28]; // Attention output projection
half* rms_norm_before_attn[28];
half* rms_norm_before_mlp[28];
half* rms_norm_after[28];
// KV Cache(预分配)
half* kv_cache_k[28]; // [n_kv_heads, head_dim, max_seq_len]
half* kv_cache_v[28]; // [n_kv_heads, head_dim, max_seq_len]
int num_layers;
int n_heads;
int n_kv_heads;
int head_dim;
int hidden_size;
int intermediate_size;
int max_seq_len;
};
// KV Cache 更新 kernel
__global__ void kv_cache_update_kernel(
const half* __restrict__ k_new, // [n_kv_heads, head_dim]
const half* __restrict__ v_new, // [n_kv_heads, head_dim]
half* __restrict__ kv_cache_k, // [n_layers, n_kv_heads, head_dim, max_seq_len]
half* __restrict__ kv_cache_v,
const int layer,
const int pos)
{
int head = blockIdx.x;
int dim = blockIdx.y * blockDim.x + threadIdx.x;
if (head < n_kv_heads && dim < head_dim) {
int cache_offset = layer * n_kv_heads * head_dim * max_seq_len
+ head * head_dim * max_seq_len
+ dim * max_seq_len + pos;
kv_cache_k[cache_offset] = k_new[head * head_dim + dim];
kv_cache_v[cache_offset] = v_new[head * head_dim + dim];
}
}
4.6 Decode Loop 主控逻辑
整个 decode 过程的主循环在 C++ 侧控制,调用一系列 CUDA kernel:
cpp
// 简化的 decode 循环伪代码
std::vector<int> generate(
const ModelParams& params,
const std::vector<int>& prompt_tokens,
int max_new_tokens)
{
std::vector<int> generated;
int cur_pos = prompt_tokens.size();
std::vector<int> input_ids = prompt_tokens;
// Prefill: 一次性处理整个 prompt
auto logits = forward(params, input_ids, 0, cur_pos);
// 采样第一个 token
int next_token = sample(logits);
generated.push_back(next_token);
// Decode: 每次生成一个 token
while (generated.size() < max_new_tokens && next_token != eos_token) {
input_ids = {next_token};
// Forward 一步(这是最耗时的部分)
auto logits = decode_step(params, input_ids, cur_pos);
next_token = sample(logits);
generated.push_back(next_token);
cur_pos++;
// 可选:early stop
if (next_token == eos_token) break;
}
return generated;
}
// decode_step 内部调用一系列 CUDA kernel:
// 1. token embedding lookup
// 2. for each layer:
// a. RMSNorm (pre-attention)
// b. QKV projection (3 GEMMs)
// c. RoPE on Q and K
// d. KV Cache update
// e. GQA Attention
// f. Residual connect
// g. RMSNorm (pre-MLP)
// h. SwiGLU MLP (W1+W3 merged GEMM + silu + W2 GEMM)
// i. Residual connect
// 3. Final RMSNorm
// 4. LM head (linear projection to vocab)
// 5. Sampling (top-k/top-p)
五、环境配置与运行测试教程
If you need the complete source code, please add the WeChat number (c17865354792)
5.1 硬件与驱动要求
| 组件 | 最低要求 | 推荐配置 |
|---|---|---|
| GPU | NVIDIA GPU (Compute Capability >= 7.0) | A100/A800 (sm_80) 或 RTX 4090 (sm_89) |
| CUDA Toolkit | 11.0+ | 12.x |
| GCC | 7+ | 9+ |
| 显存 | >= 16GB (FP16 7B 模型) | >= 24GB |
| 磁盘 | >= 15GB (模型权重) | SSD |
5.2 编译动态库
bash
# 编译 CUDA 动态库 (A100/A800/RTX4090)
make -f Makefile.cuda_lib lib A=sm_80
# 如果用的是 RTX 3090 (Ampere)
make -f Makefile.cuda_lib lib A=sm_86
# 编译产物在 build/libllm_cuda.so
5.3 下载模型权重
项目提供了两种下载方式:
bash
# 方式一:通过 ModelScope(国内网络推荐,速度更快)
pip install -U modelscope
python download_model.py \
--source modelscope \
--model deepseek-ai/DeepSeek-R1-Distill-Qwen-7B \
--local-dir ./deepseek-r1-7b
# 方式二:通过 HuggingFace Hub
pip install -U huggingface_hub
python download_model.py \
--source huggingface \
--model deepseek-ai/DeepSeek-R1-Distill-Qwen-7B \
--local-dir ./deepseek-r1-7b
5.4 运行推理
bash
# 基本推理
CUDA_VISIBLE_DEVICES=0 python python_infer.py \
--model ./deepseek-r1-7b \
--lib ./build/libllm_cuda.so \
--prompt "你好 deepseek " \
--max-new-tokens 512 \
--max-seq 800
# 参数说明:
# --model 模型权重目录路径(包含 safetensors 文件的目录)
# --lib CUDA 动态库路径
# --prompt 输入提示文本
# --max-new-tokens 最大生成 token 数
# --max-seq 最大序列长度(包括 prompt + 生成)
# --seed 随机种子(可选,固定种子可复现)
# --temperature 采样温度(可选,默认 1.0)
# --top-k top-k 采样(可选)
# --top-p top-p 采样(可选)
5.5 性能测试
使用项目内置的 benchmark 模式测量吞吐量和延迟:
bash
# 运行性能测试
CUDA_VISIBLE_DEVICES=0 python python_infer.py \
--model ./deepseek-r1-7b \
--lib ./build/libllm_cuda.so \
--prompt "测试文本" \
--max-new-tokens 512 \
--max-seq 800 \
--benchmark
# 预期结果(A100/A800):
# 512 tokens = 65.6845 tok/s
# max forward_ms = 16.1768
5.6 常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| CUDA out of memory | 显存不足 | 减小 max-seq 或检查其他进程占用 |
| Illegal memory access | 内存越界 | 检查 max-seq 是否超过模型支持范围 |
| 编译报错 undefined reference | 链接问题 | 确认 CUDA 版本和 sm_XX 架构匹配 |
| 推理结果全是重复 token | 温度参数问题 | 尝试设置 --temperature 0.7-1.0 |
六、性能数据分析
6.1 当前 Benchmark 结果
在 DeepSeek-R1-Distill-Qwen-7B 模型上的测试数据:
| 指标 | 数值 | 说明 |
|---|---|---|
| 吞吐量 | 65.68 tok/s | 生成 512 tokens 的平均速度 |
| 单步延迟 | 16.18 ms | 单次 forward 的最大耗时 |
| 测试配置 | max_seq=800, batch=1 | 单 batch 单步 decode |
| 目标 GPU | A100/A800 | sm_80 架构 |
6.2 性能解读
65 tok/s 的 decode 速度对于单 batch 场景来说是一个不错的基准。对比一下:
- vLLM 在同样硬件上通过 PagedAttention 和连续 batching 可以达到更高的吞吐,但那是多请求场景
- llama.cpp 的 CPU 推理速度通常在个位数 tok/s
- 这个引擎的优势在于每一层开销都可见,你可以精确知道哪个 kernel 拖了后腿
单步 forward 耗时 16ms 意味着理论上限是 62.5 tok/s,实际达到 65 tok/s 说明计时包含了采样等额外开销后的综合表现。进一步优化方向很明确:
- CUDA Graph:decode 阶段每次 forward 的 kernel launch 开销可以忽略不计(因为每次只算一个 token),但如果用 CUDA Graph 把整个 decode 路径 capture 成图,可以进一步减少 CPU-GPU 同步开销
- GEMV 重写:decode 阶段的 QKV 投影实际上是 GEMV(因为 seq_len=1),用专用的 GEMV kernel 比通用 GEMM 快很多
- Kernel Fusion:把 RMSNorm + GEMM 融合,减少显存读写
- 量化推理:权重量化成 INT4/INT8 可以大幅减少内存带宽压力
七、落地用途
7.1 学习研究:理解 LLM 推理的"黑盒"
这是这个项目最大的价值。读完 vLLM 或 TensorRT-LLM 的源码,你依然可能对"attention 到底怎么算的"一知半解。但这个引擎里每一个 kernel 都是你亲手写的(或者至少是你可以逐行阅读的),RoPE 的索引怎么算的、KV Cache 的内存布局是怎样的、softmax 的数值稳定怎么做------全在眼前。
7.2 算子优化实验田
想试试新的 quantization 方案?想对比 INT4 DP4A 和 FP16 在 Jetson Orin 上的实际表现?想实验 speculative decoding 的效果?在这个引擎里改一个 kernel 就能测,不需要等框架更新。之前有人在 Jetson Orin AGX 上基于类似架构做到了 INT4 + DP4A 从 9 tok/s 提升到 19.6 tok/s 的优化。
7.3 国产 GPU 适配的基础设施
这个项目的架构设计天然适合移植到其他 GPU 平台。比如已经有人把它从 NVIDIA CUDA 迁移到了摩尔线程 MTT S4000 的 MUSA 平台上,用 mcc -x musa -mtgpu 编译后跑通了 DeepSeek-R1-Distill-Qwen-7B 的推理。如果你在做国产 GPU 的适配工作,一个自己可控的推理引擎比直接改 vLLM 源码要容易得多。
7.4 边缘部署原型验证
在部署到嵌入式设备之前,用这个引擎在 GPU 上快速验证推理管线是否正确,比直接交叉编译到目标平台要高效得多。模型加载、权重解析、forward 推理、token 生成------全链路在开发机上调通后,移植到目标平台只需要替换 GPU 后端。
7.5 教学与培训
对于需要给团队培训"大模型推理底层原理"的技术 Leader 来说,这个项目是一个完美的教学素材。你可以基于它开发培训课程,从 Transformer 基础结构讲起,一步步讲到 CUDA kernel 优化,每一步都有可运行的代码作为支撑。
八、总结
这个项目最核心的价值在于透明度------它把大模型推理中那些被框架封装起来的细节全部摊开了给你看。RMSNorm 怎么算、RoPE 怎么旋转、GQA 怎么广播 KV、SwiGLU 怎么融合 GEMM、KV Cache 怎么管理内存、decode loop 怎么控制流程------每一个环节都清晰可见。
当前的 mma 版本已经针对 A100/A800 的单步 decode 做了多轮优化,达到了 65+ tok/s 的性能。但优化空间还很大:
- CUDA Graph:capture decode 路径为图,减少 kernel launch 开销
- Decode GEMV 重写:用专用 kernel 替代通用 GEMM,提升 QKV 投影速度
- 更激进的 Kernel Fusion:把更多连续操作融合到一个 kernel 里
- 量化推理:INT4/INT8 权重量化 + 计算路径优化
- Speculative Decoding:用草稿模型加速生成,提升吞吐
如果你也对从零写推理引擎感兴趣,或者正在做国产 GPU 的 LLM 推理适配,这个项目值得花时间深入研究。它不一定是最快的生产级推理引擎,但它一定是最适合"学习理解"的推理引擎之一。