use model_io::{ArchConfig, ResidentIndex};
use crate::families::spark::layer_tensor;
use crate::families::spark::state::RealSparkState;
use crate::kv_write::{encode_attention_any, encode_kv_commit, kv_write_target, KvHalf};
use crate::real_forward_dispatch::encode_gemv_any;
use crate::real_forward_types::{DecodeScratch, RealForwardError};
#[allow(clippy::too_many_arguments)]
pub(crate) fn encode_attention_block(
context: &mut gpu::MetalContext,
pass: &gpu::PassEncoder,
weights: &gpu::ResidentGpuWeights,
index: &ResidentIndex,
arch: &ArchConfig,
spark: &RealSparkState,
scratch: &DecodeScratch,
kv: &gpu::KvCacheManager,
layer: usize,
position: usize,
) -> Result<(), RealForwardError> {
let gpu_err = RealForwardError::Gpu;
let hidden = arch.hidden_size as usize;
let num_heads = arch.num_heads as u32;
let num_kv = arch.num_kv_heads as u32;
let head_dim = arch.full_head_dim as u32;
let q_dim = (num_heads * head_dim) as usize;
let kv_dim = (num_kv * head_dim) as usize;
let name = |suffix: &str| layer_tensor(layer, &format!("self_attn.{suffix}"));
let (k_buf, k_off) = kv_write_target(kv, scratch, KvHalf::K, layer, position);
let (v_buf, v_off) = kv_write_target(kv, scratch, KvHalf::V, layer, position);
encode_gemv_any(
context,
pass,
weights,
index,
&name("q_k_v_proj.weight"),
q_dim + 2 * kv_dim,
hidden,
(&scratch.normed, 0),
(&spark.qkv, 0),
)?;
encode_gemv_any(
context,
pass,
weights,
index,
&name("g_proj.weight"),
num_heads as usize,
hidden,
(&scratch.normed, 0),
(&spark.attn_gate, 0),
)?;
gpu::encode_split_qkv(
context,
pass,
(&spark.qkv, 0),
(&scratch.q, 0),
(k_buf, k_off),
(v_buf, v_off),
q_dim as u32,
kv_dim as u32,
)
.map_err(gpu_err)?;
let is_full = arch
.full_attention_layer_mask
.get(layer)
.copied()
.unwrap_or(1)
== 1;
let (rotary_dim, theta) = rope_params_for(
is_full,
spark.full_rotary_dim,
head_dim,
arch.full_rope_theta as f32,
arch.rope_theta as f32,
);
for (data, heads) in [((&scratch.q, 0u64), num_heads), ((k_buf, k_off), num_kv)] {
gpu::encode_rope_neox_subdim(
context,
pass,
data,
position as u32,
heads,
head_dim,
rotary_dim,
theta,
)
.map_err(gpu_err)?;
}
let seq_len = (position + 1) as u32;
let (kv_start, active_ring) = if is_full {
(0, 0)
} else {
let ring = kv.ring_capacity(layer) as u32;
(
seq_len.saturating_sub(arch.sliding_window as u32),
if ring > 0 && seq_len > ring { ring } else { 0 },
)
};
encode_kv_commit(
context, pass, kv, scratch, layer, position, head_dim, num_kv,
)?;
encode_attention_any(
context,
pass,
(&scratch.q, 0),
kv,
scratch,
(&scratch.attn_out, 0),
layer,
position,
head_dim,
num_heads,
num_kv,
seq_len,
kv_start,
active_ring,
arch.attention_scale as f32,
None,
)?;
gpu::encode_sigmoid_head_gate_mul(
context,
pass,
(&scratch.attn_out, 0),
(&spark.attn_gate, 0),
head_dim,
q_dim as u32,
)
.map_err(gpu_err)?;
encode_gemv_any(
context,
pass,
weights,
index,
&name("o_proj.weight"),
hidden,
q_dim,
(&scratch.attn_out, 0),
(&scratch.o, 0),
)
}
pub(crate) fn rope_params_for(
is_full: bool,
full_rotary_dim: u32,
head_dim: u32,
full_theta: f32,
sliding_theta: f32,
) -> (u32, f32) {
if is_full {
(full_rotary_dim, full_theta)
} else {
(head_dim, sliding_theta)
}
}
#[cfg(test)]
mod tests {
use super::rope_params_for;
#[test]
fn the_rope_selection_is_per_layer_class() {
let (dim, theta) = rope_params_for(true, 64, 256, 5_000_000.0, 10_000.0);
assert_eq!(dim, 64);
assert_eq!(theta, 5_000_000.0);
let (dim, theta) = rope_params_for(false, 64, 256, 5_000_000.0, 10_000.0);
assert_eq!(dim, 256);
assert_eq!(theta, 10_000.0);
}
}