use super::*;
pub(crate) struct Q1gSg {
pub(crate) mv: wgpu::ComputePipeline,
pub(crate) layout: wgpu::BindGroupLayout,
}
pub(crate) const ROWS_PER_WG: u32 = 16;
const SRC: &str = r#"
// np = tile pairs per row (cols/64); sbase = word offset of the f16 scale
// plane (behind the sign plane of ALL rows of the tensor).
struct P { np: u32, rows: u32, sbase: u32, _p: u32 };
@group(0) @binding(0) var<storage, read> gw : array<u32>;
@group(0) @binding(1) var<storage, read> gx : array<vec4<f32>>;
@group(0) @binding(2) var<storage, read_write> gy : array<f32>;
@group(0) @binding(3) var<uniform> gp : P;
// q1_tile_sum with the activation read as eight vec4 from the input:
// the same values, the same vec4 accumulation, the same final sum.
fn tile_sum(bits: u32, x4: u32) -> f32 {
var s = vec4<f32>(0.0);
for (var j = 0u; j < 8u; j = j + 1u) {
let nib = bits >> (j * 4u);
let x = gx[x4 + j];
let m = vec4<u32>(
((nib & 1u) ^ 1u) << 31u,
(((nib >> 1u) & 1u) ^ 1u) << 31u,
(((nib >> 2u) & 1u) ^ 1u) << 31u,
(((nib >> 3u) & 1u) ^ 1u) << 31u);
s = s + bitcast<vec4<f32>>(bitcast<vec4<u32>>(x) ^ m);
}
return s.x + s.y + s.z + s.w;
}
@compute @workgroup_size(256)
fn q1g_mv_sg(@builtin(workgroup_id) wid: vec3<u32>,
@builtin(num_workgroups) nwg: vec3<u32>,
@builtin(local_invocation_index) lid: u32,
@builtin(subgroup_invocation_id) sl: u32) {
let gpr = gp.np * 2u;
let half = sl >> 4u;
let lane = sl & 15u;
// Every loop bound below is workgroup-uniform (WGSL's uniformity rules
// for the shuffles): rows and pairs a lane does not own are masked by
// `if`, never by an early exit, exactly as q1_matvec masks them.
var blk = wid.x;
loop {
if (blk * 16u >= gp.rows) { break; }
let row = blk * 16u + (lid >> 5u) * 2u + half;
let live = row < gp.rows;
var acc = 0.0;
for (var ti = 0u; ti < gp.np; ti = ti + 16u) {
let pi = ti + lane;
if (live && pi < gp.np) {
let base = row * gpr + pi * 2u;
let b0 = gw[base]; let b1 = gw[base + 1u];
let g = row * (gpr >> 2u) + (pi >> 1u);
let sw = unpack2x16float(gw[gp.sbase + (g >> 1u)]);
let s = select(sw.x, sw.y, (g & 1u) == 1u);
let x4 = pi * 16u;
acc = acc + s * tile_sum(b0, x4) + s * tile_sum(b1, x4 + 8u);
}
}
var v = acc;
v = v + subgroupShuffleDown(v, 8u);
v = v + subgroupShuffleDown(v, 4u);
v = v + subgroupShuffleDown(v, 2u);
v = v + subgroupShuffleDown(v, 1u);
if (lane == 0u && live) { gy[row] = v; }
blk = blk + nwg.x;
}
}
"#;
#[cfg(test)]
pub(crate) fn module_error(c: &Ctx) -> Option<String> {
if !sg32(c) {
return None;
}
let scope = c.device.push_error_scope(wgpu::ErrorFilter::Validation);
let module = c.device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("cmf-q1g-sg-test"),
source: wgpu::ShaderSource::Wgsl(SRC.into()),
});
let _ = c
.device
.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("q1g_mv_sg-test"),
layout: None,
module: &module,
entry_point: Some("q1g_mv_sg"),
compilation_options: crate::gpu_wgpu::pco(),
cache: None,
});
pollster::block_on(scope.pop()).map(|e| format!("{e:?}"))
}
pub(crate) fn sg32(c: &Ctx) -> bool {
c.device.features().contains(wgpu::Features::SUBGROUP)
&& c.adapter_info.subgroup_min_size == 32
&& c.adapter_info.subgroup_max_size == 32
}
pub(crate) fn wanted(cols: usize) -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ON.get_or_init(|| std::env::var("CMF_Q1G_SG").as_deref() == Ok("1")) && cols >= 4096
}
fn build(c: &Ctx) -> Option<Q1gSg> {
if !sg32(c) {
return None;
}
let scope = c.device.push_error_scope(wgpu::ErrorFilter::Validation);
let module = c.device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("cmf-q1g-sg"),
source: wgpu::ShaderSource::Wgsl(SRC.into()),
});
let mv = c
.device
.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("q1g_mv_sg"),
layout: None,
module: &module,
entry_point: Some("q1g_mv_sg"),
compilation_options: crate::gpu_wgpu::pco(),
cache: c.pipeline_cache.as_ref(),
});
if let Some(e) = pollster::block_on(scope.pop()) {
tracing::warn!("cmf-q1g-sg module rejected ({e}): q1g keeps the staged kernel");
return None;
}
Some(Q1gSg {
layout: mv.get_bind_group_layout(0),
mv,
})
}
pub(crate) fn pipes(c: &Ctx) -> Option<&Q1gSg> {
c.q1g_sg_pipes.get_or_init(|| build(c)).as_ref()
}