"""Full teacher-vs-student benchmark on identical inputs. Paths measured per identical input: teacher_pure : stock compute path (layer_norm -> extract_features -> mean -> proj -> softmax) teacher_pipeline : stock funasr user path (AutoModel.generate, granularity=frame) student_total : SurgicalEmotionEncoder end-to-end (frozen backbone + adapter, 12.5 Hz outputs) adapter_only : adapter on precomputed teacher states (marginal surgical cost) Quality (same inputs, both models): top-1 agreement, JS divergence of utterance probs, pooled-embedding cosine, frame-level cosine (student upsampled to 50 Hz vs teacher states), intensity stats + correlation with conf-gated energy target. Scaling: durations 1/2/3/4/10 s, adapter batch 1/4/8, fp16 autocast variant, small CPU comparison. Saves benchmark_report.json. """ import glob import json import os import sys import time import warnings warnings.filterwarnings("ignore") import logging logging.disable(logging.CRITICAL) sys.path.insert(0, "/tmp/work/s2st_emotion") import numpy as np import torch import torch.nn.functional as F import torchaudio from funasr import AutoModel from surgical_emotion import SurgicalConfig, SurgicalEmotionEncoder FUNASR_DIR = "/tmp/work/emotion2vec_plus_base" CKPT = "/tmp/work/surgical_out/surgical_emotion_module.pt" OUT = "/tmp/work/surgical_out/benchmark_report.json" DEV = "cuda:0" LABELS = ["angry", "disgusted", "fearful", "happy", "neutral", "other", "sad", "surprised", "unk"] EMO_FIELD = {"01": "neutral", "02": "calm", "03": "happy", "04": "sad", "05": "angry", "06": "fearful", "07": "disgust", "08": "surprised"} report = {"gpu": torch.cuda.get_device_name(0), "torch": torch.__version__, "device": DEV, "labels": LABELS, "paths": {}, "files": {}, "scaling": {}, "quality_summary": {}} def load_wav(path, max_s=12.0): wav, sr = torchaudio.load(path) wav = wav.mean(0) if sr != 16000: wav = torchaudio.functional.resample(wav, sr, 16000) wav = wav[: int(max_s * 16000)] return wav / wav.abs().max().clamp(min=1e-4) def js_div(p, q, eps=1e-8): p = p.clamp(min=eps) q = q.clamp(min=eps) m = 0.5 * (p + q) kl = lambda a, b: float((a * (a.log() - b.log())).sum(-1)) return 0.5 * kl(p, m) + 0.5 * kl(q, m) def rms_env(wav, T): frames = torch.zeros(T) for i in range(T): seg = wav[i * 320: (i + 1) * 320] frames[i] = float(seg.pow(2).mean().sqrt()) if seg.numel() else (frames[i - 1] if i else 0.0) if T >= 9: frames = F.conv1d(frames[None, None], torch.ones(9)[None, None] / 9.0, padding=4)[0, 0] lo, hi = frames.min(), frames.max() return torch.full_like(frames, 0.5) if hi - lo < 1e-6 else (frames - lo) / (hi - lo) def timeit(fn, warmup=3, reps=10): for _ in range(warmup): fn() torch.cuda.synchronize() ts = [] for _ in range(reps): t0 = time.perf_counter() fn() torch.cuda.synchronize() ts.append(time.perf_counter() - t0) return {"mean_ms": 1000 * float(np.mean(ts)), "std_ms": 1000 * float(np.std(ts)), "min_ms": 1000 * float(np.min(ts))} print("=== loading models (shared teacher backbone) ===", flush=True) fun = AutoModel(model=FUNASR_DIR, trust_remote_code=False, device=DEV, disable_update=True) teacher = fun.model.eval() enc = SurgicalEmotionEncoder(teacher, SurgicalConfig()).to(DEV) payload = torch.load(CKPT, map_location=DEV, weights_only=False) enc.adapter.load_state_dict(payload["adapter_state"]) enc.eval() n_teacher = sum(p.numel() for p in teacher.parameters()) n_adapter = sum(p.numel() for p in enc.adapter.parameters()) report["params"] = {"teacher_backbone": n_teacher, "student_adapter": n_adapter, "adapter_overhead_pct": 100.0 * n_adapter / n_teacher} print(f"teacher params {n_teacher/1e6:.2f}M, adapter {n_adapter/1e6:.2f}M", flush=True) torch.set_grad_enabled(False) # inference-only benchmark torch.cuda.reset_peak_memory_stats() # ---------------- collect identical inputs ---------------- files = sorted(glob.glob("/tmp/work/ravdess_kag/Actor_*/*.wav")) files = [f for f in files if os.path.basename(f).split("-")[1] == "01" and 19 <= int(os.path.basename(f).split("-")[-1].split(".")[0]) <= 24] by_emo = {} for f in files: by_emo.setdefault(os.path.basename(f).split("-")[2], []).append(f) bench_files = [] for e in sorted(by_emo): bench_files.extend(by_emo[e][:3]) print(f"bench inputs: {len(bench_files)} files", flush=True) # ---------------- the four measured paths ---------------- def teacher_pure(wav): src = F.layer_norm(wav[None], wav[None].shape) out = teacher.extract_features(src.to(DEV), padding_mask=None, mask=False) x = out["x"] # (1, T, 768) @50Hz probs = torch.softmax(teacher.proj(x.mean(1)), -1) return x, probs def teacher_pipeline(path): return fun.generate(path, granularity="frame", extract_embedding=True) def student_total(wav): return enc(wav[None].to(DEV), torch.tensor([wav.numel()], device=DEV)) def adapter_only(states, lengths): return enc.from_states(states, lengths) # ---------------- per-file quality + timing ---------------- print("=== per-file run (teacher & student on identical wavs) ===", flush=True) agrees, jss, pool_coss, frame_coss, corrs = [], [], [], [], [] per_path_times = {"teacher_pure": [], "student_total": [], "adapter_only": []} for f in bench_files: wav = load_wav(f).cpu() dur = wav.numel() / 16000 # warm + time each path on this identical input t0 = time.perf_counter(); tx, tprob = teacher_pure(wav); torch.cuda.synchronize() per_path_times["teacher_pure"].append(time.perf_counter() - t0) states = tx.detach() lengths = torch.tensor([states.size(1)], device=DEV) t0 = time.perf_counter(); sout = student_total(wav); torch.cuda.synchronize() per_path_times["student_total"].append(time.perf_counter() - t0) t0 = time.perf_counter(); aout = adapter_only(states, lengths); torch.cuda.synchronize() per_path_times["adapter_only"].append(time.perf_counter() - t0) # quality: student vs teacher on the same input sp = sout["utt_probs"][0] tp = tprob[0] agrees.append(int(sp.argmax().item() == tp.argmax().item())) jss.append(js_div(sp[None], tp[None])) pool_coss.append(float(F.cosine_similarity(sout["utt_emb"][0], states.mean(1)[0], dim=0))) up = F.interpolate(sout["frame_embs"].transpose(1, 2), size=states.size(1), mode="nearest").transpose(1, 2) frame_coss.append(float(F.cosine_similarity(up[0], states[0], dim=-1).mean())) inten = sout["frame_intensity"][0, :, 0].cpu() T = int(lengths[0]) tgt = rms_env(wav, T) tmax = torch.softmax(teacher.proj(states[0]), -1)[:, :8].max(-1).values.cpu().clamp(0, 1).sqrt() tgt = tmax * tgt tgt_ds = F.interpolate(tgt[None, None], size=inten.size(0), mode="linear")[0, 0] c = float(np.corrcoef(inten.numpy(), tgt_ds.numpy())[0, 1]) if inten.var() > 1e-6 and tgt_ds.var() > 1e-6 else None if c is not None: corrs.append(c) report["files"][os.path.basename(f)] = { "emotion": EMO_FIELD.get(os.path.basename(f).split("-")[2], "?"), "dur_s": round(dur, 2), "teacher_top": LABELS[int(tp.argmax())], "teacher_p": round(float(tp.max()), 3), "student_top": LABELS[int(sp.argmax())], "student_p": round(float(sp.max()), 3), "js": round(jss[-1], 5), "pooled_cos": round(pool_coss[-1], 5), "frame_cos": round(frame_coss[-1], 5), "intensity": {"mean": round(float(inten.mean()), 3), "std": round(float(inten.std()), 3), "min": round(float(inten.min()), 3), "max": round(float(inten.max()), 3), "corr_target": round(c, 3) if c is not None else None}, } n = len(bench_files) report["quality_summary"] = { "n_files": n, "top1_agreement": float(np.mean(agrees)), "mean_js": float(np.mean(jss)), "mean_pooled_cos": float(np.mean(pool_coss)), "mean_frame_cos": float(np.mean(frame_coss)), "mean_intensity_corr": float(np.mean(corrs)), } durs = np.array([load_wav(f).numel() / 16000 for f in bench_files]) for k, v in per_path_times.items(): v = np.array(v) report["paths"][k] = { "mean_ms_per_utt": float(1000 * v.mean()), "rtf_mean": float((v / durs).mean()), "utt_per_s": float(1.0 / v.mean()), "audio_x_realtime": float((durs / v).mean()), } print(json.dumps(report["quality_summary"], indent=1), flush=True) # ---------------- duration scaling (identical content, trimmed) ---------------- print("=== duration scaling ===", flush=True) base = load_wav(bench_files[0]) long_wav = base.repeat(4)[: 10 * 16000] report["scaling"]["duration"] = {} for sec in [1, 2, 3, 4, 10]: w = long_wav[: sec * 16000] row = {"dur_s": sec} row["teacher_pure"] = timeit(lambda: teacher_pure(w)) row["student_total"] = timeit(lambda: student_total(w)) st, ln = (lambda r: (r[0].detach(), torch.tensor([r[0].size(1)], device=DEV)))(teacher_pure(w)) row["adapter_only"] = timeit(lambda: adapter_only(st, ln)) for k in ["teacher_pure", "student_total", "adapter_only"]: row[k]["rtf"] = row[k]["mean_ms"] / 1000.0 / sec report["scaling"]["duration"][f"{sec}s"] = row print(f"{sec}s: teacher {row['teacher_pure']['mean_ms']:.1f}ms (RTF {row['teacher_pure']['rtf']:.3f}) | " f"student {row['student_total']['mean_ms']:.1f}ms (RTF {row['student_total']['rtf']:.3f}) | " f"adapter {row['adapter_only']['mean_ms']:.2f}ms", flush=True) # ---------------- funasr pipeline (stock user path) on 3 files ---------------- t0 = time.perf_counter() for f in bench_files[:3]: teacher_pipeline(f) pipe_t = (time.perf_counter() - t0) / 3 d3 = float(np.mean([load_wav(f).numel() / 16000 for f in bench_files[:3]])) report["paths"]["teacher_pipeline_funasr"] = {"mean_ms_per_utt": pipe_t * 1000, "rtf_mean": pipe_t / d3, "utt_per_s": 1.0 / pipe_t, "audio_x_realtime": d3 / pipe_t} # ---------------- adapter batch scaling ---------------- print("=== adapter batch scaling ===", flush=True) w3 = long_wav[: 3 * 16000] x3, _ = teacher_pure(w3) T3 = x3.size(1) report["scaling"]["adapter_batch"] = {} for B in [1, 4, 8]: st = x3.expand(B, T3, 768).contiguous() ln = torch.full((B,), T3, device=DEV, dtype=torch.long) r = timeit(lambda: adapter_only(st, ln)) r["utt_per_s"] = 1000.0 / r["mean_ms"] * B report["scaling"]["adapter_batch"][f"B{B}"] = r print(f"B={B}: {r['mean_ms']:.2f}ms -> {r['utt_per_s']:.1f} utt/s", flush=True) # ---------------- fp16 autocast student ---------------- with torch.autocast("cuda", dtype=torch.float16): r16 = timeit(lambda: student_total(w3)) report["scaling"]["student_fp16_autocast_3s"] = r16 print(f"student fp16 3s: {r16['mean_ms']:.1f}ms", flush=True) # ---------------- memory ---------------- report["memory"] = {"peak_gpu_allocated_mb": torch.cuda.max_memory_allocated() / 1e6, "peak_gpu_reserved_mb": torch.cuda.max_memory_reserved() / 1e6} # ---------------- CPU comparison (3s input) ---------------- print("=== CPU comparison (3 s input, 3 reps) ===", flush=True) enc_cpu = SurgicalEmotionEncoder(teacher.to("cpu"), SurgicalConfig()) enc_cpu.adapter.load_state_dict(payload["adapter_state"]) enc_cpu.eval() w3c = w3.cpu() def student_cpu(): with torch.no_grad(): enc_cpu(w3c[None], torch.tensor([w3c.numel()])) with torch.no_grad(): tc = timeit(student_cpu, warmup=1, reps=3) report["scaling"]["cpu_3s"] = {"student_total": tc} print(f"cpu student 3s: {tc['mean_ms']:.0f}ms", flush=True) teacher.to(DEV) enc.to(DEV) with open(OUT, "w") as fp: json.dump(report, fp, indent=1) print("BENCH_DONE", flush=True)