use metal::MTLSize;
use crate::buffer::MlxBuffer;
use crate::device::MlxDevice;
use crate::encoder::{as_bytes, CapturedOpKind, CommandEncoder, KernelArg};
use crate::error::{MlxError, Result};
use crate::kernel_registry::KernelRegistry;
pub use super::flash_attn_vec_tq_hb::{
compute_nsg, tmp_buffer_bytes, FlashAttnVecTqHbParams,
};
pub static FLASH_ATTN_VEC_HYBRID_SHADER_SOURCE: &str =
include_str!("../shaders/flash_attn_vec_hybrid.metal");
pub fn register(registry: &mut KernelRegistry) {
registry.register_source("flash_attn_vec_hybrid_dk256", FLASH_ATTN_VEC_HYBRID_SHADER_SOURCE);
registry.register_source("flash_attn_vec_hybrid_dk512", FLASH_ATTN_VEC_HYBRID_SHADER_SOURCE);
}
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct FlashAttnVecHybridParamsGpu {
n_heads: u32,
n_kv_heads: u32,
head_dim: u32,
kv_seq_len: u32,
kv_capacity: u32,
scale: f32,
mask_type: u32,
sliding_window: u32,
softcap: f32,
nwg: u32,
ring_start: u32,
scale_factor_d512: f32,
codebook_bits: u32,
fuse_fwht_pre: u32,
nsg: u32,
n_queries: u32,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct FlashAttnVecReduceParamsGpu {
nrows: u32,
}
fn validate_params(params: &FlashAttnVecTqHbParams) -> Result<()> {
if params.head_dim != 256 && params.head_dim != 512 {
return Err(MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid: head_dim must be 256 or 512, got {}",
params.head_dim
)));
}
if params.num_heads == 0 || params.num_kv_heads == 0 {
return Err(MlxError::InvalidArgument(
"flash_attn_vec_hybrid: num_heads and num_kv_heads must be > 0".into(),
));
}
if params.num_heads % params.num_kv_heads != 0 {
return Err(MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid: num_heads ({}) % num_kv_heads ({}) != 0",
params.num_heads, params.num_kv_heads
)));
}
if params.kv_seq_len == 0 {
return Err(MlxError::InvalidArgument(
"flash_attn_vec_hybrid: kv_seq_len must be > 0".into(),
));
}
if params.kv_capacity < params.kv_seq_len {
return Err(MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid: kv_capacity ({}) < kv_seq_len ({})",
params.kv_capacity, params.kv_seq_len
)));
}
if !matches!(params.codebook_bits, 5 | 6 | 8) {
return Err(MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid: V codebook_bits must be 5, 6, or 8, got {}",
params.codebook_bits
)));
}
if params.nsg == 0 || (params.nsg & (params.nsg - 1)) != 0 {
return Err(MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid: nsg must be a power of 2 (1, 2, 4, ...), got {}",
params.nsg
)));
}
if params.nsg > 4 {
return Err(MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid: nsg must be ≤ 4 (kernel reduce cap), got {}",
params.nsg
)));
}
Ok(())
}
fn compute_nwg(kv_seq_len: u32) -> u32 {
use std::sync::atomic::{AtomicI32, Ordering};
static CACHED_HYBRID_NWG: AtomicI32 = AtomicI32::new(-1);
let mut v = CACHED_HYBRID_NWG.load(Ordering::Relaxed);
if v < 0 {
let parsed = std::env::var("HF2Q_HYBRID_NWG")
.ok()
.and_then(|s| s.parse::<u32>().ok())
.filter(|n| (1..=32).contains(n))
.unwrap_or(0);
CACHED_HYBRID_NWG.store(parsed as i32, Ordering::Relaxed);
v = parsed as i32;
}
if v > 0 {
return v as u32;
}
if kv_seq_len > 512 { 32 } else { 16 }
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attn_vec_hybrid(
encoder: &mut CommandEncoder,
registry: &mut KernelRegistry,
device: &MlxDevice,
q: &MlxBuffer,
k_f16: &MlxBuffer,
v_packed: &MlxBuffer,
v_norms: &MlxBuffer,
output: &MlxBuffer,
tmp: &MlxBuffer,
params: &FlashAttnVecTqHbParams,
) -> Result<()> {
validate_params(params)?;
if k_f16.dtype() != crate::DType::F16 {
return Err(MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid: k_f16 must be DType::F16, got {:?}",
k_f16.dtype()
)));
}
let head_dim = params.head_dim;
let nwg = compute_nwg(params.kv_seq_len);
let gpu_params = FlashAttnVecHybridParamsGpu {
n_heads: params.num_heads,
n_kv_heads: params.num_kv_heads,
head_dim: params.head_dim,
kv_seq_len: params.kv_seq_len,
kv_capacity: params.kv_capacity,
scale: params.scale,
mask_type: params.mask_type,
sliding_window: params.sliding_window,
softcap: params.softcap,
nwg,
ring_start: params.ring_start,
scale_factor_d512: params.scale_factor_d512,
codebook_bits: params.codebook_bits,
fuse_fwht_pre: params.fuse_fwht_pre,
nsg: params.nsg,
n_queries: 1,
};
let kernel_name = match head_dim {
256 => "flash_attn_vec_hybrid_dk256",
512 => "flash_attn_vec_hybrid_dk512",
_ => return Err(MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid: unsupported head_dim {head_dim}"
))),
};
let cbits_const = (params.codebook_bits as i32, 50usize);
let v_is_f16: i32 = match v_packed.dtype() {
crate::DType::F16 => 1,
_ => 0,
};
let pipeline = registry
.get_pipeline_with_constants(
kernel_name,
device.metal_device(),
&[],
&[(cbits_const.1, cbits_const.0), (51usize, v_is_f16)],
)?;
let pk = pad2(head_dim as usize, 128);
let pv = pad2(head_dim as usize, 128);
let sh = 4 * 32;
let nsg = params.nsg as usize;
let shmem_halfs = pk + nsg * (sh + 2 * pv);
let shmem_bytes = shmem_halfs * 2;
encoder.set_op_kind(CapturedOpKind::Sdpa);
let threadgroups = MTLSize::new(1, params.num_heads as u64, nwg as u64);
let threadgroup_size = MTLSize::new(32, params.nsg as u64, 1);
let dst_buf = if nwg == 1 { output } else { tmp };
encoder.encode_threadgroups_with_args_and_shared(
pipeline,
&[
(0, KernelArg::Bytes(as_bytes(&gpu_params))),
(1, KernelArg::Buffer(q)),
(2, KernelArg::Buffer(k_f16)), (3, KernelArg::Buffer(v_packed)),
(4, KernelArg::Buffer(v_norms)),
(5, KernelArg::Buffer(dst_buf)),
],
&[(0, shmem_bytes as u64)],
threadgroups,
threadgroup_size,
);
if nwg > 1 {
encoder.memory_barrier();
let reduce_params = FlashAttnVecReduceParamsGpu { nrows: params.num_heads };
let reduce_kernel = match head_dim {
256 => "flash_attn_vec_reduce_dk256",
512 => "flash_attn_vec_reduce_dk512",
_ => unreachable!(),
};
let reduce_pipeline = registry.get_pipeline(reduce_kernel, device.metal_device())?;
let reduce_tg = MTLSize::new(params.num_heads as u64, 1, 1);
let reduce_tg_size = MTLSize::new(32 * nwg as u64, 1, 1);
encoder.encode_threadgroups_with_args(
reduce_pipeline,
&[
(0, KernelArg::Bytes(as_bytes(&reduce_params))),
(1, KernelArg::Buffer(tmp)),
(2, KernelArg::Buffer(output)),
(3, KernelArg::Bytes(as_bytes(&nwg))),
],
reduce_tg,
reduce_tg_size,
);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attn_vec_hybrid_batched(
encoder: &mut CommandEncoder,
registry: &mut KernelRegistry,
device: &MlxDevice,
n_q: u32,
q: &MlxBuffer,
k_f16: &MlxBuffer,
v_packed: &MlxBuffer,
v_norms: &MlxBuffer,
output: &MlxBuffer,
tmp: &MlxBuffer,
slot_id_arr: &MlxBuffer,
seq_pos_arr: &MlxBuffer,
params: &FlashAttnVecTqHbParams,
) -> Result<()> {
validate_params(params)?;
if k_f16.dtype() != crate::DType::F16 {
return Err(MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid_batched: k_f16 must be DType::F16, got {:?}",
k_f16.dtype()
)));
}
let head_dim = params.head_dim;
let nwg = compute_nwg(params.kv_seq_len);
let gpu_params = FlashAttnVecHybridParamsGpu {
n_heads: params.num_heads,
n_kv_heads: params.num_kv_heads,
head_dim: params.head_dim,
kv_seq_len: params.kv_seq_len,
kv_capacity: params.kv_capacity,
scale: params.scale,
mask_type: params.mask_type,
sliding_window: params.sliding_window,
softcap: params.softcap,
nwg,
ring_start: params.ring_start,
scale_factor_d512: params.scale_factor_d512,
codebook_bits: params.codebook_bits,
fuse_fwht_pre: params.fuse_fwht_pre,
nsg: params.nsg,
n_queries: n_q,
};
let kernel_name = match head_dim {
256 => "flash_attn_vec_hybrid_batched_dk256",
512 => "flash_attn_vec_hybrid_batched_dk512",
_ => return Err(MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid_batched: unsupported head_dim {head_dim}"
))),
};
let cbits_const = (params.codebook_bits as i32, 50usize);
let v_is_f16: i32 = match v_packed.dtype() {
crate::DType::F16 => 1,
_ => 0,
};
let pipeline = registry.get_pipeline_with_constants(
kernel_name,
device.metal_device(),
&[],
&[(cbits_const.1, cbits_const.0), (51usize, v_is_f16)],
)?;
let pk = pad2(head_dim as usize, 128);
let pv = pad2(head_dim as usize, 128);
let sh = 4 * 32;
let nsg = params.nsg as usize;
let shmem_halfs = pk + nsg * (sh + 2 * pv);
let shmem_bytes = shmem_halfs * 2;
encoder.set_op_kind(CapturedOpKind::Sdpa);
let threadgroups = MTLSize::new(n_q as u64, params.num_heads as u64, nwg as u64);
let threadgroup_size = MTLSize::new(32, params.nsg as u64, 1);
let dst_buf = if nwg == 1 { output } else { tmp };
encoder.encode_threadgroups_with_args_and_shared(
pipeline,
&[
(0, KernelArg::Bytes(as_bytes(&gpu_params))),
(1, KernelArg::Buffer(q)),
(2, KernelArg::Buffer(k_f16)),
(3, KernelArg::Buffer(v_packed)),
(4, KernelArg::Buffer(v_norms)),
(5, KernelArg::Buffer(dst_buf)),
(6, KernelArg::Buffer(slot_id_arr)),
(7, KernelArg::Buffer(seq_pos_arr)),
],
&[(0, shmem_bytes as u64)],
threadgroups,
threadgroup_size,
);
if nwg > 1 {
encoder.memory_barrier();
let reduce_params = FlashAttnVecReduceParamsGpu { nrows: n_q * params.num_heads };
let reduce_kernel = match head_dim {
256 => "flash_attn_vec_reduce_dk256",
512 => "flash_attn_vec_reduce_dk512",
_ => unreachable!(),
};
let reduce_pipeline = registry.get_pipeline(reduce_kernel, device.metal_device())?;
let reduce_tg = MTLSize::new((n_q * params.num_heads) as u64, 1, 1);
let reduce_tg_size = MTLSize::new(32 * nwg as u64, 1, 1);
encoder.encode_threadgroups_with_args(
reduce_pipeline,
&[
(0, KernelArg::Bytes(as_bytes(&reduce_params))),
(1, KernelArg::Buffer(tmp)),
(2, KernelArg::Buffer(output)),
(3, KernelArg::Bytes(as_bytes(&nwg))),
],
reduce_tg,
reduce_tg_size,
);
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HybridSlotBucket {
pub nwg: u32,
pub nsg: u32,
}
impl HybridSlotBucket {
pub fn for_kv_seq_len(kv_seq_len: u32) -> Self {
Self {
nwg: compute_nwg(kv_seq_len),
nsg: compute_nsg(kv_seq_len),
}
}
}
pub const FLASH_ATTN_VEC_HYBRID_GQA_GROUPS: [u32; 4] = [1, 2, 4, 8];
pub const FLASH_ATTN_VEC_HYBRID_GQA_MAX_SHMEM_BYTES: usize = 32 * 1024;
pub fn flash_attn_vec_hybrid_batched_gqa_shmem_bytes(
head_dim: u32,
group: u32,
nsg_max: u32,
) -> usize {
let pk = pad2(head_dim as usize, 128);
let pv = pad2(head_dim as usize, 128);
let sh = 4 * 32;
group as usize * (pk + nsg_max as usize * (sh + 2 * pv)) * 2
}
pub fn flash_attn_vec_hybrid_batched_gqa_group_is_legal(
num_heads: u32,
num_kv_heads: u32,
head_dim: u32,
group: u32,
nsg_max: u32,
) -> bool {
if num_kv_heads == 0 || num_heads % num_kv_heads != 0 {
return false;
}
if head_dim == 512 && group != 1 {
return false;
}
let heads_per_kv = num_heads / num_kv_heads;
FLASH_ATTN_VEC_HYBRID_GQA_GROUPS.contains(&group)
&& heads_per_kv % group == 0
&& flash_attn_vec_hybrid_batched_gqa_shmem_bytes(head_dim, group, nsg_max)
<= FLASH_ATTN_VEC_HYBRID_GQA_MAX_SHMEM_BYTES
}
pub fn flash_attn_vec_hybrid_batched_gqa_pick_group(
num_heads: u32,
num_kv_heads: u32,
head_dim: u32,
nsg_max: u32,
) -> u32 {
FLASH_ATTN_VEC_HYBRID_GQA_GROUPS
.iter()
.rev()
.copied()
.find(|&g| {
flash_attn_vec_hybrid_batched_gqa_group_is_legal(
num_heads,
num_kv_heads,
head_dim,
g,
nsg_max,
)
})
.unwrap_or(1)
}
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct HybridSlotBucketGpu {
nwg: u32,
nsg: u32,
part_off: u32,
sm_off: u32,
}
struct HybridReduceRun {
slot0: usize,
slots: usize,
nwg: u32,
tmp_off_floats: usize,
tmp_floats: usize,
}
const GQA_MAX_SLOTS: usize = 4096 / std::mem::size_of::<HybridSlotBucketGpu>();
fn plan_gqa_layout(
num_heads: u32,
head_dim: u32,
buckets: &[HybridSlotBucket],
) -> Result<(Vec<HybridSlotBucketGpu>, Vec<HybridReduceRun>, usize)> {
let rows = num_heads as usize;
let dv = head_dim as usize;
let mut gpu = Vec::with_capacity(buckets.len());
let mut runs = Vec::new();
let mut off = 0usize;
let to_u32 = |v: usize| {
u32::try_from(v).map_err(|_| {
MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid_batched_gqa: tmp offset {v} exceeds u32"
))
})
};
let mut s = 0usize;
while s < buckets.len() {
let nwg = buckets[s].nwg;
let mut e = s + 1;
while e < buckets.len() && buckets[e].nwg == nwg {
e += 1;
}
if nwg == 1 {
for b in &buckets[s..e] {
gpu.push(HybridSlotBucketGpu { nwg: 1, nsg: b.nsg, part_off: 0, sm_off: 0 });
}
} else {
off = pad2(off, 4);
let w = nwg as usize;
let slots = e - s;
let part_floats = slots * rows * dv * w;
let sm_floats = slots * rows * 2 * w;
for (k, b) in buckets[s..e].iter().enumerate() {
gpu.push(HybridSlotBucketGpu {
nwg,
nsg: b.nsg,
part_off: to_u32(off + k * rows * dv * w)?,
sm_off: to_u32(off + part_floats + k * rows * 2 * w)?,
});
}
runs.push(HybridReduceRun {
slot0: s,
slots,
nwg,
tmp_off_floats: off,
tmp_floats: part_floats + sm_floats,
});
off += part_floats + sm_floats;
to_u32(off)?;
}
s = e;
}
Ok((gpu, runs, off))
}
pub fn flash_attn_vec_hybrid_batched_gqa_tmp_bytes(
num_heads: u32,
head_dim: u32,
buckets: &[HybridSlotBucket],
) -> Result<usize> {
let (_, _, floats) = plan_gqa_layout(num_heads, head_dim, buckets)?;
Ok(floats * std::mem::size_of::<f32>())
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attn_vec_hybrid_batched_gqa(
encoder: &mut CommandEncoder,
registry: &mut KernelRegistry,
device: &MlxDevice,
q: &MlxBuffer,
k_f16: &MlxBuffer,
v_packed: &MlxBuffer,
v_norms: &MlxBuffer,
output: &MlxBuffer,
tmp: &MlxBuffer,
slot_id_arr: &MlxBuffer,
seq_pos_arr: &MlxBuffer,
params: &FlashAttnVecTqHbParams,
group: u32,
buckets: &[HybridSlotBucket],
) -> Result<()> {
validate_params(params)?;
if k_f16.dtype() != crate::DType::F16 {
return Err(MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid_batched_gqa: k_f16 must be DType::F16, got {:?}",
k_f16.dtype()
)));
}
if buckets.is_empty() || buckets.len() > GQA_MAX_SLOTS {
return Err(MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid_batched_gqa: need 1..={GQA_MAX_SLOTS} slot buckets, got {}",
buckets.len()
)));
}
for (i, b) in buckets.iter().enumerate() {
if !(1..=32).contains(&b.nwg) || !matches!(b.nsg, 1 | 2 | 4) {
return Err(MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid_batched_gqa: slot {i} bucket {b:?} outside nwg 1..=32, nsg {{1,2,4}}"
)));
}
}
let n_q = buckets.len() as u32;
let nwg_max = buckets.iter().map(|b| b.nwg).max().unwrap_or(1);
let nsg_max = buckets.iter().map(|b| b.nsg).max().unwrap_or(1);
let head_dim = params.head_dim;
if head_dim == 512 && group != 1 {
return Err(MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid_batched_gqa: head_dim 512 requires group 1, got {group} \
(grouped d512 V scale is not bitwise-stable across Metal toolchains)"
)));
}
if !flash_attn_vec_hybrid_batched_gqa_group_is_legal(
params.num_heads,
params.num_kv_heads,
head_dim,
group,
nsg_max,
) {
return Err(MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid_batched_gqa: group {group} illegal for heads {}/{} head_dim {head_dim} nsg_max {nsg_max}",
params.num_heads, params.num_kv_heads
)));
}
if output.dtype() != crate::DType::F32 || tmp.dtype() != crate::DType::F32 {
return Err(MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid_batched_gqa: output/tmp must be F32, got {:?}/{:?}",
output.dtype(),
tmp.dtype()
)));
}
let out_bytes = n_q as usize * params.num_heads as usize * head_dim as usize * 4;
if output.data_byte_len() < out_bytes {
return Err(MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid_batched_gqa: output holds {} bytes, need {out_bytes}",
output.data_byte_len()
)));
}
let (gpu_buckets, runs, tmp_floats) = plan_gqa_layout(params.num_heads, head_dim, buckets)?;
if tmp.data_byte_len() < tmp_floats * 4 {
return Err(MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid_batched_gqa: tmp holds {} bytes, need {}",
tmp.data_byte_len(),
tmp_floats * 4
)));
}
let gpu_params = FlashAttnVecHybridParamsGpu {
n_heads: params.num_heads,
n_kv_heads: params.num_kv_heads,
head_dim: params.head_dim,
kv_seq_len: params.kv_seq_len,
kv_capacity: params.kv_capacity,
scale: params.scale,
mask_type: params.mask_type,
sliding_window: params.sliding_window,
softcap: params.softcap,
nwg: nwg_max,
ring_start: params.ring_start,
scale_factor_d512: params.scale_factor_d512,
codebook_bits: params.codebook_bits,
fuse_fwht_pre: params.fuse_fwht_pre,
nsg: nsg_max,
n_queries: n_q,
};
let kernel_name = match (group, head_dim) {
(1, 256) => "flash_attn_vec_hybrid_batched_gqa1_dk256",
(2, 256) => "flash_attn_vec_hybrid_batched_gqa2_dk256",
(4, 256) => "flash_attn_vec_hybrid_batched_gqa4_dk256",
(8, 256) => "flash_attn_vec_hybrid_batched_gqa8_dk256",
(1, 512) => "flash_attn_vec_hybrid_batched_gqa1_dk512",
_ => {
return Err(MlxError::InvalidArgument(format!(
"flash_attn_vec_hybrid_batched_gqa: unsupported group {group} / head_dim {head_dim}"
)))
}
};
let cbits_const = (params.codebook_bits as i32, 50usize);
let v_is_f16: i32 = match v_packed.dtype() {
crate::DType::F16 => 1,
_ => 0,
};
let pipeline = registry.get_pipeline_with_constants(
kernel_name,
device.metal_device(),
&[],
&[(cbits_const.1, cbits_const.0), (51usize, v_is_f16)],
)?;
let shmem_bytes = flash_attn_vec_hybrid_batched_gqa_shmem_bytes(head_dim, group, nsg_max);
encoder.set_op_kind(CapturedOpKind::Sdpa);
let threadgroups = MTLSize::new(
n_q as u64,
(params.num_heads / group) as u64,
nwg_max as u64,
);
let threadgroup_size = MTLSize::new(32, nsg_max as u64, 1);
encoder.encode_threadgroups_with_args_and_shared(
pipeline,
&[
(0, KernelArg::Bytes(as_bytes(&gpu_params))),
(1, KernelArg::Buffer(q)),
(2, KernelArg::Buffer(k_f16)),
(3, KernelArg::Buffer(v_packed)),
(4, KernelArg::Buffer(v_norms)),
(5, KernelArg::Buffer(tmp)),
(6, KernelArg::Buffer(slot_id_arr)),
(7, KernelArg::Buffer(seq_pos_arr)),
(8, KernelArg::Bytes(bytemuck::cast_slice(&gpu_buckets))),
(9, KernelArg::Buffer(output)),
],
&[(0, shmem_bytes as u64)],
threadgroups,
threadgroup_size,
);
if runs.is_empty() {
return Ok(());
}
encoder.memory_barrier();
let reduce_kernel = match head_dim {
256 => "flash_attn_vec_reduce_dk256",
512 => "flash_attn_vec_reduce_dk512",
_ => unreachable!(),
};
let row_floats = params.num_heads as usize * head_dim as usize;
for run in &runs {
let reduce_pipeline = registry.get_pipeline(reduce_kernel, device.metal_device())?;
let nrows = run.slots as u32 * params.num_heads;
let reduce_params = FlashAttnVecReduceParamsGpu { nrows };
let tmp_view = tmp.slice_view((run.tmp_off_floats * 4) as u64, run.tmp_floats);
let out_view = output.slice_view(
(run.slot0 * row_floats * 4) as u64,
run.slots * row_floats,
);
let nwg = run.nwg;
encoder.encode_threadgroups_with_args(
reduce_pipeline,
&[
(0, KernelArg::Bytes(as_bytes(&reduce_params))),
(1, KernelArg::Buffer(&tmp_view)),
(2, KernelArg::Buffer(&out_view)),
(3, KernelArg::Bytes(as_bytes(&nwg))),
],
MTLSize::new(nrows as u64, 1, 1),
MTLSize::new(32 * nwg as u64, 1, 1),
);
}
Ok(())
}
fn pad2(x: usize, n: usize) -> usize {
(x + n - 1) & !(n - 1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gpu_params_size() {
assert_eq!(std::mem::size_of::<FlashAttnVecHybridParamsGpu>(), 64);
}
#[test]
fn test_validate_bad_bits() {
let p = FlashAttnVecTqHbParams {
num_heads: 8,
num_kv_heads: 4,
head_dim: 256,
kv_seq_len: 64,
kv_capacity: 1024,
scale: 1.0,
mask_type: 0,
sliding_window: 0,
softcap: 0.0,
ring_start: 0,
scale_factor_d512: 1.0,
codebook_bits: 4, fuse_fwht_pre: 0,
nsg: 1,
};
assert!(validate_params(&p).is_err());
}
#[test]
fn test_validate_ok_8bit() {
let p = FlashAttnVecTqHbParams {
num_heads: 8,
num_kv_heads: 4,
head_dim: 256,
kv_seq_len: 64,
kv_capacity: 1024,
scale: 1.0,
mask_type: 0,
sliding_window: 0,
softcap: 0.0,
ring_start: 0,
scale_factor_d512: 1.0,
codebook_bits: 8,
fuse_fwht_pre: 0,
nsg: 1,
};
assert!(validate_params(&p).is_ok());
}
#[test]
fn test_validate_bad_head_dim() {
let p = FlashAttnVecTqHbParams {
num_heads: 8,
num_kv_heads: 4,
head_dim: 128, kv_seq_len: 64,
kv_capacity: 1024,
scale: 1.0,
mask_type: 0,
sliding_window: 0,
softcap: 0.0,
ring_start: 0,
scale_factor_d512: 1.0,
codebook_bits: 8,
fuse_fwht_pre: 0,
nsg: 1,
};
assert!(validate_params(&p).is_err());
}
}