Dynamo 发布以后,我大概速览了一些设计文档,并且提取了一些关键点,并对比一些其他方案的异同点。
Smart Router
worker 上有 KVPublisher 负责发送 kvcache 的创建和删除事件,同时 KvMetricsPublisher 用于发送监控指标(如队列排队长度等)。
router 上则包含 KVIndexer,用于收集 kvcache 的事件并建立前缀树,同时 KvMetricsAggregator 用于收集监控指标。
路由策略基于 KV match rate - Load
的最大值,旨在平衡负载与 kvcache 的匹配度。
KVPublisher
应该是侵入式实现,需要给vLLM打这个patch才能实现,需要修改代码才能捕获这些事件。所以光从他的依赖来看,应该是只支持了vLLM,其他的支持估计还没开源出来。
sgl-router 完全不依赖 worker 的信息,仅通过路由自身的请求实现可过期的前缀匹配。虽然这种方式的匹配精度不如直接获取信息,但实现上更为解耦。
vllm-router 则基于 vLLM 的 Prometheus 接口,通过 /metrics
获取监控指标,其前缀匹配是通过 block hash 的近似度实现的。
llumnix 支持请求的重调度功能,可以将排队中的请求重新分配。
aibrix gateway 同时支持基于树和哈希的匹配方式,并且支持用tokenizer使用 token 进行前缀匹配,而不像 sgl-router 基于字符的匹配。
从 Dynamo 的 Indexer 实现来看,其基于 block 级别的 radix tree,事件通过 Component 的 publish 机制进行分发然后触发radix tree的更新。
条件 PD 分离
并非所有请求的 prefill 阶段都需要在 prefill instance 中计算。如果 prefill 很短,或者 decode instance 的 KV 缓存命中率较高,通常在 decode instance 中直接完成 prefill 更为高效。Dynamo 的分解设计充分考虑了这些场景,并提供了一个灵活的框架,能够在多种条件下实现卓越性能。
在 Decode Instance(在 Dynamo 中称为普通的 worker)上,需要决定是否执行分离操作。如果需要PD分离,则将 prefill 请求交给 prefill worker,通过 prefill queue 进行处理。当 prefill queue 完成后,再通过 prefill queue 将结果传回 worker,开始 decode 阶段。
具体而言,只有在满足以下两个条件时,才会向远程 prefill instance 发送请求:
- 没有前缀缓存命中的 prompt 长度超过设定阈值。
- Prefill queue 的长度小于设定阈值。
这种条件化的 PD 分离设计,使得 Dynamo 能够在动态工作负载下实现高性能。
Prefill Queue
Prefill Queue 是一个基于 NATS Stream 的全局消息队列。
在这一部分中,最具挑战性的是 KV Cache 的传输。Mooncake 开源了其 TransferEngine,而 vLLM 提供了一些 KV Connector 和 KVStore 的抽象。可以推测 Dynamo 也在 vLLM 的基础上实现了相关功能,可以看到在这个patch中,给vLLM的kv connector实现了一个DynamoNixlConnector。
The key to high-performance disaggregation is efficient KV transfer. Dynamo leverages NIXL to transfer KV cache directly from the VRAM of the prefill engine to the VRAM of the decode engine.
Dynamo 的 KV Cache 传输是通过直接 RDMA(远程直接内存访问)实现的。
为了减少 Memory Descriptors(RDMA 的描述对象)的大小,Dynamo 采用了以下两种优化:
Memory Descriptors 缓存
每个 Worker(对应传统的 Decode Instance,但在 Prefill 较短时也会执行 Prefill)在初始化并分配所有 KV 缓存池后,会将所有块的 Memory Descriptors(也称为 NIXL 元数据)存储在分布式键值存储 ETCD 中。当 Prefill Worker 第一次服务来自 Worker 的远程预填充请求时,会从 ETCD 加载这些 Memory Descriptors 并缓存到该 Worker 中。因此,在发出 Prefill 请求时,只需要传递 KV 块 ID,而无需传递完整的 Memory Descriptors。这一优化的具体作用可能需要进一步分析 NIXL 的传输过程才能完全理解。
显存分配优化
Dynamo 在 Prefill 过程中提升了显存分配能力,通过分配连续的内存块并将其合并为更大的块,从而减少 KV 块的总数。这种合并的具体效果需要结合实现NIXL细节进一步评估。
此外,对于不同 KV 布局(例如由于不同的 TP 导致的 Decode 和 Prefill 布局差异),Dynamo 使用了一个高性能内核。在 NIXL 读取之后和写入之前,该内核会将 KV 块转置为 KV Receiver 中的匹配布局。这可能是为了将 KV Cache 分块传输到不同的 TP 上。
由于引入了 ETCD,Dynamo 支持动态调整 Worker 和 Prefill Worker 的数量。
和其他方案对比
Mooncake 的设计在架构上更加分离,主要通过一个调度器(scheduler)来负责 kvcache 的传输调度,并直接决定 P 和 D 之间的 P2P 传输,基于其 TransferEngine 实现了以下功能:
基于 kvcache 的前缀匹配分配 prefill 请求
如果 prefill 节点上缓存了足够的前缀(由 kvcache_balancing_threshold
控制),则选择预估 TTFT(Time to First Token)最小的实例:
TTFT = min(T_queue + T_prefill)
。
如果 prefill 节点上缓存不足,则选择:
TTFT = min(T_queue + T_prefill + T_transfer)
,
其中 T_transfer
指的是将最长匹配的 KVCache 从其他实例拷贝到当前实例的预估时间。
高频使用的 kvcache P2P 传输
Scheduler 负责 kvcache 的传输调度,例如从一个 prefill 节点传输到另一个 prefill 节点,或者从 prefill 节点传输到 decode 节点。
基于负载均衡的 decode 请求分配
通过负载均衡的方式预估 TBT(Time to Best Throughput),从而优化 decode instance 的请求分配。
Mooncake 的设计在模块划分上更加清晰,调度器(scheduler)与各个组件的职责分离明确。
相比之下,Dynamo 的入口在 worker(相当于 Mooncake 中的 decode instance),由 worker 决定是否将 prefill 请求交给 prefill instance。Dynamo 的特点包括:
- Worker 也可以执行 prefill 操作(即 decode instance 有时也会承担 prefill 的职责)。
- 引入了全局队列(queue)来处理 kvcache 的计算和计算就绪信息。
- 提供了 NIXL 传输引擎,但仅支持 P 到 D 的 kvcache 传输,相对实现更为直白。
AIBrix 的现状
AIBrix 目前尚未实现 PD 分离功能,相关文档和白皮书中未提及此功能。
依赖与工程复杂度
从 Dynamo 的依赖项来看,其使用了 ai-dynamo-vllm v0.7.2
,这是对 vLLM v0.7.2 的定制化补丁版本,需修改 vLLM 以支持 Publisher 功能。
Dynamo 的工程栈相对复杂,依赖消息队列和 ETCD,但其 PD 分离设计较为直白,例如仅支持 P 到 D 的传输。相比之下,Mooncake 的设计更注重架构分离,尽管目前未实现 offload 功能,但其 P2P kvcache pool 的设计为未来扩展提供了可能性。
关键问题
俗话说得好,关键问题是问题的关键。无论是 Mooncake 还是 Dynamo,其核心目标都是提高传输效率和 kvcache 的利用率。Dynamo 的实现更简化,而 Mooncake 则在架构设计上更具层次感。
KVCache 管理
KVCache Offload
当显存不足时,可以将 KVCache 卸载到更低级别的存储中,例如内存、磁盘,甚至对象存储。
管理器的核心在于结合驱逐策略,在以下两种情况之间取得平衡:
- 过度缓存:可能引入查找延迟。
- 缓存不足:导致查找失败和 KV 缓存的重新计算。
V1 单机版本
V1 版本支持将 KVCache 卸载到磁盘,同时使用 CPU 的内存作为缓存。在需要加载时,从磁盘读取数据回显存。
V2 分布式版本
V2 版本将扩展为分布式架构,形成一个全局的 KVCache 池。
Mooncake 的实现
Mooncake 的 KVCache Pool 完全基于显存的 P2P 传输,不涉及 offload 操作。它通过开源的 TransferEngine,将缓存节点上的 KVCache 调度到需要缓存的节点上。
AIBrix 的实现
AIBrix 提供了一个分布式 KVCache Pool,基于 Vineyard 的分布式内存存储。通过 Vineyard 实现 KVCache 的共享,但与专门的传输引擎相比,其传输效率可能稍逊一筹。
NIXL
NIXL 通过简化的同步和批处理以及简化的源和目标抽象简化了数据搬迁。
NIXL 能够在不同类型的内存和快速存储中抽象数据搬迁,而其他数据搬迁库通常只支持一层内存。
这些增强带来了显着的性能提升,加速了第一个词元的时间(TTFT)和整体吞吐量。
NIXL的地位应该是和Mooncake的TransferEngine相当的,至于两者谁的效果更好可能要具体看一下。
总结
看设计的话,感觉还是Mooncake更漂亮一点,层次分得较清楚,不额外依赖什么中间件,kvcache pool的这个设计虽然是纯P2P的,应该后面也可以去做offload之类的。
dynamo就显得更具有工程具体性,并且实现相对来说是要更简单一些,毕竟依赖了message queue又依赖了etcd,把一些复杂度转移给了中间件,入口从worker(or decode instance)可以自己直接prefill短prompt肯定也是做了很多tradeoff才给出了一个不完全分离的条件PD分离的实现。