附录:按需查阅,不必从头读
如果说 A1 解决的是“我缺什么基础工具”,那 A2 解决的就是另一类更常见的现场问题:你已经知道这章要做什么,也已经开始写代码了,但终端里出现的现象和讲义里承诺的不一样。
对项目课而言,这种“不一样”最容易让人慌。因为初学者往往分不清自己看到的是:
- 代码逻辑还没写完导致的正常失败;
- 练习框架自身的初始基线;
- 编译、路径、缓存、模型产物等工程问题;
- 真正的实现错误。
这份附录的任务,就是把这些情况拆开。它不试图代替每章的“常见错误”小节,而是给你一张更全的排错地图:当你面对一个症状时,先判断它属于课程推进的哪一阶段,再看这类阶段最常出现的坑。
A2.0 先建立排错顺序
在看具体条目之前,先记住一个简单但非常重要的顺序。很多人排错越来越乱,不是因为不会调试,而是因为顺序反了。
遇到问题时,优先按下面四步来:
- 先确认你在哪个目录。
很多错误不是实现错了,而是你根本没在当前 lab 里跑命令。 - 再确认你跑的是不是课程要求的标准命令。
当前主线 lab 的默认验证入口是make clean && make test。 - 再判断这是“实验初始失败”还是“你引入的新失败”。
每个 lab 的TASK.md都写了初始基线,先对照那个看。 - 最后才进入代码级调试。
也就是printf、GDB、valgrind、ASAN 这些手段。
只要这四步顺序不乱,大多数问题都能很快缩小范围。
为了让你更快定位,本附录按课程推进阶段组织,而不是简单按“错误类型”平铺。因为同样一句 Segmentation fault,出现在 Chapter 1 和出现在 Chapter 12,语境完全不同。
A2.1 Chapter 0 到 Lab01 入口阶段
这一阶段的问题,通常不是模型逻辑问题,而是课程入口、目录和工具链还没对上。也就是说,你还没真正进入“做题”,先不要上来就怀疑 student.c。
A2.1.1 course/practice/ 目录是空的 / bootstrap-practice.sh 不存在
- 症状:你已经 clone 了课程仓库,但
course/practice/下面是空的,或者只有一个 gitlink;运行bash course/practice/scripts/bootstrap-practice.sh直接报No such file or directory。 - 原因:这通常不是课程损坏,而是仓库内容不完整、目录没切对,或者你拿到的工作区本身就不是课程默认状态。
-
解决:
cd /path/to/miniLLM ls course/practice bash course/practice/scripts/bootstrap-practice.sh如果
course/practice/本身就不存在或明显不完整,先重新确认你拿到的是完整仓库,再继续执行后面的步骤。
这里真正要记住的不是某一条 Git 命令,而是一个判断:目录缺失时,先确认工作区是否完整,不要先猜课程脚本写错了。
A2.1.2 gcc / make 找不到
- 症状:一跑 bootstrap 脚本就看到
gcc: command not found或make: command not found。 - 原因:当前机器还没有主线课程默认假设的 C 工具链。
- 解决:回到 A1.1 环境与版本最低要求 补工具链,再重新跑 bootstrap。
这类错误本质上不是“lab 失败”,而是“还没进入 lab”。不要在 practice 目录里继续硬试其它命令。
A2.1.3 Lab01 一上来就是 3 FAIL + 1 PASS
- 症状:你第一次跑
lab01-step0,发现并不是全通过,而是大概3 FAIL + 1 PASS。 - 原因:这是当前课程里的正常起点。Lab01 的 smoke test 不是为了证明你已经完成 Chapter 1,而是为了证明当前 lab 能编译、能运行、能把“未完成的 TODO”以失败形式显式暴露出来。
- 解决:不要把它当成环境错误。继续去读 Chapter 1 和
lab01-step0/TASK.md,开始补student.c里的最初几个函数。
这条很重要,因为初学者很容易把“初始失败”误读成“课程坏了”。而这门课恰恰是故意把失败暴露出来,作为正式学习的起点。
A2.2 Chapter 1 到 Chapter 5:基础算子与模型骨架阶段
这一阶段学员最常遇到的不是大模型特有问题,而是更底层的工程错误:头文件没找到、维度对不上、softmax 结果爆掉、注意力配置不合法。好消息是,这些错误都比较局部,一旦学会定位,后面很多章节都能复用同样的方法。
A2.2.1 Makefile 找不到头文件
- 症状:
fatal error: tensor.h: No such file or directory,或者其它类似的xxx.h找不到。 - 原因:最常见情况是你没进具体的
stepN/或labXX-stepY/目录,就直接在别处敲了make。另一种情况是Makefile的 include 路径没带上。 -
解决:
pwd make clean && make test如果路径不对,先
cd到正确 lab。
如果路径已经对了,还报错,再去看Makefile里是否包含正确的-I选项。
这类问题表面看像“编译器抱怨”,实质上通常只是工作目录错了。
A2.2.2 hidden_dim 不能被 num_heads 整除
- 症状:看到类似
hidden_dim % num_heads == 0的断言失败,或者 attention 阶段莫名其妙段错误。 - 原因:多头注意力需要把隐藏维度平均切给每个 head。如果总维度不能整除 head 数,后续切分一定会失去边界。
-
解决:同步检查配置里的两个量,只接受下面这种关系:
head_dim = hidden_dim / num_heads hidden_dim % num_heads == 0
如果你是在本课程默认配置下遇到这个问题,优先怀疑配置被改动过;如果是你自己改实验参数,就先回纸上算一遍,不要让代码替你做整数除法猜测。
A2.2.3 矩阵维度不匹配
- 症状:
A.n == B.m一类断言失败,或 reshape 前后元素数对不上。 - 原因:你对张量形状的心理模型和代码里的真实布局不一致。最常见的是把
(seq_len, hidden_dim)和(hidden_dim, seq_len)混了,或者忘了某一步实际上还需要转置。 -
解决:不要立刻埋头改公式,先把维度打印出来:
fprintf(stderr, "Q=[%d,%d] K=[%d,%d]\n", Q.shape[0], Q.shape[1], K.shape[0], K.shape[1]);
很多维度 bug 一旦打印出形状,就已经不是抽象数学问题,而是非常具体的“第 0 维和第 1 维被我想反了”。
A2.2.4 softmax 输出全是 nan
- 症状:loss 一开始就是
nan,或者注意力权重打印出来全是nan。 - 原因:通常是数值稳定性没守住。最常见的三种情况:
- 忘了按
sqrt(head_dim)缩放; - 忘了先减去最大值;
- LayerNorm 的分母保护不够,出现了除零。
- 忘了按
-
解决:回到 Chapter 1 和 Chapter 4 的稳定实现,逐项确认:
float scale = 1.0f / sqrtf((float)head_dim);和:
scores[i] = expf(scores[i] - maxv);
当你看到 nan 时,先别怀疑训练器。数值代码里,nan 往往早在更前面就已经产生了。
A2.3 Chapter 6 到 Chapter 8:完整模型、训练与生成阶段
从这一阶段开始,错误不再只停留在单个算子层面。你会第一次面对“单个函数都没明显错,但整个系统行为不对”的情况。最常见表现是:模型能跑,但 loss 不降;能生成,但只会吐乱码;能算,但缓存和梯度流没有真正连起来。
A2.3.1 model.bin / model_bpe.bin 找不到
- 症状:运行生成或对话命令时,直接报
No such file or directory: model.bin或model_bpe.bin。 - 原因:通常只有三种可能:
- 你还没完成训练或导出;
- 产物文件在当前目录之外;
- 你从错误目录启动了程序。
-
解决:
pwd ls model.bin ls model_bpe.bin先确认文件在不在当前目录,再确认课程这一章是否本来就要求你先训练再生成。
这里最容易犯的错,是把“运行命令成功启动”误认为“模型产物已经准备好了”。对生成阶段来说,权重文件本身就是输入的一部分。
A2.3.2 未训练模型生成乱码
- 症状:模型一开始生成的是乱码、重复 token、空字符或明显无语义内容。
- 原因:这往往不是 bug,而是未训练权重的正常行为。随机初始化模型的 softmax 输出接近均匀分布,采样出来的东西当然像噪声。
-
解决:先对照本章讲义问自己一个问题:我现在看到的,是“课程要求我先观察未训练行为”,还是“我已经完成训练后仍然乱码”?
如果还没训练,这个现象本来就合理。
如果已经训练过,再去看 loss 是否真实下降,或者模型文件是否确实被更新了。
这一条经常能避免无效排错。因为不是所有“不像人话”的输出都意味着实现错了。
A2.3.3 Loss 不下降
- 症状:训练跑了很多步,loss 几乎不动,停在一个高位附近。
- 原因:最典型的几个根因是:
- 学习率太小;
- 标签没有做 GPT 需要的 shift;
- 某层梯度根本没回传;
- 数据本身过于单一。
-
解决:不要同时改十个地方。按下面顺序排:
- 临时把
learning_rate调大一点,看 loss 是否对梯度敏感; - 打印一个 batch,确认标签相对输入做了对齐偏移;
- 在关键层反向传播后看梯度是否全 0;
- 检查训练文本本身是不是几乎没信息量。
- 临时把
训练阶段最忌讳的就是“loss 不降,于是到处乱改”。先确认优化链路有没有活着,再讨论模型质量。
A2.3.4 KV Cache 长度超界
- 症状:多轮生成或对话时,跑着跑着崩掉,或者看到
seq_len > max_seq_len一类错误。 - 原因:KV cache 通常在初始化时按固定最大长度分配;如果上下文不断增长,又没有裁剪或扩容,就会越界。
-
解决:
- 临时减小对话历史长度,确认问题就是“长度累积过多”;
- 从配置里调大
max_seq_len,重新编译; - 在写 cache 之前显式检查边界,让错误更早、更清楚地暴露。
这类问题的本质不是 attention 算错了,而是系统级资源边界没有提前说清楚。
A2.4 Chapter 9 到 Chapter 13:对话、HTTP、BPE 与整合阶段
后半程的错误更容易带有“系统表象”,也就是说,程序看起来已经不是一个单纯的数学实验,而是一个在处理字符串、协议、端口、文件、子词词表的完整程序。这时排错方法也要随之切换:你不再只看张量,还要看文本边界、网络入口和持久化产物。
A2.4.1 HTTP 端口被占用
- 症状:服务启动时报
Address already in use,或curl请求始终连不上。 - 原因:端口已被旧进程或其它服务占用。
-
解决:
lsof -i :8080 ss -tlnp | grep 8080 kill <PID>或者临时换一个端口继续验证。
这里别急着看 HTTP 解析函数。端口冲突是操作系统资源问题,不是协议逻辑问题。
A2.4.2 BPE 训练数据太小
- 症状:训练出来的词表几乎没长大,或者反复合并相同的微小模式,效果很差。
- 原因:BPE 从语料统计中学习高频合并。如果输入文本太短、太单一,它根本没有足够的统计信号。
- 解决:优先增加训练文本长度,而不是一上来调小目标词表或怀疑代码框架。
对 BPE 来说,数据规模本身就是算法输入的一部分。语料太小,不是“代码性能差”,而是任务设定根本不够支撑学习。
A2.4.3 ./*_bpe 输出混乱或和预期不符
- 症状:BPE 相关命令能跑,但输出和
TASK.md里的基线差很远,或者字符边界看起来被切坏。 - 原因:可能是词表文件不匹配、训练产物没更新、编码假设混了,也可能只是还在看“未完成状态”的正常失败。
-
解决:
- 先确认词表和模型文件是不是本章最新产物;
- 再对照
TASK.md里的当前实验基线和完成后目标输出; - 如果差异仍然异常,再逐步检查 merge 逻辑和字符串拼接。
后半程排错的关键,是不要只看最终字符串,要把它拆回“词表是否对、模型是否对、当前测试阶段是否对”三件事。
A2.5 跨阶段通用问题
有些坑几乎会贯穿全课程。它们不属于某一个特定 chapter,但一旦遇到,排错方式比较稳定。
A2.5.1 valgrind: command not found
- 症状:
make memcheck一开始就找不到valgrind。 - 原因:本机没装,或者当前平台不支持。
- 解决:回到 A1.6;如果平台不适合 valgrind,直接用 ASAN 方案,不必强行折腾。
A2.5.2 definitely lost: N bytes
- 症状:valgrind 结尾显示有确定泄漏。
- 原因:申请了内存但没有在所有出口路径上释放。
- 解决:从分配点追所有权,确认函数退出前是否配对释放。必要时缩小到最小复现函数,而不是在完整程序里盲看。
A2.5.3 Invalid read / use-after-free
- 症状:valgrind 报非法读,或提示某地址属于已经释放的块。
- 原因:你释放后还在用,或者某个结构体仍保留旧指针。
- 解决:画生命周期图。
malloc、借用、转移、free各画一条线,任何交叉都值得怀疑。释放后立刻置NULL,能减少二次伤害。
A2.5.4 double free or corruption
- 症状:程序在释放阶段崩,提示二次释放或堆损坏。
- 原因:同一块资源被两个位置都当成“自己负责释放”。
- 解决:回到所有权约定,明确只有一个角色负责真正
free。别试图靠“碰巧只 free 一次”糊过去。
A2.5.5 Makefile 没把新 .c 编进去
- 症状:函数已经写了,但链接时报
undefined reference。 - 原因:对应源文件没进
SRCS。 -
解决:先看
Makefile,不要先怀疑编译器:grep -n '^SRCS' Makefile
课程里很多 lab 的边界都靠 Makefile 明确控制。实现写好了但目标文件没参与构建,是很典型的新手问题。
A2.6 当你完全不知道问题属于哪一类
如果前面的症状都没命中,说明你现在最需要的不是更多经验条目,而是一套能快速收缩范围的方法。下面这组动作比继续翻长文更有效:
- 最小复现
把问题缩到一个最短输入、一个最小函数调用。能在 20 行里复现的 bug,不要留在整套模型里找。 - 补观察点
在关键边界打印形状、长度、状态码、返回值,不要只打印“到这里了”。 - 二分问题位置
从流程中间插断言或打印,看前半段已经错了,还是后半段才错。 - 回看最近改动
对 C 项目来说,最后改过的那几处代码,命中率通常极高。
你真正要避免的,不是“查附录”,而是下面这种状态:
既没有先确认目录和命令,也没有先对照当前 lab 的基线,就直接怀疑整个课程内容有问题。
项目课的调试,很多时候不是更聪明,而是更有秩序。
下一份文档:A3 参考资料与延伸阅读