// META: title=validation tests for WebNN API conv2d operation // META: global=window // META: variant=?cpu // META: variant=?gpu // META: variant=?npu // META: script=../resources/utils_validation.js 'use strict'; // Example input in NCHW layout. const kExampleInputDescriptor = { dataType: 'float32', shape: [1, 1, 5, 5] }; // Example filter in OIHW layout. const kExampleFilterDescriptor = { dataType: 'float32', shape: [1, 1, 3, 3] }; const kExampleBiasDescriptor = { dataType: 'float32', shape: [/* output channels */ 1] }; const label = `conv_2d_*`; multi_builder_test(async (t, builder, otherBuilder) => { const inputFromOtherBuilder = otherBuilder.input('input', kExampleInputDescriptor); const filter = builder.input('filter', kExampleFilterDescriptor); assert_throws_js( TypeError, () => builder.conv2d(inputFromOtherBuilder, filter)); }, '[conv2d] throw if input is from another builder'); multi_builder_test(async (t, builder, otherBuilder) => { const filterFromOtherBuilder = otherBuilder.input('filter', kExampleFilterDescriptor); const input = builder.input('input', kExampleInputDescriptor); assert_throws_js( TypeError, () => builder.conv2d(input, filterFromOtherBuilder)); }, '[conv2d] throw if filter is from another builder'); multi_builder_test(async (t, builder, otherBuilder) => { const biasFromOtherBuilder = otherBuilder.input('bias', kExampleBiasDescriptor); const options = {inputLayout: 'nchw', bias: biasFromOtherBuilder}; const input = builder.input('input', kExampleInputDescriptor); const filter = builder.input('filter', kExampleFilterDescriptor); assert_throws_js(TypeError, () => builder.conv2d(input, filter, options)); }, '[conv2d] throw if bias option is from another builder'); const tests = [ { name: '[conv2d] Test with default options.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 3, 3]}, output: {dataType: 'float32', shape: [1, 1, 3, 3]} }, { name: '[conv2d] Test with padding.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 3, 3]}, options: { padding: [1, 1, 1, 1], }, output: {dataType: 'float32', shape: [1, 1, 5, 5]} }, { name: '[conv2d] Test with strides and padding.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 3, 3]}, options: { padding: [1, 1, 1, 1], strides: [2, 2], }, output: {dataType: 'float32', shape: [1, 1, 3, 3]} }, { name: '[conv2d] Test with strides and asymmetric padding.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 4, 2]}, options: { padding: [1, 2, 0, 1], strides: [2, 2], }, output: {dataType: 'float32', shape: [1, 1, 3, 3]} }, { name: '[conv2d] Test depthwise conv2d by setting groups to input channels.', input: {dataType: 'float32', shape: [1, 4, 2, 2]}, filter: {dataType: 'float32', shape: [4, 1, 2, 2]}, options: { groups: 4, }, output: {dataType: 'float32', shape: [1, 4, 1, 1]} }, { name: '[conv2d] Test depthwise conv2d with groups, inputLayout="nhwc" and filterLayout="ihwo".', input: {dataType: 'float32', shape: [1, 2, 2, 4]}, filter: {dataType: 'float32', shape: [1, 2, 2, 4]}, options: { groups: 4, inputLayout: 'nhwc', filterLayout: 'ihwo', }, output: {dataType: 'float32', shape: [1, 1, 1, 4]} }, { name: '[conv2d] Test with dilations, inputLayout="nhwc" and filterLayout="ihwo".', input: {dataType: 'float32', shape: [1, 65, 65, 1]}, filter: {dataType: 'float32', shape: [1, 3, 3, 1]}, options: { inputLayout: 'nhwc', filterLayout: 'ihwo', dilations: [4, 4], }, output: {dataType: 'float32', shape: [1, 57, 57, 1]} }, { name: '[conv2d] Test with inputLayout="nchw" and filterLayout="oihw".', input: {dataType: 'float32', shape: [1, 2, 5, 5]}, filter: {dataType: 'float32', shape: [1, 2, 3, 3]}, options: { inputLayout: 'nchw', filterLayout: 'oihw', }, output: {dataType: 'float32', shape: [1, 1, 3, 3]} }, { name: '[conv2d] Test with inputLayout="nchw" and filterLayout="hwio".', input: {dataType: 'float32', shape: [1, 2, 5, 5]}, filter: {dataType: 'float32', shape: [3, 3, 2, 1]}, options: { inputLayout: 'nchw', filterLayout: 'hwio', }, output: {dataType: 'float32', shape: [1, 1, 3, 3]} }, { name: '[conv2d] Test with inputLayout="nchw" and filterLayout="ohwi".', input: {dataType: 'float32', shape: [1, 2, 5, 5]}, filter: {dataType: 'float32', shape: [1, 3, 3, 2]}, options: { inputLayout: 'nchw', filterLayout: 'ohwi', }, output: {dataType: 'float32', shape: [1, 1, 3, 3]} }, { name: '[conv2d] Test with inputLayout="nchw" and filterLayout="ihwo".', input: {dataType: 'float32', shape: [1, 2, 5, 5]}, filter: {dataType: 'float32', shape: [2, 3, 3, 1]}, options: { inputLayout: 'nchw', filterLayout: 'ihwo', }, output: {dataType: 'float32', shape: [1, 1, 3, 3]} }, { name: '[conv2d] Test with inputLayout="nhwc" and filterLayout="oihw".', input: {dataType: 'float32', shape: [1, 5, 5, 2]}, filter: {dataType: 'float32', shape: [1, 2, 3, 3]}, options: { inputLayout: 'nhwc', filterLayout: 'oihw', }, output: {dataType: 'float32', shape: [1, 3, 3, 1]} }, { name: '[conv2d] Test with inputLayout="nhwc" and filterLayout="hwio".', input: {dataType: 'float32', shape: [1, 5, 5, 2]}, filter: {dataType: 'float32', shape: [3, 3, 2, 1]}, options: { inputLayout: 'nhwc', filterLayout: 'hwio', }, output: {dataType: 'float32', shape: [1, 3, 3, 1]} }, { name: '[conv2d] Test with inputLayout="nhwc" and filterLayout="ohwi".', input: {dataType: 'float32', shape: [1, 5, 5, 2]}, filter: {dataType: 'float32', shape: [1, 3, 3, 2]}, options: { inputLayout: 'nhwc', filterLayout: 'ohwi', }, output: {dataType: 'float32', shape: [1, 3, 3, 1]} }, { name: '[conv2d] Test with inputLayout="nhwc" and filterLayout="ihwo".', input: {dataType: 'float32', shape: [1, 5, 5, 2]}, filter: {dataType: 'float32', shape: [2, 3, 3, 1]}, options: { inputLayout: 'nhwc', filterLayout: 'ihwo', }, output: {dataType: 'float32', shape: [1, 3, 3, 1]} }, { name: '[conv2d] Throw if the input is not a 4-D tensor.', input: {dataType: 'float32', shape: [1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 2, 2, 1]}, options: {label}, }, { name: '[conv2d] Throw if the input data type is not floating point.', input: {dataType: 'int32', shape: [1, 1, 5, 5]}, filter: {dataType: 'int32', shape: [1, 1, 2, 2]}, options: {label}, }, { name: '[conv2d] Throw if the filter is not a 4-D tensor.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [2, 2]}, options: {label}, }, { name: '[conv2d] Throw if the filter data type doesn\'t match the input data type.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'int32', shape: [1, 1, 2, 2]}, options: { label: label, }, }, { name: '[conv2d] Throw if the length of padding is not 4.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 2, 2]}, options: { padding: [2, 2], label: label, }, }, { name: '[conv2d] Throw if the length of strides is not 2.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 2, 2]}, options: { strides: [2], label: label, }, }, { name: '[conv2d] Throw if strideHeight is smaller than 1.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 2, 2]}, options: { strides: [0, 1], label: label, }, }, { name: '[conv2d] Throw if strideWidth is smaller than 1.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 2, 2]}, options: { strides: [1, 0], label: label, }, }, { name: '[conv2d] Throw if the length of dilations is not 2.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 2, 2]}, options: { dilations: [1], label: label, }, }, { name: '[conv2d] Throw if dilationHeight is smaller than 1.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 2, 2]}, options: { dilations: [0, 1], label: label, }, }, { name: '[conv2d] Throw if dilationWidth is smaller than 1.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 2, 2]}, options: { dilations: [1, 0], label: label, }, }, { name: '[conv2d] Throw if inputChannels % groups is not 0.', input: {dataType: 'float32', shape: [1, 4, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 2, 2]}, options: { groups: 3, label: label, }, }, { name: '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels.', input: {dataType: 'float32', shape: [1, 4, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 2, 2]}, options: { groups: 2, label: label, }, }, { name: '[conv2d] Throw if the groups is smaller than 1.', input: {dataType: 'float32', shape: [1, 4, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 2, 2]}, options: { groups: 0, label: label, }, }, { name: '[conv2d] Throw due to overflow when calculating the effective filter height.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 434983, 2]}, options: { dilations: [328442, 1], label: label, }, }, { name: '[conv2d] Throw due to overflow when calculating the effective filter width.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 2, 234545]}, options: { dilations: [2, 843452], label: label, }, }, { name: '[conv2d] Throw due to overflow when dilation height is too large.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 3, 3]}, options: { dilations: [kMaxUnsignedLong, 1], label: label, }, }, { name: '[conv2d] Throw due to overflow when dilation width is too large.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 3, 3]}, options: { dilations: [1, kMaxUnsignedLong], label: label, }, }, { name: '[conv2d] Throw due to underflow when calculating the output height.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 4, 2]}, options: { dilations: [4, 1], padding: [1, 1, 1, 1], strides: [2, 2], label: label, }, }, { name: '[conv2d] Throw due to underflow when calculating the output width.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 2, 8]}, options: { dilations: [1, 4], padding: [1, 1, 1, 1], strides: [2, 2], label: label, }, }, { name: '[conv2d] Throw if the bias is not a 1-D tensor.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 2, 2]}, options: { bias: {dataType: 'float32', shape: [1, 2]}, label: label, }, }, { name: '[conv2d] Throw if the bias shape is not equal to [output_channels] with filterLayout="oihw".', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 2, 2]}, options: { bias: {dataType: 'float32', shape: [2]}, label: label, }, }, { name: '[conv2d] Throw if the bias shape is not equal to [output_channels] with filterLayout="hwio".', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [2, 2, 1, 1]}, options: { bias: {dataType: 'float32', shape: [2]}, label: label, }, }, { name: '[conv2d] Throw if the bias shape is not equal to [output_channels] with filterLayout="ohwi".', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 2, 2, 1]}, options: { bias: {dataType: 'float32', shape: [2]}, label: label, }, }, { name: '[conv2d] Throw if the bias shape is not equal to [output_channels] with filterLayout="ihwo".', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 2, 2, 1]}, options: { bias: {dataType: 'float32', shape: [2]}, label: label, }, }, { name: '[conv2d] Throw if the bias data type doesn\'t match input data type.', input: {dataType: 'float32', shape: [1, 1, 5, 5]}, filter: {dataType: 'float32', shape: [1, 1, 2, 2]}, options: { bias: {dataType: 'int32', shape: [1]}, label: label, }, }, { name: '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nchw" and filterLayout="oihw".', input: {dataType: 'float32', shape: [1, 2, 5, 5]}, filter: {dataType: 'float32', shape: [1, 2, 3, 3]}, options: { inputLayout: 'nchw', filterLayout: 'oihw', groups: 2, label: label, }, }, { name: '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nchw" and filterLayout="hwio".', input: {dataType: 'float32', shape: [1, 2, 5, 5]}, filter: {dataType: 'float32', shape: [3, 3, 2, 1]}, options: { inputLayout: 'nchw', filterLayout: 'hwio', groups: 2, label: label, }, }, { name: '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nchw" and filterLayout="ohwi".', input: {dataType: 'float32', shape: [1, 2, 5, 5]}, filter: {dataType: 'float32', shape: [1, 3, 3, 2]}, options: { inputLayout: 'nchw', filterLayout: 'ohwi', groups: 2, label: label, }, }, { name: '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nchw" and filterLayout="ihwo".', input: {dataType: 'float32', shape: [1, 2, 5, 5]}, filter: {dataType: 'float32', shape: [2, 3, 3, 1]}, options: { inputLayout: 'nchw', filterLayout: 'ihwo', groups: 2, label: label, }, }, { name: '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nhwc" and filterLayout="oihw".', input: {dataType: 'float32', shape: [1, 5, 5, 2]}, filter: {dataType: 'float32', shape: [1, 2, 3, 3]}, options: { inputLayout: 'nhwc', filterLayout: 'oihw', groups: 2, label: label, }, }, { name: '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nhwc" and filterLayout="hwio".', input: {dataType: 'float32', shape: [1, 5, 5, 2]}, filter: {dataType: 'float32', shape: [3, 3, 2, 1]}, options: { inputLayout: 'nhwc', filterLayout: 'hwio', groups: 2, label: label, }, }, { name: '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nhwc" and filterLayout="ohwi".', input: {dataType: 'float32', shape: [1, 5, 5, 2]}, filter: {dataType: 'float32', shape: [1, 3, 3, 2]}, options: { inputLayout: 'nhwc', filterLayout: 'ohwi', groups: 2, label: label, }, }, { name: '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nhwc" and filterLayout="ihwo".', input: {dataType: 'float32', shape: [1, 5, 5, 2]}, filter: {dataType: 'float32', shape: [2, 3, 3, 1]}, options: { inputLayout: 'nhwc', filterLayout: 'ihwo', groups: 2, label: label, }, }, ]; tests.forEach( test => promise_test(async t => { const builder = new MLGraphBuilder(context); const input = builder.input('input', test.input); const filter = builder.input('filter', test.filter); if (test.options && test.options.bias) { test.options.bias = builder.input('bias', test.options.bias); } if (test.output && context.opSupportLimits().conv2d.input.dataTypes.includes( test.input.dataType)) { const output = builder.conv2d(input, filter, test.options); assert_equals(output.dataType, test.output.dataType); assert_array_equals(output.shape, test.output.shape); } else { const regrexp = /\[conv_2d_\*\]/; assert_throws_with_label( () => builder.conv2d(input, filter, test.options), regrexp); } }, test.name));