|
51 | 51 | from cosmos_framework.tools.visualize.video import save_img_or_video |
52 | 52 | from cosmos_framework.configs.base.defaults.compile import CompileConfig |
53 | 53 | from cosmos_framework.configs.base.defaults.parallelism import ParallelismConfig |
54 | | -from cosmos_framework.model.vfm.omni_mot_model import OmniMoTModel |
| 54 | +from cosmos_framework.model.vfm.omni_mot_model import OmniMoTModel, cpu_offload_materialization |
55 | 55 | from cosmos_framework.model.vfm.vlm.qwen3_vl.utils import _SYSTEM_PROMPT_IMAGE_EDITING |
56 | 56 | from cosmos_framework.model.vfm.upsampler.prompts import is_upsampled_prompt |
57 | 57 |
|
|
74 | 74 | def build_omni_offload_parts(model: OmniMoTModel) -> dict[str, "torch.nn.Module"]: |
75 | 75 | """Group the in-tree Cosmos3 model into offloadable module groups. |
76 | 76 |
|
77 | | - The MoT packs both pathways into a single network with dual projections per |
78 | | - layer, so rather than pointing at whole towers we gather the per-layer |
79 | | - understanding (``reasoner``) vs generation (``generator``) submodules into two |
80 | | - synthetic ``nn.ModuleList`` groups (referencing the existing submodule objects, |
81 | | - so the module tree, checkpoint loading, and the joint forward are unaffected). |
82 | | - The generation-side diffusion heads (``vae2llm`` / ``llm2vae`` / ``time_embedder`` |
83 | | - and the optional action/sound heads) join the generator group because the |
84 | | - reasoner prefill skips vision/action/sound encode and decode entirely. The |
85 | | - ``vae`` group is the vision tokenizer network (staged around encode/decode). |
86 | | -
|
87 | | - All groups are disjoint (distinct module instances), as ``ModuleOffloadManager`` |
88 | | - requires. Returns every known group; callers stage only the requested subset. |
89 | | - Modules left out of every staged group stay GPU-resident. |
| 77 | + The reasoner/generator partition is owned by the network |
| 78 | + (:meth:`Cosmos3VFMNetwork.offload_module_groups`) so the load-time CPU |
| 79 | + materialization and this runtime ``OffloadPipeline`` share one source of truth. |
| 80 | + Each group is wrapped in an ``nn.ModuleList`` referencing the existing submodule |
| 81 | + objects (no module-tree changes), and the ``vae`` group is added for the vision |
| 82 | + tokenizer network. All groups are disjoint; callers stage only the requested |
| 83 | + subset, and modules left out of every staged group stay GPU-resident. |
90 | 84 | """ |
91 | 85 | import torch.nn as nn |
92 | 86 |
|
93 | | - net = model.net |
94 | | - lm = net.language_model.model # Qwen3VL(Moe)TextModel: embed_tokens, layers, norm, norm_moe_gen |
95 | | - |
96 | | - reasoner_modules: list[nn.Module] = [lm.embed_tokens, lm.norm] |
97 | | - generator_modules: list[nn.Module] = [lm.norm_moe_gen] |
98 | | - |
99 | | - # Generation-side diffusion encode/decode heads (only present when enabled). |
100 | | - for head_name in ("time_embedder", "vae2llm", "llm2vae", "action2llm", "llm2action", "sound2llm", "llm2sound"): |
101 | | - head = getattr(net, head_name, None) |
102 | | - if isinstance(head, nn.Module): |
103 | | - generator_modules.append(head) |
104 | | - |
105 | | - for raw_layer in lm.layers: |
106 | | - # Unwrap torch.compile's OptimizedModule so we reference the real submodules |
107 | | - # (the manager rebinds their parameters between stages, which the compiled |
108 | | - # graph picks up as graph inputs on the next call). |
109 | | - layer = getattr(raw_layer, "_orig_mod", raw_layer) |
110 | | - attn = layer.self_attn |
111 | | - reasoner_modules += [ |
112 | | - attn.q_proj, |
113 | | - attn.k_proj, |
114 | | - attn.v_proj, |
115 | | - attn.o_proj, |
116 | | - attn.q_norm, |
117 | | - attn.k_norm, |
118 | | - layer.input_layernorm, |
119 | | - layer.post_attention_layernorm, |
120 | | - layer.mlp, |
121 | | - ] |
122 | | - generator_modules += [ |
123 | | - attn.q_proj_moe_gen, |
124 | | - attn.k_proj_moe_gen, |
125 | | - attn.v_proj_moe_gen, |
126 | | - attn.o_proj_moe_gen, |
127 | | - attn.q_norm_moe_gen, |
128 | | - attn.k_norm_moe_gen, |
129 | | - layer.input_layernorm_moe_gen, |
130 | | - layer.post_attention_layernorm_moe_gen, |
131 | | - layer.mlp_moe_gen, |
132 | | - ] |
133 | | - |
134 | | - parts: dict[str, nn.Module] = { |
135 | | - REASONER_OFFLOAD_PART: nn.ModuleList(reasoner_modules), |
136 | | - GENERATOR_OFFLOAD_PART: nn.ModuleList(generator_modules), |
137 | | - } |
| 87 | + groups = model.net.offload_module_groups() # {"reasoner": [...], "generator": [...]} |
| 88 | + parts: dict[str, nn.Module] = {name: nn.ModuleList(modules) for name, modules in groups.items()} |
| 89 | + |
138 | 90 | # The vision tokenizer's underlying network (the offloadable VAE weights). |
139 | 91 | # ``tokenizer_vision_gen.model`` may itself be a thin (non-Module) wrapper whose |
140 | 92 | # ``.model`` is the actual nn.Module (e.g. Wan2pt2VAEInterface -> WanVAE -> WanVAE_). |
@@ -1102,60 +1054,68 @@ def _create(cls, setup_args: SetupArgs, **kwargs: Any) -> Self: |
1102 | 1054 | sampler_override = setup_args.sampler |
1103 | 1055 | parallelism_config = cls._get_parallelism_config(setup_args) |
1104 | 1056 | compile_config = cls._get_compile_config(setup_args) |
1105 | | - if setup_args.checkpoint_type == CheckpointType.DCP and setup_args.config_file_type == ConfigFileType.MODULE: |
1106 | | - from cosmos_framework.inference.common.config import save_config |
1107 | | - from cosmos_framework.utils.vfm.model_loader import load_model_from_checkpoint |
1108 | | - |
1109 | | - if not setup_args.experiment: |
1110 | | - raise ValueError("'experiment' is required") |
1111 | | - if not setup_args.config_file: |
1112 | | - raise ValueError("'config_file' is required") |
1113 | | - |
1114 | | - Cosmos3OmniModel.before_load_model() |
1115 | | - model, config = load_model_from_checkpoint( |
1116 | | - experiment_name=setup_args.experiment, |
1117 | | - config_file=setup_args.config_file, |
1118 | | - checkpoint_path=setup_args.checkpoint_path, |
1119 | | - credential_path=setup_args.credential_path or None, |
1120 | | - parallelism_config=attrs.asdict(parallelism_config), |
1121 | | - compile_config=attrs.asdict(compile_config), |
1122 | | - load_ema_to_reg=setup_args.use_ema_weights, |
1123 | | - experiment_opts=[ |
1124 | | - *setup_args.experiment_overrides, |
1125 | | - f"model.config.rectified_flow_inference_config.scheduler_type={sampler_override}", |
1126 | | - ], |
1127 | | - use_cache_checkpoint=setup_args.checkpoint_cache_dir is not None, |
1128 | | - cache_checkpoint_rootdir=str(setup_args.checkpoint_cache_dir or ""), |
1129 | | - ) |
1130 | | - model = cast("OmniMoTModel", model) |
1131 | | - Cosmos3OmniModel.after_load_model(model) |
1132 | | - save_config(config, setup_args.output_dir) |
1133 | | - else: |
1134 | | - checkpoint_path = setup_args.download_checkpoint() |
1135 | | - if setup_args.config_file_type == ConfigFileType.MODULE: |
1136 | | - config = None |
| 1057 | + # Two-phase materialization for single-GPU CPU offloading: build the reasoner/ |
| 1058 | + # generator towers directly on CPU during model construction so they never occupy |
| 1059 | + # GPU memory (the checkpoint shards load straight into CPU tensors). No-op when |
| 1060 | + # those stages aren't requested. |
| 1061 | + net_offload_parts = tuple( |
| 1062 | + s for s in setup_args.offload_stages if s in (REASONER_OFFLOAD_PART, GENERATOR_OFFLOAD_PART) |
| 1063 | + ) |
| 1064 | + with cpu_offload_materialization(net_offload_parts): |
| 1065 | + if setup_args.checkpoint_type == CheckpointType.DCP and setup_args.config_file_type == ConfigFileType.MODULE: |
| 1066 | + from cosmos_framework.inference.common.config import save_config |
| 1067 | + from cosmos_framework.utils.vfm.model_loader import load_model_from_checkpoint |
| 1068 | + |
| 1069 | + if not setup_args.experiment: |
| 1070 | + raise ValueError("'experiment' is required") |
| 1071 | + if not setup_args.config_file: |
| 1072 | + raise ValueError("'config_file' is required") |
| 1073 | + |
| 1074 | + Cosmos3OmniModel.before_load_model() |
| 1075 | + model, config = load_model_from_checkpoint( |
| 1076 | + experiment_name=setup_args.experiment, |
| 1077 | + config_file=setup_args.config_file, |
| 1078 | + checkpoint_path=setup_args.checkpoint_path, |
| 1079 | + credential_path=setup_args.credential_path or None, |
| 1080 | + parallelism_config=attrs.asdict(parallelism_config), |
| 1081 | + compile_config=attrs.asdict(compile_config), |
| 1082 | + load_ema_to_reg=setup_args.use_ema_weights, |
| 1083 | + experiment_opts=[ |
| 1084 | + *setup_args.experiment_overrides, |
| 1085 | + f"model.config.rectified_flow_inference_config.scheduler_type={sampler_override}", |
| 1086 | + ], |
| 1087 | + use_cache_checkpoint=setup_args.checkpoint_cache_dir is not None, |
| 1088 | + cache_checkpoint_rootdir=str(setup_args.checkpoint_cache_dir or ""), |
| 1089 | + ) |
| 1090 | + model = cast("OmniMoTModel", model) |
| 1091 | + Cosmos3OmniModel.after_load_model(model) |
| 1092 | + save_config(config, setup_args.output_dir) |
1137 | 1093 | else: |
1138 | | - model_dict = setup_args.load_model_config_dict() |
1139 | | - if setup_args.vlm_processor_from_checkpoint: |
1140 | | - # Source the VLM processor from the loaded checkpoint's own |
1141 | | - # bundled files instead of the repository hardcoded in the |
1142 | | - # model config. Drops the redundant base-model download. |
1143 | | - tokenizer_cfg = model_dict["config"]["vlm_config"]["tokenizer"] |
1144 | | - tokenizer_cfg.pop("repository", None) |
1145 | | - tokenizer_cfg.pop("revision", None) |
1146 | | - tokenizer_cfg.pop("subdir", None) |
1147 | | - tokenizer_cfg["tokenizer_type"] = str(checkpoint_path) |
1148 | | - config = Cosmos3OmniConfig(model=model_dict) |
1149 | | - model = Cosmos3OmniModel.from_pretrained_dcp( |
1150 | | - checkpoint_path, |
1151 | | - config=config, |
1152 | | - parallelism_config=parallelism_config, |
1153 | | - compile_config=compile_config, |
1154 | | - ).model |
1155 | | - if model.config.rectified_flow_inference_config.scheduler_type != sampler_override: |
1156 | | - model.config.rectified_flow_inference_config.scheduler_type = sampler_override |
1157 | | - model.set_up_scheduler_and_sampler() |
1158 | | - log.debug(f"Sampler overridden to: {sampler_override}") |
| 1094 | + checkpoint_path = setup_args.download_checkpoint() |
| 1095 | + if setup_args.config_file_type == ConfigFileType.MODULE: |
| 1096 | + config = None |
| 1097 | + else: |
| 1098 | + model_dict = setup_args.load_model_config_dict() |
| 1099 | + if setup_args.vlm_processor_from_checkpoint: |
| 1100 | + # Source the VLM processor from the loaded checkpoint's own |
| 1101 | + # bundled files instead of the repository hardcoded in the |
| 1102 | + # model config. Drops the redundant base-model download. |
| 1103 | + tokenizer_cfg = model_dict["config"]["vlm_config"]["tokenizer"] |
| 1104 | + tokenizer_cfg.pop("repository", None) |
| 1105 | + tokenizer_cfg.pop("revision", None) |
| 1106 | + tokenizer_cfg.pop("subdir", None) |
| 1107 | + tokenizer_cfg["tokenizer_type"] = str(checkpoint_path) |
| 1108 | + config = Cosmos3OmniConfig(model=model_dict) |
| 1109 | + model = Cosmos3OmniModel.from_pretrained_dcp( |
| 1110 | + checkpoint_path, |
| 1111 | + config=config, |
| 1112 | + parallelism_config=parallelism_config, |
| 1113 | + compile_config=compile_config, |
| 1114 | + ).model |
| 1115 | + if model.config.rectified_flow_inference_config.scheduler_type != sampler_override: |
| 1116 | + model.config.rectified_flow_inference_config.scheduler_type = sampler_override |
| 1117 | + model.set_up_scheduler_and_sampler() |
| 1118 | + log.debug(f"Sampler overridden to: {sampler_override}") |
1159 | 1119 |
|
1160 | 1120 | # Single-GPU CPU offloading (opt-in via --offload-stages). The diffusion parts |
1161 | 1121 | # (reasoner / generator / vae) time-share one reusable GPU arena; 'guardrails' |
|
0 commit comments