multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
5ed07ee verified
Raw
History Blame Contribute Delete
29.3 kB
"""
SVS (Singing Voice Synthesis) data loading and processing utilities.
This module contains the **preprocessed Arrow-based** SVS data pipeline used
by active training (``load_preprocessed_svs_datasets``,
``HFPreprocessedSVSDataset``, ``build_preprocessed_svs_dataloader``).
The raw folder-based pipeline and shared annotation helpers live in
:mod:`vocalrender.training.svs_raw_data` and are re-exported here for backward
compatibility.
"""
import random
from typing import Dict, List, Optional, Tuple
import torch
from datasets import Dataset
from .svs_loading import load_preprocessed_svs_datasets
class HFPreprocessedSVSDataset(torch.utils.data.Dataset):
"""
PyTorch Dataset wrapper for preprocessed SVS data (from Arrow).
This dataset loads pre-computed data directly without any on-the-fly processing.
Optionally applies random pitch/note token masking during training.
Masking — multi-strategy probabilistic selection:
``mask_strategy`` can be ``"none"`` (no masking) or a **list** of
strategy names. Each strategy has a per-sample selection
probability defined in ``mask_params``; the remaining probability
is assigned to "none". Per sample exactly one strategy fires.
Available strategies and their probability / rate keys:
=========== ================ =============== ==========================
Strategy Selection prob Internal rate Effect
=========== ================ =============== ==========================
global global_mask_prob (n/a — always Mask ALL pitch/note/BPM +
100 %) melisma collapse
paired pair_mask_prob pair_mask_rate Mask (P,N) pairs together
independent indep_mask_prob pitch_mask_rate Mask pitch / note tokens
note_mask_rate independently
=========== ================ =============== ==========================
Prompt audio / voice conditioning:
When ``prompt_audio_prob > 0`` a same-song audio segment is
prepended at the sequence front — matching the V2 TTS
pretraining layout.
"""
# Special token IDs for prompt audio boundaries (pre-SVS: inherited
# from VoxCPM2 pretraining as `<|audio_prompt_start|>` /
# `<|audio_prompt_end|>` — semantically compatible with SVS prompt-audio
# usage, so we keep the static assignment).
PROMPT_AUDIO_START_ID = 103
PROMPT_AUDIO_END_ID = 104
# Token IDs for `<audio_start>` / `<audio_end>` — inherited from VoxCPM
# pretraining (both V1 and V2).
AUDIO_START_ID = 101
AUDIO_END_ID = 102
# Map from strategy name → mask_params key for selection probability
_STRATEGY_PROB_KEYS = {
"global": "global_mask_prob",
"paired": "pair_mask_prob",
"independent": "indep_mask_prob",
}
def __init__(
self,
dataset: Dataset,
mask_strategy = "none",
mask_params: Optional[Dict] = None,
pitch_token_ids: Optional[set] = None,
note_token_ids: Optional[set] = None,
bpm_token_ids: Optional[set] = None,
mask_token_id: int = 0,
# Prompt audio configuration
prompt_audio_prob: float = 0.0,
prompt_max_frames: int = 50,
song_index: Optional[Dict[str, List[int]]] = None,
prompt_audio_seed: Optional[int] = None,
text_tokenizer=None,
):
self.dataset = dataset
self.mask_params = mask_params or {}
self.mask_token_id = mask_token_id
# --- Build strategy probability table --------------------------
# Accept str ("none" / legacy single strategy) or list of names.
if isinstance(mask_strategy, list):
active_strategies = mask_strategy
elif mask_strategy and mask_strategy != "none":
# Legacy single-strategy string
active_strategies = [mask_strategy]
else:
active_strategies = []
self._strategy_probs: List[Tuple[str, float]] = []
total_prob = 0.0
for name in active_strategies:
key = self._STRATEGY_PROB_KEYS.get(name)
if key is None:
raise ValueError(f"Unknown mask strategy: {name!r}. "
f"Choose from {list(self._STRATEGY_PROB_KEYS)}")
prob = float(self.mask_params.get(key, 0.0))
if prob > 0:
self._strategy_probs.append((name, prob))
total_prob += prob
if total_prob > 1.0 + 1e-6:
raise ValueError(f"Sum of mask strategy probabilities ({total_prob:.4f}) > 1.0")
self._masking_enabled = len(self._strategy_probs) > 0
# Prompt audio config
self.prompt_audio_prob = prompt_audio_prob
self.prompt_max_frames = prompt_max_frames
self.song_index = song_index or {}
self.prompt_audio_seed = prompt_audio_seed
# text_tokenizer kept for forward compatibility — currently unused;
# threaded in by the runner.
self.text_tokenizer = text_tokenizer
if not 0.0 <= self.prompt_audio_prob <= 1.0:
raise ValueError(
f"prompt_audio_prob must be in [0, 1], got {prompt_audio_prob}"
)
# Build reverse lookup: sample_idx -> song_name (for same-song prompt lookup)
self._idx_to_song: Dict[int, str] = {}
if self.song_index:
for song_name, indices in self.song_index.items():
for idx in indices:
self._idx_to_song[idx] = song_name
# Pre-compute frozen sets for fast O(1) lookup via torch.isin
if pitch_token_ids:
self._pitch_ids_tensor = torch.tensor(
sorted(pitch_token_ids), dtype=torch.int32
)
else:
self._pitch_ids_tensor = torch.tensor([], dtype=torch.int32)
if note_token_ids:
self._note_ids_tensor = torch.tensor(
sorted(note_token_ids), dtype=torch.int32
)
else:
self._note_ids_tensor = torch.tensor([], dtype=torch.int32)
if bpm_token_ids:
self._bpm_ids_tensor = torch.tensor(
sorted(bpm_token_ids), dtype=torch.int32
)
else:
self._bpm_ids_tensor = torch.tensor([], dtype=torch.int32)
# Union of all SVS-conditioning token IDs — pitch + note + BPM +
# <SVS_MASK>.
svs_cond_ids = set()
svs_cond_ids.update(pitch_token_ids or set())
svs_cond_ids.update(note_token_ids or set())
svs_cond_ids.update(bpm_token_ids or set())
if mask_token_id:
svs_cond_ids.add(mask_token_id)
if svs_cond_ids:
self._svs_cond_ids_tensor = torch.tensor(
sorted(svs_cond_ids), dtype=torch.int32
)
else:
self._svs_cond_ids_tensor = torch.tensor([], dtype=torch.int32)
def __len__(self):
return len(self.dataset)
def __getitem__(self, idx: int) -> Dict:
item = self.dataset[idx]
# Convert lists back to tensors
packed_text_tokens = torch.tensor(item["packed_text_tokens"], dtype=torch.int32)
audio_feats = torch.tensor(item["audio_feats"], dtype=torch.float32)
text_mask = torch.tensor(item["text_mask"], dtype=torch.int32)
audio_mask = torch.tensor(item["audio_mask"], dtype=torch.int32)
loss_mask = torch.tensor(item["loss_mask"], dtype=torch.int32)
labels = torch.tensor(item["labels"], dtype=torch.int32)
# Apply pitch/note masking (training-time data augmentation)
# Skip for weak-label samples — their BPM/pitch/note are already <SVS_MASK>
has_score = item.get("has_score", True)
if has_score and self._masking_enabled:
selected = self._select_mask_strategy()
if selected == "global":
# Global mask with melisma collapse — modifies all tensors
packed_text_tokens, audio_feats, text_mask, audio_mask, loss_mask, labels = \
self._apply_global_mask_collapse(
packed_text_tokens, audio_feats, text_mask, audio_mask, loss_mask, labels)
elif selected == "paired":
packed_text_tokens = self._mask_paired(packed_text_tokens.clone())
elif selected == "independent":
packed_text_tokens = self._mask_independent(packed_text_tokens.clone())
if self.prompt_audio_prob > 0:
if self.prompt_audio_seed is not None:
rng = random.Random(self.prompt_audio_seed + idx)
else:
rng = random
if rng.random() < self.prompt_audio_prob:
# Baseline SVS: prepend same-song prompt at the front.
packed_text_tokens, audio_feats, text_mask, audio_mask, loss_mask, labels = \
self._prepend_prompt_audio(
idx, packed_text_tokens, audio_feats,
text_mask, audio_mask, loss_mask, labels,
rng=rng,
)
total_length = packed_text_tokens.shape[0]
sample: Dict = {
"packed_text_tokens": packed_text_tokens,
"audio_feats": audio_feats,
"text_mask": text_mask,
"audio_mask": audio_mask,
"loss_mask": loss_mask,
"labels": labels,
"audio_duration": item["audio_duration"],
"text_token_count": item["text_token_count"],
"total_length": total_length,
}
return sample
# ------------------------------------------------------------------
# Prompt audio prepending
# ------------------------------------------------------------------
def _prepend_audio_prefix(
self,
prompt_audio_region: torch.Tensor,
packed_text_tokens: torch.Tensor,
audio_feats: torch.Tensor,
text_mask: torch.Tensor,
audio_mask: torch.Tensor,
loss_mask: torch.Tensor,
labels: torch.Tensor,
) -> Tuple:
"""Prepend an already-resolved audio conditioning region."""
T_prompt = prompt_audio_region.shape[0]
if T_prompt <= 0:
return packed_text_tokens, audio_feats, text_mask, audio_mask, loss_mask, labels
P, D = prompt_audio_region.shape[1], prompt_audio_region.shape[2]
prompt_text = torch.tensor(
[self.PROMPT_AUDIO_START_ID] + [0] * T_prompt + [self.PROMPT_AUDIO_END_ID],
dtype=torch.int32,
)
prompt_text_mask = torch.tensor([1] + [0] * T_prompt + [1], dtype=torch.int32)
prompt_audio_mask = torch.tensor([0] + [1] * T_prompt + [0], dtype=torch.int32)
prompt_loss_mask = torch.zeros(T_prompt + 2, dtype=torch.int32)
prompt_labels = torch.zeros(T_prompt + 2, dtype=torch.int32)
prompt_audio_padded = torch.cat([
torch.zeros(1, P, D, dtype=torch.float32),
prompt_audio_region.to(torch.float32),
torch.zeros(1, P, D, dtype=torch.float32),
], dim=0)
packed_text_tokens = torch.cat([prompt_text, packed_text_tokens])
audio_feats = torch.cat([prompt_audio_padded, audio_feats], dim=0)
text_mask = torch.cat([prompt_text_mask, text_mask])
audio_mask = torch.cat([prompt_audio_mask, audio_mask])
loss_mask = torch.cat([prompt_loss_mask, loss_mask])
labels = torch.cat([prompt_labels, labels])
return packed_text_tokens, audio_feats, text_mask, audio_mask, loss_mask, labels
def _extract_same_song_prompt(
self,
idx: int,
rng=None,
) -> Optional[torch.Tensor]:
"""Pick a random non-self segment from the same song and return its
audio latent region ``[T_prompt, P, D]``.
Returns ``None`` when the song lookup fails, the song has no other
segments, or the candidate's ``audio_mask`` contains no audio
positions. Cropping respects ``prompt_max_frames``.
"""
if rng is None:
rng = random
song_name = self._idx_to_song.get(idx)
if not song_name:
return None
candidates = self.song_index.get(song_name, [])
other_candidates = [c for c in candidates if c != idx]
if not other_candidates:
return None
prompt_idx = rng.choice(other_candidates)
prompt_item = self.dataset[prompt_idx]
prompt_audio = torch.tensor(prompt_item["audio_feats"], dtype=torch.float32)
prompt_audio_mask_raw = torch.tensor(prompt_item["audio_mask"], dtype=torch.int32)
audio_positions = torch.where(prompt_audio_mask_raw == 1)[0]
if len(audio_positions) == 0:
return None
start_pos = int(audio_positions[0].item())
end_pos = int(audio_positions[-1].item()) + 1
prompt_audio_region = prompt_audio[start_pos:end_pos]
T_prompt = int(prompt_audio_region.shape[0])
if T_prompt > self.prompt_max_frames:
crop_start = rng.randint(0, T_prompt - self.prompt_max_frames)
prompt_audio_region = prompt_audio_region[
crop_start:crop_start + self.prompt_max_frames
]
return prompt_audio_region
def _prepend_prompt_audio(
self,
idx: int,
packed_text_tokens: torch.Tensor,
audio_feats: torch.Tensor,
text_mask: torch.Tensor,
audio_mask: torch.Tensor,
loss_mask: torch.Tensor,
labels: torch.Tensor,
rng=None,
) -> Tuple:
"""Prepend a same-song audio segment as prompt prefix."""
prompt_audio_region = self._extract_same_song_prompt(idx, rng=rng)
if prompt_audio_region is None:
return packed_text_tokens, audio_feats, text_mask, audio_mask, loss_mask, labels
return self._prepend_audio_prefix(
prompt_audio_region,
packed_text_tokens,
audio_feats,
text_mask,
audio_mask,
loss_mask,
labels,
)
# ------------------------------------------------------------------
# Strategy selection
# ------------------------------------------------------------------
def _select_mask_strategy(self) -> str:
"""Randomly select a masking strategy based on configured probabilities.
Returns one of the strategy names or ``"none"``.
"""
roll = random.random()
cumulative = 0.0
for name, prob in self._strategy_probs:
cumulative += prob
if roll < cumulative:
return name
return "none"
# ------------------------------------------------------------------
# Masking helpers
# ------------------------------------------------------------------
def _mask_independent(self, tokens: torch.Tensor) -> torch.Tensor:
"""Mask each pitch / note token independently.
Uses ``pitch_mask_rate`` and ``note_mask_rate`` from mask_params
(default 1.0 = mask all when strategy is selected).
"""
pitch_rate = self.mask_params.get("pitch_mask_rate", 1.0)
note_rate = self.mask_params.get("note_mask_rate", 1.0)
if pitch_rate > 0 and len(self._pitch_ids_tensor) > 0:
is_pitch = torch.isin(tokens, self._pitch_ids_tensor)
if pitch_rate >= 1.0:
tokens[is_pitch] = self.mask_token_id
else:
mask_draw = torch.rand(tokens.shape) < pitch_rate
tokens[is_pitch & mask_draw] = self.mask_token_id
if note_rate > 0 and len(self._note_ids_tensor) > 0:
is_note = torch.isin(tokens, self._note_ids_tensor)
if note_rate >= 1.0:
tokens[is_note] = self.mask_token_id
else:
mask_draw = torch.rand(tokens.shape) < note_rate
tokens[is_note & mask_draw] = self.mask_token_id
return tokens
def _mask_paired(self, tokens: torch.Tensor) -> torch.Tensor:
"""Mask adjacent (pitch, note) pairs together.
In the aggregated prompt layout, pitch and note tokens always
appear as adjacent pairs: [..., <P_x>, <NOTE_y>, ...].
Uses ``pair_mask_rate`` from mask_params (default 1.0 = mask
all pairs when strategy is selected).
"""
pair_rate = self.mask_params.get("pair_mask_rate", 1.0)
if pair_rate <= 0:
return tokens
is_pitch = torch.isin(tokens, self._pitch_ids_tensor)
is_note = torch.isin(tokens, self._note_ids_tensor)
# Find positions where tokens[i] is pitch and tokens[i+1] is note
n = tokens.shape[0]
if n < 2:
return tokens
pair_starts = is_pitch[:-1] & is_note[1:]
pair_indices = torch.where(pair_starts)[0]
if len(pair_indices) == 0:
return tokens
if pair_rate >= 1.0:
# Mask all pairs
for idx in pair_indices:
tokens[idx] = self.mask_token_id
tokens[idx + 1] = self.mask_token_id
else:
mask_draw = torch.rand(len(pair_indices)) < pair_rate
for idx, do_mask in zip(pair_indices, mask_draw):
if do_mask:
tokens[idx] = self.mask_token_id
tokens[idx + 1] = self.mask_token_id
return tokens
def _apply_global_mask_collapse(
self,
packed_text_tokens: torch.Tensor,
audio_feats: torch.Tensor,
text_mask: torch.Tensor,
audio_mask: torch.Tensor,
loss_mask: torch.Tensor,
labels: torch.Tensor,
):
"""Global mask with melisma collapse.
Masks ALL pitch, note & BPM tokens AND collapses melisma so
each word has exactly one <SVS_MASK> pair, exactly matching
the weak-label token structure.
Since removing tokens changes sequence length, this method
operates on all tensors together (not just tokens).
Note: Strategy selection (coin flip) is handled by
``_select_mask_strategy``; this method always applies.
"""
tokens = packed_text_tokens.clone()
n = len(tokens)
# Classify each token
is_pitch = torch.isin(tokens, self._pitch_ids_tensor)
is_note = torch.isin(tokens, self._note_ids_tensor)
is_bpm = torch.isin(tokens, self._bpm_ids_tensor)
is_score = is_pitch | is_note # pitch or note token
# Mask all BPM tokens
tokens[is_bpm] = self.mask_token_id
# Walk through the sequence, find consecutive runs of score tokens
# (pitch/note groups). Within each group, keep only the first
# pitch-note pair and mark the rest for removal.
remove_mask = torch.zeros(n, dtype=torch.bool)
i = 0
while i < n:
if not is_score[i]:
i += 1
continue
# Found start of a score-token group
group_start = i
while i < n and is_score[i]:
i += 1
group_end = i # exclusive
# Count pitch tokens → each pitch starts one pair
pair_count = 0
first_pair_end = group_start # will be updated
for k in range(group_start, group_end):
if is_pitch[k]:
pair_count += 1
if pair_count == 1:
# Replace first pair with <SVS_MASK>
tokens[k] = self.mask_token_id
# The note following this pitch
if k + 1 < group_end and is_note[k + 1]:
tokens[k + 1] = self.mask_token_id
first_pair_end = k + 2
else:
first_pair_end = k + 1
# Mark everything after the first pair for removal
if pair_count > 1:
for k in range(first_pair_end, group_end):
remove_mask[k] = True
# Remove extra melisma tokens from all tensors
if remove_mask.any():
keep = ~remove_mask
tokens = tokens[keep]
audio_feats = audio_feats[keep]
text_mask = text_mask[keep]
audio_mask = audio_mask[keep]
loss_mask = loss_mask[keep]
labels = labels[keep]
return tokens, audio_feats, text_mask, audio_mask, loss_mask, labels
@staticmethod
def collate_fn(batch: List[Dict]) -> Dict[str, torch.Tensor]:
"""
Collate preprocessed SVS batch.
Pads all sequences to the maximum length in the batch.
"""
# Find max length
max_len = max(sample["total_length"] for sample in batch)
# Pad and stack tensors
packed_text_list = []
audio_feats_list = []
text_mask_list = []
audio_mask_list = []
loss_mask_list = []
labels_list = []
for sample in batch:
cur_len = sample["total_length"]
pad_len = max_len - cur_len
# Pad packed_text_tokens
if pad_len > 0:
packed_text = torch.nn.functional.pad(
sample["packed_text_tokens"], (0, pad_len), value=0
)
else:
packed_text = sample["packed_text_tokens"][:max_len]
packed_text_list.append(packed_text)
# Pad audio_feats [T, P, D]
audio_feats = sample["audio_feats"]
if pad_len > 0:
pad_feats = torch.zeros(
(pad_len, audio_feats.shape[1], audio_feats.shape[2]),
dtype=audio_feats.dtype
)
audio_feats = torch.cat([audio_feats, pad_feats], dim=0)
else:
audio_feats = audio_feats[:max_len]
audio_feats_list.append(audio_feats)
# Pad masks
for mask_list, mask_tensor in [
(text_mask_list, sample["text_mask"]),
(audio_mask_list, sample["audio_mask"]),
(loss_mask_list, sample["loss_mask"]),
(labels_list, sample["labels"]),
]:
if pad_len > 0:
padded = torch.nn.functional.pad(mask_tensor, (0, pad_len), value=0)
else:
padded = mask_tensor[:max_len]
mask_list.append(padded)
# Stack into batches
text_tokens_batch = torch.stack(packed_text_list, dim=0)
audio_feats_batch = torch.stack(audio_feats_list, dim=0)
text_mask_batch = torch.stack(text_mask_list, dim=0)
audio_mask_batch = torch.stack(audio_mask_list, dim=0)
loss_mask_batch = torch.stack(loss_mask_list, dim=0)
labels_batch = torch.stack(labels_list, dim=0)
# Return contiguous tensors to avoid pin_memory issues
out: Dict[str, torch.Tensor] = {
"text_tokens": text_tokens_batch.contiguous(),
"audio_feats": audio_feats_batch.contiguous(),
"text_mask": text_mask_batch.contiguous(),
"audio_mask": audio_mask_batch.contiguous(),
"loss_mask": loss_mask_batch.contiguous(),
"labels": labels_batch.contiguous(),
}
return out
# DynamicBatchSampler lives in its own module; re-export here for
# backward compatibility with existing imports.
from .dynamic_batch import DynamicBatchSampler # noqa: F401
# Raw folder-based pipeline and shared annotation helpers live in
# svs_raw_data; re-export here for backward compatibility.
from .svs_raw_data import ( # noqa: F401
convert_annotation_to_syllables,
convert_words_to_syllables,
expand_syllables,
get_note_duration_map,
build_svs_text_tensor,
tokenize_svs_batch,
HFSVSDataset,
build_svs_dataloader,
load_svs_datasets,
DEFAULT_AUDIO_COLUMN,
DEFAULT_ID_COLUMN,
)
def build_preprocessed_svs_dataloader(
hf_dataset: Dataset,
*,
accelerator,
batch_size: int,
num_workers: int,
drop_last: bool = False,
max_batch_tokens: int = 0,
mask_strategy: str = "none",
mask_params: Optional[Dict] = None,
pitch_token_ids: Optional[set] = None,
note_token_ids: Optional[set] = None,
bpm_token_ids: Optional[set] = None,
mask_token_id: int = 0,
# Prompt audio configuration
prompt_audio_prob: float = 0.0,
prompt_max_frames: int = 50,
song_index: Optional[Dict[str, List[int]]] = None,
prompt_audio_seed: Optional[int] = None,
text_tokenizer=None,
) -> torch.utils.data.DataLoader:
"""
Build DataLoader for preprocessed SVS dataset.
Args:
hf_dataset: HuggingFace dataset with preprocessed data
accelerator: Training accelerator
batch_size: Fixed batch size (used when max_batch_tokens=0)
num_workers: Number of data loading workers
drop_last: Whether to drop the last incomplete batch
max_batch_tokens: Maximum tokens per batch. If > 0, uses dynamic batching
instead of fixed batch_size. Recommended: 8192-16384.
mask_strategy: Masking strategy: ``\"none\"`` or list of strategies (e.g. ``[\"global\", \"paired\"]``)
mask_params: Strategy-specific parameters dict
pitch_token_ids: Set of pitch token IDs for masking
note_token_ids: Set of note token IDs for masking
bpm_token_ids: Set of BPM token IDs for masking (used by global strategy)
mask_token_id: ID of the <SVS_MASK> token
prompt_audio_prob: Probability of prepending prompt audio
prompt_max_frames: Maximum number of audio frames for same-song prompt cropping
song_index: Mapping of song_name -> list of sample indices for same-song prompt selection
prompt_audio_seed: If set, use deterministic RNG per sample (for validation)
Returns:
DataLoader with either fixed or dynamic batching
"""
torch_dataset = HFPreprocessedSVSDataset(
hf_dataset,
mask_strategy=mask_strategy,
mask_params=mask_params,
pitch_token_ids=pitch_token_ids,
note_token_ids=note_token_ids,
bpm_token_ids=bpm_token_ids,
mask_token_id=mask_token_id,
prompt_audio_prob=prompt_audio_prob,
prompt_max_frames=prompt_max_frames,
song_index=song_index,
prompt_audio_seed=prompt_audio_seed,
text_tokenizer=text_tokenizer,
)
if max_batch_tokens > 0:
# Extract lengths directly from HuggingFace dataset column (instant!)
# This avoids iterating through the dataset which would be slow
lengths = hf_dataset["total_length"] # Direct column access
# When prompt audio is enabled, increase estimated lengths to
# account for the same-song prompt prepended at the sequence front
# at runtime.
if prompt_audio_prob > 0 and prompt_max_frames > 0:
avg_prompt_overhead = int((prompt_max_frames * 0.75 + 2) * prompt_audio_prob + 0.5)
lengths = [l + avg_prompt_overhead for l in lengths]
# Use dynamic batching with distributed support
# For HYBRID_SHARD: all ranks inside the same shard group process the
# same micro-batch (params sharded, same data). Partition data across
# replica groups only, using dp_rank / dp_world_size.
batch_sampler = DynamicBatchSampler(
lengths=lengths,
max_batch_tokens=max_batch_tokens,
max_batch_size=batch_size, # Use batch_size as upper cap
shuffle=True,
drop_last=drop_last,
rank=accelerator.dp_rank,
world_size=accelerator.dp_world_size,
)
# Note: When using batch_sampler, we can't use accelerator.prepare_dataloader
# directly because it conflicts with batch_sampler. Create manually.
loader = torch.utils.data.DataLoader(
torch_dataset,
batch_sampler=batch_sampler,
num_workers=num_workers,
collate_fn=HFPreprocessedSVSDataset.collate_fn,
pin_memory=True,
)
# Store batch_sampler reference for epoch management in training loop
# Use object.__setattr__ to bypass PyTorch DataLoader's attribute restrictions
object.__setattr__(loader, '_dynamic_sampler', batch_sampler)
return loader
else:
# Use fixed batch size
return accelerator.prepare_dataloader(
torch_dataset,
batch_size=batch_size,
num_workers=num_workers,
shuffle=True,
collate_fn=HFPreprocessedSVSDataset.collate_fn,
drop_last=drop_last,
)