AI 创作说明:本项目的全部代码、注释与文档均由 Anthropic 的 AI 编程助手 Claude Code(模型 Claude Opus 4.8)编写,人类作者负责提出需求、把控方向与 验收结果。项目定位为教学与学习用途。
用**纯 C 语言(C99)**编写的大语言模型推理引擎,既是命令行工具,也可编译成
静态/动态库嵌入宿主程序(公共 API 见 tinyqwen.h)。
⚡ 冷启动极快:得益于 GGUF 权重 mmap 零拷贝映射(页由内核按需加载,
不做任何预读或反量化预处理),从进程启动到能开始回答几乎是瞬时的——
不像多数框架那样要先把几百 MB 到数 GB 的权重读进内存、构建运行时。
配合零依赖单文件,gcc 一行编译 → 立即运行。
✅ 已在 Windows、Linux、macOS 三大平台测试通过(x86-64 与 aarch64 均验证)。
特点:
- 零依赖:只用 C 标准库 + 各平台原生 API(
mmap/ Win32),不链接任何第三方库 - 单文件:全部逻辑都在
tinyqwen.c一个文件里(注释丰富) - 直接读 GGUF:解析 GGUF 模型文件格式,
权重、超参数、分词器词表全部来自一个
.gguf文件,无需额外的 tokenizer 文件 - 量化推理:支持 Q4_0 / Q4_1 的 4bit 块量化、Q8_0 的 8bit 块量化、 Q6_K 超块量化以及 F16/F32,推理时逐块反量化做矩阵乘法, 覆盖官方 Q4_0 与 Q8_0 量化文件的全部张量组合
- 原生多线程:自带跨平台线程池(POSIX pthread / Win32 线程双实现,不依赖 OpenMP),
线程数用
-j指定,默认自动取 min(核数, 32) - SIMD 运行时检测:x86 用 CPUID 检测 AVX2+FMA,aarch64 用 HWCAP 检测 NEON,支持则切换到向量化点积内核(x86 实测约 3~11 倍提速), 不支持自动退回标量路径——同一个二进制到处能跑
- 命令行问答交互:启动后加载模型,进入多轮对话 REPL(ChatML 模板 + KV 缓存记忆)
- 嵌入与重排:同一程序还支持 Qwen3-Embedding(文本向量/相似度)与 Qwen3-Reranker(查询-文档相关性打分)模型
支持 Qwen3 全系列稠密模型(0.6B / 1.7B / 4B / 8B / 14B / 32B,含 Embedding 与 Reranker 变体)——层数、维度、头数等结构差异全部由 GGUF 元数据驱动,已实测 0.6B / 4B / Embedding-0.6B / Reranker-0.6B。
make # 命令行工具 ./tinyqwen
make libs # 库: libtinyqwen.so (动态) + libtinyqwen.a (静态)
make examples # 库用法示例: example-static / example-shared或者手动编译(真的只有一条命令):
gcc -O2 -std=c99 -o tinyqwen tinyqwen.c -lm -pthread # Linux / macOS
gcc -O2 -o tinyqwen.exe tinyqwen.c # Windows (MinGW)
cl /O2 /utf-8 tinyqwen.c # Windows (MSVC)(MSVC 的 /utf-8 不能省:源码为 UTF-8 编码,MSVC 默认按系统代码页解析。)
SIMD 无需编译开关:AVX2 内核用按函数的目标属性编译,运行时检测启用。
公共 API 只有 8 个函数(详见 tinyqwen.h 的文档注释,用法示例见 example.c):
#include "tinyqwen.h"
static int on_token(const char *piece, int len, void *ud) {
fwrite(piece, 1, len, stdout); /* 流式接收回复 */
return 0; /* 返回非 0 中断生成 */
}
int main(void) {
TqModel *m = tq_load("models/Qwen3-0.6B-Q4_0.gguf", NULL);
if (!m) { fprintf(stderr, "%s\n", tq_last_error()); return 1; }
tq_chat(m, "你好", NULL, on_token, NULL); /* 多轮状态在句柄内 */
tq_reset(m); /* 开启新对话 */
float vec[1024]; /* tq_dim(m) 维 */
tq_embed(m, "文本", NULL, vec); /* Embedding 模型 */
float s = tq_rerank(m, "查询", "文档", NULL); /* Reranker 模型 */
tq_free(m);
return 0;
}# 静态链接
gcc -O2 -o app app.c libtinyqwen.a -lm -pthread
# 动态链接
gcc -O2 -o app app.c -L. -ltinyqwen -lm -pthread要点:内部函数统一返回错误码并逐层向上传递——库调用失败返回 NULL/负值
(描述见 tq_last_error()),永远不会杀死宿主进程;只有命令行工具在检查到
错误后才打印并退出。单个句柄不可并发调用;进程内多个句柄共享线程池。
-DTINYQWEN_LIB 只是去掉 main(),库与命令行工具共用同一份实现。
mkdir -p models
# 0.6B (382MB, 内存占用小, 速度快)
curl -L -o models/Qwen3-0.6B-Q4_0.gguf \
"https://modelscope.cn/models/unsloth/Qwen3-0.6B-GGUF/resolve/master/Qwen3-0.6B-Q4_0.gguf"
# 4B (2.4GB, 回答质量明显更好)
curl -L -o models/Qwen3-4B-Q4_0.gguf \
"https://modelscope.cn/models/unsloth/Qwen3-4B-GGUF/resolve/master/Qwen3-4B-Q4_0.gguf"其它规格同理:把 URL 中的 0.6B 换成 1.7B/8B/14B 即可
(也可以用 HuggingFace 上 unsloth/Qwen3-*-GGUF 仓库的同名文件)。
./tinyqwen models/Qwen3-0.6B-Q4_0.gguf # 或 models/Qwen3-4B-Q4_0.gguf示例会话:
模型: 28 层, 隐层 1024, 16/8 头(每头 128), FFN 3072, 词表 151936
上下文: 4096 token (KV 缓存 896 MB)
线程: 32 (POSIX pthread, -j 可调) · SIMD: AVX2+FMA(运行时检测)
进入交互模式: 输入问题回车提问; /clear 清空对话; /exit 或 Ctrl-D 退出
用户> 我叫小明,请记住我的名字。
助手> 你好,小明!记得我的名字。
用户> 我叫什么名字?
助手> 我叫小明。
| 选项 | 说明 |
|---|---|
-t <float> |
采样温度,默认 0.7;-t 0 为贪心解码(确定性输出) |
-p <float> |
top-p 核采样阈值,默认 0.8 |
-s <int> |
随机种子,默认取当前时间 |
-c <int> |
上下文长度上限,默认 4096(KV 缓存约 896MB,内存紧张可调小) |
-n <int> |
单轮最大生成 token 数,默认 1024 |
-j <int> |
线程数,默认 0 = 自动(min(CPU 核数, 32)),-j 1 纯单线程 |
--think |
启用 Qwen3 思考模式(回复前先生成 <think>...</think> 推理过程) |
--no-simd |
强制标量内核(不检测/不使用 AVX2) |
--prompt <文本> |
非交互模式:回答一个问题后退出 |
--embed <文本>... |
嵌入模式:单文本输出 L2 归一化向量,多文本输出余弦相似度矩阵(须放最后) |
--rerank <查询> <文档>... |
重排模式:输出各文档相关性得分与排名(须放最后) |
--instruct <指令> |
检索任务描述,配合 --embed(包装第一个文本为查询)或 --rerank |
--selftest |
对比标量与 AVX2 内核的数值误差后退出 |
--inspect |
打印模型元数据与全部张量列表后退出 |
--test-tokenizer <文本> |
分词器自检:编码→打印 token→解码→比对 |
REPL 内命令:/clear 清空对话历史,/exit(或 Ctrl-D)退出。
# 下载(ModelScope)
curl -L -o models/Qwen3-Embedding-0.6B-Q8_0.gguf \
"https://modelscope.cn/models/Qwen/Qwen3-Embedding-0.6B-GGUF/resolve/master/Qwen3-Embedding-0.6B-Q8_0.gguf"
curl -L -o models/Qwen3-Reranker-0.6B-q4_0.gguf \
"https://modelscope.cn/models/Mungert/Qwen3-Reranker-0.6B-GGUF/resolve/master/Qwen3-Reranker-0.6B-q4_0.gguf"
# 文本相似度(向量已 L2 归一化, 点积即余弦)
./tinyqwen models/Qwen3-Embedding-0.6B-Q8_0.gguf \
--embed "北京是中国的首都" "The capital of China is Beijing" "我想吃苹果"
# [0]-[1] 0.86 (中英互译) [0]-[2] 0.27 (无关)
# 检索式嵌入: --instruct 把第一个文本按官方模板包装为查询
./tinyqwen models/Qwen3-Embedding-0.6B-Q8_0.gguf \
--instruct "Given a web search query, retrieve relevant passages that answer the query" \
--embed "什么是光合作用" "光合作用是植物利用光能合成有机物的过程" "股市今天上涨"
# 重排: 相关性得分 = 模型对"该文档是否满足查询"回答 yes 的概率
./tinyqwen models/Qwen3-Reranker-0.6B-q4_0.gguf \
--rerank "什么是光合作用" "光合作用是植物利用光能合成有机物的过程" "股市今天上涨"
# 文档[0] 0.9990 文档[1] 0.0001实现说明:嵌入 = 末尾 <|endoftext|> 位置经最终 RMSNorm 的隐状态(完全跳过
lm_head 词表投影),重排 = 官方 ChatML 模板下末位 logits 的 yes/no 二元 softmax。
| 分区 | 内容 |
|---|---|
| [0] 通用工具 | 错误处理、fp16→f32 转换(含 65536 项查找表) |
| [0b] 跨平台线程池 | pthread / Win32 双实现的 parallel_for(常驻线程 + 条件变量同步) |
| [1] GGUF 解析 | mmap 打开文件(零拷贝,Windows 用 fread),解析头部/元数据/张量目录 |
| [2] 分词器 | GPT-2 风格字节级 BPE:字节↔Unicode 映射、哈希表、贪心合并编码、解码 |
| [3] 模型加载 | 读取超参数、按名字定位 28 层权重、RoPE 频率表 |
| [4] 数学内核 | Q4_0/Q4_1/Q6_K 反量化点积(热点)、RMSNorm、Softmax |
| [4b] AVX2 内核 | x86:CPUID 运行时检测;四种量化点积的 SIMD 版本,函数指针分发 |
| [4c] NEON 内核 | aarch64:HWCAP 运行时检测;同一组点积的 128 位 NEON 版本 |
| [5] 前向传播 | QK-Norm、RoPE(NeoX)、GQA 因果注意力、KV 缓存、SwiGLU FFN |
| [6] 采样器 | temperature + top-p 核采样,xorshift64* 随机数 |
| [7] 聊天主循环 | ChatML 模板拼接、逐 token 流式回调生成 |
| [7a] 嵌入与重排 | 最后 token 池化 + L2 归一化;重排模板 + yes/no 概率 |
| [8] 库 API | tq_load/tq_chat/tq_embed/tq_rerank 等 8 个导出函数 |
| [9] 命令行工具 | 参数解析、REPL(-DTINYQWEN_LIB 时不编译) |
- Q4_0 量化:每 32 个权重一个块(18 字节 = fp16 缩放 + 16 字节半字节),
权重 = (4bit值 - 8) × 缩放。矩阵乘时逐块"反量化+点积"融合计算,不生成中间浮点矩阵。 - Qwen3 结构特点:GQA(16 查询头共享 8 个 KV 头)、每头 128 维、 注意力前对每头做 QK-RMSNorm、RoPE 采用 NeoX 风格半维配对、SwiGLU 激活、 输出层与词嵌入共享权重(从 Q6_K 张量读取)。
- 多轮对话:KV 缓存跨轮保留,每轮只把新增 token 喂给模型; 上下文写满时自动清空历史重新开始。
- 思考模式:Qwen3 约定在 assistant 开头插入空
<think>\n\n</think>\n\n块即关闭思考;--think则让模型自由生成思考过程。
| 配置 | Qwen3-0.6B | Qwen3-4B |
|---|---|---|
| AVX2 + 32 线程(默认) | ~40 tok/s | ~15 tok/s |
标量 + 32 线程(--no-simd) |
~13 tok/s | — |
AVX2 单线程(-j 1) |
~12 tok/s | — |
标量单线程(-j 1 --no-simd) |
~1 tok/s | — |
标量内核保留为教学参考与未知平台的兜底路径。--selftest 会用模型真实权重
把标量与 SIMD 内核分别对照 double 精度参考值验证(误差均在 1e-7 量级)。
NEON 是 armv8-a 的强制特性,无需编译开关,运行时经 Linux HWCAP 检测启用
(Apple Silicon / Windows ARM64 按架构恒真),--no-simd 可强制标量。
四种量化内核(Q4_0/Q4_1/Q8_0/Q6_K)均有 128 位 NEON 实现,--selftest
对 double 参考误差在 1e-8 量级,贪心生成输出与标量/x86 逐字一致。
在没有 ARM 硬件时,可用交叉编译 + qemu 做数值校验:
aarch64-linux-gnu-gcc -O2 -std=c99 -static -o tinyqwen-arm64 tinyqwen.c -lm -pthread
qemu-aarch64 ./tinyqwen-arm64 models/Qwen3-0.6B-Q4_0.gguf --selftest # NEON 内核数值校验- GGUF 文件解析(mmap 零拷贝,冷启动瞬时)
- 字节级 BPE 分词器(词表/合并规则来自 GGUF 元数据)
- Transformer 前向传播(QK-Norm + RoPE + GQA + KV 缓存 + SwiGLU)
- 量化内核 Q4_0 / Q4_1 / Q8_0 / Q6_K / F16 / F32
- 三类模型:聊天(多轮 REPL)、Qwen3-Embedding、Qwen3-Reranker
- Qwen3 全系列稠密规格(0.6B ~ 32B,结构由元数据驱动)
- 跨平台原生线程池(POSIX pthread / Win32 线程),
-j指定线程数 - SIMD 运行时检测:x86 AVX2+FMA、aarch64 NEON
- 可编译为静态 / 动态库(
tinyqwen.h公共 API + 示例程序) - 三平台构建通过:Windows、Linux、macOS(x86-64 与 aarch64)
MIT。注意:模型权重文件本身的许可以其发布方为准 (Qwen3 系列模型采用 Apache-2.0 许可发布)。