-
Notifications
You must be signed in to change notification settings - Fork 232
[Train] Support Fastwam training #1241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Musisoul
wants to merge
10
commits into
main
Choose a base branch
from
train-ros
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+4,168
−3
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
ab8610b
fastwam-libero training
Musisoul ca2d584
u
Musisoul a43e410
update yaml
Musisoul a0541ec
zero1
Musisoul b73d58e
update save-resume
Musisoul eaa715e
update
Musisoul fdef47a
lint
Musisoul 8462755
lint
Musisoul fcd6494
u
Musisoul daa58cf
u
Musisoul File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
62 changes: 62 additions & 0 deletions
62
lightx2v_train/configs/train/fastwam/libero_uncond_2cam224.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| model: | ||
| name: wan_fastwam | ||
| running_dtype: bf16 | ||
| model_path: /path/to/Wan-AI/Wan2.2-TI2V-5B | ||
|
|
||
| data: | ||
| train: &libero_data | ||
| name: libero_fastwam_dataset | ||
| dataset_dirs: | ||
| # https://huggingface.co/datasets/yuanty/LIBERO-fastwam | ||
| - /path/to/LIBERO-fastwam/libero_spatial_no_noops_lerobot | ||
| - /path/to/LIBERO-fastwam/libero_object_no_noops_lerobot | ||
| - /path/to/LIBERO-fastwam/libero_goal_no_noops_lerobot | ||
| - /path/to/LIBERO-fastwam/libero_10_no_noops_lerobot | ||
| batch_size: 8 | ||
| num_workers: 8 | ||
| val: | ||
| <<: *libero_data | ||
|
|
||
| training: | ||
| method: fastwam | ||
| output_dir: runs/fastwam_libero_full | ||
| # Equivalent to the official 10-epoch, global-batch-128 LIBERO schedule. | ||
| max_train_iters: 21700 | ||
| gradient_accumulation_iters: 4 | ||
| max_grad_norm: 1.0 | ||
| save_every_iters: 2000 | ||
| save_total_limit: 2 | ||
| save_final: true | ||
| lr_scheduler: cosine | ||
| lr_warmup_iters: 1085 | ||
| optimizer: | ||
| learning_rate: 1.0e-4 | ||
| weight_decay: 1.0e-2 | ||
| adam_beta1: 0.9 | ||
| adam_beta2: 0.95 | ||
| adam_epsilon: 1.0e-8 | ||
|
|
||
| evaluation: | ||
| eval_every_iters: 200 | ||
| eval_num_inference_steps: 5 | ||
| num_samples: 1 | ||
| run_inference: true | ||
| save_video: true | ||
| save_preview: true | ||
| fps: 8 | ||
| tiled: false | ||
| seed: 42 | ||
|
|
||
| logging: | ||
| train_log_every_iters: 10 | ||
|
|
||
| resume: | ||
| auto_resume: true | ||
| resume_ckpt_path: null | ||
|
|
||
| distributed: | ||
| backend: nccl | ||
| zero1: | ||
| enabled: true | ||
| sequence_parallel: | ||
| enabled: false |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,17 @@ | ||
| from lightx2v_train.utils.registry import build_data | ||
|
|
||
| from .image_dataset import build_image_dataset | ||
| from .libero.dataset import build_libero_fastwam_dataset | ||
| from .preparation import prepare_data | ||
| from .video_dataset import build_causal_forcing_lmdb_dataset, build_prompt_dataset, build_wan_t2v_cached_dataset, build_wan_t2v_video_dataset | ||
|
|
||
| __all__ = [ | ||
| "build_data", | ||
| "build_image_dataset", | ||
| "build_libero_fastwam_dataset", | ||
| "build_prompt_dataset", | ||
| "build_wan_t2v_video_dataset", | ||
| "build_wan_t2v_cached_dataset", | ||
| "build_causal_forcing_lmdb_dataset", | ||
| "prepare_data", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from .dataset import build_libero_fastwam_dataset | ||
|
|
||
| __all__ = ["build_libero_fastwam_dataset"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| from copy import deepcopy | ||
| from pathlib import Path | ||
|
|
||
| import torch | ||
| from loguru import logger | ||
| from torch.utils.data import DataLoader, DistributedSampler | ||
|
|
||
| from lightx2v_train.runtime.distributed import get_data_parallel_rank, get_data_parallel_world_size | ||
| from lightx2v_train.utils.registry import DATA_REGISTER | ||
|
|
||
| from .processor import FastWAMProcessor | ||
| from .robot_video_dataset import RobotVideoDataset | ||
|
|
||
|
|
||
| def _default_shape_meta(): | ||
| return { | ||
| "images": [ | ||
| {"key": "image", "raw_shape": [3, 512, 512], "shape": [3, 224, 224]}, | ||
| {"key": "wrist_image", "raw_shape": [3, 512, 512], "shape": [3, 224, 224]}, | ||
| ], | ||
| "action": [{"key": "default", "raw_shape": 7, "shape": 7}], | ||
| "state": [{"key": "default", "raw_shape": 8, "shape": 8}], | ||
| } | ||
|
|
||
|
|
||
| class DatasetSliceRepeat(torch.utils.data.Dataset): | ||
| def __init__(self, dataset, max_samples=None, dataset_repeat=1): | ||
| self.dataset = dataset | ||
| self.base_len = len(dataset) if max_samples is None else min(int(max_samples), len(dataset)) | ||
| self.dataset_repeat = max(1, int(dataset_repeat)) | ||
| if self.base_len <= 0: | ||
| raise RuntimeError("LIBERO dataset is empty after applying max_samples.") | ||
|
|
||
| def __len__(self): | ||
| return self.base_len * self.dataset_repeat | ||
|
|
||
| def __getitem__(self, index): | ||
| return self.dataset[index % self.base_len] | ||
|
|
||
|
|
||
| def _path(value): | ||
| return str(Path(value).expanduser().resolve()) | ||
|
|
||
|
|
||
| def _build_dataset(config, split): | ||
| shape_meta = deepcopy(config.get("shape_meta") or _default_shape_meta()) | ||
| num_frames = int(config.get("num_frames", 33)) | ||
| processor = FastWAMProcessor(shape_meta, num_frames) | ||
| dataset_dirs = config.get("dataset_dirs") | ||
| if isinstance(dataset_dirs, (str, Path)): | ||
| dataset_dirs = [dataset_dirs] | ||
|
|
||
| dataset = RobotVideoDataset( | ||
| dataset_dirs=[_path(item) for item in dataset_dirs], | ||
| shape_meta=shape_meta, | ||
| processor=processor, | ||
| text_embedding_cache_dir=_path(config["text_embedding_cache_dir"]), | ||
| pretrained_norm_stats=_path(config["pretrained_norm_stats"]), | ||
| num_frames=num_frames, | ||
| context_len=int(config.get("context_len", 128)), | ||
| val_set_proportion=float(config.get("val_set_proportion", 0.0)), | ||
| is_training_set=bool(config.get("is_training_set", split == "train")), | ||
| global_sample_stride=int(config.get("global_sample_stride", 1)), | ||
| action_video_freq_ratio=int(config.get("action_video_freq_ratio", 4)), | ||
| skip_padding_as_possible=bool(config.get("skip_padding_as_possible", False)), | ||
| max_padding_retry=int(config.get("max_padding_retry", 3)), | ||
| video_backend=config.get("video_backend"), | ||
| ) | ||
| logger.info("[data] built LIBERO FastWAM {} dataset size={}", split, len(dataset)) | ||
| return DatasetSliceRepeat( | ||
| dataset, | ||
| max_samples=config.get("max_samples"), | ||
| dataset_repeat=config.get("dataset_repeat", 1), | ||
| ) | ||
|
|
||
|
|
||
| @DATA_REGISTER("libero_fastwam_dataset") | ||
| def build_libero_fastwam_dataset(data_config, train_or_val="train"): | ||
| dataset = _build_dataset(data_config, train_or_val) | ||
| if data_config.get("return_dataset", False): | ||
| return dataset | ||
|
|
||
| sampler = None | ||
| shuffle = bool(data_config.get("shuffle", train_or_val == "train")) | ||
| world_size = get_data_parallel_world_size() | ||
| if train_or_val == "train" and world_size > 1: | ||
| sampler = DistributedSampler( | ||
| dataset, | ||
| num_replicas=world_size, | ||
| rank=get_data_parallel_rank(), | ||
| shuffle=shuffle, | ||
| drop_last=bool(data_config.get("drop_last", False)), | ||
| ) | ||
| shuffle = False | ||
| return DataLoader( | ||
| dataset, | ||
| batch_size=int(data_config.get("batch_size", 1)), | ||
| shuffle=shuffle, | ||
| sampler=sampler, | ||
| num_workers=int(data_config.get("num_workers", 4)), | ||
| pin_memory=bool(data_config.get("pin_memory", torch.cuda.is_available())), | ||
| drop_last=bool(data_config.get("drop_last", False)), | ||
| ) |
187 changes: 187 additions & 0 deletions
187
lightx2v_train/lightx2v_train/data/libero/lerobot_dataset.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| import bisect | ||
| import json | ||
| from dataclasses import dataclass | ||
| from functools import lru_cache | ||
| from pathlib import Path | ||
|
|
||
| import numpy as np | ||
| import pyarrow as pa | ||
| import pyarrow.parquet as pq | ||
| import torch | ||
| from torch.utils.data import Dataset | ||
|
|
||
| from .video_decoder import decode_video_frames | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class Episode: | ||
| root: Path | ||
| index: int | ||
| length: int | ||
| data_path: str | ||
| video_path: str | ||
| chunks_size: int | ||
| tasks: dict[int, str] | ||
|
|
||
| def parquet_path(self) -> Path: | ||
| return self.root / self.data_path.format( | ||
| episode_chunk=self.index // self.chunks_size, | ||
| episode_index=self.index, | ||
| ) | ||
|
|
||
| def camera_path(self, key: str) -> Path: | ||
| return self.root / self.video_path.format( | ||
| episode_chunk=self.index // self.chunks_size, | ||
| episode_index=self.index, | ||
| video_key=key, | ||
| ) | ||
|
|
||
|
|
||
| def _read_json(path: Path): | ||
| with path.open(encoding="utf-8") as handle: | ||
| return json.load(handle) | ||
|
|
||
|
|
||
| def _read_jsonl(path: Path): | ||
| with path.open(encoding="utf-8") as handle: | ||
| return [json.loads(line) for line in handle if line.strip()] | ||
|
|
||
|
|
||
| def _column_to_tensor(column: pa.ChunkedArray) -> torch.Tensor: | ||
| column = column.combine_chunks() | ||
| if pa.types.is_list(column.type) or pa.types.is_fixed_size_list(column.type): | ||
| array = np.asarray(column.to_pylist()) | ||
| else: | ||
| array = column.to_numpy(zero_copy_only=False) | ||
| return torch.from_numpy(np.array(array, copy=True)) | ||
|
|
||
|
|
||
| class LiberoLeRobotDataset(Dataset): | ||
| """Minimal local reader for FastWAM's processed LIBERO LeRobot v2.1 data.""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| dataset_dirs, | ||
| image_keys, | ||
| state_key, | ||
| action_key, | ||
| num_frames, | ||
| global_sample_stride=1, | ||
| val_set_proportion=0.0, | ||
| is_training_set=True, | ||
| seed=42, | ||
| video_backend=None, | ||
| ): | ||
| if num_frames < 2: | ||
| raise ValueError(f"num_frames must be at least 2, got {num_frames}") | ||
| self.image_keys = list(image_keys) | ||
| self.state_key = state_key | ||
| self.action_key = action_key | ||
| self.num_frames = int(num_frames) | ||
| self.global_sample_stride = int(global_sample_stride) | ||
| self.video_backend = video_backend | ||
|
|
||
| episodes = [] | ||
| fps_values = set() | ||
| for dataset_dir in dataset_dirs: | ||
| root = Path(dataset_dir).expanduser().resolve() | ||
| info = _read_json(root / "meta" / "info.json") | ||
| tasks = {int(item["task_index"]): item["task"] for item in _read_jsonl(root / "meta" / "tasks.jsonl")} | ||
| episode_meta = _read_jsonl(root / "meta" / "episodes.jsonl") | ||
| fps_values.add(int(info["fps"])) | ||
| self._validate_features(root, info["features"]) | ||
|
|
||
| indices = list(range(len(episode_meta))) | ||
| if val_set_proportion >= 1e-6: | ||
| rng = np.random.default_rng(seed) | ||
| rng.shuffle(indices) | ||
| split = int(len(indices) * (1.0 - val_set_proportion)) | ||
| indices = indices[:split] if is_training_set else indices[split:] | ||
|
|
||
| by_index = {int(item["episode_index"]): item for item in episode_meta} | ||
| for episode_index in indices: | ||
| item = by_index[episode_index] | ||
| episodes.append( | ||
| Episode( | ||
| root=root, | ||
| index=episode_index, | ||
| length=int(item["length"]), | ||
| data_path=info["data_path"], | ||
| video_path=info["video_path"], | ||
| chunks_size=int(info["chunks_size"]), | ||
| tasks=tasks, | ||
| ) | ||
| ) | ||
|
|
||
| if not episodes: | ||
| raise RuntimeError("No LIBERO episodes were selected.") | ||
| if len(fps_values) != 1: | ||
| raise ValueError(f"All LIBERO datasets must have the same fps, got {sorted(fps_values)}") | ||
| self.fps = fps_values.pop() | ||
| self.episodes = episodes | ||
| self._episode_ends = np.cumsum([episode.length for episode in episodes]).tolist() | ||
|
|
||
| def _validate_features(self, root, features): | ||
| required = [*self.image_keys, self.state_key, self.action_key, "timestamp", "task_index"] | ||
| missing = [key for key in required if key not in features] | ||
| if missing: | ||
| raise ValueError(f"LIBERO dataset {root} is missing features: {missing}") | ||
| non_video = [key for key in self.image_keys if features[key].get("dtype") != "video"] | ||
| if non_video: | ||
| raise ValueError(f"LIBERO camera features must be videos, got: {non_video}") | ||
|
|
||
| def __len__(self): | ||
| return self._episode_ends[-1] | ||
|
|
||
| @lru_cache(maxsize=32) | ||
| def _load_episode(self, episode_position): | ||
| episode = self.episodes[episode_position] | ||
| table = pq.read_table(episode.parquet_path()) | ||
| return {key: _column_to_tensor(table[key]) for key in table.column_names} | ||
|
|
||
| def _locate(self, index): | ||
| if index < 0: | ||
| index += len(self) | ||
| if index < 0 or index >= len(self): | ||
| raise IndexError(f"Index {index} out of bounds for dataset of size {len(self)}") | ||
| episode_position = bisect.bisect_right(self._episode_ends, index) | ||
| episode_start = 0 if episode_position == 0 else self._episode_ends[episode_position - 1] | ||
| return episode_position, index - episode_start | ||
|
|
||
| def _window(self, values, frame_index, length): | ||
| offsets = torch.arange(length, dtype=torch.long) * self.global_sample_stride | ||
| indices = frame_index + offsets | ||
| is_pad = indices >= values.shape[0] | ||
| indices.clamp_(max=values.shape[0] - 1) | ||
| return values[indices], is_pad, indices | ||
|
|
||
| def __getitem__(self, index): | ||
| episode_position, frame_index = self._locate(index) | ||
| episode = self.episodes[episode_position] | ||
| data = self._load_episode(episode_position) | ||
|
|
||
| state, state_is_pad, observation_indices = self._window(data[self.state_key], frame_index, self.num_frames) | ||
| action, action_is_pad, _ = self._window(data[self.action_key], frame_index, self.num_frames - 1) | ||
| timestamps = data["timestamp"][observation_indices].float().tolist() | ||
| tolerance = max(1e-4, 1.0 / self.fps - 1e-4) | ||
| images = { | ||
| key: decode_video_frames( | ||
| episode.camera_path(key), | ||
| timestamps, | ||
| tolerance_s=tolerance, | ||
| backend=self.video_backend, | ||
| ) | ||
| for key in self.image_keys | ||
| } | ||
|
|
||
| task_index = int(data["task_index"][frame_index].item()) | ||
| return { | ||
| "idx": int(index), | ||
| "task": episode.tasks[task_index], | ||
| "action": {"default": action.float()}, | ||
| "state": {"default": state.float()}, | ||
| "images": {key.removeprefix("observation.images."): value for key, value in images.items()}, | ||
| "action_is_pad": action_is_pad, | ||
| "state_is_pad": state_is_pad, | ||
| "image_is_pad": state_is_pad.clone(), | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.