| |
| """ |
| Dataset Generator - Fully Async Pipeline |
| Q&A -> TTS (2 GPUs) -> Features (2 GPUs) -> Save |
| """ |
| import os |
| import sys |
| import re |
| import time |
| import gc |
| import logging |
| import multiprocessing as mp |
| from pathlib import Path |
| from concurrent.futures import ThreadPoolExecutor, as_completed |
| import numpy as np |
| import requests |
| import torch |
|
|
| |
| logger = None |
|
|
| def setup_logging(log_file=None): |
| """Setup logging to both console and file.""" |
| global logger |
| logger = logging.getLogger("dataset_generator") |
| logger.setLevel(logging.INFO) |
| logger.handlers.clear() |
|
|
| |
| console = logging.StreamHandler(sys.stdout) |
| console.setLevel(logging.INFO) |
| console.setFormatter(logging.Formatter('%(message)s')) |
| logger.addHandler(console) |
|
|
| |
| if log_file: |
| file_handler = logging.FileHandler(log_file, mode='a') |
| file_handler.setLevel(logging.INFO) |
| file_handler.setFormatter(logging.Formatter('%(asctime)s | %(message)s', datefmt='%H:%M:%S')) |
| logger.addHandler(file_handler) |
|
|
| return logger |
|
|
| def log(msg): |
| """Log message to both console and file.""" |
| global logger |
| if logger: |
| logger.info(msg) |
| |
| for handler in logger.handlers: |
| handler.flush() |
| else: |
| print(msg) |
| sys.stdout.flush() |
|
|
|
|
| def load_dotenv(env_path=None): |
| """Load environment variables from .env file.""" |
| if env_path is None: |
| |
| for path in [Path(".env"), Path(__file__).parent.parent / ".env"]: |
| if path.exists(): |
| env_path = path |
| break |
| if env_path and Path(env_path).exists(): |
| with open(env_path) as f: |
| for line in f: |
| line = line.strip() |
| if line and not line.startswith("#") and "=" in line: |
| key, value = line.split("=", 1) |
| os.environ.setdefault(key.strip(), value.strip()) |
| log(f"[ENV] Loaded from {env_path}") |
|
|
|
|
| |
| load_dotenv() |
|
|
| |
| GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "") |
| GROQ_MODEL = "openai/gpt-oss-20b" |
| GROQ_PARALLEL_REQUESTS = 10 |
| QA_PER_REQUEST = 100 |
|
|
|
|
| def get_num_gpus() -> int: |
| """Auto-detect number of available GPUs.""" |
| if not torch.cuda.is_available(): |
| return 1 |
| return max(1, torch.cuda.device_count()) |
|
|
|
|
| |
| NUM_GPUS = get_num_gpus() |
|
|
| |
| MEMORY_PER_ITEM = { |
| "tts": 0.04, |
| "whisper": 0.02, |
| "snac": 0.15, |
| } |
|
|
| |
| DEFAULT_BATCH_SIZES = { |
| "tts": 200, |
| "whisper": 8, |
| "snac": 50, |
| } |
|
|
|
|
| def get_gpu_vram_gb() -> float: |
| """Get total GPU VRAM in GB.""" |
| if not torch.cuda.is_available(): |
| return 0 |
| return torch.cuda.get_device_properties(0).total_memory / 1024**3 |
|
|
|
|
| def calculate_batch_sizes(vram_gb: float = None, shared_gpu: bool = True) -> dict: |
| """ |
| Calculate optimal batch sizes based on available VRAM. |
| |
| When shared_gpu=True (default), TTS and Features share the same GPU, |
| so batch sizes must be more conservative to avoid OOM. |
| |
| VRAM tiers (with shared GPU adjustment): |
| - 80GB+ (H100/H200): Large batches |
| - 40-80GB (A100/H100-64GB): Medium-large batches |
| - 24-40GB (RTX 4090): Medium batches |
| - 16-24GB (RTX 3090/4080): Smaller batches |
| - <16GB: Minimum safe values |
| |
| Returns dict with: tts, whisper (workers), snac batch sizes |
| """ |
| if vram_gb is None: |
| vram_gb = get_gpu_vram_gb() |
|
|
| |
| if vram_gb >= 80: |
| tts_scale = 1.0 |
| whisper_workers = 8 |
| snac_scale = 0.6 |
| elif vram_gb >= 40: |
| tts_scale = 0.75 |
| whisper_workers = 6 |
| snac_scale = 0.5 |
| elif vram_gb >= 24: |
| tts_scale = 0.5 |
| whisper_workers = 4 |
| snac_scale = 0.4 |
| elif vram_gb >= 16: |
| tts_scale = 0.33 |
| whisper_workers = 2 |
| snac_scale = 0.25 |
| else: |
| tts_scale = 0.2 |
| whisper_workers = 1 |
| snac_scale = 0.15 |
|
|
| |
| if not shared_gpu: |
| snac_scale = min(1.0, snac_scale * 1.5) |
| whisper_workers = min(8, whisper_workers + 2) |
|
|
| |
| batch_sizes = { |
| "tts": max(10, int(DEFAULT_BATCH_SIZES["tts"] * tts_scale)), |
| "whisper": whisper_workers, |
| "snac": max(8, int(DEFAULT_BATCH_SIZES["snac"] * snac_scale)), |
| } |
|
|
| return batch_sizes |
|
|
|
|
| def print_batch_config(batch_sizes: dict, vram_gb: float): |
| """Print batch configuration for transparency.""" |
| log(f"[Config] GPU VRAM: {vram_gb:.1f}GB") |
| log(f"[Config] Batch sizes: TTS={batch_sizes['tts']}, Whisper={batch_sizes['whisper']}, SNAC={batch_sizes['snac']}") |
|
|
|
|
| def parse_qa(content): |
| pairs = [] |
| content = content.replace("**", "") |
| current_q, current_a = None, None |
| for line in content.split("\n"): |
| line = line.strip() |
| if not line: |
| continue |
| qm = re.match(r"^[\d\.\)\-\*]*\s*[Qq][:\.]?\s*(.+)", line) |
| am = re.match(r"^[\d\.\)\-\*]*\s*[Aa][:\.]?\s*(.+)", line) |
| if qm: |
| current_q = qm.group(1).strip() |
| elif am: |
| current_a = am.group(1).strip() |
| if current_q and current_a and 2 <= len(current_q.split()) <= 25 and len(current_a) > 3: |
| pairs.append({"q": current_q, "a": current_a}) |
| current_q, current_a = None, None |
| return pairs |
|
|
|
|
| def make_groq_request(request_id: int) -> list: |
| headers = {"Authorization": f"Bearer {GROQ_API_KEY}", "Content-Type": "application/json"} |
| prompt = f"""Generate {QA_PER_REQUEST} unique Q&A pairs on diverse topics. |
| Format: Q: [question] |
| A: [answer] |
| Questions 2-25 words, answers 1-3 sentences.""" |
|
|
| for retry in range(3): |
| try: |
| response = requests.post( |
| "https://api.groq.com/openai/v1/chat/completions", |
| headers=headers, |
| json={"model": GROQ_MODEL, "messages": [{"role": "user", "content": prompt}], "max_tokens": 8000, "temperature": 1.0}, |
| timeout=60 |
| ) |
| if response.status_code == 429: |
| time.sleep(2 ** retry) |
| continue |
| response.raise_for_status() |
| return parse_qa(response.json()["choices"][0]["message"]["content"]) |
| except: |
| if retry < 2: |
| time.sleep(1) |
| return [] |
|
|
|
|
| def qa_producer(target_count: int, tts_queue: mp.Queue, batch_size: int, status_queue: mp.Queue, num_workers: int = 2): |
| """Produces Q&A batches for TTS - exactly target_count pairs.""" |
| seen = set() |
| pairs = [] |
| pending = [] |
| batch_idx = 0 |
| t0 = time.time() |
|
|
| while len(pairs) < target_count: |
| with ThreadPoolExecutor(max_workers=GROQ_PARALLEL_REQUESTS) as ex: |
| futures = [ex.submit(make_groq_request, i) for i in range(GROQ_PARALLEL_REQUESTS)] |
| for f in as_completed(futures): |
| for p in f.result(): |
| if len(pairs) >= target_count: |
| break |
| norm = p["q"].lower().strip().rstrip("?") |
| if norm not in seen: |
| seen.add(norm) |
| pairs.append(p) |
| pending.append(p) |
| if len(pending) >= batch_size: |
| tts_queue.put((batch_idx, pending[:batch_size])) |
| batch_idx += 1 |
| pending = pending[batch_size:] |
| if len(pairs) >= target_count: |
| break |
|
|
| status_queue.put(("qa", len(pairs), target_count, len(pairs)/(time.time()-t0))) |
|
|
| if pending: |
| tts_queue.put((batch_idx, pending)) |
| batch_idx += 1 |
|
|
| for _ in range(num_workers): |
| tts_queue.put(None) |
|
|
| status_queue.put(("qa_done", len(pairs), batch_idx)) |
|
|
|
|
| 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): |
| """TTS worker - processes batches with VRAM-adjusted batch sizes on specific GPU.""" |
| import torch |
| _orig_load = torch.load |
| torch.load = lambda *a, **kw: _orig_load(*a, **{**kw, 'weights_only': False}) |
|
|
| |
| actual_gpu = gpu_offset + (gpu_id % num_gpus) |
| torch.cuda.set_device(actual_gpu) |
| print(f"[TTS-GPU{gpu_id}] Assigned to CUDA device {actual_gpu}") |
|
|
| |
| import json |
| _orig_default = json.JSONEncoder.default |
| def _patched_default(self, obj): |
| if isinstance(obj, torch.dtype): |
| return str(obj).split('.')[-1] |
| return _orig_default(self, obj) |
| json.JSONEncoder.default = _patched_default |
|
|
| from soprano import SopranoTTS |
| |
| |
| |
| |
| vram_gb = torch.cuda.get_device_properties(actual_gpu).total_memory / 1024**3 |
| gpu_name = torch.cuda.get_device_properties(actual_gpu).name |
|
|
| |
| use_lmdeploy = "5090" not in gpu_name and "5080" not in gpu_name and "B100" not in gpu_name |
|
|
| if use_lmdeploy: |
| try: |
| dec_batch = 32 if vram_gb >= 80 else (16 if vram_gb >= 40 else 8) |
| tts = SopranoTTS( |
| backend="lmdeploy", |
| device="cuda", |
| cache_size_mb=4000 if vram_gb >= 24 else 2000, |
| decoder_batch_size=dec_batch, |
| ) |
| print(f"[TTS-GPU{gpu_id}] Using lmdeploy backend (decoder_batch={dec_batch})") |
| except Exception as e: |
| print(f"[TTS-GPU{gpu_id}] lmdeploy failed ({e}), falling back to transformers") |
| tts = SopranoTTS(backend="transformers", device="cuda") |
| else: |
| print(f"[TTS-GPU{gpu_id}] Blackwell GPU detected ({gpu_name}), using transformers backend") |
| tts = SopranoTTS(backend="transformers", device="cuda") |
|
|
| |
| tts_batch = batch_sizes.get("tts", 200) |
| print(f"[TTS-GPU{gpu_id}] Using batch size: {tts_batch}") |
|
|
| status_queue.put(("tts_ready", gpu_id)) |
|
|
| processed = 0 |
| t_start = time.time() |
| last_heartbeat = time.time() |
|
|
| while True: |
| |
| if time.time() - last_heartbeat > 30: |
| status_queue.put(("tts_heartbeat", gpu_id, processed)) |
| last_heartbeat = time.time() |
|
|
| |
| try: |
| item = tts_queue.get(timeout=5) |
| except: |
| continue |
|
|
| if item is None: |
| break |
|
|
| batch_idx, pairs = item |
|
|
| |
| all_results = [] |
| questions = [p["q"] for p in pairs] |
| answers = [p["a"] for p in pairs] |
|
|
| try: |
| |
| combined = questions + answers |
| all_audios = tts.infer_batch(combined) |
| q_audios = all_audios[:len(questions)] |
| a_audios = all_audios[len(questions):] |
|
|
| for j, p in enumerate(pairs): |
| q_np = q_audios[j].cpu().numpy() if hasattr(q_audios[j], 'numpy') else q_audios[j] |
| a_np = a_audios[j].cpu().numpy() if hasattr(a_audios[j], 'numpy') else a_audios[j] |
| all_results.append({ |
| "question": p["q"], "answer": p["a"], |
| "q_audio": np.asarray(q_np, dtype=np.float32), |
| "a_audio": np.asarray(a_np, dtype=np.float32), |
| }) |
|
|
| except Exception as e: |
| import traceback |
| status_queue.put(("tts_error", gpu_id, f"{str(e)}\n{traceback.format_exc()}")) |
| |
| continue |
|
|
| if all_results: |
| feat_queue.put((batch_idx, all_results)) |
| processed += len(all_results) |
| elapsed = time.time() - t_start |
| status_queue.put(("tts", gpu_id, processed, processed/elapsed, len(all_results)/(time.time()-t_start) if elapsed > 0 else 0)) |
|
|
| |
| feat_queue.put(None) |
| status_queue.put(("tts_done", gpu_id, processed)) |
|
|
|
|
| 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): |
| """Features worker - VRAM-adjusted batch sizes for Whisper + SNAC + NeMo NFA on specific GPU.""" |
| import torch |
| _orig = torch.load |
| torch.load = lambda *a, **kw: _orig(*a, **{**kw, 'weights_only': False}) |
|
|
| import torchaudio |
| import snac |
| import tempfile |
| import soundfile as sf |
| from transformers import AutoTokenizer, WhisperModel, WhisperFeatureExtractor |
| from huggingface_hub import login |
|
|
| |
| actual_gpu = gpu_offset + (gpu_id % num_gpus) |
| torch.cuda.set_device(actual_gpu) |
| device = f"cuda:{actual_gpu}" |
| print(f"[Features-GPU{gpu_id}] Assigned to CUDA device {actual_gpu}") |
|
|
| |
| hf_token = os.environ.get("HF_TOKEN") |
| if hf_token: |
| try: |
| login(token=hf_token) |
| except Exception as e: |
| print(f"[WARN] HuggingFace login failed: {e}") |
|
|
| |
| print("[Features] Loading Whisper large-v3-turbo (transformers)...") |
| whisper_model = WhisperModel.from_pretrained("openai/whisper-large-v3-turbo", torch_dtype=torch.float16).to(device).eval() |
| whisper_feature_extractor = WhisperFeatureExtractor.from_pretrained("openai/whisper-large-v3-turbo") |
|
|
| |
| |
| print("[Features] Whisper Turbo loaded successfully") |
|
|
| snac_model = snac.SNAC.from_pretrained("hubertsiuzdak/snac_24khz").to(device).eval() |
|
|
| |
| nfa_model = None |
| try: |
| import nemo.collections.asr as nemo_asr |
| |
| nfa_model = nemo_asr.models.EncDecCTCModel.from_pretrained("nvidia/stt_en_fastconformer_ctc_large") |
| nfa_model = nfa_model.to(device).eval() |
| print("[Features] NeMo Forced Aligner loaded (GPU-accelerated)") |
| except Exception as e: |
| print(f"[WARN] NeMo NFA not available, using proportional alignment: {e}") |
|
|
| |
| tokenizer = None |
| for model_path in ["canopylabs/orpheus-3b-0.1-pretrained", "meta-llama/Llama-3.2-3B"]: |
| try: |
| tokenizer = AutoTokenizer.from_pretrained(model_path, token=hf_token) |
| print(f"[Features] Loaded tokenizer from {model_path}") |
| break |
| except Exception as e: |
| print(f"[WARN] Failed to load tokenizer from {model_path}: {e}") |
|
|
| if tokenizer is None: |
| print("[WARN] No tokenizer available - text_tokens will not be pre-computed") |
|
|
| |
| snac_batch = batch_sizes.get("snac", 50) |
| whisper_workers = batch_sizes.get("whisper", 8) |
| print(f"[Features-GPU{gpu_id}] Using SNAC batch={snac_batch}, Whisper workers={whisper_workers}") |
|
|
| status_queue.put(("feat_ready", gpu_id)) |
|
|
| def process_whisper(audio_data): |
| """Process single audio with Whisper. GPU-accelerated resampling.""" |
| |
| audio_tensor = torch.from_numpy(audio_data).to(device) |
| audio_16k = torchaudio.functional.resample(audio_tensor, 32000, 16000) |
|
|
| |
| max_samples = 480000 |
| if audio_16k.shape[0] > max_samples: |
| audio_16k = audio_16k[:max_samples] |
|
|
| audio_16k_np = audio_16k.cpu().numpy().astype(np.float32) |
|
|
| |
| inputs = whisper_feature_extractor(audio_16k_np, sampling_rate=16000, return_tensors="pt") |
| input_features = inputs.input_features.to(device, dtype=torch.float16) |
|
|
| |
| with torch.no_grad(): |
| encoder_outputs = whisper_model.encoder(input_features) |
|
|
| |
| return encoder_outputs.last_hidden_state.squeeze(0).cpu().float() |
|
|
| def extract_snac_tokens(codes, idx): |
| """Extract SNAC tokens with correct position-based offsets. |
| |
| Each frame has 7 tokens (1 from codebook 0, 2 from codebook 1, 4 from codebook 2). |
| Each position within a frame needs a unique offset: |
| - Position 0: 128266 + 0*4096 |
| - Position 1: 128266 + 1*4096 |
| - Position 2: 128266 + 2*4096 |
| - Position 3: 128266 + 3*4096 |
| - Position 4: 128266 + 4*4096 |
| - Position 5: 128266 + 5*4096 |
| - Position 6: 128266 + 6*4096 |
| """ |
| SNAC_BASE = 128266 |
| tokens = [] |
| for j in range(codes[0].shape[-1]): |
| |
| tokens.append(codes[0][idx, j].item() + SNAC_BASE + 0 * 4096) |
|
|
| |
| if j * 2 + 1 < codes[1].shape[-1]: |
| tokens.append(codes[1][idx, j * 2].item() + SNAC_BASE + 1 * 4096) |
| tokens.append(codes[1][idx, j * 2 + 1].item() + SNAC_BASE + 2 * 4096) |
|
|
| |
| for k in range(4): |
| if j * 4 + k < codes[2].shape[-1]: |
| tokens.append(codes[2][idx, j * 4 + k].item() + SNAC_BASE + (3 + k) * 4096) |
| return tokens |
|
|
| def get_word_alignments_nfa(audio_data, text, sample_rate=32000): |
| """Get word-level alignments using NeMo Forced Aligner (GPU-accelerated).""" |
| if nfa_model is None: |
| return None |
|
|
| try: |
| |
| audio_16k = torchaudio.functional.resample( |
| torch.from_numpy(audio_data), sample_rate, 16000 |
| ).numpy().astype(np.float32) |
|
|
| |
| with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f: |
| temp_path = f.name |
| sf.write(temp_path, audio_16k, 16000) |
|
|
| |
| |
| with torch.no_grad(): |
| |
| hypotheses = nfa_model.transcribe( |
| [temp_path], |
| return_hypotheses=True, |
| batch_size=1 |
| ) |
|
|
| |
| os.unlink(temp_path) |
|
|
| |
| if hypotheses and len(hypotheses) > 0: |
| hyp = hypotheses[0] |
| if hasattr(hyp, 'timestep') and hyp.timestep is not None: |
| |
| word_alignments = [] |
| words = text.split() |
| timestamps = hyp.timestep.get('word', []) |
|
|
| |
| for i, word in enumerate(words): |
| if i < len(timestamps): |
| ts = timestamps[i] |
| start_time = ts.get('start', 0) |
| end_time = ts.get('end', start_time + 0.1) |
| else: |
| |
| audio_duration = len(audio_data) / sample_rate |
| word_start = (i / len(words)) * audio_duration |
| word_end = ((i + 1) / len(words)) * audio_duration |
| start_time, end_time = word_start, word_end |
|
|
| |
| start_frame = int(start_time * 75) |
| end_frame = int(end_time * 75) |
|
|
| word_tokens = [] |
| if tokenizer is not None: |
| word_tokens = tokenizer.encode(word, add_special_tokens=False) |
|
|
| word_alignments.append({ |
| 'word': word, |
| 'start': start_time, |
| 'end': end_time, |
| 'start_frame': start_frame, |
| 'end_frame': end_frame, |
| 'tokens': word_tokens |
| }) |
| return word_alignments |
|
|
| except Exception as e: |
| status_queue.put(("nfa_error", gpu_id, str(e))) |
|
|
| return None |
|
|
| def get_word_alignments_proportional(audio_data, text, sample_rate=32000): |
| """Fallback: proportional word alignment based on character count. |
| |
| Calculates frame indices that match actual SNAC output: |
| - SNAC operates at 24kHz with ~320 samples per frame (75 fps) |
| - Audio is resampled from sample_rate to 24kHz before SNAC |
| """ |
| words = text.split() |
| if not words: |
| return [] |
|
|
| |
| |
| audio_24k_samples = len(audio_data) * 24000 / sample_rate |
| total_snac_frames = int(audio_24k_samples / 320) |
|
|
| if total_snac_frames == 0: |
| return [] |
|
|
| total_chars = sum(len(w) for w in words) |
| if total_chars == 0: |
| return [] |
|
|
| audio_duration = len(audio_data) / sample_rate |
| word_alignments = [] |
| current_frame = 0 |
|
|
| |
| all_word_tokens = [] |
| if tokenizer is not None: |
| for word in words: |
| all_word_tokens.append(tokenizer.encode(word, add_special_tokens=False)) |
| else: |
| all_word_tokens = [[] for _ in words] |
|
|
| for i, word in enumerate(words): |
| |
| word_frames = int((len(word) / total_chars) * total_snac_frames) |
|
|
| |
| if i == len(words) - 1: |
| end_frame = total_snac_frames |
| else: |
| end_frame = min(current_frame + max(1, word_frames), total_snac_frames) |
|
|
| start_frame = current_frame |
|
|
| |
| start_time = start_frame / 75.0 |
| end_time = end_frame / 75.0 |
|
|
| word_alignments.append({ |
| 'word': word, |
| 'start': start_time, |
| 'end': end_time, |
| 'start_frame': start_frame, |
| 'end_frame': end_frame, |
| 'tokens': all_word_tokens[i] |
| }) |
| current_frame = end_frame |
|
|
| return word_alignments |
|
|
| processed = 0 |
| t_start = time.time() |
| last_heartbeat = time.time() |
|
|
| while True: |
| |
| if time.time() - last_heartbeat > 30: |
| status_queue.put(("feat_heartbeat", gpu_id, processed)) |
| last_heartbeat = time.time() |
|
|
| |
| try: |
| item = feat_queue.get(timeout=5) |
| except: |
| continue |
|
|
| if item is None: |
| break |
|
|
| batch_idx, audio_batch = item |
| t0 = time.time() |
|
|
| try: |
| |
| q_audios = [ad["q_audio"] for ad in audio_batch] |
| with ThreadPoolExecutor(max_workers=whisper_workers) as ex: |
| whisper_features = list(ex.map(process_whisper, q_audios)) |
|
|
| |
| a_audios = [ad["a_audio"] for ad in audio_batch] |
| all_tokens = [] |
|
|
| for start in range(0, len(a_audios), snac_batch): |
| end = min(start + snac_batch, len(a_audios)) |
| mini_audios = a_audios[start:end] |
|
|
| max_len = max(a.shape[0] for a in mini_audios) |
| padded = [np.pad(a, (0, max_len - len(a))) for a in mini_audios] |
|
|
| |
| audios_24k = torch.stack([ |
| torchaudio.functional.resample( |
| torch.from_numpy(a).to(device), 32000, 24000 |
| ).cpu() |
| for a in padded |
| ]) |
|
|
| with torch.no_grad(): |
| codes = snac_model.encode(audios_24k.unsqueeze(1).to(device)) |
|
|
| for i in range(len(mini_audios)): |
| all_tokens.append(extract_snac_tokens(codes, i)) |
|
|
| torch.cuda.synchronize() |
|
|
| |
| if processed > 0 and processed % (100 * len(audio_batch)) == 0: |
| torch.cuda.empty_cache() |
| gc.collect() |
|
|
| |
| results = [] |
| for i, ad in enumerate(audio_batch): |
| answer_text = ad["answer"] |
| answer_audio = ad["a_audio"] |
| result = { |
| "whisper_features": whisper_features[i], |
| "snac_tokens": torch.tensor(all_tokens[i], dtype=torch.long), |
| "text": ad["question"], |
| "answer": answer_text |
| } |
|
|
| |
| if tokenizer is not None: |
| text_tokens = tokenizer.encode(answer_text, add_special_tokens=False) |
| result["text_tokens"] = torch.tensor(text_tokens, dtype=torch.long) |
|
|
| |
| |
| word_alignments = get_word_alignments_proportional(answer_audio, answer_text) |
| if word_alignments: |
| result["word_alignments"] = word_alignments |
|
|
| results.append(result) |
|
|
| batch_time = time.time() - t0 |
| batch_rate = len(results) / batch_time if batch_time > 0 else 0 |
|
|
| |
| put_start = time.time() |
| max_put_attempts = 10 |
| for attempt in range(max_put_attempts): |
| try: |
| result_queue.put((batch_idx, results), timeout=30) |
| break |
| except Exception as put_err: |
| if attempt < max_put_attempts - 1: |
| status_queue.put(("feat_warn", gpu_id, f"Queue full, retry {attempt+1}")) |
| time.sleep(1) |
| else: |
| status_queue.put(("feat_error", gpu_id, f"Failed to put results after {max_put_attempts} attempts")) |
| raise put_err |
|
|
| processed += len(results) |
| elapsed = time.time() - t_start |
| status_queue.put(("feat", gpu_id, processed, processed/elapsed, batch_rate)) |
|
|
| |
| del whisper_features, all_tokens, results |
| if processed % 500 == 0: |
| torch.cuda.empty_cache() |
| gc.collect() |
|
|
| except Exception as e: |
| import traceback |
| error_msg = str(e) |
| status_queue.put(("feat_error", gpu_id, f"{error_msg}\n{traceback.format_exc()}")) |
|
|
| |
| torch.cuda.empty_cache() |
| torch.cuda.synchronize() |
| gc.collect() |
|
|
| |
| if "out of memory" in error_msg.lower() or "OOM" in error_msg: |
| status_queue.put(("feat_warn", gpu_id, "OOM detected, clearing memory...")) |
| time.sleep(2) |
| torch.cuda.empty_cache() |
|
|
| continue |
|
|
| status_queue.put(("feat_done", gpu_id, processed)) |
|
|
|
|
| def main(): |
| mp.set_start_method('spawn', force=True) |
|
|
| |
| import resource |
| soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) |
| resource.setrlimit(resource.RLIMIT_NOFILE, (min(65536, hard), hard)) |
|
|
| import argparse |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--count", "--num_samples", type=int, default=100, dest="count") |
| parser.add_argument("--output", type=str, default="./data/dataset.pt") |
| parser.add_argument("--gpus", type=int, default=NUM_GPUS) |
| parser.add_argument("--resume", action="store_true", help="Resume from existing checkpoint") |
| parser.add_argument("--checkpoint-interval", type=int, default=1000, help="Save checkpoint every N items") |
| parser.add_argument("--log-file", type=str, default=None, help="Log file path (default: output.log)") |
| |
| parser.add_argument("--tts-batch", type=int, default=None) |
| parser.add_argument("--snac-batch", type=int, default=None) |
| parser.add_argument("--whisper-workers", type=int, default=None) |
| args = parser.parse_args() |
|
|
| Path(args.output).parent.mkdir(parents=True, exist_ok=True) |
|
|
| |
| log_file = args.log_file or (args.output + ".log") |
| setup_logging(log_file) |
| log(f"[Log] Writing to {log_file}") |
|
|
| |
| vram_gb = get_gpu_vram_gb() |
| batch_sizes = calculate_batch_sizes(vram_gb) |
|
|
| |
| if args.tts_batch is not None: |
| batch_sizes["tts"] = args.tts_batch |
| if args.snac_batch is not None: |
| batch_sizes["snac"] = args.snac_batch |
| if args.whisper_workers is not None: |
| batch_sizes["whisper"] = args.whisper_workers |
|
|
| log("=" * 60) |
| log("Dataset Generator - Fully Async Pipeline") |
| log(f"Target: {args.count} items, GPUs: {args.gpus}") |
| print_batch_config(batch_sizes, vram_gb) |
| log("=" * 60) |
|
|
| |
| checkpoint_path = args.output + ".checkpoint" |
| existing_items = [] |
| start_count = 0 |
|
|
| |
| resume_from_path = None |
|
|
| if args.resume: |
| |
| |
|
|
| new_checkpoint = checkpoint_path + ".new" |
| new_items_count = 0 |
| base_count = 0 |
|
|
| |
| if Path(new_checkpoint).exists(): |
| try: |
| data = torch.load(new_checkpoint, map_location="cpu", weights_only=False, mmap=True) |
| new_items_count = len(data) |
| del data |
| log(f"[Resume] Found {new_items_count} new items in checkpoint") |
| except Exception as e: |
| log(f"[Resume] Failed to read {new_checkpoint}: {e}") |
|
|
| |
| if Path(args.output).exists(): |
| try: |
| data = torch.load(args.output, map_location="cpu", weights_only=False, mmap=True) |
| base_count = len(data) |
| del data |
| resume_from_path = args.output |
| log(f"[Resume] Found {base_count} base items in {args.output}") |
| except Exception as e: |
| log(f"[Resume] Failed to read {args.output}: {e}") |
|
|
| |
| start_count = base_count + new_items_count |
| if start_count > 0: |
| log(f"[Resume] Total: {start_count} items ({base_count} base + {new_items_count} new), need {args.count - start_count} more") |
| else: |
| log("[Resume] No valid resume file found, starting fresh") |
|
|
| if start_count >= args.count: |
| log(f"[Resume] Already have {start_count} items, saving final...") |
| torch.save(existing_items[:args.count], args.output) |
| log(f"COMPLETE: {args.count} items saved to {args.output}") |
| return |
|
|
| remaining_count = args.count - start_count |
| log(f"[Main] Generating {remaining_count} new items...") |
|
|
| total_start = time.time() |
|
|
| |
| |
| |
| tts_queue = mp.Queue() |
| feat_queue = mp.Queue() |
| result_queue = mp.Queue() |
| status_queue = mp.Queue() |
|
|
| workers = [] |
|
|
| |
| actual_num_gpus = get_num_gpus() if torch.cuda.is_available() else 1 |
|
|
| |
| |
| if actual_num_gpus >= 4: |
| tts_gpus = actual_num_gpus // 2 |
| feat_gpus = actual_num_gpus - tts_gpus |
| tts_gpu_offset = 0 |
| feat_gpu_offset = tts_gpus |
| 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}") |
| else: |
| tts_gpus = actual_num_gpus |
| feat_gpus = actual_num_gpus |
| tts_gpu_offset = 0 |
| feat_gpu_offset = 0 |
| log(f"\n[Main] Detected {actual_num_gpus} GPUs, sharing between TTS and Features") |
|
|
| |
| num_tts_workers = min(args.gpus, tts_gpus) |
| num_feat_workers = min(args.gpus, feat_gpus) |
| log(f"[Main] Spawning {num_tts_workers} TTS workers, {num_feat_workers} Features workers") |
|
|
| qa_proc = mp.Process(target=qa_producer, args=(remaining_count, tts_queue, batch_sizes["tts"], status_queue, num_tts_workers)) |
| qa_proc.start() |
| workers.append(qa_proc) |
|
|
| for gpu_id in range(num_tts_workers): |
| p = mp.Process(target=tts_worker, args=(gpu_id, tts_queue, feat_queue, status_queue, batch_sizes, tts_gpus, tts_gpu_offset)) |
| p.start() |
| workers.append(p) |
|
|
| for gpu_id in range(num_feat_workers): |
| p = mp.Process(target=features_worker, args=(gpu_id, feat_queue, result_queue, status_queue, batch_sizes, feat_gpus, feat_gpu_offset)) |
| p.start() |
| workers.append(p) |
|
|
| log("[Pipeline] All workers started, monitoring...") |
|
|
| results = {} |
| tts_done_count = 0 |
| feat_done_count = 0 |
| total_items = 0 |
| t0 = time.time() |
| tts_ready = 0 |
| feat_ready = 0 |
| expected_from_feat = {} |
| feat_queue_closed = False |
| last_checkpoint_count = 0 |
|
|
| |
| last_result_time = time.time() |
| last_status_time = time.time() |
| last_heartbeat_time = {f"tts_{i}": time.time() for i in range(num_tts_workers)} |
| last_heartbeat_time.update({f"feat_{i}": time.time() for i in range(num_feat_workers)}) |
| stall_warning_shown = False |
| errors = [] |
|
|
| while True: |
| |
| for _ in range(100): |
| try: |
| msg = status_queue.get_nowait() |
| last_status_time = time.time() |
| msg_type = msg[0] |
|
|
| if msg_type == "tts_ready": |
| tts_ready += 1 |
| log(f"[TTS-GPU{msg[1]}] Ready ({tts_ready}/{num_tts_workers})") |
| elif msg_type == "feat_ready": |
| feat_ready += 1 |
| log(f"[Features-GPU{msg[1]}] Ready ({feat_ready}/{num_feat_workers})") |
| elif msg_type == "qa": |
| log(f"[Q&A] {msg[1]}/{msg[2]} | {msg[3]:.1f}/s") |
| elif msg_type == "qa_done": |
| log(f"[Q&A] Done: {msg[1]} pairs, {msg[2]} batches") |
| elif msg_type == "tts": |
| log(f"[TTS-GPU{msg[1]}] {msg[2]} items | avg {msg[3]:.1f}/s | batch {msg[4]:.1f}/s") |
| last_heartbeat_time[f"tts_{msg[1]}"] = time.time() |
| elif msg_type == "tts_done": |
| tts_done_count += 1 |
| log(f"[TTS-GPU{msg[1]}] Done: {msg[2]} items") |
| elif msg_type == "feat": |
| log(f"[Feat-GPU{msg[1]}] {msg[2]} items | avg {msg[3]:.1f}/s | batch {msg[4]:.1f}/s") |
| last_heartbeat_time[f"feat_{msg[1]}"] = time.time() |
| elif msg_type == "feat_done": |
| feat_done_count += 1 |
| expected_from_feat[msg[1]] = msg[2] |
| log(f"[Features-GPU{msg[1]}] Done: {msg[2]} items") |
| elif msg_type == "tts_heartbeat": |
| last_heartbeat_time[f"tts_{msg[1]}"] = time.time() |
| elif msg_type == "feat_heartbeat": |
| last_heartbeat_time[f"feat_{msg[1]}"] = time.time() |
| elif msg_type == "feat_warn": |
| log(f"[WARN] Features-GPU{msg[1]}: {msg[2]}") |
| elif "error" in msg_type: |
| log(f"[Error] {msg}") |
| errors.append(msg) |
| except: |
| break |
|
|
| if tts_done_count >= num_tts_workers and not feat_queue_closed: |
| log(f"[Main] TTS done, waiting for features to finish...") |
| feat_queue_closed = True |
|
|
| |
| collected_this_round = 0 |
| drain_start = time.time() |
| while time.time() - drain_start < 2.0: |
| try: |
| batch_idx, items = result_queue.get_nowait() |
| results[batch_idx] = items |
| total_items += len(items) |
| collected_this_round += len(items) |
| last_result_time = time.time() |
| stall_warning_shown = False |
| except: |
| |
| time.sleep(0.05) |
| try: |
| batch_idx, items = result_queue.get_nowait() |
| results[batch_idx] = items |
| total_items += len(items) |
| collected_this_round += len(items) |
| last_result_time = time.time() |
| stall_warning_shown = False |
| except: |
| break |
|
|
| if collected_this_round > 0: |
| elapsed = time.time() - t0 |
| log(f"[Results] {total_items}/{remaining_count} | {total_items/elapsed:.1f}/s") |
|
|
| |
| if total_items - last_checkpoint_count >= args.checkpoint_interval: |
| log(f"[Checkpoint] Saving {total_items} new items...") |
| |
| items_this_run = [] |
| for i in sorted(results.keys()): |
| items_this_run.extend(results[i]) |
|
|
| |
| checkpoint_new_path = checkpoint_path + ".new" |
| all_checkpoint_items = [] |
| if Path(checkpoint_new_path).exists(): |
| try: |
| prev_items = torch.load(checkpoint_new_path, map_location="cpu", weights_only=False) |
| all_checkpoint_items = list(prev_items) |
| del prev_items |
| except: |
| pass |
|
|
| |
| items_to_add = items_this_run[len(all_checkpoint_items):] |
| all_checkpoint_items.extend(items_to_add) |
|
|
| torch.save(all_checkpoint_items, checkpoint_new_path) |
| last_checkpoint_count = total_items |
| log(f"[Checkpoint] Saved {len(all_checkpoint_items)} total to {checkpoint_new_path}") |
|
|
| |
| if total_items >= remaining_count: |
| log(f"[Main] Target reached: {total_items}/{remaining_count}") |
| break |
|
|
| |
| if feat_done_count >= num_feat_workers: |
| log(f"[Main] All workers done, draining queue...") |
| drain_start = time.time() |
| while time.time() - drain_start < 60.0: |
| try: |
| batch_idx, items = result_queue.get(timeout=0.5) |
| results[batch_idx] = items |
| total_items += len(items) |
| log(f"[Results] {total_items}/{remaining_count} | (drained)") |
| except: |
| time.sleep(0.2) |
| try: |
| batch_idx, items = result_queue.get_nowait() |
| results[batch_idx] = items |
| total_items += len(items) |
| log(f"[Results] {total_items}/{remaining_count} | (drained)") |
| except: |
| break |
| break |
|
|
| |
| alive_workers = sum(1 for p in workers if p.is_alive()) |
| time_since_result = time.time() - last_result_time |
| time_since_status = time.time() - last_status_time |
|
|
| |
| stuck_workers = [] |
| for worker_id, last_hb in last_heartbeat_time.items(): |
| if time.time() - last_hb > 120: |
| stuck_workers.append(worker_id) |
|
|
| if time_since_result > 30 and not stall_warning_shown: |
| queue_size = 0 |
| try: |
| queue_size = result_queue.qsize() |
| except: |
| pass |
| log(f"[WARN] No results for 30s, {alive_workers} workers alive, queue ~{queue_size} items") |
| if stuck_workers: |
| log(f"[WARN] Stuck workers (no heartbeat >2min): {stuck_workers}") |
| stall_warning_shown = True |
|
|
| |
| if time_since_result > 60 and total_items > last_checkpoint_count: |
| log(f"[Checkpoint] Stall detected, saving {total_items} items...") |
| items_this_run = [] |
| for i in sorted(results.keys()): |
| items_this_run.extend(results[i]) |
|
|
| |
| checkpoint_new_path = checkpoint_path + ".new" |
| all_checkpoint_items = [] |
| if Path(checkpoint_new_path).exists(): |
| try: |
| prev_items = torch.load(checkpoint_new_path, map_location="cpu", weights_only=False) |
| all_checkpoint_items = list(prev_items) |
| del prev_items |
| except: |
| pass |
|
|
| items_to_add = items_this_run[len(all_checkpoint_items):] |
| all_checkpoint_items.extend(items_to_add) |
| torch.save(all_checkpoint_items, checkpoint_new_path) |
| last_checkpoint_count = total_items |
|
|
| |
| |
| all_workers_stuck = len(stuck_workers) >= (num_tts_workers + num_feat_workers) |
| if time_since_result > 180 and all_workers_stuck: |
| log(f"[WARN] All workers stuck, stopping with {total_items} items") |
| log(f"[WARN] Errors encountered: {len(errors)}") |
| for err in errors[-5:]: |
| log(f" {err}") |
| break |
|
|
| if time_since_result > 600: |
| log(f"[WARN] No progress for 10min, stopping with {total_items} items") |
| break |
|
|
| |
| if total_items > last_checkpoint_count: |
| log(f"[Checkpoint] Final save before cleanup: {total_items} items...") |
| items_this_run = [] |
| for i in sorted(results.keys()): |
| items_this_run.extend(results[i]) |
|
|
| |
| checkpoint_new_path = checkpoint_path + ".new" |
| all_checkpoint_items = [] |
| if Path(checkpoint_new_path).exists(): |
| try: |
| prev_items = torch.load(checkpoint_new_path, map_location="cpu", weights_only=False) |
| all_checkpoint_items = list(prev_items) |
| del prev_items |
| except: |
| pass |
|
|
| items_to_add = items_this_run[len(all_checkpoint_items):] |
| all_checkpoint_items.extend(items_to_add) |
| torch.save(all_checkpoint_items, checkpoint_new_path) |
| last_checkpoint_count = total_items |
|
|
| |
| log("[Main] Waiting for workers to finish...") |
| for p in workers: |
| p.join(timeout=5) |
| if p.is_alive(): |
| p.terminate() |
|
|
| |
| for _ in range(100): |
| try: |
| batch_idx, items = result_queue.get_nowait() |
| results[batch_idx] = items |
| total_items += len(items) |
| except: |
| break |
|
|
| |
| new_items_this_run = [] |
| for i in sorted(results.keys()): |
| new_items_this_run.extend(results[i]) |
|
|
| log(f"[Main] Generated {len(new_items_this_run)} new items this run") |
|
|
| |
| new_checkpoint = checkpoint_path + ".new" |
| new_items = [] |
|
|
| if Path(new_checkpoint).exists(): |
| try: |
| prev_new = torch.load(new_checkpoint, map_location="cpu", weights_only=False) |
| new_items = list(prev_new) |
| log(f"[Main] Loaded {len(new_items)} items from previous checkpoint") |
| del prev_new |
| except Exception as e: |
| log(f"[Main] Failed to load previous checkpoint: {e}") |
|
|
| |
| new_items.extend(new_items_this_run) |
| 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)})") |
|
|
| total_new = len(new_items) |
|
|
| if total_new == 0 and (not resume_from_path or start_count == 0): |
| log("[ERROR] No items collected! Check worker logs.") |
| sys.exit(1) |
|
|
| |
| if resume_from_path and Path(resume_from_path).exists(): |
| log(f"[Main] Loading base items from {resume_from_path}...") |
| base_data = torch.load(resume_from_path, map_location="cpu", weights_only=False, mmap=True) |
| base_count = len(base_data) |
|
|
| |
| items_needed_from_base = min(base_count, args.count - total_new) |
| log(f"[Main] Combining {items_needed_from_base} base + {total_new} new items...") |
|
|
| final_items = list(base_data[:items_needed_from_base]) + new_items |
| del base_data |
| else: |
| final_items = new_items |
|
|
| |
| final_items = final_items[:args.count] |
|
|
| |
| log(f"[Main] Saving {len(final_items)} items to {args.output}...") |
| torch.save(final_items, args.output) |
|
|
| |
| if len(final_items) >= args.count: |
| for cp in [checkpoint_path, checkpoint_path + ".new"]: |
| if Path(cp).exists(): |
| Path(cp).unlink() |
| log(f"[Cleanup] Removed {cp}") |
|
|
| total_time = time.time() - total_start |
| log("\n" + "=" * 60) |
| log(f"COMPLETE: {len(final_items)} items saved to {args.output}") |
| if start_count > 0: |
| log(f" (resumed from {start_count}, added {len(final_items) - start_count} new)") |
| log(f"Total time: {total_time:.1f}s ({total_time/60:.1f}m)") |
| log(f"Throughput: {(len(final_items) - start_count)/total_time:.2f} items/s") |
| log("=" * 60) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|