From db1a9f2388bd48e0bdde095f231a4dcc1473430a Mon Sep 17 00:00:00 2001 From: Shubham Bhokare <32080845+shubhambhokare1@users.noreply.github.com> Date: Thu, 9 Sep 2021 15:56:51 -0700 Subject: [PATCH] Update Scatter style ops: Add reduction attribute (#3484) * Initial doc changes Signed-off-by: Shubham Bhokare * Add expect function logics Signed-off-by: Shubham Bhokare * Update multiply -> mul Signed-off-by: Shubham Bhokare * Added newline Signed-off-by: Shubham Bhokare * Fixed CI issues, added changelogs Signed-off-by: Shubham Bhokare * Add version converters Signed-off-by: Shubham Bhokare * Fix trailing spaces Signed-off-by: Shubham Bhokare * Add type annotation Signed-off-by: Shubham Bhokare * Use compatible adapters Signed-off-by: Shubham Bhokare * Remove git messages Signed-off-by: Shubham Bhokare * Remove git messages Signed-off-by: Shubham Bhokare Co-authored-by: Shubham Bhokare --- docs/Changelog.md | 220 +++++++++++++++++ docs/Operators.md | 147 ++++++++++- docs/TestCoverage.md | 89 ++++++- .../backend/test/case/node/scatterelements.py | 39 ++- onnx/backend/test/case/node/scatternd.py | 64 ++++- .../model.onnx | 23 ++ .../test_data_set_0/input_0.pb | Bin 0 -> 34 bytes .../test_data_set_0/input_1.pb | Bin 0 -> 33 bytes .../test_data_set_0/input_2.pb | 1 + .../test_data_set_0/output_0.pb | Bin 0 -> 31 bytes .../data/node/test_scatternd_add/model.onnx | 25 ++ .../test_data_set_0/input_0.pb | Bin 0 -> 273 bytes .../test_data_set_0/input_1.pb | Bin 0 -> 33 bytes .../test_data_set_0/input_2.pb | Bin 0 -> 148 bytes .../test_data_set_0/output_0.pb | Bin 0 -> 270 bytes .../node/test_scatternd_multiply/model.onnx | 25 ++ .../test_data_set_0/input_0.pb | Bin 0 -> 273 bytes .../test_data_set_0/input_1.pb | Bin 0 -> 33 bytes .../test_data_set_0/input_2.pb | Bin 0 -> 148 bytes .../test_data_set_0/output_0.pb | Bin 0 -> 270 bytes onnx/defs/operator_sets.h | 6 +- onnx/defs/tensor/defs.cc | 63 ++++- onnx/defs/tensor/old.cc | 228 ++++++++++++++++++ onnx/test/automatic_upgrade_test.py | 18 +- onnx/version_converter/convert.h | 4 + 25 files changed, 924 insertions(+), 28 deletions(-) create mode 100644 onnx/backend/test/data/node/test_scatter_elements_with_duplicate_indices/model.onnx create mode 100644 onnx/backend/test/data/node/test_scatter_elements_with_duplicate_indices/test_data_set_0/input_0.pb create mode 100644 onnx/backend/test/data/node/test_scatter_elements_with_duplicate_indices/test_data_set_0/input_1.pb create mode 100644 onnx/backend/test/data/node/test_scatter_elements_with_duplicate_indices/test_data_set_0/input_2.pb create mode 100644 onnx/backend/test/data/node/test_scatter_elements_with_duplicate_indices/test_data_set_0/output_0.pb create mode 100644 onnx/backend/test/data/node/test_scatternd_add/model.onnx create mode 100644 onnx/backend/test/data/node/test_scatternd_add/test_data_set_0/input_0.pb create mode 100644 onnx/backend/test/data/node/test_scatternd_add/test_data_set_0/input_1.pb create mode 100644 onnx/backend/test/data/node/test_scatternd_add/test_data_set_0/input_2.pb create mode 100644 onnx/backend/test/data/node/test_scatternd_add/test_data_set_0/output_0.pb create mode 100644 onnx/backend/test/data/node/test_scatternd_multiply/model.onnx create mode 100644 onnx/backend/test/data/node/test_scatternd_multiply/test_data_set_0/input_0.pb create mode 100644 onnx/backend/test/data/node/test_scatternd_multiply/test_data_set_0/input_1.pb create mode 100644 onnx/backend/test/data/node/test_scatternd_multiply/test_data_set_0/input_2.pb create mode 100644 onnx/backend/test/data/node/test_scatternd_multiply/test_data_set_0/output_0.pb diff --git a/docs/Changelog.md b/docs/Changelog.md index 376def81..7f1d6428 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -20161,6 +20161,226 @@ This version of the operator has been available since version 16 of the default
Constrain types to int tensors.
+### **ScatterElements-16** + + ScatterElements takes three inputs `data`, `updates`, and `indices` of the same + rank r >= 1 and an optional attribute axis that identifies an axis of `data` + (by default, the outer-most axis, that is axis 0). The output of the operation + is produced by creating a copy of the input `data`, and then updating its value + to values specified by `updates` at specific index positions specified by + `indices`. Its output shape is the same as the shape of `data`. + + For each entry in `updates`, the target index in `data` is obtained by combining + the corresponding entry in `indices` with the index of the entry itself: the + index-value for dimension = axis is obtained from the value of the corresponding + entry in `indices` and the index-value for dimension != axis is obtained from the + index of the entry itself. + + `reduction` allows specification of an optional reduction operation, which is applied to all values in `updates` + tensor into `output` at the specified `indices`. + In cases where `reduction` is set to "none", indices should not have duplicate entries: that is, if idx1 != idx2, + then indices[idx1] != indices[idx2]. For instance, in a 2-D tensor case, the update + corresponding to the [i][j] entry is performed as below: + ``` + output[indices[i][j]][j] = updates[i][j] if axis = 0, + output[i][indices[i][j]] = updates[i][j] if axis = 1, + ``` + When `reduction` is set to "add", the update corresponding to the [i][j] entry is performed as below: + ``` + output[indices[i][j]][j] += updates[i][j] if axis = 0, + output[i][indices[i][j]] += updates[i][j] if axis = 1, + ``` + When `reduction` is set to "mul", the update corresponding to the [i][j] entry is performed as below: + ``` + output[indices[i][j]][j] *= updates[i][j] if axis = 0, + output[i][indices[i][j]] *= updates[i][j] if axis = 1, + ``` + + This operator is the inverse of GatherElements. It is similar to Torch's Scatter operation. + + Example 1: + ``` + data = [ + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + ] + indices = [ + [1, 0, 2], + [0, 2, 1], + ] + updates = [ + [1.0, 1.1, 1.2], + [2.0, 2.1, 2.2], + ] + output = [ + [2.0, 1.1, 0.0] + [1.0, 0.0, 2.2] + [0.0, 2.1, 1.2] + ] + ``` + Example 2: + ``` + data = [[1.0, 2.0, 3.0, 4.0, 5.0]] + indices = [[1, 3]] + updates = [[1.1, 2.1]] + axis = 1 + output = [[1.0, 1.1, 3.0, 2.1, 5.0]] + ``` + +#### Version + +This version of the operator has been available since version 16 of the default ONNX operator set. + +#### Attributes + +
+
axis : int (default is 0)
+
Which axis to scatter on. Negative value means counting dimensions from the back. Accepted range is [-r, r-1] where r = rank(data).
+
reduction : string (default is none)
+
Type of reduction to apply: none (default), add, mul. 'none': no reduction applied. 'add': reduction using the addition operation. 'mul': reduction using the multiplication operation.
+
+ +#### Inputs + +
+
data (differentiable) : T
+
Tensor of rank r >= 1.
+
indices (non-differentiable) : Tind
+
Tensor of int32/int64 indices, of r >= 1 (same rank as input). All index values are expected to be within bounds [-s, s-1] along axis of size s. It is an error if any of the index values are out of bounds.
+
updates (differentiable) : T
+
Tensor of rank r >=1 (same rank and shape as indices)
+
+ +#### Outputs + +
+
output (differentiable) : T
+
Tensor of rank r >= 1 (same rank as input).
+
+ +#### Type Constraints + +
+
T : tensor(uint8), tensor(uint16), tensor(uint32), tensor(uint64), tensor(int8), tensor(int16), tensor(int32), tensor(int64), tensor(bfloat16), tensor(float16), tensor(float), tensor(double), tensor(string), tensor(bool), tensor(complex64), tensor(complex128)
+
Input and output types can be of any tensor type.
+
Tind : tensor(int32), tensor(int64)
+
Constrain indices to integer types
+
+ +### **ScatterND-16** + + ScatterND takes three inputs `data` tensor of rank r >= 1, `indices` tensor of rank q >= 1, + and `updates` tensor of rank q + r - indices.shape[-1] - 1. The output of the operation + is produced by creating a copy of the input `data`, and then updating its value to values + specified by `updates` at specific index positions specified by `indices`. Its output shape + is the same as the shape of `data`. Note that `indices` should not have duplicate entries. + That is, two or more `updates` for the same index-location is not supported. + + `indices` is an integer tensor. Let k denote indices.shape[-1], the last dimension in the shape of `indices`. + `indices` is treated as a (q-1)-dimensional tensor of k-tuples, where each k-tuple is a partial-index into `data`. + Hence, k can be a value at most the rank of `data`. When k equals rank(data), each update entry specifies an + update to a single element of the tensor. When k is less than rank(data) each update entry specifies an + update to a slice of the tensor. + + `updates` is treated as a (q-1)-dimensional tensor of replacement-slice-values. Thus, the + first (q-1) dimensions of updates.shape must match the first (q-1) dimensions of indices.shape. + The remaining dimensions of `updates` correspond to the dimensions of the + replacement-slice-values. Each replacement-slice-value is a (r-k) dimensional tensor, + corresponding to the trailing (r-k) dimensions of `data`. Thus, the shape of `updates` + must equal indices.shape[0:q-1] ++ data.shape[k:r-1], where ++ denotes the concatenation + of shapes. + + The `output` is calculated via the following equation: + + output = np.copy(data) + update_indices = indices.shape[:-1] + for idx in np.ndindex(update_indices): + output[indices[idx]] = updates[idx] + + The order of iteration in the above loop is not specified. + In particular, indices should not have duplicate entries: that is, if idx1 != idx2, then indices[idx1] != indices[idx2]. + This ensures that the output value does not depend on the iteration order. + + `reduction` allows specification of an optional reduction operation, which is applied to all values in `updates` + tensor into `output` at the specified `indices`. + In cases where `reduction` is set to "none", indices should not have duplicate entries: that is, if idx1 != idx2, + then indices[idx1] != indices[idx2]. This ensures that the output value does not depend on the iteration order. + When `reduction` is set to "add", `output` is calculated as follows: + + output = np.copy(data) + update_indices = indices.shape[:-1] + for idx in np.ndindex(update_indices): + output[indices[idx]] += updates[idx] + + When `reduction` is set to "mul", `output` is calculated as follows: + + output = np.copy(data) + update_indices = indices.shape[:-1] + for idx in np.ndindex(update_indices): + output[indices[idx]] *= updates[idx] + + This operator is the inverse of GatherND. + + Example 1: + ``` + data = [1, 2, 3, 4, 5, 6, 7, 8] + indices = [[4], [3], [1], [7]] + updates = [9, 10, 11, 12] + output = [1, 11, 3, 10, 9, 6, 7, 12] + ``` + + Example 2: + ``` + data = [[[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + [[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]], + [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]]] + indices = [[0], [2]] + updates = [[[5, 5, 5, 5], [6, 6, 6, 6], [7, 7, 7, 7], [8, 8, 8, 8]], + [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]]] + output = [[[5, 5, 5, 5], [6, 6, 6, 6], [7, 7, 7, 7], [8, 8, 8, 8]], + [[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]], + [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]]] + ``` + +#### Version + +This version of the operator has been available since version 16 of the default ONNX operator set. + +#### Attributes + +
+
reduction : string (default is none)
+
Type of reduction to apply: none (default), add, mul. 'none': no reduction applied. 'add': reduction using the addition operation. 'mul': reduction using the multiplication operation.
+
+ +#### Inputs + +
+
data (differentiable) : T
+
Tensor of rank r >= 1.
+
indices (non-differentiable) : tensor(int64)
+
Tensor of rank q >= 1.
+
updates (differentiable) : T
+
Tensor of rank q + r - indices_shape[-1] - 1.
+
+ +#### Outputs + +
+
output (differentiable) : T
+
Tensor of rank r >= 1.
+
+ +#### Type Constraints + +
+
T : tensor(uint8), tensor(uint16), tensor(uint32), tensor(uint64), tensor(int8), tensor(int16), tensor(int32), tensor(int64), tensor(bfloat16), tensor(float16), tensor(float), tensor(double), tensor(string), tensor(bool), tensor(complex64), tensor(complex128)
+
Constrain input and output types to any tensor type.
+
+ # ai.onnx.preview.training ## Version 1 of the 'ai.onnx.preview.training' operator set ### **ai.onnx.preview.training.Adagrad-1** diff --git a/docs/Operators.md b/docs/Operators.md index 181d7144..eae3ae75 100644 --- a/docs/Operators.md +++ b/docs/Operators.md @@ -127,8 +127,8 @@ For an operator input/output's differentiability, it can be differentiable, |Round|11| |Scan|11, 9, 8| |Scatter (deprecated)|11, 9| -|ScatterElements|13, 11| -|ScatterND|13, 11| +|ScatterElements|16, 13, 11| +|ScatterND|16, 13, 11| |Selu|6, 1| |SequenceAt|11| |SequenceConstruct|11| @@ -18477,12 +18477,25 @@ expect(node, inputs=[data, indices, updates], outputs=[y], entry in `indices` and the index-value for dimension != axis is obtained from the index of the entry itself. - For instance, in a 2-D tensor case, the update corresponding to the [i][j] entry - is performed as below: + `reduction` allows specification of an optional reduction operation, which is applied to all values in `updates` + tensor into `output` at the specified `indices`. + In cases where `reduction` is set to "none", indices should not have duplicate entries: that is, if idx1 != idx2, + then indices[idx1] != indices[idx2]. For instance, in a 2-D tensor case, the update + corresponding to the [i][j] entry is performed as below: ``` output[indices[i][j]][j] = updates[i][j] if axis = 0, output[i][indices[i][j]] = updates[i][j] if axis = 1, ``` + When `reduction` is set to "add", the update corresponding to the [i][j] entry is performed as below: + ``` + output[indices[i][j]][j] += updates[i][j] if axis = 0, + output[i][indices[i][j]] += updates[i][j] if axis = 1, + ``` + When `reduction` is set to "mul", the update corresponding to the [i][j] entry is performed as below: + ``` + output[indices[i][j]][j] *= updates[i][j] if axis = 0, + output[i][indices[i][j]] *= updates[i][j] if axis = 1, + ``` This operator is the inverse of GatherElements. It is similar to Torch's Scatter operation. @@ -18518,15 +18531,17 @@ expect(node, inputs=[data, indices, updates], outputs=[y], #### Version -This version of the operator has been available since version 13 of the default ONNX operator set. +This version of the operator has been available since version 16 of the default ONNX operator set. -Other versions of this operator: 11 +Other versions of this operator: 11, 13 #### Attributes
axis : int (default is 0)
Which axis to scatter on. Negative value means counting dimensions from the back. Accepted range is [-r, r-1] where r = rank(data).
+
reduction : string (default is none)
+
Type of reduction to apply: none (default), add, mul. 'none': no reduction applied. 'add': reduction using the addition operation. 'mul': reduction using the multiplication operation.
#### Inputs @@ -18585,6 +18600,33 @@ expect(node, inputs=[data, indices, updates], outputs=[y], +
+scatter_elements_with_duplicate_indices + +```python +axis = 1 +node = onnx.helper.make_node( + 'ScatterElements', + inputs=['data', 'indices', 'updates'], + outputs=['y'], + axis=axis, + reduction='add', +) +data = np.array([[1.0, 2.0, 3.0, 4.0, 5.0]], dtype=np.float32) +indices = np.array([[1, 1]], dtype=np.int64) +updates = np.array([[1.1, 2.1]], dtype=np.float32) + +y = scatter_elements(data, indices, updates, axis, reduction='add') +# print(y) produces +# [[1.0, 5.2, 3.0, 4.0, 5.0]] + +expect(node, inputs=[data, indices, updates], outputs=[y], + name='test_scatter_elements_with_duplicate_indices') +``` + +
+ +
scatter_elements_with_negative_indices @@ -18671,6 +18713,24 @@ expect(node, inputs=[data, indices, updates], outputs=[y], In particular, indices should not have duplicate entries: that is, if idx1 != idx2, then indices[idx1] != indices[idx2]. This ensures that the output value does not depend on the iteration order. + `reduction` allows specification of an optional reduction operation, which is applied to all values in `updates` + tensor into `output` at the specified `indices`. + In cases where `reduction` is set to "none", indices should not have duplicate entries: that is, if idx1 != idx2, + then indices[idx1] != indices[idx2]. This ensures that the output value does not depend on the iteration order. + When `reduction` is set to "add", `output` is calculated as follows: + + output = np.copy(data) + update_indices = indices.shape[:-1] + for idx in np.ndindex(update_indices): + output[indices[idx]] += updates[idx] + + When `reduction` is set to "mul", `output` is calculated as follows: + + output = np.copy(data) + update_indices = indices.shape[:-1] + for idx in np.ndindex(update_indices): + output[indices[idx]] *= updates[idx] + This operator is the inverse of GatherND. Example 1: @@ -18698,9 +18758,16 @@ expect(node, inputs=[data, indices, updates], outputs=[y], #### Version -This version of the operator has been available since version 13 of the default ONNX operator set. +This version of the operator has been available since version 16 of the default ONNX operator set. + +Other versions of this operator: 11, 13 -Other versions of this operator: 11 +#### Attributes + +
+
reduction : string (default is none)
+
Type of reduction to apply: none (default), add, mul. 'none': no reduction applied. 'add': reduction using the addition operation. 'mul': reduction using the multiplication operation.
+
#### Inputs @@ -18761,6 +18828,70 @@ expect(node, inputs=[data, indices, updates], outputs=[output],
+
+scatternd_add + +```python +node = onnx.helper.make_node( + 'ScatterND', + inputs=['data', 'indices', 'updates'], + outputs=['y'], + reduction='add', +) +data = np.array( + [[[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + [[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]], + [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]]], dtype=np.float32) +indices = np.array([[0], [0]], dtype=np.int64) +updates = np.array( + [[[5, 5, 5, 5], [6, 6, 6, 6], [7, 7, 7, 7], [8, 8, 8, 8]], + [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]]], dtype=np.float32) +# Expecting output as np.array( +# [[[7, 8, 9, 10], [13, 14, 15, 16], [18, 17, 16, 15], [16, 15, 14, 13]], +# [[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], +# [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]], +# [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]]], dtype=np.float32) +output = scatter_nd_impl(data, indices, updates, reduction='add') +expect(node, inputs=[data, indices, updates], outputs=[output], + name='test_scatternd_add') +``` + +
+ + +
+scatternd_multiply + +```python +node = onnx.helper.make_node( + 'ScatterND', + inputs=['data', 'indices', 'updates'], + outputs=['y'], + reduction='mul', +) +data = np.array( + [[[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + [[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]], + [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]]], dtype=np.float32) +indices = np.array([[0], [0]], dtype=np.int64) +updates = np.array( + [[[5, 5, 5, 5], [6, 6, 6, 6], [7, 7, 7, 7], [8, 8, 8, 8]], + [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]]], dtype=np.float32) +# Expecting output as np.array( +# [[[5, 10, 15, 20], [60, 72, 84, 96], [168, 147, 126, 105], [128, 96, 64, 32]], +# [[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], +# [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]], +# [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]]], dtype=np.float32) +output = scatter_nd_impl(data, indices, updates, reduction='mul') +expect(node, inputs=[data, indices, updates], outputs=[output], + name='test_scatternd_multiply') +``` + +
+ + ### **Selu** Selu takes one input data (Tensor) and produces one output data diff --git a/docs/TestCoverage.md b/docs/TestCoverage.md index ef89545d..4dcb63c2 100644 --- a/docs/TestCoverage.md +++ b/docs/TestCoverage.md @@ -11538,7 +11538,7 @@ expect(node, inputs=[data, indices, updates], outputs=[y], ### ScatterElements -There are 3 test cases, listed as following: +There are 4 test cases, listed as following:
scatter_elements_with_axis @@ -11562,6 +11562,31 @@ expect(node, inputs=[data, indices, updates], outputs=[y], name='test_scatter_elements_with_axis') ``` +
+
+scatter_elements_with_duplicate_indices + +```python +axis = 1 +node = onnx.helper.make_node( + 'ScatterElements', + inputs=['data', 'indices', 'updates'], + outputs=['y'], + axis=axis, + reduction='add', +) +data = np.array([[1.0, 2.0, 3.0, 4.0, 5.0]], dtype=np.float32) +indices = np.array([[1, 1]], dtype=np.int64) +updates = np.array([[1.1, 2.1]], dtype=np.float32) + +y = scatter_elements(data, indices, updates, axis, reduction='add') +# print(y) produces +# [[1.0, 5.2, 3.0, 4.0, 5.0]] + +expect(node, inputs=[data, indices, updates], outputs=[y], + name='test_scatter_elements_with_duplicate_indices') +``` +
scatter_elements_with_negative_indices @@ -11614,7 +11639,7 @@ expect(node, inputs=[data, indices, updates], outputs=[y], ### ScatterND -There are 1 test cases, listed as following: +There are 3 test cases, listed as following:
scatternd @@ -11643,6 +11668,66 @@ expect(node, inputs=[data, indices, updates], outputs=[output], name='test_scatternd') ``` +
+
+scatternd_add + +```python +node = onnx.helper.make_node( + 'ScatterND', + inputs=['data', 'indices', 'updates'], + outputs=['y'], + reduction='add', +) +data = np.array( + [[[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + [[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]], + [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]]], dtype=np.float32) +indices = np.array([[0], [0]], dtype=np.int64) +updates = np.array( + [[[5, 5, 5, 5], [6, 6, 6, 6], [7, 7, 7, 7], [8, 8, 8, 8]], + [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]]], dtype=np.float32) +# Expecting output as np.array( +# [[[7, 8, 9, 10], [13, 14, 15, 16], [18, 17, 16, 15], [16, 15, 14, 13]], +# [[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], +# [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]], +# [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]]], dtype=np.float32) +output = scatter_nd_impl(data, indices, updates, reduction='add') +expect(node, inputs=[data, indices, updates], outputs=[output], + name='test_scatternd_add') +``` + +
+
+scatternd_multiply + +```python +node = onnx.helper.make_node( + 'ScatterND', + inputs=['data', 'indices', 'updates'], + outputs=['y'], + reduction='mul', +) +data = np.array( + [[[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + [[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]], + [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]]], dtype=np.float32) +indices = np.array([[0], [0]], dtype=np.int64) +updates = np.array( + [[[5, 5, 5, 5], [6, 6, 6, 6], [7, 7, 7, 7], [8, 8, 8, 8]], + [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]]], dtype=np.float32) +# Expecting output as np.array( +# [[[5, 10, 15, 20], [60, 72, 84, 96], [168, 147, 126, 105], [128, 96, 64, 32]], +# [[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], +# [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]], +# [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]]], dtype=np.float32) +output = scatter_nd_impl(data, indices, updates, reduction='mul') +expect(node, inputs=[data, indices, updates], outputs=[output], + name='test_scatternd_multiply') +``` +
diff --git a/onnx/backend/test/case/node/scatterelements.py b/onnx/backend/test/case/node/scatterelements.py index e93bdb85..e0633f7b 100644 --- a/onnx/backend/test/case/node/scatterelements.py +++ b/onnx/backend/test/case/node/scatterelements.py @@ -13,7 +13,7 @@ from . import expect # The below ScatterElements' numpy implementation is from https://stackoverflow.com/a/46204790/11767360 -def scatter_elements(data, indices, updates, axis=0): # type: ignore +def scatter_elements(data, indices, updates, axis=0, reduction='none'): # type: ignore if axis < 0: axis = data.ndim + axis @@ -30,6 +30,12 @@ def scatter_elements(data, indices, updates, axis=0): # type: ignore unpacked = unpacked, packed[i] return unpacked + def make_indices_for_duplicate(idx): # type: ignore + final_idx = list() + for i in range(len(idx[0])): + final_idx.append(tuple(idx_element[i] for idx_element in idx)) + return list(final_idx) + # We use indices and axis parameters to create idx # idx is in a form that can be used as a NumPy advanced indices for scattering of updates param. in data idx = [[unpack(np.indices(idx_xsection_shape).reshape(indices.ndim - 1, -1)), @@ -43,7 +49,15 @@ def scatter_elements(data, indices, updates, axis=0): # type: ignore updates_idx.insert(axis, np.repeat(np.arange(indices.shape[axis]), np.prod(idx_xsection_shape))) scattered = np.copy(data) - scattered[tuple(idx)] = updates[tuple(updates_idx)] + if reduction == 'none': + scattered[tuple(idx)] = updates[tuple(updates_idx)] + else: + idx, updates_idx = make_indices_for_duplicate(idx), make_indices_for_duplicate(updates_idx) + for iter, idx_set in enumerate(idx): + if reduction == 'add': + scattered[idx_set] += updates[updates_idx[iter]] + elif reduction == 'mul': + scattered[idx_set] *= updates[updates_idx[iter]] return scattered @@ -108,3 +122,24 @@ class ScatterElements(Base): expect(node, inputs=[data, indices, updates], outputs=[y], name='test_scatter_elements_with_negative_indices') + + @staticmethod + def export_scatter_elements_with_duplicate_indices(): # type: () -> None + axis = 1 + node = onnx.helper.make_node( + 'ScatterElements', + inputs=['data', 'indices', 'updates'], + outputs=['y'], + axis=axis, + reduction='add', + ) + data = np.array([[1.0, 2.0, 3.0, 4.0, 5.0]], dtype=np.float32) + indices = np.array([[1, 1]], dtype=np.int64) + updates = np.array([[1.1, 2.1]], dtype=np.float32) + + y = scatter_elements(data, indices, updates, axis, reduction='add') + # print(y) produces + # [[1.0, 5.2, 3.0, 4.0, 5.0]] + + expect(node, inputs=[data, indices, updates], outputs=[y], + name='test_scatter_elements_with_duplicate_indices') diff --git a/onnx/backend/test/case/node/scatternd.py b/onnx/backend/test/case/node/scatternd.py index 2ba392e0..68b9938b 100644 --- a/onnx/backend/test/case/node/scatternd.py +++ b/onnx/backend/test/case/node/scatternd.py @@ -12,8 +12,7 @@ from ..base import Base from . import expect -def scatter_nd_impl(data, indices, updates): - # type: (np.ndarray, np.ndarray, np.ndarray) -> np.ndarray +def scatter_nd_impl(data, indices, updates, reduction='none'): # type: ignore # Check tensor shapes assert indices.shape[-1] <= len(data.shape) @@ -23,9 +22,12 @@ def scatter_nd_impl(data, indices, updates): output = np.copy(data) for i in np.ndindex(indices.shape[:-1]): # NOTE: The order of iteration in this loop is not specified. - # In particular, indices should not have duplicate entries: that is, if idx1 != idx2, then indices[idx1] != indices[idx2]. - # This ensures that the output value does not depend on the iteration order. - output[indices[i]] = updates[i] + if reduction == 'add': + output[indices[i]] += updates[i] + elif reduction == 'mul': + output[indices[i]] *= updates[i] + else: + output[indices[i]] = updates[i] return output @@ -55,3 +57,55 @@ class ScatterND(Base): output = scatter_nd_impl(data, indices, updates) expect(node, inputs=[data, indices, updates], outputs=[output], name='test_scatternd') + + @staticmethod + def export_scatternd_add(): # type: () -> None + node = onnx.helper.make_node( + 'ScatterND', + inputs=['data', 'indices', 'updates'], + outputs=['y'], + reduction='add', + ) + data = np.array( + [[[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + [[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]], + [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]]], dtype=np.float32) + indices = np.array([[0], [0]], dtype=np.int64) + updates = np.array( + [[[5, 5, 5, 5], [6, 6, 6, 6], [7, 7, 7, 7], [8, 8, 8, 8]], + [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]]], dtype=np.float32) + # Expecting output as np.array( + # [[[7, 8, 9, 10], [13, 14, 15, 16], [18, 17, 16, 15], [16, 15, 14, 13]], + # [[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + # [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]], + # [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]]], dtype=np.float32) + output = scatter_nd_impl(data, indices, updates, reduction='add') + expect(node, inputs=[data, indices, updates], outputs=[output], + name='test_scatternd_add') + + @staticmethod + def export_scatternd_multiply(): # type: () -> None + node = onnx.helper.make_node( + 'ScatterND', + inputs=['data', 'indices', 'updates'], + outputs=['y'], + reduction='mul', + ) + data = np.array( + [[[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + [[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]], + [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]]], dtype=np.float32) + indices = np.array([[0], [0]], dtype=np.int64) + updates = np.array( + [[[5, 5, 5, 5], [6, 6, 6, 6], [7, 7, 7, 7], [8, 8, 8, 8]], + [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]]], dtype=np.float32) + # Expecting output as np.array( + # [[[5, 10, 15, 20], [60, 72, 84, 96], [168, 147, 126, 105], [128, 96, 64, 32]], + # [[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + # [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]], + # [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]]], dtype=np.float32) + output = scatter_nd_impl(data, indices, updates, reduction='mul') + expect(node, inputs=[data, indices, updates], outputs=[output], + name='test_scatternd_multiply') diff --git a/onnx/backend/test/data/node/test_scatter_elements_with_duplicate_indices/model.onnx b/onnx/backend/test/data/node/test_scatter_elements_with_duplicate_indices/model.onnx new file mode 100644 index 00000000..9a6401b4 --- /dev/null +++ b/onnx/backend/test/data/node/test_scatter_elements_with_duplicate_indices/model.onnx @@ -0,0 +1,23 @@ + backend-test:á +N +data +indices +updatesy"ScatterElements* +axis * + reduction"add ,test_scatter_elements_with_duplicate_indicesZ +data +  + +Z +indices +  + +Z +updates +  + +b +y +  + +B \ No newline at end of file diff --git a/onnx/backend/test/data/node/test_scatter_elements_with_duplicate_indices/test_data_set_0/input_0.pb b/onnx/backend/test/data/node/test_scatter_elements_with_duplicate_indices/test_data_set_0/input_0.pb new file mode 100644 index 0000000000000000000000000000000000000000..661309dcc2f8651f19ea13124bee694309aa29e1 GIT binary patch literal 34 kcmd;J09LjIg#Z8m literal 0 HcmV?d00001 diff --git a/onnx/backend/test/data/node/test_scatter_elements_with_duplicate_indices/test_data_set_0/input_1.pb b/onnx/backend/test/data/node/test_scatter_elements_with_duplicate_indices/test_data_set_0/input_1.pb new file mode 100644 index 0000000000000000000000000000000000000000..989ee890a65902f5117ec1a958746ecee7146385 GIT binary patch literal 33 dcmd;J1||lE273ku1_vMl;szjI0K^A?_yG_zI6^Rp4H8=b#0@~~ l0K^O+Hc&66`ayO)0Gf3Gh!=p&1Y!puW&p9NU_O5L0RZD)F5&bd_yLfQj2Rpm7?5#;Jp%(WW&o;3 O#tuL}GHw9!5f}hv85%wS literal 0 HcmV?d00001 diff --git a/onnx/backend/test/data/node/test_scatternd_add/test_data_set_0/output_0.pb b/onnx/backend/test/data/node/test_scatternd_add/test_data_set_0/output_0.pb new file mode 100644 index 0000000000000000000000000000000000000000..cd316b9776cfa27da8f13aac52703b9f352539e2 GIT binary patch literal 270 zcmbu%s||oK6ouhaf~f2WY(hbz7=aP6xGRy2&`}trqwwDZEC|wk>B)V?Hu6#!!!}tX x)hua{5<1LSu)=U)zw=Y`-)Cm8_v~{c!%%RdLiC+yuN4?F1||lE273ku1_vMl;szjI0K^A?_yG_zI6^Rp4H8=b#0@~~ l0K^O+Hc&66`ayO)0Gf3Gh!=p&1Y!puW&p9NU_O5L0RZD)F5&bd_yLfQj2Rpm7?5#;Jp%(WW&o;3 O#tuL}GHw9!5f}hv85%wS literal 0 HcmV?d00001 diff --git a/onnx/backend/test/data/node/test_scatternd_multiply/test_data_set_0/output_0.pb b/onnx/backend/test/data/node/test_scatternd_multiply/test_data_set_0/output_0.pb new file mode 100644 index 0000000000000000000000000000000000000000..2b812a9ca35985f8a89da9c3771c26f1295d5092 GIT binary patch literal 270 zcmbu%F%5t)6ola`VPGV|03$9au%U*PtrC#X5gBMe0YVt~&j1Xh`SLEWc~_McuMT5a z=Tt~JO6?#m$eyFLVaI`Pk{aCP6C!(`oIQIc9T5o`2QG*{bM`JHAwtf+{rLae|9<}f DN!l)5 literal 0 HcmV?d00001 diff --git a/onnx/defs/operator_sets.h b/onnx/defs/operator_sets.h index 82c0628f..4a799bd3 100644 --- a/onnx/defs/operator_sets.h +++ b/onnx/defs/operator_sets.h @@ -990,8 +990,11 @@ class OpSet_Onnx_ver15 { } }; + // Forward declarations for ai.onnx version 16 class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Onnx, 16, RoiAlign); +class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Onnx, 16, ScatterND); +class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Onnx, 16, ScatterElements); class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Onnx, 16, If); class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Onnx, 16, Loop); class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Onnx, 16, Identity); @@ -1001,12 +1004,13 @@ class OpSet_Onnx_ver16 { public: static void ForEachSchema(std::function fn) { fn(GetOpSchema()); + fn(GetOpSchema()); + fn(GetOpSchema()); fn(GetOpSchema()); fn(GetOpSchema()); fn(GetOpSchema()); } }; - inline void RegisterOnnxOperatorSetSchema() { RegisterOpSetSchema(); RegisterOpSetSchema(); diff --git a/onnx/defs/tensor/defs.cc b/onnx/defs/tensor/defs.cc index 94a359fb..82365fb4 100644 --- a/onnx/defs/tensor/defs.cc +++ b/onnx/defs/tensor/defs.cc @@ -1302,7 +1302,7 @@ ONNX_OPERATOR_SET_SCHEMA( } })); -static const char* ScatterND_ver13_doc = R"DOC( +static const char* ScatterND_ver16_doc = R"DOC( ScatterND takes three inputs `data` tensor of rank r >= 1, `indices` tensor of rank q >= 1, and `updates` tensor of rank q + r - indices.shape[-1] - 1. The output of the operation is produced by creating a copy of the input `data`, and then updating its value to values @@ -1335,6 +1335,24 @@ The order of iteration in the above loop is not specified. In particular, indices should not have duplicate entries: that is, if idx1 != idx2, then indices[idx1] != indices[idx2]. This ensures that the output value does not depend on the iteration order. +`reduction` allows specification of an optional reduction operation, which is applied to all values in `updates` +tensor into `output` at the specified `indices`. +In cases where `reduction` is set to "none", indices should not have duplicate entries: that is, if idx1 != idx2, +then indices[idx1] != indices[idx2]. This ensures that the output value does not depend on the iteration order. +When `reduction` is set to "add", `output` is calculated as follows: + + output = np.copy(data) + update_indices = indices.shape[:-1] + for idx in np.ndindex(update_indices): + output[indices[idx]] += updates[idx] + +When `reduction` is set to "mul", `output` is calculated as follows: + + output = np.copy(data) + update_indices = indices.shape[:-1] + for idx in np.ndindex(update_indices): + output[indices[idx]] *= updates[idx] + This operator is the inverse of GatherND. Example 1: @@ -1363,9 +1381,17 @@ Example 2: ONNX_OPERATOR_SET_SCHEMA( ScatterND, - 13, + 16, OpSchema() - .SetDoc(ScatterND_ver13_doc) + .SetDoc(ScatterND_ver16_doc) + .Attr( + "reduction", + "Type of reduction to apply: none (default), add, mul. " + "'none': no reduction applied. " + "'add': reduction using the addition operation. " + "'mul': reduction using the multiplication operation.", + AttributeProto::STRING, + std::string("none")) .Input( 0, "data", @@ -1413,7 +1439,7 @@ ONNX_OPERATOR_SET_SCHEMA( } })); -static const char* ScatterElements_ver13_doc = R"DOC( +static const char* ScatterElements_ver16_doc = R"DOC( ScatterElements takes three inputs `data`, `updates`, and `indices` of the same rank r >= 1 and an optional attribute axis that identifies an axis of `data` (by default, the outer-most axis, that is axis 0). The output of the operation @@ -1427,12 +1453,25 @@ index-value for dimension = axis is obtained from the value of the corresponding entry in `indices` and the index-value for dimension != axis is obtained from the index of the entry itself. -For instance, in a 2-D tensor case, the update corresponding to the [i][j] entry -is performed as below: +`reduction` allows specification of an optional reduction operation, which is applied to all values in `updates` +tensor into `output` at the specified `indices`. +In cases where `reduction` is set to "none", indices should not have duplicate entries: that is, if idx1 != idx2, +then indices[idx1] != indices[idx2]. For instance, in a 2-D tensor case, the update +corresponding to the [i][j] entry is performed as below: ``` output[indices[i][j]][j] = updates[i][j] if axis = 0, output[i][indices[i][j]] = updates[i][j] if axis = 1, ``` +When `reduction` is set to "add", the update corresponding to the [i][j] entry is performed as below: +``` + output[indices[i][j]][j] += updates[i][j] if axis = 0, + output[i][indices[i][j]] += updates[i][j] if axis = 1, +``` +When `reduction` is set to "mul", the update corresponding to the [i][j] entry is performed as below: +``` + output[indices[i][j]][j] *= updates[i][j] if axis = 0, + output[i][indices[i][j]] *= updates[i][j] if axis = 1, +``` This operator is the inverse of GatherElements. It is similar to Torch's Scatter operation. @@ -1469,15 +1508,23 @@ Example 2: ONNX_OPERATOR_SET_SCHEMA( ScatterElements, - 13, + 16, OpSchema() - .SetDoc(ScatterElements_ver13_doc) + .SetDoc(ScatterElements_ver16_doc) .Attr( "axis", "Which axis to scatter on. Negative value means " "counting dimensions from the back. Accepted range is [-r, r-1] where r = rank(data).", AttributeProto::INT, static_cast(0)) + .Attr( + "reduction", + "Type of reduction to apply: none (default), add, mul. " + "'none': no reduction applied. " + "'add': reduction using the addition operation. " + "'mul': reduction using the multiplication operation.", + AttributeProto::STRING, + std::string("none")) .Input( 0, "data", diff --git a/onnx/defs/tensor/old.cc b/onnx/defs/tensor/old.cc index 986eb1ea..8e03fb95 100644 --- a/onnx/defs/tensor/old.cc +++ b/onnx/defs/tensor/old.cc @@ -1007,6 +1007,117 @@ ONNX_OPERATOR_SET_SCHEMA( } })); +static const char* ScatterND_ver13_doc = R"DOC( +ScatterND takes three inputs `data` tensor of rank r >= 1, `indices` tensor of rank q >= 1, +and `updates` tensor of rank q + r - indices.shape[-1] - 1. The output of the operation +is produced by creating a copy of the input `data`, and then updating its value to values +specified by `updates` at specific index positions specified by `indices`. Its output shape +is the same as the shape of `data`. Note that `indices` should not have duplicate entries. +That is, two or more `updates` for the same index-location is not supported. + +`indices` is an integer tensor. Let k denote indices.shape[-1], the last dimension in the shape of `indices`. + `indices` is treated as a (q-1)-dimensional tensor of k-tuples, where each k-tuple is a partial-index into `data`. +Hence, k can be a value at most the rank of `data`. When k equals rank(data), each update entry specifies an +update to a single element of the tensor. When k is less than rank(data) each update entry specifies an +update to a slice of the tensor. + +`updates` is treated as a (q-1)-dimensional tensor of replacement-slice-values. Thus, the +first (q-1) dimensions of updates.shape must match the first (q-1) dimensions of indices.shape. +The remaining dimensions of `updates` correspond to the dimensions of the +replacement-slice-values. Each replacement-slice-value is a (r-k) dimensional tensor, +corresponding to the trailing (r-k) dimensions of `data`. Thus, the shape of `updates` +must equal indices.shape[0:q-1] ++ data.shape[k:r-1], where ++ denotes the concatenation +of shapes. + +The `output` is calculated via the following equation: + + output = np.copy(data) + update_indices = indices.shape[:-1] + for idx in np.ndindex(update_indices): + output[indices[idx]] = updates[idx] + +The order of iteration in the above loop is not specified. +In particular, indices should not have duplicate entries: that is, if idx1 != idx2, then indices[idx1] != indices[idx2]. +This ensures that the output value does not depend on the iteration order. + +This operator is the inverse of GatherND. + +Example 1: +``` + data = [1, 2, 3, 4, 5, 6, 7, 8] + indices = [[4], [3], [1], [7]] + updates = [9, 10, 11, 12] + output = [1, 11, 3, 10, 9, 6, 7, 12] +``` + +Example 2: +``` + data = [[[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + [[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]], + [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]]] + indices = [[0], [2]] + updates = [[[5, 5, 5, 5], [6, 6, 6, 6], [7, 7, 7, 7], [8, 8, 8, 8]], + [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]]] + output = [[[5, 5, 5, 5], [6, 6, 6, 6], [7, 7, 7, 7], [8, 8, 8, 8]], + [[1, 2, 3, 4], [5, 6, 7, 8], [8, 7, 6, 5], [4, 3, 2, 1]], + [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4, 4]], + [[8, 7, 6, 5], [4, 3, 2, 1], [1, 2, 3, 4], [5, 6, 7, 8]]] +``` +)DOC"; + +ONNX_OPERATOR_SET_SCHEMA( + ScatterND, + 13, + OpSchema() + .SetDoc(ScatterND_ver13_doc) + .Input( + 0, + "data", + "Tensor of rank r >= 1.", + "T", + OpSchema::Single, + true, + 1, + OpSchema::Differentiable) + .Input( + 1, + "indices", + "Tensor of rank q >= 1.", + "tensor(int64)", + OpSchema::Single, + true, + 1, + OpSchema::NonDifferentiable) + .Input( + 2, + "updates", + "Tensor of rank q + r - indices_shape[-1] - 1.", + "T", + OpSchema::Single, + true, + 1, + OpSchema::Differentiable) + .Output( + 0, + "output", + "Tensor of rank r >= 1.", + "T", + OpSchema::Single, + true, + 1, + OpSchema::Differentiable) + .TypeConstraint( + "T", + OpSchema::all_tensor_types_with_bfloat(), + "Constrain input and output types to any tensor type.") + .TypeAndShapeInferenceFunction([](InferenceContext& ctx) { + propagateElemTypeFromInputToOutput(ctx, 0, 0); + if (hasNInputShapes(ctx, 1)) { + propagateShapeFromInputToOutput(ctx, 0, 0); + } + })); + static const char* ScatterND_ver11_doc = R"DOC( ScatterND takes three inputs `data` tensor of rank r >= 1, `indices` tensor of rank q >= 1, and `updates` tensor of rank q + r - indices.shape[-1] - 1. The output of the operation @@ -1090,6 +1201,123 @@ ONNX_OPERATOR_SET_SCHEMA( } })); +static const char* ScatterElements_ver13_doc = R"DOC( +ScatterElements takes three inputs `data`, `updates`, and `indices` of the same +rank r >= 1 and an optional attribute axis that identifies an axis of `data` +(by default, the outer-most axis, that is axis 0). The output of the operation +is produced by creating a copy of the input `data`, and then updating its value +to values specified by `updates` at specific index positions specified by +`indices`. Its output shape is the same as the shape of `data`. + +For each entry in `updates`, the target index in `data` is obtained by combining +the corresponding entry in `indices` with the index of the entry itself: the +index-value for dimension = axis is obtained from the value of the corresponding +entry in `indices` and the index-value for dimension != axis is obtained from the +index of the entry itself. + +For instance, in a 2-D tensor case, the update corresponding to the [i][j] entry +is performed as below: +``` + output[indices[i][j]][j] = updates[i][j] if axis = 0, + output[i][indices[i][j]] = updates[i][j] if axis = 1, +``` + +This operator is the inverse of GatherElements. It is similar to Torch's Scatter operation. + +Example 1: +``` + data = [ + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + ] + indices = [ + [1, 0, 2], + [0, 2, 1], + ] + updates = [ + [1.0, 1.1, 1.2], + [2.0, 2.1, 2.2], + ] + output = [ + [2.0, 1.1, 0.0] + [1.0, 0.0, 2.2] + [0.0, 2.1, 1.2] + ] +``` +Example 2: +``` + data = [[1.0, 2.0, 3.0, 4.0, 5.0]] + indices = [[1, 3]] + updates = [[1.1, 2.1]] + axis = 1 + output = [[1.0, 1.1, 3.0, 2.1, 5.0]] +``` +)DOC"; + +ONNX_OPERATOR_SET_SCHEMA( + ScatterElements, + 13, + OpSchema() + .SetDoc(ScatterElements_ver13_doc) + .Attr( + "axis", + "Which axis to scatter on. Negative value means " + "counting dimensions from the back. Accepted range is [-r, r-1] where r = rank(data).", + AttributeProto::INT, + static_cast(0)) + .Input( + 0, + "data", + "Tensor of rank r >= 1.", + "T", + OpSchema::Single, + true, + 1, + OpSchema::Differentiable) + .Input( + 1, + "indices", + "Tensor of int32/int64 indices, of r >= 1 (same rank as input). All index values are expected to be " + "within bounds [-s, s-1] along axis of size s. It is an error if any of the index values are out of bounds.", + "Tind", + OpSchema::Single, + true, + 1, + OpSchema::NonDifferentiable) + .Input( + 2, + "updates", + "Tensor of rank r >=1 (same rank and shape as indices)", + "T", + OpSchema::Single, + true, + 1, + OpSchema::Differentiable) + .Output( + 0, + "output", + "Tensor of rank r >= 1 (same rank as input).", + "T", + OpSchema::Single, + true, + 1, + OpSchema::Differentiable) + .TypeConstraint( + "T", + OpSchema::all_tensor_types_with_bfloat(), + "Input and output types can be of any tensor type.") + .TypeConstraint( + "Tind", + {"tensor(int32)", "tensor(int64)"}, + "Constrain indices to integer types") + .TypeAndShapeInferenceFunction([](InferenceContext& ctx) { + propagateElemTypeFromInputToOutput(ctx, 0, 0); + if (hasNInputShapes(ctx, 1)) { + propagateShapeFromInputToOutput(ctx, 0, 0); + } + })); + static const char* ScatterElements_ver11_doc = R"DOC( ScatterElements takes three inputs `data`, `updates`, and `indices` of the same rank r >= 1 and an optional attribute axis that identifies an axis of `data` diff --git a/onnx/test/automatic_upgrade_test.py b/onnx/test/automatic_upgrade_test.py index cc077e0f..83607f6f 100644 --- a/onnx/test/automatic_upgrade_test.py +++ b/onnx/test/automatic_upgrade_test.py @@ -841,18 +841,32 @@ class TestAutomaticUpgrade(unittest.TestCase): [TensorProto.FLOAT] ) - def test_ScatterElements(self): # type: () -> None + def test_ScatterElements_1(self): # type: () -> None self._test_op_upgrade('ScatterElements', 11, [[2, 3], [1, 2], [1, 2]], [[2, 3]], [TensorProto.FLOAT, TensorProto.INT64, TensorProto.FLOAT], [TensorProto.FLOAT] ) - def test_ScatterND(self): # type: () -> None + def test_ScatterElements_2(self): # type: () -> None + self._test_op_upgrade('ScatterElements', 16, [[2, 3], [1, 2], [1, 2]], [[2, 3]], + [TensorProto.FLOAT, TensorProto.INT64, TensorProto.FLOAT], + [TensorProto.FLOAT], + attrs={'reduction': 'add'} + ) + + def test_ScatterND_1(self): # type: () -> None self._test_op_upgrade('ScatterND', 11, [[2, 3], [1, 2], [1, 2]], [[2, 3]], [TensorProto.FLOAT, TensorProto.INT64, TensorProto.FLOAT], [TensorProto.FLOAT] ) + def test_ScatterND_2(self): # type: () -> None + self._test_op_upgrade('ScatterND', 16, [[2, 3], [1, 2], [1, 2]], [[2, 3]], + [TensorProto.FLOAT, TensorProto.INT64, TensorProto.FLOAT], + [TensorProto.FLOAT], + attrs={'reduction': 'mul'} + ) + def test_Scan(self): # type: () -> None sum_in = onnx.helper.make_tensor_value_info('sum_in', onnx.TensorProto.FLOAT, [2]) next_in = onnx.helper.make_tensor_value_info('next_in', onnx.TensorProto.FLOAT, [2]) diff --git a/onnx/version_converter/convert.h b/onnx/version_converter/convert.h index 5e195779..a0f38f63 100644 --- a/onnx/version_converter/convert.h +++ b/onnx/version_converter/convert.h @@ -758,6 +758,10 @@ class DefaultVersionConverter : public BaseVersionConverter { /******** 15 -> 16 ********/ registerAdapter(make_unique()); + registerAdapter(make_unique( + "ScatterElements", OpSetID(15), OpSetID(16))); + registerAdapter(make_unique( + "ScatterND", OpSetID(15), OpSetID(16))); registerAdapter(make_unique("Identity", OpSetID(15), OpSetID(16))); registerAdapter(make_unique("Loop", -- GitLab