1use kopitiam_core::{DType, Error, Result, Shape};
4
5use crate::storage::Storage;
6
7use super::Tensor;
8
9impl Tensor {
10 pub fn sum(&self, axis: usize, keepdim: bool) -> Result<Tensor> {
15 self.reduce(axis, keepdim, 0.0, |acc, v| acc + v)
16 }
17
18 pub fn max(&self, axis: usize, keepdim: bool) -> Result<Tensor> {
20 self.reduce(axis, keepdim, f32::NEG_INFINITY, f32::max)
21 }
22
23 pub fn argmax(&self, axis: usize, keepdim: bool) -> Result<Tensor> {
45 self.require_dtype(DType::F32)?;
46 let rank = self.rank();
47 if axis >= rank {
48 return Err(Error::IndexOutOfBounds { dim: axis, index: axis, len: rank });
49 }
50 let Storage::F32(data) = self.storage.as_ref() else { unreachable!() };
51 let contiguous: Vec<f32> = self.logical_offsets().map(|i| data[i]).collect();
52
53 let dims = self.shape.dims();
54 let axis_len = dims[axis];
55 let outer: usize = dims[..axis].iter().product();
56 let inner: usize = dims[axis + 1..].iter().product();
57
58 let mut out = vec![0i32; outer * inner];
59 for o in 0..outer {
60 for inn in 0..inner {
61 let mut best = f32::NEG_INFINITY;
62 let mut best_idx = 0usize;
63 for a in 0..axis_len {
64 let v = contiguous[o * axis_len * inner + a * inner + inn];
65 if v > best {
68 best = v;
69 best_idx = a;
70 }
71 }
72 out[o * inner + inn] = best_idx as i32;
73 }
74 }
75
76 let mut new_dims = dims.to_vec();
77 if keepdim {
78 new_dims[axis] = 1;
79 } else {
80 new_dims.remove(axis);
81 }
82 Tensor::from_i32(out, Shape::new(new_dims))
83 }
84
85 fn reduce(&self, axis: usize, keepdim: bool, init: f32, f: impl Fn(f32, f32) -> f32) -> Result<Tensor> {
86 self.require_dtype(DType::F32)?;
87 let rank = self.rank();
88 if axis >= rank {
89 return Err(Error::IndexOutOfBounds { dim: axis, index: axis, len: rank });
90 }
91 let Storage::F32(data) = self.storage.as_ref() else { unreachable!() };
92 let contiguous: Vec<f32> = self.logical_offsets().map(|i| data[i]).collect();
93
94 let dims = self.shape.dims();
95 let axis_len = dims[axis];
96 let outer: usize = dims[..axis].iter().product();
97 let inner: usize = dims[axis + 1..].iter().product();
98
99 let mut out = vec![init; outer * inner];
100 for o in 0..outer {
101 for inn in 0..inner {
102 let mut acc = init;
103 for a in 0..axis_len {
104 acc = f(acc, contiguous[o * axis_len * inner + a * inner + inn]);
105 }
106 out[o * inner + inn] = acc;
107 }
108 }
109
110 let mut new_dims = dims.to_vec();
111 if keepdim {
112 new_dims[axis] = 1;
113 } else {
114 new_dims.remove(axis);
115 }
116 Tensor::from_f32(out, Shape::new(new_dims))
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn sum_along_last_axis_matches_hand_computation() {
126 let t = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3]).unwrap();
127 let s = t.sum(1, false).unwrap();
128 assert_eq!(s.shape().dims(), &[2]);
129 assert_eq!(s.to_vec_f32().unwrap(), vec![6.0, 15.0]);
130 }
131
132 #[test]
133 fn sum_keepdim_preserves_rank() {
134 let t = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3]).unwrap();
135 let s = t.sum(1, true).unwrap();
136 assert_eq!(s.shape().dims(), &[2, 1]);
137 assert_eq!(s.to_vec_f32().unwrap(), vec![6.0, 15.0]);
138 }
139
140 #[test]
141 fn sum_along_axis_zero_matches_hand_computation() {
142 let t = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3]).unwrap();
143 let s = t.sum(0, false).unwrap();
144 assert_eq!(s.to_vec_f32().unwrap(), vec![5.0, 7.0, 9.0]);
145 }
146
147 #[test]
148 fn max_along_last_axis_matches_hand_computation() {
149 let t = Tensor::from_f32(vec![1.0, 5.0, 3.0, 9.0, 2.0, 4.0], [2, 3]).unwrap();
150 let m = t.max(1, false).unwrap();
151 assert_eq!(m.to_vec_f32().unwrap(), vec![5.0, 9.0]);
152 }
153
154 #[test]
155 fn reduce_rejects_an_axis_beyond_rank() {
156 let t = Tensor::from_f32(vec![1.0, 2.0], [2]).unwrap();
157 assert!(matches!(t.sum(1, false), Err(Error::IndexOutOfBounds { .. })));
158 }
159
160 #[test]
161 fn argmax_last_axis_matches_hand_computation_and_is_i32() {
162 let t = Tensor::from_f32(vec![1.0, 5.0, 3.0, 9.0, 2.0, 4.0], [2, 3]).unwrap();
164 let idx = t.argmax(1, false).unwrap();
165 assert_eq!(idx.dtype(), DType::I32);
166 assert_eq!(idx.shape().dims(), &[2]);
167 assert_eq!(idx.to_vec_i32().unwrap(), vec![1, 0]);
168 }
169
170 #[test]
171 fn argmax_keepdim_preserves_rank() {
172 let t = Tensor::from_f32(vec![1.0, 5.0, 3.0, 9.0, 2.0, 4.0], [2, 3]).unwrap();
173 let idx = t.argmax(1, true).unwrap();
174 assert_eq!(idx.shape().dims(), &[2, 1]);
175 assert_eq!(idx.to_vec_i32().unwrap(), vec![1, 0]);
176 }
177
178 #[test]
179 fn argmax_along_axis_zero_matches_hand_computation() {
180 let t = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3]).unwrap();
183 let idx = t.argmax(0, false).unwrap();
184 assert_eq!(idx.to_vec_i32().unwrap(), vec![1, 1, 1]);
185 }
186
187 #[test]
188 fn argmax_breaks_ties_towards_the_first_occurrence() {
189 let t = Tensor::from_f32(vec![7.0, 7.0, 7.0], [3]).unwrap();
191 let idx = t.argmax(0, false).unwrap();
192 assert_eq!(idx.to_vec_i32().unwrap(), vec![0]);
193 }
194
195 #[test]
196 fn argmax_picks_the_greedy_token_from_a_vocab_row() {
197 let logits = Tensor::from_f32(vec![0.1, -2.0, 0.5, 3.7, 1.2], [5]).unwrap();
200 let id = logits.argmax(0, false).unwrap();
201 assert_eq!(id.to_vec_i32().unwrap(), vec![3]);
202 }
203
204 #[test]
205 fn argmax_rejects_non_f32_input() {
206 let t = Tensor::from_i32(vec![1, 2, 3], [3]).unwrap();
207 assert!(matches!(t.argmax(0, false), Err(Error::DTypeMismatch { .. })));
208 }
209
210 #[test]
211 fn argmax_rejects_an_axis_beyond_rank() {
212 let t = Tensor::from_f32(vec![1.0, 2.0], [2]).unwrap();
213 assert!(matches!(t.argmax(1, false), Err(Error::IndexOutOfBounds { .. })));
214 }
215
216 #[test]
217 fn to_vec_i32_rejects_f32_input() {
218 let t = Tensor::from_f32(vec![1.0, 2.0], [2]).unwrap();
219 assert!(matches!(t.to_vec_i32(), Err(Error::DTypeMismatch { .. })));
220 }
221}