HwMeter passed the family meter's whole-host estimators straight through. Those accrue by the millisecond, so a span shorter than one — a served request, here — read 0 µJ: on a host with no live whole-request counter (this M5 Max), every request served with `server,hw-meter` or `server,hw-meter-apple` sealed 0 µJ [Estimator], against HwMeter's own rule that a span is never billed a zero. A backend that is a whole-host estimate is now billed through this crate's EstimatorMeter, which resolves a microjoule, and the bridge names itself `estimator` then. ProcessMeter (`server,metal`) sealed ~1.5–9.5 J [Estimator] per request for ~11 µs of engine work. The process CPU count does not advance over so short a serve, so ProcessSpanMeter falls back to the host meter, whose window opens before its first GPU-rail snapshot and closes after its last — 100 ms or more of the meter's own waiting at each end — and its load estimate billed that. A whole Estimator span (only that fallback gives one) is now billed this crate's estimate over the work's own window. The estimator's window for the rest of the host now opens after the span meter's starting readings, the same window as the CPU reading, instead of before its GPU snapshot wait. Measured here, one served request: hw-meter 0 → 103 µJ, hw-meter-apple 0 → 113 µJ, metal 9,480,000 → 168 µJ, all [Estimator]. Tests (each fails before): an_estimator_backend_never_bills_a_short_ span_nothing (50 spans of 50 µs over the family EstimationMeter); an_estimated_fallback_span_is_billed_the_works_own_window (energy_of over the fallback span seen here); serve_example asserts usage.energy.uj > 0 for every grade. The waits sit in jouleclaw-energy's ProcessSpanMeter (its fallback reads bracket the GPU snapshots); that is not changed here. Co-Authored-By: Claude Opus 5.5 <[email protected]>
140 lines
6.3 KiB
Rust
140 lines
6.3 KiB
Rust
//! The reference server example (`examples/serve.rs`) seals what its meter
|
|
//! read, never a fixture's figure.
|
|
//!
|
|
//! Driven here through one real `/v1/chat/completions` request over a socket,
|
|
//! with the example's own meter and mesh (the example file is compiled into
|
|
//! this test as a module, so what is tested is what `cargo run --example
|
|
//! serve` runs). Its receipt's grade must be one this build's meter can give:
|
|
//! with no hardware meter feature, only `Estimator`, with no measured figure.
|
|
//! A hardware figure must have been read: with `metal` (this process's own
|
|
//! counters) it must lie within this process's CPU energy read around the
|
|
//! request by a reader that shares no code with the meter — the kernel's
|
|
//! `ri_energy_nj`, read raw. A fixture that steps a fixed 4.8 J per snapshot
|
|
//! and calls it `HwShunt` fails both: its grade in a build without a
|
|
//! hardware meter, and the bound in a build with one (a CPU-only serve of 16
|
|
//! tokens is microjoules to millijoules here, nowhere near joules).
|
|
//!
|
|
//! And in every branch the request is billed something: a served request
|
|
//! ran, so a figure of 0 µJ — an estimator too coarse to see a span shorter
|
|
//! than its resolution — bills real work nothing.
|
|
|
|
#[allow(dead_code)]
|
|
#[path = "../examples/serve.rs"]
|
|
mod serve;
|
|
|
|
use std::io::{Read, Write};
|
|
use std::net::{SocketAddr, TcpListener, TcpStream};
|
|
use std::sync::mpsc;
|
|
use std::thread;
|
|
|
|
use serde_json::Value;
|
|
|
|
#[cfg(target_os = "macos")]
|
|
unsafe extern "C" {
|
|
fn proc_pid_rusage(pid: i32, flavor: i32, buffer: *mut std::ffi::c_void) -> i32;
|
|
}
|
|
|
|
/// This process's CPU energy, nJ: `ri_energy_nj` of `rusage_info_v6`, read
|
|
/// here into a raw buffer at the SDK's offset (336), not through any meter.
|
|
/// `None` off macOS, where no such per-process count exists.
|
|
fn cpu_nj() -> Option<u64> {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let mut words = [0u64; 464 / 8];
|
|
let rc = unsafe { proc_pid_rusage(std::process::id() as i32, 6, words.as_mut_ptr().cast()) };
|
|
assert_eq!(rc, 0, "proc_pid_rusage");
|
|
Some(words[336 / 8])
|
|
}
|
|
#[cfg(not(target_os = "macos"))]
|
|
{
|
|
None
|
|
}
|
|
}
|
|
|
|
/// POST one chat completion and return the response's JSON body.
|
|
fn post(addr: SocketAddr) -> Value {
|
|
let body = r#"{"model":"gpt-mini","messages":[{"role":"user","content":"hi"}],"max_tokens":16}"#;
|
|
let mut conn = TcpStream::connect(addr).expect("connect");
|
|
let req = format!(
|
|
"POST /v1/chat/completions HTTP/1.1\r\nHost: x\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
|
body.len(),
|
|
body
|
|
);
|
|
conn.write_all(req.as_bytes()).expect("send");
|
|
let mut raw = String::new();
|
|
conn.read_to_string(&mut raw).expect("read");
|
|
assert!(raw.starts_with("HTTP/1.1 200 OK"), "status line: {}", &raw[..raw.len().min(80)]);
|
|
serde_json::from_str(raw.split("\r\n\r\n").nth(1).expect("response body")).expect("JSON body")
|
|
}
|
|
|
|
#[test]
|
|
fn the_reference_server_seals_what_its_meter_read() {
|
|
let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
|
|
let addr = listener.local_addr().expect("addr");
|
|
let (built, meter_built) = mpsc::channel();
|
|
// The server lives in one thread, as `main` runs it: the meter, the mesh
|
|
// and its receipt chain are built and stay there.
|
|
let server = thread::spawn(move || {
|
|
let meter = serve::meter();
|
|
built.send(meter.name().to_string()).expect("report the meter");
|
|
let mut mesh = serve::mesh(meter.as_ref());
|
|
mesh_openai::server::serve_n(&listener, &mut mesh, &serve::ByteBackend, 100_000_000, 1).expect("serve");
|
|
});
|
|
// Building the meter runs its liveness probes (a third of a second of CPU
|
|
// and a GPU load, for `ProcessMeter`): wait for it, so the window below
|
|
// holds the request and not the probes.
|
|
let meter = meter_built.recv().expect("the server built its meter");
|
|
|
|
let before = cpu_nj();
|
|
let v = post(addr);
|
|
let around_uj = cpu_nj().zip(before).map(|(after, before)| (after - before) / 1_000);
|
|
server.join().expect("server thread");
|
|
|
|
let energy = &v["usage"]["energy"];
|
|
let receipt = &energy["receipt"];
|
|
let provenance = energy["provenance"].as_str().expect("usage.energy.provenance");
|
|
let measured = receipt.get("measured_uj").and_then(Value::as_u64);
|
|
eprintln!(
|
|
"serve example: meter {meter}; usage.energy {} µJ [{provenance}]; sealed {:?} measured {measured:?} estimated {}; \
|
|
this process's CPU energy around the request {around_uj:?} µJ",
|
|
energy["uj"], receipt["energy_provenance"], receipt["estimated_uj"]
|
|
);
|
|
|
|
assert_eq!(receipt["mesh_receipt"], "mesh/1");
|
|
let uj = energy["uj"].as_u64().expect("usage.energy.uj");
|
|
assert!(uj > 0, "a served request was billed 0 µJ [{provenance}] by meter {meter}: {energy}");
|
|
assert_eq!(
|
|
receipt["energy_provenance"].as_str(),
|
|
Some(provenance),
|
|
"the plaintext grade and the sealed one differ: {energy}"
|
|
);
|
|
match provenance {
|
|
"Estimator" => assert_eq!(measured, None, "an estimate sealed with a measured figure: {receipt}"),
|
|
"HwShunt" => {
|
|
if !cfg!(feature = "hw-meter") {
|
|
panic!("a build with no hardware meter sealed a hardware reading, which nothing read: {receipt}");
|
|
}
|
|
let uj = measured.expect("a HwShunt figure carries its measured figure");
|
|
assert!(uj > 0, "a served span read no energy: {receipt}");
|
|
// Only this process's own counters bound it by this process's
|
|
// energy; a machine counter reads the whole host.
|
|
if cfg!(feature = "metal") {
|
|
let around = around_uj.expect("macOS reads this process's CPU energy");
|
|
assert!(
|
|
uj <= around + 1,
|
|
"the receipt read {uj} µJ, more than this process's {around} µJ of CPU energy around a CPU-only serve: \
|
|
a figure nothing read"
|
|
);
|
|
}
|
|
}
|
|
"ModelBased" => {
|
|
if !cfg!(feature = "hw-meter") || cfg!(feature = "metal") {
|
|
panic!("no meter of this build grades a span by a model: {receipt}");
|
|
}
|
|
}
|
|
other => panic!("an unknown grade `{other}`: {receipt}"),
|
|
}
|
|
if !cfg!(feature = "hw-meter") {
|
|
assert_eq!(provenance, "Estimator", "a build with no hardware meter can only estimate: {receipt}");
|
|
}
|
|
}
|