Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

learn-llm

~3000 行 C 从零实现 Qwen3-0.6B 推理,不依赖 PyTorch 或任何深度学习框架, 附带 OpenAI 兼容的 HTTP 服务。

License: MIT C11 macOS | Linux

文本 → BPE 分词 → 词向量 → 28 层 Transformer → 采样 → 文本
       分词器、注意力、KV Cache、RoPE、采样、HTTP 服务全部手写

学习性质的项目,目标是彻底搞清一次 LLM 推理里到底发生了什么, 因此优先追求可读可验证:每一步都以 PyTorch 为标准答案做数值对拍, 完整前向预测出的 token id 与 PyTorch 完全一致

快速开始

# 1. 下载模型(约 1.4 GB,默认路径是仓库同级目录)
pip install huggingface_hub
hf download Qwen/Qwen3-0.6B --local-dir ../models/Qwen3-0.6B

# 2. 编译(无外部依赖,只需 C 编译器)
make

# 3. 跑
./qwen3 gen "用一句话解释什么是KV Cache"
./qwen3 chat                # 交互式多轮对话
./qwen3 serve               # HTTP 服务,默认 127.0.0.1:8080

模型放别处用 -m <目录> 指定。其他选项见 ./qwen3 无参数输出。

OpenAI 兼容服务

./qwen3 serve 后,任何 OpenAI 客户端都能直接连:

from openai import OpenAI
c = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="none")
print(c.chat.completions.create(
    model="qwen3-0.6b",
    messages=[{"role": "user", "content": "你好"}]
).choices[0].message.content)

Cherry Studio / ChatBox / LobeChat:添加 OpenAI 类型提供商, API 地址 http://127.0.0.1:8080,密钥填任意非空值,模型 ID qwen3-0.6b

端点
POST /v1/chat/completions 支持 stream=true 的 SSE 流式
GET /v1/models
GET /health

支持 temperature top_p top_k max_tokens stream seed, 返回 usagefinish_reason,带 CORS 头。

性能

Apple M4 / 16 GB,fp32:

matmul 后端 Prefill Decode
make BACKEND=naive 朴素三重循环 7 tok/s 4.1 tok/s
make BACKEND=gcd GCD 多线程 16 tok/s 14.6 tok/s
make Accelerate BLAS(默认) 17~25 tok/s 16~24 tok/s
参照:PyTorch + MPS (GPU) 521 tok/s ~20 tok/s

两个现象值得琢磨:

纯 CPU 的 C 在 decode 上打平 GPU。 每生成一个 token 都要把全部 2.2 GB 权重读一遍,瓶颈是内存带宽而非算力,GPU 的算力优势用不上。 M4 带宽约 120 GB/s,2.2 GB 读一遍 ≈ 18 ms,理论上限 ~55 tok/s——实测正是这个量级。

Prefill 差 20 倍。 本实现逐 token 处理,矩阵退化成向量; PyTorch 把整个提示词打包成大矩阵,GPU 才吃得饱。这正是 batching 存在的理由。

正确性

make ref     # 用 PyTorch 导出参考数值(需 torch + transformers)
make test    # 四组对拍
标准
五个算子 与 PyTorch 误差 ≤ 3e-6
单层前向(KV Cache / 因果掩码 / GQA) 2 层 × 10 token,误差 ≤ 1.1e-5
完整 28 层 + lm_head 预测的 token id 与 PyTorch 完全相同
字节级 BPE 76 条用例编码/解码全部一致

测试里包含反证:故意用错误实现(相邻配对的 RoPE、取模的 GQA 映射、 不减最大值的 softmax),断言它们必须产生不同结果。 只验证"我写的是对的"不够,还要确认测试本身有区分能力。

那些不会报错的坑

这个模型有若干不符合"教科书 Transformer"直觉的地方。写错其中任何一条, 模型都不会崩溃,只会输出通顺但内容错乱的文本——所以才需要上面的逐层对拍。

n_heads × head_dim = 2048 ≠ hidden_size = 1024 q_proj 是升维
RoPE 用 rotate_half 前后对半分 (i, i+64),不是相邻 (0,1)
QK-Norm 在 RoPE 之前 且长度是 head_dim(128),要逐头做
GQA 映射是整除 h / kv_mul,不是取模
softmax 必须减最大值 fp32 下 e^89 溢出成 inf
bf16→fp32 是左移 16 位 不是数值转换
特殊 token 要先于预切分处理 否则 `.<

完整清单与推导见 DESIGN.md

结构

src/
  json.c          极简 JSON(arena 分配 + 转义还原)
  config.c        超参数与派生量
  safetensors.c   mmap + 张量目录解析
  weights.c       bf16 → fp32,加载进单块连续内存
  ops.c           rmsnorm / matmul / rope / softmax / silu
  model.c         RunState + 层前向 + 完整前向
  tokenizer.c     字节级 BPE(预切分 + 字节映射 + 合并)
  sample.c        贪心 / temperature / top-k / top-p
  chat.c          对话模板 + 生成循环
  server.c        OpenAI 兼容 HTTP 服务
  main.c          CLI
tests/
  dump_ref.py     从 PyTorch 导出参考数值
  test_*.c        四组对拍

全程 -Wall -Wextra -Wshadow -Wconversion 零警告。

限制

  • 单请求串行。只有一份 KV Cache,请求排队。真实引擎靠 continuous batching, 需要多份 cache 与动态调度,是另一个量级的工程。
  • 权重 fp32,2.2 GB。int8 量化可降到 ~600 MB,且因 decode 是带宽瓶颈, 速度还会明显提升——这是最值得做的下一步。
  • Prefill 逐 token,没做批量 GEMM。
  • 未实现 NFC 归一化;不支持工具调用、多模态、批量推理。
  • 模型只有 0.6B,专业细节容易出错、多轮指代有时会乱。这是模型能力边界, 不是推理实现的问题——输出与官方 PyTorch 逐 token 一致。

致谢

Qwen3 · llama2.c · HuggingFace transformers(唯一的真理来源与对拍标准)

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages