概述
之前写过一篇文章【大模型】DeepSeek-R1各版本模型推理显存需求测算【理论+实践】详细写了一下模型参数量和所需显存,但在实践中,发现所占显存往往比理论值稍大一些。为此,又进行一些更深入的研究。
本文主要回答以下几个问题:
- 本地部署大模型时,如何快速判断所需显存量?
- 大模型推理所需显存由哪几部分构成?
- KV Cache是什么,为什么只有KV Cache而没有Q Cache?
- KV Cache显存如何进行计算?
- 大模型为什么会出现失忆现象,本质原因是什么?
- Ollama中,如何修改上下文长度?
1. DeepSeek-R1所需显存快速评估
在和群友交流时,发现了一个thinkinai团队制作的显存计算器网站:
链接如下:http://tools.thinkinai.xyz/#/server-calculator
它可以根据所选的模型参数/量化类型,序列长度/批次大小/GPU数量等信息,动态计算所需显存及每张显卡显存占用情况。
经实测发现,这个计算方式大致是正确的,完全可以根据这个计算结果去选配合适的版本。
但为什么说是“大致”,因为从理论分析上来说,这个计算结果还是略有保守。
2. 推理显存构成
从上面的计算器可以看出,大模型推理显存由框架固定开销、模型参数、激活值、输出张量四部分构成。
框架固定开销主要是ollama
、vllm
之类的推理框架所需显存,每张卡预留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自注意力计算方式过程如下:
- 对于输入的 x1, x2, x3,先Embedding,得到a1, a2, a3;
- 对于每个a分别乘上 W q W^q Wq, W k W^k Wk, W v W^v Wv 得到三个向量q,k,v。
- 使用q和其它k进行点乘,得到每个token的注意力分数。
- 对注意力分数进行softmax,得到注意力权重。
- 注意力权重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(dkQKT)
最后,输出是:
Z = A V Z = A V Z=AV
其中:
- Q ∈ R T × d k Q \in \mathbb{R}^{T \times d_k} Q∈RT×dk(查询向量)
- K ∈ R T × d k K \in \mathbb{R}^{T \times d_k} K∈RT×dk(键向量)
- V ∈ R T × d v V \in \mathbb{R}^{T \times d_v} V∈RT×dv(值向量)
- A ∈ R T × T A \in \mathbb{R}^{T \times T} A∈RT×T(注意力分数)
- Z ∈ R T × d v Z \in \mathbb{R}^{T \times d_v} Z∈RT×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(dkQnewK′T)
最终输出:
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(k和v)×每个元素的字节数
以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(k和v)×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×tamp=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
交流群
读者如果有问题或者选题建议,可以点击下方公众号找到交流群入口,进群讨论。