大模型训练推理与本地部署全解:从损失函数到KV Cache
2026/9/8 18:39:06 网站建设 项目流程

这一篇拖了挺久,评论区一直有人问:“上篇和中篇讲完注意力了,那梯度、损失函数、推理时为什么是一个词一个词蹦出来的,还有本地部署时候那个模型文件到底是个什么玩意儿,能不能一起说清楚?”这次中下篇,我把训练和推理这条线从头到尾收个尾,顺带把手把手部署时会遇到的坑也讲明白。目标还是老样子——高中生不用懂微积分,也能跟着思路走一遍。

1. 从“猜词游戏”到“模型训练”:大模型到底怎么学会说话的

1.1 预训练的本质:一个超级加强版的完形填空

大语言模型最核心的预训练任务,其实特别朴素:给定前面一串词,预测下一个词是什么。拿中文举个例子,输入“今天天气真”,模型要计算出“好”“差”“热”“冷”这些候选字各自的概率。这一步在学术上叫“语言建模”或“自回归预测”,但本质上就是一个超级加强版的完形填空。

那模型是怎么“计算概率”的呢?在最后一层,模型会为词表里的每一个词打一个分数,专业上叫logits,这个分数可以理解为“模型认为这个词出现在这里有多合理”。分数本身可能是负数也可能很大,不方便直接当成概率,所以接下来会做一次softmax 归一化:把所有分数换成均大于0、总和等于1的概率分布。

举个例子,如果词表只有“好”“坏”“热”三个字,模型打出的 logits 分别是 2.0、0.5、0.1,softmax 之后可能变成 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 到底是啥:从“图书馆找书”说开去

理解注意力机制,最经典的办法是类比图书馆找书。假设你站在图书馆前台想查“机器学习入门”的书,大脑里会形成一个查询需求,这个需求就是Query(Q)。书架上的每本书侧面都贴着一个标签,标签就是Key(K),比如“Python”“深度学习”“历史”。你需要做的事,就是把你的需求 Q 和每本书的标签 K 依次比对,算出一个相关度分数。

相关度分数通常会除以一个缩放因子再经过 softmax,变成一组权重:有的书高度匹配,权重接近1;有的书完全无关,权重接近0。最后,你把权重套用到每本书的正文内容上,做加权求和,得到的就是从这本书馆里“检索”出来的信息,也就是Value(V)的加权结果。一句话总结: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 时,temperature=0.7是比较常见的默认值,写代码、数学推导时我会调到0.2左右,减少胡编乱造;创意写作时则调到0.9甚至更高。

除了温度,工程上还常用 top-k 和 top-p 两种截断采样。top-k 是只保留概率最高的前 k 个词重新归一化,k 太小会限制多样性;top-p 是从概率最高的词开始累加,直到累计概率超过 p 就截断,比如 p=0.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 参数模型用 FP16(2字节)存储,约 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.1localhost都行;如果开发机在局域网里的另一台电脑,需要填模型所在电脑的局域网 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,感受一下同一个模型在不同采样参数下,从严谨到放飞自我的变化。这种手感,比背十遍注意力公式都有用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询