use super::reader::{GraftBank, GraftKind, GraftMode};
#[derive(Debug, Clone)]
pub struct BoundGraft {
pub bank: GraftBank,
pub identity_hash: u64,
}
impl BoundGraft {
pub fn bind(bank: GraftBank) -> Self {
let identity_hash = graft_params_hash(Some(&bank));
Self { bank, identity_hash }
}
pub fn n_slots(&self) -> u32 {
self.bank.n_slots
}
}
pub fn graft_params_hash(graft: Option<&GraftBank>) -> u64 {
use sha2::Digest;
let mut h = sha2::Sha256::new();
match graft {
None => h.update(b"graft=none"),
Some(bank) => {
h.update(b"graft=v1");
h.update(bank.hook_point.as_str().as_bytes());
h.update(graft_mode_str(bank.mode).as_bytes());
h.update(graft_kind_str(bank.kind).as_bytes());
h.update(&bank.n_slots.to_le_bytes());
h.update(&bank.rope_theta.to_le_bytes());
h.update(&bank.rotary_dim.to_le_bytes());
h.update(&bank.position_base.to_le_bytes());
h.update([bank.mrope_interleaved as u8]);
match &bank.compress_ratios {
None => h.update(&0u8.to_le_bytes()),
Some(ratios) => {
h.update(&1u8.to_le_bytes());
h.update(&(ratios.len() as u32).to_le_bytes());
for ratio in ratios {
h.update(&ratio.to_le_bytes());
}
}
}
for (layer, kv) in &bank.layers {
h.update(&layer.to_le_bytes());
for value in kv.k.iter().chain(kv.v.iter()) {
h.update(&value.to_le_bytes());
}
}
}
}
let digest = h.finalize();
u64::from_le_bytes(digest[..8].try_into().expect("sha256 prefix"))
}
fn graft_mode_str(mode: GraftMode) -> &'static str {
match mode {
GraftMode::SplicePrefix => "splice_prefix",
}
}
fn graft_kind_str(kind: GraftKind) -> &'static str {
match kind {
GraftKind::PrefillKv => "prefill_kv",
GraftKind::SoftPromptKv => "softprompt_kv",
GraftKind::DirectKv => "direct_kv",
}
}
#[cfg(test)]
pub(crate) fn test_bank(
n_slots: u32,
heads: usize,
head_dim: usize,
layers: &[u32],
) -> GraftBank {
use super::reader::{GraftHookPoint, GraftLayerKv};
use std::collections::BTreeMap;
GraftBank {
mode: GraftMode::SplicePrefix,
kind: GraftKind::DirectKv,
hook_point: GraftHookPoint::FullAttnKv,
n_slots,
layers: layers
.iter()
.map(|&l| {
let n = n_slots as usize * heads * head_dim;
(
l,
GraftLayerKv {
k: vec![0.25; n],
v: vec![-0.5; n],
},
)
})
.collect::<BTreeMap<u32, GraftLayerKv>>(),
n_kv_heads: heads,
head_dim,
rope_theta: 1e7,
rotary_dim: 64,
position_base: 0,
mrope_interleaved: true,
compress_ratios: None,
content_sha256: None,
quant_lane: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn graft_identity_separates_configurations() {
let a = test_bank(3, 2, 8, &[3, 7]);
let a2 = test_bank(3, 2, 8, &[3, 7]);
assert_eq!(
graft_params_hash(Some(&a)),
graft_params_hash(Some(&a2)),
"identical banks must share the identity key"
);
let mut other = test_bank(3, 2, 8, &[3, 7]);
other.layers.get_mut(&3).unwrap().k[0] = 0.5;
assert_ne!(
graft_params_hash(Some(&a)),
graft_params_hash(Some(&other)),
"content change must rebuild, not reuse"
);
let longer = test_bank(4, 2, 8, &[3, 7]);
assert_ne!(graft_params_hash(Some(&a)), graft_params_hash(Some(&longer)));
let wider = test_bank(3, 2, 8, &[3, 7, 11]);
assert_ne!(graft_params_hash(Some(&a)), graft_params_hash(Some(&wider)));
let mut rope = test_bank(3, 2, 8, &[3, 7]);
rope.rope_theta = 1e6;
assert_ne!(graft_params_hash(Some(&a)), graft_params_hash(Some(&rope)));
let none = graft_params_hash(None);
assert_eq!(none, graft_params_hash(None));
assert_ne!(none, graft_params_hash(Some(&a)));
}
#[test]
fn bound_graft_carries_bank_and_hash() {
let bank = test_bank(2, 2, 8, &[3, 7]);
let bound = BoundGraft::bind(bank.clone());
assert_eq!(bound.n_slots(), 2);
assert_eq!(bound.identity_hash, graft_params_hash(Some(&bank)));
assert_eq!(bound.bank.layers.len(), 2);
}
}