open-standards/jouleclaw/jouleclaw-rs/crates/jouleclaw-harness-econ/examples/local_model_econ.rs
Transaction Science d84155efea jouleclaw-harness-econ: report a ModelBased figure as modeled, not measured
joules_obtained() printed "measured" for ModelBased as well as HwShunt, so
the local-model examples called a vendor energy model's figure (power x
time) a measurement. It now follows the grade: "measured" for a hardware
reading only, "modeled" for a model's figure, "estimated" for an estimate.
The test that pinned ModelBased to "measured" now pins "modeled".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 22:23:23 -04:00

271 lines
11 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Real local-model dispatch, metered joules at the meter's honest provenance, in
//! the econ report — and the cheapest-sufficient energy collapse.
//!
//! Wires the pure-Rust `jouleclaw-loader-gguf` inference substrate as a cascade
//! [`Tier`]: it loads a real GGUF/safetensors model, greedily decodes an answer,
//! and meters the energy of that decode window via `jouleclaw-energy`'s
//! auto-detected meter. A recurring task is run several rounds with
//! skill-compilation on: the first few rounds pay the **real model** (real
//! decode time, real metered joules), then the class compiles into a
//! deterministic skill and the rest resolve at ~0 — the cheapest-sufficient
//! thesis in metered energy.
//!
//! HONEST ENERGY: the decode runs on this process's own threads (the loader's CPU
//! kernels on its rayon pool; this example builds the loader without its GPU
//! `fabric`), so the meter is `jouleclaw-energy`'s in-process meter
//! (`detect_in_process_meter`). On macOS that is this process's own CPU-energy
//! counter (`process-cpu`, `Provenance::HwShunt` over `EnergyDomain::ProcessCpu`)
//! where it is live, as it is for an unprivileged reader on macOS 27.0.1 on an M5
//! Max and an M3 Ultra: a hardware reading of the decode's CPU energy, which does
//! not move with anything else the host runs. Where it is not live, and off macOS,
//! it is the host meter (`detect_meter`): the IOReport counter where its CPU rail
//! is live, else a load-average × power-band ESTIMATOR (`Provenance::Estimator`).
//! Each dispatch prints which meter ran and whether its joules were measured,
//! modeled or estimated. Decode wall-time and token counts are exact measurements. A LOCAL
//! model is priced energy-only (no per-call API dollars).
//!
//! Run (defaults to a Nemotron-3-Nano-4B snapshot if present):
//! ```text
//! cargo run --release -p jouleclaw-harness-econ --example local_model_econ
//! cargo run --release -p jouleclaw-harness-econ --example local_model_econ -- /path/to/hf/snapshot
//! ```
use std::path::PathBuf;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Instant;
use jouleclaw_cascade::{
Answer, AnswerError, AnswerOutput, Cascade, ContextRef, ExecutionTrace, JouleBudget,
L3ModelId, QualityFloor, Query, QueryInput, Runtime, Tier, TierEstimate, TierId,
VerificationStatus,
};
use jouleclaw_energy::meter::detect_in_process_meter;
use jouleclaw_energy::Provenance;
use jouleclaw_harness_econ::{joules_obtained, window_joules, EconHarness, PriceModel, Task};
use jouleclaw_loader_gguf::bpe_tokenizer::BpeTokenizer;
use jouleclaw_loader_gguf::nemotron_h::{NemotronHConfig, NemotronHModel};
use jouleclaw_loader_gguf::safetensors_reader::read_safetensors_file;
use jouleclaw_skill::{ConstantSkillInducer, SkillTier};
const DEFAULT_SNAPSHOT: &str = "/Volumes/macos_4TB_external_1/hf_hub/models--nvidia--NVIDIA-Nemotron-3-Nano-4B-BF16/snapshots/dfaf35de3e30f1867dd8dbc38a7fc9fb52d3914f";
/// One real model dispatch: wall-time, tokens, and metered joules — with the meter
/// that metered them and the provenance of its readings.
#[derive(Clone, Copy)]
struct DispatchStat {
secs: f64,
tokens: usize,
joules: f64,
meter: &'static str,
provenance: Provenance,
}
/// A cascade tier backed by a real local model. Greedy-decodes an answer and
/// meters the energy of the decode window. Priced as L3, but LOCAL → the econ
/// PriceModel charges it energy only (no API call dollars).
struct LocalModelTier {
model: NemotronHModel,
tok: BpeTokenizer,
max_tokens: usize,
bos: u32,
calls: Arc<AtomicU32>,
log: Arc<Mutex<Vec<DispatchStat>>>,
}
impl Tier for LocalModelTier {
fn id(&self) -> TierId {
TierId::L3(L3ModelId(0))
}
fn estimate_cost(&self, _q: &Query) -> Option<TierEstimate> {
Some(TierEstimate {
joules: 10.0,
latency: std::time::Duration::from_secs(1),
confidence_floor: 0.8,
})
}
fn try_answer(&mut self, q: &Query, _budget: f64) -> Result<Answer, AnswerError> {
let prompt = match &q.input {
QueryInput::Text(s) => s.clone(),
_ => {
return Err(AnswerError::TierFailed {
tier: self.id(),
cause: "non-text query".into(),
})
}
};
let ids: Vec<u32> =
std::iter::once(self.bos).chain(self.tok.encode(&prompt)).collect();
// Fresh meter per dispatch (its liveness probe runs once per process): its
// first read sets the baseline, the second yields the energy over the
// decode window. The decode runs on this process's threads, so the
// in-process meter covers it. A window the meter put no figure on stops
// the run (`window_joules`): a failed read is not a zero-energy reading,
// a counter that did not advance did not see the work, and a figure the
// meter did not produce is never reported.
let meter = detect_in_process_meter();
let e0 = meter.read();
let t = Instant::now();
let mut state = self.model.new_state();
let mut logits = self.model.forward_prompt(&ids, &mut state);
let mut generated: Vec<u32> = Vec::new();
for step in 0..self.max_tokens {
let l = if step == 0 {
std::mem::take(&mut logits)
} else {
self.model.forward_token(*generated.last().unwrap(), &mut state)
};
let (arg, _) = l.iter().enumerate().fold((0usize, f32::NEG_INFINITY), |(bi, bv), (i, &v)| {
if v > bv {
(i, v)
} else {
(bi, bv)
}
});
generated.push(arg as u32);
}
let secs = t.elapsed().as_secs_f64();
let (joules, provenance) = window_joules(e0, meter.read())
.unwrap_or_else(|why| panic!("no energy figure for this dispatch from `{}`: {why}", meter.active_name()));
let text = self.tok.decode(&generated);
self.calls.fetch_add(1, Ordering::SeqCst);
self.log.lock().unwrap().push(DispatchStat {
secs,
tokens: generated.len(),
joules,
meter: meter.active_name(),
provenance,
});
Ok(Answer {
output: AnswerOutput::Text(text),
tier_used: self.id(),
joules_spent: joules,
confidence: 0.8,
trace: ExecutionTrace::default(),
verification: VerificationStatus::Resolved,
})
}
}
fn main() {
let dir = PathBuf::from(std::env::args().nth(1).unwrap_or_else(|| DEFAULT_SNAPSHOT.into()));
if !dir.join("config.json").is_file() {
eprintln!(
"model snapshot not found at {}\n(pass an HF snapshot dir with config.json + \
model.safetensors + tokenizer.json)",
dir.display()
);
return;
}
// ---- load the real model ----
let load = Instant::now();
let config: NemotronHConfig =
serde_json::from_slice(&std::fs::read(dir.join("config.json")).unwrap()).unwrap();
let st = read_safetensors_file(dir.join("model.safetensors"))
.unwrap_or_else(|e| panic!("safetensors: {e:?}"));
let model = NemotronHModel::bind(config, st).expect("bind model");
let tok = BpeTokenizer::from_file(dir.join("tokenizer.json")).expect("tokenizer");
println!("loaded {} in {:.2}s", dir.display(), load.elapsed().as_secs_f64());
let calls = Arc::new(AtomicU32::new(0));
let log = Arc::new(Mutex::new(Vec::<DispatchStat>::new()));
let tier = LocalModelTier {
model,
tok,
max_tokens: 6,
bos: 1,
calls: calls.clone(),
log: log.clone(),
};
// run() calls the builder once → take the model out of this slot then.
let slot = Mutex::new(Some(tier));
// A recurring factual task the 4B model can answer; verified by substring.
// (A base 4B model won't write applyable SWE-bench diffs — that's the honest
// reason this demo uses a task within its reach to exercise the metered tier.)
let tasks = vec![Task::text(
"capital-france",
"geo-capital",
Query {
input: QueryInput::Text("The capital of France is".into()),
budget: JouleBudget::expensive(),
quality: QualityFloor::any(),
context: ContextRef::fresh(),
deadline: None,
},
|o| matches!(o, AnswerOutput::Text(t) if t.to_lowercase().contains("paris")),
)];
// LOCAL model → energy-only dollars (no per-call API price).
let mut harness = EconHarness::new(Box::new(ConstantSkillInducer), 3);
harness.price = PriceModel { usd_per_kwh: 0.15, l3_call_usd: 0.0, l4_call_usd: 0.0 };
let rounds = 5;
let report = harness.run(
move |store| {
let mut casc = Cascade::new();
casc.register(Box::new(SkillTier::new(store)));
casc.register(Box::new(slot.lock().unwrap().take().expect("built once")));
Runtime::new_without_l0(casc)
},
&tasks,
rounds,
);
println!("\n=== Local-model econ report — real inference, metered ===\n");
print!("{}", report.to_table());
let stats = log.lock().unwrap();
println!("\nreal model dispatches: {} (rest served by the compiled skill)", stats.len());
for (i, s) in stats.iter().enumerate() {
println!(
" dispatch {}: {} tok in {:.2}s ({:.1} tok/s), {:.3} J {} by {} ({})",
i + 1,
s.tokens,
s.secs,
if s.secs > 0.0 { s.tokens as f64 / s.secs } else { 0.0 },
s.joules,
joules_obtained(s.provenance),
s.meter,
s.provenance
);
}
let model_j: f64 = stats.iter().map(|s| s.joules).sum();
let model_n = stats.len().max(1);
// The dispatches' own provenance, not an assumed one.
let provenance = match stats.first().map(|s| s.provenance) {
Some(p) if stats.iter().all(|s| s.provenance == p) => format!("{p:?}"),
Some(_) => "mixed provenance".to_string(),
None => "no dispatch".to_string(),
};
println!(
"\navg real-model dispatch : {:.3} J ({provenance}, energy-only $)",
model_j / model_n as f64
);
println!(
"compiled-skill dispatch : ~{:.0e} J (deterministic, model untouched)",
jouleclaw_skill::SKILL_JOULES
);
println!(
"\nverified fixes: {}/{}", report.verified, report.total
);
println!(
"joules per verified fix : {:.3e} J ({:?})",
report.joules_per_verified_fix, report.energy_provenance
);
println!("dollars per verified fix : ${:.8}", report.usd_per_verified_fix);
println!(
"\nThe energy axis is real inference on this machine, at each dispatch's meter and\n\
provenance above; the curve bends because the recurring class compiled into a\n\
skill and stopped paying the model."
);
}