diff --git a/configs/flux2/flux2_dev_tp.json b/configs/flux2/flux2_dev_tp.json new file mode 100644 index 000000000..3273bfcfc --- /dev/null +++ b/configs/flux2/flux2_dev_tp.json @@ -0,0 +1,16 @@ +{ + "model_cls": "flux2_dev", + "task": "t2i", + "infer_steps": 50, + "sample_guide_scale": 4.0, + "vae_scale_factor": 16, + "feature_caching": "None", + "enable_cfg": false, + "patch_size": 2, + "tokenizer_max_length": 512, + "rope_type": "flashinfer", + "text_encoder_out_layers": [10, 20, 30], + "parallel": { + "tensor_p_size": 4 + } +} diff --git a/configs/wan22/wan_moe_t2v_tp.json b/configs/wan22/wan_moe_t2v_tp.json new file mode 100755 index 000000000..88393761d --- /dev/null +++ b/configs/wan22/wan_moe_t2v_tp.json @@ -0,0 +1,24 @@ +{ + "infer_steps": 40, + "target_video_length": 81, + "text_len": 512, + "target_height": 720, + "target_width": 1280, + "self_attn_1_type": "flash_attn3", + "cross_attn_1_type": "flash_attn3", + "cross_attn_2_type": "flash_attn3", + "sample_guide_scale": [ + 4.0, + 3.0 + ], + "sample_shift": 12.0, + "enable_cfg": true, + "cpu_offload": false, + "t5_cpu_offload": false, + "vae_cpu_offload": false, + "boundary": 0.875, + "use_tiling_vae": true, + "parallel": { + "tensor_p_size": 4 + } +} diff --git a/lightx2v/models/networks/base_model.py b/lightx2v/models/networks/base_model.py index 4145a3ec1..44d98124c 100644 --- a/lightx2v/models/networks/base_model.py +++ b/lightx2v/models/networks/base_model.py @@ -91,6 +91,19 @@ def __init__(self, model_path, config, device, model_type=None, lora_path=None, self.dit_quantized = self.config.get("dit_quantized", False) if self.dit_quantized: self._check_dit_quantized() + self._init_tensor_parallel() + + def _init_tensor_parallel(self): + if self.config.get("tensor_parallel", False): + self.use_tp = True + self.tp_group = self.config.get("device_mesh").get_group(mesh_dim="tensor_p") + self.tp_rank = dist.get_rank(self.tp_group) + self.tp_size = dist.get_world_size(self.tp_group) + else: + self.use_tp = False + self.tp_group = None + self.tp_rank = 0 + self.tp_size = 1 def _check_dit_quantized(self): """Check if the model is quantized. @@ -291,13 +304,10 @@ def _should_load_weights(self): # Single GPU mode return True elif dist.is_initialized(): - if self.config.get("load_from_rank0", False): - # Multi-GPU mode, only rank 0 loads - if dist.get_rank() == 0: - logger.info(f"Loading weights from {self.model_path}") - return True - else: - return True + if self.use_tp or self.config.get("load_from_rank0", False): + # TP or explicit rank0-only loading: only rank 0 reads from disk + return dist.get_rank() == 0 + return True return False def _apply_weights(self, weight_dict=None): @@ -424,6 +434,10 @@ def _load_ckpt(self, unified_dtype, sensitive_layer): Returns: dict: Dictionary of weights """ + _tp_dev = self.use_tp + if _tp_dev: + _saved_device, self.device = self.device, torch.device("cpu") + if self.config.get("dit_original_ckpt", None): safetensors_path = self.config["dit_original_ckpt"] else: @@ -456,6 +470,8 @@ def _load_ckpt(self, unified_dtype, sensitive_layer): file_weights = self._load_safetensor_to_dict(file_path, unified_dtype, sensitive_layer) weight_dict.update(file_weights) + if _tp_dev: + self.device = _saved_device return weight_dict def _load_quant_ckpt(self, unified_dtype, sensitive_layer): @@ -468,6 +484,10 @@ def _load_quant_ckpt(self, unified_dtype, sensitive_layer): Returns: dict: Dictionary of weights """ + _tp_dev = self.use_tp + if _tp_dev: + _saved_device, self.device = self.device, torch.device("cpu") + remove_keys = self.remove_keys if hasattr(self, "remove_keys") else [] if self.config.get("dit_quantized_ckpt", None): @@ -485,6 +505,8 @@ def _load_quant_ckpt(self, unified_dtype, sensitive_layer): else: gguf_path = safetensors_path weight_dict = self._load_gguf_ckpt(gguf_path) + if _tp_dev: + self.device = _saved_device return weight_dict if os.path.isdir(safetensors_path): @@ -527,6 +549,8 @@ def _load_quant_ckpt(self, unified_dtype, sensitive_layer): for k, v in calib_data["absmax"].items(): weight_dict[k.replace(".weight", ".input_absmax")] = v.to(self.device) + if _tp_dev: + self.device = _saved_device return weight_dict def _load_gguf_ckpt(self, gguf_path): diff --git a/lightx2v/models/networks/flux2/infer/transformer_infer.py b/lightx2v/models/networks/flux2/infer/transformer_infer.py index b4d120990..869bcae06 100644 --- a/lightx2v/models/networks/flux2/infer/transformer_infer.py +++ b/lightx2v/models/networks/flux2/infer/transformer_infer.py @@ -13,6 +13,14 @@ def __init__(self, config): self.infer_conditional = True self.clean_cuda_cache = self.config.get("clean_cuda_cache", False) + self.tp_group = None + self.tp_rank = 0 + self.tp_size = 1 + if self.config.get("tensor_parallel", False): + self.tp_group = self.config.get("device_mesh").get_group(mesh_dim="tensor_p") + self.tp_rank = dist.get_rank(self.tp_group) + self.tp_size = dist.get_world_size(self.tp_group) + self.inner_dim = config.get("num_attention_heads", 24) * config.get("attention_head_dim", 64) if self.config.get("seq_parallel", False): @@ -58,7 +66,7 @@ def infer_double_stream_block( image_rotary_emb, img_attn_hook=None, ): - heads = self.config["num_attention_heads"] + heads = self.config["num_attention_heads"] // self.tp_size head_dim = self.config["attention_head_dim"] (shift_msa, scale_msa, gate_msa), (shift_mlp, scale_mlp, gate_mlp) = self._split_double_modulation(temb_mod_img) @@ -169,7 +177,7 @@ def infer_single_stream_block( image_rotary_emb, num_txt_tokens=0, ): - heads = self.config["num_attention_heads"] + heads = self.config["num_attention_heads"] // self.tp_size head_dim = self.config["attention_head_dim"] if encoder_hidden_states is not None: diff --git a/lightx2v/models/networks/flux2/model.py b/lightx2v/models/networks/flux2/model.py index d0c389194..7fd2e836c 100644 --- a/lightx2v/models/networks/flux2/model.py +++ b/lightx2v/models/networks/flux2/model.py @@ -12,6 +12,7 @@ from lightx2v.models.networks.flux2.weights.pre_weights import Flux2DevPreWeights, Flux2PreWeights from lightx2v.models.networks.flux2.weights.transformer_weights import Flux2TransformerWeights from lightx2v.utils.custom_compiler import compiled_method +from lightx2v_platform.base import global_var class _Flux2TransformerModelBase(BaseTransformerModel): @@ -28,6 +29,204 @@ def __init__(self, config, model_path, device): self._init_weights() self._init_infer() + def _rank_device(self): + ai_device = global_var.AI_DEVICE + if ai_device is None: + return torch.device("cpu") + if dist.is_initialized(): + return torch.device(f"{ai_device}:{dist.get_rank()}") + return torch.device(ai_device) + + def _sync_device(self): + ai_device = global_var.AI_DEVICE + device_module = getattr(torch, ai_device, None) if ai_device else None + if device_module is not None and hasattr(device_module, "synchronize"): + device_module.synchronize() + + def _load_weights_from_rank0(self, weight_dict, is_weight_loader): + if not self.use_tp: + return super()._load_weights_from_rank0(weight_dict, is_weight_loader) + if self.cpu_offload: + raise NotImplementedError("Flux2 tensor parallel weight loading does not support cpu_offload yet.") + + global_src_rank = 0 + target_device = self._rank_device() + + if is_weight_loader: + processed_weight_dict = {} + meta_dict = {} + processed_bias_keys = set() + for key, tensor in weight_dict.items(): + split_type = self._get_split_type(key) + if key.endswith(".weight") and split_type is not None: + split_weights = self._split_weight_for_tp(key, tensor, self.tp_size) + for rank_idx, split_weight in enumerate(split_weights): + rank_key = f"{key}__tp_rank_{rank_idx}" + processed_weight_dict[rank_key] = split_weight.contiguous() + meta_dict[key] = {"shape": split_weights[0].shape, "dtype": split_weights[0].dtype, "is_tp": True} + + bias_key = key.replace(".weight", ".bias") + if bias_key in weight_dict and split_type in ("col", "ff_fused_col", "single_fused_col"): + bias_splits = self._split_bias_for_tp(bias_key, weight_dict[bias_key], split_type, self.tp_size) + for rank_idx, split_bias in enumerate(bias_splits): + processed_weight_dict[f"{bias_key}__tp_rank_{rank_idx}"] = split_bias.contiguous() + meta_dict[bias_key] = {"shape": bias_splits[0].shape, "dtype": bias_splits[0].dtype, "is_tp": True} + processed_bias_keys.add(bias_key) + elif key not in processed_bias_keys: + processed_weight_dict[key] = tensor + meta_dict[key] = {"shape": tensor.shape, "dtype": tensor.dtype, "is_tp": False} + + obj_list = [meta_dict] + dist.broadcast_object_list(obj_list, src=global_src_rank) + synced_meta_dict = obj_list[0] + weight_dict = processed_weight_dict + else: + obj_list = [None] + dist.broadcast_object_list(obj_list, src=global_src_rank) + synced_meta_dict = obj_list[0] + + distributed_weight_dict = {key: torch.empty(meta["shape"], dtype=meta["dtype"], device=target_device) for key, meta in synced_meta_dict.items()} + dist.barrier() + + for key in sorted(synced_meta_dict.keys()): + meta = synced_meta_dict[key] + if meta.get("is_tp", False): + for rank_idx in range(self.tp_size): + if is_weight_loader: + src_tensor = weight_dict[f"{key}__tp_rank_{rank_idx}"].to(target_device, non_blocking=True) + else: + src_tensor = torch.empty(meta["shape"], dtype=meta["dtype"], device=target_device) + dist.broadcast(src_tensor, src=global_src_rank) + if rank_idx == self.tp_rank: + distributed_weight_dict[key].copy_(src_tensor, non_blocking=True) + del src_tensor + else: + if is_weight_loader: + distributed_weight_dict[key].copy_(weight_dict[key].to(target_device, non_blocking=True), non_blocking=True) + dist.broadcast(distributed_weight_dict[key], src=global_src_rank) + + self._sync_device() + return distributed_weight_dict + + def _get_split_type(self, key): + if ".norm_" in key: + return None + if key.endswith(".weight") and "single_transformer_blocks." in key and ".attn.to_qkv_mlp_proj." in key: + return "single_fused_col" + if key.endswith(".weight") and "single_transformer_blocks." in key and ".attn.to_out." in key: + return "single_fused_row" + if key.endswith(".weight") and (".ff.linear_in." in key or ".ff_context.linear_in." in key): + return "ff_fused_col" + col_patterns = ( + ".attn.to_q.", + ".attn.to_k.", + ".attn.to_v.", + ".attn.add_q_proj.", + ".attn.add_k_proj.", + ".attn.add_v_proj.", + ) + row_patterns = ( + ".attn.to_out.0.", + ".attn.to_add_out.", + ".ff.linear_out.", + ".ff_context.linear_out.", + ) + if any(pattern in key for pattern in col_patterns): + return "col" + if any(pattern in key for pattern in row_patterns): + return "row" + return None + + def _split_bias_for_tp(self, key, bias, split_type, tp_size): + if split_type == "col": + assert bias.shape[0] % tp_size == 0, f"bias dimension ({bias.shape[0]}) must be divisible by tp_size ({tp_size}) for {key}" + return list(torch.chunk(bias, tp_size, dim=0)) + + if split_type == "ff_fused_col": + assert bias.shape[0] % 2 == 0, f"invalid fused SwiGLU bias dim for {key}: {bias.shape[0]}" + ffn_dim = bias.shape[0] // 2 + assert ffn_dim % tp_size == 0, f"ffn_dim ({ffn_dim}) must be divisible by tp_size ({tp_size}) for {key}" + gate, up = torch.split(bias, [ffn_dim, ffn_dim], dim=0) + gate_chunks = torch.chunk(gate, tp_size, dim=0) + up_chunks = torch.chunk(up, tp_size, dim=0) + return [torch.cat([gate_chunks[rank_idx], up_chunks[rank_idx]], dim=0) for rank_idx in range(tp_size)] + + if split_type == "single_fused_col": + inner_dim = self.config["num_attention_heads"] * self.config["attention_head_dim"] + ffn_dim_twice = bias.shape[0] - 3 * inner_dim + assert ffn_dim_twice > 0 and ffn_dim_twice % 2 == 0, f"invalid fused qkv/mlp bias dim for {key}: {bias.shape[0]}" + ffn_dim = ffn_dim_twice // 2 + assert inner_dim % tp_size == 0, f"inner_dim ({inner_dim}) must be divisible by tp_size ({tp_size}) for {key}" + assert ffn_dim % tp_size == 0, f"ffn_dim ({ffn_dim}) must be divisible by tp_size ({tp_size}) for {key}" + q, k, v, mlp_1, mlp_2 = torch.split(bias, [inner_dim, inner_dim, inner_dim, ffn_dim, ffn_dim], dim=0) + chunks = [torch.chunk(part, tp_size, dim=0) for part in (q, k, v, mlp_1, mlp_2)] + return [torch.cat([part_chunks[rank_idx] for part_chunks in chunks], dim=0) for rank_idx in range(tp_size)] + + raise ValueError(f"Unsupported Flux2 TP bias split type {split_type} for {key}") + + def _split_weight_for_tp(self, key, weight, tp_size): + split_type = self._get_split_type(key) + if split_type is None: + return [weight] * tp_size + + if split_type == "col": + assert weight.shape[0] % tp_size == 0, f"out_dim ({weight.shape[0]}) must be divisible by tp_size ({tp_size}) for {key}" + return list(torch.chunk(weight, tp_size, dim=0)) + + if split_type == "row": + assert weight.shape[1] % tp_size == 0, f"in_dim ({weight.shape[1]}) must be divisible by tp_size ({tp_size}) for {key}" + return list(torch.chunk(weight, tp_size, dim=1)) + + if split_type == "ff_fused_col": + assert weight.shape[0] % 2 == 0, f"invalid fused SwiGLU out_dim for {key}: {weight.shape[0]}" + ffn_dim = weight.shape[0] // 2 + assert ffn_dim % tp_size == 0, f"ffn_dim ({ffn_dim}) must be divisible by tp_size ({tp_size}) for {key}" + gate, up = torch.split(weight, [ffn_dim, ffn_dim], dim=0) + gate_chunks = torch.chunk(gate, tp_size, dim=0) + up_chunks = torch.chunk(up, tp_size, dim=0) + return [torch.cat([gate_chunks[rank_idx], up_chunks[rank_idx]], dim=0) for rank_idx in range(tp_size)] + + inner_dim = self.config["num_attention_heads"] * self.config["attention_head_dim"] + if split_type == "single_fused_col": + ffn_dim_twice = weight.shape[0] - 3 * inner_dim + assert ffn_dim_twice > 0 and ffn_dim_twice % 2 == 0, f"invalid fused qkv/mlp out_dim for {key}: {weight.shape[0]}" + ffn_dim = ffn_dim_twice // 2 + assert inner_dim % tp_size == 0, f"inner_dim ({inner_dim}) must be divisible by tp_size ({tp_size}) for {key}" + assert ffn_dim % tp_size == 0, f"ffn_dim ({ffn_dim}) must be divisible by tp_size ({tp_size}) for {key}" + q, k, v, mlp_1, mlp_2 = torch.split(weight, [inner_dim, inner_dim, inner_dim, ffn_dim, ffn_dim], dim=0) + return [ + torch.cat( + [ + torch.chunk(q, tp_size, dim=0)[rank_idx], + torch.chunk(k, tp_size, dim=0)[rank_idx], + torch.chunk(v, tp_size, dim=0)[rank_idx], + torch.chunk(mlp_1, tp_size, dim=0)[rank_idx], + torch.chunk(mlp_2, tp_size, dim=0)[rank_idx], + ], + dim=0, + ) + for rank_idx in range(tp_size) + ] + + if split_type == "single_fused_row": + ffn_dim = weight.shape[1] - inner_dim + assert ffn_dim > 0, f"invalid fused output in_dim for {key}: {weight.shape[1]}" + assert inner_dim % tp_size == 0, f"inner_dim ({inner_dim}) must be divisible by tp_size ({tp_size}) for {key}" + assert ffn_dim % tp_size == 0, f"ffn_dim ({ffn_dim}) must be divisible by tp_size ({tp_size}) for {key}" + attn, mlp = torch.split(weight, [inner_dim, ffn_dim], dim=1) + return [ + torch.cat( + [ + torch.chunk(attn, tp_size, dim=1)[rank_idx], + torch.chunk(mlp, tp_size, dim=1)[rank_idx], + ], + dim=1, + ) + for rank_idx in range(tp_size) + ] + + raise ValueError(f"Unsupported Flux2 TP split type {split_type} for {key}") + def _init_infer(self): self.transformer_infer = self.transformer_infer_class(self.config) self.pre_infer = self.pre_infer_class(self.config) diff --git a/lightx2v/models/networks/flux2/weights/transformer_weights.py b/lightx2v/models/networks/flux2/weights/transformer_weights.py index da3ed9141..fccca0c72 100644 --- a/lightx2v/models/networks/flux2/weights/transformer_weights.py +++ b/lightx2v/models/networks/flux2/weights/transformer_weights.py @@ -1,7 +1,49 @@ +import torch.distributed as dist + from lightx2v.common.modules.weight_module import WeightModule, WeightModuleList from lightx2v.utils.registry_factory import ATTN_WEIGHT_REGISTER, MM_WEIGHT_REGISTER, RMS_WEIGHT_REGISTER +def _tp_info(config): + if not config.get("tensor_parallel", False): + return None, 0, 1 + tp_group = config.get("device_mesh").get_group(mesh_dim="tensor_p") + return tp_group, dist.get_rank(tp_group), dist.get_world_size(tp_group) + + +def _mm_weight(config, weight_name, bias_name=None, split_dim=None, create_cuda_buffer=False, create_cpu_buffer=False): + mm_type = config.get("dit_quant_scheme", "Default") + if config.get("tensor_parallel", False) and split_dim is not None: + tp_group, tp_rank, tp_size = _tp_info(config) + return MM_WEIGHT_REGISTER["TensorParallel"]( + weight_name=weight_name, + bias_name=bias_name, + mm_type=mm_type, + tp_group=tp_group, + tp_rank=tp_rank, + tp_size=tp_size, + split_dim=split_dim, + create_cuda_buffer=create_cuda_buffer, + create_cpu_buffer=create_cpu_buffer, + ) + return MM_WEIGHT_REGISTER[mm_type]( + weight_name, + bias_name, + create_cuda_buffer, + create_cpu_buffer, + ) + + +def _rms_weight(config, weight_name, create_cuda_buffer=False, create_cpu_buffer=False): + # Flux2 q/k RMSNorm weights are head_dim-sized, so TP over heads must replicate them. + rms_norm_type = config.get("rms_norm_type", "torch") + return RMS_WEIGHT_REGISTER[rms_norm_type]( + weight_name, + create_cuda_buffer, + create_cpu_buffer, + ) + + class Flux2DoubleBlockWeights(WeightModule): """Weights for a single double-stream transformer block.""" @@ -16,112 +58,20 @@ def __init__(self, config, block_idx, create_cuda_buffer=False, create_cpu_buffe p = f"transformer_blocks.{self.block_idx}" - self.add_module( - "to_q", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{p}.attn.to_q.weight", - None, - create_cuda_buffer, - create_cpu_buffer, - ), - ) - self.add_module( - "to_k", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{p}.attn.to_k.weight", - None, - create_cuda_buffer, - create_cpu_buffer, - ), - ) - self.add_module( - "to_v", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{p}.attn.to_v.weight", - None, - create_cuda_buffer, - create_cpu_buffer, - ), - ) - self.add_module( - "norm_q", - RMS_WEIGHT_REGISTER[self.rms_norm_type]( - f"{p}.attn.norm_q.weight", - create_cuda_buffer, - create_cpu_buffer, - ), - ) - self.add_module( - "norm_k", - RMS_WEIGHT_REGISTER[self.rms_norm_type]( - f"{p}.attn.norm_k.weight", - create_cuda_buffer, - create_cpu_buffer, - ), - ) + self.add_module("to_q", _mm_weight(config, f"{p}.attn.to_q.weight", None, "col", create_cuda_buffer, create_cpu_buffer)) + self.add_module("to_k", _mm_weight(config, f"{p}.attn.to_k.weight", None, "col", create_cuda_buffer, create_cpu_buffer)) + self.add_module("to_v", _mm_weight(config, f"{p}.attn.to_v.weight", None, "col", create_cuda_buffer, create_cpu_buffer)) + self.add_module("norm_q", _rms_weight(config, f"{p}.attn.norm_q.weight", create_cuda_buffer, create_cpu_buffer)) + self.add_module("norm_k", _rms_weight(config, f"{p}.attn.norm_k.weight", create_cuda_buffer, create_cpu_buffer)) - self.add_module( - "add_q_proj", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{p}.attn.add_q_proj.weight", - None, - create_cuda_buffer, - create_cpu_buffer, - ), - ) - self.add_module( - "add_k_proj", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{p}.attn.add_k_proj.weight", - None, - create_cuda_buffer, - create_cpu_buffer, - ), - ) - self.add_module( - "add_v_proj", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{p}.attn.add_v_proj.weight", - None, - create_cuda_buffer, - create_cpu_buffer, - ), - ) - self.add_module( - "norm_added_q", - RMS_WEIGHT_REGISTER[self.rms_norm_type]( - f"{p}.attn.norm_added_q.weight", - create_cuda_buffer, - create_cpu_buffer, - ), - ) - self.add_module( - "norm_added_k", - RMS_WEIGHT_REGISTER[self.rms_norm_type]( - f"{p}.attn.norm_added_k.weight", - create_cuda_buffer, - create_cpu_buffer, - ), - ) + self.add_module("add_q_proj", _mm_weight(config, f"{p}.attn.add_q_proj.weight", None, "col", create_cuda_buffer, create_cpu_buffer)) + self.add_module("add_k_proj", _mm_weight(config, f"{p}.attn.add_k_proj.weight", None, "col", create_cuda_buffer, create_cpu_buffer)) + self.add_module("add_v_proj", _mm_weight(config, f"{p}.attn.add_v_proj.weight", None, "col", create_cuda_buffer, create_cpu_buffer)) + self.add_module("norm_added_q", _rms_weight(config, f"{p}.attn.norm_added_q.weight", create_cuda_buffer, create_cpu_buffer)) + self.add_module("norm_added_k", _rms_weight(config, f"{p}.attn.norm_added_k.weight", create_cuda_buffer, create_cpu_buffer)) - self.add_module( - "to_out", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{p}.attn.to_out.0.weight", - None, - create_cuda_buffer, - create_cpu_buffer, - ), - ) - self.add_module( - "to_add_out", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{p}.attn.to_add_out.weight", - None, - create_cuda_buffer, - create_cpu_buffer, - ), - ) + self.add_module("to_out", _mm_weight(config, f"{p}.attn.to_out.0.weight", None, "row", create_cuda_buffer, create_cpu_buffer)) + self.add_module("to_add_out", _mm_weight(config, f"{p}.attn.to_add_out.weight", None, "row", create_cuda_buffer, create_cpu_buffer)) self.add_module("calculate", ATTN_WEIGHT_REGISTER[self.attn_type]()) @@ -131,43 +81,10 @@ def __init__(self, config, block_idx, create_cuda_buffer=False, create_cpu_buffe ATTN_WEIGHT_REGISTER[self.config["parallel"].get("seq_p_attn_type", "ulysses")](), ) - self.add_module( - "ff_net_0", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{p}.ff.linear_in.weight", - None, - create_cuda_buffer, - create_cpu_buffer, - ), - ) - self.add_module( - "ff_net_2", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{p}.ff.linear_out.weight", - None, - create_cuda_buffer, - create_cpu_buffer, - ), - ) - - self.add_module( - "ff_context_net_0", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{p}.ff_context.linear_in.weight", - None, - create_cuda_buffer, - create_cpu_buffer, - ), - ) - self.add_module( - "ff_context_net_2", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{p}.ff_context.linear_out.weight", - None, - create_cuda_buffer, - create_cpu_buffer, - ), - ) + self.add_module("ff_net_0", _mm_weight(config, f"{p}.ff.linear_in.weight", None, "col", create_cuda_buffer, create_cpu_buffer)) + self.add_module("ff_net_2", _mm_weight(config, f"{p}.ff.linear_out.weight", None, "row", create_cuda_buffer, create_cpu_buffer)) + self.add_module("ff_context_net_0", _mm_weight(config, f"{p}.ff_context.linear_in.weight", None, "col", create_cuda_buffer, create_cpu_buffer)) + self.add_module("ff_context_net_2", _mm_weight(config, f"{p}.ff_context.linear_out.weight", None, "row", create_cuda_buffer, create_cpu_buffer)) def to_cuda(self, non_blocking=True): for module in self._modules.values(): @@ -194,42 +111,10 @@ def __init__(self, config, block_idx, create_cuda_buffer=False, create_cpu_buffe p = f"single_transformer_blocks.{self.block_idx}" - self.add_module( - "to_qkv_mlp_proj", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{p}.attn.to_qkv_mlp_proj.weight", - None, - create_cuda_buffer, - create_cpu_buffer, - ), - ) - - self.add_module( - "norm_q", - RMS_WEIGHT_REGISTER[self.rms_norm_type]( - f"{p}.attn.norm_q.weight", - create_cuda_buffer, - create_cpu_buffer, - ), - ) - self.add_module( - "norm_k", - RMS_WEIGHT_REGISTER[self.rms_norm_type]( - f"{p}.attn.norm_k.weight", - create_cuda_buffer, - create_cpu_buffer, - ), - ) - - self.add_module( - "to_out", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{p}.attn.to_out.weight", - None, - create_cuda_buffer, - create_cpu_buffer, - ), - ) + self.add_module("to_qkv_mlp_proj", _mm_weight(config, f"{p}.attn.to_qkv_mlp_proj.weight", None, "col", create_cuda_buffer, create_cpu_buffer)) + self.add_module("norm_q", _rms_weight(config, f"{p}.attn.norm_q.weight", create_cuda_buffer, create_cpu_buffer)) + self.add_module("norm_k", _rms_weight(config, f"{p}.attn.norm_k.weight", create_cuda_buffer, create_cpu_buffer)) + self.add_module("to_out", _mm_weight(config, f"{p}.attn.to_out.weight", None, "row", create_cuda_buffer, create_cpu_buffer)) self.add_module("calculate", ATTN_WEIGHT_REGISTER[self.attn_type]()) @@ -260,8 +145,6 @@ def __init__(self, config): self.num_single_layers = config.get("num_single_layers", 20) self.mm_type = config.get("dit_quant_scheme", "Default") - inner_dim = config.get("num_attention_heads", 24) * config.get("attention_head_dim", 64) - self.double_blocks = WeightModuleList([Flux2DoubleBlockWeights(config, i) for i in range(self.num_layers)]) self.single_blocks = WeightModuleList([Flux2SingleBlockWeights(config, i) for i in range(self.num_single_layers)]) self.register_offload_buffers(config) @@ -269,24 +152,9 @@ def __init__(self, config): self.add_module("double_blocks", self.double_blocks) self.add_module("single_blocks", self.single_blocks) - self.add_module( - "double_stream_modulation_img_linear", - MM_WEIGHT_REGISTER[self.mm_type]( - "double_stream_modulation_img.linear.weight", - ), - ) - self.add_module( - "double_stream_modulation_txt_linear", - MM_WEIGHT_REGISTER[self.mm_type]( - "double_stream_modulation_txt.linear.weight", - ), - ) - self.add_module( - "single_stream_modulation_linear", - MM_WEIGHT_REGISTER[self.mm_type]( - "single_stream_modulation.linear.weight", - ), - ) + self.add_module("double_stream_modulation_img_linear", _mm_weight(config, "double_stream_modulation_img.linear.weight")) + self.add_module("double_stream_modulation_txt_linear", _mm_weight(config, "double_stream_modulation_txt.linear.weight")) + self.add_module("single_stream_modulation_linear", _mm_weight(config, "single_stream_modulation.linear.weight")) def register_offload_buffers(self, config): if config.get("cpu_offload", False) and config.get("offload_granularity", "block") == "block": diff --git a/lightx2v/models/networks/wan/infer/transformer_infer.py b/lightx2v/models/networks/wan/infer/transformer_infer.py index 92030d16f..b8687d6bc 100755 --- a/lightx2v/models/networks/wan/infer/transformer_infer.py +++ b/lightx2v/models/networks/wan/infer/transformer_infer.py @@ -1,6 +1,7 @@ from functools import partial import torch +import torch.distributed as dist from lightx2v.common.transformer_infer.transformer_infer import BaseTransformerInfer from lightx2v.utils.envs import * @@ -25,7 +26,12 @@ def __init__(self, config): self.blocks_num = config["num_layers"] self.phases_num = 3 self.has_post_adapter = False - self.num_heads = config["num_heads"] + if config.get("tensor_parallel", False): + _tp_group = config["device_mesh"].get_group(mesh_dim="tensor_p") + _tp_size = dist.get_world_size(_tp_group) + else: + _tp_size = 1 + self.num_heads = config["num_heads"] // _tp_size self.head_dim = config["dim"] // config["num_heads"] self.window_size = config.get("window_size", (-1, -1)) self.parallel_attention = None diff --git a/lightx2v/models/networks/wan/model.py b/lightx2v/models/networks/wan/model.py index 966c85722..a0772d58d 100755 --- a/lightx2v/models/networks/wan/model.py +++ b/lightx2v/models/networks/wan/model.py @@ -28,6 +28,7 @@ from lightx2v.utils.custom_compiler import compiled_method from lightx2v.utils.envs import * from lightx2v.utils.utils import * +from lightx2v_platform.base.global_var import AI_DEVICE class WanModel(BaseTransformerModel): @@ -52,6 +53,104 @@ def __init__(self, model_path, config, device, model_type="wan2.1", lora_path=No self._init_weights() self._init_infer() + # ------------------------------------------------------------------ TP -- + def _rank_device(self): + if dist.is_initialized(): + return torch.device(f"{AI_DEVICE}:{dist.get_rank()}") + return torch.device(AI_DEVICE) + + def _get_split_type(self, key): + if not key.endswith(".weight"): + return None + col_infixes = ( + ".self_attn.q.", + ".self_attn.k.", + ".self_attn.v.", + ".self_attn.norm_q.", + ".self_attn.norm_k.", + ".cross_attn.q.", + ".cross_attn.k.", + ".cross_attn.v.", + ".cross_attn.norm_q.", + ".cross_attn.norm_k.", + ".cross_attn.k_img.", + ".cross_attn.v_img.", + ) + row_infixes = (".self_attn.o.", ".cross_attn.o.", ".ffn.2.") + if any(s in key for s in col_infixes): + return "col" + if any(s in key for s in row_infixes): + return "row" + if ".ffn.0." in key: + return "col" + return None + + def _split_bias_for_tp(self, bias, split_type, tp_size): + if split_type == "col": + return list(torch.chunk(bias, tp_size, dim=0)) + raise ValueError(f"Unsupported bias split_type: {split_type}") + + def _split_weight_for_tp(self, key, weight, tp_size): + split_type = self._get_split_type(key) + if split_type == "col": + return list(torch.chunk(weight, tp_size, dim=0)) + if split_type == "row": + return list(torch.chunk(weight, tp_size, dim=1)) + raise ValueError(f"Unknown split_type for {key}") + + def _load_weights_from_rank0(self, weight_dict, is_weight_loader): + if not self.use_tp: + return super()._load_weights_from_rank0(weight_dict, is_weight_loader) + + src_rank = 0 + target_device = self._rank_device() + + if is_weight_loader: + processed, meta, processed_bias = {}, {}, set() + for key, tensor in weight_dict.items(): + split_type = self._get_split_type(key) + if key.endswith(".weight") and split_type is not None: + shards = self._split_weight_for_tp(key, tensor, self.tp_size) + for r, shard in enumerate(shards): + processed[f"{key}__tp_{r}"] = shard.contiguous() + meta[key] = {"shape": shards[0].shape, "dtype": shards[0].dtype, "is_tp": True} + bias_key = key.replace(".weight", ".bias") + if bias_key in weight_dict and split_type == "col": + bias_shards = self._split_bias_for_tp(weight_dict[bias_key], split_type, self.tp_size) + for r, shard in enumerate(bias_shards): + processed[f"{bias_key}__tp_{r}"] = shard.contiguous() + meta[bias_key] = {"shape": bias_shards[0].shape, "dtype": bias_shards[0].dtype, "is_tp": True} + processed_bias.add(bias_key) + elif key not in processed_bias: + processed[key] = tensor + meta[key] = {"shape": tensor.shape, "dtype": tensor.dtype, "is_tp": False} + obj_list = [meta] + else: + obj_list = [None] + + dist.broadcast_object_list(obj_list, src=src_rank) + synced_meta = obj_list[0] + + distributed = {k: torch.empty(m["shape"], dtype=m["dtype"], device=target_device) for k, m in synced_meta.items()} + + for key in sorted(synced_meta.keys()): + m = synced_meta[key] + if m["is_tp"]: + for r in range(self.tp_size): + buf = processed[f"{key}__tp_{r}"].to(target_device) if is_weight_loader else torch.empty(m["shape"], dtype=m["dtype"], device=target_device) + dist.broadcast(buf, src=src_rank, group=self.tp_group) + if r == self.tp_rank: + distributed[key].copy_(buf) + del buf + else: + if is_weight_loader: + distributed[key].copy_(processed[key].to(target_device)) + dist.broadcast(distributed[key], src=src_rank, group=self.tp_group) + + return distributed + + # ------------------------------------------------------------------ TP -- + def _init_infer_class(self): self.pre_infer_class = WanPreInfer self.post_infer_class = WanPostInfer diff --git a/lightx2v/models/networks/wan/weights/transformer_weights.py b/lightx2v/models/networks/wan/weights/transformer_weights.py index 8d09b4c1e..fcab8e0b3 100755 --- a/lightx2v/models/networks/wan/weights/transformer_weights.py +++ b/lightx2v/models/networks/wan/weights/transformer_weights.py @@ -1,3 +1,5 @@ +import torch.distributed as dist + from lightx2v.common.modules.weight_module import WeightModule, WeightModuleList from lightx2v.utils.registry_factory import ( ATTN_WEIGHT_REGISTER, @@ -8,6 +10,39 @@ ) +def _mm_weight(config, weight_name, bias_name, split_dim=None, create_cuda_buffer=False, create_cpu_buffer=False, lazy_load=False, lazy_load_file=None, lora_prefix="", lora_path=""): + mm_type = config.get("dit_quant_scheme", "Default") + if config.get("do_mm_calib", False): + mm_type = "Calib" + if config.get("tensor_parallel", False) and split_dim is not None: + tp_group = config["device_mesh"].get_group(mesh_dim="tensor_p") + return MM_WEIGHT_REGISTER["TensorParallel"]( + weight_name=weight_name, + bias_name=bias_name, + mm_type=mm_type, + tp_group=tp_group, + tp_rank=dist.get_rank(tp_group), + tp_size=dist.get_world_size(tp_group), + split_dim=split_dim, + create_cuda_buffer=create_cuda_buffer, + create_cpu_buffer=create_cpu_buffer, + lazy_load=lazy_load, + lazy_load_file=lazy_load_file, + lora_prefix=lora_prefix, + lora_path=lora_path, + ) + return MM_WEIGHT_REGISTER[mm_type]( + weight_name, + bias_name, + create_cuda_buffer, + create_cpu_buffer, + lazy_load, + lazy_load_file, + lora_prefix=lora_prefix, + lora_path=lora_path, + ) + + class WanTransformerWeights(WeightModule): def __init__(self, config, lazy_load_path=None, lora_path=None): super().__init__() @@ -256,55 +291,63 @@ def __init__( LN_WEIGHT_REGISTER["torch"](), ) + p = f"{block_prefix}.{self.block_index}" self.add_module( "self_attn_q", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{block_prefix}.{self.block_index}.self_attn.q.weight", - f"{block_prefix}.{self.block_index}.self_attn.q.bias", - create_cuda_buffer, - create_cpu_buffer, - self.lazy_load, - self.lazy_load_file, + _mm_weight( + config, + f"{p}.self_attn.q.weight", + f"{p}.self_attn.q.bias", + split_dim="col", + create_cuda_buffer=create_cuda_buffer, + create_cpu_buffer=create_cpu_buffer, + lazy_load=self.lazy_load, + lazy_load_file=self.lazy_load_file, lora_prefix=block_prefix, lora_path=lora_path, ), ) - self.add_module( "self_attn_k", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{block_prefix}.{self.block_index}.self_attn.k.weight", - f"{block_prefix}.{self.block_index}.self_attn.k.bias", - create_cuda_buffer, - create_cpu_buffer, - self.lazy_load, - self.lazy_load_file, + _mm_weight( + config, + f"{p}.self_attn.k.weight", + f"{p}.self_attn.k.bias", + split_dim="col", + create_cuda_buffer=create_cuda_buffer, + create_cpu_buffer=create_cpu_buffer, + lazy_load=self.lazy_load, + lazy_load_file=self.lazy_load_file, lora_prefix=block_prefix, lora_path=lora_path, ), ) self.add_module( "self_attn_v", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{block_prefix}.{self.block_index}.self_attn.v.weight", - f"{block_prefix}.{self.block_index}.self_attn.v.bias", - create_cuda_buffer, - create_cpu_buffer, - self.lazy_load, - self.lazy_load_file, + _mm_weight( + config, + f"{p}.self_attn.v.weight", + f"{p}.self_attn.v.bias", + split_dim="col", + create_cuda_buffer=create_cuda_buffer, + create_cpu_buffer=create_cpu_buffer, + lazy_load=self.lazy_load, + lazy_load_file=self.lazy_load_file, lora_prefix=block_prefix, lora_path=lora_path, ), ) self.add_module( "self_attn_o", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{block_prefix}.{self.block_index}.self_attn.o.weight", - f"{block_prefix}.{self.block_index}.self_attn.o.bias", - create_cuda_buffer, - create_cpu_buffer, - self.lazy_load, - self.lazy_load_file, + _mm_weight( + config, + f"{p}.self_attn.o.weight", + f"{p}.self_attn.o.bias", + split_dim="row", + create_cuda_buffer=create_cuda_buffer, + create_cpu_buffer=create_cpu_buffer, + lazy_load=self.lazy_load, + lazy_load_file=self.lazy_load_file, lora_prefix=block_prefix, lora_path=lora_path, ), @@ -486,54 +529,63 @@ def __init__( lora_path=lora_path, ), ) + cp = f"{block_prefix}.{self.block_index}" self.add_module( "cross_attn_q", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{block_prefix}.{self.block_index}.cross_attn.q.weight", - f"{block_prefix}.{self.block_index}.cross_attn.q.bias", - create_cuda_buffer, - create_cpu_buffer, - self.lazy_load, - self.lazy_load_file, + _mm_weight( + config, + f"{cp}.cross_attn.q.weight", + f"{cp}.cross_attn.q.bias", + split_dim="col", + create_cuda_buffer=create_cuda_buffer, + create_cpu_buffer=create_cpu_buffer, + lazy_load=self.lazy_load, + lazy_load_file=self.lazy_load_file, lora_prefix=block_prefix, lora_path=lora_path, ), ) self.add_module( "cross_attn_k", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{block_prefix}.{self.block_index}.cross_attn.k.weight", - f"{block_prefix}.{self.block_index}.cross_attn.k.bias", - create_cuda_buffer, - create_cpu_buffer, - self.lazy_load, - self.lazy_load_file, + _mm_weight( + config, + f"{cp}.cross_attn.k.weight", + f"{cp}.cross_attn.k.bias", + split_dim="col", + create_cuda_buffer=create_cuda_buffer, + create_cpu_buffer=create_cpu_buffer, + lazy_load=self.lazy_load, + lazy_load_file=self.lazy_load_file, lora_prefix=block_prefix, lora_path=lora_path, ), ) self.add_module( "cross_attn_v", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{block_prefix}.{self.block_index}.cross_attn.v.weight", - f"{block_prefix}.{self.block_index}.cross_attn.v.bias", - create_cuda_buffer, - create_cpu_buffer, - self.lazy_load, - self.lazy_load_file, + _mm_weight( + config, + f"{cp}.cross_attn.v.weight", + f"{cp}.cross_attn.v.bias", + split_dim="col", + create_cuda_buffer=create_cuda_buffer, + create_cpu_buffer=create_cpu_buffer, + lazy_load=self.lazy_load, + lazy_load_file=self.lazy_load_file, lora_prefix=block_prefix, lora_path=lora_path, ), ) self.add_module( "cross_attn_o", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{block_prefix}.{self.block_index}.cross_attn.o.weight", - f"{block_prefix}.{self.block_index}.cross_attn.o.bias", - create_cuda_buffer, - create_cpu_buffer, - self.lazy_load, - self.lazy_load_file, + _mm_weight( + config, + f"{cp}.cross_attn.o.weight", + f"{cp}.cross_attn.o.bias", + split_dim="row", + create_cuda_buffer=create_cuda_buffer, + create_cpu_buffer=create_cpu_buffer, + lazy_load=self.lazy_load, + lazy_load_file=self.lazy_load_file, lora_prefix=block_prefix, lora_path=lora_path, ), @@ -567,26 +619,30 @@ def __init__( if self.config["task"] in ["i2v", "flf2v", "animate", "s2v", "rs2v"] and self.config.get("use_image_encoder", True) and self.config["model_cls"] != "wan2.1_sf_mtxg2": self.add_module( "cross_attn_k_img", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{block_prefix}.{self.block_index}.cross_attn.k_img.weight", - f"{block_prefix}.{self.block_index}.cross_attn.k_img.bias", - create_cuda_buffer, - create_cpu_buffer, - self.lazy_load, - self.lazy_load_file, + _mm_weight( + config, + f"{cp}.cross_attn.k_img.weight", + f"{cp}.cross_attn.k_img.bias", + split_dim="col", + create_cuda_buffer=create_cuda_buffer, + create_cpu_buffer=create_cpu_buffer, + lazy_load=self.lazy_load, + lazy_load_file=self.lazy_load_file, lora_prefix=block_prefix, lora_path=lora_path, ), ) self.add_module( "cross_attn_v_img", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{block_prefix}.{self.block_index}.cross_attn.v_img.weight", - f"{block_prefix}.{self.block_index}.cross_attn.v_img.bias", - create_cuda_buffer, - create_cpu_buffer, - self.lazy_load, - self.lazy_load_file, + _mm_weight( + config, + f"{cp}.cross_attn.v_img.weight", + f"{cp}.cross_attn.v_img.bias", + split_dim="col", + create_cuda_buffer=create_cuda_buffer, + create_cpu_buffer=create_cpu_buffer, + lazy_load=self.lazy_load, + lazy_load_file=self.lazy_load_file, lora_prefix=block_prefix, lora_path=lora_path, ), @@ -634,28 +690,33 @@ def __init__( LN_WEIGHT_REGISTER["torch"](), ) + fp = f"{block_prefix}.{self.block_index}" self.add_module( "ffn_0", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{block_prefix}.{self.block_index}.ffn.0.weight", - f"{block_prefix}.{self.block_index}.ffn.0.bias", - create_cuda_buffer, - create_cpu_buffer, - self.lazy_load, - self.lazy_load_file, + _mm_weight( + config, + f"{fp}.ffn.0.weight", + f"{fp}.ffn.0.bias", + split_dim="col", + create_cuda_buffer=create_cuda_buffer, + create_cpu_buffer=create_cpu_buffer, + lazy_load=self.lazy_load, + lazy_load_file=self.lazy_load_file, lora_prefix=block_prefix, lora_path=lora_path, ), ) self.add_module( "ffn_2", - MM_WEIGHT_REGISTER[self.mm_type]( - f"{block_prefix}.{self.block_index}.ffn.2.weight", - f"{block_prefix}.{self.block_index}.ffn.2.bias", - create_cuda_buffer, - create_cpu_buffer, - self.lazy_load, - self.lazy_load_file, + _mm_weight( + config, + f"{fp}.ffn.2.weight", + f"{fp}.ffn.2.bias", + split_dim="row", + create_cuda_buffer=create_cuda_buffer, + create_cpu_buffer=create_cpu_buffer, + lazy_load=self.lazy_load, + lazy_load_file=self.lazy_load_file, lora_prefix=block_prefix, lora_path=lora_path, ), diff --git a/lightx2v/models/runners/default_runner.py b/lightx2v/models/runners/default_runner.py index 3e21e4a22..91cf1eaf6 100755 --- a/lightx2v/models/runners/default_runner.py +++ b/lightx2v/models/runners/default_runner.py @@ -11,6 +11,7 @@ from requests.exceptions import RequestException from lightx2v.models.runners.base_runner import BaseRunner +from lightx2v.models.runners.tp_runner_mixin import TPRunnerMixin from lightx2v.server.metrics import monitor_cli from lightx2v.utils.envs import * from lightx2v.utils.generate_task_id import generate_task_id @@ -83,7 +84,7 @@ def resize_image(img, resize_mode="adaptive", resolution="480p", bucket_shape=No return cropped_img, target_h, target_w -class DefaultRunner(BaseRunner): +class DefaultRunner(TPRunnerMixin, BaseRunner): def __init__(self, config): super().__init__(config) self.has_prompt_enhancer = False @@ -155,6 +156,16 @@ def load_vsr_model(self): @ProfilingContext4DebugL2("Load models") def load_model(self): + if self._use_tp_rank0_io() and not self._is_rank0(): + self.model = self.load_transformer() + self.text_encoders = None + self.image_encoder = None + self.vae_encoder = None + self.vae_decoder = None + self.vfi_model = None + self.vsr_model = None + return + self.model = self.load_transformer() self.text_encoders = self.load_text_encoder() self.image_encoder = self.load_image_encoder() @@ -485,6 +496,8 @@ def post_prompt_enhancer(self): return enhanced_prompt def process_images_after_vae_decoder(self): + if self.gen_video_final is None: + return None self.gen_video_final = wan_vae_to_comfy(self.gen_video_final) if "video_frame_interpolation" in self.config: diff --git a/lightx2v/models/runners/flux2/flux2_runner.py b/lightx2v/models/runners/flux2/flux2_runner.py index 3c9334295..2b665044d 100644 --- a/lightx2v/models/runners/flux2/flux2_runner.py +++ b/lightx2v/models/runners/flux2/flux2_runner.py @@ -4,6 +4,7 @@ import numpy as np import torch +import torch.distributed as dist from loguru import logger from lightx2v.models.networks.flux2.model import Flux2DevTransformerModel, Flux2KleinTransformerModel @@ -48,10 +49,31 @@ def _get_scheduler_class(self): @ProfilingContext4DebugL2("Load models") def load_model(self): + if self._use_tp_rank0_io(): + self.text_encoders = None + self.vae = None + self.model = self.load_transformer() + return + self.text_encoders = self.load_text_encoder() self.vae = self.load_vae() self.model = self.load_transformer() + def _load_rank0_vae(self): + if not self._is_rank0(): + return + if self.vae is None or self.config.get("lazy_load", False) or self.config.get("unload_modules", False): + self.vae = self.load_vae() + + def _unload_rank0_vae(self): + if not self._is_rank0(): + return + if self.vae is not None and (self._use_tp_rank0_io() or self.config.get("lazy_load", False) or self.config.get("unload_modules", False)): + del self.vae + self.vae = None + torch_device_module.empty_cache() + gc.collect() + def load_vae(self): return Flux2VAE(self.config) @@ -64,6 +86,8 @@ def init_modules(self): assert self.config.get("cpu_offload", False) task = self.config.get("task", "t2i") + if self._use_tp_rank0_io() and task == "i2i": + raise NotImplementedError("Flux2 tensor parallel currently supports t2i only; i2i needs rank0 VAE encode broadcast.") if task == "i2i": self.run_input_encoder = self._run_input_encoder_local_i2i self.run_dit = self._run_dit_local_i2i @@ -73,6 +97,24 @@ def init_modules(self): @ProfilingContext4DebugL2("Run Encoders") def _run_input_encoder_local_t2i(self): + if self._use_tp_rank0_io(): + payload = None + if self._is_rank0(): + self._load_rank0_text_encoder() + prompt = self.input_info.prompt + text_encoder_output = self.run_text_encoder(prompt, neg_prompt=self.input_info.negative_prompt) + self._unload_rank0_text_encoder() + payload = { + "inputs": { + "text_encoder_output": text_encoder_output, + "image_encoder_output": None, + }, + "target_shape": self.input_info.target_shape, + } + payload = self._broadcast_rank0_payload(payload) + self.input_info.target_shape = payload["target_shape"] + return payload["inputs"] + prompt = self.input_info.prompt if self.config.get("lazy_load", False) or self.config.get("unload_modules", False): self.text_encoders = self.load_text_encoder() @@ -88,6 +130,22 @@ def _run_input_encoder_local_t2i(self): @ProfilingContext4DebugL2("Run Encoders I2I") def _run_input_encoder_local_i2i(self): + if self._use_tp_rank0_io(): + payload = None + if self._is_rank0(): + self._load_rank0_text_encoder() + payload = { + "inputs": self._run_input_encoder_local_i2i_rank0(), + "target_shape": self.input_info.target_shape, + } + self._unload_rank0_text_encoder() + payload = self._broadcast_rank0_payload(payload) + self.input_info.target_shape = payload["target_shape"] + return payload["inputs"] + + return self._run_input_encoder_local_i2i_rank0() + + def _run_input_encoder_local_i2i_rank0(self): prompt = self.input_info.prompt if self.config.get("lazy_load", False) or self.config.get("unload_modules", False): self.text_encoders = self.load_text_encoder() @@ -330,9 +388,28 @@ def set_img_shapes(self): @ProfilingContext4DebugL1("Run VAE Decoder") def run_vae_decoder(self, latents): + if self._use_tp_rank0_io(): + images = None + if self._is_rank0(): + self._load_rank0_vae() + images = self._decode_latents_with_vae(latents) + self._unload_rank0_vae() + dist.barrier() + return images + if self.config.get("lazy_load", False) or self.config.get("unload_modules", False): self.vae = self.load_vae() + images = self._decode_latents_with_vae(latents) + + if self.config.get("lazy_load", False) or self.config.get("unload_modules", False): + del self.vae + torch_device_module.empty_cache() + gc.collect() + + return images + + def _decode_latents_with_vae(self, latents): B, _, C = latents.shape H = int((self.input_info.latent_image_ids[0, :, 1].max() + 1).item()) @@ -348,14 +425,7 @@ def run_vae_decoder(self, latents): latents = latents.permute(0, 1, 4, 2, 5, 3) latents = latents.reshape(B, C // 4, H * 2, W * 2) - images = self.vae.decode(latents, self.input_info) - - if self.config.get("lazy_load", False) or self.config.get("unload_modules", False): - del self.vae - torch_device_module.empty_cache() - gc.collect() - - return images + return self.vae.decode(latents, self.input_info) @ProfilingContext4DebugL1("RUN pipeline") def run_pipeline(self, input_info): @@ -368,6 +438,7 @@ def run_pipeline(self, input_info): latents, generator = self.run_dit() images = self.run_vae_decoder(latents) + self.end_run() if not input_info.return_result_tensor and is_main_process(): image = images[0] diff --git a/lightx2v/models/runners/tp_runner_mixin.py b/lightx2v/models/runners/tp_runner_mixin.py new file mode 100644 index 000000000..c883c0e7b --- /dev/null +++ b/lightx2v/models/runners/tp_runner_mixin.py @@ -0,0 +1,137 @@ +""" +Mixin that gives any Runner tensor-parallel rank-0 I/O support: + + - rank predicates (_use_tp_rank0_io, _is_rank0, _rank_device) + - broadcast any nested dict/list/tuple-of-tensors from rank 0 to all TP ranks + - rank-0-only text-encoder load / unload helpers +""" + +import gc + +import torch +import torch.distributed as dist + +from lightx2v_platform.base.global_var import AI_DEVICE + +torch_device_module = getattr(torch, AI_DEVICE) + +# --------------------------------------------------------------------------- +# Module-level helpers for the broadcast primitive. +# These are pure functions (no Runner state needed), so they live at module +# level rather than as instance methods. +# --------------------------------------------------------------------------- + +_TENSOR_TAG = "__T__" +_OBJECT_TAG = "__O__" + + +def _pack(obj): + """Recursively replace tensors with (shape, dtype) metadata stubs.""" + if torch.is_tensor(obj): + return {_TENSOR_TAG: (tuple(obj.shape), obj.dtype)} + if isinstance(obj, dict): + return {k: _pack(v) for k, v in obj.items()} + if isinstance(obj, (list, tuple)): + packed = [_pack(v) for v in obj] + return packed if isinstance(obj, list) else tuple(packed) + return {_OBJECT_TAG: obj} + + +def _alloc(meta, device): + """Allocate empty tensors to mirror the structure returned by _pack.""" + if isinstance(meta, dict): + if _TENSOR_TAG in meta: + shape, dtype = meta[_TENSOR_TAG] + return torch.empty(shape, dtype=dtype, device=device) + if _OBJECT_TAG in meta: + return meta[_OBJECT_TAG] + return {k: _alloc(v, device) for k, v in meta.items()} + if isinstance(meta, list): + return [_alloc(v, device) for v in meta] + if isinstance(meta, tuple): + return tuple(_alloc(v, device) for v in meta) + return meta + + +def _broadcast(skeleton, source, src_rank, rank0, device): + """Fill skeleton with tensors broadcast from rank src_rank. + + skeleton: pre-allocated structure (from _alloc) on every non-rank0 rank; + on rank0 it is the original source. + source: original payload on rank0, None on other ranks. + """ + if torch.is_tensor(skeleton): + if rank0: + skeleton = source.to(device, non_blocking=True) + dist.broadcast(skeleton, src=src_rank) + return skeleton + if isinstance(skeleton, dict): + return {k: _broadcast(skeleton[k], source[k] if rank0 else None, src_rank, rank0, device) for k in skeleton} + if isinstance(skeleton, list): + return [_broadcast(skeleton[i], source[i] if rank0 else None, src_rank, rank0, device) for i in range(len(skeleton))] + if isinstance(skeleton, tuple): + return tuple(_broadcast(skeleton[i], source[i] if rank0 else None, src_rank, rank0, device) for i in range(len(skeleton))) + return skeleton + + +# --------------------------------------------------------------------------- +# Mixin +# --------------------------------------------------------------------------- + + +class TPRunnerMixin: + """Rank-0 I/O helpers for tensor-parallel runners. + + Mix in before DefaultRunner / BaseRunner so that any runner subclass + automatically gets TP support without touching its own load_model or + run_input_encoder logic. + """ + + # --- predicates --------------------------------------------------------- + + def _use_tp_rank0_io(self): + return self.config.get("tensor_parallel", False) and dist.is_initialized() + + def _is_rank0(self): + return not dist.is_initialized() or dist.get_rank() == 0 + + def _rank_device(self): + if dist.is_initialized(): + return torch.device(f"{AI_DEVICE}:{dist.get_rank()}") + return torch.device(AI_DEVICE) + + # --- broadcast ---------------------------------------------------------- + + def _broadcast_rank0_payload(self, payload): + """Broadcast a nested dict/list/tuple-of-tensors from rank 0 to all ranks. + + Uses broadcast_object_list for the metadata (fast, zero-copy for small + dicts) and dist.broadcast for each tensor buffer (direct NCCL transfer). + """ + if not self._use_tp_rank0_io(): + return payload + + device = self._rank_device() + meta = [_pack(payload) if self._is_rank0() else None] + dist.broadcast_object_list(meta, src=0) + skeleton = payload if self._is_rank0() else _alloc(meta[0], device) + result = _broadcast(skeleton, payload if self._is_rank0() else None, 0, self._is_rank0(), device) + dist.barrier() + return result + + # --- encoder lifecycle -------------------------------------------------- + + def _load_rank0_text_encoder(self): + if not self._is_rank0(): + return + if self.text_encoders is None or self.config.get("lazy_load", False) or self.config.get("unload_modules", False): + self.text_encoders = self.load_text_encoder() + + def _unload_rank0_text_encoder(self): + if not self._is_rank0(): + return + if self.text_encoders is not None and (self._use_tp_rank0_io() or self.config.get("lazy_load", False) or self.config.get("unload_modules", False)): + del self.text_encoders + self.text_encoders = None + torch_device_module.empty_cache() + gc.collect() diff --git a/lightx2v/models/runners/wan/wan_runner.py b/lightx2v/models/runners/wan/wan_runner.py index 8d6b918c8..d831126ca 100755 --- a/lightx2v/models/runners/wan/wan_runner.py +++ b/lightx2v/models/runners/wan/wan_runner.py @@ -171,6 +171,8 @@ def load_text_encoder(self): return text_encoders def get_vae_parallel(self): + if self.config.get("tensor_parallel", False): + return False if isinstance(self.config.get("parallel", False), bool): return self.config.get("parallel", False) if isinstance(self.config.get("parallel", False), dict): @@ -629,6 +631,30 @@ def get_latent_shape_with_target_hw(self): ] return latent_shape + @ProfilingContext4DebugL2("Run Encoders") + def _run_input_encoder_local_t2v(self): + if self._use_tp_rank0_io(): + payload = None + if self._is_rank0(): + self.input_info.latent_shape = self.get_latent_shape_with_target_hw() + self._load_rank0_text_encoder() + text_encoder_output = self.run_text_encoder(self.input_info) + self._unload_rank0_text_encoder() + payload = {"text_encoder_output": text_encoder_output, "latent_shape": self.input_info.latent_shape} + payload = self._broadcast_rank0_payload(payload) + self.input_info.latent_shape = payload["latent_shape"] + return {"text_encoder_output": payload["text_encoder_output"], "image_encoder_output": None} + return super()._run_input_encoder_local_t2v() + + def run_vae_decoder(self, latents): + if self._use_tp_rank0_io(): + images = None + if self._is_rank0(): + images = super().run_vae_decoder(latents) + dist.barrier() + return images + return super().run_vae_decoder(latents) + class MultiModelStruct: def __init__(self, model_list, config, boundary=0.875, num_train_timesteps=1000): diff --git a/scripts/flux2/infer_flux2_dev_dist.sh b/scripts/flux2/infer_flux2_dev_dist.sh new file mode 100644 index 000000000..010760c9e --- /dev/null +++ b/scripts/flux2/infer_flux2_dev_dist.sh @@ -0,0 +1,14 @@ +#!/bin/bash +lightx2v_path=/data/nvme1/wushuo/LightX2V +model_path="/data/nvme1/wushuo/hf_models/models/black-forest-labs/FLUX.2-dev" +export CUDA_VISIBLE_DEVICES=0,1,2,3 + +source ${lightx2v_path}/scripts/base/base.sh + +torchrun --nproc_per_node=4 -m lightx2v.infer \ + --model_cls flux2_dev \ + --task t2i \ + --model_path $model_path \ + --prompt "Realistic macro photograph of a hermit crab using a soda can as its shell, partially emerging from the can, captured with sharp detail and natural colors, on a sunlit beach with soft shadows and a shallow depth of field, with blurred ocean waves in the background. The can has the text `BFL Diffusers` on it and it has a color gradient that start with #FF5733 at the top and transitions to #33FF57 at the bottom." \ + --save_result_path "${lightx2v_path}/save_results/flux2_dev.png" \ + --config_json "${lightx2v_path}/configs/flux2/flux2_dev_tp.json" diff --git a/scripts/wan22/run_wan22_moe_t2v_tp.sh b/scripts/wan22/run_wan22_moe_t2v_tp.sh new file mode 100755 index 000000000..2d924ba05 --- /dev/null +++ b/scripts/wan22/run_wan22_moe_t2v_tp.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# set path firstly +lightx2v_path=/data/nvme1/wushuo/LightX2V +model_path=/data/nvme1/wushuo/hf_models/models/Wan2.2-T2V-A14B + +export CUDA_VISIBLE_DEVICES=0,1,2,3 + +# set environment variables +source ${lightx2v_path}/scripts/base/base.sh + +torchrun --nproc_per_node=4 -m lightx2v.infer \ +--model_cls wan2.2_moe \ +--task t2v \ +--model_path $model_path \ +--config_json ${lightx2v_path}/configs/wan22/wan_moe_t2v_tp.json \ +--prompt "Two anthropomorphic cats in comfy boxing gear and bright gloves fight intensely on a spotlighted stage." \ +--negative_prompt "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" \ +--save_result_path ${lightx2v_path}/save_results/output_lightx2v_wan22_moe_t2v.mp4