Skip to content

Commit f69608a

Browse files
committed
[subscriber] add kvcache event subscriber
1 parent b75bdc4 commit f69608a

98 files changed

Lines changed: 22691 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

subscriber/.gitignore

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# === Qoder / local agent state ===
2+
.qoder/
3+
4+
# === 本地环境信息 ===
5+
env.txt
6+
7+
# === Python & uv ===
8+
__pycache__/
9+
*.py[cod]
10+
*$py.class
11+
*.so
12+
*.egg-info/
13+
*.egg
14+
dist/
15+
build/
16+
.eggs/
17+
18+
# uv 虚拟环境与锁文件缓存
19+
.venv/
20+
.python-version
21+
22+
# 测试与覆盖率
23+
.pytest_cache/
24+
.coverage
25+
htmlcov/
26+
.tox/
27+
.nox/
28+
29+
# mypy / ruff / linting 缓存
30+
.mypy_cache/
31+
.ruff_cache/
32+
.dmypy.json
33+
34+
# Jupyter Notebook
35+
.ipynb_checkpoints/
36+
37+
# === PyCharm / JetBrains IDE ===
38+
.idea/
39+
*.iml
40+
*.iws
41+
*.ipr
42+
out/
43+
44+
# === OS ===
45+
.DS_Store
46+
Thumbs.db
47+
48+
# === 环境变量与密钥(切勿提交)===
49+
.env
50+
.env.*
51+
!.env.example
52+
53+
*.log

subscriber/AGENTS.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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)) |

subscriber/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# KVCacheEventSubscriber
2+
3+
A process that subscribes to inference engine's KV cache events and forwards them to the kvcm service.
4+
5+
## Installation
6+
7+
```bash
8+
uv sync
9+
```
10+
11+
## Running
12+
13+
```bash
14+
# With defaults
15+
uv run python -m subscriber
16+
17+
# With CLI args
18+
uv run python -m subscriber --zmq-pub-endpoint tcp://localhost:5557 --kvcm-addr 10.0.0.1:50051
19+
20+
# With config file
21+
uv run python -m subscriber --config config.yaml
22+
```
23+
24+
## Development
25+
26+
```bash
27+
# Run tests
28+
uv run pytest
29+
30+
# Lint
31+
uv run ruff check subscriber/ tests/
32+
33+
# Type check
34+
uv run mypy subscriber/
35+
```
36+
37+
## Protobuf Compatibility Maintenance
38+
39+
Production runs `protobuf==3.20.3`, and code emitted by `grpc_tools.protoc` imports
40+
`google.protobuf.runtime_version` / `_builder`, which cannot load on that runtime.
41+
The Python pb files in `subscriber/proto/` (`_pb2.py`, `_pb2_grpc.py`, `_pb2.pyi`) are
42+
therefore **hand-maintained** — do **not** run `grpc_tools.protoc` and commit its output.
43+
44+
`subscriber/proto/kv_cache_group_metadata.proto` remains the authoritative wire schema.
45+
After changing the `.proto`, manually sync the pb files following the procedure in
46+
[AGENTS.md](AGENTS.md) (section "Protobuf 兼容代码维护"): keep the
47+
`descriptor_pb2` / `message_factory` implementation compatible with protobuf 3.20.3,
48+
never introduce `runtime_version` or `_builder`, and only touch `_pb2_grpc.py` when the
49+
RPC service/method/request/response types change.
50+
51+
The pb files are committed to the repository and are never generated at build time.
52+
After any change, run the proto/client tests and `uv run mypy subscriber/` to confirm
53+
they import cleanly under protobuf 3.20.3.

0 commit comments

Comments
 (0)