metrale_model_arch/
moe_v41.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// provenance-id: 526f6e616c6420522e205374657369616b
3
4//! 2026-09-25: DeepSeek-V4.1 Flash MoE on streamed, still-quantized experts.
5//!
6//! The routed experts stay in their GGUF blocks: gate and up run on Q2_K
7//! kernels, down on Q3_K kernels. Per token the router picks `topk` of
8//! `n_routed` experts, the [`ExpertLru`](metrale_model_weights::weights::expert_stream::ExpertLru) gathers those experts' raw
9//! slices into its device-visible slots (misses are read from the
10//! `ExpertSource`), and the kernels read the blocks directly with the
11//! activation quantised to q8_1: GEMVs for groups of up to eight rows, and by
12//! default MMQ above. The shared expert is resident, bf16 or K-quant (`ResidentMat`).
13//!
14//! Routing matches `deepseek_v41_ref::moe::gate`: f32-accumulated router
15//! logits, scores `sqrt(softplus(logit / gate_temp))`, the top-k chosen by
16//! `score + gate_bias` with a stable sort, and the weights the unbiased scores,
17//! renormalised when `norm_topk_prob` and scaled by `route_scale`. By default
18//! the selection runs on the CPU from the downloaded logits; `device_route`
19//! holds the device version.
20//!
21//! Tested in `moe_v41_tests.rs` on synthetic Q2_K / Q3_K experts against the
22//! reference `gate` and a CPU emulation of the numerics.
23//!
24//! Owner: model-arch (DeepSeek-V4.1 MoE).
25//! Invariants: none beyond the types.
26
27use metrale_gpu_runtime::gpu::{DevicePtr, KernelHandle};
28
29use metrale_model_layers::layers::ops::ResidentMat;
30
31mod device_route;
32mod forward;
33
34/// 2026-09-25: Layers the device slot table and the route headers cover;
35/// `device_route_ok` refuses the device selection for a layer at or past it.
36pub(crate) const SLOT_TABLE_LAYERS: usize = 64;
37mod init;
38mod route;
39mod shared;
40mod single;
41
42const MODULE: &str = "moe_v41";
43const GEMM_MODULE: &str = "gemm";
44
45#[derive(Clone, Debug)]
46pub struct MoeV41Cfg {
47    pub dim: usize,
48    pub inter: usize,
49    pub n_routed: usize,
50    pub topk: usize,
51    pub gate_temp: f32,
52    pub norm_topk_prob: bool,
53    pub route_scale: f32,
54    pub swiglu_limit: f32,
55    pub max_tokens: usize,
56}
57
58/// 2026-09-25: One layer's resident MoE weights: the router and the shared expert.
59pub struct MoeV41LayerWeights {
60    pub layer: u32,
61    /// 2026-09-25: bf16 `[n_routed, dim]`.
62    pub gate_w: DevicePtr,
63    /// 2026-09-25: f32 `[n_routed]` on the host, for the CPU selection.
64    pub gate_bias: Vec<f32>,
65    /// 2026-09-25: The same bias on the device, for the device-side selection.
66    pub gate_bias_dev: DevicePtr,
67    /// 2026-09-25: w1, w2, w3 as `[inter, dim]`, `[dim, inter]`, `[inter, dim]`:
68    /// bf16, or the GGUF's K-quant blocks run on the routed experts' kernels.
69    pub shared_w1: ResidentMat,
70    pub shared_w2: ResidentMat,
71    pub shared_w3: ResidentMat,
72}
73
74/// 2026-09-25: The router of one layer alone: what predicting a layer's
75/// selection from an earlier layer's input needs (see `MoeV41::predict_launch`).
76pub struct RouterWeights {
77    pub layer: u32,
78    /// 2026-09-25: bf16 `[n_routed, dim]`.
79    pub gate_w: DevicePtr,
80    pub gate_bias: Vec<f32>,
81}
82
83/// 2026-09-25: `METRALE_DS41_PREFETCH_K`, read once (default 0, off): how
84/// many of the next layer's predicted experts to start reading while this
85/// layer computes. It acts only at one token, with the host selection, and
86/// with the reader pool (`METRALE_DS41_READER_POOL=1`).
87pub fn prefetch_k() -> usize {
88    static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
89    *K.get_or_init(|| {
90        std::env::var("METRALE_DS41_PREFETCH_K")
91            .ok()
92            .and_then(|v| v.parse().ok())
93            .unwrap_or(0)
94    })
95}
96
97struct Kernels {
98    gemm: KernelHandle,
99    /// 2026-09-25: The bf16 shared expert at one token.
100    gemv: KernelHandle,
101    gemm_f32out: KernelHandle,
102    /// 2026-09-25: Router logits at up to eight tokens with
103    /// `METRALE_DS41_ROUTER_STAGED=0`, and for the prediction.
104    router_gemv: KernelHandle,
105    /// 2026-09-25: Router logits at up to eight tokens by default: one block
106    /// per expert row, the products staged in shared memory.
107    router_gemv_staged: KernelHandle,
108    q8_rows: KernelHandle,
109    mmvq_q2k: KernelHandle,
110    mmvq_q3k: KernelHandle,
111    /// 2026-09-25: The single-token arm: all selected experts in one launch
112    /// per projection.
113    mmvq_q2k_experts: KernelHandle,
114    mmvq_q3k_experts: KernelHandle,
115    /// 2026-09-25: Warps per block of the expert batch: 2, 4 or 8
116    /// (`METRALE_DS41_EXPERT_WARPS`, default 8).
117    experts_warps: u32,
118    swiglu: KernelHandle,
119    /// 2026-09-25: The SwiGLU and the q8_1 rows of its output in one launch
120    /// (up to eight rows).
121    swiglu_q8: KernelHandle,
122    accumulate: KernelHandle,
123    finish: KernelHandle,
124    gather: KernelHandle,
125    scatter_add: KernelHandle,
126    sum_rows: KernelHandle,
127    quant_d2s6: KernelHandle,
128    quant_d4: KernelHandle,
129    mmq_q2k_nc: KernelHandle,
130    mmq_q2k_wc: KernelHandle,
131    mmq_q3k_nc: KernelHandle,
132    mmq_q3k_wc: KernelHandle,
133    route_select: KernelHandle,
134    slot_table_set: KernelHandle,
135}
136
137/// 2026-09-25: Where one call's time went (wall clock, host side).
138#[derive(Clone, Copy, Debug, Default)]
139pub struct MoeV41Timing {
140    pub route_ms: f64,
141    pub fetch_ms: f64,
142    pub compute_ms: f64,
143    pub hits: u64,
144    pub misses: u64,
145    pub bytes_read: u64,
146}
147
148impl MoeV41Timing {
149    pub fn add(&mut self, o: &MoeV41Timing) {
150        self.route_ms += o.route_ms;
151        self.fetch_ms += o.fetch_ms;
152        self.compute_ms += o.compute_ms;
153        self.hits += o.hits;
154        self.misses += o.misses;
155        self.bytes_read += o.bytes_read;
156    }
157}
158
159/// 2026-09-25: What `stage_m1` leaves for `compute_m1`: the distinct expert
160/// count and the routing (for callers and diagnostics), with the host-span
161/// timing.
162pub struct MoeV41Stage {
163    pub ne: usize,
164    pub weights: Vec<f32>,
165    pub indices: Vec<usize>,
166    pub timing: MoeV41Timing,
167}
168
169pub struct MoeV41 {
170    /// 2026-09-25: The last forward's timing.
171    pub last: std::cell::Cell<MoeV41Timing>,
172    /// 2026-09-25: `METRALE_DS41_DIAG=1`: synchronise at the end of `forward`
173    /// so `compute_ms` is GPU time.
174    timing_sync: bool,
175    pub cfg: MoeV41Cfg,
176    k: Kernels,
177    logits: DevicePtr,
178    /// 2026-09-25: The next layer's router on this layer's input
179    /// (`[n_routed]` f32).
180    pred_logits: DevicePtr,
181    /// 2026-09-25: Gathered rows of one expert group, `[m, dim]` bf16; also
182    /// the staging buffer of `slot_table_update`.
183    a_rows: DevicePtr,
184    /// 2026-09-25: The group's q8_1 activations (plain rows or the MMQ layout).
185    a_q8: DevicePtr,
186    gate_out: DevicePtr,
187    up_out: DevicePtr,
188    h: DevicePtr,
189    h_q8: DevicePtr,
190    down_out: DevicePtr,
191    /// 2026-09-25: `[m * topk]` i32 token rows and f32 routing weights,
192    /// grouped by ascending expert id.
193    rows_dev: DevicePtr,
194    weight_dev: DevicePtr,
195    /// 2026-09-25: The device selection's headers (`header_words` i32 words
196    /// per layer: miss flag, picks, weight bits, plan slots) and the
197    /// `[SLOT_TABLE_LAYERS][n_routed]` i32 slot table (-1 = not resident).
198    route_hdr: DevicePtr,
199    slot_table: DevicePtr,
200    /// 2026-09-25: Gate, up and down block pointers of the token's experts,
201    /// `3 * topk` u64.
202    ptrs_dev: DevicePtr,
203    sg: DevicePtr,
204    su: DevicePtr,
205    sh: DevicePtr,
206    sd: DevicePtr,
207    acc: DevicePtr,
208    out: DevicePtr,
209    /// 2026-09-25: The shared expert's own q8_1 scratch for its SwiGLU output
210    /// (`[1, inter]`) on the side stream, so it never shares `h_q8` with the routed
211    /// experts running at the same time; the token's input rows (`a_q8`) are
212    /// written once on the forking stream and read by both.
213    sh_q8: DevicePtr,
214    /// 2026-09-25: The side stream the single-token shared expert runs on, and the two
215    /// events that fence it: `ev_in` (the input is ready, recorded on the
216    /// main stream) and `ev_out` (`sd` is ready, recorded on the side).
217    side: u64,
218    ev_in: u64,
219    ev_out: u64,
220}
221
222/// 2026-09-25: `METRALE_DS41_SHARED_SIDE`, read once (default on; `0` turns it
223/// off): at one token the shared expert runs on a side stream while the main
224/// stream runs the router and the host picks the experts. Its output is added
225/// into `acc` after the routed experts, the same point as on the main stream,
226/// so the accumulation order does not change.
227pub fn shared_side() -> bool {
228    static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
229    *V.get_or_init(|| !std::env::var("METRALE_DS41_SHARED_SIDE").is_ok_and(|v| v == "0"))
230}
231
232fn softplus(x: f32) -> f32 {
233    if x > 20.0 { x } else { x.exp().ln_1p() }
234}
235
236/// 2026-09-25: `deepseek_v41_ref::moe::gate` on f32 logits: returns
237/// (`weights[tokens, topk]`, `indices[tokens, topk]`), each token's picks in
238/// descending `score + bias` order, ties to the lower expert id. Panics on a
239/// NaN key.
240pub fn route_from_logits(
241    logits: &[f32],
242    tokens: usize,
243    bias: &[f32],
244    c: &MoeV41Cfg,
245) -> (Vec<f32>, Vec<usize>) {
246    let n = c.n_routed;
247    let mut weights = Vec::with_capacity(tokens * c.topk);
248    let mut indices = Vec::with_capacity(tokens * c.topk);
249    for t in 0..tokens {
250        let scores: Vec<f32> = (0..n)
251            .map(|e| softplus(logits[t * n + e] / c.gate_temp).sqrt())
252            .collect();
253        let mut order: Vec<usize> = (0..n).collect();
254        order.sort_by(|&a, &b| {
255            (scores[b] + bias[b])
256                .partial_cmp(&(scores[a] + bias[a]))
257                .expect("finite scores")
258        });
259        let picked = &order[..c.topk];
260        let mut wt: Vec<f32> = picked.iter().map(|&e| scores[e]).collect();
261        if c.norm_topk_prob && c.topk > 1 {
262            let sum: f32 = wt.iter().sum::<f32>() + 1e-20;
263            for v in &mut wt {
264                *v /= sum;
265            }
266        }
267        for v in &mut wt {
268            *v *= c.route_scale;
269        }
270        weights.extend(wt);
271        indices.extend_from_slice(picked);
272    }
273    (weights, indices)
274}
275
276/// 2026-09-25: `v` rounded to bf16 (nearest, ties to even) as little-endian
277/// bytes, for callers staging inputs.
278pub fn bf16_bytes(v: &[f32]) -> Vec<u8> {
279    v.iter()
280        .flat_map(|&x| {
281            let b = x.to_bits();
282            let lsb = (b >> 16) & 1;
283            ((b.wrapping_add(0x7FFF + lsb) >> 16) as u16).to_le_bytes()
284        })
285        .collect()
286}
287
288#[cfg(test)]
289#[path = "moe_v41_route_tests.rs"]
290mod route_tests;
291#[cfg(test)]
292#[path = "moe_v41_tests.rs"]
293mod tests;