不依赖 vLLM/llama.cpp:一套面向学习的 DeepSeek-R1 CUDA 推理实现

一、为什么要在 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 说明计时包含了采样等额外开销后的综合表现。进一步优化方向很明确:

  1. CUDA Graph:decode 阶段每次 forward 的 kernel launch 开销可以忽略不计(因为每次只算一个 token),但如果用 CUDA Graph 把整个 decode 路径 capture 成图,可以进一步减少 CPU-GPU 同步开销
  2. GEMV 重写:decode 阶段的 QKV 投影实际上是 GEMV(因为 seq_len=1),用专用的 GEMV kernel 比通用 GEMM 快很多
  3. Kernel Fusion:把 RMSNorm + GEMM 融合,减少显存读写
  4. 量化推理:权重量化成 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 推理适配,这个项目值得花时间深入研究。它不一定是最快的生产级推理引擎,但它一定是最适合"学习理解"的推理引擎之一。

相关推荐
kiracrimson1 小时前
string 容器:从 C 字符串到会自己扩容的类
开发语言·数据结构·c++·stl
程序猿编码1 小时前
纯 C++ 手写 LLM 推理引擎:分页 KV 缓存、连续批、多卡张量并行全部内置
c++·深度学习·缓存·大模型推理·自动微分·模型推理
ms72wx2 小时前
Docker 命令学习笔记:从入门到进阶的实战指南
笔记·学习·docker
缘友一世2 小时前
GLM5.3 CyberSec w4A16本地部署[10]:CUDA 图捕获与编译开关
ubuntu·vllm·glm5.3 cybersec
G018_star sky♬2 小时前
nodeJS他是如何用JavaScript封装C++的呢
开发语言·javascript·c++
STLearner2 小时前
KDD 2026 | (2月轮)时空数据(Spatial-Temporal)论文总结时空【下】(人群移动,城市科学(资源调度,)
大数据·人工智能·python·深度学习·机器学习·语言模型·数据挖掘
choumin2 小时前
C++中std::move和std::forward的关系和区别
c++·std move·std forward·关系和区别
程序猿编码3 小时前
YOLOv5/v7/YOLOX 全支持:基于 TensorRT 的工业视觉推理底座详解
c++·yolo·机器学习·推理·模型推理
外收内放3 小时前
FastAPI猜字谜实战(4):让AI主持人开口
开发语言·python·学习·fastapi