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 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,6 @@
1
+ """Executable entry point for python -m infersize."""
2
+
3
+ from infersize.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
@@ -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
+ )