第 8 章DeepSeek核心技术1:KV Cache加持的推理加速

671B MoE模型本地部署:内存带宽敏感型推理实战指南 大语言模型本地部署的核心瓶颈已从算力转向内存带宽——尤其对DeepSeek R1这类671B参数量的MoE架构模型而言,其推理性能80%受限于CPU与内存间的数据搬运效率,而非GPU浮点算力。动态量化技术通过识别MoE结构中‘稀疏激活’特性,对Embedding层、专家模块和中间Transformer层实施差异化精度压缩(如1.58-bit/1.73-bit),在保障关键路径精度的同时大幅降低显存与内存占用。该方案使消费级统一内存设备(如Mac Studio)或24GB云服务器也能稳定运行百亿级MoE模型, 阅读详情

DeepSeek原生应用与智能体开发实践(人工智能技术丛书)【行情 报价 价格 评测】-京东

王晓华DeepSeek开发AI应用入门书《DeepSeek原生应用与智能体开发实践》全文分享~持续更新-优快云博客

目录

8.1  自回归生成模型中的资源计算

8.1.1  自回归模型的计算量

8.1.2  自回归模型的缓存优化

8.2.1  模型推理中的“贪心生成”与“采样生成”

8.2.2  模型推理过程中的冗余计算问题解析

8.2.3  初识模型推理中的KV Cache与代码实现

8.3  减少空间占用的自回归模型代码实现与详解

8.3.1  经典自回归模型详解

8.3.2  能够减少空间占用的自回归模型代码完整实现

8.3.3  缓存使用与传递过程详解


在前面的章节中,我们已经详细讲解了基于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-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)

上面代码解释如下:

  1. 处理新的输入:只处理当前时间步的输入(input_ids[:, -1:])。
  2. 复用缓存:每层GPT-2块(GPT2Block)接收当前时间步的输入和之前的键值对(past_key_values)。
  3. 生成新的键值对:当前时间步的键值对(K和V)被计算并返回。
  4. 缓存更新:新的键值对被添加到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秒。

请注意,这个数值可能因读者的电脑硬件配置差异而有所变化,建议读者根据自身情况进行相应设置。

8.4.4  在缓存的生成模型推理资源量化展示

下面我们采用同样的长度在带有缓存的生成模型上演示推理资源的占用,读者可以首先完成短文本的生成并对比生成质量,之后使用长文本检测生成的资源占用。同样地,我们采用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显存占用来提高资源利用效率,缓存机制都展现出了显著的效果。这为我们在未来进一步优化生成模型的性能提供了有力的支持。

KV缓存压缩原理与无损重建技术实战 KV缓存是大模型推理中显存占用的核心瓶颈,其动态性、局部相关性与跨head低秩冗余特性,使传统权重量化方法失效。KV缓存压缩并非简单降比特,而是需结合块级稀疏编码、动态秩对齐与量化感知重建的协同优化机制。该技术通过SVD分解+自适应能量截断实现高保真重建,在保障BLEU、MT-Bench等任务指标无统计显著下降前提下,将70B模型KV显存从330GB压至90GB。适用于A100/H100服务器部署、vLLM集成及边缘端侧推理,是当前平衡精度、速度与成本的关键工程路径。 阅读详情

相关推荐

DeepSeek三大硬核优势:MoE落地、中文长上下文、代码数学双强

大语言模型的实用价值,取决于其在真实场景中的工程鲁棒性与领域适配能力。MoE架构并非参数堆砌的噱头,而是通过稀疏激活、路由约束与专家容量控制实现推理效率与性能的平衡;中文长上下文支持的关键,在于从tokenizer优化、FlashAttention-2到PagedAttention与局部-全局混合注意力的全链路协同;而代码与数学双强能力,则源于高质量清洗的数据闭环与‘符号推理+程序验证’的联合训练范式。这些能力共同支撑DeepSeek在金融财报解析、企业级SQL生成、工业代码补全及长文档RAG等生产级任务中

cuibinmo3519的博客 417

GLM-5.2大模型本地部署实战:从环境搭建到性能优化,实现超越官方API的推理速度

在实际部署和使用大语言模型时,很多开发者会遇到一个共同的痛点:官方提供的API服务虽然方便,但在响应速度、成本控制和数据隐私方面存在诸多限制。特别是对于GLM-5.2这类拥有100万token超长上下文、在编程和智能体任务上表现卓越的旗舰模型,如果能将其部署在自己的硬件上,不仅能获得更快的推理速度,还能实现完全的数据本地化处理。本文将以GLM-5.2为例,详细拆解从模型下载、环境搭建、推理框架选型到性能优化的完整自部署流程,并解释为什么在特定条件下,自部署的推理速度可以远超官方API。 自部署GLM-5.2

weixin_30256901的博客 539

昇腾950PR上Qwen3-27B推理decode优化实战:从22.3ms到9.2ms

大模型推理性能优化中,decode阶段往往是瓶颈所在。相比prefill阶段的高算力消耗,逐token生成的decode过程受显存带宽限制,权重读取量大但计算量小,导致吞吐低下、时延抖动。本文从显存带宽、KV Cache管理、算子调度三个核心维度展开分析,结合昇腾950PR平台和Qwen3-27B模型的实测数据,系统拆解了权重量化、KV Cache量化、算子融合、连续批处理等优化手段的实际效果。TTFT、TPOT、吞吐量等关键指标如何定义,长上下文下KV Cache膨胀如何应对,并发升高时调度器如何调整——

weixin_34099526的博客 365

一机多模,SSD 缓存两层归一 | 泛联 UbiPower 18000 升级 KV Cache 加速特性

大模型推理正加速走向超长上下文、高并发与复杂智能体场景,KV Cache 作为推理核心中间数据,直接影响推理时延、业务体验与集群建设成本。泛联 UbiPower 18000 全新 KV Cache 特性落地,依托一机多模架构,单套存储可同时承载模型训练 Checkpoint 存储、推理 KV Cache 卸载双业务;提供独有的 KV Cache SSD 缓存两层归一能力,单存储池覆盖 G3、G3.5 两层缓存,替代分层部署的本地 SSD 集群与 CMX 专用存储。精简后的架构相较原有多层 KV 缓存,

UBIX2022的博客 158

GLM-5.3-Flash部署全攻略:从API到单机异构再到8卡A100生产实践

大模型部署是当前AI工程化落地的核心环节,涉及推理框架选型、显存调度、并发控制与生产服务化等多个技术维度。以GLM-5.3-Flash为例,其部署链路覆盖了从轻量级API调用到重量级多卡推理服务的完整跨度:API路线适合快速验证与弹性扩容,单机异构路线满足数据安全与自主部署需求,而8卡A100张量并行则支撑高并发生产场景。vLLM作为主流推理框架,通过PagedAttention和KV Cache优化提升吞吐,配合张量并行、量化、前缀缓存等机制,在成本与性能间取得平衡。本文从基础概念出发,逐步剖析环境配置、

weixin_34023982的博客 391

DSH Desktop全攻略:从环境搭建到插件开发,轻松跑通本地Agent

本地人工智能部署中,环境配置往往是比模型本身更耗时的环节。理解Agent框架的运行原理,掌握跨平台本地化安装与优化技巧,是高效应用大模型的关键。DSH Desktop作为一款封装了DeepSeek Harness的桌面客户端,通过可视化方式解决了依赖冲突、路径兼容、模型下载等痛点。本文从环境检查、分平台安装、模型加载到插件开发,系统梳理了本地Agent的实际操作路径,帮助读者快速跑通完整链路,让AI工具真正服务于日常任务。

weixin_34377919的博客 390

王晓华DeepSeek开发AI应用入门书《DeepSeek原生应用与智能体开发实践》全文分享~持续更新

减少空间占用的生成模型实战与推理资源消耗量化对比。章 大模型驱动的即时金融信息采集与分析平台。在线蒸馏的损失函数与经典微分方程的求解方法。对于智能客服模型垂直领域知识注入的补充讲解。微调的目的:让生成的结果更聚焦于任务目标。减少空间占用的自回归模型代码实现与详解。驱动的即时金融信息采集与分析平台实战。未运行缓存的生成模型推理资源量化展示。自回归模型中的单分类与多分类激活函数。使用微调后的智能客服基座模型完成推理。驱动的即时金融信息采集与分析平台。在缓存的生成模型推理资源量化展示。

夏天又到了的专栏 680

2026年本地部署大模型硬件配置指南:软硬协同优化实战

大模型本地部署已从‘能跑起来’迈入‘稳定高效运行’的新阶段。其核心在于理解推理全链路——从磁盘加载、显存预热、逐token生成到响应返回——每一环节都受硬件协同影响。显存容量决定模型规模上限,但显存带宽、CPU三级缓存、内存延迟与PCIe通道数等‘隐性参数’,正成为影响RAG响应速度、多轮对话稳定性与并发吞吐的关键因素。结合INT4/NF4量化技术与vLLM 0.7等新一代推理引擎,消费级平台首次实现16B级模型的低延迟、高可用部署。本文聚焦2026年3月技术成熟节点,解析GPU-CPU-内存-存储-散热的

weixin_30279315的博客 426

Sails Waterline 模型方法 `.create()` 完全指南:创建记录、链式取回与错误协商

本文基于 Sails 框架的官方参考文档,深入讲解 Waterline ORM 中最常用的模型方法之一——[`.create()`](https://link.gitcode.com/i/784dba487cb38033dd6bea20f99edab8)。无论是搭建用户注册接口、写入业务数据,还是通过 Blueprint 路由接收 `POST` 请求,`.create()` 都是向数据库插入新记录

gitblog_00542的博客 449

PyTorch实战:手把手教你用QAT量化感知训练拯救模型精度(附避坑指南)

本文详细介绍了如何使用PyTorch进行QAT量化感知训练,以提升模型在边缘设备上的推理精度。通过核心原理讲解、实战代码示例和避坑指南,帮助开发者掌握从环境配置、模型训练到边缘部署的全流程,有效解决传统量化方法导致的精度损失问题。

c7d8e的博客 325

使用DevExpress22.X(Patch)控件库在VisualStudio2022使用C#进行Winform、WPF应用的开发,看这一篇就够了!

写在开头,Dev Express是个十分强大的控件库(下文简称Dev),但碍于其高昂的使用费用,“出于学习目的”,我们一般使用的都是Patch版本(在版权意识日趋加强的当下,不要提那两个字,现在加上那些字,百度都搜不出内容)。

平凡之路 2万+

DeepSeek V4架构深度解析:1.6万亿参数背后的效率革命

大模型正在从"更大"走向"更聪明、更高效"。1.6万亿参数听起来吓人,但真正有价值的是背后的架构创新——Mega MoE、混合注意力、KV Cache优化——这些才是让"大模型"真正落地的技术底座。技术报告再漂亮,终究要经受生产环境的考验。V4的生产稳定性、长期运行表现、昇腾生态的成熟度,这些都需要时间来验证。保持关注,小规模测试,生产部署前做好充分评估。这篇文章基于DeepSeek官方技术报告和实测经验写成。如有疏漏,欢迎指正。原创首发于优快云,转载需授权。

yp0to1的博客 541

Python爬虫实战:抓取天气数据并生成可视化图表(完整可复制代码)

工具/库核心作用选型理由requests发送HTTP请求,获取天气网页内容语法简单,上手快,支持Headers配置(反爬)解析HTML页面,提取目标天气数据无需懂复杂XPath,API直观,快速定位标签pandas数据清洗、整理(如日期/温度格式化)一键处理数据格式,与matplotlib无缝适配matplotlib生成可视化图表(折线图/饼图/柱状图)Python主流可视化库,定制性强,支持保存高清图生成随机User-Agent,应对反爬自动更新UA池,避免手动维护。

专注于Python爬虫开发,分享爬虫技巧、项目实战与反爬经验,使用Scrapy、BeautifulSoup等工具,解决数据抓取难题。 909

GPT-4稀疏激活原理:2%参数如何实现高效推理

大语言模型的参数规模已突破万亿量级,但‘参数多=能力强’的认知正被稀疏激活(Sparse Activation)范式重构。其核心原理在于动态路由机制——通过轻量门控网络,在每次token生成时仅激活约2%的专家子网络(Mixture of Experts, MoE),大幅降低显存占用与计算延迟。这一技术不仅突破了密集模型(Dense Model)在硬件带宽、功耗和延迟上的物理瓶颈,更在工程实践中实现了推理吞吐翻倍、成本下降超60%的实际价值。当前主流MoE架构如GPT-4、Qwen2-MoE、DeepSee

weixin_30314813的博客 367

MATLAB和S7-1200PLC水箱液位高度PID控制联合仿真(KEPserverOPC通信应用)

SMART PLC 向导PID的详细介绍请查看下面文章链接:S7-200 SMART PLC PID向导详细介绍(如何实现P、PD、PID控制器)-优快云博客文章浏览阅读1k次。这篇博客主要介绍SMART PLC PID向导的使用,PID控制相关的其它内容请查看专栏系列文章,常用链接如下:SMART PLC PID负压控制(过程量为负数)_负压控制pid控制程序_RXXW_Dor的博客-优快云博客1、如何实现PID反作用调节?S7-1200PLC和KEPserver OPC通信-优快云博客。

RXXW_Dor的博客 1780

SerDes系列之如何选择AC耦合电容

交流耦合电容用于隔离PCB互连时的直流分量(Common-mode voltage),同时传递交流分量(Voltage swing),其作用类似于一个高通滤波器,但是,如果电容容值选取不当,使用过程中会产生信号质量问题,典型的应用场景:当包含相同的长串1或0的NRZ数据应用于该高通滤波器时,会发生低频电压跌落,导致低频模式相关抖动(PDJ)。

一只豌豆象的博客 2350

清华大学出版社-人工智能技术丛书-介绍

AI Agent智能体开发实践》共分16章,内容包括初识智能体、智能体开发环境、智能体的关键技术、提示工程、RAG(检索增强生成)技术、智能体开发平台简介、智能体开发流程、基于LangChain的智能体开发、基于MCP的智能体开发、基于LangChain的问答智能体实战、多模态电商客服机器人实战、智能体性能优化与调试技巧、智能体部署与实施方法、多模态试驾预约Agent实战、基于RAG的多Agent客户服务助手实战、基于MCP的多Agent旅行规划助手实战。

夏天又到了的专栏 1491

博途PLC模糊PID控制(量化因子和比例因子)

参数"量化因子和比例因子",都是为了对清晰值进行比例变换而设置的,其作用是使变量按一定比例进行放大或缩小,以便和我们的模糊论域相匹配完成一一映射。这种变换对整个系统的工作性能也是有影响的。(其实相当于额外引入了比例作用,所以说模糊PID的调节是个系统工程)。限于本人能力,文中难免出现错误和不足,欢迎大家批评和指正。博途PLC的模糊PID控制完整算法可以参看下面这篇文章博途PLC模糊PID控制详解(SCL完整源代码+参数)_RXXW_Dor的博客-优快云博客。......

RXXW_Dor的博客 5268

腾讯云FDE工程师认证全解析:大模型私有化部署的最后一公里

大模型落地过程中,私有化部署已成为数据敏感型企业的刚需,但模型推理环境配置、GPU显存规划、多卡并行通信等工程问题,长期缺乏标准化的人才评价体系。FDE(前沿部署工程师)认证的推出,正是为解决这一断层而生。它系统定义了从模型选型、量化压缩、推理框架调优到业务系统集成的完整能力模型,为AI基础设施交付提供了可复用的方法论。对于从事运维、DevOps或传统软件交付的工程师而言,掌握这类技能不仅能够应对企业私有化AI项目的部署挑战,更能在日趋激烈的AI Infra人才竞争中建立差异化优势。本文结合典型交付流程,从

weixin_32075843的博客 218

Android功耗系列专题理论之六:外设功耗问题分析方法

本文介绍了移动设备功耗问题的分析方法,重点针对传感器和GPU两大模块。传感器部分详细讲解了通过ADB命令获取日志、状态差异分析和单传感器隔离测试的排查方法,并给出优化建议。GPU部分从基础功耗测量、场景测试到功能模块拆解进行多维度分析。同时阐述了GNSS/GPS功耗的测试方法,包括前后台对比、星座双频拆解及SAP/DPO等特殊功能的电流分析,为移动设备功耗优化提供系统性的诊断思路。

368

提升ARM Cortex-M系统性能的关键技术:TCM技术解析与实战指南

TCM技术为ARM Cortex-M处理器提供了宝贵的确定性性能提升手段,特别适合对时序敏感的实时应用。通过合理配置和优化,工程师可以显著提高系统性能、降低中断响应时间并优化功耗。随着嵌入式系统对实时性要求的不断提高,TCM技术将在工业控制、汽车电子、医疗设备等领域发挥更加重要的作用。未来,ARM处理器可能会进一步扩展TCM容量并优化其与其他内存子系统的协同工作方式,为开发者提供更强大的实时性能保障。

1678

人工智能世界观(认知篇)

本文系统梳理了人工智能(AI)的基础知识体系,重点解析了大模型(LLM)的核心原理、技术演进及应用边界。主要内容包括: AI基础认知:从规则驱动到数据驱动的范式转变,强调AI=数据+算法+算力的技术本质,区分了弱AI与通用AI(AGI)的能力差异。 技术发展史:回顾AI七十年发展,从符号主义、专家系统到深度学习的五次技术浪潮,重点分析Transformer架构如何推动大模型革命。 大模型原理:详解LLM的三大支柱(参数量/数据量/算力),揭示自回归生成和注意力机制的工作逻辑,对比GPT与BERT的技术路线差

轻舟已过万重山 543

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

6 款电商 AI 图片生成软件实测|国内 + 跨境卖家工具清单

电商 AI 作图工具已经从 "能不能用" 进入了 "好不好用" 的阶段。对于绝大多数卖家而言,现在的 AI 工具已经可以覆盖 60%-80% 的基础商品图需求,显著降低拍摄和设计成本。选择工具的核心逻辑不是 "哪款最强",而是 "哪款最适配你的品类、平台规模和团队能力"。垂直电商工具胜在工作流效率,通用生图工具强在创意上限,模板类工具赢在上手速度。建议根据自身业务阶段,搭配 1-2 款主力工具 + 1 款辅助工具,形成适合自己的视觉生产流程。

Mardi_Ladin的博客 266

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

我用 GPUWorks 在 M1 Pro 上搭了个发票识别原型

公司里经常要看电子发票。我想先做个小原型:把 PDF 发给一个自己部署的图文模型,让它提取发票类型、日期、交易双方和金额,再把结果交给同事核对。这个原型不用先接财务系统,先看看从部署到调用要走多少步,模型返回的内容能不能帮上忙。

鸽芷咕的博客 1万+

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

您您这可以把问小白的多个会话比如说。左侧的多个会话一次性导出吗?不是单条会话里面的多次会对话。AI导出鸭

第三层是格式编译层。这个决策背后的判断很清晰:一个批量导出工具的价值,不在于“试三次”,而在于你真正用它处理了几十份技术文档、访谈记录或课程讲义之后,它是否值得留在你的工作流里。问小白的会话列表通过分页接口加载,AI导出鸭 在页面运行时建立旁路监听,捕获每一次分页请求返回的完整对话清单,包括会话 ID、标题、时间戳和隐藏的元数据字段。让AI导出回归优雅——这句话说的是:导出不该是体力活,而应该是一次完整的、可预期的文件交付。注意:这里的“批量”指的是左侧列表里的多个独立会话,不是一条会话内的多轮对话。

laoyunsi的博客 216

在Codex 让AI模型 拥有长期智能记忆

本文介绍Claude-Mem V15新功能——智能记忆召回,解决AI遗忘、重复提示、执行不一致等问题。通过本地部署claude-mem,实现自动记录决策(如数据库选型)、跨会话智能召回,支持记忆写入、检索、遗忘全流程闭环。相比传统记忆方案,其基于Hooks自动捕获工具调用,结构化存储,可追溯、可共享、可清理,显著提升工程效率与协作一致性,是长期项目中理想的“工作黑匣子”与“工程脑”。

米莱虾 248
上一篇: 第 7 章 大模型驱动的即时金融信息采集与分析平台
下一篇: 第 9 章 DeepSeek核心技术2:MLA注意力机制
夏天又到了
博客等级 码龄19年 6582粉丝 1589原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值