DeepSeek原生应用与智能体开发实践(人工智能技术丛书)【行情 报价 价格 评测】-京东
王晓华DeepSeek开发AI应用入门书《DeepSeek原生应用与智能体开发实践》全文分享~持续更新-优快云博客
目录
在前面的章节中,我们已经详细讲解了基于DeepSeek的各类应用,展现了它在不同场景下的强大功能与实用价值。然而,我们深知,DeepSeek之所以能在众多领域大放异彩,取得令人瞩目的成功,其背后离不开内在性能的显著提升。性能的提升犹如坚实的基石,支撑着DeepSeek不断拓展应用边界,实现更卓越的表现。
从本章开始,我们将深入探索DeepSeek的核心技术,揭开其性能提升背后的神秘面纱。通过对这些核心技术的剖析,我们不仅能更全面地理解DeepSeek的运作机制,还能领略到其研发团队在技术创新上的智慧与匠心。
自回归生成模型在计算过程中,由于生成的文本长度持续累积,所需处理的输入数据量也随之不断增加,这直接导致了计算量的显著增长。每次迭代时,模型都需要考虑之前所有生成的token,这不仅增加了计算的复杂性,也对资源提出了更高要求。
本章我们将深入探讨DeepSeek中的模型推理加速与计算量优化策略,特别是聚焦于关键的KV Cache(即键值缓存)技术。通过理解和应用KV Cache,我们可以有效减少重复计算,提高生成效率,从而在不牺牲模型性能的前提下,显著减轻计算负担。我们将详细讲解KV Cache的原理、实现方式以及如何在自回归生成模型中应用这一技术。
8.1 自回归生成模型中的资源计算
在自回归生成过程中,每一次推理步骤仅生成一个token,随后将这个新生成的token拼接到当前的输入序列末尾。紧接着,基于更新后的序列,模型进行下一次推理,如此循环往复,直至生成特定的结束标志(如eos,即end of sentence)或达到预设的最大生成长度。
这种逐步生成的方式使得自回归模型能够灵活地处理长文本生成任务。通过逐步构建序列,模型能够考虑之前生成的上下文信息,从而生成更加连贯和符合逻辑的文本。然而,随着生成序列的不断增长,计算量和内存消耗也会相应增加,这对模型的推理效率和性能提出了挑战。
8.1.1 自回归模型的计算量
自回归生成模型是一种生成式模型,它逐个生成序列中的元素(通常是token),每次生成都依赖于之前已经生成的元素。这种依赖关系使得模型能够捕捉序列中的上下文信息,从而生成连贯的文本。
在自回归生成模型的推理过程中,模型需要逐步生成序列中的每个token。假设我们有一个前缀序列,其长度为P,模型从这个前缀开始生成新的序列。随着推理的进行,生成的序列长度逐渐增加,假设当前生成的序列长度为L,前缀+推理示意如图8-1所示。

图8-1 前缀+推理

在推理过程中,模型需要计算每个新生成的token的概率分布。这个计算量取决于当前已生成的序列长度(即前缀长度P加上已生成的长度L)。
生成每个token的计算量:


8.1.2 自回归模型的缓存优化
在上一章我们完成了一个基于注意力的自回归模型的设计,整体模型结构如图8-2所示。
首先,我们的输入是一个序列。这个序列的长度是可变的,并且会加上前次推理生成的token(在图中以深色部分表示)。这些输入通过自回归模型的Embedding权重矩阵进行映射,这两个矩阵的作用是将input_ids映射到高维空间,从而得到hidden_state张量。这个张量包含了输入序列在高维空间中的表示。

图8-2 基于注意力的自回归模型架构
接着,hidden_state张量通过模型的线性变换模块注意力进行处理。这个模块的作用是将hidden_state的维度提升3倍,然后将其分割成查询(Query)、键(Key)和值(Value)三个部分。这三个部分在后续的注意力计算中起着关键作用。
随后,Q、K和V被进一步分割成多个head,这是多头注意力机制的一部分。每个head分别进行注意力计算,即计算Q和K的点积,然后除以K的平方根得到注意力权重,这些权重再与Value相乘得到加权和。多个head的结果拼接起来后,通过另一个线性变换模块进行处理,以恢复hidden_state的原始维度或进行其他变换。
在得到新的hidden_state后,我们进行残差连接,即将新计算的hidden_state与之前的hidden_state相加。这一步有助于缓解梯度消失或梯度爆炸的问题,提高模型的训练稳定性。
接着,残差连接后的hidden_state通过前馈层FFN模块进行处理。前馈层是多个线性变换和激活函数的组合,用于进一步提取特征。处理完后,我们再次进行残差连接,得到更新后的hidden_state。
最后,更新后的hidden_state通过lm_head模块生成logits,即预测token的概率分布。lm_head模块实际上是一个线性映射,将hidden_state的维度从d_model变换到vocab_size(词汇表大小)。这样,我们就可以根据logits得到下一个token的预测结果。
值得注意的是,图8-2中张量里面的深色条带一开始表示的是输入序列的最后一个token。随着前向计算的进行,它逐渐变成了下一个token的概率分布,也就是logits计算矩阵的最后一行。而logits前面的行在推理阶段通常是没有意义的,因为它们代表的是之前已经生成的token的概率分布。
因此,我们不禁思考是否可以只计算最后一行以省略其他行的计算量。通过分析每个模块对最后一行的依赖关系,我们发现lm_head、mlp、layer_norm以及前面的线性变换模块的输出都只与 hidden_state的最后一行相关。这意味着理论上我们可以只计算最后一行来减少计算量。然而,在实际实现中还需要考虑其他因素,如内存访问模式和并行计算效率等。
如图8-3所示,Attention计算过程中,Q与K计算结果只影响Attention_score(注意力得分)的最后一行,但与全部的值(V)相关。而score则与查询(Q)的最后一行相关,并与键(K)的全部行相关。由此可以得出,Attention机制的最后一行与查询(Q)的最后一行、完整的键(K)和值(V)相关。这一结论非常重要,因为它揭示了为什么我们选择使用KV Cache而不是QKV Cache。
我们继续探讨,图8-3展示了注意力核心计算在使用计算不同状态的比较。

图8-3 注意力的核心计算(图像颜色参看配套资源中的相关文件)
从上图可以看到,此时注意力中的输入只有上一次推理生成的token,而不是整个 prompt 序列。在进行注意力计算之前,需要拼接完整的键(K)和值(V),因此需要将这两个量缓存起来,并在每次推理时复用。
8.2 自回归生成模型中的推理加速详解
在上一节中,我们详细讲解了自回归生成模型的原理与训练过程,揭示了训练环节的核心重要性。然而,在生成模型的全面实践中,更加关键的一步在于如何精妙地运用这些训练成熟的模型去执行实际的推理任务。而在此过程中,一个至关重要的考量因素,便是如何高效利用现有的设备和资源,以最优化的方式进行模型推理。
在本节中,我们将聚焦于生成模型中的推理加速内容,深入探讨如何通过技术手段提升模型推理的速度与效率。我们将介绍一系列策略和技巧,包括模型压缩、并行计算、硬件优化等,旨在帮助大家充分利用计算资源,减少推理时间,从而实现更快速、更高效的生成模型应用。通过本节的学习,你将能够掌握提升生成模型推理性能的关键方法,为你的实际项目注入更强大的动力。
8.2.1 模型推理中的“贪心生成”与“采样生成”
前面我们在最后讲解了模型生成时的参数temperature与采样个数Top_k,这实际上是我们在使用时默认使用了采样生成策略。在深度学习中,特别是在自然语言处理领域,文本生成是一个重要的任务。在这个过程中,贪心生成(Greedy Generation)和采样生成(Sampling Generation)是两种常用的策略。下面我们将详细讲解这两种策略及其特点。
1. 贪心生成
原理:贪心生成的核心思想是,在每一步生成过程中,都选择当前概率最大的token(即最可能的词或字符)作为预测值。这种方法简单直观,计算效率高。
优点:由于每次都选择最有可能的词,贪心生成往往能生成语法正确、语义通顺的文本。
缺点:然而,贪心策略也容易导致生成的文本缺乏多样性。因为每次都选择概率最大的词,所以生成的文本往往比较单一,缺乏创新和变化。
贪心生成的简单示例如下:
import torch
import torch.nn.functional as F
# 假设我们有一个训练好的模型model,和一个初始的输入序列input_seq
# model应该是一个PyTorch模型,其输出是词汇表大小的logits
# input_seq是一个张量,表示输入的序列
# 使用模型得到下一个token的预测分布
logits = model(input_seq)
# 使用Softmax函数得到概率分布
probs = F.softmax(logits, dim=-1)
# 选择概率最大的token作为下一个词
next_token = torch.argmax(probs, dim=-1)
print("Greedy generated token:", next_token.item())
2. 采样生成
为了增加生成的多样性,人们提出了采样生成的方法。这种方法通过采样的方式,使得非最大概率值的token也有机会被选中。
原理:在采样生成中,每个token被选中的概率与其在模型输出的概率分布中的值成正比。这样,即使某个token的概率不是最大的,也有可能被选中,从而增加了生成的多样性。
Top-k采样:在这种方法中,首先选取概率最高的k个token作为候选集,然后从这个候选集中随机选择一个token作为输出。这样做的好处是,既保证了生成的文本有一定的质量(因为候选集都是概率较高的token),又增加了多样性(因为是从k个候选词中随机选择的)。
采样生成的简单示例如下:
import torch
import torch.nn.functional as F
# 同样地,假设我们有一个训练好的model和一个初始的输入序列input_seq
# 使用模型得到下一个token的预测分布
logits = model(input_seq)
# 使用softmax函数得到概率分布
probs = F.softmax(logits, dim=-1)
# 采样生成:这里使用top-k采样作为示例
k = 5 # 设定top-k的值
# 获取概率最高的k个token的索引
top_k_probs, top_k_indices = torch.topk(probs, k, dim=-1)
# 从这k个token中随机选择一个
sampled_index = torch.multinomial(top_k_probs, 1).squeeze()
# 找到对应的token索引
next_token = top_k_indices[sampled_index]
print("Sampled token:", next_token.item())
除了前面的Top-k采样,我们还有一种称为Top-p采样的策略(核采样Nucleus Sampling)。它与Top-k采样不同,Top-p采样不是选择固定数量的候选词,而是选择一个概率阈值p。然后,从模型的预测分布中选择一个最小的词集合,使得这个集合中的单词的概率总和至少为p。这种方法更加灵活,因为它可以根据模型的输出动态调整候选词的数量。
优点:采样生成方法通过引入随机性,增加了生成的多样性,使得生成的文本更加丰富多彩。
缺点:采样生成也可能导致生成的文本质量下降。因为非最大概率值的token被选中的机会增加,所以可能会生成一些语法错误或语义不通的文本。
对于Top-p采样,实现会稍微复杂一些,因为你需要计算累积概率并找到一个阈值,使得累积概率之和达到或超过p。这通常涉及对概率分布进行排序和累积求和,直到累积和达到或超过p为止。然后,从这个累积和达到p的子集中随机选择一个token。
在实际应用中,文本生成通常涉及更复杂的处理,如处理序列长度、处理特殊标记(如<EOS>表示序列结束)以及可能的批处理操作等。此外,model和input_seq需要根据你具体的模型和数据进行替换。在实际使用时,还需要确保模型处于评估模式(model.eval()),并处理任何可能的设备(CPU/GPU)兼容性问题。
8.2.2 模型推理过程中的冗余计算问题解析
在探讨基于自回归架构的推理过程时,我们可以清晰地看到其自回归生成机制是如何运作的。以用户输入“上海的天气”为例,模型逐步生成了“是晴天”这一完整输出。整个生成过程既精妙又直观,如图8-4所示。
首先,模型接收输入“上海的天气”,并计算出每个token的注意力表示(这些注意力表示在图中以绿色部分呈现)。利用“天气”这一token的注意力表示,模型预测并生成了下一个token“是”。
随后,模型将新生成的“是”拼接到原始输入后,形成新的输入序列“上海的天气是”。再次通过模型处理,利用“是”的注意力表示,模型预测并生成了下一个token“晴”。这一过程不断重复,模型依次将新生成的token拼接到输入序列中,并基于最新的输入序列预测下一个token,直至生成完整的输出“上海的天气是晴天”。

图8-4 自回归生成示例
我们将这个推理过程用矩阵Embedding的形式进行表示,如图8-5所示。

图8-5 矩阵表示的推理过程

具体来说,我们希望有一种方法。在预填充阶段,模型会计算输入文本的Key和Value向量,并将它们缓存起来。然后,在解码阶段,模型会逐一生成token。对于每个新生成的token,模型会利用其对应的Query向量和缓存的Key、Value向量来计算注意力权重,从而预测出下一个token。带有缓存的推理过程如图8-6所示。
通过这种技术显著减少了计算量,提高了推理性能。

8.2.3 初识模型推理中的KV Cache与代码实现
在深度学习领域,特别是在自然语言处理任务中,自回归模型由于其生成特性而备受关注。然而,这类模型在生成长文本时往往面临计算量巨大的挑战。为了提升计算效率,KV Cache技术应运而生,成为优化自回归生成模型推理速度的重要手段。
KV Cache是一种通过缓存Attention机制中的键(K)和值(V)向量来减少重复计算的技术。在自回归生成模型中,每个token的生成都依赖于其之前的所有token,这意味着在生成长文本时,需要反复计算每个token的键(K)和值(V)向量。KV Cache通过预先计算并缓存这些向量,避免了在每次生成新token时的重复计算,从而显著提高了推理速度。
下面示例演示了我们在没有经过KV Cache缓存的条件下进行生成的过程:
import torch
import torch.nn as nn
batch_size = 1
seq_length = 48
hidden_size = 384
vocab_size = 1024
Wq = torch.randn(hidden_size, hidden_size)
Wk = torch.randn(hidden_size, hidden_size)
Wv = torch.randn(hidden_size, hidden_size)
embedding_layer = torch.nn.Embedding(vocab_size,hidden_size)
lm_head = torch.nn.Linear(hidden_size,vocab_size)
prompt_len = 2
#step1: 预填充阶段,处理输入prompt,生成第一个token
inputs = torch.randint(0,vocab_size,[2]) #[prompt_len],设置输入的prompt长度
print("step1:输入prompt的token:",inputs)
inputs_emb = embedding_layer(inputs) #[prompt_len, hidden_size]
Q,K,V=inputs_emb@Wq,inputs_emb@Wk ,inputs_emb@Wv#[prompt_len, hidden_size]
att_weight=Q@(K.T)#[prompt_len, prompt_len]
print("step1:att_weight:",att_weight.shape)
att_output=att_weight@V #[prompt_len, hidden_size]
output = lm_head(att_output) #[prompt_len, vocab_size]
output_token = torch.argmax(output,dim=-1)[-1:]
print("step1:输出生成的token:",output_token)
print("##########################")
#step2:解码阶段,下一个token生成
inputs = torch.cat((inputs,output_token),dim=-1)
print("step2:输入的token:",inputs)
inputs_emb = embedding_layer(inputs) #[prompt_len+1, hidden_size]
Q,K,V=inputs_emb@Wq,inputs_emb@Wk ,inputs_emb@Wv#[prompt_len+1, hidden_size]
att_weight=Q@(K.T)#[prompt_len+1, prompt_len+1]
print("step2:att_weight:",att_weight.shape)
att_output=att_weight@V #[prompt_len+1, hidden_size]
output = lm_head(att_output) #[prompt_len+1, vocab_size]
output_token = torch.argmax(output,dim=-1)[-1:]
print("step2:生成的token:",output_token)
打印结果如下所示:
step1:输入prompt的token: tensor([130, 830])
step1:att_weight: torch.Size([2, 2])
step1:输出生成的token: tensor([1010])
##########################
step2:输入的token: tensor([ 130, 830, 1010])
step2:att_weight: torch.Size([3, 3])
step2:生成的token: tensor([90])
在这个基础上我们调整一下代码,完成带有KV Cache缓存的输入,代码如下所示:
import torch
batch_size = 1
seq_length = 48
hidden_size = 384
vocab_size = 1024
Wq = torch.randn(hidden_size, hidden_size)
Wk = torch.randn(hidden_size, hidden_size)
Wv = torch.randn(hidden_size, hidden_size)
embedding_layer = torch.nn.Embedding(vocab_size,hidden_size)
lm_head = torch.nn.Linear(hidden_size,vocab_size)
prompt_len = 2
#step1: 预填充阶段,处理输入prompt,生成第一个token
inputs = torch.randint(0,vocab_size,[2]) #[prompt_len],设置输入的prompt长度
print("step1:输入prompt的token:",inputs)
inputs_emb = embedding_layer(inputs) #[prompt_len, hidden_size]
Q,K,V=inputs_emb@Wq,inputs_emb@Wk ,inputs_emb@Wv#[prompt_len, hidden_size]
cache_k = K #将prompt计算embedding作为cache_k
cache_v = V #将prompt计算embedding作为cache_v
att_weight=Q@(K.T)#[prompt_len, prompt_len]
print("step1:att_weight:",att_weight.shape)
att_output=att_weight@V #[prompt_len, hidden_size]
output = lm_head(att_output) #[prompt_len, vocab_size]
output_token = torch.argmax(output,dim=-1)[-1:]
print("step1:输出生成的token:",output_token)
print("##########################")
#step2:解码阶段,一个个token逐个生成
inputs = output_token #这里相对于前面,没有进行concat操作
print("step2:输入的token:",inputs)
#相对于前面的#[prompt_len+1, hidden_size],这里只有[1, hidden_size]
inputs_emb = embedding_layer(inputs) #[1, hidden_size]
#QKV计算量减少
Q,K,V=inputs_emb@Wq,inputs_emb@Wk ,inputs_emb@Wv#[1, hidden_size]
#kv cache显存占用
cache_k=torch.cat((cache_k,K),dim=0)#[prompt_len+1, hidden_size]
cache_v=torch.cat((cache_v,V),dim=0)#[prompt_len+1, hidden_size]
att_weight=Q@(cache_k.T)#[1, prompt_len+1]
att_output=att_weight@cache_v #[1, hidden_size]
output = lm_head(att_output) #[1, vocab_size]
output_token = torch.argmax(output,dim=-1)
print("step2:生成的token:",output_token)
打印结果如下:
step1:输入prompt的token: tensor([508, 621])
step1:att_weight: torch.Size([2, 2])
step1:输出生成的token: tensor([8])
##########################
step2:输入的token: tensor([8])
step2:生成的token: tensor([529])
从代码运行结果上来看,我们可以看到随着内容的输入,也可以获得对应的next token的输出。
8.3 减少空间占用的自回归模型代码实现与详解
在上一节中,我们详细探讨了自回归模型的计算负担,以及如何通过缓存优化技术来减轻这种负担。显然,在模型计算过程中,若使用完整序列,会显著增加计算量。而缓存技术的引入,正是为了解决这个问题。
接下来,我们将通过实现带有缓存功能的经典自回归模型——GPT-2,来具体展示缓存优化带来的效果。我们将编写GPT-2模型的完整代码,并在使用和未使用缓存技术的情况下,分别进行序列生成测试。
通过对比实验,我们可以直观地看到缓存技术在减少内存占用和提高计算效率方面的显著效果。具体来说,我们将记录并分析两种情况下模型进行序列生成时的内存占用情况和所需时间。
在模型实现过程中,我们会重点关注如何将之前的计算结果存储起来,并在后续的计算中重用这些结果,从而减少重复计算。这种优化不仅可以加快模型的推理速度,还能显著降低内存消耗,尤其是在处理长序列时效果更为显著。
实验结束后,我们将对比数据进行分析,用具体数字说话,展示缓存优化在实际应用中的价值。通过这样的实践,我们可以更加深入地理解缓存技术在自然语言处理任务中的重要性,以及它如何帮助我们更有效地利用计算资源,提升模型的性能。
8.3.1 经典自回归模型详解
GPT-2模型是经典的基于自回归架构的语言模型,以其出色的文本生成能力而备受瞩目。我们首先从自回归的角度出发,深入剖析其工作原理与魅力所在。GPT-2模型架构如图8-7所示。
图8-7 GPT-2模型架构
首先,谈及自回归,不得不提及其核心理念:基于过往信息来推测未来的结果。在GPT-2模型中,这一理念被淋漓尽致地体现出来。模型在生成文本时,会紧密依托先前已生成的文本内容,精准预测下一个词语的出现概率。这种预测方式不仅确保了文本的连贯性,更赋予了模型一种“记忆”过往并据此推演的智能。
进一步探究发现,GPT-2所依托的自回归架构为其强大的文本生成能力奠定了坚实基础。特别是Transformers的Decoder部分,通过自注意力机制的运用,使得模型能够捕捉到文本中的长距离依赖关系,进而生成更加自然、流畅的文本。这种机制就像模型的“内心之眼”,能够洞察文本中的深层结构和语义联系。
此外,GPT-2模型在处理序列数据时,巧妙地引入了位置嵌入的概念。这一设计确保了模型能够深刻理解词汇在句子中的位置的重要性,从而生成语法正确、语义通顺的文本。位置嵌入就如同模型的“指南针”,指引着词汇在句子中的正确位置。
值得一提的是,GPT-2的预训练过程是在无监督的环境下进行的。这意味着模型仅依靠文本本身进行学习,无须额外的任务标签。这种学习方式赋予了模型极高的通用性和适应性,使其能够轻松应对各种下游任务。
8.3.2 能够减少空间占用的自回归模型代码完整实现
带有缓存的GPT-2模型,其实现代码如下所示,读者可以参看代码中的注释来理解:
import torch
from torch import einsum
from torch import nn
from einops import rearrange, reduce, repeat
import math
# 多头注意力机制的实现
class MultiheadAttention(nn.Module):
def __init__(self, hidden_size: int, num_heads: int):
super().__init__()
self.hidden_size = hidden_size
self.num_heads = num_heads
self.head_size = hidden_size // num_heads # 每个头的维度
assert self.head_size * num_heads == hidden_size # 确保总维度正确
self.attentionLL = nn.Linear(hidden_size, num_heads*self.head_size*3) # 生成Q、K、V的线性层
self.outputLL = nn.Linear(num_heads*self.head_size, hidden_size) # 输出线性层
# 前向传播
def forward(self, x: torch.Tensor, past_key_values = None, return_key_values = False):
# 输入形状: [batch, seq_length, hidden_size]
if past_key_values is None:
# 如果没有past_key_values,计算当前的Q、K、V
# 形状: batch seq_len hidden_size*3
KQV = self.attentionLL(x)
# 重新排列形状: batch num_heads seq_len head_size three
KQV = rearrange(KQV, "batch seq_len (three num_heads head_size) -> batch num_heads seq_len head_size three ",
num_heads=self.num_heads, three=3)
Q = KQV[:, :, :, :, 0] # 提取Q
K = KQV[:, :, :, :, 1] # 提取K
V = KQV[:, :, :, :, 2] # 提取V
# 计算注意力模式: batch num_heads seq_len seq_len
attention_pattern = einsum('b n s h, b n t h -> b n s t', K, Q)
# 缩放注意力模式
attention_pattern = attention_pattern / math.sqrt(self.head_size)
# 仅允许Masked自注意力(当前时间步只能看到过去的时间步)
attention_pattern = torch.triu(attention_pattern) + (-1e4) * torch.tril(torch.ones_like(attention_pattern), diagonal=-1)
# 应用Softmax
attention_pattern = torch.nn.Softmax(dim=2)(attention_pattern)
# 计算注意力加权值: batch num_heads seq_len head_size
out = einsum('b n k q, b n k h -> b n q h', attention_pattern, V)
# 重新排列形状: batch seq_len (num_heads head_size)
out = rearrange(out, 'batch num_heads seq_len head_size -> batch seq_len (num_heads head_size)')
# 线性变换输出
out = self.outputLL(out)
if return_key_values:
# 如果需要返回K和V(用于缓存)
assert x.shape[0] == 1 # 只支持单个batch
return out, torch.cat((K, V), dim=3) # 合并K和V
else:
return out
else:
# 如果有past_key_values,用于缓存(如生成任务)
assert x.shape == (1, 1, self.hidden_size) # 只支持单个时间步
kqv = self.attentionLL(x)
# 重新排列形状
kqv = rearrange(kqv, "batch seq_len (three num_heads head_size) -> batch num_heads seq_len head_size three ",
num_heads=self.num_heads, three=3)
q = kqv[0, :, :, :, 0] # 提取Q
k = kqv[0, :, :, :, 1] # 提取K
v = kqv[0, :, :, :, 2] # 提取V
# 从past_key_values中获取之前的K和V
oldK, oldV = torch.split(past_key_values, (self.head_size, self.head_size), dim=2)
# 拼接新的K和V
K = torch.cat((oldK, k), dim=1)
V = torch.cat((oldV, v), dim=1)
# 计算注意力模式
attention_pattern = einsum('n s h, n t h -> n s t', q, K)
attention_pattern = attention_pattern / math.sqrt(self.head_size)
attention_pattern = torch.nn.Softmax(dim=2)(attention_pattern)
# 计算注意力加权值
out = einsum('n s t, n t h -> n s h', attention_pattern, V)
# 重新排列形状
out = rearrange(out, '(batch num_heads) seq_len head_size -> batch seq_len (num_heads head_size)', batch=1)
# 线性变换输出
out = self.outputLL(out)
if return_key_values:
# 如果需要返回新的K和V
return out, torch.cat((k, v), dim=2).unsqueeze(0)
else:
return out
# GPT-2块,包括多头注意力和前馈网络
class GPT2Block(nn.Module):
def __init__(self, hidden_size: int, num_heads: int,
dropout: float, layer_norm_epsilon: float):
super().__init__()
self.ln1 = nn.LayerNorm(hidden_size, eps=layer_norm_epsilon) # 第一层归一化
self.attn = MultiheadAttention(hidden_size, num_heads) # 多头注意力
self.ln2 = nn.LayerNorm(hidden_size, eps=layer_norm_epsilon) # 第二层归一化
self.linear1 = nn.Linear(hidden_size, hidden_size * 4) # 前馈网络的第一层
self.linear2 = nn.Linear(hidden_size * 4, hidden_size) # 前馈网络的第二层
self.dropout = nn.Dropout(dropout) # Dropout层
# 前向传播
def forward(self, x: torch.Tensor, past_key_values = None, return_key_values = False):
if return_key_values:
# 如果需要返回K和V(用于缓存)
res = x # 保存残差连接
x, keyvals = self.attn(self.ln1(x), past_key_values=past_key_values, return_key_values=True) # 注意力层
x = x + res # 残差连接
# 前馈网络
x = x + self.dropout(self.linear2(torch.nn.functional.gelu(self.linear1(self.ln2(x)))))
return x, keyvals
else:
# 如果不需要返回K和V
x = x + self.attn(self.ln1(x), past_key_values=past_key_values, return_key_values=False) # 注意力层
x = x + self.dropout(self.linear2(torch.nn.functional.gelu(self.linear1(self.ln2(x))))) # 前馈网络
return x
# GPT-2模型
class GPT2(nn.Module):
def __init__(self, num_layers = 6, num_heads = 6, vocab_size = 4000,
hidden_size = 384, max_position_embeddings = 1024, dropout = 0.1,
layer_norm_epsilon = 1e-9, use_cache=False):
super().__init__()
self.token_embedding = nn.Embedding(vocab_size, hidden_size) # 词嵌入
self.position_embedding = nn.Embedding(max_position_embeddings, hidden_size) # 位置嵌入
self.dropout = nn.Dropout(dropout) # Dropout层
# 多层GPT-2块
self.GPTBlocks = nn.Sequential(
*[GPT2Block(hidden_size, num_heads, dropout, layer_norm_epsilon)
for i in range(num_layers)]
)
self.layer_norm = nn.LayerNorm(hidden_size, layer_norm_epsilon) # 最后一层归一化
self.use_cache = use_cache #是否使用缓存
self.head_size = hidden_size // num_heads # 每个头的维度
self.num_layers = num_layers # 层数
self.num_heads = num_heads # 头数
self.reset_kv_cache() # 初始化缓存
# 重置缓存
def reset_kv_cache(self):
self.past_key_values = torch.zeros((self.num_layers, self.num_heads, 0, 2 * self.head_size))
# 前向传播
def forward(self, input_ids): # 输入形状: [batch, seq_len]
if not self.use_cache:
# 如果不使用缓存
tokens = self.token_embedding(input_ids) # 词嵌入
batch, seq_len = input_ids.shape
# 位置嵌入
position_ids = repeat(torch.arange(seq_len), 's -> b s', b = batch).to(input_ids.device)
positions = self.position_embedding(position_ids)
embedding = tokens + positions # 嵌入叠加
x = self.dropout(embedding) # Dropout
x = self.GPTBlocks(x) # 通过GPT-2块
self.last_token_encodings = x # 保存最后的编码
final_encodings = self.layer_norm(x) # 归一化
# 计算logits
logits = einsum('b s d, v d -> b s v', final_encodings, self.token_embedding.weight)
return (logits, final_encodings)
else:
# 如果使用缓存
if self.past_key_values.shape[2] == 0:
# 如果缓存为空,初始化处理
tokens = self.token_embedding(input_ids) # 词嵌入
batch, seq_len = input_ids.shape
# 位置嵌入
position_ids = repeat(torch.arange(seq_len), 's -> b s', b = batch).to(input_ids.device)
positions = self.position_embedding(position_ids)
embedding = tokens + positions # 嵌入叠加
x = self.dropout(embedding) # Dropout
new_key_values = [] # 保存新的K和V
# 依次处理每个GPT-2块
for gptblock in self.GPTBlocks:
x, new_key_value = gptblock(x, return_key_values=True)
new_key_values.append(new_key_value)
self.past_key_values = torch.cat(new_key_values, dim=0) # 更新缓存
final_encodings = self.layer_norm(x) # 归一化
# 计算logits
logits = einsum('b s d, v d -> b s v', final_encodings, self.token_embedding.weight)
return (logits, final_encodings)
else:
# 如果缓存不为空,处理新的时间步
tokens = self.token_embedding(input_ids[:, -1:]) # 仅处理最后一个时间步
batch, seq_len = input_ids.shape
# 位置嵌入(仅处理最后一个时间步)
position_ids = repeat(torch.arange(seq_len), 's -> b s', b = batch).to(input_ids.device)
positions = self.position_embedding(position_ids[:, -1:])
embedding = tokens + positions # 嵌入叠加
x = self.dropout(embedding) # Dropout
new_key_values = [] # 保存新的K和V
# 依次处理每个GPT-2块
for i, gptblock in enumerate(self.GPTBlocks):
x, new_key_value = gptblock(x,
past_key_values=self.past_key_values[i, :, :, :],
return_key_values=True)
new_key_values.append(new_key_value)
new_key_values = torch.cat(new_key_values, dim=0) # 合并新的K和V
self.past_key_values = torch.cat((self.past_key_values, new_key_values), dim=2) # 更新缓存
final_encodings = self.layer_norm(x) # 归一化
# 计算logits
logits = einsum('b s d, v d -> b s v', final_encodings, self.token_embedding.weight)
return (logits, final_encodings)
在上面代码中,我们使用了缓存优化技术past_key_values,目的在减少重复计算,从而提高推理效率。
8.3.3 缓存使用与传递过程详解
GPT-2是经典的自回归生成模型。一般在生成任务中,模型通常逐个词生成序列,缓存机制可以避免在每一步都从头计算所有之前的键(Key)和值(Value),而是复用之前的结果。下面我们详细讲解GPT-2中缓存机制的使用。
1. 缓存初始化
缓存初始化在reset_kv_cache方法中完成:
def reset_kv_cache(self):
self.past_key_values = torch.zeros((self.num_layers, self.num_heads, 0, 2 * self.head_size))
past_key_values是一个四维张量,形状为(num_layers, num_heads, 0, 2 * head_size):
- num_layers:GPT-2的层数。
- num_heads:每层中多头注意力的头数。
- 0:缓存的初始长度为0,表示没有存储任何历史键值。
- 2 * head_size:每一层的键值对(K和V)被拼接在一起,维度是2 * head_size。
2. 缓存的使用逻辑
缓存的使用逻辑主要在forward方法中实现:
1)缓存为空时的初始化
if self.past_key_values.shape[2] == 0:
tokens = self.token_embedding(input_ids)
batch, seq_len = input_ids.shape
position_ids = repeat(torch.arange(seq_len), 's -> b s', b = batch).to(input_ids.device)
positions = self.position_embedding(position_ids)
embedding = tokens + positions
x = self.dropout(embedding)
new_key_values = []
for gptblock in self.GPTBlocks:
x, new_key_value = gptblock(x, return_key_values=True)
new_key_values.append(new_key_value)
self.past_key_values = torch.cat(new_key_values, dim=0)
上面代码解释如下:
- 初始化嵌入:输入的编码(token_embedding和position_embedding)被计算。
- 逐层处理:每层GPT-2块(GPT2Block)都会返回新的键值对(new_key_value)。
- 缓存更新:新的键值对被收集到new_key_values中,并最终拼接到self.past_key_values。
2)缓存不为空时的处理
代码如下所示:
else:
tokens = self.token_embedding(input_ids[:, -1:])
batch, seq_len = input_ids.shape
position_ids = repeat(torch.arange(seq_len), 's -> b s', b = batch).to(input_ids.device)
positions = self.position_embedding(position_ids[:, -1:])
embedding = tokens + positions
x = self.dropout(embedding)
new_key_values = []
for i, gptblock in enumerate(self.GPTBlocks):
x, new_key_value = gptblock(x,
past_key_values=self.past_key_values[i, :, :, :],
return_key_values=True)
new_key_values.append(new_key_value)
new_key_values = torch.cat(new_key_values, dim=0)
self.past_key_values = torch.cat((self.past_key_values, new_key_values), dim=2)
上面代码解释如下:
- 处理新的输入:只处理当前时间步的输入(input_ids[:, -1:])。
- 复用缓存:每层GPT-2块(GPT2Block)接收当前时间步的输入和之前的键值对(past_key_values)。
- 生成新的键值对:当前时间步的键值对(K和V)被计算并返回。
- 缓存更新:新的键值对被添加到past_key_values中。
3)缓存的传递
缓存的传递主要发生在以下两部分:
(1)在MultiheadAttention的forward方法中,缓存通过past_key_values参数传递:
if past_key_values is not None:
assert x.shape == (1, 1, self.hidden_size) # 只支持单个时间步
kqv = self.attentionLL(x)
kqv = rearrange(kqv, "batch seq_len (three num_heads head_size) -> batch num_heads seq_len head_size three ", num_heads=self.num_heads, three=3)
q = kqv[0, :, :, :, 0] # 提取Q
k = kqv[0, :, :, :, 1] # 提取K
v = kqv[0, :, :, :, 2] # 提取V
oldK, oldV = torch.split(past_key_values, (self.head_size, self.head_size), dim=2)
K = torch.cat((oldK, k), dim=1) # 拼接旧的K和新的K
V = torch.cat((oldV, v), dim=1) # 拼接旧的V和新的V
在这里主要完成了以下几个步骤:
- 提取当前K和V:从当前时间步的输入中提取K和V。
- 拼接旧K和新K:将之前缓存的K(oldK)和当前的K拼接在一起。
- 拼接旧V和新V:将之前缓存的V(oldV)和当前的V拼接在一起。
(2)在GPT2Block的forward方法中,缓存通过past_key_values参数传递:
x, new_key_value = gptblock(x, past_key_values=past_key_values, return_key_values=True)
如果past_key_values非空,GPT2Block会将之前的K和V与当前的K和V拼接在一起。
4)最后在每次计算新的Key和Value后,缓存都会更新:
new_key_values = torch.cat(new_key_values, dim=0)
self.past_key_values = torch.cat((self.past_key_values, new_key_values), dim=2)
上面代码解释如下:
- 拼接新的K和V:新的键值对被拼接到缓存中。
- 维度扩展:缓存的长度(第三维)增加,对应新的时间步。
可以看到,生成模型中的缓存机制通过保存历史的键值对(Key和Value),避免了在生成任务中重复计算旧的Key和Value。这种优化显著提高了生成任务的推理速度,尤其是在处理长序列时。缓存的管理和更新是实现这一优化的关键,确保在每一步中都只计算当前时间步的键值对,并复用之前的缓存。
8.4 减少空间占用的生成模型实战与推理资源消耗量化对比
我们使用缓存对自回归生成模型进行计算时,最核心的目标是降低模型推理时的资源消耗。通过精心设计的缓存机制,我们得以高效地存储并复用中间计算结果,从而避免了在每次推理步骤中重复相同的计算。这种优化方法不仅大幅提升了模型的推理速度,还减少了对计算资源的依赖,使自回归生成模型在更多场景下都能实现高效运用。
在本节中,我们将从头开始训练一个自回归模型,并采用不同的推理方法,以便对资源消耗进行量化对比。我们希望通过这一对比实验,更直观地展示缓存机制在降低资源消耗方面的实际效果,进一步验证缓存优化策略的有效性。同时,我们也希望通过这些数据,为未来的模型优化和缓存设计提供更具体的指导和参考。通过这样的实证研究,我们不仅可以更深入地理解缓存机制在自回归模型中的作用,还能为相关领域的研究和实践提供有价值的经验和启示。
8.4.1 模型参数配置与训练数据的准备
首先需要完成模型参数的配置。在这里我们简化了配置方法,采用类的形式对所有的参数进行管理,代码如下所示:
class GPT2Config:
hidden_size = 384
vocab_size = 4000
num_attention_heads = 6
assert hidden_size % num_attention_heads == 0, 'hidden_size must be divisible by num_head'
intermediate_size = hidden_size * 4
dropout = 0.1
layer_norm_eps = 1e-12
n_layers = 6
is_cause = True
device = "cuda"
max_length = 48
接下来,我们还是希望使用前面进行评论生成的数据进行本章的学习,在这里我们可以直接使用第5章的分词模型以及评论数据集,代码如下所示:
import sentencepiece as spm
class Tokenizer:
def __init__(self,spm_path = './vocab_new/spm_model.model'):
super().__init__()
self.sp = spm.SentencePieceProcessor()
self.sp.Load(spm_path)
self.end_id = 4
def encode(self,text):
token = self.sp.EncodeAsIds(text)
return token
def decode(self,token):
_text = self.sp.DecodeIds(token)
return (_text)
def vocab_size(self):
return len(self.sp)
数据集的处理和准备:
import random,torch
from tqdm import tqdm
import sentencepiece as spm
import config
tokenizer_emo = config.Tokenizer()
print(tokenizer_emo.vocab_size())
token_list = []
with open("../dataset/ChnSentiCorp.txt", mode="r", encoding="UTF-8") as emotion_file:
for line in tqdm(emotion_file.readlines()):
line = line.strip().split(",")
text = "".join(line[1:]) + '<|end▁of▁sentence|>'
token = tokenizer_emo.encode(text)
for id in token:
token_list.append(id)
token_list = torch.tensor(token_list * 4)
class TextSamplerDataset(torch.utils.data.Dataset):
def __init__(self, data = token_list, seq_len = 48):
super().__init__()
self.data = data
self.seq_len = seq_len
def __getitem__(self, index):
rand_start = torch.randint(0, self.data.size(0) - self.seq_len, (1,))
full_seq = self.data[rand_start : rand_start + self.seq_len + 1].long()
return full_seq[:-1],full_seq[1:]
def __len__(self):
return self.data.size(0) // self.seq_len
8.4.2 带有缓存的生成模型训练
在训练带有缓存的生成模型时,我们可以完全将其视为普通的生成模型进行处理。这是因为缓存机制主要影响的是模型的推理阶段,而非训练阶段。在训练过程中,模型需要学习的是如何生成合理的序列,而缓存的引入并不会改变这一学习目标。因此,我们可以按照标准的生成模型训练流程进行,无须对训练过程进行特殊调整。
相对于前面完成的生成模型,这里我们增加了训练次数,这一点请读者注意。完整的生成模型训练代码如下所示:
import torch
from tqdm import tqdm
import torch
import config
import gpt2_cached
import get_dataset
from torch.utils.data import Dataset, DataLoader
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
gpt2config = config.GPT2Config()
tokenizer = config.Tokenizer()
model = gpt2_cached.GPT2().to(device)
model.load_state_dict(torch.load("./saver/model.pth"),strict=False)
seq_len = 64
# 获取训练数据集
train_dataset = get_dataset.TextSamplerDataset(get_dataset.token_list,seq_len=seq_len)
# 初始化 DataLoader
data_trainer = DataLoader(dataset=train_dataset,batch_size=640,shuffle=True)
opt = torch.optim.AdamW(model.parameters(),lr=2e-4)
lr_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(opt,T_max = 1200,eta_min=2e-6,last_epoch=-1)
# 损失函数
criterion = torch.nn.CrossEntropyLoss()
for epoch in range(128):
model.train() # 确保模型在训练模式下
pbar = tqdm(data_trainer, total=len(data_trainer))
for tok, lab in pbar:
tok = tok.to(device)
lab = lab.to(device)
logits,kv_caches = model(tok)
# 调整 logits 和 lab的维度
logits = logits.view(-1, logits.size(-1)) # [batch_size * sequence_length, num_classes]
lab = lab.view(-1) # [batch_size * sequence_length]
# 计算损失
loss = criterion(logits, lab)
# 反向传播和优化
opt.zero_grad() # 清除梯度
loss.backward() # 计算梯度
opt.step() # 更新参数
lr_scheduler.step() # 执行优化器
# 更新进度条上的描述
pbar.set_description(f"Epoch {epoch + 1}, Loss: {loss.item():.4f}, lr:{lr_scheduler.get_last_lr()[0]*1000:.5f}")
if epoch % 5 == 0:
print("model saved")
torch.save(model.state_dict(), "./saver/model.pth")
torch.save(model.state_dict(), "./saver/model.pth")
值得注意的是,虽然在训练阶段缓存并不直接参与,但考虑到模型在实际应用中的推理效率,我们在设计模型结构时,仍然需要预留出与缓存机制相兼容的接口。这样做的好处是,一旦模型训练完成,我们可以轻松地整合缓存功能,从而在实际应用中实现更高效的推理。
此外,尽管缓存不在训练阶段直接使用,但了解并优化缓存机制对于提升模型的整体性能至关重要。因此,在训练过程中,我们也应不断思考如何更好地结合缓存策略,以便在后续的推理阶段达到最佳效果。通过这种方式,我们不仅可以确保模型在训练阶段学习到有效的生成策略,还能为其在实际应用中的高效推理奠定坚实基础。
8.4.3 未运行缓存的生成模型推理资源量化展示
在训练结束后,我们首先对未运行缓存的生成模型进行推理展示,即先比对正常输出文本的推理模型,我们可以将生成的序列长度统一设置为48,再查看对应的文本输出,代码如下所示:
import torch
from tqdm import tqdm
import torch
import config
import gpt2_cached
import get_dataset
from torch.utils.data import Dataset, DataLoader
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
gpt2config = config.GPT2Config()
tokenizer = config.Tokenizer()
model = gpt2_cached.GPT2().to(device)
model.load_state_dict(torch.load("./saver/model.pth"),strict=False)
model.eval()
max_length = gpt2config.max_length
top_k = 5
temperature=0.90
import time
start = time.time()
import time
start_time = time.time()
for _ in range(10):
input_text = "酒店的位置"
input_ids = torch.tensor([tokenizer.encode(input_text)]).long().to(device)
past_length = input_ids.shape[-1] # 初始输入的长度
input_ids = input_ids.clone().detach().requires_grad_(False).to(device)
for token_n in range(max_length):
with torch.no_grad():
indices_to_input = input_ids
next_token_logits,_ = model(indices_to_input)
next_token_logits = next_token_logits[:, -1]
probs = torch.nn.functional.softmax(next_token_logits, dim=-1) * temperature
(values, indices) = torch.topk(probs, k=top_k)
probs[probs < values[:, -1, None]] = 0
probs = probs / probs.sum(axis=1, keepdims=True)
next_indices = torch.multinomial(probs, num_samples=1)
input_ids = torch.cat([input_ids, next_indices], dim=1)
input_ids = input_ids[0].cpu().numpy()
text = tokenizer.decode(input_ids.tolist())
text= text.split("<|end of sentence|>")[0]
# print(text)
allocated_memory = torch.cuda.memory_allocated()
print(f'当前设备上张量所占用的GPU内存: {allocated_memory} 字节')
end_time = time.time()
print("花费的时间为:", end_time - start_time)
读者可以自行运行代码查看生成的文本内容。下面我们继续查看当升级了文本长度后的推理资源耗费,简单地说,我们可以通过增加文本生成的文本长度,在一个较长的生成长度要求下对结果进行比对。
此时我们设置的文本生成长度为768,代码如下所示:
class GPT2Config:
hidden_size = 384
vocab_size = 4000
num_attention_heads = 6
assert hidden_size % num_attention_heads == 0, 'hidden_size must be divisible by num_head'
intermediate_size = hidden_size * 4
dropout = 0.1
layer_norm_eps = 1e-12
n_layers = 6
is_cause = True
device = "cuda"
max_length = 768
运行上面代码,结果如下所示:
当前设备上张量所占用的GPU内存: 74084352 字节
当前设备上张量所占用的GPU内存: 74084352 字节
当前设备上张量所占用的GPU内存: 74084352 字节
花费的时间为: 52.08263564109802
在当前的设备配置下,执行特定任务时张量所占用的GPU内存为74084352字节,且这一数值在连续三次的测试中保持一致。换算后可知,这大约占用了0.7GB的显存。完成这一任务所耗费的时间为52.08263564109802秒。从这个测试中我们可以看到,当文本生成的长度延长至768时,GPU资源的占用稳定在74084352字节,也就是大约0.7GB的显存,整个过程耗时约52秒。
请注意,这个数值可能因读者的电脑硬件配置差异而有所变化,建议读者根据自身情况进行相应设置。
下面我们采用同样的长度在带有缓存的生成模型上演示推理资源的占用,读者可以首先完成短文本的生成并对比生成质量,之后使用长文本检测生成的资源占用。同样地,我们采用768作为文本生成的长度,带有缓存的生成模型如下所示:
import torch
from tqdm import tqdm
import torch
import config
import gpt2_cached
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
gpt2config = config.GPT2Config()
tokenizer = config.Tokenizer()
model = gpt2_cached.GPT2(use_cache=True).to(device)
model.load_state_dict(torch.load("./saver/model.pth"), strict=False)
model.eval()
max_length = gpt2config.max_length
top_k = 5
temperature=0.90
import time
start_time = time.time()
for _ in range(10):
model.reset_kv_cache()
input_text = "酒店的位置"
input_ids = torch.tensor([tokenizer.encode(input_text)]).long().to(device)
past_length = input_ids.shape[-1] # 初始输入的长度
input_ids = input_ids.clone().detach().requires_grad_(False).to(device)
for token_n in range(max_length):
with torch.no_grad():
indices_to_input = input_ids
next_token_logits,_ = model(indices_to_input)
next_token_logits = next_token_logits[:, -1]
probs = torch.nn.functional.softmax(next_token_logits, dim=-1) * temperature
(values, indices) = torch.topk(probs, k=top_k)
probs[probs < values[:, -1, None]] = 0
probs = probs / probs.sum(axis=1, keepdims=True)
next_indices = torch.multinomial(probs, num_samples=1)
input_ids = torch.cat([input_ids, next_indices], dim=1)
input_ids = input_ids[0].cpu().numpy()
text = tokenizer.decode(input_ids.tolist())
text= text.split("<|end of sentence|>")[0]
#print(text)
allocated_memory = torch.cuda.memory_allocated()
print(f'当前设备上张量所占用的GPU内存: {allocated_memory} 字节')
end_time = time.time()
print("花费的时间为:", end_time - start_time)
通过执行这个代码,我们可以观察到资源耗费的另一种情况,其打印结果如下:
当前设备上张量所占用的GPU内存: 73377280 字节
当前设备上张量所占用的GPU内存: 73377280 字节
当前设备上张量所占用的GPU内存: 73377280 字节
花费的时间为: 40.05440592765808
在相同的任务下,当前设备上张量所占用的GPU内存为73377280字节,并且这个数值在连续的三次测试中同样保持稳定。这次任务所耗费的时间减少到了40.05440592765808秒。这意味着,在生成相同长度的文本内容时,我们仅用了40秒,相较于之前的52秒,显著缩短了处理时间。
8.4.5 使用细精度修正模型输出
除了使用KV Cache完成模型推理外,我们还可以使用半精度修正模型的输出,即在尽量保证输出结果的前提下,对模型精度进行调整,代码如下所示:
model = gpt2_cached.GPT2(use_cache=True).half().to(device)
可以看到,这里我们仅仅在模型的初始化阶段添加了.half()函数,即可完成模型的半精度设置,而从模型运行结果上来看,可以极大地减少缓存的占用。这一点读者可自行尝试学习。
8.5 本 章 小 结
在本章中,我们测试了一个带有缓存的自回归生成模型。在这个模型中,我们实现了精心设计的缓存机制,以存储和复用中间计算结果。通过这种方式,我们期望能够减少重复计算,从而提高推理速度并降低资源消耗。
在相同的任务下,带有缓存的模型展现出了显著的性能提升。与之前的模型相比,模型在处理相同长度的文本生成任务时,所需的时间明显减少。更重要的是,由于缓存机制的引入,模型在GPU显存的占用上也得到了有效降低。这意味着,在相同的硬件条件下,模型能够处理更长的文本生成任务,或者同时处理更多的任务,从而提高了整体的工作效率。
综上所述,通过对两个使用缓存的生成模型的推理过程进行对比,我们可以清晰地看到缓存机制在提升生成模型性能方面的巨大潜力。无论是通过减少重复计算来提高推理速度,还是通过降低GPU显存占用来提高资源利用效率,缓存机制都展现出了显著的效果。这为我们在未来进一步优化生成模型的性能提供了有力的支持。

417



被折叠的 条评论
为什么被折叠?



