ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

LMCache 多进程模式下的 Worker 存活追踪与回收(Worker Liveness Tracking and Reaping)设计解析

LMCache 多进程模式下的 Worker 存活追踪与回收(Worker Liveness Tracking and Reaping)设计解析 LMCache 多进程模式下的 Worker 存活追踪与回收Worker Liveness Tracking and Reaping设计解析【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache本指南基于 LMCache 仓库中的设计文档 docs/design/v1/multiprocess/worker_liveness.md 展开配合 lmcache/v1/multiprocess/ 服务端源码与 vLLM 多进程适配器实现完整讲解引擎 worker 异常退出时实例状态泄漏的问题根因、PING 心跳驱动的 liveness 追踪协议设计、服务端两档沉默窗口与独立 reap 线程的实现、UUID 实例 ID 生成策略、以及故障恢复与边界情形。读完本文你将掌握 LMCache 多进程模式下“worker 死而状态不清”这一问题的完整解决方案并能准确配置worker_reap_timeout_seconds与worker_registration_grace_seconds两个关键参数。1. 问题worker 死亡后泄漏的实例状态1.1 泄漏什么在多进程MP模式下每个引擎 worker 都会在 MP server 上登记一份“per-instance 状态”。当 worker 正常退出时会发送UNREGISTER_KV_CACHE清理状态但一旦 worker 以非正常方式死亡SIGKILL、被 OOM-killer 杀死、所在节点丢失UNREGISTER_KV_CACHE永远不会发出于是以下状态会在 server 上永久残留LMCache 驱动的ContextEntry一个持有 CUDA IPC 句柄的GPUCacheContextEngineDrivenContextEntryTransferStrategy配对非 GPU 传输路径的上下文与传输策略混合blend模式的 per-instance 状态例如 CB rope 缓存。设计文档docs/design/v1/multiprocess/worker_liveness.md的 Section 1明确指出没有任何机制观察 worker 的死亡在共享服务器上泄漏的 context 会不断累积直至设备内存耗尽。1.2 为什么 PID 复用会让问题更糟更糟糕的是原设计中instance_id直接使用os.getpid()。容器化 pod 会复用较小的 PID 号因此一个新 worker 可能注册到一个已死 worker 的 ID 上而注册逻辑是幂等的新 worker 会被静默绑定到那份陈旧的 context —— 结果是错误的 IPC 句柄、损坏的传输。这一根因决定了修复方案的两个方向一是换掉 PID 这个可复用的标识见第 4 节 UUID 方案二是必须引入主动的存活观察机制见第 2 节心跳方案。2. 设计总览把现成的心跳升级为存活信号2.1 核心思路server 本来就周期性地收到每个活跃 worker 的信号心跳 PING。设计方案没有引入新的探测通道而是在 PING 消息中携带 worker 的instance_id在已有的 per-instance 条目上盖时间戳last_seen运行一个周期性扫描线程把沉默超过超时阈值的条目用与客户端主动 unregister 完全相同的清理路径回收掉。2.2 心跳的既有行为保持不变心跳保留其惰性启动lazy start语义——第一个 store/retrieve 请求到来时才启动因此 warmup 阶段不产生 PING。设计文档特别强调心跳初始即为健康状态health_event 在构造时置位首个 store/retrieve 不会被 gate存活的 worker 每个间隔 ping 一次刷新 server 端的last_seen因此存活期间永远不会被 reap启动时也无需重注册从未产生过存活信号的条目落入一个更宽松的注册宽限期registration grace内判定。2.3 恢复复用既有客户端机制真正的宕机恢复路径同样复用已有机制宕机期间心跳 PING 持续失败health_event被清除当 server 恢复、下一次 PING 成功时unhealthy→healthy 的边沿触发 recover 回调回调执行重注册——如果原条目幸存例如只是短暂分区重注册走 NOOP 路径不产生任何新建。文档中的时序图可概括为engine worker adapter MP server --------------------------- -------------------------------------- | HeartbeatThread | PING [id] | ManagementModule | | (instance_id, 10s) -----------------------| ping(id) - touch_instance(id) | | lazy start on first req, | (NORMAL pool) | reaper thread (scan timeout/4) | | (starts healthy) | | - reap_stale_instances( | | unhealthy-healthy edge | | timeout, registration_grace) | | - re-register callback | REGISTER | - drop_instance_state(id) fan-out | | ------------------| | | | | register_kv_caches | STORE/RETRIEVE | v v | | (no pings until traffic) | (refresh too) | LMCacheDriven/ Blend | | | | EngineDriven (CB rope | | | | TransferModule dropped) | | | | Entry{.., last_seen, | | | | has_liveness_signal} | --------------------------- | _lock (leaf); pop - cleanup | --------------------------------------3. 协议变更PING 载荷从[]变为[int | None]3.1 载荷与调度PING 的载荷原地变更从[]变为[int | None]响应仍是bool恒为True。其中None用于标记未被追踪的探针——例如 scheduler adapter它不注册任何 KV cache因此永远不会被 reap。PING 继续在NORMAL 线程池上以 BLOCKING 方式分发。设计文档记录了曾经考虑过 SYNC 分发但被否决SYNC 运行在 MQ 主循环上一个慢的REGISTER_KV_CACHE同样是 SYNC会阻塞 PING从而让一个存活的 worker 看起来像死了一样。反之共享 NORMAL 池恰恰是期望的语义——如果线程池在心跳超时内无法应答 PINGworker 本来就应当进入降级模式这正是同一套背压信号。3.2 兼容性约束载荷变化是线上可见wire-visible的因此客户端与服务端必须同时升级。混合版本部署时每一次 PING 都会因载荷长度校验失败而失败客户端会永久处于 unhealthy 状态——这是“响亮”的失败而绝不是静默的数据损坏详见第 7 节失败模式表。4. 实例 ID 生成从 PID 换成 UUID4.1 生成方式与理由worker 适配器将os.getpid()替换为uuid.uuid4().int ((1 63) - 1)uuid4读取操作系统熵OS entropy相同种子启动的进程也不会碰撞 ((1 63) - 1)的 63 位掩码保证数值在有符号 int64范围内安全兼容 msgpack 等任何 int64 对端。该 ID 在构造时以 INFO 级别打日志便于运维人员把 reap 告警与具体 pod 关联起来。4.2 改动面很小所有携带 ID 的请求读取的都是同一个字段因此除 PING 之外的其他载荷REGISTER、STORE、RETRIEVE 等完全不需要改动。实现位置见 lmcache/integration/vllm/vllm_multi_process_adapter.py# Instance id for GPU worker. uuid4-derived (OS entropy) rather # than pid, so a restarted pod cant alias a dead worker. # Masked to 63 bits to stay signed-int64-safe for any msgpack peer. self.instance_id: int uuid.uuid4().int ((1 63) - 1)5. 服务端实现细节5.1 liveness 状态与两档沉默窗口ContextEntryLMCache 驱动与EngineDrivenContextEntry各新增两个字段last_seen: float使用time.monotonic()不受系统时钟跳变影响has_liveness_signal: bool只有 PING 会将其置位latch。该标志决定条目的沉默窗口判定档位条目状态判定窗口已经 PING 过证明在跑心跳协议reap timeout如默认120.0s从未 PING例如 lazy start 下仍在 warmup更宽松的 registration grace默认3600.0slast_seen的刷新时机包括PINGtouch_instance存在则刷新、绝不插入、registercreate 与 NOOP 两条路径都刷新、以及每一条传输路径——因此正在传输中的 worker 绝不会被 reap但流量不会置位has_liveness_signal标志只有 PING 能 latch。5.2 加锁独立线程与 MQ 线程的并发安全reap 线程独立运行因此 per-instance dict 现在会在MQ handler 线程之外被修改。每个传输模块各持有一把threading.Lock防止 reap 的 scan-and-pop 与并发的 register/unregister/transfer 竞争否则会破坏 dict 或把半删除的条目交给调用方。设计要点文档 Section 5.2在EngineDrivenTransferModule中context dict 与 strategy dict 在同一把锁下成对变更因此 reap 与 re-register 竞争时绝不会出现“新 context 没有配套 strategy”的悬空状态它是叶锁leaf lock——绝不跨 context 构造、存储调用或其他组件持锁因此任何线程都不会同时持有两把锁外部读者改用加锁访问器get_and_touch_context_entryget 并刷新 last_seen与context_entries_snapshot而不是直接触碰 dict。源码佐证在 lmcache/v1/multiprocess/modules/engine_driven_transfer.py 中_resolve_for_transfer在同一锁内同时取 entry 与 strategy 并刷新last_seen且注释明确说明“Refreshes last_seen (no latch) so an active worker is not reaped mid-transfer”。5.3 Reaper 线程ManagementModule持有ManagementModulePING 的拥有者同时拥有 reap 线程其工作方式为每reap_timeout / 4扫描一次见 lmcache/v1/multiprocess/modules/management.py 中create_periodic_thread(namelmcache-mp-worker-reaper, intervalself._reap_timeout / 4, ...)因此实例在最后一次信号后的timeout到timeout interval之间被回收每次扫描对每个目标调用reap_stale_instances(reap_timeout, registration_grace)模块锁内收集沉默时间超过其窗口的 id将其 pop 出 dict锁外执行与客户端 unregister 相同的清理并对每个实例记录 WARNING 日志——同一 id 被重复 reap 提示 timeout 设置过小被 reap 的 id 随后对每个目标调用drop_instance_state(id)BlendModule在此处丢弃该实例的 per-instance CB 状态如 rope state。设计文档特别说明Blend 不再镜像 GPU cache context该镜像上游已移除因此 reap GPU 条目现在会直接释放 contextcollectpop 与 register 的刷新共享模块锁从而串行化每一次 register-vs-reap 竞争关闭时 reap 线程在任何模块清理状态之前先 stop 并 join。5.4 公开协议与配置服务端通过一个 Protocol 覆盖两类角色liveness owner 与 state mirror见 lmcache/v1/multiprocess/engine_module.pyclass InstanceLivenessTarget(Protocol): # All methods default to a no-op; an implementer overrides only its role. def touch_instance(self, instance_id: int) - None: ... def reap_stale_instances( self, reap_timeout_s: float, registration_grace_s: float ) - list[int]: ... def tracked_instance_count(self) - int: ... def drop_instance_state(self, instance_id: int) - None: ...角色分工传输模块LMCacheDrivenTransferModule、EngineDrivenTransferModule重写 liveness 方法touch/reap/countBlendModule只重写drop_instance_state用于丢弃镜像的 CB 状态ManagementModule通过一次注入拿到全部目标列表。文档还记录了演进早先存在单独的单方法InstanceReapListener只含drop_instance_state因只有BlendModule实现它而被合并进本协议。服务端状态上报ManagementModule.report_status()在存在 liveness targets 时返回worker_liveness摘要enabled、reap_timeout_seconds、registration_grace_seconds、tracked_instances便于通过 HTTP/状态接口观测 reap 是否启用、当前跟踪多少实例。5.5 配置参数与校验配置定义于 lmcache/v1/multiprocess/config.pyMPServerConfig参数默认值约束语义worker_reap_timeout_seconds120.00表示禁用不启动 reap 线程否则 30.0已证明存活PING 过的 worker 的沉默预算worker_registration_grace_seconds3600.0 reap timeout已注册但从未 PING 的 worker 的沉默预算warmup 或首次请求前死亡校验逻辑__post_init__会抛出ValueError若reap timeout 非有限、为负、或非零却低于 30 秒下限或 grace 小于 reap timeout。源码中的校验提示还点明了调参原则reap timeout 应 3 x客户端的lmcache.mp.heartbeat_interval默认 10s这样错过几次 PING 也绝不会 reap 一个存活的 worker且 grace 若比 reap timeout 更紧反而会比崩溃的 worker 更快地 reap 掉仍在 warming 的 worker。两个参数都有对应的 CLI 标志--worker-reap-timeout-seconds、--worker-registration-grace-seconds并在服务端构造时传入ManagementModule。6. 客户端worker 适配器侧实现6.1 惰性启动与健康状态心跳保持首个 store/retrieve 才启动的惰性语义——warmup 期间无 PING这段窗口由 registration grace 覆盖构造时即置位 health_event初始健康首个 store/retrieve 不会被 gate存活的 worker 之后按lmcache.mp.heartbeat_interval默认 10s周期 ping刷新服务端last_seen因此存活期间永不 reap、启动时也无需重注册recover 回调只在真正的恢复边沿触发见 6.2server 不健康期间被丢弃的 retrieve 仍通过get_finished上报异步加载不会悬挂。适配器启动时还会做一次告警检查当3 x heartbeat_interval _SERVER_REAP_TIMEOUT_FLOOR_SECONDS30s 下限时打印 WARNING 提示服务端 timeout 必须同步调大lmcache/integration/vllm/vllm_multi_process_adapter.py 中_SERVER_REAP_TIMEOUT_FLOOR_SECONDS相关逻辑。6.2 被 reap 之后的恢复时序设计文档给出如下时间线T0 outage begins; pings time out - health_event cleared, traffic stops T0120s server: entry stale - reap pops it, frees GPUCacheContext/IPC, layout-desc refcount; blend rope state dropped via listener - leak fixed T1 connectivity back; next ping succeeds - unhealthy-healthy edge T1 recover callback re-registers (id absent - fresh context) before health_event is set; traffic resumes - exactly one context关键语义recover 回调在health_event置位之前执行HeartbeatThread.register_recover_callback明确“The callback runsbeforethe health event is set”且要求返回 bool——成功返回True才置位事件失败返回False则事件保持清除、下一次成功 PING 会再次调用回调。回调绝不应抛异常。长宕机条目被 reap 后恢复边沿的重注册会创建一个全新 context旧 id 已不在走 create 路径——恰好一个 context无泄漏短宕机分区小于 reap 窗口走 NOOP register 路径只刷新last_seen、不构建任何东西——server 永远不会要求 worker 重注册零 context 抖动。6.3 关闭顺序杜绝“幽灵 context”shutdown()在发送UNREGISTER之前先停止心跳线程确保不会有迟到 PING 落在一个正在关闭的客户端上心跳循环一旦观察到 stop 请求就跳过 recover 回调与health_event.set()recover 回调在已请求 stop 时也跳过重注册——迟到的循环绝不可能重建一个幽灵 context。7. 失败模式全表设计文档用一张表系统覆盖了各场景的行为以下为完整继承并补充说明场景行为worker 崩溃SIGKILL无 UNREGISTER已服务过PING 停止约timeout timeout/4内被 reap。Context、IPC 句柄、layout-desc 引用计数与非 GPU strategy 通过与干净 unregister 相同的清理路径释放blend rope 状态经 reap listener 丢弃。这正是本设计要修复的 bug。warmup 期间崩溃已注册、从未 PING在 registration grace 上被 reap。泄漏有界而非永久。worker 存活但从未 PING空闲超过 grace只有它从未 PING心跳从未启动才可能被 reap一旦心跳运行PING 每间隔刷新last_seen无论流量多少存活的 worker 都不会被 reap。心跳线程被饿死、worker 正在传输store/retrieve/prepare/commit 都会刷新last_seen不会被 reap。分区短于 reap 窗口不 reap。恢复时 recover 回调重注册NOOP 路径刷新last_seen零 context 抖动。worker 崩溃后重启新进程获得全新 uuid 派生 ID 与新条目旧 id 被独立 reap。无 PID 复用别名问题。混合客户端/服务端版本每个 PING 都过不了载荷长度检查客户端永久 unhealthy。这是响亮的失败绝不静默损坏两侧须同时升级。8. 测试验证无 GPU 即可覆盖 liveness 逻辑仓库中已有针对本设计的单元测试 tests/v1/multiprocess/test_worker_liveness.py覆盖传输模块的公开 liveness 接口、management reap 装配、blend reap listener 与配置校验。测试通过__new__绕过__init__避免启动 CUDA host-func dispatcher用threading.Lock 空 dict 构建“裸模块”从而在没有 GPU、没有真实 server的环境下验证touch_instance/reap_stale_instances/tracked_instance_count/drop_instance_state与MPServerConfig的校验逻辑。若要在本地跑这组测试# 在仓库根目录仅运行 worker liveness 相关测试 python -m pytest tests/v1/multiprocess/test_worker_liveness.py -v前提已按 docs/getting_started/installation.rst 完成环境与测试依赖安装。9. 调参与运维要点速查先确认心跳间隔客户端lmcache.mp.heartbeat_interval默认 10s与服务端worker_reap_timeout_seconds默认 120s要满足reap_timeout 3 x heartbeat_interval若把心跳间隔调大必须同步调大 reap timeout否则适配器启动时会打印 WARNING。reap timeout 不是越低越好默认 120s 已满足“错过几次 PING 不误杀”。过小的 timeout 会导致日志中出现同一 id 的重复 reap WARNING。grace 一定要 reap timeout否则 warming 中的活 worker 会比崩溃 worker 更快被回收配置校验ValueError会拒绝不合法的组合。观察手段ManagementModule.report_status()返回的worker_liveness字段enabled/reap_timeout_seconds/registration_grace_seconds/tracked_instances可用来确认 reap 是否启用、当前跟踪多少实例reap 日志以 WARNING 级别记录每个被回收的实例 ID含沉默时长与是否 PING 过可与 worker 启动时 INFO 打印的 UUID 实例 ID 对应排查。升级纪律PING 载荷变化是线上可见的服务端与所有 worker 适配器必须同步升级混合版本会导致客户端永久 unhealthy安全但需及时处理。关闭要禁用将worker_reap_timeout_seconds设为0可禁用 reap不启动 reap 线程适用于不希望自动回收的任何自定义运维场景。10. 总结Worker liveness 追踪与回收机制以“复用现成心跳”为核心用最小协议改动PING 携带实例 ID 两个状态字段last_seen、has_liveness_signal 一个独立 reap 线程系统性解决了 MP 模式下 worker 异常死亡导致的实例状态永久泄漏问题UUID 实例 ID 消除了 PID 复用造成的错误别名recover 回调与 NOOP 重注册让长短故障都能自愈且不产生多余 context。整个方案在源码lmcache/v1/multiprocess/modules/management.py、engine_driven_transfer.py、lmcache_driven_transfer.py、engine_module.py、config.py与单元测试tests/v1/multiprocess/test_worker_liveness.py中均有完整落地可作为在共享 GPU 服务器上长期稳定运行 LMCache 多进程模式的可靠性基石。【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表