Add architecture-variant waves 1+2: train Mistral/Qwen/Gemma/OLMo-class models by config
Wave 1 (module-level): - rope refactor: kernels read a host-precomputed inv-freq table; unlocks HF-"llama3" rope scaling (rope_scale_*), per-layer theta, and NoPE layers (nope_every, SmolLM3) - attention_bias (Qwen2.5 QKV bias), head_dim decoupled from d_model/n_heads (Qwen3), relu2 activation (nanoGPT-speedrun lineage), norm_placement pre|post|sandwich (OLMo2/Gemma) Wave 2 (attention kernels): - sliding_window + sliding_global_every (Mistral / Gemma3 local:global patterns) in the CPU reference, the unfused Metal kernels, and the fused scalar flash kernels — out-of-window KV blocks are skipped, so cost scales with the window; window > 0 auto-routes off the MMA kernel - attn_softcap (Gemma2): cap*tanh on scores pre-softmax, unfused path, exact tanh' chain in all backwards - rope_theta_global for dual-theta local/global layers (Gemma3) Parity suites cover every knob (fused + unfused paths); gradcheck and overfit stay green. New demo configs: gpt-50m-mistral, gpt-50m-gemma; ARCHITECTURES.md documents the per-family config matrix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
17 changed files +572 −139
added
configs/gpt-50m-gemma.json
+44 −0
@@ -0,0 +1,44 @@ | |||||
| 1 | +{ | ||||
| 2 | + "_comment": "Gemma-3-style variant of gpt-50m: 5 local (window 256, theta 10k) : 1 global (theta 1M) attention pattern, QK-norm, sandwich norm, embeddings scaled by sqrt(d_model), GELU. Same data/steps as gpt-50m.", | ||||
| 3 | + "model": { | ||||
| 4 | + "name": "gpt-50m-gemma", | ||||
| 5 | + "n_layers": 12, | ||||
| 6 | + "d_model": 576, | ||||
| 7 | + "n_heads": 9, | ||||
| 8 | + "n_kv_heads": 3, | ||||
| 9 | + "d_ff": 1536, | ||||
| 10 | + "vocab_size": 4096, | ||||
| 11 | + "context_length": 1024, | ||||
| 12 | + "tied_embeddings": true, | ||||
| 13 | + "use_rope": true, | ||||
| 14 | + "rope_theta": 10000.0, | ||||
| 15 | + "rope_theta_global": 1000000.0, | ||||
| 16 | + "norm": "rmsnorm", | ||||
| 17 | + "norm_eps": 1e-06, | ||||
| 18 | + "activation": "gelu", | ||||
| 19 | + "dropout": 0.0, | ||||
| 20 | + "qk_norm": true, | ||||
| 21 | + "norm_placement": "sandwich", | ||||
| 22 | + "scale_embeddings": true, | ||||
| 23 | + "sliding_window": 256, | ||||
| 24 | + "sliding_global_every": 6 | ||||
| 25 | + }, | ||||
| 26 | + "train": { | ||||
| 27 | + "lr": 0.0005, | ||||
| 28 | + "min_lr_ratio": 0.1, | ||||
| 29 | + "warmup_steps": 117, | ||||
| 30 | + "max_steps": 1170, | ||||
| 31 | + "beta1": 0.9, | ||||
| 32 | + "beta2": 0.95, | ||||
| 33 | + "eps": 1e-08, | ||||
| 34 | + "weight_decay": 0.1, | ||||
| 35 | + "grad_clip": 1.0, | ||||
| 36 | + "batch_size": 8, | ||||
| 37 | + "grad_accum_steps": 8, | ||||
| 38 | + "precision": "f32", | ||||
| 39 | + "checkpoint_every": 200, | ||||
| 40 | + "eval_every": 100, | ||||
| 41 | + "eval_batches": 20, | ||||
| 42 | + "seed": 1337 | ||||
| 43 | + } | ||||
| 44 | +} | ||||
added
configs/gpt-50m-mistral.json
+39 −0
@@ -0,0 +1,39 @@ | |||||
| 1 | +{ | ||||
| 2 | + "_comment": "Mistral-style variant of gpt-50m: sliding-window attention (256 tokens, every layer), GQA 2:1, QKV bias off. Same data/steps as gpt-50m so the only variable is the attention pattern. The fused scalar flash kernel skips out-of-window KV blocks, so attention cost scales with the window, not the context.", | ||||
| 3 | + "model": { | ||||
| 4 | + "name": "gpt-50m-mistral", | ||||
| 5 | + "n_layers": 10, | ||||
| 6 | + "d_model": 640, | ||||
| 7 | + "n_heads": 10, | ||||
| 8 | + "n_kv_heads": 5, | ||||
| 9 | + "d_ff": 1728, | ||||
| 10 | + "vocab_size": 4096, | ||||
| 11 | + "context_length": 1024, | ||||
| 12 | + "tied_embeddings": true, | ||||
| 13 | + "use_rope": true, | ||||
| 14 | + "rope_theta": 10000.0, | ||||
| 15 | + "norm": "rmsnorm", | ||||
| 16 | + "norm_eps": 1e-06, | ||||
| 17 | + "activation": "swiglu", | ||||
| 18 | + "dropout": 0.0, | ||||
| 19 | + "sliding_window": 256 | ||||
| 20 | + }, | ||||
| 21 | + "train": { | ||||
| 22 | + "lr": 0.0005, | ||||
| 23 | + "min_lr_ratio": 0.1, | ||||
| 24 | + "warmup_steps": 117, | ||||
| 25 | + "max_steps": 1170, | ||||
| 26 | + "beta1": 0.9, | ||||
| 27 | + "beta2": 0.95, | ||||
| 28 | + "eps": 1e-08, | ||||
| 29 | + "weight_decay": 0.1, | ||||
| 30 | + "grad_clip": 1.0, | ||||
| 31 | + "batch_size": 8, | ||||
| 32 | + "grad_accum_steps": 8, | ||||
| 33 | + "precision": "f32", | ||||
| 34 | + "checkpoint_every": 200, | ||||
| 35 | + "eval_every": 100, | ||||
| 36 | + "eval_batches": 20, | ||||
| 37 | + "seed": 1337 | ||||
| 38 | + } | ||||
| 39 | +} | ||||
modified
src/kernels/attention.metal
+25 −6
@@ -18,6 +18,8 @@ struct AttnParams { | |||||
| 18 | uint B, T, H, HKV, HD; | 18 | uint B, T, H, HKV, HD; | ||
| 19 | float scale; | 19 | float scale; | ||
| 20 | uint causal; | 20 | uint causal; | ||
| 21 | + uint window; // sliding window (keys kept, self incl.); 0 = full | ||||
| 22 | + float softcap; // cap*tanh(s/cap) pre-softmax; 0 = off | ||||
| 21 | }; | 23 | }; | ||
| 22 | 24 | ||||
| 23 | kernel void attention_fwd_f32(device const float* Q [[buffer(0)]], | 25 | kernel void attention_fwd_f32(device const float* Q [[buffer(0)]], | ||
@@ -40,29 +42,32 @@ kernel void attention_fwd_f32(device const float* Q [[buffer(0)]], | |||||
| 40 | device const float* qi = Q + (ulong(b) * p.T + i) * Cq + h * p.HD; | 42 | device const float* qi = Q + (ulong(b) * p.T + i) * Cq + h * p.HD; | ||
| 41 | device float* prow_base = P + ((ulong(b) * p.H + h) * p.T + i) * p.T; | 43 | device float* prow_base = P + ((ulong(b) * p.H + h) * p.T + i) * p.T; | ||
| 42 | const uint jmax = p.causal ? i : p.T - 1; | 44 | const uint jmax = p.causal ? i : p.T - 1; | ||
| 45 | + const uint jmin = (p.window > 0 && i + 1 > p.window) ? i + 1 - p.window : 0; | ||||
| 43 | 46 | ||||
| 44 | float m = -FLT_MAX; | 47 | float m = -FLT_MAX; | ||
| 45 | − for (uint j = 0; j <= jmax; ++j) { | 48 | + for (uint j = jmin; j <= jmax; ++j) { | ||
| 46 | device const float* kj = K + (ulong(b) * p.T + j) * Ckv + hkv * p.HD; | 49 | device const float* kj = K + (ulong(b) * p.T + j) * Ckv + hkv * p.HD; | ||
| 47 | float s = 0.0f; | 50 | float s = 0.0f; | ||
| 48 | for (uint d = 0; d < p.HD; ++d) s = fma(qi[d], kj[d], s); | 51 | for (uint d = 0; d < p.HD; ++d) s = fma(qi[d], kj[d], s); | ||
| 49 | s *= p.scale; | 52 | s *= p.scale; | ||
| 53 | + if (p.softcap > 0.0f) s = p.softcap * precise::tanh(s / p.softcap); | ||||
| 50 | prow_base[j] = s; | 54 | prow_base[j] = s; | ||
| 51 | m = max(m, s); | 55 | m = max(m, s); | ||
| 52 | } | 56 | } | ||
| 53 | float sum = 0.0f; | 57 | float sum = 0.0f; | ||
| 54 | − for (uint j = 0; j <= jmax; ++j) { | 58 | + for (uint j = jmin; j <= jmax; ++j) { | ||
| 55 | const float e = exp(prow_base[j] - m); | 59 | const float e = exp(prow_base[j] - m); | ||
| 56 | prow_base[j] = e; | 60 | prow_base[j] = e; | ||
| 57 | sum += e; | 61 | sum += e; | ||
| 58 | } | 62 | } | ||
| 59 | const float inv = 1.0f / sum; | 63 | const float inv = 1.0f / sum; | ||
| 60 | − for (uint j = 0; j <= jmax; ++j) prow_base[j] *= inv; | 64 | + for (uint j = 0; j < jmin; ++j) prow_base[j] = 0.0f; | ||
| 65 | + for (uint j = jmin; j <= jmax; ++j) prow_base[j] *= inv; | ||||
| 61 | for (uint j = jmax + 1; j < p.T; ++j) prow_base[j] = 0.0f; | 66 | for (uint j = jmax + 1; j < p.T; ++j) prow_base[j] = 0.0f; | ||
| 62 | 67 | ||||
| 63 | device float* oi = O + (ulong(b) * p.T + i) * Cq + h * p.HD; | 68 | device float* oi = O + (ulong(b) * p.T + i) * Cq + h * p.HD; | ||
| 64 | for (uint d = 0; d < p.HD; ++d) oi[d] = 0.0f; | 69 | for (uint d = 0; d < p.HD; ++d) oi[d] = 0.0f; | ||
| 65 | − for (uint j = 0; j <= jmax; ++j) { | 70 | + for (uint j = jmin; j <= jmax; ++j) { | ||
| 66 | const float prob = prow_base[j]; | 71 | const float prob = prow_base[j]; | ||
| 67 | if (prob == 0.0f) continue; | 72 | if (prob == 0.0f) continue; | ||
| 68 | device const float* vj = V + (ulong(b) * p.T + j) * Ckv + hkv * p.HD; | 73 | device const float* vj = V + (ulong(b) * p.T + j) * Ckv + hkv * p.HD; | ||
@@ -126,7 +131,14 @@ kernel void attention_bwd_dq_f32(device const float* Q [[buffer(0)]], | |||||
| 126 | device const float* kj = K + (ulong(b) * p.T + j) * Ckv + hkv * p.HD; | 131 | device const float* kj = K + (ulong(b) * p.T + j) * Ckv + hkv * p.HD; | ||
| 127 | float dp = 0.0f; | 132 | float dp = 0.0f; | ||
| 128 | for (uint d = 0; d < p.HD; ++d) dp = fma(doi[d], vj[d], dp); | 133 | for (uint d = 0; d < p.HD; ++d) dp = fma(doi[d], vj[d], dp); | ||
| 129 | − const float ds = prob * (dp - row_dot) * p.scale; | 134 | + float ds = prob * (dp - row_dot) * p.scale; | ||
| 135 | + if (p.softcap > 0.0f) { | ||||
| 136 | + device const float* qq = Q + (ulong(b) * p.T + i) * Cq + h * p.HD; | ||||
| 137 | + float s = 0.0f; | ||||
| 138 | + for (uint d = 0; d < p.HD; ++d) s = fma(qq[d], kj[d], s); | ||||
| 139 | + const float t = precise::tanh(s * p.scale / p.softcap); | ||||
| 140 | + ds *= 1.0f - t * t; | ||||
| 141 | + } | ||||
| 130 | for (uint d = 0; d < p.HD; ++d) dqi[d] = fma(ds, kj[d], dqi[d]); | 142 | for (uint d = 0; d < p.HD; ++d) dqi[d] = fma(ds, kj[d], dqi[d]); | ||
| 131 | } | 143 | } | ||
| 132 | } | 144 | } | ||
@@ -167,7 +179,14 @@ kernel void attention_bwd_dkv_f32(device const float* Q [[buffer(0)]], | |||||
| 167 | const float row_dot = D[(ulong(b) * p.H + h) * p.T + i]; | 179 | const float row_dot = D[(ulong(b) * p.H + h) * p.T + i]; | ||
| 168 | float dp = 0.0f; | 180 | float dp = 0.0f; | ||
| 169 | for (uint d = 0; d < p.HD; ++d) dp = fma(doi[d], vj[d], dp); | 181 | for (uint d = 0; d < p.HD; ++d) dp = fma(doi[d], vj[d], dp); | ||
| 170 | − const float ds = prob * (dp - row_dot) * p.scale; | 182 | + float ds = prob * (dp - row_dot) * p.scale; | ||
| 183 | + if (p.softcap > 0.0f) { | ||||
| 184 | + device const float* kk = K + (ulong(b) * p.T + j) * Ckv + hkv * p.HD; | ||||
| 185 | + float s = 0.0f; | ||||
| 186 | + for (uint d = 0; d < p.HD; ++d) s = fma(qi[d], kk[d], s); | ||||
| 187 | + const float t = precise::tanh(s * p.scale / p.softcap); | ||||
| 188 | + ds *= 1.0f - t * t; | ||||
| 189 | + } | ||||
| 171 | for (uint d = 0; d < p.HD; ++d) { | 190 | for (uint d = 0; d < p.HD; ++d) { | ||
| 172 | dvj[d] = fma(prob, doi[d], dvj[d]); | 191 | dvj[d] = fma(prob, doi[d], dvj[d]); | ||
| 173 | dkj[d] = fma(ds, qi[d], dkj[d]); | 192 | dkj[d] = fma(ds, qi[d], dkj[d]); | ||
modified
src/kernels/elementwise.metal
+22 −6
@@ -52,6 +52,21 @@ kernel void gelu_f32(device const float* x [[buffer(0)]], | |||||
| 52 | out[gid] = 0.5f * v * (1.0f + precise::tanh(k * (v + 0.044715f * v * v * v))); | 52 | out[gid] = 0.5f * v * (1.0f + precise::tanh(k * (v + 0.044715f * v * v * v))); | ||
| 53 | } | 53 | } | ||
| 54 | 54 | ||||
| 55 | +// ReLU² (nanoGPT-speedrun lineage): out = max(x, 0)^2. | ||||
| 56 | +kernel void relu2_f32(device const float* x [[buffer(0)]], | ||||
| 57 | + device float* out [[buffer(1)]], | ||||
| 58 | + uint gid [[thread_position_in_grid]]) { | ||||
| 59 | + const float v = max(x[gid], 0.0f); | ||||
| 60 | + out[gid] = v * v; | ||||
| 61 | +} | ||||
| 62 | + | ||||
| 63 | +kernel void relu2_bwd_f32(device const float* x [[buffer(0)]], | ||||
| 64 | + device const float* dout [[buffer(1)]], | ||||
| 65 | + device float* dx [[buffer(2)]], | ||||
| 66 | + uint gid [[thread_position_in_grid]]) { | ||||
| 67 | + dx[gid] = fma(dout[gid], 2.0f * max(x[gid], 0.0f), dx[gid]); | ||||
| 68 | +} | ||||
| 69 | + | ||||
| 55 | // Gemma-style logit softcap: out = cap * tanh(x / cap). | 70 | // Gemma-style logit softcap: out = cap * tanh(x / cap). | ||
| 56 | kernel void softcap_f32(device const float* x [[buffer(0)]], | 71 | kernel void softcap_f32(device const float* x [[buffer(0)]], | ||
| 57 | device float* out [[buffer(1)]], | 72 | device float* out [[buffer(1)]], | ||
@@ -127,13 +142,15 @@ constant bool ROPE_INV [[function_constant(2)]]; | |||||
| 127 | 142 | ||||
| 128 | struct RopeParams { | 143 | struct RopeParams { | ||
| 129 | uint T, H, HD; | 144 | uint T, H, HD; | ||
| 130 | − float theta; | ||||
| 131 | uint pos_offset; | 145 | uint pos_offset; | ||
| 132 | }; | 146 | }; | ||
| 133 | 147 | ||||
| 134 | −kernel void rope_f32(device const float* x [[buffer(0)]], | 148 | +// freqs[k] is the per-pair inverse frequency (theta^(-2k/HD), possibly | ||
| 135 | − device float* out [[buffer(1)]], | 149 | +// rescaled — llama3 rope_scaling, per-layer theta). Host-precomputed. | ||
| 136 | − constant RopeParams& p [[buffer(2)]], | 150 | +kernel void rope_f32(device const float* x [[buffer(0)]], | ||
| 151 | + device float* out [[buffer(1)]], | ||||
| 152 | + constant RopeParams& p [[buffer(2)]], | ||||
| 153 | + device const float* freqs [[buffer(3)]], | ||||
| 137 | uint gid [[thread_position_in_grid]]) { | 154 | uint gid [[thread_position_in_grid]]) { | ||
| 138 | // gid indexes (bt, h, k) pairs: one thread per rotated pair | 155 | // gid indexes (bt, h, k) pairs: one thread per rotated pair | ||
| 139 | const uint pairs_per_row = p.H * (p.HD / 2); | 156 | const uint pairs_per_row = p.H * (p.HD / 2); | ||
@@ -143,8 +160,7 @@ kernel void rope_f32(device const float* x [[buffer(0)]], | |||||
| 143 | const uint k = rem % (p.HD / 2); | 160 | const uint k = rem % (p.HD / 2); | ||
| 144 | 161 | ||||
| 145 | const float pos = float(bt % p.T + p.pos_offset); | 162 | const float pos = float(bt % p.T + p.pos_offset); | ||
| 146 | − const float freq = pow(p.theta, -2.0f * float(k) / float(p.HD)); | 163 | + const float angle = pos * freqs[k]; | ||
| 147 | − const float angle = pos * freq; | ||||
| 148 | const float c = cos(angle); | 164 | const float c = cos(angle); | ||
| 149 | const float s = ROPE_INV ? -sin(angle) : sin(angle); | 165 | const float s = ROPE_INV ? -sin(angle) : sin(angle); | ||
| 150 | 166 | ||||
modified
src/kernels/flash_attention.metal
+18 −5
@@ -32,6 +32,7 @@ struct FlashParams { | |||||
| 32 | uint B, T, H, HKV; | 32 | uint B, T, H, HKV; | ||
| 33 | float scale; | 33 | float scale; | ||
| 34 | uint causal; | 34 | uint causal; | ||
| 35 | + uint window; // sliding window (keys kept, self incl.); 0 = full attention | ||||
| 35 | }; | 36 | }; | ||
| 36 | 37 | ||||
| 37 | // ---------------------------------------------------------------- forward | 38 | // ---------------------------------------------------------------- forward | ||
@@ -90,8 +91,15 @@ kernel void flash_attn_fwd(device const float* Q [[buffer(0)]], | |||||
| 90 | const uint row_max = min(qb * TGQ + TGQ - 1, p.T - 1); | 91 | const uint row_max = min(qb * TGQ + TGQ - 1, p.T - 1); | ||
| 91 | const uint j_end = p.causal ? (row_max + 1) : p.T; | 92 | const uint j_end = p.causal ? (row_max + 1) : p.T; | ||
| 92 | const uint diag_start = p.causal ? (qb * TGQ) : p.T; // blocks below need no mask | 93 | const uint diag_start = p.causal ? (qb * TGQ) : p.T; // blocks below need no mask | ||
| 93 | − | 94 | + // Sliding window: this row's first visible key, and (uniform across the | ||
| 94 | − for (uint jb = 0; jb < j_end; jb += BKV) { | 95 | + // threadgroup) the first KV block any row here can see — earlier blocks | ||
| 96 | + // are skipped outright, so compute scales with the window, not with T. | ||||
| 97 | + const uint jmin = (p.window > 0 && i + 1 > p.window) ? i + 1 - p.window : 0; | ||||
| 98 | + const uint tg_jmin = | ||||
| 99 | + (p.window > 0 && qb * TGQ + 1 > p.window) ? qb * TGQ + 1 - p.window : 0; | ||||
| 100 | + const uint jb_begin = (tg_jmin / BKV) * BKV; | ||||
| 101 | + | ||||
| 102 | + for (uint jb = jb_begin; jb < j_end; jb += BKV) { | ||||
| 95 | const uint block_len = min(BKV, j_end - jb); | 103 | const uint block_len = min(BKV, j_end - jb); | ||
| 96 | 104 | ||||
| 97 | threadgroup_barrier(mem_flags::mem_threadgroup); | 105 | threadgroup_barrier(mem_flags::mem_threadgroup); | ||
@@ -109,6 +117,7 @@ kernel void flash_attn_fwd(device const float* Q [[buffer(0)]], | |||||
| 109 | for (uint jr = 0; jr < block_len; ++jr) { | 117 | for (uint jr = 0; jr < block_len; ++jr) { | ||
| 110 | const uint j = jb + jr; | 118 | const uint j = jb + jr; | ||
| 111 | if (needs_mask && j > i) break; // rest of the block is masked too | 119 | if (needs_mask && j > i) break; // rest of the block is masked too | ||
| 120 | + if (j < jmin) continue; // below this row's window | ||||
| 112 | 121 | ||||
| 113 | threadgroup const float* kj = Ks + jr * HD; | 122 | threadgroup const float* kj = Ks + jr * HD; | ||
| 114 | float s = 0.0f; | 123 | float s = 0.0f; | ||
@@ -174,8 +183,9 @@ kernel void flash_attn_bwd_dq(device const float* Q [[buffer(0)]], | |||||
| 174 | const float li = L[(ulong(b) * p.H + h) * p.T + i]; | 183 | const float li = L[(ulong(b) * p.H + h) * p.T + i]; | ||
| 175 | const float di = D[(ulong(b) * p.H + h) * p.T + i]; | 184 | const float di = D[(ulong(b) * p.H + h) * p.T + i]; | ||
| 176 | const uint jmax = p.causal ? i : (p.T - 1); | 185 | const uint jmax = p.causal ? i : (p.T - 1); | ||
| 186 | + const uint jmin = (p.window > 0 && i + 1 > p.window) ? i + 1 - p.window : 0; | ||||
| 177 | 187 | ||||
| 178 | − for (uint j = 0; j <= jmax; ++j) { | 188 | + for (uint j = jmin; j <= jmax; ++j) { | ||
| 179 | device const float* kj = K + (ulong(b) * p.T + j) * Ckv + hkv * HD; | 189 | device const float* kj = K + (ulong(b) * p.T + j) * Ckv + hkv * HD; | ||
| 180 | device const float* vj = V + (ulong(b) * p.T + j) * Ckv + hkv * HD; | 190 | device const float* vj = V + (ulong(b) * p.T + j) * Ckv + hkv * HD; | ||
| 181 | 191 | ||||
@@ -225,10 +235,12 @@ kernel void flash_attn_bwd_dv(device const float* Q [[buffer(0)]], | |||||
| 225 | for (uint d = 0; d < HD; ++d) dv[d] = 0.0f; | 235 | for (uint d = 0; d < HD; ++d) dv[d] = 0.0f; | ||
| 226 | 236 | ||||
| 227 | const uint imin = p.causal ? j : 0; | 237 | const uint imin = p.causal ? j : 0; | ||
| 238 | + // Sliding window: only queries within `window` of j ever saw it. | ||||
| 239 | + const uint imax = (p.window > 0) ? min(p.T, j + p.window) : p.T; | ||||
| 228 | for (uint r = 0; r < rep; ++r) { | 240 | for (uint r = 0; r < rep; ++r) { | ||
| 229 | const uint h = hkv * rep + r; | 241 | const uint h = hkv * rep + r; | ||
| 230 | device const float* Lh = L + (ulong(b) * p.H + h) * p.T; | 242 | device const float* Lh = L + (ulong(b) * p.H + h) * p.T; | ||
| 231 | − for (uint i = imin; i < p.T; ++i) { | 243 | + for (uint i = imin; i < imax; ++i) { | ||
| 232 | device const float* qi = Q + (ulong(b) * p.T + i) * Cq + h * HD; | 244 | device const float* qi = Q + (ulong(b) * p.T + i) * Cq + h * HD; | ||
| 233 | device const float* doi = dO + (ulong(b) * p.T + i) * Cq + h * HD; | 245 | device const float* doi = dO + (ulong(b) * p.T + i) * Cq + h * HD; | ||
| 234 | float s = 0.0f; | 246 | float s = 0.0f; | ||
@@ -271,11 +283,12 @@ kernel void flash_attn_bwd_dk(device const float* Q [[buffer(0)]], | |||||
| 271 | for (uint d = 0; d < HD; ++d) dk[d] = 0.0f; | 283 | for (uint d = 0; d < HD; ++d) dk[d] = 0.0f; | ||
| 272 | 284 | ||||
| 273 | const uint imin = p.causal ? j : 0; | 285 | const uint imin = p.causal ? j : 0; | ||
| 286 | + const uint imax = (p.window > 0) ? min(p.T, j + p.window) : p.T; | ||||
| 274 | for (uint r = 0; r < rep; ++r) { | 287 | for (uint r = 0; r < rep; ++r) { | ||
| 275 | const uint h = hkv * rep + r; | 288 | const uint h = hkv * rep + r; | ||
| 276 | device const float* Lh = L + (ulong(b) * p.H + h) * p.T; | 289 | device const float* Lh = L + (ulong(b) * p.H + h) * p.T; | ||
| 277 | device const float* Dh = D + (ulong(b) * p.H + h) * p.T; | 290 | device const float* Dh = D + (ulong(b) * p.H + h) * p.T; | ||
| 278 | − for (uint i = imin; i < p.T; ++i) { | 291 | + for (uint i = imin; i < imax; ++i) { | ||
| 279 | device const float* qi = Q + (ulong(b) * p.T + i) * Cq + h * HD; | 292 | device const float* qi = Q + (ulong(b) * p.T + i) * Cq + h * HD; | ||
| 280 | device const float* doi = dO + (ulong(b) * p.T + i) * Cq + h * HD; | 293 | device const float* doi = dO + (ulong(b) * p.T + i) * Cq + h * HD; | ||
| 281 | float s = 0.0f, dp = 0.0f; | 294 | float s = 0.0f, dp = 0.0f; | ||
modified
src/kernels/flash_attention_mma.metal
+1 −0
@@ -36,6 +36,7 @@ struct FlashParams { | |||||
| 36 | uint B, T, H, HKV; | 36 | uint B, T, H, HKV; | ||
| 37 | float scale; | 37 | float scale; | ||
| 38 | uint causal; | 38 | uint causal; | ||
| 39 | + uint window; // unused here: the host routes window > 0 to the scalar kernels | ||||
| 39 | }; | 40 | }; | ||
| 40 | 41 | ||||
| 41 | // Enumerators, never `constant constexpr`: see RESEARCH.md 7a — the latter is | 42 | // Enumerators, never `constant constexpr`: see RESEARCH.md 7a — the latter is | ||
modified
src/nn/attention.h
+39 −19
@@ -20,24 +20,31 @@ public: | |||||
| 20 | }; | 20 | }; | ||
| 21 | 21 | ||||
| 22 | // Causal multi-head self-attention with GQA and (optional) RoPE. | 22 | // Causal multi-head self-attention with GQA and (optional) RoPE. | ||
| 23 | −// No biases (llama convention). | 23 | +// Per-layer flavour comes from `layer_idx` + config: NoPE layers skip RoPE | ||
| 24 | +// (SmolLM3), global layers may use a different rope theta (Gemma 3), and | ||||
| 25 | +// head_dim may be decoupled from d_model/n_heads (Qwen3). Optional QKV bias | ||||
| 26 | +// (Qwen2.5, on q/k/v only — llama convention keeps wo bias-free). | ||||
| 24 | class CausalSelfAttention : public AttentionBase { | 27 | class CausalSelfAttention : public AttentionBase { | ||
| 25 | public: | 28 | public: | ||
| 26 | − CausalSelfAttention(const ModelConfig& cfg, float proj_std, std::mt19937_64& rng) | 29 | + CausalSelfAttention(const ModelConfig& cfg, float proj_std, std::mt19937_64& rng, | ||
| 30 | + int64_t layer_idx = 0) | ||||
| 27 | : n_heads_(cfg.n_heads), | 31 | : n_heads_(cfg.n_heads), | ||
| 28 | n_kv_heads_(cfg.n_kv_heads), | 32 | n_kv_heads_(cfg.n_kv_heads), | ||
| 29 | head_dim_(cfg.head_dim()), | 33 | head_dim_(cfg.head_dim()), | ||
| 30 | − use_rope_(cfg.use_rope), | 34 | + use_rope_(cfg.layer_uses_rope(layer_idx)), | ||
| 31 | − rope_theta_(cfg.rope_theta), | ||||
| 32 | qk_norm_(cfg.qk_norm), | 35 | qk_norm_(cfg.qk_norm), | ||
| 33 | − norm_eps_(cfg.norm_eps) { | 36 | + norm_eps_(cfg.norm_eps), | ||
| 37 | + window_(cfg.layer_is_global(layer_idx) ? 0 : cfg.sliding_window), | ||||
| 38 | + attn_softcap_(cfg.attn_softcap) { | ||||
| 34 | const float std = 0.02f; | 39 | const float std = 0.02f; | ||
| 35 | const int64_t C = cfg.d_model; | 40 | const int64_t C = cfg.d_model; | ||
| 36 | − const int64_t Ckv = cfg.n_kv_heads * cfg.head_dim(); | 41 | + const int64_t Cq = cfg.n_heads * head_dim_; | ||
| 37 | − wq_ = std::make_unique<Linear>(C, C, false, std, rng); | 42 | + const int64_t Ckv = cfg.n_kv_heads * head_dim_; | ||
| 38 | − wk_ = std::make_unique<Linear>(C, Ckv, false, std, rng); | 43 | + const bool bias = cfg.attention_bias; | ||
| 39 | − wv_ = std::make_unique<Linear>(C, Ckv, false, std, rng); | 44 | + wq_ = std::make_unique<Linear>(C, Cq, bias, std, rng); | ||
| 40 | − wo_ = std::make_unique<Linear>(C, C, false, proj_std, rng); // residual projection | 45 | + wk_ = std::make_unique<Linear>(C, Ckv, bias, std, rng); | ||
| 46 | + wv_ = std::make_unique<Linear>(C, Ckv, bias, std, rng); | ||||
| 47 | + wo_ = std::make_unique<Linear>(Cq, C, false, proj_std, rng); // residual projection | ||||
| 41 | const ops::QuantMode qm = quant_mode_from(cfg.quant); | 48 | const ops::QuantMode qm = quant_mode_from(cfg.quant); | ||
| 42 | wq_->set_quant(qm); | 49 | wq_->set_quant(qm); | ||
| 43 | wk_->set_quant(qm); | 50 | wk_->set_quant(qm); | ||
@@ -49,18 +56,28 @@ public: | |||||
| 49 | absorb("wo", *wo_); | 56 | absorb("wo", *wo_); | ||
| 50 | if (qk_norm_) { | 57 | if (qk_norm_) { | ||
| 51 | q_norm_w_ = register_param("q_norm.weight", | 58 | q_norm_w_ = register_param("q_norm.weight", | ||
| 52 | − Tensor::full({cfg.head_dim()}, 1.0f)); | 59 | + Tensor::full({head_dim_}, 1.0f)); | ||
| 53 | k_norm_w_ = register_param("k_norm.weight", | 60 | k_norm_w_ = register_param("k_norm.weight", | ||
| 54 | − Tensor::full({cfg.head_dim()}, 1.0f)); | 61 | + Tensor::full({head_dim_}, 1.0f)); | ||
| 62 | + } | ||||
| 63 | + if (use_rope_) { | ||||
| 64 | + const bool global = cfg.layer_is_global(layer_idx); | ||||
| 65 | + const float theta = (global && cfg.rope_theta_global > 0.0f) | ||||
| 66 | + ? cfg.rope_theta_global | ||||
| 67 | + : cfg.rope_theta; | ||||
| 68 | + freqs_ = ops::rope_freqs(head_dim_, theta, cfg.rope_scale_factor, | ||||
| 69 | + cfg.rope_scale_low, cfg.rope_scale_high, | ||||
| 70 | + cfg.rope_scale_orig_ctx); | ||||
| 55 | } | 71 | } | ||
| 56 | } | 72 | } | ||
| 57 | 73 | ||||
| 58 | Var forward(const Var& x) const override { | 74 | Var forward(const Var& x) const override { | ||
| 59 | const int64_t B = x.value().size(0), T = x.value().size(1), C = x.value().size(2); | 75 | const int64_t B = x.value().size(0), T = x.value().size(1), C = x.value().size(2); | ||
| 76 | + const int64_t Cq = n_heads_ * head_dim_; | ||||
| 60 | const int64_t Ckv = n_kv_heads_ * head_dim_; | 77 | const int64_t Ckv = n_kv_heads_ * head_dim_; | ||
| 61 | 78 | ||||
| 62 | Var x2d = x.reshaped({B * T, C}); | 79 | Var x2d = x.reshaped({B * T, C}); | ||
| 63 | − Var q = wq_->forward(x2d).reshaped({B, T, C}); | 80 | + Var q = wq_->forward(x2d).reshaped({B, T, Cq}); | ||
| 64 | Var k = wk_->forward(x2d).reshaped({B, T, Ckv}); | 81 | Var k = wk_->forward(x2d).reshaped({B, T, Ckv}); | ||
| 65 | Var v = wv_->forward(x2d).reshaped({B, T, Ckv}); | 82 | Var v = wv_->forward(x2d).reshaped({B, T, Ckv}); | ||
| 66 | 83 | ||||
@@ -68,29 +85,32 @@ public: | |||||
| 68 | // Per-head RMSNorm before RoPE (Qwen3/Gemma3): normalize over | 85 | // Per-head RMSNorm before RoPE (Qwen3/Gemma3): normalize over | ||
| 69 | // head_dim by viewing heads as rows. | 86 | // head_dim by viewing heads as rows. | ||
| 70 | q = ops::rmsnorm(q.reshaped({B * T * n_heads_, head_dim_}), q_norm_w_, | 87 | q = ops::rmsnorm(q.reshaped({B * T * n_heads_, head_dim_}), q_norm_w_, | ||
| 71 | − norm_eps_).reshaped({B, T, C}); | 88 | + norm_eps_).reshaped({B, T, Cq}); | ||
| 72 | k = ops::rmsnorm(k.reshaped({B * T * n_kv_heads_, head_dim_}), k_norm_w_, | 89 | k = ops::rmsnorm(k.reshaped({B * T * n_kv_heads_, head_dim_}), k_norm_w_, | ||
| 73 | norm_eps_).reshaped({B, T, Ckv}); | 90 | norm_eps_).reshaped({B, T, Ckv}); | ||
| 74 | } | 91 | } | ||
| 75 | 92 | ||||
| 76 | if (use_rope_) { | 93 | if (use_rope_) { | ||
| 77 | − q = ops::rope(q, n_heads_, rope_theta_, 0); | 94 | + q = ops::rope(q, n_heads_, freqs_, 0); | ||
| 78 | − k = ops::rope(k, n_kv_heads_, rope_theta_, 0); | 95 | + k = ops::rope(k, n_kv_heads_, freqs_, 0); | ||
| 79 | } | 96 | } | ||
| 80 | 97 | ||||
| 81 | const float scale = 1.0f / std::sqrt(float(head_dim_)); | 98 | const float scale = 1.0f / std::sqrt(float(head_dim_)); | ||
| 82 | − Var o = ops::attention(q, k, v, n_heads_, n_kv_heads_, /*causal=*/true, scale); | 99 | + Var o = ops::attention(q, k, v, n_heads_, n_kv_heads_, /*causal=*/true, scale, | ||
| 83 | − return wo_->forward(o.reshaped({B * T, C})).reshaped({B, T, C}); | 100 | + window_, attn_softcap_); | ||
| 101 | + return wo_->forward(o.reshaped({B * T, Cq})).reshaped({B, T, C}); | ||||
| 84 | } | 102 | } | ||
| 85 | 103 | ||||
| 86 | private: | 104 | private: | ||
| 87 | int64_t n_heads_, n_kv_heads_, head_dim_; | 105 | int64_t n_heads_, n_kv_heads_, head_dim_; | ||
| 88 | bool use_rope_; | 106 | bool use_rope_; | ||
| 89 | − float rope_theta_; | ||||
| 90 | bool qk_norm_; | 107 | bool qk_norm_; | ||
| 91 | float norm_eps_; | 108 | float norm_eps_; | ||
| 109 | + int64_t window_; // 0 = full attention (global layer or SWA off) | ||||
| 110 | + float attn_softcap_; | ||||
| 92 | std::unique_ptr<Linear> wq_, wk_, wv_, wo_; | 111 | std::unique_ptr<Linear> wq_, wk_, wv_, wo_; | ||
| 93 | Var q_norm_w_, k_norm_w_; // defined only when qk_norm | 112 | Var q_norm_w_, k_norm_w_; // defined only when qk_norm | ||
| 113 | + Tensor freqs_; // [head_dim/2], defined only when use_rope | ||||
| 94 | }; | 114 | }; | ||
| 95 | 115 | ||||
| 96 | } // namespace forge::nn | 116 | } // namespace forge::nn | ||
modified
src/nn/config.h
+61 −10
@@ -32,6 +32,20 @@ struct ModelConfig { | |||||
| 32 | bool qk_norm = false; // RMSNorm on q/k per head before RoPE (Qwen3/Gemma3) | 32 | bool qk_norm = false; // RMSNorm on q/k per head before RoPE (Qwen3/Gemma3) | ||
| 33 | float final_softcap = 0.0f; // logits = cap*tanh(logits/cap) (Gemma2: 30) | 33 | float final_softcap = 0.0f; // logits = cap*tanh(logits/cap) (Gemma2: 30) | ||
| 34 | bool scale_embeddings = false;// x *= sqrt(d_model) after embedding (Gemma) | 34 | bool scale_embeddings = false;// x *= sqrt(d_model) after embedding (Gemma) | ||
| 35 | + bool attention_bias = false; // bias on q/k/v projections only (Qwen2.5) | ||||
| 36 | + int64_t head_dim_override = 0;// decouple head_dim from d_model/n_heads (Qwen3) | ||||
| 37 | + int64_t nope_every = 0; // skip RoPE every Nth layer (SmolLM3: 4) | ||||
| 38 | + std::string norm_placement = "pre"; // "pre" | "post" (OLMo2) | "sandwich" (Gemma) | ||||
| 39 | + // RoPE scaling, HF "llama3" style (0 = off). Applied to the inv-freq table. | ||||
| 40 | + float rope_scale_factor = 0.0f; // llama3: 32 | ||||
| 41 | + float rope_scale_low = 1.0f; // low_freq_factor | ||||
| 42 | + float rope_scale_high = 4.0f; // high_freq_factor | ||||
| 43 | + int64_t rope_scale_orig_ctx = 8192; // original_max_position_embeddings | ||||
| 44 | + // Sliding-window attention (Mistral/Gemma3). 0 = full attention everywhere. | ||||
| 45 | + int64_t sliding_window = 0; | ||||
| 46 | + int64_t sliding_global_every = 0; // every Nth layer is global (Gemma3: 6); 0 = none | ||||
| 47 | + float rope_theta_global = 0.0f; // theta for global layers (Gemma3: 1e6); 0 = theta | ||||
| 48 | + float attn_softcap = 0.0f; // cap on attention scores (Gemma2: 50) | ||||
| 35 | // Quantization-aware training: linear weights are fake-quantized each | 49 | // Quantization-aware training: linear weights are fake-quantized each | ||
| 36 | // forward (per-row scales, straight-through estimator in backward). | 50 | // forward (per-row scales, straight-through estimator in backward). | ||
| 37 | // Master weights and the optimizer stay f32. | 51 | // Master weights and the optimizer stay f32. | ||
@@ -44,21 +58,36 @@ struct ModelConfig { | |||||
| 44 | float moe_aux_weight = 0.01f; // load-balance loss: E * sum_e mean_gate_e^2 | 58 | float moe_aux_weight = 0.01f; // load-balance loss: E * sum_e mean_gate_e^2 | ||
| 45 | int64_t n_shared_experts = 0; // always-active experts (DeepSeek/Kimi style) | 59 | int64_t n_shared_experts = 0; // always-active experts (DeepSeek/Kimi style) | ||
| 46 | 60 | ||||
| 47 | − int64_t head_dim() const { return d_model / n_heads; } | 61 | + int64_t head_dim() const { | ||
| 62 | + return head_dim_override > 0 ? head_dim_override : d_model / n_heads; | ||||
| 63 | + } | ||||
| 64 | + | ||||
| 65 | + // True when layer i (0-based) attends globally; only meaningful with | ||||
| 66 | + // sliding_window > 0. | ||||
| 67 | + bool layer_is_global(int64_t i) const { | ||||
| 68 | + if (sliding_window <= 0) return true; | ||||
| 69 | + return sliding_global_every > 0 && (i + 1) % sliding_global_every == 0; | ||||
| 70 | + } | ||||
| 71 | + bool layer_uses_rope(int64_t i) const { | ||||
| 72 | + return use_rope && !(nope_every > 0 && (i + 1) % nope_every == 0); | ||||
| 73 | + } | ||||
| 48 | 74 | ||||
| 49 | − // Total parameter count (SwiGLU MLP has three mats, GELU has two). | 75 | + // Total parameter count (SwiGLU MLP has three mats, GELU/ReLU² have two). | ||
| 50 | int64_t num_params() const { | 76 | int64_t num_params() const { | ||
| 51 | const int64_t hd = head_dim(); | 77 | const int64_t hd = head_dim(); | ||
| 52 | − const int64_t attn = d_model * d_model // wq | 78 | + int64_t attn = d_model * n_heads * hd // wq | ||
| 53 | − + 2 * d_model * n_kv_heads * hd // wk, wv | 79 | + + 2 * d_model * n_kv_heads * hd // wk, wv | ||
| 54 | − + d_model * d_model; // wo | 80 | + + n_heads * hd * d_model; // wo | ||
| 81 | + if (attention_bias) attn += (n_heads + 2 * n_kv_heads) * hd; | ||||
| 55 | const int64_t mlp_one = (activation == "swiglu") | 82 | const int64_t mlp_one = (activation == "swiglu") | ||
| 56 | ? 3 * d_model * d_ff | 83 | ? 3 * d_model * d_ff | ||
| 57 | : 2 * d_model * d_ff; | 84 | : 2 * d_model * d_ff; | ||
| 58 | const int64_t mlp = n_experts > 0 | 85 | const int64_t mlp = n_experts > 0 | ||
| 59 | ? (n_experts + n_shared_experts) * mlp_one + n_experts * d_model | 86 | ? (n_experts + n_shared_experts) * mlp_one + n_experts * d_model | ||
| 60 | : mlp_one; // experts + router | 87 | : mlp_one; // experts + router | ||
| 61 | − int64_t norms = (norm == "layernorm" ? 2 : 1) * d_model * (2 * n_layers + 1); | 88 | + const int64_t norms_per_layer = norm_placement == "sandwich" ? 4 : 2; | ||
| 89 | + int64_t norms = (norm == "layernorm" ? 2 : 1) * d_model * | ||||
| 90 | + (norms_per_layer * n_layers + 1); | ||||
| 62 | if (qk_norm) norms += 2 * head_dim() * n_layers; | 91 | if (qk_norm) norms += 2 * head_dim() * n_layers; | ||
| 63 | int64_t total = n_layers * (attn + mlp) + norms + vocab_size * d_model; | 92 | int64_t total = n_layers * (attn + mlp) + norms + vocab_size * d_model; | ||
| 64 | if (!tied_embeddings) total += vocab_size * d_model; | 93 | if (!tied_embeddings) total += vocab_size * d_model; | ||
@@ -129,15 +158,37 @@ inline void from_json(const nlohmann::json& j, ModelConfig& c) { | |||||
| 129 | c.qk_norm = j.value("qk_norm", c.qk_norm); | 158 | c.qk_norm = j.value("qk_norm", c.qk_norm); | ||
| 130 | c.final_softcap = j.value("final_softcap", c.final_softcap); | 159 | c.final_softcap = j.value("final_softcap", c.final_softcap); | ||
| 131 | c.scale_embeddings = j.value("scale_embeddings", c.scale_embeddings); | 160 | c.scale_embeddings = j.value("scale_embeddings", c.scale_embeddings); | ||
| 161 | + c.attention_bias = j.value("attention_bias", c.attention_bias); | ||||
| 162 | + c.head_dim_override = j.value("head_dim", c.head_dim_override); | ||||
| 163 | + c.nope_every = j.value("nope_every", c.nope_every); | ||||
| 164 | + c.norm_placement = j.value("norm_placement", c.norm_placement); | ||||
| 165 | + c.rope_scale_factor = j.value("rope_scale_factor", c.rope_scale_factor); | ||||
| 166 | + c.rope_scale_low = j.value("rope_scale_low", c.rope_scale_low); | ||||
| 167 | + c.rope_scale_high = j.value("rope_scale_high", c.rope_scale_high); | ||||
| 168 | + c.rope_scale_orig_ctx = j.value("rope_scale_orig_ctx", c.rope_scale_orig_ctx); | ||||
| 169 | + c.sliding_window = j.value("sliding_window", c.sliding_window); | ||||
| 170 | + c.sliding_global_every = j.value("sliding_global_every", c.sliding_global_every); | ||||
| 171 | + c.rope_theta_global = j.value("rope_theta_global", c.rope_theta_global); | ||||
| 172 | + c.attn_softcap = j.value("attn_softcap", c.attn_softcap); | ||||
| 132 | 173 | ||||
| 133 | − if (c.d_model % c.n_heads != 0) | 174 | + if (c.head_dim_override == 0 && c.d_model % c.n_heads != 0) | ||
| 134 | − throw std::runtime_error("config: d_model must be divisible by n_heads"); | 175 | + throw std::runtime_error("config: d_model must be divisible by n_heads " | ||
| 176 | + "(or set head_dim explicitly)"); | ||||
| 135 | if (c.n_heads % c.n_kv_heads != 0) | 177 | if (c.n_heads % c.n_kv_heads != 0) | ||
| 136 | throw std::runtime_error("config: n_heads must be divisible by n_kv_heads"); | 178 | throw std::runtime_error("config: n_heads must be divisible by n_kv_heads"); | ||
| 179 | + if (c.head_dim() % 2 != 0) | ||||
| 180 | + throw std::runtime_error("config: head_dim must be even (RoPE pairs)"); | ||||
| 137 | if (c.norm != "rmsnorm" && c.norm != "layernorm") | 181 | if (c.norm != "rmsnorm" && c.norm != "layernorm") | ||
| 138 | throw std::runtime_error("config: norm must be rmsnorm or layernorm"); | 182 | throw std::runtime_error("config: norm must be rmsnorm or layernorm"); | ||
| 139 | − if (c.activation != "swiglu" && c.activation != "gelu") | 183 | + if (c.activation != "swiglu" && c.activation != "gelu" && c.activation != "relu2") | ||
| 140 | − throw std::runtime_error("config: activation must be swiglu or gelu"); | 184 | + throw std::runtime_error("config: activation must be swiglu, gelu or relu2"); | ||
| 185 | + if (c.norm_placement != "pre" && c.norm_placement != "post" && | ||||
| 186 | + c.norm_placement != "sandwich") | ||||
| 187 | + throw std::runtime_error("config: norm_placement must be pre, post or sandwich"); | ||||
| 188 | + if (c.sliding_window < 0 || c.sliding_global_every < 0 || c.nope_every < 0) | ||||
| 189 | + throw std::runtime_error("config: window/pattern values must be >= 0"); | ||||
| 190 | + if (c.attn_softcap < 0.0f) | ||||
| 191 | + throw std::runtime_error("config: attn_softcap must be >= 0"); | ||||
| 141 | if (c.quant != "none" && c.quant != "int8" && c.quant != "ternary") | 192 | if (c.quant != "none" && c.quant != "int8" && c.quant != "ternary") | ||
| 142 | throw std::runtime_error("config: quant must be none, int8 or ternary"); | 193 | throw std::runtime_error("config: quant must be none, int8 or ternary"); | ||
| 143 | if (c.n_experts < 0) | 194 | if (c.n_experts < 0) | ||
modified
src/nn/mlp.h
+5 −4
@@ -22,11 +22,11 @@ public: | |||||
| 22 | virtual Var aux() const { return Var(); } | 22 | virtual Var aux() const { return Var(); } | ||
| 23 | }; | 23 | }; | ||
| 24 | 24 | ||||
| 25 | −// SwiGLU: w2( silu(x w1) ⊙ (x w3) ) | GELU: proj( gelu(fc(x)) ) | 25 | +// SwiGLU: w2( silu(x w1) ⊙ (x w3) ) | GELU/ReLU²: w2( act(x w1) ) | ||
| 26 | class MLP : public MLPBase { | 26 | class MLP : public MLPBase { | ||
| 27 | public: | 27 | public: | ||
| 28 | MLP(const ModelConfig& cfg, float proj_std, std::mt19937_64& rng) | 28 | MLP(const ModelConfig& cfg, float proj_std, std::mt19937_64& rng) | ||
| 29 | − : swiglu_(cfg.activation == "swiglu") { | 29 | + : swiglu_(cfg.activation == "swiglu"), relu2_(cfg.activation == "relu2") { | ||
| 30 | const float std = 0.02f; | 30 | const float std = 0.02f; | ||
| 31 | const int64_t C = cfg.d_model, F = cfg.d_ff; | 31 | const int64_t C = cfg.d_model, F = cfg.d_ff; | ||
| 32 | w1_ = std::make_unique<Linear>(C, F, false, std, rng); | 32 | w1_ = std::make_unique<Linear>(C, F, false, std, rng); | ||
@@ -49,11 +49,12 @@ public: | |||||
| 49 | Var up = w3_->forward(x); | 49 | Var up = w3_->forward(x); | ||
| 50 | return w2_->forward(ops::mul(gate, up)); | 50 | return w2_->forward(ops::mul(gate, up)); | ||
| 51 | } | 51 | } | ||
| 52 | − return w2_->forward(ops::gelu(w1_->forward(x))); | 52 | + Var h = w1_->forward(x); | ||
| 53 | + return w2_->forward(relu2_ ? ops::relu2(h) : ops::gelu(h)); | ||||
| 53 | } | 54 | } | ||
| 54 | 55 | ||||
| 55 | private: | 56 | private: | ||
| 56 | − bool swiglu_; | 57 | + bool swiglu_, relu2_; | ||
| 57 | std::unique_ptr<Linear> w1_, w2_, w3_; | 58 | std::unique_ptr<Linear> w1_, w2_, w3_; | ||
| 58 | }; | 59 | }; | ||
| 59 | 60 | ||||
modified
src/nn/transformer.h
+47 −8
@@ -30,12 +30,19 @@ private: | |||||
| 30 | Var w_, b_; | 30 | Var w_, b_; | ||
| 31 | }; | 31 | }; | ||
| 32 | 32 | ||||
| 33 | −// Pre-norm block: x += attn(norm1(x)); x += mlp(norm2(x)) | 33 | +// Residual block; norm placement is config-selected: | ||
| 34 | +// pre (default): x += attn(norm1(x)); x += mlp(norm2(x)) | ||||
| 35 | +// post (OLMo 2): x += norm1(attn(x)); x += norm2(mlp(x)) | ||||
| 36 | +// sandwich (Gemma): x += norm1p(attn(norm1(x))); x += norm2p(mlp(norm2(x))) | ||||
| 34 | class TransformerBlock : public Module { | 37 | class TransformerBlock : public Module { | ||
| 35 | public: | 38 | public: | ||
| 36 | − TransformerBlock(const ModelConfig& cfg, float proj_std, std::mt19937_64& rng) | 39 | + TransformerBlock(const ModelConfig& cfg, float proj_std, std::mt19937_64& rng, | ||
| 37 | − : norm1_(std::make_unique<Norm>(cfg)), | 40 | + int64_t layer_idx = 0) | ||
| 38 | − attn_(std::make_unique<CausalSelfAttention>(cfg, proj_std, rng)), | 41 | + : placement_(cfg.norm_placement == "pre" ? Placement::Pre | ||
| 42 | + : cfg.norm_placement == "post" ? Placement::Post | ||||
| 43 | + : Placement::Sandwich), | ||||
| 44 | + norm1_(std::make_unique<Norm>(cfg)), | ||||
| 45 | + attn_(std::make_unique<CausalSelfAttention>(cfg, proj_std, rng, layer_idx)), | ||||
| 39 | norm2_(std::make_unique<Norm>(cfg)) { | 46 | norm2_(std::make_unique<Norm>(cfg)) { | ||
| 40 | if (cfg.n_experts > 0) mlp_ = std::make_unique<MoEMLP>(cfg, proj_std, rng); | 47 | if (cfg.n_experts > 0) mlp_ = std::make_unique<MoEMLP>(cfg, proj_std, rng); | ||
| 41 | else mlp_ = std::make_unique<MLP>(cfg, proj_std, rng); | 48 | else mlp_ = std::make_unique<MLP>(cfg, proj_std, rng); | ||
@@ -43,23 +50,55 @@ public: | |||||
| 43 | absorb("attn", *attn_); | 50 | absorb("attn", *attn_); | ||
| 44 | absorb("norm2", *norm2_); | 51 | absorb("norm2", *norm2_); | ||
| 45 | absorb("mlp", *mlp_); | 52 | absorb("mlp", *mlp_); | ||
| 53 | + if (placement_ == Placement::Sandwich) { | ||||
| 54 | + norm1_post_ = std::make_unique<Norm>(cfg); | ||||
| 55 | + norm2_post_ = std::make_unique<Norm>(cfg); | ||||
| 56 | + absorb("norm1_post", *norm1_post_); | ||||
| 57 | + absorb("norm2_post", *norm2_post_); | ||||
| 58 | + } | ||||
| 46 | } | 59 | } | ||
| 47 | 60 | ||||
| 48 | Var forward(const Var& x) const { | 61 | Var forward(const Var& x) const { | ||
| 49 | const int64_t B = x.value().size(0), T = x.value().size(1), C = x.value().size(2); | 62 | const int64_t B = x.value().size(0), T = x.value().size(1), C = x.value().size(2); | ||
| 50 | − Var h = ops::add(x, attn_->forward(norm1_->forward(x))); | 63 | + Var a; | ||
| 51 | − Var m = mlp_->forward(norm2_->forward(h).reshaped({B * T, C})); | 64 | + switch (placement_) { | ||
| 52 | − return ops::add(h, m.reshaped({B, T, C})); | 65 | + case Placement::Pre: a = attn_->forward(norm1_->forward(x)); break; | ||
| 66 | + case Placement::Post: a = norm1_->forward(attn_->forward(x)); break; | ||||
| 67 | + case Placement::Sandwich: | ||||
| 68 | + a = norm1_post_->forward(attn_->forward(norm1_->forward(x))); | ||||
| 69 | + break; | ||||
| 70 | + } | ||||
| 71 | + Var h = ops::add(x, a); | ||||
| 72 | + Var m; | ||||
| 73 | + switch (placement_) { | ||||
| 74 | + case Placement::Pre: | ||||
| 75 | + m = mlp_->forward(norm2_->forward(h).reshaped({B * T, C})) | ||||
| 76 | + .reshaped({B, T, C}); | ||||
| 77 | + break; | ||||
| 78 | + case Placement::Post: | ||||
| 79 | + m = norm2_->forward( | ||||
| 80 | + mlp_->forward(h.reshaped({B * T, C})).reshaped({B, T, C})); | ||||
| 81 | + break; | ||||
| 82 | + case Placement::Sandwich: | ||||
| 83 | + m = norm2_post_->forward( | ||||
| 84 | + mlp_->forward(norm2_->forward(h).reshaped({B * T, C})) | ||||
| 85 | + .reshaped({B, T, C})); | ||||
| 86 | + break; | ||||
| 87 | + } | ||||
| 88 | + return ops::add(h, m); | ||||
| 53 | } | 89 | } | ||
| 54 | 90 | ||||
| 55 | // MoE load-balance loss of the last forward (undefined for dense MLP). | 91 | // MoE load-balance loss of the last forward (undefined for dense MLP). | ||
| 56 | Var moe_aux() const { return mlp_->aux(); } | 92 | Var moe_aux() const { return mlp_->aux(); } | ||
| 57 | 93 | ||||
| 58 | private: | 94 | private: | ||
| 95 | + enum class Placement { Pre, Post, Sandwich }; | ||||
| 96 | + Placement placement_; | ||||
| 59 | std::unique_ptr<Norm> norm1_; | 97 | std::unique_ptr<Norm> norm1_; | ||
| 60 | std::unique_ptr<AttentionBase> attn_; | 98 | std::unique_ptr<AttentionBase> attn_; | ||
| 61 | std::unique_ptr<Norm> norm2_; | 99 | std::unique_ptr<Norm> norm2_; | ||
| 62 | std::unique_ptr<MLPBase> mlp_; | 100 | std::unique_ptr<MLPBase> mlp_; | ||
| 101 | + std::unique_ptr<Norm> norm1_post_, norm2_post_; // sandwich only | ||||
| 63 | }; | 102 | }; | ||
| 64 | 103 | ||||
| 65 | // Decoder-only transformer, entirely shaped by ModelConfig. | 104 | // Decoder-only transformer, entirely shaped by ModelConfig. | ||
@@ -79,7 +118,7 @@ public: | |||||
| 79 | absorb("pos_emb", *pos_emb_); | 118 | absorb("pos_emb", *pos_emb_); | ||
| 80 | } | 119 | } | ||
| 81 | for (int64_t i = 0; i < cfg.n_layers; ++i) { | 120 | for (int64_t i = 0; i < cfg.n_layers; ++i) { | ||
| 82 | − blocks_.push_back(std::make_unique<TransformerBlock>(cfg, proj_std, rng)); | 121 | + blocks_.push_back(std::make_unique<TransformerBlock>(cfg, proj_std, rng, i)); | ||
| 83 | absorb("blocks." + std::to_string(i), *blocks_.back()); | 122 | absorb("blocks." + std::to_string(i), *blocks_.back()); | ||
| 84 | } | 123 | } | ||
| 85 | final_norm_ = std::make_unique<Norm>(cfg); | 124 | final_norm_ = std::make_unique<Norm>(cfg); | ||
modified
src/ops/cpu/cpu_ops.cpp
+46 −15
@@ -181,6 +181,23 @@ void gelu_backward(const Tensor& x, const Tensor& dout, Tensor& dx) { | |||||
| 181 | } | 181 | } | ||
| 182 | } | 182 | } | ||
| 183 | 183 | ||||
| 184 | +void relu2(const Tensor& x, Tensor& out) { | ||||
| 185 | + const float* px = x.data<float>(); | ||||
| 186 | + float* po = out.data<float>(); | ||||
| 187 | + for (int64_t i = 0; i < x.numel(); ++i) { | ||||
| 188 | + const float v = std::max(px[i], 0.0f); | ||||
| 189 | + po[i] = v * v; | ||||
| 190 | + } | ||||
| 191 | +} | ||||
| 192 | + | ||||
| 193 | +void relu2_backward(const Tensor& x, const Tensor& dout, Tensor& dx) { | ||||
| 194 | + const float* px = x.data<float>(); | ||||
| 195 | + const float* pd = dout.data<float>(); | ||||
| 196 | + float* pdx = dx.data<float>(); | ||||
| 197 | + for (int64_t i = 0; i < x.numel(); ++i) | ||||
| 198 | + pdx[i] += pd[i] * 2.0f * std::max(px[i], 0.0f); | ||||
| 199 | +} | ||||
| 200 | + | ||||
| 184 | void softcap(const Tensor& x, float cap, Tensor& out) { | 201 | void softcap(const Tensor& x, float cap, Tensor& out) { | ||
| 185 | const float* px = x.data<float>(); | 202 | const float* px = x.data<float>(); | ||
| 186 | float* po = out.data<float>(); | 203 | float* po = out.data<float>(); | ||
@@ -487,7 +504,7 @@ void embedding_backward(const Tensor& ids, const Tensor& dout, Tensor& dweight) | |||||
| 487 | 504 | ||||
| 488 | namespace { | 505 | namespace { | ||
| 489 | void rope_impl(const float* in, float* out, int64_t B, int64_t T, int64_t H, int64_t hd, | 506 | void rope_impl(const float* in, float* out, int64_t B, int64_t T, int64_t H, int64_t hd, | ||
| 490 | − float theta, int64_t pos_offset, bool inverse, bool accumulate) { | 507 | + const float* freqs, int64_t pos_offset, bool inverse, bool accumulate) { | ||
| 491 | const int64_t C = H * hd; | 508 | const int64_t C = H * hd; | ||
| 492 | parallel_rows(B * T, [&](int64_t bt) { | 509 | parallel_rows(B * T, [&](int64_t bt) { | ||
| 493 | const int64_t t = bt % T; | 510 | const int64_t t = bt % T; | ||
@@ -496,8 +513,7 @@ void rope_impl(const float* in, float* out, int64_t B, int64_t T, int64_t H, int | |||||
| 496 | float* dst = out + bt * C; | 513 | float* dst = out + bt * C; | ||
| 497 | for (int64_t h = 0; h < H; ++h) { | 514 | for (int64_t h = 0; h < H; ++h) { | ||
| 498 | for (int64_t k = 0; k < hd / 2; ++k) { | 515 | for (int64_t k = 0; k < hd / 2; ++k) { | ||
| 499 | − const float freq = std::pow(theta, -2.0f * float(k) / float(hd)); | 516 | + const float angle = pos * freqs[k]; | ||
| 500 | − const float angle = pos * freq; | ||||
| 501 | const float c = std::cos(angle); | 517 | const float c = std::cos(angle); | ||
| 502 | const float s = inverse ? -std::sin(angle) : std::sin(angle); | 518 | const float s = inverse ? -std::sin(angle) : std::sin(angle); | ||
| 503 | const int64_t i0 = h * hd + 2 * k; | 519 | const int64_t i0 = h * hd + 2 * k; | ||
@@ -517,26 +533,28 @@ void rope_impl(const float* in, float* out, int64_t B, int64_t T, int64_t H, int | |||||
| 517 | } | 533 | } | ||
| 518 | } // namespace | 534 | } // namespace | ||
| 519 | 535 | ||||
| 520 | −void rope(const Tensor& x, int64_t n_heads, float theta, int64_t pos_offset, Tensor& out) { | 536 | +void rope(const Tensor& x, int64_t n_heads, const Tensor& freqs, int64_t pos_offset, | ||
| 537 | + Tensor& out) { | ||||
| 521 | check(x.ndim() == 3, "rope: expected [B,T,C]"); | 538 | check(x.ndim() == 3, "rope: expected [B,T,C]"); | ||
| 522 | const int64_t hd = x.size(2) / n_heads; | 539 | const int64_t hd = x.size(2) / n_heads; | ||
| 523 | check(hd % 2 == 0, "rope: head_dim must be even"); | 540 | check(hd % 2 == 0, "rope: head_dim must be even"); | ||
| 541 | + check(freqs.numel() == hd / 2, "rope: freqs must have head_dim/2 entries"); | ||||
| 524 | rope_impl(x.data<float>(), out.data<float>(), x.size(0), x.size(1), n_heads, hd, | 542 | rope_impl(x.data<float>(), out.data<float>(), x.size(0), x.size(1), n_heads, hd, | ||
| 525 | − theta, pos_offset, /*inverse=*/false, /*accumulate=*/false); | 543 | + freqs.data<float>(), pos_offset, /*inverse=*/false, /*accumulate=*/false); | ||
| 526 | } | 544 | } | ||
| 527 | 545 | ||||
| 528 | −void rope_backward(const Tensor& dout, int64_t n_heads, float theta, int64_t pos_offset, | 546 | +void rope_backward(const Tensor& dout, int64_t n_heads, const Tensor& freqs, | ||
| 529 | − Tensor& dx) { | 547 | + int64_t pos_offset, Tensor& dx) { | ||
| 530 | const int64_t hd = dout.size(2) / n_heads; | 548 | const int64_t hd = dout.size(2) / n_heads; | ||
| 531 | rope_impl(dout.data<float>(), dx.data<float>(), dout.size(0), dout.size(1), n_heads, | 549 | rope_impl(dout.data<float>(), dx.data<float>(), dout.size(0), dout.size(1), n_heads, | ||
| 532 | − hd, theta, pos_offset, /*inverse=*/true, /*accumulate=*/true); | 550 | + hd, freqs.data<float>(), pos_offset, /*inverse=*/true, /*accumulate=*/true); | ||
| 533 | } | 551 | } | ||
| 534 | 552 | ||||
| 535 | // ---- attention ------------------------------------------------------------------- | 553 | // ---- attention ------------------------------------------------------------------- | ||
| 536 | 554 | ||||
| 537 | void attention(const Tensor& q, const Tensor& k, const Tensor& v, | 555 | void attention(const Tensor& q, const Tensor& k, const Tensor& v, | ||
| 538 | int64_t n_heads, int64_t n_kv_heads, bool causal, float scale, | 556 | int64_t n_heads, int64_t n_kv_heads, bool causal, float scale, | ||
| 539 | − Tensor& out, Tensor* probs_out) { | 557 | + Tensor& out, Tensor* probs_out, int64_t window, float attn_softcap) { | ||
| 540 | check(q.ndim() == 3 && k.ndim() == 3 && v.ndim() == 3, "attention: expected [B,T,C]"); | 558 | check(q.ndim() == 3 && k.ndim() == 3 && v.ndim() == 3, "attention: expected [B,T,C]"); | ||
| 541 | const int64_t B = q.size(0), T = q.size(1); | 559 | const int64_t B = q.size(0), T = q.size(1); | ||
| 542 | const int64_t hd = q.size(2) / n_heads; | 560 | const int64_t hd = q.size(2) / n_heads; | ||
@@ -560,29 +578,34 @@ void attention(const Tensor& q, const Tensor& k, const Tensor& v, | |||||
| 560 | for (int64_t i = 0; i < T; ++i) { | 578 | for (int64_t i = 0; i < T; ++i) { | ||
| 561 | const float* qi = pq + (b * T + i) * Cq + h * hd; | 579 | const float* qi = pq + (b * T + i) * Cq + h * hd; | ||
| 562 | const int64_t jmax = causal ? i : T - 1; | 580 | const int64_t jmax = causal ? i : T - 1; | ||
| 581 | + // sliding window: attend only to the last `window` keys (incl. self) | ||||
| 582 | + const int64_t jmin = window > 0 ? std::max<int64_t>(0, i - window + 1) : 0; | ||||
| 563 | // scores (masked positions never written; treated as prob 0) | 583 | // scores (masked positions never written; treated as prob 0) | ||
| 564 | float m = -INFINITY; | 584 | float m = -INFINITY; | ||
| 565 | − for (int64_t j = 0; j <= jmax; ++j) { | 585 | + for (int64_t j = jmin; j <= jmax; ++j) { | ||
| 566 | const float* kj = pk + (b * T + j) * Ckv + hkv * hd; | 586 | const float* kj = pk + (b * T + j) * Ckv + hkv * hd; | ||
| 567 | float s = 0.0f; | 587 | float s = 0.0f; | ||
| 568 | for (int64_t d = 0; d < hd; ++d) s += qi[d] * kj[d]; | 588 | for (int64_t d = 0; d < hd; ++d) s += qi[d] * kj[d]; | ||
| 569 | s *= scale; | 589 | s *= scale; | ||
| 590 | + if (attn_softcap > 0.0f) | ||||
| 591 | + s = attn_softcap * std::tanh(s / attn_softcap); | ||||
| 570 | P[i * T + j] = s; | 592 | P[i * T + j] = s; | ||
| 571 | m = std::max(m, s); | 593 | m = std::max(m, s); | ||
| 572 | } | 594 | } | ||
| 573 | float sum = 0.0f; | 595 | float sum = 0.0f; | ||
| 574 | − for (int64_t j = 0; j <= jmax; ++j) { | 596 | + for (int64_t j = jmin; j <= jmax; ++j) { | ||
| 575 | const float e = std::exp(P[i * T + j] - m); | 597 | const float e = std::exp(P[i * T + j] - m); | ||
| 576 | P[i * T + j] = e; | 598 | P[i * T + j] = e; | ||
| 577 | sum += e; | 599 | sum += e; | ||
| 578 | } | 600 | } | ||
| 579 | const float inv = 1.0f / sum; | 601 | const float inv = 1.0f / sum; | ||
| 580 | − for (int64_t j = 0; j <= jmax; ++j) P[i * T + j] *= inv; | 602 | + for (int64_t j = 0; j < jmin; ++j) P[i * T + j] = 0.0f; | ||
| 603 | + for (int64_t j = jmin; j <= jmax; ++j) P[i * T + j] *= inv; | ||||
| 581 | for (int64_t j = jmax + 1; j < T; ++j) P[i * T + j] = 0.0f; | 604 | for (int64_t j = jmax + 1; j < T; ++j) P[i * T + j] = 0.0f; | ||
| 582 | 605 | ||||
| 583 | float* oi = po + (b * T + i) * Cq + h * hd; | 606 | float* oi = po + (b * T + i) * Cq + h * hd; | ||
| 584 | for (int64_t d = 0; d < hd; ++d) oi[d] = 0.0f; | 607 | for (int64_t d = 0; d < hd; ++d) oi[d] = 0.0f; | ||
| 585 | − for (int64_t j = 0; j <= jmax; ++j) { | 608 | + for (int64_t j = jmin; j <= jmax; ++j) { | ||
| 586 | const float p = P[i * T + j]; | 609 | const float p = P[i * T + j]; | ||
| 587 | const float* vj = pv + (b * T + j) * Ckv + hkv * hd; | 610 | const float* vj = pv + (b * T + j) * Ckv + hkv * hd; | ||
| 588 | for (int64_t d = 0; d < hd; ++d) oi[d] += p * vj[d]; | 611 | for (int64_t d = 0; d < hd; ++d) oi[d] += p * vj[d]; | ||
@@ -594,7 +617,7 @@ void attention(const Tensor& q, const Tensor& k, const Tensor& v, | |||||
| 594 | void attention_backward(const Tensor& q, const Tensor& k, const Tensor& v, | 617 | void attention_backward(const Tensor& q, const Tensor& k, const Tensor& v, | ||
| 595 | const Tensor& probs, const Tensor& out, const Tensor& dout, | 618 | const Tensor& probs, const Tensor& out, const Tensor& dout, | ||
| 596 | int64_t n_heads, int64_t n_kv_heads, float scale, | 619 | int64_t n_heads, int64_t n_kv_heads, float scale, | ||
| 597 | − Tensor& dq, Tensor& dk, Tensor& dv) { | 620 | + Tensor& dq, Tensor& dk, Tensor& dv, float attn_softcap) { | ||
| 598 | const int64_t B = q.size(0), T = q.size(1); | 621 | const int64_t B = q.size(0), T = q.size(1); | ||
| 599 | const int64_t hd = q.size(2) / n_heads; | 622 | const int64_t hd = q.size(2) / n_heads; | ||
| 600 | const int64_t rep = n_heads / n_kv_heads; | 623 | const int64_t rep = n_heads / n_kv_heads; | ||
@@ -637,7 +660,15 @@ void attention_backward(const Tensor& q, const Tensor& k, const Tensor& v, | |||||
| 637 | 660 | ||||
| 638 | float dp = 0.0f; | 661 | float dp = 0.0f; | ||
| 639 | for (int64_t d = 0; d < hd; ++d) dp += doi[d] * vj[d]; | 662 | for (int64_t d = 0; d < hd; ++d) dp += doi[d] * vj[d]; | ||
| 640 | − const float ds = p * (dp - row_dot) * scale; | 663 | + float ds = p * (dp - row_dot) * scale; | ||
| 664 | + if (attn_softcap > 0.0f) { | ||||
| 665 | + // chain through s' = cap·tanh(s/cap): recompute the raw | ||||
| 666 | + // score, factor is 1 − tanh² | ||||
| 667 | + float s = 0.0f; | ||||
| 668 | + for (int64_t d = 0; d < hd; ++d) s += qi[d] * kj[d]; | ||||
| 669 | + const float t = std::tanh(s * scale / attn_softcap); | ||||
| 670 | + ds *= 1.0f - t * t; | ||||
| 671 | + } | ||||
| 641 | 672 | ||||
| 642 | for (int64_t d = 0; d < hd; ++d) { | 673 | for (int64_t d = 0; d < hd; ++d) { | ||
| 643 | dvj[d] += p * doi[d]; | 674 | dvj[d] += p * doi[d]; | ||
modified
src/ops/cpu/cpu_ops.h
+14 −6
@@ -36,6 +36,8 @@ void silu(const Tensor& x, Tensor& out); | |||||
| 36 | void silu_backward(const Tensor& x, const Tensor& dout, Tensor& dx); | 36 | void silu_backward(const Tensor& x, const Tensor& dout, Tensor& dx); | ||
| 37 | void gelu(const Tensor& x, Tensor& out); // tanh approximation (GPT-2) | 37 | void gelu(const Tensor& x, Tensor& out); // tanh approximation (GPT-2) | ||
| 38 | void gelu_backward(const Tensor& x, const Tensor& dout, Tensor& dx); | 38 | void gelu_backward(const Tensor& x, const Tensor& dout, Tensor& dx); | ||
| 39 | +void relu2(const Tensor& x, Tensor& out); // max(x,0)^2 | ||||
| 40 | +void relu2_backward(const Tensor& x, const Tensor& dout, Tensor& dx); | ||||
| 39 | void softcap(const Tensor& x, float cap, Tensor& out); // cap*tanh(x/cap) | 41 | void softcap(const Tensor& x, float cap, Tensor& out); // cap*tanh(x/cap) | ||
| 40 | // dx += dout * (1 - (y/cap)^2) — takes the forward OUTPUT y | 42 | // dx += dout * (1 - (y/cap)^2) — takes the forward OUTPUT y | ||
| 41 | void softcap_backward(const Tensor& y, const Tensor& dout, float cap, Tensor& dx); | 43 | void softcap_backward(const Tensor& y, const Tensor& dout, float cap, Tensor& dx); | ||
@@ -62,24 +64,30 @@ void embedding_backward(const Tensor& ids, const Tensor& dout, Tensor& dweight); | |||||
| 62 | // ---- RoPE (interleaved-pairs / GPT-J convention; see RESEARCH.md §7) ------- | 64 | // ---- RoPE (interleaved-pairs / GPT-J convention; see RESEARCH.md §7) ------- | ||
| 63 | // x: [B, T, H*hd]; rotates pairs (2k, 2k+1) inside each head. pos_offset | 65 | // x: [B, T, H*hd]; rotates pairs (2k, 2k+1) inside each head. pos_offset | ||
| 64 | // shifts absolute positions (KV-cache generation). | 66 | // shifts absolute positions (KV-cache generation). | ||
| 65 | −void rope(const Tensor& x, int64_t n_heads, float theta, int64_t pos_offset, Tensor& out); | 67 | +// freqs: [head_dim/2] per-pair inverse frequencies, precomputed by the caller. | ||
| 68 | +void rope(const Tensor& x, int64_t n_heads, const Tensor& freqs, int64_t pos_offset, | ||||
| 69 | + Tensor& out); | ||||
| 66 | // Backward of a rotation is the inverse rotation applied to dout. | 70 | // Backward of a rotation is the inverse rotation applied to dout. | ||
| 67 | −void rope_backward(const Tensor& dout, int64_t n_heads, float theta, int64_t pos_offset, | 71 | +void rope_backward(const Tensor& dout, int64_t n_heads, const Tensor& freqs, | ||
| 68 | − Tensor& dx); | 72 | + int64_t pos_offset, Tensor& dx); | ||
| 69 | 73 | ||||
| 70 | // ---- attention (composed reference: scores → mask → softmax → PV) ---------- | 74 | // ---- attention (composed reference: scores → mask → softmax → PV) ---------- | ||
| 71 | // q: [B, T, H*hd], k/v: [B, T, Hkv*hd], out: [B, T, H*hd]. GQA via | 75 | // q: [B, T, H*hd], k/v: [B, T, Hkv*hd], out: [B, T, H*hd]. GQA via | ||
| 72 | // kv_head = h / (H / Hkv). If probs_out is non-null it receives the softmax | 76 | // kv_head = h / (H / Hkv). If probs_out is non-null it receives the softmax | ||
| 73 | // probabilities [B, H, T, T] (needed for backward). | 77 | // probabilities [B, H, T, T] (needed for backward). | ||
| 78 | +// window > 0: sliding-window attention (attend to the last `window` keys, | ||||
| 79 | +// self included). attn_softcap > 0: cap·tanh(score/cap) pre-softmax (Gemma 2). | ||||
| 74 | void attention(const Tensor& q, const Tensor& k, const Tensor& v, | 80 | void attention(const Tensor& q, const Tensor& k, const Tensor& v, | ||
| 75 | int64_t n_heads, int64_t n_kv_heads, bool causal, float scale, | 81 | int64_t n_heads, int64_t n_kv_heads, bool causal, float scale, | ||
| 76 | − Tensor& out, Tensor* probs_out); | 82 | + Tensor& out, Tensor* probs_out, int64_t window = 0, | ||
| 83 | + float attn_softcap = 0.0f); | ||||
| 77 | // Uses the FlashAttention-2 identity rowsum(dP ∘ P) == dO_i · O_i, so `out` | 84 | // Uses the FlashAttention-2 identity rowsum(dP ∘ P) == dO_i · O_i, so `out` | ||
| 78 | −// (the forward result) is required. | 85 | +// (the forward result) is required. Masked positions have prob 0, so the | ||
| 86 | +// window needs no explicit handling here. | ||||
| 79 | void attention_backward(const Tensor& q, const Tensor& k, const Tensor& v, | 87 | void attention_backward(const Tensor& q, const Tensor& k, const Tensor& v, | ||
| 80 | const Tensor& probs, const Tensor& out, const Tensor& dout, | 88 | const Tensor& probs, const Tensor& out, const Tensor& dout, | ||
| 81 | int64_t n_heads, int64_t n_kv_heads, float scale, | 89 | int64_t n_heads, int64_t n_kv_heads, float scale, | ||
| 82 | − Tensor& dq, Tensor& dk, Tensor& dv); | 90 | + Tensor& dq, Tensor& dk, Tensor& dv, float attn_softcap = 0.0f); | ||
| 83 | 91 | ||||
| 84 | // ---- QAT / MoE -------------------------------------------------------------- | 92 | // ---- QAT / MoE -------------------------------------------------------------- | ||
| 85 | // Per-row fake quantization of a 2-D weight; mode 0 = int8 (absmax/127), | 93 | // Per-row fake quantization of a 2-D weight; mode 0 = int8 (absmax/127), | ||
modified
src/ops/metal/metal_ops.cpp
+41 −20
@@ -218,6 +218,14 @@ void gelu(const Tensor& x, Tensor& out) { | |||||
| 218 | encode_flat("gelu_f32", {&x, &out}, nullptr, 0, x.numel()); | 218 | encode_flat("gelu_f32", {&x, &out}, nullptr, 0, x.numel()); | ||
| 219 | } | 219 | } | ||
| 220 | 220 | ||||
| 221 | +void relu2(const Tensor& x, Tensor& out) { | ||||
| 222 | + encode_flat("relu2_f32", {&x, &out}, nullptr, 0, x.numel()); | ||||
| 223 | +} | ||||
| 224 | + | ||||
| 225 | +void relu2_backward(const Tensor& x, const Tensor& dout, Tensor& dx) { | ||||
| 226 | + encode_flat("relu2_bwd_f32", {&x, &dout, &dx}, nullptr, 0, x.numel()); | ||||
| 227 | +} | ||||
| 228 | + | ||||
| 221 | void softcap(const Tensor& x, float cap, Tensor& out) { | 229 | void softcap(const Tensor& x, float cap, Tensor& out) { | ||
| 222 | encode_flat("softcap_f32", {&x, &out}, &cap, sizeof(cap), x.numel()); | 230 | encode_flat("softcap_f32", {&x, &out}, &cap, sizeof(cap), x.numel()); | ||
| 223 | } | 231 | } | ||
@@ -437,14 +445,14 @@ void layernorm_backward(const Tensor& x, const Tensor& w, float eps, | |||||
| 437 | namespace { | 445 | namespace { | ||
| 438 | struct RopeParams { | 446 | struct RopeParams { | ||
| 439 | uint32_t T, H, HD; | 447 | uint32_t T, H, HD; | ||
| 440 | − float theta; | ||||
| 441 | uint32_t pos_offset; | 448 | uint32_t pos_offset; | ||
| 442 | }; | 449 | }; | ||
| 443 | 450 | ||||
| 444 | −void rope_encode(const Tensor& in, Tensor& out, int64_t n_heads, float theta, | 451 | +void rope_encode(const Tensor& in, Tensor& out, int64_t n_heads, const Tensor& freqs, | ||
| 445 | int64_t pos_offset, bool inverse) { | 452 | int64_t pos_offset, bool inverse) { | ||
| 446 | const int64_t B = in.size(0), T = in.size(1), C = in.size(2); | 453 | const int64_t B = in.size(0), T = in.size(1), C = in.size(2); | ||
| 447 | const int64_t hd = C / n_heads; | 454 | const int64_t hd = C / n_heads; | ||
| 455 | + check(freqs.numel() == hd / 2, "rope: freqs must have head_dim/2 entries"); | ||||
| 448 | Device& dev = Device::get(); | 456 | Device& dev = Device::get(); | ||
| 449 | MTL::FunctionConstantValues* constants = MTL::FunctionConstantValues::alloc()->init(); | 457 | MTL::FunctionConstantValues* constants = MTL::FunctionConstantValues::alloc()->init(); | ||
| 450 | // slots 0/1 belong to matmul TA/TB; RoPE uses slot 2 | 458 | // slots 0/1 belong to matmul TA/TB; RoPE uses slot 2 | ||
@@ -457,21 +465,22 @@ void rope_encode(const Tensor& in, Tensor& out, int64_t n_heads, float theta, | |||||
| 457 | enc->setComputePipelineState(pso); | 465 | enc->setComputePipelineState(pso); | ||
| 458 | enc->setBuffer(in.buffer(), in.buffer_offset(), 0); | 466 | enc->setBuffer(in.buffer(), in.buffer_offset(), 0); | ||
| 459 | enc->setBuffer(out.buffer(), out.buffer_offset(), 1); | 467 | enc->setBuffer(out.buffer(), out.buffer_offset(), 1); | ||
| 460 | − RopeParams p{uint32_t(T), uint32_t(n_heads), uint32_t(hd), theta, | 468 | + RopeParams p{uint32_t(T), uint32_t(n_heads), uint32_t(hd), uint32_t(pos_offset)}; | ||
| 461 | − uint32_t(pos_offset)}; | ||||
| 462 | enc->setBytes(&p, sizeof(p), 2); | 469 | enc->setBytes(&p, sizeof(p), 2); | ||
| 470 | + enc->setBuffer(freqs.buffer(), freqs.buffer_offset(), 3); | ||||
| 463 | const int64_t pairs = B * T * n_heads * (hd / 2); | 471 | const int64_t pairs = B * T * n_heads * (hd / 2); | ||
| 464 | enc->dispatchThreads(MTL::Size(NS::UInteger(pairs), 1, 1), MTL::Size(256, 1, 1)); | 472 | enc->dispatchThreads(MTL::Size(NS::UInteger(pairs), 1, 1), MTL::Size(256, 1, 1)); | ||
| 465 | } | 473 | } | ||
| 466 | } // namespace | 474 | } // namespace | ||
| 467 | 475 | ||||
| 468 | −void rope(const Tensor& x, int64_t n_heads, float theta, int64_t pos_offset, Tensor& out) { | 476 | +void rope(const Tensor& x, int64_t n_heads, const Tensor& freqs, int64_t pos_offset, | ||
| 469 | − rope_encode(x, out, n_heads, theta, pos_offset, /*inverse=*/false); | 477 | + Tensor& out) { | ||
| 478 | + rope_encode(x, out, n_heads, freqs, pos_offset, /*inverse=*/false); | ||||
| 470 | } | 479 | } | ||
| 471 | 480 | ||||
| 472 | −void rope_backward(const Tensor& dout, int64_t n_heads, float theta, int64_t pos_offset, | 481 | +void rope_backward(const Tensor& dout, int64_t n_heads, const Tensor& freqs, | ||
| 473 | − Tensor& dx) { | 482 | + int64_t pos_offset, Tensor& dx) { | ||
| 474 | − rope_encode(dout, dx, n_heads, theta, pos_offset, /*inverse=*/true); | 483 | + rope_encode(dout, dx, n_heads, freqs, pos_offset, /*inverse=*/true); | ||
| 475 | } | 484 | } | ||
| 476 | 485 | ||||
| 477 | // ---- embedding ------------------------------------------------------------------- | 486 | // ---- embedding ------------------------------------------------------------------- | ||
@@ -517,12 +526,14 @@ struct AttnParams { | |||||
| 517 | uint32_t B, T, H, HKV, HD; | 526 | uint32_t B, T, H, HKV, HD; | ||
| 518 | float scale; | 527 | float scale; | ||
| 519 | uint32_t causal; | 528 | uint32_t causal; | ||
| 529 | + uint32_t window; | ||||
| 530 | + float softcap; | ||||
| 520 | }; | 531 | }; | ||
| 521 | } // namespace | 532 | } // namespace | ||
| 522 | 533 | ||||
| 523 | void attention(const Tensor& q, const Tensor& k, const Tensor& v, | 534 | void attention(const Tensor& q, const Tensor& k, const Tensor& v, | ||
| 524 | int64_t n_heads, int64_t n_kv_heads, bool causal, float scale, | 535 | int64_t n_heads, int64_t n_kv_heads, bool causal, float scale, | ||
| 525 | − Tensor& out, Tensor* probs_out) { | 536 | + Tensor& out, Tensor* probs_out, int64_t window, float attn_softcap) { | ||
| 526 | check(probs_out != nullptr, "attention: probs_out required (unfused path)"); | 537 | check(probs_out != nullptr, "attention: probs_out required (unfused path)"); | ||
| 527 | const int64_t B = q.size(0), T = q.size(1); | 538 | const int64_t B = q.size(0), T = q.size(1); | ||
| 528 | const int64_t hd = q.size(2) / n_heads; | 539 | const int64_t hd = q.size(2) / n_heads; | ||
@@ -536,7 +547,8 @@ void attention(const Tensor& q, const Tensor& k, const Tensor& v, | |||||
| 536 | enc->setBuffer(out.buffer(), out.buffer_offset(), 3); | 547 | enc->setBuffer(out.buffer(), out.buffer_offset(), 3); | ||
| 537 | enc->setBuffer(probs_out->buffer(), probs_out->buffer_offset(), 4); | 548 | enc->setBuffer(probs_out->buffer(), probs_out->buffer_offset(), 4); | ||
| 538 | AttnParams p{uint32_t(B), uint32_t(T), uint32_t(n_heads), uint32_t(n_kv_heads), | 549 | AttnParams p{uint32_t(B), uint32_t(T), uint32_t(n_heads), uint32_t(n_kv_heads), | ||
| 539 | − uint32_t(hd), scale, causal ? 1u : 0u}; | 550 | + uint32_t(hd), scale, causal ? 1u : 0u, uint32_t(window), | ||
| 551 | + attn_softcap}; | ||||
| 540 | enc->setBytes(&p, sizeof(p), 5); | 552 | enc->setBytes(&p, sizeof(p), 5); | ||
| 541 | enc->dispatchThreads(MTL::Size(NS::UInteger(B * n_heads * T), 1, 1), | 553 | enc->dispatchThreads(MTL::Size(NS::UInteger(B * n_heads * T), 1, 1), | ||
| 542 | MTL::Size(64, 1, 1)); | 554 | MTL::Size(64, 1, 1)); | ||
@@ -545,12 +557,13 @@ void attention(const Tensor& q, const Tensor& k, const Tensor& v, | |||||
| 545 | void attention_backward(const Tensor& q, const Tensor& k, const Tensor& v, | 557 | void attention_backward(const Tensor& q, const Tensor& k, const Tensor& v, | ||
| 546 | const Tensor& probs, const Tensor& out, const Tensor& dout, | 558 | const Tensor& probs, const Tensor& out, const Tensor& dout, | ||
| 547 | int64_t n_heads, int64_t n_kv_heads, float scale, | 559 | int64_t n_heads, int64_t n_kv_heads, float scale, | ||
| 548 | − Tensor& dq, Tensor& dk, Tensor& dv) { | 560 | + Tensor& dq, Tensor& dk, Tensor& dv, float attn_softcap) { | ||
| 549 | const int64_t B = q.size(0), T = q.size(1); | 561 | const int64_t B = q.size(0), T = q.size(1); | ||
| 550 | const int64_t hd = q.size(2) / n_heads; | 562 | const int64_t hd = q.size(2) / n_heads; | ||
| 551 | Device& dev = Device::get(); | 563 | Device& dev = Device::get(); | ||
| 564 | + // window is irrelevant here: masked positions carry prob 0 in `probs`. | ||||
| 552 | AttnParams p{uint32_t(B), uint32_t(T), uint32_t(n_heads), uint32_t(n_kv_heads), | 565 | AttnParams p{uint32_t(B), uint32_t(T), uint32_t(n_heads), uint32_t(n_kv_heads), | ||
| 553 | − uint32_t(hd), scale, 1u}; | 566 | + uint32_t(hd), scale, 1u, 0u, attn_softcap}; | ||
| 554 | 567 | ||||
| 555 | // D[b,h,i] = dO_i · O_i == rowsum(dP ∘ P): computed once per query row so | 568 | // D[b,h,i] = dO_i · O_i == rowsum(dP ∘ P): computed once per query row so | ||
| 556 | // neither backward kernel needs the O(T) inner recompute. | 569 | // neither backward kernel needs the O(T) inner recompute. | ||
@@ -678,6 +691,8 @@ struct FlashParams { | |||||
| 678 | uint32_t B, T, H, HKV; | 691 | uint32_t B, T, H, HKV; | ||
| 679 | float scale; | 692 | float scale; | ||
| 680 | uint32_t causal; | 693 | uint32_t causal; | ||
| 694 | + uint32_t window; // sliding window; 0 = full. Scalar kernels only — the | ||||
| 695 | + // caller routes window > 0 away from MMA. | ||||
| 681 | }; | 696 | }; | ||
| 682 | 697 | ||||
| 683 | // Must match the INSTANTIATE_FLASH list in flash_attention.metal. | 698 | // Must match the INSTANTIATE_FLASH list in flash_attention.metal. | ||
@@ -697,13 +712,15 @@ bool flash_supported(int64_t head_dim) { | |||||
| 697 | 712 | ||||
| 698 | void flash_attention(const Tensor& q, const Tensor& k, const Tensor& v, | 713 | void flash_attention(const Tensor& q, const Tensor& k, const Tensor& v, | ||
| 699 | int64_t n_heads, int64_t n_kv_heads, bool causal, float scale, | 714 | int64_t n_heads, int64_t n_kv_heads, bool causal, float scale, | ||
| 700 | − Tensor& out, Tensor& lse, FlashKernel kernel) { | 715 | + Tensor& out, Tensor& lse, FlashKernel kernel, int64_t window) { | ||
| 701 | const int64_t B = q.size(0), T = q.size(1); | 716 | const int64_t B = q.size(0), T = q.size(1); | ||
| 702 | const int64_t hd = q.size(2) / n_heads; | 717 | const int64_t hd = q.size(2) / n_heads; | ||
| 703 | check(flash_supported(hd), "flash_attention: unsupported head_dim"); | 718 | check(flash_supported(hd), "flash_attention: unsupported head_dim"); | ||
| 704 | 719 | ||||
| 705 | − if (kernel == FlashKernel::Auto) kernel = FlashKernel::MMA; | 720 | + if (kernel == FlashKernel::Auto) | ||
| 721 | + kernel = window > 0 ? FlashKernel::Scalar : FlashKernel::MMA; | ||||
| 706 | const bool mma = kernel == FlashKernel::MMA; | 722 | const bool mma = kernel == FlashKernel::MMA; | ||
| 723 | + check(!(mma && window > 0), "flash_attention: MMA kernel has no window support"); | ||||
| 707 | 724 | ||||
| 708 | Device& dev = Device::get(); | 725 | Device& dev = Device::get(); | ||
| 709 | MTL::ComputePipelineState* pso = dev.pipeline( | 726 | MTL::ComputePipelineState* pso = dev.pipeline( | ||
@@ -716,7 +733,7 @@ void flash_attention(const Tensor& q, const Tensor& k, const Tensor& v, | |||||
| 716 | enc->setBuffer(out.buffer(), out.buffer_offset(), 3); | 733 | enc->setBuffer(out.buffer(), out.buffer_offset(), 3); | ||
| 717 | enc->setBuffer(lse.buffer(), lse.buffer_offset(), 4); | 734 | enc->setBuffer(lse.buffer(), lse.buffer_offset(), 4); | ||
| 718 | FlashParams p{uint32_t(B), uint32_t(T), uint32_t(n_heads), uint32_t(n_kv_heads), | 735 | FlashParams p{uint32_t(B), uint32_t(T), uint32_t(n_heads), uint32_t(n_kv_heads), | ||
| 719 | − scale, causal ? 1u : 0u}; | 736 | + scale, causal ? 1u : 0u, uint32_t(window)}; | ||
| 720 | enc->setBytes(&p, sizeof(p), 5); | 737 | enc->setBytes(&p, sizeof(p), 5); | ||
| 721 | // One threadgroup per (query block, head, batch). Block size and thread | 738 | // One threadgroup per (query block, head, batch). Block size and thread | ||
| 722 | // count must match the kernel's enums: the scalar kernel is one thread | 739 | // count must match the kernel's enums: the scalar kernel is one thread | ||
@@ -736,22 +753,26 @@ void flash_attention_backward(const Tensor& q, const Tensor& k, const Tensor& v, | |||||
| 736 | const Tensor& out, const Tensor& lse, const Tensor& dout, | 753 | const Tensor& out, const Tensor& lse, const Tensor& dout, | ||
| 737 | int64_t n_heads, int64_t n_kv_heads, bool causal, | 754 | int64_t n_heads, int64_t n_kv_heads, bool causal, | ||
| 738 | float scale, Tensor& dq, Tensor& dk, Tensor& dv, | 755 | float scale, Tensor& dq, Tensor& dk, Tensor& dv, | ||
| 739 | − FlashKernel kernel) { | 756 | + FlashKernel kernel, int64_t window) { | ||
| 740 | const int64_t B = q.size(0), T = q.size(1); | 757 | const int64_t B = q.size(0), T = q.size(1); | ||
| 741 | const int64_t hd = q.size(2) / n_heads; | 758 | const int64_t hd = q.size(2) / n_heads; | ||
| 742 | check(flash_supported(hd), "flash_attention_backward: unsupported head_dim"); | 759 | check(flash_supported(hd), "flash_attention_backward: unsupported head_dim"); | ||
| 743 | − if (kernel == FlashKernel::Auto) kernel = FlashKernel::MMA; | 760 | + if (kernel == FlashKernel::Auto) | ||
| 761 | + kernel = window > 0 ? FlashKernel::Scalar : FlashKernel::MMA; | ||||
| 744 | const bool mma = kernel == FlashKernel::MMA; | 762 | const bool mma = kernel == FlashKernel::MMA; | ||
| 763 | + check(!(mma && window > 0), | ||||
| 764 | + "flash_attention_backward: MMA kernel has no window support"); | ||||
| 745 | 765 | ||||
| 746 | Device& dev = Device::get(); | 766 | Device& dev = Device::get(); | ||
| 747 | FlashParams p{uint32_t(B), uint32_t(T), uint32_t(n_heads), uint32_t(n_kv_heads), | 767 | FlashParams p{uint32_t(B), uint32_t(T), uint32_t(n_heads), uint32_t(n_kv_heads), | ||
| 748 | − scale, causal ? 1u : 0u}; | 768 | + scale, causal ? 1u : 0u, uint32_t(window)}; | ||
| 749 | 769 | ||||
| 750 | // D[b,h,i] = dO_i . O_i (shared with the unfused path). | 770 | // D[b,h,i] = dO_i . O_i (shared with the unfused path). | ||
| 751 | Tensor d_term = Tensor::empty({B, n_heads, T}); | 771 | Tensor d_term = Tensor::empty({B, n_heads, T}); | ||
| 752 | { | 772 | { | ||
| 753 | AttnParams ap{uint32_t(B), uint32_t(T), uint32_t(n_heads), | 773 | AttnParams ap{uint32_t(B), uint32_t(T), uint32_t(n_heads), | ||
| 754 | − uint32_t(n_kv_heads), uint32_t(hd), scale, causal ? 1u : 0u}; | 774 | + uint32_t(n_kv_heads), uint32_t(hd), scale, causal ? 1u : 0u, | ||
| 775 | + 0u, 0.0f}; | ||||
| 755 | MTL::ComputePipelineState* pso = dev.pipeline("attention_bwd_d_f32"); | 776 | MTL::ComputePipelineState* pso = dev.pipeline("attention_bwd_d_f32"); | ||
| 756 | MTL::ComputeCommandEncoder* enc = Stream::get().encoder(); | 777 | MTL::ComputeCommandEncoder* enc = Stream::get().encoder(); | ||
| 757 | enc->setComputePipelineState(pso); | 778 | enc->setComputePipelineState(pso); | ||
modified
src/ops/metal/metal_ops.h
+19 −7
@@ -88,6 +88,8 @@ void scale(const Tensor& a, float s, Tensor& out); | |||||
| 88 | void add_bias(const Tensor& x, const Tensor& bias, Tensor& out); | 88 | void add_bias(const Tensor& x, const Tensor& bias, Tensor& out); | ||
| 89 | void silu(const Tensor& x, Tensor& out); | 89 | void silu(const Tensor& x, Tensor& out); | ||
| 90 | void gelu(const Tensor& x, Tensor& out); | 90 | void gelu(const Tensor& x, Tensor& out); | ||
| 91 | +void relu2(const Tensor& x, Tensor& out); // max(x,0)^2 | ||||
| 92 | +void relu2_backward(const Tensor& x, const Tensor& dout, Tensor& dx); | ||||
| 91 | void softcap(const Tensor& x, float cap, Tensor& out); // cap*tanh(x/cap) | 93 | void softcap(const Tensor& x, float cap, Tensor& out); // cap*tanh(x/cap) | ||
| 92 | void softcap_backward(const Tensor& y, const Tensor& dout, float cap, Tensor& dx); | 94 | void softcap_backward(const Tensor& y, const Tensor& dout, float cap, Tensor& dx); | ||
| 93 | 95 | ||||
@@ -106,20 +108,26 @@ void rmsnorm_backward(const Tensor& x, const Tensor& w, float eps, | |||||
| 106 | void layernorm_backward(const Tensor& x, const Tensor& w, float eps, | 108 | void layernorm_backward(const Tensor& x, const Tensor& w, float eps, | ||
| 107 | const Tensor& dout, Tensor& dx, Tensor& dw, Tensor& db); | 109 | const Tensor& dout, Tensor& dx, Tensor& dw, Tensor& db); | ||
| 108 | 110 | ||||
| 109 | −void rope(const Tensor& x, int64_t n_heads, float theta, int64_t pos_offset, Tensor& out); | 111 | +// freqs: [head_dim/2] per-pair inverse frequencies, host-precomputed | ||
| 110 | −void rope_backward(const Tensor& dout, int64_t n_heads, float theta, int64_t pos_offset, | 112 | +// (fixed theta, llama3 rope-scaling, per-layer theta — all just tables). | ||
| 111 | − Tensor& dx); | 113 | +void rope(const Tensor& x, int64_t n_heads, const Tensor& freqs, int64_t pos_offset, | ||
| 114 | + Tensor& out); | ||||
| 115 | +void rope_backward(const Tensor& dout, int64_t n_heads, const Tensor& freqs, | ||||
| 116 | + int64_t pos_offset, Tensor& dx); | ||||
| 112 | 117 | ||||
| 113 | void embedding(const Tensor& weight, const Tensor& ids, Tensor& out); // ids i32 | 118 | void embedding(const Tensor& weight, const Tensor& ids, Tensor& out); // ids i32 | ||
| 114 | void embedding_backward(const Tensor& ids, const Tensor& dout, Tensor& dweight); | 119 | void embedding_backward(const Tensor& ids, const Tensor& dout, Tensor& dweight); | ||
| 115 | 120 | ||||
| 121 | +// window > 0: sliding-window attention; attn_softcap > 0: cap·tanh pre-softmax | ||||
| 122 | +// (unfused path only — the fused kernels don't support softcap). | ||||
| 116 | void attention(const Tensor& q, const Tensor& k, const Tensor& v, | 123 | void attention(const Tensor& q, const Tensor& k, const Tensor& v, | ||
| 117 | int64_t n_heads, int64_t n_kv_heads, bool causal, float scale, | 124 | int64_t n_heads, int64_t n_kv_heads, bool causal, float scale, | ||
| 118 | − Tensor& out, Tensor* probs_out); | 125 | + Tensor& out, Tensor* probs_out, int64_t window = 0, | ||
| 126 | + float attn_softcap = 0.0f); | ||||
| 119 | void attention_backward(const Tensor& q, const Tensor& k, const Tensor& v, | 127 | void attention_backward(const Tensor& q, const Tensor& k, const Tensor& v, | ||
| 120 | const Tensor& probs, const Tensor& out, const Tensor& dout, | 128 | const Tensor& probs, const Tensor& out, const Tensor& dout, | ||
| 121 | int64_t n_heads, int64_t n_kv_heads, float scale, | 129 | int64_t n_heads, int64_t n_kv_heads, float scale, | ||
| 122 | − Tensor& dq, Tensor& dk, Tensor& dv); | 130 | + Tensor& dq, Tensor& dk, Tensor& dv, float attn_softcap = 0.0f); | ||
| 123 | 131 | ||||
| 124 | // Fused (flash) attention: stores no T x T probabilities, only the per-row | 132 | // Fused (flash) attention: stores no T x T probabilities, only the per-row | ||
| 125 | // logsumexp `lse` [B, n_heads, T] that the backward re-expands. Supported for | 133 | // logsumexp `lse` [B, n_heads, T] that the backward re-expands. Supported for | ||
@@ -128,14 +136,18 @@ bool flash_supported(int64_t head_dim); | |||||
| 128 | // Scalar: one thread per query row. MMA: simdgroup_matrix 8x8 tiles. Same | 136 | // Scalar: one thread per query row. MMA: simdgroup_matrix 8x8 tiles. Same | ||
| 129 | // outputs; Auto picks MMA where supported. | 137 | // outputs; Auto picks MMA where supported. | ||
| 130 | enum class FlashKernel { Auto, Scalar, MMA }; | 138 | enum class FlashKernel { Auto, Scalar, MMA }; | ||
| 139 | +// window > 0 requires the Scalar kernel (Auto routes there automatically); | ||||
| 140 | +// out-of-window KV blocks are skipped, so cost scales with the window. | ||||
| 131 | void flash_attention(const Tensor& q, const Tensor& k, const Tensor& v, | 141 | void flash_attention(const Tensor& q, const Tensor& k, const Tensor& v, | ||
| 132 | int64_t n_heads, int64_t n_kv_heads, bool causal, float scale, | 142 | int64_t n_heads, int64_t n_kv_heads, bool causal, float scale, | ||
| 133 | − Tensor& out, Tensor& lse, FlashKernel kernel = FlashKernel::Auto); | 143 | + Tensor& out, Tensor& lse, FlashKernel kernel = FlashKernel::Auto, | ||
| 144 | + int64_t window = 0); | ||||
| 134 | void flash_attention_backward(const Tensor& q, const Tensor& k, const Tensor& v, | 145 | void flash_attention_backward(const Tensor& q, const Tensor& k, const Tensor& v, | ||
| 135 | const Tensor& out, const Tensor& lse, const Tensor& dout, | 146 | const Tensor& out, const Tensor& lse, const Tensor& dout, | ||
| 136 | int64_t n_heads, int64_t n_kv_heads, bool causal, | 147 | int64_t n_heads, int64_t n_kv_heads, bool causal, | ||
| 137 | float scale, Tensor& dq, Tensor& dk, Tensor& dv, | 148 | float scale, Tensor& dq, Tensor& dk, Tensor& dv, | ||
| 138 | − FlashKernel kernel = FlashKernel::Auto); | 149 | + FlashKernel kernel = FlashKernel::Auto, | ||
| 150 | + int64_t window = 0); | ||||
| 139 | 151 | ||||
| 140 | // losses: [N] per-row buffer; loss_out: [1] mean over n_valid. dlogits | 152 | // losses: [N] per-row buffer; loss_out: [1] mean over n_valid. dlogits | ||
| 141 | // optional (accumulated). ids must be i32. | 153 | // optional (accumulated). ids must be i32. | ||
modified
src/ops/ops.cpp
+87 −30
@@ -4,6 +4,8 @@ | |||||
| 4 | #include "ops/cpu/cpu_ops.h" | 4 | #include "ops/cpu/cpu_ops.h" | ||
| 5 | #include "ops/metal/metal_ops.h" | 5 | #include "ops/metal/metal_ops.h" | ||
| 6 | 6 | ||||
| 7 | +#include <cmath> | ||||
| 8 | + | ||||
| 7 | namespace forge::ops { | 9 | namespace forge::ops { | ||
| 8 | 10 | ||||
| 9 | namespace { | 11 | namespace { | ||
@@ -208,6 +210,23 @@ Var gelu(const Var& x) { | |||||
| 208 | return result; | 210 | return result; | ||
| 209 | } | 211 | } | ||
| 210 | 212 | ||||
| 213 | +Var relu2(const Var& x) { | ||||
| 214 | + Tensor out = Tensor::empty(x.value().shape()); | ||||
| 215 | + if (gpu()) metal::relu2(x.value(), out); | ||||
| 216 | + else cpu::relu2(x.value(), out); | ||||
| 217 | + | ||||
| 218 | + const bool needs = grad_needed({&x}); | ||||
| 219 | + Var result(std::move(out), needs); | ||||
| 220 | + if (needs) { | ||||
| 221 | + Tape::get().record([x, result]() { | ||||
| 222 | + if (!x.requires_grad()) return; | ||||
| 223 | + if (gpu()) metal::relu2_backward(x.value(), result.grad(), x.grad()); | ||||
| 224 | + else cpu::relu2_backward(x.value(), result.grad(), x.grad()); | ||||
| 225 | + }); | ||||
| 226 | + } | ||||
| 227 | + return result; | ||||
| 228 | +} | ||||
| 229 | + | ||||
| 211 | Var softcap(const Var& x, float cap) { | 230 | Var softcap(const Var& x, float cap) { | ||
| 212 | Tensor out = Tensor::empty(x.value().shape()); | 231 | Tensor out = Tensor::empty(x.value().shape()); | ||
| 213 | if (gpu()) metal::softcap(x.value(), cap, out); | 232 | if (gpu()) metal::softcap(x.value(), cap, out); | ||
@@ -370,25 +389,56 @@ Var embedding(const Var& weight, const Tensor& ids) { | |||||
| 370 | return result; | 389 | return result; | ||
| 371 | } | 390 | } | ||
| 372 | 391 | ||||
| 373 | −Var rope(const Var& x, int64_t n_heads, float theta, int64_t pos_offset) { | 392 | +Tensor rope_freqs(int64_t head_dim, float theta, float scale_factor, | ||
| 393 | + float low_freq_factor, float high_freq_factor, int64_t original_ctx) { | ||||
| 394 | + Tensor freqs = Tensor::empty({head_dim / 2}); | ||||
| 395 | + float* f = freqs.data<float>(); | ||||
| 396 | + constexpr float kTwoPi = 6.28318530717958647692f; | ||||
| 397 | + for (int64_t k = 0; k < head_dim / 2; ++k) { | ||||
| 398 | + float inv = std::pow(theta, -2.0f * float(k) / float(head_dim)); | ||||
| 399 | + if (scale_factor > 0.0f) { | ||||
| 400 | + // HF "llama3" rope scaling (modeling_rope_utils.py). | ||||
| 401 | + const float wavelen = kTwoPi / inv; | ||||
| 402 | + const float lo = float(original_ctx) / low_freq_factor; // long waves | ||||
| 403 | + const float hi = float(original_ctx) / high_freq_factor; // short waves | ||||
| 404 | + if (wavelen > lo) { | ||||
| 405 | + inv /= scale_factor; | ||||
| 406 | + } else if (wavelen > hi) { | ||||
| 407 | + const float s = (float(original_ctx) / wavelen - low_freq_factor) / | ||||
| 408 | + (high_freq_factor - low_freq_factor); | ||||
| 409 | + inv = (1.0f - s) * inv / scale_factor + s * inv; | ||||
| 410 | + } | ||||
| 411 | + } | ||||
| 412 | + f[k] = inv; | ||||
| 413 | + } | ||||
| 414 | + return freqs; | ||||
| 415 | +} | ||||
| 416 | + | ||||
| 417 | +Var rope(const Var& x, int64_t n_heads, const Tensor& freqs, int64_t pos_offset) { | ||||
| 374 | Tensor out = Tensor::empty(x.value().shape()); | 418 | Tensor out = Tensor::empty(x.value().shape()); | ||
| 375 | − if (gpu()) metal::rope(x.value(), n_heads, theta, pos_offset, out); | 419 | + if (gpu()) metal::rope(x.value(), n_heads, freqs, pos_offset, out); | ||
| 376 | − else cpu::rope(x.value(), n_heads, theta, pos_offset, out); | 420 | + else cpu::rope(x.value(), n_heads, freqs, pos_offset, out); | ||
| 377 | 421 | ||||
| 378 | const bool needs = grad_needed({&x}); | 422 | const bool needs = grad_needed({&x}); | ||
| 379 | Var result(std::move(out), needs); | 423 | Var result(std::move(out), needs); | ||
| 380 | if (needs) { | 424 | if (needs) { | ||
| 381 | − Tape::get().record([x, n_heads, theta, pos_offset, result]() { | 425 | + Tape::get().record([x, n_heads, freqs, pos_offset, result]() { | ||
| 382 | if (!x.requires_grad()) return; | 426 | if (!x.requires_grad()) return; | ||
| 383 | − if (gpu()) metal::rope_backward(result.grad(), n_heads, theta, pos_offset, x.grad()); | 427 | + if (gpu()) metal::rope_backward(result.grad(), n_heads, freqs, pos_offset, x.grad()); | ||
| 384 | − else cpu::rope_backward(result.grad(), n_heads, theta, pos_offset, x.grad()); | 428 | + else cpu::rope_backward(result.grad(), n_heads, freqs, pos_offset, x.grad()); | ||
| 385 | }); | 429 | }); | ||
| 386 | } | 430 | } | ||
| 387 | return result; | 431 | return result; | ||
| 388 | } | 432 | } | ||
| 389 | 433 | ||||
| 434 | +Var rope(const Var& x, int64_t n_heads, float theta, int64_t pos_offset) { | ||||
| 435 | + const int64_t hd = x.value().size(2) / n_heads; | ||||
| 436 | + return rope(x, n_heads, rope_freqs(hd, theta), pos_offset); | ||||
| 437 | +} | ||||
| 438 | + | ||||
| 390 | Var attention(const Var& q, const Var& k, const Var& v, | 439 | Var attention(const Var& q, const Var& k, const Var& v, | ||
| 391 | − int64_t n_heads, int64_t n_kv_heads, bool causal, float scale) { | 440 | + int64_t n_heads, int64_t n_kv_heads, bool causal, float scale, | ||
| 441 | + int64_t window, float attn_softcap) { | ||||
| 392 | const int64_t B = q.value().size(0), T = q.value().size(1); | 442 | const int64_t B = q.value().size(0), T = q.value().size(1); | ||
| 393 | const int64_t head_dim = q.value().size(2) / n_heads; | 443 | const int64_t head_dim = q.value().size(2) / n_heads; | ||
| 394 | Tensor out = Tensor::empty(q.value().shape()); | 444 | Tensor out = Tensor::empty(q.value().shape()); | ||
@@ -396,14 +446,17 @@ Var attention(const Var& q, const Var& k, const Var& v, | |||||
| 396 | 446 | ||||
| 397 | // Fused path: keeps only the per-row logsumexp instead of a [B,H,T,T] | 447 | // Fused path: keeps only the per-row logsumexp instead of a [B,H,T,T] | ||
| 398 | // probability tensor, so memory is linear in T rather than quadratic. | 448 | // probability tensor, so memory is linear in T rather than quadratic. | ||
| 399 | − if (gpu() && metal::flash_supported(head_dim)) { | 449 | + // Softcap needs the materialized-probs path (the fused kernels' online | ||
| 450 | + // softmax has no cap hook yet). | ||||
| 451 | + if (gpu() && metal::flash_supported(head_dim) && attn_softcap <= 0.0f) { | ||||
| 400 | Tensor lse = Tensor::empty({B, n_heads, T}); | 452 | Tensor lse = Tensor::empty({B, n_heads, T}); | ||
| 401 | metal::flash_attention(q.value(), k.value(), v.value(), n_heads, n_kv_heads, | 453 | metal::flash_attention(q.value(), k.value(), v.value(), n_heads, n_kv_heads, | ||
| 402 | − causal, scale, out, lse); | 454 | + causal, scale, out, lse, metal::FlashKernel::Auto, | ||
| 455 | + window); | ||||
| 403 | Var result(std::move(out), needs); | 456 | Var result(std::move(out), needs); | ||
| 404 | if (needs) { | 457 | if (needs) { | ||
| 405 | Tape::get().record( | 458 | Tape::get().record( | ||
| 406 | − [q, k, v, lse, n_heads, n_kv_heads, causal, scale, result]() { | 459 | + [q, k, v, lse, n_heads, n_kv_heads, causal, scale, window, result]() { | ||
| 407 | Tensor dq_scratch, dk_scratch, dv_scratch; | 460 | Tensor dq_scratch, dk_scratch, dv_scratch; | ||
| 408 | Tensor& dq = q.requires_grad() ? q.grad() | 461 | Tensor& dq = q.requires_grad() ? q.grad() | ||
| 409 | : (dq_scratch = Tensor::zeros(q.value().shape())); | 462 | : (dq_scratch = Tensor::zeros(q.value().shape())); | ||
@@ -414,7 +467,8 @@ Var attention(const Var& q, const Var& k, const Var& v, | |||||
| 414 | metal::flash_attention_backward(q.value(), k.value(), v.value(), | 467 | metal::flash_attention_backward(q.value(), k.value(), v.value(), | ||
| 415 | result.value(), lse, result.grad(), | 468 | result.value(), lse, result.grad(), | ||
| 416 | n_heads, n_kv_heads, causal, scale, | 469 | n_heads, n_kv_heads, causal, scale, | ||
| 417 | − dq, dk, dv); | 470 | + dq, dk, dv, | ||
| 471 | + metal::FlashKernel::Auto, window); | ||||
| 418 | }); | 472 | }); | ||
| 419 | } | 473 | } | ||
| 420 | return result; | 474 | return result; | ||
@@ -423,30 +477,33 @@ Var attention(const Var& q, const Var& k, const Var& v, | |||||
| 423 | Tensor probs = Tensor::empty({B, n_heads, T, T}); | 477 | Tensor probs = Tensor::empty({B, n_heads, T, T}); | ||
| 424 | if (gpu()) | 478 | if (gpu()) | ||
| 425 | metal::attention(q.value(), k.value(), v.value(), n_heads, n_kv_heads, causal, | 479 | metal::attention(q.value(), k.value(), v.value(), n_heads, n_kv_heads, causal, | ||
| 426 | − scale, out, &probs); | 480 | + scale, out, &probs, window, attn_softcap); | ||
| 427 | else | 481 | else | ||
| 428 | cpu::attention(q.value(), k.value(), v.value(), n_heads, n_kv_heads, causal, | 482 | cpu::attention(q.value(), k.value(), v.value(), n_heads, n_kv_heads, causal, | ||
| 429 | − scale, out, &probs); | 483 | + scale, out, &probs, window, attn_softcap); | ||
| 430 | 484 | ||||
| 431 | Var result(std::move(out), needs); | 485 | Var result(std::move(out), needs); | ||
| 432 | if (needs) { | 486 | if (needs) { | ||
| 433 | − Tape::get().record([q, k, v, probs, n_heads, n_kv_heads, scale, result]() { | 487 | + Tape::get().record( | ||
| 434 | − Tensor dq_scratch, dk_scratch, dv_scratch; | 488 | + [q, k, v, probs, n_heads, n_kv_heads, scale, attn_softcap, result]() { | ||
| 435 | − Tensor& dq = q.requires_grad() ? q.grad() | 489 | + Tensor dq_scratch, dk_scratch, dv_scratch; | ||
| 436 | − : (dq_scratch = Tensor::zeros(q.value().shape())); | 490 | + Tensor& dq = q.requires_grad() ? q.grad() | ||
| 437 | − Tensor& dk = k.requires_grad() ? k.grad() | 491 | + : (dq_scratch = Tensor::zeros(q.value().shape())); | ||
| 438 | − : (dk_scratch = Tensor::zeros(k.value().shape())); | 492 | + Tensor& dk = k.requires_grad() ? k.grad() | ||
| 439 | − Tensor& dv = v.requires_grad() ? v.grad() | 493 | + : (dk_scratch = Tensor::zeros(k.value().shape())); | ||
| 440 | − : (dv_scratch = Tensor::zeros(v.value().shape())); | 494 | + Tensor& dv = v.requires_grad() ? v.grad() | ||
| 441 | − if (gpu()) | 495 | + : (dv_scratch = Tensor::zeros(v.value().shape())); | ||
| 442 | − metal::attention_backward(q.value(), k.value(), v.value(), probs, | 496 | + if (gpu()) | ||
| 443 | − result.value(), result.grad(), n_heads, | 497 | + metal::attention_backward(q.value(), k.value(), v.value(), probs, | ||
| 444 | − n_kv_heads, scale, dq, dk, dv); | 498 | + result.value(), result.grad(), n_heads, | ||
| 445 | − else | 499 | + n_kv_heads, scale, dq, dk, dv, | ||
| 446 | − cpu::attention_backward(q.value(), k.value(), v.value(), probs, | 500 | + attn_softcap); | ||
| 447 | − result.value(), result.grad(), n_heads, | 501 | + else | ||
| 448 | − n_kv_heads, scale, dq, dk, dv); | 502 | + cpu::attention_backward(q.value(), k.value(), v.value(), probs, | ||
| 449 | − }); | 503 | + result.value(), result.grad(), n_heads, | ||
| 504 | + n_kv_heads, scale, dq, dk, dv, | ||||
| 505 | + attn_softcap); | ||||
| 506 | + }); | ||||
| 450 | } | 507 | } | ||
| 451 | return result; | 508 | return result; | ||
| 452 | } | 509 | } | ||
modified
src/ops/ops.h
+18 −3
@@ -31,6 +31,7 @@ Var scale(const Var& a, float s); | |||||
| 31 | 31 | ||||
| 32 | Var silu(const Var& x); | 32 | Var silu(const Var& x); | ||
| 33 | Var gelu(const Var& x); | 33 | Var gelu(const Var& x); | ||
| 34 | +Var relu2(const Var& x); // max(x,0)^2 — nanoGPT-speedrun activation | ||||
| 34 | 35 | ||||
| 35 | // Gemma-style soft capping: cap * tanh(x / cap). Bounds logits smoothly. | 36 | // Gemma-style soft capping: cap * tanh(x / cap). Bounds logits smoothly. | ||
| 36 | Var softcap(const Var& x, float cap); | 37 | Var softcap(const Var& x, float cap); | ||
@@ -57,12 +58,26 @@ Var layernorm(const Var& x, const Var& w, const Var& b, float eps); | |||||
| 57 | // weight [V,C], ids [B,T] (u16/i32) → [B,T,C] | 58 | // weight [V,C], ids [B,T] (u16/i32) → [B,T,C] | ||
| 58 | Var embedding(const Var& weight, const Tensor& ids); | 59 | Var embedding(const Var& weight, const Tensor& ids); | ||
| 59 | 60 | ||||
| 60 | −// x [B,T,H*hd], interleaved-pairs RoPE per head | 61 | +// Build the [head_dim/2] inverse-frequency table: theta^(-2k/hd), optionally | ||
| 62 | +// rescaled HF-"llama3" style (scale_factor > 0): high-frequency components | ||||
| 63 | +// untouched, low-frequency divided by factor, smooth blend between. | ||||
| 64 | +Tensor rope_freqs(int64_t head_dim, float theta, float scale_factor = 0.0f, | ||||
| 65 | + float low_freq_factor = 1.0f, float high_freq_factor = 4.0f, | ||||
| 66 | + int64_t original_ctx = 8192); | ||||
| 67 | + | ||||
| 68 | +// x [B,T,H*hd], interleaved-pairs RoPE per head. freqs from rope_freqs() | ||||
| 69 | +// (callers cache it — one table per layer flavour). | ||||
| 70 | +Var rope(const Var& x, int64_t n_heads, const Tensor& freqs, int64_t pos_offset = 0); | ||||
| 71 | +// Convenience: plain-theta table built per call (tests / one-offs). | ||||
| 61 | Var rope(const Var& x, int64_t n_heads, float theta, int64_t pos_offset = 0); | 72 | Var rope(const Var& x, int64_t n_heads, float theta, int64_t pos_offset = 0); | ||
| 62 | 73 | ||||
| 63 | −// q [B,T,H*hd], k/v [B,T,Hkv*hd] → [B,T,H*hd]; causal + GQA | 74 | +// q [B,T,H*hd], k/v [B,T,Hkv*hd] → [B,T,H*hd]; causal + GQA. | ||
| 75 | +// window > 0: sliding-window attention (Mistral/Gemma3) — fused scalar path. | ||||
| 76 | +// attn_softcap > 0: cap·tanh(score/cap) pre-softmax (Gemma2) — routes to the | ||||
| 77 | +// unfused path (probs materialized), so reserve it for short contexts. | ||||
| 64 | Var attention(const Var& q, const Var& k, const Var& v, | 78 | Var attention(const Var& q, const Var& k, const Var& v, | ||
| 65 | − int64_t n_heads, int64_t n_kv_heads, bool causal, float scale); | 79 | + int64_t n_heads, int64_t n_kv_heads, bool causal, float scale, | ||
| 80 | + int64_t window = 0, float attn_softcap = 0.0f); | ||||
| 66 | 81 | ||||
| 67 | // logits [N,V], targets [N] (i32/u16, ignore_index=-1) → scalar mean loss | 82 | // logits [N,V], targets [N] (i32/u16, ignore_index=-1) → scalar mean loss | ||
| 68 | Var cross_entropy(const Var& logits, const Tensor& targets); | 83 | Var cross_entropy(const Var& logits, const Tensor& targets); | ||
modified
tests/test_ops.cpp
+46 −0
@@ -600,6 +600,52 @@ int main() { | |||||
| 600 | vcfg.tied_embeddings = true; | 600 | vcfg.tied_embeddings = true; | ||
| 601 | vcfg.qk_norm = true; vcfg.final_softcap = 30.0f; vcfg.scale_embeddings = true; | 601 | vcfg.qk_norm = true; vcfg.final_softcap = 30.0f; vcfg.scale_embeddings = true; | ||
| 602 | test_backend_parity_model("qk-norm + softcap + embed-scale", vcfg); | 602 | test_backend_parity_model("qk-norm + softcap + embed-scale", vcfg); | ||
| 603 | + | ||||
| 604 | + // Wave-1 knobs: ReLU² + QKV bias + decoupled head_dim + NoPE + rope scaling | ||||
| 605 | + forge::ModelConfig w1; | ||||
| 606 | + w1.n_layers = 2; w1.d_model = 16; w1.n_heads = 2; w1.n_kv_heads = 1; | ||||
| 607 | + w1.d_ff = 24; w1.vocab_size = 11; w1.context_length = 8; | ||||
| 608 | + w1.tied_embeddings = true; | ||||
| 609 | + w1.activation = "relu2"; w1.attention_bias = true; | ||||
| 610 | + w1.head_dim_override = 10; w1.nope_every = 2; | ||||
| 611 | + w1.rope_scale_factor = 8.0f; w1.rope_scale_orig_ctx = 4; | ||||
| 612 | + test_backend_parity_model("relu2 + qkv-bias + head_dim10 + nope + rope-scale", w1); | ||||
| 613 | + | ||||
| 614 | + // Norm placements: OLMo2 post and Gemma sandwich | ||||
| 615 | + forge::ModelConfig np; | ||||
| 616 | + np.n_layers = 2; np.d_model = 16; np.n_heads = 2; np.n_kv_heads = 2; | ||||
| 617 | + np.d_ff = 24; np.vocab_size = 11; np.context_length = 8; | ||||
| 618 | + np.tied_embeddings = true; | ||||
| 619 | + np.norm_placement = "post"; | ||||
| 620 | + test_backend_parity_model("post-norm (OLMo2)", np); | ||||
| 621 | + np.norm_placement = "sandwich"; | ||||
| 622 | + test_backend_parity_model("sandwich norm (Gemma)", np); | ||||
| 623 | + | ||||
| 624 | + // Wave-2: sliding window (Mistral) through the fused scalar path | ||||
| 625 | + // (head_dim 64), + local/global pattern, + Gemma3 dual rope theta | ||||
| 626 | + forge::ModelConfig sw; | ||||
| 627 | + sw.n_layers = 3; sw.d_model = 128; sw.n_heads = 2; sw.n_kv_heads = 1; | ||||
| 628 | + sw.d_ff = 96; sw.vocab_size = 11; sw.context_length = 8; | ||||
| 629 | + sw.tied_embeddings = true; | ||||
| 630 | + sw.sliding_window = 3; sw.sliding_global_every = 2; | ||||
| 631 | + sw.rope_theta_global = 1e6f; | ||||
| 632 | + test_backend_parity_model("sliding-window 3 + global-every-2 [hd64]", sw); | ||||
| 633 | + | ||||
| 634 | + // Wave-2: attention softcap (Gemma2) — unfused path on both backends | ||||
| 635 | + forge::ModelConfig sc; | ||||
| 636 | + sc.n_layers = 2; sc.d_model = 16; sc.n_heads = 2; sc.n_kv_heads = 1; | ||||
| 637 | + sc.d_ff = 24; sc.vocab_size = 11; sc.context_length = 8; | ||||
| 638 | + sc.tied_embeddings = true; | ||||
| 639 | + sc.attn_softcap = 20.0f; | ||||
| 640 | + test_backend_parity_model("attn softcap 20 (Gemma2)", sc); | ||||
| 641 | + | ||||
| 642 | + // Sliding window through the UNFUSED kernel too (head_dim 8) | ||||
| 643 | + forge::ModelConfig swu; | ||||
| 644 | + swu.n_layers = 2; swu.d_model = 16; swu.n_heads = 2; swu.n_kv_heads = 2; | ||||
| 645 | + swu.d_ff = 24; swu.vocab_size = 11; swu.context_length = 8; | ||||
| 646 | + swu.tied_embeddings = true; | ||||
| 647 | + swu.sliding_window = 3; swu.attn_softcap = 20.0f; | ||||
| 648 | + test_backend_parity_model("sliding-window 3 + softcap [unfused hd8]", swu); | ||||
| 603 | } | 649 | } | ||
| 604 | test_metal_moe_quant_ops(); | 650 | test_metal_moe_quant_ops(); | ||
| 605 | test_flash_attention(); | 651 | test_flash_attention(); | ||
| 606 | 652 | ||||