Skip to main content

ferric_tensor/
nn.rs

1//! Transformer building blocks expressed ON the general tensor runtime. The point of unification:
2//! attention is not a bespoke kernel — it's `reshape → batched matmul → softmax → matmul`, i.e. the
3//! general ops. RMSNorm/softmax/RoPE are fused fast-paths (methods on `Tensor`) but produce exactly
4//! what composing primitives would. One substrate; the model is an expression in it.
5
6use crate::Tensor;
7
8/// Linear y = x·W in the [in,out] weight convention (no bias) — just a matmul.
9pub fn linear(x: &Tensor, w: &Tensor) -> Tensor { x.matmul(w) }
10
11/// Linear in the HF convention: W is stored [out, in]; y = x·Wᵀ (direct, no transpose materialized).
12pub fn linear_hf(x: &Tensor, w: &Tensor) -> Tensor { x.matmul_bt(w) }
13
14/// Weight-quantized HF linear: W is a per-row int4/int8 [out,in]; y = x·Wᵀ, W dequantized on the fly.
15pub fn linear_hf_q(x: &Tensor, w: &crate::QRow) -> Tensor { x.matmul_qweight(w) }
16
17/// Causal multi-head attention with grouped-query attention, composed from general ops (+ fused
18/// softmax). q is [T, n_heads·dh]; k/v are [T, n_kv_heads·dh]. Returns [T, n_heads·dh].
19pub fn causal_attention(q: &Tensor, k: &Tensor, v: &Tensor, n_heads: usize, n_kv_heads: usize) -> Tensor {
20    let t = q.shape[0];
21    let d = q.shape[1];
22    let dh = d / n_heads;
23    let g = n_heads / n_kv_heads;
24    let scale = 1.0 / (dh as f32).sqrt();
25    let qh = q.reshape(&[t, n_heads, dh]).permute(&[1, 0, 2]).contiguous(); // [nh, T, dh]
26    // K/V: [T, nkv·dh] → [nkv, T, dh] → repeat each kv head g times → [nh, T, dh]
27    let kv_heads = |x: &Tensor| {
28        let hx = x.reshape(&[t, n_kv_heads, dh]).permute(&[1, 0, 2]).contiguous(); // [nkv, T, dh]
29        hx.reshape(&[n_kv_heads, 1, t, dh]).broadcast_to(&[n_kv_heads, g, t, dh]).reshape(&[n_heads, t, dh])
30    };
31    let (kh, vh) = (kv_heads(k), kv_heads(v));
32    let scores = qh.matmul(&kh.transpose(2, 1)).mul(&q.scalar(scale)); // [nh, T, T]
33    let probs = scores.add(&causal_mask(q, t)).softmax(2);             // masked softmax over keys
34    let ctx = probs.matmul(&vh);                                       // [nh, T, dh]
35    ctx.permute(&[1, 0, 2]).reshape(&[t, d])
36}
37
38/// Incremental-decode attention against a KV cache (one new query token vs all cached keys/values).
39/// q is [1, n_heads·dh]; k/v are the cache [S, n_kv_heads·dh]. No mask (cache precedes the query).
40/// Composed from general ops — the KV-cache decode path, no bespoke kernel.
41pub fn decode_attention(q: &Tensor, k: &Tensor, v: &Tensor, n_heads: usize, n_kv_heads: usize) -> Tensor {
42    let d = q.shape[1];
43    let dh = d / n_heads;
44    let s = k.shape[0];
45    let g = n_heads / n_kv_heads;
46    let scale = 1.0 / (dh as f32).sqrt();
47    let qh = q.reshape(&[1, n_heads, dh]).permute(&[1, 0, 2]).contiguous(); // [nh, 1, dh]
48    let kv_heads = |x: &Tensor| {
49        let hx = x.reshape(&[s, n_kv_heads, dh]).permute(&[1, 0, 2]).contiguous(); // [nkv, S, dh]
50        hx.reshape(&[n_kv_heads, 1, s, dh]).broadcast_to(&[n_kv_heads, g, s, dh]).reshape(&[n_heads, s, dh])
51    };
52    let (kh, vh) = (kv_heads(k), kv_heads(v)); // [nh, S, dh]
53    let probs = qh.matmul(&kh.transpose(2, 1)).mul(&q.scalar(scale)).softmax(2); // [nh, 1, S]
54    probs.matmul(&vh).permute(&[1, 0, 2]).reshape(&[1, d]) // [nh,1,dh] → [1,d]
55}
56
57/// Bidirectional (non-causal) multi-head attention — JEPA/ViT encoders. No causal mask; every
58/// position attends to all others. q [T, n_heads·dh]; k/v [T, n_kv_heads·dh]. Returns [T, n_heads·dh].
59pub fn bidirectional_attention(q: &Tensor, k: &Tensor, v: &Tensor, n_heads: usize, n_kv_heads: usize) -> Tensor {
60    let (t, d) = (q.shape[0], q.shape[1]);
61    let dh = d / n_heads;
62    let g = n_heads / n_kv_heads;
63    let scale = 1.0 / (dh as f32).sqrt();
64    let qh = q.reshape(&[t, n_heads, dh]).permute(&[1, 0, 2]).contiguous();
65    let kv_heads = |x: &Tensor| {
66        let hx = x.reshape(&[t, n_kv_heads, dh]).permute(&[1, 0, 2]).contiguous();
67        hx.reshape(&[n_kv_heads, 1, t, dh]).broadcast_to(&[n_kv_heads, g, t, dh]).reshape(&[n_heads, t, dh])
68    };
69    let (kh, vh) = (kv_heads(k), kv_heads(v));
70    let probs = qh.matmul(&kh.transpose(2, 1)).mul(&q.scalar(scale)).softmax(2); // no mask
71    probs.matmul(&vh).permute(&[1, 0, 2]).reshape(&[t, d])
72}
73
74/// Additive causal mask [T,T]: 0 on/below the diagonal, −∞ above (broadcasts over heads on add).
75fn causal_mask(like: &Tensor, t: usize) -> Tensor {
76    let mut m = vec![0.0f32; t * t];
77    for i in 0..t {
78        for j in (i + 1)..t {
79            m[i * t + j] = -1e30;
80        }
81    }
82    Tensor::from_vec(&like.ctx_arc(), &m, &[t, t])
83}