File size: 16,508 Bytes
fc3971e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# Copyright (c) 2026 SandAI. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import argparse
import copy
import json
import os
import sys
from pathlib import Path
from typing import Literal, Tuple

import torch
from inference.utils import env_is_true, print_rank_0
from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator, model_validator
from pydantic_settings import (
    BaseSettings,
    CliSettingsSource,
    JsonConfigSettingsSource,
    PydanticBaseSettingsSource,
    SettingsConfigDict,
)


class EngineConfig(BaseModel):
    # Basic settings
    seed: int = Field(1234, description="Random seed used for python, numpy, pytorch, and cuda.")
    load: str | None = Field(None, description="Directory containing a model checkpoint.")

    # Parallelism strategy
    distributed_backend: Literal["nccl", "gloo"] = Field("nccl", description="Distributed backend. Choices: ['nccl', 'gloo'].")
    distributed_timeout_minutes: int = Field(10, description="Timeout minutes for torch.distributed.")
    sequence_parallel: bool = Field(False, description="Enable sequence parallel optimization.")
    tp_size: int = Field(1, description="Degree of tensor model parallelism.")
    pp_size: int = Field(1, description="Degree of pipeline model parallelism.")
    cp_size: int = Field(1, description="Degree of context parallelism.")
    dp_size: int = Field(1, description="Degree of data parallelism.")


class ModelConfig(BaseModel):
    """Model configuration class defining various parameters for video generation model"""

    model_config = ConfigDict(arbitrary_types_allowed=True, protected_namespaces=())

    num_layers: int = Field(default=40, description="Number of Transformer layers")
    hidden_size: int = Field(default=5120, description="Hidden size of the Transformer model")
    head_dim: int = Field(default=128, description="Dimension per attention head")
    num_query_groups: int = Field(default=8, description="Number of query groups for grouped-query attention")
    video_in_channels: int = Field(default=48 * 4, description="Number of video input channels after patch embedding")
    audio_in_channels: int = Field(default=64, description="Number of audio input channels")
    text_in_channels: int = Field(default=3584, description="Number of text input channels")
    checkpoint_qk_layernorm_rope: bool = Field(default=False, description="Enable checkpointing for QK layernorm + RoPE")
    params_dtype: torch.dtype | str = Field(default=torch.float32, description="Parameter dtype")
    tread_config: dict = Field(
        default=dict(
            selection_rate=0.5, start_layer_idx=2, end_layer_idx=25  # after forward of 0, 1  # before forward of 26 27 28 29
        ),
        description="TReAD (Token Routing and Early Drop) configuration",
    )
    mm_layers: list[int] = Field(default=[0, 1, 2, 3, 36, 37, 38, 39], description="Indices of multimodal fusion layers")
    local_attn_layers: list[int] = Field(default=[], description="Indices of local attention layers")
    enable_attn_gating: bool = Field(default=True, description="Enable attention gating")
    activation_type: str = Field(default="swiglu7", description="Activation type")
    gelu7_layers: list[int] = Field(default=[0, 1, 2, 3], description="Indices of gelu7 layers")

    # Token-level Sparse MoE MLP (learned router, top-k per token).
    # When non-empty, listed middle MLPs AND mm_layers MLPs become SparseMoEMLP.
    # Attention on mm_layers stays per-modality NativeMoELinear.
    sparse_moe_layers: list[int] = Field(
        default=[],
        description="Layer indices whose MLP is replaced by token-level SparseMoEMLP. Empty disables sparse MoE. Non-empty also converts mm_layers MLPs from modality-hard NativeMoE to SparseMoEMLP.",
    )
    num_routed_experts: int = Field(default=3, description="Number of routed experts in SparseMoEMLP")
    moe_top_k: int = Field(default=1, description="Number of experts activated per token")
    moe_aux_loss_coef: float = Field(
        default=0.01, description="Default coefficient for MoE load-balancing aux loss (training may override)"
    )
    moe_router_noise: float = Field(
        default=0.0, description="Std of Gaussian noise added to router logits during training (0 disables)"
    )
    token_sparse_moe_convert_mm_mlp: bool = Field(
        default=False,
        description="If true and sparse_moe_layers is set, also replace mm_layers MLP NativeMoELinear with token-level SparseMoEMLP (3E top-1). Attention stays per-modality.",
    )

    # Add computed fields
    num_heads_q: int = Field(default=0, description="Number of query heads (calculated from hidden_size // head_dim)")
    num_heads_kv: int = Field(default=0, description="Number of key-value heads (calculated from num_query_groups)")
    post_norm_layers: list[int] = Field(default=[], description="Indices of post norm layers")

    @field_serializer("params_dtype")
    def serialize_dtype(self, value: torch.dtype | str) -> str:
        return str(value)

    @field_validator("params_dtype", mode="before")
    @classmethod
    def validate_dtype(cls, value):
        if isinstance(value, torch.dtype):
            return value
        if isinstance(value, str):
            if value == "torch.float32" or value == "float32":
                return torch.float32
            elif value == "torch.float16" or value == "float16":
                return torch.float16
            elif value == "torch.bfloat16" or value == "bfloat16":
                return torch.bfloat16
        raise ValueError(f"Unknown torch.dtype string: '{value}'")


class DataProxyConfig(BaseModel):
    t_patch_size: int = Field(default=1, description="Patch size for time dimension")
    patch_size: int = Field(default=2, description="Patch size for spatial dimensions")
    frame_receptive_field: int = Field(default=11, description="Frame receptive field")
    spatial_rope_interpolation: Literal["inter", "extra"] = Field(
        default="extra", description="Spatial rope interpolation method."
    )
    ref_audio_offset: int = Field(default=1000, description="Offset for reference audio.")
    text_offset: int = Field(default=0, description="Offset for text.")
    coords_style: Literal["v1", "v2"] = Field(default="v2", description="Coords style.")


class EvaluationConfig(BaseModel):
    """Evaluation configuration class defining parameters for model evaluation and inference"""

    model_config = ConfigDict(protected_namespaces=())

    data_proxy_config: DataProxyConfig = Field(default=DataProxyConfig(), description="Data proxy configuration")

    fps: int = Field(default=25, description="Frames per second for video generation")
    num_inference_steps: int = Field(default=32, description="Number of denoising steps during inference")
    video_txt_guidance_scale: float = Field(default=5.0, description="Video text guidance scale for text conditioning")
    audio_txt_guidance_scale: float = Field(default=5.0, description="Audio text guidance scale for text conditioning")
    txt_encoder_type: Literal["t5_gemma"] = Field(default="t5_gemma", description="Text encoder type.")
    t5_gemma_target_length: int = Field(default=640, description="Target length for T5-Gemma encoder.")
    support_ref_audio: bool = Field(default=True, description="Whether to support the ref_audio feature")
    shift: float = Field(default=5.0, description="Temporal shift parameter for video generation")
    exp_name: str = Field(default="exp_debug", description="Experiment name with evaluation suffix")
    audio_model_path: str = Field(default="", description="Path to the pretrained audio model")
    audio_vae_backend: Literal["stable_audio", "omnivae"] = Field(
        default="omnivae",
        description="Audio VAE backend: stable_audio (SA) or omnivae (OmniVAE DAC).",
    )
    omnivae_audio_ckpt_dir: str = Field(
        default=(
            ""
        ),
        description="Directory containing OmniVAE audio config.yaml + state_dict.pt",
    )
    txt_model_path: str = Field(default="", description="Path to the pretrained txt model")
    vae_model_path: str = Field(default="", description="Path to the pretrained vae model")
    vae_stride: Tuple[int, int, int] = Field(default=(4, 16, 16), description="VAE stride in format (time, height, width)")
    z_dim: int = Field(default=48, description="Dimension of z space.")
    patch_size: Tuple[int, int, int] = Field(default=(1, 2, 2), description="Patch size in format (time, height, width)")
    cfg_number: int = Field(default=2, description="Classifier-free guidance number")
    sr_cfg_number: int = Field(default=2, description="SR Classifier-free guidance number")

    # flops recording
    enable_flops_recording: bool = Field(default=False, description="Whether to enable flops recording")

    # super resolution model configuration
    use_sr_model: bool = Field(default=False, description="Whether to use the super resolution model")
    sr_model_path: str = Field(default="", description="Path to the pretrained super resolution model")
    sr_num_inference_steps: int = Field(default=5, description="Number of denoising steps during super resolution inference")
    noise_value: int = Field(default=220, description="Noise value for the super resolution model")
    sr_video_txt_guidance_scale: float = Field(
        default=3.5, description="Super resolution video text guidance scale for text conditioning"
    )
    use_cfg_trick: bool = Field(default=True, description="Whether to use the cfg trick")
    cfg_trick_start_frame: int = Field(default=13, description="Start frame for the cfg trick")
    cfg_trick_value: float = Field(default=2.0, description="Value for the cfg trick")
    using_sde_flag: bool = Field(default=False, description="Whether to use the sde flag")
    sr_audio_noise_scale: float = Field(default=0.7, description="Noise scale for the super resolution audio")

    # turbo-vae config
    use_turbo_vae: bool = Field(default=True, description="Whether to use the turbo-vae")
    student_config_path: str = Field(default="", description="Path to the student config")
    student_ckpt_path: str = Field(default="", description="Path to the student checkpoint")


class MagiPipelineConfig(BaseSettings):
    engine_config: EngineConfig = Field(description="Engine configuration.", default_factory=EngineConfig)
    arch_config: ModelConfig = Field(default=ModelConfig(), description="Model configuration.")
    evaluation_config: EvaluationConfig = Field(default=EvaluationConfig(), description="Evaluation configuration.")
    sr_arch_config: ModelConfig = Field(default=ModelConfig(), description="Super resolution model configuration.")
    model_config = SettingsConfigDict(cli_parse_args=True, cli_ignore_unknown_args=True, cli_implicit_flags=True)

    @classmethod
    def settings_customise_sources(

        cls,

        settings_cls: type[BaseSettings],

        init_settings: PydanticBaseSettingsSource,

        env_settings: PydanticBaseSettingsSource,

        dotenv_settings: PydanticBaseSettingsSource,

        file_secret_settings: PydanticBaseSettingsSource,

    ) -> tuple[PydanticBaseSettingsSource, ...]:
        parser = argparse.ArgumentParser(allow_abbrev=False)
        parser.add_argument("--config-load-path", type=str, default=None, help="Path to load the config.json from")
        args, _ = parser.parse_known_args()
        config_load_path = args.config_load_path
        sources = [env_settings, CliSettingsSource(settings_cls, cli_parse_args=True, cli_ignore_unknown_args=True)]
        if config_load_path:
            sources.append(JsonConfigSettingsSource(settings_cls, json_file=config_load_path))

        sources.extend([init_settings, dotenv_settings, file_secret_settings])
        return tuple(sources)

    def save_to_json(self, json_path: str, indent: int = 4):
        path = Path(json_path)
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(self.__str__(indent=indent))

    def __str__(self, indent: int = 4):
        data = self.model_dump(mode="json")
        formatted = json.dumps(data, indent=indent, ensure_ascii=False, sort_keys=False)
        class_name = self.__class__.__name__
        return f"{class_name}:\n{formatted}".replace('"', "")

    def __repr__(self, indent: int = 4):
        return self.__str__(indent=indent)

    @model_validator(mode="after")
    def validate_engine_config(self):
        world_size = int(os.getenv("WORLD_SIZE", "1"))
        self.engine_config.dp_size = world_size // (
            self.engine_config.tp_size * self.engine_config.pp_size * self.engine_config.cp_size
        )

        assert world_size % self.engine_config.tp_size == 0
        tp_pp_size = self.engine_config.tp_size * self.engine_config.pp_size
        assert world_size % tp_pp_size == 0
        tp_pp_cp_size = tp_pp_size * self.engine_config.cp_size
        assert world_size % tp_pp_cp_size == 0
        assert world_size == self.engine_config.dp_size * tp_pp_cp_size

        if self.engine_config.tp_size == 1:
            self.engine_config.sequence_parallel = False

        return self

    @model_validator(mode="after")
    def post_override_config(self):
        self.arch_config.num_heads_q = self.arch_config.hidden_size // self.arch_config.head_dim
        self.arch_config.num_heads_kv = self.arch_config.num_query_groups

        self.sr_arch_config = copy.deepcopy(self.arch_config)
        if env_is_true("SR2_1080"):
            self.sr_arch_config = copy.deepcopy(self.arch_config)
            # fmt: off
            self.sr_arch_config.local_attn_layers = [
                0, 1, 2,
                4, 5, 6,
                8, 9, 10,
                12, 13, 14,
                16, 17, 18,
                20, 21, 22,
                24, 25, 26,
                28, 29, 30,
                32, 33, 34,
                35, 36, 37,
                38, 39,
            ]
            # fmt: on
            self.evaluation_config.sr_video_txt_guidance_scale = 3.5

        return self


def prevent_unsupported_list_syntax():
    """

    Check sys.argv before Pydantic parsing to prevent using unsupported list syntax.

    """
    args = sys.argv[1:]
    for i, arg in enumerate(args):
        if i + 2 < len(args):
            value1, value2 = args[i + 1], args[i + 2]
            if not value1.startswith("-") and not value2.startswith("-"):
                error_msg = (
                    f"\n\nError: Detected list parameter '{arg}' using unsupported command line syntax.\n"
                    f"Error pattern: '{arg} {value1} {value2} ...'\n\n"
                    "Pydantic (or related libraries) do not support passing lists with space-separated multiple values.\n"
                    "Please use one of the following supported formats:\n\n"
                    f"1. JSON style:      {arg} '[{value1},{value2},...]'\n"
                    f"2. Argparse style:  {arg} {value1} {arg} {value2}\n"
                    f"3. Lazy style:      {arg} {value1},{value2}\n"
                )
                raise ValueError(error_msg)


def parse_config(verbose: bool = False) -> MagiPipelineConfig:
    parser = argparse.ArgumentParser(description="Load and optionally save config", allow_abbrev=False)
    parser.add_argument("--config-save-path", type=str, default=None, help="Path to save the config.json to")
    args, _ = parser.parse_known_args()

    prevent_unsupported_list_syntax()
    config = MagiPipelineConfig()

    if args.config_save_path is not None:
        config.save_to_json(args.config_save_path)

    if verbose:
        print_rank_0(config)

    return config