Skip to main content

ferric_tensor/
lib.rs

1//! Ferric L2 — a general N-dimensional tensor runtime on the GPU fabric.
2//!
3//! This is the substrate the whole ecosystem is meant to stand on: not fixed-shape, hand-fused
4//! kernels for one architecture, but a real tensor with **arbitrary rank, strided views, and
5//! broadcasting**, plus **general** elementwise ops, **general** reductions over any axes, and
6//! **batched** matmul. The transformer kernels in `ferric-core` become fused fast-paths of this.
7//!
8//! Design: eager execution, tensors are `Arc`-shared f32 buffers described by (shape, strides,
9//! offset). Views (`reshape`/`permute`/`transpose`/`broadcast_to`) are zero-copy stride tricks;
10//! `contiguous()` materializes. One general strided kernel powers elementwise + broadcasting; a
11//! segmented kernel powers reductions; a batched kernel powers matmul. Validated against a strided
12//! CPU reference on general shapes (broadcasting, non-contiguous inputs, arbitrary reduction axes).
13//!
14//! Next fabric layers (in progress): dtypes (f16/bf16/int), autograd tape for training, op fusion,
15//! and the heterogeneous scheduler.
16
17use ferric_core::Context;
18use std::sync::Arc;
19use wgpu::util::DeviceExt;
20
21pub mod autograd; // reverse-mode autodiff (training)
22pub mod cpu; // strided CPU reference (validation source of truth)
23pub use autograd::Var;
24
25/// A general N-D f32 tensor: an Arc-shared device buffer viewed through (shape, strides, offset).
26#[derive(Clone)]
27pub struct Tensor {
28    ctx: Arc<Context>,
29    buf: Arc<wgpu::Buffer>,
30    pub shape: Vec<usize>,
31    pub strides: Vec<usize>, // element strides (row-major by default)
32    offset: usize,
33}
34
35fn contig_strides(shape: &[usize]) -> Vec<usize> {
36    let mut s = vec![1usize; shape.len()];
37    for i in (0..shape.len().saturating_sub(1)).rev() {
38        s[i] = s[i + 1] * shape[i + 1];
39    }
40    s
41}
42fn numel(shape: &[usize]) -> usize { shape.iter().product() }
43
44/// Broadcast two shapes NumPy-style (right-aligned). Returns the result shape.
45fn broadcast_shapes(a: &[usize], b: &[usize]) -> Vec<usize> {
46    let r = a.len().max(b.len());
47    let mut out = vec![0usize; r];
48    for i in 0..r {
49        let da = if i + a.len() >= r { a[i + a.len() - r] } else { 1 };
50        let db = if i + b.len() >= r { b[i + b.len() - r] } else { 1 };
51        assert!(da == db || da == 1 || db == 1, "shapes {a:?} and {b:?} not broadcastable at dim {i}");
52        out[i] = da.max(db);
53    }
54    out
55}
56
57impl Tensor {
58    pub fn numel(&self) -> usize { numel(&self.shape) }
59    pub fn rank(&self) -> usize { self.shape.len() }
60    pub fn is_contiguous(&self) -> bool { self.strides == contig_strides(&self.shape) && self.offset == 0 }
61
62    // ---- construction / io ----
63    pub fn from_vec(ctx: &Arc<Context>, data: &[f32], shape: &[usize]) -> Tensor {
64        assert_eq!(data.len(), numel(shape), "data len != shape product");
65        let buf = ctx.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
66            label: Some("tensor"),
67            contents: bytemuck::cast_slice(data),
68            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
69        });
70        Tensor { ctx: ctx.clone(), buf: Arc::new(buf), shape: shape.to_vec(), strides: contig_strides(shape), offset: 0 }
71    }
72    pub fn zeros(ctx: &Arc<Context>, shape: &[usize]) -> Tensor { Self::from_vec(ctx, &vec![0.0; numel(shape)], shape) }
73
74    /// Materialize (contiguous) and read back to host in logical row-major order.
75    pub async fn to_vec(&self) -> Vec<f32> {
76        let c = self.contiguous();
77        readback(&c.ctx, &c.buf, c.numel()).await
78    }
79
80    // ---- zero-copy views ----
81    pub fn reshape(&self, shape: &[usize]) -> Tensor {
82        assert_eq!(numel(shape), self.numel(), "reshape changes numel");
83        let c = self.contiguous();
84        Tensor { ctx: c.ctx, buf: c.buf, strides: contig_strides(shape), shape: shape.to_vec(), offset: 0 }
85    }
86    pub fn permute(&self, perm: &[usize]) -> Tensor {
87        assert_eq!(perm.len(), self.rank(), "permute rank mismatch");
88        Tensor {
89            ctx: self.ctx.clone(), buf: self.buf.clone(), offset: self.offset,
90            shape: perm.iter().map(|&p| self.shape[p]).collect(),
91            strides: perm.iter().map(|&p| self.strides[p]).collect(),
92        }
93    }
94    pub fn transpose(&self, a: usize, b: usize) -> Tensor {
95        let mut p: Vec<usize> = (0..self.rank()).collect();
96        p.swap(a, b);
97        self.permute(&p)
98    }
99    /// Broadcast to a larger shape (right-aligned); broadcast dims get stride 0.
100    pub fn broadcast_to(&self, shape: &[usize]) -> Tensor {
101        let r = shape.len();
102        assert!(r >= self.rank(), "cannot broadcast to fewer dims");
103        let mut strides = vec![0usize; r];
104        for i in 0..self.rank() {
105            let (si, di) = (self.rank() - 1 - i, r - 1 - i);
106            if self.shape[si] == shape[di] {
107                strides[di] = self.strides[si];
108            } else {
109                assert_eq!(self.shape[si], 1, "cannot broadcast dim {} of {:?} to {:?}", si, self.shape, shape);
110                strides[di] = 0;
111            }
112        }
113        Tensor { ctx: self.ctx.clone(), buf: self.buf.clone(), shape: shape.to_vec(), strides, offset: self.offset }
114    }
115
116    /// Materialize a (possibly strided/broadcast) view into a fresh contiguous buffer.
117    pub fn contiguous(&self) -> Tensor {
118        if self.is_contiguous() {
119            return self.clone();
120        }
121        let n = self.numel();
122        let out = empty(&self.ctx, n);
123        // info: [rank, n, offset, shape..., strides...]
124        let mut info = vec![self.rank() as u32, n as u32, self.offset as u32];
125        info.extend(self.shape.iter().map(|&x| x as u32));
126        info.extend(self.strides.iter().map(|&x| x as u32));
127        run(&self.ctx, GATHER_WGSL, "gather", &[&self.buf, &out, &u32buf(&self.ctx, &info)], groups(n));
128        Tensor { ctx: self.ctx.clone(), buf: Arc::new(out), shape: self.shape.clone(), strides: contig_strides(&self.shape), offset: 0 }
129    }
130
131    // ---- general broadcasting elementwise ----
132    fn binary(&self, other: &Tensor, op: u32) -> Tensor {
133        let shape = broadcast_shapes(&self.shape, &other.shape);
134        let a = self.broadcast_to(&shape);
135        let b = other.broadcast_to(&shape);
136        let n = numel(&shape);
137        let out = empty(&self.ctx, n);
138        // info: [rank, op, n, offA, offB, shape..., aStr..., bStr...]
139        let mut info = vec![shape.len() as u32, op, n as u32, a.offset as u32, b.offset as u32];
140        info.extend(shape.iter().map(|&x| x as u32));
141        info.extend(a.strides.iter().map(|&x| x as u32));
142        info.extend(b.strides.iter().map(|&x| x as u32));
143        run(&self.ctx, BINARY_WGSL, "binary", &[&a.buf, &b.buf, &out, &u32buf(&self.ctx, &info)], groups(n));
144        Tensor { ctx: self.ctx.clone(), buf: Arc::new(out), shape: shape.clone(), strides: contig_strides(&shape), offset: 0 }
145    }
146    pub fn add(&self, o: &Tensor) -> Tensor { self.binary(o, 0) }
147    pub fn sub(&self, o: &Tensor) -> Tensor { self.binary(o, 1) }
148    pub fn mul(&self, o: &Tensor) -> Tensor { self.binary(o, 2) }
149    pub fn div(&self, o: &Tensor) -> Tensor { self.binary(o, 3) }
150    pub fn maximum(&self, o: &Tensor) -> Tensor { self.binary(o, 4) }
151
152    fn unary(&self, op: u32) -> Tensor {
153        let c = self.contiguous();
154        let n = c.numel();
155        let out = empty(&self.ctx, n);
156        run(&self.ctx, UNARY_WGSL, "unary", &[&c.buf, &out, &u32buf(&self.ctx, &[op, n as u32])], groups(n));
157        Tensor { ctx: self.ctx.clone(), buf: Arc::new(out), shape: c.shape, strides: c.strides, offset: 0 }
158    }
159    pub fn exp(&self) -> Tensor { self.unary(0) }
160    pub fn neg(&self) -> Tensor { self.unary(1) }
161    pub fn relu(&self) -> Tensor { self.unary(2) }
162    pub fn sqrt(&self) -> Tensor { self.unary(3) }
163    pub fn relu_mask(&self) -> Tensor { self.unary(4) } // 1 where x>0 else 0 (relu' )
164    pub(crate) fn ctx_arc(&self) -> Arc<Context> { self.ctx.clone() }
165
166    // ---- general reduction over arbitrary axes ----
167    fn reduce(&self, axes: &[usize], op: u32, keepdim: bool) -> Tensor {
168        let mut ax: Vec<usize> = axes.to_vec();
169        ax.sort_unstable();
170        ax.dedup();
171        let keep: Vec<usize> = (0..self.rank()).filter(|d| !ax.contains(d)).collect();
172        // permute reduced axes to the end, materialize → [outer, red]
173        let perm: Vec<usize> = keep.iter().chain(ax.iter()).copied().collect();
174        let moved = self.permute(&perm).contiguous();
175        let red: usize = ax.iter().map(|&d| self.shape[d]).product();
176        let outer: usize = moved.numel() / red.max(1);
177        let out = empty(&self.ctx, outer);
178        run(&self.ctx, REDUCE_WGSL, "reduce", &[&moved.buf, &out, &u32buf(&self.ctx, &[outer as u32, red as u32, op])], groups(outer));
179        let mut oshape: Vec<usize> = keep.iter().map(|&d| self.shape[d]).collect();
180        if keepdim {
181            oshape = (0..self.rank()).map(|d| if ax.contains(&d) { 1 } else { self.shape[d] }).collect();
182        }
183        if oshape.is_empty() { oshape.push(1); }
184        Tensor { ctx: self.ctx.clone(), buf: Arc::new(out), strides: contig_strides(&oshape), shape: oshape, offset: 0 }
185    }
186    pub fn sum(&self, axes: &[usize], keepdim: bool) -> Tensor { self.reduce(axes, 0, keepdim) }
187    pub fn max(&self, axes: &[usize], keepdim: bool) -> Tensor { self.reduce(axes, 1, keepdim) }
188    pub fn mean(&self, axes: &[usize], keepdim: bool) -> Tensor {
189        let n: usize = axes.iter().map(|&d| self.shape[d]).product();
190        let s = self.sum(axes, keepdim);
191        let inv = Tensor::from_vec(&self.ctx, &[1.0 / n as f32], &[1]);
192        s.mul(&inv)
193    }
194
195    // ---- batched matmul: [..., m, k] x [..., k, n] -> [..., m, n], batch dims broadcast ----
196    pub fn matmul(&self, other: &Tensor) -> Tensor {
197        let (ra, rb) = (self.rank(), other.rank());
198        assert!(ra >= 2 && rb >= 2, "matmul needs rank >= 2");
199        let (m, ka) = (self.shape[ra - 2], self.shape[ra - 1]);
200        let (kb, n) = (other.shape[rb - 2], other.shape[rb - 1]);
201        assert_eq!(ka, kb, "matmul inner dims {ka} != {kb}");
202        let batch_a = &self.shape[..ra - 2];
203        let batch_b = &other.shape[..rb - 2];
204        let batch = broadcast_shapes(batch_a, batch_b);
205        let bn: usize = numel(&batch);
206        let a_full: Vec<usize> = batch.iter().chain([m, ka].iter()).copied().collect();
207        let b_full: Vec<usize> = batch.iter().chain([kb, n].iter()).copied().collect();
208        let a = self.broadcast_to(&a_full).contiguous();
209        let b = other.broadcast_to(&b_full).contiguous();
210        let out = empty(&self.ctx, bn * m * n);
211        run(&self.ctx, MATMUL_WGSL, "bmm", &[&a.buf, &b.buf, &out, &u32buf(&self.ctx, &[bn as u32, m as u32, ka as u32, n as u32])], groups(bn * m * n));
212        let oshape: Vec<usize> = batch.iter().chain([m, n].iter()).copied().collect();
213        Tensor { ctx: self.ctx.clone(), buf: Arc::new(out), strides: contig_strides(&oshape), shape: oshape, offset: 0 }
214    }
215}
216
217// ---------- device plumbing (uses ferric-core Context's public device/queue) ----------
218fn empty(ctx: &Context, n: usize) -> wgpu::Buffer {
219    ctx.device.create_buffer(&wgpu::BufferDescriptor {
220        label: Some("t"), size: (n.max(1) * 4) as u64,
221        usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
222        mapped_at_creation: false,
223    })
224}
225fn u32buf(ctx: &Context, data: &[u32]) -> wgpu::Buffer {
226    ctx.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
227        label: Some("info"), contents: bytemuck::cast_slice(data),
228        usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
229    })
230}
231fn groups(n: usize) -> (u32, u32, u32) { (((n as u32) + 63) / 64, 1, 1) }
232fn run(ctx: &Context, wgsl: &str, label: &str, binds: &[&wgpu::Buffer], g: (u32, u32, u32)) {
233    let module = ctx.device.create_shader_module(wgpu::ShaderModuleDescriptor {
234        label: Some(label), source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(wgsl)),
235    });
236    let pipe = ctx.device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
237        label: Some(label), layout: None, module: &module, entry_point: Some("main"),
238        compilation_options: Default::default(), cache: None,
239    });
240    let entries: Vec<wgpu::BindGroupEntry> = binds.iter().enumerate()
241        .map(|(i, b)| wgpu::BindGroupEntry { binding: i as u32, resource: b.as_entire_binding() }).collect();
242    let bg = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
243        label: Some(label), layout: &pipe.get_bind_group_layout(0), entries: &entries,
244    });
245    let mut enc = ctx.device.create_command_encoder(&Default::default());
246    {
247        let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some(label), timestamp_writes: None });
248        pass.set_pipeline(&pipe);
249        pass.set_bind_group(0, &bg, &[]);
250        pass.dispatch_workgroups(g.0, g.1, g.2);
251    }
252    ctx.queue.submit([enc.finish()]);
253}
254async fn readback(ctx: &Context, buf: &wgpu::Buffer, n: usize) -> Vec<f32> {
255    let bytes = (n * 4) as u64;
256    let staging = ctx.device.create_buffer(&wgpu::BufferDescriptor {
257        label: Some("staging"), size: bytes, usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false,
258    });
259    let mut enc = ctx.device.create_command_encoder(&Default::default());
260    enc.copy_buffer_to_buffer(buf, 0, &staging, 0, bytes);
261    ctx.queue.submit([enc.finish()]);
262    let (tx, rx) = flume::bounded(1);
263    staging.slice(..).map_async(wgpu::MapMode::Read, move |r| { let _ = tx.send(r); });
264    let _ = ctx.device.poll(wgpu::PollType::wait_indefinitely());
265    rx.recv_async().await.unwrap().unwrap();
266    let data = staging.slice(..).get_mapped_range().unwrap();
267    let out = bytemuck::cast_slice(&data).to_vec();
268    drop(data);
269    staging.unmap();
270    out
271}
272
273// ---------- general kernels ----------
274// row-major decode of a linear output index into per-input strided offsets.
275const BINARY_WGSL: &str = r#"
276@group(0) @binding(0) var<storage,read>        a: array<f32>;
277@group(0) @binding(1) var<storage,read>        b: array<f32>;
278@group(0) @binding(2) var<storage,read_write>  out: array<f32>;
279@group(0) @binding(3) var<storage,read>        info: array<u32>; // rank,op,n,offA,offB,shape[r],aStr[r],bStr[r]
280@compute @workgroup_size(64)
281fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
282    let i = gid.x; let rank = info[0]; let op = info[1]; let n = info[2];
283    if (i >= n) { return; }
284    var ia = info[3]; var ib = info[4]; var rem = i;
285    for (var dd: u32 = 0u; dd < rank; dd = dd + 1u) {
286        let d = rank - 1u - dd;
287        let sz = info[5u + d];
288        let idx = rem % sz; rem = rem / sz;
289        ia = ia + idx * info[5u + rank + d];
290        ib = ib + idx * info[5u + 2u * rank + d];
291    }
292    let x = a[ia]; let y = b[ib];
293    var r: f32 = 0.0;
294    switch (op) {
295        case 0u: { r = x + y; }
296        case 1u: { r = x - y; }
297        case 2u: { r = x * y; }
298        case 3u: { r = x / y; }
299        case 4u: { r = max(x, y); }
300        default: { r = x + y; }
301    }
302    out[i] = r;
303}
304"#;
305
306const UNARY_WGSL: &str = r#"
307@group(0) @binding(0) var<storage,read>        x: array<f32>;
308@group(0) @binding(1) var<storage,read_write>  out: array<f32>;
309@group(0) @binding(2) var<storage,read>        info: array<u32>; // op, n
310@compute @workgroup_size(64)
311fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
312    let i = gid.x; if (i >= info[1]) { return; }
313    let v = x[i]; var r: f32 = v;
314    switch (info[0]) {
315        case 0u: { r = exp(v); }
316        case 1u: { r = -v; }
317        case 2u: { r = max(v, 0.0); }
318        case 3u: { r = sqrt(v); }
319        case 4u: { if (v > 0.0) { r = 1.0; } else { r = 0.0; } }
320        default: { r = v; }
321    }
322    out[i] = r;
323}
324"#;
325
326const GATHER_WGSL: &str = r#"
327@group(0) @binding(0) var<storage,read>        x: array<f32>;
328@group(0) @binding(1) var<storage,read_write>  out: array<f32>;
329@group(0) @binding(2) var<storage,read>        info: array<u32>; // rank,n,offset,shape[r],strides[r]
330@compute @workgroup_size(64)
331fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
332    let i = gid.x; let rank = info[0]; let n = info[1];
333    if (i >= n) { return; }
334    var src = info[2]; var rem = i;
335    for (var dd: u32 = 0u; dd < rank; dd = dd + 1u) {
336        let d = rank - 1u - dd;
337        let sz = info[3u + d];
338        let idx = rem % sz; rem = rem / sz;
339        src = src + idx * info[3u + rank + d];
340    }
341    out[i] = x[src];
342}
343"#;
344
345const REDUCE_WGSL: &str = r#"
346@group(0) @binding(0) var<storage,read>        x: array<f32>;   // [outer, red] contiguous
347@group(0) @binding(1) var<storage,read_write>  out: array<f32>; // [outer]
348@group(0) @binding(2) var<storage,read>        info: array<u32>; // outer, red, op(0=sum,1=max)
349@compute @workgroup_size(64)
350fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
351    let i = gid.x; let outer = info[0]; let red = info[1]; let op = info[2];
352    if (i >= outer) { return; }
353    let base = i * red;
354    if (op == 1u) {
355        var acc = x[base];
356        for (var j: u32 = 1u; j < red; j = j + 1u) { acc = max(acc, x[base + j]); }
357        out[i] = acc;
358    } else {
359        var acc = 0.0;
360        for (var j: u32 = 0u; j < red; j = j + 1u) { acc = acc + x[base + j]; }
361        out[i] = acc;
362    }
363}
364"#;
365
366const MATMUL_WGSL: &str = r#"
367@group(0) @binding(0) var<storage,read>        a: array<f32>;   // [batch, m, k]
368@group(0) @binding(1) var<storage,read>        b: array<f32>;   // [batch, k, n]
369@group(0) @binding(2) var<storage,read_write>  out: array<f32>; // [batch, m, n]
370@group(0) @binding(3) var<storage,read>        info: array<u32>; // batch, m, k, n
371@compute @workgroup_size(64)
372fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
373    let idx = gid.x; let batch = info[0]; let m = info[1]; let k = info[2]; let n = info[3];
374    if (idx >= batch * m * n) { return; }
375    let j = idx % n; let i = (idx / n) % m; let bt = idx / (m * n);
376    let ao = bt * m * k + i * k; let bo = bt * k * n;
377    var acc = 0.0;
378    for (var l: u32 = 0u; l < k; l = l + 1u) { acc = acc + a[ao + l] * b[bo + l * n + j]; }
379    out[idx] = acc;
380}
381"#;