ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从零构建大语言模型:手写Tokenizers、Transformer到部署全流程

从零构建大语言模型:手写Tokenizers、Transformer到部署全流程 最近有个项目名被问到最多ai-engineering-from-scratch。它不是一个能直接 clone 下来运行的 App而是一条把 AI 知识彻底打碎重建的路线。核心要求很直接从 tensor、token、矩阵乘法开始亲手揉出一个能说话的模型再一步步把它变成能上线服务的系统。相关的两本热门口碑内容中《Build a Large Language Model (From Scratch)》把训练 GPT 拆成了数据处理、注意力机制、预训练、微调、评估五个步骤配套代码可随时跟练是后端工程师转 AI 最适合的入口之一。我入行前两年一直是调 API 工程师OpenAI 的接口、HuggingFace 的 pipeline、LangChain 的 chain拼得飞快但遇到幻觉、重复输出、上下文太长崩掉也只能换个 prompt 再试。真正开始补 from-scratch 这条路之后很多之前靠运气的事才变成了按因果判断。这篇文章就把我走过这条路线的完整思路、实操细节、踩坑记录和个人经验都写清楚希望对想补 AI 工程底层能力的人有实际帮助。1. 这个项目到底在讲什么从“调包”到“造轮子”的思维切换1.1 为什么非要从零开始现在 AI 工程的门槛被各种框架拉得很低这是好事但也是问题。拉低门槛的同时很多人被留在了只会拼积木这一层。今天你可以用三行代码调起一个 GPT 模型但模型输出乱掉、推理慢到无法忍受、微调 loss 不降、显存爆掉能继续往深里排查的人其实不多。原因不是智商而是缺了基础认知不知道模型内部的数据流向自然不知道问题出在哪一环。这就像开车。每天都开路况熟但发动机报警灯一亮就只能拖去修理厂。真正的汽车工程能力是你自己知道哪个部件受力、哪个传感器可能误报、哪条油路最可能堵塞。AI from scratch 的训练本质就是让你把这台车拆开再装回去一次。拆装的过程里你会被迫理解为什么 Transformer 要有 residual connection为什么缩放因子要取 1/sqrt(d_k)为什么 tokenizer 的词表不能随便设大小。这些知识不是面试八股是之后你处理任何模型问题时的底层直觉。需要强调一点不从零重建不等于不学 from-scratch。没有任何生产环境会真的从零训一个 GPT-4 级模型但只有亲手写过小模型你才能回答这些每天都在发生的实战问题量化为什么能节省显存KV cache 为什么能显著加速生成LoRA 为什么能省显存微调数据质量到底怎么影响最终行为这些问题凡是只调 API 的人是答不出因果链的而 AI 工程师的薪水差异恰恰就体现在这种能否看到因果链的能力上。1.2 热词背后两条最值得跟的路线从近期社区热词来看大家最关心两件事build a large language model from scratch和build a reasoning model from scratch。前者是动手做一个 LLM 本体后者是在此之上再做一层会先思考再回答的推理模型。这两件事对应两种不同的工程栈做 LLM主要和 tokenizer、预训练、注意力、数据配比、损失函数打交道。做 reasoning 模型要在已有 LLM 上继续做 SFT、RL强化学习、推理链路设计、真实性与可控性评估。我的建议是不管最终目标是不是推理模型都先把语言模型这条路走通一遍。原因很简单reasoning 模型是构建在语言模型之上的你连 next-token prediction 的 loss 都还没亲手拉平过直接上手强化学习出了问题根本分不清是基座模型的问题还是 RL 环节的问题。如果你想找现成的课程式路线Sebastian Raschka 写的《Build a Large Language Model (From Scratch)》是目前口碑最好的入门书之一。作者把整个 GPT 训练过程拆到最小可执行单元准备数据、实现注意力机制、搭建 transformer 模块、跑预训练、做微调和评估每个步骤都配了可运行的代码。它比论文好读比博客系统。拿它当主线再配合 Karpathy 的 nanoGPT、minbpe 项目做代码参考是我实测最舒服的组合。1.3 从零到能交付系统的四段式学习地图我走完这条路之后把内容抽象成了一个四段式地图任何 AI 系统都躲不开这四段阶段核心内容关键产出最容易低估的部分一、数学底子线性代数、概率、信息论里和模型直接相关的最小集能看懂张量变换、attention 公式、交叉熵梯度更新原理二、模型构建tokenizer、transformer、训练循环一个能跑通前向和反向的小模型数据形状对齐三、训练方法warmup、学习率调度、评估指标、微调一个 loss 正常下降的真实模型超参数对收敛的影响四、系统化能力推理优化、API 服务、RAG、Agent一个别人能用起来的服务部署后的性能问题这张地图不是用来囤的是拿来逐步走完的。第一到第二阶段建议用一个周末集中打通第三到第四阶段放在实际项目里磨。下面几节我按这个地图的顺序逐个展开。2. 手写一个能用的微型 LLM从数据处理到训练循环2.1 第一步先把 tokenizer 造出来很多教程上来就让你写 Transformer 网络结构但我劝你先做 tokenizer。为什么因为模型吃进去的是 token 序列如果 tokenizer 本身是乱的后面所有环节都会一步错步步错。tokenizer 做的事可以这样理解它把一整段文本切成模型能处理的最小语义单元。切得好不好直接决定模型学得顺不顺。最广泛使用的算法是 BPEByte Pair Encoding字节对编码。它的核心思想非常朴素甚至可以类比压缩算法反复找到语料中出现频率最高的符号对把它们合并成一个新符号。合到多少步由你设定的vocab_size决定。下面是简化版 BPE 的核心代码足以演示原理from collections import Counter def get_stats(corpus_freq): pairs Counter() for word, freq in corpus_freq: symbols word.split() for i in range(len(symbols) - 1): pairs[(symbols[i], symbols[i1])] freq return pairs def merge_pair(corpus_freq, best_pair): merged [] for word, freq in corpus_freq: symbols word.split() out [] i 0 while i len(symbols): if i len(symbols) - 1 and symbols[i] best_pair[0] and symbols[i1] best_pair[1]: out.append(best_pair[0] best_pair[1]) i 2 else: out.append(symbols[i]) i 1 merged.append(( .join(out), freq)) return merged # 重复执行 vocab_size 次 # pairs get_stats(corpus_freq) # best_pair pairs.most_common(1)[0][0] # corpus_freq merge_pair(corpus_freq, best_pair)如果你第一次写 tokenizer我的建议是直接用 Karpathy 的minbpe库学实现然后选一个小数据集自己跑一遍看看vocab_size从 512 调到 4096 之后token 序列长度有什么变化。vocab_size太小长文本会被拆得很碎太大词表里大量低频 token 学不到好表示。对于手写微型 GPTvocab_size设在 1000 到 4000 之间比较合适能明显感受到不同设置对训练速度和生成质量的影响。这里有个大多数教程不会说的经验尽量让 tokenizer 的词表里包含足够的常见英文单词同时保证中文字符能覆盖。国内工程师做实验几乎都会遇到中文语料如果直接用纯英文预训练 tokenizer中文会被拆成一堆 UTF-8 字节一个 256 token 的句子可能瞬间涨到 700 多个 token显存和延迟都吃亏。2.2 手写 Transformer 核心组件多头注意力整个 from-scratch 路线里最值得逐行手写的就是 Multi-Head Attention。它不仅是 Transformer 的引擎也是你能真正理解 KV cache、FlashAttention 这些进阶优化点的基础。下面是我在实验里直接可用的精简实现import torch import torch.nn as nn import math class MultiHeadAttention(nn.Module): def __init__(self, d_model, n_heads): super().__init__() assert d_model % n_heads 0 self.d_model d_model self.n_heads n_heads self.d_k d_model // n_heads self.W_q nn.Linear(d_model, d_model) self.W_k nn.Linear(d_model, d_model) self.W_v nn.Linear(d_model, d_model) self.W_o nn.Linear(d_model, d_model) def forward(self, x, maskNone): batch, seq, _ x.size() Q self.W_q(x).view(batch, seq, self.n_heads, self.d_k).transpose(1, 2) K self.W_k(x).view(batch, seq, self.n_heads, self.d_k).transpose(1, 2) V self.W_v(x).view(batch, seq, self.n_heads, self.d_k).transpose(1, 2) attn_scores Q K.transpose(-2, -1) / math.sqrt(self.d_k) if mask is not None: attn_scores attn_scores.masked_fill(mask 0, float(-inf)) attn_probs torch.softmax(attn_scores, dim-1) out attn_probs V out out.transpose(1, 2).contiguous().view(batch, seq, self.d_model) return self.W_o(out)有几个点必须解释清楚。第一为什么要除以sqrt(d_k)因为当维度变大时点积结果的方差会变大进入 softmax 后梯度会极其小甚至消失。除以sqrt(d_k)是标准操作能把点积分值拉回合理范围稳定 softmax 的梯度。第二decoder 里的mask是 causal mask它把未来位置的 attn_score 置为-inf让模型在预测第 t 个 token 时看不到第 t1 个 token 的信息。这是 GPT 生成方向正确的根本保障漏掉它训练指标会好看到离谱但模型生成就等于瞎猜。关于参数选择我实际做微型模型常用的配置是d_model128、n_heads4、n_layers4、context_length256。选择标准有三个一是d_model必须是n_heads的整数倍否则拆分不了二是 attention 分数矩阵的大小随序列长度平方增长context_length太大收敛慢且显存压力大三是这个规模在单张消费级显卡上能在十几分钟内跑完一轮完整实验。等代码验证通过再往上加大规模这个习惯能让你把 bug 隔离在小模型里而不是在大模型里反复猜。2.3 训练循环和超参数让 loss 按预期下降模型结构写完后训练循环其实很短难的是超参数选择。我的微型 GPT 训练配置如下优化器用 AdamW初始学习率 3e-4batch size 32训练 5000 步。学习率计划使用 warmup cosine decay前 500 步从 0 线性上升到 3e-4然后按余弦曲线衰减到接近 0。这个配置几乎是语言模型训练中最稳的起点组合。为什么要 warmup因为在训练早期Adam 的动量估计器需要时间积累统计信息如果一开始就上大学习率容易造成损失剧烈波动甚至发散。cosine decay 的好处则是让模型在训练后期稳定落进更平滑的损失谷底。两者搭配起来我在多次实验里都没有碰到过loss 跑飞的问题。具体损失预期取决于数据复杂度。如果你用字符级 tokenizerloss降到 1.0 到 1.5 之间已经很不错如果用 BPE 词表比如vocab_size1000那么loss在 3.0 左右也算正常。这里最重要的是观察同一份校验集上的 loss 能否同步下降如果训练 loss 一直降、校验 loss 不降就需要考虑数据重复率过高或模型容量过大的问题。一个我强烈推荐的做法训练过程中每隔 200 步就采样一次生成结果而不是只看 loss。因为 loss 是平均指标它可能正常下降但生成内容仍然在重复或逻辑断裂。生成样本才是模型真实行为的窗口。我在实际项目中全靠这个早停信号砍掉了很多无效实验。3. 从模型到系统评估、推理与轻量部署3.1 评估一个生成模型不能只盯着 loss很多刚开始做 from-scratch 的朋友有个误区loss降下来就觉得模型成功。但语言模型的 loss 和实际生成质量之间还有一道鸿沟。比如模型可能 memorization 严重在训练集上 loss 很低但换一个说法就不会了。所以我把评估拆成两层第一层是困惑度perplexity。它由exp(loss)得到直观含义是模型在每一步的候选词中平均要犹豫多少个。PPL 从 20 降到 10说明模型对文本的预测能力提升了一倍。这个指标适合横向对比不同训练步数的模型但它表达不了语义和事实正确性。第二层是行为测试。我会固定一个 prompt 矩阵覆盖四类任务事实问答、复述改写、简单数学、指令遵循。每个类别准备 5 到 10 个稳定样本每次训练完都跑一遍。比如用一句话解释什么是梯度下降和计算 23 乘以 7 等于多少这两个 prompt 就能暴露出模型是真的会了还是只会鹦鹉学舌。这个测试集规模很小但比任何复杂指标都直观。正因为生成模型的不确定性固定样本的长期跟踪才有对比价值。3.2 推理优化三件套KV cache、量化、批处理模型能生成文本之后下一步是让它具备服务能力。这时的瓶颈往往不再是准确率而是速度和显存。我总结了一套推理三件套KV cache、量化、动态批处理。手段解决的问题主要代价我的实践建议KV cache避免每个 token 重复计算历史 KV显存换速度所有生成服务默认开启int8/int4 量化压缩模型权重降低显存和带宽压力少量精度损失用校准集评估别盲目量化动态批处理提升 GPU 利用率和吞吐单请求延迟波动适合高并发场景在线对外服务慎用KV cache 的原理值得展开说。自回归生成时要逐个预测 token每次新预测都需要重算所有历史的 Key 和 Value。如果不缓存生成 100 个 token 就要做 100 次完整前向复杂度无法接受。KV cache 就是提前把历史 token 的 K、V 存下来每次生成只算新的 Query然后把新的 K、V 追加缓存。类比写论文你不需要每次重读所有参考文献而是把关键引文卡片放在手边写到哪张就直接抽出来用。一旦接触过 KV cache你就能理解为什么服务端有时越生成越快又越生成越慢——开头要初始化缓存后面每一步只做增量计算但缓存涨到一定程度后显存带宽会成为新瓶颈。量化则是更基础的一件事把 FP16 的权重映射到 INT8 或 INT4 的整数范围过程中用 min-max 缩放和校准集减小误差。权重变小之后显存占用直接下降且推理时从显存读权重的带宽压力也减小。注意对小型实验模型量化收益不明显但对 7B 以上模型int8 往往能把显存需求砍一半int4 再砍一半。要不要量化的判断标准只有一个量化后模型在固定评估集上的效果指标下降是否可接受。3.3 把模型包成一个快速推理服务训练模型最终要给人用。最小可用服务常用 FastAPI 搭建。一个典型流程是启动时把模型加载到显存一次请求进来后做 tokenize、推理、decode返回文本。下面是可参考的最小实现from fastapi import FastAPI from pydantic import BaseModel import torch from model import load_gpt_model, generate # 你自己的模块 app FastAPI() model, tokenizer load_gpt_model(checkpoint.pt) model.eval() class GenRequest(BaseModel): prompt: str max_new_tokens: int 128 temperature: float 0.8 top_p: float 0.9 app.post(/generate) def generate_endpoint(req: GenRequest): with torch.no_grad(): text generate( model, tokenizer, promptreq.prompt, max_new_tokensreq.max_new_tokens, temperaturereq.temperature, top_preq.top_p ) return {text: text}生产环境里要注意三点。一是模型实例不能在每个请求里重复加载必须在进程启动时加载一次二是 PyTorch 的默认线程数在 CPU 上会疯跑部署时显式设置torch.set_num_threads(4)之类三是服务应设置超时和最大请求长度防止一个几十秒的超长生成请求把整个服务占满。实测中把这三件事做好一个微型 GPT 模型就能稳定扛住日常流量。4. 从单个模型走向 AI 系统RAG 与 Reasoning 的工程化4.1 RAG给模型外挂一份可以更新的知识库手写做完模型接着会遇到一个现实问题你的模型训练数据是固定的它不会自动知道公司内部文档、最新产品信息、用户私域数据。这时一般有三个方案微调、长上下文、RAG。方案原理成本适用场景微调把知识写进模型权重高且更新需重新训练风格、行为、领域能力长上下文把资料全塞进 prompttoken 成本随长度暴涨少量短文档、临时问答RAG检索相关资料再拼进 prompt中知识可随时更新大知识库、高频更新场景RAG 的实现链路很清晰切分文档、向量化存入向量库、查询时召回 top-k 片段、把片段拼进 prompt 交给模型。它像开卷考试答案写在参考书里考试时先翻书再作答。相比之下微调更像闭卷背课本成本高而且学进去的知识在权重里不可解释、不好更新。实操里最重要也最容易被忽视的参数是切分chunk_size和overlap。我踩过一个大坑把一份很长的产品文档直接按 500 字符切块导致关键信息恰好被切成两半检索引擎召回不到完整内容生成结果自然漏掉核心信息。调整成chunk_size256、overlap50之后召回质量立刻改善。top-k 一般取 3 到 5 就够太多反而会把不相关内容注入上下文干扰模型生成。4.2 Reasoning 模型从会说话到会思考如果你已经沿着build a large language model from scratch做完了基座模型那社区现在最热的下一站就是build a reasoning model from scratch。这个方向最近讨论热度极高原因是普通 LLM 只会做 next-token prediction遇到复杂推理任务容易答非所问。Reasoning 模型的目标是让模型在给出最终答案之前先输出一段思考过程再基于思考过程得出答案。从零构建 reasoning 模型我的实践路线分三步。第一步是收集带思考链的指令数据对模型做 SFT监督微调让模型学会先写出分步推理再给结论的输出格式。第二步构建一个规则验证器比如数学题的答案是否正确、代码是否能通过测试、表单字段是否完整作为强化学习的奖励信号。第三步用 GRPO/PPO 之类的策略优化算法让模型在推理正确性上继续提升。这里有一个特别重要的建议先把验证器写好再设计模型训练。很多人一上来就调 RL 算法结果模型输出一大段正确格式的思考过程但最后结论全错因为没有有效的奖励信号。我的做法是先在一个 1B ~ 3B 的小模型上、单一任务比如小学数学上跑通SFT 冷启动 RL 热驱动的完整闭环确认链路正常后再迁移到大模型或复杂任务上。小模型上跑通意味着你的数据管线、验证器、RL 参数都是可信任的扩大到大规模时才不会把问题源头搞混。另外生产环境里要注意给 reasoning 模型设计思考开关。不是所有请求都需要长思考简单问答如果也输出几千 token 的思考链成本和延迟都翻几倍。我通常用特殊标记控制模型是否进入推理模式这相当于给同一个模型装两个工作档位。5. 必备资源与工具箱清单5.1 一本手册加三个必读仓库这个方向最容易走的弯路是资料太多东看一篇西看一篇最后什么都没跑通。所以我建议主线只用一套系统资源配套代码再辅助。我选的主线就是上面提过的《Build a Large Language Model (From Scratch)》。它的价值在于代码和书同步推进每章都有独立的可运行项目从准备数据、实现注意力、预训练到微调和评估全覆盖。完整的代码配套比起单纯读书效率高得多。建议买正版中英文都可以作者对内容更新很频繁旧版本的核心代码依然适用。配套资源有三个必看仓库nanoGPTKarpathy 手写的 GPT 最小训练实现代码量小、结构清晰适合在读书后对照工业级写法和教学级写法的区别。minbpeKarpathy 写的极简 BPE tokenizer是理解字节对编码的最佳代码材料。llm.cKarpathy 用 C 语言实现 GPT 训练的项目如果你想往更底层走比如理解 CPU/GPU 上的矩阵运算、内存布局这是极好的参考。评估工具方面推荐lm-evaluation-harness它是目前社区最常用的统一评估框架支持几十种标准 benchmark你训练出的模型只需要一个命令就能跑出一组可比对的指标。推理部署方面后续如果要服务更大模型可以去看vLLM或 Text Generation Inference 的 PagedAttention、continuous batching 实现它们是提升推理吞吐的工业级方案。5.2 算力规划小步快跑别一上来就买 4090很多读者看到 GPT 三个字就觉得非得上万块钱的显卡其实 for scratch 路线在消费级硬件上完全能跑。关键在于根据自己的显存选择模型的规模线显存能做的最优选择实际场景8GB训练几十 M 参数模型或量化微调 7B本机跑通整个 from-scratch 流程16GB训练 60M ~ 100M 参数模型或 LoRA 微调 7B完整实验 轻量微调24GB训练 300M 参数级模型或 QLoRA 微调 13B进阶训练和服务部署40GB训练 1B 参数级模型或下游微调大模型严肃的模型研发工作我的实际体验是在 8GB 的笔记本上训练一个d_model128、n_layers4的微型 GPT一轮完整训练只需要几十分钟足以让你把数据准备、模型构建、训练、评估、部署整套链路全部走完。所以不要等显卡先用手里已有的设备跑通流程把核心概念变成肌肉记忆之后再加算力就顺理成章。5.3 时间投入怎么安排合理从零开始学确实要时间但不需要一年。我自己给零基础同事的建议是按三周起步、三个月内闭环来安排第一周集中理解 tokenizer 和 Transformer 结构第二周跑通一个微型 GPT 的完整训练第三周做一些推理优化和部署练习之后三个月里主攻自己业务相关的方向比如 RAG、reasoning、多模态之一。三个月后你会发现自己已经能读懂大部分开源模型代码而不是只会在 demo 页面点点点。6. 常见问题与避坑实录6.1 训练阶段最常踩的五个坑我把实验和项目里反复出现的坑整理成了一个速查表每个都是真金白银踩出来的。现象可能原因处理方式loss 不降震荡学习率过大、数据重复率过高、tokenizer 没训练好降低学习率到 1e-4检查数据去重重建 tokenizerloss 突然变 NaN混合精度下梯度溢出、学习率过高关掉 AMP 或调大 loss_scale减小学习率训练 loss 正常验证 loss 升高数据泄漏、模型过拟合、训练/验证集分布不同检查数据标注和划分降低模型规模生成内容不断重复温度太低、重复惩罚没加、模型容量不足temperature 调到 0.8 以上加 repetition_penalty显存 OOM序列太长、batch_size 过大、KV cache 没优化降低 seq_len 或 batch_size开 gradient checkpointing我自己最痛的一次是在数据准备环节从网上抓了文章直接用忘了去重结果训练 loss 降到 2.5但验证集上表现惨不忍睹。后来做了一次文本 minhash 去重同样的训练步数验证 loss 直接下降 0.3 以上。数据质量的重要性怎么强调都不为过。6.2 代码排查顺序速查很多读者代码跑不通容易陷入到处试的状态。我在排错时固定按下面这个顺序来效率比乱试高得多检查张量 shape直接打印前向传播里每一层的张量形状绝大部分 bug 都在这里。检查 tokenizer 的可逆性把 token 序列 decode 回文本看是否还原这一步能排除分词问题。做单步过拟合测试拿训练集里的几十条样本看模型能不能快速把 loss 降到很低。如果单步都无法过拟合说明模型正向传播或反向传播有 bug。检查梯度训练几步后打印梯度范数如果出现 NaN 或无穷大优先查数值稳定性。调整生成参数训练流程没问题之后再回头调 temperature、top_p、repetition_penalty 这些解码参数。6.3 到底什么时候值得从零造轮子从零走完这套路线不代表生产环境就要重写一切。造轮子之前要想清楚收益大型通用模型永远调用现成的更合适无论是成本还是效果但如果你的场景是垂直领域小模型、需要对模型内部行为做精细控制、需要为上层应用写可靠评估协议那这套从零练出的手艺就是核心竞争力。我自己现在做项目的判断标准很简单如果问题能在明确封装好的接口层解决就用现成方案一旦发现需要改模型结构、调试数据链路、或者评估结果解释不了就重新开启 from-scratch 思维把问题往下拆一层。写到最后分享一点个人体会。走完这条路线后我最大的变化不是能训练模型了而是面对一个陌生模型时不再慌。新模型拿到手我会下意识地想它的 tokenizer 是怎么训的训练数据配比如何损失函数是什么评估协议能不能复现这种把黑盒拆成因果链的能力正是 AI engineering 最值钱的部分。如果你也想获得这种掌控感不用着急从一个小模型开始把每一步走通慢就是快。
返回列表