metrale_model_arch/glm5next_mlp/
forward.rs1use anyhow::{Result, bail};
14use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
15use metrale_gpu_runtime::kernel_args::KernelLaunch;
16
17use super::forward_prefill_gemm;
18use super::weights::Glm5NextMoeWeights;
19use super::{Glm5NextMlpConfig, Glm5NextMlpKernels};
20
21mod dense;
22mod launch;
23mod moe_experts;
24mod workspace;
25
26pub use dense::forward_dense;
27use launch::{gemm, swiglu};
28pub use workspace::{mlp_ws_bytes, mlp_ws_total_bytes};
29
30const ACT_BLOCK: u32 = 256;
31
32pub(super) fn swiglu_rows(
35 gpu: &dyn GpuBackend,
36 k: KernelHandle,
37 gate: DevicePtr,
38 up: DevicePtr,
39 out: DevicePtr,
40 n: usize,
41 limit: f32,
42 stream: u64,
43) -> Result<()> {
44 swiglu(gpu, k, gate, up, out, n, limit, stream)
45}
46
47pub struct Glm5NextMlpWorkspace {
50 a_gate: DevicePtr,
53 a_up: DevicePtr,
54 a_act: DevicePtr,
55 logits: DevicePtr,
57 ids: DevicePtr,
59 wts: DevicePtr,
60 expert_out: DevicePtr,
63 shared_out: DevicePtr,
65 u_eid: DevicePtr,
68 u_slot: DevicePtr,
71 sorted_token_ids: DevicePtr,
73 sorted_expert_ids: DevicePtr,
75 expert_offsets: DevicePtr,
77 token_to_perm: DevicePtr,
80 max_inter: usize,
81 max_rows: usize,
83 max_total_expanded: usize,
85}
86
87pub fn mlp_ws_shared() -> bool {
90 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
91 *S.get_or_init(|| {
92 let shared = std::env::var("METRALE_GLM_MLP_WS_SHARED").as_deref() != Ok("0");
93 if !shared {
94 tracing::warn!(
95 "GLM MLP workspace: PER-LAYER (METRALE_GLM_MLP_WS_SHARED=0) — the pre-P2 \
96 allocation, one scratch per layer"
97 );
98 }
99 shared
100 })
101}
102
103impl Glm5NextMlpWorkspace {
104 pub(super) fn max_rows(&self) -> usize {
106 self.max_rows
107 }
108 pub(super) fn max_total_expanded(&self) -> usize {
110 self.max_total_expanded
111 }
112 pub(super) fn ids(&self) -> DevicePtr {
113 self.ids
114 }
115 pub(super) fn a_gate(&self) -> DevicePtr {
116 self.a_gate
117 }
118 pub(super) fn a_up(&self) -> DevicePtr {
119 self.a_up
120 }
121 pub(super) fn a_act(&self) -> DevicePtr {
122 self.a_act
123 }
124 pub(super) fn expert_out(&self) -> DevicePtr {
125 self.expert_out
126 }
127 pub(super) fn sorted_token_ids(&self) -> DevicePtr {
128 self.sorted_token_ids
129 }
130 pub(super) fn sorted_expert_ids(&self) -> DevicePtr {
131 self.sorted_expert_ids
132 }
133 pub(super) fn expert_offsets(&self) -> DevicePtr {
134 self.expert_offsets
135 }
136 pub(super) fn token_to_perm(&self) -> DevicePtr {
137 self.token_to_perm
138 }
139}
140
141fn row_batch_disabled() -> bool {
144 static F: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
145 *F.get_or_init(|| std::env::var("METRALE_NO_GLM_MOE_ROW_BATCH").as_deref() == Ok("1"))
146}
147
148pub fn row_batch_max() -> usize {
151 static M: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
152 *M.get_or_init(|| {
153 let m = std::env::var("METRALE_GLM_MOE_ROW_BATCH_MAX")
154 .ok()
155 .and_then(|v| v.parse::<usize>().ok())
156 .unwrap_or(MOE_ROW_BATCH_MAX_ROWS)
157 .clamp(1, MOE_ROW_BATCH_MAX_ROWS);
158 if m != MOE_ROW_BATCH_MAX_ROWS {
159 tracing::warn!(
160 "GLM MoE row-batch width capped at {m} (default {MOE_ROW_BATCH_MAX_ROWS})"
161 );
162 }
163 m
164 })
165}
166
167pub const MOE_ROW_BATCH_MAX_ROWS: usize = 8;
172
173fn moe_row_groups(rows: usize, cap: usize) -> Vec<(usize, usize)> {
182 let n = rows.div_ceil(cap.max(1)).max(1);
183 let mut out = Vec::with_capacity(n);
184 let mut start = 0usize;
185 for i in 0..n {
186 let w = (rows - start).div_ceil(n - i);
187 out.push((start, w));
188 start += w;
189 }
190 out
191}
192
193pub const MOE_ROW_UNION_MAX_IDS: usize = 64;
196
197fn announce_row_batch(batched: bool, rows: usize) {
200 use std::sync::atomic::{AtomicU16, Ordering};
201 static SEEN: AtomicU16 = AtomicU16::new(0);
202 let bit = 1u16 << rows.min(15);
203 if SEEN.fetch_or(bit, Ordering::Relaxed) & bit != 0 {
204 return;
205 }
206 if batched {
207 tracing::info!("GLM MoE: row-batched expert union ({rows} rows, one sweep each)");
208 } else {
209 tracing::info!("GLM MoE: per-row expert sweeps ({rows} rows)");
210 }
211}
212
213fn announce_grouped_prefill(on: bool, rows: usize) {
216 use std::sync::atomic::{AtomicU32, Ordering};
217 static SEEN: AtomicU32 = AtomicU32::new(0);
218 if rows <= MOE_ROW_BATCH_MAX_ROWS {
219 return;
220 }
221 let bit = 1u32 << (rows.min(31));
222 if SEEN.fetch_or(bit, Ordering::Relaxed) & bit != 0 {
223 return;
224 }
225 if on {
226 let t = super::forward_prefill_gemm::gemm_tile();
227 tracing::info!(
228 "GLM MoE prefill: grouped tensor-core W4A16 GEMM, {rows} rows in ONE launch \
229 per projection, tile `{}` (M_TILE {}, N_TILE {}, {} threads) \
230 (METRALE_GLM_MOE_PREFILL_GEMM=0 to restore the GEMV path; \
231 METRALE_GLM_MOE_GEMM_TILE=base for the prior tile)",
232 t.name,
233 t.m_tile,
234 t.n_tile,
235 t.threads
236 );
237 } else {
238 tracing::info!(
239 "GLM MoE prefill: row-batched GEMV, {rows} rows split into \
240 {MOE_ROW_BATCH_MAX_ROWS}-row sweeps"
241 );
242 }
243}
244
245fn host_dispatch_forced() -> bool {
249 static F: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
250 *F.get_or_init(|| std::env::var("METRALE_GLM_MOE_HOST_DISPATCH").as_deref() == Ok("1"))
251}
252
253fn announce_dispatch(grouped: bool) {
256 static ONCE: std::sync::Once = std::sync::Once::new();
257 ONCE.call_once(|| {
258 if grouped {
259 tracing::info!("GLM MoE: grouped device dispatch (no per-layer D2H)");
260 } else {
261 tracing::info!("GLM MoE: host dispatch (per-layer stream sync + D2H)");
262 }
263 });
264}
265
266#[allow(clippy::too_many_arguments)]
279pub fn forward_moe(
280 gpu: &dyn GpuBackend,
281 k: &Glm5NextMlpKernels,
282 cfg: &Glm5NextMlpConfig,
283 w: &Glm5NextMoeWeights,
284 x: DevicePtr,
285 out: DevicePtr,
286 rows: usize,
287 ws: &Glm5NextMlpWorkspace,
288 stream: u64,
289) -> Result<()> {
290 if rows == 0 || rows > ws.max_rows {
291 bail!(
292 "GLM MoE: {rows} rows do not fit a workspace built for {}",
293 ws.max_rows
294 );
295 }
296 if w.experts.len() != cfg.local_experts {
297 bail!(
298 "GLM MoE: {} bound experts but this rank owns {} of {}",
299 w.experts.len(),
300 cfg.local_experts,
301 cfg.num_experts
302 );
303 }
304
305 use crate::glm5next_layer::profile;
306
307 let groups = moe_row_groups(rows, row_batch_max());
308 let grouped_prefill = rows > MOE_ROW_BATCH_MAX_ROWS
309 && rows >= forward_prefill_gemm::prefill_gemm_min_rows()
310 && forward_prefill_gemm::prefill_gemm_enabled()
311 && !host_dispatch_forced()
312 && !profile::trace_on()
313 && k.moe_sort_by_expert.0 != 0
314 && k.moe_grouped_gemm.0 != 0
315 && k.combine_indexed.0 != 0
316 && rows * cfg.top_k <= ws.max_total_expanded();
317 let batched = !grouped_prefill
318 && rows >= 2
319 && !host_dispatch_forced()
320 && !row_batch_disabled()
321 && !profile::trace_on()
322 && k.moe_row_union.0 != 0
323 && groups.iter().all(|&(_, w)| {
324 w >= 2
325 && w * cfg.top_k <= MOE_ROW_UNION_MAX_IDS
326 && k.w4a16_gemv_sw_moe_batchm[w - 2].0 != 0
327 });
328 announce_row_batch(batched, rows);
329 announce_grouped_prefill(grouped_prefill, rows);
330
331 let t = profile::start();
332 if rows > metrale_model_layers::layers::ops::DENSE_GEMV_BATCHM_MAX_M as usize
333 && crate::glm5next_layer::cublas_wide_proj()
334 {
335 metrale_model_layers::layers::ops::cublas_bf16_proj_dense_f32_out(
336 x,
337 w.router,
338 ws.logits,
339 rows as u32,
340 cfg.num_experts as u32,
341 cfg.hidden as u32,
342 stream,
343 )?;
344 } else {
345 for r in 0..rows {
346 gemm(
347 gpu,
348 k.gemm_f32,
349 k.gemv_f32,
350 KernelHandle(0),
351 x.offset(r * cfg.hidden * 2),
352 w.router,
353 ws.logits.offset(r * cfg.num_experts * 4),
354 1,
355 cfg.num_experts,
356 cfg.hidden,
357 stream,
358 )?;
359 }
360 }
361 KernelLaunch::new(gpu, k.router)
363 .grid([rows as u32, 1, 1])
364 .block([ACT_BLOCK, 1, 1])
365 .arg_ptr(ws.logits)
366 .arg_ptr(w.router_bias)
367 .arg_ptr(ws.ids)
368 .arg_ptr(ws.wts)
369 .arg_u32(cfg.num_experts as u32)
370 .arg_u32(cfg.top_k as u32)
371 .arg_u32(1)
372 .arg_f32(cfg.routed_scale)
373 .arg_u32(u32::from(cfg.renormalize))
374 .arg_u32(u32::from(cfg.router_bf16_ladder))
375 .launch(stream)?;
376 profile::end(profile::MOE_ROUTER, t, gpu, stream);
377
378 gpu.memset_async(ws.expert_out, 0, rows * cfg.top_k * cfg.hidden * 2, stream)?;
381
382 let site = moe_experts::MoeSite {
383 gpu,
384 k,
385 cfg,
386 w,
387 x,
388 rows,
389 ws,
390 stream,
391 };
392 moe_experts::per_row_experts(&site, batched, grouped_prefill)?;
393
394 if grouped_prefill {
395 let t = profile::start();
398 forward_prefill_gemm::forward_moe_grouped_prefill(gpu, k, cfg, w, x, rows, ws, stream)?;
399 profile::end(profile::MOE_EXPERTS, t, gpu, stream);
400 }
401
402 if batched {
403 moe_experts::row_batched_experts(&site, &groups)?;
404 }
405
406 let t = profile::start();
407 forward_dense(
408 gpu,
409 k,
410 cfg,
411 &w.shared,
412 cfg.local_shared_intermediate,
413 x,
414 ws.shared_out,
415 rows,
416 ws,
417 stream,
418 )?;
419
420 profile::end(profile::MOE_SHARED, t, gpu, stream);
423 let t = profile::start();
424 if grouped_prefill {
428 KernelLaunch::new(gpu, k.combine_indexed)
429 .grid([rows as u32, 1, 1])
430 .block([ACT_BLOCK, 1, 1])
431 .arg_ptr(ws.expert_out)
432 .arg_ptr(ws.token_to_perm)
433 .arg_ptr(ws.wts)
434 .arg_ptr(ws.shared_out)
435 .arg_ptr(out)
436 .arg_u32(cfg.hidden as u32)
437 .arg_u32(cfg.top_k as u32)
438 .launch(stream)?;
439 } else {
440 KernelLaunch::new(gpu, k.combine)
441 .grid([rows as u32, 1, 1])
442 .block([ACT_BLOCK, 1, 1])
443 .arg_ptr(ws.expert_out)
444 .arg_ptr(ws.wts)
445 .arg_ptr(ws.shared_out)
446 .arg_ptr(out)
447 .arg_u32(cfg.hidden as u32)
448 .arg_u32(cfg.top_k as u32)
449 .launch(stream)?;
450 }
451 profile::end(profile::MOE_COMBINE, t, gpu, stream);
452 Ok(())
453}
454
455#[cfg(test)]
456mod tests;