spark_model/layers/glm_vit/
rope.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Block-major patch ids and the pure 2D axial RoPE tables built from them.
4//!
5//! Both halves of this file are pure CPU functions with no device state,
6//! because both encode an ORDER that nothing downstream can check. The 2×2
7//! downsample later reshapes four CONSECUTIVE tokens into one spatial block;
8//! that is only correct while the token stream is block-major, and a raster
9//! ordering would produce a perfectly plausible — and wrong — image embedding.
10//! So the order is stated once, here, and tested.
11
12use super::GlmVit;
13
14/// Per-token `(h_id, w_id)` in the reference's block-major order.
15///
16/// `get_vision_position_ids` builds `meshgrid(arange(h), arange(w))`, reshapes
17/// to `(h/m, m, w/m, m)`, `transpose(1, 2)` and flattens — i.e. the outer loop
18/// walks 2×2 merge blocks in raster order and the inner loop walks the four
19/// positions inside each block. The image processor's `patchify()` uses the
20/// identical nesting (`permute(0, 2, 5, 3, 6, 1, 4, 7)`), which is what lets
21/// the tower's `view(-1, 2, 2, C)` be a free reshape.
22pub fn block_major_position_ids(grid_h: usize, grid_w: usize, merge: usize) -> Vec<(u32, u32)> {
23    let m = merge.max(1);
24    let (bh, bw) = (grid_h / m, grid_w / m);
25    let mut ids = Vec::with_capacity(bh * bw * m * m);
26    for a in 0..bh {
27        for c in 0..bw {
28            for b in 0..m {
29                for d in 0..m {
30                    ids.push(((a * m + b) as u32, (c * m + d) as u32));
31                }
32            }
33        }
34    }
35    ids
36}
37
38/// `(cos, sin)` tables of `[num_tokens, head_dim]` f32, in the layout the
39/// rotate-half kernel reads.
40///
41/// `inv_freq` has `spatial_dim/2 = head_dim/4` entries. The per-token frequency
42/// row is `cat([h*inv_freq, w*inv_freq])` (length `head_dim/2`) duplicated once
43/// to fill `head_dim` — the reference's `cat([freq_hw, freq_hw], -1)`. There is
44/// no partial-rotary split: every dimension of the 64-wide head is rotated.
45pub fn build_axial_rope_tables(
46    ids: &[(u32, u32)],
47    inv_freq: &[f32],
48    head_dim: usize,
49) -> (Vec<f32>, Vec<f32>) {
50    let half = head_dim / 2;
51    let n_freq = inv_freq.len();
52    let mut cos = vec![0.0f32; ids.len() * head_dim];
53    let mut sin = vec![0.0f32; ids.len() * head_dim];
54    for (t, &(h_id, w_id)) in ids.iter().enumerate() {
55        for d in 0..half {
56            // `freq_hw[d]`: the first half indexes the h axis, the second the w.
57            let angle = if d < n_freq {
58                h_id as f32 * inv_freq[d]
59            } else {
60                w_id as f32 * inv_freq[d - n_freq]
61            };
62            let (s, c) = angle.sin_cos();
63            cos[t * head_dim + d] = c;
64            cos[t * head_dim + half + d] = c;
65            sin[t * head_dim + d] = s;
66            sin[t * head_dim + half + d] = s;
67        }
68    }
69    (cos, sin)
70}
71
72impl GlmVit {
73    /// Build this image's rope tables on the host and upload them as BF16.
74    pub(super) fn upload_rope_tables(
75        &self,
76        grid_h: usize,
77        grid_w: usize,
78        cos_dst: spark_runtime::gpu::DevicePtr,
79        sin_dst: spark_runtime::gpu::DevicePtr,
80        gpu: &dyn spark_runtime::gpu::GpuBackend,
81        stream: u64,
82    ) -> anyhow::Result<()> {
83        let ids = block_major_position_ids(grid_h, grid_w, self.spatial_merge_size);
84        let (cos, sin) = build_axial_rope_tables(&ids, &self.rope_inv_freq, self.head_dim);
85        gpu.copy_h2d_async(&to_bf16_bytes(&cos), cos_dst, stream)?;
86        gpu.copy_h2d_async(&to_bf16_bytes(&sin), sin_dst, stream)?;
87        Ok(())
88    }
89}
90
91/// Round-to-nearest-even f32 → BF16, little-endian. The tables are consumed by
92/// a BF16 kernel, and truncation here would bias every angle one way.
93pub(super) fn to_bf16_bytes(v: &[f32]) -> Vec<u8> {
94    let mut out = Vec::with_capacity(v.len() * 2);
95    for &x in v {
96        let bits = x.to_bits();
97        let lsb = (bits >> 16) & 1;
98        let rounded = bits.wrapping_add(0x7fff + lsb);
99        out.extend_from_slice(&((rounded >> 16) as u16).to_le_bytes());
100    }
101    out
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    /// The nesting the 2×2 downsample depends on: four CONSECUTIVE tokens are
109    /// one spatial block, and the blocks themselves walk in raster order.
110    #[test]
111    fn position_ids_are_block_major_not_raster() {
112        let ids = block_major_position_ids(4, 4, 2);
113        assert_eq!(ids.len(), 16);
114        // Block (0,0) occupies tokens 0..4.
115        assert_eq!(&ids[0..4], &[(0, 0), (0, 1), (1, 0), (1, 1)]);
116        // Block (0,1) — the NEXT block along w — occupies 4..8.
117        assert_eq!(&ids[4..8], &[(0, 2), (0, 3), (1, 2), (1, 3)]);
118        // Block (1,0) starts the second row of blocks.
119        assert_eq!(&ids[8..12], &[(2, 0), (2, 1), (3, 0), (3, 1)]);
120        // Raster order would have put (0,2) at index 2; it is at index 4.
121        assert_ne!(ids[2], (0, 2));
122    }
123
124    /// Non-square grids are the case that catches an h/w transposition, which
125    /// on a square test image is invisible.
126    #[test]
127    fn position_ids_handle_a_non_square_grid() {
128        // The golden's third image: 26x46 unmerged patches → 13x23 blocks.
129        let ids = block_major_position_ids(26, 46, 2);
130        assert_eq!(ids.len(), 26 * 46);
131        assert_eq!(ids[0], (0, 0));
132        // Last token of the first block row: block (0, 22), inner (1, 1).
133        assert_eq!(ids[23 * 4 - 1], (1, 45));
134        assert_eq!(*ids.last().unwrap(), (25, 45));
135        // Every id is in range, and each appears exactly once.
136        let mut seen: Vec<(u32, u32)> = ids.clone();
137        seen.sort_unstable();
138        seen.dedup();
139        assert_eq!(seen.len(), ids.len(), "block-major must be a permutation");
140    }
141
142    /// The table layout the rotate-half kernel assumes: the second half of each
143    /// row REPEATS the first, and inside the first half the h frequencies come
144    /// before the w ones.
145    #[test]
146    fn axial_tables_duplicate_the_half_row() {
147        let head_dim = 8; // spatial_dim 4 → 2 inv_freq entries
148        let inv_freq = vec![1.0f32, 0.5];
149        let ids = [(3u32, 5u32)];
150        let (cos, sin) = build_axial_rope_tables(&ids, &inv_freq, head_dim);
151        let expect: [f32; 4] = [3.0 * 1.0, 3.0 * 0.5, 5.0 * 1.0, 5.0 * 0.5];
152        for (d, &angle) in expect.iter().enumerate() {
153            assert!((cos[d] - angle.cos()).abs() < 1e-6, "cos[{d}]");
154            assert!((sin[d] - angle.sin()).abs() < 1e-6, "sin[{d}]");
155            // Duplicated into the upper half.
156            assert_eq!(cos[d], cos[head_dim / 2 + d]);
157            assert_eq!(sin[d], sin[head_dim / 2 + d]);
158        }
159    }
160
161    /// Token 0 sits at (0, 0), so every angle is zero: cos 1, sin 0. A table
162    /// that came out otherwise would mean the ids were built off-by-one.
163    #[test]
164    fn the_origin_token_is_an_identity_rotation() {
165        let inv_freq: Vec<f32> = (0..16)
166            .map(|k| 10_000f32.powf(-2.0 * k as f32 / 32.0))
167            .collect();
168        let ids = block_major_position_ids(32, 32, 2);
169        let (cos, sin) = build_axial_rope_tables(&ids, &inv_freq, 64);
170        for d in 0..64 {
171            assert_eq!(cos[d], 1.0);
172            assert_eq!(sin[d], 0.0);
173        }
174    }
175
176    #[test]
177    fn bf16_conversion_rounds_to_nearest_even() {
178        // 1.0 is exact in BF16.
179        assert_eq!(to_bf16_bytes(&[1.0]), 0x3f80u16.to_le_bytes().to_vec());
180        // A value exactly between two BF16s rounds to the even mantissa.
181        let halfway = f32::from_bits(0x3f80_8000); // 1 + 2^-8, tie
182        assert_eq!(to_bf16_bytes(&[halfway]), 0x3f80u16.to_le_bytes().to_vec());
183        // Truncation would have kept 0x3f80 here too; this one must round UP.
184        let above = f32::from_bits(0x3f80_8001);
185        assert_eq!(to_bf16_bytes(&[above]), 0x3f81u16.to_le_bytes().to_vec());
186    }
187}