Spaces:
Paused
Paused
Download src/engine/kernels.rs from deepondas/laya-snake: direct link, hf CLI and curl.
- Browser
- Download file 30 kB
-
https://huggingface.co/spaces/deepondas/laya-snake/resolve/main/src/engine/kernels.rs
- Command line
-
hf download hf://spaces/deepondas/laya-snake/src/engine/kernels.rs
-
curl -L -o kernels.rs https://huggingface.co/spaces/deepondas/laya-snake/resolve/main/src/engine/kernels.rs
30 kB
| //! CPU kernels for the Laya engine (x86-64: AVX2 + FMA + F16C). | |
| //! | |
| //! Every public function here requires [`supported`] to be true; they assert it, so the safe API | |
| //! cannot reach an unsupported instruction. Weights stay in f16 (as stored in the checkpoint) and | |
| //! are widened to f32 inside the matmul, so the arithmetic is the same f32 arithmetic the reference | |
| //! implementation performs on its up-cast copy, with only the summation order differing. | |
| //! | |
| //! All parallel kernels run on the *current* rayon pool (the engine wraps each forward in | |
| //! `ThreadPool::install`). | |
| use rayon::prelude::*; | |
| use std::arch::x86_64::*; | |
| /// True when the CPU has everything the kernels below need. | |
| pub fn supported() -> bool { | |
| is_x86_feature_detected!("avx2") | |
| && is_x86_feature_detected!("fma") | |
| && is_x86_feature_detected!("f16c") | |
| } | |
| fn check() { | |
| assert!(supported(), "AVX2/FMA/F16C required"); | |
| } | |
| /// A raw pointer that may be shared across rayon tasks. Each task writes a disjoint region. | |
| struct SendPtr<T>(*mut T); | |
| unsafe impl<T> Send for SendPtr<T> {} | |
| unsafe impl<T> Sync for SendPtr<T> {} | |
| // --------------------------------------------------------------------------------------------- | |
| // f16 | |
| // --------------------------------------------------------------------------------------------- | |
| /// IEEE binary16 -> binary32 (exact). | |
| pub fn f16_to_f32(h: u16) -> f32 { | |
| let sign = ((h >> 15) as u32) << 31; | |
| let exp = ((h >> 10) & 0x1f) as u32; | |
| let man = (h & 0x3ff) as u32; | |
| let bits = match (exp, man) { | |
| (0, 0) => sign, | |
| (0, m) => { | |
| // subnormal: renormalise | |
| let shift = m.leading_zeros() - 21; // bring the top set bit to bit 10 | |
| let m = (m << shift) & 0x3ff; | |
| let e = 127 - 15 + 1 - shift; | |
| sign | (e << 23) | (m << 13) | |
| } | |
| (0x1f, 0) => sign | 0x7f80_0000, | |
| (0x1f, m) => sign | 0x7f80_0000 | (m << 13), | |
| (e, m) => sign | ((e + 127 - 15) << 23) | (m << 13), | |
| }; | |
| f32::from_bits(bits) | |
| } | |
| pub fn f16_slice_to_f32(src: &[u16]) -> Vec<f32> { | |
| src.iter().map(|&h| f16_to_f32(h)).collect() | |
| } | |
| /// Vectorised f16 -> f32 for whole rows (`len % 8 == 0`). | |
| pub fn widen_f16(src: &[u16], dst: &mut [f32]) { | |
| check(); | |
| assert!(src.len() == dst.len() && src.len() % 8 == 0); | |
| unsafe { widen_f16_inner(src, dst) } | |
| } | |
| unsafe fn widen_f16_inner(src: &[u16], dst: &mut [f32]) { | |
| let mut i = 0; | |
| while i < src.len() { | |
| let v = _mm256_cvtph_ps(_mm_loadu_si128(src.as_ptr().add(i) as *const __m128i)); | |
| _mm256_storeu_ps(dst.as_mut_ptr().add(i), v); | |
| i += 8; | |
| } | |
| } | |
| // --------------------------------------------------------------------------------------------- | |
| // small helpers | |
| // --------------------------------------------------------------------------------------------- | |
| unsafe fn hsum(v: __m256) -> f32 { | |
| let s = _mm_add_ps(_mm256_castps256_ps128(v), _mm256_extractf128_ps::<1>(v)); | |
| let s = _mm_add_ps(s, _mm_movehl_ps(s, s)); | |
| let s = _mm_add_ss(s, _mm_shuffle_ps::<1>(s, s)); | |
| _mm_cvtss_f32(s) | |
| } | |
| /// Vector `exp` (Cephes polynomial, relative error ~1e-7 on the softmax range). | |
| unsafe fn exp256(x: __m256) -> __m256 { | |
| let x = _mm256_min_ps(x, _mm256_set1_ps(88.376_26)); | |
| let x = _mm256_max_ps(x, _mm256_set1_ps(-88.376_26)); | |
| let fx = _mm256_round_ps::<{ _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC }>(_mm256_mul_ps( | |
| x, | |
| _mm256_set1_ps(std::f32::consts::LOG2_E), | |
| )); | |
| let r = _mm256_fnmadd_ps(fx, _mm256_set1_ps(0.693_359_375), x); | |
| let r = _mm256_fnmadd_ps(fx, _mm256_set1_ps(-2.121_944_4e-4), r); | |
| let z = _mm256_mul_ps(r, r); | |
| let mut y = _mm256_set1_ps(1.987_569_15e-4); | |
| y = _mm256_fmadd_ps(y, r, _mm256_set1_ps(1.398_199_950_7e-3)); | |
| y = _mm256_fmadd_ps(y, r, _mm256_set1_ps(8.333_451_907_3e-3)); | |
| y = _mm256_fmadd_ps(y, r, _mm256_set1_ps(4.166_579_589_4e-2)); | |
| y = _mm256_fmadd_ps(y, r, _mm256_set1_ps(1.666_666_545_9e-1)); | |
| y = _mm256_fmadd_ps(y, r, _mm256_set1_ps(5.000_000_120_1e-1)); | |
| y = _mm256_fmadd_ps(y, z, r); | |
| y = _mm256_add_ps(y, _mm256_set1_ps(1.0)); | |
| let n = _mm256_cvtps_epi32(fx); | |
| let pow2n = _mm256_castsi256_ps(_mm256_slli_epi32::<23>(_mm256_add_epi32( | |
| n, | |
| _mm256_set1_epi32(127), | |
| ))); | |
| _mm256_mul_ps(y, pow2n) | |
| } | |
| // --------------------------------------------------------------------------------------------- | |
| // matmul: out[m, n] = sum_k a[m, k] * w[n, k] (+ bias[n]) (a, out f32; w f16, row-major [n, k]) | |
| // --------------------------------------------------------------------------------------------- | |
| /// Rows of `a` handled per micro-kernel call. 6x2 accumulators + 2 weight vectors + 1 activation | |
| /// vector = 15 of the 16 ymm registers. | |
| const MR: usize = 6; | |
| unsafe fn tile<const R: usize>( | |
| a: *const f32, | |
| lda: usize, | |
| w0: *const u16, | |
| w1: *const u16, | |
| k: usize, | |
| out: *mut f32, | |
| ldo: usize, | |
| bias: [f32; 2], | |
| ) { | |
| let mut c0 = [_mm256_setzero_ps(); R]; | |
| let mut c1 = [_mm256_setzero_ps(); R]; | |
| let mut p = 0; | |
| while p < k { | |
| let b0 = _mm256_cvtph_ps(_mm_loadu_si128(w0.add(p) as *const __m128i)); | |
| let b1 = _mm256_cvtph_ps(_mm_loadu_si128(w1.add(p) as *const __m128i)); | |
| for i in 0..R { | |
| let av = _mm256_loadu_ps(a.add(i * lda + p)); | |
| c0[i] = _mm256_fmadd_ps(av, b0, c0[i]); | |
| c1[i] = _mm256_fmadd_ps(av, b1, c1[i]); | |
| } | |
| p += 8; | |
| } | |
| for i in 0..R { | |
| *out.add(i * ldo) = hsum(c0[i]) + bias[0]; | |
| *out.add(i * ldo + 1) = hsum(c1[i]) + bias[1]; | |
| } | |
| } | |
| unsafe fn tile_dispatch( | |
| rows: usize, | |
| a: *const f32, | |
| lda: usize, | |
| w0: *const u16, | |
| w1: *const u16, | |
| k: usize, | |
| out: *mut f32, | |
| ldo: usize, | |
| bias: [f32; 2], | |
| ) { | |
| match rows { | |
| 1 => tile::<1>(a, lda, w0, w1, k, out, ldo, bias), | |
| 2 => tile::<2>(a, lda, w0, w1, k, out, ldo, bias), | |
| 3 => tile::<3>(a, lda, w0, w1, k, out, ldo, bias), | |
| 4 => tile::<4>(a, lda, w0, w1, k, out, ldo, bias), | |
| 5 => tile::<5>(a, lda, w0, w1, k, out, ldo, bias), | |
| _ => tile::<6>(a, lda, w0, w1, k, out, ldo, bias), | |
| } | |
| } | |
| /// One column block `[n0, n1)` of the output, for all `m` rows. | |
| unsafe fn block( | |
| a: *const f32, | |
| m: usize, | |
| k: usize, | |
| w: *const u16, | |
| n0: usize, | |
| n1: usize, | |
| bias: Option<&[f32]>, | |
| out: *mut f32, | |
| n: usize, | |
| ) { | |
| let mut i = 0; | |
| while i < m { | |
| let rows = MR.min(m - i); | |
| let mut j = n0; | |
| while j < n1 { | |
| let b = match bias { | |
| Some(b) => [b[j], b[j + 1]], | |
| None => [0.0, 0.0], | |
| }; | |
| tile_dispatch( | |
| rows, | |
| a.add(i * k), | |
| k, | |
| w.add(j * k), | |
| w.add((j + 1) * k), | |
| k, | |
| out.add(i * n + j), | |
| n, | |
| b, | |
| ); | |
| j += 2; | |
| } | |
| i += rows; | |
| } | |
| } | |
| /// `out[m, n] = a[m, k] * w[n, k]^T (+ bias)`. `n` must be even and `k` a multiple of 8. | |
| pub fn linear( | |
| a: &[f32], | |
| m: usize, | |
| k: usize, | |
| w: &[u16], | |
| n: usize, | |
| bias: Option<&[f32]>, | |
| out: &mut [f32], | |
| ) { | |
| check(); | |
| assert!(k % 8 == 0 && n % 2 == 0, "linear: k%8 and n%2 must be 0"); | |
| assert!(a.len() >= m * k && w.len() >= n * k && out.len() >= m * n); | |
| if let Some(b) = bias { | |
| assert!(b.len() >= n); | |
| } | |
| // Column blocks small enough for good load balance across heterogeneous cores, large enough | |
| // that a block's weight rows are re-used from L2 by every row tile. | |
| let threads = rayon::current_num_threads().max(1); | |
| let nb = ((n / (threads * 4)).clamp(8, 64)) & !1; | |
| let nblocks = n.div_ceil(nb); | |
| let ap = SendPtr(a.as_ptr() as *mut f32); | |
| let wp = SendPtr(w.as_ptr() as *mut u16); | |
| let op = SendPtr(out.as_mut_ptr()); | |
| (0..nblocks).into_par_iter().with_min_len(1).for_each(|bi| { | |
| let (ap, wp, op) = (ap, wp, op); | |
| let n0 = bi * nb; | |
| let n1 = (n0 + nb).min(n); | |
| // SAFETY: feature support asserted above; blocks write disjoint columns of `out`. | |
| unsafe { block(ap.0, m, k, wp.0, n0, n1, bias, op.0, n) } | |
| }); | |
| } | |
| /// Single dot product of an f32 vector with an f16 row (`len % 8 == 0`). | |
| pub fn dot_f16(a: &[f32], w: &[u16]) -> f32 { | |
| check(); | |
| assert!(a.len() == w.len() && a.len() % 8 == 0); | |
| unsafe { dot_f16_inner(a, w) } | |
| } | |
| unsafe fn dot_f16_inner(a: &[f32], w: &[u16]) -> f32 { | |
| let mut acc = _mm256_setzero_ps(); | |
| let mut p = 0; | |
| while p < a.len() { | |
| let b = _mm256_cvtph_ps(_mm_loadu_si128(w.as_ptr().add(p) as *const __m128i)); | |
| acc = _mm256_fmadd_ps(_mm256_loadu_ps(a.as_ptr().add(p)), b, acc); | |
| p += 8; | |
| } | |
| hsum(acc) | |
| } | |
| // --------------------------------------------------------------------------------------------- | |
| // layer norm | |
| // --------------------------------------------------------------------------------------------- | |
| /// Row-wise `(x - mean) / sqrt(var + eps) * weight (+ bias)`, matching candle's formulation | |
| /// (biased variance, division rather than reciprocal multiply). | |
| pub fn layer_norm( | |
| x: &[f32], | |
| rows: usize, | |
| d: usize, | |
| weight: &[f32], | |
| bias: Option<&[f32]>, | |
| eps: f32, | |
| out: &mut [f32], | |
| ) { | |
| check(); | |
| assert!(d % 8 == 0 && x.len() >= rows * d && out.len() >= rows * d && weight.len() >= d); | |
| if let Some(b) = bias { | |
| assert!(b.len() >= d); | |
| } | |
| for r in 0..rows { | |
| unsafe { | |
| layer_norm_row( | |
| &x[r * d..(r + 1) * d], | |
| weight, | |
| bias, | |
| eps, | |
| &mut out[r * d..(r + 1) * d], | |
| ) | |
| } | |
| } | |
| } | |
| unsafe fn layer_norm_row( | |
| x: &[f32], | |
| weight: &[f32], | |
| bias: Option<&[f32]>, | |
| eps: f32, | |
| out: &mut [f32], | |
| ) { | |
| let d = x.len(); | |
| let xp = x.as_ptr(); | |
| let mut s = _mm256_setzero_ps(); | |
| let mut i = 0; | |
| while i < d { | |
| s = _mm256_add_ps(s, _mm256_loadu_ps(xp.add(i))); | |
| i += 8; | |
| } | |
| let mean = _mm256_set1_ps(hsum(s) / d as f32); | |
| let mut v = _mm256_setzero_ps(); | |
| i = 0; | |
| while i < d { | |
| let c = _mm256_sub_ps(_mm256_loadu_ps(xp.add(i)), mean); | |
| v = _mm256_fmadd_ps(c, c, v); | |
| i += 8; | |
| } | |
| let denom = _mm256_set1_ps((hsum(v) / d as f32 + eps).sqrt()); | |
| let op = out.as_mut_ptr(); | |
| i = 0; | |
| while i < d { | |
| let c = _mm256_sub_ps(_mm256_loadu_ps(xp.add(i)), mean); | |
| let mut y = _mm256_mul_ps( | |
| _mm256_div_ps(c, denom), | |
| _mm256_loadu_ps(weight.as_ptr().add(i)), | |
| ); | |
| if let Some(b) = bias { | |
| y = _mm256_add_ps(y, _mm256_loadu_ps(b.as_ptr().add(i))); | |
| } | |
| _mm256_storeu_ps(op.add(i), y); | |
| i += 8; | |
| } | |
| } | |
| // --------------------------------------------------------------------------------------------- | |
| // element-wise | |
| // --------------------------------------------------------------------------------------------- | |
| /// `dst += src`. | |
| pub fn add_assign(dst: &mut [f32], src: &[f32]) { | |
| assert_eq!(dst.len(), src.len()); | |
| for (d, s) in dst.iter_mut().zip(src) { | |
| *d += *s; | |
| } | |
| } | |
| /// `x[r, :] += row` for every row. | |
| pub fn add_row_broadcast(x: &mut [f32], d: usize, row: &[f32]) { | |
| assert_eq!(row.len(), d); | |
| for r in x.chunks_exact_mut(d) { | |
| for (v, b) in r.iter_mut().zip(row) { | |
| *v += *b; | |
| } | |
| } | |
| } | |
| /// `x = max(x, 0)`. | |
| pub fn relu(x: &mut [f32]) { | |
| for v in x { | |
| *v = v.max(0.0); | |
| } | |
| } | |
| /// candle's `gelu_erf`: `(erf(v / sqrt 2) + 1) * 0.5 * v`, using the same `libm::erff`. | |
| pub fn gelu_erf(v: f32) -> f32 { | |
| (libm::erff(v * std::f32::consts::FRAC_1_SQRT_2) + 1.0) * 0.5 * v | |
| } | |
| pub fn gelu_erf_inplace(x: &mut [f32]) { | |
| for v in x { | |
| *v = gelu_erf(*v); | |
| } | |
| } | |
| /// ModernBERT GeGLU: `wi` is `[rows, 2*inter]`; `out[r, j] = gelu(wi[r, j]) * wi[r, inter + j]`. | |
| pub fn geglu(wi: &[f32], rows: usize, inter: usize, out: &mut [f32]) { | |
| assert!(wi.len() >= rows * 2 * inter && out.len() >= rows * inter); | |
| out[..rows * inter] | |
| .par_chunks_mut(inter) | |
| .zip(wi.par_chunks(2 * inter)) | |
| .for_each(|(o, r)| { | |
| let (a, g) = r.split_at(inter); | |
| for j in 0..inter { | |
| o[j] = gelu_erf(a[j]) * g[j]; | |
| } | |
| }); | |
| } | |
| // --------------------------------------------------------------------------------------------- | |
| // attention | |
| // --------------------------------------------------------------------------------------------- | |
| pub const HEAD_DIM: usize = 64; | |
| /// Split a fused `[rows, 3*d]` projection into head-major `q`, `k`, `v` (`[nh, rows, 64]`), applying | |
| /// rotate-half RoPE to q and k when `rope` is given (tables `[>=rows, 32]`) and `scale` to q. | |
| /// Order of operations (rope, then scale) follows the reference. | |
| pub fn split_qkv( | |
| qkv: &[f32], | |
| rows: usize, | |
| nh: usize, | |
| rope: Option<(&[f32], &[f32])>, | |
| scale: f32, | |
| q: &mut [f32], | |
| k: &mut [f32], | |
| v: &mut [f32], | |
| ) { | |
| let d = nh * HEAD_DIM; | |
| let half = HEAD_DIM / 2; | |
| assert!(qkv.len() >= rows * 3 * d && q.len() >= nh * rows * HEAD_DIM); | |
| q[..nh * rows * HEAD_DIM] | |
| .par_chunks_mut(rows * HEAD_DIM) | |
| .zip(k[..nh * rows * HEAD_DIM].par_chunks_mut(rows * HEAD_DIM)) | |
| .zip(v[..nh * rows * HEAD_DIM].par_chunks_mut(rows * HEAD_DIM)) | |
| .enumerate() | |
| .for_each(|(h, ((qh, kh), vh))| { | |
| for i in 0..rows { | |
| let base = i * 3 * d + h * HEAD_DIM; | |
| let (qs, ks, vs) = ( | |
| &qkv[base..base + HEAD_DIM], | |
| &qkv[base + d..base + d + HEAD_DIM], | |
| &qkv[base + 2 * d..base + 2 * d + HEAD_DIM], | |
| ); | |
| let o = i * HEAD_DIM; | |
| vh[o..o + HEAD_DIM].copy_from_slice(vs); | |
| match rope { | |
| Some((cos, sin)) => { | |
| let (c, s) = ( | |
| &cos[i * half..(i + 1) * half], | |
| &sin[i * half..(i + 1) * half], | |
| ); | |
| for j in 0..half { | |
| qh[o + j] = (qs[j] * c[j] - qs[j + half] * s[j]) * scale; | |
| qh[o + j + half] = (qs[j] * s[j] + qs[j + half] * c[j]) * scale; | |
| kh[o + j] = ks[j] * c[j] - ks[j + half] * s[j]; | |
| kh[o + j + half] = ks[j] * s[j] + ks[j + half] * c[j]; | |
| } | |
| } | |
| None => { | |
| for j in 0..HEAD_DIM { | |
| qh[o + j] = qs[j] * scale; | |
| } | |
| kh[o..o + HEAD_DIM].copy_from_slice(ks); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| /// Copy `rows` rows of a `[rows, stride]` matrix (columns `col..col + nh*64`) into head-major | |
| /// `[nh, rows, 64]`, multiplying by `scale`. | |
| pub fn to_head_major( | |
| src: &[f32], | |
| stride: usize, | |
| col: usize, | |
| rows: usize, | |
| nh: usize, | |
| scale: f32, | |
| dst: &mut [f32], | |
| ) { | |
| assert!( | |
| src.len() >= rows * stride | |
| && dst.len() >= nh * rows * HEAD_DIM | |
| && col + nh * HEAD_DIM <= stride | |
| ); | |
| dst[..nh * rows * HEAD_DIM] | |
| .par_chunks_mut(rows * HEAD_DIM) | |
| .enumerate() | |
| .for_each(|(h, dh)| { | |
| for i in 0..rows { | |
| let s = &src[i * stride + col + h * HEAD_DIM..][..HEAD_DIM]; | |
| let o = &mut dh[i * HEAD_DIM..][..HEAD_DIM]; | |
| for c in 0..HEAD_DIM { | |
| o[c] = s[c] * scale; | |
| } | |
| } | |
| }); | |
| } | |
| /// One query row against keys `[j0, j1)`: softmax(q.k) then the weighted sum of v, into `out[0..64]`. | |
| unsafe fn attend_row( | |
| q: *const f32, | |
| k: *const f32, | |
| v: *const f32, | |
| j0: usize, | |
| j1: usize, | |
| scores: &mut [f32], | |
| out: *mut f32, | |
| ) { | |
| let q0 = _mm256_loadu_ps(q); | |
| let q1 = _mm256_loadu_ps(q.add(8)); | |
| let q2 = _mm256_loadu_ps(q.add(16)); | |
| let q3 = _mm256_loadu_ps(q.add(24)); | |
| let q4 = _mm256_loadu_ps(q.add(32)); | |
| let q5 = _mm256_loadu_ps(q.add(40)); | |
| let q6 = _mm256_loadu_ps(q.add(48)); | |
| let q7 = _mm256_loadu_ps(q.add(56)); | |
| let cnt = j1 - j0; | |
| let mut mx = f32::NEG_INFINITY; | |
| for t in 0..cnt { | |
| let kp = k.add((j0 + t) * HEAD_DIM); | |
| let mut a0 = _mm256_mul_ps(q0, _mm256_loadu_ps(kp)); | |
| let mut a1 = _mm256_mul_ps(q1, _mm256_loadu_ps(kp.add(8))); | |
| let mut a2 = _mm256_mul_ps(q2, _mm256_loadu_ps(kp.add(16))); | |
| let mut a3 = _mm256_mul_ps(q3, _mm256_loadu_ps(kp.add(24))); | |
| a0 = _mm256_fmadd_ps(q4, _mm256_loadu_ps(kp.add(32)), a0); | |
| a1 = _mm256_fmadd_ps(q5, _mm256_loadu_ps(kp.add(40)), a1); | |
| a2 = _mm256_fmadd_ps(q6, _mm256_loadu_ps(kp.add(48)), a2); | |
| a3 = _mm256_fmadd_ps(q7, _mm256_loadu_ps(kp.add(56)), a3); | |
| let s = hsum(_mm256_add_ps(_mm256_add_ps(a0, a1), _mm256_add_ps(a2, a3))); | |
| *scores.get_unchecked_mut(t) = s; | |
| mx = mx.max(s); | |
| } | |
| // exp(s - max), vectorised with a scalar tail | |
| let mxv = _mm256_set1_ps(mx); | |
| let sp = scores.as_mut_ptr(); | |
| let mut sum = _mm256_setzero_ps(); | |
| let mut t = 0; | |
| while t + 8 <= cnt { | |
| let e = exp256(_mm256_sub_ps(_mm256_loadu_ps(sp.add(t)), mxv)); | |
| _mm256_storeu_ps(sp.add(t), e); | |
| sum = _mm256_add_ps(sum, e); | |
| t += 8; | |
| } | |
| let mut total = hsum(sum); | |
| while t < cnt { | |
| let e = (*sp.add(t) - mx).exp(); | |
| *sp.add(t) = e; | |
| total += e; | |
| t += 1; | |
| } | |
| let mut o0 = _mm256_setzero_ps(); | |
| let mut o1 = _mm256_setzero_ps(); | |
| let mut o2 = _mm256_setzero_ps(); | |
| let mut o3 = _mm256_setzero_ps(); | |
| let mut o4 = _mm256_setzero_ps(); | |
| let mut o5 = _mm256_setzero_ps(); | |
| let mut o6 = _mm256_setzero_ps(); | |
| let mut o7 = _mm256_setzero_ps(); | |
| for t in 0..cnt { | |
| let p = _mm256_set1_ps(*sp.add(t)); | |
| let vp = v.add((j0 + t) * HEAD_DIM); | |
| o0 = _mm256_fmadd_ps(p, _mm256_loadu_ps(vp), o0); | |
| o1 = _mm256_fmadd_ps(p, _mm256_loadu_ps(vp.add(8)), o1); | |
| o2 = _mm256_fmadd_ps(p, _mm256_loadu_ps(vp.add(16)), o2); | |
| o3 = _mm256_fmadd_ps(p, _mm256_loadu_ps(vp.add(24)), o3); | |
| o4 = _mm256_fmadd_ps(p, _mm256_loadu_ps(vp.add(32)), o4); | |
| o5 = _mm256_fmadd_ps(p, _mm256_loadu_ps(vp.add(40)), o5); | |
| o6 = _mm256_fmadd_ps(p, _mm256_loadu_ps(vp.add(48)), o6); | |
| o7 = _mm256_fmadd_ps(p, _mm256_loadu_ps(vp.add(56)), o7); | |
| } | |
| let inv = _mm256_set1_ps(1.0 / total); | |
| _mm256_storeu_ps(out, _mm256_mul_ps(o0, inv)); | |
| _mm256_storeu_ps(out.add(8), _mm256_mul_ps(o1, inv)); | |
| _mm256_storeu_ps(out.add(16), _mm256_mul_ps(o2, inv)); | |
| _mm256_storeu_ps(out.add(24), _mm256_mul_ps(o3, inv)); | |
| _mm256_storeu_ps(out.add(32), _mm256_mul_ps(o4, inv)); | |
| _mm256_storeu_ps(out.add(40), _mm256_mul_ps(o5, inv)); | |
| _mm256_storeu_ps(out.add(48), _mm256_mul_ps(o6, inv)); | |
| _mm256_storeu_ps(out.add(56), _mm256_mul_ps(o7, inv)); | |
| } | |
| /// Fused multi-head attention over head-major `q`/`k`/`v` (`[nh, rows, 64]`). `window` is the | |
| /// one-sided reach of a sliding-window layer (`|i - j| <= window`); `None` is full attention. | |
| /// Writes `out` as `[rows, nh * 64]`. Keys outside the window contribute exactly zero in the | |
| /// reference (`-inf` before softmax), so skipping them is exact. | |
| pub fn attention( | |
| q: &[f32], | |
| k: &[f32], | |
| v: &[f32], | |
| rows: usize, | |
| nh: usize, | |
| window: Option<usize>, | |
| out: &mut [f32], | |
| ) { | |
| attention_rows(q, rows, k, v, rows, nh, window, out) | |
| } | |
| /// [`attention`] with `nq` query rows against `rows` keys. A sliding window is only meaningful when | |
| /// queries and keys are the same positions (`nq == rows`). | |
| pub fn attention_rows( | |
| q: &[f32], | |
| nq: usize, | |
| k: &[f32], | |
| v: &[f32], | |
| rows: usize, | |
| nh: usize, | |
| window: Option<usize>, | |
| out: &mut [f32], | |
| ) { | |
| check(); | |
| let d = nh * HEAD_DIM; | |
| assert!( | |
| window.is_none() || nq == rows, | |
| "windowed attention needs nq == rows" | |
| ); | |
| assert!(rows <= 512, "sequence longer than the score buffer"); | |
| assert!( | |
| q.len() >= nh * nq * HEAD_DIM | |
| && k.len() >= nh * rows * HEAD_DIM | |
| && v.len() >= nh * rows * HEAD_DIM | |
| && out.len() >= nq * d | |
| ); | |
| let op = SendPtr(out.as_mut_ptr()); | |
| let chunk = 8usize; | |
| let per_head = nq.div_ceil(chunk); | |
| let tasks = nh * per_head; | |
| (0..tasks).into_par_iter().with_min_len(1).for_each(|t| { | |
| // Rebind so the closure captures the whole `Sync` wrapper, not its raw-pointer field. | |
| let op = op; | |
| let (h, c) = (t / per_head, t % per_head); | |
| let (qbase, kbase) = (h * nq * HEAD_DIM, h * rows * HEAD_DIM); | |
| let mut scores = [0f32; 512]; | |
| let scores = &mut scores[..rows]; | |
| for i in c * chunk..((c + 1) * chunk).min(nq) { | |
| let (j0, j1) = match window { | |
| Some(w) => (i.saturating_sub(w), (i + w + 1).min(rows)), | |
| None => (0, rows), | |
| }; | |
| // SAFETY: feature support asserted above; each (head, row) owns a disjoint 64-wide slice. | |
| unsafe { | |
| attend_row( | |
| q.as_ptr().add(qbase + i * HEAD_DIM), | |
| k.as_ptr().add(kbase), | |
| v.as_ptr().add(kbase), | |
| j0, | |
| j1, | |
| scores, | |
| op.0.add(i * d + h * HEAD_DIM), | |
| ); | |
| } | |
| } | |
| }); | |
| } | |
| mod tests { | |
| use super::*; | |
| struct Rng(u64); | |
| impl Rng { | |
| fn next(&mut self) -> f32 { | |
| self.0 = self | |
| .0 | |
| .wrapping_mul(6364136223846793005) | |
| .wrapping_add(1442695040888963407); | |
| ((self.0 >> 40) as f32 / (1u64 << 24) as f32) * 2.0 - 1.0 | |
| } | |
| } | |
| fn f32_to_f16(v: f32) -> u16 { | |
| // round-to-nearest-even via the F16C instruction under test-only use | |
| unsafe { _mm_extract_epi16::<0>(_mm256_cvtps_ph::<0>(_mm256_set1_ps(v))) as u16 } | |
| } | |
| fn f16_conversion_matches_hardware() { | |
| assert!(supported()); | |
| for h in (0u32..=0xffff).step_by(1) { | |
| let h = h as u16; | |
| let ours = f16_to_f32(h); | |
| let hw = unsafe { _mm_cvtss_f32(_mm_cvtph_ps(_mm_set1_epi16(h as i16))) }; | |
| assert!( | |
| ours.to_bits() == hw.to_bits() || (ours.is_nan() && hw.is_nan()), | |
| "h={h:#06x} {ours} vs {hw}" | |
| ); | |
| } | |
| } | |
| fn linear_matches_reference() { | |
| for &(m, k, n) in &[ | |
| (1usize, 8usize, 2usize), | |
| (7, 64, 10), | |
| (108, 1024, 96), | |
| (13, 2624, 34), | |
| (6, 1024, 4), | |
| ] { | |
| let mut r = Rng(m as u64 * 31 + k as u64); | |
| let a: Vec<f32> = (0..m * k).map(|_| r.next()).collect(); | |
| let w: Vec<u16> = (0..n * k).map(|_| f32_to_f16(r.next() * 0.1)).collect(); | |
| let bias: Vec<f32> = (0..n).map(|_| r.next()).collect(); | |
| let mut out = vec![0f32; m * n]; | |
| linear(&a, m, k, &w, n, Some(&bias), &mut out); | |
| for i in 0..m { | |
| for j in 0..n { | |
| let mut acc = 0f64; | |
| for p in 0..k { | |
| acc += a[i * k + p] as f64 * f16_to_f32(w[j * k + p]) as f64; | |
| } | |
| let want = acc + bias[j] as f64; | |
| let got = out[i * n + j] as f64; | |
| assert!( | |
| (got - want).abs() <= 1e-4 * (1.0 + want.abs()), | |
| "({m},{k},{n}) [{i},{j}] {got} vs {want}" | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| fn exp_accuracy() { | |
| let mut worst = 0f32; | |
| let mut x = -80.0f32; | |
| while x <= 0.0 { | |
| let got = unsafe { | |
| let mut o = [0f32; 8]; | |
| _mm256_storeu_ps(o.as_mut_ptr(), exp256(_mm256_set1_ps(x))); | |
| o[0] | |
| }; | |
| let want = x.exp(); | |
| if want > 1e-30 { | |
| worst = worst.max(((got - want) / want).abs()); | |
| } | |
| x += 0.0137; | |
| } | |
| assert!(worst < 5e-7, "worst relative error {worst}"); | |
| } | |
| fn layer_norm_matches_reference() { | |
| let (rows, d) = (5, 1024); | |
| let mut r = Rng(9); | |
| let x: Vec<f32> = (0..rows * d).map(|_| r.next() * 3.0 + 0.5).collect(); | |
| let w: Vec<f32> = (0..d).map(|_| r.next() + 1.0).collect(); | |
| let b: Vec<f32> = (0..d).map(|_| r.next()).collect(); | |
| for bias in [None, Some(&b[..])] { | |
| let mut out = vec![0f32; rows * d]; | |
| layer_norm(&x, rows, d, &w, bias, 1e-5, &mut out); | |
| for i in 0..rows { | |
| let row = &x[i * d..(i + 1) * d]; | |
| let mean = row.iter().map(|&v| v as f64).sum::<f64>() / d as f64; | |
| let var = row.iter().map(|&v| (v as f64 - mean).powi(2)).sum::<f64>() / d as f64; | |
| for j in 0..d { | |
| let mut want = (row[j] as f64 - mean) / (var + 1e-5).sqrt() * w[j] as f64; | |
| if let Some(b) = bias { | |
| want += b[j] as f64; | |
| } | |
| assert!( | |
| (out[i * d + j] as f64 - want).abs() < 2e-5, | |
| "row {i} col {j}" | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| fn attention_matches_reference() { | |
| let (rows, nh) = (37usize, 3usize); | |
| let mut r = Rng(5); | |
| let n = nh * rows * HEAD_DIM; | |
| let q: Vec<f32> = (0..n).map(|_| r.next()).collect(); | |
| let k: Vec<f32> = (0..n).map(|_| r.next()).collect(); | |
| let v: Vec<f32> = (0..n).map(|_| r.next()).collect(); | |
| for window in [None, Some(5usize), Some(64)] { | |
| let mut out = vec![0f32; rows * nh * HEAD_DIM]; | |
| attention(&q, &k, &v, rows, nh, window, &mut out); | |
| for h in 0..nh { | |
| for i in 0..rows { | |
| let js: Vec<usize> = (0..rows) | |
| .filter(|&j| { | |
| window | |
| .is_none_or(|w| (i as i64 - j as i64).unsigned_abs() as usize <= w) | |
| }) | |
| .collect(); | |
| let s: Vec<f64> = js | |
| .iter() | |
| .map(|&j| { | |
| (0..HEAD_DIM) | |
| .map(|c| { | |
| q[(h * rows + i) * HEAD_DIM + c] as f64 | |
| * k[(h * rows + j) * HEAD_DIM + c] as f64 | |
| }) | |
| .sum() | |
| }) | |
| .collect(); | |
| let mx = s.iter().cloned().fold(f64::MIN, f64::max); | |
| let e: Vec<f64> = s.iter().map(|x| (x - mx).exp()).collect(); | |
| let z: f64 = e.iter().sum(); | |
| for c in 0..HEAD_DIM { | |
| let want: f64 = js | |
| .iter() | |
| .zip(&e) | |
| .map(|(&j, &e)| e / z * v[(h * rows + j) * HEAD_DIM + c] as f64) | |
| .sum(); | |
| let got = out[i * nh * HEAD_DIM + h * HEAD_DIM + c] as f64; | |
| assert!( | |
| (got - want).abs() < 1e-5, | |
| "win {window:?} h{h} i{i} c{c}: {got} vs {want}" | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| fn rectangular_attention_matches_square_rows() { | |
| let (rows, nh) = (29usize, 2usize); | |
| let mut r = Rng(11); | |
| let n = nh * rows * HEAD_DIM; | |
| let q: Vec<f32> = (0..n).map(|_| r.next()).collect(); | |
| let k: Vec<f32> = (0..n).map(|_| r.next()).collect(); | |
| let v: Vec<f32> = (0..n).map(|_| r.next()).collect(); | |
| let mut full = vec![0f32; rows * nh * HEAD_DIM]; | |
| attention(&q, &k, &v, rows, nh, None, &mut full); | |
| let picks = [3usize, 17, 28]; | |
| let mut qs = vec![0f32; nh * picks.len() * HEAD_DIM]; | |
| for h in 0..nh { | |
| for (pi, &i) in picks.iter().enumerate() { | |
| qs[(h * picks.len() + pi) * HEAD_DIM..][..HEAD_DIM] | |
| .copy_from_slice(&q[(h * rows + i) * HEAD_DIM..][..HEAD_DIM]); | |
| } | |
| } | |
| let mut part = vec![0f32; picks.len() * nh * HEAD_DIM]; | |
| attention_rows(&qs, picks.len(), &k, &v, rows, nh, None, &mut part); | |
| for (pi, &i) in picks.iter().enumerate() { | |
| assert_eq!( | |
| &part[pi * nh * HEAD_DIM..(pi + 1) * nh * HEAD_DIM], | |
| &full[i * nh * HEAD_DIM..(i + 1) * nh * HEAD_DIM] | |
| ); | |
| } | |
| } | |
| fn geglu_matches_reference() { | |
| let (rows, inter) = (3, 40); | |
| let mut r = Rng(3); | |
| let wi: Vec<f32> = (0..rows * 2 * inter).map(|_| r.next() * 4.0).collect(); | |
| let mut out = vec![0f32; rows * inter]; | |
| geglu(&wi, rows, inter, &mut out); | |
| for i in 0..rows { | |
| for j in 0..inter { | |
| let a = wi[i * 2 * inter + j]; | |
| let g = wi[i * 2 * inter + inter + j]; | |
| assert_eq!(out[i * inter + j], gelu_erf(a) * g); | |
| } | |
| } | |
| } | |
| } | |