Skip to content

Develop/hengcang.wyd/new loader replacement - #1405

Open
Oneydauh wants to merge 89 commits into
mainfrom
develop/hengcang.wyd/new_loader_replacement
Open

Oneydauh wants to merge 89 commits into
mainfrom
develop/hengcang.wyd/new_loader_replacement

Conversation

@Oneydauh

@Oneydauh Oneydauh commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

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

  • Add the NewLoader core, model registry, weight mapper, checkpoint filtering, and loading integrity validation.
  • Add capability-aware loader routing with explicit override and legacy fallback.
  • Migrate supported dense, MoE, multimodal, and encoder model implementations.
  • Support TP/EP weight partitioning and fused QKV, gate/up, embedding, and LM-head loading.
  • Support registered quantization methods, per-layer quantization exclusions, and quantization-specific auxiliary tensors.
  • Add post-load processing for fused operators and runtime weight layouts.
  • Add clear startup failures for unsupported NewLoader combinations instead of failing during model execution.
  • Preserve the legacy loader for capabilities that have not yet migrated, including online weight updates and other explicitly detected configurations.
  • Add loader capability logging and UpdateWeights availability reporting.
  • Add focused regression coverage for model loading, tensor partitioning, quantized MoE strategies, multimodal loading, and cross-platform behavior.

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

  • NewLoader foundation and model-loading unit tests
  • Dense and MoE quantization routing tests
  • TP/EP partitioning and checkpoint integrity tests
  • CUDA, ROCm, PPU, and smoke-test coverage
  • FP8 per-tensor, per-channel, and per-block loading regressions

@LLLLKKKK LLLLKKKK left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 和量化分片补充了较完整的边界校验。

Comment thread rtp_llm/models_py/kernels/cuda/deepgemm_wrapper.py Outdated
except Exception as e:
print(f"Warning: Failed to import flashinfer.rope (likely running on CPU): {e}")
rope = None
@lru_cache(maxsize=1)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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 测试

@LLLLKKKK LLLLKKKK left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Code Review - PR #1405 (non-blocking suggestions)

2 条 P2/P3 建议,不阻塞合并。阻塞判定与完整摘要见上一条 review。


def test_replay_refreshes_plan_metadata(self):
"""Tensor-core replay must refresh FlashInfer plan metadata."""
config = self._create_config()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📍 实际位置 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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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 (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

Comment thread rtp_llm/models_py/kernels/cuda/fp8_quant.py
ignored = _merge_module_patterns(
"ignored_layers", ignored_layers, ignore_patterns
)
excluded = _merge_module_patterns("exclude_modules", exclude_modules, ignored)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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] = []

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

Comment thread rtp_llm/models_py/module_base.py Outdated
Comment thread rtp_llm/models_py/module_base.py
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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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())

@rtp-llm-review-bot rtp-llm-review-bot Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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,
)
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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}")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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]]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

@Oneydauh
Oneydauh force-pushed the develop/hengcang.wyd/new_loader_replacement branch from 598bd8e to 5b1d2e5 Compare September 24, 2026 09:26

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants