1515
1616from __future__ import annotations
1717
18+ import shutil
19+
1820import numpy as np
1921import onnx
20- from onnx import ModelProto , TensorProto , helper , numpy_helper
2122
23+ import winml .modelkit .onnx
2224from winml .modelkit .quant .fp16 import convert_to_fp16
2325
2426
2729# =============================================================================
2830
2931
30- def _build_simple_fp32_model () -> ModelProto :
32+ def _build_simple_fp32_model () -> onnx . ModelProto :
3133 """Build a simple FP32 model: out = x + weight."""
32- x = helper .make_tensor_value_info ("x" , TensorProto .FLOAT , [1 , 4 ])
33- out = helper .make_tensor_value_info ("out" , TensorProto .FLOAT , [1 , 4 ])
34- weight = numpy_helper .from_array (np .array ([[1.0 , 2.0 , 3.0 , 4.0 ]], dtype = np .float32 ), "weight" )
35- add = helper .make_node ("Add" , ["x" , "weight" ], ["out" ], name = "add" )
36- graph = helper .make_graph ([add ], "simple" , [x ], [out ], [weight ])
37- return helper .make_model (graph , opset_imports = [helper .make_opsetid ("" , 17 )])
34+ x = onnx .helper .make_tensor_value_info ("x" , onnx .TensorProto .FLOAT , [1 , 4 ])
35+ out = onnx .helper .make_tensor_value_info ("out" , onnx .TensorProto .FLOAT , [1 , 4 ])
36+ weight = onnx .numpy_helper .from_array (
37+ np .array ([[1.0 , 2.0 , 3.0 , 4.0 ]], dtype = np .float32 ), "weight"
38+ )
39+ add = onnx .helper .make_node ("Add" , ["x" , "weight" ], ["out" ], name = "add" )
40+ graph = onnx .helper .make_graph ([add ], "simple" , [x ], [out ], [weight ])
41+ return onnx .helper .make_model (graph , opset_imports = [onnx .helper .make_opsetid ("" , 17 )])
3842
3943
40- def _build_multi_op_fp32_model () -> ModelProto :
44+ def _build_multi_op_fp32_model () -> onnx . ModelProto :
4145 """Build a model with multiple ops: out = Relu(x + weight)."""
42- x = helper .make_tensor_value_info ("x" , TensorProto .FLOAT , [1 , 4 ])
43- out = helper .make_tensor_value_info ("out" , TensorProto .FLOAT , [1 , 4 ])
44- weight = numpy_helper .from_array (np .array ([[1.0 , 2.0 , 3.0 , 4.0 ]], dtype = np .float32 ), "weight" )
45- add = helper .make_node ("Add" , ["x" , "weight" ], ["add_out" ], name = "add" )
46- relu = helper .make_node ("Relu" , ["add_out" ], ["out" ], name = "relu" )
47- graph = helper .make_graph ([add , relu ], "multi_op" , [x ], [out ], [weight ])
48- return helper .make_model (graph , opset_imports = [helper .make_opsetid ("" , 17 )])
46+ x = onnx .helper .make_tensor_value_info ("x" , onnx .TensorProto .FLOAT , [1 , 4 ])
47+ out = onnx .helper .make_tensor_value_info ("out" , onnx .TensorProto .FLOAT , [1 , 4 ])
48+ weight = onnx .numpy_helper .from_array (
49+ np .array ([[1.0 , 2.0 , 3.0 , 4.0 ]], dtype = np .float32 ), "weight"
50+ )
51+ add = onnx .helper .make_node ("Add" , ["x" , "weight" ], ["add_out" ], name = "add" )
52+ relu = onnx .helper .make_node ("Relu" , ["add_out" ], ["out" ], name = "relu" )
53+ graph = onnx .helper .make_graph ([add , relu ], "multi_op" , [x ], [out ], [weight ])
54+ return onnx .helper .make_model (graph , opset_imports = [onnx .helper .make_opsetid ("" , 17 )])
4955
5056
5157# =============================================================================
@@ -61,7 +67,9 @@ def test_converts_weights_to_fp16(self) -> None:
6167 model = _build_simple_fp32_model ()
6268 result = convert_to_fp16 (model )
6369
64- has_fp16 = any (init .data_type == TensorProto .FLOAT16 for init in result .graph .initializer )
70+ has_fp16 = any (
71+ init .data_type == onnx .TensorProto .FLOAT16 for init in result .graph .initializer
72+ )
6573 assert has_fp16 , "Expected at least one FP16 initializer after conversion"
6674
6775 def test_path_conversion_uses_external_data_safe_shape_inference (self , tmp_path ) -> None :
@@ -79,27 +87,66 @@ def test_path_conversion_uses_external_data_safe_shape_inference(self, tmp_path)
7987
8088 result = convert_to_fp16 (model_path )
8189
82- assert any (init .data_type == TensorProto .FLOAT16 for init in result .graph .initializer )
90+ assert any (init .data_type == onnx .TensorProto .FLOAT16 for init in result .graph .initializer )
91+
92+ def test_already_fp16_external_data_path_is_relocatable (self , tmp_path ) -> None :
93+ """Already-FP16 path input can be saved independently of its source sidecar."""
94+ source_dir = tmp_path / "source"
95+ source_dir .mkdir ()
96+ source_path = source_dir / "source.onnx"
97+ expected_weight = np .arange (1024 , dtype = np .float16 ).reshape (1 , 1024 )
98+ x = onnx .helper .make_tensor_value_info ("x" , onnx .TensorProto .FLOAT16 , [1 , 1024 ])
99+ out = onnx .helper .make_tensor_value_info ("out" , onnx .TensorProto .FLOAT16 , [1 , 1024 ])
100+ weight = onnx .numpy_helper .from_array (expected_weight , "weight" )
101+ graph = onnx .helper .make_graph (
102+ [onnx .helper .make_node ("Add" , ["x" , "weight" ], ["out" ])],
103+ "external_fp16" ,
104+ [x ],
105+ [out ],
106+ [weight ],
107+ )
108+ model = onnx .helper .make_model (graph , opset_imports = [onnx .helper .make_opsetid ("" , 17 )])
109+ onnx .save_model (
110+ model ,
111+ str (source_path ),
112+ save_as_external_data = True ,
113+ all_tensors_to_one_file = True ,
114+ location = "source.onnx.data" ,
115+ size_threshold = 0 ,
116+ )
117+
118+ converted = convert_to_fp16 (source_path )
119+ destination_dir = tmp_path / "destination"
120+ destination_path = destination_dir / "model.onnx"
121+ winml .modelkit .onnx .save_onnx (converted , destination_path , threshold_size = 0 )
122+
123+ assert (destination_dir / "model.onnx.data" ).is_file ()
124+ shutil .rmtree (source_dir )
125+ onnx .checker .check_model (str (destination_path ))
126+ relocated = onnx .load (str (destination_path ))
127+ np .testing .assert_array_equal (
128+ onnx .numpy_helper .to_array (relocated .graph .initializer [0 ]), expected_weight
129+ )
83130
84131 def test_default_keeps_io_types (self ) -> None :
85132 """Default keep_io_types=True preserves FP32 model I/O."""
86133 model = _build_simple_fp32_model ()
87134 result = convert_to_fp16 (model , keep_io_types = True )
88135
89136 for inp in result .graph .input :
90- assert inp .type .tensor_type .elem_type == TensorProto .FLOAT
137+ assert inp .type .tensor_type .elem_type == onnx . TensorProto .FLOAT
91138 for outp in result .graph .output :
92- assert outp .type .tensor_type .elem_type == TensorProto .FLOAT
139+ assert outp .type .tensor_type .elem_type == onnx . TensorProto .FLOAT
93140
94141 def test_keep_io_types_false_converts_io (self ) -> None :
95142 """With keep_io_types=False, model I/O becomes FP16."""
96143 model = _build_simple_fp32_model ()
97144 result = convert_to_fp16 (model , keep_io_types = False )
98145
99146 for inp in result .graph .input :
100- assert inp .type .tensor_type .elem_type == TensorProto .FLOAT16
147+ assert inp .type .tensor_type .elem_type == onnx . TensorProto .FLOAT16
101148 for outp in result .graph .output :
102- assert outp .type .tensor_type .elem_type == TensorProto .FLOAT16
149+ assert outp .type .tensor_type .elem_type == onnx . TensorProto .FLOAT16
103150
104151 def test_preserves_model_structure (self ) -> None :
105152 """FP16 conversion preserves graph structure (node count diff ≤ 2)."""
@@ -131,13 +178,13 @@ def test_none_op_block_list_uses_ort_defaults(self) -> None:
131178 def test_skips_already_fp16_model (self ) -> None :
132179 """If all floating-point initializers are already FP16, conversion is skipped."""
133180 # Build a model with FP16 initializers directly
134- x = helper .make_tensor_value_info ("x" , TensorProto .FLOAT16 , [1 , 4 ])
135- out = helper .make_tensor_value_info ("out" , TensorProto .FLOAT16 , [1 , 4 ])
181+ x = onnx . helper .make_tensor_value_info ("x" , onnx . TensorProto .FLOAT16 , [1 , 4 ])
182+ out = onnx . helper .make_tensor_value_info ("out" , onnx . TensorProto .FLOAT16 , [1 , 4 ])
136183 weight_data = np .array ([[1.0 , 2.0 , 3.0 , 4.0 ]], dtype = np .float16 )
137- weight = numpy_helper .from_array (weight_data , "weight" )
138- add = helper .make_node ("Add" , ["x" , "weight" ], ["out" ], name = "add" )
139- graph = helper .make_graph ([add ], "fp16_model" , [x ], [out ], [weight ])
140- model = helper .make_model (graph , opset_imports = [helper .make_opsetid ("" , 17 )])
184+ weight = onnx . numpy_helper .from_array (weight_data , "weight" )
185+ add = onnx . helper .make_node ("Add" , ["x" , "weight" ], ["out" ], name = "add" )
186+ graph = onnx . helper .make_graph ([add ], "fp16_model" , [x ], [out ], [weight ])
187+ model = onnx . helper .make_model (graph , opset_imports = [onnx . helper .make_opsetid ("" , 17 )])
141188
142189 original_nodes = len (model .graph .node )
143190 result = convert_to_fp16 (model )
@@ -148,15 +195,15 @@ def test_skips_already_fp16_model(self) -> None:
148195
149196 def test_skips_fp16_model_with_int_initializers (self ) -> None :
150197 """FP16 model with non-float initializers (e.g. INT64 shapes) should still skip."""
151- x = helper .make_tensor_value_info ("x" , TensorProto .FLOAT16 , [1 , 4 ])
152- out = helper .make_tensor_value_info ("out" , TensorProto .FLOAT16 , [1 , 4 ])
198+ x = onnx . helper .make_tensor_value_info ("x" , onnx . TensorProto .FLOAT16 , [1 , 4 ])
199+ out = onnx . helper .make_tensor_value_info ("out" , onnx . TensorProto .FLOAT16 , [1 , 4 ])
153200 weight_data = np .array ([[1.0 , 2.0 , 3.0 , 4.0 ]], dtype = np .float16 )
154- weight = numpy_helper .from_array (weight_data , "weight" )
201+ weight = onnx . numpy_helper .from_array (weight_data , "weight" )
155202 # INT64 initializer (e.g., shape tensor) — should be ignored by skip logic
156- shape_tensor = numpy_helper .from_array (np .array ([1 , 4 ], dtype = np .int64 ), "shape" )
157- add = helper .make_node ("Add" , ["x" , "weight" ], ["out" ], name = "add" )
158- graph = helper .make_graph ([add ], "fp16_mixed" , [x ], [out ], [weight , shape_tensor ])
159- model = helper .make_model (graph , opset_imports = [helper .make_opsetid ("" , 17 )])
203+ shape_tensor = onnx . numpy_helper .from_array (np .array ([1 , 4 ], dtype = np .int64 ), "shape" )
204+ add = onnx . helper .make_node ("Add" , ["x" , "weight" ], ["out" ], name = "add" )
205+ graph = onnx . helper .make_graph ([add ], "fp16_mixed" , [x ], [out ], [weight , shape_tensor ])
206+ model = onnx . helper .make_model (graph , opset_imports = [onnx . helper .make_opsetid ("" , 17 )])
160207
161208 original_nodes = len (model .graph .node )
162209 result = convert_to_fp16 (model )
0 commit comments