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