| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- package inferencecost
- import (
- "math"
- "testing"
- )
- func defaultConfig() *Config {
- return &Config{
- AllocationMode: AllocationModeComputeTime,
- OutputTokenCostMultiplier: 2.5,
- }
- }
- func newCalc(cfg *Config) *Calculator { return NewCalculator(cfg) }
- // floatEq returns true if a and b differ by less than 1e-9.
- func floatEq(a, b float64) bool { return math.Abs(a-b) < 1e-9 }
- // ---- blended per-million-tokens ----
- func TestCalculator_BlendedCostPerMillionTokens(t *testing.T) {
- cfg := defaultConfig()
- m := &InferenceCost{
- AllocationTotalCost: 4.0,
- UsageTotalCost: 1.0,
- PromptTokens: 800_000,
- GenerationTokens: 200_000,
- TotalTokens: 1_000_000,
- EffectiveInputTokens: 800_000,
- // no timing data → multiplier fallback
- }
- newCalc(cfg).CalculateCosts([]*InferenceCost{m})
- if !floatEq(m.CostPerMillionTokens[CostBasisAllocation], 4.0) {
- t.Errorf("allocation blended want 4.0 got %f", m.CostPerMillionTokens[CostBasisAllocation])
- }
- if !floatEq(m.CostPerMillionTokens[CostBasisUsage], 1.0) {
- t.Errorf("usage blended want 1.0 got %f", m.CostPerMillionTokens[CostBasisUsage])
- }
- }
- func TestCalculator_BlendedZeroTokens(t *testing.T) {
- m := &InferenceCost{AllocationTotalCost: 1.0, UsageTotalCost: 0.5}
- newCalc(defaultConfig()).CalculateCosts([]*InferenceCost{m})
- if m.CostPerMillionTokens[CostBasisAllocation] != 0 {
- t.Error("expected zero blended cost when TotalTokens == 0")
- }
- }
- // ---- compute-time split ----
- func TestCalculator_ComputeTimeSplit_BothBases(t *testing.T) {
- cfg := defaultConfig()
- m := &InferenceCost{
- AllocationTotalCost: 4.0,
- UsageTotalCost: 1.0,
- PromptTokens: 600_000,
- GenerationTokens: 400_000,
- TotalTokens: 1_000_000,
- EffectiveInputTokens: 600_000, // no cache correction
- InputProcessingTime: 70.0,
- OutputProcessingTime: 30.0,
- }
- newCalc(cfg).CalculateCosts([]*InferenceCost{m})
- // inputFraction = 0.7, outputFraction = 0.3
- // usage: inputCost=0.7, outputCost=0.3
- wantUsageInput := 0.7 / 600_000 * 1_000_000
- wantUsageOutput := 0.3 / 400_000 * 1_000_000
- wantAllocInput := (4.0 * 0.7) / 600_000 * 1_000_000
- wantAllocOutput := (4.0 * 0.3) / 400_000 * 1_000_000
- if !floatEq(m.InputCostPerMillionTokens[CostBasisUsage], wantUsageInput) {
- t.Errorf("usage input want %f got %f", wantUsageInput, m.InputCostPerMillionTokens[CostBasisUsage])
- }
- if !floatEq(m.OutputCostPerMillionTokens[CostBasisUsage], wantUsageOutput) {
- t.Errorf("usage output want %f got %f", wantUsageOutput, m.OutputCostPerMillionTokens[CostBasisUsage])
- }
- if !floatEq(m.InputCostPerMillionTokens[CostBasisAllocation], wantAllocInput) {
- t.Errorf("alloc input want %f got %f", wantAllocInput, m.InputCostPerMillionTokens[CostBasisAllocation])
- }
- if !floatEq(m.OutputCostPerMillionTokens[CostBasisAllocation], wantAllocOutput) {
- t.Errorf("alloc output want %f got %f", wantAllocOutput, m.OutputCostPerMillionTokens[CostBasisAllocation])
- }
- if m.AllocationMethod != AllocationMethodComputeTime {
- t.Errorf("expected compute_time (no block size), got %s", m.AllocationMethod)
- }
- }
- func TestCalculator_ComputeTimeSplit_InputOutputSumToTotal(t *testing.T) {
- cfg := defaultConfig()
- m := &InferenceCost{
- AllocationTotalCost: 10.0,
- UsageTotalCost: 3.0,
- PromptTokens: 500_000,
- GenerationTokens: 500_000,
- TotalTokens: 1_000_000,
- EffectiveInputTokens: 500_000,
- InputProcessingTime: 60.0,
- OutputProcessingTime: 40.0,
- }
- newCalc(cfg).CalculateCosts([]*InferenceCost{m})
- // input_cost + output_cost must equal total for each basis.
- // Back-compute dollar amounts from the per-million rates using PromptTokens.
- for _, basis := range []CostBasis{CostBasisUsage, CostBasisAllocation} {
- var totalCost float64
- if basis == CostBasisUsage {
- totalCost = m.UsageTotalCost
- } else {
- totalCost = m.AllocationTotalCost
- }
- inputCost := m.InputCostPerMillionTokens[basis] / 1_000_000 * m.PromptTokens
- outputCost := m.OutputCostPerMillionTokens[basis] / 1_000_000 * m.GenerationTokens
- if !floatEq(inputCost+outputCost, totalCost) {
- t.Errorf("basis=%s: input+output=%.6f want %.6f", basis, inputCost+outputCost, totalCost)
- }
- }
- }
- // ---- KV cache savings fraction ----
- func TestCalculator_CacheSavingsFraction(t *testing.T) {
- cfg := &Config{
- AllocationMode: AllocationModeComputeTime,
- OutputTokenCostMultiplier: 2.5,
- }
- // 8 cached tokens out of 20 prompt tokens → 40% savings
- m := &InferenceCost{
- AllocationTotalCost: 1.0,
- UsageTotalCost: 1.0,
- PromptTokens: 20,
- GenerationTokens: 10,
- TotalTokens: 30,
- CachedTokens: 8,
- CacheConfigKnown: true,
- PrefixCachingEnabled: true,
- EffectiveInputTokens: 12,
- InputProcessingTime: 60,
- OutputProcessingTime: 40,
- }
- newCalc(cfg).CalculateCosts([]*InferenceCost{m})
- // inputCostPerMillionTokens uses PromptTokens (20) as denominator.
- wantInputCPM := (1.0 * 0.6 / 20) * 1_000_000
- got := m.InputCostPerMillionTokens[CostBasisUsage]
- if !floatEq(got, wantInputCPM) {
- t.Errorf("input CPM want %f got %f", wantInputCPM, got)
- }
- // CacheSavingsFraction = 8/20 = 0.4
- if !floatEq(m.CacheSavingsFraction, 0.4) {
- t.Errorf("CacheSavingsFraction want 0.4 got %f", m.CacheSavingsFraction)
- }
- // Method collapses to compute_time regardless of cache hits.
- if m.AllocationMethod != AllocationMethodComputeTime {
- t.Errorf("expected compute_time, got %s", m.AllocationMethod)
- }
- }
- func TestCalculator_CacheCorrection_Disabled_WhenConfigUnknown(t *testing.T) {
- cfg := &Config{
- AllocationMode: AllocationModeComputeTime,
- OutputTokenCostMultiplier: 2.5,
- }
- // CacheConfigKnown=false simulates vllm:cache_config_info being unavailable.
- m := &InferenceCost{
- AllocationTotalCost: 1.0,
- UsageTotalCost: 1.0,
- PromptTokens: 20,
- GenerationTokens: 10,
- TotalTokens: 30,
- CachedTokens: 0,
- CacheConfigKnown: false,
- EffectiveInputTokens: 20,
- InputProcessingTime: 60,
- OutputProcessingTime: 40,
- }
- newCalc(cfg).CalculateCosts([]*InferenceCost{m})
- if m.AllocationMethod != AllocationMethodComputeTime {
- t.Errorf("expected compute_time when cache config unknown, got %s", m.AllocationMethod)
- }
- wantInput := (1.0 * 0.6 / 20) * 1_000_000
- if !floatEq(m.InputCostPerMillionTokens[CostBasisUsage], wantInput) {
- t.Errorf("want %f got %f", wantInput, m.InputCostPerMillionTokens[CostBasisUsage])
- }
- if m.CacheSavingsFraction != 0 {
- t.Errorf("CacheSavingsFraction want 0 when config unknown, got %f", m.CacheSavingsFraction)
- }
- }
- func TestCalculator_PrefixCachingOff_WhenConfigKnownAndDisabled(t *testing.T) {
- cfg := &Config{
- AllocationMode: AllocationModeComputeTime,
- OutputTokenCostMultiplier: 2.5,
- }
- m := &InferenceCost{
- AllocationTotalCost: 1.0,
- UsageTotalCost: 1.0,
- PromptTokens: 100,
- GenerationTokens: 50,
- TotalTokens: 150,
- CachedTokens: 0,
- CacheConfigKnown: true,
- PrefixCachingEnabled: false, // explicitly disabled
- EffectiveInputTokens: 100,
- InputProcessingTime: 70,
- OutputProcessingTime: 30,
- }
- newCalc(cfg).CalculateCosts([]*InferenceCost{m})
- if m.AllocationMethod != AllocationMethodPrefixCachingOff {
- t.Errorf("expected prefix_caching_off, got %s", m.AllocationMethod)
- }
- }
- func TestCalculator_CacheCorrection_Disabled_WhenNoCacheHits(t *testing.T) {
- cfg := &Config{
- AllocationMode: AllocationModeComputeTime,
- OutputTokenCostMultiplier: 2.5,
- }
- m := &InferenceCost{
- AllocationTotalCost: 1.0,
- UsageTotalCost: 1.0,
- PromptTokens: 100,
- GenerationTokens: 50,
- TotalTokens: 150,
- CachedTokens: 0, // no hits in this window
- CacheConfigKnown: true,
- PrefixCachingEnabled: true, // caching is on, just no hits occurred
- EffectiveInputTokens: 100,
- InputProcessingTime: 70,
- OutputProcessingTime: 30,
- }
- newCalc(cfg).CalculateCosts([]*InferenceCost{m})
- if m.AllocationMethod != AllocationMethodComputeTime {
- t.Errorf("expected compute_time when prefix caching enabled but no hits in window, got %s", m.AllocationMethod)
- }
- }
- // ---- multiplier fallback ----
- func TestCalculator_MultiplierFallback_BothBases(t *testing.T) {
- cfg := &Config{
- AllocationMode: AllocationModeComputeTime,
- OutputTokenCostMultiplier: 2.5,
- }
- // No timing data → multiplier fallback
- m := &InferenceCost{
- AllocationTotalCost: 5.0,
- UsageTotalCost: 2.0,
- PromptTokens: 800_000,
- GenerationTokens: 200_000,
- TotalTokens: 1_000_000,
- EffectiveInputTokens: 800_000,
- }
- newCalc(cfg).CalculateCosts([]*InferenceCost{m})
- if m.AllocationMethod != AllocationMethodMultiplier {
- t.Errorf("expected multiplier method, got %s", m.AllocationMethod)
- }
- // weightedTokens = 800000 + 200000*2.5 = 1300000
- // usage: inputCPT = 2.0/1300000; inputCPM = inputCPT*1e6
- // alloc: inputCPT = 5.0/1300000
- for _, tc := range []struct {
- basis CostBasis
- totalCost float64
- }{
- {CostBasisUsage, 2.0},
- {CostBasisAllocation, 5.0},
- } {
- weighted := 800_000.0 + 200_000.0*2.5
- wantInput := (tc.totalCost / weighted) * 1_000_000
- wantOutput := wantInput * 2.5
- if !floatEq(m.InputCostPerMillionTokens[tc.basis], wantInput) {
- t.Errorf("basis=%s input want %f got %f", tc.basis, wantInput, m.InputCostPerMillionTokens[tc.basis])
- }
- if !floatEq(m.OutputCostPerMillionTokens[tc.basis], wantOutput) {
- t.Errorf("basis=%s output want %f got %f", tc.basis, wantOutput, m.OutputCostPerMillionTokens[tc.basis])
- }
- }
- }
- func TestCalculator_MultiplierFallback_ZeroTokens(t *testing.T) {
- m := &InferenceCost{AllocationTotalCost: 1.0, UsageTotalCost: 0.5}
- // EffectiveInputTokens and GenerationTokens are both 0
- cfg := &Config{AllocationMode: AllocationModeMultiplier, OutputTokenCostMultiplier: 2.5}
- newCalc(cfg).CalculateCosts([]*InferenceCost{m})
- if m.InputCostPerMillionTokens[CostBasisUsage] != 0 ||
- m.OutputCostPerMillionTokens[CostBasisAllocation] != 0 {
- t.Error("expected zero derived costs when tokens are zero")
- }
- }
- func TestCalculator_IncompleteTimingData_FallsBackToMultiplier(t *testing.T) {
- cfg := &Config{
- AllocationMode: AllocationModeComputeTime,
- OutputTokenCostMultiplier: 2.5,
- }
- m := &InferenceCost{
- AllocationTotalCost: 5.0,
- UsageTotalCost: 2.0,
- PromptTokens: 800_000,
- GenerationTokens: 200_000,
- TotalTokens: 1_000_000,
- EffectiveInputTokens: 800_000,
- InputProcessingTime: 60,
- OutputProcessingTime: 0, // incomplete timing data
- }
- newCalc(cfg).CalculateCosts([]*InferenceCost{m})
- if m.AllocationMethod != AllocationMethodMultiplier {
- t.Fatalf("expected multiplier fallback for incomplete timing data, got %s", m.AllocationMethod)
- }
- weighted := 800_000.0 + 200_000.0*2.5
- wantInput := (2.0 / weighted) * 1_000_000
- wantOutput := wantInput * 2.5
- if !floatEq(m.InputCostPerMillionTokens[CostBasisUsage], wantInput) {
- t.Errorf("usage input want %f got %f", wantInput, m.InputCostPerMillionTokens[CostBasisUsage])
- }
- if !floatEq(m.OutputCostPerMillionTokens[CostBasisUsage], wantOutput) {
- t.Errorf("usage output want %f got %f", wantOutput, m.OutputCostPerMillionTokens[CostBasisUsage])
- }
- }
|