metrale_model_layers/layers/glm_vit/
rope.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Block-major patch ids, and the 2D axial RoPE tables built from them on the host.
4//!
5//! The 2×2 downsample treats each run of four consecutive tokens as one
6//! spatial block (`merge.rs`), which is right only while the tokens are
7//! block-major; a raster order still produces an embedding, a wrong one. The
8//! pure functions here fix that order and are unit-tested.
9//!
10//! Owner: model-layers (vision).
11//! Invariants: none beyond the types.
12
13use super::GlmVit;
14
15/// 2026-09-25: Per-token `(h_id, w_id)` in block-major order: the outer loops walk
16/// the `merge × merge` blocks in raster order, the inner loops the positions
17/// inside a block. This is the order
18/// `vision_preprocess_glm::block_major_patch_index` gives the pixel patches.
19pub fn block_major_position_ids(grid_h: usize, grid_w: usize, merge: usize) -> Vec<(u32, u32)> {
20    let m = merge.max(1);
21    let (bh, bw) = (grid_h / m, grid_w / m);
22    let mut ids = Vec::with_capacity(bh * bw * m * m);
23    for a in 0..bh {
24        for c in 0..bw {
25            for b in 0..m {
26                for d in 0..m {
27                    ids.push(((a * m + b) as u32, (c * m + d) as u32));
28                }
29            }
30        }
31    }
32    ids
33}
34
35/// 2026-09-25: `(cos, sin)` tables of `[num_tokens, head_dim]` f32, in the layout
36/// `glm_vit_qknorm_rope_deint` reads.
37///
38/// With `inv_freq` of `head_dim/4` entries, the per-token angle row is
39/// `cat([h*inv_freq, w*inv_freq])` (length `head_dim/2`), written twice to
40/// fill `head_dim`. The kernel rotates every dimension of the head.
41pub fn build_axial_rope_tables(
42    ids: &[(u32, u32)],
43    inv_freq: &[f32],
44    head_dim: usize,
45) -> (Vec<f32>, Vec<f32>) {
46    let half = head_dim / 2;
47    let n_freq = inv_freq.len();
48    let mut cos = vec![0.0f32; ids.len() * head_dim];
49    let mut sin = vec![0.0f32; ids.len() * head_dim];
50    for (t, &(h_id, w_id)) in ids.iter().enumerate() {
51        for d in 0..half {
52            // 2026-09-25: The first `n_freq` angles use the h id, the rest the w id.
53            let angle = if d < n_freq {
54                h_id as f32 * inv_freq[d]
55            } else {
56                w_id as f32 * inv_freq[d - n_freq]
57            };
58            let (s, c) = angle.sin_cos();
59            cos[t * head_dim + d] = c;
60            cos[t * head_dim + half + d] = c;
61            sin[t * head_dim + d] = s;
62            sin[t * head_dim + half + d] = s;
63        }
64    }
65    (cos, sin)
66}
67
68impl GlmVit {
69    /// 2026-09-25: Build this image's rope tables on the host and upload them as BF16.
70    pub(super) fn upload_rope_tables(
71        &self,
72        grid_h: usize,
73        grid_w: usize,
74        cos_dst: metrale_gpu_runtime::gpu::DevicePtr,
75        sin_dst: metrale_gpu_runtime::gpu::DevicePtr,
76        gpu: &dyn metrale_gpu_runtime::gpu::GpuBackend,
77        stream: u64,
78    ) -> anyhow::Result<()> {
79        let ids = block_major_position_ids(grid_h, grid_w, self.spatial_merge_size);
80        let (cos, sin) = build_axial_rope_tables(&ids, &self.rope_inv_freq, self.head_dim);
81        gpu.copy_h2d_async(&to_bf16_bytes(&cos), cos_dst, stream)?;
82        gpu.copy_h2d_async(&to_bf16_bytes(&sin), sin_dst, stream)?;
83        Ok(())
84    }
85}
86
87/// 2026-09-25: Round-to-nearest-even f32 → BF16, as little-endian bytes.
88pub(super) fn to_bf16_bytes(v: &[f32]) -> Vec<u8> {
89    let mut out = Vec::with_capacity(v.len() * 2);
90    for &x in v {
91        let bits = x.to_bits();
92        let lsb = (bits >> 16) & 1;
93        let rounded = bits.wrapping_add(0x7fff + lsb);
94        out.extend_from_slice(&((rounded >> 16) as u16).to_le_bytes());
95    }
96    out
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    /// 2026-09-25: Four consecutive tokens are one spatial block, and the blocks
104    /// walk in raster order.
105    #[test]
106    fn position_ids_are_block_major_not_raster() {
107        let ids = block_major_position_ids(4, 4, 2);
108        assert_eq!(ids.len(), 16);
109        assert_eq!(&ids[0..4], &[(0, 0), (0, 1), (1, 0), (1, 1)]);
110        assert_eq!(&ids[4..8], &[(0, 2), (0, 3), (1, 2), (1, 3)]);
111        assert_eq!(&ids[8..12], &[(2, 0), (2, 1), (3, 0), (3, 1)]);
112        assert_ne!(ids[2], (0, 2));
113    }
114
115    /// 2026-09-25: A non-square grid catches an h/w transposition that a square
116    /// one hides.
117    #[test]
118    fn position_ids_handle_a_non_square_grid() {
119        let ids = block_major_position_ids(26, 46, 2);
120        assert_eq!(ids.len(), 26 * 46);
121        assert_eq!(ids[0], (0, 0));
122        assert_eq!(ids[23 * 4 - 1], (1, 45));
123        assert_eq!(*ids.last().unwrap(), (25, 45));
124        let mut seen: Vec<(u32, u32)> = ids.clone();
125        seen.sort_unstable();
126        seen.dedup();
127        assert_eq!(seen.len(), ids.len(), "block-major must be a permutation");
128    }
129
130    /// 2026-09-25: The second half of each row repeats the first, and in the
131    /// first half the h angles come before the w angles.
132    #[test]
133    fn axial_tables_duplicate_the_half_row() {
134        let head_dim = 8;
135        let inv_freq = vec![1.0f32, 0.5];
136        let ids = [(3u32, 5u32)];
137        let (cos, sin) = build_axial_rope_tables(&ids, &inv_freq, head_dim);
138        let expect: [f32; 4] = [3.0 * 1.0, 3.0 * 0.5, 5.0 * 1.0, 5.0 * 0.5];
139        for (d, &angle) in expect.iter().enumerate() {
140            assert!((cos[d] - angle.cos()).abs() < 1e-6, "cos[{d}]");
141            assert!((sin[d] - angle.sin()).abs() < 1e-6, "sin[{d}]");
142            assert_eq!(cos[d], cos[head_dim / 2 + d]);
143            assert_eq!(sin[d], sin[head_dim / 2 + d]);
144        }
145    }
146
147    /// 2026-09-25: Token 0 sits at (0, 0), so every angle is zero: cos 1, sin 0.
148    #[test]
149    fn the_origin_token_is_an_identity_rotation() {
150        let inv_freq: Vec<f32> = (0..16)
151            .map(|k| 10_000f32.powf(-2.0 * k as f32 / 32.0))
152            .collect();
153        let ids = block_major_position_ids(32, 32, 2);
154        let (cos, sin) = build_axial_rope_tables(&ids, &inv_freq, 64);
155        for d in 0..64 {
156            assert_eq!(cos[d], 1.0);
157            assert_eq!(sin[d], 0.0);
158        }
159    }
160
161    #[test]
162    fn bf16_conversion_rounds_to_nearest_even() {
163        assert_eq!(to_bf16_bytes(&[1.0]), 0x3f80u16.to_le_bytes().to_vec());
164        // 2026-09-25: 0x3f80_8000 (1 + 2^-8) is a tie, which rounds to the even mantissa.
165        let halfway = f32::from_bits(0x3f80_8000);
166        assert_eq!(to_bf16_bytes(&[halfway]), 0x3f80u16.to_le_bytes().to_vec());
167        let above = f32::from_bits(0x3f80_8001);
168        assert_eq!(to_bf16_bytes(&[above]), 0x3f81u16.to_le_bytes().to_vec());
169    }
170}