metrale_model_layers/layers/moe/
forward.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: `MoeLayer::forward`, the MoE of one decode token.
4//! `fp32_routing_active` and `apply_zero_expert` are in `forward/zero_expert.rs`.
5//!
6//! Owner: model-layers (MoE).
7//! Invariants: none beyond the types.
8
9use super::*;
10
11mod debug_dumps;
12mod ep_reduce;
13mod route;
14mod zero_expert;
15
16impl MoeLayer {
17    pub fn forward(
18        &self,
19        input: DevicePtr,
20        ctx: &ForwardContext,
21        stream: u64,
22    ) -> Result<DevicePtr> {
23        // 2026-09-25: With one sequence this pass folds the MoE LoRA deltas
24        // below, with no per-row adapter map. With more (a per-token call inside
25        // a batched decode), `reject_decode_lora` refuses an adapter-routed batch
26        // instead.
27        let single_seq_decode = ctx.attn_metadata.as_ref().map_or(1, |m| m.num_seqs) <= 1;
28        if !single_seq_decode {
29            self.reject_decode_lora(ctx, "forward")?;
30        }
31        // 2026-09-27: An NVFP4 `--expert-quantization` tier: the grouped decode, whose rows do
32        // not depend on how many share the call.
33        if self.nvfp4_grouped_decode_ok(1, ctx) {
34            return self
35                .forward_nvfp4_grouped_decode(input, 1, ctx, stream)
36                .map(|()| ctx.buffers.moe_output());
37        }
38        // 2026-09-28: The tensor-core FP8 expert kernels' grouped decode (`fp8_grouped_tc.rs`).
39        if let Some(out) = self.forward_fp8_grouped_tc_one_row(input, ctx, stream) {
40            return out;
41        }
42        // 2026-09-25: With one sequence, the router delta folds onto
43        // `gate_logits` before top-k and the expert gate/up/down deltas onto
44        // their intermediates; each fold's `moe_route_gate` still refuses a
45        // `Refuse` route.
46        //
47        // DFlash capture layers route this token through `forward_prefill(1)`
48        // when the `frankenstein_decode_via_prefill` lever
49        // (`METRALE_FRANKENSTEIN_DECODE_VIA_PREFILL`) is on; every other layer
50        // takes the decode path below.
51        if self.is_dflash_capture_layer && ctx.levers.frankenstein_decode_via_prefill {
52            if ctx.stats.once("log:moe_route") {
53                tracing::info!(
54                    "FRANKENSTEIN: routing DFlash capture-layer MoE decode through forward_prefill(M=1) (one-time log)"
55                );
56            }
57            self.forward_prefill(input, 1, ctx, stream)?;
58            return Ok(ctx.buffers.moe_output());
59        }
60        self.refuse_lean_layout("MoeLayer::forward (legacy decode kernels)")?;
61
62        let h = ctx.config.hidden_size as u32;
63        let inter = ctx.config.moe_intermediate_size as u32;
64        let shared_inter = ctx.config.shared_expert_intermediate_size as u32;
65        let num_experts = ctx.config.num_experts as u32;
66        let top_k = ctx.config.num_experts_per_tok as u32;
67        let profile = ctx.profile;
68
69        macro_rules! prof {
70            ($label:expr, $body:expr) => {{
71                if profile {
72                    let t = std::time::Instant::now();
73                    let r = $body;
74                    ctx.gpu.synchronize(stream)?;
75                    tracing::info!("    MoE {}: {:.0}μs", $label, t.elapsed().as_micros());
76                    r
77                } else {
78                    $body
79                }
80            }};
81        }
82
83        let scratch = ctx.buffers.scratch();
84        let indices_dev = scratch;
85        let weights_dev = scratch.offset(top_k as usize * 4);
86
87        {
88            let router_in = self.router_input(input, 1, h, ctx, stream)?;
89            let gate_logits = ctx.buffers.gate_logits();
90            prof!("gate", {
91                self.decode_gate_gemv(router_in, gate_logits, h, ctx, stream)
92            })?;
93
94            // 2026-09-25: Fold the router LoRA delta onto `gate_logits` before
95            // top-k, with the prefill hook at n=1. It launches only device
96            // kernels, so it can be captured; a no-op without a router delta.
97            if single_seq_decode {
98                self.apply_router_lora_prefill(router_in, gate_logits, 1, ctx, stream)?;
99            }
100
101            prof!("topk", {
102                if let Some(tid2eid) = self.tid2eid_dev {
103                    // 2026-09-25: Hash routing: the experts are the static
104                    // `tid2eid[token_id]` row; the gate's sqrtsoftplus scores
105                    // weight them (`moe_hash_route.cu`).
106                    let token_ids = ctx.token_ids.ok_or_else(|| {
107                        anyhow::anyhow!(
108                            "DeepSeek-V4 hash-MoE layer requires ForwardContext.token_ids (decode)"
109                        )
110                    })?;
111                    ops::moe_hash_route(
112                        ctx.gpu,
113                        self.moe_hash_route_k,
114                        gate_logits,
115                        tid2eid,
116                        token_ids,
117                        indices_dev,
118                        weights_dev,
119                        num_experts,
120                        top_k,
121                        ctx.config.norm_topk_prob,
122                        ctx.config.routed_scaling_factor as f32,
123                        stream,
124                    )
125                } else {
126                    self.decode_topk_scored(
127                        gate_logits,
128                        indices_dev,
129                        weights_dev,
130                        num_experts,
131                        top_k,
132                        ctx,
133                        stream,
134                    )
135                }
136            })?;
137        }
138
139        if tracing::enabled!(tracing::Level::DEBUG) && !ctx.graph_capture {
140            debug_dumps::debug_routing(ctx, indices_dev, weights_dev, top_k, stream)?;
141        }
142
143        // 2026-09-25: `pre_expert_norm`, when the layer has one, runs after
144        // routing into `ssm_deinterleaved`, leaving `input` unchanged for the
145        // blend's shared-expert gate below.
146        let expert_input = if let Some(ref norm_w) = self.pre_expert_norm {
147            let normed = ctx.buffers.ssm_deinterleaved();
148            let eps = ctx.config.rms_norm_eps as f32;
149            prof!("pre_expert_norm", {
150                ops::rms_norm(
151                    ctx.gpu,
152                    self.pre_expert_norm_k,
153                    input,
154                    norm_w,
155                    normed,
156                    1,
157                    h,
158                    eps,
159                    stream,
160                )
161            })?;
162            normed
163        } else {
164            input
165        };
166
167        let expert_gate_out = ctx.buffers.expert_gate_out();
168        let expert_up_out = ctx.buffers.expert_up_out();
169        let expert_down_out = ctx.buffers.expert_down_out();
170        // 2026-09-25: The start of `logits()` is the shared-expert gate scratch
171        // here. Another user of `logits()` during the MoE must start past it
172        // (`trait_impl/decode_b.rs` puts decode metadata at offset 65536).
173        let shared_gate_scratch = ctx.buffers.logits();
174        let shared_up_scratch = ctx.buffers.ssm_qkvz();
175        let shared_out = ctx.buffers.attn_output();
176
177        if let (Some(gp), Some(up), Some(dp), Some(shared)) = (
178            self.bf16_gate_weight_ptrs,
179            self.bf16_up_weight_ptrs,
180            self.bf16_down_weight_ptrs,
181            self.bf16_shared_expert,
182        ) {
183            // 2026-09-25: BF16 experts (`set_bf16_experts`).
184            prof!("exp_gate_up_bf16", {
185                ops::moe_expert_gate_up_shared_bf16(
186                    ctx.gpu,
187                    self.moe_expert_gate_up_shared_bf16_k,
188                    expert_input,
189                    gp,
190                    expert_gate_out,
191                    up,
192                    expert_up_out,
193                    indices_dev,
194                    shared.gate_proj.weight,
195                    shared_gate_scratch,
196                    shared.up_proj.weight,
197                    shared_up_scratch,
198                    inter,
199                    h,
200                    top_k,
201                    stream,
202                )
203            })?;
204            if single_seq_decode {
205                self.apply_expert_lora_decode_gateup(
206                    expert_gate_out,
207                    expert_up_out,
208                    expert_input,
209                    indices_dev,
210                    top_k,
211                    top_k,
212                    DevicePtr::NULL,
213                    ctx,
214                    stream,
215                )?;
216            }
217            prof!("exp_silu_down_bf16", {
218                ops::moe_expert_silu_down_shared_bf16(
219                    ctx.gpu,
220                    self.moe_expert_silu_down_shared_bf16_k,
221                    expert_gate_out,
222                    expert_up_out,
223                    dp,
224                    expert_down_out,
225                    indices_dev,
226                    shared_gate_scratch,
227                    shared_up_scratch,
228                    shared.down_proj.weight,
229                    shared_out,
230                    h,
231                    inter,
232                    top_k,
233                    stream,
234                )
235            })?;
236        } else if let (Some(gp), Some(up), Some(dp), Some(sh)) = (
237            &self.fp8_gate_weight_ptrs,
238            &self.fp8_up_weight_ptrs,
239            &self.fp8_down_weight_ptrs,
240            &self.fp8_shared_expert,
241        ) {
242            prof!("exp_gate_up_fp8", {
243                ops::moe_expert_gate_up_shared_fp8(
244                    ctx.gpu,
245                    self.moe_expert_gate_up_shared_fp8,
246                    expert_input,
247                    gp.weight_ptrs,
248                    gp.scale_ptrs,
249                    expert_gate_out,
250                    up.weight_ptrs,
251                    up.scale_ptrs,
252                    expert_up_out,
253                    indices_dev,
254                    &sh.gate_proj,
255                    shared_gate_scratch,
256                    &sh.up_proj,
257                    shared_up_scratch,
258                    inter,
259                    h,
260                    top_k,
261                    stream,
262                )
263            })?;
264
265            if single_seq_decode {
266                self.apply_expert_lora_decode_gateup(
267                    expert_gate_out,
268                    expert_up_out,
269                    expert_input,
270                    indices_dev,
271                    top_k,
272                    top_k,
273                    DevicePtr::NULL,
274                    ctx,
275                    stream,
276                )?;
277            }
278            prof!("exp_silu_down_fp8", {
279                ops::moe_expert_silu_down_shared_fp8(
280                    ctx.gpu,
281                    self.moe_expert_silu_down_shared_fp8,
282                    expert_gate_out,
283                    expert_up_out,
284                    dp.weight_ptrs,
285                    dp.scale_ptrs,
286                    expert_down_out,
287                    indices_dev,
288                    shared_gate_scratch,
289                    shared_up_scratch,
290                    &sh.down_proj,
291                    shared_out,
292                    h,
293                    inter,
294                    top_k,
295                    stream,
296                )
297            })?;
298        } else if self.use_t_layout_for_decode() {
299            prof!("exp_unified_t", {
300                self.dispatch_unified_t_decode(
301                    ctx,
302                    expert_input,
303                    expert_gate_out,
304                    expert_up_out,
305                    expert_down_out,
306                    shared_gate_scratch,
307                    shared_up_scratch,
308                    shared_out,
309                    indices_dev,
310                    h,
311                    inter,
312                    top_k,
313                    single_seq_decode,
314                    stream,
315                )
316            })?;
317        } else {
318            prof!("exp_gate_up", {
319                ops::moe_expert_gate_up_shared(
320                    ctx.gpu,
321                    self.moe_expert_gate_up_shared,
322                    expert_input,
323                    self.gate_ptrs.packed_ptrs,
324                    self.gate_ptrs.scale_ptrs,
325                    self.gate_ptrs.scale2_vals,
326                    expert_gate_out,
327                    self.up_ptrs.packed_ptrs,
328                    self.up_ptrs.scale_ptrs,
329                    self.up_ptrs.scale2_vals,
330                    expert_up_out,
331                    indices_dev,
332                    &self.weights.shared_expert.gate_proj,
333                    shared_gate_scratch,
334                    &self.weights.shared_expert.up_proj,
335                    shared_up_scratch,
336                    inter,
337                    h,
338                    top_k,
339                    stream,
340                )
341            })?;
342
343            if single_seq_decode {
344                self.apply_expert_lora_decode_gateup(
345                    expert_gate_out,
346                    expert_up_out,
347                    expert_input,
348                    indices_dev,
349                    top_k,
350                    top_k,
351                    DevicePtr::NULL,
352                    ctx,
353                    stream,
354                )?;
355            }
356
357            if tracing::enabled!(tracing::Level::DEBUG) && !ctx.graph_capture {
358                debug_dumps::debug_expert_intermediates(
359                    ctx,
360                    expert_gate_out,
361                    expert_up_out,
362                    shared_gate_scratch,
363                    shared_up_scratch,
364                    stream,
365                )?;
366            }
367
368            prof!("exp_silu_down", {
369                ops::moe_expert_silu_down_shared(
370                    ctx.gpu,
371                    self.moe_expert_silu_down_shared,
372                    expert_gate_out,
373                    expert_up_out,
374                    self.down_ptrs.packed_ptrs,
375                    self.down_ptrs.scale_ptrs,
376                    self.down_ptrs.scale2_vals,
377                    expert_down_out,
378                    indices_dev,
379                    shared_gate_scratch,
380                    shared_up_scratch,
381                    &self.weights.shared_expert.down_proj,
382                    shared_out,
383                    h,
384                    inter,
385                    top_k,
386                    stream,
387                )
388            })?;
389        }
390
391        // 2026-09-25: Fold the expert down_proj LoRA delta into `expert_down_out`
392        // (`[top_k, hidden]`) in place, recomputing `act(gate) * up` from
393        // `expert_gate_out`/`expert_up_out`. It runs before the blend, so the
394        // routing weight scales base plus delta, and before the EP memset below
395        // reuses `expert_gate_out`. A null row map leaves the decision to
396        // `moe_route_gate`. A no-op without an expert adapter.
397        if single_seq_decode {
398            self.apply_expert_lora_decode_down(
399                expert_gate_out,
400                expert_up_out,
401                expert_down_out,
402                indices_dev,
403                top_k,
404                top_k,
405                DevicePtr::NULL,
406                ctx,
407                stream,
408            )?;
409        }
410
411        if self.has_mixed_bf16_shared_expert() {
412            self.run_bf16_shared_expert(
413                input,
414                1,
415                h,
416                shared_inter,
417                shared_gate_scratch,
418                shared_up_scratch,
419                shared_out,
420                ctx,
421                stream,
422            )?;
423        }
424
425        if tracing::enabled!(tracing::Level::DEBUG) && !ctx.graph_capture {
426            ctx.gpu.synchronize(stream)?;
427            let mut down_buf = vec![0u8; 16];
428            ctx.gpu.copy_d2h(expert_down_out, &mut down_buf)?;
429            let down_vals: Vec<f32> = (0..8)
430                .map(|i| {
431                    let bits = u16::from_le_bytes([down_buf[i * 2], down_buf[i * 2 + 1]]);
432                    f32::from_bits((bits as u32) << 16)
433                })
434                .collect();
435            tracing::info!("  MoE down_out[slot0,0..8]: {:?}", down_vals);
436            let mut sh_buf = vec![0u8; 16];
437            ctx.gpu.copy_d2h(shared_out, &mut sh_buf)?;
438            let sh_vals: Vec<f32> = (0..8)
439                .map(|i| {
440                    let bits = u16::from_le_bytes([sh_buf[i * 2], sh_buf[i * 2 + 1]]);
441                    f32::from_bits((bits as u32) << 16)
442                })
443                .collect();
444            tracing::info!("  MoE shared_out[0..8]: {:?}", sh_vals);
445        }
446
447        // 2026-09-25: `moe_weighted_sum_blend` writes `output = Σ w·expert_out +
448        // sigmoid(dot(input, gate_w)) · shared_out` (sigmoid taken as 1 with no
449        // gate weight). Every rank computes the same shared expert, so under EP
450        // a zeroed buffer stands in for `shared_out` and the shared expert is
451        // added once after the all-reduce; inside the blend it would be summed
452        // `ep_world_size` times.
453        let output = ctx.buffers.moe_output();
454        let is_ep = ctx.comm.is_some() && ctx.config.ep_world_size > 1;
455        let shared_for_blend = if is_ep && !shared_out.is_null() {
456            let zero_buf = ctx.buffers.expert_gate_out();
457            ctx.gpu.memset_async(zero_buf, 0, h as usize * 2, stream)?;
458            zero_buf
459        } else {
460            shared_out
461        };
462        prof!("wsum_blend", {
463            ops::moe_weighted_sum_blend(
464                ctx.gpu,
465                self.moe_weighted_sum_blend,
466                output,
467                expert_down_out,
468                weights_dev,
469                shared_for_blend,
470                input,
471                self.weights.shared_expert_gate.weight,
472                h,
473                top_k,
474                h,
475                stream,
476            )
477        })?;
478
479        // 2026-09-25: Each rank's partial holds only its local experts, so the
480        // sum over ranks is the full routed output.
481        self.ep_reduce_shared(output, shared_out, input, h, ctx, stream)?;
482
483        if tracing::enabled!(tracing::Level::DEBUG) && !ctx.graph_capture {
484            ctx.gpu.synchronize(stream)?;
485            let mut buf = vec![0u8; 8];
486            ctx.gpu.copy_d2h(output, &mut buf)?;
487            let vals: Vec<f32> = (0..4)
488                .map(|i| {
489                    let lo = buf[i * 2];
490                    let hi = buf[i * 2 + 1];
491                    f32::from_bits(((lo as u32) | ((hi as u32) << 8)) << 16)
492                })
493                .collect();
494            tracing::info!("  MoE output: {:?}", vals);
495        }
496
497        Ok(output)
498    }
499}