calculator.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. package inferencecost
  2. import "github.com/opencost/opencost/core/pkg/log"
  3. // Calculator computes derived cost metrics for a slice of InferenceCost structs.
  4. type Calculator struct {
  5. config *Config
  6. }
  7. // NewCalculator creates a Calculator with the given config.
  8. func NewCalculator(config *Config) *Calculator {
  9. return &Calculator{config: config}
  10. }
  11. // CalculateCosts populates derived cost fields on each InferenceCost in-place.
  12. func (c *Calculator) CalculateCosts(metrics []*InferenceCost) {
  13. for _, m := range metrics {
  14. c.calculateModelCosts(m)
  15. }
  16. }
  17. func (c *Calculator) calculateModelCosts(m *InferenceCost) {
  18. m.CostPerMillionTokens = make(map[CostBasis]float64)
  19. m.InputCostPerMillionTokens = make(map[CostBasis]float64)
  20. m.OutputCostPerMillionTokens = make(map[CostBasis]float64)
  21. m.InputCost = make(map[CostBasis]float64)
  22. m.OutputCost = make(map[CostBasis]float64)
  23. // Usage cost requires evidence of actual token processing. Without tokens,
  24. // the pod was provisioned but idle: there is no active compute to charge for.
  25. if m.TotalTokens == 0 {
  26. m.UsageTotalCost = 0
  27. }
  28. // Blended cost per million tokens (all delivered tokens, including cached).
  29. // Uses TotalTokens — answers "average cost per delivered token".
  30. if m.TotalTokens > 0 {
  31. m.CostPerMillionTokens[CostBasisAllocation] = m.AllocationTotalCost / m.TotalTokens * 1_000_000
  32. m.CostPerMillionTokens[CostBasisUsage] = m.UsageTotalCost / m.TotalTokens * 1_000_000
  33. }
  34. // Case 1: no tokens or no cost — allocation method not applicable.
  35. if m.TotalTokens == 0 || (m.AllocationTotalCost == 0 && m.UsageTotalCost == 0) {
  36. m.AllocationMethod = ""
  37. return
  38. }
  39. // Cache savings fraction: fraction of prompt tokens served from KV cache.
  40. // Clamped to [0, 1]: vllm:prefix_cache_hits_total counts tokens retrieved
  41. // from cache per request, while vllm:prompt_tokens_total counts new input
  42. // tokens. In workloads with heavy prefix reuse (e.g. benchmarks), cached
  43. // tokens can exceed prompt tokens within a short window because cache hits
  44. // reflect prefixes established by earlier requests, including those outside
  45. // the current window. Values >1 before clamping indicate extreme cache reuse.
  46. if m.CacheConfigKnown && !m.PrefixCachingEnabled {
  47. m.CacheSavingsFraction = 0
  48. } else if m.PromptTokens > 0 {
  49. m.CacheSavingsFraction = min(m.CachedTokens/m.PromptTokens, 1.0)
  50. }
  51. // Input/output split — choose the allocation method.
  52. // Require both timing components to be present for compute-time allocation.
  53. // One-sided timing data is treated as incomplete and falls back to multiplier.
  54. hasCompleteTimingData := m.InputProcessingTime > 0 && m.OutputProcessingTime > 0
  55. if c.config.AllocationMode == AllocationModeComputeTime && hasCompleteTimingData {
  56. c.calculateComputeTimeSplit(m)
  57. } else {
  58. if c.config.AllocationMode == AllocationModeComputeTime && !hasCompleteTimingData {
  59. log.Debugf("InferenceCost: incomplete timing data for model %s/%s (input=%f output=%f), using multiplier fallback",
  60. m.Properties.ModelName, m.Properties.Namespace, m.InputProcessingTime, m.OutputProcessingTime)
  61. }
  62. c.calculateMultiplierSplit(m)
  63. }
  64. }
  65. // calculateComputeTimeSplit allocates costs proportionally by vLLM processing time.
  66. // Uses PromptTokens (delivered input tokens) as the input denominator.
  67. func (c *Calculator) calculateComputeTimeSplit(m *InferenceCost) {
  68. totalTime := m.InputProcessingTime + m.OutputProcessingTime
  69. if totalTime == 0 {
  70. // Timing data present but both zero — fall back.
  71. c.calculateMultiplierSplit(m)
  72. return
  73. }
  74. inputFraction := m.InputProcessingTime / totalTime
  75. outputFraction := 1 - inputFraction
  76. // Determine allocation method based on cache config.
  77. // Only set prefix_caching_off when the config was successfully retrieved
  78. // and explicitly indicates caching is disabled — not when the metric is absent.
  79. if m.CacheConfigKnown && !m.PrefixCachingEnabled {
  80. m.AllocationMethod = AllocationMethodPrefixCachingOff
  81. } else {
  82. m.AllocationMethod = AllocationMethodComputeTime
  83. }
  84. for _, basis := range []CostBasis{CostBasisUsage, CostBasisAllocation} {
  85. var totalCost float64
  86. if basis == CostBasisUsage {
  87. totalCost = m.UsageTotalCost
  88. } else {
  89. totalCost = m.AllocationTotalCost
  90. }
  91. inputCost := totalCost * inputFraction
  92. outputCost := totalCost * outputFraction
  93. m.InputCost[basis] = inputCost
  94. m.OutputCost[basis] = outputCost
  95. if m.PromptTokens > 0 {
  96. m.InputCostPerMillionTokens[basis] = inputCost / m.PromptTokens * 1_000_000
  97. }
  98. if m.GenerationTokens > 0 {
  99. m.OutputCostPerMillionTokens[basis] = outputCost / m.GenerationTokens * 1_000_000
  100. }
  101. }
  102. log.Debugf("InferenceCost: compute-time split model=%s/%s input=%.1f%% output=%.1f%% method=%s",
  103. m.Properties.ModelName, m.Properties.Namespace,
  104. inputFraction*100, outputFraction*100, m.AllocationMethod)
  105. }
  106. // calculateMultiplierSplit allocates costs using a fixed output/input ratio.
  107. // Uses EffectiveInputTokens for cost allocation; InputCostPerMillionTokens uses PromptTokens as denominator.
  108. func (c *Calculator) calculateMultiplierSplit(m *InferenceCost) {
  109. m.AllocationMethod = AllocationMethodMultiplier
  110. multiplier := c.config.OutputTokenCostMultiplier
  111. if multiplier <= 0 {
  112. multiplier = defaultOutputTokenCostMultiplier
  113. }
  114. // weightedTokens based on effective input tokens (cache-corrected).
  115. weightedTokens := m.EffectiveInputTokens + m.GenerationTokens*multiplier
  116. if weightedTokens == 0 {
  117. return
  118. }
  119. for _, basis := range []CostBasis{CostBasisUsage, CostBasisAllocation} {
  120. var totalCost float64
  121. if basis == CostBasisUsage {
  122. totalCost = m.UsageTotalCost
  123. } else {
  124. totalCost = m.AllocationTotalCost
  125. }
  126. inputCostPerToken := totalCost / weightedTokens
  127. inputCost := inputCostPerToken * m.EffectiveInputTokens
  128. outputCost := inputCostPerToken * multiplier * m.GenerationTokens
  129. m.InputCost[basis] = inputCost
  130. m.OutputCost[basis] = outputCost
  131. if m.PromptTokens > 0 {
  132. m.InputCostPerMillionTokens[basis] = inputCost / m.PromptTokens * 1_000_000
  133. }
  134. if m.GenerationTokens > 0 {
  135. m.OutputCostPerMillionTokens[basis] = outputCost / m.GenerationTokens * 1_000_000
  136. }
  137. }
  138. }