Skip to content

pricing/ec2.go silently fabricates GPU prices (4–12× low) for every modern accelerator family — estimator returns err==nil #29

Description

@scttfrdmn

Summary

pricing/ec2.go has no entries for any GPU family released since ~2024, and
estimatePriceByFamily covers the gap by silently inventing a price and
returning err == nil. For modern GPU instances the invented number is 4–12×
below the real rate, and for at least one type it fabricates a price where AWS
publishes none at all.

Found while building cultivar (a HuggingFace→AWS inference deployer whose entire
output is a cost recommendation). Verified live against the AWS Price List API in
account 942542972736, us-east-1, 2026-07-27.

Evidence

GetEC2HourlyRate("us-east-1", …) vs live Price List on-demand
(operatingSystem=Linux, tenancy=Shared, preInstalledSw=NA,
capacitystatus=Used):

instance type GetEC2HourlyRate real on-demand error
g7e.4xlarge $0.8000 $3.9982 5.0× low
p5.4xlarge $0.8000 $6.88 8.6× low
g6e.12xlarge $2.4000 $10.49 4.4× low
p6-b200.48xlarge $9.6000 $113.93 11.9× low
p5e.48xlarge $9.6000 no on-demand price exists fabricated

Reproducer:

import libpricing "github.com/spore-host/libs/pricing"

for _, t := range []string{"g7e.4xlarge", "p5.4xlarge", "p5e.48xlarge",
                           "p6-b200.48xlarge", "g6e.12xlarge"} {
    fmt.Printf("%-20s $%.4f/hr\n", t, libpricing.GetEC2HourlyRate("us-east-1", t))
}
// g7e.4xlarge          $0.8000
// p5.4xlarge           $0.8000
// p5e.48xlarge         $9.6000
// p6-b200.48xlarge     $9.6000
// g6e.12xlarge         $2.4000

Root cause

basePriceLarge in pricing/ec2.go has no g7, g7e, g6e, p5, p5e,
p6-b200, or p6-b300 key. estimatePriceByFamily (line 183) then does:

base, ok := basePriceLarge[family]
if !ok {
    base = 0.10          // <-- a GPU box priced like a t3
}
multiplier, ok := sizeMultiplier[size]
if !ok {
    multiplier = 2.0
}
return base * multiplier

0.10 × 8.0 = $0.80 for g7e.4xlarge. And staticOnDemandPricer documents the
consequence explicitly (truffle/pkg/aws/pricing.go:262):

// It never errors: unknown types fall through to a family-based estimate.

So the fabricated value is indistinguishable from a real one at every call site.

Why this is worse than a stale table

The estimate is structurally wrong for GPUs, not merely out of date. The
fallback extrapolates from vCPU count, but a GPU instance's price is dominated by
the accelerator, so no amount of table refreshing fixes the unknown-family path
— every future GPU generation will land on $0.10 × multiplier on the day it
launches. p6-b200.48xlarge is off by 11.9× today; a hypothetical p7 would be
off by more.

The p5e.48xlarge row is a distinct and arguably worse failure: AWS publishes
zero on-demand Price List rows for it (it is Capacity-Block-only — see
truffle#109). The correct answer is "there is no on-demand price"; returning
$9.60 means a caller will happily recommend and cost-model an instance that
cannot be purchased on demand at all.

Impact through truffle

truffle/pkg/aws/pricing.go:137 makes this the production default:

func newDefaultOnDemandPricer(cfg aws.Config) OnDemandPricer {
	return &fallbackPricer{
		primary:  newAWSOnDemandPricer(cfg),
		fallback: staticOnDemandPricer{},
	}
}

The fallback is well-intentioned (no creds, no network, emulator without a Price
List endpoint), but because it never errors, a throttled or credential-less run
degrades from "no price" to "confidently wrong price" — and truffle is the
pricing authority the rest of the suite consumes.

This is the fourth instance of the same pattern in the suite, in a third
location: spawn#447 (pkg/slurm hardcoded p4d/p5 prices, off 49% and 79%), the
hardcoded SageMaker premium, the hardcoded Capacity Block discount, and now this.
That recurrence is the actual argument for the fix below being structural rather
than another table refresh.

Suggested fix

  1. Make the unknown-family case fail, not guess. Return
    (0, ErrPriceUnknown) from estimatePriceByFamily when the family isn't in
    the table, and let staticOnDemandPricer propagate it. Callers can then
    render "unpriced" instead of a fake number. This is the load-bearing change;
    everything else is optional.
  2. Add the missing families: g7, g7e, g6e, p5, p5e, p5en, p6-b200, p6-b300.
    Happy to supply live-verified values for the US regions.
  3. Never estimate accelerator instances. Even with the table updated, gate
    the estimator so any family whose types carry a GPU/accelerator returns
    ErrPriceUnknown rather than a vCPU-derived guess.
  4. Represent "no on-demand price exists" distinctly from "we don't know" so
    Capacity-Block-only types like p5e.48xlarge can be reported honestly.
  5. Add a CI check that flags table entries older than N months, and a test
    asserting unknown families error rather than return a number.

If you'd prefer, I can send a PR for (1) + (3) — they're small and they're what
actually stops the silent-wrong-number class. (2) is a data refresh that will
drift again, which is why I'd rather not have it be the only fix.

Related: truffle#107, truffle#108, truffle#109, spawn#447.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions