Skip to content

[Observability] Add minimal OffloadingConnector transfer/recovery observer seams #43

Closed

Description

@Remygred

背景与范围

落实 vLLM-HUST/vllm-hust-kv-transfer-observability#2 的真实宿主接入要求:
vLLM-HUST/vllm-hust-kv-transfer-observability#2 (comment)

插件 PR #5/#6 已合并,提供 typed observation、bounded sink、descriptor capture、normalizer、显式 binding/config 和 CPU/fixture 测试;尚无真实宿主 KV 事件绑定,保持 import_only。本 Issue 只请求最小、通用、默认关闭的宿主观察出口;exporter、队列、规范化和文件发布继续在插件仓库,不迁回 core。

现有方案:
https://github.com/vLLM-HUST/vllm-hust-kv-transfer-observability/blob/53e69013dcf27fd08a93d142288c93c701edabd3/docs/current_host_seam_proposal.md
该链接为已合并旧版;以下内容是 2026-09-28 针对新代码的修正,插件本地提案也已更新但尚未推送。

已核查,不是重复 EventBus

  • core main:c7b61770097b5c6e9ae11d96baef59e3689231f1
  • Ascend main:17f68177472b896facf3f0501d424d24e16e699a
  • Manager main:701aa95ae8d5b23b5ea8c8ec475ceaa470393e54

core #3 与 #6 尚未合并;#6 head 1287794 提供 request finished/preempted/reclaimed,不提供 KV transfer/recovery/H2D/first-compute。#3 是 sampled-output 生命周期。应复用它们最终合一的 registry,不能新增第三套总线。当前 general-plugin loader/evidence 证明加载/调用,不证明 KV transfer。BlockStored/BlockRemoved 是 residency 事件,不能直接改名替代。

已检索本仓 KV observer/offloading 与 upstream offloading observer;未发现实现本完整接口的重复工作。关联本仓 #1 的最小宿主 seam 方向。

首个实际目标

OffloadingConnector 的 CPU↔设备 offload。Ascend 已注册 AscendOffloadingConnector,并将 CPUOffloadingSpec/TieringOffloadingSpec 映射到 NPU 实现;复用现有 scheduler/worker 逻辑,非整体移植 legacy 补丁。首个验证配置的 runner、TP、模型和软件版本需要在实际运行前明确,不声称全面兼容。

最小接口与调用点

  1. 共享注册/注销:在已有共享 outlet 上注册观察 sink,返回可移除关联;禁用无事件构造/计时/I/O,观察异常不能改变 serving。宿主仅发布闭合 primitive fields,不 import 插件类型。
  2. scheduler→worker 身份:复用 offloading/common.py 的 TransferJob / OffloadingConnectorMetadata,携带 bounded request/job/recovery-episode 上下文;worker 明确 rank/generation。不通过时间接近或文件名猜身份。
  3. 实际提交:offloading/worker.py 的 start_kv_transfers / handle_preemptions 中真实 submit_load/submit_store 的结果。prepare_store_kv 只是排队,不能作为传输 start。replicated-layout 非写入 worker 的 mark_completed 是确认,不是物理 D2H。
  4. 完成与回执:在 get_finished 的实际 TransferResult 处观察,利用 OffloadingWorkerMetadata 回传逐 worker 的 bounded H2D receipt。现有 completed_jobs 仅聚合 job→count,不足以证明 rank/generation 覆盖;观察字段不能改变原有调度计数语义。
  5. recovery/admission:scheduler update_connector_output 聚合回执,实际恢复调度时发布 admission,并向 worker 传递精确成功 H2D roster。普通 prefix-cache load 不是自动等于 post-preemption recovery;完成/具备调度资格也不等于实际 admission。
  6. first compute:V1 对应 core/Ascend 的真实 _model_forward;V2 对应 core gpu/model_runner.py eager/piecewise/fullgraph 执行分支,Ascend V2 委托该路径。ActiveKVConnector.pre_forward 也会被 no_forward 调用,不能仅在此无条件发 first-compute;排除 dummy/profile/capture、padding、重复或过期 generation。异步 load 可能在当前批 forward 之后提交,须关联以后真正执行该恢复请求的批次。
  7. failure/cancellation:当前 offloading worker 会 assert 提交/完成成功;在实际失败/放弃处观察但不吞掉原本的宿主错误,也不新增恢复策略。request_finished 后仍可能有合法存储任务,不可一律标 cancellation。进程消失但未发回执只能算不完整证据。
  8. descriptor 安全边界:发布 region ID、相对 offset/size/direction 等有界布局,不暴露原始 tensor、process/device address、token/hash 或 KV 内容。Ascend native/cpu_npu.py 的 batch_src/batch_dst 是原始指针数组,不能传给插件再过滤。文件 I/O 在插件有界后台 writer 中执行。

验证与外部依赖

  • disabled 等价性,observer 异常隔离,真实提交/完成顺序;
  • 跨进程/rank/generation、重复/丢失/乱序与精确 receipt roster;
  • 正常/失败/实际取消,注销及退出清理;
  • exact host/plugin 版本上的 Manager discover/check/plan/run 和目标 worker 覆盖;
  • 真实设备回执及启停正确性/开销,不以 fixture 代替设备证据,不要求加速曲线。

当前两边最新 main 的 RoutedExpertsLists/RoutedExpertsTensors 导入不匹配仍存在,关联 vLLM-HUST/vllm-ascend-hust#34。Ascend 仓库记录的 upstream verified core ced6857 是待验证候选,不能把它直接称为已通过运行测试的组织 core 版本。

这是一项具体实现跟踪,不申请额外联签或冻结记录。请沿 #3/#6 的共享 outlet 协调宿主最小调用点;插件实现及诊断工具工作独立推进。

AI assistance used for code inspection and drafting; no host source changes or hardware validation are claimed in this issue.

Activity

  1. xiehanlong834-gif commented on Sep 29, 2026

    @xiehanlong834-gif

    宿主侧最小出口已实现并提 PR:#46(base main,head 1d55929),按本 issue 请求的形态:不新增总线、宿主不 import 插件类型、默认关闭、fail-open。

    已覆盖:

    尚未包含(我这边下一步):descriptor v2 区域清单、recovery epoch/requeue/admission 与逐 worker H2D receipt roster、first-compute(core+Ascend)。

    验证:e521b42e + 该分支,tests/v1/kv_connector/unit/offloading_connector/ 472 passed / 2 skipped(CPU 测试模式),ruff clean。

  2. xiehanlong834-gif commented on Sep 29, 2026

    @xiehanlong834-gif

    第二块也已提交(同一 PR #46,新 commit e89a7e5):

    • [Misc] Sync official upstream main #4 完成与回执:worker 经 OffloadingWorkerMetadata.load_receipts 回传逐 worker 的 bounded H2D receipt(job → sorted ranks),调度侧在 pending 归零时发布 receipt(带精确 rank 集合)。原有 completed_jobs 计数语义未改。
    • [Sync] Sync rebuilt main with upstream vLLM #5 recovery/admission:抢占(preempted_req_ids)打开一个 recovery episode 并发 requeue,epoch 取 request.num_preemptions;调度器内部抢占原因对 connector 不可见,reason 保持 unclassified(不猜)。episode 内完成的 H2D load 收集为 roster;只有调度器真正 resume(resumed_req_ids)且 roster 精确时才发 admission。普通 prefix-cache load 不会变成 recovery。
    • 边界:roster 超上限(4096)的 episode 不发 admission(不发残缺 roster);非写入 worker 的确认、prepare_store_kv 的排队都不算传输点。

    验证:e521b42e + 该分支,tests/v1/kv_connector/unit/offloading_connector/ 485 passed / 2 skipped(新增 13 个用例),ruff clean。

    仍缺:descriptor v2 区域清单、first-compute(core+Ascend)。

  3. xiehanlong834-gif commented on Sep 29, 2026

    @xiehanlong834-gif

    第三块(descriptor v2 区域布局)也已进 PR #46(新 commit 537b7f0):

    • 在 copy 现场(v1/kv_offload/cpu/gpu_worker.py 的 op 构建处)直接产出 region-relative 描述符:region id = canonical tensor 索引(对应 legacy 的 src_tensor_N/dst_tensor_N),offset = 指针减各自 region base,size = 每 op 字节数。绝对指针、tensor 对象、hash、token id 一概不出边界——不需要插件再做二次过滤(对应本 issue 第 8 条:Ascend batch_src/batch_dst 不能原样外传)。
    • 有界:4096 条上限(与消费方一致),超限时上报 dropped 计数而不是静默截断;默认关闭时该路径一次分支都不进(copy 现场 layout 为 None)。
    • Ascend 侧(cpu_npu.py 自建 op)是 follow-up——同一 API,调用点要在 vllm-ascend-hust 里补。

    验证(e521b42e + 该分支,CPU 测试模式):offloading_connector/ + test_region_descriptors.py 490 passed / 2 skipped(新增 18 个用例);test_gpu_worker.py 需真卡,在与不含本改动的 main 上同为 32 failed(既有设备依赖失败,非回归);ruff clean。

    宿主侧至此只剩 first-compute 一个缺口。

  4. xiehanlong834-gif commented on Sep 29, 2026

    @xiehanlong834-gif

    Host-side update for #43: the last open item (first compute) is up on the feat/kv-offload-observability-seam branch / PR #46.

    • first_compute is published when an admitted request actually reaches a real model forward, and consumed exactly once. Dummy, profile, and graph-capture batches never report.
    • The prefill/decode classification comes from the request's own computed-token state at admission time, not from a token-count guess.
    • The V1 runner's real-forward observation sits immediately before _model_forward, the point dummy runs never reach; on V2 it is guarded by not dummy_run in both the FULL and the piecewise/eager branches.
    • Everything stays bounded and address-free; the roster is the exact sorted set of restored job ids that the admission already reports.

    With this, all eight host-side items of #43 have a call site in core except two Ascend-side follow-ups, both of which need edits in vllm-ascend-hust (the descriptor call site in cpu_npu.py, and the one-line observation in the Ascend V1 execute_model, which overrides the core runner). The Ascend V2 path delegates to core and is already covered.

  5. xiehanlong834-gif commented on Sep 29, 2026

    @xiehanlong834-gif

    Host side is complete and open for review: #46 (4 commits, 1d55929 → 98e437fd; head CI green, checks taken on origin/main @ e521b42e + the four commits)

    Item-by-item against the eight points above:

    1. Shared registration / unregistration - offloading/observability.py: register_kv_transfer_observer returns a removable handle, unregister is idempotent, dispatch is fail-open (a raising observer is logged, removed, and serving continues). Disabled costs one dict guard per call site: no clock read, no record built, no callback. The host imports no plugin type, and this adds no bus of its own - no thread, no queue, no file I/O in core. It is a connector-local registry; when the registry from feat(observability): expose default-off request lifecycle events #6 lands this surface can re-home onto it with the emit points unchanged. This is the one design call I would like your read on.
    2. scheduler→worker identity - records carry job_id + request_id from TransferJob plus an explicit rank; recovery context travels inside the existing OffloadingConnectorMetadata as recovery_admissions = {req_id: (epoch, roster, kind)} (bounded 4096). Nothing is inferred from time proximity or filenames; epoch is the request's own num_preemptions. Gap I will not paper over: the host has no worker generation identifier today. If you want one, it is a small bounded counter and I would rather add it explicitly than leave it implied.
    3. Actual submission - observed exactly where you asked: worker.py::start_kv_transfers and handle_preemptions (real submit_load/submit_store). prepare_store_kv only queues, and a replicated-layout non-writer mark_completed is a confirmation, so neither is observed.
    4. Completion and receipts - observed at the real TransferResult in worker.py::get_finished; per-worker H2D receipts travel through a new OffloadingWorkerMetadata.load_receipts = {job_id: ranks} and aggregate in scheduler.py::update_connector_output, publishing once every owing worker reported. Existing completed_jobs counters are untouched; the recovery epoch currently rides on the requeue/admission records rather than the receipt (say the word if you want it on both).
    5. recovery / admission - _observe_recovery_requeued fires on preempted_req_ids, _observe_recovery_admitted only on a real resumed_req_ids resume carrying the exact sorted roster. A plain prefix-cache load never reports an admission, and completing a load is never reported as a resume. The exact roster goes to the workers in the same step's metadata. Requeue reason is unclassified: the connector cannot see the scheduler-internal reason, and I preferred that over a guess.
    6. first compute - V1: immediately before _model_forward in gpu_model_runner.py, a point dummy, profile, and capture runs never reach; V2: gpu/model_runner.py after pre_forward in both the FULL and piecewise/eager branches, guarded by not dummy_run. pre_forward itself is never used for this, per your no_forward note. Exactly once per episode: the worker stashes the admission when the metadata is bound and pops it on the first real forward; a newer admission replaces a pending one, and finished requests drop theirs. The async case holds because the marker resolves on the batch that actually forwards the request, not on the batch that submitted its load. The prefill/decode kind comes from the request's computed-token state at admission, not from a token-count heuristic that speculative decoding would distort.
    7. failure / cancellation - still-open transfers are observed as cancelled at shutdown (host_shutdown), and nothing is marked cancelled merely because a request finished. On failures I have to be plain: the offloading worker asserts success in three places, so a failed submit or result aborts before any observation point exists. Adding one would change host error semantics, which this item explicitly does not ask for.
    8. descriptor safety boundary - v1/kv_offload/cpu/gpu_worker.py::build_region_descriptors: region id is the canonical tensor index (the numeric form of the legacy src_tensor_N), offsets are relative to the region base, the inventory is bounded at 4096 with dropped_descriptors counted. No raw tensors, process/device addresses, token ids, or hashes leave the copy site, and all file I/O stays in the plugin.

    Two Ascend follow-ups, both edits in vllm-ascend-hust, not here:

    • the descriptor call site in cpu_npu.py - the Ascend handler builds its own copy ops;
    • one call in the Ascend V1 runner - NPUModelRunner overrides both execute_model and _model_forward, so the core hook does not run there. The Ascend V2 path delegates to core and is already covered.

    What this does not claim: no device evidence, no Manager discover/check/plan/run, no overhead numbers. Validation so far is CPU component tests (500 passed / 2 skipped on a 910B2 node in CPU mode, VLLM_TARGET_DEVICE=cpu) plus green CI. The plugin should stay import_only until a real event chain runs.

    Ask: (a) is the item-1 shape acceptable - connector-local registry now, re-homed onto the #6 registry when it merges; (b) do you want the recovery epoch on receipt records, and a worker-generation field? Both are small follow-up commits if yes.

  6. ShuhaoZhangTony commented on Nov 8, 2026

    @ShuhaoZhangTony
    Member

    2026-11-08 project-owner acceptance: closing the host-observability seam as completed. The later accepted integration supersedes the original PR #46 path: Host PR #53 merged the versioned observer contract and Plugin PR vLLM-HUST/vllm-hust-kv-transfer-observability#15 merged the descriptor consumer, with fail-closed version admission, identity/replay checks, packaging, and 259 local tests plus remote CI. Current main also contains dedicated offloading observability and recovery tests. Real hardware/performance qualification remains correctly tracked in the plugin's issue #2; it is not a reason to keep this host-interface request open.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions