1use crate::dit::Proj;
10use crate::pool::Pool;
11use cortiq_core::{CmfModel, TensorDtype};
12use std::sync::Arc;
13
14pub const DENSE_ROW_TILE: usize = 32;
19
20pub struct Linear {
26 repr: LinearRepr,
27}
28
29enum LinearRepr {
30 Mapped {
31 model: Arc<CmfModel>,
32 idx: usize,
33 dtype: TensorDtype,
34 rows: usize,
35 cols: usize,
36 },
37 Quant(Proj),
38}
39
40impl std::fmt::Debug for Linear {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.debug_struct("Linear")
43 .field("rows", &self.rows())
44 .field("cols", &self.cols())
45 .field("dtype", &self.dtype())
46 .finish()
47 }
48}
49
50impl Linear {
51 pub fn load(model: &Arc<CmfModel>, name: &str) -> Result<Self, String> {
54 let idx = model
55 .tensor_index(name)
56 .ok_or_else(|| format!("missing linear tensor '{name}'"))?;
57 let entry = &model.tensors[idx];
58 if entry.shape.len() != 2 {
59 return Err(format!(
60 "linear tensor '{name}' must be rank 2, got shape {:?}",
61 entry.shape
62 ));
63 }
64 let rows = entry.shape[0];
65 let cols = entry.shape[1];
66 match entry.dtype {
67 TensorDtype::F32 | TensorDtype::F16 | TensorDtype::Bf16 => Ok(Self {
68 repr: LinearRepr::Mapped {
69 model: model.clone(),
70 idx,
71 dtype: entry.dtype,
72 rows,
73 cols,
74 },
75 }),
76 TensorDtype::Q8Row
77 | TensorDtype::Q8_2f
78 | TensorDtype::Q4Block
79 | TensorDtype::Q4Tiled
80 | TensorDtype::Q4TiledP
81 | TensorDtype::Q2TiledP
82 | TensorDtype::Vbit
83 | TensorDtype::VbitRo
84 | TensorDtype::Q1
85 | TensorDtype::Q1S
86 | TensorDtype::Q1T => Ok(Self {
87 repr: LinearRepr::Quant(Proj::from_model(model, name)?),
88 }),
89 other => Err(format!(
90 "linear tensor '{name}' has unsupported dtype {}",
91 other.name()
92 )),
93 }
94 }
95
96 #[cfg(test)]
100 pub(crate) fn from_f32_for_test(weights: Vec<f32>, rows: usize, cols: usize) -> Self {
101 assert_eq!(weights.len(), rows * cols);
102 Self {
103 repr: LinearRepr::Quant(Proj::f32(weights, cols)),
104 }
105 }
106
107 pub fn rows(&self) -> usize {
108 match &self.repr {
109 LinearRepr::Mapped { rows, .. } => *rows,
110 LinearRepr::Quant(p) => p.rows(),
111 }
112 }
113
114 pub fn cols(&self) -> usize {
115 match &self.repr {
116 LinearRepr::Mapped { cols, .. } => *cols,
117 LinearRepr::Quant(p) => p.cols(),
118 }
119 }
120
121 pub fn dtype(&self) -> Option<TensorDtype> {
122 match &self.repr {
123 LinearRepr::Mapped { dtype, .. } => Some(*dtype),
124 LinearRepr::Quant(p) => match p {
125 Proj::F32 { .. } => Some(TensorDtype::F32),
126 Proj::Q(q) => q.model_dtype(),
127 },
128 }
129 }
130
131 pub(crate) fn mapped_device_gemm(&self) -> Option<(&Arc<CmfModel>, usize)> {
133 match &self.repr {
134 LinearRepr::Mapped { .. } => None,
135 LinearRepr::Quant(Proj::Q(q)) => q.mapped_device_gemm(),
136 LinearRepr::Quant(Proj::F32 { .. }) => None,
137 }
138 }
139
140 pub(crate) fn mapped_q4tp(&self) -> Option<(&Arc<CmfModel>, usize)> {
145 match &self.repr {
146 LinearRepr::Quant(Proj::Q(q)) => q.mapped_q4tp(),
147 LinearRepr::Mapped { .. } | LinearRepr::Quant(Proj::F32 { .. }) => None,
148 }
149 }
150
151 pub(crate) fn mapped_q4t(&self) -> Option<(&Arc<CmfModel>, usize)> {
154 match &self.repr {
155 LinearRepr::Quant(Proj::Q(q)) => q.mapped_q4t(),
156 LinearRepr::Mapped { .. } | LinearRepr::Quant(Proj::F32 { .. }) => None,
157 }
158 }
159
160 pub fn forward(
168 &self,
169 x: &[f32],
170 batch: usize,
171 out: &mut [f32],
172 pool: Option<&Pool>,
173 ) -> Result<(), String> {
174 let rows = self.rows();
175 let cols = self.cols();
176 if x.len() != batch.saturating_mul(cols) {
177 return Err(format!(
178 "linear input length {} != batch {batch} × cols {cols}",
179 x.len()
180 ));
181 }
182 if out.len() != batch.saturating_mul(rows) {
183 return Err(format!(
184 "linear output length {} != batch {batch} × rows {rows}",
185 out.len()
186 ));
187 }
188 match &self.repr {
189 LinearRepr::Quant(p) => {
190 p.matmat(x, batch, out, pool);
191 Ok(())
192 }
193 LinearRepr::Mapped {
194 model,
195 idx,
196 dtype,
197 rows,
198 cols,
199 } => {
200 let entry = &model.tensors[*idx];
201 let bytes = model.entry_bytes(entry);
202 let elem_bytes = match dtype {
203 TensorDtype::F32 => 4,
204 TensorDtype::F16 | TensorDtype::Bf16 => 2,
205 _ => unreachable!("mapped linear dtype is dense"),
206 };
207 let expected = rows
208 .checked_mul(*cols)
209 .and_then(|n| n.checked_mul(elem_bytes))
210 .ok_or_else(|| "linear byte-size overflow".to_string())?;
211 if bytes.len() != expected {
212 return Err(format!(
213 "linear tensor '{}' has {} bytes, expected {}",
214 entry.name,
215 bytes.len(),
216 expected
217 ));
218 }
219 let mut tile = vec![0.0f32; DENSE_ROW_TILE * *cols];
220 let mut tile_out = vec![0.0f32; batch * DENSE_ROW_TILE];
221 for base in (0..*rows).step_by(DENSE_ROW_TILE) {
222 let tile_rows = (*rows - base).min(DENSE_ROW_TILE);
223 for r in 0..tile_rows {
224 decode_dense_row(
225 bytes,
226 *dtype,
227 base + r,
228 *cols,
229 &mut tile[r * *cols..(r + 1) * *cols],
230 );
231 }
232 crate::fcd_ops::gemm_nt(
233 x,
234 &tile[..tile_rows * *cols],
235 &mut tile_out[..batch * tile_rows],
236 batch,
237 *cols,
238 tile_rows,
239 pool,
240 );
241 for b in 0..batch {
242 for r in 0..tile_rows {
243 out[b * *rows + base + r] = tile_out[b * tile_rows + r];
244 }
245 }
246 }
247 Ok(())
248 }
249 }
250 }
251
252 pub fn forward_one(
254 &self,
255 x: &[f32],
256 out: &mut [f32],
257 pool: Option<&Pool>,
258 ) -> Result<(), String> {
259 self.forward(x, 1, out, pool)
260 }
261}
262
263#[allow(clippy::too_many_arguments)]
272pub(crate) fn forward_qkv(
273 q: &Linear,
274 k: &Linear,
275 v: &Linear,
276 x: &[f32],
277 batch: usize,
278 q_out: &mut [f32],
279 k_out: &mut [f32],
280 v_out: &mut [f32],
281 pool: Option<&Pool>,
282) -> Result<(), String> {
283 let cols = q.cols();
284 let qrows = q.rows();
285 let krows = k.rows();
286 let vrows = v.rows();
287 if k.cols() != cols || v.cols() != cols {
288 return Err(format!(
289 "Qwen Image QKV input widths disagree: q={cols} k={} v={}",
290 k.cols(),
291 v.cols()
292 ));
293 }
294 let x_len = batch
295 .checked_mul(cols)
296 .ok_or_else(|| "Qwen Image QKV input size overflows".to_string())?;
297 let q_len = batch
298 .checked_mul(qrows)
299 .ok_or_else(|| "Qwen Image QKV query size overflows".to_string())?;
300 let k_len = batch
301 .checked_mul(krows)
302 .ok_or_else(|| "Qwen Image QKV key size overflows".to_string())?;
303 let v_len = batch
304 .checked_mul(vrows)
305 .ok_or_else(|| "Qwen Image QKV value size overflows".to_string())?;
306 if x.len() != x_len || q_out.len() != q_len || k_out.len() != k_len || v_out.len() != v_len {
307 return Err(format!(
308 "Qwen Image QKV buffers have x={} (expected {x_len}), q={} (expected {q_len}), k={} (expected {k_len}), v={} (expected {v_len})",
309 x.len(),
310 q_out.len(),
311 k_out.len(),
312 v_out.len()
313 ));
314 }
315
316 let wide = batch >= 32
321 && batch
322 .checked_mul(qrows)
323 .and_then(|n| n.checked_mul(cols))
324 .is_some_and(|work| work >= 128_000_000)
325 && crate::gpu::enabled_here()
326 && crate::gpu::mm_killed() == false
327 && std::env::var("CMF_QWEN_IMAGE_FUSED_QKV").as_deref() == Ok("1");
330
331 if wide {
332 if let (Some((qm, qi)), Some((km, ki)), Some((vm, vi))) =
333 (q.mapped_q4tp(), k.mapped_q4tp(), v.mapped_q4tp())
334 {
335 if qm.uid() == km.uid() && qm.uid() == vm.uid() && krows == vrows {
336 if crate::gpu::dit_qkv(
337 qm, qi, ki, vi, x, batch, cols, qrows, krows, q_out, k_out, v_out,
338 ) {
339 return Ok(());
340 }
341 }
342 }
343
344 if let (Some((qm, qi)), Some((km, ki)), Some((vm, vi))) =
345 (q.mapped_q4t(), k.mapped_q4t(), v.mapped_q4t())
346 {
347 if qm.uid() == km.uid() && qm.uid() == vm.uid() {
348 let packed_len = batch
349 .checked_mul(
350 qrows
351 .checked_add(krows)
352 .and_then(|n| n.checked_add(vrows))
353 .ok_or_else(|| "Qwen Image fused QKV size overflows".to_string())?,
354 )
355 .ok_or_else(|| "Qwen Image fused QKV size overflows".to_string())?;
356 let mut packed = vec![0.0f32; packed_len];
357 if crate::gpu::q4t_qkv(
358 qm,
359 qi,
360 ki,
361 vi,
362 x,
363 batch,
364 cols,
365 qrows,
366 krows,
367 vrows,
368 &mut packed,
369 ) {
370 q_out.copy_from_slice(&packed[..q_len]);
371 k_out.copy_from_slice(&packed[q_len..q_len + k_len]);
372 v_out.copy_from_slice(&packed[q_len + k_len..]);
373 return Ok(());
374 }
375 }
376 }
377 }
378
379 q.forward(x, batch, q_out, pool)?;
380 k.forward(x, batch, k_out, pool)?;
381 v.forward(x, batch, v_out, pool)?;
382 Ok(())
383}
384
385fn decode_dense_row(bytes: &[u8], dtype: TensorDtype, row: usize, cols: usize, dst: &mut [f32]) {
386 debug_assert_eq!(dst.len(), cols);
387 let width = match dtype {
388 TensorDtype::F32 => 4,
389 TensorDtype::F16 | TensorDtype::Bf16 => 2,
390 _ => unreachable!("decode_dense_row only accepts dense dtypes"),
391 };
392 let start = row * cols * width;
393 let src = &bytes[start..start + cols * width];
394 for i in 0..cols {
395 let off = i * width;
396 dst[i] = match dtype {
397 TensorDtype::F32 => f32::from_le_bytes(src[off..off + 4].try_into().unwrap()),
398 TensorDtype::F16 => {
399 cortiq_core::quant::f16_to_f32(u16::from_le_bytes([src[off], src[off + 1]]))
400 }
401 TensorDtype::Bf16 => {
402 cortiq_core::quant::bf16_to_f32(u16::from_le_bytes([src[off], src[off + 1]]))
403 }
404 _ => unreachable!(),
405 };
406 }
407}
408
409pub(crate) fn add_bias(rows: &mut [f32], batch: usize, bias: &[f32]) -> Result<(), String> {
411 if rows.len() != batch.saturating_mul(bias.len()) {
412 return Err(format!(
413 "bias add length {} != batch {batch} × bias {}",
414 rows.len(),
415 bias.len()
416 ));
417 }
418 for row in rows.chunks_exact_mut(bias.len()) {
419 for (v, &b) in row.iter_mut().zip(bias) {
420 *v += b;
421 }
422 }
423 Ok(())
424}
425
426#[cfg(test)]
427mod tests {
428 use super::Linear;
429
430 #[test]
431 fn f32_fixture_projection_is_bounded_api_compatible() {
432 let p = Linear::from_f32_for_test(vec![1.0, 2.0, 3.0, 4.0], 2, 2);
433 let mut out = [0.0; 2];
434 p.forward_one(&[2.0, -1.0], &mut out, None).unwrap();
435 assert_eq!(out, [0.0, 2.0]);
436 }
437
438 #[test]
439 fn bias_add_rejects_wrong_batch_shape() {
440 assert!(super::add_bias(&mut [0.0; 3], 2, &[1.0, 2.0]).is_err());
441 }
442}