Prhub

#34980 [Diffusion] Native Hunyuan3D Paint and Delight models

原始 PR 作者 mickqian 合并时间 2026-08-16 10:03 文件变更 11 提交数 6 评论 1 代码增减 +2431 / -1756

执行摘要

Hunyuan3D Paint/Delight 迁移为 SGLang 原生模型,提速约 4 倍

PR body 明确说明动机是“replace the Diffusers-owned Hunyuan3D Paint and Delight modules with native SGLang model ownership”,并强调“so residency, CPU offload, and layerwise offload are managed consistently”。旧实现中 Paint/Delight 组件在 stage 内自行通过 Diffusers pipeline 加载模型,无法纳入组件的统一内存与 offload 管理,且存在静默回退路径掩盖模型缺失问题。

值得精读。重点看三处设计:一是 Hunyuan3DPaintTransformerBlock 的 reference 特征缓存与 mode 机制;二是 StableDiffusionUNetConfig.validate 的严格数据契约;三是 paint 阶段如何通过 use_declared_component 把模型加载让渡给组件管理器。若团队计划迁移其他 diffusion 模型,此 PR 是 diffusers 解除依赖的教科书式案例。

讨论亮点

该 PR 没有任何 review 评论,仅有一条作者自己的 /tag-and-rerun-ci extra 指令。核心验证结论全部来自 PR body:

Paint UNet parity at B=2, N=6, H=W=64: max absolute error 0, cosine 1.0
Delight two-step image path at 512x512: pixel-exact with the previous Diffusers path
Paint denoising step: previous 416.688 ms → native first step 98.038 ms → cached step 76.888 ms

值得注意的设计权衡(从实现推导):reference attention 通过共享 condition_embed_dict 跨 step 缓存,以空间换时间;turbo 模式新增注意力分支从 attn1 复制权重并零化输出投影,使分支初始化为恒等映射。这些决策没有经过 review 交锋,建议后续维护者关注。

实现拆解

  1. 新增原生 SD 2.1 模型层:在 python/sglang/multimodal_gen/runtime/models/dits/stable_diffusion.py 新增 918 行,定义 StableDiffusionUNetConfig(frozen dataclass,from_dict 解析 checkpoint 配置并 validate 严格校验四层 SD2.1 布局)、StableDiffusionAttention(基于 F.scaled_dot_product_attention)、BasicTransformerBlockTransformer2DModelResnetBlock2D 等;同时在 configs/models/vaes/stable_diffusion.py 新增 StableDiffusionVAEArchConfigStableDiffusionVAEConfig,并将 runtime/models/vaes/autoencoder.pyAutoencoderKL 构造函数签名从 FluxVAEConfig 泛化为 VAEConfig
  2. 新增 Hunyuan3DPaintUNet:在 hunyuan3d_paint.py 中把 base UNet 深拷贝为 unetunet_dual 两份,通过 _replace_transformer_blocks 将 transformer 块替换为 Hunyuan3DPaintTransformerBlockunet 开启 Multiview Attention 与 Reference View Attention,unet_dual 保持标准 UNet。conv_in 改为 12 通道并新增 learned_text_clip_gen/learned_text_clip_ref 参数与 class embedding;forward 通过 cross_attention_kwargs 中的 mode(w/r)与 condition_embed_dict 跨 denoising step 缓存参考特征,避免重复计算。
  3. 重写 paint 阶段paint.py 净删 200+ 行,删除对 StableDiffusionInstructPix2PixPipeline 的 stage-local 加载与静默回退,改为构造函数注入已装配的原生组件(transformer/vae/text encoder/tokenizer/scheduler),通过 use_declared_component 声明组件使用,component_uses 向组件管理器注册 prompt/encode/denoise/decode 各 phase。
  4. 管线装配与清理hunyuan3d_pipeline.py_resolve_paint_dir 泛化为 _resolve_model_subfolder,新增 _load_texture_components_load_stable_diffusion_unet_load_stable_diffusion_vae_load_component_weights 等加载助手,以 meta device + strict 权重加载;CLIP 文本编码器走 TextEncoderLoaderhunyuan3d.py 删除 841 行 Diffusers 胶水代码(SGLangAttentionWrapperBasic2p5DTransformerBlock 等);shape.py 增加 warmup 短路避免启动时导出文件。
  5. 配套测试与语义修正:新增 test_hunyuan3d_native_texture_models.py(原生 UNet/VAE 与 Diffusers 逐层对齐、Paint reference 分支、warmup 输出、turbo schedule);修正 Delight 采样语义以保持与旧路径 bit-exact;turbo 改走 canonical LCM schedule,去掉 redundant scheduler 告警。
文件 模块 状态 重要度
python/sglang/multimodal_gen/runtime/models/dits/stable_diffusion.py UNet 模型 added 9.17
python/sglang/multimodal_gen/runtime/models/dits/hunyuan3d_paint.py Paint 模型 added 9.28
python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/hunyuan3d/paint.py Paint 阶段 modified 8.86
python/sglang/multimodal_gen/runtime/pipelines/hunyuan3d_pipeline.py 管线装配 modified 8.69
python/sglang/multimodal_gen/runtime/models/dits/hunyuan3d.py 3D 模型 modified 8.94
python/sglang/multimodal_gen/test/unit/test_hunyuan3d_native_texture_models.py 单元测试 added 7.95
python/sglang/multimodal_gen/configs/models/vaes/stable_diffusion.py VAE 配置 added 7.42
python/sglang/multimodal_gen/runtime/models/vaes/autoencoder.py VAE 模型 modified 5.04
python/sglang/multimodal_gen/configs/pipeline_configs/hunyuan3d.py 管线配置 modified 5.0
python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/hunyuan3d/shape.py 形状阶段 modified 4.94
python/sglang/multimodal_gen/runtime/managers/memory_managers/layerwise_offload_components.py 卸载管理 modified 4.62

关键符号

Hunyuan3DPaintTransformerBlock.forward Hunyuan3DPaintUNet.__init__ Hunyuan3DPaintUNet.forward compute_voxel_grid_mask compute_multi_resolution_mask StableDiffusionUNetConfig.from_dict StableDiffusionUNetConfig.validate StableDiffusionAttention.forward _load_texture_components _load_stable_diffusion_unet _load_stable_diffusion_vae _run_delight _encode_delight_prompts component_uses

关键源码片段

python/sglang/multimodal_gen/runtime/models/dits/stable_diffusion.py core-logic

新增 918 行原生 SD 2.1 UNet 实现,是整个迁移的模型地基,包含严格配置校验与原生注意力实现

# 原生 SD 2.1 UNet 配置:frozen dataclass,直接承载 Hunyuan3D checkpoint 的 config.json
@dataclass(frozen=True)
class StableDiffusionUNetConfig:
    sample_size: int
    in_channels: int
    out_channels: int
    down_block_types: tuple[str, ...]
    up_block_types: tuple[str, ...]
    block_out_channels: tuple[int, ...]
    layers_per_block: int
    cross_attention_dim: int
    attention_head_dim: tuple[int, ...]
    use_linear_projection: bool
​
    @classmethod
    def from_dict(cls, config: dict[str, Any]) -> StableDiffusionUNetConfig:
        block_out_channels = tuple(config['block_out_channels'])
        # attention_head_dim 可能是 int(表示所有 stage 相同),也可能是列表,
        # 这里统一展开成与 block_out_channels 等长的元组。
        attention_head_dim_value = config['attention_head_dim']
        attention_head_dim = (
            (attention_head_dim_value,) * len(block_out_channels)
            if isinstance(attention_head_dim_value, int)
            else tuple(attention_head_dim_value)
        )
        parsed = cls(
            sample_size=int(config['sample_size']),
            in_channels=int(config['in_channels']),
            out_channels=int(config['out_channels']),
            down_block_types=tuple(config['down_block_types']),
            up_block_types=tuple(config['up_block_types']),
            block_out_channels=block_out_channels,
            layers_per_block=int(config['layers_per_block']),
            cross_attention_dim=int(config['cross_attention_dim']),
            attention_head_dim=attention_head_dim,
            use_linear_projection=bool(config.get('use_linear_projection', False)),
        )
        parsed.validate()
        return parsed
​
    def validate(self) -> None:
        # 原生实现目前只支持 Hunyuan3D 使用的 SD2.1 四层布局:
        # 一旦 checkpoint 出现新的 block 类型组合,这里会立即抛错,
        # 避免在静默状态下产出错误结果。
        expected_down = (
            'CrossAttnDownBlock2D', 'CrossAttnDownBlock2D',
            'CrossAttnDownBlock2D', 'DownBlock2D',
        )
        expected_up = (
            'UpBlock2D', 'CrossAttnUpBlock2D',
            'CrossAttnUpBlock2D', 'CrossAttnUpBlock2D',
        )
        if self.down_block_types != expected_down or self.up_block_types != expected_up:
            raise ValueError(
                'The native SD2 UNet currently supports only the Hunyuan3D '
                'four-level SD2.1 block layout.'
            )
        if not self.use_linear_projection:
            raise ValueError('Hunyuan3D SD2.1 checkpoints require linear projection.')
python/sglang/multimodal_gen/runtime/models/dits/hunyuan3d_paint.py core-logic

新增 386 行 Hunyuan3D Paint 专用多视角 UNet,实现 reference/multiview 注意力与跨 step 特征缓存

# 核心多视角 UNet 的 transformer 块包装器:在标准 SD2 注意力之外,
# 追加 Multiview Attention(MVA)与 Reference View Attention(RVA)两条分支。
class Hunyuan3DPaintTransformerBlock(nn.Module):
    def __init__(self, transformer, layer_name, *, use_multiview_attention, use_reference_attention, is_turbo):
        super().__init__()
        self.transformer = transformer # 复用原生 SD2 的 BasicTransformerBlock
        self.layer_name = layer_name
        self.attn_multiview = StableDiffusionAttention(...) if use_multiview_attention else None
        self.attn_refview = StableDiffusionAttention(...) if use_reference_attention else None
        if is_turbo:
            self._initialize_added_attention()
​
    def _initialize_added_attention(self) -> None:
        # turbo 模式没有独立训练的 MVA/RVA 权重:从 attn1 复制权重并把输出投影清零,
        # 使新增分支初始化为零贡献,保证未激活时不改变输出。
        for attention in (self.attn_multiview, self.attn_refview):
            if attention is None:
                continue
            attention.load_state_dict(self.transformer.attn1.state_dict())
            with torch.no_grad():
                for parameter in attention.to_out[0].parameters():
                    parameter.zero_()
​
    def forward(self, hidden_states, encoder_hidden_states, attention_mask=None, cross_attention_kwargs=None):
        options = {} if cross_attention_kwargs is None else cross_attention_kwargs
        num_views = int(options.get('num_in_batch', 1))
        mode = options.get('mode')
        condition_embeddings = options.get('condition_embed_dict')
        if mode is not None and not isinstance(condition_embeddings, dict):
            raise ValueError('Hunyuan3D reference attention requires a shared cache.')
​
        normalized = self.transformer.norm1(hidden_states)
        # 先走标准自注意力(与普通 SD2 UNet 完全一致)
        hidden_states = hidden_states + self.transformer.attn1(normalized, attention_mask=attention_mask)
​
        # mode 含 'w':把当前层归一化特征写入共享缓存,供参考视角复用
        if mode is not None and 'w' in mode:
            condition_embeddings[self.layer_name] = rearrange(
                normalized, '(b n) l c -> b (n l) c', n=num_views
            )
​
        # mode 含 'r':从缓存取参考特征并按 batch 展开,做参考视角注意力
        if mode is not None and 'r' in mode and self.use_reference_attention:
            reference = condition_embeddings[self.layer_name]
            reference = reference.unsqueeze(1).repeat(1, num_views, 1, 1)
            reference = rearrange(reference, 'b n l c -> (b n) l c')
            reference_output = self.attn_refview(normalized, encoder_hidden_states=reference)
            reference_scale = self._broadcast_scale(
                1.0 if self.is_turbo else options.get('ref_scale', 1.0),
                reference_output, num_views,
            )
            hidden_states = hidden_states + reference_scale * reference_output
​
        if num_views > 1 and self.use_multiview_attention:
            # 把 (b n) 维度折叠成 batch,让 n 个视角互相做注意力
            multiview = rearrange(normalized, '(b n) l c -> b (n l) c', n=num_views)
            position_mask = None
            if isinstance(options.get('position_attn_mask'), dict):
                position_mask = options['position_attn_mask'].get(multiview.shape[1])
            multiview_output = self.attn_multiview(
                multiview, encoder_hidden_states=multiview, attention_mask=position_mask
            )
            multiview_output = rearrange(multiview_output, 'b (n l) c -> (b n) l c', n=num_views)
            hidden_states = hidden_states + multiview_output
​
        return hidden_states + self.transformer.ff(self.transformer.norm3(hidden_states))

评论区精华

无 review 评论,验证数据集中在 PR body 测试

PR 没有任何 review 评论,仅作者自己触发了 /tag-and-rerun-ci extra。数值验证(Paint/Delight parity、性能、CLIP cosine)均由作者在 PR body 中给出,未经过第三方 review 复核。

结论:无待解决的 review 意见;建议后续维护者独立复跑 parity 测试确认数据可复现。 · closed

移除静默回退路径的行为影响 设计

PR body 明确将 'remove stage-local model loading and silent fallback paths' 列为目标。旧实现中模型缺失时仅 warning 并继续,新实现会直接抛错。这是有意的行为收紧,但未在讨论中展开对用户的影响。

结论:设计上有意收紧;实际影响是模型目录不完整时启动更早失败,错误信息更明确。 · closed

风险与影响

  1. 大段删除的引用风险hunyuan3d.py 删除 841 行,SGLangAttentionWrapperBasic2p5DTransformerBlock_chunked_feed_forward 等符号被移除,若有其他模块(如自定义 checkpoint 或工具脚本)仍在引用会直接 ImportError。
  2. 精度非完全一致:native CLIP 与 Transformers checkpoint 输出 cosine 为 0.9999982,并非 bit-exact,可能对依赖精确文本特征的场景产生微小差异。
  3. 硬编码配置校验StableDiffusionUNetConfig.validate() 硬编码四层 SD2.1 布局与 use_linear_projection=True,未来若出现新的 checkpoint 布局会直接拒绝加载。
  4. 行为变更:移除静默回退后,模型缺失或下载失败会直接抛错,对依赖旧静默行为的用户是行为变化。
  5. 测试覆盖范围:单元测试使用 32 通道小配置验证结构层面 parity,真实 checkpoint 的 E2E 验证主要依赖作者自测数据,缺少 CI 中可重复的数值回归基准。

影响范围限定在 sglang.multimodal_gen 下的 Hunyuan3D2 pipeline:Paint/Delight 组件从 Diffusers 管控转为 SGLang 原生组件,CPU offload、layerwise offload 与 residency 控制从此统一;Paint 去噪单步提速约 4-5 倍,显著降低纹理生成延迟。对使用标准 --dit-cpu-offload--dit-layerwise-offload、VAE offload 的现有用户,CLI 行为不变;pipeline config 新增 text_encoder_configsnative_only_components 字段,仅影响内部装配。对团队而言,此 PR 是 diffusion 原生集成契约(PR#34952)的首次完整落地,为后续其他模型去 Diffusers 化提供了可参照样板。

核心路径重构 大段代码删除 精度非完全一致 移除静默回退 硬编码配置校验 缺少第三方 review

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论