| """ |
| Sequential Episode DataLoader implementation. |
| |
| This DataLoader initializes batches by selecting random episodes and then |
| returning samples sequentially from each episode starting from the first step. |
| When reset, it selects new random episodes and starts from the first step again. |
| """ |
|
|
| from collections.abc import Iterator |
| import logging |
| |
| logging.basicConfig(level=logging.INFO) |
| logger = logging.getLogger(__name__) |
| import random |
| from typing import Literal, Optional, List, Dict, Any, Callable, Tuple |
|
|
| import jax |
| import numpy as np |
| import torch |
| from torch.utils.data import DataLoader as TorchDataLoaderBase |
| from torch.utils.data import Sampler |
|
|
| import openpi_value.training.config as _config |
| import openpi_value.models.model as _model |
|
|
|
|
| class SequentialEpisodeSampler(Sampler): |
| """ |
| Sampler that samples batches where each element in the batch has its own independent episode sequence. |
| |
| Each element in the batch maintains its own episode state and can be reset independently. |
| |
| Args: |
| dataset: The dataset to sample from |
| batch_size: Number of elements (episodes) in each batch |
| shuffle_episodes: Whether to shuffle the episodes when resetting |
| seed: Random seed for reproducibility |
| """ |
| def __init__( |
| self, |
| dataset, |
| batch_size: int, |
| shuffle_episodes: bool = True, |
| seed: int = 0 |
| ): |
| self.dataset = dataset._dataset |
| |
| |
| |
| |
| self.batch_size = batch_size |
| self.shuffle_episodes = shuffle_episodes |
| self.seed = seed |
| self.rng = random.Random(seed) |
| |
| self.episode_to_frames: Dict[int, List[int]] = {} |
| |
| if hasattr(self.dataset, '_datasets'): |
| current_frame_offset = 0 |
| global_ep_counter = 0 |
| |
| for sub_ds in self.dataset._datasets: |
| |
| sub_mapping = self._extract_frames_from_single_ds(sub_ds) |
| |
| |
| for local_ep_id, local_frames in sub_mapping.items(): |
| |
| unique_ep_id = global_ep_counter |
| |
| |
| global_frames = [f + current_frame_offset for f in local_frames] |
| |
| if len(global_frames) > 0: |
| self.episode_to_frames[unique_ep_id] = global_frames |
| global_ep_counter += 1 |
| |
| |
| current_frame_offset += len(sub_ds) |
| |
| |
| else: |
| |
| sub_mapping = self._extract_frames_from_single_ds(self.dataset) |
| |
| self.episode_to_frames = {k: v for k, v in sub_mapping.items() if len(v) > 0} |
| |
| |
| self.episode_indices = sorted(list(self.episode_to_frames.keys())) |
| self.num_episodes = len(self.episode_indices) |
| self.step_chunk_size = 1 |
| |
| |
| if self.num_episodes < 1: |
| raise ValueError(f"Not enough episodes in dataset. Need at least 1, " |
| f"but only found {self.num_episodes}") |
| |
| |
| |
| self.element_states = [] |
| for i in range(batch_size): |
| |
| element_rng = random.Random(seed + i) |
| self.element_states.append({ |
| 'current_episode': None, |
| 'current_step': 0, |
| 'rng': element_rng |
| }) |
| |
| |
| self.reset() |
| |
| logger.info(f"SequentialEpisodeSampler initialized with {self.num_episodes} episodes") |
| logger.info(f"Batch size: {self.batch_size}, shuffle_episodes: {self.shuffle_episodes}") |
| |
| |
| def _extract_frames_from_single_ds(self, dataset) -> Dict[int, List[int]]: |
| """ |
| Helper to extract {local_ep_id: [local_frame_indices]} from a single dataset. |
| Does NOT handle global offsets. |
| """ |
| mapping = {} |
| |
| has_skipping = ( |
| hasattr(dataset, 'index_map') and |
| dataset.index_map is not None |
| ) |
|
|
| |
| if hasattr(dataset, 'episode_data_index') and hasattr(dataset, 'episodes'): |
| |
| |
| |
| virtual_idx_counter = 0 |
| |
| |
| |
| num_episodes = len(dataset.episode_data_index['from']) |
| |
| for arr_idx in range(num_episodes): |
| |
| start = dataset.episode_data_index['from'][arr_idx].item() |
| end = dataset.episode_data_index['to'][arr_idx].item() |
| raw_len = end - start |
| |
| |
| if has_skipping: |
| |
| ratio_start = getattr(dataset, 'preceding_skipping_ratio', 0.0) |
| skip_n_start = int(raw_len * ratio_start) |
| |
| |
| ratio_end = getattr(dataset, 'trailing_skipping_ratio', 0.0) |
| skip_n_end = int(raw_len * ratio_end) |
|
|
| |
| max_allowed_skip_end = raw_len - skip_n_start - 1 |
| |
| if max_allowed_skip_end < 0: |
| |
| skip_n_start = raw_len - 1 |
| skip_n_end = 0 |
| else: |
| |
| skip_n_end = min(skip_n_end, max_allowed_skip_end) |
|
|
| valid_len = raw_len - skip_n_start - skip_n_end |
| else: |
| valid_len = raw_len |
|
|
| |
| |
| |
| ep_idx = dataset.episodes[arr_idx] |
| |
| |
| |
| mapping[ep_idx] = list(range(virtual_idx_counter, virtual_idx_counter + valid_len)) |
| |
| |
| virtual_idx_counter += valid_len |
| |
| else: |
| |
| |
| |
| raise NotImplementedError("Dataset type not supported in multi-dataset extraction.") |
| |
| return mapping |
|
|
| def reset(self, reset_episode_state: Optional[List[Tuple[int, int]]] = None, element_indices: Optional[List[int]] = None) -> None: |
| """ |
| Reset the sampler: select new random episodes for elements and start from the first step. |
| |
| Args: |
| element_indices: If specified, only reset these particular elements. |
| If None, reset all elements. |
| """ |
| |
| if not hasattr(self, 'episode_lengths'): |
| self.episode_lengths: Dict[int, int] = {ep: len(frames) for ep, frames in self.episode_to_frames.items()} |
| |
| |
| elements_to_reset = element_indices if element_indices is not None else range(self.batch_size) |
| |
| for idx in elements_to_reset: |
| if idx < 0 or idx >= self.batch_size: |
| raise ValueError(f"Invalid element index: {idx}. Must be between 0 and {self.batch_size - 1}") |
| |
| element_state = self.element_states[idx] |
| if reset_episode_state is not None: |
| element_state['current_episode'] = reset_episode_state[idx][0] |
| element_state['current_step'] = reset_episode_state[idx][1] |
| else: |
| |
| if self.shuffle_episodes: |
| |
| selected_ep = element_state['rng'].choice(self.episode_indices) |
| else: |
| |
| current_ep = element_state['current_episode'] if element_state['current_episode'] is not None else 0 |
| current_idx = self.episode_indices.index(current_ep) if current_ep in self.episode_indices else 0 |
| selected_ep = self.episode_indices[(current_idx + 1) % self.num_episodes] |
| |
| element_state['current_episode'] = selected_ep |
| element_state['current_step'] = element_state['rng'].randint(0, self.episode_lengths[element_state['current_episode']] - 1) |
| |
| logger.debug(f"Element {idx} reset. Selected episode: {selected_ep}, starting at step 0") |
|
|
| def __iter__(self): |
| """ |
| Iterate over the dataset, yielding batches where each element in the batch |
| independently progresses through its own episode sequence. |
| """ |
| |
| if not hasattr(self, 'episode_lengths'): |
| self.episode_lengths: Dict[int, int] = {ep: len(frames) for ep, frames in self.episode_to_frames.items()} |
| |
| while True: |
| batch_indices = [] |
| |
| for i in range(self.batch_size): |
| element_state = self.element_states[i] |
| |
| |
| if element_state['current_step'] >= self.episode_lengths[element_state['current_episode']]: |
| |
| new_ep_idx = self._get_new_episode(element_state['current_episode'], element_state['rng']) |
| element_state['current_episode'] = new_ep_idx |
| element_state['current_step'] = element_state['rng'].randint(0, self.episode_lengths[element_state['current_episode']] - 1) |
| |
| |
| frame_idx = self.episode_to_frames[element_state['current_episode']][element_state['current_step']] |
| batch_indices.append(frame_idx) |
| |
| |
| element_state['current_step'] += self.step_chunk_size |
| |
| |
| assert len(batch_indices) == self.batch_size, \ |
| f"Batch size mismatch: expected {self.batch_size}, got {len(batch_indices)}" |
| |
| yield batch_indices |
|
|
| def _get_new_episode(self, exclude_ep_idx: int, rng: Optional[random.Random] = None) -> int: |
| """Get a new episode index that is not the excluded one""" |
| |
| rng = rng or self.rng |
| |
| |
| available_episodes = [ep for ep in self.episode_indices if ep != exclude_ep_idx] |
| |
| |
| |
| if not available_episodes: |
| return exclude_ep_idx |
| |
| |
| return rng.choice(available_episodes) |
|
|
| def __len__(self): |
|
|
| return len(self.dataset) |
|
|
|
|
| class SequentialEpisodeDataLoader: |
| """ |
| DataLoader that initializes batches by selecting episodes and then returning |
| samples sequentially from each episode starting from the first step. |
| |
| Args: |
| dataset: The dataset to load data from |
| batch_size: Number of episodes to include in each batch |
| shuffle_episodes: Whether to shuffle the episodes when resetting |
| num_workers: Number of worker processes for data loading |
| pin_memory: Whether to pin memory in DataLoader |
| seed: Random seed for reproducibility |
| framework: The framework to use ("jax" or "pytorch") |
| sharding: JAX sharding configuration (only for JAX framework) |
| """ |
| def __init__( |
| self, |
| dataset, |
| batch_size: int, |
| shuffle_episodes: bool = True, |
| num_workers: int = 0, |
| pin_memory: bool = False, |
| seed: int = 0, |
| framework: Literal["jax", "pytorch"] = "pytorch", |
| sharding: Optional[jax.sharding.Sharding] = None, |
| ): |
| self.dataset = dataset |
| self.batch_size = batch_size |
| self.shuffle_episodes = shuffle_episodes |
| self.num_workers = num_workers |
| self.pin_memory = pin_memory |
| self.seed = seed |
| self.framework = framework |
| self.sharding = sharding |
| |
| |
| self.sampler = SequentialEpisodeSampler( |
| dataset=dataset, |
| batch_size=batch_size, |
| shuffle_episodes=shuffle_episodes, |
| seed=seed |
| ) |
| |
| |
| self.torch_loader = TorchDataLoaderBase( |
| dataset=dataset, |
| batch_sampler=self.sampler, |
| num_workers=num_workers, |
| pin_memory=pin_memory, |
| collate_fn=self._collate_fn, |
| worker_init_fn=self._worker_init_fn, |
| ) |
| |
| |
| if self.sharding is None and framework == "jax": |
| |
| self.sharding = jax.sharding.NamedSharding( |
| jax.sharding.Mesh(jax.devices(), ("B",)), |
| jax.sharding.PartitionSpec("B"), |
| ) |
|
|
| def _collate_fn(self, items): |
| """Collate function to handle dictionary items""" |
| |
| |
| return jax.tree.map(lambda *xs: np.stack([np.asarray(x) for x in xs], axis=0), *items) |
|
|
| def _worker_init_fn(self, worker_id: int) -> None: |
| """Worker initialization function""" |
| |
| worker_seed = self.seed + worker_id |
| random.seed(worker_seed) |
| np.random.seed(worker_seed) |
| |
| |
| if self.framework == "jax": |
| import os |
| os.environ["XLA_PYTHON_CLIENT_PREALLOCATE"] = "false" |
| os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" |
|
|
| def reset(self, reset_episode_state: Optional[List[Tuple[int, int]]] = None, element_indices: Optional[List[int]] = None) -> None: |
| """ |
| Reset the data loader: select new random episodes for elements and start from the first step. |
| |
| Args: |
| element_indices: If specified, only reset these particular elements. |
| If None, reset all elements. |
| """ |
| self.sampler.reset(reset_episode_state, element_indices) |
| |
| if element_indices is None: |
| logger.info("DataLoader reset: all elements have new episodes") |
| else: |
| logger.info(f"DataLoader reset: elements {element_indices} have new episodes") |
|
|
| def __iter__(self): |
| """Iterate over batches""" |
| self._iterator = iter(self.torch_loader) |
| return self |
|
|
| def __next__(self): |
| """Get the next batch""" |
| if self._iterator is None: |
| self._iterator = iter(self.torch_loader) |
|
|
| batch = next(self._iterator) |
|
|
| if self.framework == "jax" and self.sharding is not None: |
| |
| return jax.tree.map(lambda x: jax.make_array_from_process_local_data(self.sharding, x), batch) |
| else: |
| |
| return jax.tree.map(torch.as_tensor, batch) |
|
|
| def __len__(self): |
| """Number of batches""" |
| return len(self.sampler) |
|
|
|
|
| class SequentialEpisodeDataLoaderImpl: |
| """Implementation of the DataLoader interface using SequentialEpisodeDataLoader""" |
| |
| def __init__(self, data_config: _config.DataConfig, data_loader: SequentialEpisodeDataLoader): |
| self._data_config = data_config |
| self._data_loader = data_loader |
|
|
| def data_config(self) -> _config.DataConfig: |
| return self._data_config |
|
|
| def reset(self) -> None: |
| """Reset the data loader""" |
| self._data_loader.reset() |
|
|
| def __iter__(self): |
| for batch in self._data_loader: |
| yield _model.Observation.from_dict(batch), batch["actions"] |
|
|
|
|
| def create_sequential_episode_data_loader( |
| data_config: _config.DataConfig, |
| model_config: _model.BaseModelConfig, |
| action_horizon: int, |
| batch_size: int, |
| *, |
| sharding: Optional[jax.sharding.Sharding] = None, |
| skip_norm_stats: bool = False, |
| shuffle_episodes: bool = True, |
| num_workers: int = 0, |
| seed: int = 0, |
| framework: Literal["jax", "pytorch"] = "jax", |
| config: Optional[Any] = None, |
| ) -> SequentialEpisodeDataLoaderImpl: |
| """ |
| Create a sequential episode-based data loader for training. |
| |
| Args: |
| data_config: The data configuration |
| model_config: The model configuration |
| action_horizon: The action horizon |
| batch_size: Number of episodes per batch |
| sharding: JAX sharding configuration |
| skip_norm_stats: Whether to skip data normalization |
| shuffle_episodes: Whether to shuffle episodes when resetting |
| num_workers: Number of worker processes |
| seed: Random seed |
| framework: Framework to use ("jax" or "pytorch") |
| config: Additional configuration |
| """ |
| |
| from openpi_value.training.data_loader import create_torch_dataset, transform_dataset |
| |
| dataset = create_torch_dataset( |
| data_config=data_config, |
| action_horizon=action_horizon, |
| model_config=model_config, |
| config=config |
| ) |
| |
| |
| dataset = transform_dataset( |
| dataset=dataset, |
| data_config=data_config, |
| skip_norm_stats=skip_norm_stats |
| ) |
| |
| |
| local_batch_size = batch_size |
| if framework == "pytorch" and torch.distributed.is_initialized(): |
| local_batch_size = batch_size // torch.distributed.get_world_size() |
| elif framework == "jax": |
| local_batch_size = batch_size // jax.process_count() |
| |
| logging.info(f"Creating SequentialEpisodeDataLoader with batch_size: {local_batch_size}") |
| |
| |
| data_loader = SequentialEpisodeDataLoader( |
| dataset=dataset, |
| batch_size=local_batch_size, |
| shuffle_episodes=shuffle_episodes, |
| num_workers=num_workers, |
| pin_memory=True if framework == "pytorch" else False, |
| seed=seed, |
| framework=framework, |
| sharding=sharding, |
| ) |
| |
| return SequentialEpisodeDataLoaderImpl(data_config, data_loader) |
|
|
|
|
| def modify_create_data_loader_for_sequential_episodes(): |
| """ |
| Modify the original create_data_loader function to support sequential episode-based batching. |
| This function should be called after importing the original create_data_loader. |
| """ |
| import openpi_value.training.data_loader as original_dataloader |
| |
| def modified_create_torch_data_loader( |
| data_config: _config.DataConfig, |
| model_config: _model.BaseModelConfig, |
| action_horizon: int, |
| batch_size: int, |
| *, |
| sharding: Optional[jax.sharding.Sharding] = None, |
| skip_norm_stats: bool = False, |
| shuffle: bool = False, |
| num_batches: Optional[int] = None, |
| num_workers: int = 0, |
| seed: int = 0, |
| framework: str = "jax", |
| config: Optional[Any] = None, |
| ): |
| """Modified create_torch_data_loader that supports sequential episode-based batching""" |
| |
| logging.info("Using sequential episode-based data loader") |
| return create_sequential_episode_data_loader( |
| data_config=data_config, |
| model_config=model_config, |
| action_horizon=action_horizon, |
| batch_size=batch_size, |
| sharding=sharding, |
| skip_norm_stats=skip_norm_stats, |
| shuffle_episodes=shuffle, |
| num_workers=num_workers, |
| seed=seed, |
| framework=framework, |
| config=config, |
| ) |
| |
| |
| original_dataloader.create_torch_data_loader = modified_create_torch_data_loader |
| |
| logging.info("Modified create_torch_data_loader to support sequential episode-based batching") |
|
|
|
|
| def create_dataloader_with_sequential_episode(config: _config.TrainConfig, framework = "pytorch", shuffle=True): |
| """ |
| Example function to create a data loader with sequential episode-based batching. |
| |
| Args: |
| config: Training configuration |
| """ |
| |
| modify_create_data_loader_for_sequential_episodes() |
| |
| |
| from openpi_value.training.data_loader import create_data_loader |
| |
| |
| |
| data_loader = create_data_loader(config, framework=framework, shuffle=shuffle) |
| |
| return data_loader |
|
|