仅用于站内搜索,没有排版格式,具体信息请跳转上方微信公众号内链接
大家好,好久不见,最近我在细看vllmv1和sglang的代码,所以接下来会写一系列的文章来介绍它们。
在本文开始前,插一个题外话,简单解释下今年我很久不更新的原因:我在xhs等平台上,大量发现盗取我的文章进行售卖的行为,这点让我非常愤怒且无能为力,也确实打击到了我写blog的热情。一番考虑后,我想让自己更轻松一些,同时也花更多时间在写代码上,所以我放慢了更新频率(但不会因此改变更新的质量)。如果大家在别的平台上也看见这种盗取售卖的行为,欢迎和我说,我要去对线!!
现在继续回到正文内容,vllmv1的系列文章基于的代码版本是vllm0.8.2(当前已更新到0.8.4),在代码细节上,不同版本间可能有diff,但在大框架上是不变的。本文将对vllmv1的整体运作流程进行介绍,在后面的系列中,再来看关于调度器、KVcache管理、分布式推理等更多细节。对于首次了解vllm的朋友,推荐先阅读以下2篇文章:
vllm原理篇
vllmv0整体运作流程
Offlinebatching下的整体流程如下图所示,在后文中,我们称请求为req:
我们先忽略图中七七八八的细节,从大面上来看一下这个流程:
首先,观察到我们采用2个不同的进程(process0,process1)来完成整个推理。
vllm使用ZMQ来负责不同进程间的数据收发(不了解ZMQ的朋友,到此为止只要先了解它的主要功能是这个即可,后续再慢慢了解它的其余细节)
观察到:
也就是说,vllmv1将cpu和gpu上的调度拆成2个进程进行处理,这样可以更好实现cpu和gpu的overlap。举例来说,在先前vllm版本的推理中,我们经常会发现detokenize(tokenid->文本)这个步骤会成为推理瓶颈,特别是遇到长序列的情况。在等待序列做detokenize的过程中,gpu是空转的。现在通过这种拆解,让原来是串行的事情并行化。
最后,客户端的类型有很多种,由于本节我们讨论的是offlinebatching,所以展示的是SyncMPClient。在下一节中,我们会来看其余类型的客户端。
有了以上大体上的感知,现在我们可以来介绍图中细节了:
1、addreqto:LLM端接收req,并将req发送至LLMEngine的processor。
2、processreq:原始req->EngineCoreRequest,processor的作用是对req进行预处理,具体包括:
Tokenize
验证输入参数的合法性
将req封装成特定形式(EngineCoreRequest)等等
3、encode&sendreq:
将EngineCoreRequest发送给SyncMPClient
SyncMPClient会对其做encode操作:EngineCoreRequest->pickleEngineCoreRequest
———————–进程分割线(Client->EngineCoreProc)————————–
4、recv&decodereq:
在EngineCoreProc所在的进程上,会启动一个线程,在该线程中执行process_input_socket()函数,这个函数具体做了如下事情:
持续监听通过input_socket传来的pickleEngineCoreRequest
decode:(pickleEngineCoreRequest->EngineCoreRequest)
将EngineCoreRequest装入input_queue队列中
5、addreqtoscheduler:EngineCoreRequest被添加进Scheduler的waiting队列中
6、onestepinference
Scheduler执行一步调度,从waiting和running队列中选择req进行推理,输出一步推理后的结果(EngineCoreOutputs)。这边的细节我们暂时略过,留到后文细说。
8、encode&sendreq:
在EngineCoreProc所在的进程上,会启动一个线程,在该线程中执行process_output_socket()函数,这个函数具体做了如下事情:
持续监听output_queue中装入的一步推理输出结果(EngineCoreOutputs)
encode:EngineCoreOutputs->pickleEngineCoreOutputs
使用zmqoutput_socket作为通信管道,将pickleEngineCoreOutputs发送给SyncMPClient
9、recv&decodereq
在SyncMPClient所在进程上,会启动一个线程,在该线程中执行process_output_socket()函数,这个函数具体做了如下事情:
持续监听通过output_socket传递来的pickleEngineCoreOutputs
decode:pickleEngineCoreOutputs->EngineCoreOutputs
将EngineCoreOutputs装入output_queue中
10、process_output,对EngineCoreOutputs做一些后置处理,例如detokenize等操作。
这里我假设起的onlineserving是这份脚本:
我们还是先从整体上来感知onlineserving的运作流程:
和offlinebacthing相比,EngineCoreProc部分的逻辑没有变
Client和EngineCoreProc交互的逻辑没有变
所以现在,我们只需要关注Client的实现细节
OnlineServing的Client实现细节如下:
1、asyncperreqgenerate:用户发送req给vllm的APIserver,server对单req发起generate操作。这里会首先将req发送给AsyncLLM
2、createasynico.queue()foreachreq和registerthequeuetooutput_processor:
这两个步骤都在AsyncLLM上执行
createasynico.queue()foreachreq:会针对每一个req创建一个异步队列,这个异步队列将用于存储这个req的输出结果。
registerthequeuetooutput_processor:会把这个req的异步队列注册到output_processor中。
为什么每个req需要一个单独的队列:
在onlineserving中,req的输出结果是流式的(一块一块地把结果返回给用户)
在onlineserving中,用户时常有多轮对话的需求等等
基于这样的考虑,相比于offlinebatching对所有req的一次性统一输出,onlineserving下更有必要将各个req的输出隔离开来,因此才单独针对每个req创建一个用于盛放输出结果的异步队列。
3、prcocessreq和4、encode&sendreq和offlinebatching逻辑基本一致,这里不再赘述
EngineCorePro会实际执行req的推理过程,5~9的步骤我们在offlinebatching中详细解释过,这里不再赘述
10、AsyncMPClient上发起get_output_async()
:
持续监听通过output_socket传递来的pickleEngineCoreOutputs
decode:pickleEngineCoreOutputs->EngineCoreOutputs
将EngineCoreOutputs装入output_queue中
11、splitoutputintoslices:
将output_queue中的数据切成slices,方便后续做流式输出
将slices数据装入这个req注册在output_processor中、只属于这个req的异步队列
output_processor将会以slices为维度,对输出数据做诸如detokenize等的操作。
12、continouslyyieldcurrentoutput:
对于一条请求,AsyncLLM将会持续从output_processor中获取它当前的输出结果,返回给用户,直到这条请求推理完毕
到此为止,我们就把V1的大致运作流程介绍完了,在后面的文章中,我们会来看关于调度器、KVCache管理和分布式模型执行的更多细节。
进技术交流群请添加AINLP小助手微信(id:ainlp2)
请备注具体方向+所用到的相关技术点
关于AINLP
AINLP是一个有趣有AI的自然语言处理社区,专注于AI、NLP、机器学习、深度学习、推荐算法等相关技术的分享,主题包括LLM、预训练模型、自动生成、文本摘要、智能问答、聊天机器人、机器翻译、知识图谱、推荐系统、计算广告、招聘信息、求职经验分享等,欢迎关注!加技术交流群请添加AINLP小助手微信(id:ainlp2),备注工作/研究方向+加群目的。