spark_model/layers/glm5next_kda/
tp_bind.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Applying `super::tp::KdaTpPlan` — the shard COPIES, at last.
4//!
5//! # Why this is an adapter and not a change to the binder
6//!
7//! `super::binding::bind_kda_weights` is proven: exact tensor set, exact dtypes, exact
8//! shapes, gated numerically on real weights. Teaching it about TP would mean rewriting its
9//! validation to check on-disk (full) shapes against a per-rank config — i.e. editing the one
10//! piece of this lane that has never been wrong.
11//!
12//! So the slicing happens *upstream*. `KdaShardedSource` wraps any `KdaTensorSource` and
13//! hands the binder bytes that are **already this rank's**, with **local** shapes. The binder
14//! then validates them against the local config exactly as it always has and cannot tell the
15//! difference — which is the point: **TP=1 and TP=2 run the same binder code path**, so there
16//! is no "works at TP=1, silently differs at TP=2" seam for a bug to live in.
17//!
18//! # ðŸŠĪ Two slicing shapes, and the wrong one is not a shape error
19//!
20//! * **Row slicing** (`HeadRows` / `ChannelRows`) is a contiguous byte range: this rank's rows
21//!   sit next to each other on disk.
22//! * **Column slicing** (`ChannelCols`, i.e. `o_proj` alone) is a STRIDED gather — every row
23//!   contributes its own middle slice. Row-slicing `o_proj` instead yields a well-formed
24//!   `[hidden/tp, heads*head_dim]` tensor of real numbers and a wrong output.
25//!
26//! Both produce the same LOCAL element count at `tp_size = 2` when the tensor is square-ish,
27//! so a size check cannot separate them. The test below separates them by value.
28
29use anyhow::{Result, bail};
30
31use super::binding::{KdaTensorSource, RawTensor};
32use super::tp::{KdaShard, KdaTensorPlan, KdaTpPlan};
33
34/// Plan name for a checkpoint tensor name: `self_attn.q_proj.weight` → `q_proj`.
35///
36/// ðŸŠĪ `A_log` and `dt_bias` carry no `.weight` suffix; stripping unconditionally would
37/// miss them.
38fn plan_name(checkpoint_name: &str) -> &str {
39    let n = checkpoint_name
40        .strip_prefix("self_attn.")
41        .unwrap_or(checkpoint_name);
42    n.strip_suffix(".weight").unwrap_or(n)
43}
44
45/// This rank's bytes for one tensor, given its plan entry.
46///
47/// `full` is the whole on-disk tensor. Returns a fresh buffer because a column slice is not
48/// contiguous and cannot be borrowed.
49pub fn shard_bytes(plan: &KdaTensorPlan, full: &[u8]) -> Result<Vec<u8>> {
50    let e = plan.elem_bytes;
51    let expect = plan.full_rows * plan.full_row_elems * e;
52    if full.len() != expect {
53        bail!(
54            "{}: {} B on disk, the plan's full shape [{}, {}] x {e} B implies {expect} B",
55            plan.name,
56            full.len(),
57            plan.full_rows,
58            plan.full_row_elems
59        );
60    }
61    Ok(match plan.kind {
62        KdaShard::Replicated => full.to_vec(),
63        // Contiguous: this rank's rows are adjacent on disk.
64        KdaShard::HeadRows | KdaShard::ChannelRows => {
65            let row = plan.full_row_elems * e;
66            let start = plan.src_row_offset * row;
67            full[start..start + plan.local_rows * row].to_vec()
68        }
69        // ðŸŠĪ STRIDED. Every row keeps only its own `[src_col_offset, +local_row_elems)` slice.
70        KdaShard::ChannelCols => {
71            let row = plan.full_row_elems * e;
72            let lo = plan.src_col_offset * e;
73            let width = plan.local_row_elems * e;
74            let mut out = Vec::with_capacity(plan.local_rows * width);
75            for r in 0..plan.local_rows {
76                let base = r * row + lo;
77                out.extend_from_slice(&full[base..base + width]);
78            }
79            out
80        }
81    })
82}
83
84/// The local shape the binder should see, in the on-disk rank order.
85///
86/// Mirrors the on-disk rank rather than the plan's flat `[rows, row_elems]` view: the conv
87/// tensors are rank-3 `[dim, 1, kernel]` on disk and the binder validates that rank.
88fn local_shape(plan: &KdaTensorPlan, disk_shape: &[usize]) -> Vec<usize> {
89    let mut s = disk_shape.to_vec();
90    match plan.kind {
91        KdaShard::Replicated => {}
92        KdaShard::HeadRows | KdaShard::ChannelRows => {
93            if let Some(first) = s.first_mut() {
94                *first = plan.local_rows;
95            }
96        }
97        KdaShard::ChannelCols => {
98            if let Some(last) = s.last_mut() {
99                *last = plan.local_row_elems;
100            }
101        }
102    }
103    s
104}
105
106/// A [`KdaTensorSource`] that yields **this rank's slice** of every KDA tensor.
107///
108/// Wrap the full-checkpoint source in this and hand it to `bind_kda_weights` unchanged.
109pub struct KdaShardedSource<'a> {
110    inner: &'a dyn KdaTensorSource,
111    plan: &'a KdaTpPlan,
112    /// Sliced bytes, materialised up front: `KdaTensorSource::get` returns a borrow, so the
113    /// buffers have to outlive the call.
114    sliced: std::collections::BTreeMap<String, (super::binding::KdaDtype, Vec<usize>, Vec<u8>)>,
115}
116
117impl<'a> KdaShardedSource<'a> {
118    pub fn new(inner: &'a dyn KdaTensorSource, plan: &'a KdaTpPlan) -> Result<Self> {
119        let mut sliced = std::collections::BTreeMap::new();
120        for name in inner.names() {
121            // Only `self_attn.*` is a KDA tensor; the binder counts everything else as
122            // `non_attn_seen` and never fetches it, so passing it through untouched keeps
123            // that census honest.
124            let Some(p) = plan.get(plan_name(&name)) else {
125                continue;
126            };
127            let Some(t) = inner.get(&name) else { continue };
128            sliced.insert(
129                name.clone(),
130                (t.dtype, local_shape(p, &t.shape), shard_bytes(p, t.bytes)?),
131            );
132        }
133        Ok(Self {
134            inner,
135            plan,
136            sliced,
137        })
138    }
139
140    pub fn plan(&self) -> &KdaTpPlan {
141        self.plan
142    }
143}
144
145impl KdaTensorSource for KdaShardedSource<'_> {
146    fn get(&self, name: &str) -> Option<RawTensor<'_>> {
147        match self.sliced.get(name) {
148            Some((dtype, shape, bytes)) => Some(RawTensor {
149                dtype: *dtype,
150                shape: shape.clone(),
151                bytes,
152            }),
153            // Not a planned KDA tensor — pass through, so the binder's "unrecognised
154            // self_attn tensor" refusal still fires on anything unexpected.
155            None => self.inner.get(name),
156        }
157    }
158
159    fn names(&self) -> Vec<String> {
160        self.inner.names()
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::super::binding::KdaDtype;
167    use super::*;
168
169    /// GLM-5.3's real KDA geometry at TP=2.
170    fn plan(rank: usize) -> KdaTpPlan {
171        KdaTpPlan::new(rank, 2, 4096, 128, 64, 4, 128).unwrap()
172    }
173
174    #[test]
175    fn plan_names_strip_the_prefix_but_not_a_missing_suffix() {
176        assert_eq!(plan_name("self_attn.q_proj.weight"), "q_proj");
177        assert_eq!(plan_name("self_attn.q_conv1d.weight"), "q_conv1d");
178        // ðŸŠĪ no `.weight` on these two.
179        assert_eq!(plan_name("self_attn.A_log"), "A_log");
180        assert_eq!(plan_name("self_attn.dt_bias"), "dt_bias");
181        // Every plan entry must be reachable from some checkpoint name.
182        for t in &plan(0).tensors {
183            let candidates = [
184                format!("self_attn.{}.weight", t.name),
185                format!("self_attn.{}", t.name),
186            ];
187            assert!(
188                candidates.iter().any(|c| plan_name(c) == t.name),
189                "plan entry {} is unreachable from a checkpoint name",
190                t.name
191            );
192        }
193    }
194
195    /// Row slicing is contiguous; the two ranks partition the tensor exactly.
196    #[test]
197    fn channel_rows_partition_the_tensor() {
198        // GLM's real tensors are too big for a test. The smallest LEGAL stand-in: the plan
199        // enforces `2 * local_heads * head_dim % 256 == 0` (the fused conv+L2 kernel hardcodes
200        // 2 heads per 256-thread block), so heads=4 head_dim=64 at tp=2 is the floor.
201        let p = KdaTpPlan::new(0, 2, 4, 64, 4, 2, 4).unwrap();
202        let q = p.get("q_proj").unwrap().clone();
203        assert_eq!(q.kind, KdaShard::ChannelRows);
204        // [full_ch = 256, hidden = 4] BF16, byte value = row index.
205        let full: Vec<u8> = (0..=255u8).flat_map(|r| [r; 8]).collect();
206
207        let r0 = shard_bytes(&q, &full).unwrap();
208        let mut q1 = q.clone();
209        q1.src_row_offset = q.local_rows;
210        let r1 = shard_bytes(&q1, &full).unwrap();
211
212        assert_eq!(
213            r0.len() + r1.len(),
214            full.len(),
215            "the two ranks partition it"
216        );
217        let mut rejoined = r0.clone();
218        rejoined.extend_from_slice(&r1);
219        assert_eq!(rejoined, full, "and rejoin to the original, in order");
220        assert!(r0.iter().all(|b| *b < 128), "rank 0 keeps the low rows");
221        assert!(r1.iter().all(|b| *b >= 128), "rank 1 keeps the high rows");
222    }
223
224    /// ðŸ”ī The discriminating test. `o_proj` is column-sliced; a row slice of the same tensor
225    /// has the SAME LENGTH and different contents, so only a value check separates them.
226    #[test]
227    fn o_proj_is_column_sliced_not_row_sliced() {
228        let p = KdaTpPlan::new(1, 2, 4, 64, 4, 2, 4).unwrap();
229        let o = p.get("o_proj").unwrap();
230        assert_eq!(o.kind, KdaShard::ChannelCols);
231        assert_eq!(o.full_rows, 4, "hidden");
232        assert_eq!(o.full_row_elems, 256, "heads * head_dim");
233        assert_eq!(o.local_rows, 4, "every row is kept");
234        assert_eq!(o.local_row_elems, 128, "half the input dim");
235
236        // [4, 256] BF16 where each element's low byte is its column index (mod 256).
237        let full: Vec<u8> = (0..4)
238            .flat_map(|_| (0..=255u8).flat_map(|c| [c, 0]))
239            .collect();
240        let got = shard_bytes(o, &full).unwrap();
241
242        // rank 1 keeps columns 128..256 of EVERY row.
243        let want: Vec<u8> = (0..4)
244            .flat_map(|_| (128..=255u8).flat_map(|c| [c, 0]))
245            .collect();
246        assert_eq!(got, want);
247
248        // A row slice of the same byte count would be rows 2..4 — same length, different
249        // bytes. This is the failure a size check cannot catch.
250        let row_sliced = &full[full.len() / 2..];
251        assert_eq!(row_sliced.len(), got.len());
252        assert_ne!(row_sliced, got.as_slice());
253    }
254
255    /// `o_norm` is `[head_dim]` — within-head, so it must survive TP untouched. A
256    /// "shard everything that looks per-head" rule corrupts 256 B and nothing says so.
257    #[test]
258    fn o_norm_and_the_low_rank_down_projections_are_replicated() {
259        let p = plan(1);
260        for n in ["o_norm", "f_a_proj", "g_a_proj"] {
261            let t = p.get(n).unwrap();
262            assert_eq!(t.kind, KdaShard::Replicated, "{n}");
263            let full = vec![0xABu8; t.full_bytes()];
264            assert_eq!(shard_bytes(t, &full).unwrap(), full, "{n} must be verbatim");
265        }
266    }
267
268    /// ðŸŠĪ `A_log` is per-HEAD `[64]`, `dt_bias` per-CHANNEL `[8192]`. Same block, different
269    /// granularity — slicing `dt_bias` by head count keeps 1/128th of the right data.
270    #[test]
271    fn a_log_and_dt_bias_shard_at_different_granularity() {
272        let p = plan(1);
273        let a = p.get("A_log").unwrap();
274        let d = p.get("dt_bias").unwrap();
275        assert_eq!((a.full_rows, a.local_rows), (64, 32), "A_log is per-head");
276        assert_eq!(
277            (d.full_rows, d.local_rows),
278            (8192, 4096),
279            "dt_bias is per-channel"
280        );
281        // Rank 1's offsets differ by a factor of head_dim.
282        assert_eq!(d.src_row_offset, a.src_row_offset * 128);
283    }
284
285    /// At `tp_size = 1` the plan is the identity, so the sharded source is a pass-through —
286    /// which is what lets TP=1 and TP=2 share one binder code path.
287    #[test]
288    fn tp1_is_the_identity() {
289        let p = KdaTpPlan::new(0, 1, 4096, 128, 64, 4, 128).unwrap();
290        for t in &p.tensors {
291            assert_eq!(t.local_bytes(), t.full_bytes(), "{}", t.name);
292            let full = vec![0x5Au8; t.full_bytes()];
293            assert_eq!(shard_bytes(t, &full).unwrap(), full, "{}", t.name);
294        }
295        assert!(!p.needs_output_all_reduce());
296    }
297
298    /// A tensor whose on-disk size disagrees with the plan is refused, not silently
299    /// truncated — the shard would otherwise read a valid range of the wrong tensor.
300    #[test]
301    fn a_size_mismatch_is_refused() {
302        let p = plan(0);
303        let q = p.get("q_proj").unwrap();
304        let err = shard_bytes(q, &vec![0u8; q.full_bytes() - 2]).unwrap_err();
305        assert!(err.to_string().contains("on disk"), "{err}");
306    }
307
308    /// The conv tensors are rank-3 `[dim, 1, kernel]` on disk; the local shape must keep that
309    /// rank, because the binder validates it.
310    #[test]
311    fn local_shape_preserves_the_on_disk_rank() {
312        let p = plan(1);
313        let c = p.get("q_conv1d").unwrap();
314        assert_eq!(local_shape(c, &[8192, 1, 4]), vec![4096, 1, 4]);
315        let o = p.get("o_proj").unwrap();
316        assert_eq!(local_shape(o, &[4096, 8192]), vec![4096, 4096]);
317        let n = p.get("o_norm").unwrap();
318        assert_eq!(local_shape(n, &[128]), vec![128]);
319    }
320
321    /// End to end through the adapter: the binder-facing view is this rank's slice, with a
322    /// local shape, and unplanned names still pass through so the binder's refusal survives.
323    #[test]
324    fn the_adapter_presents_local_slices_and_passes_the_rest_through() {
325        struct Src(Vec<(String, KdaDtype, Vec<usize>, Vec<u8>)>);
326        impl KdaTensorSource for Src {
327            fn get(&self, name: &str) -> Option<RawTensor<'_>> {
328                self.0
329                    .iter()
330                    .find(|(n, ..)| n == name)
331                    .map(|(_, d, s, b)| RawTensor {
332                        dtype: *d,
333                        shape: s.clone(),
334                        bytes: b,
335                    })
336            }
337            fn names(&self) -> Vec<String> {
338                self.0.iter().map(|(n, ..)| n.clone()).collect()
339            }
340        }
341        let p = KdaTpPlan::new(1, 2, 4, 64, 4, 2, 4).unwrap();
342        let q = p.get("q_proj").unwrap();
343        let src = Src(vec![
344            (
345                "self_attn.q_proj.weight".into(),
346                KdaDtype::Bf16,
347                vec![256, 4],
348                (0..=255u8).flat_map(|r| [r; 8]).collect(),
349            ),
350            (
351                "input_layernorm.weight".into(),
352                KdaDtype::Bf16,
353                vec![4],
354                vec![0xEE; 8],
355            ),
356        ]);
357        let sh = KdaShardedSource::new(&src, &p).unwrap();
358
359        let t = sh.get("self_attn.q_proj.weight").unwrap();
360        assert_eq!(t.shape, vec![128, 4], "local rows, full row width");
361        assert_eq!(t.bytes.len(), q.local_bytes());
362        assert!(
363            t.bytes.iter().all(|b| *b >= 128),
364            "rank 1 keeps the high rows"
365        );
366
367        // Not a KDA tensor: untouched.
368        let n = sh.get("input_layernorm.weight").unwrap();
369        assert_eq!(n.bytes, vec![0xEE; 8]);
370        assert_eq!(sh.names().len(), 2, "the census still sees everything");
371    }
372}