spark_model/forward/
quant_weights.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2//! Per-quant weight abstraction.
3//!
4//! Plug-in trait the vendor-agnostic forward modules (e.g.
5//! [`super::qwen3_5`]) call instead of reaching into a concrete weight
6//! type like `MlxInt8Weight`. Each backend's weight loader implements
7//! `QuantWeights` and overrides whichever fused variants it ships
8//! kernels for; the rest fall back to default impls that compose the
9//! unfused primitives or fail loudly.
10//!
11//! Convention: `gemv` is mandatory (every backend ships one). Fused
12//! variants (`gemv_silu_gate`, `gemv_silu_gate_resid`,
13//! `gemv_gate_up_with`) are advisory — backends override only the
14//! ones they have fused kernels for. Defaults either fall back to a
15//! correct-but-slower composition (`gemv_gate_up_with`) or error
16//! loudly so the caller can choose between hard-requiring the fused
17//! kernel and degrading to a manual silu+mul+gemv pipeline.
18
19use anyhow::{Result, bail};
20use spark_runtime::gpu::{DevicePtr, GpuBackend};
21use spark_runtime::weights::mlx_int8::{self, MlxInt8Weight};
22
23/// A quantised weight tensor that can drive matvec / matmul ops on a
24/// `GpuBackend`. Implementations live with each backend's weight
25/// loader (Metal: `MlxInt8Weight`; future CUDA: `Nvfp4Weight`,
26/// `Fp8DenseWeight`, …).
27pub trait QuantWeights: Send + Sync {
28    /// Output dimension `N` of the underlying `[N, K]` weight.
29    fn out_features(&self) -> u32;
30
31    /// Input dimension `K` of the underlying `[N, K]` weight.
32    fn in_features(&self) -> u32;
33
34    /// Decode-path matvec: `y = self @ x`.
35    ///
36    /// `x` is a BF16 buffer of length `in_features()`; `y` must hold at
37    /// least `out_features()` BF16 slots.
38    fn gemv(&self, gpu: &dyn GpuBackend, x: DevicePtr, y: DevicePtr, stream: u64) -> Result<()>;
39
40    /// Dual-output GEMV with shared input: `gate_y = self @ x`,
41    /// `up_y = other @ x`. The default impl is two serial `gemv`
42    /// calls — correct on any backend, just slower than the fused
43    /// kernel some backends ship (e.g. Metal's
44    /// `mlx_int8_gemv_gate_up`). Backends with a fused dual-output
45    /// path override this to halve x-side memory bandwidth and
46    /// remove a launch.
47    ///
48    /// `where Self: Sized` keeps this method off the dyn-trait surface
49    /// (it's only callable through generic-parameter dispatch, which
50    /// is what the forward modules use anyway).
51    fn gemv_gate_up_with(
52        &self,
53        other: &Self,
54        gpu: &dyn GpuBackend,
55        x: DevicePtr,
56        gate_y: DevicePtr,
57        up_y: DevicePtr,
58        stream: u64,
59    ) -> Result<()>
60    where
61        Self: Sized,
62    {
63        debug_assert_eq!(self.out_features(), other.out_features());
64        debug_assert_eq!(self.in_features(), other.in_features());
65        self.gemv(gpu, x, gate_y, stream)?;
66        other.gemv(gpu, x, up_y, stream)?;
67        Ok(())
68    }
69
70    /// Fused FFN tail: `y = self @ (silu(gate) ⊙ up)`.
71    ///
72    /// Default impl errors — backends that ship the fused kernel
73    /// override; backends that don't can either error out (forcing
74    /// the caller to do the unfused dance) or override with their
75    /// own composition.
76    fn gemv_silu_gate(
77        &self,
78        _gpu: &dyn GpuBackend,
79        _gate: DevicePtr,
80        _up: DevicePtr,
81        _y: DevicePtr,
82        _stream: u64,
83    ) -> Result<()> {
84        bail!(
85            "QuantWeights::gemv_silu_gate is not implemented for this weight type — \
86             override with a fused kernel or compose silu+mul+gemv on the caller side"
87        )
88    }
89
90    /// Same as [`Self::gemv_silu_gate`] but additionally folds the
91    /// layer-output residual addition into the same kernel:
92    ///   `y[n] = x_resid[n] + sum_k self[n, k] * (silu(gate[k]) ⊙ up[k])`.
93    ///
94    /// Default impl errors. Backends that ship a `_resid` variant of
95    /// the fused kernel override.
96    fn gemv_silu_gate_resid(
97        &self,
98        _gpu: &dyn GpuBackend,
99        _gate: DevicePtr,
100        _up: DevicePtr,
101        _x_resid: DevicePtr,
102        _y: DevicePtr,
103        _stream: u64,
104    ) -> Result<()> {
105        bail!(
106            "QuantWeights::gemv_silu_gate_resid is not implemented for this weight type — \
107             override with a fused kernel or compose silu+mul+gemv+add on the caller side"
108        )
109    }
110}
111
112// ── Backend impls ────────────────────────────────────────────────────
113//
114// Lives next to the trait so the orphan rule applies (the trait is
115// foreign to spark-runtime, but native here). Each impl is a thin
116// forwarding shim onto the concrete weight type's inherent methods,
117// keeping the optimised fused-kernel paths intact.
118
119impl QuantWeights for MlxInt8Weight {
120    fn out_features(&self) -> u32 {
121        self.out_features
122    }
123    fn in_features(&self) -> u32 {
124        self.in_features
125    }
126    fn gemv(&self, gpu: &dyn GpuBackend, x: DevicePtr, y: DevicePtr, stream: u64) -> Result<()> {
127        MlxInt8Weight::gemv(self, gpu, x, y, stream)
128    }
129    fn gemv_gate_up_with(
130        &self,
131        other: &Self,
132        gpu: &dyn GpuBackend,
133        x: DevicePtr,
134        gate_y: DevicePtr,
135        up_y: DevicePtr,
136        stream: u64,
137    ) -> Result<()> {
138        // Atlas Metal ships a fused dual-output kernel
139        // (`mlx_int8_gemv_gate_up`); use it instead of two serial
140        // gemvs to halve x-side bandwidth and remove a launch.
141        mlx_int8::gemv_gate_up(gpu, self, other, x, gate_y, up_y, stream)
142    }
143    fn gemv_silu_gate(
144        &self,
145        gpu: &dyn GpuBackend,
146        gate: DevicePtr,
147        up: DevicePtr,
148        y: DevicePtr,
149        stream: u64,
150    ) -> Result<()> {
151        MlxInt8Weight::gemv_silu_gate(self, gpu, gate, up, y, stream)
152    }
153    fn gemv_silu_gate_resid(
154        &self,
155        gpu: &dyn GpuBackend,
156        gate: DevicePtr,
157        up: DevicePtr,
158        x_resid: DevicePtr,
159        y: DevicePtr,
160        stream: u64,
161    ) -> Result<()> {
162        MlxInt8Weight::gemv_silu_gate_resid(self, gpu, gate, up, x_resid, y, stream)
163    }
164}