1#![allow(unused_imports)]
11
12use anyhow::Result;
13use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
14use metrale_gpu_runtime::kernel_args::{KernelLaunch, div_ceil};
15
16use crate::weight_map::{DenseWeight, Fp8DenseWeight, Fp8Weight, QuantizedWeight};
17
18use super::*;
19
20pub struct MoeCutlassHostTables {
24 pub gate_packed: Vec<u64>,
25 pub gate_sfb: Vec<u64>,
26 pub gate_scale2: Vec<f32>,
27 pub up_packed: Vec<u64>,
28 pub up_sfb: Vec<u64>,
29 pub up_scale2: Vec<f32>,
30 pub down: Option<MoeCutlassDownHostTables>,
33}
34
35pub struct MoeCutlassDownHostTables {
37 pub packed: Vec<u64>,
38 pub sfb: Vec<u64>,
39 pub scale2: Vec<f32>,
40}
41
42pub fn read_expert_ptrs_u64(gpu: &dyn GpuBackend, p: DevicePtr, n: usize) -> Result<Vec<u64>> {
44 let mut raw = vec![0u8; n * 8];
45 gpu.copy_d2h(p, &mut raw)?;
46 Ok(raw
47 .chunks_exact(8)
48 .map(|x| u64::from_le_bytes(x.try_into().expect("8")))
49 .collect())
50}
51
52pub fn read_expert_scales_f32(gpu: &dyn GpuBackend, p: DevicePtr, n: usize) -> Result<Vec<f32>> {
54 let mut raw = vec![0u8; n * 4];
55 gpu.copy_d2h(p, &mut raw)?;
56 Ok(raw
57 .chunks_exact(4)
58 .map(|x| f32::from_le_bytes(x.try_into().expect("4")))
59 .collect())
60}
61
62impl MoeCutlassHostTables {
63 #[allow(clippy::too_many_arguments)]
67 pub fn snapshot(
68 gpu: &dyn GpuBackend,
69 num_experts: usize,
70 gate_packed: DevicePtr,
71 gate_sfb: Vec<u64>,
72 gate_scale2: DevicePtr,
73 up_packed: DevicePtr,
74 up_sfb: Vec<u64>,
75 up_scale2: DevicePtr,
76 down: Option<(DevicePtr, Vec<u64>, DevicePtr)>,
77 ) -> Result<Self> {
78 Ok(Self {
79 gate_packed: read_expert_ptrs_u64(gpu, gate_packed, num_experts)?,
80 gate_sfb,
81 gate_scale2: read_expert_scales_f32(gpu, gate_scale2, num_experts)?,
82 up_packed: read_expert_ptrs_u64(gpu, up_packed, num_experts)?,
83 up_sfb,
84 up_scale2: read_expert_scales_f32(gpu, up_scale2, num_experts)?,
85 down: match down {
86 Some((packed, sfb, scale2)) => Some(MoeCutlassDownHostTables {
87 packed: read_expert_ptrs_u64(gpu, packed, num_experts)?,
88 sfb,
89 scale2: read_expert_scales_f32(gpu, scale2, num_experts)?,
90 }),
91 None => None,
92 },
93 })
94 }
95}
96
97#[allow(clippy::too_many_arguments)]
101pub fn moe_sort_by_expert(
102 gpu: &dyn GpuBackend,
103 kernel: KernelHandle,
104 topk_ids: DevicePtr,
105 sorted_token_ids: DevicePtr,
106 sorted_expert_ids: DevicePtr,
107 expert_offsets: DevicePtr,
108 token_to_perm: DevicePtr,
109 total_expanded: u32,
110 num_experts: u32,
111 topk: u32,
112 stream: u64,
113) -> Result<()> {
114 KernelLaunch::new(gpu, kernel)
115 .grid([1, 1, 1])
116 .block([256, 1, 1])
117 .arg_ptr(topk_ids)
118 .arg_ptr(sorted_token_ids)
119 .arg_ptr(sorted_expert_ids)
120 .arg_ptr(expert_offsets)
121 .arg_ptr(token_to_perm)
122 .arg_u32(total_expanded)
123 .arg_u32(num_experts)
124 .arg_u32(topk)
125 .launch(stream)
126}
127
128#[allow(clippy::too_many_arguments)]
131pub fn moe_unpermute_reduce_indexed(
132 gpu: &dyn GpuBackend,
133 kernel: KernelHandle,
134 expert_output: DevicePtr,
135 output: DevicePtr,
136 token_to_perm: DevicePtr,
137 topk_weights: DevicePtr,
138 hidden_size: u32,
139 num_tokens: u32,
140 topk: u32,
141 stream: u64,
142) -> Result<()> {
143 KernelLaunch::new(gpu, kernel)
144 .grid([num_tokens, 1, 1])
145 .block([256, 1, 1])
146 .arg_ptr(expert_output)
147 .arg_ptr(output)
148 .arg_ptr(token_to_perm)
149 .arg_ptr(topk_weights)
150 .arg_u32(hidden_size)
151 .arg_u32(num_tokens)
152 .arg_u32(topk)
153 .launch(stream)
154}
155
156#[allow(clippy::too_many_arguments)]
160pub fn moe_unpermute_blend(
161 gpu: &dyn GpuBackend,
162 kernel: KernelHandle,
163 expert_output: DevicePtr,
164 output: DevicePtr,
165 token_to_perm: DevicePtr,
166 topk_weights: DevicePtr,
167 shared_out: DevicePtr,
168 normed: DevicePtr,
169 gate_weight: DevicePtr,
170 hidden_size: u32,
171 num_tokens: u32,
172 topk: u32,
173 stream: u64,
174) -> Result<()> {
175 anyhow::ensure!(
176 hidden_size.is_multiple_of(8),
177 "moe_unpermute_blend: hidden {hidden_size} is not a multiple of 8"
178 );
179 KernelLaunch::new(gpu, kernel)
180 .grid([num_tokens, 1, 1])
181 .block([256, 1, 1])
182 .arg_ptr(expert_output)
183 .arg_ptr(output)
184 .arg_ptr(token_to_perm)
185 .arg_ptr(topk_weights)
186 .arg_ptr(shared_out)
187 .arg_ptr(normed)
188 .arg_ptr(gate_weight)
189 .arg_u32(hidden_size)
190 .arg_u32(num_tokens)
191 .arg_u32(topk)
192 .launch(stream)
193}
194
195pub fn moe_batched_blend(
198 gpu: &dyn GpuBackend,
199 kernel: KernelHandle,
200 output: DevicePtr,
201 shared_out: DevicePtr,
202 normed: DevicePtr,
203 gate_weight: DevicePtr,
204 hidden_size: u32,
205 num_tokens: u32,
206 stream: u64,
207) -> Result<()> {
208 KernelLaunch::new(gpu, kernel)
209 .grid([num_tokens, 1, 1])
210 .block([256, 1, 1])
211 .arg_ptr(output)
212 .arg_ptr(shared_out)
213 .arg_ptr(normed)
214 .arg_ptr(gate_weight)
215 .arg_u32(hidden_size)
216 .arg_u32(num_tokens)
217 .launch(stream)
218}
219
220#[allow(clippy::too_many_arguments)]
226pub fn moe_grouped_gate_up_cutlass(
227 gpu: &dyn GpuBackend,
228 host: &MoeCutlassHostTables,
229 a: DevicePtr,
230 sorted_token_ids: DevicePtr,
231 c_gate: DevicePtr,
232 c_up: DevicePtr,
233 expert_offsets: DevicePtr,
234 inter: u32,
235 hidden: u32,
236 stream: u64,
237) -> Result<Vec<i32>> {
238 let num_experts = host.gate_packed.len();
239 let mut off_raw = vec![0u8; (num_experts + 1) * 4];
240 gpu.copy_d2h_on_stream(expert_offsets, &mut off_raw, stream)?;
241 gpu.synchronize(stream)?;
242 let eoff: Vec<i32> = off_raw
243 .chunks_exact(4)
244 .map(|c| i32::from_le_bytes([c[0], c[1], c[2], c[3]]))
245 .collect();
246
247 metrale_gpu_runtime::cutlass::nvfp4_grouped_gate_up_fused(
248 a.0,
249 sorted_token_ids.0,
250 &host.gate_packed,
251 &host.gate_sfb,
252 &host.gate_scale2,
253 &host.up_packed,
254 &host.up_sfb,
255 &host.up_scale2,
256 c_gate.0,
257 c_up.0,
258 &eoff,
259 inter,
260 hidden,
261 stream,
262 )?;
263 Ok(eoff)
264}
265
266#[allow(clippy::too_many_arguments)]
270pub fn moe_grouped_down_cutlass(
271 gpu: &dyn GpuBackend,
272 eoff_cached: Option<&[i32]>,
275 host: &MoeCutlassDownHostTables,
276 a: DevicePtr,
277 c: DevicePtr,
278 expert_offsets: DevicePtr,
279 hidden: u32,
280 inter: u32,
281 stream: u64,
282) -> Result<()> {
283 let num_experts = host.packed.len();
284 let eoff: Vec<i32> = if let Some(e) = eoff_cached {
285 e.to_vec()
286 } else {
287 let mut off_raw = vec![0u8; (num_experts + 1) * 4];
288 gpu.copy_d2h_on_stream(expert_offsets, &mut off_raw, stream)?;
289 gpu.synchronize(stream)?;
290 off_raw
291 .chunks_exact(4)
292 .map(|c| i32::from_le_bytes(c.try_into().expect("4")))
293 .collect()
294 };
295 metrale_gpu_runtime::cutlass::nvfp4_grouped_down(
296 a.0,
297 &host.packed,
298 &host.sfb,
299 &host.scale2,
300 c.0,
301 &eoff,
302 hidden,
303 inter,
304 stream,
305 )
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use metrale_gpu_runtime::gpu::mock::MockGpuBackend;
312
313 fn upload_u64(gpu: &MockGpuBackend, vals: &[u64]) -> DevicePtr {
314 let bytes: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
315 let p = gpu.alloc(bytes.len()).unwrap();
316 gpu.copy_h2d(&bytes, p).unwrap();
317 p
318 }
319
320 fn upload_f32(gpu: &MockGpuBackend, vals: &[f32]) -> DevicePtr {
321 let bytes: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
322 let p = gpu.alloc(bytes.len()).unwrap();
323 gpu.copy_h2d(&bytes, p).unwrap();
324 p
325 }
326
327 #[test]
330 fn read_reflects_current_device_contents_at_a_reused_address() {
331 let gpu = MockGpuBackend::new();
332 let old_model = [0x1111_u64, 0x2222, 0x3333];
333 let p = upload_u64(&gpu, &old_model);
334 assert_eq!(read_expert_ptrs_u64(&gpu, p, 3).unwrap(), old_model);
335
336 let new_model = [0xaaaa_u64, 0xbbbb, 0xcccc];
337 let bytes: Vec<u8> = new_model.iter().flat_map(|v| v.to_le_bytes()).collect();
338 gpu.copy_h2d(&bytes, p).unwrap();
339 assert_eq!(read_expert_ptrs_u64(&gpu, p, 3).unwrap(), new_model);
340
341 let old_scales = [1.0_f32, 2.0];
342 let ps = upload_f32(&gpu, &old_scales);
343 assert_eq!(read_expert_scales_f32(&gpu, ps, 2).unwrap(), old_scales);
344 let new_scales = [3.0_f32, 4.0];
345 let sbytes: Vec<u8> = new_scales.iter().flat_map(|v| v.to_le_bytes()).collect();
346 gpu.copy_h2d(&sbytes, ps).unwrap();
347 assert_eq!(read_expert_scales_f32(&gpu, ps, 2).unwrap(), new_scales);
348 }
349
350 #[test]
352 fn read_honors_the_requested_length() {
353 let gpu = MockGpuBackend::new();
354 let vals = [1_u64, 2, 3, 4];
355 let p = upload_u64(&gpu, &vals);
356 assert_eq!(read_expert_ptrs_u64(&gpu, p, 2).unwrap(), vals[..2]);
357 assert_eq!(read_expert_ptrs_u64(&gpu, p, 4).unwrap(), vals);
358 }
359
360 #[test]
363 fn snapshot_maps_every_table_to_its_field() {
364 let gpu = MockGpuBackend::new();
365 let n = 2;
366 let gate_packed = upload_u64(&gpu, &[10, 11]);
367 let gate_scale2 = upload_f32(&gpu, &[0.5, 0.25]);
368 let up_packed = upload_u64(&gpu, &[20, 21]);
369 let up_scale2 = upload_f32(&gpu, &[2.0, 4.0]);
370 let down_packed = upload_u64(&gpu, &[30, 31]);
371 let down_scale2 = upload_f32(&gpu, &[8.0, 16.0]);
372
373 let t = MoeCutlassHostTables::snapshot(
374 &gpu,
375 n,
376 gate_packed,
377 vec![100, 101],
378 gate_scale2,
379 up_packed,
380 vec![200, 201],
381 up_scale2,
382 Some((down_packed, vec![300, 301], down_scale2)),
383 )
384 .unwrap();
385
386 assert_eq!(t.gate_packed, [10, 11]);
387 assert_eq!(t.gate_sfb, [100, 101]);
388 assert_eq!(t.gate_scale2, [0.5, 0.25]);
389 assert_eq!(t.up_packed, [20, 21]);
390 assert_eq!(t.up_sfb, [200, 201]);
391 assert_eq!(t.up_scale2, [2.0, 4.0]);
392 let d = t.down.expect("down tables were supplied");
393 assert_eq!(d.packed, [30, 31]);
394 assert_eq!(d.sfb, [300, 301]);
395 assert_eq!(d.scale2, [8.0, 16.0]);
396 }
397}