Xenova HF Staff commited on
Commit
35f8daf
·
verified ·
1 Parent(s): f18314a

sync c928d21e6cc1

Browse files
README.md CHANGED
@@ -60,12 +60,17 @@ Attributes and default values (overridable per request):
60
  | `T` | `float32`, `float16` |
61
  | `M` | `int32` |
62
 
 
 
 
 
63
  ## Files
64
 
65
  - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
66
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
67
  - [`test.json`](build/webgpu/test.json) — correctness cases
68
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
 
69
  - [`sparse-attention.wgsl.jinja`](build/webgpu/sparse-attention.wgsl.jinja)
70
  - [`sparse-kv-append.wgsl.jinja`](build/webgpu/sparse-kv-append.wgsl.jinja)
71
  - [`sparse-q-rotary.wgsl.jinja`](build/webgpu/sparse-q-rotary.wgsl.jinja)
 
60
  | `T` | `float32`, `float16` |
61
  | `M` | `int32` |
62
 
63
+ ## Device requirements
64
+
65
+ Some implementation variants require `subgroup-matrix` and `subgroups`. These are route-specific capabilities, not package-wide requirements; availability also depends on the request shape and dtype.
66
+
67
  ## Files
68
 
69
  - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
70
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
71
  - [`test.json`](build/webgpu/test.json) — correctness cases
72
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
73
+ - [`sparse-attention-sgmat.wgsl.jinja`](build/webgpu/sparse-attention-sgmat.wgsl.jinja)
74
  - [`sparse-attention.wgsl.jinja`](build/webgpu/sparse-attention.wgsl.jinja)
75
  - [`sparse-kv-append.wgsl.jinja`](build/webgpu/sparse-kv-append.wgsl.jinja)
76
  - [`sparse-q-rotary.wgsl.jinja`](build/webgpu/sparse-q-rotary.wgsl.jinja)
build/webgpu/manifest.json CHANGED
@@ -131,8 +131,18 @@
131
  "sinCacheT": { "kind": "tensor", "semantic": "sin_cache", "role": "input", "required": false },
132
  "outputT": { "kind": "tensor", "semantic": "output", "role": "output" }
133
  },
134
- "tunables": { "WORKGROUP_SIZE": 128, "APPEND_WORKGROUP_SIZE": 256, "NARROW_MIN_WORKGROUPS": 1024, "QUERY_TILE": 4 },
 
 
 
 
 
 
135
  "derive": {
 
 
 
 
136
  "batchSize": "dim(shapes.queryT, 0)",
137
  "seqLen": "dim(shapes.queryT, 1)",
138
  "numHeads": "attrs.num_heads",
@@ -173,7 +183,12 @@
173
  "packedContract": "contract and packedQkv and not useRotary",
174
  "packedRotaryContract": "contract and packedQkv and useRotary",
175
  "separateContract": "contract and not packedQkv and not useRotary",
176
- "separateRotaryContract": "contract and not packedQkv and useRotary"
 
 
 
 
 
177
  },
178
  "constants": {
179
  "scalar": "dtypes.T",
@@ -556,6 +571,76 @@
556
  }
557
  }
558
  ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
  "attentionRotary": [
560
  { "name": "q_rotary", "semantic": "QRotary", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
561
  {
@@ -619,6 +704,70 @@
619
  ]
620
  }
621
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
622
  ]
623
  },
624
  "variants": [
@@ -646,6 +795,36 @@
646
  "dispatch": { "x": "sparseQueryTiles", "y": "batchSize * numHeads" },
647
  "constants": { "qTile": "sparseQueryTile" }
648
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
649
  ]
650
  },
651
  {
@@ -683,6 +862,47 @@
683
  "dispatch": { "x": "sparseQueryTiles", "y": "batchSize * numHeads" },
684
  "constants": { "qTile": "sparseQueryTile" }
685
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
686
  ]
687
  },
688
  {
@@ -709,6 +929,36 @@
709
  "dispatch": { "x": "sparseQueryTiles", "y": "batchSize * numHeads" },
710
  "constants": { "qTile": "sparseQueryTile" }
711
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
  ]
713
  },
714
  {
@@ -746,6 +996,47 @@
746
  "dispatch": { "x": "sparseQueryTiles", "y": "batchSize * numHeads" },
747
  "constants": { "qTile": "sparseQueryTile" }
748
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
749
  ]
750
  }
751
  ]
 
131
  "sinCacheT": { "kind": "tensor", "semantic": "sin_cache", "role": "input", "required": false },
132
  "outputT": { "kind": "tensor", "semantic": "output", "role": "output" }
133
  },
134
+ "tunables": {
135
+ "WORKGROUP_SIZE": 128,
136
+ "APPEND_WORKGROUP_SIZE": 256,
137
+ "NARROW_MIN_WORKGROUPS": 1024,
138
+ "QUERY_TILE": 4,
139
+ "V_STAGE_MAX_WORKGROUPS": 512
140
+ },
141
  "derive": {
142
+ "wave32Adapter": "has(device.adapterInfo, \"subgroupMinSize\") and has(device.adapterInfo, \"subgroupMaxSize\") and device.adapterInfo.subgroupMinSize == 32 and device.adapterInfo.subgroupMaxSize == 32",
143
+ "canPinSubgroupSize32": "device.features.has(\"subgroups\") and device.features.has(\"subgroup-size-control\") and has(device.adapterInfo, \"subgroupMinSize\") and has(device.adapterInfo, \"subgroupMaxSize\") and device.adapterInfo.subgroupMinSize <= 32 and device.adapterInfo.subgroupMaxSize >= 32",
144
+ "pinSubgroupSize32": "canPinSubgroupSize32 and not wave32Adapter",
145
+ "wave32Effective": "wave32Adapter or pinSubgroupSize32",
146
  "batchSize": "dim(shapes.queryT, 0)",
147
  "seqLen": "dim(shapes.queryT, 1)",
148
  "numHeads": "attrs.num_heads",
 
183
  "packedContract": "contract and packedQkv and not useRotary",
184
  "packedRotaryContract": "contract and packedQkv and useRotary",
185
  "separateContract": "contract and not packedQkv and not useRotary",
186
+ "separateRotaryContract": "contract and not packedQkv and useRotary",
187
+ "sparseVStageWorthIt": "sparseQueryTiles * batchSize * numHeads <= tunables.V_STAGE_MAX_WORKGROUPS",
188
+ "sgmatQueryTiles": "ceilDiv(seqLen, 64)",
189
+ "sparseSgmatLdsBytes": "(64 * 32 + 64 * 64 + 64 * 2 + 128 * 2) * 4",
190
+ "sparseSgmatGeometryOk": "256 <= device.limits.maxComputeInvocationsPerWorkgroup and 256 <= device.limits.maxComputeWorkgroupSizeX and sgmatQueryTiles <= device.limits.maxComputeWorkgroupsPerDimension and batchSize * numHeads <= device.limits.maxComputeWorkgroupsPerDimension and sparseSgmatLdsBytes <= device.limits.maxComputeWorkgroupStorageSize",
191
+ "sparseSgmatOk": "tensorDtypes.queryT == \"float32\" and seqLen >= 64 and sparseBlockSize % 64 == 0 and headSize % 32 == 0 and headSize <= 128 and maxCacheSeq % 64 == 0 and device.features.has(\"subgroups\") and wave32Effective and device.features.has(\"chromium-experimental-subgroup-matrix\") and sparseSgmatGeometryOk"
192
  },
193
  "constants": {
194
  "scalar": "dtypes.T",
 
571
  }
572
  }
573
  ],
574
+ "attentionSgmat": [
575
+ {
576
+ "name": "query",
577
+ "arg": "queryT",
578
+ "semantic": "query",
579
+ "buffer": { "type": "read-only-storage" },
580
+ "elementType": "$scalar"
581
+ },
582
+ {
583
+ "name": "present_key",
584
+ "arg": "pastKeyT",
585
+ "semantic": "past_key",
586
+ "buffer": { "type": "read-only-storage" },
587
+ "elementType": "$scalar"
588
+ },
589
+ {
590
+ "name": "present_value",
591
+ "arg": "pastValueT",
592
+ "semantic": "past_value",
593
+ "buffer": { "type": "read-only-storage" },
594
+ "elementType": "$scalar"
595
+ },
596
+ {
597
+ "name": "block_row_indices",
598
+ "arg": "blockRowIndicesT",
599
+ "semantic": "block_row_indices",
600
+ "buffer": { "type": "read-only-storage" },
601
+ "elementType": "i32"
602
+ },
603
+ {
604
+ "name": "block_col_indices",
605
+ "arg": "blockColIndicesT",
606
+ "semantic": "block_col_indices",
607
+ "buffer": { "type": "read-only-storage" },
608
+ "elementType": "i32"
609
+ },
610
+ {
611
+ "name": "key_total_sequence_lengths",
612
+ "arg": "keyTotalSequenceLengthsT",
613
+ "semantic": "key_total_sequence_lengths",
614
+ "buffer": { "type": "read-only-storage" },
615
+ "elementType": "i32"
616
+ },
617
+ {
618
+ "name": "total_sequence_length",
619
+ "arg": "totalSequenceLengthT",
620
+ "semantic": "total_sequence_length",
621
+ "buffer": { "type": "read-only-storage" },
622
+ "elementType": "i32"
623
+ },
624
+ {
625
+ "name": "output",
626
+ "arg": "outputT",
627
+ "semantic": "output",
628
+ "buffer": { "type": "storage" },
629
+ "elementType": "$scalar"
630
+ },
631
+ {
632
+ "name": "params",
633
+ "semantic": "kernel.params",
634
+ "buffer": { "type": "uniform" },
635
+ "struct": {
636
+ "name": "Params",
637
+ "fields": [
638
+ { "name": "seqLen", "type": "u32", "value": "seqLen" },
639
+ { "name": "scale", "type": "f32", "value": "attrs.scale if has(attrs, \"scale\") else 0" }
640
+ ]
641
+ }
642
+ }
643
+ ],
644
  "attentionRotary": [
645
  { "name": "q_rotary", "semantic": "QRotary", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
646
  {
 
704
  ]
705
  }
706
  }
707
+ ],
708
+ "attentionSgmatRotary": [
709
+ { "name": "q_rotary", "semantic": "QRotary", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
710
+ {
711
+ "name": "present_key",
712
+ "arg": "pastKeyT",
713
+ "semantic": "past_key",
714
+ "buffer": { "type": "read-only-storage" },
715
+ "elementType": "$scalar"
716
+ },
717
+ {
718
+ "name": "present_value",
719
+ "arg": "pastValueT",
720
+ "semantic": "past_value",
721
+ "buffer": { "type": "read-only-storage" },
722
+ "elementType": "$scalar"
723
+ },
724
+ {
725
+ "name": "block_row_indices",
726
+ "arg": "blockRowIndicesT",
727
+ "semantic": "block_row_indices",
728
+ "buffer": { "type": "read-only-storage" },
729
+ "elementType": "i32"
730
+ },
731
+ {
732
+ "name": "block_col_indices",
733
+ "arg": "blockColIndicesT",
734
+ "semantic": "block_col_indices",
735
+ "buffer": { "type": "read-only-storage" },
736
+ "elementType": "i32"
737
+ },
738
+ {
739
+ "name": "key_total_sequence_lengths",
740
+ "arg": "keyTotalSequenceLengthsT",
741
+ "semantic": "key_total_sequence_lengths",
742
+ "buffer": { "type": "read-only-storage" },
743
+ "elementType": "i32"
744
+ },
745
+ {
746
+ "name": "total_sequence_length",
747
+ "arg": "totalSequenceLengthT",
748
+ "semantic": "total_sequence_length",
749
+ "buffer": { "type": "read-only-storage" },
750
+ "elementType": "i32"
751
+ },
752
+ {
753
+ "name": "output",
754
+ "arg": "outputT",
755
+ "semantic": "output",
756
+ "buffer": { "type": "storage" },
757
+ "elementType": "$scalar"
758
+ },
759
+ {
760
+ "name": "params",
761
+ "semantic": "kernel.params",
762
+ "buffer": { "type": "uniform" },
763
+ "struct": {
764
+ "name": "Params",
765
+ "fields": [
766
+ { "name": "seqLen", "type": "u32", "value": "seqLen" },
767
+ { "name": "scale", "type": "f32", "value": "attrs.scale if has(attrs, \"scale\") else 0" }
768
+ ]
769
+ }
770
+ }
771
  ]
772
  },
773
  "variants": [
 
795
  "dispatch": { "x": "sparseQueryTiles", "y": "batchSize * numHeads" },
796
  "constants": { "qTile": "sparseQueryTile" }
797
  }
798
+ ],
799
+ "constants": { "vStageWorthIt": "sparseVStageWorthIt" }
800
+ },
801
+ {
802
+ "id": "separate_sgmat",
803
+ "priority": 20,
804
+ "when": ["separateContract", "geometryOk", "sparseSgmatOk"],
805
+ "requires": {
806
+ "features": ["subgroups", "chromium-experimental-subgroup-matrix"],
807
+ "subgroupMatrixConfigs": [{ "componentType": "f32", "resultComponentType": "f32", "M": 8, "N": 8, "K": 8 }]
808
+ },
809
+ "passes": [
810
+ {
811
+ "id": "append",
812
+ "name": "SparseAttention.Append",
813
+ "shader": "sparse-kv-append.wgsl.jinja",
814
+ "bindings": "appendSeparate",
815
+ "constants": { "kvSource": "\"new_key\"", "vSource": "\"new_value\"" },
816
+ "dispatch": {
817
+ "threads": "batchSize * kvNumHeads * seqLen * headSize",
818
+ "workgroupSize": "constants.appendWorkgroupSize"
819
+ }
820
+ },
821
+ {
822
+ "id": "attention",
823
+ "name": "SparseAttention.AttentionSgmat",
824
+ "shader": "sparse-attention-sgmat.wgsl.jinja",
825
+ "bindings": "attentionSgmat",
826
+ "dispatch": { "x": "sgmatQueryTiles", "y": "batchSize * numHeads" }
827
+ }
828
  ]
829
  },
830
  {
 
862
  "dispatch": { "x": "sparseQueryTiles", "y": "batchSize * numHeads" },
863
  "constants": { "qTile": "sparseQueryTile" }
864
  }
865
+ ],
866
+ "constants": { "vStageWorthIt": "sparseVStageWorthIt" }
867
+ },
868
+ {
869
+ "id": "separate_rotary_sgmat",
870
+ "priority": 30,
871
+ "when": ["separateRotaryContract", "geometryOk", "sparseSgmatOk"],
872
+ "requires": {
873
+ "features": ["subgroups", "chromium-experimental-subgroup-matrix"],
874
+ "subgroupMatrixConfigs": [{ "componentType": "f32", "resultComponentType": "f32", "M": 8, "N": 8, "K": 8 }]
875
+ },
876
+ "intermediates": [{ "id": "QRotary", "dtype": "float32", "shape": "[qRotaryElements]" }],
877
+ "passes": [
878
+ {
879
+ "id": "append",
880
+ "name": "SparseAttention.Append",
881
+ "shader": "sparse-kv-append.wgsl.jinja",
882
+ "bindings": "appendSeparateRotary",
883
+ "constants": { "kvSource": "\"new_key\"", "vSource": "\"new_value\"" },
884
+ "dispatch": {
885
+ "threads": "batchSize * kvNumHeads * seqLen * headSize",
886
+ "workgroupSize": "constants.appendWorkgroupSize"
887
+ }
888
+ },
889
+ {
890
+ "id": "qrotary",
891
+ "name": "SparseAttention.QueryRotary",
892
+ "shader": "sparse-q-rotary.wgsl.jinja",
893
+ "bindings": "qRotary",
894
+ "dispatch": {
895
+ "threads": "batchSize * numHeads * seqLen * headSize",
896
+ "workgroupSize": "constants.appendWorkgroupSize"
897
+ }
898
+ },
899
+ {
900
+ "id": "attention",
901
+ "name": "SparseAttention.AttentionSgmat",
902
+ "shader": "sparse-attention-sgmat.wgsl.jinja",
903
+ "bindings": "attentionSgmatRotary",
904
+ "dispatch": { "x": "sgmatQueryTiles", "y": "batchSize * numHeads" }
905
+ }
906
  ]
907
  },
908
  {
 
929
  "dispatch": { "x": "sparseQueryTiles", "y": "batchSize * numHeads" },
930
  "constants": { "qTile": "sparseQueryTile" }
931
  }
932
+ ],
933
+ "constants": { "vStageWorthIt": "sparseVStageWorthIt" }
934
+ },
935
+ {
936
+ "id": "packed_sgmat",
937
+ "priority": 20,
938
+ "when": ["packedContract", "geometryOk", "sparseSgmatOk"],
939
+ "requires": {
940
+ "features": ["subgroups", "chromium-experimental-subgroup-matrix"],
941
+ "subgroupMatrixConfigs": [{ "componentType": "f32", "resultComponentType": "f32", "M": 8, "N": 8, "K": 8 }]
942
+ },
943
+ "passes": [
944
+ {
945
+ "id": "append",
946
+ "name": "SparseAttention.Append",
947
+ "shader": "sparse-kv-append.wgsl.jinja",
948
+ "bindings": "appendPacked",
949
+ "constants": { "kvSource": "\"packed_qkv\"", "vSource": "\"packed_qkv\"" },
950
+ "dispatch": {
951
+ "threads": "batchSize * kvNumHeads * seqLen * headSize",
952
+ "workgroupSize": "constants.appendWorkgroupSize"
953
+ }
954
+ },
955
+ {
956
+ "id": "attention",
957
+ "name": "SparseAttention.AttentionSgmat",
958
+ "shader": "sparse-attention-sgmat.wgsl.jinja",
959
+ "bindings": "attentionSgmat",
960
+ "dispatch": { "x": "sgmatQueryTiles", "y": "batchSize * numHeads" }
961
+ }
962
  ]
963
  },
964
  {
 
996
  "dispatch": { "x": "sparseQueryTiles", "y": "batchSize * numHeads" },
997
  "constants": { "qTile": "sparseQueryTile" }
998
  }
999
+ ],
1000
+ "constants": { "vStageWorthIt": "sparseVStageWorthIt" }
1001
+ },
1002
+ {
1003
+ "id": "packed_rotary_sgmat",
1004
+ "priority": 30,
1005
+ "when": ["packedRotaryContract", "geometryOk", "sparseSgmatOk"],
1006
+ "requires": {
1007
+ "features": ["subgroups", "chromium-experimental-subgroup-matrix"],
1008
+ "subgroupMatrixConfigs": [{ "componentType": "f32", "resultComponentType": "f32", "M": 8, "N": 8, "K": 8 }]
1009
+ },
1010
+ "intermediates": [{ "id": "QRotary", "dtype": "float32", "shape": "[qRotaryElements]" }],
1011
+ "passes": [
1012
+ {
1013
+ "id": "append",
1014
+ "name": "SparseAttention.Append",
1015
+ "shader": "sparse-kv-append.wgsl.jinja",
1016
+ "bindings": "appendPackedRotary",
1017
+ "constants": { "kvSource": "\"packed_qkv\"", "vSource": "\"packed_qkv\"" },
1018
+ "dispatch": {
1019
+ "threads": "batchSize * kvNumHeads * seqLen * headSize",
1020
+ "workgroupSize": "constants.appendWorkgroupSize"
1021
+ }
1022
+ },
1023
+ {
1024
+ "id": "qrotary",
1025
+ "name": "SparseAttention.QueryRotary",
1026
+ "shader": "sparse-q-rotary.wgsl.jinja",
1027
+ "bindings": "qRotary",
1028
+ "dispatch": {
1029
+ "threads": "batchSize * numHeads * seqLen * headSize",
1030
+ "workgroupSize": "constants.appendWorkgroupSize"
1031
+ }
1032
+ },
1033
+ {
1034
+ "id": "attention",
1035
+ "name": "SparseAttention.AttentionSgmat",
1036
+ "shader": "sparse-attention-sgmat.wgsl.jinja",
1037
+ "bindings": "attentionSgmatRotary",
1038
+ "dispatch": { "x": "sgmatQueryTiles", "y": "batchSize * numHeads" }
1039
+ }
1040
  ]
1041
  }
1042
  ]
build/webgpu/metadata.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "com.microsoft.SparseAttention",
3
- "id": "_com_microsoft_sparseattention_webgpu_07e93c9",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
@@ -8,13 +8,14 @@
8
  "algorithm": "sha256",
9
  "files": {
10
  "bench.json": "E0HHOiKzV0TA0/6aDZXLjmxd2z/Q5iYFBFGNYeQoQM0=",
11
- "manifest.json": "L9ljQObhgAxTXE7AuOhOeG1E7zQwFIDFC+FnIgrU864=",
12
- "sparse-attention.wgsl.jinja": "zYSxDV01i/wcjq9YJjdWDzRHkAbZA6Ctx7Fi/3xdrF4=",
 
13
  "sparse-kv-append.wgsl.jinja": "RavNXJuGESPqe/Jzt5Ek6+844s5q2w/ocyUgGP3cWec=",
14
  "sparse-q-rotary.wgsl.jinja": "hI7d9cx6RqRO079h4xPBMTMj+qKqIUOKW3pF3vdeWdg=",
15
- "test.json": "aOQD21sQAgUaJwht0fYeGHtHCmUc7VPdhXjgKLH9YEU="
16
  }
17
  },
18
- "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
19
  "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/com.microsoft.SparseAttention" }
20
  }
 
1
  {
2
  "name": "com.microsoft.SparseAttention",
3
+ "id": "_com_microsoft_sparseattention_webgpu_4a03a84",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
 
8
  "algorithm": "sha256",
9
  "files": {
10
  "bench.json": "E0HHOiKzV0TA0/6aDZXLjmxd2z/Q5iYFBFGNYeQoQM0=",
11
+ "manifest.json": "gKMlEB+YkQi6M1AeKLoodBrq6h8O9hZGRuRd1MoYsak=",
12
+ "sparse-attention-sgmat.wgsl.jinja": "OKpr/4DCSDNAm1vzXOoPqlMGauOYb3Lw3cymXu+dXWI=",
13
+ "sparse-attention.wgsl.jinja": "Piza+iXd87OglOJjKORQmvbF1St7TzSgN37dCuv36K4=",
14
  "sparse-kv-append.wgsl.jinja": "RavNXJuGESPqe/Jzt5Ek6+844s5q2w/ocyUgGP3cWec=",
15
  "sparse-q-rotary.wgsl.jinja": "hI7d9cx6RqRO079h4xPBMTMj+qKqIUOKW3pF3vdeWdg=",
16
+ "test.json": "BTrN5cYcQUbwYPpPG9o1rqyvAiCPN2ePxNF+VuaQUmk="
17
  }
18
  },
19
+ "provenance": { "kernel": { "sha": "c928d21e6cc1310861cba3bafb75f5f679ecf5f3", "dirty": false } },
20
  "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/com.microsoft.SparseAttention" }
21
  }
build/webgpu/sparse-attention-sgmat.wgsl.jinja ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% macro sparse_schedule() %}
2
+ // How much history precedes this call's tokens. Prompt mode is determined from
3
+ // scalar total_sequence_length rather than per-batch values and forces the past
4
+ // length to zero; a padded prompt row must not be read as if it had history.
5
+ fn past_sequence_length(batch: u32) -> u32 {
6
+ if (u32(total_sequence_length[0]) == params.seqLen) {
7
+ return 0u;
8
+ }
9
+ let total = u32(key_total_sequence_lengths[batch]);
10
+ return select(0u, total - params.seqLen, total >= params.seqLen);
11
+ }
12
+ {%- endmacro %}
13
+
14
+ enable subgroups;
15
+ {% if pinSubgroupSize32 %}
16
+ enable subgroup_size_control;
17
+ {% endif %}
18
+ enable chromium_experimental_subgroup_matrix;
19
+ diagnostic(off, chromium.subgroup_matrix_uniformity);
20
+
21
+ {{ env.wgsl.resourceDeclarations }}
22
+
23
+ // com.microsoft.SparseAttention, subgroup-matrix attention pass (prefill band).
24
+ // One workgroup per (batch, 64-query tile, query head). The block-sparse layout
25
+ // already lists dense SPARSE_BLOCK-wide key tiles, so each selected block is
26
+ // walked as 64x64 score tiles on 8x8 subgroup matrices with no gather step: the
27
+ // K and V rows of a block are contiguous rows of the head-major cache and load
28
+ // directly as matrix fragments. Only Q goes through shared memory, for the
29
+ // query-tail zero padding.
30
+ //
31
+ // The pass runs the selected blocks twice. Sweep one folds each score tile
32
+ // into running per-row (max, denominator) softmax statistics; sweep two
33
+ // recomputes the same tiles (bit-identical: same operands, same order),
34
+ // exponentiates against the now-final row max, and accumulates P.V into
35
+ // subgroup-matrix result fragments. Recomputing QK costs half the ideal FLOPs
36
+ // again, but it is what lets both GEMMs stay on the matrix unit: a running
37
+ // online rescale of the output fragments would need an elementwise multiply on
38
+ // opaque fragments, which only exists as a shared-memory round trip per key
39
+ // tile, and the output accumulator alone (64 x head_size f32) is the whole
40
+ // workgroup storage budget.
41
+ //
42
+ // Causality, CSR duplicate suppression, the dense-row shortcut, and the
43
+ // all-masked-row uniform average reproduce the online kernel's semantics; a
44
+ // key past a query's bound contributes probability zero and multiplies away
45
+ // against whatever the cache holds there, exactly as the online kernel's
46
+ // value walk does.
47
+ const Q_HEADS: u32 = {{ numHeads }}u;
48
+ const KV_HEADS: u32 = {{ kvNumHeads }}u;
49
+ const HEAD_DIM: u32 = {{ headSize }}u;
50
+ const MAX_CACHE_SEQ: u32 = {{ maxCacheSeq }}u;
51
+ const SPARSE_BLOCK: u32 = {{ sparseBlockSize }}u;
52
+ const NUM_LAYOUT: u32 = {{ numLayout }}u;
53
+ const ROW_STRIDE: u32 = {{ maxBlocks + 1 }}u;
54
+ const COL_STRIDE: u32 = {{ maxNnz }}u;
55
+ const Q_HIDDEN: u32 = {{ numHeads * headSize }}u;
56
+ {% if not usesRotary %}
57
+ const Q_STRIDE: u32 = {{ packedStride if packedQkv else numHeads * headSize }}u;
58
+ {% endif %}
59
+ // 64 query rows x 64 key columns per score tile, staged over the head
60
+ // dimension in 32-wide chunks; 8 subgroups of 32 lanes as a 4x2 grid, each
61
+ // owning a 16x32 score sub-tile. sparse_block_size here is a multiple of 64
62
+ // and head_size a multiple of 32, so every key tile is a whole aligned run of
63
+ // cache rows and the head loop has no tail.
64
+ const TILE_M: u32 = 64u;
65
+ const TILE_N: u32 = 64u;
66
+ const TILE_K: u32 = 32u;
67
+ const SUB_TILES: u32 = {{ (sparseBlockSize / 64) | int }}u;
68
+ {% set pvColBlocks = (headSize / 16) | int %}
69
+ // Each of the two subgroup column groups owns half the output head dimension.
70
+ const PV_COL_SPAN: u32 = {{ (headSize / 2) | int }}u;
71
+
72
+ // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
73
+ // `m - m` finite so an empty lane / all--inf row contributes the exact
74
+ // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
75
+ // a zero final denominator according to their public semantics. Using -inf
76
+ // here changes +inf-row behavior.
77
+ const FLT_MAX: f32 = 3.4028234663852886e38;
78
+
79
+ fn is_finite_f32(value: f32) -> bool {
80
+ return select(false, value <= FLT_MAX, value >= -FLT_MAX);
81
+ }
82
+
83
+ // x - m that is exactly 0 when x equals a finite m, so exp(shifted) == 1
84
+ // exactly at the row max. `x - x` on an infinite max is a legal fast-math
85
+ // fold to 0, which would silently turn +inf rows finite — the explicit
86
+ // equality test keeps the NaN propagation of the serial kernels.
87
+ fn shifted_value(value: f32, maxValue: f32) -> f32 {
88
+ let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
89
+ return select(value - maxValue, 0.0, equalFiniteMax);
90
+ }
91
+ fn exp_shift(value: f32, maxValue: f32) -> f32 {
92
+ return exp(shifted_value(value, maxValue));
93
+ }
94
+
95
+ // Q staging for the score GEMM; the score epilogues alias it as the
96
+ // fragment-store scratch (8 subgroups x 4 banks x 64 elements).
97
+ var<workgroup> tile_q: array<f32, 2048>;
98
+ // Tile probabilities for the P.V GEMM; the output epilogue aliases it as the
99
+ // result-fragment scratch once the last key tile's readers are done.
100
+ var<workgroup> prob_tile: array<f32, 4096>;
101
+ var<workgroup> row_m: array<f32, 64>;
102
+ var<workgroup> row_d: array<f32, 64>;
103
+ // Per-key-tile row partials, one slot per (row, subgroup column group).
104
+ var<workgroup> part_m: array<f32, 128>;
105
+ var<workgroup> part_d: array<f32, 128>;
106
+
107
+ {% if ATTN_SCALE_DIM is not defined %}{% set ATTN_SCALE_DIM = "HEAD_DIM" %}{% endif %}
108
+ fn scale_value() -> f32 {
109
+ if (params.scale != 0.0) { return params.scale; }
110
+ return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
111
+ }
112
+
113
+
114
+ {{ sparse_schedule() }}
115
+
116
+ {% macro score_tile() %}
117
+ // S = Q.K^T for this key tile, accumulated over the head dimension in
118
+ // TILE_K chunks. Q is staged with query tails zero-padded; K loads
119
+ // straight from the cache as column-major right fragments.
120
+ for (var k_base = 0u; k_base < HEAD_DIM; k_base += TILE_K) {
121
+ {
122
+ let a_row = li / 4u;
123
+ let a_col = (li % 4u) * 8u;
124
+ for (var i = 0u; i < 8u; i++) {
125
+ let k = k_base + a_col + i;
126
+ {% if usesRotary %}
127
+ let q_index = ((batch * Q_HEADS + head) * params.seqLen
128
+ + min(tile0 + a_row, params.seqLen - 1u)) * HEAD_DIM + k;
129
+ tile_q[a_row * TILE_K + a_col + i] =
130
+ select(0.0, q_rotary[q_index], a_row < rows_live);
131
+ {% else %}
132
+ let q_index = (batch * params.seqLen + min(tile0 + a_row, params.seqLen - 1u))
133
+ * Q_STRIDE + head * HEAD_DIM + k;
134
+ tile_q[a_row * TILE_K + a_col + i] =
135
+ select(0.0, f32(query[q_index]), a_row < rows_live);
136
+ {% endif %}
137
+ }
138
+ }
139
+ workgroupBarrier();
140
+ for (var step = 0u; step < TILE_K; step += 8u) {
141
+ {% for rb in range(2) %}
142
+ let mat_a{{ rb }} = subgroupMatrixLoad<subgroup_matrix_left<f32, 8, 8>, row_major>(
143
+ &tile_q, (base_a + {{ rb * 8 }}u) * TILE_K + step, TILE_K
144
+ );
145
+ {% endfor %}
146
+ {% for cb in range(4) %}
147
+ let mat_b{{ cb }} = subgroupMatrixLoad<subgroup_matrix_right<f32, 8, 8>, col_major>(
148
+ &present_key, key_row_elem + (base_b + {{ cb * 8 }}u) * HEAD_DIM + k_base + step, HEAD_DIM
149
+ );
150
+ {% endfor %}
151
+ {% for rb in range(2) %}
152
+ {% for cb in range(4) %}
153
+ mat_s{{ rb }}{{ cb }} = subgroupMatrixMultiplyAccumulate(
154
+ mat_a{{ rb }}, mat_b{{ cb }}, mat_s{{ rb }}{{ cb }}
155
+ );
156
+ {% endfor %}
157
+ {% endfor %}
158
+ }
159
+ workgroupBarrier();
160
+ }
161
+ {% endmacro %}
162
+
163
+ {% macro sweep(phase) %}
164
+ // Consecutive queries span at most two mask rows, and every query of a row
165
+ // selects the same blocks, so one sweep per row covers the tile; a query
166
+ // contributes only to its own row's tiles.
167
+ for (var mask_row = row_first; mask_row <= row_last; mask_row++) {
168
+ let start = u32(block_row_indices[row_base + mask_row]);
169
+ let end = u32(block_row_indices[row_base + mask_row + 1u]);
170
+ // A row listing every causal block is also dense, so its column values are
171
+ // ignored, matching the online kernel and the pinned dense-layout fixture.
172
+ let dense_row = layout_is_dense || (end - start == mask_row + 1u);
173
+ // Furthest causal bound (exclusive) of the tile's queries on this row.
174
+ let bound_max = min(q_abs0 + rows_live, (mask_row + 1u) * SPARSE_BLOCK);
175
+ let tile_count = select(
176
+ (end - start) * SUB_TILES, (bound_max + TILE_N - 1u) / TILE_N, dense_row
177
+ );
178
+ for (var t = 0u; t < tile_count; t++) {
179
+ var key_base = t * TILE_N;
180
+ var tile_ok = true;
181
+ if (!dense_row) {
182
+ let entry = t / SUB_TILES;
183
+ let block = u32(block_col_indices[layout_id * COL_STRIDE + start + entry]);
184
+ // CSR is a boolean mask: a repeated column must not repeat its
185
+ // probability mass, so a duplicate entry's tiles are skipped whole.
186
+ var duplicate = false;
187
+ for (var previous = 0u; previous < entry; previous++) {
188
+ duplicate = duplicate ||
189
+ u32(block_col_indices[layout_id * COL_STRIDE + start + previous]) == block;
190
+ }
191
+ key_base = block * SPARSE_BLOCK + (t % SUB_TILES) * TILE_N;
192
+ tile_ok = !duplicate;
193
+ }
194
+ // A tile at or past every member query's bound is fully masked; skipping
195
+ // it is exact. All of this tile's control state is derived from
196
+ // workgroup-uniform reads, so the barriers below stay in uniform flow.
197
+ if (tile_ok && key_base < bound_max) {
198
+ let key_row_elem = ((batch * KV_HEADS + kv_head) * MAX_CACHE_SEQ + key_base) * HEAD_DIM;
199
+ {% for rb in range(2) %}
200
+ {% for cb in range(4) %}
201
+ var mat_s{{ rb }}{{ cb }}: subgroup_matrix_result<f32, 8, 8>;
202
+ {% endfor %}
203
+ {% endfor %}
204
+ {{ score_tile() }}
205
+ {% for rb in range(2) %}
206
+ {% if rb > 0 %}
207
+ // The banks alias the Q staging tile; the previous row block's readers
208
+ // must finish before this one overwrites them.
209
+ workgroupBarrier();
210
+ {% endif %}
211
+ {% if phase == "stats" %}
212
+ // All four lanes of a quad carry the same score row (row_in_block is
213
+ // lane / 4), so the accumulator below is a partial over one row and
214
+ // the butterfly merging it is quad-uniform.
215
+ var tile_stat_m{{ rb }} = -FLT_MAX;
216
+ var tile_stat_d{{ rb }} = 0.0;
217
+ {% endif %}
218
+ {% for cb in range(4) %}
219
+ subgroupMatrixStore<row_major>(
220
+ &tile_q, (subgroup * 4u + {{ cb }}u) * 64u, mat_s{{ rb }}{{ cb }}, 8u
221
+ );
222
+ {% endfor %}
223
+ workgroupBarrier();
224
+ {% for cb in range(4) %}
225
+ for (var pair = 0u; pair < 2u; pair++) {
226
+ let r = base_a + {{ rb * 8 }}u + row_in_block;
227
+ let key = key_base + base_b + {{ cb * 8 }}u + col_in_block + pair;
228
+ let q_abs = q_abs0 + r;
229
+ let allowed = r < rows_live && q_abs / SPARSE_BLOCK == mask_row && key <= q_abs;
230
+ {% if phase == "stats" %}
231
+ if (allowed) {
232
+ let scored = tile_q[
233
+ (subgroup * 4u + {{ cb }}u) * 64u + row_in_block * 8u + col_in_block + pair
234
+ ] * scale;
235
+ let prev_m = tile_stat_m{{ rb }};
236
+ tile_stat_m{{ rb }} = max(tile_stat_m{{ rb }}, scored);
237
+ tile_stat_d{{ rb }} = tile_stat_d{{ rb }} * exp_shift(prev_m, tile_stat_m{{ rb }})
238
+ + exp_shift(scored, tile_stat_m{{ rb }});
239
+ }
240
+ {% else %}
241
+ var prob = 0.0;
242
+ if (allowed) {
243
+ prob = exp_shift(tile_q[
244
+ (subgroup * 4u + {{ cb }}u) * 64u + row_in_block * 8u + col_in_block + pair
245
+ ] * scale, row_m[r]);
246
+ }
247
+ prob_tile[r * TILE_N + base_b + {{ cb * 8 }}u + col_in_block + pair] = prob;
248
+ {% endif %}
249
+ }
250
+ {% endfor %}
251
+ {% if phase == "stats" %}
252
+ // Butterfly the quad unconditionally: a lane whose row ran past the
253
+ // query tail carries the exact identity (-FLT_MAX, 0), which merges to
254
+ // a no-op, and a subgroup shuffle under a partial guard would not be
255
+ // uniform.
256
+ {% for xor in [1, 2] %}
257
+ {
258
+ let other_m = subgroupShuffleXor(tile_stat_m{{ rb }}, {{ xor }}u);
259
+ let other_d = subgroupShuffleXor(tile_stat_d{{ rb }}, {{ xor }}u);
260
+ let merged_m = max(tile_stat_m{{ rb }}, other_m);
261
+ tile_stat_d{{ rb }} = tile_stat_d{{ rb }} * exp_shift(tile_stat_m{{ rb }}, merged_m)
262
+ + other_d * exp_shift(other_m, merged_m);
263
+ tile_stat_m{{ rb }} = merged_m;
264
+ }
265
+ {% endfor %}
266
+ if (lane % 4u == 0u) {
267
+ let stat_row = base_a + {{ rb * 8 }}u + row_in_block;
268
+ part_m[stat_row * 2u + subtile_idx] = tile_stat_m{{ rb }};
269
+ part_d[stat_row * 2u + subtile_idx] = tile_stat_d{{ rb }};
270
+ }
271
+ {% endif %}
272
+ {% endfor %}
273
+ {% if phase == "stats" %}
274
+ workgroupBarrier();
275
+ // Fold both column groups' partials into the running row statistics,
276
+ // in the same (max, rescale, add) form as the per-lane walk: an empty
277
+ // partial (-FLT_MAX, 0) is the identity, a NaN partial poisons the
278
+ // denominator the way a NaN score does.
279
+ if (li < TILE_M) {
280
+ var merged_m = row_m[li];
281
+ var merged_d = row_d[li];
282
+ for (var g = 0u; g < 2u; g++) {
283
+ let m2 = part_m[li * 2u + g];
284
+ let d2 = part_d[li * 2u + g];
285
+ let new_m = max(merged_m, m2);
286
+ merged_d = merged_d * exp_shift(merged_m, new_m) + d2 * exp_shift(m2, new_m);
287
+ merged_m = new_m;
288
+ }
289
+ row_m[li] = merged_m;
290
+ row_d[li] = merged_d;
291
+ }
292
+ workgroupBarrier();
293
+ {% else %}
294
+ workgroupBarrier();
295
+ // O += P.V. P streams from shared memory; V rows are contiguous cache
296
+ // rows, loaded directly as right-hand fragments. A masked or padded
297
+ // slot carries probability zero and multiplies away.
298
+ for (var kk = 0u; kk < TILE_N; kk += 8u) {
299
+ {% for rb in range(2) %}
300
+ let mat_p{{ rb }} = subgroupMatrixLoad<subgroup_matrix_left<f32, 8, 8>, row_major>(
301
+ &prob_tile, (base_a + {{ rb * 8 }}u) * TILE_N + kk, TILE_N
302
+ );
303
+ {% endfor %}
304
+ {% for cb in range(pvColBlocks) %}
305
+ let mat_v{{ cb }} = subgroupMatrixLoad<subgroup_matrix_right<f32, 8, 8>, row_major>(
306
+ &present_value,
307
+ key_row_elem + kk * HEAD_DIM + subtile_idx * PV_COL_SPAN + {{ cb * 8 }}u, HEAD_DIM
308
+ );
309
+ {% endfor %}
310
+ {% for rb in range(2) %}
311
+ {% for cb in range(pvColBlocks) %}
312
+ mat_o{{ rb }}{{ cb }} = subgroupMatrixMultiplyAccumulate(
313
+ mat_p{{ rb }}, mat_v{{ cb }}, mat_o{{ rb }}{{ cb }}
314
+ );
315
+ {% endfor %}
316
+ {% endfor %}
317
+ }
318
+ // Orders this tile's prob_tile reads before the next tile rewrites it.
319
+ workgroupBarrier();
320
+ {% endif %}
321
+ }
322
+ }
323
+ }
324
+ {% endmacro %}
325
+
326
+ @compute @workgroup_size(256, 1, 1){{ " @subgroup_size(32)" if pinSubgroupSize32 else "" }}
327
+ fn main(
328
+ @builtin(workgroup_id) wg: vec3<u32>,
329
+ @builtin(local_invocation_index) li: u32,
330
+ @builtin(subgroup_invocation_id) lane: u32
331
+ ) {
332
+ let tile0 = wg.x * TILE_M;
333
+ let head = wg.y % Q_HEADS;
334
+ let batch = wg.y / Q_HEADS;
335
+
336
+ let past = past_sequence_length(batch);
337
+ let kv_head = head / (Q_HEADS / KV_HEADS);
338
+ let scale = scale_value();
339
+
340
+ // Layouts cycle over the heads: four layouts and eight heads give S0 S1 S2 S3 S0 ...
341
+ let layout_id = head % NUM_LAYOUT;
342
+ let row_base = layout_id * ROW_STRIDE;
343
+ // A layout at or above the triangular dense-block count is treated as dense.
344
+ let dense_nnz = ROW_STRIDE * (ROW_STRIDE - 1u) / 2u;
345
+ let layout_is_dense = u32(block_row_indices[row_base + ROW_STRIDE - 1u]) >= dense_nnz;
346
+
347
+ let subgroup = li / 32u;
348
+ let subtile_idy = subgroup % 4u;
349
+ let subtile_idx = subgroup / 4u;
350
+ let base_a = subtile_idy * 16u;
351
+ let base_b = subtile_idx * 32u;
352
+ let row_in_block = lane / 4u;
353
+ let col_in_block = (lane % 4u) * 2u;
354
+
355
+ // The dispatch never launches a tile past the last token, so at least one
356
+ // row of every tile is live.
357
+ let rows_live = min(TILE_M, params.seqLen - tile0);
358
+ let q_abs0 = past + tile0;
359
+ let row_first = q_abs0 / SPARSE_BLOCK;
360
+ let row_last = (q_abs0 + rows_live - 1u) / SPARSE_BLOCK;
361
+
362
+ {% for rb in range(2) %}
363
+ {% for cb in range(pvColBlocks) %}
364
+ var mat_o{{ rb }}{{ cb }}: subgroup_matrix_result<f32, 8, 8>;
365
+ {% endfor %}
366
+ {% endfor %}
367
+
368
+ for (var r = li; r < TILE_M; r += 256u) {
369
+ row_m[r] = -FLT_MAX;
370
+ row_d[r] = 0.0;
371
+ }
372
+ workgroupBarrier();
373
+
374
+ {{ sweep("stats") }}
375
+ {{ sweep("apply") }}
376
+
377
+ // Normalize by the final denominators and store; the result fragments
378
+ // round-trip through prob_tile, which no reader needs any more.
379
+ {% for rb in range(2) %}
380
+ {% if rb > 0 %}
381
+ workgroupBarrier();
382
+ {% endif %}
383
+ {% for cb in range(pvColBlocks) %}
384
+ subgroupMatrixStore<row_major>(
385
+ &prob_tile, (subgroup * {{ pvColBlocks }}u + {{ cb }}u) * 64u, mat_o{{ rb }}{{ cb }}, 8u
386
+ );
387
+ {% endfor %}
388
+ workgroupBarrier();
389
+ {% for cb in range(pvColBlocks) %}
390
+ for (var pair = 0u; pair < 2u; pair++) {
391
+ let r = base_a + {{ rb * 8 }}u + row_in_block;
392
+ if (r < rows_live && row_d[r] > 0.0) {
393
+ let dcol = subtile_idx * PV_COL_SPAN + {{ cb * 8 }}u + col_in_block + pair;
394
+ let out_index = (batch * params.seqLen + tile0 + r) * Q_HIDDEN + head * HEAD_DIM + dcol;
395
+ output[out_index] = {{ scalar }}(prob_tile[
396
+ (subgroup * {{ pvColBlocks }}u + {{ cb }}u) * 64u + row_in_block * 8u + col_in_block + pair
397
+ ] / row_d[r]);
398
+ }
399
+ }
400
+ {% endfor %}
401
+ {% endfor %}
402
+
403
+ // An all-masked row becomes a uniform average over causal history rather
404
+ // than zero — the online kernel's public semantics, NaN denominators
405
+ // included.
406
+ for (var r = 0u; r < rows_live; r++) {
407
+ if (!(row_d[r] > 0.0)) {
408
+ let key_bound = q_abs0 + r + 1u;
409
+ let cache_base = (batch * KV_HEADS + kv_head) * MAX_CACHE_SEQ * HEAD_DIM;
410
+ for (var d = li; d < HEAD_DIM; d += 256u) {
411
+ var total = 0.0;
412
+ for (var key = 0u; key < key_bound; key++) {
413
+ total += f32(present_value[cache_base + key * HEAD_DIM + d]);
414
+ }
415
+ let out_index = (batch * params.seqLen + tile0 + r) * Q_HIDDEN + head * HEAD_DIM + d;
416
+ output[out_index] = {{ scalar }}(total / f32(key_bound));
417
+ }
418
+ }
419
+ }
420
+ }
build/webgpu/sparse-attention.wgsl.jinja CHANGED
@@ -79,6 +79,16 @@ fn exp_shift(value: f32, maxValue: f32) -> f32 {
79
  var<workgroup> q_shared: array<vec4<f32>, Q_TILE * HEAD_VEC>;
80
  var<workgroup> running_out: array<vec4<f32>, Q_TILE * HEAD_VEC>;
81
  var<workgroup> probs: array<f32, Q_TILE * WG>;
 
 
 
 
 
 
 
 
 
 
82
  // One resolved cache row base per key of the current tile, so the value accumulation
83
  // re-reads a base instead of re-walking the column list per head dimension.
84
  var<workgroup> key_rows: array<u32, WG>;
@@ -374,6 +384,38 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid:
374
  // so this rescale-and-accumulate needs no further synchronization. One value vector
375
  // serves every query, which is the other half of the tile's reuse; a key outside a
376
  // query's causal bound carries prob 0 and is multiplied away.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  let tileCount = min(WG, slot_count - tileBase);
378
  for (var dv: u32 = tid; dv < HEAD_VEC; dv = dv + WG) {
379
  {% for j in range(qTile) %}
@@ -391,6 +433,7 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid:
391
  }
392
  // Orders this tile's probs and key_rows reads before the next tile writes them.
393
  workgroupBarrier();
 
394
 
395
  tileBase = tileBase + WG;
396
  }
 
79
  var<workgroup> q_shared: array<vec4<f32>, Q_TILE * HEAD_VEC>;
80
  var<workgroup> running_out: array<vec4<f32>, Q_TILE * HEAD_VEC>;
81
  var<workgroup> probs: array<f32, Q_TILE * WG>;
82
+ {% if vStageWorthIt %}
83
+ // Staged value rows for the running-output walk (small-dispatch shapes only):
84
+ // the walk itself occupies just HEAD_VEC lanes, so the whole workgroup
85
+ // prefetches each chunk of value rows at full width and the walkers read
86
+ // shared memory instead of chaining dependent global loads. Large dispatches
87
+ // already hide that latency across resident workgroups, and the chunk
88
+ // barriers only cost them, so they keep the direct walk.
89
+ const V_STAGE_KEYS: u32 = 16u;
90
+ var<workgroup> v_stage: array<vec4<f32>, V_STAGE_KEYS * HEAD_VEC>;
91
+ {% endif %}
92
  // One resolved cache row base per key of the current tile, so the value accumulation
93
  // re-reads a base instead of re-walking the column list per head dimension.
94
  var<workgroup> key_rows: array<u32, WG>;
 
384
  // so this rescale-and-accumulate needs no further synchronization. One value vector
385
  // serves every query, which is the other half of the tile's reuse; a key outside a
386
  // query's causal bound carries prob 0 and is multiplied away.
387
+ {% if vStageWorthIt %}
388
+ let tileCount = min(WG, slot_count - tileBase);
389
+ {% for j in range(qTile) %}
390
+ var vSum_{{ j }} = vec4<f32>(0.0);
391
+ {% endfor %}
392
+ for (var chunkBase: u32 = 0u; chunkBase < tileCount; chunkBase = chunkBase + V_STAGE_KEYS) {
393
+ let chunkCount = min(V_STAGE_KEYS, tileCount - chunkBase);
394
+ for (var slot = tid; slot < chunkCount * HEAD_VEC; slot = slot + WG) {
395
+ let ki = slot / HEAD_VEC;
396
+ let dvs = slot % HEAD_VEC;
397
+ v_stage[slot] = vec4<f32>(present_value[key_rows[chunkBase + ki] / 4u + dvs]);
398
+ }
399
+ workgroupBarrier();
400
+ for (var dv: u32 = tid; dv < HEAD_VEC; dv = dv + WG) {
401
+ for (var i: u32 = 0u; i < chunkCount; i = i + 1u) {
402
+ let vv = v_stage[i * HEAD_VEC + dv];
403
+ {% for j in range(qTile) %}
404
+ vSum_{{ j }} = vSum_{{ j }} + probs[{{ j }}u * WG + chunkBase + i] * vv;
405
+ {% endfor %}
406
+ }
407
+ }
408
+ // Orders this chunk's v_stage reads before the next chunk overwrites it.
409
+ workgroupBarrier();
410
+ }
411
+ for (var dv: u32 = tid; dv < HEAD_VEC; dv = dv + WG) {
412
+ {% for j in range(qTile) %}
413
+ running_out[{{ j }}u * HEAD_VEC + dv] = running_out[{{ j }}u * HEAD_VEC + dv] * correction_{{ j }} + vSum_{{ j }};
414
+ {% endfor %}
415
+ }
416
+ // Orders this tile's probs and key_rows reads before the next tile writes them.
417
+ workgroupBarrier();
418
+ {% else %}
419
  let tileCount = min(WG, slot_count - tileBase);
420
  for (var dv: u32 = tid; dv < HEAD_VEC; dv = dv + WG) {
421
  {% for j in range(qTile) %}
 
433
  }
434
  // Orders this tile's probs and key_rows reads before the next tile writes them.
435
  workgroupBarrier();
436
+ {% endif %}
437
 
438
  tileBase = tileBase + WG;
439
  }
build/webgpu/test.json CHANGED
@@ -740,6 +740,322 @@
740
  "pastKeyT": { "dtype": "float32", "shape": [1, 1, 16, 16], "tolerance": 0.000001, "relTolerance": 0.000001 },
741
  "pastValueT": { "dtype": "float32", "shape": [1, 1, 16, 16], "tolerance": 0.000001, "relTolerance": 0.000001 }
742
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
743
  }
744
  ]
745
  }
 
740
  "pastKeyT": { "dtype": "float32", "shape": [1, 1, 16, 16], "tolerance": 0.000001, "relTolerance": 0.000001 },
741
  "pastValueT": { "dtype": "float32", "shape": [1, 1, 16, 16], "tolerance": 0.000001, "relTolerance": 0.000001 }
742
  }
743
+ },
744
+ {
745
+ "name": "sgmat_prompt_separate_blk64_duplicate_csr",
746
+ "provenance": {
747
+ "notes": "Prefill shape on the subgroup-matrix route: 64-token blocks, a duplicate CSR column whose repeated block must not repeat its mass, a dense-by-count row, and an off-diagonal-only row."
748
+ },
749
+ "attrs": { "num_heads": 4, "kv_num_heads": 2, "sparse_block_size": 64 },
750
+ "inputs": {
751
+ "queryT": {
752
+ "dtype": "float32",
753
+ "shape": [1, 192, 256],
754
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
755
+ },
756
+ "keyT": {
757
+ "dtype": "float32",
758
+ "shape": [1, 192, 128],
759
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.5 }
760
+ },
761
+ "valueT": {
762
+ "dtype": "float32",
763
+ "shape": [1, 192, 128],
764
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.23, "scale": 0.5 }
765
+ },
766
+ "pastKeyT": { "dtype": "float32", "shape": [1, 2, 192, 64], "data": { "kind": "constant", "value": 0.0 } },
767
+ "pastValueT": { "dtype": "float32", "shape": [1, 2, 192, 64], "data": { "kind": "constant", "value": 0.0 } },
768
+ "blockRowIndicesT": {
769
+ "dtype": "int32",
770
+ "shape": [2, 4],
771
+ "data": { "kind": "values", "values": [0, 1, 3, 5, 0, 1, 2, 3] }
772
+ },
773
+ "blockColIndicesT": {
774
+ "dtype": "int32",
775
+ "shape": [2, 5],
776
+ "data": { "kind": "values", "values": [0, 0, 1, 1, 1, 0, 1, 2, 0, 0] }
777
+ },
778
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [192] } },
779
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [192] } }
780
+ },
781
+ "outputs": {
782
+ "outputT": { "dtype": "float32", "shape": [1, 192, 256], "tolerance": 0.0001, "relTolerance": 0.0005 },
783
+ "pastKeyT": { "dtype": "float32", "shape": [1, 2, 192, 64], "tolerance": 0.00002, "relTolerance": 0.0002 },
784
+ "pastValueT": { "dtype": "float32", "shape": [1, 2, 192, 64], "tolerance": 0.00002, "relTolerance": 0.0002 }
785
+ }
786
+ },
787
+ {
788
+ "name": "sgmat_prompt_packed_blk64",
789
+ "provenance": {
790
+ "notes": "Packed [Q|K|V] prefill on the subgroup-matrix route; layout 0 crosses the triangular dense-count threshold so its column values are ignored, layout 1 keeps a gapped sparse row."
791
+ },
792
+ "attrs": { "num_heads": 4, "kv_num_heads": 2, "sparse_block_size": 64 },
793
+ "inputs": {
794
+ "queryT": {
795
+ "dtype": "float32",
796
+ "shape": [1, 128, 512],
797
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
798
+ },
799
+ "pastKeyT": { "dtype": "float32", "shape": [1, 2, 128, 64], "data": { "kind": "constant", "value": 0.0 } },
800
+ "pastValueT": { "dtype": "float32", "shape": [1, 2, 128, 64], "data": { "kind": "constant", "value": 0.0 } },
801
+ "blockRowIndicesT": {
802
+ "dtype": "int32",
803
+ "shape": [2, 3],
804
+ "data": { "kind": "values", "values": [0, 1, 3, 0, 1, 2] }
805
+ },
806
+ "blockColIndicesT": {
807
+ "dtype": "int32",
808
+ "shape": [2, 3],
809
+ "data": { "kind": "values", "values": [0, 0, 1, 0, 1, 0] }
810
+ },
811
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [128] } },
812
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [128] } }
813
+ },
814
+ "outputs": {
815
+ "outputT": { "dtype": "float32", "shape": [1, 128, 256], "tolerance": 0.0001, "relTolerance": 0.0005 },
816
+ "pastKeyT": { "dtype": "float32", "shape": [1, 2, 128, 64], "tolerance": 0.00002, "relTolerance": 0.0002 },
817
+ "pastValueT": { "dtype": "float32", "shape": [1, 2, 128, 64], "tolerance": 0.00002, "relTolerance": 0.0002 }
818
+ }
819
+ },
820
+ {
821
+ "name": "sgmat_prompt_separate_rotary_neox_blk64",
822
+ "provenance": {
823
+ "notes": "Grouped-query prefill with partial NeoX rotary on the subgroup-matrix route; the attention pass reads the rotated-Q intermediate."
824
+ },
825
+ "attrs": { "num_heads": 2, "kv_num_heads": 1, "sparse_block_size": 64, "do_rotary": 1, "rotary_interleaved": 0 },
826
+ "inputs": {
827
+ "queryT": {
828
+ "dtype": "float32",
829
+ "shape": [1, 128, 128],
830
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
831
+ },
832
+ "keyT": {
833
+ "dtype": "float32",
834
+ "shape": [1, 128, 64],
835
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.5 }
836
+ },
837
+ "valueT": {
838
+ "dtype": "float32",
839
+ "shape": [1, 128, 64],
840
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.23, "scale": 0.5 }
841
+ },
842
+ "pastKeyT": { "dtype": "float32", "shape": [1, 1, 128, 64], "data": { "kind": "constant", "value": 0.0 } },
843
+ "pastValueT": { "dtype": "float32", "shape": [1, 1, 128, 64], "data": { "kind": "constant", "value": 0.0 } },
844
+ "blockRowIndicesT": {
845
+ "dtype": "int32",
846
+ "shape": [2, 3],
847
+ "data": { "kind": "values", "values": [0, 1, 3, 0, 1, 2] }
848
+ },
849
+ "blockColIndicesT": {
850
+ "dtype": "int32",
851
+ "shape": [2, 3],
852
+ "data": { "kind": "values", "values": [0, 0, 1, 0, 1, 0] }
853
+ },
854
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [128] } },
855
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [128] } },
856
+ "cosCacheT": { "dtype": "float32", "shape": [128, 16], "data": { "kind": "constant", "value": 0.8 } },
857
+ "sinCacheT": { "dtype": "float32", "shape": [128, 16], "data": { "kind": "constant", "value": 0.6 } }
858
+ },
859
+ "outputs": {
860
+ "outputT": { "dtype": "float32", "shape": [1, 128, 128], "tolerance": 0.0001, "relTolerance": 0.0005 },
861
+ "pastKeyT": { "dtype": "float32", "shape": [1, 1, 128, 64], "tolerance": 0.00002, "relTolerance": 0.0002 },
862
+ "pastValueT": { "dtype": "float32", "shape": [1, 1, 128, 64], "tolerance": 0.00002, "relTolerance": 0.0002 }
863
+ }
864
+ },
865
+ {
866
+ "name": "sgmat_prompt_packed_rotary_interleaved_blk64",
867
+ "provenance": {
868
+ "notes": "Packed [Q|K|V] prefill with full interleaved rotary at head_size 32, the narrowest head the subgroup-matrix route admits (a single reduction chunk)."
869
+ },
870
+ "attrs": { "num_heads": 2, "kv_num_heads": 2, "sparse_block_size": 64, "do_rotary": 1, "rotary_interleaved": 1 },
871
+ "inputs": {
872
+ "queryT": {
873
+ "dtype": "float32",
874
+ "shape": [1, 128, 192],
875
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
876
+ },
877
+ "pastKeyT": { "dtype": "float32", "shape": [1, 2, 128, 32], "data": { "kind": "constant", "value": 0.0 } },
878
+ "pastValueT": { "dtype": "float32", "shape": [1, 2, 128, 32], "data": { "kind": "constant", "value": 0.0 } },
879
+ "blockRowIndicesT": {
880
+ "dtype": "int32",
881
+ "shape": [2, 3],
882
+ "data": { "kind": "values", "values": [0, 1, 3, 0, 1, 2] }
883
+ },
884
+ "blockColIndicesT": {
885
+ "dtype": "int32",
886
+ "shape": [2, 3],
887
+ "data": { "kind": "values", "values": [0, 0, 1, 0, 1, 0] }
888
+ },
889
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [128] } },
890
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [128] } },
891
+ "cosCacheT": { "dtype": "float32", "shape": [128, 16], "data": { "kind": "constant", "value": 0.8 } },
892
+ "sinCacheT": { "dtype": "float32", "shape": [128, 16], "data": { "kind": "constant", "value": 0.6 } }
893
+ },
894
+ "outputs": {
895
+ "outputT": { "dtype": "float32", "shape": [1, 128, 64], "tolerance": 0.0001, "relTolerance": 0.0005 },
896
+ "pastKeyT": { "dtype": "float32", "shape": [1, 2, 128, 32], "tolerance": 0.00002, "relTolerance": 0.0002 },
897
+ "pastValueT": { "dtype": "float32", "shape": [1, 2, 128, 32], "tolerance": 0.00002, "relTolerance": 0.0002 }
898
+ }
899
+ },
900
+ {
901
+ "name": "sgmat_prompt_tail_s100_explicit_scale",
902
+ "provenance": {
903
+ "notes": "A 100-token prefill leaves the second query tile 36 rows live, exercising the staged-Q zero padding and guarded stores; the explicit scale attribute overrides the head-size default."
904
+ },
905
+ "attrs": { "num_heads": 2, "kv_num_heads": 1, "sparse_block_size": 64, "scale": 0.25 },
906
+ "inputs": {
907
+ "queryT": {
908
+ "dtype": "float32",
909
+ "shape": [1, 100, 64],
910
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
911
+ },
912
+ "keyT": {
913
+ "dtype": "float32",
914
+ "shape": [1, 100, 32],
915
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.5 }
916
+ },
917
+ "valueT": {
918
+ "dtype": "float32",
919
+ "shape": [1, 100, 32],
920
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.23, "scale": 0.5 }
921
+ },
922
+ "pastKeyT": { "dtype": "float32", "shape": [1, 1, 128, 32], "data": { "kind": "constant", "value": 0.0 } },
923
+ "pastValueT": { "dtype": "float32", "shape": [1, 1, 128, 32], "data": { "kind": "constant", "value": 0.0 } },
924
+ "blockRowIndicesT": { "dtype": "int32", "shape": [1, 3], "data": { "kind": "values", "values": [0, 1, 3] } },
925
+ "blockColIndicesT": { "dtype": "int32", "shape": [1, 3], "data": { "kind": "values", "values": [0, 0, 1] } },
926
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [100] } },
927
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [100] } }
928
+ },
929
+ "outputs": {
930
+ "outputT": { "dtype": "float32", "shape": [1, 100, 64], "tolerance": 0.0001, "relTolerance": 0.0005 },
931
+ "pastKeyT": { "dtype": "float32", "shape": [1, 1, 128, 32], "tolerance": 0.00002, "relTolerance": 0.0002 },
932
+ "pastValueT": { "dtype": "float32", "shape": [1, 1, 128, 32], "tolerance": 0.00002, "relTolerance": 0.0002 }
933
+ }
934
+ },
935
+ {
936
+ "name": "sgmat_prompt_blk128_subtiles",
937
+ "provenance": {
938
+ "notes": "128-token sparse blocks decompose into two 64-key tiles per block at head_size 128, the flagship geometry; the second mask row skips block 0 so its diagonal block carries the ragged causal edge."
939
+ },
940
+ "attrs": { "num_heads": 2, "kv_num_heads": 1, "sparse_block_size": 128 },
941
+ "inputs": {
942
+ "queryT": {
943
+ "dtype": "float32",
944
+ "shape": [1, 256, 256],
945
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
946
+ },
947
+ "keyT": {
948
+ "dtype": "float32",
949
+ "shape": [1, 256, 128],
950
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.5 }
951
+ },
952
+ "valueT": {
953
+ "dtype": "float32",
954
+ "shape": [1, 256, 128],
955
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.23, "scale": 0.5 }
956
+ },
957
+ "pastKeyT": { "dtype": "float32", "shape": [1, 1, 256, 128], "data": { "kind": "constant", "value": 0.0 } },
958
+ "pastValueT": { "dtype": "float32", "shape": [1, 1, 256, 128], "data": { "kind": "constant", "value": 0.0 } },
959
+ "blockRowIndicesT": { "dtype": "int32", "shape": [1, 3], "data": { "kind": "values", "values": [0, 1, 2] } },
960
+ "blockColIndicesT": { "dtype": "int32", "shape": [1, 2], "data": { "kind": "values", "values": [0, 1] } },
961
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [256] } },
962
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [256] } }
963
+ },
964
+ "outputs": {
965
+ "outputT": { "dtype": "float32", "shape": [1, 256, 256], "tolerance": 0.0001, "relTolerance": 0.0005 },
966
+ "pastKeyT": { "dtype": "float32", "shape": [1, 1, 256, 128], "tolerance": 0.00002, "relTolerance": 0.0002 },
967
+ "pastValueT": { "dtype": "float32", "shape": [1, 1, 256, 128], "tolerance": 0.00002, "relTolerance": 0.0002 }
968
+ }
969
+ },
970
+ {
971
+ "name": "sgmat_chunked_two_mask_rows",
972
+ "provenance": {
973
+ "notes": "Chunked prefill with per-batch histories of 100 and 68 tokens: each 64-query tile straddles two mask rows at different split points, the misaligned path a runtime past length forces."
974
+ },
975
+ "attrs": { "num_heads": 2, "kv_num_heads": 2, "sparse_block_size": 64 },
976
+ "inputs": {
977
+ "queryT": {
978
+ "dtype": "float32",
979
+ "shape": [2, 64, 128],
980
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
981
+ },
982
+ "keyT": {
983
+ "dtype": "float32",
984
+ "shape": [2, 64, 128],
985
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.5 }
986
+ },
987
+ "valueT": {
988
+ "dtype": "float32",
989
+ "shape": [2, 64, 128],
990
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.23, "scale": 0.5 }
991
+ },
992
+ "pastKeyT": {
993
+ "dtype": "float32",
994
+ "shape": [2, 2, 256, 64],
995
+ "data": { "kind": "fillFloat32", "sinStep": 0.011, "cosStep": 0.017, "scale": 0.25 }
996
+ },
997
+ "pastValueT": {
998
+ "dtype": "float32",
999
+ "shape": [2, 2, 256, 64],
1000
+ "data": { "kind": "fillFloat32", "sinStep": 0.019, "cosStep": 0.013, "scale": 0.25 }
1001
+ },
1002
+ "blockRowIndicesT": {
1003
+ "dtype": "int32",
1004
+ "shape": [2, 5],
1005
+ "data": { "kind": "values", "values": [0, 1, 3, 5, 7, 0, 1, 2, 4, 5] }
1006
+ },
1007
+ "blockColIndicesT": {
1008
+ "dtype": "int32",
1009
+ "shape": [2, 7],
1010
+ "data": { "kind": "values", "values": [0, 0, 1, 0, 2, 1, 3, 0, 1, 0, 2, 3, 0, 0] }
1011
+ },
1012
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [256] } },
1013
+ "keyTotalSequenceLengthsT": {
1014
+ "dtype": "int32",
1015
+ "shape": [2],
1016
+ "data": { "kind": "values", "values": [164, 132] }
1017
+ }
1018
+ },
1019
+ "outputs": {
1020
+ "outputT": { "dtype": "float32", "shape": [2, 64, 128], "tolerance": 0.0001, "relTolerance": 0.0005 },
1021
+ "pastKeyT": { "dtype": "float32", "shape": [2, 2, 256, 64], "tolerance": 0.00002, "relTolerance": 0.0002 },
1022
+ "pastValueT": { "dtype": "float32", "shape": [2, 2, 256, 64], "tolerance": 0.00002, "relTolerance": 0.0002 }
1023
+ }
1024
+ },
1025
+ {
1026
+ "name": "sgmat_empty_row_uniform_fallback",
1027
+ "provenance": {
1028
+ "notes": "The second mask row selects no blocks, so all 64 of its queries carry zero probability mass and take the uniform average over causal history on the subgroup-matrix route."
1029
+ },
1030
+ "attrs": { "num_heads": 1, "kv_num_heads": 1, "sparse_block_size": 64 },
1031
+ "inputs": {
1032
+ "queryT": {
1033
+ "dtype": "float32",
1034
+ "shape": [1, 128, 64],
1035
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
1036
+ },
1037
+ "keyT": {
1038
+ "dtype": "float32",
1039
+ "shape": [1, 128, 64],
1040
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.5 }
1041
+ },
1042
+ "valueT": {
1043
+ "dtype": "float32",
1044
+ "shape": [1, 128, 64],
1045
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.23, "scale": 0.5 }
1046
+ },
1047
+ "pastKeyT": { "dtype": "float32", "shape": [1, 1, 128, 64], "data": { "kind": "constant", "value": 0.0 } },
1048
+ "pastValueT": { "dtype": "float32", "shape": [1, 1, 128, 64], "data": { "kind": "constant", "value": 0.0 } },
1049
+ "blockRowIndicesT": { "dtype": "int32", "shape": [1, 3], "data": { "kind": "values", "values": [0, 1, 1] } },
1050
+ "blockColIndicesT": { "dtype": "int32", "shape": [1, 1], "data": { "kind": "values", "values": [0] } },
1051
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [128] } },
1052
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [128] } }
1053
+ },
1054
+ "outputs": {
1055
+ "outputT": { "dtype": "float32", "shape": [1, 128, 64], "tolerance": 0.0001, "relTolerance": 0.0005 },
1056
+ "pastKeyT": { "dtype": "float32", "shape": [1, 1, 128, 64], "tolerance": 0.00002, "relTolerance": 0.0002 },
1057
+ "pastValueT": { "dtype": "float32", "shape": [1, 1, 128, 64], "tolerance": 0.00002, "relTolerance": 0.0002 }
1058
+ }
1059
  }
1060
  ]
1061
  }