use std::path::Path;
use serde::Serialize;
use crate::error::{refuse_overwrite, CliError, Result};
pub(crate) use crate::{KernelImpl, KernelRef};
pub(crate) const FLASH2_SUPPORTED_HEAD_DIMS: [usize; 2] = [64, 128];
#[derive(Debug, Clone, PartialEq, Serialize)]
pub(crate) struct ParityObservation {
pub kernel: String,
pub reference: String,
pub seq_len: usize,
pub num_heads: usize,
pub num_kv_heads: usize,
pub head_dim: usize,
pub seed: u64,
pub regime: String,
pub max_abs_diff: f64,
pub cosine_sim: f64,
pub attn_impl: String,
pub kernel_source: Option<String>,
pub fallback: Option<String>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ParityDims {
pub seq_len: usize,
pub num_heads: usize,
pub num_kv_heads: usize,
pub head_dim: usize,
pub seed: u64,
}
pub(crate) fn run(
kernel: KernelImpl,
reference: KernelRef,
dims: ParityDims,
json: bool,
output: Option<&Path>,
force: bool,
) -> Result<()> {
if let Some(out) = output {
refuse_overwrite(out, force)?;
}
if let Err(err) = validate_dims(&dims) {
emit_error(&err, json, output)?;
return Err(CliError::ValidationFailed(err));
}
if kernel == KernelImpl::Flash2 {
return refuse_flash2(&dims, json, output);
}
let obs = measure_tiled(reference, dims)?;
let rendered = if json {
serde_json::to_string_pretty(&obs).map_err(|e| {
CliError::InvalidInput(format!("apr kernel parity: cannot serialize: {e}"))
})?
} else {
render_text(&obs)
};
write_out(&rendered, output)
}
fn write_out(rendered: &str, output: Option<&Path>) -> Result<()> {
match output {
Some(out) => std::fs::write(out, format!("{rendered}\n"))?,
None => println!("{rendered}"),
}
Ok(())
}
fn render_text(o: &ParityObservation) -> String {
format!(
"kernel parity {} vs {}\n regime : {}\n dims : seq_len={} heads={} \
kv_heads={} head_dim={} seed={}\n max_abs_diff: {:e}\n cosine_sim : {}\n \
attn_impl : {}\n provenance : {}",
o.kernel,
o.reference,
o.regime,
o.seq_len,
o.num_heads,
o.num_kv_heads,
o.head_dim,
o.seed,
o.max_abs_diff,
o.cosine_sim,
o.attn_impl,
o.kernel_source
.clone()
.or_else(|| o.fallback.clone())
.unwrap_or_default()
)
}
fn refuse_flash2(dims: &ParityDims, json: bool, output: Option<&Path>) -> Result<()> {
if !FLASH2_SUPPORTED_HEAD_DIMS.contains(&dims.head_dim) {
let supported = FLASH2_SUPPORTED_HEAD_DIMS
.iter()
.map(usize::to_string)
.collect::<Vec<_>>()
.join(", ");
let msg = format!(
"unsupported-head-dim: {} — flash2 dispatches only head_dim ∈ {{{supported}}} \
(contracts/crux-L-02-v1.yaml § flash_attn2_dispatch, arXiv:2307.08691)",
dims.head_dim
);
emit_error(&msg, json, output)?;
return Err(CliError::ValidationFailed(msg));
}
let msg = format!(
"flash2-kernel-unavailable: this binary embeds no \
hf-kernels-community:flash-attn2 kernel, so no flash2 measurement exists to report. \
Rerun with `--impl tiled` to measure the in-tree tiled kernel at head_dim {}.",
dims.head_dim
);
emit_error(&msg, json, output)?;
Err(CliError::NotImplemented(msg))
}
fn emit_error(message: &str, json: bool, output: Option<&Path>) -> Result<()> {
if !json {
return Ok(());
}
let body = serde_json::json!({ "error": message });
let rendered = serde_json::to_string_pretty(&body).unwrap_or_default();
write_out(&rendered, output)
}
fn validate_dims(d: &ParityDims) -> std::result::Result<(), String> {
if d.head_dim == 0 {
return Err("unsupported-head-dim: 0 — head_dim must be positive".to_string());
}
if d.seq_len == 0 {
return Err("apr kernel parity: --seq-len must be positive".to_string());
}
if d.num_heads == 0 || d.num_kv_heads == 0 {
return Err(
"apr kernel parity: --num-heads and --num-kv-heads must be positive".to_string(),
);
}
if d.num_heads % d.num_kv_heads != 0 {
return Err(format!(
"apr kernel parity: --num-heads {} is not a multiple of --num-kv-heads {} \
(GQA needs whole groups)",
d.num_heads, d.num_kv_heads
));
}
Ok(())
}
pub(crate) struct SplitMix64(u64);
impl SplitMix64 {
pub(crate) fn new(seed: u64) -> Self {
Self(seed)
}
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
pub(crate) fn next_unit(&mut self) -> f32 {
const SCALE: f32 = 1.0 / 16_777_216.0;
let unit = (self.next_u64() >> 40) as f32 * SCALE;
unit.mul_add(2.0, -1.0)
}
}
fn draw(rng: &mut SplitMix64, n: usize) -> Vec<f32> {
(0..n).map(|_| rng.next_unit()).collect()
}
pub(crate) fn naive_attention(
query: &[f32],
keys: &[f32],
values: &[f32],
dims: &ParityDims,
) -> Vec<f32> {
let scale = 1.0 / (dims.head_dim as f32).sqrt();
let group = dims.num_heads / dims.num_kv_heads;
let mut out = vec![0.0f32; dims.num_heads * dims.head_dim];
for h in 0..dims.num_heads {
let kv = h / group;
let q = &query[h * dims.head_dim..(h + 1) * dims.head_dim];
let mut scores = Vec::with_capacity(dims.seq_len);
for s in 0..dims.seq_len {
let base = (s * dims.num_kv_heads + kv) * dims.head_dim;
let dot: f32 = (0..dims.head_dim).map(|d| q[d] * keys[base + d]).sum();
scores.push(dot * scale);
}
let m = scores.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let mut denom = 0.0f32;
for s in &mut scores {
*s = (*s - m).exp();
denom += *s;
}
for (s, p) in scores.iter().enumerate() {
let base = (s * dims.num_kv_heads + kv) * dims.head_dim;
for d in 0..dims.head_dim {
out[h * dims.head_dim + d] += p * values[base + d];
}
}
for d in 0..dims.head_dim {
out[h * dims.head_dim + d] /= denom;
}
}
out
}
pub(crate) fn max_abs_diff(a: &[f32], b: &[f32]) -> f64 {
a.iter()
.zip(b.iter())
.map(|(x, y)| f64::from((x - y).abs()))
.fold(0.0f64, f64::max)
}
pub(crate) fn cosine_sim(a: &[f32], b: &[f32]) -> Option<f64> {
let dot: f64 = a
.iter()
.zip(b.iter())
.map(|(x, y)| f64::from(*x) * f64::from(*y))
.sum();
let na: f64 = a
.iter()
.map(|x| f64::from(*x) * f64::from(*x))
.sum::<f64>()
.sqrt();
let nb: f64 = b
.iter()
.map(|x| f64::from(*x) * f64::from(*x))
.sum::<f64>()
.sqrt();
if na == 0.0 || nb == 0.0 {
return None;
}
Some((dot / (na * nb)).clamp(-1.0, 1.0))
}
pub(crate) fn draw_qkv(dims: &ParityDims) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
let mut rng = SplitMix64(dims.seed ^ 0x5DEE_CE66_D1F5_1A3B);
let q = draw(&mut rng, dims.num_heads * dims.head_dim);
let kv_len = dims.seq_len * dims.num_kv_heads * dims.head_dim;
let k = draw(&mut rng, kv_len);
let v = draw(&mut rng, kv_len);
(q, k, v)
}
const TILED_FALLBACK_REASON: &str =
"in-tree tiled online-softmax kernel (realizar::brick::FlashAttentionBrick); \
this binary embeds no hf-kernels-community:flash-attn2 kernel, so no flash2 \
provenance sha exists to pin";
#[cfg(feature = "inference")]
fn measure_tiled(reference: KernelRef, dims: ParityDims) -> Result<ParityObservation> {
use realizar::brick::FlashAttentionBrick;
let (q, k, v) = draw_qkv(&dims);
let brick = FlashAttentionBrick::new(dims.num_heads, dims.num_kv_heads, dims.head_dim);
let tiled = brick.forward(&q, &k, &v, dims.seq_len).map_err(|e| {
CliError::ValidationFailed(format!(
"apr kernel parity: tiled kernel refused input: {e}"
))
})?;
let naive = naive_attention(&q, &k, &v, &dims);
let cos = cosine_sim(&tiled, &naive).ok_or_else(|| {
CliError::ValidationFailed(
"apr kernel parity: an output vector has zero norm, so cosine similarity is undefined"
.to_string(),
)
})?;
Ok(ParityObservation {
kernel: "tiled".to_string(),
reference: match reference {
KernelRef::Naive => "naive".to_string(),
},
seq_len: dims.seq_len,
num_heads: dims.num_heads,
num_kv_heads: dims.num_kv_heads,
head_dim: dims.head_dim,
seed: dims.seed,
regime: "decode-step: 1 query position over a seq_len KV cache".to_string(),
max_abs_diff: max_abs_diff(&tiled, &naive),
cosine_sim: cos,
attn_impl: "fallback".to_string(),
kernel_source: None,
fallback: Some(TILED_FALLBACK_REASON.to_string()),
})
}
#[cfg(not(feature = "inference"))]
fn measure_tiled(_reference: KernelRef, _dims: ParityDims) -> Result<ParityObservation> {
Err(CliError::FeatureDisabled(
"apr kernel parity --impl tiled needs the `inference` feature (it runs \
realizar::brick::FlashAttentionBrick); rebuild with --features inference"
.to_string(),
))
}
#[cfg(test)]
#[path = "kernel_parity_tests.rs"]
mod tests;