1use mdarray::{Array, Dim, DynRank, Layout, Shape, Slice};
2use mdarray_linalg::contract::{Axes, Contract, ContractBuilder, MatmulBuilder};
3use num_traits::{MulAdd, One, Zero};
4use tblis::{
5 containers::TblisTensor,
6 einsum_impl::tblis_einsum,
7 float_trait::TblisFloatAPI,
8 tensor_ops::{TblisMultCfgBuilder, tblis_tensor_mult},
9};
10
11use crate::Tblis;
12
13struct TblisMatmulBuilder<'a, T, D0, D1, D2, La, Lb>
14where
15 La: Layout,
16 Lb: Layout,
17 D0: Dim,
18 D1: Dim,
19 D2: Dim,
20{
21 alpha: T,
22 a: &'a Slice<T, (D0, D1), La>,
23 b: &'a Slice<T, (D1, D2), Lb>,
24}
25
26struct TblisContractBuilder<'a, T, Sa, Sb, La, Lb>
27where
28 La: Layout,
29 Lb: Layout,
30 Sa: Shape,
31 Sb: Shape,
32{
33 alpha: T,
34 a: &'a Slice<T, Sa, La>,
35 b: &'a Slice<T, Sb, Lb>,
36 mode: ContractMode<'a>,
37}
38
39enum ContractMode<'a> {
40 Structured { axes: Axes<'a> },
41 Einsum {
42 indices_a: &'a [u8],
43 indices_b: &'a [u8],
44 indices_c: &'a [u8],
45 },
46}
47
48impl<'a, T, D0, D1, D2, La, Lb> MatmulBuilder<'a, T, D0, D1, D2, La, Lb>
49 for TblisMatmulBuilder<'a, T, D0, D1, D2, La, Lb>
50where
51 La: Layout,
52 Lb: Layout,
53 T: TblisFloatAPI + Zero + One,
54 D0: Dim,
55 D1: Dim,
56 D2: Dim,
57{
58 fn scale(mut self, factor: T) -> Self {
59 self.alpha = self.alpha * factor;
60 self
61 }
62
63 fn eval(self) -> Array<T, (D0, D2)> {
64 let (m, _) = *self.a.shape();
65 let (_, n) = *self.b.shape();
66 let mut c = Array::from_elem((m, n), T::zero());
67 self.write(&mut c);
68 c
69 }
70
71 fn write<Lc: Layout>(self, c: &mut Slice<T, (D0, D2), Lc>) {
72 self.add_to_scaled(c, T::zero())
73 }
74
75 fn add_to<Lc: Layout>(self, c: &mut Slice<T, (D0, D2), Lc>) {
76 self.add_to_scaled(c, T::one())
77 }
78
79 fn add_to_scaled<Lc: Layout>(self, c: &mut Slice<T, (D0, D2), Lc>, beta: T) {
80 assert_eq!(self.a.dim(1), self.b.dim(0), "matrix inner dimensions must match");
81 assert_eq!(c.dim(0), self.a.dim(0), "output row count mismatch");
82 assert_eq!(c.dim(1), self.b.dim(1), "output column count mismatch");
83
84 let a_t = slice_to_tblis_tensor(self.a);
85 let b_t = slice_to_tblis_tensor(self.b);
86 let mut c_t = slice_to_tblis_tensor_mut(c);
87 let cfg = TblisMultCfgBuilder::default()
88 .alpha(self.alpha)
89 .beta(beta)
90 .build()
91 .unwrap();
92
93 unsafe {
94 tblis_tensor_mult(&a_t, "ik", &b_t, "kj", &mut c_t, "ij", Some(cfg));
95 }
96 }
97}
98
99impl<'a, T, Sa, Sb, La, Lb> ContractBuilder<'a, T, Sa, Sb, La, Lb>
100 for TblisContractBuilder<'a, T, Sa, Sb, La, Lb>
101where
102 La: Layout,
103 Lb: Layout,
104 T: TblisFloatAPI + Zero + One + MulAdd<Output = T>,
105 Sa: Shape,
106 Sb: Shape,
107{
108 fn scale(mut self, factor: T) -> Self {
109 self.alpha = self.alpha * factor;
110 self
111 }
112
113 fn eval(self) -> Array<T, DynRank> {
114 match &self.mode {
115 ContractMode::Structured { axes } => {
116 let (indices_a, indices_b, indices_c, shape_c) =
117 build_structured_subscripts(self.a, self.b, axes);
118 let mut c = Array::from_elem(shape_c, T::zero());
119 self.run_structured_into(&indices_a, &indices_b, &indices_c, &mut c, T::zero());
120 c
121 }
122 ContractMode::Einsum {
123 indices_a,
124 indices_b,
125 indices_c,
126 } => self.eval_einsum(indices_a, indices_b, indices_c),
127 }
128 }
129
130 fn write<Sc: Shape, Lc: Layout>(self, c: &mut Slice<T, Sc, Lc>) {
131 match &self.mode {
132 ContractMode::Structured { axes } => {
133 let (indices_a, indices_b, indices_c, shape_c) =
134 build_structured_subscripts(self.a, self.b, axes);
135 assert_output_shape(c, &shape_c);
136 self.run_structured_into(&indices_a, &indices_b, &indices_c, c, T::zero());
137 }
138 ContractMode::Einsum {
139 indices_a,
140 indices_b,
141 indices_c,
142 } => {
143 let result = self.eval_einsum(indices_a, indices_b, indices_c);
144 copy_result(c, &result);
145 }
146 }
147 }
148
149 fn add_to<Sc: Shape, Lc: Layout>(self, c: &mut Slice<T, Sc, Lc>) {
150 self.add_to_scaled(c, T::one())
151 }
152
153 fn add_to_scaled<Sc: Shape, Lc: Layout>(self, c: &mut Slice<T, Sc, Lc>, beta: T) {
154 match &self.mode {
155 ContractMode::Structured { axes } => {
156 let (indices_a, indices_b, indices_c, shape_c) =
157 build_structured_subscripts(self.a, self.b, axes);
158 assert_output_shape(c, &shape_c);
159 self.run_structured_into(&indices_a, &indices_b, &indices_c, c, beta);
160 }
161 ContractMode::Einsum {
162 indices_a,
163 indices_b,
164 indices_c,
165 } => {
166 let result = self.eval_einsum(indices_a, indices_b, indices_c);
167 add_result(c, &result, beta);
168 }
169 }
170 }
171}
172
173impl<'a, T, Sa, Sb, La, Lb> TblisContractBuilder<'a, T, Sa, Sb, La, Lb>
174where
175 La: Layout,
176 Lb: Layout,
177 T: TblisFloatAPI + Zero + One + MulAdd<Output = T>,
178 Sa: Shape,
179 Sb: Shape,
180{
181 fn run_structured_into<Sc: Shape, Lc: Layout>(
182 &self,
183 indices_a: &str,
184 indices_b: &str,
185 indices_c: &str,
186 c: &mut Slice<T, Sc, Lc>,
187 beta: T,
188 ) {
189 let a_t = slice_to_tblis_tensor(self.a);
190 let b_t = slice_to_tblis_tensor(self.b);
191 let mut c_t = slice_to_tblis_tensor_mut(c);
192 let cfg = TblisMultCfgBuilder::default()
193 .alpha(self.alpha)
194 .beta(beta)
195 .build()
196 .unwrap();
197
198 unsafe {
199 tblis_tensor_mult(&a_t, indices_a, &b_t, indices_b, &mut c_t, indices_c, Some(cfg));
200 }
201 }
202
203 fn eval_einsum(&self, indices_a: &[u8], indices_b: &[u8], indices_c: &[u8]) -> Array<T, DynRank> {
204 assert_eq!(indices_a.len(), self.a.rank(), "einsum indices_a length must match A rank");
205 assert_eq!(indices_b.len(), self.b.rank(), "einsum indices_b length must match B rank");
206
207 let subscripts = build_einsum_subscripts(indices_a, indices_b, indices_c);
208 let a_t = slice_to_tblis_tensor(self.a);
209 let b_t = slice_to_tblis_tensor(self.b);
210 let operands = [&a_t, &b_t];
211
212 let (vec, tsr) = unsafe {
213 tblis_einsum(&subscripts, &operands, "optimal", None, true, None)
214 .expect("tblis_einsum must allocate an output tensor")
215 };
216
217 let shape: Vec<usize> = tsr.shape.iter().map(|&d| d as usize).collect();
218 let mut result = Array::from_elem(shape, T::zero());
219 for (dst, src) in result.iter_mut().zip(vec) {
220 *dst = self.alpha * src;
221 }
222 result
223 }
224}
225
226impl<T> Contract<T> for Tblis
227where
228 T: TblisFloatAPI + Zero + One + MulAdd<Output = T>,
229{
230 fn matmul<'a, D0, D1, D2, La, Lb>(
231 &self,
232 a: &'a Slice<T, (D0, D1), La>,
233 b: &'a Slice<T, (D1, D2), Lb>,
234 ) -> impl MatmulBuilder<'a, T, D0, D1, D2, La, Lb>
235 where
236 La: Layout,
237 Lb: Layout,
238 D0: Dim,
239 D1: Dim,
240 D2: Dim,
241 {
242 TblisMatmulBuilder {
243 alpha: T::one(),
244 a,
245 b,
246 }
247 }
248
249 fn contract_all<'a, Sa, Sb, La, Lb>(
250 &self,
251 a: &'a Slice<T, Sa, La>,
252 b: &'a Slice<T, Sb, Lb>,
253 ) -> T
254 where
255 T: 'a,
256 Sa: Shape,
257 Sb: Shape,
258 La: Layout,
259 Lb: Layout,
260 {
261 assert_eq!(
262 a.rank(),
263 b.rank(),
264 "contract_all requires tensors with the same rank (got ranks {} and {})",
265 a.rank(),
266 b.rank()
267 );
268
269 self.contract_n(a, b, a.rank()).eval().into_scalar()
270 }
271
272 fn contract_n<'a, Sa, Sb, La, Lb>(
273 &self,
274 a: &'a Slice<T, Sa, La>,
275 b: &'a Slice<T, Sb, Lb>,
276 n: usize,
277 ) -> impl ContractBuilder<'a, T, Sa, Sb, La, Lb>
278 where
279 T: 'a,
280 Sa: Shape,
281 Sb: Shape,
282 La: Layout,
283 Lb: Layout,
284 {
285 TblisContractBuilder {
286 alpha: T::one(),
287 a,
288 b,
289 mode: ContractMode::Structured {
290 axes: Axes::LastFirst { k: n },
291 },
292 }
293 }
294
295 fn contract_pairs<'a, Sa, Sb, La, Lb>(
296 &self,
297 a: &'a Slice<T, Sa, La>,
298 b: &'a Slice<T, Sb, Lb>,
299 axes_a: &'a [usize],
300 axes_b: &'a [usize],
301 ) -> impl ContractBuilder<'a, T, Sa, Sb, La, Lb>
302 where
303 T: 'a,
304 Sa: Shape,
305 Sb: Shape,
306 La: Layout,
307 Lb: Layout,
308 {
309 TblisContractBuilder {
310 alpha: T::one(),
311 a,
312 b,
313 mode: ContractMode::Structured {
314 axes: Axes::Specific(axes_a, axes_b),
315 },
316 }
317 }
318
319 fn contract<'a, Sa, Sb, La, Lb>(
320 &self,
321 a: &'a Slice<T, Sa, La>,
322 b: &'a Slice<T, Sb, Lb>,
323 indices_a: &'a [u8],
324 indices_b: &'a [u8],
325 indices_c: &'a [u8],
326 ) -> impl ContractBuilder<'a, T, Sa, Sb, La, Lb>
327 where
328 T: 'a,
329 Sa: Shape,
330 Sb: Shape,
331 La: Layout,
332 Lb: Layout,
333 {
334 TblisContractBuilder {
335 alpha: T::one(),
336 a,
337 b,
338 mode: ContractMode::Einsum {
339 indices_a,
340 indices_b,
341 indices_c,
342 },
343 }
344 }
345}
346
347fn slice_to_tblis_tensor<T, S, L>(slice: &Slice<T, S, L>) -> TblisTensor<T>
348where
349 T: TblisFloatAPI,
350 S: Shape,
351 L: Layout,
352{
353 let shape: Vec<isize> = (0..slice.rank()).map(|i| slice.dim(i) as isize).collect();
354 let stride: Vec<isize> = (0..slice.rank()).map(|i| slice.stride(i)).collect();
355 TblisTensor::new(slice.as_ptr() as *mut T, &shape, &stride)
356}
357
358fn slice_to_tblis_tensor_mut<T, S, L>(slice: &mut Slice<T, S, L>) -> TblisTensor<T>
359where
360 T: TblisFloatAPI,
361 S: Shape,
362 L: Layout,
363{
364 let shape: Vec<isize> = (0..slice.rank()).map(|i| slice.dim(i) as isize).collect();
365 let stride: Vec<isize> = (0..slice.rank()).map(|i| slice.stride(i)).collect();
366 TblisTensor::new(slice.as_mut_ptr(), &shape, &stride)
367}
368
369fn resolve_axes<T, Sa, Sb, La, Lb>(
370 axes: &Axes<'_>,
371 a: &Slice<T, Sa, La>,
372 b: &Slice<T, Sb, Lb>,
373) -> (Vec<usize>, Vec<usize>)
374where
375 Sa: Shape,
376 Sb: Shape,
377 La: Layout,
378 Lb: Layout,
379{
380 let rank_a = a.rank();
381 let rank_b = b.rank();
382 match axes {
383 Axes::All => {
384 assert_eq!(rank_a, rank_b, "full contraction requires equal ranks");
385 ((0..rank_a).collect(), (0..rank_b).collect())
386 }
387 Axes::LastFirst { k } => {
388 assert!(*k <= rank_a, "cannot contract {k} axes on A of rank {rank_a}");
389 assert!(*k <= rank_b, "cannot contract {k} axes on B of rank {rank_b}");
390 (((rank_a - *k)..rank_a).collect(), (0..*k).collect())
391 }
392 Axes::Specific(ax_a, ax_b) => (ax_a.to_vec(), ax_b.to_vec()),
393 Axes::SpecificOwned(ax_a, ax_b) => (ax_a.clone(), ax_b.clone()),
394 }
395}
396
397fn build_structured_subscripts<T, Sa, Sb, La, Lb>(
398 a: &Slice<T, Sa, La>,
399 b: &Slice<T, Sb, Lb>,
400 axes: &Axes<'_>,
401) -> (String, String, String, Vec<usize>)
402where
403 Sa: Shape,
404 Sb: Shape,
405 La: Layout,
406 Lb: Layout,
407{
408 let (axes_a, axes_b) = resolve_axes(axes, a, b);
409 assert_eq!(axes_a.len(), axes_b.len(), "axis count mismatch");
410
411 let rank_a = a.rank();
412 let rank_b = b.rank();
413 let mut labels_a = vec![usize::MAX; rank_a];
414 let mut labels_b = vec![usize::MAX; rank_b];
415 let mut next_label = 0usize;
416
417 for (&ax_a, &ax_b) in axes_a.iter().zip(axes_b.iter()) {
418 assert!(ax_a < rank_a, "axis {ax_a} out of bounds for A rank {rank_a}");
419 assert!(ax_b < rank_b, "axis {ax_b} out of bounds for B rank {rank_b}");
420 assert_eq!(a.dim(ax_a), b.dim(ax_b), "dimension mismatch on contracted axes");
421 assert_eq!(labels_a[ax_a], usize::MAX, "duplicate contracted axis in A");
422 assert_eq!(labels_b[ax_b], usize::MAX, "duplicate contracted axis in B");
423 labels_a[ax_a] = next_label;
424 labels_b[ax_b] = next_label;
425 next_label += 1;
426 }
427
428 let mut output_labels = Vec::new();
429 let mut output_shape = Vec::new();
430
431 for (ax, label) in labels_a.iter_mut().enumerate() {
432 if *label == usize::MAX {
433 *label = next_label;
434 output_labels.push(next_label);
435 output_shape.push(a.dim(ax));
436 next_label += 1;
437 }
438 }
439
440 for (ax, label) in labels_b.iter_mut().enumerate() {
441 if *label == usize::MAX {
442 *label = next_label;
443 output_labels.push(next_label);
444 output_shape.push(b.dim(ax));
445 next_label += 1;
446 }
447 }
448
449 let idx_a = labels_to_subscript(&labels_a);
450 let idx_b = labels_to_subscript(&labels_b);
451 let idx_c = labels_to_subscript(&output_labels);
452
453 (idx_a, idx_b, idx_c, output_shape)
454}
455
456fn build_einsum_subscripts(indices_a: &[u8], indices_b: &[u8], indices_c: &[u8]) -> String {
457 let mut unique = Vec::<u8>::new();
458 for &label in indices_a.iter().chain(indices_b.iter()).chain(indices_c.iter()) {
459 if !unique.contains(&label) {
460 unique.push(label);
461 }
462 }
463 assert!(unique.len() <= 128, "TBLIS backend supports at most 128 distinct einsum labels");
464
465 let idx_a = labels_to_subscript(
466 &indices_a
467 .iter()
468 .map(|label| unique.iter().position(|x| x == label).unwrap())
469 .collect::<Vec<_>>(),
470 );
471 let idx_b = labels_to_subscript(
472 &indices_b
473 .iter()
474 .map(|label| unique.iter().position(|x| x == label).unwrap())
475 .collect::<Vec<_>>(),
476 );
477 let idx_c = labels_to_subscript(
478 &indices_c
479 .iter()
480 .map(|label| unique.iter().position(|x| x == label).unwrap())
481 .collect::<Vec<_>>(),
482 );
483
484 format!("{idx_a},{idx_b}->{idx_c}")
485}
486
487fn labels_to_subscript(labels: &[usize]) -> String {
488 labels
489 .iter()
490 .map(|&label| {
491 let code = 0x0100_u32 + label as u32;
492 char::from_u32(code).expect("invalid generated label")
493 })
494 .collect()
495}
496
497fn assert_output_shape<T, S, L>(c: &Slice<T, S, L>, expected: &[usize])
498where
499 S: Shape,
500 L: Layout,
501{
502 assert_eq!(c.rank(), expected.len(), "output rank mismatch");
503 for (i, &dim) in expected.iter().enumerate() {
504 assert_eq!(c.dim(i), dim, "output shape mismatch on axis {i}");
505 }
506}
507
508fn copy_result<T, Sc, Lc>(c: &mut Slice<T, Sc, Lc>, result: &Array<T, DynRank>)
509where
510 T: Copy,
511 Sc: Shape,
512 Lc: Layout,
513{
514 assert_output_shape(c, result.shape().dims());
515 for (dst, src) in c.iter_mut().zip(result.iter()) {
516 *dst = *src;
517 }
518}
519
520fn add_result<T, Sc, Lc>(c: &mut Slice<T, Sc, Lc>, result: &Array<T, DynRank>, beta: T)
521where
522 T: Copy + MulAdd<Output = T>,
523 Sc: Shape,
524 Lc: Layout,
525{
526 assert_output_shape(c, result.shape().dims());
527 for (dst, src) in c.iter_mut().zip(result.iter()) {
528 *dst = beta.mul_add(*dst, *src);
529 }
530}