File size: 6,857 Bytes
94aaaa3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
{% if foldKind == "subgroup_full" or foldKind == "subgroup_butterfly" %}
enable subgroups;
{% endif %}
{{ env.wgsl.resourceDeclarations }}

// The Engram gate, fused per (batch, sequence, hc_mult) row:
//   dot  = sum(RMSNorm(key) * RMSNorm(query)) / sqrt(HIDDEN)
//   out  = sigmoid(sign(dot) * sqrt(max(abs(dot), 1e-6))) * value
// Both inverse-RMS factors are row scalars, so they come out of the sum and the
// two sums of squares and the scaled cross term ride ONE pass as a single vec3
// accumulator. Normalizing per element first would need two passes and a
// different rounding.
//
// Each row owns a fixed LANE GROUP rather than a whole workgroup. LANES_PER_ROW
// is the smallest power of two that covers the row, capped by the workgroup
// (and, on the subgroup routes, by the subgroup) so several rows are resident at
// once, and widened toward the workgroup when there are too few rows to fill the
// dispatch. The value words stay in registers across the fold.
//
// Every accumulation is float32 whatever the tensor type; only the store narrows.
const ROW_WORDS: u32 = {{ rowWords }}u;
const HIDDEN: f32 = {{ hidden }}.0;
const HC_MULT: u32 = {{ hcMult }}u;
const EPS: f32 = {{ epsilon }};
const GATE_FLOOR: f32 = 0.000001;
const WG: u32 = {{ workgroupSize }}u;
const LANES_PER_ROW: u32 = {{ lanesPerRow }}u;
const ROWS_PER_GROUP: u32 = {{ rowsPerGroup }}u;
{% if foldKind == "workgroup_tree" %}

// One contiguous LANES_PER_ROW slice per row, carrying all three accumulators as
// one vector. The fold never crosses a slice boundary, so the rows sharing this
// array do not interfere.
var<workgroup> partials: array<vec3<f32>, WG>;
{% endif %}

fn sigmoid_safe(x: f32) -> f32 {
  if (x >= 0.0) {
    let z = exp(-x);
    return 1.0 / (1.0 + z);
  }
  let z = exp(x);
  return z / (1.0 + z);
}

// sign(0.0) is 0.0 in WGSL, so a zero dot product gives an argument of exactly
// zero and therefore a gate of exactly 0.5. A copysign spelling would map it to
// +sqrt(GATE_FLOOR) instead, which is the divergence upstream calls out.
fn engram_gate(dotValue: f32) -> f32 {
  return sigmoid_safe(sign(dotValue) * sqrt(max(abs(dotValue), GATE_FLOOR)));
}

@compute @workgroup_size(WG)
fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
  let lane = lid.x % LANES_PER_ROW;
  let localRow = lid.x / LANES_PER_ROW;
  // 2D-folded row index: wg.y carries the high bits past the dispatch's per-axis
  // workgroup fold width.
  let row = (wg.x + wg.y * {{ DISPATCH_FOLD_WIDTH }}u) * ROWS_PER_GROUP + localRow;
  let rowValid = row < params.rows;
  // A lane group past the last row folds an empty row and stores nothing, so the
  // collectives and barriers below stay in uniform control flow.
  let limit = select(0u, ROW_WORDS, rowValid);
  let base = row * ROW_WORDS;
  // The hyper-connection stream selects the scale rows; the token selects the one
  // value row that every stream of that token shares.
  let scaleBase = (row % HC_MULT) * ROW_WORDS;
  let valueBase = (row / HC_MULT) * ROW_WORDS;

  // x: sum(key^2), y: sum(query^2), z: sum(key * key_scale * query * query_scale).
  var acc = vec3<f32>(0.0);
{% if stageRow %}
{% for i in range(wordsPerLane) %}
{% if useVec4 %}
  var vv{{ i }} = vec4<f32>(0.0);
{% else %}
  var vv{{ i }} = 0.0;
{% endif %}
  let c{{ i }} = lane + {{ i }}u * LANES_PER_ROW;
  if (c{{ i }} < limit) {
{% if useVec4 %}
    let kv{{ i }} = vec4<f32>(key[base + c{{ i }}]);
    let qv{{ i }} = vec4<f32>(query[base + c{{ i }}]);
    let ks{{ i }} = vec4<f32>(kscale[scaleBase + c{{ i }}]);
    let qs{{ i }} = vec4<f32>(qscale[scaleBase + c{{ i }}]);
    vv{{ i }} = vec4<f32>(value[valueBase + c{{ i }}]);
    acc = acc + vec3<f32>(dot(kv{{ i }}, kv{{ i }}), dot(qv{{ i }}, qv{{ i }}), dot(kv{{ i }} * ks{{ i }}, qv{{ i }} * qs{{ i }}));
{% else %}
    let kv{{ i }} = f32(key[base + c{{ i }}]);
    let qv{{ i }} = f32(query[base + c{{ i }}]);
    let ks{{ i }} = f32(kscale[scaleBase + c{{ i }}]);
    let qs{{ i }} = f32(qscale[scaleBase + c{{ i }}]);
    vv{{ i }} = f32(value[valueBase + c{{ i }}]);
    acc = acc + vec3<f32>(kv{{ i }} * kv{{ i }}, qv{{ i }} * qv{{ i }}, (kv{{ i }} * ks{{ i }}) * (qv{{ i }} * qs{{ i }}));
{% endif %}
  }
{% endfor %}
{% else %}
  // The row's value words do not fit in registers at this width, so the statistic
  // pass and the store pass each walk it.
  for (var c = lane; c < limit; c = c + LANES_PER_ROW) {
{% if useVec4 %}
    let kv = vec4<f32>(key[base + c]);
    let qv = vec4<f32>(query[base + c]);
    let ks = vec4<f32>(kscale[scaleBase + c]);
    let qs = vec4<f32>(qscale[scaleBase + c]);
    acc = acc + vec3<f32>(dot(kv, kv), dot(qv, qv), dot(kv * ks, qv * qs));
{% else %}
    let kv = f32(key[base + c]);
    let qv = f32(query[base + c]);
    let ks = f32(kscale[scaleBase + c]);
    let qs = f32(qscale[scaleBase + c]);
    acc = acc + vec3<f32>(kv * kv, qv * qv, (kv * ks) * (qv * qs));
{% endif %}
  }
{% endif %}
{% if foldKind == "subgroup_full" %}
  let sums = subgroupAdd(acc);
{% elif foldKind == "subgroup_butterfly" %}
  // Segmented butterfly: only the low bits of the lane index are exchanged, so each
  // aligned group of LANES_PER_ROW lanes folds its own row and one subgroup carries
  // several rows without a barrier or any workgroup memory. All three accumulators
  // ride one shuffle.
  var folded = acc;
{% for step in range(foldSteps) %}
  folded = folded + subgroupShuffleXor(folded, 1u << {{ step }}u);
{% endfor %}
  let sums = folded;
{% elif foldKind == "workgroup_tree" %}
  partials[lid.x] = acc;
  workgroupBarrier();
  // LANES_PER_ROW is a power of two, so the halving stride is uniform across the
  // whole workgroup and every lane reaches every barrier.
  var stride = LANES_PER_ROW / 2u;
  loop {
    if (stride == 0u) {
      break;
    }
    if (lane < stride) {
      partials[lid.x] = partials[lid.x] + partials[lid.x + stride];
    }
    workgroupBarrier();
    stride = stride / 2u;
  }
  let sums = partials[localRow * LANES_PER_ROW];
{% else %}
  // One lane owns the whole row, so the accumulator already holds the row sums.
  let sums = acc;
{% endif %}
  let keyInv = inverseSqrt(sums.x / HIDDEN + EPS);
  let queryInv = inverseSqrt(sums.y / HIDDEN + EPS);
  let gate = engram_gate(sums.z * keyInv * queryInv / sqrt(HIDDEN));
{% if stageRow %}
{% for i in range(wordsPerLane) %}
  if (c{{ i }} < limit) {
{% if useVec4 %}
    out[base + c{{ i }}] = {{ vectorScalar }}(vv{{ i }} * gate);
{% else %}
    out[base + c{{ i }}] = {{ scalar }}(vv{{ i }} * gate);
{% endif %}
  }
{% endfor %}
{% else %}
  for (var c = lane; c < limit; c = c + LANES_PER_ROW) {
{% if useVec4 %}
    out[base + c] = {{ vectorScalar }}(vec4<f32>(value[valueBase + c]) * gate);
{% else %}
    out[base + c] = {{ scalar }}(f32(value[valueBase + c]) * gate);
{% endif %}
  }
{% endif %}
}