【大模型】如何正确评估DeepSeek-R1各版本所需推理显存?KV Cache原理和显存计算解析

概述

之前写过一篇文章【大模型】DeepSeek-R1各版本模型推理显存需求测算【理论+实践】详细写了一下模型参数量和所需显存,但在实践中,发现所占显存往往比理论值稍大一些。为此,又进行一些更深入的研究。

本文主要回答以下几个问题:

  1. 本地部署大模型时,如何快速判断所需显存量?
  2. 大模型推理所需显存由哪几部分构成?
  3. KV Cache是什么,为什么只有KV Cache而没有Q Cache?
  4. KV Cache显存如何进行计算?
  5. 大模型为什么会出现失忆现象,本质原因是什么?
  6. Ollama中,如何修改上下文长度?

1. DeepSeek-R1所需显存快速评估

在和群友交流时,发现了一个thinkinai团队制作的显存计算器网站:
链接如下:http://tools.thinkinai.xyz/#/server-calculator

在这里插入图片描述

它可以根据所选的模型参数/量化类型,序列长度/批次大小/GPU数量等信息,动态计算所需显存及每张显卡显存占用情况。

经实测发现,这个计算方式大致是正确的,完全可以根据这个计算结果去选配合适的版本。

但为什么说是“大致”,因为从理论分析上来说,这个计算结果还是略有保守。

2. 推理显存构成

从上面的计算器可以看出,大模型推理显存由框架固定开销、模型参数、激活值、输出张量四部分构成。

框架固定开销主要是ollamavllm之类的推理框架所需显存,每张卡预留1G基本够用。

模型参数在前文已详细分析过,总结下来可以直接用int8精度进行换算,比如70B模型int8进度所需显存就是70GB,int4精度所需显存就是35GB。

激活值是指模型运行时产生的中间计算结果,需要结合具体模型参数进行计算,以DeepSeek-R1-70B模型为例,其具体配置参数可以在huggingface中找到:https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Llama-70B/blob/main/config.json

实际只需要看两个参数:

  • 层数:80
  • 隐藏层维度:8192

每层显存占用可以用以下公式进行计算:

每层显存占用 = 序列长度 × 隐藏层维度 × 批处理大小 × 每个元素的字节数 每层显存占用=序列长度×隐藏层维度×批处理大小×每个元素的字节数 每层显存占用=序列长度×隐藏层维度×批处理大小×每个元素的字节数

其中,序列长度即输入到模型中的问题,其中不仅是用户的问题,还包括模型与用户的历史对话记录。批处理大小不考虑并发的情况下,对单用户为1,每个元素字节数由序列精度进行设定。

因此,对于DeepSeek-R1-70B,假设序列长度为2048tokens,以FP16(2字节)的序列精度情况下,每层显存占用为

每层显存占用 = 2048 × 8192 × 1 × 2 = 33554432 B = 0.03125 G B 每层显存占用=2048×8192×1×2 = 33554432 B = 0.03125 GB 每层显存占用=2048×8192×1×2=33554432B=0.03125GB

如果80层全算上的话,DeepSeek-R1-70B模型激活值所需显存即为0.03125 GB × 80 = 2.5GB

但是在实际上,推理过程是逐层进行的,即每一层的激活值在完成下一层计算后就会被释放,不会长期占用显存,因此实际只需要考虑每层显存占用就行了,这部分小到基本可以忽略[1]。因此,上面的计算器对于这部分的显存计算,看起来还是略有保守。

最后一部分,输出张量的显存占用是指模型生成结果所需的临时存储空间,主要和KV Cache,将在下一节进行剖析。

3. KV Cache原理

1. 图解自注意力计算方式

在计算KV Cache前,先要理解它是什么含义。KV Cache是一种大模型推理加速的方法,通过缓存Attention中的K和V来实现推理优化[2]。

Transformer中文本是逐个 token 生成的,每次新的预测,会基于之前生成的所有 token 的上下文信息。 KV Cache主要是针对自注意力计算机制的优化。

其中,Transformer自注意力计算方式过程如下:

  1. 对于输入的 x1, x2, x3,先Embedding,得到a1, a2, a3;
  2. 对于每个a分别乘上 W q W^q Wq W k W^k Wk W v W^v Wv 得到三个向量q,k,v。
  3. 使用q和其它k进行点乘,得到每个token的注意力分数。
  4. 对注意力分数进行softmax,得到注意力权重。
  5. 注意力权重a与对应的向量v进行加权求和,最终得到特征向量b,输入到后续网络中进行计算。

下图中展示了b1的计算方式:

在这里插入图片描述

同理,b2也可以用相同的方式进行计算:

在这里插入图片描述

2. 自注意力的矩阵计算表示

在实际中,往往通过矩阵乘法来实现并行加速计算,进一步用矩阵表示的计算公式如下:

对于输入序列 X = [ x 1 , x 2 , … , x T ] X = [x_1, x_2, \dots, x_T] X=[x1,x2,,xT],每个 Token x t x_t xt 通过投影得到 Query ( Q Q Q)、Key ( K K K) 和 Value ( V V V) 矩阵:

Q = X W Q , K = X W K , V = X W V Q = X W_Q, \quad K = X W_K, \quad V = X W_V Q=XWQ,K=XWK,V=XWV

其中 W Q , W K , W V W_Q, W_K, W_V WQ,WK,WV 是可训练的权重矩阵。
然后计算注意力得分:

A = softmax ( Q K T d k ) A = \text{softmax} \left( \frac{Q K^T}{\sqrt{d_k}} \right) A=softmax(dk QKT)

最后,输出是:

Z = A V Z = A V Z=AV

其中:

  • Q ∈ R T × d k Q \in \mathbb{R}^{T \times d_k} QRT×dk(查询向量)
  • K ∈ R T × d k K \in \mathbb{R}^{T \times d_k} KRT×dk(键向量)
  • V ∈ R T × d v V \in \mathbb{R}^{T \times d_v} VRT×dv(值向量)
  • A ∈ R T × T A \in \mathbb{R}^{T \times T} ART×T(注意力分数)
  • Z ∈ R T × d v Z \in \mathbb{R}^{T \times d_v} ZRT×dv(最终输出)

这种方式的主要问题是每次生成新 Token x T + 1 x_{T+1} xT+1 时,必须重新计算 K , V K, V K,V 并计算整个注意力矩阵 A A A,导致计算复杂度过高。

3. KV Cache 计算

KV Cache核心思想是用空间换时间,即缓存之前计算的 K K K V V V,即:

K cache = [ K 1 , K 2 , … , K T ] K_{\text{cache}} = [K_1, K_2, \dots, K_T] Kcache=[K1,K2,,KT]

V cache = [ V 1 , V 2 , … , V T ] V_{\text{cache}} = [V_1, V_2, \dots, V_T] Vcache=[V1,V2,,VT]

对于新输入 x T + 1 x_{T+1} xT+1,我们只计算它的新 Key 和 Value:

K new = x T + 1 W K , V new = x T + 1 W V K_{\text{new}} = x_{T+1} W_K, \quad V_{\text{new}} = x_{T+1} W_V Knew=xT+1WK,Vnew=xT+1WV

然后更新缓存:

K ′ = [ K cache , K new ] , V ′ = [ V cache , V new ] K' = [K_{\text{cache}}, K_{\text{new}}], \quad V' = [V_{\text{cache}}, V_{\text{new}}] K=[Kcache,Knew],V=[Vcache,Vnew]

计算新 Query:

Q new = x T + 1 W Q Q_{\text{new}} = x_{T+1} W_Q Qnew=xT+1WQ

并计算注意力分数(只对新 Query 进行计算,而不是整个序列):

A new = softmax ( Q new K ′ T d k ) A_{\text{new}} = \text{softmax} \left( \frac{Q_{\text{new}} K'^T}{\sqrt{d_k}} \right) Anew=softmax(dk QnewKT)

最终输出:

Z new = A new V ′ Z_{\text{new}} = A_{\text{new}} V' Znew=AnewV

用一个更加通俗的解释方法,把自注意力计算过程类比成 "提问+查找答案"的过程:

  • Query ( Q Q Q): 每个 Token 生成自己的问题 “我要关注哪些 Token?”
  • Key ( K K K): 之前所有 Token 存储的 “索引信息” 。
  • Value ( V V V): 之前所有 Token 存储的 “实际内容” 。

新Token只需要自己生成自己的问题(Query),然后去 Key-Value 存储库中查找答案。

实际应用中,KV Cache可以进一步分成预填充阶段(Prefill Stage)和解码阶段(Decoding Stage)。预填充阶段就是用户首次和模型对话时,模型会处理输入序列,生成每个 Token 对应的键(Key)和值(Value)向量并将其缓存起来。在模型推理推理时,进入解码阶段,从预填充阶段获得的表示出发,逐个生成输出 Token。每生成一个 Token,都会更新模型的状态,并将新生成的 Token 作为输入,继续生成下一个 Token。

在解码阶段,需要频繁地从显存乃至内存中加载 KV cache,大量的数据读取操作会增加访存开销,影响推理的吞吐率[3]。

因此,PageAttention、MQA、MGA、FlashAttention系列策略对其进一步优化,这个后面有时间再进行分析。

4. KV Cache 所需显存计算

KV Cache 所需显存计算公式如下:

K V C a c h e 显存 = 序列长度 × 层数 × K V 头数量 × 注意力头的维度 × 2 ( k 和 v ) × 每个元素的字节数 KV Cache显存 = 序列长度× 层数 × KV头数量 × 注意力头的维度 × 2(k和v) × 每个元素的字节数 KVCache显存=序列长度×层数×KV头数量×注意力头的维度×2(kv)×每个元素的字节数

以DeepSeek-R1-70B为例,假设序列长度为2048,层数为80,KV注意力头的数量为8,注意力头的维度为128,那么所需KV Cache显存为

K V C a c h e 显存 = 2048 × 80 × 8 × 128 × 2 ( k 和 v ) × 2 = 0.625 G B KV Cache显存 = 2048 × 80 × 8 × 128 × 2(k和v) × 2 = 0.625GB KVCache显存=2048×80×8×128×2(kv)×2=0.625GB

第一节的计算器中,对输出向量的计算公式是这样的:批次大小: 1 × 序列长度: 2048 × 词表大小: 128,256 × 精度: BF16 (2 bytes) ÷ (1024³),得到的结果是0.49GB,和直接显性计算的结果是差不多的。

5. 模型上下文分析及修改

5.1 模型上下文原理分析

现在已经理清楚了如何正确计算一个模型所需显存,从这种计算分析中,可以看到序列长度会影响激活值和KV Cache的显存占用。尤其是对KV Cache的显存占用影响很大。

序列长度受限于模型的上下文长度。多轮对话中,当用户输入一个问题时,不仅会将问题本身送进模型中进行推理,还会把历史问题和历史回答一并输入,这样使模型有了一定的“记忆”能力。DeepSeek官方的API文档[4]绘制了多轮对话的输入过程,如下图所示:

在这里插入图片描述

因此,大模型本质上没有记忆能力,只是输入的上下文窗口很长,在推理时看到了历史记录。如果一直进行对话,超过上下文窗口上限,模型就会出现失忆的情况。

5.2 模型上下文长度修改

在Ollama中,默认设置的上下文长度是2048。可以用ollama show 模型名的方式查看每个模型最大的上下文长度。比如,deepseek-r1:70b模型的最大上下文长度(context length) 为 131072,如果不调整Ollama的默认设置,其实发挥不出70b模型的原本记忆能力。

在这里插入图片描述

因此,显存允许的情况下,可以适当把上下文长度调大。在Ollama中,可以通过修改OLLAMA_CONTEXT_LENGTH环境变量的方式进行调整。

修改配置文件:

vim /etc/systemd/system/ollama.service

添加设置:

Environment="OLLAMA_CONTEXT_LENGTH=4096"

保存,重新加载配置文件,并重启 ollama。

systemctl daemon-reload
systemctl restart ollama

下面做了个小实验,因为KV Cache需要根据序列长度来进行动态存储,下图左侧中是模型刚刚运行起来的显存占用情况,右侧是我和模型进行了3轮对话之后的显存占用情况,可以看到每张卡的显存占用都有所增加,说明KV Cache的确在发挥作用。

在这里插入图片描述

0.5.0之后版本的Ollama进一步支持了KV Cache的量化,可以通过OLLAMA_KV_CACHE_TYPE进行设置,从官方仓库[5]中,摘录了更多环境变量的设定参数,有需要可参考:

  • OLLAMA_DEBUG:显示额外的调试信息(例如:设置 OLLAMA_DEBUG=1)。
  • OLLAMA_FLASH_ATTENTION:启用 Flash Attention。
  • OLLAMA_KV_CACHE_TYPE:设置键/值缓存的量化类型(默认值:f16)。
  • OLLAMA_GPU_OVERHEAD:为每个 GPU 保留的显存(单位:字节)。
  • OLLAMA_HOST:指定 Ollama 服务器的 IP 地址(默认值:127.0.0.1:11434)。
  • OLLAMA_KEEP_ALIVE:设置模型在内存中保持加载的时长(默认值:5m)。
  • OLLAMA_LLM_LIBRARY:设置 LLM 库以绕过自动检测。
  • OLLAMA_LOAD_TIMEOUT:设置模型加载超时时间(默认值:5m)。
  • OLLAMA_MAX_LOADED_MODELS:设置每个 GPU 上最大加载模型数量。
  • OLLAMA_MAX_QUEUE:设置请求队列的最大长度。
  • OLLAMA_MODELS:指定模型目录的路径。
  • OLLAMA_NOHISTORY:禁用 readline 历史记录保存。
  • OLLAMA_NOPRUNE:启动时不修剪模型 blobs。
  • OLLAMA_NUM_PARALLEL:设置最大并行请求数。
  • OLLAMA_ORIGINS:设置允许的源列表,使用逗号分隔。
  • OLLAMA_SCHED_SPREAD:始终在所有 GPU 上调度模型。
  • OLLAMA_MULTIUSER_CACHE:优化多用户场景下的提示缓存。
  • OLLAMA_CONTEXT_LENGTH:设置默认的上下文长度(默认值:2048)。
  • OLLAMA_NEW_ENGINE:启用新的 Ollama 引擎。

参考

[1] 大模型推理显存计算:为什么激活值显存可以忽略不计?(中英双语):https://blog.csdn.net/shizheng_Li/article/details/144150077

[2] 图解大模型推理优化之KV Cache:https://mp.weixin.qq.com/s?src=11&timestamp=1742265618&ver=5875&signature=uzWzHDLKB8taiuYsvaHNkv9CjDB-Rnom2xtP5jfQXdsdxM2Pjw8LvwK5rIxKjUaw-U6TOdQBT3IM2-NllV9MF98x-pmBnvYLRrwmP-pD9h2p7P3iEMg4J8YZS51o&new=1

[3] 备战春招!华为面试每日一题之用KV cache会存在哪些问题?:https://zhuanlan.zhihu.com/p/18748221598

[4]DeepSeep API文档:https://api-docs.deepseek.com/zh-cn/guides/reasoning_model

[5] ollama/envconfig/config.go:https://github.com/ollama/ollama/blob/021dcf089d77292976ee7655eca424dd0b53b8f4/envconfig/config.go#L233

交流群

读者如果有问题或者选题建议,可以点击下方公众号找到交流群入口,进群讨论。

### DeepSeek 显存计算方法及优化技巧 #### 1. FP8 数据类型的使用 为了有效降低显存占用,DeepSeek-V3引入了FP8数据类型用于存储激活值。相比传统的FP32或FP16,FP8显著减少了每项数值所需的内存空间,从而幅降低了整体显存量的需求[^1]。 ```python import torch # 示例:创建一个随机张量并将其转换为FP8(假设PyTorch支持) tensor_fp32 = torch.randn(1024, dtype=torch.float32) tensor_fp8 = tensor_fp32.to(torch.float8_e4m3fn) # 假设此功能存在 print(f"Original size (FP32): {tensor_fp32.element_size() * tensor_fp32.nelement()} bytes") print(f"Reduced size (FP8): {tensor_fp8.element_size() * tensor_fp8.nelement()} bytes") ``` #### 2. BF16 对于优化器状态的保存 除了采用FP8处理激活值外,DeepSeek-V3还利用BF16来储存优化器的状态参数。这种方法不仅进一步节约了宝贵的GPU资源,同时也保持了一定程度上的精度损失最小化。 #### 3. 部分操作的选择性重计算机制 针对某些特定的操作如RMSNorm、MLA Up-Proj以及SwiGLU等,DeepSeek-V3实现了按需重新计算的功能而非每次都缓存中间结果。这种做法可以在不影响最终性能的前提下极限度地释放临时使用的显存空间。 #### 4. 并行策略的设计考量 通过对上述措施的有效实施,使得设计更加灵活高效的并行方案成为可能——比如尽可能减少乃至完全取消对于张量并行这一传统手段依赖的程度。这样的改进有助于提高系统的可扩展性效率的同时也减轻了单个节点的压力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zstar-_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值