这是文章系列《从零写一个大模型推理引擎》的配套代码。目标:用 纯 C 跑通 Qwen2.5-0.5B-Instruct 的完整推理,只追求正确、可读,不追求性能。
整条链路都在 C 里:chat template → tokenizer → forward(+KV cache) → 采样 →
detokenize。运行时只吃一个 .bin 文件,不依赖 Python、不读外部分词文件。
Python 只在离线转换时跑一次,把权重和分词器一起打进 .bin。
选 0.5B 而不是 7B:纯 C 朴素实现跑 7B 太慢;0.5B 在 CPU 上可交互,最适合学习。
代码风格遵循 Linux 内核约定:Tab 缩进、struct(不 typedef)、每个算子一个函数、
返回负 errno 表示错误。所有算子以 struct tensor 作为输入输出。
| 文件 | 说明 |
|---|---|
tensor.h / tensor.c |
张量结构体 + 基础算子(matmul/add/prod/rmsnorm/silu/softmax/argmax/embedding_lookup) |
model.h / model.c |
模型加载、KV cache、完整前向(embedding/RoPE/GQA/SwiGLU) |
tokenizer.h / tokenizer.c |
byte-level BPE 分词器(词表/合并规则随 bin 加载) |
infer.c |
交互式多轮对话驱动(chat template + greedy 解码) |
test.c |
基础算子的最小自测 |
convert.py |
把 HF Qwen2.5 权重 + 分词器打成一个 .bin |
Makefile |
构建与自测 |
# macOS
brew install git-lfs
# Debian/Ubuntu
# sudo apt-get install git-lfs
git lfs installgit clone https://www.modelscope.cn/Qwen/Qwen2.5-0.5B-Instruct.git下载完应能看到 model.safetensors(约 942MB,确认不是几百字节的 LFS 指针文件)。
pip install numpy
python3 convert.py ./Qwen2.5-0.5B-Instruct qwen2.5-0.5b.bin转换脚本把所有"麻烦事"都放在 Python 侧:自己解析 safetensors(仅依赖 numpy)、
bf16→fp32、把 HF 的 [out, in] 线性层权重转置成约定的 [in, out]、处理
tie_word_embeddings、并把 vocab/merges 写进 bin。生成的 fp32 文件约 2.3GB。
make # 编译 infer,并运行基础算子自测 run_test
./infer qwen2.5-0.5b.bin然后在 你> 后输入问题,回车即可对话;空行或 Ctrl-D 退出。例如:
你> 你好,请用一句话介绍你自己
助手> 我是来自阿里云的超大规模语言模型,我叫通义千问。
可选参数:./infer qwen2.5-0.5b.bin <max_new> 控制单轮最大生成 token 数。
只跑算子自测:make test,期望全部 [ok]、末行 all tests passed。
magic[4] = "LLMC"
int32 version = 2
# 配置头
int32 hidden_size, num_hidden_layers, num_attention_heads, num_key_value_heads
int32 head_dim, intermediate_size, vocab_size, max_position_embeddings
float rope_theta, rms_norm_eps
# 张量表
int32 n_tensors
repeat:
int32 name_len; char name[name_len]
int32 rows, cols
float data[rows * cols] # 行优先 fp32,线性层已是 [in, out]
# 分词器段(v2 新增)
int32 bos_id, eos_id, eos_id2
int32 n_vocab
repeat:
uint8 is_special; int32 len; char piece[len]
int32 n_merges
repeat:
int32 len_a; char a[len_a]; int32 len_b; char b[len_b]
- 只用 fp32,张量二维行优先连续布局,不支持 stride。
- 权重布局统一为
Y = X @ W,W是[in, out],运行时不做转置。 - matmul 累加器用 fp32,保证数值稳定。
- KV cache:每层一块
[capacity, num_kv_heads*head_dim]内存(用 tensor 表示), 每个新 token 追加一行,多轮对话复用同一份 cache。 - chat template / 特殊 token 按 Qwen2.5 硬编码,只支持本模型。
- 分词预处理为务实近似:ASCII 精确分类,非 ASCII 一律按"字母"处理, 对中英文绝大多数情况与 HF 一致。
- 朴素三层循环 matmul、单线程、无 SIMD/BLAS,速度有限。
- 采样目前是 greedy(argmax),后续会加 temperature / top-k / top-p。
- 上下文容量在
infer.c里固定为CTX(默认 4096)。