Skip to content

Commit b2935ab

Browse files
committed
Fix FP16 external data relocation
1 parent 53d68e6 commit b2935ab

2 files changed

Lines changed: 100 additions & 50 deletions

File tree

src/winml/modelkit/quant/fp16.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,17 @@
1717

1818

1919
if TYPE_CHECKING:
20-
from onnx import ModelProto
20+
import onnx
2121

2222
logger = logging.getLogger(__name__)
2323

2424

2525
def convert_to_fp16(
26-
model: ModelProto | str | Path,
26+
model: onnx.ModelProto | str | Path,
2727
*,
2828
keep_io_types: bool = True,
2929
op_block_list: list[str] | None = None,
30-
) -> ModelProto:
30+
) -> onnx.ModelProto:
3131
"""Convert an ONNX model from FP32 to FP16 precision.
3232
3333
Uses onnxruntime.transformers.float16.convert_float_to_float16 internally.
@@ -49,24 +49,29 @@ def convert_to_fp16(
4949
Returns:
5050
The converted model (same object as input due to ORT in-place mutation).
5151
"""
52-
from onnx import TensorProto
52+
import onnx
5353
from onnxruntime.transformers.float16 import convert_float_to_float16
5454

5555
# Skip if model is already FP16 (check floating-point initializer dtypes)
56-
fp32_types = {TensorProto.FLOAT, TensorProto.DOUBLE, TensorProto.BFLOAT16}
56+
fp32_types = {onnx.TensorProto.FLOAT, onnx.TensorProto.DOUBLE, onnx.TensorProto.BFLOAT16}
5757
model_path = Path(model) if isinstance(model, str | Path) else None
5858
if model_path is not None:
59-
import onnx
60-
6159
inspection_model = onnx.load(str(model_path), load_external_data=False)
6260
else:
63-
inspection_model = cast("ModelProto", model)
61+
inspection_model = cast("onnx.ModelProto", model)
6462

6563
initializers = inspection_model.graph.initializer
6664
if initializers:
67-
float_inits = [t for t in initializers if t.data_type in fp32_types | {TensorProto.FLOAT16}]
68-
if float_inits and all(t.data_type == TensorProto.FLOAT16 for t in float_inits):
65+
float_inits = [
66+
t for t in initializers if t.data_type in fp32_types | {onnx.TensorProto.FLOAT16}
67+
]
68+
if float_inits and all(t.data_type == onnx.TensorProto.FLOAT16 for t in float_inits):
6969
logger.info("Model is already FP16 — skipping conversion.")
70+
if model_path is not None:
71+
# A graph-only load retains external-data locations relative to
72+
# the source model. Materialize those tensors before returning
73+
# so callers can safely persist the result in another directory.
74+
onnx.load_external_data_for_model(inspection_model, str(model_path.parent))
7075
return inspection_model
7176

7277
original_nodes = len(inspection_model.graph.node)
@@ -78,8 +83,6 @@ def convert_to_fp16(
7883
logger.info(" Keeping ops in FP32: %s", op_block_list)
7984

8085
if model_path is not None:
81-
from onnx.shape_inference import infer_shapes_path
82-
8386
# ORT's converter uses NamedTemporaryFile while it is still open,
8487
# which cannot be reopened by ONNX on Windows. Own the temporary path
8588
# here, close it before inference, and retain the file-based API that
@@ -89,12 +92,12 @@ def convert_to_fp16(
8992
) as temporary:
9093
inferred_path = Path(temporary.name)
9194
try:
92-
infer_shapes_path(str(model_path), str(inferred_path))
95+
onnx.shape_inference.infer_shapes_path(str(model_path), str(inferred_path))
9396
inferred_model = onnx.load(str(inferred_path))
9497
finally:
9598
inferred_path.unlink(missing_ok=True)
9699
converted = cast(
97-
"ModelProto",
100+
"onnx.ModelProto",
98101
convert_float_to_float16(
99102
inferred_model,
100103
keep_io_types=keep_io_types,
@@ -104,7 +107,7 @@ def convert_to_fp16(
104107
)
105108
else:
106109
converted = cast(
107-
"ModelProto",
110+
"onnx.ModelProto",
108111
convert_float_to_float16(
109112
model,
110113
keep_io_types=keep_io_types,

tests/unit/optim/test_fp16.py

Lines changed: 82 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515

1616
from __future__ import annotations
1717

18+
import shutil
19+
1820
import numpy as np
1921
import onnx
20-
from onnx import ModelProto, TensorProto, helper, numpy_helper
2122

23+
import winml.modelkit.onnx
2224
from winml.modelkit.quant.fp16 import convert_to_fp16
2325

2426

@@ -27,25 +29,29 @@
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

Comments
 (0)