Xenova HF Staff commited on
Commit
72e2e95
·
verified ·
1 Parent(s): a194743

sync 6fdf6301e2bb

Browse files
README.md CHANGED
@@ -54,7 +54,7 @@ Default values (overridable per request):
54
  - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
55
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
56
  - [`test.json`](build/webgpu/test.json) — correctness cases
57
- - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
58
  - [`cumprod-tiled-short-rows.wgsl.jinja`](build/webgpu/cumprod-tiled-short-rows.wgsl.jinja)
59
  - [`scan-row-chunked-vec4.wgsl.jinja`](build/webgpu/scan-row-chunked-vec4.wgsl.jinja)
60
  - [`scan-row-chunked.wgsl.jinja`](build/webgpu/scan-row-chunked.wgsl.jinja)
@@ -63,7 +63,7 @@ Default values (overridable per request):
63
  ## Use with `@huggingface/kernels`
64
 
65
  ```sh
66
- npm install --save-exact @huggingface/kernels@0.0.1-preview.2
67
  ```
68
 
69
  Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
 
54
  - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
55
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
56
  - [`test.json`](build/webgpu/test.json) — correctness cases
57
+ - [`bench.json`](build/webgpu/bench.json) — benchmark cases
58
  - [`cumprod-tiled-short-rows.wgsl.jinja`](build/webgpu/cumprod-tiled-short-rows.wgsl.jinja)
59
  - [`scan-row-chunked-vec4.wgsl.jinja`](build/webgpu/scan-row-chunked-vec4.wgsl.jinja)
60
  - [`scan-row-chunked.wgsl.jinja`](build/webgpu/scan-row-chunked.wgsl.jinja)
 
63
  ## Use with `@huggingface/kernels`
64
 
65
  ```sh
66
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.3
67
  ```
68
 
69
  Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
build/webgpu/cumprod-tiled-short-rows.wgsl.jinja CHANGED
@@ -15,7 +15,8 @@ var<workgroup> tile: array<{{ accType }}, {{ tileElements }}>;
15
  @compute @workgroup_size(WG, 1, 1)
16
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
17
  @builtin(local_invocation_id) lid: vec3<u32>) {
18
- let rowBase = wg.x * ROWS_PER_WG;
 
19
  if (rowBase >= params.rows) {
20
  return;
21
  }
 
15
  @compute @workgroup_size(WG, 1, 1)
16
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
17
  @builtin(local_invocation_id) lid: vec3<u32>) {
18
+ // 2D-folded tile index: wg.y carries the high bits past the dispatch limit.
19
+ let rowBase = (wg.x + wg.y * {{ DISPATCH_FOLD_WIDTH }}u) * ROWS_PER_WG;
20
  if (rowBase >= params.rows) {
21
  return;
22
  }
build/webgpu/manifest.json CHANGED
@@ -15,34 +15,32 @@
15
  "SHORT_ROW_TILE_STORAGE_TARGET": { "default": 16384 }
16
  },
17
  "derive": {
 
18
  "shortRowsPerWorkgroup": "max(1, min(tunables.WORKGROUP_SIZE, floor(min(tunables.SHORT_ROW_TILE_STORAGE_TARGET, device.limits.maxComputeWorkgroupStorageSize) / ((dim(shapes.x, args.axis) + 1) * 4))))",
19
  "shortRowTileScan": "inner(shapes.x, args.axis) == 1 and dim(shapes.x, args.axis) >= 2 and dim(shapes.x, args.axis) <= tunables.SERIAL_SHORT_DIM_MAX and rows(shapes.x, args.axis) >= tunables.SERIAL_MIN_ROWS and ceilDiv(rows(shapes.x, args.axis), shortRowsPerWorkgroup) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
20
  "cooperativeRowScan": "inner(shapes.x, args.axis) == 1 and dim(shapes.x, args.axis) > 32 and rows(shapes.x, args.axis) >= 1 and rows(shapes.x, args.axis) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and not shortRowTileScan"
21
  },
22
  "when": ["f16Ok(dtypes.T)", "numel(shapes.x) == numel(shapes.y)", "ranks.x >= 1", "ranks.y == ranks.x"],
23
  "bindings": {
24
- "x": { "buffer": "read-only-storage", "elementType": "$scalar" },
25
- "y": { "buffer": "storage", "elementType": "$scalar" },
26
  "params": {
27
- "buffer": "uniform",
28
  "struct": [
29
  { "name": "rows", "type": "u32", "value": "rows(shapes.x, args.axis)" },
30
  { "name": "dim", "type": "u32", "value": "dim(shapes.x, args.axis)" }
31
  ]
32
  },
33
- "x_2": { "name": "x", "buffer": "read-only-storage", "elementType": "$vec4Scalar" },
34
- "y_2": { "name": "y", "buffer": "storage", "elementType": "$vec4Scalar" },
35
- "params_2": {
36
  "name": "params",
37
- "buffer": "uniform",
38
  "struct": [
39
  { "name": "rows", "type": "u32", "value": "rows(shapes.x, args.axis)" },
40
  { "name": "dim4", "type": "u32", "value": "dim(shapes.x, args.axis) / 4" }
41
  ]
42
  },
43
- "params_3": {
44
  "name": "params",
45
- "buffer": "uniform",
46
  "struct": [
47
  { "name": "rows", "type": "u32", "value": "rows(shapes.x, args.axis)" },
48
  { "name": "dim", "type": "u32", "value": "dim(shapes.x, args.axis)" },
@@ -58,7 +56,6 @@
58
  "derive": {
59
  "scalar": "dtypes.T",
60
  "accType": "\"f32\" if (dtypes.T == \"f32\" or dtypes.T == \"f16\") else dtypes.T",
61
- "usesF16": "dtypes.T == \"f16\"",
62
  "workgroupSize": "tunables.WORKGROUP_SIZE",
63
  "rowsPerWorkgroup": "shortRowsPerWorkgroup",
64
  "rowStride": "dim(shapes.x, args.axis) + 1",
@@ -87,7 +84,6 @@
87
  "scalar": "dtypes.T",
88
  "vec4Scalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
89
  "accType": "\"f32\" if (dtypes.T == \"f32\" or dtypes.T == \"f16\") else dtypes.T",
90
- "usesF16": "dtypes.T == \"f16\"",
91
  "workgroupSize": "tunables.WORKGROUP_SIZE"
92
  },
93
  "passes": [
@@ -95,13 +91,8 @@
95
  "id": "main",
96
  "name": "CumProd.WorkgroupRowScanVec4",
97
  "shader": "scan-row-chunked-vec4.wgsl.jinja",
98
- "subgroupCollectivesWidth": "portable",
99
- "derive": {
100
- "op": "\"mul\"",
101
- "exclusive": "attrs.exclusive != 0",
102
- "useSubgroups": "device.features.has(\"subgroups\")"
103
- },
104
- "bindings": ["x_2", "y_2", "params_2"],
105
  "dispatch": { "x": "rows(shapes.x, args.axis)" }
106
  }
107
  ]
@@ -113,7 +104,6 @@
113
  "derive": {
114
  "scalar": "dtypes.T",
115
  "accType": "\"f32\" if (dtypes.T == \"f32\" or dtypes.T == \"f16\") else dtypes.T",
116
- "usesF16": "dtypes.T == \"f16\"",
117
  "workgroupSize": "tunables.WORKGROUP_SIZE"
118
  },
119
  "passes": [
@@ -121,9 +111,7 @@
121
  "id": "main",
122
  "name": "CumProd.WorkgroupRowScan",
123
  "shader": "scan-row-chunked.wgsl.jinja",
124
- "subgroupCollectivesWidth": "portable",
125
  "derive": {
126
- "op": "\"mul\"",
127
  "exclusive": "attrs.exclusive != 0",
128
  "reverse": "attrs.reverse != 0",
129
  "useSubgroups": "device.features.has(\"subgroups\")"
@@ -140,7 +128,6 @@
140
  "derive": {
141
  "scalar": "dtypes.T",
142
  "accType": "\"f32\" if (dtypes.T == \"f32\" or dtypes.T == \"f16\") else dtypes.T",
143
- "usesF16": "dtypes.T == \"f16\"",
144
  "workgroupSize": "tunables.WORKGROUP_SIZE"
145
  },
146
  "passes": [
@@ -148,8 +135,8 @@
148
  "id": "main",
149
  "name": "CumProd.ThreadRowScan",
150
  "shader": "scan-thread-row.wgsl.jinja",
151
- "derive": { "op": "\"mul\"", "exclusive": "attrs.exclusive != 0", "reverse": "attrs.reverse != 0" },
152
- "bindings": ["x", "y", "params_3"],
153
  "dispatch": {
154
  "x": "min(ceilDiv((rows(shapes.x, args.axis)), (tunables.WORKGROUP_SIZE)), 65535)",
155
  "y": "ceilDiv(ceilDiv((rows(shapes.x, args.axis)), (tunables.WORKGROUP_SIZE)), 65535)",
 
15
  "SHORT_ROW_TILE_STORAGE_TARGET": { "default": 16384 }
16
  },
17
  "derive": {
18
+ "op": "\"mul\"",
19
  "shortRowsPerWorkgroup": "max(1, min(tunables.WORKGROUP_SIZE, floor(min(tunables.SHORT_ROW_TILE_STORAGE_TARGET, device.limits.maxComputeWorkgroupStorageSize) / ((dim(shapes.x, args.axis) + 1) * 4))))",
20
  "shortRowTileScan": "inner(shapes.x, args.axis) == 1 and dim(shapes.x, args.axis) >= 2 and dim(shapes.x, args.axis) <= tunables.SERIAL_SHORT_DIM_MAX and rows(shapes.x, args.axis) >= tunables.SERIAL_MIN_ROWS and ceilDiv(rows(shapes.x, args.axis), shortRowsPerWorkgroup) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
21
  "cooperativeRowScan": "inner(shapes.x, args.axis) == 1 and dim(shapes.x, args.axis) > 32 and rows(shapes.x, args.axis) >= 1 and rows(shapes.x, args.axis) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and not shortRowTileScan"
22
  },
23
  "when": ["f16Ok(dtypes.T)", "numel(shapes.x) == numel(shapes.y)", "ranks.x >= 1", "ranks.y == ranks.x"],
24
  "bindings": {
25
+ "x": { "elementType": "$scalar" },
26
+ "y": { "elementType": "$scalar" },
27
  "params": {
 
28
  "struct": [
29
  { "name": "rows", "type": "u32", "value": "rows(shapes.x, args.axis)" },
30
  { "name": "dim", "type": "u32", "value": "dim(shapes.x, args.axis)" }
31
  ]
32
  },
33
+ "x_main": { "name": "x", "elementType": "$vec4Scalar" },
34
+ "y_main": { "name": "y", "elementType": "$vec4Scalar" },
35
+ "params_main": {
36
  "name": "params",
 
37
  "struct": [
38
  { "name": "rows", "type": "u32", "value": "rows(shapes.x, args.axis)" },
39
  { "name": "dim4", "type": "u32", "value": "dim(shapes.x, args.axis) / 4" }
40
  ]
41
  },
42
+ "params__uniform": {
43
  "name": "params",
 
44
  "struct": [
45
  { "name": "rows", "type": "u32", "value": "rows(shapes.x, args.axis)" },
46
  { "name": "dim", "type": "u32", "value": "dim(shapes.x, args.axis)" },
 
56
  "derive": {
57
  "scalar": "dtypes.T",
58
  "accType": "\"f32\" if (dtypes.T == \"f32\" or dtypes.T == \"f16\") else dtypes.T",
 
59
  "workgroupSize": "tunables.WORKGROUP_SIZE",
60
  "rowsPerWorkgroup": "shortRowsPerWorkgroup",
61
  "rowStride": "dim(shapes.x, args.axis) + 1",
 
84
  "scalar": "dtypes.T",
85
  "vec4Scalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
86
  "accType": "\"f32\" if (dtypes.T == \"f32\" or dtypes.T == \"f16\") else dtypes.T",
 
87
  "workgroupSize": "tunables.WORKGROUP_SIZE"
88
  },
89
  "passes": [
 
91
  "id": "main",
92
  "name": "CumProd.WorkgroupRowScanVec4",
93
  "shader": "scan-row-chunked-vec4.wgsl.jinja",
94
+ "derive": { "exclusive": "attrs.exclusive != 0", "useSubgroups": "device.features.has(\"subgroups\")" },
95
+ "bindings": ["x_main", "y_main", "params_main"],
 
 
 
 
 
96
  "dispatch": { "x": "rows(shapes.x, args.axis)" }
97
  }
98
  ]
 
104
  "derive": {
105
  "scalar": "dtypes.T",
106
  "accType": "\"f32\" if (dtypes.T == \"f32\" or dtypes.T == \"f16\") else dtypes.T",
 
107
  "workgroupSize": "tunables.WORKGROUP_SIZE"
108
  },
109
  "passes": [
 
111
  "id": "main",
112
  "name": "CumProd.WorkgroupRowScan",
113
  "shader": "scan-row-chunked.wgsl.jinja",
 
114
  "derive": {
 
115
  "exclusive": "attrs.exclusive != 0",
116
  "reverse": "attrs.reverse != 0",
117
  "useSubgroups": "device.features.has(\"subgroups\")"
 
128
  "derive": {
129
  "scalar": "dtypes.T",
130
  "accType": "\"f32\" if (dtypes.T == \"f32\" or dtypes.T == \"f16\") else dtypes.T",
 
131
  "workgroupSize": "tunables.WORKGROUP_SIZE"
132
  },
133
  "passes": [
 
135
  "id": "main",
136
  "name": "CumProd.ThreadRowScan",
137
  "shader": "scan-thread-row.wgsl.jinja",
138
+ "derive": { "exclusive": "attrs.exclusive != 0", "reverse": "attrs.reverse != 0" },
139
+ "bindings": ["x", "y", "params__uniform"],
140
  "dispatch": {
141
  "x": "min(ceilDiv((rows(shapes.x, args.axis)), (tunables.WORKGROUP_SIZE)), 65535)",
142
  "y": "ceilDiv(ceilDiv((rows(shapes.x, args.axis)), (tunables.WORKGROUP_SIZE)), 65535)",
build/webgpu/metadata.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "ai.onnx.CumProd",
3
- "id": "_ai_onnx_cumprod_webgpu_feb0f63",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
@@ -8,17 +8,17 @@
8
  "algorithm": "sha256",
9
  "files": {
10
  "bench.json": "TsJ08po3IF5cQJUCRPrMoIhyLOTSsyuddPCr6fwUmMY=",
11
- "cumprod-tiled-short-rows.wgsl.jinja": "zXgZLN3glP7YaO+4AVVr/b/xOX5jvkeUb+2bxES00DU=",
12
- "manifest.json": "goNH2mdRcylLp6WBDSFf+2rT7cryeWckBuH/GMfZeiQ=",
13
- "scan-row-chunked-vec4.wgsl.jinja": "9KkZD/utgA1D7Ns/f9SlJXlJYAkIYeGg0XJhUQsfzeM=",
14
- "scan-row-chunked.wgsl.jinja": "oPnVIjf8h3JeHcmFRCGFrN8oP6xlEixAES9S7vKzVlk=",
15
- "scan-thread-row.wgsl.jinja": "YDNpvK2v9J6jLxurSlT+vfgfUyrEn1m1kQ/bWL95pY8=",
16
- "test.json": "L08e2ck+A4AmlJOEsJ90/G/QX4H9HHctuYqvVVzS2nI="
17
  }
18
  },
19
- "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
20
  "webgpu": {
21
- "manifestSpec": "2.0",
22
  "variants": {
23
  "tiled_short_rows": ["cumprod-tiled-short-rows.wgsl.jinja"],
24
  "wg_row_scan_vec4": ["scan-row-chunked-vec4.wgsl.jinja"],
 
1
  {
2
  "name": "ai.onnx.CumProd",
3
+ "id": "_ai_onnx_cumprod_webgpu_7b33632",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
 
8
  "algorithm": "sha256",
9
  "files": {
10
  "bench.json": "TsJ08po3IF5cQJUCRPrMoIhyLOTSsyuddPCr6fwUmMY=",
11
+ "cumprod-tiled-short-rows.wgsl.jinja": "UerkBDU7fq9KungxlAQv+GKFy2l+U3AfB73nyavLe6g=",
12
+ "manifest.json": "D1sJBLkaP1YYTvDvGZer83zCF4TrdGApLVGGm4S93jo=",
13
+ "scan-row-chunked-vec4.wgsl.jinja": "UP1vcu2Akr7EoVd7rLEMKuX4TwqvkDCJ8jCDTK/BkBQ=",
14
+ "scan-row-chunked.wgsl.jinja": "YvnQVPH5vCCEQjxZrUDEMDmg9NOuKivLA7gWJOS21NE=",
15
+ "scan-thread-row.wgsl.jinja": "y4xCv3ymxbBws1At0MSD2CSd5L/NWrBsmXsIiPrGH2c=",
16
+ "test.json": "xW22ETyy/XBxMFQhxEmZNC7De9w9caIDTsysYfEIvAY="
17
  }
18
  },
19
+ "provenance": { "kernel": { "sha": "6fdf6301e2bbcc2f03bf1eaf493b7ad55ef33afc", "dirty": false } },
20
  "webgpu": {
21
+ "manifestSpec": "2.1",
22
  "variants": {
23
  "tiled_short_rows": ["cumprod-tiled-short-rows.wgsl.jinja"],
24
  "wg_row_scan_vec4": ["scan-row-chunked-vec4.wgsl.jinja"],
build/webgpu/scan-row-chunked-vec4.wgsl.jinja CHANGED
@@ -13,11 +13,10 @@
13
  // so it is the reduction of every element strictly before this thread's block.
14
  // Inclusive output k is combine(tbase, p_k); exclusive output k uses p_{k-1},
15
  // with p_{-1} equal to the identity.
16
- {% set useSubgroups = useSubgroups %}
 
17
  {% if useSubgroups %}
18
  enable subgroups;
19
- {%- set sg_exc = "subgroupExclusiveMul" if op == "mul" else "subgroupExclusiveAdd" %}
20
- {% set sg_tot = "subgroupMul" if op == "mul" else "subgroupAdd" %}
21
  {% endif %}
22
  {{ env.wgsl.resourceDeclarations }}
23
 
@@ -29,7 +28,6 @@ fn combine(a: {{ accType }}, b: {{ accType }}) -> {{ accType }} {
29
  }
30
 
31
  {% if useSubgroups %}
32
- {% set skipLogicalLaneCount = skipLogicalLaneCount is defined and skipLogicalLaneCount %}
33
  var<workgroup> sgLaneClaims: atomic<u32>;
34
 
35
  struct SubgroupLogicalLanes {
@@ -39,30 +37,28 @@ struct SubgroupLogicalLanes {
39
  count: u32,
40
  }
41
 
42
- fn subgroup_logical_lanes() -> SubgroupLogicalLanes {
43
- let rank = subgroupExclusiveAdd(1u);
44
- let count = subgroupAdd(1u);
45
- // Every lane performs the atomic so no collective follows a lane guard (a
46
- // subgroup op after a closed `if (rank == 0u)` block is the reconvergence
47
- // hazard the render gate flags): only the rank-0 lane adds its subgroup's
48
- // claim, every other lane adds 0 and discards its snapshot. The rank-0 lane is
49
- // the lowest active lane, which is the lane `subgroupBroadcastFirst` reads.
50
  let ticket = atomicAdd(&sgLaneClaims, select(0u, count | (1u << 16u), rank == 0u));
51
  let claim = subgroupBroadcastFirst(ticket);
52
  return SubgroupLogicalLanes((claim & 0xffffu) + rank, claim >> 16u, rank, count);
53
  }
54
- {% if not skipLogicalLaneCount %}
55
 
56
  fn subgroup_logical_count() -> u32 {
57
  return atomicLoad(&sgLaneClaims) >> 16u;
58
  }
59
- {% endif %}
60
-
61
  {% endif %}
62
  var<workgroup> sgTotals: array<{{ accType }}, WG>;
63
 
64
  @compute @workgroup_size(WG, 1, 1)
65
- fn main(@builtin(workgroup_id) wg: vec3<u32>{% if not useSubgroups %},
 
 
66
  @builtin(local_invocation_id) lid: vec3<u32>{% endif %}) {
67
  let row = wg.x;
68
  {% if not useSubgroups %}
@@ -75,7 +71,7 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>{% if not useSubgroups %},
75
  {% if useSubgroups %}
76
  // `ltid` is the vec4 block this invocation owns and `ord` its subgroup's carry
77
  // ordinal; both are dense and independent of the device's partition.
78
- let L = subgroup_logical_lanes();
79
  let tid = L.ltid;
80
  workgroupBarrier();
81
  let numSub = subgroup_logical_count();
 
13
  // so it is the reduction of every element strictly before this thread's block.
14
  // Inclusive output k is combine(tbase, p_k); exclusive output k uses p_{k-1},
15
  // with p_{-1} equal to the identity.
16
+ {% set sg_exc = "subgroupExclusiveMul" %}
17
+ {% set sg_tot = "subgroupMul" %}
18
  {% if useSubgroups %}
19
  enable subgroups;
 
 
20
  {% endif %}
21
  {{ env.wgsl.resourceDeclarations }}
22
 
 
28
  }
29
 
30
  {% if useSubgroups %}
 
31
  var<workgroup> sgLaneClaims: atomic<u32>;
32
 
33
  struct SubgroupLogicalLanes {
 
37
  count: u32,
38
  }
39
 
40
+ fn subgroup_logical_lanes(rank: u32, count: u32) -> SubgroupLogicalLanes {
41
+ // Every lane performs the atomic so no collective follows a lane guard: a
42
+ // subgroup op after a closed `if (rank == 0u)` block is a reconvergence
43
+ // hazard, since every subgroup operation must be reached in uniform control
44
+ // flow. Only the lane-0 lane adds its subgroup's claim, every other lane
45
+ // adds 0 and discards its snapshot. Lane 0 is the lowest lane, which is the
46
+ // lane `subgroupBroadcastFirst` reads.
 
47
  let ticket = atomicAdd(&sgLaneClaims, select(0u, count | (1u << 16u), rank == 0u));
48
  let claim = subgroupBroadcastFirst(ticket);
49
  return SubgroupLogicalLanes((claim & 0xffffu) + rank, claim >> 16u, rank, count);
50
  }
 
51
 
52
  fn subgroup_logical_count() -> u32 {
53
  return atomicLoad(&sgLaneClaims) >> 16u;
54
  }
 
 
55
  {% endif %}
56
  var<workgroup> sgTotals: array<{{ accType }}, WG>;
57
 
58
  @compute @workgroup_size(WG, 1, 1)
59
+ fn main(@builtin(workgroup_id) wg: vec3<u32>{% if useSubgroups %},
60
+ @builtin(subgroup_invocation_id) sgLane: u32,
61
+ @builtin(subgroup_size) sgWidth: u32{% else %},
62
  @builtin(local_invocation_id) lid: vec3<u32>{% endif %}) {
63
  let row = wg.x;
64
  {% if not useSubgroups %}
 
71
  {% if useSubgroups %}
72
  // `ltid` is the vec4 block this invocation owns and `ord` its subgroup's carry
73
  // ordinal; both are dense and independent of the device's partition.
74
+ let L = subgroup_logical_lanes(sgLane, sgWidth);
75
  let tid = L.ltid;
76
  workgroupBarrier();
77
  let numSub = subgroup_logical_count();
build/webgpu/scan-row-chunked.wgsl.jinja CHANGED
@@ -12,9 +12,9 @@
12
  {% if useSubgroups %}
13
  enable subgroups;
14
  {% endif %}
15
- {% set sg_inc = "subgroupInclusiveMul" if op == "mul" else "subgroupInclusiveAdd" %}
16
- {% set sg_exc = "subgroupExclusiveMul" if op == "mul" else "subgroupExclusiveAdd" %}
17
- {% set sg_tot = "subgroupMul" if op == "mul" else "subgroupAdd" %}
18
  {{ env.wgsl.resourceDeclarations }}
19
 
20
  const WG: u32 = {{ workgroupSize }}u;
@@ -25,7 +25,6 @@ fn combine(a: {{ accType }}, b: {{ accType }}) -> {{ accType }} {
25
  }
26
 
27
  {% if useSubgroups %}
28
- {% set skipLogicalLaneCount = skipLogicalLaneCount is defined and skipLogicalLaneCount %}
29
  var<workgroup> sgLaneClaims: atomic<u32>;
30
 
31
  struct SubgroupLogicalLanes {
@@ -35,39 +34,37 @@ struct SubgroupLogicalLanes {
35
  count: u32,
36
  }
37
 
38
- fn subgroup_logical_lanes() -> SubgroupLogicalLanes {
39
- let rank = subgroupExclusiveAdd(1u);
40
- let count = subgroupAdd(1u);
41
- // Every lane performs the atomic so no collective follows a lane guard (a
42
- // subgroup op after a closed `if (rank == 0u)` block is the reconvergence
43
- // hazard the render gate flags): only the rank-0 lane adds its subgroup's
44
- // claim, every other lane adds 0 and discards its snapshot. The rank-0 lane is
45
- // the lowest active lane, which is the lane `subgroupBroadcastFirst` reads.
46
  let ticket = atomicAdd(&sgLaneClaims, select(0u, count | (1u << 16u), rank == 0u));
47
  let claim = subgroupBroadcastFirst(ticket);
48
  return SubgroupLogicalLanes((claim & 0xffffu) + rank, claim >> 16u, rank, count);
49
  }
50
- {% if not skipLogicalLaneCount %}
51
 
52
  fn subgroup_logical_count() -> u32 {
53
  return atomicLoad(&sgLaneClaims) >> 16u;
54
  }
55
- {% endif %}
56
-
57
  // One partial per subgroup, addressed by its logical ordinal. A workgroup of WG
58
  // invocations holds at most WG subgroups at any adapter width, so WG is a sound
59
  // bound with no width guard.
60
  var<workgroup> sgTotals: array<{{ accType }}, WG>;
61
 
62
  @compute @workgroup_size(WG, 1, 1)
63
- fn main(@builtin(workgroup_id) wg: vec3<u32>) {
 
 
64
  let row = wg.x;
65
  if (row >= params.rows) {
66
  return;
67
  }
68
  // `ltid` is the row position this invocation owns and `ord` its subgroup's
69
  // carry ordinal; both are dense and independent of the device's partition.
70
- let L = subgroup_logical_lanes();
71
  let tid = L.ltid;
72
  workgroupBarrier();
73
  let numSub = subgroup_logical_count();
 
12
  {% if useSubgroups %}
13
  enable subgroups;
14
  {% endif %}
15
+ {% set sg_inc = "subgroupInclusiveMul" %}
16
+ {% set sg_exc = "subgroupExclusiveMul" %}
17
+ {% set sg_tot = "subgroupMul" %}
18
  {{ env.wgsl.resourceDeclarations }}
19
 
20
  const WG: u32 = {{ workgroupSize }}u;
 
25
  }
26
 
27
  {% if useSubgroups %}
 
28
  var<workgroup> sgLaneClaims: atomic<u32>;
29
 
30
  struct SubgroupLogicalLanes {
 
34
  count: u32,
35
  }
36
 
37
+ fn subgroup_logical_lanes(rank: u32, count: u32) -> SubgroupLogicalLanes {
38
+ // Every lane performs the atomic so no collective follows a lane guard: a
39
+ // subgroup op after a closed `if (rank == 0u)` block is a reconvergence
40
+ // hazard, since every subgroup operation must be reached in uniform control
41
+ // flow. Only the lane-0 lane adds its subgroup's claim, every other lane
42
+ // adds 0 and discards its snapshot. Lane 0 is the lowest lane, which is the
43
+ // lane `subgroupBroadcastFirst` reads.
 
44
  let ticket = atomicAdd(&sgLaneClaims, select(0u, count | (1u << 16u), rank == 0u));
45
  let claim = subgroupBroadcastFirst(ticket);
46
  return SubgroupLogicalLanes((claim & 0xffffu) + rank, claim >> 16u, rank, count);
47
  }
 
48
 
49
  fn subgroup_logical_count() -> u32 {
50
  return atomicLoad(&sgLaneClaims) >> 16u;
51
  }
 
 
52
  // One partial per subgroup, addressed by its logical ordinal. A workgroup of WG
53
  // invocations holds at most WG subgroups at any adapter width, so WG is a sound
54
  // bound with no width guard.
55
  var<workgroup> sgTotals: array<{{ accType }}, WG>;
56
 
57
  @compute @workgroup_size(WG, 1, 1)
58
+ fn main(@builtin(workgroup_id) wg: vec3<u32>,
59
+ @builtin(subgroup_invocation_id) sgLane: u32,
60
+ @builtin(subgroup_size) sgWidth: u32) {
61
  let row = wg.x;
62
  if (row >= params.rows) {
63
  return;
64
  }
65
  // `ltid` is the row position this invocation owns and `ord` its subgroup's
66
  // carry ordinal; both are dense and independent of the device's partition.
67
+ let L = subgroup_logical_lanes(sgLane, sgWidth);
68
  let tid = L.ltid;
69
  workgroupBarrier();
70
  let numSub = subgroup_logical_count();
build/webgpu/scan-thread-row.wgsl.jinja CHANGED
@@ -3,6 +3,14 @@
3
  // is O(numel), versus O(numel * dim) for a naive per-output-element kernel.
4
  // At each scan step, consecutive threads access consecutive inner indices. The
5
  // accumulation order is element-by-element in scan order.
 
 
 
 
 
 
 
 
6
  {{ env.wgsl.resourceDeclarations }}
7
 
8
  const IDENTITY: {{ accType }} = {% if accType == "f32" %}1.0{% elif accType == "i32" %}1i{% else %}1u{% endif %};
@@ -13,12 +21,7 @@ fn combine(a: {{ accType }}, b: {{ accType }}) -> {{ accType }} {
13
 
14
  @compute @workgroup_size({{ workgroupSize }}, 1, 1)
15
  fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
16
- // 2D-folded flat row index: gid.y carries the high bits past the
17
- // per-axis dispatch fold width (rows > 16.7M).
18
- let r = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ workgroupSize }}u;
19
- if (r >= params.rows) {
20
- return;
21
- }
22
  let outer_i = r / params.inner;
23
  let inner_i = r % params.inner;
24
  let base = outer_i * params.dim * params.inner + inner_i;
 
3
  // is O(numel), versus O(numel * dim) for a naive per-output-element kernel.
4
  // At each scan step, consecutive threads access consecutive inner indices. The
5
  // accumulation order is element-by-element in scan order.
6
+ {% macro flat_index_2d(workgroupSize, name="i", bound="params.count", guardInline=false) %}
7
+ {% set wgTerm = workgroupSize ~ "u" if workgroupSize is number else workgroupSize %}
8
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
9
+ // per-axis workgroup fold width.
10
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ wgTerm }};
11
+ if ({{ name }} >= {{ bound }}) {
12
+ return;
13
+ }{% endmacro %}
14
  {{ env.wgsl.resourceDeclarations }}
15
 
16
  const IDENTITY: {{ accType }} = {% if accType == "f32" %}1.0{% elif accType == "i32" %}1i{% else %}1u{% endif %};
 
21
 
22
  @compute @workgroup_size({{ workgroupSize }}, 1, 1)
23
  fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
24
+ {{ flat_index_2d(workgroupSize, "r", "params.rows") }}
 
 
 
 
 
25
  let outer_i = r / params.inner;
26
  let inner_i = r % params.inner;
27
  let base = outer_i * params.dim * params.inner + inner_i;
build/webgpu/test.json CHANGED
@@ -12,7 +12,7 @@
12
  {
13
  "name": "many_short_rows_axis1_portable_tiled_f32",
14
  "provenance": {
15
- "notes": "Many independent 64-element rows with nontrivial signed products exercise the capability-selected short-row route. Variable-width and minimum-limit tiers use the coalesced tiled path; compatible fixed-width subgroups use their native row scan."
16
  },
17
  "args": { "axis": 1 },
18
  "inputs": {
@@ -718,7 +718,7 @@
718
  {
719
  "name": "wg_scan_multichunk_pow2_f32",
720
  "provenance": {
721
- "notes": "Axis length 600 forces the chunk-carry path (3 chunks of 256) in the workgroup row-scan."
722
  },
723
  "args": { "axis": 1 },
724
  "inputs": { "x": { "dtype": "float32", "shape": [2, 600], "data": { "kind": "cycle", "values": [2.0, 0.5] } } },
@@ -848,7 +848,7 @@
848
  {
849
  "name": "clean_f16_last_axis_dim48_wg_scan",
850
  "provenance": {
851
- "notes": "f16 innermost-axis cumprod, dim=48 (>32, %4==0) selects wg_row_scan (no f16 vec4 path; accType=f32). Realistic f16 model doing a cumulative product over the last dim; near-1 pow2-friendly values keep the f32-accumulate-then-f16-store result within f16 tolerance of the f64 reference."
852
  },
853
  "args": { "axis": 1 },
854
  "inputs": {
@@ -862,9 +862,7 @@
862
  },
863
  {
864
  "name": "clean_f32_last_axis_nonmult4_dim50_scalar",
865
- "provenance": {
866
- "notes": "A 50-element innermost axis exceeds the short-row threshold but is not divisible by four, selecting the scalar workgroup row scan."
867
- },
868
  "args": { "axis": 1 },
869
  "inputs": {
870
  "x": {
@@ -878,7 +876,7 @@
878
  {
879
  "name": "clean_rank4_middle_axis_reverse_f32",
880
  "provenance": {
881
- "notes": "A rank-4 reverse CumProd over a middle axis exercises outer/dimension/inner index decomposition. With inner=32, workgroup row paths are ineligible and serial accumulation has an exact expected result."
882
  },
883
  "attrs": { "reverse": 1 },
884
  "args": { "axis": 1 },
 
12
  {
13
  "name": "many_short_rows_axis1_portable_tiled_f32",
14
  "provenance": {
15
+ "notes": "Many independent 64-element rows with nontrivial signed products check inclusive CumProd across device tiers."
16
  },
17
  "args": { "axis": 1 },
18
  "inputs": {
 
718
  {
719
  "name": "wg_scan_multichunk_pow2_f32",
720
  "provenance": {
721
+ "notes": "Axis length 600 (last axis) spans three 256-element chunks, two full and one of 88, requiring the running product to carry across chunk boundaries. Exact powers of two (2.0, 0.5) keep the cumulative product exact in float32."
722
  },
723
  "args": { "axis": 1 },
724
  "inputs": { "x": { "dtype": "float32", "shape": [2, 600], "data": { "kind": "cycle", "values": [2.0, 0.5] } } },
 
848
  {
849
  "name": "clean_f16_last_axis_dim48_wg_scan",
850
  "provenance": {
851
+ "notes": "Float16 innermost-axis cumulative product over 48 values cycling through 1, 0.5, and 2; the running product stays at 1 or 0.5, so every output is exact in float16."
852
  },
853
  "args": { "axis": 1 },
854
  "inputs": {
 
862
  },
863
  {
864
  "name": "clean_f32_last_axis_nonmult4_dim50_scalar",
865
+ "provenance": { "notes": "A 50-element innermost axis checks a partial four-lane tail in inclusive CumProd." },
 
 
866
  "args": { "axis": 1 },
867
  "inputs": {
868
  "x": {
 
876
  {
877
  "name": "clean_rank4_middle_axis_reverse_f32",
878
  "provenance": {
879
+ "notes": "A rank-4 reverse cumulative product over axis 1, with inner size 32 (the product of the two trailing dimensions), decomposes indices into outer, axis, and inner components. Powers-of-two values (1, 2, 0.5) give an exact expected result in float32."
880
  },
881
  "attrs": { "reverse": 1 },
882
  "args": { "axis": 1 },