最近做吞吐量调试涉及到输入batch_size的设置,为了把算力和显存用起来,同时不触发out of memory,需要提前估计大模型推理过程中的显存占用,我参考了MindIE官网的这个文档完成了估计:https://www.hiascend.com/document/detail/zh/mindie/20RC2/mindieservice/servicedev/mindie_service0105.html 。
显存估计
大模型推理的过程中,显存主要用来存储kvcache。kvcache的大小和token的数量成正比,我们首先来看一下单个token的kvcache怎么计算。假设单token输入经过embedding层之后的维度是hidden_size,那么接下来就要和k权重矩阵和v权重矩阵相乘,kv的权重矩阵shape是[hidden_size, hidden_size],所以计算得到的k和v的维度是hidden_size。需要注意的是,虽然这里kv矩阵只有1个,但实际上现在大部分模型都采用了多头注意力,注意力头的数量是num_attention_heads,单个注意力头的权重矩阵是[hidden_size, hidden_size/num_attention_heads]。此外,如果采用分组kv头,也就是模型的config.json文件中包含num_key_value_heads参数,那么kv的权重矩阵shape就是[hidden_size, (hidden_size/num_attention_heads)*num_key_value_heads],这样的话要缓存的kv的维度就是(hidden_size/num_attention_heads)*num_key_value_heads。由于大模型一般包含多层transformer,所以还需要乘以层数。总的来说,单个token占用的kvcache显存大小计算公式如下:
计算了单个token的kvcache显存大小后,我们就可以计算整个序列所占的显存大小了:
还可以估计所能支持的最大batch_size:
在估计最大batch_size的时候,我们先根据显卡的最大容量(64G)和模型大小估计了可用于缓存kvcahe的空间mem_for_kvcache,然后估算了可以分配多少个kvcache_block。接着计算一个sequence所需要的kvcache_block数量,最后用总的kvcache_block数量除以一个sequence所需要的kvcache_block数量,得到的就是支持的最大batch数量。
上面的代码运行结果如下:
为了验证理论分析是否正确,我们用MindIE跑一下qwen2.5-7B模型:
bash run.sh pa_bf16 performance [[1024,1024],[256,256]] 16 qwen /home/jinxiulang/qwen2.5/Qwen2.5_7B_Instruct 1
运行日志截图如下:
可以看到,日志中包含了“kv cache will allocate 1.75GB memory”,和我们上面估算的是一致的。
计算量估计
为了估算推理执行耗时,还需要估计模型推理消耗的计算量,然后结合芯片算力估计时延。
Transformer模型的计算量主要来自自注意力机制、前馈网络(FFN)和最后的lm_head层。假设模型参数如下:
- L:Transformer层数。
- H:隐藏层大小(隐藏维度)。
- I:FFN中间层大小(通常I>H)。
- V:词表大小。
- S:prefill输入序列长度。
- T:序列总长度(prefill+生成token)。
Prefill计算量
计算每层Transformer的FLOPs:
首先计算自注意力层。第一步是求q、k、v,计算公式是input*weight,input的shape是[S, H],weight的shape是[H, H],所以q/k/v要做SH次向量内积,每次向量内积要做H次乘法和(H-1)次加法,近似于2H,所以q、k、v所有的计算量是3*SH*2H=6SH^2。需要注意的是,如果采用的是分组kvcache,那么计算q、k、v的时候,H要换成(H/num_heads)*num_kv_heads,但是在估算的时候可以近似为H;然后是Q*K(实际上是Q乘以K的转置),输入shape是[S, H]和[H, S],计算量是2S^2H;接着是Q*K*V,输入shape是[S, S]和[S, H],计算量是2S^2H;最后是输出投影,输入shape是[S, H]和[H, H],计算量是2SH^2;所以自注意力层的计算量是8SH^2+4S^2H。
然后计算FFN层的计算量,FFN层包含一个升维层、一个gate层和一个降维层,计算量分别为2SHI、2SHI、2SIH,所以总计算量为6SHI。
所以每个transformer层的计算量为8SH^2+4S^2H+6SHI。再加上最后的lm_head,prefill的计算量为L(8SH^2+4S^2H+6SHI)+2SHV。
Decode计算量
和prefill相比,decode的主要变化是输入序列长度为1。
首先计算自注意力层。计算qkv的计算量是6H^2。计算q*k的时候,由于要把缓存的k也加上,假设当前序列长度是T,那么输入shape是[1, H]和[H, T],所以计算量是2HT;接着计算qkv,输入shape是[1,T]和[T, H],计算量是2HT。所以自注意力层的计算量是8H^2+4HT。
然后计算FFN层的计算量,参考prefill的过程,可知是6HI。
所以每个transformer层的计算量为8H^2+4HT+6HI。再加上最后的lm_head,decode的计算量为L(8H^2+4HT+6HI)+2HV。需要注意的是,这里面的T是变化的,如果要计算K个decode过程的平均耗时,可以取T=(S+S+K)/2进行估计。
实验验证
为了验证我们的理论公式,我们继续基于qwen2.5-7B进行验证,还是运行:
bash run.sh pa_bf16 performance [[1024,1024],[256,256]] 16 qwen /home/jinxiulang/qwen2.5/Qwen2.5_7B_Instruct 1
输出结果部分截图如下:
在输入/输出取1024/1024,batch_size=16的情况下,首token时延1088.62ms,decode平均时延18.77ms。
我们把理论计算公式用python实现:
注意,代码中的npu_cal_ability是我们使用的npu单卡算力,300T Flops左右。
输出结果如下:
可以看到,prefill的预估时间为862ms,和实测的1088.62ms比较接近,但是增量平均时延为0.85ms,远小于实测的18.77ms。主要有以下原因:
1,AI芯片的算力参数为峰值算力,一般情况下AI芯片的算力利用率在60%左右;
2,AI芯片的算力大部分来自矩阵乘法单元,全量计算都是大矩阵运算GEMM,可以充分利用AI Core的能力,但是增量计算都是小矩阵运算(特别是batch_size=1的时候,退化为向量矩阵运算GEMV),导致算力利用率很低;
3,token时延除了包含计算时间,还有内存搬运时间、软件栈之间的数据传输时间等等,对于decode这种运算时间短的场景,其他环节的时延会占很大的比例。
此外,对于分布式推理场景,不能仅凭计算量来估计时延,因为通信算子的耗时往往会较大,比如MOE结构模型中的alltoall通信在推理过程中可能占总时延的30%。
本文由博客一文多发平台 OpenWrite 发布!
所有评论(0)