22
33"""ONNX backend wrapper for onnx2c (ONNX to C code generator)."""
44
5+ import math
56import os
67import re
78import shutil
@@ -74,6 +75,24 @@ def _parse_tensor_info(value_info):
7475 return _c_name (value_info .name ), c_type , np_dtype , shape
7576
7677
78+ def _io_lines (tensors_info , arg_offset , mode ):
79+ """Return C lines that open and read/write tensor binary files via argv."""
80+ rw_func = "fread" if mode == "rb" else "fwrite"
81+ direction = "input" if mode == "rb" else "output"
82+ lines = []
83+ for i , (c_nm , c_type , _ , shape ) in enumerate (tensors_info ):
84+ n = math .prod (shape )
85+ arg_idx = arg_offset + i + 1
86+ lines .append (f' f = fopen(argv[{ arg_idx } ], "{ mode } ");' )
87+ lines .append (
88+ f' if (!f) {{ fprintf(stderr, "Cannot open { direction } { i } \\ n"); '
89+ f"return 1; }}"
90+ )
91+ lines .append (f" { rw_func } ({ c_nm } , sizeof({ c_type } ), { n } , f);" )
92+ lines .append (" fclose(f);" )
93+ return lines
94+
95+
7796def _generate_harness (inputs_info , outputs_info ):
7897 """Return C harness source that feeds inputs, calls entry(), writes outputs."""
7998 lines = [
@@ -84,74 +103,109 @@ def _generate_harness(inputs_info, outputs_info):
84103 "" ,
85104 ]
86105
87- # extern declarations for input/output tensors (defined in generated model.c)
88106 for c_nm , c_type , _ , shape in inputs_info :
89- n = 1
90- for d in shape :
91- n *= d
92- lines .append (f"extern { c_type } { c_nm } [{ n } ];" )
107+ lines .append (f"extern { c_type } { c_nm } [{ math .prod (shape )} ];" )
93108
94109 for c_nm , c_type , _ , shape in outputs_info :
95- n = 1
96- for d in shape :
97- n *= d
98- lines .append (f"extern { c_type } { c_nm } [{ n } ];" )
110+ lines .append (f"extern { c_type } { c_nm } [{ math .prod (shape )} ];" )
99111
100112 lines += ["" , "void entry(void);" , "" , "int main(int argc, char** argv) {" ]
101113 lines .append (" FILE* f;" )
102114 lines .append ("" )
115+ lines += _io_lines (inputs_info , 0 , "rb" )
116+ lines += ["" , " entry();" , "" ]
117+ lines += _io_lines (outputs_info , len (inputs_info ), "wb" )
118+ lines += ["" , " return 0;" , "}" ]
119+ return "\n " .join (lines )
103120
104- # Read inputs from files passed as argv[1..n_inputs]
105- for i , (c_nm , c_type , _ , shape ) in enumerate (inputs_info ):
106- n = 1
107- for d in shape :
108- n *= d
109- lines .append (f" f = fopen(argv[{ i + 1 } ], \" rb\" );" )
110- lines .append (
111- f" if (!f) {{ fprintf(stderr, \" Cannot open input { i } \\ n\" ); return 1; }}"
112- )
113- lines .append (f" fread({ c_nm } , sizeof({ c_type } ), { n } , f);" )
114- lines .append (" fclose(f);" )
115121
116- lines += ["" , " entry();" , "" ]
122+ def _parse_graph_io (graph ):
123+ """Parse graph inputs/outputs; return (inputs_info, outputs_info) or raise."""
124+ initializer_names = {init .name for init in graph .initializer }
125+ inputs_info = []
126+ for vi in graph .input :
127+ if vi .name in initializer_names :
128+ continue
129+ info = _parse_tensor_info (vi )
130+ if info is None :
131+ raise BackendIsNotSupposedToImplementIt (
132+ f"Input '{ vi .name } ' has unsupported type or dynamic shape"
133+ )
134+ inputs_info .append (info )
135+
136+ outputs_info = []
137+ for vi in graph .output :
138+ info = _parse_tensor_info (vi )
139+ if info is None :
140+ raise BackendIsNotSupposedToImplementIt (
141+ f"Output '{ vi .name } ' has unsupported type or dynamic shape"
142+ )
143+ outputs_info .append (info )
117144
118- # Write outputs to files passed as argv[n_inputs+1..n_inputs+n_outputs]
119- n_in = len (inputs_info )
120- for i , (c_nm , c_type , _ , shape ) in enumerate (outputs_info ):
121- n = 1
122- for d in shape :
123- n *= d
124- lines .append (f" f = fopen(argv[{ n_in + i + 1 } ], \" wb\" );" )
125- lines .append (
126- f" if (!f) {{ fprintf(stderr, \" Cannot open output { i } \\ n\" ); return 1; }}"
127- )
128- lines .append (f" fwrite({ c_nm } , sizeof({ c_type } ), { n } , f);" )
129- lines .append (" fclose(f);" )
145+ return inputs_info , outputs_info
130146
131- lines += ["" , " return 0;" , "}" ]
132- return "\n " .join (lines )
147+
148+ def _build_binary (workdir , model , inputs_info , outputs_info ):
149+ """Compile the ONNX model to a native binary; return the binary path."""
150+ model_path = os .path .join (workdir , "model.onnx" )
151+ with open (model_path , "wb" ) as f :
152+ f .write (model .SerializeToString ())
153+
154+ result = subprocess .run (
155+ ["onnx2c" , model_path ], capture_output = True , timeout = 120
156+ )
157+ if result .returncode != 0 :
158+ raise BackendIsNotSupposedToImplementIt (
159+ f"onnx2c failed: { result .stderr .decode ()} "
160+ )
161+ model_c_path = os .path .join (workdir , "model.c" )
162+ with open (model_c_path , "wb" ) as f :
163+ f .write (result .stdout )
164+
165+ harness_path = os .path .join (workdir , "harness.c" )
166+ with open (harness_path , "w" ) as f :
167+ f .write (_generate_harness (inputs_info , outputs_info ))
168+
169+ binary_path = os .path .join (workdir , "model_exec" )
170+ result = subprocess .run (
171+ ["gcc" , "-O2" , "-o" , binary_path , harness_path , model_c_path , "-lm" ],
172+ capture_output = True ,
173+ timeout = 120 ,
174+ )
175+ if result .returncode != 0 :
176+ raise BackendIsNotSupposedToImplementIt (
177+ f"Compilation failed: { result .stderr .decode ()} "
178+ )
179+ return binary_path
133180
134181
135182class Onnx2cBackendRep (BackendRep ):
136183 """Holds a compiled onnx2c binary and runs it for each inference call."""
137184
138185 def __init__ (self , binary_path , workdir , inputs_info , outputs_info ):
186+ """Store compiled binary path and run-time type/shape information."""
139187 self ._binary = binary_path
140188 self ._workdir = workdir
141189 # Store only dtype and shape needed at run time
142190 self ._input_dtypes = [np_dtype for _ , _ , np_dtype , _ in inputs_info ]
143- self ._output_info = [(np_dtype , shape ) for _ , _ , np_dtype , shape in outputs_info ]
191+ self ._output_info = [
192+ (np_dtype , shape ) for _ , _ , np_dtype , shape in outputs_info
193+ ]
144194
145195 def __del__ (self ):
196+ """Remove the temporary working directory on garbage collection."""
146197 if self ._workdir and os .path .isdir (self ._workdir ):
147198 shutil .rmtree (self ._workdir , ignore_errors = True )
148199
149200 def run (self , inputs , ** kwargs ):
201+ """Write inputs as binary files, invoke compiled binary, return outputs."""
150202 run_dir = tempfile .mkdtemp ()
151203 try :
152204 # Write each input as raw binary with the expected dtype
153205 input_files = []
154- for i , (inp , dtype ) in enumerate (zip (inputs , self ._input_dtypes )):
206+ for i , (inp , dtype ) in enumerate (
207+ zip (inputs , self ._input_dtypes , strict = False )
208+ ):
155209 path = os .path .join (run_dir , f"input_{ i } .bin" )
156210 np .asarray (inp , dtype = dtype ).flatten ().tofile (path )
157211 input_files .append (path )
@@ -172,7 +226,9 @@ def run(self, inputs, **kwargs):
172226 )
173227
174228 outputs = []
175- for path , (dtype , shape ) in zip (output_files , self ._output_info ):
229+ for path , (dtype , shape ) in zip (
230+ output_files , self ._output_info , strict = False
231+ ):
176232 arr = np .fromfile (path , dtype = dtype ).reshape (shape )
177233 outputs .append (arr )
178234 return outputs
@@ -185,91 +241,38 @@ class Onnx2cBackend(Backend):
185241
186242 @classmethod
187243 def is_compatible (cls , model , device = "CPU" , ** kwargs ):
244+ """Return whether this backend can attempt to handle the model."""
188245 return True
189246
190247 @classmethod
191248 def prepare (cls , model , device = "CPU" , ** kwargs ):
192- # Run ONNX shape inference so output shapes are concrete where possible
249+ """Compile the ONNX model to native code and return a runnable rep."""
193250 try :
194251 model = shape_inference .infer_shapes (model )
195- except Exception :
252+ except Exception : # noqa: BLE001
196253 pass
197254
198- initializer_names = {init .name for init in model .graph .initializer }
199-
200- # Parse inputs (exclude initializers / weights)
201- inputs_info = []
202- for vi in model .graph .input :
203- if vi .name in initializer_names :
204- continue
205- info = _parse_tensor_info (vi )
206- if info is None :
207- raise BackendIsNotSupposedToImplementIt (
208- f"Input '{ vi .name } ' has unsupported type or dynamic shape"
209- )
210- inputs_info .append (info )
211-
212- # Parse outputs
213- outputs_info = []
214- for vi in model .graph .output :
215- info = _parse_tensor_info (vi )
216- if info is None :
217- raise BackendIsNotSupposedToImplementIt (
218- f"Output '{ vi .name } ' has unsupported type or dynamic shape"
219- )
220- outputs_info .append (info )
255+ inputs_info , outputs_info = _parse_graph_io (model .graph )
221256
222257 workdir = tempfile .mkdtemp ()
223258 try :
224- # Serialize model
225- model_path = os .path .join (workdir , "model.onnx" )
226- with open (model_path , "wb" ) as f :
227- f .write (model .SerializeToString ())
228-
229- # Generate C code with onnx2c
230- result = subprocess .run (
231- ["onnx2c" , model_path ], capture_output = True , timeout = 120
232- )
233- if result .returncode != 0 :
234- raise BackendIsNotSupposedToImplementIt (
235- f"onnx2c failed: { result .stderr .decode ()} "
236- )
237- model_c_path = os .path .join (workdir , "model.c" )
238- with open (model_c_path , "wb" ) as f :
239- f .write (result .stdout )
240-
241- # Write harness
242- harness_path = os .path .join (workdir , "harness.c" )
243- with open (harness_path , "w" ) as f :
244- f .write (_generate_harness (inputs_info , outputs_info ))
245-
246- # Compile
247- binary_path = os .path .join (workdir , "model_exec" )
248- result = subprocess .run (
249- ["gcc" , "-O2" , "-o" , binary_path , harness_path , model_c_path , "-lm" ],
250- capture_output = True ,
251- timeout = 120 ,
252- )
253- if result .returncode != 0 :
254- raise BackendIsNotSupposedToImplementIt (
255- f"Compilation failed: { result .stderr .decode ()} "
256- )
257-
259+ binary_path = _build_binary (workdir , model , inputs_info , outputs_info )
258260 return Onnx2cBackendRep (binary_path , workdir , inputs_info , outputs_info )
259-
260261 except BackendIsNotSupposedToImplementIt :
261262 shutil .rmtree (workdir , ignore_errors = True )
262263 raise
263- except Exception as e :
264+ except Exception as e : # noqa: BLE001
264265 shutil .rmtree (workdir , ignore_errors = True )
265266 raise BackendIsNotSupposedToImplementIt (str (e )) from e
266267
267268 @classmethod
268269 def run_model (cls , model , inputs , device = "CPU" , ** kwargs ):
270+ """Prepare then run a model in one call."""
269271 return cls .prepare (model , device , ** kwargs ).run (inputs )
270272
271273 @classmethod
272274 def supports_device (cls , device ):
275+ """Return whether the backend supports the given device."""
273276 return device == "CPU"
274277
275278
0 commit comments