Files
bytedance--lance/data/dataset_base_train.py
T
2026-07-13 13:16:54 +08:00

999 lines
51 KiB
Python

# coding: utf-8
import os
EXP_HW_20250819 = os.environ.get("EXP_HW_20250819", "False").lower() == "true"
import random
from dataclasses import dataclass
from typing import Any, Dict, Tuple
import numpy as np
import torch
import yaml
from itertools import chain
import sys
from .data_utils import (
get_flattened_position_ids_interpolate_video,
get_flattened_position_ids_extrapolate_video,
len2weight,
prepare_attention_mask_per_sample,
patchify_video_with_merge,
)
from .dataset_info import DATASET_REGISTRY
from data.video.sampler.utils import FRAME_SAMPLER_TYPES
from data.transforms import ImageTransform, VideoTransform
from data.video.video_utils import FrameSampler
from data.parquet_utils import get_parquet_data_paths_balanced
from common.utils.logging import get_logger
from common.utils.basic import get_global_rank
import bisect
sample_task_map = {
't2v': 0,
'idip': 1,
'edit': 2,
'refedit': 3,
'maze':3,
}
modality_map = {
'system_prompt': -1,
'text': 0,
'noise': 1,
'ref_source': 2, # for vae
'ref_image': 3, # for vae
'ref_vit': 4 # for ref vit
}
@dataclass
class DataConfig:
"""
DataConfig variant where vae_downsample is a 3-tuple.
"""
grouped_datasets: Dict[str, Any]
text_cond_dropout_prob: float = 0.1
vit_cond_dropout_prob: float = 0.4
vae_cond_dropout_prob: float = 0.1
# Use a 3-tuple for vae_downsample: temporal, height, and width downsampling rates
vae_downsample: Tuple[int, int, int] = (4, 16, 16)
max_latent_size: int = 64 # by ModelArguments
vit_patch_size: int = 14 # by ModelArguments
vit_patch_size_temporal: int = 2 # by ModelArguments
vit_max_num_patch_per_side: int = 70 # by ModelArguments
max_num_frames: int = 25 # by ModelArguments
latent_patch_size: int = None # by ModelArguments
@classmethod
def from_yaml(cls, file_path: str) -> 'DataConfig':
"""
Create a DataConfig instance from a YAML file.
"""
with open(file_path, "r") as stream:
data = yaml.safe_load(stream)
return cls(grouped_datasets=data)
class PackedDataset(torch.utils.data.IterableDataset):
def __init__(
self,
data_config: DataConfig,
tokenizer,
special_tokens,
local_rank,
world_size,
num_workers,
expected_num_tokens=32768, # Expected packed sequence length
max_num_tokens_per_sample=16384, # Maximum length of a single sample
max_num_tokens=36864, # Hard limit for packed sequence length
prefer_buffer_before=16384, # Sample-length threshold for preferring the buffer
max_buffer_size=50, # Maximum buffer capacity
interpolate_pos=False,
use_flex=False,
data_status=None,
apply_chat_template=False,
image_token_id=151655,
**kwargs,
):
super().__init__()
self.expected_num_tokens = expected_num_tokens
self.max_num_tokens_per_sample = max_num_tokens_per_sample
self.prefer_buffer_before = prefer_buffer_before
self.max_num_tokens = max_num_tokens
self.max_buffer_size = max_buffer_size
self.tokenizer = tokenizer
self.local_rank = local_rank
self.world_size = world_size
self.num_workers = num_workers
self.use_flex = use_flex
self.data_config: DataConfig = data_config
self.max_num_latent_frames = self.data_config.max_num_frames // self.data_config.vae_downsample[0] + 1
self.apply_chat_template = apply_chat_template
self.cfg_type = kwargs.get("cfg_type", 0)
self.cfg_uncond_token_id = kwargs.get("cfg_uncond_token_id", 151643)
self.image_token_id = image_token_id
self.data_args = kwargs
self.expected_num_ce_loss_tokens = kwargs.get("expected_num_ce_loss_tokens", 1000000) # Default 1000000 effectively means unlimited
self.N_key_frame = kwargs.get("N_key_frame", -1)
self.fbyf_type = kwargs.get("fbyf_type", "group")
self.fbyf_group_interval = kwargs.get("fbyf_group_interval", -1)
self.incre_time_pro = kwargs.get("incre_time_pro", 0)
self.require_und_gen = kwargs.get("require_und_gen", False)
# NOTE: Add special tokens such as <|im_start|> and <|im_end|> here
for k, v in special_tokens.items():
setattr(self, k, v)
# add a logger
self.logger = get_logger()
# self.log_rank0 = lambda msg: self.logger.info(msg) if get_global_rank() == 0 else None # Log only on rank 0
grouped_datasets, is_mandatory, grouped_weights, data_type = self.build_datasets(data_config.grouped_datasets, data_status)
self.grouped_datasets = grouped_datasets
# self.dataset_iters = [iter(dataset) for dataset in grouped_datasets] # this is dataset iter
self.dataset_iters = None # Delay creation until __iter__
self.is_mandatory = is_mandatory
self.grouped_weights = grouped_weights
self.interpolate_pos = interpolate_pos
self.data_type = data_type
# Log only on rank 0; instance methods can be pickled
def log_rank0(self, msg: str):
if get_global_rank() == 0:
self.logger.info(msg)
# Avoid sending non-serializable objects such as loggers or iterators to child processes
def __getstate__(self):
state = self.__dict__.copy()
state["logger"] = None
state["dataset_iters"] = None
return state
def __setstate__(self, state):
self.__dict__.update(state)
self.logger = get_logger()
self.dataset_iters = None
def set_epoch(self, seed):
for dataset in self.grouped_datasets:
dataset.set_epoch(seed)
# for dataset in self.grouped_datasets:
# if hasattr(dataset, 'seed'):
# dataset.set_epoch(dataset.seed)
# else:
# dataset.set_epoch(seed)
def set_sequence_status(self):
sequence_status = dict(
curr = 0, # Pointer
sample_lens = [],
sample_type = [],
sample_N_target = [],
packed_position_ids = [],
nested_attention_masks = [],
split_lens = [],
attn_modes = [],
packed_text_ids = [],
packed_text_indexes = [],
packed_label_ids = [],
ce_loss_indexes = [],
ce_loss_weights = [],
vae_image_tensors = [], # image
vae_video_tensors = [], # video
packed_latent_position_ids = [],
vae_latent_shapes = [],
packed_vae_token_indexes = [],
packed_timesteps = [],
mse_loss_indexes = [],
packed_vit_tokens = [],
vit_token_seqlens = [],
packed_vit_position_ids = [],
packed_vit_token_indexes = [],
vit_video_grid_thw = [], # for vit video
vae_video_grid_thw = [], # for vae video
video_grid_thw = [], # for all video tensor
vit_video_tensors = [], # for vit original video tensor
# Offline arguments
vae_video_latent = [], # for vae video latent offline
vae_data_mode = [], # offline or online
vit_data_mode = [], # offline or online
# sample_task for joint training
sample_task = [],
sample_modality = [],
)
return sequence_status
def to_tensor(self, sequence_status: Dict[str, Any]):
data = dict(
sequence_length=sum(sequence_status['sample_lens']),
sample_lens=sequence_status['sample_lens'],
sample_type=sequence_status['sample_type'],
sample_N_target=sequence_status['sample_N_target'],
packed_text_ids=torch.tensor(sequence_status['packed_text_ids']),
packed_text_indexes=torch.tensor(sequence_status['packed_text_indexes']),
packed_position_ids=torch.tensor(sequence_status['packed_position_ids']),
vit_data_mode=sequence_status['vit_data_mode'],
video_grid_thw=sequence_status['video_grid_thw'],
sample_task=torch.tensor(sequence_status['sample_task']),
sample_modality=torch.tensor(sequence_status['sample_modality']),
)
data['vae_data_mode'] = sequence_status['vae_data_mode']
if not self.use_flex:
data['nested_attention_masks'] = sequence_status['nested_attention_masks']
else:
sequence_len = data['sequence_length']
pad_len = self.max_num_tokens - sequence_len
assert pad_len >= 0, f"pad_len must be greater than 0, but got {pad_len}" # !!!
data['split_lens'] = sequence_status['split_lens'] + [pad_len]
data['attn_modes'] = sequence_status['attn_modes'] + ['causal']
data['sample_lens'] += [pad_len]
data['sample_type'] += ['pad']
data['sample_N_target'] += [0]
# if the model has a convnet vae (e.g., as visual tokenizer)
data['padded_videos'] = sequence_status.pop('vae_video_tensors')
if len(data['padded_videos']) > 0:
# Pack as dynamic resolution
# NOTE: The following keys are shared between image and video for now
if 'patchified_vae_latent_shapes' not in data:
data['patchified_vae_latent_shapes'] = sequence_status['vae_latent_shapes']
data['packed_latent_position_ids'] = torch.cat(sequence_status['packed_latent_position_ids'], dim=0)
data['packed_vae_token_indexes'] = torch.tensor(sequence_status['packed_vae_token_indexes'])
# process for offline data: padding
if len(sequence_status["vae_video_latent"]) > 0:
video_latents = sequence_status.pop("vae_video_latent")
video_sizes = [item.shape for item in video_latents]
max_video_size = [max(item) for item in list(zip(*video_sizes))]
padded_videos_latent = torch.zeros(size=(len(video_latents), *max_video_size))
for i, video_latent in enumerate(video_latents): # [T, H, W, C]
t, h, w, c = video_latent.shape
padded_videos_latent[i, :t, :h, :w, :c] = video_latent
data["padded_latent"] = padded_videos_latent
# NOTE: The following keys are shared between image and video for now
if "patchified_vae_latent_shapes" not in data:
data["patchified_vae_latent_shapes"] = sequence_status["vae_latent_shapes"]
data["packed_latent_position_ids"] = torch.cat(sequence_status["packed_latent_position_ids"], dim=0)
data["packed_vae_token_indexes"] = torch.tensor(sequence_status["packed_vae_token_indexes"])
# if the model has a vit (e.g., as visual tokenizer)
if len(sequence_status['packed_vit_tokens']) > 0:
data['packed_vit_tokens'] = sequence_status.pop('packed_vit_tokens')
# data['packed_vit_tokens'] = torch.cat(sequence_status['packed_vit_tokens'], dim=0)
data['packed_vit_position_ids'] = torch.cat(sequence_status['packed_vit_position_ids'], dim=0)
data['packed_vit_token_indexes'] = torch.tensor(sequence_status['packed_vit_token_indexes'])
data['vit_token_seqlens'] = torch.tensor(sequence_status['vit_token_seqlens'])
# Pack as dynamic resolution
data['padded_videos_vit'] = sequence_status.pop('vit_video_tensors')
# if the model is required to perform visual generation
if len(sequence_status['packed_timesteps']) > 0:
data['packed_timesteps'] = torch.tensor(sequence_status['packed_timesteps'])
data['mse_loss_indexes'] = torch.tensor(sequence_status['mse_loss_indexes'])
# if the model is required to perform text generation
if len(sequence_status['packed_label_ids']) > 0:
data['packed_label_ids'] = torch.tensor(sequence_status['packed_label_ids'])
data['ce_loss_indexes'] = torch.tensor(sequence_status['ce_loss_indexes'])
data['ce_loss_weights'] = torch.tensor(sequence_status['ce_loss_weights'])
if len(sequence_status['vae_video_grid_thw']) > 0:
data['vae_video_grid_thw'] = torch.tensor(sequence_status['vae_video_grid_thw'])
if len(sequence_status['vit_video_grid_thw']) > 0:
data['vit_video_grid_thw'] = torch.tensor(sequence_status['vit_video_grid_thw'])
# Memory optimization: release sequence_status contents that are no longer needed
sequence_status.clear()
return data
def build_datasets(self, datasets_metainfo, data_status):
datasets = []
is_mandatory = []
grouped_weights = []
data_type = []
for grouped_dataset_name, dataset_args in datasets_metainfo.items():
if grouped_dataset_name.startswith('D'): # Handle the new multi-level nested logic
grouped_dataset_name, dataset_args = list(dataset_args.items())[0]
if '2t' in grouped_dataset_name:
data_type.append('x2t')
else:
data_type.append('x2v')
is_mandatory.append(dataset_args.pop('is_mandatory', False))
grouped_weights.append(dataset_args.pop('weight', 0.0))
if 'frame_sampler_args' in dataset_args.keys():
# NOTE: NOT for video
frame_sampler = FrameSampler(**dataset_args.pop('frame_sampler_args'))
dataset_args['frame_sampler'] = frame_sampler
if 'image_transform_args' in dataset_args.keys(): # TODO: deprecate this
transform = ImageTransform(**dataset_args.pop('image_transform_args'))
dataset_args['transform'] = transform
if 'video_transform_args' in dataset_args.keys():
# video
transform = VideoTransform(**dataset_args.pop('video_transform_args'))
dataset_args['transform'] = transform
dataset_args['vae_downsample'] = self.data_config.vae_downsample # fix: pass this in; TODO: consider vae_downsample and vit_downsample, low priority
# Add the video frame sampler here
if 'video_frame_sampler_args' in dataset_args:
dataset_args['res_dump'] = dataset_args['video_frame_sampler_args']['res_dump'] if 'res_dump' in dataset_args['video_frame_sampler_args'].keys() else ''
video_frame_sampler_args = dataset_args.pop('video_frame_sampler_args')
video_frame_sampler = FRAME_SAMPLER_TYPES[video_frame_sampler_args.get("type", "fixed")](**video_frame_sampler_args.get("params", {}))
dataset_args['video_frame_sampler'] = video_frame_sampler
if 'vit_video_transform_args' in dataset_args.keys():
# video
vit_transform = VideoTransform(**dataset_args.pop('vit_video_transform_args'))
dataset_args['vit_transform'] = vit_transform
elif 'vit_image_transform_args' in dataset_args.keys(): # TODO: deprecate this
vit_transform = ImageTransform(**dataset_args.pop('vit_image_transform_args'))
dataset_args['vit_transform'] = vit_transform
assert 'dataset_names' in dataset_args, dataset_args.keys() or "missing 'dataset_names'"
dataset_names = dataset_args.pop('dataset_names') # NOTE: Pay attention to this pop pattern
dataset_args['data_dir_list'] = []
# Iterate and build datasets
for item, meta_info in dataset_names.items():
if self.local_rank == 0:
self.logger.info(f'Preparing Dataset {grouped_dataset_name}/{item}')
data_dir = meta_info['data_dir']
if isinstance(data_dir, str):
# If it is a path
dataset_args['data_dir_list'].append(meta_info['data_dir'])
elif isinstance(data_dir, list):
# If it is a list
dataset_args['data_dir_list'].extend(data_dir)
else:
raise Exception(f'Unknown data_dir type {type(data_dir)}')
# NOTE: Collect all paths at the outer level, then pass them in
all_data_paths = get_parquet_data_paths_balanced(
data_dir_list=dataset_args.get('data_dir_list'),
rank=self.local_rank,
world_size=self.world_size,
num_repeat=dataset_args.get('num_repeat', 1),
)
if 'all_data_paths' in dataset_args.keys():
dataset_args['all_data_paths'].extend(all_data_paths)
else:
dataset_args['all_data_paths'] = all_data_paths
resume_data_status = dataset_args.pop('resume_data_status', True)
if data_status is not None and grouped_dataset_name in data_status.keys() and resume_data_status:
data_status_per_group = data_status[grouped_dataset_name]
else:
data_status_per_group = None
dataset_args.update(self.data_args)
# Update
dataset_args['vit_cond_dropout_prob'] = self.data_config.vit_cond_dropout_prob
dataset_args['text_cond_dropout_prob'] = self.data_config.text_cond_dropout_prob
dataset_args['vae_cond_dropout_prob'] = self.data_config.vae_cond_dropout_prob
dataset = DATASET_REGISTRY[grouped_dataset_name](
dataset_name=grouped_dataset_name,
tokenizer=self.tokenizer,
local_rank=self.local_rank,
world_size=self.world_size,
num_workers=self.num_workers,
data_status=data_status_per_group,
apply_chat_template=self.apply_chat_template,
**dataset_args,
)
datasets.append(dataset)
return datasets, is_mandatory, grouped_weights, data_type
# Add the video processing branch in pack_sequence
def pack_sequence(self, sample: Dict[str, Any], sequence_status: Dict[str, Any]):
image_tensor_list = sample.get('image_tensor_list', []) # just for debug
video_tensor_list = sample.get('video_tensor_list', [])
video_latent_list = sample.get('video_latent_list', [])
sample_N_target = sample.get('N_target', 1)
text_ids_list = sample['text_ids_list']
sequence_plan = sample['sequence_plan']
sample_task = sample.get('sample_task', 't2v')
split_lens, attn_modes = list(), list()
curr = sequence_status['curr']
curr_rope_id = 0
sample_lens = 0
sample_type = ''
curr_split_idx = sequence_status['curr']
apply_text_template = False
curr_video_grid_thw = []
for item in sequence_plan:
split_start = item.get('split_start', True)
if split_start:
curr_split_len = 0
# TODO: add more item types to help classification
if item['type'] == 'text':
sample_type = 'und' # This is overwritten, so only the last item takes effect
text_ids = text_ids_list.pop(0)
if item['enable_cfg'] == 1 and random.random() < self.data_config.text_cond_dropout_prob:
if self.cfg_type == 0: # 0 fully removes the text condition
continue
elif self.cfg_type == 1: # 1 keeps only special tokens
text_ids = []
elif self.cfg_type == 2: # 2 keeps special tokens and replaces middle text tokens with uncond_token
text_ids = [self.cfg_uncond_token_id] * len(text_ids)
if not item.get('special_token_start_nouse'): # When special_token_start_nouse is None or False
shifted_text_ids = [self.bos_token_id] + text_ids # NOTE: self.bos_token_id=151644 <|im_start|>
else:
shifted_text_ids = text_ids
sequence_status['packed_text_ids'].extend(shifted_text_ids)
sequence_status['packed_text_indexes'].extend(range(curr, curr + len(shifted_text_ids)))
# NOTE: item['loss'] == 1 identifies understanding vs generation
if item['loss'] == 1:
loss_token_shift = item.get('loss_token_shift') or 0
sequence_status['ce_loss_indexes'].extend(range(curr - loss_token_shift, curr + len(shifted_text_ids)))
sequence_status['ce_loss_weights'].extend([len2weight(len(shifted_text_ids) + loss_token_shift)] * (len(shifted_text_ids) + loss_token_shift))
sequence_status['packed_label_ids'].extend(text_ids + [self.eos_token_id]) # NOTE: self.eos_token_id=151645 <|im_end|>
curr += len(shifted_text_ids)
curr_split_len += len(shifted_text_ids)
# add a <|im_end|> token
if not item.get('special_token_end_nouse'):
sequence_status['packed_text_ids'].append(self.eos_token_id)
sequence_status['packed_text_indexes'].append(curr)
if item['special_token_loss'] == 1: # <|im_end|> may have loss
sequence_status['ce_loss_indexes'].append(curr)
sequence_status['ce_loss_weights'].append(1.0)
sequence_status['packed_label_ids'].append(item['special_token_label'])
curr += 1
curr_split_len += 1
# update sequence status
attn_modes.append("causal")
#if self.apply_chat_template:
sequence_status['packed_position_ids'].extend(range(curr_rope_id, curr_rope_id + curr_split_len))
curr_rope_id += curr_split_len
sequence_status['sample_modality'].extend([modality_map[item['type']]] * curr_split_len)
elif item["type"] == "text_template":
apply_text_template = True
text_ids = text_ids_list.pop(0)
# The current template only applies to UND, so ignore cfg for now
sequence_status["packed_text_ids"].extend(text_ids)
sample_lens = len(text_ids)
spans_index = item.get("spans_index", None) # Vision padding tokens as a list; each item is an index list for the corresponding video/image pad tokens
curr_sample_modality = []
caption_index = item.get("cap_index", []) # Indexes for text excluding the system prompt
for video_id, span_index in enumerate(spans_index):
vision_start, vision_end = curr_split_idx + span_index[0], curr_split_idx + span_index[-1] # Indexes of the first and last '<|video_pad|>'
sequence_status["packed_text_indexes"].extend(range(curr, vision_start))
if (vision_start - 1) - curr != 0: # Confirm there is a text split before vision; HACK: changed from the LLaVA version
curr_split_len = (vision_start - 1) - curr
sequence_status["packed_position_ids"].extend(
range(curr_rope_id, curr_rope_id + curr_split_len)
) # Note: use vision_start - 1, not vision_start, because vision_start is the starting token of the video split
curr_rope_id += curr_split_len
curr_sample_modality.extend([modality_map['system_prompt']] * curr_split_len)
if caption_index != [] and caption_index[0] in range(curr, curr + curr_split_len): # NOTE: interleaved text is not supported; text must be contiguous
split_len_1 = caption_index[0] - curr # Length of the system prompt before text
split_len_2 = len(caption_index) # Text length
split_len_3 = curr_split_len - split_len_1 - split_len_2 # Length of the system prompt after text
split_len_text = [split_len_1, split_len_2, split_len_3]
split_len_text = [x for x in split_len_text if x != 0]
attn_modes.extend(["causal"] * len(split_len_text))
split_lens.extend(split_len_text)
else:
attn_modes.append("causal")
split_lens.append(curr_split_len)
curr_split_len = len(span_index) + 2
if sequence_plan[video_id]["type"] == 'vit_video':
sequence_status["packed_vit_token_indexes"].extend(range(vision_start, vision_end + 1))
attn_modes.append("full") # TODO: add this check if the GEN branch also uses templates
curr_sample_modality.extend([modality_map['ref_vit']] * curr_split_len)
elif sequence_plan[video_id]["type"] == 'vae_video':
sequence_status["packed_vae_token_indexes"].extend(range(vision_start, vision_end + 1))
if sequence_plan[video_id]["loss"] == 0:
attn_modes.append("full_noise") # TODO: add this check if the GEN branch also uses templates
if sample_task == 'edit':
curr_sample_modality.extend([modality_map['ref_source']] * curr_split_len)
elif sample_task == 'idip' or sample_task == 'maze':
curr_sample_modality.extend([modality_map['ref_image']] * curr_split_len)
else:
if frame_condition_idx == []:
sequence_status["mse_loss_indexes"].extend(range(vision_start, vision_end + 1))
else: # Support f2v; multiple target videos are not currently supported because different videos need separate THW values
frame_condition_indexes = []
mse_loss_indexes = list(range(vision_start, vision_end + 1))
for idx in frame_condition_idx:
if idx == -1:
idx = t - 1
if idx == 1:
continue # Skip when there are only two frames to avoid using identical condition frames for all frames
frame_condition_indexes.extend(mse_loss_indexes[idx * h * w : (idx + 1) * h * w])
mse_loss_indexes = sorted(list(set(mse_loss_indexes) - set(frame_condition_indexes)))
sequence_status["mse_loss_indexes"].extend(mse_loss_indexes)
attn_modes.append("noise") # TODO: add this check if the GEN branch also uses templates
sample_type = "gen" # This is overwritten, so only the last item takes effect
curr_sample_modality.extend([modality_map['noise']] * curr_split_len)
sequence_status["packed_position_ids"].extend([curr_rope_id] * curr_split_len)
#attn_modes.append("full") # TODO: add this check if the GEN branch also uses templates
split_lens.append(len(span_index) + 2)
curr = vision_end + 1 # Index of the '<|vision_end|>' token
curr_rope_id += 1
sequence_status["packed_text_indexes"].append(curr)
curr += 1 # Starting token of the next sequence
len_split_last = sample_lens - (curr - curr_split_idx) if spans_index != [] else len(text_ids)
if len_split_last != 0: # A trailing text segment remains
split_lens.append(len_split_last)
sequence_status["packed_text_indexes"].extend(range(curr, curr + len_split_last))
sequence_status["packed_position_ids"].extend(range(curr_rope_id, curr_rope_id + len_split_last))
attn_modes.append("causal")
curr_sample_modality.extend([modality_map['system_prompt']] * len_split_last)
if item["loss"] == 1: # This marks an understanding task and requires CE loss
packed_label_index = item.get("packed_label_index", [])[:]
sequence_status["packed_label_ids"].extend(text_ids[packed_label_index[0]:])
packed_label_index = np.asarray(packed_label_index, dtype=np.int64) + curr_split_idx
ce_loss_indexes = (packed_label_index - 1).tolist()
sequence_status["ce_loss_indexes"].extend(ce_loss_indexes)
sequence_status["ce_loss_weights"].extend([len2weight(len(packed_label_index))] * (len(packed_label_index)))
sample_type = "und" # This is overwritten, so only the last item takes effect
# else: # This marks a generation task
# sample_type = "gen" # This is overwritten, so only the last item takes effect
#packed_label_index = item.get("packed_label_index", [])[1:-2]
# Get caption indexes in text and update their sample_modality
if caption_index != []:
curr_sample_modality[caption_index[0]:caption_index[-1]+1] = [modality_map['text']] * (caption_index[-1] - caption_index[0] + 1)
curr_split_idx += len(text_ids)
curr = curr_split_idx
sequence_status['sample_modality'].extend(curr_sample_modality)
elif item['type'] == 'vae_video':
sample_type = 'gen'
video_tensor = video_tensor_list.pop(0) # CTHW
if item['enable_cfg'] == 1 and random.random() < self.data_config.vae_cond_dropout_prob:
# FIXME: fix VAE dropout in video2video. TODO: confirm whether enable_cfg is needed in the vae_video branch
# curr_rope_id += 1
continue
apply_text_template = item.get('apply_text_template', False)
num_special_tokens = 0
# Add <|startofimage|> token shared by video and image. TODO: split image and video special tokens?
if not apply_text_template:
sequence_status['packed_text_ids'].append(self.start_of_image) # self.start_of_image=151652, <|vision_start|>
sequence_status['packed_text_indexes'].append(curr)
curr += 1
curr_split_len += 1
num_special_tokens += 1
# In online mode, video_tensor is a tensor; in offline mode, it is a list [latent]
if isinstance(video_tensor, torch.Tensor): # online
# Preprocess video
sequence_status["vae_video_tensors"].append(video_tensor) # Raw CTHW video, not latent
# Assume video_tensor has shape (C, T, H, W)
_, T, H, W = video_tensor.shape
_T, _H, _W = self.data_config.vae_downsample # NOTE: Absolute-scale downsample including patchification
t = (T - 1) // _T + 1 # k*N+1; normally the T dimension is not patchified. Update this if T patchification is needed
h = H // _H
w = W // _W
sequence_status["vae_data_mode"].append("online")
else: # offline
# video_latent = video_tensor[0] # [[T, H, W, C]]
sequence_status["vae_video_tensors"].append(video_tensor[0]) # video_tensor[0] is a tensor with shape [T, H, W, C]
# Assume video_latent has shape [T, H, W, C]
T, H, W, _ = video_tensor[0].shape
_T, _H, _W = self.data_config.latent_patch_size # NOTE: Only includes patchification
t = T // _T # k*N+1; normally the T dimension is not patchified. Update this if T patchification is needed
h = H // _H
w = W // _W
sequence_status["vae_data_mode"].append("offline")
spatial_merge_size = 2 # TODO: must spatial_merge_size always be 2?
vae_video_grid_thw = [
t,
h * spatial_merge_size,
w * spatial_merge_size,
] # Qwen-VL RoPE divides by spatial_merge_size by default to match VIT processing, so VAE needs an extra multiply by spatial_merge_size
if EXP_HW_20250819: # HACK: temporary experiment
vae_video_grid_thw = [1, 2, 2]
sequence_status["vae_video_grid_thw"].append(vae_video_grid_thw)
curr_video_grid_thw.append(vae_video_grid_thw)
# Use the native (t, h, w) latent shape
sequence_status['vae_latent_shapes'].append((t, h, w))
# Use the 3D-aware position encoding function
if self.interpolate_pos:
# Interpolation
packed_latent_position_ids = get_flattened_position_ids_interpolate_video(
t, h, w, 1, # Latent-space patch size is 1
max_num_frames=self.max_num_latent_frames,
max_num_patches_per_side=self.data_config.max_latent_size
)
else:
# Extrapolation
packed_latent_position_ids = get_flattened_position_ids_extrapolate_video(
t, h, w,
max_latent_size=self.data_config.max_latent_size
)
sequence_status['packed_latent_position_ids'].append(packed_latent_position_ids)
num_vid_tokens = t * h * w
if not apply_text_template:
sequence_status['packed_vae_token_indexes'].extend(range(curr, curr + num_vid_tokens))
if item["loss"] == 1:
pro_fbyf = False
if split_start:
timestep = np.random.randn() # NOTE: A sigmoid is applied outside
frame_condition_idx = item.get("frame_condition_idx", [])
packed_timesteps = [timestep] * num_vid_tokens
mse_loss_indexes = list(range(curr, curr + num_vid_tokens))
frame_condition_indexes = []
for idx in frame_condition_idx:
if idx == -1:
idx = t - 1
if idx == 1:
continue # Skip when there are only two frames to avoid using identical condition frames for all frames
frame_condition_indexes.extend(mse_loss_indexes[idx * h * w : (idx + 1) * h * w])
packed_timesteps[idx * h * w : (idx + 1) * h * w] = [-sys.float_info.max] * (h * w)
if frame_condition_idx:
mse_loss_indexes = sorted(list(set(mse_loss_indexes) - set(frame_condition_indexes)))
if not apply_text_template:
sequence_status["mse_loss_indexes"].extend(mse_loss_indexes) # range(curr, curr + num_vid_tokens))
else:
if self.incre_time_pro <= 0:
timestep = float("-inf")
else:
timestep = 0.
packed_timesteps = [timestep] * num_vid_tokens
sequence_status['packed_timesteps'].extend(packed_timesteps)
if not apply_text_template:
curr += num_vid_tokens
curr_split_len += num_vid_tokens
sequence_status["packed_text_ids"].extend([self.image_token_id] * num_vid_tokens)
# Add <|endofimage|> token
sequence_status['packed_text_ids'].append(self.end_of_image) # self.end_of_image=151653, <|vision_end|>
sequence_status['packed_text_indexes'].append(curr)
# <|endofimage|> may have loss
if item['special_token_loss'] == 1:
sequence_status['ce_loss_indexes'].append(curr)
sequence_status['ce_loss_weights'].append(1.0)
sequence_status['packed_label_ids'].append(item['special_token_label'])
curr += 1
curr_split_len += 1
num_special_tokens += 1
# Update sequence status
if split_start:
if item['loss'] == 1 and 'frame_delta' not in item.keys(): # Is frame_delta for multi-frame generation?
attn_modes.append("noise")
else:
# attn_modes.append("full")
attn_modes.append("full_noise")
sequence_status['packed_position_ids'].extend([curr_rope_id] * (num_vid_tokens + num_special_tokens)) # NOTE: why is RoPE fixed?
if 'frame_delta' in item.keys():
curr_rope_id += item['frame_delta']
elif item['loss'] == 0:
curr_rope_id += 1
# update sample sequence modality
if item['loss'] == 1:
sequence_status['sample_modality'].extend([modality_map['noise']] * curr_split_len)
elif item['loss'] == 0 and sample_task == 'edit':
sequence_status['sample_modality'].extend([modality_map['ref_source']] * curr_split_len)
elif item['loss'] == 0 and (sample_task == 'idip' or sample_task == 'maze'):
sequence_status['sample_modality'].extend([modality_map['ref_image']] * curr_split_len)
del video_tensor, packed_timesteps, packed_latent_position_ids
elif item['type'] == 'vit_video':
apply_text_template = item.get('apply_text_template', False)
video_tensor = video_tensor_list.pop(0) # CTHW
if item['enable_cfg'] == 1 and random.random() < self.data_config.vit_cond_dropout_prob and not apply_text_template:
# FIXME: fix VAE dropout in video2video. TODO: confirm whether enable_cfg is needed in the vit_video branch
curr_rope_id += 1 # HACK ????
continue
# Add <|startofimage|> token shared by video and image. TODO: split image and video special tokens?
if not apply_text_template:
sequence_status['packed_text_ids'].append(self.start_of_image) # 151652, <|vision_start|>
sequence_status['packed_text_indexes'].append(curr)
curr += 1
curr_split_len += 1
# In online mode, video_tensor is a tensor; in offline mode, it is a list [latent]
if isinstance(video_tensor, torch.Tensor): # online
sequence_status['vit_video_tensors'].append(video_tensor) # Raw CTHW video, not latent; used only for validation visualization
# preprocess video
vit_tokens = patchify_video_with_merge(video_tensor, self.data_config.vit_patch_size, self.data_config.vit_patch_size_temporal) # C T H W -> (T//2 * H//p * W//p) (p*p*2*C)
num_video_tokens = vit_tokens.shape[0] // 4 # Qwen2.5-VL also needs merging: 2x2 merges into 1, hardcoded temporarily
t, h, w = video_tensor.size(1), video_tensor.size(2), video_tensor.size(3)
sequence_status['packed_vit_tokens'].append(vit_tokens)
sequence_status['vit_data_mode'].append('online')
del vit_tokens
else: # offline
# video_latent, video_tensor = video_tensor # [L,D]
# sequence_status['vit_video_tensors'].append(video_tensor[0])
num_video_tokens = video_tensor[0].shape[0]
sequence_status['packed_vit_tokens'].append(video_tensor[0])
sequence_status['vit_data_mode'].append('offline')
thw = item.get('thw',None)
if thw is not None:
t, h, w = thw
else:
t = None
if t is not None:
vit_video_grid_thw = [
t // self.data_config.vit_patch_size_temporal,
h // self.data_config.vit_patch_size,
w // self.data_config.vit_patch_size,
] # [1, 16, 16]
sequence_status["vit_video_grid_thw"].append(vit_video_grid_thw)
curr_video_grid_thw.append(vit_video_grid_thw)
sequence_status['vit_token_seqlens'].append(num_video_tokens)
sequence_status['packed_vit_position_ids'].append(torch.zeros(num_video_tokens)) # TODO: this may not always be 0; multiple VIT sequences may be problematic, but this is currently only used in vit model:siglip
if not apply_text_template:
sequence_status['packed_vit_token_indexes'].extend(range(curr, curr + num_video_tokens))
curr += num_video_tokens
curr_split_len += num_video_tokens
# NOTE dummy position_ids
sequence_status['packed_text_ids'].extend([self.image_token_id]*num_video_tokens)
# add a <|endofimage|> token
sequence_status['packed_text_ids'].append(self.end_of_image) # 151653, <|vision_end|>
sequence_status['packed_text_indexes'].append(curr)
if item['special_token_loss'] == 1: # <|endofimage|> may have loss
sequence_status['ce_loss_indexes'].append(curr)
sequence_status['ce_loss_weights'].append(1.0)
sequence_status['packed_label_ids'].append(item['special_token_label'])
curr += 1
curr_split_len += 1
sequence_status['packed_position_ids'].extend([curr_rope_id] * curr_split_len)
curr_rope_id += 1
# update sequence status
attn_modes.append("full")
sequence_status['sample_modality'].extend([modality_map['ref_vit']] * curr_split_len)
del video_tensor
if item.get('split_end', True) and not apply_text_template:
if isinstance(curr_split_len, list):
split_lens.extend(curr_split_len)
sample_lens += sum(curr_split_len)
else:
split_lens.append(curr_split_len)
sample_lens += curr_split_len
sequence_status['curr'] = curr
sequence_status['sample_lens'].append(sample_lens) # sample_lens is the length of a pair, e.g. <text, video>
sequence_status['sample_type'].append(sample_type)
sequence_status['sample_N_target'].append(sample_N_target)
sequence_status['video_grid_thw'].append(torch.tensor(curr_video_grid_thw)) # video_grid_thw is a sequence split by sample
# prepare attention mask
if not self.use_flex:
sequence_status['nested_attention_masks'].append(
prepare_attention_mask_per_sample(split_lens, attn_modes)
)
else:
sequence_status['split_lens'].extend(split_lens)
sequence_status['attn_modes'].extend(attn_modes)
# add sample_task
sequence_status['sample_task'].extend([sample_task_map[sample_task]] * sample_lens)
return sequence_status
def sample_pro(self, sample, sequence_status, batch_data_indexes, buffer, sample_from_buffer=False, skip_count=0):
num_tokens = sample['num_tokens'] + 2 * len(sample['sequence_plan']) # NOTE: 2*2 special tokens
is_pro = False
if num_tokens < self.max_num_tokens_per_sample and sequence_status['curr'] + num_tokens < self.max_num_tokens :
sequence_status = self.pack_sequence(sample, sequence_status)
batch_data_indexes.append(sample['data_indexes'])
is_pro = True
elif sequence_status['curr'] + num_tokens > self.max_num_tokens:
if len(buffer) < self.max_buffer_size and not sample_from_buffer:
buffer.append(sample)
else:
self.logger.info(f"skip a sample with length {num_tokens}")
skip_count += 1
else:
self.logger.info(f"skip a sample with length {num_tokens}")
skip_count += 1
return sequence_status, batch_data_indexes, buffer, is_pro, skip_count
def __iter__(self):
# NOTE: Core logic repeatedly calls pack_sequence to pack multimodal data
# Create underlying dataset iterators at each iteration; spawn/fork safe
self.dataset_iters = [iter(dataset) for dataset in self.grouped_datasets]
total_weights = sum(self.grouped_weights)
assert total_weights > 0.0
group_cumprobs = [sum(self.grouped_weights[: i + 1]) / total_weights for i in range(len(self.grouped_weights))]
sequence_status = self.set_sequence_status()
batch_data_indexes = []
buffer = []
data_type_pro = ['x2t', 'x2v']
skip_count = 0
max_skips_before_reset = 30 # Threshold for resetting sequence_status
while True:
# Ensure at least one sample from each group
if sequence_status['curr'] == 0:
for group_index, group_iter in enumerate(self.dataset_iters):
if self.is_mandatory[group_index]:
while True:
sample = next(group_iter)
# if a sample is too long, skip it
sequence_status, batch_data_indexes, buffer, is_pro, skip_count = self.sample_pro(sample, sequence_status, batch_data_indexes, buffer, skip_count=skip_count)
if self.data_type[group_index] in data_type_pro:
data_type_pro.remove(self.data_type[group_index])
if is_pro:
break
if self.require_und_gen and 'x2t' in self.data_type and 'x2v' in self.data_type: # NOTE: In joint UND + GEN training, sequences with only one sample may cause communication failures
while True:
if data_type_pro == []:
break
n = random.random()
group_index = bisect.bisect_left(group_cumprobs, n)
if self.data_type[group_index] in data_type_pro:
sample = next(self.dataset_iters[group_index])
sequence_status, batch_data_indexes, buffer, is_pro, skip_count = self.sample_pro(sample, sequence_status, batch_data_indexes, buffer, skip_count=skip_count)
if is_pro:
data_type_pro.remove(self.data_type[group_index])
if skip_count >= max_skips_before_reset: # NOTE: If 30 samples are skipped consecutively, break and reset sequence_status
break
if skip_count >= max_skips_before_reset: # Reset sequence_status
self.logger.info(f"Too many skips ({skip_count}), resetting sequence_status")
sequence_status = self.set_sequence_status()
batch_data_indexes = []
data_type_pro = ['x2t', 'x2v']
skip_count = 0
continue
if sequence_status['curr'] < self.prefer_buffer_before and len(buffer) > 0:
sample = buffer.pop(0)
sample_from_buffer = True
else:
# sample normally across all groups
n = random.random()
group_index = bisect.bisect_left(group_cumprobs, n)
sample = next(self.dataset_iters[group_index])
sample_from_buffer = False
# if a sample is too long, skip it
num_tokens = sample['num_tokens'] + 2 * len(sample['sequence_plan'])
if num_tokens > self.max_num_tokens_per_sample:
self.logger.info(f"skip a sample with length {num_tokens}")
continue
if sequence_status['curr'] + num_tokens > self.max_num_tokens or len(sequence_status['ce_loss_indexes']) > self.expected_num_ce_loss_tokens:
if len(buffer) < self.max_buffer_size and not sample_from_buffer:
buffer.append(sample)
# elif self.require_und_gen:
# sample_type = 1
else:
# self.logger.info(f"Yielding data with length {sum(sequence_status['sample_lens'])}")
data = self.to_tensor(sequence_status)
data['batch_data_indexes'] = batch_data_indexes
data_type_pro = ['x2t', 'x2v']
yield data
# Reset sequence_status after yield
sequence_status = self.set_sequence_status()
batch_data_indexes = []
continue
sequence_status = self.pack_sequence(sample, sequence_status)
batch_data_indexes.append(sample['data_indexes'])
if sequence_status['curr'] >= self.expected_num_tokens or len(sequence_status['ce_loss_indexes']) >= self.expected_num_ce_loss_tokens:
data = self.to_tensor(sequence_status)
data['batch_data_indexes'] = batch_data_indexes
data_type_pro = ['x2t', 'x2v']
yield data
# Reset sequence_status after yield
sequence_status = self.set_sequence_status()
batch_data_indexes = []
class SimpleCustomBatch:
def __init__(self, batch):
data = batch[0]
for key, value in data.items():
setattr(self, key, value)
def pin_memory(self):
for key, value in self.__dict__.items():
if isinstance(value, torch.Tensor):
setattr(self, key, value.pin_memory())
elif isinstance(value, list) and value and all(isinstance(i, torch.Tensor) for i in value):
setattr(self, key, [i.pin_memory() for i in value])
return self
def cuda(self, device):
for key, value in self.__dict__.items():
if isinstance(value, torch.Tensor):
setattr(self, key, value.to(device))
elif isinstance(value, list) and value and all(isinstance(i, torch.Tensor) for i in value):
setattr(self, key, [i.to(device) for i in value])
return self
def to_dict(self):
return self.__dict__.copy()
def collate_wrapper():
def collate_fn(batch):
return SimpleCustomBatch(batch)
return collate_fn
# Top-level function; pickleable
def simple_custom_collate(batch):
return SimpleCustomBatch(batch)