链载Ai

标题: vllm近期更新的一些trick总结 [打印本页]

作者: 链载Ai    时间: 4 小时前
标题: vllm近期更新的一些trick总结

引言

本文是想总结近期vllm更新的一些trick,文章主要从三个方面来介绍该内容,首先是vllm发布重大版本v1的一些性能优化点和对比数据,看起来效果是卓越的,然后是买家秀,从用户的实际体验上看,效果似乎一言难尽,最后是vllm的扩展,不再局限于单机能力,vllm对集群式的推理架构又有了新的方向。

vllm v1介绍

在今年的1月27日,vLLM团队宣布了vLLM V1的alpha版本发布,这是对其核心架构的一次重大升级。基于过去一年半的开发经验,团队重新审视了关键设计决策,整合了多项功能,并简化了代码库,以提升灵活性和可扩展性,同时也是因为这段时间内在适配各种模型而形成的技术债务积累,到不得不新开一个版本,为了提供一个简单、模块化且易于修改的代码库,对原有架构做了非常多的重构设计。

unsetunsetCPU overhead 优化unsetunset

在vllm官方《vLLM v0.6.0: 2.7x Throughput Improvement and 5x Latency Reduction》的blog中,就介绍了vllm 6.0对于性能增强的核心内容是将 API 服务器和推理引擎分离到不同的进程:

通过将http 服务组件与 vLLM 引擎分离,并使用 ZMQ socket将它们连接起来。这种架构确保两个 CPU 密集型组件彼此隔离,同时通过一次批处理多个调度步骤,我们让 GPU 比以前更忙碌,从而减少延迟并提高吞吐量:

而在《vLLM V1: A Major Upgrade to vLLM's Core Architecture》一文中,针对之前拆解两个进程后的引擎中处理请求的方式以及与 http 请求交互的方式方面调度与细化上又做了全面的更新,如下图所示:

vLLM V1 通过将多处理架构更深入地集成到 AsyncLLM 的核心中来扩展此功能,从而创建一个EngineCore专注于调度程序和模型执行器的隔离执行循环。这种设计允许 CPU 密集型任务(例如tokenization, multimodal input processing, de-tokenization, and request streaming)与核心执行循环有更大的重叠,从而最大限度地提高模型吞吐量。

上图中,从API server下来后的asyncllminput processing,到process 1后的engineCoreForward,执行流程为:

  1. 进程0主要负责前后处理和流式传输,通过input_socket将请求发送给进程1。
  2. 进程1在接收到请求后将其放入输入队列中。
  3. engine_core的主循环会不断从输入队列中取出请求,处理完成后将其放入输出队列中。
  4. 进程1再从输出队列中取结果通过output_socket将其发送回进程0,完成后处理后即可返回用户。

这使得该过程变为:

这里我比较感兴趣的点在于如何使用的进程通信,我之前有整理过关于python进程通信方式总结(三):共享内存的方案,而vllm的trick更加让我眼前一亮,它将zmq与shared memory融合,基于以下规则,创造了一种新的机制,如下所示:

    Buffer memory layout:
data metadata
| |
| (current_idx) | (current_idx)
v v
+-------------------------------+----------------------------------------+
| chunk0 | chunk1 | ... | chunk | metadata0 | metadata1 | ... | metadata |
+-------------------------------+----------------------------------------+
| max_chunks x max_chunk_bytes | max_chunks x (1+ n_reader) bytes |

metadata memory layout: each byteisa flag, the first byteisthe written
flag,andthe rest are reader flags. The flags are set to0by default.
+--------------+--------------+--------------+-----+--------------+
| written_flag | reader0_flag | reader1_flag | ... | readerN_flag |
+--------------+--------------+--------------+-----+--------------+

The state of metadataisasfollows:

(case1)0???...???: the blockisnotwritten yet, cannot read, can write
(case2)1000...000: the blockisjust written, can read, cannot write
(case3)1???...???: the blockiswrittenandread by some readers, can readifnotread, cannot write
(case4)1111...111: the blockiswrittenandread by all readers, cannot read, can write

State transitionforreaders:

When a reader finds a block that it can read (case2or3), it canyieldthe blockforcaller to read.
Only after the caller finishes reading the block, the reader can mark the blockasread.
Readers only mark the blockasread (from0to1), the writer marks the blockasready to read (from1to0).

State transitionforwriter:

When the writer writes to a block (case1or4), it first resets the written flag to0, converting either case
to case1.Then it canyieldthe blockforcaller to write. After the caller finishes writing the block, the writer
can reset the reader flags to0,andmark the blockaswritten (from0to1).

在这种机制下,数据发送(enqueue)为:

defenqueue(self, obj, timeout: Optional[float] = None):
serialized_obj = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)

# 本地读取器处理
ifself.n_local_reader >0:
# 判断数据大小决定使用哪种通信方式
iflen(serialized_obj) >= self.buffer.max_chunk_bytes:
# 大数据:标记溢出并使用ZMQ发送
withself.acquire_write(timeout)asbuf:
buf[0] =1# 溢出标记
self.local_socket.send(serialized_obj)
else:
# 小数据:直接使用共享内存
withself.acquire_write(timeout)asbuf:
buf[0] =0# 非溢出
buf[1:len(serialized_obj) +1] = serialized_obj

# 远程读取器只能使用ZMQ
ifself.n_remote_reader >0:
self.remote_socket.send(serialized_obj)

数据接收(dequeue)为:

defdequeue(self, timeout: Optional[float] = None):
ifself._is_local_reader:
# 本地读取器先从共享内存读取
withself.acquire_read(timeout)asbuf:
overflow = buf[0] ==1
ifnotoverflow:
# 如果数据在共享内存中,直接读取
obj = pickle.loads(buf[1:])

# 如果数据溢出,则从ZMQ读取
ifoverflow:
recv = self.local_socket.recv()
obj = pickle.loads(recv)

elifself._is_remote_reader:
# 远程读取器只能从ZMQ读取
recv = self.remote_socket.recv()
obj = pickle.loads(recv)

returnobj

该方案基于的策略可以总结成:

  1. 数据大小判断 :






欢迎光临 链载Ai (https://www.lianzai.com/) Powered by Discuz! X3.5