Xenova HF Staff commited on
Commit
4dd9fa6
·
verified ·
1 Parent(s): 9b3a220

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,16 +18,16 @@ See the [ONNX `Mod` spec](https://onnx.ai/onnx/operators/onnx__Mod.html) for the
18
 
19
  ## Inputs
20
 
21
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
- | `A` | `a` | `T` | — | — | Dividend tensor. | required |
24
- | `B` | `b` | `T` | — | — | Divisor tensor. | required |
25
 
26
  ## Outputs
27
 
28
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
29
  | --- | --- | --- | --- | --- | --- | --- |
30
- | `C` | `c` | `T` | derived | broadcast result of `A` and `B` | Remainder tensor; same shape as the broadcast result of A and B. | required |
31
 
32
  ## Attributes
33
 
@@ -45,7 +45,7 @@ Default values (overridable per request):
45
 
46
  ## Files
47
 
48
- - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
49
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
50
  - [`test.json`](build/webgpu/test.json) — correctness cases
51
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
@@ -54,10 +54,14 @@ Default values (overridable per request):
54
 
55
  ## Use with `@huggingface/kernels`
56
 
57
- The loader derives every required output's shape and logical dtype from the manifest contract and this call.
58
- It then allocates the result tensors automatically.
 
 
 
59
 
60
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
 
61
 
62
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
63
 
 
18
 
19
  ## Inputs
20
 
21
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
+ | `a` | `A` | `T` | — | — | Dividend tensor. | required |
24
+ | `b` | `B` | `T` | — | — | Divisor tensor. | required |
25
 
26
  ## Outputs
27
 
28
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
29
  | --- | --- | --- | --- | --- | --- | --- |
30
+ | `c` | `C` | `T` | derived | broadcast result of `a` and `b` | Remainder tensor; same shape as the broadcast result of A and B. | required |
31
 
32
  ## Attributes
33
 
 
45
 
46
  ## Files
47
 
48
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
49
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
50
  - [`test.json`](build/webgpu/test.json) — correctness cases
51
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
 
54
 
55
  ## Use with `@huggingface/kernels`
56
 
57
+ ```sh
58
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.2
59
+ ```
60
+
61
+ Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
62
 
63
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
64
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
65
 
66
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
67
 
build/webgpu/bench.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.Mod",
3
  "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256] },
4
  "cases": [
5
  {
@@ -58,7 +57,7 @@
58
  "preset": "smoke",
59
  "vars": { "dtype": "int32", "count": 1048576 },
60
  "inputs": {
61
- "a": { "dtype": "int32", "shape": [1048576], "dist": "normal", "seed": 713, "scale": 1000 },
62
  "b": { "dtype": "int32", "shape": [1048576], "data": { "kind": "constant", "value": 7 } }
63
  },
64
  "outputs": { "c": { "dtype": "int32", "shape": [1048576] } },
@@ -69,7 +68,7 @@
69
  "preset": "stress",
70
  "attrs": { "fmod": 0 },
71
  "inputs": {
72
- "a": { "dtype": "int32", "shape": [2049, 2051], "dist": "normal", "seed": 801, "scale": 1000 },
73
  "b": { "dtype": "int32", "shape": [2051], "data": { "kind": "constant", "value": 7 } }
74
  },
75
  "outputs": { "c": { "dtype": "int32", "shape": [2049, 2051] } },
 
1
  {
 
2
  "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256] },
3
  "cases": [
4
  {
 
57
  "preset": "smoke",
58
  "vars": { "dtype": "int32", "count": 1048576 },
59
  "inputs": {
60
+ "a": { "dtype": "int32", "shape": [1048576], "dist": "uniform", "seed": 713, "min": -1000, "max": 1001 },
61
  "b": { "dtype": "int32", "shape": [1048576], "data": { "kind": "constant", "value": 7 } }
62
  },
63
  "outputs": { "c": { "dtype": "int32", "shape": [1048576] } },
 
68
  "preset": "stress",
69
  "attrs": { "fmod": 0 },
70
  "inputs": {
71
+ "a": { "dtype": "int32", "shape": [2049, 2051], "dist": "uniform", "seed": 801, "min": -1000, "max": 1001 },
72
  "b": { "dtype": "int32", "shape": [2051], "data": { "kind": "constant", "value": 7 } }
73
  },
74
  "outputs": { "c": { "dtype": "int32", "shape": [2049, 2051] } },
build/webgpu/manifest.json CHANGED
@@ -2,222 +2,114 @@
2
  "domain": "ai.onnx",
3
  "name": "Mod",
4
  "sinceVersion": 13,
5
- "description": "Performs elementwise binary modulo on tensors `A` and `B` with multidirectional broadcasting. When `fmod` is `0` (default), applies Python-style `%` with the sign of the divisor; when `fmod` is `1`, applies C-style `fmod` with the sign of the dividend.",
6
- "inputs": [
7
- { "role": "A", "dtype": "T", "description": "Dividend tensor." },
8
- { "role": "B", "dtype": "T", "description": "Divisor tensor." }
9
- ],
10
- "outputs": [
11
- {
12
- "role": "C",
13
- "dtype": "T",
14
- "rank": "max(ranks.A, ranks.B)",
15
- "description": "Remainder tensor; same shape as the broadcast result of A and B.",
16
- "shape": "broadcastShape(shapes.A, shapes.B)"
17
- }
18
- ],
19
- "attributes": { "fmod": 0 },
20
- "attributeDescriptions": {
21
- "fmod": "Controls the modulo mode: `0` (default) uses Python-style integer mod (sign of divisor); `1` uses C-style `fmod` (sign of dividend, floating-point types only)."
22
  },
 
23
  "attributeConstraints": { "fmod": { "values": [0, 1] } },
24
  "typeConstraints": { "T": ["float32", "float16", "int32", "uint32", "int16", "int8", "uint8"] },
25
- "args": {
26
- "a": { "kind": "tensor", "semantic": "A", "role": "input" },
27
- "b": { "kind": "tensor", "semantic": "B", "role": "input" },
28
- "c": { "kind": "tensor", "semantic": "C", "role": "output" }
29
- },
30
- "tunables": { "WORKGROUP_SIZE": 256 },
31
- "derive": { "floatFmodContract": "attrs.fmod != 0 or not (dtypes.T == \"f32\" or dtypes.T == \"f16\")" },
32
- "constants": {
33
  "scalar": "dtypes.T",
34
- "usesF16": "dtypes.T == \"f16\"",
35
  "isU32": "dtypes.T == \"u32\"",
36
  "isI32": "dtypes.T == \"i32\"",
37
  "fmod": "attrs.fmod != 0"
38
  },
 
 
 
 
 
 
39
  "variants": [
40
  {
41
  "id": "last_axis_broadcast_vec4",
42
  "priority": 25,
43
- "when": ["ranks.C >= 1", "sameShape(shapes.A, shapes.C)", "ranks.B == 1", "dim(shapes.B, 0) == dim(shapes.C, ranks.C - 1)", "numel(shapes.C) > 0", "numel(shapes.C) % 4 == 0", "f16Ok(dtypes.T)", "floatFmodContract"],
44
- "constants": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"" },
45
  "passes": [
46
  {
47
  "id": "main",
48
  "name": "Mod.lastAxisBroadcastVec4",
49
- "source": { "shader": "mod-vec4.wgsl.jinja", "inputs": { "perAxisB": true, "axisDim": "dim(shapes.B, 0)" } },
50
- "bindings": [
51
- {
52
- "name": "a",
53
- "arg": "a",
54
- "semantic": "A",
55
- "buffer": { "type": "read-only-storage" },
56
- "elementType": "$vectorScalar"
57
- },
58
- {
59
- "name": "b",
60
- "arg": "b",
61
- "semantic": "B",
62
- "buffer": { "type": "read-only-storage" },
63
- "elementType": "$scalar"
64
- },
65
- {
66
- "name": "c",
67
- "arg": "c",
68
- "semantic": "C",
69
- "buffer": { "type": "storage" },
70
- "elementType": "$vectorScalar"
71
- },
72
- {
73
- "name": "params",
74
- "semantic": "kernel.params",
75
- "buffer": { "type": "uniform" },
76
- "struct": {
77
- "name": "Params",
78
- "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.C) / 4" }]
79
- }
80
- }
81
- ],
82
- "dispatch": { "threads": "numel(shapes.C) / 4", "workgroupSize": "tunables.WORKGROUP_SIZE" }
83
  }
84
  ]
85
  },
86
  {
87
  "id": "scalar_b_vec4",
88
  "priority": 30,
89
- "when": ["sameShape(shapes.A, shapes.C)", "numel(shapes.B) == 1", "numel(shapes.C) > 0", "numel(shapes.C) % 4 == 0", "f16Ok(dtypes.T)", "floatFmodContract"],
90
- "constants": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"" },
91
  "passes": [
92
  {
93
  "id": "main",
94
  "name": "Mod.scalarBVec4",
95
- "source": { "shader": "mod-vec4.wgsl.jinja", "inputs": { "scalarB": true } },
96
- "bindings": [
97
- {
98
- "name": "a",
99
- "arg": "a",
100
- "semantic": "A",
101
- "buffer": { "type": "read-only-storage" },
102
- "elementType": "$vectorScalar"
103
- },
104
- {
105
- "name": "b",
106
- "arg": "b",
107
- "semantic": "B",
108
- "buffer": { "type": "read-only-storage" },
109
- "elementType": "$scalar",
110
- "length": 1
111
- },
112
- {
113
- "name": "c",
114
- "arg": "c",
115
- "semantic": "C",
116
- "buffer": { "type": "storage" },
117
- "elementType": "$vectorScalar"
118
- },
119
- {
120
- "name": "params",
121
- "semantic": "kernel.params",
122
- "buffer": { "type": "uniform" },
123
- "struct": {
124
- "name": "Params",
125
- "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.C) / 4" }]
126
- }
127
- }
128
- ],
129
- "dispatch": { "threads": "numel(shapes.C) / 4", "workgroupSize": "tunables.WORKGROUP_SIZE" }
130
  }
131
  ]
132
  },
133
  {
134
  "id": "same_shape_vec4",
135
  "priority": 26,
136
- "when": ["sameShape(shapes.A, shapes.C)", "sameShape(shapes.B, shapes.C)", "numel(shapes.C) > 0", "numel(shapes.C) % 4 == 0", "f16Ok(dtypes.T)", "floatFmodContract"],
137
- "constants": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"" },
138
  "passes": [
139
  {
140
  "id": "main",
141
  "name": "Mod.vec4",
142
- "source": { "shader": "mod-vec4.wgsl.jinja", "inputs": {} },
143
- "bindings": [
144
- {
145
- "name": "a",
146
- "arg": "a",
147
- "semantic": "A",
148
- "buffer": { "type": "read-only-storage" },
149
- "elementType": "$vectorScalar"
150
- },
151
- {
152
- "name": "b",
153
- "arg": "b",
154
- "semantic": "B",
155
- "buffer": { "type": "read-only-storage" },
156
- "elementType": "$vectorScalar"
157
- },
158
- {
159
- "name": "c",
160
- "arg": "c",
161
- "semantic": "C",
162
- "buffer": { "type": "storage" },
163
- "elementType": "$vectorScalar"
164
- },
165
- {
166
- "name": "params",
167
- "semantic": "kernel.params",
168
- "buffer": { "type": "uniform" },
169
- "struct": {
170
- "name": "Params",
171
- "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.C) / 4" }]
172
- }
173
- }
174
- ],
175
- "dispatch": { "threads": "numel(shapes.C) / 4", "workgroupSize": "tunables.WORKGROUP_SIZE" }
176
  }
177
  ]
178
  },
179
  {
180
  "id": "broadcast",
181
- "when": ["ranks.A >= 0", "ranks.B >= 0", "ranks.C >= 0", "ranks.A <= ranks.C", "ranks.B <= ranks.C", "f16Ok(dtypes.T)", "floatFmodContract"],
182
  "passes": [
183
  {
184
  "id": "main",
185
  "name": "Mod",
186
- "source": {
187
- "shader": "mod.wgsl.jinja",
188
- "inputs": {
189
- "aShape": "shapes.A",
190
- "bShape": "shapes.B",
191
- "cShape": "shapes.C",
192
- "aRank": "ranks.A",
193
- "bRank": "ranks.B",
194
- "cRank": "ranks.C"
195
- }
196
  },
197
  "bindings": [
198
- {
199
- "name": "a",
200
- "arg": "a",
201
- "semantic": "A",
202
- "buffer": { "type": "read-only-storage" },
203
- "elementType": "$scalar"
204
- },
205
- {
206
- "name": "b",
207
- "arg": "b",
208
- "semantic": "B",
209
- "buffer": { "type": "read-only-storage" },
210
- "elementType": "$scalar"
211
- },
212
- { "name": "c", "arg": "c", "semantic": "C", "buffer": { "type": "storage" }, "elementType": "$scalar" },
213
- {
214
- "name": "params",
215
- "semantic": "kernel.params",
216
- "buffer": { "type": "uniform" },
217
- "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.C)" }] }
218
- }
219
  ],
220
- "dispatch": { "threads": "numel(shapes.C)", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
 
 
221
  }
222
  ]
223
  }
 
2
  "domain": "ai.onnx",
3
  "name": "Mod",
4
  "sinceVersion": 13,
5
+ "inputs": { "a": { "onnx": "A", "dtype": "T" }, "b": { "onnx": "B", "dtype": "T" } },
6
+ "outputs": {
7
+ "c": { "onnx": "C", "dtype": "T", "rank": "max(ranks.a, ranks.b)", "shape": "broadcastShape(shapes.a, shapes.b)" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  },
9
+ "attributes": { "fmod": { "default": 0 } },
10
  "attributeConstraints": { "fmod": { "values": [0, 1] } },
11
  "typeConstraints": { "T": ["float32", "float16", "int32", "uint32", "int16", "int8", "uint8"] },
12
+ "tunables": { "WORKGROUP_SIZE": { "default": 256 } },
13
+ "derive": {
14
+ "floatFmodContract": "attrs.fmod != 0 or not (dtypes.T == \"f32\" or dtypes.T == \"f16\")",
 
 
 
 
 
15
  "scalar": "dtypes.T",
 
16
  "isU32": "dtypes.T == \"u32\"",
17
  "isI32": "dtypes.T == \"i32\"",
18
  "fmod": "attrs.fmod != 0"
19
  },
20
+ "when": ["f16Ok(dtypes.T)", "floatFmodContract"],
21
+ "bindings": {
22
+ "a": { "buffer": "read-only-storage", "elementType": "$vectorScalar" },
23
+ "c": { "buffer": "storage", "elementType": "$vectorScalar" },
24
+ "params": { "buffer": "uniform", "struct": [{ "name": "count", "type": "u32", "value": "numel(shapes.c) / 4" }] }
25
+ },
26
  "variants": [
27
  {
28
  "id": "last_axis_broadcast_vec4",
29
  "priority": 25,
30
+ "when": ["ranks.c >= 1", "sameShape(shapes.a, shapes.c)", "ranks.b == 1", "dim(shapes.b, 0) == dim(shapes.c, ranks.c - 1)", "numel(shapes.c) > 0", "numel(shapes.c) % 4 == 0"],
31
+ "derive": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"" },
32
  "passes": [
33
  {
34
  "id": "main",
35
  "name": "Mod.lastAxisBroadcastVec4",
36
+ "shader": "mod-vec4.wgsl.jinja",
37
+ "derive": { "perAxisB": true, "axisDimSpec": "dim(shapes.b, 0)" },
38
+ "bindings": ["a", "b", "c", "params"],
39
+ "dispatch": {
40
+ "x": "min(ceilDiv((numel(shapes.c) / 4), (tunables.WORKGROUP_SIZE)), 65535)",
41
+ "y": "ceilDiv(ceilDiv((numel(shapes.c) / 4), (tunables.WORKGROUP_SIZE)), 65535)",
42
+ "z": 1
43
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
  ]
46
  },
47
  {
48
  "id": "scalar_b_vec4",
49
  "priority": 30,
50
+ "when": ["sameShape(shapes.a, shapes.c)", "numel(shapes.b) == 1", "numel(shapes.c) > 0", "numel(shapes.c) % 4 == 0"],
51
+ "derive": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"" },
52
  "passes": [
53
  {
54
  "id": "main",
55
  "name": "Mod.scalarBVec4",
56
+ "shader": "mod-vec4.wgsl.jinja",
57
+ "derive": { "scalarB": true },
58
+ "bindings": ["a", { "arg": "b", "elementType": "$scalar", "length": 1 }, "c", "params"],
59
+ "dispatch": {
60
+ "x": "min(ceilDiv((numel(shapes.c) / 4), (tunables.WORKGROUP_SIZE)), 65535)",
61
+ "y": "ceilDiv(ceilDiv((numel(shapes.c) / 4), (tunables.WORKGROUP_SIZE)), 65535)",
62
+ "z": 1
63
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  }
65
  ]
66
  },
67
  {
68
  "id": "same_shape_vec4",
69
  "priority": 26,
70
+ "when": ["sameShape(shapes.a, shapes.c)", "sameShape(shapes.b, shapes.c)", "numel(shapes.c) > 0", "numel(shapes.c) % 4 == 0"],
71
+ "derive": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"" },
72
  "passes": [
73
  {
74
  "id": "main",
75
  "name": "Mod.vec4",
76
+ "shader": "mod-vec4.wgsl.jinja",
77
+ "bindings": ["a", { "arg": "b", "elementType": "$vectorScalar" }, "c", "params"],
78
+ "dispatch": {
79
+ "x": "min(ceilDiv((numel(shapes.c) / 4), (tunables.WORKGROUP_SIZE)), 65535)",
80
+ "y": "ceilDiv(ceilDiv((numel(shapes.c) / 4), (tunables.WORKGROUP_SIZE)), 65535)",
81
+ "z": 1
82
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  }
84
  ]
85
  },
86
  {
87
  "id": "broadcast",
88
+ "when": ["ranks.a >= 0", "ranks.b >= 0", "ranks.c >= 0", "ranks.a <= ranks.c", "ranks.b <= ranks.c"],
89
  "passes": [
90
  {
91
  "id": "main",
92
  "name": "Mod",
93
+ "shader": "mod.wgsl.jinja",
94
+ "derive": {
95
+ "aShape": "shapes.a",
96
+ "bShape": "shapes.b",
97
+ "cShape": "shapes.c",
98
+ "aRank": "ranks.a",
99
+ "bRank": "ranks.b",
100
+ "cRank": "ranks.c"
 
 
101
  },
102
  "bindings": [
103
+ { "arg": "a" },
104
+ "b",
105
+ { "arg": "c" },
106
+ { "name": "params", "struct": [{ "name": "count", "type": "u32", "value": "numel(shapes.c)" }] }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  ],
108
+ "dispatch": {
109
+ "x": "min(ceilDiv((numel(shapes.c)), (tunables.WORKGROUP_SIZE)), 65535)",
110
+ "y": "ceilDiv(ceilDiv((numel(shapes.c)), (tunables.WORKGROUP_SIZE)), 65535)",
111
+ "z": 1
112
+ }
113
  }
114
  ]
115
  }
build/webgpu/metadata.json CHANGED
@@ -1,19 +1,27 @@
1
  {
2
  "name": "ai.onnx.Mod",
3
- "id": "_ai_onnx_mod_webgpu_4a7f8df",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "QdfVC09wyipoUAu6hPHgQsOE7mtlr2T9OZJffvCh8YQ=",
11
- "manifest.json": "EWrTl0E4Q3pRgojKEehDCETQSaGUYd6LvDy+6DtWFCI=",
12
- "mod-vec4.wgsl.jinja": "OoOWti3TFUtnneaDtonReshC71gzRLiUQOOK4IZ9P7w=",
13
- "mod.wgsl.jinja": "R3tnz/Y9NdD2Ut/jujKAUl2H2Y2CYSu9LpKXdUYJsz4=",
14
- "test.json": "3XigfrMBzVhWpcZHr5WWptcT2N637Wb7iEjzEValb4A="
15
  }
16
  },
17
- "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
18
- "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.Mod" }
 
 
 
 
 
 
 
 
19
  }
 
1
  {
2
  "name": "ai.onnx.Mod",
3
+ "id": "_ai_onnx_mod_webgpu_821fe84",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "u7NGAyuRdKHN1czKMTxOofOGp/1+B7oS8d3EHcUjAcc=",
11
+ "manifest.json": "RLwkzGX5SssKyYjn+cZRb9VMTal+0BdSoSyxjtZhBXc=",
12
+ "mod-vec4.wgsl.jinja": "m5J+jy22HBFcUnuKvocYEGJdjpQ1xnOKyR/q7H9dxew=",
13
+ "mod.wgsl.jinja": "cp6hTSRn1C61dnn8jXx4YtvYu/4qnW3Vu9ccZLct2YE=",
14
+ "test.json": "1LykXvYWjgy9h3UgENoljeihrg0vihh13G6x4EVH8Cg="
15
  }
16
  },
17
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
18
+ "webgpu": {
19
+ "manifestSpec": "2.0",
20
+ "variants": {
21
+ "last_axis_broadcast_vec4": ["mod-vec4.wgsl.jinja"],
22
+ "scalar_b_vec4": ["mod-vec4.wgsl.jinja"],
23
+ "same_shape_vec4": ["mod-vec4.wgsl.jinja"],
24
+ "broadcast": ["mod.wgsl.jinja"]
25
+ }
26
+ }
27
  }
build/webgpu/mod-vec4.wgsl.jinja CHANGED
@@ -1,39 +1,36 @@
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
 
39
  {% if not isU32 and not isI32 %}
@@ -46,7 +43,8 @@ enable f16;
46
  fn fmod_trunc(a: f32, b: f32) -> f32 {
47
  let aa = abs(a);
48
  let ab = abs(b);
49
- // b == 0, or a non-finite: defer to the hardware formula for IEEE NaN/Inf.
 
50
  if (!(ab > 0.0) || bitcast<u32>(aa) >= 0x7f800000u) {
51
  return a - b * trunc(a / b);
52
  }
@@ -70,22 +68,22 @@ fn fmod_trunc(a: f32, b: f32) -> f32 {
70
  {% endif %}
71
 
72
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
73
- fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
74
  {{ flat_index_2d(note="vec4-limit") }}
75
- {% if source.perAxisB is defined and source.perAxisB %}
76
  // B broadcasts over the innermost axis. A/C stay 128-bit packed even when
77
  // an odd row width makes vec4 groups cross a row boundary; derive each
78
  // lane's wrapped B coordinate from the flat scalar position.
79
- let b0 = (i * 4u) % {{ source.axisDim }}u;
80
- let b1 = (b0 + 1u) % {{ source.axisDim }}u;
81
- let b2 = (b0 + 2u) % {{ source.axisDim }}u;
82
- let b3 = (b0 + 3u) % {{ source.axisDim }}u;
83
  {% endif %}
84
  {% if isU32 %}
85
- c[i] = a[i] % {% if source.scalarB is defined and source.scalarB %}vec4<u32>(b[0]){% elif source.perAxisB is defined and source.perAxisB %}vec4<u32>(b[b0], b[b1], b[b2], b[b3]){% else %}b[i]{% endif %};
86
  {% elif isI32 %}
87
  let av = a[i];
88
- let bv = {% if source.scalarB is defined and source.scalarB %}vec4<i32>(b[0]){% elif source.perAxisB is defined and source.perAxisB %}vec4<i32>(b[b0], b[b1], b[b2], b[b3]){% else %}b[i]{% endif %};
89
  let rt = av % bv;
90
  {% if fmod %}
91
  c[i] = rt;
@@ -94,10 +92,10 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups)
94
  {% endif %}
95
  {% else %}
96
  let av = vec4<f32>(a[i]);
97
- {% if source.perAxisB is defined and source.perAxisB %}
98
  let bv = vec4<f32>(f32(b[b0]), f32(b[b1]), f32(b[b2]), f32(b[b3]));
99
  {% else %}
100
- let bv = {% if source.scalarB is defined and source.scalarB %}vec4<f32>(f32(b[0])){% else %}vec4<f32>(b[i]){% endif %};
101
  {% endif %}
102
  let rt = vec4<f32>(
103
  fmod_trunc(av.x, bv.x),
 
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
  {% if not isU32 and not isI32 %}
 
43
  fn fmod_trunc(a: f32, b: f32) -> f32 {
44
  let aa = abs(a);
45
  let ab = abs(b);
46
+ // For a zero divisor or non-finite dividend, use the WGSL expression directly
47
+ // so its NaN and infinity propagation is preserved.
48
  if (!(ab > 0.0) || bitcast<u32>(aa) >= 0x7f800000u) {
49
  return a - b * trunc(a / b);
50
  }
 
68
  {% endif %}
69
 
70
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
71
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
72
  {{ flat_index_2d(note="vec4-limit") }}
73
+ {% if perAxisB is defined and perAxisB %}
74
  // B broadcasts over the innermost axis. A/C stay 128-bit packed even when
75
  // an odd row width makes vec4 groups cross a row boundary; derive each
76
  // lane's wrapped B coordinate from the flat scalar position.
77
+ let b0 = (i * 4u) % {{ axisDimSpec }}u;
78
+ let b1 = (b0 + 1u) % {{ axisDimSpec }}u;
79
+ let b2 = (b0 + 2u) % {{ axisDimSpec }}u;
80
+ let b3 = (b0 + 3u) % {{ axisDimSpec }}u;
81
  {% endif %}
82
  {% if isU32 %}
83
+ c[i] = a[i] % {% if scalarB is defined and scalarB %}vec4<u32>(b[0]){% elif perAxisB is defined and perAxisB %}vec4<u32>(b[b0], b[b1], b[b2], b[b3]){% else %}b[i]{% endif %};
84
  {% elif isI32 %}
85
  let av = a[i];
86
+ let bv = {% if scalarB is defined and scalarB %}vec4<i32>(b[0]){% elif perAxisB is defined and perAxisB %}vec4<i32>(b[b0], b[b1], b[b2], b[b3]){% else %}b[i]{% endif %};
87
  let rt = av % bv;
88
  {% if fmod %}
89
  c[i] = rt;
 
92
  {% endif %}
93
  {% else %}
94
  let av = vec4<f32>(a[i]);
95
+ {% if perAxisB is defined and perAxisB %}
96
  let bv = vec4<f32>(f32(b[b0]), f32(b[b1]), f32(b[b2]), f32(b[b3]));
97
  {% else %}
98
+ let bv = {% if scalarB is defined and scalarB %}vec4<f32>(f32(b[0])){% else %}vec4<f32>(b[i]){% endif %};
99
  {% endif %}
100
  let rt = vec4<f32>(
101
  fmod_trunc(av.x, bv.x),
build/webgpu/mod.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
  }
@@ -92,14 +92,17 @@ fn {{ fn_name }}({% if out_numel != 0 and op_numel != 1 %}out_index: u32{% endif
92
  {% endif %}
93
  {{ offset_fn(fn_name, opShape, opRank, op_same.value, op_numel.value, outShape, outRank, out_numel.value) }}
94
  {%- endmacro %}{% macro binary_broadcast_offsets() %}
95
- {{ broadcast_offset_fn("a_offset", source.aShape, source.aRank, source.cShape, source.cRank) }}
 
 
 
 
 
 
96
 
97
- {{ broadcast_offset_fn("b_offset", source.bShape, source.bRank, source.cShape, source.cRank) }}
98
  {%- endmacro %}
99
 
100
- {% if usesF16 %}
101
- enable f16;
102
- {% endif %}
103
  {{ env.wgsl.resourceDeclarations }}
104
 
105
 
@@ -115,7 +118,8 @@ enable f16;
115
  fn fmod_trunc(a: f32, b: f32) -> f32 {
116
  let aa = abs(a);
117
  let ab = abs(b);
118
- // b == 0, or a non-finite: defer to the hardware formula for IEEE NaN/Inf.
 
119
  if (!(ab > 0.0) || bitcast<u32>(aa) >= 0x7f800000u) {
120
  return a - b * trunc(a / b);
121
  }
@@ -139,13 +143,13 @@ fn fmod_trunc(a: f32, b: f32) -> f32 {
139
  {% endif %}
140
 
141
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
142
- fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
143
  {{ flat_index_2d() }}
144
  {% if isU32 %}
145
- c[i] = a[{{ broadcast_offset_call("a_offset", source.aShape, source.cShape, "i") }}] % b[{{ broadcast_offset_call("b_offset", source.bShape, source.cShape, "i") }}];
146
  {% elif isI32 %}
147
- let av = a[{{ broadcast_offset_call("a_offset", source.aShape, source.cShape, "i") }}];
148
- let bv = b[{{ broadcast_offset_call("b_offset", source.bShape, source.cShape, "i") }}];
149
  let rt = av % bv;
150
  {% if fmod %}
151
  c[i] = rt;
@@ -153,8 +157,8 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups)
153
  c[i] = select(rt, rt + bv, rt != 0 && (rt < 0) != (bv < 0));
154
  {% endif %}
155
  {% else %}
156
- let av = f32(a[{{ broadcast_offset_call("a_offset", source.aShape, source.cShape, "i") }}]);
157
- let bv = f32(b[{{ broadcast_offset_call("b_offset", source.bShape, source.cShape, "i") }}]);
158
  let rt = fmod_trunc(av, bv);
159
  c[i] = {{ scalar }}(rt);
160
  {% endif %}
 
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
  }
 
92
  {% endif %}
93
  {{ offset_fn(fn_name, opShape, opRank, op_same.value, op_numel.value, outShape, outRank, out_numel.value) }}
94
  {%- endmacro %}{% macro binary_broadcast_offsets() %}
95
+ {% set aShape = aShape | default([]) %}
96
+ {% set aRank = aRank | default(0) %}
97
+ {% set bShape = bShape | default([]) %}
98
+ {% set bRank = bRank | default(0) %}
99
+ {% set cShape = cShape | default([]) %}
100
+ {% set cRank = cRank | default(0) %}
101
+ {{ broadcast_offset_fn("a_offset", aShape, aRank, cShape, cRank) }}
102
 
103
+ {{ broadcast_offset_fn("b_offset", bShape, bRank, cShape, cRank) }}
104
  {%- endmacro %}
105
 
 
 
 
106
  {{ env.wgsl.resourceDeclarations }}
107
 
108
 
 
118
  fn fmod_trunc(a: f32, b: f32) -> f32 {
119
  let aa = abs(a);
120
  let ab = abs(b);
121
+ // For a zero divisor or non-finite dividend, use the WGSL expression directly
122
+ // so its NaN and infinity propagation is preserved.
123
  if (!(ab > 0.0) || bitcast<u32>(aa) >= 0x7f800000u) {
124
  return a - b * trunc(a / b);
125
  }
 
143
  {% endif %}
144
 
145
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
146
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
147
  {{ flat_index_2d() }}
148
  {% if isU32 %}
149
+ c[i] = a[{{ broadcast_offset_call("a_offset", aShape, cShape, "i") }}] % b[{{ broadcast_offset_call("b_offset", bShape, cShape, "i") }}];
150
  {% elif isI32 %}
151
+ let av = a[{{ broadcast_offset_call("a_offset", aShape, cShape, "i") }}];
152
+ let bv = b[{{ broadcast_offset_call("b_offset", bShape, cShape, "i") }}];
153
  let rt = av % bv;
154
  {% if fmod %}
155
  c[i] = rt;
 
157
  c[i] = select(rt, rt + bv, rt != 0 && (rt < 0) != (bv < 0));
158
  {% endif %}
159
  {% else %}
160
+ let av = f32(a[{{ broadcast_offset_call("a_offset", aShape, cShape, "i") }}]);
161
+ let bv = f32(b[{{ broadcast_offset_call("b_offset", bShape, cShape, "i") }}]);
162
  let rt = fmod_trunc(av, bv);
163
  c[i] = {{ scalar }}(rt);
164
  {% endif %}
build/webgpu/test.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.Mod",
3
  "fixtureArrays": {
4
  "ort_int32_rank3_scalar_divisor_broadcast_input_a": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
5
  },
@@ -79,7 +78,7 @@
79
  "provenance": {
80
  "source": "onnxruntime/test/providers/cpu/math/element_wise_ops_test.cc",
81
  "test": "ModOpTest.Fmod_float_mixed_sign",
82
- "notes": "Scalar broadcast companion: subnormal dividends smaller than the divisor are valid nonzero remainders."
83
  },
84
  "attrs": { "fmod": 1 },
85
  "inputs": {
@@ -457,7 +456,7 @@
457
  "outputs": { "c": { "dtype": "int32", "shape": [6] } },
458
  "provenance": {
459
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_mod_int64_fmod",
460
- "notes": "ONNX int64 tensors use framework int32/uint32 slots where representable."
461
  }
462
  },
463
  {
@@ -579,7 +578,7 @@
579
  "provenance": {
580
  "source": "onnxruntime/test/providers/cpu/math/element_wise_ops_test.cc",
581
  "test": "ModOpTest.Int32_mod_bcast",
582
- "notes": "The fmod=1 half of the same route: truncated remainder, so the sign follows the dividend and the floor-mod adjust must NOT fire. Left unpinned so the reference is exercised on the arm its pinned sibling covers only in the kernel."
583
  },
584
  "inputs": {
585
  "a": { "dtype": "int32", "shape": [8], "data": { "kind": "values", "values": [-9, 7, -7, 15, 0, -1, 22, -22] } },
@@ -681,7 +680,10 @@
681
  },
682
  "b": { "dtype": "float16", "shape": [4], "data": { "kind": "values", "values": [3.0, 0.5, 3.0, 0.125] } }
683
  },
684
- "outputs": { "c": { "dtype": "float16", "shape": [4], "tolerance": 0.5 } }
 
 
 
685
  },
686
  {
687
  "name": "u32_scalar_b_non_vec4_large_values",
@@ -727,7 +729,7 @@
727
  {
728
  "name": "last_axis_broadcast_vec4_odd_row_crossing",
729
  "provenance": {
730
- "notes": "Four vec4 groups cross odd-width row boundaries, exercising wrapped per-lane B indices. Expected values come from the TypeScript Mod reference."
731
  },
732
  "attrs": { "fmod": 1 },
733
  "inputs": {
@@ -743,7 +745,7 @@
743
  {
744
  "name": "last_axis_broadcast_vec4_f16_odd_row_crossing",
745
  "provenance": {
746
- "notes": "f16 companion for the per-lane broadcast path; locks explicit widening of scalar B lanes before fmod arithmetic."
747
  },
748
  "attrs": { "fmod": 1 },
749
  "inputs": {
 
1
  {
 
2
  "fixtureArrays": {
3
  "ort_int32_rank3_scalar_divisor_broadcast_input_a": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
4
  },
 
78
  "provenance": {
79
  "source": "onnxruntime/test/providers/cpu/math/element_wise_ops_test.cc",
80
  "test": "ModOpTest.Fmod_float_mixed_sign",
81
+ "notes": "On the scalar broadcast path, subnormal dividends smaller than the divisor produce valid nonzero remainders."
82
  },
83
  "attrs": { "fmod": 1 },
84
  "inputs": {
 
456
  "outputs": { "c": { "dtype": "int32", "shape": [6] } },
457
  "provenance": {
458
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_mod_int64_fmod",
459
+ "notes": "The int32 inputs exactly represent the source ONNX int64 values."
460
  }
461
  },
462
  {
 
578
  "provenance": {
579
  "source": "onnxruntime/test/providers/cpu/math/element_wise_ops_test.cc",
580
  "test": "ModOpTest.Int32_mod_bcast",
581
+ "notes": "With fmod=1, truncated remainder follows the dividend's sign and must not apply the floor-mod adjustment."
582
  },
583
  "inputs": {
584
  "a": { "dtype": "int32", "shape": [8], "data": { "kind": "values", "values": [-9, 7, -7, 15, 0, -1, 22, -22] } },
 
680
  },
681
  "b": { "dtype": "float16", "shape": [4], "data": { "kind": "values", "values": [3.0, 0.5, 3.0, 0.125] } }
682
  },
683
+ "outputs": { "c": { "dtype": "float16", "shape": [4], "tolerance": 0.01 } },
684
+ "provenance": {
685
+ "notes": "C-style fmod at the top of the float16 range needs float32 division and truncation; the remainders are exact, so the tolerance only covers the final float16 rounding."
686
+ }
687
  },
688
  {
689
  "name": "u32_scalar_b_non_vec4_large_values",
 
729
  {
730
  "name": "last_axis_broadcast_vec4_odd_row_crossing",
731
  "provenance": {
732
+ "notes": "Four vec4 groups cross odd-width row boundaries, exercising wrapped per-lane B indices with distinct remainders."
733
  },
734
  "attrs": { "fmod": 1 },
735
  "inputs": {
 
745
  {
746
  "name": "last_axis_broadcast_vec4_f16_odd_row_crossing",
747
  "provenance": {
748
+ "notes": "Float16 per-lane broadcasting requires scalar B lanes to be widened before fmod arithmetic."
749
  },
750
  "attrs": { "fmod": 1 },
751
  "inputs": {