#!/usr/bin/env python3 """Compare teacher, FP32 student, and INT8 student embeddings on real text.""" from __future__ import annotations import argparse import json import math import sys import time from pathlib import Path import numpy as np import onnxruntime as ort from vosk_booktts_experiment import WordPieceTokenizer, load_corpus, normalize_text def cosine_rows(left: np.ndarray, right: np.ndarray) -> np.ndarray: numerator = np.sum(left * right, axis=-1) denominator = np.linalg.norm(left, axis=-1) * np.linalg.norm(right, axis=-1) return numerator / np.maximum(denominator, 1e-12) def compare(args: argparse.Namespace) -> None: tokenizer = WordPieceTokenizer(args.vocab) token_map = np.load(args.token_map) compact_unk = int(token_map[tokenizer.vocabulary["[UNK]"]]) sessions = { "teacher": ort.InferenceSession(str(args.teacher), providers=["CPUExecutionProvider"]), "student_fp32": ort.InferenceSession(str(args.student_fp32), providers=["CPUExecutionProvider"]), "student_int8": ort.InferenceSession(str(args.student_int8), providers=["CPUExecutionProvider"]), } timings = {name: 0.0 for name in sessions} teacher_cosines: list[np.ndarray] = [] quantized_cosines: list[np.ndarray] = [] teacher_squared_error = 0.0 quantized_squared_error = 0.0 elements = 0 tokens = 0 unseen_tokens = 0 phrases = load_corpus(args.corpus)[: args.limit or None] for text in phrases: ids, _ = tokenizer.encode(normalize_text(text)) shape = (1, ids.size) feeds = { "input_ids": ids.reshape(shape), "attention_mask": np.ones(shape, dtype=np.int64), "token_type_ids": np.zeros(shape, dtype=np.int64), } outputs: dict[str, np.ndarray] = {} for name, session in sessions.items(): started = time.perf_counter() outputs[name] = session.run(None, feeds)[0].astype(np.float32, copy=False) timings[name] += time.perf_counter() - started teacher = outputs["teacher"] fp32 = outputs["student_fp32"] int8 = outputs["student_int8"] if teacher.shape != fp32.shape or fp32.shape != int8.shape: raise ValueError(f"Output shape mismatch for {text!r}: {teacher.shape}, {fp32.shape}, {int8.shape}") teacher_cosines.append(cosine_rows(teacher, fp32)) quantized_cosines.append(cosine_rows(fp32, int8)) teacher_squared_error += float(np.sum((teacher - fp32) ** 2)) quantized_squared_error += float(np.sum((fp32 - int8) ** 2)) elements += teacher.size tokens += ids.size unseen_tokens += int(np.sum((token_map[ids] == compact_unk) & (ids != tokenizer.vocabulary["[UNK]"]))) teacher_cosine = np.concatenate(teacher_cosines) quantized_cosine = np.concatenate(quantized_cosines) report = { "phrases": len(phrases), "tokens": tokens, "unseen_tokens": unseen_tokens, "unseen_percent": 100.0 * unseen_tokens / tokens, "teacher_vs_student_fp32": { "mean_token_cosine": float(np.mean(teacher_cosine)), "p05_token_cosine": float(np.percentile(teacher_cosine, 5)), "mse": teacher_squared_error / elements, }, "student_fp32_vs_int8": { "mean_token_cosine": float(np.mean(quantized_cosine)), "p05_token_cosine": float(np.percentile(quantized_cosine, 5)), "mse": quantized_squared_error / elements, }, "total_inference_seconds": timings, "milliseconds_per_phrase": {name: seconds * 1000.0 / len(phrases) for name, seconds in timings.items()}, } if not all(math.isfinite(value) for value in (teacher_squared_error, quantized_squared_error)): raise ValueError("Non-finite comparison result") rendered = json.dumps(report, ensure_ascii=False, indent=2) + "\n" if args.output: if args.output.exists(): raise FileExistsError(f"Refusing to overwrite {args.output}") args.output.write_text(rendered, encoding="utf-8") print(rendered, end="") def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--teacher", type=Path, required=True) parser.add_argument("--student-fp32", type=Path, required=True) parser.add_argument("--student-int8", type=Path, required=True) parser.add_argument("--vocab", type=Path, required=True) parser.add_argument("--token-map", type=Path, required=True) parser.add_argument("--corpus", type=Path, required=True) parser.add_argument("--output", type=Path) parser.add_argument("--limit", type=int, default=0) return parser.parse_args() if __name__ == "__main__": sys.exit(compare(parse_args()) or 0)