spark_model/layers/moe/
helpers_a.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Setters + transposes + transpose_for_prefill_unified_inner.
4
5use super::*;
6
7impl MoeLayer {
8    /// Transpose MoE weights for coalesced prefill GEMM reads.
9    ///
10    /// Transposes per-expert routed weights [N, K/2] → [K/2, N] to enable
11    /// the cp.async pipelined FP8-MMA K64 kernels. This doubles expert
12    /// memory (~17 GB for 35B, ~30 GB for 122B) but eliminates the
13    /// catastrophic uncoalesced B reads in the fallback grouped GEMM,
14    /// cutting MoE prefill time by ~2x.
15    /// Set pre-expert norm (Gemma-4 26B: pre_feedforward_layernorm_2).
16    /// Applied to input AFTER routing but BEFORE expert dispatch.
17    pub fn set_pre_expert_norm(&mut self, norm: crate::weight_map::DenseWeight) {
18        self.pre_expert_norm = Some(norm);
19    }
20
21    /// Set GeGLU activation for MoE experts (Gemma-4 26B).
22    /// Replaces SiLU with GELU in the sorted/unfused path and forces decode
23    /// to use the sorted path (avoiding fused SiLU kernels).
24    pub fn set_gelu_activation(&mut self, gpu: &dyn GpuBackend) -> Result<()> {
25        self.moe_act_mul = gpu.kernel("gelu", "gelu_mul")?;
26        self.gelu_activation = true;
27        Ok(())
28    }
29
30    pub fn transpose_for_prefill(
31        &mut self,
32        gpu: &dyn GpuBackend,
33        config: &atlas_core::config::ModelConfig,
34    ) -> Result<()> {
35        self.transpose_for_prefill_impl(gpu, config, true)
36    }
37
38    /// Transpose only the gate+up routed weights, leaving the down projection
39    /// in its original layout. Cuts the transpose memory cost from ~3×
40    /// (gate+up+down) to ~2× per expert. Used by MiniMax M2.7-NVFP4 EP=2
41    /// when the full transpose doesn't fit but gate+up does — the fused
42    /// `moe_w4a16_fused_gate_up_k64_n128` kernel still runs (capturing the
43    /// dominant gate+up bandwidth savings), while down stays on the
44    /// uncoalesced grouped-GEMM path.
45    pub fn transpose_gate_up_for_prefill(
46        &mut self,
47        gpu: &dyn GpuBackend,
48        config: &atlas_core::config::ModelConfig,
49    ) -> Result<()> {
50        self.transpose_for_prefill_impl(gpu, config, false)
51    }
52
53    pub(super) fn transpose_for_prefill_impl(
54        &mut self,
55        gpu: &dyn GpuBackend,
56        config: &atlas_core::config::ModelConfig,
57        include_down: bool,
58    ) -> Result<()> {
59        let h = config.hidden_size;
60        let inter = config.moe_intermediate_size;
61        let shared_inter = config.shared_expert_intermediate_size;
62
63        // Transpose per-expert routed weights for coalesced prefill GEMM reads.
64        let num_experts = self.weights.experts.len();
65        let mut gate_t = Vec::with_capacity(num_experts);
66        let mut up_t = Vec::with_capacity(num_experts);
67        let mut down_t = Vec::with_capacity(num_experts);
68
69        // ARM-2 Phase-K Family C: native-MXFP4 routed experts have per-32 E8M0
70        // scales ([N, K/32]); NVFP4 is per-16. The scale transpose must use the
71        // matching block size or the E8M0 kernels read a mis-shaped scale table.
72        let routed_gs =
73            if self.experts_scale_kind == crate::weight_map::WeightQuantFormat::Mxfp4E8m0 {
74                32
75            } else {
76                16
77            };
78        for expert in &self.weights.experts {
79            if expert.gate_proj.is_null() {
80                gate_t.push(QuantizedWeight::null());
81                up_t.push(QuantizedWeight::null());
82                if include_down {
83                    down_t.push(QuantizedWeight::null());
84                }
85            } else {
86                gate_t.push(
87                    expert
88                        .gate_proj
89                        .transpose_for_gemm_gs(gpu, inter, h, routed_gs)?,
90                );
91                up_t.push(
92                    expert
93                        .up_proj
94                        .transpose_for_gemm_gs(gpu, inter, h, routed_gs)?,
95                );
96                if include_down {
97                    down_t.push(
98                        expert
99                            .down_proj
100                            .transpose_for_gemm_gs(gpu, h, inter, routed_gs)?,
101                    );
102                }
103            }
104        }
105
106        self.gate_ptrs_t = Some(build_ptr_table_from_qw(&gate_t, gpu)?);
107        self.up_ptrs_t = Some(build_ptr_table_from_qw(&up_t, gpu)?);
108        if include_down {
109            self.down_ptrs_t = Some(build_ptr_table_from_qw(&down_t, gpu)?);
110        }
111
112        // Transpose shared expert weights (tiny: ~5 MB per layer).
113        if !self.weights.shared_expert.gate_proj.is_null() && shared_inter > 0 {
114            self.shared_gate_t = Some(self.weights.shared_expert.gate_proj.transpose_for_gemm(
115                gpu,
116                shared_inter,
117                h,
118            )?);
119            self.shared_up_t = Some(self.weights.shared_expert.up_proj.transpose_for_gemm(
120                gpu,
121                shared_inter,
122                h,
123            )?);
124            if include_down {
125                self.shared_down_t =
126                    Some(self.weights.shared_expert.down_proj.transpose_for_gemm(
127                        gpu,
128                        h,
129                        shared_inter,
130                    )?);
131            }
132        }
133
134        Ok(())
135    }
136
137    /// Phase 8a unified-layout transpose pass: build persistent transposed
138    /// gate/up/down for all experts, freeing the untransposed copies between
139    /// phases so the entire pass fits in tight memory budgets that the
140    /// non-unified `transpose_for_prefill_impl(true)` would reject.
141    ///
142    /// Phased flow (memory math for MiniMax M2.7-NVFP4 EP=2 ≈ 47 GB free):
143    ///   A. Transpose gate+up               (allocs +39 GB; free ≈ 8 GB)
144    ///   B. Free gate+up untransposed       (frees 39 GB; free ≈ 47 GB)
145    ///   C. Transpose down                  (allocs +20 GB; free ≈ 27 GB)
146    ///   D. Free down untransposed          (frees 20 GB; free ≈ 47 GB)
147    ///
148    /// Net memory: same as starting point, but layout is now unified
149    /// (transposed-only) — the `[N, K/2]` decode kernels can no longer
150    /// run; dispatch must use the `_t` decode kernels (which do).
151    ///
152    /// Caller responsibilities:
153    ///   1. Set `ATLAS_UNIFIED_MOE_LAYOUT=1` so `MoeLayer::use_t_layout_for_decode()`
154    ///      returns true at dispatch time.
155    ///   2. Call this method INSTEAD of `transpose_for_prefill` /
156    ///      `transpose_gate_up_for_prefill`.
157    pub fn transpose_for_prefill_unified(
158        &mut self,
159        gpu: &dyn GpuBackend,
160        config: &atlas_core::config::ModelConfig,
161    ) -> Result<()> {
162        self.transpose_for_prefill_unified_inner(gpu, config, false)
163    }
164
165    /// Hybrid-layout transpose pass — analogue of `transpose_for_prefill_unified`
166    /// that **keeps** the untransposed originals so decode + MTP verify dispatch
167    /// can continue using the warp-reduction kernels. Allocates ~58 GB
168    /// transposed alongside the existing ~58 GB originals on MiniMax M2.7-NVFP4
169    /// EP=2; fits in 122 GB GB10 with KV-cache headroom up to ~32K context.
170    /// Caller is responsible for memory-fit gating (factory checks free memory
171    /// before invoking this).
172    pub fn transpose_for_prefill_hybrid(
173        &mut self,
174        gpu: &dyn GpuBackend,
175        config: &atlas_core::config::ModelConfig,
176    ) -> Result<()> {
177        self.transpose_for_prefill_unified_inner(gpu, config, true)
178    }
179
180    /// Phased build of the transposed weight set. When `keep_originals` is true
181    /// (hybrid-layout mode), Phase B and Phase D frees are skipped so decode
182    /// paths still find the untransposed weights. When false (unified-layout
183    /// mode), the originals are freed between phases — current Phase 8a
184    /// behavior.
185    pub(super) fn transpose_for_prefill_unified_inner(
186        &mut self,
187        gpu: &dyn GpuBackend,
188        config: &atlas_core::config::ModelConfig,
189        keep_originals: bool,
190    ) -> Result<()> {
191        let h = config.hidden_size;
192        let inter = config.moe_intermediate_size;
193        let shared_inter = config.shared_expert_intermediate_size;
194        let _num_experts = self.weights.experts.len();
195
196        // ── Phase A: transpose gate+up routed experts ──
197        // ARM-2 Phase-K Family C: native-MXFP4 routed experts are per-32 E8M0.
198        let routed_gs =
199            if self.experts_scale_kind == crate::weight_map::WeightQuantFormat::Mxfp4E8m0 {
200                32
201            } else {
202                16
203            };
204        let gate_src: Vec<QuantizedWeight> = self
205            .weights
206            .experts
207            .iter()
208            .map(|e| {
209                if e.gate_proj.is_null() {
210                    QuantizedWeight::null()
211                } else {
212                    e.gate_proj
213                }
214            })
215            .collect();
216        let up_src: Vec<QuantizedWeight> = self
217            .weights
218            .experts
219            .iter()
220            .map(|e| {
221                if e.gate_proj.is_null() {
222                    QuantizedWeight::null()
223                } else {
224                    e.up_proj
225                }
226            })
227            .collect();
228        let gate_t = self.transpose_experts_gpu(gpu, &gate_src, inter, h, routed_gs)?;
229        let up_t = self.transpose_experts_gpu(gpu, &up_src, inter, h, routed_gs)?;
230        self.gate_ptrs_t = Some(build_ptr_table_from_qw(&gate_t, gpu)?);
231        self.up_ptrs_t = Some(build_ptr_table_from_qw(&up_t, gpu)?);
232        // Shared expert (tiny, do unconditionally — fits regardless).
233        if !self.weights.shared_expert.gate_proj.is_null() && shared_inter > 0 {
234            self.shared_gate_t = Some(self.weights.shared_expert.gate_proj.transpose_for_gemm(
235                gpu,
236                shared_inter,
237                h,
238            )?);
239            self.shared_up_t = Some(self.weights.shared_expert.up_proj.transpose_for_gemm(
240                gpu,
241                shared_inter,
242                h,
243            )?);
244        }
245
246        if !keep_originals {
247            // ── Phase B: free gate+up untransposed ──
248            // The previous gate_ptrs / up_ptrs device-side pointer tables now
249            // contain stale addresses, but the unified dispatch never reads
250            // them (gated by `use_t_layout_for_decode()`).
251            for expert in &mut self.weights.experts {
252                if !expert.gate_proj.weight.is_null() {
253                    gpu.free(expert.gate_proj.weight)?;
254                    gpu.free(expert.gate_proj.weight_scale)?;
255                    expert.gate_proj.weight = DevicePtr::NULL;
256                    expert.gate_proj.weight_scale = DevicePtr::NULL;
257                }
258                if !expert.up_proj.weight.is_null() {
259                    gpu.free(expert.up_proj.weight)?;
260                    gpu.free(expert.up_proj.weight_scale)?;
261                    expert.up_proj.weight = DevicePtr::NULL;
262                    expert.up_proj.weight_scale = DevicePtr::NULL;
263                }
264            }
265            if !self.weights.shared_expert.gate_proj.weight.is_null() && shared_inter > 0 {
266                gpu.free(self.weights.shared_expert.gate_proj.weight)?;
267                gpu.free(self.weights.shared_expert.gate_proj.weight_scale)?;
268                self.weights.shared_expert.gate_proj.weight = DevicePtr::NULL;
269                self.weights.shared_expert.gate_proj.weight_scale = DevicePtr::NULL;
270                gpu.free(self.weights.shared_expert.up_proj.weight)?;
271                gpu.free(self.weights.shared_expert.up_proj.weight_scale)?;
272                self.weights.shared_expert.up_proj.weight = DevicePtr::NULL;
273                self.weights.shared_expert.up_proj.weight_scale = DevicePtr::NULL;
274            }
275        }
276
277        // ── Phase C: transpose down routed experts ──
278        let down_src: Vec<QuantizedWeight> = self
279            .weights
280            .experts
281            .iter()
282            .map(|e| {
283                if e.down_proj.is_null() {
284                    QuantizedWeight::null()
285                } else {
286                    e.down_proj
287                }
288            })
289            .collect();
290        let down_t = self.transpose_experts_gpu(gpu, &down_src, h, inter, routed_gs)?;
291        self.down_ptrs_t = Some(build_ptr_table_from_qw(&down_t, gpu)?);
292        if !self.weights.shared_expert.down_proj.is_null() && shared_inter > 0 {
293            self.shared_down_t = Some(self.weights.shared_expert.down_proj.transpose_for_gemm(
294                gpu,
295                h,
296                shared_inter,
297            )?);
298        }
299
300        if !keep_originals {
301            // ── Phase D: free down untransposed ──
302            for expert in &mut self.weights.experts {
303                if !expert.down_proj.weight.is_null() {
304                    gpu.free(expert.down_proj.weight)?;
305                    gpu.free(expert.down_proj.weight_scale)?;
306                    expert.down_proj.weight = DevicePtr::NULL;
307                    expert.down_proj.weight_scale = DevicePtr::NULL;
308                }
309            }
310            if !self.weights.shared_expert.down_proj.weight.is_null() && shared_inter > 0 {
311                gpu.free(self.weights.shared_expert.down_proj.weight)?;
312                gpu.free(self.weights.shared_expert.down_proj.weight_scale)?;
313                self.weights.shared_expert.down_proj.weight = DevicePtr::NULL;
314                self.weights.shared_expert.down_proj.weight_scale = DevicePtr::NULL;
315            }
316        }
317
318        Ok(())
319    }
320
321    /// Transpose one projection across ALL routed experts on the GPU, into a
322    /// single slab allocation per buffer.
323    ///
324    /// Replaces a per-expert `QuantizedWeight::transpose_for_gemm_gs`, which
325    /// round-trips every expert through the host (D2H, a strided host byte
326    /// loop, H2D) and takes two `gpu.alloc`s each. At 256 experts x 3
327    /// projections x ~47 MoE layers that was ~36k host round-trips and ~145k
328    /// allocations, measured at ~1.0 s per layer (~48 s of load). The batched
329    /// kernel is the same one the lazy down-scratch path already uses.
330    ///
331    /// `src` supplies the per-expert untransposed `[n, k/2]` packed bytes and
332    /// `[n, k/group_size]` scales; the returned `QuantizedWeight`s point into
333    /// the two slabs and carry the source's scale metadata unchanged.
334    #[allow(clippy::too_many_arguments)]
335    fn transpose_experts_gpu(
336        &self,
337        gpu: &dyn GpuBackend,
338        src: &[QuantizedWeight],
339        n: usize,
340        k: usize,
341        group_size: usize,
342    ) -> Result<Vec<QuantizedWeight>> {
343        let num_experts = src.len();
344        let packed_each = n * (k / 2);
345        let scale_each = n * (k / group_size);
346        anyhow::ensure!(
347            packed_each > 0 && scale_each > 0,
348            "transpose_experts_gpu: zero-sized projection (n={n} k={k} gs={group_size})"
349        );
350
351        // One slab per buffer instead of two allocations per expert.
352        let packed_slab = gpu.alloc(num_experts * packed_each)?;
353        let scale_slab = gpu.alloc(num_experts * scale_each)?;
354
355        // Destinations carve the slabs; a NULL source keeps a NULL slot so the
356        // kernel's own NULL guard skips that expert (EP-remote convention).
357        let mut out = Vec::with_capacity(num_experts);
358        for (e, w) in src.iter().enumerate() {
359            if w.is_null() {
360                out.push(QuantizedWeight::null());
361            } else {
362                out.push(QuantizedWeight {
363                    weight: packed_slab.offset(e * packed_each),
364                    weight_scale: scale_slab.offset(e * scale_each),
365                    weight_scale_2: w.weight_scale_2,
366                    input_scale: w.input_scale,
367                    weight_scale_2_vec: w.weight_scale_2_vec,
368                });
369            }
370        }
371
372        let src_tbl = build_ptr_table_from_qw(src, gpu)?;
373        let dst_tbl = build_ptr_table_from_qw(&out, gpu)?;
374        let stream = gpu.default_stream();
375        // Packed [n, k/2] -> [k/2, n].
376        crate::layers::ops::moe_transpose_u8_batched(
377            gpu,
378            self.moe_transpose_u8_batched_k,
379            src_tbl.packed_ptrs,
380            dst_tbl.packed_ptrs,
381            n as u32,
382            (k / 2) as u32,
383            num_experts as u32,
384            stream,
385        )?;
386        // Scales [n, k/group_size] -> [k/group_size, n].
387        crate::layers::ops::moe_transpose_u8_batched(
388            gpu,
389            self.moe_transpose_u8_batched_k,
390            src_tbl.scale_ptrs,
391            dst_tbl.scale_ptrs,
392            n as u32,
393            (k / group_size) as u32,
394            num_experts as u32,
395            stream,
396        )?;
397        gpu.synchronize(stream)?;
398        // The pointer tables were scratch for the launch only.
399        gpu.free(src_tbl.packed_ptrs)?;
400        gpu.free(src_tbl.scale_ptrs)?;
401        gpu.free(src_tbl.scale2_vals)?;
402        gpu.free(dst_tbl.packed_ptrs)?;
403        gpu.free(dst_tbl.scale_ptrs)?;
404        gpu.free(dst_tbl.scale2_vals)?;
405        Ok(out)
406    }
407
408    /// Build per-expert swizzled SFB weight-scale tables for the CUTLASS grouped
409    /// NVFP4 path (`ATLAS_HOLO_MOE_GROUPED_CUTLASS`). For each expert, swizzle the
410    /// `[K/16,N]` `gate_ptrs_t`/`up_ptrs_t` scale into the CUTLASS SFB atom via
411    /// `pack_weight_sfb`, then upload the per-expert pointer arrays. The grouped
412    /// kernel pairs these with `gate_ptrs.packed` (`[N,K/2]`) + the real per-expert
413    /// `scale2`. Requires FAST_MOE=full (gate_ptrs_t/up_ptrs_t present); no-op else.
414    pub fn build_cutlass_grouped_sfb(
415        &mut self,
416        gpu: &dyn GpuBackend,
417        config: &atlas_core::config::ModelConfig,
418        stream: u64,
419    ) -> Result<()> {
420        let h = config.hidden_size;
421        let inter = config.moe_intermediate_size;
422        let num = self.weights.experts.len();
423        // Swizzled SFB atom size (bytes): round_up(N,128) * round_up(K/16,4).
424        let sfb_len = |n: usize, k: usize| n.div_ceil(128) * 128 * (k / 16).div_ceil(4) * 4;
425        // Prefer the Atlas-transposed [K/16,N] scales when they exist. Without
426        // them (a checkpoint served straight from its native tables, e.g.
427        // Laguna with the unified transpose disabled) fall back to the
428        // ORIGINAL [N,K/16] scales and tell the packer to read N-major — the
429        // SFB output is identical, so this avoids materialising a transposed
430        // copy purely to feed the swizzle.
431        let (gate_scale_dev, up_scale_dev, src_n_major) =
432            match (self.gate_ptrs_t.as_ref(), self.up_ptrs_t.as_ref()) {
433                (Some(g), Some(u)) => (g.scale_ptrs, u.scale_ptrs, false),
434                _ => (self.gate_ptrs.scale_ptrs, self.up_ptrs.scale_ptrs, true),
435            };
436        if gate_scale_dev.is_null() || up_scale_dev.is_null() {
437            return Ok(());
438        }
439        let down_scale_dev = match self.down_ptrs_t.as_ref() {
440            Some(d) => Some(d.scale_ptrs),
441            None if !self.down_ptrs.scale_ptrs.is_null() => Some(self.down_ptrs.scale_ptrs),
442            None => None,
443        };
444        let mut owned: Vec<DevicePtr> = Vec::new();
445        // Swizzle each expert's [K/16,N] scale into the CUTLASS SFB atom. `n`/`k`
446        // are the projection's GEMM dims: gate/up = (inter, hidden); down = (hidden, inter).
447        // Returns the HOST vector of per-expert SFB pointers: the grouped C entry
448        // consumes pointer values host-side, so no device copy of this table is
449        // ever made — the values go straight into the layer-owned snapshot below.
450        let mut build_one = |scale_ptrs_dev: DevicePtr, n: usize, k: usize| -> Result<Vec<u64>> {
451            let len = sfb_len(n, k);
452            let sp = crate::layers::ops::read_expert_ptrs_u64(gpu, scale_ptrs_dev, num)?;
453            let mut sfb_ptrs = vec![0u64; num];
454            for (e, &sptr) in sp.iter().enumerate() {
455                if sptr == 0 {
456                    continue; // remote/placeholder expert
457                }
458                let sfb = gpu.alloc(len)?;
459                spark_runtime::cutlass::pack_weight_sfb(
460                    sptr,
461                    sfb.0,
462                    n as u32,
463                    k as u32,
464                    src_n_major,
465                    stream,
466                )?;
467                sfb_ptrs[e] = sfb.0;
468                owned.push(sfb);
469            }
470            gpu.synchronize(stream)?;
471            Ok(sfb_ptrs)
472        };
473        let gate_sfb = build_one(gate_scale_dev, inter, h)?;
474        let up_sfb = build_one(up_scale_dev, inter, h)?;
475        let down = match down_scale_dev {
476            Some(ds) => Some((
477                self.down_ptrs.packed_ptrs,
478                build_one(ds, h, inter)?,
479                self.down_ptrs.scale2_vals,
480            )),
481            None => None,
482        };
483        self.cutlass_grouped_host = Some(crate::layers::ops::MoeCutlassHostTables::snapshot(
484            gpu,
485            num,
486            self.gate_ptrs.packed_ptrs,
487            gate_sfb,
488            self.gate_ptrs.scale2_vals,
489            self.up_ptrs.packed_ptrs,
490            up_sfb,
491            self.up_ptrs.scale2_vals,
492            down,
493        )?);
494        self._cutlass_sfb_owned = owned;
495        tracing::info!(
496            "CUTLASS grouped SFB: built {num} experts gate/up (N={inter} K={h}) + down (N={h} K={inter})"
497        );
498        Ok(())
499    }
500}