1use super::MixerKind;
7use crate::config::ModelConfig;
8use crate::mxfp4_e8m0::GROUP_SIZE;
9use anyhow::{Result, ensure};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum TpAxis {
13 Replicated,
14 Rows,
15 Columns,
16}
17
18pub fn tensor_plan(name: &str, mixer: MixerKind, config: &ModelConfig) -> (TpAxis, usize, usize) {
19 let canonical;
21 let name = if let Some(stem) = name
22 .strip_suffix(".weight_packed")
23 .or_else(|| name.strip_suffix(".weight_scale"))
24 {
25 canonical = format!("{stem}.weight");
26 canonical.as_str()
27 } else {
28 name
29 };
30 let tp = config.tp_world_size.max(1);
31 let h = config.hidden_size;
32 let kda_heads = config.linear_num_key_heads * tp;
33 let kda_d = config.linear_key_head_dim;
34 let kda_q = kda_heads * kda_d;
35 let conv_k = config.linear_conv_kernel_dim.max(1);
36 let mla_heads = config.num_attention_heads * tp;
37 let qk = config.qk_nope_head_dim + config.qk_rope_head_dim;
38 let dv = mla_heads * config.v_head_dim;
39 let kv_b = mla_heads * (config.qk_nope_head_dim + config.v_head_dim);
40 let inter = config.intermediate_size;
41 let eh = config.moe_intermediate_size;
42 let lat = config.moe_latent_size;
43
44 if name.ends_with(".self_attn.q_proj.weight")
45 || name.ends_with(".self_attn.k_proj.weight")
46 || name.ends_with(".self_attn.v_proj.weight")
47 {
48 return (TpAxis::Rows, kda_q, h);
49 }
50 if name.ends_with(".self_attn.q_conv1d.weight")
51 || name.ends_with(".self_attn.k_conv1d.weight")
52 || name.ends_with(".self_attn.v_conv1d.weight")
53 {
54 return (TpAxis::Rows, kda_q, conv_k);
55 }
56 if name.ends_with(".self_attn.g_proj.weight") {
57 let n = match mixer {
58 MixerKind::Kda => kda_q,
59 MixerKind::Mla => dv,
60 };
61 return (TpAxis::Rows, n, h);
62 }
63 if name.ends_with(".self_attn.o_proj.weight") {
64 let inn = match mixer {
65 MixerKind::Kda => kda_q,
66 MixerKind::Mla => dv,
67 };
68 return (TpAxis::Columns, h, inn);
69 }
70 if name.ends_with(".self_attn.b_proj.weight") {
71 return (TpAxis::Rows, kda_heads, h);
72 }
73 if name.ends_with(".self_attn.A_log") {
74 return (TpAxis::Rows, kda_heads, 1);
75 }
76 if name.ends_with(".self_attn.dt_bias") {
77 return (TpAxis::Rows, kda_q, 1);
78 }
79 if name.ends_with(".self_attn.f_b_proj.weight") {
80 return (TpAxis::Rows, kda_q, kda_d);
81 }
82 if name.ends_with(".self_attn.q_b_proj.weight") {
83 return (TpAxis::Rows, mla_heads * qk, config.q_lora_rank);
84 }
85 if name.ends_with(".self_attn.kv_b_proj.weight") {
86 return (TpAxis::Rows, kv_b, config.kv_lora_rank);
87 }
88 if name.ends_with(".mlp.gate_proj.weight") || name.ends_with(".mlp.up_proj.weight") {
89 return (TpAxis::Rows, inter, h);
90 }
91 if name.ends_with(".mlp.down_proj.weight") {
92 return (TpAxis::Columns, h, inter);
93 }
94 if name.contains(".block_sparse_moe.experts.") {
95 if name.ends_with(".w1.weight") || name.ends_with(".w3.weight") {
96 return (TpAxis::Rows, eh, lat);
97 }
98 if name.ends_with(".w2.weight") {
99 return (TpAxis::Columns, lat, eh);
100 }
101 }
102 (TpAxis::Replicated, 1, 1)
103}
104
105#[derive(Debug, Clone)]
108pub struct TpBytePlan {
109 pub axis: TpAxis,
110 pub local_shape: Vec<usize>,
111 pub source_bytes: usize,
112 offset: usize,
113 rows: usize,
114 stride: usize,
115 row_bytes: usize,
116 zero_f32_tail_from: Option<usize>,
117}
118
119impl TpBytePlan {
120 pub fn local_bytes(&self) -> usize {
121 self.rows * self.row_bytes
122 }
123
124 pub fn byte_ranges(&self) -> impl Iterator<Item = std::ops::Range<usize>> + '_ {
125 (0..self.rows).map(|row| {
126 let start = self.offset + row * self.stride;
127 start..start + self.row_bytes
128 })
129 }
130
131 pub fn validate_source(&self, source: &[u8]) -> Result<()> {
135 ensure!(
136 source.len() == self.source_bytes,
137 "K3 TP source bytes {} != {}",
138 source.len(),
139 self.source_bytes
140 );
141 if let Some(start) = self.zero_f32_tail_from {
142 ensure!(
143 source[start..]
144 .chunks_exact(4)
145 .all(|v| f32::from_le_bytes(v.try_into().unwrap()) == 0.0),
146 "K3 padded A_log has a nonzero/nonfinite tail; refusing to discard trained values"
147 );
148 }
149 Ok(())
150 }
151
152 pub fn copy_shard(&self, source: &[u8]) -> Result<Vec<u8>> {
154 self.validate_source(source)?;
155 let mut local = Vec::with_capacity(self.local_bytes());
156 for range in self.byte_ranges() {
157 local.extend_from_slice(&source[range]);
158 }
159 Ok(local)
160 }
161}
162
163fn product(values: &[usize]) -> Result<usize> {
164 values.iter().try_fold(1usize, |n, &v| {
165 ensure!(v > 0, "K3 TP zero dimension/element width");
166 n.checked_mul(v)
167 .ok_or_else(|| anyhow::anyhow!("K3 TP byte-size overflow"))
168 })
169}
170
171pub fn plan_tensor_bytes(
178 name: &str,
179 shape: &[usize],
180 element_bytes: usize,
181 mixer: MixerKind,
182 config: &ModelConfig,
183) -> Result<TpBytePlan> {
184 let world = config.tp_world_size;
185 let rank = config.tp_rank;
186 ensure!(
187 world > 0 && rank < world,
188 "K3 TP rank {rank} invalid for world {world}"
189 );
190 ensure!(
191 !shape.is_empty(),
192 "{name}: K3 TP requires a non-scalar tensor"
193 );
194 let elements = product(shape)?;
195 let source_bytes = product(&[elements, element_bytes])?;
196 let (axis, n, logical_k) = tensor_plan(name, mixer, config);
197 if name.ends_with(".self_attn.A_log")
200 && mixer == MixerKind::Kda
201 && n == 96
202 && config.linear_key_head_dim == 128
203 && shape == [128]
204 && element_bytes == 4
205 {
206 ensure!(
207 n.is_multiple_of(world),
208 "K3 A_log heads must divide TP world"
209 );
210 let row_bytes = n / world * element_bytes;
211 return Ok(TpBytePlan {
212 axis,
213 local_shape: vec![n / world],
214 source_bytes,
215 offset: rank * row_bytes,
216 rows: 1,
217 stride: row_bytes,
218 row_bytes,
219 zero_f32_tail_from: Some(n * element_bytes),
220 });
221 }
222 let packed = name.ends_with(".weight_packed");
223 let scale = name.ends_with(".weight_scale");
224 let storage_k = if packed || scale {
225 ensure!(
226 name.contains(".block_sparse_moe.experts.") && axis != TpAxis::Replicated,
227 "{name}: unsupported K3 packed projection"
228 );
229 ensure!(
230 element_bytes == 1,
231 "{name}: packed MXFP4/E8M0 storage must be one byte"
232 );
233 ensure!(
234 logical_k.is_multiple_of(GROUP_SIZE),
235 "{name}: K {logical_k} must align to E8M0 groups of 32"
236 );
237 if axis == TpAxis::Columns {
238 ensure!(
239 logical_k.is_multiple_of(world) && (logical_k / world).is_multiple_of(GROUP_SIZE),
240 "{name}: TP{world} column partition splits an E8M0 group of 32"
241 );
242 }
243 let divisor = if packed { 2 } else { GROUP_SIZE };
244 let k = logical_k / divisor;
245 ensure!(
246 shape == [n, k],
247 "{name}: packed shape {shape:?} != [{n}, {k}]"
248 );
249 k
250 } else {
251 if axis != TpAxis::Replicated {
252 ensure!(
253 matches!(element_bytes, 2 | 4),
254 "{name}: K3 dense TP requires BF16 or FP32"
255 );
256 ensure!(
257 shape[0] == n && elements == product(&[n, logical_k])?,
258 "{name}: dense shape {shape:?} != logical [{n}, {logical_k}]"
259 );
260 }
261 logical_k
262 };
263 let mut local_shape = shape.to_vec();
264 let (offset, rows, stride, row_bytes) = match axis {
265 TpAxis::Replicated => (0, 1, source_bytes, source_bytes),
266 TpAxis::Rows => {
267 ensure!(
268 n.is_multiple_of(world),
269 "{name}: rows {n} not divisible by TP{world}"
270 );
271 local_shape[0] /= world;
272 let row_bytes = source_bytes / world;
273 (rank * row_bytes, 1, row_bytes, row_bytes)
274 }
275 TpAxis::Columns => {
276 ensure!(
277 shape.len() == 2 && storage_k.is_multiple_of(world),
278 "{name}: columns {storage_k} not divisible by TP{world}"
279 );
280 local_shape[1] /= world;
281 let stride = product(&[storage_k, element_bytes])?;
282 let row_bytes = stride / world;
283 (rank * row_bytes, n, stride, row_bytes)
284 }
285 };
286 ensure!(
287 product(&local_shape)? * element_bytes == rows * row_bytes,
288 "{name}: local TP byte accounting mismatch"
289 );
290 Ok(TpBytePlan {
291 axis,
292 local_shape,
293 source_bytes,
294 offset,
295 rows,
296 stride,
297 row_bytes,
298 zero_f32_tail_from: None,
299 })
300}
301
302#[cfg(test)]
303mod tests;