description
一次问答的流程
- input prompt
- tokenization
- embedding
- transformer × n
- sampling
- detokenization
- output
重点就是 transformer × n (最耗时/计算瓶颈)
-
feed forward (前向网络)
-
multi head attentation
-
self attentation
- Q K V (Query Key Value)
-
multi - head attentation由多个self attentation组成,每个self attentation里面会将你的输入线性投影到QKV类型的向量
线性投影 => 矩阵乘法
GPT的两个特点
- Token By Token (每次只预测下一次Token)
- auto regression(自回归)
GPT如何与你上下文交流(KV Cache的作用)
每次生成的Token要作为Input来预测下一次Token,给GPT一个输入,这个输入是一个序列,需要用这个序列来预测下一个Token
[Token1, Token2, Token3... ...]
GPT把整个序列全部算一遍,但是最后只需要拿到最后一个Token在位点上的概率分布来生成一个最新的Token,所以前面N-1的Token状态虽然被计算但是没有拿来用
当最新的Token被计算出来后,会加入到序列的最后,由此反复上面的流程
上述的描述可知,GPT有一个大量的重复运算,重复的数量决定序列的长度
例如cache 把之前算好的K 和 V 存下来,当生成下一个Token的时候,把新的Token带来的QKV追加到GPU的显存里的KV cache上。等下一次输入时,把带来新的Q与已缓存的全部的KV做一次点积乘即可,增大计算效率(接近常数的计算效率)
计算吞吐量
举例:A100 80G
llama 2 -7B (4096 dim,32 layers,4096 context)
model weight = 7b × 2 bytes(FB 16)= 14GB
KV cache / token = 2 × layer × d_dim × 2 bytes = 2 × 32 × 4096 × 2 = 512 KB / token
total KV cache = 4096 × 512 KB = 2 GB
(80 - 14)/ 2 = 33 session(并发,即同时有几个上下文,实际要少,因为也有其他程序使用显存)
QKV优化(llama-3)
上述吞吐量中优化,改变KV的大小来提高吞吐量
grouped query attention会使用更少的KV head => 一旦head 减少,整个KV cache里KV数量也会减少
llama 3 - 8B(32 layer,dim 4096,32 head,8KV,d_head 4096/32 = 128,8192 context)
model = 8 × 2 bytes = 16 GB
KV cache = 2 × layer × m_kv × d_head × 2 bytes = 2 × 32 × 8 × 128 × 2 = 128 KB / token
total = 8192 × 128 KB = 1 GB
(80 GB - 16 GB) / 1 GB= 64 session
MQA(共享一份KV)
计算空间节省,推理速度增大,模型表达能力下降
使用的chat service大概率不是7B的模型,应该是Llama 70B
70 B × 2 bytes = 140 GB的显存 > 显卡显存,解决方案
-
Batching(批处理)
-
naive batching
- 收集一批requests,然后丢给GPU,一批结束放下一批
- 延迟高,水桶效应,最慢的request结束,下一个批处理才开始
-
持续批处理(continous batching)
- 只要批队列里面有空位,就从Message Queue里领取下一个task
-
batching 优化GPU还是有空闲时间
- KV cache写入有两步骤(先Prefill -> Decode)
- Prefill(write cache):把所有的input token全部算好写入 cache
- Decode(read cache + append token):cache算好之后,开始生成新的token,回到上面GPT的过程,每次只生成一个新的token,读取cache旧的数据再追加一个新的Token
- 区别:Prefill是把整个Prompt一次性的输进模型计算一个完整的context。从prompt进入模型到cache写入第一个token,这部分的延迟叫做time to first token,如果prompt比较长,即延迟增长。Decode是后续的计算,每次只生成一个token,因为有了cache,计算量较小,远小于Prefill延迟,这里的延迟叫做time to incremental token
-
-
Parallelism(并行化)
关于PB的更多优化方案
有一批任务使用continous batching执行Decode任务,有一个任务结束,生成一个EOS(end of sequential)的token,接下来把一个Prefill任务添加进去。如果Prefill处理的prompt很长,使用chunked Prefill,把Prefill切成若干的chunk,将一个长的Prefill变为一个可中断可恢复的迭代任务,减少别的任务的等待时间(类似于操作系统的抢占机制,牺牲单个任务的处理速度,提高整体的响应速度)
PD分离(PB separation)
分布式思想,同一个节点内部不同GPU上,或者跨节点
可以分别优化这两个阶段不同的节点
例如在Decode node启动cuda graph(减少CPU的开销,因为GPU执行的任务是由CPU派发的,GPU会等待CPU发布任务,cuda graph会把这个流程capture(录制)下来形成计算图,通过重放来快速执行,压缩整个通信的次数)
问题:跨界点的传输是有开销的,取决于KV cache的大小
减小KV cache的体积
- 量化(quqatization):一个浮点型32bit,使用更小的空间来表示,代价是精度上会有损失
- 分页(page attentation和page flash-attentation):page flash-attentation用来减少计算的时候 SRAM 与 HBM之间的内存访问,page attentation用来减少KV存储的碎片。在 vllm 中 flash paged attention kernel,在每次的传输中只传输 page table 和活跃的 block,不活跃的page 可以进行延迟拉锯(操作系统的分页)
- 去重(prefix sharding):有多条请求,然后多条请求的前缀是相同的,KV可以只传一次,可以把这个KV持久化到一个地方(例如内存,内存可以达到2T)。当发现某一个请求和已经持有的某一段KV cache的前缀是相同的,那只需要传输部分不同的差量页,减少搬运体积。不好的点是增加开销,KV cache要保存到本地的内存,要在上游添加一个smart router,然后根据请求里的前缀去查找一下之前是否有相似的KV cache,将这个请求转发到相应的机器
- 压缩(compression):稀疏化,裁剪一些token或header,sliding window attention、流式编码等很多方案
加快传输速度
- 硬件层面(不关心)
streaming pipeline
- 前面提到的 chunk prefill的调度(还没具体了解)
并行化:为了解决结构性问题,即设备的容量,模型占用的内存比这个显卡的内存大
-
traning 阶段
- 张量并行
- 数据并行
- 流水线并行
-
推理阶段
-
张量并行 (tensor parallelism,太复杂了不想描述,学好线性代数)
- 把一张GPU放不下的计算放到多个GPU上,本质是拆分成一个矩阵然后计算完再进行聚合
-
专家并行(export paralleism,deepseek论文):有负载均衡问题,部署动态路由
-
上下文并行(context parallelism,太复杂了不想描述)
-
quotation
zhuanlan.zhihu.com/p/224361144…
DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning:arxiv.org/abs/2501.12…
DeepSeek-V3 Technical Report:arxiv.org/abs/2412.19…
DeepSeek LLM: Scaling Open-Source Language Models with Longtermism:arxiv.org/abs/2401.02…