New:KVCache Manager
由于在decode阶段引入layerwise的kvcache load/store,显然会严重损害decode侧的吞吐,因而,无法采用上面的在HBM中只驻留部分层的策略,来期望维持bsz同时最大化吞吐;
分析目前引入priority-based scheduler后的问题
KVCache trashing
- Rollout engine对实际驻留在HBM中的非活跃KVCache无感知,当新请求到达时,由于不存在充足的可用kvcache,需要evict HBM中的KVCache,此时应该对KVCache有状态设置,high_ref与low_ref,high_ref>0代表高优Token cache,low_ref>0代表低优token cache,high_ref=0 && low_ref==0代表可被直接换出cache;在需要evict请求时,优先evict无用cache,否则evict低优先级cache,仅当请求为高优先级请求时才允许将高优先cache也evict到HBM中,evict时,采用按需evict策略,从radix tree的叶子结点往上evict,一旦到达所需cache page,则立刻停止evict;
- 若将所有可用的kvcache都evict到DRAM中,仍然无法满足该请求的所需的kvcache,则考虑,高优先级请求停止某个低优先级请求,直到满足存在可用kvcache;
- 若仍然还未满足,则观察目前已partial page存放的kvcache,通过离线profiling结果,判断当前请求改为partial page后,load/store开销小于TPOT,则作为partial page存放在HBM中,将其加入bsz中;
Code Walkthrough
本节详解新增的RefAwareHiRadixCache的具体代码流程,以及具体实现的新策略;
HiRadixCache
首先详解下HiRadixCache的具体思路;
HiRadixCache中,在初始化时,首先根据kvcache的类型,进行host DRAM kvcache的初始化,目前仅支持MHATokenToKVPool,NSATokenToKVPool以及MLATokenToKVPool三类,赋值到self.token_to_kv_pool_host中;此外,初始化self.cache_controller,正常情况,设置为HiCacheController;此外,会额外创建多个set来记录目前正在ongoing的不同操作,来避免evict时错误evict正在进行操作的TreeNode;设置write_through_threshold,当write_policy为”write_through”时为1,不然为2,并创建self.evictable_host_leaves来记录可被evict的node set;
# record the nodes with ongoing write through
self.ongoing_write_through = {}
# record the node segments with ongoing load back
self.ongoing_load_back = {}
# record the ongoing prefetch requests
self.ongoing_prefetch = {}
self.ongoing_backup = {}其中,核心提供如下方法:
写回 Host / Storage(GPU → CPU → L3)
- write_backup (line 653):把一个 TreeNode 的 device value 复制到 host。约束:write-through 模式下必须父节点已 backup(保证连续前缀)。host
内存不足时先 evict_host。成功后保存 host_value、加入 ongoing_write_through,并 inc_lock_ref 防止 GPU 端被驱逐。 - write_backup_storage (line 688):进一步把 host 上的 KV 写到 L3 storage,记录 ongoing_backup 并 protect_host。
- _inc_hit_count (line 705):write-through-selective 策略下,节点命中次数达到阈值才触发 write_backup。
- writing_check (line 716):轮询 ack_write_queue,确认异步 DMA 完成后从 ongoing_write_through 摘除节点,发出 store(CPU) event;若启用 storage
则继续触发 write_backup_storage。多 TP 用 all_reduce(MIN) 同步进度。
读取与回填(CPU → GPU) - match_prefix (line 1222):先在 radix 树上匹配出 device 命中段;遇到 evicted 节点继续向上追溯,统计 host_hit_length,并定位最近的
last_host_node(已 backup 的最深祖先)。返回 device/host 双层命中信息。 - load_back (line 952):把命中的 host 节点链 KV 拷回 GPU。先 inc_lock_ref 保护,再申请 device 内存(不足则触发 evict);过小(<
load_back_threshold)或超过 mem_quota 则放弃。成功后 split 回写到各节点的 value,记录 ongoing_load_back。 - init_load_back (line 1024):调度入口,如果失败则向上回退到未 evict 的祖先。
- loading_check (line 765):处理 ack_load_queue,DMA 完成后释放 lock。
驱逐(Eviction) - evict (line 844):基于 eviction_strategy.get_priority 的最小堆驱逐 GPU 端叶子。未 backup 节点:write-back 模式先调用 write_backup(write_back=True) 异步刷盘再驱逐;其他模式直接 _evict_regular(释放 device内存并删除节点)。已 backup 节点:_evict_backuped 只释放 device,保留 host_value(节点变 evicted)。
- evict_host (line 917):在 evictable_host_leaves 上驱逐 host KV,要求节点已 evicted 且 host_ref_counter == 0,然后从父节点摘除并维护 leaf状态。
- _update_host_leaf_status (line 829):维护”哪些 host 节点是叶子(无 evicted child)“的集合,用于 host 驱逐候选。
锁引用与 Insert - inc_lock_ref / dec_lock_ref (line 791/807):从节点向 root 累加引用,更新 evictable_size_ 与 protected_size_,同步刷新 leaf / host-leaf 集合。
- insert (line 1405):在 radix 树插入新 KV。处理三种情况:完整匹配(含 evicted 节点的 KV重算回填)、部分匹配(_split_node)、新增叶子。需要时计算 hash_value 用于 storage 寻址,并触发 store event 与 hit count 更新。
- _split_node (line 1377):标准 radix split,注意同时切分 value、host_value、hash_value,保留 lock_ref。
L3 Prefetch(Storage → Host) - prefetch_from_storage (line 1258):发起异步预取。先做 page 对齐与阈值/限速判断,protect_host 防祖先被驱逐,分配 host 内存(不足则evict_host),调用 cache_controller.prefetch 启动后台 op,登记到 ongoing_prefetch。
- check_prefetch_progress (line 1142):每个调度 tick 检查 prefetch 是否能终止(受 prefetch_stop_policy:best_effort / wait_complete / timeout,并跨 TP all_reduce 同步);终止后取已完成 token 数(取所有 TP 的 MIN),通过 _insert_helper_host 把新增的 host 段插入 radix树,释放未使用部分 host 内存,记录”实际从 storage 加载的 token 数”用于 metrics 与 L3 命中统计。
- can_terminate_prefetch / _prefetch_timeout_check_linear_func (line 1103/1095):判断停止条件,超时阈值随 page 数线性增长。
- terminate_prefetch / release_aborted_request (line 1205/1489):主动终止或清理已 abort 请求的 prefetch 状态。
- _insert_helper_host (line 1308):仅插入 host 层(不带 device value),用于 prefetch 落地。
事件循环入口 - check_hicache_events (line 1056):调度器每个 step 调用,依次执行 writing_check、loading_check、drain_storage_control_queues(合并 prefetch revoke / backup ack / host mem release 三类信号,且只做一次 TP all_reduce 来减少同步开销)以及 storage metrics 上报。
RefAwareHiRadixCache
首先详解RefAwareHiRadixCache的具体class类;
首先建立class类RefInfo,代表某个请求 (rid)具体绑定的token kvcache的相关信息,包含代表该请求的优先级,以及其reference的TreeNode set;
@dataclass
class RefInfo:
is_high: bool
nodes: Set[TreeNode] = field(default_factory=set)并分三tier为每个token的kvcache设定优先级,分别是,如果没有高低优先级的请求在使用的token,则优先级为TIER_UNUSED=0,如果被某个inflight的低优先级请求使用,则为TIER_LOW_REF=1,如果有某个inflight的高优先级请求使用,则为TIER_HIGH_REF=2,通过这样的优先级划分,能够让我们更好的在需要evict node kvcache时做决策;
# Eviction tier constants
TIER_UNUSED = 0 # high_ref == 0, low_ref == 0
TIER_LOW_REF = 1 # high_ref == 0, low_ref > 0
TIER_HIGH_REF = 2 # high_ref > 0再来看RefAwareHiRadixCache的具体实现,其中,直接继承自HiRadixCache,提供如HiRadixCache一样的方法, 核心有如下的一些差别:
class RefAwareHiRadixCache(HiRadixCache):
def __init__(self, params: CacheInitParams, server_args: ServerArgs):
self.high_priority_threshold = server_args.high_priority_threshold
self.unused_evictable_leaves: set = set()
self.low_ref_evictable_leaves: set = set()
self.high_ref_evictable_leaves: set = set()
self.unused_evictable_size_: int = 0
self.low_ref_evictable_size_: int = 0
self.high_ref_evictable_size_: int = 0
self.rid_to_ref_info: Dict[str, RefInfo] = {}
self._evict_scope_stack: list[tuple[bool, bool]] = []
super().__init__(params=params, server_args=server_args)核心记录将可被evict的leaves分为三类,未被使用的/被低优先使用的/被高优先使用的;
Benchmark
Stress test
staleness=0.5
baseline
64
High-priority clients: 64/96
Duration: 295.08s
Active decode wall time: 0.00s
High-priority turns completed: 800
High-priority completion tokens: 204800
High-priority out tok (wall): 694 tok/s
High-priority out tok (decode): 146085770 tok/s
High-priority engine decode: N/A (server did not return decode_throughput)
High-priority cache hit: 0.9087
High-priority turn>=1 cache hit: 0.9582
High-priority TTFT mean/p90/p99: 16.770 / 35.162 / 44.818s
High-priority Lat mean/p90/p99: 16.770 / 35.162 / 44.818s
96
High-priority clients: 96/144
Duration: 451.30s
High-priority makespan: 451.30s
Active decode wall time: 0.00s
High-priority turns completed: 1245
High-priority completion tokens: 318720
High-priority out tok (wall): 706 tok/s
High-priority out tok (decode): 148645512 tok/s
High-priority engine decode: N/A (server did not return decode_throughput)
High-priority cache hit: 0.9117
High-priority turn>=1 cache hit: 0.9588
High-priority TTFT mean/p90/p99: 25.939 / 46.898 / 74.079s
High-priority Lat mean/p90/p99: 25.939 / 46.898 / 74.079s
High-priority turn0 TTFT p90/p99:74.080 / 74.092s
High-priority turn>=1 TTFT p90/p99:28.007 / 47.157s
128
High-priority clients: 128/192
Duration: 1311.54s
High-priority makespan: 1311.54s
Active decode wall time: 0.00s
High-priority turns completed: 1638
High-priority completion tokens: 419328
High-priority out tok (wall): 320 tok/s
High-priority out tok (decode): 149214944 tok/s
High-priority engine decode: N/A (server did not return decode_throughput)
High-priority cache hit: 0.6231
High-priority turn>=1 cache hit: 0.6558
High-priority TTFT mean/p90/p99: 82.310 / 172.383 / 196.250s
High-priority Lat mean/p90/p99: 82.310 / 172.383 / 196.250s
High-priority turn0 TTFT p90/p99:135.726 / 135.790s
High-priority turn>=1 TTFT p90/p99:174.618 / 196.371s
High-priority per-turn breakdown:
Turn 0: n= 128, avg_out= 256.0, cache_hit=0.0000, ttft=85.744s
Turn 1: n= 128, avg_out= 256.0, cache_hit=0.9385, ttft=64.239s
Turn 2: n= 128, avg_out= 256.0, cache_hit=0.8616, ttft=31.156s
Turn 3: n= 128, avg_out= 256.0, cache_hit=0.5506, ttft=70.023s
Turn 4: n= 128, avg_out= 256.0, cache_hit=0.3414, ttft=117.115s
Turn 5: n= 121, avg_out= 256.0, cache_hit=0.2599, ttft=142.194s
Turn 6: n= 116, avg_out= 256.0, cache_hit=0.2502, ttft=152.476s
Turn 7: n= 108, avg_out= 256.0, cache_hit=0.1774, ttft=157.121s
Turn 8: n= 100, avg_out= 256.0, cache_hit=0.3552, ttft=137.530s
Turn 9: n= 85, avg_out= 256.0, cache_hit=0.4896, ttft=113.922s
Turn 10: n= 77, avg_out= 256.0, cache_hit=0.8699, ttft=64.822s
Turn 11: n= 69, avg_out= 256.0, cache_hit=0.9652, ttft=29.412s
Turn 12: n= 62, avg_out= 256.0, cache_hit=0.9667, ttft=22.810s
Turn 13: n= 55, avg_out= 256.0, cache_hit=0.9680, ttft=20.393s
Turn 14: n= 52, avg_out= 256.0, cache_hit=0.9692, ttft=17.691s
Turn 15: n= 48, avg_out= 256.0, cache_hit=0.9704, ttft=16.785s
Turn 16: n= 37, avg_out= 256.0, cache_hit=0.9714, ttft=13.712s
Turn 17: n= 30, avg_out= 256.0, cache_hit=0.9724, ttft=10.484s
Turn 18: n= 22, avg_out= 256.0, cache_hit=0.9733, ttft=8.280s
Turn 19: n= 16, avg_out= 256.0, cache_hit=0.9742, ttft=6.282sProactive KVcache manager
64
High-priority clients: 64/96
Duration: 307.54s
High-priority makespan: 269.26s
Active decode wall time: 0.00s
High-priority turns completed: 800
High-priority completion tokens: 204800
High-priority out tok (wall): 666 tok/s
High-priority out tok (decode): 141443984 tok/s
High-priority engine decode: N/A (server did not return decode_throughput)
High-priority cache hit: 0.9087
High-priority turn>=1 cache hit: 0.9582
High-priority TTFT mean/p90/p99: 14.040 / 17.587 / 44.773s
High-priority Lat mean/p90/p99: 14.040 / 17.587 / 44.773s
High-priority turn0 TTFT p90/p99:44.775 / 44.778s
High-priority turn>=1 TTFT p90/p99:15.579 / 17.975s
96
High-priority clients: 96/144
Duration: 463.12s
High-priority makespan: 369.56s
Active decode wall time: 0.00s
High-priority turns completed: 1245
High-priority completion tokens: 318720
High-priority out tok (wall): 688 tok/s
High-priority out tok (decode): 120948938 tok/s
High-priority engine decode: N/A (server did not return decode_throughput)
High-priority cache hit: 0.9117
High-priority turn>=1 cache hit: 0.9588
High-priority TTFT mean/p90/p99: 19.138 / 25.338 / 67.107s
High-priority Lat mean/p90/p99: 19.138 / 25.338 / 67.107s
High-priority turn0 TTFT p90/p99:67.114 / 67.121s
High-priority turn>=1 TTFT p90/p99:20.059 / 26.865s
128
High-priority clients: 128/192
Duration: 606.46s
High-priority makespan: 474.72s
Active decode wall time: 0.00s
High-priority turns completed: 1638
High-priority completion tokens: 419328
High-priority out tok (wall): 691 tok/s
High-priority out tok (decode): 104660024 tok/s
High-priority engine decode: N/A (server did not return decode_throughput)
High-priority cache hit: 0.9109
High-priority turn>=1 cache hit: 0.9587
High-priority TTFT mean/p90/p99: 24.897 / 29.171 / 100.443s
High-priority Lat mean/p90/p99: 24.897 / 29.171 / 100.443s
High-priority turn0 TTFT p90/p99:100.445 / 100.459s
High-priority turn>=1 TTFT p90/p99:27.896 / 39.949s
High-priority per-turn breakdown:
Turn 0: n= 128, avg_out= 256.0, cache_hit=0.0000, ttft=78.558s
Turn 1: n= 128, avg_out= 256.0, cache_hit=0.9385, ttft=27.619s
Turn 2: n= 128, avg_out= 256.0, cache_hit=0.9428, ttft=19.447s
Turn 3: n= 128, avg_out= 256.0, cache_hit=0.9466, ttft=20.258s
Turn 4: n= 128, avg_out= 256.0, cache_hit=0.9500, ttft=20.685s
Turn 5: n= 121, avg_out= 256.0, cache_hit=0.9529, ttft=22.215s
Turn 6: n= 116, avg_out= 256.0, cache_hit=0.9555, ttft=22.912s
Turn 7: n= 108, avg_out= 256.0, cache_hit=0.9579, ttft=23.042s
Turn 8: n= 100, avg_out= 256.0, cache_hit=0.9600, ttft=21.066s
Turn 9: n= 85, avg_out= 256.0, cache_hit=0.9619, ttft=19.721s
Turn 10: n= 77, avg_out= 256.0, cache_hit=0.9636, ttft=17.383s
Turn 11: n= 69, avg_out= 256.0, cache_hit=0.9652, ttft=15.987s
Turn 12: n= 62, avg_out= 256.0, cache_hit=0.9667, ttft=14.762s
Turn 13: n= 55, avg_out= 256.0, cache_hit=0.9680, ttft=14.498s
Turn 14: n= 52, avg_out= 256.0, cache_hit=0.9692, ttft=15.205s
Turn 15: n= 48, avg_out= 256.0, cache_hit=0.9704, ttft=18.286s
Turn 16: n= 37, avg_out= 256.0, cache_hit=0.9714, ttft=22.757s
Turn 17: n= 30, avg_out= 256.0, cache_hit=0.9724, ttft=21.356s
Turn 18: n= 22, avg_out= 256.0, cache_hit=0.9733, ttft=15.367s
Turn 19: n= 16, avg_out= 256.0, cache_hit=0.9742, ttft=12.484s