marcos Claude Opus 4.5 commited on
Commit ·
e20f447
1
Parent(s): 5cc96de
feat: Refactor training with SOLID principles and add optimizations
Browse filesTraining refactor:
- Modular training package (config, data, models, trainer, checkpoint)
- Auto GPU detection (H200 141GB, A100, RTX 4090)
- Flash Attention 2 with fallback to SDPA
- OOM recovery with proper cleanup (PyTorch best practices)
- NaN/Inf detection and gradient monitoring
- Sequence length bucketing for memory efficiency
- BF16 for numerical stability
Stage 1 improvements:
- Added --no_decay flag (fixed text_ratio for adapter warmup)
- Dynamic decay optional via --use_decay
Dataset tools:
- analyze_dataset.py for dataset inspection
- validate_dataset.py for format validation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- datasets/analyze_dataset.py +575 -0
- datasets/create_dataset.py +569 -142
- datasets/validate_dataset.py +318 -0
- passo0_setup.py +36 -12
- passo2_finetune_stage1.py +162 -811
- passo3_finetune_stage2.py +223 -850
- passo4_inference.py +195 -72
- training/__init__.py +30 -0
- training/checkpoint.py +273 -0
- training/config.py +231 -0
- training/data.py +734 -0
- training/interleaving.py +420 -0
- training/models.py +247 -0
- training/trainer.py +565 -0
- training/utils.py +255 -0
datasets/analyze_dataset.py
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Deep Dataset Analyzer - Comprehensive analysis of generated dataset.
|
| 4 |
+
|
| 5 |
+
Analyzes:
|
| 6 |
+
1. Structure and field presence
|
| 7 |
+
2. Whisper features (shape, statistics, quality)
|
| 8 |
+
3. SNAC tokens (offsets, distribution, frame integrity)
|
| 9 |
+
4. Text content (questions, answers, tokenization)
|
| 10 |
+
5. Word alignments (timing, coverage, token mapping)
|
| 11 |
+
6. Cross-field consistency
|
| 12 |
+
7. Training readiness checks
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import argparse
|
| 16 |
+
import sys
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
from collections import defaultdict, Counter
|
| 19 |
+
import json
|
| 20 |
+
|
| 21 |
+
import torch
|
| 22 |
+
import numpy as np
|
| 23 |
+
|
| 24 |
+
# SNAC constants
|
| 25 |
+
SNAC_BASE = 128266
|
| 26 |
+
SNAC_LAYERS = 7
|
| 27 |
+
SNAC_VOCAB_PER_LAYER = 4096
|
| 28 |
+
WHISPER_DIM = 1280
|
| 29 |
+
EOS_TOKEN = 128009
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def print_header(title):
|
| 33 |
+
print(f"\n{'='*70}")
|
| 34 |
+
print(f" {title}")
|
| 35 |
+
print('='*70)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def print_subheader(title):
|
| 39 |
+
print(f"\n{'-'*50}")
|
| 40 |
+
print(f" {title}")
|
| 41 |
+
print('-'*50)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def analyze_whisper_features(dataset):
|
| 45 |
+
"""Deep analysis of Whisper features."""
|
| 46 |
+
print_header("WHISPER FEATURES ANALYSIS")
|
| 47 |
+
|
| 48 |
+
lengths = []
|
| 49 |
+
dims = []
|
| 50 |
+
min_vals = []
|
| 51 |
+
max_vals = []
|
| 52 |
+
mean_vals = []
|
| 53 |
+
std_vals = []
|
| 54 |
+
has_nan = 0
|
| 55 |
+
has_inf = 0
|
| 56 |
+
dtypes = Counter()
|
| 57 |
+
|
| 58 |
+
for i, sample in enumerate(dataset):
|
| 59 |
+
if "whisper_features" not in sample:
|
| 60 |
+
continue
|
| 61 |
+
|
| 62 |
+
wf = sample["whisper_features"]
|
| 63 |
+
|
| 64 |
+
if not isinstance(wf, torch.Tensor):
|
| 65 |
+
print(f" [WARN] Sample {i}: whisper_features is {type(wf).__name__}, not Tensor")
|
| 66 |
+
continue
|
| 67 |
+
|
| 68 |
+
dtypes[str(wf.dtype)] += 1
|
| 69 |
+
lengths.append(wf.shape[0])
|
| 70 |
+
|
| 71 |
+
if wf.dim() >= 2:
|
| 72 |
+
dims.append(wf.shape[1])
|
| 73 |
+
|
| 74 |
+
if torch.isnan(wf).any():
|
| 75 |
+
has_nan += 1
|
| 76 |
+
if torch.isinf(wf).any():
|
| 77 |
+
has_inf += 1
|
| 78 |
+
|
| 79 |
+
min_vals.append(wf.min().item())
|
| 80 |
+
max_vals.append(wf.max().item())
|
| 81 |
+
mean_vals.append(wf.mean().item())
|
| 82 |
+
std_vals.append(wf.std().item())
|
| 83 |
+
|
| 84 |
+
if not lengths:
|
| 85 |
+
print(" No whisper_features found!")
|
| 86 |
+
return
|
| 87 |
+
|
| 88 |
+
lengths = np.array(lengths)
|
| 89 |
+
|
| 90 |
+
print(f"\n Samples with whisper_features: {len(lengths)}/{len(dataset)}")
|
| 91 |
+
print(f" Data types: {dict(dtypes)}")
|
| 92 |
+
|
| 93 |
+
print_subheader("Sequence Length")
|
| 94 |
+
print(f" Min: {lengths.min()}, Max: {lengths.max()}")
|
| 95 |
+
print(f" Mean: {lengths.mean():.1f}, Std: {lengths.std():.1f}")
|
| 96 |
+
print(f" Median: {np.median(lengths):.1f}")
|
| 97 |
+
|
| 98 |
+
# Length distribution
|
| 99 |
+
percentiles = [10, 25, 50, 75, 90, 95, 99]
|
| 100 |
+
print(f" Percentiles: {dict(zip(percentiles, [int(np.percentile(lengths, p)) for p in percentiles]))}")
|
| 101 |
+
|
| 102 |
+
if dims:
|
| 103 |
+
dims = np.array(dims)
|
| 104 |
+
unique_dims = np.unique(dims)
|
| 105 |
+
print(f"\n Feature dimension: {unique_dims}")
|
| 106 |
+
if len(unique_dims) == 1 and unique_dims[0] == WHISPER_DIM:
|
| 107 |
+
print(f" ✅ Correct Whisper dimension ({WHISPER_DIM})")
|
| 108 |
+
else:
|
| 109 |
+
print(f" ⚠️ Expected dimension {WHISPER_DIM}")
|
| 110 |
+
|
| 111 |
+
print_subheader("Value Statistics")
|
| 112 |
+
print(f" Min value: {np.min(min_vals):.4f}")
|
| 113 |
+
print(f" Max value: {np.max(max_vals):.4f}")
|
| 114 |
+
print(f" Mean of means: {np.mean(mean_vals):.4f}")
|
| 115 |
+
print(f" Mean of stds: {np.mean(std_vals):.4f}")
|
| 116 |
+
|
| 117 |
+
print_subheader("Quality Checks")
|
| 118 |
+
print(f" Samples with NaN: {has_nan} {'⚠️' if has_nan > 0 else '✅'}")
|
| 119 |
+
print(f" Samples with Inf: {has_inf} {'⚠️' if has_inf > 0 else '✅'}")
|
| 120 |
+
|
| 121 |
+
# Check for constant/zero features
|
| 122 |
+
zero_features = sum(1 for s in std_vals if s < 0.001)
|
| 123 |
+
print(f" Samples with near-zero std: {zero_features} {'⚠️' if zero_features > 0 else '✅'}")
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def analyze_snac_tokens(dataset):
|
| 127 |
+
"""Deep analysis of SNAC tokens."""
|
| 128 |
+
print_header("SNAC TOKENS ANALYSIS")
|
| 129 |
+
|
| 130 |
+
lengths = []
|
| 131 |
+
frame_counts = []
|
| 132 |
+
token_values = []
|
| 133 |
+
offset_distribution = defaultdict(int)
|
| 134 |
+
raw_token_distribution = defaultdict(int)
|
| 135 |
+
incomplete_frames = 0
|
| 136 |
+
has_offset = 0
|
| 137 |
+
no_offset = 0
|
| 138 |
+
dtypes = Counter()
|
| 139 |
+
|
| 140 |
+
for i, sample in enumerate(dataset):
|
| 141 |
+
if "snac_tokens" not in sample:
|
| 142 |
+
continue
|
| 143 |
+
|
| 144 |
+
st = sample["snac_tokens"]
|
| 145 |
+
|
| 146 |
+
if not isinstance(st, torch.Tensor):
|
| 147 |
+
print(f" [WARN] Sample {i}: snac_tokens is {type(st).__name__}, not Tensor")
|
| 148 |
+
continue
|
| 149 |
+
|
| 150 |
+
dtypes[str(st.dtype)] += 1
|
| 151 |
+
length = len(st)
|
| 152 |
+
lengths.append(length)
|
| 153 |
+
frame_counts.append(length // SNAC_LAYERS)
|
| 154 |
+
|
| 155 |
+
if length % SNAC_LAYERS != 0:
|
| 156 |
+
incomplete_frames += 1
|
| 157 |
+
|
| 158 |
+
# Analyze token values
|
| 159 |
+
for j, tok in enumerate(st.tolist()):
|
| 160 |
+
token_values.append(tok)
|
| 161 |
+
|
| 162 |
+
if tok >= SNAC_BASE:
|
| 163 |
+
has_offset += 1
|
| 164 |
+
# Determine which position offset
|
| 165 |
+
offset_idx = (tok - SNAC_BASE) // SNAC_VOCAB_PER_LAYER
|
| 166 |
+
offset_distribution[offset_idx] += 1
|
| 167 |
+
# Get raw token value
|
| 168 |
+
raw_tok = (tok - SNAC_BASE) % SNAC_VOCAB_PER_LAYER
|
| 169 |
+
raw_token_distribution[raw_tok] += 1
|
| 170 |
+
else:
|
| 171 |
+
no_offset += 1
|
| 172 |
+
|
| 173 |
+
if not lengths:
|
| 174 |
+
print(" No snac_tokens found!")
|
| 175 |
+
return
|
| 176 |
+
|
| 177 |
+
lengths = np.array(lengths)
|
| 178 |
+
frame_counts = np.array(frame_counts)
|
| 179 |
+
token_values = np.array(token_values)
|
| 180 |
+
|
| 181 |
+
print(f"\n Samples with snac_tokens: {len(lengths)}/{len(dataset)}")
|
| 182 |
+
print(f" Data types: {dict(dtypes)}")
|
| 183 |
+
|
| 184 |
+
print_subheader("Token Counts")
|
| 185 |
+
print(f" Total tokens: {len(token_values):,}")
|
| 186 |
+
print(f" Min per sample: {lengths.min()}, Max: {lengths.max()}")
|
| 187 |
+
print(f" Mean: {lengths.mean():.1f}, Std: {lengths.std():.1f}")
|
| 188 |
+
|
| 189 |
+
print_subheader("Frame Analysis (7 tokens per frame)")
|
| 190 |
+
print(f" Min frames: {frame_counts.min()}, Max: {frame_counts.max()}")
|
| 191 |
+
print(f" Mean frames: {frame_counts.mean():.1f}")
|
| 192 |
+
print(f" Incomplete frames (not multiple of 7): {incomplete_frames} {'⚠️' if incomplete_frames > 0 else '✅'}")
|
| 193 |
+
|
| 194 |
+
# Duration estimation (75 frames/second for SNAC 24kHz)
|
| 195 |
+
durations = frame_counts / 75.0
|
| 196 |
+
print(f"\n Audio duration (estimated):")
|
| 197 |
+
print(f" Min: {durations.min():.2f}s, Max: {durations.max():.2f}s")
|
| 198 |
+
print(f" Mean: {durations.mean():.2f}s, Total: {durations.sum()/60:.1f} min")
|
| 199 |
+
|
| 200 |
+
print_subheader("Token Offset Analysis")
|
| 201 |
+
print(f" Tokens with offset (>= {SNAC_BASE}): {has_offset:,} ({100*has_offset/len(token_values):.1f}%)")
|
| 202 |
+
print(f" Tokens without offset: {no_offset:,} ({100*no_offset/len(token_values):.1f}%)")
|
| 203 |
+
|
| 204 |
+
if has_offset > 0:
|
| 205 |
+
print(f"\n Offset distribution by position (0-6):")
|
| 206 |
+
for pos in range(SNAC_LAYERS):
|
| 207 |
+
count = offset_distribution.get(pos, 0)
|
| 208 |
+
expected_pct = 100 / SNAC_LAYERS
|
| 209 |
+
actual_pct = 100 * count / has_offset if has_offset > 0 else 0
|
| 210 |
+
status = "✅" if abs(actual_pct - expected_pct) < 5 else "⚠️"
|
| 211 |
+
print(f" Position {pos}: {count:,} ({actual_pct:.1f}%) {status}")
|
| 212 |
+
|
| 213 |
+
print_subheader("Token Value Range")
|
| 214 |
+
print(f" Min token: {token_values.min()}")
|
| 215 |
+
print(f" Max token: {token_values.max()}")
|
| 216 |
+
|
| 217 |
+
if has_offset > 0:
|
| 218 |
+
expected_min = SNAC_BASE
|
| 219 |
+
expected_max = SNAC_BASE + (SNAC_LAYERS * SNAC_VOCAB_PER_LAYER) - 1
|
| 220 |
+
print(f" Expected range: [{expected_min}, {expected_max}]")
|
| 221 |
+
if token_values.min() >= expected_min and token_values.max() <= expected_max:
|
| 222 |
+
print(f" ✅ Token range valid")
|
| 223 |
+
else:
|
| 224 |
+
print(f" ⚠️ Some tokens outside expected range")
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def analyze_text_content(dataset):
|
| 228 |
+
"""Analyze text fields (question, answer, text_tokens)."""
|
| 229 |
+
print_header("TEXT CONTENT ANALYSIS")
|
| 230 |
+
|
| 231 |
+
questions = []
|
| 232 |
+
answers = []
|
| 233 |
+
text_token_lengths = []
|
| 234 |
+
has_text = 0
|
| 235 |
+
has_answer = 0
|
| 236 |
+
has_text_tokens = 0
|
| 237 |
+
|
| 238 |
+
for sample in dataset:
|
| 239 |
+
if "text" in sample and sample["text"]:
|
| 240 |
+
has_text += 1
|
| 241 |
+
questions.append(sample["text"])
|
| 242 |
+
|
| 243 |
+
if "answer" in sample and sample["answer"]:
|
| 244 |
+
has_answer += 1
|
| 245 |
+
answers.append(sample["answer"])
|
| 246 |
+
|
| 247 |
+
if "text_tokens" in sample:
|
| 248 |
+
has_text_tokens += 1
|
| 249 |
+
tt = sample["text_tokens"]
|
| 250 |
+
if isinstance(tt, torch.Tensor):
|
| 251 |
+
text_token_lengths.append(len(tt))
|
| 252 |
+
elif isinstance(tt, list):
|
| 253 |
+
text_token_lengths.append(len(tt))
|
| 254 |
+
|
| 255 |
+
print(f"\n Field presence:")
|
| 256 |
+
print(f" text (question): {has_text}/{len(dataset)} ({100*has_text/len(dataset):.1f}%)")
|
| 257 |
+
print(f" answer: {has_answer}/{len(dataset)} ({100*has_answer/len(dataset):.1f}%)")
|
| 258 |
+
print(f" text_tokens: {has_text_tokens}/{len(dataset)} ({100*has_text_tokens/len(dataset):.1f}%)")
|
| 259 |
+
|
| 260 |
+
if questions:
|
| 261 |
+
print_subheader("Questions Analysis")
|
| 262 |
+
q_lengths = [len(q) for q in questions]
|
| 263 |
+
q_words = [len(q.split()) for q in questions]
|
| 264 |
+
print(f" Character length: min={min(q_lengths)}, max={max(q_lengths)}, mean={np.mean(q_lengths):.1f}")
|
| 265 |
+
print(f" Word count: min={min(q_words)}, max={max(q_words)}, mean={np.mean(q_words):.1f}")
|
| 266 |
+
|
| 267 |
+
# Sample questions
|
| 268 |
+
print(f"\n Sample questions:")
|
| 269 |
+
for q in questions[:3]:
|
| 270 |
+
print(f" - {q[:80]}{'...' if len(q) > 80 else ''}")
|
| 271 |
+
|
| 272 |
+
if answers:
|
| 273 |
+
print_subheader("Answers Analysis")
|
| 274 |
+
a_lengths = [len(a) for a in answers]
|
| 275 |
+
a_words = [len(a.split()) for a in answers]
|
| 276 |
+
print(f" Character length: min={min(a_lengths)}, max={max(a_lengths)}, mean={np.mean(a_lengths):.1f}")
|
| 277 |
+
print(f" Word count: min={min(a_words)}, max={max(a_words)}, mean={np.mean(a_words):.1f}")
|
| 278 |
+
|
| 279 |
+
# Sample answers
|
| 280 |
+
print(f"\n Sample answers:")
|
| 281 |
+
for a in answers[:3]:
|
| 282 |
+
print(f" - {a[:100]}{'...' if len(a) > 100 else ''}")
|
| 283 |
+
|
| 284 |
+
if text_token_lengths:
|
| 285 |
+
print_subheader("Pre-tokenized Answer Tokens")
|
| 286 |
+
tl = np.array(text_token_lengths)
|
| 287 |
+
print(f" Token count: min={tl.min()}, max={tl.max()}, mean={tl.mean():.1f}")
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def analyze_word_alignments(dataset):
|
| 291 |
+
"""Analyze word alignments for IST-LM interleaving."""
|
| 292 |
+
print_header("WORD ALIGNMENTS ANALYSIS")
|
| 293 |
+
|
| 294 |
+
has_alignments = 0
|
| 295 |
+
word_counts = []
|
| 296 |
+
frame_coverages = []
|
| 297 |
+
has_tokens = 0
|
| 298 |
+
token_counts = []
|
| 299 |
+
timing_issues = 0
|
| 300 |
+
|
| 301 |
+
for i, sample in enumerate(dataset):
|
| 302 |
+
if "word_alignments" not in sample or not sample["word_alignments"]:
|
| 303 |
+
continue
|
| 304 |
+
|
| 305 |
+
has_alignments += 1
|
| 306 |
+
wa = sample["word_alignments"]
|
| 307 |
+
word_counts.append(len(wa))
|
| 308 |
+
|
| 309 |
+
# Check alignment quality
|
| 310 |
+
last_end = 0
|
| 311 |
+
sample_has_tokens = True
|
| 312 |
+
sample_token_count = 0
|
| 313 |
+
|
| 314 |
+
for j, alignment in enumerate(wa):
|
| 315 |
+
# Check required fields
|
| 316 |
+
if 'word' not in alignment or 'start_frame' not in alignment or 'end_frame' not in alignment:
|
| 317 |
+
continue
|
| 318 |
+
|
| 319 |
+
start = alignment['start_frame']
|
| 320 |
+
end = alignment['end_frame']
|
| 321 |
+
|
| 322 |
+
# Check timing consistency
|
| 323 |
+
if start < last_end - 1: # Allow small overlap
|
| 324 |
+
timing_issues += 1
|
| 325 |
+
last_end = end
|
| 326 |
+
|
| 327 |
+
# Check token presence
|
| 328 |
+
if 'tokens' in alignment and alignment['tokens']:
|
| 329 |
+
sample_token_count += len(alignment['tokens'])
|
| 330 |
+
else:
|
| 331 |
+
sample_has_tokens = False
|
| 332 |
+
|
| 333 |
+
if sample_has_tokens and sample_token_count > 0:
|
| 334 |
+
has_tokens += 1
|
| 335 |
+
token_counts.append(sample_token_count)
|
| 336 |
+
|
| 337 |
+
# Calculate frame coverage
|
| 338 |
+
if "snac_tokens" in sample:
|
| 339 |
+
snac_len = len(sample["snac_tokens"])
|
| 340 |
+
total_frames = snac_len // SNAC_LAYERS
|
| 341 |
+
if total_frames > 0 and wa:
|
| 342 |
+
covered_frames = wa[-1]['end_frame'] if wa[-1]['end_frame'] else 0
|
| 343 |
+
frame_coverages.append(min(100, 100 * covered_frames / total_frames))
|
| 344 |
+
|
| 345 |
+
print(f"\n Samples with word_alignments: {has_alignments}/{len(dataset)} ({100*has_alignments/len(dataset):.1f}%)")
|
| 346 |
+
|
| 347 |
+
if not has_alignments:
|
| 348 |
+
print(" No word alignments found!")
|
| 349 |
+
return
|
| 350 |
+
|
| 351 |
+
print(f" Samples with pre-computed tokens: {has_tokens}/{has_alignments} ({100*has_tokens/has_alignments:.1f}%)")
|
| 352 |
+
|
| 353 |
+
word_counts = np.array(word_counts)
|
| 354 |
+
print_subheader("Word Count per Sample")
|
| 355 |
+
print(f" Min: {word_counts.min()}, Max: {word_counts.max()}, Mean: {word_counts.mean():.1f}")
|
| 356 |
+
|
| 357 |
+
if token_counts:
|
| 358 |
+
token_counts = np.array(token_counts)
|
| 359 |
+
print_subheader("Token Count per Sample (from alignments)")
|
| 360 |
+
print(f" Min: {token_counts.min()}, Max: {token_counts.max()}, Mean: {token_counts.mean():.1f}")
|
| 361 |
+
|
| 362 |
+
if frame_coverages:
|
| 363 |
+
fc = np.array(frame_coverages)
|
| 364 |
+
print_subheader("Frame Coverage")
|
| 365 |
+
print(f" Min: {fc.min():.1f}%, Max: {fc.max():.1f}%, Mean: {fc.mean():.1f}%")
|
| 366 |
+
|
| 367 |
+
print_subheader("Quality Checks")
|
| 368 |
+
print(f" Timing issues (overlaps): {timing_issues} {'⚠️' if timing_issues > 0 else '✅'}")
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
def analyze_cross_field_consistency(dataset):
|
| 372 |
+
"""Check consistency between related fields."""
|
| 373 |
+
print_header("CROSS-FIELD CONSISTENCY")
|
| 374 |
+
|
| 375 |
+
issues = defaultdict(int)
|
| 376 |
+
|
| 377 |
+
for i, sample in enumerate(dataset):
|
| 378 |
+
# Check answer vs text_tokens consistency
|
| 379 |
+
if "answer" in sample and "text_tokens" in sample:
|
| 380 |
+
answer = sample["answer"]
|
| 381 |
+
text_tokens = sample["text_tokens"]
|
| 382 |
+
if isinstance(text_tokens, torch.Tensor):
|
| 383 |
+
token_len = len(text_tokens)
|
| 384 |
+
else:
|
| 385 |
+
token_len = len(text_tokens) if text_tokens else 0
|
| 386 |
+
|
| 387 |
+
# Rough check: tokens should be roughly proportional to text length
|
| 388 |
+
if answer and token_len > 0:
|
| 389 |
+
chars_per_token = len(answer) / token_len
|
| 390 |
+
if chars_per_token < 1 or chars_per_token > 10:
|
| 391 |
+
issues["answer_token_mismatch"] += 1
|
| 392 |
+
|
| 393 |
+
# Check word_alignments vs answer consistency
|
| 394 |
+
if "word_alignments" in sample and "answer" in sample:
|
| 395 |
+
wa = sample["word_alignments"]
|
| 396 |
+
answer = sample["answer"]
|
| 397 |
+
if wa and answer:
|
| 398 |
+
wa_words = len(wa)
|
| 399 |
+
answer_words = len(answer.split())
|
| 400 |
+
if abs(wa_words - answer_words) > answer_words * 0.3: # 30% tolerance
|
| 401 |
+
issues["alignment_word_mismatch"] += 1
|
| 402 |
+
|
| 403 |
+
# Check snac_tokens frame count vs alignments
|
| 404 |
+
if "snac_tokens" in sample and "word_alignments" in sample:
|
| 405 |
+
snac = sample["snac_tokens"]
|
| 406 |
+
wa = sample["word_alignments"]
|
| 407 |
+
if isinstance(snac, torch.Tensor) and wa:
|
| 408 |
+
snac_frames = len(snac) // SNAC_LAYERS
|
| 409 |
+
last_frame = max((a.get('end_frame', 0) for a in wa), default=0)
|
| 410 |
+
if last_frame > snac_frames * 1.1: # 10% tolerance
|
| 411 |
+
issues["frame_overflow"] += 1
|
| 412 |
+
|
| 413 |
+
print(f"\n Consistency checks:")
|
| 414 |
+
print(f" Answer/token length mismatch: {issues['answer_token_mismatch']} {'⚠️' if issues['answer_token_mismatch'] > 0 else '✅'}")
|
| 415 |
+
print(f" Alignment/answer word mismatch: {issues['alignment_word_mismatch']} {'⚠️' if issues['alignment_word_mismatch'] > 0 else '✅'}")
|
| 416 |
+
print(f" Frame overflow in alignments: {issues['frame_overflow']} {'⚠️' if issues['frame_overflow'] > 0 else '✅'}")
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
def analyze_training_readiness(dataset):
|
| 420 |
+
"""Check if dataset is ready for training."""
|
| 421 |
+
print_header("TRAINING READINESS CHECK")
|
| 422 |
+
|
| 423 |
+
required_fields = ["whisper_features", "snac_tokens"]
|
| 424 |
+
optional_fields = ["text", "answer", "text_tokens", "word_alignments"]
|
| 425 |
+
|
| 426 |
+
field_presence = defaultdict(int)
|
| 427 |
+
complete_samples = 0
|
| 428 |
+
|
| 429 |
+
issues = []
|
| 430 |
+
|
| 431 |
+
for sample in dataset:
|
| 432 |
+
has_required = all(f in sample for f in required_fields)
|
| 433 |
+
if has_required:
|
| 434 |
+
complete_samples += 1
|
| 435 |
+
|
| 436 |
+
for field in required_fields + optional_fields:
|
| 437 |
+
if field in sample and sample[field] is not None:
|
| 438 |
+
if isinstance(sample[field], (torch.Tensor, list, str)):
|
| 439 |
+
if len(sample[field]) > 0:
|
| 440 |
+
field_presence[field] += 1
|
| 441 |
+
else:
|
| 442 |
+
field_presence[field] += 1
|
| 443 |
+
|
| 444 |
+
print(f"\n Total samples: {len(dataset)}")
|
| 445 |
+
print(f" Complete samples (with required fields): {complete_samples} ({100*complete_samples/len(dataset):.1f}%)")
|
| 446 |
+
|
| 447 |
+
print(f"\n Field presence:")
|
| 448 |
+
for field in required_fields:
|
| 449 |
+
count = field_presence[field]
|
| 450 |
+
status = "✅" if count == len(dataset) else "❌"
|
| 451 |
+
print(f" {field}: {count}/{len(dataset)} {status} (required)")
|
| 452 |
+
|
| 453 |
+
for field in optional_fields:
|
| 454 |
+
count = field_presence[field]
|
| 455 |
+
status = "✅" if count == len(dataset) else "⚠️"
|
| 456 |
+
print(f" {field}: {count}/{len(dataset)} {status} (optional)")
|
| 457 |
+
|
| 458 |
+
# Overall assessment
|
| 459 |
+
print_subheader("Overall Assessment")
|
| 460 |
+
|
| 461 |
+
ready = True
|
| 462 |
+
|
| 463 |
+
if complete_samples < len(dataset):
|
| 464 |
+
print(f" ❌ {len(dataset) - complete_samples} samples missing required fields")
|
| 465 |
+
ready = False
|
| 466 |
+
else:
|
| 467 |
+
print(f" ✅ All samples have required fields")
|
| 468 |
+
|
| 469 |
+
if field_presence.get("text_tokens", 0) == len(dataset):
|
| 470 |
+
print(f" ✅ Pre-tokenized text available (faster training)")
|
| 471 |
+
else:
|
| 472 |
+
print(f" ⚠️ text_tokens missing in some samples (will tokenize on-the-fly)")
|
| 473 |
+
|
| 474 |
+
if field_presence.get("word_alignments", 0) == len(dataset):
|
| 475 |
+
print(f" ✅ Word alignments available (semantic interleaving)")
|
| 476 |
+
else:
|
| 477 |
+
print(f" ⚠️ word_alignments missing (will use positional interleaving)")
|
| 478 |
+
|
| 479 |
+
if ready:
|
| 480 |
+
print(f"\n 🎉 DATASET READY FOR TRAINING!")
|
| 481 |
+
else:
|
| 482 |
+
print(f"\n ⚠️ Dataset has issues that need to be fixed")
|
| 483 |
+
|
| 484 |
+
return ready
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
def print_sample_inspection(dataset, num_samples=3):
|
| 488 |
+
"""Print detailed inspection of sample items."""
|
| 489 |
+
print_header(f"SAMPLE INSPECTION (first {num_samples} samples)")
|
| 490 |
+
|
| 491 |
+
for i in range(min(num_samples, len(dataset))):
|
| 492 |
+
sample = dataset[i]
|
| 493 |
+
print(f"\n Sample {i}:")
|
| 494 |
+
|
| 495 |
+
for key, value in sample.items():
|
| 496 |
+
if isinstance(value, torch.Tensor):
|
| 497 |
+
print(f" {key}: Tensor {list(value.shape)} {value.dtype}")
|
| 498 |
+
if key == "snac_tokens" and len(value) > 0:
|
| 499 |
+
print(f" First 7 tokens: {value[:7].tolist()}")
|
| 500 |
+
print(f" Last 7 tokens: {value[-7:].tolist()}")
|
| 501 |
+
elif isinstance(value, str):
|
| 502 |
+
preview = value[:60] + "..." if len(value) > 60 else value
|
| 503 |
+
print(f" {key}: '{preview}'")
|
| 504 |
+
elif isinstance(value, list):
|
| 505 |
+
print(f" {key}: list[{len(value)}]")
|
| 506 |
+
if key == "word_alignments" and len(value) > 0:
|
| 507 |
+
print(f" First: {value[0]}")
|
| 508 |
+
if len(value) > 1:
|
| 509 |
+
print(f" Last: {value[-1]}")
|
| 510 |
+
else:
|
| 511 |
+
print(f" {key}: {type(value).__name__}")
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
def main():
|
| 515 |
+
parser = argparse.ArgumentParser(description="Deep dataset analysis")
|
| 516 |
+
parser.add_argument("--path", type=str, required=True, help="Path to dataset .pt file")
|
| 517 |
+
parser.add_argument("--max-samples", type=int, default=None, help="Max samples to analyze")
|
| 518 |
+
parser.add_argument("--quick", action="store_true", help="Quick analysis (skip detailed checks)")
|
| 519 |
+
args = parser.parse_args()
|
| 520 |
+
|
| 521 |
+
print("\n" + "="*70)
|
| 522 |
+
print(" DEEP DATASET ANALYZER")
|
| 523 |
+
print("="*70)
|
| 524 |
+
print(f"\n File: {args.path}")
|
| 525 |
+
|
| 526 |
+
# Check file exists
|
| 527 |
+
if not Path(args.path).exists():
|
| 528 |
+
print(f"\n ❌ File not found: {args.path}")
|
| 529 |
+
sys.exit(1)
|
| 530 |
+
|
| 531 |
+
# Load dataset
|
| 532 |
+
print(f" Loading dataset...")
|
| 533 |
+
try:
|
| 534 |
+
dataset = torch.load(args.path, map_location="cpu", weights_only=False)
|
| 535 |
+
except Exception as e:
|
| 536 |
+
print(f"\n ❌ Failed to load: {e}")
|
| 537 |
+
sys.exit(1)
|
| 538 |
+
|
| 539 |
+
if not isinstance(dataset, list):
|
| 540 |
+
print(f"\n ❌ Dataset should be list, got {type(dataset).__name__}")
|
| 541 |
+
sys.exit(1)
|
| 542 |
+
|
| 543 |
+
print(f" Loaded {len(dataset):,} samples")
|
| 544 |
+
|
| 545 |
+
# Limit samples if requested
|
| 546 |
+
if args.max_samples and args.max_samples < len(dataset):
|
| 547 |
+
print(f" Analyzing first {args.max_samples} samples")
|
| 548 |
+
dataset = dataset[:args.max_samples]
|
| 549 |
+
|
| 550 |
+
# File size
|
| 551 |
+
file_size = Path(args.path).stat().st_size / (1024**3)
|
| 552 |
+
print(f" File size: {file_size:.2f} GB")
|
| 553 |
+
print(f" Avg per sample: {file_size*1024/len(dataset):.2f} MB")
|
| 554 |
+
|
| 555 |
+
# Run analyses
|
| 556 |
+
analyze_whisper_features(dataset)
|
| 557 |
+
analyze_snac_tokens(dataset)
|
| 558 |
+
analyze_text_content(dataset)
|
| 559 |
+
|
| 560 |
+
if not args.quick:
|
| 561 |
+
analyze_word_alignments(dataset)
|
| 562 |
+
analyze_cross_field_consistency(dataset)
|
| 563 |
+
|
| 564 |
+
ready = analyze_training_readiness(dataset)
|
| 565 |
+
print_sample_inspection(dataset)
|
| 566 |
+
|
| 567 |
+
print("\n" + "="*70)
|
| 568 |
+
print(" ANALYSIS COMPLETE")
|
| 569 |
+
print("="*70 + "\n")
|
| 570 |
+
|
| 571 |
+
sys.exit(0 if ready else 1)
|
| 572 |
+
|
| 573 |
+
|
| 574 |
+
if __name__ == "__main__":
|
| 575 |
+
main()
|
datasets/create_dataset.py
CHANGED
|
@@ -8,6 +8,7 @@ import sys
|
|
| 8 |
import re
|
| 9 |
import time
|
| 10 |
import gc
|
|
|
|
| 11 |
import multiprocessing as mp
|
| 12 |
from pathlib import Path
|
| 13 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
@@ -15,6 +16,43 @@ import numpy as np
|
|
| 15 |
import requests
|
| 16 |
import torch
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
def load_dotenv(env_path=None):
|
| 20 |
"""Load environment variables from .env file."""
|
|
@@ -31,7 +69,7 @@ def load_dotenv(env_path=None):
|
|
| 31 |
if line and not line.startswith("#") and "=" in line:
|
| 32 |
key, value = line.split("=", 1)
|
| 33 |
os.environ.setdefault(key.strip(), value.strip())
|
| 34 |
-
|
| 35 |
|
| 36 |
|
| 37 |
# Load .env file
|
|
@@ -40,7 +78,7 @@ load_dotenv()
|
|
| 40 |
# Configuration
|
| 41 |
GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
|
| 42 |
GROQ_MODEL = "openai/gpt-oss-20b"
|
| 43 |
-
GROQ_PARALLEL_REQUESTS =
|
| 44 |
QA_PER_REQUEST = 100
|
| 45 |
|
| 46 |
|
|
@@ -64,7 +102,7 @@ MEMORY_PER_ITEM = {
|
|
| 64 |
# Default batch sizes (will be auto-adjusted based on VRAM)
|
| 65 |
DEFAULT_BATCH_SIZES = {
|
| 66 |
"tts": 200, # TTS batch (processes questions + answers)
|
| 67 |
-
"whisper": 8, # Whisper parallel
|
| 68 |
"snac": 50, # SNAC encoding batch
|
| 69 |
}
|
| 70 |
|
|
@@ -84,13 +122,13 @@ def calculate_batch_sizes(vram_gb: float = None, shared_gpu: bool = True) -> dic
|
|
| 84 |
so batch sizes must be more conservative to avoid OOM.
|
| 85 |
|
| 86 |
VRAM tiers (with shared GPU adjustment):
|
| 87 |
-
- 80GB+ (H100/H200):
|
| 88 |
-
- 40-80GB (A100/H100-64GB):
|
| 89 |
-
- 24-40GB (RTX 4090):
|
| 90 |
-
- 16-24GB (RTX 3090/4080):
|
| 91 |
- <16GB: Minimum safe values
|
| 92 |
|
| 93 |
-
Returns dict with: tts, whisper, snac batch sizes
|
| 94 |
"""
|
| 95 |
if vram_gb is None:
|
| 96 |
vram_gb = get_gpu_vram_gb()
|
|
@@ -98,29 +136,35 @@ def calculate_batch_sizes(vram_gb: float = None, shared_gpu: bool = True) -> dic
|
|
| 98 |
# Determine scale factor based on VRAM
|
| 99 |
if vram_gb >= 80:
|
| 100 |
tts_scale = 1.0
|
| 101 |
-
|
|
|
|
| 102 |
elif vram_gb >= 40:
|
| 103 |
tts_scale = 0.75
|
| 104 |
-
|
|
|
|
| 105 |
elif vram_gb >= 24:
|
| 106 |
tts_scale = 0.5
|
| 107 |
-
|
|
|
|
| 108 |
elif vram_gb >= 16:
|
| 109 |
tts_scale = 0.33
|
| 110 |
-
|
|
|
|
| 111 |
else:
|
| 112 |
tts_scale = 0.2
|
|
|
|
| 113 |
snac_scale = 0.15
|
| 114 |
|
| 115 |
-
# If not sharing GPU,
|
| 116 |
if not shared_gpu:
|
| 117 |
-
snac_scale = min(1.0, snac_scale *
|
|
|
|
| 118 |
|
| 119 |
# Calculate batch sizes
|
| 120 |
batch_sizes = {
|
| 121 |
"tts": max(10, int(DEFAULT_BATCH_SIZES["tts"] * tts_scale)),
|
| 122 |
-
"whisper":
|
| 123 |
-
"snac": max(
|
| 124 |
}
|
| 125 |
|
| 126 |
return batch_sizes
|
|
@@ -128,8 +172,8 @@ def calculate_batch_sizes(vram_gb: float = None, shared_gpu: bool = True) -> dic
|
|
| 128 |
|
| 129 |
def print_batch_config(batch_sizes: dict, vram_gb: float):
|
| 130 |
"""Print batch configuration for transparency."""
|
| 131 |
-
|
| 132 |
-
|
| 133 |
|
| 134 |
|
| 135 |
def parse_qa(content):
|
|
@@ -217,14 +261,14 @@ def qa_producer(target_count: int, tts_queue: mp.Queue, batch_size: int, status_
|
|
| 217 |
status_queue.put(("qa_done", len(pairs), batch_idx))
|
| 218 |
|
| 219 |
|
| 220 |
-
def tts_worker(gpu_id: int, tts_queue: mp.Queue, feat_queue: mp.Queue, status_queue: mp.Queue, batch_sizes: dict, num_gpus: int = 1):
|
| 221 |
"""TTS worker - processes batches with VRAM-adjusted batch sizes on specific GPU."""
|
| 222 |
import torch
|
| 223 |
_orig_load = torch.load
|
| 224 |
torch.load = lambda *a, **kw: _orig_load(*a, **{**kw, 'weights_only': False})
|
| 225 |
|
| 226 |
-
# Set specific GPU for this worker (distribute across available GPUs)
|
| 227 |
-
actual_gpu = gpu_id % num_gpus
|
| 228 |
torch.cuda.set_device(actual_gpu)
|
| 229 |
print(f"[TTS-GPU{gpu_id}] Assigned to CUDA device {actual_gpu}")
|
| 230 |
|
|
@@ -240,19 +284,30 @@ def tts_worker(gpu_id: int, tts_queue: mp.Queue, feat_queue: mp.Queue, status_qu
|
|
| 240 |
from soprano import SopranoTTS
|
| 241 |
# Use lmdeploy backend for 2000x real-time speed (much faster than transformers)
|
| 242 |
# Scale decoder_batch_size based on VRAM
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
# Use centralized batch size
|
| 258 |
tts_batch = batch_sizes.get("tts", 200)
|
|
@@ -262,8 +317,20 @@ def tts_worker(gpu_id: int, tts_queue: mp.Queue, feat_queue: mp.Queue, status_qu
|
|
| 262 |
|
| 263 |
processed = 0
|
| 264 |
t_start = time.time()
|
|
|
|
|
|
|
| 265 |
while True:
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
if item is None:
|
| 268 |
break
|
| 269 |
|
|
@@ -291,7 +358,10 @@ def tts_worker(gpu_id: int, tts_queue: mp.Queue, feat_queue: mp.Queue, status_qu
|
|
| 291 |
})
|
| 292 |
|
| 293 |
except Exception as e:
|
| 294 |
-
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
if all_results:
|
| 297 |
feat_queue.put((batch_idx, all_results))
|
|
@@ -304,7 +374,7 @@ def tts_worker(gpu_id: int, tts_queue: mp.Queue, feat_queue: mp.Queue, status_qu
|
|
| 304 |
status_queue.put(("tts_done", gpu_id, processed))
|
| 305 |
|
| 306 |
|
| 307 |
-
def features_worker(gpu_id: int, feat_queue: mp.Queue, result_queue: mp.Queue, status_queue: mp.Queue, batch_sizes: dict, num_gpus: int = 1):
|
| 308 |
"""Features worker - VRAM-adjusted batch sizes for Whisper + SNAC + NeMo NFA on specific GPU."""
|
| 309 |
import torch
|
| 310 |
_orig = torch.load
|
|
@@ -317,8 +387,8 @@ def features_worker(gpu_id: int, feat_queue: mp.Queue, result_queue: mp.Queue, s
|
|
| 317 |
from transformers import AutoTokenizer, WhisperModel, WhisperFeatureExtractor
|
| 318 |
from huggingface_hub import login
|
| 319 |
|
| 320 |
-
# Set specific GPU for this worker (distribute across available GPUs)
|
| 321 |
-
actual_gpu = gpu_id % num_gpus
|
| 322 |
torch.cuda.set_device(actual_gpu)
|
| 323 |
device = f"cuda:{actual_gpu}"
|
| 324 |
print(f"[Features-GPU{gpu_id}] Assigned to CUDA device {actual_gpu}")
|
|
@@ -335,6 +405,9 @@ def features_worker(gpu_id: int, feat_queue: mp.Queue, result_queue: mp.Queue, s
|
|
| 335 |
print("[Features] Loading Whisper large-v3-turbo (transformers)...")
|
| 336 |
whisper_model = WhisperModel.from_pretrained("openai/whisper-large-v3-turbo", torch_dtype=torch.float16).to(device).eval()
|
| 337 |
whisper_feature_extractor = WhisperFeatureExtractor.from_pretrained("openai/whisper-large-v3-turbo")
|
|
|
|
|
|
|
|
|
|
| 338 |
print("[Features] Whisper Turbo loaded successfully")
|
| 339 |
|
| 340 |
snac_model = snac.SNAC.from_pretrained("hubertsiuzdak/snac_24khz").to(device).eval()
|
|
@@ -371,20 +444,26 @@ def features_worker(gpu_id: int, feat_queue: mp.Queue, result_queue: mp.Queue, s
|
|
| 371 |
status_queue.put(("feat_ready", gpu_id))
|
| 372 |
|
| 373 |
def process_whisper(audio_data):
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
|
|
|
| 378 |
# Truncate to max 30 seconds (480000 samples at 16kHz) - Whisper limit
|
| 379 |
max_samples = 480000
|
| 380 |
-
if
|
| 381 |
audio_16k = audio_16k[:max_samples]
|
|
|
|
|
|
|
|
|
|
| 382 |
# Extract features using Whisper feature extractor
|
| 383 |
-
inputs = whisper_feature_extractor(
|
| 384 |
input_features = inputs.input_features.to(device, dtype=torch.float16)
|
|
|
|
| 385 |
# Encode with Whisper
|
| 386 |
with torch.no_grad():
|
| 387 |
encoder_outputs = whisper_model.encoder(input_features)
|
|
|
|
| 388 |
# Return encoder hidden states [seq_len, 1280]
|
| 389 |
return encoder_outputs.last_hidden_state.squeeze(0).cpu().float()
|
| 390 |
|
|
@@ -493,28 +572,55 @@ def features_worker(gpu_id: int, feat_queue: mp.Queue, result_queue: mp.Queue, s
|
|
| 493 |
return None
|
| 494 |
|
| 495 |
def get_word_alignments_proportional(audio_data, text, sample_rate=32000):
|
| 496 |
-
"""Fallback: proportional word alignment based on character count.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
words = text.split()
|
| 498 |
if not words:
|
| 499 |
return []
|
| 500 |
|
| 501 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
total_chars = sum(len(w) for w in words)
|
| 503 |
if total_chars == 0:
|
| 504 |
return []
|
| 505 |
|
|
|
|
| 506 |
word_alignments = []
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
|
| 519 |
word_alignments.append({
|
| 520 |
'word': word,
|
|
@@ -522,17 +628,28 @@ def features_worker(gpu_id: int, feat_queue: mp.Queue, result_queue: mp.Queue, s
|
|
| 522 |
'end': end_time,
|
| 523 |
'start_frame': start_frame,
|
| 524 |
'end_frame': end_frame,
|
| 525 |
-
'tokens':
|
| 526 |
})
|
| 527 |
-
|
| 528 |
|
| 529 |
return word_alignments
|
| 530 |
|
| 531 |
processed = 0
|
| 532 |
t_start = time.time()
|
|
|
|
| 533 |
|
| 534 |
while True:
|
| 535 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 536 |
if item is None:
|
| 537 |
break
|
| 538 |
|
|
@@ -540,12 +657,12 @@ def features_worker(gpu_id: int, feat_queue: mp.Queue, result_queue: mp.Queue, s
|
|
| 540 |
t0 = time.time()
|
| 541 |
|
| 542 |
try:
|
| 543 |
-
# 1. Parallel Whisper encoding
|
| 544 |
q_audios = [ad["q_audio"] for ad in audio_batch]
|
| 545 |
with ThreadPoolExecutor(max_workers=whisper_workers) as ex:
|
| 546 |
whisper_features = list(ex.map(process_whisper, q_audios))
|
| 547 |
|
| 548 |
-
# 2. SNAC encoding -
|
| 549 |
a_audios = [ad["a_audio"] for ad in audio_batch]
|
| 550 |
all_tokens = []
|
| 551 |
|
|
@@ -555,8 +672,12 @@ def features_worker(gpu_id: int, feat_queue: mp.Queue, result_queue: mp.Queue, s
|
|
| 555 |
|
| 556 |
max_len = max(a.shape[0] for a in mini_audios)
|
| 557 |
padded = [np.pad(a, (0, max_len - len(a))) for a in mini_audios]
|
|
|
|
|
|
|
| 558 |
audios_24k = torch.stack([
|
| 559 |
-
torchaudio.functional.resample(
|
|
|
|
|
|
|
| 560 |
for a in padded
|
| 561 |
])
|
| 562 |
|
|
@@ -568,6 +689,11 @@ def features_worker(gpu_id: int, feat_queue: mp.Queue, result_queue: mp.Queue, s
|
|
| 568 |
|
| 569 |
torch.cuda.synchronize()
|
| 570 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 571 |
# 3. Build results with pre-computed text tokens and word alignments
|
| 572 |
results = []
|
| 573 |
for i, ad in enumerate(audio_batch):
|
|
@@ -596,15 +722,48 @@ def features_worker(gpu_id: int, feat_queue: mp.Queue, result_queue: mp.Queue, s
|
|
| 596 |
batch_time = time.time() - t0
|
| 597 |
batch_rate = len(results) / batch_time if batch_time > 0 else 0
|
| 598 |
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
processed += len(results)
|
| 603 |
elapsed = time.time() - t_start
|
| 604 |
status_queue.put(("feat", gpu_id, processed, processed/elapsed, batch_rate))
|
| 605 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
except Exception as e:
|
| 607 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 608 |
|
| 609 |
status_queue.put(("feat_done", gpu_id, processed))
|
| 610 |
|
|
@@ -619,9 +778,12 @@ def main():
|
|
| 619 |
|
| 620 |
import argparse
|
| 621 |
parser = argparse.ArgumentParser()
|
| 622 |
-
parser.add_argument("--count", type=int, default=100)
|
| 623 |
parser.add_argument("--output", type=str, default="./data/dataset.pt")
|
| 624 |
parser.add_argument("--gpus", type=int, default=NUM_GPUS)
|
|
|
|
|
|
|
|
|
|
| 625 |
# Optional overrides for batch sizes (if not set, auto-calculated from VRAM)
|
| 626 |
parser.add_argument("--tts-batch", type=int, default=None)
|
| 627 |
parser.add_argument("--snac-batch", type=int, default=None)
|
|
@@ -630,6 +792,11 @@ def main():
|
|
| 630 |
|
| 631 |
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
|
| 632 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 633 |
# Calculate batch sizes based on GPU VRAM
|
| 634 |
vram_gb = get_gpu_vram_gb()
|
| 635 |
batch_sizes = calculate_batch_sizes(vram_gb)
|
|
@@ -642,42 +809,115 @@ def main():
|
|
| 642 |
if args.whisper_workers is not None:
|
| 643 |
batch_sizes["whisper"] = args.whisper_workers
|
| 644 |
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
print_batch_config(batch_sizes, vram_gb)
|
| 649 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 650 |
|
| 651 |
total_start = time.time()
|
| 652 |
|
| 653 |
-
# Use
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
|
|
|
| 658 |
status_queue = mp.Queue()
|
| 659 |
|
| 660 |
workers = []
|
| 661 |
|
| 662 |
# Get actual GPU count for worker assignment
|
| 663 |
actual_num_gpus = get_num_gpus() if torch.cuda.is_available() else 1
|
| 664 |
-
print(f"\n[Main] Detected {actual_num_gpus} GPUs, spawning {args.gpus} workers per stage")
|
| 665 |
|
| 666 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 667 |
qa_proc.start()
|
| 668 |
workers.append(qa_proc)
|
| 669 |
|
| 670 |
-
for gpu_id in range(
|
| 671 |
-
p = mp.Process(target=tts_worker, args=(gpu_id, tts_queue, feat_queue, status_queue, batch_sizes,
|
| 672 |
p.start()
|
| 673 |
workers.append(p)
|
| 674 |
|
| 675 |
-
for gpu_id in range(
|
| 676 |
-
p = mp.Process(target=features_worker, args=(gpu_id, feat_queue, result_queue, status_queue, batch_sizes,
|
| 677 |
p.start()
|
| 678 |
workers.append(p)
|
| 679 |
|
| 680 |
-
|
| 681 |
|
| 682 |
results = {}
|
| 683 |
tts_done_count = 0
|
|
@@ -686,124 +926,311 @@ def main():
|
|
| 686 |
t0 = time.time()
|
| 687 |
tts_ready = 0
|
| 688 |
feat_ready = 0
|
| 689 |
-
expected_from_feat = {}
|
| 690 |
-
feat_queue_closed = False
|
|
|
|
| 691 |
|
| 692 |
-
# Main loop
|
| 693 |
last_result_time = time.time()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
while True:
|
| 695 |
-
# Process status messages
|
| 696 |
-
|
| 697 |
-
while True:
|
| 698 |
try:
|
| 699 |
msg = status_queue.get_nowait()
|
| 700 |
-
|
| 701 |
msg_type = msg[0]
|
| 702 |
|
| 703 |
if msg_type == "tts_ready":
|
| 704 |
tts_ready += 1
|
| 705 |
-
|
| 706 |
elif msg_type == "feat_ready":
|
| 707 |
feat_ready += 1
|
| 708 |
-
|
| 709 |
elif msg_type == "qa":
|
| 710 |
-
|
| 711 |
elif msg_type == "qa_done":
|
| 712 |
-
|
| 713 |
elif msg_type == "tts":
|
| 714 |
-
|
|
|
|
| 715 |
elif msg_type == "tts_done":
|
| 716 |
tts_done_count += 1
|
| 717 |
-
|
| 718 |
elif msg_type == "feat":
|
| 719 |
-
|
|
|
|
| 720 |
elif msg_type == "feat_done":
|
| 721 |
feat_done_count += 1
|
| 722 |
expected_from_feat[msg[1]] = msg[2]
|
| 723 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 724 |
elif "error" in msg_type:
|
| 725 |
-
|
|
|
|
| 726 |
except:
|
| 727 |
break
|
| 728 |
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
if tts_done_count >= args.gpus and not feat_queue_closed:
|
| 732 |
-
print(f"[Main] TTS done, waiting for features to finish...")
|
| 733 |
feat_queue_closed = True
|
| 734 |
|
| 735 |
-
#
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 741 |
elapsed = time.time() - t0
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 745 |
|
| 746 |
# Check exit conditions
|
| 747 |
-
if total_items >=
|
| 748 |
-
|
| 749 |
break
|
| 750 |
|
| 751 |
-
# If all workers done,
|
| 752 |
-
if feat_done_count >=
|
| 753 |
-
|
| 754 |
-
print(f"[Main] All workers done, draining queue (timeout={drain_timeout}s)...")
|
| 755 |
drain_start = time.time()
|
| 756 |
-
while time.time() - drain_start <
|
| 757 |
try:
|
| 758 |
batch_idx, items = result_queue.get(timeout=0.5)
|
| 759 |
results[batch_idx] = items
|
| 760 |
total_items += len(items)
|
| 761 |
-
|
| 762 |
except:
|
| 763 |
-
|
| 764 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 765 |
break
|
| 766 |
break
|
| 767 |
|
| 768 |
-
#
|
| 769 |
-
|
| 770 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 771 |
break
|
| 772 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 773 |
# Wait for workers to finish
|
| 774 |
-
|
| 775 |
for p in workers:
|
| 776 |
p.join(timeout=5)
|
| 777 |
if p.is_alive():
|
| 778 |
p.terminate()
|
| 779 |
|
| 780 |
-
# Final drain
|
| 781 |
-
|
| 782 |
try:
|
| 783 |
batch_idx, items = result_queue.get_nowait()
|
| 784 |
results[batch_idx] = items
|
| 785 |
total_items += len(items)
|
| 786 |
-
print(f"[Results] {total_items}/{args.count} | (final)")
|
| 787 |
except:
|
| 788 |
break
|
| 789 |
|
| 790 |
-
|
|
|
|
| 791 |
for i in sorted(results.keys()):
|
| 792 |
-
|
| 793 |
-
|
|
|
|
| 794 |
|
| 795 |
-
|
| 796 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 797 |
sys.exit(1)
|
| 798 |
|
| 799 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 800 |
|
| 801 |
total_time = time.time() - total_start
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
|
|
|
|
|
|
| 807 |
|
| 808 |
|
| 809 |
if __name__ == "__main__":
|
|
|
|
| 8 |
import re
|
| 9 |
import time
|
| 10 |
import gc
|
| 11 |
+
import logging
|
| 12 |
import multiprocessing as mp
|
| 13 |
from pathlib import Path
|
| 14 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
|
| 16 |
import requests
|
| 17 |
import torch
|
| 18 |
|
| 19 |
+
# Global logger
|
| 20 |
+
logger = None
|
| 21 |
+
|
| 22 |
+
def setup_logging(log_file=None):
|
| 23 |
+
"""Setup logging to both console and file."""
|
| 24 |
+
global logger
|
| 25 |
+
logger = logging.getLogger("dataset_generator")
|
| 26 |
+
logger.setLevel(logging.INFO)
|
| 27 |
+
logger.handlers.clear()
|
| 28 |
+
|
| 29 |
+
# Console handler
|
| 30 |
+
console = logging.StreamHandler(sys.stdout)
|
| 31 |
+
console.setLevel(logging.INFO)
|
| 32 |
+
console.setFormatter(logging.Formatter('%(message)s'))
|
| 33 |
+
logger.addHandler(console)
|
| 34 |
+
|
| 35 |
+
# File handler (if specified)
|
| 36 |
+
if log_file:
|
| 37 |
+
file_handler = logging.FileHandler(log_file, mode='a')
|
| 38 |
+
file_handler.setLevel(logging.INFO)
|
| 39 |
+
file_handler.setFormatter(logging.Formatter('%(asctime)s | %(message)s', datefmt='%H:%M:%S'))
|
| 40 |
+
logger.addHandler(file_handler)
|
| 41 |
+
|
| 42 |
+
return logger
|
| 43 |
+
|
| 44 |
+
def log(msg):
|
| 45 |
+
"""Log message to both console and file."""
|
| 46 |
+
global logger
|
| 47 |
+
if logger:
|
| 48 |
+
logger.info(msg)
|
| 49 |
+
# Force flush all handlers
|
| 50 |
+
for handler in logger.handlers:
|
| 51 |
+
handler.flush()
|
| 52 |
+
else:
|
| 53 |
+
print(msg)
|
| 54 |
+
sys.stdout.flush()
|
| 55 |
+
|
| 56 |
|
| 57 |
def load_dotenv(env_path=None):
|
| 58 |
"""Load environment variables from .env file."""
|
|
|
|
| 69 |
if line and not line.startswith("#") and "=" in line:
|
| 70 |
key, value = line.split("=", 1)
|
| 71 |
os.environ.setdefault(key.strip(), value.strip())
|
| 72 |
+
log(f"[ENV] Loaded from {env_path}")
|
| 73 |
|
| 74 |
|
| 75 |
# Load .env file
|
|
|
|
| 78 |
# Configuration
|
| 79 |
GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
|
| 80 |
GROQ_MODEL = "openai/gpt-oss-20b"
|
| 81 |
+
GROQ_PARALLEL_REQUESTS = 10
|
| 82 |
QA_PER_REQUEST = 100
|
| 83 |
|
| 84 |
|
|
|
|
| 102 |
# Default batch sizes (will be auto-adjusted based on VRAM)
|
| 103 |
DEFAULT_BATCH_SIZES = {
|
| 104 |
"tts": 200, # TTS batch (processes questions + answers)
|
| 105 |
+
"whisper": 8, # Whisper parallel workers (each uses GPU)
|
| 106 |
"snac": 50, # SNAC encoding batch
|
| 107 |
}
|
| 108 |
|
|
|
|
| 122 |
so batch sizes must be more conservative to avoid OOM.
|
| 123 |
|
| 124 |
VRAM tiers (with shared GPU adjustment):
|
| 125 |
+
- 80GB+ (H100/H200): Large batches
|
| 126 |
+
- 40-80GB (A100/H100-64GB): Medium-large batches
|
| 127 |
+
- 24-40GB (RTX 4090): Medium batches
|
| 128 |
+
- 16-24GB (RTX 3090/4080): Smaller batches
|
| 129 |
- <16GB: Minimum safe values
|
| 130 |
|
| 131 |
+
Returns dict with: tts, whisper (workers), snac batch sizes
|
| 132 |
"""
|
| 133 |
if vram_gb is None:
|
| 134 |
vram_gb = get_gpu_vram_gb()
|
|
|
|
| 136 |
# Determine scale factor based on VRAM
|
| 137 |
if vram_gb >= 80:
|
| 138 |
tts_scale = 1.0
|
| 139 |
+
whisper_workers = 8 # Parallel Whisper threads
|
| 140 |
+
snac_scale = 0.6
|
| 141 |
elif vram_gb >= 40:
|
| 142 |
tts_scale = 0.75
|
| 143 |
+
whisper_workers = 6
|
| 144 |
+
snac_scale = 0.5
|
| 145 |
elif vram_gb >= 24:
|
| 146 |
tts_scale = 0.5
|
| 147 |
+
whisper_workers = 4 # RTX 4090 - moderate parallelism
|
| 148 |
+
snac_scale = 0.4
|
| 149 |
elif vram_gb >= 16:
|
| 150 |
tts_scale = 0.33
|
| 151 |
+
whisper_workers = 2
|
| 152 |
+
snac_scale = 0.25
|
| 153 |
else:
|
| 154 |
tts_scale = 0.2
|
| 155 |
+
whisper_workers = 1
|
| 156 |
snac_scale = 0.15
|
| 157 |
|
| 158 |
+
# If not sharing GPU, can use more memory
|
| 159 |
if not shared_gpu:
|
| 160 |
+
snac_scale = min(1.0, snac_scale * 1.5)
|
| 161 |
+
whisper_workers = min(8, whisper_workers + 2)
|
| 162 |
|
| 163 |
# Calculate batch sizes
|
| 164 |
batch_sizes = {
|
| 165 |
"tts": max(10, int(DEFAULT_BATCH_SIZES["tts"] * tts_scale)),
|
| 166 |
+
"whisper": whisper_workers, # Thread count for parallel Whisper
|
| 167 |
+
"snac": max(8, int(DEFAULT_BATCH_SIZES["snac"] * snac_scale)),
|
| 168 |
}
|
| 169 |
|
| 170 |
return batch_sizes
|
|
|
|
| 172 |
|
| 173 |
def print_batch_config(batch_sizes: dict, vram_gb: float):
|
| 174 |
"""Print batch configuration for transparency."""
|
| 175 |
+
log(f"[Config] GPU VRAM: {vram_gb:.1f}GB")
|
| 176 |
+
log(f"[Config] Batch sizes: TTS={batch_sizes['tts']}, Whisper={batch_sizes['whisper']}, SNAC={batch_sizes['snac']}")
|
| 177 |
|
| 178 |
|
| 179 |
def parse_qa(content):
|
|
|
|
| 261 |
status_queue.put(("qa_done", len(pairs), batch_idx))
|
| 262 |
|
| 263 |
|
| 264 |
+
def tts_worker(gpu_id: int, tts_queue: mp.Queue, feat_queue: mp.Queue, status_queue: mp.Queue, batch_sizes: dict, num_gpus: int = 1, gpu_offset: int = 0):
|
| 265 |
"""TTS worker - processes batches with VRAM-adjusted batch sizes on specific GPU."""
|
| 266 |
import torch
|
| 267 |
_orig_load = torch.load
|
| 268 |
torch.load = lambda *a, **kw: _orig_load(*a, **{**kw, 'weights_only': False})
|
| 269 |
|
| 270 |
+
# Set specific GPU for this worker (distribute across available GPUs with offset)
|
| 271 |
+
actual_gpu = gpu_offset + (gpu_id % num_gpus)
|
| 272 |
torch.cuda.set_device(actual_gpu)
|
| 273 |
print(f"[TTS-GPU{gpu_id}] Assigned to CUDA device {actual_gpu}")
|
| 274 |
|
|
|
|
| 284 |
from soprano import SopranoTTS
|
| 285 |
# Use lmdeploy backend for 2000x real-time speed (much faster than transformers)
|
| 286 |
# Scale decoder_batch_size based on VRAM
|
| 287 |
+
# Note: Soprano TTS only accepts "cuda", not "cuda:N"
|
| 288 |
+
# torch.cuda.set_device() already selected the correct GPU above
|
| 289 |
+
vram_gb = torch.cuda.get_device_properties(actual_gpu).total_memory / 1024**3
|
| 290 |
+
gpu_name = torch.cuda.get_device_properties(actual_gpu).name
|
| 291 |
+
|
| 292 |
+
# Check if GPU supports lmdeploy (Blackwell/RTX 50xx not supported yet)
|
| 293 |
+
use_lmdeploy = "5090" not in gpu_name and "5080" not in gpu_name and "B100" not in gpu_name
|
| 294 |
+
|
| 295 |
+
if use_lmdeploy:
|
| 296 |
+
try:
|
| 297 |
+
dec_batch = 32 if vram_gb >= 80 else (16 if vram_gb >= 40 else 8)
|
| 298 |
+
tts = SopranoTTS(
|
| 299 |
+
backend="lmdeploy", # Fastest backend
|
| 300 |
+
device="cuda", # Uses current device set by torch.cuda.set_device()
|
| 301 |
+
cache_size_mb=4000 if vram_gb >= 24 else 2000, # More cache = faster
|
| 302 |
+
decoder_batch_size=dec_batch, # Parallel decoding based on VRAM
|
| 303 |
+
)
|
| 304 |
+
print(f"[TTS-GPU{gpu_id}] Using lmdeploy backend (decoder_batch={dec_batch})")
|
| 305 |
+
except Exception as e:
|
| 306 |
+
print(f"[TTS-GPU{gpu_id}] lmdeploy failed ({e}), falling back to transformers")
|
| 307 |
+
tts = SopranoTTS(backend="transformers", device="cuda")
|
| 308 |
+
else:
|
| 309 |
+
print(f"[TTS-GPU{gpu_id}] Blackwell GPU detected ({gpu_name}), using transformers backend")
|
| 310 |
+
tts = SopranoTTS(backend="transformers", device="cuda")
|
| 311 |
|
| 312 |
# Use centralized batch size
|
| 313 |
tts_batch = batch_sizes.get("tts", 200)
|
|
|
|
| 317 |
|
| 318 |
processed = 0
|
| 319 |
t_start = time.time()
|
| 320 |
+
last_heartbeat = time.time()
|
| 321 |
+
|
| 322 |
while True:
|
| 323 |
+
# Send heartbeat every 30 seconds to show worker is alive
|
| 324 |
+
if time.time() - last_heartbeat > 30:
|
| 325 |
+
status_queue.put(("tts_heartbeat", gpu_id, processed))
|
| 326 |
+
last_heartbeat = time.time()
|
| 327 |
+
|
| 328 |
+
# Use timeout to allow heartbeat even when queue is empty
|
| 329 |
+
try:
|
| 330 |
+
item = tts_queue.get(timeout=5)
|
| 331 |
+
except:
|
| 332 |
+
continue # Timeout, send heartbeat and retry
|
| 333 |
+
|
| 334 |
if item is None:
|
| 335 |
break
|
| 336 |
|
|
|
|
| 358 |
})
|
| 359 |
|
| 360 |
except Exception as e:
|
| 361 |
+
import traceback
|
| 362 |
+
status_queue.put(("tts_error", gpu_id, f"{str(e)}\n{traceback.format_exc()}"))
|
| 363 |
+
# Continue processing other batches instead of crashing
|
| 364 |
+
continue
|
| 365 |
|
| 366 |
if all_results:
|
| 367 |
feat_queue.put((batch_idx, all_results))
|
|
|
|
| 374 |
status_queue.put(("tts_done", gpu_id, processed))
|
| 375 |
|
| 376 |
|
| 377 |
+
def features_worker(gpu_id: int, feat_queue: mp.Queue, result_queue: mp.Queue, status_queue: mp.Queue, batch_sizes: dict, num_gpus: int = 1, gpu_offset: int = 0):
|
| 378 |
"""Features worker - VRAM-adjusted batch sizes for Whisper + SNAC + NeMo NFA on specific GPU."""
|
| 379 |
import torch
|
| 380 |
_orig = torch.load
|
|
|
|
| 387 |
from transformers import AutoTokenizer, WhisperModel, WhisperFeatureExtractor
|
| 388 |
from huggingface_hub import login
|
| 389 |
|
| 390 |
+
# Set specific GPU for this worker (distribute across available GPUs with offset)
|
| 391 |
+
actual_gpu = gpu_offset + (gpu_id % num_gpus)
|
| 392 |
torch.cuda.set_device(actual_gpu)
|
| 393 |
device = f"cuda:{actual_gpu}"
|
| 394 |
print(f"[Features-GPU{gpu_id}] Assigned to CUDA device {actual_gpu}")
|
|
|
|
| 405 |
print("[Features] Loading Whisper large-v3-turbo (transformers)...")
|
| 406 |
whisper_model = WhisperModel.from_pretrained("openai/whisper-large-v3-turbo", torch_dtype=torch.float16).to(device).eval()
|
| 407 |
whisper_feature_extractor = WhisperFeatureExtractor.from_pretrained("openai/whisper-large-v3-turbo")
|
| 408 |
+
|
| 409 |
+
# Note: torch.compile is skipped for Whisper as it causes issues with conv1d
|
| 410 |
+
# The batched processing already provides significant speedup
|
| 411 |
print("[Features] Whisper Turbo loaded successfully")
|
| 412 |
|
| 413 |
snac_model = snac.SNAC.from_pretrained("hubertsiuzdak/snac_24khz").to(device).eval()
|
|
|
|
| 444 |
status_queue.put(("feat_ready", gpu_id))
|
| 445 |
|
| 446 |
def process_whisper(audio_data):
|
| 447 |
+
"""Process single audio with Whisper. GPU-accelerated resampling."""
|
| 448 |
+
# GPU-accelerated resampling
|
| 449 |
+
audio_tensor = torch.from_numpy(audio_data).to(device)
|
| 450 |
+
audio_16k = torchaudio.functional.resample(audio_tensor, 32000, 16000)
|
| 451 |
+
|
| 452 |
# Truncate to max 30 seconds (480000 samples at 16kHz) - Whisper limit
|
| 453 |
max_samples = 480000
|
| 454 |
+
if audio_16k.shape[0] > max_samples:
|
| 455 |
audio_16k = audio_16k[:max_samples]
|
| 456 |
+
|
| 457 |
+
audio_16k_np = audio_16k.cpu().numpy().astype(np.float32)
|
| 458 |
+
|
| 459 |
# Extract features using Whisper feature extractor
|
| 460 |
+
inputs = whisper_feature_extractor(audio_16k_np, sampling_rate=16000, return_tensors="pt")
|
| 461 |
input_features = inputs.input_features.to(device, dtype=torch.float16)
|
| 462 |
+
|
| 463 |
# Encode with Whisper
|
| 464 |
with torch.no_grad():
|
| 465 |
encoder_outputs = whisper_model.encoder(input_features)
|
| 466 |
+
|
| 467 |
# Return encoder hidden states [seq_len, 1280]
|
| 468 |
return encoder_outputs.last_hidden_state.squeeze(0).cpu().float()
|
| 469 |
|
|
|
|
| 572 |
return None
|
| 573 |
|
| 574 |
def get_word_alignments_proportional(audio_data, text, sample_rate=32000):
|
| 575 |
+
"""Fallback: proportional word alignment based on character count.
|
| 576 |
+
|
| 577 |
+
Calculates frame indices that match actual SNAC output:
|
| 578 |
+
- SNAC operates at 24kHz with ~320 samples per frame (75 fps)
|
| 579 |
+
- Audio is resampled from sample_rate to 24kHz before SNAC
|
| 580 |
+
"""
|
| 581 |
words = text.split()
|
| 582 |
if not words:
|
| 583 |
return []
|
| 584 |
|
| 585 |
+
# Calculate actual SNAC frame count after resampling to 24kHz
|
| 586 |
+
# SNAC uses ~320 samples per frame at 24kHz
|
| 587 |
+
audio_24k_samples = len(audio_data) * 24000 / sample_rate
|
| 588 |
+
total_snac_frames = int(audio_24k_samples / 320)
|
| 589 |
+
|
| 590 |
+
if total_snac_frames == 0:
|
| 591 |
+
return []
|
| 592 |
+
|
| 593 |
total_chars = sum(len(w) for w in words)
|
| 594 |
if total_chars == 0:
|
| 595 |
return []
|
| 596 |
|
| 597 |
+
audio_duration = len(audio_data) / sample_rate
|
| 598 |
word_alignments = []
|
| 599 |
+
current_frame = 0
|
| 600 |
+
|
| 601 |
+
# Pre-tokenize all words in batch for efficiency
|
| 602 |
+
all_word_tokens = []
|
| 603 |
+
if tokenizer is not None:
|
| 604 |
+
for word in words:
|
| 605 |
+
all_word_tokens.append(tokenizer.encode(word, add_special_tokens=False))
|
| 606 |
+
else:
|
| 607 |
+
all_word_tokens = [[] for _ in words]
|
| 608 |
+
|
| 609 |
+
for i, word in enumerate(words):
|
| 610 |
+
# Distribute frames proportionally based on character count
|
| 611 |
+
word_frames = int((len(word) / total_chars) * total_snac_frames)
|
| 612 |
+
|
| 613 |
+
# Ensure last word gets remaining frames
|
| 614 |
+
if i == len(words) - 1:
|
| 615 |
+
end_frame = total_snac_frames
|
| 616 |
+
else:
|
| 617 |
+
end_frame = min(current_frame + max(1, word_frames), total_snac_frames)
|
| 618 |
+
|
| 619 |
+
start_frame = current_frame
|
| 620 |
+
|
| 621 |
+
# Calculate time from frames (for compatibility)
|
| 622 |
+
start_time = start_frame / 75.0
|
| 623 |
+
end_time = end_frame / 75.0
|
| 624 |
|
| 625 |
word_alignments.append({
|
| 626 |
'word': word,
|
|
|
|
| 628 |
'end': end_time,
|
| 629 |
'start_frame': start_frame,
|
| 630 |
'end_frame': end_frame,
|
| 631 |
+
'tokens': all_word_tokens[i]
|
| 632 |
})
|
| 633 |
+
current_frame = end_frame
|
| 634 |
|
| 635 |
return word_alignments
|
| 636 |
|
| 637 |
processed = 0
|
| 638 |
t_start = time.time()
|
| 639 |
+
last_heartbeat = time.time()
|
| 640 |
|
| 641 |
while True:
|
| 642 |
+
# Send heartbeat every 30 seconds to show worker is alive
|
| 643 |
+
if time.time() - last_heartbeat > 30:
|
| 644 |
+
status_queue.put(("feat_heartbeat", gpu_id, processed))
|
| 645 |
+
last_heartbeat = time.time()
|
| 646 |
+
|
| 647 |
+
# Use timeout to allow heartbeat even when queue is empty
|
| 648 |
+
try:
|
| 649 |
+
item = feat_queue.get(timeout=5)
|
| 650 |
+
except:
|
| 651 |
+
continue # Timeout, send heartbeat and retry
|
| 652 |
+
|
| 653 |
if item is None:
|
| 654 |
break
|
| 655 |
|
|
|
|
| 657 |
t0 = time.time()
|
| 658 |
|
| 659 |
try:
|
| 660 |
+
# 1. Parallel Whisper encoding with GPU-accelerated resampling
|
| 661 |
q_audios = [ad["q_audio"] for ad in audio_batch]
|
| 662 |
with ThreadPoolExecutor(max_workers=whisper_workers) as ex:
|
| 663 |
whisper_features = list(ex.map(process_whisper, q_audios))
|
| 664 |
|
| 665 |
+
# 2. SNAC encoding - GPU-batched with GPU resampling
|
| 666 |
a_audios = [ad["a_audio"] for ad in audio_batch]
|
| 667 |
all_tokens = []
|
| 668 |
|
|
|
|
| 672 |
|
| 673 |
max_len = max(a.shape[0] for a in mini_audios)
|
| 674 |
padded = [np.pad(a, (0, max_len - len(a))) for a in mini_audios]
|
| 675 |
+
|
| 676 |
+
# GPU-accelerated resampling for SNAC
|
| 677 |
audios_24k = torch.stack([
|
| 678 |
+
torchaudio.functional.resample(
|
| 679 |
+
torch.from_numpy(a).to(device), 32000, 24000
|
| 680 |
+
).cpu()
|
| 681 |
for a in padded
|
| 682 |
])
|
| 683 |
|
|
|
|
| 689 |
|
| 690 |
torch.cuda.synchronize()
|
| 691 |
|
| 692 |
+
# Periodic GPU memory cleanup every 100 batches to prevent fragmentation
|
| 693 |
+
if processed > 0 and processed % (100 * len(audio_batch)) == 0:
|
| 694 |
+
torch.cuda.empty_cache()
|
| 695 |
+
gc.collect()
|
| 696 |
+
|
| 697 |
# 3. Build results with pre-computed text tokens and word alignments
|
| 698 |
results = []
|
| 699 |
for i, ad in enumerate(audio_batch):
|
|
|
|
| 722 |
batch_time = time.time() - t0
|
| 723 |
batch_rate = len(results) / batch_time if batch_time > 0 else 0
|
| 724 |
|
| 725 |
+
# Put results with timeout to prevent indefinite blocking
|
| 726 |
+
put_start = time.time()
|
| 727 |
+
max_put_attempts = 10
|
| 728 |
+
for attempt in range(max_put_attempts):
|
| 729 |
+
try:
|
| 730 |
+
result_queue.put((batch_idx, results), timeout=30)
|
| 731 |
+
break
|
| 732 |
+
except Exception as put_err:
|
| 733 |
+
if attempt < max_put_attempts - 1:
|
| 734 |
+
status_queue.put(("feat_warn", gpu_id, f"Queue full, retry {attempt+1}"))
|
| 735 |
+
time.sleep(1)
|
| 736 |
+
else:
|
| 737 |
+
status_queue.put(("feat_error", gpu_id, f"Failed to put results after {max_put_attempts} attempts"))
|
| 738 |
+
raise put_err
|
| 739 |
+
|
| 740 |
processed += len(results)
|
| 741 |
elapsed = time.time() - t_start
|
| 742 |
status_queue.put(("feat", gpu_id, processed, processed/elapsed, batch_rate))
|
| 743 |
|
| 744 |
+
# Clear intermediate tensors to prevent memory accumulation
|
| 745 |
+
del whisper_features, all_tokens, results
|
| 746 |
+
if processed % 500 == 0: # More aggressive cleanup every 500 items
|
| 747 |
+
torch.cuda.empty_cache()
|
| 748 |
+
gc.collect()
|
| 749 |
+
|
| 750 |
except Exception as e:
|
| 751 |
+
import traceback
|
| 752 |
+
error_msg = str(e)
|
| 753 |
+
status_queue.put(("feat_error", gpu_id, f"{error_msg}\n{traceback.format_exc()}"))
|
| 754 |
+
|
| 755 |
+
# Clear GPU memory aggressively
|
| 756 |
+
torch.cuda.empty_cache()
|
| 757 |
+
torch.cuda.synchronize()
|
| 758 |
+
gc.collect()
|
| 759 |
+
|
| 760 |
+
# If OOM, try to recover by reducing batch sizes
|
| 761 |
+
if "out of memory" in error_msg.lower() or "OOM" in error_msg:
|
| 762 |
+
status_queue.put(("feat_warn", gpu_id, "OOM detected, clearing memory..."))
|
| 763 |
+
time.sleep(2) # Give GPU time to recover
|
| 764 |
+
torch.cuda.empty_cache()
|
| 765 |
+
|
| 766 |
+
continue
|
| 767 |
|
| 768 |
status_queue.put(("feat_done", gpu_id, processed))
|
| 769 |
|
|
|
|
| 778 |
|
| 779 |
import argparse
|
| 780 |
parser = argparse.ArgumentParser()
|
| 781 |
+
parser.add_argument("--count", "--num_samples", type=int, default=100, dest="count")
|
| 782 |
parser.add_argument("--output", type=str, default="./data/dataset.pt")
|
| 783 |
parser.add_argument("--gpus", type=int, default=NUM_GPUS)
|
| 784 |
+
parser.add_argument("--resume", action="store_true", help="Resume from existing checkpoint")
|
| 785 |
+
parser.add_argument("--checkpoint-interval", type=int, default=1000, help="Save checkpoint every N items")
|
| 786 |
+
parser.add_argument("--log-file", type=str, default=None, help="Log file path (default: output.log)")
|
| 787 |
# Optional overrides for batch sizes (if not set, auto-calculated from VRAM)
|
| 788 |
parser.add_argument("--tts-batch", type=int, default=None)
|
| 789 |
parser.add_argument("--snac-batch", type=int, default=None)
|
|
|
|
| 792 |
|
| 793 |
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
|
| 794 |
|
| 795 |
+
# Setup logging (default to output path + .log)
|
| 796 |
+
log_file = args.log_file or (args.output + ".log")
|
| 797 |
+
setup_logging(log_file)
|
| 798 |
+
log(f"[Log] Writing to {log_file}")
|
| 799 |
+
|
| 800 |
# Calculate batch sizes based on GPU VRAM
|
| 801 |
vram_gb = get_gpu_vram_gb()
|
| 802 |
batch_sizes = calculate_batch_sizes(vram_gb)
|
|
|
|
| 809 |
if args.whisper_workers is not None:
|
| 810 |
batch_sizes["whisper"] = args.whisper_workers
|
| 811 |
|
| 812 |
+
log("=" * 60)
|
| 813 |
+
log("Dataset Generator - Fully Async Pipeline")
|
| 814 |
+
log(f"Target: {args.count} items, GPUs: {args.gpus}")
|
| 815 |
print_batch_config(batch_sizes, vram_gb)
|
| 816 |
+
log("=" * 60)
|
| 817 |
+
|
| 818 |
+
# Resume from checkpoint or output file if exists
|
| 819 |
+
checkpoint_path = args.output + ".checkpoint"
|
| 820 |
+
existing_items = []
|
| 821 |
+
start_count = 0
|
| 822 |
+
|
| 823 |
+
# Track resume state
|
| 824 |
+
resume_from_path = None
|
| 825 |
+
|
| 826 |
+
if args.resume:
|
| 827 |
+
# Check for existing data to resume from
|
| 828 |
+
# Priority: .new checkpoint (partial new items) + output file, or just output file
|
| 829 |
+
|
| 830 |
+
new_checkpoint = checkpoint_path + ".new"
|
| 831 |
+
new_items_count = 0
|
| 832 |
+
base_count = 0
|
| 833 |
+
|
| 834 |
+
# Check if we have new items checkpoint
|
| 835 |
+
if Path(new_checkpoint).exists():
|
| 836 |
+
try:
|
| 837 |
+
data = torch.load(new_checkpoint, map_location="cpu", weights_only=False, mmap=True)
|
| 838 |
+
new_items_count = len(data)
|
| 839 |
+
del data
|
| 840 |
+
log(f"[Resume] Found {new_items_count} new items in checkpoint")
|
| 841 |
+
except Exception as e:
|
| 842 |
+
log(f"[Resume] Failed to read {new_checkpoint}: {e}")
|
| 843 |
+
|
| 844 |
+
# Check output file for base items
|
| 845 |
+
if Path(args.output).exists():
|
| 846 |
+
try:
|
| 847 |
+
data = torch.load(args.output, map_location="cpu", weights_only=False, mmap=True)
|
| 848 |
+
base_count = len(data)
|
| 849 |
+
del data
|
| 850 |
+
resume_from_path = args.output
|
| 851 |
+
log(f"[Resume] Found {base_count} base items in {args.output}")
|
| 852 |
+
except Exception as e:
|
| 853 |
+
log(f"[Resume] Failed to read {args.output}: {e}")
|
| 854 |
+
|
| 855 |
+
# Total count is base + new
|
| 856 |
+
start_count = base_count + new_items_count
|
| 857 |
+
if start_count > 0:
|
| 858 |
+
log(f"[Resume] Total: {start_count} items ({base_count} base + {new_items_count} new), need {args.count - start_count} more")
|
| 859 |
+
else:
|
| 860 |
+
log("[Resume] No valid resume file found, starting fresh")
|
| 861 |
+
|
| 862 |
+
if start_count >= args.count:
|
| 863 |
+
log(f"[Resume] Already have {start_count} items, saving final...")
|
| 864 |
+
torch.save(existing_items[:args.count], args.output)
|
| 865 |
+
log(f"COMPLETE: {args.count} items saved to {args.output}")
|
| 866 |
+
return
|
| 867 |
+
|
| 868 |
+
remaining_count = args.count - start_count
|
| 869 |
+
log(f"[Main] Generating {remaining_count} new items...")
|
| 870 |
|
| 871 |
total_start = time.time()
|
| 872 |
|
| 873 |
+
# Use regular mp.Queue with NO size limits to prevent deadlocks
|
| 874 |
+
# Workers will block on put() if queue is full, causing stalls
|
| 875 |
+
# Memory is managed by batch sizes instead
|
| 876 |
+
tts_queue = mp.Queue() # No maxsize - prevents TTS blocking
|
| 877 |
+
feat_queue = mp.Queue() # No maxsize - prevents Features blocking
|
| 878 |
+
result_queue = mp.Queue() # No maxsize - prevents result collection blocking
|
| 879 |
status_queue = mp.Queue()
|
| 880 |
|
| 881 |
workers = []
|
| 882 |
|
| 883 |
# Get actual GPU count for worker assignment
|
| 884 |
actual_num_gpus = get_num_gpus() if torch.cuda.is_available() else 1
|
|
|
|
| 885 |
|
| 886 |
+
# With 4+ GPUs, separate TTS and Features to avoid OOM
|
| 887 |
+
# TTS uses GPUs 0 to (N/2-1), Features uses GPUs (N/2) to (N-1)
|
| 888 |
+
if actual_num_gpus >= 4:
|
| 889 |
+
tts_gpus = actual_num_gpus // 2 # First half for TTS
|
| 890 |
+
feat_gpus = actual_num_gpus - tts_gpus # Second half for Features
|
| 891 |
+
tts_gpu_offset = 0
|
| 892 |
+
feat_gpu_offset = tts_gpus
|
| 893 |
+
log(f"\n[Main] Detected {actual_num_gpus} GPUs - Separating: TTS on GPUs 0-{tts_gpus-1}, Features on GPUs {feat_gpu_offset}-{actual_num_gpus-1}")
|
| 894 |
+
else:
|
| 895 |
+
tts_gpus = actual_num_gpus
|
| 896 |
+
feat_gpus = actual_num_gpus
|
| 897 |
+
tts_gpu_offset = 0
|
| 898 |
+
feat_gpu_offset = 0
|
| 899 |
+
log(f"\n[Main] Detected {actual_num_gpus} GPUs, sharing between TTS and Features")
|
| 900 |
+
|
| 901 |
+
# Adjust number of workers based on available GPUs
|
| 902 |
+
num_tts_workers = min(args.gpus, tts_gpus)
|
| 903 |
+
num_feat_workers = min(args.gpus, feat_gpus)
|
| 904 |
+
log(f"[Main] Spawning {num_tts_workers} TTS workers, {num_feat_workers} Features workers")
|
| 905 |
+
|
| 906 |
+
qa_proc = mp.Process(target=qa_producer, args=(remaining_count, tts_queue, batch_sizes["tts"], status_queue, num_tts_workers))
|
| 907 |
qa_proc.start()
|
| 908 |
workers.append(qa_proc)
|
| 909 |
|
| 910 |
+
for gpu_id in range(num_tts_workers):
|
| 911 |
+
p = mp.Process(target=tts_worker, args=(gpu_id, tts_queue, feat_queue, status_queue, batch_sizes, tts_gpus, tts_gpu_offset))
|
| 912 |
p.start()
|
| 913 |
workers.append(p)
|
| 914 |
|
| 915 |
+
for gpu_id in range(num_feat_workers):
|
| 916 |
+
p = mp.Process(target=features_worker, args=(gpu_id, feat_queue, result_queue, status_queue, batch_sizes, feat_gpus, feat_gpu_offset))
|
| 917 |
p.start()
|
| 918 |
workers.append(p)
|
| 919 |
|
| 920 |
+
log("[Pipeline] All workers started, monitoring...")
|
| 921 |
|
| 922 |
results = {}
|
| 923 |
tts_done_count = 0
|
|
|
|
| 926 |
t0 = time.time()
|
| 927 |
tts_ready = 0
|
| 928 |
feat_ready = 0
|
| 929 |
+
expected_from_feat = {}
|
| 930 |
+
feat_queue_closed = False
|
| 931 |
+
last_checkpoint_count = 0
|
| 932 |
|
| 933 |
+
# Main loop with improved stall detection
|
| 934 |
last_result_time = time.time()
|
| 935 |
+
last_status_time = time.time()
|
| 936 |
+
last_heartbeat_time = {f"tts_{i}": time.time() for i in range(num_tts_workers)}
|
| 937 |
+
last_heartbeat_time.update({f"feat_{i}": time.time() for i in range(num_feat_workers)})
|
| 938 |
+
stall_warning_shown = False
|
| 939 |
+
errors = []
|
| 940 |
+
|
| 941 |
while True:
|
| 942 |
+
# Process ALL pending status messages
|
| 943 |
+
for _ in range(100): # Limit to prevent infinite loop
|
|
|
|
| 944 |
try:
|
| 945 |
msg = status_queue.get_nowait()
|
| 946 |
+
last_status_time = time.time()
|
| 947 |
msg_type = msg[0]
|
| 948 |
|
| 949 |
if msg_type == "tts_ready":
|
| 950 |
tts_ready += 1
|
| 951 |
+
log(f"[TTS-GPU{msg[1]}] Ready ({tts_ready}/{num_tts_workers})")
|
| 952 |
elif msg_type == "feat_ready":
|
| 953 |
feat_ready += 1
|
| 954 |
+
log(f"[Features-GPU{msg[1]}] Ready ({feat_ready}/{num_feat_workers})")
|
| 955 |
elif msg_type == "qa":
|
| 956 |
+
log(f"[Q&A] {msg[1]}/{msg[2]} | {msg[3]:.1f}/s")
|
| 957 |
elif msg_type == "qa_done":
|
| 958 |
+
log(f"[Q&A] Done: {msg[1]} pairs, {msg[2]} batches")
|
| 959 |
elif msg_type == "tts":
|
| 960 |
+
log(f"[TTS-GPU{msg[1]}] {msg[2]} items | avg {msg[3]:.1f}/s | batch {msg[4]:.1f}/s")
|
| 961 |
+
last_heartbeat_time[f"tts_{msg[1]}"] = time.time()
|
| 962 |
elif msg_type == "tts_done":
|
| 963 |
tts_done_count += 1
|
| 964 |
+
log(f"[TTS-GPU{msg[1]}] Done: {msg[2]} items")
|
| 965 |
elif msg_type == "feat":
|
| 966 |
+
log(f"[Feat-GPU{msg[1]}] {msg[2]} items | avg {msg[3]:.1f}/s | batch {msg[4]:.1f}/s")
|
| 967 |
+
last_heartbeat_time[f"feat_{msg[1]}"] = time.time()
|
| 968 |
elif msg_type == "feat_done":
|
| 969 |
feat_done_count += 1
|
| 970 |
expected_from_feat[msg[1]] = msg[2]
|
| 971 |
+
log(f"[Features-GPU{msg[1]}] Done: {msg[2]} items")
|
| 972 |
+
elif msg_type == "tts_heartbeat":
|
| 973 |
+
last_heartbeat_time[f"tts_{msg[1]}"] = time.time()
|
| 974 |
+
elif msg_type == "feat_heartbeat":
|
| 975 |
+
last_heartbeat_time[f"feat_{msg[1]}"] = time.time()
|
| 976 |
+
elif msg_type == "feat_warn":
|
| 977 |
+
log(f"[WARN] Features-GPU{msg[1]}: {msg[2]}")
|
| 978 |
elif "error" in msg_type:
|
| 979 |
+
log(f"[Error] {msg}")
|
| 980 |
+
errors.append(msg)
|
| 981 |
except:
|
| 982 |
break
|
| 983 |
|
| 984 |
+
if tts_done_count >= num_tts_workers and not feat_queue_closed:
|
| 985 |
+
log(f"[Main] TTS done, waiting for features to finish...")
|
|
|
|
|
|
|
| 986 |
feat_queue_closed = True
|
| 987 |
|
| 988 |
+
# Collect ALL available results (drain aggressively with non-blocking gets)
|
| 989 |
+
collected_this_round = 0
|
| 990 |
+
drain_start = time.time()
|
| 991 |
+
while time.time() - drain_start < 2.0: # Spend up to 2s draining
|
| 992 |
+
try:
|
| 993 |
+
batch_idx, items = result_queue.get_nowait()
|
| 994 |
+
results[batch_idx] = items
|
| 995 |
+
total_items += len(items)
|
| 996 |
+
collected_this_round += len(items)
|
| 997 |
+
last_result_time = time.time()
|
| 998 |
+
stall_warning_shown = False
|
| 999 |
+
except:
|
| 1000 |
+
# No more items immediately available, wait briefly then check again
|
| 1001 |
+
time.sleep(0.05)
|
| 1002 |
+
try:
|
| 1003 |
+
batch_idx, items = result_queue.get_nowait()
|
| 1004 |
+
results[batch_idx] = items
|
| 1005 |
+
total_items += len(items)
|
| 1006 |
+
collected_this_round += len(items)
|
| 1007 |
+
last_result_time = time.time()
|
| 1008 |
+
stall_warning_shown = False
|
| 1009 |
+
except:
|
| 1010 |
+
break # Queue truly empty
|
| 1011 |
+
|
| 1012 |
+
if collected_this_round > 0:
|
| 1013 |
elapsed = time.time() - t0
|
| 1014 |
+
log(f"[Results] {total_items}/{remaining_count} | {total_items/elapsed:.1f}/s")
|
| 1015 |
+
|
| 1016 |
+
# Save checkpoint periodically (combine with any existing checkpoint)
|
| 1017 |
+
if total_items - last_checkpoint_count >= args.checkpoint_interval:
|
| 1018 |
+
log(f"[Checkpoint] Saving {total_items} new items...")
|
| 1019 |
+
# Collect items from this run
|
| 1020 |
+
items_this_run = []
|
| 1021 |
+
for i in sorted(results.keys()):
|
| 1022 |
+
items_this_run.extend(results[i])
|
| 1023 |
+
|
| 1024 |
+
# Load and combine with existing checkpoint if present
|
| 1025 |
+
checkpoint_new_path = checkpoint_path + ".new"
|
| 1026 |
+
all_checkpoint_items = []
|
| 1027 |
+
if Path(checkpoint_new_path).exists():
|
| 1028 |
+
try:
|
| 1029 |
+
prev_items = torch.load(checkpoint_new_path, map_location="cpu", weights_only=False)
|
| 1030 |
+
all_checkpoint_items = list(prev_items)
|
| 1031 |
+
del prev_items
|
| 1032 |
+
except:
|
| 1033 |
+
pass
|
| 1034 |
+
|
| 1035 |
+
# Only add items not already in checkpoint
|
| 1036 |
+
items_to_add = items_this_run[len(all_checkpoint_items):]
|
| 1037 |
+
all_checkpoint_items.extend(items_to_add)
|
| 1038 |
+
|
| 1039 |
+
torch.save(all_checkpoint_items, checkpoint_new_path)
|
| 1040 |
+
last_checkpoint_count = total_items
|
| 1041 |
+
log(f"[Checkpoint] Saved {len(all_checkpoint_items)} total to {checkpoint_new_path}")
|
| 1042 |
|
| 1043 |
# Check exit conditions
|
| 1044 |
+
if total_items >= remaining_count:
|
| 1045 |
+
log(f"[Main] Target reached: {total_items}/{remaining_count}")
|
| 1046 |
break
|
| 1047 |
|
| 1048 |
+
# If all feature workers done, drain remaining results
|
| 1049 |
+
if feat_done_count >= num_feat_workers:
|
| 1050 |
+
log(f"[Main] All workers done, draining queue...")
|
|
|
|
| 1051 |
drain_start = time.time()
|
| 1052 |
+
while time.time() - drain_start < 60.0:
|
| 1053 |
try:
|
| 1054 |
batch_idx, items = result_queue.get(timeout=0.5)
|
| 1055 |
results[batch_idx] = items
|
| 1056 |
total_items += len(items)
|
| 1057 |
+
log(f"[Results] {total_items}/{remaining_count} | (drained)")
|
| 1058 |
except:
|
| 1059 |
+
time.sleep(0.2)
|
| 1060 |
+
try:
|
| 1061 |
+
batch_idx, items = result_queue.get_nowait()
|
| 1062 |
+
results[batch_idx] = items
|
| 1063 |
+
total_items += len(items)
|
| 1064 |
+
log(f"[Results] {total_items}/{remaining_count} | (drained)")
|
| 1065 |
+
except:
|
| 1066 |
break
|
| 1067 |
break
|
| 1068 |
|
| 1069 |
+
# Check if workers are still alive and responding
|
| 1070 |
+
alive_workers = sum(1 for p in workers if p.is_alive())
|
| 1071 |
+
time_since_result = time.time() - last_result_time
|
| 1072 |
+
time_since_status = time.time() - last_status_time
|
| 1073 |
+
|
| 1074 |
+
# Check heartbeats - detect stuck workers
|
| 1075 |
+
stuck_workers = []
|
| 1076 |
+
for worker_id, last_hb in last_heartbeat_time.items():
|
| 1077 |
+
if time.time() - last_hb > 120: # No heartbeat for 2 minutes
|
| 1078 |
+
stuck_workers.append(worker_id)
|
| 1079 |
+
|
| 1080 |
+
if time_since_result > 30 and not stall_warning_shown:
|
| 1081 |
+
queue_size = 0
|
| 1082 |
+
try:
|
| 1083 |
+
queue_size = result_queue.qsize()
|
| 1084 |
+
except:
|
| 1085 |
+
pass
|
| 1086 |
+
log(f"[WARN] No results for 30s, {alive_workers} workers alive, queue ~{queue_size} items")
|
| 1087 |
+
if stuck_workers:
|
| 1088 |
+
log(f"[WARN] Stuck workers (no heartbeat >2min): {stuck_workers}")
|
| 1089 |
+
stall_warning_shown = True
|
| 1090 |
+
|
| 1091 |
+
# Save checkpoint on stall detection (combine with existing checkpoint)
|
| 1092 |
+
if time_since_result > 60 and total_items > last_checkpoint_count:
|
| 1093 |
+
log(f"[Checkpoint] Stall detected, saving {total_items} items...")
|
| 1094 |
+
items_this_run = []
|
| 1095 |
+
for i in sorted(results.keys()):
|
| 1096 |
+
items_this_run.extend(results[i])
|
| 1097 |
+
|
| 1098 |
+
# Load and combine with existing checkpoint
|
| 1099 |
+
checkpoint_new_path = checkpoint_path + ".new"
|
| 1100 |
+
all_checkpoint_items = []
|
| 1101 |
+
if Path(checkpoint_new_path).exists():
|
| 1102 |
+
try:
|
| 1103 |
+
prev_items = torch.load(checkpoint_new_path, map_location="cpu", weights_only=False)
|
| 1104 |
+
all_checkpoint_items = list(prev_items)
|
| 1105 |
+
del prev_items
|
| 1106 |
+
except:
|
| 1107 |
+
pass
|
| 1108 |
+
|
| 1109 |
+
items_to_add = items_this_run[len(all_checkpoint_items):]
|
| 1110 |
+
all_checkpoint_items.extend(items_to_add)
|
| 1111 |
+
torch.save(all_checkpoint_items, checkpoint_new_path)
|
| 1112 |
+
last_checkpoint_count = total_items
|
| 1113 |
+
|
| 1114 |
+
# Exit if truly stalled - but be smarter about it
|
| 1115 |
+
# Only exit if: no results for 3min AND no heartbeats AND workers dead
|
| 1116 |
+
all_workers_stuck = len(stuck_workers) >= (num_tts_workers + num_feat_workers)
|
| 1117 |
+
if time_since_result > 180 and all_workers_stuck:
|
| 1118 |
+
log(f"[WARN] All workers stuck, stopping with {total_items} items")
|
| 1119 |
+
log(f"[WARN] Errors encountered: {len(errors)}")
|
| 1120 |
+
for err in errors[-5:]: # Show last 5 errors
|
| 1121 |
+
log(f" {err}")
|
| 1122 |
+
break
|
| 1123 |
+
|
| 1124 |
+
if time_since_result > 600:
|
| 1125 |
+
log(f"[WARN] No progress for 10min, stopping with {total_items} items")
|
| 1126 |
break
|
| 1127 |
|
| 1128 |
+
# Save checkpoint before cleanup (combine with existing checkpoint)
|
| 1129 |
+
if total_items > last_checkpoint_count:
|
| 1130 |
+
log(f"[Checkpoint] Final save before cleanup: {total_items} items...")
|
| 1131 |
+
items_this_run = []
|
| 1132 |
+
for i in sorted(results.keys()):
|
| 1133 |
+
items_this_run.extend(results[i])
|
| 1134 |
+
|
| 1135 |
+
# Load and combine with existing checkpoint
|
| 1136 |
+
checkpoint_new_path = checkpoint_path + ".new"
|
| 1137 |
+
all_checkpoint_items = []
|
| 1138 |
+
if Path(checkpoint_new_path).exists():
|
| 1139 |
+
try:
|
| 1140 |
+
prev_items = torch.load(checkpoint_new_path, map_location="cpu", weights_only=False)
|
| 1141 |
+
all_checkpoint_items = list(prev_items)
|
| 1142 |
+
del prev_items
|
| 1143 |
+
except:
|
| 1144 |
+
pass
|
| 1145 |
+
|
| 1146 |
+
items_to_add = items_this_run[len(all_checkpoint_items):]
|
| 1147 |
+
all_checkpoint_items.extend(items_to_add)
|
| 1148 |
+
torch.save(all_checkpoint_items, checkpoint_new_path)
|
| 1149 |
+
last_checkpoint_count = total_items
|
| 1150 |
+
|
| 1151 |
# Wait for workers to finish
|
| 1152 |
+
log("[Main] Waiting for workers to finish...")
|
| 1153 |
for p in workers:
|
| 1154 |
p.join(timeout=5)
|
| 1155 |
if p.is_alive():
|
| 1156 |
p.terminate()
|
| 1157 |
|
| 1158 |
+
# Final drain
|
| 1159 |
+
for _ in range(100):
|
| 1160 |
try:
|
| 1161 |
batch_idx, items = result_queue.get_nowait()
|
| 1162 |
results[batch_idx] = items
|
| 1163 |
total_items += len(items)
|
|
|
|
| 1164 |
except:
|
| 1165 |
break
|
| 1166 |
|
| 1167 |
+
# Collect new results from this run
|
| 1168 |
+
new_items_this_run = []
|
| 1169 |
for i in sorted(results.keys()):
|
| 1170 |
+
new_items_this_run.extend(results[i])
|
| 1171 |
+
|
| 1172 |
+
log(f"[Main] Generated {len(new_items_this_run)} new items this run")
|
| 1173 |
|
| 1174 |
+
# Load any previously checkpointed new items and combine with this run's items
|
| 1175 |
+
new_checkpoint = checkpoint_path + ".new"
|
| 1176 |
+
new_items = []
|
| 1177 |
+
|
| 1178 |
+
if Path(new_checkpoint).exists():
|
| 1179 |
+
try:
|
| 1180 |
+
prev_new = torch.load(new_checkpoint, map_location="cpu", weights_only=False)
|
| 1181 |
+
new_items = list(prev_new)
|
| 1182 |
+
log(f"[Main] Loaded {len(new_items)} items from previous checkpoint")
|
| 1183 |
+
del prev_new
|
| 1184 |
+
except Exception as e:
|
| 1185 |
+
log(f"[Main] Failed to load previous checkpoint: {e}")
|
| 1186 |
+
|
| 1187 |
+
# Add this run's items to the checkpoint items
|
| 1188 |
+
new_items.extend(new_items_this_run)
|
| 1189 |
+
log(f"[Main] Total new items: {len(new_items)} (checkpoint: {len(new_items) - len(new_items_this_run)}, this run: {len(new_items_this_run)})")
|
| 1190 |
+
|
| 1191 |
+
total_new = len(new_items)
|
| 1192 |
+
|
| 1193 |
+
if total_new == 0 and (not resume_from_path or start_count == 0):
|
| 1194 |
+
log("[ERROR] No items collected! Check worker logs.")
|
| 1195 |
sys.exit(1)
|
| 1196 |
|
| 1197 |
+
# Combine with base items if resuming
|
| 1198 |
+
if resume_from_path and Path(resume_from_path).exists():
|
| 1199 |
+
log(f"[Main] Loading base items from {resume_from_path}...")
|
| 1200 |
+
base_data = torch.load(resume_from_path, map_location="cpu", weights_only=False, mmap=True)
|
| 1201 |
+
base_count = len(base_data)
|
| 1202 |
+
|
| 1203 |
+
# Calculate how many base items to keep
|
| 1204 |
+
items_needed_from_base = min(base_count, args.count - total_new)
|
| 1205 |
+
log(f"[Main] Combining {items_needed_from_base} base + {total_new} new items...")
|
| 1206 |
+
|
| 1207 |
+
final_items = list(base_data[:items_needed_from_base]) + new_items
|
| 1208 |
+
del base_data
|
| 1209 |
+
else:
|
| 1210 |
+
final_items = new_items
|
| 1211 |
+
|
| 1212 |
+
# Trim to target count
|
| 1213 |
+
final_items = final_items[:args.count]
|
| 1214 |
+
|
| 1215 |
+
# Save final dataset
|
| 1216 |
+
log(f"[Main] Saving {len(final_items)} items to {args.output}...")
|
| 1217 |
+
torch.save(final_items, args.output)
|
| 1218 |
+
|
| 1219 |
+
# Remove checkpoint files if complete
|
| 1220 |
+
if len(final_items) >= args.count:
|
| 1221 |
+
for cp in [checkpoint_path, checkpoint_path + ".new"]:
|
| 1222 |
+
if Path(cp).exists():
|
| 1223 |
+
Path(cp).unlink()
|
| 1224 |
+
log(f"[Cleanup] Removed {cp}")
|
| 1225 |
|
| 1226 |
total_time = time.time() - total_start
|
| 1227 |
+
log("\n" + "=" * 60)
|
| 1228 |
+
log(f"COMPLETE: {len(final_items)} items saved to {args.output}")
|
| 1229 |
+
if start_count > 0:
|
| 1230 |
+
log(f" (resumed from {start_count}, added {len(final_items) - start_count} new)")
|
| 1231 |
+
log(f"Total time: {total_time:.1f}s ({total_time/60:.1f}m)")
|
| 1232 |
+
log(f"Throughput: {(len(final_items) - start_count)/total_time:.2f} items/s")
|
| 1233 |
+
log("=" * 60)
|
| 1234 |
|
| 1235 |
|
| 1236 |
if __name__ == "__main__":
|
datasets/validate_dataset.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Dataset Validator - Validates the generated dataset for training.
|
| 4 |
+
|
| 5 |
+
Checks:
|
| 6 |
+
1. Structure: list of dicts with required fields
|
| 7 |
+
2. Whisper features: shape [seq_len, 1280]
|
| 8 |
+
3. SNAC tokens: multiple of 7, valid range
|
| 9 |
+
4. Optional fields: text, answer, text_tokens, word_alignments
|
| 10 |
+
5. Statistics and distribution
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import argparse
|
| 14 |
+
import sys
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
from collections import defaultdict
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
import numpy as np
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# SNAC token constants (from CLAUDE.md)
|
| 23 |
+
SNAC_BASE = 128266
|
| 24 |
+
SNAC_LAYERS = 7
|
| 25 |
+
SNAC_VOCAB_PER_LAYER = 4096
|
| 26 |
+
WHISPER_DIM = 1280
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def validate_sample(idx: int, sample: dict, verbose: bool = False) -> tuple[bool, list[str]]:
|
| 30 |
+
"""Validate a single sample. Returns (is_valid, list of errors)."""
|
| 31 |
+
errors = []
|
| 32 |
+
warnings = []
|
| 33 |
+
|
| 34 |
+
# Check required fields
|
| 35 |
+
if "whisper_features" not in sample:
|
| 36 |
+
errors.append("Missing 'whisper_features'")
|
| 37 |
+
if "snac_tokens" not in sample:
|
| 38 |
+
errors.append("Missing 'snac_tokens'")
|
| 39 |
+
|
| 40 |
+
if errors:
|
| 41 |
+
return False, errors
|
| 42 |
+
|
| 43 |
+
# Validate whisper_features
|
| 44 |
+
wf = sample["whisper_features"]
|
| 45 |
+
if not isinstance(wf, torch.Tensor):
|
| 46 |
+
errors.append(f"whisper_features should be Tensor, got {type(wf).__name__}")
|
| 47 |
+
else:
|
| 48 |
+
if wf.dim() != 2:
|
| 49 |
+
errors.append(f"whisper_features should be 2D [seq_len, 1280], got {wf.dim()}D")
|
| 50 |
+
elif wf.shape[1] != WHISPER_DIM:
|
| 51 |
+
errors.append(f"whisper_features dim should be {WHISPER_DIM}, got {wf.shape[1]}")
|
| 52 |
+
if wf.shape[0] == 0:
|
| 53 |
+
errors.append("whisper_features has 0 length")
|
| 54 |
+
if torch.isnan(wf).any():
|
| 55 |
+
errors.append("whisper_features contains NaN values")
|
| 56 |
+
if torch.isinf(wf).any():
|
| 57 |
+
errors.append("whisper_features contains Inf values")
|
| 58 |
+
|
| 59 |
+
# Validate snac_tokens
|
| 60 |
+
st = sample["snac_tokens"]
|
| 61 |
+
if not isinstance(st, torch.Tensor):
|
| 62 |
+
errors.append(f"snac_tokens should be Tensor, got {type(st).__name__}")
|
| 63 |
+
else:
|
| 64 |
+
if st.dim() != 1:
|
| 65 |
+
errors.append(f"snac_tokens should be 1D, got {st.dim()}D")
|
| 66 |
+
if len(st) == 0:
|
| 67 |
+
errors.append("snac_tokens has 0 length")
|
| 68 |
+
elif len(st) % SNAC_LAYERS != 0:
|
| 69 |
+
errors.append(f"snac_tokens length ({len(st)}) not multiple of {SNAC_LAYERS}")
|
| 70 |
+
|
| 71 |
+
# Check token range (raw tokens before offset, should be 0-4095)
|
| 72 |
+
if len(st) > 0:
|
| 73 |
+
min_tok = st.min().item()
|
| 74 |
+
max_tok = st.max().item()
|
| 75 |
+
# Tokens could be raw (0-4095) or with offset applied (128266+)
|
| 76 |
+
if max_tok < SNAC_BASE:
|
| 77 |
+
# Raw tokens
|
| 78 |
+
if min_tok < 0 or max_tok >= SNAC_VOCAB_PER_LAYER:
|
| 79 |
+
warnings.append(f"snac_tokens range [{min_tok}, {max_tok}] outside [0, {SNAC_VOCAB_PER_LAYER-1}]")
|
| 80 |
+
else:
|
| 81 |
+
# Tokens with offset
|
| 82 |
+
expected_max = SNAC_BASE + (SNAC_LAYERS * SNAC_VOCAB_PER_LAYER)
|
| 83 |
+
if min_tok < SNAC_BASE or max_tok >= expected_max:
|
| 84 |
+
warnings.append(f"snac_tokens with offset range [{min_tok}, {max_tok}] unexpected")
|
| 85 |
+
|
| 86 |
+
# Validate optional fields
|
| 87 |
+
if "text" in sample and not isinstance(sample["text"], str):
|
| 88 |
+
warnings.append(f"text should be str, got {type(sample['text']).__name__}")
|
| 89 |
+
|
| 90 |
+
if "answer" in sample and not isinstance(sample["answer"], str):
|
| 91 |
+
warnings.append(f"answer should be str, got {type(sample['answer']).__name__}")
|
| 92 |
+
|
| 93 |
+
if "text_tokens" in sample:
|
| 94 |
+
tt = sample["text_tokens"]
|
| 95 |
+
if not isinstance(tt, torch.Tensor):
|
| 96 |
+
warnings.append(f"text_tokens should be Tensor, got {type(tt).__name__}")
|
| 97 |
+
elif tt.dim() != 1:
|
| 98 |
+
warnings.append(f"text_tokens should be 1D, got {tt.dim()}D")
|
| 99 |
+
|
| 100 |
+
if "word_alignments" in sample:
|
| 101 |
+
wa = sample["word_alignments"]
|
| 102 |
+
if not isinstance(wa, list):
|
| 103 |
+
warnings.append(f"word_alignments should be list, got {type(wa).__name__}")
|
| 104 |
+
|
| 105 |
+
if verbose and warnings:
|
| 106 |
+
for w in warnings:
|
| 107 |
+
print(f" [WARN] Sample {idx}: {w}")
|
| 108 |
+
|
| 109 |
+
return len(errors) == 0, errors
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def compute_statistics(dataset: list) -> dict:
|
| 113 |
+
"""Compute dataset statistics."""
|
| 114 |
+
stats = {
|
| 115 |
+
"total_samples": len(dataset),
|
| 116 |
+
"whisper_lengths": [],
|
| 117 |
+
"snac_lengths": [],
|
| 118 |
+
"snac_frames": [],
|
| 119 |
+
"has_text": 0,
|
| 120 |
+
"has_answer": 0,
|
| 121 |
+
"has_text_tokens": 0,
|
| 122 |
+
"has_word_alignments": 0,
|
| 123 |
+
"text_lengths": [],
|
| 124 |
+
"answer_lengths": [],
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
for sample in dataset:
|
| 128 |
+
if "whisper_features" in sample and isinstance(sample["whisper_features"], torch.Tensor):
|
| 129 |
+
stats["whisper_lengths"].append(sample["whisper_features"].shape[0])
|
| 130 |
+
|
| 131 |
+
if "snac_tokens" in sample and isinstance(sample["snac_tokens"], torch.Tensor):
|
| 132 |
+
length = len(sample["snac_tokens"])
|
| 133 |
+
stats["snac_lengths"].append(length)
|
| 134 |
+
stats["snac_frames"].append(length // SNAC_LAYERS)
|
| 135 |
+
|
| 136 |
+
if "text" in sample:
|
| 137 |
+
stats["has_text"] += 1
|
| 138 |
+
if isinstance(sample["text"], str):
|
| 139 |
+
stats["text_lengths"].append(len(sample["text"]))
|
| 140 |
+
|
| 141 |
+
if "answer" in sample:
|
| 142 |
+
stats["has_answer"] += 1
|
| 143 |
+
if isinstance(sample["answer"], str):
|
| 144 |
+
stats["answer_lengths"].append(len(sample["answer"]))
|
| 145 |
+
|
| 146 |
+
if "text_tokens" in sample:
|
| 147 |
+
stats["has_text_tokens"] += 1
|
| 148 |
+
|
| 149 |
+
if "word_alignments" in sample:
|
| 150 |
+
stats["has_word_alignments"] += 1
|
| 151 |
+
|
| 152 |
+
return stats
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def print_statistics(stats: dict):
|
| 156 |
+
"""Print dataset statistics."""
|
| 157 |
+
print("\n" + "=" * 60)
|
| 158 |
+
print("DATASET STATISTICS")
|
| 159 |
+
print("=" * 60)
|
| 160 |
+
|
| 161 |
+
print(f"\nTotal samples: {stats['total_samples']}")
|
| 162 |
+
|
| 163 |
+
# Whisper features
|
| 164 |
+
if stats["whisper_lengths"]:
|
| 165 |
+
wl = np.array(stats["whisper_lengths"])
|
| 166 |
+
print(f"\nWhisper features length:")
|
| 167 |
+
print(f" Min: {wl.min()}, Max: {wl.max()}, Mean: {wl.mean():.1f}, Std: {wl.std():.1f}")
|
| 168 |
+
|
| 169 |
+
# SNAC tokens
|
| 170 |
+
if stats["snac_lengths"]:
|
| 171 |
+
sl = np.array(stats["snac_lengths"])
|
| 172 |
+
sf = np.array(stats["snac_frames"])
|
| 173 |
+
print(f"\nSNAC tokens:")
|
| 174 |
+
print(f" Tokens - Min: {sl.min()}, Max: {sl.max()}, Mean: {sl.mean():.1f}")
|
| 175 |
+
print(f" Frames - Min: {sf.min()}, Max: {sf.max()}, Mean: {sf.mean():.1f}")
|
| 176 |
+
|
| 177 |
+
# Duration estimate (24kHz, 512 samples per frame = 21.3ms per frame)
|
| 178 |
+
duration_sec = sf * 0.0213
|
| 179 |
+
print(f" Duration - Min: {duration_sec.min():.1f}s, Max: {duration_sec.max():.1f}s, Mean: {duration_sec.mean():.1f}s")
|
| 180 |
+
|
| 181 |
+
# Optional fields
|
| 182 |
+
print(f"\nOptional fields present:")
|
| 183 |
+
print(f" text: {stats['has_text']}/{stats['total_samples']} ({100*stats['has_text']/stats['total_samples']:.1f}%)")
|
| 184 |
+
print(f" answer: {stats['has_answer']}/{stats['total_samples']} ({100*stats['has_answer']/stats['total_samples']:.1f}%)")
|
| 185 |
+
print(f" text_tokens: {stats['has_text_tokens']}/{stats['total_samples']} ({100*stats['has_text_tokens']/stats['total_samples']:.1f}%)")
|
| 186 |
+
print(f" word_alignments: {stats['has_word_alignments']}/{stats['total_samples']} ({100*stats['has_word_alignments']/stats['total_samples']:.1f}%)")
|
| 187 |
+
|
| 188 |
+
# Text lengths
|
| 189 |
+
if stats["text_lengths"]:
|
| 190 |
+
tl = np.array(stats["text_lengths"])
|
| 191 |
+
print(f"\nText (question) length (chars):")
|
| 192 |
+
print(f" Min: {tl.min()}, Max: {tl.max()}, Mean: {tl.mean():.1f}")
|
| 193 |
+
|
| 194 |
+
if stats["answer_lengths"]:
|
| 195 |
+
al = np.array(stats["answer_lengths"])
|
| 196 |
+
print(f"\nAnswer length (chars):")
|
| 197 |
+
print(f" Min: {al.min()}, Max: {al.max()}, Mean: {al.mean():.1f}")
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def validate_dataset(path: str, max_samples: int = None, verbose: bool = False) -> bool:
|
| 201 |
+
"""Validate the dataset file."""
|
| 202 |
+
print(f"\nValidating: {path}")
|
| 203 |
+
print("=" * 60)
|
| 204 |
+
|
| 205 |
+
# Check file exists
|
| 206 |
+
if not Path(path).exists():
|
| 207 |
+
print(f"[ERROR] File not found: {path}")
|
| 208 |
+
return False
|
| 209 |
+
|
| 210 |
+
# Load dataset
|
| 211 |
+
print("Loading dataset...")
|
| 212 |
+
try:
|
| 213 |
+
dataset = torch.load(path, map_location="cpu", weights_only=False)
|
| 214 |
+
except Exception as e:
|
| 215 |
+
print(f"[ERROR] Failed to load dataset: {e}")
|
| 216 |
+
return False
|
| 217 |
+
|
| 218 |
+
# Check type
|
| 219 |
+
if not isinstance(dataset, list):
|
| 220 |
+
print(f"[ERROR] Dataset should be a list, got {type(dataset).__name__}")
|
| 221 |
+
return False
|
| 222 |
+
|
| 223 |
+
total = len(dataset)
|
| 224 |
+
print(f"Loaded {total} samples")
|
| 225 |
+
|
| 226 |
+
if total == 0:
|
| 227 |
+
print("[ERROR] Dataset is empty")
|
| 228 |
+
return False
|
| 229 |
+
|
| 230 |
+
# Validate samples
|
| 231 |
+
if max_samples and max_samples < total:
|
| 232 |
+
print(f"Validating first {max_samples} samples...")
|
| 233 |
+
samples_to_check = dataset[:max_samples]
|
| 234 |
+
else:
|
| 235 |
+
print(f"Validating all {total} samples...")
|
| 236 |
+
samples_to_check = dataset
|
| 237 |
+
|
| 238 |
+
valid_count = 0
|
| 239 |
+
error_counts = defaultdict(int)
|
| 240 |
+
|
| 241 |
+
for idx, sample in enumerate(samples_to_check):
|
| 242 |
+
if not isinstance(sample, dict):
|
| 243 |
+
print(f"[ERROR] Sample {idx}: should be dict, got {type(sample).__name__}")
|
| 244 |
+
error_counts["not_dict"] += 1
|
| 245 |
+
continue
|
| 246 |
+
|
| 247 |
+
is_valid, errors = validate_sample(idx, sample, verbose=verbose)
|
| 248 |
+
if is_valid:
|
| 249 |
+
valid_count += 1
|
| 250 |
+
else:
|
| 251 |
+
for err in errors:
|
| 252 |
+
error_counts[err] += 1
|
| 253 |
+
if verbose:
|
| 254 |
+
print(f"[ERROR] Sample {idx}: {err}")
|
| 255 |
+
|
| 256 |
+
# Summary
|
| 257 |
+
checked = len(samples_to_check)
|
| 258 |
+
invalid = checked - valid_count
|
| 259 |
+
|
| 260 |
+
print(f"\n{'=' * 60}")
|
| 261 |
+
print("VALIDATION SUMMARY")
|
| 262 |
+
print("=" * 60)
|
| 263 |
+
print(f"Samples checked: {checked}/{total}")
|
| 264 |
+
print(f"Valid: {valid_count} ({100*valid_count/checked:.1f}%)")
|
| 265 |
+
print(f"Invalid: {invalid} ({100*invalid/checked:.1f}%)")
|
| 266 |
+
|
| 267 |
+
if error_counts:
|
| 268 |
+
print(f"\nError breakdown:")
|
| 269 |
+
for err, count in sorted(error_counts.items(), key=lambda x: -x[1]):
|
| 270 |
+
print(f" {count:5d}x {err}")
|
| 271 |
+
|
| 272 |
+
# Compute and print statistics
|
| 273 |
+
stats = compute_statistics(samples_to_check)
|
| 274 |
+
print_statistics(stats)
|
| 275 |
+
|
| 276 |
+
# Check a sample for inspection
|
| 277 |
+
if verbose and valid_count > 0:
|
| 278 |
+
print(f"\n{'=' * 60}")
|
| 279 |
+
print("SAMPLE INSPECTION (first valid sample)")
|
| 280 |
+
print("=" * 60)
|
| 281 |
+
for idx, sample in enumerate(samples_to_check):
|
| 282 |
+
is_valid, _ = validate_sample(idx, sample)
|
| 283 |
+
if is_valid:
|
| 284 |
+
print(f"Sample {idx}:")
|
| 285 |
+
for key, value in sample.items():
|
| 286 |
+
if isinstance(value, torch.Tensor):
|
| 287 |
+
print(f" {key}: Tensor {value.shape} {value.dtype}")
|
| 288 |
+
elif isinstance(value, str):
|
| 289 |
+
preview = value[:100] + "..." if len(value) > 100 else value
|
| 290 |
+
print(f" {key}: '{preview}'")
|
| 291 |
+
elif isinstance(value, list):
|
| 292 |
+
print(f" {key}: list[{len(value)}]")
|
| 293 |
+
else:
|
| 294 |
+
print(f" {key}: {type(value).__name__}")
|
| 295 |
+
break
|
| 296 |
+
|
| 297 |
+
print("\n" + "=" * 60)
|
| 298 |
+
if invalid == 0:
|
| 299 |
+
print("RESULT: PASSED - All samples valid")
|
| 300 |
+
return True
|
| 301 |
+
else:
|
| 302 |
+
print(f"RESULT: FAILED - {invalid} invalid samples")
|
| 303 |
+
return False
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def main():
|
| 307 |
+
parser = argparse.ArgumentParser(description="Validate dataset for training")
|
| 308 |
+
parser.add_argument("--path", type=str, required=True, help="Path to dataset .pt file")
|
| 309 |
+
parser.add_argument("--max-samples", type=int, default=None, help="Max samples to validate (default: all)")
|
| 310 |
+
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
|
| 311 |
+
args = parser.parse_args()
|
| 312 |
+
|
| 313 |
+
success = validate_dataset(args.path, args.max_samples, args.verbose)
|
| 314 |
+
sys.exit(0 if success else 1)
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
if __name__ == "__main__":
|
| 318 |
+
main()
|
passo0_setup.py
CHANGED
|
@@ -5,7 +5,13 @@ Instala todas as dependências para:
|
|
| 5 |
- Geração de dataset (Soprano TTS, Whisper, SNAC, NeMo NFA)
|
| 6 |
- Treinamento do modelo Speech-to-Speech
|
| 7 |
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
Usage:
|
| 11 |
python passo0_setup.py [--skip_test]
|
|
@@ -51,13 +57,15 @@ def get_gpu_info():
|
|
| 51 |
|
| 52 |
def needs_cuda_128(gpu_name, compute_cap):
|
| 53 |
"""Check if GPU needs CUDA 12.8+ (Hopper/Blackwell architecture)."""
|
| 54 |
-
# H100, H200 = Hopper (sm_90)
|
| 55 |
-
|
|
|
|
| 56 |
if any(arch in gpu_name for arch in hopper_blackwell):
|
| 57 |
return True
|
| 58 |
-
# Compute capability 9.0+ needs CUDA 12.8
|
| 59 |
try:
|
| 60 |
-
|
|
|
|
| 61 |
return True
|
| 62 |
except:
|
| 63 |
pass
|
|
@@ -85,13 +93,16 @@ def main():
|
|
| 85 |
run("apt-get update -qq && apt-get install -y -qq espeak-ng espeak libsndfile1 ffmpeg git wget curl build-essential sox libsox-fmt-all", check=False)
|
| 86 |
|
| 87 |
# 2. PyTorch (version depends on GPU architecture)
|
| 88 |
-
|
|
|
|
| 89 |
if use_cuda_128:
|
| 90 |
# Hopper (H100/H200) and Blackwell (RTX 50xx) need CUDA 12.8
|
| 91 |
-
|
|
|
|
| 92 |
else:
|
| 93 |
# Ampere (A100, RTX 30xx) and Ada (RTX 40xx) can use CUDA 12.4
|
| 94 |
-
|
|
|
|
| 95 |
|
| 96 |
# 3. Core packages
|
| 97 |
log("\n[3/7] Core packages...")
|
|
@@ -116,11 +127,24 @@ def main():
|
|
| 116 |
# We use native Whisper from transformers (already installed above)
|
| 117 |
# No whisperx/ctranslate2 needed - they don't support H200 (sm_90)
|
| 118 |
|
| 119 |
-
# 6. Soprano TTS
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
run("pip install soprano-tts -q")
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
# 7. Create directories
|
| 126 |
log("\n[7/8] Creating directories...")
|
|
|
|
| 5 |
- Geração de dataset (Soprano TTS, Whisper, SNAC, NeMo NFA)
|
| 6 |
- Treinamento do modelo Speech-to-Speech
|
| 7 |
|
| 8 |
+
PyTorch 2.7.0 com suporte oficial a Blackwell (sm_120)
|
| 9 |
+
|
| 10 |
+
Suporta:
|
| 11 |
+
- Blackwell: RTX 5090, 5080, 5070, B100, B200 (sm_120) - CUDA 12.8
|
| 12 |
+
- Hopper: H100, H200 (sm_90) - CUDA 12.8
|
| 13 |
+
- Ada: RTX 4090, 4080, L40S (sm_89) - CUDA 12.4
|
| 14 |
+
- Ampere: A100, RTX 3090 (sm_80/86) - CUDA 12.4
|
| 15 |
|
| 16 |
Usage:
|
| 17 |
python passo0_setup.py [--skip_test]
|
|
|
|
| 57 |
|
| 58 |
def needs_cuda_128(gpu_name, compute_cap):
|
| 59 |
"""Check if GPU needs CUDA 12.8+ (Hopper/Blackwell architecture)."""
|
| 60 |
+
# H100, H200 = Hopper (sm_90)
|
| 61 |
+
# RTX 50xx, B100, B200 = Blackwell (sm_120) - requires PyTorch 2.7+
|
| 62 |
+
hopper_blackwell = ["H100", "H200", "RTX 50", "5090", "5080", "5070", "B100", "B200"]
|
| 63 |
if any(arch in gpu_name for arch in hopper_blackwell):
|
| 64 |
return True
|
| 65 |
+
# Compute capability 9.0+ (Hopper) or 12.0+ (Blackwell) needs CUDA 12.8
|
| 66 |
try:
|
| 67 |
+
cap = float(compute_cap)
|
| 68 |
+
if cap >= 9.0: # sm_90 (Hopper) or sm_120 (Blackwell)
|
| 69 |
return True
|
| 70 |
except:
|
| 71 |
pass
|
|
|
|
| 93 |
run("apt-get update -qq && apt-get install -y -qq espeak-ng espeak libsndfile1 ffmpeg git wget curl build-essential sox libsox-fmt-all", check=False)
|
| 94 |
|
| 95 |
# 2. PyTorch (version depends on GPU architecture)
|
| 96 |
+
# PyTorch 2.7.0 has official Blackwell (sm_120) support
|
| 97 |
+
log("\n[2/7] PyTorch 2.7.0 (Blackwell compatible)...")
|
| 98 |
if use_cuda_128:
|
| 99 |
# Hopper (H100/H200) and Blackwell (RTX 50xx) need CUDA 12.8
|
| 100 |
+
log(" Installing PyTorch 2.7.0 with CUDA 12.8 (Blackwell/Hopper support)")
|
| 101 |
+
run("pip install torch==2.7.0 torchaudio==2.7.0 torchvision==0.22.0 --index-url https://download.pytorch.org/whl/cu128 -q")
|
| 102 |
else:
|
| 103 |
# Ampere (A100, RTX 30xx) and Ada (RTX 40xx) can use CUDA 12.4
|
| 104 |
+
log(" Installing PyTorch 2.7.0 with CUDA 12.4 (Ampere/Ada)")
|
| 105 |
+
run("pip install torch==2.7.0 torchaudio==2.7.0 torchvision==0.22.0 --index-url https://download.pytorch.org/whl/cu124 -q")
|
| 106 |
|
| 107 |
# 3. Core packages
|
| 108 |
log("\n[3/7] Core packages...")
|
|
|
|
| 127 |
# We use native Whisper from transformers (already installed above)
|
| 128 |
# No whisperx/ctranslate2 needed - they don't support H200 (sm_90)
|
| 129 |
|
| 130 |
+
# 6. Soprano TTS 80M (ultra-lightweight, 2000x realtime)
|
| 131 |
+
# See: https://github.com/ekwek1/soprano
|
| 132 |
+
log("\n[6/8] Soprano TTS 80M...")
|
| 133 |
+
|
| 134 |
+
# Install soprano-tts with lmdeploy for fastest inference
|
| 135 |
+
# lmdeploy provides 2000x realtime speed vs ~10x for transformers backend
|
| 136 |
run("pip install soprano-tts -q")
|
| 137 |
+
|
| 138 |
+
# lmdeploy doesn't support Blackwell (RTX 50xx) yet, so only install on supported GPUs
|
| 139 |
+
blackwell_gpus = ["RTX 50", "5090", "5080", "5070", "B100", "B200"]
|
| 140 |
+
is_blackwell = any(arch in gpu_name for arch in blackwell_gpus)
|
| 141 |
+
|
| 142 |
+
if is_blackwell:
|
| 143 |
+
log(" Blackwell GPU detected - skipping lmdeploy (not supported yet)")
|
| 144 |
+
log(" Soprano will use transformers backend (slower but compatible)")
|
| 145 |
+
else:
|
| 146 |
+
log(" Installing lmdeploy for 2000x realtime speed...")
|
| 147 |
+
run("pip install lmdeploy -q")
|
| 148 |
|
| 149 |
# 7. Create directories
|
| 150 |
log("\n[7/8] Creating directories...")
|
passo2_finetune_stage1.py
CHANGED
|
@@ -8,862 +8,213 @@ Based on IST-LM paper combined with LLaMA-Omni 2 staging:
|
|
| 8 |
- LLM is completely frozen (adapter gets a "head start")
|
| 9 |
|
| 10 |
Usage:
|
| 11 |
-
python
|
| 12 |
|
| 13 |
Next: Stage 2 trains Adapter + LoRA together
|
| 14 |
"""
|
| 15 |
|
| 16 |
-
import os
|
| 17 |
-
import sys
|
| 18 |
import argparse
|
| 19 |
import torch
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
from
|
| 23 |
-
from
|
| 24 |
-
from
|
| 25 |
-
from
|
| 26 |
-
from
|
| 27 |
-
from huggingface_hub import login
|
| 28 |
-
from tqdm import tqdm
|
| 29 |
-
import threading
|
| 30 |
-
|
| 31 |
-
# ============================================================
|
| 32 |
-
# Config
|
| 33 |
-
# ============================================================
|
| 34 |
-
torch.backends.cuda.matmul.allow_tf32 = True
|
| 35 |
-
torch.backends.cudnn.allow_tf32 = True
|
| 36 |
-
torch.backends.cudnn.benchmark = True
|
| 37 |
-
torch.set_float32_matmul_precision('high')
|
| 38 |
-
|
| 39 |
-
# SNAC token offsets for Orpheus
|
| 40 |
-
SNAC_BASE_OFFSET = 128266
|
| 41 |
-
EOS_TOKEN = 128009
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def log(msg):
|
| 45 |
-
print(msg)
|
| 46 |
-
sys.stdout.flush()
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def apply_snac_offset(token_idx, position):
|
| 50 |
-
"""Apply position-based offset to SNAC token.
|
| 51 |
-
If token is already offset (>= SNAC_BASE_OFFSET), return as-is.
|
| 52 |
-
"""
|
| 53 |
-
if int(token_idx) >= SNAC_BASE_OFFSET:
|
| 54 |
-
# Already has offset applied
|
| 55 |
-
return int(token_idx)
|
| 56 |
-
offset = SNAC_BASE_OFFSET + (position % 7) * 4096
|
| 57 |
-
return int(token_idx) + offset
|
| 58 |
|
| 59 |
|
| 60 |
-
|
| 61 |
"""
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
| 63 |
"""
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
elif isinstance(obj, dict):
|
| 82 |
-
return {k: copy_to_cpu(v) for k, v in obj.items()}
|
| 83 |
-
return obj
|
| 84 |
-
|
| 85 |
-
state_copy = copy_to_cpu(state_dict)
|
| 86 |
-
|
| 87 |
-
def _save():
|
| 88 |
-
try:
|
| 89 |
-
torch.save(state_copy, path)
|
| 90 |
-
if is_main:
|
| 91 |
-
log(f"[ASYNC] Saved: {path}")
|
| 92 |
-
except Exception as e:
|
| 93 |
-
if is_main:
|
| 94 |
-
log(f"[ASYNC] Error: {e}")
|
| 95 |
-
|
| 96 |
-
thread = threading.Thread(target=_save, daemon=True)
|
| 97 |
-
thread.start()
|
| 98 |
-
_save_threads.append(thread)
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
def wait_for_checkpoints():
|
| 102 |
-
global _save_threads
|
| 103 |
-
for t in _save_threads:
|
| 104 |
-
t.join()
|
| 105 |
-
_save_threads = []
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
# ============================================================
|
| 109 |
-
# GPU Auto-Detection (Multi-backend: CUDA, ROCm, MPS, XPU)
|
| 110 |
-
# ============================================================
|
| 111 |
-
def auto_detect_gpu_config():
|
| 112 |
-
"""Detect GPU and return optimal batch size config."""
|
| 113 |
-
vram_gb = 0
|
| 114 |
-
gpu_name = "Unknown"
|
| 115 |
-
|
| 116 |
-
# Try CUDA (NVIDIA)
|
| 117 |
-
if torch.cuda.is_available():
|
| 118 |
-
try:
|
| 119 |
-
props = torch.cuda.get_device_properties(0)
|
| 120 |
-
vram_gb = props.total_memory // (1024**3)
|
| 121 |
-
gpu_name = props.name
|
| 122 |
-
except:
|
| 123 |
-
pass
|
| 124 |
-
|
| 125 |
-
# Try MPS (Apple Silicon)
|
| 126 |
-
elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
|
| 127 |
-
gpu_name = "Apple Silicon (MPS)"
|
| 128 |
-
# Apple Silicon unified memory - estimate based on system
|
| 129 |
-
try:
|
| 130 |
-
import subprocess
|
| 131 |
-
result = subprocess.run(['sysctl', '-n', 'hw.memsize'], capture_output=True, text=True)
|
| 132 |
-
total_mem = int(result.stdout.strip()) // (1024**3)
|
| 133 |
-
vram_gb = total_mem // 2 # Assume half for GPU
|
| 134 |
-
except:
|
| 135 |
-
vram_gb = 8 # Conservative default
|
| 136 |
-
|
| 137 |
-
# Try ROCm (AMD) - uses same API as CUDA
|
| 138 |
-
elif hasattr(torch, 'hip') or os.environ.get('ROCM_HOME'):
|
| 139 |
-
try:
|
| 140 |
-
if torch.cuda.is_available(): # ROCm exposes as CUDA
|
| 141 |
-
props = torch.cuda.get_device_properties(0)
|
| 142 |
-
vram_gb = props.total_memory // (1024**3)
|
| 143 |
-
gpu_name = f"AMD {props.name}"
|
| 144 |
-
except:
|
| 145 |
-
gpu_name = "AMD ROCm"
|
| 146 |
-
vram_gb = 16
|
| 147 |
-
|
| 148 |
-
# Fallback: try nvidia-smi
|
| 149 |
-
if vram_gb == 0:
|
| 150 |
-
try:
|
| 151 |
-
import subprocess
|
| 152 |
-
result = subprocess.run(
|
| 153 |
-
['nvidia-smi', '--query-gpu=name,memory.total', '--format=csv,noheader,nounits'],
|
| 154 |
-
capture_output=True, text=True
|
| 155 |
-
)
|
| 156 |
-
lines = result.stdout.strip().split('\n')
|
| 157 |
-
gpu_name, vram_mb = lines[0].split(', ')
|
| 158 |
-
vram_gb = int(vram_mb) // 1024
|
| 159 |
-
except:
|
| 160 |
-
pass
|
| 161 |
-
|
| 162 |
-
# Determine batch size based on VRAM
|
| 163 |
-
if vram_gb >= 150:
|
| 164 |
-
return {"name": gpu_name, "batch_size": 8, "grad_accum": 4, "vram_gb": vram_gb}
|
| 165 |
-
elif vram_gb >= 80:
|
| 166 |
-
return {"name": gpu_name, "batch_size": 6, "grad_accum": 5, "vram_gb": vram_gb}
|
| 167 |
-
elif vram_gb >= 35:
|
| 168 |
-
return {"name": gpu_name, "batch_size": 4, "grad_accum": 8, "vram_gb": vram_gb}
|
| 169 |
-
elif vram_gb >= 16:
|
| 170 |
-
return {"name": gpu_name, "batch_size": 2, "grad_accum": 16, "vram_gb": vram_gb}
|
| 171 |
-
else:
|
| 172 |
-
return {"name": gpu_name, "batch_size": 1, "grad_accum": 32, "vram_gb": max(vram_gb, 8)}
|
| 173 |
-
|
| 174 |
-
def get_ram_info():
|
| 175 |
-
"""Get RAM info in GB."""
|
| 176 |
-
try:
|
| 177 |
-
import psutil
|
| 178 |
-
total = psutil.virtual_memory().total / 1024**3
|
| 179 |
-
available = psutil.virtual_memory().available / 1024**3
|
| 180 |
-
return total, available
|
| 181 |
-
except ImportError:
|
| 182 |
-
try:
|
| 183 |
-
import subprocess
|
| 184 |
-
result = subprocess.run(
|
| 185 |
-
['free', '-g', '--output=SIZE,AVAILABLE'],
|
| 186 |
-
capture_output=True, text=True
|
| 187 |
)
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
pass
|
| 196 |
-
return 0, 0
|
| 197 |
-
|
| 198 |
-
def limit_ram_usage(max_ram_gb):
|
| 199 |
-
"""Limit RAM usage by setting resource limits."""
|
| 200 |
-
try:
|
| 201 |
-
import resource
|
| 202 |
-
max_bytes = int(max_ram_gb * 1024**3)
|
| 203 |
-
resource.setrlimit(resource.RLIMIT_AS, (max_bytes, max_bytes))
|
| 204 |
-
except:
|
| 205 |
-
pass
|
| 206 |
-
|
| 207 |
-
def log_memory_usage():
|
| 208 |
-
"""Log current memory usage."""
|
| 209 |
-
msg = []
|
| 210 |
-
if torch.cuda.is_available():
|
| 211 |
-
used = torch.cuda.memory_allocated() / 1024**3
|
| 212 |
-
reserved = torch.cuda.memory_reserved() / 1024**3
|
| 213 |
-
msg.append(f"GPU: {used:.2f}GB / {reserved:.2f}GB")
|
| 214 |
-
try:
|
| 215 |
-
import psutil
|
| 216 |
-
ram_used = psutil.virtual_memory().used / 1024**3
|
| 217 |
-
ram_total = psutil.virtual_memory().total / 1024**3
|
| 218 |
-
msg.append(f"RAM: {ram_used:.1f}GB / {ram_total:.1f}GB")
|
| 219 |
-
except:
|
| 220 |
-
pass
|
| 221 |
-
return " | ".join(msg)
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
# ============================================================
|
| 225 |
-
# Speech Adapter (LLaMA-Omni 2 Style)
|
| 226 |
-
# ============================================================
|
| 227 |
-
class SpeechAdapter(nn.Module):
|
| 228 |
-
"""
|
| 229 |
-
5× downsampling + FFN with intermediate dim 2048
|
| 230 |
-
"""
|
| 231 |
-
def __init__(self, whisper_dim=1280, llm_dim=3072, downsample=5, intermediate_dim=2048):
|
| 232 |
-
super().__init__()
|
| 233 |
-
self.downsample = downsample
|
| 234 |
-
concat_dim = whisper_dim * downsample
|
| 235 |
-
|
| 236 |
-
self.ffn = nn.Sequential(
|
| 237 |
-
nn.Linear(concat_dim, intermediate_dim),
|
| 238 |
-
nn.GELU(),
|
| 239 |
-
nn.Linear(intermediate_dim, llm_dim),
|
| 240 |
-
nn.LayerNorm(llm_dim)
|
| 241 |
)
|
| 242 |
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
T_new = (T // self.downsample) * self.downsample
|
| 246 |
-
x = x[:, :T_new]
|
| 247 |
-
x = x.reshape(B, T_new // self.downsample, D * self.downsample)
|
| 248 |
-
return self.ffn(x)
|
| 249 |
|
|
|
|
|
|
|
| 250 |
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
"""
|
| 256 |
-
Create interleaved sequence based on text_ratio with word-level alignment.
|
| 257 |
-
- text_ratio=0.9 means 90% of words are replaced by text tokens
|
| 258 |
-
- text_ratio=0.0 means 100% audio (no text replacement)
|
| 259 |
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
word = alignment['word']
|
| 304 |
-
start_frame = alignment['start_frame']
|
| 305 |
-
end_frame = min(alignment['end_frame'], total_frames)
|
| 306 |
-
|
| 307 |
-
if word_idx in text_word_indices:
|
| 308 |
-
# Replace this word's audio with text tokens
|
| 309 |
-
# Use pre-computed tokens if available, else tokenize on-the-fly
|
| 310 |
-
word_tokens = alignment.get('tokens', [])
|
| 311 |
-
if not word_tokens and tokenizer:
|
| 312 |
-
word_tokens = tokenizer.encode(word, add_special_tokens=False)
|
| 313 |
-
|
| 314 |
-
for tok in word_tokens:
|
| 315 |
-
interleaved.append(tok)
|
| 316 |
-
is_audio_mask.append(False)
|
| 317 |
-
# Skip the audio frames for this word
|
| 318 |
-
snac_position = end_frame * 7
|
| 319 |
-
else:
|
| 320 |
-
# Keep audio for this word
|
| 321 |
-
for f_idx in range(start_frame, end_frame):
|
| 322 |
-
if f_idx < total_frames:
|
| 323 |
-
frame = frames[f_idx]
|
| 324 |
-
for tok in frame:
|
| 325 |
-
interleaved.append(apply_snac_offset(tok, snac_position))
|
| 326 |
-
is_audio_mask.append(True)
|
| 327 |
-
snac_position += 1
|
| 328 |
-
|
| 329 |
-
frame_idx = end_frame
|
| 330 |
-
|
| 331 |
-
# Add any remaining frames after the last word
|
| 332 |
-
while frame_idx < total_frames:
|
| 333 |
-
frame = frames[frame_idx]
|
| 334 |
-
for tok in frame:
|
| 335 |
-
interleaved.append(apply_snac_offset(tok, snac_position))
|
| 336 |
-
is_audio_mask.append(True)
|
| 337 |
-
snac_position += 1
|
| 338 |
-
frame_idx += 1
|
| 339 |
-
|
| 340 |
-
else:
|
| 341 |
-
# Fallback: positional interleaving (original behavior)
|
| 342 |
-
total_text = len(text_tokens)
|
| 343 |
-
|
| 344 |
-
# Determine interleaving pattern based on text_ratio
|
| 345 |
-
if text_ratio >= 0.9:
|
| 346 |
-
text_per_chunk, frames_per_chunk = 1, 3
|
| 347 |
-
elif text_ratio >= 0.7:
|
| 348 |
-
text_per_chunk, frames_per_chunk = 1, 5
|
| 349 |
-
elif text_ratio >= 0.5:
|
| 350 |
-
text_per_chunk, frames_per_chunk = 1, 7
|
| 351 |
-
elif text_ratio >= 0.3:
|
| 352 |
-
text_per_chunk, frames_per_chunk = 1, 10
|
| 353 |
-
else:
|
| 354 |
-
text_per_chunk, frames_per_chunk = 0, 1
|
| 355 |
-
|
| 356 |
-
text_idx = 0
|
| 357 |
-
frame_idx = 0
|
| 358 |
-
snac_position = 0
|
| 359 |
-
|
| 360 |
-
while frame_idx < total_frames:
|
| 361 |
-
if text_per_chunk > 0 and text_idx < total_text:
|
| 362 |
-
for _ in range(text_per_chunk):
|
| 363 |
-
if text_idx < total_text:
|
| 364 |
-
interleaved.append(text_tokens[text_idx])
|
| 365 |
-
is_audio_mask.append(False)
|
| 366 |
-
text_idx += 1
|
| 367 |
-
|
| 368 |
-
for _ in range(frames_per_chunk):
|
| 369 |
-
if frame_idx < total_frames:
|
| 370 |
-
frame = frames[frame_idx]
|
| 371 |
-
for tok in frame:
|
| 372 |
-
interleaved.append(apply_snac_offset(tok, snac_position))
|
| 373 |
-
is_audio_mask.append(True)
|
| 374 |
-
snac_position += 1
|
| 375 |
-
frame_idx += 1
|
| 376 |
-
|
| 377 |
-
while text_idx < total_text:
|
| 378 |
-
interleaved.append(text_tokens[text_idx])
|
| 379 |
-
is_audio_mask.append(False)
|
| 380 |
-
text_idx += 1
|
| 381 |
-
|
| 382 |
-
# Add EOS
|
| 383 |
-
interleaved.append(EOS_TOKEN)
|
| 384 |
-
is_audio_mask.append(False)
|
| 385 |
-
|
| 386 |
-
return interleaved, is_audio_mask
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
# ============================================================
|
| 390 |
-
# Dataset
|
| 391 |
-
# ============================================================
|
| 392 |
-
class InterleavedDataset(Dataset):
|
| 393 |
-
def __init__(self, data, tokenizer, max_audio_len=500, max_seq_len=2048):
|
| 394 |
-
self.data = data
|
| 395 |
-
self.tokenizer = tokenizer
|
| 396 |
-
self.max_audio = max_audio_len * 5
|
| 397 |
-
self.max_seq_len = max_seq_len
|
| 398 |
-
|
| 399 |
-
def __len__(self):
|
| 400 |
-
return len(self.data)
|
| 401 |
-
|
| 402 |
-
def __getitem__(self, idx):
|
| 403 |
-
item = self.data[idx]
|
| 404 |
-
|
| 405 |
-
# Whisper features
|
| 406 |
-
whisper = item["whisper_features"][:self.max_audio]
|
| 407 |
-
|
| 408 |
-
# Text tokens - use pre-computed if available, otherwise tokenize
|
| 409 |
-
if "text_tokens" in item and len(item["text_tokens"]) > 0:
|
| 410 |
-
tt = item["text_tokens"]
|
| 411 |
-
text_tokens = tt.tolist() if hasattr(tt, 'tolist') else list(tt)
|
| 412 |
-
else:
|
| 413 |
-
text = item.get("answer", item.get("text", ""))
|
| 414 |
-
if isinstance(text, str) and len(text) > 0:
|
| 415 |
-
text_tokens = self.tokenizer.encode(text, add_special_tokens=False)
|
| 416 |
-
else:
|
| 417 |
-
text_tokens = []
|
| 418 |
-
|
| 419 |
-
# SNAC tokens
|
| 420 |
-
snac = item["snac_tokens"]
|
| 421 |
-
snac_len = (len(snac) // 7) * 7
|
| 422 |
-
snac = snac[:snac_len] if snac_len > 0 else snac[:7]
|
| 423 |
-
snac_list = snac.tolist() if hasattr(snac, 'tolist') else list(snac)
|
| 424 |
-
|
| 425 |
-
# Word alignments (if available)
|
| 426 |
-
word_alignments = item.get("word_alignments", None)
|
| 427 |
-
answer_text = item.get("answer", "")
|
| 428 |
-
|
| 429 |
-
return {
|
| 430 |
-
"whisper": whisper,
|
| 431 |
-
"text_tokens": text_tokens,
|
| 432 |
-
"snac_tokens": snac_list,
|
| 433 |
-
"word_alignments": word_alignments,
|
| 434 |
-
"answer_text": answer_text
|
| 435 |
-
}
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
def collate_fn(batch, text_ratio=0.9, tokenizer=None):
|
| 439 |
-
"""Collate with dynamic interleaving based on text_ratio and word alignments."""
|
| 440 |
-
max_w = max(b["whisper"].shape[0] for b in batch)
|
| 441 |
-
max_w = ((max_w + 4) // 5) * 5
|
| 442 |
-
|
| 443 |
-
whisper_batch = []
|
| 444 |
-
interleaved_batch = []
|
| 445 |
-
is_audio_batch = []
|
| 446 |
-
|
| 447 |
-
max_seq = 0
|
| 448 |
-
sequences = []
|
| 449 |
-
|
| 450 |
-
for b in batch:
|
| 451 |
-
interleaved, is_audio = create_interleaved_sequence(
|
| 452 |
-
b["text_tokens"],
|
| 453 |
-
b["snac_tokens"],
|
| 454 |
-
text_ratio,
|
| 455 |
-
word_alignments=b.get("word_alignments"),
|
| 456 |
-
tokenizer=tokenizer,
|
| 457 |
-
answer_text=b.get("answer_text")
|
| 458 |
)
|
| 459 |
-
sequences.append((interleaved, is_audio))
|
| 460 |
-
max_seq = max(max_seq, len(interleaved))
|
| 461 |
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
|
|
|
| 466 |
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
|
| 471 |
-
|
| 472 |
-
|
| 473 |
|
| 474 |
-
|
| 475 |
-
|
| 476 |
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
|
| 483 |
|
| 484 |
-
# ============================================================
|
| 485 |
-
# Arguments
|
| 486 |
-
# ============================================================
|
| 487 |
def parse_args():
|
| 488 |
-
|
| 489 |
-
parser
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
parser.add_argument("--output_dir", type=str, default="./checkpoints")
|
|
|
|
|
|
|
| 491 |
parser.add_argument("--lr", type=float, default=5e-5)
|
| 492 |
-
parser.add_argument("--epochs", type=int, default=2,
|
|
|
|
| 493 |
parser.add_argument("--batch_size", type=int, default=None)
|
| 494 |
parser.add_argument("--grad_accum", type=int, default=None)
|
| 495 |
parser.add_argument("--warmup_ratio", type=float, default=0.03)
|
| 496 |
parser.add_argument("--max_grad_norm", type=float, default=1.0)
|
| 497 |
-
parser.add_argument("--save_steps", type=int, default=200)
|
| 498 |
parser.add_argument("--label_smoothing", type=float, default=0.1)
|
|
|
|
|
|
|
| 499 |
# Scheduled interleaving
|
| 500 |
parser.add_argument("--initial_text_ratio", type=float, default=0.9)
|
| 501 |
parser.add_argument("--decay_steps", type=int, default=300)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
# Model
|
| 503 |
-
parser.add_argument("--model_path", type=str,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
parser.add_argument("--resume", type=str, default=None)
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
parser.add_argument("--
|
|
|
|
|
|
|
|
|
|
| 508 |
# Modes
|
| 509 |
parser.add_argument("--demo", action="store_true")
|
| 510 |
parser.add_argument("--test", action="store_true")
|
| 511 |
-
|
| 512 |
-
parser.add_argument("--gradient_checkpointing", action="store_true", help="Enable gradient checkpointing (auto-enabled if VRAM < 20GB)")
|
| 513 |
return parser.parse_args()
|
| 514 |
|
| 515 |
|
| 516 |
-
# ============================================================
|
| 517 |
-
# Main
|
| 518 |
-
# ============================================================
|
| 519 |
def main():
|
| 520 |
args = parse_args()
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
# Initialize Accelerator (auto-detects CUDA, ROCm, MPS, XPU)
|
| 530 |
-
accelerator = Accelerator(
|
| 531 |
-
gradient_accumulation_steps=args.grad_accum if args.grad_accum else 1,
|
| 532 |
-
mixed_precision=mixed_precision,
|
| 533 |
-
)
|
| 534 |
-
|
| 535 |
-
device = accelerator.device
|
| 536 |
-
is_main = accelerator.is_main_process
|
| 537 |
-
|
| 538 |
-
# Set seed for reproducibility
|
| 539 |
-
set_seed(42)
|
| 540 |
-
|
| 541 |
-
# GPU config
|
| 542 |
-
gpu_config = auto_detect_gpu_config()
|
| 543 |
-
if args.batch_size is None:
|
| 544 |
-
args.batch_size = gpu_config["batch_size"]
|
| 545 |
-
if args.grad_accum is None:
|
| 546 |
-
args.grad_accum = gpu_config["grad_accum"]
|
| 547 |
-
# Update accelerator's gradient accumulation
|
| 548 |
-
accelerator.gradient_accumulation_steps = args.grad_accum
|
| 549 |
-
|
| 550 |
-
# Use bf16 for CUDA, fp32 for MPS/others
|
| 551 |
-
torch_dtype = torch.bfloat16 if device.type == 'cuda' else torch.float32
|
| 552 |
-
|
| 553 |
-
# Get RAM info and set limits
|
| 554 |
-
ram_total, ram_available = get_ram_info()
|
| 555 |
-
if args.ram_limit_gb is None:
|
| 556 |
-
args.ram_limit_gb = ram_total * 0.80 # Default to 80% of RAM
|
| 557 |
-
limit_ram_usage(args.ram_limit_gb)
|
| 558 |
-
|
| 559 |
-
if is_main:
|
| 560 |
-
log("=" * 60)
|
| 561 |
-
log("STAGE 1: Adapter Only (LLM Frozen) + Interleaved Output")
|
| 562 |
-
log("=" * 60)
|
| 563 |
-
log(f"Device: {device} ({accelerator.device.type})")
|
| 564 |
-
log(f"GPU: {gpu_config['name']} ({gpu_config['vram_gb']}GB)")
|
| 565 |
-
log(f"Num processes: {accelerator.num_processes}")
|
| 566 |
-
log(f"RAM: {ram_total:.1f}GB total, {ram_available:.1f}GB available")
|
| 567 |
-
log(f"Batch: {args.batch_size}, Grad accum: {args.grad_accum}")
|
| 568 |
-
log(f"LR: {args.lr}, Epochs: {args.epochs}")
|
| 569 |
-
log(f"Initial text ratio: {args.initial_text_ratio}")
|
| 570 |
-
log(f"Decay steps: {args.decay_steps}")
|
| 571 |
-
|
| 572 |
-
# Apply memory limits for CUDA
|
| 573 |
-
if device.type == 'cuda':
|
| 574 |
-
torch.cuda.set_per_process_memory_fraction(args.vram_fraction)
|
| 575 |
-
torch.cuda.empty_cache()
|
| 576 |
-
torch.backends.cudnn.benchmark = True
|
| 577 |
-
torch.set_float32_matmul_precision('high')
|
| 578 |
-
if is_main:
|
| 579 |
-
log(f"[MEMORY] VRAM limited to {args.vram_fraction*100:.0f}%")
|
| 580 |
-
log(f"[MEMORY] RAM limited to {args.ram_limit_gb:.1f}GB")
|
| 581 |
-
|
| 582 |
-
# HuggingFace login
|
| 583 |
-
hf_token = os.environ.get("HF_TOKEN")
|
| 584 |
-
if hf_token:
|
| 585 |
-
login(token=hf_token)
|
| 586 |
-
|
| 587 |
-
# Load tokenizer
|
| 588 |
-
tokenizer = AutoTokenizer.from_pretrained(args.model_path)
|
| 589 |
-
if tokenizer.pad_token is None:
|
| 590 |
-
tokenizer.pad_token = tokenizer.eos_token
|
| 591 |
-
|
| 592 |
-
# Load datasets
|
| 593 |
-
data_paths = [p.strip() for p in args.data.split(",")]
|
| 594 |
-
all_datasets = []
|
| 595 |
-
|
| 596 |
-
if is_main:
|
| 597 |
-
log("\nLoading datasets...")
|
| 598 |
-
|
| 599 |
-
for path in data_paths:
|
| 600 |
-
if os.path.exists(path):
|
| 601 |
-
data = torch.load(path, weights_only=False, mmap=True)
|
| 602 |
-
dataset = InterleavedDataset(data, tokenizer)
|
| 603 |
-
all_datasets.append(dataset)
|
| 604 |
-
if is_main:
|
| 605 |
-
log(f" {os.path.basename(path)}: {len(data):,} samples")
|
| 606 |
-
|
| 607 |
-
if len(all_datasets) == 0:
|
| 608 |
-
raise ValueError("No datasets loaded!")
|
| 609 |
-
|
| 610 |
-
combined_dataset = ConcatDataset(all_datasets) if len(all_datasets) > 1 else all_datasets[0]
|
| 611 |
-
|
| 612 |
-
# Demo/Test mode
|
| 613 |
-
if args.test:
|
| 614 |
-
combined_dataset = torch.utils.data.Subset(combined_dataset, range(min(5, len(combined_dataset))))
|
| 615 |
-
args.batch_size = min(args.batch_size, len(combined_dataset))
|
| 616 |
-
args.grad_accum = 1
|
| 617 |
-
elif args.demo:
|
| 618 |
-
combined_dataset = torch.utils.data.Subset(combined_dataset, range(min(1000, len(combined_dataset))))
|
| 619 |
-
args.batch_size = min(4, args.batch_size)
|
| 620 |
-
args.grad_accum = max(8, args.grad_accum)
|
| 621 |
-
|
| 622 |
-
if is_main:
|
| 623 |
-
log(f"Total samples: {len(combined_dataset):,}")
|
| 624 |
-
|
| 625 |
-
# Load LLM (FROZEN)
|
| 626 |
-
if is_main:
|
| 627 |
-
log(f"\nLoading LLM (FROZEN): {args.model_path}")
|
| 628 |
-
|
| 629 |
-
llm = AutoModelForCausalLM.from_pretrained(
|
| 630 |
-
args.model_path,
|
| 631 |
-
torch_dtype=torch_dtype,
|
| 632 |
-
attn_implementation="sdpa",
|
| 633 |
-
)
|
| 634 |
-
|
| 635 |
-
# Freeze LLM completely
|
| 636 |
-
for p in llm.parameters():
|
| 637 |
-
p.requires_grad = False
|
| 638 |
-
llm.eval()
|
| 639 |
-
|
| 640 |
-
# Gradient checkpointing auto-detection
|
| 641 |
-
use_gradient_checkpointing = args.gradient_checkpointing
|
| 642 |
-
if not use_gradient_checkpointing and torch.cuda.is_available():
|
| 643 |
-
vram_gb = torch.cuda.get_device_properties(0).total_memory / 1024**3
|
| 644 |
-
if vram_gb < 20:
|
| 645 |
-
use_gradient_checkpointing = True
|
| 646 |
-
if is_main:
|
| 647 |
-
log(f"[AUTO] Enabling gradient checkpointing (VRAM={vram_gb:.1f}GB < 20GB)")
|
| 648 |
-
else:
|
| 649 |
-
if is_main:
|
| 650 |
-
log(f"[AUTO] Gradient checkpointing disabled for speed (VRAM={vram_gb:.1f}GB >= 20GB)")
|
| 651 |
-
|
| 652 |
-
if use_gradient_checkpointing:
|
| 653 |
-
llm.gradient_checkpointing_enable()
|
| 654 |
-
if is_main:
|
| 655 |
-
log("[MEMORY] Gradient checkpointing enabled")
|
| 656 |
-
|
| 657 |
-
# Create adapter (TRAINABLE)
|
| 658 |
-
adapter = SpeechAdapter(
|
| 659 |
-
whisper_dim=1280,
|
| 660 |
-
llm_dim=3072,
|
| 661 |
-
downsample=5,
|
| 662 |
-
intermediate_dim=2048
|
| 663 |
-
).to(dtype=torch_dtype)
|
| 664 |
-
|
| 665 |
-
adapter_params = sum(p.numel() for p in adapter.parameters())
|
| 666 |
-
if is_main:
|
| 667 |
-
log(f"\nTrainable: Adapter only ({adapter_params:,} = {adapter_params/1e6:.1f}M params)")
|
| 668 |
-
log("LLM: FROZEN")
|
| 669 |
-
|
| 670 |
-
# Optimizer (only adapter)
|
| 671 |
-
optimizer = torch.optim.AdamW(adapter.parameters(), lr=args.lr, weight_decay=0.01)
|
| 672 |
-
|
| 673 |
-
# Training state
|
| 674 |
-
global_step = 0
|
| 675 |
-
start_epoch = 0
|
| 676 |
-
best_loss = float("inf")
|
| 677 |
-
current_text_ratio = args.initial_text_ratio
|
| 678 |
-
|
| 679 |
-
# Resume
|
| 680 |
-
if args.resume and os.path.exists(args.resume):
|
| 681 |
-
if is_main:
|
| 682 |
-
log(f"\nResuming from: {args.resume}")
|
| 683 |
-
ckpt = torch.load(args.resume, map_location="cpu", weights_only=False)
|
| 684 |
-
if "adapter" in ckpt:
|
| 685 |
-
adapter.load_state_dict(ckpt["adapter"])
|
| 686 |
-
if "optimizer" in ckpt:
|
| 687 |
-
optimizer.load_state_dict(ckpt["optimizer"])
|
| 688 |
-
if "step" in ckpt:
|
| 689 |
-
global_step = ckpt["step"]
|
| 690 |
-
if "epoch" in ckpt:
|
| 691 |
-
start_epoch = ckpt["epoch"]
|
| 692 |
-
if "text_ratio" in ckpt:
|
| 693 |
-
current_text_ratio = ckpt["text_ratio"]
|
| 694 |
-
|
| 695 |
-
# Training
|
| 696 |
-
os.makedirs(args.output_dir, exist_ok=True)
|
| 697 |
-
|
| 698 |
-
# Create dataloader with dynamic collate
|
| 699 |
-
def collate_with_ratio(batch):
|
| 700 |
-
current_ratio = get_text_ratio(global_step, args.decay_steps, args.initial_text_ratio)
|
| 701 |
-
return collate_fn(batch, current_ratio, tokenizer=tokenizer)
|
| 702 |
-
|
| 703 |
-
train_loader = DataLoader(
|
| 704 |
-
combined_dataset,
|
| 705 |
-
batch_size=args.batch_size,
|
| 706 |
-
shuffle=True,
|
| 707 |
-
collate_fn=collate_with_ratio,
|
| 708 |
-
num_workers=4,
|
| 709 |
-
pin_memory=True
|
| 710 |
-
)
|
| 711 |
-
|
| 712 |
-
# Prepare with accelerator (handles DDP, device placement, etc.)
|
| 713 |
-
adapter, llm, optimizer, train_loader = accelerator.prepare(
|
| 714 |
-
adapter, llm, optimizer, train_loader
|
| 715 |
-
)
|
| 716 |
-
|
| 717 |
-
steps_per_epoch = max(1, len(train_loader) // args.grad_accum)
|
| 718 |
-
total_steps = max(1, steps_per_epoch * args.epochs)
|
| 719 |
-
warmup_steps = int(total_steps * args.warmup_ratio)
|
| 720 |
-
|
| 721 |
-
scheduler = CosineAnnealingLR(optimizer, T_max=max(1, total_steps - warmup_steps), eta_min=1e-6)
|
| 722 |
-
|
| 723 |
-
if is_main:
|
| 724 |
-
log(f"Steps per epoch: {steps_per_epoch}, Total: {total_steps}, Warmup: {warmup_steps}")
|
| 725 |
-
|
| 726 |
-
if is_main:
|
| 727 |
-
log("\n" + "=" * 60)
|
| 728 |
-
log("STARTING STAGE 1 TRAINING")
|
| 729 |
-
log("=" * 60)
|
| 730 |
-
|
| 731 |
-
for epoch in range(start_epoch, args.epochs):
|
| 732 |
-
# Update text ratio based on global step
|
| 733 |
-
current_text_ratio = get_text_ratio(global_step, args.decay_steps, args.initial_text_ratio)
|
| 734 |
-
|
| 735 |
-
# Get unwrapped adapter for forward pass
|
| 736 |
-
unwrapped_adapter = accelerator.unwrap_model(adapter)
|
| 737 |
-
unwrapped_llm = accelerator.unwrap_model(llm)
|
| 738 |
-
|
| 739 |
-
adapter.train()
|
| 740 |
-
epoch_loss = 0
|
| 741 |
-
accum_loss = 0
|
| 742 |
-
|
| 743 |
-
pbar = tqdm(train_loader, desc=f"Epoch {epoch+1}/{args.epochs}", disable=not is_main)
|
| 744 |
-
|
| 745 |
-
for batch_idx, batch in enumerate(pbar):
|
| 746 |
-
# Update text ratio dynamically
|
| 747 |
-
current_text_ratio = get_text_ratio(global_step, args.decay_steps, args.initial_text_ratio)
|
| 748 |
-
|
| 749 |
-
whisper = batch["whisper"]
|
| 750 |
-
interleaved = batch["interleaved"]
|
| 751 |
-
|
| 752 |
-
# Use gradient accumulation context
|
| 753 |
-
with accelerator.accumulate(adapter):
|
| 754 |
-
# Forward through adapter
|
| 755 |
-
audio_embeds = adapter(whisper)
|
| 756 |
-
|
| 757 |
-
# Get token embeddings for interleaved sequence (teacher forcing)
|
| 758 |
-
input_tokens = interleaved[:, :-1].clamp(min=0)
|
| 759 |
-
with torch.no_grad():
|
| 760 |
-
token_embeds = unwrapped_llm.model.embed_tokens(input_tokens)
|
| 761 |
-
|
| 762 |
-
# Combine: [audio_embeds] + [token_embeds]
|
| 763 |
-
combined = torch.cat([audio_embeds, token_embeds], dim=1)
|
| 764 |
-
|
| 765 |
-
# Forward through frozen LLM
|
| 766 |
-
outputs = llm(inputs_embeds=combined, use_cache=False)
|
| 767 |
-
logits = outputs.logits
|
| 768 |
-
|
| 769 |
-
# Loss: predict interleaved tokens after audio prefix
|
| 770 |
-
audio_len = audio_embeds.shape[1]
|
| 771 |
-
seq_len = interleaved.shape[1]
|
| 772 |
-
|
| 773 |
-
seq_logits = logits[:, audio_len-1:audio_len-1+seq_len]
|
| 774 |
-
targets = interleaved
|
| 775 |
-
|
| 776 |
-
loss = F.cross_entropy(
|
| 777 |
-
seq_logits.reshape(-1, logits.size(-1)),
|
| 778 |
-
targets.reshape(-1),
|
| 779 |
-
ignore_index=-100,
|
| 780 |
-
label_smoothing=args.label_smoothing
|
| 781 |
-
)
|
| 782 |
-
|
| 783 |
-
accelerator.backward(loss)
|
| 784 |
-
accum_loss += loss.item()
|
| 785 |
-
|
| 786 |
-
# Clip gradients and step
|
| 787 |
-
if accelerator.sync_gradients:
|
| 788 |
-
accelerator.clip_grad_norm_(adapter.parameters(), args.max_grad_norm)
|
| 789 |
-
|
| 790 |
-
optimizer.step()
|
| 791 |
-
optimizer.zero_grad()
|
| 792 |
-
|
| 793 |
-
# Update after accumulation
|
| 794 |
-
if accelerator.sync_gradients:
|
| 795 |
-
if global_step < warmup_steps:
|
| 796 |
-
lr_scale = (global_step + 1) / warmup_steps
|
| 797 |
-
for pg in optimizer.param_groups:
|
| 798 |
-
pg["lr"] = args.lr * lr_scale
|
| 799 |
-
else:
|
| 800 |
-
scheduler.step()
|
| 801 |
-
|
| 802 |
-
global_step += 1
|
| 803 |
-
epoch_loss += accum_loss
|
| 804 |
-
|
| 805 |
-
pbar.set_postfix(
|
| 806 |
-
loss=f"{accum_loss:.4f}",
|
| 807 |
-
text_ratio=f"{current_text_ratio:.1f}",
|
| 808 |
-
lr=f"{optimizer.param_groups[0]['lr']:.2e}"
|
| 809 |
-
)
|
| 810 |
-
|
| 811 |
-
# Save checkpoint
|
| 812 |
-
if global_step % args.save_steps == 0 and is_main:
|
| 813 |
-
accelerator.wait_for_everyone()
|
| 814 |
-
unwrapped_adapter = accelerator.unwrap_model(adapter)
|
| 815 |
-
ckpt_path = os.path.join(args.output_dir, f"stage1_step{global_step}.pt")
|
| 816 |
-
save_checkpoint_async({
|
| 817 |
-
"adapter": unwrapped_adapter.state_dict(),
|
| 818 |
-
"optimizer": optimizer.state_dict(),
|
| 819 |
-
"step": global_step,
|
| 820 |
-
"epoch": epoch,
|
| 821 |
-
"loss": accum_loss,
|
| 822 |
-
"text_ratio": current_text_ratio
|
| 823 |
-
}, ckpt_path, is_main)
|
| 824 |
-
|
| 825 |
-
accum_loss = 0
|
| 826 |
-
|
| 827 |
-
# Epoch end - divide by grad_accum to get per-batch average
|
| 828 |
-
avg_loss = epoch_loss / max(1, steps_per_epoch) / args.grad_accum
|
| 829 |
-
|
| 830 |
-
if is_main:
|
| 831 |
-
log(f"Epoch {epoch+1} avg loss: {avg_loss:.4f}, text_ratio: {current_text_ratio:.1f}")
|
| 832 |
-
|
| 833 |
-
accelerator.wait_for_everyone()
|
| 834 |
-
unwrapped_adapter = accelerator.unwrap_model(adapter)
|
| 835 |
-
ckpt_path = os.path.join(args.output_dir, f"stage1_epoch{epoch+1}.pt")
|
| 836 |
-
save_checkpoint_async({
|
| 837 |
-
"adapter": unwrapped_adapter.state_dict(),
|
| 838 |
-
"optimizer": optimizer.state_dict(),
|
| 839 |
-
"step": global_step,
|
| 840 |
-
"epoch": epoch + 1,
|
| 841 |
-
"loss": avg_loss,
|
| 842 |
-
"text_ratio": current_text_ratio
|
| 843 |
-
}, ckpt_path, is_main)
|
| 844 |
-
|
| 845 |
-
if avg_loss < best_loss:
|
| 846 |
-
best_loss = avg_loss
|
| 847 |
-
best_path = os.path.join(args.output_dir, "stage1_best.pt")
|
| 848 |
-
save_checkpoint_async({
|
| 849 |
-
"adapter": unwrapped_adapter.state_dict(),
|
| 850 |
-
"step": global_step,
|
| 851 |
-
"epoch": epoch + 1,
|
| 852 |
-
"loss": best_loss,
|
| 853 |
-
"text_ratio": current_text_ratio
|
| 854 |
-
}, best_path, is_main)
|
| 855 |
-
log(f"Best model saved! Loss: {best_loss:.4f}")
|
| 856 |
-
|
| 857 |
-
# Finish
|
| 858 |
-
accelerator.wait_for_everyone()
|
| 859 |
-
if is_main:
|
| 860 |
-
wait_for_checkpoints()
|
| 861 |
-
log("\n" + "=" * 60)
|
| 862 |
-
log("STAGE 1 COMPLETE!")
|
| 863 |
-
log(f"Best loss: {best_loss:.4f}")
|
| 864 |
-
log(f"Final text_ratio: {current_text_ratio:.1f}")
|
| 865 |
-
log("Next: Stage 2 (Adapter + LoRA)")
|
| 866 |
-
log("=" * 60)
|
| 867 |
|
| 868 |
|
| 869 |
if __name__ == "__main__":
|
|
|
|
| 8 |
- LLM is completely frozen (adapter gets a "head start")
|
| 9 |
|
| 10 |
Usage:
|
| 11 |
+
python passo2_finetune_stage1.py --data data.pt --epochs 2
|
| 12 |
|
| 13 |
Next: Stage 2 trains Adapter + LoRA together
|
| 14 |
"""
|
| 15 |
|
|
|
|
|
|
|
| 16 |
import argparse
|
| 17 |
import torch
|
| 18 |
+
from typing import Dict, Any
|
| 19 |
+
|
| 20 |
+
from training.config import TrainingConfig, GPUConfig
|
| 21 |
+
from training.models import SpeechAdapter, ModelFactory
|
| 22 |
+
from training.checkpoint import Stage1CheckpointManager, TrainingState
|
| 23 |
+
from training.trainer import BaseTrainer
|
| 24 |
+
from training.utils import log, should_enable_gradient_checkpointing
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
|
| 27 |
+
class Stage1Trainer(BaseTrainer):
|
| 28 |
"""
|
| 29 |
+
Stage 1 Trainer: Adapter only (LLM frozen).
|
| 30 |
+
|
| 31 |
+
Trains only the SpeechAdapter while keeping the LLM completely frozen.
|
| 32 |
+
This gives the adapter a "head start" in learning the audio→embedding mapping.
|
| 33 |
"""
|
| 34 |
+
|
| 35 |
+
def _get_stage_name(self) -> str:
|
| 36 |
+
return "STAGE 1: Adapter Only (LLM Frozen)"
|
| 37 |
+
|
| 38 |
+
def _setup_models(self):
|
| 39 |
+
"""Setup adapter and frozen LLM."""
|
| 40 |
+
gpu_config = GPUConfig.auto_detect()
|
| 41 |
+
dtype = gpu_config.dtype
|
| 42 |
+
|
| 43 |
+
if self.is_main:
|
| 44 |
+
log(f"\nLoading LLM (FROZEN): {self.config.model_path}")
|
| 45 |
+
|
| 46 |
+
# Check if gradient checkpointing needed
|
| 47 |
+
use_checkpointing = self.config.gradient_checkpointing or \
|
| 48 |
+
should_enable_gradient_checkpointing(
|
| 49 |
+
gpu_config.vram_gb,
|
| 50 |
+
self.config.dynamic_decay
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
)
|
| 52 |
+
|
| 53 |
+
# Load frozen LLM
|
| 54 |
+
self.llm = ModelFactory.create_llm(
|
| 55 |
+
self.config.model_path,
|
| 56 |
+
dtype=dtype,
|
| 57 |
+
freeze=True,
|
| 58 |
+
gradient_checkpointing=use_checkpointing,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
)
|
| 60 |
|
| 61 |
+
if self.is_main and use_checkpointing:
|
| 62 |
+
log("[Memory] Gradient checkpointing enabled")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
+
# Create adapter
|
| 65 |
+
self.adapter = SpeechAdapter().to(dtype=dtype)
|
| 66 |
|
| 67 |
+
if self.is_main:
|
| 68 |
+
params = self.adapter.get_num_params()
|
| 69 |
+
log(f"\nTrainable: Adapter only ({params:,} = {params/1e6:.1f}M params)")
|
| 70 |
+
log("LLM: FROZEN")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
+
def _setup_optimizer(self):
|
| 73 |
+
"""Setup optimizer for adapter only."""
|
| 74 |
+
self.optimizer = torch.optim.AdamW(
|
| 75 |
+
self.adapter.parameters(),
|
| 76 |
+
lr=self.config.learning_rate,
|
| 77 |
+
weight_decay=self.config.weight_decay,
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
def _setup_checkpoint_manager(self):
|
| 81 |
+
"""Setup Stage 1 checkpoint manager."""
|
| 82 |
+
self.checkpoint_manager = Stage1CheckpointManager(
|
| 83 |
+
self.config.output_dir,
|
| 84 |
+
verbose=self.is_main,
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
def _load_checkpoint(self, ckpt: Dict[str, Any]):
|
| 88 |
+
"""Load checkpoint state."""
|
| 89 |
+
if "adapter" in ckpt:
|
| 90 |
+
self.adapter.load_state_dict(ckpt["adapter"])
|
| 91 |
+
if "optimizer" in ckpt:
|
| 92 |
+
self.optimizer.load_state_dict(ckpt["optimizer"])
|
| 93 |
+
if "step" in ckpt:
|
| 94 |
+
self.global_step = ckpt["step"]
|
| 95 |
+
if "epoch" in ckpt:
|
| 96 |
+
self.start_epoch = ckpt["epoch"]
|
| 97 |
+
if "text_ratio" in ckpt:
|
| 98 |
+
self.current_text_ratio = ckpt["text_ratio"]
|
| 99 |
+
|
| 100 |
+
def _get_trainable_params(self):
|
| 101 |
+
"""Get trainable parameters (adapter only)."""
|
| 102 |
+
return self.adapter.parameters()
|
| 103 |
+
|
| 104 |
+
def _save_step_checkpoint(self, loss: float):
|
| 105 |
+
"""Save step checkpoint."""
|
| 106 |
+
self.accelerator.wait_for_everyone()
|
| 107 |
+
unwrapped = self.accelerator.unwrap_model(self.adapter)
|
| 108 |
+
|
| 109 |
+
state = TrainingState(
|
| 110 |
+
step=self.global_step,
|
| 111 |
+
epoch=self.start_epoch,
|
| 112 |
+
loss=loss,
|
| 113 |
+
text_ratio=self.current_text_ratio,
|
| 114 |
+
best_loss=self.best_loss,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
)
|
|
|
|
|
|
|
| 116 |
|
| 117 |
+
self.checkpoint_manager.save_step(
|
| 118 |
+
unwrapped.state_dict(),
|
| 119 |
+
self.optimizer.state_dict(),
|
| 120 |
+
state,
|
| 121 |
+
)
|
| 122 |
|
| 123 |
+
def _finish_epoch(self, epoch: int, epoch_loss: float):
|
| 124 |
+
"""Finish epoch and save checkpoints."""
|
| 125 |
+
avg_loss = epoch_loss / max(1, self.steps_per_epoch) / self.config.grad_accum
|
| 126 |
|
| 127 |
+
if self.is_main:
|
| 128 |
+
log(f"Epoch {epoch+1} avg loss: {avg_loss:.4f}, text_ratio: {self.current_text_ratio:.1f}")
|
| 129 |
|
| 130 |
+
self.accelerator.wait_for_everyone()
|
| 131 |
+
unwrapped = self.accelerator.unwrap_model(self.adapter)
|
| 132 |
|
| 133 |
+
state = TrainingState(
|
| 134 |
+
step=self.global_step,
|
| 135 |
+
epoch=epoch + 1,
|
| 136 |
+
loss=avg_loss,
|
| 137 |
+
text_ratio=self.current_text_ratio,
|
| 138 |
+
best_loss=self.best_loss,
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
# Save epoch checkpoint
|
| 142 |
+
self.checkpoint_manager.save_epoch(
|
| 143 |
+
unwrapped.state_dict(),
|
| 144 |
+
self.optimizer.state_dict(),
|
| 145 |
+
state,
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
# Save best if improved
|
| 149 |
+
if avg_loss < self.best_loss:
|
| 150 |
+
self.best_loss = avg_loss
|
| 151 |
+
state.best_loss = self.best_loss
|
| 152 |
+
self.checkpoint_manager.save_best(unwrapped.state_dict(), state)
|
| 153 |
+
log(f"Best model saved! Loss: {self.best_loss:.4f}")
|
| 154 |
|
| 155 |
|
|
|
|
|
|
|
|
|
|
| 156 |
def parse_args():
|
| 157 |
+
"""Parse command line arguments."""
|
| 158 |
+
parser = argparse.ArgumentParser(
|
| 159 |
+
description="Stage 1: Adapter Only with Interleaved Output"
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
# Data
|
| 163 |
+
parser.add_argument("--data", type=str, required=True,
|
| 164 |
+
help="Dataset path(s), comma-separated")
|
| 165 |
parser.add_argument("--output_dir", type=str, default="./checkpoints")
|
| 166 |
+
|
| 167 |
+
# Training
|
| 168 |
parser.add_argument("--lr", type=float, default=5e-5)
|
| 169 |
+
parser.add_argument("--epochs", type=int, default=2,
|
| 170 |
+
help="1-2 epochs for adapter warmup")
|
| 171 |
parser.add_argument("--batch_size", type=int, default=None)
|
| 172 |
parser.add_argument("--grad_accum", type=int, default=None)
|
| 173 |
parser.add_argument("--warmup_ratio", type=float, default=0.03)
|
| 174 |
parser.add_argument("--max_grad_norm", type=float, default=1.0)
|
|
|
|
| 175 |
parser.add_argument("--label_smoothing", type=float, default=0.1)
|
| 176 |
+
parser.add_argument("--max_seq_len", type=int, default=2048)
|
| 177 |
+
|
| 178 |
# Scheduled interleaving
|
| 179 |
parser.add_argument("--initial_text_ratio", type=float, default=0.9)
|
| 180 |
parser.add_argument("--decay_steps", type=int, default=300)
|
| 181 |
+
parser.add_argument("--dynamic_decay", action="store_true")
|
| 182 |
+
parser.add_argument("--no_decay", action="store_true", default=True,
|
| 183 |
+
help="Keep text_ratio fixed (recommended for Stage 1)")
|
| 184 |
+
parser.add_argument("--use_decay", action="store_true",
|
| 185 |
+
help="Enable decay (overrides --no_decay)")
|
| 186 |
+
parser.add_argument("--final_audio_portion", type=float, default=0.2)
|
| 187 |
+
|
| 188 |
# Model
|
| 189 |
+
parser.add_argument("--model_path", type=str,
|
| 190 |
+
default="canopylabs/3b-es_it-ft-research_release")
|
| 191 |
+
|
| 192 |
+
# Checkpointing
|
| 193 |
+
parser.add_argument("--save_steps", type=int, default=200)
|
| 194 |
parser.add_argument("--resume", type=str, default=None)
|
| 195 |
+
|
| 196 |
+
# Memory
|
| 197 |
+
parser.add_argument("--vram_fraction", type=float, default=0.80)
|
| 198 |
+
parser.add_argument("--ram_limit_gb", type=float, default=None)
|
| 199 |
+
parser.add_argument("--gradient_checkpointing", action="store_true")
|
| 200 |
+
|
| 201 |
# Modes
|
| 202 |
parser.add_argument("--demo", action="store_true")
|
| 203 |
parser.add_argument("--test", action="store_true")
|
| 204 |
+
|
|
|
|
| 205 |
return parser.parse_args()
|
| 206 |
|
| 207 |
|
|
|
|
|
|
|
|
|
|
| 208 |
def main():
|
| 209 |
args = parse_args()
|
| 210 |
+
# --use_decay overrides --no_decay
|
| 211 |
+
if args.use_decay:
|
| 212 |
+
args.no_decay = False
|
| 213 |
+
config = TrainingConfig.from_args(args)
|
| 214 |
+
|
| 215 |
+
trainer = Stage1Trainer(config)
|
| 216 |
+
trainer.setup()
|
| 217 |
+
trainer.train()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
|
| 219 |
|
| 220 |
if __name__ == "__main__":
|
passo3_finetune_stage2.py
CHANGED
|
@@ -9,916 +9,289 @@ Continues from Stage 1 checkpoint:
|
|
| 9 |
- Continues scheduled interleaving (90% text -> 0% text)
|
| 10 |
|
| 11 |
Usage:
|
| 12 |
-
python
|
| 13 |
|
| 14 |
Based on IST-LM paper + LLaMA-Omni 2 staging approach.
|
| 15 |
"""
|
| 16 |
|
| 17 |
-
import os
|
| 18 |
-
import sys
|
| 19 |
import argparse
|
| 20 |
import torch
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
from torch.utils.data import Dataset, DataLoader, ConcatDataset
|
| 24 |
-
from torch.optim.lr_scheduler import CosineAnnealingLR
|
| 25 |
-
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 26 |
-
from peft import LoraConfig, get_peft_model, TaskType
|
| 27 |
-
from accelerate import Accelerator
|
| 28 |
-
from accelerate.utils import set_seed
|
| 29 |
-
from huggingface_hub import login
|
| 30 |
-
from tqdm import tqdm
|
| 31 |
-
import threading
|
| 32 |
-
|
| 33 |
-
# ============================================================
|
| 34 |
-
# Config
|
| 35 |
-
# ============================================================
|
| 36 |
-
torch.backends.cuda.matmul.allow_tf32 = True
|
| 37 |
-
torch.backends.cudnn.allow_tf32 = True
|
| 38 |
-
torch.backends.cudnn.benchmark = True
|
| 39 |
-
torch.set_float32_matmul_precision('high')
|
| 40 |
-
|
| 41 |
-
# SNAC token offsets for Orpheus
|
| 42 |
-
SNAC_BASE_OFFSET = 128266
|
| 43 |
-
EOS_TOKEN = 128009
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
def log(msg):
|
| 47 |
-
print(msg)
|
| 48 |
-
sys.stdout.flush()
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def apply_snac_offset(token_idx, position):
|
| 52 |
-
"""Apply position-based offset to SNAC token.
|
| 53 |
-
If token is already offset (>= SNAC_BASE_OFFSET), return as-is.
|
| 54 |
-
"""
|
| 55 |
-
if int(token_idx) >= SNAC_BASE_OFFSET:
|
| 56 |
-
# Already has offset applied
|
| 57 |
-
return int(token_idx)
|
| 58 |
-
offset = SNAC_BASE_OFFSET + (position % 7) * 4096
|
| 59 |
-
return int(token_idx) + offset
|
| 60 |
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
num_decays = global_step // decay_steps
|
| 67 |
-
text_ratio = initial_ratio - (num_decays * 0.1)
|
| 68 |
-
return max(min_ratio, text_ratio)
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
# ============================================================
|
| 72 |
-
# Async Checkpoint Saving
|
| 73 |
-
# ============================================================
|
| 74 |
-
_save_threads = []
|
| 75 |
-
|
| 76 |
-
def save_checkpoint_async(state_dict, path, is_main=True):
|
| 77 |
-
global _save_threads
|
| 78 |
-
_save_threads = [t for t in _save_threads if t.is_alive()]
|
| 79 |
-
|
| 80 |
-
def copy_to_cpu(obj):
|
| 81 |
-
if isinstance(obj, torch.Tensor):
|
| 82 |
-
return obj.detach().cpu().clone()
|
| 83 |
-
elif isinstance(obj, dict):
|
| 84 |
-
return {k: copy_to_cpu(v) for k, v in obj.items()}
|
| 85 |
-
return obj
|
| 86 |
-
|
| 87 |
-
state_copy = copy_to_cpu(state_dict)
|
| 88 |
-
|
| 89 |
-
def _save():
|
| 90 |
-
try:
|
| 91 |
-
torch.save(state_copy, path)
|
| 92 |
-
if is_main:
|
| 93 |
-
log(f"[ASYNC] Saved: {path}")
|
| 94 |
-
except Exception as e:
|
| 95 |
-
if is_main:
|
| 96 |
-
log(f"[ASYNC] Error: {e}")
|
| 97 |
-
|
| 98 |
-
thread = threading.Thread(target=_save, daemon=True)
|
| 99 |
-
thread.start()
|
| 100 |
-
_save_threads.append(thread)
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
def wait_for_checkpoints():
|
| 104 |
-
global _save_threads
|
| 105 |
-
for t in _save_threads:
|
| 106 |
-
t.join()
|
| 107 |
-
_save_threads = []
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
# ============================================================
|
| 111 |
-
# GPU Auto-Detection (Multi-backend: CUDA, ROCm, MPS, XPU)
|
| 112 |
-
# ============================================================
|
| 113 |
-
def auto_detect_gpu_config():
|
| 114 |
-
"""Detect GPU and return optimal batch size config."""
|
| 115 |
-
vram_gb = 0
|
| 116 |
-
gpu_name = "Unknown"
|
| 117 |
-
|
| 118 |
-
# Try CUDA (NVIDIA)
|
| 119 |
-
if torch.cuda.is_available():
|
| 120 |
-
try:
|
| 121 |
-
props = torch.cuda.get_device_properties(0)
|
| 122 |
-
vram_gb = props.total_memory // (1024**3)
|
| 123 |
-
gpu_name = props.name
|
| 124 |
-
except:
|
| 125 |
-
pass
|
| 126 |
-
|
| 127 |
-
# Try MPS (Apple Silicon)
|
| 128 |
-
elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
|
| 129 |
-
gpu_name = "Apple Silicon (MPS)"
|
| 130 |
-
try:
|
| 131 |
-
import subprocess
|
| 132 |
-
result = subprocess.run(['sysctl', '-n', 'hw.memsize'], capture_output=True, text=True)
|
| 133 |
-
total_mem = int(result.stdout.strip()) // (1024**3)
|
| 134 |
-
vram_gb = total_mem // 2
|
| 135 |
-
except:
|
| 136 |
-
vram_gb = 8
|
| 137 |
-
|
| 138 |
-
# Try ROCm (AMD)
|
| 139 |
-
elif hasattr(torch, 'hip') or os.environ.get('ROCM_HOME'):
|
| 140 |
-
try:
|
| 141 |
-
if torch.cuda.is_available():
|
| 142 |
-
props = torch.cuda.get_device_properties(0)
|
| 143 |
-
vram_gb = props.total_memory // (1024**3)
|
| 144 |
-
gpu_name = f"AMD {props.name}"
|
| 145 |
-
except:
|
| 146 |
-
gpu_name = "AMD ROCm"
|
| 147 |
-
vram_gb = 16
|
| 148 |
-
|
| 149 |
-
# Fallback: try nvidia-smi
|
| 150 |
-
if vram_gb == 0:
|
| 151 |
-
try:
|
| 152 |
-
import subprocess
|
| 153 |
-
result = subprocess.run(
|
| 154 |
-
['nvidia-smi', '--query-gpu=name,memory.total', '--format=csv,noheader,nounits'],
|
| 155 |
-
capture_output=True, text=True
|
| 156 |
-
)
|
| 157 |
-
lines = result.stdout.strip().split('\n')
|
| 158 |
-
gpu_name, vram_mb = lines[0].split(', ')
|
| 159 |
-
vram_gb = int(vram_mb) // 1024
|
| 160 |
-
except:
|
| 161 |
-
pass
|
| 162 |
-
|
| 163 |
-
# Determine batch size based on VRAM
|
| 164 |
-
if vram_gb >= 150:
|
| 165 |
-
return {"name": gpu_name, "batch_size": 8, "grad_accum": 4, "vram_gb": vram_gb}
|
| 166 |
-
elif vram_gb >= 80:
|
| 167 |
-
return {"name": gpu_name, "batch_size": 6, "grad_accum": 5, "vram_gb": vram_gb}
|
| 168 |
-
elif vram_gb >= 35:
|
| 169 |
-
return {"name": gpu_name, "batch_size": 4, "grad_accum": 8, "vram_gb": vram_gb}
|
| 170 |
-
elif vram_gb >= 16:
|
| 171 |
-
return {"name": gpu_name, "batch_size": 2, "grad_accum": 16, "vram_gb": vram_gb}
|
| 172 |
-
else:
|
| 173 |
-
return {"name": gpu_name, "batch_size": 1, "grad_accum": 32, "vram_gb": max(vram_gb, 8)}
|
| 174 |
-
|
| 175 |
-
def get_ram_info():
|
| 176 |
-
"""Get RAM info in GB."""
|
| 177 |
-
try:
|
| 178 |
-
import psutil
|
| 179 |
-
total = psutil.virtual_memory().total / 1024**3
|
| 180 |
-
available = psutil.virtual_memory().available / 1024**3
|
| 181 |
-
return total, available
|
| 182 |
-
except ImportError:
|
| 183 |
-
try:
|
| 184 |
-
import subprocess
|
| 185 |
-
result = subprocess.run(
|
| 186 |
-
['free', '-g', '--output=SIZE,AVAILABLE'],
|
| 187 |
-
capture_output=True, text=True
|
| 188 |
-
)
|
| 189 |
-
lines = result.stdout.strip().split('\n')
|
| 190 |
-
if len(lines) >= 2:
|
| 191 |
-
total, available = map(float, lines[1].split())
|
| 192 |
-
return total, available
|
| 193 |
-
except:
|
| 194 |
-
pass
|
| 195 |
-
except:
|
| 196 |
-
pass
|
| 197 |
-
return 0, 0
|
| 198 |
-
|
| 199 |
-
def limit_ram_usage(max_ram_gb):
|
| 200 |
-
"""Limit RAM usage by setting resource limits."""
|
| 201 |
-
try:
|
| 202 |
-
import resource
|
| 203 |
-
max_bytes = int(max_ram_gb * 1024**3)
|
| 204 |
-
resource.setrlimit(resource.RLIMIT_AS, (max_bytes, max_bytes))
|
| 205 |
-
except:
|
| 206 |
-
pass
|
| 207 |
-
|
| 208 |
-
def log_memory_usage():
|
| 209 |
-
"""Log current memory usage."""
|
| 210 |
-
msg = []
|
| 211 |
-
if torch.cuda.is_available():
|
| 212 |
-
used = torch.cuda.memory_allocated() / 1024**3
|
| 213 |
-
reserved = torch.cuda.memory_reserved() / 1024**3
|
| 214 |
-
msg.append(f"GPU: {used:.2f}GB / {reserved:.2f}GB")
|
| 215 |
-
try:
|
| 216 |
-
import psutil
|
| 217 |
-
ram_used = psutil.virtual_memory().used / 1024**3
|
| 218 |
-
ram_total = psutil.virtual_memory().total / 1024**3
|
| 219 |
-
msg.append(f"RAM: {ram_used:.1f}GB / {ram_total:.1f}GB")
|
| 220 |
-
except:
|
| 221 |
-
pass
|
| 222 |
-
return " | ".join(msg)
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
# ============================================================
|
| 226 |
-
# Speech Adapter (Same as Stage 1 - LLaMA-Omni 2 Style)
|
| 227 |
-
# ============================================================
|
| 228 |
-
class SpeechAdapter(nn.Module):
|
| 229 |
-
"""
|
| 230 |
-
5x downsampling + FFN with intermediate dim 2048
|
| 231 |
-
MUST match Stage 1 architecture exactly for checkpoint loading.
|
| 232 |
-
"""
|
| 233 |
-
def __init__(self, whisper_dim=1280, llm_dim=3072, downsample=5, intermediate_dim=2048):
|
| 234 |
-
super().__init__()
|
| 235 |
-
self.downsample = downsample
|
| 236 |
-
concat_dim = whisper_dim * downsample
|
| 237 |
-
|
| 238 |
-
self.ffn = nn.Sequential(
|
| 239 |
-
nn.Linear(concat_dim, intermediate_dim),
|
| 240 |
-
nn.GELU(),
|
| 241 |
-
nn.Linear(intermediate_dim, llm_dim),
|
| 242 |
-
nn.LayerNorm(llm_dim)
|
| 243 |
-
)
|
| 244 |
-
|
| 245 |
-
def forward(self, x):
|
| 246 |
-
B, T, D = x.shape
|
| 247 |
-
T_new = (T // self.downsample) * self.downsample
|
| 248 |
-
x = x[:, :T_new]
|
| 249 |
-
x = x.reshape(B, T_new // self.downsample, D * self.downsample)
|
| 250 |
-
return self.ffn(x)
|
| 251 |
|
| 252 |
|
| 253 |
-
|
| 254 |
-
# Scheduled Interleaved Sequence Creation with Word Alignment
|
| 255 |
-
# ============================================================
|
| 256 |
-
def create_interleaved_sequence(text_tokens, snac_tokens, text_ratio=0.9, word_alignments=None, tokenizer=None, answer_text=None):
|
| 257 |
"""
|
| 258 |
-
|
| 259 |
-
- text_ratio=0.9 means 90% of words are replaced by text tokens
|
| 260 |
-
- text_ratio=0.0 means 100% audio (no text replacement)
|
| 261 |
|
| 262 |
-
|
| 263 |
-
|
| 264 |
"""
|
| 265 |
-
interleaved = []
|
| 266 |
-
is_audio_mask = []
|
| 267 |
-
|
| 268 |
-
if len(snac_tokens) == 0:
|
| 269 |
-
return text_tokens + [EOS_TOKEN], [False] * (len(text_tokens) + 1)
|
| 270 |
-
|
| 271 |
-
# Group SNAC into frames of 7
|
| 272 |
-
frames = []
|
| 273 |
-
for i in range(0, len(snac_tokens), 7):
|
| 274 |
-
frame = snac_tokens[i:i+7]
|
| 275 |
-
if len(frame) == 7:
|
| 276 |
-
frames.append(frame)
|
| 277 |
-
|
| 278 |
-
if len(frames) == 0:
|
| 279 |
-
return text_tokens + [EOS_TOKEN], [False] * (len(text_tokens) + 1)
|
| 280 |
-
|
| 281 |
-
total_frames = len(frames)
|
| 282 |
-
|
| 283 |
-
# If we have word alignments, use semantic interleaving
|
| 284 |
-
# Check if alignments have pre-computed tokens (preferred) or need tokenizer
|
| 285 |
-
has_precomputed = word_alignments and len(word_alignments) > 0 and 'tokens' in word_alignments[0] and word_alignments[0]['tokens']
|
| 286 |
-
can_interleave = word_alignments and text_ratio > 0 and (has_precomputed or (tokenizer and answer_text))
|
| 287 |
-
|
| 288 |
-
if can_interleave:
|
| 289 |
-
import random
|
| 290 |
-
|
| 291 |
-
# Decide which words to replace with text based on text_ratio
|
| 292 |
-
num_words = len(word_alignments)
|
| 293 |
-
num_text_words = int(num_words * text_ratio)
|
| 294 |
-
|
| 295 |
-
# Randomly select which word indices to replace with text
|
| 296 |
-
word_indices = list(range(num_words))
|
| 297 |
-
random.shuffle(word_indices)
|
| 298 |
-
text_word_indices = set(word_indices[:num_text_words])
|
| 299 |
-
|
| 300 |
-
# Build interleaved sequence frame by frame
|
| 301 |
-
frame_idx = 0
|
| 302 |
-
snac_position = 0
|
| 303 |
-
|
| 304 |
-
for word_idx, alignment in enumerate(word_alignments):
|
| 305 |
-
word = alignment['word']
|
| 306 |
-
start_frame = alignment['start_frame']
|
| 307 |
-
end_frame = min(alignment['end_frame'], total_frames)
|
| 308 |
-
|
| 309 |
-
if word_idx in text_word_indices:
|
| 310 |
-
# Replace this word's audio with text tokens
|
| 311 |
-
# Use pre-computed tokens if available, else tokenize on-the-fly
|
| 312 |
-
word_tokens = alignment.get('tokens', [])
|
| 313 |
-
if not word_tokens and tokenizer:
|
| 314 |
-
word_tokens = tokenizer.encode(word, add_special_tokens=False)
|
| 315 |
-
|
| 316 |
-
for tok in word_tokens:
|
| 317 |
-
interleaved.append(tok)
|
| 318 |
-
is_audio_mask.append(False)
|
| 319 |
-
# Skip the audio frames for this word
|
| 320 |
-
snac_position = end_frame * 7
|
| 321 |
-
else:
|
| 322 |
-
# Keep audio for this word
|
| 323 |
-
for f_idx in range(start_frame, end_frame):
|
| 324 |
-
if f_idx < total_frames:
|
| 325 |
-
frame = frames[f_idx]
|
| 326 |
-
for tok in frame:
|
| 327 |
-
interleaved.append(apply_snac_offset(tok, snac_position))
|
| 328 |
-
is_audio_mask.append(True)
|
| 329 |
-
snac_position += 1
|
| 330 |
-
|
| 331 |
-
frame_idx = end_frame
|
| 332 |
-
|
| 333 |
-
# Add any remaining frames after the last word
|
| 334 |
-
while frame_idx < total_frames:
|
| 335 |
-
frame = frames[frame_idx]
|
| 336 |
-
for tok in frame:
|
| 337 |
-
interleaved.append(apply_snac_offset(tok, snac_position))
|
| 338 |
-
is_audio_mask.append(True)
|
| 339 |
-
snac_position += 1
|
| 340 |
-
frame_idx += 1
|
| 341 |
-
|
| 342 |
-
else:
|
| 343 |
-
# Fallback: positional interleaving (original behavior)
|
| 344 |
-
total_text = len(text_tokens)
|
| 345 |
-
|
| 346 |
-
# Determine interleaving pattern based on text_ratio
|
| 347 |
-
if text_ratio >= 0.9:
|
| 348 |
-
text_per_chunk, frames_per_chunk = 1, 3
|
| 349 |
-
elif text_ratio >= 0.7:
|
| 350 |
-
text_per_chunk, frames_per_chunk = 1, 5
|
| 351 |
-
elif text_ratio >= 0.5:
|
| 352 |
-
text_per_chunk, frames_per_chunk = 1, 7
|
| 353 |
-
elif text_ratio >= 0.3:
|
| 354 |
-
text_per_chunk, frames_per_chunk = 1, 10
|
| 355 |
-
else:
|
| 356 |
-
text_per_chunk, frames_per_chunk = 0, 1
|
| 357 |
-
|
| 358 |
-
text_idx = 0
|
| 359 |
-
frame_idx = 0
|
| 360 |
-
snac_position = 0
|
| 361 |
-
|
| 362 |
-
while frame_idx < total_frames:
|
| 363 |
-
if text_per_chunk > 0 and text_idx < total_text:
|
| 364 |
-
for _ in range(text_per_chunk):
|
| 365 |
-
if text_idx < total_text:
|
| 366 |
-
interleaved.append(text_tokens[text_idx])
|
| 367 |
-
is_audio_mask.append(False)
|
| 368 |
-
text_idx += 1
|
| 369 |
-
|
| 370 |
-
for _ in range(frames_per_chunk):
|
| 371 |
-
if frame_idx < total_frames:
|
| 372 |
-
frame = frames[frame_idx]
|
| 373 |
-
for tok in frame:
|
| 374 |
-
interleaved.append(apply_snac_offset(tok, snac_position))
|
| 375 |
-
is_audio_mask.append(True)
|
| 376 |
-
snac_position += 1
|
| 377 |
-
frame_idx += 1
|
| 378 |
-
|
| 379 |
-
while text_idx < total_text:
|
| 380 |
-
interleaved.append(text_tokens[text_idx])
|
| 381 |
-
is_audio_mask.append(False)
|
| 382 |
-
text_idx += 1
|
| 383 |
-
|
| 384 |
-
# Add EOS
|
| 385 |
-
interleaved.append(EOS_TOKEN)
|
| 386 |
-
is_audio_mask.append(False)
|
| 387 |
-
|
| 388 |
-
return interleaved, is_audio_mask
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
# ============================================================
|
| 392 |
-
# Dataset
|
| 393 |
-
# ============================================================
|
| 394 |
-
class InterleavedDataset(Dataset):
|
| 395 |
-
def __init__(self, data, tokenizer, max_audio_len=500, max_seq_len=2048):
|
| 396 |
-
self.data = data
|
| 397 |
-
self.tokenizer = tokenizer
|
| 398 |
-
self.max_audio = max_audio_len * 5
|
| 399 |
-
self.max_seq_len = max_seq_len
|
| 400 |
-
|
| 401 |
-
def __len__(self):
|
| 402 |
-
return len(self.data)
|
| 403 |
-
|
| 404 |
-
def __getitem__(self, idx):
|
| 405 |
-
item = self.data[idx]
|
| 406 |
-
|
| 407 |
-
# Whisper features
|
| 408 |
-
whisper = item["whisper_features"][:self.max_audio]
|
| 409 |
-
|
| 410 |
-
# Text tokens - use pre-computed if available, otherwise tokenize
|
| 411 |
-
if "text_tokens" in item and len(item["text_tokens"]) > 0:
|
| 412 |
-
tt = item["text_tokens"]
|
| 413 |
-
text_tokens = tt.tolist() if hasattr(tt, 'tolist') else list(tt)
|
| 414 |
-
else:
|
| 415 |
-
text = item.get("answer", item.get("text", ""))
|
| 416 |
-
if isinstance(text, str) and len(text) > 0:
|
| 417 |
-
text_tokens = self.tokenizer.encode(text, add_special_tokens=False)
|
| 418 |
-
else:
|
| 419 |
-
text_tokens = []
|
| 420 |
-
|
| 421 |
-
# SNAC tokens
|
| 422 |
-
snac = item["snac_tokens"]
|
| 423 |
-
snac_len = (len(snac) // 7) * 7
|
| 424 |
-
snac = snac[:snac_len] if snac_len > 0 else snac[:7]
|
| 425 |
-
snac_list = snac.tolist() if hasattr(snac, 'tolist') else list(snac)
|
| 426 |
-
|
| 427 |
-
# Word alignments (if available)
|
| 428 |
-
word_alignments = item.get("word_alignments", None)
|
| 429 |
-
answer_text = item.get("answer", "")
|
| 430 |
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
"word_alignments": word_alignments,
|
| 436 |
-
"answer_text": answer_text
|
| 437 |
-
}
|
| 438 |
|
|
|
|
|
|
|
| 439 |
|
| 440 |
-
def
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
|
|
|
|
|
|
|
|
|
| 448 |
|
| 449 |
-
|
| 450 |
-
|
| 451 |
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
tokenizer=tokenizer,
|
| 459 |
-
answer_text=b.get("answer_text")
|
| 460 |
)
|
| 461 |
-
sequences.append((interleaved, is_audio))
|
| 462 |
-
max_seq = max(max_seq, len(interleaved))
|
| 463 |
-
|
| 464 |
-
for i, b in enumerate(batch):
|
| 465 |
-
w = b["whisper"]
|
| 466 |
-
w_pad = F.pad(w, (0, 0, 0, max_w - w.shape[0]))
|
| 467 |
-
whisper_batch.append(w_pad)
|
| 468 |
-
|
| 469 |
-
interleaved, is_audio = sequences[i]
|
| 470 |
-
seq_tensor = torch.tensor(interleaved, dtype=torch.long)
|
| 471 |
-
mask_tensor = torch.tensor(is_audio, dtype=torch.bool)
|
| 472 |
-
|
| 473 |
-
seq_pad = F.pad(seq_tensor, (0, max_seq - len(interleaved)), value=-100)
|
| 474 |
-
mask_pad = F.pad(mask_tensor, (0, max_seq - len(is_audio)), value=False)
|
| 475 |
-
|
| 476 |
-
interleaved_batch.append(seq_pad)
|
| 477 |
-
is_audio_batch.append(mask_pad)
|
| 478 |
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
"is_audio_mask": torch.stack(is_audio_batch)
|
| 483 |
-
}
|
| 484 |
|
|
|
|
| 485 |
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
# ============================================================
|
| 489 |
-
def parse_args():
|
| 490 |
-
parser = argparse.ArgumentParser(description="Stage 2: Adapter + LoRA with Interleaved Output")
|
| 491 |
-
parser.add_argument("--data", type=str, required=True)
|
| 492 |
-
parser.add_argument("--output_dir", type=str, default="./checkpoints")
|
| 493 |
-
parser.add_argument("--stage1_ckpt", type=str, default=None,
|
| 494 |
-
help="Path to Stage 1 adapter checkpoint (required for proper init)")
|
| 495 |
-
parser.add_argument("--lr", type=float, default=5e-5)
|
| 496 |
-
parser.add_argument("--epochs", type=int, default=3, help="3+ epochs recommended")
|
| 497 |
-
parser.add_argument("--batch_size", type=int, default=None)
|
| 498 |
-
parser.add_argument("--grad_accum", type=int, default=None)
|
| 499 |
-
parser.add_argument("--warmup_ratio", type=float, default=0.03)
|
| 500 |
-
parser.add_argument("--max_grad_norm", type=float, default=1.0)
|
| 501 |
-
parser.add_argument("--save_steps", type=int, default=200)
|
| 502 |
-
parser.add_argument("--label_smoothing", type=float, default=0.1)
|
| 503 |
-
# Scheduled interleaving
|
| 504 |
-
parser.add_argument("--initial_text_ratio", type=float, default=0.9)
|
| 505 |
-
parser.add_argument("--decay_steps", type=int, default=300)
|
| 506 |
-
# LoRA config
|
| 507 |
-
parser.add_argument("--lora_r", type=int, default=16)
|
| 508 |
-
parser.add_argument("--lora_alpha", type=int, default=32)
|
| 509 |
-
parser.add_argument("--lora_dropout", type=float, default=0.05)
|
| 510 |
-
# Model
|
| 511 |
-
parser.add_argument("--model_path", type=str, default="canopylabs/3b-es_it-ft-research_release")
|
| 512 |
-
parser.add_argument("--resume", type=str, default=None)
|
| 513 |
-
# Memory limits
|
| 514 |
-
parser.add_argument("--vram_fraction", type=float, default=0.80, help="VRAM fraction to use (default 0.80)")
|
| 515 |
-
parser.add_argument("--ram_limit_gb", type=float, default=None, help="RAM limit in GB (auto if not specified)")
|
| 516 |
-
parser.add_argument("--gradient_checkpointing", action="store_true", help="Enable gradient checkpointing to reduce VRAM")
|
| 517 |
-
# Modes
|
| 518 |
-
parser.add_argument("--demo", action="store_true")
|
| 519 |
-
parser.add_argument("--test", action="store_true")
|
| 520 |
-
return parser.parse_args()
|
| 521 |
|
|
|
|
|
|
|
| 522 |
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
# ============================================================
|
| 526 |
-
def main():
|
| 527 |
-
args = parse_args()
|
| 528 |
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 534 |
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
# GPU config
|
| 548 |
-
gpu_config = auto_detect_gpu_config()
|
| 549 |
-
if args.batch_size is None:
|
| 550 |
-
args.batch_size = gpu_config["batch_size"]
|
| 551 |
-
if args.grad_accum is None:
|
| 552 |
-
args.grad_accum = gpu_config["grad_accum"]
|
| 553 |
-
accelerator.gradient_accumulation_steps = args.grad_accum
|
| 554 |
-
|
| 555 |
-
# Use bf16 for CUDA, fp32 for MPS/others
|
| 556 |
-
torch_dtype = torch.bfloat16 if device.type == 'cuda' else torch.float32
|
| 557 |
-
|
| 558 |
-
# Get RAM info and set limits
|
| 559 |
-
ram_total, ram_available = get_ram_info()
|
| 560 |
-
if args.ram_limit_gb is None:
|
| 561 |
-
args.ram_limit_gb = ram_total * 0.80
|
| 562 |
-
limit_ram_usage(args.ram_limit_gb)
|
| 563 |
-
|
| 564 |
-
if is_main:
|
| 565 |
-
log("=" * 60)
|
| 566 |
-
log("STAGE 2: Adapter + LoRA (Both Trainable) + Interleaved Output")
|
| 567 |
-
log("=" * 60)
|
| 568 |
-
log(f"Device: {device} ({accelerator.device.type})")
|
| 569 |
-
log(f"GPU: {gpu_config['name']} ({gpu_config['vram_gb']}GB)")
|
| 570 |
-
log(f"Num processes: {accelerator.num_processes}")
|
| 571 |
-
log(f"RAM: {ram_total:.1f}GB total, {ram_available:.1f}GB available")
|
| 572 |
-
log(f"Batch: {args.batch_size}, Grad accum: {args.grad_accum}")
|
| 573 |
-
log(f"LR: {args.lr}, Epochs: {args.epochs}")
|
| 574 |
-
log(f"LoRA: r={args.lora_r}, alpha={args.lora_alpha}")
|
| 575 |
-
log(f"Initial text ratio: {args.initial_text_ratio}")
|
| 576 |
-
log(f"Decay steps: {args.decay_steps}")
|
| 577 |
-
|
| 578 |
-
# Apply memory limits for CUDA
|
| 579 |
-
if device.type == 'cuda':
|
| 580 |
-
torch.cuda.set_per_process_memory_fraction(args.vram_fraction)
|
| 581 |
-
torch.cuda.empty_cache()
|
| 582 |
-
torch.backends.cudnn.benchmark = True
|
| 583 |
-
torch.set_float32_matmul_precision('high')
|
| 584 |
-
if is_main:
|
| 585 |
-
log(f"[MEMORY] VRAM limited to {args.vram_fraction*100:.0f}%")
|
| 586 |
-
log(f"[MEMORY] RAM limited to {args.ram_limit_gb:.1f}GB")
|
| 587 |
-
|
| 588 |
-
# HuggingFace login
|
| 589 |
-
hf_token = os.environ.get("HF_TOKEN")
|
| 590 |
-
if hf_token:
|
| 591 |
-
login(token=hf_token)
|
| 592 |
-
|
| 593 |
-
# Load tokenizer
|
| 594 |
-
tokenizer = AutoTokenizer.from_pretrained(args.model_path)
|
| 595 |
-
if tokenizer.pad_token is None:
|
| 596 |
-
tokenizer.pad_token = tokenizer.eos_token
|
| 597 |
-
|
| 598 |
-
# Load datasets
|
| 599 |
-
data_paths = [p.strip() for p in args.data.split(",")]
|
| 600 |
-
all_datasets = []
|
| 601 |
-
|
| 602 |
-
if is_main:
|
| 603 |
-
log("\nLoading datasets...")
|
| 604 |
-
|
| 605 |
-
for path in data_paths:
|
| 606 |
-
if os.path.exists(path):
|
| 607 |
-
data = torch.load(path, weights_only=False, mmap=True)
|
| 608 |
-
dataset = InterleavedDataset(data, tokenizer)
|
| 609 |
-
all_datasets.append(dataset)
|
| 610 |
-
if is_main:
|
| 611 |
-
log(f" {os.path.basename(path)}: {len(data):,} samples")
|
| 612 |
-
|
| 613 |
-
if len(all_datasets) == 0:
|
| 614 |
-
raise ValueError("No datasets loaded!")
|
| 615 |
-
|
| 616 |
-
combined_dataset = ConcatDataset(all_datasets) if len(all_datasets) > 1 else all_datasets[0]
|
| 617 |
-
|
| 618 |
-
# Demo/Test mode
|
| 619 |
-
if args.test:
|
| 620 |
-
combined_dataset = torch.utils.data.Subset(combined_dataset, range(min(5, len(combined_dataset))))
|
| 621 |
-
args.batch_size = min(args.batch_size, len(combined_dataset))
|
| 622 |
-
args.grad_accum = 1
|
| 623 |
-
elif args.demo:
|
| 624 |
-
combined_dataset = torch.utils.data.Subset(combined_dataset, range(min(1000, len(combined_dataset))))
|
| 625 |
-
args.batch_size = min(4, args.batch_size)
|
| 626 |
-
args.grad_accum = max(8, args.grad_accum)
|
| 627 |
-
|
| 628 |
-
if is_main:
|
| 629 |
-
log(f"Total samples: {len(combined_dataset):,}")
|
| 630 |
-
|
| 631 |
-
# Load LLM with LoRA
|
| 632 |
-
if is_main:
|
| 633 |
-
log(f"\nLoading LLM with LoRA: {args.model_path}")
|
| 634 |
-
|
| 635 |
-
llm = AutoModelForCausalLM.from_pretrained(
|
| 636 |
-
args.model_path,
|
| 637 |
-
torch_dtype=torch_dtype,
|
| 638 |
-
attn_implementation="sdpa",
|
| 639 |
-
)
|
| 640 |
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
r=args.lora_r,
|
| 644 |
-
lora_alpha=args.lora_alpha,
|
| 645 |
-
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
|
| 646 |
-
lora_dropout=args.lora_dropout,
|
| 647 |
-
bias="none",
|
| 648 |
-
task_type=TaskType.CAUSAL_LM
|
| 649 |
-
)
|
| 650 |
-
llm = get_peft_model(llm, lora_config)
|
| 651 |
-
|
| 652 |
-
# Auto-detect gradient checkpointing based on VRAM
|
| 653 |
-
# Enable if VRAM < 20GB (needed for Stage 2 with LoRA)
|
| 654 |
-
use_gradient_checkpointing = args.gradient_checkpointing
|
| 655 |
-
if not use_gradient_checkpointing and torch.cuda.is_available():
|
| 656 |
-
vram_gb = torch.cuda.get_device_properties(0).total_memory / 1024**3
|
| 657 |
-
if vram_gb < 20:
|
| 658 |
-
use_gradient_checkpointing = True
|
| 659 |
-
if is_main:
|
| 660 |
-
log(f"[AUTO] Enabling gradient checkpointing (VRAM={vram_gb:.1f}GB < 20GB)")
|
| 661 |
-
else:
|
| 662 |
-
if is_main:
|
| 663 |
-
log(f"[AUTO] Gradient checkpointing disabled for speed (VRAM={vram_gb:.1f}GB >= 20GB)")
|
| 664 |
-
|
| 665 |
-
if use_gradient_checkpointing:
|
| 666 |
-
llm.gradient_checkpointing_enable()
|
| 667 |
-
if is_main:
|
| 668 |
-
log("[MEMORY] Gradient checkpointing enabled")
|
| 669 |
-
|
| 670 |
-
if is_main:
|
| 671 |
-
llm.print_trainable_parameters()
|
| 672 |
-
|
| 673 |
-
# Create adapter (TRAINABLE - same architecture as Stage 1)
|
| 674 |
-
adapter = SpeechAdapter(
|
| 675 |
-
whisper_dim=1280,
|
| 676 |
-
llm_dim=3072,
|
| 677 |
-
downsample=5,
|
| 678 |
-
intermediate_dim=2048
|
| 679 |
-
).to(dtype=torch_dtype)
|
| 680 |
-
|
| 681 |
-
# Load Stage 1 adapter checkpoint
|
| 682 |
-
if args.stage1_ckpt and os.path.exists(args.stage1_ckpt):
|
| 683 |
-
if is_main:
|
| 684 |
-
log(f"\nLoading Stage 1 adapter from: {args.stage1_ckpt}")
|
| 685 |
-
ckpt = torch.load(args.stage1_ckpt, map_location="cpu", weights_only=False)
|
| 686 |
-
if "adapter" in ckpt:
|
| 687 |
-
adapter.load_state_dict(ckpt["adapter"])
|
| 688 |
-
if is_main:
|
| 689 |
-
log(" Adapter weights loaded successfully!")
|
| 690 |
-
if "loss" in ckpt:
|
| 691 |
-
log(f" Stage 1 final loss: {ckpt['loss']:.4f}")
|
| 692 |
-
if "text_ratio" in ckpt:
|
| 693 |
-
log(f" Stage 1 final text_ratio: {ckpt['text_ratio']:.1f}")
|
| 694 |
-
else:
|
| 695 |
-
if is_main:
|
| 696 |
-
log(" WARNING: No 'adapter' key in checkpoint, using random init")
|
| 697 |
-
else:
|
| 698 |
-
if is_main:
|
| 699 |
-
log("\nWARNING: No Stage 1 checkpoint provided, adapter starting from random init")
|
| 700 |
-
log(" Recommended: --stage1_ckpt checkpoints/stage1_best.pt")
|
| 701 |
-
|
| 702 |
-
adapter_params = sum(p.numel() for p in adapter.parameters())
|
| 703 |
-
lora_params = sum(p.numel() for p in llm.parameters() if p.requires_grad)
|
| 704 |
-
|
| 705 |
-
if is_main:
|
| 706 |
-
log(f"\nTrainable parameters:")
|
| 707 |
-
log(f" Adapter: {adapter_params:,} ({adapter_params/1e6:.1f}M)")
|
| 708 |
-
log(f" LoRA: {lora_params:,} ({lora_params/1e6:.1f}M)")
|
| 709 |
-
log(f" Total: {adapter_params + lora_params:,} ({(adapter_params + lora_params)/1e6:.1f}M)")
|
| 710 |
-
|
| 711 |
-
# Optimizer (Adapter + LoRA together)
|
| 712 |
-
all_params = list(adapter.parameters()) + [p for p in llm.parameters() if p.requires_grad]
|
| 713 |
-
optimizer = torch.optim.AdamW(all_params, lr=args.lr, weight_decay=0.01)
|
| 714 |
-
|
| 715 |
-
# Training state
|
| 716 |
-
global_step = 0
|
| 717 |
-
start_epoch = 0
|
| 718 |
-
best_loss = float("inf")
|
| 719 |
-
current_text_ratio = args.initial_text_ratio
|
| 720 |
-
|
| 721 |
-
# Resume from Stage 2 checkpoint
|
| 722 |
-
if args.resume and os.path.exists(args.resume):
|
| 723 |
-
if is_main:
|
| 724 |
-
log(f"\nResuming from: {args.resume}")
|
| 725 |
-
ckpt = torch.load(args.resume, map_location="cpu", weights_only=False)
|
| 726 |
if "adapter" in ckpt:
|
| 727 |
-
adapter.load_state_dict(ckpt["adapter"])
|
| 728 |
if "lora" in ckpt:
|
| 729 |
-
|
|
|
|
| 730 |
if "optimizer" in ckpt:
|
| 731 |
-
optimizer.load_state_dict(ckpt["optimizer"])
|
| 732 |
if "step" in ckpt:
|
| 733 |
-
global_step = ckpt["step"]
|
| 734 |
if "epoch" in ckpt:
|
| 735 |
-
start_epoch = ckpt["epoch"]
|
| 736 |
if "text_ratio" in ckpt:
|
| 737 |
-
current_text_ratio = ckpt["text_ratio"]
|
| 738 |
-
if "loss" in ckpt:
|
| 739 |
-
best_loss = ckpt["loss"]
|
| 740 |
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
return collate_fn(batch, current_ratio, tokenizer=tokenizer)
|
| 748 |
-
|
| 749 |
-
train_loader = DataLoader(
|
| 750 |
-
combined_dataset,
|
| 751 |
-
batch_size=args.batch_size,
|
| 752 |
-
shuffle=True,
|
| 753 |
-
collate_fn=collate_with_ratio,
|
| 754 |
-
num_workers=4,
|
| 755 |
-
pin_memory=True
|
| 756 |
-
)
|
| 757 |
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
|
|
|
|
|
|
|
|
|
| 762 |
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 766 |
|
| 767 |
-
|
|
|
|
| 768 |
|
| 769 |
-
|
| 770 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 771 |
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
unwrapped_llm = accelerator.unwrap_model(llm)
|
| 784 |
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
accum_loss = 0
|
| 789 |
|
| 790 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 791 |
|
| 792 |
-
for batch_idx, batch in enumerate(pbar):
|
| 793 |
-
# Update text ratio dynamically
|
| 794 |
-
current_text_ratio = get_text_ratio(global_step, args.decay_steps, args.initial_text_ratio)
|
| 795 |
|
| 796 |
-
|
| 797 |
-
|
|
|
|
|
|
|
|
|
|
| 798 |
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
# Access base model's embed_tokens through PEFT wrapper
|
| 808 |
-
token_embeds = unwrapped_llm.get_base_model().model.embed_tokens(input_tokens)
|
| 809 |
|
| 810 |
-
|
| 811 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 812 |
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
|
|
|
| 816 |
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
|
|
|
|
|
|
| 820 |
|
| 821 |
-
|
| 822 |
-
|
|
|
|
| 823 |
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
ignore_index=-100,
|
| 828 |
-
label_smoothing=args.label_smoothing
|
| 829 |
-
)
|
| 830 |
|
| 831 |
-
|
| 832 |
-
|
|
|
|
|
|
|
| 833 |
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
|
| 838 |
-
|
| 839 |
-
optimizer.zero_grad()
|
| 840 |
|
| 841 |
-
# Update after accumulation
|
| 842 |
-
if accelerator.sync_gradients:
|
| 843 |
-
if global_step < warmup_steps:
|
| 844 |
-
lr_scale = (global_step + 1) / warmup_steps
|
| 845 |
-
for pg in optimizer.param_groups:
|
| 846 |
-
pg["lr"] = args.lr * lr_scale
|
| 847 |
-
else:
|
| 848 |
-
scheduler.step()
|
| 849 |
|
| 850 |
-
|
| 851 |
-
|
|
|
|
| 852 |
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
unwrapped_adapter = accelerator.unwrap_model(adapter)
|
| 863 |
-
unwrapped_llm = accelerator.unwrap_model(llm)
|
| 864 |
-
ckpt_path = os.path.join(args.output_dir, f"stage2_step{global_step}.pt")
|
| 865 |
-
save_checkpoint_async({
|
| 866 |
-
"adapter": unwrapped_adapter.state_dict(),
|
| 867 |
-
"lora": unwrapped_llm.state_dict(),
|
| 868 |
-
"optimizer": optimizer.state_dict(),
|
| 869 |
-
"step": global_step,
|
| 870 |
-
"epoch": epoch,
|
| 871 |
-
"loss": accum_loss,
|
| 872 |
-
"text_ratio": current_text_ratio
|
| 873 |
-
}, ckpt_path, is_main)
|
| 874 |
-
|
| 875 |
-
accum_loss = 0
|
| 876 |
-
|
| 877 |
-
# Epoch end - divide by grad_accum to get per-batch average
|
| 878 |
-
avg_loss = epoch_loss / max(1, steps_per_epoch) / args.grad_accum
|
| 879 |
-
|
| 880 |
-
if is_main:
|
| 881 |
-
log(f"Epoch {epoch+1} avg loss: {avg_loss:.4f}, text_ratio: {current_text_ratio:.1f}")
|
| 882 |
-
|
| 883 |
-
accelerator.wait_for_everyone()
|
| 884 |
-
unwrapped_adapter = accelerator.unwrap_model(adapter)
|
| 885 |
-
unwrapped_llm = accelerator.unwrap_model(llm)
|
| 886 |
-
ckpt_path = os.path.join(args.output_dir, f"stage2_epoch{epoch+1}.pt")
|
| 887 |
-
save_checkpoint_async({
|
| 888 |
-
"adapter": unwrapped_adapter.state_dict(),
|
| 889 |
-
"lora": unwrapped_llm.state_dict(),
|
| 890 |
-
"optimizer": optimizer.state_dict(),
|
| 891 |
-
"step": global_step,
|
| 892 |
-
"epoch": epoch + 1,
|
| 893 |
-
"loss": avg_loss,
|
| 894 |
-
"text_ratio": current_text_ratio
|
| 895 |
-
}, ckpt_path, is_main)
|
| 896 |
-
|
| 897 |
-
if avg_loss < best_loss:
|
| 898 |
-
best_loss = avg_loss
|
| 899 |
-
best_path = os.path.join(args.output_dir, "stage2_best.pt")
|
| 900 |
-
save_checkpoint_async({
|
| 901 |
-
"adapter": unwrapped_adapter.state_dict(),
|
| 902 |
-
"lora": unwrapped_llm.state_dict(),
|
| 903 |
-
"step": global_step,
|
| 904 |
-
"epoch": epoch + 1,
|
| 905 |
-
"loss": best_loss,
|
| 906 |
-
"text_ratio": current_text_ratio
|
| 907 |
-
}, best_path, is_main)
|
| 908 |
-
log(f"Best model saved! Loss: {best_loss:.4f}")
|
| 909 |
-
|
| 910 |
-
# Finish
|
| 911 |
-
accelerator.wait_for_everyone()
|
| 912 |
-
if is_main:
|
| 913 |
-
wait_for_checkpoints()
|
| 914 |
-
log("\n" + "=" * 60)
|
| 915 |
-
log("STAGE 2 COMPLETE!")
|
| 916 |
-
log(f"Best loss: {best_loss:.4f}")
|
| 917 |
-
log(f"Final text_ratio: {current_text_ratio:.1f}")
|
| 918 |
-
log("=" * 60)
|
| 919 |
-
log("\nCheckpoints saved:")
|
| 920 |
-
log(f" Best: {args.output_dir}/stage2_best.pt")
|
| 921 |
-
log(f" Last: {args.output_dir}/stage2_epoch{args.epochs}.pt")
|
| 922 |
|
| 923 |
|
| 924 |
if __name__ == "__main__":
|
|
|
|
| 9 |
- Continues scheduled interleaving (90% text -> 0% text)
|
| 10 |
|
| 11 |
Usage:
|
| 12 |
+
python passo3_finetune_stage2.py --data data.pt --stage1_ckpt checkpoints/stage1_best.pt --epochs 3
|
| 13 |
|
| 14 |
Based on IST-LM paper + LLaMA-Omni 2 staging approach.
|
| 15 |
"""
|
| 16 |
|
|
|
|
|
|
|
| 17 |
import argparse
|
| 18 |
import torch
|
| 19 |
+
from typing import Dict, Any
|
| 20 |
+
from itertools import chain
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
+
from training.config import TrainingConfig, GPUConfig, LoRAConfig
|
| 23 |
+
from training.models import SpeechAdapter, ModelFactory
|
| 24 |
+
from training.checkpoint import Stage2CheckpointManager, TrainingState
|
| 25 |
+
from training.trainer import BaseTrainer
|
| 26 |
+
from training.utils import log, should_enable_gradient_checkpointing
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
|
| 29 |
+
class Stage2Trainer(BaseTrainer):
|
|
|
|
|
|
|
|
|
|
| 30 |
"""
|
| 31 |
+
Stage 2 Trainer: Adapter + LoRA together.
|
|
|
|
|
|
|
| 32 |
|
| 33 |
+
Continues from Stage 1, adding LoRA to the LLM and training
|
| 34 |
+
both adapter and LoRA jointly.
|
| 35 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
+
def __init__(self, config: TrainingConfig, stage1_ckpt: str, lora_config: LoRAConfig = None):
|
| 38 |
+
super().__init__(config)
|
| 39 |
+
self.stage1_ckpt = stage1_ckpt
|
| 40 |
+
self.lora_config = lora_config or LoRAConfig()
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
def _get_stage_name(self) -> str:
|
| 43 |
+
return "STAGE 2: Adapter + LoRA (Joint Training)"
|
| 44 |
|
| 45 |
+
def _setup_models(self):
|
| 46 |
+
"""Setup adapter and LLM with LoRA."""
|
| 47 |
+
gpu_config = GPUConfig.auto_detect()
|
| 48 |
+
dtype = gpu_config.dtype
|
| 49 |
|
| 50 |
+
# Check gradient checkpointing
|
| 51 |
+
use_checkpointing = self.config.gradient_checkpointing or \
|
| 52 |
+
should_enable_gradient_checkpointing(
|
| 53 |
+
gpu_config.vram_gb,
|
| 54 |
+
self.config.dynamic_decay
|
| 55 |
+
)
|
| 56 |
|
| 57 |
+
if self.is_main:
|
| 58 |
+
log(f"\nLoading LLM: {self.config.model_path}")
|
| 59 |
|
| 60 |
+
# Load LLM (not frozen, will apply LoRA)
|
| 61 |
+
self.llm = ModelFactory.create_llm(
|
| 62 |
+
self.config.model_path,
|
| 63 |
+
dtype=dtype,
|
| 64 |
+
freeze=False,
|
| 65 |
+
gradient_checkpointing=use_checkpointing,
|
|
|
|
|
|
|
| 66 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
+
# Apply LoRA
|
| 69 |
+
if self.is_main:
|
| 70 |
+
log(f"Applying LoRA (r={self.lora_config.r}, alpha={self.lora_config.alpha})")
|
|
|
|
|
|
|
| 71 |
|
| 72 |
+
self.llm = ModelFactory.apply_lora(self.llm, self.lora_config)
|
| 73 |
|
| 74 |
+
if self.is_main and use_checkpointing:
|
| 75 |
+
log("[Memory] Gradient checkpointing enabled")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
+
# Create and load adapter from Stage 1
|
| 78 |
+
self.adapter = SpeechAdapter().to(dtype=dtype)
|
| 79 |
|
| 80 |
+
if self.is_main:
|
| 81 |
+
log(f"\nLoading adapter from Stage 1: {self.stage1_ckpt}")
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
+
ckpt = torch.load(self.stage1_ckpt, map_location="cpu", weights_only=False)
|
| 84 |
+
if "adapter" in ckpt:
|
| 85 |
+
self.adapter.load_state_dict(ckpt["adapter"])
|
| 86 |
+
else:
|
| 87 |
+
self.adapter.load_state_dict(ckpt)
|
| 88 |
+
|
| 89 |
+
# Log parameters
|
| 90 |
+
if self.is_main:
|
| 91 |
+
adapter_params = self.adapter.get_num_params()
|
| 92 |
+
lora_params = sum(p.numel() for p in self.llm.parameters() if p.requires_grad)
|
| 93 |
+
total_params = adapter_params + lora_params
|
| 94 |
+
|
| 95 |
+
log(f"\nTrainable parameters:")
|
| 96 |
+
log(f" Adapter: {adapter_params:,} ({adapter_params/1e6:.1f}M)")
|
| 97 |
+
log(f" LoRA: {lora_params:,} ({lora_params/1e6:.1f}M)")
|
| 98 |
+
log(f" Total: {total_params:,} ({total_params/1e6:.1f}M)")
|
| 99 |
+
|
| 100 |
+
def _setup_optimizer(self):
|
| 101 |
+
"""Setup optimizer for adapter + LoRA."""
|
| 102 |
+
trainable_params = chain(
|
| 103 |
+
self.adapter.parameters(),
|
| 104 |
+
(p for p in self.llm.parameters() if p.requires_grad)
|
| 105 |
+
)
|
| 106 |
|
| 107 |
+
self.optimizer = torch.optim.AdamW(
|
| 108 |
+
trainable_params,
|
| 109 |
+
lr=self.config.learning_rate,
|
| 110 |
+
weight_decay=self.config.weight_decay,
|
| 111 |
+
)
|
| 112 |
|
| 113 |
+
def _setup_checkpoint_manager(self):
|
| 114 |
+
"""Setup Stage 2 checkpoint manager."""
|
| 115 |
+
self.checkpoint_manager = Stage2CheckpointManager(
|
| 116 |
+
self.config.output_dir,
|
| 117 |
+
verbose=self.is_main,
|
| 118 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
+
def _load_checkpoint(self, ckpt: Dict[str, Any]):
|
| 121 |
+
"""Load checkpoint state."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
if "adapter" in ckpt:
|
| 123 |
+
self.adapter.load_state_dict(ckpt["adapter"])
|
| 124 |
if "lora" in ckpt:
|
| 125 |
+
# Load LoRA weights
|
| 126 |
+
self.llm.load_state_dict(ckpt["lora"], strict=False)
|
| 127 |
if "optimizer" in ckpt:
|
| 128 |
+
self.optimizer.load_state_dict(ckpt["optimizer"])
|
| 129 |
if "step" in ckpt:
|
| 130 |
+
self.global_step = ckpt["step"]
|
| 131 |
if "epoch" in ckpt:
|
| 132 |
+
self.start_epoch = ckpt["epoch"]
|
| 133 |
if "text_ratio" in ckpt:
|
| 134 |
+
self.current_text_ratio = ckpt["text_ratio"]
|
|
|
|
|
|
|
| 135 |
|
| 136 |
+
def _get_trainable_params(self):
|
| 137 |
+
"""Get trainable parameters (adapter + LoRA)."""
|
| 138 |
+
return chain(
|
| 139 |
+
self.adapter.parameters(),
|
| 140 |
+
(p for p in self.llm.parameters() if p.requires_grad)
|
| 141 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
+
def _get_lora_state_dict(self) -> Dict[str, Any]:
|
| 144 |
+
"""Get LoRA state dict."""
|
| 145 |
+
unwrapped_llm = self.accelerator.unwrap_model(self.llm)
|
| 146 |
+
return {
|
| 147 |
+
k: v for k, v in unwrapped_llm.state_dict().items()
|
| 148 |
+
if "lora" in k.lower()
|
| 149 |
+
}
|
| 150 |
|
| 151 |
+
def _save_step_checkpoint(self, loss: float):
|
| 152 |
+
"""Save step checkpoint."""
|
| 153 |
+
self.accelerator.wait_for_everyone()
|
| 154 |
+
unwrapped_adapter = self.accelerator.unwrap_model(self.adapter)
|
| 155 |
+
|
| 156 |
+
state = TrainingState(
|
| 157 |
+
step=self.global_step,
|
| 158 |
+
epoch=self.start_epoch,
|
| 159 |
+
loss=loss,
|
| 160 |
+
text_ratio=self.current_text_ratio,
|
| 161 |
+
best_loss=self.best_loss,
|
| 162 |
+
)
|
| 163 |
|
| 164 |
+
# Get LoRA state
|
| 165 |
+
lora_state = self._get_lora_state_dict()
|
| 166 |
|
| 167 |
+
# Save checkpoint
|
| 168 |
+
state_dict = {
|
| 169 |
+
"adapter": unwrapped_adapter.state_dict(),
|
| 170 |
+
"lora": lora_state,
|
| 171 |
+
"optimizer": self.optimizer.state_dict(),
|
| 172 |
+
**state.to_dict()
|
| 173 |
+
}
|
| 174 |
|
| 175 |
+
self.checkpoint_manager.save(
|
| 176 |
+
state_dict,
|
| 177 |
+
f"stage2_step{self.global_step}.pt",
|
| 178 |
+
)
|
| 179 |
|
| 180 |
+
def _finish_epoch(self, epoch: int, epoch_loss: float):
|
| 181 |
+
"""Finish epoch and save checkpoints."""
|
| 182 |
+
avg_loss = epoch_loss / max(1, self.steps_per_epoch) / self.config.grad_accum
|
| 183 |
|
| 184 |
+
if self.is_main:
|
| 185 |
+
log(f"Epoch {epoch+1} avg loss: {avg_loss:.4f}, text_ratio: {self.current_text_ratio:.1f}")
|
|
|
|
| 186 |
|
| 187 |
+
self.accelerator.wait_for_everyone()
|
| 188 |
+
unwrapped_adapter = self.accelerator.unwrap_model(self.adapter)
|
| 189 |
+
lora_state = self._get_lora_state_dict()
|
|
|
|
| 190 |
|
| 191 |
+
state = TrainingState(
|
| 192 |
+
step=self.global_step,
|
| 193 |
+
epoch=epoch + 1,
|
| 194 |
+
loss=avg_loss,
|
| 195 |
+
text_ratio=self.current_text_ratio,
|
| 196 |
+
best_loss=self.best_loss,
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
# Save epoch checkpoint
|
| 200 |
+
state_dict = {
|
| 201 |
+
"adapter": unwrapped_adapter.state_dict(),
|
| 202 |
+
"lora": lora_state,
|
| 203 |
+
"optimizer": self.optimizer.state_dict(),
|
| 204 |
+
**state.to_dict()
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
self.checkpoint_manager.save(
|
| 208 |
+
state_dict,
|
| 209 |
+
f"stage2_epoch{epoch+1}.pt",
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
# Save best if improved
|
| 213 |
+
if avg_loss < self.best_loss:
|
| 214 |
+
self.best_loss = avg_loss
|
| 215 |
+
state.best_loss = self.best_loss
|
| 216 |
+
|
| 217 |
+
self.checkpoint_manager.save_best(
|
| 218 |
+
unwrapped_adapter.state_dict(),
|
| 219 |
+
state,
|
| 220 |
+
lora_state=lora_state,
|
| 221 |
+
)
|
| 222 |
+
log(f"Best model saved! Loss: {self.best_loss:.4f}")
|
| 223 |
|
|
|
|
|
|
|
|
|
|
| 224 |
|
| 225 |
+
def parse_args():
|
| 226 |
+
"""Parse command line arguments."""
|
| 227 |
+
parser = argparse.ArgumentParser(
|
| 228 |
+
description="Stage 2: Adapter + LoRA with Interleaved Output"
|
| 229 |
+
)
|
| 230 |
|
| 231 |
+
# Data
|
| 232 |
+
parser.add_argument("--data", type=str, required=True,
|
| 233 |
+
help="Dataset path(s), comma-separated")
|
| 234 |
+
parser.add_argument("--output_dir", type=str, default="./checkpoints")
|
| 235 |
|
| 236 |
+
# Stage 1 checkpoint (required)
|
| 237 |
+
parser.add_argument("--stage1_ckpt", type=str, required=True,
|
| 238 |
+
help="Path to Stage 1 best checkpoint")
|
|
|
|
|
|
|
| 239 |
|
| 240 |
+
# Training
|
| 241 |
+
parser.add_argument("--lr", type=float, default=5e-5)
|
| 242 |
+
parser.add_argument("--epochs", type=int, default=3,
|
| 243 |
+
help="3+ epochs for joint training")
|
| 244 |
+
parser.add_argument("--batch_size", type=int, default=None)
|
| 245 |
+
parser.add_argument("--grad_accum", type=int, default=None)
|
| 246 |
+
parser.add_argument("--warmup_ratio", type=float, default=0.03)
|
| 247 |
+
parser.add_argument("--max_grad_norm", type=float, default=1.0)
|
| 248 |
+
parser.add_argument("--label_smoothing", type=float, default=0.1)
|
| 249 |
+
parser.add_argument("--max_seq_len", type=int, default=2048)
|
| 250 |
|
| 251 |
+
# LoRA
|
| 252 |
+
parser.add_argument("--lora_r", type=int, default=16)
|
| 253 |
+
parser.add_argument("--lora_alpha", type=int, default=32)
|
| 254 |
+
parser.add_argument("--lora_dropout", type=float, default=0.05)
|
| 255 |
|
| 256 |
+
# Scheduled interleaving
|
| 257 |
+
parser.add_argument("--initial_text_ratio", type=float, default=0.9)
|
| 258 |
+
parser.add_argument("--decay_steps", type=int, default=300)
|
| 259 |
+
parser.add_argument("--dynamic_decay", action="store_true")
|
| 260 |
+
parser.add_argument("--final_audio_portion", type=float, default=0.2)
|
| 261 |
|
| 262 |
+
# Model
|
| 263 |
+
parser.add_argument("--model_path", type=str,
|
| 264 |
+
default="canopylabs/3b-es_it-ft-research_release")
|
| 265 |
|
| 266 |
+
# Checkpointing
|
| 267 |
+
parser.add_argument("--save_steps", type=int, default=200)
|
| 268 |
+
parser.add_argument("--resume", type=str, default=None)
|
|
|
|
|
|
|
|
|
|
| 269 |
|
| 270 |
+
# Memory
|
| 271 |
+
parser.add_argument("--vram_fraction", type=float, default=0.80)
|
| 272 |
+
parser.add_argument("--ram_limit_gb", type=float, default=None)
|
| 273 |
+
parser.add_argument("--gradient_checkpointing", action="store_true")
|
| 274 |
|
| 275 |
+
# Modes
|
| 276 |
+
parser.add_argument("--demo", action="store_true")
|
| 277 |
+
parser.add_argument("--test", action="store_true")
|
| 278 |
|
| 279 |
+
return parser.parse_args()
|
|
|
|
| 280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
|
| 282 |
+
def main():
|
| 283 |
+
args = parse_args()
|
| 284 |
+
config = TrainingConfig.from_args(args)
|
| 285 |
|
| 286 |
+
lora_config = LoRAConfig(
|
| 287 |
+
r=args.lora_r,
|
| 288 |
+
alpha=args.lora_alpha,
|
| 289 |
+
dropout=args.lora_dropout,
|
| 290 |
+
)
|
| 291 |
|
| 292 |
+
trainer = Stage2Trainer(config, args.stage1_ckpt, lora_config)
|
| 293 |
+
trainer.setup()
|
| 294 |
+
trainer.train()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
|
| 297 |
if __name__ == "__main__":
|
passo4_inference.py
CHANGED
|
@@ -14,12 +14,12 @@ from pathlib import Path
|
|
| 14 |
|
| 15 |
# SNAC token configuration
|
| 16 |
SNAC_BASE = 128266
|
|
|
|
| 17 |
EOS_TOKEN = 128009
|
| 18 |
|
| 19 |
def load_models(checkpoint_path: str, device: str = "cuda"):
|
| 20 |
"""Load all models for inference."""
|
| 21 |
from transformers import WhisperModel, WhisperFeatureExtractor, AutoTokenizer
|
| 22 |
-
from peft import PeftModel
|
| 23 |
import snac
|
| 24 |
|
| 25 |
print("Loading models...")
|
|
@@ -34,8 +34,11 @@ def load_models(checkpoint_path: str, device: str = "cuda"):
|
|
| 34 |
print(f" Loading checkpoint: {checkpoint_path}")
|
| 35 |
checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
|
| 36 |
|
|
|
|
|
|
|
|
|
|
| 37 |
# Load LLM with LoRA
|
| 38 |
-
print(" Loading LLM
|
| 39 |
from transformers import AutoModelForCausalLM
|
| 40 |
|
| 41 |
llm = AutoModelForCausalLM.from_pretrained(
|
|
@@ -45,20 +48,41 @@ def load_models(checkpoint_path: str, device: str = "cuda"):
|
|
| 45 |
)
|
| 46 |
|
| 47 |
# Load LoRA weights if present
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
llm.eval()
|
| 64 |
|
|
@@ -71,9 +95,18 @@ def load_models(checkpoint_path: str, device: str = "cuda"):
|
|
| 71 |
downsample=5
|
| 72 |
).to(device)
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
adapter.eval()
|
| 78 |
|
| 79 |
# Load SNAC decoder
|
|
@@ -84,6 +117,12 @@ def load_models(checkpoint_path: str, device: str = "cuda"):
|
|
| 84 |
# Load tokenizer
|
| 85 |
tokenizer = AutoTokenizer.from_pretrained("canopylabs/3b-es_it-ft-research_release")
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
print("Models loaded!")
|
| 88 |
return whisper_model, feature_extractor, llm, adapter, snac_model, tokenizer
|
| 89 |
|
|
@@ -109,39 +148,60 @@ def encode_audio(audio_path: str, whisper_model, feature_extractor, adapter, dev
|
|
| 109 |
|
| 110 |
|
| 111 |
def decode_snac_tokens(tokens: list, snac_model, device: str):
|
| 112 |
-
"""Decode SNAC tokens to audio waveform.
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
layer0_tokens = []
|
| 116 |
layer1_tokens = []
|
| 117 |
layer2_tokens = []
|
| 118 |
|
| 119 |
-
# Parse
|
| 120 |
-
i
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
i
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
codes = [
|
| 142 |
-
torch.tensor([layer0_tokens
|
| 143 |
-
torch.tensor([layer1_tokens
|
| 144 |
-
torch.tensor([layer2_tokens
|
| 145 |
]
|
| 146 |
|
| 147 |
# Decode
|
|
@@ -156,45 +216,104 @@ def generate_response(
|
|
| 156 |
whisper_model, feature_extractor, llm, adapter, snac_model, tokenizer,
|
| 157 |
device: str,
|
| 158 |
max_new_tokens: int = 500,
|
| 159 |
-
temperature: float = 0.7
|
|
|
|
|
|
|
| 160 |
):
|
| 161 |
"""Generate speech response from audio input."""
|
| 162 |
|
| 163 |
# Encode input audio
|
| 164 |
print("Encoding input audio...")
|
| 165 |
audio_embeddings = encode_audio(audio_input, whisper_model, feature_extractor, adapter, device)
|
|
|
|
| 166 |
|
| 167 |
# Generate with LLM
|
| 168 |
print("Generating response...")
|
| 169 |
|
| 170 |
-
# Create input embeds by concatenating audio embeddings with start tokens
|
| 171 |
with torch.no_grad():
|
| 172 |
# Get embeddings layer
|
| 173 |
embed_layer = llm.get_input_embeddings()
|
| 174 |
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
generated_tokens = outputs[0].tolist()
|
| 195 |
-
snac_tokens = [t for t in generated_tokens if SNAC_BASE <= t < SNAC_BASE + 3 * 4096]
|
| 196 |
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
# Decode to audio
|
| 200 |
print("Decoding to audio...")
|
|
@@ -208,8 +327,10 @@ def main():
|
|
| 208 |
parser.add_argument("--checkpoint", type=str, required=True, help="Path to model checkpoint")
|
| 209 |
parser.add_argument("--input", type=str, required=True, help="Input audio file")
|
| 210 |
parser.add_argument("--output", type=str, default="output.wav", help="Output audio file")
|
| 211 |
-
parser.add_argument("--max_tokens", type=int, default=
|
| 212 |
-
parser.add_argument("--temperature", type=float, default=0.7, help="Sampling temperature")
|
|
|
|
|
|
|
| 213 |
args = parser.parse_args()
|
| 214 |
|
| 215 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
@@ -226,7 +347,9 @@ def main():
|
|
| 226 |
whisper_model, feature_extractor, llm, adapter, snac_model, tokenizer,
|
| 227 |
device,
|
| 228 |
max_new_tokens=args.max_tokens,
|
| 229 |
-
temperature=args.temperature
|
|
|
|
|
|
|
| 230 |
)
|
| 231 |
|
| 232 |
# Save output
|
|
|
|
| 14 |
|
| 15 |
# SNAC token configuration
|
| 16 |
SNAC_BASE = 128266
|
| 17 |
+
SNAC_MAX = SNAC_BASE + 7 * 4096 # 7 positions per frame, 4096 tokens each
|
| 18 |
EOS_TOKEN = 128009
|
| 19 |
|
| 20 |
def load_models(checkpoint_path: str, device: str = "cuda"):
|
| 21 |
"""Load all models for inference."""
|
| 22 |
from transformers import WhisperModel, WhisperFeatureExtractor, AutoTokenizer
|
|
|
|
| 23 |
import snac
|
| 24 |
|
| 25 |
print("Loading models...")
|
|
|
|
| 34 |
print(f" Loading checkpoint: {checkpoint_path}")
|
| 35 |
checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
|
| 36 |
|
| 37 |
+
# Debug: show checkpoint keys
|
| 38 |
+
print(f" Checkpoint keys: {list(checkpoint.keys())}")
|
| 39 |
+
|
| 40 |
# Load LLM with LoRA
|
| 41 |
+
print(" Loading LLM...")
|
| 42 |
from transformers import AutoModelForCausalLM
|
| 43 |
|
| 44 |
llm = AutoModelForCausalLM.from_pretrained(
|
|
|
|
| 48 |
)
|
| 49 |
|
| 50 |
# Load LoRA weights if present
|
| 51 |
+
lora_loaded = False
|
| 52 |
+
for lora_key in ['lora', 'lora_state_dict']:
|
| 53 |
+
if lora_key in checkpoint:
|
| 54 |
+
print(f" Found LoRA weights with key '{lora_key}'")
|
| 55 |
+
from peft import LoraConfig, get_peft_model
|
| 56 |
+
lora_config = LoraConfig(
|
| 57 |
+
r=16,
|
| 58 |
+
lora_alpha=32,
|
| 59 |
+
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
|
| 60 |
+
lora_dropout=0.0, # No dropout for inference
|
| 61 |
+
bias="none",
|
| 62 |
+
task_type="CAUSAL_LM"
|
| 63 |
+
)
|
| 64 |
+
llm = get_peft_model(llm, lora_config)
|
| 65 |
+
|
| 66 |
+
# Load the LoRA state dict
|
| 67 |
+
lora_state = checkpoint[lora_key]
|
| 68 |
+
print(f" LoRA state dict has {len(lora_state)} keys")
|
| 69 |
+
if len(lora_state) > 0:
|
| 70 |
+
print(f" Sample keys: {list(lora_state.keys())[:3]}")
|
| 71 |
+
|
| 72 |
+
# Try to load with strict=False to see what matches
|
| 73 |
+
result = llm.load_state_dict(lora_state, strict=False)
|
| 74 |
+
print(f" LoRA load - Missing: {len(result.missing_keys)}, Unexpected: {len(result.unexpected_keys)}")
|
| 75 |
+
if result.missing_keys:
|
| 76 |
+
print(f" Missing keys (first 3): {result.missing_keys[:3]}")
|
| 77 |
+
if result.unexpected_keys:
|
| 78 |
+
print(f" Unexpected keys (first 3): {result.unexpected_keys[:3]}")
|
| 79 |
+
|
| 80 |
+
lora_loaded = True
|
| 81 |
+
break
|
| 82 |
+
|
| 83 |
+
if not lora_loaded:
|
| 84 |
+
print(" WARNING: No LoRA weights found in checkpoint! Model will use base weights only.")
|
| 85 |
+
print(" This might result in no SNAC token generation.")
|
| 86 |
|
| 87 |
llm.eval()
|
| 88 |
|
|
|
|
| 95 |
downsample=5
|
| 96 |
).to(device)
|
| 97 |
|
| 98 |
+
adapter_loaded = False
|
| 99 |
+
for adapter_key in ['adapter', 'adapter_state_dict']:
|
| 100 |
+
if adapter_key in checkpoint:
|
| 101 |
+
print(f" Found adapter weights with key '{adapter_key}'")
|
| 102 |
+
result = adapter.load_state_dict(checkpoint[adapter_key], strict=False)
|
| 103 |
+
print(f" Adapter load - Missing: {len(result.missing_keys)}, Unexpected: {len(result.unexpected_keys)}")
|
| 104 |
+
adapter_loaded = True
|
| 105 |
+
break
|
| 106 |
+
|
| 107 |
+
if not adapter_loaded:
|
| 108 |
+
print(" WARNING: No adapter weights found in checkpoint!")
|
| 109 |
+
|
| 110 |
adapter.eval()
|
| 111 |
|
| 112 |
# Load SNAC decoder
|
|
|
|
| 117 |
# Load tokenizer
|
| 118 |
tokenizer = AutoTokenizer.from_pretrained("canopylabs/3b-es_it-ft-research_release")
|
| 119 |
|
| 120 |
+
# Debug tokenizer info
|
| 121 |
+
print(f" Tokenizer vocab size: {tokenizer.vocab_size}")
|
| 122 |
+
print(f" BOS token: {tokenizer.bos_token} (id={tokenizer.bos_token_id})")
|
| 123 |
+
print(f" EOS token: {tokenizer.eos_token} (id={tokenizer.eos_token_id})")
|
| 124 |
+
print(f" SNAC token range: {SNAC_BASE} to {SNAC_BASE + 3*4096 - 1}")
|
| 125 |
+
|
| 126 |
print("Models loaded!")
|
| 127 |
return whisper_model, feature_extractor, llm, adapter, snac_model, tokenizer
|
| 128 |
|
|
|
|
| 148 |
|
| 149 |
|
| 150 |
def decode_snac_tokens(tokens: list, snac_model, device: str):
|
| 151 |
+
"""Decode SNAC tokens to audio waveform.
|
| 152 |
+
|
| 153 |
+
SNAC uses 3 hierarchical layers with 1:2:4 ratio.
|
| 154 |
+
Each "frame" has 7 tokens in order:
|
| 155 |
+
- 1 token from layer 0 (position 0)
|
| 156 |
+
- 2 tokens from layer 1 (positions 1, 2)
|
| 157 |
+
- 4 tokens from layer 2 (positions 3, 4, 5, 6)
|
| 158 |
+
|
| 159 |
+
Tokens are offset by: SNAC_BASE + (position % 7) * 4096
|
| 160 |
+
"""
|
| 161 |
+
if len(tokens) == 0:
|
| 162 |
+
print(" Warning: No SNAC tokens to decode")
|
| 163 |
+
return np.zeros(24000, dtype=np.float32) # 1 second of silence
|
| 164 |
+
|
| 165 |
layer0_tokens = []
|
| 166 |
layer1_tokens = []
|
| 167 |
layer2_tokens = []
|
| 168 |
|
| 169 |
+
# Parse tokens by removing position-based offsets
|
| 170 |
+
for i, tok in enumerate(tokens):
|
| 171 |
+
pos = i % 7
|
| 172 |
+
# Remove the offset to get the original code
|
| 173 |
+
original = tok - SNAC_BASE - (pos * 4096)
|
| 174 |
+
|
| 175 |
+
# Ensure codes are valid (0-4095)
|
| 176 |
+
if original < 0 or original >= 4096:
|
| 177 |
+
print(f" Warning: Invalid code {original} at position {i} (token={tok}, pos={pos})")
|
| 178 |
+
original = max(0, min(4095, original))
|
| 179 |
+
|
| 180 |
+
if pos == 0:
|
| 181 |
+
layer0_tokens.append(original)
|
| 182 |
+
elif pos in [1, 2]:
|
| 183 |
+
layer1_tokens.append(original)
|
| 184 |
+
else: # pos in [3, 4, 5, 6]
|
| 185 |
+
layer2_tokens.append(original)
|
| 186 |
+
|
| 187 |
+
# Calculate how many complete frames we have
|
| 188 |
+
n_frames = len(tokens) // 7
|
| 189 |
+
|
| 190 |
+
if n_frames == 0:
|
| 191 |
+
print(f" Warning: Not enough tokens for a complete frame ({len(tokens)} tokens)")
|
| 192 |
+
return np.zeros(24000, dtype=np.float32)
|
| 193 |
+
|
| 194 |
+
print(f" Decoding {n_frames} frames ({len(layer0_tokens)} L0, {len(layer1_tokens)} L1, {len(layer2_tokens)} L2)")
|
| 195 |
+
|
| 196 |
+
# Truncate to complete frames
|
| 197 |
+
layer0_tokens = layer0_tokens[:n_frames]
|
| 198 |
+
layer1_tokens = layer1_tokens[:n_frames * 2]
|
| 199 |
+
layer2_tokens = layer2_tokens[:n_frames * 4]
|
| 200 |
|
| 201 |
codes = [
|
| 202 |
+
torch.tensor([layer0_tokens], dtype=torch.long, device=device),
|
| 203 |
+
torch.tensor([layer1_tokens], dtype=torch.long, device=device),
|
| 204 |
+
torch.tensor([layer2_tokens], dtype=torch.long, device=device)
|
| 205 |
]
|
| 206 |
|
| 207 |
# Decode
|
|
|
|
| 216 |
whisper_model, feature_extractor, llm, adapter, snac_model, tokenizer,
|
| 217 |
device: str,
|
| 218 |
max_new_tokens: int = 500,
|
| 219 |
+
temperature: float = 0.7,
|
| 220 |
+
debug: bool = True,
|
| 221 |
+
use_prompt: bool = False
|
| 222 |
):
|
| 223 |
"""Generate speech response from audio input."""
|
| 224 |
|
| 225 |
# Encode input audio
|
| 226 |
print("Encoding input audio...")
|
| 227 |
audio_embeddings = encode_audio(audio_input, whisper_model, feature_extractor, adapter, device)
|
| 228 |
+
print(f" Audio embeddings shape: {audio_embeddings.shape}")
|
| 229 |
|
| 230 |
# Generate with LLM
|
| 231 |
print("Generating response...")
|
| 232 |
|
|
|
|
| 233 |
with torch.no_grad():
|
| 234 |
# Get embeddings layer
|
| 235 |
embed_layer = llm.get_input_embeddings()
|
| 236 |
|
| 237 |
+
if use_prompt:
|
| 238 |
+
# Option A: Add BOS token after audio
|
| 239 |
+
bos_id = tokenizer.bos_token_id or 128000
|
| 240 |
+
prompt_tokens = torch.tensor([[bos_id]], dtype=torch.long, device=device)
|
| 241 |
+
prompt_embeds = embed_layer(prompt_tokens)
|
| 242 |
+
input_embeds = torch.cat([audio_embeddings.to(torch.bfloat16), prompt_embeds.to(torch.bfloat16)], dim=1)
|
| 243 |
+
else:
|
| 244 |
+
# Option B: Just audio embeddings (as trained)
|
| 245 |
+
# During training, target directly follows audio embeddings
|
| 246 |
+
input_embeds = audio_embeddings.to(torch.bfloat16)
|
| 247 |
+
|
| 248 |
+
print(f" Input embeds shape: {input_embeds.shape}")
|
| 249 |
+
|
| 250 |
+
# Create attention mask
|
| 251 |
+
attention_mask = torch.ones(input_embeds.shape[:2], dtype=torch.long, device=device)
|
| 252 |
+
|
| 253 |
+
# Different generation strategies
|
| 254 |
+
# Strategy 1: Pure greedy (most stable)
|
| 255 |
+
if temperature <= 0.01:
|
| 256 |
+
outputs = llm.generate(
|
| 257 |
+
inputs_embeds=input_embeds,
|
| 258 |
+
attention_mask=attention_mask,
|
| 259 |
+
max_new_tokens=max_new_tokens,
|
| 260 |
+
do_sample=False,
|
| 261 |
+
pad_token_id=tokenizer.eos_token_id,
|
| 262 |
+
eos_token_id=EOS_TOKEN,
|
| 263 |
+
use_cache=True
|
| 264 |
+
)
|
| 265 |
+
# Strategy 2: Sampling with nucleus
|
| 266 |
+
else:
|
| 267 |
+
outputs = llm.generate(
|
| 268 |
+
inputs_embeds=input_embeds,
|
| 269 |
+
attention_mask=attention_mask,
|
| 270 |
+
max_new_tokens=max_new_tokens,
|
| 271 |
+
temperature=temperature,
|
| 272 |
+
do_sample=True,
|
| 273 |
+
top_p=0.9,
|
| 274 |
+
top_k=50,
|
| 275 |
+
repetition_penalty=1.1,
|
| 276 |
+
pad_token_id=tokenizer.eos_token_id,
|
| 277 |
+
eos_token_id=EOS_TOKEN,
|
| 278 |
+
use_cache=True
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
# Extract generated tokens
|
| 282 |
generated_tokens = outputs[0].tolist()
|
|
|
|
| 283 |
|
| 284 |
+
if debug:
|
| 285 |
+
print(f"\n=== DEBUG: Generated {len(generated_tokens)} total tokens ===")
|
| 286 |
+
# Show first 50 tokens
|
| 287 |
+
print(f"First 50 tokens: {generated_tokens[:50]}")
|
| 288 |
+
# Show last 20 tokens
|
| 289 |
+
print(f"Last 20 tokens: {generated_tokens[-20:]}")
|
| 290 |
+
|
| 291 |
+
# Show token ranges with correct SNAC range
|
| 292 |
+
text_tokens = [t for t in generated_tokens if t < SNAC_BASE]
|
| 293 |
+
snac_range_tokens = [t for t in generated_tokens if SNAC_BASE <= t < SNAC_MAX]
|
| 294 |
+
other_high = [t for t in generated_tokens if t >= SNAC_MAX]
|
| 295 |
+
|
| 296 |
+
print(f"\nToken distribution:")
|
| 297 |
+
print(f" Text tokens (<{SNAC_BASE}): {len(text_tokens)}")
|
| 298 |
+
print(f" SNAC tokens ({SNAC_BASE}-{SNAC_MAX}): {len(snac_range_tokens)}")
|
| 299 |
+
print(f" Other high tokens (>={SNAC_MAX}): {len(other_high)}")
|
| 300 |
+
|
| 301 |
+
# Try to decode text tokens
|
| 302 |
+
if text_tokens:
|
| 303 |
+
try:
|
| 304 |
+
decoded_text = tokenizer.decode(text_tokens, skip_special_tokens=False)
|
| 305 |
+
print(f"\nDecoded text: {decoded_text[:500]}")
|
| 306 |
+
except Exception as e:
|
| 307 |
+
print(f"Could not decode text: {e}")
|
| 308 |
+
|
| 309 |
+
# Show some SNAC tokens if any
|
| 310 |
+
if snac_range_tokens:
|
| 311 |
+
print(f"\nFirst 20 SNAC tokens: {snac_range_tokens[:20]}")
|
| 312 |
+
|
| 313 |
+
# Extract SNAC tokens from output (correct range)
|
| 314 |
+
snac_tokens = [t for t in generated_tokens if SNAC_BASE <= t < SNAC_MAX]
|
| 315 |
+
|
| 316 |
+
print(f"\nGenerated {len(snac_tokens)} SNAC tokens")
|
| 317 |
|
| 318 |
# Decode to audio
|
| 319 |
print("Decoding to audio...")
|
|
|
|
| 327 |
parser.add_argument("--checkpoint", type=str, required=True, help="Path to model checkpoint")
|
| 328 |
parser.add_argument("--input", type=str, required=True, help="Input audio file")
|
| 329 |
parser.add_argument("--output", type=str, default="output.wav", help="Output audio file")
|
| 330 |
+
parser.add_argument("--max_tokens", type=int, default=1000, help="Max tokens to generate")
|
| 331 |
+
parser.add_argument("--temperature", type=float, default=0.7, help="Sampling temperature (0 for greedy)")
|
| 332 |
+
parser.add_argument("--use_prompt", action="store_true", help="Add BOS token after audio embeddings")
|
| 333 |
+
parser.add_argument("--no_debug", action="store_true", help="Disable debug output")
|
| 334 |
args = parser.parse_args()
|
| 335 |
|
| 336 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
|
|
| 347 |
whisper_model, feature_extractor, llm, adapter, snac_model, tokenizer,
|
| 348 |
device,
|
| 349 |
max_new_tokens=args.max_tokens,
|
| 350 |
+
temperature=args.temperature,
|
| 351 |
+
debug=not args.no_debug,
|
| 352 |
+
use_prompt=args.use_prompt
|
| 353 |
)
|
| 354 |
|
| 355 |
# Save output
|
training/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Training module for Speech-to-Speech finetuning.
|
| 3 |
+
|
| 4 |
+
Follows SOLID principles:
|
| 5 |
+
- Single Responsibility: Each module handles one concern
|
| 6 |
+
- Open/Closed: Base classes allow extension without modification
|
| 7 |
+
- Dependency Inversion: Components depend on abstractions
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from .config import TrainingConfig, GPUConfig
|
| 11 |
+
from .data import load_sharded_dataset, InterleavedDataset, create_dataloader
|
| 12 |
+
from .models import SpeechAdapter
|
| 13 |
+
from .interleaving import create_interleaved_sequence, get_text_ratio
|
| 14 |
+
from .checkpoint import CheckpointManager
|
| 15 |
+
from .utils import setup_logging, log, get_device_info
|
| 16 |
+
|
| 17 |
+
__all__ = [
|
| 18 |
+
'TrainingConfig',
|
| 19 |
+
'GPUConfig',
|
| 20 |
+
'load_sharded_dataset',
|
| 21 |
+
'InterleavedDataset',
|
| 22 |
+
'create_dataloader',
|
| 23 |
+
'SpeechAdapter',
|
| 24 |
+
'create_interleaved_sequence',
|
| 25 |
+
'get_text_ratio',
|
| 26 |
+
'CheckpointManager',
|
| 27 |
+
'setup_logging',
|
| 28 |
+
'log',
|
| 29 |
+
'get_device_info',
|
| 30 |
+
]
|
training/checkpoint.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Checkpoint management for training.
|
| 3 |
+
|
| 4 |
+
Single Responsibility: Only handles saving and loading checkpoints.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import torch
|
| 9 |
+
import threading
|
| 10 |
+
from typing import Dict, Any, Optional, List
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
from .utils import log
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass
|
| 17 |
+
class TrainingState:
|
| 18 |
+
"""Immutable training state for checkpointing."""
|
| 19 |
+
step: int
|
| 20 |
+
epoch: int
|
| 21 |
+
loss: float
|
| 22 |
+
text_ratio: float
|
| 23 |
+
best_loss: float = float("inf")
|
| 24 |
+
|
| 25 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 26 |
+
return {
|
| 27 |
+
"step": self.step,
|
| 28 |
+
"epoch": self.epoch,
|
| 29 |
+
"loss": self.loss,
|
| 30 |
+
"text_ratio": self.text_ratio,
|
| 31 |
+
"best_loss": self.best_loss,
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
@classmethod
|
| 35 |
+
def from_dict(cls, d: Dict[str, Any]) -> 'TrainingState':
|
| 36 |
+
return cls(
|
| 37 |
+
step=d.get("step", 0),
|
| 38 |
+
epoch=d.get("epoch", 0),
|
| 39 |
+
loss=d.get("loss", 0.0),
|
| 40 |
+
text_ratio=d.get("text_ratio", 0.9),
|
| 41 |
+
best_loss=d.get("best_loss", float("inf")),
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class CheckpointManager:
|
| 46 |
+
"""
|
| 47 |
+
Manages checkpoint saving and loading with async support.
|
| 48 |
+
|
| 49 |
+
Single Responsibility: Only handles checkpoint I/O.
|
| 50 |
+
Open/Closed: Can extend with new checkpoint formats without modification.
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
def __init__(
|
| 54 |
+
self,
|
| 55 |
+
output_dir: str,
|
| 56 |
+
prefix: str = "checkpoint",
|
| 57 |
+
verbose: bool = True,
|
| 58 |
+
max_checkpoints: Optional[int] = None
|
| 59 |
+
):
|
| 60 |
+
"""
|
| 61 |
+
Initialize checkpoint manager.
|
| 62 |
+
|
| 63 |
+
Args:
|
| 64 |
+
output_dir: Directory for saving checkpoints
|
| 65 |
+
prefix: Prefix for checkpoint filenames
|
| 66 |
+
verbose: Whether to log operations
|
| 67 |
+
max_checkpoints: Maximum checkpoints to keep (None = keep all)
|
| 68 |
+
"""
|
| 69 |
+
self.output_dir = Path(output_dir)
|
| 70 |
+
self.prefix = prefix
|
| 71 |
+
self.verbose = verbose
|
| 72 |
+
self.max_checkpoints = max_checkpoints
|
| 73 |
+
self._save_threads: List[threading.Thread] = []
|
| 74 |
+
|
| 75 |
+
# Create output directory
|
| 76 |
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
| 77 |
+
|
| 78 |
+
def save(
|
| 79 |
+
self,
|
| 80 |
+
state_dict: Dict[str, Any],
|
| 81 |
+
filename: str,
|
| 82 |
+
async_save: bool = True
|
| 83 |
+
) -> str:
|
| 84 |
+
"""
|
| 85 |
+
Save checkpoint.
|
| 86 |
+
|
| 87 |
+
Args:
|
| 88 |
+
state_dict: State dictionary to save
|
| 89 |
+
filename: Checkpoint filename
|
| 90 |
+
async_save: Whether to save asynchronously
|
| 91 |
+
|
| 92 |
+
Returns:
|
| 93 |
+
Path to saved checkpoint
|
| 94 |
+
"""
|
| 95 |
+
path = self.output_dir / filename
|
| 96 |
+
|
| 97 |
+
if async_save:
|
| 98 |
+
self._save_async(state_dict, path)
|
| 99 |
+
else:
|
| 100 |
+
self._save_sync(state_dict, path)
|
| 101 |
+
|
| 102 |
+
return str(path)
|
| 103 |
+
|
| 104 |
+
def save_step(
|
| 105 |
+
self,
|
| 106 |
+
adapter_state: Dict[str, Any],
|
| 107 |
+
optimizer_state: Dict[str, Any],
|
| 108 |
+
training_state: TrainingState,
|
| 109 |
+
async_save: bool = True
|
| 110 |
+
) -> str:
|
| 111 |
+
"""Save step checkpoint."""
|
| 112 |
+
state_dict = {
|
| 113 |
+
"adapter": adapter_state,
|
| 114 |
+
"optimizer": optimizer_state,
|
| 115 |
+
**training_state.to_dict()
|
| 116 |
+
}
|
| 117 |
+
filename = f"{self.prefix}_step{training_state.step}.pt"
|
| 118 |
+
return self.save(state_dict, filename, async_save)
|
| 119 |
+
|
| 120 |
+
def save_epoch(
|
| 121 |
+
self,
|
| 122 |
+
adapter_state: Dict[str, Any],
|
| 123 |
+
optimizer_state: Dict[str, Any],
|
| 124 |
+
training_state: TrainingState,
|
| 125 |
+
async_save: bool = True
|
| 126 |
+
) -> str:
|
| 127 |
+
"""Save epoch checkpoint."""
|
| 128 |
+
state_dict = {
|
| 129 |
+
"adapter": adapter_state,
|
| 130 |
+
"optimizer": optimizer_state,
|
| 131 |
+
**training_state.to_dict()
|
| 132 |
+
}
|
| 133 |
+
filename = f"{self.prefix}_epoch{training_state.epoch}.pt"
|
| 134 |
+
return self.save(state_dict, filename, async_save)
|
| 135 |
+
|
| 136 |
+
def save_best(
|
| 137 |
+
self,
|
| 138 |
+
adapter_state: Dict[str, Any],
|
| 139 |
+
training_state: TrainingState,
|
| 140 |
+
lora_state: Optional[Dict[str, Any]] = None,
|
| 141 |
+
async_save: bool = True
|
| 142 |
+
) -> str:
|
| 143 |
+
"""Save best model checkpoint."""
|
| 144 |
+
state_dict = {
|
| 145 |
+
"adapter": adapter_state,
|
| 146 |
+
**training_state.to_dict()
|
| 147 |
+
}
|
| 148 |
+
if lora_state is not None:
|
| 149 |
+
state_dict["lora"] = lora_state
|
| 150 |
+
|
| 151 |
+
filename = f"{self.prefix}_best.pt"
|
| 152 |
+
return self.save(state_dict, filename, async_save)
|
| 153 |
+
|
| 154 |
+
def load(self, path: str) -> Dict[str, Any]:
|
| 155 |
+
"""
|
| 156 |
+
Load checkpoint.
|
| 157 |
+
|
| 158 |
+
Args:
|
| 159 |
+
path: Path to checkpoint
|
| 160 |
+
|
| 161 |
+
Returns:
|
| 162 |
+
Loaded state dictionary
|
| 163 |
+
"""
|
| 164 |
+
if self.verbose:
|
| 165 |
+
log(f"Loading checkpoint: {path}")
|
| 166 |
+
return torch.load(path, map_location="cpu", weights_only=False)
|
| 167 |
+
|
| 168 |
+
def load_latest(self) -> Optional[Dict[str, Any]]:
|
| 169 |
+
"""Load the most recent checkpoint."""
|
| 170 |
+
checkpoints = self._get_checkpoints()
|
| 171 |
+
if not checkpoints:
|
| 172 |
+
return None
|
| 173 |
+
return self.load(str(checkpoints[-1]))
|
| 174 |
+
|
| 175 |
+
def wait_for_saves(self):
|
| 176 |
+
"""Wait for all async saves to complete."""
|
| 177 |
+
for t in self._save_threads:
|
| 178 |
+
t.join()
|
| 179 |
+
self._save_threads = []
|
| 180 |
+
|
| 181 |
+
def _save_sync(self, state_dict: Dict[str, Any], path: Path):
|
| 182 |
+
"""Synchronous save."""
|
| 183 |
+
# Copy tensors to CPU
|
| 184 |
+
state_copy = self._copy_to_cpu(state_dict)
|
| 185 |
+
torch.save(state_copy, path)
|
| 186 |
+
if self.verbose:
|
| 187 |
+
log(f"[Checkpoint] Saved: {path.name}")
|
| 188 |
+
|
| 189 |
+
def _save_async(self, state_dict: Dict[str, Any], path: Path):
|
| 190 |
+
"""Asynchronous save."""
|
| 191 |
+
# Clean up completed threads
|
| 192 |
+
self._save_threads = [t for t in self._save_threads if t.is_alive()]
|
| 193 |
+
|
| 194 |
+
# Copy tensors to CPU
|
| 195 |
+
state_copy = self._copy_to_cpu(state_dict)
|
| 196 |
+
|
| 197 |
+
def _save():
|
| 198 |
+
try:
|
| 199 |
+
torch.save(state_copy, path)
|
| 200 |
+
if self.verbose:
|
| 201 |
+
log(f"[Checkpoint] Saved: {path.name}")
|
| 202 |
+
except Exception as e:
|
| 203 |
+
if self.verbose:
|
| 204 |
+
log(f"[Checkpoint] Error saving {path.name}: {e}")
|
| 205 |
+
|
| 206 |
+
thread = threading.Thread(target=_save, daemon=True)
|
| 207 |
+
thread.start()
|
| 208 |
+
self._save_threads.append(thread)
|
| 209 |
+
|
| 210 |
+
# Cleanup old checkpoints if needed
|
| 211 |
+
if self.max_checkpoints:
|
| 212 |
+
self._cleanup_old_checkpoints()
|
| 213 |
+
|
| 214 |
+
def _copy_to_cpu(self, obj: Any) -> Any:
|
| 215 |
+
"""Recursively copy tensors to CPU."""
|
| 216 |
+
if isinstance(obj, torch.Tensor):
|
| 217 |
+
return obj.detach().cpu().clone()
|
| 218 |
+
elif isinstance(obj, dict):
|
| 219 |
+
return {k: self._copy_to_cpu(v) for k, v in obj.items()}
|
| 220 |
+
elif isinstance(obj, list):
|
| 221 |
+
return [self._copy_to_cpu(v) for v in obj]
|
| 222 |
+
return obj
|
| 223 |
+
|
| 224 |
+
def _get_checkpoints(self) -> List[Path]:
|
| 225 |
+
"""Get sorted list of checkpoint files."""
|
| 226 |
+
pattern = f"{self.prefix}_step*.pt"
|
| 227 |
+
checkpoints = sorted(
|
| 228 |
+
self.output_dir.glob(pattern),
|
| 229 |
+
key=lambda p: int(p.stem.split("step")[-1])
|
| 230 |
+
)
|
| 231 |
+
return checkpoints
|
| 232 |
+
|
| 233 |
+
def _cleanup_old_checkpoints(self):
|
| 234 |
+
"""Remove old checkpoints beyond max_checkpoints."""
|
| 235 |
+
if not self.max_checkpoints:
|
| 236 |
+
return
|
| 237 |
+
|
| 238 |
+
checkpoints = self._get_checkpoints()
|
| 239 |
+
while len(checkpoints) > self.max_checkpoints:
|
| 240 |
+
oldest = checkpoints.pop(0)
|
| 241 |
+
try:
|
| 242 |
+
oldest.unlink()
|
| 243 |
+
if self.verbose:
|
| 244 |
+
log(f"[Checkpoint] Removed old: {oldest.name}")
|
| 245 |
+
except Exception:
|
| 246 |
+
pass
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
class Stage1CheckpointManager(CheckpointManager):
|
| 250 |
+
"""Checkpoint manager for Stage 1 training."""
|
| 251 |
+
|
| 252 |
+
def __init__(self, output_dir: str, **kwargs):
|
| 253 |
+
super().__init__(output_dir, prefix="stage1", **kwargs)
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
class Stage2CheckpointManager(CheckpointManager):
|
| 257 |
+
"""Checkpoint manager for Stage 2 training."""
|
| 258 |
+
|
| 259 |
+
def __init__(self, output_dir: str, **kwargs):
|
| 260 |
+
super().__init__(output_dir, prefix="stage2", **kwargs)
|
| 261 |
+
|
| 262 |
+
def save_best(
|
| 263 |
+
self,
|
| 264 |
+
adapter_state: Dict[str, Any],
|
| 265 |
+
training_state: TrainingState,
|
| 266 |
+
lora_state: Dict[str, Any],
|
| 267 |
+
async_save: bool = True
|
| 268 |
+
) -> str:
|
| 269 |
+
"""Save best model with LoRA weights."""
|
| 270 |
+
return super().save_best(
|
| 271 |
+
adapter_state, training_state,
|
| 272 |
+
lora_state=lora_state, async_save=async_save
|
| 273 |
+
)
|
training/config.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Configuration management using dataclasses.
|
| 3 |
+
|
| 4 |
+
Single Responsibility: Only handles configuration.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from dataclasses import dataclass, field
|
| 8 |
+
from typing import Optional, List
|
| 9 |
+
import os
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
# ============================================================
|
| 14 |
+
# Constants (Single source of truth)
|
| 15 |
+
# ============================================================
|
| 16 |
+
SNAC_BASE_OFFSET = 128266
|
| 17 |
+
SNAC_LAYERS_PER_FRAME = 7
|
| 18 |
+
SNAC_LAYER_OFFSET = 4096
|
| 19 |
+
EOS_TOKEN = 128009
|
| 20 |
+
|
| 21 |
+
# Model defaults
|
| 22 |
+
DEFAULT_WHISPER_DIM = 1280
|
| 23 |
+
DEFAULT_LLM_DIM = 3072
|
| 24 |
+
DEFAULT_DOWNSAMPLE = 5
|
| 25 |
+
DEFAULT_INTERMEDIATE_DIM = 2048
|
| 26 |
+
DEFAULT_MODEL_PATH = "canopylabs/3b-es_it-ft-research_release"
|
| 27 |
+
|
| 28 |
+
# LoRA defaults
|
| 29 |
+
DEFAULT_LORA_R = 16
|
| 30 |
+
DEFAULT_LORA_ALPHA = 32
|
| 31 |
+
DEFAULT_LORA_DROPOUT = 0.05
|
| 32 |
+
DEFAULT_LORA_MODULES = [
|
| 33 |
+
"q_proj", "k_proj", "v_proj", "o_proj",
|
| 34 |
+
"gate_proj", "up_proj", "down_proj"
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@dataclass
|
| 39 |
+
class GPUConfig:
|
| 40 |
+
"""GPU configuration detected at runtime."""
|
| 41 |
+
name: str = "Unknown"
|
| 42 |
+
vram_gb: int = 0
|
| 43 |
+
batch_size: int = 2
|
| 44 |
+
grad_accum: int = 16
|
| 45 |
+
dtype: torch.dtype = torch.float32
|
| 46 |
+
device_type: str = "cpu"
|
| 47 |
+
|
| 48 |
+
@classmethod
|
| 49 |
+
def auto_detect(cls) -> 'GPUConfig':
|
| 50 |
+
"""Detect GPU and return optimal configuration."""
|
| 51 |
+
config = cls()
|
| 52 |
+
|
| 53 |
+
# Try CUDA (NVIDIA)
|
| 54 |
+
if torch.cuda.is_available():
|
| 55 |
+
try:
|
| 56 |
+
props = torch.cuda.get_device_properties(0)
|
| 57 |
+
config.vram_gb = props.total_memory // (1024**3)
|
| 58 |
+
config.name = props.name
|
| 59 |
+
config.device_type = "cuda"
|
| 60 |
+
config.dtype = torch.bfloat16
|
| 61 |
+
except Exception:
|
| 62 |
+
pass
|
| 63 |
+
|
| 64 |
+
# Try MPS (Apple Silicon)
|
| 65 |
+
elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
|
| 66 |
+
config.name = "Apple Silicon (MPS)"
|
| 67 |
+
config.device_type = "mps"
|
| 68 |
+
config.dtype = torch.float32
|
| 69 |
+
try:
|
| 70 |
+
import subprocess
|
| 71 |
+
result = subprocess.run(
|
| 72 |
+
['sysctl', '-n', 'hw.memsize'],
|
| 73 |
+
capture_output=True, text=True
|
| 74 |
+
)
|
| 75 |
+
total_mem = int(result.stdout.strip()) // (1024**3)
|
| 76 |
+
config.vram_gb = total_mem // 2
|
| 77 |
+
except Exception:
|
| 78 |
+
config.vram_gb = 8
|
| 79 |
+
|
| 80 |
+
# Try ROCm (AMD)
|
| 81 |
+
elif hasattr(torch, 'hip') or os.environ.get('ROCM_HOME'):
|
| 82 |
+
try:
|
| 83 |
+
if torch.cuda.is_available():
|
| 84 |
+
props = torch.cuda.get_device_properties(0)
|
| 85 |
+
config.vram_gb = props.total_memory // (1024**3)
|
| 86 |
+
config.name = f"AMD {props.name}"
|
| 87 |
+
config.device_type = "cuda"
|
| 88 |
+
config.dtype = torch.bfloat16
|
| 89 |
+
except Exception:
|
| 90 |
+
config.name = "AMD ROCm"
|
| 91 |
+
config.vram_gb = 16
|
| 92 |
+
|
| 93 |
+
# Fallback: nvidia-smi
|
| 94 |
+
if config.vram_gb == 0:
|
| 95 |
+
try:
|
| 96 |
+
import subprocess
|
| 97 |
+
result = subprocess.run(
|
| 98 |
+
['nvidia-smi', '--query-gpu=name,memory.total',
|
| 99 |
+
'--format=csv,noheader,nounits'],
|
| 100 |
+
capture_output=True, text=True
|
| 101 |
+
)
|
| 102 |
+
lines = result.stdout.strip().split('\n')
|
| 103 |
+
config.name, vram_mb = lines[0].split(', ')
|
| 104 |
+
config.vram_gb = int(vram_mb) // 1024
|
| 105 |
+
config.device_type = "cuda"
|
| 106 |
+
config.dtype = torch.bfloat16
|
| 107 |
+
except Exception:
|
| 108 |
+
pass
|
| 109 |
+
|
| 110 |
+
# Set batch size based on VRAM
|
| 111 |
+
config.batch_size, config.grad_accum = cls._get_batch_config(config.vram_gb)
|
| 112 |
+
return config
|
| 113 |
+
|
| 114 |
+
@staticmethod
|
| 115 |
+
def _get_batch_config(vram_gb: int) -> tuple:
|
| 116 |
+
"""Get optimal batch size and gradient accumulation based on VRAM."""
|
| 117 |
+
if vram_gb >= 140: # H200 (141GB)
|
| 118 |
+
return 12, 3 # H200: batch=12, effective=36
|
| 119 |
+
elif vram_gb >= 80:
|
| 120 |
+
return 6, 5
|
| 121 |
+
elif vram_gb >= 35:
|
| 122 |
+
return 4, 8
|
| 123 |
+
elif vram_gb >= 16:
|
| 124 |
+
return 2, 16
|
| 125 |
+
else:
|
| 126 |
+
return 1, 32
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
@dataclass
|
| 130 |
+
class TrainingConfig:
|
| 131 |
+
"""Training configuration with sensible defaults."""
|
| 132 |
+
# Data
|
| 133 |
+
data_paths: List[str] = field(default_factory=list)
|
| 134 |
+
output_dir: str = "./checkpoints"
|
| 135 |
+
|
| 136 |
+
# Training hyperparameters
|
| 137 |
+
learning_rate: float = 5e-5
|
| 138 |
+
epochs: int = 2
|
| 139 |
+
batch_size: Optional[int] = None
|
| 140 |
+
grad_accum: Optional[int] = None
|
| 141 |
+
warmup_ratio: float = 0.03
|
| 142 |
+
max_grad_norm: float = 1.0
|
| 143 |
+
label_smoothing: float = 0.1
|
| 144 |
+
weight_decay: float = 0.01
|
| 145 |
+
|
| 146 |
+
# Sequence limits
|
| 147 |
+
max_audio_len: int = 500
|
| 148 |
+
max_seq_len: int = 2048
|
| 149 |
+
|
| 150 |
+
# Scheduled interleaving (IST-LM)
|
| 151 |
+
initial_text_ratio: float = 0.9
|
| 152 |
+
decay_steps: int = 300
|
| 153 |
+
dynamic_decay: bool = False
|
| 154 |
+
no_decay: bool = False # Stage 1: keep text_ratio fixed
|
| 155 |
+
final_audio_portion: float = 0.2
|
| 156 |
+
|
| 157 |
+
# Model
|
| 158 |
+
model_path: str = DEFAULT_MODEL_PATH
|
| 159 |
+
|
| 160 |
+
# Checkpointing
|
| 161 |
+
save_steps: int = 200
|
| 162 |
+
resume_from: Optional[str] = None
|
| 163 |
+
|
| 164 |
+
# Memory
|
| 165 |
+
vram_fraction: float = 0.80
|
| 166 |
+
ram_limit_gb: Optional[float] = None
|
| 167 |
+
gradient_checkpointing: bool = False
|
| 168 |
+
|
| 169 |
+
# Mode flags
|
| 170 |
+
demo_mode: bool = False
|
| 171 |
+
test_mode: bool = False
|
| 172 |
+
|
| 173 |
+
def __post_init__(self):
|
| 174 |
+
"""Apply GPU auto-detection if batch_size not set."""
|
| 175 |
+
if self.batch_size is None or self.grad_accum is None:
|
| 176 |
+
gpu_config = GPUConfig.auto_detect()
|
| 177 |
+
if self.batch_size is None:
|
| 178 |
+
self.batch_size = gpu_config.batch_size
|
| 179 |
+
if self.grad_accum is None:
|
| 180 |
+
self.grad_accum = gpu_config.grad_accum
|
| 181 |
+
|
| 182 |
+
@classmethod
|
| 183 |
+
def from_args(cls, args) -> 'TrainingConfig':
|
| 184 |
+
"""Create config from argparse namespace."""
|
| 185 |
+
return cls(
|
| 186 |
+
data_paths=[p.strip() for p in args.data.split(",")],
|
| 187 |
+
output_dir=args.output_dir,
|
| 188 |
+
learning_rate=args.lr,
|
| 189 |
+
epochs=args.epochs,
|
| 190 |
+
batch_size=args.batch_size,
|
| 191 |
+
grad_accum=args.grad_accum,
|
| 192 |
+
warmup_ratio=args.warmup_ratio,
|
| 193 |
+
max_grad_norm=args.max_grad_norm,
|
| 194 |
+
label_smoothing=args.label_smoothing,
|
| 195 |
+
max_seq_len=args.max_seq_len,
|
| 196 |
+
initial_text_ratio=args.initial_text_ratio,
|
| 197 |
+
decay_steps=args.decay_steps,
|
| 198 |
+
dynamic_decay=getattr(args, 'dynamic_decay', False),
|
| 199 |
+
no_decay=getattr(args, 'no_decay', False),
|
| 200 |
+
final_audio_portion=getattr(args, 'final_audio_portion', 0.2),
|
| 201 |
+
model_path=args.model_path,
|
| 202 |
+
save_steps=args.save_steps,
|
| 203 |
+
resume_from=args.resume,
|
| 204 |
+
vram_fraction=args.vram_fraction,
|
| 205 |
+
ram_limit_gb=args.ram_limit_gb,
|
| 206 |
+
gradient_checkpointing=args.gradient_checkpointing,
|
| 207 |
+
demo_mode=args.demo,
|
| 208 |
+
test_mode=args.test,
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
@dataclass
|
| 213 |
+
class LoRAConfig:
|
| 214 |
+
"""LoRA configuration for Stage 2."""
|
| 215 |
+
r: int = DEFAULT_LORA_R
|
| 216 |
+
alpha: int = DEFAULT_LORA_ALPHA
|
| 217 |
+
dropout: float = DEFAULT_LORA_DROPOUT
|
| 218 |
+
target_modules: List[str] = field(default_factory=lambda: DEFAULT_LORA_MODULES.copy())
|
| 219 |
+
bias: str = "none"
|
| 220 |
+
|
| 221 |
+
def to_peft_config(self):
|
| 222 |
+
"""Convert to PEFT LoraConfig."""
|
| 223 |
+
from peft import LoraConfig as PeftLoraConfig, TaskType
|
| 224 |
+
return PeftLoraConfig(
|
| 225 |
+
r=self.r,
|
| 226 |
+
lora_alpha=self.alpha,
|
| 227 |
+
lora_dropout=self.dropout,
|
| 228 |
+
target_modules=self.target_modules,
|
| 229 |
+
bias=self.bias,
|
| 230 |
+
task_type=TaskType.CAUSAL_LM,
|
| 231 |
+
)
|
training/data.py
ADDED
|
@@ -0,0 +1,734 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Data loading and dataset management.
|
| 3 |
+
|
| 4 |
+
Single Responsibility: Only handles data loading and dataset creation.
|
| 5 |
+
Open/Closed: Can extend with new dataset formats without modifying existing code.
|
| 6 |
+
|
| 7 |
+
Optimizations:
|
| 8 |
+
- Lazy loading for memory efficiency
|
| 9 |
+
- Sequence length bucketing for reduced padding overhead
|
| 10 |
+
- LRU cache for batch files
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import gc
|
| 14 |
+
import random
|
| 15 |
+
import torch
|
| 16 |
+
import torch.nn.functional as F
|
| 17 |
+
from torch.utils.data import Dataset, DataLoader, ConcatDataset, Sampler
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
from typing import List, Dict, Any, Optional, Callable, Tuple, Iterator
|
| 20 |
+
from .utils import log
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# ============================================================
|
| 24 |
+
# Sequence Length Bucketing (Reduces padding overhead)
|
| 25 |
+
# ============================================================
|
| 26 |
+
class BucketBatchSampler(Sampler[List[int]]):
|
| 27 |
+
"""
|
| 28 |
+
Batch sampler that groups samples by sequence length into buckets.
|
| 29 |
+
|
| 30 |
+
This reduces padding overhead by batching similar-length sequences together.
|
| 31 |
+
Based on TensorFlow's bucket_by_sequence_length concept.
|
| 32 |
+
|
| 33 |
+
Benefits:
|
| 34 |
+
- Reduces wasted computation on padding tokens
|
| 35 |
+
- More consistent memory usage per batch
|
| 36 |
+
- Can improve training speed by 10-30%
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
lengths: List of sequence lengths for each sample
|
| 40 |
+
batch_size: Number of samples per batch
|
| 41 |
+
bucket_boundaries: Length boundaries for buckets (auto-computed if None)
|
| 42 |
+
shuffle: Whether to shuffle within buckets
|
| 43 |
+
drop_last: Whether to drop incomplete batches
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
def __init__(
|
| 47 |
+
self,
|
| 48 |
+
lengths: List[int],
|
| 49 |
+
batch_size: int,
|
| 50 |
+
bucket_boundaries: Optional[List[int]] = None,
|
| 51 |
+
shuffle: bool = True,
|
| 52 |
+
drop_last: bool = False,
|
| 53 |
+
):
|
| 54 |
+
self.lengths = lengths
|
| 55 |
+
self.batch_size = batch_size
|
| 56 |
+
self.shuffle = shuffle
|
| 57 |
+
self.drop_last = drop_last
|
| 58 |
+
|
| 59 |
+
# Auto-compute bucket boundaries if not provided
|
| 60 |
+
if bucket_boundaries is None:
|
| 61 |
+
# Create ~10 buckets based on length distribution
|
| 62 |
+
sorted_lens = sorted(lengths)
|
| 63 |
+
n = len(sorted_lens)
|
| 64 |
+
bucket_boundaries = [
|
| 65 |
+
sorted_lens[int(n * p)] for p in [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
|
| 66 |
+
]
|
| 67 |
+
# Remove duplicates and sort
|
| 68 |
+
bucket_boundaries = sorted(set(bucket_boundaries))
|
| 69 |
+
|
| 70 |
+
self.bucket_boundaries = bucket_boundaries
|
| 71 |
+
|
| 72 |
+
# Assign samples to buckets
|
| 73 |
+
self.buckets: Dict[int, List[int]] = {i: [] for i in range(len(bucket_boundaries) + 1)}
|
| 74 |
+
for idx, length in enumerate(lengths):
|
| 75 |
+
bucket_id = self._get_bucket_id(length)
|
| 76 |
+
self.buckets[bucket_id].append(idx)
|
| 77 |
+
|
| 78 |
+
def _get_bucket_id(self, length: int) -> int:
|
| 79 |
+
"""Find which bucket a length belongs to."""
|
| 80 |
+
for i, boundary in enumerate(self.bucket_boundaries):
|
| 81 |
+
if length <= boundary:
|
| 82 |
+
return i
|
| 83 |
+
return len(self.bucket_boundaries)
|
| 84 |
+
|
| 85 |
+
def __iter__(self) -> Iterator[List[int]]:
|
| 86 |
+
"""Generate batches from buckets."""
|
| 87 |
+
# Collect all batches from all buckets
|
| 88 |
+
all_batches = []
|
| 89 |
+
|
| 90 |
+
for bucket_id, indices in self.buckets.items():
|
| 91 |
+
if not indices:
|
| 92 |
+
continue
|
| 93 |
+
|
| 94 |
+
# Shuffle within bucket
|
| 95 |
+
bucket_indices = indices.copy()
|
| 96 |
+
if self.shuffle:
|
| 97 |
+
random.shuffle(bucket_indices)
|
| 98 |
+
|
| 99 |
+
# Create batches
|
| 100 |
+
for i in range(0, len(bucket_indices), self.batch_size):
|
| 101 |
+
batch = bucket_indices[i:i + self.batch_size]
|
| 102 |
+
if len(batch) == self.batch_size or not self.drop_last:
|
| 103 |
+
all_batches.append(batch)
|
| 104 |
+
|
| 105 |
+
# Shuffle batches across buckets
|
| 106 |
+
if self.shuffle:
|
| 107 |
+
random.shuffle(all_batches)
|
| 108 |
+
|
| 109 |
+
yield from all_batches
|
| 110 |
+
|
| 111 |
+
def __len__(self) -> int:
|
| 112 |
+
"""Return total number of batches."""
|
| 113 |
+
total = 0
|
| 114 |
+
for indices in self.buckets.values():
|
| 115 |
+
n_batches = len(indices) // self.batch_size
|
| 116 |
+
if not self.drop_last and len(indices) % self.batch_size != 0:
|
| 117 |
+
n_batches += 1
|
| 118 |
+
total += n_batches
|
| 119 |
+
return total
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
# ============================================================
|
| 123 |
+
# Lazy Sharded Dataset (Memory Efficient)
|
| 124 |
+
# ============================================================
|
| 125 |
+
class LazyShardedDataset(Dataset):
|
| 126 |
+
"""
|
| 127 |
+
Memory-efficient dataset that loads batch files on-demand.
|
| 128 |
+
|
| 129 |
+
Instead of loading all data into memory, maintains an index of
|
| 130 |
+
which sample is in which batch file, and loads batches as needed.
|
| 131 |
+
|
| 132 |
+
Also stores approximate sequence lengths for bucketing optimization.
|
| 133 |
+
"""
|
| 134 |
+
|
| 135 |
+
def __init__(
|
| 136 |
+
self,
|
| 137 |
+
batch_files: List[Path],
|
| 138 |
+
tokenizer,
|
| 139 |
+
max_audio_len: int = 500,
|
| 140 |
+
max_seq_len: int = 2048,
|
| 141 |
+
cache_size: int = 3, # Number of batches to keep in memory
|
| 142 |
+
verbose: bool = True
|
| 143 |
+
):
|
| 144 |
+
self.batch_files = batch_files
|
| 145 |
+
self.tokenizer = tokenizer
|
| 146 |
+
self.max_audio = max_audio_len * 5
|
| 147 |
+
self.max_seq_len = max_seq_len
|
| 148 |
+
self.cache_size = cache_size
|
| 149 |
+
self.verbose = verbose
|
| 150 |
+
|
| 151 |
+
# Build index: sample_idx -> (batch_idx, local_idx)
|
| 152 |
+
self.index: List[Tuple[int, int]] = []
|
| 153 |
+
self.batch_sizes: List[int] = []
|
| 154 |
+
# Store sequence lengths for bucketing
|
| 155 |
+
self.sequence_lengths: List[int] = []
|
| 156 |
+
|
| 157 |
+
if verbose:
|
| 158 |
+
log(f" Indexing {len(batch_files)} batch files...")
|
| 159 |
+
|
| 160 |
+
for batch_idx, bf in enumerate(batch_files):
|
| 161 |
+
# Quick load to get size and lengths
|
| 162 |
+
data = torch.load(bf, map_location="cpu", weights_only=False)
|
| 163 |
+
batch_size = len(data)
|
| 164 |
+
self.batch_sizes.append(batch_size)
|
| 165 |
+
|
| 166 |
+
for local_idx in range(batch_size):
|
| 167 |
+
self.index.append((batch_idx, local_idx))
|
| 168 |
+
# Estimate sequence length (SNAC tokens dominate)
|
| 169 |
+
item = data[local_idx]
|
| 170 |
+
snac_len = len(item.get("snac_tokens", [])) // 7 * 7
|
| 171 |
+
self.sequence_lengths.append(snac_len)
|
| 172 |
+
|
| 173 |
+
del data
|
| 174 |
+
if (batch_idx + 1) % 100 == 0:
|
| 175 |
+
gc.collect()
|
| 176 |
+
if verbose:
|
| 177 |
+
log(f" Indexed {batch_idx+1}/{len(batch_files)} batches ({len(self.index):,} samples)")
|
| 178 |
+
|
| 179 |
+
if verbose:
|
| 180 |
+
log(f" Total indexed: {len(self.index):,} samples")
|
| 181 |
+
|
| 182 |
+
# LRU cache for loaded batches
|
| 183 |
+
self._cache: Dict[int, List[Dict]] = {}
|
| 184 |
+
self._cache_order: List[int] = []
|
| 185 |
+
|
| 186 |
+
def get_sequence_lengths(self) -> List[int]:
|
| 187 |
+
"""Return sequence lengths for bucketing."""
|
| 188 |
+
return self.sequence_lengths
|
| 189 |
+
|
| 190 |
+
def __len__(self) -> int:
|
| 191 |
+
return len(self.index)
|
| 192 |
+
|
| 193 |
+
def _load_batch(self, batch_idx: int) -> List[Dict]:
|
| 194 |
+
"""Load a batch file, using cache."""
|
| 195 |
+
if batch_idx in self._cache:
|
| 196 |
+
# Move to end of cache order (most recently used)
|
| 197 |
+
self._cache_order.remove(batch_idx)
|
| 198 |
+
self._cache_order.append(batch_idx)
|
| 199 |
+
return self._cache[batch_idx]
|
| 200 |
+
|
| 201 |
+
# Load from disk
|
| 202 |
+
data = torch.load(self.batch_files[batch_idx], map_location="cpu", weights_only=False)
|
| 203 |
+
|
| 204 |
+
# Add to cache
|
| 205 |
+
self._cache[batch_idx] = data
|
| 206 |
+
self._cache_order.append(batch_idx)
|
| 207 |
+
|
| 208 |
+
# Evict old batches if cache is full
|
| 209 |
+
while len(self._cache_order) > self.cache_size:
|
| 210 |
+
old_idx = self._cache_order.pop(0)
|
| 211 |
+
del self._cache[old_idx]
|
| 212 |
+
gc.collect()
|
| 213 |
+
|
| 214 |
+
return data
|
| 215 |
+
|
| 216 |
+
def __getitem__(self, idx: int) -> Dict[str, Any]:
|
| 217 |
+
batch_idx, local_idx = self.index[idx]
|
| 218 |
+
batch_data = self._load_batch(batch_idx)
|
| 219 |
+
item = batch_data[local_idx]
|
| 220 |
+
|
| 221 |
+
# Process item (same as InterleavedDataset)
|
| 222 |
+
whisper = item["whisper_features"][:self.max_audio]
|
| 223 |
+
text_tokens = self._get_text_tokens(item)
|
| 224 |
+
snac_list = self._get_snac_tokens(item)
|
| 225 |
+
word_alignments = item.get("word_alignments", None)
|
| 226 |
+
answer_text = item.get("answer", "")
|
| 227 |
+
|
| 228 |
+
return {
|
| 229 |
+
"whisper": whisper,
|
| 230 |
+
"text_tokens": text_tokens,
|
| 231 |
+
"snac_tokens": snac_list,
|
| 232 |
+
"word_alignments": word_alignments,
|
| 233 |
+
"answer_text": answer_text
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
def _get_text_tokens(self, item: Dict[str, Any]) -> List[int]:
|
| 237 |
+
if "text_tokens" in item and len(item["text_tokens"]) > 0:
|
| 238 |
+
tt = item["text_tokens"]
|
| 239 |
+
return tt.tolist() if hasattr(tt, 'tolist') else list(tt)
|
| 240 |
+
text = item.get("answer", item.get("text", ""))
|
| 241 |
+
if isinstance(text, str) and len(text) > 0:
|
| 242 |
+
return self.tokenizer.encode(text, add_special_tokens=False)
|
| 243 |
+
return []
|
| 244 |
+
|
| 245 |
+
def _get_snac_tokens(self, item: Dict[str, Any]) -> List[int]:
|
| 246 |
+
snac = item["snac_tokens"]
|
| 247 |
+
snac_len = (len(snac) // 7) * 7
|
| 248 |
+
snac = snac[:snac_len] if snac_len > 0 else snac[:7]
|
| 249 |
+
return snac.tolist() if hasattr(snac, 'tolist') else list(snac)
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
# ============================================================
|
| 253 |
+
# Sharded Dataset Loading (Industry Standard)
|
| 254 |
+
# ============================================================
|
| 255 |
+
class ShardedDatasetLoader:
|
| 256 |
+
"""
|
| 257 |
+
Load datasets from single files or sharded batch directories.
|
| 258 |
+
|
| 259 |
+
Supports:
|
| 260 |
+
- Single .pt file
|
| 261 |
+
- Batch directory with batch_*.pt files
|
| 262 |
+
- Mixed (base file + batch files)
|
| 263 |
+
|
| 264 |
+
Dependency Inversion: Uses abstract path interface, not concrete file operations.
|
| 265 |
+
"""
|
| 266 |
+
|
| 267 |
+
def __init__(self, verbose: bool = True):
|
| 268 |
+
self.verbose = verbose
|
| 269 |
+
|
| 270 |
+
def load(self, path: str) -> List[Dict[str, Any]]:
|
| 271 |
+
"""Load dataset from path (file or directory)."""
|
| 272 |
+
path = Path(path)
|
| 273 |
+
samples = []
|
| 274 |
+
|
| 275 |
+
# Case 1: Explicit batches directory
|
| 276 |
+
if path.name.endswith('.batches') and path.is_dir():
|
| 277 |
+
samples = self._load_batches_dir(path)
|
| 278 |
+
|
| 279 |
+
# Case 2: Single file (possibly with batches)
|
| 280 |
+
elif path.exists() and path.is_file():
|
| 281 |
+
samples = self._load_file_with_batches(path)
|
| 282 |
+
|
| 283 |
+
# Case 3: Only batches directory exists
|
| 284 |
+
else:
|
| 285 |
+
batches_dir = Path(f"{path}.batches")
|
| 286 |
+
if batches_dir.exists() and batches_dir.is_dir():
|
| 287 |
+
samples = self._load_batches_dir(batches_dir)
|
| 288 |
+
else:
|
| 289 |
+
raise FileNotFoundError(f"No dataset found at {path}")
|
| 290 |
+
|
| 291 |
+
return samples
|
| 292 |
+
|
| 293 |
+
def _load_batches_dir(self, batches_dir: Path) -> List[Dict[str, Any]]:
|
| 294 |
+
"""Load all batch files from a directory."""
|
| 295 |
+
batch_files = sorted(batches_dir.glob("batch_*.pt"))
|
| 296 |
+
if not batch_files:
|
| 297 |
+
raise FileNotFoundError(f"No batch files in {batches_dir}")
|
| 298 |
+
|
| 299 |
+
if self.verbose:
|
| 300 |
+
log(f" Loading {len(batch_files)} batch files from {batches_dir.name}/")
|
| 301 |
+
|
| 302 |
+
samples = []
|
| 303 |
+
for i, bf in enumerate(batch_files):
|
| 304 |
+
items = torch.load(bf, map_location="cpu", weights_only=False)
|
| 305 |
+
samples.extend(items)
|
| 306 |
+
del items
|
| 307 |
+
if (i + 1) % 100 == 0:
|
| 308 |
+
gc.collect()
|
| 309 |
+
if self.verbose:
|
| 310 |
+
log(f" Loaded {i+1}/{len(batch_files)} batches ({len(samples):,} samples)")
|
| 311 |
+
|
| 312 |
+
return samples
|
| 313 |
+
|
| 314 |
+
def _load_file_with_batches(self, path: Path) -> List[Dict[str, Any]]:
|
| 315 |
+
"""Load base file and any associated batch files."""
|
| 316 |
+
# Load base file
|
| 317 |
+
base = torch.load(path, map_location="cpu", weights_only=False, mmap=True)
|
| 318 |
+
samples = list(base)
|
| 319 |
+
del base
|
| 320 |
+
gc.collect()
|
| 321 |
+
|
| 322 |
+
if self.verbose:
|
| 323 |
+
log(f" Base file: {len(samples):,} samples")
|
| 324 |
+
|
| 325 |
+
# Check for batch files
|
| 326 |
+
batches_dir = Path(f"{path}.batches")
|
| 327 |
+
if batches_dir.exists() and batches_dir.is_dir():
|
| 328 |
+
batch_files = sorted(batches_dir.glob("batch_*.pt"))
|
| 329 |
+
if batch_files:
|
| 330 |
+
if self.verbose:
|
| 331 |
+
log(f" Found {len(batch_files)} batch files")
|
| 332 |
+
for i, bf in enumerate(batch_files):
|
| 333 |
+
items = torch.load(bf, map_location="cpu", weights_only=False)
|
| 334 |
+
samples.extend(items)
|
| 335 |
+
del items
|
| 336 |
+
if (i + 1) % 100 == 0:
|
| 337 |
+
gc.collect()
|
| 338 |
+
if self.verbose:
|
| 339 |
+
log(f" Loaded {i+1}/{len(batch_files)} batches ({len(samples):,} total)")
|
| 340 |
+
|
| 341 |
+
return samples
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
def load_sharded_dataset(path: str, verbose: bool = True) -> List[Dict[str, Any]]:
|
| 345 |
+
"""Convenience function for loading sharded datasets."""
|
| 346 |
+
loader = ShardedDatasetLoader(verbose=verbose)
|
| 347 |
+
return loader.load(path)
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
# ============================================================
|
| 351 |
+
# Dataset Classes
|
| 352 |
+
# ============================================================
|
| 353 |
+
class InterleavedDataset(Dataset):
|
| 354 |
+
"""
|
| 355 |
+
Dataset that prepares samples for interleaved training.
|
| 356 |
+
|
| 357 |
+
Single Responsibility: Only handles sample access and preprocessing.
|
| 358 |
+
"""
|
| 359 |
+
|
| 360 |
+
def __init__(
|
| 361 |
+
self,
|
| 362 |
+
data: List[Dict[str, Any]],
|
| 363 |
+
tokenizer,
|
| 364 |
+
max_audio_len: int = 500,
|
| 365 |
+
max_seq_len: int = 2048
|
| 366 |
+
):
|
| 367 |
+
self.data = data
|
| 368 |
+
self.tokenizer = tokenizer
|
| 369 |
+
self.max_audio = max_audio_len * 5 # Account for downsampling
|
| 370 |
+
self.max_seq_len = max_seq_len
|
| 371 |
+
|
| 372 |
+
def __len__(self) -> int:
|
| 373 |
+
return len(self.data)
|
| 374 |
+
|
| 375 |
+
def __getitem__(self, idx: int) -> Dict[str, Any]:
|
| 376 |
+
item = self.data[idx]
|
| 377 |
+
|
| 378 |
+
# Whisper features (truncate if needed)
|
| 379 |
+
whisper = item["whisper_features"][:self.max_audio]
|
| 380 |
+
|
| 381 |
+
# Text tokens - use pre-computed if available
|
| 382 |
+
text_tokens = self._get_text_tokens(item)
|
| 383 |
+
|
| 384 |
+
# SNAC tokens (ensure multiple of 7)
|
| 385 |
+
snac_list = self._get_snac_tokens(item)
|
| 386 |
+
|
| 387 |
+
# Optional fields
|
| 388 |
+
word_alignments = item.get("word_alignments", None)
|
| 389 |
+
answer_text = item.get("answer", "")
|
| 390 |
+
|
| 391 |
+
return {
|
| 392 |
+
"whisper": whisper,
|
| 393 |
+
"text_tokens": text_tokens,
|
| 394 |
+
"snac_tokens": snac_list,
|
| 395 |
+
"word_alignments": word_alignments,
|
| 396 |
+
"answer_text": answer_text
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
def _get_text_tokens(self, item: Dict[str, Any]) -> List[int]:
|
| 400 |
+
"""Extract or generate text tokens from item."""
|
| 401 |
+
if "text_tokens" in item and len(item["text_tokens"]) > 0:
|
| 402 |
+
tt = item["text_tokens"]
|
| 403 |
+
return tt.tolist() if hasattr(tt, 'tolist') else list(tt)
|
| 404 |
+
|
| 405 |
+
text = item.get("answer", item.get("text", ""))
|
| 406 |
+
if isinstance(text, str) and len(text) > 0:
|
| 407 |
+
return self.tokenizer.encode(text, add_special_tokens=False)
|
| 408 |
+
|
| 409 |
+
return []
|
| 410 |
+
|
| 411 |
+
def _get_snac_tokens(self, item: Dict[str, Any]) -> List[int]:
|
| 412 |
+
"""Extract SNAC tokens, ensuring multiple of 7."""
|
| 413 |
+
snac = item["snac_tokens"]
|
| 414 |
+
snac_len = (len(snac) // 7) * 7
|
| 415 |
+
snac = snac[:snac_len] if snac_len > 0 else snac[:7]
|
| 416 |
+
return snac.tolist() if hasattr(snac, 'tolist') else list(snac)
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
# ============================================================
|
| 420 |
+
# Collate Functions
|
| 421 |
+
# ============================================================
|
| 422 |
+
def collate_simple(batch: List[Dict[str, Any]]) -> Dict[str, Any]:
|
| 423 |
+
"""
|
| 424 |
+
Simple collate that pads whisper features.
|
| 425 |
+
Interleaving is done in training loop for correct text_ratio.
|
| 426 |
+
"""
|
| 427 |
+
max_w = max(b["whisper"].shape[0] for b in batch)
|
| 428 |
+
max_w = ((max_w + 4) // 5) * 5 # Align to downsample factor
|
| 429 |
+
|
| 430 |
+
whisper_batch = []
|
| 431 |
+
raw_data = []
|
| 432 |
+
|
| 433 |
+
for b in batch:
|
| 434 |
+
w = b["whisper"]
|
| 435 |
+
w_pad = F.pad(w, (0, 0, 0, max_w - w.shape[0]))
|
| 436 |
+
whisper_batch.append(w_pad)
|
| 437 |
+
raw_data.append({
|
| 438 |
+
"text_tokens": b["text_tokens"],
|
| 439 |
+
"snac_tokens": b["snac_tokens"],
|
| 440 |
+
"word_alignments": b.get("word_alignments"),
|
| 441 |
+
"answer_text": b.get("answer_text", "")
|
| 442 |
+
})
|
| 443 |
+
|
| 444 |
+
return {
|
| 445 |
+
"whisper": torch.stack(whisper_batch),
|
| 446 |
+
"raw_data": raw_data
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
# ============================================================
|
| 451 |
+
# DataLoader Factory
|
| 452 |
+
# ============================================================
|
| 453 |
+
class DataLoaderFactory:
|
| 454 |
+
"""
|
| 455 |
+
Factory for creating DataLoaders with optimal settings.
|
| 456 |
+
|
| 457 |
+
Single Responsibility: Only handles DataLoader creation.
|
| 458 |
+
Open/Closed: Can extend multiprocessing strategies without modification.
|
| 459 |
+
|
| 460 |
+
Optimizations:
|
| 461 |
+
- Sequence length bucketing for reduced padding
|
| 462 |
+
- Optimal worker count based on system resources
|
| 463 |
+
"""
|
| 464 |
+
|
| 465 |
+
@staticmethod
|
| 466 |
+
def get_optimal_workers() -> int:
|
| 467 |
+
"""Calculate optimal num_workers based on system resources."""
|
| 468 |
+
if not torch.cuda.is_available():
|
| 469 |
+
return 0
|
| 470 |
+
|
| 471 |
+
try:
|
| 472 |
+
import os
|
| 473 |
+
num_gpus = torch.cuda.device_count()
|
| 474 |
+
cpu_cores = os.cpu_count() or 4
|
| 475 |
+
max_workers = max(1, cpu_cores // 2)
|
| 476 |
+
|
| 477 |
+
# 2 workers per GPU, capped by CPU
|
| 478 |
+
ideal_workers = num_gpus * 2
|
| 479 |
+
num_workers = min(ideal_workers, max_workers)
|
| 480 |
+
|
| 481 |
+
# Check VRAM pressure
|
| 482 |
+
try:
|
| 483 |
+
free_bytes, _ = torch.cuda.mem_get_info(0)
|
| 484 |
+
if free_bytes / 1024**3 < 5:
|
| 485 |
+
num_workers = min(num_workers, 1)
|
| 486 |
+
except Exception:
|
| 487 |
+
pass
|
| 488 |
+
|
| 489 |
+
return max(0, num_workers)
|
| 490 |
+
except Exception:
|
| 491 |
+
return 2
|
| 492 |
+
|
| 493 |
+
@classmethod
|
| 494 |
+
def create(
|
| 495 |
+
cls,
|
| 496 |
+
dataset: Dataset,
|
| 497 |
+
batch_size: int,
|
| 498 |
+
shuffle: bool = True,
|
| 499 |
+
collate_fn: Callable = collate_simple,
|
| 500 |
+
verbose: bool = True,
|
| 501 |
+
use_bucketing: bool = True,
|
| 502 |
+
) -> DataLoader:
|
| 503 |
+
"""
|
| 504 |
+
Create DataLoader with optimal settings.
|
| 505 |
+
|
| 506 |
+
Args:
|
| 507 |
+
dataset: The dataset to load from
|
| 508 |
+
batch_size: Batch size
|
| 509 |
+
shuffle: Whether to shuffle data
|
| 510 |
+
collate_fn: Function to collate samples
|
| 511 |
+
verbose: Whether to log details
|
| 512 |
+
use_bucketing: Use sequence length bucketing (reduces padding overhead)
|
| 513 |
+
"""
|
| 514 |
+
optimal_workers = cls.get_optimal_workers()
|
| 515 |
+
|
| 516 |
+
# Try to use sequence length bucketing
|
| 517 |
+
batch_sampler = None
|
| 518 |
+
if use_bucketing and shuffle:
|
| 519 |
+
try:
|
| 520 |
+
# Get sequence lengths from dataset
|
| 521 |
+
lengths = cls._get_sequence_lengths(dataset)
|
| 522 |
+
if lengths and len(lengths) > batch_size * 10: # Only bucket if enough samples
|
| 523 |
+
batch_sampler = BucketBatchSampler(
|
| 524 |
+
lengths=lengths,
|
| 525 |
+
batch_size=batch_size,
|
| 526 |
+
shuffle=True,
|
| 527 |
+
drop_last=False,
|
| 528 |
+
)
|
| 529 |
+
if verbose:
|
| 530 |
+
log(f"[DataLoader] Using sequence length bucketing ({len(batch_sampler)} batches)")
|
| 531 |
+
except Exception as e:
|
| 532 |
+
if verbose:
|
| 533 |
+
log(f"[DataLoader] Bucketing failed: {e}, using standard batching")
|
| 534 |
+
|
| 535 |
+
# Try different multiprocessing methods
|
| 536 |
+
for mp_method in ['spawn', 'fork', None]:
|
| 537 |
+
try:
|
| 538 |
+
loader = cls._try_create_loader(
|
| 539 |
+
dataset, batch_size, shuffle, collate_fn,
|
| 540 |
+
optimal_workers, mp_method, batch_sampler
|
| 541 |
+
)
|
| 542 |
+
if loader is not None:
|
| 543 |
+
if verbose and mp_method:
|
| 544 |
+
log(f"[DataLoader] Using '{mp_method}' with {optimal_workers} workers")
|
| 545 |
+
elif verbose:
|
| 546 |
+
log("[DataLoader] Using single-process mode")
|
| 547 |
+
return loader
|
| 548 |
+
except Exception as e:
|
| 549 |
+
if verbose:
|
| 550 |
+
log(f"[DataLoader] {mp_method} failed: {str(e)[:50]}...")
|
| 551 |
+
continue
|
| 552 |
+
|
| 553 |
+
# Final fallback
|
| 554 |
+
if verbose:
|
| 555 |
+
log("[DataLoader] Fallback to num_workers=0")
|
| 556 |
+
return DataLoader(
|
| 557 |
+
dataset,
|
| 558 |
+
batch_size=batch_size,
|
| 559 |
+
shuffle=shuffle,
|
| 560 |
+
collate_fn=collate_fn,
|
| 561 |
+
num_workers=0,
|
| 562 |
+
pin_memory=True
|
| 563 |
+
)
|
| 564 |
+
|
| 565 |
+
@staticmethod
|
| 566 |
+
def _get_sequence_lengths(dataset: Dataset) -> Optional[List[int]]:
|
| 567 |
+
"""Extract sequence lengths from dataset for bucketing."""
|
| 568 |
+
# Check if dataset has get_sequence_lengths method
|
| 569 |
+
if hasattr(dataset, 'get_sequence_lengths'):
|
| 570 |
+
return dataset.get_sequence_lengths()
|
| 571 |
+
|
| 572 |
+
# For ConcatDataset, try to combine lengths from components
|
| 573 |
+
if isinstance(dataset, ConcatDataset):
|
| 574 |
+
lengths = []
|
| 575 |
+
for ds in dataset.datasets:
|
| 576 |
+
if hasattr(ds, 'get_sequence_lengths'):
|
| 577 |
+
lengths.extend(ds.get_sequence_lengths())
|
| 578 |
+
else:
|
| 579 |
+
return None # Can't get lengths for all, skip bucketing
|
| 580 |
+
return lengths
|
| 581 |
+
|
| 582 |
+
return None
|
| 583 |
+
|
| 584 |
+
@staticmethod
|
| 585 |
+
def _try_create_loader(
|
| 586 |
+
dataset: Dataset,
|
| 587 |
+
batch_size: int,
|
| 588 |
+
shuffle: bool,
|
| 589 |
+
collate_fn: Callable,
|
| 590 |
+
num_workers: int,
|
| 591 |
+
mp_method: Optional[str],
|
| 592 |
+
batch_sampler: Optional[Sampler] = None,
|
| 593 |
+
) -> Optional[DataLoader]:
|
| 594 |
+
"""Try to create DataLoader with given settings."""
|
| 595 |
+
import multiprocessing
|
| 596 |
+
|
| 597 |
+
# When using batch_sampler, don't set batch_size, shuffle, or sampler
|
| 598 |
+
common_kwargs = {
|
| 599 |
+
'collate_fn': collate_fn,
|
| 600 |
+
'pin_memory': True,
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
if batch_sampler is not None:
|
| 604 |
+
common_kwargs['batch_sampler'] = batch_sampler
|
| 605 |
+
else:
|
| 606 |
+
common_kwargs['batch_size'] = batch_size
|
| 607 |
+
common_kwargs['shuffle'] = shuffle
|
| 608 |
+
|
| 609 |
+
if mp_method and num_workers > 0:
|
| 610 |
+
mp_context = multiprocessing.get_context(mp_method)
|
| 611 |
+
loader = DataLoader(
|
| 612 |
+
dataset,
|
| 613 |
+
num_workers=num_workers,
|
| 614 |
+
multiprocessing_context=mp_context,
|
| 615 |
+
persistent_workers=True,
|
| 616 |
+
**common_kwargs
|
| 617 |
+
)
|
| 618 |
+
# Test if it works
|
| 619 |
+
test_iter = iter(loader)
|
| 620 |
+
del test_iter
|
| 621 |
+
return loader
|
| 622 |
+
else:
|
| 623 |
+
return DataLoader(
|
| 624 |
+
dataset,
|
| 625 |
+
num_workers=0,
|
| 626 |
+
**common_kwargs
|
| 627 |
+
)
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
def create_dataloader(
|
| 631 |
+
dataset: Dataset,
|
| 632 |
+
batch_size: int,
|
| 633 |
+
shuffle: bool = True,
|
| 634 |
+
verbose: bool = True,
|
| 635 |
+
use_bucketing: bool = True,
|
| 636 |
+
) -> DataLoader:
|
| 637 |
+
"""
|
| 638 |
+
Convenience function for creating DataLoaders.
|
| 639 |
+
|
| 640 |
+
Args:
|
| 641 |
+
dataset: Dataset to load from
|
| 642 |
+
batch_size: Batch size
|
| 643 |
+
shuffle: Whether to shuffle
|
| 644 |
+
verbose: Whether to log
|
| 645 |
+
use_bucketing: Use sequence length bucketing
|
| 646 |
+
"""
|
| 647 |
+
return DataLoaderFactory.create(
|
| 648 |
+
dataset, batch_size, shuffle,
|
| 649 |
+
collate_fn=collate_simple, verbose=verbose,
|
| 650 |
+
use_bucketing=use_bucketing,
|
| 651 |
+
)
|
| 652 |
+
|
| 653 |
+
|
| 654 |
+
# ============================================================
|
| 655 |
+
# Dataset Loading Pipeline
|
| 656 |
+
# ============================================================
|
| 657 |
+
def load_datasets(
|
| 658 |
+
paths: List[str],
|
| 659 |
+
tokenizer,
|
| 660 |
+
max_audio_len: int = 500,
|
| 661 |
+
max_seq_len: int = 2048,
|
| 662 |
+
verbose: bool = True,
|
| 663 |
+
lazy_loading: bool = True, # Use memory-efficient lazy loading
|
| 664 |
+
) -> Dataset:
|
| 665 |
+
"""
|
| 666 |
+
Load and combine multiple datasets.
|
| 667 |
+
|
| 668 |
+
Args:
|
| 669 |
+
paths: List of dataset paths (files or directories)
|
| 670 |
+
tokenizer: Tokenizer for text encoding
|
| 671 |
+
max_audio_len: Maximum audio length
|
| 672 |
+
max_seq_len: Maximum sequence length
|
| 673 |
+
verbose: Whether to log progress
|
| 674 |
+
lazy_loading: Use memory-efficient lazy loading for batch directories
|
| 675 |
+
|
| 676 |
+
Returns:
|
| 677 |
+
Combined dataset
|
| 678 |
+
"""
|
| 679 |
+
if verbose:
|
| 680 |
+
log("\nLoading datasets (lazy loading enabled)..." if lazy_loading else "\nLoading datasets...")
|
| 681 |
+
|
| 682 |
+
all_datasets = []
|
| 683 |
+
|
| 684 |
+
for path in paths:
|
| 685 |
+
path = Path(path)
|
| 686 |
+
try:
|
| 687 |
+
# Check if this is a batch directory (use lazy loading)
|
| 688 |
+
batches_dir = None
|
| 689 |
+
if path.name.endswith('.batches') and path.is_dir():
|
| 690 |
+
batches_dir = path
|
| 691 |
+
elif Path(f"{path}.batches").exists():
|
| 692 |
+
batches_dir = Path(f"{path}.batches")
|
| 693 |
+
elif path.is_dir():
|
| 694 |
+
batch_files = list(path.glob("batch_*.pt"))
|
| 695 |
+
if batch_files:
|
| 696 |
+
batches_dir = path
|
| 697 |
+
|
| 698 |
+
if batches_dir and lazy_loading:
|
| 699 |
+
# Use memory-efficient lazy loading
|
| 700 |
+
batch_files = sorted(batches_dir.glob("batch_*.pt"))
|
| 701 |
+
if batch_files:
|
| 702 |
+
dataset = LazyShardedDataset(
|
| 703 |
+
batch_files,
|
| 704 |
+
tokenizer,
|
| 705 |
+
max_audio_len=max_audio_len,
|
| 706 |
+
max_seq_len=max_seq_len,
|
| 707 |
+
cache_size=5, # Keep 5 batches in memory
|
| 708 |
+
verbose=verbose
|
| 709 |
+
)
|
| 710 |
+
all_datasets.append(dataset)
|
| 711 |
+
if verbose:
|
| 712 |
+
log(f" {path.name}: {len(dataset):,} samples (lazy loading)")
|
| 713 |
+
else:
|
| 714 |
+
# Fall back to full loading for single files
|
| 715 |
+
loader = ShardedDatasetLoader(verbose=verbose)
|
| 716 |
+
data = loader.load(str(path))
|
| 717 |
+
if data:
|
| 718 |
+
dataset = InterleavedDataset(
|
| 719 |
+
data, tokenizer,
|
| 720 |
+
max_audio_len=max_audio_len,
|
| 721 |
+
max_seq_len=max_seq_len
|
| 722 |
+
)
|
| 723 |
+
all_datasets.append(dataset)
|
| 724 |
+
if verbose:
|
| 725 |
+
log(f" {path.name}: {len(data):,} samples")
|
| 726 |
+
|
| 727 |
+
except FileNotFoundError as e:
|
| 728 |
+
if verbose:
|
| 729 |
+
log(f" [WARN] {e}")
|
| 730 |
+
|
| 731 |
+
if not all_datasets:
|
| 732 |
+
raise ValueError("No datasets loaded!")
|
| 733 |
+
|
| 734 |
+
return ConcatDataset(all_datasets) if len(all_datasets) > 1 else all_datasets[0]
|
training/interleaving.py
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Interleaved sequence creation for IST-LM training.
|
| 3 |
+
|
| 4 |
+
Single Responsibility: Only handles interleaved sequence generation.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import random
|
| 8 |
+
import torch
|
| 9 |
+
import torch.nn.functional as F
|
| 10 |
+
from typing import List, Dict, Tuple, Optional, Any
|
| 11 |
+
from .config import SNAC_BASE_OFFSET, SNAC_LAYERS_PER_FRAME, SNAC_LAYER_OFFSET, EOS_TOKEN
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def apply_snac_offset(token_idx: int, position: int) -> int:
|
| 15 |
+
"""
|
| 16 |
+
Apply position-based offset to SNAC token.
|
| 17 |
+
|
| 18 |
+
SNAC uses 7 tokens per frame with position-based offsets:
|
| 19 |
+
offset = SNAC_BASE_OFFSET + (position % 7) * 4096
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
token_idx: Raw SNAC token index
|
| 23 |
+
position: Position in the sequence
|
| 24 |
+
|
| 25 |
+
Returns:
|
| 26 |
+
Offset-adjusted token index
|
| 27 |
+
"""
|
| 28 |
+
if int(token_idx) >= SNAC_BASE_OFFSET:
|
| 29 |
+
# Already has offset applied
|
| 30 |
+
return int(token_idx)
|
| 31 |
+
offset = SNAC_BASE_OFFSET + (position % SNAC_LAYERS_PER_FRAME) * SNAC_LAYER_OFFSET
|
| 32 |
+
return int(token_idx) + offset
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def get_text_ratio(
|
| 36 |
+
global_step: int,
|
| 37 |
+
decay_steps: int = 300,
|
| 38 |
+
initial_ratio: float = 0.9,
|
| 39 |
+
min_ratio: float = 0.0
|
| 40 |
+
) -> float:
|
| 41 |
+
"""
|
| 42 |
+
Calculate text ratio based on training step (IST-LM schedule).
|
| 43 |
+
|
| 44 |
+
Schedule: Start at 90% text, decrease by 10% every decay_steps.
|
| 45 |
+
- Step 0-299: 0.9
|
| 46 |
+
- Step 300-599: 0.8
|
| 47 |
+
- Step 600-899: 0.7
|
| 48 |
+
- ...
|
| 49 |
+
- Step 2700+: 0.0 (pure audio)
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
global_step: Current training step
|
| 53 |
+
decay_steps: Steps between each 10% decay
|
| 54 |
+
initial_ratio: Starting text ratio
|
| 55 |
+
min_ratio: Minimum text ratio
|
| 56 |
+
|
| 57 |
+
Returns:
|
| 58 |
+
Current text ratio
|
| 59 |
+
"""
|
| 60 |
+
num_decays = global_step // decay_steps
|
| 61 |
+
text_ratio = initial_ratio - (num_decays * 0.1)
|
| 62 |
+
return max(min_ratio, text_ratio)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def calculate_dynamic_decay_steps(
|
| 66 |
+
total_steps: int,
|
| 67 |
+
steps_per_epoch: int = None,
|
| 68 |
+
initial_ratio: float = 0.9,
|
| 69 |
+
final_audio_portion: float = 0.2
|
| 70 |
+
) -> int:
|
| 71 |
+
"""
|
| 72 |
+
Calculate decay_steps for scheduled interleaving.
|
| 73 |
+
|
| 74 |
+
Two modes:
|
| 75 |
+
1. If steps_per_epoch provided: Complete decay in first epoch only,
|
| 76 |
+
remaining epochs are pure audio.
|
| 77 |
+
2. Otherwise: Use final_audio_portion to spread decay across training.
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
total_steps: Total training steps
|
| 81 |
+
steps_per_epoch: Steps per epoch (if provided, decay completes in epoch 1)
|
| 82 |
+
initial_ratio: Starting text ratio (default 0.9)
|
| 83 |
+
final_audio_portion: Portion of training with p=0 (only used if steps_per_epoch=None)
|
| 84 |
+
|
| 85 |
+
Returns:
|
| 86 |
+
Calculated decay_steps
|
| 87 |
+
"""
|
| 88 |
+
# Number of decay stages: 0.9 -> 0.0 = 9 steps (not 10)
|
| 89 |
+
num_decay_stages = int(initial_ratio / 0.1)
|
| 90 |
+
|
| 91 |
+
if steps_per_epoch is not None:
|
| 92 |
+
# Complete decay in first epoch - remaining epochs are pure audio
|
| 93 |
+
return max(1, steps_per_epoch // num_decay_stages)
|
| 94 |
+
else:
|
| 95 |
+
# Original behavior: spread across training
|
| 96 |
+
steps_until_pure_audio = int(total_steps * (1 - final_audio_portion))
|
| 97 |
+
return max(1, steps_until_pure_audio // num_decay_stages)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class InterleavingStrategy:
|
| 101 |
+
"""
|
| 102 |
+
Base class for interleaving strategies.
|
| 103 |
+
|
| 104 |
+
Open/Closed: Can create new strategies without modifying this class.
|
| 105 |
+
"""
|
| 106 |
+
|
| 107 |
+
def create_sequence(
|
| 108 |
+
self,
|
| 109 |
+
text_tokens: List[int],
|
| 110 |
+
snac_tokens: List[int],
|
| 111 |
+
text_ratio: float,
|
| 112 |
+
**kwargs
|
| 113 |
+
) -> Tuple[List[int], List[bool]]:
|
| 114 |
+
"""
|
| 115 |
+
Create interleaved sequence.
|
| 116 |
+
|
| 117 |
+
Args:
|
| 118 |
+
text_tokens: Text token IDs
|
| 119 |
+
snac_tokens: SNAC audio token IDs
|
| 120 |
+
text_ratio: Ratio of text vs audio (0.0 = pure audio)
|
| 121 |
+
|
| 122 |
+
Returns:
|
| 123 |
+
Tuple of (interleaved_tokens, is_audio_mask)
|
| 124 |
+
"""
|
| 125 |
+
raise NotImplementedError
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
class PositionalInterleaving(InterleavingStrategy):
|
| 129 |
+
"""
|
| 130 |
+
Positional interleaving strategy (fallback when no alignments).
|
| 131 |
+
|
| 132 |
+
Interleaves text and audio tokens based on fixed patterns
|
| 133 |
+
determined by text_ratio.
|
| 134 |
+
"""
|
| 135 |
+
|
| 136 |
+
# Pattern lookup: text_ratio -> (text_per_chunk, frames_per_chunk)
|
| 137 |
+
PATTERNS = {
|
| 138 |
+
0.9: (1, 3), # 1 text token + 3 audio frames
|
| 139 |
+
0.7: (1, 5), # 1 text token + 5 audio frames
|
| 140 |
+
0.5: (1, 7), # 1 text token + 7 audio frames
|
| 141 |
+
0.3: (1, 10), # 1 text token + 10 audio frames
|
| 142 |
+
0.0: (0, 1), # Pure audio
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
def create_sequence(
|
| 146 |
+
self,
|
| 147 |
+
text_tokens: List[int],
|
| 148 |
+
snac_tokens: List[int],
|
| 149 |
+
text_ratio: float,
|
| 150 |
+
**kwargs
|
| 151 |
+
) -> Tuple[List[int], List[bool]]:
|
| 152 |
+
interleaved = []
|
| 153 |
+
is_audio_mask = []
|
| 154 |
+
|
| 155 |
+
if len(snac_tokens) == 0:
|
| 156 |
+
return text_tokens + [EOS_TOKEN], [False] * (len(text_tokens) + 1)
|
| 157 |
+
|
| 158 |
+
# Group SNAC into frames of 7
|
| 159 |
+
frames = self._group_into_frames(snac_tokens)
|
| 160 |
+
if len(frames) == 0:
|
| 161 |
+
return text_tokens + [EOS_TOKEN], [False] * (len(text_tokens) + 1)
|
| 162 |
+
|
| 163 |
+
# Get interleaving pattern
|
| 164 |
+
text_per_chunk, frames_per_chunk = self._get_pattern(text_ratio)
|
| 165 |
+
|
| 166 |
+
total_text = len(text_tokens)
|
| 167 |
+
total_frames = len(frames)
|
| 168 |
+
|
| 169 |
+
text_idx = 0
|
| 170 |
+
frame_idx = 0
|
| 171 |
+
snac_position = 0
|
| 172 |
+
|
| 173 |
+
while frame_idx < total_frames:
|
| 174 |
+
# Add text tokens
|
| 175 |
+
if text_per_chunk > 0 and text_idx < total_text:
|
| 176 |
+
for _ in range(text_per_chunk):
|
| 177 |
+
if text_idx < total_text:
|
| 178 |
+
interleaved.append(text_tokens[text_idx])
|
| 179 |
+
is_audio_mask.append(False)
|
| 180 |
+
text_idx += 1
|
| 181 |
+
|
| 182 |
+
# Add audio frames
|
| 183 |
+
for _ in range(frames_per_chunk):
|
| 184 |
+
if frame_idx < total_frames:
|
| 185 |
+
frame = frames[frame_idx]
|
| 186 |
+
for tok in frame:
|
| 187 |
+
interleaved.append(apply_snac_offset(tok, snac_position))
|
| 188 |
+
is_audio_mask.append(True)
|
| 189 |
+
snac_position += 1
|
| 190 |
+
frame_idx += 1
|
| 191 |
+
|
| 192 |
+
# Add remaining text (only if not pure audio mode)
|
| 193 |
+
if text_per_chunk > 0:
|
| 194 |
+
while text_idx < total_text:
|
| 195 |
+
interleaved.append(text_tokens[text_idx])
|
| 196 |
+
is_audio_mask.append(False)
|
| 197 |
+
text_idx += 1
|
| 198 |
+
|
| 199 |
+
# Add EOS
|
| 200 |
+
interleaved.append(EOS_TOKEN)
|
| 201 |
+
is_audio_mask.append(False)
|
| 202 |
+
|
| 203 |
+
return interleaved, is_audio_mask
|
| 204 |
+
|
| 205 |
+
def _group_into_frames(self, snac_tokens: List[int]) -> List[List[int]]:
|
| 206 |
+
"""Group SNAC tokens into frames of 7."""
|
| 207 |
+
frames = []
|
| 208 |
+
for i in range(0, len(snac_tokens), SNAC_LAYERS_PER_FRAME):
|
| 209 |
+
frame = snac_tokens[i:i + SNAC_LAYERS_PER_FRAME]
|
| 210 |
+
if len(frame) == SNAC_LAYERS_PER_FRAME:
|
| 211 |
+
frames.append(frame)
|
| 212 |
+
return frames
|
| 213 |
+
|
| 214 |
+
def _get_pattern(self, text_ratio: float) -> Tuple[int, int]:
|
| 215 |
+
"""Get interleaving pattern for given text_ratio."""
|
| 216 |
+
if text_ratio >= 0.9:
|
| 217 |
+
return self.PATTERNS[0.9]
|
| 218 |
+
elif text_ratio >= 0.7:
|
| 219 |
+
return self.PATTERNS[0.7]
|
| 220 |
+
elif text_ratio >= 0.5:
|
| 221 |
+
return self.PATTERNS[0.5]
|
| 222 |
+
elif text_ratio >= 0.3:
|
| 223 |
+
return self.PATTERNS[0.3]
|
| 224 |
+
else:
|
| 225 |
+
return self.PATTERNS[0.0]
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
class AlignedInterleaving(InterleavingStrategy):
|
| 229 |
+
"""
|
| 230 |
+
Word-aligned interleaving strategy.
|
| 231 |
+
|
| 232 |
+
Uses word alignments to semantically replace audio spans
|
| 233 |
+
with corresponding text tokens.
|
| 234 |
+
"""
|
| 235 |
+
|
| 236 |
+
def create_sequence(
|
| 237 |
+
self,
|
| 238 |
+
text_tokens: List[int],
|
| 239 |
+
snac_tokens: List[int],
|
| 240 |
+
text_ratio: float,
|
| 241 |
+
word_alignments: Optional[List[Dict]] = None,
|
| 242 |
+
tokenizer=None,
|
| 243 |
+
answer_text: str = "",
|
| 244 |
+
**kwargs
|
| 245 |
+
) -> Tuple[List[int], List[bool]]:
|
| 246 |
+
# Fall back to positional if no alignments
|
| 247 |
+
if not word_alignments or text_ratio <= 0:
|
| 248 |
+
return PositionalInterleaving().create_sequence(
|
| 249 |
+
text_tokens, snac_tokens, text_ratio
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
# Check for pre-computed tokens
|
| 253 |
+
has_precomputed = (
|
| 254 |
+
len(word_alignments) > 0 and
|
| 255 |
+
'tokens' in word_alignments[0] and
|
| 256 |
+
word_alignments[0]['tokens']
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
if not has_precomputed and not tokenizer:
|
| 260 |
+
return PositionalInterleaving().create_sequence(
|
| 261 |
+
text_tokens, snac_tokens, text_ratio
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
interleaved = []
|
| 265 |
+
is_audio_mask = []
|
| 266 |
+
|
| 267 |
+
# Group SNAC into frames
|
| 268 |
+
frames = []
|
| 269 |
+
for i in range(0, len(snac_tokens), SNAC_LAYERS_PER_FRAME):
|
| 270 |
+
frame = snac_tokens[i:i + SNAC_LAYERS_PER_FRAME]
|
| 271 |
+
if len(frame) == SNAC_LAYERS_PER_FRAME:
|
| 272 |
+
frames.append(frame)
|
| 273 |
+
|
| 274 |
+
if len(frames) == 0:
|
| 275 |
+
return text_tokens + [EOS_TOKEN], [False] * (len(text_tokens) + 1)
|
| 276 |
+
|
| 277 |
+
total_frames = len(frames)
|
| 278 |
+
|
| 279 |
+
# Randomly select words to replace with text
|
| 280 |
+
num_words = len(word_alignments)
|
| 281 |
+
num_text_words = int(num_words * text_ratio)
|
| 282 |
+
word_indices = list(range(num_words))
|
| 283 |
+
random.shuffle(word_indices)
|
| 284 |
+
text_word_indices = set(word_indices[:num_text_words])
|
| 285 |
+
|
| 286 |
+
# Frame rate conversion
|
| 287 |
+
max_alignment_frame = max(wa['end_frame'] for wa in word_alignments)
|
| 288 |
+
frame_ratio = total_frames / max_alignment_frame if max_alignment_frame > total_frames * 2 else 1.0
|
| 289 |
+
|
| 290 |
+
snac_position = 0
|
| 291 |
+
|
| 292 |
+
for word_idx, alignment in enumerate(word_alignments):
|
| 293 |
+
word = alignment['word']
|
| 294 |
+
start_frame = int(alignment['start_frame'] * frame_ratio)
|
| 295 |
+
end_frame = min(int(alignment['end_frame'] * frame_ratio), total_frames)
|
| 296 |
+
|
| 297 |
+
if word_idx in text_word_indices:
|
| 298 |
+
# Replace audio with text
|
| 299 |
+
word_tokens = alignment.get('tokens', [])
|
| 300 |
+
if not word_tokens and tokenizer:
|
| 301 |
+
word_tokens = tokenizer.encode(word, add_special_tokens=False)
|
| 302 |
+
|
| 303 |
+
for tok in word_tokens:
|
| 304 |
+
interleaved.append(tok)
|
| 305 |
+
is_audio_mask.append(False)
|
| 306 |
+
snac_position = end_frame * SNAC_LAYERS_PER_FRAME
|
| 307 |
+
else:
|
| 308 |
+
# Keep audio
|
| 309 |
+
for f_idx in range(start_frame, end_frame):
|
| 310 |
+
if f_idx < total_frames:
|
| 311 |
+
frame = frames[f_idx]
|
| 312 |
+
for tok in frame:
|
| 313 |
+
interleaved.append(apply_snac_offset(tok, snac_position))
|
| 314 |
+
is_audio_mask.append(True)
|
| 315 |
+
snac_position += 1
|
| 316 |
+
|
| 317 |
+
# Add remaining frames
|
| 318 |
+
remaining_start = max(wa['end_frame'] for wa in word_alignments)
|
| 319 |
+
remaining_start = min(int(remaining_start * frame_ratio), total_frames)
|
| 320 |
+
for f_idx in range(remaining_start, total_frames):
|
| 321 |
+
frame = frames[f_idx]
|
| 322 |
+
for tok in frame:
|
| 323 |
+
interleaved.append(apply_snac_offset(tok, snac_position))
|
| 324 |
+
is_audio_mask.append(True)
|
| 325 |
+
snac_position += 1
|
| 326 |
+
|
| 327 |
+
# Add EOS
|
| 328 |
+
interleaved.append(EOS_TOKEN)
|
| 329 |
+
is_audio_mask.append(False)
|
| 330 |
+
|
| 331 |
+
return interleaved, is_audio_mask
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def create_interleaved_sequence(
|
| 335 |
+
text_tokens: List[int],
|
| 336 |
+
snac_tokens: List[int],
|
| 337 |
+
text_ratio: float = 0.9,
|
| 338 |
+
word_alignments: Optional[List[Dict]] = None,
|
| 339 |
+
tokenizer=None,
|
| 340 |
+
answer_text: str = ""
|
| 341 |
+
) -> Tuple[List[int], List[bool]]:
|
| 342 |
+
"""
|
| 343 |
+
Create interleaved sequence (convenience function).
|
| 344 |
+
|
| 345 |
+
Automatically selects the best strategy based on available data.
|
| 346 |
+
"""
|
| 347 |
+
if word_alignments and text_ratio > 0:
|
| 348 |
+
strategy = AlignedInterleaving()
|
| 349 |
+
else:
|
| 350 |
+
strategy = PositionalInterleaving()
|
| 351 |
+
|
| 352 |
+
return strategy.create_sequence(
|
| 353 |
+
text_tokens=text_tokens,
|
| 354 |
+
snac_tokens=snac_tokens,
|
| 355 |
+
text_ratio=text_ratio,
|
| 356 |
+
word_alignments=word_alignments,
|
| 357 |
+
tokenizer=tokenizer,
|
| 358 |
+
answer_text=answer_text,
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
def apply_interleaving(
|
| 363 |
+
batch: Dict[str, Any],
|
| 364 |
+
text_ratio: float,
|
| 365 |
+
tokenizer=None,
|
| 366 |
+
max_seq_len: int = 2048
|
| 367 |
+
) -> Dict[str, torch.Tensor]:
|
| 368 |
+
"""
|
| 369 |
+
Apply interleaving to a batch of samples.
|
| 370 |
+
|
| 371 |
+
Args:
|
| 372 |
+
batch: Batch from DataLoader with 'whisper' and 'raw_data'
|
| 373 |
+
text_ratio: Current text ratio
|
| 374 |
+
tokenizer: Tokenizer for on-the-fly encoding
|
| 375 |
+
max_seq_len: Maximum sequence length
|
| 376 |
+
|
| 377 |
+
Returns:
|
| 378 |
+
Batch with 'whisper', 'interleaved', 'is_audio_mask'
|
| 379 |
+
"""
|
| 380 |
+
raw_data = batch["raw_data"]
|
| 381 |
+
sequences = []
|
| 382 |
+
max_seq = 0
|
| 383 |
+
|
| 384 |
+
for item in raw_data:
|
| 385 |
+
interleaved, is_audio = create_interleaved_sequence(
|
| 386 |
+
item["text_tokens"],
|
| 387 |
+
item["snac_tokens"],
|
| 388 |
+
text_ratio,
|
| 389 |
+
word_alignments=item.get("word_alignments"),
|
| 390 |
+
tokenizer=tokenizer,
|
| 391 |
+
answer_text=item.get("answer_text", "")
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
# Truncate if needed
|
| 395 |
+
if len(interleaved) > max_seq_len:
|
| 396 |
+
interleaved = interleaved[:max_seq_len]
|
| 397 |
+
is_audio = is_audio[:max_seq_len]
|
| 398 |
+
|
| 399 |
+
sequences.append((interleaved, is_audio))
|
| 400 |
+
max_seq = max(max_seq, len(interleaved))
|
| 401 |
+
|
| 402 |
+
# Pad and stack
|
| 403 |
+
interleaved_batch = []
|
| 404 |
+
is_audio_batch = []
|
| 405 |
+
|
| 406 |
+
for interleaved, is_audio in sequences:
|
| 407 |
+
seq_tensor = torch.tensor(interleaved, dtype=torch.long)
|
| 408 |
+
mask_tensor = torch.tensor(is_audio, dtype=torch.bool)
|
| 409 |
+
|
| 410 |
+
seq_pad = F.pad(seq_tensor, (0, max_seq - len(interleaved)), value=-100)
|
| 411 |
+
mask_pad = F.pad(mask_tensor, (0, max_seq - len(is_audio)), value=False)
|
| 412 |
+
|
| 413 |
+
interleaved_batch.append(seq_pad)
|
| 414 |
+
is_audio_batch.append(mask_pad)
|
| 415 |
+
|
| 416 |
+
return {
|
| 417 |
+
"whisper": batch["whisper"],
|
| 418 |
+
"interleaved": torch.stack(interleaved_batch),
|
| 419 |
+
"is_audio_mask": torch.stack(is_audio_batch)
|
| 420 |
+
}
|
training/models.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Model components for Speech-to-Speech training.
|
| 3 |
+
|
| 4 |
+
Single Responsibility: Only defines model architectures.
|
| 5 |
+
Open/Closed: Can extend with new adapters without modifying existing code.
|
| 6 |
+
|
| 7 |
+
Optimizations:
|
| 8 |
+
- Flash Attention 2 for memory-efficient attention (10-20x savings on long sequences)
|
| 9 |
+
- BFloat16 for better numerical stability than FP16
|
| 10 |
+
- Gradient checkpointing for memory savings
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
import torch.nn as nn
|
| 15 |
+
from typing import Optional
|
| 16 |
+
from .config import (
|
| 17 |
+
DEFAULT_WHISPER_DIM,
|
| 18 |
+
DEFAULT_LLM_DIM,
|
| 19 |
+
DEFAULT_DOWNSAMPLE,
|
| 20 |
+
DEFAULT_INTERMEDIATE_DIM,
|
| 21 |
+
)
|
| 22 |
+
from .utils import log
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class SpeechAdapter(nn.Module):
|
| 26 |
+
"""
|
| 27 |
+
Speech adapter that maps Whisper features to LLM embedding space.
|
| 28 |
+
|
| 29 |
+
Architecture: 5× downsampling + FFN with intermediate dim
|
| 30 |
+
|
| 31 |
+
Based on LLaMA-Omni 2 design:
|
| 32 |
+
- Concatenates 5 consecutive Whisper frames
|
| 33 |
+
- Projects through 2-layer FFN
|
| 34 |
+
- Applies LayerNorm for stability
|
| 35 |
+
|
| 36 |
+
Args:
|
| 37 |
+
whisper_dim: Dimension of Whisper features (default: 1280)
|
| 38 |
+
llm_dim: Dimension of LLM embeddings (default: 3072)
|
| 39 |
+
downsample: Downsampling factor (default: 5)
|
| 40 |
+
intermediate_dim: Hidden dimension of FFN (default: 2048)
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
def __init__(
|
| 44 |
+
self,
|
| 45 |
+
whisper_dim: int = DEFAULT_WHISPER_DIM,
|
| 46 |
+
llm_dim: int = DEFAULT_LLM_DIM,
|
| 47 |
+
downsample: int = DEFAULT_DOWNSAMPLE,
|
| 48 |
+
intermediate_dim: int = DEFAULT_INTERMEDIATE_DIM
|
| 49 |
+
):
|
| 50 |
+
super().__init__()
|
| 51 |
+
self.whisper_dim = whisper_dim
|
| 52 |
+
self.llm_dim = llm_dim
|
| 53 |
+
self.downsample = downsample
|
| 54 |
+
self.intermediate_dim = intermediate_dim
|
| 55 |
+
|
| 56 |
+
concat_dim = whisper_dim * downsample
|
| 57 |
+
|
| 58 |
+
self.ffn = nn.Sequential(
|
| 59 |
+
nn.Linear(concat_dim, intermediate_dim),
|
| 60 |
+
nn.GELU(),
|
| 61 |
+
nn.Linear(intermediate_dim, llm_dim),
|
| 62 |
+
nn.LayerNorm(llm_dim)
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 66 |
+
"""
|
| 67 |
+
Forward pass.
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
x: Whisper features [B, T, D]
|
| 71 |
+
|
| 72 |
+
Returns:
|
| 73 |
+
LLM embeddings [B, T // downsample, llm_dim]
|
| 74 |
+
"""
|
| 75 |
+
B, T, D = x.shape
|
| 76 |
+
|
| 77 |
+
# Ensure T is divisible by downsample
|
| 78 |
+
T_new = (T // self.downsample) * self.downsample
|
| 79 |
+
x = x[:, :T_new]
|
| 80 |
+
|
| 81 |
+
# Reshape: [B, T, D] -> [B, T // downsample, D * downsample]
|
| 82 |
+
x = x.reshape(B, T_new // self.downsample, D * self.downsample)
|
| 83 |
+
|
| 84 |
+
return self.ffn(x)
|
| 85 |
+
|
| 86 |
+
def get_num_params(self) -> int:
|
| 87 |
+
"""Return total number of parameters."""
|
| 88 |
+
return sum(p.numel() for p in self.parameters())
|
| 89 |
+
|
| 90 |
+
def get_config(self) -> dict:
|
| 91 |
+
"""Return configuration dict for serialization."""
|
| 92 |
+
return {
|
| 93 |
+
"whisper_dim": self.whisper_dim,
|
| 94 |
+
"llm_dim": self.llm_dim,
|
| 95 |
+
"downsample": self.downsample,
|
| 96 |
+
"intermediate_dim": self.intermediate_dim,
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
@classmethod
|
| 100 |
+
def from_config(cls, config: dict) -> 'SpeechAdapter':
|
| 101 |
+
"""Create adapter from configuration dict."""
|
| 102 |
+
return cls(**config)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
class ModelFactory:
|
| 106 |
+
"""
|
| 107 |
+
Factory for creating models with consistent settings.
|
| 108 |
+
|
| 109 |
+
Single Responsibility: Only handles model instantiation.
|
| 110 |
+
Dependency Inversion: Depends on abstractions (config), not concretions.
|
| 111 |
+
"""
|
| 112 |
+
|
| 113 |
+
@staticmethod
|
| 114 |
+
def create_adapter(
|
| 115 |
+
whisper_dim: int = DEFAULT_WHISPER_DIM,
|
| 116 |
+
llm_dim: int = DEFAULT_LLM_DIM,
|
| 117 |
+
dtype: torch.dtype = torch.float32,
|
| 118 |
+
checkpoint_path: Optional[str] = None
|
| 119 |
+
) -> SpeechAdapter:
|
| 120 |
+
"""
|
| 121 |
+
Create a SpeechAdapter, optionally loading from checkpoint.
|
| 122 |
+
|
| 123 |
+
Args:
|
| 124 |
+
whisper_dim: Whisper feature dimension
|
| 125 |
+
llm_dim: LLM embedding dimension
|
| 126 |
+
dtype: Tensor dtype
|
| 127 |
+
checkpoint_path: Optional path to checkpoint
|
| 128 |
+
|
| 129 |
+
Returns:
|
| 130 |
+
Initialized SpeechAdapter
|
| 131 |
+
"""
|
| 132 |
+
adapter = SpeechAdapter(
|
| 133 |
+
whisper_dim=whisper_dim,
|
| 134 |
+
llm_dim=llm_dim,
|
| 135 |
+
).to(dtype=dtype)
|
| 136 |
+
|
| 137 |
+
if checkpoint_path:
|
| 138 |
+
ckpt = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
|
| 139 |
+
if "adapter" in ckpt:
|
| 140 |
+
adapter.load_state_dict(ckpt["adapter"])
|
| 141 |
+
elif "state_dict" in ckpt:
|
| 142 |
+
adapter.load_state_dict(ckpt["state_dict"])
|
| 143 |
+
else:
|
| 144 |
+
adapter.load_state_dict(ckpt)
|
| 145 |
+
|
| 146 |
+
return adapter
|
| 147 |
+
|
| 148 |
+
@staticmethod
|
| 149 |
+
def create_llm(
|
| 150 |
+
model_path: str,
|
| 151 |
+
dtype: torch.dtype = torch.bfloat16,
|
| 152 |
+
freeze: bool = True,
|
| 153 |
+
gradient_checkpointing: bool = False,
|
| 154 |
+
use_flash_attention: bool = True,
|
| 155 |
+
verbose: bool = True,
|
| 156 |
+
):
|
| 157 |
+
"""
|
| 158 |
+
Create and configure the LLM with memory optimizations.
|
| 159 |
+
|
| 160 |
+
Args:
|
| 161 |
+
model_path: HuggingFace model path
|
| 162 |
+
dtype: Tensor dtype (BF16 recommended for stability)
|
| 163 |
+
freeze: Whether to freeze all parameters
|
| 164 |
+
gradient_checkpointing: Enable gradient checkpointing
|
| 165 |
+
use_flash_attention: Try to use Flash Attention 2 (10-20x memory savings)
|
| 166 |
+
verbose: Log configuration details
|
| 167 |
+
|
| 168 |
+
Returns:
|
| 169 |
+
Configured LLM model
|
| 170 |
+
"""
|
| 171 |
+
from transformers import AutoModelForCausalLM
|
| 172 |
+
|
| 173 |
+
# Determine attention implementation
|
| 174 |
+
# Flash Attention 2 provides 10-20x memory savings on long sequences
|
| 175 |
+
# Requires: Ampere/Ada/Hopper GPU (RTX 30xx, 40xx, A100, H100)
|
| 176 |
+
attn_impl = "sdpa" # Default fallback
|
| 177 |
+
|
| 178 |
+
if use_flash_attention and torch.cuda.is_available():
|
| 179 |
+
try:
|
| 180 |
+
# Actually try to import flash_attn to verify it works
|
| 181 |
+
import flash_attn
|
| 182 |
+
# Check GPU capability (Flash Attention 2 requires SM 80+)
|
| 183 |
+
major, _ = torch.cuda.get_device_capability()
|
| 184 |
+
if major >= 8: # Ampere or newer (RTX 30xx, 40xx, A100, H100)
|
| 185 |
+
attn_impl = "flash_attention_2"
|
| 186 |
+
if verbose:
|
| 187 |
+
log(f"[LLM] Using Flash Attention 2 v{flash_attn.__version__} (10-20x memory savings)")
|
| 188 |
+
else:
|
| 189 |
+
if verbose:
|
| 190 |
+
log(f"[LLM] GPU SM {major}.x too old for Flash Attention 2, using SDPA")
|
| 191 |
+
except ImportError:
|
| 192 |
+
if verbose:
|
| 193 |
+
log("[LLM] flash_attn not installed, using SDPA")
|
| 194 |
+
except Exception as e:
|
| 195 |
+
if verbose:
|
| 196 |
+
log(f"[LLM] Flash Attention check failed: {e}, using SDPA")
|
| 197 |
+
|
| 198 |
+
# Load model with optimizations
|
| 199 |
+
try:
|
| 200 |
+
llm = AutoModelForCausalLM.from_pretrained(
|
| 201 |
+
model_path,
|
| 202 |
+
torch_dtype=dtype,
|
| 203 |
+
attn_implementation=attn_impl,
|
| 204 |
+
)
|
| 205 |
+
except Exception as e:
|
| 206 |
+
# Fallback if flash_attention_2 fails
|
| 207 |
+
if attn_impl == "flash_attention_2":
|
| 208 |
+
if verbose:
|
| 209 |
+
log(f"[LLM] Flash Attention failed ({e}), falling back to SDPA")
|
| 210 |
+
llm = AutoModelForCausalLM.from_pretrained(
|
| 211 |
+
model_path,
|
| 212 |
+
torch_dtype=dtype,
|
| 213 |
+
attn_implementation="sdpa",
|
| 214 |
+
)
|
| 215 |
+
else:
|
| 216 |
+
raise
|
| 217 |
+
|
| 218 |
+
if freeze:
|
| 219 |
+
for p in llm.parameters():
|
| 220 |
+
p.requires_grad = False
|
| 221 |
+
llm.eval()
|
| 222 |
+
|
| 223 |
+
if gradient_checkpointing:
|
| 224 |
+
llm.gradient_checkpointing_enable()
|
| 225 |
+
if verbose:
|
| 226 |
+
log("[LLM] Gradient checkpointing enabled")
|
| 227 |
+
|
| 228 |
+
return llm
|
| 229 |
+
|
| 230 |
+
@staticmethod
|
| 231 |
+
def apply_lora(
|
| 232 |
+
llm,
|
| 233 |
+
lora_config: 'LoRAConfig'
|
| 234 |
+
):
|
| 235 |
+
"""
|
| 236 |
+
Apply LoRA to an LLM.
|
| 237 |
+
|
| 238 |
+
Args:
|
| 239 |
+
llm: The LLM model
|
| 240 |
+
lora_config: LoRA configuration
|
| 241 |
+
|
| 242 |
+
Returns:
|
| 243 |
+
LLM with LoRA applied
|
| 244 |
+
"""
|
| 245 |
+
from peft import get_peft_model
|
| 246 |
+
peft_config = lora_config.to_peft_config()
|
| 247 |
+
return get_peft_model(llm, peft_config)
|
training/trainer.py
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Training loop abstraction.
|
| 3 |
+
|
| 4 |
+
Single Responsibility: Handles the training loop logic.
|
| 5 |
+
Open/Closed: Base class can be extended for different training stages.
|
| 6 |
+
Liskov Substitution: Stage1Trainer and Stage2Trainer are interchangeable where Trainer is expected.
|
| 7 |
+
|
| 8 |
+
Optimizations implemented:
|
| 9 |
+
- OOM handling with proper recovery (PyTorch FAQ best practices)
|
| 10 |
+
- Gradient NaN/Inf detection and skipping
|
| 11 |
+
- Gradient norm monitoring for stability
|
| 12 |
+
- Dynamic sequence length based on text ratio
|
| 13 |
+
- CUDA memory fragmentation reduction
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
import gc
|
| 18 |
+
import math
|
| 19 |
+
import torch
|
| 20 |
+
import torch.nn.functional as F
|
| 21 |
+
from torch.optim.lr_scheduler import CosineAnnealingLR
|
| 22 |
+
from accelerate import Accelerator
|
| 23 |
+
from accelerate.utils import set_seed
|
| 24 |
+
from tqdm import tqdm
|
| 25 |
+
from typing import Optional, Dict, Any, Tuple
|
| 26 |
+
from abc import ABC, abstractmethod
|
| 27 |
+
|
| 28 |
+
# Reduce CUDA memory fragmentation (PyTorch recommendation)
|
| 29 |
+
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
| 30 |
+
|
| 31 |
+
from .config import TrainingConfig, GPUConfig
|
| 32 |
+
from .data import load_datasets, create_dataloader
|
| 33 |
+
from .models import SpeechAdapter, ModelFactory
|
| 34 |
+
from .interleaving import (
|
| 35 |
+
get_text_ratio,
|
| 36 |
+
calculate_dynamic_decay_steps,
|
| 37 |
+
apply_interleaving,
|
| 38 |
+
)
|
| 39 |
+
from .checkpoint import CheckpointManager, TrainingState
|
| 40 |
+
from .utils import (
|
| 41 |
+
log,
|
| 42 |
+
setup_cuda_optimizations,
|
| 43 |
+
setup_hf_login,
|
| 44 |
+
load_tokenizer,
|
| 45 |
+
should_enable_gradient_checkpointing,
|
| 46 |
+
write_step,
|
| 47 |
+
get_device_info,
|
| 48 |
+
limit_ram_usage,
|
| 49 |
+
get_ram_info,
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class BaseTrainer(ABC):
|
| 54 |
+
"""
|
| 55 |
+
Abstract base class for trainers.
|
| 56 |
+
|
| 57 |
+
Implements Template Method pattern: defines training skeleton,
|
| 58 |
+
subclasses implement specific steps.
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
def __init__(self, config: TrainingConfig):
|
| 62 |
+
self.config = config
|
| 63 |
+
self.accelerator: Optional[Accelerator] = None
|
| 64 |
+
self.tokenizer = None
|
| 65 |
+
self.adapter: Optional[SpeechAdapter] = None
|
| 66 |
+
self.llm = None
|
| 67 |
+
self.optimizer = None
|
| 68 |
+
self.scheduler = None
|
| 69 |
+
self.train_loader = None
|
| 70 |
+
self.checkpoint_manager: Optional[CheckpointManager] = None
|
| 71 |
+
|
| 72 |
+
# Training state
|
| 73 |
+
self.global_step = 0
|
| 74 |
+
self.start_epoch = 0
|
| 75 |
+
self.best_loss = float("inf")
|
| 76 |
+
self.current_text_ratio = config.initial_text_ratio
|
| 77 |
+
|
| 78 |
+
# Stability monitoring
|
| 79 |
+
self.nan_count = 0
|
| 80 |
+
self.oom_count = 0
|
| 81 |
+
self.last_grad_norm = 0.0
|
| 82 |
+
self.max_grad_norm_seen = 0.0
|
| 83 |
+
|
| 84 |
+
@property
|
| 85 |
+
def is_main(self) -> bool:
|
| 86 |
+
"""Check if this is the main process."""
|
| 87 |
+
return self.accelerator is None or self.accelerator.is_main_process
|
| 88 |
+
|
| 89 |
+
@property
|
| 90 |
+
def device(self):
|
| 91 |
+
"""Get current device."""
|
| 92 |
+
return self.accelerator.device if self.accelerator else torch.device("cpu")
|
| 93 |
+
|
| 94 |
+
def setup(self):
|
| 95 |
+
"""Setup training environment."""
|
| 96 |
+
self._setup_accelerator()
|
| 97 |
+
self._setup_memory()
|
| 98 |
+
self._setup_tokenizer()
|
| 99 |
+
self._setup_data()
|
| 100 |
+
self._setup_models()
|
| 101 |
+
self._setup_optimizer()
|
| 102 |
+
self._setup_checkpoint_manager()
|
| 103 |
+
self._resume_if_needed()
|
| 104 |
+
self._prepare_for_training()
|
| 105 |
+
|
| 106 |
+
def _setup_accelerator(self):
|
| 107 |
+
"""Initialize Accelerator."""
|
| 108 |
+
mixed_precision = "bf16" if torch.cuda.is_available() else None
|
| 109 |
+
|
| 110 |
+
self.accelerator = Accelerator(
|
| 111 |
+
gradient_accumulation_steps=self.config.grad_accum,
|
| 112 |
+
mixed_precision=mixed_precision,
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
set_seed(42)
|
| 116 |
+
|
| 117 |
+
if self.is_main:
|
| 118 |
+
self._log_setup_info()
|
| 119 |
+
|
| 120 |
+
def _log_setup_info(self):
|
| 121 |
+
"""Log setup information."""
|
| 122 |
+
device_info = get_device_info()
|
| 123 |
+
gpu_config = GPUConfig.auto_detect()
|
| 124 |
+
|
| 125 |
+
log("=" * 60)
|
| 126 |
+
log(self._get_stage_name())
|
| 127 |
+
log("=" * 60)
|
| 128 |
+
log(f"Device: {device_info}")
|
| 129 |
+
log(f"GPU: {gpu_config.name} ({gpu_config.vram_gb}GB)")
|
| 130 |
+
log(f"Num processes: {self.accelerator.num_processes}")
|
| 131 |
+
log(f"Batch: {self.config.batch_size}, Grad accum: {self.config.grad_accum}")
|
| 132 |
+
log(f"LR: {self.config.learning_rate}, Epochs: {self.config.epochs}")
|
| 133 |
+
if getattr(self.config, 'no_decay', False):
|
| 134 |
+
log(f"Fixed text ratio: {self.config.initial_text_ratio} (no decay)")
|
| 135 |
+
else:
|
| 136 |
+
log(f"Initial text ratio: {self.config.initial_text_ratio}")
|
| 137 |
+
log("[Optimizations] OOM recovery, NaN detection, grad monitoring, expandable_segments")
|
| 138 |
+
|
| 139 |
+
def _setup_memory(self):
|
| 140 |
+
"""Configure memory settings."""
|
| 141 |
+
if self.device.type == 'cuda':
|
| 142 |
+
setup_cuda_optimizations(self.config.vram_fraction)
|
| 143 |
+
|
| 144 |
+
ram_total, _ = get_ram_info()
|
| 145 |
+
ram_limit = self.config.ram_limit_gb or (ram_total * 0.80)
|
| 146 |
+
limit_ram_usage(ram_limit)
|
| 147 |
+
|
| 148 |
+
if self.is_main:
|
| 149 |
+
log(f"[Memory] VRAM: {self.config.vram_fraction*100:.0f}%, RAM: {ram_limit:.1f}GB")
|
| 150 |
+
|
| 151 |
+
def _setup_tokenizer(self):
|
| 152 |
+
"""Load tokenizer."""
|
| 153 |
+
setup_hf_login()
|
| 154 |
+
self.tokenizer = load_tokenizer(self.config.model_path)
|
| 155 |
+
|
| 156 |
+
def _setup_data(self):
|
| 157 |
+
"""Load datasets and create dataloader."""
|
| 158 |
+
if self.is_main:
|
| 159 |
+
log("\nLoading datasets...")
|
| 160 |
+
|
| 161 |
+
dataset = load_datasets(
|
| 162 |
+
self.config.data_paths,
|
| 163 |
+
self.tokenizer,
|
| 164 |
+
max_audio_len=self.config.max_audio_len,
|
| 165 |
+
max_seq_len=self.config.max_seq_len,
|
| 166 |
+
verbose=self.is_main,
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
# Apply demo/test mode
|
| 170 |
+
if self.config.test_mode:
|
| 171 |
+
dataset = torch.utils.data.Subset(dataset, range(min(5, len(dataset))))
|
| 172 |
+
self.config.batch_size = min(self.config.batch_size, len(dataset))
|
| 173 |
+
self.config.grad_accum = 1
|
| 174 |
+
elif self.config.demo_mode:
|
| 175 |
+
dataset = torch.utils.data.Subset(dataset, range(min(1000, len(dataset))))
|
| 176 |
+
self.config.batch_size = min(4, self.config.batch_size)
|
| 177 |
+
|
| 178 |
+
if self.is_main:
|
| 179 |
+
log(f"Total samples: {len(dataset):,}")
|
| 180 |
+
|
| 181 |
+
self.train_loader = create_dataloader(
|
| 182 |
+
dataset,
|
| 183 |
+
self.config.batch_size,
|
| 184 |
+
shuffle=True,
|
| 185 |
+
verbose=self.is_main,
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
@abstractmethod
|
| 189 |
+
def _setup_models(self):
|
| 190 |
+
"""Setup models (implemented by subclasses)."""
|
| 191 |
+
pass
|
| 192 |
+
|
| 193 |
+
@abstractmethod
|
| 194 |
+
def _setup_optimizer(self):
|
| 195 |
+
"""Setup optimizer (implemented by subclasses)."""
|
| 196 |
+
pass
|
| 197 |
+
|
| 198 |
+
@abstractmethod
|
| 199 |
+
def _setup_checkpoint_manager(self):
|
| 200 |
+
"""Setup checkpoint manager (implemented by subclasses)."""
|
| 201 |
+
pass
|
| 202 |
+
|
| 203 |
+
def _resume_if_needed(self):
|
| 204 |
+
"""Resume from checkpoint if specified."""
|
| 205 |
+
if not self.config.resume_from or not os.path.exists(self.config.resume_from):
|
| 206 |
+
return
|
| 207 |
+
|
| 208 |
+
if self.is_main:
|
| 209 |
+
log(f"\nResuming from: {self.config.resume_from}")
|
| 210 |
+
|
| 211 |
+
ckpt = self.checkpoint_manager.load(self.config.resume_from)
|
| 212 |
+
self._load_checkpoint(ckpt)
|
| 213 |
+
|
| 214 |
+
@abstractmethod
|
| 215 |
+
def _load_checkpoint(self, ckpt: Dict[str, Any]):
|
| 216 |
+
"""Load checkpoint state (implemented by subclasses)."""
|
| 217 |
+
pass
|
| 218 |
+
|
| 219 |
+
def _prepare_for_training(self):
|
| 220 |
+
"""Prepare models and optimizer for training."""
|
| 221 |
+
# Prepare with accelerator
|
| 222 |
+
prepared = self.accelerator.prepare(
|
| 223 |
+
self.adapter, self.llm, self.optimizer, self.train_loader
|
| 224 |
+
)
|
| 225 |
+
self.adapter, self.llm, self.optimizer, self.train_loader = prepared
|
| 226 |
+
|
| 227 |
+
# Calculate steps
|
| 228 |
+
self.steps_per_epoch = max(1, len(self.train_loader) // self.config.grad_accum)
|
| 229 |
+
self.total_steps = max(1, self.steps_per_epoch * self.config.epochs)
|
| 230 |
+
self.warmup_steps = int(self.total_steps * self.config.warmup_ratio)
|
| 231 |
+
|
| 232 |
+
# Calculate decay steps
|
| 233 |
+
if self.config.dynamic_decay:
|
| 234 |
+
self.effective_decay_steps = calculate_dynamic_decay_steps(
|
| 235 |
+
self.total_steps,
|
| 236 |
+
steps_per_epoch=self.steps_per_epoch, # Complete decay in epoch 1
|
| 237 |
+
initial_ratio=self.config.initial_text_ratio,
|
| 238 |
+
)
|
| 239 |
+
if self.is_main:
|
| 240 |
+
log(f"[Dynamic Decay] decay_steps={self.effective_decay_steps} (decay completes in epoch 1)")
|
| 241 |
+
else:
|
| 242 |
+
self.effective_decay_steps = self.config.decay_steps
|
| 243 |
+
|
| 244 |
+
# Setup scheduler
|
| 245 |
+
self.scheduler = CosineAnnealingLR(
|
| 246 |
+
self.optimizer,
|
| 247 |
+
T_max=max(1, self.total_steps - self.warmup_steps),
|
| 248 |
+
eta_min=1e-6,
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
if self.is_main:
|
| 252 |
+
log(f"Steps: {self.steps_per_epoch}/epoch, {self.total_steps} total, {self.warmup_steps} warmup")
|
| 253 |
+
|
| 254 |
+
@abstractmethod
|
| 255 |
+
def _get_stage_name(self) -> str:
|
| 256 |
+
"""Get stage name for logging."""
|
| 257 |
+
pass
|
| 258 |
+
|
| 259 |
+
def train(self):
|
| 260 |
+
"""Main training loop."""
|
| 261 |
+
if self.is_main:
|
| 262 |
+
log("\n" + "=" * 60)
|
| 263 |
+
log(f"STARTING {self._get_stage_name()}")
|
| 264 |
+
log("=" * 60)
|
| 265 |
+
|
| 266 |
+
for epoch in range(self.start_epoch, self.config.epochs):
|
| 267 |
+
self._train_epoch(epoch)
|
| 268 |
+
|
| 269 |
+
self._finish_training()
|
| 270 |
+
|
| 271 |
+
def _train_epoch(self, epoch: int):
|
| 272 |
+
"""Train one epoch."""
|
| 273 |
+
self.adapter.train()
|
| 274 |
+
epoch_loss = 0
|
| 275 |
+
accum_loss = 0
|
| 276 |
+
|
| 277 |
+
pbar = tqdm(
|
| 278 |
+
self.train_loader,
|
| 279 |
+
desc=f"Epoch {epoch+1}/{self.config.epochs}",
|
| 280 |
+
disable=not self.is_main,
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
for batch_idx, raw_batch in enumerate(pbar):
|
| 284 |
+
loss = self._train_step(raw_batch, batch_idx)
|
| 285 |
+
accum_loss += loss
|
| 286 |
+
|
| 287 |
+
if self.accelerator.sync_gradients:
|
| 288 |
+
self._update_after_step(accum_loss, pbar)
|
| 289 |
+
epoch_loss += accum_loss
|
| 290 |
+
accum_loss = 0
|
| 291 |
+
|
| 292 |
+
self._finish_epoch(epoch, epoch_loss)
|
| 293 |
+
|
| 294 |
+
def _check_numerical_stability(
|
| 295 |
+
self,
|
| 296 |
+
loss: torch.Tensor,
|
| 297 |
+
params,
|
| 298 |
+
) -> Tuple[bool, str]:
|
| 299 |
+
"""
|
| 300 |
+
Check for NaN/Inf in loss and gradients.
|
| 301 |
+
|
| 302 |
+
Returns:
|
| 303 |
+
(is_stable, reason): Tuple of stability flag and reason if unstable
|
| 304 |
+
"""
|
| 305 |
+
# Check loss
|
| 306 |
+
if torch.isnan(loss) or torch.isinf(loss):
|
| 307 |
+
return False, f"loss={loss.item()}"
|
| 308 |
+
|
| 309 |
+
# Check gradients
|
| 310 |
+
for name, param in params:
|
| 311 |
+
if param.grad is not None:
|
| 312 |
+
if torch.isnan(param.grad).any():
|
| 313 |
+
return False, f"NaN gradient in {name}"
|
| 314 |
+
if torch.isinf(param.grad).any():
|
| 315 |
+
return False, f"Inf gradient in {name}"
|
| 316 |
+
|
| 317 |
+
return True, ""
|
| 318 |
+
|
| 319 |
+
def _compute_grad_norm(self, params) -> float:
|
| 320 |
+
"""Compute total gradient norm for monitoring."""
|
| 321 |
+
total_norm = 0.0
|
| 322 |
+
for _, param in params:
|
| 323 |
+
if param.grad is not None:
|
| 324 |
+
param_norm = param.grad.data.norm(2)
|
| 325 |
+
total_norm += param_norm.item() ** 2
|
| 326 |
+
return math.sqrt(total_norm)
|
| 327 |
+
|
| 328 |
+
def _train_step(self, raw_batch: Dict[str, Any], batch_idx: int) -> float:
|
| 329 |
+
"""Single training step with OOM and NaN/Inf handling.
|
| 330 |
+
|
| 331 |
+
Stability features:
|
| 332 |
+
- OOM recovery outside except clause (PyTorch FAQ best practice)
|
| 333 |
+
- NaN/Inf detection in loss and gradients
|
| 334 |
+
- Gradient norm monitoring
|
| 335 |
+
- gc.collect() before empty_cache() for better cleanup
|
| 336 |
+
|
| 337 |
+
See: https://pytorch.org/docs/stable/notes/faq.html
|
| 338 |
+
"""
|
| 339 |
+
# Update text ratio (skip if no_decay mode for Stage 1)
|
| 340 |
+
if not getattr(self.config, 'no_decay', False):
|
| 341 |
+
self.current_text_ratio = get_text_ratio(
|
| 342 |
+
self.global_step,
|
| 343 |
+
self.effective_decay_steps,
|
| 344 |
+
self.config.initial_text_ratio,
|
| 345 |
+
)
|
| 346 |
+
# else: keep current_text_ratio fixed at initial value
|
| 347 |
+
|
| 348 |
+
# Dynamic max_seq_len based on text ratio
|
| 349 |
+
dynamic_max_seq = self._get_dynamic_max_seq()
|
| 350 |
+
|
| 351 |
+
# Apply interleaving
|
| 352 |
+
batch = apply_interleaving(
|
| 353 |
+
raw_batch,
|
| 354 |
+
self.current_text_ratio,
|
| 355 |
+
tokenizer=self.tokenizer,
|
| 356 |
+
max_seq_len=dynamic_max_seq,
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
# Get adapter dtype for proper casting
|
| 360 |
+
adapter_dtype = next(self.adapter.parameters()).dtype
|
| 361 |
+
whisper = batch["whisper"].to(self.device, dtype=adapter_dtype)
|
| 362 |
+
interleaved = batch["interleaved"].to(self.device)
|
| 363 |
+
|
| 364 |
+
# Clear cache periodically
|
| 365 |
+
if batch_idx % 100 == 0 and self.device.type == 'cuda':
|
| 366 |
+
torch.cuda.empty_cache()
|
| 367 |
+
|
| 368 |
+
# Forward and backward with OOM and NaN handling
|
| 369 |
+
# Use flag pattern to move recovery outside except (PyTorch FAQ recommendation)
|
| 370 |
+
oom_error = False
|
| 371 |
+
nan_error = False
|
| 372 |
+
error_reason = ""
|
| 373 |
+
seq_len_for_log = interleaved.shape[1]
|
| 374 |
+
loss_value = 0.0
|
| 375 |
+
|
| 376 |
+
try:
|
| 377 |
+
with self.accelerator.accumulate(self.adapter):
|
| 378 |
+
loss = self._compute_loss(whisper, interleaved)
|
| 379 |
+
|
| 380 |
+
# Check for NaN/Inf in loss before backward
|
| 381 |
+
if torch.isnan(loss) or torch.isinf(loss):
|
| 382 |
+
nan_error = True
|
| 383 |
+
error_reason = f"loss={loss.item()}"
|
| 384 |
+
else:
|
| 385 |
+
self.accelerator.backward(loss)
|
| 386 |
+
|
| 387 |
+
if self.accelerator.sync_gradients:
|
| 388 |
+
# Check gradient stability
|
| 389 |
+
trainable_params = list(self._get_trainable_params_named())
|
| 390 |
+
is_stable, reason = self._check_numerical_stability(loss, trainable_params)
|
| 391 |
+
|
| 392 |
+
if not is_stable:
|
| 393 |
+
nan_error = True
|
| 394 |
+
error_reason = reason
|
| 395 |
+
else:
|
| 396 |
+
# Track gradient norm for monitoring
|
| 397 |
+
self.last_grad_norm = self._compute_grad_norm(trainable_params)
|
| 398 |
+
self.max_grad_norm_seen = max(self.max_grad_norm_seen, self.last_grad_norm)
|
| 399 |
+
|
| 400 |
+
# Clip gradients
|
| 401 |
+
self.accelerator.clip_grad_norm_(
|
| 402 |
+
[p for _, p in trainable_params],
|
| 403 |
+
self.config.max_grad_norm,
|
| 404 |
+
)
|
| 405 |
+
|
| 406 |
+
if not nan_error:
|
| 407 |
+
self.optimizer.step()
|
| 408 |
+
self.optimizer.zero_grad(set_to_none=True)
|
| 409 |
+
loss_value = loss.item()
|
| 410 |
+
|
| 411 |
+
except torch.cuda.OutOfMemoryError:
|
| 412 |
+
oom_error = True
|
| 413 |
+
|
| 414 |
+
# Recovery outside except clause (prevents memory leak from exception stack frame)
|
| 415 |
+
if oom_error:
|
| 416 |
+
self.oom_count += 1
|
| 417 |
+
if self.is_main:
|
| 418 |
+
log(f"[OOM #{self.oom_count}] Skipping batch {batch_idx} (seq_len={seq_len_for_log})")
|
| 419 |
+
del whisper, interleaved, batch
|
| 420 |
+
gc.collect()
|
| 421 |
+
torch.cuda.empty_cache()
|
| 422 |
+
self.optimizer.zero_grad(set_to_none=True)
|
| 423 |
+
return 0.0
|
| 424 |
+
|
| 425 |
+
if nan_error:
|
| 426 |
+
self.nan_count += 1
|
| 427 |
+
if self.is_main:
|
| 428 |
+
log(f"[NaN #{self.nan_count}] Skipping batch {batch_idx}: {error_reason}")
|
| 429 |
+
self.optimizer.zero_grad(set_to_none=True)
|
| 430 |
+
return 0.0
|
| 431 |
+
|
| 432 |
+
return loss_value
|
| 433 |
+
|
| 434 |
+
def _get_trainable_params_named(self):
|
| 435 |
+
"""Get named trainable parameters for gradient checking."""
|
| 436 |
+
# Default: adapter parameters
|
| 437 |
+
for name, param in self.adapter.named_parameters():
|
| 438 |
+
if param.requires_grad:
|
| 439 |
+
yield f"adapter.{name}", param
|
| 440 |
+
|
| 441 |
+
def _get_dynamic_max_seq(self) -> int:
|
| 442 |
+
"""Get dynamic max sequence length based on text ratio.
|
| 443 |
+
|
| 444 |
+
More conservative limits to prevent CUDA OOM.
|
| 445 |
+
RTX 4090 (24GB) at 80% VRAM can handle ~1280-1536 max seq with LLM.
|
| 446 |
+
"""
|
| 447 |
+
# Base limit more conservative for memory safety
|
| 448 |
+
base_limit = min(self.config.max_seq_len, 1536)
|
| 449 |
+
|
| 450 |
+
if self.current_text_ratio >= 0.7:
|
| 451 |
+
return base_limit
|
| 452 |
+
elif self.current_text_ratio >= 0.5:
|
| 453 |
+
return int(base_limit * 0.75) # ~1152
|
| 454 |
+
elif self.current_text_ratio >= 0.3:
|
| 455 |
+
return int(base_limit * 0.6) # ~922
|
| 456 |
+
else:
|
| 457 |
+
return int(base_limit * 0.5) # ~768
|
| 458 |
+
|
| 459 |
+
def _compute_loss(
|
| 460 |
+
self,
|
| 461 |
+
whisper: torch.Tensor,
|
| 462 |
+
interleaved: torch.Tensor,
|
| 463 |
+
) -> torch.Tensor:
|
| 464 |
+
"""
|
| 465 |
+
Compute training loss with numerical stability.
|
| 466 |
+
|
| 467 |
+
Numerical stability measures:
|
| 468 |
+
- Clamp logits to prevent extreme values
|
| 469 |
+
- Use BF16 which has better dynamic range than FP16
|
| 470 |
+
"""
|
| 471 |
+
unwrapped_llm = self.accelerator.unwrap_model(self.llm)
|
| 472 |
+
|
| 473 |
+
# Forward through adapter
|
| 474 |
+
audio_embeds = self.adapter(whisper)
|
| 475 |
+
|
| 476 |
+
# Get token embeddings
|
| 477 |
+
input_tokens = interleaved[:, :-1].clamp(min=0)
|
| 478 |
+
with torch.no_grad():
|
| 479 |
+
token_embeds = unwrapped_llm.model.embed_tokens(input_tokens)
|
| 480 |
+
|
| 481 |
+
# Combine embeddings
|
| 482 |
+
combined = torch.cat([audio_embeds, token_embeds], dim=1)
|
| 483 |
+
|
| 484 |
+
# Forward through LLM
|
| 485 |
+
outputs = self.llm(inputs_embeds=combined, use_cache=False)
|
| 486 |
+
logits = outputs.logits
|
| 487 |
+
|
| 488 |
+
# Compute loss with numerical stability
|
| 489 |
+
audio_len = audio_embeds.shape[1]
|
| 490 |
+
seq_len = interleaved.shape[1]
|
| 491 |
+
seq_logits = logits[:, audio_len-1:audio_len-1+seq_len]
|
| 492 |
+
|
| 493 |
+
# Clamp logits to prevent numerical issues in softmax
|
| 494 |
+
# Large logits can cause overflow in exp() during cross_entropy
|
| 495 |
+
seq_logits = seq_logits.clamp(min=-100, max=100)
|
| 496 |
+
|
| 497 |
+
return F.cross_entropy(
|
| 498 |
+
seq_logits.reshape(-1, logits.size(-1)),
|
| 499 |
+
interleaved.reshape(-1),
|
| 500 |
+
ignore_index=-100,
|
| 501 |
+
label_smoothing=self.config.label_smoothing,
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
@abstractmethod
|
| 505 |
+
def _get_trainable_params(self):
|
| 506 |
+
"""Get trainable parameters for gradient clipping."""
|
| 507 |
+
pass
|
| 508 |
+
|
| 509 |
+
def _update_after_step(self, accum_loss: float, pbar):
|
| 510 |
+
"""Update after gradient accumulation step."""
|
| 511 |
+
# Learning rate schedule
|
| 512 |
+
if self.global_step < self.warmup_steps:
|
| 513 |
+
lr_scale = (self.global_step + 1) / self.warmup_steps
|
| 514 |
+
for pg in self.optimizer.param_groups:
|
| 515 |
+
pg["lr"] = self.config.learning_rate * lr_scale
|
| 516 |
+
else:
|
| 517 |
+
self.scheduler.step()
|
| 518 |
+
|
| 519 |
+
self.global_step += 1
|
| 520 |
+
if self.is_main:
|
| 521 |
+
write_step(self.global_step)
|
| 522 |
+
|
| 523 |
+
# Update progress bar with gradient norm for stability monitoring
|
| 524 |
+
avg_loss = accum_loss / self.config.grad_accum
|
| 525 |
+
pbar.set_postfix(
|
| 526 |
+
loss=f"{avg_loss:.4f}",
|
| 527 |
+
lr=f"{self.optimizer.param_groups[0]['lr']:.2e}",
|
| 528 |
+
text_ratio=f"{self.current_text_ratio:.1f}",
|
| 529 |
+
grad=f"{self.last_grad_norm:.1f}",
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
# Log warning if gradient norm is very high (potential instability)
|
| 533 |
+
if self.last_grad_norm > 100 and self.global_step % 50 == 0 and self.is_main:
|
| 534 |
+
log(f"[WARN] High gradient norm: {self.last_grad_norm:.1f} at step {self.global_step}")
|
| 535 |
+
|
| 536 |
+
# Save checkpoint
|
| 537 |
+
if self.global_step % self.config.save_steps == 0 and self.is_main:
|
| 538 |
+
self._save_step_checkpoint(accum_loss)
|
| 539 |
+
|
| 540 |
+
@abstractmethod
|
| 541 |
+
def _save_step_checkpoint(self, loss: float):
|
| 542 |
+
"""Save step checkpoint (implemented by subclasses)."""
|
| 543 |
+
pass
|
| 544 |
+
|
| 545 |
+
@abstractmethod
|
| 546 |
+
def _finish_epoch(self, epoch: int, epoch_loss: float):
|
| 547 |
+
"""Finish epoch (implemented by subclasses)."""
|
| 548 |
+
pass
|
| 549 |
+
|
| 550 |
+
def _finish_training(self):
|
| 551 |
+
"""Finish training."""
|
| 552 |
+
self.accelerator.wait_for_everyone()
|
| 553 |
+
|
| 554 |
+
if self.is_main:
|
| 555 |
+
self.checkpoint_manager.wait_for_saves()
|
| 556 |
+
log("\n" + "=" * 60)
|
| 557 |
+
log(f"{self._get_stage_name()} COMPLETE!")
|
| 558 |
+
log(f"Best loss: {self.best_loss:.4f}")
|
| 559 |
+
log(f"Final text_ratio: {self.current_text_ratio:.1f}")
|
| 560 |
+
log(f"Max gradient norm seen: {self.max_grad_norm_seen:.2f}")
|
| 561 |
+
if self.oom_count > 0:
|
| 562 |
+
log(f"OOM errors recovered: {self.oom_count}")
|
| 563 |
+
if self.nan_count > 0:
|
| 564 |
+
log(f"NaN/Inf errors recovered: {self.nan_count}")
|
| 565 |
+
log("=" * 60)
|
training/utils.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Utility functions for training.
|
| 3 |
+
|
| 4 |
+
Single Responsibility: General utilities that don't fit elsewhere.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import torch
|
| 10 |
+
from typing import Tuple, Optional
|
| 11 |
+
from dataclasses import dataclass
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# ============================================================
|
| 15 |
+
# Logging
|
| 16 |
+
# ============================================================
|
| 17 |
+
_verbose = True
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def setup_logging(verbose: bool = True):
|
| 21 |
+
"""Configure logging verbosity."""
|
| 22 |
+
global _verbose
|
| 23 |
+
_verbose = verbose
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def log(msg: str):
|
| 27 |
+
"""Log message to stdout with flush."""
|
| 28 |
+
if _verbose:
|
| 29 |
+
print(msg)
|
| 30 |
+
sys.stdout.flush()
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# ============================================================
|
| 34 |
+
# Device Information
|
| 35 |
+
# ============================================================
|
| 36 |
+
@dataclass
|
| 37 |
+
class DeviceInfo:
|
| 38 |
+
"""Information about the compute device."""
|
| 39 |
+
device_type: str
|
| 40 |
+
device_name: str
|
| 41 |
+
vram_gb: float
|
| 42 |
+
ram_total_gb: float
|
| 43 |
+
ram_available_gb: float
|
| 44 |
+
num_gpus: int
|
| 45 |
+
|
| 46 |
+
def __str__(self) -> str:
|
| 47 |
+
parts = [f"Device: {self.device_type}"]
|
| 48 |
+
if self.device_name:
|
| 49 |
+
parts.append(f"({self.device_name})")
|
| 50 |
+
if self.vram_gb > 0:
|
| 51 |
+
parts.append(f"VRAM: {self.vram_gb:.0f}GB")
|
| 52 |
+
if self.num_gpus > 1:
|
| 53 |
+
parts.append(f"x{self.num_gpus} GPUs")
|
| 54 |
+
return " | ".join(parts)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def get_device_info() -> DeviceInfo:
|
| 58 |
+
"""Get information about the compute device."""
|
| 59 |
+
device_type = "cpu"
|
| 60 |
+
device_name = ""
|
| 61 |
+
vram_gb = 0.0
|
| 62 |
+
num_gpus = 0
|
| 63 |
+
|
| 64 |
+
# CUDA
|
| 65 |
+
if torch.cuda.is_available():
|
| 66 |
+
device_type = "cuda"
|
| 67 |
+
num_gpus = torch.cuda.device_count()
|
| 68 |
+
try:
|
| 69 |
+
props = torch.cuda.get_device_properties(0)
|
| 70 |
+
device_name = props.name
|
| 71 |
+
vram_gb = props.total_memory / (1024**3)
|
| 72 |
+
except Exception:
|
| 73 |
+
pass
|
| 74 |
+
|
| 75 |
+
# MPS
|
| 76 |
+
elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
|
| 77 |
+
device_type = "mps"
|
| 78 |
+
device_name = "Apple Silicon"
|
| 79 |
+
num_gpus = 1
|
| 80 |
+
|
| 81 |
+
# RAM info
|
| 82 |
+
ram_total, ram_available = get_ram_info()
|
| 83 |
+
|
| 84 |
+
return DeviceInfo(
|
| 85 |
+
device_type=device_type,
|
| 86 |
+
device_name=device_name,
|
| 87 |
+
vram_gb=vram_gb,
|
| 88 |
+
ram_total_gb=ram_total,
|
| 89 |
+
ram_available_gb=ram_available,
|
| 90 |
+
num_gpus=num_gpus,
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def get_ram_info() -> Tuple[float, float]:
|
| 95 |
+
"""Get RAM info in GB (total, available)."""
|
| 96 |
+
try:
|
| 97 |
+
import psutil
|
| 98 |
+
total = psutil.virtual_memory().total / 1024**3
|
| 99 |
+
available = psutil.virtual_memory().available / 1024**3
|
| 100 |
+
return total, available
|
| 101 |
+
except ImportError:
|
| 102 |
+
pass
|
| 103 |
+
|
| 104 |
+
try:
|
| 105 |
+
import subprocess
|
| 106 |
+
result = subprocess.run(
|
| 107 |
+
['free', '-b'],
|
| 108 |
+
capture_output=True, text=True
|
| 109 |
+
)
|
| 110 |
+
lines = result.stdout.strip().split('\n')
|
| 111 |
+
if len(lines) >= 2:
|
| 112 |
+
parts = lines[1].split()
|
| 113 |
+
total = float(parts[1]) / 1024**3
|
| 114 |
+
available = float(parts[6]) / 1024**3 if len(parts) > 6 else float(parts[3]) / 1024**3
|
| 115 |
+
return total, available
|
| 116 |
+
except Exception:
|
| 117 |
+
pass
|
| 118 |
+
|
| 119 |
+
return 0.0, 0.0
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def log_memory_usage() -> str:
|
| 123 |
+
"""Get current memory usage string."""
|
| 124 |
+
parts = []
|
| 125 |
+
|
| 126 |
+
if torch.cuda.is_available():
|
| 127 |
+
used = torch.cuda.memory_allocated() / 1024**3
|
| 128 |
+
reserved = torch.cuda.memory_reserved() / 1024**3
|
| 129 |
+
parts.append(f"GPU: {used:.2f}GB / {reserved:.2f}GB")
|
| 130 |
+
|
| 131 |
+
try:
|
| 132 |
+
import psutil
|
| 133 |
+
ram_used = psutil.virtual_memory().used / 1024**3
|
| 134 |
+
ram_total = psutil.virtual_memory().total / 1024**3
|
| 135 |
+
parts.append(f"RAM: {ram_used:.1f}GB / {ram_total:.1f}GB")
|
| 136 |
+
except ImportError:
|
| 137 |
+
pass
|
| 138 |
+
|
| 139 |
+
return " | ".join(parts)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
# ============================================================
|
| 143 |
+
# Memory Management
|
| 144 |
+
# ============================================================
|
| 145 |
+
def limit_ram_usage(max_ram_gb: float):
|
| 146 |
+
"""Limit RAM usage via resource limits."""
|
| 147 |
+
try:
|
| 148 |
+
import resource
|
| 149 |
+
max_bytes = int(max_ram_gb * 1024**3)
|
| 150 |
+
resource.setrlimit(resource.RLIMIT_AS, (max_bytes, max_bytes))
|
| 151 |
+
except Exception:
|
| 152 |
+
pass
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def setup_cuda_optimizations(vram_fraction: float = 0.80):
|
| 156 |
+
"""Configure CUDA optimizations."""
|
| 157 |
+
if not torch.cuda.is_available():
|
| 158 |
+
return
|
| 159 |
+
|
| 160 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 161 |
+
torch.backends.cudnn.allow_tf32 = True
|
| 162 |
+
torch.backends.cudnn.benchmark = True
|
| 163 |
+
torch.set_float32_matmul_precision('high')
|
| 164 |
+
|
| 165 |
+
try:
|
| 166 |
+
torch.cuda.set_per_process_memory_fraction(vram_fraction)
|
| 167 |
+
torch.cuda.empty_cache()
|
| 168 |
+
except Exception:
|
| 169 |
+
pass
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def should_enable_gradient_checkpointing(
|
| 173 |
+
vram_gb: float,
|
| 174 |
+
dynamic_decay: bool = False,
|
| 175 |
+
threshold_fraction: float = 0.4
|
| 176 |
+
) -> bool:
|
| 177 |
+
"""
|
| 178 |
+
Determine if gradient checkpointing should be enabled.
|
| 179 |
+
|
| 180 |
+
Args:
|
| 181 |
+
vram_gb: Total VRAM in GB
|
| 182 |
+
dynamic_decay: Whether using dynamic decay (longer sequences over time)
|
| 183 |
+
threshold_fraction: Fraction of VRAM that should be free
|
| 184 |
+
|
| 185 |
+
Returns:
|
| 186 |
+
Whether to enable gradient checkpointing
|
| 187 |
+
"""
|
| 188 |
+
if not torch.cuda.is_available():
|
| 189 |
+
return False
|
| 190 |
+
|
| 191 |
+
# With dynamic_decay, sequences get longer over time
|
| 192 |
+
if dynamic_decay and vram_gb <= 32:
|
| 193 |
+
return True
|
| 194 |
+
|
| 195 |
+
# Check available VRAM
|
| 196 |
+
try:
|
| 197 |
+
torch.cuda.empty_cache()
|
| 198 |
+
free_bytes, total_bytes = torch.cuda.mem_get_info(0)
|
| 199 |
+
free_gb = free_bytes / 1024**3
|
| 200 |
+
threshold_gb = vram_gb * threshold_fraction
|
| 201 |
+
return free_gb < threshold_gb
|
| 202 |
+
except Exception:
|
| 203 |
+
# Conservative: enable if VRAM < 20GB
|
| 204 |
+
return vram_gb < 20
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
# ============================================================
|
| 208 |
+
# Step Sharing (for DDP + DataLoader workers)
|
| 209 |
+
# ============================================================
|
| 210 |
+
STEP_FILE = "/tmp/training_step.txt"
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def write_step(step: int):
|
| 214 |
+
"""Write current training step to file (main process only)."""
|
| 215 |
+
try:
|
| 216 |
+
with open(STEP_FILE, "w") as f:
|
| 217 |
+
f.write(str(step))
|
| 218 |
+
except Exception:
|
| 219 |
+
pass
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def read_step() -> int:
|
| 223 |
+
"""Read current training step from file."""
|
| 224 |
+
try:
|
| 225 |
+
with open(STEP_FILE, "r") as f:
|
| 226 |
+
return int(f.read().strip())
|
| 227 |
+
except Exception:
|
| 228 |
+
return 0
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
# ============================================================
|
| 232 |
+
# HuggingFace Helpers
|
| 233 |
+
# ============================================================
|
| 234 |
+
def setup_hf_login():
|
| 235 |
+
"""Setup HuggingFace login from environment."""
|
| 236 |
+
hf_token = os.environ.get("HF_TOKEN")
|
| 237 |
+
if hf_token:
|
| 238 |
+
try:
|
| 239 |
+
from huggingface_hub import login
|
| 240 |
+
login(token=hf_token)
|
| 241 |
+
return True
|
| 242 |
+
except Exception:
|
| 243 |
+
pass
|
| 244 |
+
return False
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def load_tokenizer(model_path: str):
|
| 248 |
+
"""Load tokenizer with proper padding token."""
|
| 249 |
+
from transformers import AutoTokenizer
|
| 250 |
+
|
| 251 |
+
tokenizer = AutoTokenizer.from_pretrained(model_path)
|
| 252 |
+
if tokenizer.pad_token is None:
|
| 253 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 254 |
+
|
| 255 |
+
return tokenizer
|