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