Conversation
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1405
Status: BLOCKING
Summary: P0/0 · P1/2 · P2/2 · P3/0
Reviewed: commit 08a3d02cd267 · 2026-09-08 23:08 UTC+8
Blocking Issues
P1
- DeepGEMM MoE 策略可能选择实际不可执行的后端 @
rtp_llm/models_py/kernels/cuda/deepgemm_wrapper.py:88- 建议:为每种 executor 声明并解析必需符号,在策略选择或加载期完成校验;不可用时回退其他策略或明确终止加载,并补充导入失败及 grouped 符号缺失测试。
- DSA Indexer 的必需后端未纳入加载期预检 @
rtp_llm/models_py/modules/base/cuda/indexer_op.py:16- 建议:在
DeepSeekV32Indexer上实现加载期后端校验,验证两个模块及实际调用符号,并增加缺失依赖、导入失败和缺失符号的加载失败测试。
- 建议:在
Non-blocking Suggestions
P2
- CUDA Graph 回放测试未实际覆盖活动批次缩减 @
rtp_llm/models_py/modules/factory/attention/cuda_impl/test/test_py_flashinfer_mha_decode.py:548- 建议:保留四槽捕获容量,但将后两个槽位构造成真实的非活动输入,并断言全部页指针、末页长度和页索引中的尾部安全值。
- 固定到 H20 的新测试缺少平台标签 @
rtp_llm/models_py/new_models/deepseek_v3/test/BUILD:28- 建议:为五个目标统一添加
tags = ["H20"];若测试应跨平台运行,则移除固定 H20 执行属性并按平台声明匹配目标。
- 建议:为五个目标统一添加
Checklist Findings (4 fail / 58 total)
General Principles Checklist
- [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
DSA Indexer 的必需后端未纳入加载期预检
DeepSeekV32Indexer虽继承RtpModule,但未实现validate_runtime_device;其IndexerOp子模块仅继承nn.Module。NewLoader 因此不会预检deep_gemm、flashinfer.rope及实际调用符号,依赖缺失、ABI 不兼容或符号缺失会在模型加载成功后的首个 DSA 请求才暴露。 - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
固定到 H20 的新测试缺少平台标签
test_deepseek_newloader、test_deepseek_vl2_load、test_llama_load、test_qwen2_dense_load和test_qwen2_moe_load均设置exec_properties.gpu=H20,却没有H20标签。依赖标签过滤时,普通 CUDA 分片会纳入这些目标,而 H20 专用分片反而无法选中它们。 - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
DSA Indexer 的必需后端未纳入加载期预检
DeepSeekV32Indexer虽继承RtpModule,但未实现validate_runtime_device;其IndexerOp子模块仅继承nn.Module。NewLoader 因此不会预检deep_gemm、flashinfer.rope及实际调用符号,依赖缺失、ABI 不兼容或符号缺失会在模型加载成功后的首个 DSA 请求才暴露。 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
CUDA Graph 回放测试未实际覆盖活动批次缩减
测试设置active_bs=2,但回放输入仍按capture_bs=4创建,四个序列长度均非零;active_bs只限制断言范围。虽然已有底层 planned-batch 测试,该用例仍未验证prepare_for_cuda_graph_replay在尾部槽位失活时是否正确刷新页表和长度元数据。
Strengths
- NewLoader 三态配置从服务参数、
ModelConfig、BaseModel到多模态加载链传播一致。 UpdateWeights的能力指标、权重管理状态和 gRPC 错误语义保持一致。- FlashInfer 规划参数在 C++、pybind、类型桩及主要调用方之间保持同步。
- MTP 缓存布局、ROCm MRoPE 和量化分片补充了较完整的边界校验。
| except Exception as e: | ||
| print(f"Warning: Failed to import flashinfer.rope (likely running on CPU): {e}") | ||
| rope = None | ||
| @lru_cache(maxsize=1) |
There was a problem hiding this comment.
[P1] DSA Indexer 的必需后端未纳入加载期预检
DeepSeekV32Indexer 虽继承 RtpModule,但未实现 validate_runtime_device;其 IndexerOp 子模块仅继承 nn.Module。NewLoader 因此不会预检 deep_gemm、flashinfer.rope 及实际调用符号,依赖缺失、ABI 不兼容或符号缺失会在模型加载成功后的首个 DSA 请求才暴露。
建议: 在 DeepSeekV32Indexer 上实现加载期后端校验,验证两个模块及实际调用符号,并增加缺失依赖、导入失败和缺失符号的加载失败测试。
Checklist: [6.1] 错误语义:fail-fast/retry/fallback/silent 行为显式;[6.1] 新逻辑有聚焦单测 + 相关集成/smoke 测试
|
|
||
| def test_replay_refreshes_plan_metadata(self): | ||
| """Tensor-core replay must refresh FlashInfer plan metadata.""" | ||
| config = self._create_config() |
There was a problem hiding this comment.
📍 实际位置 rtp_llm/models_py/modules/factory/attention/cuda_impl/test/test_py_flashinfer_mha_decode.py:548(不在 diff 展示范围内,就近挂载)
[P2] CUDA Graph 回放测试未实际覆盖活动批次缩减
测试设置 active_bs=2,但回放输入仍按 capture_bs=4 创建,四个序列长度均非零;active_bs 只限制断言范围。虽然已有底层 planned-batch 测试,该用例仍未验证 prepare_for_cuda_graph_replay 在尾部槽位失活时是否正确刷新页表和长度元数据。
建议: 保留四槽捕获容量,但将后两个槽位构造成真实的非活动输入,并断言全部页指针、末页长度和页索引中的尾部安全值。
Checklist: [6.1] 边界 case 覆盖(空、单元素、最大值)
| "//rtp_llm/models_py:new_weight_loader", | ||
| ] | ||
|
|
||
| py_test( |
There was a problem hiding this comment.
[P2] 固定到 H20 的新测试缺少平台标签
test_deepseek_newloader、test_deepseek_vl2_load、test_llama_load、test_qwen2_dense_load 和 test_qwen2_moe_load 均设置 exec_properties.gpu=H20,却没有 H20 标签。依赖标签过滤时,普通 CUDA 分片会纳入这些目标,而 H20 专用分片反而无法选中它们。
建议: 为五个目标统一添加 tags = ["H20"];若测试应跨平台运行,则移除固定 H20 执行属性并按平台声明匹配目标。
Checklist: [6.1] 分布式/跨平台变更有对应覆盖
| @@ -422,7 +499,7 @@ def __init__( | |||
| assert ( | |||
There was a problem hiding this comment.
[P3] 配置校验混用 assert 与 raise,W4A8 迁移后其余类仍保留 assert
本 PR 将 W4a8Int4PerChannelQuantConfig / CompressedW4A8Int4PerChannelQuantConfig 的 assert bits==4 and group_size>0 改为 raise TypeError/ValueError(因 assert 在 python -O 下会被剥离),但 Fp8PerTensorQuantConfig(第499行)、Fp8DynamicPerTensorQuantConfig(第537行)、CompressedW8A8Int8PerChannelQuantConfig(第1102行) 仍使用 assert (bits == 8 and group_size == 0)。同一文件内校验风格不一致,且这些 assert 在 -O 优化下同样会被静默跳过。
建议:将剩余的 bits/group_size 参数校验统一改为 raise ValueError/TypeError,与 W4A8 配置保持一致,避免 -O 下校验失效。
评审版本:08a3d02cd267
| projector_config = top_config_json.get("projector_config", {}) | ||
| config.mm_related_params.config["projector_config"] = projector_config | ||
| candidate_resolutions = top_config_json.get("candidate_resolutions", {}) | ||
| candidate_resolutions = top_config_json.get( |
There was a problem hiding this comment.
[P3] candidate_resolutions 默认值从空 dict 改为 ((384, 384),)
补丁中 - candidate_resolutions = top_config_json.get("candidate_resolutions", {}) 改为 + candidate_resolutions = top_config_json.get("candidate_resolutions", ((384, 384),))。当 DeepSeek-VL2 的 config.json 省略 candidate_resolutions 字段时,视觉候选分辨率从空集合变为单元素 (384,384),会改变多模态视觉编码的 tile 处理逻辑。
建议:确认该默认值变更是有意为之(旧空 dict 默认值可能导致视觉分支异常);若属修正,建议在注释中说明默认 (384,384) 的来源依据。
评审版本:08a3d02cd267
| } | ||
| if (mrope_dim1 < 0 || mrope_dim2 < 0 || mrope_dim3 < 0) { | ||
| return "sections must be non-negative"; | ||
| if (mrope_dim1 <= 0 || mrope_dim2 <= 0 || mrope_dim3 <= 0) { |
There was a problem hiding this comment.
[P2] MRoPE section 校验从「非负」收紧为「严格为正」,可能拒绝合法的零 section 配置
补丁中 - 行为 if (mrope_dim1 < 0 || mrope_dim2 < 0 || mrope_dim3 < 0) { return "sections must be non-negative";,+ 行改为 if (mrope_dim1 <= 0 || mrope_dim2 <= 0 || mrope_dim3 <= 0) { return "sections must be positive";。同时 FusedRopeKVCacheOp.cc 新增的 validateMropePositionIds 也要求 mrope_dim1 > 0 && mrope_dim2 > 0 && mrope_dim3 > 0。旧代码显式允许 0(非负),说明历史上存在 section==0 的合法配置(例如仅 H/W 两轴的 MRoPE 变体);收紧后此类 checkpoint 会在启动/forward 阶段直接 TORCH_CHECK 失败。补丁内未见任何兼容 0 section 的回退路径。
建议:若确有 section==0 的模型,保留对 0 的兼容(如仅校验 >=0,或对 0 section 跳过对应轴);否则在 PR 说明中明确该收紧是有意破坏性变更并列出受影响的模型。
评审版本:08a3d02cd267
| ignored = _merge_module_patterns( | ||
| "ignored_layers", ignored_layers, ignore_patterns | ||
| ) | ||
| excluded = _merge_module_patterns("exclude_modules", exclude_modules, ignored) |
There was a problem hiding this comment.
[P2] CompressedW8A8Int8PerChannelQuantConfig.exclude_modules 语义变更:合并了 ignored_layers/ignore 条目
补丁 - 行 self.exclude_modules = set(self._ignore_patterns)(其中 _ignore_patterns = list(ignore_patterns or []))表明旧版 exclude_modules 只含 ignore_patterns。新版改为 excluded = _merge_module_patterns("exclude_modules", exclude_modules, ignored) 后传入基类,使 exclude_modules = exclude_modules ∪ ignored_layers ∪ ignore_patterns。下游消费者 rtp_llm/model_loader/compressed_w8a8_int8_per_channel_weight.py:41-47 与 per_channel_fp8_quant_weight.py:94-117 用 exclude_modules 做精确/正则排除匹配;ignored_layers 条目(可能是 re: 正则、{i} 模板或前缀路径)语义与 exclude 精确路径不同,混入同一集合后可能改变这些加载器的排除判定。属有意的统一(代码注释已说明),但需确认所有消费者对新语义兼容。
建议:确认 compressed_w8a8_int8_per_channel_weight.py 与 per_channel_fp8_quant_weight.py 对混入 ignored_layers 的正则/模板条目行为符合预期,必要时为 ignored_layers 与 exclude 保留独立匹配通道,避免前缀/正则条目被当作精确路径误判。
评审版本:08a3d02cd267
| value = [value] | ||
| if not isinstance(value, (list, tuple, set)): | ||
| raise TypeError(f"{label} must be a sequence of strings") | ||
| result: List[str] = [] |
There was a problem hiding this comment.
[P3] _normalize_module_patterns 对 set 输入未排序,ignored_layers 列表顺序不确定
_normalize_module_patterns 接受 (list, tuple, set),对 set 直接 for pattern in value 迭代,顺序取决于 Python 的 set 哈希序,导致返回的 ignored_layers: List[str] 顺序不确定。同 PR 内 quantization_exclusion.py::normalize_module_patterns 对 set/frozenset 显式 sorted(values)(见其第 11-13 行),两处行为不一致。
建议:在 _normalize_module_patterns 中对 set/frozenset 输入先 sorted(value) 再迭代,保证输出列表确定性,并与 quantization_exclusion 的规范化保持一致。
评审版本:08a3d02cd267
| None, | ||
| input_bf16.dtype, | ||
| ) | ||
| if bias is not None: |
There was a problem hiding this comment.
[P2] _restore_dtype 被当作未绑定类方法调用,可能导致 ROCm FP8 NoSwizzle forward 抛 TypeError
本 PR 在同一重构中把 _quantize_input 显式改为 @staticmethod(diff 中可见 - self, input: torch.Tensor → + input: torch.Tensor,),并新增模块级函数 run_rocm_fp8_ptpc_no_swizzle,其中调用 RocmFp8PTPCLinearBase._quantize_input(input) 与 RocmFp8PTPCLinearBase._restore_dtype(output, original_dtype)。但补丁中看不到 _restore_dtype 被改为 staticmethod 的 -/+ 行(其定义是补丁外的既有代码,原 forward 通过 self._restore_dtype(output, original_dtype) 调用)。若 _restore_dtype 仍是实例方法(签名为 _restore_dtype(self, output, dtype)),则 RocmFp8PTPCLinearBase._restore_dtype(output, original_dtype) 会把 output 绑定为 self、original_dtype 绑定为 output,缺少 dtype 实参,在 ROCm 平台走 RocmFp8PTPCLinearNoSwizzle.forward(即 run_rocm_fp8_ptpc_no_swizzle)时每次都会抛 TypeError: _restore_dtype() missing 1 required positional argument。
建议:确认 _restore_dtype 是否已是 staticmethod;若不是,补上 @staticmethod 并去掉 self 参数(与 _quantize_input 一致),或改为传实例调用 self._restore_dtype(...)。同时建议为 NoSwizzle forward 路径补一个 ROCm 单测以覆盖该函数。
评审版本:08a3d02cd267
| Whether quantization is enabled | ||
| """ | ||
| return config.model_config.quant_config is not None | ||
| return config.quant_config is not None |
There was a problem hiding this comment.
[P2] MoE 量化判定改读 config.quant_config(默认 None),未显式传 quant_config 的构造点会静默退化为非量化
本分片 config_adapter.py 中 self.quant_config = quant_config 默认 None(注释明确『None means that this layer is not quantized』),而 config_resolver.py 将 - return config.model_config.quant_config is not None 改为 + return config.quant_config is not None,strategy_registry.py 同样从 config.model_config.quant_config.get_method() 改为 config.quant_config.get_moe_runtime_method_key()。测试 test_adapter_keeps_omitted_and_explicit_none_unquantized 明确断言省略 quant_config 时 has_quantization 返回 False。本分片 moe_experts.py 的 _maybe_build_fused_moe 已改为显式传 quant_config=self._effective_model_quant_config,但补丁中未看到其它生产构造点(如 legacy generic_moe.py 的 MoEConfigAdapter 构造)同步显式传 quant_config。若存在未更新的构造点,量化模型会被 StrategyRegistry 当成非量化选择 executor,造成量化权重加载进非量化执行器的静默错误。
建议:全局排查所有 MoEConfigAdapter 构造点,确保量化模型路径显式传入 quant_config(或让 MoEConfigAdapter 在 quant_config 为 None 时回退到 model_config.quant_config),避免静默退化为非量化策略。
评审版本:08a3d02cd267
| self.routed_scaling_factor = self.config.routed_scaling_factor | ||
| if not group_topk_supported( | ||
| num_experts=self.n_routed_experts, | ||
| n_group=self.num_expert_group, |
There was a problem hiding this comment.
[P2] 新增 group_topk_supported 硬校验会在 init/forward 对单专家组或 top_k==1 且 renormalize 的既有配置直接抛错
generic_moe.py 在 init 中新增 if not group_topk_supported(num_experts=..., n_group=..., topk_group=..., top_k=self.top_k, renormalize=self.renormalize): raise ValueError(...);select_topk.py 的 GroupTopK.forward 也新增同样校验。group_topk_supported 拒绝 num_experts // n_group < 2 与 top_k == 1 and renormalize。此前这些配置会静默走 kernel 的 uniform-routing fallback(结果可能错误),现在改为在模型构造/前向阶段硬失败。对既有用此类边界配置的部署是行为变更,可能直接启动失败。
建议:该校验本身正确,但建议在文档/错误信息中明确说明这是对既有静默错误结果的 fail-fast 升级,并为受影响的配置提供迁移指引(如调整 n_group 或关闭 renormalize)。
评审版本:08a3d02cd267
| def _resolve_deep_gemm() -> ModuleType: | ||
| try: | ||
| return importlib.import_module("deep_gemm") | ||
| except (ImportError, OSError) as exc: |
There was a problem hiding this comment.
[P3] deep_gemm/flashinfer 惰性导入将异常捕获从 Exception 收窄为 ImportError/OSError
原代码 try: import deep_gemm except Exception as e: ... deep_gemm = None(补丁 - 行)捕获所有异常并优雅降级;新代码 _resolve_deep_gemm 仅 except (ImportError, OSError)(第 20 行)。若 deep_gemm 导入时因 CUDA 驱动初始化等原因抛出 RuntimeError(非 ImportError/OSError),新代码会原样向上抛出,且不再有 None 降级路径。
建议:将捕获范围扩展为 except Exception(或至少包含 RuntimeError),并保留明确的 ImportError 包装,避免非导入类异常泄漏到调用方。
评审版本:08a3d02cd267
| raise RuntimeError("Failed to assign LM head weight") | ||
|
|
||
| def _copy_local_tied_weight(self, tensor: torch.Tensor) -> None: | ||
| if tuple(tensor.shape) != tuple(self.weight.shape): |
There was a problem hiding this comment.
[P3] ParallelLMHead._copy_local_tied_weight 为未被调用的死代码
embedding.py 中 _copy_local_tied_weight(约第 138 行)定义了 tied 权重分片拷贝逻辑,但 load_weights 仅调用 _copy_weight,本分片内无任何调用点。若该方法是留给子类或后续 tied-embedding 路径使用,当前属于悬空实现;若确无使用,则表明 tied LM head 的加载路径不完整或存在遗漏。
建议:确认 tied 权重路径是否需要 _copy_local_tied_weight;若不需要则删除,若需要则补充调用点或注释说明其消费者。
评审版本:08a3d02cd267
| renormalize: bool, | ||
| routed_scaling_factor: float, | ||
| ): | ||
| num_experts = scores.shape[-1] |
There was a problem hiding this comment.
[P3] GroupTopK.forward 每次前向都执行 group_topk_supported 能力校验
select_topk.py 的 GroupTopK.forward 新增 num_experts = scores.shape[-1] 并调用 group_topk_supported(...) 做校验(约第 41 行)。该校验在路由 top-k 热路径上每个 batch 都执行,且 GenericMoeLayer.__init__ 已做过一次相同校验,属于重复验证。校验本身是纯整数比较、开销很小,但属于可避免的热路径冗余。
建议:将能力校验收敛到构造期(GenericMoeLayer 已做),forward 中可移除或仅在 debug/断言模式下保留。
评审版本:08a3d02cd267
| raise ValueError( | ||
| f"{self.prefix}.{parameter_name} has invalid shard scales {invalid}" | ||
| ) | ||
| max_scale = max(scales.values()) |
There was a problem hiding this comment.
[P2] 非 weight_scale 的标量 scale(如 input_scale)按 max 合并且不校验一致性,可能静默得到错误结果
linear.py 中 MergedColumnParallelLinear._merge_per_tensor_scales 与 QKVParallelLinear._merge_qkv_per_tensor_scales 均执行 max_scale = max(scales.values()); parameter.data.fill_(max_scale),且仅在 parameter_name == 'weight_scale' 时对权重做 rescale,其余标量 scale(如 input_scale)直接取 max 不校验各 shard 是否一致。若 q/k/v(或 gate/up)的 input_scale 实际不同,合并后的 max 值会让较小 scale 的 shard 激活反量化系统性偏大,静默产生错误结果。
建议:对 input_scale 等共享输入的标量参数,在合并时校验各 shard scale 相等(math.isclose),不一致则抛 ValueError;或与 weight_scale 一样提供对应的重缩放语义。
评审版本:033817efcd72
| topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True).clamp_min( | ||
| 1e-20 | ||
| ) | ||
| return topk_weights * routed_scaling_factor, topk_ids |
There was a problem hiding this comment.
[P2] noaux_tc 参考路由在 renormalize 时仍无条件乘 routed_scaling_factor,与 topk 路径语义不一致
_select_deepseek_topk(第161-166行)对 renormalize 与 routed_scaling_factor 采用互斥的 if/else:if renormalize and top_k > 1: topk_weights = topk_weights / sum... else: topk_weights = topk_weights * routed_scaling_factor。而 _select_deepseek_noaux_topk(第225-229行)在 renormalize 分支之后无条件执行 return topk_weights * routed_scaling_factor,即 renormalize=True 时仍会再乘 scaling_factor。DeepSeek V3 参考实现(MoEGate)对 norm_topk_prob 与 routed_scaling_factor 同样是互斥处理。触发路径:当某个 noaux_tc 模型配置 has_moe_norm=True 且 routed_scaling_factor != 1.0 时,在 ROCm/CPU(_use_fast_group_topk 因 device_type != Cuda 为 False)走该参考回退路径会得到被额外缩放的门控权重,与 CUDA 上 GroupTopK 快路径(互斥处理,见第534-535行传入 renormalize 与 routed_scaling_factor)产生跨后端结果不一致。当前标准 DeepSeek V3 模型 routed_scaling_factor=1.0,故为潜在缺陷。
建议:将 _select_deepseek_noaux_topk 的返回改成与 _select_deepseek_topk 一致的互斥 if/else(renormalize 时不再乘 routed_scaling_factor),使参考回退与 GroupTopK 快路径语义统一。
评审版本:08a3d02cd267
| top_k=top_k, | ||
| renormalize=has_moe_norm, | ||
| ) | ||
| ) |
There was a problem hiding this comment.
[P2] routing_config 校验直接读取 model_config 原始字段,与 extract_config_values 从 config.json 派生的值可能不一致,导致误报 mismatch
extract_config_values 中 scoring_func(config.json 的 "softmax"→0)、has_moe_norm(config.json 的 norm_topk_prob)、n_group/topk_group(config.json 的 n_group/topk_group)都会用 config.json 覆盖 model_config 的取值,但只有 glm4_moe_lite 分支显式回写 model_config.scoring_func。而 DeepSeekV32MoEBlock.init 的 routing_config 用 (model_config.scoring_func, model_config.has_moe_norm, model_config.moe_n_group, model_config.moe_topk_group, model_config.routed_scaling_factor) 与构造参数(即 config.json 派生值)做相等比较,不一致即 raise 'DeepSeek routing config mismatch'。若 legacy loader 未把 config.json 的 n_group/topk_group/norm_topk_prob/scoring_func 同步到 ModelConfig(代码中 first_k_dense_replace/moe_layer_freq 的注释明确说明 legacy loader 不传播部分字段),加载这类 checkpoint 会在构造阶段被误拒。
建议:routing_config 校验应比较 config.json 派生值本身(或统一以 extract_config_values 返回值为准),而非依赖 model_config 原始字段;或像 glm4_moe_lite 分支一样,在 extract_config_values 中把派生值回写到 model_config,保证两边一致。
评审版本:08a3d02cd267
|
|
||
| candidate_scores = scores | ||
| if group_limited: | ||
| group_size = num_experts // n_group |
There was a problem hiding this comment.
[P2] _select_deepseek_topk 在 renormalize=True 时丢弃 routed_scaling_factor,与 noaux 路径及 DeepSeek V3 参考实现不一致
函数 _select_deepseek_topk 中:if renormalize and top_k > 1: topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True).clamp_min(1e-20) 分支只做归一化、不乘 routed_scaling_factor;仅 else 分支才 topk_weights = topk_weights * routed_scaling_factor。对比同文件 _select_deepseek_noaux_topk 在归一化后仍 return topk_weights * routed_scaling_factor(始终乘)。当 DeepSeek V3 走 greedy/group_limited_greedy 路由(非 noaux,correction_bias=False),且同时满足 has_moe_norm=True(renormalize)且 routed_scaling_factor!=1.0 时,router 权重会静默丢失 scaling factor,产生错误的路由权重。docstring 注明 'DeepSeek-V2 normalizes ... or applies routed scaling',即作者已知晓该分歧,但该函数同样服务于 V3 非 noaux 路由,属潜在正确性风险。
建议:使 _select_deepseek_topk 与 V3 参考一致,在归一化后也应用 routed_scaling_factor;或对 V3 路由路径显式断言 routed_scaling_factor==1.0,避免将来配置变化时静默出错。
评审版本:08a3d02cd267
| modules = nn.Sequential(*items) | ||
|
|
||
| if cfg.token_pooling: | ||
| self.token_pooling_layer = nn.Linear(cfg.input_dim * 4, cfg.input_dim) |
There was a problem hiding this comment.
[P2] token_pooling 与 downsample_mlp_gelu/identity 组合会产生维度不匹配
MlpProjectorConfig 仅校验 projector_type 属于 {identity, linear, mlp_gelu, downsample_mlp_gelu},未拒绝 token_pooling=True 与 downsample_mlp_gelu/identity 的矛盾组合。forward 中 token_pooling 分支输出为 (batch, hw/4, input_dim)(line 178 x = self.token_pooling_layer(patches),token_pooling_layer 是 Linear(input_dim4, input_dim)),随后 return self.layers(x)。当 projector_type=='downsample_mlp_gelu' 时 self.layers 首个 Linear 期望 input_dim*downsample_ratio^2 维(line 131-134),而 x 只有 input_dim 维,触发 nn.Linear 维度不匹配 RuntimeError;当 projector_type=='identity' 时 self.layers=nn.Identity(),输出仍为 input_dim 而非 n_embed,下游语言模型按 n_embed 消费也会出错。
建议:在 MlpProjectorConfig.init 中显式拒绝 token_pooling=True 与 projector_type 为 downsample_mlp_gelu/identity 的组合(或明确 token_pooling 仅支持 mlp_gelu/linear),并在 forward 前 fail-fast。
评审版本:08a3d02cd267
| f"checkpoint({checkpoint_use_mla})" | ||
| ) | ||
| validate_deepseek_mla_backend(model_config, use_mla, "DeepSeek-VL2") | ||
| q_lora_rank_value = raw_config.get("q_lora_rank", 1536) |
There was a problem hiding this comment.
[P3] q_lora_rank 默认值 1536 与 VL2 直接 Q 投影语义不符
模块 docstring 声明 deepseek-vl2-small/full 使用 "direct query projection (no Q-LoRA)",即 q_lora_rank 应为 0;但 _extract_config_values 中 q_lora_rank_value = raw_config.get("q_lora_rank", 1536) 将缺省值设为 DeepSeek-V3 的 Q-LoRA rank 1536,仅当显式传 None 时才归 0(language.py:167-168)。若某 VL2 checkpoint 的 language_config 省略 q_lora_rank 字段(而非设为 None),会被误判为带 Q-LoRA 的 1536 维,与官方拓扑不一致。官方 checkpoint 显式携带 q_lora_rank: None,故当前不触发。
建议:将缺省值改为 None(或 0),使省略该字段时按 VL2 直接 Q 投影语义归 0;或要求该字段必须显式出现。
评审版本:08a3d02cd267
| or not isinstance(ep_rank, int) | ||
| or not 0 <= ep_rank < ep_size | ||
| ): | ||
| raise ValueError(f"Invalid EP partition: rank={ep_rank}, size={ep_size}") |
There was a problem hiding this comment.
[P2] qwen2_moe/language.py 的 load_weights 与 process_weights_after_loading 复制自 qwen2
Qwen2MoeForCausalLM.load_weights(track has_lm_head → super().load_weights → _copy_local_tied_weight)与 process_weights_after_loading(normalize_lm_head_weight / logit_scale / _lm_head_postprocessed 三段逻辑)与 Qwen2ForCausalLM 中同名方法逐行一致。此类 lm_head 后处理逻辑在 qwen2/qwen3/qwen2_moe 三处重复。
建议:将 lm_head 的 tie/normalize/logit_scale 后处理抽到共享基类或 mixin,供 Qwen2ForCausalLM、Qwen3ForCausalLM、Qwen2MoeForCausalLM 复用。
评审版本:08a3d02cd267
| q, self.q_norm.weight.data, eps=self.q_norm.eps, out=q | ||
| ) | ||
| flashinfer.norm.rmsnorm( | ||
| k, self.k_norm.weight.data, eps=self.k_norm.eps, out=k |
There was a problem hiding this comment.
[P3] _apply_qk_norm 的 CUDA 路径只捕获 ModuleNotFoundError,无法回退到 eager 实现
代码为 try: import flashinfer; ... flashinfer.norm.rmsnorm(...) ... return ... except ModuleNotFoundError: pass。若 flashinfer 已安装但 flashinfer.norm.rmsnorm 属性缺失(AttributeError),或 rmsnorm 调用抛出其它异常,不会被捕获,异常直接向上传播,而非落入下方 eager fallback 分支。
建议:将 except 改为捕获 (ImportError, AttributeError)(或更宽泛的异常并记录日志),确保缺失 rmsnorm 后端时能回退到 eager 路径。
评审版本:08a3d02cd267
| ("scoring_func", model_config.scoring_func, scoring_func), | ||
| ("has_moe_norm", model_config.has_moe_norm, has_moe_norm), | ||
| ): | ||
| if actual != expected: |
There was a problem hiding this comment.
[P3] scoring_func 的 softmax 分支为死代码
代码为 if scoring_name == "softmax": scoring_func = 0 elif scoring_name == "sigmoid": scoring_func = 1 else: raise,随后紧跟 if scoring_func != 1: raise ValueError("Kimi correction-bias routing requires sigmoid activation")。因此 softmax 分支设置的 scoring_func=0 必然触发后续异常,该分支不可达,属于冗余逻辑。
建议:删除 softmax 分支,直接校验 scoring_name 必须为 "sigmoid",避免死代码误导后续维护者。
评审版本:08a3d02cd267
| expected_layers = set(range(1, checkpoint_layers + 1)) | ||
| if kda_layers & full_layers: | ||
| raise ValueError("KDA and full-attention layer sets must be disjoint") | ||
| if kda_layers | full_layers != expected_layers: |
There was a problem hiding this comment.
[P3] scoring_func 冗余死代码检查
_extract_config_values 中先根据 scoring_name 把 scoring_func 置为 0(softmax)或 1(sigmoid),非二者已 raise;随后又执行 if scoring_func != 1: raise ValueError("Kimi correction-bias routing requires sigmoid activation"),该分支仅等价于拒绝 softmax,与前面逻辑重复且易误导为独立校验。
建议:合并为 if scoring_name != "sigmoid": raise ...,删除冗余的 scoring_func != 1 判断。
评审版本:08a3d02cd267
| _FLASH_ATTN_RESOLVED = False | ||
|
|
||
|
|
||
| def _resolve_flash_attn_varlen() -> Optional[Callable[..., torch.Tensor]]: |
There was a problem hiding this comment.
[P3] flash attention 解析结果全局一次性缓存,失败后永久禁用
_resolve_flash_attn_varlen 在首次调用时把模块级 _FLASH_ATTN_RESOLVED 置为 True(在 try 之前),随后无论成功与否都直接返回缓存的 _FLASH_ATTN_VARLEN。若首次在 CUDA 上调用时 can_use_flash_attn() 返回 False、或 flash_attn 导入抛出 ImportError/OSError(例如不支持的 SM 架构、CUDA 上下文尚未就绪),_FLASH_ATTN_VARLEN 将永久为 None,即使之后模型迁移到支持 flash attention 的设备上也不会重试,静默退化到 SDPA 回退路径。
建议:将解析结果按设备/能力维度缓存,或在解析失败时保留 _FLASH_ATTN_RESOLVED=False 以便后续调用重试;至少避免把 import 失败与 can_use_flash_attn()==False 的结果永久固化。
评审版本:08a3d02cd267
598bd8e to
5b1d2e5
Compare
Summary
Introduce a registry-driven NewLoader framework and migrate supported model families to the new Python-native model construction and weight-loading path.
Motivation
The existing loader tightly couples model definitions, checkpoint mapping, weight transformation, and runtime initialization. This makes model migration, quantization support, tensor-parallel loading, and correctness testing difficult to
evolve independently.
NewLoader separates these responsibilities and provides explicit loading contracts, centralized checkpoint mapping, stricter validation, and model-level capability checks.
Changes
Compatibility
Existing deployments can explicitly select the legacy loader. Automatic routing only selects NewLoader when the model and requested runtime capabilities are supported; otherwise it falls back to the legacy path or reports an actionable
configuration error.
Validation