pool: a standard for energy-accounted multi-tenant scheduling #323
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/pool-energy-accounted-scheduling"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The first slice of an energy-native scheduling layer: a new standard,
pool/1.Why this exists
A cluster scheduler decides who gets contended capacity, and denominates that entitlement in devices — whole accelerators, fractions of one, or a share of wall clock. Devices are a proxy. The quantity actually consumed, paid for, and reported under an energy-disclosure regime is energy, and a device-count quota cannot express "this team may spend 40 MJ this window". The budget that matters ends up enforced nowhere and reconciled after the fact, if at all.
I surveyed our own tree before proposing anything:
over-quotaappears in zero files,fair-sharein zero, gang scheduling in one (incidental), and the 65evicthits are all cache eviction. EOC's two schedulers are work-item routing — work-stealing with joule-weighted placement, and carbon-region shifting — not multi-tenant entitlement. So the gap was on both sides.What
pool/1isAn entitlement guarantees microjoules and devices together, and both bind. Three rules carry it, and each needs the energy unit to state at all:
Estimator— nobody measured or modelled it — is admitted only inside its own guarantee, never against surplus borrowed from a sibling. A tenant may bear its own mis-estimate; a sibling's unspent joules are not the tenant's to gamble, and an unobserved estimate is the figure most likely to be wrong in the direction that overcommits.Rule 3 is why the energy unit is load-bearing rather than decorative: it ranks by cost per completed unit, which needs an outcome anchor. A scheduler that knows only device-seconds cannot express that ordering at all.
Two family disciplines, designed in rather than retrofitted
uj_per_completed_unitreturnsNonewhen nothing has completed (§5.1) — not zero, which would make a freshly started workload unreclaimable, and not infinity, which would make every new workload the first victim and destroy exactly the work that would have produced the measurement.Admission order is normative (§4) because a different order yields a different verdict: impossibility first, then devices, then the guarantee, then borrowing. Surplus is shared by weight among every borrower under one parent — so a tenant cannot take it all by claiming first — and never crosses roots.
Scope, stated as a limit
pool/1decides admission and preemption order. It does not place, meter or execute (§1.1), and a verdict is only as sound as the figure handed to it — which is why the grade travels into the verdict rather than sitting beside it. §7 says what it cannot do: reservation bounds what may be started, not what is ultimately spent, and nothing here detects a meter that lies about its own provenance.Tests and falsification
10 tests, including the discriminating pair for rule 1: the same claim, the same surplus available, differing only in whether anyone measured the estimate — one admitted borrowing 5 MJ, one queued.
a_guess_may_not_spend_a_neighbours_surplusFAILEDpreemption_follows_evidence_then_declarationFAILEDa_gang_is_tested_whole_not_per_memberFAILEDsurplus_is_shared_by_weightFAILEDEach rule is independently pinned. P2 is the one worth noting: it is the family-4 error ("unknown conflated with a value") applied to a brand-new mechanism, and the test catches it.
Verification
cargo check --workspace --all-targets(pool-rs) — exit 0cargo test --workspace— exit 0, 10 passedcargo metadata --locked— exit 0check-lockfiles— 39 lockfiles, all current (was 38)check-spec-refs— 16 standards, 1613 citations, all resolvecheck-comparative-prose— clean, 2332 files;check-no-vanity-metrics— clean, 281 filescheck-tests-in-ci— clean; thepool-rsmatrix row is added tostandards-build.ymlon thelinuxrunner, so the workspace is actually built rather than merely presentWhat this slice does not include
Placement, topology-awareness, fractional-device enforcement, admission-time workload capture, a multi-cluster control plane, and reclamation of idle workloads. Those are the next slices. This one is the entitlement model, because everything else hangs off it and it is the piece that has no analogue anywhere in joule terms.
Second slice: usage survives the process (SPEC §6)
A scheduler that restarts with empty usage hands every tenant its whole guarantee back and admits the next claim against a budget already spent. That exact defect has been found and fixed twice elsewhere in this family this week — the JCP gateway refilled every grant on restart (#305), and the JouleHook gate's budget died with each per-action process so "over budget is deny" could never fire (#310).
pool/1carries the replay from the start rather than acquiring it after someone notices.Admittedreserves the estimate;Releasedlifts the hold and charges the actual figure in its place, not in addition to it — the hold bounded what could start, and what the tenant spent is what it spent. AQueueor aDenyreserves nothing and is not recorded; that would make this a decision log, a different artifact with a different retention story.What §6.5 refuses to claim
The chain is tamper-evident as a sequence. It is not authenticated — nothing in v0.1 signs an entry, so a party able to rewrite the whole stream can rewrite a consistent one. The signed
pool/1receipt (JCR-1 canonical CBOR under COSE_Sign1, as the rest of the family seals) is a later slice, and until it lands §6.5 forbids an implementation from calling a ledger entry sealed, signed or non-repudiable. The type is namedLedger, notReceipt, for the same reason: the word would claim the half that is missing.Falsification, second slice
a_restart_does_not_refill_a_spent_guarantee+ 1 FAILEDa_release_of_a_claim_never_admitted_is_refusedFAILEDa_resumed_ledger_continues_the_chainFAILEDHeadline test is the discriminating pair this family now uses for the defect: the same tenant with its guarantee spent — replayed, still queued; not replayed, admitted.
Conformance gains four duties (§7.10–13); the honest limit moves to §8 and the two citations pointing at the old number are updated, which
check-spec-refswould have caught anyway.Totals now: 17 tests,
check-spec-refs1614 citations across 16 standards, 40 lockfiles current,cargo metadata --lockedexit 0, comparative-prose and vanity-metrics clean.Third slice: entries are sealed
pool/1JCR-1 receipts (SPEC §6.5–6.6)§6.5 previously said what the ledger was not — tamper-evident as a sequence, unauthenticated, with the spec forbidding the word "sealed". That slice lands here and §6.5 now says what an entry is: canonical det-CBOR (RFC 8949 §4.2.1) inside a COSE_Sign1 envelope (RFC 9052, EdDSA/Ed25519,
alg = -8), the steward askid— the wire format the rest of this family seals with, so apool/1receipt verifies under a tool that has never heard of Pool.The JCR-1 primitives are vendored, which is what
mesh-receiptdocuments doing for the same reason. Vendoring also kept the dependency honest:mesh-receiptreaches intojcp-schemafor the decoder, and taking that dep here would have contradicted the "self-contained" rationale in the header I had just written — so the decoder is written against JCR-1's own four-type grammar instead.Authenticity and sequence are checked separately, because they fail separately.
a_removed_entry_breaks_the_chain_though_each_entry_is_still_signeddeletes an entry, asserts the survivors still verify individually, and asserts the stream is refused anyway. Signatures without a chain are a bag of receipts, not a ledger.§6.6, canonicity is checked rather than assumed. A signature covers exact bytes, so accepting a non-canonical encoding of the same document would let two byte strings share one signature. The decoder re-encodes what it decoded and refuses any difference.
Falsification, and what it caught in my own tests
S4 and S5 initially changed no outcome. Both inputs are also caught by the §6.6 canonicity check, so
is_err()passed with the guard gone and pinned nothing — my own tests asserted "refused somehow" rather than "refused for this reason". Each now matches the specificMalformedCorereason, and re-falsification shows both mutations failing their test. The guards stay rather than being deleted as redundant: they give a precise reason where the canonicity check gives a general one, and a verifier's diagnostics are part of what makes a refusal actionable.The limit this does not remove
§8 gains it explicitly: the steward signs its own decisions. A ledger is self-attested by default — it proves this steward wrote this sequence, not that the decisions were right or that a second party saw them. The family already holds the shape of the answer elsewhere (a meter attestation signed by a key distinct from the receipt signer);
pool/1has no analogue for the decision dimension yet. Stated in the spec rather than left for a reader to find.Totals: 31 tests, conformance §7.1–14,
check-spec-refs1614 citations / 16 standards, 40 lockfiles current,cargo metadata --locked0, comparative-prose and vanity-metrics clean.Fourth slice: a witness distinct from the steward (SPEC §6.7–6.8)
§8 named the limit the third slice left behind: the steward signs its own decisions. A ledger proves this steward wrote this sequence — not that the decisions were right, and not that anyone else saw them. A party able to rewrite the whole stream can rewrite a consistent one, because it holds the only key involved.
A witness is a party holding a key the steward does not. It signs the head at a stated sequence, producing a
pool-witness/1receipt: a statement that the chain reached exactly that head at exactly that length. A steward that later rewrites its history produces a different head, and the receipt stops covering it.SelfAttestedrather than rejected — the shape this family already uses where a figure resolves to no independent attestation. What an implementation may not do is report the two as the same thing.verifyreturnsWitnessIsTheStewardrather than reporting a witnessed ledger: a countersignature from the party being countersigned establishes nothing, and would be the most misleading possible reading of the word.Falsification, fourth slice
the_steward_cannot_witness_itselfFAILEDa_rewritten_history_is_no_longer_coveredFAILEDa_receipt_for_another_steward_is_refusedFAILEDself_attested_is_a_grade_not_an_errorFAILEDEach rule is pinned by exactly one test, and the headline test is the one the slice exists for: a steward rewrites its history, every surviving entry still verifies individually, and the witness's receipt no longer covers the head.
Fifth slice: attributing a shared device's energy (SPEC §2.5)
Serving several workloads from one physical accelerator is how a scheduler stops stranding capacity, and the memory fence that bounds each tenant is enforceable because memory is partitioned. Energy is not. A device counter reports one figure for the whole device and has no notion of which tenant caused which joule — so a per-tenant figure is a split of a measurement, not a measurement. Two rules follow, and
shareexists to make them hold rather than to be remembered:HwShuntdevice total becomesModelBasedthe moment two or more occupants divide it, because the division is a model however good the counter was. It never improves — dividing anEstimatorguess does not make it a model — and dividing by one is not a division, so a sole occupant keeps the device's own grade. An implementation may not report a shared device's per-tenant figure asHwShunt: that grade asserts a counter was read for this work, and on a shared device none was.u128so a wide device total cannot overflow the weighting. Conservation is what makes a tenant's figure reconcilable against the device that produced it at all.Attribution refuses a device with no occupants, or one whose occupants all carry zero weight: there is then no basis to divide on, and discarding the figure would lose energy the counter really read. The weight basis stays the operator's to choose (allotted memory, time slices, partitions) —
pool/1requires only that one basis covers every occupant of one device and that the result is graded as the model it is.Falsification, fifth slice — and the decoration it found
provenance_degrades_on_division_but_not_on_a_sole_occupant+ 2 FAILEDthe_shares_sum_to_the_measurement_exactly+ 2 FAILEDthe_leftover_is_distributed_deterministicallyFAILEDreconciling_across_devices_takes_the_weakest_gradeFAILEDa_device_with_nobody_on_it_and_a_weightless_set_are_refusedFAILEDM4 is the one worth reading.
reconcilefolds shares back to a total and keeps the weaker grade, because a sum is no better graded than its worst term. My first test for that fed it shares that all carried the same grade — sominandmaxwere indistinguishable and the test pinned nothing. Reconciling across devices is where the grade actually matters (one device read by a counter, another only modelled), and that case is reachable, so it now has a test of its own. Note that the original same-grade test does not appear in M4's failure list: only the new one catches it.Verification after the fifth slice
cargo check --workspace --all-targets --locked(pool-rs) — exit 0cargo test --workspace --locked— exit 0, 51 passedcargo clippy --workspace --all-targets --no-deps --locked— cleancargo metadata --locked— exit 0check-spec-refs— 1614 citations across 16 standards, all resolvecheck-lockfiles— 40 lockfiles, all currentcheck-tests-in-ci— every test file is run by a job, and none can run emptycheck-comparative-prose— clean (2332 files);check-no-vanity-metrics— clean (281 files)Conformance now runs §7.1–20. Still out of scope and named as such in §1.1 and §8: placement and topology-awareness, admission-time workload capture, idle reclamation, and a multi-cluster control plane.
Sixth slice: a guess earns its way out (SPEC §2.6)
Rule 1 refuses a guess the right to spend a neighbour's capacity. Reviewing the branch as a whole, that rule had no exit: a tenant nobody has measured can never be measured, because it can never run against surplus. As written it was a permanent penalty, not an incentive.
The evidence was already in the ledger — every completed claim recorded what it actually consumed, at the grade of whatever read it. What was missing was what the reservation was for, so
Event::Admittednow records the claim's class: the operator's assertion that two claims are the same kind of work. It goes in the entry rather than in a scheduler-side map because §6.1 makes the ledger the only durable state, and such a map is exactly the second store that can drift from it.observe::deriveturns that history into an estimate under four rules:Estimatorobservations are excluded, not averaged in. Counting them would launder guesses into a grade asserting they were observed — and would make the exit circular, since a tenant would earn an observed estimate merely by having run. It earns one by being measured.ModelBasedeven where every observation came from a hardware counter, because a figure for the next run is a prediction. This is the same capshareapplies to a divided device total, for the same reason. It is a ceiling, not a floor: modelled inputs give a modelled output.required: 0floors at one, so it can never mean "derive from nothing". The count is a policy value, and §2.6 says so rather than pretendingpool/1knows the right number for a workload it has never seen.The reservation takes the largest observed figure, not the mean: under-reserving is the failure rule 1 exists to prevent, and a mean under-reserves roughly half the time by construction. The median reported beside it is the lower middle value at an even count, so no reported figure is an average nothing measured. §2.6 states the limit — the maximum of a small sample underestimates the true tail, so this is better evidence than a guess, not a bound on what the work can cost.
ledger::observationsreads evidence back out of a verified stream. Only a matchedReleasedyields an observation: an open admission is a reservation, not a result, and reserving a figure is not evidence that the work costs it. The stream's first pass is now extracted asverified_eventsand shared withreplay— a second verification path is a second place for the checks to drift.Falsification, sixth slice
a_guess_is_not_evidence_however_much_of_it_there_isFAILEDa_derived_estimate_is_never_graded_as_a_readingFAILEDone_run_is_not_a_sample+ 3 FAILEDrequired: 0means derive from nothingzero_required_still_needs_one_observationFAILEDthe_reservation_takes_the_largest_not_the_mean+ 2 FAILEDan_empty_class_is_refused_rather_than_pooledFAILEDa_broken_stream_yields_no_evidence_at_allFAILEDan_open_admission_is_not_evidence+ 1 FAILEDO7 is worth a note on method: my first attempt at it did not compile, so it initially reported as surviving. A mutation that does not build tests nothing, and I rewrote it until it compiled before reading the result.
The headline test runs end to end through a sealed ledger: three measured
trainclaims leave the tenant inside its 5 MJ guarantee, the derivation produces 1 500 002 µJ from that history, and the same number of microjoules is then offered twice — queued as a guess, admitted by borrowing once it is derived from the ledger's own measured history. The figure comes out of the history, not out of the test.Totals after the sixth slice
cargo check --workspace --all-targets --locked— exit 0;cargo test --workspace --locked— exit 0, 63 passedcargo clippy --workspace --all-targets --no-deps --locked -- -D warnings— clean;cargo metadata --locked— exit 0check-spec-refs— 1619 citations across 16 standards, all resolvecheck-lockfiles— 40 lockfiles current;check-tests-in-ci— cleancheck-comparative-proseandcheck-no-vanity-metrics— cleanA side note on that spec-reference count. It had not moved across the two previous slices, which turned out to be because the guard only validates citations written as
SPEC §x.yin.rsfiles, and this crate's doc comments used a bare§. They were therefore unchecked. Rewritten into the validated form, the count moved 1614 → 1619 and all resolve — the citations are now actually verified rather than merely present.Still out of scope and named as such in §1.1 and §8: placement and topology-awareness, idle reclamation, and a multi-cluster control plane.
Seventh slice: reclaiming idle capacity without reclaiming on a missing number (SPEC §2.7)
A workload holding accelerators while consuming almost no energy is holding capacity it is not using. A device ledger cannot see that — a held device is a held device, saturated or parked at an interactive prompt — so this is a question only an energy-denominated scheduler can ask.
It is also the most dangerous question in this standard, because the obvious implementation destroys running work.
"this workload spent 0 µJ"arrives identically from a workload that is genuinely parked and from a meter that is not there:That is this family's "unknown conflated with a value" defect in its most destructive form, so the module is built around refusing it rather than acquiring the refusal later:
Estimatorfigure cannot establish idleness whatever its value, and the grade is tested before the value meets any threshold — so no threshold setting lets a guess through, including one high enough to catch everything. A workload is reclaimed on evidence that it is idle, never on the absence of evidence that it is busy.mean_uw()returnsNone, not zero, because zero is the one value that reads as idle.reclamation_order, not ranked last in it: it is not a worse candidate than an idle workload, it is not a candidate.The check order is normative: nothing held, then the window, then the grade, then the rate. A later check never rescues an earlier refusal.
Why there is no default threshold
A parked accelerator still draws power. A threshold has to sit above the hardware's idle draw or nothing is ever idle, and below its working draw or everything is — and both are properties of the device, not of this standard.
pool/1declines to guess them, soIdlePolicy::default()reclaims nothing. That is deliberate: the failure of an unconfigured destructive action should be a missed optimisation, not destroyed work.This is also not preemption. §5.2 ranks who is spending badly under contention by cost per completed unit; this asks who is not spending at all. A workload burning energy while completing nothing is thrashing, not idle, and §5.2 is the question that catches it.
Falsification, seventh slice
a_guess_of_zero_is_not_idle_though_a_reading_of_zero_is+ 2 FAILEDno_floor_setting_lets_a_guess_be_reclaimed+ 2 FAILEDa_workload_younger_than_the_window_gets_no_verdict+ 1 FAILEDan_empty_window_has_no_rate_rather_than_a_rate_of_zeroFAILEDthe_unmeasured_are_absent_from_the_order_not_ranked_last+ 1 FAILEDa_workload_holding_nothing_is_not_a_candidateFAILEDthe_floor_is_exclusive_at_its_boundary+ 1 FAILEDthe_default_policy_reclaims_nothingFAILEDu64a_long_window_does_not_overflow_the_rateFAILEDR2 is the one worth reading, because it is the mutation a careless refactor would actually produce: the grade check is still present, just moved below the threshold comparison. The behaviour is then correct for every workload except the ones that matter, and
no_floor_setting_lets_a_guess_be_reclaimedis the test that pins the ordering rather than merely the presence of the check.As with O7 in the previous slice, R8's first form did not compile and so initially read as surviving. A mutation that does not build tests nothing; it was rewritten until it compiled before the result was read.
The headline test is the discriminating pair: the same workload, the same devices, the same window, the same figure of zero — reclaimable when a counter read it, protected when nobody did.
Totals, branch as it stands
cargo check --workspace --all-targets --locked— exit 0;cargo test --workspace --locked— exit 0, 74 passedcargo clippy --workspace --all-targets --no-deps --locked -- -D warnings— cleancargo doc --no-deps— clean; this slice also fixes three broken intra-doc links left by earlier ones (WitnessReceipt, thederivefunction/attribute ambiguity, and a public doc linking the private first pass)cargo metadata --locked— exit 0check-spec-refs— 1621 citations across 16 standards, all resolvecheck-lockfiles— 40 lockfiles current;check-tests-in-ci— cleancheck-comparative-proseandcheck-no-vanity-metrics— cleanRemaining out of scope, and named as such in §1.1 and §8: placement and topology-awareness, and a multi-cluster control plane.
🤖 Generated with Claude Code
A workload holding accelerators while consuming almost no energy is holding capacity it is not using. A device ledger cannot see that — a held device is a held device, saturated or parked at a prompt — so idle reclamation is a question only an energy-denominated scheduler can ask. It is also the most dangerous question in this standard, because the obvious implementation destroys running work. "This workload spent 0 µJ" arrives identically from a parked workload and from a meter that is not there: meter unavailable → 0 µJ → "idle" → reclaim → running work destroyed That is this family's "unknown conflated with a value" defect in its most destructive form, so the module is built around refusing it: - **Unknown is not idle.** An `Estimator` figure cannot establish idleness whatever its value, and the grade is tested **before** the value meets any threshold — so no threshold setting lets a guess through, including one high enough to catch everything. A workload is reclaimed on evidence that it is idle, never on the absence of evidence that it is busy. - **Idleness is a rate over a stated window, never an instant.** Any workload is momentarily between steps. An observation shorter than the policy's window yields no verdict: startup is not idleness. An empty window yields no rate — `mean_uw()` returns `None`, not zero, because zero is the one value that reads as idle. - **Nothing held, nothing to reclaim.** - **Four verdicts, and only one permits reclamation.** A workload with no verdict is *absent* from `reclamation_order`, not ranked last in it: it is not a worse candidate than an idle workload, it is not a candidate. Check order is normative: nothing held, then the window, then the grade, then the rate. A later check never rescues an earlier refusal. `IdlePolicy::default()` reclaims **nothing**. A threshold must sit above the hardware's idle draw or nothing is ever idle, and below its working draw or everything is, and both are properties of the device rather than of this standard — so §2.7 declines to guess and makes the unconfigured failure "nobody reclaimed" rather than "everybody reclaimed". This is not preemption. §5.2 ranks who is spending badly under contention; this asks who is not spending at all. A workload burning energy while completing nothing is thrashing, not idle, and §5.2 is what catches it. Conformance gains §7.25–28. Also fixes three broken intra-doc links left by earlier slices (`WitnessReceipt`, the `derive` function/attribute ambiguity, and a public doc linking the private first pass), so `cargo doc` is clean. Co-Authored-By: Claude Opus 5 <[email protected]>