From 0466bd271ba4756290c2265488aae97b2b27f68b Mon Sep 17 00:00:00 2001 From: Wallbreazzz <110282866+Wallbreazzz@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:27:46 +0800 Subject: [PATCH] Add single-NPU Qwen3-TTS LoRA training cookbook --- README.md | 1 + cookbook/qwen3_tts/.gitignore | 3 + cookbook/qwen3_tts/README.md | 162 +++++++++++ cookbook/qwen3_tts/requirements.txt | 8 + cookbook/qwen3_tts/scripts/audio_features.py | 42 +++ .../scripts/prepare_split_manifest.py | 128 ++++++++ .../qwen3_tts/scripts/train_qwen3_tts_lora.py | 152 ++++++++++ .../tests/batch1/isolated_support.py | 46 +++ .../qwen3_tts/tests/batch1/test_contracts.py | 273 ++++++++++++++++++ .../tests/entrypoint/test_contracts.py | 110 +++++++ .../tests/entrypoint/test_preparation.py | 88 ++++++ .../entrypoint/test_prepared_training_npu.py | 69 +++++ .../entrypoint/test_process_resume_npu.py | 111 +++++++ .../librispeech/1272-128104-0000.flac | Bin 0 -> 120041 bytes .../tests/fixtures/librispeech/ATTRIBUTION.md | 19 ++ .../tests/fixtures/librispeech/sample.json | 15 + .../librispeech/second/1272-128104-0001.flac | Bin 0 -> 101672 bytes .../fixtures/librispeech/second/sample.json | 15 + .../qwen3_tts/tests/lora/test_contracts.py | 127 ++++++++ .../qwen3_tts/tests/lora/test_real_model.py | 181 ++++++++++++ cookbook/qwen3_tts/tests/run.py | 24 ++ .../tests/training/test_contracts.py | 122 ++++++++ .../tests/training/test_resume_npu.py | 177 ++++++++++++ cookbook/qwen3_tts/tts/__init__.py | 1 + cookbook/qwen3_tts/tts/common/__init__.py | 1 + .../tts/common/multi_codebook_loss.py | 31 ++ cookbook/qwen3_tts/tts/qwen3/__init__.py | 1 + cookbook/qwen3_tts/tts/qwen3/data.py | 190 ++++++++++++ cookbook/qwen3_tts/tts/qwen3/forward.py | 88 ++++++ .../qwen3_tts/tts/qwen3/input_processor.py | 171 +++++++++++ cookbook/qwen3_tts/tts/qwen3/loss.py | 74 +++++ cookbook/qwen3_tts/tts/qwen3/manifest.py | 71 +++++ cookbook/qwen3_tts/tts/qwen3/model.py | 80 +++++ cookbook/qwen3_tts/tts/qwen3/runtime.py | 92 ++++++ .../qwen3_tts/tts/qwen3/speech_fixture.py | 23 ++ cookbook/qwen3_tts/tts/qwen3/tokenization.py | 21 ++ cookbook/qwen3_tts/tts/qwen3/training.py | 112 +++++++ 37 files changed, 2829 insertions(+) create mode 100644 cookbook/qwen3_tts/.gitignore create mode 100644 cookbook/qwen3_tts/README.md create mode 100644 cookbook/qwen3_tts/requirements.txt create mode 100644 cookbook/qwen3_tts/scripts/audio_features.py create mode 100644 cookbook/qwen3_tts/scripts/prepare_split_manifest.py create mode 100644 cookbook/qwen3_tts/scripts/train_qwen3_tts_lora.py create mode 100644 cookbook/qwen3_tts/tests/batch1/isolated_support.py create mode 100644 cookbook/qwen3_tts/tests/batch1/test_contracts.py create mode 100644 cookbook/qwen3_tts/tests/entrypoint/test_contracts.py create mode 100644 cookbook/qwen3_tts/tests/entrypoint/test_preparation.py create mode 100644 cookbook/qwen3_tts/tests/entrypoint/test_prepared_training_npu.py create mode 100644 cookbook/qwen3_tts/tests/entrypoint/test_process_resume_npu.py create mode 100644 cookbook/qwen3_tts/tests/fixtures/librispeech/1272-128104-0000.flac create mode 100644 cookbook/qwen3_tts/tests/fixtures/librispeech/ATTRIBUTION.md create mode 100644 cookbook/qwen3_tts/tests/fixtures/librispeech/sample.json create mode 100644 cookbook/qwen3_tts/tests/fixtures/librispeech/second/1272-128104-0001.flac create mode 100644 cookbook/qwen3_tts/tests/fixtures/librispeech/second/sample.json create mode 100644 cookbook/qwen3_tts/tests/lora/test_contracts.py create mode 100644 cookbook/qwen3_tts/tests/lora/test_real_model.py create mode 100644 cookbook/qwen3_tts/tests/run.py create mode 100644 cookbook/qwen3_tts/tests/training/test_contracts.py create mode 100644 cookbook/qwen3_tts/tests/training/test_resume_npu.py create mode 100644 cookbook/qwen3_tts/tts/__init__.py create mode 100644 cookbook/qwen3_tts/tts/common/__init__.py create mode 100644 cookbook/qwen3_tts/tts/common/multi_codebook_loss.py create mode 100644 cookbook/qwen3_tts/tts/qwen3/__init__.py create mode 100644 cookbook/qwen3_tts/tts/qwen3/data.py create mode 100644 cookbook/qwen3_tts/tts/qwen3/forward.py create mode 100644 cookbook/qwen3_tts/tts/qwen3/input_processor.py create mode 100644 cookbook/qwen3_tts/tts/qwen3/loss.py create mode 100644 cookbook/qwen3_tts/tts/qwen3/manifest.py create mode 100644 cookbook/qwen3_tts/tts/qwen3/model.py create mode 100644 cookbook/qwen3_tts/tts/qwen3/runtime.py create mode 100644 cookbook/qwen3_tts/tts/qwen3/speech_fixture.py create mode 100644 cookbook/qwen3_tts/tts/qwen3/tokenization.py create mode 100644 cookbook/qwen3_tts/tts/qwen3/training.py diff --git a/README.md b/README.md index 6f4b05a03..2bc24ab56 100644 --- a/README.md +++ b/README.md @@ -405,6 +405,7 @@ foundation for building customizable, enterprise-grade training services. | Component Type | Component Link | Component Function | Author | | -------------- | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------- | +| Cookbook | [Qwen3-TTS LoRA](cookbook/qwen3_tts/README.md) | Single Ascend NPU training, explicit data splits and update-boundary resume | [Wallbreazzz](https://github.com/Wallbreazzz) | | Patch | [qwen3_moe_transformers4_patch](https://www.modelscope.cn/models/twinkle-kit/qwen3_moe_transformers4_patch) | Fixes Qwen3 MoE model hang issue during FSDP2 training, effective for transformers==4.x | ModelScope Official | ## Contributions diff --git a/cookbook/qwen3_tts/.gitignore b/cookbook/qwen3_tts/.gitignore new file mode 100644 index 000000000..76782217e --- /dev/null +++ b/cookbook/qwen3_tts/.gitignore @@ -0,0 +1,3 @@ +outputs/ +__pycache__/ +*.py[cod] diff --git a/cookbook/qwen3_tts/README.md b/cookbook/qwen3_tts/README.md new file mode 100644 index 000000000..353c6bff1 --- /dev/null +++ b/cookbook/qwen3_tts/README.md @@ -0,0 +1,162 @@ +# Qwen3-TTS LoRA training on one Ascend NPU + +This cookbook connects the Qwen3-TTS 12Hz Base model to Twinkle's local +Transformers training API. It provides explicit train/dev preparation, +single-sample micro-batches, LoRA on the Talker and Code Predictor, accumulation +with a final short window, and restart at completed optimizer updates. + +It is a single-card reference implementation, not a quality-tuned training +recipe. There are no changes to Twinkle core or its default dependencies. +Generation, speaker-similarity evaluation and experimental EOS handling are +outside this contribution. + +## Installation and supported scope + +Run from the repository root with an existing Ascend PyTorch environment: + +```bash +pip install -e . +pip install -r cookbook/qwen3_tts/requirements.txt +cd cookbook/qwen3_tts +export MODEL_PATH=/models/Qwen3-TTS-12Hz-1.7B-Base +export ASCEND_RT_VISIBLE_DEVICES=0 +``` + +The model directory must already contain the complete Base weights, config, +text tokenizer, processor and `speech_tokenizer/`. Preparation and training are +offline: missing assets cause an error, not a download. No virtual environment +or model download is created by these commands. Select one physical card with +`ASCEND_RT_VISIBLE_DEVICES`; `npu:0` is relative to that selection. + +The development baseline used Twinkle `5337cc3`, Qwen3-TTS `022e286`, Python +3.12, torch 2.10.0, torch_npu 2.10.0.post2, CANN 9.1.0 and an Ascend 910B. +The optional requirements pin the Qwen implementation and its Transformers +4.57.3 / accelerate 1.12.0 dependencies. They are not installed for ordinary +Twinkle users. Match torch/torch_npu to your Ascend image before installing +these extras. Codec and reference mel extraction use CPU fp32; the TTS model +uses NPU bf16 with eager attention. Multi-card, CUDA, padding-free execution +and other Qwen3-TTS model variants are not covered by the current evidence. + +## Prepare an existing split + +Provide two UTF-8 JSONL files. Each row has `id`, `text`, `audio`, `group_id` +and `speaker_id`. Paths are relative to the input JSONL file. For example: + +```json +{"id":"train-001","text":"A training utterance.","audio":"audio/train-001.wav","group_id":"session-a","speaker_id":"speaker-a"} +``` + +Use a different held-out group and text in the dev file. Audio must be finite +mono 16 or 24 kHz; 16 kHz is resampled to 24 kHz. The preparer requires one +speaker, unique IDs, nonempty train/dev sets and a reference chosen from train. +It rejects cross-split group, normalized text, file, decoded PCM and resampled +PCM duplicates. Group identifiers still need to reflect the real recording +source; these checks do not detect all near-duplicates. + +```bash +python scripts/prepare_split_manifest.py \ + --model_path "$MODEL_PATH" \ + --train_jsonl /data/splits/train.jsonl \ + --dev_jsonl /data/splits/dev.jsonl \ + --reference_id train-001 \ + --output_dir /data/derived/qwen3-tts +``` + +The output directory must be new. The script preserves the assigned split, +text and order; it never silently resplits or edits source audio. Output rows +contain `id`, `text`, integer `audio_codes` of shape `[T,16]` in `[0,2048)`, +and `ref_mel_path`. One train-reference mel `[F,128]` is shared by both splits. +`provenance.json` records input/output hashes and the source mapping. +The dev manifest is prepared for downstream evaluation and is **not** consumed +by this training loop. No dev loss, early stopping or quality score is implied. + +## Train and resume + +```bash +python scripts/train_qwen3_tts_lora.py \ + --model_path "$MODEL_PATH" \ + --train_jsonl /data/derived/qwen3-tts/train.jsonl \ + --output_dir outputs/run-1 \ + --epochs 3 --grad_accum 4 --shuffle --seed 42 \ + --lr 1e-4 --lr_decay 1.0 --lora_r 8 --lora_dropout 0.1 \ + --save_interval 10 +``` + +These are example hyperparameters, not a convergence recommendation. The +default learning rate stays constant. `--lr_decay` optionally supplies StepLR +gamma per optimizer update; the short resume regression explicitly uses 0.8 +to exercise scheduler restoration. LoRA targets `q_proj`/`v_proj` in both +branches, with alpha equal to twice the rank. All base parameters are frozen. + +Micro-batch size is one. The loop owns accumulation windows and keeps +Twinkle's group accumulation at one; `clip_grad_and_step` normalizes by the +actual number of micro-batches, including an epoch's short final window. +The speaker encoder runs without gradients. The Talker loss uses its internal +temporal shift; residual-codebook logits are supervised against corresponding +codebooks 1–15 without another shift along the codebook axis. Tests check +last-head gradients and the official residual assembly interface. This loss +choice is not a claim about how official pretrained weights were trained. + +For a controlled stop, add `--stop_after_updates 1`. Resume to a **new** output +directory with the same training options: + +```bash +python scripts/train_qwen3_tts_lora.py \ + --model_path "$MODEL_PATH" \ + --train_jsonl /data/derived/qwen3-tts/train.jsonl \ + --output_dir outputs/run-2 --resume_from outputs/run-1/checkpoint-update-1 \ + --epochs 3 --grad_accum 4 --shuffle --seed 42 \ + --lr 1e-4 --lr_decay 1.0 --lora_r 8 --lora_dropout 0.1 \ + --save_interval 10 +``` + +Checkpoints include adapter, optimizer, scheduler, RNG and loop progress. +Resume checks content fingerprints of model/manifest/reference mel, sample +plan and settings; it fails on mismatch instead of silently starting over. +Model hashing reads the local weight files and may take time. Only complete, +trusted checkpoints produced by this workflow should be resumed. Saving is +at update boundaries; recovery from arbitrary interruption during a checkpoint +write is not guaranteed. Inspect `trace.jsonl`, `run_config.json` and +`result.json` for the actual update count and checkpoint path. + +## Tests and evidence boundaries + +With the optional dependencies installed: + +```bash +python tests/run.py +MODEL_PATH="$MODEL_PATH" python tests/run.py --npu +``` + +The first command runs data, forward/loss, projection, manifest, preparation +and accumulation contracts. It includes official mel/residual-interface tests +but uses small CPU networks, not the pretrained TTS model. The second also +loads the real local model for a LoRA update/save/reload, public split +preparation-to-training, and continuous versus independent-process resume. +The two tiny LibriSpeech fixtures are attributed under +`tests/fixtures/librispeech/`; they are regression fixtures, not an independent +quality benchmark. Tests use temporary outputs and never require user data. + +For local CPU development without Twinkle or Qwen installed, install torch, +Transformers, NumPy, scipy and soundfile, then run `python tests/run.py +--isolated`. This mode explicitly substitutes Twinkle infrastructure and skips +two official-package checks. It is not NPU or native Twinkle validation. + +Before upstream packaging, real 1.7B single-NPU runs covered nonzero LoRA +gradients/updates, unchanged frozen parameters, exact saved/reloaded adapter +weights, accumulation tails and restart in separate processes. Final adapter +weights matched exactly in the resume comparison; optimizer states were +compared with rtol=1e-3/atol=1e-5. Those results do not establish long-training +stability, speaker similarity or fine-tuning quality. The reorganized PR tree +requires its own final NPU regression before the draft is marked ready. + +## Integration boundaries + +The local `tts/` package is added to the script path; it is not registered as +a new global Twinkle task. The subclass reuses forward/backward, optimizer, +clipping, saving and resume APIs. It currently overrides +`_construct_default_optimizer_group` for consistent tokenizer/processor/loss +defaults and calls `_lazy_wrap_model` when inspecting/loading wrapped weights. +These internal dependencies are isolated here and should be reviewed when +Twinkle changes. A public generic TTS abstraction should only be added if +maintainers identify a reusable need; this cookbook does not add one prematurely. diff --git a/cookbook/qwen3_tts/requirements.txt b/cookbook/qwen3_tts/requirements.txt new file mode 100644 index 000000000..50874f350 --- /dev/null +++ b/cookbook/qwen3_tts/requirements.txt @@ -0,0 +1,8 @@ +accelerate==1.12.0 +numpy>=2.0,<2.3 +peft==0.17.1 +qwen-tts @ git+https://github.com/QwenLM/Qwen3-TTS.git@022e286b98fbec7e1e916cb940cdf532cd9f488e +safetensors +scipy +soundfile +transformers==4.57.3 diff --git a/cookbook/qwen3_tts/scripts/audio_features.py b/cookbook/qwen3_tts/scripts/audio_features.py new file mode 100644 index 000000000..cf955bd9b --- /dev/null +++ b/cookbook/qwen3_tts/scripts/audio_features.py @@ -0,0 +1,42 @@ +"""Official 24 kHz mel extraction and 16-codebook audio encoding.""" + +import numpy as np + +MEL_DIM = 128 +MEL_HOP = 256 +MEL_WIN = 1024 +MEL_FMAX = 12000.0 +TARGET_SR = 24000 + +def extract_mel(wav_24k: np.ndarray) -> np.ndarray: + """使用官方 mel 算法,返回单条 [F,128],不使用替代 fbank。""" + import torch + from qwen_tts.core.models.modeling_qwen3_tts import mel_spectrogram + + wav = np.asarray(wav_24k, dtype=np.float32) + if wav.ndim != 1: + raise ValueError('Expected mono waveform [samples]') + with torch.no_grad(): + mel = mel_spectrogram( + torch.from_numpy(wav).unsqueeze(0), n_fft=MEL_WIN, + num_mels=MEL_DIM, sampling_rate=TARGET_SR, hop_size=MEL_HOP, + win_size=MEL_WIN, fmin=0, fmax=MEL_FMAX, + ).transpose(1, 2) + return mel[0].cpu().numpy() + +def encode_audio_codes(wav_24k: np.ndarray, tokenizer) -> np.ndarray: + """单条输入必须返回一个 [T,16];兼容列表、二维张量和单批张量。""" + result = tokenizer.encode(wav_24k, sr=TARGET_SR) # wav_24k 已升采样到 24kHz + codes = result.audio_codes + if isinstance(codes, (list, tuple)): + if len(codes) != 1: + raise ValueError('Single waveform must return exactly one audio_codes entry') + codes = codes[0] + if hasattr(codes, 'cpu'): + codes = codes.cpu().numpy() + codes = np.asarray(codes) + if codes.ndim == 3 and codes.shape[0] == 1: + codes = codes[0] + if codes.ndim != 2 or codes.shape[0] == 0 or codes.shape[1] != 16: + raise ValueError(f'Expected nonempty audio_codes [T,16], got {codes.shape}') + return codes.astype(np.int64) diff --git a/cookbook/qwen3_tts/scripts/prepare_split_manifest.py b/cookbook/qwen3_tts/scripts/prepare_split_manifest.py new file mode 100644 index 000000000..50b039c24 --- /dev/null +++ b/cookbook/qwen3_tts/scripts/prepare_split_manifest.py @@ -0,0 +1,128 @@ +"""Preserve an explicitly assigned single-speaker train/dev split; never resplit.""" +import argparse +import hashlib +import json +import math +import os +from pathlib import Path +import unicodedata + +import numpy as np + + +def sha(data): + return hashlib.sha256(data).hexdigest() + + +def read_audio(path): + import soundfile as sf + from scipy.signal import resample_poly + wave, sr = sf.read(path, dtype='float32') + if sr not in (16000, 24000) or wave.ndim != 1 or not len(wave) or not np.isfinite(wave).all(): + raise ValueError('Expected finite mono 16/24 kHz audio') + original = sha(np.asarray(wave, dtype='= 2048).any()): + raise ValueError('Expected integer 12Hz codes [T,16] in [0,2048)') + encoded.append(dict(id=row['id'], text=row['text'], audio_codes=codes.tolist(), + ref_mel_path='reference.npy')) + mapping.append(dict(row, split=split, reference_id=reference_id, + output_manifest=f'{split}.jsonl')) + (out / f'{split}.jsonl').write_text(''.join(json.dumps(r, ensure_ascii=False) + '\n' for r in encoded), encoding='utf-8') + report = dict(format_version=1, counts={k: len(v) for k, v in splits.items()}, + reference_id=reference_id, reference_split='train', mapping=mapping, + source_manifest_sha256={k: sha(Path(p).read_bytes()) for k, p in + [('train', train_path), ('dev', dev_path)]}, + output_sha256={n: sha((out / n).read_bytes()) for n in + ('train.jsonl', 'dev.jsonl', 'reference.npy')}) + (out / 'provenance.json').write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding='utf-8') + return report + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ('train_jsonl', 'dev_jsonl', 'output_dir', 'reference_id', 'model_path'): + parser.add_argument('--' + name, required=True) + args = parser.parse_args() + # Audit before loading weights or creating output. No automatic split/filter fallback. + audit_split(args.train_jsonl, args.dev_jsonl, args.reference_id) + os.environ['HF_HUB_OFFLINE'] = '1' + os.environ['TRANSFORMERS_OFFLINE'] = '1' + import torch + from qwen_tts.core.models.modeling_qwen3_tts import Qwen3TTSTokenizer + from audio_features import encode_audio_codes, extract_mel + codec = Qwen3TTSTokenizer.from_pretrained(str(Path(args.model_path) / 'speech_tokenizer'), + local_files_only=True, device_map='cpu', torch_dtype=torch.float32) + codec.model.float().eval() + with torch.no_grad(): + report = prepare(args.train_jsonl, args.dev_jsonl, args.output_dir, args.reference_id, + lambda wave: encode_audio_codes(wave, codec), extract_mel) + from train_qwen3_tts_lora import model_fingerprint + report['codec_content_sha256'] = model_fingerprint(Path(args.model_path) / 'speech_tokenizer') + (Path(args.output_dir) / 'provenance.json').write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding='utf-8') + print('准备完成 / Preparation completed: ' + json.dumps(report['counts']), flush=True) + + +if __name__ == '__main__': + main() diff --git a/cookbook/qwen3_tts/scripts/train_qwen3_tts_lora.py b/cookbook/qwen3_tts/scripts/train_qwen3_tts_lora.py new file mode 100644 index 000000000..6b955167c --- /dev/null +++ b/cookbook/qwen3_tts/scripts/train_qwen3_tts_lora.py @@ -0,0 +1,152 @@ +"""Manifest-based single-NPU bf16 LoRA training with boundary-only resume. + +Each JSONL row: id, text, audio_codes [T,16], ref_mel_path [F,128] .npy. +Output directory must be new; resume writes to a new run directory. +""" +import argparse +import hashlib +import json +import math +import os +from pathlib import Path +import sys + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + + +def model_fingerprint(path): + digest = hashlib.sha256() + files = sorted(p for p in path.rglob('*') if p.is_file() and p.suffix in ('.safetensors', '.json', '.txt')) + if not any(p.suffix == '.safetensors' for p in files): + raise ValueError('Local model must contain safetensors weights') + for file in files: + digest.update(file.relative_to(path).as_posix().encode()) + with file.open('rb') as source: + for block in iter(lambda: source.read(1024 * 1024), b''): + digest.update(block) + return digest.hexdigest() + + +def parse_args(argv=None): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument('--model_path', required=True) + ap.add_argument('--train_jsonl', required=True) + ap.add_argument('--output_dir', required=True) + ap.add_argument('--resume_from') + ap.add_argument('--epochs', type=int, default=1) + ap.add_argument('--grad_accum', type=int, default=2) + ap.add_argument('--seed', type=int, default=42) + ap.add_argument('--shuffle', action='store_true') + ap.add_argument('--lr', type=float, default=1e-4) + ap.add_argument('--lr_decay', type=float, default=1.0, help='StepLR gamma per optimizer update; default keeps LR constant') + ap.add_argument('--lora_r', type=int, default=8) + ap.add_argument('--lora_dropout', type=float, default=0.1) + ap.add_argument('--save_interval', type=int, default=1, help='Optimizer updates between checkpoints') + ap.add_argument('--stop_after_updates', type=int, default=0, help='Absolute update limit; 0 runs all epochs') + args = ap.parse_args(argv) + if min(args.epochs, args.grad_accum, args.lora_r, args.save_interval) < 1 or args.stop_after_updates < 0: + ap.error('Counts must be positive (stop_after_updates may be 0)') + if not math.isfinite(args.lr_decay) or not 0 < args.lr_decay <= 1: + ap.error('lr_decay must be in (0, 1]') + if not math.isfinite(args.lr) or not args.lr > 0 or not 0 <= args.lora_dropout < 1: + ap.error('lr must be positive and dropout in [0,1)') + return args + + +def train(): + args = parse_args() + os.environ['HF_HUB_OFFLINE'] = '1' + os.environ['TRANSFORMERS_OFFLINE'] = '1' + import random + import numpy as np + import torch + import torch_npu # noqa: F401 + import twinkle + from twinkle import DeviceMesh + from peft import LoraConfig + from tts.qwen3.manifest import Manifest, sample_plan, planned_windows, validate_cursor + from tts.qwen3.runtime import load_twinkle_model, data_config, ADAPTER + from tts.qwen3.tokenization import load_text_tokenizer + from tts.qwen3.data import build_assistant_text, build_qwen3_sample, collate_qwen3_batch + from tts.qwen3.input_processor import Qwen3TTSInputProcessor + from tts.qwen3.loss import Qwen3TTSLoss + from tts.qwen3.training import Progress, train_window, save_boundary, resume_boundary + + manifest = Manifest(args.train_jsonl) + plan = sample_plan(len(manifest), args.epochs, args.seed, args.shuffle) + schedule = planned_windows(plan, args.grad_accum) + path = Path(args.model_path).resolve(strict=True) + print('计算模型内容指纹 / Hashing local model files', flush=True) + config = dict(format_version=1, manifest=manifest.fingerprint, model=model_fingerprint(path), + plan=plan, epochs=args.epochs, accumulation_steps=args.grad_accum, seed=args.seed, + shuffle=args.shuffle, lr=args.lr, lora_r=args.lora_r, lora_dropout=args.lora_dropout, + targets=['q_proj', 'v_proj'], scheduler=f'StepLR(1,{args.lr_decay})', tokenizer_fix=True) + fingerprint = hashlib.sha256(json.dumps(config, sort_keys=True).encode()).hexdigest() + out = Path(args.output_dir).resolve() + out.mkdir(parents=True, exist_ok=False) + (out / 'run_config.json').write_text(json.dumps(config, indent=2), encoding='utf-8') + torch.npu.set_device(0) + device_mesh = DeviceMesh.from_sizes(fsdp_size=1, dp_size=1) + twinkle.initialize(mode='local', global_device_mesh=device_mesh) + random.seed(args.seed) + np.random.seed(args.seed) + torch.manual_seed(args.seed) + torch.npu.manual_seed(args.seed) + model = load_twinkle_model(path, device_mesh) + model.model.speech_tokenizer = None # manifest is already codec-encoded + model.add_adapter_to_model(ADAPTER, LoraConfig(r=args.lora_r, lora_alpha=2 * args.lora_r, + target_modules=['q_proj', 'v_proj'], lora_dropout=args.lora_dropout, bias='none'), + gradient_accumulation_steps=1) + model.set_processor(Qwen3TTSInputProcessor, adapter_name=ADAPTER) + model.set_loss(Qwen3TTSLoss(), adapter_name=ADAPTER) + model.set_optimizer('AdamW', lr=args.lr, weight_decay=0.0, adapter_name=ADAPTER) + model.set_lr_scheduler('StepLR', step_size=1, gamma=args.lr_decay, adapter_name=ADAPTER) + model._lazy_wrap_model() + cfg = data_config(model.hf_config) + tokenizer = load_text_tokenizer(path) + suffix = tokenizer.encode('<|im_end|>\n<|im_start|>assistant\n', add_special_tokens=False) + + def batch(index): + row = manifest.rows[index] + ids = tokenizer.encode(build_assistant_text(row['text'])) + if not suffix or ids[-len(suffix):] != suffix: + raise ValueError('Unexpected assistant suffix') + sample = build_qwen3_sample(np.asarray(ids[:-len(suffix)]), np.asarray(row['audio_codes']), cfg) + mel = np.load(row['ref_mel_path'], allow_pickle=False) + raw = collate_qwen3_batch([sample], cfg, [mel]) + return model.optimizer_group[ADAPTER].processor(raw, model=model.strategy.unwrap_model(model.model)) + + progress = Progress(args.grad_accum) + if args.resume_from: + progress = resume_boundary(model, args.resume_from, ADAPTER, fingerprint, args.grad_accum) + validate_cursor(progress, schedule) + final_checkpoint = None + trace = out / 'trace.jsonl' + with trace.open('w', encoding='utf-8') as log: + for indices in schedule[progress.optimizer_steps:]: + if args.stop_after_updates and progress.optimizer_steps >= args.stop_after_updates: + break + losses = train_window(model, [batch(i) for i in indices], progress, ADAPTER) + record = dict(micro_steps=progress.micro_steps, optimizer_steps=progress.optimizer_steps, + consumed_samples=progress.consumed_samples, sample_ids=[manifest.rows[i]['id'] for i in indices], + losses=losses, learning_rates=model.optimizer_group[ADAPTER].lr_scheduler.get_last_lr()) + log.write(json.dumps(record, ensure_ascii=False) + '\n') + log.flush() + print(json.dumps(record, ensure_ascii=False), flush=True) + if progress.optimizer_steps % args.save_interval == 0: + final_checkpoint = save_boundary(model, progress, out, + f'checkpoint-update-{progress.optimizer_steps}', ADAPTER, fingerprint) + if progress.optimizer_steps and (final_checkpoint is None or + final_checkpoint.name != f'checkpoint-update-{progress.optimizer_steps}'): + final_checkpoint = save_boundary(model, progress, out, + f'checkpoint-update-{progress.optimizer_steps}', ADAPTER, fingerprint) + (out / 'result.json').write_text(json.dumps(dict(pid=os.getpid(), checkpoint=str(final_checkpoint) if final_checkpoint else None, + micro_steps=progress.micro_steps, optimizer_steps=progress.optimizer_steps, + consumed_samples=progress.consumed_samples, complete=progress.optimizer_steps == len(schedule)), + indent=2), encoding='utf-8') + print('本次运行完成 / Run completed', flush=True) + + +if __name__ == '__main__': + train() diff --git a/cookbook/qwen3_tts/tests/batch1/isolated_support.py b/cookbook/qwen3_tts/tests/batch1/isolated_support.py new file mode 100644 index 000000000..adf1a8c68 --- /dev/null +++ b/cookbook/qwen3_tts/tests/batch1/isolated_support.py @@ -0,0 +1,46 @@ +"""Explicit CPU test doubles for Twinkle infrastructure only. + +Production tensor operations and transformers loss remain real. This is NOT +Twinkle/NPU integration evidence. Only loaded with --isolated-twinkle. +""" +import sys +import types +from pathlib import Path +import torch + + +def install(root): + def module(name, **attrs): + obj = types.ModuleType(name) + obj.__dict__.update(attrs) + sys.modules[name] = obj + return obj + + for name, path in [('tts', 'tts'), ('tts.common', 'tts/common'), ('tts.qwen3', 'tts/qwen3')]: + module(name, __path__=[str(root / path)]) + + class Processor: + padding_map = {} + VLM_CONCAT_FIELDS = set() + def __init__(self, **kwargs): + self.padding_free = kwargs.get('padding_free', False) + + def pad_stack(values, pad_value, concat): + assert concat is False + return torch.nn.utils.rnn.pad_sequence(values, batch_first=True, padding_value=pad_value) + + module('twinkle', remote_function=lambda **kw: lambda fn: fn, + Platform=types.SimpleNamespace(get_local_device=lambda: 'cpu')) + module('twinkle.data_format', InputFeature=dict, Trajectory=dict, LossOutput=dict) + module('twinkle.data_format.output', LossOutput=dict) + module('twinkle.processor') + module('twinkle.processor.base', InputProcessor=Processor) + module('twinkle.utils') + module('twinkle.utils.torch_utils', pad_and_stack_tensors=pad_stack) + module('twinkle.loss') + module('twinkle.loss.base', Loss=object) + module('twinkle.infra', collect_tensor_dict=lambda x: x) + module('twinkle.model') + module('twinkle.model.transformers', TransformersModel=object) + from tts.common.multi_codebook_loss import MultiCodebookLoss + sys.modules['tts.common'].MultiCodebookLoss = MultiCodebookLoss diff --git a/cookbook/qwen3_tts/tests/batch1/test_contracts.py b/cookbook/qwen3_tts/tests/batch1/test_contracts.py new file mode 100644 index 000000000..8f9076233 --- /dev/null +++ b/cookbook/qwen3_tts/tests/batch1/test_contracts.py @@ -0,0 +1,273 @@ +"""Run: python tests/batch1/test_contracts.py [--isolated-twinkle]. + +Default requires installed Twinkle + Qwen3-TTS + transformers. Isolated mode +substitutes only Twinkle infrastructure, and skips official mel parity. +All tensors, autograd and ForCausalLMLoss use real torch/transformers. +""" +import ast +import importlib.util +import inspect +import sys +import types +import unittest +from pathlib import Path +from unittest.mock import patch + +import numpy as np +import torch +from torch import nn +from transformers.loss.loss_utils import ForCausalLMLoss + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +ISOLATED = '--isolated-twinkle' in sys.argv +if ISOLATED: + sys.argv.remove('--isolated-twinkle') + from isolated_support import install + install(ROOT) + +from tts.qwen3.data import Qwen3TTSDataConfig, build_qwen3_sample, collate_qwen3_batch +from tts.qwen3.input_processor import Qwen3TTSInputProcessor +from tts.qwen3.forward import qwen3_tts_forward_step +from tts.qwen3.loss import Qwen3TTSLoss +from tts.qwen3.model import Qwen3TTSModel + +spec = importlib.util.spec_from_file_location('prepare', ROOT / 'scripts/audio_features.py') +prepare = importlib.util.module_from_spec(spec) +spec.loader.exec_module(prepare) + + +def samples(n=2): + cfg = Qwen3TTSDataConfig( + tts_pad_token_id=0, tts_bos_token_id=1, tts_eos_token_id=2, + codec_pad_id=3, codec_bos_id=4, codec_eos_token_id=5, + codec_nothink_id=6, codec_think_bos_id=7, codec_think_eos_id=8) + rows = [build_qwen3_sample(np.arange(10, 15-i), + np.arange((3-i)*16).reshape(3-i, 16) % 20 + 9, cfg) for i in range(n)] + mels = [np.full((10, 128), i+1, dtype=np.float32) for i in range(n)] + return cfg, rows, mels + + +class Talker(nn.Module): + """Small differentiable double of Qwen's two-stage API, with real HF loss.""" + def __init__(self): + super().__init__() + self.config = types.SimpleNamespace(num_code_groups=16) + self.model = nn.Module() + self.model.text_embedding = nn.Embedding(64, 8) + self.text_projection = nn.Identity() + self.model.codec_embedding = nn.Embedding(64, 8) + self.code_predictor = nn.Module() + self.code_predictor.embeddings = nn.ModuleList([nn.Embedding(64, 8) for _ in range(15)]) + self.code_predictor.get_input_embeddings = lambda: self.code_predictor.embeddings + self.head = nn.Linear(8, 64) + self.heads = nn.ModuleList([nn.Linear(8, 64) for _ in range(15)]) + + def forward(self, inputs_embeds, attention_mask, labels, **kwargs): + self.labels = labels + self.hidden = inputs_embeds + self.logits = self.head(inputs_embeds) + self.logits.retain_grad() + return types.SimpleNamespace( + loss=ForCausalLMLoss(self.logits, labels, 64), + hidden_states=((inputs_embeds,), None)) + + def forward_sub_talker_finetune(self, codes, hidden): + self.selected_hidden = hidden + self.selected_codes = codes + logits = torch.stack([head(hidden) for head in self.heads], dim=1) + logits.retain_grad() + self.sub_logits = logits + return logits, ForCausalLMLoss(logits, codes[:, 1:], 64) + + +class Model(nn.Module): + dtype = torch.float32 + device = torch.device('cpu') + def __init__(self): + super().__init__() + self.talker = Talker() + self.speaker_encoder = lambda mels: mels.mean(1)[:, :8] + + +class Contracts(unittest.TestCase): + def test_initialize_accepts_local_mode(self): + tree = ast.parse((ROOT / 'scripts/train_qwen3_tts_lora.py').read_text(encoding='utf-8')) + calls = [node for node in ast.walk(tree) if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) and node.func.attr == 'initialize'] + self.assertEqual(len(calls), 1) + seen = [] + def initialize(mode, global_device_mesh): + self.assertIn(mode, ('local', 'ray')) # pinned Twinkle's accepted contract + self.assertEqual(mode, 'local') + seen.append(global_device_mesh) + eval(compile(ast.Expression(calls[0]), '', 'eval'), + {'twinkle': types.SimpleNamespace(initialize=initialize), 'device_mesh': 'single'}) + self.assertEqual(seen, ['single']) + + def test_codec_single_list_tensor_and_batch_tensor(self): + expected = np.arange(48).reshape(3, 16) + for value in ([torch.tensor(expected)], [expected], torch.tensor(expected), + torch.tensor(expected)[None], expected): + tokenizer = types.SimpleNamespace(encode=lambda *a, **kw: types.SimpleNamespace(audio_codes=value)) + np.testing.assert_array_equal(prepare.encode_audio_codes(np.ones(4096), tokenizer), expected) + + def test_codec_rejects_empty_multiple_and_wrong_shapes(self): + for value in ([], [np.zeros((2,16))]*2, np.zeros((2,3,16)), np.zeros((2,15)), np.zeros((0,16))): + with self.subTest(shape=str(type(value))), self.assertRaises(ValueError): + tokenizer = types.SimpleNamespace(encode=lambda *a, **kw: types.SimpleNamespace(audio_codes=value)) + prepare.encode_audio_codes(np.ones(4096), tokenizer) + + def test_numpy_mel_batch_identity_and_singleton(self): + for n in (1,2): + cfg, rows, mels = samples(n) + for refs in (mels, [m[None] for m in mels]): + batch = collate_qwen3_batch(rows, cfg, refs) + self.assertEqual(batch['ref_mels'].shape, (n,10,128)) + for i in range(n): + np.testing.assert_array_equal(batch['ref_mels'][i], mels[i]) + + def test_mel_length_and_count_fail_explicitly(self): + cfg, rows, mels = samples() + for refs in ([mels[0], mels[1][:7]], mels[:1], [], [mels[0], np.zeros((10,80))]): + with self.assertRaises(ValueError): + collate_qwen3_batch(rows, cfg, refs) + feats = [dict(row, ref_mels=mel) for row, mel in zip(rows, refs)] + if len(feats) == 2: + with self.assertRaises(ValueError): + Qwen3TTSInputProcessor().collate_fn(feats) + + def test_processor_entire_call_samples_and_collated(self): + for n in (1,2): + cfg, rows, mels = samples(n) + expected = collate_qwen3_batch(rows, cfg, mels) + inputs = [[dict(row, ref_mels=mel) for row,mel in zip(rows,mels)], expected, + {k:torch.as_tensor(v) for k,v in expected.items()}] + for value in inputs: + actual = Qwen3TTSInputProcessor()(value, model=Model()) + self.assertEqual(set(actual), set(expected)) + for key in expected: + np.testing.assert_array_equal(actual[key].numpy(), expected[key]) + self.assertEqual(actual['codec_mask'].dtype, torch.bool) + + def test_processor_rejects_missing_mel_bad_rank_and_packing(self): + cfg, rows, mels = samples() + batch = collate_qwen3_batch(rows, cfg, mels) + missing = dict(batch); del missing['ref_mels'] + for value in (missing, dict(batch, ref_mels=np.zeros((20,128)))): + with self.assertRaises(ValueError): + Qwen3TTSInputProcessor()(value, model=Model()) + with self.assertRaises(ValueError): + Qwen3TTSInputProcessor(padding_free=True)(batch, model=Model()) + + def test_wrapper_forward_alignment_and_every_head_gradient(self): + for n in (1,2): + cfg, rows, mels = samples(n) + batch = collate_qwen3_batch(rows, cfg, mels) + model = Model() + status = types.SimpleNamespace() + config = types.SimpleNamespace(processor=Qwen3TTSInputProcessor(), train_status=status) + wrapper = types.SimpleNamespace( + model=model, optimizer_group={'default':config}, + _get_default_group=lambda:'default', _lazy_wrap_model=lambda:None, + _not_encoded=lambda x:False, sp_strategy=None, hf_config=None) + outputs = inspect.unwrap(Qwen3TTSModel.forward)(wrapper, inputs=batch) + t = model.talker + labels = torch.as_tensor(batch['codec_0_labels']) + self.assertTrue(torch.equal(t.labels, labels)) + expected = nn.functional.cross_entropy(t.logits[:,:-1].reshape(-1,64), + labels[:,1:].reshape(-1)) + torch.testing.assert_close(outputs['talker_loss'], expected) + mask = torch.as_tensor(batch['codec_mask']) + torch.testing.assert_close(t.selected_hidden, t.hidden[mask]) + codes = torch.as_tensor(batch['codec_ids'])[mask] + torch.testing.assert_close(outputs['sub_talker_labels'], codes[:,1:]) + loss = Qwen3TTSLoss()({}, outputs)['loss'] + self.assertTrue(torch.isfinite(loss)) + loss.backward() + for b,row in enumerate(rows): + targets = np.where(row['codec_0_labels'] != -100)[0] + self.assertGreater(t.logits.grad[b,targets[0]-1].abs().sum().item(), 0) + self.assertGreater(t.logits.grad[b,targets[-1]-1].abs().sum().item(), 0) # EOS + shifted_valid = nn.functional.pad(labels[:,1:] != -100, (0,1)) + self.assertEqual(t.logits.grad[~shifted_valid].abs().sum().item(), 0) + # Every head receives its own label, including cb1 and cb15. + want = t.sub_logits.detach().softmax(-1) + want.scatter_add_(-1, codes[:,1:,None], -torch.ones_like(codes[:,1:,None], dtype=want.dtype)) + want *= 0.3 / (codes.shape[0]*15) + torch.testing.assert_close(t.sub_logits.grad, want) + for head in t.heads: + self.assertTrue(torch.isfinite(head.weight.grad).all()) + self.assertGreater(head.weight.grad.abs().sum().item(), 0) + self.assertIs(status.outputs['talker_loss'], outputs['talker_loss']) + self.assertEqual(status.loss_value, 0) + + def test_talker_time_shift_independent_of_processor(self): + cfg, rows, mels = samples() + batch = {key:torch.as_tensor(value) for key,value in collate_qwen3_batch(rows,cfg).items()} + batch['ref_mels'] = torch.as_tensor(np.stack(mels)) + model = Model() + outputs = qwen3_tts_forward_step(model,batch) + # Independent oracle: position t predicts original label[t+1], including + # the first frame and EOS. This fails the old external+internal shift. + expected = nn.functional.cross_entropy(model.talker.logits[:,:-1].reshape(-1,64), + batch['codec_0_labels'][:,1:].reshape(-1)) + torch.testing.assert_close(outputs['talker_loss'], expected) + self.assertEqual(model.talker.logits.shape[:2],batch['codec_0_labels'].shape) + + def test_official_mel_call_contract(self): + fake = types.ModuleType('qwen_tts.core.models.modeling_qwen3_tts') + def mel(wave, **kw): + self.assertEqual(wave.shape, (1,4096)) + self.assertEqual(kw, dict(n_fft=1024,num_mels=128,sampling_rate=24000, + hop_size=256,win_size=1024,fmin=0,fmax=12000.0)) + return torch.ones(1,128,16) + fake.mel_spectrogram = mel + with patch.dict(sys.modules, {'qwen_tts.core.models.modeling_qwen3_tts':fake}): + self.assertEqual(prepare.extract_mel(np.ones(4096)).shape, (16,128)) + + @unittest.skipIf(ISOLATED, 'Official qwen_tts not installed in isolated CPU run') + def test_official_sub_talker_methods_and_last_head_gradient(self): + from qwen_tts.core.models.modeling_qwen3_tts import ( + Qwen3TTSTalkerForConditionalGeneration, + Qwen3TTSTalkerCodePredictorModelForConditionalGeneration, + ) + # Run the installed official assembly and head-selection methods. Only + # the expensive transformer decoder is replaced by a tiny causal sum. + embeddings = nn.ModuleList([nn.Embedding(64,8) for _ in range(15)]) + heads = nn.ModuleList([nn.Linear(8,64) for _ in range(15)]) + cp = types.SimpleNamespace( + config=types.SimpleNamespace(num_code_groups=16, vocab_size=64, + output_attentions=False, output_hidden_states=False), + small_to_mtp_projection=nn.Identity(), lm_head=heads, + get_input_embeddings=lambda:embeddings, loss_function=ForCausalLMLoss, + model=lambda inputs_embeds, **kw: types.SimpleNamespace(last_hidden_state=inputs_embeds.cumsum(1))) + cp.forward_finetune = types.MethodType( + Qwen3TTSTalkerCodePredictorModelForConditionalGeneration.forward_finetune, cp) + talker = types.SimpleNamespace( + config=types.SimpleNamespace(num_code_groups=16,hidden_size=8), + code_predictor=cp, get_input_embeddings=lambda:embeddings[0]) + codes = torch.arange(32).reshape(2,16) + hidden = torch.randn(2,8,requires_grad=True) + logits, _ = Qwen3TTSTalkerForConditionalGeneration.forward_sub_talker_finetune(talker,codes,hidden) + logits.retain_grad() + outputs = dict(talker_loss=hidden.sum()*0,sub_talker_logits=logits,sub_talker_labels=codes[:,1:]) + Qwen3TTSLoss()({},outputs)['loss'].backward() + expected = logits.detach().softmax(-1) + expected.scatter_add_(-1,codes[:,1:,None],-torch.ones(2,15,1)) + torch.testing.assert_close(logits.grad, expected*(0.3/30)) + for head in heads: + self.assertGreater(head.weight.grad.abs().sum().item(),0) + + @unittest.skipIf(ISOLATED, 'Official qwen_tts not installed in isolated CPU run') + def test_official_mel_numerical_parity(self): + from qwen_tts.core.models.modeling_qwen3_tts import mel_spectrogram + wave = np.sin(np.arange(8192, dtype=np.float32)*0.03) + expected = mel_spectrogram(torch.from_numpy(wave)[None], n_fft=1024, + num_mels=128,sampling_rate=24000,hop_size=256,win_size=1024,fmin=0,fmax=12000).transpose(1,2)[0] + np.testing.assert_allclose(prepare.extract_mel(wave), expected.numpy(), rtol=1e-6, atol=1e-6) + + +if __name__ == '__main__': + print('CPU contracts / Twinkle infrastructure: ' + ('ISOLATED DOUBLES' if ISOLATED else 'INSTALLED LIBRARY')) + unittest.main(verbosity=2) diff --git a/cookbook/qwen3_tts/tests/entrypoint/test_contracts.py b/cookbook/qwen3_tts/tests/entrypoint/test_contracts.py new file mode 100644 index 000000000..039142252 --- /dev/null +++ b/cookbook/qwen3_tts/tests/entrypoint/test_contracts.py @@ -0,0 +1,110 @@ +"""CPU manifest/cursor tests; no Twinkle doubles or model imports needed.""" +import importlib.util +import json +from pathlib import Path +import random +import subprocess +import sys +import tempfile +from types import SimpleNamespace +import unittest +import numpy as np + +ROOT = Path(__file__).resolve().parents[2] + + +def load(name, path): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +m = load('manifest', ROOT / 'tts/qwen3/manifest.py') +cli = load('train_cli', ROOT / 'scripts/train_qwen3_tts_lora.py') + + +class ManifestContracts(unittest.TestCase): + def test_learning_rate_decay_is_explicit_and_validated(self): + required = ['--model_path', 'model', '--train_jsonl', 'train', '--output_dir', 'out'] + self.assertEqual(cli.parse_args(required).lr_decay, 1.0) + self.assertEqual(cli.parse_args(required + ['--lr_decay', '0.8']).lr_decay, 0.8) + for value in ('0', '-1', '1.1', 'nan', 'inf'): + with self.subTest(value=value), self.assertRaises(SystemExit): + cli.parse_args(required + ['--lr_decay', value]) + + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + np.save(self.root / 'mel.npy', np.zeros((3, 128), dtype=np.float32)) + self.rows = [dict(id=str(i), text=f'text {i}', audio_codes=[[i] * 16], + ref_mel_path='mel.npy') for i in range(2)] + + def write(self): + path = self.root / 'train.jsonl' + path.write_text('\n'.join(json.dumps(r) for r in self.rows), encoding='utf-8') + return path + + def test_fingerprint_tracks_content_not_location(self): + first = m.Manifest(self.write()) + self.assertEqual(len(first), 2) + self.rows[0]['ref_mel_path'] = str(self.root / 'mel.npy') + self.assertEqual(first.fingerprint, m.Manifest(self.write()).fingerprint) + self.rows[0]['text'] += ' changed' + self.assertNotEqual(first.fingerprint, m.Manifest(self.write()).fingerprint) + second = m.Manifest(self.write()).fingerprint + np.save(self.root / 'mel.npy', np.ones((3, 128), dtype=np.float32)) + self.assertNotEqual(second, m.Manifest(self.write()).fingerprint) + + def test_rejects_empty_duplicate_and_invalid_codes(self): + original = json.loads(json.dumps(self.rows)) + for rows in ([], [original[0], original[0]], + [{**original[0], 'audio_codes': [[2048] * 16]}], + [{**original[0], 'audio_codes': [[0.5] * 16]}], + [{**original[0], 'audio_codes': [[0] * 15]}]): + with self.subTest(rows=rows), self.assertRaises(ValueError): + self.rows = rows + m.Manifest(self.write()) + + def test_rejects_invalid_mel(self): + for mel in (np.zeros((2, 80)), np.full((2, 128), np.nan)): + np.save(self.root / 'mel.npy', mel) + with self.assertRaises(ValueError): + m.Manifest(self.write()) + + def test_shuffle_is_deterministic_and_does_not_consume_rng(self): + state = random.getstate() + plan = m.sample_plan(5, 3, 42, True) + self.assertEqual(state, random.getstate()) + self.assertEqual(plan, m.sample_plan(5, 3, 42, True)) + self.assertNotEqual(plan[0], plan[1]) + for order in plan: + self.assertEqual(sorted(order), list(range(5))) + self.assertEqual(m.sample_plan(2, 2, 42, False), [[0, 1], [0, 1]]) + + def test_tail_and_resume_cursor_do_not_drop_samples(self): + schedule = m.planned_windows(m.sample_plan(5, 2, 42, False), 2) + self.assertEqual([len(w) for w in schedule], [2, 2, 1, 2, 2, 1]) + p = SimpleNamespace(optimizer_steps=3, micro_steps=5, consumed_samples=5, pending_micro_steps=0) + m.validate_cursor(p, schedule) + self.assertEqual(schedule[p.optimizer_steps:], [[0, 1], [2, 3], [4]]) + for key, value in [('micro_steps', 4), ('consumed_samples', 6), + ('pending_micro_steps', 1), ('optimizer_steps', 7)]: + with self.subTest(key=key), self.assertRaises(ValueError): + m.validate_cursor(SimpleNamespace(**{**vars(p), key: value}), schedule) + + def test_cli_help_and_model_content_fingerprint(self): + result = subprocess.run([sys.executable, str(ROOT / 'scripts/train_qwen3_tts_lora.py'), + '--help'], capture_output=True, text=True, timeout=30) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn('--resume_from', result.stdout) + weights = self.root / 'model.safetensors' + weights.write_bytes(b'first mock weight content') + first = cli.model_fingerprint(self.root) + weights.write_bytes(b'second mock weight content') + self.assertNotEqual(first, cli.model_fingerprint(self.root)) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/cookbook/qwen3_tts/tests/entrypoint/test_preparation.py b/cookbook/qwen3_tts/tests/entrypoint/test_preparation.py new file mode 100644 index 000000000..4b4665dde --- /dev/null +++ b/cookbook/qwen3_tts/tests/entrypoint/test_preparation.py @@ -0,0 +1,88 @@ +"""Split preservation and provenance; codec/mel callbacks are explicit doubles.""" +import importlib.util +import json +from pathlib import Path +import tempfile +import unittest +import numpy as np +import soundfile as sf + +ROOT = Path(__file__).resolve().parents[2] +spec = importlib.util.spec_from_file_location('prepare_split', ROOT / 'scripts/prepare_split_manifest.py') +prep = importlib.util.module_from_spec(spec) +spec.loader.exec_module(prep) + + +class Preparation(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + for i in range(2): + sf.write(self.root / f'{i}.wav', np.sin(np.arange(2400) * (i + 1) / 20) * .1, 24000) + self.rows = [dict(id=str(i), text=f'Text {i}', group_id=str(i), speaker_id='speaker', audio=f'{i}.wav') for i in range(2)] + self.write() + + def write(self): + for name, row in zip(('train', 'dev'), self.rows): + (self.root / f'{name}.jsonl').write_text(json.dumps(row) + '\n', encoding='utf-8') + + def audit(self, reference='0'): + return prep.audit_split(self.root / 'train.jsonl', self.root / 'dev.jsonl', reference) + + def test_preserves_text_split_and_train_reference(self): + before = {p.name: p.read_bytes() for p in self.root.iterdir()} + out = self.root / 'derived' + report = prep.prepare(self.root/'train.jsonl', self.root/'dev.jsonl', out, '0', + lambda _: np.zeros((2, 16), dtype=np.int64), lambda _: np.zeros((3, 128), dtype=np.float32)) + self.assertEqual(report['counts'], {'train': 1, 'dev': 1}) + for i, split in enumerate(('train', 'dev')): + row = json.loads((out / f'{split}.jsonl').read_text()) + self.assertEqual(row['text'], self.rows[i]['text']) + self.assertEqual(row['id'], str(i)) + self.assertTrue((out / row['ref_mel_path']).is_file()) + self.assertEqual(report['mapping'][i]['reference_id'], '0') + for name, data in before.items(): + self.assertEqual((self.root/name).read_bytes(), data) + with self.assertRaises(FileExistsError): + prep.prepare(self.root/'train.jsonl', self.root/'dev.jsonl', out, '0', None, None) + + def test_rejects_overlap_for_each_identity(self): + original = dict(self.rows[1]) + for key in ('id', 'group_id', 'text', 'audio'): + self.rows[1] = {**original, key: self.rows[0][key]} + self.write() + with self.subTest(key=key), self.assertRaises(ValueError): + self.audit() + + def test_normalized_text_overlap_is_rejected_without_rewriting(self): + self.rows[1]['text'] = ' TEXT 0 ' + self.write() + with self.assertRaises(ValueError): + self.audit() + + def test_dev_reference_and_empty_dev_are_rejected(self): + with self.assertRaises(ValueError): + self.audit('1') + (self.root/'dev.jsonl').write_text('') + with self.assertRaises(ValueError): + self.audit() + + def test_reencoded_duplicate_audio_is_rejected(self): + wave, sr = sf.read(self.root/'0.wav', dtype='float32') + sf.write(self.root/'1.wav', wave, sr, subtype='FLOAT') + with self.assertRaisesRegex(ValueError, 'pcm'): + self.audit() + + def test_stereo_and_multiple_speakers_are_rejected(self): + self.rows[1]['speaker_id'] = 'other' + self.write() + with self.assertRaises(ValueError): + self.audit() + sf.write(self.root/'0.wav', np.zeros((100, 2)), 24000) + with self.assertRaises(ValueError): + prep.read_audio(self.root/'0.wav') + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/cookbook/qwen3_tts/tests/entrypoint/test_prepared_training_npu.py b/cookbook/qwen3_tts/tests/entrypoint/test_prepared_training_npu.py new file mode 100644 index 000000000..519f2eb6d --- /dev/null +++ b/cookbook/qwen3_tts/tests/entrypoint/test_prepared_training_npu.py @@ -0,0 +1,69 @@ +"""Actual preparation CLI -> validated manifests -> one real NPU training update.""" +import hashlib +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +import torch_npu # register the actual remote NPU runtime + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +from tts.qwen3.manifest import Manifest + + +class PreparedTraining(unittest.TestCase): + def test_public_split_preparation_and_train_only_consumption(self): + outputs = ROOT / 'outputs' + outputs.mkdir(exist_ok=True) + model = os.environ.get('MODEL_PATH', '/models/Qwen3-TTS-12Hz-1.7B-Base') + with tempfile.TemporaryDirectory(prefix='prepared-training-', dir=outputs) as folder: + root = Path(folder) + ids, source_hashes = {}, {} + for split, fixture in [('train', ROOT/'tests/fixtures/librispeech'), + ('dev', ROOT/'tests/fixtures/librispeech/second')]: + meta = json.loads((fixture/'sample.json').read_text()) + audio = fixture/meta['audio'] + self.assertEqual(hashlib.sha256(audio.read_bytes()).hexdigest(), meta['sha256']) + source_hashes[audio] = meta['sha256'] + ids[split] = meta['id'] + # Explicit fixture-only split, not a real held-out evaluation claim. + row = dict(id=meta['id'], text=meta['text'], audio=str(audio), + group_id=meta['id'], speaker_id='1272') + (root/f'{split}.jsonl').write_text(json.dumps(row)+'\n', encoding='utf-8') + derived = root/'derived' + subprocess.run([sys.executable, '-u', str(ROOT/'scripts/prepare_split_manifest.py'), + '--train_jsonl', str(root/'train.jsonl'), '--dev_jsonl', str(root/'dev.jsonl'), + '--reference_id', ids['train'], '--model_path', model, '--output_dir', str(derived)], + check=True, timeout=600) + for split in ('train', 'dev'): + data = Manifest(derived/f'{split}.jsonl') + self.assertEqual(len(data), 1) + self.assertEqual(data.rows[0]['id'], ids[split]) + provenance = json.loads((derived/'provenance.json').read_text()) + self.assertEqual(provenance['counts'], {'train': 1, 'dev': 1}) + self.assertTrue(all(r['reference_id'] == ids['train'] for r in provenance['mapping'])) + output = root/'training' + subprocess.run([sys.executable, '-u', str(ROOT/'scripts/train_qwen3_tts_lora.py'), + '--model_path', model, '--train_jsonl', str(derived/'train.jsonl'), + '--output_dir', str(output), '--epochs', '1', '--grad_accum', '2'], + check=True, timeout=600) + trace = [json.loads(line) for line in (output/'trace.jsonl').read_text().splitlines()] + self.assertEqual(len(trace), 1) + self.assertEqual(trace[0]['sample_ids'], [ids['train']]) + result = json.loads((output/'result.json').read_text()) + self.assertEqual((result['micro_steps'], result['optimizer_steps']), (1, 1)) + self.assertTrue(result['complete']) + self.assertTrue((Path(result['checkpoint'])/'adapter_model.safetensors').is_file()) + for audio, digest in source_hashes.items(): + self.assertEqual(hashlib.sha256(audio.read_bytes()).hexdigest(), digest) + print(json.dumps(dict(stage='预编码训练联通通过 / Prepared training passed', + counts=provenance['counts'], reference_id=ids['train'], consumed_ids=trace[0]['sample_ids'], + dev_consumed=False, loss=trace[0]['losses'], visible_devices=os.environ.get('ASCEND_RT_VISIBLE_DEVICES'), + scope='public regression fixtures only; no held-out quality evaluation'), ensure_ascii=False), flush=True) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/cookbook/qwen3_tts/tests/entrypoint/test_process_resume_npu.py b/cookbook/qwen3_tts/tests/entrypoint/test_process_resume_npu.py new file mode 100644 index 000000000..89dbcbcbb --- /dev/null +++ b/cookbook/qwen3_tts/tests/entrypoint/test_process_resume_npu.py @@ -0,0 +1,111 @@ +"""Actual CLI in three sequential processes, two public samples, epoch tails.""" +import gc +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / 'tests/training')) +import test_resume_npu as previous +import numpy as np +import torch +from safetensors.torch import load_file +from qwen_tts.core.models.modeling_qwen3_tts import Qwen3TTSTokenizer +from tts.qwen3.manifest import sample_plan, planned_windows +from tts.qwen3.speech_fixture import load_speech_fixture +from tts.qwen3.runtime import DEFAULT_MODEL_PATH + + +def compare(test, a, b, exact=False): + if isinstance(a, np.ndarray): + np.testing.assert_array_equal(a, b) + elif isinstance(a, dict): + test.assertEqual(a.keys(), b.keys()) + for key in a: + compare(test, a[key], b[key], exact) + elif isinstance(a, (list, tuple)): + test.assertEqual(len(a), len(b)) + for x, y in zip(a, b): + compare(test, x, y, exact) + else: + previous.assert_tree_equal(test, a, b, rtol=0 if exact else 1e-3, + atol=0 if exact else 1e-5) + + +class ProcessResume(unittest.TestCase): + def test_cli_multi_sample_shuffle_and_restart(self): + path = Path(os.environ.get('MODEL_PATH', DEFAULT_MODEL_PATH)).resolve(strict=True) + outputs = ROOT / 'outputs' + outputs.mkdir(exist_ok=True) + with tempfile.TemporaryDirectory(prefix='process-resume-', dir=outputs) as folder: + root = Path(folder) + codec = Qwen3TTSTokenizer.from_pretrained(str(path / 'speech_tokenizer'), + local_files_only=True, device_map='cpu', torch_dtype=torch.float32) + codec.model.float().eval() + rows = [] + for index, fixture in enumerate(('tests/fixtures/librispeech', 'tests/fixtures/librispeech/second')): + wave, text, meta = load_speech_fixture(ROOT / fixture) + with torch.no_grad(): + codes = previous.baseline.prepare.encode_audio_codes(wave, codec) + np.save(root / f'mel-{index}.npy', previous.baseline.prepare.extract_mel(wave)) + rows.append(dict(id=meta['id'], text=text, audio_codes=codes.tolist(), + ref_mel_path=f'mel-{index}.npy')) + del codec + gc.collect() + manifest = root / 'train.jsonl' + manifest.write_text('\n'.join(json.dumps(r) for r in rows), encoding='utf-8') + common = [sys.executable, '-u', str(ROOT / 'scripts/train_qwen3_tts_lora.py'), + '--model_path', str(path), '--train_jsonl', str(manifest), + '--lr_decay', '0.8', '--epochs', '3', '--grad_accum', '3', '--shuffle', '--seed', '42'] + + def run(name, extra=()): + output = root / name + print(f'启动独立进程 / Starting independent process: {name}', flush=True) + subprocess.run([*common, '--output_dir', str(output), *extra], check=True, timeout=1800) + return output, json.loads((output / 'result.json').read_text()) + + continuous, a = run('continuous') + interrupted, b = run('interrupted', ['--stop_after_updates', '1']) + resumed, c = run('resumed', ['--resume_from', b['checkpoint']]) + self.assertEqual(len({a['pid'], b['pid'], c['pid']}), 3) + self.assertTrue(a['complete'] and c['complete']) + self.assertFalse(b['complete']) + self.assertEqual((b['micro_steps'], b['optimizer_steps']), (2, 1)) + self.assertEqual((c['micro_steps'], c['optimizer_steps'], c['consumed_samples']), (6, 3, 6)) + + def trace(path): + return [json.loads(line) for line in (path / 'trace.jsonl').read_text().splitlines()] + + full, split = trace(continuous), trace(interrupted) + trace(resumed) + expected = [[rows[i]['id'] for i in w] for w in planned_windows(sample_plan(2, 3, 42, True), 3)] + self.assertEqual([r['sample_ids'] for r in full], expected) + self.assertEqual(len(full), len(split)) + for x, y in zip(full, split): + np.testing.assert_allclose(x['losses'], y['losses'], rtol=1e-3, atol=1e-5) + self.assertEqual({k: v for k, v in x.items() if k != 'losses'}, + {k: v for k, v in y.items() if k != 'losses'}) + ac, cc = Path(a['checkpoint']), Path(c['checkpoint']) + wa, wc = load_file(str(ac / 'adapter_model.safetensors')), load_file(str(cc / 'adapter_model.safetensors')) + compare(self, wa, wc) + for filename in ('optimizer.pt', 'scheduler.pt', 'rng_state.pt'): + # Only checkpoint files generated by these child processes are loaded. + compare(self, torch.load(ac / filename, map_location='cpu', weights_only=False), + torch.load(cc / filename, map_location='cpu', weights_only=False), + exact=filename != 'optimizer.pt') + for filename in ('cookbook_progress.json', 'trainer_state.json'): + self.assertEqual(json.loads((ac / filename).read_text()), json.loads((cc / filename).read_text())) + previous.baseline.report('独立进程续训通过 / Independent process resume passed', + pids=[a['pid'], b['pid'], c['pid']], sample_order=expected, + micro_steps=6, optimizer_steps=3, epoch_window_sizes=[2, 2, 2], + max_weight_difference=max((wa[k].float() - wc[k].float()).abs().max().item() for k in wa), + random_states_equal=True, rtol=1e-3, atol=1e-5, + visible_devices=os.environ.get('ASCEND_RT_VISIBLE_DEVICES')) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/cookbook/qwen3_tts/tests/fixtures/librispeech/1272-128104-0000.flac b/cookbook/qwen3_tts/tests/fixtures/librispeech/1272-128104-0000.flac new file mode 100644 index 0000000000000000000000000000000000000000..3b0c844f12abe55b7988eeb62f8553384b0d764f GIT binary patch literal 120041 zcmeF%RZ|>H*Dhd!yAAFPI=BZ93^TY}aCdi?K!D)x7Tn$4-95NNa0?DezUMvMKVes8 z^;y^IldGz$R^8ne^2QP{Ffc%D7;G3AWI-4rFSrw!8<-FFHzD&ZqXLHqA#fG`ZW z87O&#c)j@4mJc7-4%eF>$pIQe?q~{|8yRyEDM006%+C+y-RkATxsJ`z%e1z90ZU9SUd2ms&{qZ&?H&qDxOfM z3Ma~_NFSejA}c8U?vj+PprK`yuT81Vq9s|74_txduyE}R`rreq)s=TO2C7X z-&7f|6qKKSEH`>_{Q#=QmNbO(+ERT-9-b+UebyQfmi3@*Cu;1}w`fX*$8JF7sd$0> zakAiZk}gf4P2n83IMETYL_z5y`m-9<v{mL&=Wz9F9TggnIxl<(fvb;x!+q;1lZ382A z00bcIHEREHhaJP;zT~sJJ#aX?79j4efoPJCrxboj8$@mL>EW^|Sz<1O@2L}3l3IgF zvc24n#OTFqsWV{)n5eU_ow!qo?Qao2-|xj=Qo<|McI4loTwGuf!I5N`hzZX^cZSnl z7FwT-%d2{YO}|VGIP+C(E6Q;yRG5T0V*}7XVE<-S1z3f`H1CQ**6;lZ_^!Fkl_)`U z6u3(jA8|?b#nRoY01}6H)ZDPdePnw?QsXD-HQ}j!<-iEQ&Yw^jjw9s=4FhNOs|
H(JwXuG5sxGPCe?AL|gMk1TijE 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 literal 0 HcmV?d00001 diff --git a/cookbook/qwen3_tts/tests/fixtures/librispeech/ATTRIBUTION.md b/cookbook/qwen3_tts/tests/fixtures/librispeech/ATTRIBUTION.md new file mode 100644 index 000000000..ba9ee34d8 --- /dev/null +++ b/cookbook/qwen3_tts/tests/fixtures/librispeech/ATTRIBUTION.md @@ -0,0 +1,19 @@ +# Public speech regression fixture + +Utterance: LibriSpeech dev-clean **1272-128104-0000**. +Prepared by Vassil Panayotov with assistance from Daniel Povey; derived from LibriVox audiobooks. +Corpus: https://www.openslr.org/12 — **CC BY 4.0**: https://creativecommons.org/licenses/by/4.0/ + +Obtained from the Hugging Face internal testing mirror, clean/validation row 0: +https://huggingface.co/datasets/hf-internal-testing/librispeech_asr_dummy +Mirror revision: 5be91486e11a2d616f4ec5db8d3fd248585ac07a. +The checked-in FLAC is the viewer's audio export, with no local trimming or editing. +The transcript is preserved verbatim in sample.json. The test resamples to 24 kHz at runtime. + +This public **dev-clean** example is exclusively a regression fixture. Do not include it +in customer training/evaluation datasets or claim held-out evaluation after using it to update a model. +A matching corpus transcript is not a new manual transcription or an audio quality rating. + +## Additional process-resume fixture (2026-10-26) + +`second/1272-128104-0001.flac`: row 1 of the same dataset/revision/split, same attribution and CC BY 4.0 license. SHA256 `46a9d58622b4675b29564da2d9ba73e702241c5fa969f12c387cad4aa984276a`. No local trimming or text edits; runtime resamples 16 kHz to 24 kHz. Full transcript and provenance are in `second/sample.json`. This is a regression fixture, not held-out evaluation data. diff --git a/cookbook/qwen3_tts/tests/fixtures/librispeech/sample.json b/cookbook/qwen3_tts/tests/fixtures/librispeech/sample.json new file mode 100644 index 000000000..0034e9252 --- /dev/null +++ b/cookbook/qwen3_tts/tests/fixtures/librispeech/sample.json @@ -0,0 +1,15 @@ +{ + "id": "1272-128104-0000", + "text": "MISTER QUILTER IS THE APOSTLE OF THE MIDDLE CLASSES AND WE ARE GLAD TO WELCOME HIS GOSPEL", + "audio": "1272-128104-0000.flac", + "sha256": "4e25e22555cd16e90edb0a3b49fdcf1fe652b2a1250ab643634db33895c75b41", + "source": "https://huggingface.co/datasets/hf-internal-testing/librispeech_asr_dummy", + "source_revision": "5be91486e11a2d616f4ec5db8d3fd248585ac07a", + "source_split": "clean/validation", + "source_row": 0, + "corpus": "LibriSpeech dev-clean", + "license": "CC-BY-4.0", + "license_url": "https://creativecommons.org/licenses/by/4.0/", + "corpus_url": "https://www.openslr.org/12", + "changes": "HF viewer audio export; no local trimming or text edits. Runtime resamples 16 kHz to 24 kHz." +} diff --git a/cookbook/qwen3_tts/tests/fixtures/librispeech/second/1272-128104-0001.flac b/cookbook/qwen3_tts/tests/fixtures/librispeech/second/1272-128104-0001.flac new file mode 100644 index 0000000000000000000000000000000000000000..0c74e84150028dfe9a2139745663e850b46e657c GIT binary patch literal 101672 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

{qmtv6HBn@namUPGf3>7Ac8sIDw)-o3DI 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

YC6BpBhAaU5NIMw`W=`1Ftvn7DNq?svTnaUE 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 Zz$5@D1qg5rWB{NzU;srN0|I~!i!BU42QB~r literal 0 HcmV?d00001 diff --git a/cookbook/qwen3_tts/tests/fixtures/librispeech/second/sample.json b/cookbook/qwen3_tts/tests/fixtures/librispeech/second/sample.json new file mode 100644 index 000000000..01ffdc1f3 --- /dev/null +++ b/cookbook/qwen3_tts/tests/fixtures/librispeech/second/sample.json @@ -0,0 +1,15 @@ +{ + "id": "1272-128104-0001", + "text": "NOR IS MISTER QUILTER'S MANNER LESS INTERESTING THAN HIS MATTER", + "audio": "1272-128104-0001.flac", + "sha256": "46a9d58622b4675b29564da2d9ba73e702241c5fa969f12c387cad4aa984276a", + "source": "https://huggingface.co/datasets/hf-internal-testing/librispeech_asr_dummy", + "source_revision": "5be91486e11a2d616f4ec5db8d3fd248585ac07a", + "source_split": "clean/validation", + "source_row": 1, + "corpus": "LibriSpeech dev-clean", + "license": "CC-BY-4.0", + "license_url": "https://creativecommons.org/licenses/by/4.0/", + "corpus_url": "https://www.openslr.org/12", + "changes": "HF viewer audio export; no local trimming or text edits. Runtime resamples 16 kHz to 24 kHz." +} diff --git a/cookbook/qwen3_tts/tests/lora/test_contracts.py b/cookbook/qwen3_tts/tests/lora/test_contracts.py new file mode 100644 index 000000000..cf92c162d --- /dev/null +++ b/cookbook/qwen3_tts/tests/lora/test_contracts.py @@ -0,0 +1,127 @@ +"""CPU regressions for projection and acceptance assertions; Twinkle may be doubled.""" +import sys +import types +import unittest +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / 'tests/batch1')) +import test_contracts as previous +import torch +from torch import nn +from tts.qwen3.runtime import data_config, frozen_digest, trainable_snapshot, assert_updated, assert_gradients + + +class AcceptanceContracts(unittest.TestCase): + def test_new_optimizer_groups_receive_local_tokenizer_policy(self): + module = types.ModuleType('twinkle.model.transformers.transformers') + module.OptimizerGroup = types.SimpleNamespace + wrapper = types.SimpleNamespace(tokenizer_id='/local/model', device_mesh=None) + with patch.dict(sys.modules, {module.__name__: module}), \ + patch('tts.qwen3.tokenization.local_template') as template: + for _ in range(2): # initial group and newly added adapter group + group = previous.Qwen3TTSModel._construct_default_optimizer_group(wrapper) + self.assertIs(group.template, template.return_value) + self.assertIsInstance(group.processor, previous.Qwen3TTSInputProcessor) + self.assertIsInstance(group.loss_instance, previous.Qwen3TTSLoss) + self.assertEqual(template.call_count, 2) + template.assert_called_with('/local/model') + + def cpu_batch(self, model, cfg, rows, mels): + # These are CPU regressions even on an NPU host. Fail if the processor + # consults the platform default instead of the supplied model device. + with patch('tts.qwen3.input_processor.Platform.get_local_device', + side_effect=AssertionError('CPU fixture must use model.device')): + batch = previous.Qwen3TTSInputProcessor()( + previous.collate_qwen3_batch(rows, cfg, mels), model=model) + self.assertTrue(all(value.device.type == 'cpu' for value in batch.values())) + return batch + + def test_text_projection_handles_different_width_and_receives_gradient(self): + cfg, rows, mels = previous.samples(1) + model = previous.Model() + batch = self.cpu_batch(model, cfg, rows, mels) + model.talker.model.text_embedding = nn.Embedding(64, 12) + model.talker.text_projection = nn.Linear(12, 8) + seen = [] + handle = model.talker.text_projection.register_forward_hook(lambda m, a, o: seen.append(o.shape)) + outputs = previous.qwen3_tts_forward_step(model, batch) + previous.Qwen3TTSLoss()({}, outputs)['loss'].backward() + handle.remove() + self.assertEqual(len(seen), 1) + self.assertEqual(seen[0][-1], 8) + self.assertGreater(model.talker.text_projection.weight.grad.abs().sum().item(), 0) + + def test_speaker_encoder_has_no_gradient(self): + cfg, rows, mels = previous.samples(1) + model = previous.Model() + batch = self.cpu_batch(model, cfg, rows, mels) + # Preserve the speaker encoder's [B,F,128] -> [B,H] contract. + class Speaker(nn.Module): + def __init__(self): + super().__init__() + self.linear = nn.Linear(128, 8) + def forward(self, mel): + return self.linear(mel.mean(1)) + model.speaker_encoder = Speaker() + outputs = previous.qwen3_tts_forward_step(model, batch) + previous.Qwen3TTSLoss()({}, outputs)['loss'].backward() + self.assertIsNone(model.speaker_encoder.linear.weight.grad) + self.assertFalse(outputs['target_speaker_embedding'].requires_grad) + + def test_config_uses_all_real_token_ids_not_defaults(self): + names = ['codec_pad_id', 'codec_bos_id', 'codec_eos_token_id', + 'codec_nothink_id', 'codec_think_bos_id', 'codec_think_eos_id', 'num_code_groups'] + tc = types.SimpleNamespace(**dict(zip(names, range(10, 17)))) + config = types.SimpleNamespace(talker_config=tc, tts_pad_token_id=1, tts_bos_token_id=2, tts_eos_token_id=3) + actual = data_config(config) + self.assertEqual((actual.tts_pad_token_id, actual.tts_bos_token_id, actual.tts_eos_token_id), (1, 2, 3)) + for name in names: + self.assertEqual(getattr(actual, name), getattr(tc, name)) + + def tiny_model(self): + model = nn.Module() + model.base = nn.Module() + model.base.talker = nn.Module() + for branch in ('model', 'code_predictor'): + layer = nn.Module() + layer.lora_B = nn.ParameterDict({'smoke': nn.Parameter(torch.zeros(2, 2))}) + layer.weight = nn.Parameter(torch.ones(2, 2), requires_grad=False) + setattr(model.base.talker, branch, layer) + return model + + def test_assertions_reject_no_update_and_frozen_or_missing_branch(self): + model = self.tiny_model() + before = trainable_snapshot(model) + with self.assertRaisesRegex(AssertionError, 'No parameter update'): + assert_updated(before, trainable_snapshot(model)) + model.base.talker.model.weight.requires_grad_(True) + with self.assertRaisesRegex(AssertionError, 'Only LoRA'): + trainable_snapshot(model) + model.base.talker.model.weight.requires_grad_(False) + model.base.talker.code_predictor.lora_B['smoke'].requires_grad_(False) + with self.assertRaisesRegex(AssertionError, 'Missing trainable branch'): + trainable_snapshot(model) + + def test_gradients_updates_and_frozen_digest_detect_corruption(self): + model = self.tiny_model() + before = trainable_snapshot(model) + digest = frozen_digest(model) + with self.assertRaisesRegex(AssertionError, 'Missing/nonfinite'): + assert_gradients(model) + sum(p.sum() for p in model.parameters() if p.requires_grad).backward() + self.assertEqual(len(assert_gradients(model)), 2) + torch.optim.SGD([p for p in model.parameters() if p.requires_grad], lr=0.1).step() + self.assertEqual(len(assert_updated(before, trainable_snapshot(model))), 2) + self.assertEqual(digest, frozen_digest(model)) + with torch.no_grad(): + model.base.talker.model.weight[0, 0] += 1 + self.assertNotEqual(digest, frozen_digest(model)) + model.base.talker.model.lora_B['smoke'].grad.fill_(float('nan')) + with self.assertRaisesRegex(AssertionError, 'Missing/nonfinite'): + assert_gradients(model) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/cookbook/qwen3_tts/tests/lora/test_real_model.py b/cookbook/qwen3_tts/tests/lora/test_real_model.py new file mode 100644 index 000000000..ffd7f289c --- /dev/null +++ b/cookbook/qwen3_tts/tests/lora/test_real_model.py @@ -0,0 +1,181 @@ +"""Real pretrained Qwen3-TTS + Twinkle + one NPU. No doubles or skip path. + +Uses the existing local 1.7B Base directory without downloads. The synthetic sine +wave/text pair only exercises mechanics; it is not speech/quality evidence. +""" +import gc +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import sys +import tempfile +import unittest + +import numpy as np +import torch +import torch_npu # noqa: F401 + +# Also enforce local-only model loading when this test is invoked directly. +os.environ['HF_HUB_OFFLINE'] = '1' +os.environ['TRANSFORMERS_OFFLINE'] = '1' +from peft import LoraConfig +from safetensors.torch import load_file +import twinkle +from twinkle import DeviceMesh + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +from tts.qwen3.data import build_assistant_text, build_qwen3_sample, collate_qwen3_batch +from tts.qwen3.forward import qwen3_tts_forward_step +from tts.qwen3.input_processor import Qwen3TTSInputProcessor +from tts.qwen3.loss import Qwen3TTSLoss +from tts.qwen3.tokenization import load_text_tokenizer, assert_tokenizers_match +from tts.qwen3.runtime import (DEFAULT_MODEL_PATH, ADAPTER, data_config, + load_twinkle_model, trainable_snapshot, frozen_digest, assert_gradients, assert_updated) + +spec = importlib.util.spec_from_file_location('prepare', ROOT / 'scripts/audio_features.py') +prepare = importlib.util.module_from_spec(spec) +spec.loader.exec_module(prepare) + + +def report(stage, **values): + print(json.dumps({'stage': stage, **values}, ensure_ascii=False), flush=True) + + +def evaluation(model, batch): + model.eval() + with torch.no_grad(): + result = qwen3_tts_forward_step(model, batch) + return {key: result[key].detach().float().cpu() for key in + ('talker_loss', 'sub_talker_logits', 'target_speaker_embedding')} + + +class RealModelAcceptance(unittest.TestCase): + scope = 'one synthetic sample; not speech quality or optimizer resume' + + def sample(self): + t = np.arange(24000, dtype=np.float32) / 24000 + wave = (0.08 * np.sin(2 * np.pi * 220 * t) * np.sin(np.pi * t)**2).astype(np.float32) + return wave, '这是一条训练链路测试。', {'kind': 'synthetic sine wave'} + + def test_pretrained_single_sample_lora_update_and_fresh_reload(self): + torch.manual_seed(20261023) + np.random.seed(20261023) + torch.npu.set_device(0) + mesh = DeviceMesh.from_sizes(fsdp_size=1, dp_size=1) + twinkle.initialize(mode='local', global_device_mesh=mesh, seed=20261023) + path = Path(os.environ.get('MODEL_PATH', DEFAULT_MODEL_PATH)).expanduser().resolve(strict=True) + report('加载本地模型 / Load local model', path=str(path), + source_revision='unknown: user-provided local model', + config_sha256=hashlib.sha256((path / 'config.json').read_bytes()).hexdigest(), + visible_devices=os.environ.get('ASCEND_RT_VISIBLE_DEVICES', 'unset')) + model = load_twinkle_model(path, mesh) + cfg = data_config(model.hf_config) + self.assertEqual(model.hf_config.tts_model_type, 'base') + self.assertEqual(cfg.num_code_groups, 16) + self.assertEqual(model.model.dtype, torch.bfloat16) + + # Actual codec on CPU fp32; training and speaker encoder on NPU bf16. + wave, text, sample_metadata = self.sample() + report('样本来源 / Sample provenance', **sample_metadata) + codec = model.model.speech_tokenizer + codec.model.float().eval() + with torch.no_grad(): + codes = prepare.encode_audio_codes(wave, codec) + mel = prepare.extract_mel(wave) + model.model.speech_tokenizer = None + del codec + tokenizer = load_text_tokenizer(path) + probes = [text, '你好,世界!Hello 2026.\nA B', build_assistant_text(text)] + assert_tokenizers_match(tokenizer, + [model.optimizer_group[model._get_default_group()].template.tokenizer], probes) + text_ids = tokenizer.encode(build_assistant_text(text)) + # Verify the fixed suffix on the real tokenizer rather than blindly trim. + suffix = tokenizer.encode('<|im_end|>\n<|im_start|>assistant\n', add_special_tokens=False) + self.assertEqual(len(suffix), 5) + self.assertEqual(text_ids[-len(suffix):], suffix) + row = build_qwen3_sample(np.asarray(text_ids[:-len(suffix)]), codes, cfg) + raw_batch = collate_qwen3_batch([row], cfg, [mel]) + self.assertGreater(len(codes), 0) + self.assertGreaterEqual(int(codes.min()), 0) + self.assertLess(int(codes.max()), model.hf_config.talker_config.code_predictor_config.vocab_size) + report('样本已编码 / Sample encoded', audio_sha256=hashlib.sha256(wave.tobytes()).hexdigest(), + codec_shape=list(codes.shape), mel_shape=list(mel.shape), tokens=len(text_ids)) + + model.add_adapter_to_model(ADAPTER, LoraConfig( + r=8, lora_alpha=16, target_modules=['q_proj', 'v_proj'], + lora_dropout=0.0, bias='none'), gradient_accumulation_steps=1) + assert_tokenizers_match(tokenizer, [model.optimizer_group[ADAPTER].template.tokenizer], probes) + model.set_processor(Qwen3TTSInputProcessor, adapter_name=ADAPTER) + model.set_loss(Qwen3TTSLoss(), adapter_name=ADAPTER) + model.set_optimizer('AdamW', lr=1e-3, weight_decay=0.0, adapter_name=ADAPTER) + model._lazy_wrap_model() + base = model.strategy.unwrap_model(model.model) + self.assertEqual(base.device.type, 'npu') + self.assertEqual(base.dtype, torch.bfloat16) + before = trainable_snapshot(base) + frozen_before = frozen_digest(base) + batch = model.optimizer_group[ADAPTER].processor(raw_batch, model=base) + report('执行一次更新 / Run one update', trainable_parameters=sum(v.numel() for v in before.values()), + trainable_tensors=len(before), adapter_dtype=str(next(iter(before.values())).dtype)) + outputs = model.forward_backward(inputs=batch, adapter_name=ADAPTER) + loss = torch.as_tensor(outputs['loss']) + self.assertTrue(torch.isfinite(loss).all().item()) + nonzero = assert_gradients(base) + model.clip_grad_and_step(adapter_name=ADAPTER) + torch.npu.synchronize() + after = trainable_snapshot(base) + changed = assert_updated(before, after) + self.assertEqual(frozen_before, frozen_digest(base), 'Frozen base weights changed') + self.assertEqual(model.optimizer_group[ADAPTER].cur_step, 1) + reference = evaluation(base, batch) + for value in reference.values(): + self.assertTrue(torch.isfinite(value).all().item()) + report('更新断言通过 / Update assertions passed', loss=loss.detach().float().cpu().tolist(), + nonzero_gradient_tensors=len(nonzero), changed_tensors=len(changed), frozen_sha256=frozen_before) + + output_root = ROOT / 'outputs' + output_root.mkdir(exist_ok=True) + with tempfile.TemporaryDirectory(prefix='real-lora-', dir=output_root) as output: + checkpoint = model.save('adapter', output_dir=output, adapter_name=ADAPTER, save_optimizer=False) + assert_tokenizers_match(tokenizer, [load_text_tokenizer(checkpoint)], probes) + saved = load_file(str(Path(checkpoint) / 'adapter_model.safetensors')) + expected = {name.replace('.' + ADAPTER + '.', '.'): value for name, value in after.items()} + self.assertEqual(saved.keys(), expected.keys(), 'Checkpoint must contain only this adapter') + for name in saved: + torch.testing.assert_close(saved[name], expected[name], rtol=0, atol=0) + # Remove the original model and optimizer before loading another base. + del before, after, outputs, loss, base, model, batch + gc.collect() + torch.npu.empty_cache() + report('从新底模重载 / Reload into fresh pretrained base') + reloaded = load_twinkle_model(path, mesh) + reloaded.model.speech_tokenizer = None + reloaded.add_adapter_to_model(ADAPTER, str(checkpoint), is_trainable=True) + reloaded.set_processor(Qwen3TTSInputProcessor, adapter_name=ADAPTER) + assert_tokenizers_match(tokenizer, + [reloaded.optimizer_group[ADAPTER].template.tokenizer], probes) + report('文本编码一致 / Tokenizer IDs consistent', probes=len(probes), + fix_mistral_regex=True, paths=['text loader', 'Twinkle templates', 'saved tokenizer', 'fresh reload']) + reloaded._lazy_wrap_model() + fresh = reloaded.strategy.unwrap_model(reloaded.model) + actual_weights = trainable_snapshot(fresh) + normalized = {n.replace('.' + ADAPTER + '.', '.'): v for n, v in actual_weights.items()} + self.assertEqual(normalized.keys(), saved.keys()) + for name in saved: + torch.testing.assert_close(normalized[name], saved[name], rtol=0, atol=0) + self.assertEqual(frozen_before, frozen_digest(fresh)) + reloaded_batch = reloaded.optimizer_group[ADAPTER].processor(raw_batch, model=fresh) + actual = evaluation(fresh, reloaded_batch) + differences = {} + for key in reference: + torch.testing.assert_close(actual[key], reference[key], rtol=1e-3, atol=1e-3) + differences[key] = (actual[key] - reference[key]).abs().max().item() + report('保存重载通过 / Save and fresh reload passed', max_absolute_differences=differences, + scope=self.scope) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/cookbook/qwen3_tts/tests/run.py b/cookbook/qwen3_tts/tests/run.py new file mode 100644 index 000000000..81d75b53f --- /dev/null +++ b/cookbook/qwen3_tts/tests/run.py @@ -0,0 +1,24 @@ +"""Run each contract module in a fresh process; NPU tests are explicit opt-in.""" +import argparse +import os +from pathlib import Path +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[1] +parser = argparse.ArgumentParser(description=__doc__) +group = parser.add_mutually_exclusive_group() +group.add_argument('--isolated', action='store_true', help='CPU development with explicit Twinkle doubles') +group.add_argument('--npu', action='store_true', help='Also run real local-model integrations; set MODEL_PATH') +args = parser.parse_args() +if args.npu and not os.environ.get('MODEL_PATH'): + parser.error('--npu requires MODEL_PATH pointing to an existing local model') +modules = ['batch1/test_contracts.py', 'lora/test_contracts.py', 'training/test_contracts.py', + 'entrypoint/test_contracts.py', 'entrypoint/test_preparation.py'] +if args.npu: + modules += ['lora/test_real_model.py', 'entrypoint/test_prepared_training_npu.py', + 'entrypoint/test_process_resume_npu.py'] +for module in modules: + extra = ['--isolated-twinkle'] if args.isolated and module.startswith(('batch1/', 'lora/')) else [] + print('Running: ' + module, flush=True) + subprocess.run([sys.executable, '-u', str(ROOT/'tests'/module), *extra], check=True, timeout=3600) diff --git a/cookbook/qwen3_tts/tests/training/test_contracts.py b/cookbook/qwen3_tts/tests/training/test_contracts.py new file mode 100644 index 000000000..6e11e646e --- /dev/null +++ b/cookbook/qwen3_tts/tests/training/test_contracts.py @@ -0,0 +1,122 @@ +"""CPU loop tests with real autograd/SGD and an explicit Twinkle protocol double.""" +import importlib.util +import json +from pathlib import Path +import sys +import tempfile +import types +import unittest +import torch + +ROOT = Path(__file__).resolve().parents[2] +spec = importlib.util.spec_from_file_location('training', ROOT / 'tts/qwen3/training.py') +training = importlib.util.module_from_spec(spec) +sys.modules[spec.name] = training +spec.loader.exec_module(training) + + +class Bridge: + def __init__(self): + self.model = torch.nn.Linear(1, 1, bias=False) + self.model.weight.data.fill_(1) + self.optimizer = torch.optim.SGD(self.model.parameters(), lr=0.1) + self.scheduler = torch.optim.lr_scheduler.StepLR(self.optimizer, 1, gamma=0.8) + self.group = types.SimpleNamespace(gradient_accumulation_steps=1, cur_step=0, + train_status=types.SimpleNamespace(num_tokens=0)) + self.optimizer_group = {'test': self.group} + self.updates = [] + + def forward_backward(self, inputs, adapter_name): + loss = (self.model.weight - inputs['target']).square().mean() + loss.backward() + self.group.cur_step += 1 + self.group.train_status.num_tokens += 1 + return {'loss': loss.item()} + + def clip_grad_and_step(self, adapter_name): + count = self.group.train_status.num_tokens + for p in self.model.parameters(): + p.grad.div_(count) + self.optimizer.step() + self.optimizer.zero_grad(set_to_none=True) + self.scheduler.step() + self.updates.append((self.group.cur_step, count)) + self.group.train_status.num_tokens = 0 + + +def batch(target): + return {'input_ids': torch.zeros(1, 4, 2), 'target': float(target)} + + +class TrainingContracts(unittest.TestCase): + def test_tail_matches_independent_mean_gradient_reference(self): + model = Bridge() + progress = training.Progress(2) + targets = [2, -1, 3, 4, -2] + reference = torch.tensor(1., requires_grad=True) + optimizer = torch.optim.SGD([reference], lr=0.1) + scheduler = torch.optim.lr_scheduler.StepLR(optimizer, 1, gamma=0.8) + for chunk in [targets[:2], targets[2:4], targets[4:]]: + ((reference - torch.tensor(chunk)).square().mean()).backward() + optimizer.step() + optimizer.zero_grad(set_to_none=True) + scheduler.step() + for chunk in training.windows([batch(t) for t in targets], 2): + training.train_window(model, chunk, progress, 'test') + torch.testing.assert_close(model.model.weight.squeeze(), reference) + self.assertEqual(model.updates, [(2, 2), (4, 2), (5, 1)]) + self.assertEqual((progress.micro_steps, progress.optimizer_steps, progress.consumed_samples), (5, 3, 5)) + self.assertEqual(progress.pending_micro_steps, 0) + self.assertEqual(model.scheduler.last_epoch, 3) + + def test_invalid_batch_is_rejected_before_any_backward(self): + model = Bridge() + bad = batch(1) + bad['input_ids'] = torch.zeros(2, 4, 2) + with self.assertRaisesRegex(ValueError, 'single-sample'): + training.train_window(model, [batch(1), bad], training.Progress(2), 'test') + self.assertEqual(model.group.cur_step, 0) + self.assertIsNone(model.model.weight.grad) + + def test_empty_windows_and_invalid_accumulation_do_not_step(self): + self.assertEqual(list(training.windows([], 2)), []) + with self.assertRaises(ValueError): + training.Progress(0) + with self.assertRaises(ValueError): + training.train_window(Bridge(), [], training.Progress(2), 'test') + + def test_pending_gradients_cannot_be_checkpointed(self): + model = Bridge() + progress = training.Progress(2, pending_micro_steps=1) + with self.assertRaisesRegex(ValueError, 'unstepped'): + training.save_boundary(model, progress, '.', 'unused', 'test', 'sample') + progress.pending_micro_steps = 0 + model.model.weight.grad = torch.ones_like(model.model.weight) + with self.assertRaisesRegex(ValueError, 'cleared'): + training.save_boundary(model, progress, '.', 'unused', 'test', 'sample') + + def test_missing_resume_files_never_silently_restart(self): + with tempfile.TemporaryDirectory() as temp: + with self.assertRaises(FileNotFoundError): + training.resume_boundary(Bridge(), temp, 'test', 'sample', 2) + + def test_mismatched_resume_metadata_is_rejected_before_loading(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + for name in ('adapter_config.json', 'adapter_model.safetensors', 'optimizer.pt', + 'scheduler.pt', 'rng_state.pt', 'trainer_state.json'): + (root / name).write_text('{}') + state = {'format_version': 1, 'sample_fingerprint': 'other', + 'progress': dict(accumulation_steps=2, micro_steps=2, optimizer_steps=1, + consumed_samples=2, pending_micro_steps=0)} + (root / 'cookbook_progress.json').write_text(json.dumps(state)) + with self.assertRaisesRegex(ValueError, 'sample mismatch'): + training.resume_boundary(Bridge(), root, 'test', 'sample', 2) + state['sample_fingerprint'] = 'sample' + (root / 'cookbook_progress.json').write_text(json.dumps(state)) + with self.assertRaisesRegex(ValueError, 'accumulation mismatch'): + training.resume_boundary(Bridge(), root, 'test', 'sample', 3) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/cookbook/qwen3_tts/tests/training/test_resume_npu.py b/cookbook/qwen3_tts/tests/training/test_resume_npu.py new file mode 100644 index 000000000..f3439ead1 --- /dev/null +++ b/cookbook/qwen3_tts/tests/training/test_resume_npu.py @@ -0,0 +1,177 @@ +"""Five paired-speech micro-batches: continuous versus fresh-model resume.""" +import copy +import gc +import hashlib +import json +import os +from pathlib import Path +import random +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / 'tests/lora')) +import test_real_model as baseline # local-only imports and shared fixture utilities +import numpy as np +import torch +from peft import LoraConfig +from tts.qwen3.runtime import (ADAPTER, DEFAULT_MODEL_PATH, load_twinkle_model, data_config, + trainable_snapshot, frozen_digest, assert_updated) +from tts.qwen3.data import build_assistant_text, build_qwen3_sample, collate_qwen3_batch +from tts.qwen3.tokenization import load_text_tokenizer +from tts.qwen3.speech_fixture import load_speech_fixture +from tts.qwen3.input_processor import Qwen3TTSInputProcessor +from tts.qwen3.loss import Qwen3TTSLoss +from tts.qwen3.training import Progress, windows, train_window, save_boundary, resume_boundary + + +def cpu_tree(value): + if isinstance(value, torch.Tensor): + return value.detach().cpu().clone() + if isinstance(value, dict): + return {k: cpu_tree(v) for k, v in value.items()} + if isinstance(value, (tuple, list)): + return type(value)(cpu_tree(v) for v in value) + return copy.deepcopy(value) + + +def assert_tree_equal(test, a, b, rtol=1e-3, atol=1e-5): + if isinstance(a, torch.Tensor): + torch.testing.assert_close(a, b, rtol=rtol, atol=atol) + elif isinstance(a, dict): + test.assertEqual(a.keys(), b.keys()) + for key in a: + assert_tree_equal(test, a[key], b[key], rtol, atol) + elif isinstance(a, (tuple, list)): + test.assertEqual(len(a), len(b)) + for x, y in zip(a, b): + assert_tree_equal(test, x, y, rtol, atol) + else: + test.assertEqual(a, b) + + +def configure(model): + model.add_adapter_to_model(ADAPTER, LoraConfig(r=8, lora_alpha=16, + target_modules=['q_proj', 'v_proj'], lora_dropout=0.1, bias='none'), + gradient_accumulation_steps=1) + model.set_processor(Qwen3TTSInputProcessor, adapter_name=ADAPTER) + model.set_loss(Qwen3TTSLoss(), adapter_name=ADAPTER) + model.set_optimizer('AdamW', lr=1e-3, weight_decay=0.0, adapter_name=ADAPTER) + model.set_lr_scheduler('StepLR', step_size=1, gamma=0.8, adapter_name=ADAPTER) + model._lazy_wrap_model() + + +def rng_probe(): + return [random.random(), float(np.random.rand()), torch.rand(1).item(), + torch.rand(1, device='npu:0').cpu().item()] + + +class ResumeAcceptance(unittest.TestCase): + def test_five_micro_batches_tail_and_fresh_optimizer_resume(self): + torch.npu.set_device(0) + mesh = baseline.DeviceMesh.from_sizes(fsdp_size=1, dp_size=1) + baseline.twinkle.initialize(mode='local', global_device_mesh=mesh, seed=20261023) + path = Path(os.environ.get('MODEL_PATH', DEFAULT_MODEL_PATH)).resolve(strict=True) + model = load_twinkle_model(path, mesh) + wave, text, meta = load_speech_fixture(ROOT / 'tests/fixtures/librispeech') + codec = model.model.speech_tokenizer + codec.model.float().eval() + with torch.no_grad(): + codes = baseline.prepare.encode_audio_codes(wave, codec) + mel = baseline.prepare.extract_mel(wave) + model.model.speech_tokenizer = None + del codec + tokenizer = load_text_tokenizer(path) + ids = tokenizer.encode(build_assistant_text(text)) + suffix = tokenizer.encode('<|im_end|>\n<|im_start|>assistant\n', add_special_tokens=False) + self.assertEqual(ids[-len(suffix):], suffix) + cfg = data_config(model.hf_config) + raw = collate_qwen3_batch([build_qwen3_sample(np.asarray(ids[:-len(suffix)]), codes, cfg)], cfg, [mel]) + fingerprint = hashlib.sha256(json.dumps({'sample': meta['sha256'], 'text_ids': ids, + 'model_config': hashlib.sha256((path / 'config.json').read_bytes()).hexdigest(), + 'schedule': 'same public sample repeated five times; accumulation=2'}, sort_keys=True).encode()).hexdigest() + configure(model) + base = model.strategy.unwrap_model(model.model) + self.assertEqual(base.device.type, 'npu') + self.assertEqual(base.dtype, torch.bfloat16) + initial = trainable_snapshot(base) + frozen = frozen_digest(base) + group = model.optimizer_group[ADAPTER] + batch = group.processor(raw, model=base) + progress = Progress(accumulation_steps=2) + train_window(model, [batch, batch], progress, ADAPTER) + self.assertEqual((progress.micro_steps, progress.optimizer_steps, progress.consumed_samples), (2, 1, 2)) + at_boundary = trainable_snapshot(base) + assert_updated(initial, at_boundary) + optimizer_boundary = cpu_tree(group.optimizer.state_dict()) + scheduler_boundary = copy.deepcopy(group.lr_scheduler.state_dict()) + output_root = ROOT / 'outputs' + output_root.mkdir(exist_ok=True) + with tempfile.TemporaryDirectory(prefix='resume-', dir=output_root) as temp: + checkpoint = save_boundary(model, progress, temp, 'after-update-1', ADAPTER, fingerprint) + baseline.report('断点已保存 / Boundary saved', micro_steps=2, optimizer_steps=1, + accumulation_steps=2, dropout=0.1, visible_devices=os.environ.get('ASCEND_RT_VISIBLE_DEVICES')) + trace = [] + losses = [] + sizes = [] + for window in windows([batch] * 3, 2): + sizes.append(len(window)) + losses.extend(train_window(model, window, progress, ADAPTER, + before_micro=lambda step: trace.append((step, rng_probe())))) + expected_weights = trainable_snapshot(base) + expected_optimizer = cpu_tree(group.optimizer.state_dict()) + expected_scheduler = copy.deepcopy(group.lr_scheduler.state_dict()) + expected_progress = copy.deepcopy(progress) + self.assertEqual(sizes, [2, 1]) + self.assertEqual((progress.micro_steps, progress.optimizer_steps, progress.consumed_samples), (5, 3, 5)) + self.assertEqual(group.cur_step, 5) + self.assertEqual(group.lr_scheduler.last_epoch, 3) + self.assertEqual(group.train_status.num_tokens, 0) + self.assertTrue(all(p.grad is None for p in base.parameters())) + self.assertEqual(frozen, frozen_digest(base)) + assert_updated(at_boundary, expected_weights) + self.assertEqual(len(expected_optimizer['state']), len(initial)) + self.assertTrue(all(int(state['step'].item()) == 3 for state in expected_optimizer['state'].values())) + baseline.report('连续训练完成 / Continuous run completed', micro_steps=5, optimizer_steps=3, + window_sizes=[2, 2, 1], learning_rates=group.lr_scheduler.get_last_lr()) + del model, base, group, batch + gc.collect() + torch.npu.empty_cache() + + resumed = load_twinkle_model(path, mesh) + resumed.model.speech_tokenizer = None + configure(resumed) + fresh = resumed.strategy.unwrap_model(resumed.model) + new_group = resumed.optimizer_group[ADAPTER] + new_batch = new_group.processor(raw, model=fresh) + restored = resume_boundary(resumed, checkpoint, ADAPTER, fingerprint, 2) + restored_weights = trainable_snapshot(fresh) + for name in at_boundary: + torch.testing.assert_close(restored_weights[name], at_boundary[name], rtol=0, atol=0) + assert_tree_equal(self, optimizer_boundary, cpu_tree(new_group.optimizer.state_dict()), rtol=0, atol=0) + self.assertEqual(scheduler_boundary, new_group.lr_scheduler.state_dict()) + actual_trace = [] + actual_losses = [] + for window in windows([new_batch] * (5 - restored.consumed_samples), 2): + actual_losses.extend(train_window(resumed, window, restored, ADAPTER, + before_micro=lambda step: actual_trace.append((step, rng_probe())))) + actual_weights = trainable_snapshot(fresh) + assert_tree_equal(self, expected_weights, actual_weights) + assert_tree_equal(self, expected_optimizer, cpu_tree(new_group.optimizer.state_dict())) + self.assertEqual(expected_scheduler, new_group.lr_scheduler.state_dict()) + self.assertEqual(expected_progress, restored) + self.assertEqual(trace, actual_trace, 'Python/NumPy/CPU/NPU random streams differ') + np.testing.assert_allclose(losses, actual_losses, rtol=1e-3, atol=1e-5) + self.assertEqual(frozen, frozen_digest(fresh)) + max_difference = max((actual_weights[n].float() - expected_weights[n].float()).abs().max().item() + for n in expected_weights) + baseline.report('断点续训对照通过 / Resume comparison passed', micro_steps=restored.micro_steps, + optimizer_steps=restored.optimizer_steps, consumed_samples=restored.consumed_samples, + max_weight_difference=max_difference, random_streams_equal=True, + losses_continuous=losses, losses_resumed=actual_losses, rtol=1e-3, atol=1e-5, + scope='five repeated public-speech micro-batches; boundary-only resume, single NPU') + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/cookbook/qwen3_tts/tts/__init__.py b/cookbook/qwen3_tts/tts/__init__.py new file mode 100644 index 000000000..28cc1f3b3 --- /dev/null +++ b/cookbook/qwen3_tts/tts/__init__.py @@ -0,0 +1 @@ +"""Local Qwen3-TTS cookbook support; no global Twinkle registration.""" diff --git a/cookbook/qwen3_tts/tts/common/__init__.py b/cookbook/qwen3_tts/tts/common/__init__.py new file mode 100644 index 000000000..00ea8e69d --- /dev/null +++ b/cookbook/qwen3_tts/tts/common/__init__.py @@ -0,0 +1 @@ +from .multi_codebook_loss import MultiCodebookLoss diff --git a/cookbook/qwen3_tts/tts/common/multi_codebook_loss.py b/cookbook/qwen3_tts/tts/common/multi_codebook_loss.py new file mode 100644 index 000000000..be7166f33 --- /dev/null +++ b/cookbook/qwen3_tts/tts/common/multi_codebook_loss.py @@ -0,0 +1,31 @@ +"""多码本 loss 契约基类。各模型负责区分时间轴 next-token 与码本轴同位监督。""" + +from typing import Optional, Sequence + +from twinkle.data_format.output import LossOutput +from twinkle.loss.base import Loss + + +class MultiCodebookLoss(Loss): + """多码本 next-token CE 基类。 + + Args: + codebook_weights: 各码本 CE 权重,None=等权(Qwen3 的残差段权重 0.3 在子类里体现)。 + ar_codebook: AR 主码本 index(该码本由主模型 next-token 预测,走标准 shift+CE)。 + """ + + require_logps = False + require_logits = True + + def __init__( + self, + codebook_weights: Optional[Sequence[float]] = None, + ar_codebook: int = 0, + ): + self.codebook_weights = codebook_weights + self.ar_codebook = ar_codebook + + def __call__(self, inputs, outputs, **kwargs) -> LossOutput: + raise NotImplementedError( + '子类实现:按模型多码本组织算 CE。Qwen3 见 tts.qwen3.loss.Qwen3TTSLoss。' + ) diff --git a/cookbook/qwen3_tts/tts/qwen3/__init__.py b/cookbook/qwen3_tts/tts/qwen3/__init__.py new file mode 100644 index 000000000..c2e53cdf9 --- /dev/null +++ b/cookbook/qwen3_tts/tts/qwen3/__init__.py @@ -0,0 +1 @@ +"""Qwen3-TTS single-card training components.""" diff --git a/cookbook/qwen3_tts/tts/qwen3/data.py b/cookbook/qwen3_tts/tts/qwen3/data.py new file mode 100644 index 000000000..c5023483e --- /dev/null +++ b/cookbook/qwen3_tts/tts/qwen3/data.py @@ -0,0 +1,190 @@ +"""Qwen3-TTS 双通道样本构建与 batch 组织。特殊 token 应从目标模型 config 读取。""" + +from dataclasses import dataclass, field +from typing import Dict, List, Optional + +import numpy as np + +NUM_CODE_GROUPS = 16 # Qwen3-TTS 12Hz codec. + + +@dataclass +class Qwen3TTSDataConfig: + """Token layout for the 12Hz model. + + The loader supplies actual IDs from the local model configuration; defaults + document the 1.7B Base layout and should not be assumed for other variants. + """ + + # 文本通道(Qwen3 文本词表段,151xxx,config 顶层 tts_*_token_id) + tts_pad_token_id: int = 151671 + tts_bos_token_id: int = 151672 + tts_eos_token_id: int = 151673 + # codec 通道(codec 词表段,2xxx,config.talker_config.codec_*_id) + codec_pad_id: int = 2148 + codec_bos_id: int = 2149 + codec_eos_token_id: int = 2150 + codec_nothink_id: int = 2155 + codec_think_bos_id: int = 2156 + codec_think_eos_id: int = 2157 + num_code_groups: int = NUM_CODE_GROUPS + + +def build_assistant_text(text: str) -> str: + """官方 `_build_assistant_text`:把目标文本包成 assistant 轮(ChatML)。 + + 真实 text_ids 由 Qwen3 文本 tokenizer 对该串编码、再 `[:, :-5]` 得到。 + 本模块只负责布局,text_ids 由上游(Preprocessor/Template)传入。 + """ + return f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n" + + +def build_qwen3_sample( + text_ids: np.ndarray, + audio_codes: np.ndarray, + cfg: Qwen3TTSDataConfig, +) -> Dict[str, np.ndarray]: + """把单条 (text_ids, audio_codes) 组成双通道 InputFeature(未 pad)。 + + Args: + text_ids: 1D int array [L],L>=4(官方 `[:, :-5]` 后的文本 token,含 assistant 标签)。 + audio_codes: 2D int array [C, num_code_groups],目标音频多码本。 + cfg: 特殊 token id。 + + Returns: + dict,每项是长度 T=8+L+C 的 numpy 数组(input_ids/codec_ids 是 2D)。 + """ + text_ids = np.asarray(text_ids).reshape(-1) + audio_codes = np.asarray(audio_codes) + assert audio_codes.ndim == 2 and audio_codes.shape[1] == cfg.num_code_groups, ( + f"audio_codes shape 应为 [C, {cfg.num_code_groups}],实得 {audio_codes.shape}" + ) + L = int(text_ids.shape[0]) + C = int(audio_codes.shape[0]) + assert L >= 4, f"text_ids_len L={L} 过短(需 >=4 以满足 [3:] 非空)" + + T = 8 + L + C + audio_codec_0 = audio_codes[:, 0] # [C] + ncb = cfg.num_code_groups + + input_ids = np.zeros((T, 2), dtype=np.int64) + codec_ids = np.zeros((T, ncb), dtype=np.int64) + codec_0_labels = np.full((T,), -100, dtype=np.int64) + text_embedding_mask = np.zeros((T,), dtype=bool) + codec_embedding_mask = np.zeros((T,), dtype=bool) + codec_mask = np.zeros((T,), dtype=bool) + attention_mask = np.zeros((T,), dtype=np.int64) + + # ---- 文本通道 input_ids[:,0] ---- + input_ids[0:3, 0] = text_ids[0:3] + input_ids[3:7, 0] = cfg.tts_pad_token_id + input_ids[7, 0] = cfg.tts_bos_token_id + input_ids[8 : 8 + L - 3, 0] = text_ids[3:] # L-3 个 + input_ids[8 + L - 3, 0] = cfg.tts_eos_token_id + input_ids[8 + L - 2 : 8 + L + C, 0] = cfg.tts_pad_token_id # [8+L-2 : T] + + # ---- codec 通道 input_ids[:,1] ---- + input_ids[3:8, 1] = [ + cfg.codec_nothink_id, + cfg.codec_think_bos_id, + cfg.codec_think_eos_id, + 0, # speaker emb 槽(id=0 占位,训练步注入 speaker_emb) + cfg.codec_pad_id, + ] + input_ids[8 : 8 + L - 3, 1] = cfg.codec_pad_id + input_ids[8 + L - 3, 1] = cfg.codec_pad_id + input_ids[8 + L - 2, 1] = cfg.codec_bos_id + input_ids[8 + L - 1 : 8 + L - 1 + C, 1] = audio_codec_0 # [8+L-1 : 8+L-1+C] + input_ids[8 + L - 1 + C, 1] = cfg.codec_eos_token_id # pos T-1 + + # ---- codec_0_labels(码本0 label,-100 mask) ---- + codec_0_labels[8 + L - 1 : 8 + L - 1 + C] = audio_codec_0 + codec_0_labels[8 + L - 1 + C] = cfg.codec_eos_token_id # pos T-1 + + # ---- codec_ids(全码本,喂 Code predictor) ---- + codec_ids[8 + L - 1 : 8 + L - 1 + C, :] = audio_codes + + # ---- masks ---- + text_embedding_mask[: 8 + L + C] = True # [:T] + codec_embedding_mask[3 : 8 + L + C] = True # [3:T] + codec_embedding_mask[6] = False # speaker emb 槽 + codec_mask[8 + L - 1 : 8 + L - 1 + C] = True # [8+L-1 : 8+L+C-1) = C 个目标帧 + attention_mask[: 8 + L + C] = True # [:T] + + return { + 'input_ids': input_ids, + 'codec_ids': codec_ids, + 'codec_0_labels': codec_0_labels, + 'text_embedding_mask': text_embedding_mask, + 'codec_embedding_mask': codec_embedding_mask, + 'codec_mask': codec_mask, + 'attention_mask': attention_mask, + 'length': np.int64(T), + } + + +def collate_qwen3_batch( + samples: List[Dict[str, np.ndarray]], + cfg: Qwen3TTSDataConfig, + ref_mels: Optional[List[np.ndarray]] = None, + max_length: Optional[int] = None, +) -> Dict[str, np.ndarray]: + """把多个 build_qwen3_sample 输出 pad 成一个 batch(对齐官方 collate_fn 的 padding)。 + + padding 规则(与官方一致): + input_ids/codec_ids -> 0;masks -> False;attention_mask -> 0;codec_0_labels -> -100。 + ref_mels 单条为 [F,128] 或 [1,F,128],同长度 stack 为 [B,F,128]。 + 不同参考长度拒绝合批;使用相同参考音频或按参考长度分批,避免无掩码补零影响池化。 + """ + b = len(samples) + if max_length is None: + max_length = int(max(int(s['length']) for s in samples)) + t = max_length + ncb = cfg.num_code_groups + + input_ids = np.zeros((b, t, 2), dtype=np.int64) + codec_ids = np.zeros((b, t, ncb), dtype=np.int64) + codec_0_labels = np.full((b, t), -100, dtype=np.int64) + text_embedding_mask = np.zeros((b, t), dtype=bool) + codec_embedding_mask = np.zeros((b, t), dtype=bool) + codec_mask = np.zeros((b, t), dtype=bool) + attention_mask = np.zeros((b, t), dtype=np.int64) + + for i, s in enumerate(samples): + Ti = int(s['length']) + input_ids[i, :Ti, :] = s['input_ids'] + codec_ids[i, :Ti, :] = s['codec_ids'] + codec_0_labels[i, :Ti] = s['codec_0_labels'] + text_embedding_mask[i, :Ti] = s['text_embedding_mask'] + codec_embedding_mask[i, :Ti] = s['codec_embedding_mask'] + codec_mask[i, :Ti] = s['codec_mask'] + attention_mask[i, :Ti] = s['attention_mask'] + + batch = { + 'input_ids': input_ids, + 'codec_ids': codec_ids, + 'codec_0_labels': codec_0_labels, + 'text_embedding_mask': text_embedding_mask[:, :, None], # [b,t,1] 对齐官方 + 'codec_embedding_mask': codec_embedding_mask[:, :, None], # [b,t,1] + 'codec_mask': codec_mask, + 'attention_mask': attention_mask, + } + if ref_mels is not None: + if len(ref_mels) != b: + raise ValueError('Each sample requires one ref_mel') + batch['ref_mels'] = np.stack(normalize_ref_mels(ref_mels), axis=0) + return batch + + +def normalize_ref_mels(ref_mels): + """Validate per-sample mel shapes without padding or merging speakers.""" + normalized = [] + for mel in ref_mels: + if mel.ndim == 3 and mel.shape[0] == 1: + mel = mel[0] + if mel.ndim != 2 or mel.shape[0] == 0 or mel.shape[1] != 128: + raise ValueError('Each ref_mel must have shape [F,128] or [1,F,128], F > 0') + normalized.append(mel) + if not normalized or any(m.shape != normalized[0].shape for m in normalized): + raise ValueError('ref_mels must have equal lengths; use a shared reference or length buckets') + return normalized diff --git a/cookbook/qwen3_tts/tts/qwen3/forward.py b/cookbook/qwen3_tts/tts/qwen3/forward.py new file mode 100644 index 000000000..58d1b1f63 --- /dev/null +++ b/cookbook/qwen3_tts/tts/qwen3/forward.py @@ -0,0 +1,88 @@ +"""Qwen3-TTS two-stage forward. Talker 使用内部时间 shift;残差码本 logits 交自定义同位 CE。 +小型替身可测试调用契约;真实模型/NPU/LoRA 仍需单独验收。""" + +from typing import Dict, Optional +import torch + + +def qwen3_tts_forward_step( + model, + batch: Dict[str, torch.Tensor], + target_speaker_embedding: Optional[torch.Tensor] = None, +): + """Qwen3-TTS 训练一步前向(产出中间态 dict,供 Qwen3TTSLoss 算总 loss)。 + + Args: + model: `Qwen3TTSForConditionalGeneration`(底层,= `Qwen3TTSModel.model`),需含顶层 + `.speaker_encoder` + `.talker`(含 `.model.codec_embedding`、`.model.text_embedding`、 + `.text_projection`、`.codec_head`、`.code_predictor`、`.forward_sub_talker_finetune`)。 + batch: collate_qwen3_batch 输出转 torch 后的张量字典(input_ids[B,T,2]、codec_ids[B,T,16]、 + text_embedding_mask[B,T,1]、codec_embedding_mask[B,T,1]、codec_mask[B,T]、 + attention_mask[B,T]、codec_0_labels[B,T]、ref_mels[B,F,128])。 + target_speaker_embedding: 首步算出的目标说话人 emb(官方写进 codec_embedding.weight[3000]; + 本项目走 clone 推理,可传 None,仅用于一致性记录)。 + + Returns: + dict: + - 'talker_loss': 码本0 的 mean CE(model.talker.forward 的 outputs.loss,标准时间轴 shift 正确)。 + - 'sub_talker_logits': code_predictor 输出 [N, 15, V](N=批次目标帧总数;15=码本1-15 头数)。 + - 'sub_talker_labels': [N, 15] = talker_codec_ids[:, 1:](码本1-15 真值,已是 head 预测目标序列)。 + - 'target_speaker_embedding': 首步缓存的 speaker emb(供外部记录)。 + 总 loss = talker_loss + 0.3 × code_predictor CE,由 Qwen3TTSLoss 从本 dict 算(不在编排里算)。 + """ + talker = model.talker + # ---- ① speaker_encoder 前置(独立前向,detach 冻结)---- + with torch.no_grad(): + speaker_embedding = model.speaker_encoder(batch['ref_mels'].to(model.dtype)) + if target_speaker_embedding is None: + target_speaker_embedding = speaker_embedding + + # ---- ② 手动组装 inputs_embeds(不走 model(input_ids=...))---- + input_text_ids = batch['input_ids'][..., 0] # [B,T] + input_codec_ids = batch['input_ids'][..., 1] # [B,T] + text_emb_mask = batch['text_embedding_mask'].to(model.dtype) + codec_emb_mask = batch['codec_embedding_mask'].to(model.dtype) + codec_ids = batch['codec_ids'] # [B,T,16] + codec_mask = batch['codec_mask'] # [B,T] + + # 0.6B: text_hidden_size=2048, hidden_size=1024. Even equal-sized + # variants must use the pretrained projection, as official inference does. + input_text_emb = talker.text_projection(talker.model.text_embedding(input_text_ids)) * text_emb_mask + input_codec_emb = talker.model.codec_embedding(input_codec_ids) * codec_emb_mask + input_codec_emb[:, 6, :] = speaker_embedding # 注入 pos6 + input_embeddings = input_text_emb + input_codec_emb + + # 叠加码本 1-15 的输入嵌入(code_predictor.get_input_embeddings() 返回 ModuleList) + cp_input_embs = talker.code_predictor.get_input_embeddings() # ModuleList[15] + for i in range(1, talker.config.num_code_groups): + e = cp_input_embs[i - 1](codec_ids[..., i]) * codec_mask.unsqueeze(-1).to(model.dtype) + input_embeddings = input_embeddings + e + + # ---- ③ 完整时间序列交 Talker;内部 ForCausalLMLoss 仅 shift 一次。---- + outputs = talker( + inputs_embeds=input_embeddings, + attention_mask=batch['attention_mask'], + labels=batch['codec_0_labels'], + output_hidden_states=True, + use_cache=False, + ) + talker_loss = outputs.loss # 码本0 mean shift+CE(HF loss_function,时间轴正确) + + # ---- ④ 第二段 forward:Code predictor 预测码本 1-15(向量化,一次喂所有目标帧)---- + # 注:forward_sub_talker_finetune 已向量化(输入 [N,16]+[N,H] → 内部 [N,16,H] 一次前向), + hidden = outputs.hidden_states[0][-1] # [B, T, H] + talker_hidden = hidden[codec_mask] # 每帧对应位置的 hidden,不含 EOS/pad + talker_codec_ids = codec_ids[codec_mask] # [N, 16] + sub_talker_logits, _sub_loss_ignored = talker.forward_sub_talker_finetune( + talker_codec_ids, talker_hidden + ) + # 忽略 _sub_loss_ignored(继承的 ForCausalLMLoss 对码本轴有 off-by-one,见 loss.py); + # Qwen3TTSLoss 用 sub_talker_logits + sub_talker_labels 自己算正确的 per-head CE。 + sub_talker_labels = talker_codec_ids[:, 1:] # [N, 15] = 码本1-15 真值 + + return { + 'talker_loss': talker_loss, + 'sub_talker_logits': sub_talker_logits, # [N, 15, V] + 'sub_talker_labels': sub_talker_labels, # [N, 15] + 'target_speaker_embedding': target_speaker_embedding, + } diff --git a/cookbook/qwen3_tts/tts/qwen3/input_processor.py b/cookbook/qwen3_tts/tts/qwen3/input_processor.py new file mode 100644 index 000000000..a13994984 --- /dev/null +++ b/cookbook/qwen3_tts/tts/qwen3/input_processor.py @@ -0,0 +1,171 @@ +"""Qwen3-TTS 单卡 padded batch processor。 + +编码样本按时间 pad 并 stack;已 collate 的 dict 直接校验并转换设备/类型。 +参考 mel 保留 [B,F,128],不同参考长度明确拒绝;不走通用 LM 字段过滤管线。 +""" + +from typing import Dict, List + +import numpy as np +import torch + +from twinkle.data_format import InputFeature +from twinkle import Platform, remote_function +from twinkle.processor.base import InputProcessor +from twinkle.utils.torch_utils import pad_and_stack_tensors +from .data import normalize_ref_mels + +# 序列字段:强制 stack(concat=False)→ [B, maxT, *] +_SEQ_STACK_FIELDS = ( + 'input_ids', 'codec_ids', 'codec_0_labels', + 'text_embedding_mask', 'codec_embedding_mask', 'codec_mask', + 'attention_mask', 'position_ids', +) +# mask 字段 stack 后补 channel 维 → [B, maxT, 1](对齐官方 collate_fn + forward 广播) +_MASK_CHANNEL_FIELDS = ('text_embedding_mask', 'codec_embedding_mask') + +# to_transformers_dict 白名单追加(基类 `_keys` 之外的自定义字段) +_EXTRA_WHITELIST = ( + 'codec_ids', 'codec_0_labels', + 'text_embedding_mask', 'codec_embedding_mask', 'codec_mask', + 'ref_mels', +) + + +def _to_tensor(v) -> torch.Tensor: + """numpy/list → torch;bool → uint8(规避 F.pad 对 bool 的版本兼容问题;值仍是 0/1)。""" + if isinstance(v, np.ndarray): + v = torch.from_numpy(v) + elif isinstance(v, list): + v = torch.tensor(v) + if v.dtype == torch.bool: + v = v.to(torch.uint8) + return v + + +class Qwen3TTSInputProcessor(InputProcessor): + """Qwen3-TTS 批组织:2D 强制 stack + ref_mels stack + 自定义字段白名单。""" + + padding_map = { + **InputProcessor.padding_map, + 'codec_ids': 0, + 'codec_0_labels': -100, + 'text_embedding_mask': False, + 'codec_embedding_mask': False, + 'codec_mask': False, + 'ref_mels': 0.0, + } + + @remote_function() + def __call__(self, inputs, **kwargs): + """Single-card, padded TTS: encoded samples or an already collated dict. + + Return one tensor dict. Do not run the generic LM preparation pipeline: + it iterates dict keys, changes ranks and filters out TTS fields. + """ + if self.padding_free or kwargs.get('enable_sp', False): + raise ValueError('Qwen3 TTS supports padded single-card batches only') + if isinstance(inputs, dict) and getattr(inputs['input_ids'], 'ndim', None) == 3: + batch = self.to_transformers_dict([inputs])[0] + else: + samples = [inputs] if isinstance(inputs, dict) else inputs + if not samples: + raise ValueError('Empty TTS batch') + batch = self.to_transformers_dict(self.collate_fn(samples))[0] + required = set(_EXTRA_WHITELIST) | {'input_ids', 'attention_mask'} + missing = required - batch.keys() + if missing: + raise ValueError(f'Missing TTS fields: {sorted(missing)}') + batch = {key: torch.as_tensor(value) for key, value in batch.items()} + b, t, channels = batch['input_ids'].shape + if channels != 2 or b == 0 or t == 0: + raise ValueError('input_ids must have shape [B,T,2]') + shapes = {'codec_ids': (b, t, 16), 'codec_0_labels': (b, t), + 'attention_mask': (b, t), 'codec_mask': (b, t), + 'text_embedding_mask': (b, t, 1), 'codec_embedding_mask': (b, t, 1)} + for key, shape in shapes.items(): + if tuple(batch[key].shape) != shape: + raise ValueError(f'{key} must have shape {shape}') + mel = batch['ref_mels'] + if mel.ndim != 3 or mel.shape[0] != b or mel.shape[1] == 0 or mel.shape[2] != 128: + raise ValueError('ref_mels must have shape [B,F,128], F > 0') + if kwargs.get('micro_batch_size') not in (None, b): + raise ValueError('Split TTS micro-batches in the DataLoader') + model = kwargs.get('model') + device = getattr(model, 'device', None) + if device is None: + device = Platform.get_local_device() + for key, value in batch.items(): + dtype = (torch.float32 if key == 'ref_mels' else torch.bool + if key in (*_MASK_CHANNEL_FIELDS, 'codec_mask') else torch.long) + batch[key] = value.to(device=device, dtype=dtype) + return batch + + def collate_fn(self, inputs, micro_batch_size=None, variable_seq_lengths=False, **kwargs): + seq_out: Dict[str, torch.Tensor] = {} + + for key in _SEQ_STACK_FIELDS: + vals: List[torch.Tensor] = [] + for inp in inputs: + v = inp.get(key) + if v is None: + continue + v = _to_tensor(v) + # 撤销 prepare_inputs 对 1D 字段的 unsqueeze([1,T]→[T]); + # 2D 字段([T,2]/[T,16])shape[0]=T≠1 不受影响。 + if key not in ('input_ids', 'codec_ids') and v.dim() == 2 and v.shape[0] == 1: + v = v.squeeze(0) + vals.append(v) + if not vals: + continue + pad = int(self.padding_map.get(key, 0)) + if len(vals) == 1: + stacked = vals[0].unsqueeze(0) # 保证有 batch 维 + else: + stacked = pad_and_stack_tensors(vals, pad_value=pad, concat=False) + if key in _MASK_CHANNEL_FIELDS: + stacked = stacked.unsqueeze(-1) # [B, maxT, 1] + seq_out[key] = stacked + + # 保留每条参考的 batch 归属;无长度掩码时不自动补零。 + ref_mels: List[torch.Tensor] = [] + for inp in inputs: + v = inp.get('ref_mels') + if v is None: + continue + v = _to_tensor(v) + ref_mels.append(v) + if len(ref_mels) != len(inputs): + raise ValueError('Each sample requires one ref_mel') + seq_out['ref_mels'] = torch.stack(normalize_ref_mels(ref_mels), dim=0) + + # length:标量元数据,取 batch 内最大 + lengths = [int(inp.get('length')) for inp in inputs if inp.get('length') is not None] + if lengths: + seq_out['length'] = max(lengths) + + return [InputFeature(**seq_out)] + + @staticmethod + def to_transformers_dict(inputs, **kwargs): + results = [] + # 基类白名单 + 本模型自定义字段(基类 _keys 见 processor/base.py:663-680) + _keys = { + 'input_ids', 'inputs_embeds', 'attention_mask', 'position_ids', 'labels', + 'completion_mask', 'cu_seq_lens_q', 'cu_seq_lens_k', 'cu_seqlens_q', + 'cu_seqlens_kv', 'max_length_q', 'max_length_k', 'packed_seq_params', + 'routed_experts', 'mm_token_type_ids', 'second_per_grid_ts', + } | set(InputProcessor.VLM_CONCAT_FIELDS) | set(_EXTRA_WHITELIST) + + for _input in inputs: + output = {} + for key in list(_input.keys()): + if key not in _keys: + continue + value = _input[key] + if isinstance(value, torch.Tensor) or not isinstance(value, (list, np.ndarray)): + output[key] = value + else: + output[key] = np.array(value) + results.append(InputFeature(**output)) + return results diff --git a/cookbook/qwen3_tts/tts/qwen3/loss.py b/cookbook/qwen3_tts/tts/qwen3/loss.py new file mode 100644 index 000000000..82ee84854 --- /dev/null +++ b/cookbook/qwen3_tts/tts/qwen3/loss.py @@ -0,0 +1,74 @@ +"""Qwen3-TTS 两段 loss:Talker 时间轴 CE + 0.3 倍残差码本同位 CE。 +源码依据:Qwen3-TTS 022e286。其 residual logits 与 cb1..cb15 已对齐,再 shift 会丢 cb1 的直接监督, +并让最后 head 无直接 CE;cb15 标签会被错配给前一 head。此结论不代表官方权重的训练方式或音质收益。""" + +import torch +import torch.nn.functional as F + +from twinkle.data_format import LossOutput + +from tts.common import MultiCodebookLoss + + +def _shift_ce_like_official(logits, labels, ignore_index=-100): + """复刻 transformers ForCausalLMLoss 的 shift 语义(含 off-by-one,仅供 shift_labels=True 对照)。 + + logits [N, C, V],labels [N, C]。 + """ + from transformers.loss.loss_utils import ForCausalLMLoss + + return ForCausalLMLoss( + logits=logits, + labels=labels, + vocab_size=logits.shape[-1], + ignore_index=ignore_index, + ) + + +class Qwen3TTSLoss(MultiCodebookLoss): + """Qwen3-TTS 2 段式 loss。 + + Args: + sub_loss_weight: code_predictor 段权重(官方 0.3)。 + shift_labels: code_predictor 段是否做标准 LM shift。 + False(默认)= 修正 off-by-one,同位 per-head CE(无 shift); + True = 复现官方 ForCausalLMLoss 含 shift(仅用于对照)。 + ignore_index: 忽略的 label 值(非音频帧 / pad)。 + """ + + def __init__( + self, + sub_loss_weight: float = 0.3, + shift_labels: bool = False, + ignore_index: int = -100, + ar_codebook: int = 0, + ): + super().__init__(ar_codebook=ar_codebook) + self.sub_loss_weight = sub_loss_weight + self.shift_labels = shift_labels + self.ignore_index = ignore_index + + def __call__(self, inputs, outputs, **kwargs) -> LossOutput: + # talker 段(码本0):透传编排算好的 mean CE(标准时间轴 shift 正确) + talker_loss = outputs['talker_loss'] + # code_predictor 段(码本1-15) + sub_logits = outputs['sub_talker_logits'] # [N, 15, V] + sub_labels = outputs['sub_talker_labels'] # [N, 15] + + if self.shift_labels: + sub_loss = _shift_ce_like_official(sub_logits, sub_labels, self.ignore_index) + else: + V = sub_logits.shape[-1] + # 同位 per-head CE(无 shift):logits[:,j] 预测码本 j+1 ↔ labels[:,j]=码本 j+1 + sub_loss = F.cross_entropy( + sub_logits.reshape(-1, V), + sub_labels.reshape(-1), + ignore_index=self.ignore_index, + reduction='mean', + ) + + total = talker_loss + self.sub_loss_weight * sub_loss + # 两段均 per-token mean(镜像官方 sft_12hz.py:outputs.loss 与 sub_loss 都是 mean)。 + # num_tokens=0 走 Twinkle PER-TOKEN-MEAN 归一(grad_accum 路径), + # 与 accelerate.backward(loss)+gradient_accumulation_steps 一致。 + return LossOutput(loss=total, num_tokens=0) diff --git a/cookbook/qwen3_tts/tts/qwen3/manifest.py b/cookbook/qwen3_tts/tts/qwen3/manifest.py new file mode 100644 index 000000000..7894f4c87 --- /dev/null +++ b/cookbook/qwen3_tts/tts/qwen3/manifest.py @@ -0,0 +1,71 @@ +"""Validated pre-encoded JSONL and deterministic per-epoch sample plans.""" +import hashlib +import json +from pathlib import Path +import random +import numpy as np + + +class Manifest: + def __init__(self, path): + self.path = Path(path).resolve(strict=True) + self.rows = [] + ids = set() + digest = hashlib.sha256() + for line in self.path.read_text(encoding='utf-8').splitlines(): + if not line.strip(): + continue + row = json.loads(line) + key = row['id'] + if not isinstance(key, str) or not key or key in ids: + raise ValueError('Manifest IDs must be unique nonempty strings') + ids.add(key) + if not isinstance(row['text'], str) or not row['text'].strip(): + raise ValueError('Empty/non-string transcript') + codes = np.asarray(row['audio_codes']) + if codes.ndim != 2 or codes.shape[0] == 0 or codes.shape[1] != 16 or codes.dtype.kind not in 'iu': + raise ValueError('audio_codes must be nonempty integer [T,16]') + if (codes < 0).any() or (codes >= 2048).any(): + raise ValueError('12Hz audio codes must be in [0,2048)') + mel_path = (self.path.parent / row['ref_mel_path']).resolve(strict=True) + mel = np.load(mel_path, allow_pickle=False) + if mel.ndim != 2 or mel.shape[1] != 128 or mel.shape[0] == 0 or not np.isfinite(mel).all(): + raise ValueError('ref mel must be finite [F,128]') + content = {k: row[k] for k in ('id', 'text', 'audio_codes')} + content['mel_sha256'] = hashlib.sha256(mel_path.read_bytes()).hexdigest() + digest.update(json.dumps(content, sort_keys=True, ensure_ascii=False).encode('utf-8') + b'\n') + self.rows.append({**row, 'ref_mel_path': str(mel_path)}) + if not self.rows: + raise ValueError('Manifest must not be empty') + self.fingerprint = digest.hexdigest() + + def __len__(self): + return len(self.rows) + + +def sample_plan(count, epochs, seed, shuffle): + if count < 1 or epochs < 1: + raise ValueError('Dataset and epochs must be positive') + plan = [] + for epoch in range(epochs): + order = list(range(count)) + if shuffle: + random.Random(seed + epoch).shuffle(order) # does not consume training RNG + plan.append(order) + return plan + + +def planned_windows(plan, accumulation_steps): + if accumulation_steps < 1: + raise ValueError('accumulation_steps must be positive') + # Flush a short window at each epoch boundary, never drop or carry it over. + return [order[i:i + accumulation_steps] for order in plan + for i in range(0, len(order), accumulation_steps)] + + +def validate_cursor(progress, plan_windows): + if not 0 <= progress.optimizer_steps <= len(plan_windows): + raise ValueError('Optimizer cursor outside sample plan') + consumed = sum(len(w) for w in plan_windows[:progress.optimizer_steps]) + if progress.pending_micro_steps or progress.micro_steps != consumed or progress.consumed_samples != consumed: + raise ValueError('Checkpoint sample cursor does not match plan boundary') diff --git a/cookbook/qwen3_tts/tts/qwen3/model.py b/cookbook/qwen3_tts/tts/qwen3/model.py new file mode 100644 index 000000000..9104ee92e --- /dev/null +++ b/cookbook/qwen3_tts/tts/qwen3/model.py @@ -0,0 +1,80 @@ +'Twinkle TransformersModel bridge for the Qwen3-TTS two-stage forward and loss.' + +from copy import copy + +from twinkle import remote_function +from twinkle.infra import collect_tensor_dict +from twinkle.model.transformers import TransformersModel + +from .forward import qwen3_tts_forward_step +from .input_processor import Qwen3TTSInputProcessor + + +class Qwen3TTSModel(TransformersModel): + """Override `forward` 走 Qwen3-TTS 2 段编排 + stash 中间态。 + + `calculate_loss` / `backward` / `clip_grad_norm` 等复用基类(保留梯度累积/clip/metric 抽象 + + 多 LoRA 透传)。cookbook 仍调 `model.forward_backward(inputs=batch)`,编排全在本类内。 + """ + + def _construct_default_optimizer_group(self): + # Keep current Twinkle's group/metrics lifecycle, but pass tokenizer + # policy at construction (including groups created for new adapters). + from twinkle.model.transformers.transformers import OptimizerGroup + from .tokenization import local_template + from .loss import Qwen3TTSLoss + return OptimizerGroup( + loss_instance=Qwen3TTSLoss(), + template=local_template(self.tokenizer_id), + processor=Qwen3TTSInputProcessor(device_mesh=self.device_mesh), + _device_mesh=self.device_mesh, + ) + + @remote_function(dispatch='slice_dp', collect=collect_tensor_dict) + def forward(self, *, inputs, **kwargs): + adapter_name = kwargs.pop('adapter_name', self._get_default_group()) + # task 不用 causal_lm,但 pop 掉避免传给下游 qwen3_tts_forward_step + kwargs.pop('task', None) + kwargs.pop('temperature', 1.0) + kwargs.pop('sampling_masks', None) + kwargs.pop('return_logits', False) + optimizer_config = self.optimizer_group[adapter_name] + self._lazy_wrap_model() + if not inputs: + raise ValueError('inputs empty, check your DataLoader outputs') + self.model.train() + + # 1. Trajectory / List[Trajectory] → InputFeature(若未编码) + if (isinstance(inputs, dict) and self._not_encoded(inputs)) or ( + isinstance(inputs, list) and self._not_encoded(inputs[0]) + ): + assert optimizer_config.template is not None, \ + 'Use set_template to add a template when trying to input `List[Trajectory]`' + if isinstance(inputs, dict): + inputs = [inputs] + inputs = optimizer_config.template.batch_encode(inputs) + + # 2. 专用 processor 保留 TTS 字段和 batch 维度。 + processor = optimizer_config.processor + if not isinstance(processor, Qwen3TTSInputProcessor): + raise TypeError('Call set_processor(Qwen3TTSInputProcessor) before TTS forward') + inputs = processor( + inputs, + sp_strategy=self.sp_strategy, + model=self.model, + hf_config=self.hf_config, + enable_sp=getattr(self, '_enable_sp', False), + ) + + # 3. Qwen3-TTS 2 段前向编排(替换基类的 `self.model(**inputs)`) + # 产出中间态 dict:talker_loss / sub_talker_logits / sub_talker_labels / + # target_speaker_embedding。 + outputs = qwen3_tts_forward_step(self.model, inputs) + + # 4. stash 中间态进 train_status.outputs,供 Qwen3TTSLoss 从 outputs 读(calculate_loss 复用基类) + status = optimizer_config.train_status + status.inputs = inputs + status.outputs = outputs + status.forward_kwargs = kwargs + status.loss_value = 0 + return copy(outputs) diff --git a/cookbook/qwen3_tts/tts/qwen3/runtime.py b/cookbook/qwen3_tts/tts/qwen3/runtime.py new file mode 100644 index 000000000..6a4d13850 --- /dev/null +++ b/cookbook/qwen3_tts/tts/qwen3/runtime.py @@ -0,0 +1,92 @@ +'Local Qwen3-TTS loading and assertions used by the single-card integration tests.' +import hashlib +import torch + +from .data import Qwen3TTSDataConfig + +DEFAULT_MODEL_PATH = '/models/Qwen3-TTS-12Hz-1.7B-Base' +ADAPTER = 'smoke' + + +def data_config(config): + tc = config.talker_config + return Qwen3TTSDataConfig( + **{name: getattr(config, name) for name in + ('tts_pad_token_id', 'tts_bos_token_id', 'tts_eos_token_id')}, + **{name: getattr(tc, name) for name in + ('codec_pad_id', 'codec_bos_id', 'codec_eos_token_id', + 'codec_nothink_id', 'codec_think_bos_id', 'codec_think_eos_id', + 'num_code_groups')}) + + +def load_twinkle_model(path, device_mesh): + # Twinkle constructs a Template (AutoConfig/AutoProcessor) as well as the + # supplied model_cls. Passing config alone does not register these classes. + from transformers import AutoConfig, AutoModel, AutoProcessor + from qwen_tts.core.models import Qwen3TTSConfig, Qwen3TTSForConditionalGeneration, Qwen3TTSProcessor + from .model import Qwen3TTSModel + AutoConfig.register('qwen3_tts', Qwen3TTSConfig, exist_ok=True) + AutoModel.register(Qwen3TTSConfig, Qwen3TTSForConditionalGeneration, exist_ok=True) + AutoProcessor.register(Qwen3TTSConfig, Qwen3TTSProcessor, exist_ok=True) + model = Qwen3TTSModel( + model_id=str(path), model_cls=Qwen3TTSForConditionalGeneration, + config=Qwen3TTSConfig.from_pretrained(path, local_files_only=True), device_mesh=device_mesh, + mixed_precision='bf16', torch_dtype=torch.bfloat16, + attn_implementation='eager', local_files_only=True) + # A single short sample does not need activation checkpointing. In + # particular, reentrant checkpointing can sever gradients from frozen inputs. + model.model.gradient_checkpointing_disable() + return model + + +def trainable_snapshot(model): + params = {n: p for n, p in model.named_parameters() if p.requires_grad} + if not params or any('.lora_' not in n for n in params): + raise AssertionError('Only LoRA parameters may be trainable') + for branch in ('.talker.model.', '.talker.code_predictor.'): + if not any(branch in n for n in params): + raise AssertionError('Missing trainable branch: ' + branch) + return {n: p.detach().cpu().clone() for n, p in params.items()} + + +def frozen_digest(model): + """Hash all frozen weights, one tensor at a time; do not retain a base copy.""" + digest = hashlib.sha256() + for name, parameter in model.named_parameters(): + if parameter.requires_grad: + continue + digest.update(name.encode()) + value = parameter.detach().cpu().contiguous() + digest.update(str((tuple(value.shape), value.dtype)).encode()) + digest.update(value.view(torch.uint8).numpy().tobytes()) + return digest.hexdigest() + + +def assert_gradients(model): + nonzero = [] + for name, p in model.named_parameters(): + if not p.requires_grad: + if p.grad is not None: + raise AssertionError('Frozen parameter has gradient: ' + name) + continue + if p.grad is None or not torch.isfinite(p.grad).all(): + raise AssertionError('Missing/nonfinite LoRA gradient: ' + name) + if torch.count_nonzero(p.grad).item(): + nonzero.append(name) + # Standard LoRA starts B at zero: A can legitimately have zero gradient. + for branch in ('.talker.model.', '.talker.code_predictor.'): + if not any(branch in name for name in nonzero): + raise AssertionError('No nonzero gradient in ' + branch) + return nonzero + + +def assert_updated(before, after): + if before.keys() != after.keys(): + raise AssertionError('Trainable parameter set changed') + changed = [name for name in before if not torch.equal(before[name], after[name])] + if any(not torch.isfinite(value).all() for value in after.values()): + raise AssertionError('Nonfinite updated weights') + for branch in ('.talker.model.', '.talker.code_predictor.'): + if not any(branch in name for name in changed): + raise AssertionError('No parameter update in ' + branch) + return changed diff --git a/cookbook/qwen3_tts/tts/qwen3/speech_fixture.py b/cookbook/qwen3_tts/tts/qwen3/speech_fixture.py new file mode 100644 index 000000000..5bcefbe7c --- /dev/null +++ b/cookbook/qwen3_tts/tts/qwen3/speech_fixture.py @@ -0,0 +1,23 @@ +"""Load a hash-verified public regression sample; no download or customer data.""" +import hashlib +import json +from pathlib import Path +import numpy as np + + +def load_speech_fixture(directory): + import soundfile as sf + from scipy.signal import resample_poly + directory = Path(directory) + metadata = json.loads((directory / 'sample.json').read_text(encoding='utf-8')) + path = directory / metadata['audio'] + if hashlib.sha256(path.read_bytes()).hexdigest() != metadata['sha256']: + raise ValueError('Speech fixture SHA256 mismatch') + wave, sr = sf.read(path, dtype='float32', always_2d=False) + if sr != 16000 or wave.ndim != 1 or len(wave) == 0 or not np.isfinite(wave).all(): + raise ValueError('Expected finite mono 16 kHz LibriSpeech fixture') + if not metadata['text'].strip(): + raise ValueError('Speech fixture transcript is empty') + # Exact rational rate conversion; preserve the full utterance and transcript. + wave = resample_poly(wave, 3, 2).astype(np.float32) + return wave, metadata['text'], metadata diff --git a/cookbook/qwen3_tts/tts/qwen3/tokenization.py b/cookbook/qwen3_tts/tts/qwen3/tokenization.py new file mode 100644 index 000000000..f68e99ab6 --- /dev/null +++ b/cookbook/qwen3_tts/tts/qwen3/tokenization.py @@ -0,0 +1,21 @@ +"""One local-only tokenizer policy for training, templates and adapter reload.""" + +def load_text_tokenizer(path): + from transformers import AutoTokenizer + return AutoTokenizer.from_pretrained(str(path), local_files_only=True, fix_mistral_regex=True) + + +def local_template(path): + from twinkle.template import Template + return Template(str(path), local_files_only=True, fix_mistral_regex=True) + + +def assert_tokenizers_match(reference, others, texts): + """Compare actual IDs, and ensure the reference preserves input text.""" + for text in texts: + ids = reference.encode(text, add_special_tokens=False) + if reference.decode(ids, skip_special_tokens=False, clean_up_tokenization_spaces=False) != text: + raise AssertionError('Tokenizer does not preserve probe text') + for tokenizer in others: + if tokenizer.encode(text, add_special_tokens=False) != ids: + raise AssertionError('Tokenizer IDs differ across training/template/reload') diff --git a/cookbook/qwen3_tts/tts/qwen3/training.py b/cookbook/qwen3_tts/tts/qwen3/training.py new file mode 100644 index 000000000..5854dc6fd --- /dev/null +++ b/cookbook/qwen3_tts/tts/qwen3/training.py @@ -0,0 +1,112 @@ +'Single-card accumulation and update-boundary checkpoints. The caller owns epoch shuffling.' +from dataclasses import asdict, dataclass +import json +import math +from pathlib import Path + + +@dataclass +class Progress: + accumulation_steps: int = 2 + micro_steps: int = 0 + optimizer_steps: int = 0 + consumed_samples: int = 0 + pending_micro_steps: int = 0 + + def __post_init__(self): + if self.accumulation_steps < 1: + raise ValueError('accumulation_steps must be positive') + + +def windows(batches, size): + if size < 1: + raise ValueError('window size must be positive') + window = [] + for batch in batches: + window.append(batch) + if len(window) == size: + yield window + window = [] + if window: + yield window + + +def train_window(model, batches, progress, adapter_name, before_micro=None): + """Exactly one optimizer/scheduler update for a full or final short window.""" + batches = list(batches) + if not 1 <= len(batches) <= progress.accumulation_steps: + raise ValueError('Expected one nonempty accumulation window') + group = model.optimizer_group[adapter_name] + if group.gradient_accumulation_steps != 1: + raise ValueError('Twinkle accumulation must be 1; cookbook owns boundaries') + if progress.pending_micro_steps or group.train_status.num_tokens: + raise ValueError('Previous accumulation window is incomplete') + if group.cur_step != progress.micro_steps: + raise ValueError('Twinkle and cookbook micro-step counts differ') + # Validate the entire window before executing any backward operation. + for batch in batches: + if batch['input_ids'].shape[0] != 1: + raise ValueError('Only single-sample micro-batches are supported') + losses = [] + for batch in batches: + if before_micro is not None: + before_micro(progress.micro_steps) + result = model.forward_backward(inputs=batch, adapter_name=adapter_name) + if not math.isfinite(float(result['loss'])): + raise FloatingPointError('Nonfinite micro-batch loss; abort this run') + losses.append(float(result['loss'])) + progress.micro_steps += 1 + progress.consumed_samples += 1 + progress.pending_micro_steps += 1 + # Each Qwen loss is a mean and reports num_tokens=0. Twinkle counts each + # as 1, so clipping normalizes by the ACTUAL window size, including tails. + if group.train_status.num_tokens != len(batches): + raise AssertionError('Unexpected loss normalization count') + model.clip_grad_and_step(adapter_name=adapter_name) + progress.optimizer_steps += 1 + progress.pending_micro_steps = 0 + return losses + + +def save_boundary(model, progress, output_dir, name, adapter_name, sample_fingerprint): + group = model.optimizer_group[adapter_name] + if progress.pending_micro_steps or group.train_status.num_tokens: + raise ValueError('Cannot save with unstepped gradients') + if group.cur_step != progress.micro_steps: + raise ValueError('Micro-step count mismatch') + if any(p.grad is not None for p in model.model.parameters()): + raise ValueError('Checkpoint requires cleared gradients') + checkpoint = Path(model.save(name, output_dir=str(output_dir), adapter_name=adapter_name, + save_optimizer=True, consumed_train_samples=progress.consumed_samples)) + (checkpoint / 'cookbook_progress.json').write_text(json.dumps({ + 'format_version': 1, 'progress': asdict(progress), + 'sample_fingerprint': sample_fingerprint}, indent=2), encoding='utf-8') + return checkpoint + + +def resume_boundary(model, checkpoint, adapter_name, sample_fingerprint, accumulation_steps): + """Restore only complete locally produced checkpoints, never silently restart.""" + checkpoint = Path(checkpoint) + for name in ('adapter_config.json', 'adapter_model.safetensors', 'optimizer.pt', + 'scheduler.pt', 'rng_state.pt', 'trainer_state.json', 'cookbook_progress.json'): + if not (checkpoint / name).is_file(): + raise FileNotFoundError(checkpoint / name) + state = json.loads((checkpoint / 'cookbook_progress.json').read_text(encoding='utf-8')) + if state['format_version'] != 1 or state['sample_fingerprint'] != sample_fingerprint: + raise ValueError('Checkpoint format/sample mismatch') + progress = Progress(**state['progress']) + if progress.pending_micro_steps or progress.accumulation_steps != accumulation_steps: + raise ValueError('Checkpoint boundary/accumulation mismatch') + trainer = json.loads((checkpoint / 'trainer_state.json').read_text(encoding='utf-8')) + if (trainer['cur_step'] != progress.micro_steps + or trainer['consumed_train_samples'] != progress.consumed_samples + or trainer['gradient_accumulation_steps'] != 1): + raise ValueError('Twinkle checkpoint progress disagrees with cookbook') + group = model.optimizer_group[adapter_name] + if getattr(group, 'optimizer', None) is None or getattr(group, 'lr_scheduler', None) is None: + raise ValueError('Create optimizer and scheduler before resuming') + model.resume_from_checkpoint(str(checkpoint), adapter_name=adapter_name) + group = model.optimizer_group[adapter_name] + if group.cur_step != progress.micro_steps or group.gradient_accumulation_steps != 1: + raise AssertionError('Twinkle did not restore micro-step state') + return progress