Skip to content

Commit ae2bc66

Browse files
authored
feat(nemo): add buffered RNNT streaming path for Parakeet Unified (#3575)
1 parent a90437c commit ae2bc66

15 files changed

Lines changed: 1460 additions & 22 deletions
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2026 Milan Leonard
3+
4+
import numpy as np
5+
6+
7+
def normalize_per_feature(features: np.ndarray) -> np.ndarray:
8+
mean = features.mean(axis=0, keepdims=True)
9+
std = features.std(axis=0, keepdims=True) + 1e-5
10+
return ((features - mean) / std).astype(np.float32)
11+
12+
13+
def slice_feature_buffer(
14+
features: np.ndarray,
15+
center_start: int,
16+
left: int,
17+
chunk: int,
18+
right: int,
19+
):
20+
total = left + chunk + right
21+
left_start = center_start - left
22+
right_end = center_start + chunk + right
23+
pad_left = max(0, -left_start)
24+
pad_right = max(0, right_end - features.shape[0])
25+
start = max(0, left_start)
26+
end = min(features.shape[0], right_end)
27+
28+
window = features[start:end]
29+
if pad_left or pad_right:
30+
window = np.pad(window, ((pad_left, pad_right), (0, 0)), mode="constant")
31+
if window.shape[0] != total:
32+
raise ValueError(f"Expected {total} frames, got {window.shape[0]}")
33+
34+
valid_center = max(0, min(chunk, features.shape[0] - center_start))
35+
return window.astype(np.float32), valid_center

scripts/nemo/parakeet-unified-en-0.6b/export_onnx.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def main():
7878
"normalize_type": normalize_type,
7979
"pred_rnn_layers": asr_model.decoder.pred_rnn_layers,
8080
"pred_hidden": asr_model.decoder.pred_hidden,
81-
"subsampling_factor": 8,
81+
"subsampling_factor": asr_model.encoder.subsampling_factor,
8282
"model_type": "EncDecRNNTBPEModel",
8383
"version": "2",
8484
"model_author": "NeMo",
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2026 Milan Leonard
3+
"""Buffered streaming ONNX export for nvidia/parakeet-unified-en-0.6b."""
4+
5+
import argparse
6+
from pathlib import Path
7+
from typing import Dict
8+
9+
import nemo.collections.asr as nemo_asr
10+
import onnx
11+
import torch
12+
from onnxruntime.quantization import QuantType, quantize_dynamic
13+
14+
LATENCY_PRESETS = {
15+
"1120ms": {"left": 70, "chunk": 7, "right": 7},
16+
"560ms": {"left": 70, "chunk": 2, "right": 5},
17+
"240ms": {"left": 70, "chunk": 1, "right": 2},
18+
}
19+
20+
21+
def get_args():
22+
parser = argparse.ArgumentParser(
23+
description="Buffered streaming ONNX export for parakeet-unified-en-0.6b",
24+
)
25+
parser.add_argument(
26+
"--latency",
27+
type=str,
28+
default="1120ms",
29+
choices=sorted(LATENCY_PRESETS.keys()),
30+
help="Latency preset to export.",
31+
)
32+
return parser.parse_args()
33+
34+
35+
def add_meta_data(filename: str, meta_data: Dict[str, str]):
36+
model = onnx.load(filename)
37+
while len(model.metadata_props):
38+
model.metadata_props.pop()
39+
40+
for key, value in meta_data.items():
41+
meta = model.metadata_props.add()
42+
meta.key = key
43+
meta.value = str(value)
44+
45+
if Path(filename).name == "encoder.onnx":
46+
onnx.save(
47+
model,
48+
filename,
49+
save_as_external_data=True,
50+
all_tensors_to_one_file=True,
51+
location="encoder.weights",
52+
)
53+
else:
54+
onnx.save(model, filename)
55+
56+
57+
def print_onnx_listing():
58+
for p in sorted(Path.cwd().glob("*.onnx")):
59+
size_mb = p.stat().st_size / (1024 * 1024)
60+
print(f"{size_mb:8.2f} MB {p.name}")
61+
62+
63+
@torch.no_grad()
64+
def main():
65+
args = get_args()
66+
preset = LATENCY_PRESETS[args.latency]
67+
68+
if Path("./parakeet-unified-en-0.6b.nemo").is_file():
69+
asr_model = nemo_asr.models.ASRModel.restore_from(
70+
restore_path="./parakeet-unified-en-0.6b.nemo"
71+
)
72+
else:
73+
asr_model = nemo_asr.models.ASRModel.from_pretrained(
74+
model_name="nvidia/parakeet-unified-en-0.6b"
75+
)
76+
77+
asr_model.eval()
78+
asr_model.cfg.validation_ds = dict()
79+
asr_model.encoder.set_default_att_context_size(
80+
[preset["left"], preset["chunk"], preset["right"]]
81+
)
82+
83+
with open("./tokens.txt", "w", encoding="utf-8") as f:
84+
for i, s in enumerate(asr_model.joint.vocabulary):
85+
f.write(f"{s} {i}\n")
86+
f.write(f"<blk> {i + 1}\n")
87+
print("Saved to tokens.txt")
88+
89+
asr_model.encoder.export("encoder.onnx")
90+
asr_model.decoder.export("decoder.onnx")
91+
asr_model.joint.export("joiner.onnx")
92+
print_onnx_listing()
93+
94+
normalize_type = asr_model.cfg.preprocessor.normalize
95+
if normalize_type == "NA":
96+
normalize_type = ""
97+
98+
subsampling_factor = asr_model.encoder.subsampling_factor
99+
100+
meta_data = {
101+
"vocab_size": asr_model.decoder.vocab_size,
102+
"normalize_type": normalize_type,
103+
"pred_rnn_layers": asr_model.decoder.pred_rnn_layers,
104+
"pred_hidden": asr_model.decoder.pred_hidden,
105+
"subsampling_factor": subsampling_factor,
106+
"model_type": "EncDecRNNTBPEModel",
107+
"streaming_model_type": "nemo_parakeet_unified_streaming",
108+
"buffered_streaming": 1,
109+
"left_encoder_frames": preset["left"],
110+
"chunk_encoder_frames": preset["chunk"],
111+
"right_encoder_frames": preset["right"],
112+
"left_feature_frames": preset["left"] * subsampling_factor,
113+
"chunk_feature_frames": preset["chunk"] * subsampling_factor,
114+
"right_feature_frames": preset["right"] * subsampling_factor,
115+
"version": "2",
116+
"model_author": "NeMo",
117+
"url": "https://huggingface.co/nvidia/parakeet-unified-en-0.6b",
118+
"comment": f"Buffered streaming export, latency={args.latency}",
119+
"feat_dim": 128,
120+
"latency": args.latency,
121+
}
122+
123+
for m in ["encoder", "decoder", "joiner"]:
124+
quantize_dynamic(
125+
model_input=f"./{m}.onnx",
126+
model_output=f"./{m}.int8.onnx",
127+
weight_type=QuantType.QUInt8 if m == "encoder" else QuantType.QInt8,
128+
)
129+
print_onnx_listing()
130+
131+
add_meta_data("encoder.onnx", meta_data)
132+
add_meta_data("encoder.int8.onnx", meta_data)
133+
decoder_meta_data = {
134+
"streaming_model_type": "nemo_parakeet_unified_streaming",
135+
}
136+
add_meta_data("decoder.onnx", decoder_meta_data)
137+
add_meta_data("decoder.int8.onnx", decoder_meta_data)
138+
print("meta_data", meta_data)
139+
140+
141+
if __name__ == "__main__":
142+
main()
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env python3
2+
import unittest
3+
4+
import numpy as np
5+
6+
from buffered_streaming_helpers import normalize_per_feature, slice_feature_buffer
7+
8+
9+
class TestBufferedStreamingHelpers(unittest.TestCase):
10+
def test_slice_feature_buffer_zero_pads_left_and_right_context(self):
11+
features = np.arange(6 * 2, dtype=np.float32).reshape(6, 2)
12+
13+
window, valid_center_frames = slice_feature_buffer(
14+
features,
15+
center_start=0,
16+
left=4,
17+
chunk=3,
18+
right=2,
19+
)
20+
21+
self.assertEqual(window.shape, (9, 2))
22+
np.testing.assert_array_equal(window[:4], np.zeros((4, 2), dtype=np.float32))
23+
np.testing.assert_array_equal(window[4:9], features[:5])
24+
self.assertEqual(valid_center_frames, 3)
25+
26+
def test_slice_feature_buffer_reports_short_final_center_chunk(self):
27+
features = np.arange(5 * 2, dtype=np.float32).reshape(5, 2)
28+
29+
window, valid_center_frames = slice_feature_buffer(
30+
features,
31+
center_start=4,
32+
left=2,
33+
chunk=3,
34+
right=2,
35+
)
36+
37+
self.assertEqual(window.shape, (7, 2))
38+
np.testing.assert_array_equal(window[:3], features[2:5])
39+
np.testing.assert_array_equal(window[3:], np.zeros((4, 2), dtype=np.float32))
40+
self.assertEqual(valid_center_frames, 1)
41+
42+
def test_normalize_per_feature_normalizes_each_column(self):
43+
features = np.array(
44+
[
45+
[1.0, 10.0],
46+
[2.0, 20.0],
47+
[3.0, 30.0],
48+
],
49+
dtype=np.float32,
50+
)
51+
52+
normalized = normalize_per_feature(features)
53+
54+
np.testing.assert_allclose(normalized.mean(axis=0), [0.0, 0.0], atol=1e-6)
55+
np.testing.assert_allclose(normalized.std(axis=0), [1.0, 1.0], atol=2e-5)
56+
57+
58+
if __name__ == "__main__":
59+
unittest.main()

0 commit comments

Comments
 (0)