metrale_model_layers/layers/glm_vit/
forward.rs1use anyhow::Result;
12use metrale_gpu_runtime::gpu::GpuBackend;
13use metrale_gpu_runtime::kernel_args::{KernelLaunch, div_ceil};
14
15use super::GlmVit;
16
17fn check_pixels(
21 pixels: &[f32],
22 patches: usize,
23 end_row: usize,
24 patch_dim: usize,
25 p_max: usize,
26) -> Result<()> {
27 let want = patches
28 .checked_mul(patch_dim)
29 .ok_or_else(|| anyhow::anyhow!("glm_vit: patch count {patches} overflows"))?;
30 anyhow::ensure!(
31 pixels.len() == want,
32 "glm_vit: pixel buffer is {} floats for {patches} patches, but this encoder is built \
33 for {patch_dim} floats per patch ({want}). The checkpoint's vision_config \
34 patch_size/temporal_patch_size do not match the tower that was bound.",
35 pixels.len()
36 );
37 anyhow::ensure!(
38 end_row <= p_max,
39 "glm_vit: this upload ends at patch row {end_row} but the encoder's buffers hold \
40 {p_max} rows ({patches} patches in this image)."
41 );
42 Ok(())
43}
44
45impl GlmVit {
46 pub fn forward_batched(
54 &self,
55 images: &[(&[f32], usize, usize)],
56 gpu: &dyn GpuBackend,
57 stream: u64,
58 ) -> Result<Vec<(usize, usize, usize)>> {
59 self.scratch_init(gpu)?;
60 let sms = self.spatial_merge_size;
61 let sms2 = sms * sms;
62
63 for (_, gh, gw) in images {
64 anyhow::ensure!(
65 gh % sms == 0 && gw % sms == 0 && *gh > 0 && *gw > 0,
66 "glm_vit: patch grid {gh}x{gw} is not a multiple of the {sms}x{sms} merge — \
67 the downsample reshapes four CONSECUTIVE tokens into one block and an odd \
68 grid silently mixes rows"
69 );
70 anyhow::ensure!(
71 gh * gw <= self.p_max,
72 "glm_vit: one image of {} patches exceeds the encoder capacity of {} rows",
73 gh * gw,
74 self.p_max
75 );
76 }
77
78 let mut total_merged = 0usize;
79 for (_, gh, gw) in images {
80 total_merged += (gh * gw) / sms2;
81 }
82 anyhow::ensure!(
83 total_merged <= self.mp_max(),
84 "glm_vit: this batch packs {total_merged} merged rows into an output buffer of {} \
85 rows. Send fewer images, or allocate a larger vision scratch.",
86 self.mp_max()
87 );
88
89 let mut mp_base = 0usize;
90 let mut start = 0usize;
91 while start < images.len() {
92 let mut end = start;
93 let mut p_sum = 0usize;
94 while end < images.len() {
95 let (_, gh, gw) = images[end];
96 if p_sum + gh * gw > self.p_max {
97 break;
98 }
99 p_sum += gh * gw;
100 end += 1;
101 }
102 debug_assert!(end > start, "capacity was checked per image above");
103 self.encode_chunk(&images[start..end], mp_base, gpu, stream)?;
104 mp_base += p_sum / sms2;
105 start = end;
106 }
107
108 Ok(images
109 .iter()
110 .map(|(_, gh, gw)| (gh / sms, gw / sms, (gh * gw) / sms2))
111 .collect())
112 }
113
114 fn encode_chunk(
117 &self,
118 images: &[(&[f32], usize, usize)],
119 mp_base: usize,
120 gpu: &dyn GpuBackend,
121 stream: u64,
122 ) -> Result<()> {
123 let s = self.scratch();
124 let sms2 = self.spatial_merge_size * self.spatial_merge_size;
125 let mut p_i = Vec::with_capacity(images.len());
126 let mut p_off = Vec::with_capacity(images.len());
127 let mut p_total = 0usize;
128 for (_, gh, gw) in images {
129 p_off.push(p_total);
130 p_i.push(gh * gw);
131 p_total += gh * gw;
132 }
133
134 for (i, (_, gh, gw)) in images.iter().enumerate() {
136 self.upload_rope_tables(
137 *gh,
138 *gw,
139 s.buf_rope_cos.offset(p_off[i] * self.head_dim * 2),
140 s.buf_rope_sin.offset(p_off[i] * self.head_dim * 2),
141 gpu,
142 stream,
143 )?;
144 }
145
146 for (i, (pixels, gh, gw)) in images.iter().enumerate() {
150 let p = gh * gw;
151 let end_row = p_off[i]
152 .checked_add(p)
153 .ok_or_else(|| anyhow::anyhow!("glm_vit: patch row offset overflows"))?;
154 check_pixels(pixels, p, end_row, self.patch_dim, self.p_max)?;
155 let bytes: &[u8] = unsafe {
159 std::slice::from_raw_parts(pixels.as_ptr().cast::<u8>(), pixels.len() * 4)
160 };
161 gpu.copy_h2d_async(
162 bytes,
163 s.buf_f32.offset(p_off[i] * self.patch_dim * 4),
164 stream,
165 )?;
166 }
167 let n_f32 = (p_total * self.patch_dim) as u32;
168 KernelLaunch::new(gpu, self.k_f32_bf16)
169 .grid([div_ceil(n_f32, 256), 1, 1])
170 .block([256, 1, 1])
171 .arg_ptr(s.buf_f32)
172 .arg_ptr(s.buf_gate)
173 .arg_u32(n_f32)
174 .launch(stream)?;
175 self.gemm(
176 gpu,
177 s.buf_gate,
178 self.patch_embed_w,
179 Some(self.patch_embed_b),
180 s.buf_h1,
181 p_total as u32,
182 self.hidden_size as u32,
183 self.patch_dim as u32,
184 stream,
185 )?;
186 self.maybe_dump(
187 gpu,
188 s.buf_h1,
189 p_total * self.hidden_size,
190 "patch_embed",
191 stream,
192 )?;
193
194 for (idx, blk) in self.blocks.iter().enumerate() {
198 self.block(blk, p_total, &p_i, &p_off, gpu, stream)?;
199 self.maybe_dump(
200 gpu,
201 s.buf_h1,
202 p_total * self.hidden_size,
203 &format!("block{idx:02}"),
204 stream,
205 )?;
206 }
207
208 self.rmsnorm(
210 gpu,
211 s.buf_h1,
212 self.merger.post_layernorm_w,
213 p_total as u32,
214 self.hidden_size as u32,
215 stream,
216 )?;
217 let out = s.buf_out.offset(mp_base * self.out_hidden_size * 2);
218 self.downsample_and_merge(p_total / sms2, out, gpu, stream)?;
219 self.maybe_dump(
220 gpu,
221 out,
222 (p_total / sms2) * self.out_hidden_size,
223 "merged",
224 stream,
225 )
226 }
227
228 pub(super) fn maybe_dump(
232 &self,
233 gpu: &dyn GpuBackend,
234 ptr: metrale_gpu_runtime::gpu::DevicePtr,
235 n_elements: usize,
236 label: &str,
237 stream: u64,
238 ) -> Result<()> {
239 let Ok(dir) = std::env::var("METRALE_DUMP_GLM_VIT") else {
240 return Ok(());
241 };
242 if dir.is_empty() {
243 return Ok(());
244 }
245 gpu.synchronize(stream)?;
246 let mut buf = vec![0u8; n_elements * 2];
247 gpu.copy_d2h(ptr, &mut buf)?;
248 std::fs::create_dir_all(&dir).ok();
249 std::fs::write(
250 std::path::Path::new(&dir).join(format!("{label}.bin")),
251 &buf,
252 )?;
253 tracing::info!("METRALE_DUMP_GLM_VIT: wrote {label} ({n_elements} elements)");
254 Ok(())
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::check_pixels;
261
262 #[test]
264 fn accepts_the_glm_patch_width() {
265 assert!(check_pixels(&vec![0.0; 64 * 1176], 64, 64, 1176, 6400).is_ok());
266 }
267
268 #[test]
271 fn rejects_a_qwen_width_buffer() {
272 let err = check_pixels(&vec![0.0; 64 * 1536], 64, 64, 1176, 6400)
273 .unwrap_err()
274 .to_string();
275 assert!(err.contains("1176"), "{err}");
276 assert!(err.contains("patch_size"), "{err}");
277 }
278
279 #[test]
282 fn rejects_a_small_image_placed_past_the_end() {
283 let pixels = vec![0.0f32; 8 * 1176];
284 assert!(check_pixels(&pixels, 8, 6399 + 8, 1176, 6400).is_err());
285 assert!(check_pixels(&pixels, 8, 100 + 8, 1176, 6400).is_ok());
286 }
287
288 #[test]
289 fn rejects_a_patch_count_that_overflows() {
290 let err = check_pixels(&[], usize::MAX / 2, 0, 1176, 6400)
291 .unwrap_err()
292 .to_string();
293 assert!(err.contains("overflow"), "{err}");
294 }
295}