Prhub

#46595 [Bugfix][MooncakeStore] track resumed requests via scheduler's resumed_req_ids

原始 PR 作者 ivanium 合并时间 2026-06-25 07:50 文件变更 3 提交数 1 评论 2 代码增减 +77 / -11

执行摘要

修复 MooncakeStore 抢占恢复后的块表错乱

在混合 KV 缓存(hybrid KV cache)的重度 warm-cache 运行中, MooncakeStore 的 send-thread 抛出 list index out of range / Store chunk out of range 错误。根因是请求被抢占恢复后, _preempted_req_ids 未被清除,导致连接器误用替换语义,而 token_len 仍反映完整长度,造成索引超界。

该 PR 修复了一个隐蔽的同步缺陷,代码简洁且配合完善测试,建议精读 build_connector_meta 中 append/replace 分支的逻辑变更。

讨论亮点

Dao007forever 指出这并非首次出现调度器与 KV 调度器状态不同步的问题,建议审查其他状态,并特别强调 MRv2 中 _preempted_req_ids 未被更新的问题——njhill 回应表示该修复仅适用于 MRv1,MRv2 中将恢复请求视为新请求。

实现拆解

  1. 移除本地 _preempted_req_ids 状态:在 MooncakeStoreScheduler.__init__build_connector_meta 中删除对 _preempted_req_ids 的初始化、更新和清理逻辑(文件 scheduler.py)。
  2. 改用调度器的 resumed_req_ids:在 build_connector_meta 中,将 if req_id in self._preempted_req_ids 替换为 if req_id in cached_reqs.resumed_req_ids,直接使用调度器输出的信息判断块表是替换还是追加(文件 scheduler.py)。
  3. 改进异常日志:在 worker.pyBaseSendingThread.run 中,将 logger.error 替换为 logger.exception,并提取 req_id 一并输出,便于定位问题请求。
  4. 测试配套:新增两个回归测试:test_running_request_not_in_resumed_req_ids_appends_blocks 验证非恢复请求追加块,test_resumed_request_in_resumed_req_ids_replaces_blocks 验证恢复请求替换块。同时调整现有测试用例,移除对 _preempted_req_ids 的依赖,在 scheduler_output 中增加 resumed_req_ids 字段。
文件 模块 状态 重要度
vllm/distributed/kv_transfer/kv_connector/v1/mooncake/store/scheduler.py 调度器 modified 6.73
vllm/distributed/kv_transfer/kv_connector/v1/mooncake/store/worker.py 工作线程 modified 6.21
tests/v1/kv_connector/unit/test_mooncake_store_scheduler.py 测试 modified 7.16

关键符号

MooncakeStoreScheduler.build_connector_meta BaseSendingThread.run

关键源码片段

vllm/distributed/kv_transfer/kv_connector/v1/mooncake/store/scheduler.py core-logic

核心修复:移除本地 `_preempted_req_ids`,改用调度器的 `resumed_req_ids` 判断块表替换 / 追加。

class MooncakeStoreScheduler:
    def __init__(self, ...):
        # ... 其他初始化
        self.load_specs: dict[str, LoadSpec] = {}
        self._request_trackers: dict[str, RequestTracker] = {}
        # self._preempted_req_ids: set[str] = set() # 已移除
        self._unfinished_requests: dict[str, ...] = {}
        self._unfinished_request_ids: set[str] = set()
​
    def build_connector_meta(self, scheduler_output: SchedulerOutput) -> KVConnectorMetadata:
        # ... 清理 finished 等
        preempted_ids = scheduler_output.preempted_req_ids or set()
        # self._preempted_req_ids.update(preempted_ids) # 已移除
        for req_id in preempted_ids:
            self.load_specs.pop(req_id, None)
            if request_tracker := self._request_trackers.get(req_id):
                request_tracker.reset()
            self._unfinished_requests.pop(req_id, None)
        # ... 处理 cached_reqs
        for req_id, new_block_ids in zip(cached_reqs.req_ids, cached_reqs.new_block_ids):
            # ... 跳过 prefill 等
            # 原来:if req_id in self._preempted_req_ids:
            if req_id in cached_reqs.resumed_req_ids:
                # 抢占恢复:替换块表(new_block_ids 是完整表)
                new_block_ids = tuple(b.copy() for b in new_block_ids)
                # self._preempted_req_ids.discard(req_id) # 已移除
            else:
                # 普通 decode:追加块
                pass
vllm/distributed/kv_transfer/kv_connector/v1/mooncake/store/worker.py core-logic

改进异常日志,使用 logger.exception 并输出 req_id,便于调试。

class BaseSendingThread(threading.Thread):
    # ...
    def run(self):
        self.ready_event.set()
        while True:
            request_data = None # 预先初始化
            try:
                request_data = self.request_queue.get()
                if request_data is None:
                    logger.warning("Received a None request!")
                    self.request_queue.task_done()
                    continue
                self._handle_request(request_data)
            except Exception:
                req_id = getattr(request_data, "req_id", "<unknown>")
                logger.exception("Error in %s (req=%s)", self.name, req_id)
tests/v1/kv_connector/unit/test_mooncake_store_scheduler.py test-coverage

新增两个回归测试,验证 append 和 replace 分支的正确性;调整现有测试移除对 _preempted_req_ids 的依赖。

def test_running_request_not_in_resumed_req_ids_appends_blocks():
    """非恢复请求的 new_block_ids 应追加到 tracker,而非替换。"""
    scheduler = _make_bare_scheduler()
    _add_unfinished_request(scheduler, token_ids=list(range(48)),
                            block_hashes=[b"h0", b"h1"], prefill_end_tokens=48)
    # 调度器输出:resumed_req_ids 为空
    meta = scheduler.build_connector_meta(_make_scheduler_output(scheduled_spec_tokens=None))
    req_meta = meta.requests[0]
    # 追踪器中原有 [0,1],追加 [2] →合并为 [0,1,2]
    tracker = scheduler._request_trackers["req-0"]
    assert tracker.allocated_block_ids == ([0, 1, 2],), "应追加新块"
    # token_len(44) <= 块表长度(3*16=48),不会越界
    assert tracker.token_len <= len(tracker.allocated_block_ids[0]) * scheduler._block_sizedef test_resumed_request_in_resumed_req_ids_replaces_blocks():
    """恢复请求的 new_block_ids 应完全替换 tracker 中的旧块。"""
    scheduler = _make_bare_scheduler()
    _add_unfinished_request(scheduler, token_ids=list(range(48)),
                            block_hashes=[b"h0", b"h1"], prefill_end_tokens=48)
    # 调度器输出:resumed_req_ids 包含 "req-0",new_block_ids 为完整表
    scheduler_output = _make_resumed_scheduler_output(num_scheduled_tokens=0)
    meta = scheduler.build_connector_meta(scheduler_output)
    tracker = scheduler._request_trackers["req-0"]
    assert tracker.allocated_block_ids == ([0, 1, 2],), "应替换为新块"
    # token_len 应与块表匹配
    assert tracker.token_len <= len(tracker.allocated_block_ids[0]) * scheduler._block_size

评论区精华

状态同步问题 设计

Dao007forever 指出这并非首次出现调度器与 KV 调度器状态不同步的问题,建议审查其他状态。

结论:该 PR 仅修复了 MooncakeStore 连接器的 _preempted_req_ids 不同步问题,剩余状态问题待后续审查。 · acknowledged

MRv2 兼容性 question

njhill 指出该修复仅适用于 MRv1,MRv2 中将恢复请求视为新请求。

结论:确认修复范围限于 MRv1。 · 已解决

风险与影响

低风险:核心改动是移除一个与调度器状态脱节的本地集合,改为读取调度器直接提供的权威字段 resumed_req_ids,逻辑上更可靠。但需确保所有依赖 _preempted_req_ids 的路径已完全清理(本 PR 已处理)。异常日志改进仅增强可观测性,无副作用。

直接影响 MooncakeStore 连接器在抢占恢复场景下的正确性:修复后恢复请求的块表与 token_len 一致,消除索引越界异常。对正常运行无影响。测试覆盖增强,降低回归风险。

核心路径变更 需要 MRv2 审查

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论