mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-23 09:46:00 +00:00
big refactor to use models from diffusers and transformers
This commit is contained in:
@@ -219,8 +219,6 @@ eo1 = ["lerobot[transformers-dep]", "lerobot[qwen-vl-utils-dep]"]
|
|||||||
fastwam = [
|
fastwam = [
|
||||||
"lerobot[transformers-dep]",
|
"lerobot[transformers-dep]",
|
||||||
"lerobot[diffusers-dep]",
|
"lerobot[diffusers-dep]",
|
||||||
"ftfy>=6.1.1,<7.0.0",
|
|
||||||
"regex>=2024.0.0,<2027.0.0",
|
|
||||||
]
|
]
|
||||||
hilserl = ["lerobot[transformers-dep]", "lerobot[dataset]", "gym-hil>=0.1.13,<0.2.0", "lerobot[grpcio-dep]", "lerobot[placo-dep]"]
|
hilserl = ["lerobot[transformers-dep]", "lerobot[dataset]", "gym-hil>=0.1.13,<0.2.0", "lerobot[grpcio-dep]", "lerobot[placo-dep]"]
|
||||||
vla_jepa = ["lerobot[transformers-dep]", "lerobot[diffusers-dep]", "lerobot[qwen-vl-utils-dep]"]
|
vla_jepa = ["lerobot[transformers-dep]", "lerobot[diffusers-dep]", "lerobot[qwen-vl-utils-dep]"]
|
||||||
|
|||||||
@@ -118,10 +118,6 @@ def _coerce_policy_features(features: dict[str, Any] | None) -> dict[str, Policy
|
|||||||
return coerced
|
return coerced
|
||||||
|
|
||||||
|
|
||||||
def _coerce_normalization_mapping(mapping: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
return {key: _coerce_enum(NormalizationMode, value) for key, value in mapping.items()}
|
|
||||||
|
|
||||||
|
|
||||||
def _is_local_model_id(value: str) -> bool:
|
def _is_local_model_id(value: str) -> bool:
|
||||||
path = Path(value).expanduser()
|
path = Path(value).expanduser()
|
||||||
return path.is_absolute() or value.startswith(("./", "../", "~")) or path.exists()
|
return path.is_absolute() or value.startswith(("./", "../", "~")) or path.exists()
|
||||||
@@ -200,7 +196,7 @@ class FastWAMConfig(PreTrainedConfig):
|
|||||||
loss: dict[str, float] = field(default_factory=lambda: {"lambda_video": 1.0, "lambda_action": 1.0})
|
loss: dict[str, float] = field(default_factory=lambda: {"lambda_video": 1.0, "lambda_action": 1.0})
|
||||||
video_dit_config: dict[str, Any] | None = None
|
video_dit_config: dict[str, Any] | None = None
|
||||||
action_dit_config: dict[str, Any] | None = None
|
action_dit_config: dict[str, Any] | None = None
|
||||||
normalization_mapping: dict[str, Any] = field(
|
normalization_mapping: dict[str, NormalizationMode] = field(
|
||||||
default_factory=lambda: {
|
default_factory=lambda: {
|
||||||
"VISUAL": NormalizationMode.MEAN_STD,
|
"VISUAL": NormalizationMode.MEAN_STD,
|
||||||
"STATE": NormalizationMode.MEAN_STD,
|
"STATE": NormalizationMode.MEAN_STD,
|
||||||
@@ -220,7 +216,6 @@ class FastWAMConfig(PreTrainedConfig):
|
|||||||
self.input_features = _coerce_policy_features(self.input_features)
|
self.input_features = _coerce_policy_features(self.input_features)
|
||||||
self.output_features = _coerce_policy_features(self.output_features)
|
self.output_features = _coerce_policy_features(self.output_features)
|
||||||
self.toggle_action_dimensions = [int(dim) for dim in self.toggle_action_dimensions]
|
self.toggle_action_dimensions = [int(dim) for dim in self.toggle_action_dimensions]
|
||||||
self.normalization_mapping = _coerce_normalization_mapping(self.normalization_mapping)
|
|
||||||
self.video_dit_config = self.video_dit_config or default_video_dit_config(self.action_dim)
|
self.video_dit_config = self.video_dit_config or default_video_dit_config(self.action_dim)
|
||||||
self.action_dit_config = self.action_dit_config or default_action_dit_config(self.action_dim)
|
self.action_dit_config = self.action_dit_config or default_action_dit_config(self.action_dim)
|
||||||
self.video_dit_config["fp32_attention"] = bool(self.fp32_attention)
|
self.video_dit_config["fp32_attention"] = bool(self.fp32_attention)
|
||||||
@@ -266,6 +261,38 @@ class FastWAMConfig(PreTrainedConfig):
|
|||||||
def get_scheduler_preset(self) -> None:
|
def get_scheduler_preset(self) -> None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def set_dataset_feature_metadata(self, dataset_features: dict[str, Any]) -> None:
|
||||||
|
"""Rebuild visual input features from the dataset's real camera keys.
|
||||||
|
|
||||||
|
FastWAM's `__post_init__` installs a synthetic single-image default
|
||||||
|
(`observation.images.image` at full `image_size` width). For datasets
|
||||||
|
with one or more separately-named cameras (e.g. `observation.images.top`,
|
||||||
|
`observation.images.wrist`), this hook — invoked by `make_policy` once the
|
||||||
|
dataset metadata is known — replaces that default with the actual camera
|
||||||
|
keys, each declared at the policy's native per-camera resolution
|
||||||
|
(`image_size[0]` x `image_size[1] // num_cameras`). The accompanying
|
||||||
|
resize step in `make_fastwam_pre_post_processors` resizes raw frames to
|
||||||
|
match, so heterogeneous source resolutions (e.g. 480x640) are supported.
|
||||||
|
"""
|
||||||
|
image_keys = sorted(
|
||||||
|
key
|
||||||
|
for key, feature in dataset_features.items()
|
||||||
|
if key.startswith("observation.images.")
|
||||||
|
and feature.get("dtype") in ("video", "image")
|
||||||
|
)
|
||||||
|
if not image_keys:
|
||||||
|
return
|
||||||
|
height, total_width = self.image_size
|
||||||
|
per_cam_width = total_width // len(image_keys)
|
||||||
|
new_inputs: dict[str, PolicyFeature] = {
|
||||||
|
key: PolicyFeature(type=FeatureType.VISUAL, shape=(3, height, per_cam_width))
|
||||||
|
for key in image_keys
|
||||||
|
}
|
||||||
|
if self.proprio_dim is not None and OBS_STATE in dataset_features:
|
||||||
|
new_inputs[OBS_STATE] = PolicyFeature(type=FeatureType.STATE, shape=(self.proprio_dim,))
|
||||||
|
self.input_features = new_inputs
|
||||||
|
self.validate_features()
|
||||||
|
|
||||||
def validate_features(self) -> None:
|
def validate_features(self) -> None:
|
||||||
if self.action_dim <= 0:
|
if self.action_dim <= 0:
|
||||||
raise ValueError(f"`action_dim` must be positive, got {self.action_dim}.")
|
raise ValueError(f"`action_dim` must be positive, got {self.action_dim}.")
|
||||||
|
|||||||
@@ -14,24 +14,18 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import shutil
|
import logging
|
||||||
import warnings
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from pathlib import Path
|
from typing import Any
|
||||||
from typing import TYPE_CHECKING, Any
|
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
from torch import Tensor
|
from torch import Tensor
|
||||||
|
|
||||||
from lerobot.configs import PreTrainedConfig
|
|
||||||
from lerobot.policies.pretrained import PreTrainedPolicy
|
from lerobot.policies.pretrained import PreTrainedPolicy
|
||||||
from lerobot.utils.constants import ACTION, OBS_STATE
|
from lerobot.utils.constants import OBS_STATE
|
||||||
|
|
||||||
from .configuration_fastwam import FastWAMConfig
|
from .configuration_fastwam import FastWAMConfig
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .wan_components import WanCheckpointPaths
|
|
||||||
|
|
||||||
|
|
||||||
class FastWAMPolicy(PreTrainedPolicy):
|
class FastWAMPolicy(PreTrainedPolicy):
|
||||||
"""LeRobot policy wrapper for FastWAM.
|
"""LeRobot policy wrapper for FastWAM.
|
||||||
@@ -49,98 +43,62 @@ class FastWAMPolicy(PreTrainedPolicy):
|
|||||||
self,
|
self,
|
||||||
config: FastWAMConfig,
|
config: FastWAMConfig,
|
||||||
dataset_stats: dict[str, dict[str, Tensor]] | None = None,
|
dataset_stats: dict[str, dict[str, Tensor]] | None = None,
|
||||||
**kwargs: Any,
|
|
||||||
):
|
):
|
||||||
skip_wan_init = bool(kwargs.pop("_skip_wan_init", False))
|
|
||||||
super().__init__(config, dataset_stats)
|
super().__init__(config, dataset_stats)
|
||||||
config.validate_features()
|
config.validate_features()
|
||||||
self.config = config
|
self.config = config
|
||||||
self.dataset_stats = dataset_stats
|
self.dataset_stats = dataset_stats
|
||||||
suppress_base_init_warning = bool(kwargs.pop("_suppress_base_init_warning", False))
|
self.model = self._build_core_model(config)
|
||||||
if not skip_wan_init and not suppress_base_init_warning:
|
|
||||||
warnings.warn(
|
|
||||||
"FastWAMPolicy(config) initializes from architecture/config and does not load pretrained "
|
|
||||||
"FastWAM weights. For training or evaluation, use `make_policy(config)` or "
|
|
||||||
"`FastWAMPolicy.from_pretrained(...)`.",
|
|
||||||
RuntimeWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
if skip_wan_init:
|
|
||||||
self.model = _build_core_model_from_architecture(config)
|
|
||||||
else:
|
|
||||||
self.model = self._build_core_model(config)
|
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_pretrained(
|
def _load_as_safetensor(cls, model, model_file: str, map_location: str, strict: bool):
|
||||||
cls,
|
"""Shape-aware load that supports cross-embodiment fine-tuning.
|
||||||
pretrained_name_or_path: str | Path,
|
|
||||||
*,
|
|
||||||
config: FastWAMConfig | None = None,
|
|
||||||
force_download: bool = False,
|
|
||||||
resume_download: bool | None = None,
|
|
||||||
proxies: dict | None = None,
|
|
||||||
token: str | bool | None = None,
|
|
||||||
cache_dir: str | Path | None = None,
|
|
||||||
local_files_only: bool = False,
|
|
||||||
revision: str | None = None,
|
|
||||||
strict: bool = False,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> FastWAMPolicy:
|
|
||||||
"""Load FastWAM weights and local Wan components from one HF directory.
|
|
||||||
|
|
||||||
Args:
|
`safetensors.load_model(strict=False)` ignores missing/unexpected keys but
|
||||||
pretrained_name_or_path (str | Path): HF-format policy directory
|
still raises on a shape mismatch for a shared key. When fine-tuning from a
|
||||||
containing `config.json`, `model.safetensors`, local Wan VAE,
|
checkpoint trained on a different embodiment (e.g. the LIBERO 7-DoF / 8-dim
|
||||||
local UMT5 text encoder safetensors, and tokenizer files.
|
checkpoint adapted to a 6-DoF / 6-dim arm), the action encoder/head and
|
||||||
config (FastWAMConfig | None): Optional config override. When
|
proprio encoder legitimately differ in shape. With `strict=False` we drop
|
||||||
omitted, `config.json` is read from `pretrained_name_or_path`.
|
only those shape-mismatched tensors — leaving them at their freshly
|
||||||
force_download (bool): Forwarded to LeRobot's pretrained loader.
|
initialized values — and load every compatible tensor. With `strict=True`
|
||||||
resume_download (bool | None): Forwarded to LeRobot's pretrained loader.
|
the standard exact-match loader is used.
|
||||||
proxies (dict | None): Forwarded to LeRobot's pretrained loader.
|
|
||||||
token (str | bool | None): Forwarded to LeRobot's pretrained loader.
|
|
||||||
cache_dir (str | Path | None): Forwarded to LeRobot's pretrained loader.
|
|
||||||
local_files_only (bool): Forwarded to LeRobot's pretrained loader.
|
|
||||||
revision (str | None): Forwarded to LeRobot's pretrained loader.
|
|
||||||
strict (bool): Whether safetensors loading should require an exact
|
|
||||||
match between checkpoint keys and policy module keys.
|
|
||||||
**kwargs (Any): Extra constructor arguments forwarded to
|
|
||||||
`FastWAMPolicy`.
|
|
||||||
"""
|
"""
|
||||||
|
from safetensors import safe_open
|
||||||
|
|
||||||
pretrained_path = _resolve_pretrained_directory(
|
model_state_dict = model.state_dict()
|
||||||
pretrained_name_or_path=pretrained_name_or_path,
|
mismatched = []
|
||||||
force_download=force_download,
|
with safe_open(model_file, framework="pt") as f:
|
||||||
token=token,
|
checkpoint_keys = list(f.keys())
|
||||||
cache_dir=cache_dir,
|
for key in checkpoint_keys:
|
||||||
local_files_only=local_files_only,
|
if key in model_state_dict and tuple(model_state_dict[key].shape) != tuple(
|
||||||
revision=revision,
|
f.get_slice(key).get_shape()
|
||||||
)
|
):
|
||||||
if config is None:
|
mismatched.append(key)
|
||||||
config = PreTrainedConfig.from_pretrained(pretrained_path)
|
|
||||||
if not isinstance(config, FastWAMConfig):
|
|
||||||
raise TypeError(f"Expected FastWAM config, got {type(config).__name__}.")
|
|
||||||
kwargs["_skip_wan_init"] = True
|
|
||||||
policy = super().from_pretrained(
|
|
||||||
pretrained_path,
|
|
||||||
config=config,
|
|
||||||
force_download=force_download,
|
|
||||||
resume_download=resume_download,
|
|
||||||
proxies=proxies,
|
|
||||||
token=token,
|
|
||||||
cache_dir=cache_dir,
|
|
||||||
local_files_only=local_files_only,
|
|
||||||
revision=revision,
|
|
||||||
strict=strict,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
policy.load_wan_components_from_pretrained(pretrained_path)
|
|
||||||
policy.eval()
|
|
||||||
return policy
|
|
||||||
|
|
||||||
def _save_pretrained(self, save_directory: Path) -> None:
|
if not mismatched:
|
||||||
super()._save_pretrained(save_directory)
|
return super()._load_as_safetensor(model, model_file, map_location, strict)
|
||||||
_copy_wan_components_from_policy(policy=self, save_directory=save_directory)
|
if strict:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"FastWAM: {len(mismatched)} checkpoint tensors have a shape mismatch under "
|
||||||
|
f"strict=True: {mismatched}"
|
||||||
|
)
|
||||||
|
|
||||||
|
from safetensors.torch import load_file
|
||||||
|
|
||||||
|
logging.warning(
|
||||||
|
"FastWAM cross-embodiment load: reinitializing %d shape-mismatched tensor(s), keeping "
|
||||||
|
"every compatible weight: %s",
|
||||||
|
len(mismatched),
|
||||||
|
mismatched,
|
||||||
|
)
|
||||||
|
state_dict = load_file(model_file, device="cpu")
|
||||||
|
for key in mismatched:
|
||||||
|
state_dict.pop(key, None)
|
||||||
|
model.load_state_dict(state_dict, strict=False)
|
||||||
|
if map_location and map_location != "cpu":
|
||||||
|
model.to(map_location)
|
||||||
|
return model
|
||||||
|
|
||||||
def get_optim_params(self) -> dict[str, Any]:
|
def get_optim_params(self) -> dict[str, Any]:
|
||||||
params = (
|
params = (
|
||||||
@@ -151,21 +109,41 @@ class FastWAMPolicy(PreTrainedPolicy):
|
|||||||
params.extend(list(proprio_encoder.parameters()))
|
params.extend(list(proprio_encoder.parameters()))
|
||||||
return {"params": [p for p in params if p.requires_grad]}
|
return {"params": [p for p in params if p.requires_grad]}
|
||||||
|
|
||||||
def load_wan_components_from_pretrained(self, pretrained_name_or_path: str | Path) -> None:
|
|
||||||
"""Attach local Wan VAE, text encoder, and tokenizer from a HF directory.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pretrained_name_or_path (str | Path): Directory containing
|
|
||||||
`Wan2.2_VAE.safetensors`, `models_t5_umt5-xxl-enc-bf16.safetensors`,
|
|
||||||
and `google/umt5-xxl/` tokenizer files.
|
|
||||||
"""
|
|
||||||
|
|
||||||
paths = resolve_wan_component_paths(pretrained_name_or_path)
|
|
||||||
_load_wan_components_into_policy(policy=self, paths=paths)
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
self._action_queue: deque[Tensor] = deque([], maxlen=self.config.n_action_steps)
|
self._action_queue: deque[Tensor] = deque([], maxlen=self.config.n_action_steps)
|
||||||
|
|
||||||
|
def _batch_to_training_sample(self, batch: dict[str, Tensor]) -> dict[str, Tensor]:
|
||||||
|
"""Adapt a standard LeRobot batch to the FastWAM-native sample that
|
||||||
|
`FastWAM.build_inputs` consumes (`video`, `action`, `context`/`context_mask`,
|
||||||
|
per-frame `proprio`).
|
||||||
|
|
||||||
|
The LeRobot training loop passes raw `observation.images.*`, a single-step
|
||||||
|
`observation.state` `[B, D]`, `action`, and a language `task` string. We do
|
||||||
|
only the translation `build_inputs` can't: stack the camera frames into a
|
||||||
|
video, encode the prompt with the (frozen) text encoder (mirroring inference,
|
||||||
|
so language-conditioned datasets need no precomputed context), and give proprio
|
||||||
|
the per-frame axis `build_inputs` indexes. All shape/presence validation is
|
||||||
|
left to `build_inputs`, the single authority on the contract.
|
||||||
|
"""
|
||||||
|
sample = dict(batch)
|
||||||
|
if "video" not in sample:
|
||||||
|
sample["video"] = _stack_video_from_images(batch, self.config)
|
||||||
|
if "context" not in sample or "context_mask" not in sample:
|
||||||
|
prompt = _prompt_from_batch(batch=batch, config=self.config)
|
||||||
|
if prompt is None:
|
||||||
|
raise KeyError(
|
||||||
|
"FastWAM training requires a `task`/`prompt` to encode text context, "
|
||||||
|
"or precomputed `context`/`context_mask` in the batch."
|
||||||
|
)
|
||||||
|
sample["context"], sample["context_mask"] = self.model.encode_prompt(prompt)
|
||||||
|
if self.config.proprio_dim is not None and "proprio" not in sample:
|
||||||
|
state = sample.get(OBS_STATE)
|
||||||
|
if state is not None:
|
||||||
|
# LeRobot gives a single-step state [B, D]; build_inputs expects
|
||||||
|
# per-frame [B, T, D] and uses frame 0, so add a T=1 axis.
|
||||||
|
sample["proprio"] = state.unsqueeze(1) if state.ndim == 2 else state
|
||||||
|
return sample
|
||||||
|
|
||||||
def forward(self, batch: dict[str, Tensor]) -> dict[str, Tensor]:
|
def forward(self, batch: dict[str, Tensor]) -> dict[str, Tensor]:
|
||||||
"""Compute FastWAM training loss for a LeRobot batch.
|
"""Compute FastWAM training loss for a LeRobot batch.
|
||||||
|
|
||||||
@@ -180,7 +158,7 @@ class FastWAMPolicy(PreTrainedPolicy):
|
|||||||
key required by LeRobot and optional tensor metrics.
|
key required by LeRobot and optional tensor metrics.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sample = _batch_to_training_sample(batch=batch, config=self.config)
|
sample = self._batch_to_training_sample(batch)
|
||||||
loss, metrics = self.model.training_loss(sample)
|
loss, metrics = self.model.training_loss(sample)
|
||||||
output = {"loss": loss}
|
output = {"loss": loss}
|
||||||
for key, value in (metrics or {}).items():
|
for key, value in (metrics or {}).items():
|
||||||
@@ -230,223 +208,57 @@ class FastWAMPolicy(PreTrainedPolicy):
|
|||||||
return self._action_queue.popleft()
|
return self._action_queue.popleft()
|
||||||
|
|
||||||
def _build_core_model(self, config: FastWAMConfig) -> torch.nn.Module:
|
def _build_core_model(self, config: FastWAMConfig) -> torch.nn.Module:
|
||||||
return _build_core_model_from_wan22(config)
|
"""Build the FastWAM core for training / inference.
|
||||||
|
|
||||||
|
Only the trainable parts (the MoT DiT and the proprio encoder) are
|
||||||
def _resolve_pretrained_directory(
|
materialized empty here and then filled from the policy's
|
||||||
pretrained_name_or_path: str | Path,
|
`model.safetensors` by the base `from_pretrained`. The *frozen* Wan2.2 VAE
|
||||||
*,
|
and UMT5 text encoder are loaded with their real weights from the
|
||||||
force_download: bool,
|
`Wan-AI/Wan2.2-TI2V-5B-Diffusers` repo (cached in the HF cache, shared
|
||||||
token: str | bool | None,
|
across checkpoints) and are intentionally excluded from `model.safetensors`
|
||||||
cache_dir: str | Path | None,
|
— see `FastWAM.__init__`. The tokenizer comes from `google/umt5-xxl`.
|
||||||
local_files_only: bool,
|
"""
|
||||||
revision: str | None,
|
from .modular_fastwam import ActionDiT, FastWAM, MoT
|
||||||
) -> Path:
|
from .wan_components import (
|
||||||
path = Path(pretrained_name_or_path)
|
build_wan_tokenizer,
|
||||||
if path.is_dir():
|
load_pretrained_wan_text_encoder,
|
||||||
return path
|
load_pretrained_wan_vae,
|
||||||
|
|
||||||
from huggingface_hub import snapshot_download
|
|
||||||
|
|
||||||
from .wan_components import (
|
|
||||||
WAN_T5_CHECKPOINT,
|
|
||||||
WAN_T5_TOKENIZER,
|
|
||||||
WAN_VAE_CHECKPOINT,
|
|
||||||
)
|
|
||||||
|
|
||||||
snapshot_path = snapshot_download(
|
|
||||||
repo_id=str(pretrained_name_or_path),
|
|
||||||
revision=revision,
|
|
||||||
cache_dir=cache_dir,
|
|
||||||
force_download=force_download,
|
|
||||||
token=token,
|
|
||||||
local_files_only=local_files_only,
|
|
||||||
allow_patterns=[
|
|
||||||
"config.json",
|
|
||||||
"model.safetensors",
|
|
||||||
WAN_VAE_CHECKPOINT,
|
|
||||||
WAN_T5_CHECKPOINT,
|
|
||||||
f"{WAN_T5_TOKENIZER}/**",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
return Path(snapshot_path)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_wan_component_paths(pretrained_name_or_path: str | Path) -> WanCheckpointPaths:
|
|
||||||
"""Resolve local Wan component paths stored beside FastWAM HF weights.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pretrained_name_or_path (str | Path): HF-format FastWAM directory.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
WanCheckpointPaths: Existing VAE, text encoder, and tokenizer paths.
|
|
||||||
DiT shards are intentionally optional here because FastWAM HF
|
|
||||||
checkpoints store trainable DiT weights in `model.safetensors`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from .wan_components import resolve_wan_checkpoint_paths
|
|
||||||
|
|
||||||
return resolve_wan_checkpoint_paths(
|
|
||||||
pretrained_name_or_path,
|
|
||||||
load_dit=False,
|
|
||||||
load_text_encoder=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _load_wan_components_into_policy(policy: FastWAMPolicy, paths: WanCheckpointPaths) -> None:
|
|
||||||
from .wan_components import load_wan_text_encoder, load_wan_tokenizer, load_wan_vae
|
|
||||||
|
|
||||||
if paths.text_encoder is None or paths.tokenizer is None:
|
|
||||||
raise FileNotFoundError("FastWAM HF checkpoint requires Wan text encoder and tokenizer sidecars.")
|
|
||||||
dtype = _dtype_from_name(policy.config.torch_dtype)
|
|
||||||
device = str(policy.config.device)
|
|
||||||
policy.model.vae = load_wan_vae(paths.vae, torch_dtype=dtype, device=device)
|
|
||||||
policy.model.text_encoder = load_wan_text_encoder(paths.text_encoder, torch_dtype=dtype, device=device)
|
|
||||||
policy.model.tokenizer = load_wan_tokenizer(
|
|
||||||
paths.tokenizer,
|
|
||||||
tokenizer_max_len=int(policy.config.tokenizer_max_len),
|
|
||||||
)
|
|
||||||
model_paths = dict(getattr(policy.model, "model_paths", {}) or {})
|
|
||||||
model_paths.update(
|
|
||||||
{
|
|
||||||
"vae": str(paths.vae),
|
|
||||||
"text_encoder": str(paths.text_encoder),
|
|
||||||
"tokenizer": str(paths.tokenizer),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
policy.model.model_paths = model_paths
|
|
||||||
|
|
||||||
|
|
||||||
def _copy_wan_components_from_policy(policy: FastWAMPolicy, save_directory: Path) -> None:
|
|
||||||
model_paths = getattr(policy.model, "model_paths", {}) or {}
|
|
||||||
paths = {
|
|
||||||
"vae": model_paths.get("vae"),
|
|
||||||
"text_encoder": model_paths.get("text_encoder"),
|
|
||||||
"tokenizer": model_paths.get("tokenizer"),
|
|
||||||
}
|
|
||||||
missing = [name for name, path in paths.items() if path is None]
|
|
||||||
if missing:
|
|
||||||
raise RuntimeError(
|
|
||||||
"FastWAM save_pretrained requires local Wan component paths for "
|
|
||||||
f"{missing}. Load or initialize the policy with local Wan VAE, text encoder, and tokenizer files."
|
|
||||||
)
|
)
|
||||||
_copy_component_path(Path(paths["vae"]), save_directory / Path(paths["vae"]).name)
|
from .wan_video_dit import WanVideoDiT
|
||||||
_copy_component_path(Path(paths["text_encoder"]), save_directory / Path(paths["text_encoder"]).name)
|
|
||||||
tokenizer_source = Path(paths["tokenizer"])
|
|
||||||
_copy_component_path(tokenizer_source, save_directory / "google" / "umt5-xxl")
|
|
||||||
|
|
||||||
|
dtype = _dtype_from_name(config.torch_dtype)
|
||||||
def _copy_component_path(source: Path, destination: Path) -> None:
|
device = config.device
|
||||||
source = source.expanduser()
|
video_expert = WanVideoDiT(**config.video_dit_config).to(device=device, dtype=dtype)
|
||||||
if not source.exists():
|
action_expert = ActionDiT(**config.action_dit_config).to(device=device, dtype=dtype)
|
||||||
raise FileNotFoundError(f"FastWAM component path does not exist: {source}")
|
mot = MoT(
|
||||||
if source.resolve() == destination.resolve():
|
mixtures={"video": video_expert, "action": action_expert},
|
||||||
return
|
mot_checkpoint_mixed_attn=config.mot_checkpoint_mixed_attn,
|
||||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
if source.is_dir():
|
|
||||||
shutil.copytree(source, destination, dirs_exist_ok=True)
|
|
||||||
else:
|
|
||||||
shutil.copy2(source, destination)
|
|
||||||
|
|
||||||
|
|
||||||
def _build_core_model_from_wan22(config: FastWAMConfig) -> torch.nn.Module:
|
|
||||||
from .modular_fastwam import FastWAM
|
|
||||||
|
|
||||||
dtype = _dtype_from_name(config.torch_dtype)
|
|
||||||
return FastWAM.from_wan22_pretrained(
|
|
||||||
device=config.device,
|
|
||||||
torch_dtype=dtype,
|
|
||||||
model_id=config.model_id,
|
|
||||||
tokenizer_model_id=config.tokenizer_model_id,
|
|
||||||
tokenizer_max_len=config.tokenizer_max_len,
|
|
||||||
load_text_encoder=config.load_text_encoder,
|
|
||||||
proprio_dim=config.proprio_dim,
|
|
||||||
video_dit_config=config.video_dit_config,
|
|
||||||
action_dit_config=config.action_dit_config,
|
|
||||||
mot_checkpoint_mixed_attn=config.mot_checkpoint_mixed_attn,
|
|
||||||
video_train_shift=float(config.video_scheduler["train_shift"]),
|
|
||||||
video_infer_shift=float(config.video_scheduler["infer_shift"]),
|
|
||||||
video_num_train_timesteps=int(config.video_scheduler["num_train_timesteps"]),
|
|
||||||
action_train_shift=float(config.action_scheduler["train_shift"]),
|
|
||||||
action_infer_shift=float(config.action_scheduler["infer_shift"]),
|
|
||||||
action_num_train_timesteps=int(config.action_scheduler["num_train_timesteps"]),
|
|
||||||
loss_lambda_video=float(config.loss["lambda_video"]),
|
|
||||||
loss_lambda_action=float(config.loss["lambda_action"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _build_core_model_from_architecture(config: FastWAMConfig) -> torch.nn.Module:
|
|
||||||
from .modular_fastwam import ActionDiT, FastWAM, MoT
|
|
||||||
from .wan_video_dit import WanVideoDiT
|
|
||||||
|
|
||||||
dtype = _dtype_from_name(config.torch_dtype)
|
|
||||||
video_expert = WanVideoDiT(**config.video_dit_config).to(device=config.device, dtype=dtype)
|
|
||||||
action_expert = ActionDiT(**config.action_dit_config).to(device=config.device, dtype=dtype)
|
|
||||||
mot = MoT(
|
|
||||||
mixtures={"video": video_expert, "action": action_expert},
|
|
||||||
mot_checkpoint_mixed_attn=config.mot_checkpoint_mixed_attn,
|
|
||||||
)
|
|
||||||
return FastWAM(
|
|
||||||
video_expert=video_expert,
|
|
||||||
action_expert=action_expert,
|
|
||||||
mot=mot,
|
|
||||||
vae=_FastWAMVAEPlaceholder(),
|
|
||||||
text_encoder=None,
|
|
||||||
tokenizer=None,
|
|
||||||
text_dim=int(config.video_dit_config["text_dim"]),
|
|
||||||
proprio_dim=config.proprio_dim,
|
|
||||||
device=config.device,
|
|
||||||
torch_dtype=dtype,
|
|
||||||
video_train_shift=float(config.video_scheduler["train_shift"]),
|
|
||||||
video_infer_shift=float(config.video_scheduler["infer_shift"]),
|
|
||||||
video_num_train_timesteps=int(config.video_scheduler["num_train_timesteps"]),
|
|
||||||
action_train_shift=float(config.action_scheduler["train_shift"]),
|
|
||||||
action_infer_shift=float(config.action_scheduler["infer_shift"]),
|
|
||||||
action_num_train_timesteps=int(config.action_scheduler["num_train_timesteps"]),
|
|
||||||
loss_lambda_video=float(config.loss["lambda_video"]),
|
|
||||||
loss_lambda_action=float(config.loss["lambda_action"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class _FastWAMVAEPlaceholder(torch.nn.Module):
|
|
||||||
"""Minimal VAE placeholder for checkpoint loading without Wan2.2 VAE.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
temporal_downsample_factor (int): Temporal compression factor expected
|
|
||||||
by FastWAM latent shape logic.
|
|
||||||
upsampling_factor (int): Spatial compression factor expected by FastWAM.
|
|
||||||
z_dim (int): Latent channel count used by Wan2.2 TI2V VAE.
|
|
||||||
"""
|
|
||||||
|
|
||||||
temporal_downsample_factor: int = 4
|
|
||||||
upsampling_factor: int = 8
|
|
||||||
|
|
||||||
def __init__(self, z_dim: int = 48):
|
|
||||||
super().__init__()
|
|
||||||
self.model = type("VAEModelShape", (), {"z_dim": int(z_dim)})()
|
|
||||||
|
|
||||||
def encode(self, *args, **kwargs):
|
|
||||||
raise RuntimeError(
|
|
||||||
"FastWAM VAE placeholder cannot encode images; load Wan2.2 VAE for image inference."
|
|
||||||
)
|
)
|
||||||
|
text_encoder = (
|
||||||
def decode(self, *args, **kwargs):
|
load_pretrained_wan_text_encoder(torch_dtype=dtype, device=device)
|
||||||
raise RuntimeError(
|
if config.load_text_encoder
|
||||||
"FastWAM VAE placeholder cannot decode latents; load Wan2.2 VAE for video inference."
|
else None
|
||||||
|
)
|
||||||
|
return FastWAM(
|
||||||
|
video_expert=video_expert,
|
||||||
|
action_expert=action_expert,
|
||||||
|
mot=mot,
|
||||||
|
vae=load_pretrained_wan_vae(torch_dtype=dtype, device=device),
|
||||||
|
text_encoder=text_encoder,
|
||||||
|
tokenizer=build_wan_tokenizer(tokenizer_max_len=config.tokenizer_max_len),
|
||||||
|
text_dim=int(config.video_dit_config["text_dim"]),
|
||||||
|
proprio_dim=config.proprio_dim,
|
||||||
|
device=device,
|
||||||
|
torch_dtype=dtype,
|
||||||
|
video_train_shift=float(config.video_scheduler["train_shift"]),
|
||||||
|
video_infer_shift=float(config.video_scheduler["infer_shift"]),
|
||||||
|
video_num_train_timesteps=int(config.video_scheduler["num_train_timesteps"]),
|
||||||
|
action_train_shift=float(config.action_scheduler["train_shift"]),
|
||||||
|
action_infer_shift=float(config.action_scheduler["infer_shift"]),
|
||||||
|
action_num_train_timesteps=int(config.action_scheduler["num_train_timesteps"]),
|
||||||
|
loss_lambda_video=float(config.loss["lambda_video"]),
|
||||||
|
loss_lambda_action=float(config.loss["lambda_action"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _batch_to_training_sample(batch: dict[str, Tensor], config: FastWAMConfig) -> dict[str, Tensor]:
|
|
||||||
sample = dict(batch)
|
|
||||||
if "video" not in sample:
|
|
||||||
sample["video"] = _stack_video_from_images(batch, config)
|
|
||||||
if "proprio" not in sample and OBS_STATE in batch:
|
|
||||||
sample["proprio"] = batch[OBS_STATE]
|
|
||||||
required = {"video", ACTION, "context", "context_mask"}
|
|
||||||
missing = sorted(required - set(sample))
|
|
||||||
if missing:
|
|
||||||
raise KeyError(f"FastWAM training batch is missing keys: {missing}.")
|
|
||||||
return sample
|
|
||||||
|
|
||||||
|
|
||||||
def _batch_to_infer_kwargs(batch: dict[str, Tensor], config: FastWAMConfig) -> dict[str, Any]:
|
def _batch_to_infer_kwargs(batch: dict[str, Tensor], config: FastWAMConfig) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -24,7 +24,13 @@ import torch.nn as nn
|
|||||||
import torch.nn.functional as functional
|
import torch.nn.functional as functional
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from .wan_components import load_wan22_ti2v_5b_components
|
from .wan_components import (
|
||||||
|
build_wan_tokenizer,
|
||||||
|
load_pretrained_wan_text_encoder,
|
||||||
|
load_pretrained_wan_vae,
|
||||||
|
load_wan_video_dit,
|
||||||
|
resolve_wan_dit_paths,
|
||||||
|
)
|
||||||
from .wan_video_dit import (
|
from .wan_video_dit import (
|
||||||
FastWAMAttentionBlock,
|
FastWAMAttentionBlock,
|
||||||
WanContinuousFlowMatchScheduler,
|
WanContinuousFlowMatchScheduler,
|
||||||
@@ -846,9 +852,17 @@ class FastWAM(torch.nn.Module):
|
|||||||
# Keep trainer compatibility: optimizer and freeze logic use `model.dit`.
|
# Keep trainer compatibility: optimizer and freeze logic use `model.dit`.
|
||||||
self.dit = self.mot
|
self.dit = self.mot
|
||||||
|
|
||||||
self.vae = vae
|
# Frozen Wan2.2 components: bypass `nn.Module.__setattr__` so they are NOT
|
||||||
self.text_encoder = text_encoder
|
# registered as submodules. They are therefore excluded from `state_dict()`
|
||||||
|
# (lean checkpoints), `parameters()`, and DDP gradient sync, and are loaded
|
||||||
|
# with their real weights from the diffusers/transformers repos at construction.
|
||||||
|
# Device/dtype moves still reach them via the `_apply` override below.
|
||||||
|
object.__setattr__(self, "vae", vae)
|
||||||
|
object.__setattr__(self, "text_encoder", text_encoder)
|
||||||
self.tokenizer = tokenizer
|
self.tokenizer = tokenizer
|
||||||
|
vae.requires_grad_(False)
|
||||||
|
if text_encoder is not None:
|
||||||
|
text_encoder.requires_grad_(False)
|
||||||
if text_dim is None:
|
if text_dim is None:
|
||||||
if self.text_encoder is None:
|
if self.text_encoder is None:
|
||||||
raise ValueError("`text_dim` is required when `text_encoder` is not loaded.")
|
raise ValueError("`text_dim` is required when `text_encoder` is not loaded.")
|
||||||
@@ -913,18 +927,17 @@ class FastWAM(torch.nn.Module):
|
|||||||
raise ValueError("`video_dit_config` is required for FastWAM.from_wan22_pretrained().")
|
raise ValueError("`video_dit_config` is required for FastWAM.from_wan22_pretrained().")
|
||||||
if "text_dim" not in video_dit_config:
|
if "text_dim" not in video_dit_config:
|
||||||
raise ValueError("`video_dit_config['text_dim']` is required for FastWAM.")
|
raise ValueError("`video_dit_config['text_dim']` is required for FastWAM.")
|
||||||
|
del tokenizer_model_id # tokenizer is the stock UMT5 one (google/umt5-xxl)
|
||||||
|
|
||||||
components = load_wan22_ti2v_5b_components(
|
# Custom MoT video DiT from the original Wan2.2 repo; frozen VAE / UMT5 from
|
||||||
device=device,
|
# the diffusers conversion. This is the offline base-creation path; the
|
||||||
torch_dtype=torch_dtype,
|
# weights it loads are then bundled into the FastWAM `model.safetensors`.
|
||||||
model_id=model_id,
|
video_expert = load_wan_video_dit(
|
||||||
tokenizer_model_id=tokenizer_model_id,
|
resolve_wan_dit_paths(model_id),
|
||||||
tokenizer_max_len=tokenizer_max_len,
|
|
||||||
dit_config=video_dit_config,
|
dit_config=video_dit_config,
|
||||||
load_text_encoder=load_text_encoder,
|
torch_dtype=torch_dtype,
|
||||||
|
device=device,
|
||||||
)
|
)
|
||||||
|
|
||||||
video_expert = components.dit
|
|
||||||
action_expert = ActionDiT(**action_dit_config).to(device=device, dtype=torch_dtype)
|
action_expert = ActionDiT(**action_dit_config).to(device=device, dtype=torch_dtype)
|
||||||
if int(action_expert.num_heads) != int(video_expert.num_heads):
|
if int(action_expert.num_heads) != int(video_expert.num_heads):
|
||||||
raise ValueError("ActionDiT `num_heads` must match video expert for MoT mixed attention.")
|
raise ValueError("ActionDiT `num_heads` must match video expert for MoT mixed attention.")
|
||||||
@@ -938,13 +951,21 @@ class FastWAM(torch.nn.Module):
|
|||||||
mot_checkpoint_mixed_attn=mot_checkpoint_mixed_attn,
|
mot_checkpoint_mixed_attn=mot_checkpoint_mixed_attn,
|
||||||
)
|
)
|
||||||
|
|
||||||
model = cls(
|
vae = load_pretrained_wan_vae(torch_dtype=torch_dtype, device=device)
|
||||||
|
text_encoder = (
|
||||||
|
load_pretrained_wan_text_encoder(torch_dtype=torch_dtype, device=device)
|
||||||
|
if load_text_encoder
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
tokenizer = build_wan_tokenizer(tokenizer_max_len=tokenizer_max_len)
|
||||||
|
|
||||||
|
return cls(
|
||||||
video_expert=video_expert,
|
video_expert=video_expert,
|
||||||
action_expert=action_expert,
|
action_expert=action_expert,
|
||||||
mot=mot,
|
mot=mot,
|
||||||
vae=components.vae,
|
vae=vae,
|
||||||
text_encoder=components.text_encoder,
|
text_encoder=text_encoder,
|
||||||
tokenizer=components.tokenizer,
|
tokenizer=tokenizer,
|
||||||
text_dim=int(video_dit_config["text_dim"]),
|
text_dim=int(video_dit_config["text_dim"]),
|
||||||
proprio_dim=proprio_dim,
|
proprio_dim=proprio_dim,
|
||||||
device=device,
|
device=device,
|
||||||
@@ -958,20 +979,17 @@ class FastWAM(torch.nn.Module):
|
|||||||
loss_lambda_video=loss_lambda_video,
|
loss_lambda_video=loss_lambda_video,
|
||||||
loss_lambda_action=loss_lambda_action,
|
loss_lambda_action=loss_lambda_action,
|
||||||
)
|
)
|
||||||
model.model_paths = {
|
|
||||||
"video_dit": components.dit_path,
|
|
||||||
"vae": components.vae_path,
|
|
||||||
"text_encoder": components.text_encoder_path,
|
|
||||||
"tokenizer": components.tokenizer_path,
|
|
||||||
}
|
|
||||||
return model
|
|
||||||
|
|
||||||
def to(self, *args, **kwargs):
|
def _apply(self, fn, *args, **kwargs):
|
||||||
super().to(*args, **kwargs)
|
# `.to()` / `.cuda()` / `.cpu()` and accelerate/DDP device moves all funnel
|
||||||
self.mot.to(*args, **kwargs)
|
# through `_apply`, and the parent policy reaches us via `child._apply(fn)`
|
||||||
|
# (not `child.to()`). Propagate `fn` to the *unregistered* frozen VAE / text
|
||||||
|
# encoder here so they follow the rest of the model onto the right device,
|
||||||
|
# while staying out of `state_dict()` / `parameters()`.
|
||||||
|
super()._apply(fn, *args, **kwargs)
|
||||||
|
self.vae._apply(fn)
|
||||||
if self.text_encoder is not None:
|
if self.text_encoder is not None:
|
||||||
self.text_encoder.to(*args, **kwargs)
|
self.text_encoder._apply(fn)
|
||||||
self.vae.to(*args, **kwargs)
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -1628,7 +1646,7 @@ class FastWAM(torch.nn.Module):
|
|||||||
latent_w = width // self.vae.upsampling_factor
|
latent_w = width // self.vae.upsampling_factor
|
||||||
generator = None if seed is None else torch.Generator(device=rand_device).manual_seed(seed)
|
generator = None if seed is None else torch.Generator(device=rand_device).manual_seed(seed)
|
||||||
return torch.randn(
|
return torch.randn(
|
||||||
(1, self.vae.model.z_dim, latent_t, latent_h, latent_w),
|
(1, self.vae.z_dim, latent_t, latent_h, latent_w),
|
||||||
generator=generator,
|
generator=generator,
|
||||||
device=rand_device,
|
device=rand_device,
|
||||||
dtype=torch.float32,
|
dtype=torch.float32,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from lerobot.processor import (
|
|||||||
ActionProcessorStep,
|
ActionProcessorStep,
|
||||||
AddBatchDimensionProcessorStep,
|
AddBatchDimensionProcessorStep,
|
||||||
DeviceProcessorStep,
|
DeviceProcessorStep,
|
||||||
|
ImageCropResizeProcessorStep,
|
||||||
NormalizerProcessorStep,
|
NormalizerProcessorStep,
|
||||||
PolicyAction,
|
PolicyAction,
|
||||||
PolicyProcessorPipeline,
|
PolicyProcessorPipeline,
|
||||||
@@ -90,9 +91,8 @@ def make_fastwam_pre_post_processors(
|
|||||||
output processor pipelines discoverable by LeRobot.
|
output processor pipelines discoverable by LeRobot.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
normalization_stats: dict[str, dict[str, Any]] = {
|
# force visual stats to be mean 0.5 and std 0.5 to map [0, 1] data to [-1, 1]
|
||||||
key: dict(value) for key, value in (dataset_stats or {}).items()
|
normalization_stats: dict[str, dict[str, Any]] = dict(dataset_stats or {})
|
||||||
}
|
|
||||||
for key, feature in config.input_features.items():
|
for key, feature in config.input_features.items():
|
||||||
if feature.type != FeatureType.VISUAL:
|
if feature.type != FeatureType.VISUAL:
|
||||||
continue
|
continue
|
||||||
@@ -101,10 +101,23 @@ def make_fastwam_pre_post_processors(
|
|||||||
"mean": torch.full((channels, 1, 1), 0.5, dtype=torch.float32),
|
"mean": torch.full((channels, 1, 1), 0.5, dtype=torch.float32),
|
||||||
"std": torch.full((channels, 1, 1), 0.5, dtype=torch.float32),
|
"std": torch.full((channels, 1, 1), 0.5, dtype=torch.float32),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# resize visual inputs to match model expected input size, if necessary
|
||||||
|
visual_shapes = [
|
||||||
|
feature.shape
|
||||||
|
for feature in config.input_features.values()
|
||||||
|
if feature.type == FeatureType.VISUAL
|
||||||
|
]
|
||||||
|
resize_steps = []
|
||||||
|
if visual_shapes:
|
||||||
|
target_hw = (int(visual_shapes[0][1]), int(visual_shapes[0][2]))
|
||||||
|
resize_steps.append(ImageCropResizeProcessorStep(resize_size=target_hw))
|
||||||
|
|
||||||
input_steps = [
|
input_steps = [
|
||||||
RenameObservationsProcessorStep(rename_map={}),
|
RenameObservationsProcessorStep(rename_map={}),
|
||||||
AddBatchDimensionProcessorStep(),
|
AddBatchDimensionProcessorStep(),
|
||||||
DeviceProcessorStep(device=config.device),
|
DeviceProcessorStep(device=config.device),
|
||||||
|
*resize_steps,
|
||||||
NormalizerProcessorStep(
|
NormalizerProcessorStep(
|
||||||
features={**config.input_features, **config.output_features},
|
features={**config.input_features, **config.output_features},
|
||||||
norm_map=config.normalization_mapping,
|
norm_map=config.normalization_mapping,
|
||||||
|
|||||||
@@ -10,17 +10,16 @@ Copied files:
|
|||||||
|
|
||||||
- `wan/modules/attention.py`
|
- `wan/modules/attention.py`
|
||||||
- `wan/modules/model.py`
|
- `wan/modules/model.py`
|
||||||
- `wan/modules/t5.py`
|
|
||||||
- `wan/modules/tokenizers.py`
|
|
||||||
- `wan/modules/vae2_2.py`
|
|
||||||
- `wan/modules/__init__.py`
|
- `wan/modules/__init__.py`
|
||||||
- `wan/utils/fm_solvers.py`
|
- `wan/utils/fm_solvers.py`
|
||||||
- `wan/utils/__init__.py`
|
- `wan/utils/__init__.py`
|
||||||
|
|
||||||
FastWAM-specific model glue and any larger code adapted from these modules live outside this directory. This keeps the upstream Wan2.2 code reviewable as a vendored reference subset and makes it straightforward to replace this directory with an external Wan2.2 dependency by changing import paths.
|
This subset now only backs FastWAM's **custom MoT video DiT**. The Wan2.2 VAE,
|
||||||
|
UMT5 text encoder, and tokenizer are no longer vendored — they come from
|
||||||
|
`diffusers.AutoencoderKLWan`, `transformers.UMT5EncoderModel`, and
|
||||||
|
`transformers.AutoTokenizer` (see `../wan_adapters.py` and `../wan_components.py`).
|
||||||
|
|
||||||
Current FastWAM adapters that directly reuse this vendored subset:
|
Current FastWAM adapters that directly reuse this vendored subset:
|
||||||
|
|
||||||
- `../wan_components.py` instantiates the upstream `wan.modules.t5.umt5_xxl` encoder factory and uses `wan.modules.tokenizers.HuggingfaceTokenizer`.
|
- `../wan_video_dit.py` builds on `wan.modules.model` (`sinusoidal_embedding_1d`, `rope_params`, `rope_apply`, …) and `wan.modules.attention.flash_attention`.
|
||||||
- `../wan_adapters.py` wraps `wan.modules.vae2_2.Wan2VAE` with the FastWAM tensor-batch encode/decode API.
|
|
||||||
- `../modular_fastwam.py` reuses `wan.utils.fm_solvers.get_sampling_sigmas` for Wan-compatible inference timesteps.
|
- `../modular_fastwam.py` reuses `wan.utils.fm_solvers.get_sampling_sigmas` for Wan-compatible inference timesteps.
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
|
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
|
||||||
from .attention import flash_attention
|
from .attention import flash_attention
|
||||||
from .model import WanModel
|
from .model import WanModel
|
||||||
from .t5 import T5Decoder, T5Encoder, T5EncoderModel, T5Model
|
|
||||||
from .tokenizers import HuggingfaceTokenizer
|
|
||||||
from .vae2_2 import Wan2VAE
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Wan2VAE",
|
|
||||||
"WanModel",
|
"WanModel",
|
||||||
"T5Model",
|
|
||||||
"T5Encoder",
|
|
||||||
"T5Decoder",
|
|
||||||
"T5EncoderModel",
|
|
||||||
"HuggingfaceTokenizer",
|
|
||||||
"flash_attention",
|
"flash_attention",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,489 +0,0 @@
|
|||||||
# Modified from transformers.models.t5.modeling_t5
|
|
||||||
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
|
|
||||||
import logging
|
|
||||||
import math
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
import torch.nn.functional as functional
|
|
||||||
|
|
||||||
from .tokenizers import HuggingfaceTokenizer
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"T5Model",
|
|
||||||
"T5Encoder",
|
|
||||||
"T5Decoder",
|
|
||||||
"T5EncoderModel",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def fp16_clamp(x):
|
|
||||||
if x.dtype == torch.float16 and torch.isinf(x).any():
|
|
||||||
clamp = torch.finfo(x.dtype).max - 1000
|
|
||||||
x = torch.clamp(x, min=-clamp, max=clamp)
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def init_weights(m):
|
|
||||||
if isinstance(m, T5LayerNorm):
|
|
||||||
nn.init.ones_(m.weight)
|
|
||||||
elif isinstance(m, T5Model):
|
|
||||||
nn.init.normal_(m.token_embedding.weight, std=1.0)
|
|
||||||
elif isinstance(m, T5FeedForward):
|
|
||||||
nn.init.normal_(m.gate[0].weight, std=m.dim**-0.5)
|
|
||||||
nn.init.normal_(m.fc1.weight, std=m.dim**-0.5)
|
|
||||||
nn.init.normal_(m.fc2.weight, std=m.dim_ffn**-0.5)
|
|
||||||
elif isinstance(m, T5Attention):
|
|
||||||
nn.init.normal_(m.q.weight, std=(m.dim * m.dim_attn) ** -0.5)
|
|
||||||
nn.init.normal_(m.k.weight, std=m.dim**-0.5)
|
|
||||||
nn.init.normal_(m.v.weight, std=m.dim**-0.5)
|
|
||||||
nn.init.normal_(m.o.weight, std=(m.num_heads * m.dim_attn) ** -0.5)
|
|
||||||
elif isinstance(m, T5RelativeEmbedding):
|
|
||||||
nn.init.normal_(m.embedding.weight, std=(2 * m.num_buckets * m.num_heads) ** -0.5)
|
|
||||||
|
|
||||||
|
|
||||||
class GELU(nn.Module):
|
|
||||||
def forward(self, x):
|
|
||||||
return 0.5 * x * (1.0 + torch.tanh(math.sqrt(2.0 / math.pi) * (x + 0.044715 * torch.pow(x, 3.0))))
|
|
||||||
|
|
||||||
|
|
||||||
class T5LayerNorm(nn.Module):
|
|
||||||
def __init__(self, dim, eps=1e-6):
|
|
||||||
super().__init__()
|
|
||||||
self.dim = dim
|
|
||||||
self.eps = eps
|
|
||||||
self.weight = nn.Parameter(torch.ones(dim))
|
|
||||||
|
|
||||||
def forward(self, x):
|
|
||||||
x = x * torch.rsqrt(x.float().pow(2).mean(dim=-1, keepdim=True) + self.eps)
|
|
||||||
if self.weight.dtype in [torch.float16, torch.bfloat16]:
|
|
||||||
x = x.type_as(self.weight)
|
|
||||||
return self.weight * x
|
|
||||||
|
|
||||||
|
|
||||||
class T5Attention(nn.Module):
|
|
||||||
def __init__(self, dim, dim_attn, num_heads, dropout=0.1):
|
|
||||||
assert dim_attn % num_heads == 0
|
|
||||||
super().__init__()
|
|
||||||
self.dim = dim
|
|
||||||
self.dim_attn = dim_attn
|
|
||||||
self.num_heads = num_heads
|
|
||||||
self.head_dim = dim_attn // num_heads
|
|
||||||
|
|
||||||
# layers
|
|
||||||
self.q = nn.Linear(dim, dim_attn, bias=False)
|
|
||||||
self.k = nn.Linear(dim, dim_attn, bias=False)
|
|
||||||
self.v = nn.Linear(dim, dim_attn, bias=False)
|
|
||||||
self.o = nn.Linear(dim_attn, dim, bias=False)
|
|
||||||
self.dropout = nn.Dropout(dropout)
|
|
||||||
|
|
||||||
def forward(self, x, context=None, mask=None, pos_bias=None):
|
|
||||||
"""
|
|
||||||
x: [B, L1, C].
|
|
||||||
context: [B, L2, C] or None.
|
|
||||||
mask: [B, L2] or [B, L1, L2] or None.
|
|
||||||
"""
|
|
||||||
# check inputs
|
|
||||||
context = x if context is None else context
|
|
||||||
b, n, c = x.size(0), self.num_heads, self.head_dim
|
|
||||||
|
|
||||||
# compute query, key, value
|
|
||||||
q = self.q(x).view(b, -1, n, c)
|
|
||||||
k = self.k(context).view(b, -1, n, c)
|
|
||||||
v = self.v(context).view(b, -1, n, c)
|
|
||||||
|
|
||||||
# attention bias
|
|
||||||
attn_bias = x.new_zeros(b, n, q.size(1), k.size(1))
|
|
||||||
if pos_bias is not None:
|
|
||||||
attn_bias += pos_bias
|
|
||||||
if mask is not None:
|
|
||||||
assert mask.ndim in [2, 3]
|
|
||||||
mask = mask.view(b, 1, 1, -1) if mask.ndim == 2 else mask.unsqueeze(1)
|
|
||||||
attn_bias.masked_fill_(mask == 0, torch.finfo(x.dtype).min)
|
|
||||||
|
|
||||||
# compute attention (T5 does not use scaling)
|
|
||||||
attn = torch.einsum("binc,bjnc->bnij", q, k) + attn_bias
|
|
||||||
attn = functional.softmax(attn.float(), dim=-1).type_as(attn)
|
|
||||||
x = torch.einsum("bnij,bjnc->binc", attn, v)
|
|
||||||
|
|
||||||
# output
|
|
||||||
x = x.reshape(b, -1, n * c)
|
|
||||||
x = self.o(x)
|
|
||||||
x = self.dropout(x)
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
class T5FeedForward(nn.Module):
|
|
||||||
def __init__(self, dim, dim_ffn, dropout=0.1):
|
|
||||||
super().__init__()
|
|
||||||
self.dim = dim
|
|
||||||
self.dim_ffn = dim_ffn
|
|
||||||
|
|
||||||
# layers
|
|
||||||
self.gate = nn.Sequential(nn.Linear(dim, dim_ffn, bias=False), GELU())
|
|
||||||
self.fc1 = nn.Linear(dim, dim_ffn, bias=False)
|
|
||||||
self.fc2 = nn.Linear(dim_ffn, dim, bias=False)
|
|
||||||
self.dropout = nn.Dropout(dropout)
|
|
||||||
|
|
||||||
def forward(self, x):
|
|
||||||
x = self.fc1(x) * self.gate(x)
|
|
||||||
x = self.dropout(x)
|
|
||||||
x = self.fc2(x)
|
|
||||||
x = self.dropout(x)
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
class T5SelfAttention(nn.Module):
|
|
||||||
def __init__(self, dim, dim_attn, dim_ffn, num_heads, num_buckets, shared_pos=True, dropout=0.1):
|
|
||||||
super().__init__()
|
|
||||||
self.dim = dim
|
|
||||||
self.dim_attn = dim_attn
|
|
||||||
self.dim_ffn = dim_ffn
|
|
||||||
self.num_heads = num_heads
|
|
||||||
self.num_buckets = num_buckets
|
|
||||||
self.shared_pos = shared_pos
|
|
||||||
|
|
||||||
# layers
|
|
||||||
self.norm1 = T5LayerNorm(dim)
|
|
||||||
self.attn = T5Attention(dim, dim_attn, num_heads, dropout)
|
|
||||||
self.norm2 = T5LayerNorm(dim)
|
|
||||||
self.ffn = T5FeedForward(dim, dim_ffn, dropout)
|
|
||||||
self.pos_embedding = (
|
|
||||||
None if shared_pos else T5RelativeEmbedding(num_buckets, num_heads, bidirectional=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
def forward(self, x, mask=None, pos_bias=None):
|
|
||||||
e = pos_bias if self.shared_pos else self.pos_embedding(x.size(1), x.size(1))
|
|
||||||
x = fp16_clamp(x + self.attn(self.norm1(x), mask=mask, pos_bias=e))
|
|
||||||
x = fp16_clamp(x + self.ffn(self.norm2(x)))
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
class T5CrossAttention(nn.Module):
|
|
||||||
def __init__(self, dim, dim_attn, dim_ffn, num_heads, num_buckets, shared_pos=True, dropout=0.1):
|
|
||||||
super().__init__()
|
|
||||||
self.dim = dim
|
|
||||||
self.dim_attn = dim_attn
|
|
||||||
self.dim_ffn = dim_ffn
|
|
||||||
self.num_heads = num_heads
|
|
||||||
self.num_buckets = num_buckets
|
|
||||||
self.shared_pos = shared_pos
|
|
||||||
|
|
||||||
# layers
|
|
||||||
self.norm1 = T5LayerNorm(dim)
|
|
||||||
self.self_attn = T5Attention(dim, dim_attn, num_heads, dropout)
|
|
||||||
self.norm2 = T5LayerNorm(dim)
|
|
||||||
self.cross_attn = T5Attention(dim, dim_attn, num_heads, dropout)
|
|
||||||
self.norm3 = T5LayerNorm(dim)
|
|
||||||
self.ffn = T5FeedForward(dim, dim_ffn, dropout)
|
|
||||||
self.pos_embedding = (
|
|
||||||
None if shared_pos else T5RelativeEmbedding(num_buckets, num_heads, bidirectional=False)
|
|
||||||
)
|
|
||||||
|
|
||||||
def forward(self, x, mask=None, encoder_states=None, encoder_mask=None, pos_bias=None):
|
|
||||||
e = pos_bias if self.shared_pos else self.pos_embedding(x.size(1), x.size(1))
|
|
||||||
x = fp16_clamp(x + self.self_attn(self.norm1(x), mask=mask, pos_bias=e))
|
|
||||||
x = fp16_clamp(x + self.cross_attn(self.norm2(x), context=encoder_states, mask=encoder_mask))
|
|
||||||
x = fp16_clamp(x + self.ffn(self.norm3(x)))
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
class T5RelativeEmbedding(nn.Module):
|
|
||||||
def __init__(self, num_buckets, num_heads, bidirectional, max_dist=128):
|
|
||||||
super().__init__()
|
|
||||||
self.num_buckets = num_buckets
|
|
||||||
self.num_heads = num_heads
|
|
||||||
self.bidirectional = bidirectional
|
|
||||||
self.max_dist = max_dist
|
|
||||||
|
|
||||||
# layers
|
|
||||||
self.embedding = nn.Embedding(num_buckets, num_heads)
|
|
||||||
|
|
||||||
def forward(self, lq, lk):
|
|
||||||
device = self.embedding.weight.device
|
|
||||||
# rel_pos = torch.arange(lk).unsqueeze(0).to(device) - \
|
|
||||||
# torch.arange(lq).unsqueeze(1).to(device)
|
|
||||||
rel_pos = torch.arange(lk, device=device).unsqueeze(0) - torch.arange(lq, device=device).unsqueeze(1)
|
|
||||||
rel_pos = self._relative_position_bucket(rel_pos)
|
|
||||||
rel_pos_embeds = self.embedding(rel_pos)
|
|
||||||
rel_pos_embeds = rel_pos_embeds.permute(2, 0, 1).unsqueeze(0) # [1, N, Lq, Lk]
|
|
||||||
return rel_pos_embeds.contiguous()
|
|
||||||
|
|
||||||
def _relative_position_bucket(self, rel_pos):
|
|
||||||
# preprocess
|
|
||||||
if self.bidirectional:
|
|
||||||
num_buckets = self.num_buckets // 2
|
|
||||||
rel_buckets = (rel_pos > 0).long() * num_buckets
|
|
||||||
rel_pos = torch.abs(rel_pos)
|
|
||||||
else:
|
|
||||||
num_buckets = self.num_buckets
|
|
||||||
rel_buckets = 0
|
|
||||||
rel_pos = -torch.min(rel_pos, torch.zeros_like(rel_pos))
|
|
||||||
|
|
||||||
# embeddings for small and large positions
|
|
||||||
max_exact = num_buckets // 2
|
|
||||||
rel_pos_large = (
|
|
||||||
max_exact
|
|
||||||
+ (
|
|
||||||
torch.log(rel_pos.float() / max_exact)
|
|
||||||
/ math.log(self.max_dist / max_exact)
|
|
||||||
* (num_buckets - max_exact)
|
|
||||||
).long()
|
|
||||||
)
|
|
||||||
rel_pos_large = torch.min(rel_pos_large, torch.full_like(rel_pos_large, num_buckets - 1))
|
|
||||||
rel_buckets += torch.where(rel_pos < max_exact, rel_pos, rel_pos_large)
|
|
||||||
return rel_buckets
|
|
||||||
|
|
||||||
|
|
||||||
class T5Encoder(nn.Module):
|
|
||||||
def __init__(
|
|
||||||
self, vocab, dim, dim_attn, dim_ffn, num_heads, num_layers, num_buckets, shared_pos=True, dropout=0.1
|
|
||||||
):
|
|
||||||
super().__init__()
|
|
||||||
self.dim = dim
|
|
||||||
self.dim_attn = dim_attn
|
|
||||||
self.dim_ffn = dim_ffn
|
|
||||||
self.num_heads = num_heads
|
|
||||||
self.num_layers = num_layers
|
|
||||||
self.num_buckets = num_buckets
|
|
||||||
self.shared_pos = shared_pos
|
|
||||||
|
|
||||||
# layers
|
|
||||||
self.token_embedding = vocab if isinstance(vocab, nn.Embedding) else nn.Embedding(vocab, dim)
|
|
||||||
self.pos_embedding = (
|
|
||||||
T5RelativeEmbedding(num_buckets, num_heads, bidirectional=True) if shared_pos else None
|
|
||||||
)
|
|
||||||
self.dropout = nn.Dropout(dropout)
|
|
||||||
self.blocks = nn.ModuleList(
|
|
||||||
[
|
|
||||||
T5SelfAttention(dim, dim_attn, dim_ffn, num_heads, num_buckets, shared_pos, dropout)
|
|
||||||
for _ in range(num_layers)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
self.norm = T5LayerNorm(dim)
|
|
||||||
|
|
||||||
# initialize weights
|
|
||||||
self.apply(init_weights)
|
|
||||||
|
|
||||||
def forward(self, ids, mask=None):
|
|
||||||
x = self.token_embedding(ids)
|
|
||||||
x = self.dropout(x)
|
|
||||||
e = self.pos_embedding(x.size(1), x.size(1)) if self.shared_pos else None
|
|
||||||
for block in self.blocks:
|
|
||||||
x = block(x, mask, pos_bias=e)
|
|
||||||
x = self.norm(x)
|
|
||||||
x = self.dropout(x)
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
class T5Decoder(nn.Module):
|
|
||||||
def __init__(
|
|
||||||
self, vocab, dim, dim_attn, dim_ffn, num_heads, num_layers, num_buckets, shared_pos=True, dropout=0.1
|
|
||||||
):
|
|
||||||
super().__init__()
|
|
||||||
self.dim = dim
|
|
||||||
self.dim_attn = dim_attn
|
|
||||||
self.dim_ffn = dim_ffn
|
|
||||||
self.num_heads = num_heads
|
|
||||||
self.num_layers = num_layers
|
|
||||||
self.num_buckets = num_buckets
|
|
||||||
self.shared_pos = shared_pos
|
|
||||||
|
|
||||||
# layers
|
|
||||||
self.token_embedding = vocab if isinstance(vocab, nn.Embedding) else nn.Embedding(vocab, dim)
|
|
||||||
self.pos_embedding = (
|
|
||||||
T5RelativeEmbedding(num_buckets, num_heads, bidirectional=False) if shared_pos else None
|
|
||||||
)
|
|
||||||
self.dropout = nn.Dropout(dropout)
|
|
||||||
self.blocks = nn.ModuleList(
|
|
||||||
[
|
|
||||||
T5CrossAttention(dim, dim_attn, dim_ffn, num_heads, num_buckets, shared_pos, dropout)
|
|
||||||
for _ in range(num_layers)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
self.norm = T5LayerNorm(dim)
|
|
||||||
|
|
||||||
# initialize weights
|
|
||||||
self.apply(init_weights)
|
|
||||||
|
|
||||||
def forward(self, ids, mask=None, encoder_states=None, encoder_mask=None):
|
|
||||||
b, s = ids.size()
|
|
||||||
|
|
||||||
# causal mask
|
|
||||||
if mask is None:
|
|
||||||
mask = torch.tril(torch.ones(1, s, s).to(ids.device))
|
|
||||||
elif mask.ndim == 2:
|
|
||||||
mask = torch.tril(mask.unsqueeze(1).expand(-1, s, -1))
|
|
||||||
|
|
||||||
# layers
|
|
||||||
x = self.token_embedding(ids)
|
|
||||||
x = self.dropout(x)
|
|
||||||
e = self.pos_embedding(x.size(1), x.size(1)) if self.shared_pos else None
|
|
||||||
for block in self.blocks:
|
|
||||||
x = block(x, mask, encoder_states, encoder_mask, pos_bias=e)
|
|
||||||
x = self.norm(x)
|
|
||||||
x = self.dropout(x)
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
class T5Model(nn.Module):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
vocab_size,
|
|
||||||
dim,
|
|
||||||
dim_attn,
|
|
||||||
dim_ffn,
|
|
||||||
num_heads,
|
|
||||||
encoder_layers,
|
|
||||||
decoder_layers,
|
|
||||||
num_buckets,
|
|
||||||
shared_pos=True,
|
|
||||||
dropout=0.1,
|
|
||||||
):
|
|
||||||
super().__init__()
|
|
||||||
self.vocab_size = vocab_size
|
|
||||||
self.dim = dim
|
|
||||||
self.dim_attn = dim_attn
|
|
||||||
self.dim_ffn = dim_ffn
|
|
||||||
self.num_heads = num_heads
|
|
||||||
self.encoder_layers = encoder_layers
|
|
||||||
self.decoder_layers = decoder_layers
|
|
||||||
self.num_buckets = num_buckets
|
|
||||||
|
|
||||||
# layers
|
|
||||||
self.token_embedding = nn.Embedding(vocab_size, dim)
|
|
||||||
self.encoder = T5Encoder(
|
|
||||||
self.token_embedding,
|
|
||||||
dim,
|
|
||||||
dim_attn,
|
|
||||||
dim_ffn,
|
|
||||||
num_heads,
|
|
||||||
encoder_layers,
|
|
||||||
num_buckets,
|
|
||||||
shared_pos,
|
|
||||||
dropout,
|
|
||||||
)
|
|
||||||
self.decoder = T5Decoder(
|
|
||||||
self.token_embedding,
|
|
||||||
dim,
|
|
||||||
dim_attn,
|
|
||||||
dim_ffn,
|
|
||||||
num_heads,
|
|
||||||
decoder_layers,
|
|
||||||
num_buckets,
|
|
||||||
shared_pos,
|
|
||||||
dropout,
|
|
||||||
)
|
|
||||||
self.head = nn.Linear(dim, vocab_size, bias=False)
|
|
||||||
|
|
||||||
# initialize weights
|
|
||||||
self.apply(init_weights)
|
|
||||||
|
|
||||||
def forward(self, encoder_ids, encoder_mask, decoder_ids, decoder_mask):
|
|
||||||
x = self.encoder(encoder_ids, encoder_mask)
|
|
||||||
x = self.decoder(decoder_ids, decoder_mask, x, encoder_mask)
|
|
||||||
x = self.head(x)
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def _t5(
|
|
||||||
name,
|
|
||||||
encoder_only=False,
|
|
||||||
decoder_only=False,
|
|
||||||
return_tokenizer=False,
|
|
||||||
tokenizer_kwargs=None,
|
|
||||||
dtype=torch.float32,
|
|
||||||
device="cpu",
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
# sanity check
|
|
||||||
assert not (encoder_only and decoder_only)
|
|
||||||
tokenizer_kwargs = tokenizer_kwargs or {}
|
|
||||||
|
|
||||||
# params
|
|
||||||
if encoder_only:
|
|
||||||
model_cls = T5Encoder
|
|
||||||
kwargs["vocab"] = kwargs.pop("vocab_size")
|
|
||||||
kwargs["num_layers"] = kwargs.pop("encoder_layers")
|
|
||||||
_ = kwargs.pop("decoder_layers")
|
|
||||||
elif decoder_only:
|
|
||||||
model_cls = T5Decoder
|
|
||||||
kwargs["vocab"] = kwargs.pop("vocab_size")
|
|
||||||
kwargs["num_layers"] = kwargs.pop("decoder_layers")
|
|
||||||
_ = kwargs.pop("encoder_layers")
|
|
||||||
else:
|
|
||||||
model_cls = T5Model
|
|
||||||
|
|
||||||
# init model
|
|
||||||
with torch.device(device):
|
|
||||||
model = model_cls(**kwargs)
|
|
||||||
|
|
||||||
# set device
|
|
||||||
model = model.to(dtype=dtype, device=device)
|
|
||||||
|
|
||||||
# init tokenizer
|
|
||||||
if return_tokenizer:
|
|
||||||
from .tokenizers import HuggingfaceTokenizer
|
|
||||||
|
|
||||||
tokenizer = HuggingfaceTokenizer(f"google/{name}", **tokenizer_kwargs)
|
|
||||||
return model, tokenizer
|
|
||||||
else:
|
|
||||||
return model
|
|
||||||
|
|
||||||
|
|
||||||
def umt5_xxl(**kwargs):
|
|
||||||
cfg = {
|
|
||||||
"vocab_size": 256384,
|
|
||||||
"dim": 4096,
|
|
||||||
"dim_attn": 4096,
|
|
||||||
"dim_ffn": 10240,
|
|
||||||
"num_heads": 64,
|
|
||||||
"encoder_layers": 24,
|
|
||||||
"decoder_layers": 24,
|
|
||||||
"num_buckets": 32,
|
|
||||||
"shared_pos": False,
|
|
||||||
"dropout": 0.1,
|
|
||||||
}
|
|
||||||
cfg.update(**kwargs)
|
|
||||||
return _t5("umt5-xxl", **cfg)
|
|
||||||
|
|
||||||
|
|
||||||
class T5EncoderModel:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
text_len,
|
|
||||||
dtype=torch.bfloat16,
|
|
||||||
device=torch.cuda.current_device(),
|
|
||||||
checkpoint_path=None,
|
|
||||||
tokenizer_path=None,
|
|
||||||
shard_fn=None,
|
|
||||||
):
|
|
||||||
self.text_len = text_len
|
|
||||||
self.dtype = dtype
|
|
||||||
self.device = device
|
|
||||||
self.checkpoint_path = checkpoint_path
|
|
||||||
self.tokenizer_path = tokenizer_path
|
|
||||||
|
|
||||||
# init model
|
|
||||||
model = (
|
|
||||||
umt5_xxl(encoder_only=True, return_tokenizer=False, dtype=dtype, device=device)
|
|
||||||
.eval()
|
|
||||||
.requires_grad_(False)
|
|
||||||
)
|
|
||||||
logging.info(f"loading {checkpoint_path}")
|
|
||||||
model.load_state_dict(torch.load(checkpoint_path, map_location="cpu", weights_only=True))
|
|
||||||
self.model = model
|
|
||||||
if shard_fn is not None:
|
|
||||||
self.model = shard_fn(self.model, sync_module_states=False)
|
|
||||||
else:
|
|
||||||
self.model.to(self.device)
|
|
||||||
# init tokenizer
|
|
||||||
self.tokenizer = HuggingfaceTokenizer(name=tokenizer_path, seq_len=text_len, clean="whitespace")
|
|
||||||
|
|
||||||
def __call__(self, texts, device):
|
|
||||||
ids, mask = self.tokenizer(texts, return_mask=True, add_special_tokens=True)
|
|
||||||
ids = ids.to(device)
|
|
||||||
mask = mask.to(device)
|
|
||||||
seq_lens = mask.gt(0).sum(dim=1).long()
|
|
||||||
context = self.model(ids, mask)
|
|
||||||
return [u[:v] for u, v in zip(context, seq_lens, strict=False)]
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
|
|
||||||
import html
|
|
||||||
import string
|
|
||||||
|
|
||||||
import ftfy
|
|
||||||
import regex as re
|
|
||||||
from transformers import AutoTokenizer
|
|
||||||
|
|
||||||
__all__ = ["HuggingfaceTokenizer"]
|
|
||||||
|
|
||||||
|
|
||||||
def basic_clean(text):
|
|
||||||
text = ftfy.fix_text(text)
|
|
||||||
text = html.unescape(html.unescape(text))
|
|
||||||
return text.strip()
|
|
||||||
|
|
||||||
|
|
||||||
def whitespace_clean(text):
|
|
||||||
text = re.sub(r"\s+", " ", text)
|
|
||||||
text = text.strip()
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
def canonicalize(text, keep_punctuation_exact_string=None):
|
|
||||||
text = text.replace("_", " ")
|
|
||||||
if keep_punctuation_exact_string:
|
|
||||||
text = keep_punctuation_exact_string.join(
|
|
||||||
part.translate(str.maketrans("", "", string.punctuation))
|
|
||||||
for part in text.split(keep_punctuation_exact_string)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
text = text.translate(str.maketrans("", "", string.punctuation))
|
|
||||||
text = text.lower()
|
|
||||||
text = re.sub(r"\s+", " ", text)
|
|
||||||
return text.strip()
|
|
||||||
|
|
||||||
|
|
||||||
class HuggingfaceTokenizer:
|
|
||||||
def __init__(self, name, seq_len=None, clean=None, **kwargs):
|
|
||||||
assert clean in (None, "whitespace", "lower", "canonicalize")
|
|
||||||
self.name = name
|
|
||||||
self.seq_len = seq_len
|
|
||||||
self.clean = clean
|
|
||||||
|
|
||||||
# init tokenizer
|
|
||||||
self.tokenizer = AutoTokenizer.from_pretrained(name, **kwargs)
|
|
||||||
self.vocab_size = self.tokenizer.vocab_size
|
|
||||||
|
|
||||||
def __call__(self, sequence, **kwargs):
|
|
||||||
return_mask = kwargs.pop("return_mask", False)
|
|
||||||
|
|
||||||
# arguments
|
|
||||||
_kwargs = {"return_tensors": "pt"}
|
|
||||||
if self.seq_len is not None:
|
|
||||||
_kwargs.update({"padding": "max_length", "truncation": True, "max_length": self.seq_len})
|
|
||||||
_kwargs.update(**kwargs)
|
|
||||||
|
|
||||||
# tokenization
|
|
||||||
if isinstance(sequence, str):
|
|
||||||
sequence = [sequence]
|
|
||||||
if self.clean:
|
|
||||||
sequence = [self._clean(u) for u in sequence]
|
|
||||||
ids = self.tokenizer(sequence, **_kwargs)
|
|
||||||
|
|
||||||
# output
|
|
||||||
if return_mask:
|
|
||||||
return ids.input_ids, ids.attention_mask
|
|
||||||
else:
|
|
||||||
return ids.input_ids
|
|
||||||
|
|
||||||
def _clean(self, text):
|
|
||||||
if self.clean == "whitespace":
|
|
||||||
text = whitespace_clean(basic_clean(text))
|
|
||||||
elif self.clean == "lower":
|
|
||||||
text = whitespace_clean(basic_clean(text)).lower()
|
|
||||||
elif self.clean == "canonicalize":
|
|
||||||
text = canonicalize(basic_clean(text))
|
|
||||||
return text
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -14,16 +14,24 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from typing import TYPE_CHECKING
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
from .wan.modules.vae2_2 import Wan2VAE
|
if TYPE_CHECKING:
|
||||||
|
from diffusers import AutoencoderKLWan
|
||||||
|
|
||||||
|
|
||||||
class WanVideoVAE38(torch.nn.Module):
|
class WanVideoVAE38(torch.nn.Module):
|
||||||
"""Tensor-batch adapter around the official Wan2.2 VAE wrapper."""
|
"""FastWAM VAE contract over `diffusers.AutoencoderKLWan` (Wan2.2-TI2V-5B).
|
||||||
|
|
||||||
|
16x spatial / 4x temporal compression, 48 latent channels. diffusers'
|
||||||
|
`AutoencoderKLWan` returns *raw* latents (it does not apply `latents_mean`/
|
||||||
|
`latents_std`), so `encode`/`decode` here apply the same standardization the
|
||||||
|
Wan reference uses — `(latents - mean) / std` — done in fp32 for stability.
|
||||||
|
`encode` uses the deterministic posterior mode, matching the original VAE
|
||||||
|
which returned the latent mean `mu`.
|
||||||
|
"""
|
||||||
|
|
||||||
upsampling_factor = 16
|
upsampling_factor = 16
|
||||||
temporal_downsample_factor = 4
|
temporal_downsample_factor = 4
|
||||||
@@ -31,27 +39,35 @@ class WanVideoVAE38(torch.nn.Module):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
vae_pth: str | Path,
|
|
||||||
dtype: torch.dtype = torch.float32,
|
dtype: torch.dtype = torch.float32,
|
||||||
device: str | torch.device = "cuda",
|
device: str | torch.device = "cuda",
|
||||||
|
*,
|
||||||
|
pretrained: AutoencoderKLWan,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.wan_vae = Wan2VAE(vae_pth=str(vae_pth), dtype=dtype, device=str(device))
|
# The Wan2.2 VAE is a fixed pretrained model — it is never trained from scratch,
|
||||||
self.model = self.wan_vae.model
|
# so a real `AutoencoderKLWan` (with weights) must always be supplied (loaded from
|
||||||
self.dtype = dtype
|
# the diffusers repo by `load_pretrained_wan_vae`). No random/offline build path.
|
||||||
self.device = torch.device(device)
|
self.vae = pretrained.to(device=device, dtype=dtype)
|
||||||
|
|
||||||
def to(self, *args: Any, **kwargs: Any):
|
# Read the standardization stats from the VAE's own config (diffusers populates
|
||||||
super().to(*args, **kwargs)
|
# these from vae/config.json) — single source of truth, no local copy. diffusers'
|
||||||
self.model.to(*args, **kwargs)
|
# encode/decode return *raw* latents, so we apply (latent - mean) / std ourselves.
|
||||||
param = next(self.model.parameters())
|
# Non-persistent: kept out of state_dict.
|
||||||
self.device = param.device
|
self.register_buffer(
|
||||||
self.dtype = param.dtype
|
"latents_mean",
|
||||||
self.wan_vae.device = self.device
|
torch.tensor(self.vae.config.latents_mean).view(1, self.z_dim, 1, 1, 1),
|
||||||
self.wan_vae.dtype = self.dtype
|
persistent=False,
|
||||||
self.wan_vae.scale = [scale.to(device=self.device, dtype=self.dtype) for scale in self.wan_vae.scale]
|
)
|
||||||
self.wan_vae.model = self.model
|
self.register_buffer(
|
||||||
return self
|
"latents_std",
|
||||||
|
torch.tensor(self.vae.config.latents_std).view(1, self.z_dim, 1, 1, 1),
|
||||||
|
persistent=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _device_dtype(self) -> tuple[torch.device, torch.dtype]:
|
||||||
|
param = next(self.vae.parameters())
|
||||||
|
return param.device, param.dtype
|
||||||
|
|
||||||
def encode(
|
def encode(
|
||||||
self,
|
self,
|
||||||
@@ -61,18 +77,16 @@ class WanVideoVAE38(torch.nn.Module):
|
|||||||
tile_size: tuple[int, int] = (34, 34),
|
tile_size: tuple[int, int] = (34, 34),
|
||||||
tile_stride: tuple[int, int] = (18, 16),
|
tile_stride: tuple[int, int] = (18, 16),
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
del tile_size, tile_stride
|
del device, tile_size, tile_stride
|
||||||
if tiled:
|
if tiled:
|
||||||
raise NotImplementedError("Tiled Wan2.2 VAE encoding is not supported by the FastWAM adapter.")
|
raise NotImplementedError("Tiled Wan2.2 VAE encoding is not supported by the FastWAM adapter.")
|
||||||
target_device = self.device if device is None else torch.device(device)
|
if isinstance(videos, (list, tuple)):
|
||||||
if target_device != self.device:
|
videos = torch.stack(list(videos))
|
||||||
self.to(device=target_device)
|
dev, dtype = self._device_dtype()
|
||||||
if isinstance(videos, torch.Tensor):
|
mu = self.vae.encode(videos.to(device=dev, dtype=dtype)).latent_dist.mode().float()
|
||||||
videos = list(videos)
|
mean = self.latents_mean.float().to(mu.device)
|
||||||
hidden_states = self.wan_vae.encode([video.to(self.device) for video in videos])
|
std = self.latents_std.float().to(mu.device)
|
||||||
if hidden_states is None:
|
return (mu - mean) / std
|
||||||
raise RuntimeError("Wan2.2 VAE encode failed; expected a list of video tensors.")
|
|
||||||
return torch.stack(hidden_states)
|
|
||||||
|
|
||||||
def decode(
|
def decode(
|
||||||
self,
|
self,
|
||||||
@@ -82,18 +96,16 @@ class WanVideoVAE38(torch.nn.Module):
|
|||||||
tile_size: tuple[int, int] = (34, 34),
|
tile_size: tuple[int, int] = (34, 34),
|
||||||
tile_stride: tuple[int, int] = (18, 16),
|
tile_stride: tuple[int, int] = (18, 16),
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
del tile_size, tile_stride
|
del device, tile_size, tile_stride
|
||||||
if tiled:
|
if tiled:
|
||||||
raise NotImplementedError("Tiled Wan2.2 VAE decoding is not supported by the FastWAM adapter.")
|
raise NotImplementedError("Tiled Wan2.2 VAE decoding is not supported by the FastWAM adapter.")
|
||||||
target_device = self.device if device is None else torch.device(device)
|
if isinstance(hidden_states, (list, tuple)):
|
||||||
if target_device != self.device:
|
hidden_states = torch.stack(list(hidden_states))
|
||||||
self.to(device=target_device)
|
dev, dtype = self._device_dtype()
|
||||||
if isinstance(hidden_states, torch.Tensor):
|
z = hidden_states.float()
|
||||||
hidden_states = list(hidden_states)
|
z = z * self.latents_std.float().to(z.device) + self.latents_mean.float().to(z.device)
|
||||||
videos = self.wan_vae.decode([hidden_state.to(self.device) for hidden_state in hidden_states])
|
out = self.vae.decode(z.to(device=dev, dtype=dtype)).sample
|
||||||
if videos is None:
|
return out.float().clamp_(-1.0, 1.0)
|
||||||
raise RuntimeError("Wan2.2 VAE decode failed; expected a list of latent tensors.")
|
|
||||||
return torch.stack(videos)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["WanVideoVAE38"]
|
__all__ = ["WanVideoVAE38"]
|
||||||
|
|||||||
@@ -15,8 +15,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
from collections.abc import Sequence
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
@@ -24,57 +23,108 @@ import torch
|
|||||||
from safetensors.torch import load_file
|
from safetensors.torch import load_file
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .wan.modules.tokenizers import HuggingfaceTokenizer
|
|
||||||
from .wan_adapters import WanVideoVAE38
|
from .wan_adapters import WanVideoVAE38
|
||||||
from .wan_video_dit import WanVideoDiT
|
from .wan_video_dit import WanVideoDiT
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# The custom MoT video DiT still ships in the original (non-diffusers) Wan2.2
|
||||||
|
# repo as sharded `diffusion_pytorch_model*.safetensors`; the VAE and UMT5 text
|
||||||
|
# encoder come from the diffusers conversion. Tokenizer is the stock UMT5 one.
|
||||||
WAN_DIT_PATTERN = "diffusion_pytorch_model*.safetensors"
|
WAN_DIT_PATTERN = "diffusion_pytorch_model*.safetensors"
|
||||||
WAN_T5_SAFE_CHECKPOINT = "models_t5_umt5-xxl-enc-bf16.safetensors"
|
|
||||||
WAN_T5_CHECKPOINT = WAN_T5_SAFE_CHECKPOINT
|
|
||||||
WAN_T5_TOKENIZER = "google/umt5-xxl"
|
WAN_T5_TOKENIZER = "google/umt5-xxl"
|
||||||
WAN_VAE_SAFE_CHECKPOINT = "Wan2.2_VAE.safetensors"
|
WAN22_DIFFUSERS_MODEL_ID = "Wan-AI/Wan2.2-TI2V-5B-Diffusers"
|
||||||
WAN_VAE_CHECKPOINT = WAN_VAE_SAFE_CHECKPOINT
|
|
||||||
|
class WanTextEncoder(torch.nn.Module):
|
||||||
|
"""FastWAM text-encoder contract over `transformers.UMT5EncoderModel`.
|
||||||
|
|
||||||
|
Exposes `.dim` (hidden size) and `forward(ids, mask) -> [B, L, dim]`, matching
|
||||||
|
the call in `FastWAM.encode_prompt`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
dtype: torch.dtype = torch.bfloat16,
|
||||||
|
device: str | torch.device = "cuda",
|
||||||
|
*,
|
||||||
|
pretrained: torch.nn.Module,
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
# UMT5-XXL is a fixed pretrained encoder — never trained from scratch, so a real
|
||||||
|
# `UMT5EncoderModel` (with weights) must always be supplied (loaded from the
|
||||||
|
# diffusers repo by `load_pretrained_wan_text_encoder`). No random/offline build.
|
||||||
|
self.model = pretrained.to(device=device, dtype=dtype)
|
||||||
|
self.dim = int(self.model.config.d_model)
|
||||||
|
|
||||||
|
def forward(self, ids: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
||||||
|
return self.model(input_ids=ids, attention_mask=mask.long()).last_hidden_state
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
class WanTokenizer:
|
||||||
class WanCheckpointPaths:
|
"""UMT5 tokenizer wrapper returning `(input_ids, attention_mask)` like the
|
||||||
root: Path
|
FastWAM call site expects."""
|
||||||
dit: list[Path]
|
|
||||||
vae: Path
|
def __init__(self, name: str = WAN_T5_TOKENIZER, seq_len: int = 512) -> None:
|
||||||
text_encoder: Path | None
|
from transformers import AutoTokenizer
|
||||||
tokenizer: Path | None
|
|
||||||
|
self.tokenizer = AutoTokenizer.from_pretrained(name)
|
||||||
|
self.seq_len = int(seq_len)
|
||||||
|
|
||||||
|
def __call__(
|
||||||
|
self, sequence: str | Sequence[str], return_mask: bool = False, add_special_tokens: bool = True, **_: Any
|
||||||
|
):
|
||||||
|
if isinstance(sequence, str):
|
||||||
|
sequence = [sequence]
|
||||||
|
out = self.tokenizer(
|
||||||
|
list(sequence),
|
||||||
|
padding="max_length",
|
||||||
|
truncation=True,
|
||||||
|
max_length=self.seq_len,
|
||||||
|
add_special_tokens=add_special_tokens,
|
||||||
|
return_tensors="pt",
|
||||||
|
)
|
||||||
|
if return_mask:
|
||||||
|
return out.input_ids, out.attention_mask
|
||||||
|
return out.input_ids
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
def build_wan_tokenizer(*, tokenizer_max_len: int) -> WanTokenizer:
|
||||||
class Wan22LoadedComponents:
|
return WanTokenizer(name=WAN_T5_TOKENIZER, seq_len=int(tokenizer_max_len))
|
||||||
dit: WanVideoDiT
|
|
||||||
vae: WanVideoVAE38
|
|
||||||
text_encoder: torch.nn.Module | None
|
|
||||||
tokenizer: HuggingfaceTokenizer | None
|
|
||||||
dit_path: list[str]
|
|
||||||
vae_path: str
|
|
||||||
text_encoder_path: str | None
|
|
||||||
tokenizer_path: str | None
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_wan_checkpoint_dir(
|
def load_pretrained_wan_vae(*, torch_dtype: torch.dtype, device: str) -> WanVideoVAE38:
|
||||||
|
"""Load real Wan2.2 VAE weights from the diffusers repo (offline base creation)."""
|
||||||
|
from diffusers import AutoencoderKLWan
|
||||||
|
|
||||||
|
from .wan_adapters import WanVideoVAE38
|
||||||
|
|
||||||
|
vae = AutoencoderKLWan.from_pretrained(
|
||||||
|
WAN22_DIFFUSERS_MODEL_ID, subfolder="vae", torch_dtype=torch_dtype
|
||||||
|
)
|
||||||
|
return WanVideoVAE38(dtype=torch_dtype, device=device, pretrained=vae)
|
||||||
|
|
||||||
|
|
||||||
|
def load_pretrained_wan_text_encoder(*, torch_dtype: torch.dtype, device: str) -> WanTextEncoder:
|
||||||
|
"""Load real UMT5-XXL encoder weights from the diffusers repo (offline base creation)."""
|
||||||
|
from transformers import UMT5EncoderModel
|
||||||
|
|
||||||
|
encoder = UMT5EncoderModel.from_pretrained(
|
||||||
|
WAN22_DIFFUSERS_MODEL_ID, subfolder="text_encoder", torch_dtype=torch_dtype
|
||||||
|
)
|
||||||
|
return WanTextEncoder(dtype=torch_dtype, device=device, pretrained=encoder)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_wan_dit_paths(
|
||||||
model_id_or_path: str | Path,
|
model_id_or_path: str | Path,
|
||||||
*,
|
*,
|
||||||
cache_dir: str | Path | None = None,
|
cache_dir: str | Path | None = None,
|
||||||
local_files_only: bool = False,
|
local_files_only: bool = False,
|
||||||
revision: str | None = None,
|
revision: str | None = None,
|
||||||
) -> Path:
|
) -> list[Path]:
|
||||||
"""Return a local Wan2.2 checkpoint directory.
|
"""Resolve the custom MoT DiT shards from the original Wan2.2 repo or a local dir."""
|
||||||
|
|
||||||
Local paths are used directly. Hub repos are downloaded with the same fixed
|
|
||||||
component names used by the upstream Wan2.2 inference code.
|
|
||||||
"""
|
|
||||||
|
|
||||||
path = Path(model_id_or_path).expanduser()
|
path = Path(model_id_or_path).expanduser()
|
||||||
if path.is_dir():
|
if path.is_dir():
|
||||||
return path
|
return sorted(path.glob(WAN_DIT_PATTERN))
|
||||||
|
|
||||||
from huggingface_hub import snapshot_download
|
from huggingface_hub import snapshot_download
|
||||||
|
|
||||||
@@ -83,51 +133,9 @@ def resolve_wan_checkpoint_dir(
|
|||||||
revision=revision,
|
revision=revision,
|
||||||
cache_dir=cache_dir,
|
cache_dir=cache_dir,
|
||||||
local_files_only=local_files_only,
|
local_files_only=local_files_only,
|
||||||
allow_patterns=[
|
allow_patterns=[WAN_DIT_PATTERN],
|
||||||
WAN_DIT_PATTERN,
|
|
||||||
WAN_T5_CHECKPOINT,
|
|
||||||
WAN_VAE_CHECKPOINT,
|
|
||||||
f"{WAN_T5_TOKENIZER}/**",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
return Path(snapshot_path)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_wan_checkpoint_paths(
|
|
||||||
checkpoint_dir: str | Path,
|
|
||||||
*,
|
|
||||||
tokenizer_dir: str | Path | None = None,
|
|
||||||
load_dit: bool = True,
|
|
||||||
load_text_encoder: bool = True,
|
|
||||||
) -> WanCheckpointPaths:
|
|
||||||
root = Path(checkpoint_dir).expanduser()
|
|
||||||
tokenizer_root = Path(tokenizer_dir).expanduser() if tokenizer_dir is not None else root
|
|
||||||
dit = sorted(root.glob(WAN_DIT_PATTERN)) if load_dit else []
|
|
||||||
vae = root / WAN_VAE_SAFE_CHECKPOINT
|
|
||||||
text_encoder = root / WAN_T5_SAFE_CHECKPOINT if load_text_encoder else None
|
|
||||||
tokenizer = tokenizer_root / WAN_T5_TOKENIZER if load_text_encoder else None
|
|
||||||
|
|
||||||
missing = []
|
|
||||||
if load_dit and len(dit) == 0:
|
|
||||||
missing.append(f"DiT ({WAN_DIT_PATTERN})")
|
|
||||||
if not vae.exists():
|
|
||||||
missing.append(f"VAE ({WAN_VAE_SAFE_CHECKPOINT})")
|
|
||||||
if load_text_encoder:
|
|
||||||
if text_encoder is None or not text_encoder.exists():
|
|
||||||
missing.append(f"text encoder ({WAN_T5_SAFE_CHECKPOINT})")
|
|
||||||
if tokenizer is None or not tokenizer.exists():
|
|
||||||
missing.append(f"tokenizer ({WAN_T5_TOKENIZER})")
|
|
||||||
if missing:
|
|
||||||
raise FileNotFoundError(
|
|
||||||
f"Incomplete Wan2.2 checkpoint directory {root}: missing {', '.join(missing)}."
|
|
||||||
)
|
|
||||||
return WanCheckpointPaths(
|
|
||||||
root=root,
|
|
||||||
dit=dit,
|
|
||||||
vae=vae,
|
|
||||||
text_encoder=text_encoder,
|
|
||||||
tokenizer=tokenizer,
|
|
||||||
)
|
)
|
||||||
|
return sorted(Path(snapshot_path).glob(WAN_DIT_PATTERN))
|
||||||
|
|
||||||
|
|
||||||
def load_wan_video_dit(
|
def load_wan_video_dit(
|
||||||
@@ -145,107 +153,6 @@ def load_wan_video_dit(
|
|||||||
return model.to(device=device, dtype=torch_dtype)
|
return model.to(device=device, dtype=torch_dtype)
|
||||||
|
|
||||||
|
|
||||||
def load_wan_text_encoder(
|
|
||||||
checkpoint_path: str | Path,
|
|
||||||
*,
|
|
||||||
torch_dtype: torch.dtype,
|
|
||||||
device: str,
|
|
||||||
) -> torch.nn.Module:
|
|
||||||
from .wan.modules.t5 import umt5_xxl
|
|
||||||
|
|
||||||
model = umt5_xxl(
|
|
||||||
encoder_only=True,
|
|
||||||
return_tokenizer=False,
|
|
||||||
dtype=torch_dtype,
|
|
||||||
device=device,
|
|
||||||
)
|
|
||||||
checkpoint_path = Path(checkpoint_path)
|
|
||||||
if checkpoint_path.suffix != ".safetensors":
|
|
||||||
raise ValueError(f"Wan2.2 text encoder checkpoint must be safetensors, got {checkpoint_path}.")
|
|
||||||
state_dict = load_file(checkpoint_path)
|
|
||||||
model.load_state_dict(state_dict)
|
|
||||||
return model.to(device=device, dtype=torch_dtype)
|
|
||||||
|
|
||||||
|
|
||||||
def load_wan_tokenizer(tokenizer_path: str | Path, *, tokenizer_max_len: int) -> HuggingfaceTokenizer:
|
|
||||||
from .wan.modules.tokenizers import HuggingfaceTokenizer
|
|
||||||
|
|
||||||
return HuggingfaceTokenizer(
|
|
||||||
name=str(tokenizer_path),
|
|
||||||
seq_len=int(tokenizer_max_len),
|
|
||||||
clean="whitespace",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def load_wan_vae(checkpoint_path: str | Path, *, torch_dtype: torch.dtype, device: str) -> WanVideoVAE38:
|
|
||||||
from .wan_adapters import WanVideoVAE38
|
|
||||||
|
|
||||||
return WanVideoVAE38(vae_pth=str(checkpoint_path), dtype=torch_dtype, device=device)
|
|
||||||
|
|
||||||
|
|
||||||
def load_wan22_ti2v_5b_components(
|
|
||||||
device: str = "cuda",
|
|
||||||
torch_dtype: torch.dtype = torch.bfloat16,
|
|
||||||
model_id: str = "Wan-AI/Wan2.2-TI2V-5B",
|
|
||||||
tokenizer_model_id: str = "Wan-AI/Wan2.2-TI2V-5B",
|
|
||||||
tokenizer_max_len: int = 512,
|
|
||||||
dit_config: dict[str, Any] | None = None,
|
|
||||||
load_text_encoder: bool = True,
|
|
||||||
):
|
|
||||||
logger.info("Loading Wan2.2-TI2V-5B components...")
|
|
||||||
start = time.time()
|
|
||||||
|
|
||||||
if dit_config is None:
|
|
||||||
raise ValueError("`dit_config` is required for Wan2.2-TI2V-5B loading.")
|
|
||||||
|
|
||||||
checkpoint_dir = resolve_wan_checkpoint_dir(model_id)
|
|
||||||
tokenizer_dir = (
|
|
||||||
checkpoint_dir if tokenizer_model_id == model_id else resolve_wan_checkpoint_dir(tokenizer_model_id)
|
|
||||||
)
|
|
||||||
paths = resolve_wan_checkpoint_paths(
|
|
||||||
checkpoint_dir,
|
|
||||||
tokenizer_dir=tokenizer_dir,
|
|
||||||
load_text_encoder=load_text_encoder,
|
|
||||||
)
|
|
||||||
|
|
||||||
dit = load_wan_video_dit(
|
|
||||||
paths.dit,
|
|
||||||
dit_config=dit_config,
|
|
||||||
torch_dtype=torch_dtype,
|
|
||||||
device=device,
|
|
||||||
)
|
|
||||||
vae = load_wan_vae(paths.vae, torch_dtype=torch_dtype, device=device)
|
|
||||||
|
|
||||||
text_encoder: torch.nn.Module | None = None
|
|
||||||
tokenizer: HuggingfaceTokenizer | None = None
|
|
||||||
if load_text_encoder:
|
|
||||||
if paths.text_encoder is None or paths.tokenizer is None:
|
|
||||||
raise FileNotFoundError("Wan2.2 text encoder/tokenizer paths were not resolved.")
|
|
||||||
text_encoder = load_wan_text_encoder(
|
|
||||||
paths.text_encoder,
|
|
||||||
torch_dtype=torch_dtype,
|
|
||||||
device=device,
|
|
||||||
)
|
|
||||||
tokenizer = load_wan_tokenizer(paths.tokenizer, tokenizer_max_len=tokenizer_max_len)
|
|
||||||
else:
|
|
||||||
logger.info(
|
|
||||||
"Skipping pretrained text encoder/tokenizer load (`load_text_encoder=False`); "
|
|
||||||
"training must provide cached `context/context_mask`."
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Finished loading Wan2.2-TI2V-5B components in %.2f seconds.", time.time() - start)
|
|
||||||
return Wan22LoadedComponents(
|
|
||||||
dit=dit,
|
|
||||||
vae=vae,
|
|
||||||
text_encoder=text_encoder,
|
|
||||||
tokenizer=tokenizer,
|
|
||||||
dit_path=[str(path) for path in paths.dit],
|
|
||||||
vae_path=str(paths.vae),
|
|
||||||
text_encoder_path=str(paths.text_encoder) if paths.text_encoder is not None else None,
|
|
||||||
tokenizer_path=str(paths.tokenizer) if paths.tokenizer is not None else None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _read_wan_dit_safetensors(paths: list[str | Path]) -> dict[str, torch.Tensor]:
|
def _read_wan_dit_safetensors(paths: list[str | Path]) -> dict[str, torch.Tensor]:
|
||||||
state_dict = {}
|
state_dict = {}
|
||||||
for path in paths:
|
for path in paths:
|
||||||
@@ -254,17 +161,14 @@ def _read_wan_dit_safetensors(paths: list[str | Path]) -> dict[str, torch.Tensor
|
|||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"WAN22_DIFFUSERS_MODEL_ID",
|
||||||
"WAN_DIT_PATTERN",
|
"WAN_DIT_PATTERN",
|
||||||
"WAN_T5_CHECKPOINT",
|
|
||||||
"WAN_T5_TOKENIZER",
|
"WAN_T5_TOKENIZER",
|
||||||
"WAN_VAE_CHECKPOINT",
|
"WanTextEncoder",
|
||||||
"Wan22LoadedComponents",
|
"WanTokenizer",
|
||||||
"WanCheckpointPaths",
|
"build_wan_tokenizer",
|
||||||
"load_wan22_ti2v_5b_components",
|
"load_pretrained_wan_text_encoder",
|
||||||
"load_wan_text_encoder",
|
"load_pretrained_wan_vae",
|
||||||
"load_wan_tokenizer",
|
|
||||||
"load_wan_vae",
|
|
||||||
"load_wan_video_dit",
|
"load_wan_video_dit",
|
||||||
"resolve_wan_checkpoint_dir",
|
"resolve_wan_dit_paths",
|
||||||
"resolve_wan_checkpoint_paths",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -660,11 +660,16 @@ class WanVideoDiT(WanModel):
|
|||||||
) * timestep.to(dtype=model_dtype).view(batch_size, 1, 1)
|
) * timestep.to(dtype=model_dtype).view(batch_size, 1, 1)
|
||||||
token_timesteps[:, 0, :] = 0
|
token_timesteps[:, 0, :] = 0
|
||||||
token_timesteps = token_timesteps.reshape(batch_size, -1)
|
token_timesteps = token_timesteps.reshape(batch_size, -1)
|
||||||
token_t_emb = sinusoidal_embedding_1d(self.freq_dim, token_timesteps.reshape(-1)).to(
|
# Wan keeps the time embedding in fp32: the AdaLN modulation in the vendored
|
||||||
dtype=model_dtype
|
# Head/Block asserts e.dtype == float32 (numerical stability of the scale/shift).
|
||||||
)
|
# Upstream guarantees this via an fp32 autocast region, so it holds even when the
|
||||||
t = self.time_embedding(token_t_emb).reshape(batch_size, -1, self.hidden_dim)
|
# model runs in bf16. Mirror that here, then cast the per-block modulation back to
|
||||||
t_mod = self.time_projection(t).unflatten(2, (6, self.hidden_dim))
|
# model_dtype so the bf16 attention blocks are not upcast to fp32.
|
||||||
|
with torch.amp.autocast("cuda", dtype=torch.float32):
|
||||||
|
token_t_emb = sinusoidal_embedding_1d(self.freq_dim, token_timesteps.reshape(-1)).float()
|
||||||
|
t = self.time_embedding(token_t_emb).reshape(batch_size, -1, self.hidden_dim)
|
||||||
|
t_mod = self.time_projection(t).unflatten(2, (6, self.hidden_dim))
|
||||||
|
t_mod = t_mod.to(dtype=model_dtype)
|
||||||
|
|
||||||
x = self.patchify(x)
|
x = self.patchify(x)
|
||||||
f, h, w = x.shape[2:]
|
f, h, w = x.shape[2:]
|
||||||
|
|||||||
@@ -18,21 +18,13 @@ import json
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import torch
|
import torch
|
||||||
from safetensors.torch import save_model
|
from safetensors import safe_open
|
||||||
from torch import nn
|
from torch import nn
|
||||||
|
|
||||||
from lerobot.configs import FeatureType, PolicyFeature, PreTrainedConfig
|
from lerobot.configs import FeatureType, PolicyFeature, PreTrainedConfig
|
||||||
from lerobot.policies import FastWAMConfig, get_policy_class, make_policy_config, make_pre_post_processors
|
from lerobot.policies import FastWAMConfig, get_policy_class, make_policy_config, make_pre_post_processors
|
||||||
from lerobot.policies.fastwam import modeling_fastwam
|
from lerobot.policies.fastwam.modeling_fastwam import FastWAMPolicy
|
||||||
from lerobot.policies.fastwam.modeling_fastwam import FastWAMPolicy, resolve_wan_component_paths
|
|
||||||
from lerobot.policies.fastwam.processor_fastwam import FastWAMActionToggleProcessorStep
|
from lerobot.policies.fastwam.processor_fastwam import FastWAMActionToggleProcessorStep
|
||||||
from lerobot.policies.fastwam.wan_components import (
|
|
||||||
WAN_DIT_PATTERN,
|
|
||||||
WAN_T5_CHECKPOINT,
|
|
||||||
WAN_T5_TOKENIZER,
|
|
||||||
WAN_VAE_CHECKPOINT,
|
|
||||||
resolve_wan_checkpoint_paths,
|
|
||||||
)
|
|
||||||
from lerobot.utils.constants import ACTION, OBS_STATE
|
from lerobot.utils.constants import ACTION, OBS_STATE
|
||||||
|
|
||||||
|
|
||||||
@@ -170,8 +162,7 @@ def test_policy_forward_and_predict_action_adapt_lerobot_batches(monkeypatch):
|
|||||||
output_features={ACTION: PolicyFeature(type=FeatureType.ACTION, shape=(3,))},
|
output_features={ACTION: PolicyFeature(type=FeatureType.ACTION, shape=(3,))},
|
||||||
base_model_id=None,
|
base_model_id=None,
|
||||||
)
|
)
|
||||||
with pytest.warns(RuntimeWarning, match="does not load pretrained FastWAM weights"):
|
policy = FastWAMPolicy(cfg)
|
||||||
policy = FastWAMPolicy(cfg)
|
|
||||||
|
|
||||||
output = policy.forward(
|
output = policy.forward(
|
||||||
{
|
{
|
||||||
@@ -207,89 +198,96 @@ def test_policy_forward_and_predict_action_adapt_lerobot_batches(monkeypatch):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_from_pretrained_loads_weights_without_initializing_wan_backbone(monkeypatch, tmp_path):
|
class CoreWithFrozenComponents(FakeFastWAMCore):
|
||||||
cfg = FastWAMConfig(action_dim=3, proprio_dim=2, action_horizon=4, n_action_steps=2, base_model_id=None)
|
"""Fake core mirroring the real one: frozen VAE / text encoder held as
|
||||||
cfg.save_pretrained(tmp_path)
|
*unregistered* attributes (via `object.__setattr__`) so they are excluded from
|
||||||
monkeypatch.setattr(FastWAMPolicy, "_build_core_model", lambda self, config: FakeFastWAMCore())
|
`state_dict()` and the saved checkpoint, but still moved by the `_apply` override."""
|
||||||
reference_policy = FastWAMPolicy(cfg, _suppress_base_init_warning=True)
|
|
||||||
save_model(reference_policy, str(tmp_path / "model.safetensors"))
|
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
object.__setattr__(self, "vae", nn.Linear(2, 2))
|
||||||
|
object.__setattr__(self, "text_encoder", nn.Linear(2, 2))
|
||||||
|
self.vae.requires_grad_(False)
|
||||||
|
self.text_encoder.requires_grad_(False)
|
||||||
|
|
||||||
|
def _apply(self, fn, *args, **kwargs):
|
||||||
|
super()._apply(fn, *args, **kwargs)
|
||||||
|
self.vae._apply(fn)
|
||||||
|
self.text_encoder._apply(fn)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_pretrained_uses_base_loader_and_skips_wan_backbone(monkeypatch, tmp_path):
|
||||||
|
cfg = FastWAMConfig(action_dim=3, proprio_dim=2, action_horizon=4, n_action_steps=2, base_model_id=None)
|
||||||
|
|
||||||
|
def build_core(self, config):
|
||||||
|
core = CoreWithFrozenComponents()
|
||||||
|
with torch.no_grad():
|
||||||
|
core.dit.weight.fill_(0.5)
|
||||||
|
return core
|
||||||
|
|
||||||
|
monkeypatch.setattr(FastWAMPolicy, "_build_core_model", build_core)
|
||||||
|
|
||||||
|
reference = FastWAMPolicy(cfg)
|
||||||
|
with torch.no_grad():
|
||||||
|
reference.model.dit.weight.fill_(1.25) # a distinctive, trained-looking weight
|
||||||
|
reference.save_pretrained(tmp_path)
|
||||||
|
|
||||||
|
# Building from Wan2.2 must never happen on a checkpoint load.
|
||||||
def fail_if_wan_pretrained_is_loaded(*args, **kwargs):
|
def fail_if_wan_pretrained_is_loaded(*args, **kwargs):
|
||||||
raise AssertionError("from_pretrained must not initialize or download Wan2.2 backbone components")
|
raise AssertionError("from_pretrained must not initialize or download the Wan2.2 backbone")
|
||||||
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"lerobot.policies.fastwam.modular_fastwam.FastWAM.from_wan22_pretrained",
|
"lerobot.policies.fastwam.modular_fastwam.FastWAM.from_wan22_pretrained",
|
||||||
fail_if_wan_pretrained_is_loaded,
|
fail_if_wan_pretrained_is_loaded,
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(
|
|
||||||
modeling_fastwam,
|
|
||||||
"_build_core_model_from_architecture",
|
|
||||||
lambda config: FakeFastWAMCore(),
|
|
||||||
raising=False,
|
|
||||||
)
|
|
||||||
loaded_components_from = []
|
|
||||||
monkeypatch.setattr(
|
|
||||||
FastWAMPolicy,
|
|
||||||
"load_wan_components_from_pretrained",
|
|
||||||
lambda self, path: loaded_components_from.append(path),
|
|
||||||
)
|
|
||||||
|
|
||||||
policy = FastWAMPolicy.from_pretrained(tmp_path, strict=False)
|
policy = FastWAMPolicy.from_pretrained(tmp_path)
|
||||||
|
|
||||||
assert isinstance(policy.model, FakeFastWAMCore)
|
assert isinstance(policy.model, CoreWithFrozenComponents)
|
||||||
assert loaded_components_from == [tmp_path]
|
# The bundled checkpoint weights overwrote the freshly built (0.5) DiT weights.
|
||||||
|
assert torch.allclose(policy.model.dit.weight, torch.full_like(policy.model.dit.weight, 1.25))
|
||||||
|
|
||||||
|
|
||||||
def test_save_pretrained_copies_required_wan_sidecars(monkeypatch, tmp_path):
|
def test_save_pretrained_excludes_frozen_components(monkeypatch, tmp_path):
|
||||||
cfg = FastWAMConfig(action_dim=3, proprio_dim=2, action_horizon=4, n_action_steps=2, base_model_id=None)
|
cfg = FastWAMConfig(action_dim=3, proprio_dim=2, action_horizon=4, n_action_steps=2, base_model_id=None)
|
||||||
source = tmp_path / "source"
|
monkeypatch.setattr(FastWAMPolicy, "_build_core_model", lambda self, config: CoreWithFrozenComponents())
|
||||||
tokenizer = source / WAN_T5_TOKENIZER
|
policy = FastWAMPolicy(cfg)
|
||||||
tokenizer.mkdir(parents=True)
|
|
||||||
vae = source / WAN_VAE_CHECKPOINT
|
|
||||||
text_encoder = source / WAN_T5_CHECKPOINT
|
|
||||||
tokenizer_file = tokenizer / "tokenizer.json"
|
|
||||||
vae.write_bytes(b"vae")
|
|
||||||
text_encoder.write_bytes(b"text")
|
|
||||||
tokenizer_file.write_text("{}")
|
|
||||||
core = FakeFastWAMCore()
|
|
||||||
core.model_paths = {
|
|
||||||
"vae": str(vae),
|
|
||||||
"text_encoder": str(text_encoder),
|
|
||||||
"tokenizer": str(tokenizer),
|
|
||||||
}
|
|
||||||
monkeypatch.setattr(FastWAMPolicy, "_build_core_model", lambda self, config: core)
|
|
||||||
policy = FastWAMPolicy(cfg, _suppress_base_init_warning=True)
|
|
||||||
|
|
||||||
save_dir = tmp_path / "saved"
|
save_dir = tmp_path / "saved"
|
||||||
policy.save_pretrained(save_dir)
|
policy.save_pretrained(save_dir)
|
||||||
|
|
||||||
assert (save_dir / "model.safetensors").is_file()
|
assert (save_dir / "model.safetensors").is_file()
|
||||||
assert (save_dir / WAN_VAE_CHECKPOINT).read_bytes() == b"vae"
|
# No Wan sidecar files either: the frozen backbone comes from the diffusers repo.
|
||||||
assert (save_dir / WAN_T5_CHECKPOINT).read_bytes() == b"text"
|
assert not (save_dir / "Wan2.2_VAE.safetensors").exists()
|
||||||
assert (save_dir / WAN_T5_TOKENIZER / "tokenizer.json").read_text() == "{}"
|
assert not (save_dir / "google").exists()
|
||||||
|
|
||||||
|
with safe_open(save_dir / "model.safetensors", framework="pt") as f:
|
||||||
|
keys = set(f.keys())
|
||||||
|
# Lean checkpoint: only the trainable DiT is saved; the frozen VAE / UMT5 text
|
||||||
|
# encoder are excluded (loaded from the diffusers/transformers repos at init).
|
||||||
|
assert any(key.startswith("model.dit.") for key in keys)
|
||||||
|
assert not any(key.startswith("model.vae.") for key in keys)
|
||||||
|
assert not any(key.startswith("model.text_encoder.") for key in keys)
|
||||||
|
|
||||||
|
|
||||||
def test_wan_component_resolution_uses_fixed_safetensors_layout(tmp_path):
|
def test_frozen_components_excluded_from_params_but_follow_device_moves(monkeypatch):
|
||||||
tokenizer = tmp_path / WAN_T5_TOKENIZER
|
cfg = FastWAMConfig(action_dim=3, proprio_dim=2, action_horizon=4, n_action_steps=2, base_model_id=None)
|
||||||
tokenizer.mkdir(parents=True)
|
monkeypatch.setattr(FastWAMPolicy, "_build_core_model", lambda self, config: CoreWithFrozenComponents())
|
||||||
(tmp_path / WAN_VAE_CHECKPOINT).touch()
|
policy = FastWAMPolicy(cfg)
|
||||||
(tmp_path / WAN_T5_CHECKPOINT).touch()
|
|
||||||
(tmp_path / "diffusion_pytorch_model-00001-of-00001.safetensors").touch()
|
|
||||||
(tokenizer / "tokenizer.json").touch()
|
|
||||||
|
|
||||||
paths = resolve_wan_checkpoint_paths(tmp_path)
|
# Unregistered: excluded from state_dict and from the optimizer's parameter set.
|
||||||
sidecar_paths = resolve_wan_component_paths(tmp_path)
|
sd = policy.state_dict()
|
||||||
|
assert not any(k.startswith("model.vae.") or k.startswith("model.text_encoder.") for k in sd)
|
||||||
|
param_names = [n for n, _ in policy.named_parameters()]
|
||||||
|
assert not any("vae" in n or "text_encoder" in n for n in param_names)
|
||||||
|
|
||||||
assert paths.dit == [tmp_path / "diffusion_pytorch_model-00001-of-00001.safetensors"]
|
# ...but the `_apply` override still carries them through `.to()` (dtype stands in
|
||||||
assert paths.vae == tmp_path / WAN_VAE_CHECKPOINT
|
# for device on a CPU box), so they never strand off the rest of the model.
|
||||||
assert paths.text_encoder == tmp_path / WAN_T5_CHECKPOINT
|
policy.to(torch.float64)
|
||||||
assert paths.tokenizer == tmp_path / WAN_T5_TOKENIZER
|
assert policy.model.dit.weight.dtype == torch.float64 # registered
|
||||||
assert sidecar_paths.dit == []
|
assert policy.model.vae.weight.dtype == torch.float64 # unregistered, moved via _apply
|
||||||
assert WAN_DIT_PATTERN == "diffusion_pytorch_model*.safetensors"
|
assert policy.model.text_encoder.weight.dtype == torch.float64
|
||||||
|
|
||||||
(tmp_path / WAN_T5_CHECKPOINT).unlink()
|
|
||||||
with pytest.raises(FileNotFoundError, match="text encoder"):
|
|
||||||
resolve_wan_checkpoint_paths(tmp_path)
|
|
||||||
|
|
||||||
|
|
||||||
def test_pretrained_config_round_trips_fastwam_features(tmp_path):
|
def test_pretrained_config_round_trips_fastwam_features(tmp_path):
|
||||||
@@ -302,3 +300,57 @@ def test_pretrained_config_round_trips_fastwam_features(tmp_path):
|
|||||||
assert loaded.image_features["observation.images.image"].type == FeatureType.VISUAL
|
assert loaded.image_features["observation.images.image"].type == FeatureType.VISUAL
|
||||||
assert loaded.action_feature.shape == (7,)
|
assert loaded.action_feature.shape == (7,)
|
||||||
assert loaded.robot_state_feature.shape == (8,)
|
assert loaded.robot_state_feature.shape == (8,)
|
||||||
|
|
||||||
|
|
||||||
|
def test_vae_adapter_empty_build_encode_decode_shapes():
|
||||||
|
"""Offline glue check of the diffusers-backed VAE adapter (random weights).
|
||||||
|
|
||||||
|
Validates the encode/decode contract — 48 latent channels, 16x spatial / 4x
|
||||||
|
temporal compression, list-or-batch input, scaling round-trip — without any
|
||||||
|
weight download. (Numerical fidelity vs the original Wan VAE is a separate,
|
||||||
|
GPU + real-weights verification step.)
|
||||||
|
"""
|
||||||
|
pytest.importorskip("diffusers")
|
||||||
|
from diffusers import AutoencoderKLWan
|
||||||
|
|
||||||
|
from lerobot.policies.fastwam.wan_adapters import WanVideoVAE38
|
||||||
|
|
||||||
|
# Production always loads a real pretrained VAE from the diffusers repo; here we
|
||||||
|
# build the same architecture with random weights and dummy standardization stats
|
||||||
|
# to exercise the adapter's shape/scaling contract offline (fidelity is checked
|
||||||
|
# separately, with real weights, on GPU).
|
||||||
|
arch = {
|
||||||
|
"base_dim": 160,
|
||||||
|
"decoder_base_dim": 256,
|
||||||
|
"z_dim": 48,
|
||||||
|
"dim_mult": [1, 2, 4, 4],
|
||||||
|
"num_res_blocks": 2,
|
||||||
|
"attn_scales": [],
|
||||||
|
"temperal_downsample": [False, True, True],
|
||||||
|
"dropout": 0.0,
|
||||||
|
"is_residual": True,
|
||||||
|
"in_channels": 12,
|
||||||
|
"out_channels": 12,
|
||||||
|
"patch_size": 2,
|
||||||
|
"scale_factor_spatial": 16,
|
||||||
|
"scale_factor_temporal": 4,
|
||||||
|
"clip_output": False,
|
||||||
|
"latents_mean": [0.0] * 48,
|
||||||
|
"latents_std": [1.0] * 48,
|
||||||
|
}
|
||||||
|
raw = AutoencoderKLWan.from_config(arch)
|
||||||
|
vae = WanVideoVAE38(dtype=torch.float32, device="cpu", pretrained=raw)
|
||||||
|
assert vae.z_dim == 48
|
||||||
|
assert vae.upsampling_factor == 16
|
||||||
|
assert vae.temporal_downsample_factor == 4
|
||||||
|
|
||||||
|
video = torch.rand(1, 3, 5, 32, 32) * 2 - 1 # [B,C,T,H,W] in [-1,1]
|
||||||
|
latents = vae.encode(video)
|
||||||
|
assert latents.shape == (1, 48, 2, 2, 2) # T'=(5-1)//4+1, H'=W'=32//16
|
||||||
|
|
||||||
|
decoded = vae.decode(latents)
|
||||||
|
assert decoded.shape[0] == 1 and decoded.shape[1] == 3 and decoded.shape[-2:] == (32, 32)
|
||||||
|
assert decoded.min() >= -1.0 and decoded.max() <= 1.0
|
||||||
|
|
||||||
|
# list input is accepted and equals the batched path
|
||||||
|
assert torch.equal(vae.encode([video[0]]), latents)
|
||||||
|
|||||||
@@ -1636,18 +1636,6 @@ http = [
|
|||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ftfy"
|
|
||||||
version = "6.3.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "wcwidth" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a5/d3/8650919bc3c7c6e90ee3fa7fd618bf373cbbe55dff043bd67353dbb20cd8/ftfy-6.3.1.tar.gz", hash = "sha256:9b3c3d90f84fb267fe64d375a07b7f8912d817cf86009ae134aa03e1819506ec", size = 308927, upload-time = "2024-10-26T00:50:35.149Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ab/6e/81d47999aebc1b155f81eca4477a616a70f238a2549848c38983f3c22a82/ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083", size = 44821, upload-time = "2024-10-26T00:50:33.425Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "future"
|
name = "future"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -2708,7 +2696,6 @@ all = [
|
|||||||
{ name = "faker" },
|
{ name = "faker" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
{ name = "feetech-servo-sdk" },
|
{ name = "feetech-servo-sdk" },
|
||||||
{ name = "ftfy" },
|
|
||||||
{ name = "grpcio" },
|
{ name = "grpcio" },
|
||||||
{ name = "grpcio-tools" },
|
{ name = "grpcio-tools" },
|
||||||
{ name = "gym-aloha" },
|
{ name = "gym-aloha" },
|
||||||
@@ -2747,7 +2734,6 @@ all = [
|
|||||||
{ name = "pyzmq" },
|
{ name = "pyzmq" },
|
||||||
{ name = "qwen-vl-utils" },
|
{ name = "qwen-vl-utils" },
|
||||||
{ name = "reachy2-sdk" },
|
{ name = "reachy2-sdk" },
|
||||||
{ name = "regex" },
|
|
||||||
{ name = "rerun-sdk" },
|
{ name = "rerun-sdk" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
{ name = "scikit-image" },
|
{ name = "scikit-image" },
|
||||||
@@ -2846,8 +2832,6 @@ evaluation = [
|
|||||||
]
|
]
|
||||||
fastwam = [
|
fastwam = [
|
||||||
{ name = "diffusers" },
|
{ name = "diffusers" },
|
||||||
{ name = "ftfy" },
|
|
||||||
{ name = "regex" },
|
|
||||||
{ name = "transformers" },
|
{ name = "transformers" },
|
||||||
]
|
]
|
||||||
feetech = [
|
feetech = [
|
||||||
@@ -3108,7 +3092,6 @@ requires-dist = [
|
|||||||
{ name = "fastapi", marker = "extra == 'phone'", specifier = "<1.0" },
|
{ name = "fastapi", marker = "extra == 'phone'", specifier = "<1.0" },
|
||||||
{ name = "feetech-servo-sdk", marker = "extra == 'feetech'", specifier = ">=1.0.0,<2.0.0" },
|
{ name = "feetech-servo-sdk", marker = "extra == 'feetech'", specifier = ">=1.0.0,<2.0.0" },
|
||||||
{ name = "flash-attn", marker = "sys_platform != 'darwin' and extra == 'groot'", specifier = ">=2.5.9,<3.0.0" },
|
{ name = "flash-attn", marker = "sys_platform != 'darwin' and extra == 'groot'", specifier = ">=2.5.9,<3.0.0" },
|
||||||
{ name = "ftfy", marker = "extra == 'fastwam'", specifier = ">=6.1.1,<7.0.0" },
|
|
||||||
{ name = "grpcio", marker = "extra == 'grpcio-dep'", specifier = "==1.73.1" },
|
{ name = "grpcio", marker = "extra == 'grpcio-dep'", specifier = "==1.73.1" },
|
||||||
{ name = "grpcio-tools", marker = "extra == 'dev'", specifier = "==1.73.1" },
|
{ name = "grpcio-tools", marker = "extra == 'dev'", specifier = "==1.73.1" },
|
||||||
{ name = "gym-aloha", marker = "extra == 'aloha'", specifier = ">=0.1.2,<0.2.0" },
|
{ name = "gym-aloha", marker = "extra == 'aloha'", specifier = ">=0.1.2,<0.2.0" },
|
||||||
@@ -3277,7 +3260,6 @@ requires-dist = [
|
|||||||
{ name = "pyzmq", marker = "extra == 'pyzmq-dep'", specifier = ">=26.2.1,<28.0.0" },
|
{ name = "pyzmq", marker = "extra == 'pyzmq-dep'", specifier = ">=26.2.1,<28.0.0" },
|
||||||
{ name = "qwen-vl-utils", marker = "extra == 'qwen-vl-utils-dep'", specifier = ">=0.0.11,<0.1.0" },
|
{ name = "qwen-vl-utils", marker = "extra == 'qwen-vl-utils-dep'", specifier = ">=0.0.11,<0.1.0" },
|
||||||
{ name = "reachy2-sdk", marker = "extra == 'reachy2'", specifier = ">=1.0.15,<1.1.0" },
|
{ name = "reachy2-sdk", marker = "extra == 'reachy2'", specifier = ">=1.0.15,<1.1.0" },
|
||||||
{ name = "regex", marker = "extra == 'fastwam'", specifier = ">=2024.0.0,<2027.0.0" },
|
|
||||||
{ name = "requests", specifier = ">=2.32.0,<3.0.0" },
|
{ name = "requests", specifier = ">=2.32.0,<3.0.0" },
|
||||||
{ name = "rerun-sdk", marker = "extra == 'viz'", specifier = ">=0.24.0,<0.27.0" },
|
{ name = "rerun-sdk", marker = "extra == 'viz'", specifier = ">=0.24.0,<0.27.0" },
|
||||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.14.1" },
|
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.14.1" },
|
||||||
|
|||||||
Reference in New Issue
Block a user