vLLM V0 架构和 V1 架构的解读

V0 和 V1 的区别
vllm 已经运行了很长时间,成为了目前开源社区中使用最为广泛的推理框架。社区发现在面临诸多模型、硬件、新特性的适配方面,目前的系统架构过于复杂,不够清晰,并且存在着越来越多的技术债需要解决。因此 V1 是为了让整个软件架构发生重构,让整体设计更加清晰、可扩展,易于理解和开发。
V1 的目标
官方博客提出了以下几个点:
提供一个简单、模块化、易于破解的代码:这一点对应了 V0 版本代码过于复杂,难以理解,可以认为是 V1 最主要的目标,就是重构。
优化 CPU 性能损耗,以保障高性能
吸纳一些关键优化到统一的架构设计中
默认进行最优部署,不必进行单独配置
从博客的目标来说,V1 主要就是两个点:
架构重构:此前 V0 存在很多架构问题和技术债,需要通过架构的重构让代码更具维护性,更好理解
性能优化:在重构的过程中,重新考虑当前正在实现或者已经实现的这些关键特性,并将其最优部署默认集成到代码中
V1 的新功能
1. 优化的执行循环和 API Server

随着 GPU 的性能提升越来越大,以及推理工程的功能越来越多,CPU 在整个推理时间的占比影响会越来越大,尤其是一些像 Llama-8B 这样的小模型,当它运行在 NVIDIA H100 这样的高性能服务器时,CPU 的占比就更为明显。
V1 同样参考了 V0 的某些特性,例如在 v0.6.0 的版本中,vLLM 基于 ZerorMQ for IPC 的方式实现了多进程的 API Server,能够覆盖掉一部分在 API Server 和 AsyncLLM 之间的的开销。V1 借鉴了这一点,创建了一个隔离的 EngineCore,让 EngineCore 和 AsyncLLM 之间的任务进行重叠,进一步降低 CPU 的性能损耗。

根据 vLLM 的 V1 源码,可以看到 EngineCoreProc 也是通过监听 ZMQ 的队列进行工作的,因此 AsyncLLM 和 EngineCore 之间也实现了异步,互不干扰。
2. 简单灵活的调度程序
在请求调度上,V1 和 V0 的变化是显著的,主要体现在以下几点:
配额的分配更加灵活和精准
优先 Decode 请求
PD 混合批次
2.1 配额的分配更加灵活和精准
V0 是通过两个参数组合进行一个批次的请求拼装的,max_num_seqs 控制一个批次中最多的请求数量;max_model_len 控制一个请求的最大长度*。*
这种情况下,每个批次的大小是不均匀的,例如:
同样是 4 个 seq,最大长度同样是 6,但是实际一个 batch 中计算的 token 数量不同。

V0 就好比框出了一个矩阵的长和宽,但是不能保证这个矩阵的每一个位置都是坐满的。

而 V1 不再按照请求的维度进行控制,而是直接使用 token 的颗粒度,根据每一个请求在进行计算时所需的 token 额度进行分配。token 所需的额度包括用户输入的 promt 以及模型输出的 token,以及准备要输出的 token.
调度的决策存储在一个简单的字典中,也就是 Scheduler Output,它记录了每一个请求所需的 token 数量。
只要 batch 还有 token 可用,那么调度器会持续的进行填充,直到所有的位置都占满。这种对于 GPU 的利用率是有更好的提升。
2.2 调度的优先级策略改为了 Decode 优先
V0 的默认调度策略是优先处理 prefill 请求,且不会将 prefill 请求和 decode 请求混合在一个批次中。这种策略优化了首次 token 的响应时间,并且在当前并发数量超过 max_num_batched_tokens 之前,prefill 请求都可以在每一个批次中同步进行,如果剩下的 token 位置不足以放下整个 seq,那么 vLLM 会通过 chunked prefill 进行分块处理。
理论上来说,这种调度策略能够最大化的利用 GPU,每一个批次都会计算更多的 token。整个吞吐会随着并发数的增加而增加,直到并发数超过 max_num_batched_tokens,prefill 出现了等待,吞吐数据就会出现下降。
当然实际来说,不一定会邻近 max_num_batched_tokens 时才会出现吞吐瓶颈,在 prefill 出现大量堆积时,应该就已经出现了瓶颈,具体数据需要进行实测。
2.3 Prefill 和 Decode 可以混合批次
博客并没有提到这一点,但是通过源码可以知道 V0 在进行调度的时候,一个批次会优先调度 prefill,并且_schedule_default 默认的调度逻辑中,有明确的约束,只有 prefill 序列是空的时候,才会调度 decode。
只有开启了 chunked prefill 时,走_schedule_chunked_prefill 调度逻辑时,才会允许 decode requests 和 chunked prefill requests 混合在一个 batch 中进行调度。
而 V1 不再区分具体是什么请求,只按照他们的 token 占用情况进行调度,因此批次中不再限制 prefill 和 decode.
3. 零开销 Prefix Caching

从博客举出的测试结果来看,V1 首先优化了 Prefix Caching 的 CPU 开销,即时在命中率为 0%的时候,也没有因为多出来的 CPU 开销而导致吞吐降低,所以 Prefix Caching 得开销几乎为零。那么默认就可以启用 Prefix Caching.
4. 简洁的张量并行推理架构

上图比较清晰的说明了 V0 和 V1 的架构变化,V0 的架构中 Scheduler 和 Worker0 处于同一个进程中,导致 Worker 的功能不纯粹,并且 Worker0 和其他的 Worker 存在差异。
V1 将他们分开,Scheduler 和 Worker 分属不同的进程,从架构上来说,更加解耦合,结构也更加清晰。
V0 之所以将 Scheduler 和 worker0 放在一个进程中,是为了减少 Scheduler 和 Worker 之间的通信,V1 通过技术优化解决了这个问题,让 Scheduler 和 Worker 分开,同时不会产生更多的进程间通信。
以上是博客的说法,实际来说我认为架构的重构可能是更主要的动机,进程间通信即便没有优化,也不存在很大的问题,毕竟只是 Scheduler 和 Worker0 之间的局部通信,Scheduler 和其他 Worker 之间的通信同样存在,局部优化应该不是很重要的考量因素。
5. 高效的 Input 准备工作

这同样是一个对 CPU 开销进行优化的技术点,实际上来说,当 CPU 占比较大的时候,优化 CPU 带来的性能提升更为明显,例如小模型运行在高性能加速卡上。
简单来说就是把已经构建过的 batch 持久化,后续当其中某些 request 已经完成时,vLLM 只更新增量变化的部分,已经持久化的 request 不需要重复的构建 batch 数据,可以减少这部分不必要的 CPU 计算。
6. torch.compile 和 分段 CUDA Graphs

torch.compile 是一种自动化优化工具,可以讲模型的计算图编译为高效的机器代码。而 CUDA Graphs 是 NVIDIA 提供的优化技术,能够通过预录制 GPU 的计算和内存操作序列,减少 CPU 和 GPU 之间的通信开销,从而提升推理性能。分段 CUDA Graphs 技术能够将 CUDA 图分解为更小的片段,在动态推理场景具有更好的灵活性和性能提升。
V1 集成了这两种技术,但是具体的细节,博客中并没有过多介绍。
7. 增加对多模态模型的支持
V1 将多模态支持提高到了一等公民,引入了一些重要的关键改进:
V1 将多模态的处理移动到了一个单独的非阻塞进程,以优化图像文件处理带来的阻塞和损耗。这种对图像文件进行的像素值的张量转换、裁剪、转换,都会占用大量的 CPU 时间,导致 GPU 处于空闲状态。V1 增加异步线程处理,避免预处理对 GPU 的阻塞和影响。
其次,V1 为多模态输入引入了 Prefix Caching,除了 token 的 hash 以外,还增加了图像的 hash,用于在 KV Cache 中进行缓存,这种优化能力对于包含图像输入的多轮对话有较大的性能提升。
第三点,V1 位多模态的 chunked-prefill 功能增加了“encoder cache”功能。在 V0 的版本,图像输入和文字输入必须在同一个 step 中被处理,因为文字部分的处理会依赖图像部分的视觉嵌入信息,而这部分信息在 step 处理完成后会被丢弃。如果一个请求的文字部分被 chunked prefill 进行了分块,那么在每一个 step,都需要重复的构建视觉嵌入信息。
V1 将请求的视觉嵌入暂存起来,不需要在每一个 step 重复的生成视觉嵌入信息。
8. FlashAttention 3
FlashAttention 是业界使用最多的注意力算法,FlashAttention 3 是最新的版本,其性能上有较大的提升,V1 也会考虑集成这一个版本。
以上是结合源码和博客文章的记录,后续关于调度部分,我会结合源码整理架构的更多细节变化。
系列文章:
版权声明: 本文为 InfoQ 作者【Jason黄】的原创文章。
原文链接:【http://xie.infoq.cn/article/cbb226d549a2a3d82cb19d75a】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论