-
Notifications
You must be signed in to change notification settings - Fork 72
Eliminate unnecessary ScatterND #2422
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
Merged
Merged
Changes from all commits
Commits
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
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,65 @@ | ||
# Copyright (c) Microsoft Corporation. | ||
# Licensed under the MIT License. | ||
"""Rewrite rule to eliminate redundant ScatterND operations. | ||
|
||
Identify ScatterND(data, indices, updates) that can be replaced by Identity(updates). | ||
This is generated by the translation of `x[:, ...] = y` in PyTorch. | ||
The specific pattern is that the updated indices take the form [[0], ..., [S-1]] for the first dimension, | ||
where S is the size of the first dimension of the updated-data tensor. | ||
In effect, the scatter-update ends up being an assignment of a new value to the entire tensor. | ||
""" | ||
|
||
from __future__ import annotations | ||
|
||
import onnx_ir as ir | ||
|
||
import onnxscript.rewriter | ||
from onnxscript.rewriter import _ir_utils as ir_utils | ||
from onnxscript.rewriter import pattern as orp | ||
|
||
|
||
def fail(*args): | ||
return onnxscript.rewriter.MatchResult().fail(*args) | ||
|
||
|
||
class ScatterAll(orp.RewriteRuleClassBase): | ||
def pattern(self, op, data, axis, transposed_data, updates): | ||
# Construct update-indices spanning an entire axis: | ||
shape = op.Shape(data, start=0) | ||
dim = op.Gather(shape, axis, axis=0) | ||
full_range = op.Range(0, dim, 1) | ||
full_range_2d = op.Unsqueeze(full_range, [-1]) | ||
# The update is applied to the data transposed to bring the updated axis to the front: | ||
return op.ScatterND(transposed_data, full_range_2d, updates, reduction="none") | ||
|
||
def check(self, context, data, axis, transposed_data, **_): | ||
# Check that updated-indices represent the full range of the first dimension of the transposed data. | ||
# That is: check that the data.shape[axis] matches transposed_data.shape[0]. | ||
axis_value = ir_utils.get_singleton_value(axis) | ||
if not isinstance(axis_value, int): | ||
return fail("Axis value must be a constant integer.", axis) | ||
shape: ir.Shape | None = data.shape | ||
if shape is None: | ||
return fail("Data shape is not statically known.", data) | ||
updated_dim_value = shape[axis_value] | ||
transposed_data_shape: ir.Shape | None = transposed_data.shape | ||
if transposed_data_shape is None: | ||
return fail("Transposed data shape is not statically known.", transposed_data) | ||
actual_dim_value = transposed_data_shape[0] | ||
if updated_dim_value != actual_dim_value: | ||
titaiwangms marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# The first dimension of the transposed data does not match the updated dimension, | ||
# so we cannot apply this rule. | ||
return fail( | ||
"The first dimension of the transposed data does not match the updated dimension.", | ||
data, | ||
transposed_data, | ||
) | ||
return True | ||
|
||
def rewrite(self, op, updates, **_): | ||
return op.Identity(updates) | ||
|
||
|
||
rule = ScatterAll.rule() | ||
gramalingam marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
rules = orp.RewriteRuleSet([rule]) |
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,70 @@ | ||
# Copyright (c) Microsoft Corporation. | ||
# Licensed under the MIT License. | ||
# ruff: noqa: F821 | ||
|
||
import unittest | ||
|
||
import numpy as np | ||
import onnx_ir as ir | ||
import onnxruntime | ||
from onnx_ir.passes.common import CheckerPass, ShapeInferencePass | ||
|
||
import onnxscript.optimizer | ||
from onnxscript import FLOAT, script | ||
from onnxscript import opset18 as op | ||
from onnxscript.rewriter import redundant_scatter_nd | ||
|
||
shape_inference = ShapeInferencePass() | ||
onnx_check = CheckerPass(True) | ||
|
||
|
||
class RedundantScatterNdTest(unittest.TestCase): | ||
def test_redundant_scatter_nd(self): | ||
@script() | ||
def model_script( | ||
data: FLOAT[8, "N", 16], updates: FLOAT[8, "N", 16] | ||
) -> FLOAT[8, "N", 16]: | ||
# Construct update-indices spanning an entire axis: | ||
axis = op.Constant(value_int=1) | ||
shape = op.Shape(data, start=0) | ||
dim = op.Gather(shape, axis, axis=0) | ||
full_range = op.Range(0, dim, 1) | ||
full_range_2d = op.Unsqueeze(full_range, [-1]) | ||
# The update is applied to the data transposed to bring the updated axis to the front: | ||
transposed_data = op.Transpose(data, perm=[1, 0, 2]) | ||
transposed_updates = op.Transpose(updates, perm=[1, 0, 2]) | ||
scattered = op.ScatterND( | ||
transposed_data, full_range_2d, transposed_updates, reduction="none" | ||
) | ||
# Transpose the result back to the original shape: | ||
output = op.Transpose(scattered, perm=[1, 0, 2]) | ||
return output | ||
|
||
input_model_proto = model_script.to_model_proto() | ||
model = ir.serde.deserialize_model(input_model_proto) | ||
onnx_check(model) | ||
shape_inference(model) | ||
onnxscript.optimizer.fold_constants(model) | ||
count = redundant_scatter_nd.rules.apply_to_model(model) | ||
self.assertEqual(count, 1) | ||
onnx_check(model) | ||
optimized_model_proto = ir.serde.serialize_model(model) | ||
# Test that both models are equivalent: | ||
inputs = { | ||
"data": np.random.rand(8, 4, 16).astype(np.float32), | ||
"updates": np.random.rand(8, 4, 16).astype(np.float32), | ||
} | ||
session = onnxruntime.InferenceSession( | ||
input_model_proto.SerializeToString(), providers=["CPUExecutionProvider"] | ||
) | ||
outputs = session.run(None, inputs) | ||
optimized_session = onnxruntime.InferenceSession( | ||
optimized_model_proto.SerializeToString(), providers=["CPUExecutionProvider"] | ||
) | ||
optimized_outputs = optimized_session.run(None, inputs) | ||
for output, optimized_output in zip(outputs, optimized_outputs): | ||
np.testing.assert_allclose(output, optimized_output, rtol=1e-6, atol=1e-6) | ||
|
||
|
||
if __name__ == "__main__": | ||
unittest.main() | ||
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.
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.
I don't see these ops in the repro: pytorch/pytorch#157289
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.
Maybe you can delete
onnxscript/onnxscript/rewriter/collapse_slices.py
Line 134 in 7b89760
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.
I think we can consolidate the rules separately. (I am thinking of trying out Copilot to do it.)
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.
May need to make other dimensions symbolic. Otherwise, all of these ops will be constant-folded, and the indices becomes a constant.