use cudarc::driver::{CudaSlice, LaunchConfig, PushKernelArg};
use super::types::CudaGraphError;
use super::cudagraph_type::CudaGraph;
pub const DIT_ATTN_MAX_SEQ: usize = 8192;
pub const DIT_ATTN_MAX_HEAD_DIM: usize = 384;
const DIT_FLASH_HEAD_DIM_CAP: usize = 384;
const DIT_FLASH_BQ: usize = 8;
const DIT_FLASH_BK: usize = 32;
impl CudaGraph {
#[allow(clippy::too_many_arguments)]
fn joint_attn_flash_validate(
q: &[f32],
k: &[f32],
v: &[f32],
out: &[f32],
num_heads: usize,
seq: usize,
head_dim: usize,
) -> Result<(usize, usize, f32), CudaGraphError> {
if num_heads == 0 || seq == 0 || head_dim == 0 {
return Err(CudaGraphError::DriverError(format!(
"joint_attention_flash: dims must be non-zero (num_heads={num_heads}, seq={seq}, head_dim={head_dim})"
)));
}
if seq > DIT_ATTN_MAX_SEQ {
return Err(CudaGraphError::DriverError(format!(
"joint_attention_flash: seq {seq} exceeds kernel cap {DIT_ATTN_MAX_SEQ}"
)));
}
if head_dim > DIT_ATTN_MAX_HEAD_DIM {
return Err(CudaGraphError::DriverError(format!(
"joint_attention_flash: head_dim {head_dim} exceeds kernel cap {DIT_ATTN_MAX_HEAD_DIM}"
)));
}
if head_dim % 8 != 0 {
return Err(CudaGraphError::DriverError(format!(
"joint_attention_flash: head_dim {head_dim} must be a multiple of 8"
)));
}
if head_dim > DIT_FLASH_HEAD_DIM_CAP {
return Err(CudaGraphError::DriverError(format!(
"joint_attention_flash: head_dim {head_dim} exceeds the flash kernel cap ({DIT_FLASH_HEAD_DIM_CAP})"
)));
}
let qkv_len = num_heads * seq * head_dim;
let out_len = seq * num_heads * head_dim;
if q.len() != qkv_len || k.len() != qkv_len || v.len() != qkv_len {
return Err(CudaGraphError::DriverError(format!(
"joint_attention_flash: q/k/v len mismatch (need {qkv_len}, got {}/{}/{})",
q.len(),
k.len(),
v.len()
)));
}
if out.len() != out_len {
return Err(CudaGraphError::DriverError(format!(
"joint_attention_flash: out len mismatch (need {out_len}, got {})",
out.len()
)));
}
let scale = 1.0f32 / (head_dim as f32).sqrt();
Ok((qkv_len, out_len, scale))
}
#[allow(clippy::too_many_arguments)]
pub(crate) unsafe fn launch_joint_attention_flash_resident(
&self,
d_q: &CudaSlice<f32>,
d_k: &CudaSlice<f32>,
d_v: &CudaSlice<f32>,
d_out: &mut CudaSlice<f32>,
num_heads: u32,
seq: u32,
head_dim: u32,
scale: f32,
) -> Result<(), CudaGraphError> {
let shared_mem_bytes =
(DIT_FLASH_BK * head_dim as usize * 2 * std::mem::size_of::<f32>()) as u32;
let cfg = LaunchConfig {
grid_dim: ((seq as usize).div_ceil(DIT_FLASH_BQ) as u32, num_heads, 1),
block_dim: (128, 1, 1),
shared_mem_bytes,
};
let func = if head_dim <= 128 {
&self.modules.joint_attention_flash_f32
} else {
&self.modules.joint_attention_flash_f32_large
};
self.stream
.launch_builder(func)
.arg(d_q)
.arg(d_k)
.arg(d_v)
.arg(d_out)
.arg(&num_heads)
.arg(&seq)
.arg(&head_dim)
.arg(&scale)
.launch(cfg)
.map(|_| ())
.map_err(|e| CudaGraphError::DriverError(format!("joint_attention_flash launch: {e}")))
}
#[allow(clippy::too_many_arguments)]
pub fn encode_joint_attention_flash(
&self,
q: &[f32],
k: &[f32],
v: &[f32],
out: &mut [f32],
num_heads: usize,
seq: usize,
head_dim: usize,
) -> Result<(), CudaGraphError> {
let (qkv_len, out_len, scale) =
Self::joint_attn_flash_validate(q, k, v, out, num_heads, seq, head_dim)?;
let mut d_q = self
.stream
.alloc_zeros::<f32>(qkv_len)
.map_err(|e| CudaGraphError::DriverError(format!("alloc_zeros joint_attn q: {e}")))?;
self.stream
.memcpy_htod(&q[..qkv_len], &mut d_q)
.map_err(|e| CudaGraphError::DriverError(format!("htod joint_attn q: {e}")))?;
let mut d_k = self
.stream
.alloc_zeros::<f32>(qkv_len)
.map_err(|e| CudaGraphError::DriverError(format!("alloc_zeros joint_attn k: {e}")))?;
self.stream
.memcpy_htod(&k[..qkv_len], &mut d_k)
.map_err(|e| CudaGraphError::DriverError(format!("htod joint_attn k: {e}")))?;
let mut d_v = self
.stream
.alloc_zeros::<f32>(qkv_len)
.map_err(|e| CudaGraphError::DriverError(format!("alloc_zeros joint_attn v: {e}")))?;
self.stream
.memcpy_htod(&v[..qkv_len], &mut d_v)
.map_err(|e| CudaGraphError::DriverError(format!("htod joint_attn v: {e}")))?;
let mut d_out = self
.stream
.alloc_zeros::<f32>(out_len)
.map_err(|e| CudaGraphError::DriverError(format!("alloc_zeros joint_attn out: {e}")))?;
unsafe {
self.launch_joint_attention_flash_resident(
&d_q,
&d_k,
&d_v,
&mut d_out,
num_heads as u32,
seq as u32,
head_dim as u32,
scale,
)?;
}
self.stream
.memcpy_dtoh(&d_out, &mut out[..out_len])
.map_err(|e| CudaGraphError::DriverError(format!("dtoh joint_attn out: {e}")))?;
self.stream
.synchronize()
.map_err(|e| CudaGraphError::DriverError(format!("joint_attn sync: {e}")))?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn encode_joint_attention_flash_pooled(
&self,
q: &[f32],
k: &[f32],
v: &[f32],
out: &mut [f32],
num_heads: usize,
seq: usize,
head_dim: usize,
) -> Result<(), CudaGraphError> {
self.encode_joint_attention_flash(q, k, v, out, num_heads, seq, head_dim)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cpu_joint_attention(
q: &[f32],
k: &[f32],
v: &[f32],
num_heads: usize,
seq: usize,
head_dim: usize,
) -> Vec<f32> {
let inner = num_heads * head_dim;
let scale = 1.0f32 / (head_dim as f32).sqrt();
let mut out = vec![0.0f32; seq * inner];
for h in 0..num_heads {
let head_off = h * seq * head_dim;
for qi in 0..seq {
let q_row = &q[head_off + qi * head_dim..head_off + (qi + 1) * head_dim];
let mut scores = vec![0.0f32; seq];
for (ki, score) in scores.iter_mut().enumerate() {
let k_row = &k[head_off + ki * head_dim..head_off + (ki + 1) * head_dim];
let mut acc = 0.0f32;
for d in 0..head_dim {
acc += q_row[d] * k_row[d];
}
*score = acc * scale;
}
let mut maxv = f32::NEG_INFINITY;
for &s in scores.iter() {
maxv = maxv.max(s);
}
let mut sum = 0.0f32;
for s in scores.iter_mut() {
*s = (*s - maxv).exp();
sum += *s;
}
let inv = if sum > 0.0 { 1.0 / sum } else { 0.0 };
let dst = &mut out[qi * inner + h * head_dim..qi * inner + (h + 1) * head_dim];
for (ki, &w) in scores.iter().enumerate() {
let p = w * inv;
let v_row = &v[head_off + ki * head_dim..head_off + (ki + 1) * head_dim];
for d in 0..head_dim {
dst[d] += p * v_row[d];
}
}
}
}
out
}
fn lcg_fill(buf: &mut [f32], mut state: u64) {
for x in buf.iter_mut() {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let bits = (state >> 33) as u32; *x = (bits as f32 / (1u32 << 31) as f32) * 2.0 - 1.0;
}
}
fn cosine(a: &[f32], b: &[f32]) -> f32 {
let mut dot = 0.0f64;
let mut na = 0.0f64;
let mut nb = 0.0f64;
for (&x, &y) in a.iter().zip(b.iter()) {
dot += x as f64 * y as f64;
na += x as f64 * x as f64;
nb += y as f64 * y as f64;
}
if na == 0.0 || nb == 0.0 {
return 0.0;
}
(dot / (na.sqrt() * nb.sqrt())) as f32
}
fn max_abs_diff(a: &[f32], b: &[f32]) -> f32 {
a.iter()
.zip(b.iter())
.map(|(&x, &y)| (x - y).abs())
.fold(0.0f32, f32::max)
}
#[test]
fn joint_attention_flash_parity() {
let _serial = super::super::types::gpu_parity_test_guard();
let graph = match CudaGraph::global() {
Ok(g) => g,
Err(_) => {
eprintln!("joint_attention_flash_parity: no CUDA device — skipping");
return;
}
};
let head_dim = 128usize;
let shapes: &[(usize, usize)] = &[(1, 8), (1, 32), (2, 40), (2, 50), (24, 64)];
for &(num_heads, seq) in shapes {
let qkv_len = num_heads * seq * head_dim;
let out_len = seq * num_heads * head_dim;
let mut q = vec![0.0f32; qkv_len];
let mut k = vec![0.0f32; qkv_len];
let mut v = vec![0.0f32; qkv_len];
lcg_fill(&mut q, 0x1234_5678_9abc_def0 ^ (seq as u64));
lcg_fill(&mut k, 0x0fed_cba9_8765_4321 ^ (num_heads as u64));
lcg_fill(&mut v, 0xdead_beef_cafe_babe ^ ((seq * num_heads) as u64));
let cpu = cpu_joint_attention(&q, &k, &v, num_heads, seq, head_dim);
let mut gpu = vec![0.0f32; out_len];
graph
.encode_joint_attention_flash(&q, &k, &v, &mut gpu, num_heads, seq, head_dim)
.expect("encode_joint_attention_flash");
let cos = cosine(&gpu, &cpu);
let mad = max_abs_diff(&gpu, &cpu);
assert!(
cos >= 0.999,
"cos {cos} < 0.999 for (num_heads={num_heads}, seq={seq})"
);
assert!(
mad < 1e-3,
"max-abs {mad} >= 1e-3 for (num_heads={num_heads}, seq={seq})"
);
let mut gpu_pooled = vec![0.0f32; out_len];
graph
.encode_joint_attention_flash_pooled(
&q,
&k,
&v,
&mut gpu_pooled,
num_heads,
seq,
head_dim,
)
.expect("encode_joint_attention_flash_pooled");
assert_eq!(
gpu, gpu_pooled,
"pooled != fresh for (num_heads={num_heads}, seq={seq})"
);
}
}
#[test]
fn joint_attn_flash_validate_rejects_bad_shapes() {
let head_dim = 128usize;
let (num_heads, seq) = (2usize, 16usize);
let qkv_len = num_heads * seq * head_dim;
let out_len = seq * num_heads * head_dim;
let q = vec![0.0f32; qkv_len];
let k = vec![0.0f32; qkv_len];
let v = vec![0.0f32; qkv_len];
let out = vec![0.0f32; out_len];
assert!(
CudaGraph::joint_attn_flash_validate(&q, &k, &v, &out, num_heads, seq, head_dim)
.is_ok()
);
assert!(
CudaGraph::joint_attn_flash_validate(&q, &k, &v, &out, num_heads, seq, 130).is_err()
);
assert!(
CudaGraph::joint_attn_flash_validate(&q, &k, &v, &out, num_heads, seq, 512).is_err()
);
assert!(CudaGraph::joint_attn_flash_validate(&q, &k, &v, &out, 0, seq, head_dim).is_err());
let q_short = vec![0.0f32; qkv_len - 1];
assert!(CudaGraph::joint_attn_flash_validate(
&q_short, &k, &v, &out, num_heads, seq, head_dim
)
.is_err());
}
}