Prhub

#33880 [diffusion] optimization: reduce minimax h3 mps memory pressure

原始 PR 作者 mickqian 合并时间 2026-08-19 13:35 文件变更 26 提交数 30 评论 1 代码增减 +757 / -98

执行摘要

H3 MPS 内存优化:权重分阶段卸载、前向分块流式

MiniMax H3 的 packed 长序列和大组件工作集在 MPS 统一内存上压力极大。PR body 的动机原文是:"MiniMax-H3's long packed sequence and large component working set need stricter placement on MPS unified memory." 具体而言,融合 QKV 投影输出在 768px 分辨率下约 1.45 GiB,与 packed residual、SDPA workspace 同时驻留易挤占系统内存;同时 MPS 没有 CUDA 式 pinned memory 与独立 copy stream,原 layerwise offload 的异步预取与 Event 同步模型并不适用。

值得精读,尤其 _forward_mps_streamed_attention 是“分块流式计算规避统一内存峰值”的典型范例,LayerwiseOffloadManager 对同步/异步双路径的抽象也有借鉴价值。阅读时建议对照 commit 历史中多次 revert 的 chunk size 调参过程,理解 MPS 内存-吞吐权衡的敏感性;但由于 Draft 合并且 CI Extra 失败,合入后应优先补齐端到端精度验证,再在真实 MPS 设备上推广。

讨论亮点

本 PR 没有任何 reviewer 评论,唯一交互是作者在 Issue 评论区触发 /tag-and-rerun-ci 重跑 CI。PR body 明确标注 Draft 状态并声明“end-to-end and accuracy validation are pending”,且 CI Extra 运行(Run #32208109060)状态为失败,说明正确性验证尚未收敛;这些未决事项直接决定了本 PR 的合入风险等级。

实现拆解

实现按五步展开,全部落在 multimodal_gen 运行时,CUDA 路径仅做兼容性收口:

  1. LayerwiseOffloadManager 的 MPS 化改造(python/sglang/multimodal_gen/runtime/managers/memory_managers/layerwise_offload.py):__init__ 新增 initialize 参数使初始化可延迟;识别 _synchronous_mps 后禁用 pinned memory 与 copy stream,prefetch_size 显式保持 0 避免下一层与当前层同时驻留;新增 _mps_cpu_weights 字典保留每层原始 CPU tensor,_initialize_mps_cpu_weights 把权重迁回 CPU 并用共享空 tensor 占位;prefetch_layer 在 MPS 分支做同步搬移,release_layer 强制 torch.mps.synchronize() + empty_cache()sync_layer_to_cpu 直接跳过(推理不改参数)。同时新增 _capture_mps_cpu_non_layer_weights/materialize_mps_non_layer_weights/release_mps_non_layer_weights/restore_mps_cpu_non_layer_weights 管理条件投影、时间嵌入等非逐层权重。
  2. 加载链路先 CPU 后设备(transformer_loader.py、vae_loader.py、text_encoder_loader.py、fsdp_load.py):三个 loader 新增 customized_load_kwargs_for_component,在 MPS 且组件被配置为 layerwise offload 时返回 cpu_offload_flag=Trueload_customized 接受该参数,checkpoint_load_device 强制为 CPU,WeightLoadPlan.for_component 增加 mps_layerwise_cpu_staging 标志;VAE loader 用 load_state_dict(..., assign=True) 直接复用 CPU 原始 tensor,避免加载后再拷一份。
  3. DiT 前向分块流式化(python/sglang/multimodal_gen/runtime/models/dits/minimax_h3.py、sdpa.py):_forward_mps_streamed_attention 第一阶段按 128 token chunk 生成完整 K/V 且不保留 Q,第二阶段逐序列逐 chunk 投影 Q、立即过 attention 与 out_proj 回写;MLP 与最终投影(video_out/audio_out)同样按 128 chunk 分块;_apply_rope_apply_rope_qk 中抽取 eager 路径;qkv 权重标记 mps_zero_copy_unsafe=True 表明 checkpoint 行交错需重排、不可零拷贝;forward 入口以 x.device.type == "mps" and not ulysses_active 切到流式路径。
  4. 准入与平台参数收紧(pipeline_configs/minimax_h3.py、server_args.py):validate_server_args 在 MPS 下强制 transformer/text_encoder/video_vae/audio_vae 全部为 LAYERWISE_OFFLOAD 并拒绝 torch.compile;_adjust_platform_specific 收窄为仅允许 num_gpus=1、residency 只支持 RESIDENT 或 LAYERWISE_OFFLOAD,移除原先“MPS 强制关闭 layerwise offload”的逻辑。
  5. 平台杂项与配套测试:gpu_worker.py 改 capture_memory_snapshot() 统计峰值(max_memory_reserved() 在 MPS 不可用)并跳过 set_device 与 reset_peak_memory_stats;vae_vit.py 与 decoding.py 用 _cuda_autocast_disabled/nullcontext 让 autocast 只在 CUDA 生效;hf_diffusers_utils.py 为 client has been closed 增加指数退避重试;test_minimax_h3_admission.py 新增 MPS 准入用例 test_mps_admission_requires_layerwise_residency_for_every_h3_component
文件 模块 状态 重要度
python/sglang/multimodal_gen/runtime/managers/memory_managers/layerwise_offload.py 权重卸载 modified 8.65
python/sglang/multimodal_gen/runtime/models/dits/minimax_h3.py H3 模型 modified 8.05
python/sglang/multimodal_gen/runtime/loader/component_loaders/transformer_loader.py 组件加载 modified 7.03
python/sglang/multimodal_gen/runtime/loader/component_loaders/vae_loader.py 组件加载 modified 6.65
python/sglang/multimodal_gen/configs/pipeline_configs/minimax_h3.py 配置校验 modified 6.39
python/sglang/multimodal_gen/runtime/server_args/server_args.py 启动参数 modified 5.99
python/sglang/multimodal_gen/runtime/managers/gpu_worker.py 工作进程 modified 5.92
python/sglang/multimodal_gen/runtime/models/vaes/minimax_h3_video_vae/vae_vit.py VAE 模型 modified 6.06
python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/minimax_h3/stages/decoding.py 解码阶段 modified 6.23
python/sglang/multimodal_gen/test/unit/test_minimax_h3_admission.py 准入测试 modified 5.04

关键符号

LayerwiseOffloadManager.initialize LayerwiseOffloadManager._initialize_mps_cpu_weights LayerwiseOffloadManager.prefetch_layer LayerwiseOffloadManager.release_layer LayerwiseOffloadManager.sync_layer_to_cpu LayerwiseOffloadManager.materialize_mps_non_layer_weights LayerwiseOffloadManager.release_mps_non_layer_weights MiniMaxH3DiTModel._forward_mps_streamed_attention _apply_rope TransformerLoader.customized_load_kwargs_for_component VAELoader.customized_load_kwargs_for_component

关键源码片段

python/sglang/multimodal_gen/runtime/managers/memory_managers/layerwise_offload.py dependency-wiring

整个 PR 的核心:LayerwiseOffloadManager 从纯 CUDA 异步模型扩展为同步 MPS 与异步 CUDA 双路径,新增 _mps_cpu_weights 结构、MPS 初始化 / 取层 / 释放 / 非层权重生命周期管理,是所有 MPS 组件驻留行为的地基。

# layerwise_offload.py 节选:MPS 同步层权重的卸下与取回
# 前置状态:__init__ 识别到 current_platform.is_mps() 时,
# 会设置 _synchronous_mps=True、pin_cpu_memory=False、copy_stream=None,
# 并用 _mps_cpu_weights 字典存放每层原始 CPU tensor。@torch.compiler.disable
def _initialize_mps_cpu_weights(self) -> None:
    """把每层权重迁回 CPU,并在设备上替换为共享空 tensor 占位。    CUDA 路径会把同层同 dtype 的权重合并成扁平化的 pinned CPU 副本;
    MPS 与主机共享物理内存,直接保留原始 CPU tensor 即可,避免再驻留
    一份主机拷贝。
    """
    for name, tensor in self._named_parameters.items():
        layer_idx = self._match_layer_idx(name)
        if layer_idx is None or layer_idx >= self.num_layers:
            continue
        local_tensor = self._to_local_tensor(tensor).detach()
        cpu_tensor = (
            local_tensor
            if local_tensor.device.type == "cpu"
            else local_tensor.to("cpu")
        )
        self._mps_cpu_weights.setdefault(layer_idx, {})[name] = cpu_tensor
        self._weight_metadata.setdefault(layer_idx, {})[name] = {
            "dtype": cpu_tensor.dtype,
        }
        # 用共享的 1 元素占位 tensor 顶替原参数,释放设备侧大块显存
        tensor.data = self._get_shared_empty_tensor_for_target(
            tensor, cpu_tensor.dtype
        )
​
    torch.mps.empty_cache()
    self.register_forward_hooks()
    self._configured = True
    logger.info(
        f"Initialized synchronous MPS layerwise offload with "
        f"{self.num_layers} layers"
    )
​
​
def prefetch_layer(self, layer_idx: int, non_blocking: bool = True) -> None:
    """幂等地把指定层权重搬回设备;MPS 下是同步搬移。"""
    if not self.enabled or self.device is None:
        return
    if layer_idx < 0 or layer_idx >= self.num_layers:
        return
    if layer_idx in self._gpu_layers:
        return
​
    if self._synchronous_mps:
        cpu_weights = self._mps_cpu_weights.get(layer_idx)
        if not cpu_weights:
            return
        with torch.inference_mode(False), torch.no_grad():
            for name, cpu_tensor in cpu_weights.items():
                target = self.get_target_with_name(name)
                # 统一内存下 CPU 到 MPS 的搬移没有异步语义,non_blocking 恒为 False
                target.data = self._wrap_for_target(
                    target,
                    cpu_tensor.to(device=self.device, non_blocking=False),
                )
        self._gpu_layers.add(layer_idx)
        return
​
    # 以下是 CUDA 异步路径:pinned 副本 + copy stream + Event 记录
    # MPS 分支已在上方返回,CUDA 路径行为保持不变

评论区精华

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

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

风险与影响

1) 正确性未验证:PR body 明说 end-to-end and accuracy validation are pending,CI Extra 失败;MPS 流式 attention 按 chunk 重算 norm 与 RoPE,其等价性依赖 _apply_rope 与 CUDA 路径逐字节一致,建议补齐精度对照后再在生产启用。
2) 性能同步开销:每个 chunk 后强制 torch.mps.synchronize() + torch.mps.empty_cache(),同步点密集;最终 chunk size 128 来自 30 个 commit 中多次“Increase/Batch”后被 revert、再“Lower/Tune”的反复试验,说明对具体机型内存-吞吐权衡高度敏感。
3) 全局下载重试改动:hf_diffusers_utils.py 新增 client has been closed 的捕获与退避重试,修改 snapshot_download 的错误语义,影响所有平台的模型拉取路径。
4) 内存统计口径变化:gpu_worker.py 在 MPS 下改用 snapshot 峰值,与 CUDA 的 max_memory_reserved() 口径不同,跨平台对比数据时易产生误读。
5) 双份权重字典契约_mps_cpu_weights_consolidated_cpu_weights 并存,update_cpu_weights 等写回路径需同时维护两套结构,后续扩展时容易漏改一侧。

影响面集中在 MPS(Apple Silicon)平台的 MiniMax H3 推理:所有分支均以 current_platform.is_mps()x.device.type == "mps" 为条件,CUDA/ROCm 主路径行为不变;唯一全局影响是 Hugging Face 下载重试逻辑。对用户而言,MiniMax H3 在 MPS 上首次具备可运行性且可支撑更长序列;对团队而言,LayerwiseOffloadManager 从此承载同步(MPS)与异步(CUDA)两套执行模型,后续新增平台需同时考虑两条分支;由于 Draft 且 CI Extra 失败,该能力目前处于“可运行但未验证”状态,不建议直接作为发布基线。

正确性未验证(Draft) CI Extra 失败 MPS 专用路径,CUDA 不受影响 全局 Hugging Face 下载重试改动 同步点密集,吞吐受限 chunk size 经验调参敏感

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论