spark_model/layers/
glm_vit.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! GLM-5.3-Flash vision tower (`Glm5NextVisionModel`) — 24-block ViT with
4//! per-head QK-RMSNorm, pure 2D axial RoPE, clamped SwiGLU, a strided-conv 2×2
5//! downsample and a 4-stage SwiGLU merger.
6//!
7//! A SEPARATE encoder from [`super::VisionEncoder`] (the Qwen3-VL tower) rather
8//! than a set of flags on it. The two share no op: GLM norms are weight-only
9//! RMSNorm where Qwen's are biased LayerNorm, GLM's attention normalises Q and
10//! K per head where Qwen's does not, GLM's MLP is 3-matrix clamped SwiGLU where
11//! Qwen's is 2-matrix GELU, and GLM's spatial merge is a convolution where
12//! Qwen's is a concatenation. Threading `if glm` through the Qwen path would
13//! have put a branch inside every kernel launch in it.
14//!
15//! What IS shared: the tensor-core GEMM (`gemm::dense_gemm_bf16_pipelined`),
16//! the f32-out score GEMM, the packed `buf_out` contract the embedding splice
17//! reads, and the `(post_h, post_w, merged_p)` return shape — so the server
18//! side (`msg_entry` → `VisionItem` → `embed_chunk` splice) is reused unchanged.
19//!
20//! Reference math and its citations: `runs/ws2-vision/REFERENCE.md` in the
21//! spark-bench repo, derived from `transformers` `glm5_next` / `glm_ocr`.
22
23use spark_runtime::gpu::{DevicePtr, KernelHandle};
24
25/// One ViT block's weights. Every tensor is BF16 on disk and stays BF16 — the
26/// tower is NOT quantised in `nvidia/GLM-5.3-Flash-NVFP4` (347/347 tensors are
27/// BF16; only the text/MoE stack is NVFP4).
28pub struct GlmVitBlock {
29    pub norm1_w: DevicePtr,
30    pub qkv_w: DevicePtr,
31    pub qkv_b: DevicePtr,
32    /// Per-head RMSNorm on Q, `[head_dim]`. Applied after the QKV split and
33    /// BEFORE the rotary transform — the order is the reference's.
34    pub q_norm_w: DevicePtr,
35    pub k_norm_w: DevicePtr,
36    pub proj_w: DevicePtr,
37    pub proj_b: DevicePtr,
38    pub norm2_w: DevicePtr,
39    pub gate_w: DevicePtr,
40    pub gate_b: DevicePtr,
41    pub up_w: DevicePtr,
42    pub up_b: DevicePtr,
43    pub down_w: DevicePtr,
44    pub down_b: DevicePtr,
45}
46
47/// Everything after the last block: the post-norm, the 2×2 conv downsample and
48/// the merger MLP whose output is what gets spliced into the LM.
49pub struct GlmVitMerger {
50    /// Weight-only RMSNorm over `hidden_size`, applied before the downsample.
51    pub post_layernorm_w: DevicePtr,
52    /// `Conv2d(hidden, out_hidden, k=2, s=2)` flattened to `[out_hidden,
53    /// hidden*4]` in `(in_channel, kh, kw)` order — the conv's own layout, which
54    /// is what makes the im2col below a plain reshape.
55    pub downsample_w: DevicePtr,
56    pub downsample_b: DevicePtr,
57    pub proj_w: DevicePtr,
58    /// `post_projection_norm` is a real mean-subtracting LayerNorm with a bias
59    /// — the ONLY such norm in this tower. Every other one is weight-only
60    /// RMSNorm. The checkpoint census settles it: this is the only norm that
61    /// ships a `.bias`.
62    pub norm_w: DevicePtr,
63    pub norm_b: DevicePtr,
64    pub gate_w: DevicePtr,
65    pub up_w: DevicePtr,
66    pub down_w: DevicePtr,
67}
68
69/// Device scratch, allocated on the FIRST image rather than at load — a
70/// text-only GLM serve never pays a byte of it, which matters because the
71/// quadratic score matrix dominates the group.
72pub struct GlmVitScratch {
73    pub(crate) buf_f32: DevicePtr,
74    pub(crate) buf_h1: DevicePtr,
75    pub(crate) buf_h2: DevicePtr,
76    pub(crate) buf_gate: DevicePtr,
77    pub(crate) buf_up: DevicePtr,
78    pub(crate) buf_qr: DevicePtr,
79    pub(crate) buf_kr: DevicePtr,
80    pub(crate) buf_vt: DevicePtr,
81    pub(crate) buf_scores: DevicePtr,
82    pub(crate) buf_probs: DevicePtr,
83    pub(crate) buf_o_stage: DevicePtr,
84    pub(crate) buf_rope_cos: DevicePtr,
85    pub(crate) buf_rope_sin: DevicePtr,
86    pub(crate) buf_merge_a: DevicePtr,
87    pub(crate) buf_merge_b: DevicePtr,
88    pub(crate) buf_merge_g: DevicePtr,
89    pub(crate) buf_merge_u: DevicePtr,
90    pub buf_out: DevicePtr,
91}
92
93pub struct GlmVit {
94    pub(crate) patch_embed_w: DevicePtr,
95    pub(crate) patch_embed_b: DevicePtr,
96    pub(crate) blocks: Vec<GlmVitBlock>,
97    pub(crate) merger: GlmVitMerger,
98
99    // Shared kernels (module `gemm`).
100    pub(crate) k_gemm: KernelHandle,
101    pub(crate) k_gemm_f32: KernelHandle,
102    // GLM-specific kernels (module `glm_vit`).
103    pub(crate) k_add_bias: KernelHandle,
104    pub(crate) k_rmsnorm: KernelHandle,
105    pub(crate) k_layernorm: KernelHandle,
106    pub(crate) k_gelu: KernelHandle,
107    pub(crate) k_swiglu: KernelHandle,
108    pub(crate) k_qknorm_rope: KernelHandle,
109    pub(crate) k_softmax: KernelHandle,
110    pub(crate) k_scatter_head: KernelHandle,
111    pub(crate) k_im2col: KernelHandle,
112    pub(crate) k_copy: KernelHandle,
113    pub(crate) k_add: KernelHandle,
114    pub(crate) k_f32_bf16: KernelHandle,
115
116    // Geometry.
117    pub(crate) hidden_size: usize,
118    pub(crate) num_heads: usize,
119    pub(crate) head_dim: usize,
120    pub(crate) intermediate_size: usize,
121    pub(crate) spatial_merge_size: usize,
122    pub out_hidden_size: usize,
123    pub(crate) projection_intermediate_size: usize,
124    /// `C × temporal_patch_size × patch_size²` — 1176 for GLM (3×2×14×14).
125    /// A RUNTIME value, not the compiled `PATCH_DIM` constant the Qwen encoder
126    /// carries: that constant is 1536 and is exactly why the Qwen tower cannot
127    /// bind this checkpoint.
128    pub(crate) patch_dim: usize,
129    pub(crate) rms_norm_eps: f32,
130    pub(crate) swiglu_limit: f32,
131    /// Patch rows every buffer is sized for.
132    pub(crate) p_max: usize,
133    /// `inv_freq[k] = theta^(-2k/spatial_dim)`, `spatial_dim = head_dim/2`.
134    /// Half the entries feed the h axis and half the w axis.
135    pub(crate) rope_inv_freq: Vec<f32>,
136
137    pub(crate) scratch: std::sync::OnceLock<GlmVitScratch>,
138}
139
140mod block;
141mod forward;
142pub mod init;
143mod merge;
144mod rope;
145
146pub use init::GlmVitGeometry;
147pub use rope::{block_major_position_ids, build_axial_rope_tables};