metrale_model_layers/forward/quant_weights.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: The `QuantWeights` trait: the matvec operations a per-layer forward
4//! needs from a quantised weight, so `super::qwen3_5` never names a concrete
5//! weight type.
6//!
7//! Owner: model-layers.
8//! Invariants: none beyond the types.
9//!
10//! `gemv` is required. The fused operations are optional: `gemv_gate_up_with`
11//! defaults to two `gemv` calls, and `gemv_silu_gate` / `gemv_silu_gate_resid`
12//! default to an error, so a caller learns that no fused kernel exists instead
13//! of getting a silent slow path.
14
15use anyhow::{Result, bail};
16use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend};
17use metrale_gpu_runtime::mlx_int8::{self, MlxInt8Weight};
18
19/// 2026-09-25: A quantised `[N, K]` weight that runs matvecs on a `GpuBackend`.
20/// The one implementation, for `MlxInt8Weight`, is in this file.
21pub trait QuantWeights: Send + Sync {
22 /// 2026-09-25: Output dimension `N` of the `[N, K]` weight.
23 fn out_features(&self) -> u32;
24
25 /// 2026-09-25: Input dimension `K` of the `[N, K]` weight.
26 fn in_features(&self) -> u32;
27
28 /// 2026-09-25: Decode matvec `y = self @ x`.
29 ///
30 /// `x` is a BF16 buffer of length `in_features()`; `y` must hold at
31 /// least `out_features()` BF16 slots.
32 fn gemv(&self, gpu: &dyn GpuBackend, x: DevicePtr, y: DevicePtr, stream: u64) -> Result<()>;
33
34 /// 2026-09-25: Dual-output GEMV over one input: `gate_y = self @ x` and
35 /// `up_y = other @ x`. The default is two `gemv` calls. The
36 /// `MlxInt8Weight` impl overrides it with the single
37 /// `mlx_int8_gemv_gate_up` launch.
38 ///
39 /// `where Self: Sized` keeps this method off the `dyn QuantWeights`
40 /// surface; the forward modules call it through a generic parameter.
41 fn gemv_gate_up_with(
42 &self,
43 other: &Self,
44 gpu: &dyn GpuBackend,
45 x: DevicePtr,
46 gate_y: DevicePtr,
47 up_y: DevicePtr,
48 stream: u64,
49 ) -> Result<()>
50 where
51 Self: Sized,
52 {
53 debug_assert_eq!(self.out_features(), other.out_features());
54 debug_assert_eq!(self.in_features(), other.in_features());
55 self.gemv(gpu, x, gate_y, stream)?;
56 other.gemv(gpu, x, up_y, stream)?;
57 Ok(())
58 }
59
60 /// 2026-09-25: Fused FFN tail `y = self @ (silu(gate) ⊙ up)`. The default
61 /// returns an error; an implementation overrides it with a fused kernel
62 /// or its own composition.
63 fn gemv_silu_gate(
64 &self,
65 _gpu: &dyn GpuBackend,
66 _gate: DevicePtr,
67 _up: DevicePtr,
68 _y: DevicePtr,
69 _stream: u64,
70 ) -> Result<()> {
71 bail!(
72 "QuantWeights::gemv_silu_gate is not implemented for this weight type — \
73 override with a fused kernel or compose silu+mul+gemv on the caller side"
74 )
75 }
76
77 /// 2026-09-25: [`Self::gemv_silu_gate`] with the residual add folded in:
78 /// `y[n] = x_resid[n] + sum_k self[n, k] * (silu(gate[k]) ⊙ up[k])`.
79 ///
80 /// The default returns an error.
81 fn gemv_silu_gate_resid(
82 &self,
83 _gpu: &dyn GpuBackend,
84 _gate: DevicePtr,
85 _up: DevicePtr,
86 _x_resid: DevicePtr,
87 _y: DevicePtr,
88 _stream: u64,
89 ) -> Result<()> {
90 bail!(
91 "QuantWeights::gemv_silu_gate_resid is not implemented for this weight type — \
92 override with a fused kernel or compose silu+mul+gemv+add on the caller side"
93 )
94 }
95}
96
97// 2026-09-25: The impl sits beside the trait because of the orphan rule:
98// `MlxInt8Weight` belongs to metrale-gpu-runtime, which cannot see this trait.
99// Each method forwards to the weight type's own fused-kernel functions.
100
101impl QuantWeights for MlxInt8Weight {
102 fn out_features(&self) -> u32 {
103 self.out_features
104 }
105 fn in_features(&self) -> u32 {
106 self.in_features
107 }
108 fn gemv(&self, gpu: &dyn GpuBackend, x: DevicePtr, y: DevicePtr, stream: u64) -> Result<()> {
109 MlxInt8Weight::gemv(self, gpu, x, y, stream)
110 }
111 fn gemv_gate_up_with(
112 &self,
113 other: &Self,
114 gpu: &dyn GpuBackend,
115 x: DevicePtr,
116 gate_y: DevicePtr,
117 up_y: DevicePtr,
118 stream: u64,
119 ) -> Result<()> {
120 // 2026-09-25: One `mlx_int8_gemv_gate_up` launch computes both outputs.
121 mlx_int8::gemv_gate_up(gpu, self, other, x, gate_y, up_y, stream)
122 }
123 fn gemv_silu_gate(
124 &self,
125 gpu: &dyn GpuBackend,
126 gate: DevicePtr,
127 up: DevicePtr,
128 y: DevicePtr,
129 stream: u64,
130 ) -> Result<()> {
131 MlxInt8Weight::gemv_silu_gate(self, gpu, gate, up, y, stream)
132 }
133 fn gemv_silu_gate_resid(
134 &self,
135 gpu: &dyn GpuBackend,
136 gate: DevicePtr,
137 up: DevicePtr,
138 x_resid: DevicePtr,
139 y: DevicePtr,
140 stream: u64,
141 ) -> Result<()> {
142 MlxInt8Weight::gemv_silu_gate_resid(self, gpu, gate, up, x_resid, y, stream)
143 }
144}