Skip to main content

ferric_tensor/
dtype.rs

1//! Precision / storage dtypes for the fabric. Compute stays f32 (WebGPU-baseline has no shader-f16),
2//! but weights can LIVE on the GPU in half precision and be dequantized on-device — half the memory,
3//! and the path real fp16/bf16 checkpoints take. `Half` is a packed storage tensor (2 values per u32
4//! word); `dequant()` expands to a compute `Tensor`, `Tensor::to_half()` packs one down.
5
6use crate::{empty, groups, run, u32buf, unibuf, Tensor};
7use ferric_core::Context;
8use std::sync::Arc;
9use wgpu::util::DeviceExt;
10
11#[derive(Clone, Copy, PartialEq, Debug)]
12pub enum DType {
13    F16,
14    BF16,
15}
16impl DType {
17    fn code(self) -> u32 { match self { DType::F16 => 0, DType::BF16 => 1 } }
18}
19
20/// A half-precision tensor stored packed (2×16-bit per 32-bit word) in GPU memory.
21pub struct Half {
22    ctx: Arc<Context>,
23    buf: Arc<wgpu::Buffer>,
24    pub shape: Vec<usize>,
25    pub dtype: DType,
26}
27
28impl Half {
29    pub fn numel(&self) -> usize { self.shape.iter().product() }
30    /// Bytes actually stored on device (half of the f32 equivalent).
31    pub fn nbytes(&self) -> usize { self.numel().div_ceil(2) * 4 }
32
33    /// Build from raw 16-bit values (e.g. an fp16/bf16 slice straight out of a safetensors file).
34    pub fn from_bits(ctx: &Arc<Context>, bits: &[u16], shape: &[usize], dtype: DType) -> Half {
35        assert_eq!(bits.len(), shape.iter().product::<usize>(), "bits len != shape");
36        let words: Vec<u32> = bits.chunks(2).map(|c| c[0] as u32 | ((*c.get(1).unwrap_or(&0) as u32) << 16)).collect();
37        let buf = ctx.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
38            label: Some("half"),
39            contents: bytemuck::cast_slice(&words),
40            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
41        });
42        Half { ctx: ctx.clone(), buf: Arc::new(buf), shape: shape.to_vec(), dtype }
43    }
44
45    /// Dequantize to an f32 compute tensor, on-device.
46    pub fn dequant(&self) -> Tensor {
47        let n = self.numel();
48        let out = empty(&self.ctx, n);
49        run(&self.ctx, DEQUANT_WGSL, "dequant", &[self.buf.as_ref(), &out, &u32buf(&self.ctx, &[n as u32, self.dtype.code()])], groups(n));
50        Tensor::from_parts(&self.ctx, out, self.shape.clone())
51    }
52}
53
54impl Tensor {
55    /// Pack this f32 tensor down to half precision (round-to-nearest-even), on-device.
56    pub fn to_half(&self, dtype: DType) -> Half {
57        let c = self.contiguous();
58        let n = c.numel();
59        let words = n.div_ceil(2);
60        let out = empty(&self.ctx, words);
61        run(&self.ctx, QUANTIZE_WGSL, "quantize", &[c.buf.as_ref(), &out, &u32buf(&self.ctx, &[n as u32, dtype.code()])], groups(words));
62        Half { ctx: self.ctx.clone(), buf: Arc::new(out), shape: c.shape.clone(), dtype }
63    }
64}
65
66/// A per-tensor symmetric int8-quantized tensor (4 values packed per u32) plus its scale.
67pub struct QTensor {
68    ctx: Arc<Context>,
69    buf: Arc<wgpu::Buffer>,
70    pub scale: f32, // = max|x|/127
71    pub shape: Vec<usize>,
72}
73
74impl Tensor {
75    /// Symmetric per-tensor int8 quantization (scale = max|x|/127). Async: the scalar scale is read
76    /// back so the quantized matmul can fold both scales into one small buffer (WebGPU allows only 4
77    /// storage buffers per shader — scalars ride in the info buffer instead of their own bindings).
78    pub async fn quantize_i8(&self) -> QTensor {
79        let c = self.contiguous();
80        let n = c.numel();
81        let axes: Vec<usize> = (0..c.rank()).collect();
82        let s = c.abs().max(&axes, false).to_vec().await[0] / 127.0;
83        let s = if s == 0.0 { 1.0 } else { s };
84        let words = n.div_ceil(4);
85        let out = empty(&self.ctx, words);
86        run(&self.ctx, QUANT_I8_WGSL, "quant_i8", &[c.buf.as_ref(), &out, &u32buf(&self.ctx, &[n as u32, s.to_bits()])], groups(words));
87        QTensor { ctx: self.ctx.clone(), buf: Arc::new(out), scale: s, shape: c.shape.clone() }
88    }
89}
90
91impl QTensor {
92    /// Quantized matmul [m,k]·[k,n] → f32 (int accumulation, rescaled by both scales).
93    pub fn matmul(&self, o: &QTensor) -> Tensor {
94        let (ra, rb) = (self.shape.len(), o.shape.len());
95        assert!(ra == 2 && rb == 2, "quantized matmul is 2D for now");
96        let (m, k, n) = (self.shape[0], self.shape[1], o.shape[1]);
97        assert_eq!(k, o.shape[0], "inner dims mismatch");
98        let out = empty(&self.ctx, m * n);
99        let info = [m as u32, k as u32, n as u32, (self.scale * o.scale).to_bits()];
100        run(&self.ctx, MATMUL_I8_WGSL, "matmul_i8", &[self.buf.as_ref(), o.buf.as_ref(), &out, &u32buf(&self.ctx, &info)], groups(m * n));
101        Tensor::from_parts(&self.ctx, out, vec![m, n])
102    }
103}
104
105/// Per-row (per-output-channel) quantized 2D matrix at `bits` ∈ {4,8}, packed 32/bits per word,
106/// with one scale per row — more accurate than a single per-tensor scale, and int4 is 1/8 the memory.
107pub struct QRow {
108    ctx: Arc<Context>,
109    buf: Arc<wgpu::Buffer>,
110    scale: Arc<wgpu::Buffer>, // [rows] f32
111    pub rows: usize,
112    pub cols: usize,
113    pub bits: u32,
114}
115
116impl Tensor {
117    /// Per-row symmetric quantization of a 2D matrix at 4 or 8 bits (scale = max|row|/(2^(bits-1)−1)).
118    pub fn quantize_rowwise(&self, bits: u32) -> QRow {
119        let c = self.contiguous();
120        assert_eq!(c.rank(), 2, "rowwise quant is 2D");
121        let (rows, cols) = (c.shape[0], c.shape[1]);
122        let qmax = ((1u32 << (bits - 1)) - 1) as f32;
123        let scale = c.abs().max(&[1], false).mul(&c.scalar(1.0 / qmax)); // [rows]
124        let per_word = (32 / bits) as usize;
125        let words = (rows * cols).div_ceil(per_word);
126        let out = empty(&self.ctx, words);
127        run(&self.ctx, QUANT_ROW_WGSL, "quant_row", &[c.buf.as_ref(), scale.buf.as_ref(), &out, &u32buf(&self.ctx, &[rows as u32, cols as u32, bits, qmax.to_bits()])], groups(words));
128        QRow { ctx: self.ctx.clone(), buf: Arc::new(out), scale: scale.buf.clone(), rows, cols, bits }
129    }
130}
131
132impl Tensor {
133    /// Weight-only quantized matmul (the efficient-inference path): x [rows, in] · Wᵀ where W is a
134    /// per-row-quantized [out, in] matrix that stays packed in memory — dequantized on the fly in the
135    /// kernel. Returns [rows, out]. This is W4A16/W8A16-style: activations f32, weights int4/int8.
136    pub fn matmul_qweight(&self, w: &QRow) -> Tensor {
137        let x = self.contiguous();
138        assert_eq!(x.rank(), 2, "matmul_qweight is 2D");
139        let (rows, inn) = (x.shape[0], x.shape[1]);
140        assert_eq!(inn, w.cols, "inner dims mismatch: x[..,{inn}] vs W[..,{}]", w.cols);
141        let out = empty(&self.ctx, rows * w.rows);
142        run(&self.ctx, MATMUL_QW_WGSL, "matmul_qw", &[x.buf.as_ref(), w.buf.as_ref(), w.scale.as_ref(), &out, &unibuf(&self.ctx, &[rows as u32, w.rows as u32, inn as u32, w.bits])], groups(rows * w.rows));
143        Tensor::from_parts(&self.ctx, out, vec![rows, w.rows])
144    }
145}
146
147impl QRow {
148    pub fn nbytes(&self) -> usize { (self.rows * self.cols * self.bits as usize).div_ceil(8) }
149    /// Dequantize back to an f32 [rows, cols] tensor, on-device.
150    pub fn dequant(&self) -> Tensor {
151        let n = self.rows * self.cols;
152        let out = empty(&self.ctx, n);
153        run(&self.ctx, DEQUANT_ROW_WGSL, "dequant_row", &[self.buf.as_ref(), self.scale.as_ref(), &out, &u32buf(&self.ctx, &[self.rows as u32, self.cols as u32, self.bits])], groups(n));
154        Tensor::from_parts(&self.ctx, out, vec![self.rows, self.cols])
155    }
156}
157
158/// A ternary-weight matrix (BitNet b1.58 family): weights ∈ {−1,0,+1} packed 16 per u32 (2 bits
159/// each), with a per-output-channel scale (absmean). The matmul is effectively multiply-free — each
160/// weight just adds, subtracts, or skips an activation. 1.58 bits/weight ≈ 1/16 the memory of f32.
161pub struct Ternary {
162    ctx: Arc<Context>,
163    buf: Arc<wgpu::Buffer>,
164    scale: Arc<wgpu::Buffer>, // [out] = absmean per row
165    pub rows: usize,
166    pub cols: usize,
167}
168
169impl Tensor {
170    /// Quantize a 2D [out,in] weight to ternary {−1,0,+1} with per-row absmean scale (BitNet-style).
171    pub fn quantize_ternary(&self) -> Ternary {
172        let c = self.contiguous();
173        assert_eq!(c.rank(), 2, "ternary quant is 2D");
174        let (rows, cols) = (c.shape[0], c.shape[1]);
175        let scale = c.abs().mean(&[1], false); // [rows] absmean
176        let words = (rows * cols).div_ceil(16);
177        let out = empty(&self.ctx, words);
178        run(&self.ctx, QUANT_TERNARY_WGSL, "quant_ternary", &[c.buf.as_ref(), scale.buf.as_ref(), &out, &u32buf(&self.ctx, &[rows as u32, cols as u32])], groups(words));
179        Ternary { ctx: self.ctx.clone(), buf: Arc::new(out), scale: scale.buf.clone(), rows, cols }
180    }
181    /// Multiply-free ternary matmul: x [rows,in] · Wᵀ where W is ternary [out,in]. Returns [rows,out].
182    pub fn matmul_ternary(&self, w: &Ternary) -> Tensor {
183        let x = self.contiguous();
184        let (rows, inn) = (x.shape[0], x.shape[1]);
185        assert_eq!(inn, w.cols, "inner dims mismatch");
186        let out = empty(&self.ctx, rows * w.rows);
187        run(&self.ctx, MATMUL_TERNARY_WGSL, "matmul_ternary", &[x.buf.as_ref(), w.buf.as_ref(), w.scale.as_ref(), &out, &unibuf(&self.ctx, &[rows as u32, w.rows as u32, inn as u32, 0])], groups(rows * w.rows));
188        Tensor::from_parts(&self.ctx, out, vec![rows, w.rows])
189    }
190}
191impl Ternary {
192    pub fn nbytes(&self) -> usize { (self.rows * self.cols * 2).div_ceil(8) }
193}
194
195const QUANT_TERNARY_WGSL: &str = r#"
196@group(0) @binding(0) var<storage,read>        inp: array<f32>;
197@group(0) @binding(1) var<storage,read>        scale: array<f32>; // [rows] absmean
198@group(0) @binding(2) var<storage,read_write>  out: array<u32>;   // 16 ternary codes per word
199@group(0) @binding(3) var<storage,read>        info: array<u32>;  // rows, cols
200@compute @workgroup_size(64)
201fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
202    let w = gid.x; let rows = info[0]; let cols = info[1]; let n = rows * cols; let words = (n + 15u) / 16u;
203    if (w >= words) { return; }
204    var word: u32 = 0u;
205    for (var lane: u32 = 0u; lane < 16u; lane = lane + 1u) {
206        let idx = 16u * w + lane;
207        if (idx < n) {
208            var s = scale[idx / cols]; if (s == 0.0) { s = 1.0; }
209            let t = clamp(round(inp[idx] / s), -1.0, 1.0);      // {−1,0,+1}
210            let code = u32(i32(t) + 1);                          // {0,1,2}
211            word = word | (code << (2u * lane));
212        }
213    }
214    out[w] = word;
215}
216"#;
217
218const MATMUL_TERNARY_WGSL: &str = r#"
219@group(0) @binding(0) var<storage,read>        x: array<f32>;     // [rows, in]
220@group(0) @binding(1) var<storage,read>        tw: array<u32>;    // packed ternary [out, in]
221@group(0) @binding(2) var<storage,read>        scale: array<f32>; // [out]
222@group(0) @binding(3) var<storage,read_write>  out: array<f32>;   // [rows, out]
223@group(0) @binding(4) var<uniform>             info: vec4<u32>;   // rows, out, in
224@compute @workgroup_size(64)
225fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
226    let idx = gid.x; let rows = info.x; let o_dim = info.y; let in_dim = info.z;
227    if (idx >= rows * o_dim) { return; }
228    let o = idx % o_dim; let r = idx / o_dim;
229    var acc = 0.0;
230    for (var i: u32 = 0u; i < in_dim; i = i + 1u) {
231        let widx = o * in_dim + i;
232        let code = (tw[widx / 16u] >> (2u * (widx % 16u))) & 3u; // {0,1,2}
233        let t = f32(i32(code) - 1);                              // {−1,0,+1}  (multiply-free in spirit)
234        acc = acc + x[r * in_dim + i] * t;
235    }
236    out[idx] = acc * scale[o];
237}
238"#;
239
240const MATMUL_QW_WGSL: &str = r#"
241@group(0) @binding(0) var<storage,read>        x: array<f32>;      // [rows, in]
242@group(0) @binding(1) var<storage,read>        qw: array<u32>;     // packed per-row int, [out, in]
243@group(0) @binding(2) var<storage,read>        scale: array<f32>;  // [out]
244@group(0) @binding(3) var<storage,read_write>  out: array<f32>;    // [rows, out]
245@group(0) @binding(4) var<uniform>             info: vec4<u32>;    // rows, out, in, bits
246@compute @workgroup_size(64)
247fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
248    let idx = gid.x; let rows = info.x; let o_dim = info.y; let in_dim = info.z; let bits = info.w;
249    if (idx >= rows * o_dim) { return; }
250    let o = idx % o_dim; let r = idx / o_dim;
251    let per = 32u / bits; let mask = (1u << bits) - 1u; let signbit = 1u << (bits - 1u);
252    var acc = 0.0;
253    for (var i: u32 = 0u; i < in_dim; i = i + 1u) {
254        let widx = o * in_dim + i;                       // element in W's flat [out,in]
255        var q = i32((qw[widx / per] >> (bits * (widx % per))) & mask);
256        if (q >= i32(signbit)) { q = q - i32(1u << bits); }
257        acc = acc + x[r * in_dim + i] * f32(q);          // weight dequantized on the fly
258    }
259    out[idx] = acc * scale[o];
260}
261"#;
262
263const QUANT_ROW_WGSL: &str = r#"
264@group(0) @binding(0) var<storage,read>        inp: array<f32>;
265@group(0) @binding(1) var<storage,read>        scale: array<f32>; // [rows]
266@group(0) @binding(2) var<storage,read_write>  out: array<u32>;
267@group(0) @binding(3) var<storage,read>        info: array<u32>;  // rows, cols, bits, bitcast(qmax)
268@compute @workgroup_size(64)
269fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
270    let w = gid.x; let rows = info[0]; let cols = info[1]; let bits = info[2]; let qmax = bitcast<f32>(info[3]);
271    let per = 32u / bits; let n = rows * cols; let words = (n + per - 1u) / per;
272    if (w >= words) { return; }
273    let mask = (1u << bits) - 1u;
274    var word: u32 = 0u;
275    for (var lane: u32 = 0u; lane < per; lane = lane + 1u) {
276        let idx = w * per + lane;
277        if (idx < n) {
278            var s = scale[idx / cols]; if (s == 0.0) { s = 1.0; }
279            let q = i32(clamp(round(inp[idx] / s), -qmax, qmax));
280            word = word | ((u32(q) & mask) << (bits * lane));
281        }
282    }
283    out[w] = word;
284}
285"#;
286
287const DEQUANT_ROW_WGSL: &str = r#"
288@group(0) @binding(0) var<storage,read>        inp: array<u32>;
289@group(0) @binding(1) var<storage,read>        scale: array<f32>; // [rows]
290@group(0) @binding(2) var<storage,read_write>  out: array<f32>;
291@group(0) @binding(3) var<storage,read>        info: array<u32>;  // rows, cols, bits
292@compute @workgroup_size(64)
293fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
294    let idx = gid.x; let rows = info[0]; let cols = info[1]; let bits = info[2];
295    let n = rows * cols; if (idx >= n) { return; }
296    let per = 32u / bits; let mask = (1u << bits) - 1u; let signbit = 1u << (bits - 1u);
297    let word = inp[idx / per]; let lane = idx % per;
298    var v = i32((word >> (bits * lane)) & mask);
299    if (v >= i32(signbit)) { v = v - i32(1u << bits); }
300    out[idx] = f32(v) * scale[idx / cols];
301}
302"#;
303
304const QUANT_I8_WGSL: &str = r#"
305@group(0) @binding(0) var<storage,read>        inp: array<f32>;
306@group(0) @binding(1) var<storage,read_write>  out: array<u32>;   // 4x int8 per word
307@group(0) @binding(2) var<storage,read>        info: array<u32>;  // n, bitcast(scale)
308@compute @workgroup_size(64)
309fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
310    let w = gid.x; let n = info[0]; let words = (n + 3u) / 4u;
311    if (w >= words) { return; }
312    let s = bitcast<f32>(info[1]);
313    var word: u32 = 0u;
314    for (var lane: u32 = 0u; lane < 4u; lane = lane + 1u) {
315        let idx = 4u * w + lane;
316        if (idx < n) {
317            let q = i32(clamp(round(inp[idx] / s), -127.0, 127.0));
318            word = word | ((u32(q) & 0xffu) << (8u * lane));
319        }
320    }
321    out[w] = word;
322}
323"#;
324
325const MATMUL_I8_WGSL: &str = r#"
326@group(0) @binding(0) var<storage,read>        a: array<u32>;  // packed [m,k]
327@group(0) @binding(1) var<storage,read>        b: array<u32>;  // packed [k,n]
328@group(0) @binding(2) var<storage,read_write>  out: array<f32>;
329@group(0) @binding(3) var<storage,read>        info: array<u32>; // m,k,n, bitcast(scaleA*scaleB)
330@compute @workgroup_size(64)
331fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
332    let idx = gid.x; let m = info[0]; let k = info[1]; let n = info[2];
333    let sc = bitcast<f32>(info[3]);
334    if (idx >= m * n) { return; }
335    let j = idx % n; let i = idx / n;
336    var acc: i32 = 0;
337    for (var l: u32 = 0u; l < k; l = l + 1u) {
338        let ai = i * k + l; let wa = a[ai >> 2u]; var av = i32((wa >> (8u * (ai & 3u))) & 0xffu); if (av > 127) { av = av - 256; }
339        let bi = l * n + j; let wb = b[bi >> 2u]; var bv = i32((wb >> (8u * (bi & 3u))) & 0xffu); if (bv > 127) { bv = bv - 256; }
340        acc = acc + av * bv;
341    }
342    out[idx] = f32(acc) * sc;
343}
344"#;
345
346const DEQUANT_WGSL: &str = r#"
347@group(0) @binding(0) var<storage,read>        inp: array<u32>; // packed 2x16
348@group(0) @binding(1) var<storage,read_write>  out: array<f32>;
349@group(0) @binding(2) var<storage,read>        info: array<u32>; // n, kind(0=f16,1=bf16)
350@compute @workgroup_size(64)
351fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
352    let i = gid.x; let n = info[0]; let kind = info[1];
353    if (i >= n) { return; }
354    let word = inp[i >> 1u]; let sel = i & 1u;
355    if (kind == 0u) {
356        let pair = unpack2x16float(word);      // two f16 → f32
357        out[i] = select(pair.x, pair.y, sel == 1u);
358    } else {
359        let h = (word >> (16u * sel)) & 0xffffu;
360        out[i] = bitcast<f32>(h << 16u);        // bf16 → f32
361    }
362}
363"#;
364
365const QUANTIZE_WGSL: &str = r#"
366@group(0) @binding(0) var<storage,read>        inp: array<f32>;
367@group(0) @binding(1) var<storage,read_write>  out: array<u32>; // packed 2x16
368@group(0) @binding(2) var<storage,read>        info: array<u32>; // n, kind
369fn bf16_rne(x: f32) -> u32 {
370    let b = bitcast<u32>(x);
371    let r = b + 0x7fffu + ((b >> 16u) & 1u); // round-to-nearest-even bias
372    return r >> 16u;
373}
374@compute @workgroup_size(64)
375fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
376    let w = gid.x; let n = info[0]; let kind = info[1];
377    let words = (n + 1u) / 2u;
378    if (w >= words) { return; }
379    let i0 = 2u * w; let i1 = i0 + 1u;
380    let x0 = inp[i0];
381    var x1 = 0.0;
382    if (i1 < n) { x1 = inp[i1]; }
383    if (kind == 0u) {
384        out[w] = pack2x16float(vec2<f32>(x0, x1));
385    } else {
386        out[w] = bf16_rne(x0) | (bf16_rne(x1) << 16u);
387    }
388}
389"#;