Skip to main content

tropical_gemm/core/
gemm.rs

1use super::argmax::GemmWithArgmax;
2use super::kernel::{Microkernel, MicrokernelWithArgmax, PortableMicrokernel};
3use super::packing::{pack_a, pack_b, packed_a_size, packed_b_size, Layout, Transpose};
4use super::tiling::{BlockIterator, TilingParams};
5use super::workspace::{GemmWorkspace, PackingBuffers};
6use crate::types::{TropicalSemiring, TropicalWithArgmax};
7
8#[cfg(feature = "parallel")]
9use rayon::prelude::*;
10
11/// Tropical GEMM: C = A ⊗ B
12///
13/// Computes C[i,j] = ⊕_k (A[i,k] ⊗ B[k,j])
14///
15/// This is a portable (non-SIMD) implementation using BLIS-style blocking
16/// for cache efficiency.
17///
18/// # Parameters
19/// - `m`: Number of rows in A and C
20/// - `n`: Number of columns in B and C
21/// - `k`: Number of columns in A / rows in B
22/// - `a`: Pointer to matrix A data
23/// - `lda`: Leading dimension of A
24/// - `trans_a`: Whether A is transposed
25/// - `b`: Pointer to matrix B data
26/// - `ldb`: Leading dimension of B
27/// - `trans_b`: Whether B is transposed
28/// - `c`: Pointer to matrix C data (output)
29/// - `ldc`: Leading dimension of C
30///
31/// # Safety
32/// - All pointers must be valid for the specified dimensions
33/// - Memory regions must not overlap inappropriately
34pub unsafe fn tropical_gemm_portable<T: TropicalSemiring>(
35    m: usize,
36    n: usize,
37    k: usize,
38    a: *const T::Scalar,
39    lda: usize,
40    trans_a: Transpose,
41    b: *const T::Scalar,
42    ldb: usize,
43    trans_b: Transpose,
44    c: *mut T,
45    ldc: usize,
46) {
47    let params = TilingParams::PORTABLE;
48    let kernel = PortableMicrokernel;
49
50    tropical_gemm_inner::<T, PortableMicrokernel>(
51        m, n, k, a, lda, trans_a, b, ldb, trans_b, c, ldc, &params, &kernel,
52    );
53}
54
55/// Tropical GEMM with custom kernel and tiling parameters.
56///
57/// With the `parallel` feature, large outputs are partitioned across Rayon
58/// workers. The shared microkernel must implement `Sync`. Each output cell
59/// retains its complete, serial K reduction.
60///
61/// # Safety
62/// Same requirements as `tropical_gemm_portable`
63pub unsafe fn tropical_gemm_inner<T: TropicalSemiring, K: Microkernel<T> + Sync>(
64    m: usize,
65    n: usize,
66    k: usize,
67    a: *const T::Scalar,
68    lda: usize,
69    trans_a: Transpose,
70    b: *const T::Scalar,
71    ldb: usize,
72    trans_b: Transpose,
73    c: *mut T,
74    ldc: usize,
75    params: &TilingParams,
76    kernel: &K,
77) {
78    tropical_gemm_inner_with_workspace(
79        m,
80        n,
81        k,
82        a,
83        lda,
84        trans_a,
85        b,
86        ldb,
87        trans_b,
88        c,
89        ldc,
90        params,
91        kernel,
92        &mut GemmWorkspace::new(),
93    );
94}
95
96/// GEMM using reusable packing buffers.
97///
98/// # Safety
99/// Same requirements as [`tropical_gemm_inner`].
100#[allow(clippy::too_many_arguments)]
101pub unsafe fn tropical_gemm_inner_with_workspace<T: TropicalSemiring, K: Microkernel<T> + Sync>(
102    m: usize,
103    n: usize,
104    k: usize,
105    a: *const T::Scalar,
106    lda: usize,
107    trans_a: Transpose,
108    b: *const T::Scalar,
109    ldb: usize,
110    trans_b: Transpose,
111    c: *mut T,
112    ldc: usize,
113    params: &TilingParams,
114    kernel: &K,
115    workspace: &mut GemmWorkspace<T::Scalar>,
116) {
117    #[cfg(feature = "parallel")]
118    if let Some(tiles) = split_gemm::<T>(
119        m,
120        n,
121        k,
122        a,
123        lda,
124        trans_a,
125        b,
126        ldb,
127        trans_b,
128        c,
129        std::ptr::null_mut(),
130        ldc,
131        K::MR,
132        K::NR,
133    ) {
134        let buffers = workspace.tasks(tiles.len());
135        tiles
136            .into_par_iter()
137            .zip(buffers.par_iter_mut())
138            .for_each(|(tile, buffers)| unsafe {
139                tropical_gemm_serial::<T, K>(
140                    tile.m, tile.n, k, tile.a, lda, trans_a, tile.b, ldb, trans_b, tile.c, ldc,
141                    params, kernel, buffers,
142                );
143            });
144        return;
145    }
146    tropical_gemm_serial::<T, K>(
147        m,
148        n,
149        k,
150        a,
151        lda,
152        trans_a,
153        b,
154        ldb,
155        trans_b,
156        c,
157        ldc,
158        params,
159        kernel,
160        &mut workspace.tasks(1)[0],
161    );
162}
163
164#[allow(clippy::too_many_arguments)]
165unsafe fn tropical_gemm_serial<T: TropicalSemiring, K: Microkernel<T>>(
166    m: usize,
167    n: usize,
168    k: usize,
169    a: *const T::Scalar,
170    lda: usize,
171    trans_a: Transpose,
172    b: *const T::Scalar,
173    ldb: usize,
174    trans_b: Transpose,
175    c: *mut T,
176    ldc: usize,
177    params: &TilingParams,
178    kernel: &K,
179    buffers: &mut PackingBuffers<T::Scalar>,
180) {
181    if m == 0 || n == 0 {
182        return;
183    }
184    // Initialize once per GEMM; microkernels accumulate subsequent K panels.
185    for i in 0..m {
186        for j in 0..n {
187            c.add(i * ldc + j).write(T::tropical_zero());
188        }
189    }
190    if k == 0 {
191        return;
192    }
193
194    buffers.prepare(
195        packed_a_size(m.min(params.mc), k.min(params.kc), K::MR),
196        packed_b_size(k.min(params.kc), n.min(params.nc), K::NR),
197    );
198    let (packed_a, packed_b) = (&mut buffers.a, &mut buffers.b);
199
200    // BLIS-style 5-loop blocking
201    // Loop 5: blocks of n
202    for (jc, nc) in BlockIterator::new(n, params.nc) {
203        // Loop 4: blocks of k
204        for (pc, kc) in BlockIterator::new(k, params.kc) {
205            // Pack B panel: kc × nc
206            pack_b::<T::Scalar>(
207                kc,
208                nc,
209                b_panel_ptr(b, pc, jc, ldb, trans_b),
210                ldb,
211                Layout::RowMajor,
212                trans_b,
213                packed_b.as_mut_ptr(),
214                K::NR,
215            );
216
217            // Loop 3: blocks of m
218            for (ic, mc) in BlockIterator::new(m, params.mc) {
219                // Pack A panel: mc × kc
220                pack_a::<T::Scalar>(
221                    mc,
222                    kc,
223                    a_panel_ptr(a, ic, pc, lda, trans_a),
224                    lda,
225                    Layout::RowMajor,
226                    trans_a,
227                    packed_a.as_mut_ptr(),
228                    K::MR,
229                );
230
231                // Loop 2: micro-blocks of n
232                let n_blocks = nc.div_ceil(K::NR);
233                for jr in 0..n_blocks {
234                    let j_start = jr * K::NR;
235                    let nr = (nc - j_start).min(K::NR);
236
237                    // Loop 1: micro-blocks of m
238                    let m_blocks = mc.div_ceil(K::MR);
239                    for ir in 0..m_blocks {
240                        let i_start = ir * K::MR;
241                        let mr = (mc - i_start).min(K::MR);
242
243                        // Microkernel
244                        let a_ptr = packed_a.as_ptr().add(ir * K::MR * kc);
245                        let b_ptr = packed_b.as_ptr().add(jr * K::NR * kc);
246                        let c_ptr = c.add((ic + i_start) * ldc + (jc + j_start));
247
248                        kernel.execute(mr, nr, kc, a_ptr, b_ptr, c_ptr, ldc);
249                    }
250                }
251            }
252        }
253    }
254}
255
256/// Tropical GEMM with argmax tracking.
257///
258/// Same as `tropical_gemm_portable` but also computes argmax indices.
259///
260/// # Safety
261/// Same requirements as `tropical_gemm_portable`
262pub unsafe fn tropical_gemm_with_argmax_portable<T: TropicalWithArgmax<Index = u32>>(
263    m: usize,
264    n: usize,
265    k: usize,
266    a: *const T::Scalar,
267    lda: usize,
268    trans_a: Transpose,
269    b: *const T::Scalar,
270    ldb: usize,
271    trans_b: Transpose,
272    result: &mut GemmWithArgmax<T>,
273) {
274    let params = TilingParams::PORTABLE;
275    let kernel = PortableMicrokernel;
276
277    tropical_gemm_with_argmax_inner::<T, PortableMicrokernel>(
278        m, n, k, a, lda, trans_a, b, ldb, trans_b, result, &params, &kernel,
279    );
280}
281
282/// Tropical GEMM with argmax tracking and custom kernel.
283///
284/// Large outputs use disjoint Rayon tasks when `parallel` is enabled. The
285/// shared microkernel must implement `Sync`; K order and first-winner ties
286/// are unchanged.
287///
288/// # Safety
289/// Same requirements as `tropical_gemm_portable`
290pub unsafe fn tropical_gemm_with_argmax_inner<
291    T: TropicalWithArgmax<Index = u32>,
292    K: MicrokernelWithArgmax<T> + Sync,
293>(
294    m: usize,
295    n: usize,
296    k: usize,
297    a: *const T::Scalar,
298    lda: usize,
299    trans_a: Transpose,
300    b: *const T::Scalar,
301    ldb: usize,
302    trans_b: Transpose,
303    result: &mut GemmWithArgmax<T>,
304    params: &TilingParams,
305    kernel: &K,
306) {
307    tropical_gemm_with_argmax_inner_with_workspace(
308        m,
309        n,
310        k,
311        a,
312        lda,
313        trans_a,
314        b,
315        ldb,
316        trans_b,
317        result,
318        params,
319        kernel,
320        &mut GemmWorkspace::new(),
321    );
322}
323
324/// GEMM using reusable packing buffers.
325///
326/// # Safety
327/// Same requirements as [`tropical_gemm_with_argmax_inner`].
328#[allow(clippy::too_many_arguments)]
329pub unsafe fn tropical_gemm_with_argmax_inner_with_workspace<
330    T: TropicalWithArgmax<Index = u32>,
331    K: MicrokernelWithArgmax<T> + Sync,
332>(
333    m: usize,
334    n: usize,
335    k: usize,
336    a: *const T::Scalar,
337    lda: usize,
338    trans_a: Transpose,
339    b: *const T::Scalar,
340    ldb: usize,
341    trans_b: Transpose,
342    result: &mut GemmWithArgmax<T>,
343    params: &TilingParams,
344    kernel: &K,
345    workspace: &mut GemmWorkspace<T::Scalar>,
346) {
347    let ldc = result.ld;
348    let (c, argmax) = result.as_mut_ptrs();
349    #[cfg(feature = "parallel")]
350    if let Some(tiles) = split_gemm::<T>(
351        m,
352        n,
353        k,
354        a,
355        lda,
356        trans_a,
357        b,
358        ldb,
359        trans_b,
360        c,
361        argmax,
362        ldc,
363        K::MR,
364        K::NR,
365    ) {
366        let buffers = workspace.tasks(tiles.len());
367        tiles
368            .into_par_iter()
369            .zip(buffers.par_iter_mut())
370            .for_each(|(tile, buffers)| unsafe {
371                tropical_gemm_with_argmax_serial::<T, K>(
372                    tile.m,
373                    tile.n,
374                    k,
375                    tile.a,
376                    lda,
377                    trans_a,
378                    tile.b,
379                    ldb,
380                    trans_b,
381                    tile.c,
382                    tile.argmax,
383                    ldc,
384                    params,
385                    kernel,
386                    buffers,
387                );
388            });
389        return;
390    }
391    tropical_gemm_with_argmax_serial::<T, K>(
392        m,
393        n,
394        k,
395        a,
396        lda,
397        trans_a,
398        b,
399        ldb,
400        trans_b,
401        c,
402        argmax,
403        ldc,
404        params,
405        kernel,
406        &mut workspace.tasks(1)[0],
407    );
408}
409
410#[allow(clippy::too_many_arguments)]
411unsafe fn tropical_gemm_with_argmax_serial<
412    T: TropicalWithArgmax<Index = u32>,
413    K: MicrokernelWithArgmax<T>,
414>(
415    m: usize,
416    n: usize,
417    k: usize,
418    a: *const T::Scalar,
419    lda: usize,
420    trans_a: Transpose,
421    b: *const T::Scalar,
422    ldb: usize,
423    trans_b: Transpose,
424    c: *mut T,
425    argmax: *mut u32,
426    ldc: usize,
427    params: &TilingParams,
428    kernel: &K,
429    buffers: &mut PackingBuffers<T::Scalar>,
430) {
431    if m == 0 || n == 0 {
432        return;
433    }
434    for i in 0..m {
435        for j in 0..n {
436            c.add(i * ldc + j).write(T::tropical_zero());
437            argmax.add(i * ldc + j).write(0);
438        }
439    }
440    if k == 0 {
441        return;
442    }
443
444    buffers.prepare(
445        packed_a_size(m.min(params.mc), k.min(params.kc), K::MR),
446        packed_b_size(k.min(params.kc), n.min(params.nc), K::NR),
447    );
448    let (packed_a, packed_b) = (&mut buffers.a, &mut buffers.b);
449
450    // BLIS-style 5-loop blocking
451    for (jc, nc) in BlockIterator::new(n, params.nc) {
452        for (pc, kc) in BlockIterator::new(k, params.kc) {
453            pack_b::<T::Scalar>(
454                kc,
455                nc,
456                b_panel_ptr(b, pc, jc, ldb, trans_b),
457                ldb,
458                Layout::RowMajor,
459                trans_b,
460                packed_b.as_mut_ptr(),
461                K::NR,
462            );
463
464            for (ic, mc) in BlockIterator::new(m, params.mc) {
465                pack_a::<T::Scalar>(
466                    mc,
467                    kc,
468                    a_panel_ptr(a, ic, pc, lda, trans_a),
469                    lda,
470                    Layout::RowMajor,
471                    trans_a,
472                    packed_a.as_mut_ptr(),
473                    K::MR,
474                );
475
476                let n_blocks = nc.div_ceil(K::NR);
477                for jr in 0..n_blocks {
478                    let j_start = jr * K::NR;
479                    let nr = (nc - j_start).min(K::NR);
480
481                    let m_blocks = mc.div_ceil(K::MR);
482                    for ir in 0..m_blocks {
483                        let i_start = ir * K::MR;
484                        let mr = (mc - i_start).min(K::MR);
485
486                        let a_ptr = packed_a.as_ptr().add(ir * K::MR * kc);
487                        let b_ptr = packed_b.as_ptr().add(jr * K::NR * kc);
488                        let c_ptr = c.add((ic + i_start) * ldc + (jc + j_start));
489                        let argmax_ptr = argmax.add((ic + i_start) * ldc + (jc + j_start));
490
491                        kernel.execute_with_argmax(
492                            mr, nr, kc, pc, a_ptr, b_ptr, c_ptr, argmax_ptr, ldc,
493                        );
494                    }
495                }
496            }
497        }
498    }
499
500    // Canonicalize the argmax index of tropical-zero "no contribution" cells.
501    // Integer in-band sentinels drift under the guard-free `+` and let the
502    // accumulator adopt a spurious k; reset those cells to the deterministic
503    // seed (0) so the whole repo agrees on one value. Done as a single O(m*n)
504    // sweep here (kept out of the hot per-block write-back to preserve its
505    // vectorization), and it folds away entirely for float types, whose
506    // `is_no_contribution` is a const `false`.
507    for i in 0..m {
508        for j in 0..n {
509            if (*c.add(i * ldc + j)).is_no_contribution() {
510                argmax.add(i * ldc + j).write(0);
511            }
512        }
513    }
514}
515
516// Each tile owns the writes to a disjoint rectangle of C (and argmax).
517// Inputs remain shared and immutable until the Rayon join completes. Keeping
518// only raw pointers avoids creating overlapping mutable slices for column splits.
519#[cfg(feature = "parallel")]
520struct GemmTile<T: TropicalSemiring> {
521    m: usize,
522    n: usize,
523    a: *const T::Scalar,
524    b: *const T::Scalar,
525    c: *mut T,
526    argmax: *mut u32,
527}
528
529// SAFETY: split_gemm is the only constructor and partitions the output without
530// overlap. T and T::Scalar are Send + Sync, and the unsafe GEMM caller guarantees
531// valid, non-aliasing input/output storage for the entire synchronous call.
532#[cfg(feature = "parallel")]
533unsafe impl<T: TropicalSemiring> Send for GemmTile<T> {}
534
535#[cfg(feature = "parallel")]
536#[allow(clippy::too_many_arguments)]
537unsafe fn split_gemm<T: TropicalSemiring>(
538    m: usize,
539    n: usize,
540    k: usize,
541    a: *const T::Scalar,
542    lda: usize,
543    trans_a: Transpose,
544    b: *const T::Scalar,
545    ldb: usize,
546    trans_b: Transpose,
547    c: *mut T,
548    argmax: *mut u32,
549    ldc: usize,
550    mr: usize,
551    nr: usize,
552) -> Option<Vec<GemmTile<T>>> {
553    // Avoid starting the Rayon pool for small calls. Limit the number of tasks
554    // as well as total work so machines with many cores do not create tiny GEMMs.
555    const MIN_WORK_PER_TASK: usize = 2 * 1024 * 1024;
556    let work = m.saturating_mul(n).saturating_mul(k);
557    if m == 0 || n == 0 || k == 0 || work / MIN_WORK_PER_TASK < 2 {
558        return None;
559    }
560    let workers = rayon::current_num_threads().min(work / MIN_WORK_PER_TASK);
561    let split_rows = m >= n;
562    let (dim, tile) = if split_rows { (m, mr) } else { (n, nr) };
563    let blocks = dim.div_ceil(tile);
564    let workers = workers.min(blocks);
565    if workers < 2 {
566        return None;
567    }
568    let chunk = blocks.div_ceil(workers) * tile;
569    let mut tiles = Vec::with_capacity(workers);
570    for (start, len) in BlockIterator::new(dim, chunk) {
571        let (row, col, rows, cols) = if split_rows {
572            (start, 0, len, n)
573        } else {
574            (0, start, m, len)
575        };
576        let offset = row * ldc + col;
577        tiles.push(GemmTile {
578            m: rows,
579            n: cols,
580            a: a_panel_ptr(a, row, 0, lda, trans_a),
581            b: b_panel_ptr(b, 0, col, ldb, trans_b),
582            c: c.add(offset),
583            argmax: if argmax.is_null() {
584                argmax
585            } else {
586                argmax.add(offset)
587            },
588        });
589    }
590    Some(tiles)
591}
592
593/// Get pointer to A panel considering transpose.
594#[inline]
595unsafe fn a_panel_ptr<T>(
596    a: *const T,
597    row: usize,
598    col: usize,
599    lda: usize,
600    trans: Transpose,
601) -> *const T {
602    match trans {
603        Transpose::NoTrans => a.add(row * lda + col),
604        Transpose::Trans => a.add(col * lda + row),
605    }
606}
607
608/// Get pointer to B panel considering transpose.
609#[inline]
610unsafe fn b_panel_ptr<T>(
611    b: *const T,
612    row: usize,
613    col: usize,
614    ldb: usize,
615    trans: Transpose,
616) -> *const T {
617    match trans {
618        Transpose::NoTrans => b.add(row * ldb + col),
619        Transpose::Trans => b.add(col * ldb + row),
620    }
621}
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626    use crate::types::TropicalMaxPlus;
627    use crate::types::TropicalScalar;
628
629    #[test]
630    fn test_simple_gemm() {
631        let m = 2;
632        let n = 2;
633        let k = 3;
634
635        // A: 2x3 matrix
636        let a: [f64; 6] = [
637            1.0, 2.0, 3.0, // row 0
638            4.0, 5.0, 6.0, // row 1
639        ];
640
641        // B: 3x2 matrix
642        let b: [f64; 6] = [
643            1.0, 2.0, // row 0
644            3.0, 4.0, // row 1
645            5.0, 6.0, // row 2
646        ];
647
648        let mut c = vec![TropicalMaxPlus::tropical_zero(); m * n];
649
650        unsafe {
651            tropical_gemm_portable::<TropicalMaxPlus<f64>>(
652                m,
653                n,
654                k,
655                a.as_ptr(),
656                3,
657                Transpose::NoTrans,
658                b.as_ptr(),
659                2,
660                Transpose::NoTrans,
661                c.as_mut_ptr(),
662                n,
663            );
664        }
665
666        // C[0,0] = max(1+1, 2+3, 3+5) = max(2, 5, 8) = 8
667        assert_eq!(c[0].0, 8.0);
668        // C[0,1] = max(1+2, 2+4, 3+6) = max(3, 6, 9) = 9
669        assert_eq!(c[1].0, 9.0);
670        // C[1,0] = max(4+1, 5+3, 6+5) = max(5, 8, 11) = 11
671        assert_eq!(c[2].0, 11.0);
672        // C[1,1] = max(4+2, 5+4, 6+6) = max(6, 9, 12) = 12
673        assert_eq!(c[3].0, 12.0);
674    }
675
676    #[test]
677    fn test_gemm_with_argmax() {
678        let m = 2;
679        let n = 2;
680        let k = 3;
681
682        let a: [f64; 6] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
683        let b: [f64; 6] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
684
685        let mut result: GemmWithArgmax<TropicalMaxPlus<f64>> = GemmWithArgmax::new(m, n);
686
687        unsafe {
688            tropical_gemm_with_argmax_portable::<TropicalMaxPlus<f64>>(
689                m,
690                n,
691                k,
692                a.as_ptr(),
693                3,
694                Transpose::NoTrans,
695                b.as_ptr(),
696                2,
697                Transpose::NoTrans,
698                &mut result,
699            );
700        }
701
702        // C[0,0] = max(1+1, 2+3, 3+5) = 8 at k=2
703        assert_eq!(result.get(0, 0).0, 8.0);
704        assert_eq!(result.get_argmax(0, 0), 2);
705
706        // C[1,1] = max(4+2, 5+4, 6+6) = 12 at k=2
707        assert_eq!(result.get(1, 1).0, 12.0);
708        assert_eq!(result.get_argmax(1, 1), 2);
709    }
710
711    #[test]
712    fn test_gemm_with_argmax_all_positions() {
713        // Test that argmax correctly tracks the optimal k for all positions
714        let m = 2;
715        let n = 2;
716        let k = 3;
717
718        // Design A and B so each C[i,j] has a different optimal k
719        // A: 2x3, B: 3x2
720        // C[i,j] = max_k(A[i,k] + B[k,j])
721        let a: [f64; 6] = [
722            10.0, 1.0, 1.0, // row 0: k=0 dominates for C[0,*]
723            1.0, 1.0, 10.0, // row 1: k=2 dominates for C[1,*]
724        ];
725        let b: [f64; 6] = [
726            10.0, 1.0, // row 0: col 0 prefers k=0
727            1.0, 10.0, // row 1: col 1 prefers k=1
728            1.0, 1.0, // row 2
729        ];
730
731        let mut result: GemmWithArgmax<TropicalMaxPlus<f64>> = GemmWithArgmax::new(m, n);
732
733        unsafe {
734            tropical_gemm_with_argmax_portable::<TropicalMaxPlus<f64>>(
735                m,
736                n,
737                k,
738                a.as_ptr(),
739                3,
740                Transpose::NoTrans,
741                b.as_ptr(),
742                2,
743                Transpose::NoTrans,
744                &mut result,
745            );
746        }
747
748        // C[0,0] = max(10+10, 1+1, 1+1) = 20 at k=0
749        assert_eq!(result.get(0, 0).0, 20.0);
750        assert_eq!(result.get_argmax(0, 0), 0);
751
752        // C[0,1] = max(10+1, 1+10, 1+1) = 11 at k=0 or k=1 (both give 11)
753        assert_eq!(result.get(0, 1).0, 11.0);
754        // k=0 gives 11, k=1 gives 11 - first wins (>=)
755        assert_eq!(result.get_argmax(0, 1), 0);
756
757        // C[1,0] = max(1+10, 1+1, 10+1) = 11 at k=0 or k=2
758        assert_eq!(result.get(1, 0).0, 11.0);
759        assert_eq!(result.get_argmax(1, 0), 0); // k=0 wins first
760
761        // C[1,1] = max(1+1, 1+10, 10+1) = 11 at k=1 or k=2
762        assert_eq!(result.get(1, 1).0, 11.0);
763        assert_eq!(result.get_argmax(1, 1), 1); // k=1 wins first with 11
764    }
765
766    #[test]
767    fn test_gemm_minplus_with_argmax() {
768        use crate::types::TropicalMinPlus;
769
770        let m = 2;
771        let n = 2;
772        let k = 3;
773
774        // For MinPlus, argmax tracks argmin
775        let a: [f64; 6] = [
776            1.0, 5.0, 3.0, // row 0
777            2.0, 4.0, 6.0, // row 1
778        ];
779        let b: [f64; 6] = [
780            1.0, 2.0, // row 0
781            3.0, 4.0, // row 1
782            5.0, 6.0, // row 2
783        ];
784
785        let mut result: GemmWithArgmax<TropicalMinPlus<f64>> = GemmWithArgmax::new(m, n);
786
787        unsafe {
788            tropical_gemm_with_argmax_portable::<TropicalMinPlus<f64>>(
789                m,
790                n,
791                k,
792                a.as_ptr(),
793                3,
794                Transpose::NoTrans,
795                b.as_ptr(),
796                2,
797                Transpose::NoTrans,
798                &mut result,
799            );
800        }
801
802        // C[0,0] = min(1+1, 5+3, 3+5) = min(2, 8, 8) = 2 at k=0
803        assert_eq!(result.get(0, 0).0, 2.0);
804        assert_eq!(result.get_argmax(0, 0), 0);
805
806        // C[0,1] = min(1+2, 5+4, 3+6) = min(3, 9, 9) = 3 at k=0
807        assert_eq!(result.get(0, 1).0, 3.0);
808        assert_eq!(result.get_argmax(0, 1), 0);
809
810        // C[1,0] = min(2+1, 4+3, 6+5) = min(3, 7, 11) = 3 at k=0
811        assert_eq!(result.get(1, 0).0, 3.0);
812        assert_eq!(result.get_argmax(1, 0), 0);
813
814        // C[1,1] = min(2+2, 4+4, 6+6) = min(4, 8, 12) = 4 at k=0
815        assert_eq!(result.get(1, 1).0, 4.0);
816        assert_eq!(result.get_argmax(1, 1), 0);
817    }
818
819    #[test]
820    fn test_gemm_larger_with_argmax() {
821        // Test with larger matrix to exercise blocking code paths
822        let m = 8;
823        let n = 8;
824        let k = 8;
825
826        let a: Vec<f64> = (0..m * k).map(|i| i as f64).collect();
827        let b: Vec<f64> = (0..k * n).map(|i| (k * n - 1 - i) as f64).collect();
828
829        let mut result: GemmWithArgmax<TropicalMaxPlus<f64>> = GemmWithArgmax::new(m, n);
830
831        unsafe {
832            tropical_gemm_with_argmax_portable::<TropicalMaxPlus<f64>>(
833                m,
834                n,
835                k,
836                a.as_ptr(),
837                k,
838                Transpose::NoTrans,
839                b.as_ptr(),
840                n,
841                Transpose::NoTrans,
842                &mut result,
843            );
844        }
845
846        // Verify all results are finite and argmax indices are valid
847        for i in 0..m {
848            for j in 0..n {
849                assert!(result.get(i, j).0.is_finite());
850                assert!(result.get_argmax(i, j) < k as u32);
851            }
852        }
853    }
854
855    #[test]
856    fn test_gemm_trans_a() {
857        // Test with A transposed
858        // A is stored column-major (3x2), so A^T is 2x3
859        // A^T = [[1, 2, 3], [4, 5, 6]]
860        let m = 2;
861        let n = 2;
862        let k = 3;
863
864        let a: [f64; 6] = [
865            1.0, 4.0, // column 0
866            2.0, 5.0, // column 1
867            3.0, 6.0, // column 2
868        ];
869
870        let b: [f64; 6] = [
871            1.0, 2.0, // row 0
872            3.0, 4.0, // row 1
873            5.0, 6.0, // row 2
874        ];
875
876        let mut c = vec![TropicalMaxPlus::tropical_zero(); m * n];
877
878        unsafe {
879            tropical_gemm_portable::<TropicalMaxPlus<f64>>(
880                m,
881                n,
882                k,
883                a.as_ptr(),
884                2,
885                Transpose::Trans, // lda=2 for column-major 3x2
886                b.as_ptr(),
887                2,
888                Transpose::NoTrans,
889                c.as_mut_ptr(),
890                n,
891            );
892        }
893
894        // A^T = [[1, 2, 3], [4, 5, 6]]
895        // B = [[1, 2], [3, 4], [5, 6]]
896        // C[0,0] = max(1+1, 2+3, 3+5) = 8
897        assert_eq!(c[0].0, 8.0);
898        // C[0,1] = max(1+2, 2+4, 3+6) = 9
899        assert_eq!(c[1].0, 9.0);
900        // C[1,0] = max(4+1, 5+3, 6+5) = 11
901        assert_eq!(c[2].0, 11.0);
902        // C[1,1] = max(4+2, 5+4, 6+6) = 12
903        assert_eq!(c[3].0, 12.0);
904    }
905
906    #[test]
907    fn test_gemm_trans_b() {
908        // Test with B transposed
909        // B is stored column-major (2x3), so B^T is 3x2
910        let m = 2;
911        let n = 2;
912        let k = 3;
913
914        let a: [f64; 6] = [
915            1.0, 2.0, 3.0, // row 0
916            4.0, 5.0, 6.0, // row 1
917        ];
918
919        // B stored column-major: columns are [1,3,5], [2,4,6]
920        let b: [f64; 6] = [
921            1.0, 3.0, 5.0, // column 0 of B^T = row of B
922            2.0, 4.0, 6.0, // column 1 of B^T
923        ];
924
925        let mut c = vec![TropicalMaxPlus::tropical_zero(); m * n];
926
927        unsafe {
928            tropical_gemm_portable::<TropicalMaxPlus<f64>>(
929                m,
930                n,
931                k,
932                a.as_ptr(),
933                3,
934                Transpose::NoTrans,
935                b.as_ptr(),
936                3,
937                Transpose::Trans, // ldb=3 for column-major 2x3
938                c.as_mut_ptr(),
939                n,
940            );
941        }
942
943        // A = [[1, 2, 3], [4, 5, 6]]
944        // B^T = [[1, 2], [3, 4], [5, 6]]
945        // C[0,0] = max(1+1, 2+3, 3+5) = 8
946        assert_eq!(c[0].0, 8.0);
947        assert_eq!(c[1].0, 9.0);
948        assert_eq!(c[2].0, 11.0);
949        assert_eq!(c[3].0, 12.0);
950    }
951
952    #[test]
953    fn test_gemm_trans_both() {
954        // Test with both A and B transposed
955        let m = 2;
956        let n = 2;
957        let k = 3;
958
959        // A column-major (3x2), A^T is 2x3
960        let a: [f64; 6] = [1.0, 4.0, 2.0, 5.0, 3.0, 6.0];
961        // B column-major (2x3), B^T is 3x2
962        let b: [f64; 6] = [1.0, 3.0, 5.0, 2.0, 4.0, 6.0];
963
964        let mut c = vec![TropicalMaxPlus::tropical_zero(); m * n];
965
966        unsafe {
967            tropical_gemm_portable::<TropicalMaxPlus<f64>>(
968                m,
969                n,
970                k,
971                a.as_ptr(),
972                2,
973                Transpose::Trans,
974                b.as_ptr(),
975                3,
976                Transpose::Trans,
977                c.as_mut_ptr(),
978                n,
979            );
980        }
981
982        assert_eq!(c[0].0, 8.0);
983        assert_eq!(c[1].0, 9.0);
984        assert_eq!(c[2].0, 11.0);
985        assert_eq!(c[3].0, 12.0);
986    }
987
988    #[test]
989    fn test_gemm_empty_m() {
990        let m = 0;
991        let n = 2;
992        let k = 3;
993
994        let a: [f64; 0] = [];
995        let b: [f64; 6] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
996        let mut c: Vec<TropicalMaxPlus<f64>> = vec![];
997
998        unsafe {
999            tropical_gemm_portable::<TropicalMaxPlus<f64>>(
1000                m,
1001                n,
1002                k,
1003                a.as_ptr(),
1004                3,
1005                Transpose::NoTrans,
1006                b.as_ptr(),
1007                2,
1008                Transpose::NoTrans,
1009                c.as_mut_ptr(),
1010                n,
1011            );
1012        }
1013
1014        // Should complete without panic
1015        assert!(c.is_empty());
1016    }
1017
1018    #[test]
1019    fn test_gemm_empty_n() {
1020        let m = 2;
1021        let n = 0;
1022        let k = 3;
1023
1024        let a: [f64; 6] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1025        let b: [f64; 0] = [];
1026        let mut c: Vec<TropicalMaxPlus<f64>> = vec![];
1027
1028        unsafe {
1029            tropical_gemm_portable::<TropicalMaxPlus<f64>>(
1030                m,
1031                n,
1032                k,
1033                a.as_ptr(),
1034                3,
1035                Transpose::NoTrans,
1036                b.as_ptr(),
1037                2,
1038                Transpose::NoTrans,
1039                c.as_mut_ptr(),
1040                n,
1041            );
1042        }
1043
1044        assert!(c.is_empty());
1045    }
1046
1047    #[test]
1048    fn test_gemm_empty_k() {
1049        let m = 2;
1050        let n = 2;
1051        let k = 0;
1052
1053        let a: [f64; 0] = [];
1054        let b: [f64; 0] = [];
1055        let mut c = vec![TropicalMaxPlus::tropical_zero(); m * n];
1056
1057        unsafe {
1058            tropical_gemm_portable::<TropicalMaxPlus<f64>>(
1059                m,
1060                n,
1061                k,
1062                a.as_ptr(),
1063                0,
1064                Transpose::NoTrans,
1065                b.as_ptr(),
1066                2,
1067                Transpose::NoTrans,
1068                c.as_mut_ptr(),
1069                n,
1070            );
1071        }
1072
1073        // C should remain initialized to tropical_zero
1074        for val in &c {
1075            assert!(val.0.is_infinite() && val.0 < 0.0);
1076        }
1077    }
1078
1079    #[test]
1080    fn test_gemm_with_argmax_empty_k() {
1081        let m = 2;
1082        let n = 2;
1083        let k = 0;
1084
1085        let a: [f64; 0] = [];
1086        let b: [f64; 0] = [];
1087        let mut result: GemmWithArgmax<TropicalMaxPlus<f64>> = GemmWithArgmax::new(m, n);
1088
1089        unsafe {
1090            tropical_gemm_with_argmax_portable::<TropicalMaxPlus<f64>>(
1091                m,
1092                n,
1093                k,
1094                a.as_ptr(),
1095                0,
1096                Transpose::NoTrans,
1097                b.as_ptr(),
1098                2,
1099                Transpose::NoTrans,
1100                &mut result,
1101            );
1102        }
1103
1104        // Should complete without panic
1105        assert_eq!(result.m, 2);
1106        assert_eq!(result.n, 2);
1107    }
1108
1109    #[test]
1110    fn test_gemm_with_argmax_trans_a() {
1111        let m = 2;
1112        let n = 2;
1113        let k = 3;
1114
1115        let a: [f64; 6] = [1.0, 4.0, 2.0, 5.0, 3.0, 6.0];
1116        let b: [f64; 6] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1117
1118        let mut result: GemmWithArgmax<TropicalMaxPlus<f64>> = GemmWithArgmax::new(m, n);
1119
1120        unsafe {
1121            tropical_gemm_with_argmax_portable::<TropicalMaxPlus<f64>>(
1122                m,
1123                n,
1124                k,
1125                a.as_ptr(),
1126                2,
1127                Transpose::Trans,
1128                b.as_ptr(),
1129                2,
1130                Transpose::NoTrans,
1131                &mut result,
1132            );
1133        }
1134
1135        assert_eq!(result.get(0, 0).0, 8.0);
1136        assert_eq!(result.get_argmax(0, 0), 2);
1137    }
1138
1139    #[test]
1140    fn test_gemm_with_argmax_trans_b() {
1141        let m = 2;
1142        let n = 2;
1143        let k = 3;
1144
1145        let a: [f64; 6] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1146        let b: [f64; 6] = [1.0, 3.0, 5.0, 2.0, 4.0, 6.0];
1147
1148        let mut result: GemmWithArgmax<TropicalMaxPlus<f64>> = GemmWithArgmax::new(m, n);
1149
1150        unsafe {
1151            tropical_gemm_with_argmax_portable::<TropicalMaxPlus<f64>>(
1152                m,
1153                n,
1154                k,
1155                a.as_ptr(),
1156                3,
1157                Transpose::NoTrans,
1158                b.as_ptr(),
1159                3,
1160                Transpose::Trans,
1161                &mut result,
1162            );
1163        }
1164
1165        assert_eq!(result.get(0, 0).0, 8.0);
1166        assert_eq!(result.get_argmax(0, 0), 2);
1167    }
1168
1169    #[test]
1170    fn test_gemm_with_argmax_int_zero_cell_canonicalized() {
1171        // Row 0 of A is the tropical zero (`-∞` sentinel), so every product for
1172        // C[0, *] is a (drifted) tropical zero — no real contribution. Its argmax
1173        // must canonicalize to the seed `0`, not drift to a data-dependent k.
1174        let m = 2;
1175        let n = 2;
1176        let k = 3;
1177        let neg = <i32 as TropicalScalar>::neg_infinity();
1178        let a: [i32; 6] = [
1179            neg, neg, neg, // row 0: all tropical zero
1180            1, 2, 3, // row 1: finite
1181        ];
1182        let b: [i32; 6] = [
1183            4, 5, // row 0
1184            6, 7, // row 1
1185            8, 9, // row 2
1186        ];
1187
1188        let mut result: GemmWithArgmax<TropicalMaxPlus<i32>> = GemmWithArgmax::new(m, n);
1189        unsafe {
1190            tropical_gemm_with_argmax_portable::<TropicalMaxPlus<i32>>(
1191                m,
1192                n,
1193                k,
1194                a.as_ptr(),
1195                3,
1196                Transpose::NoTrans,
1197                b.as_ptr(),
1198                2,
1199                Transpose::NoTrans,
1200                &mut result,
1201            );
1202        }
1203
1204        // Row 0: no contribution → value stays in `-∞` territory, argmax = 0.
1205        for j in 0..n {
1206            assert!(
1207                result.get(0, j).0.is_drifted_neg_zero(),
1208                "C[0,{j}] should be in tropical-zero territory"
1209            );
1210            assert_eq!(
1211                result.get_argmax(0, j),
1212                0,
1213                "zero-cell argmax must canonicalize to 0, not drift"
1214            );
1215        }
1216        // Row 1: real contributions → finite value, true argmax_k.
1217        // C[1,0] = max(1+4, 2+6, 3+8) = 11 at k=2; C[1,1] = max(1+5,2+7,3+9) = 12 at k=2.
1218        assert_eq!(result.get(1, 0).0, 11);
1219        assert_eq!(result.get_argmax(1, 0), 2);
1220        assert_eq!(result.get(1, 1).0, 12);
1221        assert_eq!(result.get_argmax(1, 1), 2);
1222    }
1223
1224    #[test]
1225    fn test_gemm_with_argmax_float_zero_cell_keeps_seed() {
1226        // Float `-∞` is exact (never drifts); a no-contribution cell already keeps
1227        // the seed index 0. The canonicalization hook must not change this.
1228        let m = 2;
1229        let n = 2;
1230        let k = 3;
1231        let a: [f64; 6] = [
1232            f64::NEG_INFINITY,
1233            f64::NEG_INFINITY,
1234            f64::NEG_INFINITY,
1235            1.0,
1236            2.0,
1237            3.0,
1238        ];
1239        let b: [f64; 6] = [4.0, 5.0, 6.0, 7.0, 8.0, 9.0];
1240
1241        let mut result: GemmWithArgmax<TropicalMaxPlus<f64>> = GemmWithArgmax::new(m, n);
1242        unsafe {
1243            tropical_gemm_with_argmax_portable::<TropicalMaxPlus<f64>>(
1244                m,
1245                n,
1246                k,
1247                a.as_ptr(),
1248                3,
1249                Transpose::NoTrans,
1250                b.as_ptr(),
1251                2,
1252                Transpose::NoTrans,
1253                &mut result,
1254            );
1255        }
1256
1257        for j in 0..n {
1258            assert_eq!(result.get(0, j).0, f64::NEG_INFINITY);
1259            assert_eq!(result.get_argmax(0, j), 0);
1260        }
1261    }
1262}