2026年017:DeepSeek如何精确实现潜在注意力(MLA + RoPE)

017:DeepSeek如何精确实现潜在注意力(MLA + RoPE)在本系列课程中 我们将从零开始 完整地构建 DeepSeek 模型 我们将深入探讨其架构的每一个组成部分 不仅理解其工作原理 还将通过代码实现和数学推导来掌握每个细节 通过本系列学习 你将能够独立构建 DeepSeek 的各个模块 并深刻理解其背后的数学原理 上一节我们概述了本系列的目标 现在让我们回顾一下我决定创建这个系列的心路历程 这一切始于 DeepSeek R1 的发布 当时

大家好,我是讯享网,很高兴认识大家。这里提供最前沿的Ai技术和互联网信息。



在本系列课程中,我们将从零开始,完整地构建DeepSeek模型。我们将深入探讨其架构的每一个组成部分,不仅理解其工作原理,还将通过代码实现和数学推导来掌握每个细节。通过本系列学习,你将能够独立构建DeepSeek的各个模块,并深刻理解其背后的数学原理。

上一节我们概述了本系列的目标,现在让我们回顾一下我决定创建这个系列的心路历程。

这一切始于DeepSeek R1的发布。当时,我看到这个模型的表现令人惊叹,但由于每周都有大量新模型涌现,我最初认为这只是一个很快就会消失的浪潮。我查看了他们的网站,但没有进一步深入研究。

然而,情况很快变得令人震惊。一则LinkedIn帖子指出,DeepSeek导致美国科技公司市值蒸发了2万亿美元。原因是中国的DeepSeek构建了一个能与OpenAI**模型匹敌的推理AI模型,而且成本仅为百分之一,更重要的是它是开源的。

DeepSeek不仅构建成本更低,运行成本也更低。这不仅是技术问题,也涉及政治层面——硅谷是否失去了AI优势?DeepSeek引发了所有这些疑问,它比OpenAI便宜约27倍,开源且许可宽松,这令人惊叹。

随后,许多国家开始行动。例如,印度发布了构建国家首个基础模型的提案。人们突然意识到,如果中国能以比美国更少的资源做到这一点,为什么其他国家不能呢?这引发了广泛讨论。

这激起了我的好奇心。我开始大量搜索关于DeepSeek的信息。我一直喜欢从零开始构建事物,之前发布的“从零构建LLM”系列已有约40-45个视频,受到了广泛关注。

我看到一篇帖子,有人尝试复现DeepSeek V2,虽然只是一个小教程,但这激发了我的想法:为什么不尝试从零构建DeepSeek?或者至少理解DeepSeek每个模块是如何从零构建的?

我开始进行研究,意识到DeepSeek R1并非该公司首次发布的产品。以下是DeepSeek的发展时间线:

  • 2024年1月:DeepSeek LLM论文
  • 2024年1月:DeepSeek Coder
  • 2024年3月:区域语言模型
  • 2024年4月:DeepSeek Math(关于数学推理)
  • 2024年6月:DeepSeek V2
  • 2024年6月:DeepSeek Coder V2
  • 2024年7月:DeepSeek V3
  • 2025年1月:DeepSeek R1论文

DeepSeek V3真正震撼了所有人,这是最终导致2025年1月DeepSeek R1论文发表的基础模型。这背后是一年甚至更长时间的研究积累。

所有这些论文中的创新都让我渴望深入理解。我清楚知道传统LLM是如何构建的,但DeepSeek是在传统LLM模型和架构基础上的一次革命,我想学习关于它的一切。

上一节我们了解了DeepSeek的发展历程,本节我们来看看我在寻找学习材料时遇到的挑战。

很自然地,我首先在YouTube上搜索“deepseek from scratch”。我能找到一些视频,比如一个20分钟的视频约有150万次观看,另一个21分钟的视频也有大量观看。但所有这些视频要么10分钟,要么15分钟,还有一个只有8分钟。这完全不是我想要的。

我想知道DeepSeek每个部件是如何从零构建和组装的细节。就好像我想自己造一辆跑车,却只被展示一段5分钟的跑车外观介绍视频。这不是我想要的。我需要一个能解释数学细节、从零展示代码、并带我走过每个构建模块的视频,就像我为“从零构建LLM”做的那样,但在YouTube上我找不到任何这样的内容。

然后我开始在谷歌上搜索。我看到很多人在用DeepSeek构建应用。基于现有东西构建应用固然可以,但我不想只做这个。我想能够自己构建DeepSeek。在我看来,真正的力量在于此,而不是在已有事物之上构建应用。

我再次在网上搜索“build deepseek from scratch”,看到了一些论坛和文章,比如“DeepSeek R终极指南”,但这里仍然是在使用DeepSeek R来构建应用。

上一节我们看到了现有材料的不足,本节中我将介绍如何组织本系列课程,以及你能从中获得什么。

我计划录制一个庞大的系列,包含25、30甚至更多视频。在这些视频中,我将教你DeepSeek架构的每一个构建模块。所谓“教”,我指的不是仅仅展示博客或高层次概念,而是编写代码并推导每个方面的数学细节。

通过这些视频讲座,你将能够自己构建每一个DeepSeek模块。你也将能够构建架构和建模部分。

在本系列中,我计划涵盖三个主要方面:

  1. 带你经历我尝试寻找DeepSeek学习材料的过程,以及我如何理解构建DeepSeek的整个旅程。
  2. 展示我如何划分不同的主题来制作这个系列。
  3. 展示你在完成整个系列后能够达到的水平。

我相信,目前世界上没有太多工程师能够自己构建DeepSeek的每一个方面,并理解其背后的数学原理。所谓数学,我指的是展示和推导每一个矩阵乘法。

完成本系列后,你将成长为这样一位工程师。你将成为一个基础扎实、能够构建DeepSeek模型或架构的工程师。哪个组织会不想要这样的工程师呢?

通过Vizuara频道,我将为你提供这些知识。在Vizuara频道上,我们已经有许多关于机器学习、深度学习和从零构建大语言模型的系列课程。我希望你也能充分享受这个系列。

本节课中,我们一起学习了本系列课程的起源和目标。我们回顾了DeepSeek的崛起如何激发了深入理解其内部机制的需求,探讨了现有学习材料在深度和细节上的不足,并概述了本系列将如何通过代码实现和数学推导,带你从零开始完整构建DeepSeek模型。在接下来的课程中,我们将深入每个构建模块,打下坚实的基础。

在本节课中,我们将要学习三个核心内容。首先,我们将明确DeepSeek究竟是什么。其次,我们将探讨是什么让DeepSeek如此特别,以及其背后的技术细节。最后,我们将了解本系列课程的整体学习计划与主题顺序。

首先,DeepSeek是一家构建大语言模型的中国公司。在深入探讨之前,让我们先快速了解大语言模型的基本概念。

你们可能都使用过ChatGPT。例如,我可以向ChatGPT提问:“为我制定一个去意大利的旅行计划。” 随后,ChatGPT会生成一份旅行计划。这就是一个大语言模型。

但需要记住的核心是:大语言模型本质上是一个引擎,它接收一个词序列,然后给出下一个最可能出现的词的概率。当这些词被组合起来时,就形成了我们看到的句子。

因此,大语言模型本质上是预测下一个词的概率引擎。这是首先要记住的关键点。

以下是一个简单的代码示例,用于演示模型预测的概率性质:

# 示例:使用模型预测下一个词的概率 # 输入句子:“经过多年努力,你的付出将带你” # 模型会输出多个可能的后续词及其概率 

例如,对于句子“经过多年努力,你的付出将带你”,模型可能会预测“走向成功”的概率最高,但也会为“到达远方”、“进入新阶段”等词分配一定的概率。虽然你看到的答案似乎是确定的,ChatGPT显得非常自信,但请记住,在底层,每个词的生成都基于概率,我们通常选择概率最高的词。

在置信度高的预测中,下一个词的概率分布在对数尺度上通常呈现这样的形态:第一个词的概率非常高,随后概率急剧下降。

简单来说,对于“大”并没有一个确切的定义。但本质上,存在一种关于模型规模的缩放定律。最早揭示这一点的论文之一是GPT-3的论文。

GPT-3论文《Language Models are Few-Shot Learners》证明,当模型参数规模增加到1750亿时,无论是单样本还是少样本学习,模型性能都得到了显著提升。这标志着我们跨越了规模壁垒,从13亿、130亿参数最终达到了1750亿参数。一旦跨越这个规模壁垒,大语言模型就开始展现出惊人的特性。

从历史上看,从1950年代到2020年,神经网络参数数量呈指数级增长。近年来,语言模型主导了这一增长趋势,参数规模目前已达到约1万亿。

为什么我们如此关心模型规模并不断增大它?因为人们观察到,随着语言模型规模的增加,会出现所谓的涌现行为涌现特性

这些特性在较小模型中不存在,但在较大模型中显现。例如,在执行算术、单词重组等任务时,当模型规模(或等效的计算能力)超过某个临界点后,模型性能会突然提升。模型开始学习新事物,展现出神奇的特性。

尽管模型仅仅是在“预测下一个词”的任务上进行训练,但当LLM的规模超过特定大小后,模型就能展现出翻译、总结、语法检查等能力。这就是为什么业界竞相构建越来越大的模型。像OpenAI、Anthropic这样的公司甚至公开表示,他们目前就是在追求规模,因为仍有希望,也许在达到10万亿或100万亿参数后,LLM会展现出我们目前完全未知的特性。

需要指出的是,LLM与早期的NLP模型不同。早期的NLP模型本质上是为特定任务(如语言翻译)设计的。而由于我们刚刚看到的涌现特性,大语言模型能够执行广泛的任务,例如翻译、总结、事实核查、语法检查等,正如你们可能已经在ChatGPT中探索过的那样。

一个关键点是,早期的语言模型甚至无法根据自定义指令写一封电子邮件,而这对于现代大语言模型来说是轻而易举的任务。

到目前为止,我们已经看到大语言模型确实随着规模增大而变得更好,并发展出涌现特性。另一个需要注意的点是,这场语言革命的核心是被称为Transformer的架构。

如果你不知道Transformer架构是什么,不用担心,我们将在本系列课程中涵盖。本质上,有一篇名为《Attention Is All You Need》的论文介绍了Transformer架构。从图示看,它有点复杂,要真正理解Transformer架构需要几节课的时间。但本质上,这就是驱动语言模型的“秘密配方”,我们将学习它,所以请放心。

最后,我想提到的是,当我们说创建一个大语言模型时,它涉及两个阶段。

第一阶段是预训练阶段。在这个阶段,我们没有带标签的数据集,但模型可以自行创建训练数据和标签,这被称为自回归阶段。因此,经过预训练的模型也被称为基础模型

为了进行预训练,我们通常从互联网、教科书、媒体、研究文章等渠道汇集海量数据。例如,GPT-2就是在Reddit帖子、书籍、维基百科文章、开放网络语料库等数据上训练的。然后,这个庞大的语言模型就在这海量数据上进行训练。这种训练成本高达数百万美元,随着LLM规模的增大,甚至可能花费数千万乃至数亿美元。

请记住,在预训练之后,模型具备了基本能力。此后,我们通常需要对模型进行微调


本节课中,我们一起学习了DeepSeek的基本概念。我们明确了DeepSeek是一家构建大语言模型的公司,探讨了大语言模型作为概率引擎的本质,以及模型规模与涌现特性之间的重要关系。我们还了解了驱动现代语言模型的Transformer核心架构,并概述了构建大语言模型所需的预训练和微调两个关键阶段。在接下来的课程中,我们将深入这些技术细节,一步步构建起对DeepSeek的完整理解。

在本节课中,我们将要学习大型语言模型(LLM)的核心架构。我们将跟随一个“词元”的旅程,了解它从输入到输出所经历的完整过程。理解这个基础架构,是后续学习DeepSeek创新技术(如多头潜在注意力)的关键前提。

大家好,我是Raj Duneja博士,于2022年从麻省理工学院获得机器学习博士学位。我是“从零开始构建DeepSeek”系列课程的创建者。

在开始之前,我想介绍一下本系列的赞助商和合作伙伴——V AI。我们都深知基础内容和从底层构建AI模型的价值。V AI的理念与我们的原则非常相似,让我来展示一下。

这是V AI的网站。凭借一个小型工程团队,他们构建了一款出色的产品,你可以仅通过文本提示来创建高质量的AI视频。

如你所见,我输入了一个文本提示:“创建一个超写实的豪华手表视频广告,使其具有电影感”。点击生成视频后,很快我就得到了这个令人惊叹的高写实度视频。

这个视频让我着迷的是它对细节的关注。看这里,质量和纹理简直不可思议,而这一切都来自一个简单的文本提示。这就是V AI产品的力量。你刚才看到的精彩视频的支柱,是V AI的视频创作流程,他们正在从第一性原理重新思考视频生成和编辑。

为了试验和调整基础模型,他们拥有印度最大的H100和H200集群之一,并且也在试验B200。V AI是印度发展最快的AI初创公司,为世界而建,这也是我如此认同他们的原因。好消息是,他们目前有多个职位空缺,你可以加入他们优秀的团队。更多详情我将在下方描述中发布。

大家好,欢迎来到“从零开始构建DeepSeek”系列的这一讲。

在上一讲中,我们学习了本系列课程将划分的四个阶段。

第一阶段将是DeepSeek背后的创新架构。这就是这里的阶段一。

第二阶段是训练方法本身,即强化学习的兴起,以及他们如何依赖强化学习,通过基于规则的奖励系统来教授模型复杂推理。

第三阶段是GPU优化技巧,例如他们如何使用NVIDIA的并行线程执行(PTX)或QR等。

第四阶段是他们的模型生态系统本身。他们并没有止步于仅仅构建一个拥有6710亿参数的庞大模型,而是将这个大型模型“蒸馏”成了一个规模约为15亿参数的更小模型。这本质上就是第四阶段。

我们将依次学习第一阶段、第二阶段、第三阶段和第四阶段。而在今天的讲座中,我们将从第一阶段开始,即DeepSeek背后的创新架构,以及是什么使其如此高效。

其架构的两个主要方面促成了DeepSeek的高效性。首先是多头潜在注意力(MLA),它使注意力机制本身更高效。其次是专家混合(MoE),这意味着尽管参数数量是6710亿,但并非所有参数同时处于激活状态,实际上只有大约370亿参数是激活的。本质上,部分参数像灯泡一样被关闭,部分被开启。这使得模型在计算上极其高效,不需要的参数被关闭,需要时则被突然开启并开始工作。此外,我们还将学习多词元预测、量化和旋转位置编码。

因此,我想教给大家的第一件事就是多头潜在注意力。我在思考如何准确地教授这个概念,因为它本身相当高级。如果你搜索“多头潜在注意力”,你会看到一些讨论此事的博客文章。它们确实会谈论多头注意力是什么。

如果你向下滚动,你会看到这只是博客文章的一页,他们从多头查询注意力、分组查询注意力开始,讲到旋转位置编码,然后有一个关于多头潜在注意力的小节。这对于刚开始探索DeepSeek是什么的人来说,几乎不可能理解。因此,我不想采用那种仅仅通过简单讲座、并假设你已具备先验知识来解释MLA的方法。

相反,正如我在系列开始时提到的,我希望使这个讲座系列非常深入。因此,我将通过一个四部分的过程来解释多头潜在注意力。

首先,我们将理解LLM本身的架构,这将是今天讲座的主要目的。我相信,如果没有对LLM架构的直觉理解,就不可能理解潜在注意力。

其次,我们将理解为什么需要自注意力,以及自注意力机制本身是什么。

理解了自注意力之后,我们将理解自注意力如何演变为多头注意力,以及拥有多个注意力头意味着什么。这是这里的第三个方面。

然后,第四个方面本质上是键值缓存。接着,我们将理解多头注意力如何工作,并且它确实工作得很好。但之后,我们可以开始做些什么来提高多头注意力的效率,使其计算更快,并确保存储在内存中的参数数量减少。这时,键值缓存就登场了。

只有当你真正理解了键值缓存,你才会慢慢开始理解多头潜在注意力。因此,在键值缓存之后,我们才会去理解MLA。

但在我们继续学习MLA本身之前,我将投入大量时间来为你们建立这前四个概念的基础。我提到的许多博客文章都假设你已经熟悉这些内容。

在“从零开始构建DeepSeek”系列中,我们有43讲来解释这些不同方面的所有内容。在这个系列中,我不会讲得那么深入。例如,今天我只用一讲来介绍LLM架构,而在“从零开始构建LLM”系列中,这需要三到四讲。

我的目标是向你解释这些知识,同时也确保刚开始观看本系列的初学者也能跟上。因此,我面临的挑战是为本系列专门制作一套新的讲义,因为我试图在一小时内解释一个概念,但同时也想确保在这个过程中不会让初学者掉队。

所以,我为解释这些不同的概念制作了一整套新的讲义。好了,我希望每个人都理解了我们将如何理解多头潜在注意力的流程。

今天,我们的主要目标是理解LLM的架构。因此,在今天讲座之后,你们所有人都应该对当一个词或一个词元进入LLM时会发生什么,有一个心理地图或视觉路线图。

首先,让我解释一下LLM的架构意味着什么。如果你输入一个句子或一系列单词,我们已经看到,当一系列单词输入到LLM时,LLM本质上做的是预测下一个词,或者更准确地说,预测下一个词元。

因此,一个LLM或大型语言模型可以被视为一个下一个词元预测引擎。就像一个引擎,如果我搜索“引擎汽车”并复制一张图片,或者让我现在复制一张好图片。

本节课中,我们一起学习了大型语言模型(LLM)的基础架构概述。我们明确了本系列课程将分为四个阶段:创新架构、训练方法、GPU优化和模型生态系统。我们了解到,理解LLM的整体工作流程——即其作为一个“下一个词元预测引擎”的本质——是深入学习其核心组件(如多头潜在注意力)的必要前提。在接下来的课程中,我们将以此为基础,逐步拆解LLM内部的各个关键模块。

在本节课中,我们将要学习一个非常重要的概念:注意力机制。我们将探讨为什么需要注意力机制,以及它如何成为大型语言模型(LLM)性能提升的关键。

在上一节中,我们介绍了LLM的整体架构,并了解了token在模型中的“旅程”。本节中,我们来看看旅程中最核心的一环——注意力机制。

首先,让我们快速回顾一下本系列课程至今为止的内容。

在名为“DeepSeek基础”的两节课前,我们探讨了DeepSeek架构的四个阶段,或者说本系列课程将涵盖的四个阶段:

  1. DeepSeek架构中的创新
  2. 训练方法
  3. GPU优化技巧
  4. 模型生态系统

在上一节“LLM架构”中,我们开始深入第一阶段。我们的主要目标是,在接下来的两到三节课中,开始理解多头潜在注意力(Multi-Head Latent Attention, MLA),这是DeepSeek架构中的第一个主要创新。然而,为了理解MLA,我们不能直接开始研究这个概念,我们需要循序渐进地接近它。

我们首先了解了LLM本身的架构。我们看到LLM的架构大致如下:它有三个部分,首先是输入部分,然后是处理器部分,最后是输出部分。每个部分本质上由不同的构建模块组成。

因此,我们认识到,要真正理解LLM这个“引擎”如何工作,我们需要打开引擎,看看里面有什么,以及LLM本质上如何学会预测下一个token或下一个单词。如果你把这个类比想象成汽车的运动,我们看到,如果你给汽车加油,汽车本质上就会移动。类似地,对于LLM来说,“燃料”是你作为输入提供给LLM引擎的单词序列,而输出则是下一个单词或下一个token的预测。这就是为什么我们也称LLM为“下一个token预测引擎”。

这个引擎拥有大量的参数,GPT-3有1750亿个参数,GPT-4可能有大约1万亿个,而最近发布的GPT-4.5可能有大约5到10万亿个。

为了真正理解这个引擎如何工作,我们打开了这个黑匣子,并研究了这三个方面。但我没有直接向你解释这三个方面,我告诉你,如果你把自己放在一个token或单词的位置上,想象自己经历不同的阶段会怎样。所以,如果你是一个token,首先你会被分配一个token ID(你的徽章),然后你会被分配一个token嵌入向量,接着你被分配一个位置嵌入向量。token嵌入和位置嵌入相加,就得到了你的“制服”——输入嵌入。

同样,你的邻居们也有他们的“制服”或输入嵌入。你们所有人一起登上开往Transformer的列车。每个Transformer块都有多个组件,例如归一化层、多头注意力层、Dropout层、跳跃连接、又一个归一化层、前馈神经网络和另一个Dropout层,最后是另一个跳跃连接。这只是一个Transformer块。像这样的Transformer块有很多个,可能是12、24、96个等等。所以,作为一个穿着“制服”(输入嵌入)的token,你必须经历所有这些Transformer块。然后当你出来时,会经过一个归一化层,最后是一个输出层。在这个输出层中,如果你是一个768维的向量,你将被映射或向上投影到一个50000维的向量中。为什么是50000?因为那是词汇表的大小,这本质上帮助我们选择下一个token。

这就是一个token在LLM架构中经历的整个生命周期。在今天的课程中,我们将重点关注整个架构中的一个单一环节,那就是多头注意力。我希望你能理解多头注意力在哪个环节发挥作用。在token经历的所有这些步骤中,多头注意力出现在Transformer块内部,并且在这个特定的Transformer块中,它出现在归一化层之后。

所以今天我们将理解多头注意力,但要做到这一点,我们首先需要理解什么是注意力本身,以及为什么需要注意力机制。为什么我们开始谈论“注意力”这个术语?为什么它在最近变得如此流行?

因此,我们今天的目标是引出“自注意力”这个概念。我们今天不会看到自注意力机制的数学原理。今天的目标是尝试理解为什么我们首先需要注意力,以及为什么它对于大型语言模型来说是一个如此巨大的改变。

试想一下,在token经历的所有这些步骤中,最重要的步骤位于Transformer块内部,位于Transformer块的一个步骤中,那就是注意力。这就是为什么我在这里用不同的颜色标记了它。这个训练块本质上赋予了LLM所有使其在理解语言方面表现出色的特性。

为了真正理解注意力如何工作,我为你创建了这节单独的课程,在其中我们尝试引出对注意力机制的需求。

在理解为什么我们需要注意力以及它如何改变了这个领域之前,让我们先深入了解一下生成式AI领域本身。让我们回顾一下它的历史,我相信这对于理解注意力机制至关重要。

在20世纪60年代,有一个名为Eliza的聊天机器人。你可以把它看作是最早的自然语言处理聊天机器人之一,它被设计成一名治疗师。如果你问它“请告诉我是什么困扰着你”,我可以说“我在学习上遇到了困难,你能帮我吗?”它会回答“你认为遇到困难是正常的吗?是的。”你看,它并不是很有帮助,但请记住那是20世纪60年代,在当时这被认为是一场革命。最初的程序由约瑟夫·魏泽鲍姆在1966年描述。

将这与现在的ChatGPT进行比较,我们说“我想学习AI,你能帮我吗?”ChatGPT会提供详细、连贯且有用的回答。

现在,让我们回到核心问题:为什么需要注意力机制?

为了理解这一点,我们需要考虑语言模型在处理序列数据(如句子)时面临的挑战。在注意力机制出现之前,模型(如循环神经网络RNN)在处理长序列时存在信息衰减的问题。句子开头的单词信息很难传递到句子末尾,这限制了模型理解长距离依赖关系的能力。

注意力机制的核心思想是,允许模型在处理序列中的每个元素时,“关注”序列中所有其他元素的信息。这就像你在阅读一个句子时,会根据当前正在读的单词,有选择地回忆或关注句子中之前出现过的相关单词。

以下是注意力机制带来的关键优势:

  1. 解决长距离依赖问题:模型可以直接访问序列中任何位置的信息,无论距离多远。
  2. 并行计算:与RNN的顺序处理不同,注意力计算可以并行化,极大地提高了训练速度。
  3. 更好的上下文理解:模型能够动态地权衡序列中不同部分的重要性,从而更准确地理解上下文。

想象一下,你正在阅读一段复杂的文本。为了理解当前句子的含义,你的大脑会不自觉地回顾前文,寻找相关的名词、动词或概念。注意力机制在神经网络中模拟了这一过程。

在技术层面,注意力机制通过计算一个“注意力分数”矩阵来实现。对于输入序列中的每个元素(例如,一个单词的嵌入向量),模型会计算它与序列中所有其他元素(包括它自己)的关联程度。这个关联程度就是注意力分数。然后,模型根据这些分数对序列中所有元素的信息进行加权求和,得到一个“上下文向量”。这个上下文向量包含了与当前处理元素最相关的信息。

一个简化的注意力计算可以表示为:

注意力输出 = Softmax( (查询向量 * 键向量^T) / sqrt(维度) ) * 值向量

其中:

  • 查询向量:代表当前我们想要计算注意力的位置。
  • 键向量:代表序列中所有位置,用于与查询向量计算相关性。
  • 值向量:包含序列中所有位置的实际信息。
  • Softmax:将计算出的分数转换为概率分布,确保权重和为1。

这个过程允许模型为序列中更相关的位置分配更高的权重。

在Transformer架构中,我们使用的是自注意力。这意味着,查询向量、键向量和值向量都来自同一个输入序列。自注意力允许序列中的每个位置关注同一序列中的所有位置,从而捕捉序列内部的依赖关系。

在下一节课中,我们将深入探讨自注意力机制的数学细节,并了解它如何扩展为“多头”形式,使模型能够同时从不同的表示子空间中学习信息。

本节课中,我们一起学习了注意力机制的必要性和核心思想。我们回顾了LLM的架构,并指出注意力机制是Transformer块中赋予模型强大语言理解能力的关键组件。我们通过对比历史聊天机器人和现代LLM,看到了注意力机制带来的革命性进步。注意力机制通过允许模型动态关注输入序列的任何部分,有效解决了长距离依赖问题,并实现了高效的并行计算,为当今大型语言模型的成功奠定了基础。

在下一节中,我们将正式进入自注意力机制的数学世界,并开始构建多头注意力的完整图景。

在本节课中,我们将学习自注意力机制的核心部分:如何通过可训练的权重矩阵来计算注意力分数。我们将从输入嵌入向量开始,一步步推导出最终的上下文向量。


在上一讲中,我们探讨了为什么需要引入注意力机制,以及为什么简单的点积运算不足以捕捉词语间的复杂关系。本节我们将深入探讨解决方案:引入可训练的查询(Query)、键(Key)和值(Value)权重矩阵。我们将通过一个具体的例子,可视化并理解从输入嵌入到上下文向量的完整计算过程。


上一节我们介绍了LLM的架构,并探讨了引入注意力的必要性。自注意力机制的主要目标是将输入嵌入向量转换为更丰富的上下文向量

输入嵌入向量编码了词语的含义及其在序列中的位置信息,但不包含该词与序列中其他词的关系信息。而上下文向量则包含了词语的含义、位置以及它与其他词元的关系。

为了从输入嵌入向量得到上下文向量,我们曾直觉地尝试使用点积,但发现其能力有限。例如,在句子“The dog chased the ball, but it could not catch it.”中,对于查询词“it”,我们需要机制能区分出“it”指的是“ball”而不是“dog”。简单的点积无法捕捉这种区别。

因此,我们引入了可训练的权重矩阵。其核心思想是:既然我们无法手动设计出完美的注意力计算公式,不如将这个复杂的任务交给可以通过训练学习的权重矩阵。


具体做法是,我们不直接对输入嵌入向量进行点积运算,而是先将它们投影到不同的向量空间。这是通过乘以不同的可训练权重矩阵来实现的。

以下是三个核心的可训练权重矩阵:

  • 查询权重矩阵 (W_Q):用于将当前关注的词(查询)投影到查询向量空间。
  • 键权重矩阵 (W_K):用于将序列中所有其他词(用于被查询)投影到键向量空间。
  • 值权重矩阵 (W_V):用于生成最终用于聚合的信息向量。

继续之前的例子,对于查询词“it”:

  1. 我们不直接使用“it”的输入嵌入向量,而是将其乘以查询权重矩阵 W_Q,得到查询向量 (q_it)
  2. 对于候选词“dog”和“ball”,我们将其输入嵌入向量分别乘以键权重矩阵 W_K,得到它们的键向量 (k_dog, k_ball)
  3. 计算注意力分数时,我们取查询向量 q_it 与各个键向量(k_dog, k_ball)的点积。这个分数决定了对于查询“it”,应该给予“dog”和“ball”多少注意力。

我们称这些矩阵为查询、键和值,是为了符合学术文献的通用术语。本质上,这是一种将学习复杂关系的任务“外包”给神经网络的巧妙方法:我们随机初始化这些权重矩阵,期望模型在训练完成后,能自动学会如何为不同的词元分配合适的注意力。


本节中,我们来看一个具体的分步计算过程。我们将以序列“The next day is bright”为例,演示如何将输入嵌入向量转换为上下文向量。请记住,在进入注意力机制之前,这些输入嵌入已经是词嵌入(Token Embedding)和位置嵌入(Positional Embedding)的总和。

假设我们的输入嵌入维度是 d_model = 4,并且为了简化,我们使用较小的维度进行演示。以下是序列中每个词元的输入嵌入向量(示例值):

The: [1.0, 0.5, -0.2, 1.2] next: [0.3, 1.2, 0.8, -0.5] day: [-0.7, 0.9, 1.1, 0.4] is: [1.1, -0.3, 0.2, 0.9] bright:[0.5, 1.0, -0.1, 0.7] 

首先,我们随机初始化三个权重矩阵:W_Q, W_K, W_V。假设我们想要的查询/键/值向量维度 d_k = 3。那么权重矩阵的形状应为 (d_model, d_k) = (4, 3)

以下是示例初始化(在真实训练中,这些值会通过反向传播更新):

import numpy as np d_model = 4 d_k = 3 # 随机初始化权重矩阵 W_Q = np.random.randn(d_model, d_k) * 0.1 # 例如: [[0.1, -0.05, 0.02], ...] W_K = np.random.randn(d_model, d_k) * 0.1 W_V = np.random.randn(d_model, d_k) * 0.1 

对于序列中的每一个词元,我们使用其输入嵌入向量 x_i 分别与三个权重矩阵相乘,得到对应的查询向量 q_i、键向量 k_i 和值向量 v_i

计算公式为:

  • q_i = x_i · W_Q
  • k_i = x_i · W_K
  • v_i = x_i · W_V

以下是“The”这个词元的计算示例(使用伪代码表示):

x_the = np.array([1.0, 0.5, -0.2, 1.2]) q_the = np.dot(x_the, W_Q) # 结果是一个 (3,) 的向量,例如 [0.12, -0.08, 0.15] k_the = np.dot(x_the, W_K) v_the = np.dot(x_the, W_V) 

我们对序列中的每个词元都执行此操作,最终会得到五个查询向量、五个键向量和五个值向量。

现在,我们为序列中的每一个词元(作为查询)计算它对所有词元(包括自己)的注意力分数。以“day”作为查询为例:

  1. 获取“day”的查询向量 q_day
  2. 获取序列中所有词元(The, next, day, is, bright)的键向量 [k_the, k_next, k_day, k_is, k_bright]
  3. 计算 q_day 与每一个键向量的点积。这五个点积结果就是“day”对于序列中每个词的原始注意力分数
# 假设 q_day 和所有 k 向量已计算好 raw_scores = [] for k_vector in all_key_vectors: # all_key_vectors 包含 k_the, k_next, ... score = np.dot(q_day, k_vector) raw_scores.append(score) # raw_scores 可能类似于 [1.5, 0.8, 2.1, -0.3, 1.0] 

原始的注意力分数可能数值范围不稳定。我们使用Softmax函数将其转换为概率分布,即注意力权重。这些权重之和为1,表示在生成“day”的上下文时,分配给每个词元的信息比例。

import numpy as np raw_scores = np.array([1.5, 0.8, 2.1, -0.3, 1.0]) attention_weights = np.exp(raw_scores) / np.sum(np.exp(raw_scores)) # attention_weights 可能类似于 [0.18, 0.10, 0.48, 0.04, 0.20] 

结果显示,对于查询“day”,模型赋予了“day”自身最高的权重(0.48),同时也从“The”(0.18)和“bright”(0.20)等词获取了相关信息。

最后,我们使用上一步得到的注意力权重,对所有词元的值向量 (v_i) 进行加权求和。这个加权和就是查询词“day”的上下文向量 (c_day)

# 假设 all_value_vectors 是包含所有 v_i 的列表 c_day = np.zeros(d_k) # 初始化一个零向量,维度与值向量相同 for i, weight in enumerate(attention_weights): c_day += weight * all_value_vectors[i] # c_day 现在是一个 (3,) 的向量,融合了序列中所有词的信息 

这个 c_day 向量比原始的“day”输入嵌入向量包含了更丰富的上下文信息,因为它聚合了根据注意力权重从序列中所有词元提取的相关特征。

我们对序列中的每个词元都重复步骤3到步骤5,就能为每个位置生成一个对应的上下文向量。


本节课我们一起学习了自注意力机制中带可训练权重的核心计算流程。我们首先回顾了引入可训练权重的动机——为了捕捉词元间复杂的依赖关系。接着,我们详细介绍了查询、键、值三个权重矩阵的角色。

通过“The next day is bright”这个例子,我们一步步拆解了计算过程:

  1. 使用 W_Q, W_K, W_V 将输入嵌入投影为查询、键、值向量。
  2. 通过查询向量与所有键向量的点积计算原始注意力分数。
  3. 使用Softmax函数将分数归一化为注意力权重。
  4. 最后,用注意力权重对值向量进行加权求和,生成最终的上下文向量。

这个过程使得模型能够动态地、根据内容决定在编码每个词时应该关注序列中的哪些部分。在下一讲中,我们将以此为基础,探讨如何将多个这样的自注意力头组合起来,形成更强大的多头潜在注意力机制

在本节课中,我们将继续深入理解注意力机制。具体来说,我们将学习因果注意力,这是理解多头潜在注意力、键值缓存乃至整个DeepSeek架构的关键基石。我们将从回顾自注意力机制开始,逐步过渡到因果注意力的核心概念。

上一节我们详细介绍了自注意力机制。它通过查询、键、值矩阵,计算每个词元与序列中所有其他词元的相关性,从而生成包含丰富上下文信息的上下文向量。

本节中,我们来看看因果注意力。顾名思义,“因果”意味着一个事物导致另一个事物。在语言建模中,这意味着在预测下一个词元时,模型只能“看到”或“关注”它之前的词元,而不能“窥探”未来的信息。这是确保模型在生成文本时行为正确的关键。

为了清晰地阐述因果注意力的必要性,我们首先需要理解大语言模型是如何进行“下一个词元预测”训练的。

假设我们使用《哈利·波特》第一本书作为训练数据集。为了简化说明,我们取书的第一页内容。

我们首先确定一个称为上下文长度的参数。如果上下文长度设为4,我们就创建由4个连续词元组成的序列作为输入。

我们还需要确定一个步长,它定义了在创建下一个输入序列时需要跳过的词元数量。

以下是创建输入批次的过程:

  • 第一个输入序列由前4个词元组成。
  • 第二个输入序列从第2个词元开始,取接下来的4个词元(如果步长为1)。
  • 以此类推,将整个数据集分割成多个长度为4的词元块。

如果批次大小设为8,那么第一个批次将包含8个这样的输入序列。每个序列有4列,对应上下文长度。

输出批次的创建非常简单:将每个输入序列向右移动一个词元位置即可。这样,每个输入序列都对应一个目标序列,用于预测下一个词元。

以下是输入与输出配对的具体示例:

假设输入序列是 [Mr., and, Mrs., Dursley](对应词元ID:1, 15, 18, 22)。
那么其对应的输出目标序列就是 [and, Mrs., Dursley, off](对应词元ID:15, 18, 22, 3)。



关键点:在这个单一的输入-输出对中,实际上包含了四个独立的预测任务:

  1. 给定输入 [Mr.],预测输出 [and]
  2. 给定输入 [Mr., and],预测输出 [Mrs.]
  3. 给定输入 [Mr., and, Mrs.],预测输出 [Dursley]
  4. 给定输入 [Mr., and, Mrs., Dursley],预测输出 [off]

因此,在训练时,模型是在学习基于当前及之前的所有词元来预测下一个词元

现在,让我们回到自注意力机制。在标准的自注意力中,当计算某个词元(例如“Mrs.”)的上下文向量时,模型会考虑序列中所有词元(包括“Mr.”, “and”, “Mrs.”, “Dursley”, “off”)的信息。

这在训练时会产生一个问题:当模型试图预测“Mrs.”时(即上述第二个预测任务),它本应只基于“Mr.”和“and”这两个历史词元。然而,标准的自注意力机制却让“Mrs.”这个词元也能“看到”它自己以及未来的词元“Dursley”和“off”。这相当于在考试时提前偷看了答案,模型会利用未来的信息来“预测”当前词元,这显然是不正确的,会导致训练和生成时的不一致。

因果注意力就是为了解决这个问题而设计的。它的核心思想是:在计算注意力权重时,每个词元只能关注它自身及之前的词元,而不能关注未来的词元。

因果注意力,也称为掩码自注意力,是在自注意力基础上增加了一个因果掩码来实现的。

让我们回顾一下自注意力中注意力分数的计算公式:

公式Attention Scores = (Q * K^T) / sqrt(d_k)

其中 Q 是查询矩阵,K 是键矩阵,d_k 是键向量的维度。

在因果注意力中,我们在应用Softmax函数之前,将一个下三角掩码矩阵加到注意力分数矩阵上。

这个掩码矩阵的结构如下:

  • 主对角线及以下的位置为 0(允许关注)。
  • 主对角线以上的位置为一个非常大的负数(例如 -1e9),在应用Softmax后,这些位置的概率会趋近于 0(禁止关注)。

代码描述

import torch import torch.nn.functional as F # 假设我们有一个注意力分数矩阵 `attn_scores`,形状为 (序列长度, 序列长度) seq_len = attn_scores.size(-1) # 创建一个下三角矩阵(包含对角线),值为1,其余为0 mask = torch.tril(torch.ones(seq_len, seq_len)) # 将需要屏蔽的位置(上三角)设置为一个极小的值,如 -1e9 mask = mask.masked_fill(mask == 0, float('-1e9')) # 将掩码加到注意力分数上 causal_attn_scores = attn_scores + mask # 应用Softmax causal_attention_weights = F.softmax(causal_attn_scores, dim=-1) 

通过这个操作,对于序列中的第 i 个词元,其上下文向量将仅由第 1 到第 i 个词元的值向量加权求和得到,完美符合“只能关注过去”的因果约束。

本节课中我们一起学习了因果注意力机制。

  1. 我们首先回顾了自注意力机制,它允许词元关注序列中的所有位置。
  2. 接着,我们通过分析“下一个词元预测”的训练过程,揭示了标准自注意力在训练时存在的“信息泄露”问题——模型可能利用未来信息来预测当前词元。
  3. 为了解决这个问题,我们引入了因果注意力。其核心是使用一个下三角掩码矩阵,强制模型在计算注意力时,每个词元只能关注它自身及之前的词元。
  4. 我们通过公式和代码描述了因果掩码的具体实现方式。

因果注意力是构建自回归语言模型(如GPT系列、DeepSeek)不可或缺的组件。它确保了模型在训练和文本生成时行为的一致性,是理解后续更复杂机制(如多头潜在注意力)的重要基础。下一节,我们将以此为基础,探索多头注意力机制。

在本节课中,我们将要学习Transformer架构中的一个核心组件——多头注意力机制。我们将从回顾自注意力机制开始,逐步理解为什么需要引入“多头”的概念,并直观地解释其工作原理。

上一节我们介绍了因果注意力机制,它确保了模型在预测时只能看到当前及之前的信息。本节中,我们来看看如何从单一的自注意力机制扩展到更强大的多头注意力机制。

自注意力机制的主要目标是将输入嵌入向量转换为上下文嵌入向量。输入嵌入向量本身不包含关于相邻词元的信息,而自注意力机制则能捕捉一个词元与序列中所有其他词元的关系。

从输入嵌入矩阵到上下文嵌入矩阵的转换包含四个步骤:

  1. 通过可训练的权重矩阵,将输入嵌入转换为查询、键、值矩阵。
  2. 计算注意力分数(查询与键的点积)。
  3. 对注意力分数进行归一化(缩放和Softmax),得到注意力权重。
  4. 将注意力权重与值矩阵相乘,得到最终的上下文向量矩阵。

在因果注意力中,我们通过掩码确保每个词元只能关注到它自身及之前的词元。

尽管自注意力机制非常强大,但它存在一个主要问题,而多头注意力机制正是为了解决这个问题而设计的。

为了说明这个问题,让我们看一个简单的句子示例:“艺术家用画笔描绘了一位女性的肖像”。

在这个句子中,词元“with”与多个其他词元存在不同的语义关系。例如:

  • “with”可能表示“肖像”是“带有”一位女性的。
  • “with”也可能表示“描绘”这个动作是“使用”画笔完成的。

单一的自注意力机制在计算“with”的上下文向量时,需要同时捕捉这两种(可能更多)不同类型的关系。这可能会使模型难以学习到清晰、独立的语义表示。

多头注意力机制的核心思想是:与其让一个注意力头尝试学习所有类型的关系,不如使用多个注意力头,让每个头专注于学习一种特定类型的关系或模式。

以下是多头注意力机制的工作原理:

  1. 线性投影:首先,模型使用不同的、可训练的权重矩阵,将输入嵌入分别投影到多组查询、键、值子空间。公式表示为:
    多头查询 = 输入嵌入 * W_q_i (对于头 i)
    多头键 = 输入嵌入 * W_k_i
    多头值 = 输入嵌入 * W_v_i


















  2. 并行计算注意力:每个注意力头独立地在其自己的投影子空间中执行完整的自注意力计算(包括缩放点积注意力和Softmax)。
  3. 拼接与最终投影:将所有注意力头的输出上下文向量拼接起来,然后通过一个最终的可训练权重矩阵进行投影,将其映射回原始的嵌入维度。公式表示为:
    多头注意力输出 = Concat(头1输出, 头2输出, ..., 头h输出) * W_o




通过这种方式,不同的注意力头可以学会关注句子中不同方面的信息。例如,在之前的句子中,一个头可能专门学习“with”与“portrait”的修饰关系,而另一个头则学习“with”与“painted”的工具关系。

本节课中我们一起学习了多头注意力机制。我们从回顾自注意力机制出发,指出了单一注意力头在捕捉复杂、多样语义关系时的局限性。多头注意力机制通过使用多组独立的注意力头,让模型能够并行地从不同子空间学习不同类型的关系,从而增强了模型的表示能力和性能。在下一讲中,我们将动手编写多头注意力机制的代码。

在本节课中,我们将深入学习多头注意力机制的具体实现。我们将从一个具体的输入矩阵出发,逐步进行数学推导,并最终将其映射到实际的Python代码中。通过本节课,你将彻底理解多头注意力机制内部的计算流程。

上一节我们介绍了多头注意力机制的概念和优势,本节中我们来看看其具体的数学实现和代码编写。我们将使用一个具体的输入嵌入矩阵,逐步演示查询、键、值的分割,以及多个注意力头的计算与合并过程。

首先,我们定义输入嵌入矩阵。假设我们有一个批次大小为1的输入,包含3个令牌,每个令牌的嵌入维度为6。我们的输入矩阵 x 的形状为 (1, 3, 6)

import torch # 定义输入嵌入矩阵 x = torch.tensor([[[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12], [13, 14, 15, 16, 17, 18]]], dtype=torch.float32) print(f"输入矩阵 x 的形状: {x.shape}") 

接下来,我们需要设定多头注意力的关键参数:输出维度 d_out 和注意力头的数量 num_heads。在本例中,我们设 d_out = 4num_heads = 2。每个头的维度 head_dim 则为 d_out / num_heads = 2

d_in = 6 # 输入维度 d_out = 4 # 输出维度 num_heads = 2 # 注意力头数量 head_dim = d_out // num_heads # 每个头的维度,此处为2 

多头注意力机制的核心步骤是将查询、键、值的线性变换矩阵分割成多个头。以下是实现这一过程的关键步骤:

  1. 定义可训练的权重矩阵:首先,我们需要为查询、键、值定义三个可训练的线性变换矩阵 W_q, W_k, W_v。它们的形状均为 (d_in, d_out)
  2. 计算查询、键、值向量:将输入 x 分别与这三个权重矩阵相乘,得到初始的查询、键、值矩阵 Q, K, V
  3. 分割多头:将 Q, K, V 矩阵在最后一个维度(特征维度)上分割成 num_heads 份。这相当于为每个头创建了独立的 Q, K, V 子矩阵。

以下是分割多头的代码逻辑:

# 步骤1: 定义可训练权重矩阵(此处为示例,使用随机初始化) W_q = torch.randn(d_in, d_out) W_k = torch.randn(d_in, d_out) W_v = torch.randn(d_in, d_out) # 步骤2: 计算查询、键、值向量 # x.shape = (batch, seq_len, d_in) # 矩阵乘法后,Q/K/V.shape = (batch, seq_len, d_out) Q = torch.matmul(x, W_q) K = torch.matmul(x, W_k) V = torch.matmul(x, W_v) # 步骤3: 为多头操作重塑张量 # 目标形状: (batch, num_heads, seq_len, head_dim) batch_size, seq_len, _ = x.shape Q = Q.view(batch_size, seq_len, num_heads, head_dim).transpose(1, 2) K = K.view(batch_size, seq_len, num_heads, head_dim).transpose(1, 2) V = V.view(batch_size, seq_len, num_heads, head_dim).transpose(1, 2) # 此时 Q/K/V.shape = (batch, num_heads, seq_len, head_dim) 

完成分割后,每个头都可以独立计算注意力分数。对于第 i 个头,其注意力分数矩阵 scores_i 的计算公式为:

scores_i = (Q_i @ K_i.T) / sqrt(head_dim)

其中 Q_iK_i 是第 i 个头的查询和键矩阵,head_dim 是键向量的维度,缩放操作是为了稳定梯度。

# 计算注意力分数 # Q.shape = (batch, num_heads, seq_len, head_dim) # K.shape = (batch, num_heads, seq_len, head_dim) # 我们需要 K 在最后一个维度转置以进行矩阵乘法: K.transpose(-2, -1).shape = (batch, num_heads, head_dim, seq_len) attention_scores = torch.matmul(Q, K.transpose(-2, -1)) / (head_dim 0.5) # attention_scores.shape = (batch, num_heads, seq_len, seq_len) 

得到注意力分数后,我们应用因果注意力掩码(确保当前位置只能关注到它自身及之前的位置)和Softmax函数,将其转换为注意力权重。

# 创建因果注意力掩码(下三角矩阵) mask = torch.tril(torch.ones(seq_len, seq_len)).view(1, 1, seq_len, seq_len) # 将掩码中为0的位置(未来位置)的分数设置为一个极小的负数,这样在Softmax后权重接近0 attention_scores = attention_scores.masked_fill(mask == 0, float('-inf')) # 应用Softmax得到注意力权重 attention_weights = torch.softmax(attention_scores, dim=-1) # attention_weights.shape = (batch, num_heads, seq_len, seq_len) 

随后,我们将每个头的注意力权重与其对应的值向量 V_i 相乘,得到该头的上下文矩阵 context_i

context_i = attention_weights_i @ V_i

# 计算上下文向量 context = torch.matmul(attention_weights, V) # context.shape = (batch, num_heads, seq_len, head_dim) 

最后一步是将所有头计算出的上下文矩阵合并起来。我们将 context 张量的形状从 (batch, num_heads, seq_len, head_dim) 转换回 (batch, seq_len, d_out),即沿着头的维度进行拼接。

# 合并多头输出 # 首先将 num_heads 和 head_dim 两个维度合并 context = context.transpose(1, 2).contiguous().view(batch_size, seq_len, d_out) # 最终 context.shape = (batch, seq_len, d_out) 

本节课中我们一起学习了多头注意力机制从数学推导到代码实现的全过程。我们从定义输入嵌入矩阵开始,逐步完成了以下关键操作:

  1. 设定输出维度和头数,并计算每个头的维度。
  2. 通过线性变换得到查询、键、值矩阵,并将其分割成多个头。
  3. 为每个头独立计算缩放点积注意力分数。
  4. 应用因果掩码和Softmax函数得到注意力权重。
  5. 将注意力权重与值向量相乘,得到每个头的上下文输出。
  6. 将所有头的输出合并,形成最终的上下文矩阵。

这个过程使得模型能够从多个不同的子空间(即“视角”)并行地捕捉序列中元素之间的关系,从而获得比单头注意力更丰富、更强大的表征能力。理解这些步骤是掌握现代Transformer架构核心组件的基础。

在本节课中,我们将要学习Transformer架构中的一个关键优化技术——键值缓存。我们将从语言模型的推理过程开始,逐步理解为什么需要键值缓存,以及如何实现它。最后,我们也会探讨其局限性,为后续理解多头潜在注意力机制打下基础。

上一节我们介绍了多头注意力机制,本节中我们来看看语言模型在实际使用(即推理)时是如何工作的。首先需要明确一个核心概念:键值缓存在语言模型的推理阶段发挥作用。

语言模型的使用分为两个主要部分:首先是预训练模型,获得一个参数固定的模型;然后是基于这个预训练模型进行推理,即根据输入预测下一个词元。例如,当你向ChatGPT提问时,你正处于推理阶段,模型正在逐个预测输出词元。

推理过程的核心是“下一个词元预测”。模型接收一个输入序列,通过前向传播计算,预测出下一个最可能的词元,然后将这个新词元添加回输入序列末尾,构成新的输入,再预测下一个词元,如此循环。

以下是这一过程的简化示意:

# 伪代码示意推理循环 input_sequence = initial_prompt while not generation_finished: next_token = model.predict(input_sequence) # 模型前向传播,预测下一个词元 input_sequence.append(next_token) # 将新词元添加回输入 

理解了推理过程后,我们来看其中存在的计算效率问题。在标准的Transformer注意力机制中,每次预测新词元时,都需要为整个当前输入序列计算键(K)和值(V)向量。

假设序列长度为 L,注意力头的维度为 d_k。那么对于每个注意力头,键和值矩阵的形状为 [L, d_k]。在推理的每一步,当序列增加一个新词元(L 变为 L+1)时,都需要为所有 L+1 个词元重新计算K和V。这导致了大量的重复计算,因为旧词元(前 L 个)的K和V在之前的步骤中已经计算过了。

键值缓存的核心思想就是缓存这些已经计算过的键和值向量。这样,在预测新词元时,我们只需要为新加入的词元计算其对应的K和V,而直接从缓存中读取旧词元的K和V。这极大地减少了计算量。

其优势可以用一个公式来概括:

  • 无缓存时计算量(每步):O((L+1)^2 * d_k) (需要为所有词元重新计算并参与注意力计算)
  • 有缓存时计算量(每步):O((L+1) * d_k) (仅需为新词元计算K和V)

现在我们来了解如何具体实现键值缓存。关键在于修改注意力层的前向传播逻辑,使其在推理时能够存储和复用历史状态。

以下是实现键值缓存的关键步骤:

  1. 初始化缓存:在推理开始时,为模型每一层的每一个注意力头初始化一个空的键缓存和值缓存。
  2. 前向传播与缓存
    • 对于输入序列中的每个词元,模型正常计算其查询(Q)、键(K)、值(V)向量。
    • 将计算出的K和V向量追加到对应层和对应头的缓存中。
    • 注意力计算时,使用的K和V矩阵是整个缓存的内容(即所有历史词元加上当前新词元的K和V)。
  3. 增量解码:在预测下一个词元时,只需要将新词元的输入向量传入模型。模型仅计算该新词元的Q、K、V,并将其K和V追加到缓存。然后使用更新后的完整缓存进行注意力计算,输出新词元的表示。

以下是一个高度简化的代码框架,展示其思想:

class AttentionLayerWithKVCache: def __init__(self, d_model, n_heads): # ... 初始化投影矩阵等参数 ... self.k_cache = None # 键缓存 self.v_cache = None # 值缓存 def forward(self, x, use_cache=False): # x 是输入,在推理后期通常只有新词元的嵌入 Q = self.w_q(x) K = self.w_k(x) V = self.w_v(x) if use_cache: if self.k_cache is None: # 第一次调用,初始化缓存 self.k_cache = K self.v_cache = V else: # 非第一次调用,将新的K和V追加到缓存 self.k_cache = torch.cat([self.k_cache, K], dim=1) # 沿序列维度拼接 self.v_cache = torch.cat([self.v_cache, V], dim=1) # 使用完整的缓存作为当前步的K和V K = self.k_cache V = self.v_cache # 使用Q和(可能是缓存的)K、V计算注意力 attention_output = self.compute_attention(Q, K, V) return attention_output 

键值缓存虽然显著提升了推理速度,但也带来了一个重要的“阴暗面”——内存消耗随上下文长度线性增长

这正是像GPT-4这样支持更长上下文(例如32K)的模型服务成本更高的主要原因之一。更长的上下文意味着需要维护更大的键值缓存,消耗更多的GPU内存,从而增加了每次推理的计算资源成本。

本节课中我们一起学习了键值缓存。我们从语言模型的推理过程出发,理解了重复计算K和V导致的效率问题,进而引入了键值缓存作为解决方案,并分析了其实现原理。最后,我们也认识到键值缓存导致内存消耗随上下文窗口线性增长的局限性。正是为了克服这个局限性,DeepSeek等模型才引入了像“多头潜在注意力”这样更高级的优化技术。在接下来的课程中,我们将深入探讨这一机制。

在本节课中,我们将开始学习如何解决KV缓存带来的内存问题。我们将首先回顾KV缓存的概念,理解其优缺点,然后深入探讨多查询注意力机制,这是解决内存问题的关键技术之一。


上一节我们介绍了KV缓存。现在让我们快速回顾一下什么是KV缓存、为什么需要它、它的优点以及缺点。

这一切始于观察语言模型的推理流程。语言模型推理过程如下:首先有一个输入令牌序列。假设我们有四个令牌“the next day is”,然后需要预测下一个令牌,这是语言模型推理的主要任务。这个输入序列会经过整个架构,经过第一个数据预处理块,经过Transformer块,经过输出层,然后我们得到一个逻辑矩阵,通过它预测下一个令牌。

这里需要认识的一个关键点是:为了预测下一个令牌,我们只需要最后一个令牌的上下文向量。让我解释一下:一旦我们进入多头注意力层,我们就能得到最后一个令牌与所有其他令牌之间的注意力分数,这样我们就能得到最后一个令牌的上下文向量。这个上下文向量然后经过所有剩余的层,经过这些输出层,最终我们得到一个逻辑向量,对应于“is”,这是我输入序列中的最后一个令牌。这个逻辑向量是一个向量,如果词汇表大小为50,000,它就有50,000个参数。然后我选择具有最高值或最高概率的索引,然后找到对应于这个索引的令牌,这就是我的下一个令牌预测,即“bright”。

因此,LLM推理过程中需要记住的第一个关键见解是:为了获得下一个令牌预测,我只需要输入序列中最后一个令牌的上下文向量。我不需要其他上下文向量。这是第一个见解。

第二个见解是:假设“the next day is”是输入,“bright”是输出。现在这个“bright”被附加到我的输入中,成为我的新输入序列。所以新的输入序列是“the next day is bright”,它将再次通过整个架构,我们预测新的令牌。让我们看看当这个新的输入矩阵通过Transformer块时会发生什么,特别是当它通过多头注意力块时会发生什么。

在这里我们看到,我们将得到查询矩阵、键矩阵、值矩阵、注意力分数、注意力权重和上下文矩阵。但如果你仔细观察这些我标记的黑框,我在这里标记了一个黑框,在这里标记了一个黑框,在这里标记了一个黑框,在上下文矩阵中也标记了。

第二个关键认识是:所有这些黑框都已经在我的前一次迭代中计算过了。在我的前一次迭代的多头注意力中,我已经计算了所有这些查询、键、值,除了这里对应的新令牌的最后一行。我还没有计算那个。所以第二个认识是:我在这里做了很多重新计算,我可以存储一些东西吗?这就是缓存的想法出现的地方。


然后我们开始回溯以预测下一个令牌。我需要做的是根据这些输入预测下一个令牌。为了预测下一个令牌,我需要什么?我只需要对应于最后一个令牌的上下文向量。为了得到那个上下文向量,我只需要的是:我需要我的最后一个令牌的注意力权重乘以值矩阵。为了得到注意力权重,我需要注意力分数。为了得到注意力分数,我需要查询向量乘以键。

现在我标记在黑框中的所有东西都被缓存了。这意味着为了得到键矩阵,我将缓存我之前的键,我将缓存我之前的值,并且我只计算这个新行,我只计算这个新行。

因此,每当一个新的令牌进来时,我需要做的所有事情是:我必须找到对应于新令牌的查询向量,我必须找到对应于新令牌的键向量并将其附加到缓存键中,这样我就得到了总的键矩阵。我需要做的第三件事是:我必须找到新令牌的值向量,并且我必须将其附加到缓存值中。

然后我将查询与键转置相乘,从中得到注意力分数,得到注意力权重,将其与值矩阵相乘,这样我就得到了最后一个令牌的上下文向量。最后一个令牌的这个上下文向量将遍历剩余的旅程,然后它将预测我的下一个令牌。

所以你看到我们在这里做了什么:我们根本不重新计算值和键,我们从之前的迭代中存储它们,这就是缓存的想法,我们缓存键和值。


正如我们在上一讲中看到的,缓存带来的好处是它减少了我们需要做的计算量。如果你看这一行,当我们实现缓存时,我们需要做的计算量仅随输入令牌的数量线性增加。然而,如果你不做缓存,计算量会呈二次方增加。

所以实现KV缓存的一个好处是:它减少了我们需要做的计算量,最终降低了我们的成本,这对我们来说是件好事。

那么坏处是什么?KV缓存的坏处或丑陋之处在于:我们正在存储一些东西。就像每一块土地一样,拥有每一块土地,我们都必须付出代价。同样,对于存储在内存中的每一块数据,对于存储在内存中的每一个参数,我们都必须付出代价。

当你查看KV缓存时,需要存储的参数数量由这么多给出。

为什么是这么多?让我们先看一个Transformer块。假设我有四个令牌“the next day is”。假设这是我的四个令牌,每个令牌的维度是四。现在这个维度是注意力头的数量乘以头维度,所以是n * H。这里的行数等于我的上下文大小,所以是s。这只是一个批次。但如果我在多个批次中处理,那么这应该乘以B。这就是B * n * s * H * s的由来。这些是需要存储的参数数量。记住,如果这是键矩阵,将有一个值矩阵与之相伴,所以这将乘以二。然后我只是展示了一个Transformer块,语言模型中通常有多个Transformer块,所以这将乘以L,即Transformer块的数量。这个额外的“2”是每个浮点数的字节数。我假设每个存储的参数是16位,即2字节。所以KV缓存的大小由L * B * n * H * s * 2 * 2给出。


如果你有GPT-2,那是36MB的内存。如果你有GPT-3,那是4.5GB的内存。


本节课中我们一起学习了KV缓存的基本概念及其内存问题。我们回顾了KV缓存的工作原理,理解了它通过存储键值对来减少计算量的优点,同时也认识到它带来的显著内存开销。在下一节中,我们将深入探讨多查询注意力机制,这是解决KV缓存内存问题的关键技术之一。

在本节课中,我们将要学习分组查询注意力。这是解决KV缓存内存问题的关键技术之一,也是通往DeepSeek所采用的多头潜在注意力(MHA)的重要一步。

大家好,欢迎来到“从零构建DeepSeek”系列的下一个讲座。今天,我们将学习分组查询注意力。在上一讲中,我们开始学习可用于解决KV缓存内存问题的技术。我们学习的第一种技术是多查询注意力,而今天我们将学习分组查询注意力。

首先,让我们快速回顾一下在多查询注意力中学到的内容。

本质上,一切都始于观察KV缓存。在推理过程中,我们缓存键矩阵和值矩阵,因此在预测下一个token时,无需反复进行重复计算。这就是KV缓存的主要思想。

尽管KV缓存提供了许多好处,例如它提供了输入token与计算时间之间的线性关系(如果不进行缓存,计算需求会随输入token数量呈二次方增长;而进行缓存,计算需求则呈线性增长,从而节省大量计算成本),但它也有一个我们在上一讲中学到的“阴暗面”:它占用了大量内存。

实际上,KV缓存的内存需求或大小随着Transformer块的数量、批处理大小、注意力头的数量、注意力头维度和上下文长度的增加而增长。

例如,对于GPT-2这样相对较小的模型,KV缓存仅为36MB。但对于GPT-3,KV缓存增长到4.5GB。对于像DeepSeek-R1或V3这样更大的模型,它使用了61个Transformer块。即使我们假设推理时批处理大小为1,注意力头数量为128,注意力头维度也为128,上下文长度为100,000。在这些参数下,DeepSeek模型的KV缓存大小变得高达400GB。

如果我们在内存中占用如此多的空间,它会减慢我们的计算速度,并且我们必须支付更多费用。想象一下,一家公司用这种普通的KV缓存变体构建了一个基础模型,该公司必须支付大量费用,因为它必须在推理期间存储大量参数,这自然增加了公司向客户收取的推理费用。但正如我们所知,DeepSeek的API调用推理成本非常低,低得令人难以置信。所以很明显,它没有使用这种普通的KV缓存变体,对吧?

因此,人们已经找到了解决KV缓存内存问题的方法,而最终的方法是DeepSeek实施的新创新:多头潜在注意力。但要达到这一点,还有另外两项创新:第一个是我们上一讲中学到的多查询注意力,今天我们将学习分组查询注意力。

多查询注意力的关键概念非常简单。我们在上一讲中学到,在普通的多头注意力中,当我们查看可训练的键矩阵和可训练的值矩阵时,每个头本质上都有不同的值。因此,如果你查看头1、注意力头2、注意力头3或注意力头4,这些值彼此不同。这就是我使用的颜色编码所表示的:每个注意力头在WK和WV上都有不同的值。

现在,自然地,由于输入嵌入与WK和WV相乘得到键矩阵和值矩阵,当我们查看键矩阵K时,注意力头1的值与注意力头2的值非常不同,后者又与注意力头3和注意力头4的值不同,值矩阵也是如此。

现在,由于键矩阵和值矩阵的这些值对于不同的头是不同的,我们必须将所有值都存储在缓存中。因此,当我们缓存第一个头时,我们还必须缓存第二个头、第三个头和第四个头。类似地,对于值,我们必须分别缓存所有头。因此,如果你查看KV缓存大小的公式,你会发现它实际上随着注意力头数量的增加而增长:注意力头越多,我们需要存储的参数就越多。

在多查询注意力中,人们做了一个简单的技巧。他们说,如果在WK和WV矩阵中,我首先获取注意力头1的参数,然后为头2、头3和头4复制这些相同的参数,会怎么样?类似地,对于值,我获取头1的参数并为头2、头3和头4复制它。自然地,这也意味着在我的键矩阵K和值矩阵V中,我的注意力头1、头2、头3和头4的值将彼此完全相同。不同头之间的键值将没有差异,头1、头2、头3和头4将是相同的。现在,如果你查看值矩阵,V1的值将与V2、V3和V4的值相同。

因此,在多查询注意力中,键和值矩阵的注意力头都包含相同的信息,实际上是完全相同的参数。你可以想象取K1并复制它以形成K2、K3和K4,取V1并复制它以得到V1、V2、V3和V4。自然地,这样做的结果是,既然所有头的值都相同,我们只需要缓存或存储其中一个头。我们不需要存储所有不同头的矩阵,因为这些值字面上是相同的,对吧?因此,我们可以在KV缓存的大小中去掉注意力头数量n这个因子。

所以,当我们使用多查询注意力时,假设DeepSeek有128个注意力头,我们使用多查询注意力可以将KV缓存大小减少128倍,从400GB减少到仅3GB。

然而,尽管我们节省了KV缓存所需的内存存储量,但多查询注意力有许多缺点,主要缺点是它会导致显著的性能下降。主要原因如下图所示:

上一节我们回顾了多查询注意力的原理及其内存优势,但同时也看到了它可能导致性能下降。本节中,我们来看看分组查询注意力,它旨在在内存效率和模型性能之间取得更好的平衡。

分组查询注意力是多查询注意力和标准多头注意力之间的一个折中方案。其核心思想不是让所有注意力头共享完全相同的键和值(如MQA),也不是让每个头都有完全不同的键和值(如标准MHA),而是将多个注意力头分组,在组内共享键和值。

以下是分组查询注意力的工作原理:

  1. 分组:将所有的 h 个查询头分成 g 个组。例如,如果有128个查询头,我们可以将它们分成8组,每组16个头。
  2. 共享键值:在每个组内,所有查询头共享同一套键矩阵和值矩阵。这意味着,对于第 i 组,我们只有一个键变换矩阵 WK_i 和一个值变换矩阵 WV_i
  3. 独立查询:每个查询头仍然保留自己独立的查询变换矩阵 WQ

用公式和代码来描述这个核心概念:

公式描述:
假设有 h 个查询头,分成 g 组,每组有 h/g 个头。



  • 查询矩阵 Q 的维度为:[batch_size, seq_len, h, d_k]
  • 键矩阵 K 的维度为:[batch_size, seq_len, g, d_k]
  • 值矩阵 V 的维度为:[batch_size, seq_len, g, d_v]

在计算注意力时,属于第 i 组的 h/g 个查询头,都会与第 i 组的键 K_i 和值 V_i 进行计算。

简化代码描述:

# 假设输入 x 的维度: [batch_size, seq_len, model_dim] # h: 总查询头数, g: 组数, d_k: 每个头的键/查询维度 # 1. 线性变换得到查询、键、值 Q = linear_q(x) # 形状: [batch_size, seq_len, h * d_k] K = linear_k(x) # 形状: [batch_size, seq_len, g * d_k] V = linear_v(x) # 形状: [batch_size, seq_len, g * d_v] # 2. 重塑维度,分离出头和组 Q = Q.view(batch_size, seq_len, h, d_k) K = K.view(batch_size, seq_len, g, d_k) V = V.view(batch_size, seq_len, g, d_v) # 3. 计算注意力分数 (以一组为例) # 对于第 i 组,其查询是 Q[:, :, i*(h/g):(i+1)*(h/g), :] # 其键是 K[:, :, i, :],需要广播以匹配查询头的数量 # 注意力分数 = (Q_group_i @ K_group_i.T) / sqrt(d_k) # 输出 = softmax(注意力分数) @ V_group_i # 4. 将所有组的输出拼接起来 

了解了GQA的基本结构后,我们来看看它带来的好处。GQA巧妙地结合了MQA和标准MHA的优点。

以下是分组查询注意力的主要优势:

  • 显著减少KV缓存:与标准MHA相比,KV缓存大小减少了 h/g 倍。例如,如果 h=128, g=8,缓存大小减少为原来的1/16。这比MQA(减少为1/128)的压缩率低,但比标准MHA好得多。
  • 缓解性能下降:与MQA相比,GQA通过保留多组不同的键和值,为模型提供了更多的表达能力和多样性。这通常能带来比MQA更好的模型质量,尤其是在需要复杂推理的任务上。
  • 灵活的权衡g 这个超参数提供了一个旋钮,允许我们在内存开销(KV缓存大小)和模型性能之间进行灵活的权衡。g=1 时退化为MQA(内存最优,性能可能受损);g=h 时退化为标准MHA(性能最优,内存开销最大)。我们可以根据实际硬件约束和任务需求选择合适的 g

本节课中,我们一起学习了分组查询注意力。我们从回顾KV缓存的内存问题以及多查询注意力的解决方案开始,指出了MQA在节省内存的同时可能牺牲模型性能的缺点。然后,我们深入探讨了分组查询注意力,它通过将查询头分组并在组内共享键和值,在内存效率和模型表现力之间找到了一个更好的平衡点。我们了解了它的工作原理、核心公式/代码表示以及其主要优势。GQA是构建高效大型语言模型(如DeepSeek)的关键技术之一,为后续学习更高级的注意力变体(如多头潜在注意力)奠定了基础。

在本节课中,我们将学习DeepSeek模型的核心创新之一——多头潜在注意力机制。我们将从基础概念开始,逐步理解它是如何通过改变注意力机制来显著提升模型效率并降低推理成本的。


大家好,我是Raj Dunecker博士,于2022年从麻省理工学院获得机器学习博士学位,也是“从零构建DeepSeek”系列的创作者。

在开始之前,我想介绍一下本系列的赞助商和合作伙伴——V AI。V AI与我们秉持着相似的原则和理念,即从基本原理构建AI模型。让我展示一下他们的产品。

这是Inviideo AI的网站。凭借一个小型工程团队,他们构建了一个出色的产品,你可以仅通过文本提示创建高质量的AI视频。

例如,我输入一个文本提示:“创建一个超写实的豪华手表视频广告,并使其具有电影感”。点击生成视频后,很快我就得到了这个高度逼真的视频。

这个视频让我着迷的是它对细节的关注。看这里,质量和纹理都非常出色,而这一切都仅从一个文本提示创建。这就是Inviideo AI产品的力量。你刚才看到的精彩视频的支柱,是Inviideo AI的视频创作流程,他们正在从第一性原理重新思考视频生成和编辑。

为了实验和调整基础模型,他们拥有印度最大的H100和H200集群之一,并且也在试验B200。Inviideo AI是印度发展最快的AI初创公司,面向全球构建产品,这也是我与他们产生共鸣的原因。好消息是,他们目前有多个职位空缺,你可以加入他们优秀的团队。更多详情我将在下方描述中发布。


大家好,欢迎来到“从零构建DeepSeek”系列的这一讲。

今天可能是本系列中最重要的课程之一,因为今天我们将看到DeepSeek如何通过发明多头潜在注意力机制,彻底改变了注意力机制,或者说重写了Transformer架构。

DeepSeek在其Transformer架构中实现更高效率的原因,以及他们在保持Transformer架构性能的同时成功减少KV缓存大小的原因,是DeepSeek变得如此流行且推理成本如此之低的主要原因之一。

而这一推理成本降低背后的主要创新,就是多头潜在注意力机制。这个在DeepSeek论文中创新的机制,实际上改变了Transformer架构的这个组件。

如果你看整个LLM的架构,有输入块、处理器块和输出块。他们引入的潜在注意力机制,改变了Transformer架构的这个方面,即多头注意力机制。

因此,今天我们将探索通往多头潜在注意力的漫长旅程,理解潜在注意力如何工作,并理解潜在注意力背后的直觉。我将在白板上从零开始详细展示每一个细节,以确保你不会对潜在注意力感到困惑。


如果你查看2024年6月发布的DeepSeek V2论文,这里就是他们介绍多头潜在注意力架构的地方,并且这里提供了潜在注意力架构的示意图。

向下滚动,你会看到他们有多个潜在注意力机制的变体。今天我们将看到最简单的变体,他们称之为低秩键值联合压缩。在本系列后面的课程中,我们将看到解耦旋转位置嵌入的含义,但今天不会涉及。今天我们将看到多头潜在注意力的最简单版本。

浏览这篇论文时,你会看到他们有四个图示:多头注意力、分组查询注意力、多查询注意力,最后是多头潜在注意力。今天我们将快速学习前三种,最后再深入了解多头潜在注意力。


我之前已经在单独的课程中介绍过键值缓存、多查询注意力和分组查询注意力。但我知道可能有些人是第一次听这堂课,所以为了这些人,我将稍微快速地回顾所有这些概念,然后再进入多头潜在注意力。这样,这堂课本身就是一个自包含的讲座。

目前关于多头潜在注意力的可用资源存在的主要问题是,如果你搜索“多头潜在注意力”,你会找到像这样的博客文章。浏览这些博客文章,你会发现它们只有段落和这些令人难以置信难以理解的数学公式。没有任何内容是从零开始解释的,它不适合那些想从矩阵计算、矩阵乘法层面理解潜在注意力工作原理的人。本讲座旨在为所有那些想要打开机器学习黑匣子、理解其内部运作原理的人而准备。

我花了很长时间准备这堂课,但现在终于准备好了,让我们开始吧。


首先,我们必须理解什么是键值缓存,然后理解键值缓存的“阴暗面”,接着理解人们为解决键值缓存问题做了什么,即多查询注意力和分组查询注意力,最后我们将了解潜在注意力如何工作。

我们的故事始于语言模型的推理阶段。

假设预训练已经完成,现在我们处于推理阶段。在推理阶段,当你访问ChatGPT并输入一些内容时会发生什么?例如,你输入“为西班牙制定一个旅行计划”。ChatGPT或任何语言模型所做的,是每次预测一个令牌,这是推理的主要目的。在推理过程中,我们一次预测一个令牌。

假设输入令牌是“the”、“next”、“day”这三个。在推理过程中,我们必须预测下一个令牌。这三个令牌会经过整个LLM架构:它们经过数据预处理层、Transformer层,最后经过输出层。最终,我们在输出层得到逻辑矩阵,通过它我们可以预测下一个令牌。在这个例子中,假设下一个令牌是“is”。

这个下一个令牌随后被追加回输入序列,然后输入序列再次通过这个流程。请记住,当输入序列现在通过这个流程时,所有可训练的矩阵和参数都是固定的。然后我们得到下一个令牌,下一个令牌再次被追加回输入序列,整个输入序列再次通过整个流程,我们得到下一个令牌。这就是下一个令牌预测任务的工作方式。

现在,如果你仔细观察这个推理流程,你可能会直觉地意识到:我们是否在重复计算?因为这三个令牌为了预测“is”而经过了整个架构,然后这三个令牌为了预测“bright”又经过了整个架构,接着这三个令牌为了预测“and”再次经过了整个架构。

因此,我们故事的第一个要点是:在推理阶段,我们似乎在进行大量重复计算。这是第一点,请记住这一点。


现在,这是在直觉层面。我们可以做的是:可视化这些重复计算。具体来说,我想关注多头注意力块。请记住,在多头注意力块中发生的是:输入序列进入,我们得到输入嵌入向量,它们被转换为上下文向量。

让我们看看这个多头注意力块内部发生的计算,并检查是否存在重复计算。如果存在重复计算,那么我们将看看如何处理它。但直觉上,我们似乎在进行大量重复计算,对吧?让我们尝试量化这一点。

我有“the”、“next”、“day”、“is”这四个令牌,它们构成了我的输入嵌入矩阵。这四个令牌将与可训练的查询矩阵相乘,与可训练的键权重矩阵和值权重矩阵相乘,即 W_qW_kW_v


在本节课中,我们一起学习了DeepSeek模型的核心创新——多头潜在注意力机制的背景和重要性。我们从语言模型的推理阶段开始,理解了其中存在的重复计算问题,并开始探索如何通过可视化注意力块内部的计算来量化这个问题。在接下来的课程中,我们将深入探讨键值缓存的概念及其挑战,并最终揭示多头潜在注意力机制是如何优雅地解决这些效率问题的。

在本节课中,我们将学习并动手编写DeepSeek架构中引入的多头潜在注意力机制。我们将从最基础的版本开始,理解其核心概念和数学原理,并最终用代码实现它。

在上一节课中,我们初步了解了潜在注意力机制。本节我们将深入探讨其最基础的多头版本,并完成从零开始的代码实现。核心在于理解两个关键点:潜在矩阵吸收技巧

首先,让我们快速回顾潜在注意力机制的基本原理。请始终从大语言模型推理的角度来思考潜在注意力。

假设我们有一个输入序列,包含四个令牌,每个令牌的嵌入维度为八。这可以表示为一个输入嵌入矩阵 X

在潜在注意力中,第一步是将这个输入嵌入矩阵投影到一个潜在空间。为此,我们将输入矩阵 X 乘以一个下投影矩阵 W_DKV。这个矩阵将我们的输入从八维空间投影到一个更低维的空间,例如四维。

这个乘积的结果是一个我们称之为 C_KV 的矩阵。在潜在注意力中,你只需要缓存这一个矩阵。你不需要分别缓存键矩阵和值矩阵,只需缓存 C_KV,这大大减少了KV缓存的内存占用。

获得这个缓存的潜在矩阵后,我们将其分别乘以键上投影矩阵 W_UK 和值上投影矩阵 W_UV,从而得到键矩阵和值矩阵。机制的其余部分与传统的多头注意力块非常相似。

你可能会问,增加这个额外的矩阵究竟实现了什么?为了真正理解这一点,我们需要学习一个巧妙的数学技巧,即将查询权重矩阵 W_Q 和键上投影矩阵 W_UK 合并。

传统上,查询向量是通过输入 X 乘以 W_Q 计算得到的。但我们将把 W_QW_UK 合并,这样我们只需要缓存 C_KV

以下是具体过程:

  • 查询:Q = X * W_Q
  • 潜在矩阵:C_KV = X * W_DKV
  • 键矩阵:K = C_KV * W_UK
  • 值矩阵:V = C_KV * W_UV

由于 C_KV = X * W_DKV,我们可以将键矩阵重写为 K = X * W_DKV * W_UK,值矩阵重写为 V = X * W_DKV * W_UV

我们可以将 W_UK^TW_Q 合并成一个矩阵,即 W_Q * W_UK^T。这个合并后的矩阵在训练时是固定的,我们只需要计算一次。而括号中剩下的 X * W_DKV 正是我们需要缓存的 C_KV

因此,我们得到了一个“吸收查询”:吸收查询 = X * (W_Q * W_UK^T)。我们只需将这个吸收查询向量与缓存的 C_KV 相乘即可得到注意力分数。

让我们总结一下整个计算流程,以清晰理解缓存如何发挥作用。

当一个新的令牌(例如“bright”)到来时,我们首先计算它的吸收查询向量。如果新令牌是一个 d 维向量 x_bright,我们将其与合并后的矩阵 (W_Q * W_UK^T) 相乘,得到吸收查询向量。

为了计算注意力分数,我们只需将这个吸收查询向量与更新后的KV缓存相乘。

同时,我们计算新令牌的潜在向量:c_kv_new = x_bright * W_DKV,并将其追加到先前的KV缓存中,形成更新后的KV缓存。

获得注意力分数后,我们进行缩放和softmax操作得到注意力权重。

为了得到上下文向量,我们将注意力权重与值矩阵相乘。而值矩阵可以通过更新后的KV缓存乘以 W_UV 得到:V = C_KV * W_UV

最后,为了得到逻辑矩阵(用于预测下一个令牌),我们将上下文向量矩阵与输出投影矩阵 W_O 相乘。这里,C_KV 是缓存的,而 W_UV * W_O 也是在训练时固定的,只需计算一次。

以上过程表明,即使我们只缓存了潜在矩阵 C_KV,我们仍然可以计算出逻辑矩阵并进行下一个令牌的预测。

现在,我们将把上述理论转化为代码。以下是实现多头潜在注意力机制的关键步骤。

首先,我们需要初始化所有必要的权重矩阵。

import torch import torch.nn as nn import torch.nn.functional as F class MultiHeadLatentAttention(nn.Module): def __init__(self, d_model, num_heads, latent_dim): super().__init__() self.d_model = d_model self.num_heads = num_heads self.latent_dim = latent_dim self.head_dim = d_model // num_heads # 投影矩阵 self.W_q = nn.Linear(d_model, d_model) # 查询投影 self.W_dkv = nn.Linear(d_model, latent_dim) # 下投影到潜在空间 self.W_uk = nn.Linear(latent_dim, d_model) # 键上投影 self.W_uv = nn.Linear(latent_dim, d_model) # 值上投影 self.W_o = nn.Linear(d_model, d_model) # 输出投影 # 吸收技巧:预先计算合并的矩阵 (W_q * W_uk^T) # 注意:在实际训练中,这通常通过重参数化实现,这里为清晰起见分开表示。 self.absorbed_W_q = None # 将在后续计算 

接下来,实现前向传播逻辑,包括吸收技巧和缓存机制。

 def forward(self, x, past_kv_cache=None): batch_size, seq_len, _ = x.shape # 1. 计算吸收查询 (应用吸收技巧) # 首先计算标准查询 q = self.W_q(x) # [batch, seq, d_model] # 将查询重塑为多头格式 q = q.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) # [batch, heads, seq, head_dim] # 2. 计算潜在KV缓存 C_KV = X * W_DKV c_kv = self.W_dkv(x) # [batch, seq, latent_dim] # 处理缓存:如果提供了过去的缓存,则与当前计算的结果拼接 if past_kv_cache is not None: # past_kv_cache: [batch, past_seq, latent_dim] c_kv = torch.cat([past_kv_cache, c_kv], dim=1) # [batch, total_seq, latent_dim] current_kv_cache = c_kv # 用于返回,供下一个时间步使用 total_seq_len = c_kv.size(1) # 3. 从潜在缓存计算键和值 (K = C_KV * W_UK, V = C_KV * W_UV) # 为高效计算,先投影再重塑为多头 k = self.W_uk(c_kv) # [batch, total_seq, d_model] v = self.W_uv(c_kv) # [batch, total_seq, d_model] # 重塑为多头格式 k = k.view(batch_size, total_seq_len, self.num_heads, self.head_dim).transpose(1, 2) # [batch, heads, total_seq, head_dim] v = v.view(batch_size, total_seq_len, self.num_heads, self.head_dim).transpose(1, 2) # [batch, heads, total_seq, head_dim] # 4. 计算注意力分数: Q * K^T / sqrt(d_k) attn_scores = torch.matmul(q, k.transpose(-2, -1)) / (self.head_dim 0.5) # [batch, heads, seq, total_seq] # 5. 应用注意力掩码(防止看到未来令牌)并计算softmax # 生成一个下三角掩码(包括对角线) mask = torch.tril(torch.ones(seq_len, total_seq_len, device=x.device)).view(1, 1, seq_len, total_seq_len) attn_scores = attn_scores.masked_fill(mask == 0, float('-inf')) attn_weights = F.softmax(attn_scores, dim=-1) # [batch, heads, seq, total_seq] # 6. 计算上下文向量: AttnWeights * V context = torch.matmul(attn_weights, v) # [batch, heads, seq, head_dim] # 7. 合并多头输出并应用输出投影 context = context.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model) # [batch, seq, d_model] output = self.W_o(context) # [batch, seq, d_model] return output, current_kv_cache 

为了展示该机制在自回归生成中如何工作,我们可以模拟一个简单的步骤。

# 模拟参数 d_model = 512 num_heads = 8 latent_dim = 128 # 小于 d_model batch_size = 2 seq_len = 1 # 模拟生成下一个令牌 # 初始化注意力模块 attn = MultiHeadLatentAttention(d_model, num_heads, latent_dim) # 初始输入(例如,起始令牌) x_step0 = torch.randn(batch_size, 5, d_model) # 假设已有5个令牌的序列 output_step0, kv_cache = attn(x_step0) # 初始调用,无过去缓存 print(f"初始输出形状: {output_step0.shape}, KV缓存形状: {kv_cache.shape}") # 模拟生成下一个令牌(步骤1) x_step1 = torch.randn(batch_size, 1, d_model) # 新生成的1个令牌 output_step1, kv_cache = attn(x_step1, past_kv_cache=kv_cache) # 传入上一步的缓存 print(f"步骤1输出形状: {output_step1.shape}, 更新后KV缓存形状: {kv_cache.shape}") 

本节课中,我们一起学习了DeepSeek中使用的多头潜在注意力机制。我们从最基础的版本入手,深入理解了其两个核心:潜在KV缓存矩阵吸收技巧。潜在缓存将输入投影到低维空间,只需缓存此单一矩阵,显著降低了内存占用。吸收技巧通过合并矩阵,使得在推理时只需用新令牌计算吸收查询,并与缓存相乘即可高效完成注意力计算。最后,我们通过代码逐步实现了该机制,并模拟了其自回归推理过程。掌握这一机制是理解现代高效大语言模型推理优化的关键一步。

在本节课中,我们将开始学习位置编码。我们将首先理解为什么在Transformer模型中需要位置编码,然后介绍两种基础的位置编码方法:整数位置编码和二进制位置编码。这些知识是理解后续更高级的旋转位置编码的基础。

在上一节课中,我们学习了多头潜在注意力机制。我们开启这个系列课程的目的是深入理解DeepSeek的架构,并最终从零开始构建其各个组件。

那么,为什么现在要开始学习位置编码呢?让我们了解一下背景。这是2024年6月发布的DeepSeek-V2论文。如果你查看其中的多头潜在注意力部分,他们首先介绍了简化的多头潜在注意力,也就是我们上节课看到的内容。但在此之后,在第2.1.3节,他们引入了一种称为“解耦旋转位置嵌入”的技术。

他们在解耦旋转位置嵌入中所做的,是将多头潜在注意力与一种叫做旋转位置嵌入的技术结合起来。这导致了潜在注意力机制中一个更强大的多头潜在注意力版本。在我们之前看到的潜在注意力机制中,我们并没有包含位置嵌入,特别是旋转位置嵌入。

为了理解这个高级的多头潜在注意力机制,我们确实需要理解旋转位置嵌入的含义。如果你查看2025年发布的DeepSeek-V3论文,它最终引发了整个DeepSeek革命,并催生了DeepSeek-R1等模型,你会发现他们直接从这个默认使用旋转位置编码的多头潜在注意力开始。

因此,我们现在用两到三节课来讲解位置嵌入或位置编码的原因,是我们最终想要理解旋转位置编码是什么,然后我们将理解多头潜在注意力是如何与旋转位置嵌入结合,以创建一个更高级的潜在注意力机制版本。

我计划这样划分课程:在今天的课程中,我将向你介绍什么是位置嵌入。然后,我们将看看两种类型的位置编码:整数位置编码和二进制位置编码。这就是今天课程的目的。在下一节课中,我们将学习正弦位置编码,这种编码是在《Attention Is All You Need》论文中引入的。然后,在之后的课程中,我们最终将学习旋转位置嵌入,这将帮助我们理解这些旋转位置嵌入是如何与潜在注意力结合的。

我之所以想按这个顺序进行,是希望你能从零开始了解每一个细节。如果我们直接开始讲旋转位置嵌入,可能会丢失一些理解和概念。我想带你了解这些进步是如何被发现的,所以我会从今天课程的第一步,走到第二步,再到第三步。

好了,让我们开始今天的课程,我们将涵盖以下三部分内容:位置编码介绍、整数位置编码和二进制位置编码。

首先,什么是位置编码,以及为什么我们首先需要位置嵌入或位置编码?

主要原因是,一个词在句子中出现的位置对于句子的上下文非常重要。

让我澄清这一点。假设有一个句子:“The dog chased another dog”。这里有两个“dog”。假设我完全不考虑位置嵌入或位置编码,这意味着什么?这意味着通常在Transformer架构的输入块中,当给定的输入文本进入时,它首先被分词,转换成词元嵌入,然后我们将位置嵌入加到词元嵌入上,这就产生了所谓的输入嵌入,然后被传递到Transformer块。

假设我们根本没有这个位置嵌入层,只有词元嵌入。现在,如果我输入这个句子“The dog chased another dog”,并通过输入块传递。会发生的情况是,这两个“dog”都会被转换成词元嵌入。这两个词的词元嵌入是相同的,因为它们是同一个单词。所以,第一个“dog”的词元嵌入向量是这个,第二个“dog”的词元嵌入向量也是这个,这两个是完全相同的向量。现在,这两个向量将直接传递到Transformer层,而不添加任何类型的位置信息。这意味着对于这两个词元,输入到Transformer块的内容是完全相同的。

这进一步意味着,当我们从注意力块出来并获得上下文向量时,我们得到第一个“dog”的上下文向量和第二个“dog”的上下文向量,这两个上下文向量将完全相同。因为现在Transformer块对这两个“dog”的输入是完全相同的,所以它们将在Transformer块中经历完全相同的操作,导致这两个“dog”的上下文向量完全相同。

这完全不是我想要的。我希望我的Transformer块能够捕捉到这两个是不同的“dog”这一事实。我不希望我的Transformer认为它们是相同的狗,我希望这个“dog”和那个“dog”的上下文向量完全不同。这就是位置非常重要的原因。如果我们不使用位置编码,注意力机制对这两个“dog”的输出将完全相同。这不好。

从注意力块出来后,我们希望模型能够理解句子的上下文。我们希望模型理解,正在追逐的第一个“dog”与被追逐的第二个“dog”是不同的。因此,在我们进入Transformer块之前,我们想在这个“dog”和那个“dog”之间创建一些区别。我们实现这一点的方法是在词元嵌入向量上添加另一个向量,这就是位置嵌入向量。

现在,让我们看看第一种简单的位置编码方法:整数位置编码。

以下是整数位置编码的基本思想:我们为序列中的每个位置分配一个唯一的整数。例如,在句子“The dog chased another dog”中:

  • “The”在位置0
  • “dog”在位置1
  • “chased”在位置2
  • “another”在位置3
  • “dog”在位置4

然后,我们不是直接使用这些整数,而是将它们转换为一个向量。一种简单的方法是使用一个可学习的嵌入层,类似于词元嵌入层。这个嵌入层将整数位置索引映射到一个固定维度的向量。

公式表示:
假设我们的位置嵌入维度是 d_model,我们有一个可学习的嵌入矩阵 P,其形状为 (max_seq_len, d_model)。对于序列中位置为 pos 的词元,其位置编码向量 PE(pos) 就是矩阵 P 的第 pos 行。



代码描述:

import torch import torch.nn as nn # 假设最大序列长度为512,模型维度为768 max_seq_len = 512 d_model = 768 # 定义一个可学习的位置嵌入层 position_embedding = nn.Embedding(max_seq_len, d_model) # 为一个长度为5的序列生成位置索引 positions = torch.tensor([0, 1, 2, 3, 4]) # 获取位置编码向量 pos_embeddings = position_embedding(positions) # 形状: (5, 768) 

这种方法简单直观。然而,它有一个主要的局限性:它无法处理训练时未见过的、超过 max_seq_len 的序列长度。模型无法为位置512生成有意义的嵌入,因为它只学习到了0到511的位置。

为了克服整数编码的长度限制,人们提出了二进制位置编码。其核心思想是使用二进制表示来编码位置。

二进制位置编码的工作原理如下:首先,将位置索引转换为二进制形式。然后,将这个二进制表示的每一位都视为一个独立的特征维度。由于二进制表示是确定性的,模型理论上可以泛化到任意长的序列,只要二进制位数足够。

步骤说明:

  1. 确定位置编码的维度 d_model。这决定了我们用多少位二进制数来表示位置。
  2. 对于给定的位置 pos,将其转换为一个 d_model 位的二进制数。
  3. 将这个二进制数的每一位(0或1)作为位置编码向量的一个元素。

示例:
假设 d_model = 3,我们想编码位置 pos = 5



  • 5的二进制是 101
  • 那么位置编码向量就是 [1, 0, 1]

如果 d_model 更大,比如8,位置5(二进制00000101)的编码就是 [0, 0, 0, 0, 0, 1, 0, 1]

公式表示:
对于位置 pos 和维度索引 i(从0开始),位置编码的第 i 个元素 PE(pos)[i] 可以通过以下方式计算:
PE(pos)[i] = (pos >> i) & 1
其中 >> 是右移位运算符,& 是按位与运算符。

















代码描述:

import torch def binary_positional_encoding(positions, d_model): """ 生成二进制位置编码。 Args: positions: 位置索引的张量,形状为 (seq_len,) d_model: 位置编码的维度 Returns: 位置编码张量,形状为 (seq_len, d_model) """ seq_len = positions.shape[0] # 创建一个形状为 (seq_len, d_model) 的零张量 encoding = torch.zeros((seq_len, d_model), dtype=torch.float32) for i in range(d_model): # 计算每个维度的二进制位 encoding[:, i] = (positions >> i) & 1 return encoding ![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/vizuara-dpsk-scr/img/74fd058a8f902b7e13b8c2a1e66b9d15_15.png) # 示例:为位置 [0, 1, 2, 3, 4, 5] 生成3维二进制编码 positions = torch.tensor([0, 1, 2, 3, 4, 5]) d_model = 3 binary_pe = binary_positional_encoding(positions, d_model) print(binary_pe) # 输出可能类似于: # tensor([[0., 0., 0.], # 位置0: 000 # [1., 0., 0.], # 位置1: 001 # [0., 1., 0.], # 位置2: 010 # [1., 1., 0.], # 位置3: 011 # [0., 0., 1.], # 位置4: 100 # [1., 0., 1.]])# 位置5: 101 

二进制编码解决了长度泛化问题,但它引入了新的问题:向量表示是离散的(只有0和1),并且相邻位置(如7111和81000)的编码可能发生剧烈的汉明距离变化,这与位置关系的平滑性直觉不符。此外,高维二进制向量非常稀疏。

在本节课中,我们一起学习了位置编码的基础知识及其重要性。我们了解到,位置编码是为了让Transformer模型能够区分序列中不同位置的相同词元,从而更好地理解上下文。

我们详细探讨了两种基础的位置编码方法:

  1. 整数位置编码:为每个位置分配一个可学习的唯一向量。优点是简单,但无法处理超过训练时最大长度的序列。
  2. 二进制位置编码:将位置索引用二进制表示,每一位作为编码的一个维度。优点是理论上可以无限扩展长度,但表示是离散且不平滑的,相邻位置的编码变化可能很大。

这两种方法虽然各有优缺点,但它们为我们理解位置编码问题奠定了重要的基础。在下一节课中,我们将学习一种更优雅、更强大的位置编码方法——正弦位置编码,它由《Attention Is All You Need》论文提出,并成为后续许多模型(包括通向旋转位置编码)的基石。

在本节课中,我们将深入探讨Transformer架构中一个关键但可能令人困惑的组件:正弦位置编码。我们将了解它为何被提出,它如何工作,以及它如何优雅地解决了之前位置编码方案(整数和二进制编码)的局限性。

在上一节中,我们介绍了两种基本的位置编码方法:整数编码和二进制编码。整数编码简单但会污染词嵌入的语义信息,而二进制编码解决了数值范围问题,却引入了不连续的跳跃,不利于模型优化。本节中,我们来看看正弦位置编码如何通过引入连续、平滑的函数来解决这些问题。

首先,让我们快速回顾一下之前的内容,以便更好地理解正弦编码的动机。

整数位置编码:为每个位置分配一个整数向量。例如,位置200的编码向量是 [200, 200, ..., 200]。这种方法的主要问题是,位置编码的值可能非常大(取决于上下文长度),从而完全稀释了词嵌入所携带的语义信息。

二进制位置编码:将位置索引转换为二进制表示。例如,位置200(二进制)的8维编码可能是 [1, 1, 0, 0, 1, 0, 0, 0]。这解决了数值范围问题(值被限制在0和1之间),但我们观察到一个关键模式:低位索引(最低有效位)在位置之间变化最快,而高位索引(最高有效位)变化最慢。然而,二进制编码的值是离散的(非0即1),这种不连续性在模型预训练期间会给优化过程带来困难。

那么,我们能否有一种编码,既保留二进制编码中“不同频率变化”的特性,又能使数值变化平滑连续呢?这就是正弦位置编码的出发点。

正弦位置编码的设计目的是解决二进制编码的不连续性问题,并使其变得连续。它基于一个深刻的观察:我们可以用不同频率的正弦和余弦函数来模拟二进制编码中不同比特的振荡行为。

以下是正弦位置编码的核心公式。对于给定位置 pos 和编码向量中的维度索引 i,其编码值计算如下:

  • 如果 i 是偶数(即 i % 2 == 0):
    PE(pos, i) = sin(pos / 10000^(i / d_model))




  • 如果 i 是奇数(即 i % 2 == 1):
    PE(pos, i) = cos(pos / 10000^((i-1) / d_model))




其中:

  • pos 是单词在序列中的位置(0, 1, 2, ...)。
  • i 是编码向量的维度索引(0, 1, 2, ..., d_model-1)。
  • d_model 是词嵌入和位置编码的维度。

这个公式可能看起来复杂,但我们可以从几个关键角度来理解它:

  1. 连续性与平滑性sincos 函数的值域是连续的 [-1, 1]。这完美替代了二进制编码中离散的 {0, 1},使得编码值的变化是平滑的,极大地方便了基于梯度的优化算法。
  2. 频率随维度变化:注意公式中的分母 10000^(i / d_model)。随着维度索引 i 的增大,这个分母会以指数方式增长,导致 pos 除以一个更大的数。这意味着:
    • 对于较小的 i(低位维度),pos / 大数 变化相对较快,因此 sin/cos 函数的振荡频率(变化快)。
    • 对于较大的 i(高位维度),pos / 超大数 变化非常缓慢,因此 sin/cos 函数的振荡频率(变化慢)。
    • 这精确地模拟了我们在二进制编码中观察到的模式:低维编码细粒度变化(高频),高维编码粗粒度变化(低频)
  3. 正弦与余弦交替使用:交替使用 sincos 函数为每个位置提供了一个唯一且丰富的表示。它确保了即使模型遇到比训练时更长的序列,基于三角函数的性质,编码也能在一定程度上外推。

想象一下,我们将位置编码向量的每一个维度(共 d_model 个)都画成一条随着位置 pos 变化的曲线。

  • 维度0(i=0)的曲线会像高频波一样快速上下摆动。
  • 维度 d_model-1i 最大)的曲线则会像一条非常平缓、几乎像直线一样的低频波。
  • 中间的维度则具有介于两者之间的频率。

所有这些都是平滑、连续的曲线,没有跳跃。模型可以轻松地学习这些模式。

本节课中,我们一起学习了Transformer中至关重要的正弦位置编码

  • 动机:为了解决二进制位置编码的不连续性问题,同时保留其不同维度以不同频率变化的优点。
  • 核心方法:使用不同频率的正弦(sin)余弦(cos) 函数来生成位置编码。公式确保了低维度高频率(细粒度变化),高维度低频率(粗粒度变化)。
  • 优势
    1. 值域合适:编码值被限制在 [-1, 1] 的连续区间内,不会像整数编码那样淹没词嵌入信息。
    2. 平滑连续sin/cos 函数是平滑的,便于模型优化。
    3. 蕴含相对位置信息:由于三角函数的性质,模型有可能学习到如“位置5和位置7的距离与位置20和位置22的距离相似”这样的相对位置关系。

正弦位置编码是Transformer能够有效处理序列顺序的基础。理解了它,就为我们下一节探讨更先进的旋转位置编码奠定了坚实的基础。旋转位置编码正是为了解决正弦编码在自注意力机制中的某些局限性而诞生的。

在本节课中,我们将要学习一种称为旋转位置编码(Rotary Positional Encoding, RoPE)的技术。这是理解DeepSeek等现代大型语言模型架构的关键组成部分。


在之前的课程中,我们开始研究不同类型的位置编码。特别是在上一讲中,我们详细研究了正弦位置编码。在那之前的课程中,我们研究了整数和二进制位置编码。你可能会想,为什么我们要在“从零开始构建DeepSeek”系列中花时间讲解位置编码。

主要原因是,如果你查看DeepSeek的论文,会发现它们使用了多头潜在注意力机制。我们已经介绍了MLA最基本的形式。但2025年发布的DeepSeek-R1和DeepSeek-V3,将多头潜在注意力与一种特殊类型的位置嵌入相结合,这种嵌入被称为旋转位置编码。如果不理解旋转位置编码,我们就无法理解它如何与多头潜在注意力相结合。这就是为什么我们在过去几讲中一直在学习不同类型的位置编码。现在我们已经积累了足够的知识,可以最终探讨旋转位置编码究竟是什么。


上一讲末尾,我们看到了正弦位置编码的一些局限性。一个主要的限制是,在正弦位置编码中,我们将位置编码值直接添加到词元嵌入中。我们知道,词元嵌入本质上捕获了语义信息。通过将位置信息直接添加到语义中,我们污染了词元嵌入所携带的语义信息。

理想情况下,我们希望的是以下情况。让我们看看整个LLM架构。目前,对于正弦位置编码,我们正在将词元嵌入与位置编码相加。理想情况下,我不希望这样。理想情况下,我希望我的词元嵌入能够不受干扰地传递到Transformer块中,这样它们的语义含义就不会被稀释。我们在上一讲末尾提出的问题是,我们能否以某种方式在注意力机制本身中注入关于词元位置的信息。

与其在第一个数据预处理步骤中将位置嵌入添加到词元嵌入中,为什么不在多头注意力机制中包含位置嵌入的信息呢?这就是旋转位置编码思想的诞生之处。


上一讲我们看到,如果你查看多头注意力机制,它由计算注意力分数组成。我们有查询矩阵和键矩阵,查询与键的转置相乘得到注意力分数。正是在这个机制中,我们考虑了不同词元的位置并计算注意力分数。

那么,为什么我们不将位置信息添加到这些向量中呢?为什么我们不将位置信息注入到查询向量和键向量中?这是第一个问题。我们在上一讲末尾提出的第二个问题是,如果你看到词元嵌入只是被添加到位置嵌入中,词元嵌入在此步骤中被添加到位置嵌入中,这改变了词元嵌入本身的大小,这并不理想。

理想情况下,我希望的是不改变原始向量的大小。假设这是我的原始查询向量,与其添加另一个向量并形成一个新的向量,为什么不旋转这个向量呢?如果我想捕获信息或注入关于位置的信息,为什么不取一个查询向量或键向量,简单地将其旋转一个角度θ来注入位置信息呢?如果一个词元的位置较高,我将以不同的角度旋转它;如果它的位置索引较低或位置较小,我以较小的角度旋转它。本质上,角度将量化关于我位置的一些信息,但向量的大小不会改变,因为我只是进行旋转。查询向量或键向量的大小不会改变,这将确保我的原始向量大小保持不变。

因此,我们从今天这堂课开始,有两个核心思想:

  1. 与其在数据预处理块中添加位置嵌入,为什么不在注意力机制中添加它,特别是修改查询向量和键向量?
  2. 与其简单地将我的位置编码向量添加到查询向量和键向量中,为什么不直接旋转这些向量,使原始向量的大小保持不变?

有了这两个想法,我们开始今天关于理解旋转位置编码的课程。如果你理解了旋转位置编码,你将永远不会忘记它,因为它是一个非常直观的概念。但如果你直接进入数学部分,一开始可能会觉得困难甚至令人生畏。我将尽力使这堂课尽可能直观。

旋转位置编码的主要思想是获取我的查询向量和键向量,并对这些向量应用正弦位置编码。这意味着,在上一讲中我们看到,正弦位置编码有这个公式,其中任何给定位置的偶数索引由正弦值给出,奇数索引由余弦值给出。那么,如果我们使用相同的公式,但现在将其应用于查询向量和键向量,并且不是将其添加到向量中,而是进行旋转,让我们看看如何做到这一点。

现在,我将通过一个视觉示例来演示旋转位置编码在实践中是如何工作的。

为了演示的目的,假设我有这些查询向量,对应五个词元。你也可以想象这是查询或键向量。第一个查询向量是一个四维向量。第二个查询向量是“dog”,也是一个四维向量。第三个是“chased”,另一个“dog”。所以,我输入序列中的每个词元现在都是一个四维查询向量。你也可以将其视为键向量。我现在为查询向量展示的内容同样适用于键向量。

假设我们有五个查询向量,对应“The dog chased another dog”。现在,我将只关注第一个查询向量。我们将对第一个查询向量执行的操作,与我们将对后面的词元执行的操作相同。因此,我将只在我的第一个词元上展示这个操作。


本节课中,我们一起学习了旋转位置编码的基本概念和核心思想。我们了解到,旋转位置编码旨在解决传统位置编码(如正弦编码)中语义信息被“污染”以及向量大小改变的问题。其核心思想是将位置信息通过旋转操作注入到注意力机制的查询和键向量中,而不是在预处理阶段直接相加。这种方法保持了原始向量的长度,并更自然地将位置信息整合到模型对词元关系的理解中。在接下来的课程中,我们将深入探讨其数学实现和具体应用。

在本节课中,我们将要学习DeepSeek V3和DeepSeek R1模型如何实现一个结合了多头潜在注意力与旋转位置编码的先进版本。我们将深入探讨其背后的数学原理和实现细节,让复杂的公式变得清晰易懂。

大家好,我是Raj Dunecker博士,于2022年从麻省理工学院获得机器学习博士学位,也是“从零开始构建DeepSeek”系列的创作者。在开始之前,我想介绍一下本系列的赞助商和合作伙伴——V AI。

V AI与我们秉持着相似的原则和理念,致力于从基本原理构建AI模型。让我向你们展示一下。

这是V AI的网站。凭借一个小型工程团队,他们构建了一个令人惊叹的产品,你可以仅通过文本提示来创建高质量的AI视频。

如你所见,我输入了一个文本提示:“创建一个超写实的豪华手表视频广告,并使其具有电影感”。点击生成视频后,很快我就得到了这个高度逼真的精彩视频。

这个视频让我着迷的是它对细节的关注。看这里,质量和纹理简直不可思议,而这一切都仅从一个文本提示创建而来。这就是V AI产品的力量。

你们刚才看到的精彩视频背后的支柱,是V AI的视频创作流程。他们正在从第一性原理重新思考视频生成和编辑。为了实验和调整基础模型,他们拥有印度最大的H100和H200集群之一,并且也在试验B200。

V AI是印度发展最快的AI初创公司,面向全球构建产品,这也是我如此认同他们的原因。好消息是,他们目前有多个职位空缺,你可以加入他们优秀的团队。我在下面的描述中发布了更多详细信息。

大家好,欢迎来到“从零开始构建DeepSeek”系列的这一讲。

今天我们将学习DeepSeek V3和DeepSeek R1如何实际实现了一个结合了多头潜在注意力与旋转位置编码的先进版本。

在之前的课程中,我们已经学习了潜在注意力和旋转位置编码。因此,在本节课中,我将假设你已经了解这两个概念。如果你还没有看过关于这两个概念的课程,请返回复习,因为它们对今天的课程至关重要。

那么,让我们开始吧。

首先,我们需要理解为什么需要调整潜在注意力机制以包含旋转位置编码。要理解这一点,我们必须先明白潜在注意力为何有效。

回顾一下我们之前在潜在注意力中见过的示意图。输入嵌入乘以这个 W_DKV 矩阵,将我的输入向量投影到一个潜在维度。在多头潜在注意力中,我们只需要缓存这个潜在矩阵。

这种缓存之所以有效,是因为潜在注意力实现了一种称为吸收技巧的方法。

如果你计算注意力分数,它将是查询向量乘以键向量的转置。因此,查询可以表示为 x * W_Q,这是一个可训练的权重矩阵。而键可以表示为:如果你看这里,键是 C_KV * W_UK。所以键可以表示为,键的转置是 W_UK^T * W_DKV^T * x^T

这里我希望你关注的主要点是,在潜在注意力机制中,这两个矩阵被吸收成一个单一的矩阵。因此,W_QW_UK^T 变成了一个单一的矩阵,剩下的就是 x * W_DKV^T,而只有这个需要被缓存。

这被称为吸收技巧。每当一个新的查询到来时,它乘以 W_Q,同时也乘以 W_UK^T。这两个矩阵在训练时是固定的,所以我们不需要再次缓存或计算它们。唯一需要缓存的是输入的潜在矩阵。

这就是你需要理解的关于潜在注意力及其工作原理的主要内容。如果你还没有学习过多头潜在注意力机制,请去学习,因为我们在那里详细介绍了这个吸收技巧。

为了让潜在注意力工作,我们需要 W_QW_UK^T 在一起,这样它们才能相乘并吸收成一个矩阵。

请记住这一点。

现在,想象一下我们想要将旋转位置编码添加到我的查询和键中。假设我想在潜在注意力中做完全相同的吸收技巧,但我希望我的查询被注入旋转位置编码。我称之为 RPE,这是我的查询 x * W_Q。同时,我也希望我的键被注入旋转位置编码,所以对于这个键矩阵,我应用旋转位置编码。

让我简要回顾一下旋转位置编码中到底发生了什么。

假设我们正在看一个特定位置的一个查询向量或键向量。我们将其分成两个一组,然后每一组两个元素被旋转以形成另一个向量。例如,这里我们有两组:[x1, x2][x3, x4],这是第一个位置或第一个查询向量。发生的情况是,这个 [x1, x2](这是我这里的原始向量)被旋转,形成 [x1', x2']。然后 [x3, x4](这是我的原始向量)被旋转,形成 [x3', x4']

因此,如果我原始的查询向量是 [x1, x2, x3, x4],那么当你应用旋转位置编码时,它就变成了 [x1', x2', x3', x4']。所以,如果我对 x1, x2, x3, x4 应用RoPE,它就变成了 x1', x2', x3', x4'

因此,每当我稍后在描述中使用这个 RPE 操作或 RoPE 操作时,这就是将要发生的事情。我们接收完整的向量,将其分成若干对,然后旋转。

本节课中,我们一起学习了DeepSeek模型如何将旋转位置编码整合到多头潜在注意力机制中。我们首先回顾了潜在注意力的核心——吸收技巧,然后探讨了引入RoPE后带来的挑战。理解这些基础概念是读懂DeepSeek V2和V3论文中复杂公式的关键。下一节,我们将具体分析DeepSeek论文中的公式,并一步步拆解其实现逻辑。

在本节课中,我们将学习DeepSeek模型架构中的另一项核心创新:混合专家模型。我们将了解它的基本概念、它在Transformer架构中的位置,以及它如何优化模型性能。


在之前的课程中,我们深入探讨了DeepSeek的主要创新之一:多头潜在注意力。它通过减少KV缓存大小同时保持语言性能,取得了两全其美的效果。

除了潜在注意力,DeepSeek的另一项重大创新就是混合专家模型。本节课我们将介绍混合专家模型的核心思想。在后续课程中,我们将深入其数学原理,并探讨DeepSeek在此基础上的新贡献。

需要指出的是,混合专家模型作为一个概念已经存在很长时间。第一篇引入此概念的论文发表于1991年的《Neural Computation》期刊,题为“Adaptive Mixtures of Local Experts”。作者之一Geoffrey Hinton是机器学习多个领域的先驱。因此,混合专家模型并非全新概念,在DeepSeek之前,它已在其他语言模型架构中有所应用。

DeepSeek在此基础上进行了创新,加入了他们自己的新技巧。我认为DeepSeek做得非常出色的一点在于,他们没有重新发明轮子,而是借鉴了已有的成果,并在此基础上构建了真正酷炫且创新的东西。

这篇1991年的论文并非专门针对语言建模,而是将混合局部专家模型用于监督学习任务。后来,该思想才通过Mistral等模型逐渐引入语言建模领域。最终,DeepSeek利用了所有这些知识,构建了更强大的模型。

我在此简要介绍混合专家模型的历史背景,是为了说明为何我称其为DeepSeek的一项创新。请注意,他们并非此概念的首创者。


接下来,我将通过直观的方式介绍混合专家模型的整体思想。

DeepSeek的另一项创新,是对混合专家模型技术的创造性运用。首先,我们来理解混合专家模型在语言建模中是如何应用的。

由于本系列课程专注于大语言模型,我将直接从其在语言模型中的应用开始讲解,而不追溯其完整历史背景。

观察Transformer模块,其结构大致如下:输入块包含分词、词嵌入和位置嵌入;Transformer模块是核心,包含层归一化、多头注意力、Dropout、另一个层归一化、前馈神经网络、Dropout;输出层将输入嵌入转换为逻辑矩阵,并预测下一个词元。

混合专家模型这项创新,主要关注整个架构中的一个特定模块:前馈神经网络

仔细观察这个前馈神经网络,其结构类似这样:假设输入嵌入维度为 d_model,它首先将输入投影到一个隐藏层,该隐藏层的维度通常是输入维度的4倍(即 4 * d_model``),然后再投影回原始的输入维度 d_model`。

我喜欢称这个网络为“扩展-收缩”神经网络。从初始层到隐藏层是扩展过程,从隐藏层回到原始维度是收缩过程。使用这样的网络可以在保留原始维度的同时,让输入嵌入经历扩展和收缩。

此处使用前馈神经网络的原因在于,它允许语言模型探索更丰富的空间,极大地增加了维度数量。因此,它是Transformer架构的关键组成部分。实验表明,如果移除该组件,语言模型的性能会下降。前馈神经网络是语言建模架构的关键组件之一。

但需要注意前馈神经网络实际占用的参数量。让我们估算一下:假设一个Transformer模块的输入嵌入维度为768。在扩展层,我们有768个输入和 4 * 768 个隐藏层维度。因此,扩展层的权重参数量为 768 * (4 * 768)。收缩层的参数量类似,也是 768 * (4 * 768)

计算一下:768 * (4 * 768) = 2,359,296。将其乘以2(扩展层和收缩层),得到约4.7百万参数。这仅是一个Transformer模块的前馈网络参数量。如果有12个这样的Transformer模块,前馈网络部分的总参数量将达到约56百万。

我展示这个计算是为了说明,前馈神经网络拥有大量参数。这会影响整个语言模型的训练时间,也会增加模型的推理时间。

混合专家模型通过减少预训练时间和推理时间来优化这一点。


现在,让我们看看混合专家模型是如何实现这一优化的。

首先,我们称这个神经网络为压缩-扩展或扩展-收缩网络。

本节课我们一起学习了混合专家模型的基本介绍。我们了解了它的历史渊源,明确了它在DeepSeek创新中的位置,并分析了其在Transformer架构中针对前馈神经网络进行优化的动机。下一节课,我们将深入探讨混合专家模型的具体工作机制和数学原理。

在本节课中,我们将继续学习混合专家模型。我们将通过一个手把手的视觉化示例,深入理解其核心概念——稀疏性与路由机制——是如何在数学上实现的。


上一节我们介绍了混合专家模型的基本思想。本节中,我们将通过一个循序渐进的视觉化演示,具体展示混合专家模型是如何工作的。我们将重点关注两个核心概念:稀疏性路由机制


在标准的Transformer架构中,每个前馈神经网络层只有一个网络。混合专家模型的主要思想是,将这个单一的前馈神经网络替换为多个并行的神经网络,我们称这些网络为“专家”。

你可能会想,为什么要用一个更复杂的机制来替代简单的单一网络?混合专家模型的主要优势在于,它允许以更少的计算资源进行预训练,这意味着预训练和推理过程都能更快地完成。

混合专家模型能够实现这一点的关键,在于一个简单的概念:稀疏性。稀疏性的核心思想是,当一组输入令牌进入多个专家网络时,并非所有专家都会被激活。这意味着,当一个令牌输入时,它不会被路由到所有专家,而是有选择地只路由到特定的专家。

这进一步意味着,在预训练和推理过程中,所有专家不需要同时处于活跃状态。根据令牌的类型,会激活一组特定的、专门的专家。这种不激活所有专家的概念,就称为稀疏性。

稀疏性是混合专家模型背后的主要思想之一。在今天的课程中,我们将看到稀疏性是如何在数学上实现的。

在上一讲中,我们还探讨了模型的可解释性。事实证明,每个专家本质上都学习了一些特定的知识。例如,在某个Transformer层中,可能有一个专门处理标点符号的专家。这意味着,当输入包含标点符号令牌时,它会被路由到这些专门的专家。当输入包含动词时,它被路由到处理动词的专家;当输入包含视觉描述时,它被路由到处理视觉描述的专家。

因此,根据令牌的类型,我们选择它应该被路由到哪些专家。这减少了预训练时间,也加速了推理过程。这就是混合专家模型背后的主要思想。

我们还看到,模型中有多个Transformer块,每个Transformer块都有不同的专家。对于一个给定的令牌,并不意味着如果在第一个Transformer块中激活了专家一,在第二个Transformer块中也会激活同一个专家。对于一个给定的令牌,在不同的Transformer块中可能会激活不同的专家。


今天的主要目的是向你逐步展示混合专家模型是如何实际实现的。这意味着,给定任何一个令牌(例如,一个维度为768的令牌),在它通过所有专家处理后,它仍然应该保持其768的维度。在这个过程中发生了什么?我们如何利用所有专家来最终输出一个与输入维度相同的输出?

在这个过程中,我们将看到几个关键思想:

  1. 稀疏性:并非所有专家会同时被激活。
  2. 路由机制:一个基于给定令牌来决定将其路由到哪个专家的机制。

我们将按照以下七个步骤来解析这个过程:

  1. 输入嵌入矩阵
  2. 路由计算
  3. 选择Top-K专家
  4. 创建掩码
  5. 专家前向传播
  6. 加权组合输出
  7. 最终输出

我们将以完全视觉化的方式规划这堂课,以便你准确理解混合专家模型的实现。让我们从第一步开始。


这是最简单的一步。在这一步中,我们本质上是从输入嵌入矩阵开始的。假设我们有四个令牌:“The”、“next”、“day”、“is”。每个令牌的维度是768。

每个令牌在到达这个前馈神经网络阶段之前,都经历了一整个流程:令牌化、转换为令牌嵌入、添加位置嵌入、进入Transformer层、经过层归一化、多头注意力、Dropout、另一层层归一化等等。在经历了所有这些序列之后,我们得到了这些输入嵌入,现在它们是我们前馈神经网络的输入。在这里,我们将看到如何用多个神经网络(专家)来实现混合专家模块,而不是一个单一的前馈神经网络。

所以,这是我的输入矩阵,有四个令牌,每个令牌的嵌入维度为8。接下来我们要做的是,将这个输入矩阵传递给一个拥有三个专家的混合专家模型。


我们的主要想法是,假设你有这个输入嵌入矩阵,我有专家一号、专家二号和专家三号。每个专家都是一个神经网络,一个保持输入维度的“扩展-收缩”神经网络。当输入嵌入矩阵通过专家网络时,每个令牌的初始维度(例如8)会被保留。

当输入嵌入矩阵通过专家一(即第一个前馈神经网络)时,我们得到专家输出一,这是一个4x8的矩阵。为什么是4x8?这一点稍后会变得清晰,请尝试理解其背后的直觉。

在下一步中,我们将看到路由机制是如何决定每个令牌应该去往哪个专家的。

在本节课中,我们将学习混合专家模型中两种关键的平衡技术:辅助损失和负载均衡。这些技术旨在确保所有专家都能被有效利用,避免某些专家被过度使用而另一些被闲置,从而提升模型的学习效率和性能。

上一节我们介绍了混合专家的实现细节,特别是路由机制。本节中,我们来看看如何确保路由的平衡性。我们将深入探讨两种方法:辅助损失和负载均衡。

在混合专家模型中,路由机制为每个令牌选择一部分专家。到目前为止,我们尚未引入确保这种路由平衡的机制。这可能导致某些专家被频繁选择,而其他专家则很少被使用,从而造成学习效率低下和性能不理想。理想情况下,我们希望所有专家都能为模型做出贡献。

为了确保专家选择不会失衡,我们在训练过程中引入了一个称为辅助损失的项。大型语言模型的主要任务是下一个令牌预测,其训练过程包含一个主要的训练损失。辅助损失项被添加到这个主训练损失中,用于惩罚不平衡的专家选择,并推动路由函数朝着更均匀的令牌路由分布发展。也就是说,在路由完成后,我们希望看到每个专家处理的令牌数量大致相同。

现在,让我们逐步说明辅助损失是如何计算的。我们从专家选择权重矩阵开始理解。

以下是专家选择权重矩阵的一个示例:

令牌/专家 专家1 (E1) 专家2 (E2) 专家3 (E3) 令牌1 0.0 0.6 0.4 令牌2 0.9 0.0 0.1 令牌3 0.0 0.4 0.6 令牌4 0.5 0.5 0.0

这个矩阵的每一行表示分配给该特定令牌的专家及其权重。例如:

  • 第一行表示令牌1将被路由到专家2和专家3,权重分别为0.6和0.4。
  • 第二行表示令牌2将被路由到专家1和专家3,权重分别为0.9和0.1。

这个矩阵的每一列对应一个特定的专家,包含了所有令牌被路由到该专家的概率。例如,第一列(专家1)包含数值 [0.0, 0.9, 0.0, 0.5]

为了计算每个专家的总体重要性(称为专家重要性),我们将该专家对应列的所有概率值相加。

以下是计算过程:

  • 专家1的重要性0.0 + 0.9 + 0.0 + 0.5 = 1.4
  • 专家2的重要性0.6 + 0.0 + 0.4 + 0.5 = 1.5
  • 专家3的重要性0.4 + 0.1 + 0.6 + 0.0 = 1.1

这些重要性分数反映了所有令牌在路由过程中对每个专家的“关注”程度总和。

接下来,我们计算一个称为专家负载的指标。专家负载衡量的是每个专家实际被“激活”(即被选择)的频率,而不考虑权重。我们通过检查专家选择权重矩阵中每个元素是否大于0来计算。

以下是专家负载的计算过程:

  1. 将权重矩阵转换为二进制矩阵(大于0则为1,否则为0)。
  2. 对每一列(即每个专家)的二进制值求和。

对于我们的示例矩阵:

  • 专家1的负载0 + 1 + 0 + 1 = 2 (令牌2和令牌4选择了专家1)
  • 专家2的负载1 + 0 + 1 + 1 = 3 (令牌1、令牌3和令牌4选择了专家2)
  • 专家3的负载1 + 1 + 1 + 0 = 3 (令牌1、令牌2和令牌3选择了专家3)

现在,我们有了两个关键向量:

  • 重要性向量[1.4, 1.5, 1.1]
  • 负载向量[2, 3, 3]

辅助损失的目标是鼓励这两个向量都尽可能均匀。一个完美的平衡状态是所有专家的重要性相等,且所有专家的负载也相等。

辅助损失的计算公式结合了这两个向量的变异系数(标准差除以均值),旨在最小化这种不平衡性。其核心公式可以表示为:

辅助损失 = α * (专家重要性的变异系数 + 专家负载的变异系数)

其中,α 是一个超参数,用于控制辅助损失项在总损失中的权重。

通过将这个辅助损失项添加到模型的主训练损失(如下一个令牌预测的交叉熵损失)中,反向传播过程会同时优化模型参数和路由机制,促使路由网络更公平地分配令牌给各个专家。

除了辅助损失,另一种直接促进专家间负载均衡的技术称为负载均衡。这种方法的核心思想是在训练过程中,动态地调整路由决策,以防止任何专家过载或欠载。

一种常见的负载均衡实现是在路由函数的Softmax操作中加入一个与专家历史使用频率成反比的偏置项。其思路是:如果一个专家在过去被选择得较多,那么在后续的路由决策中,它被选中的“得分”会被适当降低,从而给其他专家更多机会。

负载均衡的调整可以体现在路由权重的计算中。修改后的路由得分公式可能如下所示:

调整后的路由得分 = 原始路由得分 - λ * 专家历史使用率

其中,λ 是一个控制平衡强度的超参数,专家历史使用率是该专家在近期批次中被选择的频率。

通过这种方式,负载均衡机制在训练过程中实时地、显式地干预路由决策,与辅助损失(一种通过损失函数隐式引导的、更全局的平衡目标)相辅相成,共同确保混合专家模型的高效运行。

本节课中,我们一起学习了混合专家模型中两种至关重要的平衡技术。

首先,我们深入探讨了辅助损失。它是一种通过修改损失函数来鼓励平衡的方法。我们学习了如何从专家选择权重矩阵中计算专家重要性专家负载,并理解了辅助损失如何通过最小化这两个向量的不均匀性,来惩罚不平衡的路由,从而确保所有专家都能被有效利用。

接着,我们介绍了负载均衡技术。这是一种在训练过程中动态调整路由决策的方法,通过根据专家的历史使用情况来微调其被选中的概率,直接防止某些专家过载而另一些闲置。

这两种技术通常结合使用,共同作用于混合专家模型的路由机制,是构建高效、可扩展的大型语言模型的关键组成部分。理解了这些平衡技术,我们就更清楚地掌握了如何让混合专家模型中的每一个“专家”都能发挥其应有的作用。

在本节课中,我们将要学习DeepSeek在混合专家架构中实现的主要创新。我们将探讨他们如何修改现有的损失函数,使其更高效,并介绍三个核心创新点。


混合专家模型通过用一组称为“专家”的神经网络替换传统Transformer架构中的前馈神经网络来提高效率。其核心思想是稀疏性,即每个令牌只被路由到所有专家中的一个子集。在前几讲中,我们介绍了MoE的基本原理、实现步骤以及用于确保专家负载均衡的技术,如辅助损失和负载均衡损失。

上一节我们介绍了用于平衡专家负载的损失函数,本节中我们来看看DeepSeek如何对这些损失函数进行创新性改进,以构建更高效的模型。


在传统的MoE模型中,负载均衡通过添加一个额外的损失项来实现,该损失项旨在确保令牌均匀地路由到不同的专家。这个损失项包含两个关键部分:F_iP_i

以下是负载均衡损失的传统公式:

L_balance = α * N_experts * Σ_i (F_i * P_i)

其中:

  • F_i 表示路由到第 i 个专家的令牌比例。
  • P_i 表示分配给第 i 个专家的概率比例。
  • α 是一个缩放因子。
  • N_experts 是专家总数。

最小化这个损失函数,可以使具有更高重要性的专家按比例处理更多令牌,而重要性较低的专家则处理更少的令牌,从而实现均衡。

然而,DeepSeek提出了一种更简洁的方法。他们发现,可以移除复杂的辅助损失,仅通过精心设计路由机制本身来隐式地实现负载均衡。这意味着模型在训练过程中无需额外计算和优化一个独立的负载均衡损失项,从而简化了训练流程并可能提高效率。


DeepSeek引入的第二个关键创新是“共享专家”的概念。在标准MoE中,每个令牌仅由被选中的少数几个专家处理。但有些基础能力或通用知识可能是所有令牌都需要的。

为了解决这个问题,DeepSeek在MoE层中除了常规的“专属专家”外,还设置了一个或多个“共享专家”。

以下是其工作方式的简化描述:

# 伪代码示意 def moe_layer_with_shared_experts(x, top_k=2): # x: 输入令牌 # 1. 路由逻辑:为每个令牌选择top_k个专属专家 selected_expert_indices, gate_scores = router(x, top_k) # 2. 处理专属专家路径 expert_outputs = [] for i, idx in enumerate(selected_expert_indices): out = expert_layers[idx](x[i]) expert_outputs.append(out * gate_scores[i]) # 加权 # 3. 关键创新:始终通过共享专家处理 shared_output = shared_expert_layer(x) # 4. 合并输出:专属专家加权和 + 共享专家输出 final_output = sum(expert_outputs) + shared_output return final_output 

共享专家就像一个始终处于激活状态的公共处理器,确保所有令牌都能获得一些通用的、基础的特征变换。这有助于模型稳定训练,并可能提升其泛化能力。


第三个创新是“细粒度专家分割”。在传统MoE中,每个专家通常是一个完整的、参数规模较大的前馈神经网络。

DeepSeek对此进行了更细致的划分。他们不是将整个FFN作为一个专家单元,而是将单个FFN内部的权重矩阵(例如,上投影矩阵或下投影矩阵)进行分割,每个分割后的部分可以作为一个独立的“子专家”或“专家片段”。

这个概念可以通过一个公式来理解。一个标准FFN层通常表示为:

FFN(x) = σ(x * W_up) * W_down

其中 W_upW_down 是权重矩阵。在细粒度分割中,我们可以将 W_up 矩阵沿某个维度分割成 E 个块:

W_up = [W_up_1, W_up_2, ..., W_up_E]

然后,路由机制不再选择整个FFN作为专家,而是选择使用哪个 W_up_i 块(以及可能对应的 W_down_i 块)来处理当前令牌。

这种方法的好处在于:

  1. 更高的灵活性:专家单元更小,允许更精细化和多样化的能力分配。
  2. 降低通信开销:在分布式训练中,由于每个专家单元的数据量变小,专家之间交换数据(令牌和激活值)的开销可能降低。
  3. 更好的负载均衡:更小、更多的专家单元使得负载分布更容易均匀。

本节课中我们一起学习了DeepSeek为混合专家模型带来的三项重要创新:

  1. 无辅助损失的负载均衡:通过改进路由机制本身来简化训练目标,移除了独立的负载均衡损失项。
  2. 共享专家:引入一个始终激活的公共专家层,为所有令牌提供通用处理,增强模型稳定性和基础能力。
  3. 细粒度专家分割:将大型专家网络拆分为更小的、更灵活的专家单元,以提高模型的灵活性、降低通信成本并改善负载均衡。

这些创新体现了DeepSeek团队“从第一性原理重新思考”的设计理念,通过对基础架构的深入优化,最终构建出更强大、更高效的DeepSeek模型。

在本节课中,我们将学习如何从零开始用Python和PyTorch实现一个混合专家模型。我们将基于之前课程中介绍的数学原理和DeepSeek的创新点,构建一个完整的MoE层,并将其集成到一个简化的Transformer架构中,用于文本生成任务。

在之前的四节课中,我们介绍了混合专家模型的基本概念、直观理解、数学工作原理,并深入探讨了DeepSeek在MoE架构中的创新。本节课,我们将通过动手编码来总结整个MoE模块的学习。我们将使用一个简单的莎士比亚文本数据集,构建一个包含MoE层的完整模型,并进行预训练和推理。

以下是实现混合专家模型的16个步骤。我们将重点关注MoE模块的构建,同时也会简要介绍整个架构的其他部分。

我们仅使用PyTorch库,不依赖其他高级框架,以确保从零开始构建。

import torch import torch.nn as nn import torch.nn.functional as F 

我们使用一个包含大量莎士比亚文学作品的数据集,文件名为input.txt。这个数据集将用于模型的训练。

每个专家本质上是一个前馈神经网络。它采用扩展-收缩架构:首先将输入维度扩展到四倍,然后使用ReLU激活函数,最后收缩回原始维度。

class Expert(nn.Module): def __init__(self, embed_dim, dropout_rate=0.1): super().__init__() # 扩展层:embed_dim -> 4 * embed_dim self.expansion = nn.Linear(embed_dim, 4 * embed_dim) # 收缩层:4 * embed_dim -> embed_dim self.contraction = nn.Linear(4 * embed_dim, embed_dim) self.dropout = nn.Dropout(dropout_rate) def forward(self, x): x = self.expansion(x) x = F.relu(x) x = self.contraction(x) x = self.dropout(x) return x 

路由器是MoE中最关键的组件。它接收输入矩阵(来自多头注意力块的输出),并通过一个路由权重矩阵计算每个token应该分配给哪些专家。

class Router(nn.Module): def __init__(self, embed_dim, num_experts): super().__init__() # 路由层:决定每个token应路由到哪个专家 self.gate = nn.Linear(embed_dim, num_experts, bias=False) def forward(self, x): # x shape: (batch_size, seq_len, embed_dim) # 计算路由逻辑,得到每个token对每个专家的“偏好分数” logits = self.gate(x) # shape: (batch_size, seq_len, num_experts) return logits 

路由器为每个token计算所有专家的分数后,我们通常只选择分数最高的前K个专家(例如Top-2)。同时,为了实现负载均衡,确保所有专家都能被充分利用,我们需要引入辅助损失函数。

def top_k_gating(logits, k=2): # logits shape: (batch_size, seq_len, num_experts) batch_size, seq_len, num_experts = logits.shape # 获取每个token的前k个最高分数和对应的专家索引 top_k_vals, top_k_indices = torch.topk(logits, k, dim=-1) # 应用softmax,将分数转换为权重(概率分布) top_k_weights = F.softmax(top_k_vals, dim=-1) # 创建一个掩码,标记哪些专家被选中 expert_mask = torch.zeros(batch_size, seq_len, num_experts, device=logits.device) expert_mask.scatter_(-1, top_k_indices, top_k_weights) return expert_mask, top_k_indices, top_k_weights 

为了防止少数专家主导整个模型,我们需要一个辅助损失来鼓励均匀的路由分布。这里我们实现DeepSeek V3架构中提到的辅助损失。

def load_balancing_loss(expert_mask, num_experts): # expert_mask shape: (batch_size, seq_len, num_experts) batch_size, seq_len, _ = expert_mask.shape # 计算每个专家被选中的总概率(跨批次和序列) expert_usage = expert_mask.sum(dim=(0, 1)) # shape: (num_experts,) # 计算每个专家被选中的频率 expert_freq = expert_usage / (batch_size * seq_len) # 计算辅助损失:鼓励所有专家的使用频率接近均匀分布 # 使用平方变异系数(Coefficient of Variation squared) mean_freq = expert_freq.mean() var_freq = expert_freq.var() cv_squared = var_freq / (mean_freq 2 + 1e-6) return cv_squared 

现在我们将专家网络、路由器和路由逻辑组合成一个完整的MoE层。

class MoELayer(nn.Module): def __init__(self, embed_dim, num_experts, k=2, dropout_rate=0.1): super().__init__() self.embed_dim = embed_dim self.num_experts = num_experts self.k = k # 创建专家池 self.experts = nn.ModuleList([Expert(embed_dim, dropout_rate) for _ in range(num_experts)]) # 创建路由器 self.router = Router(embed_dim, num_experts) def forward(self, x): # x shape: (batch_size, seq_len, embed_dim) batch_size, seq_len, _ = x.shape # 1. 路由计算 router_logits = self.router(x) # (batch_size, seq_len, num_experts) # 2. Top-K选择 expert_mask, top_k_indices, top_k_weights = top_k_gating(router_logits, self.k) # 3. 初始化输出张量 output = torch.zeros_like(x) # 4. 对于每个被选中的专家,处理分配给它的token for expert_idx in range(self.num_experts): # 找出哪些token的Top-K中包含当前专家 expert_selected = (top_k_indices == expert_idx).any(dim=-1) # (batch_size, seq_len) if expert_selected.any(): # 获取需要当前专家处理的token expert_input = x[expert_selected] # (num_selected_tokens, embed_dim) # 通过当前专家网络 expert_output = self.experts[expert_idx](expert_input) # (num_selected_tokens, embed_dim) # 获取这些token对当前专家的权重 # 我们需要从expert_mask中提取对应位置的权重 selected_mask = expert_mask[expert_selected] # (num_selected_tokens, num_experts) expert_weight = selected_mask[:, expert_idx].unsqueeze(-1) # (num_selected_tokens, 1) # 加权求和 weighted_output = expert_output * expert_weight # 将结果累加到输出张量的对应位置 output[expert_selected] += weighted_output # 5. 计算辅助损失(用于训练) aux_loss = load_balancing_loss(expert_mask, self.num_experts) return output, aux_loss 

现在我们将MoE层集成到一个标准的Transformer块中,替换原来的前馈网络部分。

class TransformerBlockWithMoE(nn.Module): def __init__(self, embed_dim, num_heads, num_experts, dropout_rate=0.1): super().__init__() # 层归一化 self.ln1 = nn.LayerNorm(embed_dim) self.ln2 = nn.LayerNorm(embed_dim) # 多头注意力机制 self.attention = nn.MultiheadAttention(embed_dim, num_heads, dropout=dropout_rate, batch_first=True) # MoE层(替代标准的前馈网络) self.moe = MoELayer(embed_dim, num_experts, k=2, dropout_rate=dropout_rate) # Dropout self.dropout = nn.Dropout(dropout_rate) def forward(self, x): # 残差连接1:注意力层 attn_input = self.ln1(x) attn_output, _ = self.attention(attn_input, attn_input, attn_input) x = x + self.dropout(attn_output) # 残差连接2:MoE层 moe_input = self.ln2(x) moe_output, aux_loss = self.moe(moe_input) x = x + self.dropout(moe_output) return x, aux_loss 

我们将多个Transformer块堆叠起来,形成完整的模型。

class TransformerWithMoE(nn.Module): def __init__(self, vocab_size, embed_dim, num_layers, num_heads, num_experts, max_seq_len, dropout_rate=0.1): super().__init__() self.vocab_size = vocab_size self.embed_dim = embed_dim # 词嵌入层 self.token_embedding = nn.Embedding(vocab_size, embed_dim) # 位置编码 self.position_embedding = nn.Embedding(max_seq_len, embed_dim) # Transformer块堆叠 self.blocks = nn.ModuleList([ TransformerBlockWithMoE(embed_dim, num_heads, num_experts, dropout_rate) for _ in range(num_layers) ]) # 输出层 self.ln_final = nn.LayerNorm(embed_dim) self.output_layer = nn.Linear(embed_dim, vocab_size) # 辅助损失权重 self.aux_loss_weight = 0.01 def forward(self, tokens): batch_size, seq_len = tokens.shape # 1. 创建词嵌入 token_embeds = self.token_embedding(tokens) # (batch_size, seq_len, embed_dim) # 2. 创建位置编码 positions = torch.arange(seq_len, device=tokens.device).unsqueeze(0).expand(batch_size, seq_len) position_embeds = self.position_embedding(positions) # (batch_size, seq_len, embed_dim) # 3. 组合嵌入 x = token_embeds + position_embeds # 4. 通过所有Transformer块 total_aux_loss = 0 for block in self.blocks: x, aux_loss = block(x) total_aux_loss += aux_loss # 5. 最终层归一化和输出 x = self.ln_final(x) logits = self.output_layer(x) # (batch_size, seq_len, vocab_size) return logits, total_aux_loss * self.aux_loss_weight 

我们需要将文本数据转换为模型可以处理的token序列。

def preprocess_text(text, vocab, max_seq_len): # 创建字符到索引的映射 chars = sorted(list(set(text))) vocab_size = len(chars) char_to_idx = {ch: i for i, ch in enumerate(chars)} idx_to_char = {i: ch for i, ch in enumerate(chars)} # 将文本转换为索引序列 data = torch.tensor([char_to_idx[ch] for ch in text], dtype=torch.long) # 创建训练样本(输入-目标对) inputs = [] targets = [] for i in range(0, len(data) - max_seq_len, max_seq_len): chunk = data[i:i + max_seq_len + 1] inputs.append(chunk[:-1]) targets.append(chunk[1:]) inputs = torch.stack(inputs) targets = torch.stack(targets) return inputs, targets, vocab_size, char_to_idx, idx_to_char 

现在我们实现模型的训练循环,包括主损失和辅助损失。

def train_model(model, inputs, targets, num_epochs, batch_size, learning_rate): device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) inputs, targets = inputs.to(device), targets.to(device) optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate) criterion = nn.CrossEntropyLoss() num_batches = len(inputs) // batch_size for epoch in range(num_epochs): model.train() total_loss = 0 total_aux_loss = 0 for batch_idx in range(num_batches): # 获取当前批次数据 start_idx = batch_idx * batch_size end_idx = start_idx + batch_size batch_inputs = inputs[start_idx:end_idx] batch_targets = targets[start_idx:end_idx] # 前向传播 logits, aux_loss = model(batch_inputs) # 计算主损失(交叉熵损失) main_loss = criterion(logits.view(-1, model.vocab_size), batch_targets.view(-1)) # 总损失 = 主损失 + 辅助损失 loss = main_loss + aux_loss # 反向传播和优化 optimizer.zero_grad() loss.backward() optimizer.step() total_loss += main_loss.item() total_aux_loss += aux_loss.item() # 打印每个epoch的损失 avg_loss = total_loss / num_batches avg_aux_loss = total_aux_loss / num_batches print(f'Epoch {epoch + 1}/{num_epochs}, Loss: {avg_loss:.4f}, Aux Loss: {avg_aux_loss:.4f}') return model 

训练完成后,我们可以使用模型生成新的文本。

def generate_text(model, prompt, char_to_idx, idx_to_char, max_new_tokens=100, temperature=0.8): device = next(model.parameters()).device model.eval() # 将提示转换为token序列 tokens = torch.tensor([char_to_idx[ch] for ch in prompt], dtype=torch.long, device=device).unsqueeze(0) generated = prompt with torch.no_grad(): for _ in range(max_new_tokens): # 获取模型预测 logits, _ = model(tokens[:, -model.max_seq_len:]) # 取最后一个token的预测 next_token_logits = logits[0, -1, :] / temperature # 应用softmax得到概率分布 probs = F.softmax(next_token_logits, dim=-1) # 从分布中采样下一个token next_token = torch.multinomial(probs, num_samples=1) # 将token转换为字符 next_char = idx_to_char[next_token.item()] generated += next_char # 将新token添加到序列中 tokens = torch.cat([tokens, next_token.unsqueeze(0)], dim=1) return generated 

我们需要设置模型的超参数并初始化模型。

def initialize_model(text, max_seq_len=128): # 数据预处理 inputs, targets, vocab_size, char_to_idx, idx_to_char = preprocess_text(text, None, max_seq_len) # 模型超参数 embed_dim = 256 num_layers = 4 num_heads = 8 num_experts = 8 # 初始化模型 model = TransformerWithMoE( vocab_size=vocab_size, embed_dim=embed_dim, num_layers=num_layers, num_heads=num_heads, num_experts=num_experts, max_seq_len=max_seq_len, dropout_rate=0.1 ) return model, inputs, targets, char_to_idx, idx_to_char 

配置训练参数并开始训练。

def main(): # 加载数据 with open('input.txt', 'r', encoding='utf-8') as f: text = f.read() # 初始化模型和数据 model, inputs, targets, char_to_idx, idx_to_char = initialize_model(text, max_seq_len=128) # 训练参数 num_epochs = 10 batch_size = 32 learning_rate = 3e-4 # 训练模型 print("开始训练模型...") trained_model = train_model(model, inputs, targets, num_epochs, batch_size, learning_rate) # 保存模型 torch.save({ 'model_state_dict': trained_model.state_dict(), 'char_to_idx': char_to_idx, 'idx_to_char': idx_to_char, 'config': { 'vocab_size': trained_model.vocab_size, 'embed_dim': trained_model.embed_dim, 'num_layers': len(trained_model.blocks), 'num_heads': trained_model.blocks[0].attention.num_heads, 'num_experts': trained_model.blocks[0].moe.num_experts, 'max_seq_len': 128 } }, 'moe_model.pth') print("模型训练完成并已保存!") return trained_model, char_to_idx, idx_to_char 

训练完成后,我们可以加载模型并生成新的文本。

def load_and_generate(model_path, prompt): # 加载保存的模型 checkpoint = torch.load(model_path, map_location='cpu') # 从检查点获取配置 config = checkpoint['config'] char_to_idx = checkpoint['char_to_idx'] idx_to_char = checkpoint['idx_to_char'] # 重新初始化模型 model = TransformerWithMoE( vocab_size=config['vocab_size'], embed_dim=config['embed_dim'], num_layers=config['num_layers'], num_heads=config['num_heads'], num_experts=config['num_experts'], max_seq_len=config['max_seq_len'] ) # 加载模型权重 model.load_state_dict(checkpoint['model_state_dict']) model.eval() # 生成文本 generated_text = generate_text(model, prompt, char_to_idx, idx_to_char, max_new_tokens=200) print(f"提示: {prompt}") print(f"生成文本: {generated_text}") return generated_text 

最后,我们可以评估模型的性能并分析MoE层的工作情况。

def analyze_moe_layer(model, sample_input): device = next(model.parameters()).device model.eval() with torch.no_grad(): # 获取第一个Transformer块的MoE层 moe_layer = model.blocks[0].moe # 前向传播 router_logits = moe_layer.router(sample_input) expert_mask, top_k_indices, top_k_weights = top_k_gating(router_logits, moe_layer.k) # 分析专家使用情况 expert_usage = expert_mask.sum(dim=(0, 1)) print("专家使用情况:") for i, usage in enumerate(expert_usage): print(f"专家 {i}: {usage.item():.4f}") # 计算负载均衡指标 aux_loss = load_balancing_loss(expert_mask, moe_layer.num_experts) print(f"负载均衡损失: {aux_loss.item():.4f}") return expert_usage, aux_loss 

在本节课中,我们一起学习了如何从零开始实现一个混合专家模型。我们从定义单个专家网络开始,逐步构建了路由器、Top-K选择机制、负载均衡辅助损失,最终将这些组件集成为一个完整的MoE层。我们将这个MoE层集成到Transformer架构中,替换了标准的前馈网络,并实现了完整的训练和推理流程。

通过这个实践项目,我们深入理解了MoE模型的核心概念:

  1. 专家网络:每个专家是一个独立的前馈神经网络
  2. 路由器:决定每个输入token应该分配给哪些专家
  3. Top-K选择:只使用分数最高的K个专家,提高计算效率
  4. 负载均衡:通过辅助损失确保所有专家都能被充分利用
  5. 加权组合:将多个专家的输出按权重组合,形成最终输出

这个实现虽然简化,但包含了MoE模型的所有关键要素。你可以在此基础上进一步优化,比如实现更复杂的路由机制、增加更多专家、或者尝试不同的负载均衡策略。希望这个实践能帮助你更好地理解混合专家模型的工作原理和实现细节。

在本节课中,我们将要学习DeepSeek架构中的第三个重要创新:多令牌预测。我们将了解它与传统单令牌预测的区别,并初步探讨其优势。

大家好,我是Raj Duneja博士,于2022年从麻省理工学院获得机器学习博士学位,也是“从零开始构建DeepSeek”系列的创作者。在开始之前,我想介绍一下本系列的赞助商和合作伙伴:Invideo AI。

我们非常重视基础性内容,即从最基础的原理构建AI模型。Invideo AI的理念与我们非常相似。让我来展示一下。

这是Invideo AI的网站。凭借一个小型工程团队,他们构建了一个出色的产品,你可以仅通过文本提示创建高质量的AI视频。

如图所示,我输入了一个文本提示:“创建一个超现实的豪华手表视频广告,并使其具有电影感”。点击生成视频后,不久我便看到了这个令人惊叹的、高度逼真的视频。

这个视频让我着迷的是它对细节的关注。看这里,质量和纹理都非常出色,而这一切仅从一个文本提示创建。这就是Invideo产品的力量。您刚才看到的精彩视频背后的支柱是Invideo AI的视频创作流程,他们正在从第一性原理重新思考视频生成和编辑。

为了实验和调整基础模型,他们拥有印度最大的H100和H200集群之一,并且也在试验B200。Invideo AI是印度发展最快的AI初创公司,面向全球构建产品,这也是我如此认同他们的原因。好消息是,他们目前有多个职位空缺,您可以加入他们优秀的团队。更多详情我将在下方描述中发布。

大家好,欢迎来到“从零开始构建DeepSeek”系列的这一讲。今天,我们将开始学习一个非常重要的模块,称为多令牌预测。

当你审视DeepSeek架构时,会发现他们在架构上有三个主要的革新:第一个是多头潜在注意力,第二个是在混合专家模块上的创新,第三个则是一个非常巧妙的实现,称为多令牌预测。今天我们将学习这第三个技术。在之前的课程中,我们已经涵盖了DeepSeek在多头潜在注意力和混合专家方面实现的所有内容。

如果你查阅DeepSeek在2025年1月发布的、引发了整个DeepSeek革命的论文,即DeepSeek-V3技术报告,并向下滚动,你会看到:首先,在他们的架构部分,有多头潜在注意力示意图。之后,有一个关于混合专家模块的完整章节,其中包含了诸如无辅助损失负载均衡、共享专家、细粒度专家分割等创新。最后,他们拥有的最后一个内容就是这个多令牌预测。这就是我们今天要看的。

这是DeepSeek关于多令牌预测模块的示意图。这看起来很简单,通常我们在语言模型中执行单令牌预测,那么这个多令牌预测是什么?也许我们是在预测多个令牌。但实际上,这个过程涉及很多复杂性,这就是为什么我们将花费大约两到三讲的时间,非常详细地向你解释多令牌预测的整个概念。一如既往,我们将在白板上展示所有内容,然后我也会带你了解如何从零开始编写多令牌预测模块的代码。

这就是接下来两到三讲的计划。首先,让我带你了解一下多令牌预测的历史。实际上,多令牌预测并非由DeepSeek发明,DeepSeek只是在其基础上进行构建,就像他们对许多其他架构创新所做的那样。

多令牌预测首次在这篇名为《通过多令牌预测实现更好更快的语言模型》的论文中实现。这篇论文由一组研究人员贡献,其中一组来自Meta。DeepSeek基于这篇论文,该论文于2024年4月发表,DeepSeek立即采纳了它,在其基础上构建,并将其应用于他们的V3架构中。

如果你查看摘要,这些作者说:大型语言模型使用下一个令牌预测损失进行训练。在这项工作中,我们建议训练语言模型一次预测多个未来令牌,会带来更高的样本效率。这就是多令牌预测的本质:我们将一次预测多个未来令牌。

让我们开始理解单令牌预测和多令牌预测之间的区别。在本讲中,我将只是引出多令牌预测的概念,展示它与单令牌预测的不同之处,然后我们还将讨论多令牌预测的一些优势。我将在这节课中建立你的直觉。下一讲我们将看到DeepSeek究竟是如何实现多令牌预测架构的,再下一讲我们将在代码中实现多令牌预测。

让我们开始今天的课程。如果你看一下单令牌预测,其流程大致如下:假设我们有一批输入令牌,看起来像这样。让我实际取一批8个输入令牌:“Artificial”、“Intelligence”、“is”、“changing”、“the”、“world”、“right”、“now”。假设我有这批8个输入令牌。单令牌预测的工作方式是:整个批次通过多个Transformer块的序列。这里我展示了三个Transformer块,实际上可能有24个甚至36个Transformer块。这些被称为共享Transformer主干。这是第一篇多令牌预测论文中引入的术语。我将使用相同的术语。这本质上意味着一系列Transformer块链接在一起。

假设我有这一系列Transformer块D1、D2……直到T3,它们链接在一起。之后,我有我的逻辑矩阵,它将每个令牌从嵌入维度转换到词汇表大小。现在,当这8个令牌从Transformer块输出时,最终输出是逻辑矩阵。如果词汇表大小是50,000,那么现在每个令牌都有一个对应的50,000维向量。所有这些令牌现在都有一个对应的50,000维向量。然后,我们查看具有最高关联概率的令牌索引,从而得到为所有输入令牌预测的下一个令牌。

在推理时,唯一重要的下一个令牌是最后一个令牌,那是推断出的新令牌。但在训练时,所有预测出的这些令牌都将用于计算我的训练损失。例如,对于预测出的第一个令牌,实际预测应该是“Intelligence”。如果输入是“Artificial”,实际输出应该是“Intelligence”。对于第二个输入“Artificial Intelligence”,实际输出应该是“is”。这里是实际输出,实际输出应该是“changing”等等。所以我们有实际输出和预测输出。在训练期间,损失是在实际输出和预测输出之间计算的。这本质上是单令牌预测任务。

在多令牌预测任务中,变化在于:对于我正在查看的每个令牌,假设我正在查看“Artificial”。在单令牌预测中,只预测一个令牌。在多令牌预测中,不是预测一个令牌,而是预测三个令牌。假设我称它们为S1、S2和S3。对于每个输入令牌,在预测期间预测这三个令牌。对于这三个令牌,我有我的实际令牌,即S1’、S2’和S3’。然后,我得到每个输入令牌的预测三个令牌和实际三个令牌之间的损失。所以,如果你现在看“Artificial”,你首先定义未来的视野。

本节课我们一起学习了多令牌预测的基本概念。我们了解到,与传统的单令牌预测不同,多令牌预测旨在一次预测多个未来的令牌,这有望提高模型的样本效率。我们还简要回顾了其历史渊源,并初步对比了两种预测方式的流程差异。在接下来的课程中,我们将深入探讨DeepSeek的具体实现方式,并最终动手编写代码。

在本节课中,我们将深入探讨DeepSeek如何具体实现多令牌预测。上一节我们介绍了多令牌预测的基本概念及其与单令牌预测的区别。本节中,我们将详细解析DeepSeek论文中的示意图和公式,理解其实现细节。

多令牌预测的核心思想是,对于一个输入令牌,模型同时预测未来多个位置的令牌,而不仅仅是下一个。这带来了训练信号密集化、数据效率提升、更好的规划能力和更高的推理速度等优势。DeepSeek仅在预训练阶段使用多令牌预测,推理时则切换回单令牌预测。

上一节我们介绍了单令牌预测。在单令牌预测中,多个输入令牌经过一系列Transformer块处理后,其维度保持不变。例如,假设我们有3个维度为8的输入令牌,经过Transformer块后,输出仍为3x8的矩阵。这个矩阵随后通过一个输出头,被转换为一个3x50,000的矩阵(假设词汇表大小为50,000)。对于每个输入令牌(如令牌1、2、3),我们查看其对应行中概率最高的索引,从而预测出下一个令牌。

关键点在于,一个输出头帮助每个输入令牌预测一个未来令牌

自然地,如果我们希望为每个输入令牌预测多个未来令牌,就需要多个输出头。

在今天的讲解中,我们假设预测深度为3,即为每个输入令牌预测未来3个令牌。因此,我们需要三个头:头1、头2和头3。对于每个输入令牌,头1预测第一个未来令牌,头2预测第二个,头3预测第三个。

接下来,我们将通过数学直觉来详细说明这个过程。

假设我们有8个输入令牌(i=0到7),每个令牌的维度为8。我们将以第一个输入令牌(i=0)为例进行说明,但请注意,此流程适用于所有输入令牌

我们预测的深度K=3,即预测K=1, K=2, K=3时的未来令牌。

对于每个预测深度K,我们需要两个输入:

  1. 该深度下的隐藏状态
  2. 该深度下的输入嵌入

输入嵌入比较容易理解,它就是未来位置令牌的嵌入向量。例如,对于i=0的令牌,我们预测未来位置i=1, 2, 3的令牌。因此:

  • 预测深度K=1时,输入嵌入是位置i=1的嵌入向量。
  • 预测深度K=2时,输入嵌入是位置i=2的嵌入向量。
  • 预测深度K=3时,输入嵌入是位置i=3的嵌入向量。

隐藏状态的获取则稍复杂。对于第一个预测步骤(K=1),其隐藏状态就是输入令牌经过所有Transformer块处理后得到的输出。在我们的例子中,i=0的输入令牌经过Transformer块后,输出的第一行向量就是hidden_state_0

因此,对于第一个预测头(K=1),我们有两个输入:位置i=1的input_embeddinghidden_state_0

以下是每个预测头内部发生的操作流程:

  1. 合并操作:首先,将输入嵌入和隐藏状态这两个向量以某种方式合并。
  2. 投影操作:对合并后的结果进行线性投影。
  3. Transformer层:将投影后的结果通过一个Transformer层进行处理。
  4. 输出隐藏状态:经过上述步骤后,得到当前预测深度下的新隐藏状态(例如hidden_state_1)。

这个新产生的隐藏状态(hidden_state_1)将成为下一个预测深度(K=2)的输入之一。具体来说:

  • 预测深度K=2时,输入是:位置i=2的input_embedding和上一步产生的hidden_state_1
  • 同样经过合并、投影、Transformer层处理后,得到hidden_state_2
  • 预测深度K=3时,输入是:位置i=3的input_embeddinghidden_state_2

如此,不同深度的预测通过隐藏状态串联起来,形成了一个链式结构。

最后,每个预测深度产生的隐藏状态(hidden_state_1hidden_state_2hidden_state_3)会分别通过一个逻辑矩阵(logits matrix, 即输出头)进行投影,映射到词汇表维度,从而生成对应位置的令牌预测概率分布。

本节课我们一起学习了DeepSeek实现多令牌预测的具体架构。核心在于为每个输入令牌使用多个预测头,每个头负责预测一个特定未来深度的令牌。预测过程是链式的:每个头的计算依赖于前一个头产生的隐藏状态以及对应位置的输入嵌入。这种设计使得模型能够在预训练阶段学习更丰富的序列结构信息,为下游任务打下坚实基础。

在本节课中,我们将学习并动手编写DeepSeek模型所使用的多令牌预测机制。我们将从零开始,一步步实现这个核心组件。

上一节我们介绍了多令牌预测的理论机制,本节中我们来看看如何用代码实现它。

多令牌预测是一种训练技术,它让模型在每一步同时预测未来的多个令牌,而不仅仅是下一个令牌。这能带来更密集的训练信号、更高的数据效率、更好的规划能力以及更快的推理速度。DeepSeek仅在预训练阶段使用此技术,推理时仍使用单令牌预测。

我们首先需要导入必要的库。在这个简单的演示中,我们只需要PyTorch。

import torch 

在将隐藏状态和输入令牌嵌入向量连接之前,我们需要对向量进行RMS归一化处理。这是DeepSeek论文中提到的步骤。

RMS归一化的计算步骤如下:

  1. 计算向量中每个元素的平方。
  2. 求这些平方值的和。
  3. 计算平方和的平均值。
  4. 取该平均值的平方根,得到RMS值。
  5. 将向量中的每个元素除以这个RMS值。

公式表示为:x_i' = x_i / sqrt(mean(x^2) + epsilon)

以下是RMSNorm类的实现代码:

class RMSNorm: def __init__(self, dim, eps=1e-8): self.eps = eps def __call__(self, x): # 计算平方和 sum_of_squares = torch.sum(x * x, dim=-1, keepdim=True) # 计算均值 mean_of_squares = sum_of_squares / x.size(-1) # 计算RMS值(均方根) rms = torch.sqrt(mean_of_squares + self.eps) # 归一化 return x / rms 

这是本教程最核心的部分。我们将定义一个类来实现多令牌预测机制。其核心思想是:对于输入序列中的每个位置i,我们预测其后续的k个令牌(i+1, i+2, ..., i+k)。

以下是该类的初始化部分,定义了所需的矩阵:

class MultiTokenPrediction: def __init__(self, d_model, depth, n_heads): self.depth = depth # 预测深度,例如3 self.d_model = d_model # 模型维度 # 合并矩阵:用于合并隐藏状态和输入嵌入 self.merge_matrices = torch.nn.ModuleList([ torch.nn.Linear(2 * d_model, d_model) for _ in range(depth) ]) # 投影矩阵 self.projection_matrices = torch.nn.ModuleList([ torch.nn.Linear(d_model, d_model) for _ in range(depth) ]) # Transformer层(简化版,此处用线性层示意) self.transformer_layers = torch.nn.ModuleList([ torch.nn.Linear(d_model, d_model) for _ in range(depth) ]) # 逻辑矩阵(反嵌入矩阵),用于生成词汇表概率分布 self.logits_matrices = torch.nn.ModuleList([ torch.nn.Linear(d_model, vocab_size) for _ in range(depth) ]) self.rms_norm = RMSNorm(d_model) 

接下来是前向传播方法,它实现了多令牌预测的计算流程:

 def forward(self, hidden_states, input_embeddings): batch_size, seq_len, _ = hidden_states.shape all_predictions = [] # 外层循环:遍历序列中的每个令牌位置 i for i in range(seq_len - self.depth): predictions_at_i = [] prev_hidden = hidden_states[:, i, :] # 第一个头的隐藏状态来自主模型 # 内层循环:对于位置 i,预测后续 depth 个令牌 (k=1,2,...,depth) for k in range(self.depth): # 1. 获取未来第k个位置的输入嵌入 future_embed = input_embeddings[:, i + k + 1, :] # 2. 对隐藏状态进行RMS归一化 normed_hidden = self.rms_norm(prev_hidden) # 3. 合并归一化后的隐藏状态与未来输入嵌入 combined = torch.cat([normed_hidden, future_embed], dim=-1) merged = self.merge_matrices[k](combined) # 4. 线性投影 projected = self.projection_matrices[k](merged) # 5. 通过Transformer层(简化) transformed = self.transformer_layers[k](projected) # 6. 更新隐藏状态,传递给下一个预测头 prev_hidden = transformed # 7. 生成逻辑值(预测分数) logits = self.logits_matrices[k](transformed) predictions_at_i.append(logits) all_predictions.append(predictions_at_i) # 调整输出形状: [batch, seq_len, depth, vocab_size] return torch.stack(all_predictions, dim=1) 

关键点在于,每个预测头(对应一个未来的位置k)的隐藏状态输入依赖于前一个头的输出。这建立了从近到远的因果依赖关系,是DeepSeek实现的一个创新点,不同于传统独立预测每个未来令牌的方法。

定义好类之后,我们可以用它来进行预测。以下是生成预测的示例代码:

# 假设参数 d_model = 512 depth = 3 vocab_size = 10000 batch_size = 2 seq_len = 10 # 初始化模型 mtp_model = MultiTokenPrediction(d_model, depth, vocab_size) # 创建模拟的隐藏状态和输入嵌入(随机值) hidden_states = torch.randn(batch_size, seq_len, d_model) input_embeddings = torch.randn(batch_size, seq_len, d_model) # 前向传播,获得预测 predictions = mtp_model(hidden_states, input_embeddings) print(f"预测张量形状: {predictions.shape}") # 应为 [2, 7, 3, 10000] 

最后,我们需要计算预测令牌与目标令牌之间的损失。对于每个输入位置i和每个预测深度k,我们都有一个预测值和一个对应的真实未来令牌。

以下是损失计算的示例:

def calculate_mtp_loss(predictions, targets): """ predictions: 形状为 [batch, seq_len, depth, vocab_size] 的张量 targets: 形状为 [batch, seq_len, depth] 的张量,包含真实的未来令牌ID """ total_loss = 0 batch_size, seq_len, depth, vocab_size = predictions.shape # 遍历批次、序列位置和预测深度 for b in range(batch_size): for i in range(seq_len): for k in range(depth): # 获取第k个头的预测逻辑值 logits = predictions[b, i, k, :] # 获取对应的真实令牌ID target_token = targets[b, i, k] # 计算交叉熵损失(PyTorch内置函数) loss = torch.nn.functional.cross_entropy(logits.unsqueeze(0), target_token.unsqueeze(0)) total_loss += loss # 计算平均损失 average_loss = total_loss / (batch_size * seq_len * depth) return average_loss # 创建模拟的目标令牌(随机整数) target_tokens = torch.randint(0, vocab_size, (batch_size, seq_len - depth, depth)) # 计算损失 loss = calculate_mtp_loss(predictions, target_tokens) print(f"计算得到的多令牌预测损失: {loss.item()}") 

本节课中我们一起学习了如何从零开始编写DeepSeek的多令牌预测机制。我们回顾了其核心思想:为每个输入令牌同时预测多个未来令牌以提升训练效率。我们逐步实现了四个部分:

  1. 导入必要的PyTorch库。
  2. 实现了RMS归一化层,用于稳定训练。
  3. 构建了核心的MultiTokenPrediction类,其中关键点在于预测头之间的隐藏状态传递,形成了因果依赖。
  4. 演示了如何使用模型进行预测并计算损失函数。

通过本教程,你不仅理解了多令牌预测的原理,也掌握了将其转化为可运行代码的实践能力。这个机制是使DeepSeek模型高效预训练的关键组件之一。

在本节课中,我们将开始学习一个重要的主题——量化。这是构建DeepSeek架构的最后一个支柱。

在之前的课程中,我们已经介绍了多头潜在注意力、专家混合模型以及多令牌预测这三个主要的架构部分。现在,我们将转向量化,这是DeepSeek技术报告中基础设施部分的核心内容,特别是其FP8训练方案。

量化是一个在大型语言模型中至关重要的概念。要理解它,我们首先需要了解模型参数在内存中是如何表示的。

大型语言模型的基本构建模块包括输入和权重。输入与权重相乘,然后通过激活函数。这些激活值又作为后续模块的输入。在整个过程中,有大量的参数需要相互乘加运算。

模型中的每个参数,无论是词嵌入、位置嵌入、注意力机制、前馈神经网络,还是层归一化或输出层中的参数,都会占用内存。参数占用的内存量取决于其表示方式。

最常见的默认参数表示方式是32位浮点数(FP32)。一个FP32数字由三部分组成:

  • 符号位:1位,控制数字的正负。
  • 指数位:8位。
  • 尾数位:23位。

这三部分共同决定了数字的精确值。尾数位的位数直接决定了数字的表示精度。

公式:一个浮点数可以表示为:(-1)^符号位 × 2^(指数 - 偏移量) × (1 + 尾数)

如果同一个参数用16位浮点数(FP16)表示,则它只占用16位内存。其结构变为:

  • 符号位:1位。
  • 指数位:5位。
  • 尾数位:10位。

显然,使用FP16表示可以显著减少内存占用。然而,代价是精度降低,因为用于表示数字细节的尾数位变少了。

简单来说,量化就是使用更低精度的数据类型(如FP16、INT8甚至FP8)来表示模型参数和计算中间结果的过程。其主要目的是:

  1. 减少内存占用:使大型模型能在内存有限的设备上运行。
  2. 加速计算:低精度运算通常在硬件上执行得更快。
  3. 降低功耗:减少数据传输和计算所需的能量。

然而,量化也带来了挑战,即如何在不显著损失模型性能的前提下实现这些好处。这正是DeepSeek在其量化方案中通过一系列创新来解决的问题。

在DeepSeek的技术报告中,他们提出了五项关键的量化创新。在接下来的课程中,我们将逐一深入探讨:

以下是DeepSeek量化方案的五项核心创新:

  1. 混合精度框架:在不同部分灵活使用不同精度的数据类型。
  2. 细粒度量化:对张量的不同部分采用不同的量化策略。
  3. 提高累加精度:在计算中间累加时使用更高精度以保持数值稳定性。
  4. 尾数与指数处理:优化浮点数的尾数和指数部分的表示。
  5. 在线量化:在训练过程中动态地进行量化。

直接阅读论文中的相关图表和描述可能会感到复杂。在后续课程中,我将逐一拆解这些概念,用简单直白的方式解释每个部分的作用和原理。

本节课我们一起学习了量化的基本概念。我们了解到,量化是通过降低参数和计算的数据精度来减少模型内存占用和加速计算的技术。其核心是在精度损失和效率提升之间取得平衡。

我们还预览了DeepSeek为实现高效量化所引入的五项关键技术。在下一节课中,我们将首先深入探讨混合精度框架细粒度量化提高累加精度这三个部分,理解它们如何共同工作以在降低精度的同时保持模型性能。

在本节课中,我们将开始理解DeepSeek在其版本3技术报告中实际实现的量化方法。我们将重点探讨混合精度框架和细粒度量化这两个核心概念。

量化是一种降低模型参数精度的技术,旨在减少模型的内存占用,同时尽可能保持模型的准确性。DeepSeek在其技术报告中提出了多项量化相关的创新,我们将其归纳为五个主要方面:混合精度框架、细粒度量化、增加累加精度、尾数与指数处理以及在线量化。本节课我们将聚焦于前两个方面。

在上一讲中,我们初步了解了量化。其核心思想是:模型中的每个参数都会占用存储空间。如果一个参数用32位浮点数(FP32)表示,那么它占用的空间会比用16位浮点数(FP16)或8位整数(INT8)表示时更多。量化的主要目标就是将参数从高精度(如32位)降低到低精度(如8位或16位),从而显著减少内存需求。虽然精度降低会轻微影响模型准确性,但对于大型语言模型而言,这种性能下降通常是可以接受的。

以下是本节课将涉及的数据格式:

  • FP32:32位浮点数。
  • FP16:16位浮点数,其数值范围远小于FP32。
  • BF16:脑浮点16位,其数值范围与FP32相同,但位数与FP16相同,仍为16位。
  • INT8:8位整数,数值范围非常小(-127 到 127)。

现在,让我们深入探讨DeepSeek实现的第一个核心概念:混合精度框架。在DeepSeek论文的3.3.1节中,他们提出了一个框架,其示意图如下所示。

接下来,我们将解释DeepSeek如何在这个公式中,以不同的量化格式存储权重、输入和输出。

在前向传播(F Prop)过程中:

  1. 输入(x):从BF16格式转换为FP8格式。
  2. 权重(w):本身以高精度(BF16或FP32)存储,但在计算时即时转换为FP8格式。
  3. 计算:FP8格式的权重与FP8格式的输入相乘。
  4. 输出(y):计算结果最初以FP32精度获得,以确保数值稳定性。随后,为了优化内存,该输出被转换并存储为BF16格式。

这样,在前向传播中,DeepSeek通过使用FP8进行计算,显著减少了内存占用,同时利用FP32进行中间计算和BF16进行存储,在内存效率和数值精度之间取得了平衡。

在反向传播过程中,对于某一层,我们需要计算两个梯度:

  1. 权重梯度(W Grad):用于更新该层的权重。公式为:dL/dw = x^T * (dL/dy)
  2. 输入梯度(B Grad):作为上一层的“输出梯度”传递回去。公式为:dL/dx = (dL/dy) * w^T

以下是DeepSeek在反向传播中的处理方式:

  • 计算输入梯度(B Grad)
    • 权重(w)在计算时即时转换为FP8。
    • 来自上一层的输出梯度(dL/dy)以BF16存储,但在计算时转换为FP8。
    • 两者的乘法计算最初以FP32精度进行,结果随后存储为BF16。
  • 计算权重梯度(W Grad)
    • 输入(x)以FP8格式参与计算。
    • 输出梯度(dL/dy)同样从BF16转换为FP8进行计算。
    • 权重梯度(dL/dw)的计算结果以FP32精度存储,这是为了确保后续权重更新时的数值稳定性,因此会转换为低精度格式。

通过这种方式,DeepSeek的混合精度框架在训练的不同阶段(前向和反向传播)智能地分配不同精度的数据类型,最大化内存利用率和计算效率。

上一节我们介绍了DeepSeek如何在操作层面混合使用不同精度。本节中,我们来看看他们如何对模型的不同部分进行更精细的量化处理,即细粒度量化。

标准的量化方法通常对整个张量(Tensor)或整个层使用相同的量化参数(如缩放因子)。然而,模型的不同部分对量化的敏感度可能不同。细粒度量化旨在为模型的不同组件应用不同精度或量化策略,以在保持精度的同时获得更大的压缩收益。

以下是DeepSeek可能采用的几种细粒度量化策略:

  • 分层量化:对网络中不同的层使用不同的位宽。例如,靠近输入的层和靠近输出的层可能对精度更敏感,因此保持较高精度(如BF16),而中间层可以使用更低的精度(如FP8或INT8)。
  • 分组量化:在一个权重张量内部进行分组,每组使用独立的量化参数。这比整个张量使用单一缩放因子能更好地保留信息。
  • 敏感度分析:通过分析各层或各参数对最终输出损失的贡献(敏感度),对敏感度高的部分保留高精度,对敏感度低的部分进行激进量化。

细粒度量化的核心思想是区别对待,而不是“一刀切”。通过这种精细控制,可以在整体压缩率不变甚至更高的情况下,比均匀量化获得更好的模型性能。

本节课我们一起学习了DeepSeek量化方案中的两个重要部分。

  1. 混合精度框架:DeepSeek在前向和反向传播中,动态地将数据在BF16、FP8和FP32之间转换。在计算密集型操作中使用FP8节省内存和计算量,在需要数值稳定性的地方(如梯度累加、权重更新)使用FP32,并以BF16作为主要存储格式来平衡范围和内存占用。
  2. 细粒度量化:其思想是根据模型不同部分对量化的敏感度,施加不同精度或策略的量化,从而实现更优的精度-压缩比权衡。

理解这些基础概念,为我们后续深入探讨增加累加精度、尾数与指数优化以及在线量化等高级主题打下了坚实的基础。在下节课中,我们将继续探索DeepSeek在量化方面的其他创新。

在本节课中,我们将继续探索DeepSeek的量化实现。我们将重点学习三个关键技术:提升累积精度、尾数覆盖指数技术以及在线量化。这些技术共同解决了低精度计算中的数值稳定性问题。

上一节我们介绍了混合精度框架和细粒度量化,本节中我们来看看DeepSeek如何通过创新方法进一步提升量化效果。


在矩阵乘法运算中,当我们使用低精度数据类型(如FP8)进行计算时,会遇到累积精度有限的问题。具体来说,张量核心内部使用约14位精度进行中间结果的累加,这远低于FP32的32位精度。

考虑标准的矩阵乘法公式:

Y = W × X + B 

当W和X都是FP8精度时,多个低精度数值相乘累加会导致中间结果过小,引发下溢问题。这种有限的累积精度在大型矩阵运算(如K=4096的内积维度)中可能产生高达2%的误差,严重影响模型精度。

DeepSeek提出的解决方案基于一个关键观察:张量核心(低精度计算单元)和CUDA核心(高精度计算单元)可以协同工作。

以下是解决方案的两个步骤:

步骤一:低精度MMA累积

  • 初始阶段在张量核心上使用FP8精度执行矩阵乘积累加操作
  • 中间结果以有限精度(约14位)在内部累加
  • 这对应图中的浅紫色部分,表示低精度累积

步骤二:提升到CUDA核心

  • 每处理128个元素后,将部分低精度累积结果复制到高精度寄存器
  • 这些结果在CUDA核心上以完整的FP32精度进行累加
  • 这对应图中的深紫色部分,表示高精度累积

这种“提升到CUDA核心”的操作通过特定的GPU指令实现。图中的箭头清晰地展示了从低精度张量核心到高精度CUDA核心的数据流动路径。通过这种方式,部分和始终存储在高精度内存中,有效避免了累积误差。


现在让我们看看DeepSeek如何通过尾数覆盖指数技术进一步优化量化过程。这项技术解决了动态范围与精度之间的平衡问题。

在深入技术细节前,我们需要理解浮点数的基本结构。一个浮点数通常由三部分组成:

[符号位] [指数部分] [尾数部分] 

其中指数部分控制数值的范围,尾数部分控制数值的精度。

DeepSeek发现,在某些情况下,可以通过“借用”指数位来增加尾数位的数量。具体来说:

标准FP8格式

  • 通常配置为:1位符号 + 5位指数 + 2位尾数
  • 这种配置提供较大的动态范围但精度有限

优化后的格式

  • 重新分配为:1位符号 + 4位指数 + 3位尾数
  • 通过减少指数位增加尾数位,提升精度

这种位重分配可以形式化表示为:

原格式:FP8(s=1, e=5, m=2) 新格式:FP8(s=1, e=4, m=3) 

其中s表示符号位,e表示指数位,m表示尾数位。

这种技术特别适用于权重分布相对集中的情况。当模型参数的值域范围不需要太大动态范围时,牺牲一些范围来换取精度是更优的选择。DeepSeek通过分析模型各层的数值特性,智能地应用这种位重分配策略。


最后,我们探讨DeepSeek的在线量化技术。这项技术解决了静态量化在动态数据分布下的局限性。

传统的离线量化方法基于校准数据集确定量化参数(如缩放因子和零点)。然而,在实际推理过程中,输入数据的分布可能发生变化,导致固定的量化参数不再最优。

DeepSeek的在线量化在推理过程中动态计算量化参数。具体流程如下:

以下是在线量化的关键步骤:

步骤一:实时统计

  • 在推理过程中收集当前输入数据的统计信息
  • 包括最小值、最大值、均值等分布特征

步骤二:动态计算参数

  • 基于实时统计计算缩放因子和零点
  • 使用公式:scale = (max - min) / (2^n - 1)

步骤三:即时量化

  • 使用动态计算的参数对权重和激活进行量化
  • 确保量化过程适应当前输入特性

在线量化的核心计算可以用以下伪代码表示:

def online_quantize(tensor, bits=8): # 步骤1:计算动态范围 min_val = tensor.min() max_val = tensor.max() # 步骤2:计算量化参数 scale = (max_val - min_val) / (2bits - 1) zero_point = round(-min_val / scale) # 步骤3:执行量化 quantized = round(tensor / scale) + zero_point quantized = clamp(quantized, 0, 2bits - 1) return quantized, scale, zero_point 

在线量化的主要优势在于其适应性。它能够:

  1. 处理分布变化的输入数据
  2. 减少分布偏移引起的误差
  3. 在动态环境中保持量化精度

这项技术特别适合处理多样化、非平稳的输入数据,在实际部署场景中表现出色。


本节课中我们一起学习了DeepSeek量化技术的三个关键创新:

  1. 累积精度提升:通过定期将低精度张量核心的中间结果转移到高精度CUDA核心,有效避免了累积误差,在大型矩阵运算中特别重要。
  2. 尾数覆盖指数技术:智能地重新分配浮点数的指数位和尾数位,在动态范围和精度之间找到最优平衡,适应不同层的数值特性。
  3. 在线量化:在推理过程中动态计算量化参数,适应输入数据分布的变化,提高量化模型在实际场景中的鲁棒性。

这些技术共同构成了DeepSeek高效量化的核心,使其能够在保持精度的同时大幅降低计算和存储开销。下一节课我们将探讨这些技术在实际模型中的集成与应用。

大家好,欢迎来到“从零开始构建DeepSeek”系列讲座。

如果你已经坚持学习到这个阶段,我想花点时间祝贺你。因为你已经学习了一些大型语言模型中最先进的概念,这些概念正是DeepSeek在其架构中所使用的。

在本视频中,我计划对本系列讲座迄今为止所学内容进行一次快速总结。我将带你回顾我们何时覆盖了特定的讲座,该讲座具体覆盖了哪些内容,以及它如何融入DeepSeek V3论文中。

如果你在谷歌搜索“DeepSeek V3 Paper”,你会找到这份技术报告。我们整个系列的目标,就是尽可能多地为你解读DeepSeek架构的各个方面,并希望通过白板演示和Google Colab代码来实现。这是我们的播放列表,目前包含大约30个讲座。总的来说,我们在这个系列中覆盖了大量内容。

在本节课中,我们将回顾整个系列的核心内容,总结我们如何从基础概念逐步深入到DeepSeek架构的关键创新点,并理解这些部分是如何组合在一起的。

最初,我们从了解DeepSeek是什么、什么是大型语言模型、什么是涌现特性,以及DeepSeek为何如此受欢迎开始。具体来说,我们将这些讲座的范围缩小到以下几个方面:

  1. 多头潜在注意力
  2. 专家混合
  3. 多令牌预测
  4. 量化
  5. 旋转位置编码

这五个方面是DeepSeek在其架构中的主要创新,它们彻底改变了其预训练和推理的方式。关于DeepSeek的一个非常酷的地方在于,这篇论文非常精炼,但如果你仔细阅读,会发现他们确实解释了一切。例如,论文中只用一两页解释的元素,我们可能需要两到三个讲座来阐述,因为它确实很密集。但如果你学完了这30个讲座,我相信你会对我们迄今为止涵盖的所有架构概念有非常深入的理解。

我们花了30个讲座来涵盖这些概念,从注意力的基础,到多头潜在注意力、专家混合、多令牌预测、量化、旋转位置编码等。现在,让我带你回顾一下我们规划讲座的思路流程。

在进入潜在注意力部分之前,我需要向你解释LLM架构的基础。因此,我们最初开始了这个系列讲座。你会看到,我们最初的视频是关于DeepSeek基础、LLM架构本身、注意力机制、注意力机制的核心、因果注意力机制以及多头注意力机制。

在讲义中,你会看到我们描述了令牌在LLM架构中的旅程、对注意力机制的需求、我们如何从RNN、LSTM发展到注意力机制。然后我们涵盖了自注意力的概念,之后我们研究了带有可训练权重的自注意力概念。在这里,我介绍了可训练查询矩阵、可训练键矩阵、可训练值矩阵的概念,以及我们如何从输入嵌入矩阵计算上下文向量。

在此之后,我们学习了因果注意力机制。其核心思想是,为了预测下一个令牌,我们不能窥视未来。在了解了因果注意力之后,我们看到注意力分数或注意力权重矩阵中对角线以上的元素基本上被设为零,它们不产生影响,因为我们不能窥视未来。

然后,我们涵盖了一个关于多头注意力机制的重要讲座。拥有多个头的主要思想是捕捉多个视角。自注意力在给定的输入序列中只能捕捉单一视角,无法捕捉多个视角。因此,我们了解了为什么需要多头注意力,我们研究了其理论,并查看了代码实现。

通往多头注意力的旅程始于自注意力,然后发展到因果注意力,再到多头注意力。所有这些内容都在第8个讲座“从零开始实现多头注意力”中涵盖完毕。

接下来,我们开始理解潜在注意力的旅程。理解潜在注意力的整个旅程涵盖四个方面:

  1. 键值缓存
  2. 多查询注意力
  3. 分组查询注意力
  4. 多头潜在注意力

这个多头潜在注意力正是DeepSeek彻底改变注意力机制的方式。

我们首先研究了键值缓存,这是整个故事的起点。主要问题是,当我们通过LLM进行推理时,似乎做了很多重复计算,因此键和值需要被缓存,这就是所谓的键值缓存。但键值缓存的缺点是它占用空间,占用大量空间。

为了缓解这些缺点或解决KV缓存内存问题,人们提出了不同的机制,例如多查询注意力和分组查询注意力,在这些机制中,注意力权重基本上在所有头或头组之间共享。但这导致了语言性能的下降。因此,DeepSeek开始思考:我们能否两全其美?我们能否拥有较低的缓存大小,同时获得良好的语言模型性能?这就是潜在注意力思想的诞生之处。

他们引入了一个潜在矩阵。他们获取输入嵌入矩阵,并将其投影到一个维度低得多的潜在矩阵中。这看起来很简单,但实际上它确实解决了KV缓存内存问题,同时我们没有降低语言性能。他们还有一个称为“吸收技巧”的方法,其中查询矩阵(更准确地说是Wq)与WUK合并。这里涉及投影矩阵和下投影矩阵。我们实际上花了很多时间来理解潜在注意力。

首先,我们学习了键值缓存,然后是多查询注意力,接着是分组查询注意力,最后我们准备好理解多头潜在注意力。我们还在Python中从零开始实现了多头潜在注意力。为了达到第13讲,我们必须经历第12讲到第13讲。而如果你看DeepSeek论文的架构部分,他们确实解释了多头潜在注意力,但只用10行文字就解释完了。

这里的主要问题是,我们看到的第一个版本的多头潜在注意力并不是DeepSeek实际使用的版本。DeepSeek实际做的是,他们添加了旋转位置编码,而不是传统的位置编码。

因此,我们必须学习什么是旋转位置编码。我采用了一种非常不同的方法来展示旋转位置编码的旅程。我首先向你展示了什么是整数位置编码、什么是二进制位置编码,以及什么是正弦位置编码。只有在那之后,我才涵盖了什么是旋转位置编码。

我们看到,在旋转位置编码中,我们操作查询和键,而不是操作令牌嵌入。我们分割查询和键,然后旋转它们,再将其添加到查询和键向量中。这不会像原始位置嵌入那样污染令牌嵌入矩阵或向量。

那么,我们为什么要研究旋转位置编码?原因是我们研究旋转位置编码是因为DeepSeek将多头潜在注意力与RoPE整合在一起,所以我们最终必须朝这个方向发展。

如果你看讲座流程,我们有整数和二进制位置编码、正弦位置编码、旋转位置编码,最后我们有了关于DeepSeek如何精确实现潜在注意力的讲座。在这里,我演示了他们如何将潜在注意力与旋转位置编码整合。

这个讲座花了我两个月的时间来准备,因为这不容易解释。在论文中,他们有这9个方程(1,2,3,4,5,6,7,8,9,10,11)来解释潜在注意力。从这些方程中很难理解发生了什么。所以我的任务是将其分解到一个简单的层次,并向你展示他们如何将潜在注意力与旋转位置编码整合,以及为什么这是一项具有挑战性的事情。

本质上,他们所做的是将问题分为两部分,这就是为什么他们称之为解耦RoPE。他们本质上添加了新的矩阵,其中一个矩阵应用了旋转位置编码,另一个矩阵没有应用旋转位置编码。因此,他们通过将一个矩阵分为带RoPE和不带RoPE的两个矩阵来解决这个问题。

本节课中,我们一起回顾了整个“从零开始构建DeepSeek”系列的核心脉络。我们从LLM和注意力的基础概念出发,逐步深入到多头注意力、键值缓存优化,最终抵达DeepSeek的核心创新——多头潜在注意力及其与旋转位置编码的整合。我们还简要提及了后续将涵盖的专家混合、多令牌预测和量化等主题。这个总结旨在帮助你串联起所有知识点,理解DeepSeek架构中各个关键部分的设计动机与相互关系。

小讯
上一篇 2026-03-28 16:20
下一篇 2026-03-28 16:18

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/247755.html