从SRE视角拆解LLM workload的运行特性(长进程、多依赖、网络通信等),推导可靠部署和扩缩容决策。强调理解语义才能制定通用运维策略。
📖 交叉发布。原文包含每个阶段(注意力机制、前馈步骤、采样等)的交互式逐步图表,无法在此呈现。如欲获得完整体验,请访问我的博客阅读 ↗。
我在 Careem 担任 SRE,最近开始探索如何有效地部署和服务 LLM 工作负载。这是一系列关于我迄今为止所学内容的指南,这是该指南的第 1 部分。
作为 SRE,我们能够部署并确保工作负载的可靠性,这是因为我们对其语义有深入理解,这使我们能够推广该工作负载的需求和条件。以每个 SRE 无疑都会部署的工作负载为例:微服务。我们能够可靠地运行它们,是因为我们从本质上理解微服务是一个长期运行的进程,可能连接到一个或多个数据源或另一个微服务,通过网络套接字暴露,并通过特定协议(例如 HTTP)通信。这告诉我们一个健康的微服务是什么样的,即错误率和延迟应该保持较低,其中任何一个的上升都会促使我们进行扩展或重启,这就是让我们能够可靠地运行它们的原因。理解工作负载是可靠地运行它的基础要求,因此本部分致力于将 LLM 作为工作负载来理解。
从表面上看,LLM 看起来像微服务,因为调用一个 LLM 就像你在与一个 HTTP 端点对话,但它们与常规微服务绝对没有任何相似之处。在底层,LLM 是一个超并行化的矩阵乘法野兽,需要机器学习的相关知识,特别是深度学习的知识,我不打算深入讨论。坦率地说,理解 Transformer 会打开神经网络、梯度下降、矩阵、线性代数以及概率和统计学的潘多拉魔盒:一个非常枯燥且陡峭的学习曲线,对 SRE 来说很难证明其合理性。机器学习工程师在他们的学习中花费了大量时间来掌握所有这些知识,如果你有时间和精力,我强烈建议至少建立一个高水平的理解。一旦这些概念被理解了,它们就相当简单,而且也很有趣。如果你决定走这条路,我强烈推荐 Grant Sanderson 关于注意力机制的视频,它帮助我建立了对发生情况的心理模型:
Transformers,LLM 背后的技术(深度学习第 5 章)- 3Blue1Brown
Transformer 中的注意力机制,逐步讲解(深度学习第 6 章)- 3Blue1Brown
LLM 如何存储事实(深度学习第 7 章)- 3Blue1Brown
Transformer 和注意力机制的可视化(TNG Big Tech Day '24)- Grant Sanderson
但今天我只会浅尝这些话题,并尝试用最简单的术语来解释它们。
那么到底什么是 LLM?它只是数十亿个数字的集合。这就是为什么,当你被告知一个模型是"9B 参数"时,90 亿实际上就是该模型中数字的个数。说实话,它们不是一堆随意放置的数字;它们是数组、切片或矩阵,这意味着每个数字都是有序组的一部分。下一个自然的问题是:我们对这些数字做什么?我们在一个算法内部使用它们,在这个例子中,该算法就是著名的 Transformer。
Transformer 算法做什么?简单来说,给定一个部分句子,它的单次运行预测下一个最有可能的单词(或部分单词)是什么。当我们调用这些模型时,我们实际上是在循环中运行 Transformer:给定一个句子,生成下一个单词,将该单词附加到原始句子,然后重新通过 Transformer 运行整个句子以生成下一个单词。我们一直循环运行,直到下一个单词是我们指定为句子末尾的单词。

我一直在谈论预测单词,但它不是真正的单词,而是 token,它们与单词不同。一个 token 可以是一个单词,但它也可以是单词的一部分,token 还包括空格和标点符号。换句话说,token 是一个或多个 Unicode 字符的序列。Transformer 处理 token:输入句子首先被分解为 token,传递给 Transformer,然后 Transformer 输出下一个最有可能的 token。Token 的概念对大型语言模型如此重要,以至于它们的性能(token/秒)和成本(每百万 token 多少美元)都是用 token 来衡量的。
此时,值得下载一个开放权重的模型,打开它,并检查它。所以确保你的系统上安装了 Python 和 pipx,启动一个终端,并运行以下命令:
pipx install hf
# 注意:你必须访问 huggingface.co/google/gemma-3-4b-it 上的模型页面,登录,并接受协议才能下载它
hf download google/gemma-3-4b-it --local-dir ./gemma
下载后,在文本编辑器中打开 gemma/tokenizer.json 文件。这是一个 JSON 文件;导航到 model.vocab 属性。这是 token 的完整词汇表,这意味着这些是可以从任何句子生成的可能 token。分词器是一种将字符序列转换为 token 的算法。例如,句子:
I can reliably deploy LLMs
将被分词为:
[236777, 740, 65372, 1162, 236758, 1088, 15292, 1356]

这些数字是 model.vocab 字典中的值,与句子被分解成的实际 token 相关联。有趣的是,虽然我们有一个五个单词的句子,但 token 计数是八个。这是因为,如果一个单词没有直接映射到一个数字,它会被进一步分解为较小的 token。这就是为什么 15292 是 ▁ll,1356 是 ms,当组合在一起时,它们形成 _llms。这基本上是每个分词器的基本思想:不断分解字符序列,直到每个部分都映射到词汇表中的某些内容。

接下来,这些数字代表什么,它们是如何使用的?为此,打开 gemma/config.json 并找到一个名为 hidden_size 的属性。对于我们下载的模型,该值应该是 2560。每个模型都有一个内部表示,代表其词汇表中的每个 token,这种表示是一个数字数组。这个数组的长度是多少?没错,它等于 2560,即 hidden_size 的值。这个包含 2560 个数字的数组称为该 token 的嵌入。所有可能 token 的嵌入被存储为一个巨大的表格(二维数组),在我们下载的模型中的一个 .safetensors 文件中,我们上面作为分词输出得到的数字是该表格中的索引。如果我想获得"can"的嵌入,我查看该表格的索引 740。

现在,嵌入到底是什么,为什么每个嵌入中特别是 2560 个数字?为此,我将简要介绍这些模型是如何训练的。首先,模型的训练者决定每个嵌入的维度是什么,在我们的例子中是 2560。这个大小的决定是基于训练者想要在能力和效率之间进行多少权衡。嵌入中的每个数字都是一个维度值,token 的一个维度旨在代表与该 token 相关的某个方面、属性或思想。例如,如果我说"骆驼",其嵌入中的一个维度可能代表"动物",值越高,该词就越"动物",而其他某个维度可能代表"沙漠"。在我们的模型中,我们每个 token 有 2560 个维度,这意味着每个 token 有 2560 个旋钮或属性。但让我澄清,这个例子从不是真实的,仅用于让你了解维度是什么。实际上,每个维度很少代表一个干净的想法或属性,我们可以通过看我们首先是如何得出这些值的来清楚地看到这一点。

为了训练一个大型语言模型,我们通常从打算训练模型的大量文本数据开始。然后我们用完全随机的数字初始化这些嵌入向量。从高层来看,训练过程大致如下:
首先,将文本数据分解为小句子。
然后将每个句子分成两部分。第一部分,连同所有随机数字,被输入到一个算法(一个神经网络),其任务是预测句子的第二部分。
在这个算法最初的几轮运行中,对于第二部分是什么,我们会得到非常随机的预测。出现这种情况时,算法会检查预测与实际结果之间的偏差有多大,并根据所发现的差异,按比例微调嵌入向量中的数值。这称为梯度下降。之所以能够这样做,是因为这些嵌入数组本质上是 2560 维向量,因此我们可以对它们执行点积之类的几何运算;两个向量的点积会得到一个数值,用来表示它们指向同一方向的程度,进而反映它们的相似程度。
算法会持续执行,直到我们能够以可接受的准确程度猜出第二部分。达到这一状态后,这些经过调优的数值数组就成为该模型中各个 token 的嵌入。
请注意,这些维度的调优完全取决于训练所使用的大量文本数据;它们经过调优,目标就是预测这些特定数据。如果换成完全不同的数据集,得到的嵌入也会截然不同。那么,我们要如何使用这些嵌入呢?我们用它们来预测接下来会出现什么,但只有嵌入还不够。
这里很自然会产生一个问题:我们已经为每个 token 准备了调优后的嵌入,难道它们还不足以猜测下一个 token 吗?确实不够。请看下面几组 token:["golf", "ball"]、["odd", "ball"] 和 ["tar", "ball"]。单独的 "ball" 可以表达这三者中的任何一种含义;只有当 "odd"、"tar" 或 "golf" 这样的另一个 token 与它一起出现在句子中时,我们才能理解这里所说的是哪一种 "ball"。一个词所表达的含义会受到句子中与它共同出现的其他词影响。因此,当 Transformer 猜测接下来会出现什么时,它不仅需要 token 的数值表示,还需要一种数值表示,用来描述每个 token 如何影响其他所有 token 的含义。
这正是 Transformer 所解决的关键问题之一。具体来说,该算法为 token 的嵌入引入了三种不同的表示,通常称为 Q、K 和 V 向量。它们分别表示生成它们的 token 嵌入所具有的以下信息:
Q 称为查询(query),表示这个 token 希望从句子的其他部分中寻找什么。
K 称为键(key),表示这个 token 能够提供什么,也就是它可以与什么相匹配。
V 称为值(value),表示这个 token 可能对其他 token 产生的影响。
在训练期间,神经网络中会加入一组用于生成这些向量的投影向量,每种表示对应一个,分别称为 Wq、Wk 和 Wv。它们和其他所有内容一样,会在训练期间根据下一个 token 的预测偏差按比例进行调优。唯一的区别是:只有当某个 token 出现在训练句子的第一部分时,它对应的嵌入才会得到调优,而投影向量则会针对所有句子进行调优。因此,它们最终捕获的是序列中 token 之间的关系,而不是某个单独 token 的信息。

在推理阶段,这些投影向量用于生成每个 token 的 Q、K 和 V 向量,方法是将每个 token 分别与每个投影向量相乘。现在回想一下,Q 保存的是一个 token 正在寻找什么,而 K 保存的是一个 token 能够提供什么。我们取出每个 token 的 K 向量,将其与其他所有 token 的 Q 向量相乘。这是一次点积运算,其结果是一个数值,表示一个 token(它的 K)能够为另一个 token(它的 Q)提供多少信息;这个数值越高,两个 token 共享的上下文就越多。
来看一个具体例子,假设句子是:"My cat doesn't feel like going outside because it's raining."。我们知道,句子中的 "it's" 指向的是 "raining",而不是 "cat"。这一点会直接体现在数值中:"it's" 的查询向量与 "raining" 的键向量之间的点积,得分会高于同一个查询向量与 "cat" 的键向量之间的点积。
获得所有 K 和 Q 向量的点积后,下一步是将这个点积数值与提供该 K 向量的 token 的 V 向量相乘。请记住,V 表示一个 token 对其他 token 的影响,因此,将它与点积数值相乘,就能得到应当将多少这种影响施加到嵌入上。(这里我略过了 softmax 运算,它用于将点积结果归一化为 0 到 1 之间的比例。)最后,将这些经过加权的 V 向量相加,再把结果加到原始 token 的嵌入上。此时,这个 token 不仅表示自身,还包含了序列中其他 token 的部分信息。我们刚才执行的算法称为注意力(attention)。我很想直观地解释它为什么有效,但本文开头已经说过不会深入细节,因此我把这部分内容放在了文末单独的附录中。
但还有第二个问题。有时,句子的一部分会涉及某个事实。例如,请看这个句子:"Who won the 2026 FIFA World Cup?"。我们知道答案是 Spain,但如果查看原始句子,会发现其中根本没有 "spain" 这个 token。因此,仅仅让 token 相互关注,并不会神奇地把我们引向 Spain;必须有某种机制将现有 token 引导到词汇表中某处的 "spain" token。我们知道,在训练期间,应该存在许多将这种关系作为上下文的示例句子。通过训练 Q、K 和 V 投影向量,我们已经有了一种提取上下文的方法,因此,在执行注意力的部分之后,我们又引入了一对数组,称为 Wu 和 Wd。
在讲解具体过程之前,先说明一下 Wu 和 Wd 本身。到目前为止,我们引入的所有权重(嵌入以及 Wq、Wk、Wv)都可以想象成向量,也就是单独的一行数值。Wu 和 Wd 则是第一批真正的二维结构:矩阵,即由数值组成的完整网格。每个矩阵都由我们已经见过的两个尺寸构成:一边是嵌入宽度(hidden_size,2560),另一边是大得多的 intermediate_size(10240,在 config.json 中就位于它旁边)。

接下来看看 token 的向量如何通过它们。首先,将刚刚由注意力更新过的 token 向量与 Wu 相乘,得到一个包含 10240 个数值的一维数组。其次,将这个 10240 维数组中所有低于特定阈值的数值置为 0。这称为激活(activation),而这种将数值置为 0 的具体算法称为 ReLU。第三,将得到的结果与 Wd 相乘,输出一个长度为 2560 的数组,其宽度与 token 相同,然后直接将它加到原始 token 上。这就是完整的前馈步骤。

那么,训练究竟在这两个矩阵中写入了什么?它们包含的是同一个事实相互补充的两个部分。Wu 变成了一排检测器:它的 10240 列中的每一列都代表一种上下文模式,而 token 与 Wu 相乘后,会得到每种模式当前存在强度的评分。较大的正数表示“这里存在这种模式”,任何负数则表示“不是这种模式”。我们使用 ReLU 来更准确地确定希望激活哪些模式。不激活强度较弱的模式可以消除噪声。Wd 则是与之配对的回写机制:对于 Wu 中的每个检测器,Wd 都存储了一个方向,也就是该检测器触发时需要添加到 token 中的信息。简单来说,Wu 会问“这是什么情况?”,而 Wd 则回答“那么就写入这个”。
用我们的例子走一遍这个过程。在训练数据的某处,"spain" 会不断出现在 "the 2026 FIFA World Cup … won by" 附近。梯度下降会让 Wu 中的某一列逐渐成为一个检测器,恰好在“谁赢得了 2026 年世界杯”这种情境下激活;同时,它还会让配对的 Wd 方向指向 "spain" token。没有人会为这两个位置添加标签;这只是模型在不断提高训练文本预测能力的过程中自然产生的结果。在推理时,到达这一层的 token 已经通过注意力携带了类似“现在询问的是 2026 FIFA 世界杯的获胜者”这样的信息。与 Wu 相乘后,这个检测器会以很大的幅度激活,而其余检测器保持较小的数值,或者变成负数并被截断为零。再与 Wd 相乘,就会将这个幅度转换为一个指向 "spain" 方向的增量;将其加上去后,token 就会朝 "spain" 的方向偏转。这个信息直接来自模型权重,不需要句子本身提供任何帮助。

这里也需要做出与前文中“animal”和“desert”维度相同的坦诚说明:“一个检测器对应一个事实”只是用于帮助建立直觉的说法,并非字面意义上的事实,因为在真实模型中,一个事实会分散在许多模式中,而一个模式也会参与许多事实。与注意力部分一样,我在附录中放置了一个小型的完整示例,用来说明为什么这个步骤能够回忆起正确的事实。
值得区分这两个步骤的区别,因为它们很容易混淆。注意力是唯一一个让 token 彼此相看的地方;它关乎当前这个句子,哪个 token 在塑造哪个 token。前馈步骤各自独立地作用于每个 token,在每个位置应用相同的数字;它关乎模型在所有训练数据中记忆的一切。一个处理当前句子,另一个处理存储的知识。
到目前为止,我描述的是一个注意力步骤之后跟一个前馈步骤。这两者合起来构成一个单层或块,真实的模型不仅仅有一个;它堆叠了许多层,层数在 config.json 中记为 num_hidden_layers。关于这个堆栈有几点值得了解。每一层都有自己的 Wq、Wk、Wv、Wu 和 Wd;它们不在层之间共享,这正是那数十亿个数字实际存在的地方——每层的数量乘以层数。每层都作用于前一层的输出:token 的向量直接流过堆栈,每一层都读取当前向量,用注意力再次混入上下文,用前馈步骤再次回忆和转换,然后写回一个更新的向量。粗略地说,较早的层处理较简单、更局部的结构,而较晚的层构建更抽象的含义,尽管这是一种趋势而非严格规则。在最后一层之后,位于最后位置的向量——即我们输入的最后一个 token 之后的那个——是我们读取预测的向量。

这将我们带到最后一步:把那个向量转换为真正的下一个 token。首先,我们给词汇表中的每个 token 评分。我们取序列中的最后一个向量,对词汇表中的每个 token,计算最后一个向量与该 token 自己向量之间的点积。每个点积是一个单一的数字,一个分数,每个词汇 token 有一个分数。这些分数被称为 logit。我们评分的 token 向量是一个输出表的行,在许多模型中,该表就是最开始的嵌入表,以相反的方式重复使用(你可以在 config.json 中检查 tie_word_embeddings)。所以同一个表可以发挥双重作用:它在输入时把 token 转换为向量,在输出时给候选 token 评分。

现在我们有了每个可能的下一个 token 的分数,但分数不是概率,两个步骤可以解决这个问题。首先是温度:我们将每个 logit 除以一个数字 T,即温度。如果 T 小于 1,分数之间的间隔会增大,这使得模型更有决断力,更容易重复;如果 T 大于 1,间隔会缩小,这使得它更多样化、更随机;当 T 接近 0 时,模型干脆总是选择得分最高的 token。其次是 softmax:这将温度调整后的分数转换为适当的概率,全部为正且加起来等于 1。(这与我在注意力内部掠过的"归一化为比例"步骤相同。)现在每个可能的下一个 token 都有了一个概率。
最后,我们采样:我们使用这些概率作为权重随机抽取一个 token,所以概率为 0.6 的 token 大约 60% 的时间被选中。这种随机性是为什么同一个提示可以给出不同答案的原因。在此之上有几个常见的变体:贪心跳过随机性,总是选择得分最高的 token;top-k 只保留概率最高的 k 个 token 并从中采样;top-p 按概率排序 token,保留概率加起来至少为 p 的最小组,然后从中采样。然后我们做最后一件事:把我们选的 token 追加到输入中,再为下一个运行整个模型。这就是本指南开始时的循环,现在每一步都被填充进去了。(附录中有一个关于温度和 softmax 的小练习示例。)

这就是整个前向传递:文本变成 token,token 变成嵌入,注意力让它们彼此影响,前馈步骤折叠进学到的事实,堆叠的这些层细化结果,最后的评分和采样步骤选择下一个 token,反复进行。
现在退一步,注意这一切的一个特点。我们走过的所有东西,嵌入查找,然后在每一层重复的注意力和前馈步骤,然后最后的评分,这是 Transformer 的结构。而这里是我想要表达的关键:这个确切的