Prhub

#35868 [diffusion] Preserve PEFT LoRA semantics

原始 PR 作者 mickqian 合并时间 2026-08-21 22:58 文件变更 27 提交数 3 评论 1 代码增减 +354 / -40

执行摘要

新增 PEFT LoRA 语义支持,fail-closed 拒绝无法表示的语义

PR body 说明这是对 #35774 的后续,目标是在现有 LoRA 格式转换之前先规范化常见的 PEFT 模型包装器和命名适配器槽位,应用 use_rslora 和 per-layer alpha_pattern,并对无法用 native diffusion LoRA 层表示的 DoRA 和辅助/运行时 PEFT 特性 fail-closed,避免静默运行普通 LoRA 数学产生错误结果。

值得精读。关注 peft_adapter.py 的 fail-closed 设计、normalize_peft_keys 的前缀/槽位处理、scale_fused_sections 对融合 section 的逐层 alpha 实现,以及如何在不新增 CLI 的情况下读取 adapter_config.json

讨论亮点

本 PR 没有 review 评论。设计取舍在 body 中说明:PEFT 语义不支持的特性 fail before weight injection rather than silently running ordinary LoRA math;pipelines_core/lora/__init__.py 不 re-export 符号以避免隐式依赖与循环导入。Issue 评论只有 /tag-and-rerun-ci,无技术讨论。

实现拆解

  1. 新增 pipelines_core/lora/peft_adapter.py,作为 PEFT 兼容层核心:load_peft_config(读取同目录 adapter_config.json)、get_peft_lora_alpha(校验 alpha 为正整数)、normalize_peft_keys(剥离 base_model.model. / peft_model.base_model.model. 前缀和命名槽位如 lora_A.default.weightlora_A.weight)、_validate_peft_features(fail-closed 校验)、apply_peft_config(应用 rslora/alpha_pattern 缩放)、scale_fused_sections(处理融合投影 section 的逐层缩放)。
  2. format_adapter.pynormalize_lora_state_dict 中新增 adapter_config 参数,并在格式检测前先执行 normalize_peft_keys,格式转换后执行 apply_peft_config,使所有 LoRA 格式统一走 PEFT 语义归一化。
  3. pipeline.pyload_lora_adapter 中移除手写 json 读取逻辑,统一走 load_peft_config + get_peft_lora_alpha_store_fused_lora_groups 引入 scale_fused_sections,使融合投影(如 attn.qkv 多 section)也能保留逐层 alpha。
  4. 目录重构:lora_pipeline.pypipelines_core/lora/pipeline.pylora_format_adapter.pypipelines_core/lora/format_adapter.py,并更新 pipelines_core/__init__.pygpu_worker.py、各 pipeline 文件等共 17 处导入路径。
  5. 测试配套:新增 test/unit/test_lora_peft.py(rslora 缩放保持 delta、unsupported fail-closed、非法 alpha fail-closed),扩展 test/unit/test_fused_lora_compose.py(逐层 alpha 的融合 section 数值一致性)。其中第 2、3 步是关键行为变化,第 4 步是纯组织重构,PR body 说明相对 main 的生产文件 churn 为 +51/-31,其中 17 对行是目录移动的导入修改。
文件 模块 状态 重要度
python/sglang/multimodal_gen/runtime/pipelines_core/lora/peft_adapter.py PEFT 适配 added 9.17
python/sglang/multimodal_gen/runtime/pipelines_core/lora/pipeline.py LoRA 管线 renamed 7.27
python/sglang/multimodal_gen/runtime/pipelines_core/lora/format_adapter.py 格式转换 renamed 6.56
python/sglang/multimodal_gen/test/unit/test_lora_peft.py 单元测试 added 6.73
python/sglang/multimodal_gen/test/unit/test_fused_lora_compose.py 融合测试 modified 5.45
python/sglang/multimodal_gen/runtime/pipelines_core/__init__.py 包导出 modified 4.7

关键符号

load_peft_config get_peft_lora_alpha normalize_peft_keys _validate_peft_features apply_peft_config scale_fused_sections normalize_lora_state_dict _store_fused_lora_groups load_lora_adapter

关键源码片段

python/sglang/multimodal_gen/runtime/pipelines_core/lora/peft_adapter.py core-logic

新增核心 PEFT 适配模块,包含键名规范化、配置校验、缩放计算与 fail-closed 校验,是本 PR 的核心实现。

"""PEFT 检查点语义适配到 native diffusion LoRA 层。"""# 正则匹配命名适配器槽位,例如 `lora_A.default.weight` 中的 `default`
_ADAPTER_SLOT = re.compile(r"(\.lora_[AB])\.([^.]+)\.weight$")
# 常见的 PEFT 包装前缀,按顺序尝试
_WRAPPER_PREFIXES = ("peft_model.base_model.model.", "base_model.model.")
​
​
def load_peft_config(weight_path: str) -> dict[str, Any]:
    """读取 LoRA 权重同目录下的 adapter_config.json,不存在时返回空字典。"""
    path = Path(weight_path).with_name("adapter_config.json")
    if not path.is_file():
        return {}
    with path.open(encoding="utf-8") as file:
        config = json.load(file)
    if not isinstance(config, dict):
        raise ValueError("PEFT adapter_config.json must contain a JSON object")
    return config
​
​
def get_peft_lora_alpha(config: Mapping[str, Any]) -> int | None:
    """校验并提取 lora_alpha,必须是正整数,否则抛出 ValueError。"""
    alpha = config.get("lora_alpha")
    if alpha is None:
        return None
    if (
        isinstance(alpha, bool)
        or not isinstance(alpha, (int, float))
        or alpha <= 0
        or isinstance(alpha, float)
        and not alpha.is_integer()
    ):
        raise ValueError("PEFT lora_alpha must be a positive integer")
    return int(alpha)
​
​
def normalize_peft_keys(state_dict: Mapping[str, torch.Tensor]) -> dict[str, torch.Tensor]:
    """剥离统一的 PEFT 包装前缀和命名适配器槽位,得到规范化键名。"""
    # 选择能匹配所有键的最长包装前缀,避免误删
    prefix = next(
        (
            prefix
            for prefix in _WRAPPER_PREFIXES
            if state_dict and all(name.startswith(prefix) for name in state_dict)
        ),
        "",
    )
    normalized: dict[str, torch.Tensor] = {}
    slots = set()
    has_bare_weights = False
    for name, tensor in state_dict.items():
        name = name.removeprefix(prefix)
        match = _ADAPTER_SLOT.search(name)
        if match is not None:
            slots.add(match.group(2)) # 记录命名的 adapter 槽位名
        elif name.endswith((".lora_A.weight", ".lora_B.weight")):
            has_bare_weights = True # 存在未命名的裸权重
        target = _ADAPTER_SLOT.sub(r"\1.weight", name) # 移除槽位名,如 lora_A.default.weight -> lora_A.weight
        if target in normalized:
            raise ValueError(
                "LoRA checkpoint contains multiple PEFT adapter slots for "
                f"the same tensor: {target!r}"
            )
        normalized[target] = tensor
    if len(slots) > 1:
        raise ValueError(
            f"LoRA checkpoint contains multiple PEFT adapter slots: {sorted(slots)}"
        )
    if slots and has_bare_weights:
        raise ValueError("LoRA checkpoint mixes named and unnamed PEFT adapter slots")
    return normalized
python/sglang/multimodal_gen/runtime/pipelines_core/lora/pipeline.py rename-or-move

LoRA 加载入口与融合分组写入逻辑修改,接入 PEFT 配置读取和逐层 alpha 缩放。

def _store_fused_lora_groups(
    adapter: dict[str, torch.Tensor],
    to_merge_params: dict[Hashable, dict[Any, Any]],
    adapter_alpha: int | None,
    device: torch.device | str,
) -> None:
    """将延迟收拢的融合 lora_A/B 分组写回 adapter 字典。"""
    for a_key, a_parts in list(to_merge_params.items()):
        if not str(a_key).endswith(".lora_A"):
            continue
        base = str(a_key)[: -len(".lora_A")]
        b_key = f"{base}.lora_B"
        b_parts = to_merge_params.get(b_key)
        n = max(a_parts) + 1
        # 要求 A/B 分组的索引都是连续的 0..n-1,否则跳过不融合
        if (
            b_parts is None
            or set(a_parts) != set(range(n))
            or set(b_parts) != set(range(n))
        ):
            continue
        a_list = [a_parts[i] for i in range(n)]
        b_list = [b_parts[i] for i in range(n)]
        # 若存在逐层 alpha_pattern,缩放 B 部分后再拼接,保留 PEFT 语义
        scaled_b = scale_fused_sections(
            a_parts,
            b_parts,
            to_merge_params.get(f"{base}.alpha", {}),
            adapter_alpha,
        )
        a, b, fused_alpha = stack_or_compose_fused_lora(
            a_list, scaled_b or b_list, None if scaled_b else adapter_alpha
        )
        if scaled_b:
            # 逐层缩放后统一 alpha 不能再用 r_eff,改为总行数
            fused_alpha = a.shape[-2]
        adapter[str(a_key)] = a.to(device)
        adapter[b_key] = b.to(device)
        if fused_alpha is not None:
            adapter[f"{base}.alpha"] = torch.tensor(float(fused_alpha), device=device)
python/sglang/multimodal_gen/runtime/pipelines_core/lora/format_adapter.py rename-or-move

格式转换入口接入 PEFT 键名规范化和配置应用,使所有 LoRA 格式统一支持 PEFT 语义。

def normalize_lora_state_dict(
    state_dict: Mapping[str, torch.Tensor],
    logger: Optional[logging.Logger] = None,
    *,
    adapter_config: Mapping[str, Any] | None = None,
) -> Dict[str, torch.Tensor]:
    """Normalize any supported LoRA format into a single canonical layout."""
    log = logger or globals()["logger"]
​
    # 先剥离 PEFT 包装前缀和命名槽位,再做格式检测,保证检测逻辑不受 PEFT 包装干扰
    state_dict = normalize_peft_keys(state_dict)
    keys = list(state_dict.keys())
    log.info(
        "[LoRAFormatAdapter] normalize_lora_state_dict called, #keys=%d",
        len(keys),
    )
    if keys:
        log.info(
            "[LoRAFormatAdapter] before convert, sample keys (<=20): %s",
            ", ".join(_sample_keys(keys, 20)),
        )
​
    fmt = detect_lora_format_from_state_dict(state_dict)
    log.info("[LoRAFormatAdapter] detected format: %s", fmt)
​
    normalized = convert_lora_state_dict_by_format(state_dict, fmt, log)
    # 格式转换后再应用 PEFT 配置,将 use_rslora/alpha_pattern 等语义写入权重
    normalized = apply_peft_config(normalized, adapter_config or {})
​
    norm_keys = list(normalized.keys())
    if norm_keys:
        log.info(
            "[LoRAFormatAdapter] after convert, sample keys (<=20): %s",
            ", ".join(_sample_keys(norm_keys, 20)),
        )
​
    return normalized

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

行为风险:normalize_peft_keys 要求所有键共享同一个包装前缀,若检查点混用裸权重和命名槽位会直接报错;这是有意 fail-closed,但可能让原本勉强能跑的权重被拒绝。alpha_pattern 按正则匹配目标层,误匹配会导致缩放错误。兼容风险:lora_pipeline.py 等旧路径被移动,外部直接 from ...lora_pipeline import ... 的代码会失效,仓库内已同步,但下游用户若引用旧路径需更新。数值风险:scale_fused_sections 改变了融合 LoRA 的叠加方式(按层缩放后再拼接),测试验证了数值一致,但覆盖面限于测试中的配置。无性能影响:新增逻辑只在加载期执行,不影响推理。

对用户:支持 PEFT 格式 LoRA(含 use_rslora、alpha_pattern),语义更准确;DoRA 等会得到明确报错而非错误结果。对系统:LoRA 加载路径统一,新增一个适配层,无性能影响。对团队:代码组织更清晰,为后续扩展更多 PEFT 特性奠定基础。影响范围限于 diffusion 模块的 LoRA 加载链路。

核心路径变更 fail-closed 可能拒绝原本可用的权重 目录移动影响外部导入 alpha_pattern 正则匹配风险 融合 section 数值需回归验证

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论