metrale_model_arch/glm5next_dsa/
build.rs1use anyhow::{Result, bail};
26use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend};
27
28use super::Glm5NextDsaConfig;
29use super::layer::Glm5NextDsaWeights;
30use super::tp::{DsaShard, DsaTpPlan};
31
32pub type LoadFn<'a> = &'a dyn Fn(&str) -> Result<Vec<f32>>;
35
36fn absorb_threads(rows: usize) -> usize {
40 let want = std::env::var("METRALE_MLA_ABSORB_THREADS")
41 .ok()
42 .and_then(|v| v.trim().parse::<usize>().ok())
43 .filter(|&n| n > 0)
44 .unwrap_or_else(|| {
45 std::thread::available_parallelism()
46 .map(|n| n.get())
47 .unwrap_or(1)
48 });
49 want.clamp(1, rows.max(1))
50}
51
52#[allow(clippy::too_many_arguments)]
57fn absorb_q_rows(
58 out: &mut [f32],
59 row0: usize,
60 kv_b: &[f32],
61 q_b: &[f32],
62 kvl: usize,
63 ql: usize,
64 nope: usize,
65 vd: usize,
66) {
67 for (i, acc) in out.chunks_exact_mut(ql).enumerate() {
68 let row = row0 + i;
69 let h = row / kvl;
70 let c = row % kvl;
71 let kv_base = h * (nope + vd);
72 let qb_base = h * nope;
73 acc.fill(0.0);
74 for r in 0..nope {
75 let w = kv_b[(kv_base + r) * kvl + c];
76 let q_row = &q_b[(qb_base + r) * ql..][..ql];
77 for (a, &q) in acc.iter_mut().zip(q_row.iter()) {
78 *a += q * w;
79 }
80 }
81 }
82}
83
84pub fn absorb_q(
91 cfg: &Glm5NextDsaConfig,
92 q_b: &[f32],
93 kv_b: &[f32],
94 full_heads: usize,
95) -> Result<Vec<f32>> {
96 let (nope, vd, kvl, ql) = (
97 cfg.qk_nope_head_dim,
98 cfg.v_head_dim,
99 cfg.kv_lora_rank,
100 cfg.q_lora_rank,
101 );
102 let qk = cfg.qk_head_dim();
103 if q_b.len() != full_heads * qk * ql {
104 bail!(
105 "absorb_q: q_b_proj has {} elems, expected {}",
106 q_b.len(),
107 full_heads * qk * ql
108 );
109 }
110 if kv_b.len() != full_heads * (nope + vd) * kvl {
111 bail!(
112 "absorb_q: kv_b_proj has {} elems, expected {}",
113 kv_b.len(),
114 full_heads * (nope + vd) * kvl
115 );
116 }
117 if cfg.qk_rope_head_dim != 0 {
120 bail!(
121 "absorb_q: NoPE only; qk_rope_head_dim is {}",
122 cfg.qk_rope_head_dim
123 );
124 }
125
126 let rows = full_heads * kvl;
127 let mut out = vec![0f32; rows * ql];
128 let threads = absorb_threads(rows);
129 if threads <= 1 {
130 absorb_q_rows(&mut out, 0, kv_b, q_b, kvl, ql, nope, vd);
131 } else {
132 let rows_per = rows.div_ceil(threads);
133 std::thread::scope(|scope| {
134 for (chunk_idx, chunk) in out.chunks_mut(rows_per * ql).enumerate() {
135 let row0 = chunk_idx * rows_per;
136 scope.spawn(move || {
137 absorb_q_rows(chunk, row0, kv_b, q_b, kvl, ql, nope, vd);
138 });
139 }
140 });
141 }
142 Ok(out)
143}
144
145pub fn absorb_o(
154 cfg: &Glm5NextDsaConfig,
155 o_local: &[f32],
156 kv_b_local: &[f32],
157 local_heads: usize,
158) -> Result<Vec<f32>> {
159 let (nope, vd, kvl, hidden) = (
160 cfg.qk_nope_head_dim,
161 cfg.v_head_dim,
162 cfg.kv_lora_rank,
163 cfg.hidden,
164 );
165 if cfg.qk_rope_head_dim != 0 {
166 bail!(
167 "absorb_o: NoPE only; qk_rope_head_dim is {}",
168 cfg.qk_rope_head_dim
169 );
170 }
171 let in_w = local_heads * vd;
172 let out_w = local_heads * kvl;
173 if o_local.len() != hidden * in_w {
174 bail!(
175 "absorb_o: o_proj has {} elems, expected {} ([{hidden}, {in_w}])",
176 o_local.len(),
177 hidden * in_w
178 );
179 }
180 if kv_b_local.len() != local_heads * (nope + vd) * kvl {
181 bail!(
182 "absorb_o: kv_b_proj slice has {} elems, expected {}",
183 kv_b_local.len(),
184 local_heads * (nope + vd) * kvl
185 );
186 }
187
188 let mut out = vec![0f32; hidden * out_w];
189 let threads = std::thread::available_parallelism()
192 .map(|n| n.get())
193 .unwrap_or(1)
194 .clamp(1, hidden);
195 let rows = hidden.div_ceil(threads);
196 std::thread::scope(|sc| {
197 for (o_chunk, i_chunk) in out
198 .chunks_mut(rows * out_w)
199 .zip(o_local.chunks(rows * in_w))
200 {
201 sc.spawn(move || {
202 for (dst_row, src_row) in o_chunk.chunks_mut(out_w).zip(i_chunk.chunks(in_w)) {
203 for h in 0..local_heads {
204 let dst = &mut dst_row[h * kvl..(h + 1) * kvl];
205 let v_base = h * (nope + vd) + nope;
206 for r in 0..vd {
207 let w = src_row[h * vd + r];
208 let src = &kv_b_local[(v_base + r) * kvl..(v_base + r) * kvl + kvl];
209 for (d, k) in dst.iter_mut().zip(src) {
210 *d += w * k;
211 }
212 }
213 }
214 }
215 });
216 }
217 });
218 Ok(out)
219}
220
221fn row_slice(v: &[f32], row_elems: usize, start: usize, end: usize) -> Vec<f32> {
223 v[start * row_elems..end * row_elems].to_vec()
224}
225
226fn col_slice(v: &[f32], row_elems: usize, start: usize, end: usize) -> Vec<f32> {
229 v.chunks(row_elems)
230 .flat_map(|r| r[start..end].iter().copied())
231 .collect()
232}
233
234pub fn shard_host(plan: &super::tp::DsaTensorPlan, full: &[f32]) -> Vec<f32> {
236 match plan.kind {
237 DsaShard::Replicated => full.to_vec(),
238 DsaShard::HeadRows => row_slice(
239 full,
240 plan.full_row_elems,
241 plan.src_row_offset,
242 plan.src_row_offset + plan.local_rows,
243 ),
244 DsaShard::HeadCols => col_slice(
245 full,
246 plan.full_row_elems,
247 plan.src_col_offset,
248 plan.src_col_offset + plan.local_row_elems,
249 ),
250 }
251}
252
253fn up_bf16(gpu: &dyn GpuBackend, v: &[f32]) -> Result<DevicePtr> {
254 let b: Vec<u8> = v
255 .iter()
256 .flat_map(|x| half::bf16::from_f32(*x).to_le_bytes())
257 .collect();
258 let p = gpu.alloc(b.len().max(1))?;
259 gpu.copy_h2d(&b, p)?;
260 Ok(p)
261}
262fn up_f32(gpu: &dyn GpuBackend, v: &[f32]) -> Result<DevicePtr> {
263 let b: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
264 let p = gpu.alloc(b.len().max(1))?;
265 gpu.copy_h2d(&b, p)?;
266 Ok(p)
267}
268
269pub fn build_dsa_weights(
271 gpu: &dyn GpuBackend,
272 cfg: &Glm5NextDsaConfig,
273 plan: &DsaTpPlan,
274 load: LoadFn<'_>,
275) -> Result<Glm5NextDsaWeights> {
276 let get = |n: &str| -> Result<Vec<f32>> { load(&format!("self_attn.{n}")) };
277 let shard = |n: &'static str, full: Vec<f32>| -> Result<Vec<f32>> {
278 let p = plan
279 .get(n)
280 .ok_or_else(|| anyhow::anyhow!("no shard plan for {n}"))?;
281 Ok(shard_host(p, &full))
282 };
283
284 let kv_b = get("kv_b_proj.weight")?;
285
286 let q_absorb_full = absorb_q(cfg, &get("q_b_proj.weight")?, &kv_b, plan.full_heads)?;
289 let per_head = cfg.kv_lora_rank;
290 let start = plan.tp_rank * plan.local_heads * per_head;
291 let len = plan.local_heads * per_head;
292 let q_absorb = row_slice(&q_absorb_full, cfg.q_lora_rank, start, start + len);
293
294 let kv_b_rows = cfg.qk_nope_head_dim + cfg.v_head_dim;
297 let kv_b_local = row_slice(
298 &kv_b,
299 cfg.kv_lora_rank,
300 plan.tp_rank * plan.local_heads * kv_b_rows,
301 (plan.tp_rank + 1) * plan.local_heads * kv_b_rows,
302 );
303 let o_absorb = absorb_o(
304 cfg,
305 &shard("o_proj", get("o_proj.weight")?)?,
306 &kv_b_local,
307 plan.local_heads,
308 )?;
309
310 let scale = (cfg.index_heads as f32).powf(-0.5);
312 let weights_proj: Vec<f32> = get("indexer.weights_proj.weight")?
313 .iter()
314 .map(|x| x * scale)
315 .collect();
316
317 let ape = get("indexer.index_kpool_compress_ape")?;
319
320 Ok(Glm5NextDsaWeights {
321 q_a_proj: up_bf16(gpu, &get("q_a_proj.weight")?)?,
322 q_a_layernorm: up_bf16(gpu, &get("q_a_layernorm.weight")?)?,
323 q_absorb: up_bf16(gpu, &q_absorb)?,
324 kv_a_proj: up_bf16(gpu, &get("kv_a_proj_with_mqa.weight")?)?,
325 kv_a_layernorm: up_bf16(gpu, &get("kv_a_layernorm.weight")?)?,
326 o_absorb: up_bf16(gpu, &o_absorb)?,
327 wk: up_bf16(gpu, &get("indexer.wk.weight")?)?,
328 k_norm_weight: up_bf16(gpu, &get("indexer.k_norm.weight")?)?,
329 k_norm_bias: up_bf16(gpu, &get("indexer.k_norm.bias")?)?,
330 compress_gate: up_bf16(gpu, &get("indexer.index_kpool_compress_gate")?)?,
331 wq_b: up_bf16(gpu, &get("indexer.wq_b.weight")?)?,
332 weights_proj: up_bf16(gpu, &weights_proj)?,
333 ape: up_f32(gpu, &ape)?,
334 })
335}
336
337#[cfg(test)]
338mod tests;