spark_model/layers/moe/helpers_b.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! set_down_transpose_scratch.
4
5use super::*;
6
7impl MoeLayer {
8 /// Wire a shared per-prefill down_proj scratch + transposed pointer table.
9 ///
10 /// Called by the factory after the persistent MoE transpose pass falls
11 /// back to gate+up only. The scratch and pointer tables are shared
12 /// across all MoE layers — one allocation reused layer-by-layer during
13 /// the sequential forward. The same `scale2_vals` buffer is reused
14 /// from the existing untransposed `down_ptrs` (transpose preserves
15 /// per-tensor scales).
16 pub fn set_down_transpose_scratch(
17 &mut self,
18 scratch_packed: DevicePtr,
19 scratch_scale: DevicePtr,
20 packed_ptrs_t: DevicePtr,
21 scale_ptrs_t: DevicePtr,
22 ) {
23 self.down_t_scratch_packed = Some(scratch_packed);
24 self.down_t_scratch_scale = Some(scratch_scale);
25 self.down_ptrs_t = Some(ExpertPtrTable {
26 packed_ptrs: packed_ptrs_t,
27 scale_ptrs: scale_ptrs_t,
28 scale2_vals: self.down_ptrs.scale2_vals,
29 });
30 }
31
32 /// Run the batched transpose kernel to populate `down_t_scratch_*` from
33 /// the untransposed `down_ptrs` source. Must be called once at the
34 /// start of every layer's prefill, before the silu_down GEMM. No-op
35 /// when scratch isn't wired (decode-only / persistent-full-transpose
36 /// paths).
37 pub(crate) fn transpose_down_into_scratch(
38 &self,
39 ctx: &crate::layer::ForwardContext,
40 stream: u64,
41 ) -> Result<()> {
42 let Some(dpt) = self.down_ptrs_t.as_ref() else {
43 return Ok(());
44 };
45 // Only run the transpose when scratch is wired (vs persistent
46 // transpose_for_prefill_impl path which sets down_ptrs_t to its
47 // own allocations and leaves scratch fields None).
48 if self.down_t_scratch_packed.is_none() {
49 return Ok(());
50 }
51 let num_experts = ctx.config.num_experts as u32;
52 let h = ctx.config.hidden_size as u32;
53 let inter = ctx.config.moe_intermediate_size as u32;
54 // Packed: [N=hidden, K/2=inter/2] → [K/2, N] per expert.
55 crate::layers::ops::moe_transpose_u8_batched(
56 ctx.gpu,
57 self.moe_transpose_u8_batched_k,
58 self.down_ptrs.packed_ptrs,
59 dpt.packed_ptrs,
60 h,
61 inter / 2,
62 num_experts,
63 stream,
64 )?;
65 // Scale: [N, K/GROUP_SIZE=inter/16] → [K/16, N] per expert.
66 crate::layers::ops::moe_transpose_u8_batched(
67 ctx.gpu,
68 self.moe_transpose_u8_batched_k,
69 self.down_ptrs.scale_ptrs,
70 dpt.scale_ptrs,
71 h,
72 inter / 16,
73 num_experts,
74 stream,
75 )?;
76 Ok(())
77 }
78
79 /// **NOT CURRENTLY WIRED IN** — this helper attempted to overlap the
80 /// lazy down_proj transpose with the TP attention allreduce by
81 /// kicking it off on `prefill_stream` right after attention. It
82 /// regressed cold TTFT by ~30 % on GB10 — both when scheduled
83 /// against compute-bound MoE GEMMs AND when scheduled against the
84 /// (RDMA-dominated) TP allreduce window. Either GB10's SM scheduling
85 /// has hidden contention costs across streams, or the per-call
86 /// event-sync overhead exceeds the ~4 ms transpose savings.
87 ///
88 /// Kept in source for future reference — future work that figures
89 /// out the GB10 stream-scheduling pattern can re-wire it from
90 /// `qwen3_attention::trait_impl::prefill` after attention but before
91 /// the TP allreduce, then have silu_down stall via
92 /// `lazy_transpose_done_event()`.
93 #[allow(dead_code)]
94 pub(crate) fn kick_off_lazy_transpose(
95 &self,
96 ctx: &crate::layer::ForwardContext,
97 compute_stream: u64,
98 ) -> Result<()> {
99 let Some(dpt) = self.down_ptrs_t.as_ref() else {
100 return Ok(());
101 };
102 if self.down_t_scratch_packed.is_none() {
103 return Ok(());
104 }
105 // prefill_stream waits for compute_stream's "attention done" point.
106 ctx.gpu.record_event(self.event_a, compute_stream)?;
107 ctx.gpu
108 .stream_wait_event(self.prefill_stream, self.event_a)?;
109
110 let num_experts = ctx.config.num_experts as u32;
111 let h = ctx.config.hidden_size as u32;
112 let inter = ctx.config.moe_intermediate_size as u32;
113 crate::layers::ops::moe_transpose_u8_batched(
114 ctx.gpu,
115 self.moe_transpose_u8_batched_k,
116 self.down_ptrs.packed_ptrs,
117 dpt.packed_ptrs,
118 h,
119 inter / 2,
120 num_experts,
121 self.prefill_stream,
122 )?;
123 crate::layers::ops::moe_transpose_u8_batched(
124 ctx.gpu,
125 self.moe_transpose_u8_batched_k,
126 self.down_ptrs.scale_ptrs,
127 dpt.scale_ptrs,
128 h,
129 inter / 16,
130 num_experts,
131 self.prefill_stream,
132 )?;
133 // Record transpose-done event on the secondary stream. The
134 // silu_down call site stalls compute_stream on this event before
135 // reading from scratch.
136 ctx.gpu.record_event(self.event_b, self.prefill_stream)?;
137 Ok(())
138 }
139
140 /// Companion to the (currently-unwired) `kick_off_lazy_transpose`
141 /// — silu_down would call this to know whether to stall on the
142 /// secondary-stream event.
143 #[allow(dead_code)]
144 pub(crate) fn has_overlapped_transpose(&self) -> bool {
145 self.down_t_scratch_packed.is_some()
146 }
147
148 /// Companion to `kick_off_lazy_transpose`.
149 #[allow(dead_code)]
150 pub(crate) fn lazy_transpose_done_event(&self) -> u64 {
151 self.event_b
152 }
153
154 /// True when prefill dispatch (forward_batched) should route to
155 /// `_t` transposed-layout kernels.
156 ///
157 /// Fires for both unified mode (Phase 8a — originals freed) and hybrid
158 /// mode (Block C Path 2 — originals retained alongside transposed).
159 /// Both build the same persistent `*_ptrs_t` device-side pointer tables.
160 ///
161 /// Requires:
162 /// 1. `ATLAS_UNIFIED_MOE_LAYOUT=1` OR `ATLAS_HYBRID_MOE_LAYOUT=1`
163 /// (read at construction).
164 /// 2. Persistent transposed pointer tables for all three projections.
165 /// 3. NOT the lazy-scratch path — scratch-backed `down_ptrs_t` only
166 /// holds one layer at a time, so multi-layer dispatch would read
167 /// stale data. Persistent transpose pass must have populated down_t.
168 #[inline]
169 pub(crate) fn use_t_layout_for_prefill(&self) -> bool {
170 (self.unified_layout || self.hybrid_layout)
171 && self.gate_ptrs_t.is_some()
172 && self.up_ptrs_t.is_some()
173 && self.down_ptrs_t.is_some()
174 && self.down_t_scratch_packed.is_none()
175 }
176
177 /// True when decode dispatch (forward, forward_k2, forward_k3) should
178 /// route to `_t` transposed-layout kernels.
179 ///
180 /// Only fires in unified mode — hybrid mode keeps the originals so
181 /// decode + MTP verify (small N, warp-reduction wins) can preserve
182 /// the ~35 tok/s throughput that pure unified layout regresses by 15 %.
183 #[inline]
184 /// Eligible to route DECODE through the grouped read-once GEMM
185 /// (forward_prefill). Native-NVFP4 routed only: bf16/fp8-dequant gate ptrs
186 /// absent, no DeepSeek-V4 hash routing, single-GPU (no EP). Deliberately
187 /// ALLOWS the mixed BF16 shared expert (forward_prefill handles it as a
188 /// separate batched pass), unlike forward_token_major_decode which bails.
189 pub(crate) fn grouped_decode_ok(&self) -> bool {
190 self.bf16_gate_weight_ptrs.is_none()
191 && self.fp8_gate_weight_ptrs.is_none()
192 && self.tid2eid_dev.is_none()
193 }
194
195 pub(crate) fn use_t_layout_for_decode(&self) -> bool {
196 self.unified_layout
197 && !self.hybrid_layout
198 && self.gate_ptrs_t.is_some()
199 && self.up_ptrs_t.is_some()
200 && self.down_ptrs_t.is_some()
201 && self.down_t_scratch_packed.is_none()
202 }
203}
204
205impl super::MoeLayer {
206 /// The routed grouped-GEMM kernel: the bit-exact wider-K twin when it
207 /// resolved (ATLAS_MOE_GROUPED_K32=1 and the target ships it), else the
208 /// original. Both take the same grid/block, so the launcher is shared.
209 pub(super) fn grouped_gemm_kernel(&self) -> spark_runtime::gpu::KernelHandle {
210 if self.moe_grouped_gemm_k32.0 != 0 {
211 self.moe_grouped_gemm_k32
212 } else {
213 self.moe_grouped_gemm
214 }
215 }
216}
217
218impl super::MoeLayer {
219 /// Launch the routed grouped GEMM, choosing the widest tiling this build
220 /// resolved. M_TILE=256 re-reads the expert weights ONCE instead of three
221 /// times at ~160 rows/expert, so `max_m_tiles` must be recomputed against
222 /// 256 — the same adjustment the m128 path makes with `div_ceil(2)`.
223 /// All arms are bit-exact with each other.
224 ///
225 /// ⚠ BOTH WIDE ARMS MEASURED AS NON-WINS (qwen4_exp, GB10, 2026-08-27) and
226 /// are default-OFF. Controlled four-arm sweep at 28K prefill, same box,
227 /// back-to-back: baseline 272 → +QSA-TC 286 → +k32 289 → +m256 290 tok/s.
228 /// The k32/m256 deltas (+1.0% / +1.4% over the QSA-TC arm) are inside
229 /// run-to-run noise; the entire +5.1% came from the QSA tensor-core
230 /// scorer, NOT from this GEMM.
231 ///
232 /// nsys proves the null is real rather than a silent `try_kernel`
233 /// fallback: `..._m256` appears in the trace with 540 calls / 38.1% of GPU
234 /// time, and averages 31.30 ms/call against the base kernel's 26.17 ms —
235 /// i.e. ~20% SLOWER per call at matched shapes. A DRAM-bound standalone
236 /// harness predicted 1.43x for it. Do not re-enable either arm on
237 /// microbenchmark evidence; re-measure end-to-end first.
238 #[allow(clippy::too_many_arguments)]
239 pub(super) fn launch_grouped_gemm(
240 &self,
241 gpu: &dyn spark_runtime::gpu::GpuBackend,
242 a: spark_runtime::gpu::DevicePtr,
243 packed_ptrs: spark_runtime::gpu::DevicePtr,
244 scale_ptrs: spark_runtime::gpu::DevicePtr,
245 scale2_vals: spark_runtime::gpu::DevicePtr,
246 c: spark_runtime::gpu::DevicePtr,
247 expert_offsets: spark_runtime::gpu::DevicePtr,
248 sorted_token_ids: spark_runtime::gpu::DevicePtr,
249 num_experts: u32,
250 n_out: u32,
251 k: u32,
252 max_m_tiles: u32,
253 stream: u64,
254 ) -> anyhow::Result<()> {
255 if self.moe_grouped_gemm_m256.0 != 0 {
256 return crate::layers::ops::moe_w4a16_grouped_gemm_ptrtable_m256(
257 gpu,
258 self.moe_grouped_gemm_m256,
259 a,
260 packed_ptrs,
261 scale_ptrs,
262 scale2_vals,
263 c,
264 expert_offsets,
265 sorted_token_ids,
266 num_experts,
267 n_out,
268 k,
269 max_m_tiles.div_ceil(4).max(1),
270 stream,
271 );
272 }
273 crate::layers::ops::moe_w4a16_grouped_gemm_ptrtable(
274 gpu,
275 self.grouped_gemm_kernel(),
276 a,
277 packed_ptrs,
278 scale_ptrs,
279 scale2_vals,
280 c,
281 expert_offsets,
282 sorted_token_ids,
283 num_experts,
284 n_out,
285 k,
286 max_m_tiles,
287 stream,
288 )
289 }
290}