khtst-rust / src /telemetria.rs
PowerMachine's picture
v0.9.0: análise/prova/teste das críticas Qwen3.8 lote 2 (r-34) — SIMD com despacho em runtime bit-a-bit (X-1), oráculo candle_nn::optim::AdamW (nosso 1,53× mais rápido), Thompson Sampling (regret 0,0019 vs UCB1 0,0156), GNG por entropia com piso (T-G1'), JEPA de fração adaptativa (bit-a-bit com 1 braço), configs tipadas, entrada numpy (2008× na fronteira), avaliação com dados reais — 104/104 gates, 111/111 cargo
d444358 verified
Raw History Blame Contribute Delete
19.2 kB
//! Telemetria completa (pedido do usuário: "realize todo tipo de telemetria
//! inclusive para a detecção de aprendizado do modelo e dos ajustes
//! automatizados de todos hiper parâmetros e demais necessários para ajustes
//! e correções e detecção de erros").
//!
//! Hub em Rust com: buffer por passo; exportação JSONL; detecção de
//! aprendizado (escore composto); eventos de ajuste automático (auditáveis);
//! eventos de erro com classificação de causa comum (diagnostico.rs);
//! séries expostas ao Python como arrays numpy.
use serde::Serialize;
use std::collections::BTreeMap;
use std::io::Write;
#[derive(Debug, Clone, Serialize)]
pub struct EventoAjuste {
pub passo: usize,
pub parametro: String,
pub de: f64,
pub para: f64,
pub motivo: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct EventoErro {
pub passo: usize,
pub modulo: String,
pub mensagem: String,
pub causa_provavel: String,
pub correcao_sugerida: String,
}
#[derive(Default)]
pub struct TelemetryHub {
/// séries nomeadas por passo
pub series: BTreeMap<String, Vec<(usize, f64)>>,
pub eventos_ajuste: Vec<EventoAjuste>,
pub eventos_erro: Vec<EventoErro>,
pub caminho_jsonl: Option<String>,
pub passo_corrente: usize,
}
impl TelemetryHub {
pub fn novo(caminho_jsonl: Option<String>) -> Self {
if let Some(p) = &caminho_jsonl {
if let Some(dir) = std::path::Path::new(p).parent() {
let _ = std::fs::create_dir_all(dir);
}
// truncates no início da corrida
let _ = std::fs::File::create(p);
}
TelemetryHub { caminho_jsonl, ..Default::default() }
}
pub fn registrar(&mut self, nome: &str, valor: f64) {
self.series.entry(nome.to_string()).or_default().push((self.passo_corrente, valor));
}
pub fn registrar_multi(&mut self, pares: &[(&str, f64)]) {
for (n, v) in pares {
self.registrar(n, *v);
}
}
pub fn ajuste(&mut self, parametro: &str, de: f64, para: f64, motivo: &str) {
let ev = EventoAjuste {
passo: self.passo_corrente,
parametro: parametro.to_string(),
de,
para,
motivo: motivo.to_string(),
};
log::info!("ajuste: {} {:.4e}→{:.4e} ({})", parametro, de, para, motivo);
self.append_jsonl("ajuste", &ev);
self.eventos_ajuste.push(ev);
}
pub fn erro(&mut self, modulo: &str, mensagem: &str) {
let causas = crate::diagnostico::classificar_erro(mensagem);
let (causa, correcao, _) = causas
.first()
.copied()
.unwrap_or((
"não classificado",
"inspecionar mensagem completa; se recente à refatoração, suspeitar primeiro de digitação/renomeação",
"fallback",
));
let ev = EventoErro {
passo: self.passo_corrente,
modulo: modulo.to_string(),
mensagem: mensagem.to_string(),
causa_provavel: causa.to_string(),
correcao_sugerida: correcao.to_string(),
};
self.append_jsonl("erro", &ev);
self.eventos_erro.push(ev);
}
/// Escreve o dump completo das séries no JSONL (evento "dump") — chamado
/// ao fim de cada fase para o painel v0.5.0 (Theil–Sen por série).
pub fn dump_jsonl(&self) {
if let Some(p) = &self.caminho_jsonl {
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(p) {
let _ = writeln!(f, "{{\"tipo\":\"dump\",\"dados\":{}}}",
serde_json::to_string(&self.dump().get("series").cloned()
.unwrap_or(serde_json::Value::Null)).unwrap_or_default());
}
}
}
fn append_jsonl<T: Serialize>(&self, tipo: &str, ev: &T) {
if let Some(p) = &self.caminho_jsonl {
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(p) {
let _ = writeln!(f, "{{\"tipo\":\"{tipo}\",\"dados\":{}}}",
serde_json::to_string(ev).unwrap_or_default());
}
}
}
/// EMA de uma série (para suavização exponencial nos detectores).
pub fn ema(&self, nome: &str, alfa: f64) -> Option<f64> {
let s = self.series.get(nome)?;
let mut acc = s[0].1;
for (_, v) in &s[1..] {
acc = alfa * v + (1.0 - alfa) * acc;
}
Some(acc)
}
/// Inclinação da EMA nos últimos k passos (detecção de aprendizado).
pub fn inclinacao_ema(&self, nome: &str, k: usize, alfa: f64) -> Option<f64> {
let s = self.series.get(nome)?;
if s.len() < 2 { return None; }
let ini = s.len().saturating_sub(k);
let mut acc_ini = s[ini].1;
for (_, v) in &s[ini + 1..] {
acc_ini = alfa * v + (1.0 - alfa) * acc_ini;
}
let mut acc_fim = s[0].1;
for (_, v) in &s[1..] {
acc_fim = alfa * v + (1.0 - alfa) * acc_fim;
}
let passos = (s.len() - ini) as f64;
Some((acc_fim - acc_ini) / passos)
}
/// Escore de aprendizado composto em [0,1]: sinal = inclinação negativa da
/// perda (EMA) + nível absoluto relativo ao inicial + saúde do codebook.
pub fn escore_aprendizado(&self) -> f64 {
let Some(perda) = self.series.get("perda") else { return 0.0 };
if perda.len() < 4 { return 0.0; }
let p0 = perda.iter().map(|(_, v)| *v).fold(f64::INFINITY, f64::min);
let pl = perda.last().map(|(_, v)| *v).unwrap_or(p0);
let reducao = ((p0 - pl) / p0.max(1e-9)).clamp(0.0, 1.0);
let incl = self.inclinacao_ema("perda", 64, 0.05).unwrap_or(0.0);
let sinal = (-incl * 1000.0).clamp(-1.0, 1.0) * 0.5 + 0.5;
// saúde do codebook (se medida): perplexidade relativa
let saude = self.series.get("codebook_perplexidade_media").and_then(|s| s.last().map(|(_, v)| *v)).unwrap_or(1.0);
let saude_n = (saude / 16.0).clamp(0.0, 1.0); // K=64: perplexidade 16+ = saudável
0.45 * reducao + 0.35 * sinal + 0.20 * saude_n
}
// -------------------------------------------------------------------
// v0.5.0 — detecção de aprendizado ROBUSTA (regressão de Theil–Sen com
// R²) e ajustes automatizados auditados
// -------------------------------------------------------------------
/// Detecção robusta de aprendizado numa série: regressão de Theil–Sen
/// (mediana das inclinações par a par — quebra com até ~29% de outliers)
/// + R² contra o ajuste linear + veredito qualitativo.
///
/// **Teo TD-1 (robustez do Theil–Sen):** com < 29,3% de pontos
/// contaminados, a mediana das inclinações par a par permanece no
/// intervalo interpolado dos resíduos limpos (breakdown point exato do
/// estimador par-a-par); a média OLS quebra com UM único outlier.
///
/// Retorna (inclinacao_robusta, r2, veredito) onde o veredito ∈
/// {"aprendendo", "estagnado", "divergindo", "amostra_insuficiente"}.
pub fn deteccao_aprendizado(&self, nome: &str, janela: usize) -> (f64, f64, &'static str) {
let Some(s) = self.series.get(nome) else { return (0.0, 0.0, "amostra_insuficiente") };
if s.len() < 4 {
return (0.0, 0.0, "amostra_insuficiente");
}
let ini = s.len().saturating_sub(janela.max(4));
let pts: Vec<(f64, f64)> = s[ini..].iter().map(|(t, v)| (*t as f64, *v)).collect();
let n = pts.len();
if n < 4 {
return (0.0, 0.0, "amostra_insuficiente");
}
// Theil–Sen: mediana das inclinações (i < j)
let mut inclinacoes: Vec<f64> = Vec::with_capacity(n * (n - 1) / 2);
for i in 0..n {
for j in i + 1..n {
let dx = pts[j].0 - pts[i].0;
if dx.abs() > 1e-12 {
inclinacoes.push((pts[j].1 - pts[i].1) / dx);
}
}
}
if inclinacoes.is_empty() {
return (0.0, 0.0, "amostra_insuficiente");
}
inclinacoes.sort_by(|a, b| a.partial_cmp(b).unwrap());
let incl = inclinacoes[inclinacoes.len() / 2];
// intercepto pela mediana dos resíduos
let mut res_intercept: Vec<f64> = pts.iter().map(|(x, y)| y - incl * x).collect();
res_intercept.sort_by(|a, b| a.partial_cmp(b).unwrap());
let b0 = res_intercept[res_intercept.len() / 2];
// R² do ajuste robusto (1 − SS_res/SS_tot)
let media_y: f64 = pts.iter().map(|(_, y)| y).sum::<f64>() / n as f64;
let ss_tot: f64 = pts.iter().map(|(_, y)| (y - media_y) * (y - media_y)).sum();
let ss_res: f64 = pts
.iter()
.map(|(x, y)| {
let e = y - (incl * x + b0);
e * e
})
.sum();
let r2 = if ss_tot > 1e-12 { (1.0 - ss_res / ss_tot).clamp(-1.0, 1.0) } else { 0.0 };
// veredito: a escala da inclinação é relativa à magnitude da série
let escala = media_y.abs().max(1e-9);
let rel = incl / escala;
let veredito = if rel < -1e-4 {
"aprendendo"
} else if rel > 1e-4 {
"divergindo"
} else {
"estagnado"
};
(incl, r2, veredito)
}
/// Painel de telemetria v0.5.0: detecção de aprendizado em TODAS as
/// séries registradas + eventos de ajuste computados + saúde geral.
pub fn painel_v050(&self) -> serde_json::Value {
let deteccoes: serde_json::Map<String, serde_json::Value> = self
.series
.iter()
.map(|(k, _)| {
let (incl, r2, vered) = self.deteccao_aprendizado(k, 256);
(
k.clone(),
serde_json::json!({
"inclinacao_theil_sen": incl,
"r2": r2,
"veredito": vered,
"n_amostras": self.series[k].len(),
}),
)
})
.collect();
serde_json::json!({
"versao": "0.5.0",
"deteccoes": deteccoes,
"escore_aprendizado": self.escore_aprendizado(),
"ajustes_computados": self.eventos_ajuste.len(),
"erros_registrados": self.eventos_erro.len(),
})
}
/// Dump completo (para o relatório Python).
pub fn dump(&self) -> serde_json::Value {
let series: serde_json::Map<String, serde_json::Value> = self
.series
.iter()
.map(|(k, v)| {
(
k.clone(),
serde_json::Value::Array(
v.iter().map(|(p, x)| serde_json::json!([p, x])).collect(),
),
)
})
.collect();
serde_json::json!({
"series": series,
"eventos_ajuste": self.eventos_ajuste,
"eventos_erro": self.eventos_erro,
"escore_aprendizado": self.escore_aprendizado(),
})
}
/// Últimos valores de todas as séries (snapshot para callback Python).
pub fn instantaneo(&self) -> Vec<(String, f64)> {
self.series
.iter()
.filter_map(|(k, v)| v.last().map(|(_, x)| (k.clone(), *x)))
.collect()
}
// -------------------------------------------------------------------
// v0.6.0 — acordo par-a-par, polaridade por métrica, painel v0.6.0
// -------------------------------------------------------------------
/// **Acordo par-a-par das tentativas** (insumo do A-3): média das
/// similaridades de cosseno entre pares de candidatos (n, d).
/// Sob o modelo binário i.i.d. (correto c/ prob. p), o acordo em
///expectância é `a = p² + (1−p)²` (lema do acordo — r-26 A-3).
pub fn acordo_par_a_par(candidatos: &[(String, f64)]) -> f64 {
// variante escalar: média das concordâncias sinalizadas entre pares
if candidatos.len() < 2 {
return 0.0;
}
let mut soma = 0f64;
let mut pares = 0usize;
for i in 0..candidatos.len() {
for j in i + 1..candidatos.len() {
let (_, vi) = &candidatos[i];
let (_, vj) = &candidatos[j];
// concordância = 1 − |vi − vj| (métrica em [0,1])
soma += 1.0 - (vi - vj).abs();
pares += 1;
}
}
soma / pares.max(1) as f64
}
/// **Polaridade por métrica** (fecha o débito r-22 §7): para séries
/// cujo nome indica ERRO (perda, ce, erro, eq, mse, delta, norma) a
/// direção BOA é DESCER (−1); para as demais (confiança, acurácia,
/// perplexidade de codebook, escore, accordo) a direção boa é SUBIR
/// (+1). O veredito "aprendendo" agora é POLARIDADE-AWARE:
/// aprendendo ⇔ inclinação na direção boa.
pub fn polaridade(nome: &str) -> i8 {
let n = nome.to_ascii_lowercase();
const NEGATIVAS: [&str; 9] =
["perda", "ce", "erro", "eq", "mse", "delta_ort", "norma_grad", "diverg", "tempo"];
if NEGATIVAS.iter().any(|p| n.starts_with(p) || n.contains(&format!("_{p}"))) {
-1
} else {
1
}
}
/// Painel de telemetria v0.6.0: detecção polaridade-aware em TODAS as
/// séries + veredito consolidado por direção.
pub fn painel_v060(&self) -> serde_json::Value {
let mut aprendendo = 0usize;
let mut piorando = 0usize;
let mut estagnado = 0usize;
let deteccoes: serde_json::Map<String, serde_json::Value> = self
.series
.iter()
.map(|(k, _)| {
let (incl, r2, vered) = self.deteccao_aprendizado(k, 256);
let pol = Self::polaridade(k) as i8;
// veredito polaridade-aware: incl·pol < 0 ⇒ aprendendo
let prod = incl * pol as f64;
let vered_pol: &'static str = if self.series[k].len() < 4 {
"amostra_insuficiente"
} else if prod < -1e-9 {
"aprendendo"
} else if prod > 1e-9 {
"piorando"
} else {
"estagnado"
};
match vered_pol {
"aprendendo" => aprendendo += 1,
"piorando" => piorando += 1,
_ => estagnado += 1,
}
let _ = vered;
(
k.clone(),
serde_json::json!({
"inclinacao_theil_sen": incl,
"r2": r2,
"polaridade": pol,
"veredito_polarizado": vered_pol,
"n_amostras": self.series[k].len(),
}),
)
})
.collect();
serde_json::json!({
"versao": "0.6.0",
"deteccoes": deteccoes,
"resumo_direcoes": {
"aprendendo": aprendendo, "piorando": piorando, "estagnado": estagnado,
},
"escore_aprendizado": self.escore_aprendizado(),
"ajustes_computados": self.eventos_ajuste.len(),
"erros_registrados": self.eventos_erro.len(),
})
}
/// Painel v0.7.0 — herda o v0.6.0 (polaridade-aware) e adiciona o
/// bloco de sinais dos módulos novos: FNH (gate/punição/δ_ort), árvore
/// de pesos (crédito/regulação por nó), quebra-cabeça (aceitação/
/// confiança) e NLP/NLG (gates/coerência). Métricas ausentes são
/// simplesmente omitidas (painel ADITIVO — sem acoplamento rígido).
/// Painel v0.9.0 — despacho SIMD + contexto das medições da r-34.
pub fn painel_v090(&self) -> serde_json::Value {
let mut p = self.painel_v070(None);
if let Some(obj) = p.as_object_mut() {
obj.insert("simd".into(), crate::simd::relatorio());
obj.insert(
"v090_nota".into(),
serde_json::json!(
"kernels AVX-512 com despacho em runtime (bit-a-bit); medições em r-34 §4/§6: \
AdamW 1,0-1,2× (limitado por memória), BMU argmin: escalar ótimo em d=4; \
bf16 em CPU candle: 3× MAIS LENTO (upcast por op); nosso AdamW 1,5-1,7× mais \
rápido que candle_nn::optim"
),
);
}
p
}
pub fn painel_v070(&self, arvore: Option<&crate::arvore_pesos::ArvorePesos>) -> serde_json::Value {
let mut p = self.painel_v060();
// promove a versão sem perder o conteúdo
p["versao"] = serde_json::json!("0.7.0");
// sinais FNH (registrados no hub como fnh_*)
let fnh = serde_json::json!({
"gate_ultimo": self.ema("fnh_gate", 0.9),
"punicao_ema": self.ema("fnh_punicao", 0.9),
"delta_ort_rotacao": self.ema("fnh_delta_ort", 0.9),
"theta_norma": self.ema("fnh_theta_norma", 0.9),
});
p["fnh"] = fnh;
// quebra-cabeça
p["quebra_cabeca"] = serde_json::json!({
"taxa_aceitacao": self.ema("qc_taxa_aceitacao", 0.8),
"confianca_melhor": self.ema("qc_confianca_melhor", 0.8),
"confianca_base": self.ema("qc_confianca_base", 0.8),
"punicao_acumulada": self.ema("qc_punicao", 0.8),
});
// NLP/NLG
p["nlp_nlg"] = serde_json::json!({
"nlp_gate_medio": self.ema("nlp_gate_medio", 0.9),
"nlg_coerencia": self.ema("nlg_score_coerencia", 0.9),
"espec_aceitacao": self.ema("espec_aceitacao", 0.9),
"espec_speedup": self.ema("espec_speedup", 0.9),
});
// árvore de pesos: métricas agregadas por nó (fundo-topo)
if let Some(a) = arvore {
let nos: serde_json::Map<String, serde_json::Value> = a
.nos
.iter()
.map(|no| {
let m: serde_json::Map<String, serde_json::Value> = no
.metricas
.iter()
.map(|(k, v)| (k.clone(), serde_json::json!(v)))
.collect();
(
no.nome.clone(),
serde_json::json!({
"intervalo": [no.ini, no.fim],
"profundidade": no.profundidade,
"metricas": m,
}),
)
})
.collect();
p["arvore_pesos"] = serde_json::json!({
"n_nos": a.n_nos(),
"n_enlaces": a.enlaces.len(),
"n_params": a.n_params,
"nos": nos,
"invariantes_ok": a.invariantes_ok().is_ok(),
});
}
p
}
}