44import json
55import os
66from pathlib import Path
7- from typing import Any , Dict , Optional
7+ from typing import Any , Dict
88
99import nemo .collections .asr as nemo_asr
1010import onnx
3737]
3838
3939
40- def add_meta_data (filename : str , meta_data : Dict [str , str ]):
41- """Add meta data to an ONNX model. It is changed in-place."""
42- model = onnx .load (filename )
43-
44- while len (model .metadata_props ):
45- model .metadata_props .pop ()
46-
47- for key , value in meta_data .items ():
48- meta = model .metadata_props .add ()
49- meta .key = key
50- meta .value = str (value )
51-
52- external_filename = filename .split (".onnx" )[0 ]
53- # onnx.save refuses to overwrite an existing external-data file; the
54- # prompted-encoder export already wrote one, so remove it first.
55- Path (external_filename + ".data" ).unlink (missing_ok = True )
56- onnx .save (
57- model ,
58- filename ,
59- save_as_external_data = True ,
60- all_tensors_to_one_file = True ,
61- location = external_filename + ".data" ,
62- )
63-
64-
6540def _to_plain_container (obj : Any ) -> Any :
6641 try :
6742 from omegaconf import DictConfig , ListConfig , OmegaConf
@@ -82,132 +57,36 @@ def _normalize_prompt_dictionary(obj: Any) -> Dict[str, int]:
8257 return {str (k ): int (v ) for k , v in obj .items ()}
8358
8459
85- def _get_config_value (obj : Any , key : str ) -> Any :
86- if obj is None :
87- return None
88-
89- try :
90- return getattr (obj , key )
91- except (AttributeError , KeyError ):
92- pass
93-
94- obj = _to_plain_container (obj )
95- if isinstance (obj , dict ):
96- return obj .get (key )
97-
98- return None
99-
100-
101- def get_prompt_dictionary (asr_model ) -> Dict [str , int ]:
102- """Return the model's language prompt dictionary from NeMo artifacts."""
103- cfg = getattr (asr_model , "cfg" , None )
104- model_defaults = _get_config_value (cfg , "model_defaults" )
105- if model_defaults is None :
106- raise RuntimeError ("Could not find cfg.model_defaults in the NeMo model" )
107-
108- prompt_dictionary = _get_config_value (model_defaults , "prompt_dictionary" )
109- if prompt_dictionary is None :
110- raise RuntimeError (
111- "Could not find cfg.model_defaults.prompt_dictionary in the NeMo model"
112- )
113-
114- try :
115- ans = _normalize_prompt_dictionary (prompt_dictionary )
116- except (TypeError , ValueError ) as e :
117- raise RuntimeError (
118- "cfg.model_defaults.prompt_dictionary must map language strings "
119- "to integer prompt ids"
120- ) from e
121-
122- num_prompts = int (asr_model .num_prompts )
123- for language , prompt_id in ans .items ():
124- if not 0 <= prompt_id < num_prompts :
125- raise ValueError (
126- "cfg.model_defaults.prompt_dictionary has out-of-range "
127- f"prompt id for '{ language } ': { prompt_id } ; expected "
128- f"0 <= id < { num_prompts } "
129- )
130-
131- auto_prompt_id = ans .get ("auto" )
132- if auto_prompt_id != 101 :
133- raise ValueError (f"Expected auto prompt id 101, got { auto_prompt_id } " )
134-
135- # The dictionary may use locale-style keys such as en-US or ja-JP; the
136- # runtime derives base-code aliases, so accept either form here.
137- for language in ["en" , "ja" ]:
138- if not any (k == language or k .startswith (f"{ language } -" ) for k in ans ):
139- raise RuntimeError (
140- "cfg.model_defaults.prompt_dictionary is missing " f"'{ language } '"
141- )
142-
143- return ans
144-
145-
146- def _find_sentencepiece_processor (obj : Any , max_depth : int = 5 ) -> Optional [Any ]:
147- seen = set ()
148-
149- def is_sentencepiece_processor (value : Any ) -> bool :
150- return callable (getattr (value , "get_piece_size" , None )) and callable (
151- getattr (value , "id_to_piece" , None )
152- )
153-
154- def visit (value : Any , depth : int ) -> Optional [Any ]:
155- if value is None or depth > max_depth :
156- return None
157-
158- if is_sentencepiece_processor (value ):
159- return value
160-
161- obj_id = id (value )
162- if obj_id in seen :
163- return None
164- seen .add (obj_id )
165-
166- for name in ["tokenizer" , "sp_model" , "model" , "processor" ]:
167- if hasattr (value , name ):
168- found = visit (getattr (value , name ), depth + 1 )
169- if found is not None :
170- return found
60+ def add_meta_data (filename : str , meta_data : Dict [str , str ]):
61+ """Add meta data to an ONNX model. It is changed in-place."""
62+ model = onnx .load (filename )
17163
172- if isinstance (value , dict ):
173- for v in value .values ():
174- found = visit (v , depth + 1 )
175- if found is not None :
176- return found
64+ while len (model .metadata_props ):
65+ model .metadata_props .pop ()
17766
178- return None
67+ for key , value in meta_data .items ():
68+ meta = model .metadata_props .add ()
69+ meta .key = key
70+ meta .value = str (value )
17971
180- return visit (obj , 0 )
72+ external_filename = filename .split (".onnx" )[0 ]
73+ # onnx.save refuses to overwrite an existing external-data file; the
74+ # prompted-encoder export already wrote one, so remove it first.
75+ Path (external_filename + ".data" ).unlink (missing_ok = True )
76+ onnx .save (
77+ model ,
78+ filename ,
79+ save_as_external_data = True ,
80+ all_tensors_to_one_file = True ,
81+ location = external_filename + ".data" ,
82+ )
18183
18284
18385def save_tokens (asr_model , filename : str = "tokens.txt" ) -> int :
184- sp = _find_sentencepiece_processor (getattr (asr_model , "tokenizer" , None ))
185- if sp is None :
186- raise RuntimeError ("Could not find the SentencePiece tokenizer in the model" )
187-
188- vocab_size = sp .get_piece_size ()
18986 with open (filename , "w" , encoding = "utf-8" ) as f :
190- for i in range (vocab_size ):
191- f .write (f"{ sp .id_to_piece (i )} { i } \n " )
192- f .write (f"<blk> { vocab_size } \n " )
193-
194- print (f"Saved { filename } " )
195- return vocab_size
196-
197-
198- def assert_forward_for_export_signature (encoder ):
199- if not hasattr (encoder , "forward_for_export" ):
200- raise RuntimeError ("Expected encoder.forward_for_export for ONNX export" )
201-
202- signature = inspect .signature (encoder .forward_for_export )
203- missing = [
204- name for name in FORWARD_FOR_EXPORT_ARGS if name not in signature .parameters
205- ]
206- if missing :
207- raise RuntimeError (
208- "encoder.forward_for_export is missing expected argument(s): "
209- f"{ missing } . Signature: { signature } "
210- )
87+ for i , s in enumerate (asr_model .joint .vocabulary ):
88+ f .write (f"{ s } { i } \n " )
89+ f .write (f"<blk> { i + 1 } \n " )
21190
21291
21392class PromptedStreamingEncoder (torch .nn .Module ):
@@ -221,7 +100,6 @@ def __init__(self, asr_model):
221100 )
222101
223102 self .encoder = asr_model .encoder
224- assert_forward_for_export_signature (self .encoder )
225103
226104 self .prompt_kernel = asr_model .prompt_kernel
227105 self .num_prompts = int (asr_model .num_prompts )
@@ -283,24 +161,6 @@ def remove_export_scratch_files():
283161 p .unlink ()
284162
285163
286- def assert_encoder_graph (filename : str ):
287- model = onnx .load (filename , load_external_data = False )
288-
289- input_names = [i .name for i in model .graph .input ]
290- if input_names != ENCODER_INPUT_NAMES :
291- raise RuntimeError (
292- f"{ filename } : expected encoder inputs { ENCODER_INPUT_NAMES } , "
293- f"got { input_names } "
294- )
295-
296- output_names = [o .name for o in model .graph .output ]
297- if output_names != ENCODER_OUTPUT_NAMES :
298- raise RuntimeError (
299- f"{ filename } : expected encoder outputs { ENCODER_OUTPUT_NAMES } , "
300- f"got { output_names } "
301- )
302-
303-
304164def _module_device_and_dtype (module ):
305165 try :
306166 p = next (module .parameters ())
@@ -322,7 +182,9 @@ def export_prompted_encoder(
322182):
323183 device , dtype = _module_device_and_dtype (asr_model .encoder )
324184
325- audio_signal = torch .zeros (1 , 128 , window_size , dtype = dtype , device = device )
185+ feat_dim = asr_model .cfg .preprocessor .features
186+
187+ audio_signal = torch .zeros (1 , feat_dim , window_size , dtype = dtype , device = device )
326188 length = torch .full ((1 ,), window_size , dtype = torch .int64 , device = device )
327189 cache_last_channel = torch .zeros (
328190 1 ,
@@ -365,7 +227,7 @@ def export_prompted_encoder(
365227 "encoder.export.onnx" ,
366228 input_names = ENCODER_INPUT_NAMES ,
367229 output_names = ENCODER_OUTPUT_NAMES ,
368- opset_version = 17 ,
230+ opset_version = 13 ,
369231 dynamic_axes = {
370232 "audio_signal" : {0 : "batch" , 2 : "time" },
371233 "length" : {0 : "batch" },
@@ -391,7 +253,6 @@ def export_prompted_encoder(
391253 location = "encoder.data" ,
392254 size_threshold = 0 ,
393255 )
394- assert_encoder_graph ("encoder.onnx" )
395256 for p in Path ("." ).glob ("encoder.export.onnx*" ):
396257 p .unlink ()
397258
@@ -402,17 +263,10 @@ def main():
402263
403264 asr_model = nemo_asr .models .ASRModel .from_pretrained (model_name = model_name )
404265
405- vocab_size = save_tokens (asr_model )
406- if vocab_size != asr_model .decoder .vocab_size :
407- raise ValueError (
408- f"SentencePiece vocab size { vocab_size } != decoder vocab size "
409- f"{ asr_model .decoder .vocab_size } "
410- )
266+ save_tokens (asr_model )
411267
412- prompt_dictionary = get_prompt_dictionary ( asr_model )
268+ prompt_dictionary = asr_model . cfg . model_defaults . prompt_dictionary
413269 auto_prompt_id = prompt_dictionary ["auto" ]
414- if auto_prompt_id != 101 :
415- raise ValueError (f"Expected auto prompt id 101, got { auto_prompt_id } " )
416270
417271 asr_model .eval ()
418272
@@ -508,20 +362,20 @@ def main():
508362 "model_author" : "NeMo" ,
509363 "url" : f"https://huggingface.co/{ model_name } " ,
510364 "comment" : "Only the transducer branch is exported" ,
511- "prompt_dictionary" : json .dumps (prompt_dictionary , sort_keys = True ),
365+ "prompt_dictionary" : json .dumps (
366+ _normalize_prompt_dictionary (prompt_dictionary ), sort_keys = True
367+ ),
512368 "auto_prompt_id" : auto_prompt_id ,
513369 }
514370 print ("meta_data" , meta_data )
515371 add_meta_data ("encoder.onnx" , meta_data )
516- assert_encoder_graph ("encoder.onnx" )
517372
518373 for m in ["encoder" , "decoder" , "joiner" ]:
519374 quantize_dynamic (
520375 model_input = f"{ m } .onnx" ,
521376 model_output = f"{ m } .int8.onnx" ,
522377 weight_type = QuantType .QUInt8 ,
523378 )
524- assert_encoder_graph ("encoder.int8.onnx" )
525379
526380 Path (str (ms )).mkdir (exist_ok = True )
527381 for suffix in ["onnx" , "data" ]:
0 commit comments