feat: add KVCM fallback to master client - #1335
Conversation
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1335
Status: BLOCKING
Summary: P0/0 · P1/2 · P2/13 · P3/3
Reviewed: commit c054ae8c0c2d · 2026-08-26 12:16 UTC+8
Blocking Issues
P1
- KVCM leader 解析在 async 路径内同步阻塞调用 VIPServer,停顿整个前端事件循环 @
rtp_llm/server/master_client.py:194- 建议:把 bootstrap 目标解析移出事件循环:仿
MasterService用后台线程周期刷新、resolver 只读缓存快照;或在_resolve_leader中用run_in_executor包裹并设置明确上限(不超过master_kvcm_request_timeout_ms量级)。无论哪种方案都应给底层 VIP HTTP 调用补显式 timeout。补一条断言「解析器不在事件循环线程内被同步调用」的测试。
- 建议:把 bootstrap 目标解析移出事件循环:仿
- 入口门禁放宽后每请求进入无总预算、无失败负缓存、无熔断的串行 KVCM 探测 @
rtp_llm/server/backend_rpc_server_visitor.py:226- 建议:为 KVCM 兜底设置单请求总预算(leader 解析 + query 共享一个 deadline,取
min(TTFT 剩余, 可配置上限)),避免按 target 线性放大;对 leader 解析失败加入短期负缓存/连续失败熔断,冷却窗口内直接跳过 KVCM 走 domain,使双故障退化为改动前的快速回落。补一条「KVCM 全不可用时仍快速域名兜底」的用例,断言不会退化为逐 target 串行等待。若认为熔断超出本 PR 范围,请在 PR description 中记录该降级时延特征与建议的超时/bootstrap 数量上限。
- 建议:为 KVCM 兜底设置单请求总预算(leader 解析 + query 共享一个 deadline,取
Non-blocking Suggestions
P2
- 生产路径恒转发原生哈希键,vLLM sha256_cbor 键计算与 master_kvcm_block_size 成为死代码 @
rtp_llm/server/master_client.py:274- 建议:明确 KVCM 命名空间的键族约定并只保留一条路径:既然 caller 键优先已是 FlexLB 既有语义,建议删除
calculate_vllm_block_cache_keys与master_kvcm_block_size(或在 help/注释中写明仅供「调用方不提供 block_cache_keys」的非 RTP-LLM 场景,并说明必须与目标池 block size 一致),避免两套键族并存误导。请补一条传入非空block_cache_keys的用例,断言实际下发到GetHostCacheStateRequest.block_cache_keys的取值直接来自上游键而非重算结果。
- 建议:明确 KVCM 命名空间的键族约定并只保留一条路径:既然 caller 键优先已是 FlexLB 既有语义,建议删除
- KVCM 结果硬编码 PREFILL 角色,非 PREFILL 拓扑下注入无效地址且亲和性静默失效 @
rtp_llm/server/master_client.py:320- 建议:按
backend_role_list(或由MasterClient构造时显式传入的目标角色)生成RoleAddr;若当前拓扑不含该角色则直接返回 None 并打一次告警,避免注入无效地址。对 PDFUSION-only 与 DECODE-entrance 两种拓扑各补一条断言最终role_addrs角色集合的用例。
- 建议:按
- 硬编码 QT_PREFIX_MATCH,与 FlexLB 按 role/group 解析 query type 的既有契约不一致 @
rtp_llm/server/kvcm_fallback.py:345- 建议:将 query type 变为可配置(或从 worker 状态/配置推导),至少新增一个 server arg 并在 help 中写明「必须与目标池 query type 一致」;若短期只支持
QT_PREFIX_MATCH,请在master_kvcm_fallback_enabled的 help 与 PR description 中显式声明「仅适用于非 Mamba/混合注意力部署」,并对不匹配情形补一条告警。
- 建议:将 query type 变为可配置(或从 worker 状态/配置推导),至少新增一个 server arg 并在 help 中写明「必须与目标池 query type 一致」;若短期只支持
- block_size 默认 0 使开关一启用即在启动期拉挂 backend,且四个 KVCM 字段未接线 @
rtp_llm/server/server_args/master_group_args.py:80- 建议:把 KVCM 客户端构造失败降级为「记录 error 日志 + 将
kvcm_fallback_enabled置回 False」,保证主链路可启动并保留域名兜底;在_create_kvcm_fallback_client中与其它必填项一起校验 block_size,所有报错带上master_kvcm_block_size/MASTER_KVCM_BLOCK_SIZE,help 注明「启用 KVCM 兜底时必填」。四个未接线字段请补齐 server args(lookahead_tokens需与目标池block_hash_lookahead_tokens对齐)或先删除,并对连续 N 次no_positive_match输出一次聚合 warning 以便发现错配。
- 建议:把 KVCM 客户端构造失败降级为「记录 error 日志 + 将
- HTTP 200 响应体 code=8600 的错误语义与空 master_addr 的 slave 行为被无开关保护地改变 @
rtp_llm/server/master_client.py:519- 建议:在 PR description 中显式说明这两项语义修正及影响面(原本报错的请求将改为降级成功;无 master 地址时会尝试 slave),或拆为独立提交以便单独回滚;确认 FlexLB 侧不存在「200 + 8600 表示硬拒绝」的用法,并为 8600 保留一条日志/指标以观测降级量级。同时建议为
master_flexlb_transport_timeout_ms给出非 0 推荐值,避免 slave 尝试沿用 TTFT 默认超时。
- 建议:在 PR description 中显式说明这两项语义修正及影响面(原本报错的请求将改为降级成功;无 master 地址时会尝试 slave),或拆为独立提交以便单独回滚;确认 FlexLB 侧不存在「200 + 8600 表示硬拒绝」的用法,并为 8600 保留一条日志/指标以观测降级量级。同时建议为
- KVCM 选路无专属指标且三个新增响应字段无生产消费方,降级在监控面不可见 @
rtp_llm/server/backend_rpc_server_visitor.py:162- 建议:在 :162 成功上报时带
route_source标签(与 :148-152 上报error_code标签的既有做法一致),或新增 KVCM 专属 QPS/RT/命中块数指标并把kvcm_outcome(selected/no_positive_match/no_complete_blocks)作为可聚合维度;对候选解析失败输出限频 warning(含原始host_ip_port);把「无 master 地址」与「真实连接失败」在日志级别与文案上区分。若短期不消费这三个字段,则删除以免留下无人维护的半成品结构。
- 建议:在 :162 成功上报时带
- 降级选路完全不感知负载,热点 prefix 会被确定性地打向同一 worker @
rtp_llm/server/kvcm_fallback.py:162- 建议:在
local_blocks接近的候选间引入随机或轮转 tie-break(如对 top-k 候选随机选择),或引入简单的本地并发计数上限;并在 PR description 中记录该降级模式的容量假设与运维回滚手段(关闭开关)。
- 建议:在
- master_kvcm_use_local 场景下 service_id 中填写的端口被静默丢弃 @
rtp_llm/server/master_client.py:196- 建议:use_local 时优先使用条目自带端口,仅在缺省时回落
bootstrap_port;或修正 help 文案为「仅 IP 列表,端口由 master_kvcm_bootstrap_port 决定」并在解析到带端口输入时打 warning。补一条 use_local 的解析用例。
- 建议:use_local 时优先使用条目自带端口,仅在缺省时回落
- 三个新增 py_test 全部标记 manual,且 9 个新 server args 无参数解析测试,本 PR 在 CI 无任何门禁 @
rtp_llm/server/test/BUILD:35- 建议:去掉纯内存的
master_client_fallback_test与backend_rpc_server_visitor_fallback_test的manual;kvcm_fallback_test绑定127.0.0.1:0无外部依赖通常也可运行,若远端执行器不稳可只对它保留 manual 并在 BUILD 注释原因,同时加入 CI 显式执行清单或新增一个非 manual 的test_suite聚合三者。并按现有test_env_vars_set_to_py_env_configs/test_cmd_args_override_env_vars的写法补一组 KVCM 用例,至少覆盖两个str2bool的"true"/"false"解析、整型参数 env 绑定、命令行覆盖 env 的优先级,并断言MasterConfig.to_string()含新增字段名。
- 建议:去掉纯内存的
- 假输入使用 GenerateInput 不存在的 input_ids 字段,生产装配路径与 token_ids 解包分支零覆盖 @
rtp_llm/server/test/master_client_fallback_test.py:125- 建议:
_input()改为提供与生产一致的token_ids(覆盖二维(1, N)与一维两种轻量替身,提供tolist()/shape),断言_input_ids_for_kvcm返回解包后的扁平 int 列表,并保留一条「无 token_ids」用例断言返回 None;同时删除对不存在的input_ids字段的探测与hasattr分支,直接以GenerateInput.token_ids为唯一输入来源。另补一组不注入 fake client 的用例:仅桩掉 vipserver 网络层,断言生成的KvcmFallbackConfig与 bootstrap target 字符串(含 IPv6、use_local),并用assertRaisesRegex逐一覆盖 service_id 空、instance_id 空、bootstrap_port 越界、block_size=0四条启动期失败路径。
- 建议:
- KvcmFallbackClient 的 leader 状态机与全部错误分支零覆盖 @
rtp_llm/server/test/kvcm_fallback_test.py:81- 建议:用可配置返回码的 fake servicer 扩展覆盖:非 OK 码断言抛
KvcmFallbackError;SERVER_NOT_LEADER断言_leader被清空且下次重新GetClusterInfo;servicer 侧await asyncio.sleep超过request_timeout_ms断言抛错且 leader 被失效;bootstrap 全不可达且无历史 leader 时断言抛错;补「先成功、后 bootstrap 全挂」用例锁定 stale-leader 保留语义;不设worker_grpc_port_override时断言grpc_port == http_port + 1;补 IPv6host_ip_port与各条__post_init__校验(用assertRaisesRegex);断言close()可重复调用。
- 建议:用可配置返回码的 fake servicer 扩展覆盖:非 OK 码断言抛
- visitor 测试用桩替换了它声称覆盖的生产边界,影子 FlexlbResponse 已与生产漂移 @
rtp_llm/server/test/backend_rpc_server_visitor_fallback_test.py:56- 建议:直接导入生产
FlexlbResponse(仅依赖RoleAddr,可真实导入),或改用create_autospec(MasterClient)让签名漂移立即暴露;桩的get_backend_role_addrs改为与生产同名同序显式形参并断言收到的关键字。补三条用例:fake client 分别返回connection_failed=True与fallback=True时断言domain_calls == 1且最终地址来自 domain;master_addr非空 + 开关开启行为与关闭时一致;batched 输入断言master_client.calls == 0。
- 建议:直接导入生产
- proto 与 gRPC 生成物三份手工维护,未复用仓内既有 generate_grpc_proto,无漂移校验 @
rtp_llm/server/kvcm_proto/kvcm_meta_service.proto:5- 建议:优先复用
generate_grpc_proto在构建期从单一.proto生成,删除手工提交产物,与rtp_llm/cpp/model_rpc/proto/BUILD保持一致。若因需与 Java 侧共享而必须签入,请把.proto加入某 target 的 srcs,并新增一个非 manual 的一致性测试(用已 pin 的grpc_tools.protoc现场编译并比对 descriptor,同时比对两份.proto语义),并在kvcm_proto/__init__.py写清再生成命令、toolchain 版本与「需改为相对 import」的约束;建议把 Java 侧的字段语义注释(如host_ip_port须匹配WorkerStatus#getIpPort())同步到 Python 副本。
- 建议:优先复用
P3
- 测试在模块导入期全局替换 torch 与整个 rtp_llm 包且不还原 @
rtp_llm/server/test/backend_rpc_server_visitor_fallback_test.py:146- 建议:把桩注入移入
setUpModule或用unittest.mock.patch.dict(sys.modules, ...)在退出时自动还原;或通过 BUILD 依赖引入真实模块、只对MasterClient/HostService做局部 patch,避免替换torch与顶层rtp_llm包。顺带移除未使用的import asyncio。
- 建议:把桩注入移入
- 标注 DO NOT EDIT 的生成物被格式化工具重排,且保留未使用的同步阻塞入口 @
rtp_llm/server/kvcm_proto/kvcm_meta_service_pb2_grpc.py:64- 建议:若采用构建期生成(见 proto 那条建议)则一并解决;若继续签入手工版本,删除未使用的
MetaService实验类,仅保留MetaServiceStub、MetaServiceServicer与add_MetaServiceServicer_to_server;并把rtp_llm/server/kvcm_proto/加入 pre-commit exclude 后提交 protoc 原始输出,或在kvcm_proto/__init__.py记录「protoc 输出 + 相对 import 改写 + black 格式化」的完整再生流水线。
- 建议:若采用构建期生成(见 proto 那条建议)则一并解决;若继续签入手工版本,删除未使用的
- 门禁把 KVCM 开关硬编码进中心路由逻辑,且兜底告警文案语义含混 @
rtp_llm/server/backend_rpc_server_visitor.py:240- 建议:在
MasterClient上提供语义化谓词(如can_route_without_master_addr()),visitor 调用bool(master_addr) or self.master_client.can_route_without_master_addr(),使新增兜底机制只需扩展MasterClient;日志改为先判定原因再输出(如reason="no_master_route_target"/reason="batched_input"),把三个变量作为结构化上下文,并评估把 batched 这一可预期分支降级为 debug 或采样以减少热路径噪声。
- 建议:在
Checklist Findings (23 fail / 48 total)
General Principles Checklist
- [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
三个新增 py_test 全部标记 manual,且 9 个新 server args 无参数解析测试,本 PR 在 CI 无任何门禁
kvcm_fallback_test(:35)、master_client_fallback_test(:45)、backend_rpc_server_visitor_fallback_test(:54)均带tags = ["manual"],Bazel 语义下会被//.../:all排除,全仓也无test_suite或脚本按名列出这三个 target(需说明:同包schedule_meta_test、host_service_test亦为 manual,仅vit_proxy_server_test未带,说明非 manual 可行)。而本 PR 同时变更了默认路径行为(:226 门禁、:491 KVCM 分支、:519 的 8600 语义),恰好守护这些变更的test_switch_off_preserves_direct_domain_fallback/test_explicit_8600_does_not_query_kvcm永不在 CI 执行。唯一非 manual 的 `server_args/test/s - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
门禁把 KVCM 开关硬编码进中心路由逻辑,且兜底告警文案语义含混
can_attempt_master_route = bool(master_addr) or bool(self.master_client.kvcm_fallback_enabled)(:226)让 visitor 直接依赖MasterClient的具体实现细节,而它真正关心的语义只是「无 master 地址时该 client 是否仍能给出路由」;未来新增任何 master 无关的兜底都要回改这处中心门禁,且 visitor 已持有self.master_config,形成同一开关的两条可达路径。紧随的 warning(:240-246)实际触发条件是not can_attempt_master_route or input_token_batched,但文案为 "master address: %s, KVCM fallback enabled: %s, or input token batched: %s is not valid",当仅因 batched 输入进入时会输出 "KVCM fallback enabled: False ... is not - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
降级选路完全不感知负载,热点 prefix 会被确定性地打向同一 worker
select_max_local_affinity以(-local_blocks, host_ip_port)取min,即「最大 local 命中 + 字典序确定性 tie-break」,完全不考虑 worker 负载或队列长度。该路径恰在 FlexLB 整体不可用时才启用,此时全部流量都由它决策;对共享 system prompt 的典型业务,绝大多数请求的最大命中 worker 相同,会被稳定集中到同一台机器,在降级期造成单点过载,反而放大故障。模块 docstring 明确「不重建 FlexLB 负载核算」,但没有任何分散、限流或熔断措施。 - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
KvcmFallbackClient 的 leader 状态机与全部错误分支零覆盖
_FakeMetaService的两个 RPC(:37-56)恒返回kvcm_pb2.OK,KvcmFallbackClientTest只有「命中」与「无正向匹配」两个用例。新模块中风险最高的有状态逻辑因此完全未执行:_resolve_leader的双重检查缓存与asyncio.Lock(kvcm_fallback.py:247-284)、刷新失败保留previous_leader(:281-283)、_invalidate_leader(:286)、SERVER_NOT_LEADER触发失效(:360)、非 OK 状态码抛KvcmFallbackError、RPC 超时路径(:354)、close()后_stub_for抛错(:230)与close()幂等、worker_grpc_port_override为 None 时的http_port + 1推导、_format_target的 IPv6 分支(:200-209)、__post_init__的 7 条校验(:42-60)。K - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
KvcmFallbackClient 的 leader 状态机与全部错误分支零覆盖
_FakeMetaService的两个 RPC(:37-56)恒返回kvcm_pb2.OK,KvcmFallbackClientTest只有「命中」与「无正向匹配」两个用例。新模块中风险最高的有状态逻辑因此完全未执行:_resolve_leader的双重检查缓存与asyncio.Lock(kvcm_fallback.py:247-284)、刷新失败保留previous_leader(:281-283)、_invalidate_leader(:286)、SERVER_NOT_LEADER触发失效(:360)、非 OK 状态码抛KvcmFallbackError、RPC 超时路径(:354)、close()后_stub_for抛错(:230)与close()幂等、worker_grpc_port_override为 None 时的http_port + 1推导、_format_target的 IPv6 分支(:200-209)、__post_init__的 7 条校验(:42-60)。K - [6.1] Quality — Mega-PR 已拆分为独立变更 → issue
HTTP 200 响应体 code=8600 的错误语义与空 master_addr 的 slave 行为被无开关保护地改变
diff 确认+ if code == FALLBACK_ERROR_CODE: return FlexlbResponse.fallback_response()为本 PR 在 HTTP-200 响应体分支新增(非 200 分支的同名判断为改动前既有)。config/exceptions.py中不存在 8600(仅MASTER_NO_AVAILABLE_WORKER = 8400、ROUTE_ERROR = 8500),故改动前该情形会经ExceptionType(8600)的 ValueError 退化为MASTER_NO_AVAILABLE_WORKER并raise FtRuntimeException,请求直接失败;改动后静默转为 domain routing 成功返回。同一函数还删除了if not master_addr: return connection_failed_response()早返回,使「无 master 地址但有 slave 地址」时新增一次 slave HTTP 调度请求。两项都不受 `master_k - [6.1] Quality — PR description 说明动机与设计 → issue
HTTP 200 响应体 code=8600 的错误语义与空 master_addr 的 slave 行为被无开关保护地改变
diff 确认+ if code == FALLBACK_ERROR_CODE: return FlexlbResponse.fallback_response()为本 PR 在 HTTP-200 响应体分支新增(非 200 分支的同名判断为改动前既有)。config/exceptions.py中不存在 8600(仅MASTER_NO_AVAILABLE_WORKER = 8400、ROUTE_ERROR = 8500),故改动前该情形会经ExceptionType(8600)的 ValueError 退化为MASTER_NO_AVAILABLE_WORKER并raise FtRuntimeException,请求直接失败;改动后静默转为 domain routing 成功返回。同一函数还删除了if not master_addr: return connection_failed_response()早返回,使「无 master 地址但有 slave 地址」时新增一次 slave HTTP 调度请求。两项都不受 `master_k - [6.1] Quality — 无 per-forward 调试日志 / 噪声热路径输出 → issue
门禁把 KVCM 开关硬编码进中心路由逻辑,且兜底告警文案语义含混
can_attempt_master_route = bool(master_addr) or bool(self.master_client.kvcm_fallback_enabled)(:226)让 visitor 直接依赖MasterClient的具体实现细节,而它真正关心的语义只是「无 master 地址时该 client 是否仍能给出路由」;未来新增任何 master 无关的兜底都要回改这处中心门禁,且 visitor 已持有self.master_config,形成同一开关的两条可达路径。紧随的 warning(:240-246)实际触发条件是not can_attempt_master_route or input_token_batched,但文案为 "master address: %s, KVCM fallback enabled: %s, or input token batched: %s is not valid",当仅因 batched 输入进入时会输出 "KVCM fallback enabled: False ... is not - [6.1] Quality — 逻辑变更未混入无关格式化 → issue
标注 DO NOT EDIT 的生成物被格式化工具重排,且保留未使用的同步阻塞入口
文件首行声明DO NOT EDIT!,但内容是 black 风格(双引号、尾随逗号、逐行换行),与 protoc 原始输出不一致;.pre-commit-config.yaml:31的全局 exclude 仅^rtp_llm/ops|^3rdparty,未排除rtp_llm/server/kvcm_proto/,因此每次重生成都会再被改写,评审者无法与 protoc 输出做字节比对。:64-124的MetaService实验类基于grpc.experimental.unary_unary(进程级全局 channel 缓存、同步阻塞),全仓无调用点(唯一消费者kvcm_fallback.py:242走MetaServiceStub+grpc.aio),一旦有人在 frontend 的 async 路径误用会阻塞事件循环。 - [6.1] Software Engineering — DIP:高层策略不依赖非必要具体细节 → issue
门禁把 KVCM 开关硬编码进中心路由逻辑,且兜底告警文案语义含混
can_attempt_master_route = bool(master_addr) or bool(self.master_client.kvcm_fallback_enabled)(:226)让 visitor 直接依赖MasterClient的具体实现细节,而它真正关心的语义只是「无 master 地址时该 client 是否仍能给出路由」;未来新增任何 master 无关的兜底都要回改这处中心门禁,且 visitor 已持有self.master_config,形成同一开关的两条可达路径。紧随的 warning(:240-246)实际触发条件是not can_attempt_master_route or input_token_batched,但文案为 "master address: %s, KVCM fallback enabled: %s, or input token batched: %s is not valid",当仅因 batched 输入进入时会输出 "KVCM fallback enabled: False ... is not - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
proto 与 gRPC 生成物三份手工维护,未复用仓内既有 generate_grpc_proto,无漂移校验
同一 wire 契约在仓内有三份副本:本目录.proto、手工签入的_pb2.py/_pb2_grpc.py,以及flexlb-grpc/src/main/proto/kvcm_meta_service.proto(我已逐项核对当前一致,风险在后续维护),约束仅靠:5-6的注释。rtp_llm/server/BUILD的 glob 只收kvcm_proto/*.py,.proto未参与任何构建规则。仓内已有统一做法:rtp_llm/cpp/model_rpc/proto/BUILD:2,21用//bazel:py_proto.bzl的generate_grpc_proto在构建期生成 py 产物,grpcio-tools依赖已就绪。签入的 gencode 标注 Protobuf 4.25.1,与仓内 pin 的grpcio-tools==1.57.0不一致,无法用仓内工具链复现;_pb2_grpc.py:5的相对 import 还需每次重生成后手工回补。三重人工同步无任何构建期或 CI 校验,字段 - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
标注 DO NOT EDIT 的生成物被格式化工具重排,且保留未使用的同步阻塞入口
文件首行声明DO NOT EDIT!,但内容是 black 风格(双引号、尾随逗号、逐行换行),与 protoc 原始输出不一致;.pre-commit-config.yaml:31的全局 exclude 仅^rtp_llm/ops|^3rdparty,未排除rtp_llm/server/kvcm_proto/,因此每次重生成都会再被改写,评审者无法与 protoc 输出做字节比对。:64-124的MetaService实验类基于grpc.experimental.unary_unary(进程级全局 channel 缓存、同步阻塞),全仓无调用点(唯一消费者kvcm_fallback.py:242走MetaServiceStub+grpc.aio),一旦有人在 frontend 的 async 路径误用会阻塞事件循环。 - [6.1] Software Engineering — OCP:本地扩展点优先于修改中心逻辑 → issue
门禁把 KVCM 开关硬编码进中心路由逻辑,且兜底告警文案语义含混
can_attempt_master_route = bool(master_addr) or bool(self.master_client.kvcm_fallback_enabled)(:226)让 visitor 直接依赖MasterClient的具体实现细节,而它真正关心的语义只是「无 master 地址时该 client 是否仍能给出路由」;未来新增任何 master 无关的兜底都要回改这处中心门禁,且 visitor 已持有self.master_config,形成同一开关的两条可达路径。紧随的 warning(:240-246)实际触发条件是not can_attempt_master_route or input_token_batched,但文案为 "master address: %s, KVCM fallback enabled: %s, or input token batched: %s is not valid",当仅因 batched 输入进入时会输出 "KVCM fallback enabled: False ... is not - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
三个新增 py_test 全部标记 manual,且 9 个新 server args 无参数解析测试,本 PR 在 CI 无任何门禁
kvcm_fallback_test(:35)、master_client_fallback_test(:45)、backend_rpc_server_visitor_fallback_test(:54)均带tags = ["manual"],Bazel 语义下会被//.../:all排除,全仓也无test_suite或脚本按名列出这三个 target(需说明:同包schedule_meta_test、host_service_test亦为 manual,仅vit_proxy_server_test未带,说明非 manual 可行)。而本 PR 同时变更了默认路径行为(:226 门禁、:491 KVCM 分支、:519 的 8600 语义),恰好守护这些变更的test_switch_off_preserves_direct_domain_fallback/test_explicit_8600_does_not_query_kvcm永不在 CI 执行。唯一非 manual 的 `server_args/test/s - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
proto 与 gRPC 生成物三份手工维护,未复用仓内既有 generate_grpc_proto,无漂移校验
同一 wire 契约在仓内有三份副本:本目录.proto、手工签入的_pb2.py/_pb2_grpc.py,以及flexlb-grpc/src/main/proto/kvcm_meta_service.proto(我已逐项核对当前一致,风险在后续维护),约束仅靠:5-6的注释。rtp_llm/server/BUILD的 glob 只收kvcm_proto/*.py,.proto未参与任何构建规则。仓内已有统一做法:rtp_llm/cpp/model_rpc/proto/BUILD:2,21用//bazel:py_proto.bzl的generate_grpc_proto在构建期生成 py 产物,grpcio-tools依赖已就绪。签入的 gencode 标注 Protobuf 4.25.1,与仓内 pin 的grpcio-tools==1.57.0不一致,无法用仓内工具链复现;_pb2_grpc.py:5的相对 import 还需每次重生成后手工回补。三重人工同步无任何构建期或 CI 校验,字段 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
visitor 测试用桩替换了它声称覆盖的生产边界,影子 FlexlbResponse 已与生产漂移
测试把rtp_llm.server.master_client整体替换为桩:FlexlbResponse是本地 dataclass(:55-65),只有 5 个字段,缺本 PR 新增的route_source/cache_match/kvcm_outcome,形成需人工同步的影子契约;MasterClient=object(:125);_MasterClient.get_backend_role_addrs(self, **_kwargs)(:183)吞掉全部形参名,而生产以block_cache_keys=/input=/request_id=关键字调用(backend_rpc_server_visitor.py:141-145),任一形参改名都能让测试全绿而生产TypeError。_visitor()还用__new__绕过__init__只赋 5 个属性。仅 2 个用例,缺失最关键的不变量:开关开启且 KVCM 返回connection_failed=True/fallback=True
RTP-LLM Checklist
- [I] 代码质量 — 同一功能用统一工具函数 → issue
proto 与 gRPC 生成物三份手工维护,未复用仓内既有 generate_grpc_proto,无漂移校验
同一 wire 契约在仓内有三份副本:本目录.proto、手工签入的_pb2.py/_pb2_grpc.py,以及flexlb-grpc/src/main/proto/kvcm_meta_service.proto(我已逐项核对当前一致,风险在后续维护),约束仅靠:5-6的注释。rtp_llm/server/BUILD的 glob 只收kvcm_proto/*.py,.proto未参与任何构建规则。仓内已有统一做法:rtp_llm/cpp/model_rpc/proto/BUILD:2,21用//bazel:py_proto.bzl的generate_grpc_proto在构建期生成 py 产物,grpcio-tools依赖已就绪。签入的 gencode 标注 Protobuf 4.25.1,与仓内 pin 的grpcio-tools==1.57.0不一致,无法用仓内工具链复现;_pb2_grpc.py:5的相对 import 还需每次重生成后手工回补。三重人工同步无任何构建期或 CI 校验,字段
Python Static-First Checklist
- [P.A] 静态结构与类型纪律 — 禁止 getattr/setattr literal 访问 → issue
假输入使用 GenerateInput 不存在的 input_ids 字段,生产装配路径与 token_ids 解包分支零覆盖
_input()构造SimpleNamespace(..., input_ids=list(range(12)), ...),但真实GenerateInput(utils/base_model_datatypes.py:53-64)只有token_ids: torch.Tensor,没有input_ids(该字段属于GenerateOutput)。_input_ids_for_kvcm(master_client.py:244-262)先getattr(input, "input_ids", None),故 6 个用例命中的是生产恒为 None 的分支;生产实际走token_ids,需.tolist()、二维[[...]]解包、int()强转三步,全部无测试。同时 6 个用例全部注入kvcm_fallback_client,使_create_kvcm_fallback_client(约 60 行,含三条启动期raise、VipServerWrapper构造、IPv6 方括号拼 - [P.A] 静态结构与类型纪律 — 禁止 hasattr 做控制流分支 → issue
假输入使用 GenerateInput 不存在的 input_ids 字段,生产装配路径与 token_ids 解包分支零覆盖
_input()构造SimpleNamespace(..., input_ids=list(range(12)), ...),但真实GenerateInput(utils/base_model_datatypes.py:53-64)只有token_ids: torch.Tensor,没有input_ids(该字段属于GenerateOutput)。_input_ids_for_kvcm(master_client.py:244-262)先getattr(input, "input_ids", None),故 6 个用例命中的是生产恒为 None 的分支;生产实际走token_ids,需.tolist()、二维[[...]]解包、int()强转三步,全部无测试。同时 6 个用例全部注入kvcm_fallback_client,使_create_kvcm_fallback_client(约 60 行,含三条启动期raise、VipServerWrapper构造、IPv6 方括号拼 - [P.C] 并发与异步 — async def 中禁止 blocking 调用 → issue
标注 DO NOT EDIT 的生成物被格式化工具重排,且保留未使用的同步阻塞入口
文件首行声明DO NOT EDIT!,但内容是 black 风格(双引号、尾随逗号、逐行换行),与 protoc 原始输出不一致;.pre-commit-config.yaml:31的全局 exclude 仅^rtp_llm/ops|^3rdparty,未排除rtp_llm/server/kvcm_proto/,因此每次重生成都会再被改写,评审者无法与 protoc 输出做字节比对。:64-124的MetaService实验类基于grpc.experimental.unary_unary(进程级全局 channel 缓存、同步阻塞),全仓无调用点(唯一消费者kvcm_fallback.py:242走MetaServiceStub+grpc.aio),一旦有人在 frontend 的 async 路径误用会阻塞事件循环。 - [P.C] 并发与异步 — async 代码用 asyncio.Lock 非 threading.Lock → issue
KVCM leader 解析在 async 路径内同步阻塞调用 VIPServer,停顿整个前端事件循环
resolve_bootstrap_targets调用self._kvcm_vip.get_hosts(refresh=True);非 use_local 时经host_service.py:120→get_host_list_by_domain_now→host_reactor.refresh_domain_srv_lst→vipserver_proxy.req_api,后者先srv_update_lock.acquire()(vipserver_proxy.py:30的类级threading.Lock,与后台刷新线程共享),再在:106-108执行未设 timeout 的requests.get。该闭包被kvcm_fallback.py:258在async def _resolve_leader内、且持有asyncio.Lock时直接同步调用。master_kvcm_request_timeout_ms只约束 gRPC deadline,无法约束这段阻塞。leader 过 - [P.F] 语言陷阱 — 禁止模块级 import 副作用 → issue
测试在模块导入期全局替换 torch 与整个 rtp_llm 包且不还原
_load_visitor_module()在模块顶层被直接调用(:146),把torch(:92)与rtp_llm、rtp_llm.server、rtp_llm.config、rtp_llm.cpp、rtp_llm.metrics、rtp_llm.utils(:21-32)连同rtp_llm.ops、rtp_llm.server.host_service、rtp_llm.server.master_client写入全局sys.modules且退出不恢复;master_client_fallback_test.py:52-63同样把VipServerWrapper桩为object。Bazel 下每 target 独立进程故 CI 无跨目标影响,但该文件名按字母序排在两个兄弟测试之前,本地以 pytest/unittest discover 在同一进程批量收集时,后续导入真实模块的测试会拿到桩对象,产生顺序相关失败或静默测桩。另:1的import asyncio全文未使用,触发 flake8 - [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
visitor 测试用桩替换了它声称覆盖的生产边界,影子 FlexlbResponse 已与生产漂移
测试把rtp_llm.server.master_client整体替换为桩:FlexlbResponse是本地 dataclass(:55-65),只有 5 个字段,缺本 PR 新增的route_source/cache_match/kvcm_outcome,形成需人工同步的影子契约;MasterClient=object(:125);_MasterClient.get_backend_role_addrs(self, **_kwargs)(:183)吞掉全部形参名,而生产以block_cache_keys=/input=/request_id=关键字调用(backend_rpc_server_visitor.py:141-145),任一形参改名都能让测试全绿而生产TypeError。_visitor()还用__new__绕过__init__只赋 5 个属性。仅 2 个用例,缺失最关键的不变量:开关开启且 KVCM 返回connection_failed=True/fallback=True
Strengths
- 兜底触发边界收得很准:仅
resp.connection_failed(可用性故障)才查询 KVCM(master_client.py:491),显式 8600 与业务/准入错误(8400)保持原语义,并有test_flexlb_business_error_does_not_query_kvcm/test_explicit_8600_does_not_query_kvcm断言kvcm_client.calls == 0。 - 全部新开关默认保持旧行为(
master_kvcm_fallback_enabled=False、master_flexlb_transport_timeout_ms=0显式保留 TTFT 语义),kvcm_fallback为惰性 import,关闭时完全不加载新 protobuf/gRPC 栈,回滚只需关开关。 - KVCM 失败不放大故障:
_try_kvcm_fallback捕获异常返回 None,route_ips的allow_domain_fallback保证最终仍回落 domain routing,并有test_kvcm_failure_preserves_final_caller_fallback覆盖。 - 跨语言 wire 契约逐项核对无漂移:
kvcm_proto/kvcm_meta_service.proto与flexlb-grpc/src/main/proto/kvcm_meta_service.proto的 package(kv_cache_manager.proto.meta)、字段号(block_cache_keys=4、p2p_host_count=6、meta_rpc_port=3)、枚举值(IO_ERROR=20、ERROR_MAX=65535、QT_PREFIX_MATCH_WITH_MAMBA=4)与两个 RPC 完全一致;pb2_grpc 两条 RPC 全路径与 servicer 注册名一致,Stub/Servicer 两侧序列化器方向未写反。 - 哈希实现有跨语言 golden 锚点:
kvcm_fallback_test.py:62的2164874634404590027与 JavaBlockCacheKeyCalculatorTest.java:60完全一致,并覆盖「丢弃不完整尾块」与 lookahead 分支。 - 测试不是纯 mock:
kvcm_fallback_test起真实grpc.aio.server()绑定回环端口做端到端 round-trip,并断言instance_id、QT_PREFIX_MATCH、block_cache_keys三项请求字段与 channel 复用。 - gRPC 客户端细节扎实:leader 缓存 + 双重检查锁、刷新失败保留上一个可用 leader、
SERVER_NOT_LEADER/RPC 失败时失效 leader、channel 复用、IPv6 authority 正确加方括号、(-local_blocks, host_ip_port)确定性 tie-break。 - 配置与打包接线闭环:
MasterConfig新增 10 个字段与master_group_args.py9 个参数的默认值/env/bind_to 逐项一致,to_string()同步补齐;kvcm_proto/*.py已纳入//rtp_llm/server:server的 srcs glob,不存在「本地可 import、wheel 缺文件」缺口。
|
|
||
| master_route_result: Optional[FlexlbResponse] = None | ||
| if not role_addrs_specified and master_addr and not input_token_batched: | ||
| can_attempt_master_route = bool(master_addr) or bool( |
There was a problem hiding this comment.
[P1] 入口门禁放宽后每请求进入无总预算、无失败负缓存、无熔断的串行 KVCM 探测
改动前 master_addr 为空即直接 domain 路由,零额外开销(diff 中 - if not role_addrs_specified and master_addr and not input_token_batched:)。改后只要 kvcm_fallback_enabled 为真(冷启动、master 域名未配、FlexLB 全不健康时 master_addr 恒空),每请求都会进入 get_master_route_addrs:先做全量 token 哈希,再由 _resolve_leader(kvcm_fallback.py:253-284)在 asyncio.Lock 内串行遍历全部 bootstrap target,每个各按 request_timeout_ms(默认 100ms)超时后 continue;全部失败且无 previous_leader 时抛错且不更新 _leader_refreshed_at,即无失败负缓存,下一请求完整重跑,高 QPS 下还会在锁上排队串行化。N 个 boo...
建议: 为 KVCM 兜底设置单请求总预算(leader 解析 + query 共享一个 deadline,取 min(TTFT 剩余, 可配置上限)),避免按 target 线性放大;对 leader 解析失败加入短期负缓存/连续失败熔断,冷却窗口内直接跳过 KVCM 走 domain,使双故障退化为改动前的快速回落。补一条「KVCM 全不可用时仍快速域名兜底」的用例,断言不会退化为逐 target 串行等待。若认为熔断超出本 PR 范围,请在 PR description 中记录该降级时延特征与建议的超时/bootstrap 数量上限。
| return None | ||
|
|
||
| try: | ||
| result = await self._kvcm_fallback_client.query_and_select( |
There was a problem hiding this comment.
[P2] 生产路径恒转发原生哈希键,vLLM sha256_cbor 键计算与 master_kvcm_block_size 成为死代码
_try_kvcm_fallback 把上游 block_cache_keys 原样传入 query_and_select;该键来自 backend_rpc_server_visitor.py:138 的 get_block_cache_keys,底层为 C++ hashInt64Vector(cpp/utils/HashUtil.h:25,Jenkins 滚动哈希,非 SHA256)。复核后 KVCM 并非只认 vLLM 键:RequestBlockHashService.java:49-53 明确 caller 提供的 keys 优先,CacheMatchQueryOrchestrator.java:84 把该批 caller 键原样送入 KvcmGrpcClient.findMatchingEngines,VllmBlockHashStrategy 仅在调用方只给 input_ids 时兜底,故本 PR 转发语义与 Java 侧一致。残留风险是 kvcm_fallback.py:326 的 vLLM 分支与 `master...
建议: 明确 KVCM 命名空间的键族约定并只保留一条路径:既然 caller 键优先已是 FlexLB 既有语义,建议删除 calculate_vllm_block_cache_keys 与 master_kvcm_block_size(或在 help/注释中写明仅供「调用方不提供 block_cache_keys」的非 RTP-LLM 场景,并说明必须与目标池 block size 一致),避免两套键族并存误导。请补一条传入非空 block_cache_keys 的用例,断言实际下发到 GetHostCacheStateRequest.block_cache_keys 的取值直接来自上游键而非重算结果。
| return FlexlbResponse.ok( | ||
| [ | ||
| RoleAddr( | ||
| role=RoleType.PREFILL, |
There was a problem hiding this comment.
[P2] KVCM 结果硬编码 PREFILL 角色,非 PREFILL 拓扑下注入无效地址且亲和性静默失效
_try_kvcm_fallback 恒构造 RoleAddr(role=RoleType.PREFILL, ...),与部署角色无关。但 get_backend_role_list(backend_rpc_server_visitor.py:100-123)可产出 [DECODE]、[PDFUSION] 或 [PREFILL, DECODE]。以 PDFUSION-only 前端为例:KVCM 成功后 specified_roles={PREFILL},need_domain_routing 为 True;又因成功时 get_master_route_addrs 返回 None 使 allow_domain_fallback 为 True,get_domain_route_addrs 按 missing_roles 再补入 PDFUSION。最终 role_addrs 为 [PREFILL(KVCM), PDFUSION(VIP)]:真实流量走 VIP 选出的 PDFUSION,缓存亲和完全失效,同时多出一条该拓扑不存...
建议: 按 backend_role_list(或由 MasterClient 构造时显式传入的目标角色)生成 RoleAddr;若当前拓扑不含该角色则直接返回 None 并打一次告警,避免注入无效地址。对 PDFUSION-only 与 DECODE-entrance 两种拓扑各补一条断言最终 role_addrs 角色集合的用例。
| request = kvcm_pb2.GetHostCacheStateRequest( | ||
| trace_id=request_id, | ||
| instance_id=self.config.instance_id, | ||
| query_type=kvcm_pb2.QT_PREFIX_MATCH, |
There was a problem hiding this comment.
[P2] 硬编码 QT_PREFIX_MATCH,与 FlexLB 按 role/group 解析 query type 的既有契约不一致
query_and_select 固定 query_type=kvcm_pb2.QT_PREFIX_MATCH。同一 KVCM 契约的既有消费方在 Java 侧是按角色/分组从 worker 元数据解析的:KvcmWorkerMetadataResolver.resolveQueryType(roleType, group) 对 WITH_MAMBA 返回 QT_PREFIX_MATCH_WITH_MAMBA(proto 中 QueryType=4),且 KvcmGrpcClient:152-157 在解析不到时跳过查询而非用默认值。对混合注意力/Mamba 类部署,Python 兜底会以错误的 query type 查询同一 namespace,返回结果与 FlexLB 正常路径不一致,且失败表现为静默零命中,无任何日志可区分。
建议: 将 query type 变为可配置(或从 worker 状态/配置推导),至少新增一个 server arg 并在 help 中写明「必须与目标池 query type 一致」;若短期只支持 QT_PREFIX_MATCH,请在 master_kvcm_fallback_enabled 的 help 与 PR description 中显式声明「仅适用于非 Mamba/混合注意力部署」,并对不匹配情形补一条告警。
| ) | ||
|
|
||
| master_group.add_argument( | ||
| "--master_kvcm_block_size", |
There was a problem hiding this comment.
[P2] block_size 默认 0 使开关一启用即在启动期拉挂 backend,且四个 KVCM 字段未接线
master_kvcm_block_size 默认 0(master_group_args.py:84、py_config_modules.py:394),参数层无校验;_create_kvcm_fallback_client:176-185 只校验 service_id/instance_id/bootstrap_port,block_size 的校验落到 KvcmFallbackConfig.__post_init__:45 抛 ValueError("KVCM block_size must be positive")(不含参数名)。该构造在 MasterClient.__init__:148 同步执行,而 MasterClient 由 backend_rpc_server_visitor.py:78 创建,异常直接终止 backend 启动——按 help 文案只配 enabled + service_id + instance_id 即触发。另 lookahead_tokens、minimum_local_blocks、`le...
建议: 把 KVCM 客户端构造失败降级为「记录 error 日志 + 将 kvcm_fallback_enabled 置回 False」,保证主链路可启动并保留域名兜底;在 _create_kvcm_fallback_client 中与其它必填项一起校验 block_size,所有报错带上 master_kvcm_block_size / MASTER_KVCM_BLOCK_SIZE,help 注明「启用 KVCM 兜底时必填」。四个未接线字段请补齐 server args(lookahead_tokens 需与目标池 block_hash_lookahead_tokens 对齐)或先删除,并对连续 N 次 no_positive_match 输出一次聚合 warning 以便发现错配。
| code = int(raw_code) | ||
| except (TypeError, ValueError): | ||
| code = int(ExceptionType.MASTER_NO_AVAILABLE_WORKER) | ||
| if code == FALLBACK_ERROR_CODE: |
There was a problem hiding this comment.
[P2] HTTP 200 响应体 code=8600 的错误语义与空 master_addr 的 slave 行为被无开关保护地改变
diff 确认 + if code == FALLBACK_ERROR_CODE: return FlexlbResponse.fallback_response() 为本 PR 在 HTTP-200 响应体分支新增(非 200 分支的同名判断为改动前既有)。config/exceptions.py 中不存在 8600(仅 MASTER_NO_AVAILABLE_WORKER = 8400、ROUTE_ERROR = 8500),故改动前该情形会经 ExceptionType(8600) 的 ValueError 退化为 MASTER_NO_AVAILABLE_WORKER 并 raise FtRuntimeException,请求直接失败;改动后静默转为 domain routing 成功返回。同一函数还删除了 if not master_addr: return connection_failed_response() 早返回,使「无 master 地址但有 slave 地址」时新增一次 slave HTTP 调度请求。两项都不受 `maste...
建议: 在 PR description 中显式说明这两项语义修正及影响面(原本报错的请求将改为降级成功;无 master 地址时会尝试 slave),或拆为独立提交以便单独回滚;确认 FlexLB 侧不存在「200 + 8600 表示硬拒绝」的用法,并为 8600 保留一条日志/指标以观测降级量级。同时建议为 master_flexlb_transport_timeout_ms 给出非 0 推荐值,避免 slave 尝试沿用 TTFT 默认超时。
Checklist: [6.1] Mega-PR 已拆分为独立变更;[6.1] PR description 说明动机与设计
| grpc_port: int | ||
|
|
||
| @dataclass | ||
| class FlexlbResponse: |
There was a problem hiding this comment.
[P2] visitor 测试用桩替换了它声称覆盖的生产边界,影子 FlexlbResponse 已与生产漂移
测试把 rtp_llm.server.master_client 整体替换为桩:FlexlbResponse 是本地 dataclass(:55-65),只有 5 个字段,缺本 PR 新增的 route_source/cache_match/kvcm_outcome,形成需人工同步的影子契约;MasterClient=object(:125);_MasterClient.get_backend_role_addrs(self, **_kwargs)(:183)吞掉全部形参名,而生产以 block_cache_keys=/input=/request_id= 关键字调用(backend_rpc_server_visitor.py:141-145),任一形参改名都能让测试全绿而生产 TypeError。_visitor() 还用 __new__ 绕过 __init__ 只赋 5 个属性。仅 2 个用例,缺失最关键的不变量:开关开启且 KVCM 返回 connection_failed=True/`fallback=Tru...
建议: 直接导入生产 FlexlbResponse(仅依赖 RoleAddr,可真实导入),或改用 create_autospec(MasterClient) 让签名漂移立即暴露;桩的 get_backend_role_addrs 改为与生产同名同序显式形参并断言收到的关键字。补三条用例:fake client 分别返回 connection_failed=True 与 fallback=True 时断言 domain_calls == 1 且最终地址来自 domain;master_addr 非空 + 开关开启行为与关闭时一致;batched 输入断言 master_client.calls == 0。
Checklist: [6.1] 边界 case 覆盖(空、单元素、最大值);[P.G] mock/fake/stub 不得替代本次声称覆盖的生产边界
|
|
||
| package kv_cache_manager.proto.meta; | ||
|
|
||
| // Wire-compatible subset of KVCacheManager's meta_service.proto. Keep this |
There was a problem hiding this comment.
[P2] proto 与 gRPC 生成物三份手工维护,未复用仓内既有 generate_grpc_proto,无漂移校验
同一 wire 契约在仓内有三份副本:本目录 .proto、手工签入的 _pb2.py/_pb2_grpc.py,以及 flexlb-grpc/src/main/proto/kvcm_meta_service.proto(我已逐项核对当前一致,风险在后续维护),约束仅靠 :5-6 的注释。rtp_llm/server/BUILD 的 glob 只收 kvcm_proto/*.py,.proto 未参与任何构建规则。仓内已有统一做法:rtp_llm/cpp/model_rpc/proto/BUILD:2,21 用 //bazel:py_proto.bzl 的 generate_grpc_proto 在构建期生成 py 产物,grpcio-tools 依赖已就绪。签入的 gencode 标注 Protobuf 4.25.1,与仓内 pin 的 grpcio-tools==1.57.0 不一致,无法用仓内工具链复现;_pb2_grpc.py:5 的相对 import 还需每次重生成后手工回补。三重人工同步无任何构建期或 CI 校验...
建议: 优先复用 generate_grpc_proto 在构建期从单一 .proto 生成,删除手工提交产物,与 rtp_llm/cpp/model_rpc/proto/BUILD 保持一致。若因需与 Java 侧共享而必须签入,请把 .proto 加入某 target 的 srcs,并新增一个非 manual 的一致性测试(用已 pin 的 grpc_tools.protoc 现场编译并比对 descriptor,同时比对两份 .proto 语义),并在 kvcm_proto/__init__.py 写清再生成命令、toolchain 版本与「需改为相对 import」的约束;建议把 Java 侧的字段语义注释(如 host_ip_port 须匹配 WorkerStatus#getIpPort())同步到 Python 副本。
Checklist: [6.1] DRY:重复非平凡逻辑被抽取或显式复用;[6.1] 新逻辑有聚焦单测 + 相关集成/smoke 测试;[I] 同一功能用统一工具函数
| return module, RoleAddr, RoleType, FlexlbResponse | ||
|
|
||
|
|
||
| visitor_module, RoleAddr, RoleType, FlexlbResponse = _load_visitor_module() |
There was a problem hiding this comment.
[P3] 测试在模块导入期全局替换 torch 与整个 rtp_llm 包且不还原
_load_visitor_module() 在模块顶层被直接调用(:146),把 torch(:92)与 rtp_llm、rtp_llm.server、rtp_llm.config、rtp_llm.cpp、rtp_llm.metrics、rtp_llm.utils(:21-32)连同 rtp_llm.ops、rtp_llm.server.host_service、rtp_llm.server.master_client 写入全局 sys.modules 且退出不恢复;master_client_fallback_test.py:52-63 同样把 VipServerWrapper 桩为 object。Bazel 下每 target 独立进程故 CI 无跨目标影响,但该文件名按字母序排在两个兄弟测试之前,本地以 pytest/unittest discover 在同一进程批量收集时,后续导入真实模块的测试会拿到桩对象,产生顺序相关失败或静默测桩。另 :1 的 import asyncio 全文未使用,触发 fla...
建议: 把桩注入移入 setUpModule 或用 unittest.mock.patch.dict(sys.modules, ...) 在退出时自动还原;或通过 BUILD 依赖引入真实模块、只对 MasterClient/HostService 做局部 patch,避免替换 torch 与顶层 rtp_llm 包。顺带移除未使用的 import asyncio。
Checklist: [P.F] 禁止模块级 import 副作用
| server.add_generic_rpc_handlers((generic_handler,)) | ||
|
|
||
|
|
||
| # This class is part of an EXPERIMENTAL API. |
There was a problem hiding this comment.
[P3] 标注 DO NOT EDIT 的生成物被格式化工具重排,且保留未使用的同步阻塞入口
文件首行声明 DO NOT EDIT!,但内容是 black 风格(双引号、尾随逗号、逐行换行),与 protoc 原始输出不一致;.pre-commit-config.yaml:31 的全局 exclude 仅 ^rtp_llm/ops|^3rdparty,未排除 rtp_llm/server/kvcm_proto/,因此每次重生成都会再被改写,评审者无法与 protoc 输出做字节比对。:64-124 的 MetaService 实验类基于 grpc.experimental.unary_unary(进程级全局 channel 缓存、同步阻塞),全仓无调用点(唯一消费者 kvcm_fallback.py:242 走 MetaServiceStub + grpc.aio),一旦有人在 frontend 的 async 路径误用会阻塞事件循环。
建议: 若采用构建期生成(见 proto 那条建议)则一并解决;若继续签入手工版本,删除未使用的 MetaService 实验类,仅保留 MetaServiceStub、MetaServiceServicer 与 add_MetaServiceServicer_to_server;并把 rtp_llm/server/kvcm_proto/ 加入 pre-commit exclude 后提交 protoc 原始输出,或在 kvcm_proto/__init__.py 记录「protoc 输出 + 相对 import 改写 + black 格式化」的完整再生流水线。
Checklist: [6.1] 逻辑变更未混入无关格式化;[6.1] KISS/YAGNI:无投机性抽象;[P.C] async def 中禁止 blocking 调用
| GaugeMetrics.MASTER_ROUTE_RT_METRIC, master_route_timer.cost_ms() | ||
| ) | ||
| elif not role_addrs_specified: | ||
| route_logger.warning( |
There was a problem hiding this comment.
[P3] 门禁把 KVCM 开关硬编码进中心路由逻辑,且兜底告警文案语义含混
can_attempt_master_route = bool(master_addr) or bool(self.master_client.kvcm_fallback_enabled)(:226)让 visitor 直接依赖 MasterClient 的具体实现细节,而它真正关心的语义只是「无 master 地址时该 client 是否仍能给出路由」;未来新增任何 master 无关的兜底都要回改这处中心门禁,且 visitor 已持有 self.master_config,形成同一开关的两条可达路径。紧随的 warning(:240-246)实际触发条件是 not can_attempt_master_route or input_token_batched,但文案为 "master address: %s, KVCM fallback enabled: %s, or input token batched: %s is not valid",当仅因 batched 输入进入时会输出 "KVCM fallback enabled: False ... is ...
建议: 在 MasterClient 上提供语义化谓词(如 can_route_without_master_addr()),visitor 调用 bool(master_addr) or self.master_client.can_route_without_master_addr(),使新增兜底机制只需扩展 MasterClient;日志改为先判定原因再输出(如 reason="no_master_route_target" / reason="batched_input"),把三个变量作为结构化上下文,并评估把 batched 这一可预期分支降级为 debug 或采样以减少热路径噪声。
Checklist: [6.1] 可观测性:日志/指标/超时可操作、非噪声;[6.1] 无 per-forward 调试日志 / 噪声热路径输出;[6.1] DIP:高层策略不依赖非必要具体细节;[6.1] OCP:本地扩展点优先于修改中心逻辑
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1335
Status: BLOCKING
Summary: P0/1 · P1/5 · P2/13 · P3/4
Reviewed: commit 88ef3abba516 · 2026-08-26 19:35 UTC+8
Blocking Issues
P0
- 无 package 的 worker_status proto 与引擎 model_rpc_service_pb2 符号重名,开关一开即 duplicate symbol 崩溃 @
rtp_llm/server/worker_status_proto/worker_status_service.proto:6- 建议:优先直接复用
rtp_llm.cpp.model_rpc.proto.model_rpc_service_pb2{,_grpc}:其RpcServiceStub.GetWorkerStatus方法路径同为/RpcService/GetWorkerStatus、请求/响应类型逐字段一致,且由model_rpc/proto/BUILD的generate_grpc_proto构建期生成、已随前端打包;utils/grpc_client_wrapper.py:12与multimodal/vit_proxy_start_server.py:29已有同类先例。删除worker_status_proto/可同时消解本条与下一条打包缺失。若确需与引擎解耦的独立副本,注意给 proto 加 package 会改变方法全名破坏 wire 兼容,只能把描述符注册到私有descriptor_pool.DescriptorPool()并用channel.unary_unary("/RpcService/GetWorkerStatus", ...)显式固定路径。无论选哪条路,都请补一条非manual的回归测试:同进程内先 import 引擎 pb2 再 importkvcm_fallback,把该不变量固化下来。
- 建议:优先直接复用
P1
- server py_library 的 srcs glob 漏掉 worker_status_proto,wheel 与 runfiles 均缺该模块 @
rtp_llm/server/BUILD:5- 建议:若采纳上一条建议删除该目录,本条自动消解;否则在 glob 中补
"worker_status_proto/*.py",或改为glob(["*.py", "server_args/*.py", "*_proto/*.py"])以避免后续新增 proto 子目录时同类遗漏复现。补齐后请用一个非manual的 py_test 真实import rtp_llm.server.kvcm_fallback,让打包完整性有构建期约束而非依赖人工记忆,并在沙箱/远程执行下验证一次(本地 local spawn 可能因源码树可见而假通过)。
- 建议:若采纳上一条建议删除该目录,本条自动消解;否则在 glob 中补
- master_kvcm_block_size 默认 0 与「必须为正」校验直接冲突,开关打开即启动期拉挂 @
rtp_llm/config/py_config_modules.py:394- 建议:二选一并保持一致:要么给一个可用的正默认值(并在 help 中说明与引擎
seq_size_per_block的关系),要么在_create_kvcm_fallback_client中补一条与 service_id 同风格、点名参数的前置校验,例如if block_size <= 0: raise ValueError("master_kvcm_block_size must be positive when KVCM fallback is enabled")。请同时补一条断言「开关打开 + 默认 block_size → ValueError」的单测——这是开关打开后的首个失败点,目前零覆盖。
- 建议:二选一并保持一致:要么给一个可用的正默认值(并在 help 中说明与引擎
- KVCM leader 解析在事件循环内持锁执行无 timeout 的同步 vipserver 请求,且失败无负缓存 @
rtp_llm/server/kvcm_fallback.py:569- 建议:把
self._bootstrap_resolver()移出事件循环(asyncio.to_thread/run_in_executor),或改为消费get_hosts(refresh=False)的缓存快照配合独立后台刷新;同时建议给vipserver_proxy.py的requests.get补 timeout(该文件不在本 PR 范围内,可另行提单)。为_resolve_leader增加不超过兜底预算的整体 deadline,并用asyncio.gather并发探测取首个成功者;对「leader 解析失败」加短周期负缓存。结合入口门禁放宽,还建议为「连续 N 次 KVCM 无结果」加短时间窗熔断,避免 FlexLB 与 KVCM 双故障时把 KVCM 超时叠加到每个请求的 TTFT 上(改动前该场景是零成本直接 domain 路由)。
- 建议:把
- 块→token 折算 stride 与实际发出键的 stride 无一致性校验,vLLM sha256 路径在生产不可达 @
rtp_llm/server/kvcm_fallback.py:370- 建议:把键与 stride 收敛为单一来源:删除 sha256 分支与
master_kvcm_block_size,折算直接用seq_size_per_block;或不复用 FlexLB 的block_cache_keys、只传input_ids让query_and_select统一按master_kvcm_block_size生成。若仍需手工配置,请在构造期断言master_kvcm_block_size == seq_size_per_block——注意 FlexLBKvcmWorkerMetadataResolver.resolveNamespace:49-57会把 namespace 拼成<name>_<blockSize>,而master_kvcm_instance_id是静态字符串,同一个 stride 目前需要运维在两处手工对齐。折算所需的block_size/lookahead_tokens建议直接取协议已返回的WorkerStatusPB.block_size(字段 18)/block_hash_lookahead_tokens(字段 21),而worker_load_snapshot(:348-360) 当前完全不读。另请一并对齐 query type:kvcm_fallback.py:635硬编码QT_PREFIX_MATCH,而 FlexLBKvcmGrpcClient:151-157会按 role/group 解析并在不可用时跳过查询,请确认该简化在所有部署形态下成立。最后补一个端到端用例,断言实际发往 KVCM 的 keys 与选定折算方式一致。
- 建议:把键与 stride 收敛为单一来源:删除 sha256 分支与
- 三个新增 py_test 全部 manual 且用桩替换了本该守住三个阻塞缺陷的生产边界 @
rtp_llm/server/test/BUILD:35- 建议:补一条不 stub
master_client/model_rpc_client的集成用例:同进程内先 importrtp_llm.cpp.model_rpc.model_rpc_client,再以真实MasterConfig(master_kvcm_fallback_enabled=True)构造MasterClient并断言构造成功——一条用例即可同时守住三个阻塞点。这三个测试是纯 Python、无 GPU/权重依赖、只绑127.0.0.1:0,属可常态执行的 hermetic 单测,建议去掉manual;需说明同目录schedule_meta_test/host_service_test也是 manual(vit_proxy_server_test不是),但正是该惯例让这个目录长期缺少自动覆盖,本次不宜继续沿用。若内部 CI 确实通过显式 target 列表执行,请在 PR 描述中给出注册位置以便确认。
- 建议:补一条不 stub
Non-blocking Suggestions
P2
- 新开关被 host_service.service_available 前置门控静默旁路 @
rtp_llm/server/backend_rpc_server_visitor.py:324- 建议:在
BackendRPCServerVisitor.__init__中检测master_client.kvcm_fallback_enabled and not host_service.service_available并打一条明确 warning(说明 KVCM fallback 因未配置 service domain 而不会生效),或把该开关纳入service_available的判定。至少需要一条可观测提示,避免线上开了开关却以为已生效。
- 建议:在
- KVCM 选中 worker 的 role 被丢弃并硬编码为 PREFILL,与打分层的 PDFUSION 支持不一致 @
rtp_llm/server/master_client.py:409- 建议:在
KvcmFallbackResult中带上选中 snapshot 的 role 并在_try_kvcm_fallback中映射为对应RoleType;或在进入 KVCM 兜底前校验self.backend_role_list是否包含RoleType.PREFILL,不含则直接走 domain 路由。若本次只支持 PREFILL,则从select_cache_affinity_first的过滤条件中去掉 PDFUSION,使两处语义一致。并补一条backend_role_list=[PDFUSION]的用例,断言不会混入多余的 PREFILL 地址。
- 建议:在
- 10 个新增 MasterConfig 字段没有任何 CLI/env 绑定,线上实际不可配置 @
rtp_llm/config/py_config_modules.py:397- 建议:为这 10 个字段补齐
add_argument(env_name+bind_to+type),尤其直接影响线上时延与选点质量的master_sync_request_timeout_ms、master_kvcm_worker_status_concurrency、master_kvcm_candidate_pool_size——这是注释里「与同机 FlexLB 对齐」的前置条件,也是选路偏差时唯一的免发版调参与回滚手段。若部分字段确定不暴露,则从MasterConfig移除、改为kvcm_fallback.py内的模块级常量,避免「看起来可配置、实际写死」。同时把master_client.py的getattr改为直接属性访问,让MasterConfig.__init__成为默认值唯一来源,并补一个遍历MasterConfig.__dict__断言每字段都有 binding 的测试防止再漏。另注意master_sync_request_timeout_ms→KvcmFallbackConfig.worker_status_timeout_ms的命名不对应,建议统一。
- 建议:为这 10 个字段补齐
- local_fallback_addr 在生产链路从不传入,本机保底候选不可达 @
rtp_llm/server/backend_rpc_server_visitor.py:141- 建议:二选一并在测试中体现:若该能力属本次交付范围,请在
get_master_route_addrs中把本机(或 domain 解析出的)worker 作为local_fallback_addr传下去,并补一条经route_ips的用例断言候选池包含它;若暂不启用,请删除该参数与build_candidate_pool的本机合并分支并同步修正 docstring,避免不可达代码与自证测试同时沉淀。
- 建议:二选一并在测试中体现:若该能力属本次交付范围,请在
- KVCM 兜底缺少专属指标,三个新增响应字段无任何生产消费方 @
rtp_llm/server/backend_rpc_server_visitor.py:162- 建议:上报时带
{"route_source": route_result.route_source}标签,或为 KVCM 单列 QPS/RT 指标并按selection_reason/outcome打 tag(并暴露status_success_count、latency_us/status_latency_us),使看板能区分两类路由并对兜底生效与命中率做告警;route_logger.debug("master route success...")建议附带route_source与kvcm_outcome便于按请求追溯。若暂不打点,请精简 15 项cache_match只保留日志实际使用的字段,并把 per-request info 日志降级为 debug 或加采样。
- 建议:上报时带
- gRPC channel 与选中时间字典只增不减,滚动发布后累积僵尸连接 @
rtp_llm/server/kvcm_fallback.py:510- 建议:给两个 channel 字典加容量上限或 LRU/TTL 淘汰(淘汰时
await channel.close()),或在_invalidate_leader、worker 探测连续失败时主动关闭并移除对应 channel;_last_selected_ns同样按 pool 规模定期裁剪。
- 建议:给两个 channel 字典加容量上限或 LRU/TTL 淘汰(淘汰时
- getattr 字面量访问造成默认值双份维护,并对强类型 GenerateInput 做猜测式访问 @
rtp_llm/server/master_client.py:156- 建议:
master_config与KvcmFallbackResult均为内部强类型对象,直接属性访问即可,默认值只保留在MasterConfig一处;_input_ids_for_kvcm收敛为基于input.token_ids的确定性处理(只保留二维 squeeze 这一条真实需要的分支),去掉getattr/hasattr猜测式访问,让字段改名在静态检查阶段暴露而非静默降级。
- 建议:
- FlexLB body 级 8600 从硬失败改为静默 domain fallback,且不受 KVCM 开关保护 @
rtp_llm/server/master_client.py:611- 建议:在 PR description / 发布说明中显式列出这条与 KVCM 无关的兼容性变更及其预期影响(8400 硬失败 → domain 兜底),并与 FlexLB 侧确认 8600 的期望语义;必要时评估是否需要独立开关或限流保护。这部分与 KVCM 特性正交,建议拆为独立提交以便单独回滚与 CI 二分。
- 手工签入 pb2_grpc 绕开构建期 codegen,格式不一致且与 .proto 无漂移校验 @
rtp_llm/server/worker_status_proto/worker_status_service_pb2_grpc.py:47- 建议:优先改为构建期生成(复用
generate_grpc_proto)或直接复用引擎既有生成模块,从根上去掉签入产物。若必须签入:统一格式来源(两目录都保持 protoc 原样并加入 pre-commitexclude,或生成后统一跑同一 formatter),把 protoc/grpcio-tools 版本与参数固化到脚本或 BUILD 规则以便逐字节复现;并补漂移检测测试——比对本副本DESCRIPTOR与model_rpc_service_pb2中WorkerStatusPB/TaskInfoPB的字段号与类型集合,出现新增字段即失败,KVCM 侧同理比对 FlexLB 事实源。
- 建议:优先改为构建期生成(复用
- 测试在模块导入期永久污染 sys.modules 且不恢复,含 torch 与整棵 rtp_llm 包树 @
rtp_llm/server/test/backend_rpc_server_visitor_fallback_test.py:146- 建议:把 stub 注入收敛到可恢复作用域:用
setUpModule/unittest.addModuleCleanup或unittest.mock.patch.dict(sys.modules, {...})完成注入并在退出时还原快照;torch.inference_mode这类依赖建议只 patch 属性而非替换整个torch模块。更彻底的做法是让被测模块的依赖通过构造参数注入,从而无需改写sys.modules。
- 建议:把 stub 注入收敛到可恢复作用域:用
- 测试自建影子生产类型已与生产漂移,契约变更不会被发现 @
rtp_llm/server/test/master_client_fallback_test.py:41- 建议:对
RoleAddr、ExceptionType、FlexlbResponse、MasterClient这类被断言的生产类型请直接 import 真实实现,只 stub 真正的重量级边界(torch、ModelRpcClient、kmonitor);MasterClient.__init__已提供kvcm_fallback_client注入参数(master_client.py:133),正是为此设计。用真实MasterConfig构造实例、只 stub 最外层 client,即可在契约漂移时立刻失败。
- 建议:对
- 测试从源码树重建包路径加载生产模块,绕开 Bazel 依赖图 @
rtp_llm/server/test/kvcm_fallback_test.py:12- 建议:改为直接
import rtp_llm.server.kvcm_fallback,让 BUILD 的deps真正成为被验证的约束。若目的是回避rtp_llm/__init__.py的重量级导入,更合适的做法是把kvcm_fallback需要的最小依赖拆成独立 py_library 并在 py_test 中精确声明,而不是在测试里重建包路径——这样一旦 BUILD 漏声明源文件,测试会立刻失败而非静默通过。
- 建议:改为直接
- 并发上界断言使用测试自建 semaphore,关键降级分支无覆盖 @
rtp_llm/server/test/kvcm_fallback_test.py:250- 建议:并发用例改为经公开入口
query_and_select驱动:构造多于worker_status_concurrency的候选(如 4 候选 + 并发 2),由 fake servicer 统计max_active并断言<= 2,锁住「配置项 → gather」的接线;通道复用改为断言 servicer 侧连接数或 RPC 次数等可观测行为,减少对私有属性的耦合。visitor 侧把_MasterClient改为可注入返回值,补齐allow_domain_fallback三条分支(断言host_service.domain_calls == 1且最终落到 domain 地址)以及上述两条既有边界组合。
- 建议:并发用例改为经公开入口
P3
- master_kvcm_use_local 场景下 service_id 中填写的端口被静默丢弃 @
rtp_llm/server/master_client.py:195- 建议:use_local 时优先使用 host 自带端口,仅在缺省时回落
bootstrap_port;或在 help 中明确说明端口部分会被忽略、必须通过--master_kvcm_bootstrap_port指定。
- 建议:use_local 时优先使用 host 自带端口,仅在缺省时回落
- token_ids 到 list 的 O(prompt_len) 转换在路由热路径上重复执行两次 @
rtp_llm/server/backend_rpc_server_visitor.py:133- 建议:将已展开的
token_ids作为参数传入get_backend_role_addrs直接复用;若担心接口膨胀,也可只在 KVCM 兜底真正触发时惰性转换(当前_input_ids_for_kvcm已在兜底分支内调用,浪费主要在于重复展开而非提前展开)。
- 建议:将已展开的
- 回落 domain 路由的 warning 措辞与实际条件不符且为逐请求热路径噪声 @
rtp_llm/server/backend_rpc_server_visitor.py:240- 建议:改为直接陈述判定结果,例如
"master route skipped, master_addr=%s, kvcm_fallback_enabled=%s, input_token_batched=%s, fallback to domain routing";并考虑对该 warning 做采样或降级为 debug。
- 建议:改为直接陈述判定结果,例如
- 测试脚手架存在未使用 import 与可参数化的重复用例体 @
rtp_llm/server/test/backend_rpc_server_visitor_fallback_test.py:1- 建议:删除未使用的
import asyncio;两条用例可用subTest或参数化表((kvcm_enabled, expect_master_calls, expect_domain_calls, expect_ip))驱动,后续新增降级路径用例时只需加一行数据。
- 建议:删除未使用的
Checklist Findings (19 fail / 48 total)
General Principles Checklist
- [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
FlexLB body 级 8600 从硬失败改为静默 domain fallback,且不受 KVCM 开关保护
经 diff 确认if code == FALLBACK_ERROR_CODE: return FlexlbResponse.fallback_response()(:611-612) 是本次新增行(FALLBACK_ERROR_CODE = 8600与 :475-476 的非 200 分支均为存量)。exceptions.py只定义到MASTER_NO_AVAILABLE_WORKER = 8400、ROUTE_ERROR = 8500,无 8600,故此前 HTTP 200 响应体中code=8600会走ExceptionType(8600)→ ValueError → 回落 8400 并raise FtRuntimeException(请求以 8400 失败)。改动后同一响应变成fallback=True→backend_rpc_server_visitor.py:251的allow_domain_fallback成立 → 静默 domain routing。该变化**不在 `master_kvcm_fal - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
回落 domain 路由的 warning 措辞与实际条件不符且为逐请求热路径噪声
日志文案为"master address: %s, KVCM fallback enabled: %s, or input token batched: %s is not valid, fallback to domain routing"。进入该分支的真实条件是「master_addr 为空 且 KVCM 开关关闭」或「输入为批量」,把KVCM fallback enabled=False描述为 "is not valid" 语义混乱;且该 warning 在 master 域名未配置的部署里是每请求一条,属热路径噪声。 - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
10 个新增 MasterConfig 字段没有任何 CLI/env 绑定,线上实际不可配置
MasterConfig.__init__新增 19 个字段(:389-411),master_group_args.py只注册 9 个新add_argument(全文 13 个,其中 4 个为存量)。缺绑定的 10 个经逐一比对为master_kvcm_worker_status_port(:397)、candidate_pool_size(:398)、hot_candidate_pool_size(:399)、worker_status_concurrency(:400)、master_sync_request_timeout_ms(:402)、prefill_queue_size_threshold(:403)、p2p_hit_discount(:404)、cache_affinity_first_max_extra_work_tokens(:405)、outstanding_uncached_tokens_threshold(:406)、cache_affinity_first_min_hit_rate(:407)。` - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
测试在模块导入期永久污染 sys.modules 且不恢复,含 torch 与整棵 rtp_llm 包树
本行在模块顶层(而非 setUp 内)直接调用_load_visitor_module()。该函数通过 :21-32 的循环与_module()(:11-16),把torch(:92 整体替换为只有inference_mode的桩)、rtp_llm、rtp_llm.server、rtp_llm.config、rtp_llm.cpp、rtp_llm.cpp.model_rpc、rtp_llm.metrics、rtp_llm.utils共 7 个伪包 + 10 个伪模块写入sys.modules,全程无备份与恢复(无addModuleCleanup、无 try/finally)。当前依赖 Bazel 每个 py_test 独立进程才侥幸成立;一旦该文件与其他用例同进程被收集(合并 target 或直接对该目录跑 pytest),后续任何import torch或import rtp_llm.*都会拿到假模块,产生只表现为 AttributeError 的连锁失败。 - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
master_kvcm_use_local 场景下 service_id 中填写的端口被静默丢弃
master_group_args.py:58的 help 说明master_kvcm_service_id是「KVCM bootstrap service id or local IP:port list」,:112 进一步说明 use_local 时「Treat service id as a comma-separated local IP:port list」,host_service.py:95-102也确实按ip, port = addr.split(":")构造Host(ip, port)。但resolve_bootstrap_targets(:195-203) 只取host.ip并统一拼接bootstrap_port,用户在 service_id 中写的端口被静默忽略,实际连接的是master_kvcm_bootstrap_port(默认 6381)。这在 use_local 的测试/联调场景下会产生「配置看起来生效、实际连错端口」的困惑。 - [6.1] Quality — 无 per-forward 调试日志 / 噪声热路径输出 → issue
回落 domain 路由的 warning 措辞与实际条件不符且为逐请求热路径噪声
日志文案为"master address: %s, KVCM fallback enabled: %s, or input token batched: %s is not valid, fallback to domain routing"。进入该分支的真实条件是「master_addr 为空 且 KVCM 开关关闭」或「输入为批量」,把KVCM fallback enabled=False描述为 "is not valid" 语义混乱;且该 warning 在 master 域名未配置的部署里是每请求一条,属热路径噪声。 - [6.1] Quality — 逻辑变更未混入无关格式化 → issue
手工签入 pb2_grpc 绕开构建期 codegen,格式不一致且与 .proto 无漂移校验
全仓其余 proto 均由rtp_llm/cpp/model_rpc/proto/BUILD的generate_grpc_proto+*_py_proto在构建期产出,本次新增的两个*_pb2_grpc.py是唯一手工签入的生成物,形成两套并行机制,且rtp_llm/server/BUILD只有一个 py_library glob、无 protoc 规则与一致性校验。两者风格还不一致:kvcm_meta_service_pb2_grpc.py:17-21已被 black 改写(双引号、尾随逗号),本文件 :17-21 保留 protoc 原样(单引号、8 空格续行),本行(:47) 还带前导空格。.pre-commit-config.yaml的 exclude 只有^rtp_llm/ops|^3rdparty,二者仍会被继续改写,与DO NOT EDIT头矛盾,下次重生成会产生大量与逻辑无关的 diff。worker_status_service.proto又是引擎 proto 的手工裁剪副本,引擎侧新增字段不会同 - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
token_ids 到 list 的 O(prompt_len) 转换在路由热路径上重复执行两次
get_master_route_addrs(:133-138) 已把input.token_ids展开为 python list 并算出block_cache_keys,但不把该 list 传下去;master_client._input_ids_for_kvcm(:298-317) 在 KVCM 兜底时再次tolist()并做[int(t) for t in ...]全量转换。改动后master_addr为空的部署每个请求都会命中这条路径,长 prompt(数万 token)下等于在路由热路径上多做一遍纯 Python 逐元素转换。本条未做基准测量,仅为代码层面的重复开销。 - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
测试脚手架存在未使用 import 与可参数化的重复用例体
本行的import asyncio经全文检索确认仅出现在此、从未使用(异步用例走unittest.IsolatedAsyncioTestCase);仓库.flake8只ignore = W292,F401 未被豁免,该行会触发 lint。另外 :211 与 :221 两个用例体结构完全一致,仅_visitor(True/False)与三处期望值不同,属可数据驱动化的重复脚手架。 - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
KVCM 选中 worker 的 role 被丢弃并硬编码为 PREFILL,与打分层的 PDFUSION 支持不一致
kvcm_fallback.py:389的可用性过滤同时接纳 role 含PREFILL或PDFUSION的 worker,说明打分层设计上支持 PDFUSION 部署;但WorkerLoadSnapshot.role(:355) 只用于过滤,未写入KvcmFallbackResult(selected是无 role 字段的KvcmCacheCandidate),最终本行固定构造RoleAddr(role=RoleType.PREFILL, ...)。叠加本次入口门禁放宽,在 pdfusion_domain 部署下backend_role_list=[PDFUSION],backend_rpc_server_visitor.py:248的need_domain_routing恒为真,于是既追加一次冗余 domain 兜底,又把一条角色标签与真实 worker 不符的 PREFILL 地址一并下发给引擎,下游按角色取址的行为未验证。 - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
并发上界断言使用测试自建 semaphore,关键降级分支无覆盖
测试在 :213 传入worker_status_concurrency=2,但 :250-258 自建asyncio.Semaphore(2)并直接调用私有client._probe_worker(candidate, semaphore)后断言max_active <= 2——生产上界由kvcm_fallback.py:724在query_and_select内构造,而前半段(:218-226)只有 1 个候选,故该断言只验证了asyncio.Semaphore自身语义:把 :724 改成任意大值或删除 semaphore,测试仍通过;:177/:226 还直接断言私有属性_channels/_worker_status_channels的长度。visitor 侧_MasterClient.get_backend_role_addrs(:183-187) 恒返回成功响应,本次改动真正的风险路径全部无覆盖:KVCM 无结果时connection_failed=True能否经 `allow_domain_fa - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
master_kvcm_use_local 场景下 service_id 中填写的端口被静默丢弃
master_group_args.py:58的 help 说明master_kvcm_service_id是「KVCM bootstrap service id or local IP:port list」,:112 进一步说明 use_local 时「Treat service id as a comma-separated local IP:port list」,host_service.py:95-102也确实按ip, port = addr.split(":")构造Host(ip, port)。但resolve_bootstrap_targets(:195-203) 只取host.ip并统一拼接bootstrap_port,用户在 service_id 中写的端口被静默忽略,实际连接的是master_kvcm_bootstrap_port(默认 6381)。这在 use_local 的测试/联调场景下会产生「配置看起来生效、实际连错端口」的困惑。
RTP-LLM Checklist
- [I] 代码质量 — 同一功能用统一工具函数 → issue
测试脚手架存在未使用 import 与可参数化的重复用例体
本行的import asyncio经全文检索确认仅出现在此、从未使用(异步用例走unittest.IsolatedAsyncioTestCase);仓库.flake8只ignore = W292,F401 未被豁免,该行会触发 lint。另外 :211 与 :221 两个用例体结构完全一致,仅_visitor(True/False)与三处期望值不同,属可数据驱动化的重复脚手架。
Python Static-First Checklist
- [P.A] 静态结构与类型纪律 — 禁止 getattr/setattr literal 访问 → issue
getattr 字面量访问造成默认值双份维护,并对强类型 GenerateInput 做猜测式访问
_create_kvcm_fallback_client(:156-265) 用了 13 处getattr(self.master_config, "字面量", 默认值),每个默认值(6381/100/3/2/200/1024/0.2/5.0…)都与py_config_modules.py:389-411重复一份,漂移时无告警。_input_ids_for_kvcm(:298-317) 对强类型 dataclass 用getattr(input, "input_ids", None)——已核实base_model_datatypes.py:53-58的GenerateInput只有token_ids: torch.Tensor(input_ids属于GenerateOutput),该分支在生产恒为 None;随后又用hasattr(raw_input_ids, "tolist")做控制流分支,而token_ids按声明恒为 Tensor。cache_match与日志中另有 10 处 `getattr(r - [P.A] 静态结构与类型纪律 — 禁止 hasattr 做控制流分支 → issue
getattr 字面量访问造成默认值双份维护,并对强类型 GenerateInput 做猜测式访问
_create_kvcm_fallback_client(:156-265) 用了 13 处getattr(self.master_config, "字面量", 默认值),每个默认值(6381/100/3/2/200/1024/0.2/5.0…)都与py_config_modules.py:389-411重复一份,漂移时无告警。_input_ids_for_kvcm(:298-317) 对强类型 dataclass 用getattr(input, "input_ids", None)——已核实base_model_datatypes.py:53-58的GenerateInput只有token_ids: torch.Tensor(input_ids属于GenerateOutput),该分支在生产恒为 None;随后又用hasattr(raw_input_ids, "tolist")做控制流分支,而token_ids按声明恒为 Tensor。cache_match与日志中另有 10 处 `getattr(r - [P.C] 并发与异步 — async def 中禁止 blocking 调用 → issue
KVCM leader 解析在事件循环内持锁执行无 timeout 的同步 vipserver 请求,且失败无负缓存
_resolve_leader在async with self._leader_lock(:564) 内同步调用self._bootstrap_resolver()(:569),即master_client.py:195-203的resolve_bootstrap_targets→VipServerWrapper.get_hosts(refresh=True)(host_service.py:117-122,非 use_local 时走get_host_list_by_domain_now)→vipserver_proxy.py:106的requests.get(...):同步、经核实无 timeout 参数、失败时逐个 srv 重试。这会阻塞前端整个 asyncio 事件循环,拖住同进程所有在途请求。其后 :571 是串行for target in targets,每个等request_timeout_ms,无整体 deadline;全部失败且无previous_leader时抛异常但不写负缓 - [P.F] 语言陷阱 — 禁止模块级 import 副作用 → issue
测试从源码树重建包路径加载生产模块,绕开 Bazel 依赖图
kvcm_fallback_test.py:12、master_client_fallback_test.py:11、backend_rpc_server_visitor_fallback_test.py:20均用Path(__file__).resolve().parents[2]反推包根,再通过注入__path__或spec_from_file_location加载生产模块。.resolve()会跟随 runfiles 符号链接,导入目标取决于测试进程实际落在哪棵目录树上,而不是 BUILD 中声明的 deps。这正是worker_status_protoglob 缺失未被任何测试暴露的机制性原因:源码树里文件在,runfiles 里不在,而测试读的是源码树。 - [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
并发上界断言使用测试自建 semaphore,关键降级分支无覆盖
测试在 :213 传入worker_status_concurrency=2,但 :250-258 自建asyncio.Semaphore(2)并直接调用私有client._probe_worker(candidate, semaphore)后断言max_active <= 2——生产上界由kvcm_fallback.py:724在query_and_select内构造,而前半段(:218-226)只有 1 个候选,故该断言只验证了asyncio.Semaphore自身语义:把 :724 改成任意大值或删除 semaphore,测试仍通过;:177/:226 还直接断言私有属性_channels/_worker_status_channels的长度。visitor 侧_MasterClient.get_backend_role_addrs(:183-187) 恒返回成功响应,本次改动真正的风险路径全部无覆盖:KVCM 无结果时connection_failed=True能否经 `allow_domain_fa - [P.G] 测试规范 — 数据驱动测试用 pytest.mark.parametrize → issue
测试脚手架存在未使用 import 与可参数化的重复用例体
本行的import asyncio经全文检索确认仅出现在此、从未使用(异步用例走unittest.IsolatedAsyncioTestCase);仓库.flake8只ignore = W292,F401 未被豁免,该行会触发 lint。另外 :211 与 :221 两个用例体结构完全一致,仅_visitor(True/False)与三处期望值不同,属可数据驱动化的重复脚手架。
Strengths
- 开关默认关闭且
py_config_modules.py:388显式注释「Keep disabled by default so existing FlexLB/domain routing is unchanged」;开关为假时can_attempt_master_route退化为原bool(master_addr)语义,route_ips各分支行为与改动前逐条一致,存量部署零行为变更,回滚手段明确。 - 兜底触发条件收敛精准:
master_client.py:582仅在resp.connection_failed时进入 KVCM,显式 FlexLB fallback 与全部业务/准入错误码保留原语义,未把可用性兜底与业务降级混为一谈;master_client_fallback_test.py:200-264用三个用例断言kvcm_client.calls == 0锁死这条边界。 - 二级降级链路经核实成立:KVCM 无候选时返回 None,
resp.connection_failed仍为 True,backend_rpc_server_visitor.py:251的allow_domain_fallback依然满足,不会把请求打死在 ROUTE_ERROR 上;全部 KVCM 异常在master_client.py:353收敛为返回 None,失败面收敛。 KvcmFallbackConfig.__post_init__按「必须为正 / 不得为负 / 合法端口 / 浮点非负」四组做完整前置校验并 fail-fast,且显式排除 bool 混入 int,配置错误在构造期暴露而非在请求路径上产生难定位行为。select_cache_affinity_first忠实复刻 FlexLB CacheAffinityFirst 语义:alive/role/队列长度过滤、estimated_ttft_work排序、min_hit_rate门限、outstanding 守护回退、recent打散并列项,并在_matched_tokens:369注释中说明对齐 JavaMath.round行为。worker_status_service.proto逐字段对齐引擎侧字段号,完整保留reserved 2/reserved 19, 20/reserved "inter_request_id"与optionalpresence 语义;kvcm_meta_service.proto与 flexlb-grpc 侧事实源逐字段核对后 package、字段号、类型、枚举值完全一致,wire 兼容性考虑到位。kvcm_fallback_test.py:114-124用真实grpc.aio.server()+127.0.0.1:0动态端口做端到端验证而非 mock 掉传输层,真实覆盖序列化与方法路径;:90-108用 golden vectors 锁定 block hash 并单独覆盖「丢弃不足一块的尾部」边界;:150用畸形 endpoint 做对抗性用例。rtp_llm/BUILD:80与rtp_llm/server/BUILD:11-12显式声明grpcio/protobuf直接依赖,修正了此前import grpc靠传递依赖的隐式接线;grpcio==1.62.0已在 lock 文件(rtp_llm/BUILD:606)中,跨平台构建无缺依赖风险。master_flexlb_transport_timeout_ms默认 0 并在master_client.py:558以min()收敛,完整保留「用请求 TTFT 超时」的原行为;_split_ip_port/_format_target对 IPv6 bracket 形式处理正确;close()幂等且同时清理两组 channel。
| // Keep this file package-less: the deployed engine serves | ||
| // /RpcService/GetWorkerStatus. | ||
|
|
||
| message StatusVersionPB { |
There was a problem hiding this comment.
[P0] 无 package 的 worker_status proto 与引擎 model_rpc_service_pb2 符号重名,开关一开即 duplicate symbol 崩溃
新文件注释「Keep this file package-less」(:3-4),在全局命名空间定义 StatusVersionPB(:6)、TaskInfoPB(:11)、KvCacheGroupModePB(:42)、WorkerStatusPB(:48)、service RpcService(:73)。已核实 model_rpc_service.proto 全文无 package,并在 :342/:390/:439/:445/:654 定义完全同名符号,:655 已有 rpc GetWorkerStatus(StatusVersionPB) returns (WorkerStatusPB)。两份 pb2 均调 _descriptor_pool.Default().AddSerializedFile(本副本见 worker_status_service_pb2.py:17),文件名不同而全限定名相同。链路确定:backend_rpc_server_visitor.py:10 → model_rpc_client.py:10 已加...
建议: 优先直接复用 rtp_llm.cpp.model_rpc.proto.model_rpc_service_pb2{,_grpc}:其 RpcServiceStub.GetWorkerStatus 方法路径同为 /RpcService/GetWorkerStatus、请求/响应类型逐字段一致,且由 model_rpc/proto/BUILD 的 generate_grpc_proto 构建期生成、已随前端打包;utils/grpc_client_wrapper.py:12 与 multimodal/vit_proxy_start_server.py:29 已有同类先例。删除 worker_status_proto/ 可同时消解本条与下一条打包缺失。若确需与引擎解耦的独立副本,注意给 proto 加 package 会改变方法全名破坏 wire 兼容,只能把描述符注册到私有 descriptor_pool.DescriptorPool() 并用 channel.unary_unary("/RpcService/GetWorkerStatus", ...) 显式固定路径。无论选哪条路,都请补一条非 manual 的回归测试:同进程内先 import 引擎 pb2 再 import kvcm_fallback,把该不变量固化下来。
| @@ -4,9 +4,12 @@ py_library( | |||
| name = "server", | |||
| srcs = glob([ | |||
There was a problem hiding this comment.
[P1] server py_library 的 srcs glob 漏掉 worker_status_proto,wheel 与 runfiles 均缺该模块
py_library(name="server") 的 glob 为 ["*.py", "server_args/*.py", "kvcm_proto/*.py"],本次显式补了 kvcm_proto/ 却漏掉 worker_status_proto/,而 bazel glob 不递归;该目录下也无独立 BUILD,全仓 BUILD 检索 worker_status_proto 零命中(仅 kvcm_fallback.py:22-23 与 kvcm_fallback_test.py:30,33 引用)。rtp_llm/BUILD:417,491 表明 //rtp_llm/server:server 是 rtp_llm/server/** 进入 wheel 与 py_binary 的唯一来源,故 __init__.py、worker_status_service_pb2.py、..._pb2_grpc.py 既不在 wheel 也不在 runfiles,而 kvcm_fallback.py:22-25 模块级无条件导入它 → ...
建议: 若采纳上一条建议删除该目录,本条自动消解;否则在 glob 中补 "worker_status_proto/*.py",或改为 glob(["*.py", "server_args/*.py", "*_proto/*.py"]) 以避免后续新增 proto 子目录时同类遗漏复现。补齐后请用一个非 manual 的 py_test 真实 import rtp_llm.server.kvcm_fallback,让打包完整性有构建期约束而非依赖人工记忆,并在沙箱/远程执行下验证一次(本地 local spawn 可能因源码树可见而假通过)。
| self.master_kvcm_bootstrap_port: int = 6381 | ||
| # Exact KVCM instance_id/namespace, for example <prefill-deployment>_128. | ||
| self.master_kvcm_instance_id: str = "" | ||
| self.master_kvcm_block_size: int = 0 |
There was a problem hiding this comment.
[P1] master_kvcm_block_size 默认 0 与「必须为正」校验直接冲突,开关打开即启动期拉挂
master_kvcm_block_size 默认 0(本行,master_group_args.py:84 同为 default=0)。而 kvcm_fallback.py:58-69 把 block_size 归入必须为正组:if isinstance(value, bool) or not isinstance(value, int) or value <= 0: raise ValueError(f"KVCM {name} must be positive")。该异常经 master_client.py:165→208 传入配置后由 MasterClient.__init__(:149) 抛出、再从 BackendRPCServerVisitor.__init__ 冒泡,即仅打开开关而未显式设值就会启动失败。这是同一路径上第三个独立阻塞。同函数对 service_id/instance_id/bootstrap_port 都有点名到配置项的报错(master_client.py:179-188),block_size 却只得到 `K...
建议: 二选一并保持一致:要么给一个可用的正默认值(并在 help 中说明与引擎 seq_size_per_block 的关系),要么在 _create_kvcm_fallback_client 中补一条与 service_id 同风格、点名参数的前置校验,例如 if block_size <= 0: raise ValueError("master_kvcm_block_size must be positive when KVCM fallback is enabled")。请同时补一条断言「开关打开 + 默认 block_size → ValueError」的单测——这是开关打开后的首个失败点,目前零覆盖。
| p2p_hit_discount: float, | ||
| ) -> int: | ||
| # Java Math.round for a non-negative value is floor(value + 0.5). | ||
| tokens = int(effective_cache_blocks(candidate, p2p_hit_discount) * block_size + 0.5) |
There was a problem hiding this comment.
[P1] 块→token 折算 stride 与实际发出键的 stride 无一致性校验,vLLM sha256 路径在生产不可达
生产路径 backend_rpc_server_visitor.py:138 用 get_block_cache_keys(token_ids, self.seq_size_per_block) 生成键(ops/__init__.py:176-184 按 seq_size_per_block 切块并走 C++ blockUtil.cc:8 的 hashInt64Vector),原样透传至 master_client.py:349 → kvcm_fallback.py:689 keys = list(block_cache_keys),仅当 keys 为空才调 calculate_vllm_block_cache_keys(:691)。故 prompt ≥ seq_size_per_block 时 sha256 分支与 master_group_args.py:85「used for vLLM sha256_cbor hashing」的声明用途完全不可达;短 prompt 时反而会发出 KVCM 未索引的键空间。更关键的是 `...
建议: 把键与 stride 收敛为单一来源:删除 sha256 分支与 master_kvcm_block_size,折算直接用 seq_size_per_block;或不复用 FlexLB 的 block_cache_keys、只传 input_ids 让 query_and_select 统一按 master_kvcm_block_size 生成。若仍需手工配置,请在构造期断言 master_kvcm_block_size == seq_size_per_block——注意 FlexLB KvcmWorkerMetadataResolver.resolveNamespace:49-57 会把 namespace 拼成 <name>_<blockSize>,而 master_kvcm_instance_id 是静态字符串,同一个 stride 目前需要运维在两处手工对齐。折算所需的 block_size / lookahead_tokens 建议直接取协议已返回的 WorkerStatusPB.block_size(字段 18)/ block_hash_lookahead_tokens(字段 21),而 worker_load_snapshot(:348-360) 当前完全不读。另请一并对齐 query type:kvcm_fallback.py:635 硬编码 QT_PREFIX_MATCH,而 FlexLB KvcmGrpcClient:151-157 会按 role/group 解析并在不可用时跳过查询,请确认该简化在所有部署形态下成立。最后补一个端到端用例,断言实际发往 KVCM 的 keys 与选定折算方式一致。
| "//rtp_llm:grpcio", | ||
| "//rtp_llm:protobuf", | ||
| ], | ||
| tags = ["manual"], |
There was a problem hiding this comment.
[P1] 三个新增 py_test 全部 manual 且用桩替换了本该守住三个阻塞缺陷的生产边界
kvcm_fallback_test(:35)、master_client_fallback_test(:45)、backend_rpc_server_visitor_fallback_test(:54) 均带 tags = ["manual"],被 //... 与 test_suite 展开排除,本 PR 变更中无任何显式注册处,802 行新模块与 route_ips 准入改动在 CI 中零自动执行。且即便执行也无法发现上述缺陷:visitor 测试 :104 把 model_rpc_client.ModelRpcClient 桩为 object、:125 把 MasterClient 桩为 object、:92 整体替换 torch,两份 pb2 都不会被导入;master_client_fallback_test.py:114 用 5 字段 SimpleNamespace 替代真实 MasterConfig,7 个用例全部注入 fake client,_create_kvcm_fallback_client()(`...
建议: 补一条不 stub master_client/model_rpc_client 的集成用例:同进程内先 import rtp_llm.cpp.model_rpc.model_rpc_client,再以真实 MasterConfig(master_kvcm_fallback_enabled=True) 构造 MasterClient 并断言构造成功——一条用例即可同时守住三个阻塞点。这三个测试是纯 Python、无 GPU/权重依赖、只绑 127.0.0.1:0,属可常态执行的 hermetic 单测,建议去掉 manual;需说明同目录 schedule_meta_test/host_service_test 也是 manual(vit_proxy_server_test 不是),但正是该惯例让这个目录长期缺少自动覆盖,本次不宜继续沿用。若内部 CI 确实通过显式 target 列表执行,请在 PR 描述中给出注册位置以便确认。
| self.master_client.kvcm_fallback_enabled, | ||
| input_token_batched, | ||
| ) | ||
| specified_roles = {addr.role for addr in input.generate_config.role_addrs} |
There was a problem hiding this comment.
📍 实际位置 rtp_llm/server/backend_rpc_server_visitor.py:324(不在 diff 展示范围内,就近挂载)
[P2] 新开关被 host_service.service_available 前置门控静默旁路
route_ips 只在 self.host_service.service_available 为真时被调用(本行,batch_enqueue :335-337 同理)。已核实 host_service.py:551 中 service_available = bool(self.master_vip.domain) or any(self.role_vip_map.values()),仅由 master/role domain 决定,完全不感知 master_kvcm_fallback_enabled。因此运维若只打开 KVCM 开关而未配置任何 domain,route_ips 根本不执行,本次放宽的 can_attempt_master_route 判定永不生效,且日志无任何提示——属跨层配置传播缺口导致的静默失效。
建议: 在 BackendRPCServerVisitor.__init__ 中检测 master_client.kvcm_fallback_enabled and not host_service.service_available 并打一条明确 warning(说明 KVCM fallback 因未配置 service domain 而不会生效),或把该开关纳入 service_available 的判定。至少需要一条可观测提示,避免线上开了开关却以为已生效。
| ) | ||
| return FlexlbResponse.ok( | ||
| [ | ||
| RoleAddr( |
There was a problem hiding this comment.
[P2] KVCM 选中 worker 的 role 被丢弃并硬编码为 PREFILL,与打分层的 PDFUSION 支持不一致
kvcm_fallback.py:389 的可用性过滤同时接纳 role 含 PREFILL 或 PDFUSION 的 worker,说明打分层设计上支持 PDFUSION 部署;但 WorkerLoadSnapshot.role(:355) 只用于过滤,未写入 KvcmFallbackResult(selected 是无 role 字段的 KvcmCacheCandidate),最终本行固定构造 RoleAddr(role=RoleType.PREFILL, ...)。叠加本次入口门禁放宽,在 pdfusion_domain 部署下 backend_role_list=[PDFUSION],backend_rpc_server_visitor.py:248 的 need_domain_routing 恒为真,于是既追加一次冗余 domain 兜底,又把一条角色标签与真实 worker 不符的 PREFILL 地址一并下发给引擎,下游按角色取址的行为未验证。
建议: 在 KvcmFallbackResult 中带上选中 snapshot 的 role 并在 _try_kvcm_fallback 中映射为对应 RoleType;或在进入 KVCM 兜底前校验 self.backend_role_list 是否包含 RoleType.PREFILL,不含则直接走 domain 路由。若本次只支持 PREFILL,则从 select_cache_affinity_first 的过滤条件中去掉 PDFUSION,使两处语义一致。并补一条 backend_role_list=[PDFUSION] 的用例,断言不会混入多余的 PREFILL 地址。
Checklist: [6.1] 分布式/跨平台变更有对应覆盖
| @@ -223,16 +223,25 @@ async def route_ips(self, input: GenerateInput): | |||
| input_token_batched = True | |||
There was a problem hiding this comment.
📍 实际位置 rtp_llm/server/backend_rpc_server_visitor.py:141(不在 diff 展示范围内,就近挂载)
[P2] local_fallback_addr 在生产链路从不传入,本机保底候选不可达
kvcm_fallback.py:3-4 的模块 docstring 声称打分范围是「the caller's local worker plus KVCM cache hits」,build_candidate_pool(:281-337) 专门实现了「always retaining the local worker」并合并其真实 cache match。但唯一生产调用点(本行 :141-145)只传 block_cache_keys/input/request_id;全仓检索 local_fallback_addr 的外部写入方仅 master_client_fallback_test.py:171 一处。结果是生产上 local_candidate 恒为 None(master_client.py:329-344 不可达),pool 退化为 hot[:hot_candidate_pool_size](默认 2 而非 3),且 KVCM 无正向命中时 pool 直接为空 → no_candidates → 兜底恒返回 None,「至少保底...
建议: 二选一并在测试中体现:若该能力属本次交付范围,请在 get_master_route_addrs 中把本机(或 domain 解析出的)worker 作为 local_fallback_addr 传下去,并补一条经 route_ips 的用例断言候选池包含它;若暂不启用,请删除该参数与 build_candidate_pool 的本机合并分支并同步修正 docstring,避免不可达代码与自证测试同时沉淀。
| @@ -223,16 +223,25 @@ async def route_ips(self, input: GenerateInput): | |||
| input_token_batched = True | |||
There was a problem hiding this comment.
📍 实际位置 rtp_llm/server/backend_rpc_server_visitor.py:162(不在 diff 展示范围内,就近挂载)
[P2] KVCM 兜底缺少专属指标,三个新增响应字段无任何生产消费方
准入放宽后 master_addr 为空也进入计时块,KVCM 的 gRPC 耗时被记入 GaugeMetrics.MASTER_ROUTE_RT_METRIC(:236-238),成功时统一上报 AccMetrics.MASTER_ROUTE_QPS_METRIC(本行),与 FlexLB 混在同一指标,无法区分兜底占比与 KVCM 时延。FlexlbResponse 已带 route_source/cache_match/kvcm_outcome(master_client.py:47-49、:407-418),但全仓检索显示除 master_client.py 自身赋值/回填与两个测试断言外无任何生产消费方;对 rtp_llm/server grep kmonitor 确认 master_client.py 与 kvcm_fallback.py 均未新增任何指标。唯一可观测手段是 master_client.py:394 的每请求 route_logger.info,而该分支只在 FlexLB 故障时触发——正是日...
建议: 上报时带 {"route_source": route_result.route_source} 标签,或为 KVCM 单列 QPS/RT 指标并按 selection_reason/outcome 打 tag(并暴露 status_success_count、latency_us/status_latency_us),使看板能区分两类路由并对兜底生效与命中率做告警;route_logger.debug("master route success...") 建议附带 route_source 与 kvcm_outcome 便于按请求追溯。若暂不打点,请精简 15 项 cache_match 只保留日志实际使用的字段,并把 per-request info 日志降级为 debug 或加采样。
| bool(getattr(self.master_config, "master_kvcm_use_local", False)), | ||
| ) | ||
|
|
||
| def resolve_bootstrap_targets() -> List[str]: |
There was a problem hiding this comment.
[P3] master_kvcm_use_local 场景下 service_id 中填写的端口被静默丢弃
master_group_args.py:58 的 help 说明 master_kvcm_service_id 是「KVCM bootstrap service id or local IP:port list」,:112 进一步说明 use_local 时「Treat service id as a comma-separated local IP:port list」,host_service.py:95-102 也确实按 ip, port = addr.split(":") 构造 Host(ip, port)。但 resolve_bootstrap_targets(:195-203) 只取 host.ip 并统一拼接 bootstrap_port,用户在 service_id 中写的端口被静默忽略,实际连接的是 master_kvcm_bootstrap_port(默认 6381)。这在 use_local 的测试/联调场景下会产生「配置看起来生效、实际连错端口」的困惑。
建议: use_local 时优先使用 host 自带端口,仅在缺省时回落 bootstrap_port;或在 help 中明确说明端口部分会被忽略、必须通过 --master_kvcm_bootstrap_port 指定。
Checklist: [6.1] 错误语义:fail-fast/retry/fallback/silent 行为显式;[6.1] 边界 case 覆盖(空、单元素、最大值)
| @@ -223,16 +223,25 @@ async def route_ips(self, input: GenerateInput): | |||
| input_token_batched = True | |||
There was a problem hiding this comment.
📍 实际位置 rtp_llm/server/backend_rpc_server_visitor.py:133(不在 diff 展示范围内,就近挂载)
[P3] token_ids 到 list 的 O(prompt_len) 转换在路由热路径上重复执行两次
get_master_route_addrs(:133-138) 已把 input.token_ids 展开为 python list 并算出 block_cache_keys,但不把该 list 传下去;master_client._input_ids_for_kvcm(:298-317) 在 KVCM 兜底时再次 tolist() 并做 [int(t) for t in ...] 全量转换。改动后 master_addr 为空的部署每个请求都会命中这条路径,长 prompt(数万 token)下等于在路由热路径上多做一遍纯 Python 逐元素转换。本条未做基准测量,仅为代码层面的重复开销。
建议: 将已展开的 token_ids 作为参数传入 get_backend_role_addrs 直接复用;若担心接口膨胀,也可只在 KVCM 兜底真正触发时惰性转换(当前 _input_ids_for_kvcm 已在兜底分支内调用,浪费主要在于重复展开而非提前展开)。
Checklist: [6.1] DRY:重复非平凡逻辑被抽取或显式复用
| GaugeMetrics.MASTER_ROUTE_RT_METRIC, master_route_timer.cost_ms() | ||
| ) | ||
| elif not role_addrs_specified: | ||
| route_logger.warning( |
There was a problem hiding this comment.
[P3] 回落 domain 路由的 warning 措辞与实际条件不符且为逐请求热路径噪声
日志文案为 "master address: %s, KVCM fallback enabled: %s, or input token batched: %s is not valid, fallback to domain routing"。进入该分支的真实条件是「master_addr 为空 且 KVCM 开关关闭」或「输入为批量」,把 KVCM fallback enabled=False 描述为 "is not valid" 语义混乱;且该 warning 在 master 域名未配置的部署里是每请求一条,属热路径噪声。
建议: 改为直接陈述判定结果,例如 "master route skipped, master_addr=%s, kvcm_fallback_enabled=%s, input_token_batched=%s, fallback to domain routing";并考虑对该 warning 做采样或降级为 debug。
Checklist: [6.1] 可观测性:日志/指标/超时可操作、非噪声;[6.1] 无 per-forward 调试日志 / 噪声热路径输出
| @@ -0,0 +1,233 @@ | |||
| import asyncio | |||
There was a problem hiding this comment.
[P3] 测试脚手架存在未使用 import 与可参数化的重复用例体
本行的 import asyncio 经全文检索确认仅出现在此、从未使用(异步用例走 unittest.IsolatedAsyncioTestCase);仓库 .flake8 只 ignore = W292,F401 未被豁免,该行会触发 lint。另外 :211 与 :221 两个用例体结构完全一致,仅 _visitor(True/False) 与三处期望值不同,属可数据驱动化的重复脚手架。
建议: 删除未使用的 import asyncio;两条用例可用 subTest 或参数化表((kvcm_enabled, expect_master_calls, expect_domain_calls, expect_ip))驱动,后续新增降级路径用例时只需加一行数据。
Checklist: [6.1] KISS/YAGNI:无投机性抽象;[P.G] 数据驱动测试用 pytest.mark.parametrize;[I] 同一功能用统一工具函数
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1335
Status: BLOCKING
Summary: P0/1 · P1/5 · P2/13 · P3/5
Reviewed: commit f400f0248a3f · 2026-08-26 21:38 UTC+8
Blocking Issues
P0
- 无 package 的 worker_status proto 与引擎 model_rpc_service_pb2 符号重名,开关一开即 duplicate symbol 崩溃 @
rtp_llm/server/worker_status_proto/worker_status_service.proto:6- 建议:治本方案是删除该 vendored 目录,直接复用
rtp_llm.cpp.model_rpc.proto.model_rpc_service_pb2的StatusVersionPB/WorkerStatusPB与model_rpc_service_pb2_grpc.RpcServiceStub:定义与 wire 路径完全等价,utils/grpc_client_wrapper.py:12与multimodal/vit_proxy_start_server.py:29已在前端进程内消费,可一并消除下一条打包缺失与双份 stub 维护。注意不要用「给新 proto 加 package」来修——那会把路径变成/<pkg>.RpcService/GetWorkerStatus,正好破坏该注释要保护的线上契约。若确需独立副本,则改用私有descriptor_pool.DescriptorPool()构建,并补一条「同进程内先后真实导入两份_pb2」的共存回归用例把冲突钉死。
- 建议:治本方案是删除该 vendored 目录,直接复用
P1
- server py_library 的 srcs glob 漏掉 worker_status_proto,wheel 与 runfiles 均缺该模块 @
rtp_llm/server/BUILD:5- 建议:在 glob 中补
"worker_status_proto/*.py";若采纳上一条改为复用引擎 pb2,则整体删除该目录与本条。更稳妥的做法是改用递归 glob(配合 exclude),从根上消除「新增子包忘记登记」这类缺陷。另建议新增一个不带manual标签、按真实包路径执行import rtp_llm.server.kvcm_fallback的最小用例,把打包完整性固化进 CI——当前唯一常态执行的新 targetmaster_group_args_test用spec_from_file_location按文件路径加载,起不到这个作用。
- 建议:在 glob 中补
- master_kvcm_block_size 默认 0 与「必须为正」校验直接冲突,开关打开即启动期拉挂 @
rtp_llm/config/py_config_modules.py:394- 建议:建议直接从引擎已有的
seq_size_per_block推导(在BackendRPCServerVisitor构造MasterClient时传下去),删掉这个必须手填、默认值又非法的独立参数;若因 KVCM namespace 约定必须可覆写,则至少把默认值改为合法值,并在_create_kvcm_fallback_client中补一条与 service_id 同风格的显式校验(错误信息带上master_kvcm_block_size参数名),同时在 help 文案中写明该参数为必填。
- 建议:建议直接从引擎已有的
- KVCM leader 解析在事件循环内持锁执行无 timeout 的同步 vipserver 请求,且失败无负缓存 @
rtp_llm/server/kvcm_fallback.py:569- 建议:把 bootstrap 解析移出事件循环:用
await loop.run_in_executor(None, ...)包裹,或复用HostService已有的后台刷新线程持有 KVCM host 快照、协程只读快照;并为该 HTTP 调用补显式 timeout。同时引入失败负缓存与退避(记录_leader_failed_at,窗口内直接抛错快速回落域名路由),给_resolve_leader加总时长预算,bootstrap 探测改为有界并发 + 首个成功即返回,避免 FlexLB 与 KVCM 同时不可用时路由吞吐被压到约 1/(N×timeout)——该阻塞恰在流量最紧张时刻发生。
- 建议:把 bootstrap 解析移出事件循环:用
- 块→token 折算 stride 与实际发出键的 stride 无一致性校验,vLLM sha256 路径在生产不可达 @
rtp_llm/server/kvcm_fallback.py:370- 建议:对齐 FlexLB 语义:
WorkerBlockHashConfigResolver:116-119从存活 worker 上报的blockSize/blockHashLookaheadTokens动态解析,RequestBlockHashService:98-102在自带 keys 时强校验block_size > 0。建议优先从WorkerStatusPB.block_size/block_hash_lookahead_tokens读取折算 stride 与 lookahead(proto 字段已具备),或直接从引擎seq_size_per_block传入;无论哪种方案都应在_create_kvcm_fallback_client校验其与seq_size_per_block相等、不等即 fail-fast。同时删除生产不可达的calculate_vllm_block_cache_keys分支(keys 为空时直接返回no_complete_blocks不发 RPC),并修正master_group_args.py:85只提「vLLM sha256_cbor hashing」的 help 文案。
- 建议:对齐 FlexLB 语义:
- 三个新增 py_test 全部 manual 且用桩替换了本该守住阻塞缺陷的生产边界 @
rtp_llm/server/test/BUILD:35- 建议:至少去掉
master_client_fallback_test与backend_rpc_server_visitor_fallback_test的manual;kvcm_fallback_test可用tags=["local"]替代。补三类缺失覆盖:一是不注入 fallback client、由_config()提供完整 KVCM 字段真实构造MasterClient并断言生成的KvcmFallbackConfig,配 service_id 缺失 / instance_id 缺失 / bootstrap_port 越界三个assertRaises(ValueError);二是同进程共存加载两份_pb2;三是 master_client 返回connection_failed=True时断言route_ips仍回落域名路由(当前_MasterClient永远成功,最关键降级分支无断言)。若确有环境限制须保留manual,请在 BUILD 注释写明原因并挂进显式test_suite。
- 建议:至少去掉
Non-blocking Suggestions
P2
- 新开关被 host_service.service_available 前置门控静默旁路 @
rtp_llm/server/backend_rpc_server_visitor.py:324- 建议:若 KVCM 兜底被定位为「FlexLB 已配置但不可达」时的可用性兜底,请在参数 help 与 PR 描述中写明该前置约束;若期望它能独立承担选路,则应把
master_client.client_fallback_enabled纳入service_available的计算,或在enqueue的门控处一并判断,并补一条覆盖该组合的用例。
- 建议:若 KVCM 兜底被定位为「FlexLB 已配置但不可达」时的可用性兜底,请在参数 help 与 PR 描述中写明该前置约束;若期望它能独立承担选路,则应把
- KVCM 选中 worker 的 role 被丢弃并硬编码为 PREFILL,与打分层的 PDFUSION 支持不一致 @
rtp_llm/server/master_client.py:430- 建议:把
WorkerLoadSnapshot.role透出到KvcmFallbackResult,按真实角色映射为RoleType.PREFILL/RoleType.PDFUSION后再构造RoleAddr;并在get_master_route_addrs接受 KVCM 结果前校验角色属于self.backend_role_list,不属于则视为不可用并打 warning。补一条 PDFUSION 部署下的兜底选路用例,断言不会同时下发 PREFILL 与 PDFUSION 两条地址。
- 建议:把
- local_fallback_addr 在生产链路从不传入,本机保底候选不可达 @
rtp_llm/server/backend_rpc_server_visitor.py:141- 建议:二选一并保持自洽:其一,在
get_master_route_addrs中构造本机(或host_service可解析的本组 worker)RoleAddr传入,并补一条「视图层传参 → 候选池含本机 → KVCM 零命中仍可路由」的端到端用例;其二,先移除该形参、_try_kvcm_fallback的local_candidate装配与build_candidate_pool的本机分支,同步修正 docstring,待真正接线时再引入,避免不可达代码掩盖能力缺口。
- 建议:二选一并保持自洽:其一,在
- KVCM 兜底缺少专属指标,三个新增响应字段无任何生产消费方 @
rtp_llm/server/backend_rpc_server_visitor.py:162- 建议:在
:162与:236上报时带上{"route_source": route_result.route_source}维度(:148-152已有 tag 用法),或新增独立的 KVCM 降级 QPS/命中率/时延/selection_reason分布指标,使灰度与回滚有量化依据。把「master 缺失 + 兜底未命中且已有域名兜底」的日志按connection_failed降为 warning 或按窗口聚合,并补对应 error QPS;对_parse_candidates:604-608静默continue丢弃的非法候选补限流日志或计数。
- 建议:在
- gRPC channel 与选中时间字典只增不减,滚动发布后累积僵尸连接 @
rtp_llm/server/kvcm_fallback.py:510- 建议:给这四个 dict 加容量上限与 LRU/TTL 淘汰(淘汰时
await channel.close()),或在_invalidate_leader与 worker 探测连续失败时主动关闭并移除对应 channel/stub;_last_selected_ns建议按candidate_pool_size的数倍设上限并按时间淘汰陈旧条目。
- 建议:给这四个 dict 加容量上限与 LRU/TTL 淘汰(淘汰时
- FlexLB body 级 8600 从硬失败改为静默 domain fallback,且不受 KVCM 开关保护 @
rtp_llm/server/master_client.py:631- 建议:若这是有意与非 200 分支(
:495-497)对齐的修正,请在 PR 描述与 commit message 中明确声明该兼容性变更及灰度方案,并在该分支补一条 info/warning 级日志与错误码维度指标,使「FlexLB 主动要求降级」的发生量可观测;若不希望现网默认行为改变,则把该分支纳入client_fallback_enabled或独立开关之下,并补一条开关关闭态的用例锁定预期语义。
- 建议:若这是有意与非 200 分支(
- getattr 字面量访问造成默认值三处重复维护,并对强类型 GenerateInput 做猜测式访问 @
rtp_llm/server/master_client.py:156- 建议:以
MasterConfig字段默认值为唯一来源:master_group_args.py从MasterConfig()取默认值,master_client.py改为直接读属性(该配置对象由本仓构造,属内部可信边界)。_input_ids_for_kvcm直接使用input.token_ids并按shape判断维度,与visitor:133-137保持同一写法。若短期不便重构,请在master_group_args_test补一条遍历MasterConfig()全部master_*字段、校验每字段都有对应 argument 且default与字段初值相等的断言。
- 建议:以
- 手工签入 pb2/pb2_grpc 绕开构建期 codegen,格式不一致且与 .proto 无漂移校验 @
rtp_llm/server/worker_status_proto/worker_status_service_pb2_grpc.py:47- 建议:
worker_status_proto优先按第一条 finding 删除并复用引擎 stub。KVCM 侧二选一:由 Bazel 直接以权威.proto为唯一 src 在构建期生成 pb2,消除副本;或保留副本但把两个目录加入.pre-commit-config.yaml的 exclude、统一保留 protoc 原始输出以便与再生成结果比对,并补一条轻量测试锁定消息名、字段号与ErrorCode/QueryType枚举值集合,同时在目录内注明再生成命令与 grpcio-tools 版本。
- 建议:
- 测试在模块导入期永久污染 sys.modules 且不恢复,含 torch 与整棵 rtp_llm 包树 @
rtp_llm/server/test/backend_rpc_server_visitor_fallback_test.py:146- 建议:统一收敛为一种守卫风格:改用
unittest.mock.patch.dict(sys.modules, ...)在用例作用域内替换并自动回滚,或把伪造逻辑收进setUpModule/tearDownModule(配合addModuleCleanup)在结束时恢复原条目;至少参照同 PR 的master_group_args_test.py用try/finally还原。建议把这套 stub 装配抽到test/下的共享 helper,避免三份实现继续分叉。
- 建议:统一收敛为一种守卫风格:改用
- 测试自建影子生产类型已与生产漂移,契约变更不会被发现 @
rtp_llm/server/test/master_client_fallback_test.py:41- 建议:从生产模块导入
FlexlbResponse/RoleType/RoleAddr,只 stub 真正不可用的重依赖(torch、rtp_llm.ops);_visitor()改为走真实__init__(必要时注入轻量 host_service/master_client),使client_fallback_enabled由master_config.master_client_fallback推导的链路(master_client.py:143-145)被真正覆盖。并补齐「传入生产格式非空block_cache_keys时实际发往 KVCM 的 keys 与入参一致」、input_token_batched=True、role_addrs预置时开关被正确短路三个边界断言。
- 建议:从生产模块导入
- 测试从源码树重建包路径加载生产模块,绕开 Bazel 依赖图 @
rtp_llm/server/test/kvcm_fallback_test.py:12- 建议:去掉命名空间 shim,改为按真实包路径
from rtp_llm.server.kvcm_fallback import ...(//rtp_llm/server:server已把server_args/*.py与util.py纳入 srcs),让 Bazel 依赖图与 BUILD 登记成为测试通过的前置条件;master_group_args_test同样改为直接from rtp_llm.server.server_args.master_group_args import init_master_group_args,必要时补//rtp_llm:config_ops依赖。
- 建议:去掉命名空间 shim,改为按真实包路径
- 并发上界断言使用测试自建 semaphore,生产装配路径零覆盖 @
rtp_llm/server/test/kvcm_fallback_test.py:250- 建议:改为构造多候选池后走公开入口断言:让 FakeMetaService 返回 4 个不同
host_ip_port的HostCacheMatch(配各自 worker_status 端口),worker_status_concurrency=2,再断言worker_service.max_active <= 2,使断言由生产侧Semaphore(config.worker_status_concurrency)承担,同时去掉对私有方法_probe_worker及其签名的隐式依赖。另补 leader 解析失败、GetHostCacheState返回非 OK 与SERVER_NOT_LEADER的降级分支用例。
- 建议:改为构造多候选池后走公开入口断言:让 FakeMetaService 返回 4 个不同
- master_group_args_test 用假模块替换 str2bool,且未断言 type 与 default @
rtp_llm/server/server_args/test/master_group_args_test.py:29- 建议:去掉
sys.modules顶替、直接from rtp_llm.server.server_args.master_group_args import init_master_group_args,并补断言arguments["--master_client_fallback"]["type"] is str2bool、arguments["--master_kvcm_use_local"]["type"] is str2bool,以及全部 19 个参数的default与MasterConfig()字段初值相等。建议再加一条 env 解析用例,断言MASTER_CLIENT_FALLBACK=false时开关确实为 False,把「默认关闭 + env 可正确关闭」这一兼容性承诺锁进用例。
- 建议:去掉
P3
- 路由可用性判断跨类重复,visitor 直接读取 MasterClient 内部开关 @
rtp_llm/server/backend_rpc_server_visitor.py:227- 建议:由
MasterClient暴露单一can_route()(或is_available())方法封装全部准入条件,visitor 只依赖该方法,避免可用性判断在两处漂移;顺带便于为熔断态预留扩展点。
- 建议:由
- master_kvcm_use_local 场景下 service_id 中填写的端口被静默丢弃 @
rtp_llm/server/master_client.py:201- 建议:在 use_local 模式下优先使用
host.port(存在且合法时),仅在缺省时回落bootstrap_port;或在解析出的 port 与bootstrap_port不一致时打一条明确 warning 说明取值来源。并在 help 文案中写清 use_local 模式下端口的实际取值规则。
- 建议:在 use_local 模式下优先使用
- token_ids 到 list 的 O(prompt_len) 转换在路由路径上重复执行两次 @
rtp_llm/server/backend_rpc_server_visitor.py:133- 建议:在
get_master_route_addrs中把已转换好的token_ids列表随参数传给get_backend_role_addrs(或缓存在请求上下文中),让_try_kvcm_fallback直接复用,删除_input_ids_for_kvcm中的二次tolist()与逐元素int()。
- 建议:在
- 回落 domain 路由的 warning 措辞与实际条件不符且为逐请求热路径噪声 @
rtp_llm/server/backend_rpc_server_visitor.py:240- 建议:改为结论式表述:先计算
reason = "batched_input" if input_token_batched else "no_master_addr_and_fallback_disabled",再打印reason与三个原始取值,并按窗口聚合或降为 debug,便于日志检索聚合且避免热路径噪声。
- 建议:改为结论式表述:先计算
- 测试脚手架存在未使用 import 与可参数化的重复用例体 @
rtp_llm/server/test/backend_rpc_server_visitor_fallback_test.py:1- 建议:删除未使用的
import asyncio;两个用例体用subTest或参数化表((enabled, expected_master_calls, expected_domain_calls, expected_ip))合并为一个,减少后续新增分支时的复制成本。
- 建议:删除未使用的
Checklist Findings (20 fail / 48 total)
General Principles Checklist
- [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue
无 package 的 worker_status proto 与引擎 model_rpc_service_pb2 符号重名,开关一开即 duplicate symbol 崩溃
该 proto 第 3-4 行注释为保住/RpcService/GetWorkerStatus而刻意不加 package,顶层定义StatusVersionPB(6)、TaskInfoPB(11)、KvCacheGroupModePB(42)、WorkerStatusPB(48)、service RpcService(73)。rtp_llm/cpp/model_rpc/proto/model_rpc_service.proto同样无 package,且在 342/390/439/445/654 行定义完全同名符号(其RpcService已含同签名GetWorkerStatus)。worker_status_service_pb2.py:17向_descriptor_pool.Default()二次AddSerializedFile这批 FQN。backend_rpc_server_visitor.py:10经model_rpc_client.py:10先注册前者,同文件:78构造 `Maste - [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
master_group_args_test 用假模块替换 str2bool,且未断言 type 与 default
本次给master_group_args.py:1新增了模块级依赖from rtp_llm.server.server_args.util import str2bool,而测试在第 29 行用fake_util_module.str2bool = bool顶替该模块并按文件路径加载被测模块:util.str2bool若被改名或删除,用例仍全绿而生产在 import 期即失败。断言(105-114)也只覆盖 flag/env_name/bind_to,完全未断言type与default——若--master_client_fallback被误写成type=bool(bool("false")为真,MASTER_CLIENT_FALLBACK=false反而开启开关),本用例依然通过,而它塞进去的假件恰好就是bool。这是本 PR 唯一进入 CI 常态执行的新 target。 - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
回落 domain 路由的 warning 措辞与实际条件不符且为逐请求热路径噪声
:240-246文案为 "master address: %s, client fallback enabled: %s, or input token batched: %s is not valid, fallback to domain routing",把三个状态并列后接单数 "is not valid"。该分支实际触发条件是(master_addr 为空 且 开关关闭) or input_token_batched,既不指明究竟哪一项导致降级,排障还需人工反推布尔组合;且在 master 常态缺失的部署下按请求量刷屏。 - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
KVCM 兜底缺少专属指标,三个新增响应字段无任何生产消费方
全仓检索显示新增的route_source/cache_match/kvcm_outcome(master_client.py:47-49,:436-438填充)仅在master_client.py内部赋值与master_client_fallback_test.py:191,228断言,无任何生产读取方。:162对 KVCM 兜底成功与 FlexLB 正常成功一律上报同一个无 tag 的MASTER_ROUTE_QPS_METRIC,运维无法度量降级流量占比,也没有 KVCM 命中率/时延/失败原因指标。同时开关开启且master_addr为空时 MasterClient 不发 HTTP 直接返回connection_failed=True,:165的route_logger.error会按请求量刷 ERROR,而该分支并不上报MASTER_ROUTE_ERROR_QPS_METRIC(仅:148异常路径上报),形成「日志洪水 + 指标空白」。 - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
测试在模块导入期永久污染 sys.modules 且不恢复,含 torch 与整棵 rtp_llm 包树
第 146 行在模块作用域直接执行_load_visitor_module(),该函数在 19-32 行无条件把rtp_llm、rtp_llm.server、rtp_llm.config、rtp_llm.cpp、rtp_llm.metrics、rtp_llm.utils等 7 个真实包替换为空壳,并另有 12 处_module(...)(92-133,含顶替torch与rtp_llm.ops),全程无tearDownModule恢复。master_client_fallback_test.py:12-20同为无守卫写法,kvcm_fallback_test.py:13-20带if "rtp_llm" not in sys.modules守卫,同 PR 的master_group_args_test.py:40-44则用try/finally正确还原——同一问题三种风格。当前仅因 Bazel py_test 单进程隔离才不出事。 - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
master_kvcm_use_local 场景下 service_id 中填写的端口被静默丢弃
master_kvcm_use_local=True时VipServerWrapper.__init__(host_service.py:95-102)把service_id按逗号切分、再按ip:port解析成Host(ip, port)。但resolve_bootstrap_targets(201-206)只取host.ip,端口一律用bootstrap_port拼接。因此运维按 help 文案(master_group_args.py:58,211「KVCM bootstrap service id or local IP:port list」「Treat service id as a comma-separated local IP:port list」)填写10.0.0.1:7000后,实际连接的是10.0.0.1:6381,端口被静默忽略且无任何日志提示;若省略端口则addr.split(":")直接抛 ValueError。 - [6.1] Quality — 无 per-forward 调试日志 / 噪声热路径输出 → issue
回落 domain 路由的 warning 措辞与实际条件不符且为逐请求热路径噪声
:240-246文案为 "master address: %s, client fallback enabled: %s, or input token batched: %s is not valid, fallback to domain routing",把三个状态并列后接单数 "is not valid"。该分支实际触发条件是(master_addr 为空 且 开关关闭) or input_token_batched,既不指明究竟哪一项导致降级,排障还需人工反推布尔组合;且在 master 常态缺失的部署下按请求量刷屏。 - [6.1] Quality — 逻辑变更未混入无关格式化 → issue
手工签入 pb2/pb2_grpc 绕开构建期 codegen,格式不一致且与 .proto 无漂移校验
两个包均为手工签入的DO NOT EDIT产物,未复用仓内rtp_llm/cpp/model_rpc/proto/BUILD的构建期生成模式,protoc 版本与再生成命令均无记录(worker_status_service_pb2.py:4标注 Protobuf Python Version 4.25.1,而rtp_llm/BUILD:580,597锁定 grpcio-tools 1.57.0 / protobuf 4.25)。.pre-commit-config.yaml:31的全局 exclude 仅为^rtp_llm/ops|^3rdparty,两个目录都在 black 作用域内:kvcm_meta_service_pb2_grpc.py:17-26已被 black 重排(双引号、4 空格续行),而本文件仍是 protoc 原始输出(单引号、8 空格续行,第 47 行行首残留一个空格),下次run --all-files会再次改写生成文件。 - [6.1] Software Engineering — DIP:高层策略不依赖非必要具体细节 → issue
路由可用性判断跨类重复,visitor 直接读取 MasterClient 内部开关
门控直接读self.master_client.client_fallback_enabled(:227) 判断能否尝试 master 路由,而MasterClient内部真正的准入前置是「client_fallback_enabled为真 且_kvcm_fallback_client非空」(master_client.py:148-149、:342-343)。同一判断被拆到两个类:后续若 MasterClient 增加准入条件(如 KVCM 熔断态、客户端创建失败降级),visitor 侧门控会静默失配,请求先进入注定失败的路径再回落。 - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
测试脚手架存在未使用 import 与可参数化的重复用例体
第 1 行import asyncio全文件未使用(两个异步用例由unittest.IsolatedAsyncioTestCase驱动)。:211-231的两个用例体高度重复:均为「构造 visitor →await route_ips→ 断言 master_client.calls / domain_calls / 首个 addr 的 ip」,仅client_fallback_enabled与三组期望值不同。 - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
测试脚手架存在未使用 import 与可参数化的重复用例体
第 1 行import asyncio全文件未使用(两个异步用例由unittest.IsolatedAsyncioTestCase驱动)。:211-231的两个用例体高度重复:均为「构造 visitor →await route_ips→ 断言 master_client.calls / domain_calls / 首个 addr 的 ip」,仅client_fallback_enabled与三组期望值不同。 - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
KVCM 选中 worker 的 role 被丢弃并硬编码为 PREFILL,与打分层的 PDFUSION 支持不一致
select_cache_affinity_first(kvcm_fallback.py:389)把 role 含PREFILL或PDFUSION的 worker 都视为可用,worker_load_snapshot:355也保存了真实 role,但KvcmFallbackResult.selected只回传不含角色的KvcmCacheCandidate,角色信息在此丢失,第 430 行固定构造RoleType.PREFILL。PDFUSION 部署下(backend_role_list=[PDFUSION],visitor:118-120):master 路由成功时get_master_route_addrs返回 None(visitor:163)使:251的allow_domain_fallback恒为 True,:248的need_domain_routing因{PREFILL}不覆盖[PDFUSION]亦为 True,于是再补一条域名 PDFUSION 地址,最终把角色标 - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
master_group_args_test 用假模块替换 str2bool,且未断言 type 与 default
本次给master_group_args.py:1新增了模块级依赖from rtp_llm.server.server_args.util import str2bool,而测试在第 29 行用fake_util_module.str2bool = bool顶替该模块并按文件路径加载被测模块:util.str2bool若被改名或删除,用例仍全绿而生产在 import 期即失败。断言(105-114)也只覆盖 flag/env_name/bind_to,完全未断言type与default——若--master_client_fallback被误写成type=bool(bool("false")为真,MASTER_CLIENT_FALLBACK=false反而开启开关),本用例依然通过,而它塞进去的假件恰好就是bool。这是本 PR 唯一进入 CI 常态执行的新 target。 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
master_kvcm_use_local 场景下 service_id 中填写的端口被静默丢弃
master_kvcm_use_local=True时VipServerWrapper.__init__(host_service.py:95-102)把service_id按逗号切分、再按ip:port解析成Host(ip, port)。但resolve_bootstrap_targets(201-206)只取host.ip,端口一律用bootstrap_port拼接。因此运维按 help 文案(master_group_args.py:58,211「KVCM bootstrap service id or local IP:port list」「Treat service id as a comma-separated local IP:port list」)填写10.0.0.1:7000后,实际连接的是10.0.0.1:6381,端口被静默忽略且无任何日志提示;若省略端口则addr.split(":")直接抛 ValueError。
RTP-LLM Checklist
- [I] 代码质量 — 同一功能用统一工具函数 → issue
token_ids 到 list 的 O(prompt_len) 转换在路由路径上重复执行两次
get_master_route_addrs:133-137已把input.token_ids整条tolist()为 Python list 用于get_block_cache_keys,但只把结果 keys 传给 MasterClient;进入 KVCM 兜底后_input_ids_for_kvcm:320-331又对同一 tensor 再做一次tolist()并逐元素int()转换。同一份 prompt 在同一请求内被完整物化两次,长 prompt 下是纯浪费;且第二次转换只在兜底路径需要,而兜底恰好发生在 FlexLB 故障、流量最紧张的时刻。本条未做基准测量,按可读性/DRY 问题定级。
Python Static-First Checklist
- [P.A] 静态结构与类型纪律 — 禁止 getattr/setattr literal 访问 → issue
getattr 字面量访问造成默认值三处重复维护,并对强类型 GenerateInput 做猜测式访问
同一批默认值写了三遍:py_config_modules.py:389-411字段初值、master_group_args.py:43-224的default=、以及master_client.py:156-278约 19 处getattr(self.master_config, "<字面量>", <默认值>)的兜底值(如 bootstrap 端口 6381 三处并存),三份当前一致但无任何测试守卫。另_input_ids_for_kvcm:315先探getattr(input, "input_ids", None),而base_model_datatypes.py:53-55的GenerateInput只有token_ids(input_ids属GenerateOutput,:123),该首选分支在生产恒为 None;:320的hasattr(raw_input_ids, "tolist")亦为控制流分支。测试的 fake input 恰好带input_ids,掩盖了这一点。 - [P.A] 静态结构与类型纪律 — 禁止 hasattr 做控制流分支 → issue
getattr 字面量访问造成默认值三处重复维护,并对强类型 GenerateInput 做猜测式访问
同一批默认值写了三遍:py_config_modules.py:389-411字段初值、master_group_args.py:43-224的default=、以及master_client.py:156-278约 19 处getattr(self.master_config, "<字面量>", <默认值>)的兜底值(如 bootstrap 端口 6381 三处并存),三份当前一致但无任何测试守卫。另_input_ids_for_kvcm:315先探getattr(input, "input_ids", None),而base_model_datatypes.py:53-55的GenerateInput只有token_ids(input_ids属GenerateOutput,:123),该首选分支在生产恒为 None;:320的hasattr(raw_input_ids, "tolist")亦为控制流分支。测试的 fake input 恰好带input_ids,掩盖了这一点。 - [P.C] 并发与异步 — async def 中禁止 blocking 调用 → issue
KVCM leader 解析在事件循环内持锁执行无 timeout 的同步 vipserver 请求,且失败无负缓存
第 569 行self._bootstrap_resolver()是同步调用,且位于async with self._leader_lock(564)临界区内。实参为master_client.py:199-207的resolve_bootstrap_targets→VipServerWrapper.get_hosts(refresh=True)→get_host_list_by_domain_now→vipserver_proxy.py:106的requests.get(...)——未设 timeout 且持有 threading.Lock。仓内既有代码刻意把同一调用隔离到rtp_llm_master_addr_refresh后台线程(host_service.py:220)。随后 571-578 行还在同一把锁内串行探测全部 bootstrap。且仅当previous_leader非空才在 589-591 行更新时间戳形成退避;从未成功过时第 592 行直接抛错、不记失败时间,leader 持续 - [P.F] 语言陷阱 — 禁止模块级 import 副作用 → issue
测试在模块导入期永久污染 sys.modules 且不恢复,含 torch 与整棵 rtp_llm 包树
第 146 行在模块作用域直接执行_load_visitor_module(),该函数在 19-32 行无条件把rtp_llm、rtp_llm.server、rtp_llm.config、rtp_llm.cpp、rtp_llm.metrics、rtp_llm.utils等 7 个真实包替换为空壳,并另有 12 处_module(...)(92-133,含顶替torch与rtp_llm.ops),全程无tearDownModule恢复。master_client_fallback_test.py:12-20同为无守卫写法,kvcm_fallback_test.py:13-20带if "rtp_llm" not in sys.modules守卫,同 PR 的master_group_args_test.py:40-44则用try/finally正确还原——同一问题三种风格。当前仅因 Bazel py_test 单进程隔离才不出事。 - [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
master_group_args_test 用假模块替换 str2bool,且未断言 type 与 default
本次给master_group_args.py:1新增了模块级依赖from rtp_llm.server.server_args.util import str2bool,而测试在第 29 行用fake_util_module.str2bool = bool顶替该模块并按文件路径加载被测模块:util.str2bool若被改名或删除,用例仍全绿而生产在 import 期即失败。断言(105-114)也只覆盖 flag/env_name/bind_to,完全未断言type与default——若--master_client_fallback被误写成type=bool(bool("false")为真,MASTER_CLIENT_FALLBACK=false反而开启开关),本用例依然通过,而它塞进去的假件恰好就是bool。这是本 PR 唯一进入 CI 常态执行的新 target。
Strengths
- 兜底严格限定在「传输层可用性失败」语义:仅
resp.connection_failed才查 KVCM(master_client.py:602),显式 FlexLB fallback(8600) 与业务/准入错误码保持原有含义,注释写明了这条边界,定位为可用性兜底而非调度改写。 - 上一轮评审指出的「10 个新增 MasterConfig 字段无 CLI/env 绑定」已修复:19 个字段现在全部具备
--flag+env_name+bind_to三元组,默认值与MasterConfig声明一致。 --master_client_fallback与--master_kvcm_use_local使用type=str2bool(master_group_args.py:47,209)而非bool,规避了MASTER_CLIENT_FALLBACK=false反被解析为 True 的经典陷阱。KvcmFallbackConfig.__post_init__集中校验正数、非负数、端口区间与浮点阈值,并显式排除bool(避免True当1通过整数校验),配置错误在构造期 fail-fast。calculate_vllm_block_cache_keys(kvcm_fallback.py:220-248)用定长 CBOR + sha256 精确复刻 vLLM/FlexLB 语义,含 NONE_HASH 种子、lookahead 与「丢弃末尾不完整块」,并有基于hashlib的 golden 向量固定,跨进程确定。- 默认值刻意保留旧语义:
flexlb_transport_timeout_ms=0时完全等价于原ttft_timeout_ms,worker_grpc_port_override=0时走 HTTP+1(与host_service.py:574的仓内约定一致),回滚只需关开关。 kvcm_meta_service.proto保留 packagekv_cache_manager.proto.meta,与 flexlb-grpc 侧字段号、枚举值逐项一致;生成代码用相对导入并配套__init__.py,规避 protoc 绝对导入在包内失效的坑。build_candidate_pool把候选面限制为「本机 + 少量缓存热点」,WorkerStatus 探测配asyncio.Semaphore与独立 deadline,每次 RPC 均带timeout=,避免兜底把单请求放大成全集群扫描或无界等待。- 纯函数分层清晰(
build_candidate_pool/select_cache_affinity_first/effective_cache_blocks/worker_load_snapshot均无 I/O),_split_ip_port/_format_target正确处理 IPv6 方括号形式;kvcm_fallback_test.py用真实grpc.aio.server()搭双服务端到端验证序列化与 channel 复用,127.0.0.1:0动态端口、asyncSetUp/asyncTearDown成对关闭。
| // Keep this file package-less: the deployed engine serves | ||
| // /RpcService/GetWorkerStatus. | ||
|
|
||
| message StatusVersionPB { |
There was a problem hiding this comment.
[P0] 无 package 的 worker_status proto 与引擎 model_rpc_service_pb2 符号重名,开关一开即 duplicate symbol 崩溃
该 proto 第 3-4 行注释为保住 /RpcService/GetWorkerStatus 而刻意不加 package,顶层定义 StatusVersionPB(6)、TaskInfoPB(11)、KvCacheGroupModePB(42)、WorkerStatusPB(48)、service RpcService(73)。rtp_llm/cpp/model_rpc/proto/model_rpc_service.proto 同样无 package,且在 342/390/439/445/654 行定义完全同名符号(其 RpcService 已含同签名 GetWorkerStatus)。worker_status_service_pb2.py:17 向 _descriptor_pool.Default() 二次 AddSerializedFile 这批 FQN。backend_rpc_server_visitor.py:10 经 model_rpc_client.py:10 先注册前者,同文件 :78 构造 `Ma...
建议: 治本方案是删除该 vendored 目录,直接复用 rtp_llm.cpp.model_rpc.proto.model_rpc_service_pb2 的 StatusVersionPB/WorkerStatusPB 与 model_rpc_service_pb2_grpc.RpcServiceStub:定义与 wire 路径完全等价,utils/grpc_client_wrapper.py:12 与 multimodal/vit_proxy_start_server.py:29 已在前端进程内消费,可一并消除下一条打包缺失与双份 stub 维护。注意不要用「给新 proto 加 package」来修——那会把路径变成 /<pkg>.RpcService/GetWorkerStatus,正好破坏该注释要保护的线上契约。若确需独立副本,则改用私有 descriptor_pool.DescriptorPool() 构建,并补一条「同进程内先后真实导入两份 _pb2」的共存回归用例把冲突钉死。
Checklist: [6.1] 依赖方向:无循环依赖/跨层惊喜
| @@ -4,9 +4,12 @@ py_library( | |||
| name = "server", | |||
| srcs = glob([ | |||
There was a problem hiding this comment.
[P1] server py_library 的 srcs glob 漏掉 worker_status_proto,wheel 与 runfiles 均缺该模块
py_library(name="server") 的 glob 为 ["*.py", "server_args/*.py", "kvcm_proto/*.py"],本次补了 kvcm_proto 却漏了同批新增的 worker_status_proto。全仓检索 worker_status_proto 仅命中 kvcm_fallback.py:22-25 与 kvcm_fallback_test.py:30,33,无任何 BUILD 收录,目录下也无 BUILD,且不存在覆盖 server/**/*.py 的递归 glob。但被 "*.py" 收录的 kvcm_fallback.py 在模块顶层 import 该包。//rtp_llm/server:server 是 server 源码进入 runfiles 与 py_package 的唯一供源(rtp_llm/BUILD:417,491),故这 3 个文件两处皆缺,开关打开时会先于符号冲突抛 ModuleNotFoundError。
建议: 在 glob 中补 "worker_status_proto/*.py";若采纳上一条改为复用引擎 pb2,则整体删除该目录与本条。更稳妥的做法是改用递归 glob(配合 exclude),从根上消除「新增子包忘记登记」这类缺陷。另建议新增一个不带 manual 标签、按真实包路径执行 import rtp_llm.server.kvcm_fallback 的最小用例,把打包完整性固化进 CI——当前唯一常态执行的新 target master_group_args_test 用 spec_from_file_location 按文件路径加载,起不到这个作用。
| self.master_kvcm_bootstrap_port: int = 6381 | ||
| # Exact KVCM instance_id/namespace, for example <prefill-deployment>_128. | ||
| self.master_kvcm_instance_id: str = "" | ||
| self.master_kvcm_block_size: int = 0 |
There was a problem hiding this comment.
[P1] master_kvcm_block_size 默认 0 与「必须为正」校验直接冲突,开关打开即启动期拉挂
master_kvcm_block_size 字段默认 0,master_group_args.py:84 的 default=0 与 master_client.py:165 的 getattr(..., 0) 兜底同为 0;而 kvcm_fallback.py:59,68-69 把 block_size 列入「必须为正」白名单,0 直接抛 ValueError("KVCM block_size must be positive")。_create_kvcm_fallback_client 只为 service_id、instance_id、bootstrap_port 写了三条带参数名的显式校验(master_client.py:183-192),唯独 block_size 没有,只能落到 dataclass 的通用报错。因此按现有 help 文案配齐 service_id + instance_id 打开开关,前端进程即在 MasterClient.__init__ 抛异常、服务起不来。
建议: 建议直接从引擎已有的 seq_size_per_block 推导(在 BackendRPCServerVisitor 构造 MasterClient 时传下去),删掉这个必须手填、默认值又非法的独立参数;若因 KVCM namespace 约定必须可覆写,则至少把默认值改为合法值,并在 _create_kvcm_fallback_client 中补一条与 service_id 同风格的显式校验(错误信息带上 master_kvcm_block_size 参数名),同时在 help 文案中写明该参数为必填。
| p2p_hit_discount: float, | ||
| ) -> int: | ||
| # Java Math.round for a non-negative value is floor(value + 0.5). | ||
| tokens = int(effective_cache_blocks(candidate, p2p_hit_discount) * block_size + 0.5) |
There was a problem hiding this comment.
[P1] 块→token 折算 stride 与实际发出键的 stride 无一致性校验,vLLM sha256 路径在生产不可达
生产链路 backend_rpc_server_visitor.py:138 传入的是 rtp_llm.ops.get_block_cache_keys,底层 blockUtil.cc:8 用 hashInt64Vector 链式滚动哈希、stride 为引擎 seq_size_per_block;ops/__init__.py:180-183 对长度 ≥ 一块的 prompt 必然返回非空,故 query_and_select:690-695 的 vLLM sha256 分支在生产恒不执行。但 _matched_tokens:370 与 :736 用独立的 config.block_size 把命中块折算成 token,两个 stride 之间没有任何校验,不等时命中 token 整体缩放,使 cache_affinity_first_min_hit_rate、max_extra_work_tokens 判定全部失真且无报错。此外 lookahead_tokens/p2p_host_count 未从 `_create_kv...
建议: 对齐 FlexLB 语义:WorkerBlockHashConfigResolver:116-119 从存活 worker 上报的 blockSize/blockHashLookaheadTokens 动态解析,RequestBlockHashService:98-102 在自带 keys 时强校验 block_size > 0。建议优先从 WorkerStatusPB.block_size/block_hash_lookahead_tokens 读取折算 stride 与 lookahead(proto 字段已具备),或直接从引擎 seq_size_per_block 传入;无论哪种方案都应在 _create_kvcm_fallback_client 校验其与 seq_size_per_block 相等、不等即 fail-fast。同时删除生产不可达的 calculate_vllm_block_cache_keys 分支(keys 为空时直接返回 no_complete_blocks 不发 RPC),并修正 master_group_args.py:85 只提「vLLM sha256_cbor hashing」的 help 文案。
| "//rtp_llm:grpcio", | ||
| "//rtp_llm:protobuf", | ||
| ], | ||
| tags = ["manual"], |
There was a problem hiding this comment.
[P1] 三个新增 py_test 全部 manual 且用桩替换了本该守住阻塞缺陷的生产边界
kvcm_fallback_test(:35)、master_client_fallback_test(:45)、backend_rpc_server_visitor_fallback_test(:54) 均带 tags=["manual"],Bazel 会从 //...、:all 通配中排除;同包 vit_proxy_server_test 未加该标签(schedule_meta_test/host_service_test 带),故并非统一约定。后两个为纯进程内 fake、无网络无 GPU 依赖,kvcm_fallback_test 仅需 loopback。三者又恰好绕开本次三个阻塞边界:kvcm_fallback_test:24-34 从不加载 model_rpc_service_pb2(无法命中符号冲突)、backend_rpc_server_visitor_fallback_test:122-126 把 MasterClient 换成 object、7 个 master_client 用例一律注入 `kvcm_...
建议: 至少去掉 master_client_fallback_test 与 backend_rpc_server_visitor_fallback_test 的 manual;kvcm_fallback_test 可用 tags=["local"] 替代。补三类缺失覆盖:一是不注入 fallback client、由 _config() 提供完整 KVCM 字段真实构造 MasterClient 并断言生成的 KvcmFallbackConfig,配 service_id 缺失 / instance_id 缺失 / bootstrap_port 越界三个 assertRaises(ValueError);二是同进程共存加载两份 _pb2;三是 master_client 返回 connection_failed=True 时断言 route_ips 仍回落域名路由(当前 _MasterClient 永远成功,最关键降级分支无断言)。若确有环境限制须保留 manual,请在 BUILD 注释写明原因并挂进显式 test_suite。
| self.master_client.client_fallback_enabled, | ||
| input_token_batched, | ||
| ) | ||
| specified_roles = {addr.role for addr in input.generate_config.role_addrs} |
There was a problem hiding this comment.
📍 实际位置 rtp_llm/server/backend_rpc_server_visitor.py:324(不在 diff 展示范围内,就近挂载)
[P2] 新开关被 host_service.service_available 前置门控静默旁路
enqueue(:324) 与 batch_enqueue(:335) 仅在 self.host_service.service_available 为真时才调用 route_ips,而该标志由 host_service.py:551 计算为 bool(master_vip.domain) or any(role_vip_map.values())——完全不感知新增的 master_client_fallback。因此在「不配 FlexLB master 域名、也不配 backend 角色域名,仅靠 KVCM 选路」的部署下,即使开关打开,route_ips 整段被跳过,新链路永不执行且无任何日志说明。新增的 visitor 测试直接调用 route_ips,绕过了这层门控,无法暴露该组合。
建议: 若 KVCM 兜底被定位为「FlexLB 已配置但不可达」时的可用性兜底,请在参数 help 与 PR 描述中写明该前置约束;若期望它能独立承担选路,则应把 master_client.client_fallback_enabled 纳入 service_available 的计算,或在 enqueue 的门控处一并判断,并补一条覆盖该组合的用例。
| return FlexlbResponse.ok( | ||
| [ | ||
| RoleAddr( | ||
| role=RoleType.PREFILL, |
There was a problem hiding this comment.
[P2] KVCM 选中 worker 的 role 被丢弃并硬编码为 PREFILL,与打分层的 PDFUSION 支持不一致
select_cache_affinity_first(kvcm_fallback.py:389)把 role 含 PREFILL 或 PDFUSION 的 worker 都视为可用,worker_load_snapshot:355 也保存了真实 role,但 KvcmFallbackResult.selected 只回传不含角色的 KvcmCacheCandidate,角色信息在此丢失,第 430 行固定构造 RoleType.PREFILL。PDFUSION 部署下(backend_role_list=[PDFUSION],visitor:118-120):master 路由成功时 get_master_route_addrs 返回 None(visitor:163)使 :251 的 allow_domain_fallback 恒为 True,:248 的 need_domain_routing 因 {PREFILL} 不覆盖 [PDFUSION] 亦为 True,于是再补一条域名 PDFUSION 地址,最终把...
建议: 把 WorkerLoadSnapshot.role 透出到 KvcmFallbackResult,按真实角色映射为 RoleType.PREFILL/RoleType.PDFUSION 后再构造 RoleAddr;并在 get_master_route_addrs 接受 KVCM 结果前校验角色属于 self.backend_role_list,不属于则视为不可用并打 warning。补一条 PDFUSION 部署下的兜底选路用例,断言不会同时下发 PREFILL 与 PDFUSION 两条地址。
Checklist: [6.1] 分布式/跨平台变更有对应覆盖
| @@ -223,16 +223,25 @@ async def route_ips(self, input: GenerateInput): | |||
| input_token_batched = True | |||
There was a problem hiding this comment.
📍 实际位置 rtp_llm/server/backend_rpc_server_visitor.py:141(不在 diff 展示范围内,就近挂载)
[P2] local_fallback_addr 在生产链路从不传入,本机保底候选不可达
kvcm_fallback.py:3-4 与 build_candidate_pool:286 均声明打分集合为「caller's local worker plus KVCM cache hits」「always retaining the local worker」,master_client.py:527 也新增了 local_fallback_addr 形参。但全仓该参数唯一传入点是 master_client_fallback_test.py:171;唯一生产调用方 get_master_route_addrs:141-145 只传 block_cache_keys/input/request_id。故线上 local_candidate 恒为 None,master_client.py:346 与 build_candidate_pool:305-328 的本机合并分支恒不执行——KVCM 无正向命中时本机 worker 也不会被兜底考虑,而测试制造了该能力已生效的假象。
建议: 二选一并保持自洽:其一,在 get_master_route_addrs 中构造本机(或 host_service 可解析的本组 worker)RoleAddr 传入,并补一条「视图层传参 → 候选池含本机 → KVCM 零命中仍可路由」的端到端用例;其二,先移除该形参、_try_kvcm_fallback 的 local_candidate 装配与 build_candidate_pool 的本机分支,同步修正 docstring,待真正接线时再引入,避免不可达代码掩盖能力缺口。
| @@ -223,16 +223,25 @@ async def route_ips(self, input: GenerateInput): | |||
| input_token_batched = True | |||
There was a problem hiding this comment.
📍 实际位置 rtp_llm/server/backend_rpc_server_visitor.py:162(不在 diff 展示范围内,就近挂载)
[P2] KVCM 兜底缺少专属指标,三个新增响应字段无任何生产消费方
全仓检索显示新增的 route_source/cache_match/kvcm_outcome(master_client.py:47-49,:436-438 填充)仅在 master_client.py 内部赋值与 master_client_fallback_test.py:191,228 断言,无任何生产读取方。:162 对 KVCM 兜底成功与 FlexLB 正常成功一律上报同一个无 tag 的 MASTER_ROUTE_QPS_METRIC,运维无法度量降级流量占比,也没有 KVCM 命中率/时延/失败原因指标。同时开关开启且 master_addr 为空时 MasterClient 不发 HTTP 直接返回 connection_failed=True,:165 的 route_logger.error 会按请求量刷 ERROR,而该分支并不上报 MASTER_ROUTE_ERROR_QPS_METRIC(仅 :148 异常路径上报),形成「日志洪水 + 指标空白」。
建议: 在 :162 与 :236 上报时带上 {"route_source": route_result.route_source} 维度(:148-152 已有 tag 用法),或新增独立的 KVCM 降级 QPS/命中率/时延/selection_reason 分布指标,使灰度与回滚有量化依据。把「master 缺失 + 兜底未命中且已有域名兜底」的日志按 connection_failed 降为 warning 或按窗口聚合,并补对应 error QPS;对 _parse_candidates:604-608 静默 continue 丢弃的非法候选补限流日志或计数。
Checklist: [6.1] 回滚路径:风险行为存在运维回滚手段
| ) -> None: | ||
| self.config = config | ||
| self._bootstrap_resolver = bootstrap_resolver | ||
| self._channels: dict[str, grpc.aio.Channel] = {} |
There was a problem hiding this comment.
[P2] gRPC channel 与选中时间字典只增不减,滚动发布后累积僵尸连接
_channels/_stubs/_worker_status_channels/_worker_status_stubs(510-513)与 _last_selected_ns(517)以 target 为键只做写入:_stub_for:523-536、_worker_status_stub_for:544-557 未命中即新建并永久登记,query_and_select:746 也只写不清。唯一回收点是 close()(771-784),而 MasterClient.close() 全仓仅被测试调用、生产无调用方。_invalidate_leader:594-597 只清 _leader 字符串,不关闭对应 channel。前端进程长驻且 worker 会滚动替换,被 KVCM 返回过的每个历史 host 都会永久留下一个带 30s keepalive 的 grpc.aio.insecure_channel(持续对已下线 IP 发心跳),_last_selected_ns 同样无限增长。
建议: 给这四个 dict 加容量上限与 LRU/TTL 淘汰(淘汰时 await channel.close()),或在 _invalidate_leader 与 worker 探测连续失败时主动关闭并移除对应 channel/stub;_last_selected_ns 建议按 candidate_pool_size 的数倍设上限并按时间淘汰陈旧条目。
| master_route_result: Optional[FlexlbResponse] = None | ||
| if not role_addrs_specified and master_addr and not input_token_batched: | ||
| can_attempt_master_route = bool(master_addr) or bool( | ||
| self.master_client.client_fallback_enabled |
There was a problem hiding this comment.
[P3] 路由可用性判断跨类重复,visitor 直接读取 MasterClient 内部开关
门控直接读 self.master_client.client_fallback_enabled(:227) 判断能否尝试 master 路由,而 MasterClient 内部真正的准入前置是「client_fallback_enabled 为真 且 _kvcm_fallback_client 非空」(master_client.py:148-149、:342-343)。同一判断被拆到两个类:后续若 MasterClient 增加准入条件(如 KVCM 熔断态、客户端创建失败降级),visitor 侧门控会静默失配,请求先进入注定失败的路径再回落。
建议: 由 MasterClient 暴露单一 can_route()(或 is_available())方法封装全部准入条件,visitor 只依赖该方法,避免可用性判断在两处漂移;顺带便于为熔断态预留扩展点。
Checklist: [6.1] DIP:高层策略不依赖非必要具体细节
| @@ -223,16 +223,25 @@ async def route_ips(self, input: GenerateInput): | |||
| input_token_batched = True | |||
There was a problem hiding this comment.
📍 实际位置 rtp_llm/server/backend_rpc_server_visitor.py:133(不在 diff 展示范围内,就近挂载)
[P3] token_ids 到 list 的 O(prompt_len) 转换在路由路径上重复执行两次
get_master_route_addrs:133-137 已把 input.token_ids 整条 tolist() 为 Python list 用于 get_block_cache_keys,但只把结果 keys 传给 MasterClient;进入 KVCM 兜底后 _input_ids_for_kvcm:320-331 又对同一 tensor 再做一次 tolist() 并逐元素 int() 转换。同一份 prompt 在同一请求内被完整物化两次,长 prompt 下是纯浪费;且第二次转换只在兜底路径需要,而兜底恰好发生在 FlexLB 故障、流量最紧张的时刻。本条未做基准测量,按可读性/DRY 问题定级。
建议: 在 get_master_route_addrs 中把已转换好的 token_ids 列表随参数传给 get_backend_role_addrs(或缓存在请求上下文中),让 _try_kvcm_fallback 直接复用,删除 _input_ids_for_kvcm 中的二次 tolist() 与逐元素 int()。
Checklist: [I] 同一功能用统一工具函数
| GaugeMetrics.MASTER_ROUTE_RT_METRIC, master_route_timer.cost_ms() | ||
| ) | ||
| elif not role_addrs_specified: | ||
| route_logger.warning( |
There was a problem hiding this comment.
[P3] 回落 domain 路由的 warning 措辞与实际条件不符且为逐请求热路径噪声
:240-246 文案为 "master address: %s, client fallback enabled: %s, or input token batched: %s is not valid, fallback to domain routing",把三个状态并列后接单数 "is not valid"。该分支实际触发条件是 (master_addr 为空 且 开关关闭) or input_token_batched,既不指明究竟哪一项导致降级,排障还需人工反推布尔组合;且在 master 常态缺失的部署下按请求量刷屏。
建议: 改为结论式表述:先计算 reason = "batched_input" if input_token_batched else "no_master_addr_and_fallback_disabled",再打印 reason 与三个原始取值,并按窗口聚合或降为 debug,便于日志检索聚合且避免热路径噪声。
Checklist: [6.1] 可观测性:日志/指标/超时可操作、非噪声;[6.1] 无 per-forward 调试日志 / 噪声热路径输出
| @@ -0,0 +1,235 @@ | |||
| import asyncio | |||
There was a problem hiding this comment.
[P3] 测试脚手架存在未使用 import 与可参数化的重复用例体
第 1 行 import asyncio 全文件未使用(两个异步用例由 unittest.IsolatedAsyncioTestCase 驱动)。:211-231 的两个用例体高度重复:均为「构造 visitor → await route_ips → 断言 master_client.calls / domain_calls / 首个 addr 的 ip」,仅 client_fallback_enabled 与三组期望值不同。
建议: 删除未使用的 import asyncio;两个用例体用 subTest 或参数化表((enabled, expected_master_calls, expected_domain_calls, expected_ip))合并为一个,减少后续新增分支时的复制成本。
Checklist: [6.1] DRY:重复非平凡逻辑被抽取或显式复用;[6.1] KISS/YAGNI:无投机性抽象
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1335
Status: BLOCKING
Summary: P0/2 · P1/3 · P2/14 · P3/6
Reviewed: commit 1b8cfbee4553 · 2026-08-27 04:20 UTC+8
Blocking Issues
P0
- 无 package 的 worker_status proto 与引擎 model_rpc_service proto 根符号完全重名,开关一开即 duplicate symbol 崩溃 @
rtp_llm/server/worker_status_proto/worker_status_service.proto:3- 建议:优先删除整份副本,直接复用已
//visibility:public的//rtp_llm/cpp/model_rpc/proto:model_rpc_service_py_proto,改用其RpcServiceStub(wire path/RpcService/GetWorkerStatus与新桩逐字一致,StatusVersionPB/WorkerStatusPB也已导出),可一并消除重复 descriptor 与人工同步漂移。若确需独立副本,必须声明专属 package(如rtp_llm.fallback.worker_status)或加载到私有DescriptorPool隔离——proto 线格式不含类型名,客户端以channel.unary_unary("/RpcService/GetWorkerStatus", ...)即可保持路径不变。无论哪种,都要补一条「同进程先 import 引擎 pb2 再 importkvcm_fallback」的非 manual 用例固化该不变量。
- 建议:优先删除整份副本,直接复用已
- server py_library 的 srcs glob 漏掉 worker_status_proto,wheel 与 runfiles 均缺该模块 @
rtp_llm/server/BUILD:5- 建议:在 glob 中补上
"worker_status_proto/*.py",与同一 hunk 的kvcm_proto/*.py对称(不建议改**/*.py,会误吞子包测试文件)。若采纳上一条建议复用引擎 stub,则该目录与本问题一并消失。同时增加一条非 manual 的最小 py_test,只做import rtp_llm.server.kvcm_fallback且走真实包路径(不伪造rtp_llm命名空间、不用spec_from_file_location),让「新增子包未进 srcs」在构建/测试阶段而非上线时暴露。
- 建议:在 glob 中补上
P1
- 三个新增兜底测试全部标记 manual,且伪造掉两个 P0 所在的生产边界,新特性 CI 零覆盖 @
rtp_llm/server/test/BUILD:35- 建议:
master_client_fallback_test(已替换_send_schedule_request)与backend_rpc_server_visitor_fallback_test(纯 asyncio、无网络无 GPU)应直接去掉manual;kvcm_fallback_test仅用本地环回 grpc,也适合作为常规 CPU 单测。若沙箱内grpc.aio端口确实不稳定,请把纯逻辑用例(VllmBlockHashTest、候选池/打分用例)拆到独立的非 manual target,而非整体排除,并在 BUILD 注释写明原因。同时补一条不伪造ModelRpcClient/MasterClient的用例:真实构造MasterClient(master_config=<开关开启且三项必填已给值>)并断言构造成功,可一次锁定符号注册与打包完整性两个不变量。
- 建议:
- KVCM leader 解析失败无负缓存且被单锁串行,故障期逐请求放大延迟 @
rtp_llm/server/kvcm_fallback.py:641- 建议:失败路径同样写入时间戳(引入独立
_leader_failed_at+ 指数退避),退避窗口内直接快速失败并交由上层走 domain 路由;_leader_lock改为「单飞刷新 + 其余请求不等待直接返回失败」;并对整个 bootstrap 扫描加总时限或改为并发探测取首个成功者。leader_refresh_interval_ms目前仅有 dataclass 默认值、无法从 server args 配置,建议一并暴露或在代码中注明为固定常量。
- 建议:失败路径同样写入时间戳(引入独立
- 兜底链路无整体时间预算,WorkerStatus 进程级信号量获取也无 deadline @
rtp_llm/server/kvcm_fallback.py:746- 建议:用
asyncio.wait_for给query_and_select施加「本请求剩余 TTFT 预算」上限,超时即返回 None 让上层走 domain fallback;给信号量获取单独加 deadline(可复用worker_status_timeout_ms,超时视为该候选探测失败);并在连续失败后引入短路/熔断(N 次连续失败后冷却窗口内跳过探测)。同时按预期兜底 QPS 上调master_client_fallback_worker_status_concurrency默认值或改为按请求限流,并在 help 中给出按 QPS 计算的推荐值。
- 建议:用
Non-blocking Suggestions
P2
- 兜底返回的 RoleAddr 硬编码为 PREFILL,与打分层的 PDFUSION 支持不一致 @
rtp_llm/server/master_client.py:501- 建议:用探测到的
WorkerStatusPB.role反查RoleType后再构造RoleAddr,无法映射的候选直接跳过;候选快照 resolver 按backend_role_list覆盖 PDFUSION 而非固定RoleType.PREFILL。并补一条 PDFUSION 部署的用例,断言不会产出该部署不存在的角色地址。
- 建议:用探测到的
- block→token 折算所用 block_size 与引擎 seq_size_per_block 无一致性校验,且 help 文案与实际用途不符 @
rtp_llm/server/kvcm_fallback.py:442- 建议:让
master_kvcm_block_size缺省时直接取引擎seq_size_per_block(构造BackendRPCServerVisitor时已可得),降级为可选覆盖项;若必须显式配置,请在_create_kvcm_fallback_client中与seq_size_per_block比对,不一致时 fail-fast 并在错误信息中带上参数名与环境变量名。修正 help 文案为「块数↔token 折算步长;缺失时用于 vLLM sha256_cbor 回退哈希」。补一条以真实get_block_cache_keys输出为入参的用例(现有测试传任意整数[1, 2],恰好掩盖该路径)。
- 建议:让
- FlexLB body 级 8600 由硬失败改为静默 domain fallback,且不受新开关保护 @
rtp_llm/server/master_client.py:702- 建议:若这是有意的语义修正,请在 PR 描述与 commit message 中明确说明,并把它与 KVCM 兜底拆成独立提交,便于单独回滚;若只希望在新开关下生效,请把该分支置于
client_fallback_enabled门控内。无论哪种,都应补一条断言「body code=8600 → domain fallback 且不抛异常」的非 manual 用例,并为该转换加上可观测的计数或限频日志。
- 建议:若这是有意的语义修正,请在 PR 描述与 commit message 中明确说明,并把它与 KVCM 兜底拆成独立提交,便于单独回滚;若只希望在新开关下生效,请把该分支置于
- 兜底路径缺少专属指标,三个新增响应字段无生产消费方,且无 master 地址时逐请求打 ERROR @
rtp_llm/server/backend_rpc_server_visitor.py:162- 建议:在
get_master_route_addrs成功分支读取route_source/kvcm_outcome,为兜底单独上报带route_source标签的 QPS 与 RT,并增加selection_reason维度,使降级窗口可被告警与灰度观察、进而支撑开关回滚决策;把「无可用 master 地址」与「真实连接失败」在语义上区分(前者按 WARNING 或独立 outcome 限频记录),不要复用connection_failed的建连失败含义;_parse_candidates(kvcm_fallback.py:689-696) 丢弃格式不符或端口越界的候选时至少记一次聚合计数,避免 KVCM 返回域名时无声退化为纯冷路由。
- 建议:在
- flexlb_transport_timeout_ms 默认 0 使挂死场景兜底不可达,且开关关闭时也照样生效 @
rtp_llm/server/master_client.py:639- 建议:给该参数设置有界非零默认值(与
master_kvcm_request_timeout_ms=100同量级的百毫秒级),或在开启master_client_fallback且该值为 0 时打印明确启动告警说明「兜底仅对连接级失败生效」;help 中补充 0 的含义;同时让该参数仅在client_fallback_enabled为真时生效,避免关闭开关时改变既有超时语义。并在 PR description 中给出推荐值与回滚方式。
- 建议:给该参数设置有界非零默认值(与
- 兜底必填配置项带永远非法的默认值,help 未标注且缺失项需逐轮启崩试错 @
rtp_llm/server/server_args/master_group_args.py:80- 建议:在三个 flag 的 help 中显式标注 required-when-
master_client_fallback;在init_master_group_args调用点之后(或给MasterConfig增加校验钩子)对开关打开做一次联合校验,一次性汇总输出全部缺失字段清单;异常信息中带上--master_kvcm_*/MASTER_KVCM_*名称便于定位。block_size按前述建议缺省取seq_size_per_block,从必填项降级为可选覆盖项。
- 建议:在三个 flag 的 help 中显式标注 required-when-
- gRPC channel 与选择历史字典只增不减,滚动发布后累积僵尸连接 @
rtp_llm/server/kvcm_fallback.py:594- 建议:给两组 channel 字典加上按容量或空闲时长的淘汰(淘汰时
await channel.close()),_last_selected_ns同样按容量截断,或只保留当前候选集合与最近一个 stale 窗口内的 target。
- 建议:给两组 channel 字典加上按容量或空闲时长的淘汰(淘汰时
- 兜底链路存在无消费方的死代码与无法配置的隐藏参数 @
rtp_llm/server/kvcm_fallback.py:394- 建议:删除
build_candidate_pool、select_max_local_affinity及local_fallback_addr/local_candidate参数链(连同__all__条目),相关测试改为直接测build_candidate_plan/select_cache_affinity_first。四项隐藏参数要么补上 flag,要么在代码中注明为固定常量;其中p2p_host_count=0与 FlexLBKvcmConfig.DEFAULT_P2P_HOST_COUNT一致,但既已暴露--master_client_fallback_p2p_hit_discount,建议在其 help 中说明「仅当 KVCM 返回 p2p 匹配时生效」,避免运维误以为该折扣当前已起作用。
- 建议:删除
- 手工签入 pb2/pb2_grpc 绕开构建期 codegen,无漂移校验且格式风格分叉 @
rtp_llm/server/worker_status_proto/worker_status_service_pb2_grpc.py:1- 建议:优先按既有约定改为 Bazel 构建期生成(复用
generate_grpc_proto),入库仅保留.proto。若因外部服务 proto 拷贝必须签入,请补充固定 grpcio-tools 版本的再生成命令、在 CI 增加「重新生成后 diff 为空」的 drift 检查,并在文件头注明 canonical 来源路径与同步要求;同时二选一统一格式策略:把这两个生成目录加入 pre-commitexclude以保持与生成器逐字节一致,或在再生成流程中固定「生成后立即 black」。
- 建议:优先按既有约定改为 Bazel 构建期生成(复用
- 测试在模块导入期永久污染 sys.modules 且不恢复,影子生产类型已与生产漂移 @
rtp_llm/server/test/backend_rpc_server_visitor_fallback_test.py:146- 建议:抽出共享测试辅助模块(如
rtp_llm/server/test/fake_rtp_namespace.py),统一提供「注入并在退出时恢复」的 context manager 供三份测试复用;把替身安装移入setUpModule/tearDownModule或unittest.mock.patch.dict(sys.modules, ...)限定作用域,避免模块导入期的不可逆全局副作用。同时删除影子FlexlbResponse,直接从生产模块导入(master_client_fallback_test.py:66-72已证明该模块可独立加载),把契约钉在生产类上。
- 建议:抽出共享测试辅助模块(如
- master_group_args_test 绑定空 SimpleNamespace 并 stub str2bool,断言退化为字面量自证 @
rtp_llm/server/server_args/test/master_group_args_test.py:29- 建议:改为数据驱动:以真实
MasterConfig()实例作为master_config传入,遍历其全部master_*属性,断言每个属性都存在同名 flag、bind_to目标hasattr为真、且arguments[flag]["default"] == getattr(MasterConfig(), field);引入真实str2bool并断言布尔类参数type is str2bool,同时删除抄写的字面量清单。更彻底的做法是参考同目录server_args_test.py走真实setup_args(),断言 env 与命令行两条注入路径下master_config.<field>的最终取值。
- 建议:改为数据驱动:以真实
- 对 MasterConfig 全量使用 getattr 字面量访问,默认值在三处重复维护 @
rtp_llm/server/master_client.py:161- 建议:以
MasterConfig为单一事实来源:master_group_args.py的default=直接引用传入的master_config实例属性,master_client.py改用直接属性访问;_input_ids_for_kvcm改为按GenerateInput的实际字段名直接取值。测试改为数据驱动:遍历MasterConfig()的全部master_*属性,断言每个属性都有同名 flag 且default与实例初值一致。
- 建议:以
- 新增的 VipHostSnapshot 后台刷新状态机没有任何测试覆盖 @
rtp_llm/server/host_service.py:125- 建议:新增一个针对
VipHostSnapshot的非 manual py_test(注入假VipServerWrapper与可控时钟,直接调refresh_now()驱动而不依赖真实线程时序,避免 sleep 类不稳定测试),至少覆盖:首次构造复用初始 hosts、刷新抛异常但未过期时保留旧快照、超过 stale 阈值后返回空、按(ip, port)去重、close()后线程退出且幂等。
- 建议:新增一个针对
- 门槛放宽后的关键分支未覆盖,包括兜底失败回落 domain 与批量输入 @
rtp_llm/server/test/backend_rpc_server_visitor_fallback_test.py:210- 建议:补四个用例(可用
subTest参数化「兜底返回态」避免复制):FlexlbResponse(role_addrs=None, connection_failed=True)时断言host_service.domain_calls == 1且不抛异常;构造shape=(2,4)、size(0)==2的 token_ids 断言master_client.calls == 0;role_addrs已指定时断言不调用 master_client;backend_role_list=[PREFILL, DECODE]而 master 仅返回 PREFILL 时断言 domain 补齐 DECODE。
- 建议:补四个用例(可用
P3
- 路由准入判定跨类重复,visitor 直接读取 MasterClient 内部开关 @
rtp_llm/server/backend_rpc_server_visitor.py:227- 建议:在
MasterClient上暴露语义化方法(如can_attempt_route()),内部聚合master_addr、slave_addr与_kvcm_fallback_client是否就绪返回布尔值;route_ips只调用该方法,使准入条件只有一个权威定义点,新增兜底通道时也无需改动调用方。
- 建议:在
- token_ids 到 list 的 O(prompt_len) 转换在路由路径上重复执行两次 @
rtp_llm/server/backend_rpc_server_visitor.py:133- 建议:把
:133-137已算好的token_ids列表随block_cache_keys一并传入get_backend_role_addrs/_try_kvcm_fallback,删除_input_ids_for_kvcm中的重复转换(其hasattr/getattr猜测式访问也可一并去掉,GenerateInput是强类型数据类)。
- 建议:把
- 并发上界为单边断言,无法区分正确限流与并发彻底失效 @
rtp_llm/server/test/kvcm_fallback_test.py:280- 建议:把断言改为双边,例如
assertEqual(2, self.worker_service.max_active)(配合足够的delay_s保证观测窗口),或至少加上assertGreater(self.worker_service.max_active, 1),使限流退化为串行时能被检出。
- 建议:把断言改为双边,例如
- master_kvcm_use_local 场景下 service_id 中填写的端口被静默丢弃 @
rtp_llm/server/master_client.py:213- 建议:
use_local分支优先使用解析出的host.port,仅当其缺失或非法时回退bootstrap_port;或在 help 中明确「IP:port 列表中的端口会被--master_kvcm_bootstrap_port覆盖」,并在构造 target 时对被替换的端口打印一次启动日志。
- 建议:
- 新开关被 host_service.service_available 前置门控静默旁路 @
rtp_llm/server/backend_rpc_server_visitor.py:324- 建议:在
master_client_fallback的 help 中说明「需至少配置一个 VIP 域名,否则路由入口不会被触发」;或在启动时若开关打开而service_available为 False 则打印一次明确 warning。
- 建议:在
- 测试文件存在未使用的 import @
rtp_llm/server/test/backend_rpc_server_visitor_fallback_test.py:1- 建议:删除
import asyncio。
- 建议:删除
Checklist Findings (24 fail / 48 total)
General Principles Checklist
- [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue
无 package 的 worker_status proto 与引擎 model_rpc_service proto 根符号完全重名,开关一开即 duplicate symbol 崩溃
该文件注释写明「Keep this file package-less」,顶层定义StatusVersionPB/TaskInfoPB/WorkerStatusPB/KvCacheGroupModePB与service RpcService(:73)。已用^package检索确认model_rpc_service.proto全文无 package,且在 342/390/439/445/654 行定义完全同名符号。worker_status_service_pb2.py:17的序列化描述符中符号均无命名空间前缀(.TaskInfoPB、.KvCacheGroupModePB),并向_descriptor_pool.Default()执行AddSerializedFile。同进程链路:backend_rpc_server_visitor.py:10→model_rpc_client.py注册第一份,同文件 :78 构造MasterClient,开关开启时 `master_client.py:149-1 - [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
master_group_args_test 绑定空 SimpleNamespace 并 stub str2bool,断言退化为字面量自证
_load_init_master_group_args(:25-44) 把util整体替换为str2bool = bool的假模块,测试又未断言type,因此MASTER_CLIENT_FALLBACK=false被bool()误判为 True 这一典型环境变量陷阱测不出来。master_config = SimpleNamespace()(:50) 是空对象,bind_to只被记录从未回写,:117-120 的断言只是在比对测试里抄写的 15 条字符串与master_group_args.py中的同名字面量,无法发现「绑定到MasterConfig上不存在的字段」。default也未与MasterConfig()初值比对;唯一负向断言assertNotIn("--master_kvcm_fallback_enabled")(:126) 针对一个从未存在的名字。这是本 PR 唯一进 CI 的新测试。 - [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue
路由准入判定跨类重复,visitor 直接读取 MasterClient 内部开关
:226-228与:244让 visitor 直接读self.master_client.client_fallback_enabled决定「能否尝试 master 路由」,而_try_kvcm_fallback内部已用if not self.client_fallback_enabled or self._kvcm_fallback_client is None: return None(master_client.py:426-427) 做过同类判断。同一准入语义在两层重复:后续若演进为「开关打开 + 客户端已就绪 + 未熔断」,两处必须同步修改,漏改一处即行为不一致;高层路由策略也因此依赖了MasterClient的实现细节。该属性还成了每请求读取的公开契约,一旦改名或延迟初始化即产生每请求 AttributeError,而当前无任何 CI 用例守护。 - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
token_ids 到 list 的 O(prompt_len) 转换在路由路径上重复执行两次
get_master_route_addrs在:133-137已对input.token_ids执行一次tolist()用于get_block_cache_keys,随后_try_kvcm_fallback又通过_input_ids_for_kvcm(master_client.py:404-405)对同一张量再次tolist(),并在:415逐元素int()转换。开关开启后每个走兜底的请求都要在路由路径上重复一次 O(prompt_len) 的张量转列表与整型转换。未做基准测量,故仅记为可零成本消除的冗余计算。 - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
新开关被 host_service.service_available 前置门控静默旁路
enqueue(:324) 与batch_enqueue(:335) 均以if self.host_service.service_available:门控route_ips。该字段在host_service.py:660-662定义为bool(self.master_vip.domain) or any(self.role_vip_map.values())。因此当运维未配置任何 master 域名与角色域名、期望纯靠 KVCM 客户端兜底选点(KVCM 热候选并不依赖 role VIP)时,route_ips根本不会被调用,master_client_fallback=True被静默忽略且无任何日志提示。该配置组合较为边缘,故仅记为语义说明缺口。 - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
新增的 VipHostSnapshot 后台刷新状态机没有任何测试覆盖
本 PR 新增VipHostSnapshot(:125):构造期起 daemon 线程(:157-163)、_publish(:180-183) 对空结果提前 return 以保留 last-known-good、refresh_now(:188) 在超过stale_timeout_seconds后清空快照(:208-214)、get_hosts(:221) 再做一次 stale 判定、close(:232) 负责 join。它是兜底候选池与 KVCM bootstrap target 的共同数据源(master_client.py:217-231直接依赖),docstring 明确承诺「保留上一个非空值直到过期」。但三个新增测试文件无一覆盖它,rtp_llm/server/test/BUILD也未为它新增 target,host_service_test.py不在本次改动范围且其 target 亦为 manual。 - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
新开关被 host_service.service_available 前置门控静默旁路
enqueue(:324) 与batch_enqueue(:335) 均以if self.host_service.service_available:门控route_ips。该字段在host_service.py:660-662定义为bool(self.master_vip.domain) or any(self.role_vip_map.values())。因此当运维未配置任何 master 域名与角色域名、期望纯靠 KVCM 客户端兜底选点(KVCM 热候选并不依赖 role VIP)时,route_ips根本不会被调用,master_client_fallback=True被静默忽略且无任何日志提示。该配置组合较为边缘,故仅记为语义说明缺口。 - [6.1] Quality — Commit 原子、message 与行为匹配 → issue
FlexLB body 级 8600 由硬失败改为静默 domain fallback,且不受新开关保护
已核对本 PR 的 diff:if code == FALLBACK_ERROR_CODE: return FlexlbResponse.fallback_response()为新增行。改动前 HTTP 200 且 bodycode=8600会走到 :704-715——ExceptionType(8600)抛 ValueError(exceptions.py:78确认 8600 非枚举成员,仅有MASTER_NO_AVAILABLE_WORKER = 8400)→ 降级为MASTER_NO_AVAILABLE_WORKER→raise FtRuntimeException,请求显式失败。改动后返回 fallback 响应,使backend_rpc_server_visitor.py:251-253的allow_domain_fallback成立并静默改走 domain 路由。该分支不在client_fallback_enabled门控之内,因此所有部署(含未开启新开关者)的错误语义都被改变,PR 描述未说明该 - [6.1] Quality — PR description 说明动机与设计 → issue
兜底必填配置项带永远非法的默认值,help 未标注且缺失项需逐轮启崩试错
--master_kvcm_block_size默认 0(py_config_modules.py:394),而KvcmFallbackConfig.__post_init__(kvcm_fallback.py:63-75)对block_size有must be positive校验,即默认值恒为非法;--master_kvcm_service_id(:53) 与--master_kvcm_instance_id(:71) 默认空串,同样在master_client.py:202-209抛 ValueError。三者 help 文案均未标注「启用master_client_fallback时必填」,parser 层无交叉校验;且:202-211是逐条 raise,运维只能靠把进程启崩、每轮读一个异常信息来试错。异常文案KVCM block_size must be positive也未带对应 flag/env 名。 - [6.1] Quality — 无 per-forward 调试日志 / 噪声热路径输出 → issue
兜底路径缺少专属指标,三个新增响应字段无生产消费方,且无 master 地址时逐请求打 ERROR
全仓 py 检索确认route_source/kvcm_outcome/cache_match仅在master_client.py内被赋值传递,并只被master_client_fallback_test.py:189,226断言,无任何 kmonitor 上报。兜底成功后route_result.is_ok为真,:155-163照旧上报无 tag 的MASTER_ROUTE_QPS_METRIC,兜底的 KVCM 查询 + 探测耗时也被折叠进:236-238的MASTER_ROUTE_RT_METRIC。另一侧:开关开且master_addr为空时不发任何 HTTP,master_client.py:655直接以connection_failed_response()起始,兜底未命中仍带connection_failed=True,导致:165每请求打一条 ERROR(改动前该场景仅:240一条 WARNING)。运维因此无法区分「FlexLB 正常」与「全靠兜底」。 - [6.1] Quality — 逻辑变更未混入无关格式化 → issue
测试文件存在未使用的 import
第 1 行import asyncio,但全文未出现任何asyncio.引用(异步用例由unittest.IsolatedAsyncioTestCase驱动)。仓库 flake8 默认启用 F401,未使用 import 会在 pre-commit / lint 阶段报错,属可零成本消除的噪声。 - [6.1] Software Engineering — DIP:高层策略不依赖非必要具体细节 → issue
路由准入判定跨类重复,visitor 直接读取 MasterClient 内部开关
:226-228与:244让 visitor 直接读self.master_client.client_fallback_enabled决定「能否尝试 master 路由」,而_try_kvcm_fallback内部已用if not self.client_fallback_enabled or self._kvcm_fallback_client is None: return None(master_client.py:426-427) 做过同类判断。同一准入语义在两层重复:后续若演进为「开关打开 + 客户端已就绪 + 未熔断」,两处必须同步修改,漏改一处即行为不一致;高层路由策略也因此依赖了MasterClient的实现细节。该属性还成了每请求读取的公开契约,一旦改名或延迟初始化即产生每请求 AttributeError,而当前无任何 CI 用例守护。 - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
token_ids 到 list 的 O(prompt_len) 转换在路由路径上重复执行两次
get_master_route_addrs在:133-137已对input.token_ids执行一次tolist()用于get_block_cache_keys,随后_try_kvcm_fallback又通过_input_ids_for_kvcm(master_client.py:404-405)对同一张量再次tolist(),并在:415逐元素int()转换。开关开启后每个走兜底的请求都要在路由路径上重复一次 O(prompt_len) 的张量转列表与整型转换。未做基准测量,故仅记为可零成本消除的冗余计算。 - [6.1] Software Engineering — ISP:调用方不依赖无关大接口 → issue
兜底链路存在无消费方的死代码与无法配置的隐藏参数
全仓 py 检索确认build_candidate_pool(:394) 与select_max_local_affinity(:286) 只出现在定义处与__all__(:923,928),零调用方(连测试都没有),却被注释为「compatibility helper」——而该模块是本 PR 全新引入,不存在需兼容的历史调用方。local_fallback_addr从get_backend_role_addrs(master_client.py:598) 一路透传到_try_kvcm_fallback后被del丢弃(:431),唯一生产调用方backend_rpc_server_visitor.py:141-145并未传该参数,query_and_select的local_candidate因此永远为 None。p2p_host_count、lookahead_tokens、minimum_local_blocks、leader_refresh_interval_ms四项均无对应 CLI - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
兜底链路存在无消费方的死代码与无法配置的隐藏参数
全仓 py 检索确认build_candidate_pool(:394) 与select_max_local_affinity(:286) 只出现在定义处与__all__(:923,928),零调用方(连测试都没有),却被注释为「compatibility helper」——而该模块是本 PR 全新引入,不存在需兼容的历史调用方。local_fallback_addr从get_backend_role_addrs(master_client.py:598) 一路透传到_try_kvcm_fallback后被del丢弃(:431),唯一生产调用方backend_rpc_server_visitor.py:141-145并未传该参数,query_and_select的local_candidate因此永远为 None。p2p_host_count、lookahead_tokens、minimum_local_blocks、leader_refresh_interval_ms四项均无对应 CLI - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
兜底返回的 RoleAddr 硬编码为 PREFILL,与打分层的 PDFUSION 支持不一致
select_cache_affinity_first(kvcm_fallback.py:461) 明确接受 role 含PREFILL或PDFUSION的 worker,但_try_kvcm_fallback恒返回role=RoleType.PREFILL,候选快照也固定enable_role_snapshot(RoleType.PREFILL)(master_client.py:325) 与get_snapshot(RoleType.PREFILL)(:333)。PDFUSION 部署下enable_role_snapshot因role_vip_map无 PREFILL 直接返回 False(host_service.py:712-714),冷候选为空;被选中的 PDFUSION worker 仍被标为 PREFILL,backend_rpc_server_visitor.py:248的need_domain_routing因backend_role_list=[PDFUSION]不被 - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
门槛放宽后的关键分支未覆盖,包括兜底失败回落 domain 与批量输入
两个用例只覆盖「开关开 + 兜底成功」(:211) 与「开关关 + 直接 domain」(:223),实际只验证了can_attempt_master_route这一布尔门控。生产门控为not role_addrs_specified and can_attempt_master_route and not input_token_batched(backend_rpc_server_visitor.py:229-233),而测试的_TokenIds.size恒返回 1(:152-154)、role_addrs恒为空(:206)、get_master_addr恒返回 None(:170)。因此「批量输入时即使开关开也必须跳过 master 路由」「role_addrs已指定时不得调用 master_client」「返回connection_failed=True/fallback=True时必须回落 domain 且不抛ROUTE_ERROR」「backend_role_list含 DECODE 而 master 仅返 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
master_kvcm_use_local 场景下 service_id 中填写的端口被静默丢弃
--master_kvcm_service_id的 help 写「KVCM bootstrap service id or local IP:port list」,master_kvcm_use_local=True时VipServerWrapper.__init__(host_service.py:95-102)确实按ip:port逐项解析为Host(ip, port)。但resolve_bootstrap_targets(master_client.py:223-231)只读host.ip,端口一律改用bootstrap_port(默认 6381),用户在 service_id 中写的端口被静默忽略。本地联调若填127.0.0.1:7000而未同时设置--master_kvcm_bootstrap_port,会连到一个未监听的端口,且无任何日志提示端口被替换。
RTP-LLM Checklist
- [I] 代码质量 — 同一功能用统一工具函数 → issue
手工签入 pb2/pb2_grpc 绕开构建期 codegen,无漂移校验且格式风格分叉
仓库既有约定是构建期生成:rtp_llm/cpp/model_rpc/proto/BUILD:21-42用generate_grpc_proto产出并以 public 暴露model_rpc_service_py_proto,.gitignore:45-46明确忽略生成的model_rpc_service_pb2(_grpc).py。本 PR 反向签入四份标注DO NOT EDIT!的生成物,既无 BUILD 生成规则,也无再生成脚本或 grpcio-tools 版本说明,.proto与桩之间无一致性校验;kvcm_meta_service.proto:5-6还要求与 FlexLB Java 侧 proto 手工同步。两份桩风格已分叉:kvcm_meta_service_pb2_grpc.py:17-26是 black 化结果(双引号、4 空格、尾随逗号),worker_status_service_pb2_grpc.py:17-21保留 protoc 原始单引号输出,而 pre-commit 的 black exclud
Python Static-First Checklist
- [P.A] 静态结构与类型纪律 — 禁止 getattr/setattr literal 访问 → issue
对 MasterConfig 全量使用 getattr 字面量访问,默认值在三处重复维护
_create_kvcm_fallback_client中 20 余处均为getattr(self.master_config, "字面量", 默认值)(:161-308),同一批默认值同时写在py_config_modules.py:389-414的字段初值与master_group_args.py:44-254的 argparsedefault=中,共三份。MasterConfig已保证字段存在,getattr兜底既掩盖拼写错误又制造第三份默认值来源;_input_ids_for_kvcm:399-404还对强类型GenerateInput做getattr("input_ids"/"token_ids", None)与hasattr("tolist")猜测式访问。而master_group_args_test只断言env_name/bind_to,不断言default,任一处漂移都不会被发现。 - [P.A] 静态结构与类型纪律 — 禁止 hasattr 做控制流分支 → issue
对 MasterConfig 全量使用 getattr 字面量访问,默认值在三处重复维护
_create_kvcm_fallback_client中 20 余处均为getattr(self.master_config, "字面量", 默认值)(:161-308),同一批默认值同时写在py_config_modules.py:389-414的字段初值与master_group_args.py:44-254的 argparsedefault=中,共三份。MasterConfig已保证字段存在,getattr兜底既掩盖拼写错误又制造第三份默认值来源;_input_ids_for_kvcm:399-404还对强类型GenerateInput做getattr("input_ids"/"token_ids", None)与hasattr("tolist")猜测式访问。而master_group_args_test只断言env_name/bind_to,不断言default,任一处漂移都不会被发现。 - [P.B] 错误处理 — 资源获取用 with context manager → issue
gRPC channel 与选择历史字典只增不减,滚动发布后累积僵尸连接
_channels/_stubs(:584-585) 按 bootstrap target 缓存,_worker_status_channels/_worker_status_stubs(:586-587) 按 worker status target 缓存,_last_selected_ns(:594) 按route_target只写不删累积;前四者仅在close()(:897-910) 中清理,_last_selected_ns连close()都不清理,全部无 LRU 或 TTL 淘汰。VIP 扩缩容、worker 迁移或滚动升级导致 IP 变化后,旧 target 的grpc.aiochannel 仍保留并按grpc.keepalive_time_ms=30_000(:606,627) 持续心跳,长期运行的前端进程会持续累积连接与内存。 - [P.F] 语言陷阱 — 禁止模块级 import 副作用 → issue
测试在模块导入期永久污染 sys.modules 且不恢复,影子生产类型已与生产漂移
:146在模块顶层执行_load_visitor_module(),把rtp_llm、rtp_llm.server、rtp_llm.config、rtp_llm.cpp、rtp_llm.metrics、rtp_llm.utils(:21-32) 以及torch(:92)、rtp_llm.ops(:111)、rtp_llm.cpp.model_rpc.model_rpc_client(:104)、rtp_llm.server.master_client(:122) 写入sys.modules后从不恢复;master_client_fallback_test.py:76同样在导入期无条件覆写。而kvcm_fallback_test.py:14-21用「不存在才注入」守卫、master_group_args_test.py:40-44在finally中恢复——同一需求三套不一致实现。测试内自建的影子FlexlbResponse(:56-65) 已缺少生产新增的route_source/`c - [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
门槛放宽后的关键分支未覆盖,包括兜底失败回落 domain 与批量输入
两个用例只覆盖「开关开 + 兜底成功」(:211) 与「开关关 + 直接 domain」(:223),实际只验证了can_attempt_master_route这一布尔门控。生产门控为not role_addrs_specified and can_attempt_master_route and not input_token_batched(backend_rpc_server_visitor.py:229-233),而测试的_TokenIds.size恒返回 1(:152-154)、role_addrs恒为空(:206)、get_master_addr恒返回 None(:170)。因此「批量输入时即使开关开也必须跳过 master 路由」「role_addrs已指定时不得调用 master_client」「返回connection_failed=True/fallback=True时必须回落 domain 且不抛ROUTE_ERROR」「backend_role_list含 DECODE 而 master 仅返
Strengths
- 兜底触发条件收得很准:仅
resp.connection_failed时进入(master_client.py:673),显式保留 8600 与业务/准入错误的原有语义,注释与实现一致。 - 开关默认
False(py_config_modules.py:389)且惰性 import,关闭时can_attempt_master_route等价于原bool(master_addr),具备明确的运维回滚手段。 - 打分逻辑(
select_cache_affinity_first、effective_cache_blocks、build_candidate_plan、_cold_rank)全部为无副作用纯函数,排序键均带route_target确定性 tie-breaker,线上决策可重放。 - 默认值与现网 FlexLB 逐项对齐且可交叉验证:
prefill_queue_size_threshold=1024、p2p_hit_discount=0.2、max_extra_work_tokens=0、min_hit_rate=5.0与FlexlbConfig.java:116,124,136,155一一对应;p2p_host_count=0也与KvcmConfig.java:23一致。 - 缓存键派生策略与 FlexLB 一致:均为「上游键优先、缺失时按 vLLM sha256_cbor 重算」,
_encode_vllm_hash_input的 CBOR 编码与BlockCacheKeyCalculator语义对齐。 KvcmFallbackConfig.__post_init__(:60-118)对端口区间、正数、非负数、hot<=total集中校验并显式排除bool,为 fail-fast 而非静默降级。VipHostSnapshot把阻塞式服务发现移出请求路径到 daemon 线程,读侧只持短锁并返回不可变 tuple,last-known-good + stale 语义清楚;_resolve_leader也用asyncio.to_thread避免阻塞事件循环。kvcm_meta_service.proto:3显式声明package kv_cache_manager.proto.meta,方法全名带完整命名空间前缀,天然规避符号污染,是两份 proto 中处理正确的一份。worker_status_service.proto对废弃字段正确使用reserved 2/reserved 19, 20并保留optionalpresence,与引擎 canonical proto 逐字段比对当前一致,字段号复用风险已规避。kvcm_fallback_test.py用真实grpc.aio.server()+127.0.0.1:0动态端口搭建假 MetaService/WorkerStatus 服务,并以 golden 向量锁死calculate_vllm_block_cache_keys,是本 PR 最扎实的一层覆盖。
| @@ -0,0 +1,75 @@ | |||
| syntax = "proto3"; | |||
|
|
|||
| // Keep this file package-less: the deployed engine serves | |||
There was a problem hiding this comment.
[P0] 无 package 的 worker_status proto 与引擎 model_rpc_service proto 根符号完全重名,开关一开即 duplicate symbol 崩溃
该文件注释写明「Keep this file package-less」,顶层定义 StatusVersionPB/TaskInfoPB/WorkerStatusPB/KvCacheGroupModePB 与 service RpcService(:73)。已用 ^package 检索确认 model_rpc_service.proto 全文无 package,且在 342/390/439/445/654 行定义完全同名符号。worker_status_service_pb2.py:17 的序列化描述符中符号均无命名空间前缀(.TaskInfoPB、.KvCacheGroupModePB),并向 _descriptor_pool.Default() 执行 AddSerializedFile。同进程链路:backend_rpc_server_visitor.py:10 → model_rpc_client.py 注册第一份,同文件 :78 构造 MasterClient,开关开启时 `master_client.py:14...
建议: 优先删除整份副本,直接复用已 //visibility:public 的 //rtp_llm/cpp/model_rpc/proto:model_rpc_service_py_proto,改用其 RpcServiceStub(wire path /RpcService/GetWorkerStatus 与新桩逐字一致,StatusVersionPB/WorkerStatusPB 也已导出),可一并消除重复 descriptor 与人工同步漂移。若确需独立副本,必须声明专属 package(如 rtp_llm.fallback.worker_status)或加载到私有 DescriptorPool 隔离——proto 线格式不含类型名,客户端以 channel.unary_unary("/RpcService/GetWorkerStatus", ...) 即可保持路径不变。无论哪种,都要补一条「同进程先 import 引擎 pb2 再 import kvcm_fallback」的非 manual 用例固化该不变量。
Checklist: [6.1] 依赖方向:无循环依赖/跨层惊喜
| @@ -4,9 +4,12 @@ py_library( | |||
| name = "server", | |||
| srcs = glob([ | |||
There was a problem hiding this comment.
[P0] server py_library 的 srcs glob 漏掉 worker_status_proto,wheel 与 runfiles 均缺该模块
py_library(name="server") 的 srcs 为 glob(["*.py", "server_args/*.py", "kvcm_proto/*.py"]),glob 不递归子目录。作者已为同批新增的 kvcm_proto/ 显式追加条目,却遗漏 worker_status_proto/;该目录内无自有 BUILD,全仓 BUILD* 检索 worker_status_proto 零命中,而磁盘上确实存在 __init__.py、.proto、_pb2.py、_pb2_grpc.py 四个文件。rtp_llm/BUILD:548-569 的 py_package 只收集 deps 闭包中已声明的 srcs,故该包既不进 wheel 也不进 py_test runfiles;而 kvcm_fallback.py:24-27 是模块级无条件绝对导入,master_client.py:149-150 无 try/except,开关打开时 MasterClient.__init__ 直接抛 `Module...
建议: 在 glob 中补上 "worker_status_proto/*.py",与同一 hunk 的 kvcm_proto/*.py 对称(不建议改 **/*.py,会误吞子包测试文件)。若采纳上一条建议复用引擎 stub,则该目录与本问题一并消失。同时增加一条非 manual 的最小 py_test,只做 import rtp_llm.server.kvcm_fallback 且走真实包路径(不伪造 rtp_llm 命名空间、不用 spec_from_file_location),让「新增子包未进 srcs」在构建/测试阶段而非上线时暴露。
| "//rtp_llm:grpcio", | ||
| "//rtp_llm:protobuf", | ||
| ], | ||
| tags = ["manual"], |
There was a problem hiding this comment.
[P1] 三个新增兜底测试全部标记 manual,且伪造掉两个 P0 所在的生产边界,新特性 CI 零覆盖
kvcm_fallback_test(:35)、master_client_fallback_test(:45)、backend_rpc_server_visitor_fallback_test(:54) 均带 tags = ["manual"],被 //...、:all 排除;同包 vit_proxy_server_test(:57-64) 未加 manual,说明该目录并非整体豁免。本 PR 唯一进 CI 的新测试是 master_group_args_test,只断言参数名映射。更关键的是被 stub 的位置恰好覆盖两个 P0:kvcm_fallback_test.py:12-24 自建 rtp_llm/rtp_llm.server 命名空间并把 __path__ 指向源码树,使引擎 model_rpc_service_pb2 永不被导入(符号冲突不可见);backend_rpc_server_visitor_fallback_test.py:104,122-126 把 ModelRpcClient=object、...
建议: master_client_fallback_test(已替换 _send_schedule_request)与 backend_rpc_server_visitor_fallback_test(纯 asyncio、无网络无 GPU)应直接去掉 manual;kvcm_fallback_test 仅用本地环回 grpc,也适合作为常规 CPU 单测。若沙箱内 grpc.aio 端口确实不稳定,请把纯逻辑用例(VllmBlockHashTest、候选池/打分用例)拆到独立的非 manual target,而非整体排除,并在 BUILD 注释写明原因。同时补一条不伪造 ModelRpcClient/MasterClient 的用例:真实构造 MasterClient(master_config=<开关开启且三项必填已给值>) 并断言构造成功,可一次锁定符号注册与打包完整性两个不变量。
| refresh_interval_s = self.config.leader_refresh_interval_ms / 1_000.0 | ||
| if self._leader and now - self._leader_refreshed_at < refresh_interval_s: | ||
| return self._leader | ||
| async with self._leader_lock: |
There was a problem hiding this comment.
[P1] KVCM leader 解析失败无负缓存且被单锁串行,故障期逐请求放大延迟
_resolve_leader:636-676 的快速路径依赖 self._leader 非空。冷启动且全部 bootstrap target 不可达时,:655-669 的循环全部 continue,previous_leader 为 None,于是 :676 抛错且不更新 _leader_refreshed_at——完全没有负缓存(只有 :673-675 的 stale-leader 分支才刷时间戳)。锁内每次都串行遍历全部 target,每个 target 超时 request_timeout_ms(默认 100ms)。兜底恰在 FlexLB 故障期逐请求触发,所有并发请求排在同一把 _leader_lock(:641) 之后并各自重跑完整扫描,第 N 个请求约需 N × len(targets) × 100ms,比改动前「立即 domain fallback」明显更差。失败虽被 :780-790 吞掉不致请求失败,但耗时已计入首包。
建议: 失败路径同样写入时间戳(引入独立 _leader_failed_at + 指数退避),退避窗口内直接快速失败并交由上层走 domain 路由;_leader_lock 改为「单飞刷新 + 其余请求不等待直接返回失败」;并对整个 bootstrap 扫描加总时限或改为并发探测取首个成功者。leader_refresh_interval_ms 目前仅有 dataclass 默认值、无法从 server args 配置,建议一并暴露或在代码中注明为固定常量。
| self, | ||
| candidate: KvcmCacheCandidate, | ||
| ) -> Optional[WorkerLoadSnapshot]: | ||
| async with self._worker_status_semaphore: |
There was a problem hiding this comment.
[P1] 兜底链路无整体时间预算,WorkerStatus 进程级信号量获取也无 deadline
_probe_worker 以 async with self._worker_status_semaphore 无超时获取进程级信号量(:591-593,asyncio.Semaphore(worker_status_concurrency),默认 3,help 亦写明「across fallback requests in one process」)。单次 RPC 有 200ms deadline,但排队等待无任何 deadline;query_and_select(:761) 与 _try_kvcm_fallback(master_client.py:433-438) 外层均无 asyncio.wait_for 整体预算,route_ips 侧也无连续失败短路。最坏耗时 = leader 串行扫描 + GetHostCacheState 100ms + 多轮探测(轮数随候选池增长)。FlexLB 整体不可用时全部流量同时涌入兜底,探测吞吐上限约 3/0.2s,队列持续堆积且无准入控制,兜底路由时延不可控——恰好发生在该特性最需要生效的场景。
建议: 用 asyncio.wait_for 给 query_and_select 施加「本请求剩余 TTFT 预算」上限,超时即返回 None 让上层走 domain fallback;给信号量获取单独加 deadline(可复用 worker_status_timeout_ms,超时视为该候选探测失败);并在连续失败后引入短路/熔断(N 次连续失败后冷却窗口内跳过探测)。同时按预期兜底 QPS 上调 master_client_fallback_worker_status_concurrency 默认值或改为按请求限流,并在 help 中给出按 QPS 计算的推荐值。
| return FlexlbResponse.ok( | ||
| [ | ||
| RoleAddr( | ||
| role=RoleType.PREFILL, |
There was a problem hiding this comment.
[P2] 兜底返回的 RoleAddr 硬编码为 PREFILL,与打分层的 PDFUSION 支持不一致
select_cache_affinity_first(kvcm_fallback.py:461) 明确接受 role 含 PREFILL 或 PDFUSION 的 worker,但 _try_kvcm_fallback 恒返回 role=RoleType.PREFILL,候选快照也固定 enable_role_snapshot(RoleType.PREFILL)(master_client.py:325) 与 get_snapshot(RoleType.PREFILL)(:333)。PDFUSION 部署下 enable_role_snapshot 因 role_vip_map 无 PREFILL 直接返回 False(host_service.py:712-714),冷候选为空;被选中的 PDFUSION worker 仍被标为 PREFILL,backend_rpc_server_visitor.py:248 的 need_domain_routing 因 backend_role_list=[PDFUSION] ...
建议: 用探测到的 WorkerStatusPB.role 反查 RoleType 后再构造 RoleAddr,无法映射的候选直接跳过;候选快照 resolver 按 backend_role_list 覆盖 PDFUSION 而非固定 RoleType.PREFILL。并补一条 PDFUSION 部署的用例,断言不会产出该部署不存在的角色地址。
Checklist: [6.1] 分布式/跨平台变更有对应覆盖
| p2p_hit_discount: float, | ||
| ) -> int: | ||
| # Java Math.round for a non-negative value is floor(value + 0.5). | ||
| tokens = int(effective_cache_blocks(candidate, p2p_hit_discount) * block_size + 0.5) |
There was a problem hiding this comment.
[P2] block→token 折算所用 block_size 与引擎 seq_size_per_block 无一致性校验,且 help 文案与实际用途不符
生产键由 backend_rpc_server_visitor.py:138 的 get_block_cache_keys(token_ids, self.seq_size_per_block) 生成;核对 FlexLB RequestBlockHashService.java:49-52 与 prepareProvidedBlockCacheKeys:98-102 确认 KVCM 侧查询的正是这套上游键,且 FlexLB 强制要求「提供键时 block_size 必须 > 0」并用该值折算,故 Python 侧透传行为本身正确。但 _matched_tokens:442 与 query_and_select:842 用另一个独立配置 config.block_size(master_kvcm_block_size,默认 0、需手填)把命中块数与块数折算回 token,与实际发键所用的 seq_size_per_block 无任何比对;填错即 hit_cache_tokens、hit_rate_pct(:524) 系统性偏差、静...
建议: 让 master_kvcm_block_size 缺省时直接取引擎 seq_size_per_block(构造 BackendRPCServerVisitor 时已可得),降级为可选覆盖项;若必须显式配置,请在 _create_kvcm_fallback_client 中与 seq_size_per_block 比对,不一致时 fail-fast 并在错误信息中带上参数名与环境变量名。修正 help 文案为「块数↔token 折算步长;缺失时用于 vLLM sha256_cbor 回退哈希」。补一条以真实 get_block_cache_keys 输出为入参的用例(现有测试传任意整数 [1, 2],恰好掩盖该路径)。
| code = int(raw_code) | ||
| except (TypeError, ValueError): | ||
| code = int(ExceptionType.MASTER_NO_AVAILABLE_WORKER) | ||
| if code == FALLBACK_ERROR_CODE: |
There was a problem hiding this comment.
[P2] FlexLB body 级 8600 由硬失败改为静默 domain fallback,且不受新开关保护
已核对本 PR 的 diff:if code == FALLBACK_ERROR_CODE: return FlexlbResponse.fallback_response() 为新增行。改动前 HTTP 200 且 body code=8600 会走到 :704-715——ExceptionType(8600) 抛 ValueError(exceptions.py:78 确认 8600 非枚举成员,仅有 MASTER_NO_AVAILABLE_WORKER = 8400)→ 降级为 MASTER_NO_AVAILABLE_WORKER → raise FtRuntimeException,请求显式失败。改动后返回 fallback 响应,使 backend_rpc_server_visitor.py:251-253 的 allow_domain_fallback 成立并静默改走 domain 路由。该分支不在 client_fallback_enabled 门控之内,因此所有部署(含未开启新开关者)的错误语义都被改变,PR 描述未...
建议: 若这是有意的语义修正,请在 PR 描述与 commit message 中明确说明,并把它与 KVCM 兜底拆成独立提交,便于单独回滚;若只希望在新开关下生效,请把该分支置于 client_fallback_enabled 门控内。无论哪种,都应补一条断言「body code=8600 → domain fallback 且不抛异常」的非 manual 用例,并为该转换加上可观测的计数或限频日志。
Checklist: [6.1] Commit 原子、message 与行为匹配
| @@ -223,16 +223,25 @@ async def route_ips(self, input: GenerateInput): | |||
| input_token_batched = True | |||
There was a problem hiding this comment.
📍 实际位置 rtp_llm/server/backend_rpc_server_visitor.py:162(不在 diff 展示范围内,就近挂载)
[P2] 兜底路径缺少专属指标,三个新增响应字段无生产消费方,且无 master 地址时逐请求打 ERROR
全仓 py 检索确认 route_source/kvcm_outcome/cache_match 仅在 master_client.py 内被赋值传递,并只被 master_client_fallback_test.py:189,226 断言,无任何 kmonitor 上报。兜底成功后 route_result.is_ok 为真,:155-163 照旧上报无 tag 的 MASTER_ROUTE_QPS_METRIC,兜底的 KVCM 查询 + 探测耗时也被折叠进 :236-238 的 MASTER_ROUTE_RT_METRIC。另一侧:开关开且 master_addr 为空时不发任何 HTTP,master_client.py:655 直接以 connection_failed_response() 起始,兜底未命中仍带 connection_failed=True,导致 :165 每请求打一条 ERROR(改动前该场景仅 :240 一条 WARNING)。运维因此无法区分「FlexLB 正常」与「全靠兜底」。
建议: 在 get_master_route_addrs 成功分支读取 route_source/kvcm_outcome,为兜底单独上报带 route_source 标签的 QPS 与 RT,并增加 selection_reason 维度,使降级窗口可被告警与灰度观察、进而支撑开关回滚决策;把「无可用 master 地址」与「真实连接失败」在语义上区分(前者按 WARNING 或独立 outcome 限频记录),不要复用 connection_failed 的建连失败含义;_parse_candidates(kvcm_fallback.py:689-696) 丢弃格式不符或端口越界的候选时至少记一次聚合计数,避免 KVCM 返回域名时无声退化为纯冷路由。
Checklist: [6.1] 无 per-forward 调试日志 / 噪声热路径输出
|
|
||
| resp = await self._send_schedule_request( | ||
| master_addr, payload, ttft_timeout_ms, request_id | ||
| flexlb_timeout_ms = ttft_timeout_ms |
There was a problem hiding this comment.
[P2] flexlb_transport_timeout_ms 默认 0 使挂死场景兜底不可达,且开关关闭时也照样生效
master_client_fallback_flexlb_transport_timeout_ms 默认 0(py_config_modules.py:414、master_group_args.py:252),:649 因此不收敛,flexlb_timeout_ms = ttft_timeout_ms;请求未带 ttft_timeout_ms/timeout_ms 时 :615-620 退化为 master_default_timeout_ms(默认 3600000ms)。兜底只在 resp.connection_failed 时触发(:673),而该状态需 _send_schedule_request 走到 :573-591 的异常分支。因此 FlexLB 快速拒连时兜底有效,但 FlexLB 已建连不回包(最典型的挂死故障)时要等满整个请求预算才切换,兜底形同虚设。另外 :640-653 读取该参数时未判断 client_fallback_enabled,开关关闭时用户配置该项也会单方面压缩 FlexLB 超时而无任何补...
建议: 给该参数设置有界非零默认值(与 master_kvcm_request_timeout_ms=100 同量级的百毫秒级),或在开启 master_client_fallback 且该值为 0 时打印明确启动告警说明「兜底仅对连接级失败生效」;help 中补充 0 的含义;同时让该参数仅在 client_fallback_enabled 为真时生效,避免关闭开关时改变既有超时语义。并在 PR description 中给出推荐值与回滚方式。
| @@ -223,16 +223,25 @@ async def route_ips(self, input: GenerateInput): | |||
| input_token_batched = True | |||
There was a problem hiding this comment.
📍 实际位置 rtp_llm/server/backend_rpc_server_visitor.py:133(不在 diff 展示范围内,就近挂载)
[P3] token_ids 到 list 的 O(prompt_len) 转换在路由路径上重复执行两次
get_master_route_addrs 在 :133-137 已对 input.token_ids 执行一次 tolist() 用于 get_block_cache_keys,随后 _try_kvcm_fallback 又通过 _input_ids_for_kvcm(master_client.py:404-405)对同一张量再次 tolist(),并在 :415 逐元素 int() 转换。开关开启后每个走兜底的请求都要在路由路径上重复一次 O(prompt_len) 的张量转列表与整型转换。未做基准测量,故仅记为可零成本消除的冗余计算。
建议: 把 :133-137 已算好的 token_ids 列表随 block_cache_keys 一并传入 get_backend_role_addrs/_try_kvcm_fallback,删除 _input_ids_for_kvcm 中的重复转换(其 hasattr/getattr 猜测式访问也可一并去掉,GenerateInput 是强类型数据类)。
Checklist: [6.1] 可观测性:日志/指标/超时可操作、非噪声;[6.1] DRY:重复非平凡逻辑被抽取或显式复用
| *(client._probe_worker(candidate) for candidate in extra_candidates) | ||
| ) | ||
| self.assertTrue(all(snapshot is not None for snapshot in snapshots)) | ||
| self.assertLessEqual(self.worker_service.max_active, 2) |
There was a problem hiding this comment.
[P3] 并发上界为单边断言,无法区分正确限流与并发彻底失效
在 worker_status_concurrency=2(:239) 下并发发起 4 个 client._probe_worker(:276-278),仅断言 assertLessEqual(self.worker_service.max_active, 2)(:280)。该断言是单边的:若信号量实现退化为完全串行(max_active == 1),测试同样通过,无法区分「正确限流到 2」与「并发彻底失效」这两种截然不同的性能语义。_FakeWorkerStatusService 已维护精确的 max_active 计数,具备双边断言能力。
建议: 把断言改为双边,例如 assertEqual(2, self.worker_service.max_active)(配合足够的 delay_s 保证观测窗口),或至少加上 assertGreater(self.worker_service.max_active, 1),使限流退化为串行时能被检出。
| if not 1 <= bootstrap_port <= 65_535: | ||
| raise ValueError("master_kvcm_bootstrap_port must be a valid port") | ||
|
|
||
| self._kvcm_vip = VipServerWrapper( |
There was a problem hiding this comment.
[P3] master_kvcm_use_local 场景下 service_id 中填写的端口被静默丢弃
--master_kvcm_service_id 的 help 写「KVCM bootstrap service id or local IP:port list」,master_kvcm_use_local=True 时 VipServerWrapper.__init__(host_service.py:95-102)确实按 ip:port 逐项解析为 Host(ip, port)。但 resolve_bootstrap_targets(master_client.py:223-231)只读 host.ip,端口一律改用 bootstrap_port(默认 6381),用户在 service_id 中写的端口被静默忽略。本地联调若填 127.0.0.1:7000 而未同时设置 --master_kvcm_bootstrap_port,会连到一个未监听的端口,且无任何日志提示端口被替换。
建议: use_local 分支优先使用解析出的 host.port,仅当其缺失或非法时回退 bootstrap_port;或在 help 中明确「IP:port 列表中的端口会被 --master_kvcm_bootstrap_port 覆盖」,并在构造 target 时对被替换的端口打印一次启动日志。
Checklist: [6.1] 边界 case 覆盖(空、单元素、最大值)
| self.master_client.client_fallback_enabled, | ||
| input_token_batched, | ||
| ) | ||
| specified_roles = {addr.role for addr in input.generate_config.role_addrs} |
There was a problem hiding this comment.
📍 实际位置 rtp_llm/server/backend_rpc_server_visitor.py:324(不在 diff 展示范围内,就近挂载)
[P3] 新开关被 host_service.service_available 前置门控静默旁路
enqueue(:324) 与 batch_enqueue(:335) 均以 if self.host_service.service_available: 门控 route_ips。该字段在 host_service.py:660-662 定义为 bool(self.master_vip.domain) or any(self.role_vip_map.values())。因此当运维未配置任何 master 域名与角色域名、期望纯靠 KVCM 客户端兜底选点(KVCM 热候选并不依赖 role VIP)时,route_ips 根本不会被调用,master_client_fallback=True 被静默忽略且无任何日志提示。该配置组合较为边缘,故仅记为语义说明缺口。
建议: 在 master_client_fallback 的 help 中说明「需至少配置一个 VIP 域名,否则路由入口不会被触发」;或在启动时若开关打开而 service_available 为 False 则打印一次明确 warning。
Checklist: [6.1] 回滚路径:风险行为存在运维回滚手段;[6.1] 错误语义:fail-fast/retry/fallback/silent 行为显式
| @@ -0,0 +1,235 @@ | |||
| import asyncio | |||
There was a problem hiding this comment.
[P3] 测试文件存在未使用的 import
第 1 行 import asyncio,但全文未出现任何 asyncio. 引用(异步用例由 unittest.IsolatedAsyncioTestCase 驱动)。仓库 flake8 默认启用 F401,未使用 import 会在 pre-commit / lint 阶段报错,属可零成本消除的噪声。
建议: 删除 import asyncio。
Checklist: [6.1] 逻辑变更未混入无关格式化
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1335
Status: BLOCKING
Summary: P0/2 · P1/3 · P2/13 · P3/5
Reviewed: commit 7f627e40ff44 · 2026-08-28 16:03 UTC+8
Blocking Issues
P0
- 无 package 的 worker_status proto 与引擎 model_rpc_service proto 根符号完全重名,开关一开即 duplicate symbol 崩溃 @
rtp_llm/server/worker_status_proto/worker_status_service.proto:3- 建议:优先复用已有
//rtp_llm/cpp/model_rpc/proto:model_rpc_service_py_proto:它已提供字段号完全一致的StatusVersionPB/WorkerStatusPB与方法路径同为/RpcService/GetWorkerStatus的RpcServiceStub,把kvcm_fallback.py改为从rtp_llm.cpp.model_rpc.proto.model_rpc_service_pb2(_grpc)导入并删除worker_status_proto/整个目录(同目录vit_proxy_server.py:16已是此模式),一并消除 DRY 与后续 proto 漂移。若确需保留独立轻量副本,必须为其声明专属 package 隔离符号(stub 中保留'/RpcService/GetWorkerStatus'字面路径即可维持 wire 兼容),或改用私有descriptor_pool.DescriptorPool();并补一个在真实rtp_llm包下同进程先后 import 两个 pb2 的回归用例锁死该不变量。
- 建议:优先复用已有
- server py_library 的 srcs glob 漏掉 worker_status_proto,wheel 与 runfiles 均缺该模块 @
rtp_llm/server/BUILD:5- 建议:在 glob 中补
"worker_status_proto/*.py",与kvcm_proto/*.py对称。更稳妥的是按上一条建议复用model_rpc_service_py_proto并删除该目录,或按仓库既有约定(rtp_llm/cpp/model_rpc/proto/BUILD:21-42的generate_grpc_proto+py_library)为 proto 目录建独立 target 并显式deps,由 bazel 托管 srcs,从结构上消除后续新增 proto 子包时再次漏配的可能。修复后请以非 manual 方式实跑//rtp_llm/server/test:kvcm_fallback_test验证 runfiles 完整,并补一个通过真实包路径 importrtp_llm.server.kvcm_fallback(不改写sys.modules)的轻量用例,使此类打包缺失能被 CI 直接拦住。
- 建议:在 glob 中补
P1
- 三个新增兜底测试全部标记 manual,且伪造掉两个 P0 所在的生产边界,新特性 CI 零覆盖 @
rtp_llm/server/test/BUILD:35- 建议:去掉这三个目标的
manual标记纳入 CI 通配;若kvcm_fallback_test确因远端沙箱限制 loopback 端口而必须 manual,请在 BUILD 中注明原因,并至少保证完全不涉网络的另两个目标在 CI 执行。补齐关键边界用例:fake 返回connection_failed=True/fallback=True时断言domain_calls == 1且最终取到 domain 地址、未抛 ROUTE_ERROR;input_token_batched=True时断言跳过 master 路由;get_master_addr()非空时断言行为与改动前一致。并额外补一个非 manual 的轻量导入型测试,通过真实rtp_llm.server包路径 importkvcm_fallback,使打包与符号注册类缺陷有测试兜底。提交前请实跑全部新增 target,确保「已加测试」与「测试可执行」一致。
- 建议:去掉这三个目标的
- KVCM leader 解析失败无负缓存且被单锁串行,故障期逐请求放大延迟 @
rtp_llm/server/kvcm_fallback.py:640- 建议:补充失败负缓存与熔断:解析失败时也记录
_leader_failed_at,在一个短冷却窗口(如request_timeout_ms的数倍)内让后续请求直接快速失败而不重复扫描,冷却后半开重试;bootstrap 目标改为并发asyncio.gather或限定单次扫描的目标数量,避免串行累加;并为 leader 解析失败/超时与「已熔断」状态上报 kmonitor 计数指标,使故障期行为可观测、可告警。
- 建议:补充失败负缓存与熔断:解析失败时也记录
- 兜底链路无整体时间预算,WorkerStatus 信号量获取也无 deadline @
rtp_llm/server/kvcm_fallback.py:790- 建议:用
asyncio.wait_for给query_and_select(或_try_kvcm_fallback)加整体预算,预算由 TTFT 剩余时间或专用配置派生,超时即返回 None 交由 domain fallback;信号量按并发规模放大或改为按请求限流并给获取加超时;同时为兜底整体延迟与「已超预算」次数增加 kmonitor 指标,使 fail-fast 与 fallback 的边界显式可控可告警。
- 建议:用
Non-blocking Suggestions
P2
- 兜底返回的 RoleAddr 硬编码为 PREFILL,与打分层的 PDFUSION 支持不一致 @
rtp_llm/server/master_client.py:514- 建议:按 worker 上报角色(
worker_load_snapshot已保留role)或部署形态的backend_role_list返回正确的RoleType,PDFUSION 部署应返回PDFUSION,使兜底结果能满足need_domain_routing而不被 domain 覆盖;对「兜底已给出正确角色、仅需补齐其余 role」的情形把日志降为 debug 或改用区分文案,避免把正常补齐误报为 domain 降级并污染 DOMAIN_ROUTE_QPS 基线。若当前只打算支持纯 PREFILL 部署,则应在select_cache_affinity_first中同步收窄 role 过滤并在配置校验处显式拒绝其他形态,让两层契约一致,并补一个 PDFUSION 形态下断言返回角色的用例。
- 建议:按 worker 上报角色(
- 兜底链路存在无调用方的死代码与未装配的隐藏参数,p2p_hit_discount 成为无效旋钮 @
rtp_llm/server/kvcm_fallback.py:397- 建议:删除
build_candidate_pool与select_max_local_affinity及其__all__条目(若仅测试需要,改为直接调用build_candidate_plan)。对 p2p 二选一:要么补上p2p_host_count的配置项与装配使p2p_hit_discount真正生效,要么先移除master_client_fallback_p2p_hit_discount这个无效 flag(YAGNI),待 p2p 能力接通时再一并引入,避免运维误认为可调。同时把leader_refresh_interval_ms与minimum_local_blocks暴露为master_*配置,或在注释中说明为何刻意固定,避免出现只能改代码才能调整的隐藏行为。
- 建议:删除
- flexlb_transport_timeout_ms 不受 kill switch 约束,且默认 0 使挂死场景兜底不可达 @
rtp_llm/server/master_client.py:652- 建议:二选一:(a) 把该
min()收敛进if self.client_fallback_enabled分支,使master_client_fallback成为真正的单一 kill switch,并给一个开关开启时生效的合理非零默认(或在开关开启且该值为 0 时启动期告警),确保挂死场景能在有界时间内触发兜底;(b) 若确实希望它独立于开关生效,则改名为master_flexlb_transport_timeout_ms(含 env_name)并同步修正 help 与py_config_modules.py附近注释,避免运维误判回滚范围。
- 建议:二选一:(a) 把该
- 兜底路径缺少专属指标,三个新增响应字段无生产消费方 @
rtp_llm/server/backend_rpc_server_visitor.py:162- 建议:在
:162/:236读取route_result.route_source作为 kmonitor tag 上报(或为CLIENT_FALLBACK新增独立 QPS/RT 指标),避免污染既有 FlexLB RT 基线;为kvcm_outcome、selection_reason、cache_query_outcome(含no_positive_match)与 block hash 契约不匹配各增计数指标,使兜底触发率、选点原因与配置错填可被告警发现;兜底路径的失败日志按去重或采样输出,避免热路径逐请求 ERROR。
- 建议:在
- gRPC channel 与选择历史字典只增不减,滚动发布后累积僵尸连接 @
rtp_llm/server/kvcm_fallback.py:587- 建议:给四个连接字典引入有界 LRU(容量按活跃 worker 数量级或
candidate_pool_size的若干倍设定),淘汰时await channel.close();_invalidate_leader同步关闭并移除对应 channel/stub。_last_selected_ns与_logged_worker_hash_contract_mismatches改为有界结构,或在候选快照刷新时按当前存活 target 集合收敛清理。
- 建议:给四个连接字典引入有界 LRU(容量按活跃 worker 数量级或
- 校验顺序导致后台线程泄漏,且 MasterClient/HostService 的 close 在生产中从未被调用 @
rtp_llm/server/master_client.py:229- 建议:把
KvcmFallbackConfig的构造与校验全部提前到VipServerWrapper/VipHostSnapshot创建之前,使非法配置在任何线程启动前 fail-fast;或用 try/except 包裹,在失败路径上显式close()已创建的快照。同时在服务关停链路(frontend/backend 的 shutdown 钩子)中显式调用MasterClient.close()与HostService.close(),让close()不再是只有测试会走的死路径。
- 建议:把
- 手工签入 pb2/pb2_grpc 绕开构建期 codegen,无漂移校验且格式风格分叉 @
rtp_llm/server/worker_status_proto/worker_status_service_pb2_grpc.py:1- 建议:为两个 proto 目录各加
generate_grpc_proto目标,让//rtp_llm/server:server依赖生成出的 py_library 并从源码树移除签入产物(天然同时修掉打包漏项与格式分叉)。若因跨仓分发必须签入,请补一个 CI 校验目标:以仓库锁定的 grpcio-tools 重新生成并 diff 为空,另加一个解析权威 proto、断言相关 message 的字段名/字段号/reserved/optional逐项相等的用例;并把这两个目录加入.pre-commit-config.yaml的 exclude,统一保持 protoc 原样输出以便逐字比对。
- 建议:为两个 proto 目录各加
- 对 MasterConfig 全量使用 getattr 字面量访问,默认值在多处重复维护 @
rtp_llm/server/master_client.py:161- 建议:改为直接属性访问
self.master_config.master_kvcm_block_size,让缺失/改名在启动期即报 AttributeError,默认值只保留在配置声明一处;删除host_service上的getattr+callable探测与KvcmFallbackResult的getattr包装;并把配置装配抽到kvcm_fallback.py的KvcmFallbackConfig.from_master_config(master_config)工厂方法,让MasterClient只负责生命周期。
- 建议:改为直接属性访问
- master_kvcm_block_size 与引擎 seq_size_per_block 双写且无一致性校验,必填项默认值永远非法 @
rtp_llm/server/server_args/master_group_args.py:80- 建议:由
BackendRPCServerVisitor把已有的seq_size_per_block传给MasterClient并作为 block_size 的默认来源,仅在确需覆盖时才允许配置,并在启动期与引擎值比对、不一致即 fail-fast;同时为所有「默认值永远非法」的必填项在 help 中标注 required,并在开关开启时一次性汇总所有缺失/非法项后一次报错,而非逐项 raise 让运维反复启崩。help 文案同步修正为该值同时用于 KVCM block↔token 折算与 WorkerStatus 契约校验。
- 建议:由
- FlexLB body 级 8600 由硬失败改为静默 domain fallback,且不受新开关保护 @
rtp_llm/server/master_client.py:715- 建议:在 PR description 与 commit message 中显式记录这次语义变更(当前提交聚焦 KVCM 兜底,未体现对既有 FlexLB 错误处理的影响);为该分支补单测断言返回
fallback=True且不抛异常,并为「body 级 8600 触发 domain 兜底」上报独立计数指标,使这条静默降级在线上可观测。若不希望该变更随本 PR 一起上线,应将其收敛进client_fallback_enabled分支或拆为独立提交。
- 建议:在 PR description 与 commit message 中显式记录这次语义变更(当前提交聚焦 KVCM 兜底,未体现对既有 FlexLB 错误处理的影响);为该分支补单测断言返回
- 测试在模块导入期永久污染 sys.modules 且不恢复,影子生产类型已与生产漂移 @
rtp_llm/server/test/backend_rpc_server_visitor_fallback_test.py:146- 建议:统一为
master_group_args_test.py:27-44的「先检查是否已存在 +try/finally恢复」模式,并把桩注入收敛到setUpModule/fixture 而非模块顶层执行。把与生产共享的纯 Python 类型/常量(FlexlbResponse、RoleAddr、RoleType、ExceptionType、GaugeMetrics)改为从真实模块导入,只对torch、rtp_llm.ops等重依赖打桩;无法导入时至少断言 stub 字段集合与真实类型一致。桩加载器抽到共享py_library(如fallback_test_stubs.py)消除三份不一致副本,并顺手删除本文件:1未使用的import asyncio。
- 建议:统一为
- master_group_args_test 绑定空 SimpleNamespace 并 stub str2bool,断言退化为字面量自证 @
rtp_llm/server/server_args/test/master_group_args_test.py:29- 建议:改用真实
MasterConfig实例并对每个 flag 断言default == getattr(master_config, field),使字段缺失与默认值多处双写漂移都能被捕获;改用真实str2bool(把它移出依赖rtp_llm.ops的模块,或只对rtp_llm.ops打桩而不替换整个 util 模块),对两个布尔开关断言type is str2bool、default is False,并用subTest/parametrize对"false"/"0"/"off"/"true"/"1"/"on"逐一断言解析结果;删除:130恒真的否定断言(若想固化命名约定,改为遍历所有--master_*flag 断言含fallback者前缀均为master_client_fallback_);并在server_args_test.py补MASTER_CLIENT_FALLBACK等关键项的 env→config 端到端用例。
- 建议:改用真实
- 新增的 VipHostSnapshot 后台刷新状态机没有任何测试覆盖 @
rtp_llm/server/host_service.py:125- 建议:为
VipHostSnapshot补一组不依赖真实 VIP 的纯同步单测:注入 fakevip直接驱动refresh_now()/get_hosts()而不依赖线程时序,分别断言首次发布成功、刷新抛异常时保留旧快照、连续空结果在超过stale_timeout_seconds后返回空、之后恢复非空、重复 host 去重、vip.domain为空时线程未启动、close()后线程退出。同时为HostService.enable_role_snapshot/get_backend_role_addr_snapshot/close补重复启用幂等性与线程回收断言,并确保这些用例进入 CI(非 manual)。
- 建议:为
P3
- 路由准入判定跨类重复,visitor 直接读取 MasterClient 内部开关且日志归因错误 @
rtp_llm/server/backend_rpc_server_visitor.py:226- 建议:把准入判断下沉为 master client 的一个方法(如
master_client.can_route()),由其内部一次性读取 master_addr 并结合client_fallback_enabled与_kvcm_fallback_client就绪状态给出结论,visitor 只消费该布尔结果与返回的诊断信息;同时按实际命中的条件分别打日志:batched 输入单独一条明确说明跳过 master 路由,can_attempt_master_route为假时再打一条含 master_addr 与开关状态的日志,避免单条日志混合归因。
- 建议:把准入判断下沉为 master client 的一个方法(如
- token_ids 到 list 的 O(prompt_len) 转换在路由路径上重复执行两次 @
rtp_llm/server/backend_rpc_server_visitor.py:133- 建议:把
get_master_route_addrs中已转换好的 token 列表作为参数传入get_backend_role_addrs/_try_kvcm_fallback,复用同一份 list 而不在下游重新转换;_input_ids_for_kvcm的getattr/hasattr探测改为依赖GenerateInput的显式字段与类型契约(或在函数签名上直接要求List[int])。
- 建议:把
- 新开关被 host_service.service_available 前置门控静默旁路 @
rtp_llm/server/backend_rpc_server_visitor.py:324- 建议:在
BackendRPCServerVisitor.__init__中检测master_client.client_fallback_enabled and not self.host_service.service_available,命中时打一条 warning 明确说明该开关因 host service 不可用而不会生效;或把client_fallback_enabled也纳入service_available的判定,使开关语义与实际生效范围一致。
- 建议:在
- 并发上界为单边断言,无法区分正确限流与并发彻底失效 @
rtp_llm/server/test/kvcm_fallback_test.py:280- 建议:补一条下界断言(候选数大于 1 且服务端已引入延迟时断言
max_active >= 2),使「正确限流」与「并发彻底失效」可区分;或直接断言max_active == 期望并发数,让配置回归能被捕获。
- 建议:补一条下界断言(候选数大于 1 且服务端已引入延迟时断言
- master_kvcm_use_local 场景下 service_id 中填写的端口被静默丢弃 @
rtp_llm/server/master_client.py:225- 建议:在
use_local或 service_id 含端口时保留原端口,或在检测到 host 自带端口与bootstrap_port不一致时打一条 warning 明确说明以哪个为准;若确实只支持统一端口,请同步修正 help 文案去掉 "IP:port list" 的表述。
- 建议:在
Checklist Findings (24 fail / 48 total)
General Principles Checklist
- [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue
无 package 的 worker_status proto 与引擎 model_rpc_service proto 根符号完全重名,开关一开即 duplicate symbol 崩溃
该文件注释「Keep this file package-less」,故StatusVersionPB(:6)、TaskInfoPB(:11)、KvCacheGroupModePB(:42)、WorkerStatusPB(:48)、service RpcService(:73) 全注册为顶层符号。已核实model_rpc_service.proto同样无package声明,且在 342/390/439/445/654 行定义完全同名的这 5 个符号。worker_status_service_pb2.py:7,17走_descriptor_pool.Default().AddSerializedFile(...),落进程级全局 pool;model_rpc_service_pb2由generate_grpc_proto生成、同样落默认 pool。两者在同一前端进程加载:backend_rpc_server_visitor.py:10导入ModelRpcClient(`model_rpc_client.py:10 - [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
FlexLB body 级 8600 由硬失败改为静默 domain fallback,且不受新开关保护
get_backend_role_addrs的:715-716if code == FALLBACK_ERROR_CODE: return FlexlbResponse.fallback_response()使 HTTP 200 + bodycode=8600的响应与非 200 分支(:579)语义对齐。语义对齐本身合理,但这是一次不受master_client_fallback约束的既有行为变更:已核实 8600 不在ExceptionType枚举中(exceptions.py仅到 MASTER_NO_AVAILABLE_WORKER=8400、ROUTE_ERROR=8500),故改动前该分支会经except ValueError退化为FtRuntimeException(MASTER_NO_AVAILABLE_WORKER)向用户报错;改动后变为fallback=True→route_ips:251放行 domain 兜底,即从可见硬失败转为静默降级。存量部署无需开启任何开关即受影响,且该分支无测试 - [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue
路由准入判定跨类重复,visitor 直接读取 MasterClient 内部开关且日志归因错误
:226的can_attempt_master_route = bool(master_addr) or bool(self.master_client.client_fallback_enabled)把「客户端能否路由」这一策略判断复制到调用方,而master_client.py:439的真实前置条件还包含self._kvcm_fallback_client is not None;一旦前置条件演进,visitor 的门禁会静默与之偏离。同时 visitor 在:218读一次host_service.get_master_addr()用于门禁与日志,master_client.py:620内部又独立读一次同一 snapshot,两次读取之间后台刷新线程可能更新,导致日志中的 master_addr 与实际发请求的地址不一致。另外:239-246的elif分支在开关开启时只可能因input_token_batched=True进入(此时can_attempt_master_route恒为真),但文案仍把三 - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
新开关被 host_service.service_available 前置门控静默旁路
enqueue(:324) 与batch_enqueue(:335) 仅在self.host_service.service_available为真时才调用route_ips,而host_service.py:660中service_available = bool(self.master_vip.domain) or any(self.role_vip_map.values())。若运维只打开master_client_fallback而未配置 master domain 与任何 role domain,新增的can_attempt_master_route分支永远不会被执行,开关静默无效且日志中没有任何提示,排查成本高。 - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
新开关被 host_service.service_available 前置门控静默旁路
enqueue(:324) 与batch_enqueue(:335) 仅在self.host_service.service_available为真时才调用route_ips,而host_service.py:660中service_available = bool(self.master_vip.domain) or any(self.role_vip_map.values())。若运维只打开master_client_fallback而未配置 master domain 与任何 role domain,新增的can_attempt_master_route分支永远不会被执行,开关静默无效且日志中没有任何提示,排查成本高。 - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
新增的 VipHostSnapshot 后台刷新状态机没有任何测试覆盖
VipHostSnapshot(:125-235) 承担兜底路径全部冷候选来源,其状态机有多个非平凡不变量:_publish(:180-183) 对空结果直接 return 以保留 last-known-good、refresh_now(:188-214) 在异常与空结果时按stale_timeout_seconds决定是否清空、get_hosts(:221-230) 读侧再做一次过期判断、__init__复用VipServerWrapper构造期发现结果、:162有「domain 为空则不启动线程」的分支、close(:232) 以 1s join 回收线程。这些正是典型的边界易错逻辑(首次成功、刷新异常保留旧值、超过 stale 阈值清空、清空后恢复、线程回收),但三个新增测试与既有host_service_test中均无对应用例;同批新增的enable_role_snapshot/get_backend_role_addr_snapshot/close(:703-745) 亦无覆盖。 - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
master_kvcm_use_local 场景下 service_id 中填写的端口被静默丢弃
:225-228以VipServerWrapper(service_id, use_local)构造发现源,随后resolve_bootstrap_targets(:235-243) 一律用配置的bootstrap_port重组ip:port,忽略 host 自带端口。而--master_kvcm_service_id的 help 明确写作 "KVCM bootstrap service id or local IP:port list"(master_group_args.py:58),即文档上支持直连IP:port形态;本地/直连调试时若按该说明填写host:port,端口会被静默替换为master_kvcm_bootstrap_port(默认 6381),既不报错也无日志提示,容易误判为 KVCM 不可达。 - [6.1] Quality — Commit 原子、message 与行为匹配 → issue
FlexLB body 级 8600 由硬失败改为静默 domain fallback,且不受新开关保护
get_backend_role_addrs的:715-716if code == FALLBACK_ERROR_CODE: return FlexlbResponse.fallback_response()使 HTTP 200 + bodycode=8600的响应与非 200 分支(:579)语义对齐。语义对齐本身合理,但这是一次不受master_client_fallback约束的既有行为变更:已核实 8600 不在ExceptionType枚举中(exceptions.py仅到 MASTER_NO_AVAILABLE_WORKER=8400、ROUTE_ERROR=8500),故改动前该分支会经except ValueError退化为FtRuntimeException(MASTER_NO_AVAILABLE_WORKER)向用户报错;改动后变为fallback=True→route_ips:251放行 domain 兜底,即从可见硬失败转为静默降级。存量部署无需开启任何开关即受影响,且该分支无测试 - [6.1] Quality — 无 per-forward 调试日志 / 噪声热路径输出 → issue
兜底路径缺少专属指标,三个新增响应字段无生产消费方
FlexlbResponse新增route_source(:47)、cache_match、kvcm_outcome(:49),兜底成功时置为"CLIENT_FALLBACK"与 18 项 cache_match 明细(master_client.py:472-522)。但全仓检索route_source/kvcm_outcome仅命中master_client.py自身与master_client_fallback_test.py:189,226,生产路由层完全不读取。get_master_route_addrs在:162对兜底成功与真实 FlexLB 成功一律上报同一个无标签MASTER_ROUTE_QPS_METRIC,route_ips:236的MASTER_ROUTE_RT_METRIC也无维度且混入 KVCM 发现与 worker 探测耗时,既无法度量降级触发率与恢复,也会让基于该 RT 的既有看板/告警基线漂移。另有静默失效路径:kvcm_fallback.py:774-787 - [6.1] Quality — 逻辑变更未混入无关格式化 → issue
手工签入 pb2/pb2_grpc 绕开构建期 codegen,无漂移校验且格式风格分叉
仓库既有约定是构建期生成:rtp_llm/cpp/model_rpc/proto/BUILD:2,21-25,38-42用generate_grpc_proto+py_library由.proto生成 stub,源码树中不存在model_rpc_service_pb2.py(.gitignore显式忽略),已有多个 Python 消费者。本 PR 反其道而行,把两套_pb2.py/_pb2_grpc.py直接签入并靠 BUILD glob 装配(.proto本身因 glob 只匹配*.py而完全未进入依赖跟踪),文件头硬编码Protobuf Python Version: 4.25.1而仓库锁定grpcio-tools==1.57.0,生成工具链在构建体系之外。两份 proto 又都是仓内/上游权威副本的人工镜像,仅靠「Keep this file in sync with ...」注释维持,三处均无自动化校验。此外kvcm_meta_service_pb2_grpc.py已被 Black 重排而 - [6.1] Software Engineering — DIP:高层策略不依赖非必要具体细节 → issue
路由准入判定跨类重复,visitor 直接读取 MasterClient 内部开关且日志归因错误
:226的can_attempt_master_route = bool(master_addr) or bool(self.master_client.client_fallback_enabled)把「客户端能否路由」这一策略判断复制到调用方,而master_client.py:439的真实前置条件还包含self._kvcm_fallback_client is not None;一旦前置条件演进,visitor 的门禁会静默与之偏离。同时 visitor 在:218读一次host_service.get_master_addr()用于门禁与日志,master_client.py:620内部又独立读一次同一 snapshot,两次读取之间后台刷新线程可能更新,导致日志中的 master_addr 与实际发请求的地址不一致。另外:239-246的elif分支在开关开启时只可能因input_token_batched=True进入(此时can_attempt_master_route恒为真),但文案仍把三 - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
token_ids 到 list 的 O(prompt_len) 转换在路由路径上重复执行两次
get_master_route_addrs:133-138已把input.token_ids整体tolist()一次用于get_block_cache_keys。开关开启且走兜底时,master_client._input_ids_for_kvcm(:411-430) 又对同一token_ids做第二次tolist()(:417-418) 并额外执行[int(token_id) for token_id in raw_input_ids](:428) 全量遍历,即在每请求路由路径上多一次 O(prompt_len) 的 Python 层拷贝与逐元素转换;长 prompt 场景下是可避免的重复开销(未做基准测量,故按 P3 记录)。该函数还用getattr(input, "input_ids", None)/hasattr(raw_input_ids, "tolist")做类型探测,与GenerateInput的既有字段契约脱耦。 - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
master_kvcm_block_size 与引擎 seq_size_per_block 双写且无一致性校验,必填项默认值永远非法
--master_kvcm_block_size默认 0(:84),而KvcmFallbackConfig.__post_init__:74要求其为正数,故仅开启--master_client_fallback而不设该值必然启动崩溃;--master_kvcm_service_id(:58)、--master_kvcm_instance_id(:76) 同为空默认的必填项,三者 help 均未标注必填(对照:99的 lookahead 项已正确标注 "required when fallback is enabled"),运维只能逐轮启崩试错。更根本的是该值与引擎侧同一物理量重复:backend_rpc_server_visitor.py:138用引擎的self.seq_size_per_block计算送给 KVCM 的block_cache_keys,而kvcm_fallback.py:445/:871用运维手填的config.block_size做 block↔token 折算,两者无任何交叉校验;唯一保 - [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue
兜底返回的 RoleAddr 硬编码为 PREFILL,与打分层的 PDFUSION 支持不一致
select_cache_affinity_first:461-464明确接纳"PREFILL" in role or "PDFUSION" in role的 worker,但_try_kvcm_fallback在:511-519无条件构造RoleAddr(role=RoleType.PREFILL, ...),丢弃了 worker 实际上报的角色(kvcm_fallback.py:430已从WorkerStatusPB.role取到),即 PDFUSION worker 会被贴上 PREFILL 标签。而get_backend_role_list:118-120在 FRONTEND 配置pdfusion_domain时产出[PDFUSION]。此时get_master_route_addrs:163成功返回 None,route_ips:251的allow_domain_fallback恒为 True,need_domain_routing(:248) 因{PREFILL}不覆盖 - [6.1] Software Engineering — SRP:模块/类职责单一 → issue
对 MasterConfig 全量使用 getattr 字面量访问,默认值在多处重复维护
_create_kvcm_fallback_client中约 20 处形如int(getattr(self.master_config, "master_client_fallback_discovery_refresh_ms", 1000))的字面量属性访问。这些字段已在master_group_args.py(add_argument(default=...)+bind_to)与py_config_modules.py:389-416两处声明,正常初始化的 config 上必然存在,于是同一默认值被写在三处并需手工保持一致(当前比对一致,但无任何自动化保护);一旦配置项改名或拼错,代码会静默退回硬编码默认而非 fail-fast,字符串字面量也让全仓搜索失效。同类问题::326-336用getattr(self.host_service, "enable_role_snapshot", None)+callable(...)做 hasattr 式控制流(而host_service.py:703本 PR 已定义该方 - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
三个新增兜底测试全部标记 manual,且伪造掉两个 P0 所在的生产边界,新特性 CI 零覆盖
kvcm_fallback_test(:35)、master_client_fallback_test(:45)、backend_rpc_server_visitor_fallback_test(:54) 均带tags=["manual"],按 bazel 语义不会被//...展开,本 PR 也未改任何 CI 配置去点名,故约 2000 行路由改动在 CI 中唯一会执行的是只断言 flag 名的master_group_args_test。三者均不依赖//rtp_llm:testlib、无 GPU 需求,而同文件vit_proxy_server_test(:57) 反而依赖 testlib 且未标 manual。三份测试又都用sys.modules合成rtp_llm.*命名空间(kvcm_fallback_test.py:12-24把__path__指向源码目录),恰好绕过两个 P0 所在的真实打包与 descriptor pool 边界。覆盖面亦不足:`backend_rpc_server_visitor_fa - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
新增的 VipHostSnapshot 后台刷新状态机没有任何测试覆盖
VipHostSnapshot(:125-235) 承担兜底路径全部冷候选来源,其状态机有多个非平凡不变量:_publish(:180-183) 对空结果直接 return 以保留 last-known-good、refresh_now(:188-214) 在异常与空结果时按stale_timeout_seconds决定是否清空、get_hosts(:221-230) 读侧再做一次过期判断、__init__复用VipServerWrapper构造期发现结果、:162有「domain 为空则不启动线程」的分支、close(:232) 以 1s join 回收线程。这些正是典型的边界易错逻辑(首次成功、刷新异常保留旧值、超过 stale 阈值清空、清空后恢复、线程回收),但三个新增测试与既有host_service_test中均无对应用例;同批新增的enable_role_snapshot/get_backend_role_addr_snapshot/close(:703-745) 亦无覆盖。 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
并发上界为单边断言,无法区分正确限流与并发彻底失效
test_worker_status_is_fresh_concurrency_bounded_and_channels_reused(:279) 通过 fake WorkerStatus 服务端统计并发峰值,最终只在:336断言assertLessEqual(self.worker_service.max_active, 2),即只有上界没有下界。若信号量实现退化为完全串行(max_active == 1),断言仍然通过,而这正是本 PR 探测阶段最关心的性能属性;同样地,kvcm_fallback.py:750的信号量若被误设为 1,该测试不会失败。用例已在:286设置worker_service.delay_s = 0.01刻意引入延迟,具备断言下界的条件。
RTP-LLM Checklist
- [I] 代码质量 — 同一功能用统一工具函数 → issue
手工签入 pb2/pb2_grpc 绕开构建期 codegen,无漂移校验且格式风格分叉
仓库既有约定是构建期生成:rtp_llm/cpp/model_rpc/proto/BUILD:2,21-25,38-42用generate_grpc_proto+py_library由.proto生成 stub,源码树中不存在model_rpc_service_pb2.py(.gitignore显式忽略),已有多个 Python 消费者。本 PR 反其道而行,把两套_pb2.py/_pb2_grpc.py直接签入并靠 BUILD glob 装配(.proto本身因 glob 只匹配*.py而完全未进入依赖跟踪),文件头硬编码Protobuf Python Version: 4.25.1而仓库锁定grpcio-tools==1.57.0,生成工具链在构建体系之外。两份 proto 又都是仓内/上游权威副本的人工镜像,仅靠「Keep this file in sync with ...」注释维持,三处均无自动化校验。此外kvcm_meta_service_pb2_grpc.py已被 Black 重排而
Python Static-First Checklist
- [P.A] 静态结构与类型纪律 — 禁止 getattr/setattr literal 访问 → issue
token_ids 到 list 的 O(prompt_len) 转换在路由路径上重复执行两次
get_master_route_addrs:133-138已把input.token_ids整体tolist()一次用于get_block_cache_keys。开关开启且走兜底时,master_client._input_ids_for_kvcm(:411-430) 又对同一token_ids做第二次tolist()(:417-418) 并额外执行[int(token_id) for token_id in raw_input_ids](:428) 全量遍历,即在每请求路由路径上多一次 O(prompt_len) 的 Python 层拷贝与逐元素转换;长 prompt 场景下是可避免的重复开销(未做基准测量,故按 P3 记录)。该函数还用getattr(input, "input_ids", None)/hasattr(raw_input_ids, "tolist")做类型探测,与GenerateInput的既有字段契约脱耦。 - [P.A] 静态结构与类型纪律 — 禁止 hasattr 做控制流分支 → issue
token_ids 到 list 的 O(prompt_len) 转换在路由路径上重复执行两次
get_master_route_addrs:133-138已把input.token_ids整体tolist()一次用于get_block_cache_keys。开关开启且走兜底时,master_client._input_ids_for_kvcm(:411-430) 又对同一token_ids做第二次tolist()(:417-418) 并额外执行[int(token_id) for token_id in raw_input_ids](:428) 全量遍历,即在每请求路由路径上多一次 O(prompt_len) 的 Python 层拷贝与逐元素转换;长 prompt 场景下是可避免的重复开销(未做基准测量,故按 P3 记录)。该函数还用getattr(input, "input_ids", None)/hasattr(raw_input_ids, "tolist")做类型探测,与GenerateInput的既有字段契约脱耦。 - [P.F] 语言陷阱 — 禁止模块级 import 副作用 → issue
测试在模块导入期永久污染 sys.modules 且不恢复,影子生产类型已与生产漂移
:146在模块顶层直接执行_load_visitor_module(),把rtp_llm、rtp_llm.server、rtp_llm.config、rtp_llm.cpp、rtp_llm.metrics、rtp_llm.utils与torch无条件写入sys.modules(:15/:32/:105/:141) 且无finally恢复;master_client_fallback_test.py:12-20,76同样如此。相比之下kvcm_fallback_test.py:14用了存在性守卫、master_group_args_test.py:27-44已实现完整的保存 +try/finally恢复,三份实现行为不一致。更关键的是影子类型已与生产漂移::56-65自定义一份FlexlbResponse并在:122-126整体替换真实模块、:191用__new__绕过__init__手填属性、`master_client_fallback_test.py - [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
master_group_args_test 绑定空 SimpleNamespace 并 stub str2bool,断言退化为字面量自证
:29用fake_util_module.str2bool = bool顶替生产实现以绕开 util 模块对rtp_llm.ops的依赖,:50又以空SimpleNamespace()作 master_config。于是:123的assertIs(arguments[flag]["bind_to"][0], master_config)恒真、只验证对象同一性,无法发现真实MasterConfig缺少该字段;:124比对的是测试自己硬编码的字符串;各新 flag 的type与default全无断言。这恰好绕过最关键的边界:master_client_fallback是总开关,bool("false")与bool("0")均为 True,只有真实str2bool才会解析为 False,若type=str2bool被误改为type=bool,MASTER_CLIENT_FALLBACK=false会在本应关闭的部署上静默打开兜底而该测试仍通过。此外:130 - [P.G] 测试规范 — 数据驱动测试用 pytest.mark.parametrize → issue
master_group_args_test 绑定空 SimpleNamespace 并 stub str2bool,断言退化为字面量自证
:29用fake_util_module.str2bool = bool顶替生产实现以绕开 util 模块对rtp_llm.ops的依赖,:50又以空SimpleNamespace()作 master_config。于是:123的assertIs(arguments[flag]["bind_to"][0], master_config)恒真、只验证对象同一性,无法发现真实MasterConfig缺少该字段;:124比对的是测试自己硬编码的字符串;各新 flag 的type与default全无断言。这恰好绕过最关键的边界:master_client_fallback是总开关,bool("false")与bool("0")均为 True,只有真实str2bool才会解析为 False,若type=str2bool被误改为type=bool,MASTER_CLIENT_FALLBACK=false会在本应关闭的部署上静默打开兜底而该测试仍通过。此外:130
Strengths
- KVCM 被严格限定为可用性兜底:仅
resp.connection_failed触发(master_client.py:686),FlexLB 显式 8600 与业务/准入错误语义保持不变;master_addr为空时resp以connection_failed_response()起步,route_ips:251的allow_domain_fallback仍成立,domain 安全网未被绕过。开关默认关闭(py_config_modules.py:389),翻转单个 env 即可回滚。 VipHostSnapshot(host_service.py:125-235)把阻塞式服务发现搬到 daemon 线程,读侧只取短锁并返回不可变 tuple,用stale_timeout_seconds约束 last-known-good 生命周期,并复用VipServerWrapper构造期的首次发现结果避免首请求竞态。KvcmFallbackConfig.__post_init__(kvcm_fallback.py:60-121)对正数、非负、端口范围、hot_candidate_pool_size <= candidate_pool_size做集中校验,并显式排除bool(避开isinstance(True, int)陷阱);master_client.py:209-223对必填项抛出点名配置项的可读ValueError。_probe_worker(kvcm_fallback.py:763-787)校验 worker 上报的block_size与block_hash_lookahead_tokens与本端一致,不一致即排除该 worker,属正确的 fail-closed 哈希契约保护,并按(target, block_size, lookahead)去重日志避免每请求刷屏。- 选点确定性好:
_cold_rank以sha256(request_id|instance_id|route_target)打散冷候选,ttft_key/cache_leader的排序键均以route_target收尾兜底,同分不随机化、结果可复现可回放;候选身份统一收敛为(IP, route gRPC port)并在应用 Top-H 前去重。 - 两份 proto 与仓内权威副本逐字段核对后 wire 兼容:字段号、
reserved 2、reserved 19,20、optional标记与model_rpc_service.proto:439-470逐项一致,/RpcService/GetWorkerStatus与线上引擎路径逐字相同;kvcm_meta_service.proto声明了完整kv_cache_manager.proto.meta命名空间,不污染全局符号表。 - 消费侧 gRPC 使用稳妥:
kvcm_fallback.py:607-638按 target 缓存 channel/stub 并配置 keepalive,:663/:730/:759每次 RPC 都显式传 timeout,无无界等待。 master_client_fallback_block_hash_lookahead_tokens用-1而非0作未设置哨兵(py_config_modules.py:396),正确区分「未配置」与「引擎上报 0」;新增测试用真实grpc.aio.server()做端到端 RPC 验证,calculate_vllm_block_cache_keys有 FlexLB/vLLM 双侧金标向量与「丢弃末尾不完整 block」用例。
| @@ -0,0 +1,75 @@ | |||
| syntax = "proto3"; | |||
|
|
|||
| // Keep this file package-less: the deployed engine serves | |||
There was a problem hiding this comment.
[P0] 无 package 的 worker_status proto 与引擎 model_rpc_service proto 根符号完全重名,开关一开即 duplicate symbol 崩溃
该文件注释「Keep this file package-less」,故 StatusVersionPB(:6)、TaskInfoPB(:11)、KvCacheGroupModePB(:42)、WorkerStatusPB(:48)、service RpcService(:73) 全注册为顶层符号。已核实 model_rpc_service.proto 同样无 package 声明,且在 342/390/439/445/654 行定义完全同名的这 5 个符号。worker_status_service_pb2.py:7,17 走 _descriptor_pool.Default().AddSerializedFile(...),落进程级全局 pool;model_rpc_service_pb2 由 generate_grpc_proto 生成、同样落默认 pool。两者在同一前端进程加载:backend_rpc_server_visitor.py:10 导入 ModelRpcClient(`model_rpc_client.py...
建议: 优先复用已有 //rtp_llm/cpp/model_rpc/proto:model_rpc_service_py_proto:它已提供字段号完全一致的 StatusVersionPB/WorkerStatusPB 与方法路径同为 /RpcService/GetWorkerStatus 的 RpcServiceStub,把 kvcm_fallback.py 改为从 rtp_llm.cpp.model_rpc.proto.model_rpc_service_pb2(_grpc) 导入并删除 worker_status_proto/ 整个目录(同目录 vit_proxy_server.py:16 已是此模式),一并消除 DRY 与后续 proto 漂移。若确需保留独立轻量副本,必须为其声明专属 package 隔离符号(stub 中保留 '/RpcService/GetWorkerStatus' 字面路径即可维持 wire 兼容),或改用私有 descriptor_pool.DescriptorPool();并补一个在真实 rtp_llm 包下同进程先后 import 两个 pb2 的回归用例锁死该不变量。
Checklist: [6.1] 依赖方向:无循环依赖/跨层惊喜
| @@ -4,9 +4,12 @@ py_library( | |||
| name = "server", | |||
| srcs = glob([ | |||
There was a problem hiding this comment.
[P0] server py_library 的 srcs glob 漏掉 worker_status_proto,wheel 与 runfiles 均缺该模块
py_library(name="server") 的 srcs = glob(["*.py", "server_args/*.py", "kvcm_proto/*.py"])(:5-9) 本次只补了 kvcm_proto/*.py,遗漏同批新增的 worker_status_proto/*.py;bazel 的 *.py 不递归子目录。全仓 BUILD 检索确认唯一命中为本文件 :8 的 kvcm_proto,不存在 worker_status_proto/BUILD,rtp_llm/BUILD 亦无 server/** 兜底 glob,故该目录 4 个文件不属于任何 target。而 kvcm_fallback.py:24-27 是模块级 import,master_client.py:149-150 在 client_fallback_enabled 时于 __init__ 中无 try 保护调用 _create_kvcm_fallback_client()(:155 即 import)。故 bazel 产物/wh...
建议: 在 glob 中补 "worker_status_proto/*.py",与 kvcm_proto/*.py 对称。更稳妥的是按上一条建议复用 model_rpc_service_py_proto 并删除该目录,或按仓库既有约定(rtp_llm/cpp/model_rpc/proto/BUILD:21-42 的 generate_grpc_proto + py_library)为 proto 目录建独立 target 并显式 deps,由 bazel 托管 srcs,从结构上消除后续新增 proto 子包时再次漏配的可能。修复后请以非 manual 方式实跑 //rtp_llm/server/test:kvcm_fallback_test 验证 runfiles 完整,并补一个通过真实包路径 import rtp_llm.server.kvcm_fallback(不改写 sys.modules)的轻量用例,使此类打包缺失能被 CI 直接拦住。
| "//rtp_llm:grpcio", | ||
| "//rtp_llm:protobuf", | ||
| ], | ||
| tags = ["manual"], |
There was a problem hiding this comment.
[P1] 三个新增兜底测试全部标记 manual,且伪造掉两个 P0 所在的生产边界,新特性 CI 零覆盖
kvcm_fallback_test(:35)、master_client_fallback_test(:45)、backend_rpc_server_visitor_fallback_test(:54) 均带 tags=["manual"],按 bazel 语义不会被 //... 展开,本 PR 也未改任何 CI 配置去点名,故约 2000 行路由改动在 CI 中唯一会执行的是只断言 flag 名的 master_group_args_test。三者均不依赖 //rtp_llm:testlib、无 GPU 需求,而同文件 vit_proxy_server_test(:57) 反而依赖 testlib 且未标 manual。三份测试又都用 sys.modules 合成 rtp_llm.* 命名空间(kvcm_fallback_test.py:12-24 把 __path__ 指向源码目录),恰好绕过两个 P0 所在的真实打包与 descriptor pool 边界。覆盖面亦不足:`backend_rpc_server_visitor...
建议: 去掉这三个目标的 manual 标记纳入 CI 通配;若 kvcm_fallback_test 确因远端沙箱限制 loopback 端口而必须 manual,请在 BUILD 中注明原因,并至少保证完全不涉网络的另两个目标在 CI 执行。补齐关键边界用例:fake 返回 connection_failed=True / fallback=True 时断言 domain_calls == 1 且最终取到 domain 地址、未抛 ROUTE_ERROR;input_token_batched=True 时断言跳过 master 路由;get_master_addr() 非空时断言行为与改动前一致。并额外补一个非 manual 的轻量导入型测试,通过真实 rtp_llm.server 包路径 import kvcm_fallback,使打包与符号注册类缺陷有测试兜底。提交前请实跑全部新增 target,确保「已加测试」与「测试可执行」一致。
Checklist: [6.1] 分布式/跨平台变更有对应覆盖
| self._worker_status_stubs[target] = stub | ||
| return stub | ||
|
|
||
| async def _resolve_leader(self, trace_id: str) -> str: |
There was a problem hiding this comment.
[P1] KVCM leader 解析失败无负缓存且被单锁串行,故障期逐请求放大延迟
_resolve_leader 在 async with self._leader_lock(:645) 内先 asyncio.to_thread 做 bootstrap 发现,再顺序遍历全部 target 逐个 GetClusterInfo,每个用 request_timeout_ms(默认 100ms)。全部失败且 previous_leader 为空时 :680 直接 raise,而 self._leader_refreshed_at 仍为初始 0.0——无失败时间戳、无熔断、无 backoff(全仓亦搜不到 breaker/cooldown)。:643/:647 的双检查以 self._leader 为前提,leader 为 None 时恒假,故每个排队请求都重跑完整串行扫描并互相阻塞在同一把锁上。_invalidate_leader(:682) 在 GetHostCacheState 失败(:733)与 SERVER_NOT_LEADER(:738) 时都把 _leader 置 None,使该路径在 KVC...
建议: 补充失败负缓存与熔断:解析失败时也记录 _leader_failed_at,在一个短冷却窗口(如 request_timeout_ms 的数倍)内让后续请求直接快速失败而不重复扫描,冷却后半开重试;bootstrap 目标改为并发 asyncio.gather 或限定单次扫描的目标数量,避免串行累加;并为 leader 解析失败/超时与「已熔断」状态上报 kmonitor 计数指标,使故障期行为可观测、可告警。
| return None | ||
| return worker_load_snapshot(candidate, response) | ||
|
|
||
| async def query_and_select( |
There was a problem hiding this comment.
[P1] 兜底链路无整体时间预算,WorkerStatus 信号量获取也无 deadline
query_and_select(:790-924) 与 master_client._try_kvcm_fallback(:432) 全链路无任何 asyncio.wait_for,只有单次 RPC 级 timeout;backend_rpc_server_visitor.py:235 的调用点同样未加超时,请求级 TTFT 预算对兜底路径不生效。_worker_status_semaphore(:594) 按 worker_status_concurrency(默认 3)在 __init__ 创建,为该客户端所有并发请求共享;_probe_worker(:750) 的 async with 无获取超时且跨整个 RPC 持有。默认 candidate_pool_size=3、cold_candidate_batch_size=3,单请求首轮 asyncio.gather(:875) 即占满全部许可;FlexLB 故障时所有请求都走该路径,探测排队、延迟随并发线性放大且无上界,plan.batches() 的多轮探测(:873-...
建议: 用 asyncio.wait_for 给 query_and_select(或 _try_kvcm_fallback)加整体预算,预算由 TTFT 剩余时间或专用配置派生,超时即返回 None 交由 domain fallback;信号量按并发规模放大或改为按请求限流并给获取加超时;同时为兜底整体延迟与「已超预算」次数增加 kmonitor 指标,使 fail-fast 与 fallback 的边界显式可控可告警。
| return FlexlbResponse.ok( | ||
| [ | ||
| RoleAddr( | ||
| role=RoleType.PREFILL, |
There was a problem hiding this comment.
[P2] 兜底返回的 RoleAddr 硬编码为 PREFILL,与打分层的 PDFUSION 支持不一致
select_cache_affinity_first:461-464 明确接纳 "PREFILL" in role or "PDFUSION" in role 的 worker,但 _try_kvcm_fallback 在 :511-519 无条件构造 RoleAddr(role=RoleType.PREFILL, ...),丢弃了 worker 实际上报的角色(kvcm_fallback.py:430 已从 WorkerStatusPB.role 取到),即 PDFUSION worker 会被贴上 PREFILL 标签。而 get_backend_role_list:118-120 在 FRONTEND 配置 pdfusion_domain 时产出 [PDFUSION]。此时 get_master_route_addrs:163 成功返回 None,route_ips:251 的 allow_domain_fallback 恒为 True,need_domain_routing(:248) 因 {PREFILL} ...
建议: 按 worker 上报角色(worker_load_snapshot 已保留 role)或部署形态的 backend_role_list 返回正确的 RoleType,PDFUSION 部署应返回 PDFUSION,使兜底结果能满足 need_domain_routing 而不被 domain 覆盖;对「兜底已给出正确角色、仅需补齐其余 role」的情形把日志降为 debug 或改用区分文案,避免把正常补齐误报为 domain 降级并污染 DOMAIN_ROUTE_QPS 基线。若当前只打算支持纯 PREFILL 部署,则应在 select_cache_affinity_first 中同步收窄 role 过滤并在配置校验处显式拒绝其他形态,让两层契约一致,并补一个 PDFUSION 形态下断言返回角色的用例。
Checklist: [6.1] LSP:子类/重写保持基类契约
| return CandidatePlan(tuple(hot), tuple(cold[:cold_budget])) | ||
|
|
||
|
|
||
| def build_candidate_pool( |
There was a problem hiding this comment.
[P2] 兜底链路存在无调用方的死代码与未装配的隐藏参数,p2p_hit_discount 成为无效旋钮
build_candidate_pool(:397) 自述为「Compatibility wrapper around build_candidate_plan」,但该模块本 PR 全新引入、无历史版本需兼容;全仓检索确认它与 select_max_local_affinity(:289) 除自身定义与 __all__(:953,958) 外无任何调用方,属投机性抽象。另有三个只在 KvcmFallbackConfig 带默认值、无任何 CLI/env 入口的隐藏参数:leader_refresh_interval_ms=10_000(:43,直接决定故障期 leader 重扫频率)、p2p_host_count=0(:44)、minimum_local_blocks=1(:48)。其中 p2p_host_count 直接写入 GetHostCacheStateRequest(:725) 且全文检索 master_client.py 无任何装配点,恒为 0 → KVCM 不返回 p2p 匹配、p2p_fetch_blocks 恒为 0,...
建议: 删除 build_candidate_pool 与 select_max_local_affinity 及其 __all__ 条目(若仅测试需要,改为直接调用 build_candidate_plan)。对 p2p 二选一:要么补上 p2p_host_count 的配置项与装配使 p2p_hit_discount 真正生效,要么先移除 master_client_fallback_p2p_hit_discount 这个无效 flag(YAGNI),待 p2p 能力接通时再一并引入,避免运维误认为可调。同时把 leader_refresh_interval_ms 与 minimum_local_blocks 暴露为 master_* 配置,或在注释中说明为何刻意固定,避免出现只能改代码才能调整的隐藏行为。
|
|
||
| resp = await self._send_schedule_request( | ||
| master_addr, payload, ttft_timeout_ms, request_id | ||
| flexlb_timeout_ms = ttft_timeout_ms |
There was a problem hiding this comment.
[P2] flexlb_transport_timeout_ms 不受 kill switch 约束,且默认 0 使挂死场景兜底不可达
get_backend_role_addrs 在 :652-666 无条件 getattr 读取 master_client_fallback_flexlb_transport_timeout_ms 并执行 min(ttft_timeout_ms, configured),与 client_fallback_enabled 完全无关,同时作用于 master(:670) 与 slave(:680)。两个后果:其一,该参数以 master_client_fallback_ 前缀命名、help 写作 "Per-FlexLB-attempt fallback trigger timeout"(master_group_args.py:268),灰度中先配好该值再把开关回滚为 False 时,FlexLB 仍按缩短后的 deadline 超时且此时已无 KVCM 兜底,只能退化为 domain routing 并丢失 cache 亲和路由;其二,默认值为 0(py_config_modules.py:416),此时 `flexlb_timeout_ms...
建议: 二选一:(a) 把该 min() 收敛进 if self.client_fallback_enabled 分支,使 master_client_fallback 成为真正的单一 kill switch,并给一个开关开启时生效的合理非零默认(或在开关开启且该值为 0 时启动期告警),确保挂死场景能在有界时间内触发兜底;(b) 若确实希望它独立于开关生效,则改名为 master_flexlb_transport_timeout_ms(含 env_name)并同步修正 help 与 py_config_modules.py 附近注释,避免运维误判回滚范围。
| @@ -223,16 +223,25 @@ async def route_ips(self, input: GenerateInput): | |||
| input_token_batched = True | |||
There was a problem hiding this comment.
📍 实际位置 rtp_llm/server/backend_rpc_server_visitor.py:162(不在 diff 展示范围内,就近挂载)
[P2] 兜底路径缺少专属指标,三个新增响应字段无生产消费方
FlexlbResponse 新增 route_source(:47)、cache_match、kvcm_outcome(:49),兜底成功时置为 "CLIENT_FALLBACK" 与 18 项 cache_match 明细(master_client.py:472-522)。但全仓检索 route_source/kvcm_outcome 仅命中 master_client.py 自身与 master_client_fallback_test.py:189,226,生产路由层完全不读取。get_master_route_addrs 在 :162 对兜底成功与真实 FlexLB 成功一律上报同一个无标签 MASTER_ROUTE_QPS_METRIC,route_ips:236 的 MASTER_ROUTE_RT_METRIC 也无维度且混入 KVCM 发现与 worker 探测耗时,既无法度量降级触发率与恢复,也会让基于该 RT 的既有看板/告警基线漂移。另有静默失效路径:`kvcm_fallback.py:774-7...
建议: 在 :162/:236 读取 route_result.route_source 作为 kmonitor tag 上报(或为 CLIENT_FALLBACK 新增独立 QPS/RT 指标),避免污染既有 FlexLB RT 基线;为 kvcm_outcome、selection_reason、cache_query_outcome(含 no_positive_match)与 block hash 契约不匹配各增计数指标,使兜底触发率、选点原因与配置错填可被告警发现;兜底路径的失败日志按去重或采样输出,避免热路径逐请求 ERROR。
Checklist: [6.1] 无 per-forward 调试日志 / 噪声热路径输出
| self.config = config | ||
| self._bootstrap_resolver = bootstrap_resolver | ||
| self._candidate_snapshot_resolver = candidate_snapshot_resolver | ||
| self._channels: dict[str, grpc.aio.Channel] = {} |
There was a problem hiding this comment.
[P2] gRPC channel 与选择历史字典只增不减,滚动发布后累积僵尸连接
KvcmFallbackClient 用四个普通 dict 缓存连接:_channels、_stubs、_worker_status_channels、_worker_status_stubs(:587-590);_stub_for(:601) 与 _worker_status_stub_for(:619) 只做「没有就建」,无容量上限、TTL 或失败淘汰。_invalidate_leader(:682) 只把 _leader 置 None,对应 channel 与 stub 仍留在字典中。_logged_worker_hash_contract_mismatches(:597) 与 _last_selected_ns(:598) 同样只增不减(后者在 :899 持续写入)。这些 key 都是 IP:port 形态的 route_target,在 Pod 滚动更新/扩缩容频繁的集群中会随历史节点数持续累积,长期运行进程将累积 grpc.aio channel(连同 fd 与后台 IO 资源)而永不释放;只有 `close()...
建议: 给四个连接字典引入有界 LRU(容量按活跃 worker 数量级或 candidate_pool_size 的若干倍设定),淘汰时 await channel.close();_invalidate_leader 同步关闭并移除对应 channel/stub。_last_selected_ns 与 _logged_worker_hash_contract_mismatches 改为有界结构,或在候选快照刷新时按当前存活 target 集合收敛清理。
|
|
||
| master_route_result: Optional[FlexlbResponse] = None | ||
| if not role_addrs_specified and master_addr and not input_token_batched: | ||
| can_attempt_master_route = bool(master_addr) or bool( |
There was a problem hiding this comment.
[P3] 路由准入判定跨类重复,visitor 直接读取 MasterClient 内部开关且日志归因错误
:226 的 can_attempt_master_route = bool(master_addr) or bool(self.master_client.client_fallback_enabled) 把「客户端能否路由」这一策略判断复制到调用方,而 master_client.py:439 的真实前置条件还包含 self._kvcm_fallback_client is not None;一旦前置条件演进,visitor 的门禁会静默与之偏离。同时 visitor 在 :218 读一次 host_service.get_master_addr() 用于门禁与日志,master_client.py:620 内部又独立读一次同一 snapshot,两次读取之间后台刷新线程可能更新,导致日志中的 master_addr 与实际发请求的地址不一致。另外 :239-246 的 elif 分支在开关开启时只可能因 input_token_batched=True 进入(此时 can_attempt_master_route 恒为真),但文案...
建议: 把准入判断下沉为 master client 的一个方法(如 master_client.can_route()),由其内部一次性读取 master_addr 并结合 client_fallback_enabled 与 _kvcm_fallback_client 就绪状态给出结论,visitor 只消费该布尔结果与返回的诊断信息;同时按实际命中的条件分别打日志:batched 输入单独一条明确说明跳过 master 路由,can_attempt_master_route 为假时再打一条含 master_addr 与开关状态的日志,避免单条日志混合归因。
Checklist: [6.1] 分层边界:新概念在正确层级,不泄漏内部;[6.1] DIP:高层策略不依赖非必要具体细节
| @@ -223,16 +223,25 @@ async def route_ips(self, input: GenerateInput): | |||
| input_token_batched = True | |||
There was a problem hiding this comment.
📍 实际位置 rtp_llm/server/backend_rpc_server_visitor.py:133(不在 diff 展示范围内,就近挂载)
[P3] token_ids 到 list 的 O(prompt_len) 转换在路由路径上重复执行两次
get_master_route_addrs:133-138 已把 input.token_ids 整体 tolist() 一次用于 get_block_cache_keys。开关开启且走兜底时,master_client._input_ids_for_kvcm(:411-430) 又对同一 token_ids 做第二次 tolist()(:417-418) 并额外执行 [int(token_id) for token_id in raw_input_ids](:428) 全量遍历,即在每请求路由路径上多一次 O(prompt_len) 的 Python 层拷贝与逐元素转换;长 prompt 场景下是可避免的重复开销(未做基准测量,故按 P3 记录)。该函数还用 getattr(input, "input_ids", None)/hasattr(raw_input_ids, "tolist") 做类型探测,与 GenerateInput 的既有字段契约脱耦。
建议: 把 get_master_route_addrs 中已转换好的 token 列表作为参数传入 get_backend_role_addrs/_try_kvcm_fallback,复用同一份 list 而不在下游重新转换;_input_ids_for_kvcm 的 getattr/hasattr 探测改为依赖 GenerateInput 的显式字段与类型契约(或在函数签名上直接要求 List[int])。
Checklist: [6.1] DRY:重复非平凡逻辑被抽取或显式复用;[P.A] 禁止 getattr/setattr literal 访问;[P.A] 禁止 hasattr 做控制流分支
| self.master_client.client_fallback_enabled, | ||
| input_token_batched, | ||
| ) | ||
| specified_roles = {addr.role for addr in input.generate_config.role_addrs} |
There was a problem hiding this comment.
📍 实际位置 rtp_llm/server/backend_rpc_server_visitor.py:324(不在 diff 展示范围内,就近挂载)
[P3] 新开关被 host_service.service_available 前置门控静默旁路
enqueue(:324) 与 batch_enqueue(:335) 仅在 self.host_service.service_available 为真时才调用 route_ips,而 host_service.py:660 中 service_available = bool(self.master_vip.domain) or any(self.role_vip_map.values())。若运维只打开 master_client_fallback 而未配置 master domain 与任何 role domain,新增的 can_attempt_master_route 分支永远不会被执行,开关静默无效且日志中没有任何提示,排查成本高。
建议: 在 BackendRPCServerVisitor.__init__ 中检测 master_client.client_fallback_enabled and not self.host_service.service_available,命中时打一条 warning 明确说明该开关因 host service 不可用而不会生效;或把 client_fallback_enabled 也纳入 service_available 的判定,使开关语义与实际生效范围一致。
Checklist: [6.1] 可观测性:日志/指标/超时可操作、非噪声;[6.1] 回滚路径:风险行为存在运维回滚手段
| await client.close() | ||
|
|
||
| async def test_worker_status_is_fresh_concurrency_bounded_and_channels_reused(self): | ||
| self.service.hosts = [ |
There was a problem hiding this comment.
[P3] 并发上界为单边断言,无法区分正确限流与并发彻底失效
test_worker_status_is_fresh_concurrency_bounded_and_channels_reused(:279) 通过 fake WorkerStatus 服务端统计并发峰值,最终只在 :336 断言 assertLessEqual(self.worker_service.max_active, 2),即只有上界没有下界。若信号量实现退化为完全串行(max_active == 1),断言仍然通过,而这正是本 PR 探测阶段最关心的性能属性;同样地,kvcm_fallback.py:750 的信号量若被误设为 1,该测试不会失败。用例已在 :286 设置 worker_service.delay_s = 0.01 刻意引入延迟,具备断言下界的条件。
建议: 补一条下界断言(候选数大于 1 且服务端已引入延迟时断言 max_active >= 2),使「正确限流」与「并发彻底失效」可区分;或直接断言 max_active == 期望并发数,让配置回归能被捕获。
Checklist: [6.1] 边界 case 覆盖(空、单元素、最大值)
| "when KVCM fallback is enabled" | ||
| ) | ||
|
|
||
| self._kvcm_vip = VipServerWrapper( |
There was a problem hiding this comment.
[P3] master_kvcm_use_local 场景下 service_id 中填写的端口被静默丢弃
:225-228 以 VipServerWrapper(service_id, use_local) 构造发现源,随后 resolve_bootstrap_targets(:235-243) 一律用配置的 bootstrap_port 重组 ip:port,忽略 host 自带端口。而 --master_kvcm_service_id 的 help 明确写作 "KVCM bootstrap service id or local IP:port list"(master_group_args.py:58),即文档上支持直连 IP:port 形态;本地/直连调试时若按该说明填写 host:port,端口会被静默替换为 master_kvcm_bootstrap_port(默认 6381),既不报错也无日志提示,容易误判为 KVCM 不可达。
建议: 在 use_local 或 service_id 含端口时保留原端口,或在检测到 host 自带端口与 bootstrap_port 不一致时打一条 warning 明确说明以哪个为准;若确实只支持统一端口,请同步修正 help 文案去掉 "IP:port list" 的表述。
Checklist: [6.1] 错误语义:fail-fast/retry/fallback/silent 行为显式
No description provided.