大模型训练推理与本地部署全解:从损失函数到KV Cache
这一篇拖了挺久评论区一直有人问“上篇和中篇讲完注意力了那梯度、损失函数、推理时为什么是一个词一个词蹦出来的还有本地部署时候那个模型文件到底是个什么玩意儿能不能一起说清楚”这次中下篇我把训练和推理这条线从头到尾收个尾顺带把手把手部署时会遇到的坑也讲明白。目标还是老样子——高中生不用懂微积分也能跟着思路走一遍。1. 从“猜词游戏”到“模型训练”大模型到底怎么学会说话的1.1 预训练的本质一个超级加强版的完形填空大语言模型最核心的预训练任务其实特别朴素给定前面一串词预测下一个词是什么。拿中文举个例子输入“今天天气真”模型要计算出“好”“差”“热”“冷”这些候选字各自的概率。这一步在学术上叫“语言建模”或“自回归预测”但本质上就是一个超级加强版的完形填空。那模型是怎么“计算概率”的呢在最后一层模型会为词表里的每一个词打一个分数专业上叫logits这个分数可以理解为“模型认为这个词出现在这里有多合理”。分数本身可能是负数也可能很大不方便直接当成概率所以接下来会做一次softmax 归一化把所有分数换成均大于0、总和等于1的概率分布。举个例子如果词表只有“好”“坏”“热”三个字模型打出的 logits 分别是 2.0、0.5、0.1softmax 之后可能变成 0.64、0.23、0.13 这样的概率。训练时如果正确答案是“好”模型就会努力让“好”对应的概率更接近1其余两个更接近0。这个“努力让预测更接近真相”的过程就是整个大模型训练的起点。高中生如果第一次接触可以不用管 softmax 的公式长什么样只要记住它干了两件事第一把任意实数分数变成合法概率第二让最大的分数对应的概率更大。这两个特性足够理解后面所有采样逻辑了。1.2 损失函数与梯度下降模型如何“知错能改”模型刚初始化的时候参数全是随机的预测结果基本等于瞎猜。如果输入“今天天气真”它可能给“好”的概率只有0.1给“猛”的概率反而很高。这时候我们就需要一把“刻度尺”明确告诉模型“你这次错了多少。”这把尺子就是损失函数。预训练最常用的损失函数是交叉熵损失。它做的事情很直接如果正确答案对应的概率越接近1损失就越小概率越接近0损失就越大。真实答案“好”的概率只有0.1损失就会比较大经过很多轮训练后“好”的概率升到0.95损失就很小了。损失算出来之后模型怎么调整自己的参数这里就要引出梯度的概念了。你可以想象自己站在一座山坡上闭着眼睛要往山下走。你没法一眼看到整座山的全貌只能通过脚下的坡度判断哪个方向是往下。梯度就是这座“损失山”在当前参数位置最陡峭的上升方向我们反着走就是下降方向这一步一步走下去损失就会越来越小。每一步往下走的步子大小由学习率控制。学习率太大步子迈过头可能在谷底附近来回横跳学习率太小半天走不到山底训练时间会非常长。实际操作中学习率还会随着训练轮次逐渐降低前期大步探索后期小步精调。计算梯度用的算法叫反向传播它本质上是微积分里的链式法则。不过高中生可以先把它理解成“从最后一层开始把损失对每个参数的偏导一层一层往前传”。这个过程不需要手算PyTorch 这种深度学习框架会自动求导。所以训练模型时我们真正写的代码核心就四步前向传播算出预测结果、算损失、调用loss.backward()算梯度、再用优化器更新参数。1.3 参数量越大越聪明Scaling Law 的直觉解释现在市面上的大模型动不动就70B、130B、400B这个“B”是 billion十亿参数。那参数到底是什么可以把它理解成神经网络里千千万万个“旋钮”。每个旋钮都连接着两个神经元旋钮的值不一样信息经过时的放大或缩小程度就不一样。整个模型学到的语言知识就藏在这些旋钮的具体数值里。为什么参数越多模型往往越聪明一个很直观的类比是工具箱。一个只有螺丝刀的家用工具箱能处理的电器故障很有限一个包含各种型号螺丝刀、剥线钳、万用表、电烙铁的完整工具箱能修的设备就多得多了。大模型的参数就是它拥有的“工具组合”参数越多它能容纳的“模式模板”越多也就越能从训练数据中学到各种复杂规律。但这有个前提参数不是越大越好还要有足够的高质量数据进行匹配。学界有一个著名的Scaling Law模型性能大致会随着参数量、训练数据量、训练计算量的同步增加而稳定提升。也就是说把模型做大但数据不够多或者数据很多但模型太小效果都会受到短板制约。我还想提一个容易误会的点参数多并不等于每个参数都清清楚楚是人能看懂的含义。绝大多数参数之间高度耦合你很难指着某个参数说“这个参数代表‘猫’的概念”。这跟传统程序区别极大也是很多人初学大模型时最不适应的部分。2. Transformer内部关键实现多头注意力与位置编码拆开揉碎2.1 Q、K、V 到底是啥从“图书馆找书”说开去理解注意力机制最经典的办法是类比图书馆找书。假设你站在图书馆前台想查“机器学习入门”的书大脑里会形成一个查询需求这个需求就是QueryQ。书架上的每本书侧面都贴着一个标签标签就是KeyK比如“Python”“深度学习”“历史”。你需要做的事就是把你的需求 Q 和每本书的标签 K 依次比对算出一个相关度分数。相关度分数通常会除以一个缩放因子再经过 softmax变成一组权重有的书高度匹配权重接近1有的书完全无关权重接近0。最后你把权重套用到每本书的正文内容上做加权求和得到的就是从这本书馆里“检索”出来的信息也就是ValueV的加权结果。一句话总结Q 和 K 决定“该看谁”V 决定“具体看什么内容”。在实际 Transformer 代码里Q、K、V 都是通过对输入向量乘上三个不同的权重矩阵生成的。同一个词会同时扮演查询者、被检索者和内容来源三个角色。比如“小明去商店买东西他付了钱”模型在处理“他”的时候会用“他”的 Q 去匹配“小明”和“付”的 K从而知道“他”指代小明也关联到“付钱”这个动作。需要再强调一下缩放因子sqrt(d_k)的作用。如果两个向量的点积数值很大softmax 输入值之间的差距会被拉得很大容易让输出的概率非常极端梯度就会变小不利于训练。除以一个合适的缩放因子能让注意力分数保持在一个比较稳定的区间训练过程会更顺滑。2.2 多头注意力多个图书馆员同时找书前面说的找书流程其实只描述了“一个注意力头”在做事。如果整个模型只有一种 Q、K、V 变换那它只能捕捉一种关系维度。比如这次专门抓“指代关系”下次遇到“语法主谓关系”可能就不敏感了。多头注意力的思路特别简单粗暴把整个找书过程复制好几份每份用不同的“查询偏好”。这就是Multi-Head Attention多组 Q、K、V 并行计算每组头负责一个子空间。比如一号头可能专门抓句子里的主语和谓语关系二号头专门抓指代关系三号头可能抓语义相似度。算完之后把所有头的输出拼接在一起再经过一层线性变换得到最终结果。用图书馆类比多头就是同时派好几个图书馆员去找书每人关注点不同一个专注技术类、一个专注小说类、一个专注历史类最后把所有人找到的内容汇总给你。这样模型在同一个时间点就能同时接收多个角度的信息表达能力自然比只看一遍强得多。代码实现上多头注意力往往不是真的复制多份矩阵而是把特征维度切成几段每个头分一段并行算完再拼接。这样计算效率更高实际显存占用也更可控。这是工程实现和学术描述差异比较大的地方很多初学者看论文以为自己要写一堆循环其实框架里一个矩阵 reshape 就搞定了。2.3 位置编码让模型知道“先后顺序”注意力机制本身对词序完全无感。把“猫追老鼠”和“老鼠追猫”两个句子的词向量分别送入注意力层如果没有任何额外处理模型看到的其实是同一组词袋无法区分谁是动作的发出者。这是 Transformer 和循环神经网络最大的不同循环网络天然按照时间顺序处理输入而 Transformer 是并行输入的必须额外注入位置信息。最经典的方案是Sinusoidal 位置编码论文《Attention Is All You Need》里用的是不同频率的正弦和余弦函数给每个位置生成一个固定向量。位置0用一组 sin/cos 值位置1用一组稍微不同的值不同维度有不同的波长。这样做的优点是向量能隐含相对位置关系而且不需要训练学习。现在很多大模型已经改用旋转位置编码RoPE了它的思路更巧妙把相邻 token 的位置差“旋转”到向量上让两个 token 的注意力分数天然依赖它们的相对距离。我建议初学阶段不用纠结具体公式只要记住一个核心结论RoPE 等价于在 Q 和 K 做点积之前先按位置旋转一下向量旋转角度和 token 距离成正比。距离越远旋转角度越大模型对相对位置的感知就越明显。最近各大模型不断把上下文窗口从2K扩展到32K、128K甚至1M位置编码的扩展能力是关键瓶颈之一。因为如果位置编码只在训练时见过有限长度一旦推理时遇到更长的输入模型可能完全“懵掉”。后面业界提出各种插值方法把位置编码“拉伸”本质上都是在解决这个“见都没见过怎么处理”的问题。3. 从训练到推理生成一句话背后的完整流程3.1 训练与推理的根本区别一次性教完 vs 一个字一个字蹦训练大模型的时候我们手里有完整的一句话。比如训练数据是“今天天气真好”模型输入“今天天气真”预测“好”同时输入“今天天气”预测“真”输入“今天”预测“天气”。注意这几种预测在训练时是在同一次前向传播里并行完成的模型会同时看到完整句子的所有 token只不过每个 token 只能看到它之前的 token不能看到它后面的内容。这种技巧叫teacher forcing中文可以理解成“老师手把手教”。模型预测第5个词时前4个词都用真实文本喂进去而不是用它自己刚才预测出来的词。好处是训练速度快、收敛稳定每个位置的预测都能被准确地监督到。到了推理阶段模型没有“标准答案”可参考它只能先输入开头提示词预测出第一个新词然后把这个新词拼到输入末尾再预测第二个词。这个过程就叫自回归生成像挤牙膏一样一个 token 一个 token 往外生成。每生成一个词都要跑一次完整的前向传播。训练和推理的差异带来很多连锁反应。比如训练时模型习惯的是真实上下文推理时一旦某个早期词生成得不太对后面的错误就会滚雪球越滚越大。这也是很多模型部署时需要加“重复惩罚”“温度控制”的原因之一。3.2 温度系数与采样策略为什么同样的输入输出不一样推理时模型最后一层 softmax 给出的其实是每个词的概率分布而不是一个确定的词。最简单的选词方式是直接选概率最大的那个这叫贪心解码结果稳定但容易重复枯燥尤其在长文本生成时可能会出现“今天天气好今天天气好今天天气好”这种死循环。为了让输出更丰富我们可以不从最大概率硬选而是从概率分布里“抽签”。但完全按原始分布抽又可能抽到明显不合理的词。这时候温度系数temperature登场了。温度 T 会作用在 softmax 的输入分数上具体做法是将 logits 除以 T。T 小于1时分布变得更陡峭高概率词的胜算更大输出更确定T 大于1时分布变得更平坦低概率词也有机会被选中输出更多样。实际操作中写代码调用大模型 API 时temperature0.7是比较常见的默认值写代码、数学推导时我会调到0.2左右减少胡编乱造创意写作时则调到0.9甚至更高。除了温度工程上还常用 top-k 和 top-p 两种截断采样。top-k 是只保留概率最高的前 k 个词重新归一化k 太小会限制多样性top-p 是从概率最高的词开始累加直到累计概率超过 p 就截断比如 p0.9 表示只在小范围内抽。实际项目里 temperature 和 top-p 经常搭配使用先用 top-p 砍掉长尾概率再用温度控制分布形状。3.3 上下文窗口与KV Cache显存都去哪了你可能好奇为什么同样的模型别人能生成几千字你的 API 一调就报“context length exceeded”这里要引入上下文窗口的概念模型最多能同时处理的 token 数量。输入越长内部所有 token 两两之间的注意力矩阵就越大计算量和显存占用都会迅速增加。假设输入序列长度为 N注意力分数的矩阵是 N×N显存和计算量都和 N 的平方正相关。N 从 2K 涨到 4K注意力部分开销直接变4倍这也是长上下文造价高的核心原因。推理的时候还有一个更隐蔽的显存杀手叫KV Cache。因为自回归生成是逐词进行的每生成一个新词都需要重新计算之前所有 token 的 K 和 V如果没有缓存每步都要从头算一遍整句会慢到无法接受。工程上的做法是第一次计算时把每个 token 的 K 和 V 向量存下来后续每推一个新词只需要计算新词的 K、V再用历史缓存做注意力计算。所以 KV Cache 本质上是用显存换速度。它的大小大约和层数、头数、维度、序列长度都成正比。部署时你会发现模型权重占的显存是一回事输入输出累积下来的 KV Cache 又是另一部分显存二者不能混淆。这也是为什么很多量化部署方案会优先压缩权重但 KV Cache 仍然是长上下文生成的最大瓶颈。4. 本地部署大语言模型的关键细节模型文件、量化与配置4.1 模型下载下来到底是什么不是一个可执行程序很多第一次接触本地部署的同学下载完模型文件后会愣住这既不是.exe也不是.dmg更像一个巨大的数据文件。这其实是对的大模型本身不是一个程序它只是一堆权重参数和配置信息。以最常见的一个开源模型文件为例里面至少包含三类内容第一是权重张量也就是那些“旋钮”的具体数值占了绝大部分体积第二是词表文件记录着 token 和 id 的映射关系第三是配置文件写着模型有多少层、多少头、维度多大、上下文窗口多长这些超参数。推理框架读取这些配置后再按照对应的模型结构把权重填进去才能开始计算。不同格式的模型文件差别很大。.safetensors是 PyTorch 生态常见格式存储精度高适合继续训练和微调.gguf是 llama.cpp 社区推动的格式方便量化后 CPU/GPU 混合推理.onnx则是跨平台交换格式。部署时格式选错是最常见的坑比如你想用 llama.cpp 加载一个.safetensors文件得先做格式转换否则框架直接报错。文件体积估算有个简单公式参数量 × 每个参数占用字节数。7B 参数模型用 FP162字节存储约 14GB转为 4bit 量化后大约 3.5GB。看到模型仓库页面标注“7B”和“4bit”你基本能立刻算出自己的显卡或内存够不够用。4.2 量化原理用“四舍五入”换显存量化这个词听起来高深实际思想人人都用过把一张高精度照片用 JPEG 格式压缩肉眼几乎看不出差别体积却能减少很多。大模型量化就是对权重数值做“精度打折”从原来的 FP16 或 FP32 浮点数变成 8bit 整数甚至 4bit 整数。具体怎么做呢假设某层权重范围在 [-1.5, 1.5]要量化到 4bit也就是总共16个档位。先把连续数值按比例映射到 0 到 15 的整数区间存储时只存整数计算时再用一个缩放因子还原成近似浮点数。这种做法可以大大减小存储体积和内存带宽压力但会引入一点误差。量化误差对模型的影响不是均匀的。有的层对数值极其敏感量化完效果突然崩坏有的层则非常皮实压缩到4bit也不掉多少分。所以现在很多量化方法会按通道计算缩放因子甚至混合使用不同精度重要的层保留FP16不重要的层用4bit。这种混合量化在本地部署里非常实用视觉模型尤其明显因为视觉 encoder 和语言 decoder 的敏感度差异很大。实操建议是如果显存只够勉强放下8bit模型优先试试4bit或6bit量化大多数场景效果差距可以接受但如果跑数学推理或代码生成量化等级可能让你的成绩明显下降这时候应该优先保精度甚至考虑更小的模型而不是更低比特。4.3 快速搭一个本地API服务端点、端口和地址怎么填本地部署的最终目的往往是希望其他程序能调用它。最简单的方式是把模型包成一个 API 服务。以常见的工具 Ollama 为例你拉取模型并启动后它默认会在本机的 11434 端口上提供服务。这时在代码里填写 API 地址应该写http://127.0.0.1:11434/v1其中127.0.0.1表示本机回环地址11434是端口/v1是兼容 OpenAI 风格的路由。很多人第一次配置时卡在“地址怎么填”这个问题上。其实分三种场景看如果调用程序和模型在同一个电脑上用127.0.0.1或localhost都行如果开发机在局域网里的另一台电脑需要填模型所在电脑的局域网 IP例如http://192.168.1.23:11434/v1如果是云服务器就填公网 IP 或域名。端口号必须严格匹配服务默认开在哪个端口客户端就填哪个端口。检查端口有没有在监听命令行里可以用curl http://127.0.0.1:11434/v1/models测一下如果有 JSON 返回就说明服务正常。还有个容易踩的小坑很多框架默认只监听127.0.0.1局域网其他机器无法访问需要在启动时加上--host 0.0.0.0之类的参数让它监听所有网卡。这个操作会暴露到局域网如果网络环境不信任建议设置访问密钥不要裸奔。5. 常见问题与排查技巧实录5.1 显存不够、OOM 怎么办本地部署最常见的报错就是 CUDA out of memory。这一步我的排查顺序是先看模型文件本身多大再看推理时实际占用的显存是多少。如果权重都已经加载训练时 OOM 通常是批大小或上下文长度太高推理时 OOM 则优先怀疑 KV Cache。解决方案依次试降低上下文长度、把 batch size 降到1、换更低比特量化、把部分层放到 CPU 上推理。如果这些都不行还可以试试 CPU GPU 混合推理比如 llama.cpp 支持把绝大多数层放显卡少数层放内存。这样速度会慢一点但至少能跑起来。中低端显卡用户只有几GB显存跑7B模型基本离不开这个方案。记得给系统预留一点内存别把物理内存耗干。5.2 输出乱码、重复死循环怎么调模型生成大量重复内容通常不是模型坏了而是采样策略太极端。先看温度是不是太低比如温度小于0.1概率分布被压得太陡容易反复生成同样的高概率词。再看有没有设置重复惩罚参数如果打开后反而更严重说明惩罚力度过大把正常词也罚没了适当调低。乱码问题多数出在词表匹配和编码上尤其是中文环境。检查请求里有没有显式指定 UTF-8 编码有些框架默认用 UTF-8 没问题但 Windows 下的各种奇怪的代码页会带来诡异字符。此外如果模型是纯英文基座模型但没有做中文扩展也有可能生成乱码这种需要换一个中文适配模型。5.3 API 地址连不上、请求超时怎么查当代码报连接错误不要急着改程序。我一般按这个顺序三步排查第一步确认服务进程是否还活着终端里跑一下 curl第二步确认客户端填的地址和实际服务监听地址是否一致尤其注意是127.0.0.1还是局域网 IP第三步看防火墙有没有拦截端口Linux 环境特别常见。请求超时也有讲究。如果是首次加载大模型到显存初始化可能花费几十秒程序里的超时时间设置太短就会报超时。一个很土但有效的办法是先在本地脚本里调用一次确认模型已经加载好再让业务代码去请求。另外如果服务端有并发限制多个请求同时进来也会排队表现为延迟飙升但这时不代表服务挂了只是处理不过来。我在指导初学者做本地部署时最开心的时刻往往是学生第一次把自己的 API 地址填进另一个应用然后成功生成一句话。虽然那个结果可能有些笨拙但那一刻“大语言模型实现原理”不再只是纸面上的流程图而是实实在在跑在本地机器上的东西。如果你自己折腾到这一步我建议可以尝试改一改温度系数再试试调整 top-p感受一下同一个模型在不同采样参数下从严谨到放飞自我的变化。这种手感比背十遍注意力公式都有用。