mesh-meter: a span shorter than its counter's minimum window is an estimate #630
3 changed files with 193 additions and 21 deletions
|
|
@ -297,6 +297,15 @@ impl ProcessSpanMeter {
|
|||
self.fallback.active_name()
|
||||
}
|
||||
|
||||
/// The fallback meter's minimum window: its active meter's
|
||||
/// [`crate::EnergyCounter::min_window_ns`]. A fallback figure over a span
|
||||
/// shorter than this is noise for that meter, so a caller that bills the
|
||||
/// span by it should treat it as no reading. Zero where it has no active
|
||||
/// meter.
|
||||
pub fn fallback_min_window(&self) -> std::time::Duration {
|
||||
std::time::Duration::from_nanos(self.fallback.active().map_or(0, |m| m.min_window_ns()))
|
||||
}
|
||||
|
||||
/// Take the span's starting readings: the GPU snapshot first, then the host
|
||||
/// meter, and the CPU reading last (the reverse in [`Self::finish`]). So
|
||||
/// the CPU time the snapshots spend — a wait for the GPU rail to
|
||||
|
|
|
|||
10
mesh/SPEC.md
10
mesh/SPEC.md
|
|
@ -335,6 +335,16 @@ IOReport), but a counter counts only while it is live:
|
|||
the whole request, such as a GPU counter on a host whose CPU has none, MUST NOT
|
||||
be graded `HwShunt`.
|
||||
|
||||
**A span shorter than its counter's minimum window.** A counter declares the
|
||||
smallest window over which its readings are not noise (JouleClaw's
|
||||
`min_window_ns`: about 10 ms for RAPL and IOReport, 100 ms for NVML). Across a
|
||||
shorter span its delta is whatever the counter's update and quantization gave
|
||||
— often zero, or a whole update period — not the span's energy. A span shorter
|
||||
than the minimum window of the counter that would grade it MUST NOT be graded
|
||||
`HwShunt` or `ModelBased` from that counter: it is an estimate of the request,
|
||||
`Estimator`, and SHOULD be billed by an estimator that resolves the span's own
|
||||
length, so that a short request is never billed zero.
|
||||
|
||||
### §3.6 Typed decisions — the other unit of work
|
||||
|
||||
A **decision model** takes state plus typed questions and returns a calibrated
|
||||
|
|
|
|||
|
|
@ -34,6 +34,14 @@
|
|||
//! span is graded as mesh grades an estimate of the whole request, `Estimator`
|
||||
//! with no `measured_uj`, and its figure is the counter's reading plus the
|
||||
//! bridge's own estimate of the rest of the host over the same window.
|
||||
//!
|
||||
//! A span shorter than the minimum window of the counter that would grade it
|
||||
//! (`min_window_ns`: about 10 ms for RAPL and IOReport, 100 ms for NVML) is not a
|
||||
//! reading of the span either: below that floor a counter's delta is whatever its
|
||||
//! updates and quantization gave. Such a span is graded `Estimator`, with no
|
||||
//! `measured_uj`, and billed this crate's estimator over the span's own window
|
||||
//! (SPEC §3.3), by [`HwMeter`] and by [`ProcessMeter`] where it falls back to the
|
||||
//! host meter.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
|
|
@ -110,13 +118,19 @@ impl<'m> Session<'m> {
|
|||
Open::Span(window) => return window.close(),
|
||||
Open::Snapshot(start) => start,
|
||||
};
|
||||
let end = self.meter.snapshot();
|
||||
let delta = end.uj.saturating_sub(start.uj);
|
||||
match end.provenance {
|
||||
Provenance::HwShunt => Energy::measured(delta),
|
||||
Provenance::ModelBased => Energy::modeled(delta),
|
||||
Provenance::Estimator => Energy::estimator(delta),
|
||||
}
|
||||
graded(start, self.meter.snapshot())
|
||||
}
|
||||
}
|
||||
|
||||
/// The energy between two snapshots, at the closing one's provenance: a
|
||||
/// `HwShunt` snapshot yields a measured figure, a `ModelBased` one a modeled
|
||||
/// one, an `Estimator` one an estimate.
|
||||
fn graded(start: Sample, end: Sample) -> Energy {
|
||||
let delta = end.uj.saturating_sub(start.uj);
|
||||
match end.provenance {
|
||||
Provenance::HwShunt => Energy::measured(delta),
|
||||
Provenance::ModelBased => Energy::modeled(delta),
|
||||
Provenance::Estimator => Energy::estimator(delta),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -246,12 +260,13 @@ impl Meter for SteppingMeter {
|
|||
#[cfg(feature = "hw-meter")]
|
||||
mod hw {
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::{EstimatorMeter, Meter, Sample};
|
||||
use super::{graded, EstimatorMeter, Meter, Sample, Window};
|
||||
use jouleclaw_energy::meter::{detect_meter, CompositeMeter};
|
||||
use jouleclaw_energy::EnergyDomain;
|
||||
use jouleclaw_energy::Provenance as JpProvenance;
|
||||
use mesh_schema::Provenance;
|
||||
use mesh_schema::{Energy, Provenance};
|
||||
|
||||
/// [`EstimatorMeter`]'s name: what [`HwMeter`] reports once it has fallen back.
|
||||
const FALLBACK_NAME: &str = "estimator";
|
||||
|
|
@ -284,6 +299,15 @@ mod hw {
|
|||
/// counter's figure plus this crate's [`EstimatorMeter`] for the rest of the
|
||||
/// host, so a span is billed what the counter read and an estimate of what it
|
||||
/// did not.
|
||||
///
|
||||
/// A span shorter than the active counter's minimum window
|
||||
/// ([`HwMeter::min_window`]) is not a reading of the span: below it the
|
||||
/// counter's delta is noise, often zero or a whole update period. The span is
|
||||
/// graded as an estimate of the request, `Estimator`, and billed this crate's
|
||||
/// [`EstimatorMeter`] over the span's own window, as a span across a failed
|
||||
/// read is. A [`super::Session`] opens the bridge's own window
|
||||
/// ([`Meter::open_span`]) so that the span is timed; a caller that subtracts
|
||||
/// [`Meter::snapshot`]s itself gets no such check.
|
||||
pub struct HwMeter {
|
||||
inner: CompositeMeter,
|
||||
fallback: EstimatorMeter,
|
||||
|
|
@ -333,6 +357,17 @@ mod hw {
|
|||
}
|
||||
}
|
||||
|
||||
/// The smallest span the active backend's figure stands for: its
|
||||
/// counter's `min_window_ns`. Zero where this crate's estimator bills the
|
||||
/// spans (the backend is a whole-host estimate, or a read has failed),
|
||||
/// since that resolves a microjoule.
|
||||
pub fn min_window(&self) -> Duration {
|
||||
if self.estimates || self.state().failed {
|
||||
return Duration::ZERO;
|
||||
}
|
||||
Duration::from_nanos(self.inner.active().map_or(0, |m| m.min_window_ns()))
|
||||
}
|
||||
|
||||
fn state(&self) -> MutexGuard<'_, Fallback> {
|
||||
self.state
|
||||
.lock()
|
||||
|
|
@ -417,6 +452,43 @@ mod hw {
|
|||
provenance: Provenance::Estimator,
|
||||
}
|
||||
}
|
||||
|
||||
/// A span on its own window, so that its length is judged against the
|
||||
/// backend's minimum window ([`HwWindow::close`]).
|
||||
fn open_span(&self) -> Option<Box<dyn Window + '_>> {
|
||||
let floor = self.min_window();
|
||||
let start = self.snapshot();
|
||||
let estimate_at = self.fallback.snapshot().uj;
|
||||
Some(Box::new(HwWindow { meter: self, start, estimate_at, floor, opened: Instant::now() }))
|
||||
}
|
||||
}
|
||||
|
||||
/// A span on an [`HwMeter`]: its opening snapshot, the estimator's figure at
|
||||
/// that point, the backend's minimum window then, and when the span opened.
|
||||
struct HwWindow<'m> {
|
||||
meter: &'m HwMeter,
|
||||
start: Sample,
|
||||
estimate_at: u64,
|
||||
floor: Duration,
|
||||
opened: Instant,
|
||||
}
|
||||
|
||||
impl Window for HwWindow<'_> {
|
||||
/// The span's energy as the bridge grades a snapshot pair — unless the
|
||||
/// span was shorter than the backend's minimum window, which makes it
|
||||
/// this crate's estimate of the span's own length. The length is taken
|
||||
/// from after the opening snapshot to before the closing one: a little
|
||||
/// shorter than the backend's own window, so a span at the floor is
|
||||
/// judged short rather than long.
|
||||
fn close(self: Box<Self>) -> Energy {
|
||||
let short = self.opened.elapsed() < self.floor;
|
||||
let end = self.meter.snapshot();
|
||||
let estimate = self.meter.fallback.snapshot().uj.saturating_sub(self.estimate_at);
|
||||
if short {
|
||||
return Energy::estimator(estimate);
|
||||
}
|
||||
graded(self.start, end)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -614,6 +686,7 @@ mod hw {
|
|||
reads: AtomicU64,
|
||||
domain: EnergyDomain,
|
||||
grade: JpProvenance,
|
||||
floor: Duration,
|
||||
}
|
||||
|
||||
impl EnergyCounter for Stepping {
|
||||
|
|
@ -627,7 +700,7 @@ mod hw {
|
|||
1
|
||||
}
|
||||
fn min_window_ns(&self) -> u64 {
|
||||
1
|
||||
self.floor.as_nanos() as u64
|
||||
}
|
||||
fn read(&self) -> Result<EnergyReading, EnergyError> {
|
||||
let n = self.reads.fetch_add(1, Ordering::SeqCst);
|
||||
|
|
@ -650,10 +723,51 @@ mod hw {
|
|||
}
|
||||
|
||||
fn stepping(domain: EnergyDomain, grade: JpProvenance) -> HwMeter {
|
||||
let backend = Stepping { reads: AtomicU64::new(0), domain, grade };
|
||||
stepping_with_floor(domain, grade, Duration::from_nanos(1))
|
||||
}
|
||||
|
||||
/// [`stepping`] with a minimum window of `floor`.
|
||||
fn stepping_with_floor(domain: EnergyDomain, grade: JpProvenance, floor: Duration) -> HwMeter {
|
||||
let backend = Stepping { reads: AtomicU64::new(0), domain, grade, floor };
|
||||
HwMeter::bridging(CompositeMeter::new(vec![Box::new(backend)]))
|
||||
}
|
||||
|
||||
/// A span shorter than the backend's minimum window is not a reading of
|
||||
/// it, whatever the counter's grade: it is graded `Estimator`, with no
|
||||
/// `measured_uj`, and billed the estimator over the span's own length,
|
||||
/// not the counter's delta. Past the floor the counter's figure stands.
|
||||
#[test]
|
||||
fn a_span_under_the_counters_minimum_window_is_an_estimate() {
|
||||
for grade in [JpProvenance::HwShunt, JpProvenance::ModelBased] {
|
||||
let m = stepping_with_floor(EnergyDomain::SocTotal, grade, Duration::from_millis(50));
|
||||
assert_eq!(m.min_window(), Duration::from_millis(50));
|
||||
let short = span(&m); // 20 ms
|
||||
assert_eq!((short.provenance, short.measured_uj), (Provenance::Estimator, None), "{grade:?}: {short:?}");
|
||||
assert!(short.check_provenance().is_ok());
|
||||
// 20 ms at the estimator's 15 W is ~300,000 µJ; the counter's STEP is not it.
|
||||
assert!(short.estimated_uj > STEP && short.estimated_uj < 2_000_000, "{grade:?}: {short:?}");
|
||||
|
||||
let s = Session::start(&m);
|
||||
std::thread::sleep(Duration::from_millis(60));
|
||||
let long = s.stop();
|
||||
assert_eq!(long.estimated_uj, STEP, "{grade:?}: past the floor the counter's figure stands: {long:?}");
|
||||
assert_ne!(long.provenance, Provenance::Estimator, "{grade:?}");
|
||||
assert!(long.check_provenance().is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
/// The bridge's minimum window is its counter's while the counter bills
|
||||
/// the spans, and zero once this crate's estimator does (after a failed
|
||||
/// read), since that resolves a microjoule.
|
||||
#[test]
|
||||
fn the_minimum_window_is_the_counters_until_the_bridge_estimates() {
|
||||
let m = bridge(|n| n >= 1);
|
||||
assert_eq!(m.min_window(), Duration::from_nanos(1));
|
||||
let _ = m.snapshot();
|
||||
let _ = m.snapshot(); // the second read fails
|
||||
assert_eq!(m.min_window(), Duration::ZERO, "the bridge now bills by its estimator");
|
||||
}
|
||||
|
||||
/// A live GPU counter's span is not a measurement of the request: mesh/1
|
||||
/// has no coverage field, so a HwShunt GPU figure would claim the CPU it
|
||||
/// did not read. It is graded as an estimate of the whole request —
|
||||
|
|
@ -799,7 +913,7 @@ pub use jouleclaw_energy::span::SpanUses;
|
|||
/// and GPU energy over the span ([`jouleclaw_energy::span::ProcessSpanMeter`]).
|
||||
#[cfg(feature = "hw-meter")]
|
||||
mod process {
|
||||
use jouleclaw_energy::span::{ProcessSpanMeter, SpanEnergy, SpanStart, SpanUses};
|
||||
use jouleclaw_energy::span::{ProcessSpanMeter, SpanEnergy, SpanSource, SpanStart, SpanUses};
|
||||
use jouleclaw_energy::{Coverage, EnergyError, Provenance as JpProvenance};
|
||||
use mesh_schema::Energy;
|
||||
|
||||
|
|
@ -830,7 +944,10 @@ mod process {
|
|||
/// that fallback is an estimate, the figure is this crate's
|
||||
/// [`EstimatorMeter`] over the work's own window, the estimator this crate
|
||||
/// bills the rest of a partial span with, so an estimated request is
|
||||
/// billed one way whichever path it took.
|
||||
/// billed one way whichever path it took. So it is where the span was
|
||||
/// shorter than the host meter's minimum window
|
||||
/// ([`ProcessSpanMeter::fallback_min_window`]): the host meter's figure over
|
||||
/// it is noise, not a reading of the span.
|
||||
pub struct ProcessMeter {
|
||||
span: ProcessSpanMeter,
|
||||
uses: SpanUses,
|
||||
|
|
@ -886,19 +1003,28 @@ mod process {
|
|||
impl Window for ProcessWindow<'_> {
|
||||
fn close(mut self: Box<Self>) -> Energy {
|
||||
let rest_uj = self.meter.rest.snapshot().uj.saturating_sub(self.rest_at);
|
||||
// Timed from the span's opening readings to before its closing ones:
|
||||
// a little shorter than the host meter's own window, so a span at the
|
||||
// floor is judged short rather than long.
|
||||
let host_floor = self.meter.span.fallback_min_window();
|
||||
let short = self.start.as_ref().is_some_and(|start| start.at().elapsed() < host_floor);
|
||||
let span = match self.start.take() {
|
||||
Some(start) => self.meter.span.finish(start, self.meter.uses),
|
||||
None => Err(EnergyError::NotCurrent),
|
||||
};
|
||||
energy_of(span, rest_uj)
|
||||
energy_of(span, rest_uj, short)
|
||||
}
|
||||
}
|
||||
|
||||
/// The `mesh/1` energy of a span: see [`ProcessMeter`] for the rule. Pure,
|
||||
/// so the tests drive every case. `rest_uj` is the estimator's figure for
|
||||
/// the host over the same window.
|
||||
pub(crate) fn energy_of(span: Result<SpanEnergy, EnergyError>, rest_uj: u64) -> Energy {
|
||||
/// the host over the same window; `short_for_host` says the span was shorter
|
||||
/// than the host meter's minimum window.
|
||||
pub(crate) fn energy_of(span: Result<SpanEnergy, EnergyError>, rest_uj: u64, short_for_host: bool) -> Energy {
|
||||
match span {
|
||||
// The host meter's figure over a span shorter than its floor is
|
||||
// noise: billed as this crate's estimate of the span's own length.
|
||||
Ok(s) if short_for_host && matches!(s.source, SpanSource::Fallback { .. }) => Energy::estimator(rest_uj),
|
||||
Ok(s) if s.coverage == Coverage::Whole => match s.provenance {
|
||||
JpProvenance::HwShunt => Energy::measured(s.uj),
|
||||
JpProvenance::ModelBased => Energy::modeled(s.uj),
|
||||
|
|
@ -944,28 +1070,55 @@ mod process {
|
|||
coverage: Coverage::Whole,
|
||||
source: SpanSource::Fallback { meter: "apple-silicon", why: "process CPU: did not advance".to_string() },
|
||||
});
|
||||
let e = energy_of(fallback, 169);
|
||||
let e = energy_of(fallback, 169, false);
|
||||
assert_eq!((e.provenance, e.measured_uj, e.estimated_uj), (Provenance::Estimator, None, 169));
|
||||
assert!(e.check_provenance().is_ok());
|
||||
}
|
||||
|
||||
/// A span shorter than the host meter's minimum window is not a reading
|
||||
/// of it, whatever the host meter's grade: a fallback figure over it is
|
||||
/// billed as this crate's estimate of the span's own length. A span that
|
||||
/// this process's own counters metered is not the host meter's, so the
|
||||
/// host meter's floor does not apply to it.
|
||||
#[test]
|
||||
fn a_fallback_span_under_the_host_meters_floor_is_an_estimate() {
|
||||
for grade in [JpProvenance::HwShunt, JpProvenance::ModelBased] {
|
||||
let fallback = || {
|
||||
Ok(SpanEnergy {
|
||||
uj: 2_000_000,
|
||||
provenance: grade,
|
||||
coverage: Coverage::Whole,
|
||||
source: SpanSource::Fallback { meter: "host", why: "process CPU: not live".to_string() },
|
||||
})
|
||||
};
|
||||
let short = energy_of(fallback(), 169, true);
|
||||
assert_eq!((short.provenance, short.measured_uj, short.estimated_uj), (Provenance::Estimator, None, 169));
|
||||
assert!(short.check_provenance().is_ok());
|
||||
let long = energy_of(fallback(), 169, false);
|
||||
assert_eq!(long.estimated_uj, 2_000_000, "{grade:?}: past the floor the host meter's figure stands");
|
||||
assert_ne!(long.provenance, Provenance::Estimator, "{grade:?}");
|
||||
}
|
||||
let own = energy_of(span(9_000, JpProvenance::HwShunt, Coverage::Whole), 40_000, true);
|
||||
assert_eq!((own.provenance, own.measured_uj), (Provenance::HwShunt, Some(9_000)), "not the host meter's span");
|
||||
}
|
||||
|
||||
/// Only a whole span keeps its hardware grade; a partial one is an
|
||||
/// estimate of the whole request, the reading plus the estimator's
|
||||
/// figure for the rest of the host; no figure is the estimator's.
|
||||
#[test]
|
||||
fn a_span_keeps_its_hardware_grade_only_when_it_covers_the_whole_request() {
|
||||
let whole = energy_of(span(9_000, JpProvenance::HwShunt, Coverage::Whole), 40_000);
|
||||
let whole = energy_of(span(9_000, JpProvenance::HwShunt, Coverage::Whole), 40_000, false);
|
||||
assert_eq!((whole.provenance, whole.measured_uj, whole.estimated_uj), (Provenance::HwShunt, Some(9_000), 9_000));
|
||||
assert!(whole.check_provenance().is_ok());
|
||||
|
||||
let partial = energy_of(span(9_000, JpProvenance::HwShunt, Coverage::Partial), 40_000);
|
||||
let partial = energy_of(span(9_000, JpProvenance::HwShunt, Coverage::Partial), 40_000, false);
|
||||
assert_eq!((partial.provenance, partial.measured_uj, partial.estimated_uj), (Provenance::Estimator, None, 49_000));
|
||||
assert!(partial.check_provenance().is_ok());
|
||||
|
||||
let modeled = energy_of(span(7_000, JpProvenance::ModelBased, Coverage::Whole), 40_000);
|
||||
let modeled = energy_of(span(7_000, JpProvenance::ModelBased, Coverage::Whole), 40_000, false);
|
||||
assert_eq!(modeled.provenance, Provenance::ModelBased);
|
||||
|
||||
let none = energy_of(Err(EnergyError::NotCurrent), 40_000);
|
||||
let none = energy_of(Err(EnergyError::NotCurrent), 40_000, false);
|
||||
assert_eq!((none.provenance, none.measured_uj, none.estimated_uj), (Provenance::Estimator, None, 40_000));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue