metrale_model_arch/glm5next_dsa/
tp.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: GLM-5.3-Flash DSA tensor-parallel shard plan: MLA heads sharded, the indexer
4//! replicated.
5//!
6//! Owner: model-arch (GLM-5.3 DSA).
7//! Invariants:
8//! - [`DsaTpPlan::new`] refuses `tp_rank >= tp_size` and a config that fails
9//!   `Glm5NextDsaConfig::validate`.
10//! - Every indexer tensor, `kv_a_proj_with_mqa`, and the low-rank down-projections and their
11//!   norms are replicated; `q_b_proj` and `kv_b_proj` shard by head; `o_proj` is row-parallel.
12//!
13//! Pure data, as in [`crate::glm5next_kda::tp`], so the per-rank arithmetic is tested without a
14//! GPU.
15//!
16//! # Why the indexer is replicated
17//!
18//! The indexer emits a token selection, not a partial sum. `index_scores` adds
19//! `weights[h] * relu(scale * dot)` over the indexer heads, so a head-sharded indexer gives each
20//! rank a partial score, and a per-rank top-k over partial scores can select different tokens on
21//! different ranks. Sharding is correct only with an all-reduce of the scores before the top-k.
22//! Scores are `[q_rows, n_pools]` with `n_pools = seq / kpool`: at a 262,144-token context
23//! (`max_position_embeddings` in MODEL.toml) that is 65,536 x 4 B = 256 KiB per decode token per
24//! DSA layer, against a replicated indexer of about 15 MB per layer (the `indexer.*` sizes in
25//! `tp/tests.rs`). An all-reduce would also change the order of the per-head sum, which can
26//! reorder pools whose scores tie at the cutoff.
27//!
28//! * `kv_a_proj_with_mqa` produces the shared latent every head decompresses from; it has no
29//!   head axis.
30//! * `q_b_proj` shards at `qk_head_dim` rows per head and `kv_b_proj` at
31//!   `qk_nope_head_dim + v_head_dim`; the two strides differ.
32//! * `o_proj` is row-parallel on `heads * v_head_dim`.
33
34use anyhow::{Result, bail};
35
36use super::Glm5NextDsaConfig;
37
38/// 2026-09-25: How one DSA tensor maps onto TP ranks.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum DsaShard {
41    /// 2026-09-25: Every rank holds the whole tensor: the indexer, the latent KV projection, and
42    /// the low-rank down-projections and norms.
43    Replicated,
44    /// 2026-09-25: Leading dim is `heads * per_head`: slice by this rank's head range.
45    HeadRows,
46    /// 2026-09-25: Trailing (input) dim is `heads * v_head_dim`: a row-parallel GEMM, sliced on the
47    /// input dim, whose output is all-reduced.
48    HeadCols,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct DsaTensorPlan {
53    pub name: &'static str,
54    pub kind: DsaShard,
55    pub elem_bytes: usize,
56    pub full_rows: usize,
57    pub full_row_elems: usize,
58    pub local_rows: usize,
59    pub local_row_elems: usize,
60    pub src_row_offset: usize,
61    pub src_col_offset: usize,
62}
63
64impl DsaTensorPlan {
65    pub fn local_bytes(&self) -> usize {
66        self.local_rows * self.local_row_elems * self.elem_bytes
67    }
68    pub fn full_bytes(&self) -> usize {
69        self.full_rows * self.full_row_elems * self.elem_bytes
70    }
71}
72
73const BF16: usize = 2;
74
75/// 2026-09-25: Per-rank shard plan for one DSA block.
76#[derive(Debug, Clone)]
77pub struct DsaTpPlan {
78    pub tp_rank: usize,
79    pub tp_size: usize,
80    pub full_heads: usize,
81    pub local_heads: usize,
82    pub tensors: Vec<DsaTensorPlan>,
83}
84
85impl DsaTpPlan {
86    /// 2026-09-25: `cfg.local_heads` is already per rank, so the full head count is
87    /// `local_heads * tp_size`, as in `TpGdnDims::from_config`.
88    pub fn new(tp_rank: usize, tp_size: usize, cfg: &Glm5NextDsaConfig) -> Result<Self> {
89        if tp_rank >= tp_size {
90            bail!("tp_rank {tp_rank} >= tp_size {tp_size}");
91        }
92        cfg.validate()?;
93        let local_heads = cfg.local_heads;
94        let full_heads = local_heads * tp_size;
95
96        let h = cfg.hidden;
97        let qk = cfg.qk_head_dim();
98        let kvb_per_head = cfg.qk_nope_head_dim + cfg.v_head_dim;
99        let ihd = cfg.index_head_dim;
100
101        let mk = |name, kind, full_rows: usize, full_row_elems: usize| {
102            let (local_rows, local_row_elems, src_row_offset, src_col_offset) = match kind {
103                DsaShard::Replicated => (full_rows, full_row_elems, 0, 0),
104                DsaShard::HeadRows => {
105                    let per = full_rows / tp_size;
106                    (per, full_row_elems, tp_rank * per, 0)
107                }
108                DsaShard::HeadCols => {
109                    let per = full_row_elems / tp_size;
110                    (full_rows, per, 0, tp_rank * per)
111                }
112            };
113            DsaTensorPlan {
114                name,
115                kind,
116                elem_bytes: BF16,
117                full_rows,
118                full_row_elems,
119                local_rows,
120                local_row_elems,
121                src_row_offset,
122                src_col_offset,
123            }
124        };
125
126        let tensors = vec![
127            mk("q_a_proj", DsaShard::Replicated, cfg.q_lora_rank, h),
128            mk("q_a_layernorm", DsaShard::Replicated, cfg.q_lora_rank, 1),
129            mk(
130                "q_b_proj",
131                DsaShard::HeadRows,
132                full_heads * qk,
133                cfg.q_lora_rank,
134            ),
135            // 2026-09-25: The shared latent has no head axis, so it is replicated.
136            mk(
137                "kv_a_proj_with_mqa",
138                DsaShard::Replicated,
139                cfg.kv_cache_dim(),
140                h,
141            ),
142            mk("kv_a_layernorm", DsaShard::Replicated, cfg.kv_lora_rank, 1),
143            mk(
144                "kv_b_proj",
145                DsaShard::HeadRows,
146                full_heads * kvb_per_head,
147                cfg.kv_lora_rank,
148            ),
149            mk("o_proj", DsaShard::HeadCols, h, full_heads * cfg.v_head_dim),
150            // 2026-09-25: The indexer, replicated (see the module doc).
151            mk(
152                "indexer.wq_b",
153                DsaShard::Replicated,
154                cfg.index_heads * ihd,
155                cfg.q_lora_rank,
156            ),
157            mk("indexer.wk", DsaShard::Replicated, ihd, h),
158            mk("indexer.k_norm.weight", DsaShard::Replicated, ihd, 1),
159            // 2026-09-25: `k_norm` is a LayerNorm, so it has a bias.
160            mk("indexer.k_norm.bias", DsaShard::Replicated, ihd, 1),
161            mk(
162                "indexer.weights_proj",
163                DsaShard::Replicated,
164                cfg.index_heads,
165                h,
166            ),
167            mk(
168                "indexer.index_kpool_compress_gate",
169                DsaShard::Replicated,
170                ihd,
171                h,
172            ),
173            mk(
174                "indexer.index_kpool_compress_ape",
175                DsaShard::Replicated,
176                cfg.index_kpool,
177                ihd,
178            ),
179        ];
180
181        Ok(Self {
182            tp_rank,
183            tp_size,
184            full_heads,
185            local_heads,
186            tensors,
187        })
188    }
189
190    pub fn get(&self, name: &str) -> Option<&DsaTensorPlan> {
191        self.tensors.iter().find(|t| t.name == name)
192    }
193    pub fn local_bytes(&self) -> usize {
194        self.tensors.iter().map(|t| t.local_bytes()).sum()
195    }
196    pub fn full_bytes(&self) -> usize {
197        self.tensors.iter().map(|t| t.full_bytes()).sum()
198    }
199    /// 2026-09-25: `o_proj` is row-parallel, so above `tp_size = 1` its output needs the reduce.
200    pub fn needs_output_all_reduce(&self) -> bool {
201        self.tp_size > 1
202    }
203    /// 2026-09-25: Bytes every rank holds in full.
204    pub fn replicated_bytes(&self) -> usize {
205        self.tensors
206            .iter()
207            .filter(|t| t.kind == DsaShard::Replicated)
208            .map(|t| t.full_bytes())
209            .sum()
210    }
211}
212
213#[cfg(test)]
214mod tests;