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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +189 -89
app.py CHANGED
@@ -10,6 +10,7 @@ GPU configurations for Large Language Model (LLM) deployment and training.
10
  Author: Rudali Huidrom
11
  Version: 4.0.0
12
  First Written On: 08 December 2025
 
13
 
14
  Overview
15
  --------
@@ -27,6 +28,9 @@ Key Features
27
  - Multi-GPU configuration support with communication overhead modeling
28
  - Framework-specific optimizations (vLLM, HuggingFace, TensorRT)
29
  - Real-time cost estimation with multiple pricing tiers
 
 
 
30
 
31
  Technical Approach
32
  -----------------
@@ -261,6 +265,46 @@ CUSTOM_CSS = """
261
  outline: 2px solid #3b82f6 !important;
262
  outline-offset: 2px !important;
263
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  </style>
265
  """
266
 
@@ -453,8 +497,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 +656,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: 17 December 2025
616
  # Sources:
617
  # - MLPerf Training v3.1 (November 2023)
618
  # - NVIDIA TensorRT-LLM benchmarks (Q4 2024)
@@ -795,72 +839,85 @@ def get_lora_overhead_factor(
795
  spec: Model specification (for hidden_dim and layers)
796
 
797
  Returns:
798
- Throughput multiplier (< 1.0 means slower due to LoRA overhead)
 
 
 
 
 
 
 
799
 
800
  Example:
801
  >>> get_lora_overhead_factor(64, "QLoRA", 7.0, spec)
802
- 0.87 # 13% throughput reduction with rank=64 on 7B model
803
  """
804
  if ft_method not in ["LoRA", "QLoRA"] or rank is None:
805
- return 1.0 # No LoRA overhead
806
-
807
- # Empirical overhead data from real-world benchmarks
808
- # These account for: kernel launch overhead, memory bandwidth, cache effects
809
- # Not just theoretical FLOPs (which underestimate actual impact)
 
 
 
 
 
 
 
810
 
811
  if model_size_bn <= 10:
812
- # 7B models - LoRA overhead is most significant
813
- # Kernel launch costs and memory bandwidth dominate
814
- overhead_map = {
815
- 8: 0.98, # 2% slowdown
816
- 16: 0.96, # 4% slowdown
817
- 32: 0.92, # 8% slowdown
818
- 64: 0.87, # 13% slowdown
819
- 128: 0.80, # 20% slowdown
820
- 256: 0.70, # 30% slowdown
821
  }
822
  elif model_size_bn <= 20:
823
  # 13B models
824
- overhead_map = {
825
- 8: 0.99,
826
- 16: 0.97,
827
- 32: 0.94,
828
- 64: 0.89,
829
- 128: 0.83,
830
- 256: 0.74,
831
  }
832
  elif model_size_bn <= 100:
833
- # 70B models - LoRA overhead is less significant
834
- # Base model computation dominates
835
- overhead_map = {
836
- 8: 0.99,
837
- 16: 0.98,
838
- 32: 0.96,
839
- 64: 0.92,
840
- 128: 0.87,
841
- 256: 0.79,
842
  }
843
  else:
844
- # 405B+ models - LoRA is proportionally tiny
845
- overhead_map = {
846
- 8: 1.00, # Negligible
847
- 16: 0.99,
848
- 32: 0.98,
849
- 64: 0.95,
850
- 128: 0.91,
851
- 256: 0.84,
852
  }
853
 
854
  # Find or interpolate for the given rank
855
- if rank in overhead_map:
856
- return overhead_map[rank]
857
 
858
  # Interpolate for ranks not in the map
859
- ranks = sorted(overhead_map.keys())
860
  for i in range(len(ranks) - 1):
861
  if ranks[i] < rank < ranks[i+1]:
862
  r1, r2 = ranks[i], ranks[i+1]
863
- v1, v2 = overhead_map[r1], overhead_map[r2]
864
  # Linear interpolation in log-space for smoother scaling
865
  import math
866
  log_rank = math.log(rank)
@@ -871,10 +928,10 @@ def get_lora_overhead_factor(
871
 
872
  # Extrapolate if beyond range
873
  if rank < ranks[0]:
874
- return 1.0 # No overhead for very small ranks (< 8)
875
  else:
876
- # For very high ranks (> 256), assume overhead continues growing
877
- return max(0.5, overhead_map[ranks[-1]] * 0.9)
878
 
879
  def calculate_throughput(
880
  gpu_config: GPUConfig,
@@ -884,7 +941,8 @@ def calculate_throughput(
884
  precision: str = "fp16",
885
  framework: str = "vllm",
886
  ft_method: Optional[str] = None,
887
- rank: Optional[int] = None
 
888
  ) -> Tuple[float, float, str]:
889
  """
890
  Calculate estimated throughput for a GPU configuration.
@@ -893,7 +951,8 @@ def calculate_throughput(
893
  1. Quantization (INT8/INT4 Tensor Cores provide 2-4x speedup)
894
  2. Framework (TensorRT-LLM > vLLM > HuggingFace)
895
  3. Batch size and GPU architecture
896
- 4. LoRA rank (for training - higher rank = more overhead)
 
897
 
898
  Args:
899
  gpu_config: GPU configuration
@@ -903,6 +962,7 @@ def calculate_throughput(
903
  precision: Quantization/precision format (e.g., 'fp16', 'int8', 'int4')
904
  framework: Inference framework ('vllm', 'huggingface', 'tensorrt')
905
  ft_method: Fine-tuning method ('LoRA', 'QLoRA', 'Full Fine-Tuning')
 
906
  rank: LoRA rank (only used for LoRA/QLoRA training)
907
 
908
  Returns:
@@ -944,12 +1004,12 @@ def calculate_throughput(
944
  framework_speedup = FRAMEWORK_SPEEDUP.get(framework.lower(), 1.0)
945
  tps_per_gpu *= framework_speedup
946
 
947
- # Apply LoRA overhead if applicable (TRAINING ONLY)
948
- # LoRA adds extra matrix multiplications that reduce throughput
949
- lora_overhead = 1.0 # Default: no overhead
950
  if task == "Training":
951
- lora_overhead = get_lora_overhead_factor(rank, ft_method, model_size_bn, spec)
952
- tps_per_gpu *= lora_overhead
953
 
954
  # Calculate combined speedup for description
955
  combined_speedup = quant_speedup * framework_speedup
@@ -972,6 +1032,17 @@ def calculate_throughput(
972
  batch_efficiency = (batch_size / 8) ** 0.7
973
  tps_per_gpu *= batch_efficiency
974
 
 
 
 
 
 
 
 
 
 
 
 
975
  # Apply multi-GPU communication overhead
976
  if gpu_config.count > 1:
977
  if gpu_config.count <= 4:
@@ -993,8 +1064,8 @@ def calculate_throughput(
993
  else:
994
  desc = f"Training throughput ({precision})"
995
  if ft_method in ["LoRA", "QLoRA"] and rank:
996
- # Add LoRA rank info and efficiency
997
- desc += f", LoRA r={rank}, {lora_overhead:.0%} efficiency"
998
 
999
  if gpu_config.count > 1:
1000
  desc += f" ({gpu_config.count}x GPUs, {comm_efficiency:.0%} efficiency)"
@@ -1061,10 +1132,12 @@ def validate_inputs(
1061
  if batch > 512:
1062
  warnings.append("WARNING: Very large batch size may exceed VRAM limits")
1063
 
1064
- if rank < 4:
1065
- warnings.append("WARNING: LoRA rank < 4 may be too low for effective fine-tuning")
1066
- if rank > 256:
1067
- warnings.append("WARNING: LoRA rank > 256 may be inefficient (diminishing returns)")
 
 
1068
 
1069
  if sample_count < 1:
1070
  warnings.append("ERROR: Sample count must be at least 1")
@@ -1773,8 +1846,8 @@ def generate_analysis_csv(
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",
@@ -1808,6 +1881,7 @@ def recommend_hardware(
1808
  ft_method: Optional[str] = None,
1809
  rank: Optional[int] = None,
1810
  manufacturers: Optional[list[str]] = None,
 
1811
  ) -> Tuple[Optional[str], str, str, Dict[str, Any]]:
1812
  """
1813
  Recommend GPU configurations based on VRAM requirements.
@@ -1826,6 +1900,8 @@ def recommend_hardware(
1826
  output_tokens: Output tokens per sample
1827
  ft_method: Fine-tuning method (for training tasks)
1828
  rank: LoRA rank (for LoRA/QLoRA training)
 
 
1829
 
1830
  Returns:
1831
  Tuple of (error_message, budget_rec, runner_up_rec, chart_data)
@@ -1876,16 +1952,26 @@ def recommend_hardware(
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:
1882
  price = config.get_price(pricing_tier)
1883
  vram_util = (required_vram / config.vram) * 100
1884
  _, total_tps, _ = calculate_throughput(
1885
- config, spec, task, batch_size, precision, framework, ft_method, rank
1886
  )
1887
 
1888
  chart_data["names"].append(config.name)
 
1889
  chart_data["throughput"].append(total_tps)
1890
  chart_data["cost"].append(price)
1891
  chart_data["vram_util"].append(vram_util)
@@ -1916,7 +2002,7 @@ def recommend_hardware(
1916
  vram_util = (required_vram / config.vram) * 100
1917
 
1918
  tps_per_gpu, total_tps, throughput_desc = calculate_throughput(
1919
- config, spec, task, batch_size, precision, framework, ft_method, rank
1920
  )
1921
 
1922
  time_estimate = ""
@@ -1993,7 +2079,7 @@ def process_request(
1993
  # Convert to integers
1994
  seq_len = int(seq_len)
1995
  batch = int(batch)
1996
- rank = int(rank)
1997
  sample_count = int(sample_count)
1998
  input_tokens = int(input_tokens)
1999
  output_tokens = int(output_tokens)
@@ -2039,6 +2125,7 @@ def process_request(
2039
  error, budget_rec, runner_up_rec, chart_data = recommend_hardware(
2040
  total_vram, task, spec, weights_gb, batch, dataset_tier, actual_precision, fw,
2041
  sample_count, input_tokens, output_tokens, ft_method, rank, manufacturers=manufacturers,
 
2042
  )
2043
 
2044
  if error:
@@ -2089,49 +2176,62 @@ def process_request(
2089
  combined_chart = go.Figure(data=[
2090
  go.Bar(
2091
  name='Throughput',
2092
- x=chart_data["names"],
2093
  y=throughput_normalized,
2094
  marker_color='#22c55e',
2095
- hovertemplate='%{x}<br>Throughput: %{customdata:,.0f} tok/s<extra></extra>',
2096
- customdata=chart_data["throughput"]
2097
  ),
2098
  go.Bar(
2099
  name='Cost',
2100
- x=chart_data["names"],
2101
  y=cost_normalized,
2102
  marker_color='#3b82f6',
2103
- hovertemplate='%{x}<br>Cost: ₹%{customdata:,.2f}/hr<extra></extra>',
2104
- customdata=chart_data["cost"]
2105
  ),
2106
  go.Bar(
2107
- name='VRAM Utilization',
2108
- x=chart_data["names"],
2109
  y=chart_data["vram_util"],
2110
  marker_color='#a855f7',
2111
- hovertemplate='%{x}<br>VRAM Util: %{y:.1f}%<extra></extra>'
 
2112
  )
2113
  ])
2114
 
2115
  combined_chart.update_layout(
2116
  title_text="GPU Comparison (Top 10 by Cost)",
2117
- xaxis_title="GPU Configuration",
2118
  yaxis_title="Normalized Value (% of max)",
2119
  barmode='group',
2120
- height=450,
 
2121
  xaxis_tickangle=-45,
2122
- margin=dict(b=120, t=40, l=60, r=140), # Right margin for legend
2123
  legend=dict(
2124
- orientation="v", # Vertical legend
2125
- yanchor="middle",
2126
- y=0.5,
2127
- xanchor="left",
2128
- x=1.02, # Position to the right of chart
2129
- bgcolor="rgba(255,255,255,0.8)",
2130
  bordercolor="rgba(0,0,0,0.1)",
2131
- borderwidth=1
 
 
 
 
 
 
 
 
 
 
 
 
 
2132
  ),
2133
- font=dict(size=11),
2134
- title_font=dict(size=14),
2135
  )
2136
 
2137
  # Generate CSV content and save to file
 
10
  Author: Rudali Huidrom
11
  Version: 4.0.0
12
  First Written On: 08 December 2025
13
+ Last Updated: 18 December 2025
14
 
15
  Overview
16
  --------
 
28
  - Multi-GPU configuration support with communication overhead modeling
29
  - Framework-specific optimizations (vLLM, HuggingFace, TensorRT)
30
  - Real-time cost estimation with multiple pricing tiers
31
+ - Throughput scaling with sequence length
32
+ - CSV export with detailed analysis and formulas
33
+ - Responsive UI with dark mode support
34
 
35
  Technical Approach
36
  -----------------
 
265
  outline: 2px solid #3b82f6 !important;
266
  outline-offset: 2px !important;
267
  }
268
+
269
+ /* ============================================
270
+ PLOTLY CHART RESPONSIVE STYLES
271
+ ============================================ */
272
+ /* Make Plotly chart container responsive */
273
+ .js-plotly-plot, .plotly {
274
+ width: 100% !important;
275
+ }
276
+
277
+ .js-plotly-plot .plotly .main-svg {
278
+ width: 100% !important;
279
+ }
280
+
281
+ /* Mobile: Ensure chart doesn't overflow */
282
+ @media screen and (max-width: 768px) {
283
+ .js-plotly-plot, .plotly, .plot-container {
284
+ width: 100% !important;
285
+ overflow-x: auto !important;
286
+ }
287
+
288
+ /* Make Gradio plot component full width */
289
+ .gradio-plot {
290
+ width: 100% !important;
291
+ min-height: 400px !important;
292
+ }
293
+ }
294
+
295
+ /* Small mobile: Compact chart */
296
+ @media screen and (max-width: 480px) {
297
+ .gradio-plot {
298
+ min-height: 350px !important;
299
+ }
300
+ }
301
+
302
+ /* Landscape phone: Give more width to chart */
303
+ @media screen and (max-width: 900px) and (orientation: landscape) {
304
+ .gradio-plot {
305
+ min-height: 300px !important;
306
+ }
307
+ }
308
  </style>
309
  """
310
 
 
497
  # Note: nf4 (NormalFloat4) is specifically designed for QLoRA and provides
498
  # better quality than standard int4 at the same bitwidth
499
  PRECISION_MAP = {
500
+ "float32": 4.0,
501
+ "fp32": 4.0,
502
  "bf16": 2.0, # BFloat16 - preferred for training (better range than fp16)
503
  "fp16": 2.0, # Float16 - standard for inference
504
  "nf4": 0.5625, # NormalFloat4 - QLoRA's quantization format
 
656
  # Based on real-world benchmarks from MLPerf, vendor data, and community testing
657
  # Tokens per second per GPU for different model sizes
658
  #
659
+ # Last Updated: 16 December 2025
660
  # Sources:
661
  # - MLPerf Training v3.1 (November 2023)
662
  # - NVIDIA TensorRT-LLM benchmarks (Q4 2024)
 
839
  spec: Model specification (for hidden_dim and layers)
840
 
841
  Returns:
842
+ Throughput multiplier relative to Full Fine-Tuning baseline.
843
+ > 1.0 means faster (LoRA/QLoRA train fewer parameters)
844
+ = 1.0 means baseline (Full FT)
845
+
846
+ Real-world behavior:
847
+ - Full FT: Updates ALL parameters = baseline (slowest)
848
+ - LoRA: Updates only adapter params (~0.1-1%) = 2-4x faster
849
+ - QLoRA: Same as LoRA but quant overhead reduces speedup slightly
850
 
851
  Example:
852
  >>> get_lora_overhead_factor(64, "QLoRA", 7.0, spec)
853
+ 2.5 # 2.5x faster than Full FT with rank=64 on 7B model
854
  """
855
  if ft_method not in ["LoRA", "QLoRA"] or rank is None:
856
+ return 1.0 # Full FT baseline - no speedup
857
+
858
+ # LoRA/QLoRA speedup factors based on empirical benchmarks
859
+ # The speedup comes from:
860
+ # 1. Training only adapter parameters (0.1-1% of model)
861
+ # 2. Smaller gradient computations
862
+ # 3. Reduced optimizer state updates
863
+ #
864
+ # However, there are overheads:
865
+ # 1. Forward pass still processes full model
866
+ # 2. Adapter computations add some latency
867
+ # 3. Higher ranks = more adapter params = less speedup
868
 
869
  if model_size_bn <= 10:
870
+ # 7B models - LoRA provides significant speedup
871
+ speedup_map = {
872
+ 8: 3.5, # Very small adapter = big speedup
873
+ 16: 3.2,
874
+ 32: 2.8,
875
+ 64: 2.4,
876
+ 128: 2.0,
877
+ 256: 1.6,
 
878
  }
879
  elif model_size_bn <= 20:
880
  # 13B models
881
+ speedup_map = {
882
+ 8: 3.2,
883
+ 16: 2.9,
884
+ 32: 2.5,
885
+ 64: 2.2,
886
+ 128: 1.8,
887
+ 256: 1.5,
888
  }
889
  elif model_size_bn <= 100:
890
+ # 70B models - LoRA speedup is proportionally larger
891
+ # because adapter is even smaller relative to model
892
+ speedup_map = {
893
+ 8: 4.0,
894
+ 16: 3.6,
895
+ 32: 3.0,
896
+ 64: 2.5,
897
+ 128: 2.0,
898
+ 256: 1.6,
899
  }
900
  else:
901
+ # 405B+ models - largest relative speedup
902
+ speedup_map = {
903
+ 8: 4.5,
904
+ 16: 4.0,
905
+ 32: 3.4,
906
+ 64: 2.8,
907
+ 128: 2.2,
908
+ 256: 1.8,
909
  }
910
 
911
  # Find or interpolate for the given rank
912
+ if rank in speedup_map:
913
+ return speedup_map[rank]
914
 
915
  # Interpolate for ranks not in the map
916
+ ranks = sorted(speedup_map.keys())
917
  for i in range(len(ranks) - 1):
918
  if ranks[i] < rank < ranks[i+1]:
919
  r1, r2 = ranks[i], ranks[i+1]
920
+ v1, v2 = speedup_map[r1], speedup_map[r2]
921
  # Linear interpolation in log-space for smoother scaling
922
  import math
923
  log_rank = math.log(rank)
 
928
 
929
  # Extrapolate if beyond range
930
  if rank < ranks[0]:
931
+ return speedup_map[ranks[0]] # Use smallest rank speedup
932
  else:
933
+ # For very high ranks (> 256), speedup diminishes toward 1.0
934
+ return max(1.2, speedup_map[ranks[-1]] * 0.9)
935
 
936
  def calculate_throughput(
937
  gpu_config: GPUConfig,
 
941
  precision: str = "fp16",
942
  framework: str = "vllm",
943
  ft_method: Optional[str] = None,
944
+ rank: Optional[int] = None,
945
+ seq_len: int = 2048
946
  ) -> Tuple[float, float, str]:
947
  """
948
  Calculate estimated throughput for a GPU configuration.
 
951
  1. Quantization (INT8/INT4 Tensor Cores provide 2-4x speedup)
952
  2. Framework (TensorRT-LLM > vLLM > HuggingFace)
953
  3. Batch size and GPU architecture
954
+ 4. Sequence length (longer sequences reduce throughput)
955
+ 5. LoRA rank (for training - higher rank = more overhead)
956
 
957
  Args:
958
  gpu_config: GPU configuration
 
962
  precision: Quantization/precision format (e.g., 'fp16', 'int8', 'int4')
963
  framework: Inference framework ('vllm', 'huggingface', 'tensorrt')
964
  ft_method: Fine-tuning method ('LoRA', 'QLoRA', 'Full Fine-Tuning')
965
+ seq_len: Sequence length (affects KV cache access and attention compute)
966
  rank: LoRA rank (only used for LoRA/QLoRA training)
967
 
968
  Returns:
 
1004
  framework_speedup = FRAMEWORK_SPEEDUP.get(framework.lower(), 1.0)
1005
  tps_per_gpu *= framework_speedup
1006
 
1007
+ # Apply LoRA/QLoRA speedup if applicable (TRAINING ONLY)
1008
+ # LoRA/QLoRA train only adapter parameters = faster than Full FT
1009
+ lora_speedup = 1.0 # Default: Full FT baseline (no speedup)
1010
  if task == "Training":
1011
+ lora_speedup = get_lora_overhead_factor(rank, ft_method, model_size_bn, spec)
1012
+ tps_per_gpu *= lora_speedup
1013
 
1014
  # Calculate combined speedup for description
1015
  combined_speedup = quant_speedup * framework_speedup
 
1032
  batch_efficiency = (batch_size / 8) ** 0.7
1033
  tps_per_gpu *= batch_efficiency
1034
 
1035
+ # Apply sequence length scaling
1036
+ # Longer sequences reduce throughput due to:
1037
+ # 1. Increased KV cache memory bandwidth
1038
+ # 2. O(n²) attention complexity (though optimized with Flash Attention)
1039
+ # 3. More memory pressure reducing effective parallelism
1040
+ # Baseline: 2048 tokens, ~15% reduction per doubling of sequence length
1041
+ SEQ_LEN_BASELINE = 2048
1042
+ if seq_len != SEQ_LEN_BASELINE:
1043
+ seq_factor = (SEQ_LEN_BASELINE / seq_len) ** 0.15
1044
+ tps_per_gpu *= seq_factor
1045
+
1046
  # Apply multi-GPU communication overhead
1047
  if gpu_config.count > 1:
1048
  if gpu_config.count <= 4:
 
1064
  else:
1065
  desc = f"Training throughput ({precision})"
1066
  if ft_method in ["LoRA", "QLoRA"] and rank:
1067
+ # Add LoRA rank info and speedup factor
1068
+ desc += f", LoRA r={rank}, {lora_speedup:.1f}x vs Full FT"
1069
 
1070
  if gpu_config.count > 1:
1071
  desc += f" ({gpu_config.count}x GPUs, {comm_efficiency:.0%} efficiency)"
 
1132
  if batch > 512:
1133
  warnings.append("WARNING: Very large batch size may exceed VRAM limits")
1134
 
1135
+ # Only validate rank for training with LoRA/QLoRA
1136
+ if rank is not None:
1137
+ if rank < 4:
1138
+ warnings.append("WARNING: LoRA rank < 4 may be too low for effective fine-tuning")
1139
+ if rank > 256:
1140
+ warnings.append("WARNING: LoRA rank > 256 may be inefficient (diminishing returns)")
1141
 
1142
  if sample_count < 1:
1143
  warnings.append("ERROR: Sample count must be at least 1")
 
1846
  writer.writerow(["Format", "Bytes per Parameter", "Notes"])
1847
  for fmt, bytes_val in PRECISION_MAP.items():
1848
  notes = {
1849
+ "fp32": "Full precision - maximum accuracy",
1850
+ "float32": "Full precision - maximum accuracy",
1851
  "bf16": "Brain Float16 - preferred for training",
1852
  "fp16": "Half precision - standard for inference",
1853
  "nf4": "NormalFloat4 - QLoRA format",
 
1881
  ft_method: Optional[str] = None,
1882
  rank: Optional[int] = None,
1883
  manufacturers: Optional[list[str]] = None,
1884
+ seq_len: int = 2048,
1885
  ) -> Tuple[Optional[str], str, str, Dict[str, Any]]:
1886
  """
1887
  Recommend GPU configurations based on VRAM requirements.
 
1900
  output_tokens: Output tokens per sample
1901
  ft_method: Fine-tuning method (for training tasks)
1902
  rank: LoRA rank (for LoRA/QLoRA training)
1903
+ manufacturers: List of GPU manufacturers to filter by
1904
+ seq_len: Sequence length (affects throughput calculation)
1905
 
1906
  Returns:
1907
  Tuple of (error_message, budget_rec, runner_up_rec, chart_data)
 
1952
  "vram_util": [],
1953
  "cost_efficiency": [], # tokens per rupee
1954
  "gpu_details": [], # Full details for CSV export
1955
+ "short_names": [], # Shortened names for mobile-friendly chart display
1956
  }
1957
 
1958
+ def get_short_name(full_name: str) -> str:
1959
+ """Create a shorter display name for charts."""
1960
+ # Remove vendor prefix and simplify
1961
+ name = full_name.replace("Nvidia ", "").replace("AMD ", "").replace("Intel ", "")
1962
+ # Shorten common patterns
1963
+ name = name.replace(" SXM ", " ").replace(" NVL ", " ").replace(" PCIe ", " ")
1964
+ return name
1965
+
1966
  for config in chart_configs:
1967
  price = config.get_price(pricing_tier)
1968
  vram_util = (required_vram / config.vram) * 100
1969
  _, total_tps, _ = calculate_throughput(
1970
+ config, spec, task, batch_size, precision, framework, ft_method, rank, seq_len
1971
  )
1972
 
1973
  chart_data["names"].append(config.name)
1974
+ chart_data["short_names"].append(get_short_name(config.name))
1975
  chart_data["throughput"].append(total_tps)
1976
  chart_data["cost"].append(price)
1977
  chart_data["vram_util"].append(vram_util)
 
2002
  vram_util = (required_vram / config.vram) * 100
2003
 
2004
  tps_per_gpu, total_tps, throughput_desc = calculate_throughput(
2005
+ config, spec, task, batch_size, precision, framework, ft_method, rank, seq_len
2006
  )
2007
 
2008
  time_estimate = ""
 
2079
  # Convert to integers
2080
  seq_len = int(seq_len)
2081
  batch = int(batch)
2082
+ rank = int(rank) if rank is not None else None
2083
  sample_count = int(sample_count)
2084
  input_tokens = int(input_tokens)
2085
  output_tokens = int(output_tokens)
 
2125
  error, budget_rec, runner_up_rec, chart_data = recommend_hardware(
2126
  total_vram, task, spec, weights_gb, batch, dataset_tier, actual_precision, fw,
2127
  sample_count, input_tokens, output_tokens, ft_method, rank, manufacturers=manufacturers,
2128
+ seq_len=seq_len,
2129
  )
2130
 
2131
  if error:
 
2176
  combined_chart = go.Figure(data=[
2177
  go.Bar(
2178
  name='Throughput',
2179
+ x=chart_data["short_names"], # Use shorter names for mobile
2180
  y=throughput_normalized,
2181
  marker_color='#22c55e',
2182
+ hovertemplate='%{customdata[0]}<br>Throughput: %{customdata[1]:,.0f} tok/s<extra></extra>',
2183
+ customdata=list(zip(chart_data["names"], chart_data["throughput"])) # Full name in hover
2184
  ),
2185
  go.Bar(
2186
  name='Cost',
2187
+ x=chart_data["short_names"],
2188
  y=cost_normalized,
2189
  marker_color='#3b82f6',
2190
+ hovertemplate='%{customdata[0]}<br>Cost: ₹%{customdata[1]:,.2f}/hr<extra></extra>',
2191
+ customdata=list(zip(chart_data["names"], chart_data["cost"]))
2192
  ),
2193
  go.Bar(
2194
+ name='VRAM Util',
2195
+ x=chart_data["short_names"],
2196
  y=chart_data["vram_util"],
2197
  marker_color='#a855f7',
2198
+ hovertemplate='%{customdata}<br>VRAM Util: %{y:.1f}%<extra></extra>',
2199
+ customdata=chart_data["names"]
2200
  )
2201
  ])
2202
 
2203
  combined_chart.update_layout(
2204
  title_text="GPU Comparison (Top 10 by Cost)",
2205
+ xaxis_title="", # Remove redundant title - GPU names are self-explanatory
2206
  yaxis_title="Normalized Value (% of max)",
2207
  barmode='group',
2208
+ height=520, # Slightly taller for legend spacing
2209
+ autosize=True, # Enable responsive sizing
2210
  xaxis_tickangle=-45,
2211
+ margin=dict(b=160, t=50, l=50, r=20), # More bottom margin for legend
2212
  legend=dict(
2213
+ orientation="h", # Horizontal legend
2214
+ yanchor="top",
2215
+ y=-0.38, # Move further below to avoid overlap
2216
+ xanchor="center",
2217
+ x=0.5,
2218
+ bgcolor="rgba(255,255,255,0.9)",
2219
  bordercolor="rgba(0,0,0,0.1)",
2220
+ borderwidth=1,
2221
+ font=dict(size=12),
2222
+ itemsizing='constant',
2223
+ traceorder='normal',
2224
+ ),
2225
+ font=dict(size=10),
2226
+ title_font=dict(size=13),
2227
+ # Make x-axis labels more readable
2228
+ xaxis=dict(
2229
+ tickfont=dict(size=9),
2230
+ ),
2231
+ yaxis=dict(
2232
+ tickfont=dict(size=9),
2233
+ title_font=dict(size=11),
2234
  ),
 
 
2235
  )
2236
 
2237
  # Generate CSV content and save to file