[{"content":" Attention 普遍被认为是语言模型架构的基础，然而，从过去的机器翻译的 Encoder-Decoder 架构到如今的大语言模型，Attention 也出现了大量的变体，用于适配更高效，更 scaleble 的模型。\n在今天的内容中，我们将重点介绍常见的 Attention 变体定式，从数学原理到一线的技术报告。\nSimple Attentions and MHA TLDR: 在 Attention is all you need 中，作者提出了最基本的注意力机制和多头注意力的版本。\n我们首先看最基本的 Attention 长什么样子, 从最经典的 Attention is all you need 1 开始讲起:\n图片左侧救赎最基本的 Attention，他的数学表达形式如下: $$\\text{Attention}(Q, K, V) = \\text{softmax}\\left(\\frac{QK^T}{\\sqrt{d_k}}\\right)V$$ $Q \\in \\mathbb{R}^{n \\times d_k}$：查询矩阵（Query）\n$K \\in \\mathbb{R}^{m \\times d_k}$：键矩阵（Key）\n$V \\in \\mathbb{R}^{m \\times d_v}$：值矩阵（Value）\n$d_k$：键/查询的向量维度，$\\sqrt{d_k}$ 用于防止点积数值过大导致 Softmax 梯度消失\n在实际自回归训练中，模型训练不可以看到未来的词，因此需要加上一个掩码矩阵 $$\\text{AttentionScore} = \\text{softmax}\\left(\\frac{QK^T}{\\sqrt{d_k}} + M\\right)$$ 一方面可以避免自回归的时候，未来的词被关注（此时对应的值被掩码成负无穷，softmax 后就是 0），另一方面可以高效的处理被 padding 的词，对应一个 batch 中长短不一的样本。\nMulti-Head Attention (MHA) 在 Attention is all you need 的原文中，作者就提出了 MHA（见上图）\nMHA 通过将 $Q, K, V$ 投影到多个不同的低维子空间并行计算 Attention，再将结果拼接融合：\n$$\\text{MHA}(Q, K, V) = \\text{Concat}(\\text{head}_1, \\dots, \\text{head}_h)W^O$$$$\\text{其中 } \\text{head}_i = \\text{Attention}(QW_i^Q, KW_i^K, VW_i^V)$$ $h$：注意力头的数量 $W_i^Q \\in \\mathbb{R}^{d_{model} \\times d_k}$、$W_i^K \\in \\mathbb{R}^{d_{model} \\times d_k}$、$W_i^V \\in \\mathbb{R}^{d_{model} \\times d_v}$：第 $i$ 个头的投影权重矩阵 $W^O \\in \\mathbb{R}^{h d_v \\times d_{model}}$：多头拼接后的输出线性变换矩阵（通常设 $h d_k = h d_v = d_{model}$） 具体来说，MHA 进行了一个 低维度子空间压缩 的操作，具体参数：\n$h = 8$, $d_k = d_v = d_{\\text{model}}/h = 64$ Decoding-Stage Optimization for Attentions TLDR: 原始版本的 Transformer 存在 FLOPS 时间复杂度高 \u0026amp;\u0026amp; KV 缓存占用率高的问题，后续的 MQA，GQA，MLA 都是在 MHA 的基础上添加各种组件，尝试在不导致性能下降的前提下，从架构上进行KV 缓存的压缩。\n在大模型自回归生成时，主要包含两个阶段：\nPrefill：处理输入提示词（Prompt）。这个阶段是计算密集型（Compute-bound），大量并行矩阵乘法主要消耗 GPU 的算力（Tensor Core） Decode：逐个生成新的 Token（自回归生成）。这个阶段是访存密集型（Memory-bandwidth-bound）。每生成一个新 Token，GPU 都需要把之前所有历史 Token 的 KV 缓存（Key-Value Cache） 从显存（HBM）加载到计算单元中，这导致显存带宽成为巨大瓶颈。 不同的 Attention 改进都是在 Prefill 或者 Decode 阶段进行加速优化的，例如：\nMQA, GQA, MLA 等方法通过减少 KV-Cache 利用率，提升模型 decoding 的速度 Sparse Attention 通过稀疏注意力机制，加速 Prefiling 的速度 MQA (Multi-Query Attention) and GQA (Group-Query Attention) Complexity and KV-Cache 在介绍新的 Attention 之前，我们需要计算 Multi-Head Attention 的时间复杂度：\n考虑对于长度为 $N$、特征维度为 $D$ ($d_{model}$) 的输入序列 $X \\in \\mathbb{R}^{N \\times D}$，他会经过如下的计算过程：\n线性投影得到 QKV 矩阵 每一个矩阵的维度： $(N \\times D) \\times (D \\times D)$ 的计算量为：$O(N \\cdot D^2)$ -\u0026gt; $6ND^2$ QK 矩阵相乘 $(N \\times D) \\times (D \\times N)$: $O(N^2 \\cdot D)$ Softmax 操作不是主要耗时项 一个 $(N \\times N)$ 的矩阵 和 $(N \\times D)$ 的矩阵相乘 $O(N^2 \\cdot D)$ 因此，单头注意力的时间复杂度对 N 和 D 都是平方 scale 的，但是因为对 D 来说，往往是一个固定不动的超参数，因此我们可以说，单头注意力的时间复杂度为 $O(N^2)$\n对于维度为 $N \\times D$ 的矩阵与维度为 $D \\times D$ 的矩阵相乘，计算所需的乘法和加法运算次数如下： 乘法运算次数：$N \\times D^2$ 次 加法运算次数：$N \\times D \\times (D - 1)$ 次 总浮点运算次数（FLOPs）：$2ND^2 - ND$ 次\n多头注意力的本质就是 Attention 的计算在低维子空间中完成，不会带来复杂度级别的额外运算开销，因此仍然是 $O(N^2)$ 的时间复杂度。\n这显然是一种无法忍受的开销运算，在自回归的每一次运算过程中，模型都需要忍受 $O(N^2)$ 的运算开销，因此，出现了一种 空间换取时间 的做法: KV-Cache\nKV-Cache 是一种缓存技术，其核心在于在自回归生成过程中，缓存关键的历史信息，使得模型只需要针对新输入的 input 向量进行 attention-score 的计算：\n输入新向量 模型的输入是一个向量 $x_N$，进行向量和矩阵的乘法 $q_N = x_N \\cdot W_Q$ 对应生成额外的 $k_N$, $v_N$ 向量 更新 KV-Cache $$K_{1:N} = \\text{Concat}\\Big(K_{1:N-1}, \\, k_N\\Big) \\quad \\in \\mathbb{R}^{N \\times D}$$ $$V_{1:N} = \\text{Concat}\\Big(V_{1:N-1}, \\, v_N\\Big) \\quad \\in \\mathbb{R}^{N \\times D}$$ 计算注意力权重时，只需要额外计算 当前新 token 对历史 token 的注意力权重，仍然是一个向量乘矩阵的形式 $$\\text{Score}_N = \\frac{q_N \\cdot (K_{1:N})^T}{\\sqrt{d_k}} \\quad \\in \\mathbb{R}^{1 \\times N}$$ $$\\text{Attn\\_Weight}_N = \\text{Softmax}(\\text{Score}_N) \\quad \\in \\mathbb{R}^{1 \\times N}$$ $$\\text{Output}_N = \\text{Attn\\_Weight}_N \\cdot V_{1:N} \\quad \\in \\mathbb{R}^{1 \\times D}$$ 因此，对于一个长度为 $D \\times N$ 的输入矩阵，如果使用 KV-Cache 的方式，可以保证全局的复杂度仍然为 $O(N^2)$，但是代价是需要缓存 $K_{1:N-1}$ 和 $V_{1:N-1}$ 两个大矩阵 ($\\mathbb{R}^{N \\times D}$)\nMQA and GQA 显然，如此大的 KV-Cache 和计算复杂度仍然是不可以接受的，因此，一些面向 加速优化 的 Attention 改进开始出现，或者说，加速优化的架构改进仍然是当前 Attention 优化的最关键的 Motivation 之一。\nMQA 就是在这个基础上的改进，在 MHA 中，Q, K, V 矩阵会被投影到不同的低维子空间中，因此，每一个子空间下的 KV 矩阵都要被缓存，MQA 的改进是 只投影不同的Query 到子空间，KV 缓存共用相同的部分，可以极大的加速缓存利用效率。\nGQA 仍然是在这个基础上做的改进，引入 Grouped Query，可以实现性能和缓存效率的权衡。2\nMLA (MultiHead Latent Attentions) DeepSeek-V2 提出了一种新的 Attention 机制 3, 叫做 Multihead Latent Attention。\n我们还是从 KV-Cache 计算和 FLOPS 复杂度讲起，在标准的多头注意力缓存中，需要 KV 缓存一个庞大的 K 和 V 矩阵 ($\\mathbb{R}^{N \\times D}$)：\n首先，矩阵大小和输入数据的长度 $N$ 成正比，即随着输入 sequence 的增大，缓存的矩阵大小也会同比增大。 其次，$D$ 作为特征维度，往往是一个非常高维的数字，动辄几千或者几万。 因此，DeepSeek 希望从 Hidden Dimension 出发，尝试在不像 MQA 和 GQA 的基础上，减少 KV-Cache 的比例。\n从做法上，MLA 会将输入的 hidden dim 首先压缩到一个非常小的向量维度上，再升维度进行 attention 操作，从而使得需要被缓存的 KV 矩阵的 size 非常小：\n$\\mathbf{c}_t^Q = W^{DQ}\\mathbf{h}_t$: 给定高维空间下的输入向量 $h_t$, 向量左乘一个降维矩阵，得到 $c^Q$ $[\\mathbf{q}_{t,1}^C; \\mathbf{q}_{t,2}^C; \\dots; \\mathbf{q}_{t,n_h}^C] = \\mathbf{q}_t^C = W^{UQ}\\mathbf{c}_t^Q$: 将得到的 $\\mathbf{c}_t^Q$ 进行升维和多头切分 可以对比一下，如果把两个向量合并起来，就是 $[\\mathbf{q}_{t,1}^C; \\mathbf{q}_{t,2}^C; \\dots; \\mathbf{q}_{t,n_h}^C] = \\mathbf{q}_t^C = W^{UQ}\\mathbf{c}_t^Q = W^{UQ} W^{DQ} \\mathbf{h}_t$，本质上等于多头注意力中的 $\\mathbf{q}_{t,i} = W_i^Q \\mathbf{h}_t$ 于此同时，会做 RoPE 的旋转位置编码: $[\\mathbf{q}_{t,1}^R; \\mathbf{q}_{t,2}^R; \\dots; \\mathbf{q}_{t,n_h}^R] = \\mathbf{q}_t^R = \\text{RoPE}(W^{QR}\\mathbf{c}_t^Q)$ 最终，两部分会 concat 成完整的拼接后的 Query 向量 $\\mathbf{q}_{t,i} = [\\mathbf{q}_{t,i}^C; \\mathbf{q}_{t,i}^R]$ 同样的，对于 KV 矩阵，也需要进行降维操作： $\\mathbf{c}_t^{KV} = W^{DKV}\\mathbf{h}_t$ 作为低秩压缩的向量，相比于 hidden dim 压缩了很多的维度 $[\\mathbf{k}_{t,1}^C; \\mathbf{k}_{t,2}^C; \\dots; \\mathbf{k}_{t,n_h}^C] = \\mathbf{k}_t^C = W^{UK}\\mathbf{c}_t^{KV}$ 还原升维操作 共享 Rope: $\\mathbf{k}_t^R = \\text{RoPE}(W^{KR}\\mathbf{h}_t)$ 区别于 Q 矩阵的 Rope 编码是针对不同的 $W^{QR}\\mathbf{c}_t^Q$ 进行的（维度是高维度 \u0026amp;\u0026amp; 切分），K 矩阵的 Rope 编码是经过 $W^{KR}$ 这个矩阵进行映射的，所有 k 向量共享相同的 rope 编码 $\\mathbf{k}_{t,i} = [\\mathbf{k}_{t,i}^C; \\mathbf{k}_t^R]$ 对 V 矩阵同理： $[\\mathbf{v}_{t,1}^C; \\mathbf{v}_{t,2}^C; \\dots; \\mathbf{v}_{t,n_h}^C] = \\mathbf{v}_t^C = W^{UV}\\mathbf{c}_t^{KV}$ 对于上述的算法描述，之需要缓存的部分是 $c^{KV}_{t}$ 和 ${k}_t^R$, 例如对于第 $i$ 个多头, $\\mathbf{q}_{t,i}^C$ 需要和之前的每一个 $\\mathbf{k}_{j,i}^C (j \\le i)$ 进行点积计算，根据矩阵乘法结合律：$\\mathbf{q}^T \\mathbf{k}^C = \\mathbf{q}^T (W^{UK} \\mathbf{c}^{KV}) = (\\mathbf{q}^T W^{UK}) \\mathbf{c}^{KV} = ((\\mathbf{c}_t^Q)^T (W^{UQ})^T W^{UK}) \\mathbf{c}^{KV}$, 两个矩阵可以吸收成一个矩阵乘法，加速运算。\n最终，对于 attention score 的输出：$\\mathbf{o}_{t,i} = \\sum_{j=1}^{t} \\text{Softmax}_j \\left( \\frac{\\mathbf{q}_{t,i}^T \\mathbf{k}_{j,i}}{\\sqrt{d_h^C + d_h^R}} \\right) \\mathbf{v}_{j,i}^C$， $W^{UV}$ 矩阵可以和 $W^O$ 矩阵吸收，保证 MLA 在极大减少缓存占用的同时，提升 模型推理的速度。\n相当于提取出来 $W^{UV}$ 矩阵出来，让尽可能多的数学等价运算可以在低维空间中进行。\nSparse Attention 接下来，我们来讲一类非常特殊的 attention 加速优化，叫做 sparse attention。\n传统的 Self-Attention 需要让序列中的每一个 Token 都与序列中的所有 Token 计算相关性（即 $N \\times N$ 的全连接注意力矩阵），而 Sparse Attention 限制了注意力关系的连接密度，只让每个 Token 关注特定范围或规则的 Token。\n因此，在这类优化下，每一个 token 的计算，不需要关注之前所有时间步上的 token，因此计算复杂度可以从 $O(N^2)$ 降低到 $O(N \\log N)$ 甚至 $O(N)$ 的复杂度。\n可以看见，Sparse Attention 在做减法，语义信息肯定没有 Full Self-Attention 丰富，因此 Sparse Attention 设计的好坏就是看在减少计算复杂度的前提下，能否尽可能保持模型语义信息的不丢失。\nTradition Sparse Attention Sliding Window Attention (SWA): 每一个 token 只能看到之前的 $K$ 个 token 的上下文，而不是全部的上下文 这样就保证，KV 缓存和单个 token 的计算量不会随着总上下文长度的增大而增大，因此可以保证整体 Attention 是线性的 Attention! 同样，在 KV Cache 存储的过程中，被缓存的矩阵的旧行会被不断的替换，采取一种轮转的策略 单个的 Sliding Window Attention 无法处理长上下文的输入，但是可以类似卷积神经网络的做法，堆叠很多层，这样就可以容纳很多的信息。（提升感受野的方式） Strided / Dilated Attention（步长/膨胀注意力） 原理：类似于 CNN 中的膨胀卷积（Dilated Convolution）。Token 不再只看紧挨着的邻居，而是隔几个 Token 采样一个（例如隔 $k$ 个 Token 取一次 KV）。 作用：用同样的窗口大小 $W$，获得了成倍放大（$k \\times W$）的跨度覆盖，能够以较低成本捕捉更远距离的周期性或大颗粒度信息。代表模型如 Sparse Transformer。 Block-Sparse Attention（分块稀疏注意力） 原理：将整个 $N \\times N$ 的 Attention 矩阵切分为若干个固定的 $B \\times B$ 小方块（Block），只在特定位置的方块内计算全 Attention，其余方块直接 Mask 掉。 优势：对 GPU 硬件极度友好。在 Tensor Core 上，处理连续的矩阵块效率远高于分散的稀疏点阵（如 OpenAI 早期开源的 Block Sparse GPU Kernels）。 LongFormer LongFormer 4 是一种经典的 Sparse Attention 的工作，他主要实现了 SWA, Strided Attention 和 Global Attention 等等的组合:\nSliding Window Attention Dilated Sliding Window: 膨胀滑动窗口，每隔 d 个 token 跳跃，可以极大的提升感受野 Global Attention: 对于一些全局的信息，所有的 attention 计算都要包含，且这些 token 的计算会和全局的所有 token 进行计算，同时，Global Attention 使用了不同的 QKV Projection Matrix，保留了更丰富的全局信息。 Streaming LLMs Streaming LLMs 5\nAttention Sink 作者在研究 Window Attention 的时候发现了很多退化现象：\n一旦 Window Attention 超过了 KV-Cache Size，前面的一开始的 token 会被丢弃，会出现 PPL 的快速上升 无论是 Dense Attention 还是 Window Attention，PPL 都会在超过 pre-train length 的时候出现陡增的情况 如果 SWA 不实现 KV-Cache 的复用，而是进行一种 Recomputation，模型的 PPL 可以获得有效的降低，但是此时运行的时间复杂度会变大 Based on the above insights, we propose StreamingLLM, a simple and efficient framework that enables LLMs trained with a finite attention window to work on text of infinite length without finetuning.\nAttention Sink 是研究者在发现预训练模型 Attention 分配不均匀的过程中，呈现的一种 Attention 分布的现象：在一般情况下，模型会根据语义相关性进行 Attention Score 的分配然后进行归一化，但是，模型会将前几个 token 当作 “垃圾桶”，即残留的 attention 分数会 sink 到全面的几个 token 中。\nAttention Sink 现象可以很好的解释为什么我们不可以采用轮转 SWA 的方式进行 KV Cache 阶段，因为大量的 Attention Sink 会被丢弃（在长上下文中） 因此，Streaming LLM 添加了 Attention Sink 的 token，保证这些 token 始终被计算在 attention 里面（相当于一种全局注意力），可以保证 Attention Sink 的分布不会被破坏。\n于此同时，Attention Sink 还可以被利用在训练中，即在训练中引入 sink-token，可以在预训练中充当预训练稳定器的作用。同样，也可以使用 SoftMax-off-by-On 等技巧，来稳定 Attention 计算过程中的 sink 现象。\n以上所讲的都是一些经典的 Sparse Attention 的文章，下面我们将介绍 DeepSeek 系列的 NSA 和 DSA，了解最前沿的 Sparse Attention 是如何构建的。\nNSA (Native Sparse Attention) NSA 6 是 DeepSeek 在 2025年年初提出的一种相对前沿的稀疏注意力方法。Sparse Attention 应该在训练时引入，而不是一种简单的 training-free 的方法（这也是训推一致性的一种体现）\n现有的 training-free 方法会导致模型分数降低，因为 pre-train 阶段，模型是通过 full-attention 来进行计算的 但是将现有的稀疏注意力方法引入训练中，会导致 flash-attention 等优化无法被 apply，导致训练效率下降 同时，论文也强调了 Arithmetic Intensity 这个概念，代表计算操作数和内存访问数的比例，如果太高，则 FLOPS 受限，如果太低，则内存通信受限。\n在 DSA 中，Attention 由下面三个 Attention 进行加权得到：\n$$\\text{Attention}(q, K, V) = g_c \\cdot \\text{Attn}(q, \\tilde{K}_c, \\tilde{V}_c) + g_s \\cdot \\text{Attn}(q, K_s, V_s) + g_w \\cdot \\text{Attn}(q, K_w, V_w)$$ $\\tilde{K}_c, \\tilde{V}_c$ 代表经过压缩（Compression）后的全局键值对。 $K_s, V_s$ 代表通过动态选择（Selection）挑选出的关键 Token 块。 $K_w, V_w$ 代表滑动窗口（Sliding Window）内的局部键值对。 $g_c, g_s, g_w$ 是由门控机制（Gate）动态计算出的权重系数，用于平衡三个分支的贡献。 接下来，我们详细讲解公式中的 3 个 compression：\nToken Compression（按照 Block 分块进行注意力的 Compression，有点类似于卷积操作） 对于 $:t$ 的全部 token，会按照 $l$ 作为分块的长度进行 blocking 每一次 compress 的压缩范围是 $[id + 1, id + l]$, 这里设置 $l \u003e d$ 可以保证压缩区间之间存在重叠，避免边缘信息被裁减。 $\\varphi$ 是一个可学习的 MLP（多层感知机），并且带有块内位置编码（intra-block position encoding）。它的作用是把一个包含 $l$ 个 Token 的局部 Key 块，通过神经网络映射并融合成单个压缩 Key 向量。 输入维度 $[l, d_k]$, 输出维度 $[d_k]$ (不考虑多头和 batch size 的话) $$\\tilde{K}_t^{\\text{cmp}} = f_K^{\\text{cmp}}(\\mathbf{k}_{:t}) = \\left\\{ \\varphi(\\mathbf{k}_{id+1:id+l}) \\middle\\vert{} 0 \\leqslant i \\leqslant \\left\\lfloor \\frac{t-l}{d} \\right\\rfloor \\right\\}$$ Token Selection 在 Selection 过程中，依然是 block-based selection 来加速 GPU 硬件的加速 假设 $l'$ 是 token selection 的分块长度，$l$ 是 token compression 的分块长度，$d$ 是原始分块的长度，且 $d$ 是 $l$ 和 $l'$ 的因数。 $p_t^{cmp} \\in \\mathbb{R}^{\\left\\lfloor \\frac{t-l}{d} \\right\\rfloor}$ 是 compress 后形成的注意力得分，注意因为我们分了 block，所以这个矩阵的维度是除以了 block-size $d$ 的 (但是单次 compress 的长度大于 $d$) Selection Block 会做一个 Pooling 的操作 $$\\mathbf{p}_t^{\\text{slc}}[j] = \\sum_{m=0}^{\\frac{l'}{d}-1} \\sum_{n=0}^{\\frac{l}{d}-1} \\mathbf{p}_t^{\\text{cmp}}\\left[ \\frac{l'}{d}j - m - n \\right]$$ 注意，selection block 本身就包含很多 block，但是 selection block 本身是不重叠的 对于多头注意力来说，例如 MQA 和 GQA，往往涉及共享 KV-Cache 的优化，如果多头之间涉及注意力的共享，那需要对 selection 对 kv-cache 进行一次head 之间的合并，避免资源碎片。 最终，会做 top-n 的 selection，将这部分 block 的 kv 不压缩，直接进入 attention 计算 Sliding Window Attention 正常的操作 于此同时，NSA 还针对 Flash Attention 等进行了大量工程上的加速优化，这一部分将会和 Flash Attention 一起整理呈现。\nDSA (DeepSeek Sparse Attention, DeepSeek-V3.2) Compared with DeepSeek-V3.1-Terminus, the last version of DeepSeek-V3.1, the only architectural modification of DeepSeek-V3.2 is the introduction of DeepSeek Sparse Attention (DSA) through continued training.\nDSA 7 是 DeepSeek-V3.2 引入的稀疏注意力。首先，我们来看基础的 MLA 版本和 MQA 和 MLA 结合的版本，唯一的差异就是降维的 $c_t^{KV}$ 不需要完成重新通过一个投影矩阵投影到 K 和 V 的高维向量后再切分为多头。\nMLA 的 MQA 变体不会引入新的 KV-Cache 计算，因为 KV-Cache 计算已经在 $c_t^{KV}$ 中完成了。\n我们来看基于 MQA 版本的 MLA 实现，对 q 的处理（升维降维 和 rope 处理）保持完全一致。关键架构改进在于：\nLightning Indexer： 首先通过轻量级的低维投影或计算，对每个查询（Query）和所有历史键（Key）快速打分，评估它们之间的相关性。 这一步开销很小，类似于“粗筛”，迅速锁定哪些历史片段可能是相关的。 Top-k 动态选择与稀疏计算： 接着，根据索引器的评分，为每个查询动态挑出最重要、分数最高的 $k$ 个 Token。（类似于一个 KV 压缩的部分） 我们来看索引器，给定计算查询词元 $\\mathbf{h}_t$ 与历史词元 $\\mathbf{h}_s$ ，Indexer 打分的公式是： $$I_{t,s} = \\sum_{j=1}^{H^I} w_{t,j}^I \\cdot \\text{ReLU}(\\mathbf{q}_{t,j}^I \\cdot \\mathbf{k}_s^I)$$ $H^I$ 表示索引器头的数量（index heads） $\\mathbf{q}_{t,j}^I$ 和 $w_{t,j}^I$ 是由查询词元 $\\mathbf{h}_t$ 派生出的向量和权重，$\\mathbf{k}_s^I$ 是由历史词元 $\\mathbf{h}_s$ 派生出的键向量。 由于使用的 Relu 函数吞吐量高，并且可以降低成 FP8 进行计算，因此这一步运算完成的速度非常快。 在得到相关性分数之后，只需要选择分数最高的 top-k，取 $\\{\\mathbf{c}_s\\}$ 作为进入 Attention 计算的 K 和 V。\n$$\\mathbf{u}_t = \\text{Attn}(\\mathbf{h}_t, \\{\\mathbf{c}_s \\mid I_{t,s} \\in \\text{Top-k}(I_{t,:})\\})$$\nMinimax Sparse Attention (Minimax M3) Minimax 在 2026年6月发布了 Minimax-M3，在 M2 回归 full-attention 之后，M3 再一次引入了稀疏注意力的机制，Minimax Sparse Attention 8。\nMSA 在主分支之前依然包含索引器的步骤，通过索引器选择最关键的 token 作为稀疏注意力。注意，MSA 中的索引计算是通过 token 块 而不是 单个 token 作为核心的计算单元，来提升索引的速度。\nCompressed Sparse Attention (DeepSeek-V4, V4.1) DeepSeek-V4 Tech Report\nhttps://arxiv.org/pdf/2606.19348\nGLM-5.3 Attention (GLM-5.3-Flash) https://z.ai/blog/glm-5.3-flash\nQwen Sparse Attention (Qwen-3.8-Flash-Next) Linear Attention Basic Linear Attentions Linear Attention 从另一个视角解决了这个问题，我们首先从最基本的 Linear Attention 计算原理出发。\n考虑最基本的因果掩码的 Attention 计算，一对注意力的计算公式如下:\n$$Y_i = \\frac{\\sum_{j=1}^{i} \\exp(Q_i K_j^T) V_j}{\\sum_{j=1}^{i} \\exp(Q_i K_j^T)}$$我们假设我们对架构做如下改进，引入函数 $\\phi$\n$$ Y_i = \\frac{\\sum_{j=1}^{i} \\left( \\phi(Q_i K_j^T) \\right) V_j}{\\sum_{j=1}^{i} \\phi(Q_i K_j)^T} $$ 同时，我们规定 $\\phi(x)$ 具备很好的吸收性质: $\\phi(xy) = \\phi(x) \\phi(y)$，因此，上式可以展开为：\n$$Y_i = \\frac{\\sum_{j=1}^{i} \\left( \\phi(Q_i) \\phi(K_j)^T \\right) V_j}{\\sum_{j=1}^{i} \\phi(Q_i) \\phi(K_j)^T} = \\frac{ \\phi(Q_i)\\sum_{j=1}^{i} (\\phi(K_j)^T V_j)}{\\phi(Q_i) \\sum_{j=1}^{i} \\phi(K_j)^T}$$ 注意 这里 $\\phi(Q_i)$ 不可以被约掉，因为上下都是向量的乘法。\n我们考虑一个新的 token 计算是，需要做什么样的计算：\n首先需要计算 $\\phi(Q_i)$, $\\phi(K_i)$ 此时，新 token 不需要和过去的 token 做 attention 计算，只需要取出对应缓存的 $\\phi(K_j)^T V_j$ 的部分和对应的分母求和，求出 $Y_i$ 因此，此时新 token 的计算是 $O(1)$ 级别的时间复杂度。 RNN View 我们从 RNN 的视角思考这个结构。我们把上面公式里括号内的累加项定义为两个固定大小的矩阵状态：\n隐状态矩阵 $S_i = \\sum_{j=1}^{i} \\phi(K_j)^T V_j$ 归一化向量 $Z_i = \\sum_{j=1}^{i} \\phi(K_j)^T$ 由于这是从 $1$ 到 $i$ 的逐步累加，它天然就具备了递推特性。每当新来一个词 $i$ 时，我们根本不需要回头去看所有历史数据，只需要用上一个时刻的状态进行极其简单的更新： $$\\begin{aligned} S_i \u0026= S_{i-1} + \\phi(K_i)^T V_i \\\\ Z_i \u0026= Z_{i-1} + \\phi(K_i)^T \\end{aligned}$$ 从 RNN 的视角分析，在每一次新 token 计算时，过往的所有 token 信息全部被引入到一个固定大小的隐状态矩阵中 $S_i$, 因此，在长上下文时会存在信息损失。\nMamba 我们从 RNN 的视角规范化 Linear Attention 的迭代：\n隐状态的更新: $h(t) = f(h_{t-1}, x_t)$ 前向计算，更新出当前的输出: $y_t = g(h_t, x_t)$ 在 Linear Attention 中，更新结构的函数如下:\n$h(t) = h(t-1) + \\phi(K_i)^T V_i$ $y(t) = \\phi(Q_i) h(t)$ 我们考虑隐状态的更新公式: $h(t) = h(t-1) + \\phi(K_i)^T V_i$，我们可以把隐状态的更新当做是：\n过去的历史状态 此步骤引入的新状态 很显然，Linear Attention 的更新是一个相对粗糙的步骤，两者进行简单的加和，导致模型无法学到很好的注意力分布。\n在 Mamba9 论文中，提出了一种更新的架构，具体公式表示如下:\n$$ h_t = A_t h_{t-1} + B_t x_t $$ $$ y_t = C_t $$ 其中关键的参数变量 $A_t$, $B_t$, $C_t$ 都是依赖当前状态步骤下的 $t$ 输入。\nWe identify that a key weakness of such models is their inability to perform content-based reasoning, and make several improvements. First, simply letting the SSM parameters be functions of the input addresses their weakness with discrete modalities, allowing the model to selectively propagate or forget information along the sequence length dimension depending on the current token.\n然而，由于 Mamba 的架构创新属于比较激进的结构创新，且目前暂不属于主流的 LLM 架构，因此暂时不作为核心内容介绍。\nMamba2 Mamba210 仍然保持相同的整体更新策略，和 Mamba1 结构非常类似。只不过这一次在输出层引入优化。\nGated Delta Net 在介绍 Gated DeltaNet 的基础之上，我们先介绍基本的 DeltaNet 结构：论文 Linear Transformers Are Secretly Fast Weight Programmers 11 将上述核函数形式的 linear attention 和九十年代的 fast weight controllers 结合，作者首先针对当前 Linear Attention 的结构，提出了如下局限性：\nLinear Attention 本质上是维护一个隐状态矩阵 $S_t$, 这个矩阵能够维持的信息是有限的，无法应对持续增长的上下文。从正交性的角度解释，当序列长度超过了对应 key 的 dimension length，就处于一种过容量状态。此时，模型会被迫学会更新当前的容量状态。 https://arxiv.org/pdf/2406.06484\nKimi Delta Attention Flash Attention https://arxiv.org/pdf/2205.14135 https://arxiv.org/pdf/2307.08691 https://arxiv.org/pdf/2407.08608 https://arxiv.org/pdf/2603.05451 https://github.com/dao-ailab/flash-attention\nWhat is the Frontier? Kimi (Kimi-K3, Kimi Delta Attention): https://arxiv.org/pdf/2607.24653 Qwen (Qwen-3.8-Flash-Next, GDN \u0026amp; Qwen Sparse Attention): https://github.com/QwenLM/Qwen3.8-Flash-Next/blob/main/tech_report.pdf GLM (GLM-5.3-Flash): https://z.ai/blog/glm-5.3-flash DeepSeek (DeepSeek-V4.1-Flash, Compressed Sparse Attention): https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash/blob/main/DeepSeek_V41_Tech_Report.pdf Minimax (Minimax-M3, Minimax Sparse Attention): https://arxiv.org/abs/2606.13392 References https://arxiv.org/pdf/1706.03762\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://arxiv.org/pdf/2305.13245\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://arxiv.org/pdf/2405.04434\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://arxiv.org/pdf/2004.05150\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://arxiv.org/pdf/2309.17453\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://arxiv.org/pdf/2502.11089\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://arxiv.org/pdf/2512.02556\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://arxiv.org/abs/2606.13392\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://arxiv.org/pdf/2312.00752\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://arxiv.org/abs/2405.21060\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://arxiv.org/abs/2102.11174\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://xiyuanyang-code.github.io/llm/posts/model-arch/attentions/","summary":"\u003c!-- generated by tools/sync.py from model_arch/Advanced LLMs 1 (Attentions).md; DO NOT EDIT --\u003e\n\u003cp\u003eAttention 普遍被认为是语言模型架构的基础，然而，从过去的机器翻译的 Encoder-Decoder 架构到如今的大语言模型，Attention 也出现了大量的变体，用于适配更高效，更 scaleble 的模型。\u003c/p\u003e\n\u003cp\u003e在今天的内容中，我们将重点介绍常见的 Attention 变体定式，从数学原理到一线的技术报告。\u003c/p\u003e\n\u003ch2 id=\"simple-attentions-and-mha\"\u003eSimple Attentions and MHA\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003eTLDR: 在 Attention is all you need 中，作者提出了最基本的注意力机制和多头注意力的版本。\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e我们首先看最基本的 Attention 长什么样子, 从最经典的 Attention is all you need \u003csup id=\"fnref:1\"\u003e\u003ca href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e1\u003c/a\u003e\u003c/sup\u003e 开始讲起:\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"attn-simple.png\" loading=\"lazy\" src=\"/llm/posts/model-arch/attentions/attn-simple.png\"\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e图片左侧救赎最基本的 Attention，他的数学表达形式如下:\n\u003c/p\u003e\n$$\\text{Attention}(Q, K, V) = \\text{softmax}\\left(\\frac{QK^T}{\\sqrt{d_k}}\\right)V$$\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e$Q \\in \\mathbb{R}^{n \\times d_k}$：查询矩阵（Query）\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e$K \\in \\mathbb{R}^{m \\times d_k}$：键矩阵（Key）\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e$V \\in \\mathbb{R}^{m \\times d_v}$：值矩阵（Value）\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e$d_k$：键/查询的向量维度，$\\sqrt{d_k}$ 用于防止点积数值过大导致 Softmax 梯度消失\u003c/p\u003e","title":"Advanced LLMs 1: Attentions"},{"content":" What is in Standard Transformers 在 Input Embedding 上进行 Position Embedding 的嵌入 为什么需要位置编码？什么样的位置编码是好的位置编码？ 对于序列中第 $pos$ 个位置（$pos$ 表示词语在序列中的索引，从 $0$ 开始），其位置编码向量的第 $i$ 个维度（$i$ 表示向量的维度索引）的计算公式 $$PE_{(pos, 2i)} = \\sin\\left(\\frac{pos}{10000^{\\frac{2i}{d_{model}}}}\\right)$$$$PE_{(pos, 2i+1)} = \\cos\\left(\\frac{pos}{10000^{\\frac{2i}{d_{model}}}}\\right)$$ Nomalizations Post-Norm（先做 forward 和残差，然后再归一化） \u0026amp;\u0026amp; Layer-Norm (对 $d_{\\text{model}}$ 这个维度做归一化) FFN 使用 RELU 作为激活函数 $\\text{FFN}(x) = W_2 \\max(0, W_1 x + b_1) + b_2$ 在 Attention 那一讲，我们更多探索的是现代大模型架构对 Attention 计算 (Attention Block) 的核心组件的改进和加速，在本文中，我们会思考模型架构中的一些其他组件，看看现代大模型架构相对于传统 Transformers 结构的改进。\n在第三讲，我们将会从各家最前沿的技术报告出发，探索下一个时代的前卫的 LLM 架构。\nPre-Norm and Post-Norm Pre-Norm: 先对输入做 Norm 然后再 forward \u0026amp;\u0026amp; 做残差连接 Post-Norm: 先 forward \u0026amp;\u0026amp; 做残差连接 再做 Norm 从梯度传递的角度，Pre-Norm 的训练稳定性更好 ($x_{l+1} = x_l + F(N(x_l))$) 进行反求梯度会更好 -\u0026gt; 残差项在外面，因此会更稳定\n从数学角度诠释：\nPostNorm: Forward: $x_{l+1} = N(x_l + F(x_l))$ Backward: $\\frac{\\partial x_{l+1}}{\\partial x_l} = J_N(I + J_F)$ PreNorm: Forward: $x_{l+1} = x_l + F(N(x_l))$ Backward: $\\frac{\\partial x_{l+1}}{\\partial x_l} = \\underbrace{I}_{\\text{identity path}} + J_FJ_N$ 从计算稳定性的角度，因为 Pre-Norm 对残差在外面，因此反向传播的时候会更加的稳定 https://arxiv.org/pdf/2002.04745 这篇论文给出了系统性的诠释：\n对于 PostNorm 的结构，学习率预热非常的重要（在训练初期，学习率线性上升到一个峰值，然后通过各种策略进行 decay） Pre-Norm 从理论上和实验上分别证明了其训练稳定性往往高于 Post-Norm Post-Norm 需要很谨慎的 learning rate warmup 的过程，稳定性比 Pre-Norm 差 除了标准的 Pre-Norm 和 Post-Norm，还有一些更加高级的操作：\n传统 Pre-Norm: $x_{l+1} = x_l + F(Norm(x_l))$ Double Norm: $x_{l+1} = x_l + Norm(F(Norm(x_l)))$ 注意，仍然把残差连接放在了最外面，以保证稳定性 Non-residual Post-Norm: $x_{l+1} = x_l + Norm(F(x_l))$ RMS Normalizations 我们主要讨论三种常见的 Norm 计算方式：\nBatch Norm Layer Norm RMS Norm Batch Norm 针对 batch-size 和 length 维度进行归一化，但是因为 LLM 需要处理可变长文本，因此不再采用 Batch Norm $$\\mu_B = \\frac{1}{m} \\sum_{k=1}^{m} x_k$$$$\\sigma_B^2 = \\frac{1}{m} \\sum_{k=1}^{m} (x_k - \\mu_B)^2$$$$\\hat{x}_i = \\frac{x_i - \\mu_B}{\\sqrt{\\sigma_B^2 + \\epsilon}}$$$$y_i = \\gamma \\hat{x}_i + \\beta$$Layer Norm $$\\mu = \\frac{1}{d} \\sum_{i=1}^{d} x_i$$$$\\sigma^2 = \\frac{1}{d} \\sum_{i=1}^{d} (x_i - \\mu)^2$$$$\\hat{x}_i = \\frac{x_i - \\mu}{\\sqrt{\\sigma^2 + \\epsilon}}$$$$y_i = \\gamma_i \\odot \\hat{x}_i + \\beta_i$$ 在传统的 Attention 中，模型的 Norm 使用 LayerNorm 进行优化\nRMSNorm $$\\text{RMS}(x) = \\sqrt{\\frac{1}{d} \\sum_{i=1}^{d} x_i^2 + \\epsilon}$$$$\\hat{x}_i = \\frac{x_i}{\\text{RMS}(x)}$$$$y_i = \\gamma_i \\odot \\hat{x}_i$$ RMS-Norm 仍然使用 dimension 这个维度进行归一化，同时，RMSNorm 省略了大量的中间变量（不需要计算均值和归一化的均值）\n为什么 RMS Norm 的优势显著高于 LayerNorm ?\n运算更简单，涉及更少中间变量的存储\n激活函数 激活函数在 FFN 中承担的作用：引入非线性\n常见的激活函数：\nsigmoid tanh relu leaky relu gelu silu/swish Gated FFNs and SwiGLU Gated FFN 在传统 FFN 层上引入了门控机制：\n$$\\text{FFN}(x) = W_2 \\max(0, W_1 x + b_1) + b_2$$ $$ h = \\sigma(W_1 x) $$ 对于 Gated FFN:\n$$h = \\sigma (W_g x) \\odot (W_u x) $$ Gate Branch 负责提供门控单元，需要过激活函数 Feature Branch 添加了一个可学习矩阵 SwiGLU: $$ \\text{SiLU}(x) = z \\sigma (z) $$ $$ \\text{FFN}_{\\text{SwiGLU}}(x) = W_d [\\text{SiLU}(W_g x) \\odot (W_u x)] $$ 激活函数使用 SiLU 添加 Gated FFN 同时在外面再套一层可学习矩阵 从参数角度分析，因为引入了一个 gated 参数 $W_g$，因此参数量会大 3/2 倍。\nPosition Information and RoPE 会考 Rope 的推导 \u0026amp;\u0026amp; 模型长度的内推和外推\n首先，考虑一个问题，为什么我们需要 Positiom Embedding？因为 transformer 本身的 Attention 不会考虑 position 关系，对 sequence 中的每一个 token 都做同等地位的注意力 attention score 计算。\n然后，接下来，我们考虑第二个问题，对于一个 token 在训练 batch 中具备一个编码 $i$，但是这个 $i$ 不可以按照绝对值的形式直接 append 到一个独立的维度，因为训练的 batch 在实际 inference 的时候，绝对位置可能会发生变化。因此，关注 token间相对位置 是使用 Position Enbedding 最关键的方法。\n我们接下来介绍 RoPE 的基本思想，假设当前的 query token 是 $q_t$(Positon: t)，当前 token 需要和历史的 $s$ token 进行注意力计算。\nLinear Projections: $q_t = x_t W_Q$, $k_s = x_s W_K$ Position-dependent rotations: ${q_t}^{'} = R_t q_t$, ${k_s}^{'} = R_s k_s$ RoPE 需要解决的关键是，$R_s$, $R_t$ 需要使用什么样矩阵表示？\nRotary Matrix $$\\begin{pmatrix} x' \\\\ y' \\end{pmatrix} = \\begin{pmatrix} \\cos\\theta \u0026 -\\sin\\theta \\\\ \\sin\\theta \u0026 \\cos\\theta \\end{pmatrix} \\begin{pmatrix} x \\\\ y \\end{pmatrix}$$ 几何意义上，定义上述二维矩阵是 $R(\\theta)$ 则表示为，在二维空间下按照正方向对向量进行旋转。\n旋转矩阵具备如下的性质：\n$R(\\alpha)^T = R(- \\alpha)$ $R(\\alpha)^T R(\\beta) = R(\\beta - \\alpha)$ 我们来看看如果我们使用旋转矩阵作为我们的 $R$, 我们的 attention 计算如下:\n$q'_t = R(t) q_t$, $k'_s = R(s) k_s$，展开 Attention 计算： $$(q'_t)^\\top k'_s = (R(t) q_t)^\\top (R(s) k_s) = q_t^\\top R(t)^\\top R(s) k_s = q_t^\\top R(s - t) k_s $$ 如果我们考虑旋转“角速度”： $$ (q'_t)^\\top k'_s = (R(t) q_t)^\\top (R(s) k_s) = q_t^\\top R(t)^\\top R(s) k_s = q_t^\\top R(\\omega(s - t)) k_s $$ 这个式子给了我们一个很有意思的结论：加上 Rope 旋转矩阵后，attention 的计算分数只和两个 token 的相对位置有关，而和绝对位置无关。\n对于简单的 Positional Embedding，向量的加法通过 sin 和 cos 实现不同频率的 position 位置的插入，也有类似的性质，但是没有 RoPE 性质那么好。\n将二维的旋转矩阵 apply 到高维隐空间中，核心思想是：将高维向量拆分为多个独立的 2 维子空间，并在每个子空间上分别应用不同频率的二维旋转。\n对于一个 $d$ 维的向量（假设 $d$ 是偶数），高维旋转矩阵 $R_t$ 可以写成如下形式：\n$$R_t = \\begin{pmatrix} R(\\theta_{1, t}) \u0026 0 \u0026 \\cdots \u0026 0 \\\\ 0 \u0026 R(\\theta_{2, t}) \u0026 \\cdots \u0026 0 \\\\ \\vdots \u0026 \\vdots \u0026 \\ddots \u0026 \\vdots \\\\ 0 \u0026 0 \u0026 \\cdots \u0026 R(\\theta_{d/2, t}) \\end{pmatrix}$$其中：\n整个矩阵大小为 $d \\times d$。 对角线上的每一个小块 $R(\\theta_{i, t})$ 都是一个前面讨论过的 $2 \\times 2$ 二维旋转矩阵：$$R(\\theta_{i, t}) = \\begin{pmatrix} \\cos\\theta_{i, t} \u0026 -\\sin\\theta_{i, t} \\\\ \\sin\\theta_{i, t} \u0026 \\cos\\theta_{i, t} \\end{pmatrix}$$ 每一个 2 维小块的旋转角度 $\\theta_{i, t}$ 是位置 $t$ 与不同频率 $\\omega_i$ 的乘积：$\\theta_{i, t} = t \\cdot \\omega_i$。 其中 $\\theta_{i,t}$ 指的是在绝对位置 $t$ 处，针对第 $i$ 个二维子空间的旋转角度。$\\omega_i$ 代表不同 dim 位置上的 rotation，具体的角速度分数和 positional embedding 几乎一致： $$\\omega_i = 10000^{-\\frac{2(i-1)}{d}}$$Positional Interpolation \u0026amp; Extrapolation LLM 会设置一个最大上下文长度 max-content-length, 在 vLLM 等推理引擎进行模型推理时识别这个参数，在监测到长度超过上下文时，会拒绝这一次 LLM-Call 的请求。并且，面对超过预训练长度的请求，模型会因为缺乏对应的训练数据，导致PPL 陡增。\n外推：直接去让模型处理超过训练长度的 Positional Embedding，但是会导致效果 drop 内推 (PI: Positional Interpolation)：假设模型训练时的最大上下文长度是 $L$，当处理超过上下文长度 $L' \u003e L$ 的时候，需要将 $L'$ 映射到一个在 $L$ 内部的值 $L''$（可以是小数） 定义缩放因子 $S = \\frac{L}{L''}$，则上述 Rope 被修正为: $\\theta_{i,t} = \\frac{t}{S} \\omega_i$，PI 本质上是一种线性内插的方法。\nPI 通过固定的缩放因子，可以在推理过程中动态的拉长最大上下文到一个更大的值 （e.g. 拉长 2/4 倍）\nNTK-Aware Methods https://www.reddit.com/r/LocalLLaMA/comments/14lz7j5/ntkaware_scaled_rope_allows_llama_models_to_have/\n在 rope 中，不同位置的旋转矩阵具有不同的频率，低维高频，高维低频。\n因此，在线性插值的过程中，会导致低维区域下的旋转变的异常拥挤，会导致效果 drop，可以看到，在低维高频区域，经过线性插值的 PI 和原来的 Rope 存在比较大的差异。\n在 PI 中，$\\theta$ 的格式如下: $$(\\theta_{\\text{pi}})_{i,t} = \\frac{t}{S} \\cdot b^{-\\frac{2(i - 1)}{d}}$$ $$(\\theta_{\\text{NTK}})_{i,t} = t \\cdot (b \\cdot S^{\\frac{d}{d - 2}})^{-\\frac{2(i - 1)}{d}}$$ 我们考虑 0-based 计数的 RoPE:\n$$\\theta_{i, t} = t \\cdot \\omega_i = t \\cdot b^{-\\frac{2i}{d}}$$ 这个旋转角可以通过 $\\beta$ 进制的角度解释，define $\\beta = b^{\\frac{2}{d}}$:\n$$\\theta_{i, t} = t \\cdot \\omega_i = t \\cdot b^{-\\frac{2i}{d}} = \\frac{t}{\\beta^i} $$ 因此，旋转角本质上就是求位置 $t$ 的 $\\beta$ 进制，现在我们的目的是拉长推理时的最大上下文长度，在保持 dimension 不变的情况下，模型需要对应提升 $\\beta$ ：\n$$ \\theta_{i,t} = \\frac{t/S}{\\beta^i} = \\frac{t}{(k \\beta)^i} $$$$ k = S^{-1/i} $$考虑最大维度下的偏差 -\u0026gt; $i = d/2 - 1$ -\u0026gt; $k = S^{2/(d-2)}$\n更精彩的诠释可以查看苏老师的博客：https://normxu.github.io/Rethinking-Rotary-Position-Embedding/\n同样的，在 NTK-Aware 方法之后，还出现了 NTK-by-parts 等等方法：\nNTK-by-parts 吸收了 NTK 高频和低频的在压缩时不一致的问题，因此引入了一个混合加权的策略，在低维高频区域，不引入任何压缩，在高维低频区域，引入简单的线性插值，中间部分通过滑动加权进行平滑处理。具体的数学公式可以看经典论文 https://arxiv.org/pdf/2309.00071\n此外，还有 Dynamic-NTK，只在推理上下文长度超过 $L$ 时引入对应的策略 (PI, NTK-Aware, NTK-Select, YaRN)，在正常的上下文长度时，保持为 $L$ 不变。\nYaRN https://arxiv.org/pdf/2309.00071 是在 NTK-Aware 基础上提出的一种新的拉长模型上下文的方法。\n在插值过程中，研究者发现模型因为上下文变长导致 Attention 计算后的 attention score 出现熵增的趋势（因为参与计算的 token 数量变多了）\nYarn 在 Attention 计算时引入了一种退火参数：\n$$\\text{Attention}(Q, K, V) = \\text{softmax}\\left( \\frac{QK^T}{\\sqrt{d_k} \\cdot t} \\right)V$$ 在论文中，作者也给出了 $t$ 的最佳取值\n$$\\sqrt{\\frac{1}{t}} = 0.1 \\cdot \\ln(s) + 1$$ 当我们将一个向量乘以一个大于 1 的倍数 $k$ 之后再输入到 Softmax 函数中，输出的概率分布会变得更加“尖锐”（Sharp）——即最大的概率值会更接近 1，而其余较小的概率值会迅速被压低趋近于 0。 这个有点类似于大模型输出时候，对最后一层的词表做 temperature 的 调整，再经过最后的 softmax 操作。如果 temperature 很小的时候，会导致模型 softmax 输出概率变尖锐，导致最终输出文本更趋于确定性。\n","permalink":"https://xiyuanyang-code.github.io/llm/posts/model-arch/normalizations-ffn-rope/","summary":"\u003c!-- generated by tools/sync.py from model_arch/Advanced LLMs 2 (Nomarlizations, FFNs \u0026 RoPE).md; DO NOT EDIT --\u003e\n\u003ch2 id=\"what-is-in-standard-transformers\"\u003eWhat is in Standard Transformers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e在 Input Embedding 上进行 Position Embedding 的嵌入\n\u003cul\u003e\n\u003cli\u003e\u003cmark\u003e为什么需要位置编码？什么样的位置编码是好的位置编码？\u003c/mark\u003e\u003c/li\u003e\n\u003cli\u003e对于序列中第 $pos$ 个位置（$pos$ 表示词语在序列中的索引，从 $0$ 开始），其位置编码向量的第 $i$ 个维度（$i$ 表示向量的维度索引）的计算公式\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n$$PE_{(pos, 2i)} = \\sin\\left(\\frac{pos}{10000^{\\frac{2i}{d_{model}}}}\\right)$$$$PE_{(pos, 2i+1)} = \\cos\\left(\\frac{pos}{10000^{\\frac{2i}{d_{model}}}}\\right)$$\u003cul\u003e\n\u003cli\u003eNomalizations\n\u003cul\u003e\n\u003cli\u003ePost-Norm（先做 forward 和残差，然后再归一化） \u0026amp;\u0026amp; Layer-Norm (对 $d_{\\text{model}}$ 这个维度做归一化)\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003eFFN\n\u003cul\u003e\n\u003cli\u003e使用 RELU 作为激活函数\u003c/li\u003e\n\u003cli\u003e$\\text{FFN}(x) = W_2 \\max(0, W_1 x + b_1) + b_2$\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e在 Attention 那一讲，我们更多探索的是现代大模型架构对 Attention 计算 (Attention Block) 的核心组件的改进和加速，在本文中，我们会思考模型架构中的一些其他组件，看看现代大模型架构相对于传统 Transformers 结构的改进。\u003c/p\u003e","title":"Advanced LLMs 2: Normalizations, FFNs \u0026 RoPE"},{"content":" Decoding and Prefiling 在 Advanced Attention 章节过后，我们将视角放在全局的 LLM forward 计算过程中：\nPrefill 阶段：模型处理输入 Prompt，计算 QK 矩阵，存储 KV-Cache 在这个阶段对于 Full-Attention 来说，需要完成多个 $o(D^2)$ 的大矩阵乘法来计算 Attention Score 因此是计算密集型的操作 Decode 阶段：模型开始实现自回归解码 为了简化计算，我们考虑最简单的 Full-Attention 的情况，且不考虑多头注意力：\n之前我们计算过，单次 Attention 涉及: $\\text{FLOPs}_{prefill} \\approx \\underbrace{6N d^2}_{\\text{QKV 投影}} + \\underbrace{4N^2 d}_{\\text{Attention 矩阵运算}}$ Memory Bound $\\text{Bytes}_{prefill} = \\underbrace{3d^2 \\times \\text{bytes}}_{\\text{权重 (固定)}} + \\underbrace{c_1 N d \\times \\text{bytes}}_{\\text{线性激活值/KV 读写}} + \\underbrace{c_2 N^2 \\times \\text{bytes}}_{\\text{Attention 矩阵读写}}$ 因此，计算强度定义为： $$I_{prefill} = \\frac{6N d^2 + 4N^2 d}{3d^2 + c_1 N d + c_2 N^2}$$ 我们假设 N 很大，此时计算强度为 $4d/c_2$ 是一个相对较大的数值，是计算密集型\n在 Decoding 计算，每一次只需要实现一次向量-矩阵乘法，因此 FLOPS 是 $\\text{FLOPs}_{decode} = 6d^2 + 4Nd$, 是线性的（对于生成一个 token 来说），但是每次计算需要把模型权重 \u0026amp;\u0026amp; KV-Cache 实现完整的搬运, 因此 Memory Bound = $(3d^2 + 2Nd) \\times \\text{bytes}$。 因此，在 Decode 阶段，计算强度定义为 $$I_{decode} = \\frac{6d^2 + 4Nd}{(3d^2 + 2Nd) \\times \\text{bytes}} = \\frac{2}{\\text{bytes}}$$ 因此，我们得到了一个关键结论：\nPrefiling 阶段是 Computation-bounded 的计算，因此关键的优化点在于如何优化 $O(N^2)$ 的矩阵乘法计算，例如引入 sparse attention，linear attention 来优化矩阵乘法的计算复杂度 Decoding 阶段的计算强度和 $N$ 无关，甚至在极端情况下（例如我们上面计算的最简单计算情况），模型的计算强度是一个很低的常数。 Speculative Decoding Multi-Token Prediction DeepSeek: https://arxiv.org/pdf/2412.19437\n","permalink":"https://xiyuanyang-code.github.io/llm/posts/model-arch/decoding/","summary":"\u003c!-- generated by tools/sync.py from model_arch/Advanced LLMs 3 (Decoding).md; DO NOT EDIT --\u003e\n\u003ch2 id=\"decoding-and-prefiling\"\u003eDecoding and Prefiling\u003c/h2\u003e\n\u003cp\u003e在 Advanced Attention 章节过后，我们将视角放在全局的 LLM forward 计算过程中：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePrefill 阶段：模型处理输入 Prompt，计算 QK 矩阵，存储 KV-Cache\n\u003cul\u003e\n\u003cli\u003e在这个阶段对于 Full-Attention 来说，需要完成多个 $o(D^2)$ 的大矩阵乘法来计算 Attention Score\u003c/li\u003e\n\u003cli\u003e因此是计算密集型的操作\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003eDecode 阶段：模型开始实现自回归解码\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e为了简化计算，我们考虑最简单的 Full-Attention 的情况，且不考虑多头注意力：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e之前我们计算过，单次 Attention 涉及:  $\\text{FLOPs}_{prefill} \\approx \\underbrace{6N d^2}_{\\text{QKV 投影}} + \\underbrace{4N^2 d}_{\\text{Attention 矩阵运算}}$\u003c/li\u003e\n\u003cli\u003eMemory Bound $\\text{Bytes}_{prefill} = \\underbrace{3d^2 \\times \\text{bytes}}_{\\text{权重 (固定)}} + \\underbrace{c_1 N d \\times \\text{bytes}}_{\\text{线性激活值/KV 读写}} + \\underbrace{c_2 N^2 \\times \\text{bytes}}_{\\text{Attention 矩阵读写}}$\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e因此，计算强度定义为：\n\u003c/p\u003e","title":"Advanced LLMs 3: Decoding"},{"content":" 🚧 本章节正在撰写中，内容会随笔记更新同步发布。\n","permalink":"https://xiyuanyang-code.github.io/llm/posts/model-arch/optimizers/","summary":"\u003c!-- generated by tools/sync.py from model_arch/Advanced LLMs 4 (Optimizers).md; DO NOT EDIT --\u003e\n\u003cblockquote\u003e\n\u003cp\u003e🚧 本章节正在撰写中，内容会随笔记更新同步发布。\u003c/p\u003e\u003c/blockquote\u003e","title":"Advanced LLMs 4: Optimizers"},{"content":" 🚧 本章节正在撰写中，内容会随笔记更新同步发布。\n","permalink":"https://xiyuanyang-code.github.io/llm/posts/model-arch/mixture-of-experts/","summary":"\u003c!-- generated by tools/sync.py from model_arch/Advanced LLMs 5 (Mixture of Experts).md; DO NOT EDIT --\u003e\n\u003cblockquote\u003e\n\u003cp\u003e🚧 本章节正在撰写中，内容会随笔记更新同步发布。\u003c/p\u003e\u003c/blockquote\u003e","title":"Advanced LLMs 5: Mixture of Experts"},{"content":" 本文，我们将会重点梳理 Slime 中通用的训练组建和框架，包括他如何勾连 Megatron 在 GPU 上进行训练。重心将会放在 training 的一般抽象和构建中，具体的训练算法和一些额外的 trick 将会在后续具体章节给出。\n入口文件 入口是 train.py,但它不会直接导入 actor.py。MegatronTrainRayActor 是经 RayTrainGroup 这条链、在创建 Ray actor 的那一刻延迟导入的。\n完整的导入链:\ntrain.py(仓库根目录) └── from slime.ray.placement_group import create_training_models ① 入口只导入 placement_group │ └── create_training_models(args, pgs, rollout_manager) ② placement_group.py │ └── allocate_train_group(...) ③ placement_group.py │ └── RayTrainGroup(args=..., ...) ④ actor_group.py(构造训练组) │ └── _allocate_gpus_for_actor(...) ⑤ actor_group.py(创建 actor 时) │ └── from slime.backends.megatron_utils.actor import MegatronTrainRayActor ⑥ 延迟导入! │ └── ray.remote(...)(actor_impl) ⑦ 包装成 Ray Actor 类 └── TrainRayActor.options(...).remote(rank) ⑧ 每张 GPU spawn 一个 worker 继承关系:\nM e g a t └ r ─ o ─ n T T └ r r ─ a a ─ i i n n R R R a a a y y y A A A c c c t t t o o o r r r s s s l l l i i i m m m e e e / / / b r r a a a c y y k / / e t r n r a d a y s i _ / n a m _ c e a t g c o a t r t o . r r p o . y n p ( _ y 最 u ( 底 t 通 层 i 用 的 l 训 s 练 R / a a a y c c t t a o o c r r t . o p 逻 r y 辑 ) 基 类 ) backend 层只实现 Megatron 特有部分(init、train_actor、update_weights、save_model 等),通用的 Ray 生命周期逻辑在基类里。(这部分会在 Ray 这个专题中进行讲述)\n核心训练封装 我们来看最核心的 MegatronTrainRayActor 类。\n核心的训练流程，可以抽象为两个部分:\nRollout/Generations: 模型不会进行梯度计算和权重更新，而是进行数据 rollout，拿到 log_probs 等 snapshot，存储起来作为训练数据 前向计算的核心就是谁 Rollout 什么数据: \u0026quot;什么数据为上游数据输入的准备\u0026quot; 关键在于模型权重的切换: ref_model, teacher_model, actor_model Optimizations: Training Forward: 开梯度前向计算，得到 log_probs 计算 Loss (根据现有的数据，例如 log_probs, old_log_probs, advantages) 进行计算并且反向传比 更新权重 Training 核心的函数在 train 中被定义 (MegatronTrainRayActor):\ndef train(self, rollout_id: int, rollout_data_ref: Box, external_data=None): if self.args.debug_rollout_only: return None if self.args.offload_train: # 激活唤醒 self.wake_up() with timer(\u0026#34;data_preprocess\u0026#34;): # Rollout/Generations # 主要执行数据预处理 rollout_data = self._get_rollout_data(rollout_data_ref) # 核心的训练 \u0026amp; weight update if self.role == \u0026#34;critic\u0026#34;: result = self.train_critic(rollout_id, rollout_data) else: self.train_actor(rollout_id, rollout_data, external_data=external_data) result = None # 清理数据，完成一轮训练 if self.args.offload_train: del rollout_data self.sleep() return result 在核心训练中，会分成 Actor/Critic 两个不同的角色，这两个角色的优化目标不同:\nActor 输出 [Batch, Length, Vocab_Size]，即当前 token 下模型输出的 logits (正常语言模型的输出) Critic 输出 [Batch, Length, 1], 给出一个 token 级别的 reward 输出，即在当前状态下的 token 可以拿到的预期回报。 我们先来探寻 actor 训练的核心流程。\nActor Training 核心来看 train_actor 这个函数，核心阶段是：\n前向计算，拿到不同 model 的 log_probs 等参数，准备后续 loss 的计算 计算 advantages \u0026amp; returns 等 (在训练前完成) 核心 Megatron 训练: Forward pass in torch (带梯度图) Backward Weight Update 更新 \u0026amp; 备份模型参数 def train_actor(self, rollout_id: int, rollout_data: RolloutBatch, external_data=None) -\u0026gt; None: # Create data iterator for log_probs and train. # * 从 data_iterator 中读取 rollout 数据（输入是训练数据） data_iterator = get_data_iterator(rollout_data) num_microbatches = rollout_data[\u0026#34;num_microbatches\u0026#34;] global_batch_sizes = rollout_data[\u0026#34;global_batch_sizes\u0026#34;] # * Rollout Routing Replay 技巧 # 论文 https://arxiv.org/pdf/2510.11370 # 主要核心解决的问题是 MoE 语言模型在训练和 rollout 中路由器不一致的问题，使用 replay 技巧可以提升训练的稳定性 if self.args.use_rollout_routing_replay: self.fill_routing_replay(data_iterator, num_microbatches, rollout_data) with inverse_timer(\u0026#34;train_wait\u0026#34;), timer(\u0026#34;train\u0026#34;): # 前向计算 # 在前向计算中，核心就两件事情: # - 使用什么模型权重 # - 使用什么计算，计算出什么 if self.args.compute_advantages_and_returns: if \u0026#34;ref\u0026#34; in self.weights_backuper.backup_tags: if self.args.use_routing_replay: os.environ[\u0026#34;ROUTING_REPLAY_STAGE\u0026#34;] = \u0026#34;fallthrough\u0026#34; # 如果 ref 模型存在 (常用于 KL 散度的计算) # 更新到 ref 模型的权重 \u0026amp;\u0026amp; 更新 data (前向 rollout data) self._switch_model(\u0026#34;ref\u0026#34;) rollout_data.update( self.compute_log_prob( data_iterator, num_microbatches, store_prefix=\u0026#34;ref_\u0026#34;, ) ) # 如果 teacher 模型存在 # 更新到 teacher 模型的权重 \u0026amp;\u0026amp; 更新 data # * 这一个常用作 On Policy Distillation 算法中，需要存储 teacher 前向过程中的 log_probs # Forward teacher model to get teacher_log_probs for Megatron-based OPD if \u0026#34;teacher\u0026#34; in self.weights_backuper.backup_tags: if self.args.use_routing_replay: os.environ[\u0026#34;ROUTING_REPLAY_STAGE\u0026#34;] = \u0026#34;fallthrough\u0026#34; self._switch_model(\u0026#34;teacher\u0026#34;) rollout_data.update( self.compute_log_prob( data_iterator, num_microbatches, store_prefix=\u0026#34;teacher_\u0026#34;, ) ) # actor 模型的 rollout 步骤 # * 切换成 actor (需要被更新模型的参数) # * Off Policy Importance Sampling # 在 Off Policy 的策略中，actor 模型的策略和 rollout 模型的策略是两个不同的策略，因此需要加上重要性采样的步骤 self._switch_model(\u0026#34;old_actor\u0026#34; if self.args.keep_old_actor else \u0026#34;actor\u0026#34;) # can_reuse_log_probs_in_loss 是一个核心的 tag，其代表的是模型是否能够省略一次无梯度的前向过程 # * 后续会详细的解读这一个判断逻辑，为什么可以不跑 log_probs can_reuse_log_probs_in_loss = ( len(num_microbatches) == 1 and self.args.loss_type == \u0026#34;policy_loss\u0026#34; and self.args.kl_coef == 0 and not self.args.use_rollout_logprobs and not self.args.get_mismatch_metrics and not self.args.use_critic and not self.args.keep_old_actor and not self.args.use_opd and not self.args.use_routing_replay and self.args.advantage_estimator != \u0026#34;gspo\u0026#34; ) if ( not self.args.use_rollout_logprobs or self.args.get_mismatch_metrics ) and not can_reuse_log_probs_in_loss: if self.args.use_routing_replay: if self.args.use_rollout_routing_replay: os.environ[\u0026#34;ROUTING_REPLAY_STAGE\u0026#34;] = \u0026#34;replay_forward\u0026#34; else: os.environ[\u0026#34;ROUTING_REPLAY_STAGE\u0026#34;] = \u0026#34;record\u0026#34; # * 如果 can_reuse_log_probs_in_loss 被设置为 True，这一行将会被跳过 # 注意，这里计算的是 actor 模型运行的无梯度的 prob rollout_data.update( self.compute_log_prob( data_iterator, num_microbatches, store_prefix=\u0026#34;\u0026#34;, ) ) if self.args.use_rollout_routing_replay: RoutingReplay.clear_all_forward() # 汇入 critic 数据 if self.args.use_critic: if external_data is not None and mpu.is_pipeline_last_stage(): values = external_data.get(\u0026#34;values\u0026#34;) if values is not None: from slime.backends.megatron_utils.data import tensors_to_gpu rollout_data[\u0026#34;values\u0026#34;] = tensors_to_gpu(values) if self._active_model_tag != \u0026#34;actor\u0026#34;: self._switch_model(\u0026#34;actor\u0026#34;) # Calculate adv and returns. Need to performed before training (instead of on the fly), # because we may need normalize the whole rollout. # * 核心函数: 在训练之前计算 return 和 advantages # 因为 GRPO 需要做组归一化，因此这个过程不可以在训练中执行 compute_advantages_and_returns(self.args, rollout_data) # 数据后处理，主要是日志处理等，省略 if self.rollout_data_postprocess is not None: self.rollout_data_postprocess(self.args, rollout_id, rollout_data) log_rollout_data( rollout_id, self.args, rollout_data, ) # Train # * 核心的训练 Weight Update 的过程 # 核心的 train 过程出现在 `model.py` 中 # 核心的训练过程被 megatron 封装，主要分成 forward_pass (带梯度的动态计算图) \u0026amp; backward weight update if self.args.use_routing_replay: os.environ[\u0026#34;ROUTING_REPLAY_STAGE\u0026#34;] = \u0026#34;replay_backward\u0026#34; with timer(\u0026#34;actor_train\u0026#34;): train( rollout_id, self.model, self.optimizer, self.opt_param_scheduler, data_iterator, num_microbatches, global_batch_sizes, ) self.prof.step(rollout_id=rollout_id) # 日志产物落盘 \u0026amp; 其他步骤 train_dump_utils.save_debug_train_data(self.args, rollout_id=rollout_id, rollout_data=rollout_data) if self.args.use_routing_replay: RoutingReplay.clear_all() # update the cpu actor weight to the latest model self.weights_backuper.backup(\u0026#34;actor\u0026#34;) # Update ref model if needed if ( self.args.ref_update_interval is not None and (rollout_id + 1) % self.args.ref_update_interval == 0 and \u0026#34;ref\u0026#34; in self.weights_backuper.backup_tags ): # 在某些算法中，ref model 的权重也需要被更新，在这一步执行 with timer(\u0026#34;ref_model_update\u0026#34;): if is_megatron_main_rank(): logger.info(f\u0026#34;Updating ref model at rollout_id {rollout_id}\u0026#34;) self.weights_backuper.backup(\u0026#34;ref\u0026#34;) log_perf_data(rollout_id, self.args, extra_metrics=self.weight_updater.pop_metrics()) ","permalink":"https://xiyuanyang-code.github.io/llm/posts/infra/slime/slime-training/","summary":"\u003c!-- generated by tools/sync.py from infra/slime/slime-training.md; DO NOT EDIT --\u003e\n\u003cp\u003e本文，我们将会重点梳理 Slime 中通用的训练组建和框架，包括他如何勾连 Megatron 在 GPU 上进行训练。重心将会放在 training 的一般抽象和构建中，具体的训练算法和一些额外的 trick 将会在后续具体章节给出。\u003c/p\u003e\n\u003ch2 id=\"入口文件\"\u003e入口文件\u003c/h2\u003e\n\u003cp\u003e入口是 \u003ccode\u003etrain.py\u003c/code\u003e,但它\u003cstrong\u003e不会直接导入 \u003ccode\u003eactor.py\u003c/code\u003e\u003c/strong\u003e。\u003ccode\u003eMegatronTrainRayActor\u003c/code\u003e 是经 \u003ccode\u003eRayTrainGroup\u003c/code\u003e 这条链、在\u003cstrong\u003e创建 Ray actor 的那一刻延迟导入\u003c/strong\u003e的。\u003c/p\u003e\n\u003cp\u003e完整的导入链:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003etrain.py(仓库根目录)\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e└── from slime.ray.placement_group import create_training_models    ① 入口只导入 placement_group\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    └── create_training_models(args, pgs, rollout_manager)         ② placement_group.py\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        └── allocate_train_group(...)                              ③ placement_group.py\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            └── RayTrainGroup(args=..., ...)                       ④ actor_group.py(构造训练组)\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                └── _allocate_gpus_for_actor(...)                  ⑤ actor_group.py(创建 actor 时)\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    └── from slime.backends.megatron_utils.actor import MegatronTrainRayActor  ⑥ 延迟导入!\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                        │\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                        └── ray.remote(...)(actor_impl)            ⑦ 包装成 Ray Actor 类\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                            └── TrainRayActor.options(...).remote(rank)  ⑧ 每张 GPU spawn 一个 worker\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e继承关系:\u003c/p\u003e","title":"Slime: 训练主流程"}]