1use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
4
5use crate::{
6 builder::{check_tensors, BuildOptions},
7 linear_algebra_substrate::MatmulBias,
8 region::{tag_program, wrap_anonymous},
9 tensor_ref::{TensorRef, TensorRefError},
10};
11
12use super::tiled::{linear_tiled, LINEAR_TILED_MIN_WORK, LINEAR_TILED_TILE};
13
14pub(super) const LINEAR_OP_ID: &str = "vyre-libs::nn::linear";
15#[derive(Debug, Clone)]
17pub struct Linear {
18 x: TensorRef,
19 w: TensorRef,
20 b: TensorRef,
21 out: TensorRef,
22 options: BuildOptions,
23}
24
25impl Linear {
26 #[must_use]
28 pub fn new(x: TensorRef, w: TensorRef, b: TensorRef, out: TensorRef) -> Self {
29 Self {
30 x,
31 w,
32 b,
33 out,
34 options: BuildOptions::default(),
35 }
36 }
37
38 pub fn build(self) -> Result<Program, TensorRefError> {
45 check_tensors(
46 LINEAR_OP_ID,
47 &[
48 (&self.x, DataType::U32),
49 (&self.w, DataType::U32),
50 (&self.b, DataType::U32),
51 (&self.out, DataType::U32),
52 ],
53 )?;
54 let x_shape = self.x.shape.as_ref();
55 let w_shape = self.w.shape.as_ref();
56 let b_shape = self.b.shape.as_ref();
57 let out_shape = self.out.shape.as_ref();
58 let expected_w = match x_shape {
59 [in_dim] => match out_shape {
60 [out_dim] => vec![*in_dim, *out_dim],
61 _ => vec![],
62 },
63 _ => vec![],
64 };
65 if w_shape != expected_w.as_slice() {
66 return Err(TensorRefError::ShapeMismatch {
67 name: self.w.name_str().to_string(),
68 found: self.w.shape.to_vec(),
69 expected: expected_w,
70 op: LINEAR_OP_ID,
71 });
72 }
73 if b_shape != out_shape {
74 return Err(TensorRefError::ShapeMismatch {
75 name: self.b.name_str().to_string(),
76 found: self.b.shape.to_vec(),
77 expected: self.out.shape.to_vec(),
78 op: LINEAR_OP_ID,
79 });
80 }
81 let &[in_dim] = x_shape else {
82 return Err(TensorRefError::ShapeMismatch {
83 name: self.x.name_str().to_string(),
84 found: self.x.shape.to_vec(),
85 expected: vec![1],
86 op: LINEAR_OP_ID,
87 });
88 };
89 let &[out_dim] = out_shape else {
90 return Err(TensorRefError::ShapeMismatch {
91 name: self.out.name_str().to_string(),
92 found: self.out.shape.to_vec(),
93 expected: vec![1],
94 op: LINEAR_OP_ID,
95 });
96 };
97 if in_dim == 0 {
98 return Err(TensorRefError::ShapeMismatch {
99 name: self.x.name_str().to_string(),
100 found: self.x.shape.to_vec(),
101 expected: vec![1],
102 op: LINEAR_OP_ID,
103 });
104 }
105 if out_dim == 0 {
106 return Err(TensorRefError::ShapeMismatch {
107 name: self.out.name_str().to_string(),
108 found: self.out.shape.to_vec(),
109 expected: vec![1],
110 op: LINEAR_OP_ID,
111 });
112 }
113 build_linear_program(
114 self.x.name_str(),
115 self.w.name_str(),
116 self.b.name_str(),
117 self.out.name_str(),
118 in_dim,
119 out_dim,
120 self.options,
121 )
122 .map_err(|_| TensorRefError::ElementCountOverflow {
123 name: self.w.name_str().to_string(),
124 shape: self.w.shape.to_vec(),
125 })
126 }
127}
128
129crate::builder::impl_cat_a_builder_options!(Linear);
130
131pub fn linear(
140 x: &str,
141 w: &str,
142 b: &str,
143 out: &str,
144 in_dim: u32,
145 out_dim: u32,
146) -> Result<Program, String> {
147 if in_dim
148 .checked_mul(out_dim)
149 .is_some_and(|work| work >= LINEAR_TILED_MIN_WORK)
150 {
151 return linear_tiled(x, w, b, out, in_dim, out_dim, LINEAR_TILED_TILE);
152 }
153
154 Linear::new(
155 TensorRef::u32_1d(x, in_dim),
156 TensorRef::u32_2d(w, in_dim, out_dim),
157 TensorRef::u32_1d(b, out_dim),
158 TensorRef::u32_1d(out, out_dim),
159 )
160 .build()
161 .map_err(|error| format!("Fix: {LINEAR_OP_ID} build failed: {error}"))
162}
163
164pub fn linear_rows(
173 x: &str,
174 w: &str,
175 b: &str,
176 out: &str,
177 rows: u32,
178 in_dim: u32,
179 out_dim: u32,
180) -> Result<Program, String> {
181 linear_rows_impl(
182 x,
183 w,
184 Some(b),
185 out,
186 rows,
187 in_dim,
188 out_dim,
189 DataType::F32,
190 false,
191 )
192}
193
194pub fn linear_rows_no_bias(
203 x: &str,
204 w: &str,
205 out: &str,
206 rows: u32,
207 in_dim: u32,
208 out_dim: u32,
209) -> Result<Program, String> {
210 linear_rows_impl(x, w, None, out, rows, in_dim, out_dim, DataType::F32, false)
211}
212
213#[allow(clippy::too_many_arguments)]
220pub fn linear_rows_no_bias_typed(
221 x: &str,
222 w: &str,
223 out: &str,
224 rows: u32,
225 in_dim: u32,
226 out_dim: u32,
227 dtype: DataType,
228) -> Result<Program, String> {
229 linear_rows_impl(x, w, None, out, rows, in_dim, out_dim, dtype, false)
230}
231
232#[allow(clippy::too_many_arguments)]
241pub fn linear_rows_no_bias_out_in_typed(
242 x: &str,
243 w: &str,
244 out: &str,
245 rows: u32,
246 in_dim: u32,
247 out_dim: u32,
248 dtype: DataType,
249) -> Result<Program, String> {
250 linear_rows_impl(x, w, None, out, rows, in_dim, out_dim, dtype, true)
251}
252
253fn linear_rows_impl(
254 x: &str,
255 w: &str,
256 bias: Option<&str>,
257 out: &str,
258 rows: u32,
259 in_dim: u32,
260 out_dim: u32,
261 dtype: DataType,
262 weight_out_in: bool,
263) -> Result<Program, String> {
264 if rows == 0 || in_dim == 0 || out_dim == 0 {
265 return Err(
266 "Fix: linear_rows requires nonzero rows, input dimension, and output dimension"
267 .to_string(),
268 );
269 }
270 if !matches!(dtype, DataType::F16 | DataType::BF16 | DataType::F32) {
271 return Err(format!(
272 "Fix: linear_rows supports F16, BF16, or F32 tensors; got {dtype:?}"
273 ));
274 }
275 rows.checked_mul(in_dim).ok_or_else(|| {
276 "Fix: linear_rows rows*in_dim overflows u32; split the row batch".to_string()
277 })?;
278 rows.checked_mul(out_dim).ok_or_else(|| {
279 "Fix: linear_rows rows*out_dim overflows u32; split the row batch".to_string()
280 })?;
281 in_dim.checked_mul(out_dim).ok_or_else(|| {
282 "Fix: linear_rows in_dim*out_dim overflows u32; shard the projection".to_string()
283 })?;
284 let input_count = rows * in_dim;
285 let output_count = rows * out_dim;
286 let weight_count = in_dim * out_dim;
287 let index = Expr::var("index");
288 let row = Expr::div(index.clone(), Expr::u32(out_dim));
289 let column = Expr::rem(index.clone(), Expr::u32(out_dim));
290 let accumulator = bias.map_or_else(
291 || Expr::f32(0.0),
292 |name| Expr::cast(DataType::F32, Expr::load(name, Expr::var("column"))),
293 );
294 let weight_index = if weight_out_in {
295 Expr::add(
296 Expr::mul(Expr::var("column"), Expr::u32(in_dim)),
297 Expr::var("inner"),
298 )
299 } else {
300 Expr::add(
301 Expr::mul(Expr::var("inner"), Expr::u32(out_dim)),
302 Expr::var("column"),
303 )
304 };
305 let body = vec![
306 Node::let_bind("index", Expr::InvocationId { axis: 0 }),
307 Node::if_then(
308 Expr::lt(index.clone(), Expr::u32(output_count)),
309 vec![
310 Node::let_bind("row", row),
311 Node::let_bind("column", column),
312 Node::let_bind("accumulator", accumulator),
313 Node::loop_for(
314 "inner",
315 Expr::u32(0),
316 Expr::u32(in_dim),
317 vec![Node::assign(
318 "accumulator",
319 Expr::add(
320 Expr::var("accumulator"),
321 Expr::mul(
322 Expr::cast(
323 DataType::F32,
324 Expr::load(
325 x,
326 Expr::add(
327 Expr::mul(Expr::var("row"), Expr::u32(in_dim)),
328 Expr::var("inner"),
329 ),
330 ),
331 ),
332 Expr::cast(DataType::F32, Expr::load(w, weight_index.clone())),
333 ),
334 ),
335 )],
336 ),
337 Node::Store {
338 buffer: out.into(),
339 index,
340 value: Expr::cast(dtype.clone(), Expr::var("accumulator")),
341 },
342 ],
343 ),
344 ];
345 let mut buffers = vec![
346 BufferDecl::storage(x, 0, BufferAccess::ReadOnly, dtype.clone()).with_count(input_count),
347 BufferDecl::storage(w, 1, BufferAccess::ReadOnly, dtype.clone()).with_count(weight_count),
348 ];
349 if let Some(name) = bias {
350 buffers.push(
351 BufferDecl::storage(name, 2, BufferAccess::ReadOnly, dtype.clone()).with_count(out_dim),
352 );
353 }
354 let output_slot = if bias.is_some() { 3 } else { 2 };
355 buffers.push(BufferDecl::output(out, output_slot, dtype).with_count(output_count));
356 Ok(Program::wrapped(
357 buffers,
358 [64, 1, 1],
359 vec![wrap_anonymous("vyre-libs::nn::linear_rows", body)],
360 ))
361}
362
363fn build_linear_program(
364 x: &str,
365 w: &str,
366 b: &str,
367 out: &str,
368 in_dim: u32,
369 out_dim: u32,
370 options: BuildOptions,
371) -> Result<Program, String> {
372 in_dim.checked_mul(out_dim).ok_or_else(|| {
373 "Fix: linear in_dim*out_dim overflows u32; reduce dimensions.".to_string()
374 })?;
375 let mut builder = MatmulBias::new(
376 TensorRef::u32_2d(x, 1, in_dim),
377 TensorRef::u32_2d(w, in_dim, out_dim),
378 TensorRef::u32_1d(b, out_dim),
379 TensorRef::u32_2d(out, 1, out_dim),
380 );
381 if let Some(workgroup_size) = options.workgroup_size {
382 builder = builder.with_workgroup_size(workgroup_size);
383 }
384 let program = builder
385 .build()
386 .map_err(|error| format!("Fix: linear matmul_bias build failed: {error}"))?;
387 Ok(tag_program(
388 options.region_generator.unwrap_or(LINEAR_OP_ID),
389 program,
390 ))
391}