与 vLLM 同机部署的 Python 进程。源码包在仓库根目录下的 subscriber/,独立管理依赖,不依赖父项目构建系统。
subscriber 订阅推理引擎的 KV cache 事件,转发给 KVCM,并与同机 DashServing 绑定健康状态 (同生同死)。
技术栈:Python ≥ 3.11(asyncio 全异步)、ZeroMQ(pyzmq)、gRPC(grpcio 1.75.1 +
protobuf 3.20.3,pb 手工维护)、msgspec(msgpack)、httpx、pyyaml、dashlog(可选依赖)。
工具链:uv + ruff + mypy --strict + pytest。
| 模块 | 职责 |
|---|---|
subscriber/cli.py、main.py |
入口与 SubscriberLifecycle:启动 7 步、serving 监督、关停 6 步 |
subscriber/config.py |
SubscriberConfig:字段、CLI 注册、校验、派生属性 |
subscriber/forwarding.py、pipeline/ |
producer / sender 协程与无状态 per-batch 阶段 |
subscriber/engine/ |
引擎适配层(AbstractEngineAdapter + vllm / sglang + gRPC client) |
subscriber/health/ |
引擎存活协调器(epoch 门控)+ DashServing 状态上报 |
subscriber/kvcm/ |
KVCM 客户端栈(domain client + HTTP transport + 服务发现) |
subscriber/metrics/ |
指标唯一出口(主路径 telemetry + lifecycle 点事件) |
subscriber/proto/ |
engine_service_rpc pb 绑定(手工维护,见下文) |
分层依赖方向:main → forwarding → (engine, health, kvcm, pipeline, metrics)。
engine/ 与 kvcm/ 互不依赖(KvCacheDescriptor 由 main 在启动时注入),禁止加反向 import。
稳态架构文档在 docs/architecture/,改架构前先读、改完回来更新:
| 文档 | 内容 |
|---|---|
| 00-overview | 模块划分、启动 / 退出时序、数据流总览、核心不变量 |
| 01-forwarding-pipeline | 双 pipeline 数据流、凑批、门控、丢弃语义 |
| 02-engine-adapter | adapter 契约、代际语义、vLLM 实现、replay、快照信号 |
| 03-health-and-liveness | epoch、判死、HostDown、同生同死探针 |
| 04-kvcm-client | 传输、服务发现、两步注册、心跳、location spec |
| 05-observability | span 阶段、指标出口、日志、trace id |
| 06-configuration | 配置优先级、字段分组、环境变量、校验 |
所有命令在仓库根目录(pyproject.toml 所在目录)下执行,使用 uv 管理环境。
# 初始化 / 同步依赖
uv sync --dev
# 运行测试
uv run pytest
uv run pytest -v # 详细输出
uv run pytest tests/engine/vllm/test_incremental.py # 单文件
# Lint(ruff,line-length=88,规则集 E/F/I/UP/B)
uv run ruff check subscriber/ tests/ harness/
uv run ruff check --fix subscriber/ tests/ harness/ # 自动修复
# 格式化
uv run ruff format subscriber/ tests/ harness/
uv run ruff format --check subscriber/ tests/ harness/ # 仅检查,不修改
# 类型检查(mypy strict)
uv run mypy subscriber/
# Manifest 驱动的跨仓 harness(原子记录写入已忽略的 harness/records/runs/)
harness/run_local_checks.sh baseline all
harness/run_local_checks.sh protocol all
harness/run_local_checks.sh quality all
uv run python harness/loop.py review
# 启动进程(SPECTRUM_DEPLOYMENT_NAME、--kvcm-base-url 与 --host-port 为启动必填)
export SPECTRUM_DEPLOYMENT_NAME=my-deployment
uv run python -m subscriber --kvcm-base-url grpc://10.0.0.1:6381 --host-port 8080
# 指定参数启动
SPECTRUM_DEPLOYMENT_NAME=my-deployment uv run python -m subscriber \
--kvcm-base-url spectrum://vs-example:6381 \
--host-port 8080 \
--engine-type vllm
# 使用配置文件(kvcm_base_url 与 host_port 可写在 yaml 中)
SPECTRUM_DEPLOYMENT_NAME=my-deployment uv run python -m subscriber --config config.yaml默认 gRPC 时,裸 host:port、grpc:// 和 http(s):// 都是直接 channel target;只有
static:// / spectrum:// 会经 service discovery 解析。端口必须为 KVCM meta_rpc_port;
HTTP 回退时才改为 meta_http_port。
- pre-commit 钩子(本地强制):
.pre-commit-config.yaml按暂存文件路由执行,通过uv run pre-commit install安装到.git/hooks/pre-commit后在每次 commit 时强制执行。 clone 仓库后必须先执行一次uv run pre-commit install。检查项:subscriber/、tests/或harness/的 Python 文件暂存时:ruff check、ruff format --check; subscriber/config/toolchain 变更另触发 mypy 与全量pytest --cov(覆盖率门禁fail_under = 90);subscriber/proto/文件暂存时额外执行:全部 pb2/pb2_grpc import(protobuf 3.20.3 兼容)、authoritative.proto/ runtime /.pyiparity,以及tests/proto/test_engine_service_rpc.py、tests/engine/test_grpc_clients.py和 KVCM gRPC client 测试;docs/metrics.json或 metrics 源码/测试变更时执行 metrics catalog parity;- 每次 commit 检查 staged whitespace/conflict marker;修改 hook 配置时先验证配置本身。
- pytest(开发中随手跑):commit 时 pre-commit 已强制全量测试,
开发过程中仍应随手
uv run pytest获得快速反馈(见「约束」一节的 「修改后必须通过检查」)。测试未通过的修改视为未完成,不得提交/推送。 - AoneCI(MR 触发):MR opened 时触发两条模板流水线——「Python单元测试」
(Python 3.12,
pip install '.[dev]'后跑 pytest,失败钉钉通知)与 「代码质量扫描(多语言)」。dev 依赖的唯一权威来源是[project.optional-dependencies].dev(uv 的 dev group 引用该 extra), 新增 dev 依赖写在 extras 里,否则 CI 装不上。CI 状态用a1 ci run get查询。 - e2e(显式 opt-in):
tests/e2e/的每个真实运行时用例都用pytest.mark.skipif检查各自前置条件:DashServing 用例要求DSV_BINARY(已构建的dashservingd),KVCM gRPC 用例要求KVCM_REAL_GRPC_TARGET(可选KVCM_REAL_ADMIN_HTTP_URL做隔离 setup)。 默认全部跳过,保证默认uv run pytest在任何机器上行为一致。
- 常规开发:
feat/<description>,如feat/full-kvcache - 缺陷修复:
fix/<description>,如fix/zmq-replay-seq - 个人长期特性分支:
feature/<花名>/<description> - 临时验证:
worktree-<description>(配合 git worktree,用完删掉)
不要直接在 master 上开发;不要 force push master。
沿用仓库历史的 conventional commits:<type>: <description>,type ∈
feat / fix / refactor / chore / docs / test / perf。
示例:feat: add use_eagle_pop and mamba_cache_mode in get_kv_cache_descriptor、
fix: fix zmq replay seq and frame、docs: update README/AGENTS.md
- 一个 commit 只做一件事;重构与行为变更不要混在同一个 commit。
- 禁止写入
Co-Authored-By行。 - 不得使用
--no-verify跳过 pre-commit 钩子;钩子失败要修根因。钩子失败意味着 commit 没有 发生,此时新建 commit,不要--amend。
- MR 标题与 commit message 保持一致。
- MR 描述必须包含:背景与动机、改动点、验证方式(贴
uv run pytest/ruff/mypy的实际 结论)、影响面与回滚方式。 - 跨仓库改动(dashllm gRPC server、KVCM 服务端、
.protowire schema)必须在描述里列出对 应仓库的 MR 链接,并说明上线顺序与兼容窗口。
按以下 7 步执行。每完成一个步骤后必须停下来等用户确认,不要自动继续下一步。
- 明确需求与范围 — 复述需求、边界、不做什么;不清楚的地方先问,不要猜。
- 分析代码 — 先读
docs/architecture/对应文档,再读源码,输出数据流 / 关键调用路径 / 受影响的不变量;发现文档与代码不一致,先报告再动手。 - 确定方案 — 写入
docs/specs/YYYY-MM-DD_slug.md(含方案、被影响的不变量、测试策略、 风险),等用户确认后再写代码。 - 实施修改 — 从最新
master建分支;测试先行(改行为先改/加测试);小步提交。 - 质量门禁 —
uv run pytest、uv run ruff check subscriber/ tests/ harness/、uv run mypy subscriber/全绿,并把实际输出结论贴出来。不许用「应该没问题」代替执行。 - 提交与评审 — commit → push 分支 → 创建 MR,指定评审人。
- 回顾与更新 — 检查并更新
docs/architecture/、AGENTS.md、README.md、docs/metrics.json;把落地方案与设计文档的差异回写到 spec。
生产环境固定使用 protobuf==3.20.3。当前 grpc-tools 生成的 Python
代码会导入 google.protobuf.runtime_version / _builder 并校验较新的
protobuf runtime,无法在生产环境加载。因此 subscriber/proto/ 下的
Python pb 文件必须 hand-write / 手工维护,禁止运行
grpc_tools.protoc 后直接提交生成结果。
subscriber/proto/engine_service_rpc.proto 仍是 authoritative wire
schema。修改 .proto 后:
- 手工同步
engine_service_rpc_pb2.py中的 descriptor message/field/service 定义,继续使用兼容 protobuf 3.20.3 的descriptor_pb2/message_factory实现;不得引入runtime_version或_builder。 - 手工同步
engine_service_rpc_pb2.pyi的字段、类型和构造函数。 - 只有 RPC service、method 或 request/response type 发生变化时,才
手工同步
engine_service_rpc_pb2_grpc.py;仅新增 message field 时该文件不应产生 diff。 - 保持 authoritative field number、无
package的 service identity 和 完整 RPC path 不变,并增加 descriptor field-number、raw wire parse 和 RPC path 测试。
上述 pb 文件均提交到仓库,构建时不会自动生成。修改后至少运行对应
proto/client 测试和 uv run mypy subscriber/,确认 protobuf 3.20.3
环境可以正常 import。
- asyncio 全异步:所有 IO 必须使用
zmq.asyncio.Socket,禁止在 async 函数中调用同步 ZMQ socket 方法。 - 引擎适配器隔离:ZMQ 订阅和 replay 逻辑封装在
AbstractEngineAdapter子类中,kv_event_loop不感知具体引擎实现。 - 引擎存活探测隔离:底层连接状态(如 ZMQ socket monitor)由
AbstractEngineAdapter.watch_liveness()暴露为引擎无关事件,判死、epoch、reset 策略集中在 health coordinator。 - 冷启动 reset 语义:subscriber 首次连接引擎前不发送
AllBlocksCleared;只有曾经进入可发送代际后再断线并达到重试阈值,才发送 reset。 - 发送门控:向 kvcm 转发实时或 replay batch 前必须通过
EngineHealthCoordinator.wait_ready_epoch()获取当前可发送 epoch。 - 分阶段 span 与主路径指标:adapter 创建并 yield
EngineEventBatch(batches, telemetry, trace_id);同一个BatchTelemetry经PipelineContext贯穿到 KVCM client。adapter 标记decode/replay_fetch/snapshot_*,forwarding 标记queue_wait/engine_gate_wait/block_filter,KVCM client 标记expand/kvcm_send。telemetry.mark("stage")只记录内存状态;count/gauge可带tags,空标签分别保持 counter 聚合与 gauge 最后值,非空标签保留独立 observation。仅MetricsReporter后台任务 flush 到 dashlog;BatchTelemetry与 reporter 的观测路径均 best-effort、永不阻断转发主路径。 - Metrics 上报规范:所有指标通过
subscriber/metrics/包(_base.py中的_dashlog_counter/_dashlog_gauge)统一出口上报,自动添加kvcache_subscriber_前缀。维度信息(endpoint、status、reason 等)通过tags参数传递(对应 Prometheus labels),不要编码进 metric name。dashlog 本地不可编译时所有 report 函数为 no-op(try: import dashlog / except ImportErrorfallback)。新增或修改指标后必须同步更新docs/metrics.json指标目录。_dashlog_counter按DS_EAS_USE_OTEL双路径切换(判定规则与 dashlogEASOTelClient::Enabled()一致):ASI-EAS 部署由 manager 注入该变量,直调 dashlog 原生Counter走 OTel 增量语义;未设置(PAI-EAS)时走 Gauge 累积绕行,因为 legacyAddCounters在 OTel 关闭时会静默丢数据。 - Replay 语义:检测到 seq gap 时,先聚合 replay batch 一次性 yield,再 yield 当前实时消息。vLLM replay 端会返回所有
seq >= gap_start_seq的缓冲消息(包含触发 gap 的当前实时消息及更新消息),必须过滤只转发缺失区间[gap_start_seq, current_seq - 1](其余会以实时消息到达,直接转发会重复);过滤后仍要 drain 到 END marker。replay 成功完成(返回列表,含空列表)后立即把_last_seq推进到current_seq - 1,与实时消息 decode 成败解耦,避免同一 gap 被重复 replay。replay 中途 abort(超时、异常、帧格式错误、decode 失败)必须调用_replace_replay_socket()更换 DEALER socket,防止残留帧污染下一次 replay。replay 使用 DEALER socket(非 REQ),避免 async 下严格一问一答的阻塞问题。 - 配置优先级:CLI 显式参数 >
--config yaml文件 > dataclass 默认值;kvcm_base_url为必填项, 可为 CLI / 配置文件 /KVS_KVCM_ENDPOINT环境变量提供(优先级 CLI > YAML > 环境变量), 全部为空时在validate()阶段直接报错。 - 启动必需的身份环境变量:
SPECTRUM_DEPLOYMENT_NAME必须为非空唯一部署标识;KvcmClient.start()会校验,缺失/空白直接失败(fatal),因为空值会退化为_<block_size>这类被无关实例共享的 instance_id,破坏"KVCache 仅在同一 instance_id 内复用"的隔离不变量。 - Lint/类型检查禁用规则:禁止通过内联注释(如
# noqa、# type: ignore)禁用 ruff 或 mypy 检查。自动生成的代码(如 protobuf 生成文件)应在pyproject.toml或ruff.toml中通过exclude/per-file-ignores排除整个文件;只有确实无法通过修改代码解决的极少数情况,才允许使用注释禁用,且必须在注释中说明原因。 - Docstring 规范:通用接口/模块定义(后续要实现某个接口的 placeholder)、抽象基类的 public 方法、复杂实现逻辑必须有 docstring,说明契约、默认行为和关键不变量。简单的内部 helper 和自解释代码不需要。
- 文档落盘目录:
docs/architecture/放稳态架构文档(当前代码长什么样,编号命名如03-health-and-liveness.md);docs/specs/放变更设计文档(某次改动怎么做,命名YYYY-MM-DD_slug.md,如2026-07-21_engine-node-metadata-rpc.md);docs/plans/放实施计划,docs/reviews/放评审与测试报告。不要在项目根目录或其他位置创建specs/目录。改了架构必须 回写docs/architecture/对应文档,不能只留一篇 spec。 - 仓库根目录不落运行时日志:运行时日志不得写入仓库根目录。stdlib 日志后端默认只输出 stderr,需要文件日志时设置
SUBSCRIBER_LOG_FILE=<path>(指向仓库外或部署目录);review / 报告类产物归档到docs/reviews/。 - 修改后必须通过检查:所有代码修改完成后,必须确保
uv run pytest、uv run ruff check subscriber/ tests/ harness/和uv run mypy subscriber/全部通过,否则修改视为未完成。四项均由 pre-commit 钩子在 commit 时强制(含全量 pytest --cov);开发中仍应随手运行以获得快速反馈(见「质量门禁」一节)。
mypy --strict:所有函数(含测试 helper 之外的内部函数)必须有完整注解;异步生成器标注AsyncGenerator[T, None]。- 文件首行
from __future__ import annotations。 - 模块私有用
_前缀;只在包__init__.py里 re-export 对外 API。 - 单位与语义后缀:秒
_s、毫秒_ms、字节_bytes、计数_count、上限_maxsize、 间隔_interval_s、超时_timeout_s。新字段沿用既有后缀,不要自造。 - 不可变优先:跨模块传递的数据载体用
@dataclass(frozen=True)(如EngineEventBatch、MergedBatch、KvCacheDescriptor);只有需要在 pipeline 中累积状态的才可变 (如PipelineContext)。
- 结构化调用:
logger.info("message", step="<stage>", tags={...})。消息本身是固定短语, 不要用 f-string 把变量拼进 message——变量放tags。 - debug 级诊断先判
logger.is_debug_enabled()再拼装,避免热路径做无用工作。 - 高频失败必须限流(首次详细 + 周期汇总),参考
KvcmDropTracker与 state reporter 的写法。
- 只捕获预期异常类型,禁止裸
except:;asyncio.CancelledError必须原样重新抛出。 - 观测 / 上报路径(metrics、span、日志、best-effort 状态上报)永不向主路径抛异常。
- 业务不可用与业务拒绝要区分类型(
KvcmUnavailableErrorvsKvcmReportRejectedError), 不要用同一个异常表达「可重试」和「永久失败」。 - 编程错误不要吞:非预期异常让它冒到
_serve_until_shutdown,由生命周期统一处理。
tests/目录结构镜像subscriber/;跨模块行为放顶层(test_forwarding.py/test_main.py)。- 真实 ZMQ socket 用例打
@pytest.mark.integration,需要真实dashservingd的打@pytest.mark.e2e(显式 opt-in:设置DSV_BINARY指向已构建的二进制才运行, 否则跳过)。 - 覆盖率门禁
fail_under = 90:只能往上调,不许为了让改动通过而下调。
subscriber 与同机 DashServing "同生同死":subscriber 未 active 时 DashServing 不导流,subscriber 异常时 DashServing 暴露健康失败。详细 spec 见 docs/specs/2026-07-22_subscriber-health-dashserving-integration.md。
核心不变量(修改代码前必须理解):
- 引擎健康只由
GetWorkerStatus.alive驱动:不用 HTTP/readiness做引擎健康判断,不用 DashServing 探针反馈 engine DEAD。 - KVCM 是 lossy 旁路,不是门控(运行期):进入 serving 后,KVCM 不可用不阻塞转发、不影响探针、不改变 subscriber 状态。发送失败丢弃并计数,恢复后自动续传。不要为 KVCM 加 generation/snapshot/condition 等复杂机制。启动期例外(有意设计):
_graceful_startup第 3 步会等待 KVCM 注册成功(_wait_kvcm_registered)才启动 pipeline 并上报 active——启动时需要 KVCM 注册成功才能够启动,避免在从未注册的情况下导流;等待期间 subscriber 保持 starting(readiness 503),并周期性打印 "still waiting for kvcm registration" 警告。不要把这个启动等待当作 bug"修掉"。 - seq_id 线性化:状态上报靠单调递增 seq_id 防乱序。低 seq 的 active 不能复活已接受的 inactive。不要加 session_id 除非需要支持独立重启 subscriber。
- HostDown 幂等:
AllBlocksCleared每个 sendable epoch 最多发一次。冷启动失败不发。 - 同生同死探针语义:starting → readiness 503 / liveness 200(startup grace);active → 200/200;inactive/failed/TTL expired → 503/503。
- 环境变量:
DS_LLM_LAUNCH_KV_EVENT_SUBSCRIBER=1时 DashServing 启用 subscriber 健康检查,否则探针行为不变。
不要做的事:
- 不要为引擎重启加 metadata 重新验证(拓扑不变,Pod 替换兜底)
- 不要轮询 DashServing readiness 来控制转发(
wait_ready_epoch()已是正确门控) - 不要在 KVCM transport 失败时改
is_registered(heartbeat loop 自己管重连) - 不要把 learn mode 加回来(已删除)
- 不要把 transient 启动错误报为 failed(只有 protocol/unsupported 才是 fatal)
- 参考现有
subscriber/engine/sglang/包布局:adapter.py实现AbstractEngineAdapter并加注册装饰器,__init__.pyre-export 适配器类:
# subscriber/engine/<engine>/adapter.py
from subscriber.engine.base import AbstractEngineAdapter
@AbstractEngineAdapter.register("sglang")
class SGLangAdapter(AbstractEngineAdapter):
async def subscribe_kv_events(self): ... # 实现事件订阅逻辑
async def watch_liveness(self): ... # 实现引擎无关存活事件流完整可运行示例见 subscriber/engine/sglang/adapter.py(placeholder 适配器,含全部必须实现的方法)。
- 启动时传入
--engine-type sglang,AbstractEngineAdapter.create()会自动 lazy import 并实例化。
无需修改 main.py、kv_event_loop 或任何 client 代码。
| 编号 | 内容 | 文件 | 阶段 |
|---|---|---|---|
| S-4 | subscriber 不可用恢复策略:kvcm SDK 心跳、TTL、session/checkpoint | kvcm SDK / kvcm 服务端 / subscriber | DONE(heartbeat + TTL + state reporter 已实现;session/checkpoint 见 TODO(independent-restart)) |
| S-5 | 多 DP(bootstrap runtime_topology.data_parallel_size > 1)事件订阅:当前 adapter 只支持一个 engine-discovered transport,接受 bootstrap 后会 fail closed;rank 1+ 尚未聚合 |
subscriber/engine/vllm/adapter.py | TODO(实现 per-rank bootstrap/transport 聚合前不要放开 data_parallel_size == 1 校验) |