spark_model/layers/moe_v41.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2// provenance-id: 526f6e616c6420522e205374657369616b
3
4//! DeepSeek-V4.1 Flash MoE on streamed, still-quantized experts.
5//!
6//! The routed experts never leave their Q2_K / Q3_K blocks: per token the
7//! router picks `topk` of 384, the [`ExpertLru`](spark_runtime::weights::expert_stream::ExpertLru) gathers those experts' raw
8//! slices into its device-visible slots (misses read from the SSD), and each
9//! expert runs as three K-quant GEMVs on the raw blocks (`kquant_mmvq_q2_k_w` for
10//! gate and up, `kquant_mmvq_q3_k_w` for down) with the activation quantised to
11//! q8_1 in between. The shared expert every token goes through is resident bf16
12//! and runs on the dense GEMM.
13//!
14//! Routing follows the CPU reference (`deepseek_v41_ref::moe::gate`) exactly:
15//! the logits are an f32-accumulated GEMM of the bf16 input against the bf16
16//! gate weight, then `sqrt(softplus(logit / temp))`, and the top-k is chosen by
17//! `score + correction_bias` while the weights are the unbiased scores,
18//! renormalised and scaled by `route_scale`. The selection runs on the CPU from
19//! the downloaded logits (`[tokens, 384]` f32), which keeps the tie-breaking the
20//! reference's and costs nothing at decode.
21//!
22//! Oracle: `moe_v41_tests.rs`, synthetic Q2_K / Q3_K experts on a pinned arena
23//! against the CPU decoders + q8_1 emulation + the reference's expert math.
24
25use spark_runtime::gpu::{DevicePtr, KernelHandle};
26
27use crate::layers::ops::ResidentMat;
28
29mod device_route;
30mod forward;
31
32/// Layers the device slot table covers (V4.1 has 40; the loader checks).
33pub(crate) const SLOT_TABLE_LAYERS: usize = 64;
34mod init;
35mod route;
36mod shared;
37mod single;
38
39const MODULE: &str = "moe_v41";
40const GEMM_MODULE: &str = "gemm";
41
42#[derive(Clone, Debug)]
43pub struct MoeV41Cfg {
44 pub dim: usize,
45 pub inter: usize,
46 pub n_routed: usize,
47 pub topk: usize,
48 pub gate_temp: f32,
49 pub norm_topk_prob: bool,
50 pub route_scale: f32,
51 pub swiglu_limit: f32,
52 pub max_tokens: usize,
53}
54
55/// One layer's resident MoE weights: the router and the shared expert.
56pub struct MoeV41LayerWeights {
57 pub layer: u32,
58 /// bf16 `[n_routed, dim]`
59 pub gate_w: DevicePtr,
60 /// f32 `[n_routed]`, host: the selection runs on the CPU
61 pub gate_bias: Vec<f32>,
62 /// The same bias on the device, for the device-side selection.
63 pub gate_bias_dev: DevicePtr,
64 /// `[inter, dim]`, `[dim, inter]`, `[inter, dim]`: bf16 or the GGUF's
65 /// Q2_K (w1, w3) / Q3_K (w2) blocks on the routed experts' kernels
66 pub shared_w1: ResidentMat,
67 pub shared_w2: ResidentMat,
68 pub shared_w3: ResidentMat,
69}
70
71/// The router of one layer alone: what predicting a layer's selection from
72/// an earlier layer's input needs (see `MoeV41::predict_launch`).
73pub struct RouterWeights {
74 pub layer: u32,
75 /// bf16 `[n_routed, dim]`
76 pub gate_w: DevicePtr,
77 pub gate_bias: Vec<f32>,
78}
79
80/// `ATLAS_DS41_PREFETCH_K` (default 0 = off), read once: how many of the
81/// next layer's predicted experts to start reading while this layer computes
82/// (needs `ATLAS_DS41_READER_POOL=1`). Off by default: on the 09-19 standard
83/// the predictor's router launch (2.7 ms a token) and the wasted reads on
84/// the shared disk cost more than the caught misses saved (K=4: 12.50/16.67,
85/// K=6: 13.63/16.39, against 14.20/17.73 without).
86pub fn prefetch_k() -> usize {
87 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
88 *K.get_or_init(|| {
89 std::env::var("ATLAS_DS41_PREFETCH_K")
90 .ok()
91 .and_then(|v| v.parse().ok())
92 .unwrap_or(0)
93 })
94}
95
96struct Kernels {
97 gemm: KernelHandle,
98 /// bf16 shared expert at m = 1 (the tiled GEMM idles 15 of 16 rows)
99 gemv: KernelHandle,
100 gemm_f32out: KernelHandle,
101 /// router logits at m <= 8: strict-order GEMV, bit-identical to gemm_f32out
102 router_gemv: KernelHandle,
103 /// the same logits from the products staged in shared memory, N blocks (decode)
104 router_gemv_staged: KernelHandle,
105 q8_rows: KernelHandle,
106 mmvq_q2k: KernelHandle,
107 mmvq_q3k: KernelHandle,
108 /// the single-token arm: every routed expert in one launch a projection
109 mmvq_q2k_experts: KernelHandle,
110 mmvq_q3k_experts: KernelHandle,
111 /// rows (warps) a block of the expert batch, 2 / 4 / 8 (ATLAS_DS41_EXPERT_WARPS)
112 experts_warps: u32,
113 swiglu: KernelHandle,
114 /// the swiglu and the q8_1 rows of its output in one launch (decode)
115 swiglu_q8: KernelHandle,
116 accumulate: KernelHandle,
117 finish: KernelHandle,
118 gather: KernelHandle,
119 scatter_add: KernelHandle,
120 sum_rows: KernelHandle,
121 quant_d2s6: KernelHandle,
122 quant_d4: KernelHandle,
123 mmq_q2k_nc: KernelHandle,
124 mmq_q2k_wc: KernelHandle,
125 mmq_q3k_nc: KernelHandle,
126 mmq_q3k_wc: KernelHandle,
127 route_select: KernelHandle,
128 slot_table_set: KernelHandle,
129}
130
131/// Where one call's time went (wall clock, host side).
132#[derive(Clone, Copy, Debug, Default)]
133pub struct MoeV41Timing {
134 pub route_ms: f64,
135 pub fetch_ms: f64,
136 pub compute_ms: f64,
137 pub hits: u64,
138 pub misses: u64,
139 pub bytes_read: u64,
140}
141
142impl MoeV41Timing {
143 pub fn add(&mut self, o: &MoeV41Timing) {
144 self.route_ms += o.route_ms;
145 self.fetch_ms += o.fetch_ms;
146 self.compute_ms += o.compute_ms;
147 self.hits += o.hits;
148 self.misses += o.misses;
149 self.bytes_read += o.bytes_read;
150 }
151}
152
153/// What `stage_m1` leaves for `compute_m1`: the distinct expert count and
154/// the routing (for callers and diagnostics), with the host-span timing.
155pub struct MoeV41Stage {
156 pub ne: usize,
157 pub weights: Vec<f32>,
158 pub indices: Vec<usize>,
159 pub timing: MoeV41Timing,
160}
161
162pub struct MoeV41 {
163 /// The last forward's timing.
164 pub last: std::cell::Cell<MoeV41Timing>,
165 /// sync at the end of the forward so the step timer reads GPU time (diag only)
166 timing_sync: bool,
167 pub cfg: MoeV41Cfg,
168 k: Kernels,
169 logits: DevicePtr,
170 /// the next layer's router on this layer's input (`[n_routed]` f32)
171 pred_logits: DevicePtr,
172 /// gathered rows of one expert group, `[m, dim]` bf16
173 a_rows: DevicePtr,
174 /// the group's q8_1 activations (plain rows or the MMQ layout)
175 a_q8: DevicePtr,
176 gate_out: DevicePtr,
177 up_out: DevicePtr,
178 h: DevicePtr,
179 h_q8: DevicePtr,
180 down_out: DevicePtr,
181 /// `[m * topk]` i32 token rows and f32 routing weights, group-major
182 rows_dev: DevicePtr,
183 weight_dev: DevicePtr,
184 /// The device selection's header (miss flag, picks, weight bits, plan
185 /// slots) and the `[64 layers][n_routed]` slot table (-1 = not resident).
186 route_hdr: DevicePtr,
187 slot_table: DevicePtr,
188 /// gate / up / down block pointers of the token's experts, `3 * topk`
189 ptrs_dev: DevicePtr,
190 sg: DevicePtr,
191 su: DevicePtr,
192 sh: DevicePtr,
193 sd: DevicePtr,
194 acc: DevicePtr,
195 out: DevicePtr,
196 /// The shared expert's own q8_1 scratch for its SwiGLU output (`[1,
197 /// inter]`) on the side stream, so it never shares `h_q8` with the routed
198 /// experts running at the same time; the token's input rows (`a_q8`) are
199 /// written once on the forking stream and read by both.
200 sh_q8: DevicePtr,
201 /// The side stream the single-token shared expert runs on, and the two
202 /// events that fence it: `ev_in` (the input is ready, recorded on the
203 /// main stream) and `ev_out` (`sd` is ready, recorded on the side).
204 side: u64,
205 ev_in: u64,
206 ev_out: u64,
207}
208
209/// `ATLAS_DS41_SHARED_SIDE` (default on; `0` = off), read once: at decode the
210/// shared expert runs on a side stream while the main stream does the router
211/// and the host picks the experts (the GPU is otherwise idle for that span);
212/// its output is added into `acc` after the routed experts, as before, so the
213/// accumulation order and the bits are unchanged.
214pub fn shared_side() -> bool {
215 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
216 *V.get_or_init(|| !std::env::var("ATLAS_DS41_SHARED_SIDE").is_ok_and(|v| v == "0"))
217}
218
219fn softplus(x: f32) -> f32 {
220 if x > 20.0 { x } else { x.exp().ln_1p() }
221}
222
223/// The reference's `Gate.forward` on f32 logits: returns
224/// (`weights[tokens, topk]`, `indices[tokens, topk]`) in torch's top-k order.
225pub fn route_from_logits(
226 logits: &[f32],
227 tokens: usize,
228 bias: &[f32],
229 c: &MoeV41Cfg,
230) -> (Vec<f32>, Vec<usize>) {
231 let n = c.n_routed;
232 let mut weights = Vec::with_capacity(tokens * c.topk);
233 let mut indices = Vec::with_capacity(tokens * c.topk);
234 for t in 0..tokens {
235 let scores: Vec<f32> = (0..n)
236 .map(|e| softplus(logits[t * n + e] / c.gate_temp).sqrt())
237 .collect();
238 let mut order: Vec<usize> = (0..n).collect();
239 order.sort_by(|&a, &b| {
240 (scores[b] + bias[b])
241 .partial_cmp(&(scores[a] + bias[a]))
242 .expect("finite scores")
243 });
244 let picked = &order[..c.topk];
245 let mut wt: Vec<f32> = picked.iter().map(|&e| scores[e]).collect();
246 if c.norm_topk_prob && c.topk > 1 {
247 let sum: f32 = wt.iter().sum::<f32>() + 1e-20;
248 for v in &mut wt {
249 *v /= sum;
250 }
251 }
252 for v in &mut wt {
253 *v *= c.route_scale;
254 }
255 weights.extend(wt);
256 indices.extend_from_slice(picked);
257 }
258 (weights, indices)
259}
260
261/// A `[tokens, dim]` bf16 device buffer's bytes, for callers staging inputs.
262pub fn bf16_bytes(v: &[f32]) -> Vec<u8> {
263 v.iter()
264 .flat_map(|&x| {
265 let b = x.to_bits();
266 let lsb = (b >> 16) & 1;
267 ((b.wrapping_add(0x7FFF + lsb) >> 16) as u16).to_le_bytes()
268 })
269 .collect()
270}
271
272#[cfg(test)]
273#[path = "moe_v41_route_tests.rs"]
274mod route_tests;
275#[cfg(test)]
276#[path = "moe_v41_tests.rs"]
277mod tests;