use std::collections::HashMap;
use std::path::{Path, PathBuf};
use super::impl_id::ImplId;
use super::record::{Candidate, DispatchRecord, HwStamp, MissRecord};
use super::structure_key::StructureKeyToken;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct MissKey {
wanted: StructureKeyToken,
fallback: ImplId,
hw: HwStamp,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct DispatchKey {
structure_key: Option<StructureKeyToken>,
chosen: ImplId,
hw: HwStamp,
}
#[derive(Debug, Clone, Default)]
struct DispatchAgg {
count: u64,
candidates: Vec<Candidate>,
}
#[derive(Debug, Default)]
pub struct TelemetrySink {
misses: HashMap<MissKey, u64>,
dispatches: HashMap<DispatchKey, DispatchAgg>,
}
impl TelemetrySink {
pub fn new() -> Self {
Self::default()
}
pub fn record_miss(&mut self, miss: MissRecord) {
let key = MissKey {
wanted: miss.wanted,
fallback: miss.fallback,
hw: miss.hw,
};
*self.misses.entry(key).or_insert(0) += miss.count.max(1);
}
pub fn miss_cell_count(&self) -> usize {
self.misses.len()
}
pub fn record_dispatch(&mut self, rec: DispatchRecord) {
let key = DispatchKey {
structure_key: rec.structure_key,
chosen: rec.chosen,
hw: rec.hw,
};
let agg = self.dispatches.entry(key).or_default();
agg.count += rec.count.max(1);
if !rec.candidates.is_empty() {
agg.candidates = rec.candidates;
}
}
pub fn dispatch_cell_count(&self) -> usize {
self.dispatches.len()
}
pub fn dispatch_records(&self) -> Vec<DispatchRecord> {
let mut recs: Vec<DispatchRecord> = self
.dispatches
.iter()
.map(|(key, agg)| DispatchRecord {
schema: super::record::TELEMETRY_SCHEMA_VERSION,
structure_key: key.structure_key.clone(),
chosen: key.chosen.clone(),
candidates: agg.candidates.clone(),
count: agg.count,
hw: key.hw.clone(),
})
.collect();
recs.sort_by(|a, b| {
a.structure_key
.as_ref()
.map(|t| &t.0)
.cmp(&b.structure_key.as_ref().map(|t| &t.0))
.then_with(|| a.chosen.kernel_source.cmp(&b.chosen.kernel_source))
.then_with(|| {
a.chosen
.kernel_revision_hash
.cmp(&b.chosen.kernel_revision_hash)
})
});
recs
}
pub fn miss_records(&self) -> Vec<MissRecord> {
let mut recs: Vec<MissRecord> = self
.misses
.iter()
.map(|(key, &count)| MissRecord {
schema: super::record::TELEMETRY_SCHEMA_VERSION,
wanted: key.wanted.clone(),
fallback: key.fallback.clone(),
count,
hw: key.hw.clone(),
})
.collect();
recs.sort_by(|a, b| {
a.wanted
.0
.cmp(&b.wanted.0)
.then_with(|| a.fallback.kernel_source.cmp(&b.fallback.kernel_source))
.then_with(|| {
a.fallback
.kernel_revision_hash
.cmp(&b.fallback.kernel_revision_hash)
})
});
recs
}
pub fn is_empty(&self) -> bool {
self.misses.is_empty() && self.dispatches.is_empty()
}
pub fn flush(&self, path: &Path) -> std::io::Result<usize> {
write_jsonl(path, &self.miss_records())
}
pub fn flush_dispatches(&self, path: &Path) -> std::io::Result<usize> {
write_jsonl(path, &self.dispatch_records())
}
pub fn flush_all(&self, dir: &Path) -> std::io::Result<(usize, usize)> {
std::fs::create_dir_all(dir)?;
let misses = self.flush(&dir.join("misses.jsonl"))?;
let dispatches = self.flush_dispatches(&dir.join("dispatches.jsonl"))?;
Ok((misses, dispatches))
}
}
fn write_jsonl<T: serde::Serialize>(path: &Path, records: &[T]) -> std::io::Result<usize> {
let mut body = String::new();
for r in records {
let line = serde_json::to_string(r).map_err(std::io::Error::other)?;
body.push_str(&line);
body.push('\n');
}
let mut tmp: PathBuf = path.to_path_buf();
tmp.as_mut_os_string().push(".tmp");
std::fs::write(&tmp, body.as_bytes())?;
std::fs::rename(&tmp, path)?;
Ok(records.len())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fkc::{ResolvedLayout, Tri};
use crate::telemetry::miss::{AdmittedContract, detect_miss};
use crate::telemetry::structure_key::{Contiguity, FdxOperandDesc, StructureKeyProvider};
use fuel_ir::dispatch::OpKind;
use fuel_ir::{BackendId, DType};
struct CannedProvider(String);
impl StructureKeyProvider for CannedProvider {
fn structure_key(
&self,
op_class: &str,
_operands: &[FdxOperandDesc],
_arch: &str,
) -> Option<StructureKeyToken> {
Some(StructureKeyToken(format!("{op_class}:{}", self.0)))
}
}
fn impl_id(kernel_source: &str) -> ImplId {
ImplId {
backend: BackendId::Cuda,
op: OpKind::MatMul,
dtypes: vec![DType::F16, DType::F16, DType::F16],
kernel_source: kernel_source.into(),
kernel_revision_hash: 0xabc,
}
}
fn cpu_hw() -> HwStamp {
HwStamp {
compute_capability: None,
hardware_sku: "Intel(R) Core(TM) i9-14900K".into(),
driver_version: "n/a".into(),
}
}
fn generic_best() -> AdmittedContract {
AdmittedContract {
impl_id: impl_id("baracuda-generic-strided"),
layouts: vec![
ResolvedLayout {
contiguous: Tri::NotApplicable,
strided: Tri::Accepted,
broadcast_stride0: Tri::Accepted,
start_offset: Tri::Rejected,
reverse_strides: Tri::Rejected,
};
2
],
}
}
fn operand() -> FdxOperandDesc {
FdxOperandDesc {
dtype: DType::F16,
contiguity: Contiguity::Contiguous,
broadcast: false,
flipped: false,
shape: vec![128, 256],
strides: vec![256, 1],
align_bytes: 256,
}
}
#[test]
fn generic_only_cell_emits_one_aggregated_miss_record() {
let provider = CannedProvider("innerdiv16:vec8:f16".into());
let best = generic_best();
let mut sink = TelemetrySink::new();
for _ in 0..3 {
let miss = detect_miss(&best, "matmul", &[operand()], "sm_89", &provider, cpu_hw())
.expect("generic-only cell must emit a miss");
sink.record_miss(miss);
}
assert_eq!(
sink.miss_cell_count(),
1,
"three identical misses aggregate to ONE cell"
);
let recs = sink.miss_records();
assert_eq!(recs.len(), 1);
assert_eq!(
recs[0].wanted,
StructureKeyToken("matmul:innerdiv16:vec8:f16".into())
);
assert_eq!(recs[0].fallback, impl_id("baracuda-generic-strided"));
assert_eq!(
recs[0].count, 3,
"the three observations sum in the cell count"
);
assert_eq!(recs[0].hw, cpu_hw());
}
#[test]
fn distinct_fallbacks_are_distinct_cells() {
let provider = CannedProvider("k".into());
let mut sink = TelemetrySink::new();
for src in ["baracuda-generic-strided", "portable-cpu-strided"] {
let best = AdmittedContract {
impl_id: impl_id(src),
..generic_best()
};
let miss = detect_miss(&best, "matmul", &[operand()], "sm_89", &provider, cpu_hw())
.expect("miss");
sink.record_miss(miss);
}
assert_eq!(sink.miss_cell_count(), 2, "two fallbacks ⇒ two cells");
}
#[test]
fn flush_writes_valid_jsonl_that_round_trips() {
let provider = CannedProvider("innerdiv16:f16".into());
let best = generic_best();
let mut sink = TelemetrySink::new();
let miss =
detect_miss(&best, "matmul", &[operand()], "sm_89", &provider, cpu_hw()).expect("miss");
sink.record_miss(miss);
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("telemetry.jsonl");
let n = sink.flush(&path).expect("flush");
assert_eq!(n, 1, "one aggregated miss ⇒ one line");
let body = std::fs::read_to_string(&path).expect("read back");
let lines: Vec<&str> = body.lines().collect();
assert_eq!(lines.len(), 1, "one record ⇒ one JSONL line");
let back: MissRecord = serde_json::from_str(lines[0]).expect("parse MissRecord line");
assert_eq!(
back.wanted,
StructureKeyToken("matmul:innerdiv16:f16".into())
);
assert_eq!(back.fallback, impl_id("baracuda-generic-strided"));
assert_eq!(
back.hw.compute_capability, None,
"CPU stamp round-trips to None"
);
assert_eq!(back.schema, super::super::record::TELEMETRY_SCHEMA_VERSION);
assert!(!lines[0].contains("est_speedup"), "est_speedup is omitted");
assert!(
!lines[0].contains("compute_capability"),
"None CC omitted from the wire"
);
}
#[test]
fn empty_sink_flushes_empty_file() {
let sink = TelemetrySink::new();
assert!(sink.is_empty());
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("empty.jsonl");
let n = sink.flush(&path).expect("flush empty");
assert_eq!(n, 0);
assert_eq!(std::fs::read_to_string(&path).expect("read").len(), 0);
}
fn dispatch_rec(
structure_key: Option<&str>,
chosen_source: &str,
candidates: Vec<Candidate>,
) -> DispatchRecord {
DispatchRecord {
schema: super::super::record::TELEMETRY_SCHEMA_VERSION,
structure_key: structure_key.map(|s| StructureKeyToken(s.into())),
chosen: impl_id(chosen_source),
candidates,
count: 1,
hw: cpu_hw(),
}
}
#[test]
fn dispatch_records_aggregate_by_cell_and_keep_candidates() {
let mut sink = TelemetrySink::new();
let detailed = dispatch_rec(
Some("mm:innerdiv16"),
"baracuda",
vec![Candidate {
impl_id: impl_id("baracuda"),
latency_ns: Some(41_230),
}],
);
sink.record_dispatch(detailed);
let coarse = dispatch_rec(Some("mm:innerdiv16"), "baracuda", vec![]);
sink.record_dispatch(coarse);
assert_eq!(
sink.dispatch_cell_count(),
1,
"same cell ⇒ one aggregated cell"
);
let recs = sink.dispatch_records();
assert_eq!(recs.len(), 1);
assert_eq!(recs[0].count, 2, "two observations sum in the cell count");
assert_eq!(
recs[0].candidates,
vec![Candidate {
impl_id: impl_id("baracuda"),
latency_ns: Some(41_230)
}],
"the Detailed candidates survive a later empty Coarse observation",
);
}
#[test]
fn distinct_chosen_are_distinct_dispatch_cells() {
let mut sink = TelemetrySink::new();
sink.record_dispatch(dispatch_rec(Some("mm"), "baracuda", vec![]));
sink.record_dispatch(dispatch_rec(Some("mm"), "cublas", vec![]));
assert_eq!(
sink.dispatch_cell_count(),
2,
"two chosen impls ⇒ two cells"
);
}
#[test]
fn flush_all_writes_separate_files() {
let provider = CannedProvider("innerdiv16:f16".into());
let best = generic_best();
let mut sink = TelemetrySink::new();
let miss =
detect_miss(&best, "matmul", &[operand()], "sm_89", &provider, cpu_hw()).expect("miss");
sink.record_miss(miss);
sink.record_dispatch(dispatch_rec(
Some("mm:innerdiv16"),
"baracuda",
vec![Candidate {
impl_id: impl_id("baracuda"),
latency_ns: Some(41_230),
}],
));
let dir = tempfile::tempdir().expect("tempdir");
let (m, d) = sink.flush_all(dir.path()).expect("flush_all");
assert_eq!((m, d), (1, 1), "one miss + one dispatch line");
let miss_body = std::fs::read_to_string(dir.path().join("misses.jsonl")).expect("misses");
let disp_body =
std::fs::read_to_string(dir.path().join("dispatches.jsonl")).expect("dispatches");
let _: MissRecord =
serde_json::from_str(miss_body.lines().next().expect("miss line")).expect("MissRecord");
let back: DispatchRecord =
serde_json::from_str(disp_body.lines().next().expect("disp line"))
.expect("DispatchRecord");
assert_eq!(back.chosen, impl_id("baracuda"));
assert_eq!(back.candidates[0].latency_ns, Some(41_230));
}
}