Merge branch 'rebased-refactor' of https://github.com/SolitaryThinker/FastVideo into rebased-refactor

This commit is contained in:
Peiyuan Zhang
2025-03-16 03:05:04 +00:00
155 changed files with 14197 additions and 2724 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
A lone hiker stands atop a towering cliff, silhouetted against the vast horizon. The rugged landscape stretches endlessly beneath, its earthy tones blending into the soft blues of the sky. The scene captures the spirit of exploration and human resilience. High angle, dynamic framing, with soft natural lighting emphasizing the grandeur of nature.
Will Smith casually eats noodles, his relaxed demeanor contrasting with the energetic background of a bustling street food market. The scene captures a mix of humor and authenticity. Mid-shot framing, vibrant lighting.
A lone hiker stands atop a towering cliff, silhouetted against the vast horizon. The rugged landscape stretches endlessly beneath, its earthy tones blending into the soft blues of the sky. The scene captures the spirit of exploration and human resilience. High angle, dynamic framing, with soft natural lighting emphasizing the grandeur of nature.
A hand with delicate fingers picks up a bright yellow lemon from a wooden bowl filled with lemons and sprigs of mint against a peach-colored background. The hand gently tosses the lemon up and catches it, showcasing its smooth texture. A beige string bag sits beside the bowl, adding a rustic touch to the scene. Additional lemons, one halved, are scattered around the base of the bowl. The even lighting enhances the vibrant colors and creates a fresh, inviting atmosphere.
A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes wide with interest. The playful yet serene atmosphere is complemented by soft natural light filtering through the petals. Mid-shot, warm and cheerful tones.
A superintelligent humanoid robot waking up. The robot has a sleek metallic body with futuristic design features. Its glowing red eyes are the focal point, emanating a sharp, intense light as it powers on. The scene is set in a dimly lit, high-tech laboratory filled with glowing control panels, robotic arms, and holographic screens. The setting emphasizes advanced technology and an atmosphere of mystery. The ambiance is eerie and dramatic, highlighting the moment of awakening and the robots immense intelligence. Photorealistic style with a cinematic, dark sci-fi aesthetic. Aspect ratio: 16:9 --v 6.1
-1
View File
@@ -1 +0,0 @@
from .distributed_attn import DistributedAttention
-250
View File
@@ -1,250 +0,0 @@
import os
import random
import time
from pathlib import Path
from typing import Dict, List, Optional, Union, Any
import torch
from safetensors.torch import load_file as safetensors_load_file
import json
from fastvideo.models.hunyuan.constants import NEGATIVE_PROMPT, PRECISION_TO_TYPE, PROMPT_TEMPLATE
# from fastvideo.models.hunyuan.diffusion.pipelines import HunyuanVideoPipeline
from fastvideo.pipelines.pipeline_base import DiffusionPipelineBase
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.models.hunyuan.diffusion.schedulers import FlowMatchDiscreteScheduler
# from fastvideo.models.hunyuan.modules import load_model
# from fastvideo.models.hunyuan.text_encoder import TextEncoder
from fastvideo.models.hunyuan.utils.data_utils import align_to
# from fastvideo.models.hunyuan.vae import load_vae
# from fastvideo.utils.parallel_states import nccl_info
from fastvideo.inference_args import InferenceArgs
from fastvideo.platforms import current_platform
from fastvideo.logger import init_logger
from fastvideo.pipelines.loader import PipelineLoader, get_pipeline_loader
from fastvideo.distributed.parallel_state import get_sp_group
logger = init_logger(__name__)
class DiffusionInference:
"""
Unified inference class that works with any diffusion pipeline.
This combines model loading, pipeline creation, and inference in a flexible way.
"""
def __init__(self,
args: InferenceArgs,
pipeline: DiffusionPipelineBase,
default_negative_prompt: str = NEGATIVE_PROMPT):
"""
Initialize the inference class with a pipeline and args.
Args:
args: The inference arguments
pipeline: The diffusion pipeline to use
default_negative_prompt: The default negative prompt to use
"""
self.args = args
self.pipeline = pipeline
self.default_negative_prompt = default_negative_prompt
@classmethod
def load_pipeline(cls, inference_args: InferenceArgs):
"""
Create an inference instance from pretrained model components.
Args:
inference_args: The inference arguments containing model path and other settings
Returns:
A DiffusionInference instance ready for inference
"""
logger.debug("start loading pipeline")
models_root_path = Path(inference_args.model_path)
if not models_root_path.exists():
raise ValueError(f"`models_root` not exists: {models_root_path}")
# Create save folder to save the samples
save_path = inference_args.output_path
os.makedirs(os.path.dirname(save_path), exist_ok=True)
# Log which platform we're using
print(f"Using platform: {current_platform.__class__.__name__}")
assert current_platform.is_cuda(), "CUDA is not available"
# init distributed
# _initialize_distributed()
# get loader for the pipeline
# Create pipeline using the components
pipeline_loader = get_pipeline_loader(inference_args)
pipeline = pipeline_loader.load_pipeline(
inference_args=inference_args,
)
return cls(inference_args, pipeline, default_negative_prompt=NEGATIVE_PROMPT)
def predict(self, prompt: str, inference_args: InferenceArgs) -> Dict[str, Any]:
"""
Run inference with the pipeline.
Returns:
Dictionary with generated samples and metadata
"""
out_dict = dict()
batch_size = inference_args.batch_size
num_videos_per_prompt = inference_args.num_videos
seed = inference_args.seed
height = inference_args.height
width = inference_args.width
video_length = inference_args.num_frames
# prompt = inference_args.prompt
negative_prompt = inference_args.neg_prompt
infer_steps = inference_args.num_inference_steps
guidance_scale = inference_args.guidance_scale
flow_shift = inference_args.flow_shift
# mask_strategy = inference_args.mask_strategy
embedded_guidance_scale = inference_args.embedded_cfg_scale
# ========================================================================
# Arguments: seed
# ========================================================================
if isinstance(seed, torch.Tensor):
seed = seed.tolist()
if seed is None:
seeds = [random.randint(0, 1_000_000) for _ in range(batch_size * num_videos_per_prompt)]
elif isinstance(seed, int):
seeds = [seed + i for _ in range(batch_size) for i in range(num_videos_per_prompt)]
elif isinstance(seed, (list, tuple)):
if len(seed) == batch_size:
seeds = [int(seed[i]) + j for i in range(batch_size) for j in range(num_videos_per_prompt)]
elif len(seed) == batch_size * num_videos_per_prompt:
seeds = [int(s) for s in seed]
else:
raise ValueError(
f"Length of seed must be equal to number of prompt(batch_size) or "
f"batch_size * num_videos_per_prompt ({batch_size} * {num_videos_per_prompt}), got {seed}.")
else:
raise ValueError(f"Seed must be an integer, a list of integers, or None, got {seed}.")
# Peiyuan: using GPU seed will cause A100 and H100 to generate different results...
generator = [torch.Generator("cpu").manual_seed(seed) for seed in seeds]
out_dict["seeds"] = seeds
# ========================================================================
# Arguments: target_width, target_height, target_video_length
# ========================================================================
if width <= 0 or height <= 0 or video_length <= 0:
raise ValueError(
f"`height` and `width` and `video_length` must be positive integers, got height={height}, width={width}, video_length={video_length}"
)
if (video_length - 1) % 4 != 0:
raise ValueError(f"`video_length-1` must be a multiple of 4, got {video_length}")
logger.info(f"Input (height, width, video_length) = ({height}, {width}, {video_length})")
target_height = align_to(height, 16)
target_width = align_to(width, 16)
target_video_length = video_length
out_dict["size"] = (target_height, target_width, target_video_length)
# ========================================================================
# Arguments: prompt, new_prompt, negative_prompt
# ========================================================================
if not isinstance(prompt, str):
raise TypeError(f"`prompt` must be a string, but got {type(prompt)}")
prompt = [prompt.strip()]
# negative prompt
if negative_prompt is None or negative_prompt == "":
negative_prompt = self.default_negative_prompt
if not isinstance(negative_prompt, str):
raise TypeError(f"`negative_prompt` must be a string, but got {type(negative_prompt)}")
negative_prompt = [negative_prompt.strip()]
# ========================================================================
# Scheduler
# ========================================================================
scheduler = FlowMatchDiscreteScheduler(
shift=flow_shift,
reverse=self.args.flow_reverse,
solver=self.args.flow_solver,
)
self.pipeline.scheduler = scheduler
if "884" in self.args.vae:
latents_size = [(video_length - 1) // 4 + 1, height // 8, width // 8]
elif "888" in self.args.vae:
latents_size = [(video_length - 1) // 8 + 1, height // 8, width // 8]
n_tokens = latents_size[0] * latents_size[1] * latents_size[2]
# ========================================================================
# Print infer args
# ========================================================================
debug_str = f"""
height: {target_height}
width: {target_width}
video_length: {target_video_length}
prompt: {prompt}
neg_prompt: {negative_prompt}
seed: {seed}
infer_steps: {infer_steps}
num_videos_per_prompt: {num_videos_per_prompt}
guidance_scale: {guidance_scale}
n_tokens: {n_tokens}
flow_shift: {flow_shift}
embedded_guidance_scale: {embedded_guidance_scale}"""
logger.info(debug_str)
# return
# sp_group = get_sp_group()
# local_rank = sp_group.rank
import os
local_rank = os.environ.get("LOCAL_RANK", 0)
device = torch.device(f"cuda:{local_rank}")
batch = ForwardBatch(
prompt=prompt,
negative_prompt=negative_prompt,
num_videos_per_prompt=num_videos_per_prompt,
height=inference_args.height,
width=inference_args.width,
num_frames=inference_args.num_frames,
num_inference_steps=inference_args.num_inference_steps,
guidance_scale=inference_args.guidance_scale,
generator=generator,
eta=0.0,
n_tokens=n_tokens,
data_type="video" if inference_args.num_frames > 1 else "image",
device=device,
extra={}, # Any additional parameters
)
print('===============================================')
print(batch)
print('===============================================')
print('===============================================')
print('===============================================')
print(inference_args)
# ========================================================================
# Pipeline inference
# ========================================================================
start_time = time.time()
samples = self.pipeline.forward(
batch=batch,
inference_args=inference_args,
)[0]
out_dict["samples"] = samples
out_dict["prompts"] = prompt
gen_time = time.time() - start_time
logger.info(f"Success, time: {gen_time}")
return out_dict
-322
View File
@@ -1,322 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
from enum import IntEnum
from typing import List, Optional, Union
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import PretrainedConfig
from typing_extensions import assert_never
from vllm.config import PoolerConfig
from vllm.model_executor.pooling_metadata import (PoolingMetadata,
PoolingTensors)
from vllm.sequence import PoolerOutput, PoolingSequenceGroupOutput
from vllm.transformers_utils.config import (
get_cross_encoder_activation_function)
class PoolingType(IntEnum):
"""Enumeration for different types of pooling methods."""
LAST = 0
ALL = 1
CLS = 2
STEP = 3
MEAN = 4
class SimplePooler(nn.Module):
"""A layer that pools specific information from hidden states.
This layer does the following:
1. Extracts specific tokens or aggregates data based on pooling method.
2. Normalizes output if specified.
3. Returns structured results as `PoolerOutput`.
Attributes:
pooling_type: The type of pooling to use.
normalize: Whether to normalize the pooled data.
"""
@staticmethod
def from_pooling_type(
pooling_type: PoolingType,
*,
normalize: bool,
softmax: bool,
step_tag_id: Optional[int] = None,
returned_token_ids: Optional[List[int]] = None,
) -> "SimplePooler":
if pooling_type == PoolingType.LAST:
assert step_tag_id is None and returned_token_ids is None
return LastPool(normalize=normalize, softmax=softmax)
if pooling_type == PoolingType.ALL:
assert step_tag_id is None and returned_token_ids is None
return AllPool(normalize=normalize, softmax=softmax)
if pooling_type == PoolingType.CLS:
assert step_tag_id is None and returned_token_ids is None
return CLSPool(normalize=normalize, softmax=softmax)
if pooling_type == PoolingType.MEAN:
assert step_tag_id is None and returned_token_ids is None
return MeanPool(normalize=normalize, softmax=softmax)
if pooling_type == PoolingType.STEP:
return StepPool(normalize=normalize,
softmax=softmax,
step_tag_id=step_tag_id,
returned_token_ids=returned_token_ids)
assert_never(pooling_type)
def __init__(self, *, normalize: bool, softmax: bool) -> None:
super().__init__()
self.head = PoolerHead(normalize=normalize, softmax=softmax)
def get_prompt_lens(
self,
hidden_states: torch.Tensor,
pooling_metadata: PoolingMetadata,
) -> torch.Tensor:
return PoolingTensors.from_pooling_metadata(
pooling_metadata, hidden_states.device).prompt_lens
def extract_states(
self,
hidden_states: torch.Tensor,
pooling_metadata: PoolingMetadata,
) -> Union[list[torch.Tensor], torch.Tensor]:
raise NotImplementedError
def build_output(self, data: torch.Tensor) -> PoolingSequenceGroupOutput:
return PoolingSequenceGroupOutput(data)
def forward(
self,
hidden_states: torch.Tensor,
pooling_metadata: PoolingMetadata,
) -> PoolerOutput:
pooled_data = self.extract_states(hidden_states, pooling_metadata)
pooled_data = self.head(pooled_data)
pooled_outputs = [self.build_output(data) for data in pooled_data]
return PoolerOutput(outputs=pooled_outputs)
class CLSPool(SimplePooler):
def extract_states(
self,
hidden_states: torch.Tensor,
pooling_metadata: PoolingMetadata,
) -> Union[list[torch.Tensor], torch.Tensor]:
prompt_lens = self.get_prompt_lens(hidden_states, pooling_metadata)
first_token_flat_indices = torch.zeros_like(prompt_lens)
first_token_flat_indices[1:] += torch.cumsum(prompt_lens, dim=0)[:-1]
return hidden_states[first_token_flat_indices]
class LastPool(SimplePooler):
def extract_states(
self,
hidden_states: torch.Tensor,
pooling_metadata: PoolingMetadata,
) -> Union[list[torch.Tensor], torch.Tensor]:
prompt_lens = self.get_prompt_lens(hidden_states, pooling_metadata)
last_token_flat_indices = torch.cumsum(prompt_lens, dim=0) - 1
return hidden_states[last_token_flat_indices]
class AllPool(SimplePooler):
def extract_states(
self,
hidden_states: torch.Tensor,
pooling_metadata: PoolingMetadata,
) -> Union[list[torch.Tensor], torch.Tensor]:
prompt_lens = self.get_prompt_lens(hidden_states, pooling_metadata)
offset = 0
pooled_data = list[torch.Tensor]()
for prompt_len in prompt_lens:
pooled_data.append(hidden_states[offset:offset + prompt_len])
offset += prompt_len
return pooled_data
class MeanPool(SimplePooler):
def extract_states(
self,
hidden_states: torch.Tensor,
pooling_metadata: PoolingMetadata,
) -> Union[list[torch.Tensor], torch.Tensor]:
prompt_lens = self.get_prompt_lens(hidden_states, pooling_metadata)
cumsum = torch.cumsum(hidden_states, dim=0)
start_indices = torch.cat([
torch.tensor([0], device=hidden_states.device),
torch.cumsum(prompt_lens[:-1], dim=0)
])
end_indices = torch.cumsum(prompt_lens, dim=0)
return (cumsum[end_indices - 1] - cumsum[start_indices] +
hidden_states[start_indices]) / prompt_lens.unsqueeze(1)
class StepPool(SimplePooler):
def __init__(
self,
*,
normalize: bool,
softmax: bool,
step_tag_id: Optional[int] = None,
returned_token_ids: Optional[List[int]] = None,
):
super().__init__(normalize=normalize, softmax=softmax)
self.step_tag_id = step_tag_id
self.returned_token_ids = returned_token_ids
def extract_states(
self,
hidden_states: torch.Tensor,
pooling_metadata: PoolingMetadata,
) -> Union[list[torch.Tensor], torch.Tensor]:
prompt_lens = self.get_prompt_lens(hidden_states, pooling_metadata)
returned_token_ids = self.returned_token_ids
if returned_token_ids is not None and len(returned_token_ids) > 0:
hidden_states = hidden_states[:, returned_token_ids]
step_tag_id = self.step_tag_id
offset = 0
pooled_data = list[torch.Tensor]()
for prompt_len, seq_data_i in zip(prompt_lens,
pooling_metadata.seq_data.values()):
pooled_data_i = hidden_states[offset:offset + prompt_len]
if step_tag_id is not None:
token_ids = torch.tensor(seq_data_i.prompt_token_ids)
pooled_data_i = pooled_data_i[token_ids == step_tag_id]
offset += prompt_len
pooled_data.append(pooled_data_i)
return pooled_data
class PoolerHead(nn.Module):
def __init__(self, *, normalize: bool, softmax: bool) -> None:
super().__init__()
self.normalize = normalize
self.softmax = softmax
def forward(self, pooled_data: Union[list[torch.Tensor], torch.Tensor]):
if self.normalize:
if isinstance(pooled_data, list):
pooled_data = [
F.normalize(data, p=2, dim=1) for data in pooled_data
]
else:
pooled_data = F.normalize(pooled_data, p=2, dim=1)
if self.softmax:
if isinstance(pooled_data, list):
pooled_data = [F.softmax(data, dim=-1) for data in pooled_data]
else:
pooled_data = F.softmax(pooled_data, dim=-1)
return pooled_data
class Pooler(nn.Module):
@classmethod
def from_config_with_defaults(
cls,
pooler_config: PoolerConfig,
pooling_type: PoolingType,
normalize: bool,
softmax: bool,
step_tag_id: Optional[int] = None,
returned_token_ids: Optional[List[int]] = None,
) -> SimplePooler:
return SimplePooler.from_pooling_type(
pooling_type=PoolingType[pooler_config.pooling_type]
if pooler_config.pooling_type is not None else pooling_type,
normalize=pooler_config.normalize
if pooler_config.normalize is not None else normalize,
softmax=pooler_config.softmax
if pooler_config.softmax is not None else softmax,
step_tag_id=pooler_config.step_tag_id
if pooler_config.step_tag_id is not None else step_tag_id,
returned_token_ids=pooler_config.returned_token_ids
if pooler_config.returned_token_ids is not None else
returned_token_ids,
)
class CrossEncodingPooler(nn.Module):
"""A layer that pools specific information from hidden states.
This layer does the following:
1. Extracts specific tokens or aggregates data based on pooling method.
2. Normalizes output if specified.
3. Returns structured results as `PoolerOutput`.
Attributes:
pooling_type: The type of pooling to use.
normalize: Whether to normalize the pooled data.
"""
def __init__(
self,
config: PretrainedConfig,
classifier: nn.Module,
pooler: Optional[nn.Module] = None,
):
super().__init__()
self.classifier = classifier
self.pooler = pooler
self.default_activation_function = \
get_cross_encoder_activation_function(config)
def forward(
self,
hidden_states: torch.Tensor,
pooling_metadata: PoolingMetadata,
) -> PoolerOutput:
"""Pools sentence pair scores from the hidden_states."""
prompt_lens = PoolingTensors.from_pooling_metadata(
pooling_metadata, hidden_states.device).prompt_lens
offset = 0
pooled_data_lst = []
for prompt_len in prompt_lens:
pooled_data_i = hidden_states[offset:offset + prompt_len]
if self.pooler is not None:
final_shape_tensor = self.pooler(pooled_data_i)
else:
final_shape_tensor = self.classifier(pooled_data_i)
pooled_data_lst.append(final_shape_tensor)
offset += prompt_len
pooled_output = torch.stack(pooled_data_lst)
if self.pooler is not None:
# apply classifier once on the full batch if possible
pooled_output = self.classifier(pooled_output)
scores = self.default_activation_function(pooled_output).squeeze(-1)
pooled_outputs = [PoolingSequenceGroupOutput(data) for data in scores]
return PoolerOutput(outputs=pooled_outputs)
-269
View File
@@ -1,269 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# Adapted from
# https://github.com/huggingface/transformers/blob/v4.28.0/src/transformers/models/llama/modeling_llama.py
# https://huggingface.co/Qwen/Qwen-7B/blob/main/modeling_qwen.py
# https://github.com/facebookresearch/mae/blob/efb2a8062c206524e35e47d04501ed4f544c0ae8/util/pos_embed.py#L20
#
# Copyright 2023 The Qwen team.
# Copyright 2023 The vLLM team.
# Copyright 2022 EleutherAI and the HuggingFace Inc. team. All rights reserved.
#
# This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX
# and OPT implementations in this library. It has been modified from its
# original forms to accommodate minor architectural differences compared
# to GPT-NeoX and OPT used by the Meta AI team that trained the model.
#
# 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.
"""
Shared resampler perceiver network used in multimodal models and
related helpers for sincos positional embeddings.
Example models: Qwen (Qwen-VL), MiniCPM-V 2.0
"""
import math
from functools import partial
from typing import Callable, Optional, Tuple, Union
import numpy as np
import torch
import torch.nn.functional as F
from torch import nn
from vllm.model_executor.layers.linear import ReplicatedLinear
from vllm.model_executor.layers.quantization import QuantizationConfig
DEFAULT_LN = partial(nn.LayerNorm, eps=1e-6)
def get_abs_pos(abs_pos: torch.Tensor, tgt_size: Union[torch.Tensor,
int]) -> torch.Tensor:
# abs_pos: L, C
# tgt_size: (H, W)
# return: M, C
src_size = int(math.sqrt(abs_pos.size(0)))
dtype = abs_pos.dtype
if isinstance(tgt_size, int):
tgt_size = (tgt_size, tgt_size)
if (src_size == tgt_size[0] and src_size == tgt_size[1]):
return abs_pos
return (F.interpolate(
abs_pos.float().reshape(1, src_size, src_size, -1).permute(0, 3, 1, 2),
size=(tgt_size[0], tgt_size[1]),
mode="bicubic",
align_corners=False,
).permute(0, 2, 3, 1).flatten(0, 2).to(dtype=dtype))
# sin/cos positional embedding helpers are adapted from:
# https://github.com/facebookresearch/mae/blob/efb2a8062c206524e35e47d04501ed4f544c0ae8/util/pos_embed.py#L20
def get_1d_sincos_pos_embed_from_grid(
embed_dim: int, pos: np.ndarray,
version: Tuple[int, int] = (2, 0)) -> torch.Tensor:
"""
embed_dim: output dimension for each position
pos: a list of positions to be encoded: size (M,) / (H, W)
out: (M, D) / (H, W, D)
"""
assert embed_dim % 2 == 0
omega = np.arange(embed_dim // 2, dtype=np.float32)
omega /= embed_dim / 2.0
omega = 1.0 / 10000**omega # (D/2,)
if version == (2, 0):
pos = pos.reshape(-1) # (M,)
out = np.einsum("m,d->md", pos, omega) # (M, D/2), outer product
emb_sin = np.sin(out) # (M, D/2)
emb_cos = np.cos(out) # (M, D/2)
emb = np.concatenate([emb_sin, emb_cos], axis=1) # (M, D)
else:
out = np.einsum("hw,d->hwd", pos, omega) # (H, W, D/2), outer product
emb_sin = np.sin(out) # (H, W, D/2)
emb_cos = np.cos(out) # (H, W, D/2)
emb = np.concatenate([emb_sin, emb_cos], axis=-1) # (H, W, D)
return emb
def get_2d_sincos_pos_embed_from_grid(
embed_dim: int, grid: np.ndarray,
version: Tuple[int, int] = (2, 0)) -> torch.Tensor:
assert embed_dim % 2 == 0
# use half of dimensions to encode grid_h
emb_h = get_1d_sincos_pos_embed_from_grid(
embed_dim // 2, grid[0], version) # (H*W, D/2) or (H, W, D/2)
emb_w = get_1d_sincos_pos_embed_from_grid(
embed_dim // 2, grid[1], version) # (H*W, D/2) or (H, W, D/2)
if version == (2, 0):
emb = np.concatenate([emb_h, emb_w], axis=1) # (H*W, D)
else:
emb = np.concatenate([emb_h, emb_w], axis=-1) # (H, W, D)
return emb
def get_2d_sincos_pos_embed(
embed_dim: int,
grid_size: Union[int, Tuple[int, int]],
cls_token: bool = False,
version: Tuple[int, int] = (2, 0),
) -> torch.Tensor:
"""
grid_size: int of the grid height and width
return:
pos_embed: [grid_size*grid_size, embed_dim] or
[1+grid_size*grid_size, embed_dim] (w/ or w/o cls_token)
"""
if isinstance(grid_size, int):
grid_h_size, grid_w_size = grid_size, grid_size
else:
grid_h_size, grid_w_size = grid_size[0], grid_size[1]
grid_h = np.arange(grid_h_size, dtype=np.float32)
grid_w = np.arange(grid_w_size, dtype=np.float32)
grid = np.meshgrid(grid_w, grid_h) # here w goes first
grid = np.stack(grid, axis=0)
assert isinstance(grid, np.ndarray) and \
grid.shape == (2, grid_h_size, grid_w_size)
if version == (2, 0):
grid = grid.reshape([2, 1, grid_h_size, grid_w_size])
pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid, version)
if cls_token:
pos_embed = np.concatenate([np.zeros([1, embed_dim]), pos_embed],
axis=0)
else:
pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid, version)
return pos_embed
class BaseResampler(nn.Module):
"""
A 2D perceiver-resampler network with one cross attention layers by
(grid_size**2) learnable queries and 2d sincos pos_emb.
Outputs:
A tensor with the shape of (grid_size**2, embed_dim)
"""
def __init__(self,
num_queries: int,
embed_dim: int,
num_heads: int,
kv_dim: Optional[int] = None,
norm_layer: Callable[[int], nn.LayerNorm] = DEFAULT_LN,
do_post_projection: bool = True,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "") -> None:
super().__init__()
self.num_queries = num_queries
self.embed_dim = embed_dim
self.num_heads = num_heads
self.query = nn.Parameter(torch.empty(self.num_queries, embed_dim))
if kv_dim is not None and kv_dim != embed_dim:
self.kv_proj = ReplicatedLinear(kv_dim,
embed_dim,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.kv_proj")
else:
# Maintain the same return value with ReplicatedLinear.forward
self.kv_proj = lambda *args, **kwargs: ( # type: ignore # noqa
nn.Identity()(*args, **kwargs),
None,
)
self.attn = nn.MultiheadAttention(embed_dim, num_heads)
self.ln_q = norm_layer(embed_dim)
self.ln_kv = norm_layer(embed_dim)
self.do_post_projection = do_post_projection
self.ln_post = norm_layer(embed_dim) if do_post_projection else None
self.proj = nn.Parameter(
(embed_dim**-0.5) *
torch.empty(embed_dim, embed_dim)) if do_post_projection else None
def _repeat(self, query, N: int):
return query.unsqueeze(1).repeat(1, N, 1)
class Resampler2(BaseResampler):
"""Resampler-perceiver network to be used for a variety of model types,
e.g., Qwen-vl / Minicpmv 2.0. The main difference is the addition of the
do_post_projection arg, which indicates whether or not there should be
a post layer normalization and projector after the attention. This is
present in minicpmv2.0, but not qwen-vl.
"""
def __init__(self,
grid_size: int,
embed_dim: int,
num_heads: int,
kv_dim: Optional[int] = None,
norm_layer: Callable[[int], nn.LayerNorm] = DEFAULT_LN,
adaptive: bool = False,
do_post_projection: bool = True,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "") -> None:
super().__init__(grid_size**2,
embed_dim,
num_heads,
kv_dim,
norm_layer,
do_post_projection=do_post_projection,
quant_config=quant_config,
prefix=prefix)
self.adaptive = adaptive
pos_embed_arr = get_2d_sincos_pos_embed(embed_dim,
grid_size,
version=(2, 0))
self.pos_embed = nn.Parameter(
torch.from_numpy(pos_embed_arr).requires_grad_(False))
def forward(
self,
x: torch.Tensor,
tgt_sizes: Optional[torch.Tensor] = None,
attn_mask: Optional[torch.Tensor] = None,
) -> torch.Tensor:
if tgt_sizes is None:
tgt_sizes = int(math.sqrt(x.size(1)))
if self.adaptive:
pos_embed_arr = get_2d_sincos_pos_embed(self.embed_dim,
tgt_sizes,
version=(2, 0))
pos_embed = torch.from_numpy(pos_embed_arr).to(device=x.device,
dtype=x.dtype)
else:
pos_embed = get_abs_pos(self.pos_embed,
tgt_sizes).to(device=x.device,
dtype=x.dtype)
x, _ = self.kv_proj(x)
x = self.ln_kv(x).permute(1, 0, 2)
N = x.shape[1]
q = self.ln_q(self.query)
out = self.attn(
self._repeat(q, N) + self.pos_embed.unsqueeze(1),
x + pos_embed.unsqueeze(1),
x,
attn_mask=attn_mask,
)[0]
x = out.permute(1, 0, 2)
if self.do_post_projection:
x = self.ln_post(x)
x = x @ self.proj
return x
-452
View File
@@ -1,452 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# Adapted from vllm
# Copyright 2023 The vLLM Authors.
# Copyright 2025 The FastVideo Authors.
# ruff: noqa: SIM117
import collections
import copy
import dataclasses
import fnmatch
import glob
import inspect
import itertools
import math
import os
import time
import warnings
from abc import ABC, abstractmethod
from contextlib import contextmanager
from typing import (Any, Callable, Dict, Generator, Iterable, List, Optional,
Tuple, cast)
import gguf
import huggingface_hub
import numpy as np
import torch
from huggingface_hub import HfApi
from torch import nn
from transformers import AutoModelForCausalLM
from transformers.utils import SAFE_WEIGHTS_INDEX_NAME
from vllm.attention import Attention
from vllm.config import (LoadConfig, LoadFormat, ModelConfig, ParallelConfig,
VllmConfig, set_current_vllm_config)
from vllm.distributed import (get_tensor_model_parallel_rank,
get_tensor_model_parallel_world_size)
from vllm.envs import VLLM_USE_MODELSCOPE
from vllm.logger import init_logger
from vllm.model_executor.layers.linear import (LinearBase,
MergedColumnParallelLinear,
QKVParallelLinear,
ReplicatedLinear,
RowParallelLinear)
from vllm.model_executor.layers.quantization.base_config import (
QuantizeMethodBase)
from vllm.model_executor.model_loader.tensorizer import (
TensorizerConfig, is_vllm_tensorized, load_with_tensorizer,
serialize_vllm_model, tensorizer_weights_iterator)
from vllm.model_executor.model_loader.utils import (ParamMapping,
configure_quant_config,
get_model_architecture,
set_default_torch_dtype)
from vllm.model_executor.model_loader.weight_utils import (
download_safetensors_index_file_from_hf, download_weights_from_hf,
filter_duplicate_safetensors_files, filter_files_not_needed_for_inference,
get_gguf_extra_tensor_names, get_lock, gguf_quant_weights_iterator,
initialize_dummy_weights, np_cache_weights_iterator, pt_weights_iterator,
runai_safetensors_weights_iterator, safetensors_weights_iterator)
from vllm.model_executor.utils import set_weight_attrs
from vllm.platforms import current_platform
from vllm.transformers_utils.s3_utils import glob as s3_glob
from vllm.transformers_utils.utils import is_s3
from vllm.utils import is_pin_memory_available
@contextmanager
def device_loading_context(module: torch.nn.Module,
target_device: torch.device):
if target_device.type == "cpu":
# If target is CPU, no need to move anything
yield module
return
original_device_states: Dict[str, torch.device] = {}
# Store original device states and move parameters to GPU if they're on CPU
for name, p in module.named_parameters():
if p.device.type == "cpu":
original_device_states[name] = p.device
p.data = p.data.to(target_device)
# Parameters already on target device are not touched
try:
yield module
finally:
# Restore parameters to their original devices, ignoring new parameters
pin_memory = is_pin_memory_available()
for name, p in module.named_parameters():
if name in original_device_states:
original_device: torch.device = original_device_states[name]
if original_device.type == "cpu":
# `torch.empty_like` does not support `pin_memory` argument
cpu_data = torch.empty_strided(
size=p.data.size(),
stride=p.data.stride(),
dtype=p.data.dtype,
layout=p.data.layout,
device="cpu",
pin_memory=pin_memory,
)
cpu_data.copy_(p.data)
p.data = cpu_data
else:
p.data = p.data.to(original_device)
# New parameters or parameters already on target device are untouched
logger = init_logger(__name__)
def _initialize_model(
vllm_config: VllmConfig,
*,
prefix: str = "",
) -> nn.Module:
"""Initialize a model with the given configurations."""
model_config = vllm_config.model_config
model_class, _ = get_model_architecture(model_config)
if vllm_config.quant_config is not None:
configure_quant_config(vllm_config.quant_config, model_class)
signatures = inspect.signature(model_class.__init__)
all_params = [param.name for param in signatures.parameters.values()]
if "vllm_config" in all_params and "prefix" in all_params:
# new-style model class
with set_current_vllm_config(vllm_config, check_compile=True):
return model_class(vllm_config=vllm_config, prefix=prefix)
msg = ("vLLM model class should accept `vllm_config` and `prefix` as "
"input arguments. Possibly you have an old-style model class"
" registered from out of tree and it is used for new vLLM version. "
"Check https://docs.vllm.ai/en/latest/design/arch_overview.html "
"for the design and update the model class accordingly.")
warnings.warn(msg, DeprecationWarning, stacklevel=2)
logger.warning(
"Trying to guess the arguments for old-style model class %s",
model_class,
)
# try to be compatible with old-style model class
kwargs = {}
if "prefix" in all_params:
kwargs["prefix"] = prefix
if "config" in all_params:
kwargs["config"] = model_config.hf_config
if "cache_config" in all_params:
kwargs["cache_config"] = vllm_config.cache_config
if "quant_config" in all_params:
kwargs["quant_config"] = vllm_config.quant_config
if "lora_config" in all_params:
kwargs["lora_config"] = vllm_config.lora_config
if "scheduler_config" in all_params:
kwargs["scheduler_config"] = vllm_config.scheduler_config
with set_current_vllm_config(vllm_config, check_compile=True):
return model_class(**kwargs)
def _process_weights_after_loading(model: nn.Module, model_config: ModelConfig,
target_device: torch.device) -> None:
for _, module in model.named_modules():
quant_method = getattr(module, "quant_method", None)
if isinstance(quant_method, QuantizeMethodBase):
# When quant methods need to process weights after loading
# (for repacking, quantizing, etc), they expect parameters
# to be on the global target device. This scope is for the
# case where cpu offloading is used, where we will move the
# parameters onto device for processing and back off after.
with device_loading_context(module, target_device):
quant_method.process_weights_after_loading(module)
# Currently only used by MLA.
# NOTE: This intentionally happens after other modules so we can easily
# decompress the weights for MLA.
for _, module in model.named_modules():
if isinstance(module, Attention) and \
hasattr(module, "process_weights_after_loading"):
# TODO(lucas): see if there is a way to unify the signatures
# of process_weights_after_loading
module.process_weights_after_loading(model_config.dtype)
class BaseModelLoader(ABC):
"""Base class for model loaders."""
def __init__(self, load_config: LoadConfig):
self.load_config = load_config
@abstractmethod
def download_model(self, model_config: ModelConfig) -> None:
"""Download a model so that it can be immediately loaded."""
raise NotImplementedError
@abstractmethod
def load_model(self, *, vllm_config: VllmConfig) -> nn.Module:
"""Load a model with the given configurations."""
raise NotImplementedError
class DefaultModelLoader(BaseModelLoader):
"""Model loader that can load different file types from disk."""
@dataclasses.dataclass
class Source:
"""A source for weights."""
model_or_path: str
"""The model ID or path."""
revision: Optional[str]
"""The optional model revision."""
prefix: str = ""
"""A prefix to prepend to all weights."""
fall_back_to_pt: bool = True
"""Whether .pt weights can be used."""
allow_patterns_overrides: Optional[list[str]] = None
"""If defined, weights will load exclusively using these patterns."""
counter_before_loading_weights: float = 0.0
counter_after_loading_weights: float = 0.0
def __init__(self, load_config: LoadConfig):
super().__init__(load_config)
if load_config.model_loader_extra_config:
raise ValueError(f"Model loader extra config is not supported for "
f"load format {load_config.load_format}")
def _maybe_download_from_modelscope(
self, model: str, revision: Optional[str]) -> Optional[str]:
"""Download model from ModelScope hub if VLLM_USE_MODELSCOPE is True.
Returns the path to the downloaded model, or None if the model is not
downloaded from ModelScope."""
if VLLM_USE_MODELSCOPE:
# download model from ModelScope hub,
# lazy import so that modelscope is not required for normal use.
# pylint: disable=C.
from modelscope.hub.snapshot_download import snapshot_download
if not os.path.exists(model):
# Use file lock to prevent multiple processes from
# downloading the same model weights at the same time.
with get_lock(model, self.load_config.download_dir):
model_path = snapshot_download(
model_id=model,
cache_dir=self.load_config.download_dir,
local_files_only=huggingface_hub.constants.
HF_HUB_OFFLINE,
revision=revision,
ignore_file_pattern=self.load_config.ignore_patterns,
)
else:
model_path = model
return model_path
return None
def _prepare_weights(
self,
model_name_or_path: str,
revision: Optional[str],
fall_back_to_pt: bool,
allow_patterns_overrides: Optional[list[str]],
) -> Tuple[str, List[str], bool]:
"""Prepare weights for the model.
If the model is not local, it will be downloaded."""
model_name_or_path = (self._maybe_download_from_modelscope(
model_name_or_path, revision) or model_name_or_path)
is_local = os.path.isdir(model_name_or_path)
load_format = self.load_config.load_format
use_safetensors = False
index_file = SAFE_WEIGHTS_INDEX_NAME
# Some quantized models use .pt files for storing the weights.
if load_format == LoadFormat.AUTO:
allow_patterns = ["*.safetensors", "*.bin"]
elif load_format == LoadFormat.SAFETENSORS:
use_safetensors = True
allow_patterns = ["*.safetensors"]
elif load_format == LoadFormat.MISTRAL:
use_safetensors = True
allow_patterns = ["consolidated*.safetensors"]
index_file = "consolidated.safetensors.index.json"
elif load_format == LoadFormat.PT:
allow_patterns = ["*.pt"]
elif load_format == LoadFormat.NPCACHE:
allow_patterns = ["*.bin"]
else:
raise ValueError(f"Unknown load_format: {load_format}")
if fall_back_to_pt:
allow_patterns += ["*.pt"]
if allow_patterns_overrides is not None:
allow_patterns = allow_patterns_overrides
if not is_local:
hf_folder = download_weights_from_hf(
model_name_or_path,
self.load_config.download_dir,
allow_patterns,
revision,
ignore_patterns=self.load_config.ignore_patterns,
)
else:
hf_folder = model_name_or_path
hf_weights_files: List[str] = []
for pattern in allow_patterns:
hf_weights_files += glob.glob(os.path.join(hf_folder, pattern))
if len(hf_weights_files) > 0:
if pattern == "*.safetensors":
use_safetensors = True
break
if use_safetensors:
# For models like Mistral-7B-Instruct-v0.3
# there are both sharded safetensors files and a consolidated
# safetensors file. Using both breaks.
# Here, we download the `model.safetensors.index.json` and filter
# any files not found in the index.
if not is_local:
download_safetensors_index_file_from_hf(
model_name_or_path,
index_file,
self.load_config.download_dir,
revision,
)
hf_weights_files = filter_duplicate_safetensors_files(
hf_weights_files, hf_folder, index_file)
else:
hf_weights_files = filter_files_not_needed_for_inference(
hf_weights_files)
if len(hf_weights_files) == 0:
raise RuntimeError(
f"Cannot find any model weights with `{model_name_or_path}`")
return hf_folder, hf_weights_files, use_safetensors
def _get_weights_iterator(
self, source: "Source"
) -> Generator[Tuple[str, torch.Tensor], None, None]:
"""Get an iterator for the model weights based on the load format."""
hf_folder, hf_weights_files, use_safetensors = self._prepare_weights(
source.model_or_path, source.revision, source.fall_back_to_pt,
source.allow_patterns_overrides)
if self.load_config.load_format == LoadFormat.NPCACHE:
# Currently np_cache only support *.bin checkpoints
assert use_safetensors is False
weights_iterator = np_cache_weights_iterator(
source.model_or_path,
self.load_config.download_dir,
hf_folder,
hf_weights_files,
)
elif use_safetensors:
weights_iterator = safetensors_weights_iterator(hf_weights_files)
else:
weights_iterator = pt_weights_iterator(hf_weights_files)
if current_platform.is_tpu():
# In PyTorch XLA, we should call `xm.mark_step` frequently so that
# not too many ops are accumulated in the XLA program.
import torch_xla.core.xla_model as xm
def _xla_weights_iterator(iterator: Generator):
for weights in iterator:
yield weights
xm.mark_step()
weights_iterator = _xla_weights_iterator(weights_iterator)
if self.counter_before_loading_weights == 0.0:
self.counter_before_loading_weights = time.perf_counter()
# Apply the prefix.
return ((source.prefix + name, tensor)
for (name, tensor) in weights_iterator)
def _get_all_weights(
self,
model_config: ModelConfig,
model: nn.Module,
) -> Generator[Tuple[str, torch.Tensor], None, None]:
primary_weights = DefaultModelLoader.Source(
model_config.model,
model_config.revision,
prefix="",
fall_back_to_pt=getattr(model, "fall_back_to_pt_during_load",
True),
allow_patterns_overrides=getattr(model, "allow_patterns_overrides",
None),
)
yield from self._get_weights_iterator(primary_weights)
secondary_weights = cast(
Iterable[DefaultModelLoader.Source],
getattr(model, "secondary_weights", ()),
)
for source in secondary_weights:
yield from self._get_weights_iterator(source)
def download_model(self, model_config: ModelConfig) -> None:
self._prepare_weights(model_config.model,
model_config.revision,
fall_back_to_pt=True,
allow_patterns_overrides=None)
def load_model(self, vllm_config: VllmConfig) -> nn.Module:
device_config = vllm_config.device_config
model_config = vllm_config.model_config
target_device = torch.device(device_config.device)
with set_default_torch_dtype(model_config.dtype):
with target_device:
model = _initialize_model(vllm_config=vllm_config)
weights_to_load = {name for name, _ in model.named_parameters()}
loaded_weights = model.load_weights(
self._get_all_weights(model_config, model))
self.counter_after_loading_weights = time.perf_counter()
logger.info(
"Loading weights took %.2f seconds",
self.counter_after_loading_weights -
self.counter_before_loading_weights)
# We only enable strict check for non-quantized models
# that have loaded weights tracking currently.
if model_config.quantization is None and loaded_weights is not None:
weights_not_loaded = weights_to_load - loaded_weights
if weights_not_loaded:
raise ValueError(
"Following weights were not initialized from "
f"checkpoint: {weights_not_loaded}")
_process_weights_after_loading(model, model_config, target_device)
return model.eval()
def get_model_loader(load_config: LoadConfig) -> BaseModelLoader:
"""Get a model loader based on the load format."""
if isinstance(load_config.load_format, type):
return load_config.load_format(load_config)
return DefaultModelLoader(load_config)
-273
View File
@@ -1,273 +0,0 @@
"""
Model loader for diffusion models.
This module provides a base class and implementations for loading model components
needed by diffusion pipelines.
"""
from abc import ABC, abstractmethod
from typing import Any, Dict, Optional, Type, Union
import os
import torch
import traceback
from fastvideo.inference_args import InferenceArgs
from fastvideo.logger import init_logger
logger = init_logger(__name__)
class ModelLoader(ABC):
"""
Abstract base class for model loaders.
A model loader is responsible for loading the components needed by a diffusion
pipeline, such as the UNet, VAE, text encoder, etc.
"""
@abstractmethod
def load_components(
self,
inference_args: InferenceArgs,
) -> Dict[str, Any]:
"""
Load the components needed by a diffusion pipeline.
Args:
inference_args: The inference arguments.
Returns:
A dictionary of component names to component objects.
"""
pass
class HunYuanModelLoader(ModelLoader):
"""
Model loader for HunYuan models.
This loader loads the components needed by HunYuan diffusion pipelines.
"""
def load_components(
self,
inference_args: InferenceArgs,
) -> Dict[str, Any]:
"""
Load the components needed by a HunYuan diffusion pipeline.
Args:
inference_args: The inference arguments.
Returns:
A dictionary of component names to component objects.
Raises:
FileNotFoundError: If a required model file is not found.
RuntimeError: If there is an error loading a model component.
"""
try:
from fastvideo.models.unet import UNet3DConditionModel
from fastvideo.models.vae import AutoencoderKLCausal3D
from fastvideo.models.text_encoder import TextEncoder
from fastvideo.models.tokenizer import Tokenizer
from fastvideo.models.scheduler import DDIMScheduler
logger.info(f"Loading model components from {inference_args.model_path}")
# Check if model path exists
if not os.path.exists(inference_args.model_path):
raise FileNotFoundError(f"Model path not found: {inference_args.model_path}")
# Check if DiT weight exists
if inference_args.dit_weight and not os.path.exists(inference_args.dit_weight):
raise FileNotFoundError(f"DiT weight not found: {inference_args.dit_weight}")
# Load UNet
try:
logger.info("Loading UNet...")
unet = UNet3DConditionModel.from_pretrained(
inference_args.dit_weight,
subfolder="",
load_key=inference_args.load_key,
)
logger.info("UNet loaded successfully")
except Exception as e:
logger.error(f"Error loading UNet: {e}")
logger.error(traceback.format_exc())
raise RuntimeError(f"Failed to load UNet: {e}")
# Load VAE
try:
logger.info("Loading VAE...")
vae = AutoencoderKLCausal3D.from_pretrained(
inference_args.model_path,
subfolder="vae",
use_safetensors=True,
)
logger.info("VAE loaded successfully")
except Exception as e:
logger.error(f"Error loading VAE: {e}")
logger.error(traceback.format_exc())
raise RuntimeError(f"Failed to load VAE: {e}")
# Configure VAE for sequence parallelism if needed
if inference_args.vae_sp:
try:
logger.info("Configuring VAE for sequence parallelism")
vae.enable_sequence_parallel()
logger.info("VAE configured for sequence parallelism")
except Exception as e:
logger.error(f"Error configuring VAE for sequence parallelism: {e}")
logger.error(traceback.format_exc())
logger.warning("Continuing without sequence parallelism for VAE")
# Load text encoders
try:
logger.info("Loading primary text encoder...")
text_encoder = TextEncoder.from_pretrained(
inference_args.model_path,
subfolder="text_encoder",
use_safetensors=True,
)
logger.info("Primary text encoder loaded successfully")
except Exception as e:
logger.error(f"Error loading primary text encoder: {e}")
logger.error(traceback.format_exc())
raise RuntimeError(f"Failed to load primary text encoder: {e}")
try:
logger.info("Loading secondary text encoder...")
text_encoder_2 = TextEncoder.from_pretrained(
inference_args.model_path,
subfolder="text_encoder_2",
use_safetensors=True,
)
logger.info("Secondary text encoder loaded successfully")
except Exception as e:
logger.error(f"Error loading secondary text encoder: {e}")
logger.error(traceback.format_exc())
raise RuntimeError(f"Failed to load secondary text encoder: {e}")
# Load tokenizers
try:
logger.info("Loading primary tokenizer...")
tokenizer = Tokenizer.from_pretrained(
inference_args.model_path,
subfolder="tokenizer",
)
logger.info("Primary tokenizer loaded successfully")
except Exception as e:
logger.error(f"Error loading primary tokenizer: {e}")
logger.error(traceback.format_exc())
raise RuntimeError(f"Failed to load primary tokenizer: {e}")
try:
logger.info("Loading secondary tokenizer...")
tokenizer_2 = Tokenizer.from_pretrained(
inference_args.model_path,
subfolder="tokenizer_2",
)
logger.info("Secondary tokenizer loaded successfully")
except Exception as e:
logger.error(f"Error loading secondary tokenizer: {e}")
logger.error(traceback.format_exc())
raise RuntimeError(f"Failed to load secondary tokenizer: {e}")
# Load scheduler
try:
logger.info("Loading scheduler...")
scheduler = DDIMScheduler.from_pretrained(
inference_args.model_path,
subfolder="scheduler",
)
logger.info("Scheduler loaded successfully")
except Exception as e:
logger.error(f"Error loading scheduler: {e}")
logger.error(traceback.format_exc())
raise RuntimeError(f"Failed to load scheduler: {e}")
# Move models to the appropriate device
device = torch.device(f"cuda:{os.environ.get('LOCAL_RANK', '0')}")
logger.info(f"Moving models to device: {device}")
try:
unet.to(device)
vae.to(device)
text_encoder.to(device)
text_encoder_2.to(device)
logger.info("Models moved to device successfully")
except Exception as e:
logger.error(f"Error moving models to device: {e}")
logger.error(traceback.format_exc())
raise RuntimeError(f"Failed to move models to device: {e}")
# Set precision
try:
logger.info(f"Setting UNet precision to {inference_args.precision}")
if inference_args.precision == "bf16":
unet = unet.to(torch.bfloat16)
elif inference_args.precision == "fp16":
unet = unet.to(torch.float16)
logger.info(f"Setting VAE precision to {inference_args.vae_precision}")
if inference_args.vae_precision == "fp16":
vae = vae.to(torch.float16)
logger.info(f"Setting text encoder precision to {inference_args.text_encoder_precision}")
if inference_args.text_encoder_precision == "fp16":
text_encoder = text_encoder.to(torch.float16)
logger.info(f"Setting secondary text encoder precision to {inference_args.text_encoder_precision_2}")
if inference_args.text_encoder_precision_2 == "fp16":
text_encoder_2 = text_encoder_2.to(torch.float16)
logger.info("Precision set successfully")
except Exception as e:
logger.error(f"Error setting precision: {e}")
logger.error(traceback.format_exc())
logger.warning("Continuing with default precision")
# Return the components
return {
"unet": unet,
"vae": vae,
"text_encoder": text_encoder,
"text_encoder_2": text_encoder_2,
"tokenizer": tokenizer,
"tokenizer_2": tokenizer_2,
"scheduler": scheduler,
}
except Exception as e:
logger.error(f"Unhandled error in load_components: {e}")
logger.error(traceback.format_exc())
raise
# Factory function to get the appropriate model loader
def get_model_loader(model_type: str) -> Type[ModelLoader]:
"""
Get the appropriate model loader for the given model type.
Args:
model_type: The type of model to load.
Returns:
The model loader class.
Raises:
ValueError: If the model type is not recognized.
"""
model_loaders = {
"hunyuan": HunYuanModelLoader,
"HYVideo-T/2-cfgdistill": HunYuanModelLoader,
}
if model_type not in model_loaders:
available_models = list(model_loaders.keys())
raise ValueError(
f"Model type '{model_type}' not recognized. "
f"Available model types: {available_models}"
)
return model_loaders[model_type]
-11
View File
@@ -1,11 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
from fastvideo.models.parameter import (BasevLLMParameter,
PackedvLLMParameter)
from fastvideo.models.utils import set_random_seed
__all__ = [
"set_random_seed",
"BasevLLMParameter",
"PackedvLLMParameter",
]
-4
View File
@@ -1,4 +0,0 @@
from fastvideo.models.dits.hunyuanvideo import HunyuanVideoDiT
__all__ = ["HunyuanVideoDiT"]
-22
View File
@@ -58,22 +58,6 @@ class Inference(object):
args (argparse.Namespace): The arguments for the pipeline.
device (int): The device for inference. Default is 0.
"""
from fastvideo.pipelines.loader import get_pipeline_loader
loader = get_pipeline_loader(args)
vae, vae_kwargs, text_encoder, text_encoder_2, transformer, scheduler = \
loader.load_components(args)
return cls(
args=args,
vae=vae,
vae_kwargs=vae_kwargs,
text_encoder=text_encoder,
text_encoder_2=text_encoder_2,
model=transformer,
use_cpu_offload=args.use_cpu_offload,
device=device,
logger=logger,
parallel_args=None,
)
# ========================================================================
logger.info(f"Got text-to-video model root path: {pretrained_model_path}")
@@ -223,7 +207,6 @@ class Inference(object):
else:
raise ValueError(f"Invalid model path: {dit_weight}")
print('model_path', model_path)
if not model_path.exists():
raise ValueError(f"model_path not exists: {model_path}")
logger.info(f"Loading torch model {model_path}...")
@@ -313,11 +296,6 @@ class HunyuanVideoSampler(Inference):
progress_bar_config=None,
data_type="video",
):
print("Loading diffusion pipeline...")
# print('vae', vae)
print('text_encoder', text_encoder)
print('text_encoder_2', text_encoder_2)
print('model', model)
"""Load the denoising scheduler for inference."""
if scheduler is None:
if args.denoise_type == "flow":
+5 -9
View File
@@ -9,7 +9,6 @@ except ImportError:
sliding_tile_attention = None
from fastvideo.models.flash_attn_no_pad import flash_attn_no_pad
from flash_attn import flash_attn_func
from fastvideo.utils.communications import all_gather, all_to_all_4D
from fastvideo.utils.parallel_states import get_sequence_parallel_state, nccl_info
@@ -95,17 +94,14 @@ def parallel_attention(q, k, v, img_q_len, img_kv_len, text_mask, mask_strategy=
hidden_states = sliding_tile_attention(query, key, value, windows, text_length).transpose(1, 2)
else:
txt_length = text_mask.sum().int().item()
query = torch.cat([query, encoder_query[:, :txt_length]], dim=1)
key = torch.cat([key, encoder_key[:, :txt_length]], dim=1)
value = torch.cat([value, encoder_value[:, :txt_length]], dim=1)
query = torch.cat([query, encoder_query], dim=1)
key = torch.cat([key, encoder_key], dim=1)
value = torch.cat([value, encoder_value], dim=1)
# B, S, 3, H, D
# qkv = torch.stack([query, key, value], dim=2)
qkv = torch.stack([query, key, value], dim=2)
attn_mask = F.pad(text_mask, (sequence_length, 0), value=True)
# hidden_states = flash_attn_no_pad(qkv, attn_mask, causal=False, dropout_p=0.0, softmax_scale=None)
hidden_states = flash_attn_func(query, key, value, causal=False, dropout_p=0.0, softmax_scale=None)
hidden_states = torch.cat([hidden_states, encoder_query[:, txt_length:, :]], dim=1)
hidden_states = flash_attn_no_pad(qkv, attn_mask, causal=False, dropout_p=0.0, softmax_scale=None)
hidden_states, encoder_hidden_states = hidden_states.split_with_sizes((sequence_length, encoder_sequence_length),
dim=1)
+12 -2
View File
@@ -131,12 +131,12 @@ class MMDoubleStreamBlock(nn.Module):
# Prepare image for attention.
img_modulated = self.img_norm1(img)
img_modulated = modulate(img_modulated, shift=img_mod1_shift, scale=img_mod1_scale)
img_qkv = self.img_attn_qkv(img_modulated)
img_q, img_k, img_v = rearrange(img_qkv, "B L (K H D) -> K B L H D", K=3, H=self.heads_num)
# Apply QK-Norm if needed
img_q = self.img_attn_q_norm(img_q).to(img_v)
img_k = self.img_attn_k_norm(img_k).to(img_v)
# Apply RoPE if needed.
if freqs_cis is not None:
@@ -148,12 +148,12 @@ class MMDoubleStreamBlock(nn.Module):
shrink_head(freqs_cis[0], dim=0),
shrink_head(freqs_cis[1], dim=0),
)
img_qq, img_kk = apply_rotary_emb(img_q, img_k, freqs_cis, head_first=False)
assert (img_qq.shape == img_q.shape and img_kk.shape == img_k.shape
), f"img_kk: {img_qq.shape}, img_q: {img_q.shape}, img_kk: {img_kk.shape}, img_k: {img_k.shape}"
img_q, img_k = img_qq, img_kk
# Prepare txt for attention.
txt_modulated = self.txt_norm1(txt)
txt_modulated = modulate(txt_modulated, shift=txt_mod1_shift, scale=txt_mod1_scale)
@@ -162,6 +162,7 @@ class MMDoubleStreamBlock(nn.Module):
# Apply QK-Norm if needed.
txt_q = self.txt_attn_q_norm(txt_q).to(txt_v)
txt_k = self.txt_attn_k_norm(txt_k).to(txt_v)
attn = parallel_attention(
(img_q, txt_q),
(img_k, txt_k),
@@ -173,7 +174,9 @@ class MMDoubleStreamBlock(nn.Module):
)
# attention computation end
img_attn, txt_attn = attn[:, :img.shape[1]], attn[:, img.shape[1]:]
# Calculate the img blocks.
img = img + apply_gate(self.img_attn_proj(img_attn), gate=img_mod1_gate)
img = img + apply_gate(
@@ -540,6 +543,7 @@ class HYVideoDiffusionTransformer(ModelMixin, ConfigMixin):
# text modulation
vec = vec + self.vector_in(text_states_2)
# guidance modulation
if self.guidance_embed:
if guidance is None:
@@ -547,6 +551,7 @@ class HYVideoDiffusionTransformer(ModelMixin, ConfigMixin):
# our timestep_embedding is merged into guidance_in(TimestepEmbedder)
vec = vec + self.guidance_in(guidance)
# Embed image and text.
img = self.img_in(img)
if self.text_projection == "linear":
@@ -555,11 +560,13 @@ class HYVideoDiffusionTransformer(ModelMixin, ConfigMixin):
txt = self.txt_in(txt, t, text_mask if self.use_attention_mask else None)
else:
raise NotImplementedError(f"Unsupported text_projection: {self.text_projection}")
txt_seq_len = txt.shape[1]
img_seq_len = img.shape[1]
freqs_cis = (freqs_cos, freqs_sin) if freqs_cos is not None else None
# --------------------- Pass through DiT blocks ------------------------
for index, block in enumerate(self.double_blocks):
double_block_args = [img, txt, vec, freqs_cis, text_mask, mask_strategy[index]]
img, txt = block(*double_block_args)
@@ -580,9 +587,12 @@ class HYVideoDiffusionTransformer(ModelMixin, ConfigMixin):
x = block(*single_block_args)
if output_features and _ % output_features_stride == 0:
features_list.append(x[:, :img_seq_len, ...])
img = x[:, :img_seq_len, ...]
# ---------------------------- Final layer ------------------------------
img = self.final_layer(img, vec) # (N, T, patch_size ** 2 * out_channels)
img = self.unpatchify(img, tt, th, tw)
assert not return_dict, "return_dict is not supported."
if output_features:
@@ -160,7 +160,6 @@ def apply_rotary_emb(
cos, sin = cos.to(xq.device), sin.to(xq.device)
# real * cos - imag * sin
# imag * cos + real * sin
xq_out = (xq.float() * cos + rotate_half(xq.float()) * sin).type_as(xq)
xk_out = (xk.float() * cos + rotate_half(xk.float()) * sin).type_as(xk)
else:
@@ -77,6 +77,7 @@ class IndividualTokenRefinerBlock(nn.Module):
# Self-Attention
attn = attention(q, k, v, attn_mask=attn_mask)
x = x + apply_gate(self.self_attn_proj(attn), gate_msa)
# FFN Layer
@@ -191,9 +192,11 @@ class SingleTokenRefiner(nn.Module):
else:
mask_float = mask.float().unsqueeze(-1) # [b, s1, 1]
context_aware_representations = (x * mask_float).sum(dim=1) / mask_float.sum(dim=1)
context_aware_representations = self.c_embedder(context_aware_representations.to(x.dtype))
context_aware_representations = self.c_embedder(context_aware_representations)
c = timestep_aware_representations + context_aware_representations
x = self.input_embedder(x)
x = self.individual_token_refiner(x, c, mask)
return x
@@ -21,15 +21,11 @@ from math import prod
from typing import Dict, Optional, Tuple, Union
import torch
import torch.nn as nn
import torch.distributed as dist
import torch.nn as nn
from diffusers.configuration_utils import ConfigMixin, register_to_config
from fastvideo.utils.parallel_states import nccl_info
from fastvideo.distributed.parallel_state import get_sp_group
from fastvideo.logger import init_logger
logger = init_logger(__name__)
try:
# This diffusers is modified and packed in the mirror.
@@ -311,7 +307,6 @@ class AutoencoderKLCausal3D(ModelMixin, ConfigMixin, FromOriginalVAEMixin):
def _decode(self, z: torch.FloatTensor, return_dict: bool = True) -> Union[DecoderOutput, torch.FloatTensor]:
assert len(z.shape) == 5, "The input tensor should have 5 dimensions."
logger.info(f"use_parallel: {self.use_parallel}")
if self.use_parallel:
return self.parallel_tiled_decode(z, return_dict=return_dict)
@@ -581,12 +576,7 @@ class AutoencoderKLCausal3D(ModelMixin, ConfigMixin, FromOriginalVAEMixin):
"""
Parallel version of tiled_decode that distributes both temporal and spatial computation across GPUs
"""
# world_size, rank = nccl_info.sp_size, nccl_info.rank_within_group
sp_group = get_sp_group()
world_size = sp_group.world_size
rank = sp_group.rank_in_group
world_size, rank = nccl_info.sp_size, nccl_info.rank_within_group
B, C, T, H, W = z.shape
# Calculate parameters
@@ -660,8 +650,6 @@ class AutoencoderKLCausal3D(ModelMixin, ConfigMixin, FromOriginalVAEMixin):
gathered_results = torch.zeros_like(padded_results).repeat(
world_size, *[1] * len(padded_results.shape)).contiguous(
) # use contiguous to make sure it won't copy data in the following operations
# gathered_results = sp_group.all_gather(padded_results)
# gathered_dim_metadata = sp_group.broadcast_object_list(gathered_dim_metadata, local_dim_metadata)
dist.all_gather_into_tensor(gathered_results, padded_results)
dist.all_gather_object(gathered_dim_metadata, local_dim_metadata)
# Process gathered results
-70
View File
@@ -1,70 +0,0 @@
"""
Diffusion pipelines for FastVideo.
This package contains diffusion pipelines for generating videos and images.
"""
from typing import Dict, Optional, Type, Any
# First, import the registry
from fastvideo.pipelines.pipeline_registry import PipelineRegistry, register_pipeline
from fastvideo.inference_args import InferenceArgs
# Then import the base classes
from fastvideo.pipelines.composed.composed_pipeline_base import (
ComposedPipelineBase,
DiffusionPipelineOutput
)
def get_pipeline_type(inference_args: InferenceArgs) -> str:
# hardcode for now
return "hunyuan_video"
def build_pipeline(inference_args: InferenceArgs) -> ComposedPipelineBase:
"""
Only works with valid hf diffusers configs. (model_index.json)
We want to build a pipeline based on the inference args.
1. load the correct hf config from disk or download from hub
2. based on the config, determine the pipeline class and pipeline loader class
3. parse the config to get the model component (vae, text_encoders, etc...)
names and paths
4. the pipeline loader class will use the model component names and paths to load
the models.
5. the pipeline class will be composed of the models returned by the pipeline loader
"""
# Get pipeline type
pipeline_type = get_pipeline_type(inference_args)
if not hasattr(inference_args, 'model_type') or not inference_args.model_type:
inference_args.model_type = pipeline_type
# TODO(will): Get pipeline loader (hardcoded for now)
from fastvideo.pipelines.loader import get_pipeline_loader
pipeline_loader = get_pipeline_loader(inference_args)
# Load and return pipeline
return pipeline_loader.load_pipeline_v2(inference_args)
def list_available_pipelines() -> Dict[str, Type[Any]]:
"""
List all available pipeline types.
Returns:
A dictionary of pipeline names to pipeline classes.
"""
return PipelineRegistry.list()
# Import all pipeline implementations to register them
# These imports should be at the end to avoid circular imports
from fastvideo.pipelines.implementations.hunyuan import HunyuanVideoPipeline
__all__ = [
"create_pipeline",
"list_available_pipelines",
"ComposedPipelineBase",
"DiffusionPipelineOutput",
"register_pipeline",
"HunyuanVideoPipeline",
]
@@ -1,113 +0,0 @@
"""
Text-to-video pipeline implementation.
This module contains a standard text-to-video pipeline implementation that can be
used as a base for more specific implementations.
"""
from typing import Any, Dict, List, Optional, Union, Tuple
import torch
from fastvideo.pipelines.composed.composed_pipeline_base import ComposedPipelineBase
from fastvideo.pipelines.stages import (
InputValidationStage,
PromptEncodingStage,
TimestepPreparationStage,
LatentPreparationStage,
ConditioningStage,
DenoisingStage,
DecodingStage,
PostProcessingStage,
)
# Import from the main pipelines package to avoid circular imports
from fastvideo.pipelines import register_pipeline
@register_pipeline("text-to-video")
class TextToVideoPipeline(ComposedPipelineBase):
"""
Standard text-to-video pipeline.
This pipeline implements a standard text-to-video diffusion process with the
following stages:
1. Input validation
2. Prompt encoding
3. Timestep preparation
4. Latent preparation
5. Conditioning (classifier-free guidance)
6. Denoising
7. Decoding
8. Post-processing
This pipeline supports models with either a single text encoder or dual text encoders.
Different model architectures may use secondary encoders for various purposes such as
additional conditioning information, temporal control, stylistic aspects, or other
model-specific features.
"""
is_video_pipeline = True
def __init__(
self,
input_validation_stage: InputValidationStage,
prompt_encoding_stage: PromptEncodingStage,
timestep_preparation_stage: TimestepPreparationStage,
latent_preparation_stage: LatentPreparationStage,
conditioning_stage: ConditioningStage,
denoising_stage: DenoisingStage,
decoding_stage: DecodingStage,
post_processing_stage: Optional[PostProcessingStage] = None,
secondary_prompt_encoding_stage: Optional[PromptEncodingStage] = None,
):
"""
Initialize the text-to-video pipeline.
Args:
input_validation_stage: The input validation stage.
prompt_encoding_stage: The primary prompt encoding stage.
timestep_preparation_stage: The timestep preparation stage.
latent_preparation_stage: The latent preparation stage.
conditioning_stage: The conditioning stage.
denoising_stage: The denoising stage.
decoding_stage: The decoding stage.
post_processing_stage: The post-processing stage (optional).
secondary_prompt_encoding_stage: Optional secondary prompt encoding stage for models
with dual text encoders. The purpose of this encoder varies by model architecture
and may handle additional conditioning aspects such as temporal control, style,
or other model-specific features.
"""
super().__init__()
# Add the stages in order
self.add_stage(input_validation_stage)
self.add_stage(prompt_encoding_stage)
# Add optional secondary prompt encoding stage if provided
if secondary_prompt_encoding_stage is not None:
self.add_stage(secondary_prompt_encoding_stage)
self.add_stage(timestep_preparation_stage)
self.add_stage(latent_preparation_stage)
self.add_stage(conditioning_stage)
self.add_stage(denoising_stage)
self.add_stage(decoding_stage)
if post_processing_stage is not None:
self.add_stage(post_processing_stage)
@property
def components(self) -> Dict[str, Any]:
"""Get all components used by this pipeline."""
components = {}
# Add components from each stage
for stage in self._stages:
for attr_name in dir(stage):
if attr_name.startswith("__") or attr_name.startswith("_"):
continue
attr = getattr(stage, attr_name)
if isinstance(attr, torch.nn.Module):
components[attr_name] = attr
return components
@@ -1,8 +0,0 @@
"""
Concrete implementations of diffusion pipelines.
This package contains concrete implementations of diffusion pipelines for
specific models or architectures.
"""
__all__ = []
@@ -1,125 +0,0 @@
"""
HunYuan video diffusion pipeline implementation.
This module contains an implementation of the HunYuan video diffusion pipeline
using the modular pipeline architecture.
"""
from typing import Any, Dict, List, Optional, Union, Tuple
import torch
from diffusers.image_processor import VaeImageProcessor
from fastvideo.pipelines.composed import ComposedPipelineBase
from fastvideo.pipelines.stages import (
InputValidationStage,
PromptEncodingStage,
TimestepPreparationStage,
LatentPreparationStage,
ConditioningStage,
DenoisingStage,
DecodingStage,
PostProcessingStage,
)
from fastvideo.pipelines import register_pipeline
from fastvideo.pipelines.stages.prompt_encoding import PromptEncodingStage
# from fastvideo.pipelines.stages.timestep_preparation import FlowMatchingTimestepPreparationStage
from fastvideo.inference_args import InferenceArgs
# from fastvideo.pipelines.composed.composed_pipeline_base import DiffusionPipelineOutput
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
from diffusers.utils import BaseOutput
from typing import Union, Optional, Tuple, List
import numpy as np
from dataclasses import dataclass
from fastvideo.logger import init_logger
logger = init_logger(__name__)
# class HunyuanLatentPreparationStage(LatentPreparationStage):
# def _call_implementation(self, batch: ForwardBatch, inference_args: InferenceArgs) -> ForwardBatch:
# "custom logic for HunYuan latent preparation"
# pass
# class hunyuanloader(PipelineLoader):
# def load_components(self, inference_args: InferenceArgs):
# pass
@dataclass
class DiffusionPipelineOutput(BaseOutput):
videos: Union[torch.Tensor, np.ndarray]
@register_pipeline("hunyuan-video")
class HunyuanVideoPipeline(ComposedPipelineBase):
def setup_pipeline(self, inference_args: InferenceArgs):
self._stages = []
self.add_stage("input_validation_stage",
InputValidationStage())
self.add_stage("prompt_encoding_stage_primary",
PromptEncodingStage(is_secondary=False))
self.add_stage("prompt_encoding_stage_secondary",
PromptEncodingStage(is_secondary=True))
self.add_stage("conditioning_stage",
ConditioningStage())
self.add_stage("timestep_preparation_stage",
TimestepPreparationStage())
self.add_stage("latent_preparation_stage",
LatentPreparationStage())
self.add_stage("denoising_stage",
DenoisingStage())
self.add_stage("decoding_stage",
DecodingStage())
def initialize_pipeline(self, inference_args: InferenceArgs):
assert len(self._stages) > 0, "Pipeline stages are not set"
assert len(self._modules) > 0, "Pipeline modules are not set"
vae_scale_factor = 2**(len(self.vae.config.block_out_channels) - 1)
inference_args.vae_scale_factor = vae_scale_factor
self.image_processor = VaeImageProcessor(vae_scale_factor=vae_scale_factor)
self.register_modules({"image_processor": self.image_processor})
num_channels_latents = self.transformer.in_channels
inference_args.num_channels_latents = num_channels_latents
def adjust_video_length(self, batch: ForwardBatch, inference_args: InferenceArgs):
"""Adjust video length based on VAE version"""
video_length = batch.num_frames
vae_ver = inference_args.vae
logger.info(f"Adjusting video length for VAE version: {vae_ver}")
if "884" in vae_ver:
batch.num_frames = (video_length - 1) // 4 + 1
elif "888" in vae_ver:
batch.num_frames = (video_length - 1) // 8 + 1
return batch
@torch.no_grad()
def forward(self, batch: ForwardBatch, inference_args: InferenceArgs) -> ForwardBatch:
logger.info(f"Running pipeline stages: {self._stage_name_mapping.keys()}")
logger.info(f"Batch: {batch}")
# for stage in self._stages:
# batch = stage(batch, inference_args)
# or
batch = self.input_validation_stage(batch, inference_args)
batch = self.prompt_encoding_stage_primary(batch, inference_args)
batch = self.prompt_encoding_stage_secondary(batch, inference_args)
batch = self.conditioning_stage(batch, inference_args)
batch = self.timestep_preparation_stage(batch, inference_args)
# custom logic
batch = self.adjust_video_length(batch, inference_args)
batch = self.latent_preparation_stage(batch, inference_args)
batch = self.denoising_stage(batch, inference_args)
batch = self.decoding_stage(batch, inference_args)
return DiffusionPipelineOutput(videos=batch.videos)
-69
View File
@@ -1,69 +0,0 @@
"""
Registry for pipeline implementations.
This module provides a registry for pipeline implementations, allowing them to be
looked up by name.
"""
from typing import Dict, Type, Any, Optional, Callable, TypeVar, cast
# Define a type variable for the pipeline class
T = TypeVar('T')
class PipelineRegistry:
"""Registry for pipeline implementations."""
_registry: Dict[str, Type[Any]] = {}
@classmethod
def register(cls, name: str) -> Callable[[Type[T]], Type[T]]:
"""
Register a pipeline implementation.
Args:
name: The name to register the pipeline under.
Returns:
A decorator that registers the pipeline.
"""
def decorator(pipeline_cls: Type[T]) -> Type[T]:
cls._registry[name] = pipeline_cls
return pipeline_cls
return decorator
@classmethod
def get(cls, name: str) -> Optional[Type[Any]]:
"""
Get a pipeline implementation by name.
Args:
name: The name of the pipeline.
Returns:
The pipeline class, or None if not found.
"""
return cls._registry.get(name)
@classmethod
def list(cls) -> Dict[str, Type[Any]]:
"""
List all registered pipelines.
Returns:
A dictionary of pipeline names to pipeline classes.
"""
return cls._registry.copy()
def register_pipeline(name: str) -> Callable[[Type[T]], Type[T]]:
"""
Register a pipeline implementation.
Args:
name: The name to register the pipeline under.
Returns:
A decorator that registers the pipeline.
"""
return PipelineRegistry.register(name)
-28
View File
@@ -1,28 +0,0 @@
"""
Pipeline stages for diffusion models.
This package contains the various stages that can be composed to create
complete diffusion pipelines.
"""
from fastvideo.pipelines.stages.base import PipelineStage
from fastvideo.pipelines.stages.input_validation import InputValidationStage
from fastvideo.pipelines.stages.prompt_encoding import PromptEncodingStage
from fastvideo.pipelines.stages.timestep_preparation import TimestepPreparationStage
from fastvideo.pipelines.stages.latent_preparation import LatentPreparationStage
from fastvideo.pipelines.stages.conditioning import ConditioningStage
from fastvideo.pipelines.stages.denoising import DenoisingStage
from fastvideo.pipelines.stages.decoding import DecodingStage
from fastvideo.pipelines.stages.post_processing import PostProcessingStage
__all__ = [
"PipelineStage",
"InputValidationStage",
"PromptEncodingStage",
"TimestepPreparationStage",
"LatentPreparationStage",
"ConditioningStage",
"DenoisingStage",
"DecodingStage",
"PostProcessingStage",
]
-103
View File
@@ -1,103 +0,0 @@
import argparse
import os
from pathlib import Path
import imageio
import numpy as np
import torch
import torchvision
from einops import rearrange
# Fix the import path
from fastvideo.inference import DiffusionInference
from fastvideo.inference_args import InferenceArgs
from fastvideo.utils.utils import FlexibleArgumentParser
from fastvideo.distributed import init_distributed_environment, initialize_model_parallel
from fastvideo.logger import init_logger
import torch.distributed as dist
from fastvideo.utils.parallel_states import initialize_sequence_parallel_state
logger = init_logger(__name__)
def initialize_distributed():
local_rank = int(os.getenv("RANK", 0))
world_size = int(os.getenv("WORLD_SIZE", 1))
print("world_size", world_size)
torch.cuda.set_device(local_rank)
dist.init_process_group(backend="nccl", init_method="env://", world_size=world_size, rank=local_rank)
initialize_sequence_parallel_state(world_size)
def main(inference_args: InferenceArgs):
# initialize_distributed()
# print(nccl_info.sp_size)
# local_rank = int(os.environ.get("LOCAL_RANK", 0))
# rank = int(os.environ.get("RANK", 0))
# world_size = int(os.environ.get("WORLD_SIZE", 1))
# logger.info(f"Initializing process: rank={rank}, local_rank={local_rank}, world_size={world_size}")
# init_distributed_environment(
# world_size=world_size,
# rank=rank,
# local_rank=local_rank
# )
# print(inference_args.sp_size)
# Initialize tensor model parallel groups
# initialize_model_parallel(
# sequence_model_parallel_size=inference_args.sp_size
# )
initialize_distributed()
print('Loading pipeline')
# Create inference object using the updated API
diffusion = DiffusionInference.load_pipeline(
# model_path=models_root_path,
inference_args,
# model_loader_cls=ModelLoader,
# pipeline_cls=HunyuanVideoPipeline
)
print('Pipeline loaded')
# return
# Load prompts
if inference_args.prompt.endswith('.txt'):
with open(inference_args.prompt) as f:
prompts = [line.strip() for line in f.readlines()]
else:
prompts = [inference_args.prompt]
# Process each prompt
for prompt in prompts:
outputs = diffusion.predict(
prompt=prompt,
inference_args=inference_args,
)
# Process outputs
videos = rearrange(outputs["samples"], "b c t h w -> t b c h w")
frames = []
for x in videos:
x = torchvision.utils.make_grid(x, nrow=6)
x = x.transpose(0, 1).transpose(1, 2).squeeze(-1)
frames.append((x * 255).numpy().astype(np.uint8))
# Save video
os.makedirs(os.path.dirname(inference_args.output_path), exist_ok=True)
imageio.mimsave(
os.path.join(inference_args.output_path, f"{prompt[:100]}.mp4"),
frames,
fps=inference_args.fps
)
if __name__ == "__main__":
parser = FlexibleArgumentParser()
InferenceArgs.add_cli_args(parser)
args = parser.parse_args()
inference_args = InferenceArgs.from_cli_args(args)
# Validate arguments
if inference_args.vae_sp and not inference_args.vae_tiling:
raise ValueError("Currently enabling vae_sp requires enabling vae_tiling, please set --vae-tiling to True.")
main(inference_args)
+7 -16
View File
@@ -11,8 +11,7 @@ from einops import rearrange
from fastvideo.models.hunyuan.inference import HunyuanVideoSampler
from fastvideo.utils.parallel_states import initialize_sequence_parallel_state, nccl_info
from fastvideo.inference_args import InferenceArgs
from fastvideo.utils.utils import FlexibleArgumentParser
def initialize_distributed():
local_rank = int(os.getenv("RANK", 0))
@@ -23,21 +22,21 @@ def initialize_distributed():
initialize_sequence_parallel_state(world_size)
def main(inference_args: InferenceArgs):
def main(args):
initialize_distributed()
print(nccl_info.sp_size)
print(inference_args)
models_root_path = Path(inference_args.model_path)
print(args)
models_root_path = Path(args.model_path)
if not models_root_path.exists():
raise ValueError(f"`models_root` not exists: {models_root_path}")
# Create save folder to save the samples
save_path = inference_args.output_path
save_path = args.output_path
os.makedirs(os.path.dirname(save_path), exist_ok=True)
# Load models
hunyuan_video_sampler = HunyuanVideoSampler.from_pretrained(models_root_path, args=inference_args)
hunyuan_video_sampler = HunyuanVideoSampler.from_pretrained(models_root_path, args=args)
# Get the updated args
args = hunyuan_video_sampler.args
@@ -74,15 +73,7 @@ def main(inference_args: InferenceArgs):
if __name__ == "__main__":
parser = FlexibleArgumentParser()
InferenceArgs.add_cli_args(parser)
args = parser.parse_args()
inference_args = InferenceArgs.from_cli_args(args)
# process for vae sequence parallel
if inference_args.vae_sp and not inference_args.vae_tiling:
raise ValueError("Currently enabling vae_sp requires enabling vae_tiling, please set --vae-tiling to True.")
main(inference_args)
exit()
parser = argparse.ArgumentParser()
# Basic parameters
parser.add_argument("--prompt", type=str, help="prompt file for inference")
-101
View File
@@ -1,101 +0,0 @@
import argparse
import os
from pathlib import Path
import imageio
import numpy as np
import torch
import torchvision
from einops import rearrange
# Fix the import path
from fastvideo.inference_engine import InferenceEngine
from fastvideo.inference_args import InferenceArgs
from fastvideo.utils.utils import FlexibleArgumentParser
from fastvideo.distributed import init_distributed_environment, initialize_model_parallel
from fastvideo.logger import init_logger
import torch.distributed as dist
from fastvideo.utils.parallel_states import initialize_sequence_parallel_state
logger = init_logger(__name__)
def initialize_distributed():
local_rank = int(os.getenv("RANK", 0))
world_size = int(os.getenv("WORLD_SIZE", 1))
print("world_size", world_size)
torch.cuda.set_device(local_rank)
dist.init_process_group(backend="nccl", init_method="env://", world_size=world_size, rank=local_rank)
initialize_sequence_parallel_state(world_size)
def main(inference_args: InferenceArgs):
# initialize_distributed()
# print(nccl_info.sp_size)
# local_rank = int(os.environ.get("LOCAL_RANK", 0))
# rank = int(os.environ.get("RANK", 0))
# world_size = int(os.environ.get("WORLD_SIZE", 1))
# logger.info(f"Initializing process: rank={rank}, local_rank={local_rank}, world_size={world_size}")
# init_distributed_environment(
# world_size=world_size,
# rank=rank,
# local_rank=local_rank
# )
# print(inference_args.sp_size)
# Initialize tensor model parallel groups
# initialize_model_parallel(
# sequence_model_parallel_size=inference_args.sp_size
# )
initialize_distributed()
print('Creating engine')
# Create inference object using the updated API
engine = InferenceEngine.create_engine(
inference_args,
)
print('Engine created')
# return
# Load prompts
if inference_args.prompt.endswith('.txt'):
with open(inference_args.prompt) as f:
prompts = [line.strip() for line in f.readlines()]
else:
prompts = [inference_args.prompt]
# Process each prompt
for prompt in prompts:
outputs = engine.run(
prompt=prompt,
inference_args=inference_args,
)
# Process outputs
videos = rearrange(outputs["samples"], "b c t h w -> t b c h w")
frames = []
for x in videos:
x = torchvision.utils.make_grid(x, nrow=6)
x = x.transpose(0, 1).transpose(1, 2).squeeze(-1)
frames.append((x * 255).numpy().astype(np.uint8))
# Save video
os.makedirs(os.path.dirname(inference_args.output_path), exist_ok=True)
imageio.mimsave(
os.path.join(inference_args.output_path, f"{prompt[:100]}.mp4"),
frames,
fps=inference_args.fps
)
if __name__ == "__main__":
parser = FlexibleArgumentParser()
InferenceArgs.add_cli_args(parser)
args = parser.parse_args()
inference_args = InferenceArgs.from_cli_args(args)
inference_args.check_inference_args()
# Validate arguments
if inference_args.vae_sp and not inference_args.vae_tiling:
raise ValueError("Currently enabling vae_sp requires enabling vae_tiling, please set --vae-tiling to True.")
main(inference_args)
+3
View File
@@ -0,0 +1,3 @@
from .flash_attn import (DistributedAttention, LocalAttention)
__all__ = ["DistributedAttention", "LocalAttention"]
@@ -1,9 +1,11 @@
from itertools import accumulate
from typing import List, Optional
import torch
import torch.nn as nn
from typing import Optional
from fastvideo.distributed.communication_op import sequence_model_parallel_all_to_all_4D, sequence_model_parallel_all_gather
from fastvideo.distributed.parallel_state import get_sequence_model_parallel_rank, get_sequence_model_parallel_world_size
from flash_attn import flash_attn_func
from fastvideo.v1.distributed.communication_op import sequence_model_parallel_all_to_all_4D, sequence_model_parallel_all_gather
from fastvideo.v1.distributed.parallel_state import get_sequence_model_parallel_rank, get_sequence_model_parallel_world_size
from flash_attn import flash_attn_func, flash_attn_varlen_func
class DistributedAttention(nn.Module):
@@ -33,6 +35,7 @@ class DistributedAttention(nn.Module):
self.dropout_rate = dropout_rate
self.causal = causal
self.softmax_scale = softmax_scale
def forward(
self,
q: torch.Tensor,
@@ -33,7 +33,7 @@ class CudaCommunicator(DeviceCommunicatorBase):
# lazy import to avoid documentation build error
# from vllm.distributed.device_communicators.custom_all_reduce import (
# CustomAllreduce)
from fastvideo.distributed.device_communicators.pynccl import (
from fastvideo.v1.distributed.device_communicators.pynccl import (
PyNcclCommunicator)
self.pynccl_comm: Optional[PyNcclCommunicator] = None
@@ -8,12 +8,12 @@ import torch
import torch.distributed as dist
from torch.distributed import ProcessGroup, ReduceOp
from fastvideo.distributed.device_communicators.pynccl_wrapper import (
from fastvideo.v1.distributed.device_communicators.pynccl_wrapper import (
NCCLLibrary, buffer_type, cudaStream_t, ncclComm_t, ncclDataTypeEnum,
ncclRedOpTypeEnum, ncclUniqueId)
from fastvideo.distributed.utils import StatelessProcessGroup
from fastvideo.logger import init_logger
from fastvideo.utils.utils import current_stream
from fastvideo.v1.distributed.utils import StatelessProcessGroup
from fastvideo.v1.logger import init_logger
from fastvideo.v1.utils import current_stream
logger = init_logger(__name__)
@@ -30,8 +30,8 @@ from typing import Any, Dict, List, Optional
import torch
from torch.distributed import ReduceOp
from fastvideo.logger import init_logger
from fastvideo.utils.utils import find_nccl_library
from fastvideo.v1.logger import init_logger
from fastvideo.v1.utils import find_nccl_library
logger = init_logger(__name__)
@@ -40,15 +40,15 @@ import torch
import torch.distributed
from torch.distributed import Backend, ProcessGroup
import fastvideo.envs as envs
from fastvideo.distributed.device_communicators.base_device_communicator import (
import fastvideo.v1.envs as envs
from fastvideo.v1.distributed.device_communicators.base_device_communicator import (
DeviceCommunicatorBase)
from fastvideo.distributed.utils import StatelessProcessGroup
from fastvideo.logger import init_logger
# from fastvideo.utils import (direct_register_custom_op, resolve_obj_by_qualname,
from fastvideo.v1.distributed.utils import StatelessProcessGroup
from fastvideo.v1.logger import init_logger
# from fastvideo.v1.utils import (direct_register_custom_op, resolve_obj_by_qualname,
# supports_custom_op)
from fastvideo.distributed.device_communicators.cuda_communicator import (
from fastvideo.v1.distributed.device_communicators.cuda_communicator import (
CudaCommunicator)
@@ -192,7 +192,7 @@ class GroupCoordinator:
assert self.cpu_group is not None
assert self.device_group is not None
from fastvideo.platforms import current_platform
from fastvideo.v1.platforms import current_platform
# TODO: fix it for other platforms
if current_platform.is_cuda_alike():
@@ -220,7 +220,7 @@ class GroupCoordinator:
# self.mq_broadcaster = MessageQueue.create_from_process_group(
# self.cpu_group, 1 << 22, 6)
from fastvideo.platforms import current_platform
from fastvideo.v1.platforms import current_platform
# self.use_custom_op_call = current_platform.is_cuda_alike()
self.use_custom_op_call = False
@@ -270,7 +270,7 @@ class GroupCoordinator:
# only cuda uses this function,
# so we don't abstract it into the base class
maybe_ca_context = nullcontext()
from fastvideo.distributed.device_communicators.cuda_communicator import (
from fastvideo.v1.distributed.device_communicators.cuda_communicator import (
CudaCommunicator)
if self.device_communicator is not None:
assert isinstance(self.device_communicator, CudaCommunicator)
@@ -850,7 +850,6 @@ def get_sp_group() -> GroupCoordinator:
def initialize_model_parallel(
tensor_model_parallel_size: int = 1,
pipeline_model_parallel_size: int = 1,
sequence_model_parallel_size: int = 1,
backend: Optional[str] = None,
) -> None:
@@ -885,15 +884,15 @@ def initialize_model_parallel(
get_world_group().device_group)
# Ensure the world size is compatible with the parallelism configuration
assert world_size % (tensor_model_parallel_size * pipeline_model_parallel_size * sequence_model_parallel_size) == 0, \
f"World size ({world_size}) must be divisible by tensor_model_parallel_size ({tensor_model_parallel_size}) * " \
f"pipeline_model_parallel_size ({pipeline_model_parallel_size}) * " \
f"sequence_model_parallel_size ({sequence_model_parallel_size})"
# assert world_size % (tensor_model_parallel_size * pipeline_model_parallel_size * sequence_model_parallel_size) == 0, \
# f"World size ({world_size}) must be divisible by tensor_model_parallel_size ({tensor_model_parallel_size}) * " \
# f"pipeline_model_parallel_size ({pipeline_model_parallel_size}) * " \
# f"sequence_model_parallel_size ({sequence_model_parallel_size})"
# Check for incompatible parallelism configurations
if sequence_model_parallel_size > 1:
assert tensor_model_parallel_size == 1, "Sequence parallelism (SP) is incompatible with tensor parallelism (TP). Please use SP=1 or TP=1."
assert pipeline_model_parallel_size == 1, "Sequence parallelism (SP) is incompatible with pipeline parallelism (PP). Please use SP=1 or PP=1."
# if sequence_model_parallel_size > 1:
# assert tensor_model_parallel_size == 1, "Sequence parallelism (SP) is incompatible with tensor parallelism (TP). Please use SP=1 or TP=1."
# assert pipeline_model_parallel_size == 1, "Sequence parallelism (SP) is incompatible with pipeline parallelism (PP). Please use SP=1 or PP=1."
# Build the tensor model-parallel groups.
num_tensor_model_parallel_groups: int = (world_size //
@@ -933,21 +932,6 @@ def initialize_model_parallel(
backend,
group_name="sp")
# Build the pipeline model-parallel groups.
num_pipeline_model_parallel_groups: int = (world_size //
pipeline_model_parallel_size)
global _PP
assert _PP is None, (
"pipeline model parallel group is already initialized")
group_ranks = []
for i in range(num_pipeline_model_parallel_groups):
ranks = list(range(i, world_size, num_pipeline_model_parallel_groups))
group_ranks.append(ranks)
_PP = init_model_parallel_group(group_ranks,
get_world_group().local_rank,
backend,
group_name="pp")
def get_sequence_model_parallel_world_size():
"""Return world size for the sequence model parallel group."""
@@ -1079,7 +1063,7 @@ def cleanup_dist_env_and_memory(shutdown_ray: bool = False):
import ray # Lazy import Ray
ray.shutdown()
gc.collect()
from fastvideo.platforms import current_platform
from fastvideo.v1.platforms import current_platform
if not current_platform.is_cpu():
torch.cuda.empty_cache()
try:
@@ -1172,3 +1156,267 @@ def in_the_same_node_as(pg: Union[ProcessGroup, StatelessProcessGroup],
aggregated_data += rank_data
return [x == 1 for x in aggregated_data.tolist()]
def initialize_tensor_parallel_group(
tensor_model_parallel_size: int = 1,
backend: Optional[str] = None,
group_name_suffix: str = ""
) -> GroupCoordinator:
"""Initialize a tensor parallel group for a specific model.
This function creates a tensor parallel group that can be used with the
patch_tensor_parallel_group context manager. It allows different models
to use different tensor parallelism configurations.
Arguments:
tensor_model_parallel_size: number of GPUs used for tensor model parallelism.
backend: communication backend to use.
group_name_suffix: optional suffix to make the group name unique.
Returns:
A GroupCoordinator for tensor parallelism that can be used with
the patch_tensor_parallel_group context manager.
Example usage:
```python
# Initialize tensor parallel group for model1
tp_group_model1 = initialize_tensor_parallel_group(
tensor_model_parallel_size=4,
group_name_suffix="model1"
)
# Use tensor parallelism for model1
with patch_tensor_parallel_group(tp_group_model1):
# Run model1 with tensor parallelism
output1 = model1(input1)
```
"""
# Get world size and rank. Ensure some consistencies.
assert torch.distributed.is_initialized()
world_size: int = torch.distributed.get_world_size()
backend = backend or torch.distributed.get_backend(
get_world_group().device_group)
# Ensure the world size is compatible with the parallelism configuration
assert world_size % tensor_model_parallel_size == 0, \
f"World size ({world_size}) must be divisible by tensor_model_parallel_size ({tensor_model_parallel_size})"
# Build the tensor model-parallel groups.
num_tensor_model_parallel_groups: int = (world_size // tensor_model_parallel_size)
tp_group_ranks = []
for i in range(num_tensor_model_parallel_groups):
ranks = list(
range(i * tensor_model_parallel_size,
(i + 1) * tensor_model_parallel_size))
tp_group_ranks.append(ranks)
# Create TP group coordinator with a unique name
group_name = f"tp_{group_name_suffix}" if group_name_suffix else "tp"
tp_group = init_model_parallel_group(tp_group_ranks,
get_world_group().local_rank,
backend,
use_message_queue_broadcaster=True,
group_name=group_name)
return tp_group
def initialize_sequence_parallel_group(
sequence_model_parallel_size: int = 1,
backend: Optional[str] = None,
group_name_suffix: str = ""
) -> GroupCoordinator:
"""Initialize a sequence parallel group for a specific model.
This function creates a sequence parallel group that can be used with the
patch_sequence_parallel_group context manager. It allows different models
to use different sequence parallelism configurations.
Arguments:
sequence_model_parallel_size: number of GPUs used for sequence model parallelism.
backend: communication backend to use.
group_name_suffix: optional suffix to make the group name unique.
Returns:
A GroupCoordinator for sequence parallelism that can be used with
the patch_sequence_parallel_group context manager.
Example usage:
```python
# Initialize sequence parallel group for model2
sp_group_model2 = initialize_sequence_parallel_group(
sequence_model_parallel_size=2,
group_name_suffix="model2"
)
# Use sequence parallelism for model2
with patch_sequence_parallel_group(sp_group_model2):
# Run model2 with sequence parallelism
output2 = model2(input2)
```
"""
# Get world size and rank. Ensure some consistencies.
assert torch.distributed.is_initialized()
world_size: int = torch.distributed.get_world_size()
backend = backend or torch.distributed.get_backend(
get_world_group().device_group)
# Ensure the world size is compatible with the parallelism configuration
assert world_size % sequence_model_parallel_size == 0, \
f"World size ({world_size}) must be divisible by sequence_model_parallel_size ({sequence_model_parallel_size})"
# Build the sequence model-parallel groups.
num_sequence_model_parallel_groups: int = (world_size // sequence_model_parallel_size)
sp_group_ranks = []
for i in range(num_sequence_model_parallel_groups):
# Create groups of consecutive ranks
ranks = list(range(i * sequence_model_parallel_size,
(i + 1) * sequence_model_parallel_size))
sp_group_ranks.append(ranks)
# Create SP group coordinator with a unique name
group_name = f"sp_{group_name_suffix}" if group_name_suffix else "sp"
sp_group = init_model_parallel_group(sp_group_ranks,
get_world_group().local_rank,
backend,
group_name=group_name)
return sp_group
_SP_STATE_PATCHED = False
@contextmanager
def patch_sequence_parallel_group(sp_group: GroupCoordinator):
"""Patch the sp group temporarily until this function ends.
This method allows running a model with SP while another model uses TP.
Args:
sp_group (GroupCoordinator): the sp group coordinator
"""
global _SP_STATE_PATCHED
assert not _SP_STATE_PATCHED, "Should not call when it's already patched"
_SP_STATE_PATCHED = True
old_sp_group = get_sp_group()
global _SP
_SP = sp_group
try:
yield
finally:
# restore the original state
_SP_STATE_PATCHED = False
_SP = old_sp_group
_PP_STATE_PATCHED = False
@contextmanager
def patch_pipeline_parallel_group(pp_group: GroupCoordinator):
"""Patch the pp group temporarily until this function ends.
This method allows running a model with a different PP configuration.
Args:
pp_group (GroupCoordinator): the pp group coordinator
"""
global _PP_STATE_PATCHED
assert not _PP_STATE_PATCHED, "Should not call when it's already patched"
_PP_STATE_PATCHED = True
old_pp_group = get_pp_group()
global _PP
_PP = pp_group
try:
yield
finally:
# restore the original state
_PP_STATE_PATCHED = False
_PP = old_pp_group
# Example of how to use the independent parallelism functions
"""
Here's a complete example of how to use the independent parallelism functions
for different models:
```python
import torch
from fastvideo.v1.distributed.parallel_state import (
init_distributed_environment,
initialize_tensor_parallel_group,
initialize_sequence_parallel_group,
patch_tensor_parallel_group,
patch_sequence_parallel_group,
destroy_model_parallel,
destroy_distributed_environment
)
# Initialize the distributed environment
init_distributed_environment(
world_size=8,
rank=torch.distributed.get_rank(),
distributed_init_method="tcp://localhost:12345",
local_rank=torch.distributed.get_rank() % torch.cuda.device_count()
)
try:
# Create a tensor parallel group for model1 with TP=4
tp_group_model1 = initialize_tensor_parallel_group(
tensor_model_parallel_size=4,
group_name_suffix="model1"
)
# Create a sequence parallel group for model2 with SP=2
sp_group_model2 = initialize_sequence_parallel_group(
sequence_model_parallel_size=2,
group_name_suffix="model2"
)
# Create another tensor parallel group for model3 with TP=2
tp_group_model3 = initialize_tensor_parallel_group(
tensor_model_parallel_size=2,
group_name_suffix="model3"
)
# Use model1 with tensor parallelism
with patch_tensor_parallel_group(tp_group_model1):
# Inside this context, get_tp_group() returns tp_group_model1
# Run model1 with tensor parallelism
output1 = model1(input1)
# Use model2 with sequence parallelism
with patch_sequence_parallel_group(sp_group_model2):
# Inside this context, get_sp_group() returns sp_group_model2
# Run model2 with sequence parallelism
output2 = model2(input2)
# Use model3 with a different tensor parallelism configuration
with patch_tensor_parallel_group(tp_group_model3):
# Inside this context, get_tp_group() returns tp_group_model3
# Run model3 with tensor parallelism
output3 = model3(input3)
# You can switch between models as needed
with patch_tensor_parallel_group(tp_group_model1):
# Back to using model1
more_output1 = model1(more_input1)
finally:
# Clean up
destroy_model_parallel()
destroy_distributed_environment()
```
This approach allows you to:
1. Create separate parallel groups for each model
2. Use different parallelism strategies for different models
3. Switch between models as needed
4. Use unique group names to avoid conflicts
Note that each model can use its own optimal parallelism strategy without
interfering with other models.
"""
@@ -18,8 +18,8 @@ from torch.distributed.distributed_c10d import (Backend, PrefixStore,
is_nccl_available)
from torch.distributed.rendezvous import rendezvous
import fastvideo.envs as envs
from fastvideo.logger import init_logger
import fastvideo.v1.envs as envs
from fastvideo.v1.logger import init_logger
logger = init_logger(__name__)
@@ -17,8 +17,8 @@
import argparse
import dataclasses
from fastvideo.logger import init_logger
from fastvideo.utils.utils import FlexibleArgumentParser
from fastvideo.v1.logger import init_logger
from fastvideo.v1.utils import FlexibleArgumentParser
from typing import List, Optional
@@ -29,9 +29,16 @@ class InferenceArgs:
# Model and path configuration
model_path: str
model: str = "HYVideo-T/2-cfgdistill"
use_v1_text_encoder: bool = False
use_v1_vae: bool = False
use_v1_transformer: bool = False
dit_weight: Optional[str] = None
model_dir: Optional[str] = None
# HuggingFace specific parameters
trust_remote_code: bool = False
revision: Optional[str] = None
# Parallelism
tp_size: int = 1
sp_size: int = 1
@@ -126,12 +133,29 @@ class InferenceArgs:
output_path: str = "outputs/"
seed: int = 1024
seeds: Optional[List[int]] = None
device_str: Optional[str] = None
device = None
def __post_init__(self):
pass
@staticmethod
def add_cli_args(parser: argparse.ArgumentParser):
parser.add_argument(
"--use-v1-text-encoder",
action="store_true",
help="Use the v1 text encoder",
)
parser.add_argument(
"--use-v1-vae",
action="store_true",
help="Use the v1 vae",
)
parser.add_argument(
"--use-v1-transformer",
action="store_true",
help="Use the v1 transformer",
)
# Model and path configuration
parser.add_argument(
"--model-path",
@@ -155,6 +179,20 @@ class InferenceArgs:
type=str,
help="Directory containing StepVideo model",
)
# HuggingFace specific parameters
parser.add_argument(
"--trust-remote-code",
action="store_true",
default=InferenceArgs.trust_remote_code,
help="Trust remote code when loading HuggingFace models",
)
parser.add_argument(
"--revision",
type=str,
default=InferenceArgs.revision,
help="The specific model version to use (can be a branch name, tag name, or commit id)",
)
# Parallelism
parser.add_argument(
@@ -7,20 +7,16 @@ This module provides classes and functions for running inference with diffusion
import os
import time
import torch
import numpy as np
from typing import Any, Dict, List, Optional, Union, Type
from PIL import Image
import uuid
import random
from typing import Any, Dict
from fastvideo.inference_args import InferenceArgs
from fastvideo.pipelines import ComposedPipelineBase, build_pipeline
from fastvideo.pipelines.pipeline_registry import PipelineRegistry
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.logger import init_logger
# TODO(will): remove
from fastvideo.models.hunyuan.utils.data_utils import align_to
from fastvideo.models.hunyuan.constants import NEGATIVE_PROMPT
from fastvideo.v1.inference_args import InferenceArgs
from fastvideo.v1.pipelines import ComposedPipelineBase, build_pipeline
from fastvideo.v1.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.v1.logger import init_logger
# TODO(will): remove, check if this is hunyuan specific
from fastvideo.v1.utils import align_to
# TODO(will): remove, move this to hunyuan stage
from fastvideo.v1.pipelines.implementations.hunyuan.constants import NEGATIVE_PROMPT
logger = init_logger(__name__)
@@ -72,6 +68,11 @@ class InferenceEngine:
"""
try:
logger.info(f"Building pipeline...")
# TODO(will): probably a better place to set device_str?
local_rank = os.environ.get("LOCAL_RANK", 0)
device_str = f"cuda:{local_rank}"
inference_args.device_str = device_str
inference_args.device = torch.device(device_str)
# TODO(will): I don't really like this api.
# it should be something closer to pipeline_cls.from_pretrained(...)
# this way for training we can just do pipeline_cls.from_pretrained(
@@ -153,7 +154,7 @@ class InferenceEngine:
negative_prompt = [negative_prompt.strip()]
# from fastvideo.models.hunyuan.diffusion.schedulers import FlowMatchDiscreteScheduler
# from fastvideo.v1.models.hunyuan.diffusion.schedulers import FlowMatchDiscreteScheduler
# scheduler = FlowMatchDiscreteScheduler(
# shift=flow_shift,
# reverse=self.inference_args.flow_reverse,
@@ -190,9 +191,7 @@ class InferenceEngine:
# return
# sp_group = get_sp_group()
# local_rank = sp_group.rank
import os
local_rank = os.environ.get("LOCAL_RANK", 0)
device = torch.device(f"cuda:{local_rank}")
device = torch.device(inference_args.device_str)
batch = ForwardBatch(
prompt=prompt,
negative_prompt=negative_prompt,
@@ -11,8 +11,7 @@ from vllm.distributed import (divide, get_tensor_model_parallel_rank,
get_tensor_model_parallel_world_size)
from vllm.model_executor.custom_op import CustomOp
from vllm.model_executor.utils import set_weight_attrs
from vllm.platforms import current_platform
from vllm.utils import LazyDict
from fastvideo.v1.platforms import current_platform
@CustomOp.register("fatrelu_and_mul")
@@ -8,25 +8,25 @@ import torch
import torch.nn.functional as F
from torch.nn.parameter import Parameter, UninitializedParameter
from fastvideo.distributed import (divide, get_tensor_model_parallel_rank,
from fastvideo.v1.distributed import (divide, get_tensor_model_parallel_rank,
get_tensor_model_parallel_world_size,
split_tensor_along_last_dim,
tensor_model_parallel_all_gather,
tensor_model_parallel_all_reduce)
from fastvideo.logger import init_logger
from fastvideo.v1.logger import init_logger
# TODO(will): remove this import by copying the definition from vLLM then
# manually import each quantization method we want to use. Refer to SGLang
from vllm.model_executor.layers.quantization.base_config import (
QuantizationConfig, QuantizeMethodBase)
# yapf: disable
from fastvideo.models.parameter import (BasevLLMParameter,
from fastvideo.v1.models.parameter import (BasevLLMParameter,
BlockQuantScaleParameter,
PackedColumnParameter,
PackedvLLMParameter,
PerTensorScaleParameter,
RowvLLMParameter)
# yapf: enable
from fastvideo.models.utils import set_weight_attrs
from fastvideo.v1.models.utils import set_weight_attrs
logger = init_logger(__name__)
@@ -1,5 +1,5 @@
from fastvideo.layers.linear import ReplicatedLinear
from fastvideo.layers.activation import get_act_fn
from fastvideo.v1.layers.linear import ReplicatedLinear
from fastvideo.v1.layers.activation import get_act_fn
import torch
import torch.nn as nn
from typing import Optional
@@ -30,7 +30,7 @@ import torch.nn as nn
from transformers import PretrainedConfig
from vllm.model_executor.custom_op import CustomOp
from fastvideo.distributed.parallel_state import get_sp_group
from fastvideo.v1.distributed.parallel_state import get_sp_group
def _rotate_neox(x: torch.Tensor) -> torch.Tensor:
x1 = x[..., :x.shape[-1] // 2]
@@ -500,4 +500,42 @@ def get_rotary_pos_embed(
sp_rank=sp_rank,
sp_world_size=sp_world_size
)
return freqs_cos, freqs_sin
return freqs_cos, freqs_sin
_ROPE_DICT: Dict[Tuple, RotaryEmbedding] = {}
def get_rope(
head_size: int,
rotary_dim: int,
max_position: int,
base: int,
is_neox_style: bool = True,
rope_scaling: Optional[Dict[str, Any]] = None,
dtype: Optional[torch.dtype] = None,
partial_rotary_factor: float = 1.0,
) -> RotaryEmbedding:
if dtype is None:
dtype = torch.get_default_dtype()
if rope_scaling is not None:
# Transforms every value that is a list into a tuple for caching calls
rope_scaling_tuple = {
k: tuple(v) if isinstance(v, list) else v
for k, v in rope_scaling.items()
}
rope_scaling_args = tuple(rope_scaling_tuple.items())
else:
rope_scaling_args = None
if partial_rotary_factor < 1.0:
rotary_dim = int(rotary_dim * partial_rotary_factor)
key = (head_size, rotary_dim, max_position, base, is_neox_style,
rope_scaling_args, dtype)
if key in _ROPE_DICT:
return _ROPE_DICT[key]
if rope_scaling is None:
rotary_emb = RotaryEmbedding(head_size, rotary_dim, max_position, base,
is_neox_style, dtype)
else:
raise ValueError(f"Unknown RoPE scaling {rope_scaling}")
_ROPE_DICT[key] = rotary_emb
return rotary_emb
@@ -1,10 +1,10 @@
import torch
import torch.nn as nn
import math
from fastvideo.layers.activation import get_act_fn
from fastvideo.layers.linear import ReplicatedLinear
from fastvideo.v1.layers.activation import get_act_fn
from fastvideo.v1.layers.linear import ReplicatedLinear
from typing import Optional
from fastvideo.layers.mlp import MLP
from fastvideo.v1.layers.mlp import MLP
class PatchEmbed(nn.Module):
"""2D Image to Patch Embedding
@@ -7,14 +7,14 @@ import torch
import torch.nn.functional as F
from torch.nn.parameter import Parameter, UninitializedParameter
from vllm.distributed import (divide, get_tensor_model_parallel_rank,
from fastvideo.v1.distributed import (divide, get_tensor_model_parallel_rank,
get_tensor_model_parallel_world_size,
tensor_model_parallel_all_reduce)
from vllm.model_executor.layers.quantization.base_config import (
QuantizationConfig, QuantizeMethodBase, method_has_implemented_embedding)
from vllm.model_executor.parameter import BasevLLMParameter
from vllm.model_executor.utils import set_weight_attrs
from vllm.platforms import current_platform
from fastvideo.v1.models.parameter import BasevLLMParameter
from fastvideo.v1.models.utils import set_weight_attrs
from fastvideo.v1.platforms import current_platform
DEFAULT_VOCAB_PADDING_SIZE = 64
View File
@@ -3,17 +3,17 @@
# Copyright 2025 The FastVideo Authors.
from collections import defaultdict
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Type
from itertools import chain
import torch
from torch import nn
from torch.distributed import DeviceMesh, init_device_mesh
from fastvideo.distributed.parallel_state import get_sequence_model_parallel_world_size
from fastvideo.v1.distributed.parallel_state import get_sequence_model_parallel_world_size
from torch.distributed._composable.fsdp import CPUOffloadPolicy, fully_shard
from torch.distributed._tensor import distribute_tensor
from torch.nn.modules.module import _IncompatibleKeys
from vllm.model_executor.model_loader.weight_utils import safetensors_weights_iterator
from fastvideo.models.dits.registry import ModelRegistry
import contextlib
import re
@@ -79,14 +79,13 @@ def get_param_names_mapping(mapping_dict: Dict[str, str]) -> Callable[[str], str
# TODO(PY): add compile option
def load_fsdp_model(
model_name: str,
model_cls: Type[nn.Module],
init_params: Dict[str, Any],
weight_dir_list: List[str],
device: torch.device,
cpu_offload: bool = False,
default_dtype: Optional[torch.dtype] = torch.bfloat16,
) -> torch.nn.Module:
model_cls, _ = ModelRegistry.resolve_model_cls(model_name)
with set_default_dtype(default_dtype), torch.device("meta"):
model = model_cls(**init_params)
device_mesh = init_device_mesh(
+342
View File
@@ -0,0 +1,342 @@
# SPDX-License-Identifier: Apache-2.0
# Adapted from vllm
# Copyright 2023 The vLLM Authors.
# Copyright 2025 The FastVideo Authors.
# ruff: noqa: SIM117
import collections
import copy
import dataclasses
import fnmatch
import glob
import inspect
import itertools
import math
import os
import time
import warnings
from abc import ABC, abstractmethod
from contextlib import contextmanager
from typing import (Any, Callable, Dict, Generator, Iterable, List, Optional,
Tuple, cast)
import huggingface_hub
import torch
from torch import nn
from transformers.utils import SAFE_WEIGHTS_INDEX_NAME
from fastvideo.v1.logger import init_logger
from fastvideo.v1.loader.utils import set_default_torch_dtype
from fastvideo.v1.loader.weight_utils import (
download_safetensors_index_file_from_hf, download_weights_from_hf,
filter_duplicate_safetensors_files, filter_files_not_needed_for_inference,
pt_weights_iterator,
safetensors_weights_iterator)
from fastvideo.v1.platforms import current_platform
from fastvideo.v1.models.registry import ModelRegistry
from fastvideo.v1.inference_args import InferenceArgs
@contextmanager
def device_loading_context(module: torch.nn.Module,
target_device: torch.device):
if target_device.type == "cpu":
# If target is CPU, no need to move anything
yield module
return
original_device_states: Dict[str, torch.device] = {}
# Store original device states and move parameters to GPU if they're on CPU
for name, p in module.named_parameters():
if p.device.type == "cpu":
original_device_states[name] = p.device
p.data = p.data.to(target_device)
# Parameters already on target device are not touched
try:
yield module
finally:
# Restore parameters to their original devices, ignoring new parameters
pin_memory = True
for name, p in module.named_parameters():
if name in original_device_states:
original_device: torch.device = original_device_states[name]
if original_device.type == "cpu":
# `torch.empty_like` does not support `pin_memory` argument
cpu_data = torch.empty_strided(
size=p.data.size(),
stride=p.data.stride(),
dtype=p.data.dtype,
layout=p.data.layout,
device="cpu",
pin_memory=pin_memory,
)
cpu_data.copy_(p.data)
p.data = cpu_data
else:
p.data = p.data.to(original_device)
# New parameters or parameters already on target device are untouched
logger = init_logger(__name__)
def _initialize_model(
model_path: str,
model_config,
) -> nn.Module:
"""Initialize a model with the given configurations.
Args:
model_name: The name of the model architecture
model_path: Path to the model directory
component_name: Optional component name (e.g., "text_encoder", "vae")
If provided, will load the config from the component subdirectory
Returns:
The initialized model
"""
architectures = getattr(model_config, "architectures", [])
if len(architectures) == 0:
raise ValueError("Model config does not contain a valid model architecture")
logger.info("Trying to load model from diffusers format")
class_name = model_config.pop("_class_name")
architectures = [class_name]
if architectures is None:
raise ValueError("Model config does not contain a valid model architecture")
model_cls, _ = ModelRegistry.resolve_model_cls(architectures)
logger.info(f"Loading model_cls: {model_cls}")
return model_cls(model_config)
class BaseModelLoader(ABC):
"""Base class for model loaders."""
@abstractmethod
def download_model(self, inference_args: InferenceArgs) -> None:
"""Download a model so that it can be immediately loaded."""
raise NotImplementedError
@abstractmethod
def load_model(self, *, inference_args: InferenceArgs) -> nn.Module:
"""Load a model with the given configurations."""
raise NotImplementedError
class DefaultModelLoader(BaseModelLoader):
"""Model loader that can load different file types from disk."""
@dataclasses.dataclass
class Source:
"""A source for weights."""
model_or_path: str
"""The model ID or path."""
revision: Optional[str]
"""The optional model revision."""
prefix: str = ""
"""A prefix to prepend to all weights."""
fall_back_to_pt: bool = True
"""Whether .pt weights can be used."""
allow_patterns_overrides: Optional[list[str]] = None
"""If defined, weights will load exclusively using these patterns."""
counter_before_loading_weights: float = 0.0
counter_after_loading_weights: float = 0.0
def _prepare_weights(
self,
model_name_or_path: str,
revision: Optional[str],
fall_back_to_pt: bool,
allow_patterns_overrides: Optional[list[str]],
) -> Tuple[str, List[str], bool]:
"""Prepare weights for the model.
If the model is not local, it will be downloaded."""
# model_name_or_path = (self._maybe_download_from_modelscope(
# model_name_or_path, revision) or model_name_or_path)
is_local = os.path.isdir(model_name_or_path)
assert is_local, "Model path must be a local directory"
# load_format = self.load_config.load_format
# load_format = LoadFormat.AUTO
use_safetensors = False
index_file = SAFE_WEIGHTS_INDEX_NAME
allow_patterns = ["*.safetensors", "*.bin"]
# Some quantized models use .pt files for storing the weights.
# if load_format == LoadFormat.AUTO:
# allow_patterns = ["*.safetensors", "*.bin"]
# elif load_format == LoadFormat.SAFETENSORS:
# use_safetensors = True
# allow_patterns = ["*.safetensors"]
# elif load_format == LoadFormat.MISTRAL:
# use_safetensors = True
# allow_patterns = ["consolidated*.safetensors"]
# index_file = "consolidated.safetensors.index.json"
# elif load_format == LoadFormat.PT:
# allow_patterns = ["*.pt"]
# elif load_format == LoadFormat.NPCACHE:
# allow_patterns = ["*.bin"]
# else:
# raise ValueError(f"Unknown load_format: {load_format}")
if fall_back_to_pt:
allow_patterns += ["*.pt"]
if allow_patterns_overrides is not None:
allow_patterns = allow_patterns_overrides
if not is_local:
hf_folder = download_weights_from_hf(
model_name_or_path,
self.load_config.download_dir,
allow_patterns,
revision,
ignore_patterns=self.load_config.ignore_patterns,
)
else:
hf_folder = model_name_or_path
hf_weights_files: List[str] = []
for pattern in allow_patterns:
hf_weights_files += glob.glob(os.path.join(hf_folder, pattern))
if len(hf_weights_files) > 0:
if pattern == "*.safetensors":
use_safetensors = True
break
if use_safetensors:
# For models like Mistral-7B-Instruct-v0.3
# there are both sharded safetensors files and a consolidated
# safetensors file. Using both breaks.
# Here, we download the `model.safetensors.index.json` and filter
# any files not found in the index.
if not is_local:
download_safetensors_index_file_from_hf(
model_name_or_path,
index_file,
self.load_config.download_dir,
revision,
)
hf_weights_files = filter_duplicate_safetensors_files(
hf_weights_files, hf_folder, index_file)
else:
hf_weights_files = filter_files_not_needed_for_inference(
hf_weights_files)
if len(hf_weights_files) == 0:
raise RuntimeError(
f"Cannot find any model weights with `{model_name_or_path}`")
return hf_folder, hf_weights_files, use_safetensors
def _get_weights_iterator(
self, source: "Source"
) -> Generator[Tuple[str, torch.Tensor], None, None]:
"""Get an iterator for the model weights based on the load format."""
hf_folder, hf_weights_files, use_safetensors = self._prepare_weights(
source.model_or_path, source.revision, source.fall_back_to_pt,
source.allow_patterns_overrides)
if use_safetensors:
weights_iterator = safetensors_weights_iterator(hf_weights_files)
else:
weights_iterator = pt_weights_iterator(hf_weights_files)
# if current_platform.is_tpu():
# # In PyTorch XLA, we should call `xm.mark_step` frequently so that
# # not too many ops are accumulated in the XLA program.
# import torch_xla.core.xla_model as xm
# def _xla_weights_iterator(iterator: Generator):
# for weights in iterator:
# yield weights
# xm.mark_step()
# weights_iterator = _xla_weights_iterator(weights_iterator)
if self.counter_before_loading_weights == 0.0:
self.counter_before_loading_weights = time.perf_counter()
# Apply the prefix.
return ((source.prefix + name, tensor)
for (name, tensor) in weights_iterator)
def _get_all_weights(
self,
model_config: Dict[str, Any],
model: nn.Module,
) -> Generator[Tuple[str, torch.Tensor], None, None]:
primary_weights = DefaultModelLoader.Source(
model_config.model,
"",
prefix="",
fall_back_to_pt=getattr(model, "fall_back_to_pt_during_load",
True),
allow_patterns_overrides=getattr(model, "allow_patterns_overrides",
None),
)
yield from self._get_weights_iterator(primary_weights)
secondary_weights = cast(
Iterable[DefaultModelLoader.Source],
getattr(model, "secondary_weights", ()),
)
for source in secondary_weights:
yield from self._get_weights_iterator(source)
def download_model(self, inference_args: InferenceArgs) -> None:
raise NotImplementedError
# self._prepare_weights(inference_args.model,
# inference_args.revision,
# fall_back_to_pt=True,
# allow_patterns_overrides=None)
def load_model(self, model_path: str,
model_config: Dict[str, Any],
inference_args: InferenceArgs) -> nn.Module:
logger.info(f"Loading model on device: {inference_args.device_str}")
target_device = torch.device(inference_args.device_str)
# TODO(will): add support for other dtypes
with set_default_torch_dtype(torch.float16):
with target_device:
model = _initialize_model(model_path, model_config)
weights_to_load = {name for name, _ in model.named_parameters()}
model_config.model = model_path
loaded_weights = model.load_weights(
self._get_all_weights(model_config, model))
self.counter_after_loading_weights = time.perf_counter()
logger.info(
"Loading weights took %.2f seconds",
self.counter_after_loading_weights -
self.counter_before_loading_weights)
# We only enable strict check for non-quantized models
# that have loaded weights tracking currently.
# if loaded_weights is not None:
weights_not_loaded = weights_to_load - loaded_weights
if weights_not_loaded:
raise ValueError(
"Following weights were not initialized from "
f"checkpoint: {weights_not_loaded}")
# TODO(will): add support for training/finetune
return model.eval()
def get_model_loader() -> BaseModelLoader:
"""Get a model loader based on the load format."""
# if isinstance(load_config.load_format, type):
# return load_config.load_format(load_config)
return DefaultModelLoader()
+184
View File
@@ -0,0 +1,184 @@
# SPDX-License-Identifier: Apache-2.0
"""Utilities for selecting and loading models."""
import contextlib
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple, Type
import torch
import transformers
from torch import nn
from transformers.dynamic_module_utils import get_class_from_dynamic_module
from vllm.config import ModelConfig, ModelImpl
from fastvideo.v1.logger import init_logger
from vllm.model_executor.models import ModelRegistry
from vllm.model_executor.models.adapters import (as_classification_model,
as_embedding_model,
as_reward_model)
logger = init_logger(__name__)
@contextlib.contextmanager
def set_default_torch_dtype(dtype: torch.dtype):
"""Sets the default torch dtype to the given dtype."""
old_dtype = torch.get_default_dtype()
torch.set_default_dtype(dtype)
yield
torch.set_default_dtype(old_dtype)
def load_hf_config_from_subdir(model_path: str, component_name: str, trust_remote_code: bool = False, revision: Optional[str] = None):
"""
Load a HuggingFace config from a component subdirectory.
Args:
model_path: Path to the model directory
component_name: Name of the component subdirectory (e.g., "text_encoder", "vae")
trust_remote_code: Whether to trust remote code when loading the config
revision: Optional revision to use when loading from HuggingFace Hub
Returns:
The loaded HuggingFace config
"""
import os
from transformers import AutoConfig
component_path = os.path.join(model_path, component_name)
if not os.path.exists(component_path):
raise ValueError(f"Component directory {component_path} does not exist")
config_path = os.path.join(component_path, "config.json")
if not os.path.exists(config_path):
raise ValueError(f"Config file {config_path} does not exist")
logger.info(f"Loading config from {config_path}")
config = AutoConfig.from_pretrained(
component_path,
trust_remote_code=trust_remote_code,
revision=revision
)
return config
def is_transformers_impl_compatible(
arch: str,
module: Optional[transformers.PreTrainedModel] = None) -> bool:
mod = module or getattr(transformers, arch, None)
if mod is None:
return False
if hasattr(mod, "supports_backend"):
return mod.is_backend_compatible()
else:
return mod._supports_flex_attn
def resolve_transformers_fallback(model_config: ModelConfig,
architectures: list[str]):
for i, arch in enumerate(architectures):
if arch == "TransformersModel":
continue
auto_map: dict[str, str] = getattr(model_config.hf_config, "auto_map",
None) or dict()
# Make sure that config class is always initialized before model class,
# otherwise the model class won't be able to access the config class,
# the expected auto_map should have correct order like:
# "auto_map": {
# "AutoConfig": "<your-repo-name>--<config-name>",
# "AutoModel": "<your-repo-name>--<config-name>",
# "AutoModelFor<Task>": "<your-repo-name>--<config-name>",
# },
auto_modules = {
name: get_class_from_dynamic_module(module, model_config.model)
for name, module in sorted(auto_map.items(), key=lambda x: x[0])
}
custom_model_module = auto_modules.get("AutoModel")
# TODO(Isotr0py): Further clean up these raises.
# perhaps handled them in _ModelRegistry._raise_for_unsupported?
if model_config.model_impl == ModelImpl.TRANSFORMERS:
if not is_transformers_impl_compatible(arch, custom_model_module):
raise ValueError(
f"The Transformers implementation of {arch} is not "
"compatible with vLLM.")
architectures[i] = "TransformersModel"
if model_config.model_impl == ModelImpl.AUTO:
if not is_transformers_impl_compatible(arch, custom_model_module):
raise ValueError(
f"{arch} has no vLLM implementation and the Transformers "
"implementation is not compatible with vLLM.")
logger.warning(
"%s has no vLLM implementation, falling back to Transformers "
"implementation. Some features may not be supported and "
"performance may not be optimal.", arch)
architectures[i] = "TransformersModel"
return architectures
def get_model_architecture(
model_config: ModelConfig) -> Tuple[Type[nn.Module], str]:
architectures = getattr(model_config.hf_config, "architectures", [])
# Special handling for quantized Mixtral.
# FIXME(woosuk): This is a temporary hack.
mixtral_supported = [
"fp8", "compressed-tensors", "gptq_marlin", "awq_marlin"
]
if (model_config.quantization is not None
and model_config.quantization not in mixtral_supported
and "MixtralForCausalLM" in architectures):
architectures = ["QuantMixtralForCausalLM"]
vllm_supported_archs = ModelRegistry.get_supported_archs()
is_vllm_supported = any(arch in vllm_supported_archs
for arch in architectures)
if (not is_vllm_supported
or model_config.model_impl == ModelImpl.TRANSFORMERS):
architectures = resolve_transformers_fallback(model_config,
architectures)
model_cls, arch = ModelRegistry.resolve_model_cls(architectures)
if model_config.task == "embed":
model_cls = as_embedding_model(model_cls)
elif model_config.task == "classify":
model_cls = as_classification_model(model_cls)
elif model_config.task == "reward":
model_cls = as_reward_model(model_cls)
return model_cls, arch
def get_architecture_class_name(model_config: ModelConfig) -> str:
return get_model_architecture(model_config)[1]
@dataclass
class ParamMapping:
"""
A class to handle parameter mapping for model weight loading.
It creates a bidirectional mapping between packed parameters and their
constituent parts.
"""
packed_mapping: Dict[str, List[str]]
inverse_packed_mapping: Dict[str, Tuple[str,
int]] = field(default_factory=dict)
def __post_init__(self):
for packed_name, sub_params in self.packed_mapping.items():
# Skip self-contained cases (e.g., {"W_pack": ["W_pack"]})
if len(sub_params) == 1 and sub_params[0] == packed_name:
continue
for index, param_name in enumerate(sub_params):
self.inverse_packed_mapping[param_name] = (
packed_name,
index,
)
def get_sub_modules(self,
module_name: str) -> Optional[Tuple[str, List[str]]]:
for key, value in self.packed_mapping.items():
if module_name.endswith(key):
return key, value
return None
@@ -25,23 +25,23 @@ from huggingface_hub import HfFileSystem, hf_hub_download, snapshot_download
from safetensors.torch import load_file, safe_open, save_file
from tqdm.auto import tqdm
from vllm.config import LoadConfig, ModelConfig
from vllm.distributed import get_tensor_model_parallel_rank
from vllm.logger import init_logger
from vllm.model_executor.layers.quantization import (QuantizationConfig,
get_quantization_config)
from vllm.platforms import current_platform
from vllm.utils import PlaceholderModule
# from vllm.config import LoadConfig, ModelConfig
from fastvideo.v1.distributed import get_tensor_model_parallel_rank
from fastvideo.v1.logger import init_logger
# from vllm.model_executor.layers.quantization import (QuantizationConfig,
# get_quantization_config)
from fastvideo.v1.platforms import current_platform
# from vllm.utils import PlaceholderModule
try:
from runai_model_streamer import SafetensorsStreamer
except (ImportError, OSError):
# see https://github.com/run-ai/runai-model-streamer/issues/26
# OSError will be raised on arm64 platform
runai_model_streamer = PlaceholderModule(
"runai_model_streamer") # type: ignore[assignment]
SafetensorsStreamer = runai_model_streamer.placeholder_attr(
"SafetensorsStreamer")
# try:
# from runai_model_streamer import SafetensorsStreamer
# except (ImportError, OSError):
# # see https://github.com/run-ai/runai-model-streamer/issues/26
# # OSError will be raised on arm64 platform
# runai_model_streamer = PlaceholderModule(
# "runai_model_streamer") # type: ignore[assignment]
# SafetensorsStreamer = runai_model_streamer.placeholder_attr(
# "SafetensorsStreamer")
logger = init_logger(__name__)
@@ -137,89 +137,89 @@ def convert_bin_to_safetensor_file(
# TODO(woosuk): Move this to other place.
def get_quant_config(model_config: ModelConfig,
load_config: LoadConfig) -> QuantizationConfig:
# def get_quant_config(model_config: ModelConfig,
# load_config: LoadConfig) -> QuantizationConfig:
quant_cls = get_quantization_config(model_config.quantization)
# quant_cls = get_quantization_config(model_config.quantization)
# GGUF doesn't have config file
if model_config.quantization == "gguf":
return quant_cls.from_config({})
# # GGUF doesn't have config file
# if model_config.quantization == "gguf":
# return quant_cls.from_config({})
# Read the quantization config from the HF model config, if available.
hf_quant_config = getattr(model_config.hf_config, "quantization_config",
None)
# some vision model may keep quantization_config in their text_config
hf_text_config = getattr(model_config.hf_config, "text_config", None)
if hf_quant_config is None and hf_text_config is not None:
hf_quant_config = getattr(hf_text_config, "quantization_config", None)
if hf_quant_config is None:
# compressed-tensors uses a compressions_config
hf_quant_config = getattr(model_config.hf_config, "compression_config",
None)
if hf_quant_config is not None:
return quant_cls.from_config(hf_quant_config)
# In case of bitsandbytes/QLoRA, get quant config from the adapter model.
if model_config.quantization == "bitsandbytes":
if (not load_config.model_loader_extra_config
or "qlora_adapter_name_or_path"
not in load_config.model_loader_extra_config):
return quant_cls.from_config({"adapter_name_or_path": ""})
model_name_or_path = load_config.model_loader_extra_config[
"qlora_adapter_name_or_path"]
# # Read the quantization config from the HF model config, if available.
# hf_quant_config = getattr(model_config.hf_config, "quantization_config",
# None)
# # some vision model may keep quantization_config in their text_config
# hf_text_config = getattr(model_config.hf_config, "text_config", None)
# if hf_quant_config is None and hf_text_config is not None:
# hf_quant_config = getattr(hf_text_config, "quantization_config", None)
# if hf_quant_config is None:
# # compressed-tensors uses a compressions_config
# hf_quant_config = getattr(model_config.hf_config, "compression_config",
# None)
# if hf_quant_config is not None:
# return quant_cls.from_config(hf_quant_config)
# # In case of bitsandbytes/QLoRA, get quant config from the adapter model.
# if model_config.quantization == "bitsandbytes":
# if (not load_config.model_loader_extra_config
# or "qlora_adapter_name_or_path"
# not in load_config.model_loader_extra_config):
# return quant_cls.from_config({"adapter_name_or_path": ""})
# model_name_or_path = load_config.model_loader_extra_config[
# "qlora_adapter_name_or_path"]
else:
model_name_or_path = model_config.model
is_local = os.path.isdir(model_name_or_path)
if not is_local:
# Download the config files.
with get_lock(model_name_or_path, load_config.download_dir):
hf_folder = snapshot_download(
model_name_or_path,
revision=model_config.revision,
allow_patterns="*.json",
cache_dir=load_config.download_dir,
local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
tqdm_class=DisabledTqdm,
)
else:
hf_folder = model_name_or_path
# else:
# model_name_or_path = model_config.model
# is_local = os.path.isdir(model_name_or_path)
# if not is_local:
# # Download the config files.
# with get_lock(model_name_or_path, load_config.download_dir):
# hf_folder = snapshot_download(
# model_name_or_path,
# revision=model_config.revision,
# allow_patterns="*.json",
# cache_dir=load_config.download_dir,
# local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
# tqdm_class=DisabledTqdm,
# )
# else:
# hf_folder = model_name_or_path
possible_config_filenames = quant_cls.get_config_filenames()
# possible_config_filenames = quant_cls.get_config_filenames()
# If the quantization config is not found, use the default config.
if not possible_config_filenames:
return quant_cls()
# # If the quantization config is not found, use the default config.
# if not possible_config_filenames:
# return quant_cls()
config_files = glob.glob(os.path.join(hf_folder, "*.json"))
# config_files = glob.glob(os.path.join(hf_folder, "*.json"))
quant_config_files = [
f for f in config_files if any(
f.endswith(x) for x in possible_config_filenames)
]
if len(quant_config_files) == 0:
raise ValueError(
f"Cannot find the config file for {model_config.quantization}")
if len(quant_config_files) > 1:
raise ValueError(
f"Found multiple config files for {model_config.quantization}: "
f"{quant_config_files}")
# quant_config_files = [
# f for f in config_files if any(
# f.endswith(x) for x in possible_config_filenames)
# ]
# if len(quant_config_files) == 0:
# raise ValueError(
# f"Cannot find the config file for {model_config.quantization}")
# if len(quant_config_files) > 1:
# raise ValueError(
# f"Found multiple config files for {model_config.quantization}: "
# f"{quant_config_files}")
quant_config_file = quant_config_files[0]
with open(quant_config_file) as f:
config = json.load(f)
# quant_config_file = quant_config_files[0]
# with open(quant_config_file) as f:
# config = json.load(f)
if model_config.quantization == "bitsandbytes":
config["adapter_name_or_path"] = model_name_or_path
elif model_config.quantization == "modelopt":
if config["producer"]["name"] == "modelopt":
return quant_cls.from_config(config)
else:
raise ValueError(
f"Unsupported quantization config"
f" found for {model_config.quantization} in {f}.")
# if model_config.quantization == "bitsandbytes":
# config["adapter_name_or_path"] = model_name_or_path
# elif model_config.quantization == "modelopt":
# if config["producer"]["name"] == "modelopt":
# return quant_cls.from_config(config)
# else:
# raise ValueError(
# f"Unsupported quantization config"
# f" found for {model_config.quantization} in {f}.")
return quant_cls.from_config(config)
# return quant_cls.from_config(config)
def download_weights_from_hf(
@@ -436,21 +436,21 @@ def safetensors_weights_iterator(
yield name, param
def runai_safetensors_weights_iterator(
hf_weights_files: List[str]
) -> Generator[Tuple[str, torch.Tensor], None, None]:
"""Iterate over the weights in the model safetensor files."""
enable_tqdm = not torch.distributed.is_initialized(
) or torch.distributed.get_rank() == 0
with SafetensorsStreamer() as streamer:
for st_file in tqdm(
hf_weights_files,
desc="Loading safetensors using Runai Model Streamer",
disable=not enable_tqdm,
bar_format=_BAR_FORMAT,
):
streamer.stream_file(st_file)
yield from streamer.get_tensors()
# def runai_safetensors_weights_iterator(
# hf_weights_files: List[str]
# ) -> Generator[Tuple[str, torch.Tensor], None, None]:
# """Iterate over the weights in the model safetensor files."""
# enable_tqdm = not torch.distributed.is_initialized(
# ) or torch.distributed.get_rank() == 0
# with SafetensorsStreamer() as streamer:
# for st_file in tqdm(
# hf_weights_files,
# desc="Loading safetensors using Runai Model Streamer",
# disable=not enable_tqdm,
# bar_format=_BAR_FORMAT,
# ):
# streamer.stream_file(st_file)
# yield from streamer.get_tensors()
def pt_weights_iterator(
@@ -4,7 +4,7 @@
# Copyright 2023 The vLLM Authors.
# Copyright 2023 The FastVideo Authors.
"""Logging configuration for fastvideo."""
"""Logging configuration for fastvideo.v1."""
import datetime
import json
import logging
@@ -17,7 +17,7 @@ from os import path
from types import MethodType
from typing import Any, Optional, cast
import fastvideo.envs as envs
import fastvideo.v1.envs as envs
FASTVIDEO_CONFIGURE_LOGGING = envs.FASTVIDEO_CONFIGURE_LOGGING
FASTVIDEO_LOGGING_CONFIG_PATH = envs.FASTVIDEO_LOGGING_CONFIG_PATH
@@ -31,7 +31,7 @@ _DATE_FORMAT = "%m-%d %H:%M:%S"
DEFAULT_LOGGING_CONFIG = {
"formatters": {
"fastvideo": {
"class": "fastvideo.logging_utils.NewLineFormatter",
"class": "fastvideo.v1.logging_utils.NewLineFormatter",
"datefmt": _DATE_FORMAT,
"format": _FORMAT,
},
@@ -124,8 +124,8 @@ def _configure_fastvideo_root_logger() -> None:
for formatter in logging_config.get("formatters", {}).values():
# This provides backwards compatibility after #10134.
if formatter.get("class") == "fastvideo.logging.NewLineFormatter":
formatter["class"] = "fastvideo.logging_utils.NewLineFormatter"
if formatter.get("class") == "fastvideo.v1.logging.NewLineFormatter":
formatter["class"] = "fastvideo.v1.logging_utils.NewLineFormatter"
if logging_config:
dictConfig(logging_config)
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
from fastvideo.logging_utils.formatter import NewLineFormatter
from fastvideo.v1.logging_utils.formatter import NewLineFormatter
__all__ = [
"NewLineFormatter",
+57
View File
@@ -0,0 +1,57 @@
# SPDX-License-Identifier: Apache-2.0
import torch.nn as nn
from typing import Dict, Any
from fastvideo.v1.loader.loader import get_model_loader
from fastvideo.v1.inference_args import InferenceArgs
from fastvideo.v1.logger import init_logger
import os
from transformers import PretrainedConfig
logger = init_logger(__name__)
def get_transformers_model(model_config: PretrainedConfig, model_path: str, inference_args: InferenceArgs) -> nn.Module:
"""
Load a transformers model.
Args:
architecture: The model architecture name
model_path: Path to the model directory
inference_args: Inference arguments
component_name: Optional component name (e.g., "text_encoder", "vae")
If provided, will load the config from the component subdirectory
Returns:
The loaded model
"""
model_loader = get_model_loader()
return model_loader.load_model(model_config, model_path, inference_args)
def get_diffusers_model(model_config: Dict[str, Any], model_path: str, inference_args: InferenceArgs) -> nn.Module:
"""
Load a diffusers model.
"""
raise NotImplementedError("Diffusers models are not supported yet.")
def get_scheduler(module_path: str, architecture: str, inference_args: InferenceArgs) -> Dict:
"""Create a scheduler based on the inference args. Can be overridden by subclasses."""
if hasattr(inference_args, 'denoise_type') and inference_args.denoise_type == "flow":
# TODO(will): add schedulers to register or create a new scheduler registry
# TODO(will): default to config file but allow override through
# inference args. Currently only uses inference args.
from fastvideo.v1.models.schedulers.scheduling_flow_match_euler_discrete import FlowMatchDiscreteScheduler
return FlowMatchDiscreteScheduler(
shift=inference_args.flow_shift,
reverse=inference_args.flow_reverse,
solver=inference_args.flow_solver,
)
else:
raise ValueError(f"Invalid denoise type: {inference_args.denoise_type}")
__all__ = [
"set_random_seed",
"BasevLLMParameter",
"PackedvLLMParameter",
"get_model",
"get_scheduler",
]
@@ -1,16 +1,16 @@
import torch
import torch.nn as nn
from typing import Optional, Tuple, List
from fastvideo.attention.distributed_attn import DistributedAttention, LocalAttention
from fastvideo.layers.linear import ReplicatedLinear
from fastvideo.layers.layernorm import LayerNormScaleShift, ScaleResidual, ScaleResidualLayerNormScaleShift
from fastvideo.layers.visual_embedding import PatchEmbed, TimestepEmbedder, ModulateProjection, unpatchify
from fastvideo.layers.rotary_embedding import _apply_rotary_emb, get_rotary_pos_embed
from fastvideo.distributed.parallel_state import get_sequence_model_parallel_world_size, get_sequence_model_parallel_rank
# TODO: RMSNorm ....
from fastvideo.models.hunyuan.modules.norm_layers import RMSNorm
from fastvideo.layers.mlp import MLP
from fastvideo.models.dits.base import BaseDiT
from fastvideo.v1.attention.flash_attn import DistributedAttention, LocalAttention
from fastvideo.v1.layers.linear import ReplicatedLinear
from fastvideo.v1.layers.layernorm import LayerNormScaleShift, ScaleResidual, ScaleResidualLayerNormScaleShift
from fastvideo.v1.layers.visual_embedding import PatchEmbed, TimestepEmbedder, ModulateProjection, unpatchify
from fastvideo.v1.layers.rotary_embedding import _apply_rotary_emb, get_rotary_pos_embed
from fastvideo.v1.distributed.parallel_state import get_sequence_model_parallel_world_size, get_sequence_model_parallel_rank
# TODO(will-PY-refactor): RMSNorm ....
from fastvideo.v1.v0_reference_src.models.hunyuan.modules.norm_layers import RMSNorm
from fastvideo.v1.layers.mlp import MLP
from fastvideo.v1.models.dits.base import BaseDiT
@@ -329,7 +329,7 @@ class MMSingleStreamBlock(nn.Module):
class HunyuanVideoDiT(BaseDiT):
class HunyuanVideoTransformer3DModel(BaseDiT):
"""
HunyuanVideo Transformer backbone adapted for distributed training.
@@ -799,9 +799,8 @@ class FinalLayer(nn.Module):
)
def forward(self, x, c):
# What the fuck HF? Why you change the scale and shift order here???
# What the heck HF? Why you change the scale and shift order here???
scale, shift = self.adaLN_modulation(c).chunk(2, dim=-1)
x = self.norm_final(x) * (1.0 + scale.unsqueeze(1)) + shift.unsqueeze(1)
x, _ = self.linear(x)
return x
@@ -5,20 +5,30 @@ from typing import Iterable, Optional, Set, Tuple, Union
import torch
import torch.nn as nn
from transformers import CLIPVisionConfig
from transformers import CLIPVisionConfig, CLIPTextConfig
from transformers.modeling_outputs import BaseModelOutputWithPooling
# from transformers.modeling_attn_mask_utils import _create_4d_causal_attention_mask, _prepare_4d_attention_mask
from vllm.attention.layer import MultiHeadAttention
from fastvideo.distributed import divide, get_tensor_model_parallel_world_size
from fastvideo.layers.activation import get_act_fn
from fastvideo.layers.linear import (ColumnParallelLinear,
# from fastvideo.v1.attention.flash_attn import LocalAttention
from fastvideo.v1.distributed import divide, get_tensor_model_parallel_world_size
from fastvideo.v1.layers.activation import get_act_fn
from fastvideo.v1.layers.linear import (ColumnParallelLinear,
QKVParallelLinear,
RowParallelLinear)
from vllm.model_executor.layers.quantization import QuantizationConfig
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
# TODO: support quantization
# from vllm.model_executor.layers.quantization import QuantizationConfig
from fastvideo.v1.loader.weight_utils import default_weight_loader
from vllm.model_executor.models.interfaces import SupportsQuant
from .vision import VisionEncoderInfo, resolve_visual_encoder_outputs
from fastvideo.v1.logger import init_logger
logger = init_logger(__name__)
class QuantizationConfig:
pass
class CLIPEncoderInfo(VisionEncoderInfo[CLIPVisionConfig]):
@@ -88,6 +98,48 @@ class CLIPVisionEmbeddings(nn.Module):
return embeddings
class CLIPTextEmbeddings(nn.Module):
def __init__(self, config: CLIPTextConfig):
super().__init__()
self.config = config
embed_dim = config.hidden_size
self.token_embedding = nn.Embedding(config.vocab_size, embed_dim)
self.position_embedding = nn.Embedding(config.max_position_embeddings, embed_dim)
# position_ids (1, len position emb) is contiguous in memory and exported when serialized
self.register_buffer(
"position_ids", torch.arange(config.max_position_embeddings).expand((1, -1)), persistent=False
)
def forward(
self,
input_ids: Optional[torch.LongTensor] = None,
position_ids: Optional[torch.LongTensor] = None,
inputs_embeds: Optional[torch.FloatTensor] = None,
) -> torch.Tensor:
seq_length = input_ids.shape[-1] if input_ids is not None else inputs_embeds.shape[-2]
max_position_embedding = self.position_embedding.weight.shape[0]
if seq_length > max_position_embedding:
raise ValueError(
f"Sequence length must be less than max_position_embeddings (got `sequence length`: "
f"{seq_length} and max_position_embeddings: {max_position_embedding}"
)
if position_ids is None:
position_ids = self.position_ids[:, :seq_length]
if inputs_embeds is None:
inputs_embeds = self.token_embedding(input_ids)
position_embeddings = self.position_embedding(position_ids)
embeddings = inputs_embeds + position_embeddings
return embeddings
class CLIPAttention(nn.Module):
"""Multi-headed attention from 'Attention Is All You Need' paper"""
@@ -143,8 +195,14 @@ class CLIPAttention(nn.Module):
qkv_states, _ = self.qkv_proj(hidden_states)
query_states, key_states, value_states = qkv_states.chunk(3, dim=-1)
out = self.attn(query_states, key_states, value_states)
attn_output, _ = self.out_proj(out)
# use flash_attn_func
from flash_attn import flash_attn_func
query_states = query_states.reshape(query_states.shape[0], query_states.shape[1], self.num_heads_per_partition, self.head_dim)
key_states = key_states.reshape(key_states.shape[0], key_states.shape[1], self.num_heads_per_partition, self.head_dim)
value_states = value_states.reshape(value_states.shape[0], value_states.shape[1], self.num_heads_per_partition, self.head_dim)
attn_output = flash_attn_func(query_states, key_states, value_states,softmax_scale=self.scale, causal=True)
attn_output = attn_output.reshape(attn_output.shape[0], attn_output.shape[1], self.num_heads_per_partition * self.head_dim)
attn_output, _ = self.out_proj(attn_output)
return attn_output, None
@@ -183,7 +241,7 @@ class CLIPEncoderLayer(nn.Module):
def __init__(
self,
config: CLIPVisionConfig,
config: CLIPTextConfig,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "",
) -> None:
@@ -263,6 +321,191 @@ class CLIPEncoder(nn.Module):
if return_all_hidden_states:
return hidden_states_pool
return hidden_states
class CLIPTextTransformer(nn.Module):
def __init__(self,
config: CLIPTextConfig,
quant_config: Optional[QuantizationConfig] = None,
*,
num_hidden_layers_override: Optional[int] = None,
prefix: str = ""):
super().__init__()
self.config = config
embed_dim = config.hidden_size
self.embeddings = CLIPTextEmbeddings(config)
self.encoder = CLIPEncoder(config,
quant_config=quant_config,
num_hidden_layers_override=num_hidden_layers_override,
prefix=prefix)
self.final_layer_norm = nn.LayerNorm(embed_dim, eps=config.layer_norm_eps)
# For `pooled_output` computation
self.eos_token_id = config.eos_token_id
# For attention mask, it differs between `flash_attention_2` and other attention implementations
self._use_flash_attention_2 = config._attn_implementation == "flash_attention_2"
def forward(
self,
input_ids: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.Tensor] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
) -> Union[Tuple, BaseModelOutputWithPooling]:
r"""
Returns:
"""
output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
output_hidden_states = (
output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
)
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
if input_ids is None:
raise ValueError("You have to specify input_ids")
input_shape = input_ids.size()
input_ids = input_ids.view(-1, input_shape[-1])
hidden_states = self.embeddings(input_ids=input_ids, position_ids=position_ids)
# CLIP's text model uses causal mask, prepare it here.
# https://github.com/openai/CLIP/blob/cfcffb90e69f37bf2ff1e988237a0fbe41f33c04/clip/model.py#L324
# causal_attention_mask = _create_4d_causal_attention_mask(
# input_shape, hidden_states.dtype, device=hidden_states.device
# )
# # expand attention_mask
# if attention_mask is not None and not self._use_flash_attention_2:
# raise NotImplementedError("attention_mask is not supported for CLIPTextTransformer")
# # [bsz, seq_len] -> [bsz, 1, tgt_seq_len, src_seq_len]
# attention_mask = _prepare_4d_attention_mask(attention_mask, hidden_states.dtype)
encoder_outputs = self.encoder(
inputs_embeds=hidden_states,
# attention_mask=attention_mask,
# causal_attention_mask=causal_attention_mask,
# output_attentions=output_attentions,
return_all_hidden_states=output_hidden_states,
# return_dict=return_dict,
)
last_hidden_state = encoder_outputs[-1]
last_hidden_state = self.final_layer_norm(last_hidden_state)
if self.eos_token_id == 2:
# The `eos_token_id` was incorrect before PR #24773: Let's keep what have been done here.
# A CLIP model with such `eos_token_id` in the config can't work correctly with extra new tokens added
# ------------------------------------------------------------
# text_embeds.shape = [batch_size, sequence_length, transformer.width]
# take features from the eot embedding (eot_token is the highest number in each sequence)
# casting to torch.int for onnx compatibility: argmax doesn't support int64 inputs with opset 14
pooled_output = last_hidden_state[
torch.arange(last_hidden_state.shape[0], device=last_hidden_state.device),
input_ids.to(dtype=torch.int, device=last_hidden_state.device).argmax(dim=-1),
]
else:
# The config gets updated `eos_token_id` from PR #24773 (so the use of exta new tokens is possible)
pooled_output = last_hidden_state[
torch.arange(last_hidden_state.shape[0], device=last_hidden_state.device),
# We need to get the first position of `eos_token_id` value (`pad_token_ids` might equal to `eos_token_id`)
# Note: we assume each sequence (along batch dim.) contains an `eos_token_id` (e.g. prepared by the tokenizer)
(input_ids.to(dtype=torch.int, device=last_hidden_state.device) == self.eos_token_id)
.int()
.argmax(dim=-1),
]
if not return_dict:
return (last_hidden_state, pooled_output) + encoder_outputs[1:]
# return last_hidden_state
return BaseModelOutputWithPooling(
last_hidden_state=last_hidden_state,
pooler_output=pooled_output,
hidden_states=encoder_outputs,
# attentions=encoder_outputs.attentions,
)
class CLIPTextModel(nn.Module):
def __init__(
self,
config: CLIPTextConfig,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "",
) -> None:
super().__init__()
self.config = config
self.text_model = CLIPTextTransformer(
config=config,
quant_config=quant_config,
prefix=prefix)
def forward(
self,
input_ids: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.Tensor] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
) -> Union[Tuple, BaseModelOutputWithPooling]:
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
return self.text_model(
input_ids=input_ids,
attention_mask=attention_mask,
position_ids=position_ids,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=None,
)
def load_weights(self, weights: Iterable[Tuple[str,
torch.Tensor]]) -> Set[str]:
# Define mapping for stacked parameters
stacked_params_mapping = [
# (param_name, shard_name, shard_id)
("qkv_proj", "q_proj", "q"),
("qkv_proj", "k_proj", "k"),
("qkv_proj", "v_proj", "v"),
]
params_dict = dict(self.named_parameters())
loaded_params: Set[str] = set()
for name, loaded_weight in weights:
# Handle q_proj, k_proj, v_proj -> qkv_proj mapping
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name in name:
# Replace the weight name with the parameter name
model_param_name = name.replace(weight_name, param_name)
if model_param_name in params_dict:
param = params_dict[model_param_name]
weight_loader = param.weight_loader
weight_loader(param, loaded_weight, shard_id)
loaded_params.add(model_param_name)
break
else:
# Use default weight loader for all other parameters
if name in params_dict:
param = params_dict[name]
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
loaded_params.add(name)
return loaded_params
class CLIPVisionTransformer(nn.Module):
+428
View File
@@ -0,0 +1,428 @@
# SPDX-License-Identifier: Apache-2.0
# Adapted from
# https://github.com/huggingface/transformers/blob/v4.28.0/src/transformers/models/llama/modeling_llama.py
# Copyright 2023 The vLLM team.
# Copyright 2022 EleutherAI and the HuggingFace Inc. team. All rights reserved.
#
# This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX
# and OPT implementations in this library. It has been modified from its
# original forms to accommodate minor architectural differences compared
# to GPT-NeoX and OPT used by the Meta AI team that trained the model.
#
# 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.
"""Inference-only LLaMA model compatible with HuggingFace weights."""
from typing import Any, Dict, Iterable, Optional, Set, Tuple, Type, Union
import torch
from torch import nn
from transformers import LlamaConfig
from transformers.modeling_outputs import BaseModelOutputWithPast
from vllm.attention.layer import MultiHeadAttention
from fastvideo.v1.distributed import get_tensor_model_parallel_world_size
from fastvideo.v1.layers.activation import SiluAndMul
from fastvideo.v1.layers.layernorm import RMSNorm
from fastvideo.v1.layers.linear import (MergedColumnParallelLinear,
QKVParallelLinear,
RowParallelLinear)
# from vllm.model_executor.layers.quantization import QuantizationConfig
from fastvideo.v1.layers.rotary_embedding import get_rope
from fastvideo.v1.layers.vocab_parallel_embedding import VocabParallelEmbedding
from fastvideo.v1.loader.weight_utils import (
default_weight_loader, maybe_remap_kv_scale_name)
from .utils import (extract_layer_index)
class QuantizationConfig:
pass
class LlamaMLP(nn.Module):
def __init__(
self,
hidden_size: int,
intermediate_size: int,
hidden_act: str,
quant_config: Optional[QuantizationConfig] = None,
bias: bool = False,
prefix: str = "",
) -> None:
super().__init__()
self.gate_up_proj = MergedColumnParallelLinear(
input_size=hidden_size,
output_sizes=[intermediate_size] * 2,
# output_size=intermediate_size,
bias=bias,
quant_config=quant_config,
prefix=f"{prefix}.gate_up_proj",
)
self.down_proj = RowParallelLinear(
input_size=intermediate_size,
output_size=hidden_size,
bias=bias,
quant_config=quant_config,
prefix=f"{prefix}.down_proj",
)
if hidden_act != "silu":
raise ValueError(f"Unsupported activation: {hidden_act}. "
"Only silu is supported for now.")
self.act_fn = SiluAndMul()
def forward(self, x):
x, _ = self.gate_up_proj(x)
x = self.act_fn(x)
x, _ = self.down_proj(x)
return x
class LlamaAttention(nn.Module):
def __init__(self,
config: LlamaConfig,
hidden_size: int,
num_heads: int,
num_kv_heads: int,
rope_theta: float = 10000,
rope_scaling: Optional[Dict[str, Any]] = None,
max_position_embeddings: int = 8192,
quant_config: Optional[QuantizationConfig] = None,
bias: bool = False,
bias_o_proj: bool = False,
prefix: str = "") -> None:
super().__init__()
layer_idx = extract_layer_index(prefix)
self.hidden_size = hidden_size
tp_size = get_tensor_model_parallel_world_size()
self.total_num_heads = num_heads
assert self.total_num_heads % tp_size == 0
self.num_heads = self.total_num_heads // tp_size
self.total_num_kv_heads = num_kv_heads
if self.total_num_kv_heads >= tp_size:
# Number of KV heads is greater than TP size, so we partition
# the KV heads across multiple tensor parallel GPUs.
assert self.total_num_kv_heads % tp_size == 0
else:
# Number of KV heads is less than TP size, so we replicate
# the KV heads across multiple tensor parallel GPUs.
assert tp_size % self.total_num_kv_heads == 0
self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
# MistralConfig has an optional head_dim introduced by Mistral-Nemo
self.head_dim = getattr(config, "head_dim",
self.hidden_size // self.total_num_heads)
# Phi models introduced a partial_rotary_factor parameter in the config
partial_rotary_factor = getattr(config, "partial_rotary_factor", 1)
self.rotary_dim = int(partial_rotary_factor * self.head_dim)
self.q_size = self.num_heads * self.head_dim
self.kv_size = self.num_kv_heads * self.head_dim
self.scaling = self.head_dim**-0.5
self.rope_theta = rope_theta
self.max_position_embeddings = max_position_embeddings
self.qkv_proj = QKVParallelLinear(
hidden_size=hidden_size,
head_size=self.head_dim,
total_num_heads=self.total_num_heads,
total_num_kv_heads=self.total_num_kv_heads,
bias=bias,
quant_config=quant_config,
prefix=f"{prefix}.qkv_proj",
)
self.o_proj = RowParallelLinear(
input_size=self.total_num_heads * self.head_dim,
output_size=hidden_size,
bias=bias_o_proj,
quant_config=quant_config,
prefix=f"{prefix}.o_proj",
)
is_neox_style = True
is_gguf = quant_config and quant_config.get_name() == "gguf"
if is_gguf and config.model_type == "llama":
is_neox_style = False
self.rotary_emb = get_rope(
self.head_dim,
rotary_dim=self.rotary_dim,
max_position=max_position_embeddings,
base=rope_theta,
rope_scaling=rope_scaling,
is_neox_style=is_neox_style,
)
self.attn = MultiHeadAttention(self.num_heads,
self.head_dim,
self.scaling,
self.num_kv_heads)
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
) -> torch.Tensor:
qkv, _ = self.qkv_proj(hidden_states)
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
# print(f"query_states sum before apply_rotary_pos_emb: {q.float().sum()}")
q, k = self.rotary_emb(positions, q, k)
# attn_output = self.attn(q, k, v)
# use flash_attn_func
from flash_attn import flash_attn_func
# reshape q, k, v to (batch_size, seq_len, num_heads, head_dim)
batch_size = q.shape[0]
seq_len = q.shape[1]
q = q.reshape(batch_size, seq_len, self.num_heads, self.head_dim)
k = k.reshape(batch_size, seq_len, self.num_kv_heads, self.head_dim)
v = v.reshape(batch_size, seq_len, self.num_kv_heads, self.head_dim)
# import pdb; pdb.set_trace()
attn_output = flash_attn_func(q, k, v, softmax_scale=self.scaling, causal=True)
print(f"shape of attn_output: {attn_output.shape}")
print(f"num_kv_heads: {self.num_kv_heads}")
print(f"num_heads: {self.num_heads}")
print(f"head_dim: {self.head_dim}")
attn_output = attn_output.reshape(batch_size, seq_len, self.num_heads * self.head_dim)
output, _ = self.o_proj(attn_output)
return output
class LlamaDecoderLayer(nn.Module):
def __init__(
self,
config: LlamaConfig,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "",
) -> None:
super().__init__()
self.hidden_size = config.hidden_size
rope_theta = getattr(config, "rope_theta", 10000)
rope_scaling = getattr(config, "rope_scaling", None)
if rope_scaling is not None and getattr(
config, "original_max_position_embeddings", None):
rope_scaling["original_max_position_embeddings"] = (
config.original_max_position_embeddings)
max_position_embeddings = getattr(config, "max_position_embeddings",
8192)
# Support abacusai/Smaug-72B-v0.1 with attention_bias
# Support internlm/internlm-7b with bias
attention_bias = getattr(config, "attention_bias", False) or getattr(
config, "bias", False)
bias_o_proj = attention_bias
# support internlm/internlm3-8b with qkv_bias
if hasattr(config, 'qkv_bias'):
attention_bias = config.qkv_bias
self.self_attn = LlamaAttention(
config=config,
hidden_size=self.hidden_size,
num_heads=config.num_attention_heads,
num_kv_heads=getattr(config, "num_key_value_heads",
config.num_attention_heads),
rope_theta=rope_theta,
rope_scaling=rope_scaling,
max_position_embeddings=max_position_embeddings,
quant_config=quant_config,
bias=attention_bias,
bias_o_proj=bias_o_proj,
prefix=f"{prefix}.self_attn",
)
self.mlp = LlamaMLP(
hidden_size=self.hidden_size,
intermediate_size=config.intermediate_size,
hidden_act=config.hidden_act,
quant_config=quant_config,
bias=getattr(config, "mlp_bias", False),
prefix=f"{prefix}.mlp",
)
self.input_layernorm = RMSNorm(config.hidden_size,
eps=config.rms_norm_eps)
self.post_attention_layernorm = RMSNorm(config.hidden_size,
eps=config.rms_norm_eps)
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
residual: Optional[torch.Tensor],
) -> Tuple[torch.Tensor, torch.Tensor]:
# Self Attention
if residual is None:
residual = hidden_states
hidden_states = self.input_layernorm(hidden_states)
else:
hidden_states, residual = self.input_layernorm(
hidden_states, residual)
hidden_states = self.self_attn(positions=positions,
hidden_states=hidden_states)
# Fully Connected
hidden_states, residual = self.post_attention_layernorm(
hidden_states, residual)
hidden_states = self.mlp(hidden_states)
return hidden_states, residual
class LlamaModel(nn.Module):
def __init__(self,
config: LlamaConfig,
prefix: str = "",
layer_type: Type[LlamaDecoderLayer] = LlamaDecoderLayer):
super().__init__()
quant_config = None
lora_config = None
self.config = config
self.quant_config = quant_config
lora_vocab = (lora_config.lora_extra_vocab_size *
(lora_config.max_loras or 1)) if lora_config else 0
self.vocab_size = config.vocab_size + lora_vocab
self.org_vocab_size = config.vocab_size
self.embed_tokens = VocabParallelEmbedding(
self.vocab_size,
config.hidden_size,
org_num_embeddings=config.vocab_size,
quant_config=quant_config,
)
self.layers = nn.ModuleList([
layer_type(
config=config,
quant_config=quant_config,
prefix=f"{prefix}.layers.{i}"
) for i in range(config.num_hidden_layers)
])
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
def get_input_embeddings(self, input_ids: torch.Tensor) -> torch.Tensor:
return self.embed_tokens(input_ids)
def forward(
self,
input_ids: Optional[torch.Tensor],
positions: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
inputs_embeds: Optional[torch.Tensor] = None,
output_hidden_states: Optional[bool] = None,
) -> torch.Tensor:
output_hidden_states = (
output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
)
if inputs_embeds is not None:
hidden_states = inputs_embeds
else:
hidden_states = self.get_input_embeddings(input_ids)
residual = None
if positions is None:
positions = torch.arange(
0, hidden_states.shape[1], device=hidden_states.device
).unsqueeze(0)
all_hidden_states = () if output_hidden_states else None
for layer in self.layers:
if output_hidden_states:
all_hidden_states += (hidden_states,)
hidden_states, residual = layer(positions, hidden_states, residual)
hidden_states, _ = self.norm(hidden_states, residual)
# add hidden states from the last decoder layer
if output_hidden_states:
all_hidden_states += (hidden_states,)
# TODO(will): maybe unify the output format with other models and use
# our own class
output = BaseModelOutputWithPast(
last_hidden_state=hidden_states,
# past_key_values=past_key_values if use_cache else None,
hidden_states=all_hidden_states,
# attentions=all_self_attns,
)
return output
def load_weights(self, weights: Iterable[Tuple[str,
torch.Tensor]]) -> Set[str]:
stacked_params_mapping = [
# (param_name, shard_name, shard_id)
(".qkv_proj", ".q_proj", "q"),
(".qkv_proj", ".k_proj", "k"),
(".qkv_proj", ".v_proj", "v"),
(".gate_up_proj", ".gate_proj", 0),
(".gate_up_proj", ".up_proj", 1),
]
params_dict = dict(self.named_parameters())
loaded_params: Set[str] = set()
for name, loaded_weight in weights:
if "rotary_emb.inv_freq" in name:
continue
if ("rotary_emb.cos_cached" in name
or "rotary_emb.sin_cached" in name):
# Models trained using ColossalAI may include these tensors in
# the checkpoint. Skip them.
continue
if (self.quant_config is not None and
(scale_name := self.quant_config.get_cache_scale(name))):
# Loading kv cache quantization scales
param = params_dict[scale_name]
weight_loader = getattr(param, "weight_loader",
default_weight_loader)
loaded_weight = (loaded_weight if loaded_weight.dim() == 0 else
loaded_weight[0])
weight_loader(param, loaded_weight)
loaded_params.add(scale_name)
continue
if "scale" in name:
# Remapping the name of FP8 kv-scale.
name = maybe_remap_kv_scale_name(name, params_dict)
if name is None:
continue
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name not in name:
continue
name = name.replace(weight_name, param_name)
# Skip loading extra bias for GPTQ models.
if name.endswith(".bias") and name not in params_dict:
continue
if name not in params_dict:
continue
param = params_dict[name]
weight_loader = param.weight_loader
weight_loader(param, loaded_weight, shard_id)
break
else:
# Skip loading extra bias for GPTQ models.
if name.endswith(".bias") and name not in params_dict:
continue
if name not in params_dict:
continue
param = params_dict[name]
weight_loader = getattr(param, "weight_loader",
default_weight_loader)
weight_loader(param, loaded_weight)
loaded_params.add(name)
return loaded_params
+438
View File
@@ -0,0 +1,438 @@
# SPDX-License-Identifier: Apache-2.0
import itertools
from dataclasses import dataclass, field
from typing import (Callable, Dict, Iterable, List, Literal, Mapping, Optional,
Protocol, Set, Tuple, Union, overload)
import torch
import torch.nn as nn
from torch.func import functional_call
from transformers import PretrainedConfig
from fastvideo.v1.logger import init_logger
from fastvideo.v1.loader.weight_utils import default_weight_loader
logger = init_logger(__name__)
WeightsMapping = Mapping[str, Optional[str]]
"""If a key maps to a value of `None`, the corresponding weight is ignored."""
@dataclass
class WeightsMapper:
"""Maps the name of each weight if they match the following patterns."""
orig_to_new_substr: WeightsMapping = field(default_factory=dict)
orig_to_new_prefix: WeightsMapping = field(default_factory=dict)
orig_to_new_suffix: WeightsMapping = field(default_factory=dict)
def _map_name(self, key: str) -> Optional[str]:
for substr, new_key in self.orig_to_new_substr.items():
if substr in key:
if new_key is None:
return None
key = key.replace(substr, new_key, 1)
for prefix, new_key in self.orig_to_new_prefix.items():
if key.startswith(prefix):
if new_key is None:
return None
key = key.replace(prefix, new_key, 1)
for suffix, new_key in self.orig_to_new_suffix.items():
if key.endswith(suffix):
if new_key is None:
return None
key = new_key.join(key.rsplit(suffix, 1))
return key
def apply(
self, weights: Iterable[Tuple[str, torch.Tensor]]
) -> Iterable[Tuple[str, torch.Tensor]]:
return ((out_name, data) for name, data in weights
if (out_name := self._map_name(name)) is not None)
class AutoWeightsLoader:
"""
Helper class to load weights into a :class:`torch.nn.Module`. It is able
to automatically detect child modules and parameters while iterating over
the weights only once.
The weight loading logic for individual modules can be overridden
by defining a ``load_weights`` method.
Similarly, the weight loading logic for individual parameters can be
overridden by defining a ``weight_loader`` method.
Detailed weight loading information can be viewed by setting the
environment variable ``VLLM_LOGGING_LEVEL=DEBUG``.
"""
def __init__(
self,
module: nn.Module,
*,
skip_prefixes: Optional[List[str]] = None,
ignore_unexpected_prefixes: Optional[List[str]] = None,
) -> None:
super().__init__()
self.module = module
self.skip_prefixes = skip_prefixes or []
self.ignore_unexpected_prefixes = ignore_unexpected_prefixes or []
def _groupby_prefix(
self,
weights: Iterable[Tuple[str, torch.Tensor]],
) -> Iterable[Tuple[str, Iterable[Tuple[str, torch.Tensor]]]]:
weights_by_parts = ((weight_name.split(".", 1), weight_data)
for weight_name, weight_data in weights)
for prefix, group in itertools.groupby(weights_by_parts,
key=lambda x: x[0][0]):
yield (
prefix,
# Because maxsplit=1 in weight_name.split(...),
# the length of `parts` must either be 1 or 2
(("" if len(parts) == 1 else parts[1], weights_data)
for parts, weights_data in group),
)
def _get_qualname(self, prefix: str, rest: str) -> str:
if prefix == "":
return rest
if rest == "":
return prefix
return ".".join((prefix, rest))
def _can_skip(self, qualname: str) -> bool:
return any(qualname.startswith(p) for p in self.skip_prefixes)
def _can_ignore_unexpected(self, qualname: str) -> bool:
return any(
qualname.startswith(p) for p in self.ignore_unexpected_prefixes)
def _load_param(
self,
base_prefix: str,
param: nn.Parameter,
weights: Iterable[Tuple[str, torch.Tensor]],
) -> Iterable[str]:
for weight_name, weight_data in weights:
weight_qualname = self._get_qualname(base_prefix, weight_name)
if self._can_skip(weight_qualname):
logger.debug("Skipping weight %s", weight_qualname)
continue
if weight_name != "":
if self._can_ignore_unexpected(weight_qualname):
logger.debug("Ignoring weight %s", weight_qualname)
continue
raise ValueError(
f"Attempted to load nested weight '{weight_qualname}' "
f"into a single parameter '{base_prefix}'")
weight_loader = getattr(param, "weight_loader",
default_weight_loader)
weight_loader(param, weight_data)
logger.debug("Loaded weight %s with shape %s", weight_qualname,
param.shape)
yield weight_qualname
def _load_module(
self,
base_prefix: str,
module: nn.Module,
weights: Iterable[Tuple[str, torch.Tensor]],
) -> Iterable[str]:
if isinstance(module, PPMissingLayer):
return
# Avoid infinite recursion since this function is typically
# called inside load_weights of the module itself
if module != self.module:
module_load_weights = getattr(module, "load_weights", None)
if callable(module_load_weights):
loaded_params = module_load_weights(weights)
if loaded_params is None:
logger.warning(
"Unable to collect loaded parameters "
"for module %s", module)
else:
yield from map(
lambda x: self._get_qualname(base_prefix, x),
loaded_params,
)
child_modules = dict(module.named_children())
child_params = dict(module.named_parameters(recurse=False))
for child_prefix, child_weights in self._groupby_prefix(weights):
prefix = self._get_qualname(base_prefix, child_prefix)
if child_prefix in child_modules:
if self._can_skip(prefix + "."):
logger.debug("Skipping module %s", prefix)
continue
yield from self._load_module(prefix,
child_modules[child_prefix],
child_weights)
elif child_prefix in child_params:
if self._can_skip(prefix):
logger.debug("Skipping param %s", prefix)
continue
yield from self._load_param(prefix, child_params[child_prefix],
child_weights)
else:
can_skip_module = self._can_skip(prefix + ".")
can_skip_param = self._can_skip(prefix)
if can_skip_module or can_skip_param:
logger.debug("Skipping missing %s", prefix)
continue
can_ignore_module = self._can_ignore_unexpected(prefix + ".")
can_ignore_param = self._can_ignore_unexpected(prefix)
if can_ignore_module or can_ignore_param:
logger.debug("Ignoring missing %s", prefix)
continue
msg = (f"There is no module or parameter named '{prefix}' "
f"in {type(self.module).__name__}")
raise ValueError(msg)
def load_weights(
self,
weights: Iterable[Tuple[str, torch.Tensor]],
*,
mapper: Optional[WeightsMapper] = None,
) -> Set[str]:
if mapper is not None:
weights = mapper.apply(weights)
autoloaded_weights = set(self._load_module("", self.module, weights))
return autoloaded_weights
@overload
def flatten_bn(x: torch.Tensor) -> torch.Tensor:
...
@overload
def flatten_bn(x: List[torch.Tensor]) -> List[torch.Tensor]:
...
@overload
def flatten_bn(
x: Union[List[torch.Tensor], torch.Tensor],
*,
concat: Literal[True],
) -> torch.Tensor:
...
@overload
def flatten_bn(
x: Union[List[torch.Tensor], torch.Tensor],
*,
concat: bool = False,
) -> Union[List[torch.Tensor], torch.Tensor]:
...
def flatten_bn(
x: Union[List[torch.Tensor], torch.Tensor],
*,
concat: bool = False,
) -> Union[List[torch.Tensor], torch.Tensor]:
"""
Flatten the ``B`` and ``N`` dimensions of batched multimodal inputs.
The input tensor should have shape ``(B, N, ...)```.
"""
if isinstance(x, torch.Tensor):
return x.flatten(0, 1)
if concat:
return torch.cat(x)
return [x_n for x_b in x for x_n in x_b]
class LayerFn(Protocol):
def __call__(self, prefix: str) -> torch.nn.Module:
...
class PPMissingLayer(torch.nn.Identity):
"""
A placeholder layer for missing layers in a pipeline parallel model.
"""
def __init__(self, *args, **kwargs):
super().__init__()
_CPU_OFFLOAD_BYTES = 0
_CPU_OFFLOAD_MAX_BYTES = 0
def set_cpu_offload_max_bytes(max_bytes: int) -> None:
global _CPU_OFFLOAD_MAX_BYTES, _CPU_OFFLOAD_BYTES
_CPU_OFFLOAD_BYTES = 0
_CPU_OFFLOAD_MAX_BYTES = max_bytes
def maybe_offload_to_cpu(module: torch.nn.Module) -> torch.nn.Module:
device = next(module.parameters()).device
if device == torch.device("cpu"):
return module
global _CPU_OFFLOAD_MAX_BYTES, _CPU_OFFLOAD_BYTES
if _CPU_OFFLOAD_BYTES >= _CPU_OFFLOAD_MAX_BYTES:
return module
pin_memory = True
# offload parameters to CPU
# use pin_memory if possible, which helps cudagraph capture speed
offloaded_parameters = False
for p in module.parameters():
if _CPU_OFFLOAD_BYTES >= _CPU_OFFLOAD_MAX_BYTES:
# we use per-parameter offloading
# one module might have some parameters offloaded and some not
break
# `torch.empty_like` does not support `pin_memory` argument
cpu_data = torch.empty_strided(size=p.data.size(),
stride=p.data.stride(),
dtype=p.data.dtype,
layout=p.data.layout,
device='cpu',
pin_memory=pin_memory)
cpu_data.copy_(p.data)
p.data = cpu_data
_CPU_OFFLOAD_BYTES += p.data.numel() * p.data.element_size()
offloaded_parameters = True
if offloaded_parameters:
original_forward = module.forward
def forward(*args, **kwargs):
module.forward = original_forward
device_state = {
# here we blindly call `to(device)`
# if the parameter is already on the device, it will be a no-op
k: v.to(device, non_blocking=True)
for k, v in module.state_dict().items()
}
output = functional_call(module,
device_state,
args=args,
kwargs=kwargs)
module.forward = forward
return output
module.forward = forward
return module
def maybe_prefix(prefix: str, name: str) -> str:
"""Add a prefix to a name if the prefix is non-empty.
Args:
prefix: The prefix to add. If empty, no prefix will be added.
name: The name to potentially prefix.
Returns:
The string "prefix.name" if prefix was non-empty, otherwise just "name".
"""
return name if not prefix else f"{prefix}.{name}"
def extract_layer_index(layer_name: str) -> int:
"""
Extract the layer index from the module name.
Examples:
- "encoder.layers.0" -> 0
- "encoder.layers.1.self_attn" -> 1
- "2.self_attn" -> 2
- "model.encoder.layers.0.sub.1" -> ValueError
"""
subnames = layer_name.split(".")
int_vals: List[int] = []
for subname in subnames:
try:
int_vals.append(int(subname))
except ValueError:
continue
assert len(int_vals) == 1, (f"layer name {layer_name} should"
" only contain one integer")
return int_vals[0]
def cast_overflow_tensors(
tensors: torch.Tensor,
offset: float = 1000,
) -> torch.Tensor:
if tensors.isinf().any() or tensors.isnan().any():
clamp_value = torch.finfo(tensors.dtype).max - offset
tensors = torch.clamp(tensors, min=-clamp_value, max=clamp_value)
return tensors
def make_layers(
num_hidden_layers: int,
layer_fn: LayerFn,
prefix: str,
) -> torch.nn.ModuleList:
"""Make a list of layers with the given layer function.
Args:
num_hidden_layers: Number of layers to create.
layer_fn: Function that creates a layer given a prefix.
prefix: Prefix for layer names.
Returns:
A ModuleList containing all layers.
"""
modules = torch.nn.ModuleList([
maybe_offload_to_cpu(layer_fn(prefix=f"{prefix}.{idx}"))
for idx in range(num_hidden_layers)
])
return modules
def add_prefix(name: str, prefix: str) -> str:
"""Add a weight path prefix to a module name.
Args:
name: base module name.
prefix: weight prefix str to added to the front of `name` concatenated with `.`.
Returns:
The string `prefix.name` if prefix is non-empty, otherwise just `name`.
"""
return name if not prefix else f"{prefix}.{name}"
@@ -6,11 +6,11 @@ from typing import Final, Generic, Optional, Protocol, TypeVar, Union
import torch
from transformers import PretrainedConfig
import vllm.envs as envs
import fastvideo.v1.envs as envs
from vllm.attention.selector import (backend_name_to_enum,
get_global_forced_attn_backend)
from vllm.logger import init_logger
from vllm.platforms import _Backend, current_platform
from fastvideo.v1.logger import init_logger
from fastvideo.v1.platforms import _Backend, current_platform
logger = init_logger(__name__)
+259
View File
@@ -0,0 +1,259 @@
# Copyright 2023-2024 SGLang Team
# 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.
# ==============================================================================
"""Utilities for Huggingface Transformers."""
import contextlib
import os
import warnings
from pathlib import Path
from typing import Dict, Optional, Type, Union, Any
import json
from huggingface_hub import snapshot_download
from transformers import (
AutoConfig,
AutoProcessor,
AutoTokenizer,
PretrainedConfig,
PreTrainedTokenizer,
PreTrainedTokenizerFast,
)
from transformers.models.auto.modeling_auto import MODEL_FOR_CAUSAL_LM_MAPPING_NAMES
# from fastvideo.v1.models.configs import ChatGLMConfig, DbrxConfig, ExaoneConfig, Qwen2_5_VLConfig
_CONFIG_REGISTRY: Dict[str, Type[PretrainedConfig]] = {
# ChatGLMConfig.model_type: ChatGLMConfig,
# DbrxConfig.model_type: DbrxConfig,
# ExaoneConfig.model_type: ExaoneConfig,
# Qwen2_5_VLConfig.model_type: Qwen2_5_VLConfig,
}
for name, cls in _CONFIG_REGISTRY.items():
with contextlib.suppress(ValueError):
AutoConfig.register(name, cls)
def download_from_hf(model_path: str):
if os.path.exists(model_path):
return model_path
return snapshot_download(model_path, allow_patterns=["*.json", "*.bin", "*.model"])
def get_hf_config(
model: str,
trust_remote_code: bool,
revision: Optional[str] = None,
model_override_args: Optional[dict] = None,
inference_args: Optional[dict] = None,
**kwargs,
):
is_gguf = check_gguf_file(model)
if is_gguf:
kwargs["gguf_file"] = model
model = Path(model).parent
config = AutoConfig.from_pretrained(
model, trust_remote_code=trust_remote_code, revision=revision, **kwargs
)
if config.model_type in _CONFIG_REGISTRY:
config_class = _CONFIG_REGISTRY[config.model_type]
config = config_class.from_pretrained(model, revision=revision)
# NOTE(HandH1998): Qwen2VL requires `_name_or_path` attribute in `config`.
setattr(config, "_name_or_path", model)
if model_override_args:
config.update(model_override_args)
# Special architecture mapping check for GGUF models
if is_gguf:
if config.model_type not in MODEL_FOR_CAUSAL_LM_MAPPING_NAMES:
raise RuntimeError(f"Can't get gguf config for {config.model_type}.")
model_type = MODEL_FOR_CAUSAL_LM_MAPPING_NAMES[config.model_type]
config.update({"architectures": [model_type]})
return config
def get_diffusers_config(
model: str,
inference_args: Optional[dict] = None,
) -> Dict[str, Any]:
"""Gets a configuration for the given diffusers model.
Args:
model: The model name or path.
inference_args: Optional inference arguments to override in the config.
Returns:
The loaded configuration.
"""
# Check if the model path exists
if os.path.exists(model):
config_file = os.path.join(model, "config.json")
if os.path.exists(config_file):
try:
# Load the config directly from the file
with open(config_file, "r") as f:
config_dict = json.load(f)
# TODO(will): apply any overrides from inference args
return config_dict
except Exception as e:
raise RuntimeError(f"Failed to load diffusers config from {config_file}: {e}")
else:
raise RuntimeError(f"Diffusers config file not found at {model}")
# Models don't use the same configuration key for determining the maximum
# context length. Store them here so we can sanely check them.
# NOTE: The ordering here is important. Some models have two of these and we
# have a preference for which value gets used.
CONTEXT_LENGTH_KEYS = [
"max_sequence_length",
"seq_length",
"max_seq_len",
"model_max_length",
"max_position_embeddings",
]
def get_context_length(config):
"""Get the context length of a model from a huggingface model configs."""
text_config = config
rope_scaling = getattr(text_config, "rope_scaling", None)
if rope_scaling:
rope_scaling_factor = rope_scaling.get("factor", 1)
if "original_max_position_embeddings" in rope_scaling:
rope_scaling_factor = 1
if rope_scaling.get("rope_type", None) == "llama3":
rope_scaling_factor = 1
else:
rope_scaling_factor = 1
for key in CONTEXT_LENGTH_KEYS:
val = getattr(text_config, key, None)
if val is not None:
return int(rope_scaling_factor * val)
return 2048
# A fast LLaMA tokenizer with the pre-processed `tokenizer.json` file.
_FAST_LLAMA_TOKENIZER = "hf-internal-testing/llama-tokenizer"
def get_tokenizer(
tokenizer_name: str,
*args,
tokenizer_mode: str = "auto",
trust_remote_code: bool = False,
tokenizer_revision: Optional[str] = None,
**kwargs,
) -> Union[PreTrainedTokenizer, PreTrainedTokenizerFast]:
"""Gets a tokenizer for the given model name via Huggingface."""
if tokenizer_mode == "slow":
if kwargs.get("use_fast", False):
raise ValueError("Cannot use the fast tokenizer in slow tokenizer mode.")
kwargs["use_fast"] = False
is_gguf = check_gguf_file(tokenizer_name)
if is_gguf:
kwargs["gguf_file"] = tokenizer_name
tokenizer_name = Path(tokenizer_name).parent
try:
tokenizer = AutoTokenizer.from_pretrained(
tokenizer_name,
*args,
trust_remote_code=trust_remote_code,
tokenizer_revision=tokenizer_revision,
clean_up_tokenization_spaces=False,
**kwargs,
)
except TypeError as e:
# The LLaMA tokenizer causes a protobuf error in some environments.
err_msg = (
"Failed to load the tokenizer. If you are using a LLaMA V1 model "
f"consider using '{_FAST_LLAMA_TOKENIZER}' instead of the "
"original tokenizer."
)
raise RuntimeError(err_msg) from e
except ValueError as e:
# If the error pertains to the tokenizer class not existing or not
# currently being imported, suggest using the --trust-remote-code flag.
if not trust_remote_code and (
"does not exist or is not currently imported." in str(e)
or "requires you to execute the tokenizer file" in str(e)
):
err_msg = (
"Failed to load the tokenizer. If the tokenizer is a custom "
"tokenizer not yet available in the HuggingFace transformers "
"library, consider setting `trust_remote_code=True` in LLM "
"or using the `--trust-remote-code` flag in the CLI."
)
raise RuntimeError(err_msg) from e
else:
raise e
if not isinstance(tokenizer, PreTrainedTokenizerFast):
warnings.warn(
"Using a slow tokenizer. This might cause a significant "
"slowdown. Consider using a fast tokenizer instead."
)
attach_additional_stop_token_ids(tokenizer)
return tokenizer
def get_processor(
tokenizer_name: str,
*args,
tokenizer_mode: str = "auto",
trust_remote_code: bool = False,
tokenizer_revision: Optional[str] = None,
**kwargs,
):
processor = AutoProcessor.from_pretrained(
tokenizer_name,
*args,
trust_remote_code=trust_remote_code,
tokenizer_revision=tokenizer_revision,
**kwargs,
)
attach_additional_stop_token_ids(processor.tokenizer)
return processor
def attach_additional_stop_token_ids(tokenizer):
# Special handling for stop token <|eom_id|> generated by llama 3 tool use.
if "<|eom_id|>" in tokenizer.get_added_vocab():
tokenizer.additional_stop_token_ids = set(
[tokenizer.get_added_vocab()["<|eom_id|>"]]
)
else:
tokenizer.additional_stop_token_ids = None
def check_gguf_file(model: Union[str, os.PathLike]) -> bool:
"""Check if the file is a GGUF model."""
model = Path(model)
if not model.is_file():
return False
elif model.suffix == ".gguf":
return True
with open(model, "rb") as f:
header = f.read(4)
return header == b"GGUF"
+325
View File
@@ -0,0 +1,325 @@
from abc import ABC, abstractmethod
import torch
from fastvideo.v1.inference_args import InferenceArgs
from fastvideo.v1.logger import init_logger
import os
from fastvideo.v1.distributed.parallel_state import initialize_sequence_parallel_group
import glob
from fastvideo.v1.loader.fsdp_load import load_fsdp_model
from fastvideo.v1.loader.loader import get_model_loader
from transformers import PretrainedConfig, AutoTokenizer
from fastvideo.v1.models.hf_transformer_utils import get_hf_config, get_diffusers_config
from fastvideo.v1.models import get_scheduler
from fastvideo.v1.models.registry import ModelRegistry
from safetensors.torch import load_file as safetensors_load_file
import json
logger = init_logger(__name__)
class ComponentLoader(ABC):
"""Base class for loading a specific type of model component."""
def __init__(self, device=None):
self.device = device
@abstractmethod
def load(self, model_path: str, architecture: str, inference_args: InferenceArgs):
"""
Load the component based on the model path, architecture, and inference args.
Args:
model_path: Path to the component model
architecture: Architecture of the component model
inference_args: Inference arguments
Returns:
The loaded component
"""
raise NotImplementedError
@classmethod
def for_module_type(cls, module_type: str, transformers_or_diffusers: str) -> 'ComponentLoader':
"""
Factory method to create a component loader for a specific module type.
Args:
module_type: Type of module (e.g., "vae", "text_encoder", "transformer", "scheduler")
transformers_or_diffusers: Whether the module is from transformers or diffusers
Returns:
A component loader for the specified module type
"""
# Map of module types to their loader classes and expected library
module_loaders = {
"scheduler": (SchedulerLoader, "diffusers"),
"transformer": (TransformerLoader, "diffusers"),
"vae": (VAELoader, "diffusers"),
"text_encoder": (TextEncoderLoader, "transformers"),
"text_encoder_2": (TextEncoderLoader, "transformers"),
"tokenizer": (TokenizerLoader, "transformers"),
"tokenizer_2": (TokenizerLoader, "transformers"),
}
if module_type in module_loaders:
loader_cls, expected_library = module_loaders[module_type]
# Assert that the library matches what's expected for this module type
assert transformers_or_diffusers == expected_library, f"{module_type} must be loaded from {expected_library}, got {transformers_or_diffusers}"
return loader_cls()
# For unknown module types, use a generic loader
logger.warning(f"No specific loader found for module type: {module_type}. Using generic loader.")
return GenericComponentLoader(transformers_or_diffusers)
class TextEncoderLoader(ComponentLoader):
"""Loader for text encoders."""
def load(self, model_path: str, architecture: str, inference_args: InferenceArgs):
"""Load the text encoders based on the model path, architecture, and inference args."""
# should always use v1 here. If inference_args.use_v1_text_encoder is False,
# the pipeline will overwrite this text encoder with a v0 text encoder
# during initialize_encoders()
return self.load_v1(model_path, architecture, inference_args)
def load_v1(self, model_path: str, architecture: str, inference_args: InferenceArgs):
"""Load the text encoders based on the model path, architecture, and inference args."""
model_config: PretrainedConfig = get_hf_config(
model=model_path,
trust_remote_code=inference_args.trust_remote_code,
revision=inference_args.revision,
model_override_args=None,
inference_args=inference_args,
)
logger.info(f"HF Model config: {model_config}")
model_loader = get_model_loader()
model = model_loader.load_model(model_path, model_config, inference_args)
return model
class TokenizerLoader(ComponentLoader):
"""Loader for tokenizers."""
def load(self, model_path: str, architecture: str, inference_args: InferenceArgs):
"""Load the tokenizer based on the model path, architecture, and inference args."""
logger.info(f"Loading tokenizer from {model_path}")
tokenizer = AutoTokenizer.from_pretrained(
model_path,
# TODO(will): pass these tokenizer kwargs from inference args? Maybe
# other method of config?
padding_size='right',
)
logger.info(f"Loaded tokenizer: {tokenizer.__class__.__name__}")
return tokenizer
class VAELoader(ComponentLoader):
"""Loader for VAE."""
def load(self, model_path: str, architecture: str, inference_args: InferenceArgs):
"""Load the VAE based on the model path, architecture, and inference args."""
use_v1 = inference_args.use_v1_vae
if not use_v1:
return self.load_v0(model_path, architecture, inference_args)
else:
return self.load_v1(model_path, architecture, inference_args)
def load_v0(self, model_path: str, architecture: str, inference_args: InferenceArgs):
"""Custom VAE loading for Hunyuan"""
# TODO(will): replace this with abstracted model
from fastvideo.v1.v0_reference_src.models.hunyuan.vae import load_vae
vae, _, s_ratio, t_ratio = load_vae(
inference_args.vae,
inference_args.vae_precision,
logger=logger,
device=self.device
)
vae_kwargs = {"s_ratio": s_ratio, "t_ratio": t_ratio}
vae.kwargs = vae_kwargs
return vae
def load_v1(self, model_path: str, architecture: str, inference_args: InferenceArgs):
"""Load the VAE based on the model path, architecture, and inference args."""
# TODO(will): move this to a constants file
from fastvideo.v1.utils import PRECISION_TO_TYPE
config = get_diffusers_config(model=model_path)
class_name = config.pop("_class_name")
assert class_name is not None, "Model config does not contain a _class_name attribute. Only diffusers format is supported."
config.pop("_diffusers_version")
vae_cls, _ = ModelRegistry.resolve_model_cls(class_name)
vae = vae_cls(**config).to(inference_args.device)
# Find all safetensors files
safetensors_list = glob.glob(os.path.join(str(model_path), "*.safetensors"))
# TODO(PY)
assert len(safetensors_list) == 1, f"Found {len(safetensors_list)} safetensors files in {path}"
loaded = safetensors_load_file(safetensors_list[0])
vae.load_state_dict(loaded)
dtype = PRECISION_TO_TYPE[inference_args.vae_precision]
vae = vae.eval().to(dtype)
# TODO(will): should we define hunyuan vae config class?
vae_kwargs = {
"s_ratio": config["spatial_compression_ratio"],
"t_ratio": config["temporal_compression_ratio"],
}
vae.kwargs = vae_kwargs
return vae
class TransformerLoader(ComponentLoader):
"""Loader for transformer."""
def load(self, model_path: str, architecture: str, inference_args: InferenceArgs):
"""Load the transformer based on the model path, architecture, and inference args."""
use_v1 = inference_args.use_v1_transformer
if not use_v1:
return self.load_v0(model_path, architecture, inference_args)
else:
return self.load_v1(model_path, architecture, inference_args)
def load_v0(self, model_path: str, architecture: str, inference_args: InferenceArgs):
"""Custom transformer loading for Hunyuan"""
# TODO(will): replace this with abstracted model
from fastvideo.v1.v0_reference_src.models.hunyuan.modules import load_model
from fastvideo.v1.utils import PRECISION_TO_TYPE
# Disable gradient
torch.set_grad_enabled(False)
# =========================== Build main model ===========================
logger.info("Building model...")
factor_kwargs = {"device": self.device, "dtype": PRECISION_TO_TYPE[inference_args.precision]}
in_channels = inference_args.latent_channels
out_channels = inference_args.latent_channels
model = load_model(
inference_args,
in_channels=in_channels,
out_channels=out_channels,
factor_kwargs=factor_kwargs,
)
model = model.to(self.device)
model = self._load_transformer_state_dict(inference_args, model, inference_args.model_path)
if inference_args.enable_torch_compile:
model = torch.compile(model)
model.eval()
return model
def load_v1(self, model_path: str, architecture: str, inference_args: InferenceArgs):
"""Load the transformer based on the model path, architecture, and inference args."""
model_config = get_diffusers_config(model=model_path)
cls_name = model_config.pop("_class_name")
if cls_name is None:
raise ValueError(f"Model config does not contain a _class_name attribute. "
"Only diffusers format is supported.")
model_config.pop("_diffusers_version")
model_cls, _ = ModelRegistry.resolve_model_cls(cls_name)
# Find all safetensors files
safetensors_list = glob.glob(os.path.join(str(model_path), "*.safetensors"))
if not safetensors_list:
raise ValueError(f"No safetensors files found in {model_path}")
logger.info(f"Loading model from {len(safetensors_list)} safetensors files in {model_path}")
# initialize_sequence_parallel_group(inference_args.sp_size)
# Load the model using FSDP loader
logger.info(f"Loading model from {cls_name}")
model = load_fsdp_model(
model_cls=model_cls,
init_params=model_config,
weight_dir_list=safetensors_list,
device=inference_args.device,
cpu_offload=inference_args.use_cpu_offload
)
total_params = sum(p.numel() for p in model.parameters())
logger.info(f"Loaded model with {total_params / 1e9:.2f}B parameters")
model.eval()
return model
class SchedulerLoader(ComponentLoader):
"""Loader for scheduler."""
def load(self, model_path: str, architecture: str, inference_args: InferenceArgs):
"""Load the scheduler based on the model path, architecture, and inference args."""
scheduler = get_scheduler(
module_path=model_path,
architecture=architecture,
inference_args=inference_args,
)
logger.info(f"Scheduler loaded: {scheduler}")
return scheduler
class GenericComponentLoader(ComponentLoader):
"""Generic loader for components that don't have a specific loader."""
def __init__(self, library="transformers"):
super().__init__()
self.library = library
def load(self, model_path: str, architecture: str, inference_args: InferenceArgs):
"""Load a generic component based on the model path, architecture, and inference args."""
logger.warning(f"Using generic loader for {model_path} with library {self.library}")
if self.library == "transformers":
from transformers import AutoModel
model = AutoModel.from_pretrained(
model_path,
trust_remote_code=inference_args.trust_remote_code,
revision=inference_args.revision,
)
logger.info(f"Loaded generic transformers model: {model.__class__.__name__}")
return model
elif self.library == "diffusers":
logger.warning(f"Generic loading for diffusers components is not fully implemented")
from fastvideo.v1.models.hf_transformer_utils import get_diffusers_config
model_config = get_diffusers_config(model=model_path)
logger.info(f"Diffusers Model config: {model_config}")
# This is a placeholder - in a real implementation, you'd need to handle this properly
return None
else:
raise ValueError(f"Unsupported library: {self.library}")
class PipelineComponentLoader:
"""
Utility class for loading pipeline components.
This replaces the chain of if-else statements in load_pipeline_module.
"""
@staticmethod
def load_module(module_name: str, component_model_path: str, transformers_or_diffusers: str,
architecture: str, inference_args: InferenceArgs):
"""
Load a pipeline module.
Args:
module_name: Name of the module (e.g., "vae", "text_encoder", "transformer", "scheduler")
component_model_path: Path to the component model
transformers_or_diffusers: Whether the module is from transformers or diffusers
architecture: Architecture of the component model
inference_args: Inference arguments
Returns:
The loaded module
"""
logger.info(f"Loading {module_name} using {transformers_or_diffusers} from {component_model_path}")
# Get the appropriate loader for this module type
loader = ComponentLoader.for_module_type(module_name, transformers_or_diffusers)
# Load the module
return loader.load(component_model_path, architecture, inference_args)
@@ -7,9 +7,9 @@ from typing import Callable, Optional, Union
import torch
from torch.nn import Parameter
from fastvideo.distributed import get_tensor_model_parallel_rank
from fastvideo.logger import init_logger
from fastvideo.models.utils import _make_synced_weight_loader
from fastvideo.v1.distributed import get_tensor_model_parallel_rank
from fastvideo.v1.logger import init_logger
from fastvideo.v1.models.utils import _make_synced_weight_loader
__all__ = [
"BasevLLMParameter", "PackedvLLMParameter", "PerTensorScaleParameter",
@@ -431,4 +431,4 @@ def _adjust_shard_indexes_for_packing(shard_size, shard_offset, packed_factor,
shard_size=shard_size,
shard_offset=shard_offset,
marlin_tile_size=marlin_tile_size)
return shard_size, shard_offset
return shard_size, shard_offset
@@ -11,24 +11,40 @@ import importlib
from functools import lru_cache
import cloudpickle
from torch import nn
from fastvideo.logger import logger
from fastvideo.v1.logger import logger
# huggingface class name: (fastvideo module name, fastvideo class name)
_TEXT_TO_VIDEO_MODELS = {
"HunyuanVideoTransformer3DModel": ("hunyuanvideo", "HunyuanVideoDiT"),
# huggingface class name: (component_name, fastvideo module name, fastvideo class name)
_TEXT_TO_VIDEO_DIT_MODELS = {
"HunyuanVideoTransformer3DModel": ("dits", "hunyuanvideo", "HunyuanVideoTransformer3DModel"),
}
_IMAGE_TO_VIDEO_MODELS = {
_IMAGE_TO_VIDEO_DIT_MODELS = {
# "HunyuanVideoTransformer3DModel": ("dits", "hunyuanvideo", "HunyuanVideoDiT"),
}
_TEXT_ENCODER_MODELS = {
"CLIPTextModel": ("encoders", "clip", "CLIPTextModel"),
"LlamaModel": ("encoders", "llama", "LlamaModel"),
}
_IMAGE_ENCODER_MODELS = {
# "HunyuanVideoTransformer3DModel": ("image_encoder", "hunyuanvideo", "HunyuanVideoImageEncoder"),
}
_VAE_MODELS = {
"AutoencoderKLHunyuanVideo": ("vaes", "hunyuanvae", "AutoencoderKLHunyuanVideo"),
}
_FAST_VIDEO_MODELS = {
**_TEXT_TO_VIDEO_MODELS,
**_IMAGE_TO_VIDEO_MODELS,
**_TEXT_TO_VIDEO_DIT_MODELS,
**_IMAGE_TO_VIDEO_DIT_MODELS,
**_TEXT_ENCODER_MODELS,
**_IMAGE_ENCODER_MODELS,
**_VAE_MODELS,
}
_SUBPROCESS_COMMAND = [
sys.executable, "-m", "fastvideo.models.dits.registry"
sys.executable, "-m", "fastvideo.v1.models.dits.registry"
]
@@ -111,6 +127,7 @@ class _LazyRegisteredModel(_BaseRegisteredModel):
Represents a model that has not been imported in the main process.
"""
module_name: str
component_name: str
class_name: str
# Performed in another process to avoid initializing CUDA
@@ -128,7 +145,7 @@ def _try_load_model_cls(
model_arch: str,
model: _BaseRegisteredModel,
) -> Optional[Type[nn.Module]]:
from vllm.platforms import current_platform
from fastvideo.v1.platforms import current_platform
current_platform.verify_model_arch(model_arch)
try:
return model.load_model_cls()
@@ -266,8 +283,9 @@ class _ModelRegistry:
ModelRegistry = _ModelRegistry({
model_arch:
_LazyRegisteredModel(
module_name=f"fastvideo.models.dits.{mod_relname}",
module_name=f"fastvideo.v1.models.{component_name}.{mod_relname}",
component_name=component_name,
class_name=cls_name,
)
for model_arch, (mod_relname, cls_name) in _FAST_VIDEO_MODELS.items()
for model_arch, (component_name, mod_relname, cls_name) in _FAST_VIDEO_MODELS.items()
})
@@ -0,0 +1,239 @@
# Copyright 2024 Stability AI, Katherine Crowson and The HuggingFace Team. 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.
# ==============================================================================
#
# Modified from diffusers==0.29.2
#
# ==============================================================================
from dataclasses import dataclass
from typing import Optional, Tuple, Union
import torch
from diffusers.configuration_utils import ConfigMixin, register_to_config
from diffusers.schedulers.scheduling_utils import SchedulerMixin
from diffusers.utils import BaseOutput, logging
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
@dataclass
class FlowMatchDiscreteSchedulerOutput(BaseOutput):
"""
Output class for the scheduler's `step` function output.
Args:
prev_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images):
Computed sample `(x_{t-1})` of previous timestep. `prev_sample` should be used as next model input in the
denoising loop.
"""
prev_sample: torch.FloatTensor
class FlowMatchDiscreteScheduler(SchedulerMixin, ConfigMixin):
"""
Euler scheduler.
This model inherits from [`SchedulerMixin`] and [`ConfigMixin`]. Check the superclass documentation for the generic
methods the library implements for all schedulers such as loading and saving.
Args:
num_train_timesteps (`int`, defaults to 1000):
The number of diffusion steps to train the model.
timestep_spacing (`str`, defaults to `"linspace"`):
The way the timesteps should be scaled. Refer to Table 2 of the [Common Diffusion Noise Schedules and
Sample Steps are Flawed](https://huggingface.co/papers/2305.08891) for more information.
shift (`float`, defaults to 1.0):
The shift value for the timestep schedule.
reverse (`bool`, defaults to `True`):
Whether to reverse the timestep schedule.
"""
_compatibles = []
order = 1
@register_to_config
def __init__(
self,
num_train_timesteps: int = 1000,
shift: float = 1.0,
reverse: bool = True,
solver: str = "euler",
n_tokens: Optional[int] = None,
):
sigmas = torch.linspace(1, 0, num_train_timesteps + 1)
if not reverse:
sigmas = sigmas.flip(0)
self.sigmas = sigmas
# the value fed to model
self.timesteps = (sigmas[:-1] * num_train_timesteps).to(dtype=torch.float32)
self._step_index = None
self._begin_index = None
self.supported_solver = ["euler"]
if solver not in self.supported_solver:
raise ValueError(f"Solver {solver} not supported. Supported solvers: {self.supported_solver}")
@property
def step_index(self):
"""
The index counter for current timestep. It will increase 1 after each scheduler step.
"""
return self._step_index
@property
def begin_index(self):
"""
The index for the first timestep. It should be set from pipeline with `set_begin_index` method.
"""
return self._begin_index
# Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler.set_begin_index
def set_begin_index(self, begin_index: int = 0):
"""
Sets the begin index for the scheduler. This function should be run from pipeline before the inference.
Args:
begin_index (`int`):
The begin index for the scheduler.
"""
self._begin_index = begin_index
def _sigma_to_t(self, sigma):
return sigma * self.config.num_train_timesteps
def set_timesteps(
self,
num_inference_steps: int,
device: Union[str, torch.device] = None,
n_tokens: int = None,
):
"""
Sets the discrete timesteps used for the diffusion chain (to be run before inference).
Args:
num_inference_steps (`int`):
The number of diffusion steps used when generating samples with a pre-trained model.
device (`str` or `torch.device`, *optional*):
The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
n_tokens (`int`, *optional*):
Number of tokens in the input sequence.
"""
self.num_inference_steps = num_inference_steps
sigmas = torch.linspace(1, 0, num_inference_steps + 1)
sigmas = self.sd3_time_shift(sigmas)
if not self.config.reverse:
sigmas = 1 - sigmas
self.sigmas = sigmas
self.timesteps = (sigmas[:-1] * self.config.num_train_timesteps).to(dtype=torch.float32, device=device)
# Reset step index
self._step_index = None
def index_for_timestep(self, timestep, schedule_timesteps=None):
if schedule_timesteps is None:
schedule_timesteps = self.timesteps
indices = (schedule_timesteps == timestep).nonzero()
# The sigma index that is taken for the **very** first `step`
# is always the second index (or the last index if there is only 1)
# This way we can ensure we don't accidentally skip a sigma in
# case we start in the middle of the denoising schedule (e.g. for image-to-image)
pos = 1 if len(indices) > 1 else 0
return indices[pos].item()
def _init_step_index(self, timestep):
if self.begin_index is None:
if isinstance(timestep, torch.Tensor):
timestep = timestep.to(self.timesteps.device)
self._step_index = self.index_for_timestep(timestep)
else:
self._step_index = self._begin_index
def scale_model_input(self, sample: torch.Tensor, timestep: Optional[int] = None) -> torch.Tensor:
return sample
def sd3_time_shift(self, t: torch.Tensor):
return (self.config.shift * t) / (1 + (self.config.shift - 1) * t)
def step(
self,
model_output: torch.FloatTensor,
timestep: Union[float, torch.FloatTensor],
sample: torch.FloatTensor,
return_dict: bool = True,
) -> Union[FlowMatchDiscreteSchedulerOutput, Tuple]:
"""
Predict the sample from the previous timestep by reversing the SDE. This function propagates the diffusion
process from the learned model outputs (most often the predicted noise).
Args:
model_output (`torch.FloatTensor`):
The direct output from learned diffusion model.
timestep (`float`):
The current discrete timestep in the diffusion chain.
sample (`torch.FloatTensor`):
A current instance of a sample created by the diffusion process.
generator (`torch.Generator`, *optional*):
A random number generator.
n_tokens (`int`, *optional*):
Number of tokens in the input sequence.
return_dict (`bool`):
Whether or not to return a [`~schedulers.scheduling_euler_discrete.EulerDiscreteSchedulerOutput`] or
tuple.
Returns:
[`~schedulers.scheduling_euler_discrete.EulerDiscreteSchedulerOutput`] or `tuple`:
If return_dict is `True`, [`~schedulers.scheduling_euler_discrete.EulerDiscreteSchedulerOutput`] is
returned, otherwise a tuple is returned where the first element is the sample tensor.
"""
if (isinstance(timestep, int) or isinstance(timestep, torch.IntTensor)
or isinstance(timestep, torch.LongTensor)):
raise ValueError(("Passing integer indices (e.g. from `enumerate(timesteps)`) as timesteps to"
" `EulerDiscreteScheduler.step()` is not supported. Make sure to pass"
" one of the `scheduler.timesteps` as a timestep."), )
if self.step_index is None:
self._init_step_index(timestep)
# Upcast to avoid precision issues when computing prev_sample
sample = sample.to(torch.float32)
dt = self.sigmas[self.step_index + 1] - self.sigmas[self.step_index]
if self.config.solver == "euler":
prev_sample = sample + model_output.to(torch.float32) * dt
else:
raise ValueError(f"Solver {self.config.solver} not supported. Supported solvers: {self.supported_solver}")
# upon completion increase step index by one
self._step_index += 1
if not return_dict:
return (prev_sample, )
return FlowMatchDiscreteSchedulerOutput(prev_sample=prev_sample)
def __len__(self):
return self.config.num_train_timesteps
+255
View File
@@ -0,0 +1,255 @@
from dataclasses import dataclass
from typing import Optional, Tuple
import torch
import torch.nn as nn
from transformers.utils import ModelOutput
from fastvideo.v1.logger import init_logger
logger = init_logger(__name__)
def use_default(value, default):
return value if value is not None else default
@dataclass
class TextEncoderModelOutput(ModelOutput):
"""
Base class for model's outputs that also contains a pooling of the last hidden states.
Args:
hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, hidden_size)`):
Sequence of hidden-states at the output of the last layer of the model.
attention_mask (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*):
Mask to avoid performing attention on padding token indices. Mask values selected in ``[0, 1]``:
hidden_states_list (`tuple(torch.FloatTensor)`, *optional*, returned when `output_hidden_states=True` is passed):
Tuple of `torch.FloatTensor` (one for the output of the embeddings, if the model has an embedding layer, +
one for the output of each layer) of shape `(batch_size, sequence_length, hidden_size)`.
Hidden-states of the model at the output of each layer plus the optional initial embedding outputs.
text_outputs (`list`, *optional*, returned when `return_texts=True` is passed):
List of decoded texts.
"""
hidden_state: torch.FloatTensor = None
attention_mask: Optional[torch.LongTensor] = None
hidden_states_list: Optional[Tuple[torch.FloatTensor, ...]] = None
text_outputs: Optional[list] = None
class TextEncoder(nn.Module):
def __init__(
self,
text_encoder,
tokenizer,
max_length: int,
text_encoder_precision: Optional[str] = None,
text_encoder_path: Optional[str] = None,
output_key: Optional[str] = None,
use_attention_mask: bool = True,
prompt_template: Optional[dict] = None,
prompt_template_video: Optional[dict] = None,
hidden_state_skip_layer: Optional[int] = None,
apply_final_norm: bool = False,
reproduce: bool = False,
device=None,
):
super().__init__()
# TODO(will): check if there's a cleaner way to do this
self.text_encoder_type = text_encoder.config.architectures[0]
self.max_length = max_length
self.precision = text_encoder_precision
self.model_path = text_encoder_path
self.use_attention_mask = use_attention_mask
if prompt_template_video is not None:
assert (use_attention_mask is True), "Attention mask is True required when training videos."
self.prompt_template = prompt_template
self.prompt_template_video = prompt_template_video
self.hidden_state_skip_layer = hidden_state_skip_layer
self.apply_final_norm = apply_final_norm
self.reproduce = reproduce
self.use_template = self.prompt_template is not None
if self.use_template:
assert (isinstance(self.prompt_template, dict) and "template" in self.prompt_template
), f"`prompt_template` must be a dictionary with a key 'template', got {self.prompt_template}"
assert "{}" in str(self.prompt_template["template"]), (
"`prompt_template['template']` must contain a placeholder `{}` for the input text, "
f"got {self.prompt_template['template']}")
self.use_video_template = self.prompt_template_video is not None
if self.use_video_template:
if self.prompt_template_video is not None:
assert (
isinstance(self.prompt_template_video, dict) and "template" in self.prompt_template_video
), f"`prompt_template_video` must be a dictionary with a key 'template', got {self.prompt_template_video}"
assert "{}" in str(self.prompt_template_video["template"]), (
"`prompt_template_video['template']` must contain a placeholder `{}` for the input text, "
f"got {self.prompt_template_video['template']}")
if "T5" in self.text_encoder_type:
self.output_key = output_key or "last_hidden_state"
elif "CLIPTextModel" in self.text_encoder_type:
self.output_key = output_key or "pooler_output"
elif "LlamaModel" in self.text_encoder_type or "glm" in self.text_encoder_type:
self.output_key = output_key or "last_hidden_state"
else:
raise ValueError(f"Unsupported text encoder type: {self.text_encoder_type}")
self.model = text_encoder
# self.dtype = self.model.dtype
self.device = device
self.tokenizer = tokenizer
def __repr__(self):
return f"{self.text_encoder_type} ({self.precision} - {self.model_path})"
@staticmethod
def apply_text_to_template(text, template, prevent_empty_text=True):
"""
Apply text to template.
Args:
text (str): Input text.
template (str or list): Template string or list of chat conversation.
prevent_empty_text (bool): If True, we will prevent the user text from being empty
by adding a space. Defaults to True.
"""
if isinstance(template, str):
# Will send string to tokenizer. Used for llm
return template.format(text)
else:
raise TypeError(f"Unsupported template type: {type(template)}")
def text2tokens(self, text, data_type="image"):
"""
Tokenize the input text.
Args:
text (str or list): Input text.
"""
tokenize_input_type = "str"
if self.use_template:
if data_type == "image":
prompt_template = self.prompt_template["template"]
elif data_type == "video":
prompt_template = self.prompt_template_video["template"]
else:
raise ValueError(f"Unsupported data type: {data_type}")
if isinstance(text, (list, tuple)):
text = [self.apply_text_to_template(one_text, prompt_template) for one_text in text]
if isinstance(text[0], list):
tokenize_input_type = "list"
elif isinstance(text, str):
text = self.apply_text_to_template(text, prompt_template)
if isinstance(text, list):
tokenize_input_type = "list"
else:
raise TypeError(f"Unsupported text type: {type(text)}")
kwargs = dict(
truncation=True,
max_length=self.max_length,
padding="max_length",
return_tensors="pt",
)
if tokenize_input_type == "str":
return self.tokenizer(
text,
return_length=False,
return_overflowing_tokens=False,
return_attention_mask=True,
**kwargs,
)
elif tokenize_input_type == "list":
return self.tokenizer.apply_chat_template(
text,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
**kwargs,
)
else:
raise ValueError(f"Unsupported tokenize_input_type: {tokenize_input_type}")
def encode(
self,
batch_encoding,
use_attention_mask=None,
output_hidden_states=False,
do_sample=None,
hidden_state_skip_layer=None,
return_texts=False,
data_type="image",
device=None,
):
"""
Args:
batch_encoding (dict): Batch encoding from tokenizer.
use_attention_mask (bool): Whether to use attention mask. If None, use self.use_attention_mask.
Defaults to None.
output_hidden_states (bool): Whether to output hidden states. If False, return the value of
self.output_key. If True, return the entire output. If set self.hidden_state_skip_layer,
output_hidden_states will be set True. Defaults to False.
do_sample (bool): Whether to sample from the model. Used for Decoder-Only LLMs. Defaults to None.
When self.produce is False, do_sample is set to True by default.
hidden_state_skip_layer (int): Number of hidden states to hidden_state_skip_layer. 0 means the last layer.
If None, self.output_key will be used. Defaults to None.
return_texts (bool): Whether to return the decoded texts. Defaults to False.
"""
device = self.model.device if device is None else device
use_attention_mask = use_default(use_attention_mask, self.use_attention_mask)
hidden_state_skip_layer = use_default(hidden_state_skip_layer, self.hidden_state_skip_layer)
do_sample = use_default(do_sample, not self.reproduce)
attention_mask = (batch_encoding["attention_mask"].to(device) if use_attention_mask else None)
# print(f"batch_encoding: {batch_encoding}")
# logger.info(f"attention_mask: {attention_mask}")
outputs = self.model(
input_ids=batch_encoding["input_ids"].to(device),
# attention_mask=attention_mask,
output_hidden_states=output_hidden_states or hidden_state_skip_layer is not None,
)
if hidden_state_skip_layer is not None:
last_hidden_state = outputs.hidden_states[-(hidden_state_skip_layer + 1)]
# Real last hidden state already has layer norm applied. So here we only apply it
# for intermediate layers.
if hidden_state_skip_layer > 0 and self.apply_final_norm:
last_hidden_state = self.model.final_layer_norm(last_hidden_state)
else:
last_hidden_state = outputs[self.output_key]
# Remove hidden states of instruction tokens, only keep prompt tokens.
if self.use_template:
if data_type == "image":
crop_start = self.prompt_template.get("crop_start", -1)
elif data_type == "video":
crop_start = self.prompt_template_video.get("crop_start", -1)
else:
raise ValueError(f"Unsupported data type: {data_type}")
if crop_start > 0:
last_hidden_state = last_hidden_state[:, crop_start:]
attention_mask = (attention_mask[:, crop_start:] if use_attention_mask else None)
if output_hidden_states:
return TextEncoderModelOutput(last_hidden_state, attention_mask, outputs.hidden_states)
return TextEncoderModelOutput(last_hidden_state, attention_mask)
def forward(
self,
text,
use_attention_mask=None,
output_hidden_states=False,
do_sample=False,
hidden_state_skip_layer=None,
return_texts=False,
):
batch_encoding = self.text2tokens(text)
return self.encode(
batch_encoding,
use_attention_mask=use_attention_mask,
output_hidden_states=output_hidden_states,
do_sample=do_sample,
hidden_state_skip_layer=hidden_state_skip_layer,
return_texts=return_texts,
)
@@ -5,9 +5,52 @@ from typing import Any, Dict, Optional
import torch
# TODO(PY): move it elsewhere
def auto_attributes(init_func):
"""
Decorator that automatically adds all initialization arguments as object attributes.
Example:
@auto_attributes
def __init__(self, a=1, b=2):
pass
# This will automatically set:
# - self.a = 1 and self.b = 2
# - self.config.a = 1 and self.config.b = 2
"""
def wrapper(self, *args, **kwargs):
# Get the function signature
import inspect
signature = inspect.signature(init_func)
parameters = signature.parameters
# Get parameter names (excluding 'self')
param_names = list(parameters.keys())[1:]
# Bind arguments to parameters
bound_args = signature.bind(self, *args, **kwargs)
bound_args.apply_defaults()
# Create config object if it doesn't exist
if not hasattr(self, 'config'):
self.config = type('Config', (), {})()
# Set attributes on self and self.config
for name in param_names:
if name in bound_args.arguments:
value = bound_args.arguments[name]
setattr(self, name, value)
setattr(self.config, name, value)
# Call the original __init__ function
return init_func(self, *args, **kwargs)
return wrapper
def set_random_seed(seed: int) -> None:
from fastvideo.platforms import current_platform
from fastvideo.v1.platforms import current_platform
current_platform.seed_everything(seed)
@@ -39,7 +82,7 @@ def set_weight_attrs(
# This sometimes causes OOM errors during model loading. To avoid this,
# we sync the param tensor after its weight loader is called.
# TODO(woosuk): Remove this hack once we have a better solution.
from fastvideo.platforms import current_platform
from fastvideo.v1.platforms import current_platform
if current_platform.is_tpu() and key == "weight_loader":
value = _make_synced_weight_loader(value)
setattr(weight, key, value)
+469
View File
@@ -0,0 +1,469 @@
import torch
from typing import Optional, Tuple
import numpy as np
import torch.nn as nn
from abc import abstractmethod, ABC
import torch.distributed as dist
from fastvideo.v1.distributed import tensor_model_parallel_all_gather, get_sequence_model_parallel_rank, get_sequence_model_parallel_world_size
from math import prod
class ParallelTiledVAE(ABC):
def __init__(self, *args, **kwargs):
# Check if subclass has defined all required properties
required_attributes = [
'tile_sample_min_height',
'tile_sample_min_width',
'tile_sample_min_num_frames',
'tile_sample_stride_height',
'tile_sample_stride_width',
'tile_sample_stride_num_frames',
'use_tiling'
]
for attr in required_attributes:
if not hasattr(self, attr):
raise AttributeError(f"Subclasses of ParallelVAE must define '{attr}' property")
@abstractmethod
def _encode(self, *args, **kwargs):
pass
@abstractmethod
def _decode(self, *args, **kwargs):
pass
def encode(self, x: torch.Tensor) -> torch.Tensor:
batch_size, num_channels, num_frames, height, width = x.shape
if self.use_tiling and num_frames > self.tile_sample_min_num_frames:
latents = self.tiled_encode(x)
elif self.use_tiling and (width > self.tile_sample_min_width or height > self.tile_sample_min_height):
latents = self.spatial_tiled_encode(x)
else:
latents = self._encode(x)
return DiagonalGaussianDistribution(latents)
def decode(self, z: torch.Tensor) -> torch.Tensor:
batch_size, num_channels, num_frames, height, width = z.shape
tile_latent_min_height = self.tile_sample_min_height // self.spatial_compression_ratio
tile_latent_min_width = self.tile_sample_stride_width // self.spatial_compression_ratio
tile_latent_min_num_frames = self.tile_sample_min_num_frames // self.temporal_compression_ratio
if self.use_tiling and get_sequence_model_parallel_world_size() > 1:
return self.parallel_tiled_decode(z)
if self.use_tiling and num_frames > tile_latent_min_num_frames:
return self.tiled_decode(z)
if self.use_tiling and (width > tile_latent_min_width or height > tile_latent_min_height):
return self.spatial_tiled_decode(z)
return self._decode(z)
def blend_v(self, a: torch.Tensor, b: torch.Tensor, blend_extent: int) -> torch.Tensor:
blend_extent = min(a.shape[-2], b.shape[-2], blend_extent)
for y in range(blend_extent):
b[:, :, :, y, :] = a[:, :, :, -blend_extent + y, :] * (1 - y / blend_extent) + b[:, :, :, y, :] * (
y / blend_extent
)
return b
def blend_h(self, a: torch.Tensor, b: torch.Tensor, blend_extent: int) -> torch.Tensor:
blend_extent = min(a.shape[-1], b.shape[-1], blend_extent)
for x in range(blend_extent):
b[:, :, :, :, x] = a[:, :, :, :, -blend_extent + x] * (1 - x / blend_extent) + b[:, :, :, :, x] * (
x / blend_extent
)
return b
def blend_t(self, a: torch.Tensor, b: torch.Tensor, blend_extent: int) -> torch.Tensor:
blend_extent = min(a.shape[-3], b.shape[-3], blend_extent)
for x in range(blend_extent):
b[:, :, x, :, :] = a[:, :, -blend_extent + x, :, :] * (1 - x / blend_extent) + b[:, :, x, :, :] * (
x / blend_extent
)
return b
def spatial_tiled_encode(self, x: torch.Tensor) -> torch.Tensor:
r"""Encode a batch of images using a tiled encoder.
Args:
x (`torch.Tensor`): Input batch of videos.
Returns:
`torch.Tensor`:
The latent representation of the encoded videos.
"""
batch_size, num_channels, num_frames, height, width = x.shape
latent_height = height // self.spatial_compression_ratio
latent_width = width // self.spatial_compression_ratio
tile_latent_min_height = self.tile_sample_min_height // self.spatial_compression_ratio
tile_latent_min_width = self.tile_sample_min_width // self.spatial_compression_ratio
tile_latent_stride_height = self.tile_sample_stride_height // self.spatial_compression_ratio
tile_latent_stride_width = self.tile_sample_stride_width // self.spatial_compression_ratio
blend_height = tile_latent_min_height - tile_latent_stride_height
blend_width = tile_latent_min_width - tile_latent_stride_width
# Split x into overlapping tiles and encode them separately.
# The tiles have an overlap to avoid seams between tiles.
rows = []
for i in range(0, height, self.tile_sample_stride_height):
row = []
for j in range(0, width, self.tile_sample_stride_width):
tile = x[:, :, :, i : i + self.tile_sample_min_height, j : j + self.tile_sample_min_width]
tile = self._encode(tile)
row.append(tile)
rows.append(row)
return self._merge_spatial_tiles(rows, blend_height, blend_width,tile_latent_stride_height, tile_latent_stride_width)
def _parallel_data_generator(self, gathered_results,
gathered_dim_metadata):
global_idx = 0
for i, per_rank_metadata in enumerate(gathered_dim_metadata):
_start_shape = 0
for shape in per_rank_metadata:
mul_shape = prod(shape)
yield (gathered_results[i, _start_shape:_start_shape +
mul_shape].reshape(shape), global_idx)
_start_shape += mul_shape
global_idx += 1
def parallel_tiled_decode(self, z: torch.FloatTensor) -> torch.FloatTensor:
"""
Parallel version of tiled_decode that distributes both temporal and spatial computation across GPUs
"""
world_size, rank = get_sequence_model_parallel_world_size(), get_sequence_model_parallel_rank()
B, C, T, H, W = z.shape
# Calculate parameters
tile_latent_min_height = self.tile_sample_min_height // self.spatial_compression_ratio
tile_latent_min_width = self.tile_sample_min_width // self.spatial_compression_ratio
tile_latent_min_num_frames = self.tile_sample_min_num_frames // self.temporal_compression_ratio
tile_latent_stride_height = self.tile_sample_stride_height // self.spatial_compression_ratio
tile_latent_stride_width = self.tile_sample_stride_width // self.spatial_compression_ratio
tile_latent_stride_num_frames = self.tile_sample_stride_num_frames // self.temporal_compression_ratio
blend_height = self.tile_sample_min_height - self.tile_sample_stride_height
blend_width = self.tile_sample_min_width - self.tile_sample_stride_width
blend_num_frames = self.tile_sample_min_num_frames - self.tile_sample_stride_num_frames
# Calculate tile dimensions
num_t_tiles = (T + tile_latent_stride_num_frames - 1) // tile_latent_stride_num_frames
num_h_tiles = (H + tile_latent_stride_height - 1) // tile_latent_stride_height
num_w_tiles = (W + tile_latent_stride_width - 1) // tile_latent_stride_width
total_spatial_tiles = num_h_tiles * num_w_tiles
total_tiles = num_t_tiles * total_spatial_tiles
# Calculate tiles per rank and padding
tiles_per_rank = (total_tiles + world_size - 1) // world_size
start_tile_idx = rank * tiles_per_rank
end_tile_idx = min((rank + 1) * tiles_per_rank, total_tiles)
local_results = []
local_dim_metadata = []
# Process assigned tiles
for local_idx, global_idx in enumerate(range(start_tile_idx, end_tile_idx)):
t_idx = global_idx // total_spatial_tiles
spatial_idx = global_idx % total_spatial_tiles
h_idx = spatial_idx // num_w_tiles
w_idx = spatial_idx % num_w_tiles
# Calculate positions
t_start = t_idx * tile_latent_stride_num_frames
h_start = h_idx * tile_latent_stride_height
w_start = w_idx * tile_latent_stride_width
# Extract and process tile
tile = z[:, :, t_start:t_start + tile_latent_min_num_frames + 1,
h_start:h_start + tile_latent_min_height,
w_start:w_start + tile_latent_min_width]
# Process tile
tile = self._decode(tile)
if t_start > 0:
tile = tile[:, :, 1:, :, :]
# Store metadata
shape = tile.shape
# Store decoded data (flattened)
decoded_flat = tile.reshape(-1)
local_results.append(decoded_flat)
local_dim_metadata.append(shape)
results = torch.cat(local_results, dim=0).contiguous()
del local_results
torch.cuda.empty_cache()
# first gather size to pad the results
local_size = torch.tensor([results.size(0)],
device=results.device,
dtype=torch.int64)
all_sizes = [
torch.zeros(1, device=results.device, dtype=torch.int64)
for _ in range(world_size)
]
dist.all_gather(all_sizes, local_size)
max_size = max(size.item() for size in all_sizes)
padded_results = torch.zeros(max_size, device=results.device)
padded_results[:results.size(0)] = results
del results
torch.cuda.empty_cache()
# Gather all results
gathered_dim_metadata = [None] * world_size
gathered_results = torch.zeros_like(padded_results).repeat(
world_size, *[1] * len(padded_results.shape)
).contiguous() # use contiguous to make sure it won't copy data in the following operations
# TODO (PY): use fastvideo distributed methods
dist.all_gather_into_tensor(gathered_results, padded_results)
dist.all_gather_object(gathered_dim_metadata, local_dim_metadata)
# Process gathered results
data = [[[[] for _ in range(num_w_tiles)] for _ in range(num_h_tiles)]
for _ in range(num_t_tiles)]
for current_data, global_idx in self._parallel_data_generator(
gathered_results, gathered_dim_metadata):
t_idx = global_idx // total_spatial_tiles
spatial_idx = global_idx % total_spatial_tiles
h_idx = spatial_idx // num_w_tiles
w_idx = spatial_idx % num_w_tiles
data[t_idx][h_idx][w_idx] = current_data
# Merge results
result_slices = []
last_slice_data = None
for i, tem_data in enumerate(data):
slice_data = self._merge_spatial_tiles(tem_data, blend_height, blend_width,
self.tile_sample_stride_height, self.tile_sample_stride_width)
if i > 0:
slice_data = self.blend_t(last_slice_data, slice_data, blend_num_frames)
result_slices.append(slice_data[:, :, :self.tile_sample_stride_num_frames, :, :])
else:
result_slices.append(slice_data[:, :, :self.tile_sample_stride_num_frames + 1, :, :])
last_slice_data = slice_data
dec = torch.cat(result_slices, dim=2)
return dec
def _merge_spatial_tiles(self, tiles, blend_height, blend_width, stride_height, stride_width):
"""Helper function to merge spatial tiles with blending"""
result_rows = []
for i, row in enumerate(tiles):
result_row = []
for j, tile in enumerate(row):
if i > 0:
tile = self.blend_v(tiles[i - 1][j], tile,
blend_height)
if j > 0:
tile = self.blend_h(row[j - 1], tile, blend_width)
result_row.append(tile[:, :, :, :stride_height, :stride_width])
result_rows.append(torch.cat(result_row, dim=-1))
return torch.cat(result_rows, dim=-2)
def spatial_tiled_decode(self, z: torch.Tensor) -> torch.Tensor:
r"""
Decode a batch of images using a tiled decoder.
Args:
z (`torch.Tensor`): Input batch of latent vectors.
Returns:
`torch.Tensor`:
The decoded images.
"""
batch_size, num_channels, num_frames, height, width = z.shape
sample_height = height * self.spatial_compression_ratio
sample_width = width * self.spatial_compression_ratio
tile_latent_min_height = self.tile_sample_min_height // self.spatial_compression_ratio
tile_latent_min_width = self.tile_sample_min_width // self.spatial_compression_ratio
tile_latent_stride_height = self.tile_sample_stride_height // self.spatial_compression_ratio
tile_latent_stride_width = self.tile_sample_stride_width // self.spatial_compression_ratio
blend_height = self.tile_sample_min_height - self.tile_sample_stride_height
blend_width = self.tile_sample_min_width - self.tile_sample_stride_width
# Split z into overlapping tiles and decode them separately.
# The tiles have an overlap to avoid seams between tiles.
rows = []
for i in range(0, height, tile_latent_stride_height):
row = []
for j in range(0, width, tile_latent_stride_width):
tile = z[:, :, :, i : i + tile_latent_min_height, j : j + tile_latent_min_width]
decoded = self._decode(tile)
row.append(decoded)
rows.append(row)
return self._merge_spatial_tiles(rows, blend_height, blend_width,self.tile_sample_stride_height, self.tile_sample_stride_width)
def tiled_encode(self, x: torch.Tensor) -> torch.Tensor:
batch_size, num_channels, num_frames, height, width = x.shape
latent_num_frames = (num_frames - 1) // self.temporal_compression_ratio + 1
tile_latent_min_num_frames = self.tile_sample_min_num_frames // self.temporal_compression_ratio
tile_latent_stride_num_frames = self.tile_sample_stride_num_frames // self.temporal_compression_ratio
blend_num_frames = tile_latent_min_num_frames - tile_latent_stride_num_frames
row = []
for i in range(0, num_frames, self.tile_sample_stride_num_frames):
tile = x[:, :, i : i + self.tile_sample_min_num_frames + 1, :, :]
if self.use_tiling and (height > self.tile_sample_min_height or width > self.tile_sample_min_width):
tile = self.spatial_tiled_encode(tile)
else:
tile = self._encode(tile)
if i > 0:
tile = tile[:, :, 1:, :, :]
row.append(tile)
result_row = []
for i, tile in enumerate(row):
if i > 0:
tile = self.blend_t(row[i - 1], tile, blend_num_frames)
result_row.append(tile[:, :, :tile_latent_stride_num_frames, :, :])
else:
result_row.append(tile[:, :, : tile_latent_stride_num_frames + 1, :, :])
enc = torch.cat(result_row, dim=2)[:, :, :latent_num_frames]
return enc
def tiled_decode(self, z: torch.Tensor) -> torch.Tensor:
batch_size, num_channels, num_frames, height, width = z.shape
num_sample_frames = (num_frames - 1) * self.temporal_compression_ratio + 1
tile_latent_min_height = self.tile_sample_min_height // self.spatial_compression_ratio
tile_latent_min_width = self.tile_sample_min_width // self.spatial_compression_ratio
tile_latent_min_num_frames = self.tile_sample_min_num_frames // self.temporal_compression_ratio
tile_latent_stride_num_frames = self.tile_sample_stride_num_frames // self.temporal_compression_ratio
blend_num_frames = self.tile_sample_min_num_frames - self.tile_sample_stride_num_frames
row = []
for i in range(0, num_frames, tile_latent_stride_num_frames):
tile = z[:, :, i : i + tile_latent_min_num_frames + 1, :, :]
if self.use_tiling and (tile.shape[-1] > tile_latent_min_width or tile.shape[-2] > tile_latent_min_height):
decoded = self.spatial_tiled_decode(tile)
else:
decoded = self._decode(tile)
if i > 0:
decoded = decoded[:, :, 1:, :, :]
row.append(decoded)
result_row = []
result_row = []
for i, tile in enumerate(row):
if i > 0:
tile = self.blend_t(row[i - 1], tile, blend_num_frames)
result_row.append(tile[:, :, : self.tile_sample_stride_num_frames, :, :])
else:
result_row.append(tile[:, :, : self.tile_sample_stride_num_frames + 1, :, :])
dec = torch.cat(result_row, dim=2)[:, :, :num_sample_frames]
return dec
def enable_tiling(
self,
tile_sample_min_height: Optional[int] = None,
tile_sample_min_width: Optional[int] = None,
tile_sample_min_num_frames: Optional[int] = None,
tile_sample_stride_height: Optional[float] = None,
tile_sample_stride_width: Optional[float] = None,
tile_sample_stride_num_frames: Optional[float] = None,
) -> None:
r"""
Enable tiled VAE decoding. When this option is enabled, the VAE will split the input tensor into tiles to
compute decoding and encoding in several steps. This is useful for saving a large amount of memory and to allow
processing larger images.
Args:
tile_sample_min_height (`int`, *optional*):
The minimum height required for a sample to be separated into tiles across the height dimension.
tile_sample_min_width (`int`, *optional*):
The minimum width required for a sample to be separated into tiles across the width dimension.
tile_sample_min_num_frames (`int`, *optional*):
The minimum number of frames required for a sample to be separated into tiles across the frame
dimension.
tile_sample_stride_height (`int`, *optional*):
The minimum amount of overlap between two consecutive vertical tiles. This is to ensure that there are
no tiling artifacts produced across the height dimension.
tile_sample_stride_width (`int`, *optional*):
The stride between two consecutive horizontal tiles. This is to ensure that there are no tiling
artifacts produced across the width dimension.
tile_sample_stride_num_frames (`int`, *optional*):
The stride between two consecutive frame tiles. This is to ensure that there are no tiling artifacts
produced across the frame dimension.
"""
self.use_tiling = True
self.tile_sample_min_height = tile_sample_min_height or self.tile_sample_min_height
self.tile_sample_min_width = tile_sample_min_width or self.tile_sample_min_width
self.tile_sample_min_num_frames = tile_sample_min_num_frames or self.tile_sample_min_num_frames
self.tile_sample_stride_height = tile_sample_stride_height or self.tile_sample_stride_height
self.tile_sample_stride_width = tile_sample_stride_width or self.tile_sample_stride_width
self.tile_sample_stride_num_frames = tile_sample_stride_num_frames or self.tile_sample_stride_num_frames
def disable_tiling(self) -> None:
r"""
Disable tiled VAE decoding. If `enable_tiling` was previously enabled, this method will go back to computing
decoding in one step.
"""
self.use_tiling = False
# adapted from https://github.com/huggingface/diffusers/blob/e7ffeae0a191f710881d1fbde00cd6ff025e81f2/src/diffusers/models/autoencoders/vae.py#L691
class DiagonalGaussianDistribution(object):
def __init__(self, parameters: torch.Tensor, deterministic: bool = False):
self.parameters = parameters
self.mean, self.logvar = torch.chunk(parameters, 2, dim=1)
self.logvar = torch.clamp(self.logvar, -30.0, 20.0)
self.deterministic = deterministic
self.std = torch.exp(0.5 * self.logvar)
self.var = torch.exp(self.logvar)
if self.deterministic:
self.var = self.std = torch.zeros_like(
self.mean, device=self.parameters.device, dtype=self.parameters.dtype
)
def sample(self, generator: Optional[torch.Generator] = None) -> torch.Tensor:
# make sure sample is on the same device as the parameters and has same dtype
sample = torch.randn(
self.mean.shape,
generator=generator,
device=self.parameters.device,
dtype=self.parameters.dtype,
)
x = self.mean + self.std * sample
return x
def kl(self, other: "DiagonalGaussianDistribution" = None) -> torch.Tensor:
if self.deterministic:
return torch.Tensor([0.0])
else:
if other is None:
return 0.5 * torch.sum(
torch.pow(self.mean, 2) + self.var - 1.0 - self.logvar,
dim=[1, 2, 3],
)
else:
return 0.5 * torch.sum(
torch.pow(self.mean - other.mean, 2) / other.var
+ self.var / other.var
- 1.0
- self.logvar
+ other.logvar,
dim=[1, 2, 3],
)
def nll(self, sample: torch.Tensor, dims: Tuple[int, ...] = [1, 2, 3]) -> torch.Tensor:
if self.deterministic:
return torch.Tensor([0.0])
logtwopi = np.log(2.0 * np.pi)
return 0.5 * torch.sum(
logtwopi + self.logvar + torch.pow(sample - self.mean, 2) / self.var,
dim=dims,
)
def mode(self) -> torch.Tensor:
return self.mean
+799
View File
@@ -0,0 +1,799 @@
# Copyright 2024 The Hunyuan Team, The HuggingFace Team and The FastVideo Team. 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.
from typing import Optional, Tuple, Union
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.checkpoint
from fastvideo.v1.models.vaes.common import DiagonalGaussianDistribution, ParallelTiledVAE
from fastvideo.v1.layers.activation import get_act_fn
from fastvideo.v1.models.utils import auto_attributes
def prepare_causal_attention_mask(
num_frames: int, height_width: int, dtype: torch.dtype, device: torch.device, batch_size: int = None
) -> torch.Tensor:
indices = torch.arange(1, num_frames + 1, dtype=torch.int32, device=device)
indices_blocks = indices.repeat_interleave(height_width)
x, y = torch.meshgrid(indices_blocks, indices_blocks, indexing="xy")
mask = torch.where(x <= y, 0, -float("inf")).to(dtype=dtype)
if batch_size is not None:
mask = mask.unsqueeze(0).expand(batch_size, -1, -1)
return mask
class HunyuanVAEAttention(nn.Module):
def __init__(self, in_channels, heads, dim_head, eps, norm_num_groups, bias):
super().__init__()
self.in_channels = in_channels
self.heads = heads
self.dim_head = dim_head
self.eps = eps
self.norm_num_groups = norm_num_groups
self.bias = bias
inner_dim = heads * dim_head
# Define the projection layers
self.to_q = nn.Linear(in_channels, inner_dim, bias=bias)
self.to_k = nn.Linear(in_channels, inner_dim, bias=bias)
self.to_v = nn.Linear(in_channels, inner_dim, bias=bias)
self.to_out = nn.Sequential(
nn.Linear(inner_dim, in_channels, bias=bias)
)
# Optional normalization layers
self.group_norm = nn.GroupNorm(norm_num_groups, in_channels, eps=eps, affine=True)
def forward(self, hidden_states: torch.Tensor, attention_mask: Optional[torch.Tensor] = None) -> torch.Tensor:
residual = hidden_states
batch_size, sequence_length, _ = hidden_states.shape
hidden_states = self.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2)
# Project to query, key, value
query = self.to_q(hidden_states)
key = self.to_k(hidden_states)
value = self.to_v(hidden_states)
# Reshape for multi-head attention
head_dim = self.dim_head
query = query.view(batch_size, -1, self.heads, head_dim).transpose(1, 2)
key = key.view(batch_size, -1, self.heads, head_dim).transpose(1, 2)
value = value.view(batch_size, -1, self.heads, head_dim).transpose(1, 2)
# Perform scaled dot-product attention
hidden_states = F.scaled_dot_product_attention(
query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False
)
# Reshape back
hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, self.heads * head_dim)
hidden_states = hidden_states.to(query.dtype)
# Linear projection
hidden_states = self.to_out(hidden_states)
# Residual connection and rescale
hidden_states = hidden_states + residual
return hidden_states
class HunyuanVideoCausalConv3d(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: Union[int, Tuple[int, int, int]] = 3,
stride: Union[int, Tuple[int, int, int]] = 1,
padding: Union[int, Tuple[int, int, int]] = 0,
dilation: Union[int, Tuple[int, int, int]] = 1,
bias: bool = True,
pad_mode: str = "replicate",
) -> None:
super().__init__()
kernel_size = (kernel_size, kernel_size, kernel_size) if isinstance(kernel_size, int) else kernel_size
self.pad_mode = pad_mode
self.time_causal_padding = (
kernel_size[0] // 2,
kernel_size[0] // 2,
kernel_size[1] // 2,
kernel_size[1] // 2,
kernel_size[2] - 1,
0,
)
self.conv = nn.Conv3d(in_channels, out_channels, kernel_size, stride, padding, dilation, bias=bias)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
hidden_states = F.pad(hidden_states, self.time_causal_padding, mode=self.pad_mode)
return self.conv(hidden_states)
class HunyuanVideoUpsampleCausal3D(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: Optional[int] = None,
kernel_size: int = 3,
stride: int = 1,
bias: bool = True,
upsample_factor: Tuple[float, float, float] = (2, 2, 2),
) -> None:
super().__init__()
out_channels = out_channels or in_channels
self.upsample_factor = upsample_factor
self.conv = HunyuanVideoCausalConv3d(in_channels, out_channels, kernel_size, stride, bias=bias)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
num_frames = hidden_states.size(2)
first_frame, other_frames = hidden_states.split((1, num_frames - 1), dim=2)
first_frame = F.interpolate(
first_frame.squeeze(2), scale_factor=self.upsample_factor[1:], mode="nearest"
).unsqueeze(2)
if num_frames > 1:
# See: https://github.com/pytorch/pytorch/issues/81665
# Unless you have a version of pytorch where non-contiguous implementation of F.interpolate
# is fixed, this will raise either a runtime error, or fail silently with bad outputs.
# If you are encountering an error here, make sure to try running encoding/decoding with
# `vae.enable_tiling()` first. If that doesn't work, open an issue at:
# https://github.com/huggingface/diffusers/issues
other_frames = other_frames.contiguous()
other_frames = F.interpolate(other_frames, scale_factor=self.upsample_factor, mode="nearest")
hidden_states = torch.cat((first_frame, other_frames), dim=2)
else:
hidden_states = first_frame
hidden_states = self.conv(hidden_states)
return hidden_states
class HunyuanVideoDownsampleCausal3D(nn.Module):
def __init__(
self,
channels: int,
out_channels: Optional[int] = None,
padding: int = 1,
kernel_size: int = 3,
bias: bool = True,
stride=2,
) -> None:
super().__init__()
out_channels = out_channels or channels
self.conv = HunyuanVideoCausalConv3d(channels, out_channels, kernel_size, stride, padding, bias=bias)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
hidden_states = self.conv(hidden_states)
return hidden_states
class HunyuanVideoResnetBlockCausal3D(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: Optional[int] = None,
dropout: float = 0.0,
groups: int = 32,
eps: float = 1e-6,
non_linearity: str = "silu",
) -> None:
super().__init__()
out_channels = out_channels or in_channels
self.nonlinearity = get_act_fn(non_linearity)
self.norm1 = nn.GroupNorm(groups, in_channels, eps=eps, affine=True)
self.conv1 = HunyuanVideoCausalConv3d(in_channels, out_channels, 3, 1, 0)
self.norm2 = nn.GroupNorm(groups, out_channels, eps=eps, affine=True)
self.dropout = nn.Dropout(dropout)
self.conv2 = HunyuanVideoCausalConv3d(out_channels, out_channels, 3, 1, 0)
self.conv_shortcut = None
if in_channels != out_channels:
self.conv_shortcut = HunyuanVideoCausalConv3d(in_channels, out_channels, 1, 1, 0)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
hidden_states = hidden_states.contiguous()
residual = hidden_states
hidden_states = self.norm1(hidden_states)
hidden_states = self.nonlinearity(hidden_states)
hidden_states = self.conv1(hidden_states)
hidden_states = self.norm2(hidden_states)
hidden_states = self.nonlinearity(hidden_states)
hidden_states = self.dropout(hidden_states)
hidden_states = self.conv2(hidden_states)
if self.conv_shortcut is not None:
residual = self.conv_shortcut(residual)
hidden_states = hidden_states + residual
return hidden_states
class HunyuanVideoMidBlock3D(nn.Module):
def __init__(
self,
in_channels: int,
dropout: float = 0.0,
num_layers: int = 1,
resnet_eps: float = 1e-6,
resnet_act_fn: str = "silu",
resnet_groups: int = 32,
add_attention: bool = True,
attention_head_dim: int = 1,
) -> None:
super().__init__()
resnet_groups = resnet_groups if resnet_groups is not None else min(in_channels // 4, 32)
self.add_attention = add_attention
# There is always at least one resnet
resnets = [
HunyuanVideoResnetBlockCausal3D(
in_channels=in_channels,
out_channels=in_channels,
eps=resnet_eps,
groups=resnet_groups,
dropout=dropout,
non_linearity=resnet_act_fn,
)
]
attentions = []
for _ in range(num_layers):
if self.add_attention:
attentions.append(
HunyuanVAEAttention(
in_channels,
heads=in_channels // attention_head_dim,
dim_head=attention_head_dim,
eps=resnet_eps,
norm_num_groups=resnet_groups,
bias=True,
)
)
else:
attentions.append(None)
resnets.append(
HunyuanVideoResnetBlockCausal3D(
in_channels=in_channels,
out_channels=in_channels,
eps=resnet_eps,
groups=resnet_groups,
dropout=dropout,
non_linearity=resnet_act_fn,
)
)
self.attentions = nn.ModuleList(attentions)
self.resnets = nn.ModuleList(resnets)
self.gradient_checkpointing = False
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
if torch.is_grad_enabled() and self.gradient_checkpointing:
hidden_states = self._gradient_checkpointing_func(self.resnets[0], hidden_states)
for attn, resnet in zip(self.attentions, self.resnets[1:]):
if attn is not None:
batch_size, num_channels, num_frames, height, width = hidden_states.shape
hidden_states = hidden_states.permute(0, 2, 3, 4, 1).flatten(1, 3)
attention_mask = prepare_causal_attention_mask(
num_frames, height * width, hidden_states.dtype, hidden_states.device, batch_size=batch_size
)
hidden_states = attn(hidden_states, attention_mask=attention_mask)
hidden_states = hidden_states.unflatten(1, (num_frames, height, width)).permute(0, 4, 1, 2, 3)
hidden_states = self._gradient_checkpointing_func(resnet, hidden_states)
else:
hidden_states = self.resnets[0](hidden_states)
for attn, resnet in zip(self.attentions, self.resnets[1:]):
if attn is not None:
batch_size, num_channels, num_frames, height, width = hidden_states.shape
hidden_states = hidden_states.permute(0, 2, 3, 4, 1).flatten(1, 3)
attention_mask = prepare_causal_attention_mask(
num_frames, height * width, hidden_states.dtype, hidden_states.device, batch_size=batch_size
)
hidden_states = attn(hidden_states, attention_mask=attention_mask)
hidden_states = hidden_states.unflatten(1, (num_frames, height, width)).permute(0, 4, 1, 2, 3)
hidden_states = resnet(hidden_states)
return hidden_states
class HunyuanVideoDownBlock3D(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int,
dropout: float = 0.0,
num_layers: int = 1,
resnet_eps: float = 1e-6,
resnet_act_fn: str = "silu",
resnet_groups: int = 32,
add_downsample: bool = True,
downsample_stride: int = 2,
downsample_padding: int = 1,
) -> None:
super().__init__()
resnets = []
for i in range(num_layers):
in_channels = in_channels if i == 0 else out_channels
resnets.append(
HunyuanVideoResnetBlockCausal3D(
in_channels=in_channels,
out_channels=out_channels,
eps=resnet_eps,
groups=resnet_groups,
dropout=dropout,
non_linearity=resnet_act_fn,
)
)
self.resnets = nn.ModuleList(resnets)
if add_downsample:
self.downsamplers = nn.ModuleList(
[
HunyuanVideoDownsampleCausal3D(
out_channels,
out_channels=out_channels,
padding=downsample_padding,
stride=downsample_stride,
)
]
)
else:
self.downsamplers = None
self.gradient_checkpointing = False
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
if torch.is_grad_enabled() and self.gradient_checkpointing:
for resnet in self.resnets:
hidden_states = self._gradient_checkpointing_func(resnet, hidden_states)
else:
for resnet in self.resnets:
hidden_states = resnet(hidden_states)
if self.downsamplers is not None:
for downsampler in self.downsamplers:
hidden_states = downsampler(hidden_states)
return hidden_states
class HunyuanVideoUpBlock3D(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int,
dropout: float = 0.0,
num_layers: int = 1,
resnet_eps: float = 1e-6,
resnet_act_fn: str = "silu",
resnet_groups: int = 32,
add_upsample: bool = True,
upsample_scale_factor: Tuple[int, int, int] = (2, 2, 2),
) -> None:
super().__init__()
resnets = []
for i in range(num_layers):
input_channels = in_channels if i == 0 else out_channels
resnets.append(
HunyuanVideoResnetBlockCausal3D(
in_channels=input_channels,
out_channels=out_channels,
eps=resnet_eps,
groups=resnet_groups,
dropout=dropout,
non_linearity=resnet_act_fn,
)
)
self.resnets = nn.ModuleList(resnets)
if add_upsample:
self.upsamplers = nn.ModuleList(
[
HunyuanVideoUpsampleCausal3D(
out_channels,
out_channels=out_channels,
upsample_factor=upsample_scale_factor,
)
]
)
else:
self.upsamplers = None
self.gradient_checkpointing = False
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
if torch.is_grad_enabled() and self.gradient_checkpointing:
for resnet in self.resnets:
hidden_states = self._gradient_checkpointing_func(resnet, hidden_states)
else:
for resnet in self.resnets:
hidden_states = resnet(hidden_states)
if self.upsamplers is not None:
for upsampler in self.upsamplers:
hidden_states = upsampler(hidden_states)
return hidden_states
class HunyuanVideoEncoder3D(nn.Module):
r"""
Causal encoder for 3D video-like data introduced in [Hunyuan Video](https://huggingface.co/papers/2412.03603).
"""
def __init__(
self,
in_channels: int = 3,
out_channels: int = 3,
down_block_types: Tuple[str, ...] = (
"HunyuanVideoDownBlock3D",
"HunyuanVideoDownBlock3D",
"HunyuanVideoDownBlock3D",
"HunyuanVideoDownBlock3D",
),
block_out_channels: Tuple[int, ...] = (128, 256, 512, 512),
layers_per_block: int = 2,
norm_num_groups: int = 32,
act_fn: str = "silu",
double_z: bool = True,
mid_block_add_attention=True,
temporal_compression_ratio: int = 4,
spatial_compression_ratio: int = 8,
) -> None:
super().__init__()
self.conv_in = HunyuanVideoCausalConv3d(in_channels, block_out_channels[0], kernel_size=3, stride=1)
self.mid_block = None
self.down_blocks = nn.ModuleList([])
output_channel = block_out_channels[0]
for i, down_block_type in enumerate(down_block_types):
if down_block_type != "HunyuanVideoDownBlock3D":
raise ValueError(f"Unsupported down_block_type: {down_block_type}")
input_channel = output_channel
output_channel = block_out_channels[i]
is_final_block = i == len(block_out_channels) - 1
num_spatial_downsample_layers = int(np.log2(spatial_compression_ratio))
num_time_downsample_layers = int(np.log2(temporal_compression_ratio))
if temporal_compression_ratio == 4:
add_spatial_downsample = bool(i < num_spatial_downsample_layers)
add_time_downsample = bool(
i >= (len(block_out_channels) - 1 - num_time_downsample_layers) and not is_final_block
)
elif temporal_compression_ratio == 8:
add_spatial_downsample = bool(i < num_spatial_downsample_layers)
add_time_downsample = bool(i < num_time_downsample_layers)
else:
raise ValueError(f"Unsupported time_compression_ratio: {temporal_compression_ratio}")
downsample_stride_HW = (2, 2) if add_spatial_downsample else (1, 1)
downsample_stride_T = (2,) if add_time_downsample else (1,)
downsample_stride = tuple(downsample_stride_T + downsample_stride_HW)
down_block = HunyuanVideoDownBlock3D(
num_layers=layers_per_block,
in_channels=input_channel,
out_channels=output_channel,
add_downsample=bool(add_spatial_downsample or add_time_downsample),
resnet_eps=1e-6,
resnet_act_fn=act_fn,
resnet_groups=norm_num_groups,
downsample_stride=downsample_stride,
downsample_padding=0,
)
self.down_blocks.append(down_block)
self.mid_block = HunyuanVideoMidBlock3D(
in_channels=block_out_channels[-1],
resnet_eps=1e-6,
resnet_act_fn=act_fn,
attention_head_dim=block_out_channels[-1],
resnet_groups=norm_num_groups,
add_attention=mid_block_add_attention,
)
self.conv_norm_out = nn.GroupNorm(num_channels=block_out_channels[-1], num_groups=norm_num_groups, eps=1e-6)
self.conv_act = nn.SiLU()
conv_out_channels = 2 * out_channels if double_z else out_channels
self.conv_out = HunyuanVideoCausalConv3d(block_out_channels[-1], conv_out_channels, kernel_size=3)
self.gradient_checkpointing = False
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
hidden_states = self.conv_in(hidden_states)
if torch.is_grad_enabled() and self.gradient_checkpointing:
for down_block in self.down_blocks:
hidden_states = self._gradient_checkpointing_func(down_block, hidden_states)
hidden_states = self._gradient_checkpointing_func(self.mid_block, hidden_states)
else:
for down_block in self.down_blocks:
hidden_states = down_block(hidden_states)
hidden_states = self.mid_block(hidden_states)
hidden_states = self.conv_norm_out(hidden_states)
hidden_states = self.conv_act(hidden_states)
hidden_states = self.conv_out(hidden_states)
return hidden_states
class HunyuanVideoDecoder3D(nn.Module):
r"""
Causal decoder for 3D video-like data introduced in [Hunyuan Video](https://huggingface.co/papers/2412.03603).
"""
def __init__(
self,
in_channels: int = 3,
out_channels: int = 3,
up_block_types: Tuple[str, ...] = (
"HunyuanVideoUpBlock3D",
"HunyuanVideoUpBlock3D",
"HunyuanVideoUpBlock3D",
"HunyuanVideoUpBlock3D",
),
block_out_channels: Tuple[int, ...] = (128, 256, 512, 512),
layers_per_block: int = 2,
norm_num_groups: int = 32,
act_fn: str = "silu",
mid_block_add_attention=True,
time_compression_ratio: int = 4,
spatial_compression_ratio: int = 8,
):
super().__init__()
self.layers_per_block = layers_per_block
self.conv_in = HunyuanVideoCausalConv3d(in_channels, block_out_channels[-1], kernel_size=3, stride=1)
self.up_blocks = nn.ModuleList([])
# mid
self.mid_block = HunyuanVideoMidBlock3D(
in_channels=block_out_channels[-1],
resnet_eps=1e-6,
resnet_act_fn=act_fn,
attention_head_dim=block_out_channels[-1],
resnet_groups=norm_num_groups,
add_attention=mid_block_add_attention,
)
# up
reversed_block_out_channels = list(reversed(block_out_channels))
output_channel = reversed_block_out_channels[0]
for i, up_block_type in enumerate(up_block_types):
if up_block_type != "HunyuanVideoUpBlock3D":
raise ValueError(f"Unsupported up_block_type: {up_block_type}")
prev_output_channel = output_channel
output_channel = reversed_block_out_channels[i]
is_final_block = i == len(block_out_channels) - 1
num_spatial_upsample_layers = int(np.log2(spatial_compression_ratio))
num_time_upsample_layers = int(np.log2(time_compression_ratio))
if time_compression_ratio == 4:
add_spatial_upsample = bool(i < num_spatial_upsample_layers)
add_time_upsample = bool(
i >= len(block_out_channels) - 1 - num_time_upsample_layers and not is_final_block
)
else:
raise ValueError(f"Unsupported time_compression_ratio: {time_compression_ratio}")
upsample_scale_factor_HW = (2, 2) if add_spatial_upsample else (1, 1)
upsample_scale_factor_T = (2,) if add_time_upsample else (1,)
upsample_scale_factor = tuple(upsample_scale_factor_T + upsample_scale_factor_HW)
up_block = HunyuanVideoUpBlock3D(
num_layers=self.layers_per_block + 1,
in_channels=prev_output_channel,
out_channels=output_channel,
add_upsample=bool(add_spatial_upsample or add_time_upsample),
upsample_scale_factor=upsample_scale_factor,
resnet_eps=1e-6,
resnet_act_fn=act_fn,
resnet_groups=norm_num_groups,
)
self.up_blocks.append(up_block)
prev_output_channel = output_channel
# out
self.conv_norm_out = nn.GroupNorm(num_channels=block_out_channels[0], num_groups=norm_num_groups, eps=1e-6)
self.conv_act = nn.SiLU()
self.conv_out = HunyuanVideoCausalConv3d(block_out_channels[0], out_channels, kernel_size=3)
self.gradient_checkpointing = False
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
hidden_states = self.conv_in(hidden_states)
if torch.is_grad_enabled() and self.gradient_checkpointing:
hidden_states = self._gradient_checkpointing_func(self.mid_block, hidden_states)
for up_block in self.up_blocks:
hidden_states = self._gradient_checkpointing_func(up_block, hidden_states)
else:
hidden_states = self.mid_block(hidden_states)
for up_block in self.up_blocks:
hidden_states = up_block(hidden_states)
# post-process
hidden_states = self.conv_norm_out(hidden_states)
hidden_states = self.conv_act(hidden_states)
hidden_states = self.conv_out(hidden_states)
return hidden_states
class AutoencoderKLHunyuanVideo(nn.Module, ParallelTiledVAE):
r"""
A VAE model with KL loss for encoding videos into latents and decoding latent representations into videos.
Introduced in [HunyuanVideo](https://huggingface.co/papers/2412.03603).
This model inherits from [`ModelMixin`]. Check the superclass documentation for it's generic methods implemented
for all models (such as downloading or saving).
"""
_supports_gradient_checkpointing = True
@auto_attributes
def __init__(
self,
in_channels: int = 3,
out_channels: int = 3,
latent_channels: int = 16,
down_block_types: Tuple[str, ...] = (
"HunyuanVideoDownBlock3D",
"HunyuanVideoDownBlock3D",
"HunyuanVideoDownBlock3D",
"HunyuanVideoDownBlock3D",
),
up_block_types: Tuple[str, ...] = (
"HunyuanVideoUpBlock3D",
"HunyuanVideoUpBlock3D",
"HunyuanVideoUpBlock3D",
"HunyuanVideoUpBlock3D",
),
block_out_channels: Tuple[int] = (128, 256, 512, 512),
layers_per_block: int = 2,
act_fn: str = "silu",
norm_num_groups: int = 32,
scaling_factor: float = 0.476986,
spatial_compression_ratio: int = 8,
temporal_compression_ratio: int = 4,
mid_block_add_attention: bool = True,
load_encoder: bool = True,
load_decoder: bool = True,
) -> None:
super().__init__()
# TODO(will): only pass in config. We do this by manually defining a
# config for hunyuan vae
self.block_out_channels = block_out_channels
if load_encoder:
self.encoder = HunyuanVideoEncoder3D(
in_channels=in_channels,
out_channels=latent_channels,
down_block_types=down_block_types,
block_out_channels=block_out_channels,
layers_per_block=layers_per_block,
norm_num_groups=norm_num_groups,
act_fn=act_fn,
double_z=True,
mid_block_add_attention=mid_block_add_attention,
temporal_compression_ratio=temporal_compression_ratio,
spatial_compression_ratio=spatial_compression_ratio,
)
self.quant_conv = nn.Conv3d(2 * latent_channels, 2 * latent_channels, kernel_size=1)
if load_decoder:
self.decoder = HunyuanVideoDecoder3D(
in_channels=latent_channels,
out_channels=out_channels,
up_block_types=up_block_types,
block_out_channels=block_out_channels,
layers_per_block=layers_per_block,
norm_num_groups=norm_num_groups,
act_fn=act_fn,
time_compression_ratio=temporal_compression_ratio,
spatial_compression_ratio=spatial_compression_ratio,
mid_block_add_attention=mid_block_add_attention,
)
self.post_quant_conv = nn.Conv3d(latent_channels, latent_channels, kernel_size=1)
# When decoding spatially large video latents, the memory requirement is very high. By breaking the video latent
# frames spatially into smaller tiles and performing multiple forward passes for decoding, and then blending the
# intermediate tiles together, the memory requirement can be lowered.
self.use_tiling = True
# The minimal tile height and width for spatial tiling to be used
self.tile_sample_min_height = 256
self.tile_sample_min_width = 256
self.tile_sample_min_num_frames = 16
# The minimal distance between two spatial tiles
self.tile_sample_stride_height = 192
self.tile_sample_stride_width = 192
self.tile_sample_stride_num_frames = 12
def _encode(self, x: torch.Tensor) -> torch.Tensor:
x = self.encoder(x)
enc = self.quant_conv(x)
return enc
def _decode(self, z: torch.Tensor) -> torch.Tensor:
z = self.post_quant_conv(z)
dec = self.decoder(z)
return dec
def forward(
self,
sample: torch.Tensor,
sample_posterior: bool = False,
generator: Optional[torch.Generator] = None,
) -> torch.Tensor:
r"""
Args:
sample (`torch.Tensor`): Input sample.
sample_posterior (`bool`, *optional*, defaults to `False`):
Whether to sample from the posterior.
"""
x = sample
posterior = self.encode(x).latent_dist
if sample_posterior:
z = posterior.sample(generator=generator)
else:
z = posterior.mode()
dec = self.decode(z)
return dec
+244
View File
@@ -0,0 +1,244 @@
"""
Diffusion pipelines for fastvideo.v1.
This package contains diffusion pipelines for generating videos and images.
"""
import os
import json
from copy import deepcopy
from typing import Dict, Optional, Type, Any
from fastvideo.v1.pipelines.pipeline_registry import PipelineRegistry
from fastvideo.v1.inference_args import InferenceArgs
from fastvideo.v1.logger import init_logger
from huggingface_hub import snapshot_download
from transformers import PretrainedConfig
from fastvideo.v1.models.hf_transformer_utils import get_hf_config, get_diffusers_config
from fastvideo.v1.models import get_scheduler
import glob
from fastvideo.v1.loader.fsdp_load import load_fsdp_model
from fastvideo.v1.models.loader import PipelineComponentLoader
logger = init_logger(__name__)
# Then import the base classes
from fastvideo.v1.pipelines.composed.composed_pipeline_base import (
ComposedPipelineBase,
DiffusionPipelineOutput
)
def get_pipeline_type(inference_args: InferenceArgs) -> str:
# hardcode for now
return "hunyuan_video"
def maybe_download_model(model_path: str) -> str:
"""
Check if the model path is a Hugging Face Hub model ID and download it if needed.
Args:
model_path: Local path or Hugging Face Hub model ID
Returns:
Local path to the model
"""
# If the path exists locally, return it
if os.path.exists(model_path):
logger.info(f"Model already exists locally at {model_path}")
return model_path
# Otherwise, assume it's a HF Hub model ID and try to download it
try:
logger.info(f"Downloading model snapshot from HF Hub for {model_path}...")
local_path = snapshot_download(
repo_id=model_path,
# allow_patterns=["*.json", "*.bin", "*.safetensors", "*.pt", "*.pth", "*.ckpt"],
ignore_patterns=["*.onnx", "*.msgpack"],
)
logger.info(f"Downloaded model to {local_path}")
return local_path
except Exception as e:
raise ValueError(f"Could not find model at {model_path} and failed to download from HF Hub: {e}")
def verify_model_config_and_directory(model_path: str) -> dict:
"""
Verify that the model directory contains a valid diffusers configuration.
Args:
model_path: Path to the model directory
Returns:
The loaded model configuration as a dictionary
"""
# Check for model_index.json which is required for diffusers models
config_path = os.path.join(model_path, "model_index.json")
if not os.path.exists(config_path):
raise ValueError(
f"Model directory {model_path} does not contain model_index.json. "
"Only Hugging Face diffusers format is supported."
)
# Check for transformer and vae directories
transformer_dir = os.path.join(model_path, "transformer")
vae_dir = os.path.join(model_path, "vae")
if not os.path.exists(transformer_dir):
raise ValueError(f"Model directory {model_path} does not contain a transformer/ directory.")
if not os.path.exists(vae_dir):
raise ValueError(f"Model directory {model_path} does not contain a vae/ directory.")
# Load the config
try:
with open(config_path, "r") as f:
config = json.load(f)
except Exception as e:
raise ValueError(f"Failed to load model configuration from {config_path}: {e}")
# Verify diffusers version exists
if "_diffusers_version" not in config:
raise ValueError(f"model_index.json does not contain _diffusers_version")
logger.info(f"Diffusers version: {config['_diffusers_version']}")
return config
def load_pipeline_module(module_name: str, component_model_path: str, transformers_or_diffusers: str, architecture: str, inference_args: InferenceArgs) -> Any:
"""
Load a pipeline module using the appropriate loader.
Args:
module_name: Name of the module (e.g., "vae", "text_encoder", "transformer", "scheduler")
component_model_path: Path to the component model
transformers_or_diffusers: Whether the module is from transformers or diffusers
architecture: Architecture of the component model
inference_args: Inference arguments
Returns:
The loaded module
"""
return PipelineComponentLoader.load_module(
module_name=module_name,
component_model_path=component_model_path,
transformers_or_diffusers=transformers_or_diffusers,
architecture=architecture,
inference_args=inference_args
)
def load_pipeline_modules(model_path: str, config: Dict, inference_args: InferenceArgs) -> dict[str, Any]:
"""
Load the pipeline modules from the config.
Args:
config: The model_index.json config
inference_args: Inference arguments
Returns:
Dictionary mapping module names to loaded modules
"""
logger.info(f"Loading pipeline modules from config: {config}")
modules_config = deepcopy(config)
# remove keys that are not pipeline modules
modules_config.pop("_class_name")
modules_config.pop("_diffusers_version")
# some sanity checks
assert len(modules_config) > 1, "model_index.json must contain at least one pipeline module"
required_modules = ["vae", "text_encoder", "transformer", "scheduler", "tokenizer"]
for module_name in required_modules:
if module_name not in modules_config:
raise ValueError(f"model_index.json must contain a {module_name} module")
logger.info(f"Diffusers config passed sanity checks")
# all the component models used by the pipeline
pipeline_modules = {}
for module_name, (transformers_or_diffusers, architecture) in modules_config.items():
component_model_path = os.path.join(model_path, module_name)
module = load_pipeline_module(
module_name,
component_model_path,
transformers_or_diffusers,
architecture,
inference_args,
)
pipeline_modules[module_name] = module
# Check if all required modules were loaded
for module_name in required_modules:
if module_name not in pipeline_modules or pipeline_modules[module_name] is None:
logger.warning(f"Required module {module_name} was not loaded properly")
return pipeline_modules
def build_pipeline(inference_args: InferenceArgs) -> ComposedPipelineBase:
"""
Only works with valid hf diffusers configs. (model_index.json)
We want to build a pipeline based on the inference args mode_path:
1. download the model from the hub if it's not already downloaded
2. verify the model config and directory
3. based on the config, determine the pipeline class
4. parse the config to get the model components (vae, text_encoders, etc...)
5. the pipeline loader class will use the model component names and paths to load
6. the pipeline class will be composed of the models returned by the pipeline loader
"""
# Get pipeline type
model_path = inference_args.model_path
model_path = maybe_download_model(model_path)
# inference_args.downloaded_model_path = model_path
logger.info(f"Model path: {model_path}")
config = verify_model_config_and_directory(model_path)
pipeline_architecture = config.get("_class_name")
if pipeline_architecture is None:
raise ValueError("Model config does not contain a _class_name attribute. "
"Only diffusers format is supported.")
pipeline_cls, pipeline_architecture = PipelineRegistry.resolve_pipeline_cls(pipeline_architecture)
# instantiate the pipeline
pipeline = pipeline_cls()
try:
pipeline_modules = load_pipeline_modules(model_path, config, inference_args)
except Exception as e:
logger.error(f"Failed to load pipeline modules: {e}")
raise ValueError(f"Failed to load pipeline modules: {e}")
logger.info(f"Initializing encoders")
pipeline.initialize_encoders(pipeline_modules, inference_args)
logger.info(f"Registering modules")
pipeline.register_modules(pipeline_modules)
logger.info(f"Setting up pipeline")
pipeline.setup_pipeline(inference_args)
logger.info(f"Initializing pipeline")
pipeline.initialize_pipeline(inference_args)
# pipeline is now initialized and ready to use
return pipeline
def list_available_pipelines() -> Dict[str, Type[Any]]:
"""
List all available pipeline types.
Returns:
A dictionary of pipeline names to pipeline classes.
"""
return PipelineRegistry.list()
__all__ = [
"build_pipeline",
"list_available_pipelines",
"ComposedPipelineBase",
"DiffusionPipelineOutput",
]
@@ -4,7 +4,7 @@ Composed pipelines for diffusion models.
This package contains pipelines that are composed of multiple stages.
"""
from fastvideo.pipelines.composed.composed_pipeline_base import (
from fastvideo.v1.pipelines.composed.composed_pipeline_base import (
ComposedPipelineBase,
DiffusionPipelineOutput,
)
@@ -11,10 +11,10 @@ import torch
import numpy as np
from tqdm.auto import tqdm
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.inference_args import InferenceArgs
from fastvideo.pipelines.stages import PipelineStage
from fastvideo.logger import init_logger
from fastvideo.v1.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.v1.inference_args import InferenceArgs
from fastvideo.v1.pipelines.stages import PipelineStage
from fastvideo.v1.logger import init_logger
logger = init_logger(__name__)
@@ -63,6 +63,14 @@ class ComposedPipelineBase(ABC):
"""
...
@abstractmethod
def initialize_encoders(self, modules: Dict[str, Any], inference_args: InferenceArgs):
"""
Initialize the encoders. Will remove the encoders/tokenizers modules from the
modules. Will add the TextEncoder or ImageEncoder to the modules.
"""
...
def register_modules(self, modules: Dict[str, Any]):
"""
Register modules with the pipeline and its stages.
@@ -4,10 +4,10 @@ HunYuan pipeline implementations.
This package contains implementations of diffusion pipelines for HunYuan models.
"""
from fastvideo.pipelines.implementations.hunyuan.hunyuan_pipeline import (
HunyuanVideoPipeline,
from fastvideo.v1.pipelines.implementations.hunyuan.hunyuan_pipeline import (
EntryClass,
)
__all__ = [
"HunyuanVideoPipeline",
"EntryClass",
]
@@ -3,20 +3,20 @@ import torch.nn as nn
from pathlib import Path
from safetensors.torch import load_file as safetensors_load_file
from fastvideo.pipelines.loader import PipelineLoader
from fastvideo.inference_args import InferenceArgs
from fastvideo.pipelines.implementations.hunyuan.constants import PRECISION_TO_TYPE, PROMPT_TEMPLATE
from fastvideo.logger import init_logger
from fastvideo.v1.pipelines.loader import PipelineLoader
from fastvideo.v1.inference_args import InferenceArgs
from fastvideo.v1.pipelines.implementations.hunyuan.constants import PRECISION_TO_TYPE, PROMPT_TEMPLATE
from fastvideo.v1.logger import init_logger
from typing import Tuple, Optional
from fastvideo.v1.models.dits import HunyuanVideoTransformer3DModel
import os
import glob
import json
from fastvideo.loader.fsdp_load import load_fsdp_model
from fastvideo.models.hunyuan.text_encoder import TextEncoder
from fastvideo.platforms import current_platform
from fastvideo.v1.loader.fsdp_load import load_fsdp_model
from fastvideo.v1.models.hunyuan.text_encoder import TextEncoder
from fastvideo.v1.models.vaes import AutoencoderKLHunyuanVideo
from safetensors.torch import load_file as safetensors_load_file
from fastvideo.v1.platforms import current_platform
logger = init_logger(__name__)
@@ -113,7 +113,7 @@ class HunyuanPipelineLoader(PipelineLoader):
"""Custom transformer loading for Hunyuan"""
# Path to model files
# TODO(PY): remove this hardcode
# TODO(PY/Will): remove this hardcode
path = "data/hunyuanvideo_community/transformer"
path = Path(path)
@@ -130,7 +130,7 @@ class HunyuanPipelineLoader(PipelineLoader):
config.pop("_class_name")
if "_diffusers_version" in config:
config.pop("_diffusers_version")
# Find all safetensors files
safetensors_list = glob.glob(os.path.join(str(path), "*.safetensors"))
if not safetensors_list:
@@ -139,8 +139,9 @@ class HunyuanPipelineLoader(PipelineLoader):
logger.info(f"Loading model from {len(safetensors_list)} safetensors files in {path}")
# Load the model using FSDP loader
# TOOD(Will) new registry for DiT
model = load_fsdp_model(
model_name="HunyuanVideoTransformer3DModel",
model_cls=HunyuanVideoTransformer3DModel,
init_params=config,
weight_dir_list=safetensors_list,
device=self.device,
@@ -157,7 +158,7 @@ class HunyuanPipelineLoader(PipelineLoader):
def load_transformer_v0(self, inference_args: InferenceArgs):
"""Custom transformer loading for Hunyuan"""
# TODO(will): replace this with abstracted model
from fastvideo.models.hunyuan.modules import load_model
from fastvideo.v1.models.hunyuan.modules import load_model
# Disable gradient
torch.set_grad_enabled(False)
@@ -183,12 +184,55 @@ class HunyuanPipelineLoader(PipelineLoader):
def load_vae(self, inference_args: InferenceArgs):
"""Custom VAE loading for Hunyuan"""
# TODO(will): replace this with abstracted model
from fastvideo.models.hunyuan.vae import load_vae
use_v1_loader = True
if use_v1_loader:
return self.load_vae_v1(inference_args)
else:
return self.load_vae_v0(inference_args)
def load_vae_v1(self, inference_args: InferenceArgs):
"""Custom VAE loading for Hunyuan"""
path = "data/hunyuanvideo_community/vae"
path = Path(path)
# TODO(PY/Will): remove this hardcode
# Load config file
config_path = path / "config.json"
if not config_path.exists():
raise ValueError(f"Config file not found at {config_path}")
with open(config_path, "r") as f:
config = json.load(f)
# Clean up config
class_name = config.pop("_class_name")
if "_diffusers_version" in config:
config.pop("_diffusers_version")
vae = AutoencoderKLHunyuanVideo(**config).to(self.device)
# Find all safetensors files
safetensors_list = glob.glob(os.path.join(str(path), "*.safetensors"))
# TODO(PY)
assert len(safetensors_list) == 1, f"Found {len(safetensors_list)} safetensors files in {path}"
loaded = safetensors_load_file(safetensors_list[0])
vae.load_state_dict(loaded)
dtype = PRECISION_TO_TYPE[inference_args.vae_precision]
vae = vae.eval().to(dtype)
return vae, {"s_ratio": config["spatial_compression_ratio"], "t_ratio": config["temporal_compression_ratio"]}
def load_vae_v0(self, inference_args: InferenceArgs):
"""Custom VAE loading for Hunyuan"""
# TODO(will): replace this with abstracted model
from fastvideo.v1.models.hunyuan.vae import load_vae
vae, _, s_ratio, t_ratio = load_vae(
inference_args.vae,
inference_args.vae_precision,
logger=logger,
device=self.device if not inference_args.use_cpu_offload else "cpu",
device=self.device
)
vae_kwargs = {"s_ratio": s_ratio, "t_ratio": t_ratio}
return vae, vae_kwargs
@@ -0,0 +1,245 @@
"""
HunYuan video diffusion pipeline implementation.
This module contains an implementation of the HunYuan video diffusion pipeline
using the modular pipeline architecture.
"""
from typing import Union, Any, Dict
import torch
from diffusers.image_processor import VaeImageProcessor
from fastvideo.v1.pipelines.composed import ComposedPipelineBase
from fastvideo.v1.pipelines.stages import (
InputValidationStage,
PromptEncodingStage,
TimestepPreparationStage,
LatentPreparationStage,
ConditioningStage,
DenoisingStage,
DecodingStage,
PostProcessingStage,
)
from fastvideo.v1.pipelines.stages.prompt_encoding import PromptEncodingStage
# from fastvideo.v1.pipelines.stages.timestep_preparation import FlowMatchingTimestepPreparationStage
from fastvideo.v1.inference_args import InferenceArgs
# from fastvideo.v1.pipelines.composed.composed_pipeline_base import DiffusionPipelineOutput
from fastvideo.v1.pipelines.pipeline_batch_info import ForwardBatch
# TODO(will): move PRECISION_TO_TYPE to better place
from .constants import PROMPT_TEMPLATE, PRECISION_TO_TYPE
from diffusers.utils import BaseOutput
import numpy as np
from dataclasses import dataclass
from fastvideo.v1.logger import init_logger
logger = init_logger(__name__)
# class HunyuanLatentPreparationStage(LatentPreparationStage):
# def _call_implementation(self, batch: ForwardBatch, inference_args: InferenceArgs) -> ForwardBatch:
# "custom logic for HunYuan latent preparation"
# pass
# class hunyuanloader(PipelineLoader):
# def load_components(self, inference_args: InferenceArgs):
# pass
@dataclass
class DiffusionPipelineOutput(BaseOutput):
videos: Union[torch.Tensor, np.ndarray]
class HunyuanVideoPipeline(ComposedPipelineBase):
def initialize_encoders(self, modules: Dict[str, Any], inference_args: InferenceArgs):
use_v1 = inference_args.use_v1_text_encoder
if use_v1:
self.initialize_encoders_v1(modules, inference_args)
else:
self.initialize_encoders_v0(modules, inference_args)
def initialize_encoders_v0(self, modules: Dict[str, Any], inference_args: InferenceArgs):
from fastvideo.v1.v0_reference_src.models.hunyuan.text_encoder import TextEncoder
# Text encoder
if inference_args.prompt_template_video is not None:
crop_start = PROMPT_TEMPLATE[inference_args.prompt_template_video].get("crop_start", 0)
elif inference_args.prompt_template is not None:
crop_start = PROMPT_TEMPLATE[inference_args.prompt_template].get("crop_start", 0)
else:
crop_start = 0
max_length = inference_args.text_len + crop_start
# prompt_template
prompt_template = (PROMPT_TEMPLATE[inference_args.prompt_template] if inference_args.prompt_template is not None else None)
# prompt_template_video
prompt_template_video = (PROMPT_TEMPLATE[inference_args.prompt_template_video]
if inference_args.prompt_template_video is not None else None)
text_encoder = TextEncoder(
text_encoder_type=inference_args.text_encoder,
max_length=max_length,
text_encoder_precision=inference_args.text_encoder_precision,
tokenizer_type=inference_args.tokenizer,
prompt_template=prompt_template,
prompt_template_video=prompt_template_video,
hidden_state_skip_layer=inference_args.hidden_state_skip_layer,
apply_final_norm=inference_args.apply_final_norm,
reproduce=inference_args.reproduce,
logger=logger,
device=self.device if not inference_args.use_cpu_offload else "cpu",
)
text_encoder_2 = None
if inference_args.text_encoder_2 is not None:
text_encoder_2 = TextEncoder(
text_encoder_type=inference_args.text_encoder_2,
max_length=inference_args.text_len_2,
text_encoder_precision=inference_args.text_encoder_precision_2,
tokenizer_type=inference_args.tokenizer_2,
reproduce=inference_args.reproduce,
logger=logger,
device=self.device if not inference_args.use_cpu_offload else "cpu",
)
modules["text_encoder"] = text_encoder
modules["text_encoder_2"] = text_encoder_2
def initialize_encoders_v1(self, modules: Dict[str, Any], inference_args: InferenceArgs):
"""
Initialize the encoders. Will remove the encoders/tokenizers modules from the
modules. Will add the TextEncoder or ImageEncoder to the modules.
"""
from fastvideo.v1.models.text_encoder import TextEncoder
if inference_args.prompt_template_video is not None:
crop_start = PROMPT_TEMPLATE[inference_args.prompt_template_video].get("crop_start", 0)
elif inference_args.prompt_template is not None:
crop_start = PROMPT_TEMPLATE[inference_args.prompt_template].get("crop_start", 0)
else:
crop_start = 0
max_length = inference_args.text_len + crop_start
# prompt_template
prompt_template = (PROMPT_TEMPLATE[inference_args.prompt_template] if inference_args.prompt_template is not None else None)
# prompt_template_video
prompt_template_video = (PROMPT_TEMPLATE[inference_args.prompt_template_video]
if inference_args.prompt_template_video is not None else None)
encoder_1 = modules.pop("text_encoder")
assert encoder_1 is not None, "Text encoder is not found"
encoder_1.to(inference_args.device)
encoder_1.to(dtype=PRECISION_TO_TYPE[inference_args.text_encoder_precision])
encoder_1.requires_grad_(False)
tokenizer_1 = modules.pop("tokenizer")
assert tokenizer_1 is not None, "Tokenizer is not found"
text_encoder = TextEncoder(
text_encoder=encoder_1,
tokenizer=tokenizer_1,
# text_encoder_type="text_encoder"
max_length=max_length,
# text_encoder_precision=inference_args.text_encoder_precision,
prompt_template=prompt_template,
prompt_template_video=prompt_template_video,
hidden_state_skip_layer=inference_args.hidden_state_skip_layer,
apply_final_norm=inference_args.apply_final_norm,
reproduce=inference_args.reproduce,
device=inference_args.device if not inference_args.use_cpu_offload else "cpu",
)
encoder_2 = modules.pop("text_encoder_2")
assert encoder_2 is not None, "Text encoder 2 is not found"
encoder_2.to(inference_args.device)
encoder_2.to(dtype=PRECISION_TO_TYPE[inference_args.text_encoder_precision])
encoder_2.requires_grad_(False)
tokenizer_2 = modules.pop("tokenizer_2")
assert tokenizer_2 is not None, "Tokenizer 2 is not found"
text_encoder_2 = TextEncoder(
text_encoder=encoder_2,
tokenizer=tokenizer_2,
# text_encoder_type="text_encoder_2",
max_length=inference_args.text_len_2,
# text_encoder_precision=inference_args.text_encoder_precision,
reproduce=inference_args.reproduce,
device=inference_args.device if not inference_args.use_cpu_offload else "cpu",
)
modules["text_encoder"] = text_encoder
modules["text_encoder_2"] = text_encoder_2
def setup_pipeline(self, inference_args: InferenceArgs):
self.add_stage("input_validation_stage",
InputValidationStage())
self.add_stage("prompt_encoding_stage_primary",
PromptEncodingStage(is_secondary=False))
self.add_stage("prompt_encoding_stage_secondary",
PromptEncodingStage(is_secondary=True))
self.add_stage("conditioning_stage",
ConditioningStage())
self.add_stage("timestep_preparation_stage",
TimestepPreparationStage())
self.add_stage("latent_preparation_stage",
LatentPreparationStage())
self.add_stage("denoising_stage",
DenoisingStage())
self.add_stage("decoding_stage",
DecodingStage())
def initialize_pipeline(self, inference_args: InferenceArgs):
assert len(self._stages) > 0, "Pipeline stages are not set"
assert len(self._modules) > 0, "Pipeline modules are not set"
vae_scale_factor = 2**(len(self.vae.block_out_channels) - 1)
inference_args.vae_scale_factor = vae_scale_factor
self.image_processor = VaeImageProcessor(vae_scale_factor=vae_scale_factor)
self.register_modules({"image_processor": self.image_processor})
num_channels_latents = self.transformer.in_channels
inference_args.num_channels_latents = num_channels_latents
def adjust_video_length(self, batch: ForwardBatch, inference_args: InferenceArgs):
"""Adjust video length based on VAE version"""
video_length = batch.num_frames
vae_ver = inference_args.vae
logger.info(f"Adjusting video length for VAE version: {vae_ver}")
if "884" in vae_ver:
batch.num_frames = (video_length - 1) // 4 + 1
elif "888" in vae_ver:
batch.num_frames = (video_length - 1) // 8 + 1
return batch
@torch.no_grad()
def forward(self, batch: ForwardBatch, inference_args: InferenceArgs) -> ForwardBatch:
logger.info(f"Running pipeline stages: {self._stage_name_mapping.keys()}")
logger.info(f"Batch: {batch}")
# for stage in self._stages:
# batch = stage(batch, inference_args)
# or
batch = self.input_validation_stage(batch, inference_args)
batch = self.prompt_encoding_stage_primary(batch, inference_args)
batch = self.prompt_encoding_stage_secondary(batch, inference_args)
batch = self.conditioning_stage(batch, inference_args)
batch = self.timestep_preparation_stage(batch, inference_args)
# custom logic
batch = self.adjust_video_length(batch, inference_args)
batch = self.latent_preparation_stage(batch, inference_args)
batch = self.denoising_stage(batch, inference_args)
batch = self.decoding_stage(batch, inference_args)
return DiffusionPipelineOutput(videos=batch.videos)
EntryClass = HunyuanVideoPipeline
@@ -0,0 +1,91 @@
# Adapted from https://github.com/vllm-project/vllm/blob/v0.6.4.post1/vllm/model_executor/models/registry.py
# and https://github.com/sgl-project/sglang/blob/v0.4.3/python/sglang/srt/models/registry.py
import importlib
import pkgutil
from dataclasses import dataclass, field
from functools import lru_cache
from typing import AbstractSet, Dict, List, Optional, Tuple, Type, Union
from fastvideo.v1.logger import init_logger
import torch.nn as nn
logger = init_logger(__name__)
@dataclass
class _PipelineRegistry:
# Keyed by pipeline_arch
pipelines: Dict[str, Union[Type[nn.Module], str]] = field(default_factory=dict)
def get_supported_archs(self) -> AbstractSet[str]:
return self.pipelines.keys()
def _raise_for_unsupported(self, architectures: List[str]):
all_supported_archs = self.get_supported_archs()
if any(arch in all_supported_archs for arch in architectures):
raise ValueError(
f"Pipeline architectures {architectures} failed "
"to be inspected. Please check the logs for more details."
)
raise ValueError(
f"Pipeline architectures {architectures} are not supported for now. "
f"Supported architectures: {all_supported_archs}"
)
def _try_load_pipeline_cls(self, pipeline_arch: str) -> Optional[Type[nn.Module]]:
if pipeline_arch not in self.pipelines:
return None
return self.pipelines[pipeline_arch]
def resolve_pipeline_cls(
self,
architecture: str,
) -> Tuple[Type[nn.Module], str]:
if not architecture:
logger.warning("No pipeline architecture is specified")
pipeline_cls = self._try_load_pipeline_cls(architecture)
if pipeline_cls is not None:
return (pipeline_cls, architecture)
return self._raise_for_unsupported(architecture)
@lru_cache()
def import_pipeline_classes():
pipeline_arch_name_to_cls = {}
package_name = "fastvideo.v1.pipelines.implementations"
package = importlib.import_module(package_name)
for _, name, ispkg in pkgutil.iter_modules(package.__path__, package_name + "."):
if ispkg:
try:
module = importlib.import_module(name)
except Exception as e:
logger.warning(f"Ignore import error when loading {name}. " f"{e}")
continue
if hasattr(module, "EntryClass"):
entry = module.EntryClass
print(entry)
print(entry.__name__)
if isinstance(
entry, list
): # To support multiple pipeline classes in one module
for tmp in entry:
assert (
tmp.__name__ not in pipeline_arch_name_to_cls
), f"Duplicated pipeline implementation for {tmp.__name__}"
pipeline_arch_name_to_cls[tmp.__name__] = tmp
else:
assert (
entry.__name__ not in pipeline_arch_name_to_cls
), f"Duplicated pipeline implementation for {entry.__name__}"
pipeline_arch_name_to_cls[entry.__name__] = entry
return pipeline_arch_name_to_cls
PipelineRegistry = _PipelineRegistry(import_pipeline_classes())
+28
View File
@@ -0,0 +1,28 @@
"""
Pipeline stages for diffusion models.
This package contains the various stages that can be composed to create
complete diffusion pipelines.
"""
from fastvideo.v1.pipelines.stages.base import PipelineStage
from fastvideo.v1.pipelines.stages.input_validation import InputValidationStage
from fastvideo.v1.pipelines.stages.prompt_encoding import PromptEncodingStage
from fastvideo.v1.pipelines.stages.timestep_preparation import TimestepPreparationStage
from fastvideo.v1.pipelines.stages.latent_preparation import LatentPreparationStage
from fastvideo.v1.pipelines.stages.conditioning import ConditioningStage
from fastvideo.v1.pipelines.stages.denoising import DenoisingStage
from fastvideo.v1.pipelines.stages.decoding import DecodingStage
from fastvideo.v1.pipelines.stages.post_processing import PostProcessingStage
__all__ = [
"PipelineStage",
"InputValidationStage",
"PromptEncodingStage",
"TimestepPreparationStage",
"LatentPreparationStage",
"ConditioningStage",
"DenoisingStage",
"DecodingStage",
"PostProcessingStage",
]
@@ -11,9 +11,9 @@ import time
import traceback
from typing import Dict, Any
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.inference_args import InferenceArgs
from fastvideo.logger import init_logger
from fastvideo.v1.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.v1.inference_args import InferenceArgs
from fastvideo.v1.logger import init_logger
logger = init_logger(__name__)
@@ -36,7 +36,7 @@ class PipelineStage(ABC):
"""
self._enable_logging = enable_logging
self._stage_name = self.__class__.__name__
self._logger = init_logger(f"fastvideo.pipelines.stages.{self._stage_name}")
self._logger = init_logger(f"fastvideo.v1.pipelines.stages.{self._stage_name}")
@property
def device(self) -> torch.device:
@@ -5,10 +5,10 @@ Conditioning stage for diffusion pipelines.
import torch
from typing import Optional
from fastvideo.pipelines.stages.base import PipelineStage
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.inference_args import InferenceArgs
from fastvideo.logger import init_logger
from fastvideo.v1.pipelines.stages.base import PipelineStage
from fastvideo.v1.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.v1.inference_args import InferenceArgs
from fastvideo.v1.logger import init_logger
logger = init_logger(__name__)
@@ -3,17 +3,11 @@ Decoding stage for diffusion pipelines.
"""
import torch
from typing import Optional, Union, List, Tuple
import numpy as np
from fastvideo.pipelines.stages.base import PipelineStage
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.inference_args import InferenceArgs
from fastvideo.models.hunyuan.constants import PRECISION_TO_TYPE
from fastvideo.models.hunyuan.vae.autoencoder_kl_causal_3d import AutoencoderKLCausal3D
from fastvideo.logger import init_logger
from diffusers.utils import BaseOutput
from fastvideo.v1.pipelines.stages.base import PipelineStage
from fastvideo.v1.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.v1.inference_args import InferenceArgs
from fastvideo.v1.utils import PRECISION_TO_TYPE
from fastvideo.v1.logger import init_logger
logger = init_logger(__name__)
@@ -50,18 +44,6 @@ class DecodingStage(PipelineStage):
vae_dtype = PRECISION_TO_TYPE[inference_args.vae_precision]
vae_autocast_enabled = (vae_dtype != torch.float32) and not inference_args.disable_autocast
# Handle different latent shapes
expand_temporal_dim = False
if len(latents.shape) == 4:
if isinstance(self.vae, AutoencoderKLCausal3D):
latents = latents.unsqueeze(2)
expand_temporal_dim = True
elif len(latents.shape) == 5:
pass
else:
raise ValueError(
f"Only support latents with shape (b, c, h, w) or (b, c, f, h, w), but got {latents.shape}."
)
# Apply scaling/shifting if needed
if (hasattr(self.vae.config, "shift_factor") and self.vae.config.shift_factor):
@@ -73,11 +55,11 @@ class DecodingStage(PipelineStage):
with torch.autocast(device_type="cuda", dtype=vae_dtype, enabled=vae_autocast_enabled):
if inference_args.vae_tiling:
self.vae.enable_tiling()
image = self.vae.decode(latents, return_dict=False, generator=batch.generator)[0]
# if inference_args.vae_sp:
# self.vae.enable_parallel()
image = self.vae.decode(latents)
# Handle temporal dimension if needed
if expand_temporal_dim or image.shape[2] == 1:
image = image.squeeze(2)
# Normalize image to [0, 1] range
image = (image / 2 + 0.5).clamp(0, 1)
@@ -8,13 +8,14 @@ from typing import Optional, Dict, Any, List
from tqdm.auto import tqdm
from einops import rearrange
from fastvideo.pipelines.stages.base import PipelineStage
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.inference_args import InferenceArgs
from fastvideo.models.hunyuan.constants import PRECISION_TO_TYPE
from fastvideo.v1.pipelines.stages.base import PipelineStage
from fastvideo.v1.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.v1.inference_args import InferenceArgs
from fastvideo.v1.utils import PRECISION_TO_TYPE
# TODO(will-refactor): change this to fastvideo.distributed
from fastvideo.utils.parallel_states import nccl_info, get_sequence_parallel_state
from fastvideo.utils.communications import all_gather
from fastvideo.logger import init_logger
from fastvideo.v1.logger import init_logger
logger = init_logger(__name__)
@@ -5,10 +5,10 @@ Input validation stage for diffusion pipelines.
from typing import Optional, Union, List
import torch
from fastvideo.pipelines.stages.base import PipelineStage
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.inference_args import InferenceArgs
from fastvideo.logger import init_logger
from fastvideo.v1.pipelines.stages.base import PipelineStage
from fastvideo.v1.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.v1.inference_args import InferenceArgs
from fastvideo.v1.logger import init_logger
import random
logger = init_logger(__name__)
@@ -5,11 +5,11 @@ Latent preparation stage for diffusion pipelines.
import torch
from typing import Optional, Tuple
from fastvideo.pipelines.stages.base import PipelineStage
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.inference_args import InferenceArgs
from fastvideo.v1.pipelines.stages.base import PipelineStage
from fastvideo.v1.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.v1.inference_args import InferenceArgs
from diffusers.utils.torch_utils import randn_tensor
from fastvideo.logger import init_logger
from fastvideo.v1.logger import init_logger
logger = init_logger(__name__)
@@ -6,11 +6,11 @@ import torch
import numpy as np
from typing import Optional, Union, List, Tuple
from fastvideo.pipelines.stages.base import PipelineStage
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.inference_args import InferenceArgs
from fastvideo.v1.pipelines.stages.base import PipelineStage
from fastvideo.v1.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.v1.inference_args import InferenceArgs
from diffusers.utils import BaseOutput
from fastvideo.logger import init_logger
from fastvideo.v1.logger import init_logger
logger = init_logger(__name__)
@@ -7,11 +7,11 @@ This module contains implementations of prompt encoding stages for diffusion pip
from typing import Any, Dict, List, Optional, Union, Tuple
import torch
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.inference_args import InferenceArgs
from fastvideo.pipelines.stages import PipelineStage
from fastvideo.models.hunyuan.text_encoder import TextEncoder
from fastvideo.logger import init_logger
from fastvideo.v1.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.v1.inference_args import InferenceArgs
from fastvideo.v1.pipelines.stages import PipelineStage
from fastvideo.v1.models.text_encoder import TextEncoder
from fastvideo.v1.logger import init_logger
logger = init_logger(__name__)
@@ -87,13 +87,19 @@ class PromptEncodingStage(PipelineStage):
attention_mask = attention_mask.view(bs_embed * num_videos_per_prompt, seq_len)
if text_encoder is not None:
prompt_embeds_dtype = text_encoder.dtype
# TODO(will-refactor): use text_encoder.dtype
prompt_embeds_dtype = torch.float16
elif self.transformer is not None:
prompt_embeds_dtype = self.transformer.dtype
else:
prompt_embeds_dtype = prompt_embeds.dtype
prompt_embeds = prompt_embeds.to(dtype=prompt_embeds_dtype, device=device)
# print("prompt_embeds", type(prompt_embeds))
# logger.info(f"prompt_embeds shape: {prompt_embeds.shape}")
if prompt_embeds.ndim == 1:
prompt_embeds = prompt_embeds.unsqueeze(0)
# attention_mask = attention_mask.unsqueeze(0)
if prompt_embeds.ndim == 2:
bs_embed, _ = prompt_embeds.shape
@@ -8,10 +8,10 @@ from typing import Any, Dict, List, Optional, Union, Tuple
import torch
import inspect
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.inference_args import InferenceArgs
from fastvideo.pipelines.stages.base import PipelineStage
from fastvideo.logger import init_logger
from fastvideo.v1.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.v1.inference_args import InferenceArgs
from fastvideo.v1.pipelines.stages.base import PipelineStage
from fastvideo.v1.logger import init_logger
logger = init_logger(__name__)
@@ -12,9 +12,9 @@ import torch
from typing_extensions import ParamSpec
# import custom ops, trigger op registration
import fastvideo.envs as envs
from fastvideo.logger import init_logger
from fastvideo.utils import import_pynvml
import fastvideo.v1.envs as envs
from fastvideo.v1.logger import init_logger
from fastvideo.v1.utils import import_pynvml
from .interface import DeviceCapability, Platform, PlatformEnum, _Backend
@@ -115,7 +115,7 @@ class CudaPlatformBase(Platform):
@classmethod
def get_device_communicator_cls(cls) -> str:
return "fastvideo.distributed.device_communicators.cuda_communicator.CudaCommunicator" # noqa
return "fastvideo.v1.distributed.device_communicators.cuda_communicator.CudaCommunicator" # noqa
# NVML utils
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, NamedTuple, Optional, Tuple, Union
import numpy as np
import torch
from fastvideo.logger import init_logger
from fastvideo.v1.logger import init_logger
logger = init_logger(__name__)
@@ -186,7 +186,7 @@ class Platform:
"""
Get device specific communicator class for distributed communication.
"""
return "fastvideo.distributed.device_communicators.base_device_communicator.DeviceCommunicatorBase" # noqa
return "fastvideo.v1.distributed.device_communicators.base_device_communicator.DeviceCommunicatorBase" # noqa
class UnspecifiedPlatform(Platform):
@@ -9,23 +9,13 @@ import torchvision
from einops import rearrange
# Fix the import path
from fastvideo.inference_engine import InferenceEngine
from fastvideo.inference_args import InferenceArgs
from fastvideo.utils.utils import FlexibleArgumentParser
from fastvideo.distributed import init_distributed_environment, initialize_model_parallel
from fastvideo.logger import init_logger
import torch.distributed as dist
from fastvideo.utils.parallel_states import initialize_sequence_parallel_state
from fastvideo.v1.inference_engine import InferenceEngine
from fastvideo.v1.inference_args import InferenceArgs
from fastvideo.v1.utils import FlexibleArgumentParser
from fastvideo.v1.distributed import init_distributed_environment, initialize_model_parallel
from fastvideo.v1.logger import init_logger
logger = init_logger(__name__)
def initialize_distributed():
local_rank = int(os.getenv("RANK", 0))
world_size = int(os.getenv("WORLD_SIZE", 1))
print("world_size", world_size)
torch.cuda.set_device(local_rank)
dist.init_process_group(backend="nccl", init_method="env://", world_size=world_size, rank=local_rank)
initialize_sequence_parallel_state(world_size)
def main(inference_args: InferenceArgs):
# initialize_distributed()
# print(nccl_info.sp_size)
@@ -43,9 +33,10 @@ def main(inference_args: InferenceArgs):
# Initialize tensor model parallel groups
initialize_model_parallel(
sequence_model_parallel_size=inference_args.sp_size
sequence_model_parallel_size=inference_args.sp_size,
tensor_model_parallel_size=inference_args.tp_size,
)
initialize_sequence_parallel_state(world_size)
# initialize_sequence_parallel_state(world_size)
# initialize_distributed()
@@ -1,4 +1,4 @@
from fastvideo.logger import init_logger
from fastvideo.v1.logger import init_logger
logger = init_logger(__name__)
import os
+207
View File
@@ -0,0 +1,207 @@
from fastvideo.v1.models.encoders.clip import CLIPTextModel
from fastvideo.v1.v0_reference_src.models.hunyuan.text_encoder import TextEncoder, load_text_encoder, load_tokenizer
import os
import torch
import torch.nn as nn
import argparse
import numpy as np
from fastvideo.v1.logger import init_logger
from transformers import AutoConfig
from fastvideo.v1.distributed import init_distributed_environment, initialize_model_parallel
# from fastvideo.v1.models.hunyuan.text_encoder import load_text_encoder, load_tokenizer
logger = init_logger(__name__)
def initialize_identical_weights(model1, model2, seed=42):
"""Initialize both models with identical weights using a fixed seed for reproducibility."""
# Get all parameters from both models
params1 = dict(model1.named_parameters())
params2 = dict(model2.named_parameters())
# Initialize each layer with identical values
with torch.no_grad():
# Initialize weights
for name1, param1 in params1.items():
if 'weight' in name1:
# Set seed before each weight initialization
torch.manual_seed(seed)
nn.init.normal_(param1, mean=0.0, std=0.05)
for name2, param2 in params2.items():
if 'weight' in name2:
# Reset seed to get same initialization
torch.manual_seed(seed)
nn.init.normal_(param2, mean=0.0, std=0.05)
# Initialize biases
for name1, param1 in params1.items():
if 'bias' in name1:
torch.manual_seed(seed)
nn.init.normal_(param1, mean=0.0, std=0.05)
param1.data = param1.data.to(torch.bfloat16)
for name2, param2 in params2.items():
if 'bias' in name2:
torch.manual_seed(seed)
nn.init.normal_(param2, mean=0.0, std=0.05)
param2.data = param2.data.to(torch.bfloat16)
logger.info("Both models initialized with identical weights in bfloat16")
return model1, model2
def setup_args():
parser = argparse.ArgumentParser(description='CLIP Text Encoder Test')
parser.add_argument('--model-path', type=str, default="openai/clip-vit-large-patch14",
help='Path to the CLIP model')
parser.add_argument('--precision', type=str, default="float16",
help='Precision to use for the model (float32, float16, bfloat16)')
return parser.parse_args()
def test_clip_encoder():
init_distributed_environment(world_size=1, rank=0, distributed_init_method="env://", local_rank=0, backend="nccl")
initialize_model_parallel(tensor_model_parallel_size=1, sequence_model_parallel_size=1, backend="nccl")
args = setup_args()
# Set fixed random seed for reproducibility
torch.manual_seed(42)
np.random.seed(42)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# Initialize the two model implementations
logger.info(f"Loading models from {args.model_path}")
model_path = "data/hunyuanvideo-community/HunyuanVideo/text_encoder_2"
# config = json.load(open(os.path.join(model_path, "config.json")))
hf_config = AutoConfig.from_pretrained(model_path)
print(hf_config)
print(hf_config.use_return_dict)
# Load our implementation using the loader from text_encoder/__init__.py
model1, _ = load_text_encoder(
text_encoder_type="clipL",
text_encoder_precision='fp16',
text_encoder_path=model_path,
logger=logger,
device=device
)
from fastvideo.v1.models.loader import TextEncoderLoader
from fastvideo.v1.loader.loader import DefaultModelLoader
loader = DefaultModelLoader()
args.device_str = "cuda:0"
model2 = loader.load_model(model_path, hf_config, args)
# Load the HuggingFace implementation directly
# model2 = CLIPTextModel(hf_config)
model2 = model2.to(torch.float16)
model2 = model2.to(device)
model2.eval()
# Sanity check weights between the two models
logger.info("Comparing model weights for sanity check...")
params1 = dict(model1.named_parameters())
params2 = dict(model2.named_parameters())
# Check number of parameters
logger.info(f"Model1 has {len(params1)} parameters")
logger.info(f"Model2 has {len(params2)} parameters")
# Compare a few key parameters
# weight_diffs = []
# for (name1, param1), (name2, param2) in zip(
# sorted(params1.items()), sorted(params2.items())
# ):
# # if len(weight_diffs) < 5: # Just check a few parameters
# max_diff = torch.max(torch.abs(param1 - param2)).item()
# mean_diff = torch.mean(torch.abs(param1 - param2)).item()
# weight_diffs.append((name1, name2, max_diff, mean_diff))
# logger.info(f"Parameter: {name1} vs {name2}")
# logger.info(f" Max diff: {max_diff}, Mean diff: {mean_diff}")
# Load tokenizer
tokenizer, _ = load_tokenizer(
tokenizer_type="clipL",
tokenizer_path=args.model_path,
logger=logger
)
# Test with some sample prompts
prompts = [
"a photo of a cat",
"a beautiful landscape with mountains",
"an astronaut riding a horse on the moon"
]
logger.info("Testing CLIP text encoder with sample prompts")
with torch.no_grad():
for prompt in prompts:
logger.info(f"Testing prompt: '{prompt}'")
# Tokenize the prompt
tokens = tokenizer(
prompt,
padding="max_length",
max_length=77,
truncation=True,
return_tensors="pt"
).to(device)
# Get embeddings from our implementation
outputs1 = model1(
input_ids=tokens.input_ids,
attention_mask=tokens.attention_mask,
output_hidden_states=True
)
logger.info(f"Testing model2")
# Get embeddings from HuggingFace implementation
outputs2 = model2(
input_ids=tokens.input_ids,
# attention_mask=tokens.attention_mask,
output_hidden_states=True
)
# Compare last hidden states
last_hidden_state1 = outputs1.last_hidden_state[tokens.attention_mask==1]
last_hidden_state2 = outputs2.last_hidden_state[tokens.attention_mask==1]
# print("last_hidden_state1", last_hidden_state1)
# print("last_hidden_state2", last_hidden_state2)
assert last_hidden_state1.shape == last_hidden_state2.shape, \
f"Hidden state shapes don't match: {last_hidden_state1.shape} vs {last_hidden_state2.shape}"
max_diff_hidden = torch.max(torch.abs(last_hidden_state1 - last_hidden_state2))
mean_diff_hidden = torch.mean(torch.abs(last_hidden_state1 - last_hidden_state2))
logger.info(f"Maximum difference in last hidden states: {max_diff_hidden.item()}")
logger.info(f"Mean difference in last hidden states: {mean_diff_hidden.item()}")
# Compare pooler outputs
pooler_output1 = outputs1.pooler_output
pooler_output2 = outputs2.pooler_output
assert pooler_output1.shape == pooler_output2.shape, \
f"Pooler output shapes don't match: {pooler_output1.shape} vs {pooler_output2.shape}"
max_diff_pooler = torch.max(torch.abs(pooler_output1 - pooler_output2))
mean_diff_pooler = torch.mean(torch.abs(pooler_output1 - pooler_output2))
logger.info(f"Maximum difference in pooler outputs: {max_diff_pooler.item()}")
logger.info(f"Mean difference in pooler outputs: {mean_diff_pooler.item()}")
# Check if outputs are similar (allowing for small numerical differences)
assert max_diff_hidden < 1e-4, \
f"Hidden states differ significantly: max diff = {max_diff_hidden.item()}"
assert max_diff_pooler < 1e-4, \
f"Pooler outputs differ significantly: max diff = {max_diff_pooler.item()}"
logger.info("Test passed! Both CLIP text encoder implementations produce similar outputs.")
logger.info("Test completed successfully")
if __name__ == "__main__":
test_clip_encoder()
+161
View File
@@ -0,0 +1,161 @@
from fastvideo.v1.models.vaes.hunyuanvae import AutoencoderKLHunyuanVideo as MyHunyuanVAE
from diffusers import AutoencoderKLHunyuanVideo as DiffusersHunyuanVAE
import os
import torch
import torch.nn as nn
import argparse
import numpy as np
import json
from fastvideo.v1.logger import init_logger
from safetensors.torch import load_file
logger = init_logger(__name__)
def initialize_identical_weights(model1, model2, seed=42):
"""Initialize both models with identical weights using a fixed seed for reproducibility."""
# Get all parameters from both models
params1 = dict(model1.named_parameters())
params2 = dict(model2.named_parameters())
# Initialize each layer with identical values
with torch.no_grad():
# Initialize weights
for name1, param1 in params1.items():
if 'weight' in name1:
# Set seed before each weight initialization
torch.manual_seed(seed)
nn.init.normal_(param1, mean=0.0, std=0.05)
for name2, param2 in params2.items():
if 'weight' in name2:
# Reset seed to get same initialization
torch.manual_seed(seed)
nn.init.normal_(param2, mean=0.0, std=0.05)
# Initialize biases
for name1, param1 in params1.items():
if 'bias' in name1:
torch.manual_seed(seed)
nn.init.normal_(param1, mean=0.0, std=0.05)
param1.data = param1.data.to(torch.bfloat16)
for name2, param2 in params2.items():
if 'bias' in name2:
torch.manual_seed(seed)
nn.init.normal_(param2, mean=0.0, std=0.05)
param2.data = param2.data.to(torch.bfloat16)
logger.info("Both models initialized with identical weights in bfloat16")
return model1, model2
def setup_args():
parser = argparse.ArgumentParser(description='HunyuanVAE Test')
parser.add_argument('--in-channels', type=int, default=4,
help='Number of input channels')
parser.add_argument('--out-channels', type=int, default=4,
help='Number of output channels')
parser.add_argument('--latent-channels', type=int, default=4,
help='Number of latent channels')
return parser.parse_args()
def test_hunyuan_vae():
args = setup_args()
# Set fixed random seed for reproducibility
torch.manual_seed(42)
np.random.seed(42)
# Model parameters
in_channels = args.in_channels
out_channels = args.out_channels
latent_channels = args.latent_channels
device = torch.device("cuda:0")
print
# Initialize the two model implementations
path = "data/hunyuanvideo-community/HunyuanVideo/vae"
config_path = os.path.join(path, "config.json")
config = json.load(open(config_path))
config.pop("_class_name")
config.pop("_diffusers_version")
model1 = MyHunyuanVAE(
**config
).to(torch.bfloat16)
model2 = DiffusersHunyuanVAE(
**config
).to(torch.bfloat16)
loaded = load_file(os.path.join(path, "diffusion_pytorch_model.safetensors"))
model1.load_state_dict(loaded)
model2.load_state_dict(loaded)
# Set both models to eval mode
model1.eval()
model2.eval()
# Move to GPU
model1 = model1.to(device)
model2 = model2.to(device)
model1.enable_tiling(
tile_sample_min_height=32,
tile_sample_min_width=32,
tile_sample_min_num_frames=8,
tile_sample_stride_height=16,
tile_sample_stride_width=16,
tile_sample_stride_num_frames=4
)
model2.enable_tiling(
tile_sample_min_height=32,
tile_sample_min_width=32,
tile_sample_min_num_frames=8,
tile_sample_stride_height=16,
tile_sample_stride_width=16,
tile_sample_stride_num_frames=4
)
# Create identical inputs for both models
batch_size = 1
# Video input [B, C, T, H, W]
input_tensor = torch.randn(batch_size, 3, 21, 64, 64, device=device, dtype=torch.bfloat16)
# Disable gradients for inference
with torch.no_grad():
# Test encoding
logger.info("Testing encoding...")
latent1 = model1.encode(input_tensor).mean
print("--------------------------------")
latent2 = model2.encode(input_tensor).latent_dist.mean
# Check if latents have the same shape
assert latent1.shape == latent2.shape, f"Latent shapes don't match: {latent1.shape} vs {latent2.shape}"
assert latent1.shape == latent2.shape, f"Latent shapes don't match: {latent1.shape} vs {latent2.shape}"
# Check if latents are similar
max_diff_encode = torch.max(torch.abs(latent1 - latent2))
mean_diff_encode = torch.mean(torch.abs(latent1 - latent2))
logger.info(f"Maximum difference between encoded latents: {max_diff_encode.item()}")
logger.info(f"Mean difference between encoded latents: {mean_diff_encode.item()}")
assert max_diff_encode < 1e-4, f"Encoded latents differ significantly: max diff = {max_diff_encode.item()}"
# Test decoding
logger.info("Testing decoding...")
output1 = model1.decode(latent1)
output2 = model2.decode(latent2).sample
# Check if outputs have the same shape
assert output1.shape == output2.shape, f"Output shapes don't match: {output1.shape} vs {output2.shape}"
# Check if outputs are similar
max_diff_decode = torch.max(torch.abs(output1 - output2))
mean_diff_decode = torch.mean(torch.abs(output1 - output2))
logger.info(f"Maximum difference between decoded outputs: {max_diff_decode.item()}")
logger.info(f"Mean difference between decoded outputs: {mean_diff_decode.item()}")
assert max_diff_decode < 1e-4, f"Decoded outputs differ significantly: max diff = {max_diff_decode.item()}"
logger.info("Test passed! Both VAE implementations produce similar outputs.")
logger.info("Test completed successfully")
if __name__ == "__main__":
test_hunyuan_vae()

Some files were not shown because too many files have changed in this diff Show More