gemma1b-tts-integration / scripts /mvp_aligned.py
marcos
Aligned NAR teacher-distill pipeline (forced-align + overfit/MVP + dataset gen)
5d2c572
Raw
History Blame Contribute Delete
6.58 kB
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import torch
from scripts.overfit_aligned import PHONE_VOCAB, AlignedSesameAR, frame_phone_tensor, load_rows
from speech_bridge_gemma.mimi_overfit import compute_accuracy, compute_loss, selected_decoder_layers
from speech_bridge_gemma.qwen3_tts_tokenizer_smoke import ensure_audio_frame_token, synthesis_condition_text, tokenize_qwen3_batch
def pad_batch(rows: list[dict[str, Any]], audio_slots: int, num_quantizers: int, device: str) -> tuple[torch.Tensor, torch.Tensor]:
b = len(rows)
targets = torch.full((b, audio_slots, num_quantizers), -100, dtype=torch.long)
phones = torch.zeros((b, audio_slots), dtype=torch.long)
for i, r in enumerate(rows):
f = r["frames"]
targets[i, :f] = r["codes"].transpose(0, 1)[:f]
phones[i] = frame_phone_tensor(r["align"], audio_slots)
return targets.to(device), phones.to(device)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--codes-pt", required=True)
parser.add_argument("--align-pt", required=True)
parser.add_argument("--out-dir", required=True)
parser.add_argument("--llm-model", default="Qwen/Qwen3.5-0.8B")
parser.add_argument("--max-rows", type=int, default=3600)
parser.add_argument("--heldout", type=int, default=200)
parser.add_argument("--steps", type=int, default=4000)
parser.add_argument("--batch-size", type=int, default=8)
parser.add_argument("--lr", type=float, default=4e-4)
parser.add_argument("--backbone-lr", type=float, default=2e-5)
parser.add_argument("--train-last-n-layers", type=int, default=8)
parser.add_argument("--num-quantizers", type=int, default=16)
parser.add_argument("--codebook-size", type=int, default=2048)
parser.add_argument("--speech-conditioning", default="prompt_answer")
parser.add_argument("--save-steps", type=int, default=1000)
parser.add_argument("--device", default="cuda")
args = parser.parse_args()
import torch.nn as nn
from transformers import AutoModelForCausalLM, AutoTokenizer
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
rows, max_frames = load_rows(args.codes_pt, args.align_pt, args.max_rows)
audio_slots = max_frames + 2
held = rows[-args.heldout:] if args.heldout else []
train = rows[: len(rows) - len(held)]
print(json.dumps({"event": "data", "train": len(train), "held": len(held), "audio_slots": audio_slots}), flush=True)
tokenizer = AutoTokenizer.from_pretrained(args.llm_model, trust_remote_code=True)
llm = AutoModelForCausalLM.from_pretrained(args.llm_model, torch_dtype=torch.bfloat16, trust_remote_code=True, low_cpu_mem_usage=True).to(args.device)
llm.config.use_cache = False
frame_token_id = ensure_audio_frame_token(tokenizer, llm)
for p in llm.parameters():
p.requires_grad_(False)
backbone_params: list[nn.Parameter] = []
if args.train_last_n_layers:
for layer in selected_decoder_layers(llm, args.train_last_n_layers):
for p in layer.parameters():
p.requires_grad_(True)
backbone_params.append(p)
model = AlignedSesameAR(llm=llm, frame_token_id=frame_token_id, audio_slots=audio_slots, num_quantizers=args.num_quantizers, codebook_size=args.codebook_size, depth_dim=512, depth_layers=4, depth_heads=8, depth_ff_mult=4).to(args.device)
model.train()
groups = [{"params": model.audio_parameters(), "lr": args.lr}]
if backbone_params:
groups.append({"params": backbone_params, "lr": args.backbone_lr})
optimizer = torch.optim.AdamW(groups)
order = list(range(len(train)))
step = 0
running = 0.0
cursor = 0
while step < args.steps:
step += 1
if cursor + args.batch_size > len(order):
cursor = 0
order = order[::-1]
batch_rows = [train[order[cursor + k]] for k in range(args.batch_size)]
cursor += args.batch_size
conds = [synthesis_condition_text(r["question"], r["answer"], args.speech_conditioning) for r in batch_rows]
tok = tokenize_qwen3_batch(tokenizer, conds, audio_slots, args.device, True)
targets, phones = pad_batch(batch_rows, audio_slots, args.num_quantizers, args.device)
model.set_align(phones)
logits = model(tok["input_ids"], tok["attention_mask"], targets=targets)
loss = compute_loss(logits=logits, targets=targets, codebook_size=args.codebook_size, first_codebook_weight=1.5, depth_weight=1.0)
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_([p for g in groups for p in g["params"]], 1.0)
optimizer.step()
running += float(loss.detach().cpu())
if step % 100 == 0 or step == 1:
with torch.no_grad():
acc, per_q = compute_accuracy(logits.detach(), targets)
print(json.dumps({"step": step, "loss": round(running / (100 if step % 100 == 0 else 1), 4), "acc": round(acc, 4), "cb0": round(per_q[0], 4)}), flush=True)
running = 0.0
if args.save_steps and step % args.save_steps == 0:
torch.save({"audio_head": model.audio_head.state_dict(), "phone_embed": model.phone_embed.state_dict()}, out_dir / "aligned_head.pt")
torch.save({"audio_head": model.audio_head.state_dict(), "phone_embed": model.phone_embed.state_dict()}, out_dir / "aligned_head.pt")
model.eval()
pred_dir = out_dir / "heldout_pred"
pred_dir.mkdir(exist_ok=True)
for r in held:
phones = frame_phone_tensor(r["align"], audio_slots).unsqueeze(0).to(args.device)
cond = synthesis_condition_text(r["question"], r["answer"], args.speech_conditioning)
tok = tokenize_qwen3_batch(tokenizer, [cond], audio_slots, args.device, True)
model.set_align(phones)
with torch.inference_mode():
logits = model(tok["input_ids"], tok["attention_mask"], frames=r["frames"])
codes = logits[0, : r["frames"]].argmax(dim=-1).transpose(0, 1).contiguous().cpu()
torch.save(codes, pred_dir / f"{r['id']}_codes.pt")
(pred_dir / f"{r['id']}.json").write_text(json.dumps({"id": r["id"], "expected": r["answer"]}, ensure_ascii=False), encoding="utf-8")
print(json.dumps({"event": "heldout_predicted", "n": len(held), "dir": str(pred_dir)}), flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())