|
| 1 | +# AGENTS.md — subscriber |
| 2 | + |
| 3 | +与 vLLM 同机部署的 Python 进程。源码包在仓库根目录下的 `subscriber/`,独立管理依赖,不依赖父项目构建系统。 |
| 4 | + |
| 5 | +## 开发命令 |
| 6 | + |
| 7 | +所有命令在仓库根目录(`pyproject.toml` 所在目录)下执行,使用 `uv` 管理环境。 |
| 8 | + |
| 9 | +```bash |
| 10 | +# 初始化 / 同步依赖 |
| 11 | +uv sync --dev |
| 12 | + |
| 13 | +# 运行测试 |
| 14 | +uv run pytest |
| 15 | +uv run pytest -v # 详细输出 |
| 16 | +uv run pytest tests/engine/vllm/test_incremental.py # 单文件 |
| 17 | + |
| 18 | +# Lint(ruff,line-length=88,规则集 E/F/I/UP/B) |
| 19 | +uv run ruff check subscriber/ tests/ |
| 20 | +uv run ruff check --fix subscriber/ tests/ # 自动修复 |
| 21 | + |
| 22 | +# 格式化 |
| 23 | +uv run ruff format subscriber/ tests/ |
| 24 | +uv run ruff format --check subscriber/ tests/ # 仅检查,不修改 |
| 25 | + |
| 26 | +# 类型检查(mypy strict) |
| 27 | +uv run mypy subscriber/ |
| 28 | + |
| 29 | +# 启动进程(默认连接本机 vLLM) |
| 30 | +uv run python -m subscriber |
| 31 | + |
| 32 | +# 指定参数启动 |
| 33 | +uv run python -m subscriber \ |
| 34 | + --zmq-pub-endpoint tcp://localhost:5557 \ |
| 35 | + --zmq-replay-endpoint tcp://localhost:5558 \ |
| 36 | + --kvcm-addr 10.0.0.1:50051 \ |
| 37 | + --engine-type vllm |
| 38 | + |
| 39 | +# 使用配置文件 |
| 40 | +uv run python -m subscriber --config config.yaml |
| 41 | +``` |
| 42 | + |
| 43 | +## 质量门禁(Quality Gates) |
| 44 | + |
| 45 | +- **pre-commit 钩子(本地强制)**:`.pre-commit-config.yaml` 定义了 ruff check / |
| 46 | + ruff format --check / mypy 三项检查,通过 `uv run pre-commit install` 安装到 |
| 47 | + `.git/hooks/pre-commit` 后在每次 commit 时强制执行。clone 仓库后必须先执行 |
| 48 | + 一次 `uv run pre-commit install`。 |
| 49 | +- **pytest 门禁(显式手动策略)**:本仓库当前没有 CI,也不使用 pre-push 钩子 |
| 50 | + 跑测试。`uv run pytest` 的执行点是**手动策略**:每次代码修改完成后、以及 |
| 51 | + push 之前,必须手动运行 `uv run pytest` 且全部通过(见「约束」一节的 |
| 52 | + 「修改后必须通过检查」)。测试未通过的修改视为未完成,不得提交/推送。 |
| 53 | + |
| 54 | +## Protobuf 兼容代码维护 |
| 55 | + |
| 56 | +生产环境固定使用 `protobuf==3.20.3`。当前 `grpc-tools` 生成的 Python |
| 57 | +代码会导入 `google.protobuf.runtime_version` / `_builder` 并校验较新的 |
| 58 | +protobuf runtime,无法在生产环境加载。因此 `subscriber/proto/` 下的 |
| 59 | +Python pb 文件必须 hand-write / 手工维护,**禁止运行 |
| 60 | +`grpc_tools.protoc` 后直接提交生成结果**。 |
| 61 | + |
| 62 | +`subscriber/proto/kv_cache_group_metadata.proto` 仍是 authoritative wire |
| 63 | +schema。修改 `.proto` 后: |
| 64 | + |
| 65 | +1. 手工同步 `kv_cache_group_metadata_pb2.py` 中的 descriptor |
| 66 | + message/field/service 定义,继续使用兼容 protobuf 3.20.3 的 |
| 67 | + `descriptor_pb2` / `message_factory` 实现;不得引入 |
| 68 | + `runtime_version` 或 `_builder`。 |
| 69 | +2. 手工同步 `kv_cache_group_metadata_pb2.pyi` 的字段、类型和构造函数。 |
| 70 | +3. 只有 RPC service、method 或 request/response type 发生变化时,才 |
| 71 | + 手工同步 `kv_cache_group_metadata_pb2_grpc.py`;仅新增 message field |
| 72 | + 时该文件不应产生 diff。 |
| 73 | +4. 保持 authoritative field number、无 `package` 的 service identity 和 |
| 74 | + 完整 RPC path 不变,并增加 descriptor field-number、raw wire parse 和 |
| 75 | + RPC path 测试。 |
| 76 | + |
| 77 | +上述 pb 文件均提交到仓库,构建时不会自动生成。修改后至少运行对应 |
| 78 | +proto/client 测试和 `uv run mypy subscriber/`,确认 protobuf 3.20.3 |
| 79 | +环境可以正常 import。 |
| 80 | + |
| 81 | +## 约束 |
| 82 | + |
| 83 | +- **asyncio 全异步**:所有 IO 必须使用 `zmq.asyncio.Socket`,禁止在 async 函数中调用同步 ZMQ socket 方法。 |
| 84 | +- **引擎适配器隔离**:ZMQ 订阅和 replay 逻辑封装在 `AbstractEngineAdapter` 子类中,`kv_event_loop` 不感知具体引擎实现。 |
| 85 | +- **引擎存活探测隔离**:底层连接状态(如 ZMQ socket monitor)由 `AbstractEngineAdapter.watch_liveness()` 暴露为引擎无关事件,判死、epoch、reset 策略集中在 health coordinator。 |
| 86 | +- **冷启动 reset 语义**:subscriber 首次连接引擎前不发送 `AllBlocksCleared`;只有曾经进入可发送代际后再断线并达到重试阈值,才发送 reset。 |
| 87 | +- **发送门控**:向 kvcm 转发实时或 replay batch 前必须通过 `EngineHealthCoordinator.wait_ready_epoch()` 获取当前可发送 epoch。 |
| 88 | +- **分阶段 span 计时**:从 event 到手到发送 kvcm 的每个阶段用 span 埋点,可拓展。载体 `EngineEventBatch(batches, timer)` 由 adapter yield:timer 起源于 adapter 开始处理该 event,adapter 标记自身阶段(live=`decode`、replay=`replay_fetch`),`send_incremental_events` 在同一 timer 上续标 `queue_wait`/`gate_wait`/`block_filter`/`metadata_learn`/`kvcm_send`。加新阶段只需在相应位置多一次 `timer.mark("stage")`。span 上报是 best-effort,`StageTimer.mark/spans` 与 `report()` 契约永不抛异常,绝不阻断转发主路径。 |
| 89 | +- **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 ImportError` fallback)。新增或修改指标后必须同步更新 `docs/metrics.json` 指标目录。 |
| 90 | +- **Replay 语义**:检测到 seq gap 时,先聚合所有 replay batch 一次性 yield,再 yield 当前实时消息。replay 使用 DEALER socket(非 REQ),避免 async 下严格一问一答的阻塞问题。 |
| 91 | +- **配置优先级**:CLI 显式参数 > `--config yaml` 文件 > dataclass 默认值。 |
| 92 | +- **Lint/类型检查禁用规则**:禁止通过内联注释(如 `# noqa`、`# type: ignore`)禁用 ruff 或 mypy 检查。自动生成的代码(如 protobuf 生成文件)应在 `pyproject.toml` 或 `ruff.toml` 中通过 `exclude` / `per-file-ignores` 排除整个文件;只有确实无法通过修改代码解决的极少数情况,才允许使用注释禁用,且必须在注释中说明原因。 |
| 93 | +- **Docstring 规范**:通用接口/模块定义(后续要实现某个接口的 placeholder)、抽象基类的 public 方法、复杂实现逻辑必须有 docstring,说明契约、默认行为和关键不变量。简单的内部 helper 和自解释代码不需要。 |
| 94 | +- **设计文档落盘目录**:所有 spec / 设计文档写入 `docs/superpowers/specs/`,命名格式为 `YYYY-MM-DD_slug.md`(如 `2026-07-21_engine-node-metadata-rpc.md`)。不要在项目根目录或其他位置创建 `specs/` 目录。 |
| 95 | +- **仓库根目录不落运行时日志**:运行时日志不得写入仓库根目录;review / 报告类产物归档到 `docs/superpowers/reviews/`。 |
| 96 | +- **修改后必须通过检查**:所有代码修改完成后,必须确保 `uv run pytest`、`uv run ruff check subscriber/ tests/` 和 `uv run mypy subscriber/` 全部通过,否则修改视为未完成。ruff/mypy 由 pre-commit 钩子在 commit 时强制;pytest 为手动门禁(见「质量门禁」一节)。 |
| 97 | + |
| 98 | +## Health Integration 开发原则 |
| 99 | + |
| 100 | +subscriber 与同机 DashServing "同生同死":subscriber 未 active 时 DashServing 不导流,subscriber 异常时 DashServing 暴露健康失败。详细 spec 见 `docs/superpowers/specs/2026-07-22_subscriber-health-dashserving-integration.md`。 |
| 101 | + |
| 102 | +**核心不变量(修改代码前必须理解):** |
| 103 | + |
| 104 | +- **引擎健康只由 `GetWorkerStatus.alive` 驱动**:不用 HTTP `/readiness` 做引擎健康判断,不用 DashServing 探针反馈 engine DEAD。 |
| 105 | +- **KVCM 是 lossy 旁路,不是门控**:KVCM 不可用不阻塞转发、不影响探针、不改变 subscriber 状态。发送失败丢弃并计数,恢复后自动续传。不要为 KVCM 加 generation/snapshot/condition 等复杂机制。 |
| 106 | +- **seq_id 线性化**:状态上报靠单调递增 seq_id 防乱序。低 seq 的 active 不能复活已接受的 inactive。不要加 session_id 除非需要支持独立重启 subscriber。 |
| 107 | +- **HostDown 幂等**:`AllBlocksCleared` 每个 sendable epoch 最多发一次。冷启动失败不发。 |
| 108 | +- **同生同死探针语义**:starting → readiness 503 / liveness 200(startup grace);active → 200/200;inactive/failed/TTL expired → 503/503。 |
| 109 | +- **环境变量**:`DS_LLM_LAUNCH_KV_EVENT_SUBSCRIBER=1` 时 DashServing 启用 subscriber 健康检查,否则探针行为不变。 |
| 110 | + |
| 111 | +**不要做的事:** |
| 112 | + |
| 113 | +- 不要为引擎重启加 metadata 重新验证(拓扑不变,Pod 替换兜底) |
| 114 | +- 不要轮询 DashServing readiness 来控制转发(`wait_ready_epoch()` 已是正确门控) |
| 115 | +- 不要在 KVCM transport 失败时改 `is_registered`(heartbeat loop 自己管重连) |
| 116 | +- 不要把 learn mode 加回来(已删除) |
| 117 | +- 不要把 transient 启动错误报为 failed(只有 protocol/unsupported 才是 fatal) |
| 118 | + |
| 119 | +## 添加新引擎适配器(参考现有 SGLang 实现) |
| 120 | + |
| 121 | +1. 参考现有 `subscriber/engine/sglang/` 包布局:`adapter.py` 实现 `AbstractEngineAdapter` 并加注册装饰器,`__init__.py` re-export 适配器类: |
| 122 | + |
| 123 | +```python |
| 124 | +# subscriber/engine/<engine>/adapter.py |
| 125 | +from subscriber.engine.base import AbstractEngineAdapter |
| 126 | + |
| 127 | +@AbstractEngineAdapter.register("sglang") |
| 128 | +class SGLangAdapter(AbstractEngineAdapter): |
| 129 | + async def subscribe_kv_events(self): |
| 130 | + ... # 实现事件订阅逻辑 |
| 131 | + |
| 132 | + async def watch_liveness(self): |
| 133 | + ... # 实现引擎无关存活事件流 |
| 134 | +``` |
| 135 | + |
| 136 | +完整可运行示例见 `subscriber/engine/sglang/adapter.py`(placeholder 适配器,含全部必须实现的方法)。 |
| 137 | + |
| 138 | +2. 启动时传入 `--engine-type sglang`,`AbstractEngineAdapter.create()` 会自动 lazy import 并实例化。 |
| 139 | + |
| 140 | +无需修改 `main.py`、`kv_event_loop` 或任何 client 代码。 |
| 141 | + |
| 142 | +## Stub 待接入项 |
| 143 | + |
| 144 | +| 编号 | 内容 | 文件 | 阶段 | |
| 145 | +|---|---|---|---| |
| 146 | +| S-4 | subscriber 不可用恢复策略:kvcm SDK 心跳、TTL、session/checkpoint | kvcm SDK / kvcm 服务端 / subscriber | DONE(heartbeat + TTL + state reporter 已实现;session/checkpoint 见 TODO(independent-restart)) | |
0 commit comments