对应实践:
course/practice/labs/lab13-end-to-end/
主要修改文件:course/practice/labs/lab13-end-to-end/framework/student.c
验证命令:make clean && make test
到了这里,课程已经把后半段需要的几个关键部件都讲过了:
- 采样;
- 对话模板;
- HTTP 协议边界;
- KV cache;
- BPE 核心算子。
如果这些章节只各自孤立存在,学员会很容易形成一种错觉:
自己“每章都懂一点”,但还没有真正把它们重新接成一条完整路径。
Chapter 13 的任务就是解决这件事。
需要特别澄清的是,当前这个 lab 的真实角色并不是“重新训练一个大模型”。
它更接近一次后半段组件整合实验:把 BPE 编码 + 模型前向 + 采样 + 解码 + 简单 chat loop 串起来。
所以这一章最准确的名字,不是“端到端训练”,而是:
用 BPE 输入路径把对话最小闭环重新接通。
13.1 为什么最后一章还要再做一次整合
项目型课程做到后期,最常见的问题不是学员完全不会某一章,而是学员的理解停留在“部件级”。
比如:
- 知道 BPE 会输出 token id;
- 知道模型会吐 logits;
- 知道采样能从 logits 里挑下一个 token;
- 知道 chat loop 会读输入、打输出;
但这些知识如果没有重新装成一条完整数据流,就很容易仍然是散的。
所以课程最后一章最应该做的,不是再引入一个更大的新主题,而是把这些后半段组件重新接起来,让学员明确看到:
- 文本怎样进来;
- token 怎样流经模型;
- 采样怎样把模型输出变回 token;
- token 又怎样被解码成可读文本;
- 这一切怎样放进一个最小 REPL 循环里。
13.2 本章你要建立哪些判断
这一章结束后,你应当能够清楚说出:
- BPE 对话闭环里最小数据流是什么。
- 为什么输入侧要先编码,而输出侧要再解码。
- 为什么单轮
one_turn和外层chat_loop最好分开实现。 - 为什么 cache 让“后续 token 只喂增量输入”成为可能。
- 为什么 BPE 词表和模型权重必须配套使用。
如果这些判断建立起来,课程后半程就真正从“很多局部功能”变成了一条完整系统路径。
13.3 先看 practice target:这章改哪里
本章实践目录是:
course/practice/labs/lab13-end-to-end/
├── TASK.md
├── Makefile
└── framework/
├── student.c <- 主要修改这里
├── student.h
├── verify.c <- 自动验证,不改
└── verify.h
本章需要你实现的函数有四个:
student_bpe_chat_tokenizestudent_bpe_chat_decodestudent_bpe_chat_one_turnstudent_chat_loop
这个切法非常好,因为它刚好把最终整合路径拆成四层:
- 输入编码;
- 输出解码;
- 单轮生成;
- 外层交互循环。
也就是说,这章不是让你面对一个巨大函数,而是把完整路径重新拆回可以一段段观察的形态。
13.4 为什么编码和解码必须单独成为接口
在教学上,把编码和解码拆成独立函数有一个非常大的好处:
它把“文本世界”和“token 世界”的边界明确暴露出来了。
如果你把它们直接写死在单轮对话函数里,学员很容易只看到一条很长的流程,而看不清哪些动作发生在:
- 模型之外;
- 模型之内;
- 模型输出之后。
单独拆成:
student_bpe_chat_tokenizestudent_bpe_chat_decode
就能非常明确地告诉学员:
模型从不直接理解字符串,它只处理 token 序列;字符串和 token 之间的翻译必须明确存在。
13.5 为什么 one_turn 才是整合章节的真正核心
外层 REPL 当然重要,但本章真正的核心还是 student_bpe_chat_one_turn。
因为它正好承接了前面所有后半段主题:
- 用 BPE 把用户输入变成 token;
- 用模型前向得到 logits;
- 用采样策略选出新 token;
- 利用 cache 继续增量生成;
- 再把输出 token 解码回文本。
如果你把这一条链真正写通了,那前面几章学过的内容才第一次全部进入同一个函数视野。
13.6 为什么外层 chat_loop 仍然值得单独写
有些人会觉得外层循环只是“读一行、打一句”,没有什么技术含量。
但课程把它单独拿出来,恰恰是为了让学员看到:系统整合并不总是出在最复杂数学上,有时也出在很朴素的交互边界上。
一个能工作的最小 REPL,至少要明确:
- 输入从哪里读;
- 什么时候退出;
- 回复由谁生成;
- 生成后的内存由谁释放。
这正是很多小系统第一次真正“像个系统”的地方。
13.7 为什么当前 Lab13 不是“训练型端到端”
这里需要把定位讲清楚,不然学员和助教很容易被目录名误导。
当前 lab13-end-to-end 的真实内容,并不是从语料重新开始训练一整条大流水。
它更准确地说,是:
- 在已有 framework 能力之上,
- 把 BPE 输入路径和简单对话回路整合起来。
所以如果你在这一章里期待看到完整训练 epoch、模型文件落盘和复训逻辑,那就会和实际 lab 内容错位。
课程文稿必须把这一点写清楚,避免学员带着错误预期进入最后一章。
13.8 本章实践步骤
task 13.1:先读 student.c 和 verify.c
进入:
cd course/practice/labs/lab13-end-to-end
建议先读:
framework/student.hframework/student.cframework/verify.c
当前验证器会检查:
- 编码接口是否能返回非空 token 序列;
- 解码接口是否能返回非空字符串;
- 单轮生成是否至少能走完一轮;
- 外层 chat loop 是否能读入一轮再退出;
- 空指针防护是否存在。
也就是说,这章并不要求“生成内容很好”,而是优先要求“整条接口链能走通”。
task 13.2:实现 student_bpe_chat_tokenize
这一题最适合先做,因为它最短,而且能把输入边界先稳住。
重点是:
- 明确空指针守护;
- 明确是否加 BOS;
- 明确是否加 EOS。
这会直接决定后面模型前向看到的序列起点长什么样。
task 13.3:实现 student_bpe_chat_decode
这一步和编码刚好相反。
它的意义不仅是“把 token 变回字符串”,更是在提醒你:
生成结束时,模型世界必须重新回到用户世界。
task 13.4:实现 student_bpe_chat_one_turn
这是本章最重要的函数。
建议你在实现时始终围绕一条清晰的数据流思考:
- 先把输入编码;
- 再跑前向;
- 再从最后一行 logits 采样;
- 再把新 token 继续喂回模型;
- 最后解码输出。
如果你把它写成“很多杂乱的小步骤”,很容易丢失主线。
但如果你始终守住这条数据流,整个函数其实并不难理解。
task 13.5:实现 student_chat_loop
这一题的重点不是花哨界面,而是交互闭环:
- 打印欢迎语;
- 读一行;
- 判断
/quit; - 调
one_turn; - 打印回复;
- 释放资源。
到这里,后半段所有组件才第一次真正进入一个面向用户的最小系统。
task 13.6:运行当前真实基线
在还没补完四个函数前,先执行:
make clean && make test
当前 Lab13 的真实初始状态是:
- 2 通过,3 失败。
已经通过的部分主要是:
student_bpe_chat_one_turn当前占位返回了一个非空字符串,因此“不崩”测试能过;chat_loop当前也能打印占位信息后返回,因此“一轮 stdin 后退出”的测试能过。
而失败的部分主要集中在:
- 真实编码;
- 真实解码;
- 空指针守护。
这组结果很有代表性,因为它说明当前 lab 已经能把整条框架链拉起来,但真正的接口细节还没有补全。
task 13.7:完成后重新验证
当你补完四个函数后,再执行:
make clean && make test
如果实现正确,你应该看到:
- token 化和解码都可用;
- 单轮生成能稳定返回字符串;
- chat loop 能完成一次真实回合;
- 空指针边界不会崩。
到这里,课程后半段组件就真正重新接成了一条完整路径。
13.9 常见错误与排查顺序
最常见的错误一般是:
- 编码接口没正确处理 BOS/EOS;
- 解码接口在空输入上行为不明;
one_turn没有把最后一行 logits 正确拿出来;- cache 增量使用方式不清楚;
chat_loop忘了在 EOF 或/quit时退出;- 临时内存释放不完整。
建议排查顺序是:
- 先看 tokenize/decode;
- 再看
one_turn; - 最后看外层 loop。
因为外层 loop 的正确性依赖前面三个接口都已经稳住。
13.10 思考题
- 为什么当前一章最适合先追求“链路通”,而不是先追求“回复质量好”?
- 为什么 BPE 词表和模型权重必须配套使用?
- 为什么把
one_turn和chat_loop分开,会让整合更清晰?
13.11 本章小结
Chapter 13 的意义,不在于再引入一个更新的主题,而在于把后半段已经学过的部件真正重新装起来。
从这一章结束时开始,学员应该能明确看到一条完整路径:
- 文本输入;
- BPE 编码;
- 模型前向;
- 采样生成;
- BPE 解码;
- 外层对话循环。
这条路径一旦真正走通,课程才算把“讲明白”和“做出来”完整闭合。