Some checks failed
Linux Build / Linux cpu (push) Has been cancelled
Linux Build / Linux cpu-clang (push) Has been cancelled
Linux Build / Linux vulkan (push) Has been cancelled
mac-build / macOS CPU (push) Has been cancelled
Nix Build / Nix (cpu) (push) Has been cancelled
Nix Build / Nix (vulkan) (push) Has been cancelled
windows-build / Windows CPU (push) Has been cancelled
135 lines
5 KiB
Python
135 lines
5 KiB
Python
#!/usr/bin/env python3
|
|
"""Dump NeMo Parakeet-TDT reference activations for numerical parity testing.
|
|
|
|
Loads the real `nvidia/parakeet-tdt-0.6b-v3` NeMo checkpoint, runs it on a
|
|
given audio clip, and saves a set of intermediate activations as .npy files
|
|
plus a summary.json of their shapes/means/stds. compare_parity.py then
|
|
compares these against the equivalent C++ dump produced by
|
|
dump_cpp_reference.cpp.
|
|
|
|
Captured tensors:
|
|
mel_features preprocessor output, [1, n_mels, T] (before the encoder)
|
|
pre_encode encoder.pre_encode output == the FastConformer
|
|
subsampling stack's linear projection, BEFORE the
|
|
xscale (sqrt(d_model)) multiply. [1, T', d_model]
|
|
pos_emb the raw sinusoidal relative positional encoding fed
|
|
into every layer's linear_pos projection (shared
|
|
across layers), [1, 2*T'-1, d_model]
|
|
layer_{i} full output of encoder.layers[i] for every layer,
|
|
[1, T', d_model]
|
|
enc_out final encoder output (after all layers, before the
|
|
joint's encoder projection), [1, d_model, T']
|
|
|
|
Setup (NeMo + its dependency chain isn't a lightweight install):
|
|
python3 -m venv /tmp/parakeet_parity_venv
|
|
/tmp/parakeet_parity_venv/bin/pip install torch nemo_toolkit[asr] librosa pysqlite3-binary numpy
|
|
|
|
# If your system Python lacks a usable sqlite3 module (common in minimal
|
|
# containers), NeMo's dependency chain pulls it in transitively via IPython
|
|
# history; work around it by installing pysqlite3-binary and shimming
|
|
# sys.modules['sqlite3'] before importing nemo, exactly as this script does
|
|
# below.
|
|
|
|
Usage:
|
|
/tmp/parakeet_parity_venv/bin/python3 dump_nemo_reference.py \
|
|
--audio ../assets/2086-149220-0033.wav \
|
|
--output-dir /tmp/parakeet_nemo_dump
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
os.environ.setdefault("CUDA_VISIBLE_DEVICES", "")
|
|
|
|
try:
|
|
import sqlite3 # noqa: F401
|
|
except ImportError:
|
|
sys.modules["sqlite3"] = __import__("pysqlite3")
|
|
|
|
import librosa
|
|
import numpy as np
|
|
import torch
|
|
|
|
|
|
def load_model(model_name: str):
|
|
import nemo.collections.asr as nemo_asr
|
|
|
|
model = nemo_asr.models.ASRModel.from_pretrained(model_name=model_name, map_location="cpu")
|
|
model = model.cpu()
|
|
model.eval()
|
|
return model
|
|
|
|
|
|
def dump(model_name: str, audio_path: str, output_dir: str) -> dict:
|
|
model = load_model(model_name)
|
|
audio, _sr = librosa.load(audio_path, sr=16000, mono=True)
|
|
|
|
captured: dict[str, np.ndarray] = {}
|
|
|
|
def hook(name):
|
|
def fn(_module, _inp, out):
|
|
tensor = out[0] if isinstance(out, tuple) else out
|
|
captured[name] = tensor.detach().cpu().numpy()
|
|
|
|
return fn
|
|
|
|
model.encoder.pre_encode.register_forward_hook(hook("pre_encode"))
|
|
for i, layer in enumerate(model.encoder.layers):
|
|
layer.register_forward_hook(hook(f"layer_{i}"))
|
|
model.encoder.register_forward_hook(hook("enc_out"))
|
|
|
|
# pos_enc.forward returns (scaled_x, pos_emb); wrap it to capture pos_emb
|
|
# without disturbing the actual forward pass.
|
|
orig_pos_enc_forward = model.encoder.pos_enc.forward
|
|
|
|
def pos_enc_hook(x, cache_len=0):
|
|
scaled_x, pos_emb = orig_pos_enc_forward(x, cache_len)
|
|
captured["pos_emb"] = pos_emb.detach().cpu().numpy()
|
|
return scaled_x, pos_emb
|
|
|
|
model.encoder.pos_enc.forward = pos_enc_hook
|
|
|
|
with torch.no_grad():
|
|
mel, mel_len = model.preprocessor(
|
|
input_signal=torch.tensor(audio).unsqueeze(0), length=torch.tensor([len(audio)])
|
|
)
|
|
captured["mel_features"] = mel.detach().cpu().numpy()
|
|
model.encoder(audio_signal=mel, length=mel_len)
|
|
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
summary = {}
|
|
for name, tensor in captured.items():
|
|
np.save(os.path.join(output_dir, f"{name}.npy"), tensor)
|
|
summary[name] = {
|
|
"shape": list(tensor.shape),
|
|
"mean": float(np.mean(tensor)),
|
|
"std": float(np.std(tensor)),
|
|
}
|
|
with open(os.path.join(output_dir, "summary.json"), "w") as f:
|
|
json.dump(summary, f, indent=2)
|
|
return summary
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument("--audio", required=True, help="Path to a mono 16kHz WAV/FLAC test clip")
|
|
parser.add_argument("--output-dir", required=True, help="Directory to write .npy dumps + summary.json into")
|
|
parser.add_argument(
|
|
"--model-name",
|
|
default="nvidia/parakeet-tdt-0.6b-v3",
|
|
help="HuggingFace/NGC model name passed to ASRModel.from_pretrained",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
summary = dump(args.model_name, args.audio, args.output_dir)
|
|
for name, stats in summary.items():
|
|
print(f"{name}: shape={stats['shape']} mean={stats['mean']:.6f} std={stats['std']:.6f}")
|
|
print(f"\nSaved {len(summary)} tensors to {args.output_dir}/")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|