Skip to content

07-17 上午:算法—系统协同设计

最后更新于·约 12821 字

课程题目是 Algorithm-Infra Co-Design for AGI。这里的 infra1 包括训练集群、kernel2、并行策略、显存管理、编译器、推理引擎和服务调度。模型结构决定了通信量、KV cache3 和访存量,系统与硬件的限制反过来又约束注意率、MoE4、精度和上下文长度能怎么选。

协同设计的基本对象可以用一条流水线串起来。文本先被切成 token,token 经过 embedding 变成向量,再经过多层 Transformer block;每层包含 attention5 和 FFN。训练时在这些计算之后计算 loss、反向传播并更新参数;推理时保存 KV cache、采样下一个 token,并把请求交给调度器。集群、显存和网络带宽分别影响这条流水线的不同位置。把术语放回这条流水线,后面各节的优化目标就不会散开。

术语自测
  1. kernel 在 GPU 语境中指什么?
  2. KV cache 保存的是哪两个张量,为什么要保存?
  3. MoE 里的 expert 为什么通常不能按“数学专家”理解?
  4. 量化6中的 scale 起什么作用?

答案分别为 GPU 上执行的一段计算函数;按层保存的 key/value,用于避免重复计算历史 token;同构但参数不同的 FFN 分支,分工由训练得到;把整数格点映射回浮点范围的缩放系数。

观察到的约束 需要同时检查的对象
参数、梯度和优化器状态放不下 分片策略、精度、offload 与通信代价
attention 中间量过大 分块 I/O、上下文并行、窗口或稀疏结构
MoE 吞吐不稳定 路由分布、token 重排、专家 batch 与 All-to-All
decode 延迟随上下文增长 KV cache 容量、带宽、分页和调度
低比特权重未带来加速 解包/反量化、scale 读取、矩阵指令和实际 shape

算法与系统协同设计

近十余年的模型发展是一脉相承的:CNN、Transformer、预训练、对话模型、多模态模型与 agent7 并不是彼此替代的几条支线。新的阶段通常沿用前一阶段的表示、训练数据和基础设施,再把规模或任务边界推远一点。

CNN 是 convolutional neural network,卷积神经网络,主要用于图像等具有局部空间结构的输入。Transformer 是由 attention 和 FFN 堆叠成的神经网络结构,是当前语言模型的主流骨架。预训练是在海量通用数据上训练模型的过程;后训练是在预训练模型上继续用指令数据、偏好数据或强化学习调整行为。多模态模型同时处理文本、图像、音频等不同形式的输入输出。这些阶段共享 tokenizer、embedding、矩阵乘法和自动微分等基础设施,因此一个模型结构的变化会沿着训练框架、显存布局和推理服务继续传递。

预训练带来的变化尤其大。模型不再只为某个固定数据集训练,而是用大量通用语料先得到一个基座模型,再通过指令数据、偏好数据、强化学习或工具交互适配任务。这样训练出来的模型覆盖面更广,但训练和服务成本也被放大到不能只靠多买几张卡解决。

讲者把目前的工作分成两个容易割裂的社群:算法、数据和模型能力是一边,并行、编译、存储与运行时是另一边。实际方案常要同时改动两侧。GPU 矩阵单元偏好低精度时,量化误差需要重新评估;MoE 路由触发大量 All-to-All 时,expert placement 和通信路径就进入设计;KV cache 受限时,MQA、GQA8 或 latent attention 会成为模型层面的选项。

协同设计发生在方案尚未固定时。计算、内存、通信和部署代价都要进入模型选择;等模型定型后才处理运行代价,能调整的空间往往很小。

课件的 MLSys 三角:Compute、Memory、Communication 三者互相牵制,任何一方成为瓶颈都会进入系统设计的视野。

图:算力、显存与互连构成系统三角;协同设计是在三者之间找平衡,而不是只盯峰值算力。

一个具体的判断方式是先问瓶颈落在哪里。训练时,参数状态可能放不进显存,activation 也可能更大;长上下文服务时,瓶颈常转向 KV cache 的容量与访存;MoE 则会把 token 路由和 All-to-All 通信推到前台。原因不同,优先动作也不同。将所有问题统称为显卡不够会掩盖真正的约束。

思考题

长上下文服务中 KV cache 放不下,为什么只换更大显存不是完整答案?

答案

更早需要判断瓶颈是容量、带宽还是调度。可以通过 GQA/MQA/MLA 压缩 KV、分页管理、前缀复用、冷块换出或限制并发来改善。只增加显存可能仍被带宽和尾延迟限制,还会改变并发与成本。

思考题

一个方案在模型结构上减少了 attention 计算量,却让每层都必须额外做一次跨卡 AllGather。为什么不能只看 FLOPs 就下结论?

答案

端到端时间由算力、显存容量、访存带宽、通信和调度共同决定。FLOPs 减少只说明理论计算量变少,若额外通信不能被计算覆盖,实际延迟可能上升。协同设计要比较完整负载下的时间和显存,而不是只比较某一个算子的算术量。

Scaling Law 与数据

大模型能力提升可以分成预训练、后训练和测试时推理三个阶段。预训练常讨论 scaling law9:在模型参数量、训练 token 数和训练计算量增加时,训练损失怎样下降。它给的是经验上的资源分配规律,不是保证能力线性增长的承诺。数据质量、重复率、模型结构和训练稳定性都会改变实际曲线。

课件说明 scaling law 与数据飞轮:更多算力与更好的数据可用性相互促进,形成持续改进的循环。

图:scaling 不只是模型更大,还包括数据、算力与评估之间的反馈循环(data flywheel)。

语言模型的预训练目标仍很朴素。给定前面的 token,预测下一个 token:

公式里的 \(p_\theta(x_t\mid x_{<t})\) 可以用一个三词例子说明。若前文是“深度 学习”,模型对下一个 token 输出

模型 0.60   学习 0.05   系统 0.30   其他 0.05

真实文本的下一个 token 是“模型”,交叉熵损失就是 \(-\log 0.60\),约为 0.51。若模型把“模型”的概率提高到 0.80,损失降到 \(-\log 0.80\),约为 0.22。交叉熵奖励的是给正确 token 更大概率;训练损失下降并不自动等价于回答质量、事实正确性或安全性提升,所以后训练和评测仍然必要。

\[ \mathcal{L}_{\mathrm{LM}}=-\sum_t\log p_\theta(x_t\mid x_{<t}). \]

交叉熵只要求模型拟合训练分布,并不会直接告诉它哪一种回答对用户更有用。所以预训练之后还需要监督微调、偏好优化或强化学习。代码、数学、可执行工具等场景有一个特点:编译是否通过、测试是否通过、工具调用是否成功都是相对明确的反馈,这类反馈可以继续变成训练数据。

测试时计算是另一条尺度轴。对同一个基座模型,系统可以让它生成更长的推理过程、并行采样多个候选、调用检索或工具、进行验证和反思。这样会提高单次请求的计算和延迟,是否划算要看任务是否能从额外搜索中稳定获益。不能把多想几步当成免费能力。

思考题

模型变大一定效果更好吗?为什么还要同时讨论数据量、训练计算和质量?

答案

不一定。经验上的 scaling law 通常把参数量、数据量和计算预算放在一起看。若数据不足或重复严重,大模型容易过拟合;数据质量差也会让目标本身不可靠。同一计算预算下,模型规模和数据量之间存在较优配比,不是单方面无限放大参数。

思考题

测试时让模型并行采样多个候选,为什么不一定带来更好结果?

答案

多采样增加计算和延迟,还需要选择、验证或投票机制。若任务没有可靠判别器,错误候选可能被保留;若候选高度相似,额外计算收益有限。测试时计算只有在能稳定提升最终决策时才划算。

思考题

若训练预算固定,参数量增加 4 倍而训练 token 数不变,为什么不一定比同时调整两者更划算?

答案

固定预算下,参数量、数据量和可达损失之间存在经验上的配比。参数很多而数据不足时,模型容易记住或过拟合训练分布,损失下降受到限制;数据多而参数不足时,模型容量又可能不够。Chinchilla 类结论强调的就是在预算内寻找参数与 token 数的较优比例。

预训练状态与并行策略

一次训练 step 至少包含前向、反向和优化器更新。显存里同时存在参数、梯度、优化器状态和激活。前向为了输出损失而保留部分 activation;反向需要它们求导;Adam 等优化器还会保存额外状态。对大模型而言,参数文件的大小只是这笔账的一部分。

这四类对象可以用 7B 参数的 AdamW 训练粗算一遍。BF16 参数约 14GB,同形状 BF16 梯度约 14GB,FP32 主权重约 28GB,一阶动量和二阶动量各约 28GB,模型状态合计约 112GB。activation 另由 batch、序列长度、层数和 checkpointing 决定。检查显存时把这几项分开列,比只报“模型是 7B”更能说明单卡为何放不下,也能说明 ZeRO、FSDP、activation checkpointing 各自减少的是哪一笔。

模型规模超出单卡后,训练框架通常混合多种并行维度:

  • 数据并行把不同 batch 交给模型副本,最后同步梯度;
  • 张量并行把同一层的矩阵乘法切给一组设备;
  • 流水线并行按层切模型,activation 沿 stage 传递;
  • ZeRO/FSDP 分片参数、梯度和优化器状态;
  • sequence/context parallelism 沿长序列切 attention 的工作。

课件按并行维度大致画了并行策略:数据并行沿 batch 维度把工作分给各 GPU,ZeRO/FSDP 再进一步分片显存冗余。

图:并行方式按切什么分类——切数据、切矩阵、切层、分片状态;不同切法对应不同通信模式。

并行度不是越大越好。把矩阵切得更细会降低单卡显存,却提高通信频率;流水线 stage 变多会产生 bubble;数据并行增大后全局 batch 和学习率策略也会变化。训练日志里的 MFU(model FLOPs utilization)只反映计算单元利用率的一面,还要看通信等待、数据加载和故障恢复。

训练框架只是把这些选择实现成不同的默认组合。以 FSDP、Megatron 和 DeepSpeed 为例:FSDP 更贴近 PyTorch 原生生态,善于做参数/梯度/优化器状态分片;Megatron-LM/Core 通常提供成熟的 TP、PP、DP、EP、CP 组合与高性能 kernel;DeepSpeed 覆盖 ZeRO、offload、MoE 等较多场景。它们没有放之四海皆准的胜负关系。模型大小、网络拓扑、团队维护能力和所需并行策略,才是选型的依据。

课件用表格对比 PyTorch FSDP2、Megatron-LM/Core 与 DeepSpeed 三种框架的核心思路与优缺点。

图:三类框架分别是贴近 PyTorch 的分片、成熟的多维并行组合、多样的 ZeRO/offload/MoE 覆盖;选型取决于场景而非优劣。

实际排障时,先把一次 step 切成数据准备、前向、反向、梯度同步、优化器更新几个时间段,再看哪段长、哪张卡在等。看到 GPU 利用率低,可能是数据加载不足,也可能是 collective 卡住、流水线不平衡或显存碎片引起的重算。优化前先测量,才不会把时间花在并不主导的部分。

优化器本身也是一个可省显存、可省算力的变量。AdamW 是最常见的默认项:它对每个参数维护一阶与二阶动量,训练效果好,但多出两份状态、占内存不小。与其在给定优化器下堆并行,不如也考虑换一个更新规则是否划算。

课件对比 AdamW 与 Muon:AdamW 维护一阶/二阶动量,Muon 是面向矩阵的更新,宣称以约一半的训练 FLOPs 达到 AdamW 级 loss。

图:更好的优化器等同一笔免费算力——若新优化器在更少 step 里达到相同 loss,就直接省下整段训练时间。

以 Muon 为代表的矩阵感知优化器利用参数是矩阵的结构做更新,在部分实验中明显降低达到目标 loss 所需的 FLOPs。它是否适用取决于模型与超参,不能想当然替换;但优化器质量确实是训练成本方程里的一个自由变量,和并行策略、精度选择一样值得在动手堆硬件前先尝试。实际判断仍靠同输入、同 loss 目标下的收敛曲线对比。

思考题

从 checkpoint 恢复训练时,为什么不能只看参数张量,还要记录优化器状态、数据位置和并行切分?

答案

训练状态包含参数、梯度、Adam 的一阶二阶动量、学习率调度、随机数状态和数据进度。并行训练还涉及张量如何切到各 rank,恢复时布局不一致会导致张量无法直接加载或语义错位。完整 checkpoint 才能保证继续训练与原轨迹尽可能一致。

思考题

MFU 很高,为什么仍不能说明训练系统已经没有浪费?

答案

MFU 主要衡量计算单元利用率。训练时间还包括通信等待、流水线 bubble、数据加载、checkpoint、重启和同步。某些卡可能空闲而另一些卡高负载,端到端时间仍可能明显超过理想计算时间。

思考题

FSDP 训练一段全连接层时,为什么要先 AllGather 分片参数,计算后又释放完整参数?

答案

分片保存降低常驻显存,但当前计算可能需要这一层的完整权重。AllGather 临时凑出完整张量,前向或反向用完就释放,显存峰值只保留必要层的完整副本。代价是每次使用分片参数都要通信,因此需要通信计算重叠和合理的分块粒度。

高效 Attention 与内存访问

标准 attention 写为

先给 Q、K、V 一个具体读法。query 是当前 token 用来查询其他 token 的向量,key 是每个 token 用来被查询的向量,value 是被选中后实际贡献给输出的内容向量。三者由同一批隐藏向量乘不同投影矩阵得到。注意力分数来自 query 与 key 的点积,分数经 softmax 变成权重,再对 value 加权求和。

Scaled dot-product attention 的计算流程:分数、softmax 与加权求和。

图:Q 与 K 相乘得到相关性分数,softmax 把分数变成权重,最后对 V 加权求和。图源:UvA Deep Learning Tutorials

设单个 head 的形状为 \(Q\in\mathbb R^{S\times d}\)\(K,V\in\mathbb R^{S\times d}\)\(QK^T\)\(S\times S\),第 \((i,j)\) 个元素表示第 \(i\) 个 query 对第 \(j\) 个 key 的未归一化分数。softmax 对每一行独立操作,把 \(S\) 个分数变成和为 1 的权重;再乘 \(V\) 后得到 \(S\times d\)。除以 \(\sqrt d\) 是为了让点积方差不至于随维度增大而过大。

\[ \operatorname{Attention}(Q,K,V)=\operatorname{softmax}\left(\frac{QK^T}{\sqrt d}\right)V. \]

课件回顾 Transformer 的 self-attention:从隐藏向量得到 queries、keys、values,先算相关性再加权选出输出。

图:Q/K/V 来自同一序列的不同投影;attention 让每个位置按相似度聚合其他位置的信息。

若序列长度为 \(S\),中间的 \(QK^T\)\(S\times S\)。长上下文时,直接物化这个矩阵既占显存,又会在 GPU 高带宽内存和片上存储之间搬运大量数据。问题不只在 FLOPs,也在 I/O。

\(S=4096\) 为例,单个 head 的注意力矩阵有 \(4096^2\) 个元素,约 1677 万个;若用 FP32 保存,仅这一层一个 head 就要约 64MB,再乘 batch 数、head 数和层数,规模会迅速失控。乘法和加法的次数仍在,但真正的困难还包括把这些中间元素从 HBM 写回、再读出的时间。

FlashAttention10 的核心做法是把 \(Q,K,V\) 分块,利用寄存器和 shared memory 完成块内计算,避免把完整 attention matrix 写回 HBM。softmax 的分母依赖所有 key,因此它维护块级的运行最大值与归一化量,在数值稳定的条件下合并每个块的结果。输出没有变成近似值,改变的是数据的存取顺序。

分块后的计算顺序可以从矩阵上看。把 Q 沿行切成小块,例如每块 128 个 query;K 和 V 沿 key 维切成对应小块。每个 Q 块依次与每个 K/V 块相乘。对某个 Q 块来说,所有 K 块都贡献同一个 softmax 的分母,因此不能各块单独归一化后直接相加。

online softmax 可以用一个数值例子说明。设某个 query 对前两个 key 的分数是 \([2,0]\),对后两个 key 的分数是 \([1,3]\)。若分别做 softmax 再相加,权重定义就是错的。正确做法是保留当前最大值 \(m\)、分母 \(l=\sum_j e^{s_j-m}\) 和分子 \(u=\sum_j e^{s_j-m}v_j\)。后一块最大值 3 大于旧最大值 2 时,旧块结果要乘 \(e^{2-3}\) 后并入新的累计量。FlashAttention 在寄存器和 shared memory 中维护这些运行量,因此不需要把完整分数矩阵写回 HBM。

小游戏:找出真正的瓶颈

场景是一个长上下文推理服务。模型权重约占 20GB,当前并发请求的 KV cache 共占 60GB,GPU 计算单元利用率只有 35%,日志显示大量时间在 attention kernel 读 HBM。候选方案有四个。

  • A. 增加数据并行副本
  • B. 使用 GQA 或压缩 KV
  • C. 继续提高 batch size
  • D. 只把权重从 BF16 改成 INT4

最优先检查 B。证据指向 KV cache 与访存,batch size 继续增大可能让 KV cache 更早触顶;D 主要减少权重读取,对 60GB 的 KV cache 帮助有限;A 能提高总吞吐,却不解决单副本的上下文容量和访存瓶颈。

课件说明 FlashAttention 是 IO-aware 的 exact attention:把 Q/K/V 按 tile 放进片上 SRAM,算一块、归并一块、丢掉不需要的中间分数。

图:FlashAttention 用分块与在线 softmax 减少 HBM 往返,不改 attention 的数学结果,主要优化的是 I/O 而不是算术量。

GPU 的层级存储决定了这种改写为何有效。HBM 容量大但访问代价高,shared memory 和寄存器容量小却更快。标准实现如果不断把巨大的中间矩阵在 HBM 中读出、写回,算术单元会等待数据;分块后的 kernel 让同一块数据在更近的存储层完成更多计算。这里优化的是 I/O 复杂度,而非把 attention 的数学定义悄悄替换掉。

线性 attention、稀疏 attention、滑动窗口 attention 会改变计算形式或可见范围。它们在代价、表达能力和长序列行为上各有取舍。阅读论文时要分开看:FlashAttention 优化的是 exact attention 的实现;线性或稀疏方法会改变 attention 的计算复杂度或近似方式。

长上下文再继续增大时,context parallelism 会把 token 或 K/V 块分到多张 GPU。Ring Attention 让 K/V 块在设备间循环传递,每张卡依次处理自己的 Q 与到达的 K/V 块;这把单卡峰值内存降下来,但把问题变成计算与环形通信如何重叠。

传输并不必等到所有计算结束。一个 K/V 块参与本轮 attention 后,可以在当前设备继续算后续 FFN 的同时送往下一张卡;另一侧也可以边接收边准备下一块。通信若能被这段计算遮住,端到端时间就不会简单等于计算时间加通信时间。实际能否重叠取决于块大小、网络带宽、kernel 排程和各阶段负载,写代码时仍要从 timeline 验证。

线性 attention 和稀疏 attention 则试图改变随序列长度增长的代价。线性 attention 用特征映射近似或重写相似度,使某些计算可按结合律重排,避免显式构造全部 \(S\times S\) 关系;因果场景中,它可维护随 token 更新的状态。代价是表示能力、训练稳定性和现有推理缓存机制都要重新评估。稀疏 attention 只让一个 query 关注部分 token,固定窗口、块稀疏和动态选 token 是常见做法;动态选择本身也会引入索引和不规则访存的成本。

思考题

FlashAttention 类方法为什么能降低显存占用?它改变的是注意力数学,还是计算和访存组织?

答案

核心是不再把完整 S×S 注意力矩阵作为中间结果落回显存,而是分块计算、在线更新 softmax 分母,并让 tile 留在高速存储中逐步完成加权求和。近似数学上仍是注意力,但改变了计算顺序和访存模式,减少大中间矩阵的读写,也更适合 GPU。

MoE 路由与执行

Transformer block 里有 attention 和 FFN 两块主要计算。attention 让 token 之间交换信息;普通 FFN 则对每个 token 的隐藏向量单独做两次矩阵投影和一次激活。MoE 没有把整个 Transformer 换掉,它只是用很多个参数不同的 FFN 取代原来的一个 FFN。

课件将 Transformer block 中的 FFN 展开为 DeepSeekMoE:router 为每个 token 选出少数 routed experts,结果再合并回原 token。

图:MoE 位于原 FFN 的位置;真正要处理的是 token 到 expert 的不规则映射。

课件进一步画 DeepSeekMoE 的组织:除了被路由选择的 routed expert,还有一组共享专家始终参与,平衡容量与开销。

图:DeepSeekMoE 在 routed expert 之外加入 shared expert,是如何组织专家集合的一种具体取舍。

若一个 token 的隐藏状态为 \(x_t\),router 会为 \(E\) 个 expert 给分并选出 top-\(k\)。被选中的 expert 各自计算一个 FFN 输出,最后按 gate 权重加权:

\[ y_t=\sum_{e\in\operatorname{topk}(x_tW_{\mathrm{router}})}g_{t,e}\,\operatorname{FFN}_e(x_t). \]

这里的 expert 不是人能直接命名的数学专家或代码专家。它们是同构、参数不同的神经网络分支,如何分工由训练得到。MoE 的容量因此可以很大,而单个 token 只激活少数专家,计算量不随总专家数线性增长。

用一个三 token、四 expert、top-2 的例子读这条公式。router 输出每行四个分数:

token A: expert0 0.55, expert1 0.25, expert2 0.15, expert3 0.05
token B: expert0 0.10, expert1 0.50, expert2 0.30, expert3 0.10
token C: expert0 0.20, expert1 0.10, expert2 0.45, expert3 0.25

A 选择 expert0 和 expert1,B 选择 expert1 和 expert2,C 选择 expert2 和 expert0。dispatch 阶段把这些 token 重新排成 expert0 的 [A,C]、expert1 的 [A,B]、expert2 的 [B,C],各 expert 对自己的小矩阵执行 FFN,最后按原 token 位置 scatter 回去并加权相加。系统真正处理的正是这种不规则重排。

从系统角度看,困难不在这个公式本身。router 选出的 token 往往散落在 batch 的不同位置,而同一个 expert 的权重希望服务一小批连续 token。实现通常要先记录 (token_id, expert_id, gate),按 expert 重排 token,完成专家内部的批量 GEMM,再按 token_id scatter 回输出。单机上这是索引、缓冲区和数据布局问题;多卡 expert parallel 中,它会进一步变成 All-to-All。若少数 expert 被频繁选中,计算和通信都会失衡,因此容量控制和负载均衡也进入训练设计。

route、dispatch、专家矩阵乘法和 weighted merge 分别有自己的数据布局与计时边界。token 按 expert 重排后,矩阵单元才会看到足够规则的小批量;重排本身、索引和 scatter 也必须计入总时间。这里的性能判断依赖矩阵形状、低精度算术和内存访问,而不是只看某一条 GEMM 指令。

思考题

top-k 路由和负载均衡目标为什么可能冲突?系统上如何缓解?

答案

top-k 只关心每个 token 选质量最好的 expert,容易造成热门 expert 被过度选中;系统希望各 GPU 的工作量均衡。容量限制、辅助均衡损失、expert 复制或重排调度都在这个矛盾上折中。设计目标要同时看模型质量和吞吐,不能只看路由精度。

思考题

FlashAttention 减少 HBM 读写,为什么这常常比减少 FLOPs 更重要?

答案

attention 的大 \(S\times S\) 中间矩阵写入显存再读回,会消耗大量带宽。分块计算让 tile 留在 shared memory 或寄存器中完成乘加、softmax 累计和输出更新,中间矩阵不必完整写回 HBM。计算总量近似不变,但访存次数显著减少,GPU 空等数据的时间下降。

参数高效微调

什么是量化成低比特:用整数近似浮点权重

深度学习里的每个权重通常是一个 32 位浮点数(FP32),更常用 16 位(FP16/BF16)来省一半内存。量化更进一步:把权重用很少的比特(常见的 8 位 INT8 甚至 4 位 INT4)来近似表示。它的动机很直接——权重从 16 位降到 4 位,存储和搬迁量可以降到约四分之一,模型更容易装进显存,推理时也能少读数据。

但 4 位只能表示很少的不同数值,直接把每个浮点权重四舍五入到最近的 4 位格点会引入误差。所以量化通常配合一个 scale(缩放系数):近似地 \(\text{权重}\approx scale\times(\text{整数} - zero)\)。重点是近似——量化放弃了一部分精度,换来内存与带宽的节省。哪种做法误差可接受,取决于任务对数值波动的容忍度,这正是本节后面 GPTQ、W4A16 等内容在回答的问题。

一句话的直觉:量化好比用更粗的刻度尺量长度,省了存储,代价是测量不够精细;好的算法努力让量出来的结果仍然尽量接近原值。

全量微调要更新所有参数,并保存相应梯度和优化器状态。LoRA11 将一个预训练权重矩阵 \(W_0\) 固定,只学习低秩更新:

\[ W=W_0+\Delta W,\qquad \Delta W=BA, \]

其中 \(A\in\mathbb{R}^{r\times d}\)\(B\in\mathbb{R}^{k\times r}\),秩 \(r\) 远小于原矩阵维度。训练时只保存和更新小矩阵 \(A,B\);部署时可把 \(BA\) 合并进 \(W_0\),也可保留 adapter 以便在同一基座上切换任务。

低秩的含义来自矩阵乘法形状。设原线性层把 \(d\) 维输入变成 \(k\) 维输出,则 \(W_0\)\(k\times d\),全量微调要更新 \(kd\) 个参数。LoRA 令 \(A\)\(r\times d\)\(B\)\(k\times r\),其中 \(r\ll \min(k,d)\)。可训练参数只有 \(r(k+d)\)。若 \(k=d=4096\)\(r=16\),全量参数约 1678 万,LoRA 分支约 13.1 万,比例不到 1%。前向计算也保持清晰,输入 \(x\)\(A\) 得到 \(r\) 维中间向量,乘 \(B\) 得到 \(k\) 维增量,加到 \(W_0x\) 上。

LoRA 冻结基座权重,只训练低秩增量;Q-LoRA 将基座以低比特保存,把显存压力进一步压低。

图:训练 LoRA 时只更新低秩分支,基座权重保持冻结。

课件直接给出 LoRA 的公式:冻结预训练权重 \(W_0\),只学习一个低秩更新 \(\Delta W = BA\),训练时只保存和更新小矩阵。

图:低秩分解 \(BA\) 让可训练参数量大幅下降;部署时把 \(BA\) 合并回 \(W_0\) 或不保留 adapter 均可。

Q-LoRA 再把冻结的基座量化到低比特,以更少显存加载大模型,同时让 LoRA 分支保持可训练精度。量化并不等于所有张量都直接截成相同格式。权重、激活、梯度和 KV cache 对误差的敏感度不同;缩放因子的粒度、异常值、累积精度和硬件指令都会影响最终效果。

思考题

原线性层是 [4096, 4096],LoRA 取秩 \(r=16\)。为什么可训练参数远少于全量微调?

答案

全量微调要更新约 1678 万个权重。LoRA 用 B [4096,16]A [16,4096] 表示低秩增量,可训练参数约 13.1 万,不到 1%。基座权重冻结,训练状态和梯度的显存也相应减少。

思考题

LoRA 把 \(\Delta W\) 写成 \(BA\),训练时冻结 \(W\)。为什么这仍能改变模型行为?

答案

前向输出变成 \(Wx+BAx\)\(W\) 固定,但低秩矩阵 \(B\)\(A\) 参与每次计算,等价于给原线性层增加一个可学习的增量。微调更新的是这个增量,而不是重写整个权重矩阵。参数少、训练和保存成本低,代价是表达能力受秩 \(r\) 限制。

KV Cache 与推理内存

自回归推理分为 prefill 和 decode。prefill 一次处理完整 prompt,计算所有输入 token 的 K/V 并建立 cache;decode 每步生成一个 token,要读取前文 cache、计算新 token 的 Q/K/V,再把新的 K/V 追加进去。

设有 \(L\) 层、\(H_{kv}\) 个 KV head、每个 head 维度为 \(d\)、上下文长度为 \(S\),单请求 KV cache 大小近似随

\[ 2\times L\times S\times H_{kv}\times d \]

增长,其中 2 表示 K 与 V。多并发请求、长上下文和多轮对话会很快填满 GPU HBM。decode 每步算术量并不大,却反复读取越来越长的 cache,常呈现 memory-bound 或 I/O-bound 特征。

代入一个典型规模。\(L=32\)\(H_{kv}=8\)\(d=128\)\(S=1000\)、每个元素用 BF16 的 2 字节保存时,单请求 KV cache 为

\[ 2\times32\times1000\times8\times128\times2=131{,}072{,}000\ \text{bytes}, \]

约 125MiB。100 个并发请求就超过 12GiB。这还没有计入 allocator 元数据、碎片和临时 buffer。decode 每步新计算量主要是当前 token 的投影,但 attention 必须读取全部历史 K/V,因此长上下文服务常被显存带宽限制。

课件的分层 KV cache:GPU HBM 保存最热的块,CPU DRAM 或更低层保存冷数据,容量和带宽形成明显梯度。

图:分层缓存用较慢但更大的存储层容纳冷 KV 块。

分层缓存是其中一种处理法。热请求与正在生成的 token 留在 GPU;暂时不用或已被逐出的块可以转到 CPU DRAM,再在需要时取回。代价是 PCIe/NVLink 传输与调度复杂度,不能假设缓存能放下就等于延迟仍然好。PagedAttention 一类设计把 KV cache 切成固定大小 block,用逻辑块表管理物理块,减少连续大块分配造成的碎片,也便于请求共享前缀。

前缀缓存能复用的前提很严格:从开头到命中位置的 token 序列必须相同。共享系统提示词、相同文档前缀或多人重复查询同一段资料时,收益很高;工具调用不断返回不同结果的 agent 场景里,一处差异就会让后续前缀无法复用。缓存压缩、选择性保留和线性 attention 的固定状态都可能减小内存,却也可能降低这类复用能力。系统设计要把平均命中率、尾延迟和请求类型一起看。

模型结构也能降低 cache。Multi-Query Attention 让多个 query head 共用较少的 KV head;Grouped-Query Attention 是两者间的折中;某些 latent attention 先把 KV 压缩为更小的潜变量。这些选择会影响推理内存、带宽、模型质量和 kernel 形状,正是算法—系统共同取舍的例子。

课件梳理 KV cache 与 attention 架构的协同:从 MHA 到 MQA、GQA,再到 MLA 之类压缩 KV 的变体。

图:共享或压缩 KV head 能显著缩小缓存与带宽,代价是表达能力与实现复杂度——这正是算法与系统协同设计的例子。

这几种变体在缓存字节数上有干净的对照。若每层有 \(h\) 个 query head、分组 \(g\) 个、每 head 维度 \(d_k\),则 MHA 需为每个 head 各存一份 K/V,缓存约 \(2h d_k\);GQA 让一组 query head 共享一份 K/V,约 \(2g d_k\)(MHA 的 \(g/h\));MQA 让全部 query head 共用一个 K/V,约 \(2d_k\)(MHA 的 \(1/h\)),是三者里缓存最小的。

以 32 个 query head、每个 head 64 维为例。MHA 缓存 \(2\times32\times64=4096\) 个元素;GQA 若 4 个 query head 共用一份 KV,缓存 \(2\times8\times64=1024\) 个元素;MQA 缓存 \(2\times1\times64=128\) 个元素。数值上很容易看出为什么 GQA 会成为推理系统的常见折中。

课件给出 MHA、MQA、GQA 与 MLA 的对照:缓存大小、效果与显存占用各有倾斜,MLA 靠低秩压缩拿到接近 MQA 的缓存、接近 MHA 的效果。

图:MHA 效果最好但缓存最大,MQA 缓存最小但效果有损,GQA 是折中;MLA 把 K/V 压缩成共享潜变量,避开缓存小与效果好之间的两难。

MLA(Multi-head Latent Attention)更进一步,不直接共享完整 K/V,而是把 KV 低秩联合压缩成一个较小的潜变量 \(c_t\),推理时只缓存这个潜向量,再在线还原。它的缓存量(如论文中 \(d_c+d_r\))与 head 数无关,却能保留接近 MHA 的表达能力。正因为 KV cache 是长上下文推理的显存与带宽瓶颈,压缩 KV 这件事既是模型架构选择,也是系统能在多长上下文、多大并发下运行的变量。

思考题

batch size 为 1,序列长度 4096,层数 32,head 数 8,head dim 128,K 和 V 均为 FP16。KV Cache 大约是多少字节?

答案

每个 token 每层需要 K 和 V 各 8×128 个元素,共 2×8×128=2048 个 FP16 值。32 层就是 2048×32=65536 个值,每值 2 B,单 token 约 131072 B。4096 个 token 约为 131072×4096=536870912 B,即约 512 MiB。

思考题

MHA、GQA、MQA 的 KV cache 大小为什么不同?

答案

MHA 为每个 query head 保存一份 K/V;GQA 让一组 query head 共享一份 K/V;MQA 让所有 query head 共用一份 K/V。因此同样 head 数下,MQA 缓存最小,MHA 最大,GQA 位于两者之间。

量化推理与服务调度

低比特权重减少容量和带宽压力,校准误差、反量化路径与服务调度决定这部分节省能否转化成端到端收益。

对称量化和非对称量化的区别可以用同一组数看清楚。对称 INT4 通常把整数范围取为 \([-7,7]\),若一组权重最大绝对值是 1.0,scale 为 \(1/7\)。权重 \([-1.0,0.2,0.8]\) 量化为 \([-7,1,6]\),反量化为 \([-1.0,0.143,0.857]\),误差分别为 \([0,0.057,0.057]\)

非对称量化适合分布整体偏移的数据。若取值范围是 \([-1,1]\),整数范围 \([0,15]\),可令 \(s=2/15\)\(z=7.5\),公式为 \(x\approx s(q-z)\)。零点不再必须占据整数 0,因此同样的位宽能覆盖不对称区间。选择哪一种,取决于权重、激活或 KV cache 的分布以及 kernel 是否高效支持 zero-point。

量化计算检验

一组权重为 \([-0.7,0.1,0.7]\),采用对称 INT4,整数范围为 \([-7,7]\),scale 取 \(0.7/7=0.1\)。写出量化整数和反量化结果。

量化整数为 \([-7,1,7]\),反量化结果为 \([-0.7,0.1,0.7]\)。这组数恰好落在格点上,因此没有逐元素误差;若原值是 \(0.15\),会被量化为 2 并还原成 0.2,误差为 0.05。实际权重分布越偏离格点,量化误差越需要靠 scale 粒度、校准和补偿算法控制。

课件说明神经网络量化的本质:把高精度权重与激活映射到更少的离散取值,用 scale 等参数近似还原。

图:量化的核心是把连续范围映射到有限格点;格点越粗,存储越省,误差越大。

量化误差与 GPTQ

权重量化并非只最小化 \(\lVert W-\hat{W}\rVert\)。线性层真正关心输出扰动 \(X(W-\hat{W})\);输入激活在某些方向幅度很大时,即使权重逐元素误差很小,输出也可能明显偏移。GPTQ 用校准样本的激活二阶信息近似这种敏感度,按列/组逐步量化并补偿尚未量化的权重误差。校准集不需要标签,却应覆盖实际 prompt 的激活分布;用完全无关的短文本校准,量化后的困惑度可能恶化。

量化还有一个简单的基线:RTN(round-to-nearest),把每个浮点权重直接四舍五入到最近的格点,不做校准、也不补偿误差。它快、可复现,常作为对比 GPTQ 的什么都没有做起点。对大的 4-bit 权重,RTN 通常会让 NLL12 明显变差,因为单个权重误差受激活方向放大;GPTQ 的价值恰恰在于把这份误差按真实输入分布补偿掉。读 quantize.py 时先看它走的是 RTN 还是带校准的 GPTQ——两者的输出质量不可直接比较,速度也不一样。

group-wise scale 是质量、元数据和 kernel 复杂度的折中。组越小,scale 更能贴合权重分布,但每组都要读取 scale/zero-point,量化元数据比例升高,fused kernel 的访存也更碎。对称 INT4 通常把 4-bit 值解释为有符号范围并使用一个 scale;非对称方案额外使用 zero-point。无论编码方式如何,反量化的语义都应写清:w ≈ scale * (q - zero),以及 scale 按输出通道、输入组还是张量共享。

NLL 用于检验推理是否仍是同一概率模型。对 token 序列 \(x_1,\ldots,x_T\),忽略不计分的前缀后,

\[ \mathrm{NLL}=-\sum_t\log p_\theta(x_t\mid x_{<t}). \]

一个最小例子可以说明 NLL 的敏感性。目标 token 的概率从 0.9 变成 0.8,单点 NLL 从 0.105 增到 0.223,变化并不醒目;若第 1000 个 token 的概率都发生这种下降,总 NLL 会增加约 118。逐 token 概率误差会累积,因此固定测试集上的 NLL 比观察一段生成文本更适合检查量化是否破坏了模型分布。

比较 W4A16 与高精度模型时必须使用完全相同的 tokenizer、样本、mask 和 logit 处理;只比较生成文本的看起来像不像无法发现小而系统性的概率偏差。端到端计时也应说明 tokenizer、KV cache 预热与采样策略是否包含。

位宽、缩放与累加路径

推理量化的直接收益是减少权重和激活的存储与搬运,并让硬件使用更高吞吐的低精度矩阵路径。低比特格式往往需要按 tensor、channel 或小 block 设置 scale;scale 的粒度越细,误差通常越小,但元数据和计算也更多。不能只看 4-bit 这个标签,还要看格式、缩放和校准方式。

量化可粗略理解为把浮点值 \(x\) 映射到整数或低位浮点表示 \(q\),运行时再借助 scale 还原近似值:\(x\approx s(q-z)\)\(s\) 决定步长,\(z\) 在非对称量化中处理零点。若整个张量只共用一个 scale,少数异常大值会浪费大部分可表示范围;按更小 block 设 scale 会改善误差,但会增加 scale 的读取和 kernel 处理。权重、激活和 KV cache 的数值分布不同,通常也不会使用完全相同的方案。

量化还要区分训练和推理。推理时模型权重已经固定,常可先做校准再转换格式;训练中的权重与激活持续变化,低精度算子、梯度累积和主权重精度都要参与设计。低位宽的收益只有在硬件有相应矩阵指令、数据布局与 kernel 能把它用起来时才会兑现。文件体积缩小,不自动等于吞吐按同一比例提高。

在 W8A8 这类路径中,权重和激活以 int8 保存,点积通常以更宽的 int32 累加,再依靠 scale 回到浮点范围。这样做减少了搬运字节数,也可能使用硬件的整数矩阵乘加单元;同时,scale、量化误差、tile 布局和较小的专家 batch 仍会决定实际速度。它和 MoE 的关系很直接:专家 FFN 是最重的规则计算部分,量化与矩阵 kernel 往往就落在那里,路由和合并则仍需单独处理。

融合反量化与 GEMM

W4A16 把权重压成 INT4,激活仍以 FP16/BF16 等较高精度参与计算。实际 kernel 通常按 group 保存 scale 和 zero point,把两个 INT4 值打包进一个字节;运行时再解包、转换到计算精度并乘上 scale。若先把所有权重反量化成 fp16 临时矩阵再调用 GEMM,就会额外写出并读回一个完整高精度权重张量。

fused dequant-GEMM 在 tile 被搬入寄存器或 shared memory 时完成解包和缩放,让结果直接参与矩阵乘加,避免这个全局中间量。它仍受 tensor core 所需布局、tile 对齐、group 边界和 scale 加载影响;当 batch 很小或矩阵很窄,launch、访存和 reduction 开销可能盖过理论算力优势。看到实现中的 packunpackscalesgroup_size,可以顺着检查数据在哪个维度分组、谁负责读 scale,以及反量化后的值是否离开计算单元。

端到端服务调度

课件对比低精度格式的缩放粒度,并将它放在实际预训练和推理吞吐的语境中。

图:低比特格式除了位宽不同,缩放因子的粒度也不同。

服务端要同时处理吞吐和延迟。continuous batching 允许请求在不同时间加入批次,提高设备利用率;prefill 与 decode 的资源特征不同,常被拆开调度;speculative decoding13 用较小模型或草稿分支一次提出多个 token,再由目标模型验证,以额外计算换取更少的串行 decode 轮数。是否有效取决于接受率、模型大小和请求长度。

假设草稿模型一次提出四个 token,目标模型一次前向验证这四个位置。若前三个 token 都符合目标模型分布,第四个被拒绝,则这一轮接受三个 token;传统 decode 要三次串行目标模型前向。若草稿质量很差,多数候选被拒绝,目标模型既要验证失败候选又要回退,额外计算可能超过节省。接受率、草稿长度和并行验证的实现共同决定收益。

课件画出 speculative decoding:小模型 draft 一次提出多个候选 token,目标模型并行验证,接受被认可的,从而减少串行 decode 轮数。

图:用便宜草稿模型生成一批候选,再由目标模型一次性验证;以额外计算换更少的串行生成步数。

大模型在有限显存上运行时,GPTQ 将权重压到 INT4,NLL 用来确认概率质量没有越过阈值。优化对象不只是一层线性算子,还包括反量化与 GEMM、KV cache 分配、prefill/decode 调度、CPU/GPU offload 与 profiler 中真正占时的阶段。显存、省下的字节和吞吐相互制约,必须在完整推理负载上比较。

量化正确性不是能生成一句通顺文本

输出文本受采样随机性影响,难以比较。固定 token 序列、tokenizer、mask 与 logit 处理后计算 NLL,才能判断误差来自量化还是输入前处理。融合 kernel 改了权重布局时,也要先确认 dequant 后的数值语义未变。

思考题

为什么量化误差常常不只是“把浮点改成整数”这一步?计算路径中还有哪些误差来源?

答案

权重和激活要先用 scale 映射到低比特范围,超出表示范围的值会被截断或饱和。整数矩阵乘后的累加类型、反量化顺序、scale 精度、激活异常值和 GEMM 融合方式都会影响结果。若逐层误差被后续非线性放大,最终输出差异可能明显,因此要做端到端校验。

思考题

INT4 权重文件变小了,为什么推理速度不一定按 4 倍提升?

答案

速度取决于反量化、解包、scale 读取、矩阵形状、tensor core 利用率、KV cache 和调度。若 batch 很小或 kernel 需要频繁解包,固定开销可能占主导。文件大小只反映存储,不自动等于端到端吞吐。

思考题

speculative decoding 的草稿模型接受率很低时,为什么可能比普通 decode 更慢?

答案

目标模型仍要验证整批草稿 token,还会为被拒绝的位置做回退。接受率低意味着多数额外前向没有换来有效输出,同时草稿模型本身的生成也有开销。收益取决于每次接受的 token 数、草稿长度和并行验证实现,不是固定加速比。

Agent 与 Harness

上面讲的训练、推理、显存和调度,最终都要被一段干活的代码编排起来。课程把这类编排概括成一个公式:Agent = Model + Harness14

  • Model 是能理解与生成的神经网络,负责会说什么;
  • Harness 是控制 agent 如何工作的代码——如何构造 prompt、发起工具调用、管理子 agent、控制流程、维护 memory 与工作流。同一个模型换上不同 harness,就变成不同的 agent 行为。

课件画出 Agent Loop 并把 Agent 拆成 Model 与 Harness:模型负责能力,harness 负责把能力编排成多轮工具调用与决策流程。

图:模型是会推理的脑子,harness 是怎么用这个脑子的系统代码;两者分离后,能力升级与流程改造可各自进行。

把自动化做成 agent,重点在于系统地编排环境、工具和记忆,单靠加强模型推理并不够。以 coding agent 为例,harness 决定何时查代码、运行测试、读报错日志、该怎么重试。因为 harness 完全由我们自己控制,针对它的优化走白盒、便宜且快;改动模型则是要重训或微调,慢且贵。于是在这里,系统层(harness)是多数迭代成本低、收益快的一侧——这也是算法—系统协同设计在 agent 场景下的体现。

环境、工具、记忆和状态机是 harness 的四个基本部件。环境提供观察结果,例如文件内容或命令输出;工具是模型可以调用的函数,例如读文件、搜索代码、运行测试;记忆保存跨轮可用的信息;控制流决定什么时候停止、重试或请求人工确认。模型输出的只是文本或结构化调用意图,真正执行动作、检查返回值和处理异常的是 harness。

多模态模型又把输入扩展为图像、视频、音频或动作。视觉编码器把图像变成视觉 token,再与文本 token 在语言模型中融合;生成任务可能使用自回归、扩散或两者混合的架构。图像 token 数、视频帧数和跨模态 attention 都会直接放大上下文与推理成本。

性能分析先把瓶颈落到算力、显存、带宽、通信或请求调度中的某一项,再决定改模型、kernel、精度还是系统。笼统地说模型跑得慢无法导出可验证的优化实验。

思考题

同一个大模型接上不同 harness,为什么表现可能差别很大?

答案

harness 决定如何组织 prompt、调用工具、保存状态、处理错误和停止任务。模型只输出文本或调用意图,真正执行动作和检查结果的是外围代码。工具选择、上下文管理和重试策略不同,任务成功率也会不同。


  1. infrastructure,基础设施。这里指训练集群、kernel、并行策略、显存管理、编译器、推理引擎和服务调度。 

  2. GPU 语境中指一段在 GPU 上执行的函数,例如矩阵乘法或 softmax;与操作系统 kernel 不同。 

  3. 推理时保存的历史 key/value 向量。 

  4. Mixture of Experts,混合专家模型。router 为每个 token 选择少数 expert 分支。 

  5. Transformer 中让不同 token 互相交换信息的算子。 

  6. 量化。把高精度浮点数近似成低位整数。 

  7. 能根据环境、记忆和工具调用持续决策的程序系统。 

  8. grouped-query attention、multi-query attention 和 multi-head latent attention。不同形式的注意力缓存压缩技术。 

  9. scaling law,规模定律。描述模型性能随参数、数据和算力增长的统计规律。 

  10. 一种通过分块计算和 online softmax 降低访存量的注意力实现。 

  11. low-rank adaptation,低秩适配。冻结原权重,只训练低秩增量矩阵。 

  12. negative log-likelihood,负对数似然,语言模型常用的逐 token 损失。 

  13. 投机解码。小模型或草稿分支提出候选 token,目标模型一次性验证。 

  14. agent 外围的执行框架,负责工具调用、状态维护、重试和停止条件。 

有用的话请给我个 star => Stars 本站总浏览