feat: enable Qwen3.5 W8A8 integration - #1323
Conversation
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1323
Status: LGTM
Summary: P0/0 · P1/0 · P2/15 · P3/9
Reviewed: commit 131f35d17f8a · 2026-08-20 21:21 UTC+8
lgtm ready to ci
Non-blocking Suggestions
P2
- propose_tokens_gpu 两个写入方形状与取值语义分叉,混合 batch 改换取值路径 @
rtp_llm/cpp/model_rpc/DecodeRpcServer.cc:338- 建议:在
GenerateStream.h:91补上与draft_token_gpu一致的形状/内容契约注释(明确是“全部 propose_step 个 draft”还是“单 token 种子”),让DecodeRpcServer与StreamCacheResource两个 producer 写入同一约定。若确认 decode gRPC 交接按设计只需单 token,请在DecodeRpcServer.cc:333注明与 P2P 多 token 语义的差异并对propose_step>1加断言;否则按契约写入全部 draft。补一条propose_step>1单测,断言pickOneStepDraftToken与collectLegacyProposeSlices在 gRPC/P2P 两种来源下取到同一 token。
- 建议:在
- backend hook 在全局 RLock 内执行,与 Python import 锁构成锁序反转 @
rtp_llm/utils/backend_registry.py:96- 建议:锁内只做生命周期判定与 hook 快照,出锁后执行:
with _lock: ...; hooks = tuple(_hooks.get(slot, ())),再在with块外循环hook(**context);_started/_repeatable置位保留在锁内。若需保持“槽位消费前注册已生效”的可见性,可配合 per-slotthreading.Event让并发的第二个调用方等待首次完成,或改为 per-slot 锁打断跨槽位环路。并在文档「失败语义」一节写明 hook 执行期间持有哪些锁。
- 建议:锁内只做生命周期判定与 hook 快照,出锁后执行:
- 一次性槽位在 hook 失败或 owner 重建后静默 no-op,退化为部分注册 @
rtp_llm/utils/backend_registry.py:91- 建议:区分“同一 owner 重复消费”与“新 owner”:在
except中把该槽位从_started/_repeatable移除后 re-raise,使状态与实际注册结果一致;或记录已服务过的 owner 身份,对新 owner 重放冻结后的 hook 集合。若坚持当前一次性语义,请在文档「失败语义」中显式写明“hook 失败后该槽位不再执行”,并让第二次调用记录 warning 而非完全静默。
- 建议:区分“同一 owner 重复消费”与“新 owner”:在
- moe_strategy_choices 把 argparse 私有内部固化为跨仓契约,且文档与测试给出两种互不兼容写法 @
rtp_llm/server/server_args/moe_group_args.py:201- 建议:把 context 收窄为可直接操作的对象:在
moe_group_args或EnvArgumentParser上提供具名扩展函数(如add_moe_strategy_choices(*names),内部封装 action 定位、去重与容器类型统一),以run_backend_registrations("moe_strategy_choices", repeatable=True, add_choices=...)传出;文档与测试统一改用该公开入口,不再示范_actions。若为兼容既有外部 hook 必须保留parser=,请标注为过渡期契约并给出迁移目标。
- 建议:把 context 收窄为可直接操作的对象:在
- 设备无关的中心工厂硬编码 W8A8 后端专属文案,且绕过统一的 quant_method 取值工具 @
rtp_llm/models_py/modules/factory/fused_moe/strategy_registry.py:87- 建议:删除两处
if分支,改为在通用ValueError/logger.error中带上quant_method与已注册策略类名列表;若确需后端定制文案,让后端在注册 hook 时向 registry 登记quant_method -> 提示语映射,把知识放回后端侧。把方法名收敛为CompressedW8A8Int8PerChannelQuantConfig.get_method()的引用(deepep_wrapper.py同),并把strategy_registry.py:74-78统一为MoeConfigResolver().get_quant_method(config)。
- 建议:删除两处
- ROCm EP 量化白名单与 executor 映射构成多源真值,早退丢失 ConditionChecker 诊断 @
rtp_llm/models_py/modules/factory/fused_moe/impl/rocm/strategy/ep.py:25- 建议:将白名单与映射合并为单一数据结构(如
Dict[Optional[str], Tuple[executor, FusedMoEQuantConfig]]),can_handle用 keys 过滤、_resolve_executor_and_quant直接查表,使二者不可能漂移;确认内源 quant_config 同样不产出后,一并清理上述三处死名。前置过滤改到check_conditions()中checker.check(...)以保留拒绝原因,与 CUDA 侧一致。补正向断言(如CompressedW8A8Int8PerChannelQuantConfig.get_method() not in _SUPPORTED_QUANT_METHODS、各支持类get_method()均in白名单)。若此改动改变了以往未支持格式的 BF16 兜底行为,请在 PR description 标注对存量 ROCm 部署的影响。
- 建议:将白名单与映射合并为单一数据结构(如
- QuantMethod→字符串映射在两处 switch 重复维护且各自漏枚举值,worker status 持续打 ERROR 并对外上报 UNKNOWN @
rtp_llm/cpp/model_rpc/LocalRpcServer.cc:439- 建议:将映射收敛为一个共享函数(例如在
QuantInfo.h/QuantInfo.cc暴露quantMethodToString),LocalRpcServer与ModelConfig::to_string均复用,并顺带补齐ModelOptFP4与QuarkMXFP4。建议去掉default:改为穷举全部枚举值,让下一次新增QuantMethod在编译期暴露缺口,而不是在生产以每次轮询一条 ERROR 日志的形式暴露。
- 建议:将映射收敛为一个共享函数(例如在
- _prepare_dispatch_input 元组返回的下游契约未表达,scale 归一与 topk 重映射仍由基类隐式分支决定 @
rtp_llm/models_py/modules/factory/fused_moe/impl/cuda/routers/deepep_normal_router.py:151- 建议:在 docstring 补齐元组返回的完整下游契约(“非 FP8 元组返回将走 bf16 topk 重映射且 scale 不做
[:,0]归一”),或为 scale 后处理与 topk 重映射也提供可覆写钩子,避免外部后端依赖隐式基类分支。并在 hook 测试补一条expert_topk_ids断言把该语义钉住;另建议对非元组分支补回类型检查,使非张量返回在源头失败而非延后到expert_x.device。
- 建议:在 docstring 补齐元组返回的完整下游契约(“非 FP8 元组返回将走 bf16 topk 重映射且 scale 不做
- 槽位名与上下文键为自由字符串 + kwargs 透传,测试用生产不存在的槽位名,拼写错误静默丢注册 @
rtp_llm/utils/test/backend_registry_test.py:34- 建议:将槽位名收敛为共享
Enum/Literal(如BackendSlot.LINEAR/FUSED_MOE/ATTENTION/MOE_STRATEGY_CHOICES),register_backend_hook/run_backend_registrations只接受该类型;测试改用常量,并补一条用例:向各槽位注册 fake hook 后 import 对应工厂,断言 hook 被调用且收到预期 context 键(fused_moe 收registry、linear 收factory、attention 收四个 imps),使拼写错误在 CI 而非推理数值上暴露。
- 建议:将槽位名收敛为共享
- 两个新增量化准入守卫类的唯一回归覆盖落在带 open_skip 的 target @
rtp_llm/models_py/modules/factory/fused_moe/tests/test_cuda_strategies.py:159- 建议:确认
open_skip在当前流水线的实际语义;若确为跳过,请把这两个测试类拆到不带该标签的 target(或移除该 target 的标签),并在 PR 描述说明由哪条 CI 任务保障。ROCm 侧补正向断言(如各支持类get_method()均in _SUPPORTED_QUANT_METHODS),并考虑迁到tags=["rocm"]的 target,使白名单在两平台都有门禁。
- 建议:确认
- FP8 dtype 残留检查退化为源码文本计数断言 @
rtp_llm/model_loader/test/test_compressed_w8a8_int8_per_channel.py:484- 建议:删除该源码扫描用例,改为参数化行为断言:对 INT8 与 FP8 两种 quant_config 分别
WeightModule.create,遍历各权重名的 kernel 子权重,断言其data_type与配置声明 dtype 一致(含 MoE/stacked 调用点),从张量属性而非源码文本真正覆盖“某个调用点漏改”的风险。
- 建议:删除该源码扫描用例,改为参数化行为断言:对 INT8 与 FP8 两种 quant_config 分别
- DeepEP hook 测试用 object.new 绕过构造函数,字段与位置参数契约不受保护 @
rtp_llm/models_py/modules/factory/fused_moe/impl/cuda/routers/test/deepep_normal_router_hook_test.py:55- 建议:保留 fake buffer,但把实例构造改为对真实
__init__的最小化调用(仅 patchDeepEPWrapper.get_instance与DeepepWrapperConfig.from_config_adapter),使字段重命名立即让测试失败;dispatch改用具名关键字或显式解包完整位置参数;补一条只 stub_do_quant的 FP8 用例,断言expert_x_scale被收窄为一维。
- 建议:保留 fake buffer,但把实例构造改为对真实
- 工厂守卫测试仅覆盖空/伪注册表分支,未覆盖真实注册表状态 @
rtp_llm/model_loader/test/test_compressed_w8a8_int8_per_channel.py:151- 建议:补一条用真实注册表的用例:构造 W8A8 的 MoE config,断言导入
fused_moe包后的生产StrategyRegistry抛 W8A8 专属ValueError;再补一条“未量化配置在真实注册表下仍能选出策略”的反向基线。这两类断言测的是models_py/modules/factory的行为,建议把BackendAvailabilityGuardTest迁到该目录下的测试目标。
- 建议:补一条用真实注册表的用例:构造 W8A8 的 MoE config,断言导入
- server_args 新用例把其声称覆盖的入口加载边界整体 mock 掉且缺反向基线 @
rtp_llm/server/server_args/test/server_args_test.py:18- 建议:保留现有时序断言,另补:(a) 不 mock 加载函数、向
sys.modules注入模块级调用register_backend_hook的临时入口模块,走真实链路;(b) 无 hook 时setup_args(["--moe_strategy","external_test_strategy"])触发SystemExit,锁定 hook 是放行外部取值的唯一来源;(c) 槽位启动后注册抛RuntimeError(带assertRaisesRegex)。并把该用例移入独立测试类,清理改由setUp/addCleanup管理。
- 建议:保留现有时序断言,另补:(a) 不 mock 加载函数、向
- reset_backend_registrations 无法恢复入口导入缓存,可能污染同进程后续用例 @
rtp_llm/utils/backend_registry.py:99- 建议:在
reset_backend_registrations()中同时调用import_optional_internal_source_entrypoint.cache_clear()(或提供reset_backend_entrypoint_cache()供测试显式调用),使入口可重新导入并重新登记;并在该函数 docstring 与文档职责表补一句“仅在已 patch 入口加载函数的测试中安全——单纯cache_clear()也无法让sys.modules中的入口模块重新执行”。
- 建议:在
P3
- setup_args 与 run_backend_registrations 重复触发入口加载且丢弃返回值 @
rtp_llm/server/server_args/server_args.py:534- 建议:二选一:删除这次重复调用,由槽位消费点统一负责加载;或保留但消费返回值(如
logging.info("internal backend entrypoint loaded=%s", loaded)),使--moe_strategy出现invalid choice时能从启动日志一眼定位到 entrypoint 未加载。若保留,请在:532-533注释中说明它与 registry 内部加载的关系(仅让时机可读,不承担正确性)。
- 建议:二选一:删除这次重复调用,由槽位消费点统一负责加载;或保留但消费返回值(如
- 库目标未声明新增的 //rtp_llm:utils 依赖,仅测试目标补了 @
rtp_llm/server/server_args/moe_group_args.py:2- 建议:在
rtp_llm/server/BUILD的server目标 deps 补上"//rtp_llm:utils",使运行期依赖与声明一致,避免仅依赖//rtp_llm/server:server的下游目标 runfiles 缺少backend_registry.py。同包内既有同类欠账可另开变更统一清理,不必在本 PR 扩大范围。
- 建议:在
- GenerateStream 新用例只覆盖“缺失则保留”,未覆盖“提供则刷新” @
rtp_llm/cpp/engine_base/stream/test/GenerateStreamTest.cc:117- 建议:追加一次带已定义
draft_token_gpu(如值 11)的specUpdate,断言propose_tokens_gpu被刷新为 11,使两条分支同时被钉住;改用指定初始化或具名构造避免位置错位;顺带断言 CPUtokens的哨兵值以表达“为何不能回退到 CPU tokens”的意图。
- 建议:追加一次带已定义
- choices 扩展点只保护走 argparse 校验的通道,env 补齐通道静默绕过 @
rtp_llm/server/server_args/server_args.py:366- 建议:不必在本 PR 修改解析器行为,但建议在该扩展点或
backend_registry文档中说明:choices 扩展仅对走 argparse 校验的通道生效,env 补齐通道不校验取值。若后续要收口,可在_apply_config_bindings之前对带choices的 action 统一补一次_check_value,并把当前静默pass改为显式self.error(...),使非法 env 取值 fail-fast。
- 建议:不必在本 PR 修改解析器行为,但建议在该扩展点或
- W8A8 分支对 input_activations 缺失键抛裸 KeyError,与同函数具名报错风格不一致 @
rtp_llm/config/quant_config.py:268- 建议:对这三个键改用
.get(...)判空或读取前做键存在性校验,使畸形 W8A8 checkpoint 落入既有的unsupported compressed-tensors scheme ...明确报错分支,保持 fail-fast 信息风格一致。
- 建议:对这三个键改用
- backend_registry 生命周期异常断言未校验错误消息 @
rtp_llm/utils/test/backend_registry_test.py:66- 建议:四处改为
assertRaisesRegex,分别带上"already initialised"、"lifecycle changed"、"backend is broken"等匹配串,与本 PR 其他测试保持一致,并使未来分支演化不会让用例悄悄换成另一条路径通过。
- 建议:四处改为
- no_auant 拼写错误的策略取值已出现第三处副本,且测试绕过生产决策入口 @
rtp_llm/models_py/modules/factory/fused_moe/tests/test_cuda_strategies.py:179- 建议:将策略名收敛为模块级常量,供 argparse
choices、策略比较与测试共同引用(顺带在一处修正拼写,并按需保留旧取值作为兼容别名);_conditions_pass补注释说明“为规避get_attributes()的 deep_ep/deepgemm 惰性导入才绕过can_handle”,避免后续读者误以为覆盖了生产决策入口。
- 建议:将策略名收敛为模块级常量,供 argparse
- PPU 设备识别依赖环境变量真值判断且无任何日志,CUDA 主机可能被静默重分类 @
rtp_llm/device/device_type.py:20- 建议:在设备类型解析处补一条 info 日志,写明判定结果与依据(
PPU_HOME命中或 torch 版本串命中),使误判可从启动日志一眼看出;并考虑把PPU_HOME的真值判断收紧为路径存在性校验,避免空目录或残留变量触发重分类。请作者确认该判定分支的引入范围与既有 CUDA 部署的兼容性。
- 建议:在设备类型解析处补一条 info 日志,写明判定结果与依据(
- arch.py 新增的 is_ppu 自身未使用,仅作为隐式再导出通道 @
rtp_llm/models_py/utils/arch.py:11- 建议:让
qwen3_next.py直接从rtp_llm.device.device_type导入这四个符号,并移除arch.py中未使用的导入;若确实要把arch作为设备判定门面,请显式声明__all__并在模块 docstring 写明其再导出职责,使该约定可被工具与读者识别。
- 建议:让
Checklist Findings (19 fail / 55 total)
General Principles Checklist
- [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue
库目标未声明新增的 //rtp_llm:utils 依赖,仅测试目标补了
本次为测试目标补了"//rtp_llm:utils"(server_args/test/BUILD),但真正新增模块级 import 的是库代码:moe_group_args.py:2与server_args.py:68导入rtp_llm.utils.backend_registry,而rtp_llm/server/BUILD:8-22的py_library(name="server")deps 只有:request_headers、//rtp_llm:warmup、//rtp_llm:vipserver与一个 proto 目标,无//rtp_llm:utils。同包内已存在同类未声明导入,故属既有欠账;//rtp_llm:utils以 glob 收纳utils/**/*.py,新文件本身会被自动纳入。 - [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
PPU 设备识别依赖环境变量真值判断且无任何日志,CUDA 主机可能被静默重分类
get_device_type()在torch.cuda.is_available()分支内以os.environ.get("PPU_HOME") or "ppu" in getattr(torch,"__version__","").lower()(:20-24)判定DeviceType.Ppu,全程无日志。一旦 CUDA 主机存在任意非空PPU_HOME(例如残留环境或并装工具链),is_cuda()即翻为 False,连带改变fused_moe/__init__.py的 FP4 策略注册、arch.py的 SM 判定与get_num_device_sms(),而启动日志中没有任何可据以定位的线索。该文件未被任何分片认领,由集成侧补验。 - [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue
arch.py 新增的 is_ppu 自身未使用,仅作为隐式再导出通道
arch.py:6-12新增导入is_ppu,但全文件仅第 11 行出现该符号,无任何使用;其唯一消费者是qwen3_next.py:238-248,它从rtp_llm.models_py.utils.arch而非源头rtp_llm.device.device_type导入get_device_type/is_cuda/is_hip/is_ppu。该文件既无__all__也非包__init__,因此这是一个未声明的再导出通道:linter 会将其报为未使用导入,任何“清理未使用导入”的改动都会直接打断qwen3_next.py。is_hip/get_device_type已存在同样情况,属既有约定。 - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
PPU 设备识别依赖环境变量真值判断且无任何日志,CUDA 主机可能被静默重分类
get_device_type()在torch.cuda.is_available()分支内以os.environ.get("PPU_HOME") or "ppu" in getattr(torch,"__version__","").lower()(:20-24)判定DeviceType.Ppu,全程无日志。一旦 CUDA 主机存在任意非空PPU_HOME(例如残留环境或并装工具链),is_cuda()即翻为 False,连带改变fused_moe/__init__.py的 FP4 策略注册、arch.py的 SM 判定与get_num_device_sms(),而启动日志中没有任何可据以定位的线索。该文件未被任何分片认领,由集成侧补验。 - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
reset_backend_registrations 无法恢复入口导入缓存,可能污染同进程后续用例
reset_backend_registrations()只清_hooks/_started/_repeatable(:101-104),但入口导入由import_optional_internal_source_entrypoint的@lru_cache(maxsize=None)缓存(import_util.py:26),且已导入模块留在sys.modules不会二次执行模块体。清理后进程仍认为入口“已加载”,ensure_backend_entrypoint_loaded()直接命中缓存返回 True,而入口在导入期登记的 hook 已丢失且无法再登记。这正是server_args_test.py:18那条用例必须双重 mock 入口加载函数来规避的限制;在存在内源且未 patch 加载函数的环境中,任何依赖“reset 后重放真实入口”的用例都会静默失去--moe_strategy扩展。 - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
W8A8 分支对 input_activations 缺失键抛裸 KeyError,与同函数具名报错风格不一致
W8A8 判定链在isinstance(activation_config, dict)之后直接下标activation_config["type"]、["num_bits"]、["strategy"](:268-270),仅dynamic用了.get(..., False)。若 checkpoint 提供了input_activations字典但缺这三个键,会抛裸KeyError;而同函数其它不支持场景(非对称 INT8、未识别 scheme)均给出带上下文的ValueError(:277、:317),错误语义不一致。 - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
no_auant 拼写错误的策略取值已出现第三处副本,且测试绕过生产决策入口
TestCudaNoQuantFallbackStrategies硬编码"no_auant_cpp"(:179,188)与"no_auant_dp_normal"(:197,205)。该拼写错误(应为no_quant_*)已存在于moe_group_args.py:167-169的 argparsechoices与no_quant.py:60,88的比较中,本测试成为第三处副本,日后修正 CLI 取值需同步三处。另外_conditions_pass(:162-172)直接调用strategy.check_conditions而非生产入口can_handle,若MoeStrategy.can_handle不再调用check_conditions,测试仍通过(同文件 ROCm 用例用的是can_handle,两者不一致)。 - [6.1] Software Engineering — ISP:调用方不依赖无关大接口 → issue
moe_strategy_choices 把 argparse 私有内部固化为跨仓契约,且文档与测试给出两种互不兼容写法
run_backend_registrations("moe_strategy_choices", repeatable=True, parser=parser)(:201)只把裸parser交给 hook,定位逻辑全部落给外部后端。仓内两处示例都因此遍历私有属性:文档backend_registration.md:107-115遍历parser._actions后action.choices = list(...)重新赋值;新增测试server_args_test.py:28-32则原地.choices.append(...)。两种写法对容器类型假设不同:choices改为 tuple 时文档写法可用而测试写法抛AttributeError;--moe_strategy一旦重命名,next(...)抛StopIteration。两种失败都在服务启动路径,且开源侧重构无法通过仓内引用搜索发现下游消费者。文档:166-169还承诺保留parser=上下文,进一步固化耦合。 - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
arch.py 新增的 is_ppu 自身未使用,仅作为隐式再导出通道
arch.py:6-12新增导入is_ppu,但全文件仅第 11 行出现该符号,无任何使用;其唯一消费者是qwen3_next.py:238-248,它从rtp_llm.models_py.utils.arch而非源头rtp_llm.device.device_type导入get_device_type/is_cuda/is_hip/is_ppu。该文件既无__all__也非包__init__,因此这是一个未声明的再导出通道:linter 会将其报为未使用导入,任何“清理未使用导入”的改动都会直接打断qwen3_next.py。is_hip/get_device_type已存在同样情况,属既有约定。 - [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue
_prepare_dispatch_input 元组返回的下游契约未表达,scale 归一与 topk 重映射仍由基类隐式分支决定
新钩子(:228-249)允许后端在use_fp8=False时也返回(activation, scale),dispatch 输出随之改为isinstance(output, tuple)驱动。但下游if use_fp8 and self.quant_config.is_per_act_token: expert_x_scale = expert_x_scale[:,0].contiguous()(:154-155)对非 FP8 元组不做归一;if recv_topk_idx.numel()!=0 and (not use_fp8) and (not use_fp4)(:167-172)又会对该路径施加 bf16 风格的expert_topk_ids重映射。钩子 docstring(:235-241)只说“tuple 返回须保持该后端 executor 期望的 scale 布局”,未提及 topk 语义。新增 hook 测试(deepep_normal_router_hook_test.py:88)只断言 scale 形状 - [6.1] Software Engineering — OCP:本地扩展点优先于修改中心逻辑 → issue
设备无关的中心工厂硬编码 W8A8 后端专属文案,且绕过统一的 quant_method 取值工具
StrategyRegistry.get_strategy(:87-93)与LinearFactory.create(linear/factory.py:117-123)各自硬编码quant_method == "W8A8_INT8_PER_CHANNEL_COMPRESSED"抛定制文案,而两者都是全设备共享的中心选择层,该量化格式执行能力由外部后端提供——PR 文档自己要求公共注册机制保持设备/厂商无关(backend_registration.md:171-172)。每新增一种仅外部后端可消费的格式都要回到两处中心加if(违反 OCP)。根因是通用报错(strategy_registry.py:94-97)未输出quant_method(linear 侧:128已输出quant_config)。该字面量另散落在deepep_wrapper.py:235、quant_config.py:949,全仓无共享常量。另strategy_registry.py:74-78直接 `quant_config.get_ - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
两个新增量化准入守卫类的唯一回归覆盖落在带 open_skip 的 target
TestCudaNoQuantFallbackStrategies(:159)是本 PR“no-quant 策略必须拒绝量化 ckpt”(no_quant.py:58,86新增quant_method is None)的唯一直接覆盖,TestRocmEpStrategyQuantFiltering(:211)是 ROCm 过滤守卫的唯一覆盖。两者都在test_cuda_strategies.py,而tests/BUILD:16该 target 带tags=["open_skip"]且本 PR 未改。作者在同 PR 的routers/test/BUILD:10为新 target 刻意只加H20未加open_skip(同文件其余 target 带),说明该标签确实影响执行范围。ROCm 用例还只断言assertFalse(can_handle)(:227),对白名单中仍应放行的None/FP8 系列无正向断言,字面量拼错会静默排除 ROCm FP8 EP 而测试全绿;且该断言运行在gpu:H20 - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
GenerateStream 新用例只覆盖“缺失则保留”,未覆盖“提供则刷新”
mtpUpdateKeepsLastGpuProposalWhenNextProposalIsMissing(:117)只构造draft_token_gpu未定义的StreamSpecUpdateInfo(:127-132),断言旧 GPU 镜像值 9 被保留。生产逻辑是if (update_info.draft_token_gpu.defined())才覆盖(GenerateStream.cc:1024-1025),另一条“定义时刷新”分支无用例:若该条件被误写成恒假或整块删除,PDFUSION 路径将永久使用陈旧 propose token,而本测试依然通过。StreamSpecUpdateInfo采用 6 字段位置聚合初始化,中间插入同类型字段会静默错位;用例也未断言tokens中-1哨兵语义。 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
GenerateStream 新用例只覆盖“缺失则保留”,未覆盖“提供则刷新”
mtpUpdateKeepsLastGpuProposalWhenNextProposalIsMissing(:117)只构造draft_token_gpu未定义的StreamSpecUpdateInfo(:127-132),断言旧 GPU 镜像值 9 被保留。生产逻辑是if (update_info.draft_token_gpu.defined())才覆盖(GenerateStream.cc:1024-1025),另一条“定义时刷新”分支无用例:若该条件被误写成恒假或整块删除,PDFUSION 路径将永久使用陈旧 propose token,而本测试依然通过。StreamSpecUpdateInfo采用 6 字段位置聚合初始化,中间插入同类型字段会静默错位;用例也未断言tokens中-1哨兵语义。
RTP-LLM Checklist
- [I] 代码质量 — 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理 → issue
ROCm EP 量化白名单与 executor 映射构成多源真值,早退丢失 ConditionChecker 诊断
_SUPPORTED_QUANT_METHODS(:25-31)与_resolve_executor_and_quant的 if/elif(:64-97)是同一份知识两处表达:新增分支时漏改集合会让can_handle先返回 False,最终以“No suitable MOE strategy found”收场,线索指向配置而非漏配白名单。同类漂移已发生——FP8_PER_BLOCK_QUARK/FP4_PER_GROUP/FP4_PER_GROUP_QUARK仍留在rocm_moe.py:289,404-405、pure_tp_router.py:209、linear/factory.py:248的判定中,而经核验quant_config.py全部get_method()(:389-949)均不产出这三个名字。此外can_handle(:44-45)直接return False绕过ConditionChecker,运维看不到拒绝原因,与本 PR 在no_quant.py采用的 - [I] 代码质量 — 同一功能用统一工具函数 → issue
no_auant 拼写错误的策略取值已出现第三处副本,且测试绕过生产决策入口
TestCudaNoQuantFallbackStrategies硬编码"no_auant_cpp"(:179,188)与"no_auant_dp_normal"(:197,205)。该拼写错误(应为no_quant_*)已存在于moe_group_args.py:167-169的 argparsechoices与no_quant.py:60,88的比较中,本测试成为第三处副本,日后修正 CLI 取值需同步三处。另外_conditions_pass(:162-172)直接调用strategy.check_conditions而非生产入口can_handle,若MoeStrategy.can_handle不再调用check_conditions,测试仍通过(同文件 ROCm 用例用的是can_handle,两者不一致)。
Python Static-First Checklist
- [P.A] 静态结构与类型纪律 — 字符串分发用 Enum/Literal → issue
槽位名与上下文键为自由字符串 + kwargs 透传,测试用生产不存在的槽位名,拼写错误静默丢注册
生产槽位仅四个:linear、fused_moe、attention、moe_strategy_choices(各 factory__init__.py与moe_group_args.py:201),全为内联字面量、无共享常量。测试却用"moe"(:34-37)、"parser"(:64,70,73)、"nobody_registered_here"(:116);真实 MoE 槽位名fused_moe从未在本单测被 drain。register_backend_hook仅在“槽位已启动”时报错(backend_registry.py:62),名字或 context 键写错既不报错也不执行 hook——正是模块 docstring 自述的“wrong numerics rather than a startup failure”。attention 的四个 context 键同样无契约断言。 - [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
reset_backend_registrations 无法恢复入口导入缓存,可能污染同进程后续用例
reset_backend_registrations()只清_hooks/_started/_repeatable(:101-104),但入口导入由import_optional_internal_source_entrypoint的@lru_cache(maxsize=None)缓存(import_util.py:26),且已导入模块留在sys.modules不会二次执行模块体。清理后进程仍认为入口“已加载”,ensure_backend_entrypoint_loaded()直接命中缓存返回 True,而入口在导入期登记的 hook 已丢失且无法再登记。这正是server_args_test.py:18那条用例必须双重 mock 入口加载函数来规避的限制;在存在内源且未 patch 加载函数的环境中,任何依赖“reset 后重放真实入口”的用例都会静默失去--moe_strategy扩展。 - [P.G] 测试规范 — pytest.raises 带 match 参数 → issue
backend_registry 生命周期异常断言未校验错误消息
:66(repeatable 拒绝迟到 hook)、:72(生命周期变更)、:101(drain 后再注册)、:112(hook 异常)均只用assertRaises。生产侧存在两条语义不同的RuntimeError:was already initialised(backend_registry.py:64)与lifecycle changed after it started(:86)。经核验二者当前分属register_backend_hook与run_backend_registrations,故这些用例暂无歧义;但一旦其中一处新增/合并分支,测试将无法证明被测不变量。本 PR 其他新增测试(test_compressed_w8a8_int8_per_channel.py:153、deepep_normal_router_hook_test.py:99)均用assertRaisesRegex,同一 PR 内断言风格不一致。
Strengths
- 枚举与跨语言契约扩展稳妥:
W8A8INT8PTPC追加为 12,ModelOptFP4=10/QuarkMXFP4=11编号未变,pybind、.pyi存根与 C++ 三方同步,get_algo()="w8a8_int8_per_channel"的 python→C++ 回环由QuantAlgoBindingTest(test_compressed_w8a8_int8_per_channel.py:491)钉住。 - 权重加载子类化克制且分派唯一:
supported_quant_config_types使基类只认 FP8、子类只认 W8A8,配合WeightModule.create的“多类命中即报错”约束保证每个 config 恰好一个 loader;INT8 走 byte-exact 路径,test_int8_skips_the_fp8_conversion(:450)以convert_calls==0钉住不经 FP8 设备转换。 - 失败语义普遍 fail-fast 且带上下文:W8A8 部分层 exclude 命中量化模板主动抛错而非静默数值降级;未识别的 compressed-tensors 方案抛具名
ValueError(quant_config.py:317)替代抽象类实例化TypeError;ROCm EP 兜底else: raise ValueError(ep.py:95)把量化 checkpoint 被静默当 bf16 执行改成 fail-fast。 _pick_config_group(quant_config.py:24)保住group_0优先语义,仅在原先根本读不到的单命名组形状上扩展,多 group 时告警而非静默。- CUDA Graph 修复定位准确:host 镜像清零
kv_cache_kernel_block_id.fill_(0)(cuda_graph_runner.cc:395)与设备侧fill_(0)(:379,383)语义对称,注释(:389-394)准确写出“padding 行残留旧 block ID → KV 写入活跃请求 block”的触发链,Block 0 为保留安全目标。 backend_registry.py分层选址有据:模块级仅依赖logging/threading/typing,对import_util采用函数内延迟导入以规避它自身要解决的 eager import 循环;moe_strategy_choices用repeatable=True正确处理同进程多次setup_args()各自新建 parser 的场景;hook 异常不吞。StrategyRegistry.get_strategy将带副作用的get_attributes()由多次调用收敛为一次并注释说明其惰性导入/日志副作用(strategy_registry.py:99-102),语义与原priority等价。- 测试多为行为断言而非白盒:
_RecordingDevice.convert_calls计数验证 FP8 设备转换门控,registered MOE/Linear compute backend报错用assertRaisesRegex具备判别力,BackendAvailabilityGuardTest用setUp/tearDown快照恢复LinearFactory._strategies避免跨用例污染。
| auto accept_tokens = torch::zeros({1, static_cast<int64_t>(propose_step + 1)}, cuda_i32); | ||
| accept_tokens[0][0] = sp_output_buffer->tokens[0][0]; | ||
| propose_tokens_gpu[0] = sp_output_buffer->tokens[0][1]; | ||
| sp_output_buffer->propose_tokens_gpu = propose_tokens_gpu; |
There was a problem hiding this comment.
[P2] propose_tokens_gpu 两个写入方形状与取值语义分叉,混合 batch 改换取值路径
GenerateStream.h:58-61 为 draft_token_gpu 写明 shape:[propose_step],经 GenerateStream.cc:1025 流入 propose_tokens_gpu(:91 无形状契约)。同字段现有三种写入形状:gRPC 交接(本次新增)写 1 维 {1} 且仅含 tokens[0][1](首个 draft,DecodeRpcServer.cc:333,337);P2P 写 2 维 [1,N] 含已提交 token(StreamCacheResource.cc:223);device-state 分支 narrow(1,1,N-1) 并注释“应持有全部 propose_step draft”(:234-236)。消费方统一 lastColumnAsFlat(MtpBatchStreamProcessor.cc:242,250,372):propose_step==1 等价,>1 时 gRPC 取首个、P2P 取末位。`collectLegacyPropos...
建议: 在 GenerateStream.h:91 补上与 draft_token_gpu 一致的形状/内容契约注释(明确是“全部 propose_step 个 draft”还是“单 token 种子”),让 DecodeRpcServer 与 StreamCacheResource 两个 producer 写入同一约定。若确认 decode gRPC 交接按设计只需单 token,请在 DecodeRpcServer.cc:333 注明与 P2P 多 token 语义的差异并对 propose_step>1 加断言;否则按契约写入全部 draft。补一条 propose_step>1 单测,断言 pickOneStepDraftToken 与 collectLegacyProposeSlices 在 gRPC/P2P 两种来源下取到同一 token。
| _repeatable.add(slot) | ||
| for hook in tuple(_hooks.get(slot, ())): | ||
| logger.debug("running backend registration hook for slot %r", slot) | ||
| hook(**context) |
There was a problem hiding this comment.
[P2] backend hook 在全局 RLock 内执行,与 Python import 锁构成锁序反转
run_backend_registrations 在 with _lock:(:81)内直接 hook(**context)(:96),而文档明确鼓励“较重的实现模块放在回调内部导入”(backend_registration.md:88-89,126-127),示例 :95-104 亦在 hook 内 import。四个槽位消费点均位于模块体(import 期):fused_moe/__init__.py:120、linear/__init__.py:31、attention/__init__.py:159。于是可形成相反加锁顺序:线程 A 持 _lock 等模块 M 的 import 锁;线程 B 持 M 的 import 锁后在模块体消费另一槽位并等 _lock。环中一条边是 threading.RLock,CPython 的 import 死锁检测无法识别,表现为启动期无日志挂起。仓内 import_util.py:77-81 正是刻意先出锁再 importlib.import_module。
建议: 锁内只做生命周期判定与 hook 快照,出锁后执行:with _lock: ...; hooks = tuple(_hooks.get(slot, ())),再在 with 块外循环 hook(**context);_started/_repeatable 置位保留在锁内。若需保持“槽位消费前注册已生效”的可见性,可配合 per-slot threading.Event 让并发的第二个调用方等待首次完成,或改为 per-slot 锁打断跨槽位环路。并在文档「失败语义」一节写明 hook 执行期间持有哪些锁。
|
|
||
| # Out-of-tree backends ship MoE strategies the public parser must not | ||
| # advertise, so let them extend the accepted choices here instead. | ||
| run_backend_registrations("moe_strategy_choices", repeatable=True, parser=parser) |
There was a problem hiding this comment.
[P2] moe_strategy_choices 把 argparse 私有内部固化为跨仓契约,且文档与测试给出两种互不兼容写法
run_backend_registrations("moe_strategy_choices", repeatable=True, parser=parser)(:201)只把裸 parser 交给 hook,定位逻辑全部落给外部后端。仓内两处示例都因此遍历私有属性:文档 backend_registration.md:107-115 遍历 parser._actions 后 action.choices = list(...) 重新赋值;新增测试 server_args_test.py:28-32 则原地 .choices.append(...)。两种写法对容器类型假设不同:choices 改为 tuple 时文档写法可用而测试写法抛 AttributeError;--moe_strategy 一旦重命名,next(...) 抛 StopIteration。两种失败都在服务启动路径,且开源侧重构无法通过仓内引用搜索发现下游消费者。文档 :166-169 还承诺保留 parser= 上下文,进一步固化耦合。
建议: 把 context 收窄为可直接操作的对象:在 moe_group_args 或 EnvArgumentParser 上提供具名扩展函数(如 add_moe_strategy_choices(*names),内部封装 action 定位、去重与容器类型统一),以 run_backend_registrations("moe_strategy_choices", repeatable=True, add_choices=...) 传出;文档与测试统一改用该公开入口,不再示范 _actions。若为兼容既有外部 hook 必须保留 parser=,请标注为过渡期契约并给出迁移目标。
Checklist: [6.1] ISP:调用方不依赖无关大接口
| f"tp_size={config.tp_size}, " | ||
| f"use_deepep_low_latency={config.moe_config.use_deepep_low_latency if config.moe_config else False}" | ||
| ) | ||
| if quant_method == "W8A8_INT8_PER_CHANNEL_COMPRESSED": |
There was a problem hiding this comment.
[P2] 设备无关的中心工厂硬编码 W8A8 后端专属文案,且绕过统一的 quant_method 取值工具
StrategyRegistry.get_strategy(:87-93)与 LinearFactory.create(linear/factory.py:117-123)各自硬编码 quant_method == "W8A8_INT8_PER_CHANNEL_COMPRESSED" 抛定制文案,而两者都是全设备共享的中心选择层,该量化格式执行能力由外部后端提供——PR 文档自己要求公共注册机制保持设备/厂商无关(backend_registration.md:171-172)。每新增一种仅外部后端可消费的格式都要回到两处中心加 if(违反 OCP)。根因是通用报错(strategy_registry.py:94-97)未输出 quant_method(linear 侧 :128 已输出 quant_config)。该字面量另散落在 deepep_wrapper.py:235、quant_config.py:949,全仓无共享常量。另 strategy_registry.py:74-78 直接 `quant_config.g...
建议: 删除两处 if 分支,改为在通用 ValueError/logger.error 中带上 quant_method 与已注册策略类名列表;若确需后端定制文案,让后端在注册 hook 时向 registry 登记 quant_method -> 提示语 映射,把知识放回后端侧。把方法名收敛为 CompressedW8A8Int8PerChannelQuantConfig.get_method() 的引用(deepep_wrapper.py 同),并把 strategy_registry.py:74-78 统一为 MoeConfigResolver().get_quant_method(config)。
Checklist: [6.1] OCP:本地扩展点优先于修改中心逻辑
| def test_repeatable_slot_rejects_late_hooks(self): | ||
| run_backend_registrations("parser", repeatable=True, parser="first") | ||
|
|
||
| with self.assertRaises(RuntimeError): |
There was a problem hiding this comment.
[P3] backend_registry 生命周期异常断言未校验错误消息
:66(repeatable 拒绝迟到 hook)、:72(生命周期变更)、:101(drain 后再注册)、:112(hook 异常)均只用 assertRaises。生产侧存在两条语义不同的 RuntimeError:was already initialised(backend_registry.py:64)与 lifecycle changed after it started(:86)。经核验二者当前分属 register_backend_hook 与 run_backend_registrations,故这些用例暂无歧义;但一旦其中一处新增/合并分支,测试将无法证明被测不变量。本 PR 其他新增测试(test_compressed_w8a8_int8_per_channel.py:153、deepep_normal_router_hook_test.py:99)均用 assertRaisesRegex,同一 PR 内断言风格不一致。
建议: 四处改为 assertRaisesRegex,分别带上 "already initialised"、"lifecycle changed"、"backend is broken" 等匹配串,与本 PR 其他测试保持一致,并使未来分支演化不会让用例悄悄换成另一条路径通过。
Checklist: [P.G] pytest.raises 带 match 参数
| self._conditions_pass( | ||
| CudaNoQuantCppStrategy, | ||
| create_model_config_without_quant(), | ||
| "no_auant_cpp", |
There was a problem hiding this comment.
[P3] no_auant 拼写错误的策略取值已出现第三处副本,且测试绕过生产决策入口
TestCudaNoQuantFallbackStrategies 硬编码 "no_auant_cpp"(:179,188)与 "no_auant_dp_normal"(:197,205)。该拼写错误(应为 no_quant_*)已存在于 moe_group_args.py:167-169 的 argparse choices 与 no_quant.py:60,88 的比较中,本测试成为第三处副本,日后修正 CLI 取值需同步三处。另外 _conditions_pass(:162-172)直接调用 strategy.check_conditions 而非生产入口 can_handle,若 MoeStrategy.can_handle 不再调用 check_conditions,测试仍通过(同文件 ROCm 用例用的是 can_handle,两者不一致)。
建议: 将策略名收敛为模块级常量,供 argparse choices、策略比较与测试共同引用(顺带在一处修正拼写,并按需保留旧取值作为兼容别名);_conditions_pass 补注释说明“为规避 get_attributes() 的 deep_ep/deepgemm 惰性导入才绕过 can_handle”,避免后续读者误以为覆盖了生产决策入口。
Checklist: [6.1] DRY:重复非平凡逻辑被抽取或显式复用;[I] 同一功能用统一工具函数
| @@ -32,3 +32,7 @@ def is_cuda() -> bool: | |||
|
|
|||
There was a problem hiding this comment.
📍 实际位置 rtp_llm/device/device_type.py:20(不在 diff 展示范围内,就近挂载)
[P3] PPU 设备识别依赖环境变量真值判断且无任何日志,CUDA 主机可能被静默重分类
get_device_type() 在 torch.cuda.is_available() 分支内以 os.environ.get("PPU_HOME") or "ppu" in getattr(torch,"__version__","").lower()(:20-24)判定 DeviceType.Ppu,全程无日志。一旦 CUDA 主机存在任意非空 PPU_HOME(例如残留环境或并装工具链),is_cuda() 即翻为 False,连带改变 fused_moe/__init__.py 的 FP4 策略注册、arch.py 的 SM 判定与 get_num_device_sms(),而启动日志中没有任何可据以定位的线索。该文件未被任何分片认领,由集成侧补验。
建议: 在设备类型解析处补一条 info 日志,写明判定结果与依据(PPU_HOME 命中或 torch 版本串命中),使误判可从启动日志一眼看出;并考虑把 PPU_HOME 的真值判断收紧为路径存在性校验,避免空目录或残留变量触发重分类。请作者确认该判定分支的引入范围与既有 CUDA 部署的兼容性。
Checklist: [6.1] 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全;[6.1] 可观测性:日志/指标/超时可操作、非噪声
| get_device_type, | ||
| is_cuda, | ||
| is_hip, | ||
| is_ppu, |
There was a problem hiding this comment.
[P3] arch.py 新增的 is_ppu 自身未使用,仅作为隐式再导出通道
arch.py:6-12 新增导入 is_ppu,但全文件仅第 11 行出现该符号,无任何使用;其唯一消费者是 qwen3_next.py:238-248,它从 rtp_llm.models_py.utils.arch 而非源头 rtp_llm.device.device_type 导入 get_device_type/is_cuda/is_hip/is_ppu。该文件既无 __all__ 也非包 __init__,因此这是一个未声明的再导出通道:linter 会将其报为未使用导入,任何“清理未使用导入”的改动都会直接打断 qwen3_next.py。is_hip/get_device_type 已存在同样情况,属既有约定。
建议: 让 qwen3_next.py 直接从 rtp_llm.device.device_type 导入这四个符号,并移除 arch.py 中未使用的导入;若确实要把 arch 作为设备判定门面,请显式声明 __all__ 并在模块 docstring 写明其再导出职责,使该约定可被工具与读者识别。
Checklist: [6.1] 分层边界:新概念在正确层级,不泄漏内部;[6.1] KISS/YAGNI:无投机性抽象
131f35d to
ab5ca04
Compare
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1323
Status: LGTM
Summary: P0/0 · P1/0 · P2/19 · P3/14
Reviewed: commit ab5ca04e147f · 2026-08-20 23:47 UTC+8
lgtm ready to ci
Non-blocking Suggestions
P2
- propose_tokens_gpu 两个生产者的形状与取值语义分叉 @
rtp_llm/cpp/model_rpc/DecodeRpcServer.cc:338- 建议:统一形状契约:建议与
StreamCacheResource.cc:236对齐,按tokens.narrow(1, 1, size-1)发布全部剩余 draft(也使accept_tokens的propose_step + 1列语义自洽);若确认该路径只支持单步提案,请就地断言propose_step == 1,并在SpeculativeExecutorStreamOutput::propose_tokens_gpu字段声明处写明「仅首个 draft token」。同时在MtpBatchStreamProcessorTest.cc补一条propose_step >= 2且 gRPC 与 P2P 握手流混于同一 batch 的用例。
- 建议:统一形状契约:建议与
- W8A8 部分 exclude 的 fail-fast 守卫对 MoE {expert_id} 模板完全失效 @
rtp_llm/model_loader/compressed_w8a8_int8_per_channel_weight.py:38- 建议:在
_exclude_pattern_for中把{expert_id}(及其它在用占位符)一并替换为\d+,使 MoE 模板可被匹配;或在CompressedW8A8Int8PerChannelWeight.support中对含未知占位符的模板显式拒绝并说明「该模板无法校验 exclude,暂不支持」,避免守卫在 MoE 路径上静默失效。同时修正类 docstring 中「exclude handling 与 FP8 路径一致」的表述以标注该盲区,并补一条「MoE split 格式 + 单 expert ignore」的用例。
- 建议:在
- 逐层枚举式的完整 exclude 被误拒,且 support() 由纯谓词变为可抛异常 @
rtp_llm/model_loader/compressed_w8a8_int8_per_channel_weight.py:46- 建议:让
support()恢复为纯 bool:把 partial-exclude 校验移到__init__(此时已确定选中本 loader,上下文更准确),或在WeightModule.create选出target_cls后增加显式validate()步骤,把「不适用」与「配置非法」两种语义分开。校验请下移到能拿到num_layers的阶段,命中条目数等于层数时按完整 exclude 走非量化回退;若坚持启动期 fail-fast,请在错误信息中列出命中的具体条目并指向model.layers.{i}.xxx模板写法(test_exact_template_ignore_can_use_unquantized_fallback已覆盖该形式)。第 8 行跨模块导入了私有函数_ckpt_base_matches_quant_exclude,若校验留在 loader 层,建议提升为公开 helper 或基类 protected classmethod。
- 建议:让
- QuantMethod→字符串映射在两处 switch 重复维护且各自漏枚举值,worker status 持续打 ERROR 并对外上报 UNKNOWN @
rtp_llm/cpp/model_rpc/LocalRpcServer.cc:439- 建议:在
QuantInfo.h/.cc提供唯一的quantMethodToString(QuantMethod),两处统一调用(LocalRpcServer侧None → "FP16"的特例单独 case 保留),新增枚举值只需在一处登记;顺带补齐当前落入default的ModelOptFP4与QuarkMXFP4,并考虑去掉default:分支让-Wswitch在下次新增枚举时强制补齐,ERROR 日志也才恢复可操作性。
- 建议:在
- backend hook 在持有全局 RLock 时执行,与 Python import 锁构成锁序反转 @
rtp_llm/utils/backend_registry.py:96- 建议:分离「状态变更」与「hook 执行」:锁内完成
_started/_repeatable判定并tuple(_hooks.get(slot, ()))快照,释放锁后再执行 hook;若需保证后到线程看到注册完成,用 per-slotthreading.Event(首个线程执行完set(),后到线程不持锁wait())而非长期持锁。同时补一个多线程用例(两线程同时 drain 同一槽位、hook 内做 import),并在 docstring 与文档「失败语义」一节补一条约束:hook 内不得 import 会反向 import 槽位消费者的模块。
- 建议:分离「状态变更」与「hook 执行」:锁内完成
- 一次性槽位先标记 started 再执行 hook,失败或 owner 重建后静默退化为部分注册 @
rtp_llm/utils/backend_registry.py:91- 建议:把 started 标记推迟到 hook 全部成功之后,或在异常路径上回滚
_started/_repeatable(try/except 后重新 raise),使失败的槽位下次仍会重试并保持「全部注册或整体失败」的原子语义;若确实希望「失败即终态」,请显式记录失败并让后续调用重新抛出同一异常,而不是静默 no-op。补两条用例:hook 抛异常后再次 drain 的行为,以及多 hook 场景下部分成功时的最终状态。
- 建议:把 started 标记推迟到 hook 全部成功之后,或在异常路径上回滚
- 后端 hook 执行结果缺少默认级别可观测性 @
rtp_llm/utils/backend_registry.py:95- 建议:槽位在启动期最多执行数次,INFO 级日志不构成噪声。建议在
run_backend_registrations()中当 hook 数量大于 0 时打一条 INFO,记录 slot 名与本次执行的 hook 数量(可含getattr(hook, "__qualname__", repr(hook)));并把ensure_backend_entrypoint_loaded()的返回值在首次调用时以 INFO 记录一次「可选后端入口已加载 / 未找到」,使运维仅凭默认日志即可判定外部后端是否参与了实现选择。
- 建议:槽位在启动期最多执行数次,INFO 级日志不构成噪声。建议在
- moe_strategy_choices 把 argparse 私有结构固化为跨仓契约,且文档与测试给出两种写法 @
rtp_llm/server/server_args/moe_group_args.py:201- 建议:把上下文收窄为最小接口:在
init_moe_group_args中保存add_argument返回的 action,改为下发回调(如run_backend_registrations("moe_strategy_choices", repeatable=True, add_choices=...)),或在EnvArgumentParser上提供稳定方法extend_choices("--moe_strategy", [...]),并同步更新文档示例与测试,使三方共用同一份契约。这样--moe_strategy的名称与查找方式重新变成公共侧内部细节,还能在 in-tree 统一做重名校验。
- 建议:把上下文收窄为最小接口:在
- attention / fused_moe 的注册 hook 未被设备导入失败保护,与 linear 槽位行为不一致 @
rtp_llm/models_py/modules/factory/attention/__init__.py:159- 建议:参照
linear/__init__.py,把 attention / fused_moe 的设备分支导入包进try/except并记录 warning,使run_backend_registrations在设备实现导入失败时仍能执行;attention 侧还应把else分支按DeviceType细分,避免非 CUDA 设备强依赖 flashinfer,并补一个与linear/factory.py:117同风格的「无可用实现」显式报错,让缺后端时的失败文案指向设备支持而不是 import 栈。三个槽位的失败语义应在文档「失败语义」一节统一声明。
- 建议:参照
- ROCm EP 量化 allow-list 与 executor 映射构成双份真值,收窄后 QuarkMXFP4 改为启动失败且被删字符串仍被消费 @
rtp_llm/models_py/modules/factory/fused_moe/impl/rocm/strategy/ep.py:25- 建议:消除双份真值:提取
_QUANT_METHOD_TO_EXECUTOR映射表,can_handle()直接判成员关系,并删除不可达的else raise;_get_quant_method()返回类型由Any收紧为Optional[str]。对三个被删字符串二选一:确认为死字符串则同步清理上述 5 处引用并在 commit message 说明,否则保留在表中,避免同一权重格式在 EP 与 pure-TP/Linear 得出不同结论。行为收窄请在 PR description 给出受影响 ROCm 配置矩阵与回退方式(例如临时降级 pure TP 的步骤),并在可执行 ROCm 的 target 上补一条选择用例。
- 建议:消除双份真值:提取
- 设备无关的中心工厂硬编码 W8A8 后端专属文案(两处复制),且绕过统一的 quant_method 取值工具 @
rtp_llm/models_py/modules/factory/fused_moe/strategy_registry.py:87- 建议:本 PR 已引入
backend_registry扩展点,建议用同一思路替代中心 if:让后端在注册 hook 时登记「可消费的量化方法 → 诊断提示」映射,中心逻辑只做查表;退一步也可把提示收敛为共享数据表_UNSERVED_QUANT_HINTS,由linear/factory.py与strategy_registry.py共用。量化方法名请提升为 Enum/Literal 或模块级常量,由quant_config单点定义、其余三处引用。文案保留并行度信息(quant_method=<resolved>+ 「若已安装后端请检查 ep_size / moe_strategy」),并统一通过MoeConfigResolver().get_quant_method(config)取值。
- 建议:本 PR 已引入
- _prepare_dispatch_input 元组返回的下游契约未表达,topk 重映射仍由 use_fp8 隐式决定 @
rtp_llm/models_py/modules/factory/fused_moe/impl/cuda/routers/deepep_normal_router.py:167- 建议:把 :167 的判据从
use_fp8/use_fp4改为「本次 dispatch 是否产出了 scale」(例如expert_x_scale is None),使量化与非量化路径的 topk 处理由同一事实驱动;若确实希望 tuple 路径沿用 BF16 式重映射,请在_prepare_dispatch_input的 docstring 中把该约定与 scale 布局并列写清,并在deepep_normal_router_hook_test.py补一条payload.expert_topk_ids断言把契约钉住。
- 建议:把 :167 的判据从
- compressed-tensors targets 校验按组名不对称,同内容 checkpoint 结果取决于组名 @
rtp_llm/config/quant_config.py:55- 建议:把「是否校验 targets」从「组名是不是 group_0」解耦为「该 scheme 是否要求全模型 Linear 作用域」:
_pick_config_group只负责选组不做校验,由各 scheme 分支自行决定——W8A8 保留 :273 的调用,FP8/W4A8 等需维持历史宽松语义的分支显式不调用并注释原因。这样 :55 的重复调用可以删除,接受条件也不再隐含依赖组名。
- 建议:把「是否校验 targets」从「组名是不是 group_0」解耦为「该 scheme 是否要求全模型 Linear 作用域」:
- Mega-PR:MTP/PD handoff 与 CUDA Graph 修复混入 W8A8 量化与后端注册特性 @
rtp_llm/cpp/engine_base/stream/GenerateStream.cc:1034- 建议:将 MTP/PD handoff 与 CUDA Graph 清零(
GenerateStream.cc/.h、DecodeRpcServer.cc、GenerateStreamTest.cc、cuda_graph_runner.cc)拆为独立 PR 先行合入,本 PR 只保留 W8A8 量化与 backend 注册两条主线;若因发布节奏无法拆分,至少保证 commit 原子、message 分别说明动机,并在 PR description 中分节交代三条主线及各自的回滚边界。
- 建议:将 MTP/PD handoff 与 CUDA Graph 清零(
- 新增 py_test 缺设备标签却 eager import 整个 factory 包,且工厂守卫用例错位、仅覆盖空注册表 @
rtp_llm/model_loader/test/BUILD:40- 建议:优先拆分:把
BackendAvailabilityGuardTest移到linear/test/与fused_moe/tests/下已有设备标签的目标(改动这两个 factory 的人才会跑到),让test_compressed_w8a8_int8_per_channel保持仅依赖quant_config+model_loader的纯 CPU 测试;若必须同文件,则补上与 W4A8 兄弟目标一致的tags=["H20"]与exec_properties。守卫覆盖请至少补一条基于真实注册表状态的用例(真实 registry 下传入 W8A8 config 断言文案),避免只验证「空表必报错」这一自明结论。
- 建议:优先拆分:把
- dtype 模板化改造靠源码文本计数兜底,漏掉两处位置传参调用点 @
rtp_llm/model_loader/test/test_compressed_w8a8_int8_per_channel.py:484- 建议:删除源码文本扫描(它测文本而非行为,且在 zip / 仅 pyc 部署下
inspect.getsource不可用),改为对w8a8_weight_list中各 key 参数化(subTest)构造WeightModule.create,至少补上linear_attn_qkvz_w、linear_attn_out_w、attn_qkv_w、ffn_w13、moe_w1、moe_w2,断言weight.kernel.data_type is torch.int8且weight.scale.data_type is torch.float32;同一模板再用 FP8 config 断言float8_e4m3fn。这样既覆盖位置传参调用点,也不依赖源码可读性。
- 建议:删除源码文本扫描(它测文本而非行为,且在 zip / 仅 pyc 部署下
- ROCm EP 量化过滤用例在其实际运行的 CUDA 机型上恒真,且唯一覆盖落在 open_skip target @
rtp_llm/models_py/modules/factory/fused_moe/tests/test_cuda_strategies.py:214- 建议:让 False 只可能来自量化过滤:
patch.object(MoeStrategy, "can_handle", return_value=True)后再断言仍为 False,或用 spy 断言get_attributes未被调用;并补一条正向用例(FP8_PER_CHANNEL_COMPRESSED或quant_config=None不被拦截),否则白名单写漏一项会让 ROCm FP8 部署静默失配。ROCm 语义建议迁到带rocmtag 的 target(同仓test_inline_fp8_quant有先例),并与作者确认能否把该量化准入回归移出open_skip以获得 CI 保护。
- 建议:让 False 只可能来自量化过滤:
- 测试用 reset_backend_registrations 清空进程级注册表,污染不可逆且 mock 掉其声称覆盖的入口边界 @
rtp_llm/server/server_args/test/server_args_test.py:37- 建议:改为快照/恢复而非清空:在
backend_registry暴露仅供测试的 context manager(进入时保存三个容器副本、退出时还原),测试以self.addCleanup使用;backend_registry_test.py的setUp/tearDown(:13、:22)宜一并改造。同时补两条直接断言:hook 执行后--moe_strategyaction 的 choices 确实包含新值,以及未注册的策略名会被SystemExit拒绝,使「外部后端扩展生效」不依赖间接推断。
- 建议:改为快照/恢复而非清空:在
- 槽位名与上下文键为自由字符串 + kwargs 透传,测试使用生产不存在的槽位名 @
rtp_llm/utils/test/backend_registry_test.py:34- 建议:把四个槽位名收敛为模块级常量或
Enum/Literal(由backend_registry导出),register_backend_hook/run_backend_registrations只接受该类型并对未知槽位显式报错;上下文建议改为按槽位定义的TypedDict或具名参数,避免**context把键名错误推迟到 hook 内部。测试改为引用同一批常量,这样任一侧改名都会被静态检查或用例立即拦住。
- 建议:把四个槽位名收敛为模块级常量或
P3
- backend_registry 生命周期异常断言未校验消息,且 noop 用例无任何断言 @
rtp_llm/utils/test/backend_registry_test.py:66- 建议:统一改用
assertRaisesRegex并锚定关键片段::72 用"lifecycle changed",:66/:101 用"already initialised",:112 用"backend is broken",使不同错误路径互不掩盖;:115 显式断言无 hook 被执行(如调用计数列表仍为空),让「noop」语义可验证。
- 建议:统一改用
- GenerateStream 新用例只覆盖「缺失则保留」,未覆盖「提供则刷新」与 commit-only 陈旧提案边界 @
rtp_llm/cpp/engine_base/stream/test/GenerateStreamTest.cc:117- 建议:补两个断言方向:(1) 传入已定义的
draft_token_gpu,断言propose_tokens_gpu被刷新为新值而非停留旧值;(2) 在MtpBatchStreamProcessorTest.cc补一条 commit-only 步之后的 gather 用例,明确断言陈旧 GPU 提案不会被当作本轮提案消费,或断言其回落到 CPUtokens路径。另建议去掉 :124 的.to(torch::kCUDA)——被验证语义与设备无关,同文件同类用例用的是 CPU 张量,改回 CPU 可使断言在非 CUDA 后端复用。
- 建议:补两个断言方向:(1) 传入已定义的
- CUDA Graph host block table 每步全量清零落在 decode 热路径且新不变量无覆盖 @
rtp_llm/cpp/cuda_graph/cuda_graph_runner.cc:397- 建议:把清零范围收窄为 padding 区间(
narrow(0, current_batch_size, graph_bs - current_batch_size).fill_(0)),保留同样的安全性而避免对有效行做无用写入;若确认fillParams也会改写有效行或全量 memset 成本可忽略,请在注释中说明原因或实测量级。另建议补一条 CUDA Graph 层面的用例:先以大 batch replay 写入 block ID,再以小 batch replay,断言 padding 行的 host mirror 为 0。
- 建议:把清零范围收窄为 padding 区间(
- 非 FP8 分支移除了 dispatch 返回值的类型校验,错误定位后移 @
rtp_llm/models_py/modules/factory/fused_moe/impl/cuda/routers/deepep_normal_router.py:161- 建议:在 else 分支恢复一条
isinstance(output, torch.Tensor)校验,或改为与 FP8 分支对称的raise ValueError,把错误定位保持在 dispatch 边界上。
- 建议:在 else 分支恢复一条
- LoadQuantPerChannelFp8Weight 绕过了新引入的 supported_quant_config_types 单一来源 @
rtp_llm/model_loader/per_channel_fp8_quant_weight.py:707- 建议:让该子类也声明
supported_quant_config_types = (Fp8PerChannelCompressedQuantConfig,)并改为读取该属性做 isinstance 判定(is_quanted()取反的差异单独保留),使「哪个 loader 认哪些 config」在整条继承链上只有一处定义。
- 建议:让该子类也声明
- setup_args 中的入口加载与槽位内部保证重复,且返回值被丢弃 @
rtp_llm/server/server_args/server_args.py:534- 建议:二选一收敛加载责任:删除该显式调用完全依赖槽位内部加载,或让
run_backend_registrations不再自行加载、由调用方统一加载一次。若保留是为了让入口 import 异常在建 parser 之前尽早暴露,请把这层 fail-fast 意图写进注释(当前注释描述的顺序已由槽位保证),并记录一次加载结果。
- 建议:二选一收敛加载责任:删除该显式调用完全依赖槽位内部加载,或让
- W8A8 分支混用强下标与 get,input_activations 缺键时抛裸 KeyError @
rtp_llm/config/quant_config.py:268- 建议:把该条件内的三个强下标统一改为
.get(...)(与同分支其余字段一致),让任何不匹配形态都落到 :317 的具名 ValueError;并补一条「input_activations 为 dict 但缺 type/num_bits/strategy」的用例,断言得到具名错误而非 KeyError。
- 建议:把该条件内的三个强下标统一改为
- choices 扩展点只保护 argparse 校验通道,env 补齐通道静默绕过 @
rtp_llm/server/server_args/server_args.py:366- 建议:在 env 补齐路径中,当
action.choices非空时校验转换结果并在不合法时走self.error(...),使 CLI 与 env 两条通道共享同一准入集合;except (ValueError, TypeError): pass至少应记录一条 warning 说明哪个环境变量被忽略。至少应在文档「扩展槽位」一节说明该槽位只影响 CLI 校验通道,避免外部后端误以为 env 亦被覆盖。
- 建议:在 env 补齐路径中,当
- no_auant 拼写错误的策略取值出现第三处副本 @
rtp_llm/models_py/modules/factory/fused_moe/tests/test_cuda_strategies.py:179- 建议:将策略名收敛为共享常量或
Enum/Literal(例如放在fused_moe/defs),由 parser choices、策略条件判断与测试共同引用,后续纠正拼写可在一处收敛并配合别名兼容。最低成本过渡措施:加一行注释指明「no_auant_*是生产侧既有拼写,非本测试笔误」;本用例本意是隔离 quant 校验,也可直接传"auto"规避对具体名称的耦合。
- 建议:将策略名收敛为共享常量或
- PPU 设备识别依赖环境变量真值且无任何日志,CUDA 主机可能被静默重分类 @
rtp_llm/device/device_type.py:20- 建议:把判定收紧为显式解析(例如要求
PPU_HOME指向存在的目录、复用str2bool,或引入显式覆盖变量并对非法值报错),并在首次解析出非默认设备类型时打一条 INFO 记录判定依据(env 命中还是 torch 版本命中),使混装环境中的误分类在启动日志中即可发现。
- 建议:把判定收紧为显式解析(例如要求
- arch.py 新增的 is_ppu 自身未使用,仅作为隐式再导出通道 @
rtp_llm/models_py/utils/arch.py:11- 建议:让
qwen3_next.py直接从rtp_llm.device.device_type导入这四个函数,并删除arch.py中未使用的is_ppu导入;若确实希望arch作为统一门面,请显式声明__all__并在注释中说明这是有意的再导出,使意图可检查而不是靠未使用的 import 承载。
- 建议:让
- server 库目标未声明新增的 //rtp_llm:utils 依赖,仅测试目标补了 @
rtp_llm/server/server_args/moe_group_args.py:2- 建议:在
//rtp_llm/server:server的 deps 中显式加入//rtp_llm:utils,与测试目标保持一致,使依赖声明匹配实际 import;顺带确认是否存在utils反向依赖server的环,若有则应把backend_registry的依赖边界再收窄一层。
- 建议:在
- 路由钩子测试用 object.new 绕过构造函数并依赖魔法位置下标 @
rtp_llm/models_py/modules/factory/fused_moe/impl/cuda/routers/test/deepep_normal_router_hook_test.py:55- 建议:改为经真实
__init__构造(必要时对deepep_buffer_wrapper等外部依赖做窄粒度 patch 或参数注入),使字段契约受保护;若确因外部依赖过重无法构造,请在_new_router上加一行注释说明绕过原因。fake buffer 的dispatch改为按关键字接收(或至少为args[5]/args[6]注明对应参数名),并补一条对payload.expert_topk_ids的断言以覆盖 topk 重映射契约。
- 建议:改为经真实
- 三个 no-quant 策略重复同一量化判定,其中两处与 executor 完全冗余且新增用例绕过 can_handle() @
rtp_llm/models_py/modules/factory/fused_moe/impl/cuda/strategy/no_quant.py:58- 建议:把三处重复判定抽为共享 mixin 或基类方法,并复用
resolver.has_quantization(config)(与 executor 同一工具函数)作为单一来源;若确认 :58/:86 与 executor 侧完全冗余,删除这两处、仅保留 executor 约束更清晰。测试改为经can_handle(config)断言候选筛选结果,从而覆盖 strategy/router/executor 条件组合后的真实选择行为。
- 建议:把三处重复判定抽为共享 mixin 或基类方法,并复用
Checklist Findings (26 fail / 55 total)
General Principles Checklist
- [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue
server 库目标未声明新增的 //rtp_llm:utils 依赖,仅测试目标补了
moe_group_args.py:2与server_args.py新增了from rtp_llm.utils.backend_registry import ...,两者都被//rtp_llm/server:server的glob(["*.py", "server_args/*.py"])收录(server/BUILD:8-22),但该目标的 deps 仅有:request_headers、//rtp_llm:warmup、//rtp_llm:vipserver与一个 proto 目标;//rtp_llm:warmup只含utils/warmup.py且无 deps,无法提供该模块。本 PR 只给测试目标server_args/test/BUILD:8补了//rtp_llm:utils,库侧仍依赖传递闭包成立。 - [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
PPU 设备识别依赖环境变量真值且无任何日志,CUDA 主机可能被静默重分类
get_device_type()以os.environ.get("PPU_HOME")的真值(任意非空值,包括"0")或 torch 版本串含ppu判定为DeviceType.Ppu(:20-24),且全程无日志。判定逻辑本身是既有代码,但本 PR 新增的is_ppu()(:37-38)把它接入qwen3_next.py:248的模型 allowlist,放大了误分类的影响面:一台 CUDA 主机若残留PPU_HOME即被静默重分类,is_cuda()转为 False,arch.py的is_sm90/is_blackwell全部退化,attention/linear/fused_moe 落入else分支、FP4 策略不再注册,而运维在默认日志中看不到任何线索。 - [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue
arch.py 新增的 is_ppu 自身未使用,仅作为隐式再导出通道
arch.py:6-12新增从device_type导入is_ppu,但全文件没有任何使用点(is_sm90/is_blackwell等均只调用is_cuda())。它的唯一作用是让qwen3_next.py:238-243能从models_py.utils.arch一次性导入get_device_type, is_cuda, is_hip, is_ppu——即把arch(架构能力判定层)当成device_type(设备识别层)的隐式再导出通道。__all__未声明,静态检查也无法区分「有意再导出」与「未使用导入」。 - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
PPU 设备识别依赖环境变量真值且无任何日志,CUDA 主机可能被静默重分类
get_device_type()以os.environ.get("PPU_HOME")的真值(任意非空值,包括"0")或 torch 版本串含ppu判定为DeviceType.Ppu(:20-24),且全程无日志。判定逻辑本身是既有代码,但本 PR 新增的is_ppu()(:37-38)把它接入qwen3_next.py:248的模型 allowlist,放大了误分类的影响面:一台 CUDA 主机若残留PPU_HOME即被静默重分类,is_cuda()转为 False,arch.py的is_sm90/is_blackwell全部退化,attention/linear/fused_moe 落入else分支、FP4 策略不再注册,而运维在默认日志中看不到任何线索。 - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
ROCm EP 量化 allow-list 与 executor 映射构成双份真值,收窄后 QuarkMXFP4 改为启动失败且被删字符串仍被消费
_SUPPORTED_QUANT_METHODS(:25-31)与_resolve_executor_and_quant()的 if/elif 链(:64-97)编码同一份 allow-list 需人工同步,且因can_handle已前置过滤,:95 的raise ValueError在正常路径不可达。方向认可(原else把任意方法映射到 BF16 executor,属静默错数值),但QuarkMXFP4是真实方法(quant_config.py:651)且 ROCm 在 gfx950 上注册了RocmMXFp4PureTPStrategy(fused_moe/init.py:64-65),故 gfx950 + MXFP4 + EP 由「静默选错 executor」变为启动即No suitable MOE strategy found。被删的FP8_PER_BLOCK_QUARK/FP4_PER_GROUP*(diff:2456/2476)仍被rocm_moe.py:289,404-405、`pure_tp - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
测试用 reset_backend_registrations 清空进程级注册表,污染不可逆且 mock 掉其声称覆盖的入口边界
用例在 :37(patch 生效之前)与finally(:64)各调用一次reset_backend_registrations(),该函数清空进程级_hooks/_started/_repeatable(backend_registry.py:99-104)。而真实 hook 由入口模块 import 副作用注册,import_optional_internal_source_entrypoint带@lru_cache(maxsize=None)(import_util.py:26)且内部走importlib.import_module(:38),两层缓存都保证注册不会重放,被清掉的 hook 在本进程内永久丢失;按 unittest 字典序本类先于ServerArgsSetTest执行,之后十余处setup_args()都会以空 hook 集把槽位标记 started。该用例还把server_args与backend_registry两处ensure_backend_entrypoint_loaded全部 - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
choices 扩展点只保护 argparse 校验通道,env 补齐通道静默绕过
EnvArgumentParser的 env 补齐路径(:342-371)在 CLI 未显式提供该参数时用action.type(env_value)转换后直接setattr,全程不校验action.choices;:366-368 对ValueError/TypeError还直接pass静默跳过。因此MOE_STRATEGY=<任意字符串>可绕过--moe_strategy的 choices 白名单(含本 PR 新增的扩展点),最终以未知策略名进入 MoE 选择逻辑并表现为「无候选」。该行为是 env 通道既有实现,但本 PR 的扩展点使「choices 即准入集合」的预期更强,故值得记录。 - [6.1] Quality — Commit 原子、message 与行为匹配 → issue
Mega-PR:MTP/PD handoff 与 CUDA Graph 修复混入 W8A8 量化与后端注册特性
本 PR 横跨 6 个风险类,至少包含三条互不依赖的主线:(1) compressed-tensors W8A8 INT8 识别与加载;(2) out-of-tree backend 注册机制(backend_registry.py+ 四槽位 + 文档);(3) MTP/PD 分离 handoff 的 device-state 修复(GenerateStream.cc:1034-1036保留最后有效 GPU 镜像、DecodeRpcServer.cc:333-338发布、GenerateStreamTest.cc用例)与cuda_graph_runner.cc:397的 host mirror 清零。第三条与前两条无任何调用或数据依赖(位于 speculative handoff 段落,与量化配置链毫无交集),却与量化枚举改动共处同一文件集,回滚时无法只撤销其中一条。 - [6.1] Quality — Mega-PR 已拆分为独立变更 → issue
Mega-PR:MTP/PD handoff 与 CUDA Graph 修复混入 W8A8 量化与后端注册特性
本 PR 横跨 6 个风险类,至少包含三条互不依赖的主线:(1) compressed-tensors W8A8 INT8 识别与加载;(2) out-of-tree backend 注册机制(backend_registry.py+ 四槽位 + 文档);(3) MTP/PD 分离 handoff 的 device-state 修复(GenerateStream.cc:1034-1036保留最后有效 GPU 镜像、DecodeRpcServer.cc:333-338发布、GenerateStreamTest.cc用例)与cuda_graph_runner.cc:397的 host mirror 清零。第三条与前两条无任何调用或数据依赖(位于 speculative handoff 段落,与量化配置链毫无交集),却与量化枚举改动共处同一文件集,回滚时无法只撤销其中一条。 - [6.1] Quality — PR description 说明动机与设计 → issue
Mega-PR:MTP/PD handoff 与 CUDA Graph 修复混入 W8A8 量化与后端注册特性
本 PR 横跨 6 个风险类,至少包含三条互不依赖的主线:(1) compressed-tensors W8A8 INT8 识别与加载;(2) out-of-tree backend 注册机制(backend_registry.py+ 四槽位 + 文档);(3) MTP/PD 分离 handoff 的 device-state 修复(GenerateStream.cc:1034-1036保留最后有效 GPU 镜像、DecodeRpcServer.cc:333-338发布、GenerateStreamTest.cc用例)与cuda_graph_runner.cc:397的 host mirror 清零。第三条与前两条无任何调用或数据依赖(位于 speculative handoff 段落,与量化配置链毫无交集),却与量化枚举改动共处同一文件集,回滚时无法只撤销其中一条。 - [6.1] Software Engineering — DIP:高层策略不依赖非必要具体细节 → issue
moe_strategy_choices 把 argparse 私有结构固化为跨仓契约,且文档与测试给出两种写法
该槽位以parser=parser把整个EnvArgumentParser交给外源 hook,而 hook 真正需要的只是--moe_strategy的 choices。于是 hook 必须遍历私有属性parser._actions并读写action.option_strings/action.choices:backend_registration.md:107-115用list(action.choices or ())+ 判重 + 重新赋值action.choices,server_args_test.py:28-32却用next(...).choices.append(...)(无默认值、无判重),两份唯一参考实现已不一致。参数改名或 choices 改为 tuple 会让前者静默失效、后者抛 StopIteration/AttributeError;因异常有意不吞,故障表现为启动期一个与 MoE 无关的晦涩异常。该上下文还允许 hook 增删任意无关 action。 - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
三个 no-quant 策略重复同一量化判定,其中两处与 executor 完全冗余且新增用例绕过 can_handle()
no_quant.py的 :29、:58、:86 三处各自MoeConfigResolver()+get_quant_method+check(quant_method is None)。其中 :58/:86 两个策略的 executor 均为TritonFusedMoeExecutor,其check_conditions已断言not resolver.has_quantization(config)(triton_fused_executor.py:36-38),两条件逻辑等价;而MoeStrategy.can_handle()(strategy_base.py:71-77)必然调用 executor 检查,故这两处新增行为无实际变化(:29 因 executor 接受[None, "FP8_PER_BLOCK"]而确有价值)。新增 4 个用例直接调用strategy.check_conditions(...)(test_cuda_strategies.py:170-172)绕过can_handle(),仅能证 - [6.1] Software Engineering — ISP:调用方不依赖无关大接口 → issue
moe_strategy_choices 把 argparse 私有结构固化为跨仓契约,且文档与测试给出两种写法
该槽位以parser=parser把整个EnvArgumentParser交给外源 hook,而 hook 真正需要的只是--moe_strategy的 choices。于是 hook 必须遍历私有属性parser._actions并读写action.option_strings/action.choices:backend_registration.md:107-115用list(action.choices or ())+ 判重 + 重新赋值action.choices,server_args_test.py:28-32却用next(...).choices.append(...)(无默认值、无判重),两份唯一参考实现已不一致。参数改名或 choices 改为 tuple 会让前者静默失效、后者抛 StopIteration/AttributeError;因异常有意不吞,故障表现为启动期一个与 MoE 无关的晦涩异常。该上下文还允许 hook 增删任意无关 action。 - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
arch.py 新增的 is_ppu 自身未使用,仅作为隐式再导出通道
arch.py:6-12新增从device_type导入is_ppu,但全文件没有任何使用点(is_sm90/is_blackwell等均只调用is_cuda())。它的唯一作用是让qwen3_next.py:238-243能从models_py.utils.arch一次性导入get_device_type, is_cuda, is_hip, is_ppu——即把arch(架构能力判定层)当成device_type(设备识别层)的隐式再导出通道。__all__未声明,静态检查也无法区分「有意再导出」与「未使用导入」。 - [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue
_prepare_dispatch_input 元组返回的下游契约未表达,topk 重映射仍由 use_fp8 隐式决定
改动前use_fp8=False且 dispatch 返回 tuple 会被assert isinstance(output, torch.Tensor)拦死;现在 :151 改为isinstance(output, tuple),该组合成为合法路径。此时 :154 正确跳过expert_x_scale[:, 0]归约,但 :167 的(not use_fp8) and (not use_fp4)仍然成立,expert_topk_ids会走torch.where(recv_topk_idx == -1, ...) + rank_expert_offset重映射——这是 BF16 无量化路径的语义。即新的量化 tuple 路径同时拿到「scale 直通」与「BF16 式 topk 重映射」,判据是use_fp8而非「dispatch 是否携带 scale」。_prepare_dispatch_input的 docstring(:235-241)只约定 scale 布局,未提 topk 分支;hook 测试也只断言 `exp - [6.1] Software Engineering — OCP:本地扩展点优先于修改中心逻辑 → issue
设备无关的中心工厂硬编码 W8A8 后端专属文案(两处复制),且绕过统一的 quant_method 取值工具
get_strategy()在无候选时新增if quant_method == "W8A8_INT8_PER_CHANNEL_COMPRESSED"的专用文案(:87-93),同样的硬编码分支在linear/factory.py:117-123又重复一次。该方法名在仓内无任何策略消费(仅quant_config.py:949产出),完全为外部后端服务,并被deepep_wrapper.py:235第四次以裸字面量比较,四处互相锁死、无共享常量。该分支只按量化方法判定:W8A8 权重叠加错误ep_size/moe_strategy导致无候选时,用户拿到的是「去安装后端」的提示,比原通用提示更具误导性。此外 :74-78 自行get_method()并判空,而仓库统一访问器是MoeConfigResolver.get_quant_method(config)(config_resolver.py:52-64)。 - [6.1] Software Engineering — SRP:模块/类职责单一 → issue
新增 py_test 缺设备标签却 eager import 整个 factory 包,且工厂守卫用例错位、仅覆盖空注册表
新 target(BUILD:40-44)未声明tags/exec_properties,而同类兄弟test_compressed_w4a8_int4_per_channel(:24-32)声明了tags=["H20"]+exec_properties。该测试 :22/:25 导入factory.fused_moe.strategy_registry与factory.linear.factory,会先执行父包factory/__init__.py:6-8(attention/fused_moe/linear 全量导入),其中 attention 三个分支都无保护地 import flashinfer 系列;该导入还会执行三个run_backend_registrations,把槽位加入_started。而BackendAvailabilityGuardTest(:130-179)断言的是两个 factory 的守卫文案,与权重加载职责无关,还直接改写LinearFactory._strategies私有属性,: - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
ROCm EP 量化过滤用例在其实际运行的 CUDA 机型上恒真,且唯一覆盖落在 open_skip target
TestRocmEpStrategyQuantFiltering断言RocmEpNormalStrategy().can_handle(config)为 False(:227),其 target 配置为exec_properties={'gpu':'H20'}(CUDA 机型)。若移除新增白名单(ep.py:44-46),can_handle会走super().can_handle→get_attributes()→ 导入impl/rocm/executors/rocm_moe.py(其 :4 即模块级import aiter),在 H20 上抛 ModuleNotFoundError,被strategy_base.py:58-64的except ImportError捕获后同样return False:两条路径结果相同,删除生产守卫用例仍绿。该 target 还带tags=["open_skip"],意味着这是本次量化过滤修复的唯一定向覆盖却不在开源 CI 中执行。 - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
路由钩子测试用 object.__new__ 绕过构造函数并依赖魔法位置下标
_new_router用object.__new__(router_class)跳过DeepepNormalRouterBase.__init__并手工赋 7 个字段(:55-72),因此__init__新增或重命名必需字段不会被本测试发现,反而会以指向测试自身的 AttributeError 变红;_TupleDispatchBuffer.dispatch又以args[5]/args[6]读取 topk_ids/topk_weights(:26-27),与生产端buffer.dispatch的位置参数顺序(deepep_normal_router.py:137-147)形成无注释的隐式耦合,调整参数顺序时该 fake 会静默取错值。 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
GenerateStream 新用例只覆盖「缺失则保留」,未覆盖「提供则刷新」与 commit-only 陈旧提案边界
用例仅构造draft_token_gpu未定义的情形,断言propose_tokens_gpu仍为旧值 9(:135-136)。两处缺口:一是defined()为真时应刷新旧值的分支(GenerateStream.cc:1034-1036)无用例,而这是 PDFUSION 每步都走的主路径;二是本用例的draft_token=-1正是 commit-only 步,GenerateStream.cc:1023会propose_token_.clear(),而消费方MtpBatchStreamProcessor.cc:249-252只判断defined()便返回该张量,即「无下一步提案却保留了上一步 GPU 提案」这一新引入的陈旧读风险没有任何测试固定其安全边界。
RTP-LLM Checklist
- [I] 代码质量 — 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理 → issue
ROCm EP 量化 allow-list 与 executor 映射构成双份真值,收窄后 QuarkMXFP4 改为启动失败且被删字符串仍被消费
_SUPPORTED_QUANT_METHODS(:25-31)与_resolve_executor_and_quant()的 if/elif 链(:64-97)编码同一份 allow-list 需人工同步,且因can_handle已前置过滤,:95 的raise ValueError在正常路径不可达。方向认可(原else把任意方法映射到 BF16 executor,属静默错数值),但QuarkMXFP4是真实方法(quant_config.py:651)且 ROCm 在 gfx950 上注册了RocmMXFp4PureTPStrategy(fused_moe/init.py:64-65),故 gfx950 + MXFP4 + EP 由「静默选错 executor」变为启动即No suitable MOE strategy found。被删的FP8_PER_BLOCK_QUARK/FP4_PER_GROUP*(diff:2456/2476)仍被rocm_moe.py:289,404-405、`pure_tp - [I] 代码质量 — 同一功能用统一工具函数 → issue
no_auant 拼写错误的策略取值出现第三处副本
新用例在 :179、:188、:197、:206 直接写入"no_auant_cpp"/"no_auant_dp_normal"。该拼写(应为no_quant)目前同时存在于moe_group_args.py:167-169的--moe_strategychoices 与no_quant.py:60/88的条件判断中,因此测试当前是对的;但测试以裸字符串再复制一遍且无任何说明,使三处独立字面量互相锁死——将来纠正拼写时这些用例会莫名失败,读者也难以判断哪边才是笔误。
Python Static-First Checklist
- [P.A] 静态结构与类型纪律 — 字符串分发用 Enum/Literal → issue
no_auant 拼写错误的策略取值出现第三处副本
新用例在 :179、:188、:197、:206 直接写入"no_auant_cpp"/"no_auant_dp_normal"。该拼写(应为no_quant)目前同时存在于moe_group_args.py:167-169的--moe_strategychoices 与no_quant.py:60/88的条件判断中,因此测试当前是对的;但测试以裸字符串再复制一遍且无任何说明,使三处独立字面量互相锁死——将来纠正拼写时这些用例会莫名失败,读者也难以判断哪边才是笔误。 - [P.F] 语言陷阱 — 禁止模块级 import 副作用 → issue
新增 py_test 缺设备标签却 eager import 整个 factory 包,且工厂守卫用例错位、仅覆盖空注册表
新 target(BUILD:40-44)未声明tags/exec_properties,而同类兄弟test_compressed_w4a8_int4_per_channel(:24-32)声明了tags=["H20"]+exec_properties。该测试 :22/:25 导入factory.fused_moe.strategy_registry与factory.linear.factory,会先执行父包factory/__init__.py:6-8(attention/fused_moe/linear 全量导入),其中 attention 三个分支都无保护地 import flashinfer 系列;该导入还会执行三个run_backend_registrations,把槽位加入_started。而BackendAvailabilityGuardTest(:130-179)断言的是两个 factory 的守卫文案,与权重加载职责无关,还直接改写LinearFactory._strategies私有属性,: - [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
三个 no-quant 策略重复同一量化判定,其中两处与 executor 完全冗余且新增用例绕过 can_handle()
no_quant.py的 :29、:58、:86 三处各自MoeConfigResolver()+get_quant_method+check(quant_method is None)。其中 :58/:86 两个策略的 executor 均为TritonFusedMoeExecutor,其check_conditions已断言not resolver.has_quantization(config)(triton_fused_executor.py:36-38),两条件逻辑等价;而MoeStrategy.can_handle()(strategy_base.py:71-77)必然调用 executor 检查,故这两处新增行为无实际变化(:29 因 executor 接受[None, "FP8_PER_BLOCK"]而确有价值)。新增 4 个用例直接调用strategy.check_conditions(...)(test_cuda_strategies.py:170-172)绕过can_handle(),仅能证 - [P.G] 测试规范 — pytest.raises 带 match 参数 → issue
backend_registry 生命周期异常断言未校验消息,且 noop 用例无任何断言
backend_registry.py有两处语义不同的 RuntimeError:register_backend_hook抛「slot ... was already initialised」(:63-66),run_backend_registrations抛「slot ... lifecycle changed after it started」(:85-87)。测试 4 处assertRaises均不带消息校验(:66、:72、:101、:112),其中 :72 本意校验 lifecycle 冲突,但只要任何 RuntimeError 被抛出即通过;若实现退化成从 register 路径报错,该用例仍绿。:115test_draining_slot_without_hooks_is_noop无任何断言。同 PR 的另两个新测试文件全程使用assertRaisesRegex,断言精度不一致。
Strengths
- 复用而非复制:
PerChannelFp8Weight抽出weight_dtype/apply_fp8_device_conversion/supported_quant_config_types三个类属性后(per_channel_fp8_quant_weight.py:266-289),INT8 子类仅 3 行属性即完成接入(compressed_w8a8_int8_per_channel_weight.py:22-24),未复制整套加载器。 apply_fp8_device_conversion=False有真实必要性而非投机抽象:ROCm 的convert_fp8_weight_params开头即断言 FP8 dtype,若不关闭 INT8 权重会在加载期断言失败;CUDA 侧该函数是恒等实现,故 CUDA 数值行为零变化,并由_RecordingDevice的调用计数从行为侧钉住(test:450-482)。- 跨语言枚举链路完整闭环且以追加方式扩展:
QuantInfo.h:19的 value=12 未重排既有 0–11,ConfigInit.cc:1150-1187与.pyi:1385/1432同批更新,QuantAlgoBindingTest(test:494-504)用真实 pybind 对象钉住往返;.pyi更新还顺手补回__members__中此前遗漏的QuarkMXFP4(diff:2905-2906),修掉一处既有存根漂移。 - 配置解析兼容性把控到位:
_pick_config_group明确保留group_0优先并在多组时告警(quant_config.py:32-44),把新增命名组回退严格限制在原先完全读不到的场景,既有 checkpoint 加载路径零变化;未识别组合从「抽象类实例化 TypeError」改为附带 weights/input_activations 原文的具名 ValueError(:317)。 CompressedW8A8Int8PerChannelQuantConfig全具名参数、allowed_keys白名单拒绝未知键(:972-983)、显式拒绝re:模式(:929-936),无**kwargs兜底,明显优于同文件其他吞参的量化配置类;get_supported_kv_cache_dtypes主动收窄为 fp16/bf16 并注明「未验证组合宁可启动失败」(:962-968),把--fp8_kv_cache误配拦在启动期。_ckpt_base_matches_quant_exclude的新增 Note(per_channel_fp8_quant_weight.py:83-92)如实记录「按 template 而非 layer 实例匹配会在部分排除时产生静默数值错误」这一既有缺陷,并说明为何只在新 W8A8 路径 fail-fast,是对行为边界的诚实交代。- 注册机制的分层选址正确且动机可追溯:
backend_registry.py放在rtp_llm.utils而非 factory 旁,避免参数解析阶段被迫导入models_py.modules.factory触发全部 factory 与通信库 eager import(docstring:22-31 写明动机),文件内确实不含任何设备或厂商名。 deepep_normal_router.py:151把分支判据从use_fp8标志改为isinstance(output, tuple),与 DeepEP「输入 tuple 则输出 tuple」的实际契约一致;157-160 把assert换成ValueError,避免python -O下 FP8 缺 scale 被带到 executor,并有assertRaisesRegex定向覆盖。CudaNoQuantEpLowLatencyStrategy新增的quant_method is None检查是真实修复:其 executorDeepGemmMaskedExecutor.check_conditions接受[None, "FP8_PER_BLOCK"](deepgemm_masked_executor.py:56),此前 FP8_PER_BLOCK 配置可能命中 no-quant 策略而拿到quant_dtype=None。RocmEpNormalStrategy.can_handle()用 allow-list 前置过滤替代原else: RocmExpertsBf16 + quant_dtype=None(diff:2485-2492 确认),把量化专家权重被 BF16 executor 消费的「静默错数值」前移为启动失败,方向正确。cuda_graph_runner.cc:390-398的 host mirror 清零补齐了与 tagged 分支zero_()(:473-475)对称的缺口,注释交代了触发条件与「block 0 为保留块」前提,且位于stridedCopyHost回填(:525)之前,顺序正确。GenerateStreamTest.cc:117-137是有效回归测试:draft_token=-1走 commit-only 分支、accept_token_num=1跳过 block swap,可真实抵达GenerateStream.cc:1034的新守卫;改动前propose_tokens_gpu会被赋成 undefined 而使ASSERT_TRUE失败。DecodeRpcServer.cc:333-338先做torch::Tensor拷贝赋值、:359 才std::move,引用计数顺序正确,sp_output_buffer->propose_tokens_gpu不会因移动而失效。docs/backend/backend_registration.md与实现逐条对得上(入口加载时机、四槽位表、上下文关键字、失败语义、测试清单),并显式声明该机制必须保持设备与厂商无关。
| auto accept_tokens = torch::zeros({1, static_cast<int64_t>(propose_step + 1)}, cuda_i32); | ||
| accept_tokens[0][0] = sp_output_buffer->tokens[0][0]; | ||
| propose_tokens_gpu[0] = sp_output_buffer->tokens[0][1]; | ||
| sp_output_buffer->propose_tokens_gpu = propose_tokens_gpu; |
There was a problem hiding this comment.
[P2] propose_tokens_gpu 两个生产者的形状与取值语义分叉
本行把 torch::empty({1})、仅含 tokens[0][1](首个 draft)的张量写入 sp_output_buffer->propose_tokens_gpu。同一字段的另一生产者 StreamCacheResource.cc:223 写入完整 {1,N},其 :234-236 注释明确「MTP verify path expects propose_tokens_gpu to hold all propose_step draft tokens」。共同消费者 MtpBatchStreamProcessor.cc:250/372 统一施加 lastColumnAsFlat()(:147-150 取最后一列):对 {1} 得首个 draft,对 {1,N} 得第 N-1 个;CPU 回落 columnAsFlat(tokens, 1)(:252)取的是首个。propose_step==1 时三者等价,默认配置无回归;propose_step>=2 时同一 batch 内两类 handoff 流取值来源不同。
建议: 统一形状契约:建议与 StreamCacheResource.cc:236 对齐,按 tokens.narrow(1, 1, size-1) 发布全部剩余 draft(也使 accept_tokens 的 propose_step + 1 列语义自洽);若确认该路径只支持单步提案,请就地断言 propose_step == 1,并在 SpeculativeExecutorStreamOutput::propose_tokens_gpu 字段声明处写明「仅首个 draft token」。同时在 MtpBatchStreamProcessorTest.cc 补一条 propose_step >= 2 且 gRPC 与 P2P 握手流混于同一 batch 的用例。
| or src_weight_info.name not in cls.w8a8_weight_list | ||
| ): | ||
| return False | ||
| for ckpt_w in src_weight_info.weights: |
There was a problem hiding this comment.
[P2] W8A8 部分 exclude 的 fail-fast 守卫对 MoE {expert_id} 模板完全失效
新守卫依赖 _ckpt_base_matches_quant_exclude,而 _exclude_pattern_for(per_channel_fp8_quant_weight.py:61-64)只把 {i} 替换为 \d+,{expert_id} 被 re.escape 成字面量。目标模型的 MoE split 模板正是 layers.{i}.mlp.experts.{expert_id}.down_proj.weight(qwen3_next_weight.py:409/422/429),support() 看到的仍是未展开的占位符,生成的正则永远无法匹配 checkpoint 中形如 model.layers.3.mlp.experts.5.down_proj 的 ignore 项。而 W.moe_w1/moe_w2 都在 w8a8_weight_list(:276-277)中,于是既不抛 ValueError 也不返回 False:被排除 expert 的 bf16 张量会与其它 expert 一起以 `data_typ...
建议: 在 _exclude_pattern_for 中把 {expert_id}(及其它在用占位符)一并替换为 \d+,使 MoE 模板可被匹配;或在 CompressedW8A8Int8PerChannelWeight.support 中对含未知占位符的模板显式拒绝并说明「该模板无法校验 exclude,暂不支持」,避免守卫在 MoE 路径上静默失效。同时修正类 docstring 中「exclude handling 与 FP8 路径一致」的表述以标注该盲区,并补一条「MoE split 格式 + 单 expert ignore」的用例。
| base_name, quant_config.exclude_modules | ||
| ) | ||
| ): | ||
| raise ValueError( |
There was a problem hiding this comment.
[P2] 逐层枚举式的完整 exclude 被误拒,且 support() 由纯谓词变为可抛异常
两个问题同源。一,support() 只看到单个权重模板、入参无 LoadConfig,拿不到 num_layers,无法区分「只排除第 7 层」与「逐层列出全部 N 层」;而 __init__ 又明确拒绝 re: 模式(quant_config.py:929-936),用户没有正则退路,一份把某模块在所有层都排除的合法 checkpoint 会启动即失败,文案 "per-layer fallback is not supported" 未给出规避方式。二,weight_module.py:73-77 把 support() 当纯谓词用于列表推导筛选 valid_classes,model_loader 下其余 loader(含同文件 LoadQuantPerChannelFp8Weight.support:703-712)一律只返回 bool,本处 raise 会中断整张注册表扫描。
建议: 让 support() 恢复为纯 bool:把 partial-exclude 校验移到 __init__(此时已确定选中本 loader,上下文更准确),或在 WeightModule.create 选出 target_cls 后增加显式 validate() 步骤,把「不适用」与「配置非法」两种语义分开。校验请下移到能拿到 num_layers 的阶段,命中条目数等于层数时按完整 exclude 走非量化回退;若坚持启动期 fail-fast,请在错误信息中列出命中的具体条目并指向 model.layers.{i}.xxx 模板写法(test_exact_template_ignore_can_use_unquantized_fallback 已覆盖该形式)。第 8 行跨模块导入了私有函数 _ckpt_base_matches_quant_exclude,若校验留在 loader 层,建议提升为公开 helper 或基类 protected classmethod。
| break; | ||
| case QuantMethod::None: | ||
| status_info.precision = "FP16"; | ||
| break; |
There was a problem hiding this comment.
📍 实际位置 rtp_llm/cpp/model_rpc/LocalRpcServer.cc:439(不在 diff 展示范围内,就近挂载)
[P2] QuantMethod→字符串映射在两处 switch 重复维护且各自漏枚举值,worker status 持续打 ERROR 并对外上报 UNKNOWN
同一份映射手工维护在 ModelConfig.cc:53 quantMethodToString 与 LocalRpcServer.cc:405 getWorkerStatusInfo 两处,本 PR 又各补一条 W8A8INT8PTPC。对照 QuantInfo.h:6-20,漂移已发生:ModelConfig.cc 缺 QuarkMXFP4(11),LocalRpcServer.cc 同时缺 ModelOptFP4(10) 与 QuarkMXFP4(11);两处都有 default:,-Wswitch 不会告警。而 QuarkMXFP4 是真实可产出的方法(quant_config.py:651)。后果是以这些格式部署时,每次 worker status 查询都走 :439 default 分支,打 RTP_LLM_LOG_ERROR("unknown quant method") 并把对外的 status_info.precision 置为 "UNKNOWN"。
建议: 在 QuantInfo.h/.cc 提供唯一的 quantMethodToString(QuantMethod),两处统一调用(LocalRpcServer 侧 None → "FP16" 的特例单独 case 保留),新增枚举值只需在一处登记;顺带补齐当前落入 default 的 ModelOptFP4 与 QuarkMXFP4,并考虑去掉 default: 分支让 -Wswitch 在下次新增枚举时强制补齐,ERROR 日志也才恢复可操作性。
| _repeatable.add(slot) | ||
| for hook in tuple(_hooks.get(slot, ())): | ||
| logger.debug("running backend registration hook for slot %r", slot) | ||
| hook(**context) |
There was a problem hiding this comment.
[P2] backend hook 在持有全局 RLock 时执行,与 Python import 锁构成锁序反转
run_backend_registrations() 在 with _lock:(:81)块内直接 hook(**context)(:96),而文档与 docstring 都要求 hook 内部做惰性 import(backend_registration.md:95-104 的接入示例即 from my_backend.moe import ...)。三个 Factory 槽位的消费点都位于模块体内(fused_moe/init.py:120、attention/init.py:159、linear/init.py:31),调用线程此时已持有该模块的 import lock。若线程 A 持 _lock 执行 hook 并 import 模块 M,线程 B 正在 import M 且其 import 又调用 run_backend_registrations(),两者互等即死锁。文档 141 行自述该锁用于「避免并发导入时重复消费槽位」,说明并发导入在预期内,但 11 条单测无任何并发场景。
建议: 分离「状态变更」与「hook 执行」:锁内完成 _started/_repeatable 判定并 tuple(_hooks.get(slot, ())) 快照,释放锁后再执行 hook;若需保证后到线程看到注册完成,用 per-slot threading.Event(首个线程执行完 set(),后到线程不持锁 wait())而非长期持锁。同时补一个多线程用例(两线程同时 drain 同一槽位、hook 内做 import),并在 docstring 与文档「失败语义」一节补一条约束:hook 内不得 import 会反向 import 槽位消费者的模块。
| @@ -32,3 +32,7 @@ def is_cuda() -> bool: | |||
|
|
|||
There was a problem hiding this comment.
📍 实际位置 rtp_llm/device/device_type.py:20(不在 diff 展示范围内,就近挂载)
[P3] PPU 设备识别依赖环境变量真值且无任何日志,CUDA 主机可能被静默重分类
get_device_type() 以 os.environ.get("PPU_HOME") 的真值(任意非空值,包括 "0")或 torch 版本串含 ppu 判定为 DeviceType.Ppu(:20-24),且全程无日志。判定逻辑本身是既有代码,但本 PR 新增的 is_ppu()(:37-38)把它接入 qwen3_next.py:248 的模型 allowlist,放大了误分类的影响面:一台 CUDA 主机若残留 PPU_HOME 即被静默重分类,is_cuda() 转为 False,arch.py 的 is_sm90/is_blackwell 全部退化,attention/linear/fused_moe 落入 else 分支、FP4 策略不再注册,而运维在默认日志中看不到任何线索。
建议: 把判定收紧为显式解析(例如要求 PPU_HOME 指向存在的目录、复用 str2bool,或引入显式覆盖变量并对非法值报错),并在首次解析出非默认设备类型时打一条 INFO 记录判定依据(env 命中还是 torch 版本命中),使混装环境中的误分类在启动日志中即可发现。
Checklist: [6.1] 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全;[6.1] 可观测性:日志/指标/超时可操作、非噪声
| get_device_type, | ||
| is_cuda, | ||
| is_hip, | ||
| is_ppu, |
There was a problem hiding this comment.
[P3] arch.py 新增的 is_ppu 自身未使用,仅作为隐式再导出通道
arch.py:6-12 新增从 device_type 导入 is_ppu,但全文件没有任何使用点(is_sm90/is_blackwell 等均只调用 is_cuda())。它的唯一作用是让 qwen3_next.py:238-243 能从 models_py.utils.arch 一次性导入 get_device_type, is_cuda, is_hip, is_ppu——即把 arch(架构能力判定层)当成 device_type(设备识别层)的隐式再导出通道。__all__ 未声明,静态检查也无法区分「有意再导出」与「未使用导入」。
建议: 让 qwen3_next.py 直接从 rtp_llm.device.device_type 导入这四个函数,并删除 arch.py 中未使用的 is_ppu 导入;若确实希望 arch 作为统一门面,请显式声明 __all__ 并在注释中说明这是有意的再导出,使意图可检查而不是靠未使用的 import 承载。
Checklist: [6.1] 分层边界:新概念在正确层级,不泄漏内部;[6.1] KISS/YAGNI:无投机性抽象
| @@ -1,4 +1,5 @@ | |||
| from rtp_llm.server.server_args.util import str2bool | |||
| from rtp_llm.utils.backend_registry import run_backend_registrations | |||
There was a problem hiding this comment.
[P3] server 库目标未声明新增的 //rtp_llm:utils 依赖,仅测试目标补了
moe_group_args.py:2 与 server_args.py 新增了 from rtp_llm.utils.backend_registry import ...,两者都被 //rtp_llm/server:server 的 glob(["*.py", "server_args/*.py"]) 收录(server/BUILD:8-22),但该目标的 deps 仅有 :request_headers、//rtp_llm:warmup、//rtp_llm:vipserver 与一个 proto 目标;//rtp_llm:warmup 只含 utils/warmup.py 且无 deps,无法提供该模块。本 PR 只给测试目标 server_args/test/BUILD:8 补了 //rtp_llm:utils,库侧仍依赖传递闭包成立。
建议: 在 //rtp_llm/server:server 的 deps 中显式加入 //rtp_llm:utils,与测试目标保持一致,使依赖声明匹配实际 import;顺带确认是否存在 utils 反向依赖 server 的环,若有则应把 backend_registry 的依赖边界再收窄一层。
Checklist: [6.1] 依赖方向:无循环依赖/跨层惊喜
| class DeepepNormalRouterHookTest(TestCase): | ||
| @staticmethod | ||
| def _new_router(router_class, quant_dtype): | ||
| router = object.__new__(router_class) |
There was a problem hiding this comment.
[P3] 路由钩子测试用 object.new 绕过构造函数并依赖魔法位置下标
_new_router 用 object.__new__(router_class) 跳过 DeepepNormalRouterBase.__init__ 并手工赋 7 个字段(:55-72),因此 __init__ 新增或重命名必需字段不会被本测试发现,反而会以指向测试自身的 AttributeError 变红;_TupleDispatchBuffer.dispatch 又以 args[5]/args[6] 读取 topk_ids/topk_weights(:26-27),与生产端 buffer.dispatch 的位置参数顺序(deepep_normal_router.py:137-147)形成无注释的隐式耦合,调整参数顺序时该 fake 会静默取错值。
建议: 改为经真实 __init__ 构造(必要时对 deepep_buffer_wrapper 等外部依赖做窄粒度 patch 或参数注入),使字段契约受保护;若确因外部依赖过重无法构造,请在 _new_router 上加一行注释说明绕过原因。fake buffer 的 dispatch 改为按关键字接收(或至少为 args[5]/args[6] 注明对应参数名),并补一条对 payload.expert_topk_ids 的断言以覆盖 topk 重映射契约。
Checklist: [6.1] 新逻辑有聚焦单测 + 相关集成/smoke 测试
| def check_conditions(cls, checker: Any, config: MoEConfigAdapter) -> None: | ||
| resolver = MoeConfigResolver() | ||
| quant_method = resolver.get_quant_method(config) | ||
| checker.check(quant_method is None) |
There was a problem hiding this comment.
[P3] 三个 no-quant 策略重复同一量化判定,其中两处与 executor 完全冗余且新增用例绕过 can_handle()
no_quant.py 的 :29、:58、:86 三处各自 MoeConfigResolver() + get_quant_method + check(quant_method is None)。其中 :58/:86 两个策略的 executor 均为 TritonFusedMoeExecutor,其 check_conditions 已断言 not resolver.has_quantization(config)(triton_fused_executor.py:36-38),两条件逻辑等价;而 MoeStrategy.can_handle()(strategy_base.py:71-77)必然调用 executor 检查,故这两处新增行为无实际变化(:29 因 executor 接受 [None, "FP8_PER_BLOCK"] 而确有价值)。新增 4 个用例直接调用 strategy.check_conditions(...)(test_cuda_strategies.py:170-172)绕过 can_handle(),...
建议: 把三处重复判定抽为共享 mixin 或基类方法,并复用 resolver.has_quantization(config)(与 executor 同一工具函数)作为单一来源;若确认 :58/:86 与 executor 侧完全冗余,删除这两处、仅保留 executor 约束更清晰。测试改为经 can_handle(config) 断言候选筛选结果,从而覆盖 strategy/router/executor 条件组合后的真实选择行为。
Checklist: [6.1] DRY:重复非平凡逻辑被抽取或显式复用;[P.G] mock/fake/stub 不得替代本次声称覆盖的生产边界
ab5ca04 to
51a75af
Compare
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1323
Status: LGTM
Summary: P0/0 · P1/0 · P2/21 · P3/14
Reviewed: commit 51a75afea42b · 2026-08-21 01:07 UTC+8
lgtm ready to ci
Non-blocking Suggestions
P2
- propose_tokens_gpu 两个生产者的形状与取值语义分叉 @
rtp_llm/cpp/model_rpc/DecodeRpcServer.cc:338- 建议:统一该字段契约:建议 gRPC 侧改为与 P2P 一致的
sp_output_buffer->tokens.narrow(1, 1, propose_tokens.size() - 1).to(cuda_i32, /*non_blocking=*/true)({1, propose_step}、全部 draft),并在GenerateStream.h:96处用注释固化「shape{batch, propose_step}、最后一列为最新 draft」。若确定只支持单步语义,请在此处补RTP_LLM_CHECK_WITH_INFO(propose_step == 1, ...)让多步配置显式失败,而不是依赖select(-1)恰好吸收形状差异。另补一条propose_step >= 2的用例,断言两条通道经pickOneStepDraftToken得到同一 token。
- 建议:统一该字段契约:建议 gRPC 侧改为与 P2P 一致的
- W8A8 部分 exclude 的 fail-fast 守卫对 MoE {expert_id} 模板完全失效 @
rtp_llm/model_loader/compressed_w8a8_int8_per_channel_weight.py:38- 建议:让
_exclude_pattern_for通用化:把模板中所有{...}形式的占位符统一替换为\d+(或显式支持{expert_id}/{i_1}),并为moe_w1/moe_w2补一条「ignore 命中单个 expert 时行为明确」的用例(raise 或整模板回退,二选一并写进注释)。若不打算支持 MoE 逐专家排除,请在support()处显式判定模板是否含非{i}占位符并直接报错,避免守卫给出虚假保障。
- 建议:让
- 逐层枚举式的完整 exclude 被误拒,且 support() 由纯谓词变为可抛异常 @
rtp_llm/model_loader/compressed_w8a8_int8_per_channel_weight.py:46- 建议:按具体层实例而非模板判定:用
_exclude_pattern_for(base_name)收集命中的层集合,与load_config.num_layers覆盖的层集合比较,全覆盖时return False走未量化回退,仅部分覆盖时 raise,并把实际命中与缺失的层号写进错误信息。若暂不实现,请把文案改为陈述限制本身(如「该权重模板存在具体层排除项,W8A8 路径不支持逐层回退」)。同时把校验从support移到 loader 构造或独立的validate_quant_config阶段以保持谓词纯净;若坚持保留,请在基类文档中声明「support允许抛出配置级致命错误」并说明 FP8/W4A8 未对齐的原因。补一条「全层具名枚举」的边界用例。
- 建议:按具体层实例而非模板判定:用
- QuantMethod→字符串映射在两处 switch 重复维护且各自漏枚举值,worker status 持续打 ERROR 并对外上报 UNKNOWN @
rtp_llm/cpp/model_rpc/LocalRpcServer.cc:433- 建议:把映射收敛到一处:将
quantMethodToString提升到rtp_llm/cpp/model_utils/QuantInfo.h/.cc(与 enum 同处)并导出,LocalRpcServer::getWorkerStatusInfo直接复用,仅把QuantMethod::None → "FP16"这一处业务差异留在调用点覆盖;同时补齐ModelOptFP4/QuarkMXFP4分支以消除虚假 ERROR 日志与UNKNOWN上报。此后新增枚举值只需改一个 switch。
- 建议:把映射收敛到一处:将
- backend hook 在持有全局 RLock 时执行,与 Python import 锁构成锁序反转 @
rtp_llm/utils/backend_registry.py:96- 建议:把「取快照」与「执行 hook」分离:锁内完成
_started/_repeatable判定并复制出待执行的 hook 元组,释放锁后再逐个调用hook(**context);一次性语义仍由_started保证。若确需串行化 hook 执行,改用槽位级独立锁而非覆盖全部槽位的进程级锁,把锁粒度与 import 锁解耦;backend_registration.md:141-142关于「进程级锁保护」的表述需同步更新。
- 建议:把「取快照」与「执行 hook」分离:锁内完成
- 一次性槽位先标记 started 再执行 hook,失败后静默退化为部分注册且无默认级别日志 @
rtp_llm/utils/backend_registry.py:91- 建议:失败时恢复槽位状态,或引入
_failed集合:某槽位执行失败后,后续对该槽位的run_backend_registrations()直接重新抛出原因异常而不是按「已完成」静默返回;抛出前用logger.error记录槽位名与 hook 标识,并把成功执行的 hook 数量与槽位名提升到logger.info,使「外部后端是否注册成功」在默认日志级别可观测。相应在backend_registry_test.py补一条「hook 失败后再次消费该槽位仍然报错」的用例。
- 建议:失败时恢复槽位状态,或引入
- moe_strategy_choices 把 argparse 私有结构固化为跨仓契约,且文档与测试给出两种写法 @
rtp_llm/server/server_args/moe_group_args.py:201- 建议:把上下文收窄为契约明确的对象:保留
moe_group.add_argument("--moe_strategy", ...)的返回值,改为run_backend_registrations("moe_strategy_choices", repeatable=True, moe_strategy_action=action),或下发add_choice: Callable[[str], None]闭包并在其中断言choices为 list、新名未重复。文档与测试 hook 统一到同一写法,不再示范parser._actions遍历;若为兼容既有 hook 必须保留parser=,请把新入口标为推荐、parser=标为待废弃。
- 建议:把上下文收窄为契约明确的对象:保留
- attention / fused_moe 的注册 hook 未被设备导入失败保护,与 linear 槽位行为不一致 @
rtp_llm/models_py/modules/factory/attention/__init__.py:159- 建议:为
DeviceType.Ppu增加显式分支(哪怕只是「不注册任何 in-tree impl,交由钩子填充」),或把else分支的 CUDA impl 导入收窄为仅DeviceType.Cuda,让未知设备走「空列表 + 钩子填充」;至少与linear槽位对齐,用try/except包住设备导入并记录 warning,保证钩子一定被执行。补一条「未知设备下 attention / fused_moe 钩子仍会执行」的断言,并在backend_registration.md的槽位表中写明该保证。
- 建议:为
- ROCm EP 量化 allow-list 与 executor 映射构成双份真值,收窄后 QuarkMXFP4 改为启动失败且被删字符串仍被消费 @
rtp_llm/models_py/modules/factory/fused_moe/impl/rocm/strategy/ep.py:25- 建议:把量化方法到
(executor_class, FusedMoEQuantConfig)的映射收敛为单个类级 dict,can_handle()判定quant_method in _QUANT_TABLE,_resolve_executor_and_quant()直接查表,消除双份枚举;同一 PR 内清理上述 5 处残留声明(R.I.2 要求删除内部 registry entry 时全仓搜索消费者),并补一条「受支持量化方法在can_handle中不被过滤」的正向用例。QuarkMXFP4的启动失败变化请在 PR description 记录,并说明运维旁路(是否需显式--moe_strategy或关闭 EP)。
- 建议:把量化方法到
- 设备无关的中心工厂硬编码 W8A8 后端专属文案(两处复制),且量化方法串在三处字面量消费 @
rtp_llm/models_py/modules/factory/fused_moe/strategy_registry.py:87- 建议:删除两处专用分支,把已解析出的
quant_method直接写进通用异常文案(如「no registered MOE/Linear backend can consume quant_method=; ep_size=..., tp_size=...」),对任何量化方法都给出同等可诊断信息且无需再改中心逻辑;quant_method解析改为复用MoeConfigResolver().get_quant_method(config)。同时在QuantizationConfig上提供能力查询(如is_per_act_token()/ 返回 Enum 的activation_granularity)由各子类声明,deepep_wrapper改为询问能力而非比较字面量;get_method()的返回值建议改为 Enum 或Literal以获得静态检查。
- 建议:删除两处专用分支,把已解析出的
- _prepare_dispatch_input 元组返回的下游契约未表达,scale 布局与 topk 重映射仍由 use_fp8 隐式决定 @
rtp_llm/models_py/modules/factory/fused_moe/impl/cuda/routers/deepep_normal_router.py:167- 建议:把「dispatch 是否返回量化数据」从
use_fp8抽出为独立判据(例如依据expert_x_scale is None,或在基类增设dispatch_is_quantized属性 / 让 hook 返回显式的 dispatch 描述对象),:154 与 :167 改用该判据;在 docstring 中写明覆写方对expert_topk_ids的-1语义要求(由 executor 处理还是需重映射);对 tuple 返回补len(output) == 2校验;并在 hook 测试中用含-1的recv_topk_idx与非零rank_expert_offset显式断言payload.expert_topk_ids。
- 建议:把「dispatch 是否返回量化数据」从
- compressed-tensors targets 校验按组名不对称,同内容 checkpoint 结果取决于组名 @
rtp_llm/config/quant_config.py:55- 建议:将「多 group」与「targets 校验」两个判定从组名解耦:在
_pick_config_group返回前对选中的 group 无条件调用_validate_compressed_targets(可去掉 :273 的重复调用),并让len(config_groups) > 1在两条分支上采用同一策略——建议统一抛错并列出全部组名。若确需兼容既有 group_0 多组 checkpoint,用显式开关承载放行而非隐含在组名上;若有意保留 group_0 的 legacy 宽松行为,请在 :55 处注释说明「group_0 分支的 targets 校验交由各方案分支决定」,避免后续改动误删其中一处。
- 建议:将「多 group」与「targets 校验」两个判定从组名解耦:在
- server 库新增模块级依赖但只在测试 target 补了 //rtp_llm:utils @
rtp_llm/server/server_args/moe_group_args.py:2- 建议:在
rtp_llm/server/BUILD的py_library(name = "server")的 deps 中加入//rtp_llm:utils,让依赖显式化(//rtp_llm:utils当前 deps 不含 server,无环形风险)。加上库级依赖后,server_args/test/BUILD里的那条可保留亦可移除,但不应作为唯一修复手段。
- 建议:在
- 测试用 reset_backend_registrations 清空进程级注册表,污染不可逆且双重 mock 掩盖其声称覆盖的入口边界 @
rtp_llm/server/server_args/test/server_args_test.py:37- 建议:改为局部隔离:在
backend_registry提供仅测试可见的 snapshot/restore(返回并接受不透明 state,或 contextmanager),或用patch.dict/patch.object替换三个容器的副本,退出时精确恢复;并去掉对rtp_llm.utils.backend_registry.ensure_backend_entrypoint_loaded的 patch,只 patchimport_optional_internal_source_entrypoint(或注入真实的 fake entrypoint 模块),让自加载分支真实执行。同时把用例改名为反映实际断言(如「每个新 parser 都能解析外部 MoE 策略」)。若坚持沿用reset_backend_registrations(),请在其 docstring 中写明「调用后同进程内生产 hook 不可恢复」,并把相关用例隔离到独立 py_test target。
- 建议:改为局部隔离:在
- 工厂「缺少计算后端」守卫用空注册表自证,未验证真实注册表不会误认领 @
rtp_llm/model_loader/test/test_compressed_w8a8_int8_per_channel.py:152- 建议:保留现有文案用例,另加基于真实注册表的断言:
setUp(:132)已保存list(LinearFactory._strategies),可按cls.__module__.startswith("rtp_llm.")筛出在树策略并断言无一can_handleW8A8 配置;MOE 侧通过生产工厂初始化路径取得已填充的StrategyRegistry后再断言,并改用真实MoEConfigAdapter构造 helper(test_cuda_strategies.py已有)替代SimpleNamespace。私有属性替换建议由 factory 提供测试专用的set_strategies或 contextmanager 入口。
- 建议:保留现有文案用例,另加基于真实注册表的断言:
- dtype 模板化改造靠源码文本计数兜底,漏掉两处位置传参调用点 @
rtp_llm/model_loader/test/test_compressed_w8a8_int8_per_channel.py:484- 建议:改为行为断言替代源码文本断言:参数化遍历
w8a8_weight_list的关键 key(至少补齐linear_attn_qkvz_w、linear_attn_out_w、attn_qkv_w、moe_w1),经WeightModule.create生成 INT8 loader 并断言weight.kernel.data_type == torch.int8且 scale 为torch.float32,从而覆盖位置传参站点并移除对inspect.getsource的依赖;退一步也可把断言改为assertNotIn("float8_e4m3fn", source)以覆盖位置参数写法。
- 建议:改为行为断言替代源码文本断言:参数化遍历
- ROCm EP 量化过滤用例在其实际运行的 CUDA 机型上恒真,且唯一覆盖落在 open_skip target @
rtp_llm/models_py/modules/factory/fused_moe/tests/test_cuda_strategies.py:214- 建议:把「过滤在
super().can_handle()之前短路」这一契约直接断言出来(例如 patch/spyRocmEpNormalStrategy.get_attributes断言其未被调用,或先 patch 为可用 attributes 再断言不支持方法返回 False、且至少一个受支持方法返回 True——正向用例目前完全缺失),使 CUDA 机型上的通过具有判别力;并把不依赖 GPU 的条件类用例拆到同目录下无open_skip、无 GPUexec_properties的新 py_test target。若受限于 tag 无法调整,请在 PR 描述中说明这些断言仅在内部 CI 生效。
- 建议:把「过滤在
- 三个 no-quant 策略重复同一量化判定,其中两处与 router 完全冗余且新增用例绕过 can_handle() @
rtp_llm/models_py/modules/factory/fused_moe/impl/cuda/strategy/no_quant.py:58- 建议:把测试断言改为调用
strategy.can_handle(config)以覆盖真实选择边界,并用表驱动(本文件已有subTest)把三个策略 × 两种 model_config 组合起来,至少补上CudaNoQuantEpLowLatencyStrategy+no_auant_ep_low_latency的接受/拒绝两条断言;三处重复的三行抽成共享 helper 或基类实现。若 Cpp/DpNormal 两处确为纵深防御,请在注释中写明「与 router 条件重复、仅为防御」。
- 建议:把测试断言改为调用
- 槽位名与上下文键为自由字符串 + kwargs 透传,测试使用生产不存在的槽位名 @
rtp_llm/utils/test/backend_registry_test.py:34- 建议:把槽位定义为
Enum或模块级常量(并让文档槽位表引用同一常量),register_backend_hook/run_backend_registrations只接受该类型并对未知槽位 fail-fast;上下文改由每个槽位的TypedDict/dataclass 描述,或至少在文档与类型标注中固定各槽位的键集合。测试改用真实槽位常量,使「槽位名拼错」成为导入期错误而非静默 no-op。
- 建议:把槽位定义为
- W8A8 新增的 act_qscheme 与 worker precision 映射零测试覆盖 @
rtp_llm/cpp/engine_base/Executor.h:64- 建议:为
act_qscheme选择补一条覆盖(若有 pybind 暴露则在 Python 侧断言,否则在rtp_llm/cpp侧加轻量 cc_test,参数化遍历 PerTensor/FP8PTPC/W4A8/W8A8 四条分支);worker status 的 precision 映射在收敛为单一quantMethodToString后,补一条「每个 QuantMethod 枚举值都有非 UNKNOWN 映射」的穷尽性用例。
- 建议:为
- Mega-PR:MTP/PD handoff 与 CUDA Graph 修复混入 W8A8 量化与后端注册特性 @
rtp_llm/cpp/engine_base/stream/GenerateStream.cc:1034- 建议:按主线拆分为独立 PR:先合入 (3) handoff 修复与 (4) CUDA Graph 清零修复(各自附回归用例),再合入 (2) 注册机制(纯基建、无行为变化),最后合入 (1) W8A8 特性与 PPU 启用。若因发布窗口必须合并,请在 PR description 中分节说明四条主线的动机、相互无依赖关系、各自的验证方式与独立回滚手段,并在 commit 粒度上保持一一对应,便于 revert 单条线。
P3
- CUDA Graph host block table 每步全量清零落在 decode 热路径且新不变量无覆盖 @
rtp_llm/cpp/cuda_graph/cuda_graph_runner.cc:397- 建议:参照 :539-543 把清零窄化为
slice(0, state.current_batch_size, selected_graph_batch_size).fill_(0)(current_batch_size >= selected_graph_batch_size时跳过)。若活跃行的列尾同样会被fillParams解引用,请改为「padding 行整行清零 + 活跃行列尾清零」两段窄化填充并在注释写明该约束;若确认必须整表清零,请在注释中补上典型graph_bs/max_blocks下的实测每步开销。同时补一条 cc_test:构造current_batch_size < selected_graph_batch_size且上一轮写过非零 block ID 的场景,断言 padding 行为 0。
- 建议:参照 :539-543 把清零窄化为
- GenerateStream 新用例只覆盖「缺失则保留」,未覆盖「提供则刷新」与 commit-only 陈旧提案边界 @
rtp_llm/cpp/engine_base/stream/test/GenerateStreamTest.cc:117- 建议:补对偶用例:同样预置 9,但第 6 个字段传
torch::tensor({11}, torch::kInt32).to(torch::kCUDA),断言变为 11;再补一条 commit-only 步(accept_token_num使propose_token_被清空)后pickOneStepDraftToken取值的断言,固化「保留陈旧 GPU 镜像」是有意行为。建议把位置聚合初始化改为指派初始化或加字段名注释。
- 建议:补对偶用例:同样预置 9,但第 6 个字段传
- backend_registry 生命周期异常断言未校验消息,且 noop 用例无任何断言 @
rtp_llm/utils/test/backend_registry_test.py:66- 建议:改用
assertRaisesRegex并各自匹配特征片段(already initialised、lifecycle changed、backend is broken);no-op 用例补上可观察断言(例如先注册另一槽位的 hook,调用空槽位后断言该 hook 未被执行、且空槽位再次消费仍不报错)。
- 建议:改用
- 非 FP8 分支移除了 dispatch 返回值的类型校验,错误定位后移 @
rtp_llm/models_py/modules/factory/fused_moe/impl/cuda/routers/deepep_normal_router.py:161- 建议:tuple 分支加
len(output) == 2校验,else分支保留一次显式类型检查,并在消息中指明_prepare_dispatch_input的返回契约。
- 建议:tuple 分支加
- LoadQuantPerChannelFp8Weight 绕过了新引入的 supported_quant_config_types 单一来源 @
rtp_llm/model_loader/per_channel_fp8_quant_weight.py:707- 建议:让该子类显式声明
supported_quant_config_types = (Fp8PerChannelCompressedQuantConfig,)并在support()中改用isinstance(quant_config, cls.supported_quant_config_types),使「认领范围」在整个继承树上只有一种表达方式;若刻意保持与基类不同的语义(仅 on-the-fly 量化),请在该属性或方法上注释说明差异原因。
- 建议:让该子类显式声明
- setup_args 中的入口加载与槽位内部保证重复,注释描述的契约在生产路径不成立 @
rtp_llm/server/server_args/server_args.py:534- 建议:二选一并让注释与实现自洽:若目的是「启动早期 fail-fast,避免 entrypoint 的 ImportError 深埋在工厂构造里」,把注释改成这个理由并处理返回值(如
logger.info记录是否加载到外部后端);若无此需求,直接删除该行与对应 import,依赖run_backend_registrations的自加载。
- 建议:二选一并让注释与实现自洽:若目的是「启动早期 fail-fast,避免 entrypoint 的 ImportError 深埋在工厂构造里」,把注释改成这个理由并处理返回值(如
- W8A8 分支混用强下标与 get,input_activations 缺键时抛裸 KeyError @
rtp_llm/config/quant_config.py:268- 建议:把条件中的下标统一改为
.get()(如activation_config.get("strategy") == "token"),让不满足条件的 checkpoint 自然落到末尾带weights=/input_activations=上下文的raise ValueError;weights_config的三处一并处理,并补一条「缺 strategy 字段」的边界用例断言错误类型与文案。
- 建议:把条件中的下标统一改为
- choices 扩展点只保护 argparse 校验通道,env 补齐通道静默绕过 @
rtp_llm/server/server_args/server_args.py:366- 建议:在 :358-371 的补齐分支中复用 action 的校验(对有
choices的 action 调用self._check_value(action, converted_value),失败时self.error(...)给出与 argparse 一致的报错),并把except (ValueError, TypeError): pass改为报错或至少 warning;这样--moe_strategy的取值集合成为唯一真值来源,扩展点在两条通道上语义一致。补一条用例:给定一个命令行参数 + 非法MOE_STRATEGY,断言解析期即失败。
- 建议:在 :358-371 的补齐分支中复用 action 的校验(对有
- ignore 中的 re: 模式被硬失败且无运维旁路 @
rtp_llm/config/quant_config.py:932- 建议:在错误信息中给出可操作的迁移指引(把
re:展开为字面量模块名,并指明需展开到ignore的哪一层),或在_ignore_patterns侧支持将re:编译后交给 W8A8 loader 的 exclude 判定;同时明确 W4A8 的静默忽略是否为预期。若本次不打算支持,请在 PR description 与该 raise 处记录已知限制及受影响的 checkpoint 来源,便于部署前筛查。
- 建议:在错误信息中给出可操作的迁移指引(把
- PPU 设备识别依赖环境变量真值且无任何日志,CUDA 主机可能被静默重分类 @
rtp_llm/device/device_type.py:20- 建议:收紧判定并补可观测性:只接受明确的开关值(如要求
PPU_HOME指向存在的目录,或改用RTP_LLM_DEVICE_TYPE=ppu这类显式量),并在返回非 Cuda 结果时用logger.info打印判定依据(命中的变量名与取值),使误分类在启动日志中可见;建议对get_device_type()加functools.lru_cache固化单进程内判定结果,并补一条覆盖「CUDA 可用 + PPU_HOME 为空串/0」的用例。
- 建议:收紧判定并补可观测性:只接受明确的开关值(如要求
- arch.py 新增的 is_ppu 自身未使用,仅作为隐式再导出通道 @
rtp_llm/models_py/utils/arch.py:11- 建议:让
qwen3_next.py直接从rtp_llm.device.device_type导入这些谓词(与is_cuda/is_hip的来源保持一致),删除arch.py中未使用的导入;若确实希望arch.py作为统一门面,请显式声明__all__并在模块注释中说明它对外重导出设备判定函数。
- 建议:让
- no_auant 拼写错误的策略取值出现第三处副本 @
rtp_llm/models_py/modules/factory/fused_moe/tests/test_cuda_strategies.py:179- 建议:把策略名收敛为 Enum 或模块级常量,由 CLI choices 与策略实现共同引用,测试引用同一常量;若要纠正拼写,请在同一变更中保留旧名作为兼容别名并在文档中标注该已知拼写。
- hook 测试白盒构造 router,且伪造 buffer 依赖 dispatch 位置参数下标 @
rtp_llm/models_py/modules/factory/fused_moe/impl/cuda/routers/test/deepep_normal_router_hook_test.py:55- 建议:fake 改用完整命名签名(
def dispatch(self, x, handle, num_tokens_per_rank, num_tokens_per_rdma_rank, is_token_in_rank, num_tokens_per_expert, topk_idx, topk_weights, *, expert_alignment)),使参数顺序变更成为调用期错误;或改为保留__init__、只替换DeepEPWrapper.get_instance来构造 router,并至少断言一次由__init__推导的字段(如以ep_size=2构造后校验rank_expert_offset)。
- 建议:fake 改用完整命名签名(
- 单个测试文件跨五个模块,且直接改写生产私有状态 @
rtp_llm/model_loader/test/test_compressed_w8a8_int8_per_channel.py:130- 建议:按归属拆分:
LinearFactory/StrategyRegistry的守卫测试移入各自 factory 的 test 目标,DeepEP 分桶测试移入rtp_llm/models_py/distributed的 test 目标,quant_config 解析测试移入配置侧 test 目标;本文件只保留 loader 与 dtype/postprocess 用例。私有属性替换建议由 factory 提供测试专用的set_strategies或 contextmanager 入口。
- 建议:按归属拆分:
Checklist Findings (26 fail / 68 total)
General Principles Checklist
- [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue
server 库新增模块级依赖但只在测试 target 补了 //rtp_llm:utils
moe_group_args.py:2与server_args.py:68新增模块级from rtp_llm.utils.backend_registry import ...。两文件都被rtp_llm/server/BUILD:10-15的 glob(*.py+server_args/*.py)纳入//rtp_llm/server:server,而该 target 的 deps(:16-21)只有:request_headers、//rtp_llm:warmup(srcs 仅utils/warmup.py,见rtp_llm/BUILD:180-182)、//rtp_llm:vipserver与 flexlb proto,均不提供rtp_llm/utils/backend_registry.py(它归//rtp_llm:utils的utils/**/*.pyglob)。本 PR 只在server_args/test/BUILD:8补了//rtp_llm:utils,即修在消费方 - [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
PPU 设备识别依赖环境变量真值且无任何日志,CUDA 主机可能被静默重分类
get_device_type()在torch.cuda.is_available()且非 HIP 时,只要os.environ.get("PPU_HOME")为任意真值(含"0"、任意残留路径)或 torch 版本串含ppu,就返回DeviceType.Ppu(:20-24),无值内容校验、无日志、无缓存。后果落在本 PR 涉及的分支上:is_cuda()变为 False,attention/__init__.py与fused_moe/__init__.py从 CUDA 分支跌入else兜底(丢掉 headwise/TRT-LLM/XQA/MLA 等 impl 与fused_moe/__init__.py:106的 FP4 策略注册),arch.py的 SM 判定全部转 False,并直接改变本 PR 新加的qwen3_next.py:248白名单判定——一台正常 CUDA 机器只因残留环境变量就被静默降级,排查时无任何线索。 - [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue
arch.py 新增的 is_ppu 自身未使用,仅作为隐式再导出通道
arch.py:6-12从rtp_llm.device.device_type导入DeviceType/get_device_type/is_cuda/is_hip/is_ppu,但is_ppu在本文件内没有任何使用点(全仓is_ppu仅 3 个引用:定义处、arch.py:11、qwen3_next.py:242)。真正的消费者qwen3_next.py:238-243从models_py.utils.arch而非定义模块导入,使arch.py成为设备判定的隐式再导出层:既无__all__声明该意图,也让「设备判定归属rtp_llm.device」这一分层被模糊,后续清理未使用 import 的工具会直接删掉它并打挂 qwen3_next 的设备门禁。 - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
PPU 设备识别依赖环境变量真值且无任何日志,CUDA 主机可能被静默重分类
get_device_type()在torch.cuda.is_available()且非 HIP 时,只要os.environ.get("PPU_HOME")为任意真值(含"0"、任意残留路径)或 torch 版本串含ppu,就返回DeviceType.Ppu(:20-24),无值内容校验、无日志、无缓存。后果落在本 PR 涉及的分支上:is_cuda()变为 False,attention/__init__.py与fused_moe/__init__.py从 CUDA 分支跌入else兜底(丢掉 headwise/TRT-LLM/XQA/MLA 等 impl 与fused_moe/__init__.py:106的 FP4 策略注册),arch.py的 SM 判定全部转 False,并直接改变本 PR 新加的qwen3_next.py:248白名单判定——一台正常 CUDA 机器只因残留环境变量就被静默降级,排查时无任何线索。 - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
ignore 中的 re: 模式被硬失败且无运维旁路
CompressedW8A8Int8PerChannelQuantConfig.__init__(:929-936)对任何以re:开头的 ignore 模式抛ValueError。llm-compressor 生成的 compressed-tensors checkpoint 普遍在ignore中混用字面量与re:正则(如re:.*mlp.gate$),此类 checkpoint 将完全无法加载,且没有任何开关可临时放行或降级,运维侧只能改写 checkpoint 的 config.json。对比同族的CompressedW4A8Int4PerChannelQuantConfig(:859-874):它只把ignore_patterns存入_ignore_patterns且从不写入exclude_modules,因此re:在 W4A8 路径上被静默忽略——同一 checkpoint 字段在两条路径上一边硬失败、一边静默忽略。该限制与上文「逐层完整枚举被误拒」叠加后,实际可用的 exclude 写法非常狭窄。 - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
测试用 reset_backend_registrations 清空进程级注册表,污染不可逆且双重 mock 掩盖其声称覆盖的入口边界
用例在 :37 与finally(:64)各调用一次reset_backend_registrations()(清空_hooks/_started/_repeatable,backend_registry.py:99-104),两次都在patch(...)作用域之外。真实 hook 只在 backend entrypoint 首次 import 时注册,而import_optional_internal_source_entrypoint带@lru_cache(maxsize=None)(import_util.py:26)且模块已在sys.modules中,被清空的 hook 无法重新登记;因此在含 internal_source 的构建里,本用例结束后同进程所有槽位退化为空 no-op,同文件按类名排在其后的ServerArgsSetTest多次setup_args()静默拿不到外部策略。用例还同时 patch 了 `server_args.ensure_backend_entrypoint_loade - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
choices 扩展点只保护 argparse 校验通道,env 补齐通道静默绕过
EnvArgumentParser.parse_args有两条 env 路径:无命令行参数时把 env 拼成--option value交给 argparse(:263-291,choices 生效);存在任意命令行参数(has_cmd_args=True)时,对未显式提供的项走 :343-371 的补齐分支,只做action.type(env_value)转换后setattr(:362-363),完全不校验action.choices,且except (ValueError, TypeError): pass(:366-368)会静默丢弃转换失败。因此MOE_STRATEGY=<任意串>配合任一命令行参数即可绕过 choices,落到StrategyRegistry才以「No suitable MOE strategy found」失败,错误位置远离根因。该绕过早于本 PR,但本 PR 恰好把 choices 建成了外部后端的正式扩展契约。 - [6.1] Quality — Commit 原子、message 与行为匹配 → issue
Mega-PR:MTP/PD handoff 与 CUDA Graph 修复混入 W8A8 量化与后端注册特性
41 个改动文件至少含四条彼此无依赖的主线:(1) compressed-tensors W8A8 INT8 加载链路与枚举暴露(quant_config.py、model_loader/**、QuantInfo.*、ConfigInit.cc、Executor.h、.pyi,约 15 文件);(2) out-of-tree 后端延迟注册机制(backend_registry.py+ 四个槽位消费点 +server_args*+ docs,约 10 文件);(3) MTP/PD 交接语义(GenerateStream.cc:1034由无条件赋值改为条件赋值、DecodeRpcServer.cc:338新增发布);(4) CUDA Graph host block table 清零修复(cuda_graph_runner.cc:397);外加 PPU 设备启用(device_type.py、arch.py、qwen3_next.py)。(3)(4) 是可独立回滚验证的 KV/token 正确性修复,一旦 (1) 或 (2) - [6.1] Quality — Mega-PR 已拆分为独立变更 → issue
Mega-PR:MTP/PD handoff 与 CUDA Graph 修复混入 W8A8 量化与后端注册特性
41 个改动文件至少含四条彼此无依赖的主线:(1) compressed-tensors W8A8 INT8 加载链路与枚举暴露(quant_config.py、model_loader/**、QuantInfo.*、ConfigInit.cc、Executor.h、.pyi,约 15 文件);(2) out-of-tree 后端延迟注册机制(backend_registry.py+ 四个槽位消费点 +server_args*+ docs,约 10 文件);(3) MTP/PD 交接语义(GenerateStream.cc:1034由无条件赋值改为条件赋值、DecodeRpcServer.cc:338新增发布);(4) CUDA Graph host block table 清零修复(cuda_graph_runner.cc:397);外加 PPU 设备启用(device_type.py、arch.py、qwen3_next.py)。(3)(4) 是可独立回滚验证的 KV/token 正确性修复,一旦 (1) 或 (2) - [6.1] Quality — PR description 说明动机与设计 → issue
Mega-PR:MTP/PD handoff 与 CUDA Graph 修复混入 W8A8 量化与后端注册特性
41 个改动文件至少含四条彼此无依赖的主线:(1) compressed-tensors W8A8 INT8 加载链路与枚举暴露(quant_config.py、model_loader/**、QuantInfo.*、ConfigInit.cc、Executor.h、.pyi,约 15 文件);(2) out-of-tree 后端延迟注册机制(backend_registry.py+ 四个槽位消费点 +server_args*+ docs,约 10 文件);(3) MTP/PD 交接语义(GenerateStream.cc:1034由无条件赋值改为条件赋值、DecodeRpcServer.cc:338新增发布);(4) CUDA Graph host block table 清零修复(cuda_graph_runner.cc:397);外加 PPU 设备启用(device_type.py、arch.py、qwen3_next.py)。(3)(4) 是可独立回滚验证的 KV/token 正确性修复,一旦 (1) 或 (2) - [6.1] Software Engineering — DIP:高层策略不依赖非必要具体细节 → issue
moe_strategy_choices 把 argparse 私有结构固化为跨仓契约,且文档与测试给出两种写法
槽位只需扩展--moe_strategy的choices,却把整个EnvArgumentParser交给 hook(parser=parser),于是唯一可行写法是遍历私有属性parser._actions反查 action,同时 hook 获得改写任意其它参数(含bind_to绑定)的能力。该泄漏已固化为两种不一致实现:backend_registration.md:107-115重建列表后回写action.choices,带幂等判断与缺失时的RuntimeError;server_args_test.py:28-32用无 default 的next(...)(缺失时抛StopIteration)并原地choices.append,无幂等判断。一旦--moe_strategy改名、choices改为 tuple,或EnvArgumentParser改为包装/重建 action,所有外部后端同时失效,且只在解析期以AttributeError/StopIteration - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
setup_args 中的入口加载与槽位内部保证重复,注释描述的契约在生产路径不成立
run_backend_registrations(backend_registry.py:79)在取_lock、在_started.add(slot)之前就已调用ensure_backend_entrypoint_loaded(),因此任何槽位的首次消费都会先完成入口加载与 hook 登记,register_backend_hook不会因槽位已启动而抛错。删掉server_args.py:534后,init_moe_group_args里的run_backend_registrations仍会自行加载并正确应用 hook,结果完全一致;返回值也被丢弃。而相邻注释(:532-533)把这行描述成必要的时序保证,与实现不符——新测试对rtp_llm.utils.backend_registry.ensure_backend_entrypoint_loaded的额外 patch 恰好掩盖了这一冗余。该行还让只走init_all_group_args的离线工具路径与setup_args路径的加载时机不一致。 - [6.1] Software Engineering — ISP:调用方不依赖无关大接口 → issue
moe_strategy_choices 把 argparse 私有结构固化为跨仓契约,且文档与测试给出两种写法
槽位只需扩展--moe_strategy的choices,却把整个EnvArgumentParser交给 hook(parser=parser),于是唯一可行写法是遍历私有属性parser._actions反查 action,同时 hook 获得改写任意其它参数(含bind_to绑定)的能力。该泄漏已固化为两种不一致实现:backend_registration.md:107-115重建列表后回写action.choices,带幂等判断与缺失时的RuntimeError;server_args_test.py:28-32用无 default 的next(...)(缺失时抛StopIteration)并原地choices.append,无幂等判断。一旦--moe_strategy改名、choices改为 tuple,或EnvArgumentParser改为包装/重建 action,所有外部后端同时失效,且只在解析期以AttributeError/StopIteration - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
setup_args 中的入口加载与槽位内部保证重复,注释描述的契约在生产路径不成立
run_backend_registrations(backend_registry.py:79)在取_lock、在_started.add(slot)之前就已调用ensure_backend_entrypoint_loaded(),因此任何槽位的首次消费都会先完成入口加载与 hook 登记,register_backend_hook不会因槽位已启动而抛错。删掉server_args.py:534后,init_moe_group_args里的run_backend_registrations仍会自行加载并正确应用 hook,结果完全一致;返回值也被丢弃。而相邻注释(:532-533)把这行描述成必要的时序保证,与实现不符——新测试对rtp_llm.utils.backend_registry.ensure_backend_entrypoint_loaded的额外 patch 恰好掩盖了这一冗余。该行还让只走init_all_group_args的离线工具路径与setup_args路径的加载时机不一致。 - [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue
LoadQuantPerChannelFp8Weight 绕过了新引入的 supported_quant_config_types 单一来源
本 PR 在基类引入supported_quant_config_types(:286-289)作为「该 loader 认领哪些 config」的单一来源,并在注释中声明子类必须收窄以维持WeightModule.create的唯一匹配不变量,基类support()(:295-297)据此判定。但同文件的子类LoadQuantPerChannelFp8Weight.support()(:707-709)仍硬编码isinstance(quant_config, Fp8PerChannelCompressedQuantConfig),与继承来的类属性(含Fp8PerChannelQuarkQuantConfig)不一致。当前因它要求is_quanted()为 False 而与 W8A8 无交集,不构成缺陷;但后续通过修改类属性调整认领范围时,这条路径不会跟随,破坏刚建立的约定。 - [6.1] Software Engineering — OCP:本地扩展点优先于修改中心逻辑 → issue
设备无关的中心工厂硬编码 W8A8 后端专属文案(两处复制),且量化方法串在三处字面量消费
StrategyRegistry.get_strategy()是设备与量化格式无关的通用选择器,现在为W8A8_INT8_PER_CHANNEL_COMPRESSED单独加了分支与专用文案(:87-93);linear/factory.py:113-123有一份同款复制,两处文案需人工保持一致。该格式只有外部后端能执行,等于把具体后端的知识写进中心逻辑:下一个由外部后端承载的量化方法又要再改这两处。该串唯一权威定义在quant_config.py:949的get_method(),却以字面量第三次出现在deepep_wrapper.py:235的is_per_act_token元组;漏改该处不会报错,只会把 low-latency 分桶静默退回[64,128]或抛 "Unsupported quantization config"(:261)。另外 :74-78 用内联三元重算quant_method,重复了MoeConfigResolver.get_quant_method()。 - [6.1] Software Engineering — SRP:模块/类职责单一 → issue
单个测试文件跨五个模块,且直接改写生产私有状态
该文件挂在//rtp_llm/model_loader/test下,却同时覆盖LinearFactory、StrategyRegistry(:22-25)、quant_config的 checkpoint 解析(:189)、DeepepWrapperConfig.calc_low_latency_max_token_per_rank(:318)与 pybindQuantAlgo(:491-504)五个模块;BackendAvailabilityGuardTest还直接赋值生产私有属性LinearFactory._strategies(:132/135/152/162)。同风险类的兄弟测试test_compressed_w4a8_int4_per_channel.py仅一个类、无跨模块导入。后果是这些模块自身的测试目标保持沉默,回归定位会统一指向 model_loader;且对linear/factory.py:119、strategy_registry.py:89的错误文案与_strategies属性名形成隐式耦 - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
ROCm EP 量化过滤用例在其实际运行的 CUDA 机型上恒真,且唯一覆盖落在 open_skip target
test_unsupported_quant_methods_return_false(:214-227)断言RocmEpNormalStrategy().can_handle(config)为 False。但在 CUDA 机器上,即使删掉ep.py:44的 allow-list,MoeStrategy.can_handle(strategy_base.py:58-64)也会因get_attributes()(ep.py:101-104)惰性导入 ROCm executor 抛ImportError而返回 False——断言在两种情况下都通过,无法区分「被 allow-list 过滤」与「因缺 ROCm 依赖被过滤」。同时该用例所在 target 带tags = ["open_skip"]且exec_properties = {'gpu':'H20'}(fused_moe/tests/BUILD:16-17,本 PR 未修改),而这两组条件类用例只调用check_conditions/can_handle,并不需 - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
单个测试文件跨五个模块,且直接改写生产私有状态
该文件挂在//rtp_llm/model_loader/test下,却同时覆盖LinearFactory、StrategyRegistry(:22-25)、quant_config的 checkpoint 解析(:189)、DeepepWrapperConfig.calc_low_latency_max_token_per_rank(:318)与 pybindQuantAlgo(:491-504)五个模块;BackendAvailabilityGuardTest还直接赋值生产私有属性LinearFactory._strategies(:132/135/152/162)。同风险类的兄弟测试test_compressed_w4a8_int4_per_channel.py仅一个类、无跨模块导入。后果是这些模块自身的测试目标保持沉默,回归定位会统一指向 model_loader;且对linear/factory.py:119、strategy_registry.py:89的错误文案与_strategies属性名形成隐式耦 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
W8A8 分支混用强下标与 get,input_activations 缺键时抛裸 KeyError
新增分支的判定条件混用两种取值风格:activation_config["type"](:268)、["num_bits"](:269)、["strategy"](:270)为直接下标,而同一条件里的dynamic/symmetric用.get(..., False)/.get(..., True);weights_config["num_bits"](:228)与["type"]/["strategy"](:230-232)同样是强下标。compressed-tensors 的QuantizationArgs对num_bits/type/strategy都定义了默认值,手写或旧版本导出的 config.json 可能省略其中任一字段,此时条件求值本身抛KeyError: 'strategy',直接跳过同一 PR 在 :312-320 精心添加的带上下文ValueError,排查体验回到改动前水平。
RTP-LLM Checklist
- [D] 性能 — 推理热路径禁止引入 per-forward Python callback、GIL 持有、.item()/.cpu()/.tolist()、同步 flush 或隐式 GPU sync;必要时必须证明 default-off、异步或限频 → issue
CUDA Graph host block table 每步全量清零落在 decode 热路径且新不变量无覆盖
kv_cache_kernel_block_id是[graph_bs, max_blocks]的 pinned CPU 视图,fill_(0)每次prepareAttentionInputs都 memset 全部graph_bs * max_blocks * 4字节,而该函数每个 decode step 都执行且位于 replay 前的串行关键路径;按注释自述只有 padding 行需要清理,活跃行随后被 :525 的stridedCopyHost覆盖。同文件对同类 padding 清理已有窄区间约定::539-543(sequence_lengths)只slice(0, fill_start, selected_graph_batch_size),本次新增与该约定不一致。此外「padding 行必须为 0,否则 KV 写入活跃请求 block」这一新不变量没有任何单测或断言。本轮未做基准测量,故按无实测证据的性能项定级。 - [I] 代码质量 — 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理 → issue
ROCm EP 量化 allow-list 与 executor 映射构成双份真值,收窄后 QuarkMXFP4 改为启动失败且被删字符串仍被消费
_SUPPORTED_QUANT_METHODS(:25-31)与_resolve_executor_and_quant()的 if/elif(:64-97)必须手工同步:只加分支不加集合 →can_handle在 :44 提前 False,方法静默不可用;只加集合不加分支 → :95 抛ValueError,而strategy_base.py:58-64只捕获ImportError,异常会冲出策略选择流程。被删的三个串在消费侧仍被声明为可支持:rocm/executors/rocm_moe.py:289、:404-405、rocm/routers/pure_tp_router.py:209、linear/impl/rocm/fp8_deepgemm_linear.py:40、linear/factory.py:248。已核对全部get_method()返回值,确认无 config 产出这三个串(非现网破坏),但留下自相矛盾的支持声明。另有真实行为变化:QuarkMXFP4是可达取值,原先落else得到 - [I] 代码质量 — 同一功能用统一工具函数 → issue
no_auant 拼写错误的策略取值出现第三处副本
新增用例把"no_auant_cpp"/"no_auant_dp_normal"(no_quant的拼写错误)作为字面量再复制一份(:179、:188、:197、:206),与no_quant.py:60/88及moe_group_args.py:167-169的--moe_strategychoices 构成第三处副本。该取值同时是 CLI 对外可见的字符串,纠正需要同步生产、测试与参数 choices 三处,副本越多越难修。
Python Static-First Checklist
- [P.A] 静态结构与类型纪律 — 字符串分发用 Enum/Literal → issue
no_auant 拼写错误的策略取值出现第三处副本
新增用例把"no_auant_cpp"/"no_auant_dp_normal"(no_quant的拼写错误)作为字面量再复制一份(:179、:188、:197、:206),与no_quant.py:60/88及moe_group_args.py:167-169的--moe_strategychoices 构成第三处副本。该取值同时是 CLI 对外可见的字符串,纠正需要同步生产、测试与参数 choices 三处,副本越多越难修。 - [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
hook 测试白盒构造 router,且伪造 buffer 依赖 dispatch 位置参数下标
_new_router用object.__new__(router_class)(:55)跳过DeepepNormalRouterBase.__init__,手工赋 7 个内部属性,因此expert_num_per_rank、rank_expert_offset的真实推导关系(deepep_normal_router.py:68-70)未被验证,测得的rank_expert_offset=0(:69)是手工设定值——而该值恰好决定 :167-172 的 topk 重映射结果。_TupleDispatchBuffer.dispatch(:23-33)通过args[5]/args[6]取 topk_ids/topk_weights,与生产prepare的位置实参强耦合:调用顺序调整时 fake 会静默回显错误张量而断言仍通过;__init__新增prepare依赖的字段时,测试会以AttributeError失败而非表达契约破坏。 - [P.G] 测试规范 — pytest.raises 带 match 参数 → issue
backend_registry 生命周期异常断言未校验消息,且 noop 用例无任何断言
test_repeatable_slot_rejects_late_hooks(:66)、test_slot_lifecycle_cannot_change_after_start(:72)、test_registering_after_drain_raises(:101)都只断言RuntimeError类型,而backend_registry.py有两条语义完全不同的 RuntimeError(:63-66 "already initialised" 与 :85-87 "lifecycle changed"):任一被误抛成另一条,三个用例仍全绿,区分能力完全丢失。test_hook_exception_propagates(:112)同理未校验是否为 hook 自身异常。test_draining_slot_without_hooks_is_noop(:115-116)只调用一次函数、无任何断言,无法区分「no-op」与「异常被静默吞掉」。
Strengths
- W8A8 跨语言契约一次改全且被测试钉住:
QuantInfo.h:19追加枚举 12 未重排既有编号,QuantInfo.cc:49-52强制group_size=0/bits=8,ConfigInit.cc、.pyi、Executor.h、ModelConfig.cc逐项对齐,QuantAlgoBindingTest直接断言 Pythonget_algo()→ C++isW8a8Int8PTPC()往返;stub 还顺带补回__members__中漏掉的QuarkMXFP4: 11。 - INT8 加载器只声明
weight_dtype、apply_fp8_device_conversion、supported_quant_config_types三个类属性即复用 FP8 的全部 tensor 映射与 TP/EP 切分(compressed_w8a8_int8_per_channel_weight.py:22-24),属本地扩展点而非改中心逻辑;apply_fp8_device_conversion门控有硬必要性(ROCmconvert_fp8_weight_params带 FP8 dtype 断言),而 CUDA 基类为恒等返回,行为零变化。 supported_quant_config_types的收窄服务于WeightModule.create的「唯一匹配」不变量,且 W8A8 config 与 FP8/W4A8 无 isinstance 交集,不会触发多 loader 命中报错。CompressedW8A8Int8PerChannelQuantConfig彻底避免**kwargs汇聚:__init__全具名参数,_from_config(quant_config.py:971-983)以allowed_keys白名单显式TypeError拒绝未知 key,拼错配置键不会静默退化成空exclude_modules——与同文件 W4A8 的kwargs.get(...)相比是明确改进。_pick_config_group(quant_config.py:32-44)保持group_0优先返回,只在无group_0且恰有唯一命名组时才走新分支,既支持 Qwen3.5 的命名组,又保证现存 checkpoint 解析结果逐字节不变。- 未识别的 compressed-tensors 组合从「抽象类实例化 TypeError」改为带
weights=/input_activations=上下文的ValueError(quant_config.py:312-320),并用isinstance(activation_config, dict)守卫(:245/:267)让 weight-only checkpoint 不再崩在无意义位置。 get_supported_kv_cache_dtypes主动收窄为[float16, bfloat16]并注明「fp8 KV cache 组合未验证,宁可启动即失败」(quant_config.py:962-968),是保守且可回滚的默认值选择。backend_registry.py保持设备与厂商无关,只依赖稳定入口名models_py;生命周期语义清晰(槽位启动后冻结 hook 集合、拒绝过晚注册、拒绝生命周期切换、不吞 hook 异常),模块 docstring 明确交代「静默丢注册 → 选错实现 → 错数值而非启动失败」的失效模式,并有 11 条定向用例。docs/backend/backend_registration.md的槽位表与四个消费点的上下文键(factory=、registry=、四个 attention 列表、parser=)逐项对得上,并已接入docs/index.rst;moe_strategy_choices的repeatable=True与「每次新建 choices list 字面量」(moe_group_args.py:165-185)配合正确,server_args_test.py:51-56的两次连续setup_args正是该语义的有效回归点。strategy_registry.py:102把get_attributes()从每候选多次收敛为一次性物化并复用于排序与日志,消除了原先经懒加载 import 与后端日志反复触发的重复副作用,排序数值与稳定性与原实现等价。deepep_normal_router.py:157把 FP8 dispatch 缺 scale 从assert改为带说明的ValueError,在-O运行下同样生效,并有定向用例覆盖;ROCmep.py把未识别量化方法从「静默回落 bf16 executor」改为显式ValueError。cuda_graph_runner.cc:390-398的注释完整交代了触发条件、后果(padding 行残留上次 replay 的 block ID 并把 KV 写入活跃请求的 block)与「block 0 为保留块」的前提;清零位于 :525stridedCopyHost回填之前,顺序正确,tagged cache 分支已由 :473-476 单独覆盖。PerChannelFp8PostprocessTest用记录调用次数的假 device 与哨兵张量断言_postprocess的实际行为(INT8 跳过转换、byte-exact、kernel/scale 形状重排),压的是行为而非驱动它的类属性。_ckpt_base_matches_quant_exclude的 Note(per_channel_fp8_quant_weight.py:83-92)诚实记录了 FP8/W4A8 既有的静默数值错误风险及不在本 PR 修复的理由;qwen3_next.py:248-252把get_device_type().name写进报错文案,使允许清单增长后信息仍然真实。
| auto accept_tokens = torch::zeros({1, static_cast<int64_t>(propose_step + 1)}, cuda_i32); | ||
| accept_tokens[0][0] = sp_output_buffer->tokens[0][0]; | ||
| propose_tokens_gpu[0] = sp_output_buffer->tokens[0][1]; | ||
| sp_output_buffer->propose_tokens_gpu = propose_tokens_gpu; |
There was a problem hiding this comment.
[P2] propose_tokens_gpu 两个生产者的形状与取值语义分叉
新增 sp_output_buffer->propose_tokens_gpu = propose_tokens_gpu,该张量为 torch::empty({1})(:333)且只写入 tokens[0][1](第一个 draft,:337)。同字段另一生产者 StreamCacheResource.cc:223 写 tokens.to(cuda_i32),形状 {1,N}、含 index 0 的已提交 token,其 MtpAsyncDeviceState 版本用 narrow(1,1,N-1) 并在 :234-235 注明「MTP verify 路径期望持有全部 propose_step 个 draft」。消费方 pickOneStepDraftToken(MtpBatchStreamProcessor.cc:250)统一走 lastColumnAsFlat(:147-150 取最后一列):{1} 得第一个 draft,{1,N} 得最后一个。propose_step==1 时两路等价(也与被替代的 `columnAs...
建议: 统一该字段契约:建议 gRPC 侧改为与 P2P 一致的 sp_output_buffer->tokens.narrow(1, 1, propose_tokens.size() - 1).to(cuda_i32, /*non_blocking=*/true)({1, propose_step}、全部 draft),并在 GenerateStream.h:96 处用注释固化「shape {batch, propose_step}、最后一列为最新 draft」。若确定只支持单步语义,请在此处补 RTP_LLM_CHECK_WITH_INFO(propose_step == 1, ...) 让多步配置显式失败,而不是依赖 select(-1) 恰好吸收形状差异。另补一条 propose_step >= 2 的用例,断言两条通道经 pickOneStepDraftToken 得到同一 token。
| or src_weight_info.name not in cls.w8a8_weight_list | ||
| ): | ||
| return False | ||
| for ckpt_w in src_weight_info.weights: |
There was a problem hiding this comment.
[P2] W8A8 部分 exclude 的 fail-fast 守卫对 MoE {expert_id} 模板完全失效
守卫依赖 _ckpt_base_matches_quant_exclude,其正则由 _exclude_pattern_for(per_channel_fp8_quant_weight.py:57-64)生成,只把 {i} 替换为 \d+,其余字符全部 re.escape。而 MoE 专家权重模板含第二个占位符,例如 qwen3_next_weight.py:409 的 layers.{i}.mlp.experts.{expert_id}.down_proj.weight;编译后 \{expert_id\} 成为字面量,永远匹配不上 checkpoint 里的 model.layers.3.mlp.experts.5.down_proj。w8a8_weight_list 明确含 W.moe_w1/W.moe_w2(:276-277),因此 MoE 专家被 ignore 时既不触发新增的 raise,基类 support()(:303-309)也不会返回 False,仍按 INT8 加载并索取不存在的 `.weight_sca...
建议: 让 _exclude_pattern_for 通用化:把模板中所有 {...} 形式的占位符统一替换为 \d+(或显式支持 {expert_id}/{i_1}),并为 moe_w1/moe_w2 补一条「ignore 命中单个 expert 时行为明确」的用例(raise 或整模板回退,二选一并写进注释)。若不打算支持 MoE 逐专家排除,请在 support() 处显式判定模板是否含非 {i} 占位符并直接报错,避免守卫给出虚假保障。
| base_name, quant_config.exclude_modules | ||
| ) | ||
| ): | ||
| raise ValueError( |
There was a problem hiding this comment.
[P2] 逐层枚举式的完整 exclude 被误拒,且 support() 由纯谓词变为可抛异常
判定式为 base_name not in exclude_modules and _ckpt_base_matches_quant_exclude(...)(:40-45):base_name 是带 {i} 的内部模板,真实 compressed-tensors 的 ignore 只写具体层名,永远不等于模板串;re: 又已在 quant_config.py:932 被拒。于是即使 checkpoint 在全部层逐一列出某模块(数值上安全的完整回退),也与「部分排除」走同一分支 raise,super().support 的未量化回退对真实 checkpoint 不可达;文案却断言 "ignore targets only some instances",误导排查。同时 WeightModule.support 是声明为 -> bool 的抽象谓词、且被 create 对全部注册类求值,此处 raise 让一次能力探测可中断整个 create,而 FP8/W4A8 同形态仍静默返回 False。
建议: 按具体层实例而非模板判定:用 _exclude_pattern_for(base_name) 收集命中的层集合,与 load_config.num_layers 覆盖的层集合比较,全覆盖时 return False 走未量化回退,仅部分覆盖时 raise,并把实际命中与缺失的层号写进错误信息。若暂不实现,请把文案改为陈述限制本身(如「该权重模板存在具体层排除项,W8A8 路径不支持逐层回退」)。同时把校验从 support 移到 loader 构造或独立的 validate_quant_config 阶段以保持谓词纯净;若坚持保留,请在基类文档中声明「support 允许抛出配置级致命错误」并说明 FP8/W4A8 未对齐的原因。补一条「全层具名枚举」的边界用例。
| case QuantMethod::W4A8INT4PTPC: | ||
| status_info.precision = "W4A8INT4PTPC"; | ||
| break; | ||
| case QuantMethod::W8A8INT8PTPC: |
There was a problem hiding this comment.
[P2] QuantMethod→字符串映射在两处 switch 重复维护且各自漏枚举值,worker status 持续打 ERROR 并对外上报 UNKNOWN
本 PR 对两份语义相同的映射各加了一次 W8A8INT8PTPC:ModelConfig.cc:53-82 的 quantMethodToString(调试串)与 LocalRpcServer.cc:405-442 的 switch(经 set_precision 外发的 gRPC worker status)。两份已发散:LocalRpcServer 缺 ModelOptFP4(10) 与 QuarkMXFP4(11),这两种部署每次状态上报都落入 default:,打一条 RTP_LLM_LOG_ERROR("unknown quant method: %d") 并把 precision 报成 "UNKNOWN";quantMethodToString 覆盖了 ModelOptFP4 但对 QuarkMXFP4 返回 UNKNOWN(11);None 在前者是 "None"、后者是 "FP16"。QuarkMXFP4 是可达取值(QuantInfo.cc:53 由 mxfp4-quark 产...
建议: 把映射收敛到一处:将 quantMethodToString 提升到 rtp_llm/cpp/model_utils/QuantInfo.h/.cc(与 enum 同处)并导出,LocalRpcServer::getWorkerStatusInfo 直接复用,仅把 QuantMethod::None → "FP16" 这一处业务差异留在调用点覆盖;同时补齐 ModelOptFP4/QuarkMXFP4 分支以消除虚假 ERROR 日志与 UNKNOWN 上报。此后新增枚举值只需改一个 switch。
| _repeatable.add(slot) | ||
| for hook in tuple(_hooks.get(slot, ())): | ||
| logger.debug("running backend registration hook for slot %r", slot) | ||
| hook(**context) |
There was a problem hiding this comment.
[P2] backend hook 在持有全局 RLock 时执行,与 Python import 锁构成锁序反转
hook(**context)(:96)在 with _lock:(:81)内执行,而全部四个槽位消费点都发生在模块 import 期间(fused_moe/__init__.py:120、attention/__init__.py:159、linear/__init__.py:31、moe_group_args.py:201),此时线程已持有对应模块的 import lock;文档推荐写法(backend_registration.md:95-104)也会在 hook 体内 import 实现模块。于是形成两种加锁顺序:A 线程「import lock(fused_moe) → _lock → hook 内 import 后端模块」,B 线程「import lock(该模块) → 等待 _lock」。其中一条边是普通 RLock,CPython 的模块锁死锁检测无法识别,表现为启动期挂死而非报错。
建议: 把「取快照」与「执行 hook」分离:锁内完成 _started/_repeatable 判定并复制出待执行的 hook 元组,释放锁后再逐个调用 hook(**context);一次性语义仍由 _started 保证。若确需串行化 hook 执行,改用槽位级独立锁而非覆盖全部槽位的进程级锁,把锁粒度与 import 锁解耦;backend_registration.md:141-142 关于「进程级锁保护」的表述需同步更新。
| @@ -32,3 +32,7 @@ def is_cuda() -> bool: | |||
|
|
|||
There was a problem hiding this comment.
📍 实际位置 rtp_llm/device/device_type.py:20(不在 diff 展示范围内,就近挂载)
[P3] PPU 设备识别依赖环境变量真值且无任何日志,CUDA 主机可能被静默重分类
get_device_type() 在 torch.cuda.is_available() 且非 HIP 时,只要 os.environ.get("PPU_HOME") 为任意真值(含 "0"、任意残留路径)或 torch 版本串含 ppu,就返回 DeviceType.Ppu(:20-24),无值内容校验、无日志、无缓存。后果落在本 PR 涉及的分支上:is_cuda() 变为 False,attention/__init__.py 与 fused_moe/__init__.py 从 CUDA 分支跌入 else 兜底(丢掉 headwise/TRT-LLM/XQA/MLA 等 impl 与 fused_moe/__init__.py:106 的 FP4 策略注册),arch.py 的 SM 判定全部转 False,并直接改变本 PR 新加的 qwen3_next.py:248 白名单判定——一台正常 CUDA 机器只因残留环境变量就被静默降级,排查时无任何线索。
建议: 收紧判定并补可观测性:只接受明确的开关值(如要求 PPU_HOME 指向存在的目录,或改用 RTP_LLM_DEVICE_TYPE=ppu 这类显式量),并在返回非 Cuda 结果时用 logger.info 打印判定依据(命中的变量名与取值),使误分类在启动日志中可见;建议对 get_device_type() 加 functools.lru_cache 固化单进程内判定结果,并补一条覆盖「CUDA 可用 + PPU_HOME 为空串/0」的用例。
Checklist: [6.1] 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全;[6.1] 可观测性:日志/指标/超时可操作、非噪声
| get_device_type, | ||
| is_cuda, | ||
| is_hip, | ||
| is_ppu, |
There was a problem hiding this comment.
[P3] arch.py 新增的 is_ppu 自身未使用,仅作为隐式再导出通道
arch.py:6-12 从 rtp_llm.device.device_type 导入 DeviceType/get_device_type/is_cuda/is_hip/is_ppu,但 is_ppu 在本文件内没有任何使用点(全仓 is_ppu 仅 3 个引用:定义处、arch.py:11、qwen3_next.py:242)。真正的消费者 qwen3_next.py:238-243 从 models_py.utils.arch 而非定义模块导入,使 arch.py 成为设备判定的隐式再导出层:既无 __all__ 声明该意图,也让「设备判定归属 rtp_llm.device」这一分层被模糊,后续清理未使用 import 的工具会直接删掉它并打挂 qwen3_next 的设备门禁。
建议: 让 qwen3_next.py 直接从 rtp_llm.device.device_type 导入这些谓词(与 is_cuda/is_hip 的来源保持一致),删除 arch.py 中未使用的导入;若确实希望 arch.py 作为统一门面,请显式声明 __all__ 并在模块注释中说明它对外重导出设备判定函数。
Checklist: [6.1] 分层边界:新概念在正确层级,不泄漏内部
| self._conditions_pass( | ||
| CudaNoQuantCppStrategy, | ||
| create_model_config_without_quant(), | ||
| "no_auant_cpp", |
There was a problem hiding this comment.
[P3] no_auant 拼写错误的策略取值出现第三处副本
新增用例把 "no_auant_cpp" / "no_auant_dp_normal"(no_quant 的拼写错误)作为字面量再复制一份(:179、:188、:197、:206),与 no_quant.py:60/88 及 moe_group_args.py:167-169 的 --moe_strategy choices 构成第三处副本。该取值同时是 CLI 对外可见的字符串,纠正需要同步生产、测试与参数 choices 三处,副本越多越难修。
建议: 把策略名收敛为 Enum 或模块级常量,由 CLI choices 与策略实现共同引用,测试引用同一常量;若要纠正拼写,请在同一变更中保留旧名作为兼容别名并在文档中标注该已知拼写。
Checklist: [P.A] 字符串分发用 Enum/Literal;[I] 同一功能用统一工具函数
| class DeepepNormalRouterHookTest(TestCase): | ||
| @staticmethod | ||
| def _new_router(router_class, quant_dtype): | ||
| router = object.__new__(router_class) |
There was a problem hiding this comment.
[P3] hook 测试白盒构造 router,且伪造 buffer 依赖 dispatch 位置参数下标
_new_router 用 object.__new__(router_class)(:55)跳过 DeepepNormalRouterBase.__init__,手工赋 7 个内部属性,因此 expert_num_per_rank、rank_expert_offset 的真实推导关系(deepep_normal_router.py:68-70)未被验证,测得的 rank_expert_offset=0(:69)是手工设定值——而该值恰好决定 :167-172 的 topk 重映射结果。_TupleDispatchBuffer.dispatch(:23-33)通过 args[5]/args[6] 取 topk_ids/topk_weights,与生产 prepare 的位置实参强耦合:调用顺序调整时 fake 会静默回显错误张量而断言仍通过;__init__ 新增 prepare 依赖的字段时,测试会以 AttributeError 失败而非表达契约破坏。
建议: fake 改用完整命名签名(def dispatch(self, x, handle, num_tokens_per_rank, num_tokens_per_rdma_rank, is_token_in_rank, num_tokens_per_expert, topk_idx, topk_weights, *, expert_alignment)),使参数顺序变更成为调用期错误;或改为保留 __init__、只替换 DeepEPWrapper.get_instance 来构造 router,并至少断言一次由 __init__ 推导的字段(如以 ep_size=2 构造后校验 rank_expert_offset)。
Checklist: [P.G] mock/fake/stub 不得替代本次声称覆盖的生产边界
| return _AcceptingMoeAttributes() | ||
|
|
||
|
|
||
| class BackendAvailabilityGuardTest(unittest.TestCase): |
There was a problem hiding this comment.
[P3] 单个测试文件跨五个模块,且直接改写生产私有状态
该文件挂在 //rtp_llm/model_loader/test 下,却同时覆盖 LinearFactory、StrategyRegistry(:22-25)、quant_config 的 checkpoint 解析(:189)、DeepepWrapperConfig.calc_low_latency_max_token_per_rank(:318)与 pybind QuantAlgo(:491-504)五个模块;BackendAvailabilityGuardTest 还直接赋值生产私有属性 LinearFactory._strategies(:132/135/152/162)。同风险类的兄弟测试 test_compressed_w4a8_int4_per_channel.py 仅一个类、无跨模块导入。后果是这些模块自身的测试目标保持沉默,回归定位会统一指向 model_loader;且对 linear/factory.py:119、strategy_registry.py:89 的错误文案与 _strategies 属性名形成...
建议: 按归属拆分:LinearFactory/StrategyRegistry 的守卫测试移入各自 factory 的 test 目标,DeepEP 分桶测试移入 rtp_llm/models_py/distributed 的 test 目标,quant_config 解析测试移入配置侧 test 目标;本文件只保留 loader 与 dtype/postprocess 用例。私有属性替换建议由 factory 提供测试专用的 set_strategies 或 contextmanager 入口。
Checklist: [6.1] SRP:模块/类职责单一;[6.1] 新逻辑有聚焦单测 + 相关集成/smoke 测试
Add W8A8 configuration parsing, INT8 per-channel loading, dispatch contracts, strategy guards, and fail-fast validation.
Provide backend-neutral registration slots for Linear, Fused MoE, Attention, and repeatable MoE strategy parser choices, with tests and integration documentation.
51a75af to
1f03a0e
Compare
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1323
Status: LGTM
Summary: P0/0 · P1/0 · P2/26 · P3/10
Reviewed: commit 1f03a0e83161 · 2026-08-21 14:39 UTC+8
lgtm ready to ci
Non-blocking Suggestions
P2
- propose_tokens_gpu 两个生产者的形状与取值语义分叉 @
rtp_llm/cpp/model_rpc/DecodeRpcServer.cc:338- 建议:在
SpeculativeExecutorStreamOutput::propose_tokens_gpu声明处写明唯一 shape 契约(「仅下一步单个 draft」还是「全部 propose_step drafts」),把构造逻辑抽成两条通道共用的工具函数;若确为一步语义,同步修正StreamCacheResource.cc的实现与其:234-235注释。兜底可在collectLegacyProposeSlices(:360-375)对各 slice 的numel()一致性加RTP_LLM_CHECK。请补一条 propose_step=1 与 >1 的 gRPC 握手回归用例,断言 GPU 路径与 host 回落取到同一 draft token。
- 建议:在
- 握手快照发布无门控与回滚开关,CPU 回落路径被本 PR 变为不可达 @
rtp_llm/cpp/model_rpc/DecodeRpcServer.cc:353- 建议:给
:338的发布加上与MtpAsyncDeviceState一致的环境变量门控(或独立开关),保留出问题时回退到 CPUtokens的运维手段;或在specUpdate中当draft_token_gpu缺失而draft_token >= 0时主动清空propose_tokens_gpu,让 reader 显式回落。请补一条「GPU 镜像未刷新而 CPU token 已前进」的回归用例。
- 建议:给
- QuantMethod→字符串映射在两处 switch 重复维护且各自漏枚举值 @
rtp_llm/cpp/model_rpc/LocalRpcServer.cc:433- 建议:把枚举到字符串的映射收敛为
QuantInfo.h/cc中唯一一个quantMethodToString()(ModelConfig.cc:53已有同名静态函数可直接上提),两处均改为调用它(precision只需对QuantMethod::None保留"FP16"展示层特例);同时补齐ModelOptFP4与QuarkMXFP4,避免这两类部署的高频轮询接口持续输出 ERROR 并对外上报 UNKNOWN。
- 建议:把枚举到字符串的映射收敛为
- W8A8 部分排除守卫对 re: 单层正则留有静默全层降级缺口 @
rtp_llm/model_loader/compressed_w8a8_int8_per_channel_weight.py:46- 建议:让
re:与具体路径走同一层感知判定:至少代入两个不同层号(如0与1)分别求值,全部命中才认定为整模板排除,否则按部分排除 fail-fast;若能拿到num_layers,在真实层区间上展开模板统计命中层数,全覆盖返回 False、部分覆盖 raise。补「regex 仅命中部分层」与「regex 命中全部层」两条用例。
- 建议:让
- 守卫与 exclude 匹配对 MoE {expert_id} 模板完全失效 @
rtp_llm/model_loader/compressed_w8a8_int8_per_channel_weight.py:43- 建议:把模板→正则的构造统一为「所有
{...}占位符都展开为通配」(复用_exclude_pattern_for并支持任意占位符名),使{expert_id}模板与 MoE 的ignore条目能正常比对;随后守卫对部分排除按 fail-fast、对完整排除走非量化回退。请补一条 MoE{expert_id}模板被ignore覆盖(部分与全部两种)的用例。
- 建议:把模板→正则的构造统一为「所有
- 逐层枚举排除全部层的合法 checkpoint 被误判为部分排除并启动失败 @
rtp_llm/model_loader/compressed_w8a8_int8_per_channel_weight.py:39- 建议:把匹配到的具体层索引收集起来与层数比较:全覆盖返回
False走非量化回退,仅部分覆盖时才raise。若此处确实拿不到层数,退一步改为logging.warning+ 返回False(失败安全)并在文档中声明部分排除不受支持,同时补一条「逐层枚举全部层」的用例。另请把support的quant_config注解从CompressedW8A8Int8PerChannelQuantConfig还原为基类QuantizationConfig——注册表以任意配置调用它,收窄参数类型与实际契约相反,需要窄类型时在 isinstance 判定后用局部变量表达。
- 建议:把匹配到的具体层索引收集起来与层数比较:全覆盖返回
- 共享基类的 re: 语义被改变,但 docstring 声称行为未变且 FP8/Quark 侧无覆盖 @
rtp_llm/model_loader/per_channel_fp8_quant_weight.py:100- 建议:修正 docstring,明确「
re:解释为正则」是本次引入的新语义且适用于所有 per-channel 配置;为 FP8/Quark 配置补一条带re:条目的回归用例固定新语义。同时请确认quant_config.py:381-382只从quant_config["exclude"]填充 FP8 侧exclude_modules(compressed-tensors 实际用ignore)是有意为之——若是遗漏,则被ignore的模块当前会被错误量化,影响面远大于本变更。
- 建议:修正 docstring,明确「
- 正则排除匹配逻辑在两个函数中逐行重复实现 @
rtp_llm/model_loader/per_channel_fp8_quant_weight.py:114- 建议:抽出单一返回「匹配来源」的公开模块级函数(如返回
None/LITERAL/REGEX),两处共用;_ckpt_base_matches_quant_exclude内只保留通配分支并调用该函数;把跨模块使用的 helper 提升为非下划线公开名,并让 W8A8support复用一次计算结果而不是先自查再交给基类重算。
- 建议:抽出单一返回「匹配来源」的公开模块级函数(如返回
- dispatch 元组返回契约脱离 use_fp8,scale 布局与 topk 重映射不自洽 @
rtp_llm/models_py/modules/factory/fused_moe/impl/cuda/routers/deepep_normal_router.py:167- 建议:让扩展点显式声明是否携带 scale(新增子类属性或
_dispatch_returns_scale()钩子),base 侧对「未声明携带 scale 却收到 tuple」继续 fail-fast,并恢复非 tuple 分支的张量类型校验;把:167的重映射条件从not use_fp8改为「dispatch 未返回 scale」以与新判定对齐,同时在_prepare_dispatch_inputdocstring 中写明 scale 布局与 topk 重映射契约。补一个recv_topk_idx含 -1 的 tuple-dispatch 用例,锁定 padding token 的 expert id 与 scale 形状。
- 建议:让扩展点显式声明是否携带 scale(新增子类属性或
- PPU 无 in-tree attention 分支,落入 CUDA else 分支且 hook 在其后才运行 @
rtp_llm/models_py/modules/factory/attention/__init__.py:137- 建议:为
DeviceType.Ppu增加显式分支(可为空,仅依赖 hook 注册),或把 else 分支的 flashinfer 导入收窄为 Cuda/未知设备并允许无 in-tree 实现的设备跳过;同时在 allowlist 通过后追加一次后端就绪校验(确认PREFILL_MHA_IMPS/ MoE registry 中存在 PPU 实现),缺失时给出「需要安装或注册 PPU backend」的可操作错误。建议顺带在get_device_type()首次判定处打一条 INFO 记录判定依据。
- 建议:为
- ROCm EP 支持的量化方法存在两处真值来源,被删除的方法名仍被 executor/router 接受 @
rtp_llm/models_py/modules/factory/fused_moe/impl/rocm/strategy/ep.py:25- 建议:用一个
Dict[Optional[str], Tuple[executor_class, quant_config_factory]]把映射定义一次,_SUPPORTED_QUANT_METHODS由其 keys 派生;并同步清理或注释说明rocm_moe.py与 ROCmpure_tp_router.py中已不可由get_method()产生的同名分支。mxfp4-quark在 ROCm EP 上由「静默回落 BF16 executor」改为「策略不可选」是改善,但属行为变化,请在 PR description 中显式说明。
- 建议:用一个
- ROCm EP 量化过滤的唯一用例在其实际运行的 CUDA 机型上恒真 @
rtp_llm/models_py/modules/factory/fused_moe/tests/test_cuda_strategies.py:214- 建议:改为断言过滤动作本身而非最终布尔值:用
patch.object(RocmEpNormalStrategy, "get_attributes")记录调用次数,断言不受支持方法下未被调用、受支持方法(如quant_config=None、Fp8BlockWiseQuantConfig)下被调用一次,形成正反对照。并把 ROCm 用例移到不带open_skip、不申请 GPU 的 CPU-only target(utils/test/BUILD:218-226已有该模式),避免 CUDA target 顶层导入impl/rocm/strategy包初始化。
- 建议:改为断言过滤动作本身而非最终布尔值:用
- 设备无关的中心工厂硬编码 W8A8 后端专属文案,量化方法串三处字面量消费 @
rtp_llm/models_py/modules/factory/fused_moe/strategy_registry.py:87- 建议:把提示语通用化:在通用报错中带上
quant_method与当前已注册策略类名列表,并提示「可能缺少对应后端的计算实现」,无需枚举具体方法名;若必须保留特判,请引用CompressedW8A8Int8PerChannelQuantConfig.get_method()而非字面量,或把说明文本下沉到后端注册扩展点。
- 建议:把提示语通用化:在通用报错中带上
- 一次性槽位在 hook 失败后被整体重置,重试会重放已成功的 hook 且重新放开迟到登记 @
rtp_llm/utils/backend_registry.py:136- 建议:明确失败语义并写入文档:要么按 hook 粒度记录完成状态(重试只补跑未完成项),要么保留
_started让该槽位永久失败(与「注册失败即启动失败」的初衷一致);同时新增「已开始但失败」状态以继续拒绝迟到登记,并在文档中要求 hook 自身幂等。
- 建议:明确失败语义并写入文档:要么按 hook 粒度记录完成状态(重试只补跑未完成项),要么保留
- 注册机制的并发与重入协议无任何测试,等待既无超时也无可观测性 @
rtp_llm/utils/backend_registry.py:119- 建议:补两条纯同步用例(hook 内递归调用同槽位断言
RuntimeError("re-entrant");hook 内调用reset_backend_registrations()断言cannot reset)与一条双线程用例(hook 内阻塞占位,第二线程断言等待后正常返回、占位失败时收到含 "retry is allowed" 的RuntimeError且__cause__为原异常)。同时给event.wait()加超时并输出槽位名与 owner 线程;若并发路径确实不可达,建议直接用with _lock:删掉_inflight/Event这层投机抽象(YAGNI),并相应修正文档措辞。
- 建议:补两条纯同步用例(hook 内递归调用同槽位断言
- 槽位名与上下文键为自由字符串 + kwargs 透传,测试用的槽位名与生产不一致 @
rtp_llm/utils/test/backend_registry_test.py:34- 建议:把四个槽位名与其 context 参数收敛为模块级常量或
Literal/Enum(例如BackendSlot.FUSED_MOE),register_backend_hook对未知槽位 fail-fast;测试中把"moe"/"parser"换成从生产模块导入的槽位常量,使名字漂移可被捕获。
- 建议:把四个槽位名与其 context 参数收敛为模块级常量或
- moe_strategy_choices 槽位以整个 parser 为上下文,把 argparse 私有结构固化为跨仓契约 @
rtp_llm/server/server_args/moe_group_args.py:201- 建议:把
moe_group.add_argument("--moe_strategy", ...)的返回值存为局部 action 变量,改为传精确上下文(如add_moe_strategy_choice=<闭包,内部做去重与类型校验>),由moe_group_args.py内部完成查找,把 argparse 细节封闭在公共 parser 模块内;同步统一文档示例与测试 hook 的写法。若为兼容已上线外部 hook 必须保留parser=,请在文档槽位表中显式声明「依赖_actions属私有契约」并给出迁移期限。
- 建议:把
- 测试裸调 reset_backend_registrations 会永久丢弃同进程内已登记的真实 backend hook @
rtp_llm/server/server_args/test/server_args_test.py:37- 建议:不要在共享进程里裸调
reset_backend_registrations()。建议在用例内对三个容器做「快照—恢复」(如patch.dict分别替换),或由backend_registry提供返回上下文管理器的隔离入口;并让reset_backend_registrations()一并调用import_optional_internal_source_entrypoint.cache_clear(),使 reset 成为真正可恢复的操作。另请补一条只保留最底层 mock(向sys.modules注入假的入口模块)的用例:当前用例同时 patch 了server_args.ensure_backend_entrypoint_loaded(:41-45)与backend_registry.ensure_backend_entrypoint_loaded(:46-49),真实入口发现路径全程未执行。
- 建议:不要在共享进程里裸调
- FP8 per-tensor compressed 既有路径把 input_activations.dynamic 从必填放宽为默认 False @
rtp_llm/config/quant_config.py:257- 建议:对
dynamic保持必填:改为显式判断(if "dynamic" not in activation_config: raise ValueError(...))后再取值,或沿用下标访问。若确实要给默认值,请在注释中写明该默认对应 compressed-tensors 规范的哪一条,并补一个「缺失 dynamic」的解析用例锁定预期。
- 建议:对
- compressed-tensors targets 校验按组名不对称,同内容 checkpoint 结果取决于组名 @
rtp_llm/config/quant_config.py:55- 建议:若目标是「不改变今天能加载的 checkpoint」,请在
group_0分支加logging.warning说明targets未被校验、按全模型量化处理并给出失效表现,避免同内容不同组名结果不一致且无任何提示;若可接受收紧,则对两条分支统一调用_validate_compressed_targets,并在 PR description 中列出会因此启动失败的 checkpoint 形态。
- 建议:若目标是「不改变今天能加载的 checkpoint」,请在
- server 库新增模块级依赖但只在测试 target 补了 //rtp_llm:utils @
rtp_llm/server/server_args/moe_group_args.py:2- 建议:在
rtp_llm/server/BUILD的py_library(name = "server")deps 中直接加入//rtp_llm:utils,让声明与实际 import 对齐;测试 target 的显式依赖可以保留。
- 建议:在
- 用源码文本计数替代 dtype 行为断言,漏掉两处位置传参调用点 @
rtp_llm/model_loader/test/test_compressed_w8a8_int8_per_channel.py:504- 建议:删除源码文本计数,改为参数化行为覆盖:用
subTest遍历w8a8_weight_list全部 weight key,对 INT8 与 FP8 两种配置分别构建 weight 并断言kernel.data_type == cls.weight_dtype、scale.data_type == torch.float32,覆盖全部_get_*_quant_weight分支且不受格式影响。
- 建议:删除源码文本计数,改为参数化行为覆盖:用
- 工厂「缺少计算后端」守卫用空注册表自证,未验证真实注册表不会误认领 @
rtp_llm/model_loader/test/test_compressed_w8a8_int8_per_channel.py:152- 建议:把两组守卫用例移到
.../factory/linear/test与.../factory/fused_moe/tests下对应目标;断言改为在真实注册表上传入 W8A8 配置,验证没有任何 in-tree 策略认领它(候选集为空)并抛出该专用错误,从而同时锁住「文案正确」与「无策略误认领」两个方向。
- 建议:把两组守卫用例移到
- DeepEP dispatch stub 用魔法位置下标耦合生产签名,签名变化时测试静默通过 @
rtp_llm/models_py/modules/factory/fused_moe/impl/cuda/routers/test/deepep_normal_router_hook_test.py:26- 建议:把 stub 的
dispatch改为按生产签名声明具名参数(或先assertEqual(len(args), 7)再取值),并补断言payload.expert_topk_ids/expert_topk_weights与输入的对应关系;把两个裸assert换成raise AssertionError/self.fail()等不受-O影响的形式。
- 建议:把 stub 的
- W8A8 新增的 act_qscheme 与 worker precision 映射零测试覆盖 @
rtp_llm/cpp/engine_base/Executor.h:64- 建议:补一条 C++ 单测:构造
quant_algo.setQuantAlgo("w8a8_int8_per_channel", 8, 0)的ModelConfig,断言Executor::genModelDescription(...)得到的act_qscheme == QScheme::Qint8PerToken;并为LocalRpcServer::getWorkerStatusInfo的 precision 映射补一条覆盖W8A8INT8PTPC与ModelOptFP4/QuarkMXFP4的表驱动用例,同时钉住不落 default(可与统一后的quantMethodToString()一起做)。
- 建议:补一条 C++ 单测:构造
- Mega-PR:MTP/PD handoff 与 CUDA Graph 修复混入 W8A8 量化与后端注册特性 @
rtp_llm/cpp/pybind/ConfigInit.cc:1163- 建议:按方向拆分为独立 PR:后端注册机制(含文档与测试)、W8A8 INT8 量化链路、MTP propose token 修复、CUDA graph 清零、PPU 放行。若因发布节奏必须合并,请在 PR description 中按方向分节说明动机、影响面与各自的回滚方式(当前 description 未做此说明)。
P3
- host 侧全表同步清零落在 decode 热路径,且与 tagged 分支的守卫强度不一致 @
rtp_llm/cpp/cuda_graph/cuda_graph_runner.cc:397- 建议:用
RTP_LLM_PROFILE_SCOPE包住该清零以便归因,并在注释中给出量级(max_bs × max_blocks × 4B);若实测占比可观,可只清[current_bs, graph_bs)行加上有效行的列尾区间。把 host/device 两侧清零收敛到一个 helper 以统一守卫与不变量。同时在rtp_llm/cpp/cuda_graph/tests/cuda_graph_decode_padding.py中补「先大 batch 再小 batch 两次 replay」用例,断言第二次 prepare 后 host 镜像[current_bs, graph_bs)行全为 0,tagged 场景补同类断言。
- 建议:用
- 三个 hook 消费点对设备导入失败的处理不一致 @
rtp_llm/models_py/modules/factory/linear/__init__.py:27- 建议:统一三处的失败语义:要么都 fail-fast(推荐,与
backend_registry模块 docstring「注册失败即启动失败」的取向一致),要么都降级并在文档职责表中写明降级后 factory 可能为空、以及此时期望的报错位置。若保留linear的宽松行为,请把warning提为error并带上 device_type 与异常类型。
- 建议:统一三处的失败语义:要么都 fail-fast(推荐,与
- no-quant 策略新增校验与 executor 既有条件重复,且新测试绕过 can_handle @
rtp_llm/models_py/modules/factory/fused_moe/impl/cuda/strategy/no_quant.py:58- 建议:二选一:删除策略侧重复校验,改由 executor
check_conditions单点负责,测试改为经can_handle()断言选择结果;或保留策略侧校验并在注释中说明它是为「executor 被替换后仍不误认领量化 checkpoint」而存在的独立护栏。无论哪种,请补moe_strategy="auto"的用例。
- 建议:二选一:删除策略侧重复校验,改由 executor
- setup_args 中的入口预加载与 run_backend_registrations 内部自加载重复 @
rtp_llm/server/server_args/server_args.py:534- 建议:二选一:删除该显式调用,让加载时序完全由
run_backend_registrations负责,测试改为只 mock 最底层的import_optional_internal_source_entrypoint;或保留并把注释改成「防御性预加载:即使未来出现更早消费槽位的参数组也能保证顺序」,明确它是冗余但有意的护栏。
- 建议:二选一:删除该显式调用,让加载时序完全由
- choices 扩展点只保护 argparse 校验通道,env 补齐通道静默绕过 @
rtp_llm/server/server_args/server_args.py:366- 建议:让 env 回填通道复用同一校验:回填前查对应 action 的
choices并在不匹配时报可读错误。同时补两条用例:不注册 hook 时--moe_strategy external_test_strategy触发SystemExit;以及经MOE_STRATEGY注入未知取值时被拒绝。若暂不修,请把「有 CLI 参数时 env 回填不校验 choices」记入文档已知行为。
- 建议:让 env 回填通道复用同一校验:回填前查对应 action 的
- 量化方案以硬编码字符串元组在中心函数集中分派 @
rtp_llm/models_py/distributed/deepep_wrapper.py:230- 建议:在
QuantizationConfig上增加is_per_act_token()(或low_latency_token_buckets())由各配置类自行声明,该函数改为查询该能力;若暂不重构,至少把字符串集合提为模块级常量并注明与get_method()的同步要求。
- 建议:在
- 设备判定函数经 models_py.utils.arch 隐式 re-export @
rtp_llm/models/qwen3_next/qwen3_next.py:238- 建议:直接从
rtp_llm.device.device_type导入这四个符号;若确实需要arch.py作为设备门面,请在其中声明__all__并注明门面职责,避免被当作未使用导入清理掉。
- 建议:直接从
- LoadQuantPerChannelFp8Weight 绕过了新引入的 supported_quant_config_types 单一来源 @
rtp_llm/model_loader/per_channel_fp8_quant_weight.py:733- 建议:让该子类也通过
cls.supported_quant_config_types判定(必要时在子类上覆写该属性),使「认领哪些配置」只有一个声明位置。
- 建议:让该子类也通过
- GenerateStream 新用例只覆盖「缺失则保留」,未覆盖「提供则刷新」 @
rtp_llm/cpp/engine_base/stream/test/GenerateStreamTest.cc:117- 建议:补一条对称用例:构造
draft_token_gpu = torch::tensor({11}, torch::kInt32).to(torch::kCUDA),调用specUpdate后断言propose_tokens_gpu.cpu().item<int32_t>() == 11(从 9 刷新为 11);并补一条 commit-only(propose_token_清空)路径的边界断言。
- 建议:补一条对称用例:构造
- backend_registry 生命周期异常断言未校验消息,且 noop 用例无任何断言 @
rtp_llm/utils/test/backend_registry_test.py:66- 建议:三条异常用例改用
assertRaisesRegex钉住各自消息关键片段(如"already initialised"/"re-entrant"/"lifecycle changed");noop 用例补断言(例如随后register_backend_hook应因槽位已开始而 raise,以证明状态确实被记录)。
- 建议:三条异常用例改用
Checklist Findings (25 fail / 55 total)
General Principles Checklist
- [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue
设备判定函数经 models_py.utils.arch 隐式 re-export
改动前is_hip直接来自rtp_llm.device.device_type,改动后get_device_type/is_cuda/is_hip/is_ppu全部改从rtp_llm.models_py.utils.arch导入(:238-243)。arch.py:6-12只是from rtp_llm.device.device_type import ...的透传,其中get_device_type/is_hip/is_ppu/DeviceType在该模块内均未被使用,也未声明__all__,属隐式 re-export:任何 unused-import 清理(autoflake / ruff F401)都会让本文件运行时 ImportError,mypy 严格模式同样报错;同一批符号出现两个可用导入路径,后续容易分叉。 - [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
choices 扩展点只保护 argparse 校验通道,env 补齐通道静默绕过
扩展点的前提是「合法取值由--moe_strategy的 choices 收口」。但server_args.py:343-371在存在任意命令行参数时(has_cmd_args为真)对未经 CLI 提供的 dest 用setattr(parsed_args, dest, ...)回填 env 值,完全绕过 choices 校验;只有「零命令行参数」分支(:263-291)以--flag value形式回灌因而受校验。于是MOE_STRATEGY=<未注册策略>在混合部署下既不会被拒绝也不会有任何提示,扩展点在该场景形同虚设。新测试也未覆盖「未注册任何 backend 时未知 strategy 应被拒绝」这一反向路径。 - [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue
设备判定函数经 models_py.utils.arch 隐式 re-export
改动前is_hip直接来自rtp_llm.device.device_type,改动后get_device_type/is_cuda/is_hip/is_ppu全部改从rtp_llm.models_py.utils.arch导入(:238-243)。arch.py:6-12只是from rtp_llm.device.device_type import ...的透传,其中get_device_type/is_hip/is_ppu/DeviceType在该模块内均未被使用,也未声明__all__,属隐式 re-export:任何 unused-import 清理(autoflake / ruff F401)都会让本文件运行时 ImportError,mypy 严格模式同样报错;同一批符号出现两个可用导入路径,后续容易分叉。 - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
host 侧全表同步清零落在 decode 热路径,且与 tagged 分支的守卫强度不一致
kv_cache_kernel_block_id形状为[max_bs_, max_blocks](:939-940),新增的fill_(0)每次 replay 准备都会同步 memset 整表,在大 max_bs / max_seq_len 部署下是数百 KB 级 CPU 写,位于 decode 每步关键路径;与 device 侧不同——device 清零走invokeCudaGraphPrepareFill异步 kernel 且有RTP_LLM_PROFILE_SCOPE(:333),host 这一行既是同步阻塞又落在任何 profile scope 之外。同时该行对字段无条件调用,而同文件 tagged 分支先判defined() && !is_cuda()再zero_()(:473-476):同一不变量在相邻代码里被表述为两种强度。修复本身也无任何断言。 - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
PPU 无 in-tree attention 分支,落入 CUDA else 分支且 hook 在其后才运行
qwen3_next.py:248把 PPU 加入 Qwen3Next 允许列表(改前非 cuda/rocm 立即RuntimeError),使该组合成为受支持路径。但 attention 工厂是if ROCm(:32) / elif Cuda(:52) / else(:137),PPU 落到else::138-154无条件from ...cuda_impl.py_flashinfer_mha import ...与cuda_cp_impl.prefill_cp_flashinfer import CPFlashInferImpl,并把 CUDA 实现注册进PREFILL_MHA_IMPS;run_backend_registrations("attention", ...)在:159,其后才运行。PPU 环境无 flashinfer 时模块导入即失败、hook 永不执行;有 flashinfer 而树外 backend 未加载时,则带着 CUDA 实现继续走下去。另device_type.py:20-24由 `PP - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
测试裸调 reset_backend_registrations 会永久丢弃同进程内已登记的真实 backend hook
用例在:37与finally(:64)各调一次reset_backend_registrations(),清空进程级_hooks/_started/_repeatable/_failures(backend_registry.py:148-156)。但入口由import_optional_internal_source_entrypoint加载,该函数带@lru_cache(maxsize=None)(import_util.py:26)且模块体被sys.modules缓存,reset 后ensure_backend_entrypoint_loaded()是 no-op,被清掉的 hook 无法重新登记。若同进程中先有任何路径触发过入口导入,本用例结束后linear/fused_moe/attention槽位的真实 hook 即永久丢失——正是backend_registry.py:28-31自述要避免的「静默丢弃注册导致 Factory 选到另一实现,表现为错误数值」。 - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
choices 扩展点只保护 argparse 校验通道,env 补齐通道静默绕过
扩展点的前提是「合法取值由--moe_strategy的 choices 收口」。但server_args.py:343-371在存在任意命令行参数时(has_cmd_args为真)对未经 CLI 提供的 dest 用setattr(parsed_args, dest, ...)回填 env 值,完全绕过 choices 校验;只有「零命令行参数」分支(:263-291)以--flag value形式回灌因而受校验。于是MOE_STRATEGY=<未注册策略>在混合部署下既不会被拒绝也不会有任何提示,扩展点在该场景形同虚设。新测试也未覆盖「未注册任何 backend 时未知 strategy 应被拒绝」这一反向路径。 - [6.1] Quality — Commit 原子、message 与行为匹配 → issue
Mega-PR:MTP/PD handoff 与 CUDA Graph 修复混入 W8A8 量化与后端注册特性
41 个文件承载至少四组无共同动机的变更:(1) W8A8 INT8 量化链路(quant_config.py、QuantInfo.*、ConfigInit.cc、ModelConfig.cc、LocalRpcServer.cc、.pyi、model_loader、工厂特判);(2) 树外后端延迟注册机制(backend_registry.py+ 四个槽位 + 文档 +server_args);(3) MTP 投机握手propose_tokens_gpu(DecodeRpcServer.cc、GenerateStream.cc)与 CUDA graph host block table 清零(cuda_graph_runner.cc);(4) PPU 设备放行(device_type.py、arch.py、qwen3_next.py)。packet 的risk_tags达 13 个并触发large_pr_route。后果:任一方向出问题需整体回滚,二分定位与回滚粒度被放大。 - [6.1] Quality — Mega-PR 已拆分为独立变更 → issue
Mega-PR:MTP/PD handoff 与 CUDA Graph 修复混入 W8A8 量化与后端注册特性
41 个文件承载至少四组无共同动机的变更:(1) W8A8 INT8 量化链路(quant_config.py、QuantInfo.*、ConfigInit.cc、ModelConfig.cc、LocalRpcServer.cc、.pyi、model_loader、工厂特判);(2) 树外后端延迟注册机制(backend_registry.py+ 四个槽位 + 文档 +server_args);(3) MTP 投机握手propose_tokens_gpu(DecodeRpcServer.cc、GenerateStream.cc)与 CUDA graph host block table 清零(cuda_graph_runner.cc);(4) PPU 设备放行(device_type.py、arch.py、qwen3_next.py)。packet 的risk_tags达 13 个并触发large_pr_route。后果:任一方向出问题需整体回滚,二分定位与回滚粒度被放大。 - [6.1] Quality — PR description 说明动机与设计 → issue
Mega-PR:MTP/PD handoff 与 CUDA Graph 修复混入 W8A8 量化与后端注册特性
41 个文件承载至少四组无共同动机的变更:(1) W8A8 INT8 量化链路(quant_config.py、QuantInfo.*、ConfigInit.cc、ModelConfig.cc、LocalRpcServer.cc、.pyi、model_loader、工厂特判);(2) 树外后端延迟注册机制(backend_registry.py+ 四个槽位 + 文档 +server_args);(3) MTP 投机握手propose_tokens_gpu(DecodeRpcServer.cc、GenerateStream.cc)与 CUDA graph host block table 清零(cuda_graph_runner.cc);(4) PPU 设备放行(device_type.py、arch.py、qwen3_next.py)。packet 的risk_tags达 13 个并触发large_pr_route。后果:任一方向出问题需整体回滚,二分定位与回滚粒度被放大。 - [6.1] Software Engineering — DIP:高层策略不依赖非必要具体细节 → issue
moe_strategy_choices 槽位以整个 parser 为上下文,把 argparse 私有结构固化为跨仓契约
槽位语义只需往--moe_strategy的choices追加一个值,但 context 传的是整个 top-level parser。docs/backend/backend_registration.md:107-115与server_args_test.py:28-32都必须遍历 argparse 私有属性parser._actions、按"--moe_strategy"字面量匹配再改choices;两处写法还不一致(文档重建 list 且做幂等去重、找不到时raise RuntimeError,测试用裸next()+ 就地.choices.append(...),参数改名会抛裸StopIteration)。hook 拿到 parser 后可改写任意参数,作用域远超声明语义;--moe_strategy改用子解析器或 choices 改为 tuple/Enum 会让所有外部 hook 同时静默失效。 - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
LoadQuantPerChannelFp8Weight 绕过了新引入的 supported_quant_config_types 单一来源
本 PR 引入supported_quant_config_types作为「本 loader 认领哪些配置」的单一来源(:309-315注释明确说明它保证WeightModule.create只匹配一个 loader),基类support(:321-323)与 INT8 子类都据此判定;但同一继承链上的LoadQuantPerChannelFp8Weight.support(:729-738)仍直接写isinstance(quant_config, Fp8PerChannelCompressedQuantConfig),未使用该类属性。新增第三种 per-channel 配置时,这一处会被漏改,且漏改的表现是 loader 冲突(weight_module.py:80报错)或静默不认领。 - [6.1] Software Engineering — ISP:调用方不依赖无关大接口 → issue
moe_strategy_choices 槽位以整个 parser 为上下文,把 argparse 私有结构固化为跨仓契约
槽位语义只需往--moe_strategy的choices追加一个值,但 context 传的是整个 top-level parser。docs/backend/backend_registration.md:107-115与server_args_test.py:28-32都必须遍历 argparse 私有属性parser._actions、按"--moe_strategy"字面量匹配再改choices;两处写法还不一致(文档重建 list 且做幂等去重、找不到时raise RuntimeError,测试用裸next()+ 就地.choices.append(...),参数改名会抛裸StopIteration)。hook 拿到 parser 后可改写任意参数,作用域远超声明语义;--moe_strategy改用子解析器或 choices 改为 tuple/Enum 会让所有外部 hook 同时静默失效。 - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
setup_args 中的入口预加载与 run_backend_registrations 内部自加载重复
run_backend_registrations()(backend_registry.py:82)在取锁之前已无条件调用ensure_backend_entrypoint_loaded();全仓搜索确认setup_args路径上唯一的槽位消费者是moe_group_args.py:201,它在紧随其后的init_all_group_args(server_args.py:544)内被调用,没有更早的参数组消费槽位。因此:534这次显式调用在当前代码下不产生额外效果,注释(:532-533)所称的顺序保证由被调方自身提供。副作用是新用例完全建立在这个冗余调用之上(通过 patch 它注入 hook),使「删除冗余调用」这一无害重构会直接打破测试。 - [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue
LoadQuantPerChannelFp8Weight 绕过了新引入的 supported_quant_config_types 单一来源
本 PR 引入supported_quant_config_types作为「本 loader 认领哪些配置」的单一来源(:309-315注释明确说明它保证WeightModule.create只匹配一个 loader),基类support(:321-323)与 INT8 子类都据此判定;但同一继承链上的LoadQuantPerChannelFp8Weight.support(:729-738)仍直接写isinstance(quant_config, Fp8PerChannelCompressedQuantConfig),未使用该类属性。新增第三种 per-channel 配置时,这一处会被漏改,且漏改的表现是 loader 冲突(weight_module.py:80报错)或静默不认领。 - [6.1] Software Engineering — OCP:本地扩展点优先于修改中心逻辑 → issue
量化方案以硬编码字符串元组在中心函数集中分派
calc_low_latency_max_token_per_rank通过quant_config.get_method() in (...)的五元素字符串元组(:230-236)判断 per-act-token,本次为 W8A8 追加第五个字符串(与quant_config.py:945的返回值一致)。每新增一个 per-token 方案都必须回到这个中心函数改一行,且字符串与get_method()的对应关系没有类型层面约束——拼错只会静默落到错误分桶或raise ValueError("Unsupported quantization config"),而 token bucket 直接决定 DeepEP low-latency 缓冲尺寸。 - [6.1] Software Engineering — SRP:模块/类职责单一 → issue
工厂「缺少计算后端」守卫用空注册表自证,未验证真实注册表不会误认领
BackendAvailabilityGuardTest(:130-180)测的是LinearFactory与StrategyRegistry(models_py/modules/factory)的行为,却挂在 model_loader 的测试目标下,改动 linear/moe 工厂的人不会想到运行它。手法是直写私有类属性LinearFactory._strategies = [](:152、:162)与新建空StrategyRegistry()(:173、:176),因此断言的只是「候选为空时的错误文案」,与生产环境中由fused_moe/__init__.py:120、linear/__init__.py:31填充的真实注册表对 W8A8 的判定无关——即使将来某个 CUDA 策略错误声称能处理 W8A8,这两条用例仍会通过。 - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
W8A8 新增的 act_qscheme 与 worker precision 映射零测试覆盖
全仓搜索W8A8INT8PTPC/isW8a8Int8PTPC的消费者只有Executor.h:64-65(act_qscheme = QScheme::Qint8PerToken)、LocalRpcServer.cc:433-434(对外precision字段)、ModelConfig.cc:75-76与 pybind 绑定;唯一测试QuantAlgoBindingTest(test_compressed_w8a8_int8_per_channel.py:514-524)只断言枚举、bits 与 groupwise,未覆盖 act_qscheme 选择与 precision 字符串。若Executor.h的 else-if 被插到isFp8PTPC()之前的错误位置或误写为Qfp8PerToken,激活量化方案与权重 INT8 不匹配,只会表现为数值错误;precision是对外 gRPC 字段取值域,改错同样无 CI 拦截。 - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
backend_registry 生命周期异常断言未校验消息,且 noop 用例无任何断言
test_repeatable_slot_rejects_late_hooks(:65-69)、test_slot_lifecycle_cannot_change_after_start(:71-75)与test_registering_after_drain_raises(:98-104)都只用assertRaises(RuntimeError),不校验消息。backend_registry.py里三条RuntimeError文案各不相同(:66-69迟到登记、:92-94重入、:98-100生命周期变更),任一分支被误改成另一分支的抛出点,三条用例仍全绿。test_draining_slot_without_hooks_is_noop(:141-142)只调用一次函数、无任何断言,不区分「正常 no-op」与「静默吞掉后续异常」。 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
GenerateStream 新用例只覆盖「缺失则保留」,未覆盖「提供则刷新」
本次把GenerateStream.cc:1035的无条件赋值改为if (update_info.draft_token_gpu.defined()),引入两个分支。新增用例mtpUpdateKeepsLastGpuProposalWhenNextProposalIsMissing(:117-137)只传 undefined 张量,断言旧值 9 被保留。全仓搜索propose_tokens_gpu后确认:没有任何测试通过specUpdate断言「传入 defined 张量时会刷新为新值」——MtpBatchStreamProcessorTest.cc各处都直接对state/sp_output_buffer赋值,绕过specUpdate。若该判断被误写为恒假(propose_tokens_gpu永不刷新,每步都吃上一轮 proposal),全部测试仍会通过,而这正是 MTP gather 读到过期 GPU proposal 的回归形态。
RTP-LLM Checklist
- [I] 代码质量 — 同一功能用统一工具函数 → issue
LoadQuantPerChannelFp8Weight 绕过了新引入的 supported_quant_config_types 单一来源
本 PR 引入supported_quant_config_types作为「本 loader 认领哪些配置」的单一来源(:309-315注释明确说明它保证WeightModule.create只匹配一个 loader),基类support(:321-323)与 INT8 子类都据此判定;但同一继承链上的LoadQuantPerChannelFp8Weight.support(:729-738)仍直接写isinstance(quant_config, Fp8PerChannelCompressedQuantConfig),未使用该类属性。新增第三种 per-channel 配置时,这一处会被漏改,且漏改的表现是 loader 冲突(weight_module.py:80报错)或静默不认领。
Python Static-First Checklist
- [P.A] 静态结构与类型纪律 — 公开 API 禁止 **kwargs 透传 → issue
槽位名与上下文键为自由字符串 + kwargs 透传,测试用的槽位名与生产不一致
register_backend_hook(slot: str, ...)与run_backend_registrations(slot, **context)的槽位名和上下文键都是自由字符串,无 Enum/Literal 约束,也无「未知槽位」校验:外部后端把"fused_moe"拼成"moe"时不会报错,只会静默不执行(正是模块 docstring:28-31声称要避免的失败形态)。测试恰好演示了这一点::36-39用"moe"、:66-75用"parser",而生产槽位是"fused_moe"(fused_moe/__init__.py:120)与"moe_strategy_choices"(moe_group_args.py:201)。这两个真实槽位名及其 context 键在测试中没有约束点,改名不会被 CI 捕获。 - [P.A] 静态结构与类型纪律 — 字符串分发用 Enum/Literal → issue
量化方案以硬编码字符串元组在中心函数集中分派
calc_low_latency_max_token_per_rank通过quant_config.get_method() in (...)的五元素字符串元组(:230-236)判断 per-act-token,本次为 W8A8 追加第五个字符串(与quant_config.py:945的返回值一致)。每新增一个 per-token 方案都必须回到这个中心函数改一行,且字符串与get_method()的对应关系没有类型层面约束——拼错只会静默落到错误分桶或raise ValueError("Unsupported quantization config"),而 token bucket 直接决定 DeepEP low-latency 缓冲尺寸。 - [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
no-quant 策略新增校验与 executor 既有条件重复,且新测试绕过 can_handle
no_quant.py:58(CudaNoQuantCppStrategy)与:86(CudaNoQuantDpNormalStrategy)新增的checker.check(quant_method is None)与TritonFusedMoeExecutor.check_conditions(triton_fused_executor.py:36-38的not resolver.has_quantization(config))等价,而基类can_handle(strategy_base.py:71-77)本来就会对 strategy/router/executor 逐个调用check_conditions,故新增校验在生产路径上是冗余防御。新测试TestCudaNoQuantFallbackStrategies(test_cuda_strategies.py:159-208)直接调strategy.check_conditions(checker, config)而不经can_handle(),因此只能 - [P.G] 测试规范 — pytest.raises 带 match 参数 → issue
backend_registry 生命周期异常断言未校验消息,且 noop 用例无任何断言
test_repeatable_slot_rejects_late_hooks(:65-69)、test_slot_lifecycle_cannot_change_after_start(:71-75)与test_registering_after_drain_raises(:98-104)都只用assertRaises(RuntimeError),不校验消息。backend_registry.py里三条RuntimeError文案各不相同(:66-69迟到登记、:92-94重入、:98-100生命周期变更),任一分支被误改成另一分支的抛出点,三条用例仍全绿。test_draining_slot_without_hooks_is_noop(:141-142)只调用一次函数、无任何断言,不区分「正常 no-op」与「静默吞掉后续异常」。
Strengths
QuantMethod新值在枚举尾部追加(QuantInfo.h:19W8A8INT8PTPC = 12),既有编号不位移;六个消费点QuantInfo.cc:49、QuantInfo.h:54、Executor.h:64、ModelConfig.cc:75、LocalRpcServer.cc:433、ConfigInit.cc:1163/1180与.pyi:1419/1432全部同步。QuantAlgoBindingTest(test_compressed_w8a8_int8_per_channel.py:514-524)用真实rtp_llm.ops.QuantAlgo而非 mock 钉住 Python → C++ 字符串契约:w8a8_int8_per_channel→W8A8INT8PTPC、bits=8、group_size=0、非 groupwise,与QuantInfo.cc:49-52完全一致,是本 PR 质量最高的一组测试。PerChannelFp8Weight通过weight_dtype/apply_fp8_device_conversion/supported_quant_config_types三个类属性参数化(:292-315),让 INT8 子类零复制复用全部 tensor 映射与 TP/EP 切分规则。apply_fp8_device_conversion=False是必需门控而非装饰:ROCm 的convert_fp8_weight_params带assert weight.dtype == torch.float8_e4m3fn,INT8 走该路径会直接断言失败。supported_quant_config_types让基类与 INT8 子类的 isinstance 判定互斥(子类super().support()中cls仍是子类),规避了weight_module.py:80的len(valid_classes) > 1报错,loader 唯一性成立。- 未识别的 compressed-tensors 组合改为抛出携带
weights/input_activations内容的ValueError(quant_config.py:315-323)。原路径会落到通用尾部实例化抽象基类并抛难以定位的TypeError,这是严格的错误语义改善。 CompressedW8A8Int8PerChannelQuantConfig.__init__全部具名参数、无**kwargs兜底,_from_config用allowed_keys白名单显式拒绝未知键(quant_config.py:966-979);get_supported_kv_cache_dtypes()(:958-964)主动收窄为 fp16/bf16 并在注释中说明理由,把未验证的--fp8_kv_cache组合挡在启动期。_pick_config_group的兼容性设计克制:group_0存在时行为与改动前逐字节一致(:32-44),多 group 时补 warning,回退只覆盖原先根本无法读取的「单一自定义命名 group」。backend_registryhook 严格在锁外执行(:127-133),backend_registry_test.py:131-139直接断言_lock._is_owned() == False,避免 hook 内 import 与注册锁形成锁序反转;模块选址在rtp_llm.utils而非 factory 旁(docstring:22-26),从根上规避参数解析路径 eager import 通信库。上一轮评审提出的锁序反转与「先标记 started」两点已修复。- hook 异常刻意不吞,模块 docstring(
:28-31)明确说明「静默丢弃注册会表现为数值错误而不是启动失败」,错误语义显式。 - CUDA graph host mirror 清零(
cuda_graph_runner.cc:390-398)补齐了 device 侧 fused fill(:331-350,全表 0..numel())的对称性,注释准确交代触发条件、后果(padding 行残留 block ID 把 KV 写进活跃请求的块)与块 0 为何是安全目标;清零位于 D2D/H2H 回填之前,顺序正确,且该 mirror 由:939-940恒分配为 pinned CPU,不会崩。 DecodeRpcServer.cc:338的语句顺序正确:先向sp_output_buffer赋值再于:359对局部变量std::move,避免 use-after-move;取值tokens[0][1]与 reader 的 legacy CPU 回退columnAsFlat(tokens, 1)(MtpBatchStreamProcessor.cc:252)严格一致。_prepare_dispatch_input(deepep_normal_router.py:228-249)让树外后端无需 fork router 即可携带量化元数据,且 FP8 输出契约校验用raise ValueError(:156-160)而非assert,在-O下不会被剥离。strategy_registry.py:99-105把get_attributes()由每候选多次调用收敛为一次并复用,消除重复 lazy import 与日志噪声;排序键calculate_priority()与旧priority语义等价,属零风险重构。PerChannelFp8PostprocessTest._run(test_compressed_w8a8_int8_per_channel.py:450-463)刻意用非方阵 (2,3) 输入让_postprocess的 reshape 可观测,并用与输入无法混淆的哨兵张量而非类属性验证 FP8 转换是否被调用,是正确的行为级断言写法。
| auto accept_tokens = torch::zeros({1, static_cast<int64_t>(propose_step + 1)}, cuda_i32); | ||
| accept_tokens[0][0] = sp_output_buffer->tokens[0][0]; | ||
| propose_tokens_gpu[0] = sp_output_buffer->tokens[0][1]; | ||
| sp_output_buffer->propose_tokens_gpu = propose_tokens_gpu; |
There was a problem hiding this comment.
[P2] propose_tokens_gpu 两个生产者的形状与取值语义分叉
:333-338 向 sp_output_buffer->propose_tokens_gpu 写入 {1} 一维 CUDA 张量,只含 tokens[0][1](首个 draft)。同一字段的另一生产者 StreamCacheResource.cc:223 写 sp_output_buffer->tokens.to(cuda_i32),即完整 {1,N}(index 0 是 target token)。唯一消费者 MtpBatchStreamProcessor.cc:250 / :372 统一用 lastColumnAsFlat(:147-149 取最后一列):对 {1} 得首个 draft,对 {1,N} 得最后一个。propose_step > 1 时两条握手通道经同一 reader 给出不同 token。
建议: 在 SpeculativeExecutorStreamOutput::propose_tokens_gpu 声明处写明唯一 shape 契约(「仅下一步单个 draft」还是「全部 propose_step drafts」),把构造逻辑抽成两条通道共用的工具函数;若确为一步语义,同步修正 StreamCacheResource.cc 的实现与其 :234-235 注释。兜底可在 collectLegacyProposeSlices(:360-375)对各 slice 的 numel() 一致性加 RTP_LLM_CHECK。请补一条 propose_step=1 与 >1 的 gRPC 握手回归用例,断言 GPU 路径与 host 回落取到同一 draft token。
| sp_output_buffer->propose_tokens_gpu = propose_tokens_gpu; | ||
|
|
||
| auto next_seq_len = torch::ones({1}, cuda_i32); | ||
| next_seq_len[0] = generate_stream->seqLength(); |
There was a problem hiding this comment.
📍 实际位置 rtp_llm/cpp/model_rpc/DecodeRpcServer.cc:353(不在 diff 展示范围内,就近挂载)
[P2] 握手快照发布无门控与回滚开关,CPU 回落路径被本 PR 变为不可达
紧邻的 setMtpAsyncDeviceState(:353-365)受 RTP_LLM_STREAM_ASYNC / RTP_LLM_MTP_ASYNC_DEVICE_STATE 门控,:344-348 注释明写「未启用对应流水线时发布静态快照会留下永久陈旧状态」;而 :338 的发布无任何开关。GenerateStream.cc 改动前注释是「PD-disaggregate path leaves it undefined and readers fall back to the CPU tokens tensor」,改后(:1034)仅在 draft_token_gpu.defined() 时刷新,而 MtpBatchStreamProcessor.cc:588 只在 on_gpu && next_batch_size > 0 时填该字段、:596-603 却仍刷新 CPU draft_token,此时 pickOneStepDraftToken(:249-250)优先读陈旧 GPU 镜像。
建议: 给 :338 的发布加上与 MtpAsyncDeviceState 一致的环境变量门控(或独立开关),保留出问题时回退到 CPU tokens 的运维手段;或在 specUpdate 中当 draft_token_gpu 缺失而 draft_token >= 0 时主动清空 propose_tokens_gpu,让 reader 显式回落。请补一条「GPU 镜像未刷新而 CPU token 已前进」的回归用例。
| case QuantMethod::W4A8INT4PTPC: | ||
| status_info.precision = "W4A8INT4PTPC"; | ||
| break; | ||
| case QuantMethod::W8A8INT8PTPC: |
There was a problem hiding this comment.
[P2] QuantMethod→字符串映射在两处 switch 重复维护且各自漏枚举值
LocalRpcServer.cc:405-442 与 ModelConfig.cc:53-82 是两份各自手工维护的 QuantMethod → string switch,本 PR 在两处各复制一个 W8A8INT8PTPC case。漂移已发生:ModelConfig.cc 缺 QuarkMXFP4(11);LocalRpcServer.cc 同时缺 ModelOptFP4(10) 与 QuarkMXFP4(11)。两值均可由 QuantInfo.cc:53,57(mxfp4-quark / modelopt_fp4)到达。落 default 后 ModelConfig::to_string() 打印 UNKNOWN(11),LocalRpcServer.cc:440 每次 worker status 轮询打一条 RTP_LLM_LOG_ERROR 并把对外 RPC 字段 precision 置为 "UNKNOWN"。
建议: 把枚举到字符串的映射收敛为 QuantInfo.h/cc 中唯一一个 quantMethodToString()(ModelConfig.cc:53 已有同名静态函数可直接上提),两处均改为调用它(precision 只需对 QuantMethod::None 保留 "FP16" 展示层特例);同时补齐 ModelOptFP4 与 QuarkMXFP4,避免这两类部署的高频轮询接口持续输出 ERROR 并对外上报 UNKNOWN。
| and _ckpt_base_matches_quant_exclude( | ||
| base_name, quant_config.exclude_modules | ||
| ) | ||
| and not _ckpt_base_matches_regex_exclude( |
There was a problem hiding this comment.
[P2] W8A8 部分排除守卫对 re: 单层正则留有静默全层降级缺口
_ckpt_base_matches_regex_exclude(per_channel_fp8_quant_weight.py:114-127)把 {i} 一律替换为 "0",只对第 0 层求值,无法区分「整模板正则」与「单层正则」。若 ignore 写 re:model\.layers\.0\.self_attn\.gate,第三个条件为 True,守卫不抛错;随后 super().support()(:329-335)因 _ckpt_base_matches_quant_exclude 命中返回 False,weight_module.py:78-79 原样返回未量化 AtomicWeight,所有层把 INT8 字节按 compute dtype 读取,无 scale、无报错。等价的具体路径 model.layers.0.self_attn.gate 反而硬失败(test_..._fails,:418-423),两条语义相互矛盾。测试只覆盖 re:^model\.layers\.\d+\....$ 整模板正则...
建议: 让 re: 与具体路径走同一层感知判定:至少代入两个不同层号(如 0 与 1)分别求值,全部命中才认定为整模板排除,否则按部分排除 fail-fast;若能拿到 num_layers,在真实层区间上展开模板统计命中层数,全覆盖返回 False、部分覆盖 raise。补「regex 仅命中部分层」与「regex 命中全部层」两条用例。
| base_name = ckpt_w.name.rsplit(".", 1)[0] | ||
| if ( | ||
| base_name not in quant_config.exclude_modules | ||
| and _ckpt_base_matches_quant_exclude( |
There was a problem hiding this comment.
[P2] 守卫与 exclude 匹配对 MoE {expert_id} 模板完全失效
MoE 权重的 ckpt 模板同时含 {i} 与 {expert_id}(如 model.layers.{i}.mlp.experts.{expert_id}.down_proj.weight,见 compressed_w4a8_int4_per_channel_weight.py:116、ffn_weight.py:350)。_exclude_pattern_for(per_channel_fp8_quant_weight.py:57-64)只把 {i} 展开为 \d+,_ckpt_base_matches_* 也只 replace("{i}", "0"),{expert_id} 原样留在候选串里,因此任何具体 expert 路径与任何正则都匹配不上:守卫第二个条件恒为 False → 永不抛错,基类 exclude 检查同样恒为 False → 该权重仍按量化加载。被 ignore 的 expert 缺少 weight_scale,失败推迟到权重加载阶段变成与根因无关的缺张量错误。
建议: 把模板→正则的构造统一为「所有 {...} 占位符都展开为通配」(复用 _exclude_pattern_for 并支持任意占位符名),使 {expert_id} 模板与 MoE 的 ignore 条目能正常比对;随后守卫对部分排除按 fail-fast、对完整排除走非量化回退。请补一条 MoE {expert_id} 模板被 ignore 覆盖(部分与全部两种)的用例。
| @@ -232,6 +232,7 @@ def calc_low_latency_max_token_per_rank( | |||
| "FP8_DYNAMIC_PER_TENSOR", | |||
There was a problem hiding this comment.
📍 实际位置 rtp_llm/models_py/distributed/deepep_wrapper.py:230(不在 diff 展示范围内,就近挂载)
[P3] 量化方案以硬编码字符串元组在中心函数集中分派
calc_low_latency_max_token_per_rank 通过 quant_config.get_method() in (...) 的五元素字符串元组(:230-236)判断 per-act-token,本次为 W8A8 追加第五个字符串(与 quant_config.py:945 的返回值一致)。每新增一个 per-token 方案都必须回到这个中心函数改一行,且字符串与 get_method() 的对应关系没有类型层面约束——拼错只会静默落到错误分桶或 raise ValueError("Unsupported quantization config"),而 token bucket 直接决定 DeepEP low-latency 缓冲尺寸。
建议: 在 QuantizationConfig 上增加 is_per_act_token()(或 low_latency_token_buckets())由各配置类自行声明,该函数改为查询该能力;若暂不重构,至少把字符串集合提为模块级常量并注明与 get_method() 的同步要求。
Checklist: [6.1] OCP:本地扩展点优先于修改中心逻辑;[P.A] 字符串分发用 Enum/Literal
|
|
||
| from rtp_llm.device.device_type import is_hip | ||
| from rtp_llm.models_py.utils.arch import is_cuda | ||
| from rtp_llm.models_py.utils.arch import ( |
There was a problem hiding this comment.
[P3] 设备判定函数经 models_py.utils.arch 隐式 re-export
改动前 is_hip 直接来自 rtp_llm.device.device_type,改动后 get_device_type/is_cuda/is_hip/is_ppu 全部改从 rtp_llm.models_py.utils.arch 导入(:238-243)。arch.py:6-12 只是 from rtp_llm.device.device_type import ... 的透传,其中 get_device_type/is_hip/is_ppu/DeviceType 在该模块内均未被使用,也未声明 __all__,属隐式 re-export:任何 unused-import 清理(autoflake / ruff F401)都会让本文件运行时 ImportError,mypy 严格模式同样报错;同一批符号出现两个可用导入路径,后续容易分叉。
建议: 直接从 rtp_llm.device.device_type 导入这四个符号;若确实需要 arch.py 作为设备门面,请在其中声明 __all__ 并注明门面职责,避免被当作未使用导入清理掉。
Checklist: [6.1] 依赖方向:无循环依赖/跨层惊喜;[6.1] 分层边界:新概念在正确层级,不泄漏内部
| ) | ||
| processed_res[self.scale.name] = scale_weight | ||
| processed_res[self.kernel.name] = kernel_weight | ||
|
|
There was a problem hiding this comment.
📍 实际位置 rtp_llm/model_loader/per_channel_fp8_quant_weight.py:733(不在 diff 展示范围内,就近挂载)
[P3] LoadQuantPerChannelFp8Weight 绕过了新引入的 supported_quant_config_types 单一来源
本 PR 引入 supported_quant_config_types 作为「本 loader 认领哪些配置」的单一来源(:309-315 注释明确说明它保证 WeightModule.create 只匹配一个 loader),基类 support(:321-323)与 INT8 子类都据此判定;但同一继承链上的 LoadQuantPerChannelFp8Weight.support(:729-738)仍直接写 isinstance(quant_config, Fp8PerChannelCompressedQuantConfig),未使用该类属性。新增第三种 per-channel 配置时,这一处会被漏改,且漏改的表现是 loader 冲突(weight_module.py:80 报错)或静默不认领。
建议: 让该子类也通过 cls.supported_quant_config_types 判定(必要时在子类上覆写该属性),使「认领哪些配置」只有一个声明位置。
Checklist: [6.1] DRY:重复非平凡逻辑被抽取或显式复用;[6.1] LSP:子类/重写保持基类契约;[I] 同一功能用统一工具函数
| auto stream2 = builder.createDecoderStream({1, 2, 3, 4, 5}, {1, 2, 3}); | ||
| } | ||
|
|
||
| TEST_F(GenerateStreamTest, mtpUpdateKeepsLastGpuProposalWhenNextProposalIsMissing) { |
There was a problem hiding this comment.
[P3] GenerateStream 新用例只覆盖「缺失则保留」,未覆盖「提供则刷新」
本次把 GenerateStream.cc:1035 的无条件赋值改为 if (update_info.draft_token_gpu.defined()),引入两个分支。新增用例 mtpUpdateKeepsLastGpuProposalWhenNextProposalIsMissing(:117-137)只传 undefined 张量,断言旧值 9 被保留。全仓搜索 propose_tokens_gpu 后确认:没有任何测试通过 specUpdate 断言「传入 defined 张量时会刷新为新值」——MtpBatchStreamProcessorTest.cc 各处都直接对 state/sp_output_buffer 赋值,绕过 specUpdate。若该判断被误写为恒假(propose_tokens_gpu 永不刷新,每步都吃上一轮 proposal),全部测试仍会通过,而这正是 MTP gather 读到过期 GPU proposal 的回归形态。
建议: 补一条对称用例:构造 draft_token_gpu = torch::tensor({11}, torch::kInt32).to(torch::kCUDA),调用 specUpdate 后断言 propose_tokens_gpu.cpu().item<int32_t>() == 11(从 9 刷新为 11);并补一条 commit-only(propose_token_ 清空)路径的边界断言。
Checklist: [6.1] 边界 case 覆盖(空、单元素、最大值)
| self.assertEqual(seen, ["first", "second"]) | ||
|
|
||
| def test_repeatable_slot_rejects_late_hooks(self): | ||
| run_backend_registrations("parser", repeatable=True, parser="first") |
There was a problem hiding this comment.
[P3] backend_registry 生命周期异常断言未校验消息,且 noop 用例无任何断言
test_repeatable_slot_rejects_late_hooks(:65-69)、test_slot_lifecycle_cannot_change_after_start(:71-75)与 test_registering_after_drain_raises(:98-104)都只用 assertRaises(RuntimeError),不校验消息。backend_registry.py 里三条 RuntimeError 文案各不相同(:66-69 迟到登记、:92-94 重入、:98-100 生命周期变更),任一分支被误改成另一分支的抛出点,三条用例仍全绿。test_draining_slot_without_hooks_is_noop(:141-142)只调用一次函数、无任何断言,不区分「正常 no-op」与「静默吞掉后续异常」。
建议: 三条异常用例改用 assertRaisesRegex 钉住各自消息关键片段(如 "already initialised" / "re-entrant" / "lifecycle changed");noop 用例补断言(例如随后 register_backend_hook 应因槽位已开始而 raise,以证明状态确实被记录)。
Checklist: [6.1] 新逻辑有聚焦单测 + 相关集成/smoke 测试;[P.G] pytest.raises 带 match 参数
背景
本 PR 汇总 PPU Qwen3.5 支持所需的通用外源能力,便于统一 review、CI 验证和合入。
PPU 专属 FA3、DeepGEMM、W8A8 INT8 kernel、router 和 executor 仍保留在内源;本 PR 只提供开源侧可复用的配置、加载、注册契约和正确性修复。
主要改动
1. compressed-tensors W8A8 INT8
group_0兼容行为。_prepare_dispatch_input()扩展契约。targets/scheme/regex ignore 等组合 fail-fast。ignore仅在实际部分命中可量化{i}模板时拒绝,兼容真实 checkpoint 中与量化权重无关的具体层条目。2. out-of-tree backend 延迟注册
register_backend_hook()/run_backend_registrations()。linear、fused_moe、attention和moe_strategy_choices四个扩展槽位。moe_strategy_choices对每个新 parser 重放,其余 Factory 槽位只执行一次。内源 PPU backend 在启动时只登记 hook;外源 Factory 消费 hook 后,将内源 FA3、DeepGEMM Linear/MoE strategy 注册到公共 Factory,避免外源直接依赖设备专属实现或使用 monkey patch。
3. Qwen3.5 PPU model 入口
is_ppu()设备判断。该改动仅开放模型构建入口,具体 PPU Attention、Linear 和 MoE 实现仍由内源 backend 提供。
4. 正确性修复
内外源职责
本 PR 负责:
本 PR 不包含:
相关实现放置于内源
Commit 组织
feat(quant): support compressed-tensors W8A8 INT8 checkpointsfeat(runtime): add deferred out-of-tree backend registrationfeat(ppu): enable Qwen3.5 runtime and correctness fixes三个 commit 分别对应量化加载、扩展机制、模型入口与公共正确性修复,最终代码与此前验证组合保持一致。
验证
在本 PR 最终代码与内源 PPU backend 的组合上完成:
git diff --check通过。Supersedes
This PR consolidates and supersedes: