Xenova HF Staff commited on
Commit
b4e7a99
·
verified ·
1 Parent(s): b6dbd82

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,20 +18,20 @@ See the [ONNX `TopK` spec](https://onnx.ai/onnx/operators/onnx__TopK.html) for t
18
 
19
  ## Inputs
20
 
21
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
- | `X` | `x` | `T` | — | — | Values from which the top `k` entries are selected along `axis`. | required |
24
 
25
  ## Outputs
26
 
27
- | Name | Bind key | Logical dtype | WebGPU storage | Rank | Shape | Description | Presence |
28
  | --- | --- | --- | --- | --- | --- | --- | --- |
29
- | `Values` | `values` | `T` | runtime-selected; narrow integers and bool use 32-bit slots | same as `X` | derived; see description | Selected values; the reduced axis has length `k`. | required |
30
- | `Indices` | `indices` | `I` | `uint32` | same as `X` | derived; see description | Logical int64 indices of the selected values along the reduced axis; WebGPU stores these bounded indices as uint32. | required |
31
 
32
  ## Runtime arguments
33
 
34
- | Name | Kind | Semantic | Description | Presence |
35
  | --- | --- | --- | --- | --- |
36
  | `k` | `u32` | `kernel.k` | Number of values to select along the configured axis. | required |
37
 
@@ -52,13 +52,33 @@ Default values (overridable per request):
52
  | `T` | `float32`, `float16`, `int8`, `int16`, `int32`, `uint8`, `uint32` |
53
  | `I` | `int64` |
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  ## Device requirements
56
 
57
  Some implementation variants require `subgroups`. These are route-specific capabilities, not package-wide requirements; availability also depends on the request shape and dtype.
58
 
59
  ## Files
60
 
61
- - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
62
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
63
  - [`test.json`](build/webgpu/test.json) — correctness cases
64
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
@@ -73,10 +93,14 @@ Some implementation variants require `subgroups`. These are route-specific capab
73
 
74
  ## Use with `@huggingface/kernels`
75
 
76
- The loader derives every required output's shape and logical dtype from the manifest contract and this call.
77
- It then allocates the result tensors automatically.
 
 
 
78
 
79
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
 
80
 
81
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
82
 
 
18
 
19
  ## Inputs
20
 
21
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
+ | `x` | `X` | `T` | — | — | Values from which the top `k` entries are selected along `axis`. | required |
24
 
25
  ## Outputs
26
 
27
+ | Name | Upstream name | Logical dtype | WebGPU storage | Rank | Shape | Description | Presence |
28
  | --- | --- | --- | --- | --- | --- | --- | --- |
29
+ | `values` | `Values` | `T` | runtime-selected; narrow integers and bool use 32-bit slots | same as `x` | derived | Selected values; the reduced axis has length `k`. | required |
30
+ | `indices` | `Indices` | `I` | `uint32` | same as `x` | derived | Logical int64 indices of the selected values along the reduced axis; WebGPU stores these bounded indices as uint32. | required |
31
 
32
  ## Runtime arguments
33
 
34
+ | Name | Kind | Upstream attribute | Description | Presence |
35
  | --- | --- | --- | --- | --- |
36
  | `k` | `u32` | `kernel.k` | Number of values to select along the configured axis. | required |
37
 
 
52
  | `T` | `float32`, `float16`, `int8`, `int16`, `int32`, `uint8`, `uint32` |
53
  | `I` | `int64` |
54
 
55
+ ## Implementation variants
56
+
57
+ One implementation is selected per call from the device capabilities, the request shapes and the dtypes; these notes say what each one covers.
58
+
59
+ - `axis_smallk_tournament` — Scans a strided non-last axis with one workgroup per output position and retains only a short candidate list instead of sorting the whole axis. It is favored when many independent outputs amortize the strided scan and remains the bounded-storage route when the axis does not fit shared memory.
60
+ - `last_axis_large_top1` — Finds one winner on rows too wide for the direct shared-memory route by reducing blocks to scratch candidates. A second pass selects the final candidate.
61
+ - `axis_bitonic` — Shared bitonic selection for arbitrary axes; floating inputs encode order keys once and gather original values after sorting, preserving stable ties and value bits.
62
+ - `subgroup_rows_smallk` — Retains only K candidates per row and selects a row-parallel reduction layout according to row width and available subgroup geometry. It avoids shared storage proportional to the full axis.
63
+ - `subgroup_min_rows_smallk` — Retains only K candidates per row and selects a row-parallel reduction layout according to row width and available subgroup geometry. It avoids shared storage proportional to the full axis.
64
+ - `portable_rows_smallk` — Retains only K candidates per row and selects a row-parallel reduction layout according to row width and available subgroup geometry. It avoids shared storage proportional to the full axis.
65
+ - `small_rows_batched` — Retains only K candidates per row and selects a row-parallel reduction layout according to row width and available subgroup geometry. It avoids shared storage proportional to the full axis.
66
+ - `last_axis_large_one_merge` — Staged stable TopK with bounded candidate buffers. Floating inputs encode order keys on load and gather selected source values after the final merge; integer inputs retain exact integer keys.
67
+ - `last_axis_large_two_merge` — Staged stable TopK with bounded candidate buffers. Floating inputs encode order keys on load and gather selected source values after the final merge; integer inputs retain exact integer keys.
68
+ - `last_axis_large_one_merge_int` — Staged stable TopK with bounded candidate buffers. Floating inputs encode order keys on load and gather selected source values after the final merge; integer inputs retain exact integer keys.
69
+ - `last_axis_large_two_merge_int` — Staged stable TopK with bounded candidate buffers. Floating inputs encode order keys on load and gather selected source values after the final merge; integer inputs retain exact integer keys.
70
+ - `axis_large_one_merge` — Staged stable TopK with bounded candidate buffers. Floating inputs encode order keys on load and gather selected source values after the final merge; integer inputs retain exact integer keys.
71
+ - `axis_large_two_merge` — Staged stable TopK with bounded candidate buffers. Floating inputs encode order keys on load and gather selected source values after the final merge; integer inputs retain exact integer keys.
72
+ - `last_axis_large_three_merge` — Staged stable TopK with bounded candidate buffers. Floating inputs encode order keys on load and gather selected source values after the final merge; integer inputs retain exact integer keys.
73
+ - `last_axis_large_three_merge_int` — Staged stable TopK with bounded candidate buffers. Floating inputs encode order keys on load and gather selected source values after the final merge; integer inputs retain exact integer keys.
74
+
75
  ## Device requirements
76
 
77
  Some implementation variants require `subgroups`. These are route-specific capabilities, not package-wide requirements; availability also depends on the request shape and dtype.
78
 
79
  ## Files
80
 
81
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
82
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
83
  - [`test.json`](build/webgpu/test.json) — correctness cases
84
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
 
93
 
94
  ## Use with `@huggingface/kernels`
95
 
96
+ ```sh
97
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.2
98
+ ```
99
+
100
+ Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
101
 
102
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
103
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
104
 
105
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
106
 
build/webgpu/bench.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.TopK",
3
  "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256] },
4
  "cases": [
5
  {
@@ -274,7 +273,7 @@
274
  }
275
  },
276
  {
277
- "name": "topk-axis1-f32-2048x256-k4-occupied-sibling",
278
  "preset": "smoke",
279
  "vars": { "rows": 2048, "cols": 256, "k": 4 },
280
  "args": { "k": 4 },
@@ -468,6 +467,393 @@
468
  { "name": "io", "type": "bandwidth", "value": "args.axis * args.inner * 4 + args.k * args.inner * 8" }
469
  ]
470
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  }
472
  ]
473
  }
 
1
  {
 
2
  "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256] },
3
  "cases": [
4
  {
 
273
  }
274
  },
275
  {
276
+ "name": "topk-axis1-f32-2048x256-k4-many-rows",
277
  "preset": "smoke",
278
  "vars": { "rows": 2048, "cols": 256, "k": 4 },
279
  "args": { "k": 4 },
 
467
  { "name": "io", "type": "bandwidth", "value": "args.axis * args.inner * 4 + args.k * args.inner * 8" }
468
  ]
469
  }
470
+ },
471
+ {
472
+ "name": "topk-router-f32-131072x8-k2-unsorted-contract",
473
+ "preset": "stress",
474
+ "provenance": {
475
+ "notes": "This router shape selects two values from each of 131,072 eight-element rows with sorted=0. The unspecified output order permits the batched small-row selection path without a full row sort."
476
+ },
477
+ "vars": { "rows": 131072, "cols": 8, "k": 2 },
478
+ "args": { "k": 2 },
479
+ "attrs": { "axis": -1, "largest": 1, "sorted": 0 },
480
+ "inputs": { "x": { "shape": [131072, 8], "dtype": "float32", "dist": "normal", "seed": 213, "scale": 2 } },
481
+ "outputs": {
482
+ "values": { "shape": [131072, 2], "dtype": "float32", "dist": "empty" },
483
+ "indices": { "shape": [131072, 2], "dtype": "uint32", "dist": "empty" }
484
+ },
485
+ "bench": {
486
+ "metrics": [
487
+ { "name": "io", "type": "bandwidth", "value": "args.rows * args.cols * 4 + args.rows * args.k * 8" }
488
+ ]
489
+ }
490
+ },
491
+ {
492
+ "name": "topk-vocab-f32-64x50257-k50-two-merge-pathology",
493
+ "preset": "stress",
494
+ "provenance": {
495
+ "notes": "64 GPT-2 vocabulary rows with k=50: 99 blocks of 512 produce 4950 candidates per row, above the 2048 merge chunk, so every row takes the two-merge large-axis route. Control: topk-vocab-f32-64x50257-k1-large-top1-control (same input bytes, the k=1 large-top1 route)."
496
+ },
497
+ "vars": { "rows": 64, "cols": 50257, "k": 50 },
498
+ "args": { "k": 50 },
499
+ "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
500
+ "inputs": { "x": { "shape": [64, 50257], "dtype": "float32", "dist": "normal", "seed": 218, "scale": 4 } },
501
+ "outputs": {
502
+ "values": { "shape": [64, 50], "dtype": "float32", "dist": "empty" },
503
+ "indices": { "shape": [64, 50], "dtype": "uint32", "dist": "empty" }
504
+ },
505
+ "bench": {
506
+ "metrics": [
507
+ {
508
+ "name": "io",
509
+ "type": "bandwidth",
510
+ "value": "args.rows * args.cols * 4 + args.rows * ceilDiv(args.cols, (512 if args.k <= 128 else 2048)) * args.k * 16 + args.rows * ceilDiv(ceilDiv(args.cols, (512 if args.k <= 128 else 2048)) * args.k, 2048) * args.k * 16 + args.rows * args.k * 8"
511
+ }
512
+ ]
513
+ }
514
+ },
515
+ {
516
+ "name": "topk-vocab-f32-64x50257-k1-large-top1-control",
517
+ "preset": "stress",
518
+ "provenance": {
519
+ "notes": "Sixty-four GPT-2 vocabulary rows with k=1 exercise a single block reduction followed by one combine pass."
520
+ },
521
+ "vars": { "rows": 64, "cols": 50257, "k": 1 },
522
+ "args": { "k": 1 },
523
+ "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
524
+ "inputs": { "x": { "shape": [64, 50257], "dtype": "float32", "dist": "normal", "seed": 219, "scale": 4 } },
525
+ "outputs": {
526
+ "values": { "shape": [64, 1], "dtype": "float32", "dist": "empty" },
527
+ "indices": { "shape": [64, 1], "dtype": "uint32", "dist": "empty" }
528
+ },
529
+ "bench": {
530
+ "metrics": [
531
+ {
532
+ "name": "io",
533
+ "type": "bandwidth",
534
+ "value": "args.rows * args.cols * 4 + args.rows * ceilDiv(args.cols, 512) * 8 + args.rows * args.k * 8"
535
+ }
536
+ ]
537
+ }
538
+ },
539
+ {
540
+ "name": "topk-router-f32-rank3-8x512x32-k2-batched-rows",
541
+ "provenance": {
542
+ "notes": "Shape [8,512,32] represents 4,096 independent 32-element rows. Flattening the leading dimensions allows the batched small-row path to select k=2 from each row."
543
+ },
544
+ "preset": "smoke",
545
+ "vars": { "rows": 4096, "cols": 32, "k": 2 },
546
+ "args": { "k": 2 },
547
+ "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
548
+ "inputs": { "x": { "shape": [8, 512, 32], "dtype": "float32", "dist": "normal", "seed": 119, "scale": 2 } },
549
+ "outputs": {
550
+ "values": { "shape": [8, 512, 2], "dtype": "float32" },
551
+ "indices": { "shape": [8, 512, 2], "dtype": "uint32" }
552
+ }
553
+ },
554
+ {
555
+ "name": "bitonic_prefix_boundary_float32_axis256_k17_largest0",
556
+ "preset": "stress",
557
+ "attrs": { "axis": -1, "largest": 0 },
558
+ "args": { "k": 17 },
559
+ "inputs": { "x": { "dtype": "float32", "shape": [64, 256], "dist": "normal", "seed": 1739 } },
560
+ "outputs": {
561
+ "values": { "dtype": "float32", "shape": [64, 17] },
562
+ "indices": { "dtype": "uint32", "shape": [64, 17] }
563
+ },
564
+ "bench": {
565
+ "metrics": [
566
+ {
567
+ "type": "bandwidth",
568
+ "value": "numel(shapes.x) * dtypeBytes(args.dtype) + numel(shapes.values) * (dtypeBytes(args.dtype) + 4)"
569
+ }
570
+ ]
571
+ },
572
+ "vars": { "dtype": "float32" }
573
+ },
574
+ {
575
+ "name": "bitonic_prefix_boundary_float32_axis256_k17_largest1",
576
+ "preset": "stress",
577
+ "attrs": { "axis": -1, "largest": 1 },
578
+ "args": { "k": 17 },
579
+ "inputs": { "x": { "dtype": "float32", "shape": [64, 256], "dist": "normal", "seed": 1739 } },
580
+ "outputs": {
581
+ "values": { "dtype": "float32", "shape": [64, 17] },
582
+ "indices": { "dtype": "uint32", "shape": [64, 17] }
583
+ },
584
+ "bench": {
585
+ "metrics": [
586
+ {
587
+ "type": "bandwidth",
588
+ "value": "numel(shapes.x) * dtypeBytes(args.dtype) + numel(shapes.values) * (dtypeBytes(args.dtype) + 4)"
589
+ }
590
+ ]
591
+ },
592
+ "vars": { "dtype": "float32" }
593
+ },
594
+ {
595
+ "name": "bitonic_prefix_boundary_float32_axis257_k17_largest0",
596
+ "preset": "stress",
597
+ "attrs": { "axis": -1, "largest": 0 },
598
+ "args": { "k": 17 },
599
+ "inputs": { "x": { "dtype": "float32", "shape": [64, 257], "dist": "normal", "seed": 1739 } },
600
+ "outputs": {
601
+ "values": { "dtype": "float32", "shape": [64, 17] },
602
+ "indices": { "dtype": "uint32", "shape": [64, 17] }
603
+ },
604
+ "bench": {
605
+ "metrics": [
606
+ {
607
+ "type": "bandwidth",
608
+ "value": "numel(shapes.x) * dtypeBytes(args.dtype) + numel(shapes.values) * (dtypeBytes(args.dtype) + 4)"
609
+ }
610
+ ]
611
+ },
612
+ "vars": { "dtype": "float32" }
613
+ },
614
+ {
615
+ "name": "bitonic_prefix_boundary_float32_axis257_k17_largest1",
616
+ "preset": "stress",
617
+ "attrs": { "axis": -1, "largest": 1 },
618
+ "args": { "k": 17 },
619
+ "inputs": { "x": { "dtype": "float32", "shape": [64, 257], "dist": "normal", "seed": 1739 } },
620
+ "outputs": {
621
+ "values": { "dtype": "float32", "shape": [64, 17] },
622
+ "indices": { "dtype": "uint32", "shape": [64, 17] }
623
+ },
624
+ "bench": {
625
+ "metrics": [
626
+ {
627
+ "type": "bandwidth",
628
+ "value": "numel(shapes.x) * dtypeBytes(args.dtype) + numel(shapes.values) * (dtypeBytes(args.dtype) + 4)"
629
+ }
630
+ ]
631
+ },
632
+ "vars": { "dtype": "float32" }
633
+ },
634
+ {
635
+ "name": "bitonic_prefix_boundary_float16_axis256_k17_largest0",
636
+ "preset": "stress",
637
+ "attrs": { "axis": -1, "largest": 0 },
638
+ "args": { "k": 17 },
639
+ "inputs": { "x": { "dtype": "float16", "shape": [64, 256], "dist": "normal", "seed": 1739 } },
640
+ "outputs": {
641
+ "values": { "dtype": "float16", "shape": [64, 17] },
642
+ "indices": { "dtype": "uint32", "shape": [64, 17] }
643
+ },
644
+ "bench": {
645
+ "metrics": [
646
+ {
647
+ "type": "bandwidth",
648
+ "value": "numel(shapes.x) * dtypeBytes(args.dtype) + numel(shapes.values) * (dtypeBytes(args.dtype) + 4)"
649
+ }
650
+ ]
651
+ },
652
+ "vars": { "dtype": "float16" }
653
+ },
654
+ {
655
+ "name": "bitonic_prefix_boundary_float16_axis256_k17_largest1",
656
+ "preset": "stress",
657
+ "attrs": { "axis": -1, "largest": 1 },
658
+ "args": { "k": 17 },
659
+ "inputs": { "x": { "dtype": "float16", "shape": [64, 256], "dist": "normal", "seed": 1739 } },
660
+ "outputs": {
661
+ "values": { "dtype": "float16", "shape": [64, 17] },
662
+ "indices": { "dtype": "uint32", "shape": [64, 17] }
663
+ },
664
+ "bench": {
665
+ "metrics": [
666
+ {
667
+ "type": "bandwidth",
668
+ "value": "numel(shapes.x) * dtypeBytes(args.dtype) + numel(shapes.values) * (dtypeBytes(args.dtype) + 4)"
669
+ }
670
+ ]
671
+ },
672
+ "vars": { "dtype": "float16" }
673
+ },
674
+ {
675
+ "name": "bitonic_prefix_boundary_float16_axis257_k17_largest0",
676
+ "preset": "stress",
677
+ "attrs": { "axis": -1, "largest": 0 },
678
+ "args": { "k": 17 },
679
+ "inputs": { "x": { "dtype": "float16", "shape": [64, 257], "dist": "normal", "seed": 1739 } },
680
+ "outputs": {
681
+ "values": { "dtype": "float16", "shape": [64, 17] },
682
+ "indices": { "dtype": "uint32", "shape": [64, 17] }
683
+ },
684
+ "bench": {
685
+ "metrics": [
686
+ {
687
+ "type": "bandwidth",
688
+ "value": "numel(shapes.x) * dtypeBytes(args.dtype) + numel(shapes.values) * (dtypeBytes(args.dtype) + 4)"
689
+ }
690
+ ]
691
+ },
692
+ "vars": { "dtype": "float16" }
693
+ },
694
+ {
695
+ "name": "bitonic_prefix_boundary_float16_axis257_k17_largest1",
696
+ "preset": "stress",
697
+ "attrs": { "axis": -1, "largest": 1 },
698
+ "args": { "k": 17 },
699
+ "inputs": { "x": { "dtype": "float16", "shape": [64, 257], "dist": "normal", "seed": 1739 } },
700
+ "outputs": {
701
+ "values": { "dtype": "float16", "shape": [64, 17] },
702
+ "indices": { "dtype": "uint32", "shape": [64, 17] }
703
+ },
704
+ "bench": {
705
+ "metrics": [
706
+ {
707
+ "type": "bandwidth",
708
+ "value": "numel(shapes.x) * dtypeBytes(args.dtype) + numel(shapes.values) * (dtypeBytes(args.dtype) + 4)"
709
+ }
710
+ ]
711
+ },
712
+ "vars": { "dtype": "float16" }
713
+ },
714
+ {
715
+ "name": "order-keys-float32-n257-axis0-k17",
716
+ "preset": "all",
717
+ "vars": { "rows": 7, "cols": 257, "k": 17 },
718
+ "args": { "k": 17 },
719
+ "attrs": { "axis": 0, "largest": 1, "sorted": 1 },
720
+ "inputs": { "x": { "dtype": "float32", "shape": [257, 7], "dist": "normal", "seed": 613, "scale": 2 } },
721
+ "outputs": {
722
+ "values": { "dtype": "float32", "shape": [17, 7] },
723
+ "indices": { "dtype": "uint32", "shape": [17, 7] }
724
+ },
725
+ "bench": {
726
+ "primary": true,
727
+ "metrics": [
728
+ { "name": "io", "type": "bandwidth", "value": "args.rows * args.cols * 4 + args.rows * args.k * 8" }
729
+ ]
730
+ }
731
+ },
732
+ {
733
+ "name": "order-keys-float32-n257-axis1-k17",
734
+ "preset": "all",
735
+ "vars": { "rows": 7, "cols": 257, "k": 17 },
736
+ "args": { "k": 17 },
737
+ "attrs": { "axis": 1, "largest": 1, "sorted": 1 },
738
+ "inputs": { "x": { "dtype": "float32", "shape": [7, 257], "dist": "normal", "seed": 613, "scale": 2 } },
739
+ "outputs": {
740
+ "values": { "dtype": "float32", "shape": [7, 17] },
741
+ "indices": { "dtype": "uint32", "shape": [7, 17] }
742
+ },
743
+ "bench": {
744
+ "primary": true,
745
+ "metrics": [
746
+ { "name": "io", "type": "bandwidth", "value": "args.rows * args.cols * 4 + args.rows * args.k * 8" }
747
+ ]
748
+ }
749
+ },
750
+ {
751
+ "name": "order-keys-float32-n4099-axis0-k17",
752
+ "preset": "all",
753
+ "vars": { "rows": 7, "cols": 4099, "k": 17 },
754
+ "args": { "k": 17 },
755
+ "attrs": { "axis": 0, "largest": 1, "sorted": 1 },
756
+ "inputs": { "x": { "dtype": "float32", "shape": [4099, 7], "dist": "normal", "seed": 613, "scale": 2 } },
757
+ "outputs": {
758
+ "values": { "dtype": "float32", "shape": [17, 7] },
759
+ "indices": { "dtype": "uint32", "shape": [17, 7] }
760
+ },
761
+ "bench": {
762
+ "primary": true,
763
+ "metrics": [
764
+ { "name": "io", "type": "bandwidth", "value": "args.rows * args.cols * 4 + args.rows * args.k * 8" }
765
+ ]
766
+ }
767
+ },
768
+ {
769
+ "name": "order-keys-float32-n4099-axis1-k17",
770
+ "preset": "all",
771
+ "vars": { "rows": 7, "cols": 4099, "k": 17 },
772
+ "args": { "k": 17 },
773
+ "attrs": { "axis": 1, "largest": 1, "sorted": 1 },
774
+ "inputs": { "x": { "dtype": "float32", "shape": [7, 4099], "dist": "normal", "seed": 613, "scale": 2 } },
775
+ "outputs": {
776
+ "values": { "dtype": "float32", "shape": [7, 17] },
777
+ "indices": { "dtype": "uint32", "shape": [7, 17] }
778
+ },
779
+ "bench": {
780
+ "primary": true,
781
+ "metrics": [
782
+ { "name": "io", "type": "bandwidth", "value": "args.rows * args.cols * 4 + args.rows * args.k * 8" }
783
+ ]
784
+ }
785
+ },
786
+ {
787
+ "name": "order-keys-float16-n257-axis0-k17",
788
+ "preset": "all",
789
+ "vars": { "rows": 7, "cols": 257, "k": 17 },
790
+ "args": { "k": 17 },
791
+ "attrs": { "axis": 0, "largest": 1, "sorted": 1 },
792
+ "inputs": { "x": { "dtype": "float16", "shape": [257, 7], "dist": "normal", "seed": 613, "scale": 2 } },
793
+ "outputs": {
794
+ "values": { "dtype": "float16", "shape": [17, 7] },
795
+ "indices": { "dtype": "uint32", "shape": [17, 7] }
796
+ },
797
+ "bench": {
798
+ "primary": true,
799
+ "metrics": [
800
+ { "name": "io", "type": "bandwidth", "value": "args.rows * args.cols * 2 + args.rows * args.k * 6" }
801
+ ]
802
+ }
803
+ },
804
+ {
805
+ "name": "order-keys-float16-n257-axis1-k17",
806
+ "preset": "all",
807
+ "vars": { "rows": 7, "cols": 257, "k": 17 },
808
+ "args": { "k": 17 },
809
+ "attrs": { "axis": 1, "largest": 1, "sorted": 1 },
810
+ "inputs": { "x": { "dtype": "float16", "shape": [7, 257], "dist": "normal", "seed": 613, "scale": 2 } },
811
+ "outputs": {
812
+ "values": { "dtype": "float16", "shape": [7, 17] },
813
+ "indices": { "dtype": "uint32", "shape": [7, 17] }
814
+ },
815
+ "bench": {
816
+ "primary": true,
817
+ "metrics": [
818
+ { "name": "io", "type": "bandwidth", "value": "args.rows * args.cols * 2 + args.rows * args.k * 6" }
819
+ ]
820
+ }
821
+ },
822
+ {
823
+ "name": "order-keys-float16-n4099-axis0-k17",
824
+ "preset": "all",
825
+ "vars": { "rows": 7, "cols": 4099, "k": 17 },
826
+ "args": { "k": 17 },
827
+ "attrs": { "axis": 0, "largest": 1, "sorted": 1 },
828
+ "inputs": { "x": { "dtype": "float16", "shape": [4099, 7], "dist": "normal", "seed": 613, "scale": 2 } },
829
+ "outputs": {
830
+ "values": { "dtype": "float16", "shape": [17, 7] },
831
+ "indices": { "dtype": "uint32", "shape": [17, 7] }
832
+ },
833
+ "bench": {
834
+ "primary": true,
835
+ "metrics": [
836
+ { "name": "io", "type": "bandwidth", "value": "args.rows * args.cols * 2 + args.rows * args.k * 6" }
837
+ ]
838
+ }
839
+ },
840
+ {
841
+ "name": "order-keys-float16-n4099-axis1-k17",
842
+ "preset": "all",
843
+ "vars": { "rows": 7, "cols": 4099, "k": 17 },
844
+ "args": { "k": 17 },
845
+ "attrs": { "axis": 1, "largest": 1, "sorted": 1 },
846
+ "inputs": { "x": { "dtype": "float16", "shape": [7, 4099], "dist": "normal", "seed": 613, "scale": 2 } },
847
+ "outputs": {
848
+ "values": { "dtype": "float16", "shape": [7, 17] },
849
+ "indices": { "dtype": "uint32", "shape": [7, 17] }
850
+ },
851
+ "bench": {
852
+ "primary": true,
853
+ "metrics": [
854
+ { "name": "io", "type": "bandwidth", "value": "args.rows * args.cols * 2 + args.rows * args.k * 6" }
855
+ ]
856
+ }
857
  }
858
  ]
859
  }
build/webgpu/manifest.json CHANGED
@@ -2,90 +2,65 @@
2
  "domain": "ai.onnx",
3
  "name": "TopK",
4
  "sinceVersion": 11,
5
- "description": "Retrieves the top-`k` largest or smallest elements along the selected axis, returning values and stable lower-index tie-breaking indices.",
6
- "inputs": [
7
- { "role": "X", "dtype": "T", "description": "Values from which the top `k` entries are selected along `axis`." }
8
- ],
9
- "outputs": [
10
- {
11
- "role": "Values",
12
  "dtype": "T",
13
- "rank": "ranks.X",
14
- "description": "Selected values; the reduced axis has length `k`.",
15
- "shape": "prefix(shapes.X, normalizedAxis) + [args.k] + suffix(shapes.X, normalizedAxis + 1)"
16
  },
17
- {
18
- "role": "Indices",
19
  "dtype": "I",
20
- "rank": "ranks.X",
21
- "description": "Logical int64 indices of the selected values along the reduced axis; WebGPU stores these bounded indices as uint32.",
22
- "shape": "prefix(shapes.X, normalizedAxis) + [args.k] + suffix(shapes.X, normalizedAxis + 1)"
23
  }
24
- ],
25
- "attributes": { "axis": -1, "largest": 1, "sorted": 1 },
26
- "attributeDescriptions": {
27
- "axis": "Axis to reduce; negative values count from the back.",
28
- "largest": "Select largest values when 1, smallest values when 0.",
29
- "sorted": "Sort selected values when 1. A sorted result is also valid when output order is unspecified (`sorted=0`)."
30
  },
 
 
31
  "attributeConstraints": { "largest": { "values": [0, 1] }, "sorted": { "values": [0, 1] } },
32
  "typeConstraints": { "T": ["float32", "float16", "int8", "int16", "int32", "uint8", "uint32"], "I": ["int64"] },
33
- "args": {
34
- "x": { "kind": "tensor", "semantic": "X", "role": "input" },
35
- "values": { "kind": "tensor", "semantic": "Values", "role": "output" },
36
- "indices": { "kind": "tensor", "semantic": "Indices", "role": "output", "dtype": "uint32" },
37
- "k": {
38
- "kind": "u32",
39
- "semantic": "kernel.k",
40
- "role": "attribute",
41
- "description": "Number of values to select along the configured axis."
42
- }
43
- },
44
  "tunables": {
45
- "WORKGROUP_SIZE": 256,
46
- "AXIS_SHARED_MAX_VALUES": 2048,
47
- "TOP1_BLOCK_SIZE": 512,
48
- "SMALL_K_BLOCK_SIZE": 512,
49
- "LARGE_K_BLOCK_SIZE": 2048,
50
- "SMALL_K_LIMIT": 128,
51
- "MERGE_CHUNK_SIZE": 2048,
52
- "SMALL_ROWS_MIN_ROWS": 4096,
53
- "SMALL_ROWS_MAX_AXIS": 64,
54
- "SMALL_ROWS_MAX_K": 8,
55
- "PORTABLE_ROW_WORKGROUP_SIZE": 32,
56
- "PORTABLE_ROWS_MIN": 1024,
57
- "PORTABLE_ROWS_MAX_AXIS": 256,
58
- "TOURNAMENT_MAX_AXIS": 4096,
59
- "TOURNAMENT_MAX_K": 16,
60
- "TOURNAMENT_MIN_OUTPUTS": 256
61
- },
62
- "tunableDescriptions": {
63
- "AXIS_SHARED_MAX_VALUES": "Caps the padded axis size admitted to the single-workgroup shared bitonic route; larger axes use staged block selection.",
64
- "MERGE_CHUNK_SIZE": "Caps the candidate count consumed by one large-axis merge workgroup; exceeding it adds another merge stage.",
65
- "SMALL_ROWS_MIN_ROWS": "Sets the minimum row count for the one-lane-per-row small-K route; `SMALL_ROWS_MAX_AXIS` and `SMALL_ROWS_MAX_K` bound each lane's serial work.",
66
- "PORTABLE_ROWS_MIN": "Sets the minimum row count for the portable small-K route, ensuring enough independent rows for cooperative per-row merging.",
67
- "TOURNAMENT_MIN_OUTPUTS": "Sets the minimum independent output count that retains the strided small-K tournament when shared bitonic sorting also fits."
68
  },
69
  "derive": {
70
  "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
71
- "foldedDispatchCapacity": "device.limits.maxComputeWorkgroupsPerDimension * device.limits.maxComputeWorkgroupsPerDimension",
72
  "wave32Adapter": "has(device.adapterInfo, \"subgroupMinSize\") and has(device.adapterInfo, \"subgroupMaxSize\") and device.adapterInfo.subgroupMinSize == 32 and device.adapterInfo.subgroupMaxSize == 32",
73
  "subgroupsWave32": "device.features.has(\"subgroups\") and wave32Adapter",
74
- "normalizedAxis": "attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.X",
75
- "axisInRange": "ranks.X >= 1 and normalizedAxis >= 0 and normalizedAxis < ranks.X",
76
- "axisOuter": "outer(shapes.X, normalizedAxis)",
77
- "axisDim": "dim(shapes.X, normalizedAxis)",
78
- "axisInner": "inner(shapes.X, normalizedAxis)",
79
  "outputPositions": "axisOuter * axisInner",
80
- "outputShapeOk": "ranks.Values == ranks.X and ranks.Indices == ranks.X and outer(shapes.Values, normalizedAxis) == axisOuter and outer(shapes.Indices, normalizedAxis) == axisOuter and dim(shapes.Values, normalizedAxis) == args.k and dim(shapes.Indices, normalizedAxis) == args.k and inner(shapes.Values, normalizedAxis) == axisInner and inner(shapes.Indices, normalizedAxis) == axisInner",
81
  "dtypeOk": "f16Ok(dtypes.T)",
82
  "shapeContract": "axisInRange and outputShapeOk and args.k <= axisDim",
83
  "baseContract": "shapeContract and dtypeOk",
84
- "lastAxis": "normalizedAxis == ranks.X - 1",
85
  "floatInput": "dtypes.T == \"f32\" or dtypes.T == \"f16\"",
86
- "workgroupSize": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
87
- "workgroupStorageFits": "workgroupSize * 8 <= device.limits.maxComputeWorkgroupStorageSize",
88
  "outputDispatchFits": "outputPositions <= foldedDispatchCapacity",
 
89
  "axisSharedSize": "pow2ceil(max(1, axisDim))",
90
  "axisSharedFits": "axisSharedSize <= tunables.AXIS_SHARED_MAX_VALUES and axisSharedSize * 8 <= device.limits.maxComputeWorkgroupStorageSize",
91
  "top1Blocks": "ceilDiv(axisDim, tunables.TOP1_BLOCK_SIZE)",
@@ -110,631 +85,216 @@
110
  "secondMergeDispatchFits": "outputPositions * secondGroups <= foldedDispatchCapacity",
111
  "largeSharedFits": "largeBlockSize * 8 <= device.limits.maxComputeWorkgroupStorageSize and mergeChunkSize * 8 <= device.limits.maxComputeWorkgroupStorageSize"
112
  },
113
- "bindingSets": {
114
- "smallRows": [
115
- { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
116
- { "name": "values", "arg": "values", "semantic": "Values", "buffer": { "type": "storage" }, "elementType": "$T" },
117
- {
118
- "name": "indices",
119
- "arg": "indices",
120
- "semantic": "Indices",
121
- "buffer": { "type": "storage" },
122
- "elementType": "$I"
123
- },
124
- {
125
- "name": "params",
126
- "semantic": "kernel.params",
127
- "buffer": { "type": "uniform" },
128
- "struct": {
129
- "name": "Params",
130
- "fields": [
131
- { "name": "rows", "type": "u32", "value": "outputPositions" },
132
- { "name": "cols", "type": "u32", "value": "axisDim" },
133
- { "name": "k", "type": "u32", "value": "args.k" }
134
- ]
135
- }
136
- }
137
- ],
138
- "axisGeometry": [
139
- { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
140
- { "name": "values", "arg": "values", "semantic": "Values", "buffer": { "type": "storage" }, "elementType": "$T" },
141
- {
142
- "name": "indices",
143
- "arg": "indices",
144
- "semantic": "Indices",
145
- "buffer": { "type": "storage" },
146
- "elementType": "$I"
147
- },
148
- {
149
- "name": "params",
150
- "semantic": "kernel.params",
151
- "buffer": { "type": "uniform" },
152
- "struct": {
153
- "name": "Params",
154
- "fields": [
155
- { "name": "outputPositions", "type": "u32", "value": "outputPositions" },
156
- { "name": "axis", "type": "u32", "value": "axisDim" },
157
- { "name": "inner", "type": "u32", "value": "axisInner" },
158
- { "name": "k", "type": "u32", "value": "args.k" }
159
- ]
160
- }
161
- }
162
- ],
163
- "directTop1": [
164
- { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
165
- { "name": "values", "arg": "values", "semantic": "Values", "buffer": { "type": "storage" }, "elementType": "$T" },
166
- {
167
- "name": "indices",
168
- "arg": "indices",
169
- "semantic": "Indices",
170
- "buffer": { "type": "storage" },
171
- "elementType": "$I"
172
- },
173
- {
174
- "name": "params",
175
- "semantic": "kernel.params",
176
- "buffer": { "type": "uniform" },
177
- "struct": {
178
- "name": "Params",
179
- "fields": [
180
- { "name": "rows", "type": "u32", "value": "outputPositions" },
181
- { "name": "cols", "type": "u32", "value": "axisDim" }
182
- ]
183
- }
184
- }
185
- ],
186
- "top1Block": [
187
- { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
188
- { "name": "candidateVals", "semantic": "candidateVals", "buffer": { "type": "storage" }, "elementType": "f32" },
189
- { "name": "candidateIdxs", "semantic": "candidateIdxs", "buffer": { "type": "storage" }, "elementType": "u32" },
190
- {
191
- "name": "params",
192
- "semantic": "kernel.params",
193
- "buffer": { "type": "uniform" },
194
- "struct": {
195
- "name": "Params",
196
- "fields": [
197
- { "name": "rows", "type": "u32", "value": "outputPositions" },
198
- { "name": "cols", "type": "u32", "value": "axisDim" },
199
- { "name": "blocks", "type": "u32", "value": "top1Blocks" }
200
- ]
201
- }
202
- }
203
- ],
204
- "top1Output": [
205
- {
206
- "name": "candidateVals",
207
- "semantic": "candidateVals",
208
- "buffer": { "type": "read-only-storage" },
209
- "elementType": "f32"
210
- },
211
- {
212
- "name": "candidateIdxs",
213
- "semantic": "candidateIdxs",
214
- "buffer": { "type": "read-only-storage" },
215
- "elementType": "u32"
216
- },
217
- { "name": "values", "arg": "values", "semantic": "Values", "buffer": { "type": "storage" }, "elementType": "$T" },
218
- {
219
- "name": "indices",
220
- "arg": "indices",
221
- "semantic": "Indices",
222
- "buffer": { "type": "storage" },
223
- "elementType": "$I"
224
- },
225
- {
226
- "name": "params",
227
- "semantic": "kernel.params",
228
- "buffer": { "type": "uniform" },
229
- "struct": {
230
- "name": "Params",
231
- "fields": [
232
- { "name": "rows", "type": "u32", "value": "outputPositions" },
233
- { "name": "blocks", "type": "u32", "value": "top1Blocks" }
234
- ]
235
- }
236
- }
237
- ],
238
- "largeBlock": [
239
- { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
240
- { "name": "candidateVals", "semantic": "candidateVals", "buffer": { "type": "storage" }, "elementType": "f32" },
241
- { "name": "candidateIdxs", "semantic": "candidateIdxs", "buffer": { "type": "storage" }, "elementType": "u32" },
242
- {
243
- "name": "params",
244
- "semantic": "kernel.params",
245
- "buffer": { "type": "uniform" },
246
- "struct": {
247
- "name": "Params",
248
- "fields": [
249
- { "name": "rows", "type": "u32", "value": "outputPositions" },
250
- { "name": "cols", "type": "u32", "value": "axisDim" },
251
- { "name": "k", "type": "u32", "value": "args.k" },
252
- { "name": "blocks", "type": "u32", "value": "largeBlockCount" }
253
- ]
254
- }
255
- }
256
- ],
257
- "mergeOutput": [
258
- {
259
- "name": "candidateVals",
260
- "semantic": "candidateVals",
261
- "buffer": { "type": "read-only-storage" },
262
- "elementType": "f32"
263
- },
264
- {
265
- "name": "candidateIdxs",
266
- "semantic": "candidateIdxs",
267
- "buffer": { "type": "read-only-storage" },
268
- "elementType": "u32"
269
- },
270
- { "name": "values", "arg": "values", "semantic": "Values", "buffer": { "type": "storage" }, "elementType": "$T" },
271
- {
272
- "name": "indices",
273
- "arg": "indices",
274
- "semantic": "Indices",
275
- "buffer": { "type": "storage" },
276
- "elementType": "$I"
277
- },
278
- {
279
- "name": "params",
280
- "semantic": "kernel.params",
281
- "buffer": { "type": "uniform" },
282
- "struct": {
283
- "name": "Params",
284
- "fields": [
285
- { "name": "rows", "type": "u32", "value": "outputPositions" },
286
- { "name": "inCandidates", "type": "u32", "value": "firstCandidates" },
287
- { "name": "k", "type": "u32", "value": "args.k" }
288
- ]
289
- }
290
- }
291
- ],
292
- "mergeScratch": [
293
- {
294
- "name": "candidateVals",
295
- "semantic": "candidateVals",
296
- "buffer": { "type": "read-only-storage" },
297
- "elementType": "f32"
298
- },
299
- {
300
- "name": "candidateIdxs",
301
- "semantic": "candidateIdxs",
302
- "buffer": { "type": "read-only-storage" },
303
- "elementType": "u32"
304
- },
305
- { "name": "candidateVals2", "semantic": "candidateVals2", "buffer": { "type": "storage" }, "elementType": "f32" },
306
- { "name": "candidateIdxs2", "semantic": "candidateIdxs2", "buffer": { "type": "storage" }, "elementType": "u32" },
307
- {
308
- "name": "params",
309
- "semantic": "kernel.params",
310
- "buffer": { "type": "uniform" },
311
- "struct": {
312
- "name": "Params",
313
- "fields": [
314
- { "name": "rows", "type": "u32", "value": "outputPositions" },
315
- { "name": "inCandidates", "type": "u32", "value": "firstCandidates" },
316
- { "name": "outGroups", "type": "u32", "value": "secondGroups" },
317
- { "name": "k", "type": "u32", "value": "args.k" }
318
- ]
319
- }
320
- }
321
- ],
322
- "mergeOutput2": [
323
- {
324
- "name": "candidateVals",
325
- "semantic": "candidateVals2",
326
- "buffer": { "type": "read-only-storage" },
327
- "elementType": "f32"
328
- },
329
- {
330
- "name": "candidateIdxs",
331
- "semantic": "candidateIdxs2",
332
- "buffer": { "type": "read-only-storage" },
333
- "elementType": "u32"
334
- },
335
- { "name": "values", "arg": "values", "semantic": "Values", "buffer": { "type": "storage" }, "elementType": "$T" },
336
- {
337
- "name": "indices",
338
- "arg": "indices",
339
- "semantic": "Indices",
340
- "buffer": { "type": "storage" },
341
- "elementType": "$I"
342
- },
343
- {
344
- "name": "params",
345
- "semantic": "kernel.params",
346
- "buffer": { "type": "uniform" },
347
- "struct": {
348
- "name": "Params",
349
- "fields": [
350
- { "name": "rows", "type": "u32", "value": "outputPositions" },
351
- { "name": "inCandidates", "type": "u32", "value": "secondCandidates" },
352
- { "name": "k", "type": "u32", "value": "args.k" }
353
- ]
354
- }
355
- }
356
- ],
357
- "largeBlockIntKeys": [
358
- { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
359
- { "name": "candidateVals", "semantic": "candidateVals", "buffer": { "type": "storage" }, "elementType": "u32" },
360
- { "name": "candidateIdxs", "semantic": "candidateIdxs", "buffer": { "type": "storage" }, "elementType": "u32" },
361
- {
362
- "name": "params",
363
- "semantic": "kernel.params",
364
- "buffer": { "type": "uniform" },
365
- "struct": {
366
- "name": "Params",
367
- "fields": [
368
- { "name": "rows", "type": "u32", "value": "outputPositions" },
369
- { "name": "cols", "type": "u32", "value": "axisDim" },
370
- { "name": "k", "type": "u32", "value": "args.k" },
371
- { "name": "blocks", "type": "u32", "value": "largeBlockCount" }
372
- ]
373
- }
374
- }
375
- ],
376
- "mergeOutputIntKeys": [
377
- {
378
- "name": "candidateVals",
379
- "semantic": "candidateVals",
380
- "buffer": { "type": "read-only-storage" },
381
- "elementType": "u32"
382
- },
383
- {
384
- "name": "candidateIdxs",
385
- "semantic": "candidateIdxs",
386
- "buffer": { "type": "read-only-storage" },
387
- "elementType": "u32"
388
- },
389
- { "name": "values", "arg": "values", "semantic": "Values", "buffer": { "type": "storage" }, "elementType": "$T" },
390
- {
391
- "name": "indices",
392
- "arg": "indices",
393
- "semantic": "Indices",
394
- "buffer": { "type": "storage" },
395
- "elementType": "$I"
396
- },
397
- {
398
- "name": "params",
399
- "semantic": "kernel.params",
400
- "buffer": { "type": "uniform" },
401
- "struct": {
402
- "name": "Params",
403
- "fields": [
404
- { "name": "rows", "type": "u32", "value": "outputPositions" },
405
- { "name": "inCandidates", "type": "u32", "value": "firstCandidates" },
406
- { "name": "k", "type": "u32", "value": "args.k" }
407
- ]
408
- }
409
- }
410
- ],
411
- "mergeScratchIntKeys": [
412
- {
413
- "name": "candidateVals",
414
- "semantic": "candidateVals",
415
- "buffer": { "type": "read-only-storage" },
416
- "elementType": "u32"
417
- },
418
- {
419
- "name": "candidateIdxs",
420
- "semantic": "candidateIdxs",
421
- "buffer": { "type": "read-only-storage" },
422
- "elementType": "u32"
423
- },
424
- { "name": "candidateVals2", "semantic": "candidateVals2", "buffer": { "type": "storage" }, "elementType": "u32" },
425
- { "name": "candidateIdxs2", "semantic": "candidateIdxs2", "buffer": { "type": "storage" }, "elementType": "u32" },
426
- {
427
- "name": "params",
428
- "semantic": "kernel.params",
429
- "buffer": { "type": "uniform" },
430
- "struct": {
431
- "name": "Params",
432
- "fields": [
433
- { "name": "rows", "type": "u32", "value": "outputPositions" },
434
- { "name": "inCandidates", "type": "u32", "value": "firstCandidates" },
435
- { "name": "outGroups", "type": "u32", "value": "secondGroups" },
436
- { "name": "k", "type": "u32", "value": "args.k" }
437
- ]
438
- }
439
- }
440
- ],
441
- "mergeOutput2IntKeys": [
442
- {
443
- "name": "candidateVals",
444
- "semantic": "candidateVals2",
445
- "buffer": { "type": "read-only-storage" },
446
- "elementType": "u32"
447
- },
448
- {
449
- "name": "candidateIdxs",
450
- "semantic": "candidateIdxs2",
451
- "buffer": { "type": "read-only-storage" },
452
- "elementType": "u32"
453
- },
454
- { "name": "values", "arg": "values", "semantic": "Values", "buffer": { "type": "storage" }, "elementType": "$T" },
455
- {
456
- "name": "indices",
457
- "arg": "indices",
458
- "semantic": "Indices",
459
- "buffer": { "type": "storage" },
460
- "elementType": "$I"
461
- },
462
- {
463
- "name": "params",
464
- "semantic": "kernel.params",
465
- "buffer": { "type": "uniform" },
466
- "struct": {
467
- "name": "Params",
468
- "fields": [
469
- { "name": "rows", "type": "u32", "value": "outputPositions" },
470
- { "name": "inCandidates", "type": "u32", "value": "secondCandidates" },
471
- { "name": "k", "type": "u32", "value": "args.k" }
472
- ]
473
- }
474
- }
475
- ],
476
- "mergeScratch2": [
477
- {
478
- "name": "candidateVals",
479
- "semantic": "candidateVals2",
480
- "buffer": { "type": "read-only-storage" },
481
- "elementType": "f32"
482
- },
483
- {
484
- "name": "candidateIdxs",
485
- "semantic": "candidateIdxs2",
486
- "buffer": { "type": "read-only-storage" },
487
- "elementType": "u32"
488
- },
489
- { "name": "candidateVals2", "semantic": "candidateVals3", "buffer": { "type": "storage" }, "elementType": "f32" },
490
- { "name": "candidateIdxs2", "semantic": "candidateIdxs3", "buffer": { "type": "storage" }, "elementType": "u32" },
491
- {
492
- "name": "params",
493
- "semantic": "kernel.params",
494
- "buffer": { "type": "uniform" },
495
- "struct": {
496
- "name": "Params",
497
- "fields": [
498
- { "name": "rows", "type": "u32", "value": "outputPositions" },
499
- { "name": "inCandidates", "type": "u32", "value": "secondCandidates" },
500
- { "name": "outGroups", "type": "u32", "value": "thirdGroups" },
501
- { "name": "k", "type": "u32", "value": "args.k" }
502
- ]
503
- }
504
- }
505
- ],
506
- "mergeOutput3": [
507
- {
508
- "name": "candidateVals",
509
- "semantic": "candidateVals3",
510
- "buffer": { "type": "read-only-storage" },
511
- "elementType": "f32"
512
- },
513
- {
514
- "name": "candidateIdxs",
515
- "semantic": "candidateIdxs3",
516
- "buffer": { "type": "read-only-storage" },
517
- "elementType": "u32"
518
- },
519
- { "name": "values", "arg": "values", "semantic": "Values", "buffer": { "type": "storage" }, "elementType": "$T" },
520
- {
521
- "name": "indices",
522
- "arg": "indices",
523
- "semantic": "Indices",
524
- "buffer": { "type": "storage" },
525
- "elementType": "$I"
526
- },
527
- {
528
- "name": "params",
529
- "semantic": "kernel.params",
530
- "buffer": { "type": "uniform" },
531
- "struct": {
532
- "name": "Params",
533
- "fields": [
534
- { "name": "rows", "type": "u32", "value": "outputPositions" },
535
- { "name": "inCandidates", "type": "u32", "value": "thirdCandidates" },
536
- { "name": "k", "type": "u32", "value": "args.k" }
537
- ]
538
- }
539
- }
540
- ],
541
- "mergeScratch2IntKeys": [
542
- {
543
- "name": "candidateVals",
544
- "semantic": "candidateVals2",
545
- "buffer": { "type": "read-only-storage" },
546
- "elementType": "u32"
547
- },
548
- {
549
- "name": "candidateIdxs",
550
- "semantic": "candidateIdxs2",
551
- "buffer": { "type": "read-only-storage" },
552
- "elementType": "u32"
553
- },
554
- { "name": "candidateVals2", "semantic": "candidateVals3", "buffer": { "type": "storage" }, "elementType": "u32" },
555
- { "name": "candidateIdxs2", "semantic": "candidateIdxs3", "buffer": { "type": "storage" }, "elementType": "u32" },
556
- {
557
- "name": "params",
558
- "semantic": "kernel.params",
559
- "buffer": { "type": "uniform" },
560
- "struct": {
561
- "name": "Params",
562
- "fields": [
563
- { "name": "rows", "type": "u32", "value": "outputPositions" },
564
- { "name": "inCandidates", "type": "u32", "value": "secondCandidates" },
565
- { "name": "outGroups", "type": "u32", "value": "thirdGroups" },
566
- { "name": "k", "type": "u32", "value": "args.k" }
567
- ]
568
- }
569
- }
570
- ],
571
- "mergeOutput3IntKeys": [
572
- {
573
- "name": "candidateVals",
574
- "semantic": "candidateVals3",
575
- "buffer": { "type": "read-only-storage" },
576
- "elementType": "u32"
577
- },
578
- {
579
- "name": "candidateIdxs",
580
- "semantic": "candidateIdxs3",
581
- "buffer": { "type": "read-only-storage" },
582
- "elementType": "u32"
583
- },
584
- { "name": "values", "arg": "values", "semantic": "Values", "buffer": { "type": "storage" }, "elementType": "$T" },
585
- {
586
- "name": "indices",
587
- "arg": "indices",
588
- "semantic": "Indices",
589
- "buffer": { "type": "storage" },
590
- "elementType": "$I"
591
- },
592
- {
593
- "name": "params",
594
- "semantic": "kernel.params",
595
- "buffer": { "type": "uniform" },
596
- "struct": {
597
- "name": "Params",
598
- "fields": [
599
- { "name": "rows", "type": "u32", "value": "outputPositions" },
600
- { "name": "inCandidates", "type": "u32", "value": "thirdCandidates" },
601
- { "name": "k", "type": "u32", "value": "args.k" }
602
- ]
603
- }
604
- }
605
- ],
606
- "largeBlockStrided": [
607
- { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
608
- { "name": "candidateVals", "semantic": "candidateVals", "buffer": { "type": "storage" }, "elementType": "f32" },
609
- { "name": "candidateIdxs", "semantic": "candidateIdxs", "buffer": { "type": "storage" }, "elementType": "u32" },
610
- {
611
- "name": "params",
612
- "semantic": "kernel.params",
613
- "buffer": { "type": "uniform" },
614
- "struct": {
615
- "name": "Params",
616
- "fields": [
617
- { "name": "rows", "type": "u32", "value": "outputPositions" },
618
- { "name": "cols", "type": "u32", "value": "axisDim" },
619
- { "name": "k", "type": "u32", "value": "args.k" },
620
- { "name": "inner", "type": "u32", "value": "axisInner" },
621
- { "name": "blocks", "type": "u32", "value": "largeBlockCount" }
622
- ]
623
- }
624
- }
625
- ],
626
- "mergeOutputStrided": [
627
- {
628
- "name": "candidateVals",
629
- "semantic": "candidateVals",
630
- "buffer": { "type": "read-only-storage" },
631
- "elementType": "f32"
632
- },
633
- {
634
- "name": "candidateIdxs",
635
- "semantic": "candidateIdxs",
636
- "buffer": { "type": "read-only-storage" },
637
- "elementType": "u32"
638
- },
639
- { "name": "values", "arg": "values", "semantic": "Values", "buffer": { "type": "storage" }, "elementType": "$T" },
640
- {
641
- "name": "indices",
642
- "arg": "indices",
643
- "semantic": "Indices",
644
- "buffer": { "type": "storage" },
645
- "elementType": "$I"
646
- },
647
- {
648
- "name": "params",
649
- "semantic": "kernel.params",
650
- "buffer": { "type": "uniform" },
651
- "struct": {
652
- "name": "Params",
653
- "fields": [
654
- { "name": "rows", "type": "u32", "value": "outputPositions" },
655
- { "name": "inCandidates", "type": "u32", "value": "firstCandidates" },
656
- { "name": "k", "type": "u32", "value": "args.k" },
657
- { "name": "inner", "type": "u32", "value": "axisInner" }
658
- ]
659
- }
660
- }
661
- ],
662
- "mergeOutput2Strided": [
663
- {
664
- "name": "candidateVals",
665
- "semantic": "candidateVals2",
666
- "buffer": { "type": "read-only-storage" },
667
- "elementType": "f32"
668
- },
669
- {
670
- "name": "candidateIdxs",
671
- "semantic": "candidateIdxs2",
672
- "buffer": { "type": "read-only-storage" },
673
- "elementType": "u32"
674
- },
675
- { "name": "values", "arg": "values", "semantic": "Values", "buffer": { "type": "storage" }, "elementType": "$T" },
676
- {
677
- "name": "indices",
678
- "arg": "indices",
679
- "semantic": "Indices",
680
- "buffer": { "type": "storage" },
681
- "elementType": "$I"
682
- },
683
- {
684
- "name": "params",
685
- "semantic": "kernel.params",
686
- "buffer": { "type": "uniform" },
687
- "struct": {
688
- "name": "Params",
689
- "fields": [
690
- { "name": "rows", "type": "u32", "value": "outputPositions" },
691
- { "name": "inCandidates", "type": "u32", "value": "secondCandidates" },
692
- { "name": "k", "type": "u32", "value": "args.k" },
693
- { "name": "inner", "type": "u32", "value": "axisInner" }
694
- ]
695
- }
696
- }
697
- ],
698
- "noopParams": [
699
- {
700
- "name": "params",
701
- "semantic": "kernel.params",
702
- "buffer": { "type": "uniform" },
703
- "struct": { "name": "Params", "fields": [{ "name": "dummy", "type": "u32", "value": 0 }] }
704
- }
705
- ]
706
  },
707
  "variants": [
708
  {
709
  "id": "axis_smallk_tournament",
710
- "description": "Scans a strided non-last axis with one workgroup per output position and retains only a short candidate list instead of sorting the whole axis. It is favored when many independent outputs amortize the strided scan and remains the bounded-storage route when the axis does not fit shared memory.",
711
  "priority": 25,
 
712
  "demoteWhen": ["axisSharedFits and not (axisInner > 1 and outputPositions >= tunables.TOURNAMENT_MIN_OUTPUTS)"],
713
- "when": ["baseContract", "attrs.sorted == 1", "not lastAxis", "args.k >= 1", "args.k <= tunables.TOURNAMENT_MAX_K", "axisDim <= tunables.TOURNAMENT_MAX_AXIS", "workgroupStorageFits", "outputDispatchFits"],
714
- "constants": {
715
  "scalar": "dtypes.T",
716
  "usesF16": "dtypes.T == \"f16\"",
717
- "localItems": "ceilDiv(axisDim, workgroupSize)"
 
718
  },
719
  "passes": [
720
  {
721
  "id": "main",
722
  "name": "TopK.AxisSmallKTournament",
723
- "source": { "shader": "topk-strided-smallk.wgsl.jinja", "inputs": { "nativeValues": true } },
724
- "bindings": "axisGeometry",
725
- "dispatch": { "workgroups": "outputPositions" }
 
726
  }
727
  ]
728
  },
729
  {
730
  "id": "last_axis_large_top1",
731
- "description": "Finds one winner on rows too wide for the direct shared-memory route by reducing blocks to scratch candidates. A second pass selects the final candidate.",
732
  "priority": 20,
733
  "when": ["baseContract", "floatInput", "lastAxis", "args.k == 1", "axisDim > tunables.AXIS_SHARED_MAX_VALUES", "workgroupStorageFits", "top1ScratchFits", "outputDispatchFits"],
734
- "constants": {
735
  "scalar": "dtypes.T",
736
  "usesF16": "dtypes.T == \"f16\"",
737
- "top1BlockSize": "tunables.TOP1_BLOCK_SIZE"
 
738
  },
739
  "intermediates": [
740
  { "id": "candidateVals", "dtype": "float32", "shape": "[top1ScratchElements]" },
@@ -744,41 +304,74 @@
744
  {
745
  "id": "block",
746
  "name": "TopK.LargeTop1Block",
747
- "source": {
748
- "shader": "topk-top1-last-axis.wgsl.jinja",
749
- "inputs": { "stage": "\"block\"", "nativeValues": false }
750
- },
751
- "bindings": "top1Block",
752
- "dispatch": { "workgroups": "outputPositions * top1Blocks" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
753
  },
754
  {
755
  "id": "output",
756
  "name": "TopK.LargeTop1Output",
757
- "source": {
758
- "shader": "topk-top1-last-axis.wgsl.jinja",
759
- "inputs": { "stage": "\"output\"", "nativeValues": false }
760
- },
761
- "bindings": "top1Output",
762
- "dispatch": { "workgroups": "outputPositions" }
 
 
 
 
 
 
 
 
 
 
763
  }
764
  ]
765
  },
766
  {
767
  "id": "last_axis_top1",
768
  "priority": 15,
769
- "supersededBy": ["last_axis_large_top1"],
770
  "when": ["baseContract", "lastAxis", "args.k == 1", "workgroupStorageFits", "outputDispatchFits"],
771
- "constants": { "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"" },
 
772
  "passes": [
773
  {
774
  "id": "main",
775
  "name": "TopK.LastAxisTop1",
776
- "source": {
777
- "shader": "topk-top1-last-axis.wgsl.jinja",
778
- "inputs": { "stage": "\"direct\"", "nativeValues": true }
779
- },
780
- "bindings": "directTop1",
781
- "dispatch": { "workgroups": "outputPositions" }
 
 
 
 
 
 
 
 
 
782
  }
783
  ]
784
  },
@@ -786,14 +379,24 @@
786
  "id": "axis_bitonic",
787
  "priority": 0,
788
  "when": ["baseContract", "axisSharedFits", "outputDispatchFits"],
789
- "constants": { "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"", "sharedSize": "axisSharedSize" },
 
 
 
 
 
 
790
  "passes": [
791
  {
792
  "id": "main",
793
  "name": "TopK.AxisBitonic",
794
  "shader": "topk-axis.wgsl.jinja",
795
- "bindings": "axisGeometry",
796
- "dispatch": { "workgroups": "0 if args.k == 0 else outputPositions" }
 
 
 
 
797
  }
798
  ]
799
  },
@@ -806,40 +409,56 @@
806
  "id": "noop",
807
  "name": "TopK.Top0",
808
  "shader": "topk-noop.wgsl.jinja",
809
- "bindings": "noopParams",
810
  "dispatch": { "x": 0 }
811
  }
812
  ]
813
  },
814
  {
815
  "id": "subgroup_rows_smallk",
816
- "description": "Retains only K candidates per row and selects a row-parallel reduction layout according to row width and available subgroup geometry. It avoids shared storage proportional to the full axis.",
817
  "priority": 35,
818
- "when": ["baseContract", "floatInput", "subgroupsWave32", "device.wgslLanguageFeatures.has(\"subgroup_id\")", "ranks.X == 2", "lastAxis", "attrs.sorted == 1", "args.k >= 1", "args.k <= tunables.SMALL_ROWS_MAX_K", "axisDim >= device.adapterInfo.subgroupMinSize * 4", "axisDim <= workgroupSize", "outputPositions >= workgroupSize * 4", "workgroupSize % device.adapterInfo.subgroupMinSize == 0", "outputDispatchFits"],
819
- "constants": {
820
  "scalar": "dtypes.T",
821
  "usesF16": "dtypes.T == \"f16\"",
822
  "subgroupWidth": "device.adapterInfo.subgroupMinSize",
823
- "subgroupsPerWorkgroup": "workgroupSize / device.adapterInfo.subgroupMinSize",
824
- "localItems": "ceilDiv(axisDim, device.adapterInfo.subgroupMinSize)"
 
825
  },
826
  "passes": [
827
  {
828
  "id": "main",
829
  "name": "TopK.SubgroupRowsSmallK",
830
- "source": { "shader": "topk-subgroup-rows.wgsl.jinja", "inputs": { "nativeValues": true } },
831
- "subgroupCollectivesWidth": 32,
832
- "bindings": "smallRows",
833
- "dispatch": { "workgroups": "ceilDiv(outputPositions, workgroupSize / device.adapterInfo.subgroupMinSize)" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
834
  }
835
  ]
836
  },
837
  {
838
  "id": "subgroup_min_rows_smallk",
839
- "description": "Retains only K candidates per row and selects a row-parallel reduction layout according to row width and available subgroup geometry. It avoids shared storage proportional to the full axis.",
840
  "priority": 35,
841
- "when": ["baseContract", "floatInput", "not subgroupsWave32", "device.features.has(\"subgroups\")", "device.wgslLanguageFeatures.has(\"subgroup_id\")", "has(device.adapterInfo, \"subgroupMinSize\")", "has(device.adapterInfo, \"subgroupMaxSize\")", "device.adapterInfo.subgroupMinSize >= 4", "device.adapterInfo.subgroupMinSize <= device.adapterInfo.subgroupMaxSize", "device.adapterInfo.subgroupMinSize <= device.limits.maxComputeInvocationsPerWorkgroup", "device.adapterInfo.subgroupMinSize <= device.limits.maxComputeWorkgroupSizeX", "ranks.X == 2", "lastAxis", "attrs.sorted == 1", "args.k >= 1", "args.k <= tunables.SMALL_ROWS_MAX_K", "axisDim > tunables.SMALL_ROWS_MAX_AXIS", "axisDim <= tunables.PORTABLE_ROWS_MAX_AXIS", "outputPositions >= device.adapterInfo.subgroupMinSize * 4", "outputDispatchFits"],
842
- "constants": {
843
  "scalar": "dtypes.T",
844
  "usesF16": "dtypes.T == \"f16\"",
845
  "workgroupSize": "device.adapterInfo.subgroupMinSize",
@@ -851,46 +470,88 @@
851
  {
852
  "id": "main",
853
  "name": "TopK.SubgroupMinRowsSmallK",
854
- "source": { "shader": "topk-subgroup-rows.wgsl.jinja", "inputs": { "nativeValues": true } },
855
- "bindings": "smallRows",
856
- "dispatch": { "workgroups": "outputPositions" }
 
 
 
 
 
 
 
 
 
 
 
 
 
857
  }
858
  ]
859
  },
860
  {
861
  "id": "portable_rows_smallk",
862
- "description": "Retains only K candidates per row and selects a row-parallel reduction layout according to row width and available subgroup geometry. It avoids shared storage proportional to the full axis.",
863
  "priority": 34,
864
- "when": ["baseContract", "floatInput", "not subgroupsWave32 or not device.wgslLanguageFeatures.has(\"subgroup_id\")", "ranks.X == 2", "lastAxis", "attrs.sorted == 1", "args.k >= 1", "args.k <= tunables.SMALL_ROWS_MAX_K", "axisDim > tunables.SMALL_ROWS_MAX_AXIS", "axisDim <= tunables.PORTABLE_ROWS_MAX_AXIS", "outputPositions >= tunables.PORTABLE_ROWS_MIN", "tunables.PORTABLE_ROW_WORKGROUP_SIZE <= device.limits.maxComputeInvocationsPerWorkgroup", "tunables.PORTABLE_ROW_WORKGROUP_SIZE <= device.limits.maxComputeWorkgroupSizeX", "tunables.PORTABLE_ROW_WORKGROUP_SIZE * 8 + 4 <= device.limits.maxComputeWorkgroupStorageSize", "outputDispatchFits"],
865
- "constants": {
866
  "scalar": "dtypes.T",
867
  "usesF16": "dtypes.T == \"f16\"",
868
  "rowWorkgroupSize": "tunables.PORTABLE_ROW_WORKGROUP_SIZE",
869
- "localItems": "ceilDiv(axisDim, tunables.PORTABLE_ROW_WORKGROUP_SIZE)"
 
870
  },
871
  "passes": [
872
  {
873
  "id": "main",
874
  "name": "TopK.PortableRowsSmallK",
875
- "source": { "shader": "topk-portable-rows-smallk.wgsl.jinja", "inputs": { "nativeValues": true } },
876
- "bindings": "smallRows",
877
- "dispatch": { "workgroups": "outputPositions" }
 
 
 
 
 
 
 
 
 
 
 
 
 
878
  }
879
  ]
880
  },
881
  {
882
  "id": "small_rows_batched",
883
- "description": "Retains only K candidates per row and selects a row-parallel reduction layout according to row width and available subgroup geometry. It avoids shared storage proportional to the full axis.",
884
  "priority": 30,
885
- "when": ["baseContract", "floatInput", "ranks.X == 2", "lastAxis", "attrs.sorted == 1", "args.k >= 1", "args.k <= tunables.SMALL_ROWS_MAX_K", "axisOuter >= tunables.SMALL_ROWS_MIN_ROWS", "axisDim <= tunables.SMALL_ROWS_MAX_AXIS", "outputDispatchFits"],
886
- "constants": { "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"" },
887
  "passes": [
888
  {
889
  "id": "main",
890
  "name": "TopK.SmallRowsBatched",
891
- "source": { "shader": "topk-small-rows-batched.wgsl.jinja", "inputs": { "nativeValues": true } },
892
- "bindings": "smallRows",
893
- "dispatch": { "threads": "outputPositions", "workgroupSize": "workgroupSize" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
894
  }
895
  ]
896
  },
@@ -898,26 +559,37 @@
898
  "id": "last_axis_large_one_merge",
899
  "priority": 10,
900
  "when": ["baseContract", "lastAxis", "args.k > 1", "axisDim > tunables.AXIS_SHARED_MAX_VALUES", "firstCandidates <= mergeChunkSize", "firstScratchFits", "largeSharedFits", "largeBlockDispatchFits", "outputDispatchFits", "floatInput"],
901
- "constants": { "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"", "blockSize": "largeBlockSize" },
 
 
 
 
 
 
902
  "intermediates": [
903
- { "id": "candidateVals", "dtype": "float32", "shape": "[firstScratchElements]" },
904
  { "id": "candidateIdxs", "dtype": "uint32", "shape": "[firstScratchElements]" }
905
  ],
906
  "passes": [
907
  {
908
  "id": "block",
909
  "name": "TopK.LargeBlock",
910
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"block\"" } },
911
- "bindings": "largeBlock",
912
- "dispatch": { "workgroups": "outputPositions * largeBlockCount" }
 
 
 
 
 
913
  },
914
  {
915
  "id": "merge",
916
  "name": "TopK.LargeMergeOutput",
917
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"output\"" } },
918
- "constants": { "sharedSize": "pow2ceil(firstCandidates)" },
919
- "bindings": "mergeOutput",
920
- "dispatch": { "workgroups": "outputPositions" }
921
  }
922
  ]
923
  },
@@ -925,36 +597,51 @@
925
  "id": "last_axis_large_two_merge",
926
  "priority": 10,
927
  "when": ["baseContract", "lastAxis", "args.k > 1", "axisDim > tunables.AXIS_SHARED_MAX_VALUES", "firstCandidates > mergeChunkSize", "secondCandidates <= mergeChunkSize", "firstScratchFits", "secondScratchFits", "largeSharedFits", "largeBlockDispatchFits", "secondMergeDispatchFits", "outputDispatchFits", "floatInput"],
928
- "constants": { "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"", "blockSize": "largeBlockSize" },
 
 
 
 
 
 
929
  "intermediates": [
930
- { "id": "candidateVals", "dtype": "float32", "shape": "[firstScratchElements]" },
931
  { "id": "candidateIdxs", "dtype": "uint32", "shape": "[firstScratchElements]" },
932
- { "id": "candidateVals2", "dtype": "float32", "shape": "[secondScratchElements]" },
933
  { "id": "candidateIdxs2", "dtype": "uint32", "shape": "[secondScratchElements]" }
934
  ],
935
  "passes": [
936
  {
937
  "id": "block",
938
  "name": "TopK.LargeBlock",
939
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"block\"" } },
940
- "bindings": "largeBlock",
941
- "dispatch": { "workgroups": "outputPositions * largeBlockCount" }
 
 
 
 
 
942
  },
943
  {
944
  "id": "merge_scratch",
945
  "name": "TopK.LargeMergeScratch",
946
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"scratch\"" } },
947
- "constants": { "sharedSize": "mergeChunkSize" },
948
- "bindings": "mergeScratch",
949
- "dispatch": { "workgroups": "outputPositions * secondGroups" }
 
 
 
 
950
  },
951
  {
952
  "id": "merge",
953
  "name": "TopK.LargeMergeOutput",
954
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"output\"" } },
955
- "constants": { "sharedSize": "pow2ceil(secondCandidates)" },
956
- "bindings": "mergeOutput2",
957
- "dispatch": { "workgroups": "outputPositions" }
958
  }
959
  ]
960
  },
@@ -962,7 +649,13 @@
962
  "id": "last_axis_large_one_merge_int",
963
  "priority": 10,
964
  "when": ["baseContract", "lastAxis", "args.k > 1", "axisDim > tunables.AXIS_SHARED_MAX_VALUES", "firstCandidates <= mergeChunkSize", "firstScratchFits", "largeSharedFits", "largeBlockDispatchFits", "outputDispatchFits", "not floatInput"],
965
- "constants": { "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"", "blockSize": "largeBlockSize" },
 
 
 
 
 
 
966
  "intermediates": [
967
  { "id": "candidateVals", "dtype": "uint32", "shape": "[firstScratchElements]" },
968
  { "id": "candidateIdxs", "dtype": "uint32", "shape": "[firstScratchElements]" }
@@ -971,17 +664,22 @@
971
  {
972
  "id": "block",
973
  "name": "TopK.LargeBlock",
974
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"block\"" } },
975
- "bindings": "largeBlockIntKeys",
976
- "dispatch": { "workgroups": "outputPositions * largeBlockCount" }
 
 
 
 
 
977
  },
978
  {
979
  "id": "merge",
980
  "name": "TopK.LargeMergeOutput",
981
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"output\"" } },
982
- "constants": { "sharedSize": "pow2ceil(firstCandidates)" },
983
- "bindings": "mergeOutputIntKeys",
984
- "dispatch": { "workgroups": "outputPositions" }
985
  }
986
  ]
987
  },
@@ -989,7 +687,13 @@
989
  "id": "last_axis_large_two_merge_int",
990
  "priority": 10,
991
  "when": ["baseContract", "lastAxis", "args.k > 1", "axisDim > tunables.AXIS_SHARED_MAX_VALUES", "firstCandidates > mergeChunkSize", "secondCandidates <= mergeChunkSize", "firstScratchFits", "secondScratchFits", "largeSharedFits", "largeBlockDispatchFits", "secondMergeDispatchFits", "outputDispatchFits", "not floatInput"],
992
- "constants": { "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"", "blockSize": "largeBlockSize" },
 
 
 
 
 
 
993
  "intermediates": [
994
  { "id": "candidateVals", "dtype": "uint32", "shape": "[firstScratchElements]" },
995
  { "id": "candidateIdxs", "dtype": "uint32", "shape": "[firstScratchElements]" },
@@ -1000,25 +704,34 @@
1000
  {
1001
  "id": "block",
1002
  "name": "TopK.LargeBlock",
1003
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"block\"" } },
1004
- "bindings": "largeBlockIntKeys",
1005
- "dispatch": { "workgroups": "outputPositions * largeBlockCount" }
 
 
 
 
 
1006
  },
1007
  {
1008
  "id": "merge_scratch",
1009
  "name": "TopK.LargeMergeScratch",
1010
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"scratch\"" } },
1011
- "constants": { "sharedSize": "mergeChunkSize" },
1012
- "bindings": "mergeScratchIntKeys",
1013
- "dispatch": { "workgroups": "outputPositions * secondGroups" }
 
 
 
 
1014
  },
1015
  {
1016
  "id": "merge",
1017
  "name": "TopK.LargeMergeOutput",
1018
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"output\"" } },
1019
- "constants": { "sharedSize": "pow2ceil(secondCandidates)" },
1020
- "bindings": "mergeOutput2IntKeys",
1021
- "dispatch": { "workgroups": "outputPositions" }
1022
  }
1023
  ]
1024
  },
@@ -1026,26 +739,37 @@
1026
  "id": "axis_large_one_merge",
1027
  "priority": 10,
1028
  "when": ["baseContract", "not lastAxis", "args.k >= 1", "axisDim > tunables.AXIS_SHARED_MAX_VALUES", "firstCandidates <= mergeChunkSize", "firstScratchFits", "largeSharedFits", "largeBlockDispatchFits", "outputDispatchFits", "floatInput"],
1029
- "constants": { "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"", "blockSize": "largeBlockSize" },
 
 
 
 
 
 
1030
  "intermediates": [
1031
- { "id": "candidateVals", "dtype": "float32", "shape": "[firstScratchElements]" },
1032
  { "id": "candidateIdxs", "dtype": "uint32", "shape": "[firstScratchElements]" }
1033
  ],
1034
  "passes": [
1035
  {
1036
  "id": "block",
1037
  "name": "TopK.LargeBlock",
1038
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"block\"", "strided": true } },
1039
- "bindings": "largeBlockStrided",
1040
- "dispatch": { "workgroups": "outputPositions * largeBlockCount" }
 
 
 
 
 
1041
  },
1042
  {
1043
  "id": "merge",
1044
  "name": "TopK.LargeMergeOutput",
1045
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"output\"", "strided": true } },
1046
- "constants": { "sharedSize": "pow2ceil(firstCandidates)" },
1047
- "bindings": "mergeOutputStrided",
1048
- "dispatch": { "workgroups": "outputPositions" }
1049
  }
1050
  ]
1051
  },
@@ -1053,36 +777,51 @@
1053
  "id": "axis_large_two_merge",
1054
  "priority": 10,
1055
  "when": ["baseContract", "not lastAxis", "args.k >= 1", "axisDim > tunables.AXIS_SHARED_MAX_VALUES", "firstCandidates > mergeChunkSize", "secondCandidates <= mergeChunkSize", "firstScratchFits", "secondScratchFits", "largeSharedFits", "largeBlockDispatchFits", "secondMergeDispatchFits", "outputDispatchFits", "floatInput"],
1056
- "constants": { "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"", "blockSize": "largeBlockSize" },
 
 
 
 
 
 
1057
  "intermediates": [
1058
- { "id": "candidateVals", "dtype": "float32", "shape": "[firstScratchElements]" },
1059
  { "id": "candidateIdxs", "dtype": "uint32", "shape": "[firstScratchElements]" },
1060
- { "id": "candidateVals2", "dtype": "float32", "shape": "[secondScratchElements]" },
1061
  { "id": "candidateIdxs2", "dtype": "uint32", "shape": "[secondScratchElements]" }
1062
  ],
1063
  "passes": [
1064
  {
1065
  "id": "block",
1066
  "name": "TopK.LargeBlock",
1067
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"block\"", "strided": true } },
1068
- "bindings": "largeBlockStrided",
1069
- "dispatch": { "workgroups": "outputPositions * largeBlockCount" }
 
 
 
 
 
1070
  },
1071
  {
1072
  "id": "merge_scratch",
1073
  "name": "TopK.LargeMergeScratch",
1074
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"scratch\"" } },
1075
- "constants": { "sharedSize": "mergeChunkSize" },
1076
- "bindings": "mergeScratch",
1077
- "dispatch": { "workgroups": "outputPositions * secondGroups" }
 
 
 
 
1078
  },
1079
  {
1080
  "id": "merge",
1081
  "name": "TopK.LargeMergeOutput",
1082
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"output\"", "strided": true } },
1083
- "constants": { "sharedSize": "pow2ceil(secondCandidates)" },
1084
- "bindings": "mergeOutput2Strided",
1085
- "dispatch": { "workgroups": "outputPositions" }
1086
  }
1087
  ]
1088
  },
@@ -1090,46 +829,65 @@
1090
  "id": "last_axis_large_three_merge",
1091
  "priority": 10,
1092
  "when": ["baseContract", "lastAxis", "args.k > 1", "axisDim > tunables.AXIS_SHARED_MAX_VALUES", "firstCandidates > mergeChunkSize", "secondCandidates > mergeChunkSize", "thirdCandidates <= mergeChunkSize", "firstScratchFits", "secondScratchFits", "thirdScratchFits", "largeSharedFits", "largeBlockDispatchFits", "secondMergeDispatchFits", "thirdMergeDispatchFits", "outputDispatchFits", "floatInput"],
1093
- "constants": { "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"", "blockSize": "largeBlockSize" },
 
 
 
 
 
 
1094
  "intermediates": [
1095
- { "id": "candidateVals", "dtype": "float32", "shape": "[firstScratchElements]" },
1096
  { "id": "candidateIdxs", "dtype": "uint32", "shape": "[firstScratchElements]" },
1097
- { "id": "candidateVals2", "dtype": "float32", "shape": "[secondScratchElements]" },
1098
  { "id": "candidateIdxs2", "dtype": "uint32", "shape": "[secondScratchElements]" },
1099
- { "id": "candidateVals3", "dtype": "float32", "shape": "[thirdScratchElements]" },
1100
  { "id": "candidateIdxs3", "dtype": "uint32", "shape": "[thirdScratchElements]" }
1101
  ],
1102
  "passes": [
1103
  {
1104
  "id": "block",
1105
  "name": "TopK.LargeBlock",
1106
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"block\"" } },
1107
- "bindings": "largeBlock",
1108
- "dispatch": { "workgroups": "outputPositions * largeBlockCount" }
 
 
 
 
 
1109
  },
1110
  {
1111
  "id": "merge_scratch",
1112
  "name": "TopK.LargeMergeScratch",
1113
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"scratch\"" } },
1114
- "constants": { "sharedSize": "mergeChunkSize" },
1115
- "bindings": "mergeScratch",
1116
- "dispatch": { "workgroups": "outputPositions * secondGroups" }
 
 
 
 
1117
  },
1118
  {
1119
  "id": "merge_scratch2",
1120
  "name": "TopK.LargeMergeScratch2",
1121
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"scratch\"" } },
1122
- "constants": { "sharedSize": "mergeChunkSize" },
1123
- "bindings": "mergeScratch2",
1124
- "dispatch": { "workgroups": "outputPositions * thirdGroups" }
 
 
 
 
1125
  },
1126
  {
1127
  "id": "merge",
1128
  "name": "TopK.LargeMergeOutput",
1129
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"output\"" } },
1130
- "constants": { "sharedSize": "pow2ceil(thirdCandidates)" },
1131
- "bindings": "mergeOutput3",
1132
- "dispatch": { "workgroups": "outputPositions" }
1133
  }
1134
  ]
1135
  },
@@ -1137,7 +895,13 @@
1137
  "id": "last_axis_large_three_merge_int",
1138
  "priority": 10,
1139
  "when": ["baseContract", "lastAxis", "args.k > 1", "axisDim > tunables.AXIS_SHARED_MAX_VALUES", "firstCandidates > mergeChunkSize", "secondCandidates > mergeChunkSize", "thirdCandidates <= mergeChunkSize", "firstScratchFits", "secondScratchFits", "thirdScratchFits", "largeSharedFits", "largeBlockDispatchFits", "secondMergeDispatchFits", "thirdMergeDispatchFits", "outputDispatchFits", "not floatInput"],
1140
- "constants": { "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"", "blockSize": "largeBlockSize" },
 
 
 
 
 
 
1141
  "intermediates": [
1142
  { "id": "candidateVals", "dtype": "uint32", "shape": "[firstScratchElements]" },
1143
  { "id": "candidateIdxs", "dtype": "uint32", "shape": "[firstScratchElements]" },
@@ -1150,33 +914,46 @@
1150
  {
1151
  "id": "block",
1152
  "name": "TopK.LargeBlock",
1153
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"block\"" } },
1154
- "bindings": "largeBlockIntKeys",
1155
- "dispatch": { "workgroups": "outputPositions * largeBlockCount" }
 
 
 
 
 
1156
  },
1157
  {
1158
  "id": "merge_scratch",
1159
  "name": "TopK.LargeMergeScratch",
1160
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"scratch\"" } },
1161
- "constants": { "sharedSize": "mergeChunkSize" },
1162
- "bindings": "mergeScratchIntKeys",
1163
- "dispatch": { "workgroups": "outputPositions * secondGroups" }
 
 
 
 
1164
  },
1165
  {
1166
  "id": "merge_scratch2",
1167
  "name": "TopK.LargeMergeScratch2",
1168
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"scratch\"" } },
1169
- "constants": { "sharedSize": "mergeChunkSize" },
1170
- "bindings": "mergeScratch2IntKeys",
1171
- "dispatch": { "workgroups": "outputPositions * thirdGroups" }
 
 
 
 
1172
  },
1173
  {
1174
  "id": "merge",
1175
  "name": "TopK.LargeMergeOutput",
1176
- "source": { "shader": "topk-large-block.wgsl.jinja", "inputs": { "stage": "\"output\"" } },
1177
- "constants": { "sharedSize": "pow2ceil(thirdCandidates)" },
1178
- "bindings": "mergeOutput3IntKeys",
1179
- "dispatch": { "workgroups": "outputPositions" }
1180
  }
1181
  ]
1182
  }
 
2
  "domain": "ai.onnx",
3
  "name": "TopK",
4
  "sinceVersion": 11,
5
+ "inputs": { "x": { "onnx": "X", "dtype": "T" } },
6
+ "outputs": {
7
+ "values": {
8
+ "onnx": "Values",
 
 
 
9
  "dtype": "T",
10
+ "rank": "ranks.x",
11
+ "shape": "prefix(shapes.x, normalizedAxis) + [args.k] + suffix(shapes.x, normalizedAxis + 1)"
 
12
  },
13
+ "indices": {
14
+ "onnx": "Indices",
15
  "dtype": "I",
16
+ "rank": "ranks.x",
17
+ "shape": "prefix(shapes.x, normalizedAxis) + [args.k] + suffix(shapes.x, normalizedAxis + 1)",
18
+ "storage": "uint32"
19
  }
 
 
 
 
 
 
20
  },
21
+ "args": { "k": { "kind": "u32", "onnx": "kernel.k" } },
22
+ "attributes": { "axis": { "default": -1 }, "largest": { "default": 1 }, "sorted": { "default": 1 } },
23
  "attributeConstraints": { "largest": { "values": [0, 1] }, "sorted": { "values": [0, 1] } },
24
  "typeConstraints": { "T": ["float32", "float16", "int8", "int16", "int32", "uint8", "uint32"], "I": ["int64"] },
 
 
 
 
 
 
 
 
 
 
 
25
  "tunables": {
26
+ "WORKGROUP_SIZE": { "default": 256 },
27
+ "AXIS_SHARED_MAX_VALUES": { "default": 2048 },
28
+ "TOP1_BLOCK_SIZE": { "default": 512 },
29
+ "SMALL_K_BLOCK_SIZE": { "default": 512 },
30
+ "LARGE_K_BLOCK_SIZE": { "default": 2048 },
31
+ "SMALL_K_LIMIT": { "default": 128 },
32
+ "MERGE_CHUNK_SIZE": { "default": 2048 },
33
+ "SMALL_ROWS_MIN_ROWS": { "default": 4096 },
34
+ "SMALL_ROWS_MAX_AXIS": { "default": 64 },
35
+ "SMALL_ROWS_MAX_K": { "default": 8 },
36
+ "PORTABLE_ROW_WORKGROUP_SIZE": { "default": 32 },
37
+ "PORTABLE_ROWS_MIN": { "default": 1024 },
38
+ "PORTABLE_ROWS_MAX_AXIS": { "default": 256 },
39
+ "TOURNAMENT_MAX_AXIS": { "default": 4096 },
40
+ "TOURNAMENT_MAX_K": { "default": 16 },
41
+ "TOURNAMENT_MIN_OUTPUTS": { "default": 256 }
 
 
 
 
 
 
 
42
  },
43
  "derive": {
44
  "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
45
+ "foldedDispatchCapacity": "min(device.limits.maxComputeWorkgroupsPerDimension, 65535) * min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
46
  "wave32Adapter": "has(device.adapterInfo, \"subgroupMinSize\") and has(device.adapterInfo, \"subgroupMaxSize\") and device.adapterInfo.subgroupMinSize == 32 and device.adapterInfo.subgroupMaxSize == 32",
47
  "subgroupsWave32": "device.features.has(\"subgroups\") and wave32Adapter",
48
+ "normalizedAxis": "attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.x",
49
+ "axisInRange": "ranks.x >= 1 and normalizedAxis >= 0 and normalizedAxis < ranks.x",
50
+ "axisOuter": "outer(shapes.x, normalizedAxis)",
51
+ "axisDim": "dim(shapes.x, normalizedAxis)",
52
+ "axisInner": "inner(shapes.x, normalizedAxis)",
53
  "outputPositions": "axisOuter * axisInner",
54
+ "outputShapeOk": "ranks.values == ranks.x and ranks.indices == ranks.x and outer(shapes.values, normalizedAxis) == axisOuter and outer(shapes.indices, normalizedAxis) == axisOuter and dim(shapes.values, normalizedAxis) == args.k and dim(shapes.indices, normalizedAxis) == args.k and inner(shapes.values, normalizedAxis) == axisInner and inner(shapes.indices, normalizedAxis) == axisInner",
55
  "dtypeOk": "f16Ok(dtypes.T)",
56
  "shapeContract": "axisInRange and outputShapeOk and args.k <= axisDim",
57
  "baseContract": "shapeContract and dtypeOk",
58
+ "lastAxis": "normalizedAxis == ranks.x - 1",
59
  "floatInput": "dtypes.T == \"f32\" or dtypes.T == \"f16\"",
60
+ "workgroupSizePlan": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
61
+ "workgroupStorageFits": "workgroupSizePlan * 8 <= device.limits.maxComputeWorkgroupStorageSize",
62
  "outputDispatchFits": "outputPositions <= foldedDispatchCapacity",
63
+ "sortRetainedSize": "pow2ceil(max(1, args.k))",
64
  "axisSharedSize": "pow2ceil(max(1, axisDim))",
65
  "axisSharedFits": "axisSharedSize <= tunables.AXIS_SHARED_MAX_VALUES and axisSharedSize * 8 <= device.limits.maxComputeWorkgroupStorageSize",
66
  "top1Blocks": "ceilDiv(axisDim, tunables.TOP1_BLOCK_SIZE)",
 
85
  "secondMergeDispatchFits": "outputPositions * secondGroups <= foldedDispatchCapacity",
86
  "largeSharedFits": "largeBlockSize * 8 <= device.limits.maxComputeWorkgroupStorageSize and mergeChunkSize * 8 <= device.limits.maxComputeWorkgroupStorageSize"
87
  },
88
+ "bindings": {
89
+ "x": { "buffer": "read-only-storage", "elementType": "$T" },
90
+ "values": { "buffer": "storage", "elementType": "$T" },
91
+ "indices": { "buffer": "storage", "elementType": "$I" },
92
+ "params": {
93
+ "buffer": "uniform",
94
+ "struct": [
95
+ { "name": "outputPositions", "type": "u32", "value": "outputPositions" },
96
+ { "name": "axis", "type": "u32", "value": "axisDim" },
97
+ { "name": "inner", "type": "u32", "value": "axisInner" },
98
+ { "name": "k", "type": "u32" }
99
+ ]
100
+ },
101
+ "candidateVals": { "buffer": "storage", "elementType": "f32" },
102
+ "candidateIdxs": { "buffer": "storage", "elementType": "u32" },
103
+ "candidateVals_2": { "name": "candidateVals", "buffer": "read-only-storage", "elementType": "f32" },
104
+ "candidateIdxs_2": { "name": "candidateIdxs", "buffer": "read-only-storage", "elementType": "u32" },
105
+ "params_7": {
106
+ "name": "params",
107
+ "buffer": "uniform",
108
+ "struct": [
109
+ { "name": "rows", "type": "u32", "value": "outputPositions" },
110
+ { "name": "cols", "type": "u32", "value": "axisDim" },
111
+ { "name": "k", "type": "u32" },
112
+ { "name": "blocks", "type": "u32", "value": "largeBlockCount" }
113
+ ]
114
+ },
115
+ "candidateVals_3": { "name": "candidateVals", "buffer": "storage", "elementType": "u32" },
116
+ "params_8": {
117
+ "name": "params",
118
+ "buffer": "uniform",
119
+ "struct": [
120
+ { "name": "rows", "type": "u32", "value": "outputPositions" },
121
+ { "name": "cols", "type": "u32", "value": "axisDim" },
122
+ { "name": "k", "type": "u32" },
123
+ { "name": "inner", "type": "u32", "value": "axisInner" },
124
+ { "name": "blocks", "type": "u32", "value": "largeBlockCount" }
125
+ ]
126
+ },
127
+ "params_9": {
128
+ "name": "params",
129
+ "buffer": "uniform",
130
+ "struct": [
131
+ { "name": "rows", "type": "u32", "value": "outputPositions" },
132
+ { "name": "inCandidates", "type": "u32", "value": "firstCandidates" },
133
+ { "name": "k", "type": "u32" }
134
+ ]
135
+ },
136
+ "candidateVals_4": { "name": "candidateVals", "buffer": "read-only-storage", "elementType": "u32" },
137
+ "candidateIdxs2": { "buffer": "storage", "elementType": "u32" },
138
+ "params_11": {
139
+ "name": "params",
140
+ "buffer": "uniform",
141
+ "struct": [
142
+ { "name": "rows", "type": "u32", "value": "outputPositions" },
143
+ { "name": "inCandidates", "type": "u32", "value": "firstCandidates" },
144
+ { "name": "outGroups", "type": "u32", "value": "secondGroups" },
145
+ { "name": "k", "type": "u32" }
146
+ ]
147
+ },
148
+ "candidateVals2_2": { "name": "candidateVals2", "buffer": "storage", "elementType": "u32" },
149
+ "candidateIdxs_3": {
150
+ "scratch": "candidateIdxs2",
151
+ "name": "candidateIdxs",
152
+ "buffer": "read-only-storage",
153
+ "elementType": "u32"
154
+ },
155
+ "params_12": {
156
+ "name": "params",
157
+ "buffer": "uniform",
158
+ "struct": [
159
+ { "name": "rows", "type": "u32", "value": "outputPositions" },
160
+ { "name": "inCandidates", "type": "u32", "value": "secondCandidates" },
161
+ { "name": "k", "type": "u32" }
162
+ ]
163
+ },
164
+ "candidateVals_6": {
165
+ "scratch": "candidateVals2",
166
+ "name": "candidateVals",
167
+ "buffer": "read-only-storage",
168
+ "elementType": "u32"
169
+ },
170
+ "candidateIdxs2_2": {
171
+ "scratch": "candidateIdxs3",
172
+ "name": "candidateIdxs2",
173
+ "buffer": "storage",
174
+ "elementType": "u32"
175
+ },
176
+ "params_14": {
177
+ "name": "params",
178
+ "buffer": "uniform",
179
+ "struct": [
180
+ { "name": "rows", "type": "u32", "value": "outputPositions" },
181
+ { "name": "inCandidates", "type": "u32", "value": "secondCandidates" },
182
+ { "name": "outGroups", "type": "u32", "value": "thirdGroups" },
183
+ { "name": "k", "type": "u32" }
184
+ ]
185
+ },
186
+ "candidateVals2_4": {
187
+ "scratch": "candidateVals3",
188
+ "name": "candidateVals2",
189
+ "buffer": "storage",
190
+ "elementType": "u32"
191
+ },
192
+ "candidateIdxs_4": {
193
+ "scratch": "candidateIdxs3",
194
+ "name": "candidateIdxs",
195
+ "buffer": "read-only-storage",
196
+ "elementType": "u32"
197
+ },
198
+ "params_15": {
199
+ "name": "params",
200
+ "buffer": "uniform",
201
+ "struct": [
202
+ { "name": "rows", "type": "u32", "value": "outputPositions" },
203
+ { "name": "inCandidates", "type": "u32", "value": "thirdCandidates" },
204
+ { "name": "k", "type": "u32" }
205
+ ]
206
+ },
207
+ "candidateVals_8": {
208
+ "scratch": "candidateVals3",
209
+ "name": "candidateVals",
210
+ "buffer": "read-only-storage",
211
+ "elementType": "u32"
212
+ },
213
+ "params_9_source": {
214
+ "name": "params",
215
+ "buffer": "uniform",
216
+ "struct": [
217
+ { "name": "rows", "type": "u32", "value": "outputPositions" },
218
+ { "name": "inCandidates", "type": "u32", "value": "firstCandidates" },
219
+ { "name": "k", "type": "u32" },
220
+ { "name": "cols", "type": "u32", "value": "axisDim" }
221
+ ]
222
+ },
223
+ "params_12_source": {
224
+ "name": "params",
225
+ "buffer": "uniform",
226
+ "struct": [
227
+ { "name": "rows", "type": "u32", "value": "outputPositions" },
228
+ { "name": "inCandidates", "type": "u32", "value": "secondCandidates" },
229
+ { "name": "k", "type": "u32" },
230
+ { "name": "cols", "type": "u32", "value": "axisDim" }
231
+ ]
232
+ },
233
+ "params_10_source": {
234
+ "name": "params",
235
+ "buffer": "uniform",
236
+ "struct": [
237
+ { "name": "rows", "type": "u32", "value": "outputPositions" },
238
+ { "name": "inCandidates", "type": "u32", "value": "firstCandidates" },
239
+ { "name": "k", "type": "u32" },
240
+ { "name": "inner", "type": "u32", "value": "axisInner" },
241
+ { "name": "cols", "type": "u32", "value": "axisDim" }
242
+ ]
243
+ },
244
+ "params_13_source": {
245
+ "name": "params",
246
+ "buffer": "uniform",
247
+ "struct": [
248
+ { "name": "rows", "type": "u32", "value": "outputPositions" },
249
+ { "name": "inCandidates", "type": "u32", "value": "secondCandidates" },
250
+ { "name": "k", "type": "u32" },
251
+ { "name": "inner", "type": "u32", "value": "axisInner" },
252
+ { "name": "cols", "type": "u32", "value": "axisDim" }
253
+ ]
254
+ },
255
+ "params_15_source": {
256
+ "name": "params",
257
+ "buffer": "uniform",
258
+ "struct": [
259
+ { "name": "rows", "type": "u32", "value": "outputPositions" },
260
+ { "name": "inCandidates", "type": "u32", "value": "thirdCandidates" },
261
+ { "name": "k", "type": "u32" },
262
+ { "name": "cols", "type": "u32", "value": "axisDim" }
263
+ ]
264
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  },
266
  "variants": [
267
  {
268
  "id": "axis_smallk_tournament",
 
269
  "priority": 25,
270
+ "when": ["baseContract", "not lastAxis", "args.k >= 1", "args.k <= tunables.TOURNAMENT_MAX_K", "axisDim <= tunables.TOURNAMENT_MAX_AXIS", "workgroupStorageFits", "outputDispatchFits"],
271
  "demoteWhen": ["axisSharedFits and not (axisInner > 1 and outputPositions >= tunables.TOURNAMENT_MIN_OUTPUTS)"],
272
+ "derive": {
 
273
  "scalar": "dtypes.T",
274
  "usesF16": "dtypes.T == \"f16\"",
275
+ "localItems": "ceilDiv(axisDim, workgroupSizePlan)",
276
+ "workgroupSize": "workgroupSizePlan"
277
  },
278
  "passes": [
279
  {
280
  "id": "main",
281
  "name": "TopK.AxisSmallKTournament",
282
+ "shader": "topk-strided-smallk.wgsl.jinja",
283
+ "derive": { "nativeValues": true },
284
+ "bindings": ["x", "values", "indices", "params"],
285
+ "dispatch": { "x": "min(outputPositions, 65535)", "y": "ceilDiv(outputPositions, 65535)", "z": 1 }
286
  }
287
  ]
288
  },
289
  {
290
  "id": "last_axis_large_top1",
 
291
  "priority": 20,
292
  "when": ["baseContract", "floatInput", "lastAxis", "args.k == 1", "axisDim > tunables.AXIS_SHARED_MAX_VALUES", "workgroupStorageFits", "top1ScratchFits", "outputDispatchFits"],
293
+ "derive": {
294
  "scalar": "dtypes.T",
295
  "usesF16": "dtypes.T == \"f16\"",
296
+ "top1BlockSize": "tunables.TOP1_BLOCK_SIZE",
297
+ "workgroupSize": "workgroupSizePlan"
298
  },
299
  "intermediates": [
300
  { "id": "candidateVals", "dtype": "float32", "shape": "[top1ScratchElements]" },
 
304
  {
305
  "id": "block",
306
  "name": "TopK.LargeTop1Block",
307
+ "shader": "topk-top1-last-axis.wgsl.jinja",
308
+ "derive": { "stage": "\"block\"", "nativeValues": false },
309
+ "bindings": [
310
+ "x",
311
+ "candidateVals",
312
+ "candidateIdxs",
313
+ {
314
+ "name": "params",
315
+ "struct": [
316
+ { "name": "rows", "type": "u32", "value": "outputPositions" },
317
+ { "name": "cols", "type": "u32", "value": "axisDim" },
318
+ { "name": "blocks", "type": "u32", "value": "top1Blocks" }
319
+ ]
320
+ }
321
+ ],
322
+ "dispatch": {
323
+ "x": "min(outputPositions * top1Blocks, 65535)",
324
+ "y": "ceilDiv(outputPositions * top1Blocks, 65535)",
325
+ "z": 1
326
+ }
327
  },
328
  {
329
  "id": "output",
330
  "name": "TopK.LargeTop1Output",
331
+ "shader": "topk-top1-last-axis.wgsl.jinja",
332
+ "derive": { "stage": "\"output\"", "nativeValues": false },
333
+ "bindings": [
334
+ "candidateVals_2",
335
+ "candidateIdxs_2",
336
+ "values",
337
+ "indices",
338
+ {
339
+ "name": "params",
340
+ "struct": [
341
+ { "name": "rows", "type": "u32", "value": "outputPositions" },
342
+ { "name": "blocks", "type": "u32", "value": "top1Blocks" }
343
+ ]
344
+ }
345
+ ],
346
+ "dispatch": { "x": "min(outputPositions, 65535)", "y": "ceilDiv(outputPositions, 65535)", "z": 1 }
347
  }
348
  ]
349
  },
350
  {
351
  "id": "last_axis_top1",
352
  "priority": 15,
 
353
  "when": ["baseContract", "lastAxis", "args.k == 1", "workgroupStorageFits", "outputDispatchFits"],
354
+ "supersededBy": ["last_axis_large_top1"],
355
+ "derive": { "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"", "workgroupSize": "workgroupSizePlan" },
356
  "passes": [
357
  {
358
  "id": "main",
359
  "name": "TopK.LastAxisTop1",
360
+ "shader": "topk-top1-last-axis.wgsl.jinja",
361
+ "derive": { "stage": "\"direct\"", "nativeValues": true },
362
+ "bindings": [
363
+ "x",
364
+ "values",
365
+ "indices",
366
+ {
367
+ "name": "params",
368
+ "struct": [
369
+ { "name": "rows", "type": "u32", "value": "outputPositions" },
370
+ { "name": "cols", "type": "u32", "value": "axisDim" }
371
+ ]
372
+ }
373
+ ],
374
+ "dispatch": { "x": "min(outputPositions, 65535)", "y": "ceilDiv(outputPositions, 65535)", "z": 1 }
375
  }
376
  ]
377
  },
 
379
  "id": "axis_bitonic",
380
  "priority": 0,
381
  "when": ["baseContract", "axisSharedFits", "outputDispatchFits"],
382
+ "derive": {
383
+ "scalar": "dtypes.T",
384
+ "usesF16": "dtypes.T == \"f16\"",
385
+ "sharedSize": "axisSharedSize",
386
+ "workgroupSize": "workgroupSizePlan",
387
+ "floatOrderKeys": "floatInput"
388
+ },
389
  "passes": [
390
  {
391
  "id": "main",
392
  "name": "TopK.AxisBitonic",
393
  "shader": "topk-axis.wgsl.jinja",
394
+ "bindings": ["x", "values", "indices", "params"],
395
+ "dispatch": {
396
+ "x": "min(0 if args.k == 0 else outputPositions, 65535)",
397
+ "y": "ceilDiv(0 if args.k == 0 else outputPositions, 65535)",
398
+ "z": 1
399
+ }
400
  }
401
  ]
402
  },
 
409
  "id": "noop",
410
  "name": "TopK.Top0",
411
  "shader": "topk-noop.wgsl.jinja",
412
+ "bindings": [{ "name": "params", "struct": [{ "name": "dummy", "type": "u32", "value": 0 }] }],
413
  "dispatch": { "x": 0 }
414
  }
415
  ]
416
  },
417
  {
418
  "id": "subgroup_rows_smallk",
 
419
  "priority": 35,
420
+ "when": ["baseContract", "floatInput", "subgroupsWave32", "device.wgslLanguageFeatures.has(\"subgroup_id\")", "lastAxis", "args.k >= 1", "args.k <= tunables.SMALL_ROWS_MAX_K", "axisDim >= device.adapterInfo.subgroupMinSize * 4", "axisDim <= workgroupSizePlan", "outputPositions >= workgroupSizePlan * 4", "workgroupSizePlan % device.adapterInfo.subgroupMinSize == 0", "outputDispatchFits"],
421
+ "derive": {
422
  "scalar": "dtypes.T",
423
  "usesF16": "dtypes.T == \"f16\"",
424
  "subgroupWidth": "device.adapterInfo.subgroupMinSize",
425
+ "subgroupsPerWorkgroup": "workgroupSizePlan / device.adapterInfo.subgroupMinSize",
426
+ "localItems": "ceilDiv(axisDim, device.adapterInfo.subgroupMinSize)",
427
+ "workgroupSize": "workgroupSizePlan"
428
  },
429
  "passes": [
430
  {
431
  "id": "main",
432
  "name": "TopK.SubgroupRowsSmallK",
433
+ "shader": "topk-subgroup-rows.wgsl.jinja",
434
+ "derive": { "nativeValues": true },
435
+ "bindings": [
436
+ "x",
437
+ "values",
438
+ "indices",
439
+ {
440
+ "name": "params",
441
+ "struct": [
442
+ { "name": "rows", "type": "u32", "value": "outputPositions" },
443
+ { "name": "cols", "type": "u32", "value": "axisDim" },
444
+ { "name": "k", "type": "u32" }
445
+ ]
446
+ }
447
+ ],
448
+ "dispatch": {
449
+ "x": "min(ceilDiv(outputPositions, workgroupSize / device.adapterInfo.subgroupMinSize), 65535)",
450
+ "y": "ceilDiv(ceilDiv(outputPositions, workgroupSize / device.adapterInfo.subgroupMinSize), 65535)",
451
+ "z": 1
452
+ },
453
+ "subgroupCollectivesWidth": 32
454
  }
455
  ]
456
  },
457
  {
458
  "id": "subgroup_min_rows_smallk",
 
459
  "priority": 35,
460
+ "when": ["baseContract", "floatInput", "not subgroupsWave32", "device.features.has(\"subgroups\")", "device.wgslLanguageFeatures.has(\"subgroup_id\")", "has(device.adapterInfo, \"subgroupMinSize\")", "has(device.adapterInfo, \"subgroupMaxSize\")", "device.adapterInfo.subgroupMinSize >= 4", "device.adapterInfo.subgroupMinSize <= device.adapterInfo.subgroupMaxSize", "device.adapterInfo.subgroupMinSize <= device.limits.maxComputeInvocationsPerWorkgroup", "device.adapterInfo.subgroupMinSize <= device.limits.maxComputeWorkgroupSizeX", "lastAxis", "args.k >= 1", "args.k <= tunables.SMALL_ROWS_MAX_K", "axisDim > tunables.SMALL_ROWS_MAX_AXIS", "axisDim <= tunables.PORTABLE_ROWS_MAX_AXIS", "outputPositions >= device.adapterInfo.subgroupMinSize * 4", "outputDispatchFits"],
461
+ "derive": {
462
  "scalar": "dtypes.T",
463
  "usesF16": "dtypes.T == \"f16\"",
464
  "workgroupSize": "device.adapterInfo.subgroupMinSize",
 
470
  {
471
  "id": "main",
472
  "name": "TopK.SubgroupMinRowsSmallK",
473
+ "shader": "topk-subgroup-rows.wgsl.jinja",
474
+ "derive": { "nativeValues": true },
475
+ "bindings": [
476
+ "x",
477
+ "values",
478
+ "indices",
479
+ {
480
+ "name": "params",
481
+ "struct": [
482
+ { "name": "rows", "type": "u32", "value": "outputPositions" },
483
+ { "name": "cols", "type": "u32", "value": "axisDim" },
484
+ { "name": "k", "type": "u32" }
485
+ ]
486
+ }
487
+ ],
488
+ "dispatch": { "x": "min(outputPositions, 65535)", "y": "ceilDiv(outputPositions, 65535)", "z": 1 }
489
  }
490
  ]
491
  },
492
  {
493
  "id": "portable_rows_smallk",
 
494
  "priority": 34,
495
+ "when": ["baseContract", "floatInput", "not subgroupsWave32 or not device.wgslLanguageFeatures.has(\"subgroup_id\")", "lastAxis", "args.k >= 1", "args.k <= tunables.SMALL_ROWS_MAX_K", "axisDim > tunables.SMALL_ROWS_MAX_AXIS", "axisDim <= tunables.PORTABLE_ROWS_MAX_AXIS", "outputPositions >= tunables.PORTABLE_ROWS_MIN", "tunables.PORTABLE_ROW_WORKGROUP_SIZE <= device.limits.maxComputeInvocationsPerWorkgroup", "tunables.PORTABLE_ROW_WORKGROUP_SIZE <= device.limits.maxComputeWorkgroupSizeX", "tunables.PORTABLE_ROW_WORKGROUP_SIZE * 8 + 4 <= device.limits.maxComputeWorkgroupStorageSize", "outputDispatchFits"],
496
+ "derive": {
497
  "scalar": "dtypes.T",
498
  "usesF16": "dtypes.T == \"f16\"",
499
  "rowWorkgroupSize": "tunables.PORTABLE_ROW_WORKGROUP_SIZE",
500
+ "localItems": "ceilDiv(axisDim, tunables.PORTABLE_ROW_WORKGROUP_SIZE)",
501
+ "workgroupSize": "workgroupSizePlan"
502
  },
503
  "passes": [
504
  {
505
  "id": "main",
506
  "name": "TopK.PortableRowsSmallK",
507
+ "shader": "topk-portable-rows-smallk.wgsl.jinja",
508
+ "derive": { "nativeValues": true },
509
+ "bindings": [
510
+ "x",
511
+ "values",
512
+ "indices",
513
+ {
514
+ "name": "params",
515
+ "struct": [
516
+ { "name": "rows", "type": "u32", "value": "outputPositions" },
517
+ { "name": "cols", "type": "u32", "value": "axisDim" },
518
+ { "name": "k", "type": "u32" }
519
+ ]
520
+ }
521
+ ],
522
+ "dispatch": { "x": "min(outputPositions, 65535)", "y": "ceilDiv(outputPositions, 65535)", "z": 1 }
523
  }
524
  ]
525
  },
526
  {
527
  "id": "small_rows_batched",
 
528
  "priority": 30,
529
+ "when": ["baseContract", "floatInput", "lastAxis", "args.k >= 1", "args.k <= tunables.SMALL_ROWS_MAX_K", "axisOuter >= tunables.SMALL_ROWS_MIN_ROWS", "axisDim <= tunables.SMALL_ROWS_MAX_AXIS", "outputDispatchFits"],
530
+ "derive": { "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"", "workgroupSize": "workgroupSizePlan" },
531
  "passes": [
532
  {
533
  "id": "main",
534
  "name": "TopK.SmallRowsBatched",
535
+ "shader": "topk-small-rows-batched.wgsl.jinja",
536
+ "derive": { "nativeValues": true },
537
+ "bindings": [
538
+ "x",
539
+ "values",
540
+ "indices",
541
+ {
542
+ "name": "params",
543
+ "struct": [
544
+ { "name": "rows", "type": "u32", "value": "outputPositions" },
545
+ { "name": "cols", "type": "u32", "value": "axisDim" },
546
+ { "name": "k", "type": "u32" }
547
+ ]
548
+ }
549
+ ],
550
+ "dispatch": {
551
+ "x": "min(ceilDiv((outputPositions), (workgroupSize)), 65535)",
552
+ "y": "ceilDiv(ceilDiv((outputPositions), (workgroupSize)), 65535)",
553
+ "z": 1
554
+ }
555
  }
556
  ]
557
  },
 
559
  "id": "last_axis_large_one_merge",
560
  "priority": 10,
561
  "when": ["baseContract", "lastAxis", "args.k > 1", "axisDim > tunables.AXIS_SHARED_MAX_VALUES", "firstCandidates <= mergeChunkSize", "firstScratchFits", "largeSharedFits", "largeBlockDispatchFits", "outputDispatchFits", "floatInput"],
562
+ "derive": {
563
+ "scalar": "dtypes.T",
564
+ "usesF16": "dtypes.T == \"f16\"",
565
+ "blockSize": "largeBlockSize",
566
+ "workgroupSize": "workgroupSizePlan",
567
+ "floatOrderKeys": true
568
+ },
569
  "intermediates": [
570
+ { "id": "candidateVals", "dtype": "uint32", "shape": "[firstScratchElements]" },
571
  { "id": "candidateIdxs", "dtype": "uint32", "shape": "[firstScratchElements]" }
572
  ],
573
  "passes": [
574
  {
575
  "id": "block",
576
  "name": "TopK.LargeBlock",
577
+ "shader": "topk-large-block.wgsl.jinja",
578
+ "derive": { "stage": "\"block\"" },
579
+ "bindings": ["x", "candidateVals_3", "candidateIdxs", "params_7"],
580
+ "dispatch": {
581
+ "x": "min(outputPositions * largeBlockCount, 65535)",
582
+ "y": "ceilDiv(outputPositions * largeBlockCount, 65535)",
583
+ "z": 1
584
+ }
585
  },
586
  {
587
  "id": "merge",
588
  "name": "TopK.LargeMergeOutput",
589
+ "shader": "topk-large-block.wgsl.jinja",
590
+ "derive": { "sharedSize": "pow2ceil(firstCandidates)", "stage": "\"output\"" },
591
+ "bindings": ["candidateVals_4", "candidateIdxs_2", "values", "indices", "params_9_source", "x"],
592
+ "dispatch": { "x": "min(outputPositions, 65535)", "y": "ceilDiv(outputPositions, 65535)", "z": 1 }
593
  }
594
  ]
595
  },
 
597
  "id": "last_axis_large_two_merge",
598
  "priority": 10,
599
  "when": ["baseContract", "lastAxis", "args.k > 1", "axisDim > tunables.AXIS_SHARED_MAX_VALUES", "firstCandidates > mergeChunkSize", "secondCandidates <= mergeChunkSize", "firstScratchFits", "secondScratchFits", "largeSharedFits", "largeBlockDispatchFits", "secondMergeDispatchFits", "outputDispatchFits", "floatInput"],
600
+ "derive": {
601
+ "scalar": "dtypes.T",
602
+ "usesF16": "dtypes.T == \"f16\"",
603
+ "blockSize": "largeBlockSize",
604
+ "workgroupSize": "workgroupSizePlan",
605
+ "floatOrderKeys": true
606
+ },
607
  "intermediates": [
608
+ { "id": "candidateVals", "dtype": "uint32", "shape": "[firstScratchElements]" },
609
  { "id": "candidateIdxs", "dtype": "uint32", "shape": "[firstScratchElements]" },
610
+ { "id": "candidateVals2", "dtype": "uint32", "shape": "[secondScratchElements]" },
611
  { "id": "candidateIdxs2", "dtype": "uint32", "shape": "[secondScratchElements]" }
612
  ],
613
  "passes": [
614
  {
615
  "id": "block",
616
  "name": "TopK.LargeBlock",
617
+ "shader": "topk-large-block.wgsl.jinja",
618
+ "derive": { "stage": "\"block\"" },
619
+ "bindings": ["x", "candidateVals_3", "candidateIdxs", "params_7"],
620
+ "dispatch": {
621
+ "x": "min(outputPositions * largeBlockCount, 65535)",
622
+ "y": "ceilDiv(outputPositions * largeBlockCount, 65535)",
623
+ "z": 1
624
+ }
625
  },
626
  {
627
  "id": "merge_scratch",
628
  "name": "TopK.LargeMergeScratch",
629
+ "shader": "topk-large-block.wgsl.jinja",
630
+ "derive": { "sharedSize": "mergeChunkSize", "stage": "\"scratch\"" },
631
+ "bindings": ["candidateVals_4", "candidateIdxs_2", "candidateVals2_2", "candidateIdxs2", "params_11"],
632
+ "dispatch": {
633
+ "x": "min(outputPositions * secondGroups, 65535)",
634
+ "y": "ceilDiv(outputPositions * secondGroups, 65535)",
635
+ "z": 1
636
+ }
637
  },
638
  {
639
  "id": "merge",
640
  "name": "TopK.LargeMergeOutput",
641
+ "shader": "topk-large-block.wgsl.jinja",
642
+ "derive": { "sharedSize": "pow2ceil(secondCandidates)", "stage": "\"output\"" },
643
+ "bindings": ["candidateVals_6", "candidateIdxs_3", "values", "indices", "params_12_source", "x"],
644
+ "dispatch": { "x": "min(outputPositions, 65535)", "y": "ceilDiv(outputPositions, 65535)", "z": 1 }
645
  }
646
  ]
647
  },
 
649
  "id": "last_axis_large_one_merge_int",
650
  "priority": 10,
651
  "when": ["baseContract", "lastAxis", "args.k > 1", "axisDim > tunables.AXIS_SHARED_MAX_VALUES", "firstCandidates <= mergeChunkSize", "firstScratchFits", "largeSharedFits", "largeBlockDispatchFits", "outputDispatchFits", "not floatInput"],
652
+ "derive": {
653
+ "scalar": "dtypes.T",
654
+ "usesF16": "dtypes.T == \"f16\"",
655
+ "blockSize": "largeBlockSize",
656
+ "workgroupSize": "workgroupSizePlan",
657
+ "floatOrderKeys": false
658
+ },
659
  "intermediates": [
660
  { "id": "candidateVals", "dtype": "uint32", "shape": "[firstScratchElements]" },
661
  { "id": "candidateIdxs", "dtype": "uint32", "shape": "[firstScratchElements]" }
 
664
  {
665
  "id": "block",
666
  "name": "TopK.LargeBlock",
667
+ "shader": "topk-large-block.wgsl.jinja",
668
+ "derive": { "stage": "\"block\"" },
669
+ "bindings": ["x", "candidateVals_3", "candidateIdxs", "params_7"],
670
+ "dispatch": {
671
+ "x": "min(outputPositions * largeBlockCount, 65535)",
672
+ "y": "ceilDiv(outputPositions * largeBlockCount, 65535)",
673
+ "z": 1
674
+ }
675
  },
676
  {
677
  "id": "merge",
678
  "name": "TopK.LargeMergeOutput",
679
+ "shader": "topk-large-block.wgsl.jinja",
680
+ "derive": { "sharedSize": "pow2ceil(firstCandidates)", "stage": "\"output\"" },
681
+ "bindings": ["candidateVals_4", "candidateIdxs_2", "values", "indices", "params_9"],
682
+ "dispatch": { "x": "min(outputPositions, 65535)", "y": "ceilDiv(outputPositions, 65535)", "z": 1 }
683
  }
684
  ]
685
  },
 
687
  "id": "last_axis_large_two_merge_int",
688
  "priority": 10,
689
  "when": ["baseContract", "lastAxis", "args.k > 1", "axisDim > tunables.AXIS_SHARED_MAX_VALUES", "firstCandidates > mergeChunkSize", "secondCandidates <= mergeChunkSize", "firstScratchFits", "secondScratchFits", "largeSharedFits", "largeBlockDispatchFits", "secondMergeDispatchFits", "outputDispatchFits", "not floatInput"],
690
+ "derive": {
691
+ "scalar": "dtypes.T",
692
+ "usesF16": "dtypes.T == \"f16\"",
693
+ "blockSize": "largeBlockSize",
694
+ "workgroupSize": "workgroupSizePlan",
695
+ "floatOrderKeys": false
696
+ },
697
  "intermediates": [
698
  { "id": "candidateVals", "dtype": "uint32", "shape": "[firstScratchElements]" },
699
  { "id": "candidateIdxs", "dtype": "uint32", "shape": "[firstScratchElements]" },
 
704
  {
705
  "id": "block",
706
  "name": "TopK.LargeBlock",
707
+ "shader": "topk-large-block.wgsl.jinja",
708
+ "derive": { "stage": "\"block\"" },
709
+ "bindings": ["x", "candidateVals_3", "candidateIdxs", "params_7"],
710
+ "dispatch": {
711
+ "x": "min(outputPositions * largeBlockCount, 65535)",
712
+ "y": "ceilDiv(outputPositions * largeBlockCount, 65535)",
713
+ "z": 1
714
+ }
715
  },
716
  {
717
  "id": "merge_scratch",
718
  "name": "TopK.LargeMergeScratch",
719
+ "shader": "topk-large-block.wgsl.jinja",
720
+ "derive": { "sharedSize": "mergeChunkSize", "stage": "\"scratch\"" },
721
+ "bindings": ["candidateVals_4", "candidateIdxs_2", "candidateVals2_2", "candidateIdxs2", "params_11"],
722
+ "dispatch": {
723
+ "x": "min(outputPositions * secondGroups, 65535)",
724
+ "y": "ceilDiv(outputPositions * secondGroups, 65535)",
725
+ "z": 1
726
+ }
727
  },
728
  {
729
  "id": "merge",
730
  "name": "TopK.LargeMergeOutput",
731
+ "shader": "topk-large-block.wgsl.jinja",
732
+ "derive": { "sharedSize": "pow2ceil(secondCandidates)", "stage": "\"output\"" },
733
+ "bindings": ["candidateVals_6", "candidateIdxs_3", "values", "indices", "params_12"],
734
+ "dispatch": { "x": "min(outputPositions, 65535)", "y": "ceilDiv(outputPositions, 65535)", "z": 1 }
735
  }
736
  ]
737
  },
 
739
  "id": "axis_large_one_merge",
740
  "priority": 10,
741
  "when": ["baseContract", "not lastAxis", "args.k >= 1", "axisDim > tunables.AXIS_SHARED_MAX_VALUES", "firstCandidates <= mergeChunkSize", "firstScratchFits", "largeSharedFits", "largeBlockDispatchFits", "outputDispatchFits", "floatInput"],
742
+ "derive": {
743
+ "scalar": "dtypes.T",
744
+ "usesF16": "dtypes.T == \"f16\"",
745
+ "blockSize": "largeBlockSize",
746
+ "workgroupSize": "workgroupSizePlan",
747
+ "floatOrderKeys": true
748
+ },
749
  "intermediates": [
750
+ { "id": "candidateVals", "dtype": "uint32", "shape": "[firstScratchElements]" },
751
  { "id": "candidateIdxs", "dtype": "uint32", "shape": "[firstScratchElements]" }
752
  ],
753
  "passes": [
754
  {
755
  "id": "block",
756
  "name": "TopK.LargeBlock",
757
+ "shader": "topk-large-block.wgsl.jinja",
758
+ "derive": { "stage": "\"block\"", "strided": true },
759
+ "bindings": ["x", "candidateVals_3", "candidateIdxs", "params_8"],
760
+ "dispatch": {
761
+ "x": "min(outputPositions * largeBlockCount, 65535)",
762
+ "y": "ceilDiv(outputPositions * largeBlockCount, 65535)",
763
+ "z": 1
764
+ }
765
  },
766
  {
767
  "id": "merge",
768
  "name": "TopK.LargeMergeOutput",
769
+ "shader": "topk-large-block.wgsl.jinja",
770
+ "derive": { "sharedSize": "pow2ceil(firstCandidates)", "stage": "\"output\"", "strided": true },
771
+ "bindings": ["candidateVals_4", "candidateIdxs_2", "values", "indices", "params_10_source", "x"],
772
+ "dispatch": { "x": "min(outputPositions, 65535)", "y": "ceilDiv(outputPositions, 65535)", "z": 1 }
773
  }
774
  ]
775
  },
 
777
  "id": "axis_large_two_merge",
778
  "priority": 10,
779
  "when": ["baseContract", "not lastAxis", "args.k >= 1", "axisDim > tunables.AXIS_SHARED_MAX_VALUES", "firstCandidates > mergeChunkSize", "secondCandidates <= mergeChunkSize", "firstScratchFits", "secondScratchFits", "largeSharedFits", "largeBlockDispatchFits", "secondMergeDispatchFits", "outputDispatchFits", "floatInput"],
780
+ "derive": {
781
+ "scalar": "dtypes.T",
782
+ "usesF16": "dtypes.T == \"f16\"",
783
+ "blockSize": "largeBlockSize",
784
+ "workgroupSize": "workgroupSizePlan",
785
+ "floatOrderKeys": true
786
+ },
787
  "intermediates": [
788
+ { "id": "candidateVals", "dtype": "uint32", "shape": "[firstScratchElements]" },
789
  { "id": "candidateIdxs", "dtype": "uint32", "shape": "[firstScratchElements]" },
790
+ { "id": "candidateVals2", "dtype": "uint32", "shape": "[secondScratchElements]" },
791
  { "id": "candidateIdxs2", "dtype": "uint32", "shape": "[secondScratchElements]" }
792
  ],
793
  "passes": [
794
  {
795
  "id": "block",
796
  "name": "TopK.LargeBlock",
797
+ "shader": "topk-large-block.wgsl.jinja",
798
+ "derive": { "stage": "\"block\"", "strided": true },
799
+ "bindings": ["x", "candidateVals_3", "candidateIdxs", "params_8"],
800
+ "dispatch": {
801
+ "x": "min(outputPositions * largeBlockCount, 65535)",
802
+ "y": "ceilDiv(outputPositions * largeBlockCount, 65535)",
803
+ "z": 1
804
+ }
805
  },
806
  {
807
  "id": "merge_scratch",
808
  "name": "TopK.LargeMergeScratch",
809
+ "shader": "topk-large-block.wgsl.jinja",
810
+ "derive": { "sharedSize": "mergeChunkSize", "stage": "\"scratch\"" },
811
+ "bindings": ["candidateVals_4", "candidateIdxs_2", "candidateVals2_2", "candidateIdxs2", "params_11"],
812
+ "dispatch": {
813
+ "x": "min(outputPositions * secondGroups, 65535)",
814
+ "y": "ceilDiv(outputPositions * secondGroups, 65535)",
815
+ "z": 1
816
+ }
817
  },
818
  {
819
  "id": "merge",
820
  "name": "TopK.LargeMergeOutput",
821
+ "shader": "topk-large-block.wgsl.jinja",
822
+ "derive": { "sharedSize": "pow2ceil(secondCandidates)", "stage": "\"output\"", "strided": true },
823
+ "bindings": ["candidateVals_6", "candidateIdxs_3", "values", "indices", "params_13_source", "x"],
824
+ "dispatch": { "x": "min(outputPositions, 65535)", "y": "ceilDiv(outputPositions, 65535)", "z": 1 }
825
  }
826
  ]
827
  },
 
829
  "id": "last_axis_large_three_merge",
830
  "priority": 10,
831
  "when": ["baseContract", "lastAxis", "args.k > 1", "axisDim > tunables.AXIS_SHARED_MAX_VALUES", "firstCandidates > mergeChunkSize", "secondCandidates > mergeChunkSize", "thirdCandidates <= mergeChunkSize", "firstScratchFits", "secondScratchFits", "thirdScratchFits", "largeSharedFits", "largeBlockDispatchFits", "secondMergeDispatchFits", "thirdMergeDispatchFits", "outputDispatchFits", "floatInput"],
832
+ "derive": {
833
+ "scalar": "dtypes.T",
834
+ "usesF16": "dtypes.T == \"f16\"",
835
+ "blockSize": "largeBlockSize",
836
+ "workgroupSize": "workgroupSizePlan",
837
+ "floatOrderKeys": true
838
+ },
839
  "intermediates": [
840
+ { "id": "candidateVals", "dtype": "uint32", "shape": "[firstScratchElements]" },
841
  { "id": "candidateIdxs", "dtype": "uint32", "shape": "[firstScratchElements]" },
842
+ { "id": "candidateVals2", "dtype": "uint32", "shape": "[secondScratchElements]" },
843
  { "id": "candidateIdxs2", "dtype": "uint32", "shape": "[secondScratchElements]" },
844
+ { "id": "candidateVals3", "dtype": "uint32", "shape": "[thirdScratchElements]" },
845
  { "id": "candidateIdxs3", "dtype": "uint32", "shape": "[thirdScratchElements]" }
846
  ],
847
  "passes": [
848
  {
849
  "id": "block",
850
  "name": "TopK.LargeBlock",
851
+ "shader": "topk-large-block.wgsl.jinja",
852
+ "derive": { "stage": "\"block\"" },
853
+ "bindings": ["x", "candidateVals_3", "candidateIdxs", "params_7"],
854
+ "dispatch": {
855
+ "x": "min(outputPositions * largeBlockCount, 65535)",
856
+ "y": "ceilDiv(outputPositions * largeBlockCount, 65535)",
857
+ "z": 1
858
+ }
859
  },
860
  {
861
  "id": "merge_scratch",
862
  "name": "TopK.LargeMergeScratch",
863
+ "shader": "topk-large-block.wgsl.jinja",
864
+ "derive": { "sharedSize": "mergeChunkSize", "stage": "\"scratch\"" },
865
+ "bindings": ["candidateVals_4", "candidateIdxs_2", "candidateVals2_2", "candidateIdxs2", "params_11"],
866
+ "dispatch": {
867
+ "x": "min(outputPositions * secondGroups, 65535)",
868
+ "y": "ceilDiv(outputPositions * secondGroups, 65535)",
869
+ "z": 1
870
+ }
871
  },
872
  {
873
  "id": "merge_scratch2",
874
  "name": "TopK.LargeMergeScratch2",
875
+ "shader": "topk-large-block.wgsl.jinja",
876
+ "derive": { "sharedSize": "mergeChunkSize", "stage": "\"scratch\"" },
877
+ "bindings": ["candidateVals_6", "candidateIdxs_3", "candidateVals2_4", "candidateIdxs2_2", "params_14"],
878
+ "dispatch": {
879
+ "x": "min(outputPositions * thirdGroups, 65535)",
880
+ "y": "ceilDiv(outputPositions * thirdGroups, 65535)",
881
+ "z": 1
882
+ }
883
  },
884
  {
885
  "id": "merge",
886
  "name": "TopK.LargeMergeOutput",
887
+ "shader": "topk-large-block.wgsl.jinja",
888
+ "derive": { "sharedSize": "pow2ceil(thirdCandidates)", "stage": "\"output\"" },
889
+ "bindings": ["candidateVals_8", "candidateIdxs_4", "values", "indices", "params_15_source", "x"],
890
+ "dispatch": { "x": "min(outputPositions, 65535)", "y": "ceilDiv(outputPositions, 65535)", "z": 1 }
891
  }
892
  ]
893
  },
 
895
  "id": "last_axis_large_three_merge_int",
896
  "priority": 10,
897
  "when": ["baseContract", "lastAxis", "args.k > 1", "axisDim > tunables.AXIS_SHARED_MAX_VALUES", "firstCandidates > mergeChunkSize", "secondCandidates > mergeChunkSize", "thirdCandidates <= mergeChunkSize", "firstScratchFits", "secondScratchFits", "thirdScratchFits", "largeSharedFits", "largeBlockDispatchFits", "secondMergeDispatchFits", "thirdMergeDispatchFits", "outputDispatchFits", "not floatInput"],
898
+ "derive": {
899
+ "scalar": "dtypes.T",
900
+ "usesF16": "dtypes.T == \"f16\"",
901
+ "blockSize": "largeBlockSize",
902
+ "workgroupSize": "workgroupSizePlan",
903
+ "floatOrderKeys": false
904
+ },
905
  "intermediates": [
906
  { "id": "candidateVals", "dtype": "uint32", "shape": "[firstScratchElements]" },
907
  { "id": "candidateIdxs", "dtype": "uint32", "shape": "[firstScratchElements]" },
 
914
  {
915
  "id": "block",
916
  "name": "TopK.LargeBlock",
917
+ "shader": "topk-large-block.wgsl.jinja",
918
+ "derive": { "stage": "\"block\"" },
919
+ "bindings": ["x", "candidateVals_3", "candidateIdxs", "params_7"],
920
+ "dispatch": {
921
+ "x": "min(outputPositions * largeBlockCount, 65535)",
922
+ "y": "ceilDiv(outputPositions * largeBlockCount, 65535)",
923
+ "z": 1
924
+ }
925
  },
926
  {
927
  "id": "merge_scratch",
928
  "name": "TopK.LargeMergeScratch",
929
+ "shader": "topk-large-block.wgsl.jinja",
930
+ "derive": { "sharedSize": "mergeChunkSize", "stage": "\"scratch\"" },
931
+ "bindings": ["candidateVals_4", "candidateIdxs_2", "candidateVals2_2", "candidateIdxs2", "params_11"],
932
+ "dispatch": {
933
+ "x": "min(outputPositions * secondGroups, 65535)",
934
+ "y": "ceilDiv(outputPositions * secondGroups, 65535)",
935
+ "z": 1
936
+ }
937
  },
938
  {
939
  "id": "merge_scratch2",
940
  "name": "TopK.LargeMergeScratch2",
941
+ "shader": "topk-large-block.wgsl.jinja",
942
+ "derive": { "sharedSize": "mergeChunkSize", "stage": "\"scratch\"" },
943
+ "bindings": ["candidateVals_6", "candidateIdxs_3", "candidateVals2_4", "candidateIdxs2_2", "params_14"],
944
+ "dispatch": {
945
+ "x": "min(outputPositions * thirdGroups, 65535)",
946
+ "y": "ceilDiv(outputPositions * thirdGroups, 65535)",
947
+ "z": 1
948
+ }
949
  },
950
  {
951
  "id": "merge",
952
  "name": "TopK.LargeMergeOutput",
953
+ "shader": "topk-large-block.wgsl.jinja",
954
+ "derive": { "sharedSize": "pow2ceil(thirdCandidates)", "stage": "\"output\"" },
955
+ "bindings": ["candidateVals_8", "candidateIdxs_4", "values", "indices", "params_15"],
956
+ "dispatch": { "x": "min(outputPositions, 65535)", "y": "ceilDiv(outputPositions, 65535)", "z": 1 }
957
  }
958
  ]
959
  }
build/webgpu/metadata.json CHANGED
@@ -1,25 +1,46 @@
1
  {
2
  "name": "ai.onnx.TopK",
3
- "id": "_ai_onnx_topk_webgpu_148201a",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "mfHtgd+qPu1FdwN2IYJL8z9saLYe1dxUbiQ4utLBkxg=",
11
- "manifest.json": "4FlmxzZXEt5u1iom1e5ehHAik/1Nn0aIhyIU/6dai1o=",
12
- "test.json": "WrNZJCAriH+P2DxOGsAbrS4beAQMOiOilMoGY7shG5g=",
13
- "topk-axis.wgsl.jinja": "lSuHRRplKZnODO49Op7ehqJiOqGmI5F/dahAsauzBoM=",
14
- "topk-large-block.wgsl.jinja": "ZF/+hgaZ42J/Gun/JOXOqRzbwn6xbs2ekew8F4sOndk=",
15
  "topk-noop.wgsl.jinja": "rzwkvcZd8vJ3qLSosuI96CfUW+WUQyFxjc2Hgvq1bdo=",
16
- "topk-portable-rows-smallk.wgsl.jinja": "jc8DhH2f7LZMHBFPqON9zet+kLnMb/fNVHBkYdAyzAo=",
17
- "topk-small-rows-batched.wgsl.jinja": "7cPf2k8yNMXg1bZrusvl71RIcBSjOLKPBATaj1Y4bfo=",
18
- "topk-strided-smallk.wgsl.jinja": "10odKZ0VhoUSwcQZIP3rnTEfN8gfP7OUQOilJEIfsNU=",
19
- "topk-subgroup-rows.wgsl.jinja": "zO4KDfA5eM9COJ9chF7wwvYdJbpPKcKkB0qQJLeYMvA=",
20
- "topk-top1-last-axis.wgsl.jinja": "C+DVDZ78X/DimUDKyRtlC5HV/nRwiXQil8IjGjXcnB0="
21
  }
22
  },
23
- "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
24
- "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.TopK" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  }
 
1
  {
2
  "name": "ai.onnx.TopK",
3
+ "id": "_ai_onnx_topk_webgpu_52be754",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "PurEWvvW6gbYNkFFlfRDPuV5w56IJxPpwT0HV0cXd0A=",
11
+ "manifest.json": "YqipHsTNrefVMyk2lSH1Vp9eXwjmXHibQ+FXiPhg3iw=",
12
+ "test.json": "Fr5n36m9d6NTCaetFemmXK+rj/6dXPaw6INsNyX9jqs=",
13
+ "topk-axis.wgsl.jinja": "Xl+bV55WupVkNPIof1xc0YfVFd0YlmTvWBoVKXuKH0s=",
14
+ "topk-large-block.wgsl.jinja": "r0f8d02UPzDntSxjLxFsP7ix3osvWFRmzsyt6YQS5K8=",
15
  "topk-noop.wgsl.jinja": "rzwkvcZd8vJ3qLSosuI96CfUW+WUQyFxjc2Hgvq1bdo=",
16
+ "topk-portable-rows-smallk.wgsl.jinja": "WLo2yQv5mq1yMAW6vNAFbCSzRwuQ8fvM3MtuYH/d5PM=",
17
+ "topk-small-rows-batched.wgsl.jinja": "zaHFjRfth6UmWbFEYmjRbE2hpIdjuqyafituQUBTGvg=",
18
+ "topk-strided-smallk.wgsl.jinja": "pMdoMVSn+bYSKpP/FHMFLL2hnGeMQyakRPPDTITae4I=",
19
+ "topk-subgroup-rows.wgsl.jinja": "NPnHo98cYexGK/mRtrzxZvXOjmVF/1soDP/EaBRaApw=",
20
+ "topk-top1-last-axis.wgsl.jinja": "AYmJPcushgFs3ixDK6sJtDQDNEGbBk/CAAPWRMF3g+Y="
21
  }
22
  },
23
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
24
+ "webgpu": {
25
+ "manifestSpec": "2.0",
26
+ "variants": {
27
+ "axis_smallk_tournament": ["topk-strided-smallk.wgsl.jinja"],
28
+ "last_axis_large_top1": ["topk-top1-last-axis.wgsl.jinja"],
29
+ "last_axis_top1": ["topk-top1-last-axis.wgsl.jinja"],
30
+ "axis_bitonic": ["topk-axis.wgsl.jinja"],
31
+ "top0_noop": ["topk-noop.wgsl.jinja"],
32
+ "subgroup_rows_smallk": ["topk-subgroup-rows.wgsl.jinja"],
33
+ "subgroup_min_rows_smallk": ["topk-subgroup-rows.wgsl.jinja"],
34
+ "portable_rows_smallk": ["topk-portable-rows-smallk.wgsl.jinja"],
35
+ "small_rows_batched": ["topk-small-rows-batched.wgsl.jinja"],
36
+ "last_axis_large_one_merge": ["topk-large-block.wgsl.jinja"],
37
+ "last_axis_large_two_merge": ["topk-large-block.wgsl.jinja"],
38
+ "last_axis_large_one_merge_int": ["topk-large-block.wgsl.jinja"],
39
+ "last_axis_large_two_merge_int": ["topk-large-block.wgsl.jinja"],
40
+ "axis_large_one_merge": ["topk-large-block.wgsl.jinja"],
41
+ "axis_large_two_merge": ["topk-large-block.wgsl.jinja"],
42
+ "last_axis_large_three_merge": ["topk-large-block.wgsl.jinja"],
43
+ "last_axis_large_three_merge_int": ["topk-large-block.wgsl.jinja"]
44
+ }
45
+ }
46
  }
build/webgpu/test.json CHANGED
@@ -1,12 +1,15 @@
1
  {
2
- "op": "ai.onnx.TopK",
 
 
 
3
  "cases": [
4
  {
5
  "name": "onnx_default_last_axis_omitted",
6
  "provenance": {
7
  "source": "https://onnx.ai/onnx/operators/onnx__TopK.html",
8
  "test": "TopK axis default",
9
- "notes": "Omitting axis must select the last dimension (ONNX default -1), not axis 1. Rank 3 makes those axes distinct, and both output values and indices are pinned. ONNX int64 indices use the framework's representable uint32 slot."
10
  },
11
  "args": { "k": 1 },
12
  "inputs": {
@@ -53,7 +56,7 @@
53
  "provenance": {
54
  "source": "onnxruntime/test/providers/cpu/math/topk_op_test.cc",
55
  "test": "TopKOperator.Top1DefaultAxisLargestElements",
56
- "notes": "Axis-0 companion for finite subnormal ordering: a positive subnormal is strictly greater than zero; TopK should return its index rather than tie-breaking on a flushed zero."
57
  },
58
  "attrs": { "axis": 0, "largest": 1, "sorted": 1 },
59
  "args": { "k": 1 },
@@ -87,7 +90,7 @@
87
  "provenance": {
88
  "source": "onnxruntime/test/providers/cpu/math/topk_op_test.cc",
89
  "test": "TopKOperator.Top3ExplicitAxisSmallestElements",
90
- "notes": "Axis-0 companion for finite subnormal ordering: a negative subnormal is strictly less than zero; smallest TopK should return its index."
91
  },
92
  "attrs": { "axis": 0, "largest": 0, "sorted": 1 },
93
  "args": { "k": 1 },
@@ -107,7 +110,7 @@
107
  "x": {
108
  "dtype": "float32",
109
  "shape": [2, 5],
110
- "data": { "kind": "values", "values": [1.5, 2.0, 2.0, -1.0, 0.5, 0.1, 3.0, 2.5, 3.0, -2.0] }
111
  }
112
  },
113
  "outputs": {
@@ -403,7 +406,7 @@
403
  "x": {
404
  "dtype": "float32",
405
  "shape": [2, 5],
406
- "data": { "kind": "values", "values": [1.0, 5.0, 3.0, 4.0, 2.0, 2.0, 8.0, 7.0, 6.0, 1.0] }
407
  }
408
  },
409
  "outputs": {
@@ -1099,7 +1102,7 @@
1099
  },
1100
  "provenance": {
1101
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_top_k",
1102
- "notes": "ONNX TopK K input represented as framework scalar arg k. ONNX int64 tensors use framework int32/uint32 slots where representable."
1103
  }
1104
  },
1105
  {
@@ -1119,7 +1122,7 @@
1119
  },
1120
  "provenance": {
1121
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_top_k_negative_axis",
1122
- "notes": "ONNX TopK K input represented as framework scalar arg k. ONNX int64 tensors use framework int32/uint32 slots where representable."
1123
  }
1124
  },
1125
  {
@@ -1139,7 +1142,7 @@
1139
  },
1140
  "provenance": {
1141
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_top_k_smallest",
1142
- "notes": "ONNX TopK K input represented as framework scalar arg k. ONNX int64 tensors use framework int32/uint32 slots where representable."
1143
  }
1144
  },
1145
  {
@@ -1166,7 +1169,7 @@
1166
  "name": "onnx_backend_top_k_same_values_largest",
1167
  "provenance": {
1168
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_top_k_same_values_largest",
1169
- "notes": "Official value tensors use int64; this fixture adapts them to supported float32 values and uint32 indices. The test_top_k_same_values vector projects to this same explicit largest=1 request."
1170
  },
1171
  "attrs": { "axis": 0, "largest": 1, "sorted": 1 },
1172
  "args": { "k": 3 },
@@ -1365,7 +1368,7 @@
1365
  "provenance": {
1366
  "source": "onnxruntime/test/providers/cpu/math/topk_op_test.cc",
1367
  "test": "TopKOperator.Top1ExplicitAxisLargestElements",
1368
- "notes": "Non-last-axis companion for exact int32 ordering: the axis sorter must compare and emit integer values without routing them through f32."
1369
  },
1370
  "attrs": { "axis": 0, "largest": 1, "sorted": 1 },
1371
  "args": { "k": 1 },
@@ -1396,7 +1399,7 @@
1396
  "provenance": {
1397
  "source": "onnxruntime/test/providers/cpu/math/topk_op_test.cc",
1398
  "test": "TopKOperator.Top3ExplicitAxisSmallestElements",
1399
- "notes": "Non-last-axis signed companion for exact int32 ordering below -2^24; f32 round-trip collapses the chosen sentinel values."
1400
  },
1401
  "attrs": { "axis": 0, "largest": 0, "sorted": 1 },
1402
  "args": { "k": 1 },
@@ -1458,7 +1461,7 @@
1458
  "provenance": {
1459
  "source": "onnxruntime/test/providers/cpu/math/topk_op_test.cc",
1460
  "test": "TopKOperator.Top2ExplicitAxisMultiDInputSmallestElements",
1461
- "notes": "Sorted k=2 signed-smallest companion for exact int32 values below -2^24."
1462
  },
1463
  "attrs": { "axis": -1, "largest": 0, "sorted": 1 },
1464
  "args": { "k": 2 },
@@ -1520,7 +1523,7 @@
1520
  "provenance": {
1521
  "source": "onnxruntime/test/providers/cpu/math/topk_op_test.cc",
1522
  "test": "TopKOperator.TopK_Int32_ExplicitAxis",
1523
- "notes": "Middle-axis signed-smallest companion for exact int32 ordering below -2^24."
1524
  },
1525
  "attrs": { "axis": 1, "largest": 0, "sorted": 1 },
1526
  "args": { "k": 1 },
@@ -1565,7 +1568,7 @@
1565
  {
1566
  "name": "dispatch_cliff_rows_65537",
1567
  "provenance": {
1568
- "notes": "65537 rows > 65535 forces the folded primary-axis dispatch (x=65535, y=2) in topk-axis.wgsl.jinja. Validates row = workgroup_id.x + workgroup_id.y*nwg.x. The 11-value cycle shifts each row (8 mod 11) so rows differ and a wrong fold would mismatch; 8 distinct values per row avoid tie-break ambiguity. Before the 2D fold the plan-time dispatch-limit guard throws."
1569
  },
1570
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
1571
  "args": { "k": 4 },
@@ -1584,7 +1587,7 @@
1584
  {
1585
  "name": "dispatch_cliff_rank3_axis1_cols_65537",
1586
  "provenance": {
1587
- "notes": "rank3 axis1 top-k with dim2=65537 (>65535) forces the x->y dispatch fold (x=65535, y=2) while z carries the batch axis (dim0). Validates the col = wg.x + wg.y*nwg.x reconstruction in topk-axis.wgsl.jinja (axisLayout 2), guarded by col >= params.dim2. Before the fold the plan-time dispatch-limit guard throws."
1588
  },
1589
  "attrs": { "axis": 1, "largest": 1, "sorted": 1 },
1590
  "args": { "k": 1 },
@@ -1707,7 +1710,7 @@
1707
  "x": {
1708
  "dtype": "float32",
1709
  "shape": [1025, 128],
1710
- "data": { "kind": "cycle", "values": [0.1, 0.5, 0.9, 0.3, 0.7, 0.2, 0.6, 0.4, 0.8, 0.05, 0.95] }
1711
  }
1712
  },
1713
  "outputs": {
@@ -1718,7 +1721,7 @@
1718
  {
1719
  "name": "small_row_batched_smallest_ties_4096x8_k4",
1720
  "provenance": {
1721
- "notes": "The consolidated batched-row variant also renders largest=0. Equal values must retain lower input indices in ascending index order."
1722
  },
1723
  "attrs": { "axis": -1, "largest": 0, "sorted": 1 },
1724
  "args": { "k": 4 },
@@ -1769,7 +1772,7 @@
1769
  {
1770
  "name": "rank3_axis1_2049x_k8_tournament_coverage",
1771
  "provenance": {
1772
- "notes": "Compact coverage lock one element beyond the historical 2048 non-last-axis ceiling. The tournament specializes to nine local candidates per lane and covers the shape without a generic-bitonic fallback."
1773
  },
1774
  "attrs": { "axis": 1, "largest": 1, "sorted": 1 },
1775
  "args": { "k": 8 },
@@ -1826,7 +1829,7 @@
1826
  "provenance": {
1827
  "source": "onnxruntime/core/providers/webgpu/math/top_k.cc",
1828
  "test": "float-typed kernel registration",
1829
- "notes": "ORT restricts WebGPU TopK to float types; our large-axis merge kernels accept i32/u32, so the candidate scratch must preserve integer exactness (values above 2^24 and u32 above 2^31 are not f32-representable)."
1830
  },
1831
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
1832
  "args": { "k": 2 },
@@ -1857,7 +1860,7 @@
1857
  "provenance": {
1858
  "source": "onnxruntime/core/providers/webgpu/math/top_k.cc",
1859
  "test": "float-typed kernel registration",
1860
- "notes": "ORT restricts WebGPU TopK to float types; our large-axis merge kernels accept i32/u32, so the candidate scratch must preserve integer exactness (values above 2^24 and u32 above 2^31 are not f32-representable)."
1861
  },
1862
  "attrs": { "axis": -1, "largest": 0, "sorted": 1 },
1863
  "args": { "k": 2 },
@@ -1888,7 +1891,7 @@
1888
  "provenance": {
1889
  "source": "onnxruntime/core/providers/webgpu/math/top_k.cc",
1890
  "test": "float-typed kernel registration",
1891
- "notes": "ORT restricts WebGPU TopK to float types; our large-axis merge kernels accept i32/u32, so the candidate scratch must preserve integer exactness (values above 2^24 and u32 above 2^31 are not f32-representable)."
1892
  },
1893
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
1894
  "args": { "k": 3 },
@@ -1922,7 +1925,7 @@
1922
  "provenance": {
1923
  "source": "onnxruntime/core/providers/webgpu/math/top_k.cc",
1924
  "test": "float-typed kernel registration",
1925
- "notes": "ORT restricts WebGPU TopK to float types; our large-axis merge kernels accept i32/u32, so the candidate scratch must preserve integer exactness (values above 2^24 and u32 above 2^31 are not f32-representable)."
1926
  },
1927
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
1928
  "args": { "k": 64 },
@@ -2040,7 +2043,7 @@
2040
  {
2041
  "name": "two_merge_distinct_values_vocab_k16_262144",
2042
  "provenance": {
2043
- "notes": "Distinct (non-tie) data through the two-merge chain at vocab scale. Guards the scratch-binding semantic redirect: with name-first resolution the final merge silently re-read the stage-1 candidate buffer, which every all-ties fixture masked."
2044
  },
2045
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
2046
  "args": { "k": 16 },
@@ -2059,7 +2062,7 @@
2059
  {
2060
  "name": "two_merge_monotonic_ramp_k128_8704",
2061
  "provenance": {
2062
- "notes": "Monotonic ramp: the true top-128 is the last 128 indices, so any stage reading a stale candidate buffer surfaces immediately (the name-first scratch lookup returned block-1 candidates here)."
2063
  },
2064
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
2065
  "args": { "k": 128 },
@@ -2109,7 +2112,7 @@
2109
  "name": "dsa_index_topk_2048_decode_16k",
2110
  "requires": { "limits": { "maxComputeWorkgroupStorageSize": 32768 } },
2111
  "provenance": {
2112
- "notes": "DeepSeek-V3.2 / AXK2 sparse-attention select step: index_topk defaults to 2048 in both configs. One query row against a 16K indexer key cache is the decode shape that co.huggingface.SparseAttentionScore feeds. The merge chunk is derived from k rather than fixed at MERGE_CHUNK_SIZE: a chunk no larger than k is a fixed point (each stage re-emits ceilDiv(candidates, chunk) * k), so the ladder never converged at k = 2048. Twice k halves the candidates per stage. That chunk needs 32 KiB of workgroup storage, which the case declares."
2113
  },
2114
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
2115
  "args": { "k": 2048 },
@@ -2129,7 +2132,7 @@
2129
  "name": "dsa_index_topk_2048_prefill_rows",
2130
  "requires": { "limits": { "maxComputeWorkgroupStorageSize": 32768 } },
2131
  "provenance": {
2132
- "notes": "Prefill form of the same select step: several query rows, each choosing 2048 of 4096 candidate keys. The merge chunk is derived from k rather than fixed at MERGE_CHUNK_SIZE: a chunk no larger than k is a fixed point (each stage re-emits ceilDiv(candidates, chunk) * k), so the ladder never converged at k = 2048. Twice k halves the candidates per stage. That chunk needs 32 KiB of workgroup storage, which the case declares."
2133
  },
2134
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
2135
  "args": { "k": 2048 },
@@ -2148,7 +2151,7 @@
2148
  {
2149
  "name": "small_rows_batched_4096x32_k4_f16",
2150
  "provenance": {
2151
- "notes": "float16 twin for the batched small-rows rung: at least SMALL_ROWS_MIN_ROWS (4096) rows, an axis within SMALL_ROWS_MAX_AXIS (64) and k within SMALL_ROWS_MAX_K (8). The 33-value cycle is one longer than the axis, so every row is a rotation of the previous one and no row repeats a value, which leaves the top-4 indices unambiguous. Each value is a multiple of 1/64 and so is exact in float16."
2152
  },
2153
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
2154
  "args": { "k": 4 },
@@ -2156,10 +2159,7 @@
2156
  "x": {
2157
  "dtype": "float16",
2158
  "shape": [4096, 32],
2159
- "data": {
2160
- "kind": "cycle",
2161
- "values": [0.015625, 0.03125, 0.046875, 0.0625, 0.078125, 0.09375, 0.109375, 0.125, 0.140625, 0.15625, 0.171875, 0.1875, 0.203125, 0.21875, 0.234375, 0.25, 0.265625, 0.28125, 0.296875, 0.3125, 0.328125, 0.34375, 0.359375, 0.375, 0.390625, 0.40625, 0.421875, 0.4375, 0.453125, 0.46875, 0.484375, 0.5, 0.515625]
2162
- }
2163
  }
2164
  },
2165
  "outputs": {
@@ -2170,7 +2170,7 @@
2170
  {
2171
  "name": "portable_rows_smallk_1025x128_k4_f16",
2172
  "provenance": {
2173
- "notes": "float16 twin for the portable small-k row rung, which takes an axis above SMALL_ROWS_MAX_AXIS (64) and within PORTABLE_ROWS_MAX_AXIS (256), and more than PORTABLE_ROWS_MIN (1024) rows. The 129-value cycle is one longer than the axis so each row is a rotation with no repeated value, and every value is a multiple of 1/256 and exact in float16."
2174
  },
2175
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
2176
  "args": { "k": 4 },
@@ -2188,6 +2188,588 @@
2188
  "values": { "dtype": "float16", "shape": [1025, 4], "tolerance": 0 },
2189
  "indices": { "dtype": "uint32", "shape": [1025, 4], "tolerance": 0 }
2190
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2191
  }
2192
  ]
2193
  }
 
1
  {
2
+ "fixtureArrays": {
3
+ "topk_small_rows_rank3_cycle33": [0.015625, 0.03125, 0.046875, 0.0625, 0.078125, 0.09375, 0.109375, 0.125, 0.140625, 0.15625, 0.171875, 0.1875, 0.203125, 0.21875, 0.234375, 0.25, 0.265625, 0.28125, 0.296875, 0.3125, 0.328125, 0.34375, 0.359375, 0.375, 0.390625, 0.40625, 0.421875, 0.4375, 0.453125, 0.46875, 0.484375, 0.5, 0.515625],
4
+ "order_key_extremes": ["-Infinity", -65504, -2, 0, 0, 2, 65504, "Infinity", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
5
+ },
6
  "cases": [
7
  {
8
  "name": "onnx_default_last_axis_omitted",
9
  "provenance": {
10
  "source": "https://onnx.ai/onnx/operators/onnx__TopK.html",
11
  "test": "TopK axis default",
12
+ "notes": "Omitting axis must select the last dimension (ONNX default -1), not axis 1. Rank 3 makes those axes distinct, and both output values and indices are pinned. Representable ONNX int64 indices are stored in uint32 slots."
13
  },
14
  "args": { "k": 1 },
15
  "inputs": {
 
56
  "provenance": {
57
  "source": "onnxruntime/test/providers/cpu/math/topk_op_test.cc",
58
  "test": "TopKOperator.Top1DefaultAxisLargestElements",
59
+ "notes": "On axis 0, a positive subnormal is strictly greater than zero; TopK must return its index rather than tie-break against a flushed zero."
60
  },
61
  "attrs": { "axis": 0, "largest": 1, "sorted": 1 },
62
  "args": { "k": 1 },
 
90
  "provenance": {
91
  "source": "onnxruntime/test/providers/cpu/math/topk_op_test.cc",
92
  "test": "TopKOperator.Top3ExplicitAxisSmallestElements",
93
+ "notes": "On axis 0, a negative subnormal is strictly less than zero; smallest TopK must return its index."
94
  },
95
  "attrs": { "axis": 0, "largest": 0, "sorted": 1 },
96
  "args": { "k": 1 },
 
110
  "x": {
111
  "dtype": "float32",
112
  "shape": [2, 5],
113
+ "data": { "kind": "values", "values": [1.5, 2.0, 2.0, -1.0, 0.5, 0.1, 2.5, 3.0, 3.0, -2.0] }
114
  }
115
  },
116
  "outputs": {
 
406
  "x": {
407
  "dtype": "float32",
408
  "shape": [2, 5],
409
+ "data": { "kind": "values", "values": [1.0, 5.0, 3.0, 4.0, 2.0, 2.0, 7.0, 8.0, 6.0, 1.0] }
410
  }
411
  },
412
  "outputs": {
 
1102
  },
1103
  "provenance": {
1104
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_top_k",
1105
+ "notes": "The fixture represents the scalar TopK `K` input as argument `k`. This WebGPU package stores representable ONNX int64 values and indices in int32 or uint32 slots."
1106
  }
1107
  },
1108
  {
 
1122
  },
1123
  "provenance": {
1124
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_top_k_negative_axis",
1125
+ "notes": "The fixture represents the scalar TopK `K` input as argument `k`. This WebGPU package stores representable ONNX int64 values and indices in int32 or uint32 slots."
1126
  }
1127
  },
1128
  {
 
1142
  },
1143
  "provenance": {
1144
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_top_k_smallest",
1145
+ "notes": "The fixture represents the scalar TopK `K` input as argument `k`. This WebGPU package stores representable ONNX int64 values and indices in int32 or uint32 slots."
1146
  }
1147
  },
1148
  {
 
1169
  "name": "onnx_backend_top_k_same_values_largest",
1170
  "provenance": {
1171
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_top_k_same_values_largest",
1172
+ "notes": "The official int64 value tensor is represented as supported float32 values with uint32 indices and an explicit `largest=1` request."
1173
  },
1174
  "attrs": { "axis": 0, "largest": 1, "sorted": 1 },
1175
  "args": { "k": 3 },
 
1368
  "provenance": {
1369
  "source": "onnxruntime/test/providers/cpu/math/topk_op_test.cc",
1370
  "test": "TopKOperator.Top1ExplicitAxisLargestElements",
1371
+ "notes": "An axis-0 TopK must compare and emit exact int32 values without routing them through f32."
1372
  },
1373
  "attrs": { "axis": 0, "largest": 1, "sorted": 1 },
1374
  "args": { "k": 1 },
 
1399
  "provenance": {
1400
  "source": "onnxruntime/test/providers/cpu/math/topk_op_test.cc",
1401
  "test": "TopKOperator.Top3ExplicitAxisSmallestElements",
1402
+ "notes": "An axis-0 smallest TopK must preserve exact int32 ordering below -2^24; an f32 round trip collapses the chosen sentinel values."
1403
  },
1404
  "attrs": { "axis": 0, "largest": 0, "sorted": 1 },
1405
  "args": { "k": 1 },
 
1461
  "provenance": {
1462
  "source": "onnxruntime/test/providers/cpu/math/topk_op_test.cc",
1463
  "test": "TopKOperator.Top2ExplicitAxisMultiDInputSmallestElements",
1464
+ "notes": "Sorted smallest TopK with k=2 must preserve exact int32 ordering below -2^24."
1465
  },
1466
  "attrs": { "axis": -1, "largest": 0, "sorted": 1 },
1467
  "args": { "k": 2 },
 
1523
  "provenance": {
1524
  "source": "onnxruntime/test/providers/cpu/math/topk_op_test.cc",
1525
  "test": "TopKOperator.TopK_Int32_ExplicitAxis",
1526
+ "notes": "A rank-3 middle-axis smallest TopK must preserve exact int32 ordering below -2^24."
1527
  },
1528
  "attrs": { "axis": 1, "largest": 0, "sorted": 1 },
1529
  "args": { "k": 1 },
 
1568
  {
1569
  "name": "dispatch_cliff_rows_65537",
1570
  "provenance": {
1571
+ "notes": "With 65,537 rows, TopK uses a two-row workgroup dispatch. An 11-value cycle shifts every eight-element row and avoids ties, making folded row-index errors observable."
1572
  },
1573
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
1574
  "args": { "k": 4 },
 
1587
  {
1588
  "name": "dispatch_cliff_rank3_axis1_cols_65537",
1589
  "provenance": {
1590
+ "notes": "A rank-3 axis-1 TopK with trailing extent 65,537 uses a two-row workgroup dispatch while the z dimension carries the batch. Distinct column values make folded column-index errors observable."
1591
  },
1592
  "attrs": { "axis": 1, "largest": 1, "sorted": 1 },
1593
  "args": { "k": 1 },
 
1710
  "x": {
1711
  "dtype": "float32",
1712
  "shape": [1025, 128],
1713
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5, "srqStep": 0.02 }
1714
  }
1715
  },
1716
  "outputs": {
 
1721
  {
1722
  "name": "small_row_batched_smallest_ties_4096x8_k4",
1723
  "provenance": {
1724
+ "notes": "For 4,096 equal eight-element rows with largest=0, sorted output must retain the lower input indices in ascending order."
1725
  },
1726
  "attrs": { "axis": -1, "largest": 0, "sorted": 1 },
1727
  "args": { "k": 4 },
 
1772
  {
1773
  "name": "rank3_axis1_2049x_k8_tournament_coverage",
1774
  "provenance": {
1775
+ "notes": "A non-last axis of length 2,049 gives each tournament lane nine local candidates and avoids the generic bitonic implementation."
1776
  },
1777
  "attrs": { "axis": 1, "largest": 1, "sorted": 1 },
1778
  "args": { "k": 8 },
 
1829
  "provenance": {
1830
  "source": "onnxruntime/core/providers/webgpu/math/top_k.cc",
1831
  "test": "float-typed kernel registration",
1832
+ "notes": "Large-axis merge scratch must preserve exact int32 values above 2^24 rather than store candidates as f32."
1833
  },
1834
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
1835
  "args": { "k": 2 },
 
1860
  "provenance": {
1861
  "source": "onnxruntime/core/providers/webgpu/math/top_k.cc",
1862
  "test": "float-typed kernel registration",
1863
+ "notes": "Large-axis merge scratch must preserve exact negative int32 values below -2^24 rather than store candidates as f32."
1864
  },
1865
  "attrs": { "axis": -1, "largest": 0, "sorted": 1 },
1866
  "args": { "k": 2 },
 
1891
  "provenance": {
1892
  "source": "onnxruntime/core/providers/webgpu/math/top_k.cc",
1893
  "test": "float-typed kernel registration",
1894
+ "notes": "Large-axis merge scratch must preserve exact uint32 values above 2^31 rather than store candidates as f32."
1895
  },
1896
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
1897
  "args": { "k": 3 },
 
1925
  "provenance": {
1926
  "source": "onnxruntime/core/providers/webgpu/math/top_k.cc",
1927
  "test": "float-typed kernel registration",
1928
+ "notes": "Two merge stages must preserve exact int32 values above 2^24 rather than store candidates as f32."
1929
  },
1930
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
1931
  "args": { "k": 64 },
 
2043
  {
2044
  "name": "two_merge_distinct_values_vocab_k16_262144",
2045
  "provenance": {
2046
+ "notes": "Distinct values over 262,144 candidates require two merge stages. The final stage must read the immediately preceding candidate buffer; otherwise the returned top 16 values and indices change."
2047
  },
2048
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
2049
  "args": { "k": 16 },
 
2062
  {
2063
  "name": "two_merge_monotonic_ramp_k128_8704",
2064
  "provenance": {
2065
+ "notes": "For a monotonic ramp, the true top 128 values occupy the final 128 indices, making stale candidate-buffer reads observable at every merge stage."
2066
  },
2067
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
2068
  "args": { "k": 128 },
 
2112
  "name": "dsa_index_topk_2048_decode_16k",
2113
  "requires": { "limits": { "maxComputeWorkgroupStorageSize": 32768 } },
2114
  "provenance": {
2115
+ "notes": "Selecting k=2048 from 16,384 candidates requires merge chunks larger than k; a chunk no larger than k does not reduce the candidate count. A 4,096-element chunk halves the candidates per stage and requires 32 KiB of workgroup storage."
2116
  },
2117
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
2118
  "args": { "k": 2048 },
 
2132
  "name": "dsa_index_topk_2048_prefill_rows",
2133
  "requires": { "limits": { "maxComputeWorkgroupStorageSize": 32768 } },
2134
  "provenance": {
2135
+ "notes": "Several query rows each select 2,048 of 4,096 candidate keys. A 4,096-element merge chunk halves the candidate count at each stage and requires the declared 32 KiB of workgroup storage."
2136
  },
2137
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
2138
  "args": { "k": 2048 },
 
2151
  {
2152
  "name": "small_rows_batched_4096x32_k4_f16",
2153
  "provenance": {
2154
+ "notes": "A float16 tensor with 4,096 rows, axis length 32, and k=4 exercises batched small-row selection. A 33-value cycle rotates distinct multiples of 1/64 through each row, leaving exact and unambiguous top-4 indices."
2155
  },
2156
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
2157
  "args": { "k": 4 },
 
2159
  "x": {
2160
  "dtype": "float16",
2161
  "shape": [4096, 32],
2162
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/topk_small_rows_rank3_cycle33" } }
 
 
 
2163
  }
2164
  },
2165
  "outputs": {
 
2170
  {
2171
  "name": "portable_rows_smallk_1025x128_k4_f16",
2172
  "provenance": {
2173
+ "notes": "A float16 tensor with 1,025 rows, axis length 128, and k=4 exercises the portable small-k row specialization. A 129-value cycle rotates distinct, exactly representable values through each row."
2174
  },
2175
  "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
2176
  "args": { "k": 4 },
 
2188
  "values": { "dtype": "float16", "shape": [1025, 4], "tolerance": 0 },
2189
  "indices": { "dtype": "uint32", "shape": [1025, 4], "tolerance": 0 }
2190
  }
2191
+ },
2192
+ {
2193
+ "name": "unsorted_small_rows_4096x8_k2_batched_route",
2194
+ "provenance": {
2195
+ "notes": "Exactly 4,096 eight-element rows with k=2 exercise batched small-row selection when output order is unspecified. A nine-value cycle rotates eight distinct values through each row, leaving the top-2 set unambiguous."
2196
+ },
2197
+ "attrs": { "axis": -1, "largest": 1, "sorted": 0 },
2198
+ "args": { "k": 2 },
2199
+ "inputs": {
2200
+ "x": {
2201
+ "dtype": "float32",
2202
+ "shape": [4096, 8],
2203
+ "data": { "kind": "cycle", "values": [0.1, 0.5, 0.9, 0.3, 0.7, 0.2, 0.6, 0.4, 0.8] }
2204
+ }
2205
+ },
2206
+ "outputs": {
2207
+ "values": { "dtype": "float32", "shape": [4096, 2], "tolerance": 0.000001 },
2208
+ "indices": { "dtype": "uint32", "shape": [4096, 2], "tolerance": 0 }
2209
+ }
2210
+ },
2211
+ {
2212
+ "name": "small_rows_batched_rank3_4096rows_32axis_k4",
2213
+ "provenance": {
2214
+ "notes": "A rank-3 tensor flattens to 4,096 rows of 32 values with k=4, exercising batched small-row selection independently of tensor rank. A 33-value cycle rotates distinct values through each row and leaves the top-4 indices unambiguous."
2215
+ },
2216
+ "attrs": { "axis": -1, "largest": 1, "sorted": 1 },
2217
+ "args": { "k": 4 },
2218
+ "inputs": {
2219
+ "x": {
2220
+ "dtype": "float32",
2221
+ "shape": [128, 32, 32],
2222
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/topk_small_rows_rank3_cycle33" } }
2223
+ }
2224
+ },
2225
+ "outputs": {
2226
+ "values": { "dtype": "float32", "shape": [128, 32, 4], "tolerance": 0 },
2227
+ "indices": { "dtype": "uint32", "shape": [128, 32, 4] }
2228
+ }
2229
+ },
2230
+ {
2231
+ "name": "bitonic_prefix_boundary_float32_axis256_k17_largest0",
2232
+ "provenance": {
2233
+ "notes": "Covers both sides of the default workgroup-width pruning boundary and a non-power-of-two retained prefix. Rounded adjacent ties retain stable index ordering while selected values vary across positions; integer cases preserve exact ordering beyond float32 integer precision."
2234
+ },
2235
+ "attrs": { "axis": -1, "largest": 0 },
2236
+ "args": { "k": 17 },
2237
+ "inputs": {
2238
+ "x": {
2239
+ "dtype": "float32",
2240
+ "shape": [2, 256],
2241
+ "data": { "kind": "linspace", "start": 16777200.0, "end": 16777600.0 }
2242
+ }
2243
+ },
2244
+ "outputs": {
2245
+ "values": { "dtype": "float32", "shape": [2, 17], "tolerance": 0 },
2246
+ "indices": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 }
2247
+ }
2248
+ },
2249
+ {
2250
+ "name": "bitonic_prefix_boundary_float32_axis256_k17_largest1",
2251
+ "provenance": {
2252
+ "notes": "Covers both sides of the default workgroup-width pruning boundary and a non-power-of-two retained prefix. Rounded adjacent ties retain stable index ordering while selected values vary across positions; integer cases preserve exact ordering beyond float32 integer precision."
2253
+ },
2254
+ "attrs": { "axis": -1, "largest": 1 },
2255
+ "args": { "k": 17 },
2256
+ "inputs": {
2257
+ "x": {
2258
+ "dtype": "float32",
2259
+ "shape": [2, 256],
2260
+ "data": { "kind": "linspace", "start": 16777200.0, "end": 16777600.0 }
2261
+ }
2262
+ },
2263
+ "outputs": {
2264
+ "values": { "dtype": "float32", "shape": [2, 17], "tolerance": 0 },
2265
+ "indices": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 }
2266
+ }
2267
+ },
2268
+ {
2269
+ "name": "bitonic_prefix_boundary_float32_axis257_k17_largest0",
2270
+ "provenance": {
2271
+ "notes": "Covers both sides of the default workgroup-width pruning boundary and a non-power-of-two retained prefix. Rounded adjacent ties retain stable index ordering while selected values vary across positions; integer cases preserve exact ordering beyond float32 integer precision."
2272
+ },
2273
+ "attrs": { "axis": -1, "largest": 0 },
2274
+ "args": { "k": 17 },
2275
+ "inputs": {
2276
+ "x": {
2277
+ "dtype": "float32",
2278
+ "shape": [2, 257],
2279
+ "data": { "kind": "linspace", "start": 16777200.0, "end": 16777600.0 }
2280
+ }
2281
+ },
2282
+ "outputs": {
2283
+ "values": { "dtype": "float32", "shape": [2, 17], "tolerance": 0 },
2284
+ "indices": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 }
2285
+ }
2286
+ },
2287
+ {
2288
+ "name": "bitonic_prefix_boundary_float32_axis257_k17_largest1",
2289
+ "provenance": {
2290
+ "notes": "Covers both sides of the default workgroup-width pruning boundary and a non-power-of-two retained prefix. Rounded adjacent ties retain stable index ordering while selected values vary across positions; integer cases preserve exact ordering beyond float32 integer precision."
2291
+ },
2292
+ "attrs": { "axis": -1, "largest": 1 },
2293
+ "args": { "k": 17 },
2294
+ "inputs": {
2295
+ "x": {
2296
+ "dtype": "float32",
2297
+ "shape": [2, 257],
2298
+ "data": { "kind": "linspace", "start": 16777200.0, "end": 16777600.0 }
2299
+ }
2300
+ },
2301
+ "outputs": {
2302
+ "values": { "dtype": "float32", "shape": [2, 17], "tolerance": 0 },
2303
+ "indices": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 }
2304
+ }
2305
+ },
2306
+ {
2307
+ "name": "bitonic_prefix_boundary_float16_axis256_k17_largest0",
2308
+ "provenance": {
2309
+ "notes": "Covers both sides of the default workgroup-width pruning boundary and a non-power-of-two retained prefix. Rounded adjacent ties retain stable index ordering while selected values vary across positions; integer cases preserve exact ordering beyond float32 integer precision."
2310
+ },
2311
+ "attrs": { "axis": -1, "largest": 0 },
2312
+ "args": { "k": 17 },
2313
+ "inputs": {
2314
+ "x": { "dtype": "float16", "shape": [2, 256], "data": { "kind": "linspace", "start": 1000.0, "end": 1200.0 } }
2315
+ },
2316
+ "outputs": {
2317
+ "values": { "dtype": "float16", "shape": [2, 17], "tolerance": 0 },
2318
+ "indices": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 }
2319
+ }
2320
+ },
2321
+ {
2322
+ "name": "bitonic_prefix_boundary_float16_axis256_k17_largest1",
2323
+ "provenance": {
2324
+ "notes": "Covers both sides of the default workgroup-width pruning boundary and a non-power-of-two retained prefix. Rounded adjacent ties retain stable index ordering while selected values vary across positions; integer cases preserve exact ordering beyond float32 integer precision."
2325
+ },
2326
+ "attrs": { "axis": -1, "largest": 1 },
2327
+ "args": { "k": 17 },
2328
+ "inputs": {
2329
+ "x": { "dtype": "float16", "shape": [2, 256], "data": { "kind": "linspace", "start": 1000.0, "end": 1200.0 } }
2330
+ },
2331
+ "outputs": {
2332
+ "values": { "dtype": "float16", "shape": [2, 17], "tolerance": 0 },
2333
+ "indices": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 }
2334
+ }
2335
+ },
2336
+ {
2337
+ "name": "bitonic_prefix_boundary_float16_axis257_k17_largest0",
2338
+ "provenance": {
2339
+ "notes": "Covers both sides of the default workgroup-width pruning boundary and a non-power-of-two retained prefix. Rounded adjacent ties retain stable index ordering while selected values vary across positions; integer cases preserve exact ordering beyond float32 integer precision."
2340
+ },
2341
+ "attrs": { "axis": -1, "largest": 0 },
2342
+ "args": { "k": 17 },
2343
+ "inputs": {
2344
+ "x": { "dtype": "float16", "shape": [2, 257], "data": { "kind": "linspace", "start": 1000.0, "end": 1200.0 } }
2345
+ },
2346
+ "outputs": {
2347
+ "values": { "dtype": "float16", "shape": [2, 17], "tolerance": 0 },
2348
+ "indices": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 }
2349
+ }
2350
+ },
2351
+ {
2352
+ "name": "bitonic_prefix_boundary_float16_axis257_k17_largest1",
2353
+ "provenance": {
2354
+ "notes": "Covers both sides of the default workgroup-width pruning boundary and a non-power-of-two retained prefix. Rounded adjacent ties retain stable index ordering while selected values vary across positions; integer cases preserve exact ordering beyond float32 integer precision."
2355
+ },
2356
+ "attrs": { "axis": -1, "largest": 1 },
2357
+ "args": { "k": 17 },
2358
+ "inputs": {
2359
+ "x": { "dtype": "float16", "shape": [2, 257], "data": { "kind": "linspace", "start": 1000.0, "end": 1200.0 } }
2360
+ },
2361
+ "outputs": {
2362
+ "values": { "dtype": "float16", "shape": [2, 17], "tolerance": 0 },
2363
+ "indices": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 }
2364
+ }
2365
+ },
2366
+ {
2367
+ "name": "bitonic_prefix_boundary_int32_axis256_k17_largest0",
2368
+ "provenance": {
2369
+ "notes": "Covers both sides of the default workgroup-width pruning boundary and a non-power-of-two retained prefix. Rounded adjacent ties retain stable index ordering while selected values vary across positions; integer cases preserve exact ordering beyond float32 integer precision."
2370
+ },
2371
+ "attrs": { "axis": -1, "largest": 0 },
2372
+ "args": { "k": 17 },
2373
+ "inputs": {
2374
+ "x": {
2375
+ "dtype": "int32",
2376
+ "shape": [2, 256],
2377
+ "data": { "kind": "linspace", "start": -16777473, "end": -16777216 }
2378
+ }
2379
+ },
2380
+ "outputs": {
2381
+ "values": { "dtype": "int32", "shape": [2, 17], "tolerance": 0 },
2382
+ "indices": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 }
2383
+ }
2384
+ },
2385
+ {
2386
+ "name": "bitonic_prefix_boundary_int32_axis256_k17_largest1",
2387
+ "provenance": {
2388
+ "notes": "Covers both sides of the default workgroup-width pruning boundary and a non-power-of-two retained prefix. Rounded adjacent ties retain stable index ordering while selected values vary across positions; integer cases preserve exact ordering beyond float32 integer precision."
2389
+ },
2390
+ "attrs": { "axis": -1, "largest": 1 },
2391
+ "args": { "k": 17 },
2392
+ "inputs": {
2393
+ "x": {
2394
+ "dtype": "int32",
2395
+ "shape": [2, 256],
2396
+ "data": { "kind": "linspace", "start": -16777473, "end": -16777216 }
2397
+ }
2398
+ },
2399
+ "outputs": {
2400
+ "values": { "dtype": "int32", "shape": [2, 17], "tolerance": 0 },
2401
+ "indices": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 }
2402
+ }
2403
+ },
2404
+ {
2405
+ "name": "bitonic_prefix_boundary_int32_axis257_k17_largest0",
2406
+ "provenance": {
2407
+ "notes": "Covers both sides of the default workgroup-width pruning boundary and a non-power-of-two retained prefix. Rounded adjacent ties retain stable index ordering while selected values vary across positions; integer cases preserve exact ordering beyond float32 integer precision."
2408
+ },
2409
+ "attrs": { "axis": -1, "largest": 0 },
2410
+ "args": { "k": 17 },
2411
+ "inputs": {
2412
+ "x": {
2413
+ "dtype": "int32",
2414
+ "shape": [2, 257],
2415
+ "data": { "kind": "linspace", "start": -16777473, "end": -16777216 }
2416
+ }
2417
+ },
2418
+ "outputs": {
2419
+ "values": { "dtype": "int32", "shape": [2, 17], "tolerance": 0 },
2420
+ "indices": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 }
2421
+ }
2422
+ },
2423
+ {
2424
+ "name": "bitonic_prefix_boundary_int32_axis257_k17_largest1",
2425
+ "provenance": {
2426
+ "notes": "Covers both sides of the default workgroup-width pruning boundary and a non-power-of-two retained prefix. Rounded adjacent ties retain stable index ordering while selected values vary across positions; integer cases preserve exact ordering beyond float32 integer precision."
2427
+ },
2428
+ "attrs": { "axis": -1, "largest": 1 },
2429
+ "args": { "k": 17 },
2430
+ "inputs": {
2431
+ "x": {
2432
+ "dtype": "int32",
2433
+ "shape": [2, 257],
2434
+ "data": { "kind": "linspace", "start": -16777473, "end": -16777216 }
2435
+ }
2436
+ },
2437
+ "outputs": {
2438
+ "values": { "dtype": "int32", "shape": [2, 17], "tolerance": 0 },
2439
+ "indices": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 }
2440
+ }
2441
+ },
2442
+ {
2443
+ "name": "bitonic_prefix_boundary_uint32_axis256_k17_largest0",
2444
+ "provenance": {
2445
+ "notes": "Covers both sides of the default workgroup-width pruning boundary and a non-power-of-two retained prefix. Rounded adjacent ties retain stable index ordering while selected values vary across positions; integer cases preserve exact ordering beyond float32 integer precision."
2446
+ },
2447
+ "attrs": { "axis": -1, "largest": 0 },
2448
+ "args": { "k": 17 },
2449
+ "inputs": {
2450
+ "x": {
2451
+ "dtype": "uint32",
2452
+ "shape": [2, 256],
2453
+ "data": { "kind": "linspace", "start": 4294967040, "end": 4294967295 }
2454
+ }
2455
+ },
2456
+ "outputs": {
2457
+ "values": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 },
2458
+ "indices": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 }
2459
+ }
2460
+ },
2461
+ {
2462
+ "name": "bitonic_prefix_boundary_uint32_axis256_k17_largest1",
2463
+ "provenance": {
2464
+ "notes": "Covers both sides of the default workgroup-width pruning boundary and a non-power-of-two retained prefix. Rounded adjacent ties retain stable index ordering while selected values vary across positions; integer cases preserve exact ordering beyond float32 integer precision."
2465
+ },
2466
+ "attrs": { "axis": -1, "largest": 1 },
2467
+ "args": { "k": 17 },
2468
+ "inputs": {
2469
+ "x": {
2470
+ "dtype": "uint32",
2471
+ "shape": [2, 256],
2472
+ "data": { "kind": "linspace", "start": 4294967040, "end": 4294967295 }
2473
+ }
2474
+ },
2475
+ "outputs": {
2476
+ "values": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 },
2477
+ "indices": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 }
2478
+ }
2479
+ },
2480
+ {
2481
+ "name": "bitonic_prefix_boundary_uint32_axis257_k17_largest0",
2482
+ "provenance": {
2483
+ "notes": "Covers both sides of the default workgroup-width pruning boundary and a non-power-of-two retained prefix. Rounded adjacent ties retain stable index ordering while selected values vary across positions; integer cases preserve exact ordering beyond float32 integer precision."
2484
+ },
2485
+ "attrs": { "axis": -1, "largest": 0 },
2486
+ "args": { "k": 17 },
2487
+ "inputs": {
2488
+ "x": {
2489
+ "dtype": "uint32",
2490
+ "shape": [2, 257],
2491
+ "data": { "kind": "linspace", "start": 4294967040, "end": 4294967295 }
2492
+ }
2493
+ },
2494
+ "outputs": {
2495
+ "values": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 },
2496
+ "indices": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 }
2497
+ }
2498
+ },
2499
+ {
2500
+ "name": "bitonic_prefix_boundary_uint32_axis257_k17_largest1",
2501
+ "provenance": {
2502
+ "notes": "Covers both sides of the default workgroup-width pruning boundary and a non-power-of-two retained prefix. Rounded adjacent ties retain stable index ordering while selected values vary across positions; integer cases preserve exact ordering beyond float32 integer precision."
2503
+ },
2504
+ "attrs": { "axis": -1, "largest": 1 },
2505
+ "args": { "k": 17 },
2506
+ "inputs": {
2507
+ "x": {
2508
+ "dtype": "uint32",
2509
+ "shape": [2, 257],
2510
+ "data": { "kind": "linspace", "start": 4294967040, "end": 4294967295 }
2511
+ }
2512
+ },
2513
+ "outputs": {
2514
+ "values": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 },
2515
+ "indices": { "dtype": "uint32", "shape": [2, 17], "tolerance": 0 }
2516
+ }
2517
+ },
2518
+ {
2519
+ "name": "order_keys_float32_n257_axis0_largest0",
2520
+ "args": { "k": 17 },
2521
+ "attrs": { "axis": 0, "largest": 0, "sorted": 1 },
2522
+ "inputs": {
2523
+ "x": {
2524
+ "dtype": "float32",
2525
+ "shape": [257, 3],
2526
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/order_key_extremes" } }
2527
+ }
2528
+ },
2529
+ "outputs": {
2530
+ "values": { "dtype": "float32", "shape": [17, 3], "tolerance": 0 },
2531
+ "indices": { "dtype": "uint32", "shape": [17, 3], "tolerance": 0 }
2532
+ }
2533
+ },
2534
+ {
2535
+ "name": "order_keys_float32_n257_axis0_largest1",
2536
+ "args": { "k": 17 },
2537
+ "attrs": { "axis": 0, "largest": 1, "sorted": 1 },
2538
+ "inputs": {
2539
+ "x": {
2540
+ "dtype": "float32",
2541
+ "shape": [257, 3],
2542
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/order_key_extremes" } }
2543
+ }
2544
+ },
2545
+ "outputs": {
2546
+ "values": { "dtype": "float32", "shape": [17, 3], "tolerance": 0 },
2547
+ "indices": { "dtype": "uint32", "shape": [17, 3], "tolerance": 0 }
2548
+ }
2549
+ },
2550
+ {
2551
+ "name": "order_keys_float32_n257_axis1_largest0",
2552
+ "args": { "k": 17 },
2553
+ "attrs": { "axis": 1, "largest": 0, "sorted": 1 },
2554
+ "inputs": {
2555
+ "x": {
2556
+ "dtype": "float32",
2557
+ "shape": [3, 257],
2558
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/order_key_extremes" } }
2559
+ }
2560
+ },
2561
+ "outputs": {
2562
+ "values": { "dtype": "float32", "shape": [3, 17], "tolerance": 0 },
2563
+ "indices": { "dtype": "uint32", "shape": [3, 17], "tolerance": 0 }
2564
+ }
2565
+ },
2566
+ {
2567
+ "name": "order_keys_float32_n257_axis1_largest1",
2568
+ "args": { "k": 17 },
2569
+ "attrs": { "axis": 1, "largest": 1, "sorted": 1 },
2570
+ "inputs": {
2571
+ "x": {
2572
+ "dtype": "float32",
2573
+ "shape": [3, 257],
2574
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/order_key_extremes" } }
2575
+ }
2576
+ },
2577
+ "outputs": {
2578
+ "values": { "dtype": "float32", "shape": [3, 17], "tolerance": 0 },
2579
+ "indices": { "dtype": "uint32", "shape": [3, 17], "tolerance": 0 }
2580
+ }
2581
+ },
2582
+ {
2583
+ "name": "order_keys_float32_n4099_axis0_largest0",
2584
+ "args": { "k": 70 },
2585
+ "attrs": { "axis": 0, "largest": 0, "sorted": 1 },
2586
+ "inputs": {
2587
+ "x": {
2588
+ "dtype": "float32",
2589
+ "shape": [4099, 3],
2590
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/order_key_extremes" } }
2591
+ }
2592
+ },
2593
+ "outputs": {
2594
+ "values": { "dtype": "float32", "shape": [70, 3], "tolerance": 0 },
2595
+ "indices": { "dtype": "uint32", "shape": [70, 3], "tolerance": 0 }
2596
+ }
2597
+ },
2598
+ {
2599
+ "name": "order_keys_float32_n4099_axis0_largest1",
2600
+ "args": { "k": 70 },
2601
+ "attrs": { "axis": 0, "largest": 1, "sorted": 1 },
2602
+ "inputs": {
2603
+ "x": {
2604
+ "dtype": "float32",
2605
+ "shape": [4099, 3],
2606
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/order_key_extremes" } }
2607
+ }
2608
+ },
2609
+ "outputs": {
2610
+ "values": { "dtype": "float32", "shape": [70, 3], "tolerance": 0 },
2611
+ "indices": { "dtype": "uint32", "shape": [70, 3], "tolerance": 0 }
2612
+ }
2613
+ },
2614
+ {
2615
+ "name": "order_keys_float32_n4099_axis1_largest0",
2616
+ "args": { "k": 70 },
2617
+ "attrs": { "axis": 1, "largest": 0, "sorted": 1 },
2618
+ "inputs": {
2619
+ "x": {
2620
+ "dtype": "float32",
2621
+ "shape": [3, 4099],
2622
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/order_key_extremes" } }
2623
+ }
2624
+ },
2625
+ "outputs": {
2626
+ "values": { "dtype": "float32", "shape": [3, 70], "tolerance": 0 },
2627
+ "indices": { "dtype": "uint32", "shape": [3, 70], "tolerance": 0 }
2628
+ }
2629
+ },
2630
+ {
2631
+ "name": "order_keys_float32_n4099_axis1_largest1",
2632
+ "args": { "k": 70 },
2633
+ "attrs": { "axis": 1, "largest": 1, "sorted": 1 },
2634
+ "inputs": {
2635
+ "x": {
2636
+ "dtype": "float32",
2637
+ "shape": [3, 4099],
2638
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/order_key_extremes" } }
2639
+ }
2640
+ },
2641
+ "outputs": {
2642
+ "values": { "dtype": "float32", "shape": [3, 70], "tolerance": 0 },
2643
+ "indices": { "dtype": "uint32", "shape": [3, 70], "tolerance": 0 }
2644
+ }
2645
+ },
2646
+ {
2647
+ "name": "order_keys_float16_n257_axis0_largest0",
2648
+ "args": { "k": 17 },
2649
+ "attrs": { "axis": 0, "largest": 0, "sorted": 1 },
2650
+ "inputs": {
2651
+ "x": {
2652
+ "dtype": "float16",
2653
+ "shape": [257, 3],
2654
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/order_key_extremes" } }
2655
+ }
2656
+ },
2657
+ "outputs": {
2658
+ "values": { "dtype": "float16", "shape": [17, 3], "tolerance": 0 },
2659
+ "indices": { "dtype": "uint32", "shape": [17, 3], "tolerance": 0 }
2660
+ }
2661
+ },
2662
+ {
2663
+ "name": "order_keys_float16_n257_axis0_largest1",
2664
+ "args": { "k": 17 },
2665
+ "attrs": { "axis": 0, "largest": 1, "sorted": 1 },
2666
+ "inputs": {
2667
+ "x": {
2668
+ "dtype": "float16",
2669
+ "shape": [257, 3],
2670
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/order_key_extremes" } }
2671
+ }
2672
+ },
2673
+ "outputs": {
2674
+ "values": { "dtype": "float16", "shape": [17, 3], "tolerance": 0 },
2675
+ "indices": { "dtype": "uint32", "shape": [17, 3], "tolerance": 0 }
2676
+ }
2677
+ },
2678
+ {
2679
+ "name": "order_keys_float16_n257_axis1_largest0",
2680
+ "args": { "k": 17 },
2681
+ "attrs": { "axis": 1, "largest": 0, "sorted": 1 },
2682
+ "inputs": {
2683
+ "x": {
2684
+ "dtype": "float16",
2685
+ "shape": [3, 257],
2686
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/order_key_extremes" } }
2687
+ }
2688
+ },
2689
+ "outputs": {
2690
+ "values": { "dtype": "float16", "shape": [3, 17], "tolerance": 0 },
2691
+ "indices": { "dtype": "uint32", "shape": [3, 17], "tolerance": 0 }
2692
+ }
2693
+ },
2694
+ {
2695
+ "name": "order_keys_float16_n257_axis1_largest1",
2696
+ "args": { "k": 17 },
2697
+ "attrs": { "axis": 1, "largest": 1, "sorted": 1 },
2698
+ "inputs": {
2699
+ "x": {
2700
+ "dtype": "float16",
2701
+ "shape": [3, 257],
2702
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/order_key_extremes" } }
2703
+ }
2704
+ },
2705
+ "outputs": {
2706
+ "values": { "dtype": "float16", "shape": [3, 17], "tolerance": 0 },
2707
+ "indices": { "dtype": "uint32", "shape": [3, 17], "tolerance": 0 }
2708
+ }
2709
+ },
2710
+ {
2711
+ "name": "order_keys_float16_n4099_axis0_largest0",
2712
+ "args": { "k": 70 },
2713
+ "attrs": { "axis": 0, "largest": 0, "sorted": 1 },
2714
+ "inputs": {
2715
+ "x": {
2716
+ "dtype": "float16",
2717
+ "shape": [4099, 3],
2718
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/order_key_extremes" } }
2719
+ }
2720
+ },
2721
+ "outputs": {
2722
+ "values": { "dtype": "float16", "shape": [70, 3], "tolerance": 0 },
2723
+ "indices": { "dtype": "uint32", "shape": [70, 3], "tolerance": 0 }
2724
+ }
2725
+ },
2726
+ {
2727
+ "name": "order_keys_float16_n4099_axis0_largest1",
2728
+ "args": { "k": 70 },
2729
+ "attrs": { "axis": 0, "largest": 1, "sorted": 1 },
2730
+ "inputs": {
2731
+ "x": {
2732
+ "dtype": "float16",
2733
+ "shape": [4099, 3],
2734
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/order_key_extremes" } }
2735
+ }
2736
+ },
2737
+ "outputs": {
2738
+ "values": { "dtype": "float16", "shape": [70, 3], "tolerance": 0 },
2739
+ "indices": { "dtype": "uint32", "shape": [70, 3], "tolerance": 0 }
2740
+ }
2741
+ },
2742
+ {
2743
+ "name": "order_keys_float16_n4099_axis1_largest0",
2744
+ "args": { "k": 70 },
2745
+ "attrs": { "axis": 1, "largest": 0, "sorted": 1 },
2746
+ "inputs": {
2747
+ "x": {
2748
+ "dtype": "float16",
2749
+ "shape": [3, 4099],
2750
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/order_key_extremes" } }
2751
+ }
2752
+ },
2753
+ "outputs": {
2754
+ "values": { "dtype": "float16", "shape": [3, 70], "tolerance": 0 },
2755
+ "indices": { "dtype": "uint32", "shape": [3, 70], "tolerance": 0 }
2756
+ }
2757
+ },
2758
+ {
2759
+ "name": "order_keys_float16_n4099_axis1_largest1",
2760
+ "args": { "k": 70 },
2761
+ "attrs": { "axis": 1, "largest": 1, "sorted": 1 },
2762
+ "inputs": {
2763
+ "x": {
2764
+ "dtype": "float16",
2765
+ "shape": [3, 4099],
2766
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/order_key_extremes" } }
2767
+ }
2768
+ },
2769
+ "outputs": {
2770
+ "values": { "dtype": "float16", "shape": [3, 70], "tolerance": 0 },
2771
+ "indices": { "dtype": "uint32", "shape": [3, 70], "tolerance": 0 }
2772
+ }
2773
  }
2774
  ]
2775
  }
build/webgpu/topk-axis.wgsl.jinja CHANGED
@@ -1,34 +1,47 @@
1
- {% if usesF16 %}
2
- enable f16;
3
- {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
  // Generic (outer, axis, inner) bitonic TopK. Rank and layout differences reduce
6
  // to uniform geometry, so one implementation covers every tensor rank.
7
  {% set is_int = scalar == "i32" or scalar == "u32" %}
8
- {% set val_t = scalar if is_int else "f32" %}
9
- {% set zero = scalar ~ "(0)" if is_int else "0.0" %}
 
10
  // Stable bitonic comparator for TopK values and their original indices.
11
  // Stable order key and NaN predicate for direct, tournament, and bitonic TopK.
12
  {% if order_scalar is not defined %}{% set order_scalar = scalar %}{% endif %}
13
  const PAD_INDEX: u32 = 0xffffffffu;
14
 
 
 
 
 
 
15
  fn order_key(value: {{ val_t }}) -> u32 {
16
- {% if is_int and order_scalar == "i32" %}
17
  return bitcast<u32>(value) ^ 0x80000000u;
18
- {% elif is_int and order_scalar == "u32" %}
19
  return value;
20
  {% else %}
21
- let bits0 = bitcast<u32>(value);
22
- let bits = select(bits0, 0u, bits0 == 0x80000000u);
23
- return select(bits | 0x80000000u, ~bits, (bits & 0x80000000u) != 0u);
24
  {% endif %}
25
  }
26
 
27
  {% if not is_int %}
28
- fn is_nan_value(value: f32) -> bool {
 
 
 
 
29
  let bits = bitcast<u32>(value);
30
  return (bits & 0x7f800000u) == 0x7f800000u
31
  && (bits & 0x007fffffu) != 0u;
 
 
 
 
 
 
 
 
32
  }
33
  {% endif %}
34
 
@@ -73,9 +86,8 @@ var<workgroup> shared_indices: array<u32, {{ sharedSize }}>;
73
  fn main(
74
  @builtin(workgroup_id) workgroup: vec3<u32>,
75
  @builtin(local_invocation_id) local: vec3<u32>,
76
- @builtin(num_workgroups) num_workgroups: vec3<u32>,
77
  ) {
78
- let output_position = workgroup.x + workgroup.y * num_workgroups.x;
79
  if (output_position >= params.outputPositions) {
80
  return;
81
  }
@@ -86,7 +98,7 @@ fn main(
86
 
87
  for (var axis_index = tid; axis_index < SHARED_SIZE; axis_index += WG) {
88
  if (axis_index < params.axis) {
89
- shared_values[axis_index] = {{ val_t }}(x[input_base + axis_index * params.inner]);
90
  shared_indices[axis_index] = axis_index;
91
  } else {
92
  shared_values[axis_index] = {{ zero }};
@@ -95,23 +107,16 @@ fn main(
95
  }
96
  workgroupBarrier();
97
 
98
- // In-place bitonic ordering of values and their original indices.
99
- for (var block_size = 2u; block_size <= SHARED_SIZE; block_size <<= 1u) {
100
- for (var gap = block_size >> 1u; gap > 0u; gap >>= 1u) {
101
- for (var pair = tid; pair < SHARED_SIZE / 2u; pair += WG) {
102
- // `gap` and `block_size` are powers of two by construction (block_size
103
- // doubles from 2, gap halves from block_size >> 1), so the index math is
104
- // masks and shifts. GPUs have no integer divide unit and neither
105
- // value is a compile-time constant here, so a literal `/` and `%` expand
106
- // to a full division per pair per stage — three of them, against a body
107
- // that is otherwise four shared loads and a compare.
108
  let position = pair & (gap - 1u);
109
  let left = ((pair - position) << 1u) + position;
110
  let right = left + gap;
111
  {% if attrs.largest == 0 %}
112
- let ascending = (left & block_size) == 0u;
113
  {% else %}
114
- let ascending = (left & block_size) != 0u;
115
  {% endif %}
116
  let left_value = shared_values[left];
117
  let right_value = shared_values[right];
@@ -124,13 +129,25 @@ fn main(
124
  shared_indices[right] = left_index;
125
  }
126
  }
 
 
 
 
 
 
127
  workgroupBarrier();
128
  }
129
  }
 
 
 
 
 
 
130
 
131
  for (var rank = tid; rank < params.k; rank += WG) {
132
  let output_index = (outer_index * params.k + rank) * params.inner + inner_index;
133
- values[output_index] = {{ scalar }}(shared_values[rank]);
134
  indices[output_index] = shared_indices[rank];
135
  }
136
  }
 
 
 
 
1
  {{ env.wgsl.resourceDeclarations }}
2
  // Generic (outer, axis, inner) bitonic TopK. Rank and layout differences reduce
3
  // to uniform geometry, so one implementation covers every tensor rank.
4
  {% set is_int = scalar == "i32" or scalar == "u32" %}
5
+ {% set val_t = scalar if is_int else "u32" %}
6
+ {% set zero = scalar ~ "(0)" if is_int else "0u" %}
7
+ {% set order_scalar = val_t %}
8
  // Stable bitonic comparator for TopK values and their original indices.
9
  // Stable order key and NaN predicate for direct, tournament, and bitonic TopK.
10
  {% if order_scalar is not defined %}{% set order_scalar = scalar %}{% endif %}
11
  const PAD_INDEX: u32 = 0xffffffffu;
12
 
13
+ {% macro float_order_key(value) %}
14
+ let bits0 = bitcast<u32>({{ value }});
15
+ let bits = select(bits0, 0u, bits0 == 0x80000000u);
16
+ return select(bits | 0x80000000u, ~bits, (bits & 0x80000000u) != 0u);
17
+ {% endmacro %}
18
  fn order_key(value: {{ val_t }}) -> u32 {
19
+ {% if order_scalar == "i32" %}
20
  return bitcast<u32>(value) ^ 0x80000000u;
21
+ {% elif order_scalar == "u32" %}
22
  return value;
23
  {% else %}
24
+ {{ float_order_key("value") }}
 
 
25
  {% endif %}
26
  }
27
 
28
  {% if not is_int %}
29
+ fn is_nan_value(value: {{ val_t }}) -> bool {
30
+ {% if floatOrderKeys is defined and floatOrderKeys %}
31
+ // Encoded infinities bound the finite keys; either outer range is NaN.
32
+ return value > 0xff800000u || value < 0x007fffffu;
33
+ {% else %}
34
  let bits = bitcast<u32>(value);
35
  return (bits & 0x7f800000u) == 0x7f800000u
36
  && (bits & 0x007fffffu) != 0u;
37
+ {% endif %}
38
+ }
39
+ {% endif %}
40
+ {% if floatOrderKeys is defined and floatOrderKeys and (stage is not defined or stage == "block") %}
41
+ // Encode once on input. Comparators consume keys, while output gathers the
42
+ // selected original values to preserve their representation, including -0.
43
+ fn input_order_key(value: {{ scalar }}) -> u32 {
44
+ {{ float_order_key("f32(value)") }}
45
  }
46
  {% endif %}
47
 
 
86
  fn main(
87
  @builtin(workgroup_id) workgroup: vec3<u32>,
88
  @builtin(local_invocation_id) local: vec3<u32>,
 
89
  ) {
90
+ let output_position = workgroup.x + workgroup.y * {{ DISPATCH_FOLD_WIDTH }}u;
91
  if (output_position >= params.outputPositions) {
92
  return;
93
  }
 
98
 
99
  for (var axis_index = tid; axis_index < SHARED_SIZE; axis_index += WG) {
100
  if (axis_index < params.axis) {
101
+ shared_values[axis_index] = {{ val_t if is_int else "input_order_key" }}(x[input_base + axis_index * params.inner]);
102
  shared_indices[axis_index] = axis_index;
103
  } else {
104
  shared_values[axis_index] = {{ zero }};
 
107
  }
108
  workgroupBarrier();
109
 
110
+ {% macro compare_pairs(pairCount, finalStage) %}
111
+ for (var pair = tid; pair < {{ pairCount }}; pair += WG) {
112
+ // Power-of-two gaps let masks and shifts identify disjoint pairs.
 
 
 
 
 
 
 
113
  let position = pair & (gap - 1u);
114
  let left = ((pair - position) << 1u) + position;
115
  let right = left + gap;
116
  {% if attrs.largest == 0 %}
117
+ let ascending = {{ "true" if finalStage else "(left & block_size) == 0u" }};
118
  {% else %}
119
+ let ascending = {{ "false" if finalStage else "(left & block_size) != 0u" }};
120
  {% endif %}
121
  let left_value = shared_values[left];
122
  let right_value = shared_values[right];
 
129
  shared_indices[right] = left_index;
130
  }
131
  }
132
+ {% endmacro %}
133
+ {% set sortSize = blockSize if (stage is defined and stage == "block") else sharedSize %}
134
+ {% set pruneFinal = sortSize > workgroupSize and sortRetainedSize < sortSize %}
135
+ for (var block_size = 2u; block_size {{ "<" if pruneFinal else "<=" }} SHARED_SIZE; block_size <<= 1u) {
136
+ for (var gap = block_size >> 1u; gap > 0u; gap >>= 1u) {
137
+ {{ compare_pairs("SHARED_SIZE / 2u", false) }}
138
  workgroupBarrier();
139
  }
140
  }
141
+ {% if pruneFinal %}
142
+ for (var gap = SHARED_SIZE >> 1u; gap > 0u; gap >>= 1u) {
143
+ {{ compare_pairs("max(gap, " ~ ((sortRetainedSize / 2)|int) ~ "u)", true) }}
144
+ workgroupBarrier();
145
+ }
146
+ {% endif %}
147
 
148
  for (var rank = tid; rank < params.k; rank += WG) {
149
  let output_index = (outer_index * params.k + rank) * params.inner + inner_index;
150
+ values[output_index] = {% if floatOrderKeys is defined and floatOrderKeys %}x[input_base + shared_indices[rank] * params.inner]{% else %}{{ scalar }}(shared_values[rank]){% endif %};
151
  indices[output_index] = shared_indices[rank];
152
  }
153
  }
build/webgpu/topk-large-block.wgsl.jinja CHANGED
@@ -1,38 +1,49 @@
1
- {% if usesF16 and source.stage != "scratch" %}
2
  enable f16;
3
  {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
- // i32/u32 inputs sort by u32 order keys so candidate scratch remains exact; an
6
- // f32 scratch would corrupt integers above 2^24.
7
  {% set is_int = scalar == "i32" or scalar == "u32" %}
8
- {% if is_int %}
9
  {% set val_t = "u32" %}
10
  {% set order_scalar = "u32" %}
11
- {% else %}
12
- {% set val_t = "f32" %}
13
- {% endif %}
14
  // Stable bitonic comparator for TopK values and their original indices.
15
  // Stable order key and NaN predicate for direct, tournament, and bitonic TopK.
16
  {% if order_scalar is not defined %}{% set order_scalar = scalar %}{% endif %}
17
  const PAD_INDEX: u32 = 0xffffffffu;
18
 
 
 
 
 
 
19
  fn order_key(value: {{ val_t }}) -> u32 {
20
- {% if is_int and order_scalar == "i32" %}
21
  return bitcast<u32>(value) ^ 0x80000000u;
22
- {% elif is_int and order_scalar == "u32" %}
23
  return value;
24
  {% else %}
25
- let bits0 = bitcast<u32>(value);
26
- let bits = select(bits0, 0u, bits0 == 0x80000000u);
27
- return select(bits | 0x80000000u, ~bits, (bits & 0x80000000u) != 0u);
28
  {% endif %}
29
  }
30
 
31
  {% if not is_int %}
32
- fn is_nan_value(value: f32) -> bool {
 
 
 
 
33
  let bits = bitcast<u32>(value);
34
  return (bits & 0x7f800000u) == 0x7f800000u
35
  && (bits & 0x007fffffu) != 0u;
 
 
 
 
 
 
 
 
36
  }
37
  {% endif %}
38
 
@@ -66,7 +77,7 @@ fn should_swap(a: {{ val_t }}, a_index: u32, b: {{ val_t }}, b_index: u32, ascen
66
  return select(descending_swap, ascending_swap, ascending);
67
  }
68
 
69
- {% if is_int and source.stage == "block" %}
70
  fn input_order_key(v: {{ scalar }}) -> u32 {
71
  {% if scalar == "i32" %}
72
  return bitcast<u32>(v) ^ 0x80000000u;
@@ -75,7 +86,7 @@ fn input_order_key(v: {{ scalar }}) -> u32 {
75
  {% endif %}
76
  }
77
  {% endif %}
78
- {% if is_int and source.stage == "output" %}
79
  fn key_to_value(k: u32) -> {{ scalar }} {
80
  {% if scalar == "i32" %}
81
  return bitcast<i32>(k ^ 0x80000000u);
@@ -85,21 +96,20 @@ fn key_to_value(k: u32) -> {{ scalar }} {
85
  }
86
  {% endif %}
87
 
88
- const SHARED_SIZE: u32 = {{ blockSize if source.stage == "block" else sharedSize }}u;
89
  const WG: u32 = {{ workgroupSize }}u;
90
 
91
- var<workgroup> shared_values: array<{{ val_t }}, {{ blockSize if source.stage == "block" else sharedSize }}>;
92
- var<workgroup> shared_indices: array<u32, {{ blockSize if source.stage == "block" else sharedSize }}>;
93
 
94
  @compute @workgroup_size(WG, 1, 1)
95
  fn main(
96
  @builtin(workgroup_id) workgroup: vec3<u32>,
97
  @builtin(local_invocation_id) local: vec3<u32>,
98
- @builtin(num_workgroups) num_workgroups: vec3<u32>,
99
  ) {
100
  let tid = local.x;
101
- {% if source.stage == "output" %}
102
- let row = workgroup.x + workgroup.y * num_workgroups.x;
103
  if (row >= params.rows) {
104
  return;
105
  }
@@ -107,16 +117,16 @@ fn main(
107
  let input_count = params.inCandidates;
108
  let input_base = row * input_count;
109
  {% else %}
110
- let flat_group = workgroup.x + workgroup.y * num_workgroups.x;
111
- let groups_per_row = params.{{ "blocks" if source.stage == "block" else "outGroups" }};
112
  if (flat_group >= params.rows * groups_per_row) {
113
  return;
114
  }
115
  let row = flat_group / groups_per_row;
116
  let group = flat_group % groups_per_row;
117
- {% if source.stage == "block" %}
118
  let input_count = params.cols;
119
- {% if source.strided is defined and source.strided %}
120
  // Strided (outer, axis, inner) view: row = outer * inner + innerIndex, and
121
  // axis element j sits at (outer * cols + j) * inner + innerIndex.
122
  let input_base = ((row / params.inner) * params.cols) * params.inner + (row % params.inner);
@@ -132,38 +142,31 @@ fn main(
132
  for (var slot = tid; slot < SHARED_SIZE; slot += WG) {
133
  let source_index = group * SHARED_SIZE + slot;
134
  if (source_index < input_count) {
135
- {% if source.stage == "block" %}
136
- {% set X_INDEX = "input_base + source_index * params.inner" if (source.strided is defined and source.strided) else "input_base + slot" %}
137
- shared_values[slot] = {{ "input_order_key" if is_int else "f32" }}(x[{{ X_INDEX }}]);
138
  shared_indices[slot] = source_index;
139
  {% else %}
140
  shared_values[slot] = candidateVals[input_base + slot];
141
  shared_indices[slot] = candidateIdxs[input_base + slot];
142
  {% endif %}
143
  } else {
144
- shared_values[slot] = {{ "0u" if is_int else "0.0" }};
145
  shared_indices[slot] = PAD_INDEX;
146
  }
147
  }
148
  workgroupBarrier();
149
 
150
- // In-place bitonic ordering of values and their original indices.
151
- for (var block_size = 2u; block_size <= SHARED_SIZE; block_size <<= 1u) {
152
- for (var gap = block_size >> 1u; gap > 0u; gap >>= 1u) {
153
- for (var pair = tid; pair < SHARED_SIZE / 2u; pair += WG) {
154
- // `gap` and `block_size` are powers of two by construction (block_size
155
- // doubles from 2, gap halves from block_size >> 1), so the index math is
156
- // masks and shifts. GPUs have no integer divide unit and neither
157
- // value is a compile-time constant here, so a literal `/` and `%` expand
158
- // to a full division per pair per stage — three of them, against a body
159
- // that is otherwise four shared loads and a compare.
160
  let position = pair & (gap - 1u);
161
  let left = ((pair - position) << 1u) + position;
162
  let right = left + gap;
163
  {% if attrs.largest == 0 %}
164
- let ascending = (left & block_size) == 0u;
165
  {% else %}
166
- let ascending = (left & block_size) != 0u;
167
  {% endif %}
168
  let left_value = shared_values[left];
169
  let right_value = shared_values[right];
@@ -176,26 +179,46 @@ fn main(
176
  shared_indices[right] = left_index;
177
  }
178
  }
 
 
 
 
 
 
179
  workgroupBarrier();
180
  }
181
  }
 
 
 
 
 
 
182
 
183
  for (var rank = tid; rank < params.k; rank += WG) {
184
- {% if source.stage == "block" %}
185
  let output_index = (row * params.blocks + group) * params.k + rank;
186
  candidateVals[output_index] = shared_values[rank];
187
  candidateIdxs[output_index] = shared_indices[rank];
188
- {% elif source.stage == "scratch" %}
189
  let output_index = (row * params.outGroups + group) * params.k + rank;
190
  candidateVals2[output_index] = shared_values[rank];
191
  candidateIdxs2[output_index] = shared_indices[rank];
192
  {% else %}
193
- {% if source.strided is defined and source.strided %}
194
  let output_index = ((row / params.inner) * params.k + rank) * params.inner + (row % params.inner);
195
  {% else %}
196
  let output_index = row * params.k + rank;
197
  {% endif %}
198
- values[output_index] = {{ "key_to_value" if is_int else scalar }}(shared_values[rank]);
 
 
 
 
 
 
 
 
199
  indices[output_index] = shared_indices[rank];
200
  {% endif %}
201
  }
 
1
+ {% if usesF16 and stage != "scratch" %}
2
  enable f16;
3
  {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
+ // All inputs sort by precomputed u32 order keys. Integer keys remain exact;
6
+ // floating outputs gather the source value after selection to retain its bits.
7
  {% set is_int = scalar == "i32" or scalar == "u32" %}
 
8
  {% set val_t = "u32" %}
9
  {% set order_scalar = "u32" %}
 
 
 
10
  // Stable bitonic comparator for TopK values and their original indices.
11
  // Stable order key and NaN predicate for direct, tournament, and bitonic TopK.
12
  {% if order_scalar is not defined %}{% set order_scalar = scalar %}{% endif %}
13
  const PAD_INDEX: u32 = 0xffffffffu;
14
 
15
+ {% macro float_order_key(value) %}
16
+ let bits0 = bitcast<u32>({{ value }});
17
+ let bits = select(bits0, 0u, bits0 == 0x80000000u);
18
+ return select(bits | 0x80000000u, ~bits, (bits & 0x80000000u) != 0u);
19
+ {% endmacro %}
20
  fn order_key(value: {{ val_t }}) -> u32 {
21
+ {% if order_scalar == "i32" %}
22
  return bitcast<u32>(value) ^ 0x80000000u;
23
+ {% elif order_scalar == "u32" %}
24
  return value;
25
  {% else %}
26
+ {{ float_order_key("value") }}
 
 
27
  {% endif %}
28
  }
29
 
30
  {% if not is_int %}
31
+ fn is_nan_value(value: {{ val_t }}) -> bool {
32
+ {% if floatOrderKeys is defined and floatOrderKeys %}
33
+ // Encoded infinities bound the finite keys; either outer range is NaN.
34
+ return value > 0xff800000u || value < 0x007fffffu;
35
+ {% else %}
36
  let bits = bitcast<u32>(value);
37
  return (bits & 0x7f800000u) == 0x7f800000u
38
  && (bits & 0x007fffffu) != 0u;
39
+ {% endif %}
40
+ }
41
+ {% endif %}
42
+ {% if floatOrderKeys is defined and floatOrderKeys and (stage is not defined or stage == "block") %}
43
+ // Encode once on input. Comparators consume keys, while output gathers the
44
+ // selected original values to preserve their representation, including -0.
45
+ fn input_order_key(value: {{ scalar }}) -> u32 {
46
+ {{ float_order_key("f32(value)") }}
47
  }
48
  {% endif %}
49
 
 
77
  return select(descending_swap, ascending_swap, ascending);
78
  }
79
 
80
+ {% if is_int and stage == "block" %}
81
  fn input_order_key(v: {{ scalar }}) -> u32 {
82
  {% if scalar == "i32" %}
83
  return bitcast<u32>(v) ^ 0x80000000u;
 
86
  {% endif %}
87
  }
88
  {% endif %}
89
+ {% if is_int and stage == "output" %}
90
  fn key_to_value(k: u32) -> {{ scalar }} {
91
  {% if scalar == "i32" %}
92
  return bitcast<i32>(k ^ 0x80000000u);
 
96
  }
97
  {% endif %}
98
 
99
+ const SHARED_SIZE: u32 = {{ blockSize if stage == "block" else sharedSize }}u;
100
  const WG: u32 = {{ workgroupSize }}u;
101
 
102
+ var<workgroup> shared_values: array<{{ val_t }}, {{ blockSize if stage == "block" else sharedSize }}>;
103
+ var<workgroup> shared_indices: array<u32, {{ blockSize if stage == "block" else sharedSize }}>;
104
 
105
  @compute @workgroup_size(WG, 1, 1)
106
  fn main(
107
  @builtin(workgroup_id) workgroup: vec3<u32>,
108
  @builtin(local_invocation_id) local: vec3<u32>,
 
109
  ) {
110
  let tid = local.x;
111
+ {% if stage == "output" %}
112
+ let row = workgroup.x + workgroup.y * {{ DISPATCH_FOLD_WIDTH }}u;
113
  if (row >= params.rows) {
114
  return;
115
  }
 
117
  let input_count = params.inCandidates;
118
  let input_base = row * input_count;
119
  {% else %}
120
+ let flat_group = workgroup.x + workgroup.y * {{ DISPATCH_FOLD_WIDTH }}u;
121
+ let groups_per_row = params.{{ "blocks" if stage == "block" else "outGroups" }};
122
  if (flat_group >= params.rows * groups_per_row) {
123
  return;
124
  }
125
  let row = flat_group / groups_per_row;
126
  let group = flat_group % groups_per_row;
127
+ {% if stage == "block" %}
128
  let input_count = params.cols;
129
+ {% if strided is defined and strided %}
130
  // Strided (outer, axis, inner) view: row = outer * inner + innerIndex, and
131
  // axis element j sits at (outer * cols + j) * inner + innerIndex.
132
  let input_base = ((row / params.inner) * params.cols) * params.inner + (row % params.inner);
 
142
  for (var slot = tid; slot < SHARED_SIZE; slot += WG) {
143
  let source_index = group * SHARED_SIZE + slot;
144
  if (source_index < input_count) {
145
+ {% if stage == "block" %}
146
+ {% set X_INDEX = "input_base + source_index * params.inner" if (strided is defined and strided) else "input_base + slot" %}
147
+ shared_values[slot] = input_order_key(x[{{ X_INDEX }}]);
148
  shared_indices[slot] = source_index;
149
  {% else %}
150
  shared_values[slot] = candidateVals[input_base + slot];
151
  shared_indices[slot] = candidateIdxs[input_base + slot];
152
  {% endif %}
153
  } else {
154
+ shared_values[slot] = 0u;
155
  shared_indices[slot] = PAD_INDEX;
156
  }
157
  }
158
  workgroupBarrier();
159
 
160
+ {% macro compare_pairs(pairCount, finalStage) %}
161
+ for (var pair = tid; pair < {{ pairCount }}; pair += WG) {
162
+ // Power-of-two gaps let masks and shifts identify disjoint pairs.
 
 
 
 
 
 
 
163
  let position = pair & (gap - 1u);
164
  let left = ((pair - position) << 1u) + position;
165
  let right = left + gap;
166
  {% if attrs.largest == 0 %}
167
+ let ascending = {{ "true" if finalStage else "(left & block_size) == 0u" }};
168
  {% else %}
169
+ let ascending = {{ "false" if finalStage else "(left & block_size) != 0u" }};
170
  {% endif %}
171
  let left_value = shared_values[left];
172
  let right_value = shared_values[right];
 
179
  shared_indices[right] = left_index;
180
  }
181
  }
182
+ {% endmacro %}
183
+ {% set sortSize = blockSize if (stage is defined and stage == "block") else sharedSize %}
184
+ {% set pruneFinal = sortSize > workgroupSize and sortRetainedSize < sortSize %}
185
+ for (var block_size = 2u; block_size {{ "<" if pruneFinal else "<=" }} SHARED_SIZE; block_size <<= 1u) {
186
+ for (var gap = block_size >> 1u; gap > 0u; gap >>= 1u) {
187
+ {{ compare_pairs("SHARED_SIZE / 2u", false) }}
188
  workgroupBarrier();
189
  }
190
  }
191
+ {% if pruneFinal %}
192
+ for (var gap = SHARED_SIZE >> 1u; gap > 0u; gap >>= 1u) {
193
+ {{ compare_pairs("max(gap, " ~ ((sortRetainedSize / 2)|int) ~ "u)", true) }}
194
+ workgroupBarrier();
195
+ }
196
+ {% endif %}
197
 
198
  for (var rank = tid; rank < params.k; rank += WG) {
199
+ {% if stage == "block" %}
200
  let output_index = (row * params.blocks + group) * params.k + rank;
201
  candidateVals[output_index] = shared_values[rank];
202
  candidateIdxs[output_index] = shared_indices[rank];
203
+ {% elif stage == "scratch" %}
204
  let output_index = (row * params.outGroups + group) * params.k + rank;
205
  candidateVals2[output_index] = shared_values[rank];
206
  candidateIdxs2[output_index] = shared_indices[rank];
207
  {% else %}
208
+ {% if strided is defined and strided %}
209
  let output_index = ((row / params.inner) * params.k + rank) * params.inner + (row % params.inner);
210
  {% else %}
211
  let output_index = row * params.k + rank;
212
  {% endif %}
213
+ {% if floatOrderKeys is defined and floatOrderKeys %}
214
+ {% if strided is defined and strided %}
215
+ values[output_index] = x[((row / params.inner) * params.cols + shared_indices[rank]) * params.inner + row % params.inner];
216
+ {% else %}
217
+ values[output_index] = x[row * params.cols + shared_indices[rank]];
218
+ {% endif %}
219
+ {% else %}
220
+ values[output_index] = key_to_value(shared_values[rank]);
221
+ {% endif %}
222
  indices[output_index] = shared_indices[rank];
223
  {% endif %}
224
  }
build/webgpu/topk-portable-rows-smallk.wgsl.jinja CHANGED
@@ -1,15 +1,11 @@
1
  // Portable one-workgroup-per-row TopK for many medium-width rows and small K.
2
- // Each lane keeps a small sorted register list for its strided columns. For each
3
  // output rank, lane zero scans one candidate per lane in shared memory. This
4
- // O(WG*K) merge avoids sorting and synchronizing the whole row while retaining
5
- // the subgroup route's register-local algorithmic structure.
6
- {% if usesF16 %}
7
- enable f16;
8
- {% endif %}
9
  {{ env.wgsl.resourceDeclarations }}
10
  // Stable TopK ordering. Native integer routes retain i32/u32 values; float and
11
  // split-scratch routes compare f32 values.
12
- {% set is_int = source.nativeValues and (scalar == "i32" or scalar == "u32") %}
13
  {% set val_t = scalar if is_int else "f32" %}
14
  {% set zero = scalar ~ "(0)" if is_int else "0.0" %}
15
 
@@ -17,24 +13,39 @@ enable f16;
17
  {% if order_scalar is not defined %}{% set order_scalar = scalar %}{% endif %}
18
  const PAD_INDEX: u32 = 0xffffffffu;
19
 
 
 
 
 
 
 
20
  fn order_key(value: {{ val_t }}) -> u32 {
21
- {% if is_int and order_scalar == "i32" %}
22
  return bitcast<u32>(value) ^ 0x80000000u;
23
- {% elif is_int and order_scalar == "u32" %}
24
  return value;
25
  {% else %}
26
- // IEEE regards signed zero as equal, so normalize -0 before tie-breaking.
27
- let bits0 = bitcast<u32>(value);
28
- let bits = select(bits0, 0u, bits0 == 0x80000000u);
29
- return select(bits | 0x80000000u, ~bits, (bits & 0x80000000u) != 0u);
30
  {% endif %}
31
  }
32
 
33
  {% if not is_int %}
34
- fn is_nan_value(value: f32) -> bool {
 
 
 
 
35
  let bits = bitcast<u32>(value);
36
  return (bits & 0x7f800000u) == 0x7f800000u
37
  && (bits & 0x007fffffu) != 0u;
 
 
 
 
 
 
 
 
38
  }
39
  {% endif %}
40
 
@@ -66,9 +77,8 @@ var<workgroup> winnerIndex: u32;
66
 
67
  @compute @workgroup_size(WG, 1, 1)
68
  fn main(@builtin(workgroup_id) wgid: vec3<u32>,
69
- @builtin(local_invocation_id) lid: vec3<u32>,
70
- @builtin(num_workgroups) nwg: vec3<u32>) {
71
- let row = wgid.x + wgid.y * nwg.x;
72
  if (row >= params.rows) { return; }
73
 
74
  var localValues: array<{{ val_t }}, {{ localItems }}>;
 
1
  // Portable one-workgroup-per-row TopK for many medium-width rows and small K.
2
+ // Each lane keeps a small sorted candidate list for its strided columns. For each
3
  // output rank, lane zero scans one candidate per lane in shared memory. This
4
+ // O(WG*K) merge avoids sorting and synchronizing the whole row.
 
 
 
 
5
  {{ env.wgsl.resourceDeclarations }}
6
  // Stable TopK ordering. Native integer routes retain i32/u32 values; float and
7
  // split-scratch routes compare f32 values.
8
+ {% set is_int = nativeValues and (scalar == "i32" or scalar == "u32") %}
9
  {% set val_t = scalar if is_int else "f32" %}
10
  {% set zero = scalar ~ "(0)" if is_int else "0.0" %}
11
 
 
13
  {% if order_scalar is not defined %}{% set order_scalar = scalar %}{% endif %}
14
  const PAD_INDEX: u32 = 0xffffffffu;
15
 
16
+ {% macro float_order_key(value) %}
17
+ // IEEE regards signed zero as equal, so normalize -0 before tie-breaking.
18
+ let bits0 = bitcast<u32>({{ value }});
19
+ let bits = select(bits0, 0u, bits0 == 0x80000000u);
20
+ return select(bits | 0x80000000u, ~bits, (bits & 0x80000000u) != 0u);
21
+ {% endmacro %}
22
  fn order_key(value: {{ val_t }}) -> u32 {
23
+ {% if order_scalar == "i32" %}
24
  return bitcast<u32>(value) ^ 0x80000000u;
25
+ {% elif order_scalar == "u32" %}
26
  return value;
27
  {% else %}
28
+ {{ float_order_key("value") }}
 
 
 
29
  {% endif %}
30
  }
31
 
32
  {% if not is_int %}
33
+ fn is_nan_value(value: {{ val_t }}) -> bool {
34
+ {% if floatOrderKeys is defined and floatOrderKeys %}
35
+ // Encoded infinities bound the finite keys; either outer range is NaN.
36
+ return value > 0xff800000u || value < 0x007fffffu;
37
+ {% else %}
38
  let bits = bitcast<u32>(value);
39
  return (bits & 0x7f800000u) == 0x7f800000u
40
  && (bits & 0x007fffffu) != 0u;
41
+ {% endif %}
42
+ }
43
+ {% endif %}
44
+ {% if floatOrderKeys is defined and floatOrderKeys and (stage is not defined or stage == "block") %}
45
+ // Encode once on input. Comparators consume keys, while output gathers the
46
+ // selected original values to preserve their representation, including -0.
47
+ fn input_order_key(value: {{ scalar }}) -> u32 {
48
+ {{ float_order_key("f32(value)") }}
49
  }
50
  {% endif %}
51
 
 
77
 
78
  @compute @workgroup_size(WG, 1, 1)
79
  fn main(@builtin(workgroup_id) wgid: vec3<u32>,
80
+ @builtin(local_invocation_id) lid: vec3<u32>) {
81
+ let row = wgid.x + wgid.y * {{ DISPATCH_FOLD_WIDTH }}u;
 
82
  if (row >= params.rows) { return; }
83
 
84
  var localValues: array<{{ val_t }}, {{ localItems }}>;
build/webgpu/topk-small-rows-batched.wgsl.jinja CHANGED
@@ -1,11 +1,8 @@
1
- {% if usesF16 %}
2
- enable f16;
3
- {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
  // One lane owns each independent short row and retains the native integer type.
6
  // Stable TopK ordering. Native integer routes retain i32/u32 values; float and
7
  // split-scratch routes compare f32 values.
8
- {% set is_int = source.nativeValues and (scalar == "i32" or scalar == "u32") %}
9
  {% set val_t = scalar if is_int else "f32" %}
10
  {% set zero = scalar ~ "(0)" if is_int else "0.0" %}
11
 
@@ -13,24 +10,39 @@ enable f16;
13
  {% if order_scalar is not defined %}{% set order_scalar = scalar %}{% endif %}
14
  const PAD_INDEX: u32 = 0xffffffffu;
15
 
 
 
 
 
 
 
16
  fn order_key(value: {{ val_t }}) -> u32 {
17
- {% if is_int and order_scalar == "i32" %}
18
  return bitcast<u32>(value) ^ 0x80000000u;
19
- {% elif is_int and order_scalar == "u32" %}
20
  return value;
21
  {% else %}
22
- // IEEE regards signed zero as equal, so normalize -0 before tie-breaking.
23
- let bits0 = bitcast<u32>(value);
24
- let bits = select(bits0, 0u, bits0 == 0x80000000u);
25
- return select(bits | 0x80000000u, ~bits, (bits & 0x80000000u) != 0u);
26
  {% endif %}
27
  }
28
 
29
  {% if not is_int %}
30
- fn is_nan_value(value: f32) -> bool {
 
 
 
 
31
  let bits = bitcast<u32>(value);
32
  return (bits & 0x7f800000u) == 0x7f800000u
33
  && (bits & 0x007fffffu) != 0u;
 
 
 
 
 
 
 
 
34
  }
35
  {% endif %}
36
 
@@ -60,10 +72,9 @@ const MAX_K: u32 = {{ tunables.SMALL_ROWS_MAX_K }}u;
60
  @compute @workgroup_size(WG, 1, 1)
61
  fn main(
62
  @builtin(global_invocation_id) gid: vec3<u32>,
63
- @builtin(num_workgroups) nwg: vec3<u32>,
64
  ) {
65
  // Rebuild the row index after a large grid folds into dispatch.y.
66
- let row = gid.x + gid.y * nwg.x * WG;
67
  if (row >= params.rows) {
68
  return;
69
  }
 
 
 
 
1
  {{ env.wgsl.resourceDeclarations }}
2
  // One lane owns each independent short row and retains the native integer type.
3
  // Stable TopK ordering. Native integer routes retain i32/u32 values; float and
4
  // split-scratch routes compare f32 values.
5
+ {% set is_int = nativeValues and (scalar == "i32" or scalar == "u32") %}
6
  {% set val_t = scalar if is_int else "f32" %}
7
  {% set zero = scalar ~ "(0)" if is_int else "0.0" %}
8
 
 
10
  {% if order_scalar is not defined %}{% set order_scalar = scalar %}{% endif %}
11
  const PAD_INDEX: u32 = 0xffffffffu;
12
 
13
+ {% macro float_order_key(value) %}
14
+ // IEEE regards signed zero as equal, so normalize -0 before tie-breaking.
15
+ let bits0 = bitcast<u32>({{ value }});
16
+ let bits = select(bits0, 0u, bits0 == 0x80000000u);
17
+ return select(bits | 0x80000000u, ~bits, (bits & 0x80000000u) != 0u);
18
+ {% endmacro %}
19
  fn order_key(value: {{ val_t }}) -> u32 {
20
+ {% if order_scalar == "i32" %}
21
  return bitcast<u32>(value) ^ 0x80000000u;
22
+ {% elif order_scalar == "u32" %}
23
  return value;
24
  {% else %}
25
+ {{ float_order_key("value") }}
 
 
 
26
  {% endif %}
27
  }
28
 
29
  {% if not is_int %}
30
+ fn is_nan_value(value: {{ val_t }}) -> bool {
31
+ {% if floatOrderKeys is defined and floatOrderKeys %}
32
+ // Encoded infinities bound the finite keys; either outer range is NaN.
33
+ return value > 0xff800000u || value < 0x007fffffu;
34
+ {% else %}
35
  let bits = bitcast<u32>(value);
36
  return (bits & 0x7f800000u) == 0x7f800000u
37
  && (bits & 0x007fffffu) != 0u;
38
+ {% endif %}
39
+ }
40
+ {% endif %}
41
+ {% if floatOrderKeys is defined and floatOrderKeys and (stage is not defined or stage == "block") %}
42
+ // Encode once on input. Comparators consume keys, while output gathers the
43
+ // selected original values to preserve their representation, including -0.
44
+ fn input_order_key(value: {{ scalar }}) -> u32 {
45
+ {{ float_order_key("f32(value)") }}
46
  }
47
  {% endif %}
48
 
 
72
  @compute @workgroup_size(WG, 1, 1)
73
  fn main(
74
  @builtin(global_invocation_id) gid: vec3<u32>,
 
75
  ) {
76
  // Rebuild the row index after a large grid folds into dispatch.y.
77
+ let row = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
78
  if (row >= params.rows) {
79
  return;
80
  }
build/webgpu/topk-strided-smallk.wgsl.jinja CHANGED
@@ -1,10 +1,7 @@
1
- {% if usesF16 %}
2
- enable f16;
3
- {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
  // Stable TopK ordering. Native integer routes retain i32/u32 values; float and
6
  // split-scratch routes compare f32 values.
7
- {% set is_int = source.nativeValues and (scalar == "i32" or scalar == "u32") %}
8
  {% set val_t = scalar if is_int else "f32" %}
9
  {% set zero = scalar ~ "(0)" if is_int else "0.0" %}
10
 
@@ -12,24 +9,39 @@ enable f16;
12
  {% if order_scalar is not defined %}{% set order_scalar = scalar %}{% endif %}
13
  const PAD_INDEX: u32 = 0xffffffffu;
14
 
 
 
 
 
 
 
15
  fn order_key(value: {{ val_t }}) -> u32 {
16
- {% if is_int and order_scalar == "i32" %}
17
  return bitcast<u32>(value) ^ 0x80000000u;
18
- {% elif is_int and order_scalar == "u32" %}
19
  return value;
20
  {% else %}
21
- // IEEE regards signed zero as equal, so normalize -0 before tie-breaking.
22
- let bits0 = bitcast<u32>(value);
23
- let bits = select(bits0, 0u, bits0 == 0x80000000u);
24
- return select(bits | 0x80000000u, ~bits, (bits & 0x80000000u) != 0u);
25
  {% endif %}
26
  }
27
 
28
  {% if not is_int %}
29
- fn is_nan_value(value: f32) -> bool {
 
 
 
 
30
  let bits = bitcast<u32>(value);
31
  return (bits & 0x7f800000u) == 0x7f800000u
32
  && (bits & 0x007fffffu) != 0u;
 
 
 
 
 
 
 
 
33
  }
34
  {% endif %}
35
 
@@ -62,10 +74,9 @@ var<workgroup> shared_indices: array<u32, {{ workgroupSize }}>;
62
  @compute @workgroup_size(WG, 1, 1)
63
  fn main(
64
  @builtin(workgroup_id) workgroup: vec3<u32>,
65
- @builtin(num_workgroups) nwg: vec3<u32>,
66
  @builtin(local_invocation_id) local: vec3<u32>,
67
  ) {
68
- let output_position = workgroup.x + workgroup.y * nwg.x;
69
  let tid = local.x;
70
  if (output_position >= params.outputPositions) {
71
  return;
 
 
 
 
1
  {{ env.wgsl.resourceDeclarations }}
2
  // Stable TopK ordering. Native integer routes retain i32/u32 values; float and
3
  // split-scratch routes compare f32 values.
4
+ {% set is_int = nativeValues and (scalar == "i32" or scalar == "u32") %}
5
  {% set val_t = scalar if is_int else "f32" %}
6
  {% set zero = scalar ~ "(0)" if is_int else "0.0" %}
7
 
 
9
  {% if order_scalar is not defined %}{% set order_scalar = scalar %}{% endif %}
10
  const PAD_INDEX: u32 = 0xffffffffu;
11
 
12
+ {% macro float_order_key(value) %}
13
+ // IEEE regards signed zero as equal, so normalize -0 before tie-breaking.
14
+ let bits0 = bitcast<u32>({{ value }});
15
+ let bits = select(bits0, 0u, bits0 == 0x80000000u);
16
+ return select(bits | 0x80000000u, ~bits, (bits & 0x80000000u) != 0u);
17
+ {% endmacro %}
18
  fn order_key(value: {{ val_t }}) -> u32 {
19
+ {% if order_scalar == "i32" %}
20
  return bitcast<u32>(value) ^ 0x80000000u;
21
+ {% elif order_scalar == "u32" %}
22
  return value;
23
  {% else %}
24
+ {{ float_order_key("value") }}
 
 
 
25
  {% endif %}
26
  }
27
 
28
  {% if not is_int %}
29
+ fn is_nan_value(value: {{ val_t }}) -> bool {
30
+ {% if floatOrderKeys is defined and floatOrderKeys %}
31
+ // Encoded infinities bound the finite keys; either outer range is NaN.
32
+ return value > 0xff800000u || value < 0x007fffffu;
33
+ {% else %}
34
  let bits = bitcast<u32>(value);
35
  return (bits & 0x7f800000u) == 0x7f800000u
36
  && (bits & 0x007fffffu) != 0u;
37
+ {% endif %}
38
+ }
39
+ {% endif %}
40
+ {% if floatOrderKeys is defined and floatOrderKeys and (stage is not defined or stage == "block") %}
41
+ // Encode once on input. Comparators consume keys, while output gathers the
42
+ // selected original values to preserve their representation, including -0.
43
+ fn input_order_key(value: {{ scalar }}) -> u32 {
44
+ {{ float_order_key("f32(value)") }}
45
  }
46
  {% endif %}
47
 
 
74
  @compute @workgroup_size(WG, 1, 1)
75
  fn main(
76
  @builtin(workgroup_id) workgroup: vec3<u32>,
 
77
  @builtin(local_invocation_id) local: vec3<u32>,
78
  ) {
79
+ let output_position = workgroup.x + workgroup.y * {{ DISPATCH_FOLD_WIDTH }}u;
80
  let tid = local.x;
81
  if (output_position >= params.outputPositions) {
82
  return;
build/webgpu/topk-subgroup-rows.wgsl.jinja CHANGED
@@ -1,7 +1,4 @@
1
  enable subgroups;
2
- {% if usesF16 %}
3
- enable f16;
4
- {% endif %}
5
  {{ env.wgsl.resourceDeclarations }}
6
  // One subgroup owns one contiguous row. Each lane keeps a short local TopK, then
7
  // log2(SG) shuffle reductions choose each row winner without workgroup memory or
@@ -9,7 +6,7 @@ enable f16;
9
  // variable-width routes use one minimum-width subgroup per workgroup.
10
  // Stable TopK ordering. Native integer routes retain i32/u32 values; float and
11
  // split-scratch routes compare f32 values.
12
- {% set is_int = source.nativeValues and (scalar == "i32" or scalar == "u32") %}
13
  {% set val_t = scalar if is_int else "f32" %}
14
  {% set zero = scalar ~ "(0)" if is_int else "0.0" %}
15
 
@@ -17,24 +14,39 @@ enable f16;
17
  {% if order_scalar is not defined %}{% set order_scalar = scalar %}{% endif %}
18
  const PAD_INDEX: u32 = 0xffffffffu;
19
 
 
 
 
 
 
 
20
  fn order_key(value: {{ val_t }}) -> u32 {
21
- {% if is_int and order_scalar == "i32" %}
22
  return bitcast<u32>(value) ^ 0x80000000u;
23
- {% elif is_int and order_scalar == "u32" %}
24
  return value;
25
  {% else %}
26
- // IEEE regards signed zero as equal, so normalize -0 before tie-breaking.
27
- let bits0 = bitcast<u32>(value);
28
- let bits = select(bits0, 0u, bits0 == 0x80000000u);
29
- return select(bits | 0x80000000u, ~bits, (bits & 0x80000000u) != 0u);
30
  {% endif %}
31
  }
32
 
33
  {% if not is_int %}
34
- fn is_nan_value(value: f32) -> bool {
 
 
 
 
35
  let bits = bitcast<u32>(value);
36
  return (bits & 0x7f800000u) == 0x7f800000u
37
  && (bits & 0x007fffffu) != 0u;
 
 
 
 
 
 
 
 
38
  }
39
  {% endif %}
40
 
@@ -66,11 +78,10 @@ const LOCAL_ITEMS: u32 = {{ localItems }}u;
66
  @compute @workgroup_size(WG, 1, 1)
67
  fn main(
68
  @builtin(workgroup_id) workgroup: vec3<u32>,
69
- @builtin(num_workgroups) nwg: vec3<u32>,
70
  @builtin(subgroup_id) subgroup_id: u32,
71
  @builtin(subgroup_invocation_id) lane: u32,
72
  ) {
73
- let group = workgroup.x + workgroup.y * nwg.x;
74
  let row = group * SUBGROUPS_PER_WG + subgroup_id;
75
  if (row >= params.rows) {
76
  return;
 
1
  enable subgroups;
 
 
 
2
  {{ env.wgsl.resourceDeclarations }}
3
  // One subgroup owns one contiguous row. Each lane keeps a short local TopK, then
4
  // log2(SG) shuffle reductions choose each row winner without workgroup memory or
 
6
  // variable-width routes use one minimum-width subgroup per workgroup.
7
  // Stable TopK ordering. Native integer routes retain i32/u32 values; float and
8
  // split-scratch routes compare f32 values.
9
+ {% set is_int = nativeValues and (scalar == "i32" or scalar == "u32") %}
10
  {% set val_t = scalar if is_int else "f32" %}
11
  {% set zero = scalar ~ "(0)" if is_int else "0.0" %}
12
 
 
14
  {% if order_scalar is not defined %}{% set order_scalar = scalar %}{% endif %}
15
  const PAD_INDEX: u32 = 0xffffffffu;
16
 
17
+ {% macro float_order_key(value) %}
18
+ // IEEE regards signed zero as equal, so normalize -0 before tie-breaking.
19
+ let bits0 = bitcast<u32>({{ value }});
20
+ let bits = select(bits0, 0u, bits0 == 0x80000000u);
21
+ return select(bits | 0x80000000u, ~bits, (bits & 0x80000000u) != 0u);
22
+ {% endmacro %}
23
  fn order_key(value: {{ val_t }}) -> u32 {
24
+ {% if order_scalar == "i32" %}
25
  return bitcast<u32>(value) ^ 0x80000000u;
26
+ {% elif order_scalar == "u32" %}
27
  return value;
28
  {% else %}
29
+ {{ float_order_key("value") }}
 
 
 
30
  {% endif %}
31
  }
32
 
33
  {% if not is_int %}
34
+ fn is_nan_value(value: {{ val_t }}) -> bool {
35
+ {% if floatOrderKeys is defined and floatOrderKeys %}
36
+ // Encoded infinities bound the finite keys; either outer range is NaN.
37
+ return value > 0xff800000u || value < 0x007fffffu;
38
+ {% else %}
39
  let bits = bitcast<u32>(value);
40
  return (bits & 0x7f800000u) == 0x7f800000u
41
  && (bits & 0x007fffffu) != 0u;
42
+ {% endif %}
43
+ }
44
+ {% endif %}
45
+ {% if floatOrderKeys is defined and floatOrderKeys and (stage is not defined or stage == "block") %}
46
+ // Encode once on input. Comparators consume keys, while output gathers the
47
+ // selected original values to preserve their representation, including -0.
48
+ fn input_order_key(value: {{ scalar }}) -> u32 {
49
+ {{ float_order_key("f32(value)") }}
50
  }
51
  {% endif %}
52
 
 
78
  @compute @workgroup_size(WG, 1, 1)
79
  fn main(
80
  @builtin(workgroup_id) workgroup: vec3<u32>,
 
81
  @builtin(subgroup_id) subgroup_id: u32,
82
  @builtin(subgroup_invocation_id) lane: u32,
83
  ) {
84
+ let group = workgroup.x + workgroup.y * {{ DISPATCH_FOLD_WIDTH }}u;
85
  let row = group * SUBGROUPS_PER_WG + subgroup_id;
86
  if (row >= params.rows) {
87
  return;
build/webgpu/topk-top1-last-axis.wgsl.jinja CHANGED
@@ -1,10 +1,7 @@
1
- {% if usesF16 %}
2
- enable f16;
3
- {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
  // Stable TopK ordering. Native integer routes retain i32/u32 values; float and
6
  // split-scratch routes compare f32 values.
7
- {% set is_int = source.nativeValues and (scalar == "i32" or scalar == "u32") %}
8
  {% set val_t = scalar if is_int else "f32" %}
9
  {% set zero = scalar ~ "(0)" if is_int else "0.0" %}
10
 
@@ -12,24 +9,39 @@ enable f16;
12
  {% if order_scalar is not defined %}{% set order_scalar = scalar %}{% endif %}
13
  const PAD_INDEX: u32 = 0xffffffffu;
14
 
 
 
 
 
 
 
15
  fn order_key(value: {{ val_t }}) -> u32 {
16
- {% if is_int and order_scalar == "i32" %}
17
  return bitcast<u32>(value) ^ 0x80000000u;
18
- {% elif is_int and order_scalar == "u32" %}
19
  return value;
20
  {% else %}
21
- // IEEE regards signed zero as equal, so normalize -0 before tie-breaking.
22
- let bits0 = bitcast<u32>(value);
23
- let bits = select(bits0, 0u, bits0 == 0x80000000u);
24
- return select(bits | 0x80000000u, ~bits, (bits & 0x80000000u) != 0u);
25
  {% endif %}
26
  }
27
 
28
  {% if not is_int %}
29
- fn is_nan_value(value: f32) -> bool {
 
 
 
 
30
  let bits = bitcast<u32>(value);
31
  return (bits & 0x7f800000u) == 0x7f800000u
32
  && (bits & 0x007fffffu) != 0u;
 
 
 
 
 
 
 
 
33
  }
34
  {% endif %}
35
 
@@ -54,7 +66,7 @@ fn is_better(value: {{ val_t }}, index: u32, best: {{ val_t }}, best_index: u32)
54
 
55
 
56
  const WG: u32 = {{ workgroupSize }}u;
57
- {% if source.stage == "block" %}
58
  const BLOCK_SIZE: u32 = {{ top1BlockSize }}u;
59
 
60
  {% endif %}
@@ -65,11 +77,10 @@ var<workgroup> shared_indices: array<u32, {{ workgroupSize }}>;
65
  fn main(
66
  @builtin(workgroup_id) workgroup: vec3<u32>,
67
  @builtin(local_invocation_id) local: vec3<u32>,
68
- @builtin(num_workgroups) num_workgroups: vec3<u32>,
69
  ) {
70
  let tid = local.x;
71
- {% if source.stage == "block" %}
72
- let flat_group = workgroup.x + workgroup.y * num_workgroups.x;
73
  if (flat_group >= params.rows * params.blocks) {
74
  return;
75
  }
@@ -78,11 +89,11 @@ fn main(
78
  let begin = block * BLOCK_SIZE;
79
  let end = min(begin + BLOCK_SIZE, params.cols);
80
  {% else %}
81
- let row = workgroup.x + workgroup.y * num_workgroups.x;
82
  if (row >= params.rows) {
83
  return;
84
  }
85
- {% if source.stage == "direct" %}
86
  let begin = 0u;
87
  let end = params.cols;
88
  {% endif %}
@@ -90,7 +101,7 @@ fn main(
90
 
91
  var best_value = {{ zero }};
92
  var best_index = PAD_INDEX;
93
- {% if source.stage == "output" %}
94
  let input_base = row * params.blocks;
95
  for (var candidate = tid; candidate < params.blocks; candidate += WG) {
96
  let value = candidateVals[input_base + candidate];
@@ -130,7 +141,7 @@ fn main(
130
  }
131
 
132
  if (tid == 0u) {
133
- {% if source.stage == "block" %}
134
  let output_index = row * params.blocks + block;
135
  candidateVals[output_index] = shared_values[0];
136
  candidateIdxs[output_index] = shared_indices[0];
 
 
 
 
1
  {{ env.wgsl.resourceDeclarations }}
2
  // Stable TopK ordering. Native integer routes retain i32/u32 values; float and
3
  // split-scratch routes compare f32 values.
4
+ {% set is_int = nativeValues and (scalar == "i32" or scalar == "u32") %}
5
  {% set val_t = scalar if is_int else "f32" %}
6
  {% set zero = scalar ~ "(0)" if is_int else "0.0" %}
7
 
 
9
  {% if order_scalar is not defined %}{% set order_scalar = scalar %}{% endif %}
10
  const PAD_INDEX: u32 = 0xffffffffu;
11
 
12
+ {% macro float_order_key(value) %}
13
+ // IEEE regards signed zero as equal, so normalize -0 before tie-breaking.
14
+ let bits0 = bitcast<u32>({{ value }});
15
+ let bits = select(bits0, 0u, bits0 == 0x80000000u);
16
+ return select(bits | 0x80000000u, ~bits, (bits & 0x80000000u) != 0u);
17
+ {% endmacro %}
18
  fn order_key(value: {{ val_t }}) -> u32 {
19
+ {% if order_scalar == "i32" %}
20
  return bitcast<u32>(value) ^ 0x80000000u;
21
+ {% elif order_scalar == "u32" %}
22
  return value;
23
  {% else %}
24
+ {{ float_order_key("value") }}
 
 
 
25
  {% endif %}
26
  }
27
 
28
  {% if not is_int %}
29
+ fn is_nan_value(value: {{ val_t }}) -> bool {
30
+ {% if floatOrderKeys is defined and floatOrderKeys %}
31
+ // Encoded infinities bound the finite keys; either outer range is NaN.
32
+ return value > 0xff800000u || value < 0x007fffffu;
33
+ {% else %}
34
  let bits = bitcast<u32>(value);
35
  return (bits & 0x7f800000u) == 0x7f800000u
36
  && (bits & 0x007fffffu) != 0u;
37
+ {% endif %}
38
+ }
39
+ {% endif %}
40
+ {% if floatOrderKeys is defined and floatOrderKeys and (stage is not defined or stage == "block") %}
41
+ // Encode once on input. Comparators consume keys, while output gathers the
42
+ // selected original values to preserve their representation, including -0.
43
+ fn input_order_key(value: {{ scalar }}) -> u32 {
44
+ {{ float_order_key("f32(value)") }}
45
  }
46
  {% endif %}
47
 
 
66
 
67
 
68
  const WG: u32 = {{ workgroupSize }}u;
69
+ {% if stage == "block" %}
70
  const BLOCK_SIZE: u32 = {{ top1BlockSize }}u;
71
 
72
  {% endif %}
 
77
  fn main(
78
  @builtin(workgroup_id) workgroup: vec3<u32>,
79
  @builtin(local_invocation_id) local: vec3<u32>,
 
80
  ) {
81
  let tid = local.x;
82
+ {% if stage == "block" %}
83
+ let flat_group = workgroup.x + workgroup.y * {{ DISPATCH_FOLD_WIDTH }}u;
84
  if (flat_group >= params.rows * params.blocks) {
85
  return;
86
  }
 
89
  let begin = block * BLOCK_SIZE;
90
  let end = min(begin + BLOCK_SIZE, params.cols);
91
  {% else %}
92
+ let row = workgroup.x + workgroup.y * {{ DISPATCH_FOLD_WIDTH }}u;
93
  if (row >= params.rows) {
94
  return;
95
  }
96
+ {% if stage == "direct" %}
97
  let begin = 0u;
98
  let end = params.cols;
99
  {% endif %}
 
101
 
102
  var best_value = {{ zero }};
103
  var best_index = PAD_INDEX;
104
+ {% if stage == "output" %}
105
  let input_base = row * params.blocks;
106
  for (var candidate = tid; candidate < params.blocks; candidate += WG) {
107
  let value = candidateVals[input_base + candidate];
 
141
  }
142
 
143
  if (tid == 0u) {
144
+ {% if stage == "block" %}
145
  let output_index = row * params.blocks + block;
146
  candidateVals[output_index] = shared_values[0];
147
  candidateIdxs[output_index] = shared_indices[0];