1use anyhow::Result;
17use metrale_gpu_runtime::gpu::GpuBackend;
18
19use super::super::VisionEncoder;
20
21fn check_packed_rows(mp_i: &[usize], mp_off: &[usize], p_max: usize) -> Result<()> {
29 anyhow::ensure!(
30 mp_i.len() == mp_off.len(),
31 "vision: {} merged row counts but {} offsets; the packed layout is inconsistent",
32 mp_i.len(),
33 mp_off.len()
34 );
35 let end = match (mp_off.last(), mp_i.last()) {
36 (Some(off), Some(n)) => off
37 .checked_add(*n)
38 .ok_or_else(|| anyhow::anyhow!("vision: merged row offset {off} + {n} overflows"))?,
39 _ => 0,
40 };
41 anyhow::ensure!(
42 end <= p_max,
43 "vision: this batch packs {end} merged rows into an output buffer of {p_max} rows. \
44 A video arrives as one media item whose temporal groups encode as a single batch, \
45 which defeats the scheduler's per-item cap. Send fewer frames, or allocate a \
46 larger vision scratch."
47 );
48 Ok(())
49}
50
51impl VisionEncoder {
52 pub fn forward(
55 &self,
56 pixels: &[f32],
57 grid_h: usize,
58 grid_w: usize,
59 gpu: &dyn GpuBackend,
60 stream: u64,
61 ) -> Result<usize> {
62 let images = [(pixels, grid_h, grid_w)];
63 let per_image = self.forward_batched(&images, gpu, stream)?;
64 let merged_p = per_image[0].2;
65 Ok((1 + self.deepstack_indexes.len()) * merged_p)
66 }
67
68 pub fn forward_batched(
85 &self,
86 images: &[(&[f32], usize, usize)],
87 gpu: &dyn GpuBackend,
88 stream: u64,
89 ) -> Result<Vec<(usize, usize, usize)>> {
90 self.scratch_init(gpu)?;
93 let sms2 = self.spatial_merge_size * self.spatial_merge_size;
94 let sms = self.spatial_merge_size.max(1);
95 let n_img = images.len();
96
97 let mut p_i = Vec::with_capacity(n_img);
100 let mut p_off = Vec::with_capacity(n_img);
101 let mut mp_i = Vec::with_capacity(n_img);
102 let mut mp_off = Vec::with_capacity(n_img);
103 let (mut p_total, mut mp_total) = (0usize, 0usize);
104 for (_px, gh, gw) in images.iter() {
105 let p = gh * gw;
106 let mp = p / sms2;
107 p_off.push(p_total);
108 mp_off.push(mp_total);
109 p_i.push(p);
110 mp_i.push(mp);
111 p_total += p;
112 mp_total += mp;
113 }
114
115 if p_total > self.p_max {
118 return self.forward_oversized_fallback(images, &p_i, &mp_i, &mp_off, sms, gpu, stream);
119 }
120
121 let _sec0 = std::time::Instant::now();
122 let pos_interp_on = std::env::var("METRALE_VISION_POSINTERP")
124 .map(|v| v != "0")
125 .unwrap_or(true);
126 for (i, (_px, gh, gw)) in images.iter().enumerate() {
127 let p = p_i[i];
128 let pos_dst = self
129 .scratch()
130 .buf_pos_resampled
131 .offset(p_off[i] * self.hidden_size * 2);
132 if pos_interp_on {
133 self.resample_pos_embed_into(*gh, *gw, pos_dst, gpu, stream)?;
134 } else {
135 self.gpu_copy_bf16(
136 gpu,
137 self.pos_embed,
138 pos_dst,
139 p * self.hidden_size * 2,
140 stream,
141 )?;
142 }
143 let cos_dst = self
144 .scratch()
145 .buf_rope_cos
146 .offset(p_off[i] * self.head_dim * 2);
147 let sin_dst = self
148 .scratch()
149 .buf_rope_sin
150 .offset(p_off[i] * self.head_dim * 2);
151 self.build_rope_cossin_into(*gh, *gw, cos_dst, sin_dst, gpu, stream)?;
152 }
153
154 let timing = std::env::var("METRALE_VISION_TIMING").is_ok();
155 if timing {
156 gpu.synchronize(stream).ok();
157 tracing::info!(
158 "VIT_SEC host_prep({n_img} imgs): {:.1}ms",
159 _sec0.elapsed().as_secs_f64() * 1000.0
160 );
161 }
162 let _sec1 = std::time::Instant::now();
163 self.patch_embed_batched(images, &p_off, p_total, gpu, stream)?;
165 Self::maybe_dump_buf(
166 gpu,
167 self.scratch().buf_h1,
168 p_total * self.hidden_size,
169 "patch_embed",
170 stream,
171 )?;
172
173 let n_h_bytes = p_total * self.hidden_size * 2;
176 let mut deepstack_iter = self.deepstack_indexes.iter().enumerate();
177 let mut next_ds = deepstack_iter.next(); for (block_idx, blk) in self.blocks.iter().enumerate() {
179 self.vit_block_batched(blk, p_total, &p_i, &p_off, gpu, stream)?;
180 Self::maybe_dump_buf(
181 gpu,
182 self.scratch().buf_h1,
183 p_total * self.hidden_size,
184 &format!("block{block_idx:02}"),
185 stream,
186 )?;
187 if let Some((ds_idx, &ds_block)) = next_ds
188 && block_idx + 1 == ds_block
189 {
190 self.gpu_copy_bf16(
194 gpu,
195 self.scratch().buf_h1,
196 self.scratch().buf_h2,
197 n_h_bytes,
198 stream,
199 )?;
200 let ds_region_base = (ds_idx + 1) * mp_total;
201 for (i, (_px, gh, gw)) in images.iter().enumerate() {
202 let src = self
203 .scratch()
204 .buf_h2
205 .offset(p_off[i] * self.hidden_size * 2);
206 let out_rows = ds_region_base + mp_off[i];
207 let out_slice = self
208 .scratch()
209 .buf_out
210 .offset(out_rows * self.out_hidden_size * 2);
211 self.apply_merger(
212 &self.deepstack[ds_idx],
213 p_i[i],
214 *gh,
215 *gw,
216 src,
217 out_slice,
218 gpu,
219 stream,
220 )?;
221 }
222 next_ds = deepstack_iter.next();
223 }
224 }
225
226 if timing {
227 gpu.synchronize(stream).ok();
228 tracing::info!(
229 "VIT_SEC patch+27blocks(M={p_total}): {:.1}ms",
230 _sec1.elapsed().as_secs_f64() * 1000.0
231 );
232 }
233 let _sec2 = std::time::Instant::now();
234 for (i, (_px, gh, gw)) in images.iter().enumerate() {
236 let src = self
237 .scratch()
238 .buf_h1
239 .offset(p_off[i] * self.hidden_size * 2);
240 let out_slice = self
241 .scratch()
242 .buf_out
243 .offset(mp_off[i] * self.out_hidden_size * 2);
244 self.apply_merger(&self.merger, p_i[i], *gh, *gw, src, out_slice, gpu, stream)?;
245 }
246 if timing {
247 gpu.synchronize(stream).ok();
248 tracing::info!(
249 "VIT_SEC mergers(final+{} ds): {:.1}ms",
250 self.deepstack_indexes.len(),
251 _sec2.elapsed().as_secs_f64() * 1000.0
252 );
253 }
254 let dump_rows = (1 + self.deepstack_indexes.len()) * mp_total;
256 Self::maybe_dump_buf(
257 gpu,
258 self.scratch().buf_out,
259 dump_rows * self.out_hidden_size,
260 "final",
261 stream,
262 )?;
263
264 Ok(images
265 .iter()
266 .map(|(_px, gh, gw)| (gh / sms, gw / sms, (gh * gw) / sms2))
267 .collect())
268 }
269
270 #[allow(clippy::too_many_arguments)]
274 fn forward_oversized_fallback(
275 &self,
276 images: &[(&[f32], usize, usize)],
277 p_i: &[usize],
278 mp_i: &[usize],
279 mp_off: &[usize],
280 sms: usize,
281 gpu: &dyn GpuBackend,
282 stream: u64,
283 ) -> Result<Vec<(usize, usize, usize)>> {
284 check_packed_rows(mp_i, mp_off, self.p_max)?;
285 let pos_interp_on = std::env::var("METRALE_VISION_POSINTERP")
286 .map(|v| v != "0")
287 .unwrap_or(true);
288 for (i, (pixels, gh, gw)) in images.iter().enumerate() {
289 let p = p_i[i];
290 if pos_interp_on {
291 self.resample_pos_embed(*gh, *gw, gpu, stream)?;
292 } else {
293 self.gpu_copy_bf16(
294 gpu,
295 self.pos_embed,
296 self.scratch().buf_pos_resampled,
297 p * self.hidden_size * 2,
298 stream,
299 )?;
300 }
301 self.build_rope_cossin(*gh, *gw, gpu, stream)?;
302 self.patch_embed(pixels, p, gpu, stream)?;
303 for blk in self.blocks.iter() {
304 self.vit_block(blk, p, gpu, stream)?;
305 }
306 let out_slice = self
307 .scratch()
308 .buf_out
309 .offset(mp_off[i] * self.out_hidden_size * 2);
310 self.apply_merger(
311 &self.merger,
312 p,
313 *gh,
314 *gw,
315 self.scratch().buf_h1,
316 out_slice,
317 gpu,
318 stream,
319 )?;
320 }
321 Ok(images
322 .iter()
323 .map(|(_px, gh, gw)| (gh / sms, gw / sms, (gh * gw) / (sms * sms)))
324 .collect())
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::check_packed_rows;
331
332 #[test]
336 fn refuses_the_video_batch_that_poisoned_the_cuda_context() {
337 let groups = 19;
338 let merged_per_group = (30 / 2) * (34 / 2);
339 let mp_i = vec![merged_per_group; groups];
340 let mp_off: Vec<usize> = (0..groups).map(|g| g * merged_per_group).collect();
341 assert_eq!(mp_off.last().unwrap() + mp_i.last().unwrap(), 4845);
342
343 let err = check_packed_rows(&mp_i, &mp_off, 1024)
344 .unwrap_err()
345 .to_string();
346 assert!(
347 err.contains("4845"),
348 "must name the rows it would have written: {err}"
349 );
350 assert!(err.contains("1024"), "must name the capacity: {err}");
351 }
352
353 #[test]
356 fn admits_a_batch_that_fits() {
357 assert!(check_packed_rows(&[100, 200], &[0, 100], 1024).is_ok());
358 assert!(check_packed_rows(&[24], &[1000], 1024).is_ok());
359 assert!(check_packed_rows(&[25], &[1000], 1024).is_err());
360 }
361
362 #[test]
365 fn handles_empty_and_mismatched_layouts_without_panicking() {
366 assert!(check_packed_rows(&[], &[], 1024).is_ok());
367 let err = check_packed_rows(&[], &[0], 1024).unwrap_err().to_string();
368 assert!(err.contains("inconsistent"), "{err}");
369 }
370
371 #[test]
374 fn an_overflowing_offset_is_an_error_not_a_wrap() {
375 let err = check_packed_rows(&[2], &[usize::MAX - 1], 1024)
376 .unwrap_err()
377 .to_string();
378 assert!(err.contains("overflows"), "{err}");
379 }
380}