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

498 lines
24 KiB
Python

# coding: utf-8
from abc import ABC, abstractmethod
import os
import random
from typing import Any, Dict, Iterator, List, Optional
import pyarrow.parquet as pq
from PIL import Image
import numpy as np
from io import BytesIO
import torch
from data.datasets_factory.distributed_iterable_dataset import DistributedIterableDataset
from data.parquet_utils import init_arrow_pf_fs, read_parquet_rows
from data.transforms import VideoTransform
from common.utils.logging import get_logger
import decord
import cv2
from decord import VideoReader
from data.video.sampler.frames import FrameSamplerOutput
from config.config_factory import TemplateArguments
from data.common import parse_videochat2it_doubao_caption
class TextCleaner:
def __call__(self, text: Any) -> str:
if text is None:
return ""
return str(text).strip()
def _collect_target_positions(element_dtype_array: List[str], target_modality: str) -> List[int]:
# Targets can only be selected from index >= 1, excluding index 0
return [i for i, t in enumerate(element_dtype_array) if i >= 1 and t == target_modality]
def sample_task(task_type, task_type_rate):
"""
Sample one item from task_type according to task_type_rate.
If all weights are zero or negative, fall back to uniform sampling.
"""
if len(task_type) != len(task_type_rate):
raise ValueError("task_type and task_type_rate must have the same length")
# Treat negative weights as 0
weights = [max(0.0, float(w)) for w in task_type_rate]
if sum(weights) == 0.0:
weights = [1.0] * len(task_type) # Fall back to uniform weights
return random.choices(task_type, weights=weights, k=1)[0]
def data_invert_text_image_pair(interleave_array, element_dtype_array, target_modality): # Handle a single image-text pair by swapping without considering position
if len(element_dtype_array) == 2:
if element_dtype_array[-1] != target_modality:
interleave_array = interleave_array[::-1]
element_dtype_array = element_dtype_array[::-1]
return interleave_array, element_dtype_array
class BaseMMParquetDataset(DistributedIterableDataset, ABC):
def __init__(
self,
dataset_name: str,
tokenizer: Any,
data_dir_list: List[str],
local_rank: int = 0,
world_size: int = 1,
num_workers: int = 8,
data_status: Optional[Any] = None,
**kwargs: Any,
):
"""
data_dir_list: list of data directories contains parquet files
"""
super().__init__(dataset_name, local_rank, world_size, num_workers)
# Store config only and delay real initialization
self.tokenizer = tokenizer
self.data_dir_list = data_dir_list
self.data_status = data_status
self.seed = kwargs.get('seed', 42)
self.caption_key = kwargs.get(
'caption_key', 'v3_0_long_internlm_caption_en_text'
)
self.transform: VideoTransform = kwargs.get('transform')
self.frame_sampler = kwargs.get("video_frame_sampler") # Video sampling
self.vae_downsample = kwargs.get(
'vae_downsample',
(
getattr(self.transform, 'stride_temporal', 4),
getattr(self.transform, 'stride_spatial', 16),
getattr(self.transform, 'stride_spatial', 16),
)
)
self.max_bytes = kwargs.get('max_bytes', -1)
self.logger = get_logger()
# Mark as not initialized yet
self.data_paths = kwargs.get('all_data_paths')
self.cpu_count = os.cpu_count() or 1
self.apply_chat_template = kwargs.get('apply_chat_template', False)
if self.apply_chat_template:
self.chat_template = TemplateArguments().chat_template_T2I
self.vision_stream = kwargs.get("vision_stream", "vae_video") # 'vae_video' | 'vit_video'
self.vit_downsample = kwargs.get("vit_downsample", (2, 28, 28))
if kwargs.get('vit_transform') is not None:
self.vit_transform: VideoTransform = kwargs.get('vit_transform')
else:
self.vit_transform: VideoTransform = kwargs.get('transform')
self.text_cleaner = TextCleaner()
self.dataset_type = kwargs.get("dataset_type", "interleave")
self.force_last_as_gt_prob = kwargs.get("force_last_as_gt_prob", 0.0)
self.N_target = kwargs.get("N_target", 1)
self.N_target_random_prob = kwargs.get("N_target_random_prob", 0.0)
self.max_num_split_vit, self.max_num_split_vae, self.max_num_split_text = kwargs.get("max_num_split", [1000, 1000, 1000])
self.is_image = kwargs.get("is_image", True)
self.res_dump = kwargs.get("res_dump", "12fps_192p")
self.data_mode = kwargs.get("data_mode", "online")
self.text_template = kwargs.get("text_template", False)
self.vision_cond_type = kwargs.get("vision_cond_type", ["vit"])
self.fbyf_group_interval = kwargs.get("fbyf_group_interval", -1)
self.fbyf_type = kwargs.get("fbyf_type", "group")
self.sample_task = kwargs.get("sample_task", "t2v") # Task identifier for joint multi-task training
if "ocr" in self.dataset_type:
self.data_filter = kwargs.get("data_filter", {})
self.save_video_image = kwargs.get("save_video_image", False)
self.data_config = kwargs
# ==== Hooks required or optionally overridden by subclasses ====
def select_columns(self) -> Optional[List[str]]:
"""Optional: return column names to read from parquet to reduce IO. None reads all columns."""
if "interleave" in self.dataset_type:
return None # ["element_dtype_array", "interleave_array"]
elif "ffhq" in self.dataset_type or "imagenet" in self.dataset_type:
return ["tos_url", self.caption_key] # Select a subset of columns
elif "hav" in self.dataset_type:
return ["media_url", "properties"]
elif "vertical" in self.dataset_type:
return ["meta_url"]
elif "audio_human" in self.dataset_type:
return ["video_meta_url"]
return None
@staticmethod
def _read_decord(video: VideoReader, frame_idx: List[int]) -> List[Image.Image]:
# Use get_batch() instead of reading frames one by one for better performance
frames_np = video.get_batch(frame_idx).asnumpy()
return [Image.fromarray(frame) for frame in frames_np]
def vision_token_count(self, video_tensor: torch.Tensor) -> int:
_, T, H, W = video_tensor.shape
if self.vision_stream == "vit_video":
_T, _H, _W = self.vit_downsample
return (T // _T) * (H // _H) * (W // _W)
elif self.vision_stream == "vae_video":
_T, _H, _W = self.vae_downsample
return ((T // _T) + 1) * (H // _H) * (W // _W)
else:
raise ValueError(f"Unknown vision_stream: {self.vision_stream}")
def get_thwc_url_new(self, media_url, worker_id):
raise NotImplementedError("Remote media URLs are not supported. Use local parquet rows with embedded bytes.")
video_reader = VideoReader(video_stream, ctx=decord.cpu(worker_id % self.cpu_count))
total_frames = len(video_reader)
sampler_name = self.frame_sampler.__class__.__name__
if sampler_name == "MultiClipsFrameSampler":
fps =24
try:
fps = int(round(float(video_reader.get_avg_fps())))
except Exception:
pass
frames_info = {
"clip_indices": [(0, total_frames)], # Left-closed, right-open interval; default is a single clip
"fps": fps, # Default is 24
}
elif sampler_name == "FixedFrameSampler":
frames_info = {
"start_frame": 0,
"end_frame": total_frames,
"total_frames": total_frames,
}
else:
raise ValueError(f"Not verified frame sampler type: {sampler_name}")
frames_sampler_output: FrameSamplerOutput = self.frame_sampler(frames_info)
video_frames = self._read_decord(video_reader, frames_sampler_output.indices)
# Default DIT path
video_tensor = self.vit_transform(video_frames) # fix: use List input
if self.is_image:
video_tensor = video_tensor.repeat(1, 2, 1, 1) # NOTE: Duplicate single images because the encoder temporal patch size is 2
# NOTE: Video length must be even
if video_tensor.shape[1] % 2 == 1:
last_frame = video_tensor[:, -1:, :, :]
video_tensor = torch.cat([video_tensor, last_frame], dim=1)
_, T, H, W = video_tensor.shape
return (T, H, W)
def get_video_tensor_online(self, media_url, vision_stream, worker_id=0, element_dtype="image", raw_bytes_input=False) -> torch.Tensor:
self.vision_stream = vision_stream
if raw_bytes_input:
# raise NotImplementedError(f"raw_bytes_input must be True for {vision_stream}")
video_stream = BytesIO(media_url)
# # Method A: write directly to file; simplest debug code
# from datetime import datetime # Import datetime module
# timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
# with open(f"saved_image_{timestamp}.png", "wb") as f:
# f.write(video_stream.getvalue()) # getvalue() returns all bytes in BytesIO
# # Method A: write directly to file; simplest debug code
# from datetime import datetime # Import datetime module
# # Keep a high-precision timestamp: year-month-day_hour-minute-second-microsecond
# timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
# # Use a video filename with .mp4 suffix and write video bytes
# with open(f"saved_video_{timestamp}.mp4", "wb") as f:
# f.write(video_stream.getvalue()) # video_stream is a BytesIO object containing video bytes
else:
raise NotImplementedError("Remote media URLs are not supported. Use raw_bytes_input=True with local parquet bytes.")
if self.is_image and element_dtype == "image":
image = Image.open(video_stream)
if image.mode == "P":
image = image.convert("RGBA")
if image.mode == "RGBA":
# Composite on a white background to remove transparency
bg = Image.new("RGB", image.size, (255, 255, 255))
bg.paste(image, mask=image.split()[3]) # Use the alpha channel as the mask
image = bg
else:
image = image.convert("RGB")
video_frames = [image]
# Save image
if self.save_video_image:
self.path = f"{self.path}.jpg"
image.save(self.path, quality=95)
print(f"Saved image to {self.path}")
else: # for video
video_reader = VideoReader(video_stream, ctx=decord.cpu(worker_id % self.cpu_count))
total_frames = len(video_reader)
# Save video
if self.save_video_image:
fps = video_reader.get_avg_fps()
width, height = video_reader[0].shape[1], video_reader[0].shape[0]
self.path =f"{self.path}.mp4" # Saved video path
# Save video with OpenCV
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(self.path, fourcc, fps, (width, height))
for frame in video_reader:
frame_rgb = cv2.cvtColor(frame.asnumpy(), cv2.COLOR_BGR2RGB) # Convert BGR to RGB
out.write(frame_rgb)
out.release()
print(f"Saved image to {self.path} with fps {fps}")
sampler_name = self.frame_sampler.__class__.__name__
if sampler_name == "MultiClipsFrameSampler":
fps =24
try:
fps = int(round(float(video_reader.get_avg_fps())))
except Exception:
pass
frames_info = {
"clip_indices": [(0, total_frames)], # Left-closed, right-open interval; default is a single clip
"fps": fps, # Default is 24
}
elif sampler_name == "FixedFrameSampler":
frames_info = {
"start_frame": 0,
"end_frame": total_frames,
"total_frames": total_frames,
}
else:
raise ValueError(f"Not verified frame sampler type: {sampler_name}")
frames_sampler_output: FrameSamplerOutput = self.frame_sampler(frames_info)
video_frames = self._read_decord(video_reader, frames_sampler_output.indices)
if vision_stream == "vae_video":
video_tensor = self.transform(video_frames) # fix: use List input
elif vision_stream == "vit_video":
video_tensor = self.vit_transform(video_frames) # fix: use List input
if self.is_image:
video_tensor = video_tensor.repeat(1, 2, 1, 1) # NOTE: Duplicate single images because the encoder temporal patch size is 2
# NOTE: Video length must be even
if video_tensor.shape[1] % 2 == 1:
last_frame = video_tensor[:, -1:, :, :]
video_tensor = torch.cat([video_tensor, last_frame], dim=1)
else:
raise ValueError(f"Unknown vision_stream: {vision_stream}")
if not (self.is_image and element_dtype == "image"):
del video_frames, video_reader, video_stream
return video_tensor, self.vision_token_count(video_tensor)
def get_video_tensor_offline(self, media_url, vision_stream, worker_id=0) -> torch.Tensor:
self.vision_stream = vision_stream
if vision_stream == "vae_video":
video_tensor = media_url[0] # [t, h, w, c]
num_token = video_tensor.shape[0] * video_tensor.shape[1] * video_tensor.shape[2]
elif vision_stream == "vit_video":
video_tensor = media_url[1] # [L, D]
num_token = video_tensor.shape[0]
if len(media_url) == 3 and vision_stream == "vit_video":
if isinstance( media_url[2], str): # Get THW information
thw = self.get_thwc_url_new(media_url[2], worker_id = worker_id)
else:
thw = media_url[2][1:]
num_token_ = thw[0] * thw[1] * thw[2] // self.vit_downsample[0] // self.vit_downsample[1] // self.vit_downsample[2]
if num_token_ != num_token:
raise ValueError(f"Video tensor shape {video_tensor.shape} not match thw {thw}: {num_token_} != {num_token}")
else:
thw = None
return video_tensor, num_token, thw
def get_video_tensor(self, media_url, vision_stream, worker_id=0, element_dtype="image", raw_bytes_input=False) -> torch.Tensor:
if isinstance(media_url, tuple): # offline
video_tensor, num_tokens_, thw = self.get_video_tensor_offline(media_url, vision_stream=vision_stream)
self.data_mode = "offline"
video_tensor = [video_tensor] # Return as a list to distinguish this from online format
else: # online
video_tensor, num_tokens_ = self.get_video_tensor_online(media_url, vision_stream=vision_stream, worker_id=worker_id, element_dtype=element_dtype, raw_bytes_input=raw_bytes_input)
self.data_mode = "online"
thw = None
return video_tensor, num_tokens_, thw
# Get the sample count for each file during initialization
def get_file_sample_counts(self, data_paths):
sample_counts = []
for path in data_paths:
fs = init_arrow_pf_fs(path)
with fs.open_input_file(path) as f:
fr = pq.ParquetFile(f)
# Estimate or exactly compute the sample count in each file
count = sum(fr.metadata.row_group(i).num_rows for i in range(fr.num_row_groups))
sample_counts.append(count)
return sample_counts
def get_condition_target_idx(
self,
element_dtype_array,
):
if len(element_dtype_array) == 1 and self.target_modality in element_dtype_array: # A single element means there is no condition
return [], [0], 1
target_pos_all = _collect_target_positions(element_dtype_array, self.target_modality) # Get target element positions excluding position 0
pos_all = list(range(len(element_dtype_array)))
N_all = len(target_pos_all)
if N_all == 0:
# If no target type is available, fall back to all condition and empty target; upstream should drop this sample
return None
if random.random() < self.N_target_random_prob: # Randomly choose target count by probability; if N_target_random_prob is 0, default to self.N_target
N_target = random.randint(1, N_all)
else:
N_target = self.N_target
if self.target_modality in ["image", "video"] :
N_target = min(N_target, N_all, self.max_num_split_vae) # Ensure target count does not exceed total count
elif self.target_modality == "text":
N_target = min(N_target, N_all, self.max_num_split_text)
# --- Select target set ---
choose_last = random.random() < self.force_last_as_gt_prob # Randomly decide whether to force the last item as target
if choose_last:
target_idx = target_pos_all[-N_target:] # Indexes of target elements
else:
target_last = random.randint(N_target - 1, N_all - 1) # Ensure the selected last target element index can cover N_target targets
target_idx = target_pos_all[target_last - N_target + 1 : target_last + 1] # Add 1 because target_last is the target element index
condition_idx = pos_all[: target_idx[0]] # Indexes of condition elements
return condition_idx, target_idx, N_target
@abstractmethod
def _process_row(self, row, parquet_idx, row_group_id, row_idx, worker_id, parquet_file_path):
pass
def __iter__(self) -> Iterator[Dict[str, Any]]:
data_paths_per_worker, worker_id = self.get_data_paths_per_worker()
if self.data_status is not None:
parquet_start_id = self.data_status[worker_id][0]
row_group_start_id = self.data_status[worker_id][1]
row_start_id = self.data_status[worker_id][2] + 1
else:
parquet_start_id = 0
row_group_start_id = 0
row_start_id = 0
# log
if data_paths_per_worker:
self.logger.info(
f"Rank-{self.local_rank} worker-{worker_id} dataset-{self.dataset_name}: "
f"{len(data_paths_per_worker)} parquet files (first: {data_paths_per_worker[0]}, "
f"last: {data_paths_per_worker[-1]}), "
f"resuming at parquet#{parquet_start_id}, rg#{row_group_start_id}, row#{row_start_id}"
)
else:
self.logger.warning(f"Rank-{self.local_rank} worker-{worker_id} dataset-{self.dataset_name}: " "has 0 parquet files!")
while True:
data_paths_per_worker_ = data_paths_per_worker[parquet_start_id:]
for parquet_idx, parquet_file_path in enumerate(data_paths_per_worker_, start=parquet_start_id):
fs = init_arrow_pf_fs(parquet_file_path)
with fs.open_input_file(parquet_file_path) as f:
fr = pq.ParquetFile(f)
row_group_ids = list(range(fr.num_row_groups))
row_group_ids_ = row_group_ids[row_group_start_id:]
# Column pruning: subclasses can provide select_columns()
cols = self.select_columns() # Default is None, which reads all columns
for row_group_id in row_group_ids_:
rows = read_parquet_rows(fr, row_group_id, columns=cols)
rows = rows[row_start_id:]
for row_idx, row in enumerate(rows, start=row_start_id):
sample = self._process_row(row, parquet_idx, row_group_id, row_idx, worker_id, parquet_file_path)
if sample:
yield sample
# self.logger.info(f"parquet_file_path: {parquet_file_path}, row_idx: {row_idx}, row_group_id: {row_group_id}, worker_id:{worker_id}, self.local_rank:{self.local_rank}") # Useful for locating bad data
row_start_id = 0
row_group_start_id = 0
parquet_start_id = 0
if self.local_rank == 0:
self.logger.info(f"{self.dataset_name} repeat in rank-{self.local_rank} worker-{worker_id}")
pass
def transform_row(self, row):
if self.dataset_type == "text2video_general":
video_bytes = row["video_bytes"]
caption = row["caption"]
interleave_array = [caption, video_bytes] if self.text_first else [video_bytes, caption]
element_dtype_array = ["text", "video"] if self.text_first else ["video", "text"]
elif self.dataset_type == "text2image_general":
image_bytes = row["image_bytes"]
caption = row["caption"]
interleave_array = [caption, image_bytes] if self.text_first else [image_bytes, caption]
element_dtype_array = ["text", "image"] if self.text_first else ["image", "text"]
elif self.dataset_type == "x2t_general":
if all(key in row for key in ["caption_i", "caption_q", "caption_a"]):
caption = [row["caption_i"], row["caption_q"], row["caption_a"]]
else:
caption = parse_videochat2it_doubao_caption(row)
if "image_bytes" in row:
interleave_array = [row["image_bytes"], caption] if not self.text_first else [caption, row["image_bytes"]]
element_dtype_array = ["image", "text"] if not self.text_first else ["text", "image"]
elif "video_bytes" in row:
interleave_array = [row["video_bytes"], caption] if not self.text_first else [caption, row["video_bytes"]]
element_dtype_array = ["video", "text"] if not self.text_first else ["text", "video"]
else:
interleave_array = [caption]
element_dtype_array = ["text"]
elif self.dataset_type == "image2image":
interleave_array = [row["caption"], row["input_image_bytes"], row["output_image_bytes"]]
element_dtype_array = ["text", "image", "image"]
self.force_last_as_gt_prob = 1
self.N_target = 1
self.N_target_random_prob = 0
self.sample_task = "edit"
elif self.dataset_type == "image2image_online":
interleave_array = [row["instruction"], row["input_image_url"], row["output_image_url"]]
element_dtype_array = ["text", "image", "image"]
self.force_last_as_gt_prob = 1
self.N_target = 1
self.N_target_random_prob = 0
self.sample_task = "edit"
elif self.dataset_type == "video2video":
interleave_array = [row["caption"], row["input_video_bytes"], row["output_video_bytes"]]
element_dtype_array = ["text", "video", "video"]
self.force_last_as_gt_prob = 1
self.N_target = 1
self.N_target_random_prob = 0
self.sample_task = "edit"
else:
raise ValueError(f"dataset_type {self.dataset_type} not supported")
return interleave_array, np.array(element_dtype_array).astype(dtype=object)