系统目标

计算机系统工程关注程序怎样在真实设备上提供服务。系统规模可以从一台机器扩到许多机器,但扩容后仍需要保存正确结果、处理故障,并在约定时间内完成请求。

设计通常从需求开始,再把需求写成可以检查的规格(specification)。例如“做一个可靠的文件服务”还缺少具体条件,需要继续说明文件怎样读写、成功回复代表什么、允许哪些设备失效,以及需要支持多少并发请求。

项目 需要说明的条件
功能 提供哪些接口,参数和返回值是什么
正确性 转账总额、文件内容和操作顺序要满足什么规则
故障 需要承受断电、节点宕机、网络中断中的哪些情况
性能 在什么数据量与请求量下,测量延迟、吞吐和资源占用
扩展 增加设备后,容量与处理能力能增长多少,搬迁要做哪些工作

接下来才是实现和验证。实现可以由人编写,也可以借助编程工具;验证需要依据已经写清楚的规格,检查实现是否满足条件。设计判断要联系具体用途:训练数据读取、银行转账和聊天同步的访问方式与故障要求各有特点。

思考题:一个并行程序在两台机器上运行会出错。修改测试,让它只使用一台机器后通过测试,能否说明问题已经解决?

解答:

原要求包含并行执行。单机测试没有触发原来的并发场景,因此还需要在规定的机器数、数据量和交错顺序下验证。测试条件、预期结果和必须保留的功能,都应作为规格的一部分。

检查性能优化时也要这样做:固定输入与正确性标准,再测优化前后的时间。删掉计算、缩小问题或减少并发,都会改变被测任务。

规模增长

计算机把数据表示成二进制的 0、1。一个二进制位称为 bit,8 bit 组成一个字节,记为 B。文件大小表示存了多少字节,计算量表示处理这些数据需要做多少运算,用户请求数表示同时有多少人让系统做事。这三种规模可以分别增长。

思考题:把模型写成 action=F(input)action=F(input) 时,F、input、action 分别表示什么?为什么常用语言表示输入和输出?扩大模型、数据和计算规模可以带来什么作用?

解答:

F 是模型的计算过程,input 是输入和当前上下文,action 是模型给出的下一步输出。语言可以描述任务、背景和结果,也有大量文本可用于训练。增加参数能提高模型表示复杂规律的能力,增加数据和计算为学习提供条件;能改善多少,仍要通过具体训练和评测判断。

token 可以是一个词、词的一部分或一个符号,由文本编码方式决定。模型为候选 token 给出原始分数 logits,再把分数换成概率;采样表示按这些概率抽取下一项,抽到后继续生成后面的内容。

文本先被分词器转换成 token 编号,再由模型转换成数值向量进行计算。图像也能数字化:像素保存颜色数值,多模态模型通过图像编码器取得数值表示。程序代码、工具调用与机器人动作同样可以用约定格式记录。数字化提供了可保存、传输和计算的输入,具体表示还要适合所使用的模型。

把模型写成 action=F(input)action=F(input) 时,可以把一次执行理解为“读取当前输入,计算输出,再按应用规则使用输出”。例如编程助手读到用户要求和工作区文件,生成修改建议或工具调用;工具执行后的结果又成为下一次输入。文件系统负责保存工作区,推理程序负责计算输出,应用负责协调二者。

数据变多、计算量增加、用户请求增多时,存储和计算系统也要扩展。语言模型生成文本时反复计算下一个 token,使用更多参数、处理更长文本或同时服务更多请求,都会增加计算和存储工作。

编程助手、客户服务等应用需要读取数据、运行模型,并保存用户请求和结果。使用规模增加后,这些工作会给计算设备、网络和存储带来更多负担。

练习:初始值为 1,每天比前一天增长 1%,连续 365 天后是多少?写出计算式。

解答:

每天乘以 1.01,连续乘 365 次,得到 1×1.01365≈37.781\times1.01^{365}\approx37.78。因此一年后的值约为 37.78,是初始值的 37.78 倍。

初始值为 1、每天增长 1% 时,每天把已有结果乘以 1.01,365 次相乘得到 1.01365≈37.81.01^{365}\approx37.8。要在计算系统中获得持续增长的收益,算法也得能用好增加的资源。

搜索和学习

搜索从候选方案中寻找较好的结果,例如尝试棋盘上的不同着法。学习用已有数据调整模型中的数值参数,例如看过很多带标签的图片后,调整判断图片类别的规则。训练是调整参数的过程,推理是用已经训练好的参数处理新输入。

思考题:增加计算量为什么可能改善模型或搜索结果?这种推理需要对算法作什么假设?

解答:

搜索可以利用额外计算探索更多着法、看得更深;学习可以利用额外计算处理更多样本、完成更多参数更新。前提是所选方法能有效使用新增资源,数据和训练安排也适合任务。单纯增加无效或重复的计算,收益需要另行评测。

Rich Sutton 在《苦涩的教训》中总结,长期来看,能持续利用计算资源的方法往往更有效。搜索和学习都可以利用更多计算资源,分别探索更多候选方案、从更多数据中调整模型。具体任务能提高多少,仍取决于算法、数据和训练方式。

程序员设计搜索或学习过程,让程序从计算和反馈中找出具体策略。系统负责保存数据、执行计算、传递结果,并处理运行中的故障。

棋类搜索

搜索树的每个节点代表一个局面,每条边代表一步着法,叶子是暂时停止搜索的位置。分数越大,表示对己方越有利。在自己的回合,从子节点中取最大分数;在对手回合,从子节点中取最小分数,模拟对手会选对己方最不利的一步。这样逐层算回根节点,就能比较当前可选的着法。

棋类程序包含评估函数和搜索。评估函数 f(s)f(s) 给未结束局面打分,搜索展开未来着法。Minimax 假设己方选最大收益、对手选最小收益;alpha-beta 剪枝根据已知上下界跳过不会改变选择的分支。

Minimax 的分支值与剪枝

图中白方在根节点取最大值,黑方在下一层取最小值。左分支叶子 3、5、6 给出最坏结果 3;中间 6、7、4 给出 4。右分支先发现 1,最终最小值不可能高于 1,因此白方已有 4 时无需继续看右侧 9、2。根最终选择中间分支。

Deep Blue 在 1997 年击败 Kasparov,它使用深度搜索、专用硬件和并行计算。往后多看几步,程序有机会发现当前走法的后果。搜索树也会变大,剪枝和并行计算用来控制这部分成本。

若每个局面平均有 b 种着法,看 d 层的末层就约有 bdb^d 个节点。例如 b=35,d=4b=35,d=4 时,末层约有 150 万个节点。比赛还限定思考时间,因此程序通常在某个深度停止搜索,用评估函数估计尚未结束的局面,再把分数逐层传回。

更多算力可以让程序在同一时间内检查更多节点。专用硬件、并行搜索、棋局知识及已有局面资料共同帮助实现这件事;搜索时间、局面评估和数据访问仍需要配合。IBM 的 Deep Blue 介绍说明了其并行计算设计。

围棋自博弈

自博弈让程序与自己下棋,保存局面、选择的着法和最后胜负,再用这些记录训练模型。策略模型为候选着法分配概率,价值模型估计某个局面的胜算,搜索再利用这些估计继续探索。Elo 是依据对局结果计算的棋力评分,图中分数上升表示在相应评测对手面前胜率提高。

围棋约 2×101702\times10^{170} 个合法局面、国际象棋约 104710^{47} 个的量级,并比较约 250 与 35 的候选着法数。围棋搜索空间更大,直接穷举难以实行;AlphaGo 结合搜索与学得的策略和局面评估,引导探索方向。

自博弈与棋谱监督训练的 Elo 曲线

横轴是训练时间,纵轴是衡量棋力的 Elo 分数。紫线利用人类棋谱训练,起点较高。蓝线从较弱水平开始自博弈,在这次实验中继续提高,超过紫线和 AlphaGo Lee 的虚线。程序通过对局结果获得反馈,再调整下次的选择。

图像特征

图像由像素组成,每个像素保存亮度或颜色数值。特征是从这些数值中提取的有用表示,例如边缘方向、角点或某种纹理。SIFT 寻找在缩放、旋转后仍较稳定的关键点,并为附近图案计算描述向量。CNN(卷积神经网络)让小滤波器在图像上移动,对附近像素做加权计算,再把多层结果交给分类器。

SIFT 由人设计关键点、尺度、方向和描述子规则;CNN 在规定网络结构后训练滤波器等权重。两者都需要方法设计,区别在于哪些表示可随训练调整。

同一图像经过 SIFT 与 CNN 的处理路径

上方先找关键点、确定方向、计算特征,再汇集特征并训练分类器。图中的特征提取规则由人预先确定。下方的 CNN 用多层计算提取特征,把预测结果与 cat 标签比较,再沿蓝色箭头调整网络参数。

AlexNet 第一层训练后得到的滤波器

这些滤波器能识别不同方向的边缘和颜色变化,参数通过训练得到,网络结构和训练条件由开发者选择。2012 年 ImageNet 比赛中,AlexNet 的 top-5 错误率为 15.3%,第二名为 26.2%。top-5 错误率指模型给出的前五个候选类别都未命中正确类别的比例。

算力和数据

CPU、GPU 执行计算,内存保存正在处理的数据,磁盘或 SSD 长期保存训练资料。网络把不同机器上的数据送到需要它的位置。某个设备的运算速度增加后,其他部分也要及时提供数据,否则计算设备就会等待。

思考题:单芯片受到功耗、散热和制造条件限制时,怎样从系统层面增加可用计算量?增加机器后又要处理哪些工作?

解答:

可以增加处理器核心、GPU 或机器数量,把独立任务或可拆分的计算同时执行。机器增加后,要安排任务和数据位置,传输中间结果,协调共同修改,并处理机器与网络故障。共享网络、存储或串行步骤的耗时还要计入总时间。

单位计算成本长期下降,使更多搜索与学习成为可能。单芯片仍受功耗、散热和制造约束,多芯片、多机能扩大总计算量,但通信、调度和故障会增加系统工作。

互联网和代码仓库积累了大量文本,为语言模型提供了训练数据。存储系统保存这些内容,让训练程序反复读取;计算设备和网络则负责处理并传递数据。

思考题:语言模型可以从哪些地方取得大量训练数据?存储容量和读取速度对训练有什么影响?

解答:

通用文本来自互联网等资料,程序与说明可以来自代码仓库。容量决定能保留多少训练资料;读取速度决定计算设备能否及时取得下一批输入。若输入供应较慢,GPU 会等待,因此还要组织文件、安排读取和传输,并检查数据质量。

训练计算量

模型参数是训练过程中调整的数值,常用“权重”表示。一次浮点加法或乘法各计为一次 FLOP,FLOPs 表示运算总数,FLOP/s 表示每秒完成多少运算。批次是一次交给模型的一组样本;序列长度是每个样本包含多少 token。Transformer 是处理序列的网络结构,其中的注意力计算用于结合不同 token 的信息。

思考题:B 条序列、平均每条 S 个 token、参数量为 W 的模型,一步训练大约需要多少浮点运算?公式中的系数 6 怎样得到?同一近似下,一个 token 的前向计算和训练分别约需多少运算?

解答:

一步处理 BS 个 token。一次乘法和一次加法约计两次运算,所以每个 token 的前向计算约为 2W2W;计算输入梯度和权重梯度约再需 4W4W,训练合计约为 6W6W。因此一步训练约需 6BWS6BWS 次运算。这是简化估算,注意力等具体计算还会带来额外开销。

思考题:B、W、S 分别提供了哪些扩展方向?单步批次大小与整次训练使用的数据总量有什么关系?

解答:

B 是每步处理的序列数,W 是参数量,S 是平均序列长度。增大 B 能在一步中处理更多输入,增大 W 扩大模型,增大 S 处理更长上下文。整次训练处理的 token 总数要把所有步骤相加:D=∑tBtStD=\sum_t B_tS_t;只改变一次批次的大小,还要结合训练步数才能确定总处理量。

Chain-of-Thought(思维链)在回答中写出中间推理步骤,需要生成较长文本。训练通过调整权重学习规律;推理时生成更长的过程,则增加本次请求的计算量和上下文。这些计算需要分别记录,才能判断资源花在了哪一步,以及具体任务的结果是否改善。

若 B=128B=128、S=2048S=2048、W=109W=10^9,这一步约需 1.57×10151.57\times10^{15} 次浮点操作。估算运行时间时,还要知道设备每秒能完成多少操作,以及读取数据和通信花费的时间。

缩放定律

损失是衡量预测误差的数值,通常越小越好。例如,模型给正确答案很低的概率时,损失就较大。训练通过计算参数变化会怎样影响损失,再据此调整参数。图中的测试损失在未直接用于训练的数据上计算,用来观察模型处理新输入的效果。幂律形如 L=aN−bL=aN^{-b}:N 增加一倍时,这一项损失乘上 2−b2^{-b}。

损失随计算、数据与参数规模变化的曲线

三幅图横轴分别为计算量、数据集 token 数和参数量,纵轴为测试损失。在所示范围的对数坐标里,趋势近似直线,因而可用幂律拟合;浅色训练轨迹还表明,同一预算下配置不同,结果也不同。

思考题:训练预算固定时,应怎样安排模型参数量和训练数据量?为什么需要寻找二者的配比?

解答:

设参数量为 N、总训练 token 数为 D,总计算量近似为 6ND6ND。预算固定时,增加参数会挤占可用于处理数据的计算,增加数据也会占用同一预算。可以用较小实验拟合损失,再在预算约束下寻找损失最低的配比。图中每条固定预算曲线的最低点,就是相应实验的较合适配置。

Chinchilla 进一步研究固定训练预算如何分配参数 N 和数据 D,得到下面的拟合公式:

L^(N,D)=E+ANα+BDβ\hat L(N,D)=E+\frac{A}{N^\alpha}+\frac{B}{D^\beta}

E 是公式中与 N、D 无关的常数项。E、A、B 等系数由实验拟合得到。这里的 B 是拟合系数,训练计算量公式中的 B 表示批次大小,两处符号按所在公式解释。

固定 FLOP 预算下的参数量与损失

每条曲线对应一个固定计算预算。模型较小时,增加参数能降低损失。继续增加参数,会减少同一预算下可处理的训练数据,损失可能回升。曲线最低点表示这次实验中较合适的参数和数据配比。

拟合出的较优参数和数据规模随预算变化

右侧两图分别给出不同预算下较合适的参数量和 token 数。在这项研究的训练条件下,两者都接近按计算量的平方根增长。预算增加时,需要一起增加参数和训练数据。实际选模型大小时,还要考虑推理成本、数据质量和网络结构。

选择训练配置可以按下面的步骤进行:

  1. 固定数据处理方法、训练方法和验证集,使不同实验的损失具有可比性。
  2. 先训练几组较小模型,改变参数量与训练 token 数,记录计算量和验证损失。
  3. 用这些结果拟合损失与规模的关系,在预算约束 C≈6NDC\approx6ND 下寻找较合适的配比。
  4. 用更大的中间实验检查预测,再决定是否投入完整训练预算。

例如预算固定时,模型参数量 N 增加一倍,按这一近似,能够处理的 token 数 D 就要减少一半。大模型可能有更强的表达能力,也需要足够数据完成训练,因此需要同时评估两项。小规模实验形成的逐级验证过程,常称为 scaling ladder。

拟合是对实验范围内趋势的估计。模型结构、数据分布和训练方式变化后,需要重新检查趋势;少数实验点也可能无法可靠确定最低点。Chinchilla 论文给出了固定预算下参数量与数据量的实验研究。

工作区示例

一个贯穿多个系统问题的例子是带远程工作区的编程助手。用户在笔记本上提出要求,助手运行工具读写文件,模型服务生成下一步输出。工作区放在远端后,更换笔记本也可以重新连接到原来的文件;正在进行的修改和回复仍需要由服务记录。

组成 需要完成的工作
文件系统 把名称和字节偏移转换为实际存储位置
RPC 将本地请求传给远端,匹配回复并处理超时
并发控制 协调多个工具或用户对共同文件的修改
副本与日志 在节点失效后保留已确认数据,恢复处理进度
推理程序 读取模型权重,计算 token,并优化相同任务的执行速度

推理实验可以使用较小模型,例如 Qwen3-0.6B。B 表示十亿,0.6B 对应约六亿这个量级的参数。若只按六亿个 32 bit 权重估算,权重内容约占 6×108×4=2.4×1096\times10^8\times4=2.4\times10^9 字节;实际运行还需要缓存、中间结果和程序内存,具体参数数目与格式以模型文件为准。

优化这类程序时,先确认输出符合要求,再固定硬件、输入长度、输出长度和并发量测量。相同工作量的执行时间从 10 秒降到 5 秒,才得到两倍加速;还可以分别检查模型加载、计算、文件读取和网络等待占了多少时间。

应用和存储

应用是实现聊天、购物或生成文本等功能的程序。用户操作产生请求,应用根据请求读取或修改保存的数据,再返回页面或结果。文件系统按文件名访问字节;数据库按表、行和字段组织数据。接口规定调用方要传什么参数、能得到什么结果,让应用可以逐步请求底层完成工作。

思考题:一个应用同时提供社交、购物交易和模型服务,后台需要哪些组件?一次用户请求怎样经过这些组件?

解答:

需要请求入口、业务程序、文件与数据库存储,以及模型计算服务。LAMP 中,Linux 管理资源,Apache 等服务器接收请求,PHP 等业务代码读取或修改 MySQL 和文件系统中的记录,再生成响应;模型请求还会送到计算服务。一次购物请求可依次经过入口、业务判断、商品查询或交易提交,最后返回页面。

一个应用可以同时提供社交、交易和模型服务。LAMP 中 Linux 管理系统资源,Apache 一类服务器处理 Web 请求,MySQL 与文件系统保存表和文件,PHP 等业务层生成响应。

思考题:应用使用文件名和字节偏移,磁盘使用块号。谁负责记录文件对应哪些块?断电后,系统怎样重新找到这些记录?

解答:

文件系统通过目录项找到 inode,再由 inode 和索引块找到文件的数据块。名称、大小、索引和分配记录也需要可靠保存。重新挂载时,系统读取超级块等布局信息,从根目录逐级查找路径,再沿 inode 和块索引读取内容;故障恢复按已保存的日志或其他恢复记录处理未完成更新。

应用通过文件路径、键值和表访问数据。文件系统记录 chat.log 的名字、大小和数据块位置,数据库处理表和查询。应用调用这些接口后,由相应系统完成底层读写。

目标 问题
一致性 并发读写与多副本结果是否符合约定
可靠性 故障时数据是否保存、服务是否可用
性能 单个请求多久完成、单位时间处理多少请求

缓存、副本和并行都同时影响这些目标,需要一起检查。

存储扩展

容量表示能保存的数据总量。例如 40 TB 是全部可用空间,每天新增 8.6 TB 是增长速度;容量除以每天增长量,得到能使用的天数。保留三份副本时,同一份新照片会占三份空间,因此每天实际新增占用量也乘以三。

练习:每周新增约 60 TB 数据,按平均每天 8.6 TB 计算,40 TB 空间能使用多少天?保存三份副本时又能使用多少天?

解答:

按题目给出的每天 8.6 TB 计算,一份数据占用时,40/8.6≈4.6540/8.6\approx4.65 天,约为 4.7 天。三份副本每天占用 3×8.6=25.83\times8.6=25.8 TB,40/25.8≈1.5540/25.8\approx1.55 天,约为 1.6 天。这里把 40 TB 全部看作可用于新增内容的空间。

Facebook 2010 年的一组上传数据为:每周约十亿张照片,产生约 60 TB 新数据,平均每天约 8.6 TB。按 40 TB 存储容量估算,约 40/8.6=4.740/8.6=4.7 天装满;若数据保留三份,约 40/(3×8.6)=1.640/(3\times8.6)=1.6 天就用完容量。

分布式存储把多个磁盘接入网络,应用通过 RPC 使用统一接口;增加磁盘或阵列可扩容量。从一块本机磁盘扩展到多个远端磁盘阵列后,系统要记录每条数据所在的节点,并安排扩容时的数据迁移。

这里有两份规模不同的信息:实际文件内容可能很大,定位它们的管理记录也会随文件数增长。记录每个文件的位置是一种方法;根据键计算归属可以减少逐文件查表的工作,但仍需要知道当前有哪些节点、这些节点在映射中的位置,以及失效后怎样找到其他副本。

数据分片

键是用于寻找一条数据的标识,例如照片 ID。哈希函数把键转换成整数,再用这个整数选择存储节点。取模 a % N 表示 a 除以 N 后的余数:21 除以 3 余 0,除以 4 余 1,所以节点数从三变四时,这条数据的归属会变化。分片就是把不同键分配给不同节点,每个节点保存全部数据中的一部分。

思考题:数据原来按 hash(key) % N 分配,节点数增加到 N+1 后,旧数据的归属会怎样变化?图中的哈希值 21、39、28、34、44、26 从模 3 改成模 4 时,有几项需要迁移?

解答:

取模的除数变化后,旧键的余数也可能变化,所以已有数据需要重新定位并迁移。六个值的变化为:21 从 0 到 1,39 从 0 到 3,28 从 1 到 0,34 从 1 到 2,44 从 2 到 0,26 仍为 2。因此有五项迁移,一项留在原节点。

哈希取模使用 hash(key) % N。固定 N 时可散布键;节点数变化会改变许多余数,均衡程度还取决于哈希与键分布。

三节点扩为四节点时,示例六个键的迁移

哈希值 模 3 模 4
21 0 1
39 0 3
28 1 0
34 1 2
44 2 0
26 2 2

图中 A、B、C、D 对应余数 0、1、2、3。哈希值 21 的数据从 A 移到 B,26 留在 C,六项中有五项迁移。迁移会占用网络和磁盘,系统需要安排搬运速度,并处理搬运期间的读写。

一致性哈希把节点和键映到环,键交给顺时针遇到的第一个节点。

这个方法增加了一层间接映射:键先进入固定的逻辑空间,再由这个空间上的区间找到物理节点。节点数变化时,逻辑空间保持不变,只调整有关区间的归属。名称中的“一致性”描述这种映射随节点增减的变化性质,副本读写是否一致还需要单独的协议。Dynamo 论文介绍了这种分片方法及虚拟节点。

加入 D 后仅改变相邻区间的归属

扩容前,k3、k4 顺时针找到的第一个节点都是 B。D 插入 A 与 B 之间后,k3 先遇到 D,因此从 B 移到 D;k4 位于 D 之后,仍归 B。均匀分布的情况下,新节点平均接收约 1/(N+1)1/(N+1) 的数据。实际结果还受节点位置和数据分布影响。一个物理节点可以在环上对应多个虚拟节点,帮助分散负载。

移动范围变小后,搬迁仍要处理并发读写。一个简化的在线搬迁流程是:先从旧节点复制区间内容,记录复制期间的新修改;新节点追上更新后,发布新的归属版本;仍使用旧映射的请求由旧节点转发或要求重新查询。确认旧访问路径已退出后,才回收旧内容。实际系统按自己的协议实现这些步骤。

均匀散布键也需要结合请求分布检查。如果一个热门文件承担了大部分请求,即使每台保存的字节数接近,该文件所在节点仍可能过载。容量均衡、请求均衡和节点处理能力需要分别测量。

延迟和缓存

命中表示所需数据已经在缓存中,未命中时需要继续访问较慢的存储。假设一次命中用 1 ms,未命中的完整请求用 10 ms,100 次请求有 80 次命中,则平均耗时为 (80×1+20×10)/100=2.8(80\times1+20\times10)/100=2.8 ms。缓存有效程度同时取决于命中比例和两条访问路径的耗时。

延迟是一次请求从发送到收到回复的时间,吞吐量是每秒完成的请求数或数据量。远端读取要经过网络传输、服务器排队、磁盘读取和回复传输。增加机器可以分担请求,减少排队;网络传播时间还受距离影响。

不同访问路径的延迟数量级

图中的示例值包括:内存访问约 100 ns,SSD 随机读 4 KB 约 150 μs,同一数据中心的网络往返约 500 μs,磁盘寻道约 10 ms。150 μs 等于 150000 ns,能看出这些操作的时间差距。图中数字来自不同年代和条件,分析自己的程序时需要实际测量。

思考题:速度快的存储容量有限,为什么较小的缓存仍可能改善很多请求的耗时?应观察哪些访问统计?

解答:

访问通常集中在一部分热门数据上,缓存这部分内容就能覆盖许多请求。可以统计各键的访问频率,确定热点;再测量命中率、命中和未命中时的完整请求耗时,计算平均等待时间。效果取决于缓存覆盖了多少访问,以及两条访问路径的速度。

缓存把常用数据放到速度更快的位置。如果少量数据承担了大部分访问,小容量缓存也能减少很多等待。是否有效,可以通过命中率和请求耗时来测量。

延迟表示一次请求从发出到取得结果用了多久,吞吐表示单位时间完成多少请求或传输多少数据。远端访问会经过编码、发送、排队、处理和回复,计算时需要把这些部分都算进去。

网络传播还有物理限制。若两点直线距离为 d,传播速度至多为光速 c,单程传播时间至少为 d/cd/c。距离约 1000 km 时,这一下界约为 3.3 ms,实际路径和设备处理还会增加时间。减少往返次数、在近处缓存数据,可以减少请求经过长路径的次数。

缓存收益可以用平均时间估算。假设命中耗时 1 ms,未命中耗时 20 ms,命中率为 80%,忽略其他开销,平均访问时间为 0.8×1+0.2×20=4.80.8\times1+0.2\times20=4.8 ms。常说的“少量数据承担多数访问”是负载的一种可能特征,需要用真实访问记录确认。

缓存更新

先看写缓存带来的确认问题。A 的请求由服务器 1 处理,B 的请求由服务器 2 处理,两台各有缓存,共同访问后面的存储服务。

步骤 状态
① A 写入 X 新内容先进入服务器 1 的缓存
② 返回成功 后端存储尚未接收到新内容
③ A 告诉 B 已完成 B 向服务器 2 请求 X
④ B 查询后端 服务器 2 缓存未命中,后端仍返回旧内容或不存在

如果接口承诺“写成功后其他用户能读到新值”,步骤②就需要等待满足这一承诺的条件。可以先把内容提交到后端,再回复,并使其他缓存失效或重新验证;若接口允许暂时读取旧值,也要明确这种范围。写入确认、持久保存和其他读者可见分别需要检查。

读取结果也包含“查无此项”。把这个结果缓存起来后,后续查询可以很快返回,但创建文件时也要处理这份旧记录。版本号是每次修改后推进的编号;客户端发现版本变化,就重新取得内容。一致性模型进一步规定:更新传播过程中,返回旧值、新值或暂时等待,哪些行为符合接口约定。

思考题:B 已把“X 不存在”存入缓存,A 随后创建 X。B 再次查询会发生什么?系统可以怎样让它取得新结果?

解答:

B 若直接使用旧缓存,仍会返回“X 不存在”。A 创建 X 后,可以通知 B 清除该记录;也可以让缓存按 TTL 过期,或让 B 查询修改时间、版本号后重新读取。缓存失效后,B 再到服务器查找,才能取得新文件的信息。

B 再次查询 X 时仍使用旧缓存

B 先查询 X,得到“文件不存在”,并把这个结果存入缓存。A 随后写入 X。B 再次查询时直接返回缓存中的 X:none,因此仍看不到新文件。系统可以在写入时通知 B 清除旧缓存,也可以让 B 定期检查修改时间或版本。TTL 是缓存的有效时长,超过这个时间后要重新查询。采用这些办法时,需要说明更新后多久能读到新内容。

多种一致性模型

一致性模型说明多次读写可以出现什么结果。最终一致性允许副本暂时不同,更新停止且通信恢复后,各副本最终一致。因果一致性要求有前后依赖的操作按同一顺序被看到。顺序一致性要求所有操作能排成一个共同顺序,并保留各进程的执行顺序。线性一致性还要保留实际时间上的先后关系。图中的 strict 表示更严格的理想时间要求。

为了满足更多顺序要求,节点通常需要交换更多消息。选择哪种模型,要看业务能接受哪些读取结果和等待时间。

机器故障

平均故障间隔描述长期统计。单台机器平均一万天故障一次,可以估算每天产生 1/100001/10000 次故障;一万台的预期次数相加,得到平均每天一次。某天可能没有故障,也可能多台同时出问题。设备增加后,维护和恢复需要成为日常流程。

故障可能来自硬件损坏、配置错误、软件 bug、内存泄漏、网络中断和电力问题。还需要考虑暂时停顿:例如 Java 垃圾回收的某些阶段会暂停应用线程,服务器稍后恢复,但客户端在等待期间可能已经超时。垃圾回收是程序运行中的常规内存管理活动,暂停方式取决于收集器。Oracle 的 G1 文档介绍了暂停与并发执行的关系。

没有回复时,客户端通常只能确认“等待已经超出限制”。服务器可能已经宕机,也可能仍在处理,或者处理完成但回复被网络延迟。时间限制帮助系统安排重试、切换和错误返回,操作最终有没有执行,还需要依据请求身份与服务端记录判断。

静默错误

有些故障会停止程序,有些会让程序继续运行并产生错误数据。后者称为静默数据损坏(silent data corruption,SDC)。例如内存中的 00000010 表示 2,一个 bit 变化后成为 00000011,程序读到的数值就变为 3,却可能没有收到异常。

在模型训练中,一个错误数值可能进入后续梯度与参数更新,影响许多步骤。大量设备长时间工作,会增加遇到低概率错误的机会;具体风险还取决于硬件、运行环境和软件检查。

处理时需要同时考虑发现与恢复。校验和可以检查保存或传输的内容是否变化;ECC 内存能够检测、纠正其设计范围内的部分错误;关键计算还可以进行有规则的结果检查或重复计算。发现错误后,可以隔离相应设备,从此前确认可靠的检查点恢复,再重算受影响步骤。

检查点也需要验证完整性,复制错误数据可能把同一错误传播到多个副本。不同检查手段保护的对象和错误范围需要写清楚。Silent Data Corruptions at Scale给出了大规模系统中这类问题的实际案例。

写入和恢复

原子更新需要对外呈现完整结果。例如同时修改账户 A、B 时,可以先保存一份记录,注明这笔转账要修改哪些值;恢复程序读取记录后,判断应完成两边修改还是撤销。日志保存修改过程,写时复制先写入另一份完整内容,再更新所使用的版本。持久性还要求相应记录在规定的故障后能读回来。

持久性说明成功写入的数据在系统规定的故障情况下能继续保存。原子性说明一组更新要么全部完成,要么全部撤销。系统需要分别说明能承受哪些故障,以及哪些操作共同完成。

思考题:写整个文件时,只写完一部分机器就崩溃。恢复后可能读到什么?成功写入的持久性与整份更新的原子性分别要求什么?

解答:

如果只完成部分块的更新,恢复后可能出现一部分新内容、一部分旧内容。持久性要求已报告成功的数据在规定故障后仍可恢复;原子性要求整份替换对外呈现完整旧版本或完整新版本。若需要整份原子替换,可以用包含相应数据的日志或写时复制,配合提交记录决定恢复哪一版本。

多块写入中断,出现一半新一半旧的文件内容

图中只写入了一部分新数据,应用读到一半新、一半旧的内容。如果需要整份文件同时更新,系统要安排日志、写时复制或其他保存流程,在恢复时选出完整旧版本或新版本。

A、B 分处不同机器时,单边转账完成使总额错误

原本 A=100、B=0。B 加上 100 后,A 的扣款还没完成,机器就崩溃了,随后读到 A+B=200。转账需要把两次修改放在同一事务中,并在故障恢复时一起完成或撤销。

服务和副本

可用性表示某段时间内服务能正常响应的比例。不可用概率 p 表示在所选时间窗口里,一个副本无法服务的机会。独立故障时,要让三个副本同时失效,需要三个事件共同发生,所以概率相乘得到 p3p^3。共享电源或网络会让故障彼此相关,部署时应把这种共同依赖算进去。

思考题:什么是分布式系统?为什么一台应用没有直接访问过的机器发生故障,也可能让应用无法继续工作?

解答:

分布式系统由多台联网机器上的组件共同提供服务。应用可能先调用数据库,数据库再依赖另一台存储机器,因此间接依赖的故障也会影响请求。系统需要识别故障,保存可恢复的记录,再为相关请求安排其他访问路径、重试或错误返回。

思考题:保存多份数据和失败后重试怎样帮助系统继续工作?多个副本的更新顺序需要怎样协调?

解答:

空间中的多份副本提供多条访问路径,一台机器失效后可由其他副本服务;时间上的重试让暂时丢失或未完成的请求有机会继续。更新要按协议确定共同接受的顺序,并让副本依次应用。重试还需使用稳定请求标识,识别已经完成的操作,避免重复产生业务修改。

假设在同一时间窗口内,每个副本不可用的概率都是 p,且 k 个副本的故障相互独立,那么它们同时不可用的概率为 pkp^k。p=10−3p=10^{-3}、k=3k=3 时为 10−910^{-9}。这个计算依赖故障相互独立的假设。实际部署时,常把副本放在不同机架,使用不同的电源,减少它们同时故障的机会。

60 日服务健康统计快照

按图中的统计,60 天内异常时间约为 60×24×(1−0.9929)=10.22460\times24\times(1-0.9929)=10.224 小时。顶部状态表示当前运行情况,下方历史条记录过去 60 天的情况。

应对故障的常见做法包括保存多份数据,以及在请求失败后重试。数据副本分布在不同机器上,一台机器失效后,其他机器可以继续服务。系统还要协调副本更新,并决定主节点失效后由谁接替。

Raft 和 Paxos 用来协调节点对状态变化的意见,并处理主节点选择。它们通过交换提议和确认消息,让参与节点确定共同接受的记录,故障后再按协议恢复服务。

副本的数目还需要与协议的确认条件一起理解。三份内容能增加数据保留的机会;继续接受更新,则要保证各节点不会独立决定互相冲突的结果。Raft 通常需要可通信的多数节点,三节点集群的多数是两台,因此一台失效时仍可能继续,只有一台可通信时暂停新的提交。Raft 原论文说明了多数确认与选举规则。

前面的 p3p^3 只表示三台全部不可用的概率。若某项服务需要三台中的至少两台,且独立不可用概率均为 p,则不能取得多数的概率为 3p2(1−p)+p33p^2(1-p)+p^3,分别对应恰好两台与三台不可用。数据保留、能够读取旧内容和能够提交新修改,要按实际服务条件分别计算。

重试也会消耗容量。先向一台发送请求,等待 50 ms 后再联系其他副本,增加的是时间上的尝试;同时向多台发出请求,增加的是空间上的尝试。两种方式都会让服务收到重复请求,修改数据时需要识别同一逻辑操作,读取时也要检查结果是否符合版本要求。

服务容量

纵向扩展(scale-up)增加单机的核心、线程、内存或设备能力;横向扩展(scale-out)增加机器并分配任务。处理能力还会影响请求的排队等待,六个请求的数值例子及延迟模型见并发原子性。

并行计算

向量是一列数,矩阵是按行和列排列的数。矩阵与向量相乘时,每个输出项由一行矩阵与整个输入向量计算得到。例如一行是 [2,3]、输入向量是 [4,5],输出为 2×4+3×5=232\times4+3\times5=23。各输出行可以分给不同计算单元处理,最后拼成完整输出向量。

一个线程等待 I/O 时,其他线程可以继续处理请求。增加机器也能同时处理更多任务。如果它们共用的网络或存储已经忙满,继续加机器的收益就会减小。

矩阵向量乘法按行分给三个计算单元

AA 分为 A0,A1,A2A_0,A_1,A_2,三个计算单元读取同一个 x,分别写入 y0,y1,y2y_0,y_1,y_2。串行计算约需 3T;并行计算约需 T,再加上分配任务和收集结果的时间。三台机器仍各做约 T 的计算,加速来自同时执行。

模型并行

网络层接收上一层的输出并计算下一组数值。把不同层放在不同 GPU 上称为模型并行,前一层的结果要通过网络或设备间链路送给后一层。训练的前向传播计算预测,反向传播计算损失对各权重的梯度,优化器再按梯度更新权重。梯度描述权重稍微变化时损失怎样变化,这些前后依赖决定了可同时执行哪些工作。

同一批数据按层经过不同 GPU,留下等待气泡

layer 1 把计算结果传给 layer 2,layer 2 再传给 layer 3。处理一批数据时,图中很多时间都在等待,这些空闲时段称为流水线气泡。交错处理多个小批次可以减少等待,调度时仍要安排好各层的数据依赖。

分层还可以解决容量问题:一个模型的权重和中间状态放不进单块 GPU 时,把不同层交给不同设备共同保存和计算。不过,一批数据必须先经过第 1 层再进入第 2 层,各层之间仍有顺序。

以三层、每层耗时 T 的纯前向例子说明流水线。只有一个请求时,总耗时为 3T,每块 GPU 只工作 T,平均利用率约为三分之一。若把四个独立请求交错送入三层,忽略传输,总耗时约为 (4+3−1)T=6T(4+3-1)T=6T;每块工作 4T,利用率升到三分之二。每个请求仍需依次经过三层,收益来自让不同请求同时占用不同层。

真实训练还包含反向传播、参数更新与缓存容量限制,需要额外调度。长时间租用大量 GPU 时,空闲资源也有成本。例如按每天 90 万美元计费、平均三分之一资源空闲的假设,约 30 万美元对应空闲资源;这个计算用于理解利用率,具体费用与设备和计费条件有关。

一个训练步先做前向传播,再计算梯度,最后更新参数。反向传播要用前向结果,更新要等梯度完成。满足这些条件后,独立的子任务可以同时执行。

练习:模型有三层,每层计算耗时 T。每个 GPU 都能运行完整模型时,三个 GPU 同时处理三个独立请求,每个请求需要多久,期间共能完成几个请求?

解答:

每个请求依次经过三层,按题目忽略额外开销,耗时为 T+T+T=3TT+T+T=3T。三个 GPU 在这段时间内分别完成一个请求,共完成三个。对应吞吐量为 3/(3T)=1/T3/(3T)=1/T 个请求每单位时间;每个请求的三层计算仍保留前后依赖。

三个 GPU 各跑完整模型,处理三个不同请求

图中每块 GPU 都能运行完整模型。每个请求依次经过三层,耗时约 3T;三块 GPU 在这段时间内完成三个请求,4、5、6 还在排队。实际能处理多少请求,还取决于模型大小、内存和其他开销。

训练同步

学习率决定沿梯度方向走多大一步。以损失 w2w^2 为例,在 w=1w=1 附近,w 增加 0.01,损失约增加 0.02,局部变化率约为 2;这就是该位置的梯度。学习率取 0.6 时,更新量为 −0.6×2=−1.2-0.6\times2=-1.2。更新后 w=−0.2w=-0.2,损失为 0.04;再次使用旧更新量得到 w=−1.4w=-1.4,损失变为 1.96。更新所依据的参数版本因此会影响训练结果。

同步等待汇总与异步陈旧更新的数值例子

两个 GPU 都读到 w=1w=1,各自算出更新量 Δw=−1.2\Delta w=-1.2。同步训练先把两份更新取平均,再执行一次,得到 w=−0.2w=-0.2。异步训练收到第一份更新后得到 -0.2,收到第二份后继续加上 -1.2,得到 -1.4。第二份更新是根据旧的 w=1w=1 算出的。图中的损失为 w2w^2,从 -0.2 变成 -1.4 会让损失增大。同步训练需要等待较慢的工作节点;异步训练则需要控制旧参数算出的更新对结果的影响。

特定实验中同步和异步的精度及收敛训练量

横轴是 worker 数,蓝线是同步训练,绿线是异步训练。在这次实验中,蓝线的测试精度更高;worker 较多时,绿线达到收敛所需的训练量也更大。比较训练总时间,还要结合每步耗时。

图中的陈旧更新说明:改变执行顺序后,需要重新检查训练算法的条件。同步更新也要满足合适的步长等条件,异步方法则可能通过限制延迟、调整步长或利用稀疏更新进行分析。HOGWILD!给出了特定稀疏条件下异步并行更新的收敛分析。

性能目标通常是达到某个质量所需的总时间。可以估算为“每步时间 × 达到目标所需步数”,再计入读取、通信和保存检查点的时间。减少单步等待后,如果需要更多步骤才能达到目标,仍要用完整训练过程判断收益。

任务划分

矩阵乘法 C=ABC=AB 中,cij=∑r=1kairbrjc_{ij}=\sum_{r=1}^{k}a_{ir}b_{rj},表示 A 的第 i 行和 B 的第 j 列逐项相乘后相加。m、k、n 是对应的行列数。按输出行分工时,每台完成不同的 cijc_{ij};沿 k 分工时,每台只计算每个 cijc_{ij} 的部分加法项,最后还要把这些部分和相加。

设 A:m×kA:m\times k、B:k×nB:k\times n、C:m×nC:m\times n。不同划分方法需要保存不同输入,也会产生不同数量的传输和求和。

按行复用整个 B,与二维分块的对照

左图按行划分 A,每台机器保存一部分 A 和完整的 B,独立计算对应的输出行。图中的“零通信”指输入已经放好后,各机器在计算过程中可以独立工作;开始时分发输入、结束时收集结果仍需要传输数据。右图把输出划成二维块,每台机器取得计算该块所需的输入,并按划分方式传递数据或汇总部分结果。

练习:四台机器共同计算 C=ABC=AB,输入已经放在对应机器上,结果集中收集。

  1. 按输出行划分,每台完成四分之一的 C,收集需要传输多少结果,还要做几次矩阵加法?
  2. 沿求和维度 k 划分,每台得到一份完整大小的部分和,收集与求和代价分别是多少?

解答:

  1. 按行划分时,四份输出各为四分之一 C,收集量为 4×(C/4)=C4\times(C/4)=C,直接拼接即可,矩阵加法为 0 次。
  2. 沿 k 划分时,每台都产生与 C 同样大小的部分和,按图中的集中收集方式共传输 4C4C。四份部分和相加需要 3 次完整矩阵加法;若 C 有 mn 个元素,就是 3mn3mn 次标量加法。

一般矩阵按行与沿 k 切分的归并代价

按行划分时,每台计算 Ci=AiBC_i=A_iB,输出的行互不重叠,收集后直接拼接,总共传输一份 C。沿 k 划分时,每台计算 Pi=AiBiP_i=A_iB_i,每份都与 C 一样大,最终 C=∑iPiC=\sum_iP_i。图中的集中收集方式要传输四份 C,再做三次加法。这里假设输入已经放在相应机器上。

思考题:A 只有一行时,按行分给四台机器会有几台空闲?沿 k 划分还能怎样使用四台机器,最后需要怎样合并结果?

解答:

按行划分只有一行可分,交给一台机器后,其余三台空闲。若 k 可划成四部分,四台可以各计算这行输出的部分和,最后把四份结果相加。收集四份与 C 同样大小的结果,再做三次求和,就能得到完整输出。

只有一行时,按行切分让三台机器无工作

A 只有一行,按行划分只能交给一台机器,其余三台空闲,图中标为 3 idle。沿 k 划分仍能让四台一起计算,最后再把结果相加。因此,选择划分方法时要先看矩阵的行数和列数。

请求调度

在一个请求内部划分计算,称为任务内划分(intra-task);把多个请求分给不同实例,称为任务间调度(inter-task)。前者检查子任务的依赖与通信,后者还要检查每台实例已有的缓存和等待队列。

语言模型逐个生成 token 时,可以保存前面 token 的注意力键 K 与值 V,形成 KV cache,后续生成便能继续使用已经算好的结果。相同模型版本、相同前缀及相关计算条件下,请求还可能复用已有前缀缓存。缓存占用内存,转移请求时也可能需要重新计算或传输它。PagedAttention 论文讨论了 KV cache 的管理与共享。

因此,一个请求在缓存命中的实例上计算更快,也可能因为该实例排队较长而更晚完成。估算时要把排队、准备数据或缓存、实际计算共同相加,再决定放到哪台。

练习:有八个请求,实例 1 已缓存请求 1 至 6 所需数据,实例 2、3 分别缓存 7、8。根据图中的准备与执行耗时,全部就近分配、平均分配、混合分配各需要多久?为什么调度要同时考虑缓存和排队?

解答:

图中命中已有数据的请求耗时为 1,转到其他实例时,准备加执行共耗时 2。就近分配的负载为 6、1、1,完成时间为 6。图中的大致平均分配让实例 2 执行 7、4、5,耗时 1+2+2=51+2+2=5,总完成时间为 5。混合分配让实例 1 执行 1 至 4,其余两台各处理一个本地请求和一个转移请求,负载为 4、3、3,完成时间为 4。调度要同时计入已有数据的节省和最长队列的等待。

请求在三台实例上的分配方案

图中有八个请求。实例 1 已缓存请求 1 至 6 所需的数据,实例 2、3 分别处理 7、8。把 1 至 6 都留在实例 1,完成时间是 6;把请求分散出去,其他实例要先准备数据,示例中完成时间是 5;混合方案让实例 1 保留 1 至 4,其余请求转移后,完成时间降到 4。安排请求时,要同时计算等待队列和准备数据的时间。

扩展限制

扩展后的总耗时由计算、数据读取、通信、排队和协调共同组成。安排任务时,可以先列出每一步依赖哪份数据、由哪台机器执行,再计算最慢的一条依赖路径。新增设备能承担的独立工作越多,等待公共资源的时间越少,扩展收益通常越大。

系统扩展会受到芯片算力、功耗、网络延迟和带宽限制。CAP 定理讨论网络分区时的读写服务:如果节点间无法通信,系统要在保持线性一致性和继续回答所有未故障节点的请求之间作选择。实际服务还要规定请求最多等待多久。

事务、故障恢复、副本协调、安全和网络都会影响扩展结果。系统规模增加后,仍要保证读写结果正确,故障后能恢复数据,并满足请求的完成时间和处理量要求。

思考题:两个副本之间发生网络分区时,能让每一侧持续完成读写,同时让读取保留全局实时顺序吗?CAP 定理说明了什么约束?

解答:

在允许网络分区的读写模型中,这一保证组合无法同时成立。C 要求线性一致性,A 要求非故障节点收到的操作最终得到符合接口的响应,P 表示系统需要面对节点间长期无法通信。
若一侧已经完成写入,另一侧随后读取,它需要知道那次更新才能保留实时顺序。通信持续中断时,可以等待或暂停部分操作来保留 C;若两侧继续独立完成操作,某些读取就可能缺少已完成的更新。约束针对这种分区条件下的保证。CAP 原论文

参考资料