一、为什么要在 C++ 里从零手写一个 LLM 推理引擎?

如果你玩过 LLM 部署,大概率经历过这样的痛苦:用 Python 调 API 很快,但一到生产环境就要面对 PyTorch 动辄几个 G 的内存开销、GPU 利用率不到 30% 的尴尬,以及"换个模型就要重装一套依赖"的部署噩梦。

于是出现了一批"反框架"项目——不依赖 PyTorch、不依赖 Transformers、不依赖 vLLM,纯 C++ 手写推理路径。其中有一个项目尤其值得关注:它支持 Llama 3.2、Qwen 2.5、Qwen 3、Mistral 等主流模型,实现了分页 KV Cache、连续批处理、Flash Attention、多 GPU 张量并行等工业级优化,同时还提供了一个 OpenAI 兼容的 HTTP 服务器和 Web UI。

这个项目的设计哲学很明确:把大模型推理的"黑盒"打开,让每个算子、每块内存、每次 GPU 通信都透明可控。 本文就带你从源码层面拆解它的完整实现。

二、行业技术知识点速查

在深入代码之前,先快速过一遍这个引擎涉及的核心技术概念。

2.1 BPE 分词器

BPE(Byte Pair Encoding)是目前大模型最主流的分词算法。它的核心思想很简单:

  1. 从字符级词汇表开始
  2. 统计文本中相邻 token 对的频率
  3. 把频率最高的 pair 合并成一个新 token
  4. 重复步骤 2-3 直到达到目标词汇表大小

这个项目实现的 BPE 分词器灵感来自 OpenAI 的 tiktoken,关键优化点在于零堆分配——所有内存操作在编译期或初始化期完成,推理时不产生任何 heap allocation,这对低延迟推理至关重要。

2.2 KV Cache 与分页管理

Transformer 解码阶段有个性能瓶颈:每生成一个新 token,都需要重新计算之前所有 token 的 Key 和 Value 矩阵。KV Cache 的做法是把历史 K/V 缓存起来,新 token 只需与缓存中的 K/V 做注意力计算。

但传统连续内存分配有个问题:不同请求的序列长度不同,容易产生内存碎片。这个项目采用了 Paged KV Cache 策略——把 KV 缓存切成固定大小的"页"(page),用类似操作系统的分页机制来管理,自动扩容,彻底解决碎片化问题。

2.3 Flash Attention

传统 Attention 的计算复杂度是 O(n²),因为需要把完整的 QK^T 矩阵加载到 GPU 显存中做 softmax。Flash Attention 的核心优化是 tiling + recomputation:

  • 把矩阵切成小块(tile),每块 fit 进 L1 缓存
  • 先算 softmax 的 online 版本(不需要存中间矩阵)
  • 反向传播时重新计算中间结果(用时间换空间)

这个项目通过 TinyFA 库实现 Flash Attention,在长序列场景下可以显著降低显存占用并提升速度。

2.4 连续批处理(Continuous Batching)

传统批处理有一个问题:一批请求中,如果某个请求先生成完了,它占用的计算资源就浪费了,要等整批结束才能处理新请求。连续批处理的做法是:

  • 每个请求独立跟踪自己的生成进度
  • 一个请求生成完毕后,立即从队列中取出新请求填补空缺
  • 预填充(prefill)阶段也支持分块(chunked),避免长 prompt 阻塞短请求

2.5 张量并行(Tensor Parallelism)

当模型太大单卡放不下时,可以把权重矩阵沿某个维度切分到多张 GPU 上。以注意力层为例,Q/K/V 三个投影矩阵分别放在不同的 GPU 上,每卡算自己那部分,最后用 NCCL 做 all-reduce 合并结果。

三、架构设计思路

3.1 项目目录结构

整个项目的源码组织得非常清晰,按功能域分成了几个独立的目录:

src/
├── CMakeLists.txt              # 构建配置
├── binding/                    # 语言绑定(Python/Rust等)
├── distributed/                # 分布式推理(NCCL张量并行)
├── engine/                     # 推理引擎核心
│   ├── GPTEngine.cpp/h         # GPT推理引擎主类
│   ├── PagedKVCache.cpp/h      # 分页KV缓存
│   ├── Scheduler.cpp/h         # 调度器(连续批处理)
│   ├── Sampler.cpp/h           # 采样器
│   ├── ForwardContext.cpp/h    # 前向传播上下文
│   └── CUDAGraphRunner.cpp/h   # CUDA Graph执行器
├── huggingface/                # HF模型权重加载
├── kernel/                     # CUDA算子
│   ├── AttentionOps.cu/h       # 注意力算子
│   ├── Dispatch.h              # 类型分发器
│   ├── EmbeddingOps.cu/h       # 词嵌入算子
│   ├── GemvOps.cu/h            # 矩阵向量乘法
│   ├── RopeOps.cu/h            # RoPE位置编码
│   └── SamplerOps.cu/h         # 采样CUDA算子
├── layer/                      # 网络层
│   ├── Activation.h            # 激活函数
│   ├── Attention.h             # 注意力层
│   ├── DecoderLayer.h          # Decoder层
│   ├── GatedMLP.h              # 门控MLP
│   ├── Linear.h                # 线性层
│   └── RoPE.h                  # RoPE层封装
├── model/                      # 模型定义
├── tokenizer/                  # 分词器
│   ├── Tokenizer.cpp/h         # 分词器主类
│   ├── BPE.cpp/h               # BPE核心
│   ├── Base.cpp/h              # 分词器基类
│   ├── ByteFallback.cpp/h      # Byte Fallback
│   ├── ByteLevel.cpp/h         # ByteLevel预处理
│   ├── ChatTemplate.cpp/h      # Chat模板
│   ├── Fuse.cpp/h              # 分词融合
│   ├── Metaspace.cpp/h         # 元空间处理
│   ├── Regex.cpp/h             # 正则表达式
│   ├── Replace.cpp/h           # 字符替换
│   ├── Split.cpp/h             # 分割
│   ├── Strip.cpp/h             # 去除空白
│   ├── TemplateProcessing.cpp/h# 模板处理
│   └── UnicodeNorm.cpp/h       # Unicode标准化
└── util/                       # 工具函数

If you need the complete source code, please add the WeChat number (c17865354792)

3.2 数据流向总览

用户文本输入
    │
    ▼
┌─────────────┐
│  Tokenizer   │  ← BPE + ByteLevel + Metaspace + Regex + Replace + Split + Strip
└──────┬──────┘
       │ token IDs
       ▼
┌─────────────┐
│  GPTEngine   │  ← 调度器(Scheduler) + 分页KV缓存(PagedKVCache)
│              │    前向传播(ForwardContext) + CUDA Graph
└──────┬──────┘
       │ logits
       ▼
┌─────────────┐
│   Sampler    │  ← top-p / top-k / temperature / min-p
└──────┬──────┘
       │ new token ID
       ▼
┌─────────────┐
│  Decoder     │  ← DecoderLayer × N
│  Layers      │    [Attention(RoPE+FlashAttention) + GatedMLP(SwiGLU) + Linear]
└──────┬──────┘
       │
       ▼
  输出文本(通过Tokenizer decode)

3.3 依赖组件说明

依赖库用途
TinyTorch张量运算(矩阵乘法、归一化等)
TinyFAFlash Attention 实现
RapidJSONJSON 解析(配置文件/API请求)
pcre2正则表达式(分词器预处理)
utf8procUnicode 标准化处理
ankerl::unordered_dense高性能哈希表(词表查找)
moodycamel::ConcurrentQueue无锁并发队列(调度器任务队列)
cpp-httplib轻量级 HTTP 服务器

四、核心代码实现原理

4.1 分词器管道:从文本到 Token ID

分词器是这个引擎的第一个关卡。它的实现采用了**处理管道(Pipeline)**模式,每个预处理步骤都是一个独立的模块:

原始文本 → UnicodeNorm(NFKC标准化) → Replace(字符替换) → Split(分割) → 
Strip(去空白) → ByteLevel(字节级编码) → BPE(合并) → Metaspace(元空间) → Token IDs

核心文件 Tokenizer.cpp 负责协调整个管道。BPE 合并逻辑在 BPE.cpp 中实现,使用了 ankerl::unordered_dense 哈希表来存储合并规则,查找速度远超标准 std::unordered_map。

特别值得关注的是 ChatTemplate.cpp(约 50KB 代码量最大的文件),它实现了一个类 Jinja2 的模板引擎,支持各种模型的对话格式(LLaMA、Qwen、Mistral 等都有各自的 chat template)。这意味着你不需要手动拼接 system/user/assistant 消息,引擎会自动处理。

4.2 推理引擎核心:GPTEngine 与 Scheduler

GPTEngine 是整个推理的总控类。它的主要职责包括:

  • 加载模型权重(从 HuggingFace safetensors 格式)
  • 管理 KV Cache 的生命周期
  • 协调前向传播的各个阶段
  • 处理多请求的调度与批处理

Scheduler(约 27KB 代码)是最复杂的模块之一,实现了连续批处理的核心逻辑:

请求队列 → 预填充阶段(chunked prefill) → 解码阶段(逐token生成)
    │                                        │
    ▼                                        ▼
新请求进入队列                          完成请求移出队列
空闲slot → 从队列取新请求预填充          请求完成→释放KV Cache页

关键设计点:

  • Chunked Prefill:长 prompt 不会一次性全部预填充,而是分成多个 chunk 逐步处理,避免阻塞短请求
  • Auto-sizing KV Cache:分页缓存自动扩容,不需要用户手动指定最大序列长度
  • CUDA Graph:对解码阶段(形状固定的推理)捕获 CUDA Graph,消除 CPU-GPU 同步开销

4.3 分页 KV Cache:PagedKVCache

分页 KV Cache 的思路借鉴了 vLLM 的设计。核心数据结构如下:

KV Cache 物理存储 = 连续的 GPU 内存块(预分配)
    │
    ├── Page 0: [KV_block_0, KV_block_1, ..., KV_block_N]
    ├── Page 1: [KV_block_{N+1}, ..., KV_block_{2N}]
    └── ...
    
每个请求的 KV 状态 = 一组 Page 的索引列表(类似进程的地址空间)

PagedKVCache.h 定义了页面管理接口,PagedKVCache.cpp 实现了页面的分配、释放和复用。当序列长度超过当前分配的页面数时,自动申请新的物理页面并追加到序列中。

4.4 CUDA 算子层:kernel/ 目录

kernel/ 目录包含了所有 GPU 算子的 CUDA 实现:

AttentionOps.cu — 注意力算子,集成 Flash Attention:

  • 实现 scaled dot-product attention:Attention(Q,K,V) = softmax(QK^T / √d_k)V
  • 通过 TinyFA 库调用优化的 Flash Attention kernel
  • 支持 GQA(Grouped Query Attention),即多个 query head 共享一个 key/value head

RopeOps.cu — RoPE 旋转位置编码:

  • 对 Q 和 K 向量施加旋转矩阵,注入位置信息
  • 公式:RoPE(pos, head) = R(pos) × W,其中 R 是旋转矩阵

GemvOps.cu — 矩阵向量乘法:

  • 用于 Decoder 层中的线性投影(Q/K/V/W/O 的矩阵乘法)
  • 针对 decode 阶段(batch size = 1)做了专门优化

SamplerOps.cu — 采样算子:

  • 在 GPU 上直接执行 top-p、temperature scaling、multinomial sampling
  • 避免 logits 从 GPU 拷贝到 CPU 再采样的开销

Dispatch.h — 类型分发器:

  • 编译期根据数据类型(FP32/FP16/BF16)分发到不同的 kernel 实例
  • 用模板元编程实现零开销的类型抽象

4.5 网络层实现:layer/ 目录

DecoderLayer.h — 每一层 Decoder 的结构:

输入 → LayerNorm → MultiHeadAttention(RoPE + FlashAttention) → Dropout → 残差连接 → 
LayerNorm → GatedMLP(SwiGLU) → Dropout → 残差连接 → 输出

GatedMLP.h — 门控 MLP(SwiGLU 激活):

  • 现代 LLM(Llama、Qwen)普遍使用 SwiGLU 替代传统的 ReLU/GELU
  • 结构:MLP(x) = (Linear₁(x) ⊙ SiLU(Linear₂(x))) × Linear₃(x)
  • 其中 ⊙ 是逐元素乘法,SiLU(x) = x × sigmoid(x)

Attention.h — 注意力层封装:

  • 管理 Q/K/V/O 四个投影矩阵
  • 调用 kernel/ 中的 AttentionOps 执行实际计算
  • 支持 GQA:当 num_query_heads > num_kv_heads 时,K/V 做 repeat 广播

4.6 多 GPU 张量并行:distributed/ 目录

张量并行的核心思路是把大矩阵沿行或列切分,多卡并行计算:

模型层权重 W [shape: (out_dim, in_dim)] 沿列切分为 W₀, W₁, ..., W_{N-1}
    │
    ├── GPU 0: 只持有 W₀,计算 out_part₀ = X @ W₀
    ├── GPU 1: 只持有 W₁,计算 out_part₁ = X @ W₁
    ├── ...
    └── GPU N-1: 只持有 W_{N-1},计算 out_part_{N-1} = X @ W_{N-1}
    
    最后 all-reduce 合并: output = out_part₀ + out_part₁ + ... + out_part_{N-1}

初始化方式支持两种:

  • env://(默认):通过环境变量 RANK、WORLD_SIZE、MASTER_ADDR、MASTER_PORT 初始化 NCCL 通信
  • tcp://host:port:直接指定 TCP 地址进行 rendezvous

五、环境配置与运行测试教程

5.1 基础环境要求

  • CMake ≥ 3.10
  • C++17 兼容编译器(GCC 7+ / Clang 5+ / MSVC 2017+)
  • CUDA Toolkit 11.0+(GPU 推理必需)
  • NCCL(多 GPU 张量并行必需)

5.2 编译步骤

# 1. 克隆仓库(注意 --recurse-submodules 获取子模块依赖)
git clone --recurse-submodules 代码托管平台上的项目仓库地址
cd 项目根目录

# 2. 创建构建目录
mkdir build
cd build

# 3. 配置构建(Release 模式)
cmake -B . -DCMAKE_BUILD_TYPE=Release

# 4. 编译
cmake --build . --config Release

5.3 下载模型文件

支持从 HuggingFace 下载模型:

# Llama 3.2
git clone HuggingFace 上的 Llama 3.2 1B 模型仓库

# Qwen 2.5
git clone HuggingFace 上的 Qwen2.5 0.5B 模型仓库
git clone HuggingFace 上的 Qwen2.5 3B 模型仓库

# Qwen 3
git clone HuggingFace 上的 Qwen3 1.7B 模型仓库

# Mistral
git clone HuggingFace 上的 Mistral 7B v0.3 模型仓库

5.4 推理测试

cd examples/inference/bin
./推理示例程序 --model /path/to/model \
  --device cuda \
  --dtype bf16 \
  --max-tokens 32 \
  --temperature 0.8 \
  --top-p 0.9 \
  --input "The future of AI is"

参数说明:

参数默认值说明
–model(必填)HuggingFace 模型目录路径
–devicecuda设备类型(cpu/cuda)
–dtypebf16数据精度(fp32/fp16/bf16)
–max-tokens32最大生成 token 数
–temperature0.8采样温度(越高越随机)
–top-p0.9Top-p 核采样阈值
–input“The future of AI is”输入提示文本
–max-graph-batch64CUDA Graph 最大 batch size
–tensor-parallel1张量并行 GPU 数量
–tp-initenv://并行初始化方法

5.5 启动 HTTP 服务器(OpenAI 兼容 API)

cd server/bin
./GPT --model /path/to/model --port 8080

启动后可以通过浏览器访问内置 Web UI,也可以通过 API 调用:

端点方法说明
/v1/modelsGET列出可用模型
/v1/completionsPOST文本补全
/v1/chat/completionsPOST对话补全(支持 SSE 流式)

服务器额外参数:

参数默认值说明
–host0.0.0.0服务器监听地址
–max-tokens4096每请求最大生成 token 数
–min-p0.0Min-p 采样阈值
–chat-templateauto自定义对话模板(Jinja2 字符串或文件路径)
–web-dirautoWeb UI 目录路径
–max-batch-tokens8192每批最大 token 数
–prefill-chunk-size512预填充每步最大 token 数

5.6 多 GPU 张量并行推理

以 2 张 GPU 为例:

# 终端 1:Rank 0(同时担任 HTTP 服务器)
RANK=0 WORLD_SIZE=2 MASTER_ADDR=127.0.0.1 MASTER_PORT=29500 \
  ./GPT --model /path/to/model --tensor-parallel 2

# 终端 2:Rank 1(纯工作进程)
RANK=1 WORLD_SIZE=2 MASTER_ADDR=127.0.0.1 MASTER_PORT=29500 \
  ./GPT --model /path/to/model --tensor-parallel 2

注意:张量并行要求 --device cuda 且模型不能使用 tied word embeddings(即输入嵌入和输出嵌入权重不共享)。

5.7 Python 绑定

项目还提供了 Python 绑定,可以直接在 Python 中使用:

# pip install .
import tinygpt

enc = tinygpt.Tokenizer()
enc.init_with_config("tokenizer.json", "tokenizer_config.json")
ids = enc.encode("This is a test")

六、五大落地用途

6.1 学习研究

这是该项目最大的价值所在。整个推理管线——从分词、权重加载、前向传播到采样生成——全部用 C++ 手写,没有依赖任何深度学习框架。对于想深入理解 LLM 推理底层原理的开发者来说,这是一份极好的"解剖教材"。

6.2 算子优化实验

由于所有 CUDA kernel 都是手写代码,你可以直接修改 kernel/ 目录下的 .cu 文件进行性能调优。比如替换 GemvOps 中的矩阵乘法实现、调整 Flash Attention 的 tile size、或者尝试新的量化方案。

6.3 生产环境部署

纯 C++ 实现意味着没有 Python 运行时依赖,编译出来的二进制文件可以直接部署到生产服务器。配合 HTTP 服务器和 OpenAI 兼容 API,可以快速搭建私有化 LLM 服务。

6.4 多卡推理扩展

内置的 NCCL 张量并行支持多 GPU 推理,对于 7B 以上规模的模型,单卡显存可能不够。通过简单的环境变量配置即可扩展到多卡,适合拥有多 GPU 的实验室或中小型企业。

6.5 边缘设备适配

虽然目前主要面向 GPU,但项目也支持 CPU 推理模式(--device cpu)。结合模型量化技术,未来可以部署到资源受限的边缘设备上。

七、总结与展望

这个项目展示了一个完整的、工业级质量的 C++ LLM 推理引擎应该长什么样:

  • 分层清晰:从 tokenizer → engine → kernel → layer,每层职责单一,易于理解和修改
  • 优化到位:分页 KV Cache、连续批处理、Flash Attention、CUDA Graph、多精度支持——这些都是现代推理框架的标配
  • 工程完善:CMake 构建、多语言绑定、HTTP 服务器、Web UI、多 GPU 并行——开箱即用

未来可能的优化方向包括:INT4/INT8 量化推理、Speculative Decoding(投机解码)、更细粒度的流水线并行、以及向 WebAssembly 等更多平台的移植。

对于任何想要深入理解大模型推理底层实现、或者需要在生产环境中部署轻量级 LLM 服务的开发者来说,这个项目都值得花时间去仔细研读源码。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐