use super::{dummy_storage, groups64, uniform, ComputeEngine};
use crate::{UnifiedTensor, WgpuContext};
use wgpu::Buffer;
impl ComputeEngine {
pub fn rope(&self, ctx: &WgpuContext, tensor: &UnifiedTensor, head_dim: usize, pos: usize) {
self.rope_theta(ctx, tensor, head_dim, pos, 10000.0);
}
pub fn rope_theta(
&self,
ctx: &WgpuContext,
tensor: &UnifiedTensor,
head_dim: usize,
pos: usize,
theta: f32,
) {
let (seq_len, tokens) = match tensor.shape.len() {
0 | 1 => (1, 1),
2 => (tensor.shape[0], tensor.shape[0]),
_ => (tensor.shape[1], tensor.shape[0] * tensor.shape[1]),
};
let total = tensor.shape.iter().product::<usize>();
let heads = total / (tokens * head_dim).max(1);
let params = uniform(
ctx,
bytemuck::cast_slice(&[
head_dim as u32,
seq_len as u32,
pos as u32,
heads as u32,
theta.to_bits(),
0,
0,
0,
]),
);
let pairs = tensor.shape.iter().product::<usize>() / 2;
self.run(
ctx,
&self.rope,
&[(0, tensor.buffer()), (1, ¶ms)],
groups64(pairs),
);
}
pub fn flash_attention(
&self,
ctx: &WgpuContext,
q: &UnifiedTensor,
k: &UnifiedTensor,
v: &UnifiedTensor,
out: &UnifiedTensor,
causal: bool,
) {
let [q4, k4, v4, o4] = [q, k, v, out].map(single_head_view);
self.flash_attention_v2(ctx, &q4, &k4, &v4, &o4, causal, 0.0, 0, None);
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attention_v2(
&self,
ctx: &WgpuContext,
q: &UnifiedTensor,
k: &UnifiedTensor,
v: &UnifiedTensor,
out: &UnifiedTensor,
causal: bool,
dropout_prob: f32,
seed: u64,
mask: Option<&UnifiedTensor>,
) {
let opts = AttentionOptions {
causal,
dropout_prob,
seed,
..AttentionOptions::default()
};
self.flash_attention_v2_with(ctx, q, k, v, out, &opts, mask);
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attention_v2_with(
&self,
ctx: &WgpuContext,
q: &UnifiedTensor,
k: &UnifiedTensor,
v: &UnifiedTensor,
out: &UnifiedTensor,
opts: &AttentionOptions,
mask: Option<&UnifiedTensor>,
) {
let dims = AttentionDims::new(q, k);
let params = dims.params(ctx, opts, mask);
let dummy;
let mask_buf = match mask {
Some(m) => m.buffer(),
None => {
dummy = dummy_storage(ctx);
&dummy
}
};
self.run(
ctx,
&self.flash_attn_v2,
&[
(0, q.buffer()),
(1, k.buffer()),
(2, v.buffer()),
(3, out.buffer()),
(4, ¶ms),
(5, mask_buf),
],
(1, dims.seq_q.div_ceil(128), dims.batch * dims.heads),
);
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attention_backward(
&self,
ctx: &WgpuContext,
q: &UnifiedTensor,
k: &UnifiedTensor,
v: &UnifiedTensor,
out: &UnifiedTensor,
grad_out: &UnifiedTensor,
grad_q: &UnifiedTensor,
grad_k: &UnifiedTensor,
grad_v: &UnifiedTensor,
causal: bool,
) {
let views = [q, k, v, out, grad_out, grad_q, grad_k, grad_v].map(single_head_view);
let [q4, k4, v4, o4, go4, gq4, gk4, gv4] = &views;
self.flash_attention_v2_backward(ctx, q4, k4, v4, o4, go4, gq4, gk4, gv4, causal);
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attention_v2_backward(
&self,
ctx: &WgpuContext,
q: &UnifiedTensor,
k: &UnifiedTensor,
v: &UnifiedTensor,
out: &UnifiedTensor,
grad_out: &UnifiedTensor,
grad_q: &UnifiedTensor,
grad_k: &UnifiedTensor,
grad_v: &UnifiedTensor,
causal: bool,
) {
let opts = AttentionOptions {
causal,
..AttentionOptions::default()
};
self.flash_attention_v2_backward_with(
ctx, q, k, v, out, grad_out, grad_q, grad_k, grad_v, &opts, None,
);
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attention_v2_backward_with(
&self,
ctx: &WgpuContext,
q: &UnifiedTensor,
k: &UnifiedTensor,
v: &UnifiedTensor,
out: &UnifiedTensor,
grad_out: &UnifiedTensor,
grad_q: &UnifiedTensor,
grad_k: &UnifiedTensor,
grad_v: &UnifiedTensor,
opts: &AttentionOptions,
mask: Option<&UnifiedTensor>,
) {
let dims = AttentionDims::new(q, k);
let params = dims.params_bwd(ctx, opts, mask);
let dummy;
let mask_buf = match mask {
Some(m) => m.buffer(),
None => {
dummy = dummy_storage(ctx);
&dummy
}
};
let stats = ctx.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Flash Bwd Stats"),
size: (dims.batch * dims.heads * dims.seq_q).max(1) as u64 * 8,
usage: wgpu::BufferUsages::STORAGE,
mapped_at_creation: false,
});
let (b, h, hkv) = (dims.batch, dims.heads, dims.kv_heads);
self.run(
ctx,
&self.flash_attn_v2_bw_dq,
&[
(0, q.buffer()),
(1, k.buffer()),
(2, v.buffer()),
(3, out.buffer()),
(4, grad_out.buffer()),
(5, grad_q.buffer()),
(8, ¶ms),
(9, mask_buf),
(10, &stats),
],
(1, dims.seq_q.div_ceil(128), b * h),
);
self.run(
ctx,
&self.flash_attn_v2_bw_dkdv,
&[
(0, q.buffer()),
(1, k.buffer()),
(2, v.buffer()),
(4, grad_out.buffer()),
(6, grad_k.buffer()),
(7, grad_v.buffer()),
(8, ¶ms),
(9, mask_buf),
(10, &stats),
],
(1, dims.seq_k.div_ceil(128), b * hkv),
);
}
}
fn single_head_view(t: &UnifiedTensor) -> UnifiedTensor {
assert_eq!(t.shape.len(), 2, "expected a [seq_len, head_dim] tensor");
t.reshaped(&[1, t.shape[0], 1, t.shape[1]])
}
struct AttentionDims {
batch: u32,
seq_q: u32,
seq_k: u32,
heads: u32,
kv_heads: u32,
head_dim: u32,
}
impl AttentionDims {
fn new(q: &UnifiedTensor, k: &UnifiedTensor) -> Self {
assert_eq!(q.shape.len(), 4, "q must be [batch, seq, heads, head_dim]");
assert_eq!(
k.shape.len(),
4,
"k must be [batch, seq, kv_heads, head_dim]"
);
let dims = Self {
batch: q.shape[0] as u32,
seq_q: q.shape[1] as u32,
heads: q.shape[2] as u32,
head_dim: q.shape[3] as u32,
seq_k: k.shape[1] as u32,
kv_heads: k.shape[2] as u32,
};
assert!(
dims.head_dim <= 128,
"flash attention supports head_dim <= 128, got {}",
dims.head_dim
);
assert!(
dims.kv_heads > 0 && dims.heads.is_multiple_of(dims.kv_heads),
"num_heads ({}) must be a multiple of num_kv_heads ({})",
dims.heads,
dims.kv_heads
);
dims
}
fn scale(&self) -> f32 {
1.0 / (self.head_dim as f32).sqrt()
}
fn params(
&self,
ctx: &WgpuContext,
opts: &AttentionOptions,
mask: Option<&UnifiedTensor>,
) -> Buffer {
let (has_mask, s) = mask_strides(mask);
let params: [u32; 20] = [
self.batch,
self.heads,
self.seq_q,
self.seq_k,
self.head_dim,
opts.causal as u32,
self.scale().to_bits(),
self.kv_heads,
opts.dropout_prob.to_bits(),
opts.seed as u32,
has_mask,
opts.causal_offset,
s[0],
s[1],
s[2],
s[3],
opts.window,
0,
0,
0,
];
uniform(ctx, bytemuck::cast_slice(¶ms))
}
fn params_bwd(
&self,
ctx: &WgpuContext,
opts: &AttentionOptions,
mask: Option<&UnifiedTensor>,
) -> Buffer {
let (has_mask, s) = mask_strides(mask);
let params: [u32; 20] = [
self.batch,
self.heads,
self.seq_q,
self.seq_k,
self.head_dim,
self.kv_heads,
self.scale().to_bits(),
opts.dropout_prob.to_bits(),
opts.seed as u32,
opts.causal as u32,
has_mask,
opts.causal_offset,
s[0],
s[1],
s[2],
s[3],
opts.window,
0,
0,
0,
];
uniform(ctx, bytemuck::cast_slice(¶ms))
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct AttentionOptions {
pub causal: bool,
pub causal_offset: u32,
pub window: u32,
pub dropout_prob: f32,
pub seed: u64,
}
fn mask_strides(mask: Option<&UnifiedTensor>) -> (u32, [u32; 4]) {
let Some(m) = mask else {
return (0, [0; 4]);
};
let shape = &m.shape;
if shape.len() != 4 {
return (1, [0; 4]);
}
let dim_b = shape[0] as u32;
let dim_h = shape[1] as u32;
let dim_q = shape[2] as u32;
let dim_k = shape[3] as u32;
let s = |dim: u32, stride: u32| if dim > 1 { stride } else { 0 };
(
1,
[
s(dim_b, dim_k * dim_q * dim_h),
s(dim_h, dim_k * dim_q),
s(dim_q, dim_k),
s(dim_k, 1),
],
)
}