news 2026/11/3 14:47:58

从零手写大模型:三个月复现语言模型与推理模型全记录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零手写大模型:三个月复现语言模型与推理模型全记录

这两年,AI engineering领域最热的方向绕不开一个词:from scratch。从《Build a Large Language Model (From Scratch)》被技术社区反复推荐,到越来越多人尝试build a reasoning model from scratch,复现类似R1那样的长思维链推理,大家的共识越来越清楚——天天调API、套框架,换来的是对模型内部机制越来越薄的直觉。

这篇文章是我用三个月时间,从零构建自己的小语言模型和推理模型的完整记录。不用Transformers库,不加载任何预训练权重,从tokenizer到训练循环全部手写,最后在千万级参数的小模型上,跑通了一条从语言建模、到CoT微调、再到强化学习推理训练的完整链路。如果你已经会写PyTorch、用过HuggingFace,却总觉得大模型是个黑盒;或者你对"推理模型到底是怎么被训练出来的"这件事充满好奇,那这份记录就是我踩完坑之后最想递给你的地图。

1. 我为什么坚持从零手写,而不是继续用框架调参

先说这段经历的起点。之前几年我一直做模型微调和部署,HuggingFace生态用得飞起,fine-tune、量化、推理加速都上手过,自认为对Transformer相当熟悉。直到有一天同事问我:这段attention的耗时为什么和序列长度呈二次方增长?能不能在不引入FlashAttention的情况下自己写一个融合版本?我愣了一下,发现自己连QKV投影的shape变化都要现翻文档。

那次之后我做了个决定:把手头项目放一放,花三个月时间,不开任何"现成模型",把一个大语言模型从空白搭起来。参考书我主要用《Build a Large Language Model (From Scratch)》(Sebastian Raschka著),配合几个开源课程的代码对照读。项目名就叫ai-engineering-from-scratch,很直白——就是要搞清楚每一步到底发生了什么。

1.1 一个关于注意力层的追问,暴露了我的底牌

我当时的真实水平是:能跑通训练脚本,能调超参,能部署服务,但模型内部每一步算的是什么,基本靠猜。transformers库把一切封装得太好了,embedding层、attention mask、KV cache、layer norm,全是一行调用的事。出了问题,我看错误日志的时间比看论文的时间还多。

"从scratch"这件事最直接的价值就是逼你把手伸进泥里。当你自己写causal mask、自己处理softmax的数值溢出、自己调试首次生成的一堆乱码时,你才会真正理解那些框架层背后的设计决策。build a logic model from scratch(我这里特指逻辑推理模型)的热潮里,很多人都以为难点在模型结构,真正上手才发现,推理模型的难点在采样循环、奖励设计和数据格式上——这些恰恰是框架藏得最深的部分。

1.2 我给自己划定的"从零"边界

"从零构建"如果不划边界,很容易变成某种偏执。我做之前先把范围写清楚了:

  • 手写范围:BPE tokenizer、词嵌入、多头自注意力、前馈网络、LayerNorm、训练循环、采样器、GRPO更新逻辑。
  • 不手写范围:autograd自动微分、CUDA kernel、底层矩阵乘法。AdamW优化器我单独手写了一遍,纯粹为了检验自己对梯度更新的理解。
  • 目标模型:参数规模10M到60M之间,训练数据自己采集并清洗,最后在一套小的数学/逻辑多选题数据集上做CoT + SFT + GRPO。

这里要特别说清楚:我不是反框架派。生产环境该用vLLM用vLLM,该用HuggingFace pipeline就用pipeline。"从零"是教学工具,目的是把直觉补上。等你真正手写过一遍,再回去看框架的源码,你会发现自己第一次能读懂那些抽象层到底在帮你做什么。

2. 动手前必须拿下的三项底层能力,否则后面全是翻车现场

很多人一上来就照教程写Transformer,结果卡在第一个loss就是NaN,然后就开始怀疑人生。我踩过的顺序是:先补底层能力,再写模型。三项里缺任何一项,后面都会花双倍时间补课。

2.1 数学基础:矩阵形状、softmax数值稳定和梯度直觉

做这个项目,你不需要把《深度学习》里的公式全部推导一遍,但有三样东西必须"过手":

第一,每一层的shape变化。输入是(B, T, C),经过embedding变成(B, T, d_model),attention之后还是(B, T, d_model),但Q/K/V的维度怎么拆分、多头怎么reshape和permute,这些必须闭着眼睛写得出来。我写代码期间一半的bug都是shape写错。

第二,softmax的数值稳定。这个坑我踩得特别深:第一版cross entropy在fp16精度下直接溢出,loss变成inf。正确做法是softmax前先减去最大值:scores - scores.max(dim=-1, keepdim=True)。原理很简单,指数函数在负数区间不会溢出。

第三,梯度从loss反向流到每一层的shape直觉。你不需要手算每个偏导,但至少要能回答"这层的梯度为什么是那个shape"。

我建议正式动手前先干一件小事:用纯numpy写一个两层神经网络,手动实现一次反向传播,哪怕不看答案抄一遍也行。这个半天工程能帮你省掉后面两周的排错时间。

2.2 数据管线:清洗时间远超写模型结构的时间

这是我觉得整个项目里最反直觉的一点:写模型结构用了大概一周,清洗数据用了将近两周。很多人跟着教程搭完模型就开训,结果模型生成的全是碎片,第一反应是改结构、调超参,其实问题根源在数据。

我的数据管线长这样:原始收集了大约2.4GB英文文本加少量中文,经过重复行过滤、超短行过滤、语言检测、文档切分之后,剩下1.7GB左右。然后用其中大概5万个代表性句子训练BPE tokenizer,词表大小选8192。这里有个容易被忽视的教训:如果数据集里有一条新闻被重复了几百次,小模型会直接把这条新闻背下来,而不是学语言规律。另外,验证集一定要提前切好,否则训练集里泄漏了验证样本,你看到的loss全是虚的。

2.3 算力预算与实验纪律

我实际用的是一张RTX 4090 24GB,再加一些云GPU按需实例。刚开始千万不要规划什么130B大模型,老老实实从千万级参数起步。我给自己排了三组实验:

实验线参数量序列长度每步batch预计时长
baseline10M256321~2小时
主力模型33M512328~12小时
压力测试60M51216 + 梯度检查点1~2天

实验纪律也很重要:固定随机种子、每个配置一个独立文件夹、每100步记录一次loss、每个实验结束必须导出生成样例。这不是强迫症——三周之后,你根本回想不起哪个配置跑出了最好的loss,那时候你就知道日志和文件夹命名多值钱了。

3. 最小可用LLM的完整拆解:从BPE到能输出"像样句子"

这一节是整个项目最核心的部分。我按实际动手顺序拆开讲,每个模块都附上关键代码和当时的判断逻辑。

3.1 Tokenizer:为什么我坚持自己训练BPE

很多人觉得tokenizer是现成的东西,直接pip install tiktoken不就行了。但从scratch的角度看,加载GPT-2的tokenizer只是调用,不是理解。自己训练BPE会让你明白vocab_size、未登录词、special token到底意味着什么。

我用的BPE核心逻辑是这个,整体思路就是反复找最高频的相邻token对、合并、编码成新token:

import collections from typing import List def get_stats(ids: List[int]): counts = collections.defaultdict(int) for pair in zip(ids[:-1], ids[1:]): counts[pair] += 1 return counts def merge(ids: List[int], pair, new_idx: int): new_ids = [] i = 0 while i < len(ids): if i < len(ids) - 1 and ids[i] == pair[0] and ids[i + 1] == pair[1]: new_ids.append(new_idx) i += 2 else: new_ids.append(ids[i]) i += 1 return new_ids

实际训练前需要先做预分词(把文本切成字母数字片段和标点),然后在预分词后的整数序列上反复执行上面两步。我保留了<|begin_of_reason|>和<|end_of_reason|>两个占位token,专门为后面的推理阶段用,这在后面会提到。

这里有个选型的坑:vocab过大的话,embedding层会吃掉小模型一半以上的参数。对小项目来说,8192到16000都是合适的区间。词表大小本身不需要卷。

3.2 自注意力与Transformer:从公式到可运行代码

接下来是重头戏。我强烈建议先写单头的自注意力,确认理解后再扩成多头。核心是这几件事:Q、K、V从哪里投影来、为什么要除以sqrt(d_k)、因果掩码怎么加。

import torch import torch.nn.functional as F def causal_self_attention(x, w_q, w_k, w_v, w_o): B, T, C = x.shape d = w_q.shape[1] # 每个head的维度 q = x @ w_q # (B, T, d) k = x @ w_k v = x @ w_v scores = q @ k.transpose(-1, -2) / (d ** 0.5) mask = torch.tril(torch.ones(T, T, device=x.device)).view(1, T, T) scores = scores.masked_fill(mask == 0, float("-inf")) weights = F.softmax(scores, dim=-1) out = weights @ v return out @ w_o

注意除以sqrt(d_k)的原因:如果不做这个缩放,当d_k变大时attention分数的方差会跟着变大,softmax会过早饱和,梯度就消失了。这是transformer原论文里刻意设计的细节,自己写一次才会真正记住。

整体结构我选了GPT式的pre-norm:每个block先LayerNorm再进attention/FFN,残差连接包在外面。实测下来,pre-norm在小规模训练里比post-norm稳定得多,学习率稍微大一点也不太容易炸。位置编码用的是可学习参数,最大序列长度设了1024。

写到这里有个体会:你在框架里点一个model.generate()觉得云淡风轻,但自己写gen循环时,会第一次意识到每生成一个token都要重新跑一遍前面的上下文,KV cache的空间换时间意义就是这样暴露出来的。这个直觉在后面看推理框架源码时价值连城。

3.3 训练循环、损失曲线与"能不能说人话"的评判标准

模型结构写完,训练循环反而简单:cross entropy损失、AdamW优化器、warmup 500步 + cosine学习率衰减。我实际的训练曲线大体是这个节奏:

训练步数验证loss生成效果
20006.9左右输出的全是词碎片
100005.6左右能拼出常见短词
300004.7~5.0出现语法残缺的短句
500004.4~4.6常见句式成形

第一次生成出来一堆乱码是正常的,这几乎是必经之路。评估模型别只盯着loss,准备一组固定prompt,每次训练到检查点时用同样的prompt采样几段,对比看有没有改善。千万不要每次换prompt,否则你没法判断哪个改动真正有效。

"能说人话"的标准在从零项目里要放低:它不需要达到GPT-4水平,只要明显学会语言的基本形状,比如空格、词频、标点位置开始合理,就可以进入下一阶段。

4. 从语言模型到推理模型:CoT数据、SFT与GRPO的实操记录

语言模型能生成句子,离"会推理"还有一座山。这部分是build a reasoning model from scratch的核心,也是我认为整个项目里信息密度最高的三个月。

4.1 推理模型到底把"推理"放在了哪里

观察推理模型的输出你会发现,它在给出答案之前会先写一大段内部推理,类似"先看条件A,再看条件B,所以..."。R1路线的基本配方可以概括为:长思维链CoT + 可验证的奖励 + 强化学习。

这里有个很关键的理解:预训练阶段模型学会的是"接续上文生成",而不是"推理"。推理能力很大程度是后训练阶段被奖励信号逼出来的。我做了对照实验:直接把CoT数据喂给模型做SFT,模型学会的是输出格式,它知道应该先写"思考"再写"答案",但在训练集范围外并不会主动展开多步推理。只有用GRPO这类RL方法,让模型在采样中尝试不同的中间步骤,并用答案正确性做奖励,它才会逐渐内化"多写几步思考能提高答对概率"这个因果。

所以我的结论是:推理从零复现的难点不在Transformer结构,而在RL训练系统本身。

4.2 在自己模型上复现CoT效果的三个步骤

第一步,构建小规模CoT数据集。我用的是数学选择题和简单逻辑题,格式统一为「问题、思考、答案」三段。思考部分控制在3~6步,总共约8000条,一半是手写模板生成的,另一半从公开数据集过滤得到。思考步骤不要写太长,太长模型在RL阶段会学到凑字数。

第二步,SFT教格式。训练3个epoch,学习率降到1e-4,context拉长到1024以容纳思考过程。这一步不追求推理变强,只追求一件事:模型稳定输出"思考"再"答案"的格式。评估指标用一个格式合规率:生成结果里有没有正确出现结尾的答案标记。合规率达到90%以上再进RL,否则后面奖励信号会被格式问题搅浑。

第三步,GRPO强化学习。GRPO的核心是对于同一个问题采样一组回答,组内归一化收益,再用归一化后的优势更新策略,同时用KL约束让它不要偏离SFT模型太远。示意代码如下:

def compute_advantages(rewards): # rewards: (group_size,) mean = rewards.mean() std = rewards.std() + 1e-4 return (rewards - mean) / std # 对每个问题采样k条轨迹,记录policy和ref的logprob # ref是冻结的SFT模型,policy是从SFT模型复制出来继续训练的 def grpo_loss(logprobs, ref_logprobs, rewards, kl_beta=0.02): norm_rewards = compute_advantages(rewards) ratio = torch.exp(logprobs - ref_logprobs) kl = torch.exp(ref_logprobs - logprobs) - (ref_logprobs - logprobs) - 1 policy_loss = -torch.mean(torch.clamp(ratio, 0.8, 1.2) * norm_rewards) return policy_loss + kl_beta * kl.mean()

奖励设计我调了很多版,最后稳定到:答案正确+1.0;格式错误(没写思考段或没写答案标记)-0.5;超长截断-1.0。这里最想强调的一点是:不要给"思考过程质量"打主观分,一旦模型发现过程分可以被凑出来,它就会用各种方式hack,后面细说。

4.3 用GRPO做推理微调时我踩到的坑

这一小节全是真金白银的教训,每一条都花了我至少一整天。

坑一:reward hacking。模型很快就学会在思考段里反复重复题目文字,最后随便蒙一个答案。因为格式正确能拿到基础分,重复文字能凑够长度,模型成了"复读机"。对策就是我前面说的:格式相关只给罚分不给正分,唯一正分来源是答案正确。

坑二:奖励稀疏。开放题没法自动判分,模型采样半天也不知道自己对没对,学得很慢。我的解法是RL阶段只用选择题和简单计算题,答案可以程序化校验;开放题留在SFT阶段处理,不放进RL。

坑三:KL系数失控。kl_beta设太小,模型一两个小epoch后输出就开始退化、词频失衡。我最后把kl_beta固定在0.02到0.05之间,并且给ratio加了clip(0.8, 1.2),能显著减缓退化。

坑四:采样长度失控。RL早期模型为了凑满分疯狂输出垃圾,2000个token都不够。给生成设max_new_tokens=512,超长直接截断并按格式错误扣分。

走完这趟,我的体感是:推理从零构建的日常工作,80%在数据和奖励工程,20%才是模型和训练技巧。外界看那些复现项目总觉得难点在"模型有多聪明",实际全在工程细节里。

5. 关于算力、成本和"值不值得从零开始"的实在话

最后聊点计划之外的实在话。很多读者卡在第一步就放弃了,理由是"我没有那么多GPU"。这个心结值得打开。

5.1 没有A100也能练起来

我一直用本地RTX 4090,辅助用一些云GPU按需实例,配合Colab和Kaggle的免费配额做极小实验。核心诀窍是用bf16混合精度训练,一个30M参数模型在512序列长度下,显存占用能压到8GB以内。大致参考:

参数量序列长度精度显存(含Adam状态)参考训练时长
10M512fp32约3GB1~3小时
33M512bf16约6GB8~14小时
60M1024bf16 + 梯度检查点约10GB1~2天

工程上的小习惯很省事:数据加载器记得开num_workers和pin_memory;loss不要每步都写盘,100步写一次;检查点至少留两个,防止训练中途炸了直接回到解放前。别小看这些细节,一次训练跑十几个小时,因为没存检查点而从头再跑的感觉,这辈子不想体验第二次。

5.2 该停止较劲的时候就停止:从零构建的边界

诚实地说,我最后训练出来的模型,单纯从能力上讲没有任何生产价值,它跑不过任何开源小模型。但这正是我推荐每个想做AI engineering的人都走一遍的原因:它的价值是直觉。当你亲自调试过一次梯度爆炸、一次NaN loss、一次RL reward hacking之后,你再排查线上模型的问题,路径会清晰得多,因为你脑内有完整的因果模型,而不是一堆黑盒调用。

做完基础版之后还有很多扩展方向:手写RoPE旋转位置编码、RMSNorm、GQA分组查询注意力、KV cache、投机采样。每补一个模块,对推理框架的理解就深一层。但注意,从零构建是学习工具,不是生产方案;如果你的目标是做一个产品,请立刻回到框架上。

最后说点题外话。这个项目给我最大的回报,不是那个能生成"像样短句"的小模型,而是后来排查模型退化问题时,我几乎不需要翻文档就能猜出是数据环节出了问题,还是哪一层的数值溢出了。如果你也准备花三个月做这件事,我的建议只有一个:把每次实验的loss曲线和生成样例完整存下来,三个月后回看,你会感谢那个愿意从头搭起的自己。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 14:47:55

动态规划解完全平方数问题:从暴力递归到数学优化

1. 题目解析&#xff1a;完全平方数问题 力扣第279题"完全平方数"是一个经典的动态规划问题&#xff0c;题目要求我们找到最少数量的完全平方数&#xff0c;使得它们的和等于给定的正整数n。完全平方数指的是1,4,9,16,...这样能表示为某个整数的平方的数。 这个问题…

作者头像 李华
网站建设 2026/11/3 14:46:17

AI工程从零开始:构建可交付的AI服务系统

1. 这不是“搭个LLM API”——AI工程从零开始的真实含义很多人看到“AI Engineering from Scratch”第一反应是&#xff1a;哦&#xff0c;手写一个Transformer&#xff1f;或者用PyTorch从头实现Attention&#xff1f;不是。这标题里藏着一个被严重低估的行业共识&#xff1a;…

作者头像 李华
网站建设 2026/11/3 14:45:26

DAMA数据管理知识地图:从数据本质到治理落地的全景解析

简介&#xff1a;面向数据治理从业者、数据架构师与备考DAMA认证的学习者&#xff0c;这份DAMA2数据管理知识体系资料以DAMA BOK为脉络&#xff0c;系统梳理数据管理的基本概念与落地框架&#xff0c;帮助读者建立从数据本质、语境与元数据&#xff0c;到数据架构、建模、治理及…

作者头像 李华
网站建设 2026/11/3 14:42:56

hindsight系统性复盘工程:基于Python+Docker+OpenAI的可观测性实践

1. 项目概述&#xff1a;hindsight 不是“事后诸葛亮”&#xff0c;而是一套可落地的系统性复盘工程实践 最近在多个技术团队的内部分享会上&#xff0c;我反复听到一个词—— hindsight 。它不是指那种“早知道就该那样做”的懊悔式感慨&#xff0c;而是指一套 基于真实运行…

作者头像 李华
网站建设 2026/11/3 14:42:40

树莓派环境监测站:从传感器到数据可视化

1. 项目概述 树莓派作为一款信用卡大小的微型计算机&#xff0c;凭借其丰富的GPIO接口和强大的扩展能力&#xff0c;成为物联网开发的理想平台。这个项目将指导你使用树莓派配合常见传感器&#xff0c;构建一个功能完善的环境监测站&#xff0c;实时采集温度、湿度等环境数据&a…

作者头像 李华
网站建设 2026/11/3 14:39:58

Univer文档引擎:可嵌入、可定制的国产开源协同内核

1. Univer 是什么&#xff1a;一个被严重低估的国产开源文档协同引擎你可能在 GitHub 上刷到过它&#xff0c;也可能在阿里云开发者大会的某个角落听过它的名字&#xff0c;甚至在某家 SaaS 创业公司的技术选型会上看到工程师快速敲出一行import { Univer } from univerjs/core…

作者头像 李华