卷了1个多月KV Cache,我终于明白了……

在 Transformer 的 Encoder-base 的模型(如 BERT 系列)中,推理和训练过程保持了高度的统一性(差异仅仅在于是否存在反向过程)。

而在 Decoder-base 的生成式模型(如 GPT系列)中,推理和训练存在相当大的差异性。

主要体现在推理过程具有以下 3 点特征:

  • 自回归
  • 两阶段(第一阶段输入 prompt,第二阶段输入上一个生成的 token)
  • KV cache

以上三点实际上也是相辅相成、不可分割的,其中自回归的生成模式是根本原因,两阶段是外在的体现形式,KV cache 是优化手段。

下面将通过梳理整个推理过程,来理解 KV cache 的作用及优化方法。

01

KV cache 的由来与基本矛盾

图片

第一阶段(prompt 输入):

图片

图片

图片

图片

KV cache 作用过程

第二阶段(token by token):

图片

图片

图片

图片

KV cache的显存占用分析:

图片

图片

图片

可见随着 batch size 和 长度的增大,KV cache 占用的显存开销快速增大,甚至会超过模型本身。

而 LLM 的窗口长度也在不断增大,因此就出现一组主要矛盾,即:对不断增长的 LLM 的窗口长度的需要与有限的 GPU 显存之间的矛盾。因此优化 KV cache 就显得非常必要。

02

KV cache 优化的典型方法

(1)共用 KV cache:MQA,GQA

MQA (Multi Query Attention,多查询注意力) 是多头注意力的一种变体。

其主要区别在于,在 MQA 中不同的注意力头共享一个 K 和 V 的集合,每个头只单独保留了一份查询参数。

因此 K 和 V 的矩阵仅有一份,这大幅度减少了显存占用,使其更高效。由于 MQA 改变了注意力机制的结构,因此模型通常需要从训练开始就支持 MQA。

也可以通过对已经训练好的模型进行微调来添加多查询注意力支持,仅需要约 5% 的原始训练数据量就可以达到不错的效果。

包括 Falcon、SantaCoder、StarCoder 等在内很多模型都采用了 MQA 机制。

# Multi Head Attentionself.Wqkv = nn.Linear(     # Multi-Head Attention 的创建方法    self.d_model,    3 * self.d_model,     # Q、K和V 3 个矩阵, 所以是 3 * d_model    device=device)query, key, value = qkv.chunk(3, dim=2)      # 每个 tensor 都是 (1, 512, 768)
# Multi Query Attentionself.Wqkv = nn.Linear(       # Multi-Query Attention 的创建方法    d_model,    d_model + 2 * self.head_dim,    # 只创建Q的头向量,所以是 1* d_model, 而K和V不再具备单独的头向量, 所以是 2 * self.head_dim    device=device,)query, key, value = qkv.split(    [self.d_model, self.head_dim, self.head_dim],    # query -> (1, 512, 768), key   -> (1, 512, 96), value -> (1, 512, 96)    dim=2)

图片

MHA v.s. GQA v.s. MQA

GQA(Grouped Query Attention,分组查询注意力)是一种介于多头注意力和 MQA 之间的折中方案。

它将查询头(Query Heads)分组,并在每组中共享一个键头(Key Head)和一个值头(Value Head)。

表达能力与推理速度:GQA 既保留了多头注意力的一定表达能力,又通过减少内存访问压力来加速推理速度。

图片

MHA, GQA, MQA 性能比较

(2)窗口优化

图片

图片

3)箭型 attention 窗口,在 LM-Infinit 中就已经被提出了,其基本原理和 StreamingLLM 是一致的。

图片

(3)量化与稀疏

该类方法是基于压缩的思想,通过量化与稀疏压缩 KV cache 的 显存消耗。

当前主流推理框架都在逐步支持 KV cache 量化,一个典型的案例是 lmdeploy,下图展示了其在 TurboMind 框架下 KV INT8 的支持情况。

图片

lmdeploy 的推理特性

稀疏的方法也比较简单,其做法无外乎以下几种方式:

图片

这里最值得一提的是 H2O。简单来说就是通过动态的评价方式来判断需要保留和废弃的 KV 值。

其评估的算法如下所示:

图片

结果显示,在 KV cache 稀疏到只有原来的 20% 时仍然可以保持很高的精度。

图片

(4)存储与计算优化

该方法的典型代表即 vLLM 的 PagedAttention,简单来说就是允许在非连续的内存空间中存储连续的 K 和 V。

FlashDecoding 是在 FlashAttention 的基础上针对 inference 的优化主要分为三步:

  • 长文本下将 KV 分成更小且方便并行的 chunk
  • 对每个 chunk 的 KV,Q 和他们进行之前一样的 FlashAttention 获取这个 chunk 的结果
  • 对每个 chunk 的结果进行 reduce

图片

03

StreamingLLM:简洁高效的“无限长度”

StreamingLLM 的基本思想同样是来源于上述的窗口思想,其最大的创新在于提出了识别并保存模型固有的「注意力池」(attention sinks)锚定其推理的初始 token。下面将详细讨论其工作的原理。

(1)精度是如何保证的?

核心的发现:Lost in the Middle。

多个研究都发现,self-attention 的注意力比较集中于头部和尾部,对文本中段的注意力相对较弱,如下图所示:

图片

绘制出 self-attention 的热力图也能看到这一点,由此当文本长度超过额定长度时,头部的 token 就会被遗弃掉,这就会在 softmax 阶段产生很大的问题。

图片

图片

图片

(2) “无限长度”是如何做到的?

该问实际上可以换种表述为:如何在文本长度不断增加的情况下,保证 GPU 显存不会溢出。

由于该方案主要应用于多轮对话的场景,那么有必要回顾一下当前多轮对话生成的主流做法。

概括起来就以下几点:

  • 将用户输入与模型输出拼接,中间做必要分割;
  • 多个轮次之间倒序排列,并拼接;
  • 如果前边所有轮次长度之和超过最大长度,则截断到最大长度;

上述过程可以用代码描述如下:

    history = ["\n[|Human|]{}\n[|AI|]{}".format(x[0], x[1]) for x in history]    history.append("\n[|Human|]{}\n[|AI|]".format(text))    history_text = ""    flag = False    for x in history[::-1]:        if tokenizer(prompt + history_text + x, return_tensors="pt")["input_ids"].size(-1) <= max_length:            history_text = x + history_text            flag = True        else:            break    if flag:        inputs = tokenizer(prompt + history_text, return_tensors="pt")        input_ids = inputs["input_ids"][:, -max_length:].to(device)        torch.cuda.empty_cache()        return input_ids, text    else:        return None

实际上这就是典型的滑动窗口的做法,滑窗 l 的存在保证了 GPU 的显存不会溢出,但是由于上节的讨论,会存在精度损失。

图片

图片

来源:https://zhuanlan.zhihu.com/p/659770503

如何学习AI大模型 ?

“最先掌握AI的人,将会比较晚掌握AI的人有竞争优势”。

这句话,放在计算机、互联网、移动互联网的开局时期,都是一样的道理。

我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

我意识到有很多经验和知识值得分享给大家,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。【保证100%免费】🆓

对于0基础小白入门:

如果你是零基础小白,想快速入门大模型是可以考虑的。

一方面是学习时间相对较短,学习内容更全面更集中。
二方面是可以根据这些资料规划好学习计划和方向。

😝有需要的小伙伴,可以VX扫描下方二维码免费领取🆓
在这里插入图片描述

👉1.大模型入门学习思维导图👈

要学习一门新的技术,作为新手一定要先学习成长路线图,方向不对,努力白费。

对于从来没有接触过AI大模型的同学,我们帮你准备了详细的学习成长路线图&学习规划。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。(全套教程文末领取哈)
在这里插入图片描述

👉2.AGI大模型配套视频👈

很多朋友都不喜欢晦涩的文字,我也为大家准备了视频教程,每个章节都是当前板块的精华浓缩。

在这里插入图片描述
在这里插入图片描述

👉3.大模型实际应用报告合集👈

这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。(全套教程文末领取哈)

在这里插入图片描述

👉4.大模型落地应用案例PPT👈

光学理论是没用的,要学会跟着一起做,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。(全套教程文末领取哈)

在这里插入图片描述

👉5.大模型经典学习电子书👈

随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。(全套教程文末领取哈)
img

在这里插入图片描述

👉6.大模型面试题&答案👈

截至目前大模型已经超过200个,在大模型纵横的时代,不仅大模型技术越来越卷,就连大模型相关的岗位和面试也开始越来越卷了。为了让大家更容易上车大模型算法赛道,我总结了大模型常考的面试题。(全套教程文末领取哈)

在这里插入图片描述
👉学会后的收获:👈
基于大模型全栈工程实现(前端、后端、产品经理、设计、数据分析等),通过这门课可获得不同能力;

能够利用大模型解决相关实际项目需求: 大数据时代,越来越多的企业和机构需要处理海量数据,利用大模型技术可以更好地处理这些数据,提高数据分析和决策的准确性。因此,掌握大模型应用开发技能,可以让程序员更好地应对实际项目需求;

基于大模型和企业数据AI应用开发,实现大模型理论、掌握GPU算力、硬件、LangChain开发框架和项目实战技能, 学会Fine-tuning垂直训练大模型(数据准备、数据蒸馏、大模型部署)一站式掌握;

能够完成时下热门大模型垂直领域模型训练能力,提高程序员的编码能力: 大模型应用开发需要掌握机器学习算法、深度学习

这份完整版的 AI 大模型学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

😝有需要的小伙伴,可以Vx扫描下方二维码免费领取🆓

在这里插入图片描述

抱歉,作为AI语言模型,我无法提供完整的代码。但是我可以给你一些思路,帮助你实现基于EEPROM的KV存储系统。 1. 首先,需要确定你使用的EEPROM芯片型号,并在代码中引入相应的头文件。 2. 定义数据结构,包括键和值的类型以及长度。例如: ```c typedef struct { char key[10]; int value; } kv_pair_t; ``` 3. 定义读写函数,以读写一个KV对为例: ```c void kv_write(kv_pair_t* pair, int addr) { eeprom_write_block(pair, (void*)addr, sizeof(kv_pair_t)); } void kv_read(kv_pair_t* pair, int addr) { eeprom_read_block(pair, (void*)addr, sizeof(kv_pair_t)); } ``` 这里使用了AVR库函数`eeprom_write_block`和`eeprom_read_block`,这些函数可以直接读写EEPROM中的数据。 4. 实现KV存储系统的增删改查功能,以添加KV对为例: ```c void kv_put(char* key, int value) { // 检查是否已存在相同的键 int addr = kv_find(key); if (addr >= 0) { // 如果存在,则更新该键的值 kv_pair_t pair; kv_read(&pair, addr); pair.value = value; kv_write(&pair, addr); return; } // 如果不存在,则在EEPROM中添加新的KVkv_pair_t pair; memset(&pair, 0, sizeof(kv_pair_t)); strncpy(pair.key, key, sizeof(pair.key) - 1); pair.value = value; kv_write(&pair, kv_size() * sizeof(kv_pair_t)); } ``` `kv_find`函数用于查找指定键的地址,`kv_size`函数用于获取当前存储的KV对数量。 5. 最后,需要在程序启动时初始化EEPROM。例如: ```c void kv_init() { eeprom_write_byte(0, 0xff); // 初始化EEPROM } ``` 这里使用了AVR库函数`eeprom_write_byte`来写入EEPROM的第一个字节,以表示EEPROM已被初始化过。 以上是一个简单的基于EEPROM的KV存储系统实现思路。具体实现需要根据你的需求和使用的硬件进行调整。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值