Xじゃないか

\[\newcommand{\c}{\mathcal} \newcommand{\eps}{\epsilon} \newcommand{\co}[2]{{\color{#1}{#2}}} \]

Lec 1

数据越来越多,我们该怎么办?并且,这其中绝大部分 (80%+) 的数据都是 unstructured 的,需要更多数据处理技巧!

显然不能全靠人工。需要自动数据处理技巧!

此外,scaling 除了算法效率提升,更需要硬件结构支持——而 Moore 定律引导下的增长效率小于算法的优化效率!

  • Scale up:增加硬件性能而提速。受 Moore 定律指导。
  • Scale out:并行,增加节点数目而提速。增长速度比算法效率提高更容易,还要大几个数量级。

算了,以上东西都没用!


更大的模型需要更大的 NPU (Neural Network Processing Unit) 才能塞得下。这使得通讯消耗变大。

GPU 并行的类型:

  • 数据 (data) 并行:模型在每一个节点上都有(因此模型的大小受到限制),每个节点分到数据的一部分。在 Forward 时,没有通讯;在 Backword 时,双方需要共享信息,但是这玩意是 non-blocking 的,不需要等待进行完毕。

  • 模型 (model) 并行:数据在每个节点上都有,每个节点分到模型的一部分。不论是前向还是后向,每一个阶段后都需要阻塞式通讯,将信息同步。

  • 可以在模型并行外面套数据并行。

  • 流程 (pipeline) 并行:每个节点处理 pipeline 的一部分。一个节点做完后,需要阻塞式将 output 传输。因此在哪里切割以保证传输量较小是重要的。此外,后面的节点不需要收到所有信息再干活,可以收到一部分信息就开工。这就会产生一个 “bubble”

    NPU1         F20 F21 F22 F23 B23 B22 B21 B20
    NPU2     F10 F11 F12 F13         B13 B12 B11 B10
    NPU3 F00 F01 F02 F03     "Bubble"    B03 B02 B01 B00
    

    其中,F 是前向的每一块,而 B 即为后向。

  • 张量 (tensor) 并行:每个节点分到一部分张量。常见于 GPU 加速矩乘。

  • Sequence Parallism:出现在长文本训练的场合。此时数据要被拆分为小的 sequence。

并行的过程中,能否支持异步运算也是很重要的,不然会被所有节点中最慢的一个卡住。

此外,矩阵乘法中,小矩阵乘法就会使得大部分运算单元闲置。解决方案包括用小核拼接成大核,这样一次运算只会激活部分小核,但代价是需要内部通讯。还有稀疏矩阵乘法,解决方案包括对矩阵压缩等。

Reduce-Scatter:有 \(n\) 个进程,每个进程有数组 \(x^i\)。执行效果是将每个 \(x^i\) 切成 \(n\) 块后,第 \(i\) 个进程获得所有进程第 \(i\) 块信息的合并(所谓的 规约 操作)。它可以被理解为,将所有数组全部按位合并后,每一个进程分发到数组的与部分,

All-Gather:每个进程获得所有数组的拼接 \(\begin{bmatrix}x^1&x^2&\dots&x^n\end{bmatrix}\)。执行后所有进程获得均相同的信息。

All-Reduce:每个进程获得所有数组的合并 \(\bigoplus x^i\)。执行后所有进程均获得相同的信息。

All-to-all:每个进程获得所有进程第 \(i\) 块信息的拼接。

Lec 2

Attention 负责在 各个 token 间交流信息,但是每个 token 本身需要一些更强的非线性性。这就是 FFN 层的必要性:它为每个位置提供了独立的、互不干涉的非线性信息,因此是必须的。FFN 层先升维后降维,相当于是把压缩的信息展开了,是必要的。

Layer Norm 在 Attention 和 FFN 层之前的模式 (pre norm) 会更好一点:这样关键层的输入是归一化的,对它们的压力会更小一点。


来点变种!

ALiBi:早期的 Positional Embedding。在 Attention 矩阵上加上两个位置的间距乘以一个常数超参(\(q_ik_j+m\cdot(i-j)\))。评价为不如 RoPE。

RoPE 的好处:通过对 Positional Embedding 进行插值,可以增大 context length。

原来我以前对 KV Cache 的理解存在一车错误。

KV Cache:在 Decoder Only 模型中,注意力是单向的。这意味着,往后面追加新 token 不改变注意力矩阵中已经确定的部分。

这是否意味着我们可以存储已经计算好的注意力矩阵,然后每次往后面追加一行?并非如此!注意到我们计算注意力矩阵,归根到底是为了确定每层中所有 KV 的值。因此在确定新增 token 时,之前位置的 attention 不需要重复计算——它们的计算结果已经作为 KV 计算完毕了。因此我们只需把已经计算好的位置的 KV 值存储下来,然后往后面 append 新 token 时,只需要维护 embedding,然后计算得到 Q,然后对存储的 K 计算单个位置的 attention,然后对 Q 作权重求和即可。

但是也有问题:KV cache 太耗空间了!总消耗空间可以用下式计算:

\[\text{空间}=\text{Batch Size}\times\text{头数}\times\text{层数}\times\text{序列长度}\times\text{Embedding Size}\times(\text{KV 均需存储}=2)\times\text{量化大小} \]

当 batchsize 很大的时候,cache size 可能已经超过了 model size。

因此为了减少开销,就有如下策略:

  • 首先,朴素的是 Multi Head Attention (MHA):每个头都有不同的 QKV。
  • 然后有 Multi Query Attention (MQA):所有头的 KV 都相同,只有 Q 不同。这样就可以不需要每个头存储不同的 KV 了,头数这个 parameter 就可以被省掉。但这样肯定会掉点。
  • 为了让它效果好一点,折衷的方法是 Grouped Query Attention (GQA),把头分成若干组,共用 Q。一般会 \(8\) 头为一组(我真该消灭你了,真的)。

此外,朴素 FFN 也可以被替换为一个类似 LSTM 的更优秀的结构 GLU。

  • 朴素 FFN 对于算出的结果,维数为 \(d\)\(A\),把它过线性层升维到 \(4d\),过一个激活层(如 ReLU 或 GeLU),然后再降维到 \(d\)
  • GLU 则分别用两个线性层独立升维到 \(\dfrac83d\),然后把其中一个过一个激活函数(例如 Swish 函数 \(\dfrac x{1+e^{-x}}\))后,二者点积之后降维到 \(d\)。Swish 函数引导的部分相当于一个遗忘门,会更优。

Emergent Ability:在训练数据大小达到一定规模后,模型突然就有了很牛的一些能力。

OPT:meta 的早期开源模型。Decoder Only,pre-norm,ReLU-FFN。

LLaMA:Decoder Only,pre-norm,Swish-FFN,RoPE。

LLaMA2:GQA。

LLaMA3:多语言 token、简化 post-training:SFT、Rejection Sampling (RS——用来配合 RL 生成数据的)、DPO。

Mistral7B:Sliding Window Attention (SWA Attention),通过类似 CNN 的 Attention 机制扩张感受野。但是主流大模型都不用,怎么回事呢?


Chinchilla Law:为在固定 computational budget 下跑出效果最好的模型,模型的参数量和训练数据量应该同步增长。由此开始了从卷参数量到卷数据量的新路线。

另一方面,为了降低 Inference 开销,还有的模型(例如 LLaMA)选择为小模型提供远超 Chinchilla Law 的数据量(过饱和训练)。这样做虽然训出来的模型达不到 SOTA,但是它能得到一个性能可以接受,且 Inference 开销极低的模型。


多模态:

有两种主流路线——Flamingo 路线,选择把图像信息 cross attention 到 LLM 里面(类似 Encoder-Decoder 模式);PaLM-E 路线,即 Visual Tokens as Input。

  • Flamingo:图片过 Encoder(预训练且冻结的视觉模型)得到数量不定的高维度特征向量;过可训练的 Perceiver Resampler 得到少量固定长度的视觉特征;然后使用门控交叉注意力 (Gated Cross-Attention——「门控」体现在 LLM 可以自行通过一个可学习的 \(\lambda\) 门决定要不要 Vision 信息) 把 Vision 信息导进去,LLM 本身是冻结的。
  • PaLM-E(如今的绝对主流):直接把图像过冻结 ViT 转成 token 给模型。优势是简单粗暴,缺点是必须 train from scratch。

MoE:针对 FFN 层的改造。把一个大 FFN 替换为多个小 FFN,使用一个 router 来决定分发给哪些 FFN——使用 topK 决策。优点是 Inference 时不用全部激活,效率高;缺点是 expert 间可能存在负载不均衡,因此需要负载均衡损失避免闲置 expert;训练容易不稳定;就算仅有部分 expert 启用,所有东西仍然要上显存。

因为其是仅针对 FFN 层的改造,所以一切 MLP 其实都可以应用 MoE 架构,只是 Transformer 中这玩意占比最大,所以首先在其中应用罢了。在其它领域的应用目前还没有落地。

经过 Attention 后的 embedding 本身掺杂了上下文中的很多语义,可以通过 FFN 层中升维层展开,降维层相当于作查询。


一个神秘的术语:自 \(x_0\) 时刻到 \(x_t\) 时刻的加噪函数 \(q(x_t\mid x_0)=\c N(x_0;\sqrt{\bar \alpha_t}x_0,(1-\bar\alpha_t)I)\) 被称作 Diffusion Kernel。


对 Diffusion 作并行时,因为它本身是一个 Markov Chain 式的降噪过程,所以可以简单作被称作 Patch ParallelismSpatial Parallelism 的方式:把图像切成两半,每个 node 分到半个 patch,这样通信只需要在每一步结尾时进行即可

Lec 3

Communication Primitives

可以定义哪些通信方法?

One-to-One:一对一的通信。最常见的有 Send 和 Recv 两者:前者由发送方发起通信,后者由接收方发起通信。

One-to-Many:一对多的通信。最常见的有 Scatter 和 Gather 两者:前者是发送方将 不同 的 tensor(可以是一个大 tensor 的切片)分别发送给其它所有人(收到切片的可以包含发送方本人);后者是接收方从其它所有人接受不同的 tensor,并把它们拼接在一块,tensor shape 与每个人向其发送的不同(因为拼接了)。

此外,还有 Reduce:接收方从其它所有人接受不同的 tensor,并把它们通过某种运算缝在一块,其 shape 与收到的每一个均相同。

最后是 Broadcast:发送方将 相同 的 tensor 发送给每一个人。

Many-to-Many:

  • All-Reduce:等于 Reduce+Broadcast。所有人得到相同的缝合 tensor。
  • All-Gather:等于 Gather+Broadcast。所有人得到相同的拼接 tensor。

Data Parallelism

有一个中心服务器 Parameter Server (PS) 负责聚合与分发梯度。Workers 使用分配到的数据在本地计算梯度。

  • Replicate & Pull:PS 向所有 worker Broadcast 其组合好的模型。Worker 需要 \(O(1)\) 的带宽,但是 PS 需要 \(O(n)\),其中 \(n\) 是 worker 数目。
  • Push & Sum:PS 从所有 worker 接受计算完的梯度,然后将它们组合。显然是 Reduce。带宽要求同上。
  • 这样之后,PS 使用聚合的梯度更新本地模型,然后开始新一轮计算。

通信的 bottleneck 在于 PS 的通信。能不能去中心化通信?答曰:All-Reduce。

怎么实现?显然一个最粗暴的方式是所有人分别做一遍 Reduce。但是效率很慢,带宽也没有减少。

试试看 Ring-Based All-Reduce!所有人排成一个环,每个人从上一个人处收到结果后,将结果与其存储的信息缝合后,传递给下一个人。带宽即减少到 \(O(1)\)。但是复杂度(准确地说,耗时)还是 \(O(n)\) 的。

还有就是所有人同时跑!耗时确实是 \(O(1)\) 的,但是总 bandwidth 作为代价飙升到 \(O(n^2)\)

来点倍增罢!蝴蝶变换即可。首先相邻配对互相交换,变成 \(n/2\) 段。然后再互相交换,变成 \(n/4\) 段……。这样,耗时是 \(O(\log n)\) 的,同时 bandwidth 即减少到 \(O(1)\)

ZeRO 1/2/3

所有的 GPU 上都要被塞一个模型。塞不下!

考虑 BP 要存哪些东西。假设使用 fp16,则一个参数需要如下的空间存储:

  • 2B 存储 weight。
  • 2B 存储 grad。
  • 12B 使用 Adam Optimizer。

如何压缩?

  • ZeRO-1:解决方案是把 Optimizer State 分布式存储,这样消耗 \(2+2+12/N\)
  • ZeRO-2:把梯度也分布式存储。消耗 \(2+2/N+12/N\)
  • ZeRO-3:把模型也分布式存储。

PyTorch 中已经实现了 ZeRO-3,即 FullySharedDataParallel,“FSDP”。

Pipeline Parallelism

因为每个 node 只塞几个步骤,所以大的模型就塞得下了。如果一次跑一个,会有很大的 Bubble;使用 micro batch pipeline,因为同一个 batch 中模型并没有变化,因此其中数据可以同时在 pipeline 跑,故能减少 Bubble。这就是 Gpipe 的思想。

Tensor Parallelism

矩阵乘法。一个设计得当的算法(如 Attention)应该只需要很少的不可并行操作。事实就是如此:它只需进入 head 的一次 scatter 和出 head 的一次 gather 即可。

Sequence Parallelism

比较复杂。例如 QKV 计算时涉及到跨 GPU 的 QK 配对。

Ulysses 式解决方法:每次进 Attention 前,做一遍 All-to-All,把一个 Head 的 QKV 整到同一个 GPU 上算。之所以这么搞,是因为 FFN 时必须按照序列划分,因此过完 FFN 后,每个 GPU 上的东西是所有 Head 的各一部分东西。这样做有两个弊端:一是 All-to-All 传 QKV 非常慢,二是必须有 #GPUs = #Partition of Sequence = #Heads,难以扩展。

Ring 式解决方法:每个 GPU 上都有 QKV;每一步 Q 不动,KV 作循环,这样循环一整圈后,所有信息就同步了。但是注意到,作 masked attention 时,要计算的 QK 乘法是一个下三角矩阵,如果每个 GPU 分到其中连续的一段下标,则其负载是不均衡的。解决方案包括让每个 GPU 分到的东西不是连续下标、将一头一尾的东西配对以负载均衡等。

令序列长度为 \(n\)、hidden size 为 \(h\)、GPU 数目为 \(P\)。对比二者:

  • 通信量。

Ulysses 本质上是对所有数据的重排,因此总通信量为 \(O(nh)\)

Ring 会进行 \(P\) 轮,每轮所有设备都在同步传输其上的 QK,因此总通信量为 \(O(Pnh)\)

可以发现,GPU 数目增多反倒会使得 Ring 的场合的通信量增大,因此极度依赖计算重叠以提高效率。

  • 通信方式。

Ulysses 是带宽很糟糕的 All-to-All。Ring 直接作 one-to-one 通信即可,非常简单。

  • 内存消耗:相近。

  • 模型结构泛化性:Ring 更好,因为 Ulysses 受限于 Head 数目——多于 Head 数目的 GPU 是无效的。

  • 输入长度敏感性:Ulysses 更好

Expert Parallelism

mks 极度厌恶这玩意,因为它无法在编译时决定,而要在每次 inference 时动态决策,为网络带来了很大的不确定性和极度的负载不均衡。但是效果很好(?)

MoE 的 mixture:

  1. 模型级别 Mixture。在最外层套上 router,合并多个子模型的输出。是有很长历史的方法,在 CNN 时代就已经作为 ensemble 方法而广为人知了。
  2. Block 级别 Mixture。每个 Attention Block 前面加 router。
  3. FFN 级别 Mixture。只对 FFN 加 router。

实验证明,model 级别的 mixture 基本上要更胜一筹。与此同时,更复杂的 router 设计无济于事,简单的 linear router 足矣。

此外,model 和 block 级别都只能将一个 sample 作为整体进行 router (sample-level routing);但是 FFN 级别可以达到更精细的 token-level routing,把每个 token 分开 routing(反正它们彼此是独立的)。两者似乎区别不大。

最后,可以尝试 Hybrid mixture,结合上述 mix 方式。在 math 和 code 上好用。

还有一些更神奇的做法。例如,可以对现有模型作聚类,合并出几个核心模型后,使用 router 决定要被发送至哪个模型。


MoE 也有很多技巧。比如说,可以加一个固定启用的 shared expert 当作 base,这样可以让负载更可预计。还可以采用一个 pyramid design,浅层的 expert 更少,深层的更多。还能再加一点 loss 让各个 expert 被激活概率均等。

此外考虑 routing 策略。Top-\(1\) 明显是极度不均衡的。Top-\(K\) 可能也没好到哪去。比较邪教的是作一遍 Hash 然后随机丢到一个 expert 里面。还有一种策略是让每个 expert 自行选择 token,这样可能可以更好 balance。


如何训练 MoE?一种思路是直接训 sparse model。还有一种策略是先训一个 dense model,将其转化为 sparse 后,再进行进一步训练。从 dense 到 sparse 可以选择将 FFN 层复制若干份(参数相应增加)或者拆碎(参数数目不变)。

Sparse Upcycling(升级改造) 就是前者,拆分后 router + MLPs + weighted sum。这个很简单,但是问题是 expert 彼此相似,这一点其实不太棒;然后 Sparse Splitting 就是后者,有各种神秘拆法,例如随机拆、聚类、找重要向量或是找公用神经元等。还有 Sparse Dropout 策略,一点点扩大 FFN 的维数和 expert 的数目;等等等等。


在前向和后向过程中,MoE 基本上很需要 All2All 的通讯。其流程如下:

  1. 每个 GPU 内部进行 layout transformation,即预排序,将要发给同一个 expert 处理的 token 排到一块去。
  2. 作 All2All 分发,把同一个 expert 处理的 token 集中到同一个 GPU 中。
  3. 每个 expert 独立 FFN。
  4. 再次 All2All 发回。
  5. 再次 layout transform 重排。

Expert Para 与 Tensor Para 的区别:前者每个 GPU 上分了所有 expert 的部分数据,后者每个 GPU 上只分了部分 expert 的部分数据。两者结合时,每个 GPU 计算出部分 Attention 结果后,通过 All reduce 进行合并。但是这样做后,每个 GPU 上都有完整的数据,此时产生了冗余,可以各自 drop 掉半份数据,减少 All2All 通讯量。

Hybrid Parallelism

前面的 Para 每一种各自可能很优雅,但是放到一块呢?例如,2D Para 就是一个糅合 Data Para 和 Pipeline Para 的经典模式,其中 GPU13、GPU20 各自为一个组,组间作 DP,组内作 PP。复杂的甚至都到了 4D Para。其它还有外侧 Pipeline 内侧 Tensor 等模式。

但这甚至只是个开始。GPU 本身在硬件上也有分组,组内通讯是快的,组间是慢的。此时如果要作 Sequence Para,就可以在组内用 Ulysses 式高带宽高效率算法,组间用 Ring 式低带宽低效率算法。而 Tensor Para 因为通信量很大所以最好在同一组内实现。

这种种考量,有没有自动化大手子捞一捞,写个算法自动找到最优的布局?有工作尝试进行一些自动化搜索。但是自从 Expert Para 来了后,数据流不再能预计算了,目前还没人能救。

Communication Cost

通讯量 matters。如果能压制传输信息的 size,并行效果就能好很多。

所以,一个思路是对梯度作 pruning,把某些位置干成 \(0\);或者作 quantization,把 32bits 压成 8bits。

前者的思路例如,只发送 topK 的梯度,其它部分裁掉不发。当然裁掉的东西也没真的丢掉,可以留在本地存着,攒到 topK 后再打包发出去。但是发现这么做效果不好,原因是因为 Adam 中更新梯度是通过动量法进行的,越久远的梯度信息就会在总更新中产生更大的分量(虽然这个分量最终是收敛的,因为 \(\alpha<1\));但是本地攒着的结果中,不同时刻的留存梯度的地位相同,因此在本地也作相同修正即可。

还有就是学习率 warmup,小学习率能避免积攒误差过大;或者对 sparsity 也 warmup。


虽然每个 GPU 上的梯度都很 sparse,但绕了一圈后合起来不再 sparse 也是很有可能的。所以也有人用的低秩分解。

或者还有夸张点的用 1bit 的量化,其它东西全放到误差里面。

更激进的如 TernGrad 直接用 0/±1 的量化,但是引入随机性,让量化结果的期望与被量化值(\(\dfrac{g_i}{\max g}\in[-1,1]\))相等,这样就不需要额外的修正机制了。但缺点也很明显:方差太大了!


传输过程中,Bandwidth 是比较容易提升的(包括前述梯度压缩,以及硬件升级),但 Latency 是难的——传输受限于距离和信号拥塞等。

为了减少 Latency 的影响,可以采取 Delayed Gradient Averaging 的策略,即本地先算着,同时各 worker 的信息也在传输并平均。这样 Latency 就不会成为 bottleneck。

NCCL

实现数据收发的库!虽然有各种实现方式,但似乎绝大多数都可以以 Ring 的形式完成。例如 Broadcast 就可以一个传一个,「单向 ring」也就是链。虽然理论上只有在上游收到后才能发,但上游可以不收完就开发,即分块发送,就提高了收发重叠度。All reduce 同样可以(但前提是这玩意具有可加性)。

其具体步骤包括 topology detection,获取部署环境的布局;然后做 graph search 获取最优路径;然后使用各种连接方式,在硬件建建立通道;最后调 CUDA 内核并开跑。

除了 ring 以外,NCCL 还用了很多 tree 的连接。但是非叶子节点的带宽有限,解决方案是两棵树一组,一棵树里的叶子是另一棵树中的非叶子,二者即更加平衡。NCCL 也会死锁,这需要在写代码时着重处理。

Lec 4

Interconnection Network:系统内部的连接。互联网不属于这一范畴。

一个 Topology 是一个 interconnection network 连接着一众 processor 和 memory。设计的目标:

  • 低延时。
  • 高带宽。
  • 对于共享的稀缺资源(如 buffer、link 和 logic)能够高效利用,以减少占地与功耗。

Interconnection Network 的分类:

  • On-Chip Network (OCNs or NoCs)
  • System/Storage Area Networks (SANs)
  • Local Area Networks (LANs)
  • Wide Arae Networks (WANs)

例如,芯片上连接各个 Processor 和 Cache 以及 Memory Controller 的就是一个 NoC。SAN 的场合,往往是多个有独立内存、IO、Processor 和 Cache 的部件间的通讯;更大的 LAN 就需要 Bridging 和交换器了。

Off Chip Network 受限于芯片引脚的输出带宽,同时延时受到长程线缆的限制。反之,On Chip 则可以用各种金属层和布线实现很高的 bandwidth,且因为路程更短 latency 也低,但是没法 scale。


不同需求需要不同的布线方式。例如,CMP 架构所有核间彼此互联,很容易实现动态 All2All 通信;MPSoC (Multi-Processor System on Chip) 架构更利好静态网络;至于 DNN Accelerator,这个更关注 communication,结构一般是一个 Fat Tree 模式。


Scalability Matters。

  • 早期连接单位是 总线 (bus),其可以连接多个元件。早期总线是独占的,只要上面有任两个在通讯,其它就无法通讯;后来也实现了只要传输路径不重叠即可的其它形式。虽然很容易 scale,但是带宽太低了。
  • 后来也有 All-to-All 的东西,但是根本没法物理布线,没法 scale。

如何在 NoC 上通信?有两种模式:共享内存(常见于 CMP 架构),实现简单但常常出现不同步;信息传递(常见于 MPSoC 架构),需要在多个 block 间通信。


NoC 的结构:每个 Tile 上包括对外开放的 Router 作为接口,以及内部的 Network Interface、各级 Cache 与 Core。一般将其抽象为单个 Router 足矣。

Topology 描述 Router 间互联的方式。Router 会选择信息被路由到哪条路线上。Flow Control 试图让信息不会拥塞。

但是,Network Design 复杂的一点在于,资源常常是分布式的,难以有中心化的调控;流量常常是无法预测的(但是模型的场合,除了 MoE 这种东西以外,其它基本上是可预期的)。


Performance:Latency,Bandwidth,每个 link 的消耗,每个 router 的消耗,消耗的面积。

Latency Offered Traffic (bits/sec) SaturationThroughput

可以发现,随着 traffic 逐渐增大,Latency 先缓慢上升,后期急剧上升。Latency 变为无负载时的三倍的时刻被视作一个有价值的参数,即能承受的最优限度,称作 Saturation Throughput。

Topology

决定了实现复杂度也即 cost,同时也决定了 performance。

Fixed Topology:在设计时即固定的 Topology,对某个算法可能是最优的。缺点是无法改变,且无法动态适应应用中与数据相关的负载不均衡。

如何衡量一个 Topology 好不好呢?有以下几个尺度:

  • Degree:一个节点 (node) 所拥有的端口 (port) 数目,衡量其可以拥有的 link 数。高 Degree 会意味着物理面积增加且耗能增加。可以衡量成本。
  • Bisection Bandwidth:所有将网络对半(或接近对半)分后的最小割(带权,因为各通道的带宽可能不同)。衡量最糟糕情况下的通信瓶颈。其可能具有误导性,因为没有考虑实际的路由算法和流量控制——这可能会导致对分带宽没有被充分利用。可以衡量带宽。
  • Diameter:网络中任两点间最短路径(以链路跳数衡量)的最大值。可以衡量延迟。

在运行时,有以下参数:

  • Hop count:链路跳数,正在通讯的对间经过的 link 数。受实际应用与映射影响。Average Hop Count 是所有合法路径的平均值。
  • Channel load:信道负载。经过一个 link 的流量。最大 channel load 决定了吞吐量。
  • Path diversity:某对节点间的最短路数目。可以被路由算法利用。决定了容错度。

Fully Connected 有极高的 Bisection Bandwidth 和极低的 Diameter,但拥有极大的 Degree,且因为布线原因难以 scale。

Bus 有极低的 Degree 和 Diameter,但同时有极低的 Bandwidth,故难以 scale。

如何让 Bus 能 scale?可以使用 Hierarchical Bus,对不同速率需求的设备提供不同级别的总线;或者 split bus,在主设备发出请求后,非分离式总线会在从设备(即受到请求的设备)准备回应期间持续占用总线,但分离式总线则会搁置,等到从设备准备好后再以新主设备的身份进行回应。


Topology 分为两种。

  • Direct:所有节点都是 traffic 的起讫点,同时也可以作为 router 中转。
  • Indirect:router 和 terminal node 是有区别的,前者只中继,后者只收发。

\(k\)-nary \(n\)-cube:有 \(k^n\) 个节点,\(n\) 是维数,\(k\) 是每维上节点数目。连成超立方体的形式。

按照它是否带循环,其有 mesh(不带)和 torus(带两种)。

torus 的数据:

  • degree:\(2n\)
  • bisection BW:\(2k^{n-1}\)
  • diameter:\(nk/2\)
  • avg distance:\(nk/4\)。(长度为 \(k\) 的环上,随机两个点期望距离是 \(k/4\)

ring 是 torus 式的 \(1\)-cube。好处是便宜,坏处是延迟高、难以 scale(BW 是常数)、没有 path diversity。

一般单称 torus 时,指的是 torus 式的 \(2\)-cube。好处仍然是便宜,此外 path diversity 也更高,还具有对称性和更多的邻居。坏处是 links 不一样长,并且布线也更困难。

mesh 的数据:

  • degree:基本上是 \(2n\)
  • bisection BW:\(k^{n-1}\)
  • diameter:\(nk\)
  • avg distance:\(nk/3\)

单称 mesh 时,也指的是 mesh 式的 \(2\)-cube。好处多了一条实现容易,但坏处也多了一条是不对称,此外居中的连接也可能成为 bottleneck。

Folded Torus 是一种更加复杂的 Torus(很遗憾,没有 AI 能够写出来可以渲染的代码。我们的 AI 确有问题!),以连接长度翻倍为代价,一定程度上更易于布线,同时所有连接长度全部相等。缺点是还是没那么容易布线。


如何减少 hop count?这是可能的,但会伴随着 switch 的 radix 增加。

还可以专门设置一些定制的长线 link 和对应的 switch,但是应该加在哪里呢?

ST Spidergon:这种思想的产物,在一个 ring 上的对跖 (zhí) 点间增加了高速连接。实际实现时还是会尽量把它摆成 mesh 的形式(因为占地面积小)。


maximum throughput 是 maximum channel load 的倒数,后者可以被 bisection BW 刻画。


来点 indirect topology。crossbar 是电路中常见的布线方式,所有节点都与其它点相连,同时有低延迟和高带宽,在 GPU 中使用。但缺点是占地面积和功耗是平方级别,布线很昂贵,且交叉点实现比较困难。

S S S S D D D D

上图,S 是 source,D 是 destination,每个交叉点都是一个 router。不计算 router 消耗,则其 degree 和 diameter 都是 \(1\)(有点太投机取巧了吧!)

另一种更优雅的方式是 FFT 的 butterfly 结构。一个 \(k\)-nary \(n\)-fly 包含 \(k^n\) 个 terminal node,每个 switch 包含出度与入度各 \(k\) 个,一共有 \(n\) 层。每层里面有 \(k^{n-1}\) 个 switch,每个 switch 内部是一个 \(k\times k\) crossbar。这样 diameter 是 \(n+1\)。缺点是没有 path diversity。

Beneš 结构是一种优化后的 butterfly,是两个 butterfly 背对背连在一起(所以被称为 back-to-back butterfly),存在 \(n\) 条 alternative route。

shuffle network 是任两层布线相同的 butterfly,也被称作 isomorphic butterfly。

flattened butterfly 是 butterfly 的实际布线产物,长得跟个 mesh 一样。似乎万事万物都能转成 mesh 结构。

fat tree 是二叉树结构,也是最广泛应用的结构。所有的叶子是 terminal node、非叶节点是 router。但靠近根的 link 会有更高的负载,因此实际实现时在上层还是会使用类 butterfly 结构。

Routing

如何设计路径?贪心、随机(不回头)或自适应。哪种最优?要看具体 task 来定。

例如,有一个 \(8\)-ring,其上的 traffic 是 tornado,即 \(i\to(i+3)\bmod 8\)。此时最短路会造成堵塞(因为所有人都在同向移动),因为一个 link 可以支持双向同步通信,所以让一些人回头走较长路径反倒会更优。

Routing Algorithm 也有分类:

  • 确定性的 (deterministic):\(x\to y\) 总是走同一条路。
  • 无知觉的 (oblivious):寻路时不管实际情况(例如,拥塞)。deterministic 是 oblivious 的一种。
  • 自适应的 (adaptive):寻路时管实际情况。实际情况通过 link availability, buffer occupancy, history of channel load 等方式衡量——但是这些信息常常无法完整获取。

例如,butterfly 寻路的方式是 minimal 且 deterministic 的。

mesh 的场合呢?一种策略是 XY routing,即总是先走 \(x\) 轴再走 \(y\) 轴。缺点是 mesh 提供的 path diversity 完全没有用上,且 load balancing 很差。有 XY 就有 YX,还可以把两个结合在一起,每次随机选一个。这听起来很对,但是……会有死锁!这么搞后有可能出现环状的四个 link 全被堵住!除了死锁还有 活锁,即转圈圈不出去。

一种想法是 Valiant's routing algorithm,随机一个 \(d'\),先发到 \(d'\) 处中继然后再发到目标。好处是增加了随机性、平衡了负载,坏处显而易见——它不是最优的,且破坏了 locality!(它是 non-minimal 且 oblivious 的,但也可以被改造为 adaptive 的)。在 Beneš 的场合会比较优雅。

ROMM:Valiant 的改进,限制 \(d'\) 在起讫点围出的矩形中,由此达到了 minimal。


来点 Adaptive Routing。显然,局部最优不一定有全局最优,还可能会出现各种问题(例如不断绕路绕出 livelock 出来)。

如何感知拥堵?可以使用 backpressure 机制:拥塞的 link 会自动告诉前驱这一事项。


Routing 的实现方法:

  • Source Routing:数据从发出的那一刻就把完整的路径一起打包发出去。优点是每一跳都不需要额外的 routing 延迟,hardware 机制也可以被省略,更容易适配不平凡的 topology;缺点是每个数据产生地都要存储完整的 topology,同时每次传输都要多好多 routing bits,同时难以对 congestion adapt。
  • Node-Table Routing:每个 router 有一个 table,对于每个来源,将其投放到固定的目标去。好蠢啊。
  • Combinational Circuits:packet 仅仅携带着终点坐标,每个 router 根据终点和当前状态决定下一步目的地。不论哪种类型的 routing 都可以这么干。

Deadlock

来看看死锁问题。首先,如何判断死锁?假如有一个排他性资源 \(A\),它可以在被某个 agent \(X\) 拿着的同时,\(X\) 在等待另一个资源 \(B\),那么 \(A\) 就依赖 \(B\),依赖关系成环即有可能死锁。

正因如此,我们希望 resource dependence graph 是 DAG。

如何解决 deadlock?

  • proactive:保证永远不死锁。几乎所有现代 网络 都这么做,也就是前述把 dependency graph 搞成 DAG 的模式。
  • reactive:允许找到死锁后自行恢复。
  • subactive:周期性检测并强制解锁。

recap:及膝盖上,提到主流策略是不管 deadlock,假设它不存在。但区别在于其 context 是「操作系统」,在该场合下 deadlock 是不频繁且威胁不大的。然而在 network 的场合,deadlock 一旦出现就有很大威胁,所以必须在设计阶段就予以排除。

turn model:在 mesh 上 ban 掉一些 turn 的方式。例如,XY model 和 YX model 分开用都没问题,但是合在一起就会出现被称为 01 turn 的经典死锁现象。所以解决方法是把逆时针和顺时针的环上各自 ban 掉一种 turn 的方式。当然,不是随便 ban 都行:有一种叫做 six turn 的错误案例,可以拼出一个八字形的环来。

另一种方式是 resource (channel) ordering,即为所有 link 编号,一条 path 必须满足编号的单调性。如 XY model 和 turn model 等都是这种方式的子集。(相当于人工编了一个拓扑序出来)

如何绘制 Channel Dependency Graph (CDG) 呢?把所有的 link 当节点,向其可以走到的后继 link 连边即可。


还有另一种思路是准备多个并行的 Channel,然后把某些 channel 设作 dateline,经过一次就令时刻加一,并行 channel 中某一部分就被禁用了。这样让死锁就不至于绕过多圈。这种手法叫做 virtual channel,可以得到 extended CDG。

这样做的一个问题是假如没有死锁,VC 就是浪费的。解决方案是 Overlapping Resource Class,令 class 0 从所有 channel 中从前往后取,class 1 从后往前取,但是第一个只能 class 0 取,最后一个只能 class 1 取,保证每个 class 至少有一个即可。


当然,就算有环,一种 routing 仍然可以是 deadlock-free 的,只要环存在某种至无环子图的退出机制。这被称作 escape path 或者 escape VC。

Lec 5

展开来讲 escape VC。此时的 VC 一般是不启用的,但是一旦产生死锁,VC 就会被启用,提供一份额外的资源以打破死锁。只要每个环上都有一个 VC,就可以保证跳出。

但是,光有不行,还得能用。一套精细的系统被设计:

  • 一个数据包在请求输出时,可以请求该端口上所有可用的 VC,包括常规 VC 和 espace VC。
  • 为了保证 escape VC 被正常启用,要使用例如轮询等机制,保证一个请求 escape VC 的数据包不会被永久忽略。
  • escape VC 不是单向的:一个数据包,只要它仍处于最小路径(即每一步都离目的地更近的路径),就可以自由在常规路径和 escape VC 间来回切换,保证灵活性;但是,一旦它开始绕路,之后又进入了 escape VC,为了保证不出现死锁,就必须在 escape VC 上走到结束。

另外还可以使用 bubble flow control 的方式来解决死锁。

一个环上,只要有一个空,就不会死锁。但是如果有两个空(所谓的 bubble),就可以再插一个。因此一个激进的策略是 BFC Injection Rule,即只有在存在至少两个空时,新任务才会被插入——但是这效率显然会受影响。不那么激进的策略是不插 critical bubble,即那些会堵住后面一长串内容的 bubble。


现在再来介绍 reactive 解锁的方式。死锁不常见,但是一旦出现就需要改正。

  • 首先要检测是否有 deadlock。常见做法是 timeout 太久就认为出现 deadlock——但这会出现假阳性,把活锁当成死锁。
  • 然后要处理 deadlock。
    • 回退 (regressive) 式:把造成死锁的包干掉。会引起丢包。
    • 前进 (progressive) 式:选择一个幸运包不断往前发,路上的其它包全部放到临时 buffer 里。

先看看死锁检测。设计时即在每个节点上放计数器,如果上面的包超过某个可设定的阈值后还没被发出,就认为存在可能的死锁,要去尝试探测之。探测的方式是发一个探针,沿着依赖链不断传播,如果传回发起者了,就确认死锁出现了;反之,如果没传回,则认为不是死锁。

处理 deadlock,有一个想法是 static bubble,即在一些关键节点上放一些静态的 bubble,检测到死锁后再启用以处理之。还有一个想法是,出现死锁必然有上一步,不如大家各退一步,死锁说不定就解开了。所以发一条指令出去同步环上所有节点的计时器,然后一声令下大家同步回退一步。

当然,有时候一步不够,那就得再退(解决方案:每退一步就发一个探针检查还锁不锁,并在仍锁的场合同步下一次回退)。光退还不行,死锁解开后,要再换一种方式,不能重蹈覆辙。


最后还有 subactive。包括时间到了就随机乱发,说不定就绕出死锁了之类的,课上没详细提。


除了相互依赖导致的死锁外,还有另一种 protocal deadlock。具体而言,在接收方收到传输后,其需要回复一个「收到」信息给发出者。但是如果其在之前还发了若干个请求,则这个「收到」就会被堵在半路传不回去,一边等着回复,另一边回复传不出去,就死锁了。

解决方案是为请求和回复分别使用不同的 virtual channel,此时称作 virtual network。


最主流的死锁 handling 方式还是主动避免。另外两者 mks 声称只在论文里见过。

Flow Control

在 topology 和 routing 都决定后,flow control 决定如何为请求同一份资源的数据包分配。

首先,一条消息 (message) 被切分为多个数据包 (packet)。每个 packet 中包含一个包头 (header) 和一个负载 (payload),前者包括控制信息,如路由信息 (route) 或在原始消息中的顺序 (seq#) 后者则是真实要传输的信息。

packet 又会被继续切分为更小的单元,称为流控单元 (flow control unit, flit),是网络中进行 flow control 的基本单位。flit 分为三种类型:

  • head flit:包含完整的 route,可以用于寻路。在开辟出路径之后,剩余 flit 即跟随之。
  • body flit:主要承载数据。
  • tail flit:告知 packet 已经传输完毕,并提供校验机制等。
  • 此外,如果整个 packet 太短,那也不会硬切,而是直接用一个 head-tail flit 全部承载。

一个典型的 flit 结构包括:

  • Route*,即路由信息。只有 head 和 head-tail 的 flit 才有。
  • Type,标记了是哪种 flit(head/body/tail/head-tail)。
  • VCID:指明当前在走哪条 VC。
  • 若干 Phit,即物理单元 (physical unit),是物理链路上单周期可传输的数据量,与引脚数/导线数相关。一个 flit 可能需要一个或多个 Phit 传输。

Off-Chip 的场合,消息大但引脚少,Phit 很窄,因此 flit 往往比 phit 宽,需要多个时钟周期串行发送。

On-Chip 的场合,导线资源丰富,Phit 可以很宽,一次性传输整个 flit。此时往往有 Flit = Phit,且因为通信量小,直接有 message = packet。

举个例子。假如要发一个 64B 的 cacheline,flit 的大小是 128b = 16B,但是因为还要存控制信息,所以需要在开头补一个 head flit,共 5 个 flits。至于对 cacheline 的请求,因为量很小,直接发一个 head-tail flit 就可以完全解决。

flow control 即可以从三个尺度进行:message-based,packet-based 和 flit-based。


首先介绍 message-based,即 circuit-switching 方法。它类似于老式电话接线员,通过发送一个 setup probe 的方式,预留整条链路,一旦建立后,从源到目标所有的资源全被占用,传完就断开。

  • 优势:传输延迟固定且有保障,不需要 buffer(因为独占性,没有竞争)、router 简单。
  • 劣势:建立延迟高(因为在建立期间也不能有其它人使用资源)。这是其最核心的缺点。

现在已经不常用了。


然后是最常用的 packet-based。它允许在 link 间 interleave,因此利用率更高。但同时也需要 buffer 以等待输出 channel 空出来。

其有两种技巧:首先是 store-and-forward,以整个包为单位传输,head flit 只有在整个包完全收到之后才能继续 forward,在这之前都必须在 router 等待。这会导致每一跳的 latency 高。

那么自然可以想到,为什么 head 就必须等 tail,完全可以直接 pipeline 起来。于是有 virtual cut-through,只要下一个 router 有空间塞得下整个 packet,就可以继续 forward(避免一个包发了一半后面的卡着动不了了)。


更进一步就到了 flit-based,只要下一个 router 有空间塞得下单个 flit,就可以继续 forward。这样每个 router 的 buffer 就不需要是整个 packet-level,可以是 flit-level 的。

其同样有两种技巧:首先是 wormhole,其中所有链路按照 packet 为单位分配(一个 packet 传完前,这个 link 都被占用——不管这个 packet 的第一个 flit 已经跑了多远)——带来的后果就是,只要 head flit 被阻塞了,后面的 flit 一个也动不了,且因为一个 package 的所有 flits 可能长长地拖在一众 router 上,所以这可能导致多个 link 同时阻塞 (head-of-line blocking, HOL blocking),非常依赖与 VC 的协同,也即第二种技巧,virtual channel flow control。

VC flow control 也有它的问题。例如,多个包的 interleaving,假如采取 fair interleaving(轮流传输)的场合,两个包的延迟都会上升;因此要根据数据传输的具体应用,来使用合适的 interleaving 方法。

链路分配 Buffer 占用 存在问题
Circuit-Switching message none 建立延迟高
Store-and-Forward packet packet head 等待 tail
Virtual Cut-Through packet packet 有 bubble,buffer 大
Wormhole packet flit HOL
VC flit flit interleaving

Flow control 的结构如下:

graph LR D[内核对外的输出 buffer] --> C A --> C[arbiter] A[外界输入 buffer] --> B[对内核的输入 buffer] C --> E[对外输出 buffer]

这样就涵盖了外对内、内对外、外对外的多种方法,被称作 arbitration 模式,结构被称作 arbiter,它决定了何者占用对外的输出口。

如果 buffer 满了咋办?可以直接 drop 并回传 NACK 指令或 timeout,请求 re-transmit,不需要额外的 buffer。坏处是回传代价大。还可以做 misrouting,在 ring 的场合,塞不下就直接传出去,无非多转一圈(bouncing)罢了。好处仍然是不需要额外 buffer,坏处就很多了,包括 non-minimal,同时可能会让发送时出现乱序,且还有可能出现活锁。最后就是耐心 wait,给前一个路由器 backpressure 一个信号,让它也等着。

但是问题是,backpressure 有传输、处理、关闭流程的延迟,传回去的时候可能已经有传输过程中的 flits 了。因此要预留阈值,不能等全满了再发。同理,等后面的 router 把积攒的数据发完后,同样要发送信号打开前一个 router,这同样也需要经历传输、处理、打开的流程,因此也要留出阈值,不能在这段时间内后一个 router 已经把攒的东西处理完、出现闲置。

另一种思路是 credit-based flow control,即上游记录下游可用的 buffer 数目。下游每发出一个包,就给上游传信增加上游的 credit,同时减少自身的 credit(因为下游多了一个包)。好处是所有资源都被完整利用了。坏处是发了一车信号,同时周转 credit 时会产生额外的闲置,可以与 backpressure 的 threshold 机制结合使用。

Router Microarchitecture

理论讲完了,来点硬核的。

graph LR %% --- 样式定义 --- %% --- 节点定义 --- flit[FLIT] %% 占位符用于输入和输出箭头 out1[ ] out2[ ] %% 主路由器容器 subgraph Router %% --- 布局控制:定义所有节点和子图 --- subgraph IB1 [Input Buffers 1 from outside] subgraph VN0 ["Virtual Network 0"] vc0["Virtual Channel 0"] vc1["VC 1"] vc0 ~~~ vc1 end subgraph VN1 ["VN 1"] vcm["..."] vcn_mid["VC n"] vcm ~~~ vcn_mid end VN0 ~~~ VN1 end subgraph IB2 [Input Buffers 2] vc1_b["VC 1"] vc2_b["VC 2"] vcn_b["...<br>VC n"] end rc[Route Compute] vca["VC (Virtual Channel) Allocator"] swa["SW (Switch) Allocator"] xbar["X<br>Crossbar Switch"] %% --- 强制布局:使用不可见的链接 --- %% 1. 让控制逻辑块垂直堆叠 rc ~~~ vca ~~~ swa %% 2. 让两个 Input Buffer 块垂直堆叠 IB1 ~~~ IB2 %% 3. 让每个 VN 内部的 VC 垂直堆叠 vc0 ~~~ vc1 vcm ~~~ vcn_mid vc1_b ~~~ vc2_b ~~~ vcn_b end %% --- 真实连接关系(可见的箭头) --- flit -- FLIT In --> IB1 IB1 <--> rc IB1 <--> vca IB1 --> swa swa <--> xbar IB1 --> xbar IB2 --> xbar xbar -- Output 1 --> out1 xbar -- Output 2 --> out2

因为 mermaid 能力有限所以只能画出来上面这个比较挫的东西出来。

实现 router 的流程如下:

  • BW:Buffer Write。读入 flit,将其存储在每个 channel 对应的 input buffer 里面。
  • RC:Route Compute。根据 flit 中的 routing 信息,与 Route Compute 单元交互,计算发包目的地。
  • VA:VC Allocation,决定每个 input VC 应该走哪条 output VC 发出去。
  • SA:Switch Allocation,把输入端口的东西定向到输出端口去。
  • BR:Buffer Read,从输入 Buffer 读取数据。
  • ST:Switch Traversal,过 All2All 的 crossbar switch 传输到输出端口去。
  • LT:Link Traversal,沿着链路出 Router。

首先考虑 Buffer。需要多少 Buffer?要兼顾表现和正确性。VC 的数目则是要与 deadlock avoidance 的算法挂钩。Buffer 可以是每个 VC 私有的,也可以是公有的——只要每个 VC 有至少一个私有 buffer 保证不阻塞即可。

然后考虑 Routing Logic。首先是 Source Routing,那么 RC 模块就可以很简单,读取 Route 的第一个然后把它丢掉即可,所有的计算都在发信地做完。或者可以在每个 Router 维护一个表,好处是一切 Routing 都可以通过改表实现,坏处是所有 Router 都要知道完整的网络信息,需要很大的空间存储。

然后是 switch 和 VC allocation。这两个逻辑在 NoC 上往往是决定 cycle time 的 metric,因此它必须快,和/或可以并行。

arbiter 如何保证公平呢?有这几种尺度:

  • weak fairness:只要所有 request 最终都会被服务即可。
  • strong fairness:所有 request 被等频率地服务。
  • FIFO fairness:作 FIFO。

你可以在每个 router 都作 strong fairness:所有的 requester 等概率被服务,但是这样做在全局会更偏好短距离的传输——它们经过的 hop 数更少。实际使用 round robin 机制,它与电路很好地兼容。

上课讲了一大坨 matrix arbiter 的东西。我不想听,也不感兴趣。

看看 crossbar switch 咋做的。有两种模式:

  • Mux 模式:所有的 switch 在输出的地方处理。需要更多的 area 和 power。
  • Matrix 模式:switch 在矩阵的每个节点处处理。需要更仔细的设计。

link 也有说法,但是太物理不管了。

最后,多个传输如何 pipeline?注意到 RC、VA 这两个步骤只有在 head flit 里才会被执行,body 和 tail 直接继承 head 的结果即可。


Router 还要与计算单元进行交互。这方面存在很多 interface,基本上就是两种技术模式:消息传递或共享内存。

Lec 6

Nvidia GPU 演进:塞更多的 transistor。

以前 chiplet 基本上全是 2D/2.5D 的。现在趋势是更加先进的 3D 集成、更加大规模的继承。

标准服务器形态的 chiplet (PCIe/OAM):目标是高性价比实现大芯片。

定制服务器形态:目标是高性能、高可靠地实现,迈向服务器级别 Mole 定律。


面积过大的芯片因为良率低、光学性能的问题难以制造。所以解决方法是把单硅片 (monolithic) 拆分为多芯粒 (chiplet) 系统,用小芯片拼出大的来。但是传统方法要加一个 interposer 进行封装,这会使得成本超过 monolithic。假如能降低封装标准,成本就能低于 monolithic。

当然,先进封装也能提供更高的互联密度。例如,早期的 2D 封装就是往基板上加了两片 die;2.5D 多加了一层 RDL;更先进的多了一个 interposer die;最强大的则是 3D Hybrid bound。

这就是标准服务器形态:在有限大小和功能的条件下,实现最佳的芯片。


定制形态下,可以打破标准时的面积和继承形式约束,提高 scale-up 域规模。

  • scale-up:在同一个机柜/rack/芯片级别,互联很多个子单元;
  • scale-out:拉出机柜,使用光纤互联。

现状:

小计算(芯片被 ban 了)。

小内存(High Bandwidth Memory HBM 被 ban 了),现在堆叠 3D DRAM 的技术还有很长的路要走。(但是使用这种技术可以让两者间的带宽大)

因此就只能优化互联了,即用更强的互联把小计算拼起来、把小内存拼起来。

但也有问题:NVLink 是私有协议;需要强大交换机(但我们也没有)。

现在的 Topology 策略仍然存在问题。例如,在多 chiplet 互联系统中,有很多流量都是通过某个 chiplet 作为中转的 inter-chip traffic,真正的 intra-chip traffic 占比很小。此外,chiplet 互联也是一种网络,也会遇到各种死锁问题。

解决方案是为 intra-chip 流量提供专门的高速通道;


部署的 LLM 服务,其分为两个阶段:

  • prefill,计算 prompt 部分的 attention,metric 是 TTFT (Time to first token),计算是瓶颈,指标不敏感,国产设备即可。
  • decode,不断 append 新 token,metric 是 TBT (Time between token),带宽是瓶颈,指标要求高,还要发展。

DeepSeek 使用了一种名叫 MLA (multi-head latent attention) 的机制来优化 attention。

首先是原始 MLA,也称 preceiver:

它定义了一个 \(m\times d\) 的可学习的参数矩阵 latent array,其中 \(m\) 是潜在向量数量,通常远小于 \(n\)。Q 使用该 latent array 过一个权重矩阵 \(W_q\) 得到,而 K,V 仍然从原始输入 embedding 得到。这一步是在作信息压缩,使用潜在向量概括了原始序列的庞大输入。

过了上述 latent array 与 input 的 cross attention 后,得到的长度为 \(m\) 的序列再作标准的 Attention(含 self-attention 和 FFN),效率即可被提升至 \(m^2\)

原始 preceiver 是用来作类似 BERT 的 encoder 机制的,因此过了唯一一个 encoder cross attention 后,就直接过若干个 latent transformer block 提取结果了。

而这显然是不牛的。因此改进后的 preceiver IO 机制是一个 Encoder-Decoder 结构,类似 VAE,在最后又加了一个 decoder cross attention,不同的是此处结构倒转,latent 作 K,V,而输出查询数组作 Q 了。

但这仍然不是 DS 使用的机制。DS 把这个概念借鉴到单个 attention block 里面,压缩历史的 KV cache 信息,把它换成固定长度的 latent array。这样,它便由完整的 encoder-decoder 模型转变为一个可替换的 attention 模块,这是好的。但是注意,DS 使用的机制大概也就名字相同了——它并没有可学习的 latent array,所有的信息都是 input array 出发过一车映射得到的。

具体怎么压缩?首先对于输入,过一个线性变换到降维后的 latent array \(c^{KV}\),然后各自过两个线性变换到升维后的 \(k^C\)\(v^C\)。然后 Q 的生成也过了一个相同的降维-升维流程(虽然这不能优化 KV cache,但是能优化训练!)。但是注意到还没有加入 positional embedding。分析得出降维和 RoPE 不可兼得,所以额外从降维后的 \(c^Q\) 生成一批 RoPE Q,从原始输入生成一批 RoPE K,然后两种 Q、两种 K、一种 V 一起再做了一个 attention。挺复杂的,但是好像效果好。


DS 使用了非常精巧的 para 技巧。例如,在训练时,因为通信量大,不用 Tensor Para(但 inference 时还是可以用的);使用了神秘的增强的 Pipeline Para 技巧;对 Expert Para 加速。

posted @ 2025-09-17 20:17  Troverld  阅读(58)  评论(0)    收藏  举报