Prhub

#35962 [diffusion] Admit compatible quantized native encoders

原始 PR 作者 mickqian 合并时间 2026-08-22 18:51 文件变更 8 提交数 1 评论 1 代码增减 +66 / -105

执行摘要

扩散编码器量化准入改动态检测,兼容 checkpoint 免声明

PR body 明确指出旧机制的痛点:量化原生编码器通过模型类 allowlist 准入,但兼容性实际由 loader 和从 checkpoint 元数据构造的 linear 模块决定,导致每个兼容 checkpoint 都要新增一份模型特定声明,无法提供 day-0 行为。本 PR 的目标是把兼容性判定收敛到事实检查本身——模型构造出的量化层——并保持 fail-closed 语义,避免未知或不兼容组合被静默接受。

值得精读。这是一个典型的「静态声明 → 运行时能力检测」设计迁移,两点经验可复用:其一,fail-closed 的检查点应放在副作用(读取权重)之前;其二,泛化机制必须显式保留既有特殊生命周期(自管理量化、BnB4 委托),避免一刀切破坏边界情况。建议结合 test_qwen3_encoder.py 新增的 packed QKV scale 契约测试阅读,理解量化加载的完整数据流。

讨论亮点

本 PR 没有实质性的 review 讨论(review 评论数为 0),唯一 issue 评论来自 mintlify bot 的文档预览部署通知,无技术内容。设计取舍体现在实现本身:作者用 _require_quantized_encoder_layers 的运行时检查替代静态声明,同时显式保留两类例外(manages_checkpoint_quantization 模型自管理、标准 BnB4 委托 Transformers),避免泛化机制破坏既有特殊生命周期。PR body 自述的约束是:「This generalizes the native encoder loader; it does not claim that unrelated component materializers support every format.」

实现拆解

实现按以下 5 步拆解:

  1. 移除静态能力声明契约:在 python/sglang/multimodal_gen/runtime/models/encoders/base.py 删除 CheckpointQuantizationCapability frozen dataclass(含 backendmethods 字段)、EncoderTensorParallelMixin.checkpoint_quantization_capabilityTextEncoder.supported_checkpoint_quantization_methods,并清理 dataclassLiteral 相关 import;minimax_h3_qwen3vl.py 同步删除 MiniMaxH3Qwen3VLEncoder 上的 capability 声明。这一步把「支持哪些量化格式」的声明责任从模型类收回。

  2. 简化量化配置判定text_encoder_loader.py_configure_encoder_quantization 删除三组 capability 检查(capability 为 None、backend 非 diffusionquant_method 不在 methods),仅保留两个硬约束:manages_checkpoint_quantization 模型自管理例外、标准 BnB4 委托 Transformers 例外;其余量化 checkpoint 一律要求模型类继承 EncoderTensorParallelMixin(in-tree 原生编码器),否则拒绝。

  3. 新增动态层检测实现 fail closed:新增 _require_quantized_encoder_layers(model, component_name),遍历 model.modules(),只要存在一个 LinearBasequant_method 非空且非 UnquantizedLinearMethod 的模块即通过,否则抛 ComponentCheckpointUnsupportedError。该检查在 TextEncoderLoader.load_model 中位于 model.bind_encoder_tp_group() 之后、model.load_weights() 之前,保证任何权重读取前完成拒绝。

  4. 测试配套test_text_encoder_loader.py 把 H3 专属 FP8 测试改为通用的 test_serialized_checkpoint_configures_native_encoder(用 TextEncoder 基类),删除 test_encoder_class_must_opt_intest_srt_backend_is_not_admitted_without_an_adapter,新增 test_encoder_must_use_native_loadertest_rejects_native_encoder_without_quantized_layerstest_qwen3_encoder.py 新增 test_fp8_qkv_scale_uses_the_packed_parameter_loader,验证 QKV 融合投影的 weight_scale_inv 由 packed parameter 的 weight_loadershard_id 分发回写 q/k/v 分片;test_image_encoder_loader.py 删除基于旧错误信息的 CLIP 断言。

  5. 文档配套docs/docs/sglang-diffusion/quantization.mdx 更新组件行为表格并把编码器描述改为通用契约,删除模型特定的「MiniMax-H3 Text Encoder FP8」章节;docs/docs/sglang-diffusion/api/cli.mdx 同步更新准入描述,强调「无模型名 allowlist、按能力 materialization 判定」。

文件 模块 状态 重要度
python/sglang/multimodal_gen/runtime/loader/component_loaders/text_encoder_loader.py 编码器加载 modified 7.06
python/sglang/multimodal_gen/runtime/models/encoders/base.py 编码器基类 modified 6.65
python/sglang/multimodal_gen/runtime/models/encoders/minimax_h3_qwen3vl.py H3 编码器 modified 5.69
python/sglang/multimodal_gen/test/unit/test_text_encoder_loader.py 加载器测试 modified 6.25
python/sglang/multimodal_gen/test/unit/test_qwen3_encoder.py Qwen3 测试 modified 5.63
python/sglang/multimodal_gen/test/unit/test_image_encoder_loader.py 图像编码器测试 modified 4.19
docs/docs/sglang-diffusion/quantization.mdx 量化文档 modified 3.43
docs/docs/sglang-diffusion/api/cli.mdx CLI 文档 modified 2.35

关键符号

_require_quantized_encoder_layers _configure_encoder_quantization _resolve_and_configure_encoder_quantization _process_quantized_encoder_weights load_model load_scale

关键源码片段

python/sglang/multimodal_gen/runtime/loader/component_loaders/text_encoder_loader.py core-logic

准入机制核心文件:删除 CheckpointQuantizationCapability 三组静态检查,新增 _require_quantized_encoder_layers 运行时检测,并把调用点放在权重读取之前,实现 fail-closed 准入。

# text_encoder_loader.py —— 新增准入检查与既有后处理构成一组前后校验:
# _require_quantized_encoder_layers 在读取权重前把关,
# _process_quantized_encoder_weights 在权重之后执行量化后处理。def _require_quantized_encoder_layers(
    model: nn.Module,
    component_name: str,
) -> None:
    # 替代旧的静态 capability 声明:模型无需显式声明支持某种量化格式,
    # 只要它依据 checkpoint 元数据构造出带量化方法的 linear 层即可准入。
    if any(
        isinstance(module, LinearBase)
        and module.quant_method is not None
        and not isinstance(module.quant_method, UnquantizedLinearMethod)
        for module in model.modules()
    ):
        return
    # 一个量化层都没有就直接拒绝,保证「量化 checkpoint + 不兼容实现」
    # 组合在读取任何权重之前 fail closed,避免静默降级或中途报错。
    raise ComponentCheckpointUnsupportedError(
        f'The native {type(model).__name__} implementation does not construct '
        f'quantized linear layers for {component_name!r}'
    )
​
​
def _process_quantized_encoder_weights(
    model: nn.Module,
    process_device: torch.device,
    component_name: str,
) -> int:
    processed_layers = 0
    for module in model.modules():
        if not isinstance(module, LinearBase):
            continue
        quant_method = module.quant_method
        if quant_method is None or isinstance(quant_method, UnquantizedLinearMethod):
            continue
​
        origin_device = _module_tensor_device(module)
        should_stage = origin_device is not None and origin_device != process_device
        if should_stage:
            module.to(process_device) # 跨设备后处理前先暂存到目标设备
        try:
            quant_method.process_weights_after_loading(module)
            processed_layers += 1
        finally:
            # 后处理方法可能替换参数或注册新 buffer;把完整层移回原设备,
            # 让组件驻留状态保持权威性。
            if should_stage:
                module.to(origin_device)
    # 有了 _require_quantized_encoder_layers 的前置把关,
    # processed_layers == 0 在这里只是防御性兜底。
    if processed_layers == 0:
        raise ValueError(
            f'The {component_name!r} checkpoint declares quantization, but the '
            'model did not construct any quantized linear layers'
        )
    return processed_layers
​
​
# TextEncoderLoader.load_model 中的调用点:模型构造并绑定 TP 组之后、
# 读取任何权重之前,立即校验量化层是否存在。
model.bind_encoder_tp_group(encoder_tp_group)
if quant_config is not None:
    _require_quantized_encoder_layers(model, component_name)
python/sglang/multimodal_gen/test/unit/test_qwen3_encoder.py test-coverage

新增 packed QKV scale 加载契约测试,锚定 FP8 checkpoint 的融合投影 scale 分发数据流。

# test_qwen3_encoder.py —— 锚定 FP8 checkpoint 的 packed QKV scale 契约:
# Qwen3 的 QKV 融合投影把 q/k/v 三份 scale 打包成单个参数,
# 由 weight_loader 按 shard_id 分发回写,checkpoint 侧仍以未融合的
# q_proj 前缀出现。
def test_fp8_qkv_scale_uses_the_packed_parameter_loader():
    model = Qwen3ForCausalLM.__new__(Qwen3ForCausalLM)
    torch.nn.Module.__init__(model)
    layer = torch.nn.Module()
    layer.self_attn = torch.nn.Module()
    layer.self_attn.qkv_proj = torch.nn.Module()
    scale = torch.nn.Parameter(torch.zeros(3, 1), requires_grad=False)
​
    # 模拟 packed 参数的加载器:按 shard_id 把外部 scale 写进对应分片。
    def load_scale(param, loaded_scale, shard_id):
        param.data[{'q': 0, 'k': 1, 'v': 2}[shard_id]].copy_(loaded_scale)
​
    scale.weight_loader = load_scale
    layer.self_attn.qkv_proj.register_parameter('weight_scale_inv', scale)
    model.layers = torch.nn.ModuleList([layer])
    model.config = SimpleNamespace(
        arch_config=SimpleNamespace(
            stacked_params_mapping=[('.qkv_proj', '.q_proj', 'q')]
        )
    )
​
    # 权重名仍以未融合的 .q_proj 形式出现在 checkpoint 中,
    # 但最终回写目标是融合后的 .qkv_proj.weight_scale_inv。
    loaded = model.load_weights(
        [('model.layers.0.self_attn.q_proj.weight_scale_inv', torch.tensor([2.0]))]
    )
​
    assert loaded == {'layers.0.self_attn.qkv_proj.weight_scale_inv'}
    torch.testing.assert_close(scale[:, 0], torch.tensor([2.0, 0.0, 0.0]))

评论区精华

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

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

风险与影响

  1. 行为语义变化:CLIP 等非原生编码器遇到量化 checkpoint 时,错误信息从「no checkpoint quantization capability」变为「requires an in-tree native encoder」,test_image_encoder_loader.py 中对应断言已被删除,需在后续版本中确认该路径仍被其他测试覆盖。
  2. 动态检测依赖 quant_method 标记准确性:若某模型构造了 LinearBase 子类却漏设 quant_method 会被误拒;反之误标也会误准入,并在 _process_quantized_encoder_weights 后处理阶段才暴露。
  3. 静态契约符号移除CheckpointQuantizationCapabilitysupported_checkpoint_quantization_methods 是模块内导出符号,仓库引用已清理,但外部插件或自定义编码器若引用这些符号会受影响。
  4. 性能影响model.modules() 遍历仅在每次加载时执行一次,开销可忽略。
  5. 回归面:改动集中在 sglang/multimodal_gen 编码器加载链路,推理 kernel 无变化;CI 中 Extra 运行失败,需确认是否与本次准入逻辑相关。

对用户:MiniMax-H3 的 FP8 text encoder 以及任何其他兼容量化格式的原生编码器 checkpoint 均可直接加载,无需模型特定声明,获得 day-0 支持;不兼容组合仍会在读取权重前明确报错。对系统:编码器加载路径分支减少,契约从「模型特定声明」收敛为「实现构造量化层」,便于后续新增编码器。对团队:接入新原生编码器的量化 checkpoint 时少一步声明工作,文档从模型特定案例改为通用契约。影响范围仅限 diffusion 编码器加载链路,不涉及推理 kernel、调度或显存管理。

核心加载路径变更 依赖 quant_method 标记准确性 错误信息行为变化 静态契约符号移除

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论