基于CPU内存的v1 Transfer Connector
vLLM最近支持了外部加载Transfer Connector,基于LMCache给出的StorageSharedConnector的例子,我尝试实现了一个基于共享内存的Transfer Connector。
v1的接口是一个可以layer wise的实现。
实际上使用下来其实没有明确的区分Producer和Consumer的角色,在P2P的场景下可能比较明显,实际上谁生产kv cache谁消费kv cache其实没有明确规定。
这个的好处是Prefix Cache和KV Cache Transfer没有明确的区别了,Prefill和Worker之间唯一的区别就变成了max_token=1
和max_token
为真实值的区别了。
设想几个场景,Worker即是生成者,也可以是消费者,Prefill也可以即是生产者又是消费者:
- Worker 生成的对话可以存入一个中心化的缓存当中,在多轮对话的时候Prefill可以直接复用这个缓存,只需要计算新的用户对话。
- Prefill 基于新的对话可以生成一个缓存,被Worker使用,也可以被其他的Prefill在新的多轮对话中使用。
有一个比较hack的查看调用栈的方法就是在对应的接口函数抛出异常让程序崩溃,就能在stack trace上看到函数调用的路径了。
当然这个接口也可以实现P2P,毕竟他提供了对应的wait接口,无非是等中心化的缓存是否就绪还是Prefill的直接传输是否就绪区别了,这在提供的接口列表当中可以看到。
实现一个Connector需要关注几个接口。
Worker side
Layer Wise
vllm/vllm/attention/layer.py
中可以看到。
1 | def unified_attention( |
每一层会有
wait_for_kv_layer_from_connector
调用connector.wait_for_layer_load
。在计算结束以后会有
maybe_save_kv_layer_to_connector
调用connector.save_kv_layer
。
他们分别对应了decode和prefill,其中wait是同步的,save是异步的。
Model Wise
在vllm/vllm/attention/layer.py
中
1 | self.maybe_setup_kv_connector(scheduler_output) |
connector.start_load_kv
来自于maybe_setup_kv_connector
,在decoder的model forward之前调用,用于异步启动kv的load。connector.wait_for_save
来自于maybe_wait_for_kv_save
,在prefill的model forward之后调用,用于整体的save是同步的。
其他的一些接口包括:
get_finished
返回对于给定的request ids对应的已经完成的sending和recving的request ids。register_kv_caches
用于connector提前注册kvcaches,在初始化kvcache的时候调用,这个应该是NIXL需要这样直接读取整个的kvcaches的显存地址做RDMA和注册。bind_connector_metadata
,model forward之前bind metadata,这个数据结构是Metadata的数据结构是实现者自由定义的,在start_load_kv
之前调用。clear_connector_metadata
,model forward之后clear。
换成prefiller和decoder的视角来看
Prefiller Connector
每一层调用save_kv_layer
,这个可以是异步的,在model foward之后会调用wait_for_save
保证kvcache被传输完,不然其中的kvcache可能会被之后的forward所覆盖。clear_connector_metadata
可以帮助清理这次forward相关的metadata。
Decoder Connector
bind_connector_metadata
帮助设置forward相关的metadata。
每一层调用start_load_kv
,这个可以是异步的,在model forward之前调用,在每一层forward之前wait_for_layer_load
,这个是同步的。
Scheduler Side
get_num_new_matched_tokens
,基于传入的num_computed_tokens
获取可以从外部加载的kvcache,这个是给scheduler用的,表明decoder需要加载的tokens。computed_token
代表已经计算过kvcache的token.
调度器要额外分配一个external_computed_tokens
的slots给外部加载用并且把这部分也算在computed_token
,然后在根据budget_token - computed_token
分配new_token
。update_state_after_alloc
在scheduler分配slots以后更新connector内部状态,比如用于告知connector是否要加载kvcache。build_connector_metadata
用于构建connector metadata的相关输出,不能修改输入中的schedulerOutput。request_finished
在request结束,blocks free之前被调用,可以帮助connector触发相关回调。
上面的接口比如register_kv_caches
,bind_connector_metada
和clear_connector_metadata
不一定要实现,可以把他们理解为一些初始化路径,计算路径上的调用hook,我们希望在相关的hook上处理一些东西就实现这些接口。
CPUMemorySharedConnector
我实现了一个基于共享缓存的实现,Prefill只生成缓存,Worker只消费缓存。
主要是通过用layer和tokens hash做key创建SharedMemory。
完整的项目在这里。