spark_model/weight_loader/
glm5_next_vision.rs1use anyhow::{Context, Result};
21use avarok_core::config::{ModelConfig, VisionConfig};
22use spark_runtime::gpu::{DevicePtr, GpuBackend};
23use spark_runtime::weights::{WeightDtype, WeightStore};
24
25use crate::layers::glm_vit::init::GlmVitGeometry;
26use crate::layers::{GlmVit, GlmVitBlock, GlmVitMerger, VisionTower};
27
28fn bf16(store: &WeightStore, name: &str) -> Result<DevicePtr> {
35 let w = store
36 .get(name)
37 .with_context(|| format!("glm5_next vision: missing tensor {name}"))?;
38 anyhow::ensure!(
39 w.dtype == WeightDtype::BF16,
40 "glm5_next vision: {name} is {:?}, but the GLM tower is BF16 throughout (347/347 \
41 tensors in the official NVFP4 export). A quantised tower needs a dequant path, not \
42 a cast of this pointer.",
43 w.dtype
44 );
45 Ok(w.ptr)
46}
47
48fn check_shape(store: &WeightStore, name: &str, rows: usize, cols: usize) -> Result<()> {
57 let shape = store.get(name)?.shape.clone();
58 let got_rows = shape.first().copied().unwrap_or(0);
59 let got_cols: usize = shape.iter().skip(1).product::<usize>().max(1);
60 anyhow::ensure!(
61 got_rows == rows && (shape.len() == 1 || got_cols == cols),
62 "glm5_next vision: {name} has shape {shape:?}, which flattens to \
63 [{got_rows}, {got_cols}] — the encoder was built for [{rows}, {cols}] from \
64 vision_config. The checkpoint's tower geometry does not match its config."
65 );
66 Ok(())
67}
68
69fn load_block(store: &WeightStore, vp: &str, i: usize, v: &VisionConfig) -> Result<GlmVitBlock> {
70 let bp = format!("{vp}.blocks.{i}");
71 let h = v.hidden_size;
72 let head_dim = h / v.num_heads.max(1);
73 check_shape(store, &format!("{bp}.attn.qkv.weight"), 3 * h, h)?;
74 check_shape(store, &format!("{bp}.attn.q_norm.weight"), head_dim, 1)?;
75 check_shape(
76 store,
77 &format!("{bp}.mlp.gate_proj.weight"),
78 v.intermediate_size,
79 h,
80 )?;
81 Ok(GlmVitBlock {
82 norm1_w: bf16(store, &format!("{bp}.norm1.weight"))?,
83 qkv_w: bf16(store, &format!("{bp}.attn.qkv.weight"))?,
84 qkv_b: bf16(store, &format!("{bp}.attn.qkv.bias"))?,
85 q_norm_w: bf16(store, &format!("{bp}.attn.q_norm.weight"))?,
86 k_norm_w: bf16(store, &format!("{bp}.attn.k_norm.weight"))?,
87 proj_w: bf16(store, &format!("{bp}.attn.proj.weight"))?,
88 proj_b: bf16(store, &format!("{bp}.attn.proj.bias"))?,
89 norm2_w: bf16(store, &format!("{bp}.norm2.weight"))?,
90 gate_w: bf16(store, &format!("{bp}.mlp.gate_proj.weight"))?,
91 gate_b: bf16(store, &format!("{bp}.mlp.gate_proj.bias"))?,
92 up_w: bf16(store, &format!("{bp}.mlp.up_proj.weight"))?,
93 up_b: bf16(store, &format!("{bp}.mlp.up_proj.bias"))?,
94 down_w: bf16(store, &format!("{bp}.mlp.down_proj.weight"))?,
95 down_b: bf16(store, &format!("{bp}.mlp.down_proj.bias"))?,
96 })
97}
98
99fn load_merger(store: &WeightStore, vp: &str, v: &VisionConfig) -> Result<GlmVitMerger> {
100 let out = v.out_hidden_size;
101 let sms2 = v.spatial_merge_size * v.spatial_merge_size;
102 check_shape(
103 store,
104 &format!("{vp}.downsample.weight"),
105 out,
106 v.hidden_size * sms2,
107 )?;
108 check_shape(store, &format!("{vp}.merger.proj.weight"), out, out)?;
109 check_shape(
110 store,
111 &format!("{vp}.merger.gate_proj.weight"),
112 v.projection_intermediate_size,
113 out,
114 )?;
115 check_shape(
116 store,
117 &format!("{vp}.merger.down_proj.weight"),
118 out,
119 v.projection_intermediate_size,
120 )?;
121 Ok(GlmVitMerger {
122 post_layernorm_w: bf16(store, &format!("{vp}.post_layernorm.weight"))?,
123 downsample_w: bf16(store, &format!("{vp}.downsample.weight"))?,
124 downsample_b: bf16(store, &format!("{vp}.downsample.bias"))?,
125 proj_w: bf16(store, &format!("{vp}.merger.proj.weight"))?,
126 norm_w: bf16(store, &format!("{vp}.merger.post_projection_norm.weight"))?,
127 norm_b: bf16(store, &format!("{vp}.merger.post_projection_norm.bias"))?,
132 gate_w: bf16(store, &format!("{vp}.merger.gate_proj.weight"))?,
133 up_w: bf16(store, &format!("{vp}.merger.up_proj.weight"))?,
134 down_w: bf16(store, &format!("{vp}.merger.down_proj.weight"))?,
135 })
136}
137
138pub fn load_glm5_next_vision(
146 store: &WeightStore,
147 config: &ModelConfig,
148 gpu: &dyn GpuBackend,
149) -> Result<Option<VisionTower>> {
150 let Some(v) = config.vision.clone() else {
151 return Ok(None);
152 };
153 let vp = ["model.visual", "model.language_model.visual", "visual"]
157 .into_iter()
158 .find(|p| store.contains(&format!("{p}.patch_embed.proj.weight")));
159 let Some(vp) = vp else {
160 tracing::warn!(
161 "glm5_next declares a vision_config but no `*.visual.patch_embed.proj.weight` is \
162 present in the checkpoint; serving TEXT-ONLY (image input will be refused)."
163 );
164 return Ok(None);
165 };
166
167 let patch_dim = 3 * v.temporal_patch_size * v.patch_size * v.patch_size;
168 check_shape(
169 store,
170 &format!("{vp}.patch_embed.proj.weight"),
171 v.hidden_size,
172 patch_dim,
173 )?;
174 let patch_embed_w = bf16(store, &format!("{vp}.patch_embed.proj.weight"))?;
175 let patch_embed_b = bf16(store, &format!("{vp}.patch_embed.proj.bias"))?;
176
177 let mut blocks = Vec::with_capacity(v.depth);
178 for i in 0..v.depth {
179 blocks.push(load_block(store, vp, i, &v)?);
180 }
181 let merger = load_merger(store, vp, &v)?;
182
183 let geo = GlmVitGeometry {
184 hidden_size: v.hidden_size,
185 num_heads: v.num_heads,
186 intermediate_size: v.intermediate_size,
187 spatial_merge_size: v.spatial_merge_size,
188 out_hidden_size: v.out_hidden_size,
189 projection_intermediate_size: v.projection_intermediate_size,
190 patch_size: v.patch_size,
191 temporal_patch_size: v.temporal_patch_size,
192 rms_norm_eps: v.rms_norm_eps,
193 swiglu_limit: v.swiglu_limit,
194 rope_theta: v.rope_theta,
195 max_pixels: v.max_pixels,
196 };
197 let encoder = GlmVit::new(patch_embed_w, patch_embed_b, blocks, merger, &geo, gpu)?;
198 tracing::info!(
199 "GLM-5.3 vision tower bound: depth={} hidden={} heads={} patch={} merge={} → {} \
200 (BF16, {} tensors)",
201 v.depth,
202 v.hidden_size,
203 v.num_heads,
204 v.patch_size,
205 v.spatial_merge_size,
206 v.out_hidden_size,
207 v.depth * 14 + 11,
208 );
209 Ok(Some(VisionTower::glm(encoder)))
210}
211
212#[cfg(test)]
213mod tests {
214 #[test]
218 fn the_bound_tensor_count_is_the_checkpoints_347() {
219 let per_block = 14; let non_block = 11; assert_eq!(24 * per_block + non_block, 347);
223 }
224}