use super::*;
pub(crate) fn device_ok(c: &Ctx) -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ON.get_or_init(|| std::env::var("CMF_GPU_KV16").as_deref() != Ok("0"))
&& c.device.features().contains(wgpu::Features::SHADER_F16)
&& c.device.features().contains(wgpu::Features::SUBGROUP)
&& c.adapter_info.subgroup_min_size == 32
&& c.adapter_info.subgroup_max_size == 32
}
fn notes() -> &'static std::sync::Mutex<HashMap<u64, bool>> {
static N: std::sync::OnceLock<std::sync::Mutex<HashMap<u64, bool>>> = std::sync::OnceLock::new();
N.get_or_init(|| std::sync::Mutex::new(HashMap::new()))
}
pub fn note(kv_id: u64, allowed: bool) {
let mut n = notes().lock().unwrap();
if !n.contains_key(&kv_id) && std::env::var("CMF_GPU_DEBUG").is_ok() {
eprintln!("kv16: sequence {kv_id} allowed={allowed}");
}
n.entry(kv_id).or_insert(allowed);
}
pub(crate) fn forget(kv_id: u64) {
notes().lock().unwrap().remove(&kv_id);
}
pub(crate) fn wanted(c: &Ctx, kv_id: u64) -> bool {
device_ok(c)
&& notes().lock().unwrap().get(&kv_id).copied().unwrap_or(false)
&& pipes(c).is_some()
&& super::dense_batch::attn_for(c, true).is_some()
}
pub(crate) fn half_src(src: &str, names: &[&str]) -> String {
let mut vec4 = HashMap::new();
let mut lines = Vec::new();
for line in src.lines() {
let mut l = line.to_string();
if l.contains("var<storage") {
for &n in names {
let at = l.find(&format!(" {n} ")).or_else(|| l.find(&format!(" {n}:")));
if let Some(p) = at.filter(|&p| l[p..].contains("array<")) {
let tail = l[p..].to_string();
let (t, v4) = if tail.contains("array<vec4<f32>>") {
(tail.replacen("array<vec4<f32>>", "array<vec4<f16>>", 1), true)
} else if tail.contains("array<f32>") {
(tail.replacen("array<f32>", "array<f16>", 1), false)
} else {
panic!("half_src: {n} is not an f32 or vec4<f32> array");
};
l = format!("{}{}", &l[..p], t);
vec4.insert(n, v4);
}
}
}
lines.push(l);
}
for &n in names {
assert!(vec4.contains_key(n), "half_src: {n} is not declared");
}
let mut out = String::from("enable f16;\n");
out.push_str(&rewrite(&lines.join("\n"), names, &vec4));
out.push('\n');
out
}
fn rewrite(text: &str, names: &[&str], vec4: &HashMap<&str, bool>) -> String {
let ident = |ch: char| ch.is_ascii_alphanumeric() || ch == '_';
let b = text.as_bytes();
let mut out = String::with_capacity(text.len() + 64);
let mut i = 0;
while i < text.len() {
if text[i..].starts_with("//") {
let end = text[i..].find('\n').map_or(text.len(), |e| i + e);
out.push_str(&text[i..end]);
i = end;
continue;
}
let hit = names.iter().find(|&&n| {
text[i..].starts_with(n)
&& text[i + n.len()..].starts_with('[')
&& (i == 0 || !ident(text[..i].chars().next_back().unwrap()))
});
let Some(&n) = hit else {
let ch = text[i..].chars().next().unwrap();
out.push(ch);
i += ch.len_utf8();
continue;
};
let open = i + n.len();
let mut depth = 0usize;
let mut close = open;
for (j, &c) in b.iter().enumerate().skip(open) {
match c {
b'[' => depth += 1,
b']' => {
depth -= 1;
if depth == 0 {
close = j;
break;
}
}
_ => {}
}
}
let access = format!("{n}[{}]", rewrite(&text[open + 1..close], names, vec4));
let rest = &text[close + 1..];
let trimmed = rest.trim_start();
let (t32, t16) = if vec4[n] { ("vec4<f32>", "vec4<f16>") } else { ("f32", "f16") };
if trimmed.starts_with('=') && !trimmed.starts_with("==") {
let eq = close + 1 + (rest.len() - trimmed.len());
let semi = eq + text[eq..].find(';').expect("half_src: store without ';'");
out.push_str(&access);
out.push_str(" = ");
out.push_str(t16);
out.push('(');
out.push_str(&rewrite(text[eq + 1..semi].trim(), names, vec4));
out.push_str(");");
i = semi + 1;
} else {
out.push_str(t32);
out.push('(');
out.push_str(&access);
out.push(')');
i = close + 1;
}
}
out
}
fn cut<'a>(src: &'a str, from: &str, to: &str) -> &'a str {
let a = src.find(from).unwrap_or_else(|| panic!("kv16: marker {from:?} missing"));
let b = a + src[a..].find(to).unwrap_or_else(|| panic!("kv16: marker {to:?} missing"));
&src[a..b]
}
pub(crate) fn kv_append_src() -> String {
half_src(
cut(
super::WGSL,
"struct KvP { nkv: u32, hd: u32, cap: u32, stored: u32 };",
"// Grouped decode attention, one 32-thread workgroup per Q-head.",
),
&["kv_kb", "kv_vb"],
)
}
pub(crate) fn attend_dec_src() -> String {
half_src(
cut(
super::WGSL,
"struct AtP { nh: u32, hpk: u32, hd: u32, cap: u32, n: u32, scale: f32, _b: u32, _c: u32 };",
"// `gqa_attend_dec` with RoPE and the KV append folded in",
),
&["at_k", "at_v"],
)
}
pub(crate) struct Pipes {
pub(crate) kv_append: wgpu::ComputePipeline,
pub(crate) attend_dec: wgpu::ComputePipeline,
}
fn compile(c: &Ctx, label: &str, src: &str, entry: &str) -> Option<wgpu::ComputePipeline> {
let scope = c.device.push_error_scope(wgpu::ErrorFilter::Validation);
let module = c.device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(label),
source: wgpu::ShaderSource::Wgsl(src.into()),
});
let p = c.device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some(entry),
layout: None,
module: &module,
entry_point: Some(entry),
compilation_options: crate::gpu_wgpu::pco(),
cache: c.pipeline_cache.as_ref(),
});
if let Some(e) = pollster::block_on(scope.pop()) {
tracing::warn!("{label} rejected ({e}): K/V mirrors stay f32");
return None;
}
Some(p)
}
pub(crate) fn pipes(c: &Ctx) -> Option<&'static Pipes> {
static P: std::sync::OnceLock<Option<Pipes>> = std::sync::OnceLock::new();
P.get_or_init(|| {
Some(Pipes {
kv_append: compile(c, "cmf-kv16-append", &kv_append_src(), "kv_append")?,
attend_dec: compile(c, "cmf-kv16-dec", &attend_dec_src(), "gqa_attend_dec")?,
})
})
.as_ref()
}
pub(crate) fn to_half_bytes(rows: &[f32]) -> Vec<u8> {
let mut b = Vec::with_capacity(rows.len() * 2);
for &v in rows {
b.extend_from_slice(&cortiq_core::quant::f32_to_f16(v).to_le_bytes());
}
b
}
pub(crate) fn from_mirror_bytes(bytes: &[u8], half: bool) -> Vec<f32> {
if half {
bytes
.chunks_exact(2)
.map(|h| cortiq_core::quant::f16_to_f32(u16::from_le_bytes([h[0], h[1]])))
.collect()
} else {
bytemuck::cast_slice::<u8, f32>(bytes).to_vec()
}
}
pub(crate) fn write_rows(c: &Ctx, buf: &wgpu::Buffer, elem: usize, rows: &[f32], half: bool) {
if half {
c.queue.write_buffer(buf, (elem * 2) as u64, &to_half_bytes(rows));
} else {
c.queue.write_buffer(buf, (elem * 4) as u64, bytemuck::cast_slice(rows));
}
}
#[cfg(test)]
mod tests {
use super::*;
fn validate(src: &str) {
let module = wgpu::naga::front::wgsl::parse_str(src).unwrap_or_else(|e| panic!("{e}\n{src}"));
wgpu::naga::valid::Validator::new(
wgpu::naga::valid::ValidationFlags::all(),
wgpu::naga::valid::Capabilities::all(),
)
.validate(&module)
.unwrap_or_else(|e| panic!("{e:?}"));
}
#[test]
fn zi_mm_q4_validates() {
use super::super::zimage::{default_cfg, mm_src, Epi, MmCfg};
let g = MmCfg { q4: true, dscale: true, ..default_cfg(Epi::F32) };
assert!(g.valid());
let src = mm_src(g);
assert!(src.contains("fn qcode"));
let module = wgpu::naga::front::wgsl::parse_str(&src).unwrap_or_else(|e| panic!("{e}"));
wgpu::naga::valid::Validator::new(
wgpu::naga::valid::ValidationFlags::all(),
wgpu::naga::valid::Capabilities::all(),
)
.validate(&module)
.unwrap_or_else(|e| panic!("{e:?}"));
}
#[test]
fn half_src_rewrites_reads_and_stores() {
let src = "@group(0) @binding(0) var<storage, read_write> kb : array<f32>;\n\
@group(0) @binding(1) var<storage, read> kv : array<vec4<f32>>;\n\
@group(0) @binding(2) var<storage, read_write> out : array<f32>;\n\
@compute @workgroup_size(1)\n\
fn main() {\n kb[1u] = out[0u] * 2.0;\n out[1u] = kv[kb[0u] == 0.0 && true][2u] + out[2u];\n kb[0u] = 0.0;\n}\n";
let h = half_src(src, &["kb", "kv"]);
assert!(h.contains("kb : array<f16>"), "{h}");
assert!(h.contains("kv : array<vec4<f16>>"), "{h}");
assert!(h.contains("kb[1u] = f16(out[0u] * 2.0);"), "{h}");
assert!(h.contains("vec4<f32>(kv[f32(kb[0u]) == 0.0 && true])[2u]"), "{h}");
assert!(h.contains("kb[0u] = f16(0.0);"), "{h}");
assert!(!h.contains("out : array<f16>"), "{h}");
}
#[test]
fn twins_validate() {
validate(&kv_append_src());
validate(&attend_dec_src());
assert!(attend_dec_src().contains("at_k : array<vec4<f16>>"));
validate(&super::super::attend_fd::src_for_test(4, 256, true));
validate(&half_src(super::super::dense_batch::ATTN_BT_WGSL, super::super::dense_batch::HALF_NAMES));
}
}