rhui96 commited on
Commit
ff706d7
·
verified ·
1 Parent(s): 59cb0a5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +332 -29
app.py CHANGED
@@ -453,8 +453,8 @@ MODEL_CHOICES = [
453
  # Note: nf4 (NormalFloat4) is specifically designed for QLoRA and provides
454
  # better quality than standard int4 at the same bitwidth
455
  PRECISION_MAP = {
456
- "float32": 4.0,
457
- "fp32": 4.0,
458
  "bf16": 2.0, # BFloat16 - preferred for training (better range than fp16)
459
  "fp16": 2.0, # Float16 - standard for inference
460
  "nf4": 0.5625, # NormalFloat4 - QLoRA's quantization format
@@ -612,7 +612,7 @@ GPU_DATABASE = [
612
  # Based on real-world benchmarks from MLPerf, vendor data, and community testing
613
  # Tokens per second per GPU for different model sizes
614
  #
615
- # Last Updated: 16 December 2025
616
  # Sources:
617
  # - MLPerf Training v3.1 (November 2023)
618
  # - NVIDIA TensorRT-LLM benchmarks (Q4 2024)
@@ -1504,7 +1504,9 @@ def calculate_vram(
1504
  weights_gb = calculate_model_weights(spec, precision)
1505
 
1506
  # Calculate KV cache
1507
- kv_cache_gb = calculate_kv_cache(spec, batch_size, seq_len, precision)
 
 
1508
 
1509
  if task == "Inference":
1510
  # Inference: weights + KV cache + framework overhead
@@ -1514,7 +1516,9 @@ def calculate_vram(
1514
 
1515
  else: # Training
1516
  # Training: weights + KV + activations + optimizer + gradients
1517
- activations_gb = calculate_activations(spec, batch_size, seq_len, precision)
 
 
1518
  optimizer_gb, _ = calculate_optimizer_states(weights_gb, ft_method or "Full Fine-Tuning", rank or 64, spec)
1519
  gradients_gb, _ = calculate_gradients(weights_gb, ft_method or "Full Fine-Tuning", rank or 64, spec)
1520
 
@@ -1540,6 +1544,255 @@ def calculate_vram(
1540
  # Hardware Recommendation Engine
1541
  # =================================================================================================
1542
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1543
  def recommend_hardware(
1544
  required_vram: float,
1545
  task: str,
@@ -1622,6 +1875,7 @@ def recommend_hardware(
1622
  "cost": [],
1623
  "vram_util": [],
1624
  "cost_efficiency": [], # tokens per rupee
 
1625
  }
1626
 
1627
  for config in chart_configs:
@@ -1636,7 +1890,22 @@ def recommend_hardware(
1636
  chart_data["cost"].append(price)
1637
  chart_data["vram_util"].append(vram_util)
1638
  # Cost efficiency: tokens per rupee per hour
1639
- chart_data["cost_efficiency"].append(total_tps / price if price > 0 else 0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1640
 
1641
  # Generate recommendation cards
1642
  def make_card(title: str, emoji: str, config: Optional[GPUConfig]) -> str:
@@ -1708,14 +1977,14 @@ def process_request(
1708
  output_tokens: float,
1709
  dataset_tier: str,
1710
  manufacturers: list[str],
1711
- ) -> Tuple[str, str, str, Any]:
1712
  """
1713
  Main request processing function.
1714
 
1715
  Orchestrates model resolution, VRAM calculation, and hardware recommendation.
1716
 
1717
  Returns:
1718
- Tuple of (report, budget_rec, runner_up_rec, combined_chart)
1719
  """
1720
  try:
1721
  # Validate inputs
@@ -1773,7 +2042,7 @@ def process_request(
1773
  )
1774
 
1775
  if error:
1776
- return error, "", "", None
1777
 
1778
  # Generate report
1779
  report = f"""
@@ -1843,23 +2112,6 @@ def process_request(
1843
  )
1844
  ])
1845
 
1846
- # combined_chart.update_layout(
1847
- # title_text="GPU Comparison (Top 10 by Cost)",
1848
- # xaxis_title="GPU Configuration",
1849
- # yaxis_title="Normalized Value (% of max)",
1850
- # barmode='group',
1851
- # height=450,
1852
- # xaxis_tickangle=-45,
1853
- # margin=dict(b=120),
1854
- # legend=dict(
1855
- # orientation="h",
1856
- # yanchor="bottom",
1857
- # y=1.02,
1858
- # xanchor="center",
1859
- # x=0.5
1860
- # )
1861
- # )
1862
-
1863
  combined_chart.update_layout(
1864
  title_text="GPU Comparison (Top 10 by Cost)",
1865
  xaxis_title="GPU Configuration",
@@ -1882,7 +2134,48 @@ def process_request(
1882
  title_font=dict(size=14),
1883
  )
1884
 
1885
- return report, budget_rec, runner_up_rec, combined_chart
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1886
 
1887
  except Exception as e:
1888
  error_msg = f"""
@@ -1891,7 +2184,7 @@ def process_request(
1891
  <p>{str(e)}</p>
1892
  </div>
1893
  """
1894
- return error_msg, "", "", None
1895
 
1896
  # =================================================================================================
1897
  # UI Event Handlers
@@ -2053,6 +2346,16 @@ def create_interface() -> gr.Blocks:
2053
  with gr.Column(elem_classes=["runner-box"]):
2054
  rec_out_2 = gr.Markdown()
2055
 
 
 
 
 
 
 
 
 
 
 
2056
  # Bar chart comparison section
2057
  with gr.Accordion("📊 GPU Comparison (Top 10 by Cost)", open=False):
2058
  combined_plot = gr.Plot(label="GPU Comparison")
@@ -2091,7 +2394,7 @@ def create_interface() -> gr.Blocks:
2091
  ft_method, rank, batch, sample_count,
2092
  input_tokens, output_tokens, dataset_tier, manufacturers,
2093
  ],
2094
- outputs=[report_out, rec_out_1, rec_out_2, combined_plot]
2095
  )
2096
 
2097
  return demo
 
453
  # Note: nf4 (NormalFloat4) is specifically designed for QLoRA and provides
454
  # better quality than standard int4 at the same bitwidth
455
  PRECISION_MAP = {
456
+ # "float32": 4.0,
457
+ # "fp32": 4.0,
458
  "bf16": 2.0, # BFloat16 - preferred for training (better range than fp16)
459
  "fp16": 2.0, # Float16 - standard for inference
460
  "nf4": 0.5625, # NormalFloat4 - QLoRA's quantization format
 
612
  # Based on real-world benchmarks from MLPerf, vendor data, and community testing
613
  # Tokens per second per GPU for different model sizes
614
  #
615
+ # Last Updated: 17 December 2025
616
  # Sources:
617
  # - MLPerf Training v3.1 (November 2023)
618
  # - NVIDIA TensorRT-LLM benchmarks (Q4 2024)
 
1504
  weights_gb = calculate_model_weights(spec, precision)
1505
 
1506
  # Calculate KV cache
1507
+ # For training, KV cache is always at compute precision (bf16/fp16), not quantized
1508
+ kv_precision = "bf16" if task == "Training" else precision
1509
+ kv_cache_gb = calculate_kv_cache(spec, batch_size, seq_len, kv_precision)
1510
 
1511
  if task == "Inference":
1512
  # Inference: weights + KV cache + framework overhead
 
1516
 
1517
  else: # Training
1518
  # Training: weights + KV + activations + optimizer + gradients
1519
+ # IMPORTANT: Activations are always stored at compute precision (bf16/fp16)
1520
+ # even for QLoRA, because the forward pass computes at full precision
1521
+ activations_gb = calculate_activations(spec, batch_size, seq_len, "bf16")
1522
  optimizer_gb, _ = calculate_optimizer_states(weights_gb, ft_method or "Full Fine-Tuning", rank or 64, spec)
1523
  gradients_gb, _ = calculate_gradients(weights_gb, ft_method or "Full Fine-Tuning", rank or 64, spec)
1524
 
 
1544
  # Hardware Recommendation Engine
1545
  # =================================================================================================
1546
 
1547
+ def generate_analysis_csv(
1548
+ # Input parameters
1549
+ model_name: str,
1550
+ task: str,
1551
+ quant: str,
1552
+ framework: str,
1553
+ batch_size: int,
1554
+ seq_len: int,
1555
+ ft_method: Optional[str],
1556
+ rank: Optional[int],
1557
+ sample_count: int,
1558
+ input_tokens: int,
1559
+ output_tokens: int,
1560
+ pricing_tier: str,
1561
+ manufacturers: List[str],
1562
+ # Calculated values
1563
+ spec: 'ModelSpec',
1564
+ weights_gb: float,
1565
+ variable_gb: float,
1566
+ total_vram: float,
1567
+ actual_precision: str,
1568
+ # GPU recommendations
1569
+ gpu_recommendations: List[Dict[str, Any]],
1570
+ source: str,
1571
+ ) -> str:
1572
+ """
1573
+ Generate a detailed CSV containing all analysis parameters, formulas, and recommendations.
1574
+
1575
+ Returns:
1576
+ CSV content as a string
1577
+ """
1578
+ import csv
1579
+ import io
1580
+ from datetime import datetime
1581
+
1582
+ output = io.StringIO()
1583
+ writer = csv.writer(output)
1584
+
1585
+ # ==========================================
1586
+ # SECTION 1: HEADER
1587
+ # ==========================================
1588
+ writer.writerow(["IndiaAI GPU Infrastructure Recommender - Analysis Report"])
1589
+ writer.writerow(["Generated", datetime.now().strftime("%Y-%m-%d %H:%M:%S")])
1590
+ writer.writerow([])
1591
+
1592
+ # ==========================================
1593
+ # SECTION 2: INPUT PARAMETERS
1594
+ # ==========================================
1595
+ writer.writerow(["*" * 50])
1596
+ writer.writerow(["INPUT PARAMETERS"])
1597
+ writer.writerow(["*" * 50])
1598
+ writer.writerow(["Parameter", "Value", "Description"])
1599
+ writer.writerow(["Model Name", model_name, "User-specified model identifier"])
1600
+ writer.writerow(["Task", task, "Inference or Training"])
1601
+ writer.writerow(["Quantization", quant, "Precision format for model weights"])
1602
+ writer.writerow(["Framework", framework, "Inference/training framework"])
1603
+ writer.writerow(["Batch Size", batch_size, "Number of samples processed together"])
1604
+ writer.writerow(["Sequence Length", seq_len, "Maximum context length (tokens)"])
1605
+ if task == "Training":
1606
+ writer.writerow(["Fine-tuning Method", ft_method or "N/A", "Training strategy (QLoRA/LoRA/Full FT)"])
1607
+ writer.writerow(["LoRA Rank", rank if ft_method in ["LoRA", "QLoRA"] else "N/A", "Adapter rank for LoRA/QLoRA"])
1608
+ writer.writerow(["Sample Count", sample_count, "Total samples to process"])
1609
+ writer.writerow(["Input Tokens", input_tokens, "Average input tokens per sample"])
1610
+ writer.writerow(["Output Tokens", output_tokens, "Average output tokens per sample"])
1611
+ writer.writerow(["Pricing Tier", pricing_tier, "Selected pricing model"])
1612
+ writer.writerow(["GPU Manufacturers", ", ".join(manufacturers), "Filtered GPU vendors"])
1613
+ writer.writerow([])
1614
+
1615
+ # ==========================================
1616
+ # SECTION 3: MODEL ARCHITECTURE (Derived)
1617
+ # ==========================================
1618
+ writer.writerow(["*" * 50])
1619
+ writer.writerow(["MODEL ARCHITECTURE (Derived)"])
1620
+ writer.writerow(["*" * 50])
1621
+ writer.writerow(["Parameter", "Value", "Formula / Source"])
1622
+ writer.writerow(["Source", source, "How model info was obtained"])
1623
+ writer.writerow(["Parameters (Billions)", f"{spec.params_bn:.2f}", "From model config or estimated from name"])
1624
+ writer.writerow(["Parameters (Exact)", f"{spec.params:,}", "params_bn × 1,000,000,000"])
1625
+ writer.writerow(["Layers", spec.layers, "Number of transformer layers"])
1626
+ writer.writerow(["Attention Heads", spec.heads, "Number of query attention heads"])
1627
+ writer.writerow(["KV Heads", spec.kv_heads, "Number of key/value heads (GQA)"])
1628
+ writer.writerow(["Head Dimension", spec.head_dim, "Dimension per attention head"])
1629
+ writer.writerow(["Hidden Size", spec.heads * spec.head_dim, "heads × head_dim"])
1630
+ writer.writerow(["Max Context Length", spec.context, "Maximum supported sequence length"])
1631
+ writer.writerow([])
1632
+
1633
+ # ==========================================
1634
+ # SECTION 4: PRECISION PARAMETERS
1635
+ # ==========================================
1636
+ writer.writerow(["*" * 50])
1637
+ writer.writerow(["PRECISION PARAMETERS"])
1638
+ writer.writerow(["*" * 50])
1639
+ writer.writerow(["Parameter", "Value", "Formula / Explanation"])
1640
+
1641
+ bytes_per_param = PRECISION_MAP.get(quant, 2.0)
1642
+ writer.writerow(["Selected Precision", quant, "User-selected quantization format"])
1643
+ writer.writerow(["Bytes per Parameter", bytes_per_param, f"From PRECISION_MAP['{quant}']"])
1644
+ writer.writerow(["Actual Precision Used", actual_precision, "May differ for LoRA/Full FT (requires bf16)"])
1645
+
1646
+ if actual_precision != quant:
1647
+ actual_bytes = PRECISION_MAP.get(actual_precision, 2.0)
1648
+ writer.writerow(["Actual Bytes per Param", actual_bytes, f"Overridden to {actual_precision} for training"])
1649
+ writer.writerow([])
1650
+
1651
+ # ==========================================
1652
+ # SECTION 5: VRAM CALCULATION BREAKDOWN
1653
+ # ==========================================
1654
+ writer.writerow(["*" * 50])
1655
+ writer.writerow(["VRAM CALCULATION BREAKDOWN"])
1656
+ writer.writerow(["*" * 50])
1657
+ writer.writerow(["Component", "Value (GB)", "Formula"])
1658
+
1659
+ # Model weights calculation
1660
+ actual_bytes_per_param = PRECISION_MAP.get(actual_precision, 2.0)
1661
+ weights_formula = f"{spec.params_bn:.2f}B params × {actual_bytes_per_param} bytes / 1024³"
1662
+ writer.writerow(["Model Weights", f"{weights_gb:.2f}", weights_formula])
1663
+
1664
+ # KV Cache calculation
1665
+ # For training, KV cache uses bf16 (compute precision), not quantized
1666
+ if task == "Training":
1667
+ kv_bytes_per_elem = 2.0 # Always bf16 for training
1668
+ kv_cache_gb = calculate_kv_cache(spec, batch_size, seq_len, "bf16")
1669
+ kv_formula = f"2 × {spec.layers} layers × {batch_size} batch × {seq_len} seq × {spec.kv_heads} kv_heads × {spec.head_dim} head_dim × {kv_bytes_per_elem} bytes (bf16) / 1024³"
1670
+ else:
1671
+ kv_bytes_per_elem = 2.0 if quant in ['nf4', '4bit', 'int4'] else PRECISION_MAP.get(quant, 2.0)
1672
+ kv_cache_gb = calculate_kv_cache(spec, batch_size, seq_len, quant)
1673
+ kv_formula = f"2 × {spec.layers} layers × {batch_size} batch × {seq_len} seq × {spec.kv_heads} kv_heads × {spec.head_dim} head_dim × {kv_bytes_per_elem} bytes / 1024³"
1674
+ writer.writerow(["KV Cache", f"{kv_cache_gb:.2f}", kv_formula])
1675
+
1676
+ if task == "Training":
1677
+ # Activations - always at bf16 compute precision, even for QLoRA
1678
+ # The forward pass computes at full precision, so activations are stored at bf16
1679
+ hidden_size = spec.heads * spec.head_dim
1680
+ activations_gb = calculate_activations(spec, batch_size, seq_len, "bf16")
1681
+ act_formula = f"{batch_size} × {seq_len} × {hidden_size} hidden × {spec.layers} layers × 12 (checkpointing) × 2 bytes (bf16) / 1024³"
1682
+ writer.writerow(["Activations", f"{activations_gb:.2f}", act_formula])
1683
+
1684
+ # Optimizer states
1685
+ optimizer_gb, opt_desc = calculate_optimizer_states(weights_gb, ft_method or "Full Fine-Tuning", rank or 64, spec)
1686
+ if ft_method == "Full Fine-Tuning":
1687
+ opt_formula = f"{weights_gb:.2f} GB weights × 4 (Adam: 2 states × fp32)"
1688
+ else:
1689
+ adapter_params = 2 * (rank or 64) * hidden_size * spec.layers
1690
+ opt_formula = f"LoRA adapters ({adapter_params:,} params) × 4 (Adam states)"
1691
+ writer.writerow(["Optimizer States", f"{optimizer_gb:.2f}", opt_formula])
1692
+
1693
+ # Gradients
1694
+ gradients_gb, grad_desc = calculate_gradients(weights_gb, ft_method or "Full Fine-Tuning", rank or 64, spec)
1695
+ if ft_method == "Full Fine-Tuning":
1696
+ grad_formula = f"{weights_gb:.2f} GB weights × 2 (fp32 gradients)"
1697
+ else:
1698
+ grad_formula = f"LoRA adapter gradients (fp16)"
1699
+ writer.writerow(["Gradients", f"{gradients_gb:.2f}", grad_formula])
1700
+
1701
+ # LoRA adapters
1702
+ if ft_method in ["LoRA", "QLoRA"]:
1703
+ adapter_params = 2 * (rank or 64) * hidden_size * spec.layers
1704
+ adapter_gb = (adapter_params * 2) / (1024**3)
1705
+ adapter_formula = f"2 × {rank} rank × {hidden_size} hidden × {spec.layers} layers × 2 bytes / 1024³"
1706
+ writer.writerow(["LoRA Adapters", f"{adapter_gb:.4f}", adapter_formula])
1707
+ else:
1708
+ # Framework overhead for inference
1709
+ overhead_gb = FRAMEWORK_OVERHEAD.get(framework, 2.0)
1710
+ writer.writerow(["Framework Overhead", f"{overhead_gb:.2f}", f"FRAMEWORK_OVERHEAD['{framework}']"])
1711
+
1712
+ writer.writerow([])
1713
+ raw_total = weights_gb + variable_gb
1714
+ buffer_amount = raw_total * 0.1
1715
+ total_with_buffer = raw_total * 1.1
1716
+ writer.writerow(["TOTAL VRAM (calculated)", f"{raw_total:.2f}", "Sum of all components"])
1717
+ writer.writerow(["Safety Buffer (10%)", f"{buffer_amount:.2f}", "Total × 0.10 (used for GPU selection)"])
1718
+ writer.writerow(["TOTAL VRAM + BUFFER", f"{total_with_buffer:.2f}", "Total × 1.10 (GPUs must have >= this VRAM)"])
1719
+ writer.writerow([])
1720
+
1721
+ # ==========================================
1722
+ # SECTION 6: GPU RECOMMENDATIONS (Top 10)
1723
+ # ==========================================
1724
+ writer.writerow(["*" * 50])
1725
+ writer.writerow(["GPU RECOMMENDATIONS (Top 10 by Cost)"])
1726
+ writer.writerow(["*" * 50])
1727
+ writer.writerow([
1728
+ "Rank", "GPU Configuration", "Total VRAM (GB)", "VRAM/GPU (GB)", "GPU Count",
1729
+ "VRAM Utilization (%)", "Throughput (tok/s)", "Price (₹/hr)",
1730
+ "Cost Efficiency (tok/₹)", "TFLOPS", "Bandwidth (GB/s)"
1731
+ ])
1732
+
1733
+ for i, gpu in enumerate(gpu_recommendations, 1):
1734
+ writer.writerow([
1735
+ i,
1736
+ gpu["name"],
1737
+ gpu["vram"],
1738
+ gpu["vram_per_gpu"],
1739
+ gpu["gpu_count"],
1740
+ f"{gpu['vram_util']:.1f}",
1741
+ f"{gpu['throughput']:.0f}",
1742
+ f"{gpu['price']:.2f}",
1743
+ f"{gpu['cost_efficiency']:.2f}",
1744
+ f"{gpu['tflops']:.0f}",
1745
+ f"{gpu['bandwidth']:.0f}",
1746
+ ])
1747
+
1748
+ writer.writerow([])
1749
+
1750
+ # ==========================================
1751
+ # SECTION 7: FORMULAS REFERENCE
1752
+ # ==========================================
1753
+ writer.writerow(["*" * 50])
1754
+ writer.writerow(["FORMULAS REFERENCE"])
1755
+ writer.writerow(["*" * 50])
1756
+ writer.writerow(["Calculation", "Formula"])
1757
+ writer.writerow(["Model Weights (GB)", "num_parameters × bytes_per_param / 1024³"])
1758
+ writer.writerow(["KV Cache (GB)", "2 × layers × batch × seq_len × kv_heads × head_dim × bytes_per_elem / 1024³"])
1759
+ writer.writerow(["Activations (GB)", "batch × seq_len × hidden_size × layers × multiplier × bytes / 1024³"])
1760
+ writer.writerow(["Optimizer States (GB)", "trainable_params × 8 bytes (Adam: momentum + variance at fp32)"])
1761
+ writer.writerow(["Gradients (GB)", "trainable_params × bytes_per_grad"])
1762
+ writer.writerow(["LoRA Adapter Size", "2 × rank × hidden_dim × layers × 2 bytes"])
1763
+ writer.writerow(["VRAM Utilization (%)", "(required_vram / gpu_vram) × 100"])
1764
+ writer.writerow(["Cost Efficiency", "throughput (tok/s) / price (₹/hr)"])
1765
+ writer.writerow([])
1766
+
1767
+ # ==========================================
1768
+ # SECTION 8: PRECISION MAP REFERENCE
1769
+ # ==========================================
1770
+ writer.writerow(["*" * 50])
1771
+ writer.writerow(["PRECISION MAP REFERENCE"])
1772
+ writer.writerow(["*" * 50])
1773
+ writer.writerow(["Format", "Bytes per Parameter", "Notes"])
1774
+ for fmt, bytes_val in PRECISION_MAP.items():
1775
+ notes = {
1776
+ # "fp32": "Full precision - maximum accuracy",
1777
+ # "float32": "Full precision - maximum accuracy",
1778
+ "bf16": "Brain Float16 - preferred for training",
1779
+ "fp16": "Half precision - standard for inference",
1780
+ "nf4": "NormalFloat4 - QLoRA format",
1781
+ "4bit": "4-bit quantization",
1782
+ "int4": "Integer 4-bit",
1783
+ "int8": "Integer 8-bit - good accuracy/size tradeoff",
1784
+ "awq": "Activation-aware Weight Quantization (inference-only)",
1785
+ "gptq": "GPTQ quantization (inference-only)",
1786
+ }.get(fmt, "")
1787
+ writer.writerow([fmt, bytes_val, notes])
1788
+ writer.writerow([])
1789
+
1790
+ writer.writerow(["*" * 50])
1791
+ writer.writerow(["END OF REPORT"])
1792
+ writer.writerow(["*" * 50])
1793
+
1794
+ return output.getvalue()
1795
+
1796
  def recommend_hardware(
1797
  required_vram: float,
1798
  task: str,
 
1875
  "cost": [],
1876
  "vram_util": [],
1877
  "cost_efficiency": [], # tokens per rupee
1878
+ "gpu_details": [], # Full details for CSV export
1879
  }
1880
 
1881
  for config in chart_configs:
 
1890
  chart_data["cost"].append(price)
1891
  chart_data["vram_util"].append(vram_util)
1892
  # Cost efficiency: tokens per rupee per hour
1893
+ cost_eff = total_tps / price if price > 0 else 0
1894
+ chart_data["cost_efficiency"].append(cost_eff)
1895
+
1896
+ # Full GPU details for CSV export
1897
+ chart_data["gpu_details"].append({
1898
+ "name": config.name,
1899
+ "vram": config.vram,
1900
+ "vram_per_gpu": config.vram_per_gpu,
1901
+ "gpu_count": config.count,
1902
+ "vram_util": vram_util,
1903
+ "throughput": total_tps,
1904
+ "price": price,
1905
+ "cost_efficiency": cost_eff,
1906
+ "tflops": config.tflops,
1907
+ "bandwidth": config.bandwidth,
1908
+ })
1909
 
1910
  # Generate recommendation cards
1911
  def make_card(title: str, emoji: str, config: Optional[GPUConfig]) -> str:
 
1977
  output_tokens: float,
1978
  dataset_tier: str,
1979
  manufacturers: list[str],
1980
+ ) -> Tuple[str, str, str, Any, Optional[str]]:
1981
  """
1982
  Main request processing function.
1983
 
1984
  Orchestrates model resolution, VRAM calculation, and hardware recommendation.
1985
 
1986
  Returns:
1987
+ Tuple of (report, budget_rec, runner_up_rec, combined_chart, csv_filepath)
1988
  """
1989
  try:
1990
  # Validate inputs
 
2042
  )
2043
 
2044
  if error:
2045
+ return error, "", "", None, None
2046
 
2047
  # Generate report
2048
  report = f"""
 
2112
  )
2113
  ])
2114
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2115
  combined_chart.update_layout(
2116
  title_text="GPU Comparison (Top 10 by Cost)",
2117
  xaxis_title="GPU Configuration",
 
2134
  title_font=dict(size=14),
2135
  )
2136
 
2137
+ # Generate CSV content and save to file
2138
+ csv_filepath = None
2139
+ if chart_data and chart_data.get("gpu_details"):
2140
+ csv_content = generate_analysis_csv(
2141
+ model_name=model_name,
2142
+ task=task,
2143
+ quant=quant,
2144
+ framework=fw,
2145
+ batch_size=batch,
2146
+ seq_len=seq_len,
2147
+ ft_method=ft_method,
2148
+ rank=rank,
2149
+ sample_count=sample_count,
2150
+ input_tokens=input_tokens,
2151
+ output_tokens=output_tokens,
2152
+ pricing_tier=dataset_tier,
2153
+ manufacturers=manufacturers,
2154
+ spec=spec,
2155
+ weights_gb=weights_gb,
2156
+ variable_gb=variable_gb,
2157
+ total_vram=total_vram,
2158
+ actual_precision=actual_precision,
2159
+ gpu_recommendations=chart_data["gpu_details"],
2160
+ source=source,
2161
+ )
2162
+
2163
+ # Save CSV to a temporary file
2164
+ import tempfile
2165
+ import os
2166
+
2167
+ # Create a filename based on model and timestamp
2168
+ from datetime import datetime
2169
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
2170
+ safe_model_name = model_name.replace("/", "_").replace(" ", "_")[:30]
2171
+ filename = f"gpu_analysis_{safe_model_name}_{timestamp}.csv"
2172
+
2173
+ # Save to temp directory
2174
+ csv_filepath = os.path.join(tempfile.gettempdir(), filename)
2175
+ with open(csv_filepath, 'w', newline='', encoding='utf-8') as f:
2176
+ f.write(csv_content)
2177
+
2178
+ return report, budget_rec, runner_up_rec, combined_chart, csv_filepath
2179
 
2180
  except Exception as e:
2181
  error_msg = f"""
 
2184
  <p>{str(e)}</p>
2185
  </div>
2186
  """
2187
+ return error_msg, "", "", None, None
2188
 
2189
  # =================================================================================================
2190
  # UI Event Handlers
 
2346
  with gr.Column(elem_classes=["runner-box"]):
2347
  rec_out_2 = gr.Markdown()
2348
 
2349
+ # Download button for CSV export
2350
+ with gr.Row():
2351
+ csv_download = gr.File(
2352
+ label="📥 Download Analysis (CSV)",
2353
+ visible=True,
2354
+ file_count="single",
2355
+ type="filepath",
2356
+ interactive=False,
2357
+ )
2358
+
2359
  # Bar chart comparison section
2360
  with gr.Accordion("📊 GPU Comparison (Top 10 by Cost)", open=False):
2361
  combined_plot = gr.Plot(label="GPU Comparison")
 
2394
  ft_method, rank, batch, sample_count,
2395
  input_tokens, output_tokens, dataset_tier, manufacturers,
2396
  ],
2397
+ outputs=[report_out, rec_out_1, rec_out_2, combined_plot, csv_download]
2398
  )
2399
 
2400
  return demo