spark_model/layers/moe/forward_ep.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! MoeLayer::forward_ep_dispatch.
4
5use super::*;
6
7impl MoeLayer {
8 /// 4. Computes local experts on local + received tokens
9 /// 5. Sends results back (combine)
10 /// 6. Weighted sum into output
11 ///
12 /// Currently scaffolding only — builds routing table and logs statistics.
13 /// Expert compute and actual dispatch use the existing per-token path.
14 /// The all-reduce fallback is used for the actual output until dispatch
15 /// kernels are implemented.
16 pub fn forward_ep_dispatch(
17 &self,
18 input: DevicePtr,
19 ctx: &ForwardContext,
20 stream: u64,
21 ) -> Result<DevicePtr> {
22 // LongCat zero-experts are wired only on the single-token decode
23 // + prefill paths (v1); this variant would silently mis-route the
24 // 384-wide router. Named refusal, not silent wrongness.
25 anyhow::ensure!(
26 self.router_logits_n as usize == ctx.config.num_experts,
27 "zero-expert MoE routing is not wired on this dispatch variant yet (forward_ep)"
28 );
29
30 use super::super::ep_dispatch::build_ep_routing_table;
31
32 let h = ctx.config.hidden_size as u32;
33 let num_experts = ctx.config.num_experts as u32;
34 let top_k = ctx.config.num_experts_per_tok as u32;
35 let (local_start, local_end) = ctx.config.local_expert_range();
36
37 // Gemma-4 router pre-norm (no-op for other models). EP dispatch is
38 // per-token so uses num_tokens=1.
39 let router_in = self.router_input(input, 1, h, ctx, stream)?;
40 // Step 1: Gate projection (same as forward())
41 let gate_logits = ctx.buffers.gate_logits();
42 if let Some(ref nvfp4) = self.gate_nvfp4 {
43 ops::w4a16_decode_gemv(
44 ctx.gpu,
45 self.w4a16_gemv,
46 self.w4a16_gemv_sw,
47 ctx.levers.gemv_sw,
48 router_in,
49 nvfp4,
50 gate_logits,
51 num_experts,
52 h,
53 stream,
54 )?;
55 } else {
56 ops::dense_gemv(
57 ctx.gpu,
58 self.dense_gemv,
59 router_in,
60 &self.weights.gate,
61 gate_logits,
62 num_experts,
63 h,
64 stream,
65 )?;
66 }
67
68 // Step 2: Top-K routing
69 let scratch = ctx.buffers.scratch();
70 let indices_dev = scratch;
71 let weights_dev = scratch.offset(top_k as usize * 4);
72
73 ops::moe_topk_softmax(
74 ctx.gpu,
75 self.moe_topk,
76 gate_logits,
77 indices_dev,
78 weights_dev,
79 num_experts,
80 top_k,
81 ctx.config.norm_topk_prob,
82 stream,
83 )?;
84
85 // Step 3: Build routing table (requires D2H copy of indices/weights)
86 // This is CPU-side work — acceptable for scaffolding, will move to
87 // GPU-side routing table construction in the optimized path.
88 ctx.gpu.synchronize(stream)?;
89 let k = top_k as usize;
90 let mut idx_buf = vec![0u8; k * 4];
91 let mut wt_buf = vec![0u8; k * 4];
92 ctx.gpu.copy_d2h(indices_dev, &mut idx_buf)?;
93 ctx.gpu.copy_d2h(weights_dev, &mut wt_buf)?;
94
95 let gate_indices: Vec<u32> = (0..k)
96 .map(|i| {
97 u32::from_le_bytes([
98 idx_buf[i * 4],
99 idx_buf[i * 4 + 1],
100 idx_buf[i * 4 + 2],
101 idx_buf[i * 4 + 3],
102 ])
103 })
104 .collect();
105 let gate_weights: Vec<f32> = (0..k)
106 .map(|i| {
107 f32::from_le_bytes([
108 wt_buf[i * 4],
109 wt_buf[i * 4 + 1],
110 wt_buf[i * 4 + 2],
111 wt_buf[i * 4 + 3],
112 ])
113 })
114 .collect();
115
116 let routing =
117 build_ep_routing_table(&gate_indices, &gate_weights, 1, k, local_start, local_end);
118
119 tracing::debug!(
120 "EP dispatch: local={} remote={} (rank {}, experts {}..{})",
121 routing.local_count(),
122 routing.remote_count(),
123 ctx.config.ep_rank,
124 local_start,
125 local_end,
126 );
127
128 // Steps 4-6: For now, fall back to the existing forward() path
129 // which uses all-reduce. The routing table is built but not yet
130 // used for actual dispatch. This will be replaced with:
131 // - comm.group_start()
132 // - comm.send_to() for remote tokens
133 // - comm.recv_from() for incoming tokens
134 // - comm.group_end()
135 // - Local expert compute on local + received tokens
136 // - comm.group_start()
137 // - comm.send_to() results back
138 // - comm.recv_from() results from partner
139 // - comm.group_end()
140 // - Weighted sum into output
141 let _ = routing; // suppress unused warning until dispatch is wired
142 self.forward(input, ctx, stream)
143 }
144}