Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b4b5415
Do not load external data when analyze
chinazhangchao Apr 22, 2026
37063e7
Merge branch 'main' into chao/largemodel
chinazhangchao Apr 22, 2026
ee1350f
Merge branch 'main' of https://github.com/microsoft/WinML-ModelKit in…
chinazhangchao Apr 23, 2026
3e812e9
Update src/winml/modelkit/analyze/core/runtime_checker_query.py
chinazhangchao Apr 23, 2026
5e0089a
Update src/winml/modelkit/analyze/core/runtime_checker_query.py
chinazhangchao Apr 23, 2026
289404c
fix comments
chinazhangchao Apr 23, 2026
e22dcf1
fix comments
chinazhangchao Apr 23, 2026
889c70a
Potential fix for pull request finding 'CodeQL / Module is imported w…
chinazhangchao Apr 23, 2026
b52d269
Potential fix for pull request finding 'CodeQL / Module is imported w…
chinazhangchao Apr 23, 2026
7cc4544
Merge branch 'main' into chao/largemodel
chinazhangchao Apr 24, 2026
876e8f0
Merge branch 'main' of https://github.com/microsoft/WinML-ModelKit in…
chinazhangchao Apr 27, 2026
d8492dd
Merge branch 'main' of https://github.com/microsoft/WinML-ModelKit in…
chinazhangchao Apr 27, 2026
09ef033
fix test
chinazhangchao Apr 27, 2026
6363edc
Merge branch 'main' of https://github.com/microsoft/WinML-ModelKit in…
chinazhangchao Apr 27, 2026
4fb32c6
fix lint
chinazhangchao Apr 27, 2026
b62c552
fix comments
chinazhangchao Apr 27, 2026
4bdc6d0
Merge branch 'main' of https://github.com/microsoft/WinML-ModelKit in…
chinazhangchao Apr 27, 2026
f92fea3
Merge branch 'main' into chao/largemodel
chinazhangchao Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/winml/modelkit/analyze/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,8 +581,9 @@ def analyze(

# Load ONNX model
try:
# Load without strict validation to allow custom attributes like hierarchy_tag
model_proto = onnx.load(str(model_file), load_external_data=True)
# Load without external data — static analysis only needs graph structure,
# shapes, and small embedded constants; not multi-GB weight tensors.
model_proto = onnx.load(str(model_file), load_external_data=False)
# Skip onnx.checker.check_model() which rejects custom attributes
except (OSError, FileNotFoundError) as e:
raise RuntimeError(f"Failed to load ONNX model: {e}") from e
Expand Down
2 changes: 1 addition & 1 deletion src/winml/modelkit/analyze/core/onnx_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def load(self) -> ONNXModel:
# FR-001: Load ONNX model from file
try:
logger.info("Loading ONNX model from: %s", self._model_path)
model_proto = onnx.load(str(self._model_path))
model_proto = onnx.load(str(self._model_path), load_external_data=False)
except Exception as e:
# FR-038: Provide clear error message
raise ONNXLoadError(
Expand Down
1 change: 1 addition & 0 deletions src/winml/modelkit/analyze/core/runtime_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def _get_query(self) -> RuntimeCheckerQuery:
model_proto=model_proto,
ep_name=self._ep,
device_type=self._device,
model_path=self._model.model_path,
dynamic_axis_strict_mode=self._dynamic_axis_strict_mode,
)

Expand Down
185 changes: 169 additions & 16 deletions src/winml/modelkit/analyze/core/runtime_checker_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,7 @@ def _expand_snapshot_payload(
base_opset = payload.get(SNAPSHOT_BASE_OPSET_KEY)
if not isinstance(base_opset, int):
logger.warning(
"Delta snapshot %s in %s missing integer %s; "
"applying delta on empty base.",
"Delta snapshot %s in %s missing integer %s; applying delta on empty base.",
file_name,
zip_path,
SNAPSHOT_BASE_OPSET_KEY,
Expand Down Expand Up @@ -217,8 +216,7 @@ def _expand_snapshot_payload(
base_raw = _read_json_from_zip(base_zip_path, base_file_name)
if base_raw is None:
logger.warning(
"Base snapshot %s not found in %s (from %s); "
"applying delta on empty base.",
"Base snapshot %s not found in %s (from %s); applying delta on empty base.",
base_file_name,
base_zip_path,
file_name,
Expand Down Expand Up @@ -633,6 +631,103 @@ def _normalize_type_var_annotation(type_value: str) -> str:
return SupportedONNXType.normalize_annotation(type_value)


# Query conditions are later normalized with make_hashable, which materializes
# numpy arrays. Keep external initializer loading bounded until the broader
# condition pipeline can stay lazy end-to-end.
MAX_EXTERNAL_INITIALIZER_BYTES_FOR_QUERY = 1024 * 1024


def _get_external_tensor_info(tensor: onnx.TensorProto) -> tuple[str | None, int, int | None]:
"""Extract location, offset, and length metadata for an external tensor."""
info = {entry.key: entry.value for entry in tensor.external_data}
location = info.get("location")
offset = int(info.get("offset", "0"))
length = int(info["length"]) if "length" in info else None
return location, offset, length


def _tensor_proto_dtype_to_np_dtype(tensor_type: int) -> np.dtype[Any]:
Comment thread
chinazhangchao marked this conversation as resolved.
"""Convert a TensorProto dtype enum to a numpy dtype."""
try:
from onnx.helper import tensor_dtype_to_np_dtype as onnx_tensor_dtype_to_np_dtype
except ImportError:
from onnx.mapping import TENSOR_TYPE_TO_NP_TYPE

return np.dtype(TENSOR_TYPE_TO_NP_TYPE[tensor_type])

return np.dtype(onnx_tensor_dtype_to_np_dtype(tensor_type))


def _try_load_external_initializer_array(
tensor: onnx.TensorProto,
model_path: str | Path | None,
) -> np.ndarray | None:
"""Load a small external initializer from disk for runtime-query conditions.

Returns None when the model path is unavailable, metadata is incomplete,
the sidecar file is missing, or the tensor is too large for the current
hash-based condition pipeline.
"""
if tensor.data_location != onnx.TensorProto.EXTERNAL or model_path is None:
return None

location, offset, length = _get_external_tensor_info(tensor)
if location is None:
return None

model_path = Path(model_path)
data_path = Path(location)
if not data_path.is_absolute():
data_path = model_path.parent / data_path
if not data_path.exists():
return None

try:
np_dtype = _tensor_proto_dtype_to_np_dtype(tensor.data_type)
shape = tuple(tensor.dims)
numel = int(np.prod(shape)) if shape else 1
expected_bytes = numel * np_dtype.itemsize
if length is not None and length < expected_bytes:
logger.debug(
"External initializer %s length %s is smaller than expected %s",
tensor.name,
length,
expected_bytes,
)
return None
if expected_bytes > MAX_EXTERNAL_INITIALIZER_BYTES_FOR_QUERY:
logger.debug(
"Skipping external initializer %s for query conditions because %s bytes exceeds %s",
tensor.name,
expected_bytes,
MAX_EXTERNAL_INITIALIZER_BYTES_FOR_QUERY,
)
return None

with data_path.open("rb") as f:
f.seek(offset)
arr = np.fromfile(f, dtype=np_dtype, count=numel)

if arr.size != numel:
logger.debug(
"External initializer %s only yielded %s elements, expected %s",
tensor.name,
arr.size,
numel,
)
return None

return arr.reshape(shape)
Comment thread
chinazhangchao marked this conversation as resolved.
except Exception as e:
logger.debug(
"Failed to read external initializer %s from %s: %s",
tensor.name,
data_path,
e,
)
return None


def _get_pattern_type_var_conditions(
pattern_match: PatternMatchResult,
gen: Any,
Expand Down Expand Up @@ -753,6 +848,7 @@ def get_query_conditions_for_node(
domain: ONNXDomain,
input_to_dq: dict[str, QDQTypeInfo],
output_to_q: dict[str, QDQTypeInfo],
model_path: str | Path | None = None,
dynamic_axis_strict_mode: bool = False,
) -> tuple[dict[str, Any], list[str], bool]:
"""Extract query conditions for runtime checking of an ONNX node.
Expand All @@ -766,6 +862,7 @@ def get_query_conditions_for_node(
domain: ONNX domain of the node.
input_to_dq: Maps DQ output name -> QDQTypeInfo (for nodes consuming DQ outputs).
output_to_q: Maps Q input name -> QDQTypeInfo (for nodes producing Q inputs).
model_path: Path to the source ONNX model when external data may need to be resolved.
dynamic_axis_strict_mode: If False (default), maps any dynamic axes to (0,)
for matching against first_axis test data. If True, preserves exact indices.

Expand Down Expand Up @@ -855,8 +952,8 @@ def update_conditions_(
input_name: str,
is_variadic: bool,
is_constant: bool,
shape: tuple | None = None,
value: tuple | None = None,
shape: tuple | list | None = None,
value: Any = None,
):
dyn_axes = _compute_dynamic_axes(shape, is_constant)
if is_variadic:
Expand Down Expand Up @@ -927,8 +1024,31 @@ def update_conditions_(

if inp_name in initializers:
init = initializers[inp_name]
arr = numpy_helper.to_array(init)
update_conditions_(conditions, input_name, is_variadic, True, arr.shape, arr)
# External data initializers (large weights) may not have data loaded;
# keep the known shape but treat them as non-constant when the payload
# is unavailable so downstream rules do not assume *_value is usable.
# External-data tensors keep their payload out-of-line, so the typed
# TensorProto data fields should also be empty; raw_data is therefore
# the relevant signal for whether bytes were loaded into this proto.
external_arr = None
if init.data_location == onnx.TensorProto.EXTERNAL and not init.raw_data:
external_arr = _try_load_external_initializer_array(init, model_path)

if external_arr is not None:
update_conditions_(
conditions,
input_name,
is_variadic,
True,
external_arr.shape,
external_arr,
)
else:
shape = tuple(init.dims) if init.dims is not None else None
Comment thread
chinazhangchao marked this conversation as resolved.
update_conditions_(conditions, input_name, is_variadic, False, shape, None)
else:
arr = numpy_helper.to_array(init)
update_conditions_(conditions, input_name, is_variadic, True, arr.shape, arr)
Comment thread
chinazhangchao marked this conversation as resolved.
Comment thread
chinazhangchao marked this conversation as resolved.
conditions[f"{input_name}_is_none"] = False

# Add type_vars info for initializers
Expand Down Expand Up @@ -1102,6 +1222,7 @@ def __init__(
model_proto: onnx.ModelProto,
ep_name: str,
device_type: str,
model_path: str | Path | None = None,
dynamic_axis_strict_mode: bool = False,
) -> None:
"""Initialize runtime checker query.
Expand All @@ -1110,11 +1231,13 @@ def __init__(
model_proto: ONNX model proto
ep_name: Execution provider name
device_type: Device type (e.g., "CPU", "GPU", "NPU")
model_path: Path to the source ONNX model for resolving external data.
dynamic_axis_strict_mode: If False (default), maps any dynamic axes to (0,)
for matching against first_axis test data. If True, preserves exact
dynamic axis indices.
"""
self.dynamic_axis_strict_mode = dynamic_axis_strict_mode
self.model_path = Path(model_path) if model_path is not None else None
# Try shape inference: standard ONNX first, then symbolic (onnxruntime)
try:
# Standard ONNX shape inference — uses temp file for models
Expand Down Expand Up @@ -1356,7 +1479,23 @@ def _build_single_node_model(
if not inp_name:
continue
if inp_name in self.initializers:
graph_initializers.append(self.initializers[inp_name])
init = self.initializers[inp_name]
# Skip external data initializers without loaded data;
# treat them as graph inputs instead so the model stays valid.
if init.data_location == onnx.TensorProto.EXTERNAL and not init.raw_data:
vi = self.valueinfo.get(inp_name)
if vi is not None:
graph_inputs.append(vi)
else:
# init.dims is a protobuf repeated field (always a sequence, never None).
# For external-data initializers the dims metadata is always present.
graph_inputs.append(
onnx.helper.make_tensor_value_info(
inp_name, init.data_type, list(init.dims)
)
Comment thread
chinazhangchao marked this conversation as resolved.
)
Comment thread
chinazhangchao marked this conversation as resolved.
else:
graph_initializers.append(init)
elif inp_name in self.constants:
# Convert Constant node output to initializer
graph_initializers.append(self.constants[inp_name])
Expand Down Expand Up @@ -1433,18 +1572,31 @@ def _generate_node_inputs(self, node: onnx.NodeProto) -> dict[str, np.ndarray]:
for inp_name in node.input:
if not inp_name:
continue
# Skip initializers and constants - they are embedded in the model
if inp_name in self.initializers or inp_name in self.constants:
# Skip initializers and constants - they are embedded in the model.
# Exception: external-data initializers without loaded raw_data are
# converted to graph inputs by _build_single_node_model, so we must
# generate dummy data for them.
if inp_name in self.initializers:
init = self.initializers[inp_name]
if not (init.data_location == onnx.TensorProto.EXTERNAL and not init.raw_data):
continue
elif inp_name in self.constants:
continue

vi = self.valueinfo.get(inp_name)
if vi is None:
raise ValueError(
f"Input '{inp_name}' for node '{node.name}' ({node.op_type}) "
f"not found in valueinfo"
)
init = self.initializers.get(inp_name)
if init is not None and init.data_location == onnx.TensorProto.EXTERNAL:
shape = tuple(init.dims)
dtype_str = dtype_from_tensorproto_enum(init.data_type)
else:
raise ValueError(
f"Input '{inp_name}' for node '{node.name}' ({node.op_type}) "
f"not found in valueinfo"
)
else:
shape, dtype_str = shape_and_dtype_from_valueinfo(vi)

shape, dtype_str = shape_and_dtype_from_valueinfo(vi)
if dtype_str is None:
raise ValueError(
f"Input '{inp_name}' for node '{node.name}' ({node.op_type}) "
Expand Down Expand Up @@ -1932,6 +2084,7 @@ def get_pattern_id(is_qdq):
op_domain,
self.input_to_dq_type,
self.output_to_q_type,
model_path=self.model_path,
dynamic_axis_strict_mode=self.dynamic_axis_strict_mode,
)
except (
Expand Down
Loading
Loading