Skip to content

Commit dc86509

Browse files
committed
feat(cost): amortized fixed-cost attribution for shared pool runs (calque#102)
cost.Measured gains WarmHit (default false = today's dedicated-acquisition behavior, unchanged for every existing caller). Verdict now labels which fixed-cost regime produced a K — DEDICATED ACQUISITION (this run paid the full acquire+enter alone) vs WARM HIT (this run reused an already-loaded pool worker) — mirroring the existing occupancy-scope labeling discipline so a reader can't mistake one regime's fixed cost for the other's. internal/pool's Worker now captures whether the runner was already warm BEFORE each claim's drain (not after — DrainBatch always leaves it warm on a clean finish, which would make every claim after the pool's first look like a hit) and reports it in the completion summary via ResultWriter's new warmHit param, so a future submitter (#103) can set cost.Measured.WarmHit honestly per claim.
1 parent 4fbbb2b commit dc86509

5 files changed

Lines changed: 112 additions & 16 deletions

File tree

‎internal/cost/cost.go‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,22 @@ type Measured struct {
2626
// for before any work) + any warm-idle; part of the AWS rectangle (§8).
2727
AcquireSeconds float64
2828
EnterSeconds float64 // one-time warm @enter load (amortized across items)
29+
30+
// WarmHit is true when THIS run's AcquireSeconds/EnterSeconds reflect a
31+
// shared-pool warm hit (calque#100/#101's pool worker was already resident
32+
// and loaded — the caller reports near-zero fixed cost for this run) rather
33+
// than a dedicated acquire-and-load (the pool/session/real default: this run
34+
// paid the FULL fixed cost alone, per docs/pool-queue-contract.md). It does
35+
// not change how the model computes anything — AcquireSeconds/EnterSeconds
36+
// are used exactly as given either way — it only changes what the verdict
37+
// SAYS about what those numbers mean, so a reader doesn't assume a hit's
38+
// near-zero fixed cost is the STEADY STATE for every run against a shared
39+
// pool (most runs might hit; the one that triggers a cold load pays the
40+
// whole fixed cost alone, and reporting every run's K as if it were that one
41+
// misattributes cost, exactly as reporting every run's K as if it always hit
42+
// warm would understate it). Zero-value (false) is today's existing
43+
// dedicated-acquisition behavior — no change for any pre-#102 caller.
44+
WarmHit bool
2945
}
3046

3147
// SideCost is one side's total dollars at a given item count.
@@ -256,9 +272,24 @@ func (m *Model) Verdict(atItems int) (string, error) {
256272
fmt.Fprintf(&b, "NOTE: occupancy is a WHOLE-RUN mean (includes %s), so it\n"+
257273
" understates steady-state GPU fill and makes this K pessimistic for AWS.\n", load)
258274
}
275+
fmt.Fprintf(&b, "%s\n", m.warmHitLabel())
259276
return b.String(), nil
260277
}
261278

279+
// warmHitLabel names WHICH fixed-cost regime this K's AcquireSeconds/EnterSeconds
280+
// came from (calque#102), mirroring occScopeLabel's discipline: an amortized-cost
281+
// number is only auditable if the reader is told which regime produced it, not
282+
// left to assume every run against a shared pool costs the same as this one.
283+
func (m *Model) warmHitLabel() string {
284+
if m.M.WarmHit {
285+
return "Fixed cost regime: WARM HIT — this run reused an already-loaded pool worker " +
286+
"(near-zero acquire+enter); most runs against a healthy pool look like this, but the " +
287+
"run that triggers a cold load pays the full fixed cost alone (see a non-warm-hit K for that number)."
288+
}
289+
return "Fixed cost regime: DEDICATED ACQUISITION — this run paid the full acquire+enter cost alone " +
290+
"(today's default outside a shared pool; calque#100/#101)."
291+
}
292+
262293
// occScopeLabel renders the occupancy window for the verdict line.
263294
func (m *Model) occScopeLabel() string {
264295
switch m.M.OccupancyScope {

‎internal/cost/cost_test.go‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,44 @@ func TestVerdictLabelsOccupancyScope(t *testing.T) {
179179
t.Errorf("unlabeled occupancy must warn like whole_run does, got:\n%s", v3)
180180
}
181181
}
182+
183+
// TestVerdictLabelsFixedCostRegime proves K declares WHETHER its
184+
// AcquireSeconds/EnterSeconds came from a shared-pool warm hit or a dedicated
185+
// acquisition (calque#102) — the same near-zero fixed cost means two very
186+
// different things (steady state for most pool runs vs. this SPECIFIC run's
187+
// full cost) depending on which regime produced it, mirroring
188+
// TestVerdictLabelsOccupancyScope's occupancy-scope discipline.
189+
func TestVerdictLabelsFixedCostRegime(t *testing.T) {
190+
r := loadTestRates(t)
191+
base := Measured{
192+
CardAskedFor: "H100", InstanceUsed: "g7e.2xlarge",
193+
SecPerItem: 0.5, Occupancy: 0.95, SampleItems: 100,
194+
}
195+
196+
dedicated := base
197+
dedicated.AcquireSeconds, dedicated.EnterSeconds = 120, 30
198+
v, err := (&Model{Rates: r, M: dedicated}).Verdict(100000)
199+
if err != nil {
200+
t.Fatal(err)
201+
}
202+
if !strings.Contains(v, "DEDICATED ACQUISITION") {
203+
t.Errorf("default (WarmHit=false) K must label itself DEDICATED ACQUISITION, got:\n%s", v)
204+
}
205+
if strings.Contains(v, "WARM HIT") {
206+
t.Errorf("dedicated-acquisition K must NOT claim a warm hit, got:\n%s", v)
207+
}
208+
209+
warmHit := base
210+
warmHit.AcquireSeconds, warmHit.EnterSeconds = 0, 0 // this run reused an already-loaded worker
211+
warmHit.WarmHit = true
212+
v2, err := (&Model{Rates: r, M: warmHit}).Verdict(100000)
213+
if err != nil {
214+
t.Fatal(err)
215+
}
216+
if !strings.Contains(v2, "WARM HIT") {
217+
t.Errorf("WarmHit=true K must label itself WARM HIT, got:\n%s", v2)
218+
}
219+
if strings.Contains(v2, "DEDICATED ACQUISITION") {
220+
t.Errorf("warm-hit K must NOT claim a dedicated acquisition, got:\n%s", v2)
221+
}
222+
}

‎internal/pool/pool.go‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,13 @@ type ResultWriter interface {
7272
// batch — implementations key it by the manifest's own Bucket/ResultPrefix
7373
// so concurrent claims' results never collide.
7474
Sink(man calexec.Manifest) warm.Sink
75-
// WriteSummary persists the claim's completion record (failed indices) so
76-
// a submitter polling man.SummaryKey (via calexec.WaitForSummary, reused
77-
// unmodified) observes the claim as done.
78-
WriteSummary(ctx context.Context, man calexec.Manifest, failed []int) error
75+
// WriteSummary persists the claim's completion record (failed indices,
76+
// plus warmHit — calque#102: was the resident runner already warm and
77+
// loaded when this claim was served, or did this claim pay a fresh
78+
// acquire+@enter?) so a submitter polling man.SummaryKey (via
79+
// calexec.WaitForSummary, reused unmodified) observes the claim as done
80+
// AND knows which fixed-cost regime to feed cost.Measured.WarmHit with.
81+
WriteSummary(ctx context.Context, man calexec.Manifest, failed []int, warmHit bool) error
7982
}
8083

8184
// Queue is the slice of spawn's taskpool.Queue this package needs — an
@@ -201,7 +204,13 @@ func (w *Worker) runOne(ctx context.Context, ref ClaimRef, receipt string) {
201204
return
202205
}
203206

204-
if !w.Supervisor.IsWarm() {
207+
// Capture BEFORE DrainBatch: this is "was the runner already warm when THIS
208+
// claim arrived" (calque#102's WarmHit), not "is it warm now" (DrainBatch
209+
// always leaves it warm on a clean drain, which would make every claim
210+
// after the pool's first look like a hit regardless of whether IT paid a
211+
// reload after a crash).
212+
wasWarm := w.Supervisor.IsWarm()
213+
if !wasWarm {
205214
w.Supervisor.Config = warm.Config{EnterBody: man.EnterBody, MethodBody: man.MethodBody, MethodArg: man.MethodArg}
206215
}
207216
w.Supervisor.Sink = w.Results.Sink(man)
@@ -217,7 +226,7 @@ func (w *Worker) runOne(ctx context.Context, ref ClaimRef, receipt string) {
217226
return
218227
}
219228

220-
if serr := w.Results.WriteSummary(ctx, man, failed); serr != nil {
229+
if serr := w.Results.WriteSummary(ctx, man, failed, wasWarm); serr != nil {
221230
// Wrote results (if any landed before this point they're already in the
222231
// sink) but couldn't signal completion. Leave un-acked: a redelivery
223232
// re-drains against the (still warm, unaffected) resident runner and

‎internal/pool/pool_test.go‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,9 @@ type fakeResults struct {
115115
}
116116

117117
type writtenSummary struct {
118-
man calexec.Manifest
119-
failed []int
118+
man calexec.Manifest
119+
failed []int
120+
warmHit bool
120121
}
121122

122123
func (f *fakeResults) Sink(_ calexec.Manifest) warm.Sink {
@@ -127,13 +128,13 @@ func (f *fakeResults) Sink(_ calexec.Manifest) warm.Sink {
127128
return s
128129
}
129130

130-
func (f *fakeResults) WriteSummary(_ context.Context, man calexec.Manifest, failed []int) error {
131+
func (f *fakeResults) WriteSummary(_ context.Context, man calexec.Manifest, failed []int, warmHit bool) error {
131132
if f.writeErr != nil {
132133
return f.writeErr
133134
}
134135
f.mu.Lock()
135136
defer f.mu.Unlock()
136-
f.summaries = append(f.summaries, writtenSummary{man: man, failed: failed})
137+
f.summaries = append(f.summaries, writtenSummary{man: man, failed: failed, warmHit: warmHit})
137138
return nil
138139
}
139140

@@ -257,6 +258,15 @@ func TestWorker_StaysWarmAcrossClaims(t *testing.T) {
257258
if call != 2 {
258259
t.Errorf("second claim's result call=%d, want 2 (runner state did not persist across claims)", call)
259260
}
261+
// calque#102: the first claim pays the cold load (warmHit=false); the
262+
// second reuses the resident runner (warmHit=true) — this is the exact
263+
// signal a submitter needs to report cost.Measured.WarmHit honestly.
264+
if results.summaries[0].warmHit {
265+
t.Error("first claim's summary reports warmHit=true, want false (it triggered the cold load)")
266+
}
267+
if !results.summaries[1].warmHit {
268+
t.Error("second claim's summary reports warmHit=false, want true (it reused the resident runner)")
269+
}
260270
}
261271

262272
// TestWorker_MismatchedModelClaimIsAckedNotRun: a claim whose Model doesn't

‎internal/pool/s3.go‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,21 @@ func (r *S3Results) Sink(man calexec.Manifest) warm.Sink {
5454
}
5555

5656
// poolSummary is the completion record a pool claim writes — deliberately
57-
// smaller than cmd/warmd's full Summary (no occupancy/enter-seconds
58-
// bookkeeping here; those belong to the AMORTIZED cost-attribution work,
59-
// calque#102, tracked separately so this issue doesn't grow to cover it).
57+
// smaller than cmd/warmd's full Summary (no occupancy bookkeeping here). It
58+
// carries just enough for a submitter to feed cost.Measured honestly
59+
// (calque#102): WarmHit tells the submitter whether THIS claim's
60+
// AcquireSeconds/EnterSeconds should be reported as near-zero (a pool hit)
61+
// or the pool's own dedicated first-load cost (a miss) — the submitter,
62+
// not the worker, ultimately builds the cost.Model, since only the
63+
// submitter knows the run's item count and card-asked-for.
6064
type poolSummary struct {
61-
Failed []int `json:"failed"`
65+
Failed []int `json:"failed"`
66+
WarmHit bool `json:"warm_hit"`
6267
}
6368

6469
// WriteSummary implements ResultWriter.
65-
func (r *S3Results) WriteSummary(ctx context.Context, man calexec.Manifest, failed []int) error {
66-
body, err := json.Marshal(poolSummary{Failed: failed})
70+
func (r *S3Results) WriteSummary(ctx context.Context, man calexec.Manifest, failed []int, warmHit bool) error {
71+
body, err := json.Marshal(poolSummary{Failed: failed, WarmHit: warmHit})
6772
if err != nil {
6873
return err
6974
}

0 commit comments

Comments
 (0)