1use std::any::{Any, TypeId};
28use std::sync::Arc;
29
30use burn::backend::wgpu::{CubeBackend, CubeTensor, WgpuDevice, WgpuRuntime};
31use burn::tensor::backend::Backend;
32use burn::tensor::{DType, Device, FloatDType, Shape, Tensor, TensorPrimitive};
33use burn_cubecl::fusion::FusionCubeRuntime;
34use burn_cubecl::kernel::into_contiguous;
35use burn_cubecl_fusion::CubeFusionHandle;
36use burn_fusion::Fusion;
37use burn_fusion::stream::{Operation, OperationStreams};
38use burn_ir::{CustomOpIr, HandleContainer, OperationIr, TensorIr, TensorStatus};
39use combs_formats::ModelSource;
40
41use crate::llama::linear as dense_linear;
42use crate::qmatmul::QuantWeight;
43use crate::{ModelError, Result};
44
45type FusedF32 = Fusion<CubeBackend<WgpuRuntime, f32, i32, u32>>;
47type UnfusedF32 = CubeBackend<WgpuRuntime, f32, i32, u32>;
49type UnfusedF16 = CubeBackend<WgpuRuntime, burn::tensor::f16, i32, u32>;
51type InnerF32 = CubeBackend<WgpuRuntime, f32, i32, u32>;
53
54pub trait QuantLinearOp<B: Backend>: Send + Sync {
57 fn forward(&self, x: Tensor<B, 3>) -> Tensor<B, 3>;
59 fn dims(&self) -> [usize; 2];
61 fn vram_bytes(&self) -> usize;
63}
64
65pub enum Linear<B: Backend> {
68 Dense(Tensor<B, 2>),
70 Quant(Box<dyn QuantLinearOp<B>>),
72}
73
74impl<B: Backend> Linear<B> {
75 pub fn dims(&self) -> [usize; 2] {
77 match self {
78 Linear::Dense(w) => w.dims(),
79 Linear::Quant(op) => op.dims(),
80 }
81 }
82
83 pub fn forward(&self, x: Tensor<B, 3>, bias: Option<&Tensor<B, 1>>) -> Tensor<B, 3> {
85 match self {
86 Linear::Dense(w) => dense_linear(x, w, bias),
87 Linear::Quant(op) => {
88 let out = op.forward(x);
89 match bias {
90 Some(b) => {
91 let [batch, seq, dim] = out.dims();
92 out + b.clone().reshape([1, 1, dim]).expand([batch, seq, dim])
93 }
94 None => out,
95 }
96 }
97 }
98 }
99}
100
101struct CubeQuantLinear {
104 w: Arc<QuantWeight>,
105}
106
107impl CubeQuantLinear {
108 fn dims(&self) -> [usize; 2] {
109 [self.w.n_out(), self.w.k()]
110 }
111
112 fn forward_cube(&self, x: CubeTensor<WgpuRuntime>, batch: usize, seq: usize) -> CubeTensor<WgpuRuntime> {
114 let x = into_contiguous(x);
115 let out_h = self.w.matmul_device(&x.client, x.handle.clone(), batch * seq);
116 CubeTensor::new_contiguous(
117 x.client.clone(),
118 x.device.clone(),
119 Shape::from([batch, seq, self.w.n_out()]),
120 out_h,
121 DType::F32,
122 )
123 }
124}
125
126fn to_f32<B: Backend>(x: Tensor<B, 3>) -> Tensor<B, 3> {
131 match x.dtype() {
132 DType::F32 => x,
133 _ => x.cast(FloatDType::F32),
134 }
135}
136
137fn to_dtype<B: Backend>(out: Tensor<B, 3>, dtype: DType) -> Tensor<B, 3> {
138 match dtype {
139 DType::F16 => out.cast(FloatDType::F16),
140 DType::BF16 => out.cast(FloatDType::BF16),
141 _ => out,
142 }
143}
144
145impl QuantLinearOp<UnfusedF32> for CubeQuantLinear {
146 fn forward(&self, x: Tensor<UnfusedF32, 3>) -> Tensor<UnfusedF32, 3> {
147 let in_dtype = x.dtype();
148 let [batch, seq, _] = x.dims();
149 let prim = to_f32(x).into_primitive().tensor();
150 let out = self.forward_cube(prim, batch, seq);
151 to_dtype(
152 Tensor::from_primitive(TensorPrimitive::Float(out)),
153 in_dtype,
154 )
155 }
156
157 fn dims(&self) -> [usize; 2] {
158 CubeQuantLinear::dims(self)
159 }
160
161 fn vram_bytes(&self) -> usize {
162 self.w.vram_bytes()
163 }
164}
165
166impl QuantLinearOp<UnfusedF16> for CubeQuantLinear {
167 fn forward(&self, x: Tensor<UnfusedF16, 3>) -> Tensor<UnfusedF16, 3> {
168 let in_dtype = x.dtype();
172 let [batch, seq, _] = x.dims();
173 let prim = to_f32(x).into_primitive().tensor();
174 let out = self.forward_cube(prim, batch, seq);
175 to_dtype(
176 Tensor::<UnfusedF16, 3>::from_primitive(TensorPrimitive::Float(out)),
177 in_dtype,
178 )
179 }
180
181 fn dims(&self) -> [usize; 2] {
182 CubeQuantLinear::dims(self)
183 }
184
185 fn vram_bytes(&self) -> usize {
186 self.w.vram_bytes()
187 }
188}
189
190struct QuantMatmulOp {
193 desc: CustomOpIr,
194 w: Arc<QuantWeight>,
195 batch: usize,
196 seq: usize,
197}
198
199impl core::fmt::Debug for QuantMatmulOp {
200 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
201 write!(
202 f,
203 "QuantMatmulOp {{ w: [{}, {}], m: {} }}",
204 self.w.n_out(),
205 self.w.k(),
206 self.batch * self.seq
207 )
208 }
209}
210
211impl Operation<FusionCubeRuntime<WgpuRuntime>> for QuantMatmulOp {
212 fn execute(&self, handles: &mut HandleContainer<CubeFusionHandle<WgpuRuntime>>) {
213 let ([input], [output]) = self.desc.as_fixed::<1, 1>();
214 let x: CubeTensor<WgpuRuntime> = handles.get_float_tensor::<InnerF32>(input);
215 let x = into_contiguous(x);
216 let out_h = self.w.matmul_device(&x.client, x.handle.clone(), self.batch * self.seq);
217 let out = CubeTensor::new_contiguous(
218 x.client.clone(),
219 x.device.clone(),
220 Shape::from([self.batch, self.seq, self.w.n_out()]),
221 out_h,
222 DType::F32,
223 );
224 handles.register_float_tensor::<InnerF32>(&output.id, out);
225 }
226}
227
228impl QuantLinearOp<FusedF32> for CubeQuantLinear {
229 fn forward(&self, x: Tensor<FusedF32, 3>) -> Tensor<FusedF32, 3> {
230 let in_dtype = x.dtype();
231 let [batch, seq, _] = x.dims();
232 let prim = to_f32(x).into_primitive().tensor();
233 let client = prim.client.clone();
234
235 let mut streams = OperationStreams::default();
236 streams.tensor(&prim);
237 let input_ir = prim.into_ir();
238 let out_ir = TensorIr {
239 id: client.create_empty_handle(),
240 shape: Shape::from([batch, seq, self.w.n_out()]),
241 status: TensorStatus::NotInit,
242 dtype: DType::F32,
243 };
244 let desc = CustomOpIr::new("combs_quant_matmul", &[input_ir], &[out_ir]);
245 let op = QuantMatmulOp {
246 desc: desc.clone(),
247 w: self.w.clone(),
248 batch,
249 seq,
250 };
251 let mut outputs = client.register(streams, OperationIr::Custom(desc), op);
252 let out = outputs.pop().expect("custom op declares one output");
253 to_dtype(
254 Tensor::from_primitive(TensorPrimitive::Float(out)),
255 in_dtype,
256 )
257 }
258
259 fn dims(&self) -> [usize; 2] {
260 CubeQuantLinear::dims(self)
261 }
262
263 fn vram_bytes(&self) -> usize {
264 self.w.vram_bytes()
265 }
266}
267
268fn cast_op<B: Backend, T: Backend>(op: Box<dyn QuantLinearOp<T>>) -> Option<Box<dyn QuantLinearOp<B>>> {
271 let any: Box<dyn Any> = Box::new(op);
272 any.downcast::<Box<dyn QuantLinearOp<B>>>().ok().map(|b| *b)
273}
274
275fn debug_quant(name: &str, outcome: &str) {
279 if std::env::var_os("COMBS_DEBUG_QUANT").is_some() {
280 eprintln!("quant-linear {name}: {outcome}");
281 }
282}
283
284pub fn try_quant_linear<B: Backend>(
285 source: &dyn ModelSource,
286 name: &str,
287 device: &Device<B>,
288) -> Result<Option<Box<dyn QuantLinearOp<B>>>> {
289 if std::env::var_os("COMBS_NO_QUANT_KERNELS").is_some_and(|v| v != "0") {
292 return Ok(None);
293 }
294 let supported = [
295 TypeId::of::<FusedF32>(),
296 TypeId::of::<UnfusedF32>(),
297 TypeId::of::<UnfusedF16>(),
298 ];
299 if !supported.contains(&TypeId::of::<B>()) {
300 debug_quant(name, "backend not wgpu f32/f16 — dense fallback");
301 return Ok(None);
302 }
303 let device_any: &dyn Any = device;
304 let Some(wgpu_device) = device_any.downcast_ref::<WgpuDevice>() else {
305 debug_quant(name, "device not WgpuDevice — dense fallback");
306 return Ok(None);
307 };
308 let Some(qt) = source.open_tensor_quant(name).map_err(ModelError::Format)? else {
309 debug_quant(name, "no packed quant tensor — dense fallback");
310 return Ok(None);
311 };
312 let &[n_out, k] = qt.shape.as_slice() else {
313 debug_quant(name, "not rank-2 — dense fallback");
314 return Ok(None);
315 };
316
317 let client = <WgpuRuntime as cubecl::prelude::Runtime>::client(wgpu_device);
318 let Ok(w) = QuantWeight::from_quant_tensor(&client, qt.format, &qt.data, n_out, k) else {
323 debug_quant(name, "kernel-incompatible shape — dense fallback");
324 return Ok(None);
325 };
326 debug_quant(name, "packed on device");
327 let lin = CubeQuantLinear { w: Arc::new(w) };
328
329 if TypeId::of::<B>() == TypeId::of::<FusedF32>() {
330 return Ok(cast_op::<B, FusedF32>(Box::new(lin)));
331 }
332 if TypeId::of::<B>() == TypeId::of::<UnfusedF32>() {
333 return Ok(cast_op::<B, UnfusedF32>(Box::new(lin)));
334 }
335 if TypeId::of::<B>() == TypeId::of::<UnfusedF16>() {
336 return Ok(cast_op::<B, UnfusedF16>(Box::new(lin)));
337 }
338 Ok(None)
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use burn::tensor::TensorData;
345 use combs_formats::QuantFormat;
346 use cubecl::prelude::Runtime;
347
348 fn synth_q4_0(n_blocks: usize) -> Vec<u8> {
350 let mut out = Vec::with_capacity(n_blocks * 18);
351 let mut s = 0x12345678u32;
352 for b in 0..n_blocks {
353 let scale = burn::tensor::f16::from_f32(0.003 * ((b % 11) as f32 + 1.0));
354 out.extend_from_slice(&scale.to_le_bytes());
355 for _ in 0..16 {
356 s = s.wrapping_mul(1664525).wrapping_add(1013904223);
357 out.push((s >> 24) as u8);
358 }
359 }
360 out
361 }
362
363 fn pin_device_dtypes() {
370 use std::sync::Once;
371 static PIN: Once = Once::new();
372 PIN.call_once(|| {
373 let device = WgpuDevice::default();
374 let _ = burn::tensor::set_default_dtypes::<UnfusedF32>(
375 &device,
376 FloatDType::F32,
377 burn::tensor::IntDType::I32,
378 );
379 });
380 }
381
382 fn quant_and_dense<B: Backend>(
383 device: &Device<B>,
384 n_out: usize,
385 k: usize,
386 dtype: FloatDType,
387 ) -> (Linear<B>, Linear<B>)
388 where
389 CubeQuantLinear: QuantLinearOp<B>,
390 {
391 let data = synth_q4_0(n_out * k / 32);
392 let client = <WgpuRuntime as Runtime>::client(&Default::default());
393 let w = Arc::new(
394 QuantWeight::from_quant_tensor(&client, QuantFormat::Q4_0, &data, n_out, k).unwrap(),
395 );
396 let quant = Linear::Quant(Box::new(CubeQuantLinear { w }) as Box<dyn QuantLinearOp<B>>);
397 let wf = combs_formats::quants::dequantize_q4_0(&data, n_out * k).unwrap();
398 let dense = Linear::Dense(
399 Tensor::<B, 2>::from_data(TensorData::new(wf, [n_out, k]), device).cast(dtype),
400 );
401 (quant, dense)
402 }
403
404 fn assert_close(got: &[f32], expect: &[f32], rel: f32) {
405 assert_eq!(got.len(), expect.len());
406 for (i, (g, e)) in got.iter().zip(expect.iter()).enumerate() {
407 let tol = rel * e.abs().max(1.0);
408 assert!((g - e).abs() <= tol, "[{i}]: got {g}, expect {e}");
409 }
410 }
411
412 #[test]
415 fn fused_backend_matches_dense() {
416 if crate::skip_no_gpu() {
417 return;
418 }
419 pin_device_dtypes();
420 let device: Device<FusedF32> = Default::default();
421 let (n_out, k) = (48, 64);
422 let (quant, dense) = quant_and_dense::<FusedF32>(&device, n_out, k, FloatDType::F32);
423 assert_eq!(quant.dims(), [n_out, k]);
424
425 let x: Vec<f32> = (0..3 * k).map(|i| ((i % 32) as f32) / 16.0 - 1.0).collect();
426 let x = Tensor::<FusedF32, 3>::from_data(TensorData::new(x, [1, 3, k]), &device)
427 .cast(FloatDType::F32);
428 let b: Vec<f32> = (0..n_out).map(|i| (i as f32) / 100.0).collect();
429 let bias = Tensor::<FusedF32, 1>::from_data(TensorData::new(b, [n_out]), &device)
430 .cast(FloatDType::F32);
431
432 let got: Vec<f32> = quant
433 .forward(x.clone(), Some(&bias))
434 .into_data()
435 .to_vec()
436 .unwrap();
437 let expect: Vec<f32> = dense
438 .forward(x, Some(&bias))
439 .into_data()
440 .to_vec()
441 .unwrap();
442 assert_close(&got, &expect, 1e-4);
443 }
444
445 #[test]
448 fn f16_backend_matches_dense() {
449 if crate::skip_no_gpu() {
450 return;
451 }
452 pin_device_dtypes();
453 let device: Device<UnfusedF16> = Default::default();
454 let (n_out, k) = (48, 64);
455 let (quant, dense) = quant_and_dense::<UnfusedF16>(&device, n_out, k, FloatDType::F16);
456
457 let x: Vec<f32> = (0..3 * k).map(|i| ((i % 32) as f32) / 16.0 - 1.0).collect();
458 let x = Tensor::<UnfusedF16, 3>::from_data(TensorData::new(x, [1, 3, k]), &device)
459 .cast(FloatDType::F16);
460
461 let got: Vec<f32> = quant
462 .forward(x.clone(), None)
463 .into_data()
464 .convert::<f32>()
465 .to_vec()
466 .unwrap();
467 let expect: Vec<f32> = dense
468 .forward(x, None)
469 .into_data()
470 .convert::<f32>()
471 .to_vec()
472 .unwrap();
473 assert_close(&got, &expect, 1e-2);
474 }
475}