Boof2015 commited on
Commit
ef4950b
·
verified ·
1 Parent(s): 1e20b1d

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1173 -18
index.html CHANGED
@@ -1,19 +1,1174 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>VRAM Estimator | NovaAI</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
10
+
11
+ <style>
12
+ :root {
13
+ --bg-body: #0f0f0f;
14
+ --bg-card: #141414;
15
+ --bg-card-hover: #1a1a1a;
16
+ --bg-input: #0a0a0a;
17
+ --border-color: #27272a;
18
+ --border-focus: #3b82f6;
19
+
20
+ --text-main: #e4e4e7;
21
+ --text-muted: #a1a1aa;
22
+ --text-dim: #52525b;
23
+
24
+ --accent: #3b82f6;
25
+ --accent-glow: rgba(59, 130, 246, 0.15);
26
+
27
+ --color-weights: #3b82f6;
28
+ --color-cache: #8b5cf6;
29
+ --color-overhead: #71717a;
30
+ --color-overload: #ef4444;
31
+
32
+ --success: #10b981;
33
+ --warning: #f59e0b;
34
+ --danger: #ef4444;
35
+ }
36
+
37
+ /* Prevent body conflicts with WordPress theme */
38
+ body.page .vram-calculator-wrapper {
39
+ display: block !important;
40
+ }
41
+
42
+ /* Calculator wrapper - contains all styles */
43
+ .vram-calculator-wrapper {
44
+ background-color: var(--bg-body);
45
+ color: var(--text-main);
46
+ font-family: 'Inter', sans-serif;
47
+ font-size: 14px;
48
+ line-height: 1.5;
49
+ min-height: 100vh;
50
+ }
51
+
52
+ .vram-calculator-wrapper * {
53
+ margin: 0;
54
+ padding: 0;
55
+ box-sizing: border-box;
56
+ }
57
+
58
+ .vram-calculator-wrapper .app-container {
59
+ max-width: 1400px;
60
+ margin: 0 auto;
61
+ padding: 2rem;
62
+ width: 100%;
63
+ flex: 1;
64
+ display: flex;
65
+ flex-direction: column;
66
+ gap: 2rem;
67
+ }
68
+
69
+ .vram-calculator-wrapper header {
70
+ display: flex;
71
+ justify-content: space-between;
72
+ align-items: center;
73
+ padding-bottom: 1.5rem;
74
+ border-bottom: 1px solid var(--border-color);
75
+ }
76
+
77
+ .vram-calculator-wrapper h1 { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.025em; }
78
+ .vram-calculator-wrapper .brand { color: var(--accent); }
79
+
80
+ .vram-calculator-wrapper .dashboard {
81
+ display: grid;
82
+ grid-template-columns: 1fr 400px;
83
+ gap: 2rem;
84
+ align-items: start;
85
+ }
86
+ @media (max-width: 1024px) {
87
+ .vram-calculator-wrapper .dashboard { grid-template-columns: 1fr; }
88
+ }
89
+
90
+ /* --- Cards --- */
91
+ .vram-calculator-wrapper .card {
92
+ background: var(--bg-card);
93
+ border: 1px solid var(--border-color);
94
+ border-radius: 12px;
95
+ transition: border-color 0.2s;
96
+ position: relative;
97
+ }
98
+
99
+ .vram-calculator-wrapper .card-header {
100
+ padding: 1rem 1.5rem;
101
+ border-bottom: 1px solid var(--border-color);
102
+ background: rgba(255,255,255,0.02);
103
+ font-weight: 600;
104
+ display: flex;
105
+ align-items: center;
106
+ gap: 0.5rem;
107
+ color: var(--text-main);
108
+ border-radius: 12px 12px 0 0;
109
+ }
110
+
111
+ .vram-calculator-wrapper .card-body { padding: 1.5rem; }
112
+
113
+ /* --- Inputs --- */
114
+ .vram-calculator-wrapper .input-group { margin-bottom: 1.25rem; position: relative; }
115
+ .vram-calculator-wrapper .input-group:last-child { margin-bottom: 0; }
116
+ .vram-calculator-wrapper .input-group label {
117
+ display: block;
118
+ color: var(--text-muted);
119
+ font-size: 0.85rem;
120
+ margin-bottom: 0.5rem;
121
+ font-weight: 500;
122
+ }
123
+
124
+ .vram-calculator-wrapper input[type="text"],
125
+ .vram-calculator-wrapper input[type="number"],
126
+ .vram-calculator-wrapper input[type="password"],
127
+ .vram-calculator-wrapper select {
128
+ width: 100%;
129
+ background: var(--bg-input) !important;
130
+ border: 1px solid var(--border-color) !important;
131
+ color: var(--text-main) !important;
132
+ padding: 0 1rem !important;
133
+ height: 42px !important;
134
+ border-radius: 6px !important;
135
+ font-family: inherit !important;
136
+ font-size: 0.9rem !important;
137
+ transition: all 0.2s ease !important;
138
+ -webkit-appearance: none !important;
139
+ -moz-appearance: none !important;
140
+ appearance: none !important;
141
+ background-image: none !important;
142
+ }
143
+
144
+ /* Custom dropdown arrow for selects */
145
+ .vram-calculator-wrapper select {
146
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23a1a1aa' d='M6 9L1 4h10z'/%3E%3C/svg%3E") !important;
147
+ background-repeat: no-repeat !important;
148
+ background-position: right 1rem center !important;
149
+ padding-right: 2.5rem !important;
150
+ }
151
+
152
+ /* Remove any WordPress/theme pseudo-elements */
153
+ .vram-calculator-wrapper select::after,
154
+ .vram-calculator-wrapper select::before {
155
+ display: none !important;
156
+ content: none !important;
157
+ }
158
+
159
+ .vram-calculator-wrapper input:focus,
160
+ .vram-calculator-wrapper select:focus {
161
+ outline: none !important;
162
+ border-color: var(--border-focus) !important;
163
+ box-shadow: 0 0 0 2px var(--accent-glow) !important;
164
+ }
165
+
166
+ .vram-calculator-wrapper .form-row {
167
+ display: grid;
168
+ grid-template-columns: 1fr 1fr;
169
+ gap: 1.5rem;
170
+ margin-top: 1.25rem;
171
+ }
172
+ @media (max-width: 700px) {
173
+ .vram-calculator-wrapper .form-row { grid-template-columns: 1fr; gap: 1rem; }
174
+ }
175
+
176
+ .vram-calculator-wrapper .form-grid {
177
+ display: grid;
178
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
179
+ gap: 1rem;
180
+ }
181
+
182
+ .vram-calculator-wrapper .btn-primary {
183
+ background: var(--accent);
184
+ color: white;
185
+ border: none;
186
+ padding: 0 1.5rem;
187
+ height: 42px;
188
+ border-radius: 6px;
189
+ font-weight: 600;
190
+ cursor: pointer;
191
+ transition: opacity 0.2s;
192
+ white-space: nowrap;
193
+ display: inline-flex;
194
+ align-items: center;
195
+ justify-content: center;
196
+ }
197
+ .vram-calculator-wrapper .btn-primary:hover { opacity: 0.9; }
198
+
199
+ /* --- Visualization --- */
200
+ .vram-calculator-wrapper .viz-container { margin-top: 1.5rem; }
201
+ .vram-calculator-wrapper .viz-bar-wrapper { height: 32px; background: var(--bg-input); border-radius: 6px; display: flex; overflow: hidden; position: relative; }
202
+ .vram-calculator-wrapper .viz-segment { height: 100%; transition: width 0.4s ease; }
203
+ .vram-calculator-wrapper .seg-model { background: var(--color-weights); }
204
+ .vram-calculator-wrapper .seg-kv { background: var(--color-cache); }
205
+ .vram-calculator-wrapper .seg-sys { background: var(--color-overhead); }
206
+ .vram-calculator-wrapper .seg-over { background: repeating-linear-gradient(45deg, var(--color-overload), var(--color-overload) 10px, #b91c1c 10px, #b91c1c 20px); }
207
+ .vram-calculator-wrapper .legend { display: flex; gap: 1.25rem; margin-top: 1rem; flex-wrap: wrap; }
208
+ .vram-calculator-wrapper .legend-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; color: var(--text-muted); }
209
+ .vram-calculator-wrapper .dot { width: 10px; height: 10px; border-radius: 3px; }
210
+ .vram-calculator-wrapper .dot.seg-model { background: var(--color-weights); }
211
+ .vram-calculator-wrapper .dot.seg-kv { background: var(--color-cache); }
212
+ .vram-calculator-wrapper .dot.seg-sys { background: var(--color-overhead); }
213
+ .vram-calculator-wrapper .dot.seg-over { background: var(--color-overload); }
214
+
215
+ .vram-calculator-wrapper .limit-line { position: absolute; top: -4px; bottom: -4px; width: 3px; background: var(--text-main); border-radius: 2px; z-index: 10; display: none; box-shadow: 0 0 8px rgba(255,255,255,0.5); }
216
+
217
+ /* Recommendations */
218
+ .vram-calculator-wrapper .recs-box { margin-top: 1.5rem; padding: 1rem; border-radius: 8px; background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16, 185, 129, 0.2); }
219
+ .vram-calculator-wrapper .recs-box.warning { background: rgba(245, 158, 11, 0.1); border-color: rgba(245, 158, 11, 0.3); }
220
+ .vram-calculator-wrapper .recs-box.danger { background: rgba(239, 68, 68, 0.1); border-color: rgba(239, 68, 68, 0.3); }
221
+ .vram-calculator-wrapper .recs-title { font-weight: 600; margin-bottom: 0.75rem; }
222
+ .vram-calculator-wrapper .rec-step { display: flex; align-items: center; gap: 0.5rem; margin: 0.5rem 0; font-size: 0.9rem; }
223
+ .vram-calculator-wrapper .rec-tag { font-size: 0.7rem; padding: 2px 6px; border-radius: 4px; font-weight: 600; text-transform: uppercase; }
224
+ .vram-calculator-wrapper .tag-quant { background: rgba(59, 130, 246, 0.2); color: #60a5fa; }
225
+ .vram-calculator-wrapper .tag-ctx { background: rgba(139, 92, 246, 0.2); color: #a78bfa; }
226
+ .vram-calculator-wrapper .tag-cache { background: rgba(16, 185, 129, 0.2); color: #34d399; }
227
+ .vram-calculator-wrapper .rec-solution { font-weight: 500; margin-bottom: 0.5rem; color: var(--text-main); }
228
+
229
+ /* Specs Grid */
230
+ .vram-calculator-wrapper .specs-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; }
231
+ .vram-calculator-wrapper .spec-item { display: flex; flex-direction: column; gap: 0.25rem; }
232
+ .vram-calculator-wrapper .spec-label { font-size: 0.75rem; color: var(--text-dim); text-transform: uppercase; }
233
+ .vram-calculator-wrapper .spec-val { font-family: 'JetBrains Mono', monospace; font-size: 1rem; color: var(--text-main); }
234
+
235
+ /* Searchable dropdown */
236
+ .vram-calculator-wrapper .dropdown-container { position: relative; }
237
+ .vram-calculator-wrapper .dropdown-results { position: absolute; top: 100%; left: 0; right: 0; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 6px; max-height: 200px; overflow-y: auto; z-index: 50; display: none; margin-top: 4px; }
238
+ .vram-calculator-wrapper .dropdown-results.active { display: block; }
239
+ .vram-calculator-wrapper .dropdown-item { padding: 0.75rem 1rem; cursor: pointer; display: flex; justify-content: space-between; align-items: center; }
240
+ .vram-calculator-wrapper .dropdown-item:hover { background: var(--bg-card-hover); }
241
+ .vram-calculator-wrapper .dropdown-meta { font-size: 0.8rem; color: var(--text-dim); font-family: 'JetBrains Mono', monospace; }
242
+
243
+ /* GPU Grid */
244
+ .vram-calculator-wrapper .gpu-section { margin-top: 2rem; }
245
+ .vram-calculator-wrapper .gpu-section-title {
246
+ font-size: 1rem;
247
+ font-weight: 600;
248
+ margin-bottom: 1rem;
249
+ color: var(--text-main);
250
+ display: flex;
251
+ justify-content: space-between;
252
+ align-items: center;
253
+ gap: 1rem;
254
+ flex-wrap: wrap;
255
+ }
256
+ .vram-calculator-wrapper .gpu-section-title select {
257
+ max-width: 200px;
258
+ min-width: 150px;
259
+ flex-shrink: 0;
260
+ }
261
+ @media (max-width: 600px) {
262
+ .vram-calculator-wrapper .gpu-section-title {
263
+ flex-direction: column;
264
+ align-items: flex-start;
265
+ }
266
+ .vram-calculator-wrapper .gpu-section-title select {
267
+ width: 100%;
268
+ max-width: none;
269
+ }
270
+ }
271
+ .vram-calculator-wrapper .gpu-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem; }
272
+ .vram-calculator-wrapper .gpu-card { background: var(--bg-card); border: 1px solid var(--border-color); padding: 1.25rem; border-radius: 10px; transition: all 0.2s; }
273
+ .vram-calculator-wrapper .gpu-card:hover { transform: translateY(-2px); border-color: var(--border-focus); }
274
+ .vram-calculator-wrapper .gpu-top { display: flex; justify-content: space-between; margin-bottom: 1rem; }
275
+ .vram-calculator-wrapper .gpu-name { font-weight: 600; font-size: 0.95rem; color: var(--text-main); }
276
+ .vram-calculator-wrapper .gpu-vram { font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; color: var(--text-muted); background: rgba(255,255,255,0.05); padding: 2px 6px; border-radius: 4px; }
277
+ .vram-calculator-wrapper .gpu-bar-bg { height: 6px; background: var(--bg-input); border-radius: 3px; overflow: hidden; margin-bottom: 0.75rem; }
278
+ .vram-calculator-wrapper .gpu-bar-fill { height: 100%; border-radius: 3px; transition: width 0.3s ease; }
279
+
280
+ .vram-calculator-wrapper .hidden { display: none !important; }
281
+ .vram-calculator-wrapper .sticky-panel { position: sticky; top: 2rem; }
282
+ .vram-calculator-wrapper .checkbox-group { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; user-select: none; padding: 0.75rem; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-input); }
283
+ .vram-calculator-wrapper .spinner { width: 16px; height: 16px; border: 2px solid rgba(255,255,255,0.3); border-radius: 50%; border-top-color: #fff; animation: spin 0.8s linear infinite; }
284
+ @keyframes spin { to { transform: rotate(360deg); } }
285
+
286
+ ::-webkit-scrollbar { width: 8px; }
287
+ ::-webkit-scrollbar-track { background: var(--bg-body); }
288
+ ::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 4px; }
289
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
290
+
291
+ .vram-calculator-wrapper .mobile-footer { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: var(--bg-card); border-top: 1px solid var(--border-color); padding: 1rem; z-index: 100; box-shadow: 0 -4px 20px rgba(0,0,0,0.5); }
292
+ @media (max-width: 768px) {
293
+ .vram-calculator-wrapper .mobile-footer { display: flex; justify-content: space-between; align-items: center; }
294
+ .vram-calculator-wrapper .app-container { padding: 1rem; padding-bottom: 80px; }
295
+ .vram-calculator-wrapper .gpu-grid { grid-template-columns: 1fr; }
296
+ }
297
+
298
+ /* Lock Toggle Styles */
299
+ .vram-calculator-wrapper .lockable-input {
300
+ display: flex;
301
+ gap: 0.5rem;
302
+ align-items: stretch;
303
+ }
304
+ .vram-calculator-wrapper .lockable-input select,
305
+ .vram-calculator-wrapper .lockable-input input {
306
+ flex: 1;
307
+ min-width: 0;
308
+ }
309
+ .vram-calculator-wrapper .lock-btn {
310
+ width: 42px;
311
+ height: 42px;
312
+ display: flex;
313
+ align-items: center;
314
+ justify-content: center;
315
+ background: var(--bg-input);
316
+ border: 1px solid var(--border-color);
317
+ border-radius: 6px;
318
+ cursor: pointer;
319
+ transition: all 0.2s;
320
+ color: var(--text-dim);
321
+ flex-shrink: 0;
322
+ }
323
+ .vram-calculator-wrapper .lock-btn:hover {
324
+ border-color: var(--text-muted);
325
+ color: var(--text-muted);
326
+ }
327
+ .vram-calculator-wrapper .lock-btn.locked {
328
+ background: rgba(59, 130, 246, 0.15);
329
+ border-color: var(--accent);
330
+ color: var(--accent);
331
+ }
332
+ .vram-calculator-wrapper .lock-btn svg {
333
+ width: 16px;
334
+ height: 16px;
335
+ }
336
+
337
+ /* Optimal badge */
338
+ .vram-calculator-wrapper .optimal-badge {
339
+ display: inline-flex;
340
+ align-items: center;
341
+ gap: 0.25rem;
342
+ font-size: 0.7rem;
343
+ padding: 2px 6px;
344
+ border-radius: 4px;
345
+ background: rgba(16, 185, 129, 0.15);
346
+ color: var(--success);
347
+ font-weight: 500;
348
+ margin-left: 0.5rem;
349
+ }
350
+
351
+ /* Label with badge container */
352
+ .vram-calculator-wrapper .label-row {
353
+ display: flex;
354
+ align-items: center;
355
+ justify-content: space-between;
356
+ margin-bottom: 0.5rem;
357
+ }
358
+ .vram-calculator-wrapper .label-row label {
359
+ margin-bottom: 0;
360
+ }
361
+ </style>
362
+ </head>
363
+ <body>
364
+ <div class="vram-calculator-wrapper">
365
+
366
+ <div class="app-container">
367
+ <header>
368
+ <h1>LLM VRAM <span class="brand">Calculator</span></h1>
369
+ <span id="header-custom-text" style="color: var(--text-muted); font-size: 0.85rem;">HF version of the calculator found at https://novaml.ai/vram/</span>
370
+ <a href="#" onclick="window.location.reload()" style="color: var(--text-muted); text-decoration: none; font-size: 0.9rem;">Reset</a>
371
+ </header>
372
+
373
+ <!-- Main Loader Card -->
374
+ <div class="card">
375
+ <div class="card-body">
376
+ <div class="input-group">
377
+ <label>HuggingFace Model Path</label>
378
+ <div style="display: flex; gap: 1rem;">
379
+ <input type="text" id="model-path" placeholder="e.g. meta-llama/Llama-3.3-70B-Instruct">
380
+ <button id="load-btn" class="btn-primary">Load Model</button>
381
+ </div>
382
+ </div>
383
+
384
+ <div class="form-row">
385
+ <div class="input-group">
386
+ <label>HF Token (Optional)</label>
387
+ <input type="password" id="hf-token" placeholder="hf_...">
388
+ </div>
389
+
390
+ <div class="input-group">
391
+ <label>Your Hardware (Optional)</label>
392
+ <div class="dropdown-container">
393
+ <input type="text" id="selected-gpu-input" placeholder="Search GPU (e.g. 4090)" autocomplete="off">
394
+ <div class="dropdown-results" id="gpu-dropdown"></div>
395
+ </div>
396
+ </div>
397
+ </div>
398
+
399
+ <div id="error-msg" style="color: var(--danger); font-size: 0.85rem; margin-top: 0.5rem;"></div>
400
+ </div>
401
+ </div>
402
+
403
+ <div class="dashboard">
404
+ <!-- Left: Settings -->
405
+ <div class="settings-column">
406
+ <div class="card hidden" id="model-specs" style="margin-bottom: 2rem; border-left: 3px solid var(--accent);">
407
+ <div class="card-header">
408
+ <span>Model Specifications</span>
409
+ <span id="spec-arch-badge" style="font-size:0.75rem; background:rgba(255,255,255,0.1); padding:2px 8px; border-radius:4px;">-</span>
410
+ </div>
411
+ <div class="card-body">
412
+ <div class="specs-grid">
413
+ <div class="spec-item"><span class="spec-label">Parameters</span><span class="spec-val" id="spec-params">-</span></div>
414
+ <div class="spec-item"><span class="spec-label">Hidden Size</span><span class="spec-val" id="spec-hidden">-</span></div>
415
+ <div class="spec-item"><span class="spec-label">Layers</span><span class="spec-val" id="spec-layers">-</span></div>
416
+ <div class="spec-item"><span class="spec-label">Attn Heads</span><span class="spec-val" id="spec-heads">-</span></div>
417
+ </div>
418
+ </div>
419
+ </div>
420
+
421
+ <div class="card" id="config-panel" style="opacity: 0.5; pointer-events: none;">
422
+ <div class="card-header">
423
+ <span>Inference Configuration</span>
424
+ <span id="lock-hint" class="hidden" style="font-size: 0.75rem; font-weight: 400; color: var(--text-dim); margin-left: auto;">🔒 = recalculate with this parameter</span>
425
+ </div>
426
+ <div class="card-body">
427
+ <div class="form-grid">
428
+ <div class="input-group">
429
+ <div class="label-row">
430
+ <label>Quantization Method</label>
431
+ <span id="quant-optimal" class="optimal-badge hidden">✓ Optimal</span>
432
+ </div>
433
+ <div class="lockable-input">
434
+ <select id="quant">
435
+ <optgroup label="High Quality">
436
+ <option value="FP16">FP16 (16.0 bpw)</option>
437
+ <option value="Q8_0">Q8_0 (8.5 bpw)</option>
438
+ <option value="Q6_K">Q6_K (6.59 bpw)</option>
439
+ </optgroup>
440
+ <optgroup label="Balanced">
441
+ <option value="Q5_K_M">Q5_K_M (5.69 bpw)</option>
442
+ <option value="Q5_K_S">Q5_K_S (5.54 bpw)</option>
443
+ <option value="Q4_K_M" selected>Q4_K_M (4.85 bpw)</option>
444
+ <option value="Q4_K_S">Q4_K_S (4.58 bpw)</option>
445
+ <option value="Q4_0">Q4_0 (4.55 bpw)</option>
446
+ </optgroup>
447
+ <optgroup label="Aggressive">
448
+ <option value="Q3_K_M">Q3_K_M (3.91 bpw)</option>
449
+ <option value="Q3_K_S">Q3_K_S (3.5 bpw)</option>
450
+ <option value="Q2_K">Q2_K (3.35 bpw)</option>
451
+ <option value="IQ3_XXS">IQ3_XXS (3.06 bpw)</option>
452
+ <option value="IQ2_XXS">IQ2_XXS (2.06 bpw)</option>
453
+ </optgroup>
454
+ </select>
455
+ <button type="button" class="lock-btn" id="lock-quant" title="Lock: optimizer won't change this setting">
456
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
457
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
458
+ <path d="M7 11V7a5 5 0 0 1 5-5 5 5 0 0 1 5 5v1"></path>
459
+ </svg>
460
+ </button>
461
+ </div>
462
+ </div>
463
+ <div class="input-group">
464
+ <div class="label-row">
465
+ <label>KV Cache Precision</label>
466
+ <span id="cache-optimal" class="optimal-badge hidden">✓ Optimal</span>
467
+ </div>
468
+ <div class="lockable-input">
469
+ <select id="cache-type">
470
+ <option value="fp16" selected>FP16 (Standard)</option>
471
+ <option value="q8_0">Q8_0 (Compressed)</option>
472
+ <option value="q4_0">Q4_0 (Highly Compressed)</option>
473
+ </select>
474
+ <button type="button" class="lock-btn" id="lock-cache" title="Lock: optimizer won't change this setting">
475
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
476
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
477
+ <path d="M7 11V7a5 5 0 0 1 5-5 5 5 0 0 1 5 5v1"></path>
478
+ </svg>
479
+ </button>
480
+ </div>
481
+ </div>
482
+ </div>
483
+
484
+ <div class="form-grid">
485
+ <div class="input-group">
486
+ <div class="label-row">
487
+ <label>Context Length</label>
488
+ <span id="context-optimal" class="optimal-badge hidden">✓ Optimal</span>
489
+ </div>
490
+ <div class="lockable-input">
491
+ <input type="number" id="context" value="8192" step="1024" min="512">
492
+ <button type="button" class="lock-btn" id="lock-context" title="Lock: optimizer won't change this setting">
493
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
494
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
495
+ <path d="M7 11V7a5 5 0 0 1 5-5 5 5 0 0 1 5 5v1"></path>
496
+ </svg>
497
+ </button>
498
+ </div>
499
+ </div>
500
+ <div class="input-group">
501
+ <label>Batch Size</label>
502
+ <input type="number" id="batch" value="512" step="1" min="1">
503
+ </div>
504
+ </div>
505
+
506
+ <div class="input-group">
507
+ <label>Framework</label>
508
+ <select id="framework">
509
+ <option value="llama.cpp" selected>llama.cpp (Efficient)</option>
510
+ <option value="ExLlamaV2">ExLlamaV2 (Very Efficient)</option>
511
+ <option value="vLLM">vLLM (Production)</option>
512
+ <option value="transformers">HuggingFace Transformers (Heavy)</option>
513
+ </select>
514
+ </div>
515
+
516
+ <div class="form-grid">
517
+ <label class="checkbox-group">
518
+ <input type="checkbox" id="flash-attn" checked>
519
+ <span>Flash Attention</span>
520
+ </label>
521
+ <label class="checkbox-group hidden" id="mmproj-wrapper">
522
+ <input type="checkbox" id="mmproj" checked>
523
+ <span>Vision Adapter</span>
524
+ </label>
525
+ </div>
526
+ </div>
527
+ </div>
528
+ </div>
529
+
530
+ <!-- Right: Results -->
531
+ <div class="results-column">
532
+ <div class="card sticky-panel">
533
+ <div class="card-header">
534
+ <span>Estimation Results</span>
535
+ </div>
536
+ <div class="card-body">
537
+ <div style="text-align: center; margin-bottom: 1.5rem;">
538
+ <div style="font-size: 0.8rem; color: var(--text-muted); text-transform: uppercase;">Estimated Usage</div>
539
+ <div style="font-size: 2.5rem; font-weight: 700; color: var(--text-main); font-family: 'JetBrains Mono';">
540
+ <span id="total-vram">0.0</span> <span style="font-size: 1rem; color: var(--text-dim);">GB</span>
541
+ </div>
542
+ </div>
543
+
544
+ <div class="viz-container">
545
+ <div class="viz-bar-wrapper">
546
+ <div id="limit-line" class="limit-line"></div>
547
+ <div class="viz-segment seg-model" id="seg-model" style="width:0%"></div>
548
+ <div class="viz-segment seg-kv" id="seg-kv" style="width:0%"></div>
549
+ <div class="viz-segment seg-sys" id="seg-sys" style="width:0%"></div>
550
+ <div class="viz-segment seg-over" id="seg-over" style="width:0%; display:none;"></div>
551
+ </div>
552
+ <div class="legend">
553
+ <div class="legend-item"><div class="dot seg-model"></div> Model</div>
554
+ <div class="legend-item"><div class="dot seg-kv"></div> Context</div>
555
+ <div class="legend-item"><div class="dot seg-sys"></div> Overhead</div>
556
+ <div class="legend-item hidden" id="legend-over"><div class="dot seg-over"></div> Overload</div>
557
+ </div>
558
+ </div>
559
+
560
+ <div id="rec-container" class="hidden">
561
+ <div id="rec-box" class="recs-box">
562
+ <div class="recs-title" id="rec-title"></div>
563
+ <div id="rec-text"></div>
564
+ </div>
565
+ </div>
566
+ </div>
567
+ </div>
568
+ </div>
569
+ </div>
570
+
571
+ <div class="gpu-section">
572
+ <div class="gpu-section-title">
573
+ <span>Compatibility Matrix</span>
574
+ <select id="gpu-filter">
575
+ <option value="">All GPUs</option>
576
+ <option value="consumer">Consumer</option>
577
+ <option value="datacenter">Datacenter</option>
578
+ </select>
579
+ </div>
580
+ <div id="gpu-grid" class="gpu-grid"></div>
581
+ </div>
582
+ </div>
583
+
584
+ <div class="mobile-footer">
585
+ <div>
586
+ <div style="font-size: 0.75rem; color: var(--text-muted);">Total VRAM</div>
587
+ <div style="font-weight: 700; color: var(--text-main); font-size: 1.2rem;">
588
+ <span id="mobile-vram-val">0.0</span> GB
589
+ </div>
590
+ </div>
591
+ <button onclick="window.scrollTo({top: 0, behavior: 'smooth'})" class="btn-primary" style="padding: 0.5rem 1rem;">Edit</button>
592
+ </div>
593
+
594
+ <script>
595
+ // ============================================
596
+ // CONSTANTS
597
+ // ============================================
598
+ const BPW = {
599
+ 'FP16': 16, 'Q8_0': 8.5, 'Q6_K': 6.59, 'Q5_K_M': 5.69, 'Q5_K_S': 5.54,
600
+ 'Q4_K_M': 4.85, 'Q4_K_S': 4.58, 'Q4_0': 4.55, 'Q3_K_M': 3.91, 'Q3_K_S': 3.5,
601
+ 'Q2_K': 3.35, 'IQ3_XXS': 3.06, 'IQ2_XXS': 2.06
602
+ };
603
+
604
+ // Priority ordered (highest quality first)
605
+ const QUANTS_ORDERED = ['FP16', 'Q8_0', 'Q6_K', 'Q5_K_M', 'Q5_K_S', 'Q4_K_M', 'Q4_K_S', 'Q4_0', 'Q3_K_M', 'Q3_K_S', 'Q2_K', 'IQ3_XXS', 'IQ2_XXS'];
606
+ const CACHE_ORDERED = ['fp16', 'q8_0', 'q4_0'];
607
+ const CONTEXT_TIERS = [131072, 65536, 32768, 16384, 8192, 4096, 2048];
608
+
609
+ const GPUS = [
610
+ { name: 'NVIDIA RTX 4090', vram: 24, type: 'consumer' },
611
+ { name: 'NVIDIA RTX 4080 Super', vram: 16, type: 'consumer' },
612
+ { name: 'NVIDIA RTX 4080', vram: 16, type: 'consumer' },
613
+ { name: 'NVIDIA RTX 4070 Ti Super', vram: 16, type: 'consumer' },
614
+ { name: 'NVIDIA RTX 4070 Ti', vram: 12, type: 'consumer' },
615
+ { name: 'NVIDIA RTX 4060 Ti 16GB', vram: 16, type: 'consumer' },
616
+ { name: 'NVIDIA RTX 3090', vram: 24, type: 'consumer' },
617
+ { name: 'NVIDIA A100 80GB', vram: 80, type: 'datacenter' },
618
+ { name: 'NVIDIA A6000 Ada', vram: 48, type: 'datacenter' },
619
+ { name: 'NVIDIA H100 80GB', vram: 80, type: 'datacenter' },
620
+ { name: 'NVIDIA L40S', vram: 48, type: 'datacenter' },
621
+ { name: 'Mac M3 Max (128GB)', vram: 128, type: 'consumer' },
622
+ { name: 'Mac M3 Max (64GB)', vram: 64, type: 'consumer' },
623
+ { name: 'Mac M3 Pro (36GB)', vram: 36, type: 'consumer' }
624
+ ];
625
+
626
+ let modelConfig = null;
627
+ let selectedGPUVRAM = null;
628
+ let optimalConfig = null;
629
+
630
+ // Lock states
631
+ const locks = {
632
+ quant: false,
633
+ context: false,
634
+ cache: false
635
+ };
636
+
637
+ // ============================================
638
+ // DOM ELEMENTS
639
+ // ============================================
640
+ const els = {
641
+ loadBtn: document.getElementById('load-btn'),
642
+ modelPath: document.getElementById('model-path'),
643
+ errorMsg: document.getElementById('error-msg'),
644
+ configPanel: document.getElementById('config-panel'),
645
+ modelSpecs: document.getElementById('model-specs'),
646
+ totalVram: document.getElementById('total-vram'),
647
+ mobileVram: document.getElementById('mobile-vram-val'),
648
+ segModel: document.getElementById('seg-model'),
649
+ segKv: document.getElementById('seg-kv'),
650
+ segSys: document.getElementById('seg-sys'),
651
+ segOver: document.getElementById('seg-over'),
652
+ limitLine: document.getElementById('limit-line'),
653
+ legendOver: document.getElementById('legend-over'),
654
+ recContainer: document.getElementById('rec-container'),
655
+ recBox: document.getElementById('rec-box'),
656
+ recTitle: document.getElementById('rec-title'),
657
+ recText: document.getElementById('rec-text'),
658
+ gpuGrid: document.getElementById('gpu-grid'),
659
+ lockHint: document.getElementById('lock-hint'),
660
+ specs: {
661
+ params: document.getElementById('spec-params'),
662
+ hidden: document.getElementById('spec-hidden'),
663
+ layers: document.getElementById('spec-layers'),
664
+ heads: document.getElementById('spec-heads'),
665
+ badge: document.getElementById('spec-arch-badge')
666
+ },
667
+ locks: {
668
+ quant: document.getElementById('lock-quant'),
669
+ context: document.getElementById('lock-context'),
670
+ cache: document.getElementById('lock-cache')
671
+ },
672
+ optimal: {
673
+ quant: document.getElementById('quant-optimal'),
674
+ context: document.getElementById('context-optimal'),
675
+ cache: document.getElementById('cache-optimal')
676
+ }
677
+ };
678
+
679
+ // ============================================
680
+ // GPU DROPDOWN
681
+ // ============================================
682
+ const gpuInput = document.getElementById('selected-gpu-input');
683
+ const gpuDropdown = document.getElementById('gpu-dropdown');
684
+
685
+ function renderDropdown(filterText = '') {
686
+ gpuDropdown.innerHTML = '';
687
+ const lowerFilter = filterText.toLowerCase();
688
+ const filtered = GPUS.filter(g => g.name.toLowerCase().includes(lowerFilter));
689
+
690
+ if (filtered.length === 0) {
691
+ const div = document.createElement('div');
692
+ div.className = 'dropdown-item';
693
+ div.textContent = 'No GPUs found';
694
+ div.style.color = 'var(--text-dim)';
695
+ gpuDropdown.appendChild(div);
696
+ } else {
697
+ filtered.forEach(gpu => {
698
+ const div = document.createElement('div');
699
+ div.className = 'dropdown-item';
700
+ div.innerHTML = `<span>${gpu.name}</span><span class="dropdown-meta">${gpu.vram} GB</span>`;
701
+ div.onclick = () => {
702
+ gpuInput.value = gpu.name;
703
+ selectedGPUVRAM = gpu.vram;
704
+ gpuDropdown.classList.remove('active');
705
+ onHardwareChange();
706
+ };
707
+ gpuDropdown.appendChild(div);
708
+ });
709
+ }
710
+ }
711
+
712
+ gpuInput.addEventListener('focus', () => { renderDropdown(gpuInput.value); gpuDropdown.classList.add('active'); });
713
+ gpuInput.addEventListener('input', (e) => {
714
+ renderDropdown(e.target.value);
715
+ if(e.target.value === '') {
716
+ selectedGPUVRAM = null;
717
+ } else {
718
+ const match = GPUS.find(g => g.name.toLowerCase() === e.target.value.toLowerCase());
719
+ if(match) selectedGPUVRAM = match.vram;
720
+ }
721
+ onHardwareChange();
722
+ });
723
+ document.addEventListener('click', (e) => {
724
+ if (!gpuInput.contains(e.target) && !gpuDropdown.contains(e.target)) gpuDropdown.classList.remove('active');
725
+ });
726
+
727
+ // ============================================
728
+ // LOCK TOGGLES
729
+ // ============================================
730
+ Object.keys(els.locks).forEach(key => {
731
+ els.locks[key].addEventListener('click', () => {
732
+ locks[key] = !locks[key];
733
+ els.locks[key].classList.toggle('locked', locks[key]);
734
+
735
+ // Update icon to locked/unlocked
736
+ if (locks[key]) {
737
+ els.locks[key].innerHTML = `
738
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
739
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
740
+ <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
741
+ </svg>`;
742
+ els.locks[key].title = 'Locked: optimizer will keep this setting';
743
+ } else {
744
+ els.locks[key].innerHTML = `
745
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
746
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
747
+ <path d="M7 11V7a5 5 0 0 1 5-5 5 5 0 0 1 5 5v1"></path>
748
+ </svg>`;
749
+ els.locks[key].title = 'Lock: optimizer won\'t change this setting';
750
+ }
751
+
752
+ onHardwareChange();
753
+ });
754
+ });
755
+
756
+ // ============================================
757
+ // EVENT LISTENERS
758
+ // ============================================
759
+ els.loadBtn.addEventListener('click', loadModel);
760
+ els.modelPath.addEventListener('keydown', e => e.key === 'Enter' && loadModel());
761
+
762
+ const inputs = ['quant', 'cache-type', 'context', 'batch', 'framework', 'flash-attn', 'mmproj', 'gpu-filter'];
763
+ inputs.forEach(id => {
764
+ const el = document.getElementById(id);
765
+ if(el) {
766
+ el.addEventListener('change', calculate);
767
+ if(el.tagName === 'INPUT') el.addEventListener('input', calculate);
768
+ }
769
+ });
770
+
771
+ // ============================================
772
+ // MODEL LOADING
773
+ // ============================================
774
+ async function loadModel() {
775
+ const path = els.modelPath.value.trim();
776
+ if (!path) { showError('Enter model path'); return; }
777
+
778
+ showError('');
779
+ els.loadBtn.disabled = true;
780
+ els.loadBtn.innerHTML = '<div class="spinner"></div>';
781
+
782
+ try {
783
+ const token = document.getElementById('hf-token').value.trim();
784
+ const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
785
+ const res = await fetch(`https://huggingface.co/${path}/raw/main/config.json`, { headers });
786
+
787
+ if (!res.ok) throw new Error(res.status === 401 ? 'Model is gated. Provide HF Token.' : 'Model not found');
788
+
789
+ let config = await res.json();
790
+ if (config.text_config) config = config.text_config;
791
+
792
+ let params = 0;
793
+ let hidden = config.hidden_size || config.d_model || 4096;
794
+ let layers = config.num_hidden_layers || config.n_layer || 32;
795
+ let heads = config.num_attention_heads || config.n_head || 32;
796
+ let kvHeads = config.num_key_value_heads || heads;
797
+
798
+ try {
799
+ const idx = await fetch(`https://huggingface.co/${path}/resolve/main/model.safetensors.index.json`, { headers });
800
+ if (idx.ok) {
801
+ const data = await idx.json();
802
+ params = data.metadata?.total_size ? (data.metadata.total_size / 2) / 1e9 : 0;
803
+ }
804
+ } catch(e){}
805
+
806
+ if (!params) params = ((hidden * hidden * layers * 12) + (hidden * 32000)) / 1e9;
807
+
808
+ modelConfig = {
809
+ name: path,
810
+ params: params,
811
+ layers: layers,
812
+ hidden: hidden,
813
+ heads: heads,
814
+ kvHeads: kvHeads,
815
+ isVision: config.architectures?.[0]?.toLowerCase().includes('llava') || false,
816
+ arch: config.architectures?.[0] || 'Transformer'
817
+ };
818
+
819
+ els.configPanel.style.opacity = '1';
820
+ els.configPanel.style.pointerEvents = 'auto';
821
+ els.modelSpecs.classList.remove('hidden');
822
+
823
+ els.specs.params.textContent = `${modelConfig.params.toFixed(1)}B`;
824
+ els.specs.hidden.textContent = hidden;
825
+ els.specs.layers.textContent = layers;
826
+ els.specs.heads.textContent = `${heads} / ${kvHeads} KV`;
827
+ els.specs.badge.textContent = modelConfig.arch;
828
+ document.getElementById('mmproj-wrapper').classList.toggle('hidden', !modelConfig.isVision);
829
+
830
+ onHardwareChange();
831
+
832
+ } catch (err) {
833
+ showError(err.message);
834
+ } finally {
835
+ els.loadBtn.disabled = false;
836
+ els.loadBtn.textContent = 'Load Model';
837
+ }
838
+ }
839
+
840
+ // ============================================
841
+ // VRAM CALCULATION
842
+ // ============================================
843
+ function calculateVRAM(overrides = {}) {
844
+ const quant = overrides.quant || document.getElementById('quant').value;
845
+ const ctx = overrides.ctx !== undefined ? overrides.ctx : parseInt(document.getElementById('context').value);
846
+ const cache = overrides.cache || document.getElementById('cache-type').value;
847
+ const batch = overrides.batch || parseInt(document.getElementById('batch').value) || 1;
848
+ const flash = overrides.flash !== undefined ? overrides.flash : document.getElementById('flash-attn').checked;
849
+ const fw = overrides.fw || document.getElementById('framework').value;
850
+ const vision = document.getElementById('mmproj').checked && modelConfig.isVision;
851
+
852
+ const weights = (modelConfig.params * BPW[quant]) / 8;
853
+ const kvRatio = modelConfig.heads / modelConfig.kvHeads;
854
+ const elPerToken = 2 * modelConfig.layers * (modelConfig.hidden / kvRatio);
855
+ let cacheBytes = cache === 'fp16' ? 2 : (cache === 'q8_0' ? 1 : 0.5);
856
+ let kv = (elPerToken * ctx * cacheBytes) / (1024**3);
857
+ if (flash) kv *= 0.65;
858
+
859
+ const frameworkOverheadPct = { 'llama.cpp': 0.15, 'ExLlamaV2': 0.14, 'vLLM': 0.12, 'transformers': 0.22 };
860
+ const visionSize = vision ? 0.8 : 0;
861
+ const overhead = (weights * frameworkOverheadPct[fw]) + visionSize;
862
+ const total = weights + kv + overhead;
863
+
864
+ return { total, weights, kv, overhead };
865
+ }
866
+
867
+ // ============================================
868
+ // OPTIMAL CONFIG FINDER
869
+ // ============================================
870
+ function findOptimalConfig(vramLimit, constraints = {}) {
871
+ if (!modelConfig) return null;
872
+
873
+ const flash = document.getElementById('flash-attn').checked;
874
+
875
+ // If all three are locked, just check if it fits
876
+ if (constraints.quant && constraints.ctx && constraints.cache) {
877
+ const sim = calculateVRAM({
878
+ quant: constraints.quant,
879
+ ctx: constraints.ctx,
880
+ cache: constraints.cache,
881
+ flash
882
+ });
883
+ return sim.total <= vramLimit ? {
884
+ quant: constraints.quant,
885
+ ctx: constraints.ctx,
886
+ cache: constraints.cache,
887
+ vram: sim.total,
888
+ fits: true
889
+ } : null;
890
+ }
891
+
892
+ // Get iteration ranges based on locks
893
+ const quantRange = constraints.quant ? [constraints.quant] : QUANTS_ORDERED;
894
+ const cacheRange = constraints.cache ? [constraints.cache] : CACHE_ORDERED;
895
+
896
+ // For context, start from locked value or try tiers
897
+ let contextRange;
898
+ if (constraints.ctx) {
899
+ contextRange = [constraints.ctx];
900
+ } else {
901
+ // Use standard tiers, filtered to reasonable values
902
+ contextRange = CONTEXT_TIERS.filter(c => c <= 131072);
903
+ }
904
+
905
+ // Greedy search: prioritize quant > cache > context
906
+ // (Preserve FP16 cache over high context - cache quantization has more quality impact)
907
+ for (const quant of quantRange) {
908
+ for (const cache of cacheRange) {
909
+ for (const ctx of contextRange) {
910
+ const sim = calculateVRAM({ quant, ctx, cache, flash });
911
+ if (sim.total <= vramLimit) {
912
+ return {
913
+ quant,
914
+ ctx,
915
+ cache,
916
+ vram: sim.total,
917
+ fits: true
918
+ };
919
+ }
920
+ }
921
+ }
922
+ }
923
+
924
+ return null; // Nothing fits
925
+ }
926
+
927
+ // ============================================
928
+ // MAIN UPDATE FLOW
929
+ // ============================================
930
+ function onHardwareChange() {
931
+ if (!modelConfig) return;
932
+
933
+ // Show/hide lock hint based on GPU selection
934
+ els.lockHint.classList.toggle('hidden', !selectedGPUVRAM);
935
+
936
+ // Build constraints from locks
937
+ const constraints = {};
938
+ if (locks.quant) constraints.quant = document.getElementById('quant').value;
939
+ if (locks.context) constraints.ctx = parseInt(document.getElementById('context').value);
940
+ if (locks.cache) constraints.cache = document.getElementById('cache-type').value;
941
+
942
+ // Find optimal config
943
+ if (selectedGPUVRAM) {
944
+ optimalConfig = findOptimalConfig(selectedGPUVRAM, constraints);
945
+
946
+ // If no locks, apply optimal config automatically
947
+ if (!locks.quant && !locks.context && !locks.cache && optimalConfig) {
948
+ document.getElementById('quant').value = optimalConfig.quant;
949
+ document.getElementById('context').value = optimalConfig.ctx;
950
+ document.getElementById('cache-type').value = optimalConfig.cache;
951
+ }
952
+ } else {
953
+ optimalConfig = null;
954
+ }
955
+
956
+ calculate();
957
+ }
958
+
959
+ function calculate() {
960
+ if (!modelConfig) return;
961
+ const res = calculateVRAM();
962
+ updateUI(res.total, res.weights, res.kv, res.overhead);
963
+ renderGrid(res.total);
964
+ }
965
+
966
+ function updateUI(total, w, k, o) {
967
+ els.totalVram.textContent = total.toFixed(1);
968
+ els.mobileVram.textContent = total.toFixed(1);
969
+
970
+ const limit = selectedGPUVRAM || total;
971
+ const isOver = total > limit && selectedGPUVRAM !== null;
972
+ const totalWidth = isOver ? total : limit;
973
+
974
+ els.segModel.style.width = `${(w / totalWidth) * 100}%`;
975
+ els.segKv.style.width = `${(k / totalWidth) * 100}%`;
976
+ els.segSys.style.width = `${(o / totalWidth) * 100}%`;
977
+
978
+ // Update optimal badges
979
+ updateOptimalBadges();
980
+
981
+ if (isOver) {
982
+ const limitPos = (limit / total) * 100;
983
+ els.segOver.style.display = 'block';
984
+ els.segOver.style.position = 'absolute';
985
+ els.segOver.style.left = `${limitPos}%`;
986
+ els.segOver.style.right = '0';
987
+ els.segOver.style.width = 'auto';
988
+
989
+ els.limitLine.style.display = 'block';
990
+ els.limitLine.style.left = `${limitPos}%`;
991
+ els.legendOver.classList.remove('hidden');
992
+
993
+ showOverflowRecommendation(total, limit);
994
+ } else {
995
+ els.segOver.style.display = 'none';
996
+ els.limitLine.style.display = 'none';
997
+ els.legendOver.classList.add('hidden');
998
+
999
+ if (selectedGPUVRAM) {
1000
+ showFitRecommendation(total, limit);
1001
+ } else {
1002
+ els.recContainer.classList.add('hidden');
1003
+ }
1004
+ }
1005
+ }
1006
+
1007
+ function updateOptimalBadges() {
1008
+ if (!optimalConfig || !selectedGPUVRAM) {
1009
+ els.optimal.quant.classList.add('hidden');
1010
+ els.optimal.context.classList.add('hidden');
1011
+ els.optimal.cache.classList.add('hidden');
1012
+ return;
1013
+ }
1014
+
1015
+ const currentQuant = document.getElementById('quant').value;
1016
+ const currentCtx = parseInt(document.getElementById('context').value);
1017
+ const currentCache = document.getElementById('cache-type').value;
1018
+
1019
+ els.optimal.quant.classList.toggle('hidden', currentQuant !== optimalConfig.quant);
1020
+ els.optimal.context.classList.toggle('hidden', currentCtx !== optimalConfig.ctx);
1021
+ els.optimal.cache.classList.toggle('hidden', currentCache !== optimalConfig.cache);
1022
+ }
1023
+
1024
+ // ============================================
1025
+ // RECOMMENDATIONS
1026
+ // ============================================
1027
+ function showOverflowRecommendation(currentTotal, limit) {
1028
+ els.recContainer.classList.remove('hidden');
1029
+ els.recBox.className = 'recs-box danger';
1030
+ const diff = currentTotal - limit;
1031
+ els.recTitle.innerHTML = `<span style="color:var(--danger)">⚠️ Over VRAM Limit by ${diff.toFixed(1)} GB</span>`;
1032
+
1033
+ // Check if optimal exists
1034
+ if (optimalConfig) {
1035
+ const currentQuant = document.getElementById('quant').value;
1036
+ const currentCtx = parseInt(document.getElementById('context').value);
1037
+ const currentCache = document.getElementById('cache-type').value;
1038
+
1039
+ let html = '<div class="rec-solution">Suggested changes to fit:</div>';
1040
+ let hasChanges = false;
1041
+
1042
+ if (optimalConfig.quant !== currentQuant) {
1043
+ html += `<div class="rec-step"><span class="rec-tag tag-quant">QUANT</span> Switch to <b>${optimalConfig.quant}</b></div>`;
1044
+ hasChanges = true;
1045
+ }
1046
+ if (optimalConfig.ctx !== currentCtx) {
1047
+ html += `<div class="rec-step"><span class="rec-tag tag-ctx">CONTEXT</span> ${optimalConfig.ctx < currentCtx ? 'Reduce' : 'Increase'} to <b>${optimalConfig.ctx.toLocaleString()}</b></div>`;
1048
+ hasChanges = true;
1049
+ }
1050
+ if (optimalConfig.cache !== currentCache) {
1051
+ html += `<div class="rec-step"><span class="rec-tag tag-cache">CACHE</span> Switch to <b>${optimalConfig.cache.toUpperCase()}</b></div>`;
1052
+ hasChanges = true;
1053
+ }
1054
+
1055
+ if (hasChanges) {
1056
+ html += `<div style="margin-top:0.75rem; color:var(--text-muted); font-size:0.85rem;">This would use ${optimalConfig.vram.toFixed(1)} GB (${(limit - optimalConfig.vram).toFixed(1)} GB free)</div>`;
1057
+ }
1058
+
1059
+ els.recText.innerHTML = html;
1060
+ } else {
1061
+ // Nothing fits even with optimization
1062
+ let html = '<div style="color:var(--text-muted);">This model cannot fit on your GPU';
1063
+
1064
+ // Check which locks are causing the issue
1065
+ const lockedSettings = [];
1066
+ if (locks.quant) lockedSettings.push('quantization');
1067
+ if (locks.context) lockedSettings.push('context length');
1068
+ if (locks.cache) lockedSettings.push('cache precision');
1069
+
1070
+ if (lockedSettings.length > 0) {
1071
+ html += ` with locked ${lockedSettings.join(', ')}. Try unlocking some settings.`;
1072
+ } else {
1073
+ html += ' even with maximum optimization.';
1074
+ }
1075
+ html += '</div>';
1076
+ els.recText.innerHTML = html;
1077
+ }
1078
+ }
1079
+
1080
+ function showFitRecommendation(currentTotal, limit) {
1081
+ els.recContainer.classList.remove('hidden');
1082
+ const headroom = limit - currentTotal;
1083
+
1084
+ const currentQuant = document.getElementById('quant').value;
1085
+ const currentCtx = parseInt(document.getElementById('context').value);
1086
+ const currentCache = document.getElementById('cache-type').value;
1087
+
1088
+ // Check if at optimal
1089
+ const isOptimal = optimalConfig &&
1090
+ currentQuant === optimalConfig.quant &&
1091
+ currentCtx === optimalConfig.ctx &&
1092
+ currentCache === optimalConfig.cache;
1093
+
1094
+ if (isOptimal) {
1095
+ els.recBox.className = 'recs-box';
1096
+ els.recTitle.innerHTML = `<span style="color:var(--success)">✅ Optimal Configuration</span>`;
1097
+
1098
+ if (headroom < 1.0) {
1099
+ els.recText.innerHTML = '<div style="color:var(--text-muted);">Fit is tight. Consider closing other GPU applications.</div>';
1100
+ } else {
1101
+ els.recText.innerHTML = `<div style="color:var(--text-muted);">Best settings for your hardware. ${headroom.toFixed(1)} GB headroom.</div>`;
1102
+ }
1103
+ } else if (optimalConfig) {
1104
+ // Show delta from optimal
1105
+ els.recBox.className = 'recs-box warning';
1106
+ els.recTitle.innerHTML = `<span style="color:var(--warning)">💡 Better Configuration Available</span>`;
1107
+
1108
+ let html = '<div class="rec-solution">For optimal performance:</div>';
1109
+
1110
+ if (optimalConfig.quant !== currentQuant) {
1111
+ const qIdxCurrent = QUANTS_ORDERED.indexOf(currentQuant);
1112
+ const qIdxOptimal = QUANTS_ORDERED.indexOf(optimalConfig.quant);
1113
+ const direction = qIdxOptimal < qIdxCurrent ? 'Upgrade' : 'Downgrade';
1114
+ html += `<div class="rec-step"><span class="rec-tag tag-quant">QUANT</span> ${direction} to <b>${optimalConfig.quant}</b></div>`;
1115
+ }
1116
+ if (optimalConfig.ctx !== currentCtx) {
1117
+ const direction = optimalConfig.ctx > currentCtx ? 'Increase' : 'Reduce';
1118
+ html += `<div class="rec-step"><span class="rec-tag tag-ctx">CONTEXT</span> ${direction} to <b>${optimalConfig.ctx.toLocaleString()}</b></div>`;
1119
+ }
1120
+ if (optimalConfig.cache !== currentCache) {
1121
+ html += `<div class="rec-step"><span class="rec-tag tag-cache">CACHE</span> Switch to <b>${optimalConfig.cache.toUpperCase()}</b></div>`;
1122
+ }
1123
+
1124
+ els.recText.innerHTML = html;
1125
+ } else {
1126
+ // No GPU selected case (shouldn't reach here but safety)
1127
+ els.recBox.className = 'recs-box';
1128
+ els.recTitle.innerHTML = `<span style="color:var(--success)">✅ Configuration Valid</span>`;
1129
+ els.recText.innerHTML = `<div style="color:var(--text-muted);">${headroom.toFixed(1)} GB headroom remaining.</div>`;
1130
+ }
1131
+ }
1132
+
1133
+ // ============================================
1134
+ // GPU GRID
1135
+ // ============================================
1136
+ function renderGrid(req) {
1137
+ const filter = document.getElementById('gpu-filter').value;
1138
+ els.gpuGrid.innerHTML = '';
1139
+
1140
+ let list = GPUS;
1141
+ if (filter) list = list.filter(g => g.type === filter);
1142
+
1143
+ list.forEach(gpu => {
1144
+ const percent = (req / gpu.vram) * 100;
1145
+ const isSafe = req <= gpu.vram;
1146
+ const isTight = isSafe && req > (gpu.vram * 0.9);
1147
+ let color = isSafe ? (isTight ? 'var(--warning)' : 'var(--success)') : 'var(--danger)';
1148
+
1149
+ const html = `
1150
+ <div class="gpu-card" style="${!isSafe ? 'opacity:0.8;' : ''}">
1151
+ <div class="gpu-top">
1152
+ <span class="gpu-name">${gpu.name}</span>
1153
+ <span class="gpu-vram">${gpu.vram} GB</span>
1154
+ </div>
1155
+ <div class="gpu-bar-bg">
1156
+ <div class="gpu-bar-fill" style="width:${Math.min(percent, 100)}%; background:${color}"></div>
1157
+ </div>
1158
+ <div style="display:flex; justify-content:space-between; font-size:0.8rem;">
1159
+ <span style="color:${color}; font-weight:500">${isSafe ? (isTight ? 'Tight Fit' : 'Comfortable') : 'Insufficient'}</span>
1160
+ <span style="color:var(--text-dim)">${percent.toFixed(0)}% usage</span>
1161
+ </div>
1162
+ </div>
1163
+ `;
1164
+ els.gpuGrid.innerHTML += html;
1165
+ });
1166
+ }
1167
+
1168
+ function showError(msg) { els.errorMsg.textContent = msg; }
1169
+ </script>
1170
+
1171
+ </div>
1172
+ </body>
1173
  </html>
1174
+