推理成本基础
智能体工程师并不构建模型服务系统,但智能体中的每一项设计决策都要付出相应代价:输出一个词元所需的时间远多于处理一个提示词元所需的时间;上下文在整条轨迹的生命周期内始终占用内存;等待工具期间,这些内存处于闲置状态;而集群所需的容量取决于轨迹持续多久,而非调用耗时多久。各章在首次需要讨论这些后果时加以说明,并以词元数、轮次、秒数和美元金额衡量其代价。本附录从模型服务的工作原理出发,集中推导这些后果,使希望核查某章论断,或针对不同模型或加速器重新估算的读者,能够在同一处找到所需的计算过程。此处的所有数值均来自各章采用的同一参考模型和加速器,并汇总于 节 6,以便替换参数。
如何使用本附录
从 表 1 中的智能体层面问题出发,跳转到推导其答案的相应章节。
| 如果你想了解…… | 跳转至…… | 你将获得 |
|---|---|---|
| 为什么输出一个词元所需的时间比处理一个提示词元约高两个数量级 | 节 1 | 屋顶线模型、预填充与解码的计算强度、每个词元的耗时下限,以及这一比值的来源 |
| 为什么在长上下文下,首词元延迟的增长快于线性增长 | 节 1 | 预填充中的注意力项,以及该项与权重项相等时的上下文长度 |
| 为什么批处理能降低集群的每词元成本,却不能降低单条轨迹的延迟 | 节 1 | 单步耗时随批大小变化的函数关系,以及解码转为受计算限制时的批大小 |
| 采样和语法掩码在哪里执行,以及为什么推测解码能够保持输出不变 | 节 2 | logit 与词元标识符传输的对比、掩码表的大小,以及接受规则 |
| 一条轨迹的上下文占用多少内存,以及注意力设计如何改变这一占用 | 节 3 | 根据模型结构参数推导的每词元字节数,以及多头注意力、分组查询注意力与多查询注意力的对比 |
| 同时存续的轨迹有多少条,以及工具等待和利用率如何增加容量需求 | 节 4 | 纳入工具等待的利特尔定律、驻留状态与活跃状态的对比,以及排队曲线的拐点 |
| 为什么数据库事务无法延伸至智能体的工具 | 节 5 | 作为背景知识的两阶段提交、Saga 事务和预写日志 |
| 本书假定采用哪些模型、加速器和节点参数值 | 节 6 | 可直接替换的参考输入参数 |
为什么输出的成本高于输入
每次模型调用都执行两类工作:读取提示词,这一步称为预填充(prefill);逐个生成词元以形成回复,这一步称为解码(decode)。两者在同一加速器上运行,并使用相同的权重,但在参考配置下,生成一个输出词元所需的时间约为处理一个提示词词元的 295 倍。这一比值是智能体设计中影响最深远的模型服务事实(原则 1),可由一个加速器性能模型推导出来。
屋顶线
加速器有两个极限:执行算术运算的速率 \(\pi\)(每秒运算次数),以及内存提供数据的速率 \(\beta\)(每秒字节数)。计算的算术强度 \(\mathcal{I}\) 是其每搬移一个字节所执行的运算次数。屋顶线模型 (Williams 等 2009年) 以最先构成瓶颈的极限为准,限定可达到的性能:
\[P = \min\left( \pi, \; \mathcal{I} \cdot \beta \right)\]
这两个极限在屋脊点 \(\mathcal{I}^* = \pi / \beta\) 相交。满足 \(\mathcal{I} < \mathcal{I}^*\) 的计算受内存带宽限制,增加算术运算能力对其毫无帮助;满足 \(\mathcal{I} \ge \mathcal{I}^*\) 的计算则受算术运算能力限制。对于采用 8 位精度的参考加速器,\(\pi\) = 1,979 万亿次运算/秒,\(\beta\) = 3.35 TB/s,因此 \(\mathcal{I}^* \approx\) 591 次运算/字节。
预填充与解码
一个具有 \(N\) 个参数的 Transformer,每个词元约执行 \(2N\) 次运算,即每个权重对应一次乘法和一次加法,并且必须读取共计 \(N b\) 字节的权重,其中 \(b\) 为每个参数的字节数。预填充与解码的区别在于,一次权重读取由多少个词元共享。
预填充在一次前向计算中处理全部 \(S\) 个提示词词元,因此一次权重读取可供 \(S\) 个词元使用,其算术强度约为 \(2NS / Nb = 2S/b\)。只要提示词长度超过几百个词元,这一数值就远高于屋脊点,预填充便受算术运算能力限制,其每个词元的处理时间趋近于
\[t_{\text{prefill}} \approx \frac{2N}{\pi} \tag{1}\]
单个流的解码在每次前向计算中生成一个词元,而每个词元都依赖前一个词元,因此无法通过增加同时处理的词元数来扩展这次计算。一次完整的权重读取仅供一个词元使用,算术强度为 \(2/b\),在 8 位精度下为每字节 2 次运算,约为屋脊点的百分之 0.34,每个词元的处理时间取决于从内存流式读取权重的速度:
\[t_{\text{decode}} \ge \frac{N b}{\beta} \tag{2}\]
对于参考模型,其参数量为 \(N\) = 70.6 十亿,采用 8 位精度,公式 2 给出的解码时间下限为每个输出词元 21.1 ms,而 公式 1 给出的处理时间约为每个提示词词元 71 µs。两者之比为
\[\frac{t_{\text{decode}}}{t_{\text{prefill}}} = \frac{N b / \beta}{2N / \pi} = \frac{\pi \, b}{2 \beta} = \frac{b}{2} \, \mathcal{I}^* \tag{3}\]
在参考配置下,该比值为 295。这一比值仅取决于加速器的屋脊点和权重精度,与模型大小无关,因此在同一加速器上以相同精度提供服务的所有模型都具有相同的比值。同一个数值还有第二层含义:它表示一次前向计算必须处理多少个词元,才能摆脱内存限制。下文关于批处理和推测解码的论证都会用到这一事实。
由此可得出三个智能体层面的结论,加速器服务延迟 对每一项都给出了成本量化。一次调用的延迟主要由其输出长度决定,因此输出是智能体能够控制的延迟来源。一条轨迹的成本主要由每一轮重新发送的输入决定,因为单个输入词元虽然便宜,但数量众多。而任何将输出词元转化为输入词元的设计,例如用差异内容代替全文重写,或用结构化结果代替自然语言叙述,都会将工作从 公式 3 中成本高的一侧转移到成本低的一侧。
长上下文
公式 1 仅计入权重。注意力还会增加随上下文本身增长的计算工作。位置 \(s\) 处的词元将其查询与此前的 \(s\) 个键进行比较,并组合此前的 \(s\) 个值,约需 \(4 L d s\) 次运算,这里模型的层数为 \(L\),宽度为 \(d\)。对包含 \(S\) 个词元的因果预填充过程求和,注意力会额外增加约 \(2 L d S^2\) 次运算,叠加在权重所需的 \(2NS\) 次运算之上,因此
\[T_{\text{prefill}}(S) \approx \frac{2NS + 2 L d S^2}{\pi} \tag{4}\]
二次项在 \(S^* = N / (L d)\) 处与线性项相等,对参考模型而言约为 108,000 个词元,其中 \(L\) = 80,且 \(d\) = 8,192。在 32,768 个词元时,注意力已使预填充运算量增加约 30%,而超过 \(S^*\) 后,注意力便占据主导地位。这就是上下文较长时首词元时间的增长快于线性增长的原因(工作集容量),也解释了为何将上下文维持在接近其工作集的大小,既能降低每词元成本,也能降低总成本。
批处理与集群的每词元成本
运行多条轨迹的服务器不会逐条对其进行解码,而是将各条轨迹的解码步骤组成批次,使一次权重读取就能为 \(B\) 条流中的每一条生成一个词元。忽略各条流还需读取的注意力状态,单步耗时为
\[t_{\text{step}}(B) \approx \max\left( \frac{N b}{\beta}, \; \frac{2 N B}{\pi} \right) \tag{5}\]
单步耗时始终维持在受内存带宽限制的下限,直到 \(B\) 达到 \(\pi b / 2\beta\),即 公式 3 的比值。在这一批大小以内,增加流几乎不会增加单步耗时,因此集群生成每个词元的耗时按 \(1/B\) 下降,趋近于 \(2N/\pi\),与提示词元的耗时相同。正是在这个意义上,集群的费用随其处理的词元总数而变化;对于智能体而言,这笔费用主要来自重复发送的输入(加速器服务延迟)。
批处理无法降低单条轨迹的延迟。批处理流的每一步仍至少需要受内存带宽限制的下限时间,而且轨迹仍须等待每次工具调用的结果后才能发起下一次调用,因此无论集群的批次多大,智能体关键路径的推进速度始终受限于单流解码速度。在实践中,注意力状态会先于运算能力限制批处理规模。每条流在每一步都要读取自身的键和值,读取量为 \(s \cdot m_{\text{token}}\) 字节,对应包含 \(s\) 个词元的上下文(节 3)。因此,长上下文智能体的批处理规模远未达到 公式 5 所预测的批大小,就已受到内存带宽的限制;而存储这些状态所需的内存容量,也限制了总共能够容纳的流数(节 4)。
分块预填充
当一条轨迹在等待工具后恢复运行时,必须对其新增观测进行预填充,而同一服务器上的其他轨迹仍在解码。若一次性执行,大规模预填充在整个过程中都会受运算能力限制,并阻塞所有共同调度的解码任务,直至预填充完成,导致这些轨迹的词元之间出现较长间隔。分块预填充将观测拆分为固定大小的块,每块包含 \(c\) 个词元,并在每轮执行中将一个块与正在进行的解码任务一同运行 (Agrawal 等 2024年)。由于单独的解码任务几乎不占用运算能力(公式 5),最多约 \(\pi b / 2\beta\) 个词元的块可以几乎不增加额外耗时地一同执行,使每轮耗时保持在接近解码耗时下限的水平。超过这一大小的块会延长每个共同调度步骤的耗时。恢复运行的轨迹需要稍长的时间才能得到首个词元,而其他每条轨迹的词元间隔都能保持在有界范围内(分块预填充调度)。
解码循环机制
除前向传播外,每个解码步骤还包含三项逐词元工作:将 logits 转换为词元、应用语法掩码,以及在使用推测解码时检查草拟词元。只有在采样器附近执行时,这些工作的成本才会较低。本节估算其开销。
采样在哪里执行
在每一步,模型都会为词表中的每个条目生成一个 logit。若词表包含 \(|\mathcal{V}| = 128{,}000\) 个条目,并以 16 位精度存储,则每条流每个词元产生的一个 logit 向量大小为
\[128{,}000 \times 2\text{ bytes} = 256{,}000\text{ bytes}\]
一台运行 \(B = 64\) 条流、每条流每秒生成 \(R = 40\) 个词元的服务器,每秒会输出 \(B \cdot R = 2{,}560\) 个词元,因此,将原始 logits 发送到独立进程进行采样所需传输的数据量为
\[2{,}560 \times 256{,}000\text{ bytes/s} \approx 655\text{ MB/s}\]
通过主机互连传输,每秒进行 2,560 次小规模传输,每次都在解码关键路径上产生固定的传输和同步开销。在加速器上进行采样,可将每一步的输出缩减为一个 4 字节的整数词元标识符;在相同负载下,传输量约为 10 KB/s,降幅为 \(256{,}000 / 4 = 64{,}000\times\)。因此,词元标识符是模型服务与其外部所有组件之间的自然交换单位,也是调用接口以流式方式传输的内容(调用契约)。
语法掩码
受语法约束的解码器在每一步都需要获取当前自动机状态下的合法词元集合(文法引导解码)。若为词表中的每个条目存储一位,则 \(|\mathcal{V}| = 131{,}072\) 的掩码占用
\[\frac{131{,}072}{8} = 16{,}384\text{ bytes}\]
而编译为 \(|Q| = 512\) 个自动机状态的模式,其完整表需要 \(512 \times 16{,}384 = 8{,}388{,}608\) 字节,约为 8.4 MB。这种大小的表可以放入加速器的片上缓存,因此应用掩码只需一次查表,以及一次融合到采样步骤中的逐元素加法。
另一种方案是在独立的主机进程中推进自动机。这样,每一步都需要将采样得到的词元传输到主机、在主机端执行状态转移、将下一个掩码传回,并重新启动下一步。合计而言,在理想条件下,这些操作的开销约为每个词元 20 µs;在主机资源发生争用时,可达到 50 至 150 µs。对于包含 500 个词元的输出,这意味着关键路径上约有 10 至 75 ms 的停顿,直接增加了解码吞吐量的开销。对于状态空间过大、无法使用稠密表的大型语法或递归语法,可以使用压缩的词元前缀树,并为可能的后续状态预先计算掩码,但原则不变:当 logits 就绪时,掩码也必须就绪。何时约束解码 决定何时值得付出这一约束成本,而 确定性有限自动机与语法约束 Logit 解码 则证明受约束的输出始终可以被解析。
推测解码
推测解码在不改变输出分布的前提下缩短串行链 (Leviathan 等 2023年; Chen 等 2023年)。小型草稿模型 \(q\) 逐个提出 \(k\) 个词元 \(\tilde{x}_1, \dots, \tilde{x}_k\);由于草稿模型较小,这一过程的成本很低。随后,目标模型 \(p\) 在一次前向传播中对全部 \(k\) 个位置进行评分。该次传播只需读取一次目标模型的权重,即可处理 \(k\) 个词元,因此根据 公式 5,只要 \(k\) 远低于 \(\pi b / 2\beta\),其耗时就与生成一个词元大致相同。从左到右扫描时,候选词元 \(\tilde{x}_j\) 被接受的概率为
\[\alpha_j = \min\left(1, \frac{p(\tilde{x}_j \mid \cdot)}{q(\tilde{x}_j \mid \cdot)}\right) \tag{6}\]
在首次拒绝时,剩余的草稿词元会被丢弃,并从归一化的残差分布中抽取一个替代词元
\[p_{\text{res}}(v) = \frac{\max\left(0,\; p(v) - q(v)\right)}{\sum_{w \in \mathcal{V}} \max\left(0,\; p(w) - q(w)\right)} \tag{7}\]
公式 6 的接受检验与 公式 7 的重采样规则相结合,使每个输出词元的分布都与仅从 \(p\) 中采样时完全一致。因此,每次目标模型传播会输出一个至 \(k + 1\) 个词元,加速效果取决于草稿模型与目标模型达成一致的频率。串行依赖仍然存在。目标模型仍按顺序验证草稿词元,而一次拒绝会丢弃其后的所有词元,因此推测解码降低的是每个输出词元的常数成本,而非消除这条串行链。在采用大批量处理的服务器上,验证传播会与其他流争夺计算资源,这也是该技术在负载较轻时收益最大的原因(推测解码何时值得采用)。
注意力状态的大小
模型在生成 token \(t\) 时,会关注此前每个 token 的键和值。如果在每一步都重新计算这些键和值,每一步的计算量就会随整个前缀的长度增长,因此服务系统会将它们保存在 **离散 token 表示** 的 KV 缓存中。每个 token 所需的缓存大小由模型的结构参数决定。
每个 token 的字节数
对于一个具有 \(L\) 层、每层 \(H_{\text{kv}}\) 个键值头、头维度为 \(d_{\text{head}}\)、每个存储元素占 \(b_{\text{kv}}\) 字节的解码器,每个 token 在每一层的每个键值头中都存储一个键向量和一个值向量:
\[m_{\text{token}} = 2 \cdot L \cdot H_{\text{kv}} \cdot d_{\text{head}} \cdot b_{\text{kv}} \tag{8}\]
而包含 \(T\) 个 token 的上下文占用
\[M_{\text{KV}}(T) = T \cdot m_{\text{token}}\]
对于参考模型,\(L\) = 80、\(H_{\text{kv}}\) = 8、\(d_{\text{head}}\) = 128,并采用 16 位存储,因此 \(m_{\text{token}}\) = 327,680 字节,所以包含 32,768 个 token 的上下文约占 10.7 GB。从上下文 token 到 KV 状态 将其换算为单个节点可容纳的轨迹数量。
注意力设计
键值头的数量是一项设计选择,它直接影响 \(m_{\text{token}}\)。多头注意力为每个查询头提供独立的键和值。多查询注意力让所有查询头共享一个键值头 (Shazeer 2019年)。分组查询注意力介于两者之间,让每组查询头共享一个键值头 (Ainslie 等 2023年)。不同注意力设计下的注意力状态 在保持参考模型其他维度不变的情况下应用这三种设计。
| 设计 | 键值头数量 | 每个 token 的字节数(16 位) | 32,768 个 token 时 |
|---|---|---|---|
| 多头注意力 | 64(每个查询头对应一个) | 2,621,440 | 85.9 GB |
| 分组查询注意力 | 8 | 327,680 | 10.7 GB |
| 多查询注意力 | 1 | 40,960 | 1.34 GB |
以 8 位存储键和值可使表中每项数值减半,使分组查询设计中每个 token 的占用降至 163,840 字节,但可能影响准确性。丢弃受到较少注意力的条目可以进一步限制内存占用,但会使状态产生信息损失,因此后续步骤无法恢复已丢弃的内容(从上下文 token 到 KV 状态)。
工具等待期间的占用
服务内存的成本取决于占用量和占用时长。如果一条轨迹的注意力状态在时长为 \(t_{\text{wait}}\) 的工具等待期间始终驻留内存,则它会占用
\[O = M_{\text{KV}}(T) \cdot t_{\text{wait}}\]
字节·秒,却不生成任何内容。由于智能体的工具等待时间通常远长于两次等待之间的模型调用时间,这种空闲占用可能超过活跃解码期间的占用,因此值得针对每次等待,作出 保留、驱逐、重计算或卸载 中所述的保留、驱逐、重新计算或卸载决策。
分页存储
为每条轨迹分配一块按其最大上下文长度确定大小的连续区域,会浪费该区域的大部分空间,因为智能体上下文的增长难以预测,而且往往提前结束。分页存储将内存池划分为固定大小的块,每块包含 \(B_s\) 个 token,并通过一张表将每条轨迹中的位置映射到块,因此一条轨迹只占用已写入数据的块 (Kwon 等 2023年)。唯一的浪费是每条轨迹最后一块中尚未填满的尾部空间,平均每条轨迹为
\[\mathbb{E}[\text{waste}] \approx \frac{B_s - 1}{2} \cdot m_{\text{token}}\]
即几千字节到几兆字节,与上下文长度无关。块还可以共享,使分叉分支以及具有共同前缀的轨迹只需保留该前缀的一份副本(分页 KV 分配)。
轨迹排队
智能体的容量取决于轨迹的存续时间,而非调用耗时;轨迹在其整个生命周期内占用的每一种资源,都必须根据这一生命周期确定容量。
连续批处理
服务器将多条轨迹的解码步骤组成批次(公式 5),而批次的组织方式决定了调用的等待时间。静态批处理会保留所有请求,直到耗时最长的请求完成,因此短暂的工具调用也会排在耗时较长的规划过程之后等待。连续批处理以单个解码步骤为粒度进行调度 (Yu 等 2022年)。输出结束的请求,无论是遇到停止 token 还是发起工具调用,都会在下一步退出批次;等待中的请求,例如携带新观测从工具返回的轨迹,则会在下一步加入。因此,调用等待的是空闲槽位,而非整个批次处理完毕;服务器的行为就像一个队列,其服务台数量等于批次槽位数量。本节其余部分将按这一模型展开分析。
包含工具等待的利特尔定律
对于任何稳定系统,系统内的平均项目数量等于其到达率乘以其在系统内的平均停留时间 (Little 1961年):
\[L = \lambda \cdot W \tag{9}\]
一条包含 \(H\) 轮的轨迹,每轮先进行模型调用,再执行工具,因此其在系统内的停留时间为
\[W_{\text{traj}} = \sum_{k=1}^{H} \left( T_{\text{model}}^{(k)} + T_{\text{tool}}^{(k)} \right)\]
这对应于 轨迹时长核算 中的时长核算,但不包含审批项和运行时项。作为计算示例,假设轨迹以每秒 2 条的速率启动,每条轨迹运行 15 轮,每轮的模型耗时为 4 秒,工具耗时为 20 秒。每条轨迹存续 360 秒,约为 6 分钟,因此由 公式 9 可得 720 条存续轨迹。每条轨迹只有 17% 的时间用于模型调用,因此在任一时刻,平均有 120 条轨迹正在调用模型。
这两个数字之间的差距,就是在工具等待期间保留状态所带来的成本。如果每条存续轨迹都让一个包含 32,768 个 token 的上下文保持驻留,注意力状态的总量约为 7.7 TB,相当于 15 个节点的容量,这里采用的是 节 6 中的参考资源池。如果只有正在调用模型的轨迹保留该状态,同样的工作只需要约 2.4 个节点的容量,代价则是在轨迹返回时,对其上下文重新进行预填充或重新加载。这一权衡就是 保留、驱逐、重计算或卸载 中关于保留、驱逐、重新计算或卸载的决策;同样,到达率与生命周期的乘积也用于确定运行框架工作进程、沙箱租约以及速率限制余量的规模(轨迹容量、沙箱池与重置)。
排队曲线的拐点
共享服务器,无论是模型端点、沙箱池还是验证器,都会随着繁忙程度的增加而让调用方等待更久,而且等待时间会在服务器达到饱和之前急剧上升。对于一个利用率为 \(\rho\)、平均服务时间为 \(\mathbb{E}[S]\)、到达间隔的变异系数平方为 \(c_a^2\)、服务时间的变异系数平方为 \(c_s^2\) 的单队列系统,金曼近似给出的平均等待时间为:
\[W_q \approx \frac{c_a^2 + c_s^2}{2} \cdot \frac{\rho}{1 - \rho} \cdot \mathbb{E}[S]\]
第一个因子表示工作负载的变异程度,第二个因子表示其负载水平。当到达呈突发性(\(c_a^2\) = 2),且服务时间近似服从指数分布(\(c_s^2\) = 1)时,变异因子为 1.5;平均等待时间分别约为服务时间的 1.5、3.5 和 13.5 倍,对应的利用率分别为 50%、70% 和 90%。智能体工作负载在这两个方面都具有较高的变异性,因为轨迹会以突发方式扇出调用,而生成长度可能相差多个数量级,因此共享智能体基础设施通常在远低于饱和的水平下运行。对于 \(c\) 台相同的服务器,Allen-Cunneen 近似将 \(\rho/(1-\rho)\) 替换为所有服务器均处于忙碌状态的概率,该概率由 Erlang C 公式 (Erlang 1909年) 给出,再除以 \(c(1-\rho)\)。将多台服务器汇集成池,可以缩短给定利用率下的等待时间,但拐点仍然存在。当服务时间呈重尾分布时,少数长轨迹会主导等待时间,这也是调度器将它们与短轨迹分开处理的原因(重尾排队论)。
能容纳多少条轨迹
对于一台内存为 \(M_{\text{HBM}}\)、权重占用 \(N b\) 字节、运行时预留内存为 \(M_{\text{runtime}}\) 的服务器,可用于注意力状态的剩余内存为
\[M_{\text{dynamic}} = M_{\text{HBM}} - N b - M_{\text{runtime}} \tag{10}\]
而平均上下文长度为 \(\bar{T}\)、能够同时驻留内存的轨迹数量为
\[B_{\max} = \left\lfloor \frac{M_{\text{dynamic}}}{\bar{T} \cdot m_{\text{token}}} \right\rfloor \tag{11}\]
对于参考节点,公式 10 留下 526.0 GB 的内存,公式 11 则允许 48 条轨迹在上下文长度为 32,768 个 token 时同时驻留,这与 从上下文 token 到 KV 状态 推导出的数量相同。超过 \(B_{\max}\) 时,服务器必须将某条等待中的轨迹的状态移出内存。重新计算该状态需要对其 \(T\) 个 token 执行预填充,耗时约为 \(2NT/\pi\) 秒,依据为 公式 1(上下文较长时耗时更多,依据为 公式 4);重新加载该状态则需要通过连接其卸载位置的链路传输 \(T \cdot m_{\text{token}} / \beta_{\text{link}}\),因此
\[\text{recompute when} \quad \frac{2 N}{\pi} < \frac{m_{\text{token}}}{\beta_{\text{link}}}\]
为每个上下文 token 对应的成本。在一阶近似下,这一比较不依赖上下文长度,因此,盈亏平衡点通常由链路和模型决定,而非由轨迹决定。保留、驱逐、重计算或卸载 进一步纳入等待时长,以判断这两种操作是否值得执行。
经典事务背景
数据库早已解决了智能体恢复问题的一种形式,持久执行 和 故障恢复 借鉴了这一解决方案,同时解释了为何它无法被完整照搬。本节为尚未接触过这些机制的读者概述经典机制。
事务与两阶段提交
数据库事务使一组写入具有原子性:要么全部提交,要么由数据库引擎中止并撤销全部写入;引擎使用撤销日志和锁,确保其他客户端看不到中间状态 (Gray 1981年)。两阶段提交将原子性扩展到多个数据库。协调者要求每个参与者做好准备,即获取所需的锁并承诺能够提交;只有所有参与者都同意后,才在所有数据库中提交。这两种机制都依赖两个假设:每个参与者都能将变更保持在已准备但不可见的状态,并且锁可以一直持有到事务结束。
智能体的工具不满足这两个假设。代码托管平台、云控制平面、支付服务或邮件服务器都不提供准备阶段,而是在每个请求到达时立即提交,使其他人和程序能够看到结果并据此采取行动。此外,一条轨迹中,每次模型调用持续数秒,每次测试运行持续数分钟,等待审批则持续数小时;如果在轨迹的整个生命周期内持有锁,就会阻塞这些资源的所有其他用户。事务边界的瓦解 阐述了这对恢复的影响。
Saga 事务
Saga 用一系列短事务取代一个长事务,每个短事务都立即提交,并为每个步骤 \(T_i\) 配置一个在语义上撤销它的补偿步骤 \(C_i\) (Garcia-Molina 和 Salem 1987年)。如果步骤 \(T_k\) 失败,Saga 就按逆序执行 \(C_{k-1}, \dots, C_1\)。补偿恢复的是应用所关心的不变量,而非精确的先前状态;没有补偿操作的步骤(例如发送电子邮件)是一个枢轴点,越过该点后,Saga 只能继续向前完成。轨迹 Saga 模式 基于这一模式构建轨迹 Saga,枢轴操作的不可逆性 则确定枢轴点的位置。
预写日志
数据库使用预写日志在崩溃后恢复自身的数据页。每项变更都会先被持久记录,然后才写入其所修改的数据页;每条日志记录都包含一个日志序列号,以及一个指向同一事务前一条记录的指针。ARIES 算法 (Mohan 等 1992年) 分三轮执行恢复:扫描日志以确定哪些事务仍在进行;重演历史以恢复每项已记录的变更;通过恢复日志中记录的先前值,撤销未提交事务的变更。
智能体保留了这一规约的前半部分,却舍弃了后半部分。其运行时会在动作离开主机之前记录执行该动作的意图,因此在崩溃之后,每个效应要么已知是有意产生的,要么已知尚未发生(意图先于效果)。但这些效应落在日志无法掌控的系统中,因此日志可以记录某个效应,却无法通过恢复某个值来撤销它。撤销由此转变为补偿,而补偿是 saga 的职责,不是日志的职责。
参考值
本附录中的估算以及各章中的服务估算,均采用同一个参考模型和同一种参考加速器,其数值汇总于 表 2。各章仅将其描述为运行在当前数据中心加速器上的一个具有 700 亿参数的模型,因为论证并不依赖于具体产品。这些数值来自 MLSysIM 注册表。若要针对不同硬件或不同模型重新估算,可将其数值代入上述方程;公式 3 的比值和 公式 8 的字节数是大多数章节所依赖的两个结果。
| 量 | 符号 | 参考值 | 用途 |
|---|---|---|---|
| 参数量 | \(N\) | 70.6 十亿 | 每 token 的下界、内存中的权重 |
| 层数 | \(L\) | 80 | 注意力状态、长上下文预填充 |
| 模型宽度 | \(d\) | 8,192 | 长上下文预填充 |
| 查询头数 / 键值头数 | \(H_q\) / \(H_{\text{kv}}\) | 64 / 8 | 由设计决定的注意力状态 |
| 头维度 | \(d_{\text{head}}\) | 128 | 注意力状态 |
| 权重精度 | \(b\) | 1 字节(8 位) | 解码下界、从脊点到比值的换算 |
| 键值精度 | \(b_{\text{kv}}\) | 2 字节(16 位) | 注意力状态 |
| 加速器内存带宽 | \(\beta\) | 3.35 TB/s | 解码下界、脊点 |
| 加速器峰值运算速率,8 位 | \(\pi\) | 1,979 万亿次运算/秒 | 预填充下界、脊点 |
| 加速器内存容量 | \(M_{\text{HBM}}\) | 80 GB | 常驻轨迹 |
| 每节点加速器数量 | \(n_{\text{acc}}\) | 8 | 节点内存池 |
| 脊点 | \(\mathcal{I}^*\) | 591 次运算/字节 | 前向计算转为算术运算受限的位置 |
| 每个输出 token 的解码下界 | \(t_{\text{decode}}\) | 21.1 ms | 调用延迟 |
| 每个提示 token 的预填充时间 | \(t_{\text{prefill}}\) | 71 µs | 调用延迟、重发开销 |
| 每 token 的注意力状态 | \(m_{\text{token}}\) | 327,680 字节 | 每条轨迹的内存占用 |
| 每节点注意力状态池 | \(M_{\text{dynamic}}\) | 526.0 GB(16 位权重,预留 20 GB) | 每节点轨迹数 |