#!/usr/bin/env python3
"""Own GPU vs pay-per-token: cost per million output tokens as a function of
utilization, and the utilization at which owning breaks even with an API.

Every input is a flag, so put your own numbers in:

  python3 breakeven.py --gpu-price 1800 --host-price 800 --years 3 \
      --kwh 0.38 --load-w 300 --idle-w 196 --tok-s 147 --api-out 1.70

--load-w and --idle-w are whole-machine watts at the wall (GPU + host). If
you only know the GPU figure from nvidia-smi, add the host: 60-100 W for a
desktop. --tok-s is the card's aggregate output rate at the concurrency
you actually run; measure it, do not take it from a spec sheet.

Written for https://paraloncloud.com/resources/own-gpu-vs-pay-per-token-break-even
"""
import argparse

p = argparse.ArgumentParser()
p.add_argument("--gpu-price", type=float, default=1800, help="USD, the card")
p.add_argument("--host-price", type=float, default=800, help="USD, the rest of the machine")
p.add_argument("--years", type=float, default=3, help="amortization period")
p.add_argument("--kwh", type=float, default=0.38, help="USD per kWh at the wall")
p.add_argument("--load-w", type=float, default=300, help="whole-machine watts while serving")
p.add_argument("--idle-w", type=float, default=196, help="whole-machine watts while idle but on")
p.add_argument("--tok-s", type=float, default=147, help="output tokens per second while serving")
p.add_argument("--api-out", type=float, default=1.70, help="API price, USD per 1M output tokens")
p.add_argument("--rent-hour", type=float, default=None, help="optional: rental price per hour for the same card")
a = p.parse_args()

hours = a.years * 365 * 24
amort = (a.gpu_price + a.host_price) / hours            # USD per hour, whether used or not
tok_per_hour = a.tok_s * 3600

def own_cost_per_M(util):
    """USD per 1M output tokens when the machine is busy `util` of the time and on 24/7."""
    power_kw = (util * a.load_w + (1 - util) * a.idle_w) / 1000
    per_hour = amort + power_kw * a.kwh
    return per_hour / (util * tok_per_hour) * 1e6

def breakeven_util():
    # per_hour(util) = api_out * util * tok_per_hour / 1e6  ->  linear in util, solve directly
    idle_cost = amort + a.idle_w / 1000 * a.kwh
    slope = a.api_out * tok_per_hour / 1e6 - (a.load_w - a.idle_w) / 1000 * a.kwh
    return idle_cost / slope if slope > 0 else None

print(f"amortization: ${amort:.3f}/h   output: {tok_per_hour/1e6:.3f}M tok/h at {a.tok_s} tok/s")
print(f"own machine at 100% load: ${amort + a.load_w/1000*a.kwh:.3f}/h   idle: ${amort + a.idle_w/1000*a.kwh:.3f}/h")
print(f"API: ${a.api_out:.2f} per 1M output tokens")
if a.rent_hour is not None:
    print(f"rented card, fully loaded: ${a.rent_hour / tok_per_hour * 1e6:.2f} per 1M output tokens")
print()
print("utilization   own $/1M out   vs API")
for u in (1.0, 0.75, 0.5, 0.25, 0.10, 0.05, 0.02):
    c = own_cost_per_M(u)
    print(f"{u*100:>9.0f}%   {c:>12.2f}   {'cheaper' if c < a.api_out else 'dearer'} ({c / a.api_out:.1f}x)")
be = breakeven_util()
print()
if be is None or be > 1:
    print("break-even: never (the API is cheaper even at 100% utilization)")
else:
    print(f"break-even utilization: {be*100:.0f}%  ({be*24:.1f} hours of full load per day)")
