Repository navigation
[7/17] DLA dequantization and reshape stages #2959
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
mgohil-png
wants to merge
6
commits into
dla/review-06-standalone-five-dimensional-reshape-conversion
from
dla/review-07-logged-casts-dequantization-and-reshape-stages
Draft
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
f6bf955
[DLA_transforms_7] [7/17] Logged casts, dequantization and reshape st…
mgohil-png f082a3a
Preserve DLA dequantization and reshape-stage contracts
mgohil-png 8d2da63
Preserve DLA control-flow captures and complete pattern matching
mgohil-png 0c37970
Declare computed scalar QDQ control ranks in the stage fixture
mgohil-png 8325db8
Retain generated transpose shapes for ordered reductions
mgohil-png 20d3ac1
Retain generated branch layouts for later rank stages
mgohil-png File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
265 changes: 265 additions & 0 deletions
265
modelopt/onnx/graph_surgery/dla_transforms/dla_remove_deqlin.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,265 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """Fold ``DequantizeLinear`` nodes whose data input is an initializer into a float32 initializer. | ||
|
|
||
| Skip conditions — DQ node is left in place when its output feeds: | ||
|
|
||
| * ``Conv`` weight (``input[1]``) with INT8 / UINT8 / INT4 / UINT4 zero-point. | ||
| * ``ConvTranspose`` weight with INT8 / UINT8 zero-point. | ||
| * ``MatMul`` / ``Gemm`` weight that will be converted to a ``ConvTranspose`` chain by | ||
| ``dla_matmul_to_transpose_conv_transpose`` (detected via ``check_to_apply_transpose``). | ||
| Pre-fusing the float32 weight would corrupt the per-channel quantization axis that the | ||
| conversion adjusts. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import numpy as np | ||
| import onnx | ||
| from onnx import TensorProto, helper, numpy_helper | ||
|
|
||
| from ...logging_config import logger | ||
| from ..utils.graph_utils import ( | ||
| GraphCache, | ||
| add_unique_initializers, | ||
| check_to_apply_transpose, | ||
| get_node_attr_i, | ||
| ) | ||
| from .dla_graph_cleanup import _subgraph_captures | ||
|
|
||
| # ── Zero-point types that trigger the skip rule ────────────────────────────── | ||
| _CONV_SKIP_ZP = frozenset( | ||
| {TensorProto.INT8, TensorProto.UINT8, TensorProto.INT4, TensorProto.UINT4} | ||
| ) | ||
| _CONVT_SKIP_ZP = frozenset({TensorProto.INT8, TensorProto.UINT8}) | ||
| _MATMUL_SKIP_ZP = _CONV_SKIP_ZP | ||
|
|
||
|
|
||
| # ── Helpers ─────────────────────────────────────────────────────────────────── | ||
|
|
||
|
|
||
| def _dequantize( | ||
| x_arr: np.ndarray, | ||
| scale_arr: np.ndarray, | ||
| zp_arr: np.ndarray, | ||
| axis: int, | ||
| ) -> np.ndarray: | ||
| """Compute ``(x - zero_point) * scale`` with per-channel broadcasting. | ||
|
|
||
| For scalar (rank-0) ``x`` the broadcast reshape is skipped. | ||
| """ | ||
| int32_data = x_arr.dtype == np.int32 | ||
| zp = zp_arr.astype(np.int32 if int32_data else np.float32) | ||
| scale = scale_arr.astype(np.float32) | ||
| if zp.size == 1: | ||
| zp = zp.reshape(()) | ||
| if scale.size == 1: | ||
| scale = scale.reshape(()) | ||
| if x_arr.ndim > 0 and (zp.size > 1 or scale.size > 1): | ||
| broadcast_shape = [1] * x_arr.ndim | ||
| broadcast_shape[axis] = -1 | ||
| zp = zp.reshape(broadcast_shape) | ||
| scale = scale.reshape(broadcast_shape) | ||
| if int32_data: | ||
| # Keep established INT32 rounding without a whole-weight float64 temporary. | ||
| result = np.empty(x_arr.shape, dtype=np.float32) | ||
| with np.nditer( | ||
| [x_arr, zp, scale, result], | ||
| flags=["external_loop", "buffered", "zerosize_ok"], | ||
| op_flags=[["readonly"], ["readonly"], ["readonly"], ["writeonly"]], | ||
| buffersize=2**18, | ||
| ) as chunks: | ||
| for values, zeros, scales, output in chunks: | ||
| output[...] = (values - zeros) * scales | ||
| return result | ||
| result = x_arr.astype(np.float32) | ||
| np.subtract(result, zp, out=result) | ||
| np.multiply(result, scale, out=result) | ||
| return result | ||
|
|
||
|
|
||
| def _should_skip( | ||
| dq_output: str, | ||
| zp_dtype: int, | ||
| consumer_map: dict, | ||
| model: onnx.ModelProto, | ||
| shape_map: dict, | ||
| ) -> bool: | ||
| """Retain weights required or potentially required by any downstream consumer.""" | ||
| for consumer_node, input_idx in consumer_map.get(dq_output, []): | ||
| if input_idx != 1 or consumer_node.domain not in {"", "ai.onnx"} or consumer_node.overload: | ||
| continue | ||
| op = consumer_node.op_type | ||
| if op == "Conv" and zp_dtype in _CONV_SKIP_ZP: | ||
| return True | ||
| if op == "ConvTranspose" and zp_dtype in _CONVT_SKIP_ZP: | ||
| return True | ||
| if op in ("MatMul", "Gemm") and zp_dtype in _MATMUL_SKIP_ZP: | ||
| try: | ||
| if check_to_apply_transpose(consumer_node, model, shape_map): | ||
| return True | ||
| except (ValueError, RuntimeError): | ||
| return True | ||
| return False | ||
|
|
||
|
|
||
| # ── Main transform ───────────────────────────────────────────────────────────── | ||
|
|
||
|
|
||
| def _apply_remove_deqlin(model: onnx.ModelProto) -> onnx.ModelProto: | ||
| """Replace eligible DequantizeLinear nodes with pre-computed float32 initializers.""" | ||
| graph = model.graph | ||
| cache = GraphCache(graph) | ||
| captured = _subgraph_captures(graph) | ||
| local_functions = { | ||
| (function.domain, function.name, function.overload) for function in model.functions | ||
| } | ||
| opsets = {entry.domain: entry.version for entry in model.opset_import} | ||
|
|
||
| # Snapshot for check_to_apply_transpose (expects a plain dict) | ||
| shape_map_snapshot: dict = {} | ||
| for vi in list(graph.value_info) + list(graph.input) + list(graph.output): | ||
| s = cache.get_shape(vi.name) | ||
| if s is not None: | ||
| shape_map_snapshot[vi.name] = s | ||
|
|
||
| consumer_map = cache.consumer_map | ||
| initializer_names = cache.init_names | ||
| unique_init: set[str] = set(cache.init_names) | ||
| nodes_to_remove: list = [] | ||
| cnt = 0 | ||
|
|
||
| for node in cache.nodes_by_op("DequantizeLinear"): | ||
| if node.overload or (node.domain, node.op_type, node.overload) in local_functions: | ||
| continue | ||
| if node.domain not in {"", "ai.onnx"} and not ( | ||
| node.domain == "com.microsoft" and opsets.get(node.domain) == 1 | ||
| ): | ||
| continue | ||
| if any(name in cache.graph_input_names for name in node.input): | ||
| continue | ||
| if get_node_attr_i(node, "block_size", 0): | ||
| continue | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| if not node.input or node.input[0] not in initializer_names: | ||
| continue # data input is not a static initializer — skip | ||
|
|
||
| dq_output = node.output[0] | ||
| if dq_output in captured: | ||
| continue | ||
|
|
||
| # Fetch scale and zero-point initializers (inputs 1 and 2) | ||
| scale_init = cache.get_init(node.input[1]) if len(node.input) > 1 else None | ||
| zp_init = cache.get_init(node.input[2]) if len(node.input) > 2 else None | ||
|
|
||
| if len(node.input) > 2 and node.input[2] and zp_init is None: | ||
| continue | ||
| x_init = cache.get_init(node.input[0]) | ||
| if x_init is None: | ||
| continue | ||
| if ( | ||
| x_init.data_type | ||
| not in _CONV_SKIP_ZP | {TensorProto.INT16, TensorProto.UINT16, TensorProto.INT32} | ||
| or (scale_init is not None and scale_init.data_type != TensorProto.FLOAT) | ||
| or get_node_attr_i(node, "output_dtype", 0) not in {0, TensorProto.FLOAT} | ||
| ): | ||
| continue | ||
|
|
||
| if scale_init is None: | ||
| logger.debug( | ||
| "DequantizeLinear %r: scale not found as initializer; skipping.", | ||
| node.name, | ||
| ) | ||
| continue | ||
|
|
||
| if zp_init is None: | ||
| logger.debug( | ||
| "DequantizeLinear %r: zero_point not found as initializer; using 0 as zero_point.", | ||
| node.name, | ||
| ) | ||
|
|
||
| zp_dtype: int = zp_init.data_type if zp_init is not None else x_init.data_type | ||
|
|
||
| if _should_skip(dq_output, zp_dtype, consumer_map, model, shape_map_snapshot): | ||
| logger.debug( | ||
| "DequantizeLinear %r: skip (downstream op keeps quantised weight).", node.name | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| ) | ||
| continue | ||
|
|
||
| # ── Dequantize ──────────────────────────────────────────────────────── | ||
| x_arr = numpy_helper.to_array(x_init) | ||
| scale_arr = numpy_helper.to_array(scale_init) | ||
| zp_arr = ( | ||
| numpy_helper.to_array(zp_init) | ||
| if zp_init is not None | ||
| else np.zeros(scale_arr.shape, dtype=np.int32) | ||
| ) | ||
|
|
||
| axis = get_node_attr_i(node, "axis", 1) | ||
|
|
||
| dequantized = _dequantize(x_arr, scale_arr, zp_arr, axis) | ||
| del x_arr, scale_arr, zp_arr | ||
| new_name = node.input[0] + "_dequantized" | ||
| previous = cache.get_init(new_name) | ||
| if previous is None: | ||
| new_name = cache.unique_name(new_name) | ||
| if previous is not None and ( | ||
| new_name in cache.graph_input_names | ||
| or list(previous.dims) != list(dequantized.shape) | ||
| or previous.data_type != TensorProto.FLOAT | ||
| or numpy_helper.to_array(previous).tobytes() != dequantized.tobytes() | ||
| ): | ||
| new_name = cache.unique_name(new_name) | ||
| new_init = ( | ||
| numpy_helper.from_array(dequantized, name=new_name) | ||
| if new_name not in unique_init | ||
| else None | ||
| ) | ||
| del dequantized | ||
|
|
||
| # ── Rewire all consumers ────────────────────────────────────────────── | ||
| for consumer_node, _ in consumer_map.get(dq_output, []): | ||
| for i, inp in enumerate(consumer_node.input): | ||
| if inp == dq_output: | ||
| consumer_node.input[i] = new_name | ||
| # no break — update every occurrence of dq_output in this node's inputs | ||
|
|
||
| if new_init is not None: | ||
| add_unique_initializers(graph, unique_init, [new_init]) | ||
| cache.init_map[new_name] = graph.initializer[-1] | ||
| del new_init | ||
| if dq_output in cache.graph_output_names: | ||
| position = next(index for index, current in enumerate(graph.node) if current is node) | ||
| graph.node.insert( | ||
| position, | ||
| helper.make_node( | ||
| "Identity", | ||
| [new_name], | ||
| [dq_output], | ||
| name=cache.unique_name(node.name + "_output"), | ||
| ), | ||
| ) | ||
|
|
||
| nodes_to_remove.append(node) | ||
| cnt += 1 | ||
| logger.debug("DequantizeLinear %r → float32 initializer %r.", node.name, new_name) | ||
|
|
||
| for node in nodes_to_remove: | ||
| graph.node.remove(node) | ||
|
|
||
| logger.debug( | ||
| "remove_deqlin: folded %d DequantizeLinear node(s) into float32 initializers.", cnt | ||
| ) | ||
| return model | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: NVIDIA/Model-Optimizer
Length of output: 5835
🏁 Script executed:
Repository: NVIDIA/Model-Optimizer
Length of output: 21910
🏁 Script executed:
Repository: NVIDIA/Model-Optimizer
Length of output: 41544
🏁 Script executed:
Repository: NVIDIA/Model-Optimizer
Length of output: 8821
Preserve DQ for every qualifying consumer.
A shared DQ output that feeds a quantized
Convweight and another consumer fails thelen(consumers) != 1gate. The transform then rewires theConvto a float32 initializer and removes the DQ, violating the required quantized-weight path.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in 9324929. The skip decision now checks every consumer and preserves DQ when any supported quantized-weight use requires it. Four checked Conv cases cover shared/unshared uses with present/omitted zero points; the three previously broken combinations fail against the preceding source and now preserve the graph byte-for-byte.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Length of output: 23188
🏁 Script executed:
Length of output: 21303
@mgohil-pngVerified by source inspection at932492983b._should_skipnow checks all consumers and retains DQ if any qualifying quantized-weight use requires it.The four Conv cases cover shared/unshared uses with present/omitted zero points. They assert byte-for-byte graph preservation and output equality. I did not run the tests.
This addresses the finding. Per
AGENTS.md, I will leave thread resolution to a human reviewer.🧠 Learnings used
You are interacting with an AI system.