Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
| 457 |
-
|
| 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:
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 799 |
|
| 800 |
Example:
|
| 801 |
>>> get_lora_overhead_factor(64, "QLoRA", 7.0, spec)
|
| 802 |
-
|
| 803 |
"""
|
| 804 |
if ft_method not in ["LoRA", "QLoRA"] or rank is None:
|
| 805 |
-
return 1.0 #
|
| 806 |
-
|
| 807 |
-
#
|
| 808 |
-
#
|
| 809 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 810 |
|
| 811 |
if model_size_bn <= 10:
|
| 812 |
-
# 7B models - LoRA
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
256: 0.70, # 30% slowdown
|
| 821 |
}
|
| 822 |
elif model_size_bn <= 20:
|
| 823 |
# 13B models
|
| 824 |
-
|
| 825 |
-
8:
|
| 826 |
-
16:
|
| 827 |
-
32:
|
| 828 |
-
64:
|
| 829 |
-
128:
|
| 830 |
-
256:
|
| 831 |
}
|
| 832 |
elif model_size_bn <= 100:
|
| 833 |
-
# 70B models - LoRA
|
| 834 |
-
#
|
| 835 |
-
|
| 836 |
-
8:
|
| 837 |
-
16:
|
| 838 |
-
32:
|
| 839 |
-
64:
|
| 840 |
-
128:
|
| 841 |
-
256:
|
| 842 |
}
|
| 843 |
else:
|
| 844 |
-
# 405B+ models -
|
| 845 |
-
|
| 846 |
-
8:
|
| 847 |
-
16:
|
| 848 |
-
32:
|
| 849 |
-
64:
|
| 850 |
-
128:
|
| 851 |
-
256:
|
| 852 |
}
|
| 853 |
|
| 854 |
# Find or interpolate for the given rank
|
| 855 |
-
if rank in
|
| 856 |
-
return
|
| 857 |
|
| 858 |
# Interpolate for ranks not in the map
|
| 859 |
-
ranks = sorted(
|
| 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 =
|
| 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
|
| 875 |
else:
|
| 876 |
-
# For very high ranks (> 256),
|
| 877 |
-
return max(
|
| 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.
|
|
|
|
| 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
|
| 948 |
-
# LoRA
|
| 949 |
-
|
| 950 |
if task == "Training":
|
| 951 |
-
|
| 952 |
-
tps_per_gpu *=
|
| 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
|
| 997 |
-
desc += f", LoRA r={rank}, {
|
| 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 |
-
|
| 1065 |
-
|
| 1066 |
-
|
| 1067 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 1777 |
-
|
| 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["
|
| 2093 |
y=throughput_normalized,
|
| 2094 |
marker_color='#22c55e',
|
| 2095 |
-
hovertemplate='%{
|
| 2096 |
-
customdata=chart_data["throughput"]
|
| 2097 |
),
|
| 2098 |
go.Bar(
|
| 2099 |
name='Cost',
|
| 2100 |
-
x=chart_data["
|
| 2101 |
y=cost_normalized,
|
| 2102 |
marker_color='#3b82f6',
|
| 2103 |
-
hovertemplate='%{
|
| 2104 |
-
customdata=chart_data["cost"]
|
| 2105 |
),
|
| 2106 |
go.Bar(
|
| 2107 |
-
name='VRAM
|
| 2108 |
-
x=chart_data["
|
| 2109 |
y=chart_data["vram_util"],
|
| 2110 |
marker_color='#a855f7',
|
| 2111 |
-
hovertemplate='%{
|
|
|
|
| 2112 |
)
|
| 2113 |
])
|
| 2114 |
|
| 2115 |
combined_chart.update_layout(
|
| 2116 |
title_text="GPU Comparison (Top 10 by Cost)",
|
| 2117 |
-
xaxis_title="
|
| 2118 |
yaxis_title="Normalized Value (% of max)",
|
| 2119 |
barmode='group',
|
| 2120 |
-
height=
|
|
|
|
| 2121 |
xaxis_tickangle=-45,
|
| 2122 |
-
margin=dict(b=
|
| 2123 |
legend=dict(
|
| 2124 |
-
orientation="
|
| 2125 |
-
yanchor="
|
| 2126 |
-
y=0.
|
| 2127 |
-
xanchor="
|
| 2128 |
-
x=
|
| 2129 |
-
bgcolor="rgba(255,255,255,0.
|
| 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
|