1use kopitiam_core::{DType, Error, Result, Shape};
4
5use crate::storage::Storage;
6
7use super::Tensor;
8
9impl Tensor {
10 pub fn gather_rows(&self, indices: &Tensor) -> Result<Tensor> {
35 indices.require_dtype(DType::I32)?;
36 if self.rank() != 2 {
37 return Err(Error::ShapeMismatch { expected: self.shape.clone(), actual: self.shape.clone() });
38 }
39 if let Storage::Quantized { dtype, bytes } = self.storage.as_ref() {
42 return self.gather_rows_quantized(indices, *dtype, bytes);
43 }
44 self.require_dtype(DType::F32)?;
45 self.gather_rows_f32(indices)
46 }
47
48 fn gather_rows_quantized(
69 &self,
70 indices: &Tensor,
71 dtype: DType,
72 bytes: &[u8],
73 ) -> Result<Tensor> {
74 let vocab = self.shape.dims()[0];
75 let hidden = self.shape.dims()[1];
76 let block_size = dtype.block_size();
77 let block_bytes = dtype.block_bytes();
78
79 if !hidden.is_multiple_of(block_size)
82 || !self.is_contiguous()
83 || self.offset != 0
84 {
85 return Err(Error::UnsupportedDType { op: "gather_rows", dtype });
86 }
87 let blocks_per_row = hidden / block_size;
88 let row_bytes = blocks_per_row * block_bytes;
89
90 let Storage::I32(idx_data) = indices.storage.as_ref() else { unreachable!() };
91 let mut out = Vec::with_capacity(indices.elem_count() * hidden);
92 for offset in indices.logical_offsets() {
93 let id = idx_data[offset];
94 if id < 0 || id as usize >= vocab {
95 return Err(Error::IndexOutOfBounds {
96 dim: 0,
97 index: id.max(0) as usize,
98 len: vocab,
99 });
100 }
101 let start = id as usize * row_bytes;
102 let row = bytes
103 .get(start..start + row_bytes)
104 .ok_or(Error::IndexOutOfBounds { dim: 0, index: id as usize, len: vocab })?;
105 out.extend(crate::quant::dequantize(dtype, row)?);
106 }
107
108 let mut out_dims = indices.shape.dims().to_vec();
109 out_dims.push(hidden);
110 Tensor::from_f32(out, Shape::new(out_dims))
111 }
112
113 fn gather_rows_f32(&self, indices: &Tensor) -> Result<Tensor> {
116 let vocab = self.shape.dims()[0];
117 let hidden = self.shape.dims()[1];
118
119 let Storage::F32(data) = self.storage.as_ref() else { unreachable!() };
120 let Storage::I32(idx_data) = indices.storage.as_ref() else { unreachable!() };
121
122 let mut out = Vec::with_capacity(indices.elem_count() * hidden);
123 for offset in indices.logical_offsets() {
124 let id = idx_data[offset];
125 if id < 0 || id as usize >= vocab {
126 return Err(Error::IndexOutOfBounds {
127 dim: 0,
128 index: id.max(0) as usize,
129 len: vocab,
130 });
131 }
132 let row_start = self.offset + id as usize * self.strides[0];
133 for c in 0..hidden {
134 out.push(data[row_start + c * self.strides[1]]);
135 }
136 }
137
138 let mut out_dims = indices.shape.dims().to_vec();
139 out_dims.push(hidden);
140 Tensor::from_f32(out, Shape::new(out_dims))
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn gather_rows_picks_the_requested_embedding_rows() {
150 let table = Tensor::from_f32((0..8).map(|v| v as f32).collect(), [4, 2]).unwrap();
152 let ids = Tensor::from_i32(vec![2, 0, 3], [3]).unwrap();
153 let out = table.gather_rows(&ids).unwrap();
154 assert_eq!(out.shape().dims(), &[3, 2]);
155 assert_eq!(out.to_vec_f32().unwrap(), vec![4.0, 5.0, 0.0, 1.0, 6.0, 7.0]);
156 }
157
158 #[test]
159 fn gather_rows_preserves_the_indices_shape_for_batched_lookup() {
160 let table = Tensor::from_f32((0..6).map(|v| v as f32).collect(), [3, 2]).unwrap();
162 let ids = Tensor::from_i32(vec![0, 1, 2, 0], [2, 2]).unwrap();
163 let out = table.gather_rows(&ids).unwrap();
164 assert_eq!(out.shape().dims(), &[2, 2, 2]);
165 assert_eq!(out.to_vec_f32().unwrap(), vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 0.0, 1.0]);
166 }
167
168 #[test]
169 fn gather_rows_works_through_a_transposed_view_of_the_table() {
170 let table = Tensor::from_f32(vec![0.0, 2.0, 4.0, 6.0, 1.0, 3.0, 5.0, 7.0], [2, 4]).unwrap();
173 let transposed = table.transpose(0, 1).unwrap(); let ids = Tensor::from_i32(vec![1], [1]).unwrap();
175 let out = transposed.gather_rows(&ids).unwrap();
176 assert_eq!(out.to_vec_f32().unwrap(), vec![2.0, 3.0]);
177 }
178
179 #[test]
180 fn gather_rows_rejects_an_out_of_range_id() {
181 let table = Tensor::from_f32(vec![1.0, 2.0], [1, 2]).unwrap();
182 let ids = Tensor::from_i32(vec![5], [1]).unwrap();
183 assert!(matches!(table.gather_rows(&ids), Err(Error::IndexOutOfBounds { .. })));
184 }
185
186 #[test]
187 fn gather_rows_requires_i32_indices() {
188 let table = Tensor::from_f32(vec![1.0, 2.0], [1, 2]).unwrap();
189 let bad_ids = Tensor::from_f32(vec![0.0], [1]).unwrap();
190 assert!(matches!(table.gather_rows(&bad_ids), Err(Error::DTypeMismatch { .. })));
191 }
192}
193
194#[cfg(test)]
195mod quantized_gather_tests {
196 use super::*;
197
198 fn q8_table(rows: usize, cols: usize) -> Vec<u8> {
202 let mut bytes = Vec::new();
203 for r in 0..rows {
204 for b in 0..(cols / 32) {
205 let scale: u16 = if (r + b) % 2 == 0 { 0x3C00 } else { 0x3800 };
207 bytes.extend_from_slice(&scale.to_le_bytes());
208 for t in 0..32 {
209 bytes.push((((r * 7 + b * 3 + t) % 255) as i32 - 128) as i8 as u8);
210 }
211 }
212 }
213 bytes
214 }
215
216 #[test]
221 fn quantized_gather_matches_dequantize_then_gather() {
222 let (rows, cols) = (8usize, 64usize);
223 let bytes = q8_table(rows, cols);
224 let quantized = Tensor::from_quantized(DType::Q8_0, bytes, [rows, cols]).unwrap();
225 let full = quantized.to_dtype(DType::F32).unwrap();
226
227 let ids = Tensor::from_i32(vec![3, 0, 7, 3, 5], [5]).unwrap();
229 let from_quant = quantized.gather_rows(&ids).unwrap().to_vec_f32().unwrap();
230 let from_f32 = full.gather_rows(&ids).unwrap().to_vec_f32().unwrap();
231 assert_eq!(from_quant.len(), 5 * cols);
232 assert_eq!(from_quant, from_f32, "quantized gather diverged from the f32 path");
233 }
234
235 #[test]
236 fn quantized_gather_rejects_an_out_of_range_id() {
237 let bytes = q8_table(4, 32);
238 let t = Tensor::from_quantized(DType::Q8_0, bytes, [4, 32]).unwrap();
239 let ids = Tensor::from_i32(vec![4], [1]).unwrap();
240 assert!(matches!(
241 t.gather_rows(&ids),
242 Err(Error::IndexOutOfBounds { .. })
243 ));
244 }
245
246 #[test]
250 fn a_row_that_straddles_a_block_boundary_is_refused() {
251 let bytes = vec![0u8; 3 * 34];
253 let t = Tensor::from_quantized(DType::Q8_0, bytes, [2, 48]);
254 if let Ok(t) = t {
256 let ids = Tensor::from_i32(vec![0], [1]).unwrap();
257 assert!(t.gather_rows(&ids).is_err(), "a straddling row must not be gathered");
258 }
259 }
260}