spark_model/layers/glm_vit/
init.rs1use anyhow::Result;
6use spark_runtime::gpu::{DevicePtr, GpuBackend};
7
8use super::{GlmVit, GlmVitBlock, GlmVitMerger, GlmVitScratch};
9use crate::layers::vision_encoder::enc_impl::init::derive_max_patches;
10
11#[derive(Debug, Clone)]
15pub struct GlmVitGeometry {
16 pub hidden_size: usize,
17 pub num_heads: usize,
18 pub intermediate_size: usize,
19 pub spatial_merge_size: usize,
20 pub out_hidden_size: usize,
21 pub projection_intermediate_size: usize,
22 pub patch_size: usize,
23 pub temporal_patch_size: usize,
24 pub rms_norm_eps: f32,
25 pub swiglu_limit: f32,
26 pub rope_theta: f32,
27 pub max_pixels: Option<usize>,
28}
29
30impl GlmVit {
31 pub fn new(
32 patch_embed_w: DevicePtr,
33 patch_embed_b: DevicePtr,
34 blocks: Vec<GlmVitBlock>,
35 merger: GlmVitMerger,
36 geo: &GlmVitGeometry,
37 gpu: &dyn GpuBackend,
38 ) -> Result<Self> {
39 anyhow::ensure!(
40 geo.num_heads > 0 && geo.hidden_size.is_multiple_of(geo.num_heads),
41 "glm_vit: hidden_size {} is not divisible by num_heads {}",
42 geo.hidden_size,
43 geo.num_heads
44 );
45 let head_dim = geo.hidden_size / geo.num_heads;
46 anyhow::ensure!(
47 head_dim.is_multiple_of(4),
48 "glm_vit: head_dim {head_dim} must be a multiple of 4 — the axial RoPE splits it \
49 into an h half and a w half, each of which is then rotate-half'd"
50 );
51 anyhow::ensure!(
52 geo.spatial_merge_size == 2,
53 "glm_vit: spatial_merge_size {} — the 2x2 conv downsample and its im2col are \
54 written for 2 and only 2; a different merge needs a different kernel, not a \
55 different argument",
56 geo.spatial_merge_size
57 );
58 anyhow::ensure!(
59 geo.patch_size > 0 && geo.temporal_patch_size > 0,
60 "glm_vit: patch_size/temporal_patch_size must be non-zero"
61 );
62
63 let (p_max, asked_for) = derive_max_patches(geo.max_pixels, geo.patch_size);
64 if let Some(wanted) = asked_for {
65 tracing::warn!(
66 "GLM vision encoder capacity clamped to {p_max} patches; the resolved area \
67 bound wanted {wanted}. The ViT materialises a full [seq, seq] score matrix, \
68 so scratch is O(patches^2). Lower --vision-max-pixels to reclaim memory."
69 );
70 }
71
72 let spatial_dim = head_dim / 2;
76 let rope_inv_freq: Vec<f32> = (0..spatial_dim / 2)
77 .map(|k| 1.0 / geo.rope_theta.powf(2.0 * k as f32 / spatial_dim as f32))
78 .collect();
79
80 Ok(Self {
81 patch_embed_w,
82 patch_embed_b,
83 blocks,
84 merger,
85 k_gemm: gpu.kernel("gemm", "dense_gemm_bf16_pipelined")?,
86 k_gemm_f32: gpu.kernel("gemm", "dense_gemm_bf16_f32out")?,
87 k_add_bias: gpu.kernel("glm_vit", "glm_vit_add_bias")?,
88 k_rmsnorm: gpu.kernel("glm_vit", "glm_vit_rmsnorm")?,
89 k_layernorm: gpu.kernel("glm_vit", "glm_vit_layernorm")?,
90 k_gelu: gpu.kernel("glm_vit", "glm_vit_gelu_erf")?,
91 k_swiglu: gpu.kernel("glm_vit", "glm_vit_swiglu_clamp")?,
92 k_qknorm_rope: gpu.kernel("glm_vit", "glm_vit_qknorm_rope_deint")?,
93 k_softmax: gpu.kernel("glm_vit", "glm_vit_softmax_rows")?,
94 k_scatter_head: gpu.kernel("glm_vit", "glm_vit_scatter_head")?,
95 k_im2col: gpu.kernel("glm_vit", "glm_vit_im2col_2x2")?,
96 k_copy: gpu.kernel("glm_vit", "glm_vit_copy")?,
97 k_add: gpu.kernel("glm_vit", "glm_vit_add_inplace")?,
98 k_f32_bf16: gpu.kernel("glm_vit", "glm_vit_f32_to_bf16")?,
99 hidden_size: geo.hidden_size,
100 num_heads: geo.num_heads,
101 head_dim,
102 intermediate_size: geo.intermediate_size,
103 spatial_merge_size: geo.spatial_merge_size,
104 out_hidden_size: geo.out_hidden_size,
105 projection_intermediate_size: geo.projection_intermediate_size,
106 patch_dim: 3 * geo.temporal_patch_size * geo.patch_size * geo.patch_size,
107 rms_norm_eps: geo.rms_norm_eps,
108 swiglu_limit: geo.swiglu_limit,
109 p_max,
110 rope_inv_freq,
111 scratch: std::sync::OnceLock::new(),
112 })
113 }
114
115 pub(crate) fn mp_max(&self) -> usize {
117 let sms2 = self.spatial_merge_size * self.spatial_merge_size;
118 self.p_max.div_ceil(sms2)
119 }
120
121 fn build_scratch(&self, gpu: &dyn GpuBackend) -> Result<GlmVitScratch> {
122 let p = self.p_max;
123 let mp = self.mp_max();
124 let h = self.hidden_size;
125 let inter = self.intermediate_size;
126 let out = self.out_hidden_size;
127 let pi = self.projection_intermediate_size;
128 let wide = inter.max(3 * h).max(self.patch_dim);
132 Ok(GlmVitScratch {
133 buf_f32: gpu.alloc(p * self.patch_dim * 4)?,
134 buf_h1: gpu.alloc(p * h * 2)?,
135 buf_h2: gpu.alloc(p * h * 2)?,
136 buf_gate: gpu.alloc(p * wide * 2)?,
137 buf_up: gpu.alloc(p * inter * 2)?,
138 buf_qr: gpu.alloc(p * h * 2)?,
139 buf_kr: gpu.alloc(p * h * 2)?,
140 buf_vt: gpu.alloc(p * h * 2)?,
141 buf_scores: gpu.alloc(p * p * 4)?,
144 buf_probs: gpu.alloc(p * p * 2)?,
145 buf_o_stage: gpu.alloc(p * self.head_dim * 2)?,
146 buf_rope_cos: gpu.alloc(p * self.head_dim * 2)?,
147 buf_rope_sin: gpu.alloc(p * self.head_dim * 2)?,
148 buf_merge_a: gpu.alloc(mp * out * 2)?,
149 buf_merge_b: gpu.alloc(mp * out * 2)?,
150 buf_merge_g: gpu.alloc(mp * pi * 2)?,
151 buf_merge_u: gpu.alloc(mp * pi * 2)?,
152 buf_out: gpu.alloc(mp * out * 2)?,
153 })
154 }
155
156 pub(crate) fn scratch_init(&self, gpu: &dyn GpuBackend) -> Result<()> {
159 if self.scratch.get().is_none() {
160 let s = self.build_scratch(gpu)?;
161 let _ = self.scratch.set(s);
162 tracing::info!(
163 "GLM vision scratch allocated on first image: {} patches ({} merged rows)",
164 self.p_max,
165 self.mp_max()
166 );
167 }
168 Ok(())
169 }
170
171 pub(crate) fn scratch(&self) -> &GlmVitScratch {
172 self.scratch
173 .get()
174 .expect("glm_vit scratch: encode entry must call scratch_init(gpu) first")
175 }
176
177 pub fn out_row(&self, row: usize) -> DevicePtr {
180 self.scratch()
181 .buf_out
182 .offset(row * self.out_hidden_size * 2)
183 }
184}
185
186#[cfg(test)]
187mod tests {
188
189 #[test]
193 fn glm_patch_dim_is_a_runtime_value_of_1176() {
194 let patch_dim = 3 * 2 * 14 * 14;
195 assert_eq!(patch_dim, 1176);
196 assert_ne!(patch_dim, crate::layers::vision_encoder::PATCH_DIM);
197 }
198
199 #[test]
202 fn rope_inv_freq_matches_the_reference_formula() {
203 let head_dim = 64usize;
204 let spatial_dim = head_dim / 2;
205 let theta = 10_000.0f32;
206 let f: Vec<f32> = (0..spatial_dim / 2)
207 .map(|k| 1.0 / theta.powf(2.0 * k as f32 / spatial_dim as f32))
208 .collect();
209 assert_eq!(f.len(), 16);
210 assert_eq!(f[0], 1.0);
211 let last = 1.0 / theta.powf(30.0 / 32.0);
212 assert!((f[15] - last).abs() < 1e-9);
213 }
214}