1use anyhow::{Result, bail};
18
19mod hd128;
20pub use hd128::ragged_prefill_bf16_hd128;
21
22#[cfg(metrale_flashinfer)]
23use std::ffi::c_void;
24#[cfg(metrale_flashinfer)]
25use std::sync::OnceLock;
26
27#[cfg(metrale_flashinfer)]
28unsafe extern "C" {
29 #[allow(dead_code)]
31 fn metrale_fi_ragged_prefill_workspace_sizes(
32 max_batch: u32,
33 max_total_qo_rows: u32,
34 num_qo_heads: u32,
35 num_kv_heads: u32,
36 head_dim: u32,
37 float_ws_bytes_out: *mut usize,
38 int_ws_bytes_out: *mut usize,
39 pinned_int_ws_bytes_out: *mut usize,
40 ) -> i32;
41
42 #[allow(clippy::too_many_arguments)]
43 fn metrale_fi_ragged_prefill_bf16_hd256(
44 q: *const c_void,
45 k: *const c_void,
46 v: *const c_void,
47 o: *mut c_void,
48 qo_indptr_h: *const i32,
49 kv_indptr_h: *const i32,
50 qo_indptr_d: *const i32,
51 kv_indptr_d: *const i32,
52 batch: u32,
53 total_qo_rows: u32,
54 total_kv_rows: u32,
55 num_qo_heads: u32,
56 num_kv_heads: u32,
57 head_dim: u32,
58 sm_scale: f32,
59 causal: i32,
60 float_ws: *mut c_void,
61 float_ws_bytes: usize,
62 int_ws: *mut c_void,
63 int_ws_bytes: usize,
64 pinned_int_ws: *mut c_void,
65 pinned_int_ws_bytes: usize,
66 stream: *mut c_void,
67 ) -> i32;
68
69 #[cfg(metrale_flashinfer)]
70 fn cuMemAlloc_v2(dptr: *mut u64, bytesize: usize) -> i32;
71 #[cfg(metrale_flashinfer)]
72 fn cudaHostAlloc(ptr: *mut *mut c_void, size: usize, flags: u32) -> i32;
73}
74
75pub fn available() -> bool {
78 cfg!(metrale_flashinfer)
79}
80
81#[cfg(metrale_flashinfer)]
84struct Workspaces {
85 float_ws: u64,
86 int_ws: u64,
87 pinned_int_ws: u64,
88 float_sz: usize,
89 int_sz: usize,
90 pinned_sz: usize,
91}
92#[cfg(metrale_flashinfer)]
93unsafe impl Send for Workspaces {}
94#[cfg(metrale_flashinfer)]
95unsafe impl Sync for Workspaces {}
96#[cfg(metrale_flashinfer)]
97static WS: OnceLock<Workspaces> = OnceLock::new();
101
102#[cfg(metrale_flashinfer)]
103const MAX_BATCH: u32 = 16;
104#[cfg(metrale_flashinfer)]
105const MAX_TOTAL_QO_ROWS: u32 = 16 * 16384;
106#[cfg(metrale_flashinfer)]
107const N_QO_HEADS: u32 = 16;
108#[cfg(metrale_flashinfer)]
109const N_KV_HEADS: u32 = 2;
110#[cfg(metrale_flashinfer)]
111const HEAD_DIM: u32 = 256;
112
113#[cfg(metrale_flashinfer)]
116const FLOAT_WS_BYTES: usize = 256 << 20;
117#[cfg(metrale_flashinfer)]
118const INT_WS_BYTES: usize = 64 << 20;
119#[cfg(metrale_flashinfer)]
120const PINNED_WS_BYTES: usize = 64 << 20;
121
122#[cfg(metrale_flashinfer)]
123fn workspaces() -> Result<&'static Workspaces> {
124 if let Some(w) = WS.get() {
125 return Ok(w);
126 }
127 let _ = (
128 MAX_BATCH,
129 MAX_TOTAL_QO_ROWS,
130 N_QO_HEADS,
131 N_KV_HEADS,
132 HEAD_DIM,
133 );
134 let (fsz, isz, psz) = (FLOAT_WS_BYTES, INT_WS_BYTES, PINNED_WS_BYTES);
135 let mut float_ws = 0u64;
136 let mut int_ws = 0u64;
137 let mut pinned = std::ptr::null_mut::<c_void>();
138 unsafe {
139 let s1 = cuMemAlloc_v2(&mut float_ws, fsz.max(1));
140 if s1 != 0 {
141 bail!("cuMemAlloc FlashInfer float ws ({fsz}B) failed: {s1}");
142 }
143 let s2 = cuMemAlloc_v2(&mut int_ws, isz.max(1));
144 if s2 != 0 {
145 bail!("cuMemAlloc FlashInfer int ws ({isz}B) failed: {s2}");
146 }
147 let s3 = cudaHostAlloc(&mut pinned, psz.max(1), 0);
148 if s3 != 0 {
149 bail!("cudaHostAlloc FlashInfer pinned int ws ({psz}B) failed: {s3}");
150 }
151 }
152 let _ = WS.set(Workspaces {
153 float_ws,
154 int_ws,
155 pinned_int_ws: pinned as u64,
156 float_sz: fsz,
157 int_sz: isz,
158 pinned_sz: psz,
159 });
160 Ok(WS.get().unwrap())
161}
162
163#[allow(clippy::too_many_arguments)]
172pub fn ragged_prefill_bf16_hd256(
173 q: u64,
174 k: u64,
175 v: u64,
176 o: u64,
177 qo_indptr_h: &[i32],
178 kv_indptr_h: &[i32],
179 qo_indptr_d: u64,
180 kv_indptr_d: u64,
181 batch: u32,
182 total_qo_rows: u32,
183 total_kv_rows: u32,
184 num_qo_heads: u32,
185 num_kv_heads: u32,
186 head_dim: u32,
187 sm_scale: f32,
188 causal: bool,
189 stream: u64,
190) -> Result<()> {
191 #[cfg(metrale_flashinfer)]
192 {
193 if head_dim != HEAD_DIM {
194 bail!("FlashInfer wrapper is head_dim=256 only (got {head_dim})");
195 }
196 if qo_indptr_h.len() != (batch + 1) as usize || kv_indptr_h.len() != (batch + 1) as usize {
197 bail!("indptr host slices must be batch+1 long");
198 }
199 let ws = workspaces()?;
200 let st = unsafe {
201 metrale_fi_ragged_prefill_bf16_hd256(
202 q as *const c_void,
203 k as *const c_void,
204 v as *const c_void,
205 o as *mut c_void,
206 qo_indptr_h.as_ptr(),
207 kv_indptr_h.as_ptr(),
208 qo_indptr_d as *const i32,
209 kv_indptr_d as *const i32,
210 batch,
211 total_qo_rows,
212 total_kv_rows,
213 num_qo_heads,
214 num_kv_heads,
215 head_dim,
216 sm_scale,
217 if causal { 1 } else { 0 },
218 ws.float_ws as *mut c_void,
219 ws.float_sz,
220 ws.int_ws as *mut c_void,
221 ws.int_sz,
222 ws.pinned_int_ws as *mut c_void,
223 ws.pinned_sz,
224 stream as *mut c_void,
225 )
226 };
227 if st != 0 {
228 bail!(
229 "FlashInfer ragged prefill failed: status {st} (batch={batch}, qo={total_qo_rows})"
230 );
231 }
232 Ok(())
233 }
234 #[cfg(not(metrale_flashinfer))]
235 {
236 let _ = (
237 q,
238 k,
239 v,
240 o,
241 qo_indptr_h,
242 kv_indptr_h,
243 qo_indptr_d,
244 kv_indptr_d,
245 batch,
246 total_qo_rows,
247 total_kv_rows,
248 num_qo_heads,
249 num_kv_heads,
250 head_dim,
251 sm_scale,
252 causal,
253 stream,
254 );
255 bail!("FlashInfer support was not built; set FLASHINFER_HOME when building")
256 }
257}
258
259#[cfg(all(test, metrale_flashinfer))]
260mod tests {
261 use super::*;
262 use std::ffi::c_void;
263
264 const H2D: i32 = 1;
265 const D2H: i32 = 2;
266 unsafe extern "C" {
267 fn cudaMalloc(p: *mut *mut c_void, n: usize) -> i32;
268 fn cudaFree(p: *mut c_void) -> i32;
269 fn cudaMemcpy(d: *mut c_void, s: *const c_void, n: usize, k: i32) -> i32;
270 fn cudaDeviceSynchronize() -> i32;
271 }
272 fn f32_to_bf16(x: f32) -> u16 {
273 let b = x.to_bits();
274 ((b + 0x7fff + ((b >> 16) & 1)) >> 16) as u16
275 }
276 fn bf16_to_f32(x: u16) -> f32 {
277 f32::from_bits((x as u32) << 16)
278 }
279 unsafe fn dev<T>(data: &[T]) -> u64 {
280 let bytes = std::mem::size_of_val(data);
281 let mut p = std::ptr::null_mut();
282 assert_eq!(unsafe { cudaMalloc(&mut p, bytes.max(1)) }, 0);
283 assert_eq!(
284 unsafe { cudaMemcpy(p, data.as_ptr() as *const c_void, bytes, H2D) },
285 0
286 );
287 p as u64
288 }
289
290 #[test]
291 #[ignore = "requires a free CUDA device + FLASHINFER_HOME build"]
292 #[allow(clippy::needless_range_loop)] fn flashinfer_ragged_prefill_matches_cpu_reference() {
294 const HD: usize = 256;
295 const NQO: usize = 4;
296 const NKV: usize = 2;
297 let lens = [6usize, 10usize];
298 let qo_indptr: Vec<i32> = {
299 let mut v = vec![0i32];
300 for &l in &lens {
301 v.push(v.last().unwrap() + l as i32);
302 }
303 v
304 };
305 let kv_indptr = qo_indptr.clone();
306 let total: usize = lens.iter().sum();
307 let sm_scale = 1.0f32 / (HD as f32).sqrt();
308
309 let rnd = |seed: u64| -> f32 {
310 let mut x = seed.wrapping_mul(0x9E3779B97F4A7C15).wrapping_add(1);
311 x ^= x >> 31;
312 x = x.wrapping_mul(0xBF58476D1CE4E5B9);
313 ((x >> 40) as f32 / (1u64 << 24) as f32 - 0.5) * 0.5
314 };
315 let q: Vec<u16> = (0..total * NQO * HD)
316 .map(|i| f32_to_bf16(rnd(i as u64)))
317 .collect();
318 let k: Vec<u16> = (0..total * NKV * HD)
319 .map(|i| f32_to_bf16(rnd(i as u64 ^ 0x1111)))
320 .collect();
321 let v: Vec<u16> = (0..total * NKV * HD)
322 .map(|i| f32_to_bf16(rnd(i as u64 ^ 0x2222)))
323 .collect();
324 let mut o = vec![0u16; total * NQO * HD];
325
326 let (q_d, k_d, v_d, o_d, qo_d, kv_d);
327 unsafe {
328 q_d = dev(&q);
329 k_d = dev(&k);
330 v_d = dev(&v);
331 o_d = dev(&o);
332 qo_d = dev(&qo_indptr);
333 kv_d = dev(&kv_indptr);
334 }
335
336 ragged_prefill_bf16_hd256(
337 q_d,
338 k_d,
339 v_d,
340 o_d,
341 &qo_indptr,
342 &kv_indptr,
343 qo_d,
344 kv_d,
345 lens.len() as u32,
346 total as u32,
347 total as u32,
348 NQO as u32,
349 NKV as u32,
350 HD as u32,
351 sm_scale,
352 true,
353 0,
354 )
355 .unwrap();
356 unsafe {
357 assert_eq!(cudaDeviceSynchronize(), 0);
358 assert_eq!(
359 cudaMemcpy(
360 o.as_mut_ptr() as *mut c_void,
361 o_d as *const c_void,
362 o.len() * 2,
363 D2H
364 ),
365 0
366 );
367 }
368
369 let group = NQO / NKV;
371 let qf = |r: usize, h: usize, d: usize| bf16_to_f32(q[(r * NQO + h) * HD + d]);
372 let kf = |r: usize, kh: usize, d: usize| bf16_to_f32(k[(r * NKV + kh) * HD + d]);
373 let vf = |r: usize, kh: usize, d: usize| bf16_to_f32(v[(r * NKV + kh) * HD + d]);
374 let mut max_rel = 0.0f64;
375 let mut worst_cos = 1.0f64;
376 for (b, &len) in lens.iter().enumerate() {
377 let start = qo_indptr[b] as usize;
378 for qi in 0..len {
379 for h in 0..NQO {
380 let kh = h / group;
381 let mut scores = vec![0f32; qi + 1];
382 for j in 0..=qi {
383 let mut s = 0.0f32;
384 for d in 0..HD {
385 s += qf(start + qi, h, d) * kf(start + j, kh, d);
386 }
387 scores[j] = s * sm_scale;
388 }
389 let mx = scores.iter().cloned().fold(f32::MIN, f32::max);
390 let mut den = 0.0f32;
391 for s in &mut scores {
392 *s = (*s - mx).exp();
393 den += *s;
394 }
395 let mut out_ref = vec![0f32; HD];
396 for (j, &p) in scores.iter().enumerate() {
397 let w = p / den;
398 for d in 0..HD {
399 out_ref[d] += w * vf(start + j, kh, d);
400 }
401 }
402 let mut dot = 0.0f64;
403 let mut na = 0.0f64;
404 let mut nb = 0.0f64;
405 for d in 0..HD {
406 let g = bf16_to_f32(o[(start + qi) * NQO * HD + h * HD + d]) as f64;
407 let r = out_ref[d] as f64;
408 dot += g * r;
409 na += g * g;
410 nb += r * r;
411 max_rel = max_rel.max((g - r).abs() / (r.abs() + 1e-3));
412 }
413 let cos = dot / (na.sqrt() * nb.sqrt() + 1e-12);
414 worst_cos = worst_cos.min(cos);
415 }
416 }
417 }
418 unsafe {
419 for p in [q_d, k_d, v_d, o_d, qo_d, kv_d] {
420 cudaFree(p as *mut c_void);
421 }
422 }
423 tracing::debug!("FLASHINFER_RAGGED worst_cos={worst_cos:.6} max_rel={max_rel:.4}");
424 assert!(
425 worst_cos > 0.99,
426 "FlashInfer ragged prefill diverges from CPU ref: cos {worst_cos}"
427 );
428 }
429}