phase 5: naive multi-head attention

- Batched GEMM via cublasGemmStridedBatchedEx
- Causal mask CUDA kernel (F32 + BF16)
- Element-wise scale CUDA kernel (F32 + BF16)
- attention() composing: batched_matmul + scale + causal_mask + softmax
- Fixed to_device/contiguous infinite recursion (GPU contiguous via CPU round-trip)
- 5 attention tests passing (max_err < 3e-7 F32)
- Total: 61 tests passing across all crates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 21:17:23 +08:00
parent c8e8153702
commit 6035ffdc0b
10 changed files with 550 additions and 12 deletions

View File

@@ -0,0 +1,77 @@
use std::ffi::c_void;
use xserv_tensor::{DType, Tensor};
use crate::activation::scale;
use crate::gemm::batched_matmul;
use crate::softmax::softmax;
unsafe extern "C" {
fn launch_causal_mask_f32(scores: *mut c_void, batch: i32, rows: i32, cols: i32,
offset: i32, stream: *mut c_void);
fn launch_causal_mask_bf16(scores: *mut c_void, batch: i32, rows: i32, cols: i32,
offset: i32, stream: *mut c_void);
}
fn apply_causal_mask(scores: &Tensor, offset: usize) {
let ndim = scores.ndim();
let rows = scores.shape()[ndim - 2];
let cols = scores.shape()[ndim - 1];
let batch: usize = scores.shape()[..ndim - 2].iter().product();
unsafe {
match scores.dtype() {
DType::F32 => launch_causal_mask_f32(
scores.data_ptr() as *mut c_void,
batch as i32, rows as i32, cols as i32, offset as i32,
std::ptr::null_mut(),
),
DType::BF16 => launch_causal_mask_bf16(
scores.data_ptr() as *mut c_void,
batch as i32, rows as i32, cols as i32, offset as i32,
std::ptr::null_mut(),
),
_ => panic!("unsupported dtype for causal mask"),
}
}
xserv_cuda::device::synchronize().unwrap();
}
/// Multi-head attention (naive, materializes S×S score matrix).
///
/// q, k, v: [batch, num_heads, seq_len, head_dim] — contiguous, on GPU
/// Returns: [batch, num_heads, seq_len, head_dim]
pub fn attention(q: &Tensor, k: &Tensor, v: &Tensor, causal: bool) -> Tensor {
assert_eq!(q.ndim(), 4);
assert_eq!(k.ndim(), 4);
assert_eq!(v.ndim(), 4);
assert!(q.is_contiguous() && k.is_contiguous() && v.is_contiguous());
let batch = q.shape()[0];
let num_heads = q.shape()[1];
let q_len = q.shape()[2];
let head_dim = q.shape()[3];
let kv_len = k.shape()[2];
assert_eq!(k.shape(), &[batch, num_heads, kv_len, head_dim]);
assert_eq!(v.shape(), &[batch, num_heads, kv_len, head_dim]);
// scores = Q @ K^T → [B, H, q_len, kv_len]
let k_t = k.transpose(2, 3).contiguous();
let scores = batched_matmul(q, &k_t);
// Scale by 1/sqrt(head_dim)
let scale_factor = 1.0 / (head_dim as f32).sqrt();
let scaled_scores = scale(&scores, scale_factor);
// Causal mask
if causal {
let offset = kv_len - q_len;
apply_causal_mask(&scaled_scores, offset);
}
// Softmax
let weights = softmax(&scaled_scores);
// output = weights @ V → [B, H, q_len, head_dim]
batched_matmul(&weights, v)
}