host hit、load-back 与前缀分层复用#
这章解决什么问题#
前面的调度与内存章节已经把 prefix match、lock ref、cache_protected_len 讲出来了,但还有一层对真实缓存命中体验非常关键的机制没有单独讲清:为什么有些前缀虽然“命中了”,系统仍然要再做一次 load-back 或数据搬运?为什么 prefix_indices 里有时会同时混着 device 命中和 host/storage 回灌的部分?
这层问题集中落在几个线索上:
host_hit_lengthprefix_indices的 device/host 拼接语义- load-back 路径
- 分层缓存或 host-backed cache 的前缀复用
如果不把这一层讲清楚,读者很容易把“prefix hit”误读成一个二元状态:命中就是快,未命中就是慢。但源码实际上表达的是更细的一层现实:
- 前缀可以部分命中 device
- 部分命中 host / storage
- 还可能需要先 load-back,才能真正继续执行
为什么这层值得单独成章#
因为它决定了“缓存命中了但仍然不够快”这类问题该怎么解释。
对现实 inference runtime 来说,最难的常常不是完全 miss,而是这种半命中状态:
- 逻辑上看已经有 prefix
- 但物理上不全在当前 device-ready 的位置
- 因而还需要额外恢复、搬运或等待
这正是优秀技术书应该主动讲透的地方:不是只说“有层级缓存”,而是要说“层级缓存如何改变 prefix reuse 的真实含义”。
一张图:prefix 命中不只是一层,而是可能跨 device / host 分层成立#
这张图解决的理解障碍是:很多读者会把前缀复用想成“树里命中就能直接 forward”,但实际缓存状态可能分布在不同层上。
flowchart LR
Req["new request"] --> Match["match_prefix(...)"]
Match --> Dev["device-resident prefix"]
Match --> Host["host/storage-resident prefix"]
Host --> Load["load-back / restore"]
Dev --> Merge["prefix_indices"]
Load --> Merge
Merge --> Run["extend / prefill continues"]图比纯文字多解释的一点是:prefix 命中不一定直接等于“可立即执行的全量 device 前缀”,它可能是多层状态拼接之后的结果。
host_hit_length 为什么是这层最重要的提示量#
schedule_batch.py 里请求状态会同时带着:
prefix_indiceshost_hit_length
这说明系统明确区分:
- 当前请求总共已经拿到了多少前缀索引
- 其中有多少是来自 host / storage 层命中并被回灌的
从教学角度看,这个字段非常重要,因为它直接告诉你:
prefix_indices不是单纯“树匹配的 device 结果”- 它可能已经混入了回灌后的前缀部分
换句话说,前缀命中结果本身就可能是分层拼接后的视图,而不是原生 device cache 的纯快照。
为什么 prefix_indices 不能再被当成“纯 device 命中结果”#
在 schedule_batch.py 的注释里,上游已经明确提醒:
- 到某些时刻,
prefix_indices可能已经扩展过 host data - 此时
len(prefix_indices)代表的是 device original + host loaded 的总和
这非常值得技术书主动拿出来讲。因为一旦进入层级缓存或 load-back 场景,prefix_indices 的语义就变了:
- 它不再只是“当前树缓存原生命中的那一段”
- 而是“请求接下来可以当作 prefix 使用的总视图”
这是一个典型的 runtime 语义升级点。
load-back 为什么不该被视为“缓存失效”#
从工程视角看,很多人一看到需要 load-back,就会下意识认为缓存失效了。更准确的说法应该是:
- 逻辑前缀复用仍然成立
- 只是物理前缀并不全在当前最便宜的层里
这和“完全 miss”有本质区别。因为系统并不需要重新理解这段前缀,只需要把它从 host/storage 层恢复到 device-ready 状态。
从整本书的逻辑来看,这意味着 prefix reuse 至少应分成三种状态:
- 完全 device 命中
- 分层命中,需要 load-back
- 完全 miss
如果不做这个区分,读者就很难解释很多“命中率看起来不低,但延迟还是上来了”的现象。
ScheduleBatch 在这里承担了什么角色#
ScheduleBatch 不是只负责把请求拼成一批,它还要把:
- prefix 命中
- host hit 长度
- 当前 extend 输入长度
- 可能的 load-back 代价
一起折进 batch 的真实可执行形态里。
这就是为什么这章属于“调度与内存”,而不是单独放在层级缓存章节里。因为 load-back 不是纯缓存子系统问题,它会直接改变:
- 当前 batch 的有效前缀长度
- 需要真正 forward 的剩余 token
- 甚至请求是否值得继续被接纳
cache_unfinished_req(...) 和层级前缀视图怎样接起来#
在上一章里,我们已经解释了:
cache_unfinished_req(...)会把一部分 prefix 提前写回树- 并更新
cache_protected_len
这一章要补充的是:一旦 host-backed prefix 也被拼进来,prefix_indices 的“已可用视图”可能会比“树中原生 device 节点”更长。于是:
cache_protected_len仍然表示树已正式保护的边界prefix_indices则可能表示“device + host load-back 后的可用前缀视图”
这进一步说明:
- 保护边界
- 可用前缀视图
- 分层命中来源
是三件相关但不同的事。
这层最容易出现的误判#
1. 以为 prefix 命中就是纯 device hit#
在层级缓存路径里,这常常不成立。
2. 以为 load-back 等于缓存完全失效#
它更准确地表示“逻辑命中仍成立,但物理位置需要恢复”。
3. 以为 len(prefix_indices) 就能说明 cache 命中的真实质量#
你还需要结合 host_hit_length 看它到底有多少是回灌出来的。
如果 prefix reuse 看起来“命中了但还是慢”,先怎么查#
建议按这个顺序:
- 看
host_hit_length是否不为零 - 看
prefix_indices当前长度与真正 device original 的差异 - 看 load-back 是否频繁发生,导致 latency 被搬运成本放大
- 再看 allocator / eviction 是否只是加剧了这种分层命中状态
这样查能避免一种很常见的误判:看到 prefix hit rate 不错,就排除缓存问题;但问题恰恰可能出在“命中主要来自 host 回灌,而不是纯 device reuse”。
工程收益与代价#
收益:
- 逻辑前缀不必因为 device miss 就完全失效
- 系统可以把多层缓存状态统一折成请求的可用前缀视图
- 对长会话或热门上下文更友好
代价:
- “命中”这个词本身变复杂了
- 前缀长度、受保护长度、host hit 长度必须分开看
- 排障时不再能只看一个 cache hit rate 或一个长度字段
小结#
这一章真正要补齐的,是 prefix reuse 这条主线里最容易被忽略的现实层:
- prefix 命中可能是分层成立的,而不是纯 device 命中
host_hit_length说明有多少前缀来自回灌prefix_indices表示当前可用视图,不再只等于树的原生命中结果
到这里,调度与内存部分对“缓存命中为什么有时仍然不够快”的解释,就真正落到了运行时语义上,而不是只停留在抽象命中率层面。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。