infersize 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- infersize/__init__.py +20 -0
- infersize/__main__.py +6 -0
- infersize/analytics/__init__.py +49 -0
- infersize/analytics/comparison.py +218 -0
- infersize/analytics/cost.py +259 -0
- infersize/analytics/report.py +275 -0
- infersize/analytics/vram.py +237 -0
- infersize/cli.py +838 -0
- infersize/hardware/__init__.py +18 -0
- infersize/hardware/pricing.py +81 -0
- infersize/hardware/specs.py +175 -0
- infersize/models/__init__.py +20 -0
- infersize/models/loader.py +225 -0
- infersize/models/specs.py +272 -0
- infersize-0.2.0.dist-info/METADATA +258 -0
- infersize-0.2.0.dist-info/RECORD +19 -0
- infersize-0.2.0.dist-info/WHEEL +4 -0
- infersize-0.2.0.dist-info/entry_points.txt +2 -0
- infersize-0.2.0.dist-info/licenses/LICENSE +201 -0
infersize/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""infersize: Analytical Performance Modeling & VRAM Capacity Planner for LLM Serving."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.2.0"
|
|
4
|
+
__author__ = "Prajwal Pujari"
|
|
5
|
+
|
|
6
|
+
from infersize.analytics.vram import VRAMEstimateResult, estimate_vram_usage
|
|
7
|
+
from infersize.hardware.specs import GPUProfile, get_gpu_profile, list_gpus
|
|
8
|
+
from infersize.models.specs import ModelArch, get_model_spec, list_models
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"__version__",
|
|
12
|
+
"ModelArch",
|
|
13
|
+
"GPUProfile",
|
|
14
|
+
"VRAMEstimateResult",
|
|
15
|
+
"estimate_vram_usage",
|
|
16
|
+
"get_model_spec",
|
|
17
|
+
"get_gpu_profile",
|
|
18
|
+
"list_models",
|
|
19
|
+
"list_gpus",
|
|
20
|
+
]
|
infersize/__main__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Analytics, VRAM capacity modeling, hardware comparisons, and latency estimations."""
|
|
2
|
+
|
|
3
|
+
from infersize.analytics.comparison import (
|
|
4
|
+
DEFAULT_COMPARISON_GPUS,
|
|
5
|
+
GPUComparisonItem,
|
|
6
|
+
GPUComparisonReport,
|
|
7
|
+
compare_gpus_for_workload,
|
|
8
|
+
)
|
|
9
|
+
from infersize.analytics.cost import (
|
|
10
|
+
CostEstimateResult,
|
|
11
|
+
MultiGPUCostReport,
|
|
12
|
+
compare_cloud_costs_for_workload,
|
|
13
|
+
estimate_serving_cost,
|
|
14
|
+
)
|
|
15
|
+
from infersize.analytics.report import (
|
|
16
|
+
ArchitectureReport,
|
|
17
|
+
generate_architecture_report,
|
|
18
|
+
)
|
|
19
|
+
from infersize.analytics.vram import (
|
|
20
|
+
VRAMEstimateResult,
|
|
21
|
+
calculate_activation_memory_gb,
|
|
22
|
+
calculate_kv_cache_bytes_per_token,
|
|
23
|
+
calculate_kv_cache_total_gb,
|
|
24
|
+
calculate_weight_memory_gb,
|
|
25
|
+
estimate_vram_usage,
|
|
26
|
+
find_max_concurrent_batch_size,
|
|
27
|
+
find_max_context_length,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"VRAMEstimateResult",
|
|
32
|
+
"calculate_kv_cache_bytes_per_token",
|
|
33
|
+
"calculate_weight_memory_gb",
|
|
34
|
+
"calculate_kv_cache_total_gb",
|
|
35
|
+
"calculate_activation_memory_gb",
|
|
36
|
+
"estimate_vram_usage",
|
|
37
|
+
"find_max_context_length",
|
|
38
|
+
"find_max_concurrent_batch_size",
|
|
39
|
+
"GPUComparisonItem",
|
|
40
|
+
"GPUComparisonReport",
|
|
41
|
+
"compare_gpus_for_workload",
|
|
42
|
+
"DEFAULT_COMPARISON_GPUS",
|
|
43
|
+
"CostEstimateResult",
|
|
44
|
+
"MultiGPUCostReport",
|
|
45
|
+
"estimate_serving_cost",
|
|
46
|
+
"compare_cloud_costs_for_workload",
|
|
47
|
+
"ArchitectureReport",
|
|
48
|
+
"generate_architecture_report",
|
|
49
|
+
]
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Analytical hardware comparison engine for multi-GPU evaluation and recommendation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from infersize.analytics.vram import (
|
|
10
|
+
VRAMEstimateResult,
|
|
11
|
+
estimate_vram_usage,
|
|
12
|
+
find_max_concurrent_batch_size,
|
|
13
|
+
find_max_context_length,
|
|
14
|
+
)
|
|
15
|
+
from infersize.hardware.specs import GPU_PRESETS, GPUProfile
|
|
16
|
+
from infersize.models.specs import ModelArch
|
|
17
|
+
|
|
18
|
+
StatusVerdict = Literal["OPTIMAL", "FITS", "TIGHT", "UNDERUTILIZED", "OOM"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GPUComparisonItem(BaseModel):
|
|
22
|
+
"""Detailed capacity and suitability verdict for a single GPU profile."""
|
|
23
|
+
|
|
24
|
+
gpu_id: str = Field(description="Preset key or identifier of the GPU")
|
|
25
|
+
gpu_name: str = Field(description="Full display name of the GPU")
|
|
26
|
+
vram_total_gb: float = Field(description="Total addressable GPU memory in GB")
|
|
27
|
+
memory_bandwidth_gb_s: float = Field(description="Peak memory bandwidth in GB/s")
|
|
28
|
+
required_vram_gb: float = Field(description="Total VRAM demand for workload in GB")
|
|
29
|
+
headroom_gb: float = Field(description="Remaining free headroom in GB")
|
|
30
|
+
utilization_pct: float = Field(description="Percentage of VRAM occupied by workload")
|
|
31
|
+
fits: bool = Field(description="True if workload fits within physical memory")
|
|
32
|
+
status: StatusVerdict = Field(description="Categorical suitability status")
|
|
33
|
+
max_concurrent_batch: int = Field(description="Max concurrent streams supported at context")
|
|
34
|
+
max_context_tokens: int = Field(description="Max context supported at current batch")
|
|
35
|
+
recommendation_note: str = Field(description="Actionable procurement recommendation")
|
|
36
|
+
vram_estimate: VRAMEstimateResult = Field(description="Full breakdown of VRAM components")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class GPUComparisonReport(BaseModel):
|
|
40
|
+
"""Comprehensive comparison matrix across multiple candidate accelerators."""
|
|
41
|
+
|
|
42
|
+
model_name: str
|
|
43
|
+
precision: str
|
|
44
|
+
kv_cache_dtype: str
|
|
45
|
+
batch_size: int
|
|
46
|
+
context_tokens: int
|
|
47
|
+
tensor_parallel: int
|
|
48
|
+
items: list[GPUComparisonItem]
|
|
49
|
+
recommended_optimal_gpu: str | None = Field(
|
|
50
|
+
default=None, description="Most cost-effective GPU that safely satisfies workload"
|
|
51
|
+
)
|
|
52
|
+
recommended_performance_gpu: str | None = Field(
|
|
53
|
+
default=None, description="Highest-throughput GPU among all fitting candidates"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def to_markdown_table(self) -> str:
|
|
57
|
+
"""Render the comparison report into a clean Markdown table for RFCs."""
|
|
58
|
+
lines = [
|
|
59
|
+
f"### InferSize Hardware Comparison: `{self.model_name}`",
|
|
60
|
+
f"- **Workload**: Batch={self.batch_size}, Context={self.context_tokens:,} tokens, "
|
|
61
|
+
f"Weights={self.precision.upper()}, KV-Cache={self.kv_cache_dtype.upper()}, TP={self.tensor_parallel}",
|
|
62
|
+
"",
|
|
63
|
+
"| GPU | VRAM Total | VRAM Req | Util (%) | Headroom | Max Batch | Bandwidth | Status | Verdict |",
|
|
64
|
+
"|:---|---:|---:|---:|---:|---:|---:|:---:|:---|",
|
|
65
|
+
]
|
|
66
|
+
for item in self.items:
|
|
67
|
+
sign = "+" if item.headroom_gb >= 0 else ""
|
|
68
|
+
status_symbol = {
|
|
69
|
+
"OPTIMAL": "⭐ Optimal",
|
|
70
|
+
"FITS": "✔ Fits",
|
|
71
|
+
"TIGHT": "⚠ Tight",
|
|
72
|
+
"UNDERUTILIZED": "ℹ Overkill",
|
|
73
|
+
"OOM": "❌ OOM",
|
|
74
|
+
}.get(item.status, item.status)
|
|
75
|
+
lines.append(
|
|
76
|
+
f"| **{item.gpu_id}** | {item.vram_total_gb:.1f} GB | {item.required_vram_gb:.1f} GB | "
|
|
77
|
+
f"{item.utilization_pct:.1f}% | {sign}{item.headroom_gb:.1f} GB | {item.max_concurrent_batch} | "
|
|
78
|
+
f"{item.memory_bandwidth_gb_s:.0f} GB/s | {status_symbol} | {item.recommendation_note} |"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
lines.append("")
|
|
82
|
+
if self.recommended_optimal_gpu:
|
|
83
|
+
lines.append(f"**Right-Sized / Cost-Effective**: `{self.recommended_optimal_gpu}`")
|
|
84
|
+
if self.recommended_performance_gpu:
|
|
85
|
+
lines.append(f"**Peak Throughput**: `{self.recommended_performance_gpu}`")
|
|
86
|
+
|
|
87
|
+
return "\n".join(lines)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Default representative GPUs for general evaluation
|
|
91
|
+
DEFAULT_COMPARISON_GPUS = [
|
|
92
|
+
"rtx3050",
|
|
93
|
+
"rtx4060",
|
|
94
|
+
"rtx4090",
|
|
95
|
+
"t4",
|
|
96
|
+
"l4",
|
|
97
|
+
"a100-40gb",
|
|
98
|
+
"a100-80gb",
|
|
99
|
+
"h100-80gb",
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def compare_gpus_for_workload(
|
|
104
|
+
model: ModelArch,
|
|
105
|
+
candidate_gpus: list[GPUProfile] | None = None,
|
|
106
|
+
precision: str = "fp16",
|
|
107
|
+
kv_cache_dtype: str = "fp16",
|
|
108
|
+
batch_size: int = 1,
|
|
109
|
+
context_tokens: int = 2048,
|
|
110
|
+
tensor_parallel: int = 1,
|
|
111
|
+
) -> GPUComparisonReport:
|
|
112
|
+
"""Compare a model serving workload across multiple GPUs side-by-side.
|
|
113
|
+
|
|
114
|
+
Analytically solves memory footprint, max batch headroom, and yields
|
|
115
|
+
procurement recommendations (Right-Sized vs Peak Performance).
|
|
116
|
+
"""
|
|
117
|
+
if candidate_gpus is None:
|
|
118
|
+
candidate_gpus = [GPU_PRESETS[gid] for gid in DEFAULT_COMPARISON_GPUS if gid in GPU_PRESETS]
|
|
119
|
+
|
|
120
|
+
items: list[GPUComparisonItem] = []
|
|
121
|
+
|
|
122
|
+
for gpu in candidate_gpus:
|
|
123
|
+
est = estimate_vram_usage(
|
|
124
|
+
model=model,
|
|
125
|
+
gpu=gpu,
|
|
126
|
+
precision=precision,
|
|
127
|
+
kv_cache_dtype=kv_cache_dtype,
|
|
128
|
+
batch_size=batch_size,
|
|
129
|
+
context_tokens=context_tokens,
|
|
130
|
+
tensor_parallel=tensor_parallel,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
max_b = find_max_concurrent_batch_size(
|
|
134
|
+
model=model,
|
|
135
|
+
gpu=gpu,
|
|
136
|
+
precision=precision,
|
|
137
|
+
context_tokens=context_tokens,
|
|
138
|
+
kv_cache_dtype=kv_cache_dtype,
|
|
139
|
+
tensor_parallel=tensor_parallel,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
max_c = find_max_context_length(
|
|
143
|
+
model=model,
|
|
144
|
+
gpu=gpu,
|
|
145
|
+
precision=precision,
|
|
146
|
+
batch_size=batch_size,
|
|
147
|
+
kv_cache_dtype=kv_cache_dtype,
|
|
148
|
+
tensor_parallel=tensor_parallel,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Categorize suitability
|
|
152
|
+
if not est.fits_in_vram:
|
|
153
|
+
status: StatusVerdict = "OOM"
|
|
154
|
+
note = (
|
|
155
|
+
f"Deficit of {abs(est.free_vram_headroom_gb):.1f} GB. Requires quantization or TP."
|
|
156
|
+
)
|
|
157
|
+
elif est.free_vram_headroom_gb < 1.0 or est.vram_utilization_pct > 92.0:
|
|
158
|
+
status = "TIGHT"
|
|
159
|
+
note = "High preemption risk under dynamic KV-cache growth."
|
|
160
|
+
elif est.vram_utilization_pct < 30.0:
|
|
161
|
+
status = "UNDERUTILIZED"
|
|
162
|
+
note = "Excess VRAM headroom. Ideal for large batching, costly for B=1."
|
|
163
|
+
else:
|
|
164
|
+
status = "FITS"
|
|
165
|
+
note = "Healthy capacity headroom with low preemption risk."
|
|
166
|
+
|
|
167
|
+
items.append(
|
|
168
|
+
GPUComparisonItem(
|
|
169
|
+
gpu_id=gpu.name,
|
|
170
|
+
gpu_name=gpu.display_name,
|
|
171
|
+
vram_total_gb=gpu.vram_gb,
|
|
172
|
+
memory_bandwidth_gb_s=gpu.memory_bandwidth_gb_s,
|
|
173
|
+
required_vram_gb=est.total_required_vram_gb,
|
|
174
|
+
headroom_gb=est.free_vram_headroom_gb,
|
|
175
|
+
utilization_pct=est.vram_utilization_pct,
|
|
176
|
+
fits=est.fits_in_vram,
|
|
177
|
+
status=status,
|
|
178
|
+
max_concurrent_batch=max_b,
|
|
179
|
+
max_context_tokens=max_c,
|
|
180
|
+
recommendation_note=note,
|
|
181
|
+
vram_estimate=est,
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Determine Best Budget / Optimal GPU:
|
|
186
|
+
# Smallest total VRAM among candidates that fit with at least 8% headroom
|
|
187
|
+
fitting_items = [it for it in items if it.fits and it.utilization_pct <= 92.0]
|
|
188
|
+
|
|
189
|
+
optimal_gpu_id: str | None = None
|
|
190
|
+
performance_gpu_id: str | None = None
|
|
191
|
+
|
|
192
|
+
if fitting_items:
|
|
193
|
+
# Sort by VRAM ascending to find smallest capable GPU
|
|
194
|
+
sorted_by_vram = sorted(fitting_items, key=lambda x: x.vram_total_gb)
|
|
195
|
+
optimal_item = sorted_by_vram[0]
|
|
196
|
+
optimal_gpu_id = optimal_item.gpu_id
|
|
197
|
+
# Update its status and note to OPTIMAL
|
|
198
|
+
for item in items:
|
|
199
|
+
if item.gpu_id == optimal_gpu_id:
|
|
200
|
+
item.status = "OPTIMAL"
|
|
201
|
+
item.recommendation_note = "Right-sized: Minimal idle VRAM waste, optimal cost."
|
|
202
|
+
|
|
203
|
+
# Sort by Memory Bandwidth descending to find peak throughput GPU
|
|
204
|
+
sorted_by_bw = sorted(fitting_items, key=lambda x: x.memory_bandwidth_gb_s, reverse=True)
|
|
205
|
+
perf_item = sorted_by_bw[0]
|
|
206
|
+
performance_gpu_id = perf_item.gpu_id
|
|
207
|
+
|
|
208
|
+
return GPUComparisonReport(
|
|
209
|
+
model_name=model.name,
|
|
210
|
+
precision=precision,
|
|
211
|
+
kv_cache_dtype=kv_cache_dtype,
|
|
212
|
+
batch_size=batch_size,
|
|
213
|
+
context_tokens=context_tokens,
|
|
214
|
+
tensor_parallel=tensor_parallel,
|
|
215
|
+
items=items,
|
|
216
|
+
recommended_optimal_gpu=optimal_gpu_id,
|
|
217
|
+
recommended_performance_gpu=performance_gpu_id,
|
|
218
|
+
)
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Cloud TCO, memory bandwidth roofline modeling, and $/1M tokens cost analytics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from infersize.analytics.vram import (
|
|
8
|
+
BYTES_PER_PARAM,
|
|
9
|
+
calculate_kv_cache_bytes_per_token,
|
|
10
|
+
estimate_vram_usage,
|
|
11
|
+
)
|
|
12
|
+
from infersize.hardware.pricing import get_hourly_rate
|
|
13
|
+
from infersize.hardware.specs import GPU_PRESETS, GPUProfile
|
|
14
|
+
from infersize.models.specs import ModelArch
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CostEstimateResult(BaseModel):
|
|
18
|
+
"""Detailed analytical throughput, hardware roofline, and cloud cost metrics."""
|
|
19
|
+
|
|
20
|
+
model_name: str
|
|
21
|
+
gpu_name: str
|
|
22
|
+
gpu_id: str
|
|
23
|
+
provider: str
|
|
24
|
+
hourly_rate_usd: float = Field(description="Hourly rental rate per GPU in USD")
|
|
25
|
+
total_cluster_hourly_usd: float = Field(
|
|
26
|
+
description="Total hourly rate across all TP GPUs in USD"
|
|
27
|
+
)
|
|
28
|
+
batch_size: int
|
|
29
|
+
context_tokens: int
|
|
30
|
+
precision: str
|
|
31
|
+
kv_cache_dtype: str
|
|
32
|
+
tensor_parallel: int
|
|
33
|
+
|
|
34
|
+
# Memory traffic per decode step
|
|
35
|
+
weight_bytes_per_step: int = Field(description="DRAM bytes read for weights per decode step")
|
|
36
|
+
kv_bytes_per_step: int = Field(description="DRAM bytes read for KV-cache per decode step")
|
|
37
|
+
total_dram_bytes_per_step: int = Field(description="Total DRAM bytes transferred per step")
|
|
38
|
+
|
|
39
|
+
# Roofline throughput & latency
|
|
40
|
+
memory_bandwidth_gb_s: float = Field(description="Theoretical peak memory bandwidth in GB/s")
|
|
41
|
+
bus_efficiency: float = Field(
|
|
42
|
+
description="Assumed memory bus utilization efficiency (e.g. 0.80)"
|
|
43
|
+
)
|
|
44
|
+
effective_bandwidth_gb_s: float = Field(description="Effective achievable bandwidth in GB/s")
|
|
45
|
+
|
|
46
|
+
time_per_step_ms: float = Field(description="Time taken per decode step in milliseconds")
|
|
47
|
+
time_per_output_token_ms: float = Field(
|
|
48
|
+
description="Time Per Output Token (TPOT) per user stream in ms"
|
|
49
|
+
)
|
|
50
|
+
tokens_per_sec_per_user: float = Field(
|
|
51
|
+
description="Generation speed per individual user stream"
|
|
52
|
+
)
|
|
53
|
+
aggregate_tokens_per_sec: float = Field(
|
|
54
|
+
description="Total token generation throughput across entire batch"
|
|
55
|
+
)
|
|
56
|
+
tokens_per_hour: float = Field(description="Theoretical maximum token capacity per hour")
|
|
57
|
+
|
|
58
|
+
# Cloud Unit Economics
|
|
59
|
+
cost_per_million_tokens_usd: float = Field(
|
|
60
|
+
description="Unit cost in USD to generate 1,000,000 tokens"
|
|
61
|
+
)
|
|
62
|
+
monthly_dedicated_cost_usd: float = Field(
|
|
63
|
+
description="24/7 monthly dedicated cluster rental cost (730 hours)"
|
|
64
|
+
)
|
|
65
|
+
fits_in_vram: bool = Field(description="Whether the workload fits in GPU memory")
|
|
66
|
+
oom_deficit_gb: float = Field(
|
|
67
|
+
default=0.0, description="VRAM memory deficit in GB if workload exceeds capacity"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def estimate_serving_cost(
|
|
72
|
+
model: ModelArch,
|
|
73
|
+
gpu: GPUProfile,
|
|
74
|
+
precision: str = "fp16",
|
|
75
|
+
kv_cache_dtype: str = "fp16",
|
|
76
|
+
batch_size: int = 1,
|
|
77
|
+
context_tokens: int = 2048,
|
|
78
|
+
tensor_parallel: int = 1,
|
|
79
|
+
provider: str = "runpod",
|
|
80
|
+
custom_hourly_rate: float | None = None,
|
|
81
|
+
bus_efficiency: float = 0.80,
|
|
82
|
+
) -> CostEstimateResult:
|
|
83
|
+
"""Analytically calculate decode throughput, TPOT, and cost per million tokens.
|
|
84
|
+
|
|
85
|
+
Models the autoregressive decode phase as memory-bandwidth bound:
|
|
86
|
+
Step Time = (Weight Bytes + KV Cache Bytes) / Effective Bandwidth
|
|
87
|
+
"""
|
|
88
|
+
tp = max(1, tensor_parallel)
|
|
89
|
+
vram_res = estimate_vram_usage(
|
|
90
|
+
model=model,
|
|
91
|
+
gpu=gpu,
|
|
92
|
+
precision=precision,
|
|
93
|
+
kv_cache_dtype=kv_cache_dtype,
|
|
94
|
+
batch_size=batch_size,
|
|
95
|
+
context_tokens=context_tokens,
|
|
96
|
+
tensor_parallel=tp,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
hourly_rate = get_hourly_rate(
|
|
100
|
+
gpu_id=gpu.name,
|
|
101
|
+
provider=provider,
|
|
102
|
+
custom_rate=custom_hourly_rate,
|
|
103
|
+
)
|
|
104
|
+
cluster_hourly = hourly_rate * tp
|
|
105
|
+
|
|
106
|
+
# Calculate DRAM bytes transferred per decode token step
|
|
107
|
+
# For dense models, active params == total params. For MoE models,
|
|
108
|
+
# decode streams top-K active routed experts per token.
|
|
109
|
+
active_params = model.calculate_active_parameters()
|
|
110
|
+
bytes_per_param = BYTES_PER_PARAM.get(precision.lower(), 2.0)
|
|
111
|
+
# Per GPU active weight slice
|
|
112
|
+
weight_bytes = int((active_params * bytes_per_param) / tp)
|
|
113
|
+
|
|
114
|
+
# Per GPU KV-cache slice: kv_bytes_per_token * batch_size * context_tokens / tp
|
|
115
|
+
kv_bytes_per_token = calculate_kv_cache_bytes_per_token(model, kv_cache_dtype)
|
|
116
|
+
kv_bytes = int((kv_bytes_per_token * batch_size * context_tokens) / tp)
|
|
117
|
+
|
|
118
|
+
total_dram_step = weight_bytes + kv_bytes
|
|
119
|
+
|
|
120
|
+
eff_bw_gb_s = gpu.memory_bandwidth_gb_s * bus_efficiency
|
|
121
|
+
eff_bw_bytes_s = eff_bw_gb_s * 1e9
|
|
122
|
+
|
|
123
|
+
if not vram_res.fits_in_vram:
|
|
124
|
+
deficit = abs(vram_res.free_vram_headroom_gb)
|
|
125
|
+
return CostEstimateResult(
|
|
126
|
+
model_name=model.name,
|
|
127
|
+
gpu_name=gpu.display_name,
|
|
128
|
+
gpu_id=gpu.name,
|
|
129
|
+
provider=provider,
|
|
130
|
+
hourly_rate_usd=hourly_rate,
|
|
131
|
+
total_cluster_hourly_usd=cluster_hourly,
|
|
132
|
+
batch_size=batch_size,
|
|
133
|
+
context_tokens=context_tokens,
|
|
134
|
+
precision=precision.lower(),
|
|
135
|
+
kv_cache_dtype=kv_cache_dtype.lower(),
|
|
136
|
+
tensor_parallel=tp,
|
|
137
|
+
weight_bytes_per_step=weight_bytes,
|
|
138
|
+
kv_bytes_per_step=kv_bytes,
|
|
139
|
+
total_dram_bytes_per_step=total_dram_step,
|
|
140
|
+
memory_bandwidth_gb_s=gpu.memory_bandwidth_gb_s,
|
|
141
|
+
bus_efficiency=bus_efficiency,
|
|
142
|
+
effective_bandwidth_gb_s=round(eff_bw_gb_s, 1),
|
|
143
|
+
time_per_step_ms=0.0,
|
|
144
|
+
time_per_output_token_ms=0.0,
|
|
145
|
+
tokens_per_sec_per_user=0.0,
|
|
146
|
+
aggregate_tokens_per_sec=0.0,
|
|
147
|
+
tokens_per_hour=0.0,
|
|
148
|
+
cost_per_million_tokens_usd=0.0,
|
|
149
|
+
monthly_dedicated_cost_usd=round(cluster_hourly * 730, 2),
|
|
150
|
+
fits_in_vram=False,
|
|
151
|
+
oom_deficit_gb=round(deficit, 2),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Time per decode step in seconds: DRAM bytes / Effective Bandwidth
|
|
155
|
+
step_time_s = total_dram_step / eff_bw_bytes_s
|
|
156
|
+
step_time_ms = step_time_s * 1000.0
|
|
157
|
+
|
|
158
|
+
# User-perceived latency (TPOT)
|
|
159
|
+
tpot_ms = step_time_ms
|
|
160
|
+
user_tok_s = 1.0 / step_time_s if step_time_s > 0 else 0.0
|
|
161
|
+
|
|
162
|
+
# Aggregate throughput across all B concurrent streams
|
|
163
|
+
aggregate_tok_s = user_tok_s * batch_size
|
|
164
|
+
tokens_per_hour = aggregate_tok_s * 3600.0
|
|
165
|
+
|
|
166
|
+
# $/1M Tokens = (Hourly Cost / Tokens Per Hour) * 1,000,000
|
|
167
|
+
if tokens_per_hour > 0 and cluster_hourly > 0:
|
|
168
|
+
cost_per_1m = (cluster_hourly / tokens_per_hour) * 1e6
|
|
169
|
+
else:
|
|
170
|
+
cost_per_1m = 0.0
|
|
171
|
+
|
|
172
|
+
monthly_cost = cluster_hourly * 730.0
|
|
173
|
+
|
|
174
|
+
return CostEstimateResult(
|
|
175
|
+
model_name=model.name,
|
|
176
|
+
gpu_name=gpu.display_name,
|
|
177
|
+
gpu_id=gpu.name,
|
|
178
|
+
provider=provider,
|
|
179
|
+
hourly_rate_usd=round(hourly_rate, 3),
|
|
180
|
+
total_cluster_hourly_usd=round(cluster_hourly, 3),
|
|
181
|
+
batch_size=batch_size,
|
|
182
|
+
context_tokens=context_tokens,
|
|
183
|
+
precision=precision.lower(),
|
|
184
|
+
kv_cache_dtype=kv_cache_dtype.lower(),
|
|
185
|
+
tensor_parallel=tp,
|
|
186
|
+
weight_bytes_per_step=weight_bytes,
|
|
187
|
+
kv_bytes_per_step=kv_bytes,
|
|
188
|
+
total_dram_bytes_per_step=total_dram_step,
|
|
189
|
+
memory_bandwidth_gb_s=gpu.memory_bandwidth_gb_s,
|
|
190
|
+
bus_efficiency=bus_efficiency,
|
|
191
|
+
effective_bandwidth_gb_s=round(eff_bw_gb_s, 1),
|
|
192
|
+
time_per_step_ms=round(step_time_ms, 2),
|
|
193
|
+
time_per_output_token_ms=round(tpot_ms, 2),
|
|
194
|
+
tokens_per_sec_per_user=round(user_tok_s, 1),
|
|
195
|
+
aggregate_tokens_per_sec=round(aggregate_tok_s, 1),
|
|
196
|
+
tokens_per_hour=round(tokens_per_hour, 0),
|
|
197
|
+
cost_per_million_tokens_usd=round(cost_per_1m, 4),
|
|
198
|
+
monthly_dedicated_cost_usd=round(monthly_cost, 2),
|
|
199
|
+
fits_in_vram=True,
|
|
200
|
+
oom_deficit_gb=0.0,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class MultiGPUCostReport(BaseModel):
|
|
205
|
+
"""Side-by-side cost and unit economics comparison across multiple cloud setups."""
|
|
206
|
+
|
|
207
|
+
model_name: str
|
|
208
|
+
precision: str
|
|
209
|
+
kv_cache_dtype: str
|
|
210
|
+
batch_size: int
|
|
211
|
+
context_tokens: int
|
|
212
|
+
results: list[CostEstimateResult]
|
|
213
|
+
lowest_cost_setup: CostEstimateResult | None = None
|
|
214
|
+
fastest_setup: CostEstimateResult | None = None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def compare_cloud_costs_for_workload(
|
|
218
|
+
model: ModelArch,
|
|
219
|
+
candidate_gpus: list[GPUProfile] | None = None,
|
|
220
|
+
precision: str = "fp16",
|
|
221
|
+
kv_cache_dtype: str = "fp16",
|
|
222
|
+
batch_size: int = 1,
|
|
223
|
+
context_tokens: int = 2048,
|
|
224
|
+
provider: str = "runpod",
|
|
225
|
+
tensor_parallel: int = 1,
|
|
226
|
+
) -> MultiGPUCostReport:
|
|
227
|
+
"""Compare throughput, TPOT, and $/1M tokens across multiple GPU candidates."""
|
|
228
|
+
if candidate_gpus is None:
|
|
229
|
+
candidate_ids = ["rtx4090", "l4", "a100-40gb", "a100-80gb", "h100-80gb"]
|
|
230
|
+
candidate_gpus = [GPU_PRESETS[gid] for gid in candidate_ids if gid in GPU_PRESETS]
|
|
231
|
+
|
|
232
|
+
results: list[CostEstimateResult] = []
|
|
233
|
+
for gpu in candidate_gpus:
|
|
234
|
+
res = estimate_serving_cost(
|
|
235
|
+
model=model,
|
|
236
|
+
gpu=gpu,
|
|
237
|
+
precision=precision,
|
|
238
|
+
kv_cache_dtype=kv_cache_dtype,
|
|
239
|
+
batch_size=batch_size,
|
|
240
|
+
context_tokens=context_tokens,
|
|
241
|
+
tensor_parallel=tensor_parallel,
|
|
242
|
+
provider=provider,
|
|
243
|
+
)
|
|
244
|
+
results.append(res)
|
|
245
|
+
|
|
246
|
+
fitting = [r for r in results if r.fits_in_vram and r.cost_per_million_tokens_usd > 0]
|
|
247
|
+
lowest = min(fitting, key=lambda x: x.cost_per_million_tokens_usd) if fitting else None
|
|
248
|
+
fastest = max(fitting, key=lambda x: x.aggregate_tokens_per_sec) if fitting else None
|
|
249
|
+
|
|
250
|
+
return MultiGPUCostReport(
|
|
251
|
+
model_name=model.name,
|
|
252
|
+
precision=precision,
|
|
253
|
+
kv_cache_dtype=kv_cache_dtype,
|
|
254
|
+
batch_size=batch_size,
|
|
255
|
+
context_tokens=context_tokens,
|
|
256
|
+
results=results,
|
|
257
|
+
lowest_cost_setup=lowest,
|
|
258
|
+
fastest_setup=fastest,
|
|
259
|
+
)
|