Skip to content
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

Implement argMax and argMin #62

Merged
merged 6 commits into from
Dec 18, 2023

Conversation

BruceDai
Copy link
Contributor

@huningxin @fdwr @miaobin PTAL, thanks.

src/arg_max_min.js Outdated Show resolved Hide resolved
test/reduce_test.js Outdated Show resolved Hide resolved
test/arg_max_min_test.js Show resolved Hide resolved
src/arg_max_min.js Outdated Show resolved Hide resolved
@BruceDai
Copy link
Contributor Author

@huningxin I've updated commit to address your comments, please take another look, thanks.

Copy link
Contributor

@huningxin huningxin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link

@fdwr fdwr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some corner case considerations and minor suggestions, else LGTM.

src/arg_max_min.js Outdated Show resolved Hide resolved
src/arg_max_min.js Outdated Show resolved Hide resolved
} = {}) {
// If axes doesn't present (defaulting to null), all dimensions are reduced.
// See https://webmachinelearning.github.io/webnn/#dom-mlargminmaxoptions-axes.
const inpAxes = axes ?? new Array(input.rank).fill(0).map((_, i) => i);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding axes ??, I'm not that familiar with the falsesyness of JS evaluation. If you pass axes = [], will that correctly use the explicit empty axes (output shape is then the same size as the input, and there is a 1:1 mapping of output indices) rather than generate full reduction?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

axes = []

It's so very special, it's neither null nor a sequence having values in the range [0, N-1]. Is there any model using this axes?

According to https://webmachinelearning.github.io/webnn/#dom-mlargminmaxoptions-axes:

axes, of type sequence<unsigned long>, defaulting to null
A sequence of [unsigned long](https://webidl.spec.whatwg.org/#idl-unsigned-long). The dimensions to reduce. The values in the sequence must be in the range [0, N-1] where N is the [rank](https://webmachinelearning.github.io/webnn/#mloperand-rank) of the input tensor. If not present, all dimensions are reduced.

Copy link

@fdwr fdwr Dec 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My argument is mainly driven by robustness plus consistency with similar behavior for the reduction operators when passed explicit [] axes. You probably won't find a model unless you look really hard, but some model generation parameters could programmatically produce it; and we see that in related reduction operators like ReduceSum that explicit empty axes [] was important enough that ONNX specifically added the noop_with_empty_axes attribute later https://onnx.ai/onnx/operators/onnx__ReduceSum.html. Speaking of that, do we have any explicit axes = [] cases in reduceSum?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last we also missed considering this axes = [] cases in reduceSum.
Since current Spec is lack of explanation for axes = [], I will open an issue for discussing it on WG.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will open an issue for discussing it on WG.

Alrighty, and I will add my clear expectation of what should happen in the nop case 😉 (especially if WebNN is to support existing frameworks behavior where [] means nop).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was fast! I tried ONNX and TF:

x = tf.constant([[0,100],[200,300]], dtype=tf.float32)
y = tf.math.reduce_sum(x, axis=[]) # https://www.tensorflow.org/api_docs/python/tf/math/reduce_sum
print("value:", y)
print("shape:", y.shape)

# Prints:
# value: tf.Tensor(
# [[  0. 100.]
#  [200. 300.]], shape=(2, 2), dtype=float32)
# shape: (2, 2)

(will add that to the issue...)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to wait for the spec discussion or proceed and react later accordingly? (I don't mind either way, and you might not get a reply for the next 2 weeks...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I will follow your guide to support axes=[] here.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p.s. Appears that the Chromium implementation supports explicit axes = [] gracefully:
https://chromium-review.googlesource.com/c/chromium/src/+/5125652

src/reduce.js Show resolved Hide resolved
src/reduce.js Outdated

const valuesToReduce = [];
// Find all values to reduce.
for (let reduceIndex = 0; reduceIndex < reduceElements; ++reduceIndex) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about reducedElementCount? reduceElements sounds like it's actually the elements themselves (the values).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

src/reduce.js Outdated
const reduceDims = axes.map((axis) => input.shape[axis]);
const reduceElements = sizeOfShape(reduceDims);
const reduceStrides = new Array(axes.length);
reduceStrides[reduceStrides.length - 1] = 1;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appears this will fail on the axes = [] case, which should return exactly one element of shape []. Most of the logic already looks correct to handle that case (including sizeOfShape which will return 1).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's strange. It can work for this case without changes, my understanding is that index should be start from 0.

reduceStrides = [];
reduceStrides[-1] = 1; // it can work

But I add if condition block before this, then it never goes here for axes=[], please see the update commit, thanks.

Copy link

@fdwr fdwr Dec 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's strange. It can work for this case without changes
reduceStrides[-1] = 1; // it can work

(huh, what a weird language: https://stackoverflow.com/questions/58570277/adding-an-element-in-negative-index-of-an-array)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reduceStrides[-1] = 1 getting [-1: 1] is bad, we should avoid that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @huningxin!
I've updated commit to avoid it, please take another look, thanks.

},
argMax,
{
axes: [1, 0],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Good that you have an axis-reversed case too in the argMinMax tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

selectLastIndex: true,
});
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For completeness, we should include an empty axes case:

  it('argMin axes=[]', function() {
    testArgMaxMin(
        {
          shape: [3, 3],
          value: [
            1, 2, 3,
            3, 0, 0,
            2, 5, 2,
          ],
        },
        {
          shape: [3, 3],
          value: [
            0, 0, 0,
            0, 0, 0,
            0, 0, 0,
          ],
        },
        argMin,
        {
          axes: [],
        });
  });

And the corner case for scalar (which can only ever return a scalar of 0):

  it('argMin scalar axes=[]', function() {
    testArgMaxMin(
        {
          shape: [],
          value: [3],
        },
        {
          shape: [],
          value: [0],
        },
        argMin,
        {
          axes: [],
        });
  });

(note in these cases, keepDimensions and selectLastIndex have no effect on the output)

@BruceDai
Copy link
Contributor Author

@fdwr I've updated commit to address your comments, please take another look. Thanks a lot!

Copy link

@fdwr fdwr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks Bruce. ⭐

src/reduce.js Outdated
const reduceDims = axes.map((axis) => input.shape[axis]);
const reduceElements = sizeOfShape(reduceDims);
const reduceStrides = new Array(axes.length);
reduceStrides[reduceStrides.length - 1] = 1;
Copy link

@fdwr fdwr Dec 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's strange. It can work for this case without changes
reduceStrides[-1] = 1; // it can work

(huh, what a weird language: https://stackoverflow.com/questions/58570277/adding-an-element-in-negative-index-of-an-array)

});
});

it('argMax scalar axes=[] none effect by both keepDimensions and selectLastIndex being true',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no effect

src/reduce.js Outdated
const reduceDims = axes.map((axis) => input.shape[axis]);
const reducedElementCount = sizeOfShape(reduceDims);
const reduceStrides = new Array(axes.length);
reduceStrides[reduceStrides.length - 1] = 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
reduceStrides[reduceStrides.length - 1] = 1;
if (reduceStrides.length > 0) {
reduceStrides[reduceStrides.length - 1] = 1;
}

Copy link

@fdwr fdwr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with Ningxin's comment applied.

@huningxin huningxin merged commit 058aa77 into webmachinelearning:main Dec 18, 2023
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants