MoreRSS

site iconOilbeater修改

灵雀云技术合伙人, Kube-OVN Malacca 项目发起人,维护者
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Oilbeater的 RSS 预览

DeepSeek MLA -- 为成本优化而生的注意力机制

2025-04-15 01:10:03

DeepSeek 第一次出名是因为 DeepSeek V2 做到了一百万 Token 只要 0.14 美元。同期的 GPT-4 是 30 美元,当时被认为极具性价比的 GPT-3.5 也要 1.5 美元。这个突破性价格的出现在国内引发了一轮价格战,大批大厂模型大幅降价甚至免费。然而和其他大厂烧钱补贴的逻辑不同,DeepSeek 是通过一系列的技术创新实现了成本数量级的下降。这篇文章就来介绍一下这背后最关键的一个技术创新 —— MLA(Multi-Head Latent Attention)。

MLA 最本质的数学技巧并不复杂,论文里也是一段话就说完了,看完让人感叹竟然还有如此精妙的解法。但是由于和整个 Transformer 架构耦合会导致理解起来有些困难,我这里会尽量简化描述,哪怕你之前完全不了解 Transformer 应该也可以领会到这个方法的精妙。

当然还是需要有一些线性代数的基础,如果你还记的一个形状为 5*4 的矩阵乘形状为 4*3 的矩阵,结果是一个形状为 5 * 3 的矩阵就可以继续了。

KVCache

大模型推理的成本的瓶颈在哪里?答案可能会出乎意料 —— 是显存。显卡有大量计算单元,而推理任务又是线性的一次只能出一个 Token,为了充分利用显卡的计算资源达到最大的吞吐,一般会同时运行尽可能多的生成任务。而每个任务在推理过程中都会占用大量显存,如果想运行尽可能多的任务就需要运行时显存占用足够小。而 MLA 在运行时的显存占用是原始注意力机制的 **6.7%**,你没看错,不是降低了 6.7%,是降低了 **93.3%**。打个比喻这一刀下去不是腰斩,而是脚踝斩。在不考虑模型本身的显存占用情况下,近似可以认为 MLA 在相同显存下可以容纳 15 倍的生成任务。

虽然 MLA 的论文里没有细说这个灵感的来源,但我认为他们是从原先的 KVCache 倒推,诞生了一种全新的注意力机制。到这里就不得不说下 KVCache 是什么。

大模型每生成一个 Token 都需要对之前所有的 Token 进行计算,来得出最新的一个 Token 是什么。但是一般任务不会生成一个 Token 就结束,往往要一直生成到结束符号,这就会导致前面的 Token 每一次都要重复计算。于是 KVCache 的作用就是把之前每个 Token 计算生成的中间结果保存下来,这样就可以避免重复计算了。你可以理解为每个 Token 都被映射成了一个 1000 * 1000 的矩阵,那么我们有没有什么办法来减少这个矩阵的内存占用呢?

MLA

这里有意思的事情终于可以开始了,我们可以用两个小矩阵相乘来近似一个大矩阵。这里刚才你还记得的线性代数知识可以用上了,1000 * 2 的矩阵乘一个 2 * 1000 的矩阵也可以得到一个 1000 * 1000 的矩阵,而这两个矩阵总共只有 4000 个元素,是 1000 * 1000 矩阵元素数量的 0.4%。

这就是 MLA 在数学上最核心的思路了,在 DeepSeek V2 中,本来一个 Token 应该被映射为一个 1*16k 的向量,而在使用 MLA 后会先通过一个压缩矩阵将这个 Token 映射为 1*512 的向量,等到需要的时候再通过一个 512 * 16k 的解压矩阵还原成 1*16k 的向量。在这里压缩矩阵和解压矩阵都是通过训练得来,是模型的一部分只会占用固定的显存,而运行时针对每个 Token 的显存占用就只剩这个 1*512 的向量,只有原来的 3%。

一个完整的对比如下图所示,原先的 MHA 需要 Cache 完整矩阵,而 MLA 只需要 Cache 中间压缩后的一个向量,再还原出完整矩阵。


这一切真的这么美好嘛?让我们想想 KVCache 的最初目的是什么,是为了减少 Token 的重复中间计算,MLA 虽然压缩了 KVCache,但是每次还需要一个解压操作,计算量又回来了。

这里就是一个更精彩的故事了,按照 Transformer 的计算,中间的 Cache 乘一个解压矩阵后还要再乘一个输出矩阵得到最终的结果,可以粗略理解为最终计算的公式是 Cache * W解压 * W输出 ,根据矩阵计算的结合率,可以先计算后两个矩阵的乘积,将后两个矩阵融合乘一个新的矩阵。由于 W解压 和 W输出 在训练后是确定的,因此做个简单的后处理把这部分提前算出来就好了。作者在论文中也用 Fortunately 来形容这件事情。

也就是说我们最初是出于压缩 KVCache 的思路去做了压缩和解压,但在实际推理过程中根本不存在解压的过程。在大幅压缩了显存的同时由于过程中的矩阵都变小了,推理所需的计算量也变小了。

模型能力

但在这里我其实还有个疑问没有解开,本质上 MLA 是用两个小矩阵相乘得到一个大矩阵,但是并不是所有的大矩阵都能完美分解成两个小矩阵。MLA 实际的搜索空间是小于 MHA 的,理论上来讲 MLA 的模型能力应该更弱。但是按照 DeepSeek 论文里的评测,MLA 的模型能力是要略强于 MHA 的。

这个事情其实就不太好理解了,我倾向于认为 MLA 虽然搜索空间降低了,但是最优解的概率反而变大了,收敛到了一个相比 MHA 更优的解。另外虽然 MLA 的优化是从 MHA 出发,但最终的结果其实是一套全新的注意力机制,模型的架构都发生了很大的变化,或许 DeepSeek 真的发现了一个更有效的注意力机制。

总结

有不少性能优化方案其实是在玩跷跷板游戏,比如用 GPU 计算时间交换显存空间,或者牺牲模型能力来换成本下降。而 MLA 在把显存打脚踝斩的情况下同时还做到了计算需求下降和模型能力提升,简直匪夷所思。

另一个感触是在经历了国内移动互联网时代的我们很容易认为价格战就是要赔本赚吆喝,却忘了技术创新才应该是那个最大的杠杆。

这篇博客只是介绍了 MLA 最核心的理念,在实际应用中还有很多具体的问题,例如:如何处理旋转位置编码?K 和 V 的解压矩阵融合其实略有不同,一个是直接应用结合律,一个是转置后再结合,等等。还是建议大家阅读 DeepSeek V2 的原始论文,有这篇文章做基础应该容易理解很多。

博客里部分图片源自 DeepSeek-v2 MLA 原理讲解,也建议大家看下这个视频。

混乱的 Llama 4

2025-04-06 23:57:23

前段时间 Meta AI 的负责人离职让人怀疑 Llama 4 的进度出现了问题,结果没过两天 Meta 就发布了 Llama 4,似乎是为了打破传言。然而看完了现在已经公布的模型基础信息,我反而更觉得 Llama 项目内部已经极度混乱了。下面是我根据已有信息的分析,欢迎指正。

模型基础信息

Llama 这次正式发布了一个 109B 和 一个 400B 参数量的模型,以及一个未发布的 2T 参数量模型的信息,我把关键的架构信息汇总如下,以下信息均来自 Llama 自己的博客和 huggingface 模型页面:

名称 参数量 激活参数量 专家数 上下文 语料量 GPU 时间
Scout 109B 17B 16 10M 40T 5.0M
Maverick 400B 17B 128+1 1M 22T 2.38M
Behemoth 2T 288B 16 - - -

这里都不用再看模型的评分表现了,这几个模型架构方面的对比就能看出很多问题了。

奇怪的 MoE 架构

Llama 4 这次从 Dense 模型全面转向了 MoE,但是诡异的点在于他们三个模型采用了两套 MoE 架构。最大的 Behemoth 和最小的 Scout 采用的是传统的 MoE,专家数也是 16 这个传统认为比较常规的一个专家数量,而中间的那个 Maverick 采用的却是 DeepSeek MoE 提出的一个新的细粒度专家加共享专家的模型是个 128 专家加 1 共享专家的架构。

一般来说一代模型都是采用同一个架构,只是在模型的层数和每层的宽度上做调整,两个有很大差异的模型在同一代就很奇怪。而且就算有变化也不应该是最大和最小的保持一致,把中间规模的给换了,给人的感觉是中间的这个 Maverick 其实是被 DeepSeek 冲击下重新仿照 DeepSeek 模型重新训练的,但是时间上来不及把三个都重做,只好就放在一块发布了。

奇怪的成本投入

一般来讲,模型参数规模越大,需要投入的成本越高。一方面是更大的模型可以容纳更多的知识,会提供给更大规模模型更多的语料;另一方在语料相同的情况下,更大的模型需要训练的参数更多 GPU 开销也会更高。所以通常来讲模型规模越大,需要的成本会越高。

然而到了 Llama 4 这里出现了两个指标都相反的情况。Maverick 的参数规模是 Scout 的接近 4 倍,但是 Maverick 训练的语料量只有 Scout 的二分之一,消耗的 GPU 时间同样也只有二分之一。考虑到这两个模型的激活参数量是一致的这个 GPU 时间可以理解,但是语料量也只给一半这个事情就很奇怪了。给我的感觉是要么这次只是试水新型的 MoE 架构,并没有想做完整训练,要么就是训到后面训崩了,从中间那个 snapshot 出来了。

奇怪的上下文长度

一般来讲更大的模型,能力会越强。可在这一代 Llama,最让人感到震撼的 10M 上下文是给的最小规模的 Scout,更大的 Maverick 反而是 1M 上下文。考虑到目前扩充上下文的主流方法还是在后训练做微调,更大的 Maverick 在后训练的投入上还不如更小的 Scout。

总结

给我的感觉是 Llama 4 这一代本来是想走传统 MoE,被 DeepSeek 冲击后又半路开始看 DeepSeek MoE。但是训练可能已经开始了,停下来又有阻力,所以中间又插了一个中规模的 Maverick。按照这个参数量选择来看是想用比 DeepSeek V3 小的参数量实现类似的性能。但是 17B 的激活要追平 DeepSeek V3 的 39B 激活我觉得还是有很大难度的。不过最后能让这一代的模型以这么混乱的形式发布,还加了个期货模型,我还是觉得 Llama 项目内部出了不少的问题。

DeepSeek MoE -- 创新型的 MoE 架构

2025-03-29 20:54:37

从 DeepSeek V3/R1 开始关注 DeepSeek 工作的人很容易认为 DeepSeek 大量的工作都是在工程上优化效率,但是回看 DeepSeek 过去一年的论文才会发现他们其实一直在模型架构和训练方法上做各种创新,而 V3 和 R1 只是在之前架构创新的基础上进行 Scale。DeepSeek MoE 这篇论文就介绍了 DeepSeek 在 MoE 架构上的主要创新,现在看上去也很有希望成为未来 MoE 架构的标准。

MoE vs Dense

先说一下 MoE 和传统的 Dense 架构的区别。早期的 LLM 基本都是 Dense 架构,也就是每生成一个 Token 需要激活所有的神经元参与计算,这种方式其实和人脑的思考方式是有很大区别的,人脑是不会任何问题都需要调动所有脑细胞的,如果这样的话人早就累死了。所以很自然的一个想法就是生成 Token 的时候不要再激活所有的神经元了,每次只激活和当前任务最相关的神经元,于是就有了 MoE(Mixture of Experts) 架构,把 LLM 里每一层的神经元分割成 N 个 Expert,通过 Router 去选择 K 个最相关的 Expert 激活。

这个架构的好处就是在推理的时候不需要激活所有的神经元,计算量会大幅下降。在 DeepSeek MoE 前最常见的 8 选 2 模式下计算量可以下降到接近 Dense 模型的三分之一。

MoE 的架构看上去很理想,但本质上是在用少量 Experts 来模拟 Dense 模型的表现,所以关键是在每个 Expert 是否有足够的专业性,能否真的模拟 Dense 模型的表现。如果类比人脑,当神经元足够特化时,特定任务只需要激活少量神经元即可完成。

DeepSeek MoE 这篇论文就介绍了他们为了把每个 Expert 专业性推到极致所做的两个创新:

  • 更多更小的 Expert
  • 知识共享 Expert

更多更小的 Expert

使用更多更小的 Expert 来增加每个 Expert 的专业性看似是个很符合直观的思路,但是之前主流 MoE 都是 8 个或者 16 个 Expert。可以想象 LLM 要处理的问题类型千千万,这个数量规模的 Expert 显然不可能做到高度的专业化,每个 Expert 都会有大量当前任务无关的知识。

但是随着 Expert 的数量变大,训练的难度也会变大,Router 很容易只选择少数几个 Expert 导致负载的极度不均衡。最终,理论上的 MoE 架构可能会变成每次只激活同一组 Expert 的小模型。因此,之前大部分 MoE 架构的 Expert 数量都不会太多。

DeepSeek 经过一组设计的损失函数,给重复选择同一个 Expert 增加了惩罚,从而迫使 Router 更均衡的去选择 Expert。通过这个方式 DeepSeek 解决了训练的问题,开始一步步尝试 scale Expert 的数量。从这篇论文里的 64 选 6,扩展到 128 选 12,到 V2 的 160 选 6,再到 V3 的 256 选 8。

可以看到 DeepSeek 一步步将 Expert 数量扩展,而且所需要选中的 Expert 比例也从 9% 一步步降低到 2%,证明了确实在 Expert 足够专业化后只需要更少部分的激活就可以完成对应的任务。

知识共享 Expert

随着 Expert 变小和 Expert 数量增加其实还会带来另外一个问题,那就是每个 Expert 除了需要特定领域的知识外,其实还需要一些通用知识,例如一些通用的语言理解和逻辑分析,可能是每个 Expert 都需要的。如果每个 Expert 都记忆了相关知识那么其实会造成大量的知识冗余,当 Expert 数量变多时,问题会更加明显。这其实会限制每个 Expert 的专业化,训练和推理过程中也会造成资源的浪费。

DeepSeek 提出的做法是增加一组共享 Expert,这一组 Expert 每个训练样本都会被激活,希望他们在训练过程中可以学到通用的知识,这样其他的 Expert 就无需再去学习这些通用知识,只需要学习专业知识了。当推理过程中这组共享 Expert 也会每次都被激活,来提供通用的知识信息。

这同样是一个很符合直觉的架构创新,但是由于之间 MoE 架构的 Expert 规模本来就不大,这个优化的意义其实并不明显,只有当规模上去了这个问题才会暴露出来。在这篇论文里 DeepSeek 还根据 Expert 数量按比例扩充了共享型 Expert 数量,但是随着更多的训练和实践,发现其实并不需要那么多共享型 Expert,等到 V3 的时候其实只使用到了 1 个共享型 Expert。

感想

看完这篇论文我最大的感受是 DeepSeek 并不是拿一个已经验证过的架构无脑堆数据,而是真正的在做模型层面的创新。这也导致在 V3/R1 大火之后很多框架第一时间都无法运行 DeepSeek 的模型或者性能也很差,因为 DeepSeek 的模型架构和其他人都有明显的差别。

并且相比 DeepSeek 其他论文里提到的 MLA、GRPO 和 NSA 这些需要复杂数学功底的创新不同,这两个模型创新都还是相对符合直觉的,但在那个时间点只有 DeepSeek 敢于这么尝试,其他人还在 Follow Llama 的 Dense 模型,敢于去做非主流的尝试,还是需要很大的勇气,这里只能对 DeepSeek 团队再次表达 Respect。

从 DeepSeek LLM 到 DeepSeek R1 —— DeepSeek LLM

2025-03-14 18:48:50

最近找到了 DeepSeek 发表过的论文合集,包含了从 DeepSeek 第一版的 LLM 到最新的 R1 的演变过程。从当下的角度我们当然知道 DeepSeek R1 在模型能力上已经接近了业界最领先的水平,但他是如何一步步从一个在中国一开始都没有被重视的量化公司走到这里的其实更吸引我的注意。

这个系列的博客我会从论文阅读的角度,试图去寻找他们一步步探索的轨迹,从论文的路径上来看就是 DeepSeek LLM -> DeepSeek MoE -> DeepSeek V2 -> DeepSeek V3 -> DeepSeek R1。在整理论文时我才发现,DeepSeek 第一篇对外发布的论文是在 2024 年的 1 月,当时他们刚发布第一版模型,即使在 AI 行业内也不被认为是个主要竞争者。然而仅仅一年后的 2025 年 1 月,就已经进化到了 R1 这种业界领先水平。都说 AI 一天,人间一年,但是当真看到人间一年的进展时,还是深深的被 DeepSeek 的速度所震撼。

背景

DeepSeek LLM 是 DeepSeek 开源的第一个 LLM,当时开源 LLM 里最受关注的是 LLaMA-2,很多模型也是基于它的架构和基础进行的。现在从后视的角度我们知道 DeepSeek 最终选择了和 LLaMA 这类 Dense 架构不同的 MoE 架构,但是在当时第一版的时候还是基本上照搬了 LLaMA-2 的架构,进行局部的调整。可以猜测当时团队内部还处于探索模型架构的阶段。

尽管架构大体和 LLaMA-2 相同,训练的数据量也都是 2T tokens,但是在性能评测上,如下图所示 DeepSeek LLM 基本上是全面超越了 LLaMA-2。论文里介绍了他们发现的一些有趣的关于数据,训练和微调的方法。

alt text

值得注意的是 DeepSeek 在训练过程中是可以使用更多的数据,使用更大参数量的模型的,显然这样做会提升模型性能。但是这篇论文目的主要是和 LLaMA-2 对比,因此特意把数据规模和参数量都做到尽可能相近,来比较在其他方面还有哪些地方可以提升。

数据

LLaMA-2 和 DeepSeek LLM 在数据的选择上还是有很大的区别的。虽然都是 2T 的 token 量,LLaMA-2 里接近 90% 的语料都是英文,而 DeepSeek LLM 虽然没有详细说语言的比例,但是看表述语料里的英文和中文比例应该是比较接近的。所以逻辑上来看 DeepSeek LLM 在中文的评测上大幅领先并不是个意外。意外的反而是在英文评测指标上,DeepSeek LLM 在训练量明显少的情况下依然取得了接近的性能结果。

我猜测这种现象的原因有两个,第一是 DeepSeek LLM 的语料质量更高,弥补了数量上的劣势。LLaMA-2 在介绍预训练语料的时候说除了一些敏感信息没有对数据集进行过滤,而 DeepSeek LLM 中介绍了为了提高数据质量专门做了模型去评估数据质量,还特意把一些冷门领域的数据占比放大,已获得更好的数据多样性。因此可以推测英文部分的语料质量 DeepSeek LLM 要高一些。作为参考 LLaMA-3 也在数据准备过程中引入了去重和质量过滤来提升语料的质量。

另一个原因我猜大概是中文语料的引入也提升了模型最终在英文上的表现。OpenAI GPT 3.5 的时候训练语料也是英文为主,但是最终在中文的表现上不差,一个猜测的原因就是在英文语料上学习到的一些知识迁移到了中文。同样中文语料里学习到的一些知识也可以迁移到英文。此外由于中文和英文在语法和表现形式上也有比较大的区别,这种多样化的数据是不是一定程度上也提升了模型的能力?还有就是不同语言本身就有不同的文化背景和内容倾向,这其实也是进一步增加了数据的多样性。如果这个猜测成立的话,那准备语料其实应该刻意地去增加不同语言的比重,让模型可以学习更丰富的语言表达形式。

模型架构

模型的架构层面 DeepSeek LLM 和 LLaMA-2 几乎完全一样,各个路径上用到的技术比如 Pre-Norm,FFN 的激活函数,位置编码器都一模一样。最大的区别在于使用 GQA(Group
Query Attention)上。GQA 相比最原始的 MHA(Multi Head Attention),可以理解为为了节省训练和推理的 kv cache 占用,直接让多个 Query 头共享一组 Key 和 Value 的参数矩阵,这样可以大幅压缩显存的使用。但是带来的问题就是减少了 Key 和 Value 的潜空间个数,模型的表达能力也出现了下降。LLaMA-2 的做法是通过增加 FFN 网络的宽度来提供更多的非线性表达能力,而 DeepSeek LLM 的做法是增加 Attention 的层数。可以粗略理解尽管模型参数量相同,但是 LLaMA-2 是一个更宽的模型,而 DeepSeek LLM 是一个更深的模型。当然从后视的角度来看,DeepSeek 后续在 V2 公布的 MLA 对 Attention 层做了一个极其激进的更新,直接把推理所需的 KV Cache 降低了一个数量级。

另一个区别在于 LLaMA-2 使用的是 cosine learning scheduler 而 DeepSeek LLM 使用的是 Multi-Step learning scheduler。给出的理由是当增加数据量的时候,Multi-Step 可以更好的利用前一阶段的结果,持续训练速度会更快。

此外论文里还花了很大篇幅来介绍如何在不同的数据规模,数据质量,模型规模下选择合适的超参,如何去画 scaling law 曲线。这块是作者当成最大亮点来讲的,但是我看上去感觉和炼丹一样,看的我脑壳疼,感兴趣的同学可以自己看看。

后训练

在论文发表的那个时间点,后训练主要是做对齐,也就是通过 SFT 和 RLHF 来对齐人类的偏好,增加模型的安全性。用到的数据基本上也都是一些带标记的对话文本,并没有对数据的分布做特别的处理。DeepSeek LLM 在这里对数据的选择又做出了和 LLaMA-2 很不一样的选择。

如果看最上面模型性能评估的对比图,可以看到 DeepSeek LLM 在 MATH,HumanEval 和 MBPP 几个非中文的指标表现也要好很多。因为 DeepSeek 在后训练的 SFT 阶段将近 70% 的样本都是 Math 和 Code 相关数据。可见他们根本就没把对齐作为后训练的重点,而是把提升模型能力作为后训练的重点,所以这更像是一个鸡贼的刷榜优化。

当时主流的做法还是在 base model 训练好了后再 SFT 一个代码和数学领域的模型,比如 Code LLaMA 和 OpenAI Codex 是分别在 LLaMA-2 和 OpenAI GPT3 上 SFT 出来的。Meta 当时甚至还在 Code LLaMA 上再 SFT 一个 Python 专用的 LLM 出来。

现在我们当然知道在后训练阶段通过在 Math 和 Code 样本上进行 RL 可以激发出模型 CoT 的推理能力,R1 的想法可能在这个时候就已经诞生了。

此外 DeepSeek LLM 在这里并没有用当时很流行的 RLHF,而是选择 DPO(Direct Preference Optimization) 进行和人类偏好的对齐。这种方法直接对两个不同生成结果的概率差作为优化目标进行训练,这相比 RL 其实更直观也更容易设计,在 LLaMA-3 的后训练过程中也用到了 DPO。可以看出 DeepSeek 团队当时对已有的 RL 算法还是不太满意的,还在探索。这也就造就了后来在 DeepSeek Math 中公布的 GRPO。

Future Work

在我上学的时候,老师和我说真正的 Future Work 不要写在论文里,自己偷偷做再发下一篇,论文的 Future Work 里就写你觉得没戏的和你觉得做不出来的。而 DeepSeek LLM 最后 Future Work 的几句话从现在的视角来看都太真诚了,几乎已经把 R1 的路子给指出来了。

DeepSeek LLM 将会是一个长期项目,专注于促进开源模型进步,

这个不好说,毕竟满打满算也就一年多。

Soon,我们将会发布 Code 和 MoE 架构的技术报告。MoE 的架构看上去很有希望。

这个 Soon 指的是一周发布 MoE,半个月发布 DeepSeek Code。而我们已经知道 MoE 成为了 V2,V3 和 R1 模型的基础架构,参数量也上升到了 671B。

我们现在已经有了大得多质量好得多的数据集,下一代模型所有指标都会显著提升。

数据量半年后从 2T 变成了 8T,不过同期 LLaMA-3 变成了 15T。DeepSeek V2 的各项指标相比同期的 LLaMA-3 在英文上是稍微落后的,而且在 V2 的时候他们的重点就已经变成了疯狂降低成本。

我们的对齐团队发现强化学习能够增强模型的复杂推理能力。

从今天回头来看,这不就是 R1 最重要的方法么。

总结

从今天的视角来看,DeepSeek 当时应该还在探索期,还在和业界的开源模型对齐,还做了很多理论上的研究。但是从论文的各个细节上来看,一年后那个石破天惊的 R1 诞生的条件已经差不多具备了。

从 Network Binding Plugin 看 KubeVirt 的扩展方式

2025-01-12 16:16:07

在 KubeVirt v1.4 的新版本里将 Network Binding Plugin 提升到了 Beta,提供了一种新的扩展 KubeVirt 网络的方式。虽然名义上是为了扩展网络的能力,但实际上从实现上来看,这个机制能做的事情远不止网络,所有和 libvirt domain xml 相关的变更都可以通过这个机制来实现。

KubeVirt Network Overview

先从网络的角度来看下 KubeVirt 之前的网络机制有什么问题,新的机制又是如何进行扩展的。

由于 KubeVirt 使用的是 Pod 里面跑 VM 的架构,所以复用了 CNI 的网络机制。这样的话就将网络分成了两个部分,一个是 Pod 网络由各个 CNI 提供。另一部分就是如何将 CNI 提供的网络接入 VM,在 libvirt 里这部分叫做 Domain 网络。

KubeVirt 之前的各种网络机制(Bridge,Masquerade,Passt,Slirp)所做的事情就是通过不同的技术方案将 Pod 里的 eth0 接入到 VM 的 tap0 网卡。例如 Bridge 将 tap0 和 eth0 接入到同一个网桥,Masquerade 将 tap0 的流量经过 iptables nat 规则导入 eth0,Passt 和 Slirp 通过用户态的网络栈做流量重定向。

alt text

这些方法在实现上都是类似的,在 Pod 内部做一些网络相关的配置,然后修改 libvirt 的启动参数接入对应的网络。但是现有的机制都是写死在 KubeVirt Core 里的,并没有扩展机制,想要新增一种机制或者修改已有的机制都需要修改 KubeVirt 的代码很不灵活,例如默认的 bridge 插件会劫持 DHCP 请求,但是又不支持 IP, 所以 bridge 模式下的双栈就很难实现,而 Kube-OVN 中已经实现的 DHCP 又被这个机制绕过去了,之前想做 bridge 的双栈就需要改 KubeVirt 的代码来关闭默认的 DHCP 十分麻烦。因此新版本中将这套机制抽象出来提供了一套通用的机制。

Hook Sidecar

先来看一种在 KubeVirt 中已经存在的扩展机制 Hook Sidecar

这套机制是在 VM 正式创建前,可以加载一个用户自定义的镜像,或者一段 ConfigMap 里保存的 Shell 或者 Python 脚本,来修改 VM 启动前 libvirt 的启动参数和 cloud-init 参数。

它的执行机制和 CNI 有些类似,virt-handler 在启动 VM 前会去对应目录寻找 /usr/bin/onDefineDomain 和 /usr/bin/preCloudInitIso 两个二进制文件,前者传入 virt-handler 生成的 libvirt XML 配置,返回修改后的配置;后者传入 cloudInit 配置,返回修改后的 cloudInit 配置。这样的话所有 KubeVirt 本身不支持的 libvirt 和 cloudInit 参数都可以通过这种机制来注入修改。并且由于 Sidecar 内实际可以执行任意代码,所能做的事情远不止修改这两个配置,所有初始化阶段 KubeVirt 没有实现的能力其实都可以在这里来实现。

Network Binding Plugin

现在可以到 Network Binding Plugin 这个机制了,这个机制其实和 Hook Sidecar 基本上大同小异。主要区别是将二进制调用改成了 gRPC 调用,gRPC 里注册的方法还是 onDefineDomainpreCloudInitIso 参数传递从命令行参数改为了 gRPC Request 里的参数,其他都是一样的。

具体的例子可以参考目前还在 KubeVirt 代码里的 Slirp Binding 的实现。尽管在 Network Binding Plugin 的规范里还增加了networkAttachmentDefinition 字段可以选择一个 CNI,但这个其实使用之前的网卡选择机制也能实现,甚至由于 Sidecar 里可以执行任意代码,在里面再实现一个 CNI 覆盖 Pod 原先的网络也是可以的。

那么之后的网络架构就变成了下图这样:

alt text

总结

虽然 Network Binding Plugin 的机制是为 Network 扩展准备的,但实际上几乎可以扩展所有 KubeVirt 在 virt-handler 侧的处理逻辑。甚至可以把 KubeVirt 也只当一个框架,所有的逻辑都通过 Sidecar 来处理,相信未来可以玩出不少花活来。

参考资料

加速容器镜像下载:从缓存到按需加载

2024-10-31 19:59:25

在容器的启动过程中,镜像下载速度往往是影响启动速度的最主要因素,通常占据了启动时间的 70% 以上。特别是对于体积庞大的 VM、AI 镜像,它们的大小可能达到数十 GB,导致下载和解压速度都成为启动的瓶颈。本文将探讨镜像下载的主要瓶颈、常见的优化方案以及最新的按需加载技术,以加速容器启动。

镜像下载速度慢的原因

容器镜像下载慢的原因主要有以下几点:

  • 镜像体积过大:VM、AI 镜像体积通常较大,可能达到数十 GB,使得下载时间显著。
  • gzip 解压耗时:特别是在内网环境中,解压时间往往远高于网络传输时间,导致解压成为新的瓶颈。

常见的镜像优化思路

为了解决下载和解压的速度问题,业界提出了多种优化方案:

  1. 镜像缓存
    镜像缓存是提升镜像下载速度的一种方法,通过缓存镜像可以避免重复下载。然而,缓存无法解决冷启动问题,并且镜像频繁变更(如应用更新或安全更新)会导致缓存失效。要实现高效的缓存管理,还需要较复杂的机制来管理缓存更新。

  2. 减小镜像体积
    减少镜像体积也有助于缩短下载时间,但在某些场景下,例如 VM、AI、CUDA 镜像,体积优化空间有限。它们通常需要使用超过 7 GB 的存储空间,难以进一步缩减。

按需加载:是否可行?

目前,大多数容器在启动时并不需要完整的镜像内容。一些论文表明,启动期间仅需 6.4% 的镜像内容,因此理论上可以通过按需下载来优化启动速度。然而,现有的镜像格式存在以下问题,限制了按需下载的实现:

  • OverlayFS 的限制:需要所有镜像层下载完毕后才能得知最终文件结构。
  • gzip 不支持随机访问:即使只需下载单个文件,也要下载并解压整个层。
  • 校验问题:镜像 digest 是按整个层计算的,无法针对单个文件校验。

eStargz:实现按需加载

为了解决上述问题,eStargz 提出了针对 gzip 层的优化方案,即每个文件单独压缩并增加文件级别索引。eStargz 引入了如下优化:

  1. 独立压缩:每个文件单独压缩并索引,解决了 gzip 无法随机访问的问题。
  2. 文件校验:可以对单个文件进行校验,无需校验整个层。

具体的存储格式如下图:

alt text

每个文件被单独压缩合并成一个大的 blob,在 blob 最后增加一个 TOC 的描述文件记录每个文件的偏移量和校验值,这样就实现了按文件的索引和校验。

以下是 eStargz 的 TOC 格式示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"version": 1,
"entries": [
{
"name": "bin/",
"type": "dir",
"modtime": "2019-08-20T10:30:43Z",
"mode": 16877,
"NumLink": 0
},
{
"name": "bin/busybox",
"type": "reg",
"size": 833104,
"modtime": "2019-06-12T17:52:45Z",
"mode": 33261,
"offset": 126,
"NumLink": 0,
"digest": "sha256:8b7c559b8cccca0d30d01bc4b5dc944766208a53d18a03aa8afe97252207521f",
"chunkDigest": "sha256:8b7c559b8cccca0d30d01bc4b5dc944766208a53d18a03aa8afe97252207521f"
}
]
}

通过这种改进,eStargz 实现了对单个文件的按需加载,且可以实现文件级别校验。

性能权衡:优先级加载

虽然按需加载大幅优化了下载性能,但也可能带来运行时性能的下降。为此,eStargz 采用特殊标识来实现优先级加载,将启动所需文件放置于 prioritized zone 中,确保这些文件优先下载,进而提升运行时性能。

alt text

按照作者测试的性能表现如下:

alt text

代价与挑战

尽管 eStargz 带来了按需加载的性能提升,但也带来了以下代价:

  • 存储空间增加:每个文件单独压缩会增加额外的 metadata,降低压缩率。
  • 额外插件支持:eStargz 需要插件支持,例如在容器镜像推送和拉取时需要特定处理插件。

如何使用 eStargz

以下是 eStargz 的使用方法,适用于 containerd 的子项目以及一些支持 eStargz 的工具:

  1. Docker, kaniko, nerdctl 命令行参数

    1
    2
    3
    4
    5
    docker buildx build -t ghcr.io/ktock/hello:esgz \
    -o type=registry,oci-mediatypes=true,compression=estargz,force-compression=true \
    /tmp/buildctx/

    nerdctl image convert --estargz --oci ghcr.io/ktock/hello:1 ghcr.io/ktock/hello:esgz
  2. containerd 插件配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    version = 2

    [proxy_plugins]
    [proxy_plugins.stargz]
    type = "snapshot"
    address = "/run/containerd-stargz-grpc/containerd-stargz-grpc.sock"

    [plugins."io.containerd.grpc.v1.cri".containerd]
    snapshotter = "stargz"
    disable_snapshot_annotations = false

此外,GKE 等云平台的集群已默认启用类似方案,进一步加速了镜像启动速度。看阿里也发表了基于 block device 的按需加载,这类的实现看上去在云厂商都有了比较大规模的落地。

总结

从传统的镜像缓存、镜像体积优化,到按需加载,eStargz 提供了一种兼顾性能和灵活性的方案,使得容器可以在仅下载部分内容的情况下启动。

参考资料