Xenova HF Staff commited on
Commit
ab820a8
·
verified ·
1 Parent(s): a0e12a2

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,19 +18,19 @@ See the [ONNX `BatchNormalization` spec](https://onnx.ai/onnx/operators/onnx__Ba
18
 
19
  ## Inputs
20
 
21
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
- | `X` | `x` | `T` | — | — | Input data tensor with shape `(N, C, D1, ..., Dn)`, normalized independently per channel using the supplied estimated statistics. | required |
24
- | `scale` | `scale` | `T` | `1` | — | Per-channel scale tensor with shape `(C)`. | required |
25
- | `B` | `b` | `T` | `1` | — | Per-channel bias tensor with shape `(C)`. | required |
26
- | `input_mean` | `inputMean` | `T` | `1` | — | Precomputed estimated mean tensor with shape `(C)` used for inference. | required |
27
- | `input_var` | `inputVar` | `T` | `1` | — | Precomputed estimated variance tensor with shape `(C)` used for inference. | required |
28
 
29
  ## Outputs
30
 
31
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
32
  | --- | --- | --- | --- | --- | --- | --- |
33
- | `Y` | `y` | `T` | same as `X` | same as `X` | Batch-normalized output tensor with the same shape as `X`. | required |
34
 
35
  ## Attributes
36
 
@@ -50,20 +50,25 @@ Default values (overridable per request):
50
 
51
  ## Files
52
 
53
- - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
54
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
55
  - [`test.json`](build/webgpu/test.json) — correctness cases
56
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
57
  - [`batch-normalization-nc-vec4.wgsl.jinja`](build/webgpu/batch-normalization-nc-vec4.wgsl.jinja)
 
58
  - [`batch-normalization-nchw-vec4.wgsl.jinja`](build/webgpu/batch-normalization-nchw-vec4.wgsl.jinja)
59
  - [`batch-normalization-nchw.wgsl.jinja`](build/webgpu/batch-normalization-nchw.wgsl.jinja)
60
 
61
  ## Use with `@huggingface/kernels`
62
 
63
- The loader derives every required output's shape and logical dtype from the manifest contract and this call.
64
- It then allocates the result tensors automatically.
 
 
 
65
 
66
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
 
67
 
68
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
69
 
 
18
 
19
  ## Inputs
20
 
21
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
+ | `x` | `X` | `T` | — | — | Input data tensor with shape `(N, C, D1, ..., Dn)`, normalized independently per channel using the supplied estimated statistics. | required |
24
+ | `scale` | | `T` | `1` | — | Per-channel scale tensor with shape `(C)`. | required |
25
+ | `b` | `B` | `T` | `1` | — | Per-channel bias tensor with shape `(C)`. | required |
26
+ | `inputMean` | `input_mean` | `T` | `1` | — | Precomputed estimated mean tensor with shape `(C)` used for inference. | required |
27
+ | `inputVar` | `input_var` | `T` | `1` | — | Precomputed estimated variance tensor with shape `(C)` used for inference. | required |
28
 
29
  ## Outputs
30
 
31
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
32
  | --- | --- | --- | --- | --- | --- | --- |
33
+ | `y` | `Y` | `T` | same as `x` | same as `x` | Batch-normalized output tensor with the same shape as `X`. | required |
34
 
35
  ## Attributes
36
 
 
50
 
51
  ## Files
52
 
53
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
54
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
55
  - [`test.json`](build/webgpu/test.json) — correctness cases
56
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
57
  - [`batch-normalization-nc-vec4.wgsl.jinja`](build/webgpu/batch-normalization-nc-vec4.wgsl.jinja)
58
+ - [`batch-normalization-nchw-flat-vec4.wgsl.jinja`](build/webgpu/batch-normalization-nchw-flat-vec4.wgsl.jinja)
59
  - [`batch-normalization-nchw-vec4.wgsl.jinja`](build/webgpu/batch-normalization-nchw-vec4.wgsl.jinja)
60
  - [`batch-normalization-nchw.wgsl.jinja`](build/webgpu/batch-normalization-nchw.wgsl.jinja)
61
 
62
  ## Use with `@huggingface/kernels`
63
 
64
+ ```sh
65
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.2
66
+ ```
67
+
68
+ Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
69
 
70
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
71
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
72
 
73
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
74
 
build/webgpu/batch-normalization-nc-vec4.wgsl.jinja CHANGED
@@ -1,30 +1,30 @@
1
  {% macro flat_index_2d(name="i", bound="params.count", guardInline=false, note="dispatch-limit") %}
2
  {% if note == "dispatch-limit" %}
3
- // 2D-folded flat index: gid.y carries the high bits past the
4
- // maxComputeWorkgroupsPerDimension dispatch limit (outputs > 16.7M elements).
5
  {% elif note == "limit" %}
6
- // 2D-folded flat index: gid.y carries the high bits past the
7
- // maxComputeWorkgroupsPerDimension limit.
8
  {% elif note == "device-axis" %}
9
- // The flat dispatch is folded across x/y at the device's per-axis workgroup
10
- // limit; gid.y carries the high portion of the output index.
11
  {% elif note == "vec4-limit" %}
12
- // 2D-folded flat vec4 index: gid.y carries the high bits past the
13
- // maxComputeWorkgroupsPerDimension limit (the dispatch caps x and spills into y).
14
  {% elif note == "element-limit" %}
15
  // 2D-folded flat element index: gid.y carries the high bits past the
16
- // maxComputeWorkgroupsPerDimension limit.
17
  {% elif note == "dispatch" %}
18
- // 2D-folded flat index: gid.y carries the high bits past the
19
- // maxComputeWorkgroupsPerDimension dispatch limit.
20
  {% endif %}
21
  {% if bound == "" %}
22
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
23
  {%- elif guardInline %}
24
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
25
  if ({{ name }} >= {{ bound }}) { return; }
26
  {%- else %}
27
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
28
  if ({{ name }} >= {{ bound }}) {
29
  return;
30
  }
@@ -36,9 +36,12 @@
36
  // Rank-2 [N, C] inference vec4 specialization. Vectors run across adjacent channels, so
37
  // scale/bias/mean/var are also bound as vec4<f32> and C must be divisible by 4.
38
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
39
- fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
40
  {{ flat_index_2d("i", "params.count4") }}
41
  let channel4 = i % params.channels4;
42
  let alpha = scale[channel4] * inverseSqrt(input_var[channel4] + vec4<f32>(params.epsilon));
43
- y[i] = x[i] * alpha + (bias[channel4] - input_mean[channel4] * alpha);
 
 
 
44
  }
 
1
  {% macro flat_index_2d(name="i", bound="params.count", guardInline=false, note="dispatch-limit") %}
2
  {% if note == "dispatch-limit" %}
3
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
4
+ // per-axis workgroup fold width (outputs > 16.7M elements).
5
  {% elif note == "limit" %}
6
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
7
+ // per-axis workgroup fold width.
8
  {% elif note == "device-axis" %}
9
+ // The flat dispatch is folded across x/y at a fixed per-axis workgroup
10
+ // width; gid.y carries the high portion of the output index.
11
  {% elif note == "vec4-limit" %}
12
+ // 2D-folded flat vec4 index: gid.y carries the high bits past the dispatch's
13
+ // per-axis workgroup fold width (the dispatch caps x and spills into y).
14
  {% elif note == "element-limit" %}
15
  // 2D-folded flat element index: gid.y carries the high bits past the
16
+ // dispatch's per-axis workgroup fold width.
17
  {% elif note == "dispatch" %}
18
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
19
+ // per-axis workgroup fold width.
20
  {% endif %}
21
  {% if bound == "" %}
22
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
23
  {%- elif guardInline %}
24
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
25
  if ({{ name }} >= {{ bound }}) { return; }
26
  {%- else %}
27
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
28
  if ({{ name }} >= {{ bound }}) {
29
  return;
30
  }
 
36
  // Rank-2 [N, C] inference vec4 specialization. Vectors run across adjacent channels, so
37
  // scale/bias/mean/var are also bound as vec4<f32> and C must be divisible by 4.
38
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
39
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
40
  {{ flat_index_2d("i", "params.count4") }}
41
  let channel4 = i % params.channels4;
42
  let alpha = scale[channel4] * inverseSqrt(input_var[channel4] + vec4<f32>(params.epsilon));
43
+ // Subtract the mean before scaling. The expanded form
44
+ // x * alpha + (bias - mean * alpha) can lose the small residual (x - mean)
45
+ // when |mean| is much larger than |x - mean|.
46
+ y[i] = (x[i] - input_mean[channel4]) * alpha + bias[channel4];
47
  }
build/webgpu/batch-normalization-nchw-flat-vec4.wgsl.jinja ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% macro flat_index_2d(name="i", bound="params.count", guardInline=false, note="dispatch-limit") %}
2
+ {% if note == "dispatch-limit" %}
3
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
4
+ // per-axis workgroup fold width (outputs > 16.7M elements).
5
+ {% elif note == "limit" %}
6
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
7
+ // per-axis workgroup fold width.
8
+ {% elif note == "device-axis" %}
9
+ // The flat dispatch is folded across x/y at a fixed per-axis workgroup
10
+ // width; gid.y carries the high portion of the output index.
11
+ {% elif note == "vec4-limit" %}
12
+ // 2D-folded flat vec4 index: gid.y carries the high bits past the dispatch's
13
+ // per-axis workgroup fold width (the dispatch caps x and spills into y).
14
+ {% elif note == "element-limit" %}
15
+ // 2D-folded flat element index: gid.y carries the high bits past the
16
+ // dispatch's per-axis workgroup fold width.
17
+ {% elif note == "dispatch" %}
18
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
19
+ // per-axis workgroup fold width.
20
+ {% endif %}
21
+ {% if bound == "" %}
22
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
23
+ {%- elif guardInline %}
24
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
25
+ if ({{ name }} >= {{ bound }}) { return; }
26
+ {%- else %}
27
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
28
+ if ({{ name }} >= {{ bound }}) {
29
+ return;
30
+ }
31
+ {%- endif %}
32
+ {% endmacro %}
33
+
34
+ {{ env.wgsl.resourceDeclarations }}
35
+
36
+ // Inference-only vec4 specialization for channel planes whose length is not a
37
+ // multiple of four. The vec4 grid covers the flat tensor, so a group can cross a
38
+ // channel boundary. Boundary groups resolve each component's channel separately;
39
+ // all other groups share one channel. Each component computes:
40
+ // (x - mean) * inverseSqrt(var + epsilon) * scale + bias
41
+ fn normalize_at(value: f32, channel: u32) -> f32 {
42
+ let normalized = (value - input_mean[channel]) * inverseSqrt(input_var[channel] + params.epsilon);
43
+ return normalized * scale[channel] + bias[channel];
44
+ }
45
+
46
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
47
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
48
+ {{ flat_index_2d("i", "params.count4") }}
49
+ let base = i * 4u;
50
+ let first = (base / params.spatial) % params.channels;
51
+ let last = ((base + 3u) / params.spatial) % params.channels;
52
+ let value = x[i];
53
+ if (first == last) {
54
+ let normalized = (value - vec4<f32>(input_mean[first])) * inverseSqrt(input_var[first] + params.epsilon);
55
+ y[i] = normalized * vec4<f32>(scale[first]) + vec4<f32>(bias[first]);
56
+ return;
57
+ }
58
+ y[i] = vec4<f32>(
59
+ normalize_at(value.x, first),
60
+ normalize_at(value.y, ((base + 1u) / params.spatial) % params.channels),
61
+ normalize_at(value.z, ((base + 2u) / params.spatial) % params.channels),
62
+ normalize_at(value.w, last));
63
+ }
build/webgpu/batch-normalization-nchw-vec4.wgsl.jinja CHANGED
@@ -1,30 +1,30 @@
1
  {% macro flat_index_2d(name="i", bound="params.count", guardInline=false, note="dispatch-limit") %}
2
  {% if note == "dispatch-limit" %}
3
- // 2D-folded flat index: gid.y carries the high bits past the
4
- // maxComputeWorkgroupsPerDimension dispatch limit (outputs > 16.7M elements).
5
  {% elif note == "limit" %}
6
- // 2D-folded flat index: gid.y carries the high bits past the
7
- // maxComputeWorkgroupsPerDimension limit.
8
  {% elif note == "device-axis" %}
9
- // The flat dispatch is folded across x/y at the device's per-axis workgroup
10
- // limit; gid.y carries the high portion of the output index.
11
  {% elif note == "vec4-limit" %}
12
- // 2D-folded flat vec4 index: gid.y carries the high bits past the
13
- // maxComputeWorkgroupsPerDimension limit (the dispatch caps x and spills into y).
14
  {% elif note == "element-limit" %}
15
  // 2D-folded flat element index: gid.y carries the high bits past the
16
- // maxComputeWorkgroupsPerDimension limit.
17
  {% elif note == "dispatch" %}
18
- // 2D-folded flat index: gid.y carries the high bits past the
19
- // maxComputeWorkgroupsPerDimension dispatch limit.
20
  {% endif %}
21
  {% if bound == "" %}
22
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
23
  {%- elif guardInline %}
24
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
25
  if ({{ name }} >= {{ bound }}) { return; }
26
  {%- else %}
27
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
28
  if ({{ name }} >= {{ bound }}) {
29
  return;
30
  }
@@ -38,7 +38,7 @@
38
  // calculation is:
39
  // (x - mean) * inverseSqrt(var + epsilon) * scale + bias
40
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
41
- fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
42
  {{ flat_index_2d("i", "params.count4") }}
43
  let channel = (i / params.spatial4) % params.channels;
44
  let normalized = (x[i] - vec4<f32>(input_mean[channel])) * inverseSqrt(input_var[channel] + params.epsilon);
 
1
  {% macro flat_index_2d(name="i", bound="params.count", guardInline=false, note="dispatch-limit") %}
2
  {% if note == "dispatch-limit" %}
3
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
4
+ // per-axis workgroup fold width (outputs > 16.7M elements).
5
  {% elif note == "limit" %}
6
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
7
+ // per-axis workgroup fold width.
8
  {% elif note == "device-axis" %}
9
+ // The flat dispatch is folded across x/y at a fixed per-axis workgroup
10
+ // width; gid.y carries the high portion of the output index.
11
  {% elif note == "vec4-limit" %}
12
+ // 2D-folded flat vec4 index: gid.y carries the high bits past the dispatch's
13
+ // per-axis workgroup fold width (the dispatch caps x and spills into y).
14
  {% elif note == "element-limit" %}
15
  // 2D-folded flat element index: gid.y carries the high bits past the
16
+ // dispatch's per-axis workgroup fold width.
17
  {% elif note == "dispatch" %}
18
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
19
+ // per-axis workgroup fold width.
20
  {% endif %}
21
  {% if bound == "" %}
22
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
23
  {%- elif guardInline %}
24
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
25
  if ({{ name }} >= {{ bound }}) { return; }
26
  {%- else %}
27
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
28
  if ({{ name }} >= {{ bound }}) {
29
  return;
30
  }
 
38
  // calculation is:
39
  // (x - mean) * inverseSqrt(var + epsilon) * scale + bias
40
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
41
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
42
  {{ flat_index_2d("i", "params.count4") }}
43
  let channel = (i / params.spatial4) % params.channels;
44
  let normalized = (x[i] - vec4<f32>(input_mean[channel])) * inverseSqrt(input_var[channel] + params.epsilon);
build/webgpu/batch-normalization-nchw.wgsl.jinja CHANGED
@@ -1,42 +1,39 @@
1
  {% macro flat_index_2d(name="i", bound="params.count", guardInline=false, note="dispatch-limit") %}
2
  {% if note == "dispatch-limit" %}
3
- // 2D-folded flat index: gid.y carries the high bits past the
4
- // maxComputeWorkgroupsPerDimension dispatch limit (outputs > 16.7M elements).
5
  {% elif note == "limit" %}
6
- // 2D-folded flat index: gid.y carries the high bits past the
7
- // maxComputeWorkgroupsPerDimension limit.
8
  {% elif note == "device-axis" %}
9
- // The flat dispatch is folded across x/y at the device's per-axis workgroup
10
- // limit; gid.y carries the high portion of the output index.
11
  {% elif note == "vec4-limit" %}
12
- // 2D-folded flat vec4 index: gid.y carries the high bits past the
13
- // maxComputeWorkgroupsPerDimension limit (the dispatch caps x and spills into y).
14
  {% elif note == "element-limit" %}
15
  // 2D-folded flat element index: gid.y carries the high bits past the
16
- // maxComputeWorkgroupsPerDimension limit.
17
  {% elif note == "dispatch" %}
18
- // 2D-folded flat index: gid.y carries the high bits past the
19
- // maxComputeWorkgroupsPerDimension dispatch limit.
20
  {% endif %}
21
  {% if bound == "" %}
22
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
23
  {%- elif guardInline %}
24
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
25
  if ({{ name }} >= {{ bound }}) { return; }
26
  {%- else %}
27
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
28
  if ({{ name }} >= {{ bound }}) {
29
  return;
30
  }
31
  {%- endif %}
32
  {% endmacro %}
33
 
34
- {% if usesF16 %}
35
- enable f16;
36
- {% endif %}
37
  {{ env.wgsl.resourceDeclarations }}
38
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
39
- fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
40
  {{ flat_index_2d("index") }}
41
  let spatial = params.height * params.width;
42
  let channel = (index / spatial) % params.channels;
 
1
  {% macro flat_index_2d(name="i", bound="params.count", guardInline=false, note="dispatch-limit") %}
2
  {% if note == "dispatch-limit" %}
3
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
4
+ // per-axis workgroup fold width (outputs > 16.7M elements).
5
  {% elif note == "limit" %}
6
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
7
+ // per-axis workgroup fold width.
8
  {% elif note == "device-axis" %}
9
+ // The flat dispatch is folded across x/y at a fixed per-axis workgroup
10
+ // width; gid.y carries the high portion of the output index.
11
  {% elif note == "vec4-limit" %}
12
+ // 2D-folded flat vec4 index: gid.y carries the high bits past the dispatch's
13
+ // per-axis workgroup fold width (the dispatch caps x and spills into y).
14
  {% elif note == "element-limit" %}
15
  // 2D-folded flat element index: gid.y carries the high bits past the
16
+ // dispatch's per-axis workgroup fold width.
17
  {% elif note == "dispatch" %}
18
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
19
+ // per-axis workgroup fold width.
20
  {% endif %}
21
  {% if bound == "" %}
22
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
23
  {%- elif guardInline %}
24
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
25
  if ({{ name }} >= {{ bound }}) { return; }
26
  {%- else %}
27
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
28
  if ({{ name }} >= {{ bound }}) {
29
  return;
30
  }
31
  {%- endif %}
32
  {% endmacro %}
33
 
 
 
 
34
  {{ env.wgsl.resourceDeclarations }}
35
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
36
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
37
  {{ flat_index_2d("index") }}
38
  let spatial = params.height * params.width;
39
  let channel = (index / spatial) % params.channels;
build/webgpu/bench.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.BatchNormalization",
3
  "cases": [
4
  {
5
  "name": "nchw_4x64x112x112_vec4",
@@ -84,6 +83,34 @@
84
  }
85
  ]
86
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  }
88
  ]
89
  }
 
1
  {
 
2
  "cases": [
3
  {
4
  "name": "nchw_4x64x112x112_vec4",
 
83
  }
84
  ]
85
  }
86
+ },
87
+ {
88
+ "name": "batchnorm-nchw-f32-c256-plane49-unaligned",
89
+ "preset": "edge",
90
+ "attrs": { "epsilon": 0.00001 },
91
+ "inputs": {
92
+ "x": { "dtype": "float32", "shape": [128, 256, 7, 7] },
93
+ "scale": { "dtype": "float32", "shape": [256] },
94
+ "b": { "dtype": "float32", "shape": [256] },
95
+ "inputMean": { "dtype": "float32", "shape": [256] },
96
+ "inputVar": { "dtype": "float32", "shape": [256], "min": 0.5, "max": 2 }
97
+ },
98
+ "outputs": { "y": { "dtype": "float32", "shape": [128, 256, 7, 7] } },
99
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "numel(shapes.x) * 4 * 2" }] }
100
+ },
101
+ {
102
+ "name": "batchnorm-nchw-f32-c256-plane64-aligned-control",
103
+ "preset": "edge",
104
+ "attrs": { "epsilon": 0.00001 },
105
+ "inputs": {
106
+ "x": { "dtype": "float32", "shape": [98, 256, 8, 8] },
107
+ "scale": { "dtype": "float32", "shape": [256] },
108
+ "b": { "dtype": "float32", "shape": [256] },
109
+ "inputMean": { "dtype": "float32", "shape": [256] },
110
+ "inputVar": { "dtype": "float32", "shape": [256], "min": 0.5, "max": 2 }
111
+ },
112
+ "outputs": { "y": { "dtype": "float32", "shape": [98, 256, 8, 8] } },
113
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "numel(shapes.x) * 4 * 2" }] }
114
  }
115
  ]
116
  }
build/webgpu/manifest.json CHANGED
@@ -2,241 +2,155 @@
2
  "domain": "ai.onnx",
3
  "name": "BatchNormalization",
4
  "sinceVersion": 15,
5
- "description": "Applies inference-mode batch normalization: `Y = (X - input_mean) / sqrt(input_var + epsilon) * scale + B`. This package supports `training_mode=0`, rank-2-or-higher inputs, and a common float16 or float32 dtype for every tensor. ONNX training mode is intentionally not implemented because this inference-only release does not expose its required running-mean and running-variance outputs.",
6
- "inputs": [
7
- {
8
- "role": "X",
9
- "dtype": "T",
10
- "description": "Input data tensor with shape `(N, C, D1, ..., Dn)`, normalized independently per channel using the supplied estimated statistics."
11
- },
12
- { "role": "scale", "dtype": "T", "rank": 1, "description": "Per-channel scale tensor with shape `(C)`." },
13
- { "role": "B", "dtype": "T", "rank": 1, "description": "Per-channel bias tensor with shape `(C)`." },
14
- {
15
- "role": "input_mean",
16
- "dtype": "T",
17
- "rank": 1,
18
- "description": "Precomputed estimated mean tensor with shape `(C)` used for inference."
19
- },
20
- {
21
- "role": "input_var",
22
- "dtype": "T",
23
- "rank": 1,
24
- "description": "Precomputed estimated variance tensor with shape `(C)` used for inference."
25
- }
26
- ],
27
- "outputs": [
28
- {
29
- "role": "Y",
30
- "dtype": "T",
31
- "rank": "ranks.X",
32
- "description": "Batch-normalized output tensor with the same shape as `X`.",
33
- "shape": "shapes.X"
34
- }
35
- ],
36
- "attributes": { "epsilon": 0.00001, "momentum": 0.9, "training_mode": 0 },
37
- "attributeDescriptions": {
38
- "epsilon": "Small value added to the variance before taking the square root to avoid division by zero.",
39
- "momentum": "Standard ONNX running-statistics momentum. This inference-only package accepts the default `0.9`; non-default values are reserved for the unsupported training-state update.",
40
- "training_mode": "Execution mode. This inference-only package supports the default value 0; value 1 is rejected because the ONNX training outputs are not exposed."
41
- },
42
- "attributeConstraints": {
43
- "momentum": { "values": [0.9], "comparison": "float32" },
44
- "training_mode": { "values": [0] }
45
  },
 
 
 
46
  "typeConstraints": { "T": ["float32", "float16"] },
47
- "args": {
48
- "x": { "kind": "tensor", "semantic": "X", "role": "input" },
49
- "scale": { "kind": "tensor", "semantic": "scale", "role": "input" },
50
- "b": { "kind": "tensor", "semantic": "B", "role": "input" },
51
- "inputMean": { "kind": "tensor", "semantic": "input_mean", "role": "input" },
52
- "inputVar": { "kind": "tensor", "semantic": "input_var", "role": "input" },
53
- "y": { "kind": "tensor", "semantic": "Y", "role": "output" }
54
- },
55
- "tunables": { "WORKGROUP_SIZE": 256 },
56
  "derive": {
57
- "normalizationParamsOk": "ranks.scale == 1 and ranks.B == 1 and ranks.input_mean == 1 and ranks.input_var == 1 and dim(shapes.scale, 0) == dim(shapes.X, 1) and dim(shapes.B, 0) == dim(shapes.X, 1) and dim(shapes.input_mean, 0) == dim(shapes.X, 1) and dim(shapes.input_var, 0) == dim(shapes.X, 1)",
58
- "inferenceContractOk": "f16Ok(dtypes.T) and ranks.X >= 2 and ranks.Y == ranks.X and sameShape(shapes.Y, shapes.X) and normalizationParamsOk"
59
  },
60
- "bindingSets": {
61
- "ncInferenceVec4": [
62
- {
63
- "name": "x",
64
- "arg": "x",
65
- "semantic": "X",
66
- "buffer": { "type": "read-only-storage" },
67
- "elementType": "vec4<f32>"
68
- },
69
- {
70
- "name": "scale",
71
- "arg": "scale",
72
- "semantic": "scale",
73
- "buffer": { "type": "read-only-storage" },
74
- "elementType": "vec4<f32>"
75
- },
76
- {
77
- "name": "bias",
78
- "arg": "b",
79
- "semantic": "B",
80
- "buffer": { "type": "read-only-storage" },
81
- "elementType": "vec4<f32>"
82
- },
83
- {
84
- "name": "input_mean",
85
- "arg": "inputMean",
86
- "semantic": "input_mean",
87
- "buffer": { "type": "read-only-storage" },
88
- "elementType": "vec4<f32>"
89
- },
90
- {
91
- "name": "input_var",
92
- "arg": "inputVar",
93
- "semantic": "input_var",
94
- "buffer": { "type": "read-only-storage" },
95
- "elementType": "vec4<f32>"
96
- },
97
- { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "vec4<f32>" },
98
- {
99
- "name": "params",
100
- "semantic": "kernel.params",
101
- "buffer": { "type": "uniform" },
102
- "struct": {
103
- "name": "Params",
104
- "fields": [
105
- { "name": "count4", "type": "u32", "value": "numel(shapes.Y) / 4" },
106
- { "name": "channels4", "type": "u32", "value": "dim(shapes.X, 1) / 4" },
107
- { "name": "epsilon", "type": "f32", "value": "attrs.epsilon" }
108
- ]
109
- }
110
- }
111
- ],
112
- "spatialInferenceVec4": [
113
- {
114
- "name": "x",
115
- "arg": "x",
116
- "semantic": "X",
117
- "buffer": { "type": "read-only-storage" },
118
- "elementType": "vec4<f32>"
119
- },
120
- {
121
- "name": "scale",
122
- "arg": "scale",
123
- "semantic": "scale",
124
- "buffer": { "type": "read-only-storage" },
125
- "elementType": "$T"
126
- },
127
- { "name": "bias", "arg": "b", "semantic": "B", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
128
- {
129
- "name": "input_mean",
130
- "arg": "inputMean",
131
- "semantic": "input_mean",
132
- "buffer": { "type": "read-only-storage" },
133
- "elementType": "$T"
134
- },
135
- {
136
- "name": "input_var",
137
- "arg": "inputVar",
138
- "semantic": "input_var",
139
- "buffer": { "type": "read-only-storage" },
140
- "elementType": "$T"
141
- },
142
- { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "vec4<f32>" },
143
- {
144
- "name": "params",
145
- "semantic": "kernel.params",
146
- "buffer": { "type": "uniform" },
147
- "struct": {
148
- "name": "Params",
149
- "fields": [
150
- { "name": "count4", "type": "u32", "value": "numel(shapes.Y) / 4" },
151
- { "name": "spatial4", "type": "u32", "value": "inner(shapes.X, 1) / 4" },
152
- { "name": "channels", "type": "u32", "value": "dim(shapes.X, 1)" },
153
- { "name": "epsilon", "type": "f32", "value": "attrs.epsilon" }
154
- ]
155
- }
156
- }
157
- ],
158
- "inferenceScalar": [
159
- { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
160
- {
161
- "name": "scale",
162
- "arg": "scale",
163
- "semantic": "scale",
164
- "buffer": { "type": "read-only-storage" },
165
- "elementType": "$T"
166
- },
167
- { "name": "bias", "arg": "b", "semantic": "B", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
168
- {
169
- "name": "input_mean",
170
- "arg": "inputMean",
171
- "semantic": "input_mean",
172
- "buffer": { "type": "read-only-storage" },
173
- "elementType": "$T"
174
- },
175
- {
176
- "name": "input_var",
177
- "arg": "inputVar",
178
- "semantic": "input_var",
179
- "buffer": { "type": "read-only-storage" },
180
- "elementType": "$T"
181
- },
182
- { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$T" },
183
- {
184
- "name": "params",
185
- "semantic": "kernel.params",
186
- "buffer": { "type": "uniform" },
187
- "struct": {
188
- "name": "Params",
189
- "fields": [
190
- { "name": "channels", "type": "u32", "value": "dim(shapes.X, 1)" },
191
- { "name": "height", "type": "u32", "value": "1 if ranks.X == 2 else dim(shapes.X, 2)" },
192
- { "name": "width", "type": "u32", "value": "1 if ranks.X == 2 else inner(shapes.X, 2)" },
193
- { "name": "epsilon", "type": "f32", "value": "attrs.epsilon" },
194
- { "name": "count", "type": "u32", "value": "numel(shapes.Y)" }
195
- ]
196
- }
197
- }
198
- ]
199
  },
200
  "variants": [
201
  {
202
  "id": "inference_scalar",
203
- "when": ["inferenceContractOk"],
204
- "constants": { "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"" },
205
  "passes": [
206
  {
207
  "id": "main",
208
  "name": "BatchNormalization.InferenceScalar",
209
  "shader": "batch-normalization-nchw.wgsl.jinja",
210
- "bindings": "inferenceScalar",
211
- "dispatch": { "threads": "numel(shapes.Y)", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  }
213
  ]
214
  },
215
  {
216
  "id": "nc_inference_vec4",
217
  "priority": 110,
218
- "when": ["inferenceContractOk", "dtypes.T == \"f32\"", "ranks.X == 2", "dim(shapes.X, 1) % 4 == 0"],
219
  "passes": [
220
  {
221
  "id": "main",
222
  "name": "BatchNormalization.NcInferenceVec4",
223
  "shader": "batch-normalization-nc-vec4.wgsl.jinja",
224
- "bindings": "ncInferenceVec4",
225
- "dispatch": { "threads": "numel(shapes.Y) / 4", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  }
227
  ]
228
  },
229
  {
230
  "id": "nchw_inference_vec4",
231
  "priority": 100,
232
- "when": ["inferenceContractOk", "dtypes.T == \"f32\"", "ranks.X >= 3", "inner(shapes.X, 1) % 4 == 0"],
233
  "passes": [
234
  {
235
  "id": "main",
236
  "name": "BatchNormalization.InferenceVec4",
237
  "shader": "batch-normalization-nchw-vec4.wgsl.jinja",
238
- "bindings": "spatialInferenceVec4",
239
- "dispatch": { "threads": "numel(shapes.Y) / 4", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
 
 
240
  }
241
  ]
242
  }
 
2
  "domain": "ai.onnx",
3
  "name": "BatchNormalization",
4
  "sinceVersion": 15,
5
+ "inputs": {
6
+ "x": { "onnx": "X", "dtype": "T" },
7
+ "scale": { "dtype": "T", "rank": 1 },
8
+ "b": { "onnx": "B", "dtype": "T", "rank": 1 },
9
+ "inputMean": { "onnx": "input_mean", "dtype": "T", "rank": 1 },
10
+ "inputVar": { "onnx": "input_var", "dtype": "T", "rank": 1 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  },
12
+ "outputs": { "y": { "onnx": "Y", "dtype": "T", "rank": "ranks.x", "shape": "shapes.x" } },
13
+ "attributes": { "epsilon": { "default": 0.00001 }, "momentum": { "default": 0.9 }, "training_mode": { "default": 0 } },
14
+ "attributeConstraints": { "momentum": { "values": [0.9] }, "training_mode": { "values": [0] } },
15
  "typeConstraints": { "T": ["float32", "float16"] },
16
+ "tunables": { "WORKGROUP_SIZE": { "default": 256 } },
 
 
 
 
 
 
 
 
17
  "derive": {
18
+ "normalizationParamsOk": "ranks.scale == 1 and ranks.b == 1 and ranks.inputMean == 1 and ranks.inputVar == 1 and dim(shapes.scale, 0) == dim(shapes.x, 1) and dim(shapes.b, 0) == dim(shapes.x, 1) and dim(shapes.inputMean, 0) == dim(shapes.x, 1) and dim(shapes.inputVar, 0) == dim(shapes.x, 1)",
19
+ "inferenceContractOk": "f16Ok(dtypes.T) and ranks.x >= 2 and ranks.y == ranks.x and sameShape(shapes.y, shapes.x) and normalizationParamsOk"
20
  },
21
+ "when": ["inferenceContractOk"],
22
+ "bindings": {
23
+ "scale": { "buffer": "read-only-storage", "elementType": "$T" },
24
+ "bias": { "arg": "b", "buffer": "read-only-storage", "elementType": "$T" },
25
+ "input_mean": { "arg": "inputMean", "buffer": "read-only-storage", "elementType": "$T" },
26
+ "input_var": { "arg": "inputVar", "buffer": "read-only-storage", "elementType": "$T" },
27
+ "x_2": { "name": "x", "buffer": "read-only-storage", "elementType": "vec4<f32>" },
28
+ "scale_2": { "name": "scale", "buffer": "read-only-storage", "elementType": "vec4<f32>" },
29
+ "bias_2": { "arg": "b", "name": "bias", "buffer": "read-only-storage", "elementType": "vec4<f32>" },
30
+ "input_mean_2": {
31
+ "arg": "inputMean",
32
+ "name": "input_mean",
33
+ "buffer": "read-only-storage",
34
+ "elementType": "vec4<f32>"
35
+ },
36
+ "input_var_2": { "arg": "inputVar", "name": "input_var", "buffer": "read-only-storage", "elementType": "vec4<f32>" },
37
+ "y_2": { "name": "y", "buffer": "storage", "elementType": "vec4<f32>" },
38
+ "params_2": {
39
+ "name": "params",
40
+ "buffer": "uniform",
41
+ "struct": [
42
+ { "name": "count4", "type": "u32", "value": "numel(shapes.y) / 4" },
43
+ { "name": "channels4", "type": "u32", "value": "dim(shapes.x, 1) / 4" },
44
+ { "name": "epsilon", "type": "f32", "value": "attrs.epsilon" }
45
+ ]
46
+ },
47
+ "params_3": {
48
+ "name": "params",
49
+ "buffer": "uniform",
50
+ "struct": [
51
+ { "name": "count4", "type": "u32", "value": "numel(shapes.y) / 4" },
52
+ { "name": "spatial", "type": "u32", "value": "inner(shapes.x, 1)" },
53
+ { "name": "channels", "type": "u32", "value": "dim(shapes.x, 1)" },
54
+ { "name": "epsilon", "type": "f32", "value": "attrs.epsilon" }
55
+ ]
56
+ },
57
+ "params_4": {
58
+ "name": "params",
59
+ "buffer": "uniform",
60
+ "struct": [
61
+ { "name": "count4", "type": "u32", "value": "numel(shapes.y) / 4" },
62
+ { "name": "spatial4", "type": "u32", "value": "inner(shapes.x, 1) / 4" },
63
+ { "name": "channels", "type": "u32", "value": "dim(shapes.x, 1)" },
64
+ { "name": "epsilon", "type": "f32", "value": "attrs.epsilon" }
65
+ ]
66
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  },
68
  "variants": [
69
  {
70
  "id": "inference_scalar",
71
+ "derive": { "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"" },
 
72
  "passes": [
73
  {
74
  "id": "main",
75
  "name": "BatchNormalization.InferenceScalar",
76
  "shader": "batch-normalization-nchw.wgsl.jinja",
77
+ "bindings": [
78
+ "x",
79
+ "scale",
80
+ "bias",
81
+ "input_mean",
82
+ "input_var",
83
+ "y",
84
+ {
85
+ "name": "params",
86
+ "struct": [
87
+ { "name": "channels", "type": "u32", "value": "dim(shapes.x, 1)" },
88
+ { "name": "height", "type": "u32", "value": "1 if ranks.x == 2 else dim(shapes.x, 2)" },
89
+ { "name": "width", "type": "u32", "value": "1 if ranks.x == 2 else inner(shapes.x, 2)" },
90
+ { "name": "epsilon", "type": "f32", "value": "attrs.epsilon" },
91
+ { "name": "count", "type": "u32", "value": "numel(shapes.y)" }
92
+ ]
93
+ }
94
+ ],
95
+ "dispatch": {
96
+ "x": "min(ceilDiv((numel(shapes.y)), (tunables.WORKGROUP_SIZE)), 65535)",
97
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (tunables.WORKGROUP_SIZE)), 65535)",
98
+ "z": 1
99
+ }
100
  }
101
  ]
102
  },
103
  {
104
  "id": "nc_inference_vec4",
105
  "priority": 110,
106
+ "when": ["dtypes.T == \"f32\"", "ranks.x == 2", "dim(shapes.x, 1) % 4 == 0"],
107
  "passes": [
108
  {
109
  "id": "main",
110
  "name": "BatchNormalization.NcInferenceVec4",
111
  "shader": "batch-normalization-nc-vec4.wgsl.jinja",
112
+ "bindings": ["x_2", "scale_2", "bias_2", "input_mean_2", "input_var_2", "y_2", "params_2"],
113
+ "dispatch": {
114
+ "x": "min(ceilDiv((numel(shapes.y) / 4), (tunables.WORKGROUP_SIZE)), 65535)",
115
+ "y": "ceilDiv(ceilDiv((numel(shapes.y) / 4), (tunables.WORKGROUP_SIZE)), 65535)",
116
+ "z": 1
117
+ }
118
+ }
119
+ ]
120
+ },
121
+ {
122
+ "id": "nchw_inference_flat_vec4",
123
+ "priority": 90,
124
+ "when": ["dtypes.T == \"f32\"", "ranks.x >= 3", "inner(shapes.x, 1) % 4 != 0 and numel(shapes.x) % 4 == 0"],
125
+ "passes": [
126
+ {
127
+ "id": "main",
128
+ "name": "BatchNormalization.InferenceFlatVec4",
129
+ "shader": "batch-normalization-nchw-flat-vec4.wgsl.jinja",
130
+ "bindings": ["x_2", "scale", "bias", "input_mean", "input_var", "y_2", "params_3"],
131
+ "dispatch": {
132
+ "x": "min(ceilDiv((numel(shapes.y) / 4), (tunables.WORKGROUP_SIZE)), 65535)",
133
+ "y": "ceilDiv(ceilDiv((numel(shapes.y) / 4), (tunables.WORKGROUP_SIZE)), 65535)",
134
+ "z": 1
135
+ }
136
  }
137
  ]
138
  },
139
  {
140
  "id": "nchw_inference_vec4",
141
  "priority": 100,
142
+ "when": ["dtypes.T == \"f32\"", "ranks.x >= 3", "inner(shapes.x, 1) % 4 == 0"],
143
  "passes": [
144
  {
145
  "id": "main",
146
  "name": "BatchNormalization.InferenceVec4",
147
  "shader": "batch-normalization-nchw-vec4.wgsl.jinja",
148
+ "bindings": ["x_2", "scale", "bias", "input_mean", "input_var", "y_2", "params_4"],
149
+ "dispatch": {
150
+ "x": "min(ceilDiv((numel(shapes.y) / 4), (tunables.WORKGROUP_SIZE)), 65535)",
151
+ "y": "ceilDiv(ceilDiv((numel(shapes.y) / 4), (tunables.WORKGROUP_SIZE)), 65535)",
152
+ "z": 1
153
+ }
154
  }
155
  ]
156
  }
build/webgpu/metadata.json CHANGED
@@ -1,20 +1,29 @@
1
  {
2
  "name": "ai.onnx.BatchNormalization",
3
- "id": "_ai_onnx_batchnormalization_webgpu_36b417d",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "batch-normalization-nc-vec4.wgsl.jinja": "Lrc9lNARoZohL7Uv2G+JcdoTcEs6dqsaiP1+NPW1rsk=",
11
- "batch-normalization-nchw-vec4.wgsl.jinja": "Rsd2siANpuQMlk5/ueUS07ZKscDp/jF8EQKsOyQAXN8=",
12
- "batch-normalization-nchw.wgsl.jinja": "TfDSthFwA8AdlFykWHTOroAgZeC/TS7i8Wt7OKHyJbc=",
13
- "bench.json": "GitYvHBWRsJ0jrz/w4vwR6ryXBDINuJ0gS+Fcrml9Gc=",
14
- "manifest.json": "YZlooDSV8vRLHoVdW2ELx4Bhy0CGOB8w5jIIBn7j5ig=",
15
- "test.json": "uHgc/mVjiAXJHTJLWPa9ZswwhfBjhvH0QmiRAzqFncE="
 
16
  }
17
  },
18
- "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
19
- "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.BatchNormalization" }
 
 
 
 
 
 
 
 
20
  }
 
1
  {
2
  "name": "ai.onnx.BatchNormalization",
3
+ "id": "_ai_onnx_batchnormalization_webgpu_4d2bfed",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "batch-normalization-nc-vec4.wgsl.jinja": "yZ3Hf6SM+D6EW4GvvG5Q0N5tuxQlys5l78zAfqwH0IE=",
11
+ "batch-normalization-nchw-flat-vec4.wgsl.jinja": "ZU77NvidV7PpSQC2Fhc/pDgtSlG6EypswF7MWBi+qzw=",
12
+ "batch-normalization-nchw-vec4.wgsl.jinja": "L4z18z+y+0qESVx8FkmHJhTORNeEAOPz1KoV/e7WkCE=",
13
+ "batch-normalization-nchw.wgsl.jinja": "bv0TT2Ry2691M8bUjXYC1tlsrPZ+vPsGgdcUKSj59pg=",
14
+ "bench.json": "yPXcRY22XiVLwm5V3NsowDKaM6BKq2z0Batz3+SR+cI=",
15
+ "manifest.json": "PRmol3fOKZDuk/f21hi3uxRSbxsOhP7RQd+KA2zkW0E=",
16
+ "test.json": "AAN+Vhz8gU2l1tY8tLPNyRAYMpP+8dGalRvLSrzR/5M="
17
  }
18
  },
19
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
20
+ "webgpu": {
21
+ "manifestSpec": "2.0",
22
+ "variants": {
23
+ "inference_scalar": ["batch-normalization-nchw.wgsl.jinja"],
24
+ "nc_inference_vec4": ["batch-normalization-nc-vec4.wgsl.jinja"],
25
+ "nchw_inference_flat_vec4": ["batch-normalization-nchw-flat-vec4.wgsl.jinja"],
26
+ "nchw_inference_vec4": ["batch-normalization-nchw-vec4.wgsl.jinja"]
27
+ }
28
+ }
29
  }
build/webgpu/test.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.BatchNormalization",
3
  "fixtureArrays": {
4
  "ort_positive_single_channel_7x7_exact_default_epsilon_input_x": [0.329876, -0.287158, -0.411425, 0.473621, 0.18156, -0.170596, -0.329516, -0.170733, -0.121664, 0.4372, -0.485668, 0.218049, -0.360263, 0.107016, 0.45358, 0.325056, 0.15995, 0.098852, -0.283453, -0.373051, 0.257542, 0.0614853, -0.0592363, 0.434488, -0.0179583, 0.398374, -0.451602, -0.132009, -0.174468, -0.0247169, 0.418897, -0.47159, -0.131925, 0.470943, 0.118357, 0.155664, 0.370062, -0.279229, 0.240311, -0.451034, 0.249178, -0.294496, 0.13683, -0.0806475, -0.309849, -0.450604, -0.28048, -0.420197, -0.433369]
5
  },
@@ -83,7 +82,7 @@
83
  "provenance": {
84
  "source": "onnxruntime/test/providers/cpu/nn/batch_norm_op_test.cc",
85
  "test": "BatchNormTest.PositiveTestCase",
86
- "notes": "Vec4 inference companion: subnormal scale should not collapse an otherwise ordinary normalized channel to zero."
87
  },
88
  "attrs": { "epsilon": 0.00001 },
89
  "inputs": {
@@ -190,7 +189,7 @@
190
  "provenance": {
191
  "source": "onnxruntime/test/providers/cpu/nn/batch_norm_op_test.cc",
192
  "test": "BatchNormTest.PositiveTestCase",
193
- "notes": "Project regression adapted from ORT BatchNormalization positive cases to stress zero variance with a large epsilon."
194
  },
195
  "attrs": { "epsilon": 0.25 },
196
  "inputs": {
@@ -450,7 +449,7 @@
450
  "provenance": {
451
  "source": "onnxruntime/test/providers/cpu/nn/batch_norm_op_test.cc",
452
  "test": "BatchNormTest.BatchNorm2d_fp16",
453
- "notes": "Compact rank-4 fp16 inference projection of ORT's BatchNorm2d_fp16 coverage."
454
  },
455
  "attrs": { "epsilon": 0.00001 },
456
  "inputs": {
@@ -615,6 +614,69 @@
615
  "inputVar": { "dtype": "float32", "shape": [2], "data": { "kind": "values", "values": [0.5, 2.0] } }
616
  },
617
  "outputs": { "y": { "dtype": "float32", "shape": [1, 2, 2, 1, 2, 1, 2, 2], "tolerance": 0.000001 } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
618
  }
619
  ]
620
  }
 
1
  {
 
2
  "fixtureArrays": {
3
  "ort_positive_single_channel_7x7_exact_default_epsilon_input_x": [0.329876, -0.287158, -0.411425, 0.473621, 0.18156, -0.170596, -0.329516, -0.170733, -0.121664, 0.4372, -0.485668, 0.218049, -0.360263, 0.107016, 0.45358, 0.325056, 0.15995, 0.098852, -0.283453, -0.373051, 0.257542, 0.0614853, -0.0592363, 0.434488, -0.0179583, 0.398374, -0.451602, -0.132009, -0.174468, -0.0247169, 0.418897, -0.47159, -0.131925, 0.470943, 0.118357, 0.155664, 0.370062, -0.279229, 0.240311, -0.451034, 0.249178, -0.294496, 0.13683, -0.0806475, -0.309849, -0.450604, -0.28048, -0.420197, -0.433369]
4
  },
 
82
  "provenance": {
83
  "source": "onnxruntime/test/providers/cpu/nn/batch_norm_op_test.cc",
84
  "test": "BatchNormTest.PositiveTestCase",
85
+ "notes": "On the vec4 inference path, a subnormal scale must not collapse an otherwise ordinary normalized channel to zero."
86
  },
87
  "attrs": { "epsilon": 0.00001 },
88
  "inputs": {
 
189
  "provenance": {
190
  "source": "onnxruntime/test/providers/cpu/nn/batch_norm_op_test.cc",
191
  "test": "BatchNormTest.PositiveTestCase",
192
+ "notes": "Zero variance with a large epsilon must still produce a finite, correctly scaled output."
193
  },
194
  "attrs": { "epsilon": 0.25 },
195
  "inputs": {
 
449
  "provenance": {
450
  "source": "onnxruntime/test/providers/cpu/nn/batch_norm_op_test.cc",
451
  "test": "BatchNormTest.BatchNorm2d_fp16",
452
+ "notes": "A compact rank-4 float16 tensor exercises two-dimensional inference normalization."
453
  },
454
  "attrs": { "epsilon": 0.00001 },
455
  "inputs": {
 
614
  "inputVar": { "dtype": "float32", "shape": [2], "data": { "kind": "values", "values": [0.5, 2.0] } }
615
  },
616
  "outputs": { "y": { "dtype": "float32", "shape": [1, 2, 2, 1, 2, 1, 2, 2], "tolerance": 0.000001 } }
617
+ },
618
+ {
619
+ "name": "nc_inference_rank2_vec4_large_mean_small_residual",
620
+ "provenance": {
621
+ "notes": "Inputs sit within 0.1..0.4 of channel means of magnitude 125..1000 with variance 1e-6 (alpha = 1000, epsilon 0). The expanded form x*alpha + (bias - mean*alpha) rounds two ~1e6 products to a 1/16 ulp before cancelling and loses ~0.04 of a ~100 result; (x - mean)*alpha + bias keeps the residual exact."
622
+ },
623
+ "attrs": { "epsilon": 0 },
624
+ "inputs": {
625
+ "x": {
626
+ "dtype": "float32",
627
+ "shape": [2, 8],
628
+ "data": {
629
+ "kind": "values",
630
+ "values": [1000.4, -1000.1999999999999, 500.9, -500.5, 250.4, -249.79999999999998, 126.30000000000001, -125.5, 1000.1999999999999, -1000.4, 500.5, -500.9, 249.79999999999998, -250.4, 125.5, -126.30000000000001]
631
+ }
632
+ },
633
+ "scale": { "dtype": "float32", "shape": [8], "data": { "kind": "constant", "value": 1.0 } },
634
+ "b": {
635
+ "dtype": "float32",
636
+ "shape": [8],
637
+ "data": { "kind": "values", "values": [0.5, -0.5, 0.25, -0.25, 0.125, -0.125, 0.0625, -0.0625] }
638
+ },
639
+ "inputMean": {
640
+ "dtype": "float32",
641
+ "shape": [8],
642
+ "data": { "kind": "values", "values": [1000.3, -1000.3, 500.7, -500.7, 250.1, -250.1, 125.9, -125.9] }
643
+ },
644
+ "inputVar": { "dtype": "float32", "shape": [8], "data": { "kind": "constant", "value": 0.000001 } }
645
+ },
646
+ "outputs": { "y": { "dtype": "float32", "shape": [2, 8], "tolerance": 0.002 } }
647
+ },
648
+ {
649
+ "name": "nchw_inference_plane9_flat_vec4",
650
+ "provenance": {
651
+ "source": "ONNX BatchNormalization-15 specification",
652
+ "notes": "A 3x3 channel plane is not a multiple of four, so four consecutive elements can belong to two channels and each channel's own scale, bias, mean and variance must still reach its own elements. The parameters differ per channel so a group taken from the wrong channel changes the result."
653
+ },
654
+ "attrs": { "epsilon": 0.00001 },
655
+ "inputs": {
656
+ "x": {
657
+ "dtype": "float32",
658
+ "shape": [2, 2, 3, 3],
659
+ "data": {
660
+ "kind": "values",
661
+ "values": [1.0, 1.145098, 1.126169, 0.970775, 0.726453, 0.451691, 0.205283, 0.035887, -0.026454, 0.024608, 0.170881, 0.373162, 0.578522, 0.730102, 0.77774, 0.687602, 0.449162, 0.078279, -0.384177, -0.878017, -1.332846, -1.679122, -1.859379, -1.837756, -1.606254, -1.186673, -0.627873, 0.001237, 0.621873, 1.15709, 1.543172, 1.738877, 1.731074, 1.535902, 1.195315, 0.76962]
662
+ }
663
+ },
664
+ "scale": { "dtype": "float32", "shape": [2], "data": { "kind": "values", "values": [1.0, 0.5] } },
665
+ "b": { "dtype": "float32", "shape": [2], "data": { "kind": "values", "values": [0.0, 1.0] } },
666
+ "inputMean": { "dtype": "float32", "shape": [2], "data": { "kind": "values", "values": [0.25, -0.5] } },
667
+ "inputVar": { "dtype": "float32", "shape": [2], "data": { "kind": "values", "values": [4.0, 0.25] } }
668
+ },
669
+ "outputs": {
670
+ "y": {
671
+ "dtype": "float32",
672
+ "shape": [2, 2, 3, 3],
673
+ "data": {
674
+ "kind": "values",
675
+ "values": [0.375, 0.447548, 0.438084, 0.360387, 0.238226, 0.100845, -0.022358, -0.107056, -0.138227, 1.524598, 1.670868, 1.873145, 2.0785, 2.230077, 2.277714, 2.187578, 1.949143, 1.578267, -0.317088, -0.564008, -0.791422, -0.96456, -1.054688, -1.043877, -0.928126, -0.718336, -0.438936, 1.501227, 2.121851, 2.657057, 3.043131, 3.238832, 3.231029, 3.035861, 2.695281, 2.269595]
676
+ },
677
+ "tolerance": 0.00001
678
+ }
679
+ }
680
  }
681
  ]
682
  }