prototype seedvr

Signed-off-by: Vladimir Mandic <mandic00@live.com>
This commit is contained in:
Vladimir Mandic
2025-10-11 18:35:40 -04:00
parent a376f89fd6
commit eaa7dc119b
87 changed files with 11024 additions and 5 deletions
+1
View File
@@ -29,6 +29,7 @@ ignore-paths=/usr/lib/.*$,
modules/postprocess/aurasr_arch.py,
modules/prompt_parser_xhinker.py,
modules/ras,
modules/seedvr,
modules/rife,
modules/schedulers,
modules/taesd,
+1
View File
@@ -12,6 +12,7 @@ exclude = [
"modules/pag",
"modules/schedulers",
"modules/teacache",
"modules/seedvr",
"modules/control/proc",
"modules/control/units",
+4 -3
View File
@@ -42,12 +42,13 @@
- **ROCm** for Windows
support for both official torch preview release of `torch-rocm` for windows and **TheRock** unoffical `torch-rocm` builds for windows
note that rocm for windows is still in preview and has limited gpu support, please check rocm docs for details
- **DirectML** warn as end-of-life
- **DirectML** warn as *end-of-life*
`torch-directml` received no updates in over 1 year and its currently superceded by `rocm` or `zluda`
- command line params `--use-zluda` and `--use-rocm` will attempt desired operation or fail if not possible
previously sdnext was performing a fallback to `torch-cpu` which is not desired
- if `--use-cuda` or `--use-rocm` are specified and `torch-cpu` is installed, installer will attempt to reinstall correct torch package
- **installer**: warn if cuda or rocm are available and `torch-cpu` is installed
- **installer** if `--use-cuda` or `--use-rocm` are specified and `torch-cpu` is installed, installer will attempt to reinstall correct torch package
- **installer** warn if *cuda* or *rocm* are available and `torch-cpu` is installed
- support for `torch==2.10-nightly` with `cuda==13.0`
- **Extensions**
- [Agent-Scheduler](https://github.com/SipherAGI/sd-webui-agent-scheduler)
was a high-value built-in extension, but it has not been maintained for 1.5 years
+1
View File
@@ -1353,6 +1353,7 @@ def set_environment():
allocator += ',backend:cudaMallocAsync'
if opts.get("torch_expandable_segments", False):
allocator += ',expandable_segments:True'
os.environ.setdefault('PYTORCH_ALLOC_CONF', allocator)
os.environ.setdefault('PYTORCH_CUDA_ALLOC_CONF', allocator)
os.environ.setdefault('PYTORCH_HIP_ALLOC_CONF', allocator)
log.debug(f'Torch allocator: "{allocator}"')
+76
View File
@@ -0,0 +1,76 @@
import time
import numpy as np
import torch
from PIL import Image
from torchvision.transforms import ToPILImage
from rich.progress import Progress, TextColumn, BarColumn, TaskProgressColumn, TimeRemainingColumn, TimeElapsedColumn
from modules import devices
from modules.shared import opts, log
from modules.upscaler import Upscaler, UpscalerData
MODELS_MAP = {
"SeedVR2 3B": "seedvr2_ema_3b_fp16.safetensors",
"SeedVR2 7B": "seedvr2_ema_7b_fp16.safetensors",
"SeedVR2 7B Sharp": "seedvr2_ema_7b_sharp_fp16.safetensors",
}
to_pil = ToPILImage()
class UpscalerSeedVR(Upscaler):
def __init__(self, dirname=None):
self.name = "SeedVR"
super().__init__()
self.scalers = [
UpscalerData(name="SeedVR2 3B", path=None, upscaler=self, model=None, scale=1),
UpscalerData(name="SeedVR2 7B", path=None, upscaler=self, model=None, scale=1),
UpscalerData(name="SeedVR2 7B Sharp", path=None, upscaler=self, model=None, scale=1),
]
self.model = None
self.model_loaded = None
def load_model(self, path: str):
model_name = MODELS_MAP.get(path, None)
if (self.model is None) or (self.model_loaded != model_name):
log.debug(f'Upscaler load: name="{self.name}" model="{model_name}"')
from modules.seedvr.src.core.model_manager import configure_runner
self.model = configure_runner(
model_name=model_name,
cache_dir=opts.hfcache_dir,
device=devices.device,
dtype=devices.dtype,
)
def do_upscale(self, img: Image.Image, selected_file):
devices.torch_gc()
self.load_model(selected_file)
if self.model is None:
return img
from modules.seedvr.src.core.generation import generation_loop
width = int(self.scale * img.width) // 8 * 8
image_tensor = np.array(img)
image_tensor = torch.from_numpy(image_tensor).to(device=devices.device, dtype=devices.dtype).unsqueeze(0) / 255.0
t0 = time.time()
result_tensor = generation_loop(
runner=self.model,
images=image_tensor,
cfg_scale=1.0,
seed=42,
res_w=width,
batch_size=1,
temporal_overlap=0,
device=devices.device,
)
t1 = time.time()
log.info(f'Upscaler: type="{self.name}" model="{selected_file}" scale={self.scale} time={t1 - t0:.2f}')
img = to_pil(result_tensor.squeeze().permute((2, 0, 1)))
devices.torch_gc()
if opts.upscaler_unload:
self.model = None
log.debug(f'Upscaler unload: type="{self.name}" model="{selected_file}"')
devices.torch_gc(force=True)
return img
View File
+97
View File
@@ -0,0 +1,97 @@
__object__:
path: projects.video_diffusion_sr.train
name: VideoDiffusionTrainer
dit:
model:
__object__:
path:
- "custom_nodes.ComfyUI-SeedVR2_VideoUpscaler.src.models.dit_v2.nadit"
- "ComfyUI.custom_nodes.ComfyUI-SeedVR2_VideoUpscaler.src.models.dit_v2.nadit"
- "src.models.dit_v2.nadit"
name: "NaDiT"
args: "as_params"
vid_in_channels: 33
vid_out_channels: 16
vid_dim: 2560
vid_out_norm: fusedrms
txt_in_dim: 5120
txt_in_norm: fusedln
txt_dim: ${.vid_dim}
emb_dim: ${eval:'6 * ${.vid_dim}'}
heads: 20
head_dim: 128 # llm-like
expand_ratio: 4
norm: fusedrms
norm_eps: 1.0e-05
ada: single
qk_bias: False
qk_norm: fusedrms
patch_size: [1, 2, 2]
num_layers: 32 # llm-like
mm_layers: 10
mlp_type: swiglu
msa_type: None
block_type: ${eval:'${.num_layers} * ["mmdit_sr"]'} # space-full
window: ${eval:'${.num_layers} * [(4,3,3)]'} # space-full
window_method: ${eval:'${.num_layers} // 2 * ["720pwin_by_size_bysize","720pswin_by_size_bysize"]'} # space-full
rope_type: mmrope3d
rope_dim: 128
compile: False
gradient_checkpoint: True
fsdp:
sharding_strategy: _HYBRID_SHARD_ZERO2
ema:
decay: 0.9998
vae:
model:
__object__:
path:
- "custom_nodes.ComfyUI-SeedVR2_VideoUpscaler.src.models.video_vae_v3.modules.attn_video_vae"
- "ComfyUI.custom_nodes.ComfyUI-SeedVR2_VideoUpscaler.src.models.video_vae_v3.modules.attn_video_vae"
- "src.models.video_vae_v3.modules.attn_video_vae"
name: "VideoAutoencoderKLWrapper"
args: "as_params"
freeze_encoder: False
gradient_checkpoint: True # Disabled to prevent VRAM leaks in inference
slicing:
split_size: 4
memory_device: same
memory_limit:
conv_max_mem: 0.5
norm_max_mem: 0.5
checkpoint: ema_vae_fp16.safetensors
scaling_factor: 0.9152
compile: False
grouping: False
dtype: float16
diffusion:
schedule:
type: lerp
T: 1000.0
sampler:
type: euler
prediction_type: v_lerp
timesteps:
training:
type: logitnormal
loc: 0.0
scale: 1.0
sampling:
type: uniform_trailing
steps: 50
transform: True
loss:
type: v_lerp
cfg:
scale: 7.5
rescale: 0
condition:
i2v: 0.0
v2v: 0.0
sr: 1.0
noise_scale: 0.25
+94
View File
@@ -0,0 +1,94 @@
__object__:
path: projects.video_diffusion_sr.train
name: VideoDiffusionTrainer
dit:
model:
__object__:
path:
- "custom_nodes.ComfyUI-SeedVR2_VideoUpscaler.src.models.dit.nadit"
- "ComfyUI.custom_nodes.ComfyUI-SeedVR2_VideoUpscaler.src.models.dit.nadit"
- "src.models.dit.nadit"
name: "NaDiT"
args: "as_params"
vid_in_channels: 33
vid_out_channels: 16
vid_dim: 3072
txt_in_dim: 5120
txt_dim: ${.vid_dim}
emb_dim: ${eval:'6 * ${.vid_dim}'}
heads: 24
head_dim: 128 # llm-like
expand_ratio: 4
norm: fusedrms
norm_eps: 1e-5
ada: single
qk_bias: False
qk_rope: True
qk_norm: fusedrms
patch_size: [1, 2, 2]
num_layers: 36 # llm-like
shared_mlp: False
shared_qkv: False
mlp_type: normal
block_type: ${eval:'${.num_layers} * ["mmdit_sr"]'} # space-full
window: ${eval:'${.num_layers} * [(4,3,3)]'} # space-full
window_method: ${eval:'${.num_layers} // 2 * ["720pwin_by_size_bysize","720pswin_by_size_bysize"]'} # space-full
compile: False
gradient_checkpoint: True
fsdp:
sharding_strategy: _HYBRID_SHARD_ZERO2
ema:
decay: 0.9998
vae:
model:
__object__:
path:
- "custom_nodes.ComfyUI-SeedVR2_VideoUpscaler.src.models.video_vae_v3.modules.attn_video_vae"
- "ComfyUI.custom_nodes.ComfyUI-SeedVR2_VideoUpscaler.src.models.video_vae_v3.modules.attn_video_vae"
- "src.models.video_vae_v3.modules.attn_video_vae"
name: "VideoAutoencoderKLWrapper"
args: "as_params"
freeze_encoder: False
# gradient_checkpoint: True
slicing:
split_size: 4
memory_device: same
memory_limit:
conv_max_mem: 0.5
norm_max_mem: 0.5
checkpoint: ema_vae_fp16.safetensors
scaling_factor: 0.9152
compile: False
grouping: False
dtype: float16
diffusion:
schedule:
type: lerp
T: 1000.0
sampler:
type: euler
prediction_type: v_lerp
timesteps:
training:
type: logitnormal
loc: 0.0
scale: 1.0
sampling:
type: uniform_trailing
steps: 50
transform: True
loss:
type: v_lerp
cfg:
scale: 7.5
rescale: 0
condition:
i2v: 0.0
v2v: 0.0
sr: 1.0
noise_scale: 0.25
+37
View File
@@ -0,0 +1,37 @@
"""
SeedVR2 Video Upscaler - Modular Architecture
Refactored from monolithic seedvr2.py for better maintainability
Author: Refactored codebase
Version: 2.0.0 - Modular
Available Modules:
- utils: Download and path utilities
- optimization: Memory, performance, and compatibility optimizations
- core: Model management and generation pipeline (NEW)
- processing: Video and tensor processing (coming next)
- interfaces: ComfyUI integration
"""
'''
# Track which modules are available for progressive migration
MODULES_AVAILABLE = {
'downloads': True, # ✅ Module 1 - Downloads and model management
'memory_manager': True, # ✅ Module 2 - Memory optimization
'performance': True, # ✅ Module 3 - Performance optimizations
'compatibility': True, # ✅ Module 4 - FP8/FP16 compatibility
'model_manager': True, # ✅ Module 5 - Model configuration and loading
'generation': True, # ✅ Module 6 - Generation loop and inference
'video_transforms': True, # ✅ Module 7 - Video processing and transforms
'comfyui_node': True, # ✅ Module 8 - ComfyUI node interface (COMPLETE!)
'infer': True, # ✅ Module 9 - Infer
}
'''
# Core imports (always available)
import os
import sys
# Add current directory to path for fallback imports
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
if parent_dir not in sys.path:
sys.path.insert(0, parent_dir)
+33
View File
@@ -0,0 +1,33 @@
from typing import Callable
class Cache:
"""Caching reusable args for faster inference"""
def __init__(self, disable=False, prefix="", cache=None):
self.cache = cache if cache is not None else {}
self.disable = disable
self.prefix = prefix
def __call__(self, key: str, fn: Callable):
if self.disable:
return fn()
key = self.prefix + key
try:
result = self.cache[key]
except KeyError:
result = fn()
self.cache[key] = result
return result
def namespace(self, namespace: str):
return Cache(
disable=self.disable,
prefix=self.prefix + namespace + ".",
cache=self.cache,
)
def get(self, key: str):
key = self.prefix + key
return self.cache[key]
+128
View File
@@ -0,0 +1,128 @@
import importlib
from typing import Any, Callable, List, Union
from omegaconf import DictConfig, ListConfig, OmegaConf
try:
OmegaConf.register_new_resolver("eval", eval)
except Exception as e:
if "already registered" not in str(e):
raise
def load_config(path: str, argv: List[str] = None) -> Union[DictConfig, ListConfig]:
"""
Load a configuration. Will resolve inheritance.
"""
config = OmegaConf.load(path)
if argv is not None:
config_argv = OmegaConf.from_dotlist(argv)
config = OmegaConf.merge(config, config_argv)
config = resolve_recursive(config, resolve_inheritance)
return config
def resolve_recursive(
config: Any,
resolver: Callable[[Union[DictConfig, ListConfig]], Union[DictConfig, ListConfig]],
) -> Any:
config = resolver(config)
if isinstance(config, DictConfig):
for k in config.keys():
v = config.get(k)
if isinstance(v, (DictConfig, ListConfig)):
config[k] = resolve_recursive(v, resolver)
if isinstance(config, ListConfig):
for i in range(len(config)):
v = config.get(i)
if isinstance(v, (DictConfig, ListConfig)):
config[i] = resolve_recursive(v, resolver)
return config
def resolve_inheritance(config: Union[DictConfig, ListConfig]) -> Any:
"""
Recursively resolve inheritance if the config contains:
__inherit__: path/to/parent.yaml or a ListConfig of such paths.
"""
if isinstance(config, DictConfig):
inherit = config.pop("__inherit__", None)
if inherit:
inherit_list = inherit if isinstance(inherit, ListConfig) else [inherit]
parent_config = None
for parent_path in inherit_list:
assert isinstance(parent_path, str)
parent_config = (
load_config(parent_path)
if parent_config is None
else OmegaConf.merge(parent_config, load_config(parent_path))
)
if len(config.keys()) > 0:
config = OmegaConf.merge(parent_config, config)
else:
config = parent_config
return config
def import_item(path: Union[str, List[str]], name: str) -> Any:
"""
Import a python item with fallback support.
Args:
path: Single path string or list of paths to try (fallback order)
name: Class/function name to import
Returns:
Imported object
Example:
import_item("path.to.file", "MyClass") -> MyClass
import_item(["path1.to.file", "path2.to.file"], "MyClass") -> MyClass (first working path)
"""
if isinstance(path, str):
# Single path - original behavior
return getattr(importlib.import_module(path), name)
elif isinstance(path, (list, ListConfig)):
# Multiple paths - try each until one works
last_error = None
for single_path in path:
try:
return getattr(importlib.import_module(single_path), name)
except ImportError as e:
last_error = e
continue
# If we get here, none of the paths worked
raise ImportError(f"Could not import '{name}' from any of the paths: {path}. Last error: {last_error}")
else:
raise ValueError(f"Path must be string or list of strings, got: {type(path)}")
def create_object(config: DictConfig) -> Any:
"""
Create an object from config.
The config is expected to contains the following:
__object__:
path: path.to.module
name: MyClass
args: as_config | as_params (default to as_config)
"""
item = import_item(
path=config.__object__.path,
name=config.__object__.name,
)
args = config.__object__.get("args", "as_config")
if args == "as_config":
return item(config)
if args == "as_params":
config = OmegaConf.to_object(config)
config.pop("__object__")
return item(**config)
raise NotImplementedError(f"Unknown args type: {args}")
+126
View File
@@ -0,0 +1,126 @@
import functools
import threading
from typing import Callable
import torch
from .distributed import barrier_if_distributed, get_global_rank, get_local_rank
from .logger import get_logger
logger = get_logger(__name__)
def log_on_entry(func: Callable) -> Callable:
"""
Functions with this decorator will log the function name at entry.
When using multiple decorators, this must be applied innermost to properly capture the name.
"""
def log_on_entry_wrapper(*args, **kwargs):
logger.info(f"Entering {func.__name__}")
return func(*args, **kwargs)
return log_on_entry_wrapper
def barrier_on_entry(func: Callable) -> Callable:
"""
Functions with this decorator will start executing when all ranks are ready to enter.
"""
def barrier_on_entry_wrapper(*args, **kwargs):
barrier_if_distributed()
return func(*args, **kwargs)
return barrier_on_entry_wrapper
def _conditional_execute_wrapper_factory(execute: bool, func: Callable) -> Callable:
"""
Helper function for local_rank_zero_only and global_rank_zero_only.
"""
def conditional_execute_wrapper(*args, **kwargs):
# Only execute if needed.
result = func(*args, **kwargs) if execute else None
# All GPUs must wait.
barrier_if_distributed()
# Return results.
return result
return conditional_execute_wrapper
def _asserted_wrapper_factory(condition: bool, func: Callable, err_msg: str = "") -> Callable:
"""
Helper function for some functions with special constraints,
especially functions called by other global_rank_zero_only / local_rank_zero_only ones,
in case they are wrongly invoked in other scenarios.
"""
def asserted_execute_wrapper(*args, **kwargs):
assert condition, err_msg
result = func(*args, **kwargs)
return result
return asserted_execute_wrapper
def local_rank_zero_only(func: Callable) -> Callable:
"""
Functions with this decorator will only execute on local rank zero.
"""
return _conditional_execute_wrapper_factory(get_local_rank() == 0, func)
def global_rank_zero_only(func: Callable) -> Callable:
"""
Functions with this decorator will only execute on global rank zero.
"""
return _conditional_execute_wrapper_factory(get_global_rank() == 0, func)
def assert_only_global_rank_zero(func: Callable) -> Callable:
"""
Functions with this decorator are only accessible to processes with global rank zero.
"""
return _asserted_wrapper_factory(
get_global_rank() == 0, func, err_msg="Not accessible to processes with global_rank != 0"
)
def assert_only_local_rank_zero(func: Callable) -> Callable:
"""
Functions with this decorator are only accessible to processes with local rank zero.
"""
return _asserted_wrapper_factory(
get_local_rank() == 0, func, err_msg="Not accessible to processes with local_rank != 0"
)
def new_thread(func: Callable) -> Callable:
"""
Functions with this decorator will run in a new thread.
The function will return the thread, which can be joined to wait for completion.
"""
def new_thread_wrapper(*args, **kwargs):
thread = threading.Thread(target=func, args=args, kwargs=kwargs)
thread.start()
return thread
return new_thread_wrapper
def log_runtime(func: Callable) -> Callable:
"""
Functions with this decorator will logging the runtime.
"""
@functools.wraps(func)
def wrapped(*args, **kwargs):
torch.distributed.barrier()
result = func(*args, **kwargs)
torch.distributed.barrier()
return result
return wrapped
@@ -0,0 +1,56 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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.
"""
Diffusion package.
"""
from .config import (
create_sampler_from_config,
create_sampling_timesteps_from_config,
create_schedule_from_config,
)
from .samplers.base import Sampler
from .samplers.euler import EulerSampler
from .schedules.base import Schedule
from .schedules.lerp import LinearInterpolationSchedule
from .timesteps.base import SamplingTimesteps, Timesteps
from .timesteps.sampling.trailing import UniformTrailingSamplingTimesteps
from .types import PredictionType, SamplingDirection
from .utils import classifier_free_guidance, classifier_free_guidance_dispatcher, expand_dims
__all__ = [
# Configs
"create_sampler_from_config",
"create_sampling_timesteps_from_config",
"create_schedule_from_config",
# Schedules
"Schedule",
"DiscreteVariancePreservingSchedule",
"LinearInterpolationSchedule",
# Samplers
"Sampler",
"EulerSampler",
# Timesteps
"Timesteps",
"SamplingTimesteps",
# Types
"PredictionType",
"SamplingDirection",
"UniformTrailingSamplingTimesteps",
# Utils
"classifier_free_guidance",
"classifier_free_guidance_dispatcher",
"expand_dims",
]
@@ -0,0 +1,71 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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.
"""
Utility functions for creating schedules and samplers from config.
"""
import torch
from omegaconf import DictConfig
from .samplers.base import Sampler
from .samplers.euler import EulerSampler
from .schedules.base import Schedule
from .schedules.lerp import LinearInterpolationSchedule
from .timesteps.base import SamplingTimesteps
from .timesteps.sampling.trailing import UniformTrailingSamplingTimesteps
def create_schedule_from_config(
config: DictConfig,
) -> Schedule:
"""
Create a schedule from configuration.
"""
if config.type == "lerp":
return LinearInterpolationSchedule(T=config.get("T", 1.0))
raise NotImplementedError
def create_sampler_from_config(
config: DictConfig,
schedule: Schedule,
timesteps: SamplingTimesteps,
) -> Sampler:
"""
Create a sampler from configuration.
"""
if config.type == "euler":
return EulerSampler(
schedule=schedule,
timesteps=timesteps,
prediction_type=config.prediction_type,
)
raise NotImplementedError
def create_sampling_timesteps_from_config(
config: DictConfig,
schedule: Schedule,
device: torch.device,
) -> SamplingTimesteps:
if config.type == "uniform_trailing":
return UniformTrailingSamplingTimesteps(
T=schedule.T,
steps=config.steps,
shift=config.get("shift", 1.0),
device=device,
)
raise NotImplementedError
@@ -0,0 +1,108 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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.
"""
Sampler base class.
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Callable
import torch
from tqdm import tqdm
from ..schedules.base import Schedule
from ..timesteps.base import SamplingTimesteps
from ..types import PredictionType, SamplingDirection
from ..utils import assert_schedule_timesteps_compatible
@dataclass
class SamplerModelArgs:
x_t: torch.Tensor
t: torch.Tensor
i: int
class Sampler(ABC):
"""
Samplers are ODE/SDE solvers.
"""
def __init__(
self,
schedule: Schedule,
timesteps: SamplingTimesteps,
prediction_type: PredictionType,
return_endpoint: bool = True,
):
assert_schedule_timesteps_compatible(
schedule=schedule,
timesteps=timesteps,
)
self.schedule = schedule
self.timesteps = timesteps
self.prediction_type = prediction_type
self.return_endpoint = return_endpoint
@abstractmethod
def sample(
self,
x: torch.Tensor,
f: Callable[[SamplerModelArgs], torch.Tensor],
) -> torch.Tensor:
"""
Generate a new sample given the the intial sample x and score function f.
"""
def get_next_timestep(
self,
t: torch.Tensor,
) -> torch.Tensor:
"""
Get the next sample timestep.
Support multiple different timesteps t in a batch.
If no more steps, return out of bound value -1 or T+1.
"""
T = self.timesteps.T
steps = len(self.timesteps)
curr_idx = self.timesteps.index(t)
next_idx = curr_idx + 1
bound = -1 if self.timesteps.direction == SamplingDirection.backward else T + 1
s = self.timesteps[next_idx.clamp_max(steps - 1)]
s = s.where(next_idx < steps, bound)
return s
def get_endpoint(
self,
pred: torch.Tensor,
x_t: torch.Tensor,
t: torch.Tensor,
) -> torch.Tensor:
"""
Get to the endpoint of the probability flow.
"""
x_0, x_T = self.schedule.convert_from_pred(pred, self.prediction_type, x_t, t)
return x_0 if self.timesteps.direction == SamplingDirection.backward else x_T
def get_progress_bar(self):
"""
Get progress bar for sampling.
"""
return tqdm(
iterable=range(len(self.timesteps) - (0 if self.return_endpoint else 1)),
dynamic_ncols=True,
desc=self.__class__.__name__,
)
@@ -0,0 +1,107 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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.
"""
Euler ODE solver.
"""
from typing import Callable
import torch
from einops import rearrange
from torch.nn import functional as F
#from ....models.dit_v2 import na
from ..types import PredictionType
from ..utils import expand_dims
from .base import Sampler, SamplerModelArgs
class EulerSampler(Sampler):
"""
The Euler method is the simplest ODE solver.
<https://en.wikipedia.org/wiki/Euler_method>
"""
def sample(
self,
x: torch.Tensor,
f: Callable[[SamplerModelArgs], torch.Tensor],
) -> torch.Tensor:
timesteps = self.timesteps.timesteps
#progress = self.get_progress_bar()
i = 0
# Optimisations VRAM
original_dtype = x.dtype
device = x.device
for t, s in zip(timesteps[:-1], timesteps[1:]):
# Appel du modèle avec monitoring
pred = f(SamplerModelArgs(x, t, i))
# Étape suivante
x = self.step_to(pred, x, t, s)
# Nettoyer les tenseurs temporaires
del pred
i += 1
#progress.update()
if self.return_endpoint:
t = timesteps[-1]
pred = f(SamplerModelArgs(x, t, i))
x = self.get_endpoint(pred, x, t)
del pred
#progress.update()
# Restaurer le dtype original si nécessaire
if original_dtype != torch.float16:
x = x.to(original_dtype)
return x
def step(
self,
pred: torch.Tensor,
x_t: torch.Tensor,
t: torch.Tensor,
) -> torch.Tensor:
"""
Step to the next timestep.
"""
return self.step_to(pred, x_t, t, self.get_next_timestep(t))
def step_to(
self,
pred: torch.Tensor,
x_t: torch.Tensor,
t: torch.Tensor,
s: torch.Tensor,
) -> torch.Tensor:
"""
Steps from x_t at timestep t to x_s at timestep s. Returns x_s.
"""
t = expand_dims(t, x_t.ndim)
s = expand_dims(s, x_t.ndim)
T = self.schedule.T
# Step from x_t to x_s.
pred_x_0, pred_x_T = self.schedule.convert_from_pred(pred, self.prediction_type, x_t, t)
pred_x_s = self.schedule.forward(pred_x_0, pred_x_T, s.clamp(0, T))
# Clamp x_s to x_0 and x_T if s is out of bound.
pred_x_s = pred_x_s.where(s >= 0, pred_x_0)
pred_x_s = pred_x_s.where(s <= T, pred_x_T)
return pred_x_s
@@ -0,0 +1,131 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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.
"""
Schedule base class.
"""
from abc import ABC, abstractmethod, abstractproperty
from typing import Tuple, Union
import torch
from ..types import PredictionType
from ..utils import expand_dims
class Schedule(ABC):
"""
Diffusion schedules are uniquely defined by T, A, B:
x_t = A(t) * x_0 + B(t) * x_T, where t in [0, T]
Schedules can be continuous or discrete.
"""
@abstractproperty
def T(self) -> Union[int, float]:
"""
Maximum timestep inclusive.
Schedule is continuous if float, discrete if int.
"""
@abstractmethod
def A(self, t: torch.Tensor) -> torch.Tensor:
"""
Interpolation coefficient A.
Returns tensor with the same shape as t.
"""
@abstractmethod
def B(self, t: torch.Tensor) -> torch.Tensor:
"""
Interpolation coefficient B.
Returns tensor with the same shape as t.
"""
# ----------------------------------------------------
def snr(self, t: torch.Tensor) -> torch.Tensor:
"""
Signal to noise ratio.
Returns tensor with the same shape as t.
"""
return (self.A(t) ** 2) / (self.B(t) ** 2)
def isnr(self, snr: torch.Tensor) -> torch.Tensor:
"""
Inverse signal to noise ratio.
Returns tensor with the same shape as snr.
Subclass may implement.
"""
raise NotImplementedError
# ----------------------------------------------------
def is_continuous(self) -> bool:
"""
Whether the schedule is continuous.
"""
return isinstance(self.T, float)
def forward(self, x_0: torch.Tensor, x_T: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
"""
Diffusion forward function.
"""
t = expand_dims(t, x_0.ndim)
return self.A(t) * x_0 + self.B(t) * x_T
def convert_from_pred(
self, pred: torch.Tensor, pred_type: PredictionType, x_t: torch.Tensor, t: torch.Tensor
) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Convert from prediction. Return predicted x_0 and x_T.
"""
t = expand_dims(t, x_t.ndim)
A_t = self.A(t)
B_t = self.B(t)
if pred_type == PredictionType.x_T:
pred_x_T = pred
pred_x_0 = (x_t - B_t * pred_x_T) / A_t
elif pred_type == PredictionType.x_0:
pred_x_0 = pred
pred_x_T = (x_t - A_t * pred_x_0) / B_t
elif pred_type == PredictionType.v_cos:
pred_x_0 = A_t * x_t - B_t * pred
pred_x_T = A_t * pred + B_t * x_t
elif pred_type == PredictionType.v_lerp:
pred_x_0 = (x_t - B_t * pred) / (A_t + B_t)
pred_x_T = (x_t + A_t * pred) / (A_t + B_t)
else:
raise NotImplementedError
return pred_x_0, pred_x_T
def convert_to_pred(
self, x_0: torch.Tensor, x_T: torch.Tensor, t: torch.Tensor, pred_type: PredictionType
) -> torch.FloatTensor:
"""
Convert to prediction target given x_0 and x_T.
"""
if pred_type == PredictionType.x_T:
return x_T
if pred_type == PredictionType.x_0:
return x_0
if pred_type == PredictionType.v_cos:
t = expand_dims(t, x_0.ndim)
return self.A(t) * x_T - self.B(t) * x_0
if pred_type == PredictionType.v_lerp:
return x_T - x_0
raise NotImplementedError
@@ -0,0 +1,55 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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.
"""
Linear interpolation schedule (lerp).
"""
from typing import Union
import torch
from .base import Schedule
class LinearInterpolationSchedule(Schedule):
"""
Linear interpolation schedule (lerp) is proposed by flow matching and rectified flow.
It leads to straighter probability flow theoretically. It is also used by Stable Diffusion 3.
<https://arxiv.org/abs/2209.03003>
<https://arxiv.org/abs/2210.02747>
x_t = (1 - t) * x_0 + t * x_T
Can be either continuous or discrete.
"""
def __init__(self, T: Union[int, float] = 1.0):
self._T = T
@property
def T(self) -> Union[int, float]:
return self._T
def A(self, t: torch.Tensor) -> torch.Tensor:
return 1 - (t / self.T)
def B(self, t: torch.Tensor) -> torch.Tensor:
return t / self.T
# ----------------------------------------------------
def isnr(self, snr: torch.Tensor) -> torch.Tensor:
t = self.T / (1 + snr**0.5)
t = t if self.is_continuous() else t.round().int()
return t
@@ -0,0 +1,72 @@
from abc import ABC, abstractmethod
from typing import Sequence, Union
import torch
from ..types import SamplingDirection
class Timesteps(ABC):
"""
Timesteps base class.
"""
def __init__(self, T: Union[int, float]):
assert T > 0
self._T = T
@property
def T(self) -> Union[int, float]:
"""
Maximum timestep inclusive.
int if discrete, float if continuous.
"""
return self._T
def is_continuous(self) -> bool:
"""
Whether the schedule is continuous.
"""
return isinstance(self.T, float)
class SamplingTimesteps(Timesteps):
"""
Sampling timesteps.
It defines the discretization of sampling steps.
"""
def __init__(
self,
T: Union[int, float],
timesteps: torch.Tensor,
direction: SamplingDirection,
):
assert timesteps.ndim == 1
super().__init__(T)
self.timesteps = timesteps
self.direction = direction
def __len__(self) -> int:
"""
Number of sampling steps.
"""
return len(self.timesteps)
def __getitem__(self, idx: Union[int, torch.IntTensor]) -> torch.Tensor:
"""
The timestep at the sampling step.
Returns a scalar tensor if idx is int,
or tensor of the same size if idx is a tensor.
"""
return self.timesteps[idx]
def index(self, t: torch.Tensor) -> torch.Tensor:
"""
Find index by t.
Return index of the same shape as t.
Index is -1 if t not found in timesteps.
"""
i, j = t.reshape(-1, 1).eq(self.timesteps).nonzero(as_tuple=True)
idx = torch.full_like(t, fill_value=-1, dtype=torch.int)
idx.view(-1)[i] = j.int()
return idx
@@ -0,0 +1,49 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // Licensed under the Apache License, Version 2.0 (the "License");
# // you may not use this file except in compliance with the License.
# // You may obtain a copy of the License at
# //
# // http://www.apache.org/licenses/LICENSE-2.0
# //
# // Unless required by applicable law or agreed to in writing, software
# // distributed under the License is distributed on an "AS IS" BASIS,
# // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# // See the License for the specific language governing permissions and
# // limitations under the License.
import torch
from ...types import SamplingDirection
from ..base import SamplingTimesteps
class UniformTrailingSamplingTimesteps(SamplingTimesteps):
"""
Uniform trailing sampling timesteps.
Defined in (https://arxiv.org/abs/2305.08891)
Shift is proposed in SD3 for RF schedule.
Defined in (https://arxiv.org/pdf/2403.03206) eq.23
"""
def __init__(
self,
T: int,
steps: int,
shift: float = 1.0,
device: torch.device = "cpu",
):
# Create trailing timesteps.
timesteps = torch.arange(1.0, 0.0, -1.0 / steps, device=device)
# Shift timesteps.
timesteps = shift * timesteps / (1 + (shift - 1) * timesteps)
# Scale to T range.
if isinstance(T, float):
timesteps = timesteps * T
else:
timesteps = timesteps.mul(T + 1).sub(1).round().int()
super().__init__(T=T, timesteps=timesteps, direction=SamplingDirection.backward)
@@ -0,0 +1,59 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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.
"""
Type definitions.
"""
from enum import Enum
class PredictionType(str, Enum):
"""
x_0:
Predict data sample.
x_T:
Predict noise sample.
Proposed by DDPM (https://arxiv.org/abs/2006.11239)
Proved problematic by zsnr paper (https://arxiv.org/abs/2305.08891)
v_cos:
Predict velocity dx/dt based on the cosine schedule (A_t * x_T - B_t * x_0).
Proposed by progressive distillation (https://arxiv.org/abs/2202.00512)
v_lerp:
Predict velocity dx/dt based on the lerp schedule (x_T - x_0).
Proposed by rectified flow (https://arxiv.org/abs/2209.03003)
"""
x_0 = "x_0"
x_T = "x_T"
v_cos = "v_cos"
v_lerp = "v_lerp"
class SamplingDirection(str, Enum):
"""
backward: Sample from x_T to x_0 for data generation.
forward: Sample from x_0 to x_T for noise inversion.
"""
backward = "backward"
forward = "forward"
@staticmethod
def reverse(direction):
if direction == SamplingDirection.backward:
return SamplingDirection.forward
if direction == SamplingDirection.forward:
return SamplingDirection.backward
raise NotImplementedError
@@ -0,0 +1,84 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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.
"""
Utility functions.
"""
from typing import Callable
import torch
def expand_dims(tensor: torch.Tensor, ndim: int):
"""
Expand tensor to target ndim. New dims are added to the right.
For example, if the tensor shape was (8,), target ndim is 4, return (8, 1, 1, 1).
"""
shape = tensor.shape + (1,) * (ndim - tensor.ndim)
return tensor.reshape(shape)
def assert_schedule_timesteps_compatible(schedule, timesteps):
"""
Check if schedule and timesteps are compatible.
"""
if schedule.T != timesteps.T:
raise ValueError("Schedule and timesteps must have the same T.")
if schedule.is_continuous() != timesteps.is_continuous():
raise ValueError("Schedule and timesteps must have the same continuity.")
def classifier_free_guidance(
pos: torch.Tensor,
neg: torch.Tensor,
scale: float,
rescale: float = 0.0,
):
"""
Apply classifier-free guidance.
"""
# Classifier-free guidance (https://arxiv.org/abs/2207.12598)
cfg = neg + scale * (pos - neg)
# Classifier-free guidance rescale (https://arxiv.org/pdf/2305.08891.pdf)
if rescale != 0.0:
pos_std = pos.std(dim=list(range(1, pos.ndim)), keepdim=True)
cfg_std = cfg.std(dim=list(range(1, cfg.ndim)), keepdim=True)
factor = pos_std / cfg_std
factor = rescale * factor + (1 - rescale)
cfg *= factor
return cfg
def classifier_free_guidance_dispatcher(
pos: Callable,
neg: Callable,
scale: float,
rescale: float = 0.0,
):
"""
Optionally execute models depending on classifer-free guidance scale.
"""
# If scale is 1, no need to execute neg model.
if scale == 1.0:
return pos()
# Otherwise, execute both pos nad neg models and apply cfg.
return classifier_free_guidance(
pos=pos(),
neg=neg(),
scale=scale,
rescale=rescale,
)
@@ -0,0 +1,35 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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.
"""
Distributed package.
"""
from .basic import (
barrier_if_distributed,
convert_to_ddp,
get_device,
get_global_rank,
get_local_rank,
get_world_size,
)
__all__ = [
"barrier_if_distributed",
"convert_to_ddp",
"get_device",
"get_global_rank",
"get_local_rank",
"get_world_size",
]
@@ -0,0 +1,208 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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.
"""
Advanced distributed functions for sequence parallel.
"""
from typing import Optional, List
import torch
import torch.distributed as dist
from torch.distributed.device_mesh import DeviceMesh, init_device_mesh
from torch.distributed.fsdp import ShardingStrategy
from .basic import get_global_rank, get_world_size
_DATA_PARALLEL_GROUP = None
_SEQUENCE_PARALLEL_GROUP = None
_SEQUENCE_PARALLEL_CPU_GROUP = None
_MODEL_SHARD_CPU_INTER_GROUP = None
_MODEL_SHARD_CPU_INTRA_GROUP = None
_MODEL_SHARD_INTER_GROUP = None
_MODEL_SHARD_INTRA_GROUP = None
_SEQUENCE_PARALLEL_GLOBAL_RANKS = None
def get_data_parallel_group() -> Optional[dist.ProcessGroup]:
"""
Get data parallel process group.
"""
return _DATA_PARALLEL_GROUP
def get_sequence_parallel_group() -> Optional[dist.ProcessGroup]:
"""
Get sequence parallel process group.
"""
return _SEQUENCE_PARALLEL_GROUP
def get_sequence_parallel_cpu_group() -> Optional[dist.ProcessGroup]:
"""
Get sequence parallel CPU process group.
"""
return _SEQUENCE_PARALLEL_CPU_GROUP
def get_data_parallel_rank() -> int:
"""
Get data parallel rank.
"""
group = get_data_parallel_group()
return dist.get_rank(group) if group else get_global_rank()
def get_data_parallel_world_size() -> int:
"""
Get data parallel world size.
"""
group = get_data_parallel_group()
return dist.get_world_size(group) if group else get_world_size()
def get_sequence_parallel_rank() -> int:
"""
Get sequence parallel rank.
"""
group = get_sequence_parallel_group()
return dist.get_rank(group) if group else 0
def get_sequence_parallel_world_size() -> int:
"""
Get sequence parallel world size.
"""
group = get_sequence_parallel_group()
return dist.get_world_size(group) if group else 1
def get_model_shard_cpu_intra_group() -> Optional[dist.ProcessGroup]:
"""
Get the CPU intra process group of model sharding.
"""
return _MODEL_SHARD_CPU_INTRA_GROUP
def get_model_shard_cpu_inter_group() -> Optional[dist.ProcessGroup]:
"""
Get the CPU inter process group of model sharding.
"""
return _MODEL_SHARD_CPU_INTER_GROUP
def get_model_shard_intra_group() -> Optional[dist.ProcessGroup]:
"""
Get the GPU intra process group of model sharding.
"""
return _MODEL_SHARD_INTRA_GROUP
def get_model_shard_inter_group() -> Optional[dist.ProcessGroup]:
"""
Get the GPU inter process group of model sharding.
"""
return _MODEL_SHARD_INTER_GROUP
def init_sequence_parallel(sequence_parallel_size: int):
"""
Initialize sequence parallel.
"""
global _DATA_PARALLEL_GROUP
global _SEQUENCE_PARALLEL_GROUP
global _SEQUENCE_PARALLEL_CPU_GROUP
global _SEQUENCE_PARALLEL_GLOBAL_RANKS
assert dist.is_initialized()
world_size = dist.get_world_size()
rank = dist.get_rank()
data_parallel_size = world_size // sequence_parallel_size
for i in range(data_parallel_size):
start_rank = i * sequence_parallel_size
end_rank = (i + 1) * sequence_parallel_size
ranks = range(start_rank, end_rank)
group = dist.new_group(ranks)
cpu_group = dist.new_group(ranks, backend="gloo")
if rank in ranks:
_SEQUENCE_PARALLEL_GROUP = group
_SEQUENCE_PARALLEL_CPU_GROUP = cpu_group
_SEQUENCE_PARALLEL_GLOBAL_RANKS = list(ranks)
def init_model_shard_group(
*,
sharding_strategy: ShardingStrategy,
device_mesh: Optional[DeviceMesh] = None,
):
"""
Initialize process group of model sharding.
"""
global _MODEL_SHARD_INTER_GROUP
global _MODEL_SHARD_INTRA_GROUP
global _MODEL_SHARD_CPU_INTER_GROUP
global _MODEL_SHARD_CPU_INTRA_GROUP
assert dist.is_initialized()
world_size = dist.get_world_size()
if device_mesh is not None:
num_shards_per_group = device_mesh.shape[1]
elif sharding_strategy == ShardingStrategy.NO_SHARD:
num_shards_per_group = 1
elif sharding_strategy in [
ShardingStrategy.HYBRID_SHARD,
ShardingStrategy._HYBRID_SHARD_ZERO2,
]:
num_shards_per_group = torch.cuda.device_count()
else:
num_shards_per_group = world_size
num_groups = world_size // num_shards_per_group
device_mesh = (num_groups, num_shards_per_group)
gpu_mesh_2d = init_device_mesh("cuda", device_mesh, mesh_dim_names=("inter", "intra"))
cpu_mesh_2d = init_device_mesh("cpu", device_mesh, mesh_dim_names=("inter", "intra"))
_MODEL_SHARD_INTER_GROUP = gpu_mesh_2d.get_group("inter")
_MODEL_SHARD_INTRA_GROUP = gpu_mesh_2d.get_group("intra")
_MODEL_SHARD_CPU_INTER_GROUP = cpu_mesh_2d.get_group("inter")
_MODEL_SHARD_CPU_INTRA_GROUP = cpu_mesh_2d.get_group("intra")
def get_sequence_parallel_global_ranks() -> List[int]:
"""
Get all global ranks of the sequence parallel process group
that the caller rank belongs to.
"""
if _SEQUENCE_PARALLEL_GLOBAL_RANKS is None:
return [dist.get_rank()]
return _SEQUENCE_PARALLEL_GLOBAL_RANKS
def get_next_sequence_parallel_rank() -> int:
"""
Get the next global rank of the sequence parallel process group
that the caller rank belongs to.
"""
sp_global_ranks = get_sequence_parallel_global_ranks()
sp_rank = get_sequence_parallel_rank()
sp_size = get_sequence_parallel_world_size()
return sp_global_ranks[(sp_rank + 1) % sp_size]
def get_prev_sequence_parallel_rank() -> int:
"""
Get the previous global rank of the sequence parallel process group
that the caller rank belongs to.
"""
sp_global_ranks = get_sequence_parallel_global_ranks()
sp_rank = get_sequence_parallel_rank()
sp_size = get_sequence_parallel_world_size()
return sp_global_ranks[(sp_rank + sp_size - 1) % sp_size]
@@ -0,0 +1,67 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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.
"""
Distributed basic functions.
"""
import os
import torch
def get_global_rank() -> int:
"""
Get the global rank, the global index of the GPU.
"""
return int(os.environ.get("RANK", "0"))
def get_local_rank() -> int:
"""
Get the local rank, the local index of the GPU.
"""
return int(os.environ.get("LOCAL_RANK", "0"))
def get_world_size() -> int:
"""
Get the world size, the total amount of GPUs.
"""
return int(os.environ.get("WORLD_SIZE", "1"))
def get_device() -> torch.device:
"""
Get current rank device.
"""
return torch.device("cuda", get_local_rank())
def barrier_if_distributed(*args, **kwargs):
"""
Synchronizes all processes if under distributed context.
"""
import torch.distributed as dist
if dist.is_initialized():
return dist.barrier(*args, **kwargs)
def convert_to_ddp(module: torch.nn.Module, **kwargs):
from torch.nn.parallel import DistributedDataParallel
return DistributedDataParallel(
module=module,
device_ids=[get_local_rank()],
output_device=get_local_rank(),
**kwargs,
)
@@ -0,0 +1,41 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // Licensed under the Apache License, Version 2.0 (the "License");
# // you may not use this file except in compliance with the License.
# // You may obtain a copy of the License at
# //
# // http://www.apache.org/licenses/LICENSE-2.0
# //
# // Unless required by applicable law or agreed to in writing, software
# // distributed under the License is distributed on an "AS IS" BASIS,
# // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# // See the License for the specific language governing permissions and
# // limitations under the License.
import torch
from rotary_embedding_torch import RotaryEmbedding
from torch import nn
from torch.distributed.fsdp._common_utils import _is_fsdp_flattened
__all__ = ["meta_non_persistent_buffer_init_fn"]
def meta_non_persistent_buffer_init_fn(module: nn.Module) -> nn.Module:
"""
Used for materializing `non-persistent tensor buffers` while model resuming.
Since non-persistent tensor buffers are not saved in state_dict,
when initializing model with meta device, user should materialize those buffers manually.
Currently, only `rope.dummy` is this special case.
"""
with torch.no_grad():
for submodule in module.modules():
if not isinstance(submodule, RotaryEmbedding):
continue
for buffer_name, buffer in submodule.named_buffers(recurse=False):
if buffer.is_meta and "dummy" in buffer_name:
materialized_buffer = torch.zeros_like(buffer, device="cpu")
setattr(submodule, buffer_name, materialized_buffer)
assert not any(b.is_meta for n, b in module.named_buffers())
return module
@@ -0,0 +1,494 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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.
"""
Distributed ops for supporting sequence parallel.
"""
from collections import defaultdict
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
import torch
import torch.distributed as dist
from torch import Tensor
from ..cache import Cache
from .advanced import (
get_sequence_parallel_group,
get_sequence_parallel_rank,
get_sequence_parallel_world_size,
)
from .basic import get_device
_SEQ_DATA_BUF = defaultdict(lambda: [None, None, None])
_SEQ_DATA_META_SHAPES = defaultdict()
_SEQ_DATA_META_DTYPES = defaultdict()
_SEQ_DATA_ASYNC_COMMS = defaultdict(list)
_SYNC_BUFFER = defaultdict(dict)
def single_all_to_all(
local_input: Tensor,
scatter_dim: int,
gather_dim: int,
group: dist.ProcessGroup,
async_op: bool = False,
):
"""
A function to do all-to-all on a tensor
"""
seq_world_size = dist.get_world_size(group)
prev_scatter_dim = scatter_dim
if scatter_dim != 0:
local_input = local_input.transpose(0, scatter_dim)
if gather_dim == 0:
gather_dim = scatter_dim
scatter_dim = 0
inp_shape = list(local_input.shape)
inp_shape[scatter_dim] = inp_shape[scatter_dim] // seq_world_size
input_t = local_input.reshape(
[seq_world_size, inp_shape[scatter_dim]] + inp_shape[scatter_dim + 1 :]
).contiguous()
output = torch.empty_like(input_t)
comm = dist.all_to_all_single(output, input_t, group=group, async_op=async_op)
if async_op:
# let user's code transpose & reshape
return output, comm, prev_scatter_dim
# first dim is seq_world_size, so we can split it directly
output = torch.cat(output.split(1), dim=gather_dim + 1).squeeze(0)
if prev_scatter_dim:
output = output.transpose(0, prev_scatter_dim).contiguous()
return output
def _all_to_all(
local_input: Tensor,
scatter_dim: int,
gather_dim: int,
group: dist.ProcessGroup,
):
seq_world_size = dist.get_world_size(group)
input_list = [
t.contiguous() for t in torch.tensor_split(local_input, seq_world_size, scatter_dim)
]
output_list = [torch.empty_like(input_list[0]) for _ in range(seq_world_size)]
dist.all_to_all(output_list, input_list, group=group)
return torch.cat(output_list, dim=gather_dim).contiguous()
class SeqAllToAll(torch.autograd.Function):
@staticmethod
def forward(
ctx: Any,
group: dist.ProcessGroup,
local_input: Tensor,
scatter_dim: int,
gather_dim: int,
async_op: bool,
) -> Tensor:
ctx.group = group
ctx.scatter_dim = scatter_dim
ctx.gather_dim = gather_dim
ctx.async_op = async_op
if async_op:
output, comm, prev_scatter_dim = single_all_to_all(
local_input, scatter_dim, gather_dim, group, async_op=async_op
)
ctx.prev_scatter_dim = prev_scatter_dim
return output, comm
return _all_to_all(local_input, scatter_dim, gather_dim, group)
@staticmethod
def backward(ctx: Any, *grad_output: Tensor) -> Tuple[None, Tensor, None, None]:
if ctx.async_op:
input_t = torch.cat(grad_output[0].split(1), dim=ctx.gather_dim + 1).squeeze(0)
if ctx.prev_scatter_dim:
input_t = input_t.transpose(0, ctx.prev_scatter_dim)
else:
input_t = grad_output[0]
return (
None,
_all_to_all(input_t, ctx.gather_dim, ctx.scatter_dim, ctx.group),
None,
None,
None,
)
class Slice(torch.autograd.Function):
@staticmethod
def forward(ctx: Any, group: dist.ProcessGroup, local_input: Tensor, dim: int) -> Tensor:
ctx.group = group
ctx.rank = dist.get_rank(group)
seq_world_size = dist.get_world_size(group)
ctx.seq_world_size = seq_world_size
ctx.dim = dim
dim_size = local_input.shape[dim]
return local_input.split(dim_size // seq_world_size, dim=dim)[ctx.rank].contiguous()
@staticmethod
def backward(ctx: Any, grad_output: Tensor) -> Tuple[None, Tensor, None]:
dim_size = list(grad_output.size())
split_size = dim_size[0]
dim_size[0] = dim_size[0] * ctx.seq_world_size
output = torch.empty(dim_size, dtype=grad_output.dtype, device=torch.cuda.current_device())
dist._all_gather_base(output, grad_output, group=ctx.group)
return (None, torch.cat(output.split(split_size), dim=ctx.dim), None)
class Gather(torch.autograd.Function):
@staticmethod
def forward(
ctx: Any,
group: dist.ProcessGroup,
local_input: Tensor,
dim: int,
grad_scale: Optional[bool] = False,
) -> Tensor:
ctx.group = group
ctx.rank = dist.get_rank(group)
ctx.dim = dim
ctx.grad_scale = grad_scale
seq_world_size = dist.get_world_size(group)
ctx.seq_world_size = seq_world_size
dim_size = list(local_input.size())
split_size = dim_size[0]
ctx.part_size = dim_size[dim]
dim_size[0] = dim_size[0] * seq_world_size
output = torch.empty(dim_size, dtype=local_input.dtype, device=torch.cuda.current_device())
dist._all_gather_base(output, local_input.contiguous(), group=ctx.group)
return torch.cat(output.split(split_size), dim=dim)
@staticmethod
def backward(ctx: Any, grad_output: Tensor) -> Tuple[None, Tensor]:
if ctx.grad_scale:
grad_output = grad_output * ctx.seq_world_size
return (
None,
grad_output.split(ctx.part_size, dim=ctx.dim)[ctx.rank].contiguous(),
None,
None,
)
def gather_seq_scatter_heads_qkv(
qkv_tensor: Tensor,
*,
seq_dim: int,
qkv_shape: Optional[Tensor] = None,
cache: Cache = Cache(disable=True),
restore_shape: bool = True,
):
"""
A func to sync splited qkv tensor
qkv_tensor: the tensor we want to do alltoall with. The last dim must
be the projection_idx, which we will split into 3 part. After
spliting, the gather idx will be projecttion_idx + 1
seq_dim: gather_dim for all2all comm
restore_shape: if True, output will has the same shape length as input
"""
group = get_sequence_parallel_group()
if not group:
return qkv_tensor
world = get_sequence_parallel_world_size()
orig_shape = qkv_tensor.shape
scatter_dim = qkv_tensor.dim()
bef_all2all_shape = list(orig_shape)
qkv_proj_dim = bef_all2all_shape[-1]
bef_all2all_shape = bef_all2all_shape[:-1] + [3, qkv_proj_dim // 3]
qkv_tensor = qkv_tensor.view(bef_all2all_shape)
qkv_tensor = SeqAllToAll.apply(group, qkv_tensor, scatter_dim, seq_dim, False)
if restore_shape:
out_shape = list(orig_shape)
out_shape[seq_dim] *= world
out_shape[-1] = qkv_proj_dim // world
qkv_tensor = qkv_tensor.view(out_shape)
# remove padding
if qkv_shape is not None:
unpad_dim_size = cache(
"unpad_dim_size", lambda: torch.sum(torch.prod(qkv_shape, dim=-1)).item()
)
if unpad_dim_size % world != 0:
padding_size = qkv_tensor.size(seq_dim) - unpad_dim_size
qkv_tensor = _unpad_tensor(qkv_tensor, seq_dim, padding_size)
return qkv_tensor
def slice_inputs(x: Tensor, dim: int, padding: bool = True):
"""
A func to slice the input sequence in sequence parallel
"""
group = get_sequence_parallel_group()
if group is None:
return x
sp_rank = get_sequence_parallel_rank()
sp_world = get_sequence_parallel_world_size()
dim_size = x.shape[dim]
unit = (dim_size + sp_world - 1) // sp_world
if padding and dim_size % sp_world:
padding_size = sp_world - (dim_size % sp_world)
x = _pad_tensor(x, dim, padding_size)
slc = [slice(None)] * len(x.shape)
slc[dim] = slice(unit * sp_rank, unit * (sp_rank + 1))
return x[slc]
def remove_seqeunce_parallel_padding(x: Tensor, dim: int, unpad_dim_size: int):
"""
A func to remove the padding part of the tensor based on its original shape
"""
group = get_sequence_parallel_group()
if group is None:
return x
sp_world = get_sequence_parallel_world_size()
if unpad_dim_size % sp_world == 0:
return x
padding_size = sp_world - (unpad_dim_size % sp_world)
assert (padding_size + unpad_dim_size) % sp_world == 0
return _unpad_tensor(x, dim=dim, padding_size=padding_size)
def gather_heads_scatter_seq(x: Tensor, head_dim: int, seq_dim: int) -> Tensor:
"""
A func to sync attention result with alltoall in sequence parallel
"""
group = get_sequence_parallel_group()
if not group:
return x
dim_size = x.size(seq_dim)
sp_world = get_sequence_parallel_world_size()
if dim_size % sp_world != 0:
padding_size = sp_world - (dim_size % sp_world)
x = _pad_tensor(x, seq_dim, padding_size)
return SeqAllToAll.apply(group, x, seq_dim, head_dim, False)
def gather_seq_scatter_heads(x: Tensor, seq_dim: int, head_dim: int) -> Tensor:
"""
A func to sync embedding input with alltoall in sequence parallel
"""
group = get_sequence_parallel_group()
if not group:
return x
return SeqAllToAll.apply(group, x, head_dim, seq_dim, False)
def scatter_heads(x: Tensor, dim: int) -> Tensor:
"""
A func to split heads before attention in sequence parallel
"""
group = get_sequence_parallel_group()
if not group:
return x
return Slice.apply(group, x, dim)
def gather_heads(x: Tensor, dim: int, grad_scale: Optional[bool] = False) -> Tensor:
"""
A func to gather heads for the attention result in sequence parallel
"""
group = get_sequence_parallel_group()
if not group:
return x
return Gather.apply(group, x, dim, grad_scale)
def gather_outputs(
x: Tensor,
*,
gather_dim: int,
padding_dim: Optional[int] = None,
unpad_shape: Optional[Tensor] = None,
cache: Cache = Cache(disable=True),
scale_grad=True,
):
"""
A func to gather the outputs for the model result in sequence parallel
"""
group = get_sequence_parallel_group()
if not group:
return x
x = Gather.apply(group, x, gather_dim, scale_grad)
if padding_dim is not None:
unpad_dim_size = cache(
"unpad_dim_size", lambda: torch.sum(torch.prod(unpad_shape, dim=1)).item()
)
x = remove_seqeunce_parallel_padding(x, padding_dim, unpad_dim_size)
return x
def _pad_tensor(x: Tensor, dim: int, padding_size: int):
shape = list(x.shape)
shape[dim] = padding_size
pad = torch.zeros(shape, dtype=x.dtype, device=x.device)
return torch.cat([x, pad], dim=dim)
def _unpad_tensor(x: Tensor, dim: int, padding_size):
slc = [slice(None)] * len(x.shape)
slc[dim] = slice(0, -padding_size)
return x[slc]
def _broadcast_data(data, shape, dtype, src, group, async_op):
comms = []
if isinstance(data, (list, tuple)):
for i, sub_shape in enumerate(shape):
comms += _broadcast_data(data[i], sub_shape, dtype[i], src, group, async_op)
elif isinstance(data, dict):
for key, sub_data in data.items():
comms += _broadcast_data(sub_data, shape[key], dtype[key], src, group, async_op)
elif isinstance(data, Tensor):
comms.append(dist.broadcast(data, src=src, group=group, async_op=async_op))
return comms
def _traverse(data: Any, op: Callable) -> Union[None, List, Dict, Any]:
if isinstance(data, (list, tuple)):
return [_traverse(sub_data, op) for sub_data in data]
elif isinstance(data, dict):
return {key: _traverse(sub_data, op) for key, sub_data in data.items()}
elif isinstance(data, Tensor):
return op(data)
else:
return None
def _get_shapes(data):
return _traverse(data, op=lambda x: x.shape)
def _get_dtypes(data):
return _traverse(data, op=lambda x: x.dtype)
def _construct_broadcast_buffer(shapes, dtypes, device):
if isinstance(shapes, torch.Size):
return torch.empty(shapes, dtype=dtypes, device=device)
if isinstance(shapes, (list, tuple)):
buffer = []
for i, sub_shape in enumerate(shapes):
buffer.append(_construct_broadcast_buffer(sub_shape, dtypes[i], device))
elif isinstance(shapes, dict):
buffer = {}
for key, sub_shape in shapes.items():
buffer[key] = _construct_broadcast_buffer(sub_shape, dtypes[key], device)
else:
return None
return buffer
class SPDistForward:
"""A forward tool to sync different result across sp group
Args:
module: a function or module to process users input
sp_step: current training step to judge which rank to broadcast its result to all
name: a distinct str to save meta and async comm
comm_shape: if different ranks have different shape, mark this arg to True
device: the device for current rank, can be empty
"""
def __init__(
self,
name: str,
comm_shape: bool,
device: torch.device = None,
):
self.name = name
self.comm_shape = comm_shape
if device:
self.device = device
else:
self.device = get_device()
def __call__(self, inputs) -> Any:
group = get_sequence_parallel_group()
if not group:
yield inputs
else:
device = self.device
sp_world = get_sequence_parallel_world_size()
sp_rank = get_sequence_parallel_rank()
for local_step in range(sp_world):
src_rank = dist.get_global_rank(group, local_step)
is_src = sp_rank == local_step
local_shapes = []
local_dtypes = []
if local_step == 0:
local_result = inputs
_SEQ_DATA_BUF[self.name][-1] = local_result
local_shapes = _get_shapes(local_result)
local_dtypes = _get_dtypes(local_result)
if self.comm_shape:
group_shapes_lists = [None] * sp_world
dist.all_gather_object(group_shapes_lists, local_shapes, group=group)
_SEQ_DATA_META_SHAPES[self.name] = group_shapes_lists
else:
_SEQ_DATA_META_SHAPES[self.name] = [local_shapes] * sp_world
_SEQ_DATA_META_DTYPES[self.name] = local_dtypes
shapes = _SEQ_DATA_META_SHAPES[self.name][local_step]
dtypes = _SEQ_DATA_META_DTYPES[self.name]
buf_id = local_step % 2
if local_step == 0:
sync_data = (
local_result
if is_src
else _construct_broadcast_buffer(shapes, dtypes, device)
)
_broadcast_data(sync_data, shapes, dtypes, src_rank, group, False)
_SEQ_DATA_BUF[self.name][buf_id] = sync_data
# wait for async comm ops
if _SEQ_DATA_ASYNC_COMMS[self.name]:
for comm in _SEQ_DATA_ASYNC_COMMS[self.name]:
comm.wait()
# before return the sync result, do async broadcast for next batch
if local_step < sp_world - 1:
next_buf_id = 1 - buf_id
shapes = _SEQ_DATA_META_SHAPES[self.name][local_step + 1]
src_rank = dist.get_global_rank(group, local_step + 1)
is_src = sp_rank == local_step + 1
next_sync_data = (
_SEQ_DATA_BUF[self.name][-1]
if is_src
else _construct_broadcast_buffer(shapes, dtypes, device)
)
_SEQ_DATA_ASYNC_COMMS[self.name] = _broadcast_data(
next_sync_data, shapes, dtypes, src_rank, group, True
)
_SEQ_DATA_BUF[self.name][next_buf_id] = next_sync_data
yield _SEQ_DATA_BUF[self.name][buf_id]
sync_inputs = SPDistForward(name="bef_fwd", comm_shape=True)
def sync_data(data, sp_idx, name="tmp"):
group = get_sequence_parallel_group()
if group is None:
return data
# if sp_idx in _SYNC_BUFFER[name]:
# return _SYNC_BUFFER[name][sp_idx]
sp_rank = get_sequence_parallel_rank()
src_rank = dist.get_global_rank(group, sp_idx)
objects = [data] if sp_rank == sp_idx else [None]
dist.broadcast_object_list(objects, src=src_rank, group=group)
# _SYNC_BUFFER[name] = {sp_idx: objects[0]}
return objects[0]
@@ -0,0 +1,61 @@
import torch.nn.functional as F
def safe_pad_operation(x, padding, mode='constant', value=0.0):
"""Safe padding operation that handles Half precision only for problematic modes"""
# Modes qui nécessitent le fix Half precision
problematic_modes = ['replicate', 'reflect', 'circular']
if mode in problematic_modes:
try:
return F.pad(x, padding, mode=mode, value=value)
except RuntimeError as e:
if "not implemented for 'Half'" in str(e):
original_dtype = x.dtype
return F.pad(x.float(), padding, mode=mode, value=value).to(original_dtype)
else:
raise e
else:
# Pour 'constant' et autres modes compatibles, pas de fix nécessaire
return F.pad(x, padding, mode=mode, value=value)
def safe_interpolate_operation(x, size=None, scale_factor=None, mode='nearest', align_corners=None, recompute_scale_factor=None):
"""Safe interpolate operation that handles Half precision for problematic modes"""
# Modes qui peuvent causer des problèmes avec Half precision
problematic_modes = ['bilinear', 'bicubic', 'trilinear']
if mode in problematic_modes:
try:
return F.interpolate(
x,
size=size,
scale_factor=scale_factor,
mode=mode,
align_corners=align_corners,
recompute_scale_factor=recompute_scale_factor
)
except RuntimeError as e:
if ("not implemented for 'Half'" in str(e) or
"compute_indices_weights" in str(e)):
original_dtype = x.dtype
return F.interpolate(
x.float(),
size=size,
scale_factor=scale_factor,
mode=mode,
align_corners=align_corners,
recompute_scale_factor=recompute_scale_factor
).to(original_dtype)
else:
raise e
else:
# Pour 'nearest' et autres modes compatibles, pas de fix nécessaire
return F.interpolate(
x,
size=size,
scale_factor=scale_factor,
mode=mode,
align_corners=align_corners,
recompute_scale_factor=recompute_scale_factor
)
+44
View File
@@ -0,0 +1,44 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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.
"""
Logging utility functions.
"""
import logging
import sys
from typing import Optional
from .distributed import get_global_rank, get_local_rank, get_world_size
_default_handler = logging.StreamHandler(sys.stdout)
_default_handler.setFormatter(
logging.Formatter(
"%(asctime)s "
+ (f"[Rank:{get_global_rank()}]" if get_world_size() > 1 else "")
+ (f"[LocalRank:{get_local_rank()}]" if get_world_size() > 1 else "")
+ "[%(threadName).12s][%(name)s][%(levelname).5s] "
+ "%(message)s"
)
)
def get_logger(name: Optional[str] = None) -> logging.Logger:
"""
Get a logger.
"""
logger = logging.getLogger(name)
logger.addHandler(_default_handler)
logger.setLevel(logging.INFO)
return logger
+59
View File
@@ -0,0 +1,59 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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.
"""
Partition utility functions.
"""
from typing import Any, List
def partition_by_size(data: List[Any], size: int) -> List[List[Any]]:
"""
Partition a list by size.
When indivisible, the last group contains fewer items than the target size.
Examples:
- data: [1,2,3,4,5]
- size: 2
- return: [[1,2], [3,4], [5]]
"""
assert size > 0
return [data[i : (i + size)] for i in range(0, len(data), size)]
def partition_by_groups(data: List[Any], groups: int) -> List[List[Any]]:
"""
Partition a list by groups.
When indivisible, some groups may have more items than others.
Examples:
- data: [1,2,3,4,5]
- groups: 2
- return: [[1,3,5], [2,4]]
"""
assert groups > 0
return [data[i::groups] for i in range(groups)]
def shift_list(data: List[Any], n: int) -> List[Any]:
"""
Rotate a list by n elements.
Examples:
- data: [1,2,3,4,5]
- n: 3
- return: [4,5,1,2,3]
"""
return data[(n % len(data)) :] + data[: (n % len(data))]
+30
View File
@@ -0,0 +1,30 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // Licensed under the Apache License, Version 2.0 (the "License");
# // you may not use this file except in compliance with the License.
# // You may obtain a copy of the License at
# //
# // http://www.apache.org/licenses/LICENSE-2.0
# //
# // Unless required by applicable law or agreed to in writing, software
# // distributed under the License is distributed on an "AS IS" BASIS,
# // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# // See the License for the specific language governing permissions and
# // limitations under the License.
import random
from typing import Optional
import numpy as np
import torch
from .distributed import get_global_rank
def set_seed(seed: Optional[int], same_across_ranks: bool = False):
"""Function that sets the seed for pseudo-random number generators."""
if seed is not None:
seed += get_global_rank() if not same_across_ranks else 0
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
+47
View File
@@ -0,0 +1,47 @@
"""
Core Module for SeedVR2
Contains the main business logic and model management functionality:
- Model configuration and loading
- Architecture detection and memory estimation
- Runner creation and management
- Generation pipeline and logic
"""
'''
from .model_manager import (
configure_runner,
load_quantized_state_dict,
configure_dit_model_inference,
configure_vae_model_inference,
)
from .generation import (
generation_step,
generation_loop,
cut_videos,
prepare_video_transforms,
load_text_embeddings,
calculate_optimal_batch_params
)
from .infer import VideoDiffusionInfer
__all__ = [
# Model management
'configure_runner',
'load_quantized_state_dict',
'configure_dit_model_inference',
'configure_vae_model_inference',
# Generation logic
'generation_step',
'generation_loop',
'cut_videos',
'prepare_video_transforms',
'load_text_embeddings',
'calculate_optimal_batch_params',
# Infer
'VideoDiffusionInfer'
]
'''
+365
View File
@@ -0,0 +1,365 @@
import torch
from torchvision.transforms import Compose, Lambda, Normalize
from src.optimization.performance import optimized_video_rearrange, optimized_single_video_rearrange, optimized_sample_to_image_format
from src.common.seed import set_seed
from src.data.image.transforms.divisible_crop import DivisibleCrop
from src.data.image.transforms.na_resize import NaResize
from src.utils.color_fix import wavelet_reconstruction
def generation_step(runner, text_embeds_dict, cond_latents, temporal_overlap, device):
"""
Execute a single generation step with adaptive dtype handling
Args:
runner: VideoDiffusionInfer instance
text_embeds_dict (dict): Text embeddings for positive and negative prompts
cond_latents (list): Conditional latents for generation
temporal_overlap (int): Number of frames for temporal overlap
Returns:
tuple: (samples, last_latents) for potential temporal continuation
Features:
- Adaptive dtype detection (FP8/FP16/BFloat16)
- Optimal autocast configuration for each model type
- Memory-efficient noise generation and reuse
- Automatic device placement with dtype preservation
- Advanced inference optimization
"""
# Adaptive dtype detection for optimal performance
model_dtype = next(runner.dit.parameters()).dtype
# Configure dtypes according to model architecture
if model_dtype in (torch.float8_e4m3fn, torch.float8_e5m2):
# FP8 native: use BFloat16 for intermediate calculations (optimal compatibility)
dtype = torch.bfloat16
elif model_dtype == torch.float16:
dtype = torch.float16
else:
dtype = torch.bfloat16
def _move_to_cuda(x):
"""Move tensors to CUDA with adaptive optimal dtype"""
return [i.to(device, dtype=dtype) for i in x]
# Memory optimization: Generate noise once and reuse to save VRAM
with torch.cuda.device(device):
base_noise = torch.randn_like(cond_latents[0], dtype=dtype)
noises = [base_noise]
aug_noises = [base_noise * 0.1 + torch.randn_like(base_noise) * 0.05]
# Move tensors with adaptive dtype (optimized for FP8/FP16/BFloat16)
noises, aug_noises, cond_latents = _move_to_cuda(noises), _move_to_cuda(aug_noises), _move_to_cuda(cond_latents)
cond_noise_scale = 0.0
def _add_noise(x, aug_noise):
# Use adaptive optimal dtype
t = (
torch.tensor([1000.0], device=device, dtype=dtype)
* cond_noise_scale
)
shape = torch.tensor(x.shape[1:], device=device)[None]
t = runner.timestep_transform(t, shape)
x = runner.schedule.forward(x, aug_noise, t)
return x
# Generate conditions with memory optimization
runner.dit.to(device=device)
condition = runner.get_condition(
noises[0],
task="sr",
latent_blur=_add_noise(cond_latents[0], aug_noises[0]),
)
conditions = [condition]
# Use adaptive autocast for optimal performance
with torch.no_grad():
video_tensors = runner.inference(
noises=noises,
conditions=conditions,
temporal_overlap=temporal_overlap,
**text_embeds_dict,
)
# Process samples with advanced optimization
samples = optimized_video_rearrange(video_tensors)
noises = noises[0].to("cpu")
aug_noises = aug_noises[0].to("cpu")
cond_latents = cond_latents[0].to("cpu")
conditions = conditions[0].to("cpu")
condition = condition.to("cpu")
return samples #, last_latents
def cut_videos(videos):
t = videos.size(1)
if t % 4 == 1:
return videos
padding_needed = (4 - (t % 4)) % 4 + 1
last_frame = videos[:, -1:].expand(-1, padding_needed, -1, -1).contiguous()
result = torch.cat([videos, last_frame], dim=1)
return result
def generation_loop(runner, images, cfg_scale=1.0, seed=666, res_w=720, batch_size=90, temporal_overlap=0, progress_callback=None, device:str='cpu'):
"""
Main generation loop with context-aware temporal processing
Args:
runner: VideoDiffusionInfer instance
images (torch.Tensor): Input images for upscaling
cfg_scale (float): Classifier-free guidance scale
seed (int): Random seed for reproducibility
res_w (int): Target resolution width
batch_size (int): Batch size for processing
temporal_overlap (int): Frames for temporal continuity
progress_callback (callable): Optional callback for progress reporting
Returns:
torch.Tensor: Generated video frames
Features:
- Context-aware generation with temporal overlap
- Adaptive dtype pipeline (FP8/FP16/BFloat16)
- Memory-optimized batch processing
- Advanced video transformation pipeline
- Intelligent VRAM management throughout process
- Real-time progress reporting
"""
model_dtype = None
model_dtype = next(runner.dit.parameters()).dtype
compute_dtype = model_dtype
# Configure classifier-free guidance
runner.config.diffusion.cfg.scale = cfg_scale
runner.config.diffusion.cfg.rescale = 0.0
# Configure sampling steps
runner.config.diffusion.timesteps.sampling.steps = 1
runner.configure_diffusion()
# Set random seed
set_seed(seed)
# Advanced video transformation pipeline
video_transform = Compose([
NaResize(
resolution=(res_w),
mode="side",
downsample_only=False,
),
Lambda(lambda x: torch.clamp(x, 0.0, 1.0)),
DivisibleCrop((16, 16)),
Normalize(0.5, 0.5),
Lambda(lambda x: x.permute(1, 0, 2, 3)), # t c h w -> c t h w (faster than Rearrange)
])
# Initialize generation state
batch_samples = []
# Load text embeddings with adaptive dtype
text_embeds = {"texts_pos": [runner.text_pos_embeds], "texts_neg": [runner.text_neg_embeds]}
# Calculate processing parameters
step = batch_size - temporal_overlap
if step <= 0:
step = batch_size
temporal_overlap = 0
# Calculate total batches for progress reporting
total_batches = len(range(0, len(images), step))
# Main processing loop with context awareness
for batch_count, batch_idx in enumerate(range(0, len(images), step)):
# Calculate batch indices with overlap
if batch_idx == 0:
# First batch: no overlap
start_idx = 0
end_idx = min(batch_size, len(images))
effective_batch_size = end_idx - start_idx
else:
# Subsequent batches: temporal overlap
start_idx = batch_idx
end_idx = min(start_idx + batch_size, len(images))
effective_batch_size = end_idx - start_idx
if effective_batch_size <= temporal_overlap:
break # Not enough new frames, stop
current_frames = end_idx - start_idx
# Process current batch
video = images[start_idx:end_idx]
# Use adaptive computation dtype
video = video.permute(0, 3, 1, 2).to(device, dtype=compute_dtype)
# Apply video transformations with memory optimization
transformed_video = video_transform(video)
del video
#video = video.to("cpu")
#del video
ori_lengths = [transformed_video.size(1)]
# Handle correct format: frames % 4 == 1
t = transformed_video.size(1)
if len(images) >= 5 and t % 4 != 1:
transformed_video = cut_videos(transformed_video)
# Context-aware temporal strategy
# First batch: standard complete diffusion
cond_latents = runner.vae_encode([transformed_video])
# Normal generation
samples = generation_step(runner, text_embeds, cond_latents=cond_latents, temporal_overlap=temporal_overlap, device=device)
#del cond_latents
del cond_latents
# Post-process samples
sample = samples[0]
del samples
#del samples
if ori_lengths[0] < sample.shape[0]:
sample = sample[:ori_lengths[0]]
# Apply color correction if available
transformed_video = transformed_video.to(device)
input_video = [optimized_single_video_rearrange(transformed_video)]
del transformed_video
sample = wavelet_reconstruction(sample, input_video[0][:sample.size(0)])
del input_video
# Convert to final image format
sample = optimized_sample_to_image_format(sample)
sample = sample.clip(-1, 1).mul_(0.5).add_(0.5)
sample_cpu = sample.to(torch.float16).to("cpu")
del sample
batch_samples.append(sample_cpu)
#del sample
# Aggressive cleanup after each batch
# Progress callback - batch start
if progress_callback:
progress_callback(batch_count+1, total_batches, current_frames, "Processing batch...")
runner.vae.to(device="cpu")
runner.dit.to(device="cpu")
# OPTIMISATION ULTIME : Pré-allocation et copie directe (évite les torch.cat multiples)
# 1. Calculer la taille totale finale
total_frames = sum(batch.shape[0] for batch in batch_samples)
if len(batch_samples) > 0:
sample_shape = batch_samples[0].shape
H, W, C = sample_shape[1], sample_shape[2], sample_shape[3]
# 2. Pré-allouer le tensor final directement sur CPU (évite concatenations)
final_video_images = torch.empty((total_frames, H, W, C), dtype=torch.float16)
# 3. Copier par blocs directement dans le tensor final
block_size = 500
current_idx = 0
for block_start in range(0, len(batch_samples), block_size):
block_end = min(block_start + block_size, len(batch_samples))
# Charger le bloc en VRAM
current_block = []
for i in range(block_start, block_end):
current_block.append(batch_samples[i].to(device))
# Concatener en VRAM (rapide)
block_result = torch.cat(current_block, dim=0)
# Convertir en Float16 sur GPU
#if block_result.dtype != torch.float16:
# block_result = block_result.to(torch.float16)
# Copier directement dans le tensor final (pas de concatenation!)
block_frames = block_result.shape[0]
final_video_images[current_idx:current_idx + block_frames] = block_result.to("cpu")
current_idx += block_frames
# Nettoyage immédiat VRAM
del current_block, block_result
else:
print("SeedVR2: No batch_samples to process")
final_video_images = torch.empty((0, 0, 0, 0), dtype=torch.float16)
# Cleanup batch_samples
#del batch_samples
return final_video_images
def prepare_video_transforms(res_w):
"""
Prepare optimized video transformation pipeline
Args:
res_w (int): Target resolution width
Returns:
Compose: Configured transformation pipeline
Features:
- Resolution-aware upscaling (no downsampling)
- Proper normalization for model compatibility
- Memory-efficient tensor operations
"""
return Compose([
NaResize(
resolution=(res_w),
mode="side",
downsample_only=False, # Model trained for high resolution
),
Lambda(lambda x: torch.clamp(x, 0.0, 1.0)),
DivisibleCrop((16, 16)),
Normalize(0.5, 0.5),
Lambda(lambda x: x.permute(1, 0, 2, 3)), # t c h w -> c t h w
])
def calculate_optimal_batch_params(total_frames, batch_size, temporal_overlap):
"""
Calculate optimal batch processing parameters
Args:
total_frames (int): Total number of frames
batch_size (int): Desired batch size
temporal_overlap (int): Temporal overlap frames
Returns:
dict: Optimized parameters and recommendations
Features:
- 4n+1 constraint optimization
- Padding waste calculation
- Performance recommendations
"""
step = batch_size - temporal_overlap
if step <= 0:
step = batch_size
temporal_overlap = 0
# Find optimal batch sizes (4n+1 constraint)
optimal_batches = [x for x in [i for i in range(1, 200) if i % 4 == 1] if x <= total_frames]
best_batch = max(optimal_batches) if optimal_batches else 1
# Calculate potential padding waste
padding_waste = 0
if batch_size not in optimal_batches:
padding_waste = sum(((i // 4) + 1) * 4 + 1 - i for i in range(batch_size, total_frames, batch_size))
return {
'step': step,
'temporal_overlap': temporal_overlap,
'best_batch': best_batch,
'padding_waste': padding_waste,
'is_optimal': batch_size in optimal_batches
}
+357
View File
@@ -0,0 +1,357 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 List, Optional, Tuple, Union
import torch
from einops import rearrange
from omegaconf import DictConfig, ListConfig
from torch import Tensor
from src.common.diffusion import (
classifier_free_guidance_dispatcher,
create_sampler_from_config,
create_sampling_timesteps_from_config,
create_schedule_from_config,
)
from src.common.distributed import (
get_device,
)
# from common.fs import download
from src.models.dit_v2 import na
def optimized_channels_to_last(tensor):
"""🚀 Optimized replacement for rearrange(tensor, 'b c ... -> b ... c')
Moves channels from position 1 to last position using PyTorch native operations.
"""
if tensor.ndim == 3: # [batch, channels, spatial]
return tensor.permute(0, 2, 1)
elif tensor.ndim == 4: # [batch, channels, height, width]
return tensor.permute(0, 2, 3, 1)
elif tensor.ndim == 5: # [batch, channels, depth, height, width]
return tensor.permute(0, 2, 3, 4, 1)
else:
# Fallback for other dimensions - move channel (dim=1) to last
dims = list(range(tensor.ndim))
dims = [dims[0]] + dims[2:] + [dims[1]] # [0, 2, 3, ..., 1]
return tensor.permute(*dims)
def optimized_channels_to_second(tensor):
"""🚀 Optimized replacement for rearrange(tensor, 'b ... c -> b c ...')
Moves channels from last position to position 1 using PyTorch native operations.
"""
if tensor.ndim == 3: # [batch, spatial, channels]
return tensor.permute(0, 2, 1)
elif tensor.ndim == 4: # [batch, height, width, channels]
return tensor.permute(0, 3, 1, 2)
elif tensor.ndim == 5: # [batch, depth, height, width, channels]
return tensor.permute(0, 4, 1, 2, 3)
else:
# Fallback for other dimensions - move last dim to position 1
dims = list(range(tensor.ndim))
dims = [dims[0], dims[-1]] + dims[1:-1] # [0, -1, 1, 2, ..., -2]
return tensor.permute(*dims)
class VideoDiffusionInfer():
def __init__(self, config: DictConfig, device: str, dtype: torch.dtype):
self.config = config
self.device = device
self.dtype = dtype
self.vae = None
self.dit = None
self.sampler = None
self.schedule = None
def get_condition(self, latent: Tensor, latent_blur: Tensor, task: str) -> Tensor:
t, h, w, c = latent.shape
cond = torch.zeros([t, h, w, c + 1], device=latent.device, dtype=latent.dtype)
if task == "t2v" or t == 1:
# t2i or t2v generation.
if task == "sr":
cond[:, ..., :-1] = latent_blur[:]
cond[:, ..., -1:] = 1.0
return cond
if task == "i2v":
# i2v generation.
cond[:1, ..., :-1] = latent[:1]
cond[:1, ..., -1:] = 1.0
return cond
if task == "v2v":
# v2v frame extension.
cond[:2, ..., :-1] = latent[:2]
cond[:2, ..., -1:] = 1.0
return cond
if task == "sr":
# sr generation.
cond[:, ..., :-1] = latent_blur[:]
cond[:, ..., -1:] = 1.0
return cond
raise NotImplementedError
def configure_diffusion(self):
self.schedule = create_schedule_from_config(
config=self.config.diffusion.schedule,
)
self.sampling_timesteps = create_sampling_timesteps_from_config( # pylint: disable=attribute-defined-outside-init
config=self.config.diffusion.timesteps.sampling,
schedule=self.schedule,
device=self.device,
)
self.sampler = create_sampler_from_config(
config=self.config.diffusion.sampler,
schedule=self.schedule,
timesteps=self.sampling_timesteps,
)
# -------------------------------- Helper ------------------------------- #
@torch.no_grad()
def vae_encode(self, samples: List[Tensor]) -> List[Tensor]:
self.dit.to(device="cpu")
self.vae.to(device=self.device)
use_sample = self.config.vae.get("use_sample", True)
latents = []
if len(samples) > 0:
device = get_device()
dtype = self.vae.dtype
scale = self.config.vae.scaling_factor
shift = self.config.vae.get("shifting_factor", 0.0)
if isinstance(scale, ListConfig):
scale = torch.tensor(scale, device=device, dtype=dtype)
if isinstance(shift, ListConfig):
shift = torch.tensor(shift, device=device, dtype=dtype)
# Group samples of the same shape to batches if enabled.
if self.config.vae.grouping:
batches, indices = na.pack(samples)
else:
batches = [sample.unsqueeze(0) for sample in samples]
# Vae process by each group.
for sample in batches:
sample = sample.to(device, dtype)
if hasattr(self.vae, "preprocess"):
sample = self.vae.preprocess(sample)
if use_sample:
latent = self.vae.encode(sample).latent
else:
# Deterministic vae encode, only used for i2v inference (optionally)
latent = self.vae.encode(sample).posterior.mode().squeeze(2)
latent = latent.unsqueeze(2) if latent.ndim == 4 else latent
latent = rearrange(latent, "b c ... -> b ... c")
#latent = optimized_channels_to_last(latent)
latent = (latent - shift) * scale
latents.append(latent)
# Ungroup back to individual latent with the original order.
if self.config.vae.grouping:
latents = na.unpack(latents, indices)
else:
latents = [latent.squeeze(0) for latent in latents]
self.vae.to(device="cpu")
return latents
@torch.no_grad()
def vae_decode(self, latents: List[Tensor], target_dtype: torch.dtype = None) -> List[Tensor]:
"""🚀 VAE decode optimisé - décodage direct sans chunking, compatible avec autocast externe"""
self.dit.to(device="cpu")
self.vae.to(device=self.device)
samples = []
if len(latents) > 0:
device = get_device()
dtype = self.vae.dtype
scale = self.config.vae.scaling_factor
shift = self.config.vae.get("shifting_factor", 0.0)
if isinstance(scale, ListConfig):
scale = torch.tensor(scale, device=device, dtype=dtype)
if isinstance(shift, ListConfig):
shift = torch.tensor(shift, device=device, dtype=dtype)
# 🚀 OPTIMISATION 1: Group latents intelligemment pour batch processing
if self.config.vae.grouping:
latents, indices = na.pack(latents)
else:
latents = [latent.unsqueeze(0) for latent in latents]
# 🚀 OPTIMISATION 2: Traitement batch optimisé avec dtype adaptatif
for _i, latent in enumerate(latents):
# Préparation optimisée du latent
# Utiliser target_dtype si fourni (évite double autocast)
effective_dtype = target_dtype if target_dtype is not None else dtype
latent = latent.to(device, effective_dtype, non_blocking=True)
latent = latent / scale + shift
latent = rearrange(latent, "b ... c -> b c ...")
#latent = optimized_channels_to_second(latent)
latent = latent.squeeze(2)
# 🚀 OPTIMISATION 3: Décodage direct SANS autocast (utilise l'autocast externe)
sample = self.vae.decode(latent).sample
#sample = self.vae.decode(latent).sample
#sample = self.vae.decode(latent).sample
# 🚀 OPTIMISATION 4: Post-processing conditionnel
if hasattr(self.vae, "postprocess"):
sample = self.vae.postprocess(sample)
samples.append(sample)
# Ungroup back to individual sample with the original order.
if self.config.vae.grouping:
samples = na.unpack(samples, indices)
else:
samples = [sample.squeeze(0) for sample in samples]
self.vae.to(device="cpu")
return samples
def timestep_transform(self, timesteps: Tensor, latents_shapes: Tensor):
# Skip if not needed.
if not self.config.diffusion.timesteps.get("transform", False):
return timesteps
# Compute resolution.
vt = self.config.vae.model.get("temporal_downsample_factor", 4)
vs = self.config.vae.model.get("spatial_downsample_factor", 8)
frames = (latents_shapes[:, 0] - 1) * vt + 1
heights = latents_shapes[:, 1] * vs
widths = latents_shapes[:, 2] * vs
# Compute shift factor.
def get_lin_function(x1, y1, x2, y2):
m = (y2 - y1) / (x2 - x1)
b = y1 - m * x1
return lambda x: m * x + b
img_shift_fn = get_lin_function(x1=256 * 256, y1=1.0, x2=1024 * 1024, y2=3.2)
vid_shift_fn = get_lin_function(x1=256 * 256 * 37, y1=1.0, x2=1280 * 720 * 145, y2=5.0)
shift = torch.where(
frames > 1,
vid_shift_fn(heights * widths * frames),
img_shift_fn(heights * widths),
)
# Shift timesteps.
timesteps = timesteps / self.schedule.T
timesteps = shift * timesteps / (1 + (shift - 1) * timesteps)
timesteps = timesteps * self.schedule.T
return timesteps
@torch.no_grad()
def inference(
self,
noises: List[Tensor],
conditions: List[Tensor],
texts_pos: Union[List[str], List[Tensor], List[Tuple[Tensor]]],
texts_neg: Union[List[str], List[Tensor], List[Tuple[Tensor]]],
cfg_scale: Optional[float] = None,
temporal_overlap: int = 0, # pylint: disable=unused-argument
) -> List[Tensor]:
assert len(noises) == len(conditions) == len(texts_pos) == len(texts_neg)
batch_size = len(noises)
# Return if empty.
if batch_size == 0:
return []
# Set cfg scale
if cfg_scale is None:
cfg_scale = self.config.diffusion.cfg.scale
# 🚀 OPTIMISATION: Détecter le dtype du modèle pour performance optimale
model_dtype = next(self.dit.parameters()).dtype
# Adapter les dtypes selon le modèle
if model_dtype in (torch.float8_e4m3fn, torch.float8_e5m2):
target_dtype = torch.float16
elif model_dtype == torch.float16:
target_dtype = torch.float16
else:
target_dtype = torch.bfloat16
# Text embeddings.
assert type(texts_pos[0]) is type(texts_neg[0])
if isinstance(texts_pos[0], str):
text_pos_embeds, text_pos_shapes = self.text_encode(texts_pos) # pylint: disable=no-member
text_neg_embeds, text_neg_shapes = self.text_encode(texts_neg) # pylint: disable=no-member
elif isinstance(texts_pos[0], tuple):
text_pos_embeds, text_pos_shapes = [], []
text_neg_embeds, text_neg_shapes = [], []
for pos in zip(*texts_pos):
emb, shape = na.flatten(pos)
text_pos_embeds.append(emb)
text_pos_shapes.append(shape)
for neg in zip(*texts_neg):
emb, shape = na.flatten(neg)
text_neg_embeds.append(emb)
text_neg_shapes.append(shape)
else:
text_pos_embeds, text_pos_shapes = na.flatten(texts_pos)
text_neg_embeds, text_neg_shapes = na.flatten(texts_neg)
# Adapter les embeddings texte au dtype cible (compatible avec FP8)
if isinstance(text_pos_embeds, torch.Tensor):
text_pos_embeds = text_pos_embeds.to(target_dtype)
if isinstance(text_neg_embeds, torch.Tensor):
text_neg_embeds = text_neg_embeds.to(target_dtype)
# Flatten.
latents, latents_shapes = na.flatten(noises)
latents_cond, _ = na.flatten(conditions)
# Adapter les latents au dtype cible (compatible avec FP8)
latents = latents.to(target_dtype) if latents.dtype != target_dtype else latents
latents_cond = latents_cond.to(target_dtype) if latents_cond.dtype != target_dtype else latents_cond
latents = self.sampler.sample(
x=latents,
f=lambda args: classifier_free_guidance_dispatcher(
pos=lambda: self.dit(
vid=torch.cat([args.x_t, latents_cond], dim=-1),
txt=text_pos_embeds,
vid_shape=latents_shapes,
txt_shape=text_pos_shapes,
timestep=args.t.repeat(batch_size),
).vid_sample,
neg=lambda: self.dit(
vid=torch.cat([args.x_t, latents_cond], dim=-1),
txt=text_neg_embeds,
vid_shape=latents_shapes,
txt_shape=text_neg_shapes,
timestep=args.t.repeat(batch_size),
).vid_sample,
scale=(
cfg_scale
if (args.i + 1) / len(self.sampler.timesteps)
<= self.config.diffusion.cfg.get("partial", 1)
else 1.0
),
rescale=self.config.diffusion.cfg.rescale,
),
)
latents = na.unflatten(latents, latents_shapes)
# 🎯 Pré-calcul des dtypes (une seule fois)
vae_dtype = self.vae.dtype
decode_dtype = torch.float16 if (vae_dtype == torch.float16 or target_dtype == torch.float16) else vae_dtype
samples = self.vae_decode(latents, target_dtype=decode_dtype)
if samples and len(samples) > 0 and samples[0].dtype != torch.float16:
samples = [sample.to(torch.float16, non_blocking=True) for sample in samples]
return samples
+61
View File
@@ -0,0 +1,61 @@
import os
import torch
from omegaconf import OmegaConf
from safetensors.torch import load_file as load_safetensors_file
from huggingface_hub import hf_hub_download
from src.optimization.memory_manager import preinitialize_rope_cache
from src.common.config import load_config, create_object
from src.core.infer import VideoDiffusionInfer
def configure_runner(model_name, cache_dir, device:str='cpu', dtype:torch.dtype=None):
repo_id = "vladmandic/SeedVR2"
script_directory = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
config_path = os.path.join(script_directory, './config_7b.yaml') if "7b" in model_name else os.path.join(script_directory, './config_3b.yaml')
config = load_config(config_path)
vae_config_path = os.path.join(script_directory, 'src/models/video_vae_v3/s8_c16_t4_inflation_sd3.yaml')
vae_config = OmegaConf.load(vae_config_path)
vae_config.spatial_downsample_factor = vae_config.get('spatial_downsample_factor', 8)
vae_config.temporal_downsample_factor = vae_config.get('temporal_downsample_factor', 4)
config.vae.model = OmegaConf.merge(config.vae.model, vae_config)
runner = VideoDiffusionInfer(config, device=device, dtype=dtype)
OmegaConf.set_readonly(runner.config, False)
# load dit
with torch.device("meta"):
runner.dit = create_object(config.dit.model)
runner.dit.eval().to(dtype)
runner.dit.to_empty(device="cpu")
model_file = hf_hub_download(repo_id=repo_id, filename=model_name, cache_dir=cache_dir)
state_dict = load_safetensors_file(model_file)
runner.dit.load_state_dict(state_dict, assign=True)
del state_dict
runner.dit = runner.dit.to(device=device, dtype=dtype)
# load vae
vae_file = hf_hub_download(repo_id=repo_id, filename=config.vae.checkpoint, cache_dir=cache_dir)
with torch.device("meta"):
runner.vae = create_object(config.vae.model)
runner.vae.requires_grad_(False).eval()
runner.vae.to_empty(device="cpu")
state_dict = load_safetensors_file(vae_file)
runner.vae.load_state_dict(state_dict)
del state_dict
runner.vae = runner.vae.to(device=device, dtype=dtype)
runner.config.vae.dtype = str(dtype)
runner.vae.set_causal_slicing(**config.vae.slicing)
runner.vae.set_memory_limit(**runner.config.vae.memory_limit)
# load embeds
pos_embeds_file = hf_hub_download(repo_id=repo_id, filename='pos_emb.pt', cache_dir=cache_dir)
neg_embeds_file = hf_hub_download(repo_id=repo_id, filename='neg_emb.pt', cache_dir=cache_dir)
runner.text_pos_embeds = torch.load(pos_embeds_file).to(device=device, dtype=dtype)
runner.text_neg_embeds = torch.load(neg_embeds_file).to(device=device, dtype=dtype)
preinitialize_rope_cache(runner)
return runner
@@ -0,0 +1,131 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // Licensed under the Apache License, Version 2.0 (the "License");
# // you may not use this file except in compliance with the License.
# // You may obtain a copy of the License at
# //
# // http://www.apache.org/licenses/LICENSE-2.0
# //
# // Unless required by applicable law or agreed to in writing, software
# // distributed under the License is distributed on an "AS IS" BASIS,
# // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# // See the License for the specific language governing permissions and
# // limitations under the License.
import math
import random
from typing import Union
import torch
from PIL import Image
from torchvision.transforms import functional as TVF
from torchvision.transforms.functional import InterpolationMode
class AreaResize:
def __init__(
self,
max_area: float,
downsample_only: bool = False,
interpolation: InterpolationMode = InterpolationMode.BICUBIC,
):
self.max_area = max_area
self.downsample_only = downsample_only
self.interpolation = interpolation
def __call__(self, image: Union[torch.Tensor, Image.Image]):
if isinstance(image, torch.Tensor):
height, width = image.shape[-2:]
elif isinstance(image, Image.Image):
width, height = image.size
else:
raise NotImplementedError
scale = math.sqrt(self.max_area / (height * width))
# keep original height and width for small pictures.
scale = 1 if scale >= 1 and self.downsample_only else scale
resized_height, resized_width = round(height * scale), round(width * scale)
return TVF.resize(
image,
size=(resized_height, resized_width),
interpolation=self.interpolation,
)
class AreaRandomCrop:
def __init__(
self,
max_area: float,
):
self.max_area = max_area
def get_params(self, input_size, output_size):
"""Get parameters for ``crop`` for a random crop.
Args:
img (PIL Image): Image to be cropped.
output_size (tuple): Expected output size of the crop.
Returns:
tuple: params (i, j, h, w) to be passed to ``crop`` for random crop.
"""
# w, h = _get_image_size(img)
h, w = input_size
th, tw = output_size
if w <= tw and h <= th:
return 0, 0, h, w
i = random.randint(0, h - th)
j = random.randint(0, w - tw)
return i, j, th, tw
def __call__(self, image: Union[torch.Tensor, Image.Image]):
if isinstance(image, torch.Tensor):
height, width = image.shape[-2:]
elif isinstance(image, Image.Image):
width, height = image.size
else:
raise NotImplementedError
resized_height = math.sqrt(self.max_area / (width / height))
resized_width = (width / height) * resized_height
resized_height, resized_width = round(resized_height), round(resized_width)
i, j, h, w = self.get_params((height, width), (resized_height, resized_width))
image = TVF.crop(image, i, j, h, w)
return image
class ScaleResize:
def __init__(
self,
scale: float,
):
self.scale = scale
def __call__(self, image: Union[torch.Tensor, Image.Image]):
if isinstance(image, torch.Tensor):
height, width = image.shape[-2:]
interpolation_mode = InterpolationMode.BILINEAR
antialias = True if image.ndim == 4 else "warn"
elif isinstance(image, Image.Image):
width, height = image.size
interpolation_mode = InterpolationMode.LANCZOS
antialias = "warn"
else:
raise NotImplementedError
scale = self.scale
# keep original height and width for small pictures
resized_height, resized_width = round(height * scale), round(width * scale)
image = TVF.resize(
image,
size=(resized_height, resized_width),
interpolation=interpolation_mode,
antialias=antialias,
)
return image
@@ -0,0 +1,40 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 Union
import torch
from PIL import Image
from torchvision.transforms import functional as TVF
class DivisibleCrop:
def __init__(self, factor):
if not isinstance(factor, tuple):
factor = (factor, factor)
self.height_factor, self.width_factor = factor[0], factor[1]
def __call__(self, image: Union[torch.Tensor, Image.Image]):
if isinstance(image, torch.Tensor):
height, width = image.shape[-2:]
elif isinstance(image, Image.Image):
width, height = image.size
else:
raise NotImplementedError
cropped_height = height - (height % self.height_factor)
cropped_width = width - (width % self.width_factor)
image = TVF.center_crop(img=image, output_size=(cropped_height, cropped_width))
return image
@@ -0,0 +1,50 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 Literal
from torchvision.transforms import CenterCrop, Compose, InterpolationMode, Resize
from .area_resize import AreaResize
from .side_resize import SideResize
def NaResize(
resolution: int,
mode: Literal["area", "side"],
downsample_only: bool,
interpolation: InterpolationMode = InterpolationMode.BICUBIC,
):
if mode == "area":
return AreaResize(
max_area=resolution**2,
downsample_only=downsample_only,
interpolation=interpolation,
)
if mode == "side":
return SideResize(
size=resolution,
downsample_only=downsample_only,
interpolation=interpolation,
)
if mode == "square":
return Compose(
[
Resize(
size=resolution,
interpolation=interpolation,
),
CenterCrop(resolution),
]
)
raise ValueError(f"Unknown resize mode: {mode}")
@@ -0,0 +1,54 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 Union
import torch
from PIL import Image
from torchvision.transforms import InterpolationMode
from torchvision.transforms import functional as TVF
class SideResize:
def __init__(
self,
size: int,
downsample_only: bool = False,
interpolation: InterpolationMode = InterpolationMode.BICUBIC,
):
self.size = size
self.downsample_only = downsample_only
self.interpolation = interpolation
def __call__(self, image: Union[torch.Tensor, Image.Image]):
"""
Args:
image (PIL Image or Tensor): Image to be scaled.
Returns:
PIL Image or Tensor: Rescaled image.
"""
if isinstance(image, torch.Tensor):
height, width = image.shape[-2:]
elif isinstance(image, Image.Image):
width, height = image.size
else:
raise NotImplementedError
if self.downsample_only and min(width, height) < self.size:
# keep original height and width for small pictures.
size = min(width, height)
else:
size = self.size
return TVF.resize(image, size, self.interpolation)
@@ -0,0 +1,94 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // Licensed under the Apache License, Version 2.0 (the "License");
# // you may not use this file except in compliance with the License.
# // You may obtain a copy of the License at
# //
# // http://www.apache.org/licenses/LICENSE-2.0
# //
# // Unless required by applicable law or agreed to in writing, software
# // distributed under the License is distributed on an "AS IS" BASIS,
# // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# // See the License for the specific language governing permissions and
# // limitations under the License.
import torch
import torch.nn.functional as F
#from flash_attn import flash_attn_varlen_func
from torch import nn
def pytorch_varlen_attention(q, k, v, cu_seqlens_q, cu_seqlens_k, max_seqlen_q, max_seqlen_k, dropout_p=0.0, softmax_scale=None, causal=False, deterministic=False):
"""
A PyTorch-based implementation of variable-length attention to replace flash_attn_varlen_func.
It processes each sequence in the batch individually.
"""
# Create an empty tensor to store the output.
output = torch.empty_like(q)
# Iterate over each sequence in the batch. The batch size is the number of sequences.
for i in range(len(cu_seqlens_q) - 1):
# Determine the start and end indices for the current sequence.
start_q, end_q = cu_seqlens_q[i], cu_seqlens_q[i+1]
start_k, end_k = cu_seqlens_k[i], cu_seqlens_k[i+1]
# Slice the q, k, and v tensors to get the data for the current sequence.
# The shape is (seq_len, heads, head_dim).
q_i = q[start_q:end_q]
k_i = k[start_k:end_k]
v_i = v[start_k:end_k]
# Reshape for torch's scaled_dot_product_attention which expects (batch, heads, seq, dim).
# Here, we treat each sequence as a batch of 1.
q_i = q_i.permute(1, 0, 2).unsqueeze(0) # (1, heads, seq_len_q, head_dim)
k_i = k_i.permute(1, 0, 2).unsqueeze(0) # (1, heads, seq_len_k, head_dim)
v_i = v_i.permute(1, 0, 2).unsqueeze(0) # (1, heads, seq_len_k, head_dim)
# Use PyTorch's built-in scaled dot-product attention.
output_i = F.scaled_dot_product_attention(
q_i, k_i, v_i,
dropout_p=dropout_p if not deterministic else 0.0,
is_causal=causal
)
# Reshape the output back to the original format (seq_len, heads, head_dim)
output_i = output_i.squeeze(0).permute(1, 0, 2)
# Place the result for the current sequence into the main output tensor.
output[start_q:end_q] = output_i
return output
class TorchAttention(nn.Module):
def tflops(self, args, kwargs, output) -> float:
assert len(args) == 0 or len(args) > 2, "query, key should both provided by args / kwargs"
q = kwargs.get("query") or args[0]
k = kwargs.get("key") or args[1]
b, h, sq, d = q.shape
b, h, sk, d = k.shape
return b * h * (4 * d * (sq / 1e6) * (sk / 1e6))
def forward(self, *args, **kwargs):
#return pytorch_varlen_attention(*args, **kwargs)
return F.scaled_dot_product_attention(*args, **kwargs)
class FlashAttentionVarlen(nn.Module):
def tflops(self, args, kwargs, output) -> float:
cu_seqlens_q = kwargs["cu_seqlens_q"]
cu_seqlens_k = kwargs["cu_seqlens_k"]
_, h, d = output.shape
seqlens_q = (cu_seqlens_q[1:] - cu_seqlens_q[:-1]) / 1e6
seqlens_k = (cu_seqlens_k[1:] - cu_seqlens_k[:-1]) / 1e6
return h * (4 * d * (seqlens_q * seqlens_k).sum())
def forward(self, *args, **kwargs):
kwargs["deterministic"] = torch.are_deterministic_algorithms_enabled()
try:
from flash_attn import flash_attn_varlen_func
return flash_attn_varlen_func(*args, **kwargs)
except ImportError:
return pytorch_varlen_attention(*args, **kwargs)
@@ -0,0 +1,25 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 .mmdit_window_block import MMWindowTransformerBlock
dit_blocks = {
"mmdit_window": MMWindowTransformerBlock,
}
def get_block(block_type: str):
if block_type in dit_blocks:
return dit_blocks[block_type]
raise NotImplementedError(f"{block_type} is not supported")
@@ -0,0 +1,233 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 Tuple, Union
import torch
from einops import rearrange
from torch import nn
from torch.nn import functional as F
from torch.nn.modules.utils import _triple
from ....common.half_precision_fixes import safe_pad_operation
from ....common.distributed.ops import (
gather_heads,
gather_heads_scatter_seq,
gather_seq_scatter_heads_qkv,
scatter_heads,
)
from ..attention import TorchAttention
from ..mlp import get_mlp
from ..mm import MMArg, MMModule
from ..modulation import ada_layer_type
from ..normalization import norm_layer_type
from ..rope import RotaryEmbedding3d
class MMWindowAttention(nn.Module):
def __init__(
self,
vid_dim: int,
txt_dim: int,
heads: int,
head_dim: int,
qk_bias: bool,
qk_rope: bool,
qk_norm: norm_layer_type,
qk_norm_eps: float,
window: Union[int, Tuple[int, int, int]],
window_method: str,
shared_qkv: bool,
):
super().__init__()
dim = MMArg(vid_dim, txt_dim)
inner_dim = heads * head_dim
qkv_dim = inner_dim * 3
self.window = _triple(window)
self.window_method = window_method
assert all(map(lambda v: isinstance(v, int) and v >= 0, self.window))
self.head_dim = head_dim
self.proj_qkv = MMModule(nn.Linear, dim, qkv_dim, bias=qk_bias, shared_weights=shared_qkv)
self.proj_out = MMModule(nn.Linear, inner_dim, dim, shared_weights=shared_qkv)
self.norm_q = MMModule(qk_norm, dim=head_dim, eps=qk_norm_eps, elementwise_affine=True)
self.norm_k = MMModule(qk_norm, dim=head_dim, eps=qk_norm_eps, elementwise_affine=True)
self.rope = RotaryEmbedding3d(dim=head_dim // 2) if qk_rope else None
self.attn = TorchAttention()
def forward(
self,
vid: torch.FloatTensor, # b T H W c
txt: torch.FloatTensor, # b L c
txt_mask: torch.BoolTensor, # b L
) -> Tuple[
torch.FloatTensor,
torch.FloatTensor,
]:
# Project q, k, v.
vid_qkv, txt_qkv = self.proj_qkv(vid, txt)
vid_qkv = gather_seq_scatter_heads_qkv(vid_qkv, seq_dim=2)
_, T, H, W, _ = vid_qkv.shape
_, L, _ = txt.shape
if self.window_method == "win":
nt, nh, nw = self.window
tt, hh, ww = T // nt, H // nh, W // nw
elif self.window_method == "win_by_size":
tt, hh, ww = self.window
tt, hh, ww = (
tt if tt > 0 else T,
hh if hh > 0 else H,
ww if ww > 0 else W,
)
nt, nh, nw = T // tt, H // hh, W // ww
else:
raise NotImplementedError
vid_qkv = rearrange(vid_qkv, "b T H W (o h d) -> o b h (T H W) d", o=3, d=self.head_dim)
txt_qkv = rearrange(txt_qkv, "b L (o h d) -> o b h L d", o=3, d=self.head_dim)
txt_qkv = scatter_heads(txt_qkv, dim=2)
vid_q, vid_k, vid_v = vid_qkv.unbind()
txt_q, txt_k, txt_v = txt_qkv.unbind()
vid_q, txt_q = self.norm_q(vid_q, txt_q)
vid_k, txt_k = self.norm_k(vid_k, txt_k)
if self.rope:
vid_q, vid_k = self.rope(vid_q, vid_k, (T, H, W))
def vid_window(v):
return rearrange(
v,
"b h (nt tt nh hh nw ww) d -> b h (nt nh nw) (tt hh ww) d",
hh=hh,
ww=ww,
tt=tt,
nh=nh,
nw=nw,
nt=nt,
)
def txt_window(t):
return rearrange(t, "b h L d -> b h 1 L d").expand(-1, -1, nt * nh * nw, -1, -1)
# Process video attention.
vid_msk = safe_pad_operation(txt_mask, (tt * hh * ww, 0), value=True)
vid_msk = rearrange(vid_msk, "b l -> b 1 1 1 l").expand(-1, 1, 1, tt * hh * ww, -1)
vid_out = self.attn(
vid_window(vid_q),
torch.cat([vid_window(vid_k), txt_window(txt_k)], dim=-2),
torch.cat([vid_window(vid_v), txt_window(txt_v)], dim=-2),
vid_msk,
)
vid_out = rearrange(
vid_out,
"b h (nt nh nw) (tt hh ww) d -> b (nt tt) (nh hh) (nw ww) (h d)",
hh=hh,
ww=ww,
tt=tt,
nh=nh,
nw=nw,
)
vid_out = gather_heads_scatter_seq(vid_out, head_dim=4, seq_dim=2)
# Process text attention.
txt_msk = safe_pad_operation(txt_mask, (T * H * W, 0), value=True)
txt_msk = rearrange(txt_msk, "b l -> b 1 1 l").expand(-1, 1, L, -1)
txt_out = self.attn(
txt_q,
torch.cat([vid_k, txt_k], dim=-2),
torch.cat([vid_v, txt_v], dim=-2),
txt_msk,
)
txt_out = rearrange(txt_out, "b h L d -> b L (h d)")
txt_out = gather_heads(txt_out, dim=2)
# Project output.
vid_out, txt_out = self.proj_out(vid_out, txt_out)
return vid_out, txt_out
class MMWindowTransformerBlock(nn.Module):
def __init__(
self,
*,
vid_dim: int,
txt_dim: int,
emb_dim: int,
heads: int,
head_dim: int,
expand_ratio: int,
norm: norm_layer_type,
norm_eps: float,
ada: ada_layer_type,
qk_bias: bool,
qk_rope: bool,
qk_norm: norm_layer_type,
window: Union[int, Tuple[int, int, int]],
window_method: str,
shared_qkv: bool,
shared_mlp: bool,
mlp_type: str,
**kwargs,
):
super().__init__()
dim = MMArg(vid_dim, txt_dim)
self.attn_norm = MMModule(norm, dim=dim, eps=norm_eps, elementwise_affine=False)
self.attn = MMWindowAttention(
vid_dim=vid_dim,
txt_dim=txt_dim,
heads=heads,
head_dim=head_dim,
qk_bias=qk_bias,
qk_rope=qk_rope,
qk_norm=qk_norm,
qk_norm_eps=norm_eps,
window=window,
window_method=window_method,
shared_qkv=shared_qkv,
)
self.mlp_norm = MMModule(norm, dim=dim, eps=norm_eps, elementwise_affine=False)
self.mlp = MMModule(
get_mlp(mlp_type),
dim=dim,
expand_ratio=expand_ratio,
shared_weights=shared_mlp,
)
self.ada = MMModule(ada, dim=dim, emb_dim=emb_dim, layers=["attn", "mlp"])
def forward(
self,
vid: torch.FloatTensor,
txt: torch.FloatTensor,
txt_mask: torch.BoolTensor,
emb: torch.FloatTensor,
) -> Tuple[
torch.FloatTensor,
torch.FloatTensor,
]:
vid_attn, txt_attn = self.attn_norm(vid, txt)
vid_attn, txt_attn = self.ada(vid_attn, txt_attn, emb=emb, layer="attn", mode="in")
vid_attn, txt_attn = self.attn(vid_attn, txt_attn, txt_mask=txt_mask)
vid_attn, txt_attn = self.ada(vid_attn, txt_attn, emb=emb, layer="attn", mode="out")
vid_attn, txt_attn = (vid_attn + vid), (txt_attn + txt)
vid_mlp, txt_mlp = self.mlp_norm(vid_attn, txt_attn)
vid_mlp, txt_mlp = self.ada(vid_mlp, txt_mlp, emb=emb, layer="mlp", mode="in")
vid_mlp, txt_mlp = self.mlp(vid_mlp, txt_mlp)
vid_mlp, txt_mlp = self.ada(vid_mlp, txt_mlp, emb=emb, layer="mlp", mode="out")
vid_mlp, txt_mlp = (vid_mlp + vid_attn), (txt_mlp + txt_attn)
return vid_mlp, txt_mlp
@@ -0,0 +1,62 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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, Union
import torch
from diffusers.models.embeddings import get_timestep_embedding
from torch import nn
def emb_add(emb1: torch.Tensor, emb2: Optional[torch.Tensor]):
return emb1 if emb2 is None else emb1 + emb2
class TimeEmbedding(nn.Module):
def __init__(
self,
sinusoidal_dim: int,
hidden_dim: int,
output_dim: int,
):
super().__init__()
self.sinusoidal_dim = sinusoidal_dim
self.proj_in = nn.Linear(sinusoidal_dim, hidden_dim)
self.proj_hid = nn.Linear(hidden_dim, hidden_dim)
self.proj_out = nn.Linear(hidden_dim, output_dim)
self.act = nn.SiLU()
def forward(
self,
timestep: Union[int, float, torch.IntTensor, torch.FloatTensor],
device: torch.device,
dtype: torch.dtype,
) -> torch.FloatTensor:
if not torch.is_tensor(timestep):
timestep = torch.tensor([timestep], device=device, dtype=dtype)
if timestep.ndim == 0:
timestep = timestep[None]
emb = get_timestep_embedding(
timesteps=timestep,
embedding_dim=self.sinusoidal_dim,
flip_sin_to_cos=False,
downscale_freq_shift=0,
)
emb = emb.to(dtype)
emb = self.proj_in(emb)
emb = self.act(emb)
emb = self.proj_hid(emb)
emb = self.act(emb)
emb = self.proj_out(emb)
return emb
+62
View File
@@ -0,0 +1,62 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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
import torch
import torch.nn.functional as F
from torch import nn
def get_mlp(mlp_type: Optional[str] = "normal"):
if mlp_type == "normal":
return MLP
elif mlp_type == "swiglu":
return SwiGLUMLP
class MLP(nn.Module):
def __init__(
self,
dim: int,
expand_ratio: int,
):
super().__init__()
self.proj_in = nn.Linear(dim, dim * expand_ratio)
self.act = nn.GELU("tanh")
self.proj_out = nn.Linear(dim * expand_ratio, dim)
def forward(self, x: torch.FloatTensor) -> torch.FloatTensor:
x = self.proj_in(x)
x = self.act(x)
x = self.proj_out(x)
return x
class SwiGLUMLP(nn.Module):
def __init__(
self,
dim: int,
expand_ratio: int,
multiple_of: int = 256,
):
super().__init__()
hidden_dim = int(2 * dim * expand_ratio / 3)
hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)
self.proj_in_gate = nn.Linear(dim, hidden_dim, bias=False)
self.proj_out = nn.Linear(hidden_dim, dim, bias=False)
self.proj_in = nn.Linear(dim, hidden_dim, bias=False)
def forward(self, x: torch.FloatTensor) -> torch.FloatTensor:
x = self.proj_out(F.silu(self.proj_in_gate(x)) * self.proj_in(x))
return x
+67
View File
@@ -0,0 +1,67 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 dataclasses import dataclass
from typing import Any, Callable, Dict, List, Tuple
import torch
from torch import nn
@dataclass
class MMArg:
vid: Any
txt: Any
def get_args(key: str, args: List[Any]) -> List[Any]:
return [getattr(v, key) if isinstance(v, MMArg) else v for v in args]
def get_kwargs(key: str, kwargs: Dict[str, Any]) -> Dict[str, Any]:
return {k: getattr(v, key) if isinstance(v, MMArg) else v for k, v in kwargs.items()}
class MMModule(nn.Module):
def __init__(
self,
module: Callable[..., nn.Module],
*args,
shared_weights: bool = False,
**kwargs,
):
super().__init__()
self.shared_weights = shared_weights
if self.shared_weights:
assert get_args("vid", args) == get_args("txt", args)
assert get_kwargs("vid", kwargs) == get_kwargs("txt", kwargs)
self.all = module(*get_args("vid", args), **get_kwargs("vid", kwargs))
else:
self.vid = module(*get_args("vid", args), **get_kwargs("vid", kwargs))
self.txt = module(*get_args("txt", args), **get_kwargs("txt", kwargs))
def forward(
self,
vid: torch.FloatTensor,
txt: torch.FloatTensor,
*args,
**kwargs,
) -> Tuple[
torch.FloatTensor,
torch.FloatTensor,
]:
vid_module = self.vid if not self.shared_weights else self.all
txt_module = self.txt if not self.shared_weights else self.all
vid = vid_module(vid, *get_args("vid", args), **get_kwargs("vid", kwargs))
txt = txt_module(txt, *get_args("txt", args), **get_kwargs("txt", kwargs))
return vid, txt
@@ -0,0 +1,97 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 Callable, List, Optional
import torch
from einops import rearrange
from torch import nn
from ...common.cache import Cache
from ...common.distributed.ops import slice_inputs
# (dim: int, emb_dim: int)
ada_layer_type = Callable[[int, int], nn.Module]
def get_ada_layer(ada_layer: str) -> ada_layer_type:
if ada_layer == "single":
return AdaSingle
raise NotImplementedError(f"{ada_layer} is not supported")
def expand_dims(x: torch.Tensor, dim: int, ndim: int):
"""
Expand tensor "x" to "ndim" by adding empty dims at "dim".
Example: x is (b d), target ndim is 5, add dim at 1, return (b 1 1 1 d).
"""
shape = x.shape
shape = shape[:dim] + (1,) * (ndim - len(shape)) + shape[dim:]
return x.reshape(shape)
class AdaSingle(nn.Module):
def __init__(
self,
dim: int,
emb_dim: int,
layers: List[str],
):
assert emb_dim == 6 * dim, "AdaSingle requires emb_dim == 6 * dim"
super().__init__()
self.dim = dim
self.emb_dim = emb_dim
self.layers = layers
for l in layers:
self.register_parameter(f"{l}_shift", nn.Parameter(torch.randn(dim) / dim**0.5))
self.register_parameter(f"{l}_scale", nn.Parameter(torch.randn(dim) / dim**0.5 + 1))
self.register_parameter(f"{l}_gate", nn.Parameter(torch.randn(dim) / dim**0.5))
def forward(
self,
hid: torch.FloatTensor, # b ... c
emb: torch.FloatTensor, # b d
layer: str,
mode: str,
cache: Cache = Cache(disable=True),
branch_tag: str = "",
hid_len: Optional[torch.LongTensor] = None, # b
) -> torch.FloatTensor:
idx = self.layers.index(layer)
emb = rearrange(emb, "b (d l g) -> b d l g", l=len(self.layers), g=3)[..., idx, :]
emb = expand_dims(emb, 1, hid.ndim + 1)
if hid_len is not None:
emb = cache(
f"emb_repeat_{idx}_{branch_tag}",
lambda: slice_inputs(
torch.cat([e.repeat(l, *([1] * e.ndim)) for e, l in zip(emb, hid_len)]),
dim=0,
),
)
shiftA, scaleA, gateA = emb.unbind(-1)
shiftB, scaleB, gateB = (
getattr(self, f"{layer}_shift"),
getattr(self, f"{layer}_scale"),
getattr(self, f"{layer}_gate"),
)
if mode == "in":
return hid.mul_(scaleA + scaleB).add_(shiftA + shiftB)
if mode == "out":
return hid.mul_(gateA + gateB)
raise NotImplementedError
def extra_repr(self) -> str:
return f"dim={self.dim}, emb_dim={self.emb_dim}, layers={self.layers}"
+241
View File
@@ -0,0 +1,241 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 itertools import chain
from typing import Callable, Dict, List, Tuple
import einops
import torch
def flatten(
hid: List[torch.FloatTensor], # List of (*** c)
) -> Tuple[
torch.FloatTensor, # (L c)
torch.LongTensor, # (b n)
]:
assert len(hid) > 0
shape = torch.stack([torch.tensor(x.shape[:-1], device=hid[0].device) for x in hid])
hid = torch.cat([x.flatten(0, -2) for x in hid])
return hid, shape
def unflatten(
hid: torch.FloatTensor, # (L c) or (L ... c)
hid_shape: torch.LongTensor, # (b n)
) -> List[torch.Tensor]: # List of (*** c) or (*** ... c)
hid_len = hid_shape.prod(-1)
hid = hid.split(hid_len.tolist())
hid = [x.unflatten(0, s.tolist()) for x, s in zip(hid, hid_shape)]
return hid
def concat(
vid: torch.FloatTensor, # (VL ... c)
txt: torch.FloatTensor, # (TL ... c)
vid_len: torch.LongTensor, # (b)
txt_len: torch.LongTensor, # (b)
) -> torch.FloatTensor: # (L ... c)
vid = torch.split(vid, vid_len.tolist())
txt = torch.split(txt, txt_len.tolist())
return torch.cat(list(chain(*zip(vid, txt))))
def concat_idx(
vid_len: torch.LongTensor, # (b)
txt_len: torch.LongTensor, # (b)
) -> Tuple[
Callable,
Callable,
]:
device = vid_len.device
vid_idx = torch.arange(vid_len.sum(), device=device)
txt_idx = torch.arange(len(vid_idx), len(vid_idx) + txt_len.sum(), device=device)
tgt_idx = concat(vid_idx, txt_idx, vid_len, txt_len)
src_idx = torch.argsort(tgt_idx)
return (
lambda vid, txt: torch.index_select(torch.cat([vid, txt]), 0, tgt_idx),
lambda all: torch.index_select(all, 0, src_idx).split([len(vid_idx), len(txt_idx)]),
)
def unconcat(
all: torch.FloatTensor, # (L ... c)
vid_len: torch.LongTensor, # (b)
txt_len: torch.LongTensor, # (b)
) -> Tuple[
torch.FloatTensor, # (VL ... c)
torch.FloatTensor, # (TL ... c)
]:
interleave_len = list(chain(*zip(vid_len.tolist(), txt_len.tolist())))
all = all.split(interleave_len)
vid = torch.cat(all[0::2])
txt = torch.cat(all[1::2])
return vid, txt
def repeat_concat(
vid: torch.FloatTensor, # (VL ... c)
txt: torch.FloatTensor, # (TL ... c)
vid_len: torch.LongTensor, # (n*b)
txt_len: torch.LongTensor, # (b)
txt_repeat: List, # (n)
) -> torch.FloatTensor: # (L ... c)
vid = torch.split(vid, vid_len.tolist())
txt = torch.split(txt, txt_len.tolist())
txt = [[x] * n for x, n in zip(txt, txt_repeat)]
txt = list(chain(*txt))
return torch.cat(list(chain(*zip(vid, txt))))
def repeat_concat_idx(
vid_len: torch.LongTensor, # (n*b)
txt_len: torch.LongTensor, # (b)
txt_repeat: torch.LongTensor, # (n)
) -> Tuple[
Callable,
Callable,
]:
device = vid_len.device
vid_idx = torch.arange(vid_len.sum(), device=device)
txt_idx = torch.arange(len(vid_idx), len(vid_idx) + txt_len.sum(), device=device)
txt_repeat_list = txt_repeat.tolist()
tgt_idx = repeat_concat(vid_idx, txt_idx, vid_len, txt_len, txt_repeat)
src_idx = torch.argsort(tgt_idx)
txt_idx_len = len(tgt_idx) - len(vid_idx)
repeat_txt_len = (txt_len * txt_repeat).tolist()
def unconcat_coalesce(all):
"""
Un-concat vid & txt, and coalesce the repeated txt.
e.g. vid [0 1 2 3 4 5 6 7 8] -> 3 splits -> [0 1 2] [3 4 5] [6 7 8]
txt [9 10]
repeat_concat ==> [0 1 2 9 10 3 4 5 9 10 6 7 8 9 10]
1. argsort re-index ==> [0 1 2 3 4 5 6 7 8 9 9 9 10 10 10]
split ==> vid_out [0 1 2 3 4 5 6 7 8] txt_out [9 9 9 10 10 10]
2. reshape & mean for each sample to coalesce the repeated txt.
"""
vid_out, txt_out = all[src_idx].split([len(vid_idx), txt_idx_len])
txt_out_coalesced = []
for txt, repeat_time in zip(txt_out.split(repeat_txt_len), txt_repeat_list):
txt = txt.reshape(-1, repeat_time, *txt.shape[1:]).mean(1)
txt_out_coalesced.append(txt)
return vid_out, torch.cat(txt_out_coalesced)
# Note: Backward of torch.index_select is non-deterministic when existing repeated index,
# the difference may cumulative like torch.repeat_interleave, so we use vanilla index here.
return (
lambda vid, txt: torch.cat([vid, txt])[tgt_idx],
lambda all: unconcat_coalesce(all),
)
def rearrange(
hid: torch.FloatTensor, # (L c)
hid_shape: torch.LongTensor, # (b n)
pattern: str,
**kwargs: Dict[str, int],
) -> Tuple[
torch.FloatTensor,
torch.LongTensor,
]:
return flatten([einops.rearrange(h, pattern, **kwargs) for h in unflatten(hid, hid_shape)])
def rearrange_idx(
hid_shape: torch.LongTensor, # (b n)
pattern: str,
**kwargs: Dict[str, int],
) -> Tuple[Callable, Callable, torch.LongTensor]:
hid_idx = torch.arange(hid_shape.prod(-1).sum(), device=hid_shape.device).unsqueeze(-1)
tgt_idx, tgt_shape = rearrange(hid_idx, hid_shape, pattern, **kwargs)
tgt_idx = tgt_idx.squeeze(-1)
src_idx = torch.argsort(tgt_idx)
return (
lambda hid: torch.index_select(hid, 0, tgt_idx),
lambda hid: torch.index_select(hid, 0, src_idx),
tgt_shape,
)
def repeat(
hid: torch.FloatTensor, # (L c)
hid_shape: torch.LongTensor, # (b n)
pattern: str,
**kwargs: Dict[str, torch.LongTensor], # (b)
) -> Tuple[
torch.FloatTensor,
torch.LongTensor,
]:
hid = unflatten(hid, hid_shape)
kwargs = [{k: v[i].item() for k, v in kwargs.items()} for i in range(len(hid))]
return flatten([einops.repeat(h, pattern, **a) for h, a in zip(hid, kwargs)])
def pack(
samples: List[torch.Tensor], # List of (h w c).
) -> Tuple[
List[torch.Tensor], # groups [(b1 h1 w1 c1), (b2 h2 w2 c2)]
List[List[int]], # reversal indices.
]:
batches = {}
indices = {}
for i, sample in enumerate(samples):
shape = sample.shape
batches[shape] = batches.get(shape, [])
indices[shape] = indices.get(shape, [])
batches[shape].append(sample)
indices[shape].append(i)
batches = list(map(torch.stack, batches.values()))
indices = list(indices.values())
return batches, indices
def unpack(
batches: List[torch.Tensor],
indices: List[List[int]],
) -> List[torch.Tensor]:
samples = [None] * (max(chain(*indices)) + 1)
for batch, index in zip(batches, indices):
for sample, i in zip(batch.unbind(), index):
samples[i] = sample
return samples
def window(
hid: torch.FloatTensor, # (L c)
hid_shape: torch.LongTensor, # (b n)
window_fn: Callable[[torch.Tensor], List[torch.Tensor]],
):
hid = unflatten(hid, hid_shape)
hid = list(map(window_fn, hid))
hid_windows = torch.tensor(list(map(len, hid)), device=hid_shape.device)
hid, hid_shape = flatten(list(chain(*hid)))
return hid, hid_shape, hid_windows
def window_idx(
hid_shape: torch.LongTensor, # (b n)
window_fn: Callable[[torch.Tensor], List[torch.Tensor]],
):
hid_idx = torch.arange(hid_shape.prod(-1).sum(), device=hid_shape.device).unsqueeze(-1)
tgt_idx, tgt_shape, tgt_windows = window(hid_idx, hid_shape, window_fn)
tgt_idx = tgt_idx.squeeze(-1)
src_idx = torch.argsort(tgt_idx)
return (
lambda hid: torch.index_select(hid, 0, tgt_idx),
lambda hid: torch.index_select(hid, 0, src_idx),
tgt_shape,
tgt_windows,
)
@@ -0,0 +1,25 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 .mmsr_block import NaMMSRTransformerBlock
nadit_blocks = {
"mmdit_sr": NaMMSRTransformerBlock,
}
def get_nablock(block_type: str):
if block_type in nadit_blocks:
return nadit_blocks[block_type]
raise NotImplementedError(f"{block_type} is not supported")
@@ -0,0 +1,248 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 Tuple, Union
import torch
from einops import rearrange
from torch.nn import functional as F
# from ..cache import Cache
from ....common.cache import Cache
from ....common.distributed.ops import gather_heads_scatter_seq, gather_seq_scatter_heads_qkv
from .. import na
from ..attention import FlashAttentionVarlen
from ..blocks.mmdit_window_block import MMWindowAttention, MMWindowTransformerBlock
from ..mm import MMArg
from ..modulation import ada_layer_type
from ..normalization import norm_layer_type
from ..rope import NaRotaryEmbedding3d
from ..window import get_window_op
from ....common.half_precision_fixes import safe_pad_operation
class NaSwinAttention(MMWindowAttention):
def __init__(
self,
vid_dim: int,
txt_dim: int,
heads: int,
head_dim: int,
qk_bias: bool,
qk_rope: bool,
qk_norm: norm_layer_type,
qk_norm_eps: float,
window: Union[int, Tuple[int, int, int]],
window_method: str,
shared_qkv: bool,
**kwargs,
):
super().__init__(
vid_dim=vid_dim,
txt_dim=txt_dim,
heads=heads,
head_dim=head_dim,
qk_bias=qk_bias,
qk_rope=qk_rope,
qk_norm=qk_norm,
qk_norm_eps=qk_norm_eps,
window=window,
window_method=window_method,
shared_qkv=shared_qkv,
)
self.rope = NaRotaryEmbedding3d(dim=head_dim // 2) if qk_rope else None
self.attn = FlashAttentionVarlen()
self.window_op = get_window_op(window_method)
def forward(
self,
vid: torch.FloatTensor, # l c
txt: torch.FloatTensor, # l c
vid_shape: torch.LongTensor, # b 3
txt_shape: torch.LongTensor, # b 1
cache: Cache,
) -> Tuple[
torch.FloatTensor,
torch.FloatTensor,
]:
vid_qkv, txt_qkv = self.proj_qkv(vid, txt)
vid_qkv = gather_seq_scatter_heads_qkv(
vid_qkv,
seq_dim=0,
qkv_shape=vid_shape,
cache=cache.namespace("vid"),
)
txt_qkv = gather_seq_scatter_heads_qkv(
txt_qkv,
seq_dim=0,
qkv_shape=txt_shape,
cache=cache.namespace("txt"),
)
# re-org the input seq for window attn
cache_win = cache.namespace(f"{self.window_method}_{self.window}_sd3")
def make_window(x: torch.Tensor):
t, h, w, _ = x.shape
window_slices = self.window_op((t, h, w), self.window)
return [x[st, sh, sw] for (st, sh, sw) in window_slices]
window_partition, window_reverse, window_shape, window_count = cache_win(
"win_transform",
lambda: na.window_idx(vid_shape, make_window),
)
vid_qkv_win = window_partition(vid_qkv)
vid_qkv_win = rearrange(vid_qkv_win, "l (o h d) -> l o h d", o=3, d=self.head_dim)
txt_qkv = rearrange(txt_qkv, "l (o h d) -> l o h d", o=3, d=self.head_dim)
vid_q, vid_k, vid_v = vid_qkv_win.unbind(1)
txt_q, txt_k, txt_v = txt_qkv.unbind(1)
vid_q, txt_q = self.norm_q(vid_q, txt_q)
vid_k, txt_k = self.norm_k(vid_k, txt_k)
txt_len = cache("txt_len", lambda: txt_shape.prod(-1))
vid_len_win = cache_win("vid_len", lambda: window_shape.prod(-1))
txt_len_win = cache_win("txt_len", lambda: txt_len.repeat_interleave(window_count))
all_len_win = cache_win("all_len", lambda: vid_len_win + txt_len_win)
concat_win, unconcat_win = cache_win(
"mm_pnp", lambda: na.repeat_concat_idx(vid_len_win, txt_len, window_count)
)
# window rope
if self.rope:
vid_q, vid_k = self.rope(vid_q, vid_k, window_shape, cache_win)
out = self.attn(
q=concat_win(vid_q, txt_q).bfloat16(),
k=concat_win(vid_k, txt_k).bfloat16(),
v=concat_win(vid_v, txt_v).bfloat16(),
cu_seqlens_q=cache_win(
"vid_seqlens_q", lambda: safe_pad_operation(all_len_win.cumsum(0), (1, 0)).int()
),
cu_seqlens_k=cache_win(
"vid_seqlens_k", lambda: safe_pad_operation(all_len_win.cumsum(0), (1, 0)).int()
),
max_seqlen_q=cache_win("vid_max_seqlen_q", lambda: all_len_win.max().item()),
max_seqlen_k=cache_win("vid_max_seqlen_k", lambda: all_len_win.max().item()),
).type_as(vid_q)
# text pooling
vid_out, txt_out = unconcat_win(out)
vid_out = rearrange(vid_out, "l h d -> l (h d)")
txt_out = rearrange(txt_out, "l h d -> l (h d)")
vid_out = window_reverse(vid_out)
vid_out = gather_heads_scatter_seq(vid_out, head_dim=1, seq_dim=0)
txt_out = gather_heads_scatter_seq(txt_out, head_dim=1, seq_dim=0)
vid_out, txt_out = self.proj_out(vid_out, txt_out)
return vid_out, txt_out
class NaMMSRTransformerBlock(MMWindowTransformerBlock):
def __init__(
self,
*,
vid_dim: int,
txt_dim: int,
emb_dim: int,
heads: int,
head_dim: int,
expand_ratio: int,
norm: norm_layer_type,
norm_eps: float,
ada: ada_layer_type,
qk_bias: bool,
qk_rope: bool,
qk_norm: norm_layer_type,
shared_qkv: bool,
shared_mlp: bool,
mlp_type: str,
**kwargs,
):
super().__init__(
vid_dim=vid_dim,
txt_dim=txt_dim,
emb_dim=emb_dim,
heads=heads,
head_dim=head_dim,
expand_ratio=expand_ratio,
norm=norm,
norm_eps=norm_eps,
ada=ada,
qk_bias=qk_bias,
qk_rope=qk_rope,
qk_norm=qk_norm,
shared_qkv=shared_qkv,
shared_mlp=shared_mlp,
mlp_type=mlp_type,
**kwargs,
)
self.attn = NaSwinAttention(
vid_dim=vid_dim,
txt_dim=txt_dim,
heads=heads,
head_dim=head_dim,
qk_bias=qk_bias,
qk_rope=qk_rope,
qk_norm=qk_norm,
qk_norm_eps=norm_eps,
shared_qkv=shared_qkv,
**kwargs,
)
def forward(
self,
vid: torch.FloatTensor, # l c
txt: torch.FloatTensor, # l c
vid_shape: torch.LongTensor, # b 3
txt_shape: torch.LongTensor, # b 1
emb: torch.FloatTensor,
cache: Cache,
) -> Tuple[
torch.FloatTensor,
torch.FloatTensor,
torch.LongTensor,
torch.LongTensor,
]:
hid_len = MMArg(
cache("vid_len", lambda: vid_shape.prod(-1)),
cache("txt_len", lambda: txt_shape.prod(-1)),
)
ada_kwargs = {
"emb": emb,
"hid_len": hid_len,
"cache": cache,
"branch_tag": MMArg("vid", "txt"),
}
vid_attn, txt_attn = self.attn_norm(vid, txt)
vid_attn, txt_attn = self.ada(vid_attn, txt_attn, layer="attn", mode="in", **ada_kwargs)
vid_attn, txt_attn = self.attn(vid_attn, txt_attn, vid_shape, txt_shape, cache)
vid_attn, txt_attn = self.ada(vid_attn, txt_attn, layer="attn", mode="out", **ada_kwargs)
vid_attn, txt_attn = (vid_attn + vid), (txt_attn + txt)
vid_mlp, txt_mlp = self.mlp_norm(vid_attn, txt_attn)
vid_mlp, txt_mlp = self.ada(vid_mlp, txt_mlp, layer="mlp", mode="in", **ada_kwargs)
vid_mlp, txt_mlp = self.mlp(vid_mlp, txt_mlp)
vid_mlp, txt_mlp = self.ada(vid_mlp, txt_mlp, layer="mlp", mode="out", **ada_kwargs)
vid_mlp, txt_mlp = (vid_mlp + vid_attn), (txt_mlp + txt_attn)
return vid_mlp, txt_mlp, vid_shape, txt_shape
+350
View File
@@ -0,0 +1,350 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 dataclasses import dataclass
from typing import Optional, Tuple, Union, Callable
import torch
from torch import nn
from ...common.cache import Cache
from ...common.distributed.ops import slice_inputs
from . import na
from .embedding import TimeEmbedding
from .modulation import get_ada_layer
from .nablocks import get_nablock
from .normalization import get_norm_layer
from .patch import NaPatchIn, NaPatchOut
# Fake func, no checkpointing is required for inference
def gradient_checkpointing(module: Union[Callable, nn.Module], *args, enabled: bool, **kwargs):
return module(*args, **kwargs)
@dataclass
class NaDiTOutput:
vid_sample: torch.Tensor
class NaDiT(nn.Module):
"""
Native Resolution Diffusion Transformer (NaDiT)
"""
gradient_checkpointing = False
def __init__(
self,
vid_in_channels: int,
vid_out_channels: int,
vid_dim: int,
txt_in_dim: Optional[int],
txt_dim: Optional[int],
emb_dim: int,
heads: int,
head_dim: int,
expand_ratio: int,
norm: Optional[str],
norm_eps: float,
ada: str,
qk_bias: bool,
qk_rope: bool,
qk_norm: Optional[str],
patch_size: Union[int, Tuple[int, int, int]],
num_layers: int,
block_type: Union[str, Tuple[str]],
shared_qkv: bool = False,
shared_mlp: bool = False,
mlp_type: str = "normal",
window: Optional[Tuple] = None,
window_method: Optional[Tuple[str]] = None,
temporal_window_size: int = None,
temporal_shifted: bool = False,
**kwargs,
):
ada = get_ada_layer(ada)
norm = get_norm_layer(norm)
qk_norm = get_norm_layer(qk_norm)
if isinstance(block_type, str):
block_type = [block_type] * num_layers
elif len(block_type) != num_layers:
raise ValueError("The ``block_type`` list should equal to ``num_layers``.")
super().__init__()
self.vid_in = NaPatchIn(
in_channels=vid_in_channels,
patch_size=patch_size,
dim=vid_dim,
)
self.txt_in = (
nn.Linear(txt_in_dim, txt_dim)
if txt_in_dim and txt_in_dim != txt_dim
else nn.Identity()
)
self.emb_in = TimeEmbedding(
sinusoidal_dim=256,
hidden_dim=max(vid_dim, txt_dim),
output_dim=emb_dim,
)
if window is None or isinstance(window[0], int):
window = [window] * num_layers
if window_method is None or isinstance(window_method, str):
window_method = [window_method] * num_layers
if temporal_window_size is None or isinstance(temporal_window_size, int):
temporal_window_size = [temporal_window_size] * num_layers
if temporal_shifted is None or isinstance(temporal_shifted, bool):
temporal_shifted = [temporal_shifted] * num_layers
self.blocks = nn.ModuleList(
[
get_nablock(block_type[i])(
vid_dim=vid_dim,
txt_dim=txt_dim,
emb_dim=emb_dim,
heads=heads,
head_dim=head_dim,
expand_ratio=expand_ratio,
norm=norm,
norm_eps=norm_eps,
ada=ada,
qk_bias=qk_bias,
qk_rope=qk_rope,
qk_norm=qk_norm,
shared_qkv=shared_qkv,
shared_mlp=shared_mlp,
mlp_type=mlp_type,
window=window[i],
window_method=window_method[i],
temporal_window_size=temporal_window_size[i],
temporal_shifted=temporal_shifted[i],
**kwargs,
)
for i in range(num_layers)
]
)
self.vid_out = NaPatchOut(
out_channels=vid_out_channels,
patch_size=patch_size,
dim=vid_dim,
)
self.need_txt_repeat = block_type[0] in [
"mmdit_stwin",
"mmdit_stwin_spatial",
"mmdit_stwin_3d_spatial",
]
def set_gradient_checkpointing(self, enable: bool):
self.gradient_checkpointing = enable
def forward(
self,
vid: torch.FloatTensor, # l c
txt: torch.FloatTensor, # l c
vid_shape: torch.LongTensor, # b 3
txt_shape: torch.LongTensor, # b 1
timestep: Union[int, float, torch.IntTensor, torch.FloatTensor], # b
disable_cache: bool = True, # for test
):
# Text input.
if txt_shape.size(-1) == 1 and self.need_txt_repeat:
txt, txt_shape = na.repeat(txt, txt_shape, "l c -> t l c", t=vid_shape[:, 0])
# slice vid after patching in when using sequence parallelism
txt = slice_inputs(txt, dim=0)
txt = self.txt_in(txt)
# Video input.
# Sequence parallel slicing is done inside patching class.
vid, vid_shape = self.vid_in(vid, vid_shape)
# Embedding input.
emb = self.emb_in(timestep, device=vid.device, dtype=vid.dtype)
# Body
cache = Cache(disable=disable_cache)
for i, block in enumerate(self.blocks):
vid, txt, vid_shape, txt_shape = gradient_checkpointing(
enabled=(self.gradient_checkpointing and self.training),
module=block,
vid=vid,
txt=txt,
vid_shape=vid_shape,
txt_shape=txt_shape,
emb=emb,
cache=cache,
)
vid, vid_shape = self.vid_out(vid, vid_shape, cache)
return NaDiTOutput(vid_sample=vid)
class NaDiTUpscaler(nn.Module):
"""
Native Resolution Diffusion Transformer (NaDiT)
"""
gradient_checkpointing = False
def __init__(
self,
vid_in_channels: int,
vid_out_channels: int,
vid_dim: int,
txt_in_dim: Optional[int],
txt_dim: Optional[int],
emb_dim: int,
heads: int,
head_dim: int,
expand_ratio: int,
norm: Optional[str],
norm_eps: float,
ada: str,
qk_bias: bool,
qk_rope: bool,
qk_norm: Optional[str],
patch_size: Union[int, Tuple[int, int, int]],
num_layers: int,
block_type: Union[str, Tuple[str]],
shared_qkv: bool = False,
shared_mlp: bool = False,
mlp_type: str = "normal",
window: Optional[Tuple] = None,
window_method: Optional[Tuple[str]] = None,
temporal_window_size: int = None,
temporal_shifted: bool = False,
**kwargs,
):
ada = get_ada_layer(ada)
norm = get_norm_layer(norm)
qk_norm = get_norm_layer(qk_norm)
if isinstance(block_type, str):
block_type = [block_type] * num_layers
elif len(block_type) != num_layers:
raise ValueError("The ``block_type`` list should equal to ``num_layers``.")
super().__init__()
self.vid_in = NaPatchIn(
in_channels=vid_in_channels,
patch_size=patch_size,
dim=vid_dim,
)
self.txt_in = (
nn.Linear(txt_in_dim, txt_dim)
if txt_in_dim and txt_in_dim != txt_dim
else nn.Identity()
)
self.emb_in = TimeEmbedding(
sinusoidal_dim=256,
hidden_dim=max(vid_dim, txt_dim),
output_dim=emb_dim,
)
self.emb_scale = TimeEmbedding(
sinusoidal_dim=256,
hidden_dim=max(vid_dim, txt_dim),
output_dim=emb_dim,
)
if window is None or isinstance(window[0], int):
window = [window] * num_layers
if window_method is None or isinstance(window_method, str):
window_method = [window_method] * num_layers
if temporal_window_size is None or isinstance(temporal_window_size, int):
temporal_window_size = [temporal_window_size] * num_layers
if temporal_shifted is None or isinstance(temporal_shifted, bool):
temporal_shifted = [temporal_shifted] * num_layers
self.blocks = nn.ModuleList(
[
get_nablock(block_type[i])(
vid_dim=vid_dim,
txt_dim=txt_dim,
emb_dim=emb_dim,
heads=heads,
head_dim=head_dim,
expand_ratio=expand_ratio,
norm=norm,
norm_eps=norm_eps,
ada=ada,
qk_bias=qk_bias,
qk_rope=qk_rope,
qk_norm=qk_norm,
shared_qkv=shared_qkv,
shared_mlp=shared_mlp,
mlp_type=mlp_type,
window=window[i],
window_method=window_method[i],
temporal_window_size=temporal_window_size[i],
temporal_shifted=temporal_shifted[i],
**kwargs,
)
for i in range(num_layers)
]
)
self.vid_out = NaPatchOut(
out_channels=vid_out_channels,
patch_size=patch_size,
dim=vid_dim,
)
self.need_txt_repeat = block_type[0] in [
"mmdit_stwin",
"mmdit_stwin_spatial",
"mmdit_stwin_3d_spatial",
]
def set_gradient_checkpointing(self, enable: bool):
self.gradient_checkpointing = enable
def forward(
self,
vid: torch.FloatTensor, # l c
txt: torch.FloatTensor, # l c
vid_shape: torch.LongTensor, # b 3
txt_shape: torch.LongTensor, # b 1
timestep: Union[int, float, torch.IntTensor, torch.FloatTensor], # b
downscale: Union[int, float, torch.IntTensor, torch.FloatTensor], # b
disable_cache: bool = False, # for test
):
# Text input.
if txt_shape.size(-1) == 1 and self.need_txt_repeat:
txt, txt_shape = na.repeat(txt, txt_shape, "l c -> t l c", t=vid_shape[:, 0])
# slice vid after patching in when using sequence parallelism
txt = slice_inputs(txt, dim=0)
txt = self.txt_in(txt)
# Video input.
# Sequence parallel slicing is done inside patching class.
vid, vid_shape = self.vid_in(vid, vid_shape)
# Embedding input.
emb = self.emb_in(timestep, device=vid.device, dtype=vid.dtype)
emb_scale = self.emb_scale(downscale, device=vid.device, dtype=vid.dtype)
emb = emb + emb_scale
# Body
cache = Cache(disable=disable_cache)
for i, block in enumerate(self.blocks):
vid, txt, vid_shape, txt_shape = gradient_checkpointing(
enabled=(self.gradient_checkpointing and self.training),
module=block,
vid=vid,
txt=txt,
vid_shape=vid_shape,
txt_shape=txt_shape,
emb=emb,
cache=cache,
)
vid, vid_shape = self.vid_out(vid, vid_shape, cache)
return NaDiTOutput(vid_sample=vid)
@@ -0,0 +1,131 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 Callable, Optional
from diffusers.models.normalization import RMSNorm
from torch import nn
import torch
import torch.nn.functional as F
import numbers
from torch.nn.parameter import Parameter
from torch.nn import init
# (dim: int, eps: float, elementwise_affine: bool)
norm_layer_type = Callable[[int, float, bool], nn.Module]
class CustomLayerNorm(nn.Module):
"""
Custom LayerNorm implementation to replace Apex FusedLayerNorm
"""
def __init__(self, normalized_shape, eps=1e-5, elementwise_affine=True):
super(CustomLayerNorm, self).__init__()
if isinstance(normalized_shape, numbers.Integral):
normalized_shape = (normalized_shape,)
self.normalized_shape = torch.Size(normalized_shape)
self.eps = eps
self.elementwise_affine = elementwise_affine
if self.elementwise_affine:
self.weight = Parameter(torch.Tensor(*normalized_shape))
self.bias = Parameter(torch.Tensor(*normalized_shape))
else:
self.register_parameter('weight', None)
self.register_parameter('bias', None)
self.reset_parameters()
def reset_parameters(self):
if self.elementwise_affine:
init.ones_(self.weight)
init.zeros_(self.bias)
def forward(self, input):
return F.layer_norm(
input, self.normalized_shape, self.weight, self.bias, self.eps)
class CustomRMSNorm(nn.Module):
"""
Custom RMSNorm implementation to replace Apex FusedRMSNorm
"""
def __init__(self, normalized_shape, eps=1e-5, elementwise_affine=True):
super(CustomRMSNorm, self).__init__()
if isinstance(normalized_shape, numbers.Integral):
normalized_shape = (normalized_shape,)
self.normalized_shape = torch.Size(normalized_shape)
self.eps = eps
self.elementwise_affine = elementwise_affine
if self.elementwise_affine:
self.weight = Parameter(torch.ones(*normalized_shape))
else:
self.register_parameter('weight', None)
def forward(self, input):
# RMS normalization: x / sqrt(mean(x^2) + eps) * weight
dims = tuple(range(-len(self.normalized_shape), 0))
# Calculate RMS: sqrt(mean(x^2))
variance = input.pow(2).mean(dim=dims, keepdim=True)
rms = torch.sqrt(variance + self.eps)
# Normalize
normalized = input / rms
if self.elementwise_affine:
return normalized * self.weight
return normalized
def get_norm_layer(norm_type: Optional[str]) -> norm_layer_type:
def _norm_layer(dim: int, eps: float, elementwise_affine: bool):
if norm_type is None:
return nn.Identity()
if norm_type == "layer":
return nn.LayerNorm(
normalized_shape=dim,
eps=eps,
elementwise_affine=elementwise_affine,
)
if norm_type == "rms":
return RMSNorm(
dim=dim,
eps=eps,
elementwise_affine=elementwise_affine,
)
if norm_type == "fusedln":
# Use custom LayerNorm instead of Apex FusedLayerNorm
return CustomLayerNorm(
normalized_shape=dim,
elementwise_affine=elementwise_affine,
eps=eps,
)
if norm_type == "fusedrms":
# Use custom RMSNorm instead of Apex FusedRMSNorm
return CustomRMSNorm(
normalized_shape=dim,
elementwise_affine=elementwise_affine,
eps=eps,
)
raise NotImplementedError(f"{norm_type} is not supported")
return _norm_layer
+112
View File
@@ -0,0 +1,112 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 Tuple, Union
import torch
from einops import rearrange
from torch import nn
from torch.nn.modules.utils import _triple
from ...common.cache import Cache
from ...common.distributed.ops import gather_outputs, slice_inputs
from . import na
class PatchIn(nn.Module):
def __init__(
self,
in_channels: int,
patch_size: Union[int, Tuple[int, int, int]],
dim: int,
):
super().__init__()
t, h, w = _triple(patch_size)
self.patch_size = t, h, w
self.proj = nn.Linear(in_channels * t * h * w, dim)
def forward(
self,
vid: torch.Tensor,
) -> torch.Tensor:
t, h, w = self.patch_size
vid = rearrange(vid, "b c (T t) (H h) (W w) -> b T H W (t h w c)", t=t, h=h, w=w)
vid = self.proj(vid)
return vid
class PatchOut(nn.Module):
def __init__(
self,
out_channels: int,
patch_size: Union[int, Tuple[int, int, int]],
dim: int,
):
super().__init__()
t, h, w = _triple(patch_size)
self.patch_size = t, h, w
self.proj = nn.Linear(dim, out_channels * t * h * w)
def forward(
self,
vid: torch.Tensor,
) -> torch.Tensor:
t, h, w = self.patch_size
vid = self.proj(vid)
vid = rearrange(vid, "b T H W (t h w c) -> b c (T t) (H h) (W w)", t=t, h=h, w=w)
return vid
class NaPatchIn(PatchIn):
def forward(
self,
vid: torch.Tensor, # l c
vid_shape: torch.LongTensor,
) -> torch.Tensor:
t, h, w = self.patch_size
if not (t == h == w == 1):
vid, vid_shape = na.rearrange(
vid, vid_shape, "(T t) (H h) (W w) c -> T H W (t h w c)", t=t, h=h, w=w
)
# slice vid after patching in when using sequence parallelism
vid = slice_inputs(vid, dim=0)
vid = self.proj(vid)
return vid, vid_shape
class NaPatchOut(PatchOut):
def forward(
self,
vid: torch.FloatTensor, # l c
vid_shape: torch.LongTensor,
cache: Cache = Cache(disable=True),
) -> Tuple[
torch.FloatTensor,
torch.LongTensor,
]:
t, h, w = self.patch_size
vid = self.proj(vid)
# gather vid before patching out when enabling sequence parallelism
vid = gather_outputs(
vid,
gather_dim=0,
padding_dim=0,
unpad_shape=vid_shape,
cache=cache.namespace("vid"),
)
if not (t == h == w == 1):
vid, vid_shape = na.rearrange(
vid, vid_shape, "T H W (t h w c) -> (T t) (H h) (W w) c", t=t, h=h, w=w
)
return vid, vid_shape
+102
View File
@@ -0,0 +1,102 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 functools import lru_cache
from typing import Tuple
import torch
from einops import rearrange
from rotary_embedding_torch import RotaryEmbedding, apply_rotary_emb
from torch import nn
from ...common.cache import Cache
class RotaryEmbeddingBase(nn.Module):
def __init__(self, dim: int, rope_dim: int):
super().__init__()
self.rope = RotaryEmbedding(
dim=dim // rope_dim,
freqs_for="pixel",
max_freq=256,
)
# 1. Set model.requires_grad_(True) after model creation will make
# the `requires_grad=False` for rope freqs no longer hold.
# 2. Even if we don't set requires_grad_(True) explicitly,
# FSDP is not memory efficient when handling fsdp_wrap
# with mixed requires_grad=True/False.
# With above consideration, it is easier just remove the freqs
# out of nn.Parameters when `learned_freq=False`
freqs = self.rope.freqs
del self.rope.freqs
self.rope.register_buffer("freqs", freqs.data)
@lru_cache(maxsize=128)
def get_axial_freqs(self, *dims):
return self.rope.get_axial_freqs(*dims)
class RotaryEmbedding3d(RotaryEmbeddingBase):
def __init__(self, dim: int):
super().__init__(dim, rope_dim=3)
def forward(
self,
q: torch.FloatTensor, # b h l d
k: torch.FloatTensor, # b h l d
size: Tuple[int, int, int],
) -> Tuple[
torch.FloatTensor,
torch.FloatTensor,
]:
T, H, W = size
freqs = self.get_axial_freqs(T, H, W)
q = rearrange(q, "b h (T H W) d -> b h T H W d", T=T, H=H, W=W)
k = rearrange(k, "b h (T H W) d -> b h T H W d", T=T, H=H, W=W)
q = apply_rotary_emb(freqs, q)
k = apply_rotary_emb(freqs, k)
q = rearrange(q, "b h T H W d -> b h (T H W) d")
k = rearrange(k, "b h T H W d -> b h (T H W) d")
return q, k
class NaRotaryEmbedding3d(RotaryEmbedding3d):
def forward(
self,
q: torch.FloatTensor, # L h d
k: torch.FloatTensor, # L h d
shape: torch.LongTensor,
cache: Cache,
) -> Tuple[
torch.FloatTensor,
torch.FloatTensor,
]:
freqs = cache("rope_freqs_3d", lambda: self.get_freqs(shape))
freqs = freqs.to(device=q.device, dtype=q.dtype)
q = rearrange(q, "L h d -> h L d")
k = rearrange(k, "L h d -> h L d")
q = apply_rotary_emb(freqs, q.float()).to(q.dtype)
k = apply_rotary_emb(freqs, k.float()).to(k.dtype)
q = rearrange(q, "h L d -> L h d")
k = rearrange(k, "h L d -> L h d")
return q, k
def get_freqs(
self,
shape: torch.LongTensor,
) -> torch.Tensor:
freq_list = []
for f, h, w in shape.tolist():
freqs = self.get_axial_freqs(f, h, w)
freq_list.append(freqs.view(-1, freqs.size(-1)))
return torch.cat(freq_list, dim=0)
+83
View File
@@ -0,0 +1,83 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 math import ceil
from typing import Tuple
import math
def get_window_op(name: str):
if name == "720pwin_by_size_bysize":
return make_720Pwindows_bysize
if name == "720pswin_by_size_bysize":
return make_shifted_720Pwindows_bysize
raise ValueError(f"Unknown windowing method: {name}")
# -------------------------------- Windowing -------------------------------- #
def make_720Pwindows_bysize(size: Tuple[int, int, int], num_windows: Tuple[int, int, int]):
t, h, w = size
resized_nt, resized_nh, resized_nw = num_windows
#cal windows under 720p
scale = math.sqrt((45 * 80) / (h * w))
resized_h, resized_w = round(h * scale), round(w * scale)
wh, ww = ceil(resized_h / resized_nh), ceil(resized_w / resized_nw) # window size.
wt = ceil(min(t, 30) / resized_nt) # window size.
nt, nh, nw = ceil(t / wt), ceil(h / wh), ceil(w / ww) # window size.
return [
(
slice(it * wt, min((it + 1) * wt, t)),
slice(ih * wh, min((ih + 1) * wh, h)),
slice(iw * ww, min((iw + 1) * ww, w)),
)
for iw in range(nw)
if min((iw + 1) * ww, w) > iw * ww
for ih in range(nh)
if min((ih + 1) * wh, h) > ih * wh
for it in range(nt)
if min((it + 1) * wt, t) > it * wt
]
def make_shifted_720Pwindows_bysize(size: Tuple[int, int, int], num_windows: Tuple[int, int, int]):
t, h, w = size
resized_nt, resized_nh, resized_nw = num_windows
#cal windows under 720p
scale = math.sqrt((45 * 80) / (h * w))
resized_h, resized_w = round(h * scale), round(w * scale)
wh, ww = ceil(resized_h / resized_nh), ceil(resized_w / resized_nw) # window size.
wt = ceil(min(t, 30) / resized_nt) # window size.
st, sh, sw = ( # shift size.
0.5 if wt < t else 0,
0.5 if wh < h else 0,
0.5 if ww < w else 0,
)
nt, nh, nw = ceil((t - st) / wt), ceil((h - sh) / wh), ceil((w - sw) / ww) # window size.
nt, nh, nw = ( # number of window.
nt + 1 if st > 0 else 1,
nh + 1 if sh > 0 else 1,
nw + 1 if sw > 0 else 1,
)
return [
(
slice(max(int((it - st) * wt), 0), min(int((it - st + 1) * wt), t)),
slice(max(int((ih - sh) * wh), 0), min(int((ih - sh + 1) * wh), h)),
slice(max(int((iw - sw) * ww), 0), min(int((iw - sw + 1) * ww), w)),
)
for iw in range(nw)
if min(int((iw - sw + 1) * ww), w) > max(int((iw - sw) * ww), 0)
for ih in range(nh)
if min(int((ih - sh + 1) * wh), h) > max(int((ih - sh) * wh), 0)
for it in range(nt)
if min(int((it - st + 1) * wt), t) > max(int((it - st) * wt), 0)
]
@@ -0,0 +1,93 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // Licensed under the Apache License, Version 2.0 (the "License");
# // you may not use this file except in compliance with the License.
# // You may obtain a copy of the License at
# //
# // http://www.apache.org/licenses/LICENSE-2.0
# //
# // Unless required by applicable law or agreed to in writing, software
# // distributed under the License is distributed on an "AS IS" BASIS,
# // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# // See the License for the specific language governing permissions and
# // limitations under the License.
import torch
import torch.nn.functional as F
#from flash_attn import flash_attn_varlen_func
from torch import nn
def pytorch_varlen_attention(q, k, v, cu_seqlens_q, cu_seqlens_k, max_seqlen_q, max_seqlen_k, dropout_p=0.0, softmax_scale=None, causal=False, deterministic=False):
"""
A PyTorch-based implementation of variable-length attention to replace flash_attn_varlen_func.
It processes each sequence in the batch individually.
"""
# Create an empty tensor to store the output.
output = torch.empty_like(q)
# Iterate over each sequence in the batch. The batch size is the number of sequences.
for i in range(len(cu_seqlens_q) - 1):
# Determine the start and end indices for the current sequence.
start_q, end_q = cu_seqlens_q[i], cu_seqlens_q[i+1]
start_k, end_k = cu_seqlens_k[i], cu_seqlens_k[i+1]
# Slice the q, k, and v tensors to get the data for the current sequence.
# The shape is (seq_len, heads, head_dim).
q_i = q[start_q:end_q]
k_i = k[start_k:end_k]
v_i = v[start_k:end_k]
# Reshape for torch's scaled_dot_product_attention which expects (batch, heads, seq, dim).
# Here, we treat each sequence as a batch of 1.
q_i = q_i.permute(1, 0, 2).unsqueeze(0) # (1, heads, seq_len_q, head_dim)
k_i = k_i.permute(1, 0, 2).unsqueeze(0) # (1, heads, seq_len_k, head_dim)
v_i = v_i.permute(1, 0, 2).unsqueeze(0) # (1, heads, seq_len_k, head_dim)
# Use PyTorch's built-in scaled dot-product attention.
output_i = F.scaled_dot_product_attention(
q_i, k_i, v_i,
dropout_p=dropout_p if not deterministic else 0.0,
is_causal=causal
)
# Reshape the output back to the original format (seq_len, heads, head_dim)
output_i = output_i.squeeze(0).permute(1, 0, 2)
# Place the result for the current sequence into the main output tensor.
output[start_q:end_q] = output_i
return output
class TorchAttention(nn.Module):
def tflops(self, args, kwargs, output) -> float:
assert len(args) == 0 or len(args) > 2, "query, key should both provided by args / kwargs"
q = kwargs.get("query") or args[0]
k = kwargs.get("key") or args[1]
b, h, sq, d = q.shape
b, h, sk, d = k.shape
return b * h * (4 * d * (sq / 1e6) * (sk / 1e6))
def forward(self, *args, **kwargs):
return F.scaled_dot_product_attention(*args, **kwargs)
class FlashAttentionVarlen(nn.Module):
def tflops(self, args, kwargs, output) -> float:
cu_seqlens_q = kwargs["cu_seqlens_q"]
cu_seqlens_k = kwargs["cu_seqlens_k"]
_, h, d = output.shape
seqlens_q = (cu_seqlens_q[1:] - cu_seqlens_q[:-1]) / 1e6
seqlens_k = (cu_seqlens_k[1:] - cu_seqlens_k[:-1]) / 1e6
return h * (4 * d * (seqlens_q * seqlens_k).sum())
def forward(self, *args, **kwargs):
kwargs["deterministic"] = torch.are_deterministic_algorithms_enabled()
try:
from flash_attn import flash_attn_varlen_func
return flash_attn_varlen_func(*args, **kwargs)
except ImportError:
return pytorch_varlen_attention(*args, **kwargs)
@@ -0,0 +1,62 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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, Union
import torch
from diffusers.models.embeddings import get_timestep_embedding
from torch import nn
def emb_add(emb1: torch.Tensor, emb2: Optional[torch.Tensor]):
return emb1 if emb2 is None else emb1 + emb2
class TimeEmbedding(nn.Module):
def __init__(
self,
sinusoidal_dim: int,
hidden_dim: int,
output_dim: int,
):
super().__init__()
self.sinusoidal_dim = sinusoidal_dim
self.proj_in = nn.Linear(sinusoidal_dim, hidden_dim)
self.proj_hid = nn.Linear(hidden_dim, hidden_dim)
self.proj_out = nn.Linear(hidden_dim, output_dim)
self.act = nn.SiLU()
def forward(
self,
timestep: Union[int, float, torch.IntTensor, torch.FloatTensor],
device: torch.device,
dtype: torch.dtype,
) -> torch.FloatTensor:
if not torch.is_tensor(timestep):
timestep = torch.tensor([timestep], device=device, dtype=dtype)
if timestep.ndim == 0:
timestep = timestep[None]
emb = get_timestep_embedding(
timesteps=timestep,
embedding_dim=self.sinusoidal_dim,
flip_sin_to_cos=False,
downscale_freq_shift=0,
)
emb = emb.to(dtype)
emb = self.proj_in(emb)
emb = self.act(emb)
emb = self.proj_hid(emb)
emb = self.act(emb)
emb = self.proj_out(emb)
return emb
+62
View File
@@ -0,0 +1,62 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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
import torch
import torch.nn.functional as F
from torch import nn
def get_mlp(mlp_type: Optional[str] = "normal"):
if mlp_type == "normal":
return MLP
elif mlp_type == "swiglu":
return SwiGLUMLP
class MLP(nn.Module):
def __init__(
self,
dim: int,
expand_ratio: int,
):
super().__init__()
self.proj_in = nn.Linear(dim, dim * expand_ratio)
self.act = nn.GELU("tanh")
self.proj_out = nn.Linear(dim * expand_ratio, dim)
def forward(self, x: torch.FloatTensor) -> torch.FloatTensor:
x = self.proj_in(x)
x = self.act(x)
x = self.proj_out(x)
return x
class SwiGLUMLP(nn.Module):
def __init__(
self,
dim: int,
expand_ratio: int,
multiple_of: int = 256,
):
super().__init__()
hidden_dim = int(2 * dim * expand_ratio / 3)
hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)
self.proj_in_gate = nn.Linear(dim, hidden_dim, bias=False)
self.proj_out = nn.Linear(hidden_dim, dim, bias=False)
self.proj_in = nn.Linear(dim, hidden_dim, bias=False)
def forward(self, x: torch.FloatTensor) -> torch.FloatTensor:
x = self.proj_out(F.silu(self.proj_in_gate(x)) * self.proj_in(x))
return x
+74
View File
@@ -0,0 +1,74 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 dataclasses import dataclass
from typing import Any, Callable, Dict, List, Tuple
import torch
from torch import nn
@dataclass
class MMArg:
vid: Any
txt: Any
def get_args(key: str, args: List[Any]) -> List[Any]:
return [getattr(v, key) if isinstance(v, MMArg) else v for v in args]
def get_kwargs(key: str, kwargs: Dict[str, Any]) -> Dict[str, Any]:
return {k: getattr(v, key) if isinstance(v, MMArg) else v for k, v in kwargs.items()}
class MMModule(nn.Module):
def __init__(
self,
module: Callable[..., nn.Module],
*args,
shared_weights: bool = False,
vid_only: bool = False,
**kwargs,
):
super().__init__()
self.shared_weights = shared_weights
self.vid_only = vid_only
if self.shared_weights:
assert get_args("vid", args) == get_args("txt", args)
assert get_kwargs("vid", kwargs) == get_kwargs("txt", kwargs)
self.all = module(*get_args("vid", args), **get_kwargs("vid", kwargs))
else:
self.vid = module(*get_args("vid", args), **get_kwargs("vid", kwargs))
self.txt = (
module(*get_args("txt", args), **get_kwargs("txt", kwargs))
if not vid_only
else None
)
def forward(
self,
vid: torch.FloatTensor,
txt: torch.FloatTensor,
*args,
**kwargs,
) -> Tuple[
torch.FloatTensor,
torch.FloatTensor,
]:
vid_module = self.vid if not self.shared_weights else self.all
vid = vid_module(vid, *get_args("vid", args), **get_kwargs("vid", kwargs))
if not self.vid_only:
txt_module = self.txt if not self.shared_weights else self.all
txt = txt_module(txt, *get_args("txt", args), **get_kwargs("txt", kwargs))
return vid, txt
@@ -0,0 +1,118 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 Callable, List, Optional
import torch
from einops import rearrange
from torch import nn
from ...common.cache import Cache
from ...common.distributed.ops import slice_inputs
# (dim: int, emb_dim: int)
ada_layer_type = Callable[[int, int], nn.Module]
def get_ada_layer(ada_layer: str) -> ada_layer_type:
if ada_layer == "single":
return AdaSingle
raise NotImplementedError(f"{ada_layer} is not supported")
def expand_dims(x: torch.Tensor, dim: int, ndim: int):
"""
Expand tensor "x" to "ndim" by adding empty dims at "dim".
Example: x is (b d), target ndim is 5, add dim at 1, return (b 1 1 1 d).
"""
shape = x.shape
shape = shape[:dim] + (1,) * (ndim - len(shape)) + shape[dim:]
return x.reshape(shape)
class AdaSingle(nn.Module):
def __init__(
self,
dim: int,
emb_dim: int,
layers: List[str],
modes: List[str] = ["in", "out"],
):
assert emb_dim == 6 * dim, "AdaSingle requires emb_dim == 6 * dim"
super().__init__()
self.dim = dim
self.emb_dim = emb_dim
self.layers = layers
for l in layers:
if "in" in modes:
self.register_parameter(f"{l}_shift", nn.Parameter(torch.randn(dim) / dim**0.5))
self.register_parameter(
f"{l}_scale", nn.Parameter(torch.randn(dim) / dim**0.5 + 1)
)
if "out" in modes:
self.register_parameter(f"{l}_gate", nn.Parameter(torch.randn(dim) / dim**0.5))
def forward(
self,
hid: torch.FloatTensor, # b ... c
emb: torch.FloatTensor, # b d
layer: str,
mode: str,
cache: Cache = Cache(disable=True),
branch_tag: str = "",
hid_len: Optional[torch.LongTensor] = None, # b
) -> torch.FloatTensor:
idx = self.layers.index(layer)
emb = rearrange(emb, "b (d l g) -> b d l g", l=len(self.layers), g=3)[..., idx, :]
emb = expand_dims(emb, 1, hid.ndim + 1)
if hid_len is not None:
emb = cache(
f"emb_repeat_{idx}_{branch_tag}",
lambda: slice_inputs(
torch.cat([e.repeat(l, *([1] * e.ndim)) for e, l in zip(emb, hid_len)]),
dim=0,
),
)
shiftA, scaleA, gateA = emb.unbind(-1)
shiftB, scaleB, gateB = (
getattr(self, f"{layer}_shift", None),
getattr(self, f"{layer}_scale", None),
getattr(self, f"{layer}_gate", None),
)
# 🚀 FP8 COMPATIBILITY: Convert parameters to match embedding dtype
# This prevents "Promotion for Float8 Types is not supported" errors
target_dtype = shiftA.dtype
if mode == "in":
# Convert parameters to match embedding dtype for FP8 compatibility
if scaleB is not None and scaleB.dtype != target_dtype:
scaleB = scaleB.to(target_dtype)
if shiftB is not None and shiftB.dtype != target_dtype:
shiftB = shiftB.to(target_dtype)
return hid.mul_(scaleA + scaleB).add_(shiftA + shiftB)
if mode == "out":
# Convert gate parameter to match embedding dtype for FP8 compatibility
if gateB is not None and gateB.dtype != target_dtype:
gateB = gateB.to(target_dtype)
return hid.mul_(gateA + gateB)
raise NotImplementedError
def extra_repr(self) -> str:
return f"dim={self.dim}, emb_dim={self.emb_dim}, layers={self.layers}"
+241
View File
@@ -0,0 +1,241 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 itertools import chain
from typing import Callable, Dict, List, Tuple
import einops
import torch
def flatten(
hid: List[torch.FloatTensor], # List of (*** c)
) -> Tuple[
torch.FloatTensor, # (L c)
torch.LongTensor, # (b n)
]:
assert len(hid) > 0
shape = torch.stack([torch.tensor(x.shape[:-1], device=hid[0].device) for x in hid])
hid = torch.cat([x.flatten(0, -2) for x in hid])
return hid, shape
def unflatten(
hid: torch.FloatTensor, # (L c) or (L ... c)
hid_shape: torch.LongTensor, # (b n)
) -> List[torch.Tensor]: # List of (*** c) or (*** ... c)
hid_len = hid_shape.prod(-1)
hid = hid.split(hid_len.tolist())
hid = [x.unflatten(0, s.tolist()) for x, s in zip(hid, hid_shape)]
return hid
def concat(
vid: torch.FloatTensor, # (VL ... c)
txt: torch.FloatTensor, # (TL ... c)
vid_len: torch.LongTensor, # (b)
txt_len: torch.LongTensor, # (b)
) -> torch.FloatTensor: # (L ... c)
vid = torch.split(vid, vid_len.tolist())
txt = torch.split(txt, txt_len.tolist())
return torch.cat(list(chain(*zip(vid, txt))))
def concat_idx(
vid_len: torch.LongTensor, # (b)
txt_len: torch.LongTensor, # (b)
) -> Tuple[
Callable,
Callable,
]:
device = vid_len.device
vid_idx = torch.arange(vid_len.sum(), device=device)
txt_idx = torch.arange(len(vid_idx), len(vid_idx) + txt_len.sum(), device=device)
tgt_idx = concat(vid_idx, txt_idx, vid_len, txt_len)
src_idx = torch.argsort(tgt_idx)
return (
lambda vid, txt: torch.index_select(torch.cat([vid, txt]), 0, tgt_idx),
lambda all: torch.index_select(all, 0, src_idx).split([len(vid_idx), len(txt_idx)]),
)
def unconcat(
all: torch.FloatTensor, # (L ... c)
vid_len: torch.LongTensor, # (b)
txt_len: torch.LongTensor, # (b)
) -> Tuple[
torch.FloatTensor, # (VL ... c)
torch.FloatTensor, # (TL ... c)
]:
interleave_len = list(chain(*zip(vid_len.tolist(), txt_len.tolist())))
all = all.split(interleave_len)
vid = torch.cat(all[0::2])
txt = torch.cat(all[1::2])
return vid, txt
def repeat_concat(
vid: torch.FloatTensor, # (VL ... c)
txt: torch.FloatTensor, # (TL ... c)
vid_len: torch.LongTensor, # (n*b)
txt_len: torch.LongTensor, # (b)
txt_repeat: List, # (n)
) -> torch.FloatTensor: # (L ... c)
vid = torch.split(vid, vid_len.tolist())
txt = torch.split(txt, txt_len.tolist())
txt = [[x] * n for x, n in zip(txt, txt_repeat)]
txt = list(chain(*txt))
return torch.cat(list(chain(*zip(vid, txt))))
def repeat_concat_idx(
vid_len: torch.LongTensor, # (n*b)
txt_len: torch.LongTensor, # (b)
txt_repeat: torch.LongTensor, # (n)
) -> Tuple[
Callable,
Callable,
]:
device = vid_len.device
vid_idx = torch.arange(vid_len.sum(), device=device)
txt_idx = torch.arange(len(vid_idx), len(vid_idx) + txt_len.sum(), device=device)
txt_repeat_list = txt_repeat.tolist()
tgt_idx = repeat_concat(vid_idx, txt_idx, vid_len, txt_len, txt_repeat)
src_idx = torch.argsort(tgt_idx)
txt_idx_len = len(tgt_idx) - len(vid_idx)
repeat_txt_len = (txt_len * txt_repeat).tolist()
def unconcat_coalesce(all):
"""
Un-concat vid & txt, and coalesce the repeated txt.
e.g. vid [0 1 2 3 4 5 6 7 8] -> 3 splits -> [0 1 2] [3 4 5] [6 7 8]
txt [9 10]
repeat_concat ==> [0 1 2 9 10 3 4 5 9 10 6 7 8 9 10]
1. argsort re-index ==> [0 1 2 3 4 5 6 7 8 9 9 9 10 10 10]
split ==> vid_out [0 1 2 3 4 5 6 7 8] txt_out [9 9 9 10 10 10]
2. reshape & mean for each sample to coalesce the repeated txt.
"""
vid_out, txt_out = all[src_idx].split([len(vid_idx), txt_idx_len])
txt_out_coalesced = []
for txt, repeat_time in zip(txt_out.split(repeat_txt_len), txt_repeat_list):
txt = txt.reshape(-1, repeat_time, *txt.shape[1:]).mean(1)
txt_out_coalesced.append(txt)
return vid_out, torch.cat(txt_out_coalesced)
# Note: Backward of torch.index_select is non-deterministic when existing repeated index,
# the difference may cumulative like torch.repeat_interleave, so we use vanilla index here.
return (
lambda vid, txt: torch.cat([vid, txt])[tgt_idx],
lambda all: unconcat_coalesce(all),
)
def rearrange(
hid: torch.FloatTensor, # (L c)
hid_shape: torch.LongTensor, # (b n)
pattern: str,
**kwargs: Dict[str, int],
) -> Tuple[
torch.FloatTensor,
torch.LongTensor,
]:
return flatten([einops.rearrange(h, pattern, **kwargs) for h in unflatten(hid, hid_shape)])
def rearrange_idx(
hid_shape: torch.LongTensor, # (b n)
pattern: str,
**kwargs: Dict[str, int],
) -> Tuple[Callable, Callable, torch.LongTensor]:
hid_idx = torch.arange(hid_shape.prod(-1).sum(), device=hid_shape.device).unsqueeze(-1)
tgt_idx, tgt_shape = rearrange(hid_idx, hid_shape, pattern, **kwargs)
tgt_idx = tgt_idx.squeeze(-1)
src_idx = torch.argsort(tgt_idx)
return (
lambda hid: torch.index_select(hid, 0, tgt_idx),
lambda hid: torch.index_select(hid, 0, src_idx),
tgt_shape,
)
def repeat(
hid: torch.FloatTensor, # (L c)
hid_shape: torch.LongTensor, # (b n)
pattern: str,
**kwargs: Dict[str, torch.LongTensor], # (b)
) -> Tuple[
torch.FloatTensor,
torch.LongTensor,
]:
hid = unflatten(hid, hid_shape)
kwargs = [{k: v[i].item() for k, v in kwargs.items()} for i in range(len(hid))]
return flatten([einops.repeat(h, pattern, **a) for h, a in zip(hid, kwargs)])
def pack(
samples: List[torch.Tensor], # List of (h w c).
) -> Tuple[
List[torch.Tensor], # groups [(b1 h1 w1 c1), (b2 h2 w2 c2)]
List[List[int]], # reversal indices.
]:
batches = {}
indices = {}
for i, sample in enumerate(samples):
shape = sample.shape
batches[shape] = batches.get(shape, [])
indices[shape] = indices.get(shape, [])
batches[shape].append(sample)
indices[shape].append(i)
batches = list(map(torch.stack, batches.values()))
indices = list(indices.values())
return batches, indices
def unpack(
batches: List[torch.Tensor],
indices: List[List[int]],
) -> List[torch.Tensor]:
samples = [None] * (max(chain(*indices)) + 1)
for batch, index in zip(batches, indices):
for sample, i in zip(batch.unbind(), index):
samples[i] = sample
return samples
def window(
hid: torch.FloatTensor, # (L c)
hid_shape: torch.LongTensor, # (b n)
window_fn: Callable[[torch.Tensor], List[torch.Tensor]],
):
hid = unflatten(hid, hid_shape)
hid = list(map(window_fn, hid))
hid_windows = torch.tensor(list(map(len, hid)), device=hid_shape.device)
hid, hid_shape = flatten(list(chain(*hid)))
return hid, hid_shape, hid_windows
def window_idx(
hid_shape: torch.LongTensor, # (b n)
window_fn: Callable[[torch.Tensor], List[torch.Tensor]],
):
hid_idx = torch.arange(hid_shape.prod(-1).sum(), device=hid_shape.device).unsqueeze(-1)
tgt_idx, tgt_shape, tgt_windows = window(hid_idx, hid_shape, window_fn)
tgt_idx = tgt_idx.squeeze(-1)
src_idx = torch.argsort(tgt_idx)
return (
lambda hid: torch.index_select(hid, 0, tgt_idx),
lambda hid: torch.index_select(hid, 0, src_idx),
tgt_shape,
tgt_windows,
)
@@ -0,0 +1,26 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 .mmsr_block import NaMMSRTransformerBlock
nadit_blocks = {
"mmdit_sr": NaMMSRTransformerBlock,
}
def get_nablock(block_type: str):
if block_type in nadit_blocks:
return nadit_blocks[block_type]
raise NotImplementedError(f"{block_type} is not supported")
@@ -0,0 +1,25 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 .mmattn import NaMMAttention
attns = {
"mm_full": NaMMAttention,
}
def get_attn(attn_type: str):
if attn_type in attns:
return attns[attn_type]
raise NotImplementedError(f"{attn_type} is not supported")
@@ -0,0 +1,267 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 torch
from einops import rearrange
from torch import nn
from torch.nn import functional as F
from torch.nn.modules.utils import _triple
from .....common.cache import Cache
from .....common.distributed.ops import gather_heads_scatter_seq, gather_seq_scatter_heads_qkv
from .....common.half_precision_fixes import safe_pad_operation
from ... import na
from ...attention import FlashAttentionVarlen
from ...mm import MMArg, MMModule
from ...normalization import norm_layer_type
from ...rope import get_na_rope
from ...window import get_window_op
from itertools import chain
class NaMMAttention(nn.Module):
def __init__(
self,
vid_dim: int,
txt_dim: int,
heads: int,
head_dim: int,
qk_bias: bool,
qk_norm: norm_layer_type,
qk_norm_eps: float,
rope_type: Optional[str],
rope_dim: int,
shared_weights: bool,
**kwargs,
):
super().__init__()
dim = MMArg(vid_dim, txt_dim)
inner_dim = heads * head_dim
qkv_dim = inner_dim * 3
self.head_dim = head_dim
self.proj_qkv = MMModule(
nn.Linear, dim, qkv_dim, bias=qk_bias, shared_weights=shared_weights
)
self.proj_out = MMModule(nn.Linear, inner_dim, dim, shared_weights=shared_weights)
self.norm_q = MMModule(
qk_norm,
dim=head_dim,
eps=qk_norm_eps,
elementwise_affine=True,
shared_weights=shared_weights,
)
self.norm_k = MMModule(
qk_norm,
dim=head_dim,
eps=qk_norm_eps,
elementwise_affine=True,
shared_weights=shared_weights,
)
self.rope = get_na_rope(rope_type=rope_type, dim=rope_dim)
self.attn = FlashAttentionVarlen()
def forward(
self,
vid: torch.FloatTensor, # l c
txt: torch.FloatTensor, # l c
vid_shape: torch.LongTensor, # b 3
txt_shape: torch.LongTensor, # b 1
cache: Cache,
) -> Tuple[
torch.FloatTensor,
torch.FloatTensor,
]:
vid_qkv, txt_qkv = self.proj_qkv(vid, txt)
vid_qkv = gather_seq_scatter_heads_qkv(
vid_qkv,
seq_dim=0,
qkv_shape=vid_shape,
cache=cache.namespace("vid"),
)
txt_qkv = gather_seq_scatter_heads_qkv(
txt_qkv,
seq_dim=0,
qkv_shape=txt_shape,
cache=cache.namespace("txt"),
)
vid_qkv = rearrange(vid_qkv, "l (o h d) -> l o h d", o=3, d=self.head_dim)
txt_qkv = rearrange(txt_qkv, "l (o h d) -> l o h d", o=3, d=self.head_dim)
vid_q, vid_k, vid_v = vid_qkv.unbind(1)
txt_q, txt_k, txt_v = txt_qkv.unbind(1)
vid_q, txt_q = self.norm_q(vid_q, txt_q)
vid_k, txt_k = self.norm_k(vid_k, txt_k)
if self.rope:
if self.rope.mm:
vid_q, vid_k, txt_q, txt_k = self.rope(
vid_q, vid_k, vid_shape, txt_q, txt_k, txt_shape, cache
)
else:
vid_q, vid_k = self.rope(vid_q, vid_k, vid_shape, cache)
vid_len = cache("vid_len", lambda: vid_shape.prod(-1))
txt_len = cache("txt_len", lambda: txt_shape.prod(-1))
all_len = cache("all_len", lambda: vid_len + txt_len)
concat, unconcat = cache("mm_pnp", lambda: na.concat_idx(vid_len, txt_len))
attn = self.attn(
q=concat(vid_q, txt_q).bfloat16(),
k=concat(vid_k, txt_k).bfloat16(),
v=concat(vid_v, txt_v).bfloat16(),
cu_seqlens_q=cache("mm_seqlens", lambda: safe_pad_operation(all_len.cumsum(0), (1, 0)).int()),
cu_seqlens_k=cache("mm_seqlens", lambda: safe_pad_operation(all_len.cumsum(0), (1, 0)).int()),
max_seqlen_q=cache("mm_maxlen", lambda: all_len.max().item()),
max_seqlen_k=cache("mm_maxlen", lambda: all_len.max().item()),
).type_as(vid_q)
attn = rearrange(attn, "l h d -> l (h d)")
vid_out, txt_out = unconcat(attn)
vid_out = gather_heads_scatter_seq(vid_out, head_dim=1, seq_dim=0)
txt_out = gather_heads_scatter_seq(txt_out, head_dim=1, seq_dim=0)
vid_out, txt_out = self.proj_out(vid_out, txt_out)
return vid_out, txt_out
class NaSwinAttention(NaMMAttention):
def __init__(
self,
*args,
window: Union[int, Tuple[int, int, int]],
window_method: str,
**kwargs,
):
super().__init__(*args, **kwargs)
self.window = _triple(window)
self.window_method = window_method
assert all(map(lambda v: isinstance(v, int) and v >= 0, self.window))
self.window_op = get_window_op(window_method)
def forward(
self,
vid: torch.FloatTensor, # l c
txt: torch.FloatTensor, # l c
vid_shape: torch.LongTensor, # b 3
txt_shape: torch.LongTensor, # b 1
cache: Cache,
) -> Tuple[
torch.FloatTensor,
torch.FloatTensor,
]:
vid_qkv, txt_qkv = self.proj_qkv(vid, txt)
vid_qkv = gather_seq_scatter_heads_qkv(
vid_qkv,
seq_dim=0,
qkv_shape=vid_shape,
cache=cache.namespace("vid"),
)
txt_qkv = gather_seq_scatter_heads_qkv(
txt_qkv,
seq_dim=0,
qkv_shape=txt_shape,
cache=cache.namespace("txt"),
)
# re-org the input seq for window attn
cache_win = cache.namespace(f"{self.window_method}_{self.window}_sd3")
def make_window(x: torch.Tensor):
t, h, w, _ = x.shape
window_slices = self.window_op((t, h, w), self.window)
return [x[st, sh, sw] for (st, sh, sw) in window_slices]
window_partition, window_reverse, window_shape, window_count = cache_win(
"win_transform",
lambda: na.window_idx(vid_shape, make_window),
)
vid_qkv_win = window_partition(vid_qkv)
vid_qkv_win = rearrange(vid_qkv_win, "l (o h d) -> l o h d", o=3, d=self.head_dim)
txt_qkv = rearrange(txt_qkv, "l (o h d) -> l o h d", o=3, d=self.head_dim)
vid_q, vid_k, vid_v = vid_qkv_win.unbind(1)
txt_q, txt_k, txt_v = txt_qkv.unbind(1)
vid_q, txt_q = self.norm_q(vid_q, txt_q)
vid_k, txt_k = self.norm_k(vid_k, txt_k)
txt_len = cache("txt_len", lambda: txt_shape.prod(-1))
vid_len_win = cache_win("vid_len", lambda: window_shape.prod(-1))
txt_len_win = cache_win("txt_len", lambda: txt_len.repeat_interleave(window_count))
all_len_win = cache_win("all_len", lambda: vid_len_win + txt_len_win)
concat_win, unconcat_win = cache_win(
"mm_pnp", lambda: na.repeat_concat_idx(vid_len_win, txt_len, window_count)
)
# window rope
if self.rope:
if self.rope.mm:
# repeat text q and k for window mmrope
_, num_h, _ = txt_q.shape
txt_q_repeat = rearrange(txt_q, "l h d -> l (h d)")
txt_q_repeat = na.unflatten(txt_q_repeat, txt_shape)
txt_q_repeat = [[x] * n for x, n in zip(txt_q_repeat, window_count)]
txt_q_repeat = list(chain(*txt_q_repeat))
txt_q_repeat, txt_shape_repeat = na.flatten(txt_q_repeat)
txt_q_repeat = rearrange(txt_q_repeat, "l (h d) -> l h d", h=num_h)
txt_k_repeat = rearrange(txt_k, "l h d -> l (h d)")
txt_k_repeat = na.unflatten(txt_k_repeat, txt_shape)
txt_k_repeat = [[x] * n for x, n in zip(txt_k_repeat, window_count)]
txt_k_repeat = list(chain(*txt_k_repeat))
txt_k_repeat, _ = na.flatten(txt_k_repeat)
txt_k_repeat = rearrange(txt_k_repeat, "l (h d) -> l h d", h=num_h)
vid_q, vid_k, txt_q, txt_k = self.rope(
vid_q, vid_k, window_shape, txt_q_repeat, txt_k_repeat, txt_shape_repeat, cache_win
)
else:
vid_q, vid_k = self.rope(vid_q, vid_k, window_shape, cache_win)
out = self.attn(
q=concat_win(vid_q, txt_q).bfloat16(),
k=concat_win(vid_k, txt_k).bfloat16(),
v=concat_win(vid_v, txt_v).bfloat16(),
cu_seqlens_q=cache_win(
"vid_seqlens_q", lambda: safe_pad_operation(all_len_win.cumsum(0), (1, 0)).int()
),
cu_seqlens_k=cache_win(
"vid_seqlens_k", lambda: safe_pad_operation(all_len_win.cumsum(0), (1, 0)).int()
),
max_seqlen_q=cache_win("vid_max_seqlen_q", lambda: all_len_win.max().item()),
max_seqlen_k=cache_win("vid_max_seqlen_k", lambda: all_len_win.max().item()),
).type_as(vid_q)
# text pooling
vid_out, txt_out = unconcat_win(out)
vid_out = rearrange(vid_out, "l h d -> l (h d)")
txt_out = rearrange(txt_out, "l h d -> l (h d)")
vid_out = window_reverse(vid_out)
vid_out = gather_heads_scatter_seq(vid_out, head_dim=1, seq_dim=0)
txt_out = gather_heads_scatter_seq(txt_out, head_dim=1, seq_dim=0)
vid_out, txt_out = self.proj_out(vid_out, txt_out)
return vid_out, txt_out
@@ -0,0 +1,126 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 Tuple
import torch
import torch.nn as nn
# from ..cache import Cache
from ....common.cache import Cache
from .attention.mmattn import NaSwinAttention
from ..mm import MMArg
from ..modulation import ada_layer_type
from ..normalization import norm_layer_type
from ..mm import MMModule
from ..mlp import get_mlp
class NaMMSRTransformerBlock(nn.Module):
def __init__(
self,
*,
vid_dim: int,
txt_dim: int,
emb_dim: int,
heads: int,
head_dim: int,
expand_ratio: int,
norm: norm_layer_type,
norm_eps: float,
ada: ada_layer_type,
qk_bias: bool,
qk_norm: norm_layer_type,
mlp_type: str,
shared_weights: bool,
rope_type: str,
rope_dim: int,
is_last_layer: bool,
**kwargs,
):
super().__init__()
dim = MMArg(vid_dim, txt_dim)
self.attn_norm = MMModule(norm, dim=dim, eps=norm_eps, elementwise_affine=False, shared_weights=shared_weights,)
self.attn = NaSwinAttention(
vid_dim=vid_dim,
txt_dim=txt_dim,
heads=heads,
head_dim=head_dim,
qk_bias=qk_bias,
qk_norm=qk_norm,
qk_norm_eps=norm_eps,
rope_type=rope_type,
rope_dim=rope_dim,
shared_weights=shared_weights,
window=kwargs.pop("window", None),
window_method=kwargs.pop("window_method", None),
)
self.mlp_norm = MMModule(norm, dim=dim, eps=norm_eps, elementwise_affine=False, shared_weights=shared_weights, vid_only=is_last_layer)
self.mlp = MMModule(
get_mlp(mlp_type),
dim=dim,
expand_ratio=expand_ratio,
shared_weights=shared_weights,
vid_only=is_last_layer
)
self.ada = MMModule(ada, dim=dim, emb_dim=emb_dim, layers=["attn", "mlp"], shared_weights=shared_weights, vid_only=is_last_layer)
self.is_last_layer = is_last_layer
def forward(
self,
vid: torch.FloatTensor, # l c
txt: torch.FloatTensor, # l c
vid_shape: torch.LongTensor, # b 3
txt_shape: torch.LongTensor, # b 1
emb: torch.FloatTensor,
cache: Cache,
) -> Tuple[
torch.FloatTensor,
torch.FloatTensor,
torch.LongTensor,
torch.LongTensor,
]:
hid_len = MMArg(
cache("vid_len", lambda: vid_shape.prod(-1)),
cache("txt_len", lambda: txt_shape.prod(-1)),
)
ada_kwargs = {
"emb": emb,
"hid_len": hid_len,
"cache": cache,
"branch_tag": MMArg("vid", "txt"),
}
vid_attn, txt_attn = self.attn_norm(vid, txt)
vid_attn, txt_attn = self.ada(vid_attn, txt_attn, layer="attn", mode="in", **ada_kwargs)
vid_attn, txt_attn = self.attn(vid_attn, txt_attn, vid_shape, txt_shape, cache)
vid_attn, txt_attn = self.ada(vid_attn, txt_attn, layer="attn", mode="out", **ada_kwargs)
vid_attn, txt_attn = (vid_attn + vid), (txt_attn + txt)
vid_mlp, txt_mlp = self.mlp_norm(vid_attn, txt_attn)
# ADD BY NUMZ
if vid_mlp.dtype != vid_attn.dtype:
vid_mlp = vid_mlp.to(vid_attn.dtype)
if txt_mlp.dtype != txt_attn.dtype:
txt_mlp = txt_mlp.to(txt_attn.dtype)
# END BY NUMZ
vid_mlp, txt_mlp = self.ada(vid_mlp, txt_mlp, layer="mlp", mode="in", **ada_kwargs)
vid_mlp, txt_mlp = self.mlp(vid_mlp, txt_mlp)
vid_mlp, txt_mlp = self.ada(vid_mlp, txt_mlp, layer="mlp", mode="out", **ada_kwargs)
vid_mlp, txt_mlp = (vid_mlp + vid_attn), (txt_mlp + txt_attn)
return vid_mlp, txt_mlp, vid_shape, txt_shape
+246
View File
@@ -0,0 +1,246 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 dataclasses import dataclass
from typing import List, Optional, Tuple, Union, Callable
import torch
from torch import nn
from ...common.cache import Cache
from ...common.distributed.ops import slice_inputs
from . import na
from .embedding import TimeEmbedding
from .modulation import get_ada_layer
from .nablocks import get_nablock
from .normalization import get_norm_layer
from .patch import get_na_patch_layers
# Fake func, no checkpointing is required for inference
def gradient_checkpointing(module: Union[Callable, nn.Module], *args, enabled: bool, **kwargs):
return module(*args, **kwargs)
@dataclass
class NaDiTOutput:
vid_sample: torch.Tensor
class NaDiT(nn.Module):
"""
Native Resolution Diffusion Transformer (NaDiT)
"""
gradient_checkpointing = False
def __init__(
self,
vid_in_channels: int,
vid_out_channels: int,
vid_dim: int,
txt_in_dim: Union[int, List[int]],
txt_dim: Optional[int],
emb_dim: int,
heads: int,
head_dim: int,
expand_ratio: int,
norm: Optional[str],
norm_eps: float,
ada: str,
qk_bias: bool,
qk_norm: Optional[str],
patch_size: Union[int, Tuple[int, int, int]],
num_layers: int,
block_type: Union[str, Tuple[str]],
mm_layers: Union[int, Tuple[bool]],
mlp_type: str = "normal",
patch_type: str = "v1",
rope_type: Optional[str] = "rope3d",
rope_dim: Optional[int] = None,
window: Optional[Tuple] = None,
window_method: Optional[Tuple[str]] = None,
msa_type: Optional[Tuple[str]] = None,
mca_type: Optional[Tuple[str]] = None,
txt_in_norm: Optional[str] = None,
txt_in_norm_scale_factor: int = 0.01,
txt_proj_type: Optional[str] = "linear",
vid_out_norm: Optional[str] = None,
**kwargs,
):
ada = get_ada_layer(ada)
norm = get_norm_layer(norm)
qk_norm = get_norm_layer(qk_norm)
rope_dim = rope_dim if rope_dim is not None else head_dim // 2
if isinstance(block_type, str):
block_type = [block_type] * num_layers
elif len(block_type) != num_layers:
raise ValueError("The ``block_type`` list should equal to ``num_layers``.")
super().__init__()
NaPatchIn, NaPatchOut = get_na_patch_layers(patch_type)
self.vid_in = NaPatchIn(
in_channels=vid_in_channels,
patch_size=patch_size,
dim=vid_dim,
)
if not isinstance(txt_in_dim, int):
self.txt_in = nn.ModuleList([])
for in_dim in txt_in_dim:
txt_norm_layer = get_norm_layer(txt_in_norm)(txt_dim, norm_eps, True)
if txt_proj_type == "linear":
txt_proj_layer = nn.Linear(in_dim, txt_dim)
else:
txt_proj_layer = nn.Sequential(
nn.Linear(in_dim, in_dim), nn.GELU("tanh"), nn.Linear(in_dim, txt_dim)
)
torch.nn.init.constant_(txt_norm_layer.weight, txt_in_norm_scale_factor)
self.txt_in.append(
nn.Sequential(
txt_proj_layer,
txt_norm_layer,
)
)
else:
self.txt_in = (
nn.Linear(txt_in_dim, txt_dim)
if txt_in_dim and txt_in_dim != txt_dim
else nn.Identity()
)
self.emb_in = TimeEmbedding(
sinusoidal_dim=256,
hidden_dim=max(vid_dim, txt_dim),
output_dim=emb_dim,
)
if window is None or isinstance(window[0], int):
window = [window] * num_layers
if window_method is None or isinstance(window_method, str):
window_method = [window_method] * num_layers
if msa_type is None or isinstance(msa_type, str):
msa_type = [msa_type] * num_layers
if mca_type is None or isinstance(mca_type, str):
mca_type = [mca_type] * num_layers
self.blocks = nn.ModuleList(
[
get_nablock(block_type[i])(
vid_dim=vid_dim,
txt_dim=txt_dim,
emb_dim=emb_dim,
heads=heads,
head_dim=head_dim,
expand_ratio=expand_ratio,
norm=norm,
norm_eps=norm_eps,
ada=ada,
qk_bias=qk_bias,
qk_norm=qk_norm,
shared_weights=not (
(i < mm_layers) if isinstance(mm_layers, int) else mm_layers[i]
),
mlp_type=mlp_type,
window=window[i],
window_method=window_method[i],
msa_type=msa_type[i],
mca_type=mca_type[i],
rope_type=rope_type,
rope_dim=rope_dim,
is_last_layer=(i == num_layers - 1),
**kwargs,
)
for i in range(num_layers)
]
)
self.vid_out_norm = None
if vid_out_norm is not None:
self.vid_out_norm = get_norm_layer(vid_out_norm)(
dim=vid_dim,
eps=norm_eps,
elementwise_affine=True,
)
self.vid_out_ada = ada(
dim=vid_dim,
emb_dim=emb_dim,
layers=["out"],
modes=["in"],
)
self.vid_out = NaPatchOut(
out_channels=vid_out_channels,
patch_size=patch_size,
dim=vid_dim,
)
def set_gradient_checkpointing(self, enable: bool):
self.gradient_checkpointing = enable
def forward(
self,
vid: torch.FloatTensor, # l c
txt: Union[torch.FloatTensor, List[torch.FloatTensor]], # l c
vid_shape: torch.LongTensor, # b 3
txt_shape: Union[torch.LongTensor, List[torch.LongTensor]], # b 1
timestep: Union[int, float, torch.IntTensor, torch.FloatTensor], # b
disable_cache: bool = False, # for test
):
cache = Cache(disable=disable_cache)
# slice vid after patching in when using sequence parallelism
if isinstance(txt, list):
assert isinstance(self.txt_in, nn.ModuleList)
txt = [
na.unflatten(fc(i), s) for fc, i, s in zip(self.txt_in, txt, txt_shape)
] # B L D
txt, txt_shape = na.flatten([torch.cat(t, dim=0) for t in zip(*txt)])
txt = slice_inputs(txt, dim=0)
else:
txt = slice_inputs(txt, dim=0)
txt = self.txt_in(txt)
# Video input.
# Sequence parallel slicing is done inside patching class.
vid, vid_shape = self.vid_in(vid, vid_shape, cache)
# Embedding input.
emb = self.emb_in(timestep, device=vid.device, dtype=vid.dtype)
# Body
for i, block in enumerate(self.blocks):
vid, txt, vid_shape, txt_shape = gradient_checkpointing(
enabled=(self.gradient_checkpointing and self.training),
module=block,
vid=vid,
txt=txt,
vid_shape=vid_shape,
txt_shape=txt_shape,
emb=emb,
cache=cache,
)
# Video output norm.
if self.vid_out_norm:
vid = self.vid_out_norm(vid)
vid = self.vid_out_ada(
vid,
emb=emb,
layer="out",
mode="in",
hid_len=cache("vid_len", lambda: vid_shape.prod(-1)),
cache=cache,
branch_tag="vid",
)
# Video output.
vid, vid_shape = self.vid_out(vid, vid_shape, cache)
return NaDiTOutput(vid_sample=vid)
@@ -0,0 +1,147 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 Callable, Optional
from diffusers.models.normalization import RMSNorm
from torch import nn
import torch
import torch.nn.functional as F
import numbers
from torch.nn.parameter import Parameter
from torch.nn import init
# (dim: int, eps: float, elementwise_affine: bool)
norm_layer_type = Callable[[int, float, bool], nn.Module]
class CustomLayerNorm(nn.Module):
"""
Custom LayerNorm implementation to replace Apex FusedLayerNorm
"""
def __init__(self, normalized_shape, eps=1e-5, elementwise_affine=True):
super(CustomLayerNorm, self).__init__()
if isinstance(normalized_shape, numbers.Integral):
normalized_shape = (normalized_shape,)
self.normalized_shape = torch.Size(normalized_shape)
self.eps = eps
self.elementwise_affine = elementwise_affine
if self.elementwise_affine:
self.weight = Parameter(torch.Tensor(*normalized_shape))
self.bias = Parameter(torch.Tensor(*normalized_shape))
else:
self.register_parameter('weight', None)
self.register_parameter('bias', None)
self.reset_parameters()
def reset_parameters(self):
if self.elementwise_affine:
init.ones_(self.weight)
init.zeros_(self.bias)
def forward(self, input):
# 🚀 FP8 COMPATIBILITY: Convert parameters to match input dtype
# This prevents "Promotion for Float8 Types is not supported" errors
weight = self.weight
bias = self.bias
if self.elementwise_affine and weight is not None:
if weight.dtype != input.dtype:
weight = weight.to(input.dtype)
if bias is not None and bias.dtype != input.dtype:
bias = bias.to(input.dtype)
return F.layer_norm(
input, self.normalized_shape, weight, bias, self.eps)
class CustomRMSNorm(nn.Module):
"""
Custom RMSNorm implementation to replace Apex FusedRMSNorm
"""
def __init__(self, normalized_shape, eps=1e-5, elementwise_affine=True):
super(CustomRMSNorm, self).__init__()
if isinstance(normalized_shape, numbers.Integral):
normalized_shape = (normalized_shape,)
self.normalized_shape = torch.Size(normalized_shape)
self.eps = eps
self.elementwise_affine = elementwise_affine
if self.elementwise_affine:
self.weight = Parameter(torch.ones(*normalized_shape))
else:
self.register_parameter('weight', None)
def forward(self, input):
# RMS normalization: x / sqrt(mean(x^2) + eps) * weight
dims = tuple(range(-len(self.normalized_shape), 0))
# Calculate RMS: sqrt(mean(x^2))
variance = input.pow(2).mean(dim=dims, keepdim=True)
rms = torch.sqrt(variance + self.eps)
# Normalize
normalized = input / rms
if self.elementwise_affine:
# 🚀 FP8 COMPATIBILITY: Convert weight to match normalized dtype
# This prevents "Promotion for Float8 Types is not supported" errors
weight = self.weight
if weight.dtype != normalized.dtype:
weight = weight.to(normalized.dtype)
return normalized * weight
return normalized
def get_norm_layer(norm_type: Optional[str]) -> norm_layer_type:
def _norm_layer(dim: int, eps: float, elementwise_affine: bool):
if norm_type is None:
return nn.Identity()
if norm_type == "layer":
return nn.LayerNorm(
normalized_shape=dim,
eps=eps,
elementwise_affine=elementwise_affine,
)
if norm_type == "rms":
return RMSNorm(
dim=dim,
eps=eps,
elementwise_affine=elementwise_affine,
)
if norm_type == "fusedln":
# Use custom LayerNorm instead of Apex FusedLayerNorm
return CustomLayerNorm(
normalized_shape=dim,
elementwise_affine=elementwise_affine,
eps=eps,
)
if norm_type == "fusedrms":
# Use custom RMSNorm instead of Apex FusedRMSNorm
return CustomRMSNorm(
normalized_shape=dim,
elementwise_affine=elementwise_affine,
eps=eps,
)
raise NotImplementedError(f"{norm_type} is not supported")
return _norm_layer
@@ -0,0 +1,19 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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.
def get_na_patch_layers(patch_type="v1"):
assert patch_type in ["v1"]
if patch_type == "v1":
from .patch_v1 import NaPatchIn, NaPatchOut
return NaPatchIn, NaPatchOut
@@ -0,0 +1,127 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 Tuple, Union
import torch
from einops import rearrange
from torch import nn
from torch.nn.modules.utils import _triple
from ....common.cache import Cache
from ....common.distributed.ops import gather_outputs, slice_inputs
from .. import na
class PatchIn(nn.Module):
def __init__(
self,
in_channels: int,
patch_size: Union[int, Tuple[int, int, int]],
dim: int,
):
super().__init__()
t, h, w = _triple(patch_size)
self.patch_size = t, h, w
self.proj = nn.Linear(in_channels * t * h * w, dim)
def forward(
self,
vid: torch.Tensor,
) -> torch.Tensor:
t, h, w = self.patch_size
if t > 1:
assert vid.size(2) % t == 1
vid = torch.cat([vid[:, :, :1]] * (t - 1) + [vid], dim=2)
vid = rearrange(vid, "b c (T t) (H h) (W w) -> b T H W (t h w c)", t=t, h=h, w=w)
vid = self.proj(vid)
return vid
class PatchOut(nn.Module):
def __init__(
self,
out_channels: int,
patch_size: Union[int, Tuple[int, int, int]],
dim: int,
):
super().__init__()
t, h, w = _triple(patch_size)
self.patch_size = t, h, w
self.proj = nn.Linear(dim, out_channels * t * h * w)
def forward(
self,
vid: torch.Tensor,
) -> torch.Tensor:
t, h, w = self.patch_size
vid = self.proj(vid)
vid = rearrange(vid, "b T H W (t h w c) -> b c (T t) (H h) (W w)", t=t, h=h, w=w)
if t > 1:
vid = vid[:, :, (t - 1) :]
return vid
class NaPatchIn(PatchIn):
def forward(
self,
vid: torch.Tensor, # l c
vid_shape: torch.LongTensor,
cache: Cache = Cache(disable=True), # for test
) -> torch.Tensor:
cache = cache.namespace("patch")
vid_shape_before_patchify = cache("vid_shape_before_patchify", lambda: vid_shape)
t, h, w = self.patch_size
if not (t == h == w == 1):
vid = na.unflatten(vid, vid_shape)
for i in range(len(vid)):
if t > 1 and vid_shape_before_patchify[i, 0] % t != 0:
vid[i] = torch.cat([vid[i][:1]] * (t - vid[i].size(0) % t) + [vid[i]], dim=0)
vid[i] = rearrange(vid[i], "(T t) (H h) (W w) c -> T H W (t h w c)", t=t, h=h, w=w)
vid, vid_shape = na.flatten(vid)
# slice vid after patching in when using sequence parallelism
vid = slice_inputs(vid, dim=0)
vid = self.proj(vid)
return vid, vid_shape
class NaPatchOut(PatchOut):
def forward(
self,
vid: torch.FloatTensor, # l c
vid_shape: torch.LongTensor,
cache: Cache = Cache(disable=True), # for test
) -> Tuple[
torch.FloatTensor,
torch.LongTensor,
]:
cache = cache.namespace("patch")
vid_shape_before_patchify = cache.get("vid_shape_before_patchify")
t, h, w = self.patch_size
vid = self.proj(vid)
# gather vid before patching out when enabling sequence parallelism
vid = gather_outputs(
vid, gather_dim=0, padding_dim=0, unpad_shape=vid_shape, cache=cache.namespace("vid")
)
if not (t == h == w == 1):
vid = na.unflatten(vid, vid_shape)
for i in range(len(vid)):
vid[i] = rearrange(vid[i], "T H W (t h w c) -> (T t) (H h) (W w) c", t=t, h=h, w=w)
if t > 1 and vid_shape_before_patchify[i, 0] % t != 0:
vid[i] = vid[i][(t - vid_shape_before_patchify[i, 0] % t) :]
vid, vid_shape = na.flatten(vid)
return vid, vid_shape
+155
View File
@@ -0,0 +1,155 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 functools import lru_cache
from typing import Optional, Tuple
import torch
from einops import rearrange
from rotary_embedding_torch import RotaryEmbedding, apply_rotary_emb
from torch import nn
from src.common.cache import Cache
class RotaryEmbeddingBase(nn.Module):
def __init__(self, dim: int, rope_dim: int):
super().__init__()
self.rope = RotaryEmbedding(
dim=dim // rope_dim,
freqs_for="pixel",
max_freq=256,
)
# 1. Set model.requires_grad_(True) after model creation will make
# the `requires_grad=False` for rope freqs no longer hold.
# 2. Even if we don't set requires_grad_(True) explicitly,
# FSDP is not memory efficient when handling fsdp_wrap
# with mixed requires_grad=True/False.
# With above consideration, it is easier just remove the freqs
# out of nn.Parameters when `learned_freq=False`
freqs = self.rope.freqs
del self.rope.freqs
self.rope.register_buffer("freqs", freqs.data)
@lru_cache(maxsize=128)
def get_axial_freqs(self, *dims):
return self.rope.get_axial_freqs(*dims)
class RotaryEmbedding3d(RotaryEmbeddingBase):
def __init__(self, dim: int):
super().__init__(dim, rope_dim=3)
self.mm = False
def forward(
self,
q: torch.FloatTensor, # b h l d
k: torch.FloatTensor, # b h l d
size: Tuple[int, int, int],
) -> Tuple[
torch.FloatTensor,
torch.FloatTensor,
]:
T, H, W = size
freqs = self.get_axial_freqs(T, H, W)
q = rearrange(q, "b h (T H W) d -> b h T H W d", T=T, H=H, W=W)
k = rearrange(k, "b h (T H W) d -> b h T H W d", T=T, H=H, W=W)
q = apply_rotary_emb(freqs, q.float()).to(q.dtype)
k = apply_rotary_emb(freqs, k.float()).to(k.dtype)
q = rearrange(q, "b h T H W d -> b h (T H W) d")
k = rearrange(k, "b h T H W d -> b h (T H W) d")
return q, k
class MMRotaryEmbeddingBase(RotaryEmbeddingBase):
def __init__(self, dim: int, rope_dim: int):
super().__init__(dim, rope_dim)
self.rope = RotaryEmbedding(
dim=dim // rope_dim,
freqs_for="lang",
theta=10000,
)
freqs = self.rope.freqs
del self.rope.freqs
self.rope.register_buffer("freqs", freqs.data)
self.mm = True
class NaMMRotaryEmbedding3d(MMRotaryEmbeddingBase):
def __init__(self, dim: int):
super().__init__(dim, rope_dim=3)
def forward(
self,
vid_q: torch.FloatTensor, # L h d
vid_k: torch.FloatTensor, # L h d
vid_shape: torch.LongTensor, # B 3
txt_q: torch.FloatTensor, # L h d
txt_k: torch.FloatTensor, # L h d
txt_shape: torch.LongTensor, # B 1
cache: Cache,
) -> Tuple[
torch.FloatTensor,
torch.FloatTensor,
torch.FloatTensor,
torch.FloatTensor,
]:
vid_freqs, txt_freqs = cache(
"mmrope_freqs_3d",
lambda: self.get_freqs(vid_shape, txt_shape),
)
target_device = vid_q.device
if vid_freqs.device != target_device:
vid_freqs = vid_freqs.to(target_device)
if txt_freqs.device != target_device:
txt_freqs = txt_freqs.to(target_device)
vid_q = rearrange(vid_q, "L h d -> h L d")
vid_k = rearrange(vid_k, "L h d -> h L d")
vid_q = apply_rotary_emb(vid_freqs, vid_q.float()).to(vid_q.dtype)
vid_k = apply_rotary_emb(vid_freqs, vid_k.float()).to(vid_k.dtype)
vid_q = rearrange(vid_q, "h L d -> L h d")
vid_k = rearrange(vid_k, "h L d -> L h d")
txt_q = rearrange(txt_q, "L h d -> h L d")
txt_k = rearrange(txt_k, "L h d -> h L d")
txt_q = apply_rotary_emb(txt_freqs, txt_q.float()).to(txt_q.dtype)
txt_k = apply_rotary_emb(txt_freqs, txt_k.float()).to(txt_k.dtype)
txt_q = rearrange(txt_q, "h L d -> L h d")
txt_k = rearrange(txt_k, "h L d -> L h d")
return vid_q, vid_k, txt_q, txt_k
def get_freqs(
self,
vid_shape: torch.LongTensor,
txt_shape: torch.LongTensor,
) -> Tuple[
torch.Tensor,
torch.Tensor,
]:
vid_freqs = self.get_axial_freqs(1024, 128, 128)
txt_freqs = self.get_axial_freqs(1024)
vid_freq_list, txt_freq_list = [], []
for (f, h, w), l in zip(vid_shape.tolist(), txt_shape[:, 0].tolist()):
vid_freq = vid_freqs[l : l + f, :h, :w].reshape(-1, vid_freqs.size(-1))
txt_freq = txt_freqs[:l].repeat(1, 3).reshape(-1, vid_freqs.size(-1))
vid_freq_list.append(vid_freq)
txt_freq_list.append(txt_freq)
return torch.cat(vid_freq_list, dim=0), torch.cat(txt_freq_list, dim=0)
def get_na_rope(rope_type: Optional[str], dim: int):
if rope_type is None:
return None
if rope_type == "mmrope3d":
return NaMMRotaryEmbedding3d(dim=dim)
raise NotImplementedError(f"{rope_type} is not supported.")
@@ -0,0 +1,83 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 math import ceil
from typing import Tuple
import math
def get_window_op(name: str):
if name == "720pwin_by_size_bysize":
return make_720Pwindows_bysize
if name == "720pswin_by_size_bysize":
return make_shifted_720Pwindows_bysize
raise ValueError(f"Unknown windowing method: {name}")
# -------------------------------- Windowing -------------------------------- #
def make_720Pwindows_bysize(size: Tuple[int, int, int], num_windows: Tuple[int, int, int]):
t, h, w = size
resized_nt, resized_nh, resized_nw = num_windows
#cal windows under 720p
scale = math.sqrt((45 * 80) / (h * w))
resized_h, resized_w = round(h * scale), round(w * scale)
wh, ww = ceil(resized_h / resized_nh), ceil(resized_w / resized_nw) # window size.
wt = ceil(min(t, 30) / resized_nt) # window size.
nt, nh, nw = ceil(t / wt), ceil(h / wh), ceil(w / ww) # window size.
return [
(
slice(it * wt, min((it + 1) * wt, t)),
slice(ih * wh, min((ih + 1) * wh, h)),
slice(iw * ww, min((iw + 1) * ww, w)),
)
for iw in range(nw)
if min((iw + 1) * ww, w) > iw * ww
for ih in range(nh)
if min((ih + 1) * wh, h) > ih * wh
for it in range(nt)
if min((it + 1) * wt, t) > it * wt
]
def make_shifted_720Pwindows_bysize(size: Tuple[int, int, int], num_windows: Tuple[int, int, int]):
t, h, w = size
resized_nt, resized_nh, resized_nw = num_windows
#cal windows under 720p
scale = math.sqrt((45 * 80) / (h * w))
resized_h, resized_w = round(h * scale), round(w * scale)
wh, ww = ceil(resized_h / resized_nh), ceil(resized_w / resized_nw) # window size.
wt = ceil(min(t, 30) / resized_nt) # window size.
st, sh, sw = ( # shift size.
0.5 if wt < t else 0,
0.5 if wh < h else 0,
0.5 if ww < w else 0,
)
nt, nh, nw = ceil((t - st) / wt), ceil((h - sh) / wh), ceil((w - sw) / ww) # window size.
nt, nh, nw = ( # number of window.
nt + 1 if st > 0 else 1,
nh + 1 if sh > 0 else 1,
nw + 1 if sw > 0 else 1,
)
return [
(
slice(max(int((it - st) * wt), 0), min(int((it - st + 1) * wt), t)),
slice(max(int((ih - sh) * wh), 0), min(int((ih - sh + 1) * wh), h)),
slice(max(int((iw - sw) * ww), 0), min(int((iw - sw + 1) * ww), w)),
)
for iw in range(nw)
if min(int((iw - sw + 1) * ww), w) > max(int((iw - sw) * ww), 0)
for ih in range(nh)
if min(int((ih - sh + 1) * wh), h) > max(int((ih - sh) * wh), 0)
for it in range(nt)
if min(int((it - st + 1) * wt), t) > max(int((it - st) * wt), 0)
]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,437 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // Licensed under the Apache License, Version 2.0 (the "License");
# // you may not use this file except in compliance with the License.
# // You may obtain a copy of the License at
# //
# // http://www.apache.org/licenses/LICENSE-2.0
# //
# // Unless required by applicable law or agreed to in writing, software
# // distributed under the License is distributed on an "AS IS" BASIS,
# // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# // See the License for the specific language governing permissions and
# // limitations under the License.
import math
from contextlib import contextmanager
from typing import List, Optional, Union
import torch
import torch.nn.functional as F
from diffusers.models.normalization import RMSNorm
from einops import rearrange
from torch import Tensor, nn
from torch.nn import Conv3d
from .context_parallel_lib import cache_send_recv, get_cache_size
from .global_config import get_norm_limit
from .types import MemoryState, _inflation_mode_t, _memory_device_t
from ....common.half_precision_fixes import safe_pad_operation
# Single GPU inference - no distributed processing needed
# Mock distributed functions for single GPU inference
def get_sequence_parallel_group():
return None
def get_sequence_parallel_rank():
return 0
def get_sequence_parallel_world_size():
return 1
def get_next_sequence_parallel_rank():
return 0
def get_prev_sequence_parallel_rank():
return 0
@contextmanager
def ignore_padding(model):
orig_padding = model.padding
model.padding = (0, 0, 0)
try:
yield
finally:
model.padding = orig_padding
class InflatedCausalConv3d(Conv3d):
def __init__(
self,
*args,
inflation_mode: _inflation_mode_t,
memory_device: _memory_device_t = "same",
**kwargs,
):
self.inflation_mode = inflation_mode
self.memory = None
super().__init__(*args, **kwargs)
self.temporal_padding = self.padding[0]
self.memory_device = memory_device
self.padding = (0, *self.padding[1:]) # Remove temporal pad to keep causal.
self.memory_limit = float("inf")
def set_memory_limit(self, value: float):
self.memory_limit = value
def set_memory_device(self, memory_device: _memory_device_t):
self.memory_device = memory_device
def memory_limit_conv(
self,
x,
*,
split_dim=3,
padding=(0, 0, 0, 0, 0, 0),
prev_cache=None,
):
# Compatible with no limit.
if math.isinf(self.memory_limit):
if prev_cache is not None:
x = torch.cat([prev_cache, x], dim=split_dim - 1)
return super().forward(x)
# Compute tensor shape after concat & padding.
shape = torch.tensor(x.size())
if prev_cache is not None:
shape[split_dim - 1] += prev_cache.size(split_dim - 1)
shape[-3:] += torch.tensor(padding).view(3, 2).sum(-1).flip(0)
memory_occupy = shape.prod() * x.element_size() / 1024**3 # GiB
if memory_occupy < self.memory_limit or split_dim == x.ndim:
if prev_cache is not None:
x = torch.cat([prev_cache, x], dim=split_dim - 1)
x = safe_pad_operation(x, padding, mode='constant', value=0.0)
with ignore_padding(self):
return super().forward(x)
# Exceed memory limit, splitting tensor
# Split input (& prev_cache).
num_splits = math.ceil(memory_occupy / self.memory_limit)
size_per_split = x.size(split_dim) // num_splits
split_sizes = [size_per_split] * (num_splits - 1)
split_sizes += [x.size(split_dim) - sum(split_sizes)]
x = list(x.split(split_sizes, dim=split_dim))
if prev_cache is not None:
prev_cache = list(prev_cache.split(split_sizes, dim=split_dim))
# Loop Fwd.
cache = None
for idx in range(len(x)):
# Concat prev cache from last dim
if prev_cache is not None:
x[idx] = torch.cat([prev_cache[idx], x[idx]], dim=split_dim - 1)
# Get padding pattern.
lpad_dim = (x[idx].ndim - split_dim - 1) * 2
rpad_dim = lpad_dim + 1
padding = list(padding)
padding[lpad_dim] = self.padding[split_dim - 2] if idx == 0 else 0
padding[rpad_dim] = self.padding[split_dim - 2] if idx == len(x) - 1 else 0
pad_len = padding[lpad_dim] + padding[rpad_dim]
padding = tuple(padding)
# Prepare cache for next slice (this dim).
next_cache = None
cache_len = cache.size(split_dim) if cache is not None else 0
next_catch_size = get_cache_size(
conv_module=self,
input_len=x[idx].size(split_dim) + cache_len,
pad_len=pad_len,
dim=split_dim - 2,
)
if next_catch_size != 0:
assert next_catch_size <= x[idx].size(split_dim)
next_cache = (
x[idx].transpose(0, split_dim)[-next_catch_size:].transpose(0, split_dim)
)
# Recursive.
x[idx] = self.memory_limit_conv(
x[idx],
split_dim=split_dim + 1,
padding=padding,
prev_cache=cache,
)
# Update cache.
cache = next_cache
output = torch.cat(x, split_dim)
return output
def forward(
self,
input: Union[Tensor, List[Tensor]],
memory_state: MemoryState = MemoryState.UNSET,
) -> Tensor:
assert memory_state != MemoryState.UNSET
if memory_state != MemoryState.ACTIVE:
self.memory = None
if (
math.isinf(self.memory_limit)
and torch.is_tensor(input)
and get_sequence_parallel_group() is None
):
return self.basic_forward(input, memory_state)
return self.slicing_forward(input, memory_state)
def basic_forward(self, input: Tensor, memory_state: MemoryState = MemoryState.UNSET):
mem_size = self.stride[0] - self.kernel_size[0]
if (self.memory is not None) and (memory_state == MemoryState.ACTIVE):
input = extend_head(input, memory=self.memory, times=-1)
else:
input = extend_head(input, times=self.temporal_padding * 2)
memory = (
input[:, :, mem_size:].detach()
if (mem_size != 0 and memory_state != MemoryState.DISABLED)
else None
)
if (
memory_state != MemoryState.DISABLED
and not self.training
and (self.memory_device is not None)
):
self.memory = memory
if self.memory_device == "cpu" and self.memory is not None:
self.memory = self.memory.to("cpu")
return super().forward(input)
def slicing_forward(
self,
input: Union[Tensor, List[Tensor]],
memory_state: MemoryState = MemoryState.UNSET,
) -> Tensor:
squeeze_out = False
if torch.is_tensor(input):
input = [input]
squeeze_out = True
cache_size = self.kernel_size[0] - self.stride[0]
cache = cache_send_recv(
input, cache_size=cache_size, memory=self.memory, times=self.temporal_padding * 2
)
# Single GPU inference - simplified memory management
if (
memory_state in [MemoryState.INITIALIZING, MemoryState.ACTIVE] # use_slicing
and not self.training
and (self.memory_device is not None)
and cache_size != 0
):
if cache_size > input[-1].size(2) and cache is not None and len(input) == 1:
input[0] = torch.cat([cache, input[0]], dim=2)
cache = None
if cache_size <= input[-1].size(2):
self.memory = input[-1][:, :, -cache_size:].detach().contiguous()
if self.memory_device == "cpu" and self.memory is not None:
self.memory = self.memory.to("cpu")
padding = tuple(x for x in reversed(self.padding) for _ in range(2))
for i in range(len(input)):
# Prepare cache for next input slice.
next_cache = None
cache_size = 0
if i < len(input) - 1:
cache_len = cache.size(2) if cache is not None else 0
cache_size = get_cache_size(self, input[i].size(2) + cache_len, pad_len=0)
if cache_size != 0:
if cache_size > input[i].size(2) and cache is not None:
input[i] = torch.cat([cache, input[i]], dim=2)
cache = None
assert cache_size <= input[i].size(2), f"{cache_size} > {input[i].size(2)}"
next_cache = input[i][:, :, -cache_size:]
# Conv forward for this input slice.
input[i] = self.memory_limit_conv(
input[i],
padding=padding,
prev_cache=cache,
)
# Update cache.
cache = next_cache
return input[0] if squeeze_out else input
def tflops(self, args, kwargs, output) -> float:
if torch.is_tensor(output):
output_numel = output.numel()
elif isinstance(output, list):
output_numel = sum(o.numel() for o in output)
else:
raise NotImplementedError
return (2 * math.prod(self.kernel_size) * self.in_channels * (output_numel / 1e6)) / 1e6
def _load_from_state_dict(
self, state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs
):
if self.inflation_mode != "none":
state_dict = modify_state_dict(
self,
state_dict,
prefix,
inflate_weight_fn=inflate_weight,
inflate_bias_fn=inflate_bias,
)
super()._load_from_state_dict(
state_dict,
prefix,
local_metadata,
(strict and self.inflation_mode == "none"),
missing_keys,
unexpected_keys,
error_msgs,
)
def init_causal_conv3d(
*args,
inflation_mode: _inflation_mode_t,
**kwargs,
):
"""
Initialize a Causal-3D convolution layer.
Parameters:
inflation_mode: Listed as below. It's compatible with all the 3D-VAE checkpoints we have.
- none: No inflation will be conducted.
The loading logic of state dict will fall back to default.
- tail / replicate: Refer to the definition of `InflatedCausalConv3d`.
"""
return InflatedCausalConv3d(*args, inflation_mode=inflation_mode, **kwargs)
def causal_norm_wrapper(norm_layer: nn.Module, x: torch.Tensor) -> torch.Tensor:
input_dtype = x.dtype
if isinstance(norm_layer, (nn.LayerNorm, RMSNorm)):
if x.ndim == 4:
x = rearrange(x, "b c h w -> b h w c")
x = norm_layer(x)
x = rearrange(x, "b h w c -> b c h w")
return x.to(input_dtype)
if x.ndim == 5:
x = rearrange(x, "b c t h w -> b t h w c")
x = norm_layer(x)
x = rearrange(x, "b t h w c -> b c t h w")
return x.to(input_dtype)
if isinstance(norm_layer, (nn.GroupNorm, nn.BatchNorm2d, nn.SyncBatchNorm)):
if x.ndim <= 4:
return norm_layer(x).to(input_dtype)
if x.ndim == 5:
t = x.size(2)
x = rearrange(x, "b c t h w -> (b t) c h w")
memory_occupy = x.numel() * x.element_size() / 1024**3
if isinstance(norm_layer, nn.GroupNorm) and memory_occupy > get_norm_limit():
num_chunks = min(4 if x.element_size() == 2 else 2, norm_layer.num_groups)
assert norm_layer.num_groups % num_chunks == 0
num_groups_per_chunk = norm_layer.num_groups // num_chunks
x = list(x.chunk(num_chunks, dim=1))
weights = norm_layer.weight.chunk(num_chunks, dim=0)
biases = norm_layer.bias.chunk(num_chunks, dim=0)
for i, (w, b) in enumerate(zip(weights, biases)):
x[i] = F.group_norm(x[i], num_groups_per_chunk, w, b, norm_layer.eps)
x[i] = x[i].to(input_dtype)
# ADD BY NUMZ
# ADD BY NUMZ
x = torch.cat(x, dim=1)
else:
x = norm_layer(x)
x = rearrange(x, "(b t) c h w -> b c t h w", t=t)
return x.to(input_dtype)
raise NotImplementedError
def remove_head(tensor: Tensor, times: int = 1) -> Tensor:
"""
Remove duplicated first frame features in the up-sampling process.
"""
# Single GPU inference - always process
if times == 0:
return tensor
return torch.cat(tensors=(tensor[:, :, :1], tensor[:, :, times + 1 :]), dim=2)
def extend_head(tensor: Tensor, times: int = 2, memory: Optional[Tensor] = None) -> Tensor:
"""
When memory is None:
- Duplicate first frame features in the down-sampling process.
When memory is not None:
- Concatenate memory features with the input features to keep temporal consistency.
"""
if memory is not None:
return torch.cat((memory.to(tensor), tensor), dim=2)
assert times >= 0, "Invalid input for function 'extend_head'!"
if times == 0:
return tensor
else:
tile_repeat = [1] * tensor.ndim
tile_repeat[2] = times
return torch.cat(tensors=(torch.tile(tensor[:, :, :1], tile_repeat), tensor), dim=2)
def inflate_weight(weight_2d: torch.Tensor, weight_3d: torch.Tensor, inflation_mode: str):
"""
Inflate a 2D convolution weight matrix to a 3D one.
Parameters:
weight_2d: The weight matrix of 2D conv to be inflated.
weight_3d: The weight matrix of 3D conv to be initialized.
inflation_mode: the mode of inflation
"""
assert inflation_mode in ["tail", "replicate"]
assert weight_3d.shape[:2] == weight_2d.shape[:2]
with torch.no_grad():
if inflation_mode == "replicate":
depth = weight_3d.size(2)
weight_3d.copy_(weight_2d.unsqueeze(2).repeat(1, 1, depth, 1, 1) / depth)
else:
weight_3d.fill_(0.0)
weight_3d[:, :, -1].copy_(weight_2d)
return weight_3d
def inflate_bias(bias_2d: torch.Tensor, bias_3d: torch.Tensor, inflation_mode: str):
"""
Inflate a 2D convolution bias tensor to a 3D one
Parameters:
bias_2d: The bias tensor of 2D conv to be inflated.
bias_3d: The bias tensor of 3D conv to be initialized.
inflation_mode: Placeholder to align `inflate_weight`.
"""
assert bias_3d.shape == bias_2d.shape
with torch.no_grad():
bias_3d.copy_(bias_2d)
return bias_3d
def modify_state_dict(layer, state_dict, prefix, inflate_weight_fn, inflate_bias_fn):
"""
the main function to inflated 2D parameters to 3D.
"""
weight_name = prefix + "weight"
bias_name = prefix + "bias"
if weight_name in state_dict:
weight_2d = state_dict[weight_name]
if weight_2d.dim() == 4:
# Assuming the 2D weights are 4D tensors (out_channels, in_channels, h, w)
weight_3d = inflate_weight_fn(
weight_2d=weight_2d,
weight_3d=layer.weight,
inflation_mode=layer.inflation_mode,
)
state_dict[weight_name] = weight_3d
else:
return state_dict
# It's a 3d state dict, should not do inflation on both bias and weight.
if bias_name in state_dict:
bias_2d = state_dict[bias_name]
if bias_2d.dim() == 1:
# Assuming the 2D biases are 1D tensors (out_channels,)
bias_3d = inflate_bias_fn(
bias_2d=bias_2d,
bias_3d=layer.bias,
inflation_mode=layer.inflation_mode,
)
state_dict[bias_name] = bias_3d
return state_dict
@@ -0,0 +1,66 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 List
import torch
import torch.nn.functional as F
from torch import Tensor
from .types import MemoryState
# Single GPU inference - no distributed processing needed
def causal_conv_slice_inputs(x, split_size, memory_state):
# Single GPU inference - no slicing needed, return full tensor
return x
def causal_conv_gather_outputs(x):
# Single GPU inference - no gathering needed, return tensor as is
return x
def get_output_len(conv_module, input_len, pad_len, dim=0):
dilated_kernerl_size = conv_module.dilation[dim] * (conv_module.kernel_size[dim] - 1) + 1
output_len = (input_len + pad_len - dilated_kernerl_size) // conv_module.stride[dim] + 1
return output_len
def get_cache_size(conv_module, input_len, pad_len, dim=0):
dilated_kernerl_size = conv_module.dilation[dim] * (conv_module.kernel_size[dim] - 1) + 1
output_len = (input_len + pad_len - dilated_kernerl_size) // conv_module.stride[dim] + 1
remain_len = (
input_len + pad_len - ((output_len - 1) * conv_module.stride[dim] + dilated_kernerl_size)
)
overlap_len = dilated_kernerl_size - conv_module.stride[dim]
cache_len = overlap_len + remain_len # >= 0
assert output_len > 0
return cache_len
def cache_send_recv(tensor: List[Tensor], cache_size, times, memory=None):
# Single GPU inference - simplified cache handling
recv_buffer = None
# Handle memory buffer for single GPU case
if memory is not None:
recv_buffer = memory.to(tensor[0])
elif times > 0:
tile_repeat = [1] * tensor[0].ndim
tile_repeat[2] = times
recv_buffer = torch.tile(tensor[0][:, :, :1], tile_repeat)
return recv_buffer
@@ -0,0 +1,28 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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
_NORM_LIMIT = float("inf")
def get_norm_limit():
return _NORM_LIMIT
def set_norm_limit(value: Optional[float] = None):
global _NORM_LIMIT
if value is None:
value = float("inf")
_NORM_LIMIT = value
@@ -0,0 +1,106 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 functools import partial
from typing import Literal, Optional
from torch import Tensor
from torch.nn import Conv3d
from .inflated_lib import (
MemoryState,
extend_head,
inflate_bias,
inflate_weight,
modify_state_dict,
)
_inflation_mode_t = Literal["none", "tail", "replicate"]
_memory_device_t = Optional[Literal["cpu", "same"]]
class InflatedCausalConv3d(Conv3d):
def __init__(
self,
*args,
inflation_mode: _inflation_mode_t,
memory_device: _memory_device_t = "same",
**kwargs,
):
self.inflation_mode = inflation_mode
self.memory = None
super().__init__(*args, **kwargs)
self.temporal_padding = self.padding[0]
self.memory_device = memory_device
self.padding = (0, *self.padding[1:]) # Remove temporal pad to keep causal.
def set_memory_device(self, memory_device: _memory_device_t):
self.memory_device = memory_device
def forward(self, input: Tensor, memory_state: MemoryState = MemoryState.DISABLED) -> Tensor:
mem_size = self.stride[0] - self.kernel_size[0]
if (self.memory is not None) and (memory_state == MemoryState.ACTIVE):
input = extend_head(input, memory=self.memory)
else:
input = extend_head(input, times=self.temporal_padding * 2)
memory = (
input[:, :, mem_size:].detach()
if (mem_size != 0 and memory_state != MemoryState.DISABLED)
else None
)
if (
memory_state != MemoryState.DISABLED
and not self.training
and (self.memory_device is not None)
):
self.memory = memory
if self.memory_device == "cpu" and self.memory is not None:
self.memory = self.memory.to("cpu")
return super().forward(input)
def _load_from_state_dict(
self, state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs
):
if self.inflation_mode != "none":
state_dict = modify_state_dict(
self,
state_dict,
prefix,
inflate_weight_fn=partial(inflate_weight, position="tail"),
inflate_bias_fn=partial(inflate_bias, position="tail"),
)
super()._load_from_state_dict(
state_dict,
prefix,
local_metadata,
(strict and self.inflation_mode == "none"),
missing_keys,
unexpected_keys,
error_msgs,
)
def init_causal_conv3d(
*args,
inflation_mode: _inflation_mode_t,
**kwargs,
):
"""
Initialize a Causal-3D convolution layer.
Parameters:
inflation_mode: Listed as below. It's compatible with all the 3D-VAE checkpoints we have.
- none: No inflation will be conducted.
The loading logic of state dict will fall back to default.
- tail / replicate: Refer to the definition of `InflatedCausalConv3d`.
"""
return InflatedCausalConv3d(*args, inflation_mode=inflation_mode, **kwargs)
@@ -0,0 +1,156 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 enum import Enum
from typing import Optional
import numpy as np
import torch
from diffusers.models.normalization import RMSNorm
from einops import rearrange
from torch import Tensor, nn
from ....common.logger import get_logger
logger = get_logger(__name__)
class MemoryState(Enum):
"""
State[Disabled]: No memory bank will be enabled.
State[Initializing]: The model is handling the first clip,
need to reset / initialize the memory bank.
State[Active]: There has been some data in the memory bank.
"""
DISABLED = 0
INITIALIZING = 1
ACTIVE = 2
def causal_norm_wrapper(norm_layer: nn.Module, x: torch.Tensor) -> torch.Tensor:
if isinstance(norm_layer, (nn.LayerNorm, RMSNorm)):
if x.ndim == 4:
x = rearrange(x, "b c h w -> b h w c")
x = norm_layer(x)
x = rearrange(x, "b h w c -> b c h w")
return x
if x.ndim == 5:
x = rearrange(x, "b c t h w -> b t h w c")
x = norm_layer(x)
x = rearrange(x, "b t h w c -> b c t h w")
return x
if isinstance(norm_layer, (nn.GroupNorm, nn.BatchNorm2d, nn.SyncBatchNorm)):
if x.ndim <= 4:
return norm_layer(x)
if x.ndim == 5:
t = x.size(2)
x = rearrange(x, "b c t h w -> (b t) c h w")
x = norm_layer(x)
x = rearrange(x, "(b t) c h w -> b c t h w", t=t)
return x
raise NotImplementedError
def remove_head(tensor: Tensor, times: int = 1) -> Tensor:
"""
Remove duplicated first frame features in the up-sampling process.
"""
if times == 0:
return tensor
return torch.cat(tensors=(tensor[:, :, :1], tensor[:, :, times + 1 :]), dim=2)
def extend_head(
tensor: Tensor, times: Optional[int] = 2, memory: Optional[Tensor] = None
) -> Tensor:
"""
When memory is None:
- Duplicate first frame features in the down-sampling process.
When memory is not None:
- Concatenate memory features with the input features to keep temporal consistency.
"""
if times == 0:
return tensor
if memory is not None:
return torch.cat((memory.to(tensor), tensor), dim=2)
else:
tile_repeat = np.ones(tensor.ndim).astype(int)
tile_repeat[2] = times
return torch.cat(tensors=(torch.tile(tensor[:, :, :1], list(tile_repeat)), tensor), dim=2)
def inflate_weight(weight_2d: torch.Tensor, weight_3d: torch.Tensor, inflation_mode: str):
"""
Inflate a 2D convolution weight matrix to a 3D one.
Parameters:
weight_2d: The weight matrix of 2D conv to be inflated.
weight_3d: The weight matrix of 3D conv to be initialized.
inflation_mode: the mode of inflation
"""
assert inflation_mode in ["constant", "replicate"]
assert weight_3d.shape[:2] == weight_2d.shape[:2]
with torch.no_grad():
if inflation_mode == "replicate":
depth = weight_3d.size(2)
weight_3d.copy_(weight_2d.unsqueeze(2).repeat(1, 1, depth, 1, 1) / depth)
else:
weight_3d.fill_(0.0)
weight_3d[:, :, -1].copy_(weight_2d)
return weight_3d
def inflate_bias(bias_2d: torch.Tensor, bias_3d: torch.Tensor, inflation_mode: str):
"""
Inflate a 2D convolution bias tensor to a 3D one
Parameters:
bias_2d: The bias tensor of 2D conv to be inflated.
bias_3d: The bias tensor of 3D conv to be initialized.
inflation_mode: Placeholder to align `inflate_weight`.
"""
assert bias_3d.shape == bias_2d.shape
with torch.no_grad():
bias_3d.copy_(bias_2d)
return bias_3d
def modify_state_dict(layer, state_dict, prefix, inflate_weight_fn, inflate_bias_fn):
"""
the main function to inflated 2D parameters to 3D.
"""
weight_name = prefix + "weight"
bias_name = prefix + "bias"
if weight_name in state_dict:
weight_2d = state_dict[weight_name]
if weight_2d.dim() == 4:
# Assuming the 2D weights are 4D tensors (out_channels, in_channels, h, w)
weight_3d = inflate_weight_fn(
weight_2d=weight_2d,
weight_3d=layer.weight,
inflation_mode=layer.inflation_mode,
)
state_dict[weight_name] = weight_3d
else:
return state_dict
# It's a 3d state dict, should not do inflation on both bias and weight.
if bias_name in state_dict:
bias_2d = state_dict[bias_name]
if bias_2d.dim() == 1:
# Assuming the 2D biases are 1D tensors (out_channels,)
bias_3d = inflate_bias_fn(
bias_2d=bias_2d,
bias_3d=layer.bias,
inflation_mode=layer.inflation_mode,
)
state_dict[bias_name] = bias_3d
return state_dict
@@ -0,0 +1,76 @@
# // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# //
# // 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 enum import Enum
from typing import Dict, Literal, NamedTuple, Optional
import torch
_receptive_field_t = Literal["half", "full"]
_inflation_mode_t = Literal["none", "tail", "replicate"]
_memory_device_t = Optional[Literal["cpu", "same"]]
_gradient_checkpointing_t = Optional[Literal["half", "full"]]
_selective_checkpointing_t = Optional[Literal["coarse", "fine"]]
class DiagonalGaussianDistribution:
def __init__(self, mean: torch.Tensor, logvar: torch.Tensor):
self.mean = mean
self.logvar = torch.clamp(logvar, -30.0, 20.0)
self.std = torch.exp(0.5 * self.logvar)
self.var = torch.exp(self.logvar)
def mode(self) -> torch.Tensor:
return self.mean
def sample(self) -> torch.FloatTensor:
return self.mean + self.std * torch.randn_like(self.mean)
def kl(self) -> torch.Tensor:
return 0.5 * torch.sum(
self.mean**2 + self.var - 1.0 - self.logvar,
dim=list(range(1, self.mean.ndim)),
)
class MemoryState(Enum):
"""
State[Disabled]: No memory bank will be enabled.
State[Initializing]: The model is handling the first clip, need to reset the memory bank.
State[Active]: There has been some data in the memory bank.
State[Unset]: Error state, indicating users didn't pass correct memory state in.
"""
DISABLED = 0
INITIALIZING = 1
ACTIVE = 2
UNSET = 3
class QuantizerOutput(NamedTuple):
latent: torch.Tensor
extra_loss: torch.Tensor
statistics: Dict[str, torch.Tensor]
class CausalAutoencoderOutput(NamedTuple):
sample: torch.Tensor
latent: torch.Tensor
posterior: Optional[DiagonalGaussianDistribution]
class CausalEncoderOutput(NamedTuple):
latent: torch.Tensor
posterior: Optional[DiagonalGaussianDistribution]
class CausalDecoderOutput(NamedTuple):
sample: torch.Tensor
@@ -0,0 +1,956 @@
# Copyright (c) 2023 HuggingFace Team
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates.
# SPDX-License-Identifier: Apache License, Version 2.0 (the "License")
#
# This file has been modified by ByteDance Ltd. and/or its affiliates. on 1st June 2025
#
# Original file was released under Apache License, Version 2.0 (the "License"), with the full license text
# available at http://www.apache.org/licenses/LICENSE-2.0.
#
# This modified file is released under the same license.
from contextlib import nullcontext
from typing import Optional, Tuple, Literal, Callable, Union
import torch
import torch.nn as nn
import torch.nn.functional as F
from diffusers.models.autoencoders.vae import DiagonalGaussianDistribution
from einops import rearrange
from ....common.half_precision_fixes import safe_pad_operation
from ....common.distributed.advanced import get_sequence_parallel_world_size
from ....common.logger import get_logger
from .causal_inflation_lib import (
InflatedCausalConv3d,
causal_norm_wrapper,
init_causal_conv3d,
remove_head,
)
from .context_parallel_lib import (
causal_conv_gather_outputs,
causal_conv_slice_inputs,
)
from .global_config import set_norm_limit
from .types import (
CausalAutoencoderOutput,
CausalDecoderOutput,
CausalEncoderOutput,
MemoryState,
_inflation_mode_t,
_memory_device_t,
_receptive_field_t,
_selective_checkpointing_t,
)
logger = get_logger(__name__) # pylint: disable=invalid-name
# Fake func, no checkpointing is required for inference
def gradient_checkpointing(module: Union[Callable, nn.Module], *args, enabled: bool, **kwargs):
return module(*args, **kwargs)
class ResnetBlock2D(nn.Module):
r"""
A Resnet block.
Parameters:
in_channels (`int`): The number of channels in the input.
out_channels (`int`, *optional*, default to be `None`):
The number of output channels for the first conv2d layer.
If None, same as `in_channels`.
dropout (`float`, *optional*, defaults to `0.0`): The dropout probability to use.
"""
def __init__(
self, *, in_channels: int, out_channels: Optional[int] = None, dropout: float = 0.0
):
super().__init__()
self.in_channels = in_channels
out_channels = in_channels if out_channels is None else out_channels
self.out_channels = out_channels
self.nonlinearity = nn.SiLU()
self.norm1 = torch.nn.GroupNorm(
num_groups=32, num_channels=in_channels, eps=1e-6, affine=True
)
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1)
self.norm2 = torch.nn.GroupNorm(
num_groups=32, num_channels=out_channels, eps=1e-6, affine=True
)
self.dropout = torch.nn.Dropout(dropout)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1)
self.use_in_shortcut = self.in_channels != out_channels
self.conv_shortcut = None
if self.use_in_shortcut:
self.conv_shortcut = nn.Conv2d(
in_channels, out_channels, kernel_size=1, stride=1, padding=0
)
def forward(self, input_tensor: torch.Tensor) -> torch.Tensor:
hidden = input_tensor
hidden = self.norm1(hidden)
hidden = self.nonlinearity(hidden)
hidden = self.conv1(hidden)
hidden = self.norm2(hidden)
hidden = self.nonlinearity(hidden)
hidden = self.dropout(hidden)
hidden = self.conv2(hidden)
if self.conv_shortcut is not None:
input_tensor = self.conv_shortcut(input_tensor)
output_tensor = input_tensor + hidden
return output_tensor
class Upsample3D(nn.Module):
"""A 3D upsampling layer."""
def __init__(
self,
channels: int,
inflation_mode: _inflation_mode_t = "tail",
temporal_up: bool = False,
spatial_up: bool = True,
slicing: bool = False,
):
super().__init__()
self.channels = channels
self.conv = init_causal_conv3d(
self.channels, self.channels, kernel_size=3, padding=1, inflation_mode=inflation_mode
)
self.temporal_up = temporal_up
self.spatial_up = spatial_up
self.temporal_ratio = 2 if temporal_up else 1
self.spatial_ratio = 2 if spatial_up else 1
self.slicing = slicing
upscale_ratio = (self.spatial_ratio**2) * self.temporal_ratio
self.upscale_conv = nn.Conv3d(
self.channels, self.channels * upscale_ratio, kernel_size=1, padding=0
)
identity = (
torch.eye(self.channels).repeat(upscale_ratio, 1).reshape_as(self.upscale_conv.weight)
)
self.upscale_conv.weight.data.copy_(identity)
nn.init.zeros_(self.upscale_conv.bias)
self.gradient_checkpointing = False
def forward(
self,
hidden_states: torch.FloatTensor,
memory_state: MemoryState,
) -> torch.FloatTensor:
return gradient_checkpointing(
self.custom_forward,
hidden_states,
memory_state,
enabled=self.training and self.gradient_checkpointing,
)
def custom_forward(
self,
hidden_states: torch.FloatTensor,
memory_state: MemoryState,
) -> torch.FloatTensor:
assert hidden_states.shape[1] == self.channels
if self.slicing:
split_size = hidden_states.size(2) // 2
hidden_states = list(
hidden_states.split([split_size, hidden_states.size(2) - split_size], dim=2)
)
else:
hidden_states = [hidden_states]
for i in range(len(hidden_states)):
hidden_states[i] = self.upscale_conv(hidden_states[i])
hidden_states[i] = rearrange(
hidden_states[i],
"b (x y z c) f h w -> b c (f z) (h x) (w y)",
x=self.spatial_ratio,
y=self.spatial_ratio,
z=self.temporal_ratio,
)
# [Overridden] For causal temporal conv
if self.temporal_up and memory_state != MemoryState.ACTIVE:
hidden_states[0] = remove_head(hidden_states[0])
if self.slicing:
hidden_states = self.conv(hidden_states, memory_state=memory_state)
return torch.cat(hidden_states, dim=2)
else:
return self.conv(hidden_states[0], memory_state=memory_state)
class Downsample3D(nn.Module):
"""A 3D downsampling layer."""
def __init__(
self,
channels: int,
inflation_mode: _inflation_mode_t = "tail",
temporal_down: bool = False,
spatial_down: bool = True,
):
super().__init__()
self.channels = channels
self.temporal_down = temporal_down
self.spatial_down = spatial_down
self.temporal_ratio = 2 if temporal_down else 1
self.spatial_ratio = 2 if spatial_down else 1
self.temporal_kernel = 3 if temporal_down else 1
self.spatial_kernel = 3 if spatial_down else 1
self.conv = init_causal_conv3d(
self.channels,
self.channels,
kernel_size=(self.temporal_kernel, self.spatial_kernel, self.spatial_kernel),
stride=(self.temporal_ratio, self.spatial_ratio, self.spatial_ratio),
padding=((1 if self.temporal_down else 0), 0, 0),
inflation_mode=inflation_mode,
)
self.gradient_checkpointing = False
def forward(
self,
hidden_states: torch.FloatTensor,
memory_state: MemoryState,
) -> torch.FloatTensor:
return gradient_checkpointing(
self.custom_forward,
hidden_states,
memory_state,
enabled=self.training and self.gradient_checkpointing,
)
def custom_forward(
self,
hidden_states: torch.FloatTensor,
memory_state: MemoryState,
) -> torch.FloatTensor:
assert hidden_states.shape[1] == self.channels
if self.spatial_down:
hidden_states = safe_pad_operation(hidden_states, (0, 1, 0, 1), mode="constant", value=0)
hidden_states = self.conv(hidden_states, memory_state=memory_state)
return hidden_states
class ResnetBlock3D(ResnetBlock2D):
def __init__(
self,
*args,
inflation_mode: _inflation_mode_t = "tail",
time_receptive_field: _receptive_field_t = "half",
**kwargs,
):
super().__init__(*args, **kwargs)
self.conv1 = init_causal_conv3d(
self.in_channels,
self.out_channels,
kernel_size=3,
stride=1,
padding=1,
inflation_mode=inflation_mode,
)
self.conv2 = init_causal_conv3d(
self.out_channels,
self.out_channels,
kernel_size=(1, 3, 3) if time_receptive_field == "half" else (3, 3, 3),
stride=1,
padding=(0, 1, 1) if time_receptive_field == "half" else (1, 1, 1),
inflation_mode=inflation_mode,
)
if self.use_in_shortcut:
self.conv_shortcut = init_causal_conv3d(
self.in_channels,
self.out_channels,
kernel_size=1,
stride=1,
padding=0,
bias=(self.conv_shortcut.bias is not None),
inflation_mode=inflation_mode,
)
self.gradient_checkpointing = False
def forward(self, input_tensor: torch.Tensor, memory_state: MemoryState = MemoryState.UNSET):
return gradient_checkpointing(
self.custom_forward,
input_tensor,
memory_state,
enabled=self.training and self.gradient_checkpointing,
)
def custom_forward(
self, input_tensor: torch.Tensor, memory_state: MemoryState = MemoryState.UNSET
):
assert memory_state != MemoryState.UNSET
hidden_states = input_tensor
hidden_states = causal_norm_wrapper(self.norm1, hidden_states)
hidden_states = self.nonlinearity(hidden_states)
hidden_states = self.conv1(hidden_states, memory_state=memory_state)
hidden_states = causal_norm_wrapper(self.norm2, hidden_states)
hidden_states = self.nonlinearity(hidden_states)
hidden_states = self.dropout(hidden_states)
hidden_states = self.conv2(hidden_states, memory_state=memory_state)
if self.conv_shortcut is not None:
input_tensor = self.conv_shortcut(input_tensor, memory_state=memory_state)
output_tensor = input_tensor + hidden_states
return output_tensor
class DownEncoderBlock3D(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int,
dropout: float = 0.0,
num_layers: int = 1,
add_downsample: bool = True,
inflation_mode: _inflation_mode_t = "tail",
time_receptive_field: _receptive_field_t = "half",
temporal_down: bool = True,
spatial_down: bool = True,
):
super().__init__()
resnets = []
for i in range(num_layers):
in_channels = in_channels if i == 0 else out_channels
resnets.append(
ResnetBlock3D(
in_channels=in_channels,
out_channels=out_channels,
dropout=dropout,
inflation_mode=inflation_mode,
time_receptive_field=time_receptive_field,
)
)
self.resnets = nn.ModuleList(resnets)
self.downsamplers = None
if add_downsample:
# Todo: Refactor this line before V5 Image VAE Training.
self.downsamplers = nn.ModuleList(
[
Downsample3D(
channels=out_channels,
inflation_mode=inflation_mode,
temporal_down=temporal_down,
spatial_down=spatial_down,
)
]
)
def forward(
self, hidden_states: torch.FloatTensor, memory_state: MemoryState
) -> torch.FloatTensor:
for resnet in self.resnets:
hidden_states = resnet(hidden_states, memory_state=memory_state)
if self.downsamplers is not None:
for downsampler in self.downsamplers:
hidden_states = downsampler(hidden_states, memory_state=memory_state)
return hidden_states
class UpDecoderBlock3D(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int,
dropout: float = 0.0,
num_layers: int = 1,
add_upsample: bool = True,
inflation_mode: _inflation_mode_t = "tail",
time_receptive_field: _receptive_field_t = "half",
temporal_up: bool = True,
spatial_up: bool = True,
slicing: bool = False,
):
super().__init__()
resnets = []
for i in range(num_layers):
input_channels = in_channels if i == 0 else out_channels
resnets.append(
ResnetBlock3D(
in_channels=input_channels,
out_channels=out_channels,
dropout=dropout,
inflation_mode=inflation_mode,
time_receptive_field=time_receptive_field,
)
)
self.resnets = nn.ModuleList(resnets)
self.upsamplers = None
# Todo: Refactor this line before V5 Image VAE Training.
if add_upsample:
self.upsamplers = nn.ModuleList(
[
Upsample3D(
channels=out_channels,
inflation_mode=inflation_mode,
temporal_up=temporal_up,
spatial_up=spatial_up,
slicing=slicing,
)
]
)
def forward(
self, hidden_states: torch.FloatTensor, memory_state: MemoryState
) -> torch.FloatTensor:
for resnet in self.resnets:
hidden_states = resnet(hidden_states, memory_state=memory_state)
if self.upsamplers is not None:
for upsampler in self.upsamplers:
hidden_states = upsampler(hidden_states, memory_state=memory_state)
return hidden_states
class UNetMidBlock3D(nn.Module):
def __init__(
self,
channels: int,
dropout: float = 0.0,
inflation_mode: _inflation_mode_t = "tail",
time_receptive_field: _receptive_field_t = "half",
):
super().__init__()
self.resnets = nn.ModuleList(
[
ResnetBlock3D(
in_channels=channels,
out_channels=channels,
dropout=dropout,
inflation_mode=inflation_mode,
time_receptive_field=time_receptive_field,
),
ResnetBlock3D(
in_channels=channels,
out_channels=channels,
dropout=dropout,
inflation_mode=inflation_mode,
time_receptive_field=time_receptive_field,
),
]
)
def forward(self, hidden_states: torch.Tensor, memory_state: MemoryState):
for resnet in self.resnets:
hidden_states = resnet(hidden_states, memory_state)
return hidden_states
class Encoder3D(nn.Module):
r"""
The `Encoder` layer of a variational autoencoder that encodes
its input into a latent representation.
"""
def __init__(
self,
in_channels: int = 3,
out_channels: int = 3,
block_out_channels: Tuple[int, ...] = (64,),
layers_per_block: int = 2,
double_z: bool = True,
temporal_down_num: int = 2,
inflation_mode: _inflation_mode_t = "tail",
time_receptive_field: _receptive_field_t = "half",
selective_checkpointing: Tuple[_selective_checkpointing_t] = ("none",),
):
super().__init__()
self.layers_per_block = layers_per_block
self.temporal_down_num = temporal_down_num
self.conv_in = init_causal_conv3d(
in_channels,
block_out_channels[0],
kernel_size=3,
stride=1,
padding=1,
inflation_mode=inflation_mode,
)
self.down_blocks = nn.ModuleList([])
# down
output_channel = block_out_channels[0]
for i in range(len(block_out_channels)):
input_channel = output_channel
output_channel = block_out_channels[i]
is_final_block = i == len(block_out_channels) - 1
is_temporal_down_block = i >= len(block_out_channels) - self.temporal_down_num - 1
# Note: take the last one
down_block = DownEncoderBlock3D(
num_layers=self.layers_per_block,
in_channels=input_channel,
out_channels=output_channel,
add_downsample=not is_final_block,
temporal_down=is_temporal_down_block,
spatial_down=True,
inflation_mode=inflation_mode,
time_receptive_field=time_receptive_field,
)
self.down_blocks.append(down_block)
# mid
self.mid_block = UNetMidBlock3D(
channels=block_out_channels[-1],
inflation_mode=inflation_mode,
time_receptive_field=time_receptive_field,
)
# out
self.conv_norm_out = nn.GroupNorm(
num_channels=block_out_channels[-1], num_groups=32, eps=1e-6
)
self.conv_act = nn.SiLU()
conv_out_channels = 2 * out_channels if double_z else out_channels
self.conv_out = init_causal_conv3d(
block_out_channels[-1], conv_out_channels, 3, padding=1, inflation_mode=inflation_mode
)
assert len(selective_checkpointing) == len(self.down_blocks)
self.set_gradient_checkpointing(selective_checkpointing)
def set_gradient_checkpointing(self, checkpointing_types):
gradient_checkpointing = []
for down_block, sac_type in zip(self.down_blocks, checkpointing_types):
if sac_type == "coarse":
gradient_checkpointing.append(True)
elif sac_type == "fine":
for n, m in down_block.named_modules():
if hasattr(m, "gradient_checkpointing"):
m.gradient_checkpointing = True
logger.debug(f"set gradient_checkpointing: {n}")
gradient_checkpointing.append(False)
else:
gradient_checkpointing.append(False)
self.gradient_checkpointing = gradient_checkpointing
logger.info(f"[Encoder3D] gradient_checkpointing: {checkpointing_types}")
def forward(self, sample: torch.FloatTensor, memory_state: MemoryState) -> torch.FloatTensor:
r"""The forward method of the `Encoder` class."""
sample = self.conv_in(sample, memory_state=memory_state)
# down
for down_block, sac in zip(self.down_blocks, self.gradient_checkpointing):
sample = gradient_checkpointing(
down_block,
sample,
memory_state=memory_state,
enabled=self.training and sac,
)
# middle
sample = self.mid_block(sample, memory_state=memory_state)
# post-process
sample = causal_norm_wrapper(self.conv_norm_out, sample)
sample = self.conv_act(sample)
sample = self.conv_out(sample, memory_state=memory_state)
return sample
class Decoder3D(nn.Module):
r"""
The `Decoder` layer of a variational autoencoder that
decodes its latent representation into an output sample.
"""
def __init__(
self,
in_channels: int = 3,
out_channels: int = 3,
block_out_channels: Tuple[int, ...] = (64,),
layers_per_block: int = 2,
inflation_mode: _inflation_mode_t = "tail",
time_receptive_field: _receptive_field_t = "half",
temporal_up_num: int = 2,
slicing_up_num: int = 0,
selective_checkpointing: Tuple[_selective_checkpointing_t] = ("none",),
):
super().__init__()
self.layers_per_block = layers_per_block
self.temporal_up_num = temporal_up_num
self.conv_in = init_causal_conv3d(
in_channels,
block_out_channels[-1],
kernel_size=3,
stride=1,
padding=1,
inflation_mode=inflation_mode,
)
self.up_blocks = nn.ModuleList([])
# mid
self.mid_block = UNetMidBlock3D(
channels=block_out_channels[-1],
inflation_mode=inflation_mode,
time_receptive_field=time_receptive_field,
)
# up
reversed_block_out_channels = list(reversed(block_out_channels))
output_channel = reversed_block_out_channels[0]
for i in range(len(reversed_block_out_channels)):
prev_output_channel = output_channel
output_channel = reversed_block_out_channels[i]
is_final_block = i == len(block_out_channels) - 1
is_temporal_up_block = i < self.temporal_up_num
is_slicing_up_block = i >= len(block_out_channels) - slicing_up_num
# Note: Keep symmetric
up_block = UpDecoderBlock3D(
num_layers=self.layers_per_block + 1,
in_channels=prev_output_channel,
out_channels=output_channel,
add_upsample=not is_final_block,
temporal_up=is_temporal_up_block,
slicing=is_slicing_up_block,
inflation_mode=inflation_mode,
time_receptive_field=time_receptive_field,
)
self.up_blocks.append(up_block)
# out
self.conv_norm_out = nn.GroupNorm(
num_channels=block_out_channels[0], num_groups=32, eps=1e-6
)
self.conv_act = nn.SiLU()
self.conv_out = init_causal_conv3d(
block_out_channels[0], out_channels, 3, padding=1, inflation_mode=inflation_mode
)
assert len(selective_checkpointing) == len(self.up_blocks)
self.set_gradient_checkpointing(selective_checkpointing)
def set_gradient_checkpointing(self, checkpointing_types):
gradient_checkpointing = []
for up_block, sac_type in zip(self.up_blocks, checkpointing_types):
if sac_type == "coarse":
gradient_checkpointing.append(True)
elif sac_type == "fine":
for n, m in up_block.named_modules():
if hasattr(m, "gradient_checkpointing"):
m.gradient_checkpointing = True
logger.debug(f"set gradient_checkpointing: {n}")
gradient_checkpointing.append(False)
else:
gradient_checkpointing.append(False)
self.gradient_checkpointing = gradient_checkpointing
logger.info(f"[Decoder3D] gradient_checkpointing: {checkpointing_types}")
def forward(self, sample: torch.FloatTensor, memory_state: MemoryState) -> torch.FloatTensor:
r"""The forward method of the `Decoder` class."""
sample = self.conv_in(sample, memory_state=memory_state)
# middle
sample = self.mid_block(sample, memory_state=memory_state)
# up
for up_block, sac in zip(self.up_blocks, self.gradient_checkpointing):
sample = gradient_checkpointing(
up_block,
sample,
memory_state=memory_state,
enabled=self.training and sac,
)
# post-process
sample = causal_norm_wrapper(self.conv_norm_out, sample)
sample = self.conv_act(sample)
sample = self.conv_out(sample, memory_state=memory_state)
return sample
class VideoAutoencoderKL(nn.Module):
def __init__(
self,
in_channels: int = 3,
out_channels: int = 3,
block_out_channels: Tuple[int] = (64,),
layers_per_block: int = 1,
latent_channels: int = 4,
use_quant_conv: bool = True,
use_post_quant_conv: bool = True,
enc_selective_checkpointing: Tuple[_selective_checkpointing_t] = ("none",),
dec_selective_checkpointing: Tuple[_selective_checkpointing_t] = ("none",),
temporal_scale_num: int = 3,
slicing_up_num: int = 0,
inflation_mode: _inflation_mode_t = "tail",
time_receptive_field: _receptive_field_t = "half",
slicing_sample_min_size: int = None,
spatial_downsample_factor: int = 16,
temporal_downsample_factor: int = 8,
freeze_encoder: bool = False,
):
super().__init__()
self.spatial_downsample_factor = spatial_downsample_factor
self.temporal_downsample_factor = temporal_downsample_factor
self.freeze_encoder = freeze_encoder
if slicing_sample_min_size is None:
slicing_sample_min_size = temporal_downsample_factor
self.slicing_sample_min_size = slicing_sample_min_size
self.slicing_latent_min_size = slicing_sample_min_size // (2**temporal_scale_num)
# pass init params to Encoder
self.encoder = Encoder3D(
in_channels=in_channels,
out_channels=latent_channels,
block_out_channels=block_out_channels,
layers_per_block=layers_per_block,
double_z=True,
temporal_down_num=temporal_scale_num,
selective_checkpointing=enc_selective_checkpointing,
inflation_mode=inflation_mode,
time_receptive_field=time_receptive_field,
)
# pass init params to Decoder
self.decoder = Decoder3D(
in_channels=latent_channels,
out_channels=out_channels,
block_out_channels=block_out_channels,
layers_per_block=layers_per_block,
# [Override] add temporal_up_num parameter
temporal_up_num=temporal_scale_num,
slicing_up_num=slicing_up_num,
selective_checkpointing=dec_selective_checkpointing,
inflation_mode=inflation_mode,
time_receptive_field=time_receptive_field,
)
self.quant_conv = (
init_causal_conv3d(
in_channels=2 * latent_channels,
out_channels=2 * latent_channels,
kernel_size=1,
inflation_mode=inflation_mode,
)
if use_quant_conv
else None
)
self.post_quant_conv = (
init_causal_conv3d(
in_channels=latent_channels,
out_channels=latent_channels,
kernel_size=1,
inflation_mode=inflation_mode,
)
if use_post_quant_conv
else None
)
self.use_slicing = False
def enable_slicing(self):
self.use_slicing = True
def disable_slicing(self):
self.use_slicing = False
def encode(self, x: torch.FloatTensor) -> CausalEncoderOutput:
if x.ndim == 4:
x = x.unsqueeze(2)
h = self.slicing_encode(x)
p = DiagonalGaussianDistribution(h)
z = p.sample()
return CausalEncoderOutput(z, p)
def decode(self, z: torch.FloatTensor) -> CausalDecoderOutput:
if z.ndim == 4:
z = z.unsqueeze(2)
x = self.slicing_decode(z)
return CausalDecoderOutput(x)
def _encode(self, x: torch.Tensor, memory_state: MemoryState) -> torch.Tensor:
x = causal_conv_slice_inputs(x, self.slicing_sample_min_size, memory_state=memory_state)
h = self.encoder(x, memory_state=memory_state)
h = self.quant_conv(h, memory_state=memory_state) if self.quant_conv is not None else h
h = causal_conv_gather_outputs(h)
return h
def _decode(self, z: torch.Tensor, memory_state: MemoryState) -> torch.Tensor:
z = causal_conv_slice_inputs(z, self.slicing_latent_min_size, memory_state=memory_state)
z = (
self.post_quant_conv(z, memory_state=memory_state)
if self.post_quant_conv is not None
else z
)
x = self.decoder(z, memory_state=memory_state)
x = causal_conv_gather_outputs(x)
return x
def slicing_encode(self, x: torch.Tensor) -> torch.Tensor:
sp_size = get_sequence_parallel_world_size()
if self.use_slicing and (x.shape[2] - 1) > self.slicing_sample_min_size * sp_size:
x_slices = x[:, :, 1:].split(split_size=self.slicing_sample_min_size * sp_size, dim=2)
encoded_slices = [
self._encode(
torch.cat((x[:, :, :1], x_slices[0]), dim=2),
memory_state=MemoryState.INITIALIZING,
)
]
for x_idx in range(1, len(x_slices)):
encoded_slices.append(
self._encode(x_slices[x_idx], memory_state=MemoryState.ACTIVE)
)
return torch.cat(encoded_slices, dim=2)
else:
return self._encode(x, memory_state=MemoryState.DISABLED)
def slicing_decode(self, z: torch.Tensor) -> torch.Tensor:
sp_size = get_sequence_parallel_world_size()
if self.use_slicing and (z.shape[2] - 1) > self.slicing_latent_min_size * sp_size:
z_slices = z[:, :, 1:].split(split_size=self.slicing_latent_min_size * sp_size, dim=2)
decoded_slices = [
self._decode(
torch.cat((z[:, :, :1], z_slices[0]), dim=2),
memory_state=MemoryState.INITIALIZING,
)
]
for z_idx in range(1, len(z_slices)):
decoded_slices.append(
self._decode(z_slices[z_idx], memory_state=MemoryState.ACTIVE)
)
return torch.cat(decoded_slices, dim=2)
else:
return self._decode(z, memory_state=MemoryState.DISABLED)
def forward(self, x: torch.FloatTensor) -> CausalAutoencoderOutput:
with torch.no_grad() if self.freeze_encoder else nullcontext():
z, p = self.encode(x)
x = self.decode(z).sample
return CausalAutoencoderOutput(x, z, p)
def preprocess(self, x: torch.Tensor):
# x should in [B, C, T, H, W], [B, C, H, W]
assert x.ndim == 4 or x.size(2) % self.temporal_downsample_factor == 1
return x
def postprocess(self, x: torch.Tensor):
# x should in [B, C, T, H, W], [B, C, H, W]
return x
def set_causal_slicing(
self,
*,
split_size: Optional[int],
memory_device: _memory_device_t,
):
assert (
split_size is None or memory_device is not None
), "if split_size is set, memory_device must not be None."
if split_size is not None:
self.enable_slicing()
self.slicing_sample_min_size = split_size
self.slicing_latent_min_size = split_size // self.temporal_downsample_factor
else:
self.disable_slicing()
for module in self.modules():
if isinstance(module, InflatedCausalConv3d):
module.set_memory_device(memory_device)
def set_memory_limit(self, conv_max_mem: Optional[float], norm_max_mem: Optional[float]):
set_norm_limit(norm_max_mem)
for m in self.modules():
if isinstance(m, InflatedCausalConv3d):
m.set_memory_limit(conv_max_mem if conv_max_mem is not None else float("inf"))
class VideoAutoencoderKLWrapper(VideoAutoencoderKL):
def __init__(
self, *args, spatial_downsample_factor: int, temporal_downsample_factor: int, **kwargs
):
self.spatial_downsample_factor = spatial_downsample_factor
self.temporal_downsample_factor = temporal_downsample_factor
super().__init__(*args, **kwargs)
def forward(self, x) -> CausalAutoencoderOutput:
z, _, p = self.encode(x)
x, _ = self.decode(z)
return CausalAutoencoderOutput(x, z, None, p)
def encode(self, x) -> CausalEncoderOutput:
if x.ndim == 4:
x = x.unsqueeze(2)
p = super().encode(x).latent_dist
z = p.sample().squeeze(2)
return CausalEncoderOutput(z, None, p)
def decode(self, z) -> CausalDecoderOutput:
if z.ndim == 4:
z = z.unsqueeze(2)
x = super().decode(z).sample.squeeze(2)
return CausalDecoderOutput(x, None)
def preprocess(self, x):
# x should in [B, C, T, H, W], [B, C, H, W]
assert x.ndim == 4 or x.size(2) % 4 == 1
return x
def postprocess(self, x):
# x should in [B, C, T, H, W], [B, C, H, W]
return x
def set_causal_slicing(
self,
*,
split_size: Optional[int],
memory_device: Optional[Literal["cpu", "same"]],
):
assert (
split_size is None or memory_device is not None
), "if split_size is set, memory_device must not be None."
if split_size is not None:
self.enable_slicing()
else:
self.disable_slicing()
self.slicing_sample_min_size = split_size
if split_size is not None:
self.slicing_latent_min_size = split_size // self.temporal_downsample_factor
for module in self.modules():
if isinstance(module, InflatedCausalConv3d):
module.set_memory_device(memory_device)
@@ -0,0 +1,28 @@
act_fn: silu
block_out_channels:
- 128
- 256
- 512
- 512
down_block_types:
- DownEncoderBlock3D
- DownEncoderBlock3D
- DownEncoderBlock3D
- DownEncoderBlock3D
in_channels: 3
latent_channels: 16
layers_per_block: 2
norm_num_groups: 32
out_channels: 3
slicing_sample_min_size: 4
temporal_scale_num: 2
inflation_mode: pad
up_block_types:
- UpDecoderBlock3D
- UpDecoderBlock3D
- UpDecoderBlock3D
- UpDecoderBlock3D
spatial_downsample_factor: 8
temporal_downsample_factor: 4
use_quant_conv: False
use_post_quant_conv: False
@@ -0,0 +1,131 @@
"""
Memory management module for SeedVR2
Handles VRAM usage, cache management, and memory optimization
Extracted from: seedvr2.py (lines 373-405, 607-626, 1016-1044)
"""
import os
import torch
import gc
from typing import Tuple, Optional
from src.common.cache import Cache
from src.models.dit_v2.rope import RotaryEmbeddingBase
def preinitialize_rope_cache(runner) -> None:
"""
🚀 Pre-initialize RoPE cache to avoid OOM at first launch
Args:
runner: The model runner containing DiT and VAE models
"""
try:
# Create dummy tensors to simulate common shapes
# Format: [batch, channels, frames, height, width] for vid_shape
# Format: [batch, seq_len] for txt_shape
common_shapes = [
# Common video resolutions
(torch.tensor([[1, 3, 3]], dtype=torch.long), torch.tensor([[77]], dtype=torch.long)), # 1 frame, 77 tokens
(torch.tensor([[4, 3, 3]], dtype=torch.long), torch.tensor([[77]], dtype=torch.long)), # 4 frames
(torch.tensor([[5, 3, 3]], dtype=torch.long), torch.tensor([[77]], dtype=torch.long)), # 5 frames (4n+1 format)
(torch.tensor([[1, 4, 4]], dtype=torch.long), torch.tensor([[77]], dtype=torch.long)), # Higher resolution
]
# Create mock cache for pre-initialization
temp_cache = Cache()
# Access RoPE modules in DiT (recursive search)
def find_rope_modules(module):
rope_modules = []
for name, child in module.named_modules():
if hasattr(child, 'get_freqs') and callable(child.get_freqs):
rope_modules.append((name, child))
return rope_modules
rope_modules = find_rope_modules(runner.dit)
# Pre-calculate for each RoPE module found
for name, rope_module in rope_modules:
# Temporarily move module to CPU if necessary
original_device = next(rope_module.parameters()).device if list(rope_module.parameters()) else torch.device('cpu')
rope_module.to('cpu')
try:
for vid_shape, txt_shape in common_shapes:
cache_key = f"720pswin_by_size_bysize_{tuple(vid_shape[0].tolist())}_sd3.mmrope_freqs_3d"
def compute_freqs():
# Calculate with reduced dimensions to avoid OOM
with torch.no_grad():
# Detect RoPE module type
module_type = type(rope_module).__name__
if module_type == 'NaRotaryEmbedding3d':
# NaRotaryEmbedding3d: only takes shape (vid_shape)
return rope_module.get_freqs(vid_shape.cpu())
else:
# Standard RoPE: takes vid_shape and txt_shape
return rope_module.get_freqs(vid_shape.cpu(), txt_shape.cpu())
# Store in cache
temp_cache(cache_key, compute_freqs)
except Exception as e:
print(f" ❌ Error in module {name}: {e}")
finally:
# Restore to original device
rope_module.to(original_device)
# Copy temporary cache to runner cache
if hasattr(runner, 'cache'):
runner.cache.cache.update(temp_cache.cache)
else:
runner.cache = temp_cache
except Exception as e:
print(f" ⚠️ Error during RoPE pre-init: {e}")
print(" 🔄 Model will work but could have OOM at first launch")
def clear_rope_cache(runner) -> None:
"""
🧹 Clear RoPE cache to free VRAM
Args:
runner: The model runner containing the cache
"""
print("🧹 Cleaning RoPE cache...")
if hasattr(runner, 'cache') and hasattr(runner.cache, 'cache'):
# Count entries before cleanup
cache_size = len(runner.cache.cache)
# Free all tensors from cache
for key, value in runner.cache.cache.items():
if isinstance(value, (tuple, list)):
for item in value:
if hasattr(item, 'cpu'):
item.cpu()
del item
elif hasattr(value, 'cpu'):
value.cpu()
del value
# Clear the cache
runner.cache.cache.clear()
print(f" ✅ RoPE cache cleared ({cache_size} entries removed)")
if hasattr(runner, 'dit'):
cleared_lru_count = 0
for module in runner.dit.modules():
if isinstance(module, RotaryEmbeddingBase):
if hasattr(module.get_axial_freqs, 'cache_clear'):
module.get_axial_freqs.cache_clear()
cleared_lru_count += 1
if cleared_lru_count > 0:
print(f" ✅ Cleared {cleared_lru_count} LRU caches from RoPE modules.")
print("🎯 RoPE cache cleanup completed!")
@@ -0,0 +1,160 @@
"""
Performance optimization module for SeedVR2
Contains optimized tensor operations and video processing functions
Extracted from: seedvr2.py (lines 1633-1730)
"""
import torch
from typing import List, Union
def optimized_video_rearrange(video_tensors: List[torch.Tensor]) -> List[torch.Tensor]:
"""
🚀 OPTIMIZED version of video rearrangement
Replaces slow loops with vectorized operations
Transforms:
- 3D: c h w -> t c h w (with t=1)
- 4D: c t h w -> t c h w
Expected gains: 5-10x faster than naive loops
Args:
video_tensors: List of video tensors to rearrange
Returns:
List of rearranged tensors in t c h w format
"""
if not video_tensors:
return []
# 🔍 Analyze dimensions to optimize processing
videos_3d = []
videos_4d = []
indices_3d = []
indices_4d = []
for i, video in enumerate(video_tensors):
if video.ndim == 3:
videos_3d.append(video)
indices_3d.append(i)
else: # ndim == 4
videos_4d.append(video)
indices_4d.append(i)
# 🎯 Prepare final result
samples = [None] * len(video_tensors)
# 🚀 BATCH PROCESSING for 3D videos (c h w -> 1 c h w)
if videos_3d:
# Method 1: Stack + permute (faster than rearrange)
# c h w -> c 1 h w -> 1 c h w
batch_3d = torch.stack([v.unsqueeze(1) for v in videos_3d]) # [batch, c, 1, h, w]
batch_3d = batch_3d.permute(0, 2, 1, 3, 4) # [batch, 1, c, h, w]
for i, idx in enumerate(indices_3d):
samples[idx] = batch_3d[i] # [1, c, h, w]
# 🚀 BATCH PROCESSING for 4D videos (c t h w -> t c h w)
if videos_4d:
# Check if all 4D videos have the same shape for maximum optimization
shapes = [v.shape for v in videos_4d]
if len(set(shapes)) == 1:
# 🎯 MAXIMUM OPTIMIZATION: All shapes identical
# Stack + permute in single operation
batch_4d = torch.stack(videos_4d) # [batch, c, t, h, w]
batch_4d = batch_4d.permute(0, 2, 1, 3, 4) # [batch, t, c, h, w]
for i, idx in enumerate(indices_4d):
samples[idx] = batch_4d[i] # [t, c, h, w]
else:
# 🔄 FALLBACK: Different shapes, optimized individual processing
for i, idx in enumerate(indices_4d):
# Use permute instead of rearrange (faster)
samples[idx] = videos_4d[i].permute(1, 0, 2, 3) # c t h w -> t c h w
return samples
def optimized_single_video_rearrange(video: torch.Tensor) -> torch.Tensor:
"""
🚀 OPTIMIZED version for single video tensor
Replaces rearrange() with native PyTorch operations
Transforms:
- 3D: c h w -> 1 c h w (add temporal dimension)
- 4D: c t h w -> t c h w (permute dimensions)
Expected gains: 2-5x faster than rearrange()
Args:
video: Input video tensor
Returns:
Rearranged tensor with temporal dimension first
"""
if video.ndim == 3:
# c h w -> 1 c h w (add temporal dimension t=1)
return video.unsqueeze(0)
else: # ndim == 4
# c t h w -> t c h w (permute channels and temporal)
return video.permute(1, 0, 2, 3)
def optimized_sample_to_image_format(sample: torch.Tensor) -> torch.Tensor:
"""
🚀 OPTIMIZED version to convert sample to image format
Replaces rearrange() with native PyTorch operations
Transforms:
- 3D: c h w -> 1 h w c (add temporal dimension + permute to image format)
- 4D: t c h w -> t h w c (permute to image format)
Expected gains: 2-5x faster than rearrange()
Args:
sample: Input sample tensor
Returns:
Tensor in image format (channels last)
"""
if sample.ndim == 3:
# c h w -> 1 h w c (add temporal dimension then permute)
return sample.unsqueeze(0).permute(0, 2, 3, 1)
else: # ndim == 4
# t c h w -> t h w c (permute channels to last)
return sample.permute(0, 2, 3, 1)
def temporal_latent_blending(latents1: torch.Tensor, latents2: torch.Tensor, blend_frames: int) -> torch.Tensor:
"""
🎨 Temporal blending in latent space to avoid discontinuities
Args:
latents1: Latents from previous batch (end frames)
latents2: Latents from current batch (start frames)
blend_frames: Number of frames to blend
Returns:
Blended latents for smooth transition
"""
if latents1.shape[0] != latents2.shape[0]:
# Adjust dimensions if necessary
min_frames = min(latents1.shape[0], latents2.shape[0])
latents1 = latents1[:min_frames]
latents2 = latents2[:min_frames]
# Create linear blending weights
# Frame 0: 100% latents1, 0% latents2
# Frame n: 0% latents1, 100% latents2
weights1 = torch.linspace(1.0, 0.0, blend_frames).view(-1, 1, 1, 1).to(latents1.device)
weights2 = torch.linspace(0.0, 1.0, blend_frames).view(-1, 1, 1, 1).to(latents2.device)
# Apply blending
blended_latents = weights1 * latents1 + weights2 * latents2
return blended_latents
+138
View File
@@ -0,0 +1,138 @@
import torch
from PIL import Image
from torch import Tensor
from torch.nn import functional as F
from src.common.half_precision_fixes import safe_pad_operation, safe_interpolate_operation
from torchvision.transforms import ToTensor, ToPILImage
def adain_color_fix(target: Image, source: Image):
# Convert images to tensors
to_tensor = ToTensor()
target_tensor = to_tensor(target).unsqueeze(0)
source_tensor = to_tensor(source).unsqueeze(0)
# Apply adaptive instance normalization
result_tensor = adaptive_instance_normalization(target_tensor, source_tensor)
# Convert tensor back to image
to_image = ToPILImage()
result_image = to_image(result_tensor.squeeze(0).clamp_(0.0, 1.0))
return result_image
def wavelet_color_fix(target: Image, source: Image):
# Convert images to tensors
to_tensor = ToTensor()
target_tensor = to_tensor(target).unsqueeze(0)
source_tensor = to_tensor(source).unsqueeze(0)
# Apply wavelet reconstruction
result_tensor = wavelet_reconstruction(target_tensor, source_tensor)
# Convert tensor back to image
to_image = ToPILImage()
result_image = to_image(result_tensor.squeeze(0).clamp_(0.0, 1.0))
return result_image
def calc_mean_std(feat: Tensor, eps=1e-5):
"""Calculate mean and std for adaptive_instance_normalization.
Args:
feat (Tensor): 4D tensor.
eps (float): A small value added to the variance to avoid
divide-by-zero. Default: 1e-5.
"""
size = feat.size()
assert len(size) == 4, 'The input feature should be 4D tensor.'
b, c = size[:2]
feat_var = feat.view(b, c, -1).var(dim=2) + eps
feat_std = feat_var.sqrt().view(b, c, 1, 1)
feat_mean = feat.view(b, c, -1).mean(dim=2).view(b, c, 1, 1)
return feat_mean, feat_std
def adaptive_instance_normalization(content_feat:Tensor, style_feat:Tensor):
"""Adaptive instance normalization.
Adjust the reference features to have the similar color and illuminations
as those in the degradate features.
Args:
content_feat (Tensor): The reference feature.
style_feat (Tensor): The degradate features.
"""
size = content_feat.size()
style_mean, style_std = calc_mean_std(style_feat)
content_mean, content_std = calc_mean_std(content_feat)
normalized_feat = (content_feat - content_mean.expand(size)) / content_std.expand(size)
return normalized_feat * style_std.expand(size) + style_mean.expand(size)
def wavelet_blur(image: Tensor, radius: int):
"""
Apply wavelet blur to the input tensor.
"""
# input shape: (1, 3, H, W)
# convolution kernel
kernel_vals = [
[0.0625, 0.125, 0.0625],
[0.125, 0.25, 0.125],
[0.0625, 0.125, 0.0625],
]
kernel = torch.tensor(kernel_vals, dtype=image.dtype, device=image.device)
# add channel dimensions to the kernel to make it a 4D tensor
kernel = kernel[None, None]
# repeat the kernel across all input channels
kernel = kernel.repeat(3, 1, 1, 1)
image = safe_pad_operation(image, (radius, radius, radius, radius), mode='replicate')
# apply convolution
output = F.conv2d(image, kernel, groups=3, dilation=radius)
return output
def wavelet_decomposition(image: Tensor, levels=5):
"""
Apply wavelet decomposition to the input tensor.
This function only returns the low frequency & the high frequency.
"""
high_freq = torch.zeros_like(image)
for i in range(levels):
radius = 2 ** i
low_freq = wavelet_blur(image, radius)
high_freq += (image - low_freq)
image = low_freq
return high_freq, low_freq
def wavelet_reconstruction(content_feat:Tensor, style_feat:Tensor):
"""
Apply wavelet decomposition, so that the content will have the same color as the style.
"""
# Vérifier et ajuster les dimensions si nécessaire
if content_feat.shape != style_feat.shape:
# Redimensionner style_feat pour correspondre à content_feat
target_shape = content_feat.shape
if len(target_shape) >= 3: # Au moins 3 dimensions
# Utiliser interpolation pour ajuster les dimensions spatiales
style_feat = safe_interpolate_operation(
style_feat,
size=target_shape[-2:], # Dernières 2 dimensions (H, W)
mode='bilinear',
align_corners=False
)
# calculate the wavelet decomposition of the content feature
content_high_freq, content_low_freq = wavelet_decomposition(content_feat)
del content_low_freq
# calculate the wavelet decomposition of the style feature
style_high_freq, style_low_freq = wavelet_decomposition(style_feat)
del style_high_freq
# Vérification finale avant addition
if content_high_freq.shape != style_low_freq.shape:
style_low_freq = safe_interpolate_operation(
style_low_freq,
size=content_high_freq.shape[-2:],
mode='bilinear',
align_corners=False
)
# reconstruct the content feature with the style's high frequency
return content_high_freq + style_low_freq
+58
View File
@@ -0,0 +1,58 @@
import os
import argparse
import numpy as np
import torch
from PIL import Image
from huggingface_hub import snapshot_download
from torchvision.transforms import ToPILImage
from src.core.generation import generation_loop
from src.core.model_manager import configure_runner
device = 'cuda'
dtype = torch.bfloat16
model_dir = 'seedvr2_models'
model = 'seedvr2_ema_3b_fp16.safetensors'
resolution = 1024
seed = 100
cfg = 1.0
input_image = '/home/vlado/generative/Samples/cutie-512.png'
to_pil = ToPILImage()
runner = None
loaded_model = None
def upscale_image(model_name:str, image_path:str):
global runner, loaded_model
if (runner is None) or (loaded_model != model_name):
runner = configure_runner(model_name, model_dir, device=device, dtype=dtype)
loaded_model = model_name
image = Image.open(image_path).convert("RGB")
image_tensor = np.array(image)
image_tensor = torch.from_numpy(image_tensor).to(device=device, dtype=dtype).unsqueeze(0) / 255.0
result_tensor = generation_loop(
runner=runner,
images=image_tensor,
cfg_scale=cfg,
seed=seed,
res_w=resolution,
batch_size=1,
temporal_overlap=0,
device=device,
)
image = to_pil(result_tensor.squeeze().permute((2, 0, 1)))
output_path = os.path.join('/tmp', os.path.basename(image_path))
image.save(output_path, quality=95)
return image
if __name__ == "__main__":
output_image = upscale_image(model, input_image)
print('input:', input_image)
print('output:', output_image)
+2 -2
View File
@@ -79,7 +79,7 @@ class Upscaler:
scalers.append(scaler)
loaded.append(model_path)
# shared.log.debug(f'Upscaler type={self.name} folder="{self.user_path}" model="{model[0]}" path="{model_path}"')
if not os.path.exists(self.user_path):
if self.user_path is None or not os.path.exists(self.user_path):
return scalers
self.find_folder(self.user_path, scalers, loaded)
return scalers
@@ -148,7 +148,7 @@ class UpscalerData:
scaler: Upscaler = None
model: None
def __init__(self, name: str, path: str, upscaler: Upscaler = None, scale: int = 4, model=None):
def __init__(self, name: str, path: str = None, upscaler: Upscaler = None, scale: int = 4, model=None):
self.name = name
self.data_path = path
self.local_data_path = path