maufadel commited on
Commit
cd94e1f
·
1 Parent(s): ef3a19e

Initial commit;

Browse files
.gitignore ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
README.md CHANGED
@@ -1,15 +1,347 @@
1
  ---
2
- title: Wattgpu
3
- emoji: 💻
4
  colorFrom: green
5
- colorTo: purple
6
  sdk: gradio
7
- sdk_version: 6.26.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
11
  license: apache-2.0
12
- short_description: Estimate energy, emissions, and latency for LLMs on GPUs
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: WattGPU
3
+ emoji:
4
  colorFrom: green
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 5.49.1
 
8
  app_file: app.py
9
  pinned: false
10
  license: apache-2.0
11
+ short_description: Predict LLM inference power and latency on any GPU
12
  ---
13
 
14
+ # WattGPU demo
15
+
16
+ Predict the **mean GPU power draw** and **inter-token latency** of serving an LLM,
17
+ without profiling it and without access to the hardware.
18
+
19
+ A research demo by [Leaner AI](https://leaner-ai.com), built on the models from
20
+ [*WattGPU: Predicting Inference Power and Latency on Unseen GPUs and
21
+ LLMs*](https://arxiv.org/abs/2607.02391) (1st Workshop on Sustainability and
22
+ Resource-Efficiency of AI @ IJCAI 2026).
23
+
24
+ Search any model on the Hugging Face Hub and pick any GPU:
25
+
26
+ - the LLM's architecture is read from its `config.json` on the Hub and adapted
27
+ to WattGPU's feature schema by `llm_info_from_config`, with the parameter
28
+ count taken from the Hub's safetensors index;
29
+ - the GPU's specifications come from a [dbgpu](https://github.com/painebenjamin/dbgpu)-derived
30
+ database, restricted to NVIDIA — every profiled run used NVIDIA hardware under
31
+ vLLM, and the hardware features the models lean on do not carry across to
32
+ other vendors' matrix engines. The dropdown lists only the measured GPUs;
33
+ typing searches the rest of the database.
34
+
35
+ ## Operational scenario
36
+
37
+ The dataset contains exactly two server request rates plus an offline setting,
38
+ so these are three **measured operating points**, not ranges. Converted at a
39
+ typical one message per minute per active user (0.017 requests/s each):
40
+
41
+ | Setting | Request rate | ≈ concurrent users | Paper scenario |
42
+ |---|---|---|---|
43
+ | Low load — a single concurrent user | 0.017 /s | ~1 | `server_low` |
44
+ | Medium load — about 20 concurrent users | 0.330 /s | ~20 | `server_high` |
45
+ | Offline / batch | work always queued | — | `offline` |
46
+
47
+ A fully saturated server keeps its GPU continuously busy just as a batch job
48
+ does, so the **offline / batch power figure is a reasonable estimate for a
49
+ saturated server** too, and its throughput is the ceiling the pair can sustain.
50
+ Per-user responsiveness does not carry over.
51
+
52
+ Where load tips into saturation is a property of the whole deployment — model
53
+ size against memory bandwidth, prompt and response lengths, how the server
54
+ batches concurrent requests, how bursty arrivals are — so the demo cannot locate
55
+ it. `scenario` enters the model as one of three categories, with no
56
+ interpolation to an arbitrary request rate.
57
+
58
+ ### What the latency target means, and where throughput comes from
59
+
60
+ Under **low and medium load** the dataset times individual requests, so the model
61
+ predicts a genuine **inter-token latency**: the gap one user sees between tokens.
62
+ System throughput cannot be recovered from it without knowing how many requests
63
+ run at once, so it is not reported.
64
+
65
+ **Offline** the dataset times only the batch as a whole, so the target is
66
+ wall-clock time per token across every request in flight. **Throughput is
67
+ reported as the exact inverse of that estimate** — no assumption about batching
68
+ needed. For Llama-3.1-8B on an H200 that is 29,245 tok/s aggregate, against a
69
+ per-user 5.08 ms between tokens under load: the two are ~150× apart because
70
+ roughly that many requests are being served at once. Because the offline figure
71
+ is measured across the whole batch rather than per request, it cannot be used to
72
+ derive the inter-token latency an individual user would see on a server, and the
73
+ UI says so.
74
+
75
+ Energy per token is `mean power ÷ throughput`, and so is reported offline only.
76
+
77
+ ## The certainty tier
78
+
79
+ Every prediction is labelled by how far it sits from the 42 LLMs and 8 GPUs that
80
+ were actually profiled:
81
+
82
+ | Tier | Meaning | Validation protocol |
83
+ |---|---|---|
84
+ | 🟢 High | Both LLM and GPU in the training data | 5-fold grouped CV |
85
+ | 🟡 Moderate | LLM not in training data, GPU is | leave-one-LLM-out |
86
+ | 🟠 Lower | GPU not in training data, LLM is | leave-one-GPU-out |
87
+ | 🟠 Lower | Neither in the training data, under server load | double holdout (both removed) |
88
+ | 🔴 Low | Neither in the training data, offline / batch | double holdout (both removed) |
89
+
90
+ The first three protocols are the paper's. The **double holdout** is an
91
+ extension added here: for each of the 262 measured pairs, the model is retrained
92
+ with that GPU *and* that LLM removed entirely, then asked to predict the pair.
93
+ It is what lets the lowest tier carry a measured error rather than an
94
+ unvalidated guess.
95
+
96
+ Each prediction shows the median absolute percentage error (MdAPE) measured
97
+ under exactly the protocol that matches its tier, split by offline and server
98
+ operation:
99
+
100
+ | | Power (offline / server) | ITL (offline / server) |
101
+ |---|---|---|
102
+ | 🟢 | 1.4% / 5.7% | 12.7% / 4.9% |
103
+ | 🟡 | 1.9% / 6.8% | 15.6% / 5.6% |
104
+ | 🟠 | 3.9% / 13.0% | 24.9% / 8.5% |
105
+ | 🔴 | 3.9% / 15.1% | 36.2% / 8.6% |
106
+
107
+ Dropping the LLM as well as the GPU costs surprisingly little: the hardware axis
108
+ dominates the difficulty, so once a GPU is held out, also holding out the model
109
+ adds a few points rather than breaking the estimate. Offline latency is the
110
+ exception, where it goes from 24.9% to 36.2%.
111
+
112
+ That is why the hardest case appears in two bands. Under server load the double
113
+ holdout tracks leave-one-GPU-out closely enough to share the orange band;
114
+ offline it does not, and drops to red. The band is presentational — the error
115
+ quoted is always the one measured for the actual coverage, so a both-unseen pair
116
+ shown in orange still reports the double-holdout figure (15.1%, not 13.0%).
117
+
118
+ Requests outside the models' scope are refused rather than tiered:
119
+ mixture-of-experts and quantised models, embedding and reranking models, and
120
+ pairs whose weights do not fit in GPU memory.
121
+
122
+ ## Accuracy reported in the paper
123
+
124
+ Tables 2 and 3 of the paper, reproduced by `scripts/train_models.py`. τ is
125
+ Kendall's tau over the induced ranking of GPUs and of LLMs — how reliably the
126
+ model puts hardware, and models, in the right order.
127
+
128
+ **Mean power draw (Table 2)**
129
+
130
+ | Model | Scenario | Strategy | MdAPE | Pearson r | GPU τ | LLM τ |
131
+ |---|---|---|---|---|---|---|
132
+ | XGBoost | Offline | CV | **1.6%** | 0.992 | 0.96 | 0.44 |
133
+ | XGBoost | Offline | LOGO | **3.4%** | 0.988 | 0.95 | 0.40 |
134
+ | XGBoost | Offline | LOLO | **2.0%** | 0.979 | 0.97 | 0.33 |
135
+ | XGBoost | Server | CV | **5.5%** | 0.981 | 0.86 | 0.68 |
136
+ | XGBoost | Server | LOGO | **13.5%** | 0.965 | 0.76 | 0.72 |
137
+ | XGBoost | Server | LOLO | **6.7%** | 0.955 | 0.85 | 0.54 |
138
+ | TDP baseline | Offline | — | 4.4% | 0.916 | 0.96 | — |
139
+ | TDP baseline | Server | — | 190.1% | 0.590 | 0.60 | — |
140
+ | Least-squares TDP | Offline | — | 7.2% | 0.916 | 0.96 | — |
141
+ | Least-squares TDP | Server | — | 26.0% | 0.779 | 0.60 | — |
142
+
143
+ Assuming a GPU draws its TDP is roughly adequate offline, where a saturated GPU
144
+ does sit near its power limit, and badly wrong under server load (190% error).
145
+
146
+ **Inter-token latency (Table 3)**
147
+
148
+ | Model | Scenario | Strategy | MdAPE | Pearson r | GPU τ | LLM τ |
149
+ |---|---|---|---|---|---|---|
150
+ | XGBoost | Offline | CV | **12.8%** | 0.813 | 0.86 | 0.81 |
151
+ | XGBoost | Offline | LOGO | **24.9%** | 0.727 | 0.72 | 0.83 |
152
+ | XGBoost | Offline | LOLO | **15.6%** | 0.808 | 0.97 | 0.66 |
153
+ | XGBoost | Server | CV | **4.9%** | 0.991 | 0.94 | 0.89 |
154
+ | XGBoost | Server | LOGO | **8.5%** | 0.972 | 0.78 | 0.88 |
155
+ | XGBoost | Server | LOLO | **5.6%** | 0.990 | 0.97 | 0.88 |
156
+ | Roofline baseline | Offline | — | 80.2% | 0.545 | 0.82 | 0.61 |
157
+ | Roofline baseline | Server | — | 29.6% | 0.991 | 0.88 | 0.91 |
158
+
159
+ Latency is harder offline, where throughput depends on how vLLM batches
160
+ requests. The roofline baseline tracks the ordering well but is off by 80% in
161
+ magnitude offline, because it ignores batching entirely.
162
+
163
+ CV rows correspond to the green tier, LOLO to yellow, LOGO to orange.
164
+
165
+ ## Running it
166
+
167
+ ```bash
168
+ uv venv --python 3.12
169
+ source .venv/bin/activate
170
+ uv pip install -r requirements.txt
171
+ python app.py
172
+ ```
173
+
174
+ The repository ships the trained models and the GPU database, so this is all
175
+ that is needed. To rebuild them from the paper's data in `../data`:
176
+
177
+ ```bash
178
+ python scripts/build_gpu_db.py # GPU specifications, from dbgpu
179
+ python scripts/train_models.py # fit the models and measure tier accuracy
180
+ ```
181
+
182
+ No Hugging Face token is needed. Licence-gated models work anyway, in two ways:
183
+ the 42 measured LLMs ship with their architectures, and a handful of popular
184
+ gated models are cached separately by reading a **public, unquantised mirror**
185
+ of the same weights:
186
+
187
+ ```bash
188
+ python scripts/cache_gated_models.py # writes data/gated_llms.json
189
+ ```
190
+
191
+ Only the integers WattGPU needs are stored — layer count, hidden size, head
192
+ counts, parameter count — all of which are published in the model cards and
193
+ papers, so nothing gated is redistributed. Each entry records the mirror it came
194
+ from, so any value can be traced. Being in this cache says only that the
195
+ architecture is known offline; those models still land in the certainty tier
196
+ their coverage earns, which for all of them is 🟡.
197
+
198
+ ### Tokens
199
+
200
+ A token is only needed for a gated model that is not bundled, or when the Hub
201
+ rate-limits the deployment's IP. Two ways to supply one:
202
+
203
+ - **Per visitor.** A masked field, *Use your own Hugging Face token*, sits with
204
+ the accordions below the estimate. The token is used for that request and
205
+ discarded: never stored, logged, rendered, or put into a shared link, and
206
+ Gradio analytics are disabled so nothing leaves the deployment. The gated and
207
+ rate-limit error messages point at this field.
208
+ - **Per deployment.** `HF_TOKEN` or `HUGGING_FACE_HUB_TOKEN` in the environment,
209
+ which every visitor then shares. A visitor's own token takes precedence.
210
+
211
+ Use read-only tokens in both cases.
212
+
213
+ ## Layout
214
+
215
+ | Path | What it is |
216
+ |---|---|
217
+ | `app.py` | Gradio UI and result rendering |
218
+ | `wattgpu_demo/hf_models.py` | Hub search, and the `config.json` → features adapter |
219
+ | `wattgpu_demo/gpu_db.py` | GPU specification look-ups |
220
+ | `wattgpu_demo/features.py` | feature engineering, shared by training and inference |
221
+ | `wattgpu_demo/predictor.py` | prediction and certainty tiers |
222
+ | `scripts/build_gpu_db.py` | builds `data/gpu_database.csv` from dbgpu |
223
+ | `scripts/train_models.py` | fits the models, writes `data/models/` |
224
+ | `scripts/cache_gated_models.py` | caches gated architectures from public mirrors |
225
+ | `wattgpu_demo/demand_log.py` | records refused queries as a coverage roadmap |
226
+ | `assets/leaner-ai-logo.svg` | Leaner AI wordmark, inlined in the header |
227
+ | `tests/test_demo.py` | tests, including all four tiers |
228
+
229
+ ## Tests
230
+
231
+ ```bash
232
+ pytest # everything
233
+ pytest -m "not network" # skip the tests that reach the Hub
234
+ ```
235
+
236
+ ## What is out of scope
237
+
238
+ Every measurement behind WattGPU used a **dense** model with **FP16** weights,
239
+ on a single GPU under vLLM with 512-token prompts. Two model families are
240
+ therefore refused outright rather than estimated:
241
+
242
+ - **Mixture-of-experts**, detected from `num_local_experts`, `num_experts`,
243
+ `n_routed_experts`, `moe_intermediate_size` and friends, or from a `*MoE*`
244
+ architecture name.
245
+ - **Quantised weights**, detected from `quantization_config` (GPTQ, AWQ,
246
+ bitsandbytes, compressed-tensors, FP8…), from a non-16-bit `torch_dtype` /
247
+ `dtype`, or, as a fallback, from markers such as `gptq`, `awq`, `-4bit` or
248
+ `fp8` in the repository name.
249
+
250
+ Detection is reliable but not infallible — a repository can quantise without
251
+ declaring it — so every estimate for a model that was not measured carries a
252
+ note saying the architecture was read from `config.json` and what that implies.
253
+
254
+ A pair whose **weights do not fit in GPU memory** is refused too, rather than
255
+ estimated. The budget follows the paper's own feature engineering: vLLM claims
256
+ 90% of the card by default, and the FP16 weights come out of that, so a model
257
+ fits when `0.9 × memory_size_gb − 2 × total_b_params > 0`. No pair in the
258
+ dataset is refused by this rule — all 262 measured combinations pass — which is
259
+ what pins the threshold; anything stricter would start contradicting runs that
260
+ actually completed. Pairs that fit with under 2 GB to spare still get an
261
+ estimate, plus a warning that the KV cache has little room and the figure will
262
+ be optimistic.
263
+
264
+ Quantised, multi-GPU and long-context deployments are all outside what the
265
+ models were fitted on.
266
+
267
+ ## Energy and emissions
268
+
269
+ Every energy figure is at the **facility level**. The models predict the GPU
270
+ board's draw; that is grossed up to the whole host (the GPU taken as 70% of it,
271
+ the rest being CPU, memory, storage, NICs and fans) and multiplied by a
272
+ data-centre PUE of 1.2:
273
+
274
+ ```
275
+ facility watts = GPU watts ÷ 0.7 × 1.2
276
+ ```
277
+
278
+ Both factors are typical values rather than measurements. The same scaling is
279
+ applied in every scenario, so the offline and server numbers describe one system
280
+ boundary and can be compared.
281
+
282
+ **Offline / batch reports per million tokens.** The GPU is saturated, so every
283
+ joule is attributable to a token and energy per token is a stable property of
284
+ the model and the hardware. Llama-3.1-8B on an H200 comes to 9.1 Wh per million
285
+ tokens.
286
+
287
+ **Server load reports per hour.** There the GPU spends much of its time waiting
288
+ for the next request, so energy per token would mostly reflect how busy the
289
+ server happens to be rather than anything about the model or the card. What an
290
+ operator can act on is the draw of keeping the deployment up: 109 W on the card
291
+ is about 187 W at the wall, or 187 Wh per hour. This split follows the paper's
292
+ own reasoning.
293
+
294
+ Emissions are the energy figure times one visible multiplier, the grid's carbon
295
+ intensity, defaulting to **240 gCO₂e/kWh** (roughly the EU average). Real
296
+ intensity varies by an order of magnitude between regions and by the hour within
297
+ one, so the field links to
298
+ [Electricity Maps](https://app.electricitymaps.com/map/live/fifteen_minutes).
299
+ Clearing the box restores the default and writes it back, so the field never
300
+ disagrees with the result.
301
+
302
+ All of this lives in a collapsed **Assumptions** panel below the estimate rather
303
+ than on every result.
304
+
305
+ Cost is deliberately not included: prices depend on provider and region and go
306
+ stale, whereas the energy figure needs no maintenance.
307
+
308
+ ## Sharing a result
309
+
310
+ Every estimate carries a prominent **Share** button next to the certainty tier.
311
+ It copies an absolute link to that exact estimate — `?model=…&gpu=…&scenario=…`,
312
+ plus `carbon=` when the intensity is not the default — and confirms with a brief
313
+ "Link copied". On devices that have a native share sheet it opens that instead.
314
+
315
+ The button stores a *relative* href, resolved against the current page in the
316
+ browser, so it works at any deployed URL without the server knowing what that
317
+ URL is. Opening the link fills the form in and runs the estimate; values are
318
+ validated against the catalogues on load, so a hand-edited link cannot put the
319
+ form into a state the app would not otherwise reach.
320
+
321
+ ## Demand log
322
+
323
+ Refusals that represent real unmet coverage — mixture-of-experts, quantised,
324
+ non-generative, and pairs that do not fit — are appended to
325
+ `data/demand_log.jsonl` as `{at, model, gpu, scenario, reason}`. Typos and
326
+ unreachable repositories are not logged, since they say nothing about demand.
327
+ Nothing identifying the requester is stored, and the footer discloses it.
328
+
329
+ ```bash
330
+ python -c "from wattgpu_demo.demand_log import summarise; print(*summarise(), sep='\n')"
331
+ ```
332
+
333
+ Set `WATTGPU_DEMAND_LOG` to choose the path, or `WATTGPU_DEMAND_LOG_DISABLED=1`
334
+ to turn it off. On Hugging Face Spaces the container filesystem is ephemeral, so
335
+ point `WATTGPU_DEMAND_LOG` at persistent storage or a mounted dataset repo if
336
+ the log needs to survive restarts.
337
+
338
+ ## Links
339
+
340
+ - [Leaner AI](https://leaner-ai.com)
341
+ - [The paper](https://arxiv.org/abs/2607.02391)
342
+ - [Watt Counts](https://arxiv.org/abs/2604.09048) — the measurement dataset
343
+ - [Source](https://github.com/maufadel/wattgpu)
344
+
345
+ ## Licence
346
+
347
+ Apache 2.0, as the parent repository.
app.py ADDED
@@ -0,0 +1,1105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """WattGPU — predict LLM inference power and latency on a GPU you don't have.
2
+
3
+ A Gradio front end over the models from "WattGPU: Predicting Inference Power
4
+ and Latency on Unseen GPUs and LLMs" (SuRE @ IJCAI 2026). Pick any Hugging
5
+ Face model and any GPU; the app reads the model's `config.json` from the Hub
6
+ and the GPU's specifications from a dbgpu-derived database, and returns mean
7
+ power draw and inter-token latency together with a certainty tier.
8
+
9
+ Run locally: python app.py
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import html
15
+ import os
16
+ import re
17
+ import traceback
18
+ import urllib.parse
19
+
20
+ import gradio as gr
21
+
22
+ from wattgpu_demo import demand_log
23
+ from wattgpu_demo.emissions import (
24
+ DEFAULT_CARBON_INTENSITY_G_PER_KWH,
25
+ DEFAULT_CARBON_INTENSITY_LABEL,
26
+ DEFAULT_PUE,
27
+ ELECTRICITY_MAPS_URL,
28
+ GPU_SHARE_OF_HOST_POWER,
29
+ MAX_CARBON_INTENSITY,
30
+ MIN_CARBON_INTENSITY,
31
+ clean_carbon_intensity,
32
+ format_co2,
33
+ format_energy,
34
+ )
35
+ from wattgpu_demo.errors import (
36
+ GPUNotFoundError,
37
+ InsufficientMemoryError,
38
+ ModelConfigError,
39
+ ModelNotFoundError,
40
+ UnsupportedModelError,
41
+ WattGPUError,
42
+ )
43
+ from wattgpu_demo.features import SCENARIO_LABELS, SCENARIOS
44
+ from wattgpu_demo.hf_models import has_hf_token, search_models
45
+ from wattgpu_demo.predictor import Prediction, WattGPUPredictor
46
+
47
+ PAPER_TITLE = "WattGPU: Predicting Inference Power and Latency on Unseen GPUs and LLMs"
48
+ PAPER_VENUE = "1st Workshop on Sustainability and Resource-Efficiency of AI @ IJCAI 2026"
49
+ PAPER_URL = "https://arxiv.org/abs/2607.02391"
50
+ LEANER_AI_URL = "https://leaner-ai.com"
51
+ CODE_URL = "https://github.com/maufadel/wattgpu"
52
+ # The measurement dataset the models were fitted on.
53
+ WATT_COUNTS_URL = "https://arxiv.org/abs/2604.09048"
54
+ CONTACT_URL = "https://leaner-ai.com/#report-form"
55
+
56
+ ASSETS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
57
+
58
+ # Four confidence tiers, plus a visually distinct marker for refusals — being
59
+ # out of scope is not a low confidence level, it is a different kind of answer.
60
+ TIER_GLYPHS = {"green": "●", "yellow": "●", "orange": "●", "red": "●", "error": "■"}
61
+
62
+ # Injected into the page head. The share button is rendered inside a Gradio HTML
63
+ # component, which is replaced on every estimate, so the handler has to live
64
+ # outside it rather than in a <script> beside the button.
65
+ HEAD = """
66
+ <script>
67
+ window.wgShare = async function (button) {
68
+ const url = new URL(button.getAttribute("data-href"), window.location.href).href;
69
+ const label = button.querySelector(".wg-share-label");
70
+ const original = label.textContent;
71
+
72
+ const flash = (text) => {
73
+ label.textContent = text;
74
+ button.classList.add("wg-shared");
75
+ setTimeout(() => {
76
+ label.textContent = original;
77
+ button.classList.remove("wg-shared");
78
+ }, 2000);
79
+ };
80
+
81
+ // Native share sheet where there is one (phones, some desktops).
82
+ if (navigator.share) {
83
+ try {
84
+ await navigator.share({ title: "WattGPU estimate", url: url });
85
+ return;
86
+ } catch (err) {
87
+ if (err && err.name === "AbortError") return; // user dismissed it
88
+ }
89
+ }
90
+
91
+ try {
92
+ await navigator.clipboard.writeText(url);
93
+ flash("Link copied");
94
+ return;
95
+ } catch (err) {
96
+ // clipboard needs a secure context; fall back to a scratch textarea
97
+ }
98
+
99
+ const scratch = document.createElement("textarea");
100
+ scratch.value = url;
101
+ scratch.setAttribute("readonly", "");
102
+ scratch.style.position = "fixed";
103
+ scratch.style.opacity = "0";
104
+ document.body.appendChild(scratch);
105
+ scratch.select();
106
+ let copied = false;
107
+ try {
108
+ copied = document.execCommand("copy");
109
+ } catch (err) {
110
+ copied = false;
111
+ }
112
+ scratch.remove();
113
+ flash(copied ? "Link copied" : "Press Ctrl+C to copy");
114
+ };
115
+ </script>
116
+ """
117
+
118
+ CSS = """
119
+ /* --- Leaner AI palette -------------------------------------------------
120
+ Brand sky #38bdf8 (hover #0284c7), ink #0f172a, muted #64748b, and the
121
+ brand gradient sky -> green -> violet used on leaner-ai.com. Tier accents
122
+ stay semantic but are drawn from the same family, with a dark variant each
123
+ because Gradio toggles dark mode with a `.dark` class. */
124
+ .gradio-container {
125
+ --wg-brand: #0284c7;
126
+ --wg-brand-soft: #38bdf8;
127
+ --wg-ink: #0f172a;
128
+ --wg-muted: #64748b;
129
+ --wg-gradient: linear-gradient(90deg, #38bdf8 0%, #22c55e 50%, #a78bfa 100%);
130
+ }
131
+ .dark .gradio-container, .gradio-container.dark {
132
+ --wg-brand: #38bdf8;
133
+ --wg-brand-soft: #7dd3fc;
134
+ --wg-ink: #f8fafc;
135
+ --wg-muted: #94a3b8;
136
+ }
137
+
138
+ .wg-green { --wg-accent: #15803d; }
139
+ .wg-yellow { --wg-accent: #a16207; }
140
+ .wg-orange { --wg-accent: #c2410c; }
141
+ .wg-red { --wg-accent: #be123c; }
142
+ .wg-error { --wg-accent: #475569; }
143
+ .dark .wg-green { --wg-accent: #22c55e; }
144
+ .dark .wg-yellow { --wg-accent: #eab308; }
145
+ .dark .wg-orange { --wg-accent: #fb923c; }
146
+ .dark .wg-red { --wg-accent: #fb7185; }
147
+ .dark .wg-error { --wg-accent: #94a3b8; }
148
+
149
+ /* --- Header ----------------------------------------------------------- */
150
+ .wg-header { display: flex; align-items: center; justify-content: space-between;
151
+ flex-wrap: wrap; gap: 14px; padding: 4px 0 10px; }
152
+ .wg-header .wg-brandmark { display: flex; align-items: center; gap: 12px; }
153
+ .wg-header .wg-brandmark svg { height: 26px; width: auto; display: block; }
154
+ .wg-header .wg-eyebrow { font-size: 12px; letter-spacing: .08em; text-transform: uppercase;
155
+ color: var(--wg-muted); border-left: 1px solid var(--border-color-primary);
156
+ padding-left: 12px; }
157
+ .wg-links { display: flex; gap: 8px; flex-wrap: wrap; }
158
+ .wg-links a { display: inline-flex; align-items: center; gap: 6px; text-decoration: none;
159
+ font-size: 13px; font-weight: 600; padding: 6px 12px; border-radius: 999px;
160
+ border: 1px solid var(--border-color-primary); color: var(--wg-brand);
161
+ background: var(--background-fill-secondary); transition: .15s; }
162
+ .wg-links a:hover { border-color: var(--wg-brand); color: var(--wg-brand);
163
+ background: color-mix(in srgb, var(--wg-brand) 12%, transparent); }
164
+ .wg-title-bar { height: 3px; border-radius: 3px; background: var(--wg-gradient);
165
+ margin: 0 0 18px; }
166
+
167
+ /* --- Result --------------------------------------------------------- */
168
+ .wg-chip { display: inline-flex; align-items: center; gap: 7px; margin-bottom: 12px;
169
+ padding: 5px 13px; border-radius: 999px; font-size: 13px; font-weight: 700;
170
+ color: var(--wg-accent); border: 1px solid var(--wg-accent);
171
+ background: color-mix(in srgb, var(--wg-accent) 12%, transparent); }
172
+
173
+ .wg-details { margin-top: 14px; }
174
+ .wg-details > summary { cursor: pointer; font-size: 13px; font-weight: 600;
175
+ color: var(--wg-muted); padding: 6px 0; list-style-position: outside; }
176
+ .wg-details > summary:hover { color: var(--wg-brand); }
177
+ .wg-details[open] > summary { margin-bottom: 10px; }
178
+
179
+ .wg-banner { border-radius: 10px; padding: 14px 16px; margin-bottom: 14px;
180
+ border-left: 6px solid var(--wg-accent); font-size: 15px;
181
+ background: color-mix(in srgb, var(--wg-accent) 10%, var(--background-fill-secondary));
182
+ color: var(--body-text-color); }
183
+ .wg-banner .wg-title { font-weight: 700; margin-bottom: 4px; color: var(--wg-accent); }
184
+ .wg-banner .wg-body { font-size: 14px; line-height: 1.55; color: var(--body-text-color); }
185
+ .wg-cards { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
186
+ .wg-card { flex: 1 1 190px; border: 1px solid var(--border-color-primary);
187
+ border-top: 3px solid var(--wg-brand-soft); border-radius: 10px; padding: 13px 16px;
188
+ background: var(--background-fill-secondary); color: var(--body-text-color); }
189
+ .wg-card .wg-label { font-size: 12px; text-transform: uppercase; letter-spacing: .05em;
190
+ color: var(--wg-muted); margin-bottom: 6px; }
191
+ .wg-card .wg-value { font-size: 26px; font-weight: 700; line-height: 1.15; color: var(--wg-ink); }
192
+ .wg-card .wg-sub { font-size: 12px; color: var(--wg-muted);
193
+ margin-top: 6px; line-height: 1.4; }
194
+ .wg-note { border-radius: 8px; padding: 10px 14px; margin-bottom: 8px; font-size: 13.5px;
195
+ background: var(--background-fill-secondary); color: var(--body-text-color);
196
+ border-left: 4px solid var(--wg-brand-soft); line-height: 1.55; }
197
+ .wg-specs { display: flex; gap: 26px; flex-wrap: wrap; font-size: 13.5px;
198
+ margin-top: 4px; color: var(--body-text-color); }
199
+ .wg-specs table { border-collapse: collapse; }
200
+ .wg-specs td { padding: 3px 14px 3px 0; vertical-align: top; }
201
+ .wg-specs td:first-child { color: var(--wg-muted); white-space: nowrap; }
202
+ .wg-specs h4 { margin: 0 0 6px; font-size: 12px; text-transform: uppercase;
203
+ letter-spacing: .05em; color: var(--wg-muted); }
204
+ .wg-banner code, .wg-note code { background: var(--background-fill-primary);
205
+ padding: 1px 5px; border-radius: 4px; font-size: .92em; }
206
+
207
+ .wg-result-head { display: flex; align-items: center; justify-content: space-between;
208
+ gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
209
+ .wg-result-head .wg-chip { margin-bottom: 0; }
210
+ /* Scoped through .gradio-container so Gradio's own button reset, which sets
211
+ background, colour, padding and weight on every button, does not win. */
212
+ /* Matches the primary button: white on sky-500 in both themes. */
213
+ .gradio-container button.wg-share {
214
+ display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
215
+ font-family: inherit; font-size: 13px; font-weight: 600; line-height: 1.2;
216
+ padding: 8px 16px; border-radius: 999px; border: 1px solid #0ea5e9;
217
+ color: #fff; background: #0ea5e9; box-shadow: none; transition: .15s;
218
+ }
219
+ .gradio-container button.wg-share:hover { filter: brightness(1.08); }
220
+ .gradio-container button.wg-share:active { transform: translateY(1px); }
221
+ .gradio-container button.wg-share.wg-shared,
222
+ .dark .gradio-container button.wg-share.wg-shared {
223
+ background: #15803d; border-color: #15803d; color: #fff;
224
+ }
225
+
226
+ .wg-note-quiet { border-left-color: var(--border-color-primary);
227
+ color: var(--wg-muted); font-size: 13px; }
228
+
229
+ .wg-scenario-head { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap;
230
+ margin: 6px 0 -4px; }
231
+ .wg-scenario-title { font-size: 14px; font-weight: 600; color: var(--body-text-color); }
232
+
233
+ .wg-loadhelp { margin: 0; }
234
+ .wg-loadhelp > summary { cursor: pointer; display: flex; align-items: center; gap: 8px;
235
+ font-size: 13px; font-weight: 600; color: var(--wg-muted);
236
+ padding: 4px 0; list-style: none; }
237
+ .wg-loadhelp > summary::-webkit-details-marker { display: none; }
238
+ .wg-loadhelp > summary:hover { color: var(--wg-brand); }
239
+ .wg-q { display: inline-flex; align-items: center; justify-content: center;
240
+ width: 17px; height: 17px; border-radius: 50%; font-size: 11px; font-weight: 700;
241
+ border: 1px solid currentColor; flex: none; }
242
+ .wg-loadhelp-body { font-size: 13px; color: var(--body-text-color); line-height: 1.65;
243
+ border-left: 3px solid var(--wg-brand-soft); padding: 2px 0 2px 14px;
244
+ margin: 6px 0 4px; }
245
+ .wg-loadhelp-body p { margin: 0 0 9px; }
246
+ .wg-loadhelp-body ul { margin: 0 0 9px; padding-left: 20px; }
247
+ .wg-loadhelp-body li { margin-bottom: 3px; }
248
+ .wg-loadhelp-body pre { margin: 0 0 9px; padding: 8px 11px; border-radius: 6px;
249
+ background: var(--background-fill-secondary); font-size: 12.5px;
250
+ white-space: pre-wrap; }
251
+ .wg-loadhelp-body code { background: var(--background-fill-secondary);
252
+ padding: 1px 5px; border-radius: 4px; font-size: .92em; }
253
+
254
+ /* --- Footer ----------------------------------------------------------- */
255
+ .wg-footer { margin-top: 22px; padding-top: 14px; font-size: 13px; color: var(--wg-muted);
256
+ border-top: 1px solid var(--border-color-primary); line-height: 1.6; }
257
+ .wg-footer a { color: var(--wg-brand); text-decoration: none; font-weight: 600; }
258
+ .wg-footer a:hover { text-decoration: underline; }
259
+ .wg-footer p { margin: 0 0 9px; }
260
+ .wg-footer .wg-legal { font-size: 11.5px; line-height: 1.55; opacity: .85; }
261
+ """
262
+
263
+ # ---------------------------------------------------------------------------
264
+ # Rendering
265
+ # ---------------------------------------------------------------------------
266
+
267
+ def _banner(tier: str, title: str, body: str) -> str:
268
+ """Tier banner. Colours come from the CSS classes so both themes work."""
269
+ return (
270
+ f'<div class="wg-banner wg-{tier}">'
271
+ f'<div class="wg-title">{TIER_GLYPHS[tier]} {html.escape(title)}</div>'
272
+ f'<div class="wg-body">{body}</div></div>'
273
+ )
274
+
275
+
276
+ def _chip(tier: str, label: str) -> str:
277
+ """Compact certainty marker shown above the numbers."""
278
+ return (f'<div class="wg-chip wg-{tier}">{TIER_GLYPHS[tier]} '
279
+ f'{html.escape(label)}</div>')
280
+
281
+
282
+ SHARE_ICON = (
283
+ '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" '
284
+ 'stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'
285
+ '<circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle>'
286
+ '<circle cx="18" cy="19" r="3"></circle>'
287
+ '<line x1="8.6" y1="10.5" x2="15.4" y2="6.5"></line>'
288
+ '<line x1="8.6" y1="13.5" x2="15.4" y2="17.5"></line></svg>'
289
+ )
290
+
291
+
292
+ def _result_head(certainty, query: str) -> str:
293
+ """The tier chip and the share button, on one line above the numbers."""
294
+ return (
295
+ '<div class="wg-result-head">'
296
+ + _chip(certainty.tier, certainty.label)
297
+ + f'<button type="button" class="wg-share" data-href="?{html.escape(query)}" '
298
+ 'onclick="wgShare(this)" title="Copy a link to this estimate">'
299
+ f'{SHARE_ICON}<span class="wg-share-label">Share</span></button>'
300
+ + "</div>"
301
+ )
302
+
303
+
304
+ def _card(label: str, value: str, sub: str = "") -> str:
305
+ sub_html = f'<div class="wg-sub">{sub}</div>' if sub else ""
306
+ return (f'<div class="wg-card"><div class="wg-label">{html.escape(label)}</div>'
307
+ f'<div class="wg-value">{value}</div>{sub_html}</div>')
308
+
309
+
310
+ def _spec_table(heading: str, rows: list[tuple[str, str]]) -> str:
311
+ body = "".join(
312
+ f"<tr><td>{html.escape(k)}</td><td>{html.escape(str(v))}</td></tr>" for k, v in rows
313
+ )
314
+ return f"<div><h4>{html.escape(heading)}</h4><table>{body}</table></div>"
315
+
316
+
317
+ # Only links to our own site are turned into anchors, so a model id or GPU name
318
+ # echoed back from user input can never become a link.
319
+ _ALLOWED_LINK_PREFIXES = ("https://leaner-ai.com", "https://arxiv.org/abs/",
320
+ "mailto:mfa@leaner-ai.com")
321
+
322
+ _LINK_RE = re.compile(r"\[([^\]]+)\]\(((?:https|mailto):[^\s)]+)\)")
323
+
324
+
325
+ def _markdown_bold(text: str) -> str:
326
+ """Escape user-facing text, then honour `**bold**`, `code` and links."""
327
+ out = html.escape(text)
328
+
329
+ def link(match: re.Match[str]) -> str:
330
+ label, url = match.group(1), match.group(2)
331
+ if not url.startswith(_ALLOWED_LINK_PREFIXES):
332
+ return label
333
+ return f'<a href="{url}" target="_blank" rel="noopener">{label}</a>'
334
+
335
+ out = _LINK_RE.sub(link, out)
336
+ while out.count("**") >= 2:
337
+ out = out.replace("**", "<strong>", 1).replace("**", "</strong>", 1)
338
+ while out.count("`") >= 2:
339
+ out = out.replace("`", "<code>", 1).replace("`", "</code>", 1)
340
+ return out.replace("\n\n", "<br><br>")
341
+
342
+
343
+ def _format_seconds(seconds: float) -> str:
344
+ if seconds >= 1:
345
+ return f"{seconds:.2f} s"
346
+ if seconds >= 1e-3:
347
+ return f"{seconds * 1e3:.2f} ms"
348
+ return f"{seconds * 1e6:.0f} µs"
349
+
350
+
351
+ def render_error(error: WattGPUError) -> str:
352
+ body = _markdown_bold(error.hint) if error.hint else ""
353
+ return _banner("error", "Cannot make this estimate",
354
+ f"{_markdown_bold(error.message)}<br><br>{body}" if body
355
+ else _markdown_bold(error.message))
356
+
357
+
358
+ def render_prediction(prediction: Prediction) -> str:
359
+ """Render one prediction: a tier chip, the numbers, caveats, details."""
360
+ certainty = prediction.certainty
361
+ offline = prediction.scenario == "offline"
362
+
363
+ # The tier is a compact chip. What each tier means lives in the accordion
364
+ # below the form, so it is not repeated on every result.
365
+ parts: list[str] = [] # the head is prepended once the permalink is known
366
+
367
+ power_sub = f"{prediction.power_fraction_of_tdp * 100:.0f}% of {prediction.gpu.thermal_design_power_w:.0f} W TDP"
368
+ if prediction.power_mdape is not None:
369
+ power_sub += f" · ±{prediction.power_mdape:.1f}% typical error"
370
+ cards = [_card("GPU mean power draw", f"{prediction.power_w:,.0f} W", power_sub)]
371
+
372
+ if prediction.itl_s is not None:
373
+ error_suffix = (f" · ±{prediction.itl_mdape:.1f}% typical error"
374
+ if prediction.itl_mdape is not None else "")
375
+ if prediction.latency_is_per_user:
376
+ cards.append(_card(
377
+ "Inter-token latency", _format_seconds(prediction.itl_s),
378
+ "the time the user experiences between tokens" + error_suffix))
379
+ else:
380
+ # Offline the model predicts wall-clock time per token over the whole
381
+ # batch, so throughput is exactly its inverse.
382
+ cards.append(_card(
383
+ "Throughput", f"{prediction.tokens_per_second:,.0f} tok/s",
384
+ "aggregate across the batch" + error_suffix))
385
+
386
+ intensity = f"at {prediction.carbon_intensity_g_per_kwh:g} gCO₂e/kWh"
387
+
388
+ energy_wh = prediction.energy_per_million_tokens_wh
389
+ if energy_wh is not None:
390
+ cards.append(_card(
391
+ "Energy per 1M tokens", format_energy(energy_wh),
392
+ "whole server, including data-centre overhead"))
393
+ cards.append(_card(
394
+ "CO₂e per 1M tokens",
395
+ format_co2(prediction.co2_per_million_tokens_g or 0.0), intensity))
396
+
397
+ hourly_wh = prediction.energy_per_hour_wh
398
+ if hourly_wh is not None:
399
+ cards.append(_card(
400
+ "Host energy per hour", format_energy(hourly_wh),
401
+ "whole server, including data-centre overhead"))
402
+ cards.append(_card(
403
+ "CO₂e per hour",
404
+ format_co2(prediction.co2_per_hour_g or 0.0), intensity))
405
+ else:
406
+ label = "Inter-token latency" if prediction.latency_is_per_user else "Throughput"
407
+ cards.append(_card(label, "—", "unavailable, see below"))
408
+
409
+ parts.append(f'<div class="wg-cards">{"".join(cards)}</div>')
410
+
411
+ if prediction.itl_s is not None and not prediction.latency_is_per_user:
412
+ parts.append(
413
+ '<div class="wg-note">Throughput is the inverse of the inter-token latency '
414
+ 'estimated for this scenario. That latency is measured across the whole batch '
415
+ 'rather than per request, so it cannot be used to derive the inter-token '
416
+ 'latency an individual user would see on a server.</div>'
417
+ )
418
+ if prediction.itl_unavailable_reason:
419
+ parts.append(f'<div class="wg-note">{html.escape(prediction.itl_unavailable_reason)}</div>')
420
+ for warning in prediction.warnings:
421
+ parts.append(f'<div class="wg-note">⚠ {_markdown_bold(warning)}</div>')
422
+ for note in prediction.notes:
423
+ parts.append(f'<div class="wg-note wg-note-quiet">{_markdown_bold(note)}</div>')
424
+
425
+ # A relative href resolves against the current page, so this is a working
426
+ # permalink with no JavaScript and no need to know the deployed URL.
427
+ # A relative href resolves against the current page, so the share button
428
+ # works at any deployed URL without the server knowing what that URL is.
429
+ link_params = {
430
+ "model": prediction.llm.model_id,
431
+ "gpu": prediction.gpu.gpu_db_name,
432
+ "scenario": prediction.scenario,
433
+ }
434
+ # Only carried when it differs from the default, so ordinary links stay short.
435
+ if prediction.carbon_intensity_g_per_kwh != DEFAULT_CARBON_INTENSITY_G_PER_KWH:
436
+ link_params["carbon"] = f"{prediction.carbon_intensity_g_per_kwh:g}"
437
+ parts.insert(0, _result_head(certainty, urllib.parse.urlencode(link_params)))
438
+
439
+ # Inputs used, collapsed: available for checking, out of the way by default.
440
+ llm_heading = ("Model, from the profiled dataset"
441
+ if prediction.llm.params_source == "profiled dataset"
442
+ else "Model, from its config.json on the Hub")
443
+ parts.append(
444
+ '<details class="wg-details"><summary>Inputs used for this prediction</summary>'
445
+ '<div class="wg-specs">'
446
+ + _spec_table(llm_heading, prediction.llm.spec_rows())
447
+ + _spec_table("GPU, from the specification database", prediction.gpu.spec_rows())
448
+ + "</div></details>"
449
+ )
450
+ return "".join(parts)
451
+
452
+
453
+ def _leaner_ai_logo() -> str:
454
+ """Inline the Leaner AI wordmark.
455
+
456
+ Inlined rather than served as a file so the SVG picks up the page's own
457
+ sizing rules and needs no static-file route.
458
+ """
459
+ path = os.path.join(ASSETS_DIR, "leaner-ai-logo.svg")
460
+ try:
461
+ with open(path, encoding="utf-8") as fh:
462
+ svg = fh.read()
463
+ except OSError:
464
+ return '<strong style="font-size:18px">Leaner AI</strong>'
465
+ # Drop the fixed width/height so the CSS controls the size.
466
+ return re.sub(r'\s(width|height)="\d+"', "", svg, count=2)
467
+
468
+
469
+ def _header() -> str:
470
+ links = [
471
+ (LEANER_AI_URL, "Leaner AI", "↗"),
472
+ (PAPER_URL, "Read the paper", "↗"),
473
+ (CODE_URL, "Code", "↗"),
474
+ ]
475
+ anchors = "".join(
476
+ f'<a href="{url}" target="_blank" rel="noopener">{html.escape(text)}'
477
+ f'<span aria-hidden="true">{glyph}</span></a>'
478
+ for url, text, glyph in links
479
+ )
480
+ return (
481
+ '<div class="wg-header">'
482
+ f'<div class="wg-brandmark"><a href="{LEANER_AI_URL}" target="_blank" '
483
+ f'rel="noopener" aria-label="Leaner AI">{_leaner_ai_logo()}</a>'
484
+ '<span class="wg-eyebrow">Research demo</span></div>'
485
+ f'<div class="wg-links">{anchors}</div></div>'
486
+ '<div class="wg-title-bar"></div>'
487
+ )
488
+
489
+
490
+ def _footer() -> str:
491
+ """Attribution, then the disclaimer and terms of use."""
492
+ return (
493
+ '<div class="wg-footer">'
494
+ f'<p>A research demo by <a href="{LEANER_AI_URL}" target="_blank" rel="noopener">Leaner AI</a>. '
495
+ f'Method and results: <a href="{PAPER_URL}" target="_blank" rel="noopener">{html.escape(PAPER_TITLE)}</a>, '
496
+ f'{html.escape(PAPER_VENUE)}. '
497
+ f'Source: <a href="{CODE_URL}" target="_blank" rel="noopener">github.com/maufadel/wattgpu</a>.</p>'
498
+
499
+ '<p><strong>Need this for a real deployment?</strong> Leaner AI does this '
500
+ 'as a service — measured benchmarking and deployment advice for your own '
501
+ 'models, hardware and workload. '
502
+ f'<a href="{CONTACT_URL}" target="_blank" rel="noopener">Get in touch</a>.</p>'
503
+
504
+ '<p class="wg-legal"><strong>Disclaimer and terms of use.</strong> '
505
+ 'This is a research demonstrator provided for informational and '
506
+ 'illustrative purposes only, and is intended as a rough guide rather than '
507
+ 'a basis for decisions. All figures are statistical estimates derived from '
508
+ 'public specifications, not measurements of your deployment, and they carry '
509
+ 'the errors reported above. They are not a benchmark, a certification, an '
510
+ 'energy or emissions disclosure, a capacity plan, or professional advice of '
511
+ 'any kind.</p>'
512
+
513
+ '<p class="wg-legal">The demo and its output are provided <strong>"as is" '
514
+ 'and "as available", without warranties or conditions of any kind</strong>, '
515
+ 'whether express, implied or statutory, including without limitation any '
516
+ 'warranty of accuracy, completeness, reliability, fitness for a particular '
517
+ 'purpose, non-infringement, or uninterrupted or error-free operation.</p>'
518
+
519
+ '<p class="wg-legal">To the fullest extent permitted by applicable law, '
520
+ 'Leaner AI and the authors accept no liability and assume no responsibility '
521
+ 'for any use of this demo or its output, or for any loss or damage of any '
522
+ 'kind — including direct, indirect, incidental, special, consequential, '
523
+ 'exemplary or punitive damages, and any loss of profit, revenue, data, '
524
+ 'business or goodwill — arising out of or in connection with such use, '
525
+ 'whether in contract, tort (including negligence), strict liability or '
526
+ 'otherwise, and whether or not advised of the possibility of such damage. '
527
+ 'Nothing here excludes or limits liability that cannot be excluded or '
528
+ 'limited by law.</p>'
529
+
530
+ '<p class="wg-legal"><strong>You are solely responsible for how you use '
531
+ 'this demo and any data it produces</strong>, including any decision taken '
532
+ 'in reliance on it and any consequences of that decision. You should '
533
+ 'independently verify any figure before relying on it. By using this demo '
534
+ 'you accept these terms; if you do not accept them, do not use it.</p>'
535
+
536
+ '<p class="wg-legal">When an estimate cannot be produced, the model name, '
537
+ 'GPU name, scenario and reason are recorded so we can see which coverage '
538
+ 'gaps matter most. Nothing about who asked is stored.</p>'
539
+
540
+ f'<p class="wg-legal">Provider identification and further legal notices: '
541
+ f'<a href="{LEANER_AI_URL}" target="_blank" rel="noopener">leaner-ai.com</a>.</p>'
542
+ '</div>'
543
+ )
544
+
545
+
546
+ # ---------------------------------------------------------------------------
547
+ # App
548
+ # ---------------------------------------------------------------------------
549
+
550
+ # Leaner AI's brand sky, as Gradio's primary hue (Tailwind `sky`).
551
+ BRAND_SKY = gr.themes.Color(
552
+ c50="#f0f9ff", c100="#e0f2fe", c200="#bae6fd", c300="#7dd3fc", c400="#38bdf8",
553
+ c500="#0ea5e9", c600="#0284c7", c700="#0369a1", c800="#075985", c900="#0c4a6e",
554
+ c950="#082f49",
555
+ )
556
+
557
+ THEME = gr.themes.Soft(
558
+ primary_hue=BRAND_SKY,
559
+ secondary_hue=BRAND_SKY,
560
+ neutral_hue="slate",
561
+ font=[gr.themes.GoogleFont("Inter"), "system-ui", "-apple-system", "sans-serif"],
562
+ )
563
+
564
+ predictor = WattGPUPredictor()
565
+
566
+ SCENARIO_CHOICES = [(SCENARIO_LABELS[s], s) for s in SCENARIOS]
567
+
568
+ # A profiled, widely recognised model, so the first prediction lands on green.
569
+ DEFAULT_MODEL = next(
570
+ (m for m in ("meta-llama/Llama-3.1-8B-Instruct", "Qwen/Qwen2.5-7B-Instruct")
571
+ if m in predictor.profiled_models),
572
+ predictor.profiled_models[0],
573
+ )
574
+
575
+
576
+ def _refusal_reason(error: WattGPUError) -> str | None:
577
+ """Short category for the demand log, or None if not worth recording.
578
+
579
+ Typos and unreachable repositories say nothing about demand, so only
580
+ refusals that represent real unmet coverage are logged.
581
+ """
582
+ if isinstance(error, UnsupportedModelError):
583
+ message = error.message
584
+ if "mixture-of-experts" in message:
585
+ return "moe"
586
+ if "quantised" in message:
587
+ return "quantised"
588
+ return "not-generative"
589
+ if isinstance(error, InsufficientMemoryError):
590
+ return "does-not-fit"
591
+ if isinstance(error, (ModelNotFoundError, ModelConfigError, GPUNotFoundError)):
592
+ return None
593
+ return None
594
+
595
+
596
+ def predict(model_id: str, gpu_name: str, scenario: str,
597
+ carbon_intensity: float | None = None,
598
+ hf_token: str | None = None) -> str:
599
+ """Gradio callback. Every failure becomes a rendered message, never a crash.
600
+
601
+ `hf_token` is a per-request credential: it is handed to the Hub lookup and
602
+ never stored, logged, rendered, or put into a shareable link.
603
+ """
604
+ try:
605
+ return render_prediction(
606
+ predictor.predict(model_id, gpu_name, scenario, carbon_intensity, hf_token))
607
+ except WattGPUError as error:
608
+ reason = _refusal_reason(error)
609
+ if reason:
610
+ demand_log.record_refusal(model_id, gpu_name, scenario, reason)
611
+ return render_error(error)
612
+ except Exception: # noqa: BLE001 - the UI must never show a raw traceback
613
+ traceback.print_exc()
614
+ return _banner(
615
+ "error", "Something went wrong",
616
+ "An unexpected error occurred while making this prediction. The details "
617
+ "were written to the server log. Please try a different model or GPU.",
618
+ )
619
+
620
+
621
+ INTRO = f"""
622
+ # WattGPU
623
+
624
+ Estimate the **power draw** and **inter-token latency** of serving an LLM, before
625
+ you rent the GPU. Search any Hugging Face model, search the NVIDIA GPU database,
626
+ and pick your operational scenario.
627
+
628
+ Every estimate is labelled by how far it sits from the
629
+ {len(predictor.profiled_models)} LLMs and {len(predictor.profiled_gpus)} GPUs that were
630
+ actually measured. [Read the paper]({PAPER_URL}).
631
+ """
632
+
633
+ LEGEND = """
634
+ | | Tier | Meaning | Validation protocol |
635
+ |---|---|---|---|
636
+ | 🟢 | **High** | Both the LLM and the GPU are in the training data. | 5-fold grouped CV |
637
+ | 🟡 | **Moderate** | LLM not in training data, GPU is. | Leave-one-LLM-out (LOLO) |
638
+ | 🟠 | **Lower** | GPU not in training data, LLM is. | Leave-one-GPU-out (LOGO) |
639
+ | 🟠 | **Lower** | Neither is in the training data, under server load. | Double holdout (both removed) |
640
+ | 🔴 | **Low** | Neither is in the training data, offline / batch. | Double holdout (both removed) |
641
+
642
+ Generalising to hardware that was never measured is consistently harder than
643
+ generalising to an unmeasured model architecture, which is why an unknown GPU
644
+ ranks below an unknown LLM.
645
+
646
+ The first three protocols are the paper's. The **double holdout** is an
647
+ extension added for this demo: for every measured pair, the model is retrained
648
+ with that GPU **and** that LLM removed entirely, then asked to predict the pair.
649
+ It is what lets the hardest case carry a measured error instead of an
650
+ unvalidated guess.
651
+
652
+ That measurement is also why the hardest case appears in two different bands.
653
+ Under server load, dropping the LLM as well as the GPU barely hurts — 15.1%
654
+ against 13.1% for power, 8.6% against 8.5% for latency — so it sits in the same
655
+ band as an unknown GPU alone. Offline it does hurt: latency goes from 24.9% to
656
+ 36.2%, which is a genuinely weaker estimate and gets its own band. Whichever
657
+ band is shown, the error quoted is always the one measured for the actual
658
+ situation.
659
+
660
+ Some requests are refused outright rather than given a tier — mixture-of-experts
661
+ and quantised models, embedding and reranking models, and pairs whose weights do
662
+ not fit in GPU memory. Those are outside what the models were fitted on, so an
663
+ estimate would be misleading rather than merely uncertain.
664
+ """
665
+
666
+ TYPICAL_ERROR = """
667
+ The **typical error** shown beside each number is the *median absolute
668
+ percentage error* (MdAPE) of the model, measured during training and looked up
669
+ when you ask — it is not computed from your particular query.
670
+
671
+ ### How it is measured
672
+
673
+ Every row of the dataset is predicted by a model that never saw it
674
+ (*out-of-fold* prediction), then:
675
+
676
+ ```
677
+ MdAPE = median( |actual − predicted| ÷ actual ) × 100
678
+ ```
679
+
680
+ The median rather than the mean, following the paper: it is not distorted by the
681
+ few pairs the model gets badly wrong — but that is also why it describes a
682
+ typical case rather than a worst case.
683
+
684
+ ### Why it changes with the tier
685
+
686
+ The data is split in whatever way reproduces the situation the tier describes,
687
+ so the error you are shown was measured under exactly that kind of
688
+ generalisation:
689
+
690
+ | Tier | How the data was split | What it simulates |
691
+ |---|---|---|
692
+ | 🟢 High | 5-fold, grouped on the (LLM, GPU) pair | both already measured |
693
+ | 🟡 Moderate | leave-one-LLM-out | an architecture never measured |
694
+ | 🟠 Lower | leave-one-GPU-out | hardware never measured |
695
+ | 🟠 / 🔴 | that GPU **and** that LLM both removed | neither ever measured |
696
+
697
+ The band shown and the protocol behind it are not always the same. A pair where
698
+ neither side was measured is backed by the double holdout in every scenario, but
699
+ under server load that protocol performs closely enough to leave-one-GPU-out to
700
+ share its band. The number beside the estimate always comes from the protocol,
701
+ never from the band.
702
+
703
+ Grouping on the pair for the green tier keeps the repeated runs of one
704
+ configuration from landing on both sides of the split, which would otherwise
705
+ flatter the result.
706
+
707
+ The figures are also split between offline and server operation, because the two
708
+ regimes differ a lot — especially for latency, where offline behaviour depends on
709
+ batching that the features only partly capture.
710
+
711
+ ### What it does not tell you
712
+
713
+ - It is a **median across a whole tier**, so half the held-out cases in that tier
714
+ were worse. It is not a bound, and not a confidence interval for your pair.
715
+ - There is **no per-prediction uncertainty**: two queries in the same tier and
716
+ regime always show the same figure.
717
+ - It measures the model against the paper's setup — vLLM, FP16, one GPU,
718
+ 512-token prompts. It says nothing about how well that setup matches yours.
719
+ """
720
+
721
+ SCENARIO_HEADING = """
722
+ <div class="wg-scenario-head">
723
+ <span class="wg-scenario-title">Operational scenario</span>
724
+ <details class="wg-loadhelp">
725
+ <summary><span class="wg-q">?</span> What do these mean?</summary>
726
+ <div class="wg-loadhelp-body">
727
+
728
+ <p>These are three <strong>measured operating points</strong>, not ranges. Every
729
+ server run behind WattGPU used one of exactly two request rates, and the offline
730
+ runs kept the GPU permanently busy:</p>
731
+
732
+ <ul>
733
+ <li><strong>Low load</strong> — 0.017 requests/s. The GPU is idle most of the time.</li>
734
+ <li><strong>Medium load</strong> — 0.33 requests/s. The GPU still keeps up comfortably.</li>
735
+ <li><strong>Offline / batch</strong> — a batch job with work always queued. The GPU
736
+ never goes idle.</li>
737
+ </ul>
738
+
739
+ <p><strong>Concurrent users</strong> are the people <em>using</em> the system at the
740
+ same time — everyone with the chat open, reading a reply or typing the next
741
+ message. That is not the same as concurrent <em>requests</em>: a user spends most of
742
+ their session thinking and reading, and only occasionally sends something for the
743
+ model to answer.</p>
744
+
745
+ <p>To convert:</p>
746
+
747
+ <pre>concurrent users × messages per user per second = requests per second</pre>
748
+
749
+ <p>At about one message per minute each, a user generates
750
+ <code>1 ÷ 60 = 0.017</code> requests per second. So the two measured rates work out
751
+ at roughly <strong>1</strong> and <strong>20</strong> concurrent users — ten people
752
+ chatting sit between the two.</p>
753
+
754
+ <p><strong>Using offline / batch for a busy server.</strong> A server that is fully
755
+ saturated keeps its GPU continuously busy, just as a batch job does, so the
756
+ <em>mean power draw</em> estimated for offline / batch is a reasonable figure for a
757
+ saturated server too. The <em>throughput</em> carries over as the ceiling the pair
758
+ can sustain. What does not carry over is per-user responsiveness: in a saturated
759
+ batch each individual request waits far longer between tokens than the aggregate
760
+ rate suggests.</p>
761
+
762
+ <p><strong>Why it is not easy to determine when a server saturates.</strong> The point where
763
+ load tips into saturation is a property of the whole deployment, not of the
764
+ hardware alone. It moves with model size against memory bandwidth, prompt and
765
+ response lengths, how the server batches concurrent requests, and how bursty the
766
+ arrivals are. A small model on an H200 stays responsive with far more users than a
767
+ 30B model on an L4. WattGPU takes the scenario as one of three measured categories,
768
+ so it cannot interpolate to an arbitrary request rate or locate that tipping point —
769
+ treat low, medium and offline as three reference points rather than a continuum.</p>
770
+
771
+ </div>
772
+ </details>
773
+ </div>
774
+ """
775
+
776
+ ENERGY_ASSUMPTIONS = f"""
777
+ ### What the energy figures cover
778
+
779
+ The models predict the **GPU board's** power draw. The figures shown are scaled
780
+ up from there to roughly what the wall socket sees:
781
+
782
+ ```
783
+ facility watts = GPU watts ÷ {GPU_SHARE_OF_HOST_POWER} × {DEFAULT_PUE}
784
+ ```
785
+
786
+ The GPU is taken as **{GPU_SHARE_OF_HOST_POWER:.0%} of the host server's draw** — the rest is CPU,
787
+ memory, storage, NICs and fans — and a **PUE of {DEFAULT_PUE}** is applied on top for
788
+ cooling and power distribution. Both are typical values rather than
789
+ measurements, so your own hardware and site will differ. The same scaling
790
+ applies in every scenario, so the per-token and per-hour numbers describe the
791
+ same system boundary.
792
+
793
+ ### Why per token offline, but per hour under server load
794
+
795
+ Offline the GPU is saturated: it is generating essentially all of the time, so
796
+ every joule is attributable to a token and **energy per token** is a stable
797
+ property of the model and the hardware.
798
+
799
+ Under server load it is not. The GPU spends much of its time waiting for the
800
+ next request, so energy per token would mostly reflect how busy the server
801
+ happens to be rather than anything about the model or the card. What an operator
802
+ can act on there is the draw of keeping the deployment up, so the figures are
803
+ reported **per hour** instead.
804
+
805
+ ### Grid carbon intensity
806
+ """
807
+
808
+ HF_TOKEN_HELP = f"""
809
+ The demo reads model architectures from the Hugging Face Hub anonymously, which
810
+ is enough almost all of the time. A token helps in two cases:
811
+
812
+ - **Licence-gated models.** Most of the well-known gated models are already
813
+ bundled, but for one that is not, a token from an account that has accepted
814
+ the licence will read it.
815
+ - **Rate limiting.** The Hub limits anonymous requests per IP address, and every
816
+ visitor to this demo shares one. Your own token gives you your own allowance.
817
+
818
+ Create a **read-only** token at
819
+ [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens). It is
820
+ used for your request and then discarded — never stored, never logged, and never
821
+ included in a shared link. Even so, prefer a fine-grained read-only token over
822
+ one with write access.
823
+
824
+ Measured and bundled models need no token at all: they are answered without
825
+ contacting the Hub.
826
+ """
827
+
828
+ CITATION = rf"""
829
+ If this demo or the models behind it are useful in your work, please cite the
830
+ paper:
831
+
832
+ > Fadel Argerich, M., Fürst, J., and Patiño-Martínez, M. (2026).
833
+ > *{PAPER_TITLE}*.
834
+ > {PAPER_VENUE}. CEUR Workshop Proceedings, ISSN 1613-0073.
835
+ > [arxiv.org/abs/2607.02391]({PAPER_URL})
836
+
837
+ ```bibtex
838
+ @inproceedings{{fadelargerich2026wattgpu,
839
+ title = {{{{WattGPU}}: Predicting Inference Power and Latency on
840
+ Unseen GPUs and LLMs}},
841
+ author = {{Fadel Argerich, Mauricio and F{{\"u}}rst, Jonathan and
842
+ Pati{{\~n}}o-Mart{{\'i}}nez, Marta}},
843
+ booktitle = {{Proceedings of the 1st Workshop on Sustainability and
844
+ Resource-Efficiency of Artificial Intelligence (SuRE) @ IJCAI}},
845
+ series = {{CEUR Workshop Proceedings}},
846
+ publisher = {{CEUR-WS.org}},
847
+ issn = {{1613-0073}},
848
+ year = {{2026}},
849
+ eprint = {{2607.02391}},
850
+ archivePrefix = {{arXiv}},
851
+ url = {{{PAPER_URL}}}
852
+ }}
853
+ ```
854
+
855
+ The code, the trained models and the measurement subset are available under
856
+ Apache 2.0 at [github.com/maufadel/wattgpu]({CODE_URL}).
857
+ """
858
+
859
+ # Tables 2 and 3 of the paper, reproduced by `scripts/train_models.py`.
860
+ # MdAPE is the median absolute percentage error; τ is Kendall's tau over the
861
+ # induced ranking of GPUs and of LLMs.
862
+ PAPER_RESULTS = f"""
863
+ These are the numbers reported in
864
+ [{PAPER_TITLE}]({PAPER_URL}), reproduced by `scripts/train_models.py` in this
865
+ repository. **MdAPE** is the median absolute percentage error. **Pearson r**
866
+ measures how well predictions track the true values, and **GPU τ** / **LLM τ**
867
+ are Kendall rank correlations — how reliably the model puts GPUs, and LLMs, in
868
+ the right order. Ranking matters as much as absolute error when the question is
869
+ "which of these should I deploy on?".
870
+
871
+ ### Mean power draw (Table 2)
872
+
873
+ | Model | Scenario | Strategy | MdAPE | Pearson r | GPU τ | LLM τ |
874
+ |---|---|---|---|---|---|---|
875
+ | XGBoost | Offline | CV | **1.6%** | 0.992 | 0.96 | 0.44 |
876
+ | XGBoost | Offline | LOGO | **3.4%** | 0.988 | 0.95 | 0.40 |
877
+ | XGBoost | Offline | LOLO | **2.0%** | 0.979 | 0.97 | 0.33 |
878
+ | XGBoost | Server | CV | **5.5%** | 0.981 | 0.86 | 0.68 |
879
+ | XGBoost | Server | LOGO | **13.5%** | 0.965 | 0.76 | 0.72 |
880
+ | XGBoost | Server | LOLO | **6.7%** | 0.955 | 0.85 | 0.54 |
881
+ | TDP baseline | Offline | — | 4.4% | 0.916 | 0.96 | — |
882
+ | TDP baseline | Server | — | 190.1% | 0.590 | 0.60 | — |
883
+ | Least-squares TDP | Offline | — | 7.2% | 0.916 | 0.96 | — |
884
+ | Least-squares TDP | Server | — | 26.0% | 0.779 | 0.60 | — |
885
+
886
+ Assuming a GPU simply draws its TDP is roughly adequate offline, where a
887
+ saturated GPU really does sit near its power limit, and badly wrong under server
888
+ load (190% error) where utilisation is far below peak. That is where the learned
889
+ model earns its place.
890
+
891
+ ### Inter-token latency (Table 3)
892
+
893
+ | Model | Scenario | Strategy | MdAPE | Pearson r | GPU τ | LLM τ |
894
+ |---|---|---|---|---|---|---|
895
+ | XGBoost | Offline | CV | **12.8%** | 0.813 | 0.86 | 0.81 |
896
+ | XGBoost | Offline | LOGO | **24.9%** | 0.727 | 0.72 | 0.83 |
897
+ | XGBoost | Offline | LOLO | **15.6%** | 0.808 | 0.97 | 0.66 |
898
+ | XGBoost | Server | CV | **4.9%** | 0.991 | 0.94 | 0.89 |
899
+ | XGBoost | Server | LOGO | **8.5%** | 0.972 | 0.78 | 0.88 |
900
+ | XGBoost | Server | LOLO | **5.6%** | 0.990 | 0.97 | 0.88 |
901
+ | Roofline baseline | Offline | — | 80.2% | 0.545 | 0.82 | 0.61 |
902
+ | Roofline baseline | Server | — | 29.6% | 0.991 | 0.88 | 0.91 |
903
+
904
+ Latency is harder offline than under server load: offline throughput depends on
905
+ how vLLM batches requests, which the features capture only partly. The roofline
906
+ baseline — the time to stream the model's FP16 weights from memory once per
907
+ token — tracks the *ordering* well (τ up to 0.91) but is off by 80% in
908
+ magnitude offline, because it ignores batching entirely.
909
+
910
+ ### How to read this
911
+
912
+ - **CV** rows are the green tier, **LOLO** the yellow, **LOGO** the orange.
913
+ - Generalising to an unseen **GPU** (LOGO) is consistently harder than to an
914
+ unseen **LLM** (LOLO). That is why an unprofiled GPU is ranked below an
915
+ unprofiled LLM in the certainty tiers.
916
+ - All errors are for the paper's setup: vLLM, FP16 weights, a single GPU,
917
+ 512-token prompts. They are not warranties for other configurations.
918
+ """
919
+
920
+ def search_hub(key_up: gr.KeyUpData, hf_token: str | None = None) -> gr.Dropdown:
921
+ """Live search of the Hugging Face Hub as the user types.
922
+
923
+ Profiled models are surfaced first, since those give the most reliable
924
+ estimates, then whatever the Hub returns for the query.
925
+ """
926
+ query = (key_up.input_value or "").strip()
927
+ if len(query) < 2:
928
+ return gr.Dropdown(choices=predictor.known_models)
929
+
930
+ lowered = query.lower()
931
+ known = [m for m in predictor.known_models if lowered in m.lower()]
932
+ hub = [m for m in search_models(query, token=hf_token) if m not in known]
933
+ return gr.Dropdown(choices=known + hub)
934
+
935
+
936
+ def on_submit(model_id: str, gpu_name: str, scenario: str,
937
+ carbon_intensity: float | None,
938
+ hf_token: str | None = None) -> tuple[str, float]:
939
+ """Run an estimate and echo back the intensity actually used.
940
+
941
+ Clearing the intensity leaves it empty, so the value that was applied is
942
+ written back into it -- the box never disagrees with the number in the
943
+ result. The token is passed straight through and never echoed back.
944
+ """
945
+ intensity = clean_carbon_intensity(carbon_intensity)
946
+ return predict(model_id, gpu_name, scenario, intensity, hf_token), intensity
947
+
948
+
949
+ def search_gpus(key_up: gr.KeyUpData) -> gr.Dropdown:
950
+ """Search the whole GPU catalogue as the user types.
951
+
952
+ The list starts as just the measured GPUs, which are the ones that give a
953
+ high-confidence estimate; the other 500-odd are a search away.
954
+ """
955
+ query = (key_up.input_value or "").strip()
956
+ if len(query) < 2:
957
+ return gr.Dropdown(choices=predictor.profiled_gpus)
958
+
959
+ lowered = query.lower()
960
+ measured = [g for g in predictor.profiled_gpus if lowered in g.lower()]
961
+ rest = [g for g in predictor.gpu_db.search(query) if g not in measured]
962
+ return gr.Dropdown(choices=measured + rest)
963
+
964
+
965
+ def restore_from_link(request: gr.Request):
966
+ """Populate the form from a shared permalink, and run it.
967
+
968
+ Values are validated against the catalogues before use, so a hand-edited
969
+ link cannot put the form into a state the app would not otherwise reach.
970
+ """
971
+ params = dict(request.query_params or {})
972
+ model = (params.get("model") or "").strip()
973
+ gpu = (params.get("gpu") or "").strip()
974
+ scenario = (params.get("scenario") or "").strip()
975
+
976
+ unchanged = (gr.skip(), gr.skip(), gr.skip(), gr.skip(), gr.skip())
977
+ if not (model and gpu):
978
+ return unchanged
979
+ if scenario not in SCENARIOS:
980
+ scenario = "server_low"
981
+ try:
982
+ gpu = predictor.gpu_db.get(gpu).gpu_db_name
983
+ except WattGPUError:
984
+ return unchanged # do not open the page on an error
985
+
986
+ carbon = clean_carbon_intensity(params.get("carbon"))
987
+ return (
988
+ gr.Dropdown(choices=sorted({*predictor.known_models, model}), value=model),
989
+ gr.Dropdown(choices=sorted({*predictor.profiled_gpus, gpu}), value=gpu),
990
+ scenario,
991
+ carbon,
992
+ predict(model, gpu, scenario, carbon),
993
+ )
994
+
995
+
996
+ # `analytics_enabled=False`: the form carries a per-request Hugging Face token,
997
+ # so no usage telemetry leaves the deployment.
998
+ with gr.Blocks(title="WattGPU — Leaner AI", theme=THEME, css=CSS, head=HEAD,
999
+ analytics_enabled=False) as demo:
1000
+ gr.HTML(_header())
1001
+ gr.Markdown(INTRO)
1002
+
1003
+ with gr.Row():
1004
+ with gr.Column(scale=3):
1005
+ model_input = gr.Dropdown(
1006
+ label="Model",
1007
+ info="Search any model on the Hugging Face Hub.",
1008
+ choices=predictor.known_models,
1009
+ value=DEFAULT_MODEL,
1010
+ allow_custom_value=True,
1011
+ filterable=True,
1012
+ )
1013
+ with gr.Column(scale=3):
1014
+ gpu_input = gr.Dropdown(
1015
+ label="GPU",
1016
+ info=("Measured GPUs are shown in the list. "
1017
+ "Type to search the rest of the NVIDIA database."),
1018
+ choices=predictor.profiled_gpus,
1019
+ value=predictor.profiled_gpus[0],
1020
+ allow_custom_value=True,
1021
+ filterable=True,
1022
+ )
1023
+
1024
+ gr.HTML(SCENARIO_HEADING)
1025
+ scenario_input = gr.Radio(
1026
+ label="Operational scenario",
1027
+ show_label=False, # the heading above carries the label and its help
1028
+ choices=SCENARIO_CHOICES,
1029
+ value="server_low",
1030
+ )
1031
+
1032
+
1033
+ submit = gr.Button("Estimate power and latency", variant="primary", size="lg")
1034
+ output = gr.HTML(label="Estimate")
1035
+
1036
+ with gr.Accordion("Assumptions behind the energy and emissions figures", open=False):
1037
+ gr.Markdown(ENERGY_ASSUMPTIONS)
1038
+ carbon_input = gr.Number(
1039
+ label="Grid carbon intensity (gCO₂e/kWh)",
1040
+ info=(f"Default is the {DEFAULT_CARBON_INTENSITY_LABEL}. Your own grid "
1041
+ "might be different — check it for your region and hour on "
1042
+ f"[Electricity Maps]({ELECTRICITY_MAPS_URL}). Leave it empty to "
1043
+ "go back to the default."),
1044
+ value=DEFAULT_CARBON_INTENSITY_G_PER_KWH,
1045
+ minimum=MIN_CARBON_INTENSITY,
1046
+ maximum=MAX_CARBON_INTENSITY,
1047
+ step=10,
1048
+ precision=0,
1049
+ )
1050
+
1051
+ with gr.Accordion("Use your own Hugging Face token", open=False):
1052
+ gr.Markdown(HF_TOKEN_HELP)
1053
+ token_input = gr.Textbox(
1054
+ label="Hugging Face access token",
1055
+ placeholder="hf_…",
1056
+ type="password",
1057
+ lines=1,
1058
+ autofocus=False,
1059
+ )
1060
+
1061
+ with gr.Accordion("How to read the certainty tier", open=False):
1062
+ gr.Markdown(LEGEND)
1063
+
1064
+ with gr.Accordion("How the typical error is calculated", open=False):
1065
+ gr.Markdown(TYPICAL_ERROR)
1066
+
1067
+ with gr.Accordion("Model accuracy, as reported in the paper", open=False):
1068
+ gr.Markdown(PAPER_RESULTS)
1069
+
1070
+ with gr.Accordion("Cite this work", open=False):
1071
+ gr.Markdown(CITATION)
1072
+
1073
+ with gr.Accordion("What was measured", open=False):
1074
+ gr.Markdown(
1075
+ f"**GPUs ({len(predictor.profiled_gpus)}):** "
1076
+ + ", ".join(f"`{g}`" for g in predictor.profiled_gpus)
1077
+ + f"\n\n**LLMs ({len(predictor.profiled_models)}):** "
1078
+ + ", ".join(f"`{m}`" for m in predictor.profiled_models)
1079
+ + "\n\nAll from the subset of "
1080
+ f"[Watt Counts]({WATT_COUNTS_URL}) used in the paper: vLLM, FP16 "
1081
+ "weights, one GPU, 512-token prompts. Quantised, multi-GPU or "
1082
+ "long-context deployments will differ.\n\n"
1083
+ + ("Licence-gated models are readable: an `HF_TOKEN` is configured."
1084
+ if has_hf_token() else
1085
+ "Licence-gated models that were not measured (some Llama and Gemma "
1086
+ "variants) need an `HF_TOKEN` environment variable from an account "
1087
+ "that has accepted their licences.")
1088
+ )
1089
+
1090
+ gr.HTML(_footer())
1091
+
1092
+ demo.load(restore_from_link,
1093
+ outputs=[model_input, gpu_input, scenario_input, carbon_input, output])
1094
+ model_input.key_up(search_hub, inputs=token_input, outputs=model_input,
1095
+ show_progress="hidden", queue=False)
1096
+ gpu_input.key_up(search_gpus, outputs=gpu_input,
1097
+ show_progress="hidden", queue=False)
1098
+ submit.click(on_submit,
1099
+ inputs=[model_input, gpu_input, scenario_input, carbon_input,
1100
+ token_input],
1101
+ outputs=[output, carbon_input])
1102
+
1103
+
1104
+ if __name__ == "__main__":
1105
+ demo.launch(server_name=os.environ.get("GRADIO_SERVER_NAME", "127.0.0.1"))
assets/leaner-ai-logo.svg ADDED
data/demand_log.jsonl ADDED
@@ -0,0 +1 @@
 
 
1
+ {"at": "2026-09-03T13:21:01+00:00", "model": "microsoft/phi-4", "gpu": "Tesla T4", "scenario": "offline", "reason": "does-not-fit"}
data/gated_llms.json ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "google/gemma-3-1b-it": {
3
+ "architectures": [
4
+ "Gemma3ForCausalLM"
5
+ ],
6
+ "hidden_size": 1152,
7
+ "max_position_embeddings": 32768,
8
+ "model_type": "gemma3_text",
9
+ "num_attention_heads": 4,
10
+ "num_key_value_heads": 1,
11
+ "num_layers": 26,
12
+ "source_mirror": "unsloth/gemma-3-1b-it",
13
+ "torch_dtype": "bfloat16",
14
+ "total_b_params": 0.999886
15
+ },
16
+ "google/gemma-3-270m": {
17
+ "architectures": [
18
+ "Gemma3ForCausalLM"
19
+ ],
20
+ "hidden_size": 640,
21
+ "max_position_embeddings": 32768,
22
+ "model_type": "gemma3_text",
23
+ "num_attention_heads": 4,
24
+ "num_key_value_heads": 1,
25
+ "num_layers": 18,
26
+ "source_mirror": "unsloth/gemma-3-270m",
27
+ "torch_dtype": "bfloat16",
28
+ "total_b_params": 0.268098
29
+ },
30
+ "meta-llama/Llama-3.2-1B": {
31
+ "architectures": [
32
+ "LlamaForCausalLM"
33
+ ],
34
+ "hidden_size": 2048,
35
+ "max_position_embeddings": 131072,
36
+ "model_type": "llama",
37
+ "num_attention_heads": 32,
38
+ "num_key_value_heads": 8,
39
+ "num_layers": 16,
40
+ "source_mirror": "unsloth/Llama-3.2-1B",
41
+ "torch_dtype": "bfloat16",
42
+ "total_b_params": 1.235814
43
+ },
44
+ "meta-llama/Llama-3.2-1B-Instruct": {
45
+ "architectures": [
46
+ "LlamaForCausalLM"
47
+ ],
48
+ "hidden_size": 2048,
49
+ "max_position_embeddings": 131072,
50
+ "model_type": "llama",
51
+ "num_attention_heads": 32,
52
+ "num_key_value_heads": 8,
53
+ "num_layers": 16,
54
+ "source_mirror": "unsloth/Llama-3.2-1B-Instruct",
55
+ "torch_dtype": "bfloat16",
56
+ "total_b_params": 1.235814
57
+ },
58
+ "meta-llama/Meta-Llama-3-8B-Instruct": {
59
+ "architectures": [
60
+ "LlamaForCausalLM"
61
+ ],
62
+ "hidden_size": 4096,
63
+ "max_position_embeddings": 8192,
64
+ "model_type": "llama",
65
+ "num_attention_heads": 32,
66
+ "num_key_value_heads": 8,
67
+ "num_layers": 32,
68
+ "source_mirror": "NousResearch/Meta-Llama-3-8B-Instruct",
69
+ "torch_dtype": "bfloat16",
70
+ "total_b_params": 8.030261
71
+ }
72
+ }
data/gpu_database.csv ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ manufacturer,gpu_db_name,gpu_name,generation,architecture,base_clock_mhz,boost_clock_mhz,process_size_nm,transistor_count_m,release_date,memory_clock_mhz,memory_size_gb,memory_bus_bits,memory_bandwidth_gb_s,memory_type,shading_units,streaming_multiprocessors,tensor_cores,l2_cache_mb,thermal_design_power_w,half_float_performance_gflop_s,single_float_performance_gflop_s,tpu_url,release_year,tensor_tflops_16b,tensor_tflops_source,boost_percentage
2
+ NVIDIA,A10 PCIe,GA102,Server Ampere(Axx),Ampere,885.0,1695.0,8.0,28300.0,2021-04-12,1563.0,24.0,384.0,600.2,GDDR6,9216,72,288,6.0,150.0,31240.0,31240.0,https://www.techpowerup.com/gpu-specs/a10-pcie.c3793,2021,125.0,reported,1.9152542372881356
3
+ NVIDIA,A100 PCIe 40 GB,GA100,Server Ampere(Axx),Ampere,765.0,1410.0,7.0,54200.0,2020-06-22,1215.0,40.0,5120.0,1560.0,HBM2e,6912,108,432,40.0,250.0,77970.0,19490.0,https://www.techpowerup.com/gpu-specs/a100-pcie-40-gb.c3623,2020,312.0,reported,1.8431372549019607
4
+ NVIDIA,A100 PCIe 80 GB,GA100,Server Ampere(Axx),Ampere,1065.0,1410.0,7.0,54200.0,2021-06-28,1512.0,80.0,5120.0,1940.0,HBM2e,6912,108,432,80.0,300.0,77970.0,19490.0,https://www.techpowerup.com/gpu-specs/a100-pcie-80-gb.c3821,2021,312.0,reported,1.323943661971831
5
+ NVIDIA,A100 SXM4 40 GB,GA100,Server Ampere(Axx),Ampere,1095.0,1410.0,7.0,54200.0,2020-05-14,1215.0,40.0,5120.0,1560.0,HBM2e,6912,108,432,40.0,400.0,77970.0,19490.0,https://www.techpowerup.com/gpu-specs/a100-sxm4-40-gb.c3506,2020,312.0,reported,1.2876712328767124
6
+ NVIDIA,A100 SXM4 80 GB,GA100,Server Ampere(Axx),Ampere,1275.0,1410.0,7.0,54200.0,2020-11-16,1593.0,80.0,5120.0,2040.0,HBM2e,6912,108,432,40.0,400.0,77970.0,19490.0,https://www.techpowerup.com/gpu-specs/a100-sxm4-80-gb.c3746,2020,312.0,reported,1.1058823529411765
7
+ NVIDIA,A100X,GA100,Server Ampere(Axx),Ampere,795.0,1440.0,7.0,54200.0,2021-06-28,1593.0,80.0,5120.0,2040.0,HBM2e,6912,108,432,80.0,300.0,79630.0,19910.0,https://www.techpowerup.com/gpu-specs/a100x.c3958,2021,318.5,estimated,1.8113207547169812
8
+ NVIDIA,A10G,GA102,Server Ampere(Axx),Ampere,1320.0,1710.0,8.0,28300.0,2021-04-12,1563.0,24.0,384.0,600.2,GDDR6,9216,72,288,6.0,150.0,31520.0,31520.0,https://www.techpowerup.com/gpu-specs/a10g.c3798,2021,70.0,reported,1.2954545454545454
9
+ NVIDIA,A10M,GA102,Server Ampere(Axx),Ampere,975.0,1635.0,8.0,28300.0,2022-02-01,1563.0,20.0,320.0,500.2,GDDR6,7168,56,224,6.0,150.0,23440.0,23440.0,https://www.techpowerup.com/gpu-specs/a10m.c4200,2022,93.8,estimated,1.676923076923077
10
+ NVIDIA,A16 PCIe,GA107,Server Ampere(Axx),Ampere,1312.0,1755.0,8.0,8700.0,2021-04-12,1563.0,16.0,128.0,200.1,GDDR6,1280,10,40,2.0,250.0,4493.0,4493.0,https://www.techpowerup.com/gpu-specs/a16-pcie.c3794,2021,71.0,reported,1.3376524390243902
11
+ NVIDIA,A2,GA107,Workstation Ampere(Ax000),Ampere,1440.0,1770.0,8.0,8700.0,2021-11-10,1563.0,16.0,128.0,200.1,GDDR6,1280,10,40,2.0,60.0,4531.0,4531.0,https://www.techpowerup.com/gpu-specs/a2.c3848,2021,18.1,estimated,1.2291666666666667
12
+ NVIDIA,A2 PCIe,GA107,Server Ampere(Axx),Ampere,1440.0,1770.0,8.0,8700.0,2021-11-10,1563.0,16.0,128.0,200.1,GDDR6,1280,10,40,2.0,60.0,4531.0,4531.0,https://www.techpowerup.com/gpu-specs/a2-pcie.c4112,2021,36.0,reported,1.2291666666666667
13
+ NVIDIA,A30 PCIe,GA100,Server Ampere(Axx),Ampere,930.0,1440.0,7.0,54200.0,2021-04-12,1215.0,24.0,3072.0,933.1,HBM2e,3584,56,224,24.0,165.0,10320.0,10320.0,https://www.techpowerup.com/gpu-specs/a30-pcie.c3792,2021,165.0,reported,1.5483870967741935
14
+ NVIDIA,A30X,GA100,Server Ampere(Axx),Ampere,1035.0,1440.0,7.0,54200.0,2021-04-12,1593.0,24.0,3072.0,1220.0,HBM2e,3584,56,224,24.0,230.0,10320.0,10320.0,https://www.techpowerup.com/gpu-specs/a30x.c3968,2021,165.2,estimated,1.391304347826087
15
+ NVIDIA,A40 PCIe,GA102,Server Ampere(Axx),Ampere,1305.0,1740.0,8.0,28300.0,2020-10-05,1812.0,48.0,384.0,695.8,GDDR6,10752,84,336,6.0,300.0,37420.0,37420.0,https://www.techpowerup.com/gpu-specs/a40-pcie.c3700,2020,150.0,reported,1.3333333333333333
16
+ NVIDIA,A800 PCIe 40 GB,GA100,Server Ampere(Axx),Ampere,765.0,1410.0,7.0,54200.0,2022-11-08,1215.0,40.0,5120.0,1560.0,HBM2e,6912,108,432,40.0,250.0,77970.0,19490.0,https://www.techpowerup.com/gpu-specs/a800-pcie-40-gb.c3964,2022,311.9,estimated,1.8431372549019607
17
+ NVIDIA,A800 PCIe 80 GB,GA100,Server Ampere(Axx),Ampere,1065.0,1410.0,7.0,54200.0,2022-11-08,1512.0,80.0,5120.0,1940.0,HBM2e,6912,108,432,80.0,250.0,77970.0,19490.0,https://www.techpowerup.com/gpu-specs/a800-pcie-80-gb.c3965,2022,311.9,estimated,1.323943661971831
18
+ NVIDIA,A800 SXM4 80 GB,GA100,Server Ampere(Axx),Ampere,1155.0,1410.0,7.0,54200.0,2022-08-11,1593.0,80.0,5120.0,2040.0,HBM2e,6912,108,432,40.0,400.0,77970.0,19490.0,https://www.techpowerup.com/gpu-specs/a800-sxm4-80-gb.c3966,2022,311.9,estimated,1.2207792207792207
19
+ NVIDIA,B200,GB100,Server Blackwell(Bxx),Blackwell,700.0,1965.0,5.0,104000.0,2024-01-01,2000.0,90.0,4096.0,4099.999999999999,HBM3e,18944,148,592,50.0,1000.0,1191200.0,74450.0,https://www.techpowerup.com/gpu-specs/b200.c4210,2024,297.8,estimated,2.807142857142857
20
+ NVIDIA,B300,GB110,Server Blackwell(Bxx),Blackwell Ultra,1665.0,2032.0,5.0,104000.0,2025-09-11,2000.0,144.0,4096.0,4099.999999999999,HBM3e,18944,148,592,50.0,1400.0,1231800.0,76990.0,https://www.techpowerup.com/gpu-specs/b300.c4375,2025,,unknown,1.2204204204204203
21
+ NVIDIA,CMP 100HX-210,GV100,Mining GPUs,Volta,555.0,1147.0,12.0,21100.0,2020-01-01,810.0,16.0,4096.0,829.4,HBM2,5120,80,640,6.0,250.0,23490.0,11750.0,https://www.techpowerup.com/gpu-specs/cmp-100hx-210.c4202,2020,94.0,estimated,2.066666666666667
22
+ NVIDIA,CMP 170HX 10 GB,GA100,Mining GPUs,Ampere,1140.0,1410.0,7.0,54200.0,2021-09-01,1215.0,10.0,5120.0,1560.0,HBM2e,4480,70,280,10.0,250.0,50530.0,12630.0,https://www.techpowerup.com/gpu-specs/cmp-170hx-10-gb.c3957,2021,202.1,estimated,1.236842105263158
23
+ NVIDIA,CMP 170HX 8 GB,GA100,Mining GPUs,Ampere,1140.0,1410.0,7.0,54200.0,2021-09-01,1458.0,8.0,4096.0,1490.0,HBM2e,4480,70,280,8.0,250.0,50530.0,12630.0,https://www.techpowerup.com/gpu-specs/cmp-170hx-8-gb.c3830,2021,202.1,estimated,1.236842105263158
24
+ NVIDIA,CMP 30HX,TU116,Mining GPUs,Turing,1530.0,1785.0,12.0,6600.0,2021-02-25,1750.0,6.0,192.0,336.0,GDDR6,1408,22,0,1.5,125.0,10050.0,5027.0,https://www.techpowerup.com/gpu-specs/cmp-30hx.c3780,2021,,unknown,1.1666666666666667
25
+ NVIDIA,CMP 40HX,TU106,Mining GPUs,Turing,1470.0,1650.0,12.0,10800.0,2021-02-25,1750.0,8.0,256.0,448.0,GDDR6,2304,36,288,4.0,185.0,15210.0,7603.0,https://www.techpowerup.com/gpu-specs/cmp-40hx.c3781,2021,60.8,estimated,1.1224489795918366
26
+ NVIDIA,CMP 50HX,TU102,Mining GPUs,Turing,1350.0,1545.0,12.0,18600.0,2021-06-24,1750.0,10.0,320.0,560.0,GDDR6,3584,56,448,5.0,250.0,22150.0,11070.0,https://www.techpowerup.com/gpu-specs/cmp-50hx.c3782,2021,88.6,estimated,1.1444444444444444
27
+ NVIDIA,CMP 90HX,GA102,Mining GPUs,Ampere,1500.0,1710.0,8.0,28300.0,2021-07-28,1188.0,10.0,320.0,760.3,GDDR6X,6400,50,200,5.0,320.0,21890.0,21890.0,https://www.techpowerup.com/gpu-specs/cmp-90hx.c3783,2021,87.6,estimated,1.14
28
+ NVIDIA,DRIVE A100 PROD,GA100,DRIVE(Axx),Ampere,1260.0,1260.0,7.0,54200.0,2020-05-14,1215.0,32.0,6144.0,1870.0,HBM2e,6912,108,432,32.0,400.0,69670.0,17420.000000000004,https://www.techpowerup.com/gpu-specs/drive-a100-prod.c3967,2020,278.7,estimated,1.0
29
+ NVIDIA,GB10,GB20B,Server Blackwell(Bxx),Blackwell 2.0,1665.0,2418.0,5.0,,2025-10-15,1067.0,128.0,256.0,273.2,LPDDR5X,6144,48,384,50.0,140.0,29710.0,29710.0,https://www.techpowerup.com/gpu-specs/gb10.c4342,2025,237.7,estimated,1.4522522522522523
30
+ NVIDIA,GRID A100A,GA100,GRID(Ax),Ampere,1095.0,1410.0,7.0,54200.0,2020-05-14,1215.0,32.0,6144.0,1870.0,HBM2e,6912,108,432,32.0,400.0,77970.0,19490.0,https://www.techpowerup.com/gpu-specs/grid-a100a.c3579,2020,311.9,estimated,1.2876712328767124
31
+ NVIDIA,GRID A100B,GA100,GRID(Ax),Ampere,900.0,1005.0,7.0,54200.0,2020-05-14,1215.0,48.0,6144.0,1870.0,HBM2e,6912,108,432,48.0,400.0,55570.0,13890.0,https://www.techpowerup.com/gpu-specs/grid-a100b.c3578,2020,222.3,estimated,1.1166666666666667
32
+ NVIDIA,GRID M10-8Q,GM107,GRID(Mx),Maxwell,1033.0,1306.0,28.0,1870.0,2016-05-18,1300.0,8.0,128.0,83.2,GDDR5,640,5,0,2.0,225.0,,1672.0,https://www.techpowerup.com/gpu-specs/grid-m10-8q.c3086,2016,,unknown,1.2642787996127782
33
+ NVIDIA,GRID M40,GM107,GRID(Mx),Maxwell,1033.0,1033.0,28.0,1870.0,2016-05-18,1300.0,8.0,128.0,83.2,GDDR5,384,3,0,2.0,50.0,,793.3,https://www.techpowerup.com/gpu-specs/grid-m40.c2518,2016,,unknown,1.0
34
+ NVIDIA,GRID M6-8Q,GM204,GRID(Mx),Maxwell 2.0,722.0,722.0,28.0,5200.0,2015-08-30,1253.0,8.0,256.0,160.4,GDDR5,1536,12,0,2.0,100.0,,2218.0,https://www.techpowerup.com/gpu-specs/grid-m6-8q.c3052,2015,,unknown,1.0
35
+ NVIDIA,GRID M60-8Q,GM204,GRID(Mx),Maxwell 2.0,557.0,1178.0,28.0,5200.0,2015-08-30,1253.0,8.0,256.0,160.4,GDDR5,2048,16,0,2.0,225.0,,4825.0,https://www.techpowerup.com/gpu-specs/grid-m60-8q.c3089,2015,,unknown,2.1149012567324954
36
+ NVIDIA,GeForce GTX 1060 6 GB,GP106,GeForce 10,Pascal,1506.0,1709.0,16.0,4400.0,2016-07-19,2002.0,6.0,192.0,192.2,GDDR5,1280,10,0,1.5,120.0,68.36,4375.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-1060-6-gb.c2862,2016,,unknown,1.1347941567065074
37
+ NVIDIA,GeForce GTX 1060 6 GB 9Gbps,GP106,GeForce 10,Pascal,1506.0,1709.0,16.0,4400.0,2017-04-20,2257.0,6.0,192.0,216.7,GDDR5,1280,10,0,1.5,120.0,68.36,4375.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-1060-6-gb-9gbps.c2952,2017,,unknown,1.1347941567065074
38
+ NVIDIA,GeForce GTX 1060 6 GB GDDR5X,GP104,GeForce 10,Pascal,1506.0,1709.0,16.0,7200.0,2018-10-18,1001.0,6.0,192.0,192.2,GDDR5X,1280,10,0,1.5,120.0,68.36,4375.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-1060-6-gb-gddr5x.c3328,2018,,unknown,1.1347941567065074
39
+ NVIDIA,GeForce GTX 1060 6 GB GP104,GP104,GeForce 10,Pascal,1506.0,1708.0,16.0,7200.0,2018-03-08,2002.0,6.0,192.0,192.2,GDDR5,1280,10,0,1.5,120.0,68.32,4372.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-1060-6-gb-gp104.c3250,2018,,unknown,1.1341301460823374
40
+ NVIDIA,GeForce GTX 1060 6 GB Rev. 2,GP106,GeForce 10,Pascal,1506.0,1709.0,16.0,4400.0,2018-01-05,2002.0,6.0,192.0,192.2,GDDR5,1280,10,0,1.5,120.0,68.36,4375.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-1060-6-gb-rev-2.c3063,2018,,unknown,1.1347941567065074
41
+ NVIDIA,GeForce GTX 1060 Max-Q,GP106,GeForce 10 Mobile,Pascal,1063.0,1480.0,16.0,4400.0,2017-06-27,2002.0,6.0,192.0,192.2,GDDR5,1280,10,0,1.5,80.0,59.2,3789.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-1060-max-q.c2976,2017,,unknown,1.3922859830667922
42
+ NVIDIA,GeForce GTX 1060 Mobile,GP106,GeForce 10 Mobile,Pascal,1405.0,1671.0,16.0,4400.0,2016-08-15,2002.0,6.0,192.0,192.2,GDDR5,1280,10,0,1.5,80.0,66.84,4277.999999999999,https://www.techpowerup.com/gpu-specs/geforce-gtx-1060-mobile.c3016,2016,,unknown,1.1893238434163702
43
+ NVIDIA,GeForce GTX 1070,GP104,GeForce 10,Pascal,1506.0,1683.0,16.0,7200.0,2016-06-10,2002.0,8.0,256.0,256.3,GDDR5,1920,15,0,2.0,150.0,101.0,6463.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-1070.c2840,2016,,unknown,1.1175298804780875
44
+ NVIDIA,GeForce GTX 1070 GDDR5X,GP104,GeForce 10,Pascal,1506.0,1683.0,16.0,7200.0,2018-12-04,1001.0,8.0,256.0,256.3,GDDR5X,1920,15,0,2.0,150.0,101.0,6463.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-1070-gddr5x.c3343,2018,,unknown,1.1175298804780875
45
+ NVIDIA,GeForce GTX 1070 Max-Q,GP104B,GeForce 10 Mobile,Pascal,1215.0,1379.0,16.0,7200.0,2017-06-27,2002.0,8.0,256.0,256.3,GDDR5,2048,16,0,2.0,115.0,88.26,5648.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-1070-max-q.c2974,2017,,unknown,1.1349794238683129
46
+ NVIDIA,GeForce GTX 1070 Mobile,GP104B,GeForce 10 Mobile,Pascal,1443.0,1645.0,16.0,7200.0,2016-08-15,2002.0,8.0,256.0,256.3,GDDR5,2048,16,0,2.0,120.0,105.3,6738.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-1070-mobile.c2869,2016,,unknown,1.13998613998614
47
+ NVIDIA,GeForce GTX 1070 Ti,GP104,GeForce 10,Pascal,1607.0,1683.0,16.0,7200.0,2017-11-02,2002.0,8.0,256.0,256.3,GDDR5,2432,19,0,2.0,180.0,127.9,8186.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-1070-ti.c3010,2017,,unknown,1.047293092719353
48
+ NVIDIA,GeForce GTX 1080,GP104,GeForce 10,Pascal,1607.0,1733.0,16.0,7200.0,2016-05-27,1251.0,8.0,256.0,320.3,GDDR5X,2560,20,0,2.0,180.0,138.6,8873.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-1080.c2839,2016,,unknown,1.0784069695084006
49
+ NVIDIA,GeForce GTX 1080 11Gbps,GP104,GeForce 10,Pascal,1607.0,1733.0,16.0,7200.0,2017-04-20,1376.0,8.0,256.0,352.3,GDDR5X,2560,20,0,2.0,180.0,138.6,8873.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-1080-11gbps.c2951,2017,,unknown,1.0784069695084006
50
+ NVIDIA,GeForce GTX 1080 Max-Q,GP104B,GeForce 10 Mobile,Pascal,1277.0,1366.0,16.0,7200.0,2017-06-27,1251.0,8.0,256.0,320.3,GDDR5X,2560,20,0,2.0,150.0,109.3,6994.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-1080-max-q.c2975,2017,,unknown,1.0696945967110416
51
+ NVIDIA,GeForce GTX 1080 Mobile,GP104B,GeForce 10 Mobile,Pascal,1557.0,1734.0,16.0,7200.0,2016-08-15,1251.0,8.0,256.0,320.3,GDDR5X,2560,20,0,2.0,150.0,138.7,8878.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-1080-mobile.c2870,2016,,unknown,1.1136801541425818
52
+ NVIDIA,GeForce GTX 1080 Ti,GP102,GeForce 10,Pascal,1481.0,1582.0,16.0,11800.0,2017-03-10,1376.0,11.0,352.0,484.4,GDDR5X,3584,28,0,2.75,250.0,177.2,11340.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-1080-ti.c2877,2017,,unknown,1.0681971640783254
53
+ NVIDIA,GeForce GTX 1660,TU116,GeForce 16,Turing,1530.0,1785.0,12.0,6600.0,2019-03-14,2001.0,6.0,192.0,192.1,GDDR5,1408,22,0,1.5,120.0,10050.0,5027.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-1660.c3365,2019,,unknown,1.1666666666666667
54
+ NVIDIA,GeForce GTX 1660 SUPER,TU116,GeForce 16,Turing,1530.0,1785.0,12.0,6600.0,2019-10-29,1750.0,6.0,192.0,336.0,GDDR6,1408,22,0,1.5,125.0,10050.0,5027.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-1660-super.c3458,2019,,unknown,1.1666666666666667
55
+ NVIDIA,GeForce GTX 1660 Ti,TU116,GeForce 16,Turing,1500.0,1770.0,12.0,6600.0,2019-02-22,1500.0,6.0,192.0,288.0,GDDR6,1536,24,0,1.5,120.0,10870.0,5437.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-1660-ti.c3364,2019,,unknown,1.18
56
+ NVIDIA,GeForce GTX 780 6 GB,GK110B,GeForce 700,Kepler,863.0,902.0,28.0,7080.0,2013-09-10,1502.0,6.0,384.0,288.4,GDDR5,2304,0,0,1.5,250.0,,4155.999999999999,https://www.techpowerup.com/gpu-specs/geforce-gtx-780-6-gb.c2707,2013,,unknown,1.045191193511008
57
+ NVIDIA,GeForce GTX 880M,GK104,GeForce 800M,Kepler,954.0,993.0,28.0,3540.0,2014-03-12,1250.0,8.0,256.0,160.0,GDDR5,1536,0,0,0.5,122.0,,3050.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-880m.c2534,2014,,unknown,1.0408805031446542
58
+ NVIDIA,GeForce GTX 980 Ti,GM200,GeForce 900,Maxwell 2.0,1000.0,1076.0,28.0,8000.0,2015-06-02,1753.0,6.0,384.0,336.6,GDDR5,2816,22,0,3.0,250.0,,6060.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-980-ti.c2724,2015,,unknown,1.076
59
+ NVIDIA,GeForce GTX 980MX,GM204,GeForce 900M,Maxwell 2.0,1050.0,1178.0,28.0,5200.0,2016-06-01,1500.0,8.0,256.0,192.0,GDDR5,1664,13,0,2.0,148.0,,3920.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-980mx.c2808,2016,,unknown,1.121904761904762
60
+ NVIDIA,GeForce GTX TITAN,GK110,GeForce 700,Kepler,836.0,876.0,28.0,7080.0,2013-02-19,1502.0,6.0,384.0,288.4,GDDR5,2688,0,0,1.5,250.0,,4709.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-titan.c1996,2013,,unknown,1.0478468899521531
61
+ NVIDIA,GeForce GTX TITAN BLACK,GK110B,GeForce 700,Kepler,889.0,980.0,28.0,7080.0,2014-02-18,1750.0,6.0,384.0,336.0,GDDR5,2880,0,0,1.5,250.0,,5645.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-titan-black.c2549,2014,,unknown,1.1023622047244095
62
+ NVIDIA,GeForce GTX TITAN X,GM200,GeForce 900,Maxwell 2.0,1000.0,1089.0,28.0,8000.0,2015-03-17,1753.0,12.0,384.0,336.6,GDDR5,3072,24,0,3.0,250.0,,6691.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-titan-x.c2632,2015,,unknown,1.089
63
+ NVIDIA,GeForce GTX TITAN Z,GK110B,GeForce 700,Kepler,705.0,876.0,28.0,7080.0,2014-05-28,1750.0,6.0,384.0,336.0,GDDR5,2880,0,0,1.5,375.0,,5046.0,https://www.techpowerup.com/gpu-specs/geforce-gtx-titan-z.c2575,2014,,unknown,1.2425531914893617
64
+ NVIDIA,GeForce RTX 2060,TU106,GeForce 20,Turing,1365.0,1680.0,12.0,10800.0,2019-01-07,1750.0,6.0,192.0,336.0,GDDR6,1920,30,240,3.0,160.0,12900.0,6451.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2060.c3310,2019,51.6,estimated,1.2307692307692308
65
+ NVIDIA,GeForce RTX 2060 12 GB,TU106,GeForce 20,Turing,1470.0,1650.0,12.0,10800.0,2021-12-07,1750.0,12.0,192.0,336.0,GDDR6,2176,34,272,3.0,184.0,14360.0,7181.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2060-12-gb.c3836,2021,57.4,estimated,1.1224489795918366
66
+ NVIDIA,GeForce RTX 2060 Max-Q,TU106,GeForce 20 Mobile,Turing,975.0,1185.0,12.0,10800.0,2020-01-29,1375.0,6.0,192.0,264.0,GDDR6,1920,30,240,3.0,65.0,9101.0,4550.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2060-max-q.c3533,2020,36.4,estimated,1.2153846153846153
67
+ NVIDIA,GeForce RTX 2060 Max-Q Refresh,TU106B,GeForce 20 Mobile,Turing,960.0,1200.0,12.0,10800.0,2019-01-29,1353.0,6.0,192.0,259.8,GDDR6,1920,30,240,3.0,115.0,9216.0,4608.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2060-max-q-refresh.c3628,2019,36.9,estimated,1.25
68
+ NVIDIA,GeForce RTX 2060 Mobile,TU106,GeForce 20 Mobile,Turing,960.0,1200.0,12.0,10800.0,2019-01-29,1750.0,6.0,192.0,336.0,GDDR6,1920,30,240,3.0,115.0,9216.0,4608.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2060-mobile.c3348,2019,36.9,estimated,1.25
69
+ NVIDIA,GeForce RTX 2060 Mobile Refresh,TU106B,GeForce 20 Mobile,Turing,1005.0,1560.0,12.0,10800.0,2019-01-29,1375.0,6.0,192.0,264.0,GDDR6,1920,30,240,3.0,65.0,11980.0,5990.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2060-mobile-refresh.c3567,2019,47.9,estimated,1.5522388059701493
70
+ NVIDIA,GeForce RTX 2060 SUPER,TU106,GeForce 20,Turing,1470.0,1650.0,12.0,10800.0,2019-07-09,1750.0,8.0,256.0,448.0,GDDR6,2176,34,272,4.0,175.0,14360.0,7181.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2060-super.c3441,2019,57.4,estimated,1.1224489795918366
71
+ NVIDIA,GeForce RTX 2060 SUPER Mobile,TU106,GeForce 20 Mobile,Turing,1470.0,1530.0,12.0,10800.0,2019-07-09,1750.0,8.0,256.0,448.0,GDDR6,2176,34,272,4.0,175.0,13320.0,6659.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2060-super-mobile.c4150,2019,53.3,estimated,1.0408163265306123
72
+ NVIDIA,GeForce RTX 2060 TU104,TU104,GeForce 20,Turing,1365.0,1680.0,12.0,13600.0,2020-01-10,1750.0,6.0,192.0,336.0,GDDR6,1920,30,240,3.0,160.0,12900.0,6451.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2060-tu104.c3495,2020,51.6,estimated,1.2307692307692308
73
+ NVIDIA,GeForce RTX 2070,TU106,GeForce 20,Turing,1410.0,1620.0,12.0,10800.0,2018-10-17,1750.0,8.0,256.0,448.0,GDDR6,2304,36,288,4.0,175.0,14930.0,7465.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2070.c3252,2018,59.7,estimated,1.148936170212766
74
+ NVIDIA,GeForce RTX 2070 Max-Q,TU106,GeForce 20 Mobile,Turing,885.0,1185.0,12.0,10800.0,2019-01-29,1500.0,8.0,256.0,384.0,GDDR6,2304,36,288,4.0,90.0,10920.0,5460.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2070-max-q.c3392,2019,43.7,estimated,1.3389830508474576
75
+ NVIDIA,GeForce RTX 2070 Max-Q Refresh,TU106B,GeForce 20 Mobile,Turing,900.0,1125.0,12.0,10800.0,2020-03-04,1375.0,8.0,256.0,352.0,GDDR6,2304,36,288,4.0,115.0,10370.0,5184.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2070-max-q-refresh.c3574,2020,41.5,estimated,1.25
76
+ NVIDIA,GeForce RTX 2070 Mobile,TU106,GeForce 20 Mobile,Turing,1215.0,1440.0,12.0,10800.0,2019-01-29,1750.0,8.0,256.0,448.0,GDDR6,2304,36,288,4.0,115.0,13270.0,6636.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2070-mobile.c3349,2019,53.1,estimated,1.1851851851851851
77
+ NVIDIA,GeForce RTX 2070 Mobile Refresh,TU106B,GeForce 20 Mobile,Turing,1260.0,1455.0,12.0,10800.0,2020-03-04,1375.0,8.0,256.0,352.0,GDDR6,2304,36,288,4.0,115.0,13410.0,6705.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2070-mobile-refresh.c3573,2020,53.6,estimated,1.1547619047619047
78
+ NVIDIA,GeForce RTX 2070 SUPER,TU104,GeForce 20,Turing,1605.0,1770.0,12.0,13600.0,2019-07-09,1750.0,8.0,256.0,448.0,GDDR6,2560,40,320,4.0,215.0,18120.0,9062.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2070-super.c3440,2019,72.5,estimated,1.102803738317757
79
+ NVIDIA,GeForce RTX 2070 SUPER Max-Q,TU104,GeForce 20 Mobile,Turing,930.0,1155.0,12.0,13600.0,2020-04-02,1375.0,8.0,256.0,352.0,GDDR6,2560,40,320,4.0,80.0,11830.0,5914.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2070-super-max-q.c3563,2020,47.3,estimated,1.2419354838709677
80
+ NVIDIA,GeForce RTX 2070 SUPER Mobile,TU104,GeForce 20 Mobile,Turing,1140.0,1380.0,12.0,13600.0,2020-04-02,1750.0,8.0,256.0,448.0,GDDR6,2560,40,320,4.0,115.0,14130.0,7066.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2070-super-mobile.c3514,2020,56.5,estimated,1.2105263157894737
81
+ NVIDIA,GeForce RTX 2080,TU104,GeForce 20,Turing,1515.0,1710.0,12.0,13600.0,2018-09-20,1750.0,8.0,256.0,448.0,GDDR6,2944,46,368,4.0,215.0,20140.0,10070.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2080.c3224,2018,80.5,estimated,1.1287128712871286
82
+ NVIDIA,GeForce RTX 2080 Max-Q,TU104,GeForce 20 Mobile,Turing,735.0,1095.0,12.0,13600.0,2019-01-29,1500.0,8.0,256.0,384.0,GDDR6,2944,46,368,4.0,80.0,12890.0,6447.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2080-max-q.c3363,2019,51.6,estimated,1.489795918367347
83
+ NVIDIA,GeForce RTX 2080 Mobile,TU104,GeForce 20 Mobile,Turing,1380.0,1590.0,12.0,13600.0,2019-01-29,1750.0,8.0,256.0,448.0,GDDR6,2944,46,368,4.0,150.0,18720.0,9362.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2080-mobile.c3312,2019,74.9,estimated,1.1521739130434783
84
+ NVIDIA,GeForce RTX 2080 SUPER,TU104,GeForce 20,Turing,1650.0,1815.0,12.0,13600.0,2019-07-23,1937.0,8.0,256.0,495.9,GDDR6,3072,48,384,4.0,250.0,22300.0,11150.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2080-super.c3439,2019,89.2,estimated,1.1
85
+ NVIDIA,GeForce RTX 2080 SUPER Max-Q,TU104,GeForce 20 Mobile,Turing,735.0,975.0,12.0,13600.0,2020-04-02,1375.0,8.0,256.0,352.0,GDDR6,3072,48,384,4.0,80.0,11980.0,5990.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2080-super-max-q.c3566,2020,47.9,estimated,1.3265306122448979
86
+ NVIDIA,GeForce RTX 2080 SUPER Mobile,TU104,GeForce 20 Mobile,Turing,1365.0,1560.0,12.0,13600.0,2020-04-02,1750.0,8.0,256.0,448.0,GDDR6,3072,48,384,4.0,150.0,19170.0,9585.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2080-super-mobile.c3513,2020,76.7,estimated,1.1428571428571428
87
+ NVIDIA,GeForce RTX 2080 Ti,TU102,GeForce 20,Turing,1350.0,1545.0,12.0,18600.0,2018-09-20,1750.0,11.0,352.0,616.0,GDDR6,4352,68,544,5.5,250.0,26900.0,13450.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-2080-ti.c3305,2018,107.6,estimated,1.1444444444444444
88
+ NVIDIA,GeForce RTX 3050 6 GB,GA107,GeForce 30,Ampere,1042.0,1470.0,8.0,8700.0,2024-02-02,1750.0,6.0,96.0,168.0,GDDR6,2304,18,72,2.0,70.0,6774.0,6774.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3050-6-gb.c4188,2024,27.1,estimated,1.4107485604606527
89
+ NVIDIA,GeForce RTX 3050 8 GB,GA106,GeForce 30,Ampere,1552.0,1777.0,8.0,12000.0,2022-01-04,1750.0,8.0,128.0,224.0,GDDR6,2560,20,80,2.0,130.0,9098.0,9098.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3050-8-gb.c3858,2022,36.4,estimated,1.1449742268041236
90
+ NVIDIA,GeForce RTX 3050 8 GB GA107,GA107,GeForce 30,Ampere,1552.0,1777.0,8.0,8700.0,2022-12-16,1750.0,8.0,128.0,224.0,GDDR6,2560,20,80,2.0,115.0,9098.0,9098.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3050-8-gb-ga107.c3880,2022,36.4,estimated,1.1449742268041236
91
+ NVIDIA,GeForce RTX 3050 Max-Q Refresh 6 GB,GA107,GeForce 30 Mobile,Ampere,622.0,990.0,8.0,8700.0,2022-07-06,1375.0,6.0,96.0,132.0,GDDR6,2560,20,80,2.0,35.0,5069.0,5069.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3050-max-q-refresh-6-gb.c3970,2022,20.3,estimated,1.5916398713826367
92
+ NVIDIA,GeForce RTX 3050 Mobile Refresh 6 GB,GA107,GeForce 30 Mobile,Ampere,1237.0,1492.0,8.0,8700.0,2022-07-06,1750.0,6.0,96.0,168.0,GDDR6,2560,20,80,2.0,75.0,7639.0,7639.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3050-mobile-refresh-6-gb.c3971,2022,30.6,estimated,1.206143896523848
93
+ NVIDIA,GeForce RTX 3050 OEM,GA106,GeForce 30,Ampere,1515.0,1755.0,8.0,12000.0,2022-01-04,1750.0,8.0,128.0,224.0,GDDR6,2304,18,72,2.0,130.0,8087.0,8087.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3050-oem.c3915,2022,32.3,estimated,1.1584158415841583
94
+ NVIDIA,GeForce RTX 3060 12 GB,GA106,GeForce 30,Ampere,1320.0,1777.0,8.0,12000.0,2021-01-12,1875.0,12.0,192.0,360.0,GDDR6,3584,28,112,3.0,170.0,12740.0,12740.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3060-12-gb.c3682,2021,51.0,estimated,1.3462121212121212
95
+ NVIDIA,GeForce RTX 3060 12 GB GA104,GA104,GeForce 30,Ampere,1320.0,1777.0,8.0,17400.0,2021-09-01,1875.0,12.0,192.0,360.0,GDDR6,3584,28,112,3.0,170.0,12740.0,12740.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3060-12-gb-ga104.c3832,2021,51.0,estimated,1.3462121212121212
96
+ NVIDIA,GeForce RTX 3060 3840SP,GA106,GeForce 30,Ampere,1627.0,1852.0,8.0,12000.0,2021-01-01,1750.0,6.0,192.0,336.0,GDDR6,3840,30,120,3.0,185.0,14220.0,14220.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3060-3840sp.c4080,2021,56.9,estimated,1.1382913337430853
97
+ NVIDIA,GeForce RTX 3060 8 GB,GA106,GeForce 30,Ampere,1320.0,1777.0,8.0,12000.0,2022-10-12,1875.0,8.0,128.0,240.0,GDDR6,3584,28,112,3.0,170.0,12740.0,12740.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3060-8-gb.c3937,2022,51.0,estimated,1.3462121212121212
98
+ NVIDIA,GeForce RTX 3060 8 GB GA104,GA104,GeForce 30,Ampere,1320.0,1777.0,8.0,17400.0,2022-10-01,1875.0,8.0,128.0,240.0,GDDR6,3584,28,112,3.0,195.0,12740.0,12740.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3060-8-gb-ga104.c4132,2022,51.0,estimated,1.3462121212121212
99
+ NVIDIA,GeForce RTX 3060 Max-Q,GA106,GeForce 30 Mobile,Ampere,817.0,1282.0,8.0,12000.0,2021-01-12,1500.0,6.0,192.0,288.0,GDDR6,3840,30,120,3.0,60.0,9846.0,9846.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3060-max-q.c3752,2021,39.4,estimated,1.569155446756426
100
+ NVIDIA,GeForce RTX 3060 Mobile,GA106,GeForce 30 Mobile,Ampere,900.0,1425.0,8.0,12000.0,2021-01-12,1750.0,6.0,192.0,336.0,GDDR6,3840,30,120,3.0,80.0,10940.0,10940.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3060-mobile.c3757,2021,43.8,estimated,1.5833333333333333
101
+ NVIDIA,GeForce RTX 3060 Ti,GA104,GeForce 30,Ampere,1410.0,1665.0,8.0,17400.0,2020-12-01,1750.0,8.0,256.0,448.0,GDDR6,4864,38,152,4.0,200.0,16200.0,16200.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3060-ti.c3681,2020,64.8,estimated,1.1808510638297873
102
+ NVIDIA,GeForce RTX 3060 Ti GA103,GA103,GeForce 30,Ampere,1410.0,1665.0,8.0,22000.0,2022-02-23,1750.0,8.0,256.0,448.0,GDDR6,4864,38,152,4.0,200.0,16200.0,16200.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3060-ti-ga103.c3872,2022,64.8,estimated,1.1808510638297873
103
+ NVIDIA,GeForce RTX 3060 Ti GDDR6X,GA104,GeForce 30,Ampere,1410.0,1665.0,8.0,17400.0,2022-10-19,1188.0,8.0,256.0,608.3,GDDR6X,4864,38,152,4.0,225.0,16200.0,16200.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3060-ti-gddr6x.c3935,2022,64.8,estimated,1.1808510638297873
104
+ NVIDIA,GeForce RTX 3070,GA104,GeForce 30,Ampere,1500.0,1725.0,8.0,17400.0,2020-09-01,1750.0,8.0,256.0,448.0,GDDR6,5888,46,184,4.0,220.0,20310.0,20310.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3070.c3674,2020,81.3,estimated,1.15
105
+ NVIDIA,GeForce RTX 3070 Max-Q,GA104,GeForce 30 Mobile,Ampere,780.0,1290.0,8.0,17400.0,2021-01-12,1500.0,8.0,256.0,384.0,GDDR6,5120,40,160,4.0,80.0,13210.0,13210.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3070-max-q.c3685,2021,52.8,estimated,1.6538461538461537
106
+ NVIDIA,GeForce RTX 3070 Mobile,GA104,GeForce 30 Mobile,Ampere,1110.0,1560.0,8.0,17400.0,2021-01-12,1750.0,8.0,256.0,448.0,GDDR6,5120,40,160,4.0,115.0,15970.0,15970.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3070-mobile.c3712,2021,63.9,estimated,1.4054054054054055
107
+ NVIDIA,GeForce RTX 3070 Ti,GA104,GeForce 30,Ampere,1575.0,1770.0,8.0,17400.0,2021-05-31,1188.0,8.0,256.0,608.3,GDDR6X,6144,48,192,4.0,290.0,21750.0,21750.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3070-ti.c3675,2021,87.0,estimated,1.1238095238095238
108
+ NVIDIA,GeForce RTX 3070 Ti 8 GB GA102,GA102,GeForce 30,Ampere,1575.0,1770.0,8.0,28300.0,2022-10-21,1188.0,8.0,256.0,608.3,GDDR6X,6144,48,192,4.0,290.0,21750.0,21750.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3070-ti-8-gb-ga102.c3936,2022,87.0,estimated,1.1238095238095238
109
+ NVIDIA,GeForce RTX 3070 Ti Max-Q,GA104,GeForce 30 Mobile,Ampere,510.0,1035.0,8.0,17400.0,2022-01-04,1500.0,8.0,256.0,384.0,GDDR6,5888,46,184,4.0,80.0,12190.0,12190.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3070-ti-max-q.c3923,2022,48.8,estimated,2.0294117647058822
110
+ NVIDIA,GeForce RTX 3070 Ti Mobile,GA104,GeForce 30 Mobile,Ampere,915.0,1410.0,8.0,17400.0,2022-01-04,1750.0,8.0,256.0,448.0,GDDR6,5888,46,184,4.0,115.0,16600.000000000004,16600.000000000004,https://www.techpowerup.com/gpu-specs/geforce-rtx-3070-ti-mobile.c3852,2022,66.4,estimated,1.540983606557377
111
+ NVIDIA,GeForce RTX 3070 TiM,GA104,GeForce 30,Ampere,915.0,1410.0,8.0,17400.0,2022-11-01,1750.0,8.0,256.0,448.0,GDDR6,5888,46,184,4.0,220.0,16600.000000000004,16600.000000000004,https://www.techpowerup.com/gpu-specs/geforce-rtx-3070-tim.c3951,2022,66.4,estimated,1.540983606557377
112
+ NVIDIA,GeForce RTX 3080,GA102,GeForce 30,Ampere,1440.0,1710.0,8.0,28300.0,2020-09-01,1188.0,10.0,320.0,760.3,GDDR6X,8704,68,272,5.0,320.0,29770.0,29770.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3080.c3621,2020,119.1,estimated,1.1875
113
+ NVIDIA,GeForce RTX 3080 12 GB,GA102,GeForce 30,Ampere,1260.0,1710.0,8.0,28300.0,2022-01-11,1188.0,12.0,384.0,912.4,GDDR6X,8960,70,280,6.0,350.0,30640.0,30640.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3080-12-gb.c3834,2022,122.6,estimated,1.3571428571428572
114
+ NVIDIA,GeForce RTX 3080 Max-Q,GA104,GeForce 30 Mobile,Ampere,780.0,1245.0,8.0,17400.0,2021-01-12,1500.0,8.0,256.0,384.0,GDDR6,6144,48,192,4.0,80.0,15300.0,15300.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3080-max-q.c3753,2021,61.2,estimated,1.5961538461538463
115
+ NVIDIA,GeForce RTX 3080 Mobile,GA104,GeForce 30 Mobile,Ampere,1110.0,1545.0,8.0,17400.0,2021-01-12,1750.0,8.0,256.0,448.0,GDDR6,6144,48,192,4.0,115.0,18980.0,18980.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3080-mobile.c3684,2021,75.9,estimated,1.3918918918918919
116
+ NVIDIA,GeForce RTX 3080 Ti,GA102,GeForce 30,Ampere,1365.0,1665.0,8.0,28300.0,2021-05-31,1188.0,12.0,384.0,912.4,GDDR6X,10240,80,320,6.0,350.0,34100.0,34100.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3080-ti.c3735,2021,136.4,estimated,1.2197802197802199
117
+ NVIDIA,GeForce RTX 3080 Ti 20 GB,GA102,GeForce 30,Ampere,1335.0,1665.0,8.0,28300.0,2022-01-01,1188.0,20.0,320.0,760.3,GDDR6X,10240,80,320,6.0,350.0,34100.0,34100.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3080-ti-20-gb.c3831,2022,136.4,estimated,1.247191011235955
118
+ NVIDIA,GeForce RTX 3080 Ti Max-Q,GA103,GeForce 30 Mobile,Ampere,585.0,1125.0,8.0,22000.0,2022-01-25,1500.0,16.0,256.0,384.0,GDDR6,7424,58,232,4.0,80.0,16700.0,16700.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3080-ti-max-q.c3841,2022,66.8,estimated,1.9230769230769231
119
+ NVIDIA,GeForce RTX 3080 Ti Mobile,GA103,GeForce 30 Mobile,Ampere,810.0,1260.0,8.0,22000.0,2022-01-25,2000.0,16.0,256.0,512.0,GDDR6,7424,58,232,4.0,115.0,18710.0,18710.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3080-ti-mobile.c3840,2022,74.8,estimated,1.5555555555555556
120
+ NVIDIA,GeForce RTX 3090,GA102,GeForce 30,Ampere,1395.0,1695.0,8.0,28300.0,2020-09-01,1219.0,24.0,384.0,936.2,GDDR6X,10496,82,328,6.0,350.0,35580.0,35580.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3090.c3622,2020,142.3,estimated,1.2150537634408602
121
+ NVIDIA,GeForce RTX 3090 Ti,GA102,GeForce 30,Ampere,1560.0,1860.0,8.0,28300.0,2022-01-27,1313.0,24.0,384.0,1010.0,GDDR6X,10752,84,336,6.0,450.0,40000.0,40000.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-3090-ti.c3829,2022,160.0,estimated,1.1923076923076923
122
+ NVIDIA,GeForce RTX 4050 Max-Q,AD107,GeForce 40 Mobile,Ada Lovelace,1140.0,1605.0,5.0,18900.0,2023-01-03,2000.0,6.0,96.0,192.0,GDDR6,2560,20,80,12.0,35.0,8218.0,8218.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4050-max-q.c3987,2023,32.9,estimated,1.4078947368421053
123
+ NVIDIA,GeForce RTX 4050 Mobile,AD107,GeForce 40 Mobile,Ada Lovelace,1455.0,1755.0,5.0,18900.0,2023-01-03,2000.0,6.0,96.0,192.0,GDDR6,2560,20,80,12.0,50.0,8986.0,8986.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4050-mobile.c3953,2023,35.9,estimated,1.2061855670103092
124
+ NVIDIA,GeForce RTX 4060,AD107,GeForce 40,Ada Lovelace,1830.0,2460.0,5.0,18900.0,2023-05-18,2125.0,8.0,128.0,272.0,GDDR6,3072,24,96,24.0,115.0,15110.0,15110.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4060.c4107,2023,60.5,estimated,1.3442622950819672
125
+ NVIDIA,GeForce RTX 4060 AD106,AD106,GeForce 40,Ada Lovelace,1830.0,2460.0,5.0,22900.0,2024-04-01,2125.0,8.0,128.0,272.0,GDDR6,3072,24,96,24.0,115.0,15110.0,15110.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4060-ad106.c3891,2024,60.5,estimated,1.3442622950819672
126
+ NVIDIA,GeForce RTX 4060 Max-Q,AD107,GeForce 40 Mobile,Ada Lovelace,1140.0,1470.0,5.0,18900.0,2023-01-03,2000.0,8.0,128.0,256.0,GDDR6,3072,24,96,32.0,35.0,9032.0,9032.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4060-max-q.c3986,2023,36.1,estimated,1.2894736842105263
127
+ NVIDIA,GeForce RTX 4060 Mobile,AD107,GeForce 40 Mobile,Ada Lovelace,1545.0,1890.0,5.0,18900.0,2023-01-03,2000.0,8.0,128.0,256.0,GDDR6,3072,24,96,32.0,115.0,11610.0,11610.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4060-mobile.c3946,2023,46.4,estimated,1.2233009708737863
128
+ NVIDIA,GeForce RTX 4060 Ti 16 GB,AD106,GeForce 40,Ada Lovelace,2310.0,2535.0,5.0,22900.0,2023-05-18,2250.0,16.0,128.0,288.0,GDDR6,4352,34,136,32.0,165.0,22060.0,22060.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4060-ti-16-gb.c4155,2023,88.3,estimated,1.0974025974025974
129
+ NVIDIA,GeForce RTX 4060 Ti 8 GB,AD106,GeForce 40,Ada Lovelace,2310.0,2535.0,5.0,22900.0,2023-05-18,2250.0,8.0,128.0,288.0,GDDR6,4352,34,136,32.0,160.0,22060.0,22060.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4060-ti-8-gb.c3890,2023,88.3,estimated,1.0974025974025974
130
+ NVIDIA,GeForce RTX 4060 Ti AD104,AD104,GeForce 40,Ada Lovelace,2310.0,2535.0,5.0,35800.0,2024-04-01,2250.0,8.0,128.0,288.0,GDDR6,4352,34,136,32.0,160.0,22060.0,22060.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4060-ti-ad104.c4204,2024,88.3,estimated,1.0974025974025974
131
+ NVIDIA,GeForce RTX 4070,AD104,GeForce 40,Ada Lovelace,1920.0,2475.0,5.0,35800.0,2023-04-12,1313.0,12.0,192.0,504.2,GDDR6X,5888,46,184,36.0,200.0,29150.0,29150.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4070.c3924,2023,116.6,estimated,1.2890625
132
+ NVIDIA,GeForce RTX 4070 AD103,AD103,GeForce 40,Ada Lovelace,1920.0,2475.0,5.0,45900.0,2024-03-01,1313.0,12.0,192.0,504.2,GDDR6X,5888,46,184,36.0,200.0,29150.0,29150.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4070-ad103.c4205,2024,116.6,estimated,1.2890625
133
+ NVIDIA,GeForce RTX 4070 GDDR6,AD104,GeForce 40,Ada Lovelace,1920.0,2475.0,5.0,35800.0,2024-08-20,2500.0,12.0,192.0,480.0,GDDR6,5888,46,184,36.0,200.0,29150.0,29150.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4070-gddr6.c4228,2024,116.6,estimated,1.2890625
134
+ NVIDIA,GeForce RTX 4070 Max-Q,AD106,GeForce 40 Mobile,Ada Lovelace,735.0,1230.0,5.0,22900.0,2023-01-03,2000.0,8.0,128.0,256.0,GDDR6,4608,36,144,32.0,35.0,11340.0,11340.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4070-max-q.c3954,2023,45.3,estimated,1.6734693877551021
135
+ NVIDIA,GeForce RTX 4070 Mobile,AD106,GeForce 40 Mobile,Ada Lovelace,1395.0,1695.0,5.0,22900.0,2023-01-03,2000.0,8.0,128.0,256.0,GDDR6,4608,36,144,32.0,115.0,15620.0,15620.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4070-mobile.c3944,2023,62.5,estimated,1.2150537634408602
136
+ NVIDIA,GeForce RTX 4070 SUPER,AD104,GeForce 40,Ada Lovelace,1980.0,2475.0,5.0,35800.0,2024-01-08,1313.0,12.0,192.0,504.2,GDDR6X,7168,56,224,48.0,220.0,35480.0,35480.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4070-super.c4186,2024,141.9,estimated,1.25
137
+ NVIDIA,GeForce RTX 4070 Ti,AD104,GeForce 40,Ada Lovelace,2310.0,2610.0,5.0,35800.0,2023-01-03,1313.0,12.0,192.0,504.2,GDDR6X,7680,60,240,48.0,285.0,40090.0,40090.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4070-ti.c3950,2023,160.4,estimated,1.12987012987013
138
+ NVIDIA,GeForce RTX 4070 Ti SUPER,AD103,GeForce 40,Ada Lovelace,2340.0,2610.0,5.0,45900.0,2024-01-08,1313.0,16.0,256.0,672.3,GDDR6X,8448,66,264,48.0,285.0,44100.0,44100.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4070-ti-super.c4187,2024,176.4,estimated,1.1153846153846154
139
+ NVIDIA,GeForce RTX 4070 Ti SUPER AD102,AD102,GeForce 40,Ada Lovelace,2340.0,2610.0,5.0,76300.0,2024-06-10,1313.0,16.0,256.0,672.3,GDDR6X,8448,66,264,48.0,285.0,44100.0,44100.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4070-ti-super-ad102.c4215,2024,176.4,estimated,1.1153846153846154
140
+ NVIDIA,GeForce RTX 4080,AD103,GeForce 40,Ada Lovelace,2205.0,2505.0,5.0,45900.0,2022-09-20,1400.0,16.0,256.0,716.8,GDDR6X,9728,76,304,64.0,320.0,48740.0,48740.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4080.c3888,2022,194.9,estimated,1.1360544217687074
141
+ NVIDIA,GeForce RTX 4080 Max-Q,AD104,GeForce 40 Mobile,Ada Lovelace,795.0,1350.0,5.0,35800.0,2023-01-03,2250.0,12.0,192.0,432.0,GDDR6,7424,58,232,48.0,60.0,20040.0,20040.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4080-max-q.c3948,2023,80.2,estimated,1.6981132075471699
142
+ NVIDIA,GeForce RTX 4080 Mobile,AD104,GeForce 40 Mobile,Ada Lovelace,1290.0,1665.0,5.0,35800.0,2023-01-03,2250.0,12.0,192.0,432.0,GDDR6,7424,58,232,48.0,110.0,24720.0,24720.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4080-mobile.c3947,2023,98.9,estimated,1.2906976744186047
143
+ NVIDIA,GeForce RTX 4080 SUPER,AD103,GeForce 40,Ada Lovelace,2295.0,2550.0,5.0,45900.0,2024-01-08,1438.0,16.0,256.0,736.3,GDDR6X,10240,80,320,64.0,320.0,52220.0,52220.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4080-super.c4182,2024,208.9,estimated,1.1111111111111112
144
+ NVIDIA,GeForce RTX 4090,AD102,GeForce 40,Ada Lovelace,2235.0,2520.0,5.0,76300.0,2022-09-20,1313.0,24.0,384.0,1010.0,GDDR6X,16384,128,512,72.0,450.0,82580.0,82580.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4090.c3889,2022,330.3,estimated,1.1275167785234899
145
+ NVIDIA,GeForce RTX 4090 D,AD102,GeForce 40,Ada Lovelace,2280.0,2520.0,5.0,76300.0,2023-12-28,1313.0,24.0,384.0,1010.0,GDDR6X,14592,114,456,72.0,425.0,73540.0,73540.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4090-d.c4189,2023,294.2,estimated,1.105263157894737
146
+ NVIDIA,GeForce RTX 4090 Max-Q,AD103,GeForce 40 Mobile,Ada Lovelace,930.0,1455.0,5.0,45900.0,2023-01-03,2250.0,16.0,256.0,576.0,GDDR6,9728,76,304,64.0,80.0,28310.0,28310.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-4090-max-q.c3988,2023,113.2,estimated,1.564516129032258
147
+ NVIDIA,GeForce RTX 4090 Mobile,AD103,GeForce 40 Mobile,Ada Lovelace,1335.0,1695.0,5.0,45900.0,2023-01-03,2250.0,16.0,256.0,576.0,GDDR6,9728,76,304,64.0,120.0,32979.99999999999,32979.99999999999,https://www.techpowerup.com/gpu-specs/geforce-rtx-4090-mobile.c3949,2023,131.9,estimated,1.2696629213483146
148
+ NVIDIA,GeForce RTX 5050,GB207,GeForce 50,Blackwell 2.0,2317.0,2572.0,5.0,16900.0,2025-07-01,2500.0,8.0,128.0,320.0,GDDR6,2560,20,80,24.0,130.0,13170.0,13170.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-5050.c4220,2025,52.7,estimated,1.110056107034959
149
+ NVIDIA,GeForce RTX 5050 Mobile,GB207,GeForce 50 Mobile,Blackwell 2.0,1020.0,1500.0,5.0,16900.0,2025-06-24,1500.0,8.0,128.0,384.0,GDDR7,2560,20,80,32.0,50.0,7680.0,7680.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-5050-mobile.c4239,2025,30.7,estimated,1.4705882352941178
150
+ NVIDIA,GeForce RTX 5060,GB206,GeForce 50,Blackwell 2.0,2280.0,2497.0,5.0,21900.0,2025-05-19,1750.0,8.0,128.0,448.0,GDDR7,3840,30,120,32.0,145.0,19180.0,19180.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-5060.c4219,2025,76.7,estimated,1.0951754385964911
151
+ NVIDIA,GeForce RTX 5060 Mobile,GB206,GeForce 50 Mobile,Blackwell 2.0,952.0,1455.0,5.0,21900.0,2025-05-20,1500.0,8.0,128.0,384.0,GDDR7,3328,26,104,32.0,45.0,9684.0,9684.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-5060-mobile.c4230,2025,38.7,estimated,1.528361344537815
152
+ NVIDIA,GeForce RTX 5060 Ti 16 GB,GB206,GeForce 50,Blackwell 2.0,2407.0,2572.0,5.0,21900.0,2025-04-16,1750.0,16.0,128.0,448.0,GDDR7,4608,36,144,32.0,180.0,23700.0,23700.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-5060-ti-16-gb.c4292,2025,94.8,estimated,1.0685500623182385
153
+ NVIDIA,GeForce RTX 5060 Ti 8 GB,GB206,GeForce 50,Blackwell 2.0,2407.0,2572.0,5.0,21900.0,2025-04-16,1750.0,8.0,128.0,448.0,GDDR7,4608,36,144,32.0,180.0,23700.0,23700.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-5060-ti-8-gb.c4246,2025,94.8,estimated,1.0685500623182385
154
+ NVIDIA,GeForce RTX 5070,GB205,GeForce 50,Blackwell 2.0,2325.0,2512.0,5.0,31100.0,2025-03-04,1750.0,12.0,192.0,672.0,GDDR7,6144,48,192,48.0,250.0,30870.0,30870.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-5070.c4218,2025,123.5,estimated,1.0804301075268816
155
+ NVIDIA,GeForce RTX 5070 Mobile,GB206,GeForce 50 Mobile,Blackwell 2.0,907.0,1425.0,5.0,21900.0,2025-04-01,1500.0,8.0,128.0,384.0,GDDR7,4608,36,144,32.0,50.0,13130.0,13130.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-5070-mobile.c4237,2025,52.5,estimated,1.5711135611907387
156
+ NVIDIA,GeForce RTX 5070 Ti,GB203,GeForce 50,Blackwell 2.0,2295.0,2452.0,5.0,45600.0,2025-02-20,1750.0,16.0,256.0,896.0,GDDR7,8960,70,280,48.0,300.0,43940.0,43940.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-5070-ti.c4243,2025,175.8,estimated,1.068409586056645
157
+ NVIDIA,GeForce RTX 5070 Ti Mobile,GB205,GeForce 50 Mobile,Blackwell 2.0,847.0,1447.0,5.0,31100.0,2025-03-01,1750.0,12.0,192.0,672.0,GDDR7,5888,46,184,48.0,60.0,17040.0,17040.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-5070-ti-mobile.c4238,2025,68.2,estimated,1.7083825265643446
158
+ NVIDIA,GeForce RTX 5080,GB203,GeForce 50,Blackwell 2.0,2295.0,2617.0,5.0,45600.0,2025-01-30,1875.0,16.0,256.0,960.0,GDDR7,10752,84,336,64.0,360.0,56280.0,56280.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-5080.c4217,2025,225.1,estimated,1.1403050108932462
159
+ NVIDIA,GeForce RTX 5080 Mobile,GB203,GeForce 50 Mobile,Blackwell 2.0,975.0,1500.0,5.0,45600.0,2025-04-02,1750.0,16.0,256.0,896.0,GDDR7,7680,60,240,64.0,80.0,23040.0,23040.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-5080-mobile.c4236,2025,92.2,estimated,1.5384615384615385
160
+ NVIDIA,GeForce RTX 5090,GB202,GeForce 50,Blackwell 2.0,2017.0,2407.0,5.0,92200.0,2025-01-30,1750.0,32.0,512.0,1790.0,GDDR7,21760,170,680,96.0,575.0,104800.0,104800.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-5090.c4216,2025,419.0,estimated,1.193356470004958
161
+ NVIDIA,GeForce RTX 5090 D,GB202,GeForce 50,Blackwell 2.0,2017.0,2407.0,5.0,92200.0,2025-01-30,1750.0,32.0,512.0,1790.0,GDDR7,21760,170,680,96.0,575.0,104800.0,104800.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-5090-d.c4253,2025,419.0,estimated,1.193356470004958
162
+ NVIDIA,GeForce RTX 5090 D V2,GB202,GeForce 50,Blackwell 2.0,2017.0,2407.0,5.0,92200.0,2025-08-15,1750.0,24.0,384.0,1340.0,GDDR7,21760,170,680,96.0,575.0,104800.0,104800.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-5090-d-v2.c4310,2025,419.0,estimated,1.193356470004958
163
+ NVIDIA,GeForce RTX 5090 Mobile,GB203,GeForce 50 Mobile,Blackwell 2.0,990.0,1515.0,5.0,45600.0,2025-03-27,1750.0,24.0,256.0,896.0,GDDR7,10496,82,328,64.0,95.0,31800.0,31800.0,https://www.techpowerup.com/gpu-specs/geforce-rtx-5090-mobile.c4235,2025,127.2,estimated,1.5303030303030303
164
+ NVIDIA,H100 CNX,GH100,Server Hopper(Hxx),Hopper,690.0,1845.0,5.0,80000.0,2023-03-21,1593.0,80.0,5120.0,2040.0,HBM2e,14592,114,456,50.0,350.0,215400.0,53840.0,https://www.techpowerup.com/gpu-specs/h100-cnx.c4131,2023,861.5,estimated,2.6739130434782608
165
+ NVIDIA,H100 NVL 94 GB,GH100,Server Hopper(Hxx),Hopper,1080.0,1785.0,5.0,80000.0,2023-03-21,1310.0,94.0,6016.0,3940.0,HBM3,16896,132,528,50.0,400.0,241300.0,60320.0,https://www.techpowerup.com/gpu-specs/h100-nvl-94-gb.c4327,2023,835.0,reported,1.6527777777777777
166
+ NVIDIA,H100 PCIe 80 GB,GH100,Server Hopper(Hxx),Hopper,1095.0,1755.0,5.0,80000.0,2022-10-01,1593.0,80.0,5120.0,2040.0,HBM2e,14592,114,456,50.0,350.0,204900.0,51220.0,https://www.techpowerup.com/gpu-specs/h100-pcie-80-gb.c3899,2022,756.0,reported,1.6027397260273972
167
+ NVIDIA,H100 PCIe 96 GB,GH100,Server Hopper(Hxx),Hopper,1665.0,1837.0,5.0,80000.0,2023-03-21,1313.0,96.0,5120.0,3360.0,HBM3,16896,132,528,50.0,700.0,248300.0,62080.0,https://www.techpowerup.com/gpu-specs/h100-pcie-96-gb.c4164,2023,993.2,estimated,1.1033033033033033
168
+ NVIDIA,H100 SXM5 64 GB,GH100,Server Hopper(Hxx),Hopper,1665.0,1980.0,5.0,80000.0,2023-03-21,1313.0,64.0,3072.0,2020.0,HBM3,16896,132,528,30.0,700.0,267600.00000000006,66910.0,https://www.techpowerup.com/gpu-specs/h100-sxm5-64-gb.c4165,2023,1070.5,estimated,1.1891891891891893
169
+ NVIDIA,H100 SXM5 80 GB,GH100,Server Hopper(Hxx),Hopper,1590.0,1980.0,5.0,80000.0,2022-10-01,1313.0,80.0,5120.0,3360.0,HBM3,16896,132,528,50.0,700.0,267600.00000000006,66910.0,https://www.techpowerup.com/gpu-specs/h100-sxm5-80-gb.c3900,2022,989.0,reported,1.2452830188679245
170
+ NVIDIA,H100 SXM5 94 GB,GH100,Server Hopper(Hxx),Hopper,1350.0,1980.0,5.0,80000.0,2023-03-21,1313.0,94.0,5120.0,3360.0,HBM3,16896,132,528,50.0,700.0,267600.00000000006,66910.0,https://www.techpowerup.com/gpu-specs/h100-sxm5-94-gb.c4294,2023,1070.5,estimated,1.4666666666666666
171
+ NVIDIA,H100 SXM5 96 GB,GH100,Server Hopper(Hxx),Hopper,1350.0,1980.0,5.0,80000.0,2023-03-21,1313.0,96.0,5120.0,3360.0,HBM3,16896,132,528,50.0,700.0,267600.00000000006,66910.0,https://www.techpowerup.com/gpu-specs/h100-sxm5-96-gb.c3974,2023,989.0,reported,1.4666666666666666
172
+ NVIDIA,H200 NVL,GH100,Server Hopper(Hxx),Hopper,1365.0,1785.0,5.0,80000.0,2024-11-18,1593.0,141.0,6144.0,4890.0,HBM3e,16896,132,528,50.0,600.0,241300.0,60320.0,https://www.techpowerup.com/gpu-specs/h200-nvl.c4254,2024,835.0,reported,1.3076923076923077
173
+ NVIDIA,H200 SXM 141 GB,GH100,Server Hopper(Hxx),Hopper,1500.0,1980.0,5.0,80000.0,2024-11-18,1593.0,141.0,6144.0,4890.0,HBM3e,16896,132,528,50.0,700.0,267600.00000000006,66910.0,https://www.techpowerup.com/gpu-specs/h200-sxm-141-gb.c4255,2024,989.0,reported,1.32
174
+ NVIDIA,H800 PCIe 80 GB,GH100,Server Hopper(Hxx),Hopper,1095.0,1755.0,5.0,80000.0,2023-03-21,1593.0,80.0,5120.0,2040.0,HBM2e,14592,114,456,50.0,350.0,204900.0,51220.0,https://www.techpowerup.com/gpu-specs/h800-pcie-80-gb.c4181,2023,819.5,estimated,1.6027397260273972
175
+ NVIDIA,H800 SXM5,GH100,Server Hopper(Hxx),Hopper,1095.0,1755.0,5.0,80000.0,2023-03-21,1313.0,80.0,5120.0,3360.0,HBM3,16896,132,528,50.0,700.0,237200.0,59300.0,https://www.techpowerup.com/gpu-specs/h800-sxm5.c3975,2023,948.9,estimated,1.6027397260273972
176
+ NVIDIA,Jetson AGX Orin 32 GB,GA10B,Tegra(Ampere),Ampere,930.0,930.0,8.0,,2023-02-01,800.0,32.0,256.0,204.8,LPDDR5,1792,14,56,6.0,40.0,6666.0,3333.0,https://www.techpowerup.com/gpu-specs/jetson-agx-orin-32-gb.c4084,2023,13.3,estimated,1.0
177
+ NVIDIA,Jetson AGX Orin 64 GB,GA10B,Tegra(Ampere),Ampere,1300.0,1300.0,8.0,,2023-03-01,800.0,64.0,256.0,204.8,LPDDR5,2048,16,64,6.0,60.0,10650.0,5325.0,https://www.techpowerup.com/gpu-specs/jetson-agx-orin-64-gb.c4085,2023,21.3,estimated,1.0
178
+ NVIDIA,Jetson AGX Xavier 16 GB,GV10B,Tegra(Volta),Volta,854.0,1377.0,12.0,9000.0,2018-10-01,2133.0,16.0,256.0,136.5,LPDDR4X,512,8,64,0.5,30.0,2820.0,1410.0,https://www.techpowerup.com/gpu-specs/jetson-agx-xavier-16-gb.c3232,2018,11.3,estimated,1.6124121779859484
179
+ NVIDIA,Jetson AGX Xavier 32 GB,GV10B,Tegra(Volta),Volta,854.0,1377.0,12.0,9000.0,2018-10-01,2133.0,32.0,256.0,136.5,LPDDR4X,512,8,64,0.5,30.0,2820.0,1410.0,https://www.techpowerup.com/gpu-specs/jetson-agx-xavier-32-gb.c4088,2018,11.3,estimated,1.6124121779859484
180
+ NVIDIA,Jetson Orin NX 16 GB,GA10B,Tegra(Ampere),Ampere,918.0,918.0,8.0,,2023-02-01,800.0,16.0,128.0,102.4,LPDDR5,1024,8,32,4.0,25.0,3760.0,1880.0,https://www.techpowerup.com/gpu-specs/jetson-orin-nx-16-gb.c4086,2023,7.5,estimated,1.0
181
+ NVIDIA,Jetson Orin NX 8 GB,GA10B,Tegra(Ampere),Ampere,765.0,765.0,8.0,,2023-03-01,800.0,8.0,128.0,102.4,LPDDR5,1024,8,32,4.0,20.0,3133.0,1567.0,https://www.techpowerup.com/gpu-specs/jetson-orin-nx-8-gb.c4081,2023,6.3,estimated,1.0
182
+ NVIDIA,Jetson Orin Nano 8 GB,GA10B,Tegra(Ampere),Ampere,625.0,625.0,8.0,,2023-03-01,533.0,8.0,128.0,68.22,LPDDR5,1024,8,32,2.0,15.0,2560.0,1280.0,https://www.techpowerup.com/gpu-specs/jetson-orin-nano-8-gb.c4082,2023,5.1,estimated,1.0
183
+ NVIDIA,Jetson Orin Nano Super,GA10B,Tegra(Ampere),Ampere,1020.0,1020.0,8.0,,2024-12-17,800.0,8.0,128.0,102.4,LPDDR5,1024,8,32,2.0,25.0,4178.0,2089.0,https://www.techpowerup.com/gpu-specs/jetson-orin-nano-super.c4377,2024,8.4,estimated,1.0
184
+ NVIDIA,Jetson T4000,GB10B,Server Blackwell(Bxx),Blackwell,1665.0,2525.0,3.0,,2025-08-27,1067.0,64.0,256.0,273.2,LPDDR5X,1536,12,64,50.0,40.0,31030.0,7757.0,https://www.techpowerup.com/gpu-specs/jetson-t4000.c4369,2025,41.4,estimated,1.5165165165165164
185
+ NVIDIA,Jetson T5000,GB10B,Server Blackwell(Bxx),Blackwell,1665.0,2525.0,3.0,,2025-08-27,1067.0,128.0,256.0,273.2,LPDDR5X,2560,20,96,50.0,40.0,51710.0,12930.0,https://www.techpowerup.com/gpu-specs/jetson-t5000.c4370,2025,62.1,estimated,1.5165165165165164
186
+ NVIDIA,Jetson TX2,GP10B,Tegra(Pascal),Pascal,1300.0,1300.0,16.0,,2016-01-01,1866.0,8.0,128.0,59.71,LPDDR4,256,2,0,0.5,15.0,1331.2,665.6,https://www.techpowerup.com/gpu-specs/jetson-tx2.c3231,2016,,unknown,1.0
187
+ NVIDIA,Jetson Xavier NX 16 GB,GV10B,Tegra(Volta),Volta,854.0,1100.0,12.0,9000.0,2020-05-14,1866.0,16.0,128.0,59.71,LPDDR4X,384,6,48,0.25,20.0,1690.0,844.8,https://www.techpowerup.com/gpu-specs/jetson-xavier-nx-16-gb.c4087,2020,6.8,estimated,1.288056206088993
188
+ NVIDIA,Jetson Xavier NX 8 GB,GV10B,Tegra(Volta),Volta,854.0,1100.0,12.0,9000.0,2020-05-14,1866.0,8.0,128.0,59.71,LPDDR4X,384,6,48,0.25,15.0,1690.0,844.8,https://www.techpowerup.com/gpu-specs/jetson-xavier-nx-8-gb.c3642,2020,6.8,estimated,1.288056206088993
189
+ NVIDIA,L20,AD102,Server Ada(Lxx),Ada Lovelace,1440.0,2520.0,5.0,76300.0,2023-11-16,2250.0,48.0,384.0,864.0,GDDR6,11776,92,368,96.0,275.0,59350.0,59350.0,https://www.techpowerup.com/gpu-specs/l20.c4206,2023,237.4,estimated,1.75
190
+ NVIDIA,L4,AD104,Server Ada(Lxx),Ada Lovelace,795.0,2040.0,5.0,35800.0,2023-03-21,1563.0,24.0,192.0,300.1,GDDR6,7424,60,240,48.0,72.0,30290.0,30290.0,https://www.techpowerup.com/gpu-specs/l4.c4091,2023,121.0,reported,2.5660377358490565
191
+ NVIDIA,L40,AD102,Server Ada(Lxx),Ada Lovelace,735.0,2490.0,5.0,76300.0,2022-10-13,2250.0,48.0,384.0,864.0,GDDR6,18176,142,568,96.0,300.0,90520.0,90520.0,https://www.techpowerup.com/gpu-specs/l40.c3959,2022,181.0,reported,3.3877551020408165
192
+ NVIDIA,L40 CNX,AD102,Server Ada(Lxx),Ada Lovelace,1005.0,2475.0,5.0,76300.0,2022-10-13,2250.0,24.0,384.0,864.0,GDDR6,18176,142,568,48.0,300.0,89970.0,89970.0,https://www.techpowerup.com/gpu-specs/l40-cnx.c3961,2022,359.9,estimated,2.462686567164179
193
+ NVIDIA,L40G,AD102,Server Ada(Lxx),Ada Lovelace,1005.0,2475.0,5.0,76300.0,2022-10-13,2250.0,24.0,384.0,864.0,GDDR6,18176,142,568,48.0,300.0,89970.0,89970.0,https://www.techpowerup.com/gpu-specs/l40g.c3960,2022,359.9,estimated,2.462686567164179
194
+ NVIDIA,L40S,AD102,Server Ada(Lxx),Ada Lovelace,1110.0,2520.0,5.0,76300.0,2022-10-13,2250.0,48.0,384.0,864.0,GDDR6,18176,142,568,48.0,300.0,91610.0,91610.0,https://www.techpowerup.com/gpu-specs/l40s.c4173,2022,362.0,reported,2.27027027027027
195
+ NVIDIA,P102-101,GP102,Mining GPUs,Pascal,1557.0,1670.0,16.0,11800.0,2018-01-01,2002.0,10.0,320.0,320.3,GDDR5,3200,25,0,2.5,250.0,167.0,10690.0,https://www.techpowerup.com/gpu-specs/p102-101.c3284,2018,,unknown,1.0725754656390494
196
+ NVIDIA,P106-100,GP106,Mining GPUs,Pascal,1506.0,1709.0,16.0,4400.0,2017-06-19,2002.0,6.0,192.0,192.2,GDDR5,1280,10,0,1.5,120.0,68.36,4375.0,https://www.techpowerup.com/gpu-specs/p106-100.c2980,2017,,unknown,1.1347941567065074
197
+ NVIDIA,PG506-207,GA100,Server Ampere(Axx),Ampere,930.0,1440.0,7.0,54200.0,2021-04-12,1215.0,24.0,3072.0,933.1,HBM2,3584,56,224,24.0,165.0,10320.0,10320.0,https://www.techpowerup.com/gpu-specs/pg506-207.c3962,2021,165.2,estimated,1.5483870967741935
198
+ NVIDIA,PG506-217,GA100,Server Ampere(Axx),Ampere,930.0,1440.0,7.0,54200.0,2021-04-12,1215.0,24.0,3072.0,933.1,HBM2,3584,56,224,24.0,165.0,10320.0,10320.0,https://www.techpowerup.com/gpu-specs/pg506-217.c3963,2021,165.2,estimated,1.5483870967741935
199
+ NVIDIA,PG506-232,GA100,Server Ampere(Axx),Ampere,930.0,1440.0,7.0,54200.0,2021-04-12,1215.0,24.0,3072.0,933.1,HBM2,3584,56,224,24.0,165.0,10320.0,10320.0,https://www.techpowerup.com/gpu-specs/pg506-232.c3799,2021,165.2,estimated,1.5483870967741935
200
+ NVIDIA,PG506-242,GA100,Server Ampere(Axx),Ampere,930.0,1440.0,7.0,54200.0,2021-04-12,1215.0,24.0,3072.0,933.1,HBM2,3584,56,224,24.0,165.0,10320.0,10320.0,https://www.techpowerup.com/gpu-specs/pg506-242.c3823,2021,165.2,estimated,1.5483870967741935
201
+ NVIDIA,Quadro 6000,GF100,Quadro Fermi(x000),Fermi,574.0,574.0,40.0,3100.0,2010-12-10,747.0,6.0,384.0,143.4,GDDR5,448,14,0,0.75,204.0,,1027.7,https://www.techpowerup.com/gpu-specs/quadro-6000.c896,2010,,unknown,1.0
202
+ NVIDIA,Quadro 6000 SDI,GF100,Quadro Fermi(x000),Fermi,574.0,574.0,40.0,3100.0,2011-07-25,747.0,6.0,384.0,143.4,GDDR5,448,14,0,0.75,231.0,,1027.7,https://www.techpowerup.com/gpu-specs/quadro-6000-sdi.c2141,2011,,unknown,1.0
203
+ NVIDIA,Quadro 7000,GF110,Quadro Fermi(x000),Fermi 2.0,651.0,651.0,40.0,3000.0,2012-05-02,851.0,6.0,384.0,163.4,GDDR5,512,14,0,0.75,204.0,,1332.2,https://www.techpowerup.com/gpu-specs/quadro-7000.c1840,2012,,unknown,1.0
204
+ NVIDIA,Quadro GP100,GP100,Quadro Pascal(Px000),Pascal,1304.0,1443.0,16.0,15300.0,2016-10-01,715.0,16.0,4096.0,732.2,HBM2,3584,56,0,4.0,235.0,20690.0,10340.0,https://www.techpowerup.com/gpu-specs/quadro-gp100.c2994,2016,,unknown,1.10659509202454
205
+ NVIDIA,Quadro GV100,GV100,Quadro Volta(Vx000),Volta,1132.0,1627.0,12.0,21100.0,2018-03-27,848.0,32.0,4096.0,868.4,HBM2,5120,80,640,6.0,250.0,33320.0,16660.0,https://www.techpowerup.com/gpu-specs/quadro-gv100.c3066,2018,133.3,estimated,1.437279151943463
206
+ NVIDIA,Quadro K5100M,GK104,Quadro Kepler-M(Kx100M),Kepler,771.0,771.0,28.0,3540.0,2013-07-23,900.0,8.0,256.0,115.2,GDDR5,1536,0,0,0.5,100.0,,2369.0,https://www.techpowerup.com/gpu-specs/quadro-k5100m.c2425,2013,,unknown,1.0
207
+ NVIDIA,Quadro K5200,GK110B,Quadro Kepler(Kx200),Kepler,667.0,771.0,28.0,7080.0,2014-07-22,1502.0,8.0,256.0,192.3,GDDR5,2304,0,0,0.0,150.0,,3553.0,https://www.techpowerup.com/gpu-specs/quadro-k5200.c2603,2014,,unknown,1.1559220389805098
208
+ NVIDIA,Quadro K6000,GK110B,Quadro Kepler(Kx000),Kepler,797.0,902.0,28.0,7080.0,2013-07-23,1502.0,12.0,384.0,288.4,GDDR5,2880,0,0,1.5,225.0,,5196.0,https://www.techpowerup.com/gpu-specs/quadro-k6000.c2426,2013,,unknown,1.1317440401505645
209
+ NVIDIA,Quadro K6000 SDI,GK110,Quadro Kepler(Kx000),Kepler,902.0,902.0,28.0,7080.0,2013-07-23,1502.0,12.0,384.0,288.4,GDDR5,2880,0,0,1.5,239.0,,5196.0,https://www.techpowerup.com/gpu-specs/quadro-k6000-sdi.c2436,2013,,unknown,1.0
210
+ NVIDIA,Quadro M4000,GM204,Quadro Maxwell(Mx000),Maxwell 2.0,773.0,773.0,28.0,5200.0,2015-06-29,1502.0,8.0,256.0,192.3,GDDR5,1664,13,0,2.0,120.0,,2573.0,https://www.techpowerup.com/gpu-specs/quadro-m4000.c2757,2015,,unknown,1.0
211
+ NVIDIA,Quadro M5000,GM204,Quadro Maxwell(Mx000),Maxwell 2.0,861.0,1038.0,28.0,5200.0,2015-06-29,1653.0,8.0,256.0,211.6,GDDR5,2048,16,0,2.0,150.0,,4252.0,https://www.techpowerup.com/gpu-specs/quadro-m5000.c2756,2015,,unknown,1.2055749128919862
212
+ NVIDIA,Quadro M5000M,GM204,Quadro Maxwell-M(Mx000M),Maxwell 2.0,962.0,1051.0,28.0,5200.0,2015-08-18,1253.0,8.0,256.0,160.4,GDDR5,1536,12,0,2.0,100.0,,3229.0,https://www.techpowerup.com/gpu-specs/quadro-m5000m.c2823,2015,,unknown,1.0925155925155925
213
+ NVIDIA,Quadro M5500 Mobile,GM204,Quadro Maxwell-M(Mx000M),Maxwell 2.0,1140.0,1165.0,28.0,5200.0,2016-04-08,1753.0,8.0,256.0,224.4,GDDR5,2048,16,0,2.0,150.0,,4772.0,https://www.techpowerup.com/gpu-specs/quadro-m5500-mobile.c2838,2016,,unknown,1.0219298245614035
214
+ NVIDIA,Quadro M6000,GM200,Quadro Maxwell(Mx000),Maxwell 2.0,988.0,1114.0,28.0,8000.0,2015-03-21,1653.0,12.0,384.0,317.4,GDDR5,3072,24,0,3.0,250.0,,6844.0,https://www.techpowerup.com/gpu-specs/quadro-m6000.c2638,2015,,unknown,1.1275303643724697
215
+ NVIDIA,Quadro M6000 24 GB,GM200,Quadro Maxwell(Mx000),Maxwell 2.0,988.0,1114.0,28.0,8000.0,2016-03-05,1653.0,24.0,384.0,317.4,GDDR5,3072,24,0,3.0,250.0,,6844.0,https://www.techpowerup.com/gpu-specs/quadro-m6000-24-gb.c2824,2016,,unknown,1.1275303643724697
216
+ NVIDIA,Quadro P3000 Mobile,GP104,Quadro Pascal-M(Px000),Pascal,1088.0,1215.0,16.0,7200.0,2017-01-11,1752.0,6.0,192.0,168.2,GDDR5,1280,10,0,1.5,75.0,48.6,3110.0,https://www.techpowerup.com/gpu-specs/quadro-p3000-mobile.c2923,2017,,unknown,1.1167279411764706
217
+ NVIDIA,Quadro P3200 Max-Q,GP104,Quadro Pascal-M(Px200),Pascal,1139.0,1404.0,16.0,7200.0,2018-02-21,1753.0,6.0,192.0,168.3,GDDR5,1792,14,0,1.5,75.0,78.62,5032.0,https://www.techpowerup.com/gpu-specs/quadro-p3200-max-q.c3315,2018,,unknown,1.2326602282704127
218
+ NVIDIA,Quadro P3200 Mobile,GP104,Quadro Pascal-M(Px200),Pascal,1328.0,1543.0,16.0,7200.0,2018-02-21,1752.0,6.0,192.0,168.2,GDDR5,1792,14,0,1.5,75.0,86.41,5530.0,https://www.techpowerup.com/gpu-specs/quadro-p3200-mobile.c3147,2018,,unknown,1.1618975903614457
219
+ NVIDIA,Quadro P4000,GP104,Quadro Pascal(Px000),Pascal,1202.0,1480.0,16.0,7200.0,2017-02-06,1901.0,8.0,256.0,243.3,GDDR5,1792,14,0,2.0,105.0,82.88,5304.0,https://www.techpowerup.com/gpu-specs/quadro-p4000.c2930,2017,,unknown,1.2312811980033278
220
+ NVIDIA,Quadro P4000 Max-Q,GP104,Quadro Pascal-M(Px000),Pascal,1114.0,1228.0,16.0,7200.0,2017-01-11,1502.0,8.0,256.0,192.3,GDDR5,1792,14,0,2.0,100.0,68.77,4401.0,https://www.techpowerup.com/gpu-specs/quadro-p4000-max-q.c3015,2017,,unknown,1.1023339317773788
221
+ NVIDIA,Quadro P4000 Mobile,GP104,Quadro Pascal-M(Px000),Pascal,1202.0,1228.0,16.0,7200.0,2017-01-11,1500.0,8.0,256.0,192.0,GDDR5,1792,14,0,2.0,100.0,68.77,4401.0,https://www.techpowerup.com/gpu-specs/quadro-p4000-mobile.c2924,2017,,unknown,1.021630615640599
222
+ NVIDIA,Quadro P4200 Max-Q,GP104,Quadro Pascal-M(Px200),Pascal,1215.0,1480.0,16.0,7200.0,2018-02-21,1753.0,8.0,256.0,224.4,GDDR5,2304,18,0,2.0,100.0,106.6,6820.0,https://www.techpowerup.com/gpu-specs/quadro-p4200-max-q.c4004,2018,,unknown,1.2181069958847737
223
+ NVIDIA,Quadro P4200 Mobile,GP104,Quadro Pascal-M(Px200),Pascal,1418.0,1594.0,16.0,7200.0,2018-02-21,1753.0,8.0,256.0,224.4,GDDR5,2304,18,0,2.0,100.0,114.8,7345.0,https://www.techpowerup.com/gpu-specs/quadro-p4200-mobile.c3199,2018,,unknown,1.1241184767277856
224
+ NVIDIA,Quadro P5000,GP104,Quadro Pascal(Px000),Pascal,1607.0,1733.0,16.0,7200.0,2016-10-01,1127.0,16.0,256.0,288.5,GDDR5X,2560,20,0,2.0,180.0,138.6,8873.0,https://www.techpowerup.com/gpu-specs/quadro-p5000.c2864,2016,,unknown,1.0784069695084006
225
+ NVIDIA,Quadro P5000 Mobile,GP104,Quadro Pascal-M(Px000),Pascal,1164.0,1506.0,16.0,7200.0,2017-01-11,1500.0,16.0,256.0,192.0,GDDR5,2048,16,0,2.0,100.0,96.38,6169.0,https://www.techpowerup.com/gpu-specs/quadro-p5000-mobile.c2925,2017,,unknown,1.2938144329896908
226
+ NVIDIA,Quadro P5000 X2 Mobile,GP104,Quadro Pascal-M(Px000),Pascal,1164.0,1506.0,16.0,7200.0,2017-01-11,1500.0,16.0,256.0,192.0,GDDR5,2048,16,0,2.0,200.0,96.38,6169.0,https://www.techpowerup.com/gpu-specs/quadro-p5000-x2-mobile.c4122,2017,,unknown,1.2938144329896908
227
+ NVIDIA,Quadro P5200 Max-Q,GP104,Quadro Pascal-M(Px200),Pascal,1316.0,1569.0,16.0,7200.0,2018-02-21,1804.0,16.0,256.0,230.9,GDDR5,2560,20,0,2.0,100.0,125.5,8032.999999999999,https://www.techpowerup.com/gpu-specs/quadro-p5200-max-q.c3347,2018,,unknown,1.1922492401215805
228
+ NVIDIA,Quadro P5200 Mobile,GP104,Quadro Pascal-M(Px200),Pascal,1582.0,1759.0,16.0,7200.0,2018-02-21,1804.0,16.0,256.0,230.9,GDDR5,2560,20,0,2.0,100.0,140.7,9006.0,https://www.techpowerup.com/gpu-specs/quadro-p5200-mobile.c3020,2018,,unknown,1.1118836915297092
229
+ NVIDIA,Quadro P6000,GP102,Quadro Pascal(Px000),Pascal,1506.0,1645.0,16.0,11800.0,2016-10-01,1127.0,24.0,384.0,432.8,GDDR5X,3840,30,0,3.0,250.0,197.4,12630.0,https://www.techpowerup.com/gpu-specs/quadro-p6000.c2865,2016,,unknown,1.092297476759628
230
+ NVIDIA,Quadro Plex 7000,GF110,Quadro Plex,Fermi 2.0,574.0,574.0,40.0,3000.0,2011-07-25,750.0,6.0,384.0,144.0,GDDR5,512,16,0,0.75,600.0,,1175.6,https://www.techpowerup.com/gpu-specs/quadro-plex-7000.c902,2011,,unknown,1.0
231
+ NVIDIA,Quadro RTX 3000 Max-Q,TU106,Quadro Turing-M(Tx000),Turing,600.0,1215.0,12.0,10800.0,2019-05-27,1500.0,6.0,192.0,288.0,GDDR6,1920,30,240,3.0,60.0,9331.0,4666.0,https://www.techpowerup.com/gpu-specs/quadro-rtx-3000-max-q.c3429,2019,37.3,estimated,2.025
232
+ NVIDIA,Quadro RTX 3000 Mobile,TU106,Quadro Turing-M(Tx000),Turing,945.0,1380.0,12.0,10800.0,2019-05-27,1750.0,6.0,192.0,336.0,GDDR6,1920,30,240,3.0,80.0,10600.0,5299.0,https://www.techpowerup.com/gpu-specs/quadro-rtx-3000-mobile.c3428,2019,42.4,estimated,1.4603174603174602
233
+ NVIDIA,Quadro RTX 3000 Mobile Refresh,TU106,Quadro Turing-M(Tx000),Turing,945.0,1380.0,12.0,10800.0,2019-05-27,1750.0,6.0,192.0,336.0,GDDR6,1920,30,240,4.0,80.0,10600.0,5299.0,https://www.techpowerup.com/gpu-specs/quadro-rtx-3000-mobile-refresh.c3697,2019,42.4,estimated,1.4603174603174602
234
+ NVIDIA,Quadro RTX 3000 X2 Mobile,TU106,Quadro Turing-M(Tx000),Turing,945.0,1380.0,12.0,10800.0,2019-05-27,1750.0,6.0,192.0,336.0,GDDR6,1920,30,240,4.0,160.0,10600.0,5299.0,https://www.techpowerup.com/gpu-specs/quadro-rtx-3000-x2-mobile.c4120,2019,42.4,estimated,1.4603174603174602
235
+ NVIDIA,Quadro RTX 4000,TU104,Quadro Turing(Tx000),Turing,1005.0,1545.0,12.0,13600.0,2018-11-13,1625.0,8.0,256.0,416.0,GDDR6,2304,36,288,4.0,160.0,14240.0,7119.0,https://www.techpowerup.com/gpu-specs/quadro-rtx-4000.c3336,2018,57.0,estimated,1.537313432835821
236
+ NVIDIA,Quadro RTX 4000 Max-Q,TU104,Quadro Turing-M(Tx000),Turing,780.0,1380.0,12.0,13600.0,2019-05-27,1500.0,8.0,256.0,384.0,GDDR6,2560,40,320,4.0,80.0,14130.0,7066.0,https://www.techpowerup.com/gpu-specs/quadro-rtx-4000-max-q.c3427,2019,56.5,estimated,1.7692307692307692
237
+ NVIDIA,Quadro RTX 4000 Mobile,TU104,Quadro Turing-M(Tx000),Turing,1110.0,1560.0,12.0,13600.0,2019-05-27,1750.0,8.0,256.0,448.0,GDDR6,2560,40,320,4.0,110.0,15970.0,7987.0,https://www.techpowerup.com/gpu-specs/quadro-rtx-4000-mobile.c3430,2019,63.9,estimated,1.4054054054054055
238
+ NVIDIA,Quadro RTX 5000,TU104,Quadro Turing(Tx000),Turing,1620.0,1815.0,12.0,13600.0,2018-08-13,1750.0,16.0,256.0,448.0,GDDR6,3072,48,384,4.0,230.0,22300.0,11150.0,https://www.techpowerup.com/gpu-specs/quadro-rtx-5000.c3308,2018,89.2,estimated,1.1203703703703705
239
+ NVIDIA,Quadro RTX 5000 Max-Q,TU104,Quadro Turing-M(Tx000),Turing,600.0,1350.0,12.0,13600.0,2019-05-27,1500.0,16.0,256.0,384.0,GDDR6,3072,48,384,4.0,80.0,16590.0,8294.0,https://www.techpowerup.com/gpu-specs/quadro-rtx-5000-max-q.c3432,2019,66.4,estimated,2.25
240
+ NVIDIA,Quadro RTX 5000 Mobile,TU104,Quadro Turing-M(Tx000),Turing,1035.0,1530.0,12.0,13600.0,2019-05-27,1750.0,16.0,256.0,448.0,GDDR6,3072,48,384,4.0,110.0,18800.0,9400.0,https://www.techpowerup.com/gpu-specs/quadro-rtx-5000-mobile.c3431,2019,75.2,estimated,1.4782608695652173
241
+ NVIDIA,Quadro RTX 5000 Mobile Refresh,TU104B,Quadro Turing-M(Tx000),Turing,1035.0,1530.0,12.0,13600.0,2020-06-08,1750.0,16.0,256.0,448.0,GDDR6,3072,48,384,4.0,110.0,18800.0,9400.0,https://www.techpowerup.com/gpu-specs/quadro-rtx-5000-mobile-refresh.c3625,2020,75.2,estimated,1.4782608695652173
242
+ NVIDIA,Quadro RTX 5000 X2 Mobile,TU104,Quadro Turing-M(Tx000),Turing,1035.0,1530.0,12.0,13600.0,2020-06-08,1750.0,16.0,256.0,448.0,GDDR6,3072,48,384,4.0,110.0,18800.0,9400.0,https://www.techpowerup.com/gpu-specs/quadro-rtx-5000-x2-mobile.c4121,2020,75.2,estimated,1.4782608695652173
243
+ NVIDIA,Quadro RTX 6000,TU102,Quadro Turing(Tx000),Turing,1440.0,1770.0,12.0,18600.0,2018-08-13,1750.0,24.0,384.0,672.0,GDDR6,4608,72,576,6.0,260.0,32619.999999999996,16309.999999999998,https://www.techpowerup.com/gpu-specs/quadro-rtx-6000.c3307,2018,130.5,estimated,1.2291666666666667
244
+ NVIDIA,Quadro RTX 6000 Passive,TU102,Quadro Turing(Tx000),Turing,1275.0,1620.0,12.0,18600.0,2018-08-13,1625.0,24.0,384.0,624.0,GDDR6,4608,72,576,6.0,260.0,29860.0,14930.0,https://www.techpowerup.com/gpu-specs/quadro-rtx-6000-passive.c3469,2018,119.4,estimated,1.2705882352941176
245
+ NVIDIA,Quadro RTX 8000,TU102,Quadro Turing(Tx000),Turing,1395.0,1770.0,12.0,18600.0,2018-08-13,1750.0,48.0,384.0,672.0,GDDR6,4608,72,576,6.0,260.0,32619.999999999996,16309.999999999998,https://www.techpowerup.com/gpu-specs/quadro-rtx-8000.c3306,2018,130.5,estimated,1.2688172043010753
246
+ NVIDIA,Quadro RTX 8000 Passive,TU102,Quadro Turing(Tx000),Turing,1230.0,1620.0,12.0,18600.0,2018-08-13,1625.0,48.0,384.0,624.0,GDDR6,4608,72,576,6.0,260.0,29860.0,14930.0,https://www.techpowerup.com/gpu-specs/quadro-rtx-8000-passive.c3470,2018,119.4,estimated,1.3170731707317074
247
+ NVIDIA,RTX 1000 Mobile Ada Generation,AD107,Ada-MW(x000A),Ada Lovelace,1485.0,2025.0,5.0,18900.0,2024-02-26,2000.0,6.0,96.0,192.0,GDDR6,2560,20,80,12.0,35.0,10370.0,10370.0,https://www.techpowerup.com/gpu-specs/rtx-1000-mobile-ada-generation.c4208,2024,41.5,estimated,1.3636363636363635
248
+ NVIDIA,RTX 2000 Ada Generation,AD107,Workstation Ada(x000A),Ada Lovelace,1620.0,2130.0,5.0,18900.0,2024-02-12,2000.0,16.0,128.0,256.0,GDDR6,2816,22,88,12.0,70.0,12000.0,12000.0,https://www.techpowerup.com/gpu-specs/rtx-2000-ada-generation.c4199,2024,48.0,estimated,1.3148148148148149
249
+ NVIDIA,RTX 2000 Embedded Ada Generation,AD107,Ada-MW(x000A),Ada Lovelace,1530.0,2010.0,5.0,18900.0,2023-03-21,2000.0,8.0,128.0,256.0,GDDR6,3072,24,96,12.0,50.0,12350.0,12350.0,https://www.techpowerup.com/gpu-specs/rtx-2000-embedded-ada-generation.c4177,2023,49.4,estimated,1.3137254901960784
250
+ NVIDIA,RTX 2000 Max-Q Ada Generation,AD107,Ada-MW(x000A),Ada Lovelace,930.0,1455.0,5.0,18900.0,2023-03-21,2000.0,8.0,128.0,256.0,GDDR6,3072,24,96,12.0,35.0,8940.0,8940.0,https://www.techpowerup.com/gpu-specs/rtx-2000-max-q-ada-generation.c4094,2023,35.8,estimated,1.564516129032258
251
+ NVIDIA,RTX 2000 Mobile Ada Generation,AD107,Ada-MW(x000A),Ada Lovelace,1635.0,2115.0,5.0,18900.0,2023-03-21,2000.0,8.0,128.0,256.0,GDDR6,3072,24,96,12.0,50.0,12990.0,12990.0,https://www.techpowerup.com/gpu-specs/rtx-2000-mobile-ada-generation.c4093,2023,52.0,estimated,1.2935779816513762
252
+ NVIDIA,RTX 3000 Mobile Ada Generation,AD106,Ada-MW(x000A),Ada Lovelace,1395.0,1695.0,5.0,22900.0,2023-03-21,2000.0,8.0,128.0,256.0,GDDR6,4608,36,144,32.0,115.0,15620.0,15620.0,https://www.techpowerup.com/gpu-specs/rtx-3000-mobile-ada-generation.c4095,2023,62.5,estimated,1.2150537634408602
253
+ NVIDIA,RTX 3500 Embedded Ada Generation,AD104,Ada-MW(x000A),Ada Lovelace,1725.0,2250.0,5.0,35800.0,2023-03-21,2250.0,12.0,192.0,432.0,GDDR6,5120,40,160,48.0,100.0,23040.0,23040.0,https://www.techpowerup.com/gpu-specs/rtx-3500-embedded-ada-generation.c4201,2023,92.2,estimated,1.3043478260869565
254
+ NVIDIA,RTX 3500 Mobile Ada Generation,AD104,Ada-MW(x000A),Ada Lovelace,1110.0,1545.0,5.0,35800.0,2023-03-21,2250.0,12.0,192.0,432.0,GDDR6,5120,40,160,48.0,100.0,15820.0,15820.0,https://www.techpowerup.com/gpu-specs/rtx-3500-mobile-ada-generation.c4098,2023,63.3,estimated,1.3918918918918919
255
+ NVIDIA,RTX 4000 Ada Generation,AD104,Workstation Ada(x000A),Ada Lovelace,1500.0,2175.0,5.0,35800.0,2023-08-09,2250.0,20.0,160.0,360.0,GDDR6,6144,48,192,48.0,130.0,26730.0,26730.0,https://www.techpowerup.com/gpu-specs/rtx-4000-ada-generation.c4171,2023,106.9,estimated,1.45
256
+ NVIDIA,RTX 4000 Mobile Ada Generation,AD104,Ada-MW(x000A),Ada Lovelace,1290.0,1665.0,5.0,35800.0,2023-03-21,2250.0,12.0,192.0,432.0,GDDR6,7424,58,232,48.0,110.0,24720.0,24720.0,https://www.techpowerup.com/gpu-specs/rtx-4000-mobile-ada-generation.c4096,2023,98.9,estimated,1.2906976744186047
257
+ NVIDIA,RTX 4000 SFF Ada Generation,AD104,Workstation Ada(x000A),Ada Lovelace,720.0,1560.0,5.0,35800.0,2023-03-21,1750.0,20.0,160.0,280.0,GDDR6,6144,48,192,48.0,70.0,19170.0,19170.0,https://www.techpowerup.com/gpu-specs/rtx-4000-sff-ada-generation.c4139,2023,76.7,estimated,2.1666666666666665
258
+ NVIDIA,RTX 4500 Ada Generation,AD103,Workstation Ada(x000A),Ada Lovelace,2070.0,2580.0,5.0,45900.0,2023-08-09,2250.0,24.0,192.0,432.0,GDDR6,7680,60,240,48.0,210.0,39630.0,39630.0,https://www.techpowerup.com/gpu-specs/rtx-4500-ada-generation.c4172,2023,158.5,estimated,1.2463768115942029
259
+ NVIDIA,RTX 5000 Ada Generation,AD102,Workstation Ada(x000A),Ada Lovelace,1155.0,2550.0,5.0,76300.0,2023-08-09,2250.0,32.0,256.0,576.0,GDDR6,12800,100,400,72.0,250.0,65280.0,65280.0,https://www.techpowerup.com/gpu-specs/rtx-5000-ada-generation.c4152,2023,262.0,reported,2.207792207792208
260
+ NVIDIA,RTX 5000 Embedded Ada Generation,AD103,Ada-MW(x000A),Ada Lovelace,930.0,1680.0,5.0,45900.0,2023-03-21,2250.0,16.0,256.0,576.0,GDDR6,9728,76,304,64.0,120.0,32689.999999999996,32689.999999999996,https://www.techpowerup.com/gpu-specs/rtx-5000-embedded-ada-generation.c4176,2023,130.7,estimated,1.8064516129032258
261
+ NVIDIA,RTX 5000 Embedded Ada Generation X2,AD103,Ada-MW(x000A),Ada Lovelace,930.0,1680.0,5.0,45900.0,2023-03-21,2250.0,16.0,256.0,576.0,GDDR6,9728,76,304,64.0,150.0,32689.999999999996,32689.999999999996,https://www.techpowerup.com/gpu-specs/rtx-5000-embedded-ada-generation-x2.c4256,2023,130.7,estimated,1.8064516129032258
262
+ NVIDIA,RTX 5000 Max-Q Ada Generation,AD103,Ada-MW(x000A),Ada Lovelace,930.0,1680.0,5.0,45900.0,2023-03-21,2250.0,16.0,256.0,576.0,GDDR6,9728,76,304,64.0,120.0,32689.999999999996,32689.999999999996,https://www.techpowerup.com/gpu-specs/rtx-5000-max-q-ada-generation.c4154,2023,130.7,estimated,1.8064516129032258
263
+ NVIDIA,RTX 5000 Mobile Ada Generation,AD103,Ada-MW(x000A),Ada Lovelace,1425.0,2115.0,5.0,45900.0,2023-03-21,2250.0,16.0,256.0,576.0,GDDR6,9728,76,304,64.0,120.0,41150.0,41150.0,https://www.techpowerup.com/gpu-specs/rtx-5000-mobile-ada-generation.c4097,2023,164.6,estimated,1.4842105263157894
264
+ NVIDIA,RTX 5880 Ada Generation,AD102,Workstation Ada(x000A),Ada Lovelace,975.0,2460.0,5.0,76300.0,2024-01-05,2250.0,48.0,384.0,864.0,GDDR6,14080,110,440,72.0,285.0,69269.99999999999,69269.99999999999,https://www.techpowerup.com/gpu-specs/rtx-5880-ada-generation.c4191,2024,277.1,estimated,2.523076923076923
265
+ NVIDIA,RTX 6000 Ada Generation,AD102,Workstation Ada(x000A),Ada Lovelace,915.0,2505.0,5.0,76300.0,2022-12-03,2500.0,48.0,384.0,960.0,GDDR6,18176,142,568,96.0,300.0,91060.0,91060.0,https://www.techpowerup.com/gpu-specs/rtx-6000-ada-generation.c3933,2022,364.0,reported,2.737704918032787
266
+ NVIDIA,RTX 6000D,GB202,Blackwell PRO W(x000),Blackwell 2.0,1590.0,2430.0,5.0,92200.0,2025-03-18,1750.0,84.0,448.0,1570.0,GDDR7,19968,156,624,128.0,600.0,97040.0,97040.0,https://www.techpowerup.com/gpu-specs/rtx-6000d.c4363,2025,388.2,estimated,1.528301886792453
267
+ NVIDIA,RTX A1000,GA107,Workstation Ampere(Ax000),Ampere,727.0,1462.0,8.0,8700.0,2024-04-16,1500.0,8.0,128.0,192.0,GDDR6,2304,18,72,2.0,50.0,6737.0,6737.0,https://www.techpowerup.com/gpu-specs/rtx-a1000.c4211,2024,26.9,estimated,2.0110041265474554
268
+ NVIDIA,RTX A1000 Mobile 6 GB,GA107,Ampere-MW(Ax000),Ampere,652.0,1140.0,8.0,8700.0,2022-03-30,1375.0,6.0,96.0,132.0,GDDR6,2560,20,80,2.0,60.0,5837.0,5837.0,https://www.techpowerup.com/gpu-specs/rtx-a1000-mobile-6-gb.c4137,2022,23.3,estimated,1.7484662576687116
269
+ NVIDIA,RTX A2000,GA106,Workstation Ampere(Ax000),Ampere,562.0,1200.0,8.0,12000.0,2021-08-10,1500.0,6.0,192.0,288.0,GDDR6,3328,26,104,3.0,70.0,7987.0,7987.0,https://www.techpowerup.com/gpu-specs/rtx-a2000.c3820,2021,31.9,estimated,2.1352313167259784
270
+ NVIDIA,RTX A2000 12 GB,GA106,Workstation Ampere(Ax000),Ampere,562.0,1200.0,8.0,12000.0,2021-11-23,1500.0,12.0,192.0,288.0,GDDR6,3328,26,104,3.0,70.0,7987.0,7987.0,https://www.techpowerup.com/gpu-specs/rtx-a2000-12-gb.c3853,2021,31.9,estimated,2.1352313167259784
271
+ NVIDIA,RTX A2000 Max-Q 8 GB,GA107,Ampere-MW(Ax000),Ampere,607.0,1177.0,8.0,8700.0,2021-04-12,1375.0,8.0,128.0,176.0,GDDR6,2560,20,80,2.0,95.0,6026.0,6026.0,https://www.techpowerup.com/gpu-specs/rtx-a2000-max-q-8-gb.c4288,2021,24.1,estimated,1.9390444810543657
272
+ NVIDIA,RTX A2000 Mobile 8 GB,GA107,Ampere-MW(Ax000),Ampere,1117.0,1612.0,8.0,8700.0,2021-04-12,1750.0,8.0,128.0,224.0,GDDR6,2560,20,80,2.0,95.0,8253.0,8253.0,https://www.techpowerup.com/gpu-specs/rtx-a2000-mobile-8-gb.c4287,2021,33.0,estimated,1.4431512981199641
273
+ NVIDIA,RTX A3000 Mobile,GA104,Ampere-MW(Ax000),Ampere,600.0,1230.0,8.0,17400.0,2021-04-12,1375.0,6.0,192.0,264.0,GDDR6,4096,32,128,4.0,70.0,10080.0,10080.0,https://www.techpowerup.com/gpu-specs/rtx-a3000-mobile.c3806,2021,40.3,estimated,2.05
274
+ NVIDIA,RTX A3000 Mobile 12 GB,GA104,Ampere-MW(Ax000),Ampere,855.0,1440.0,8.0,17400.0,2022-03-22,1750.0,12.0,192.0,336.0,GDDR6,4096,32,128,4.0,115.0,11800.0,11800.0,https://www.techpowerup.com/gpu-specs/rtx-a3000-mobile-12-gb.c3903,2022,47.2,estimated,1.6842105263157894
275
+ NVIDIA,RTX A4000,GA104,Workstation Ampere(Ax000),Ampere,735.0,1560.0,8.0,17400.0,2021-04-12,1750.0,16.0,256.0,448.0,GDDR6,6144,48,192,4.0,140.0,19170.0,19170.0,https://www.techpowerup.com/gpu-specs/rtx-a4000.c3756,2021,76.7,estimated,2.122448979591837
276
+ NVIDIA,RTX A4000 Max-Q,GA104,Ampere-MW(Ax000),Ampere,780.0,1395.0,8.0,17400.0,2021-04-12,1375.0,8.0,256.0,352.0,GDDR6,5120,40,160,4.0,80.0,14280.0,14280.0,https://www.techpowerup.com/gpu-specs/rtx-a4000-max-q.c4008,2021,57.1,estimated,1.7884615384615385
277
+ NVIDIA,RTX A4000 Mobile,GA104,Ampere-MW(Ax000),Ampere,1140.0,1680.0,8.0,17400.0,2021-04-12,1500.0,8.0,256.0,384.0,GDDR6,5120,40,160,4.0,115.0,17200.0,17200.0,https://www.techpowerup.com/gpu-specs/rtx-a4000-mobile.c3804,2021,68.8,estimated,1.4736842105263157
278
+ NVIDIA,RTX A4000H,GA104,Workstation Ampere(Ax000),Ampere,735.0,1560.0,8.0,17400.0,2021-04-12,1750.0,16.0,256.0,448.0,GDDR6,6144,48,192,4.0,140.0,19170.0,19170.0,https://www.techpowerup.com/gpu-specs/rtx-a4000h.c3969,2021,76.7,estimated,2.122448979591837
279
+ NVIDIA,RTX A4500,GA102,Workstation Ampere(Ax000),Ampere,1050.0,1650.0,8.0,28300.0,2021-11-23,2000.0,20.0,320.0,640.0,GDDR6,7168,56,224,6.0,200.0,23650.0,23650.0,https://www.techpowerup.com/gpu-specs/rtx-a4500.c3849,2021,94.6,estimated,1.5714285714285714
280
+ NVIDIA,RTX A4500 Embedded,GA104,Ampere-MW(Ax000),Ampere,510.0,1215.0,8.0,17400.0,2022-03-30,1500.0,16.0,256.0,384.0,GDDR6,5888,46,184,4.0,80.0,14310.0,14310.0,https://www.techpowerup.com/gpu-specs/rtx-a4500-embedded.c3893,2022,57.2,estimated,2.3823529411764706
281
+ NVIDIA,RTX A4500 Max-Q,GA104,Ampere-MW(Ax000),Ampere,510.0,1215.0,8.0,17400.0,2022-03-22,1750.0,16.0,256.0,448.0,GDDR6,5888,46,184,4.0,80.0,14310.0,14310.0,https://www.techpowerup.com/gpu-specs/rtx-a4500-max-q.c4009,2022,57.2,estimated,2.3823529411764706
282
+ NVIDIA,RTX A4500 Mobile,GA104,Ampere-MW(Ax000),Ampere,930.0,1500.0,8.0,17400.0,2022-03-22,2000.0,16.0,256.0,512.0,GDDR6,5888,46,184,4.0,140.0,17660.0,17660.0,https://www.techpowerup.com/gpu-specs/rtx-a4500-mobile.c3851,2022,70.7,estimated,1.6129032258064515
283
+ NVIDIA,RTX A5000,GA102,Workstation Ampere(Ax000),Ampere,1170.0,1695.0,8.0,28300.0,2021-04-12,2000.0,24.0,384.0,768.0,GDDR6,8192,64,256,6.0,230.0,27770.0,27770.0,https://www.techpowerup.com/gpu-specs/rtx-a5000.c3748,2021,111.0,reported,1.4487179487179487
284
+ NVIDIA,RTX A5000 Max-Q,GA104,Ampere-MW(Ax000),Ampere,720.0,1350.0,8.0,17400.0,2021-04-12,1500.0,16.0,256.0,384.0,GDDR6,6144,48,192,4.0,80.0,16590.0,16590.0,https://www.techpowerup.com/gpu-specs/rtx-a5000-max-q.c4010,2021,66.4,estimated,1.875
285
+ NVIDIA,RTX A5000 Mobile,GA104,Ampere-MW(Ax000),Ampere,900.0,1575.0,8.0,17400.0,2021-04-12,1750.0,16.0,256.0,448.0,GDDR6,6144,48,192,4.0,150.0,19350.0,19350.0,https://www.techpowerup.com/gpu-specs/rtx-a5000-mobile.c3805,2021,77.4,estimated,1.75
286
+ NVIDIA,RTX A5000-12Q,GA102,Workstation Ampere(Ax000),Ampere,1170.0,1695.0,8.0,28300.0,2021-04-12,2000.0,12.0,384.0,768.0,GDDR6,8192,64,256,6.0,230.0,27770.0,27770.0,https://www.techpowerup.com/gpu-specs/rtx-a5000-12q.c3991,2021,111.1,estimated,1.4487179487179487
287
+ NVIDIA,RTX A5000-8Q,GA102,Workstation Ampere(Ax000),Ampere,1170.0,1695.0,8.0,28300.0,2021-04-12,2000.0,8.0,384.0,768.0,GDDR6,8192,64,256,6.0,230.0,27770.0,27770.0,https://www.techpowerup.com/gpu-specs/rtx-a5000-8q.c3990,2021,111.1,estimated,1.4487179487179487
288
+ NVIDIA,RTX A5500,GA102,Workstation Ampere(Ax000),Ampere,1080.0,1665.0,8.0,28300.0,2022-03-22,2000.0,24.0,384.0,768.0,GDDR6,10240,80,320,6.0,230.0,34100.0,34100.0,https://www.techpowerup.com/gpu-specs/rtx-a5500.c3901,2022,136.4,estimated,1.5416666666666667
289
+ NVIDIA,RTX A5500 Max-Q,GA103,Ampere-MW(Ax000),Ampere,585.0,1260.0,8.0,22000.0,2022-03-22,1750.0,16.0,256.0,448.0,GDDR6,7424,58,232,4.0,80.0,18710.0,18710.0,https://www.techpowerup.com/gpu-specs/rtx-a5500-max-q.c4011,2022,74.8,estimated,2.1538461538461537
290
+ NVIDIA,RTX A5500 Mobile,GA103,Ampere-MW(Ax000),Ampere,975.0,1500.0,8.0,22000.0,2022-03-22,2000.0,16.0,256.0,512.0,GDDR6,7424,58,232,4.0,165.0,22270.0,22270.0,https://www.techpowerup.com/gpu-specs/rtx-a5500-mobile.c3902,2022,89.1,estimated,1.5384615384615385
291
+ NVIDIA,RTX A6000,GA102,Workstation Ampere(Ax000),Ampere,1410.0,1800.0,8.0,28300.0,2020-10-05,2000.0,48.0,384.0,768.0,GDDR6,10752,84,336,6.0,300.0,38710.0,38710.0,https://www.techpowerup.com/gpu-specs/rtx-a6000.c3686,2020,155.0,reported,1.2765957446808511
292
+ NVIDIA,RTX PRO 2000 Blackwell,GB206,Blackwell PRO W(x000),Blackwell 2.0,982.0,1957.0,5.0,21900.0,2025-08-11,1125.0,16.0,128.0,288.0,GDDR7,4352,34,136,32.0,70.0,17030.000000000004,17030.000000000004,https://www.techpowerup.com/gpu-specs/rtx-pro-2000-blackwell.c4330,2025,68.1,estimated,1.9928716904276986
293
+ NVIDIA,RTX PRO 4000 Blackwell,GB203,Blackwell PRO W(x000),Blackwell 2.0,1230.0,2055.0,5.0,45600.0,2025-03-18,1750.0,24.0,192.0,672.0,GDDR7,8960,70,280,48.0,140.0,36830.0,36830.0,https://www.techpowerup.com/gpu-specs/rtx-pro-4000-blackwell.c4279,2025,147.3,estimated,1.670731707317073
294
+ NVIDIA,RTX PRO 4000 Blackwell SFF,GB203,Blackwell PRO W(x000),Blackwell 2.0,577.0,1432.0,5.0,45600.0,2025-08-11,1125.0,24.0,192.0,432.0,GDDR7,8960,70,280,48.0,70.0,25660.0,25660.0,https://www.techpowerup.com/gpu-specs/rtx-pro-4000-blackwell-sff.c4329,2025,102.6,estimated,2.481802426343154
295
+ NVIDIA,RTX PRO 4500 Blackwell,GB203,Blackwell PRO W(x000),Blackwell 2.0,1635.0,2407.0,5.0,45600.0,2025-03-18,1750.0,32.0,256.0,896.0,GDDR7,10496,82,328,64.0,200.0,50530.0,50530.0,https://www.techpowerup.com/gpu-specs/rtx-pro-4500-blackwell.c4278,2025,202.1,estimated,1.47217125382263
296
+ NVIDIA,RTX PRO 5000 72 GB Blackwell,GB202,Blackwell PRO W(x000),Blackwell 2.0,1740.0,2377.0,5.0,92200.0,2025-10-21,1750.0,72.0,384.0,1340.0,GDDR7,14080,110,440,96.0,300.0,66940.0,66940.0,https://www.techpowerup.com/gpu-specs/rtx-pro-5000-72-gb-blackwell.c4357,2025,267.7,estimated,1.3660919540229886
297
+ NVIDIA,RTX PRO 5000 Blackwell,GB202,Blackwell PRO W(x000),Blackwell 2.0,1740.0,2377.0,5.0,92200.0,2025-03-18,1750.0,48.0,384.0,1340.0,GDDR7,14080,110,440,96.0,300.0,66940.0,66940.0,https://www.techpowerup.com/gpu-specs/rtx-pro-5000-blackwell.c4276,2025,267.7,estimated,1.3660919540229886
298
+ NVIDIA,RTX PRO 6000 Blackwell,GB202,Blackwell PRO W(x000),Blackwell 2.0,1590.0,2617.0,5.0,92200.0,2025-03-18,1750.0,96.0,512.0,1790.0,GDDR7,24064,188,752,128.0,600.0,126000.0,126000.0,https://www.techpowerup.com/gpu-specs/rtx-pro-6000-blackwell.c4272,2025,503.8,estimated,1.6459119496855346
299
+ NVIDIA,RTX PRO 6000 Blackwell Max-Q,GB202,Blackwell PRO W(x000),Blackwell 2.0,1035.0,2280.0,5.0,92200.0,2025-03-18,1750.0,96.0,512.0,1790.0,GDDR7,24064,188,752,128.0,300.0,109700.0,109700.0,https://www.techpowerup.com/gpu-specs/rtx-pro-6000-blackwell-max-q.c4273,2025,438.9,estimated,2.2028985507246377
300
+ NVIDIA,RTX PRO 6000 Blackwell Server,GB202,Server Blackwell(Bxx),Blackwell 2.0,1590.0,2617.0,5.0,92200.0,2025-03-18,1750.0,96.0,512.0,1790.0,GDDR7,24064,188,752,128.0,600.0,126000.0,126000.0,https://www.techpowerup.com/gpu-specs/rtx-pro-6000-blackwell-server.c4274,2025,503.8,estimated,1.6459119496855346
301
+ NVIDIA,RTX PRO 6000D Blackwell Max-Q,GB202,Blackwell PRO W(x000),Blackwell 2.0,1590.0,2288.0,5.0,92200.0,2025-03-18,1750.0,96.0,512.0,1790.0,GDDR7,24064,188,752,128.0,300.0,110100.0,110100.0,https://www.techpowerup.com/gpu-specs/rtx-pro-6000d-blackwell-max-q.c4364,2025,440.5,estimated,1.4389937106918238
302
+ NVIDIA,Switch 2 GPU,GA10B,Console GPU(Nintendo),Ampere,561.0,1400.0,8.0,,2025-06-05,800.0,12.0,128.0,102.4,LPDDR5X,1536,12,48,4.0,40.0,8602.0,4301.0,https://www.techpowerup.com/gpu-specs/switch-2-gpu.c4313,2025,17.2,estimated,2.4955436720142603
303
+ NVIDIA,T1000 8 GB,TU117,Quadro Turing(Tx000),Turing,1065.0,1395.0,12.0,4700.0,2021-05-06,1250.0,8.0,128.0,160.0,GDDR6,896,14,0,1.0,50.0,5000.0,2500.0,https://www.techpowerup.com/gpu-specs/t1000-8-gb.c3842,2021,,unknown,1.3098591549295775
304
+ NVIDIA,TITAN RTX,TU102,GeForce 20,Turing,1350.0,1770.0,12.0,18600.0,2018-12-18,1750.0,24.0,384.0,672.0,GDDR6,4608,72,576,6.0,280.0,32619.999999999996,16309.999999999998,https://www.techpowerup.com/gpu-specs/titan-rtx.c3311,2018,130.5,estimated,1.3111111111111111
305
+ NVIDIA,TITAN V,GV100,GeForce 10,Volta,1200.0,1455.0,12.0,21100.0,2017-12-07,848.0,12.0,3072.0,651.3,HBM2,5120,80,640,4.5,250.0,29800.0,14900.0,https://www.techpowerup.com/gpu-specs/titan-v.c3051,2017,119.2,estimated,1.2125
306
+ NVIDIA,TITAN V CEO Edition,GV100,GeForce 10,Volta,1200.0,1455.0,12.0,21100.0,2018-06-21,848.0,32.0,4096.0,868.4,HBM2,5120,80,640,6.0,250.0,29800.0,14900.0,https://www.techpowerup.com/gpu-specs/titan-v-ceo-edition.c3277,2018,119.2,estimated,1.2125
307
+ NVIDIA,TITAN X Pascal,GP102,GeForce 10,Pascal,1417.0,1531.0,16.0,11800.0,2016-08-02,1251.0,12.0,384.0,480.4,GDDR5X,3584,28,0,3.0,250.0,171.5,10970.0,https://www.techpowerup.com/gpu-specs/titan-x-pascal.c2863,2016,,unknown,1.0804516584333097
308
+ NVIDIA,TITAN Xp,GP102,GeForce 10,Pascal,1405.0,1582.0,16.0,11800.0,2017-04-06,1426.0,12.0,384.0,547.6,GDDR5X,3840,30,0,3.0,250.0,189.8,12150.0,https://www.techpowerup.com/gpu-specs/titan-xp.c2948,2017,,unknown,1.1259786476868328
309
+ NVIDIA,Tesla C2070,GF100,Tesla Fermi(x20xx),Fermi,574.0,574.0,40.0,3100.0,2011-07-25,747.0,6.0,384.0,143.4,GDDR5,448,14,0,0.75,238.0,,1027.7,https://www.techpowerup.com/gpu-specs/tesla-c2070.c924,2011,,unknown,1.0
310
+ NVIDIA,Tesla C2075,GF110,Tesla Fermi(x20xx),Fermi 2.0,574.0,574.0,40.0,3000.0,2011-07-25,783.0,6.0,384.0,150.3,GDDR5,448,14,0,0.75,247.0,,1027.7,https://www.techpowerup.com/gpu-specs/tesla-c2075.c563,2011,,unknown,1.0
311
+ NVIDIA,Tesla C2090,GF110,Tesla Fermi(x20xx),Fermi 2.0,651.0,651.0,40.0,3000.0,2011-07-25,924.0,6.0,384.0,177.4,GDDR5,512,16,0,0.75,250.0,,1332.2,https://www.techpowerup.com/gpu-specs/tesla-c2090.c2317,2011,,unknown,1.0
312
+ NVIDIA,Tesla K20X,GK110,Tesla Kepler(Kxx),Kepler,732.0,732.0,28.0,7080.0,2012-11-12,1300.0,6.0,384.0,249.6,GDDR5,2688,0,0,1.5,235.0,,3935.0,https://www.techpowerup.com/gpu-specs/tesla-k20x.c2315,2012,,unknown,1.0
313
+ NVIDIA,Tesla K20Xm,GK110,Tesla Kepler(Kxx),Kepler,732.0,732.0,28.0,7080.0,2012-11-12,1300.0,6.0,384.0,249.6,GDDR5,2688,0,0,1.5,235.0,,3935.0,https://www.techpowerup.com/gpu-specs/tesla-k20xm.c1884,2012,,unknown,1.0
314
+ NVIDIA,Tesla K40c,GK180,Tesla Kepler(Kxx),Kepler,745.0,876.0,28.0,7080.0,2013-10-08,1502.0,12.0,384.0,288.4,GDDR5,2880,0,0,1.5,245.0,,5046.0,https://www.techpowerup.com/gpu-specs/tesla-k40c.c2505,2013,,unknown,1.1758389261744966
315
+ NVIDIA,Tesla K40d,GK110B,Tesla Kepler(Kxx),Kepler,745.0,876.0,28.0,7080.0,2013-11-22,1502.0,12.0,384.0,288.4,GDDR5,2880,0,0,1.5,245.0,,5046.0,https://www.techpowerup.com/gpu-specs/tesla-k40d.c3402,2013,,unknown,1.1758389261744966
316
+ NVIDIA,Tesla K40m,GK110B,Tesla Kepler(Kxx),Kepler,745.0,876.0,28.0,7080.0,2013-11-22,1502.0,12.0,384.0,288.4,GDDR5,2880,0,0,1.5,245.0,,5046.0,https://www.techpowerup.com/gpu-specs/tesla-k40m.c2529,2013,,unknown,1.1758389261744966
317
+ NVIDIA,Tesla K40s,GK110B,Tesla Kepler(Kxx),Kepler,706.0,706.0,28.0,7080.0,2013-11-22,1502.0,12.0,384.0,288.4,GDDR5,2880,0,0,1.5,245.0,,4067.0,https://www.techpowerup.com/gpu-specs/tesla-k40s.c2528,2013,,unknown,1.0
318
+ NVIDIA,Tesla K40st,GK110B,Tesla Kepler(Kxx),Kepler,575.0,575.0,28.0,7080.0,2013-11-22,1502.0,12.0,384.0,288.4,GDDR5,2880,0,0,1.5,245.0,,3312.0,https://www.techpowerup.com/gpu-specs/tesla-k40st.c2530,2013,,unknown,1.0
319
+ NVIDIA,Tesla K40t,GK110B,Tesla Kepler(Kxx),Kepler,745.0,876.0,28.0,7080.0,2013-11-22,1502.0,12.0,384.0,288.4,GDDR5,2880,0,0,1.5,245.0,,5046.0,https://www.techpowerup.com/gpu-specs/tesla-k40t.c3403,2013,,unknown,1.1758389261744966
320
+ NVIDIA,Tesla K8,GK104,Tesla Kepler(Kxx),Kepler,693.0,811.0,28.0,3540.0,2014-09-16,1250.0,8.0,256.0,160.0,GDDR5,1536,0,0,0.5,100.0,,2491.0,https://www.techpowerup.com/gpu-specs/tesla-k8.c2619,2014,,unknown,1.1702741702741704
321
+ NVIDIA,Tesla K80,GK210,Tesla Kepler(Kxx),Kepler 2.0,562.0,824.0,28.0,7100.0,2014-11-17,1253.0,12.0,384.0,240.6,GDDR5,2496,0,0,1.5,300.0,,4113.000000000001,https://www.techpowerup.com/gpu-specs/tesla-k80.c2616,2014,,unknown,1.4661921708185053
322
+ NVIDIA,Tesla M10,GM107,Tesla Maxwell(Mxx),Maxwell,1033.0,1306.0,28.0,1870.0,2016-05-18,1300.0,8.0,128.0,83.2,GDDR5,640,5,0,2.0,225.0,,1672.0,https://www.techpowerup.com/gpu-specs/tesla-m10.c3035,2016,,unknown,1.2642787996127782
323
+ NVIDIA,Tesla M2070,GF100,Tesla Fermi(x20xx),Fermi,574.0,574.0,40.0,3100.0,2011-07-25,783.0,6.0,384.0,150.3,GDDR5,448,14,0,0.75,225.0,,1030.4,https://www.techpowerup.com/gpu-specs/tesla-m2070.c1535,2011,,unknown,1.0
324
+ NVIDIA,Tesla M2070-Q,GF100,Tesla Fermi(x20xx),Fermi,574.0,574.0,40.0,3100.0,2011-07-25,783.0,6.0,384.0,150.3,GDDR5,448,14,0,0.75,225.0,,1027.7,https://www.techpowerup.com/gpu-specs/tesla-m2070-q.c1536,2011,,unknown,1.0
325
+ NVIDIA,Tesla M2075,GF110,Tesla Fermi(x20xx),Fermi 2.0,574.0,574.0,40.0,3000.0,2011-07-25,783.0,6.0,384.0,150.3,GDDR5,448,14,0,0.75,225.0,,1027.7,https://www.techpowerup.com/gpu-specs/tesla-m2075.c2025,2011,,unknown,1.0
326
+ NVIDIA,Tesla M2090,GF110,Tesla Fermi(x20xx),Fermi 2.0,651.0,651.0,40.0,3000.0,2011-07-25,924.0,6.0,384.0,177.4,GDDR5,512,16,0,0.75,250.0,,1332.2,https://www.techpowerup.com/gpu-specs/tesla-m2090.c1537,2011,,unknown,1.0
327
+ NVIDIA,Tesla M40,GM200,Tesla Maxwell(Mxx),Maxwell 2.0,948.0,1112.0,28.0,8000.0,2015-11-10,1502.0,12.0,384.0,288.4,GDDR5,3072,24,0,3.0,250.0,,6832.0,https://www.techpowerup.com/gpu-specs/tesla-m40.c2771,2015,,unknown,1.1729957805907174
328
+ NVIDIA,Tesla M40 24 GB,GM200,Tesla Maxwell(Mxx),Maxwell 2.0,948.0,1112.0,28.0,8000.0,2015-11-10,1502.0,24.0,384.0,288.4,GDDR5,3072,24,0,3.0,250.0,,6832.0,https://www.techpowerup.com/gpu-specs/tesla-m40-24-gb.c3838,2015,,unknown,1.1729957805907174
329
+ NVIDIA,Tesla M6 Mobile,GM204,Tesla Maxwell(Mxx),Maxwell 2.0,930.0,1180.0,28.0,5200.0,2015-08-30,1253.0,8.0,256.0,160.4,GDDR5,1536,12,0,2.0,100.0,,3625.0,https://www.techpowerup.com/gpu-specs/tesla-m6-mobile.c2818,2015,,unknown,1.2688172043010753
330
+ NVIDIA,Tesla M6 X2 Mobile,GM204,Tesla Maxwell(Mxx),Maxwell 2.0,930.0,1180.0,28.0,5200.0,2015-08-30,1253.0,8.0,256.0,160.4,GDDR5,1536,12,0,2.0,100.0,,3625.0,https://www.techpowerup.com/gpu-specs/tesla-m6-x2-mobile.c4123,2015,,unknown,1.2688172043010753
331
+ NVIDIA,Tesla M60,GM204,Tesla Maxwell(Mxx),Maxwell 2.0,557.0,1178.0,28.0,5200.0,2015-08-30,1253.0,8.0,256.0,160.4,GDDR5,2048,16,0,2.0,300.0,,4825.0,https://www.techpowerup.com/gpu-specs/tesla-m60.c2760,2015,,unknown,2.1149012567324954
332
+ NVIDIA,Tesla P10,GP102,Tesla Pascal(Pxx),Pascal,1025.0,1493.0,16.0,11800.0,2016-09-13,1808.0,24.0,384.0,694.3,GDDR5X,3840,30,0,3.0,150.0,179.2,11470.0,https://www.techpowerup.com/gpu-specs/tesla-p10.c3750,2016,,unknown,1.4565853658536585
333
+ NVIDIA,Tesla P100 DGXS,GP100,Tesla Pascal(Pxx),Pascal,1328.0,1480.0,16.0,15300.0,2016-04-05,715.0,16.0,4096.0,732.2,HBM2,3584,56,0,4.0,300.0,21220.0,10610.0,https://www.techpowerup.com/gpu-specs/tesla-p100-dgxs.c3285,2016,,unknown,1.1144578313253013
334
+ NVIDIA,Tesla P100 PCIe 12 GB,GP100,Tesla Pascal(Pxx),Pascal,1190.0,1329.0,16.0,15300.0,2016-06-20,715.0,12.0,3072.0,549.1,HBM2,3584,56,0,3.0,250.0,19050.0,9526.0,https://www.techpowerup.com/gpu-specs/tesla-p100-pcie-12-gb.c2915,2016,,unknown,1.1168067226890757
335
+ NVIDIA,Tesla P100 PCIe 16 GB,GP100,Tesla Pascal(Pxx),Pascal,1190.0,1329.0,16.0,15300.0,2016-06-20,715.0,16.0,4096.0,732.2,HBM2,3584,56,0,4.0,250.0,19050.0,9526.0,https://www.techpowerup.com/gpu-specs/tesla-p100-pcie-16-gb.c2888,2016,,unknown,1.1168067226890757
336
+ NVIDIA,Tesla P100 SXM2,GP100,Tesla Pascal(Pxx),Pascal,1328.0,1480.0,16.0,15300.0,2016-04-05,715.0,16.0,4096.0,732.2,HBM2,3584,56,0,4.0,300.0,21220.0,10610.0,https://www.techpowerup.com/gpu-specs/tesla-p100-sxm2.c3183,2016,,unknown,1.1144578313253013
337
+ NVIDIA,Tesla P4,GP104,Tesla Pascal(Pxx),Pascal,886.0,1114.0,16.0,7200.0,2016-09-13,1502.0,8.0,256.0,192.3,GDDR5,2560,20,0,2.0,75.0,89.12,5704.0,https://www.techpowerup.com/gpu-specs/tesla-p4.c2879,2016,,unknown,1.257336343115124
338
+ NVIDIA,Tesla P40,GP102,Tesla Pascal(Pxx),Pascal,1303.0,1531.0,16.0,11800.0,2016-09-13,1808.0,24.0,384.0,347.1,GDDR5,3840,30,0,3.0,250.0,183.7,11760.0,https://www.techpowerup.com/gpu-specs/tesla-p40.c2878,2016,,unknown,1.1749808135072908
339
+ NVIDIA,Tesla P6 Mobile,GP104,Tesla Pascal(Pxx),Pascal,1012.0,1506.0,16.0,7200.0,2017-03-24,1502.0,16.0,256.0,192.3,GDDR5,2048,16,0,2.0,90.0,96.38,6169.0,https://www.techpowerup.com/gpu-specs/tesla-p6-mobile.c3036,2017,,unknown,1.4881422924901186
340
+ NVIDIA,Tesla PG500-216,GV100,Tesla Volta(Vxx),Volta,1260.0,1380.0,12.0,21100.0,2019-11-26,1106.0,32.0,4096.0,1130.0,HBM2,5120,80,640,6.0,250.0,28260.0,14130.0,https://www.techpowerup.com/gpu-specs/tesla-pg500-216.c3791,2019,113.0,estimated,1.0952380952380953
341
+ NVIDIA,Tesla PG503-216,GV100,Tesla Volta(Vxx),Volta,1312.0,1530.0,12.0,21100.0,2019-11-26,1106.0,32.0,4096.0,1130.0,HBM2,4608,80,640,6.0,250.0,28200.0,14100.0,https://www.techpowerup.com/gpu-specs/tesla-pg503-216.c3790,2019,125.3,estimated,1.166158536585366
342
+ NVIDIA,Tesla T10 16 GB,TU102,Tesla Turing(Txx),Turing,1065.0,1395.0,12.0,18600.0,2020-01-01,1575.0,16.0,256.0,403.2,GDDR6,3584,56,448,6.0,150.0,20000.0,9999.0,https://www.techpowerup.com/gpu-specs/tesla-t10-16-gb.c4036,2020,80.0,estimated,1.3098591549295775
343
+ NVIDIA,Tesla T4,TU104,Tesla Turing(Txx),Turing,585.0,1590.0,12.0,13600.0,2018-09-13,1250.0,16.0,256.0,320.0,GDDR6,2560,40,320,4.0,70.0,65129.99999999999,8141.0,https://www.techpowerup.com/gpu-specs/tesla-t4.c3316,2018,65.0,reported,2.717948717948718
344
+ NVIDIA,Tesla T4G,TU104,Tesla Turing(Txx),Turing,585.0,1590.0,12.0,13600.0,2018-09-13,1250.0,16.0,256.0,320.0,GDDR6,2560,40,320,4.0,70.0,65129.99999999999,8141.0,https://www.techpowerup.com/gpu-specs/tesla-t4g.c4134,2018,65.1,estimated,2.717948717948718
345
+ NVIDIA,Tesla V100 DGXS 16 GB,GV100,Tesla Volta(Vxx),Volta,1354.0,1582.0,12.0,21100.0,2018-03-27,876.0,16.0,4096.0,897.0,HBM2,5120,80,640,6.0,250.0,32400.0,16200.0,https://www.techpowerup.com/gpu-specs/tesla-v100-dgxs-16-gb.c3763,2018,129.6,estimated,1.1683899556868538
346
+ NVIDIA,Tesla V100 DGXS 32 GB,GV100,Tesla Volta(Vxx),Volta,1297.0,1530.0,12.0,21100.0,2018-03-27,876.0,32.0,4096.0,897.0,HBM2,5120,80,640,6.0,250.0,31330.0,15670.0,https://www.techpowerup.com/gpu-specs/tesla-v100-dgxs-32-gb.c3186,2018,125.3,estimated,1.1796453353893601
347
+ NVIDIA,Tesla V100 FHHL,GV100,Tesla Volta(Vxx),Volta,937.0,1290.0,12.0,21100.0,2018-03-27,808.0,16.0,4096.0,827.4,HBM2,5120,80,640,6.0,250.0,26420.0,13210.0,https://www.techpowerup.com/gpu-specs/tesla-v100-fhhl.c3248,2018,105.7,estimated,1.376734258271078
348
+ NVIDIA,Tesla V100 PCIe 16 GB,GV100,Tesla Volta(Vxx),Volta,1245.0,1380.0,12.0,21100.0,2017-06-21,876.0,16.0,4096.0,897.0,HBM2,5120,80,640,6.0,300.0,28260.0,14130.0,https://www.techpowerup.com/gpu-specs/tesla-v100-pcie-16-gb.c2957,2017,112.0,reported,1.108433734939759
349
+ NVIDIA,Tesla V100 PCIe 32 GB,GV100,Tesla Volta(Vxx),Volta,1230.0,1380.0,12.0,21100.0,2018-03-27,876.0,32.0,4096.0,897.0,HBM2,5120,80,640,6.0,250.0,28260.0,14130.0,https://www.techpowerup.com/gpu-specs/tesla-v100-pcie-32-gb.c3184,2018,113.0,estimated,1.1219512195121952
350
+ NVIDIA,Tesla V100 SXM2 16 GB,GV100,Tesla Volta(Vxx),Volta,1245.0,1597.0,12.0,21100.0,2019-11-26,1106.0,16.0,4096.0,1130.0,HBM2,5120,80,640,6.0,250.0,32710.0,16350.000000000002,https://www.techpowerup.com/gpu-specs/tesla-v100-sxm2-16-gb.c3471,2019,125.0,reported,1.282730923694779
351
+ NVIDIA,Tesla V100 SXM2 32 GB,GV100,Tesla Volta(Vxx),Volta,1290.0,1530.0,12.0,21100.0,2018-03-27,877.0,32.0,4096.0,898.0,HBM2,5120,80,640,6.0,250.0,31330.0,15670.0,https://www.techpowerup.com/gpu-specs/tesla-v100-sxm2-32-gb.c3185,2018,125.0,reported,1.186046511627907
352
+ NVIDIA,Tesla V100 SXM3 32 GB,GV100,Tesla Volta(Vxx),Volta,1380.0,1597.0,12.0,21100.0,2018-03-27,958.0,32.0,4096.0,981.0,HBM2,5120,80,640,6.0,250.0,32710.0,16350.000000000002,https://www.techpowerup.com/gpu-specs/tesla-v100-sxm3-32-gb.c3472,2018,130.8,estimated,1.1572463768115941
353
+ NVIDIA,Tesla V100S PCIe 32 GB,GV100,Tesla Volta(Vxx),Volta,1245.0,1597.0,12.0,21100.0,2019-11-26,1107.0,32.0,4096.0,1130.0,HBM2,5120,80,640,6.0,250.0,32710.0,16350.000000000002,https://www.techpowerup.com/gpu-specs/tesla-v100s-pcie-32-gb.c3467,2019,130.0,reported,1.282730923694779
354
+ NVIDIA,Tesla X2070,GF100,Tesla Fermi(x20xx),Fermi,651.0,651.0,40.0,3100.0,2011-07-25,924.0,6.0,384.0,177.4,GDDR5,448,14,0,0.75,225.0,,1165.7,https://www.techpowerup.com/gpu-specs/tesla-x2070.c2024,2011,,unknown,1.0
355
+ NVIDIA,Tesla X2090,GF110,Tesla Fermi(x20xx),Fermi 2.0,651.0,651.0,40.0,3000.0,2011-07-25,924.0,6.0,384.0,177.4,GDDR5,512,16,0,0.75,225.0,,1332.2,https://www.techpowerup.com/gpu-specs/tesla-x2090.c1887,2011,,unknown,1.0
data/models/itl.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c31f2a56d528318e0462ae82b0419beb77ff20a6411009ff1bbb4186813c0792
3
+ size 238314
data/models/meta.json ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "seen_models": [
3
+ "01-ai/Yi-1.5-6B-Chat",
4
+ "01-ai/Yi-1.5-9B-Chat",
5
+ "EleutherAI/gpt-j-6b",
6
+ "EleutherAI/gpt-neox-20b",
7
+ "HuggingFaceTB/SmolLM2-1.7B-Instruct",
8
+ "Qwen/Qwen2.5-0.5B-Instruct",
9
+ "Qwen/Qwen2.5-1.5B-Instruct",
10
+ "Qwen/Qwen2.5-3B-Instruct",
11
+ "Qwen/Qwen2.5-7B-Instruct",
12
+ "Qwen/Qwen3-14B",
13
+ "allenai/OLMo-2-1124-13B-Instruct",
14
+ "allenai/OLMo-2-1124-7B-Instruct",
15
+ "berkeley-nest/Starling-LM-7B-alpha",
16
+ "deepseek-ai/deepseek-llm-7b-chat",
17
+ "google/gemma-2-2b-it",
18
+ "google/gemma-3-12b-it",
19
+ "google/gemma-3-27b-it",
20
+ "ibm-granite/granite-3.0-2b-instruct",
21
+ "ibm-granite/granite-3.0-8b-instruct",
22
+ "internlm/internlm2-7b",
23
+ "internlm/internlm2_5-20b-chat",
24
+ "meta-llama/Llama-2-13b-chat-hf",
25
+ "meta-llama/Llama-3.1-8B-Instruct",
26
+ "meta-llama/Llama-3.2-3B-Instruct",
27
+ "microsoft/Phi-3-medium-4k-instruct",
28
+ "microsoft/Phi-3-mini-4k-instruct",
29
+ "microsoft/phi-2",
30
+ "microsoft/phi-4",
31
+ "mistralai/Mistral-7B-Instruct-v0.3",
32
+ "mistralai/Mistral-Small-24B-Instruct-2501",
33
+ "mistralai/Mistral-Small-Instruct-2409",
34
+ "mlabonne/AlphaMonarch-7B",
35
+ "mlabonne/NeuralBeagle14-7B",
36
+ "nvidia/AceMath-1.5B-Instruct",
37
+ "nvidia/AceMath-7B-Instruct",
38
+ "nvidia/Llama-3.1-Minitron-4B-Width-Base",
39
+ "nvidia/Mistral-NeMo-Minitron-8B-Instruct",
40
+ "nvidia/Nemotron-Mini-4B-Instruct",
41
+ "openai-community/gpt2",
42
+ "tiiuae/Falcon3-3B-Instruct",
43
+ "upstage/SOLAR-10.7B-Instruct-v1.0",
44
+ "upstage/solar-pro-preview-instruct"
45
+ ],
46
+ "seen_gpus": [
47
+ "A100 SXM4 40 GB",
48
+ "A30 PCIe",
49
+ "H100 NVL 94 GB",
50
+ "H200 NVL",
51
+ "L4",
52
+ "L40S",
53
+ "Tesla T4",
54
+ "Tesla V100 SXM2 32 GB"
55
+ ],
56
+ "power_features": [
57
+ "scenario",
58
+ "log_total_b_params",
59
+ "model_type",
60
+ "num_key_value_heads",
61
+ "num_layers",
62
+ "memory_bandwidth_gb_s",
63
+ "boost_percentage",
64
+ "transistor_count_m",
65
+ "base_clock_mhz",
66
+ "release_year",
67
+ "bandwidth_latency"
68
+ ],
69
+ "itl_features": [
70
+ "scenario",
71
+ "model_type",
72
+ "total_b_params",
73
+ "hidden_size",
74
+ "num_attention_heads",
75
+ "num_key_value_heads",
76
+ "num_layers",
77
+ "memory_bandwidth_gb_s",
78
+ "memory_size_gb",
79
+ "memory_type",
80
+ "tflops_16b",
81
+ "release_year",
82
+ "bandwidth_latency",
83
+ "compute_latency_s"
84
+ ],
85
+ "tier_accuracy_mdape": {
86
+ "power": {
87
+ "green": {
88
+ "offline": 1.4,
89
+ "server": 5.7
90
+ },
91
+ "yellow": {
92
+ "offline": 1.9,
93
+ "server": 6.8
94
+ },
95
+ "orange": {
96
+ "offline": 3.9,
97
+ "server": 13.0
98
+ },
99
+ "red": {
100
+ "offline": 3.9,
101
+ "server": 15.1
102
+ }
103
+ },
104
+ "itl": {
105
+ "green": {
106
+ "offline": 12.7,
107
+ "server": 4.9
108
+ },
109
+ "yellow": {
110
+ "offline": 15.6,
111
+ "server": 5.6
112
+ },
113
+ "orange": {
114
+ "offline": 24.9,
115
+ "server": 8.5
116
+ },
117
+ "red": {
118
+ "offline": 36.2,
119
+ "server": 8.6
120
+ }
121
+ }
122
+ },
123
+ "log_transformed_targets": [
124
+ "itl"
125
+ ],
126
+ "n_training_runs": {
127
+ "power": 4213,
128
+ "itl": 4163
129
+ },
130
+ "gpu_tdp": {
131
+ "A30 PCIe": 165.0,
132
+ "H200 NVL": 700.0,
133
+ "A100 SXM4 40 GB": 400.0,
134
+ "L40S": 350.0,
135
+ "Tesla V100 SXM2 32 GB": 300.0,
136
+ "L4": 72.0,
137
+ "Tesla T4": 70.0
138
+ }
139
+ }
data/models/power.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:551b7e9b99ffc3f45294d968f5b09147f52259fa62098948c7650a633bfc3b14
3
+ size 771329
data/models/profiled_llms.json ADDED
@@ -0,0 +1,548 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "01-ai/Yi-1.5-6B-Chat": {
3
+ "architectures": [
4
+ "LlamaForCausalLM"
5
+ ],
6
+ "hidden_size": 4096,
7
+ "max_position_embeddings": 4096,
8
+ "model_type": "llama",
9
+ "num_attention_heads": 32,
10
+ "num_key_value_heads": 4,
11
+ "num_layers": 32,
12
+ "torch_dtype": null,
13
+ "total_b_params": 6.0
14
+ },
15
+ "01-ai/Yi-1.5-9B-Chat": {
16
+ "architectures": [
17
+ "LlamaForCausalLM"
18
+ ],
19
+ "hidden_size": 4096,
20
+ "max_position_embeddings": 4096,
21
+ "model_type": "llama",
22
+ "num_attention_heads": 32,
23
+ "num_key_value_heads": 4,
24
+ "num_layers": 48,
25
+ "torch_dtype": null,
26
+ "total_b_params": 9.0
27
+ },
28
+ "EleutherAI/gpt-j-6b": {
29
+ "architectures": [
30
+ "GPTJForCausalLM"
31
+ ],
32
+ "hidden_size": 4096,
33
+ "max_position_embeddings": null,
34
+ "model_type": "gptj",
35
+ "num_attention_heads": 16,
36
+ "num_key_value_heads": 16,
37
+ "num_layers": 28,
38
+ "torch_dtype": null,
39
+ "total_b_params": 6.05
40
+ },
41
+ "EleutherAI/gpt-neox-20b": {
42
+ "architectures": [
43
+ "GPTNeoXForCausalLM"
44
+ ],
45
+ "hidden_size": 6144,
46
+ "max_position_embeddings": 2048,
47
+ "model_type": "gpt_neox",
48
+ "num_attention_heads": 64,
49
+ "num_key_value_heads": 64,
50
+ "num_layers": 44,
51
+ "torch_dtype": null,
52
+ "total_b_params": 20.0
53
+ },
54
+ "HuggingFaceTB/SmolLM2-1.7B-Instruct": {
55
+ "architectures": [
56
+ "LlamaForCausalLM"
57
+ ],
58
+ "hidden_size": 2048,
59
+ "max_position_embeddings": 8192,
60
+ "model_type": "llama",
61
+ "num_attention_heads": 32,
62
+ "num_key_value_heads": 32,
63
+ "num_layers": 24,
64
+ "torch_dtype": null,
65
+ "total_b_params": 1.7
66
+ },
67
+ "Qwen/Qwen2.5-0.5B-Instruct": {
68
+ "architectures": [
69
+ "Qwen2ForCausalLM"
70
+ ],
71
+ "hidden_size": 896,
72
+ "max_position_embeddings": 32768,
73
+ "model_type": "qwen2",
74
+ "num_attention_heads": 14,
75
+ "num_key_value_heads": 2,
76
+ "num_layers": 24,
77
+ "torch_dtype": null,
78
+ "total_b_params": 0.49
79
+ },
80
+ "Qwen/Qwen2.5-1.5B-Instruct": {
81
+ "architectures": [
82
+ "Qwen2ForCausalLM"
83
+ ],
84
+ "hidden_size": 1536,
85
+ "max_position_embeddings": 32768,
86
+ "model_type": "qwen2",
87
+ "num_attention_heads": 12,
88
+ "num_key_value_heads": 2,
89
+ "num_layers": 28,
90
+ "torch_dtype": null,
91
+ "total_b_params": 1.54
92
+ },
93
+ "Qwen/Qwen2.5-3B-Instruct": {
94
+ "architectures": [
95
+ "Qwen2ForCausalLM"
96
+ ],
97
+ "hidden_size": 2048,
98
+ "max_position_embeddings": 32768,
99
+ "model_type": "qwen2",
100
+ "num_attention_heads": 16,
101
+ "num_key_value_heads": 2,
102
+ "num_layers": 36,
103
+ "torch_dtype": null,
104
+ "total_b_params": 3.09
105
+ },
106
+ "Qwen/Qwen2.5-7B-Instruct": {
107
+ "architectures": [
108
+ "Qwen2ForCausalLM"
109
+ ],
110
+ "hidden_size": 3584,
111
+ "max_position_embeddings": 32768,
112
+ "model_type": "qwen2",
113
+ "num_attention_heads": 28,
114
+ "num_key_value_heads": 4,
115
+ "num_layers": 28,
116
+ "torch_dtype": null,
117
+ "total_b_params": 7.62
118
+ },
119
+ "Qwen/Qwen3-14B": {
120
+ "architectures": [
121
+ "Qwen3ForCausalLM"
122
+ ],
123
+ "hidden_size": 5120,
124
+ "max_position_embeddings": 40960,
125
+ "model_type": "qwen3",
126
+ "num_attention_heads": 40,
127
+ "num_key_value_heads": 8,
128
+ "num_layers": 40,
129
+ "torch_dtype": null,
130
+ "total_b_params": 14.0
131
+ },
132
+ "allenai/OLMo-2-1124-13B-Instruct": {
133
+ "architectures": [
134
+ "Olmo2ForCausalLM"
135
+ ],
136
+ "hidden_size": 5120,
137
+ "max_position_embeddings": 4096,
138
+ "model_type": "olmo2",
139
+ "num_attention_heads": 40,
140
+ "num_key_value_heads": 40,
141
+ "num_layers": 40,
142
+ "torch_dtype": null,
143
+ "total_b_params": 14.0
144
+ },
145
+ "allenai/OLMo-2-1124-7B-Instruct": {
146
+ "architectures": [
147
+ "Olmo2ForCausalLM"
148
+ ],
149
+ "hidden_size": 4096,
150
+ "max_position_embeddings": 4096,
151
+ "model_type": "olmo2",
152
+ "num_attention_heads": 32,
153
+ "num_key_value_heads": 32,
154
+ "num_layers": 32,
155
+ "torch_dtype": null,
156
+ "total_b_params": 7.0
157
+ },
158
+ "berkeley-nest/Starling-LM-7B-alpha": {
159
+ "architectures": [
160
+ "MistralForCausalLM"
161
+ ],
162
+ "hidden_size": 4096,
163
+ "max_position_embeddings": 8192,
164
+ "model_type": "mistral",
165
+ "num_attention_heads": 32,
166
+ "num_key_value_heads": 8,
167
+ "num_layers": 32,
168
+ "torch_dtype": null,
169
+ "total_b_params": 7.0
170
+ },
171
+ "deepseek-ai/deepseek-llm-7b-chat": {
172
+ "architectures": [
173
+ "LlamaForCausalLM"
174
+ ],
175
+ "hidden_size": 4096,
176
+ "max_position_embeddings": 4096,
177
+ "model_type": "llama",
178
+ "num_attention_heads": 32,
179
+ "num_key_value_heads": 32,
180
+ "num_layers": 30,
181
+ "torch_dtype": null,
182
+ "total_b_params": 7.0
183
+ },
184
+ "google/gemma-2-2b-it": {
185
+ "architectures": [
186
+ "Gemma2ForCausalLM"
187
+ ],
188
+ "hidden_size": 2304,
189
+ "max_position_embeddings": 8192,
190
+ "model_type": "gemma2",
191
+ "num_attention_heads": 8,
192
+ "num_key_value_heads": 4,
193
+ "num_layers": 26,
194
+ "torch_dtype": null,
195
+ "total_b_params": 2.61
196
+ },
197
+ "google/gemma-3-12b-it": {
198
+ "architectures": [
199
+ "Gemma3ForConditionalGeneration"
200
+ ],
201
+ "hidden_size": 3840,
202
+ "max_position_embeddings": null,
203
+ "model_type": "gemma3",
204
+ "num_attention_heads": 16,
205
+ "num_key_value_heads": 8,
206
+ "num_layers": 48,
207
+ "torch_dtype": null,
208
+ "total_b_params": 12.0
209
+ },
210
+ "google/gemma-3-27b-it": {
211
+ "architectures": [
212
+ "Gemma3ForConditionalGeneration"
213
+ ],
214
+ "hidden_size": 5376,
215
+ "max_position_embeddings": null,
216
+ "model_type": "gemma3",
217
+ "num_attention_heads": 32,
218
+ "num_key_value_heads": 16,
219
+ "num_layers": 62,
220
+ "torch_dtype": null,
221
+ "total_b_params": 27.0
222
+ },
223
+ "ibm-granite/granite-3.0-2b-instruct": {
224
+ "architectures": [
225
+ "GraniteForCausalLM"
226
+ ],
227
+ "hidden_size": 2048,
228
+ "max_position_embeddings": 4096,
229
+ "model_type": "granite",
230
+ "num_attention_heads": 32,
231
+ "num_key_value_heads": 8,
232
+ "num_layers": 40,
233
+ "torch_dtype": null,
234
+ "total_b_params": 2.0
235
+ },
236
+ "ibm-granite/granite-3.0-8b-instruct": {
237
+ "architectures": [
238
+ "GraniteForCausalLM"
239
+ ],
240
+ "hidden_size": 4096,
241
+ "max_position_embeddings": 4096,
242
+ "model_type": "granite",
243
+ "num_attention_heads": 32,
244
+ "num_key_value_heads": 8,
245
+ "num_layers": 40,
246
+ "torch_dtype": null,
247
+ "total_b_params": 8.0
248
+ },
249
+ "internlm/internlm2-7b": {
250
+ "architectures": [
251
+ "InternLM2ForCausalLM"
252
+ ],
253
+ "hidden_size": 4096,
254
+ "max_position_embeddings": 32768,
255
+ "model_type": "internlm2",
256
+ "num_attention_heads": 32,
257
+ "num_key_value_heads": 8,
258
+ "num_layers": 32,
259
+ "torch_dtype": null,
260
+ "total_b_params": 7.0
261
+ },
262
+ "internlm/internlm2_5-20b-chat": {
263
+ "architectures": [
264
+ "InternLM2ForCausalLM"
265
+ ],
266
+ "hidden_size": 6144,
267
+ "max_position_embeddings": 32768,
268
+ "model_type": "internlm2",
269
+ "num_attention_heads": 48,
270
+ "num_key_value_heads": 8,
271
+ "num_layers": 48,
272
+ "torch_dtype": null,
273
+ "total_b_params": 20.0
274
+ },
275
+ "meta-llama/Llama-2-13b-chat-hf": {
276
+ "architectures": [
277
+ "LlamaForCausalLM"
278
+ ],
279
+ "hidden_size": 5120,
280
+ "max_position_embeddings": 4096,
281
+ "model_type": "llama",
282
+ "num_attention_heads": 40,
283
+ "num_key_value_heads": 40,
284
+ "num_layers": 40,
285
+ "torch_dtype": null,
286
+ "total_b_params": 13.0
287
+ },
288
+ "meta-llama/Llama-3.1-8B-Instruct": {
289
+ "architectures": [
290
+ "LlamaForCausalLM"
291
+ ],
292
+ "hidden_size": 4096,
293
+ "max_position_embeddings": 131072,
294
+ "model_type": "llama",
295
+ "num_attention_heads": 32,
296
+ "num_key_value_heads": 8,
297
+ "num_layers": 32,
298
+ "torch_dtype": null,
299
+ "total_b_params": 8.03
300
+ },
301
+ "meta-llama/Llama-3.2-3B-Instruct": {
302
+ "architectures": [
303
+ "LlamaForCausalLM"
304
+ ],
305
+ "hidden_size": 3072,
306
+ "max_position_embeddings": 131072,
307
+ "model_type": "llama",
308
+ "num_attention_heads": 24,
309
+ "num_key_value_heads": 8,
310
+ "num_layers": 28,
311
+ "torch_dtype": null,
312
+ "total_b_params": 3.21
313
+ },
314
+ "microsoft/Phi-3-medium-4k-instruct": {
315
+ "architectures": [
316
+ "Phi3ForCausalLM"
317
+ ],
318
+ "hidden_size": 5120,
319
+ "max_position_embeddings": 4096,
320
+ "model_type": "phi3",
321
+ "num_attention_heads": 40,
322
+ "num_key_value_heads": 10,
323
+ "num_layers": 40,
324
+ "torch_dtype": null,
325
+ "total_b_params": 14.0
326
+ },
327
+ "microsoft/Phi-3-mini-4k-instruct": {
328
+ "architectures": [
329
+ "Phi3ForCausalLM"
330
+ ],
331
+ "hidden_size": 3072,
332
+ "max_position_embeddings": 4096,
333
+ "model_type": "phi3",
334
+ "num_attention_heads": 32,
335
+ "num_key_value_heads": 32,
336
+ "num_layers": 32,
337
+ "torch_dtype": null,
338
+ "total_b_params": 3.8
339
+ },
340
+ "microsoft/phi-2": {
341
+ "architectures": [
342
+ "PhiForCausalLM"
343
+ ],
344
+ "hidden_size": 2560,
345
+ "max_position_embeddings": 2048,
346
+ "model_type": "phi",
347
+ "num_attention_heads": 32,
348
+ "num_key_value_heads": 32,
349
+ "num_layers": 32,
350
+ "torch_dtype": null,
351
+ "total_b_params": 2.7
352
+ },
353
+ "microsoft/phi-4": {
354
+ "architectures": [
355
+ "Phi3ForCausalLM"
356
+ ],
357
+ "hidden_size": 5120,
358
+ "max_position_embeddings": 16384,
359
+ "model_type": "phi3",
360
+ "num_attention_heads": 40,
361
+ "num_key_value_heads": 10,
362
+ "num_layers": 40,
363
+ "torch_dtype": null,
364
+ "total_b_params": 14.7
365
+ },
366
+ "mistralai/Mistral-7B-Instruct-v0.3": {
367
+ "architectures": [
368
+ "MistralForCausalLM"
369
+ ],
370
+ "hidden_size": 4096,
371
+ "max_position_embeddings": 32768,
372
+ "model_type": "mistral",
373
+ "num_attention_heads": 32,
374
+ "num_key_value_heads": 8,
375
+ "num_layers": 32,
376
+ "torch_dtype": null,
377
+ "total_b_params": 7.25
378
+ },
379
+ "mistralai/Mistral-Small-24B-Instruct-2501": {
380
+ "architectures": [
381
+ "MistralForCausalLM"
382
+ ],
383
+ "hidden_size": 5120,
384
+ "max_position_embeddings": 32768,
385
+ "model_type": "mistral",
386
+ "num_attention_heads": 32,
387
+ "num_key_value_heads": 8,
388
+ "num_layers": 40,
389
+ "torch_dtype": null,
390
+ "total_b_params": 24.0
391
+ },
392
+ "mistralai/Mistral-Small-Instruct-2409": {
393
+ "architectures": [
394
+ "MistralForCausalLM"
395
+ ],
396
+ "hidden_size": 6144,
397
+ "max_position_embeddings": 32768,
398
+ "model_type": "mistral",
399
+ "num_attention_heads": 48,
400
+ "num_key_value_heads": 8,
401
+ "num_layers": 56,
402
+ "torch_dtype": null,
403
+ "total_b_params": 24.0
404
+ },
405
+ "mlabonne/AlphaMonarch-7B": {
406
+ "architectures": [
407
+ "MistralForCausalLM"
408
+ ],
409
+ "hidden_size": 4096,
410
+ "max_position_embeddings": 32768,
411
+ "model_type": "mistral",
412
+ "num_attention_heads": 32,
413
+ "num_key_value_heads": 8,
414
+ "num_layers": 32,
415
+ "torch_dtype": null,
416
+ "total_b_params": 7.0
417
+ },
418
+ "mlabonne/NeuralBeagle14-7B": {
419
+ "architectures": [
420
+ "MistralForCausalLM"
421
+ ],
422
+ "hidden_size": 4096,
423
+ "max_position_embeddings": 32768,
424
+ "model_type": "mistral",
425
+ "num_attention_heads": 32,
426
+ "num_key_value_heads": 8,
427
+ "num_layers": 32,
428
+ "torch_dtype": null,
429
+ "total_b_params": 7.0
430
+ },
431
+ "nvidia/AceMath-1.5B-Instruct": {
432
+ "architectures": [
433
+ "Qwen2ForCausalLM"
434
+ ],
435
+ "hidden_size": 1536,
436
+ "max_position_embeddings": 4096,
437
+ "model_type": "qwen2",
438
+ "num_attention_heads": 12,
439
+ "num_key_value_heads": 2,
440
+ "num_layers": 28,
441
+ "torch_dtype": null,
442
+ "total_b_params": 1.5
443
+ },
444
+ "nvidia/AceMath-7B-Instruct": {
445
+ "architectures": [
446
+ "Qwen2ForCausalLM"
447
+ ],
448
+ "hidden_size": 3584,
449
+ "max_position_embeddings": 4096,
450
+ "model_type": "qwen2",
451
+ "num_attention_heads": 28,
452
+ "num_key_value_heads": 4,
453
+ "num_layers": 28,
454
+ "torch_dtype": null,
455
+ "total_b_params": 7.0
456
+ },
457
+ "nvidia/Llama-3.1-Minitron-4B-Width-Base": {
458
+ "architectures": [
459
+ "LlamaForCausalLM"
460
+ ],
461
+ "hidden_size": 3072,
462
+ "max_position_embeddings": 131072,
463
+ "model_type": "llama",
464
+ "num_attention_heads": 32,
465
+ "num_key_value_heads": 8,
466
+ "num_layers": 32,
467
+ "torch_dtype": null,
468
+ "total_b_params": 5.0
469
+ },
470
+ "nvidia/Mistral-NeMo-Minitron-8B-Instruct": {
471
+ "architectures": [
472
+ "MistralForCausalLM"
473
+ ],
474
+ "hidden_size": 4096,
475
+ "max_position_embeddings": 8192,
476
+ "model_type": "mistral",
477
+ "num_attention_heads": 32,
478
+ "num_key_value_heads": 8,
479
+ "num_layers": 40,
480
+ "torch_dtype": null,
481
+ "total_b_params": 8.0
482
+ },
483
+ "nvidia/Nemotron-Mini-4B-Instruct": {
484
+ "architectures": [
485
+ "NemotronForCausalLM"
486
+ ],
487
+ "hidden_size": 3072,
488
+ "max_position_embeddings": 4096,
489
+ "model_type": "nemotron",
490
+ "num_attention_heads": 24,
491
+ "num_key_value_heads": 8,
492
+ "num_layers": 32,
493
+ "torch_dtype": null,
494
+ "total_b_params": 4.0
495
+ },
496
+ "openai-community/gpt2": {
497
+ "architectures": [
498
+ "GPT2LMHeadModel"
499
+ ],
500
+ "hidden_size": 768,
501
+ "max_position_embeddings": null,
502
+ "model_type": "gpt2",
503
+ "num_attention_heads": 12,
504
+ "num_key_value_heads": 12,
505
+ "num_layers": 12,
506
+ "torch_dtype": null,
507
+ "total_b_params": 0.124
508
+ },
509
+ "tiiuae/Falcon3-3B-Instruct": {
510
+ "architectures": [
511
+ "LlamaForCausalLM"
512
+ ],
513
+ "hidden_size": 3072,
514
+ "max_position_embeddings": 32768,
515
+ "model_type": "llama",
516
+ "num_attention_heads": 12,
517
+ "num_key_value_heads": 4,
518
+ "num_layers": 22,
519
+ "torch_dtype": null,
520
+ "total_b_params": 3.0
521
+ },
522
+ "upstage/SOLAR-10.7B-Instruct-v1.0": {
523
+ "architectures": [
524
+ "LlamaForCausalLM"
525
+ ],
526
+ "hidden_size": 4096,
527
+ "max_position_embeddings": 4096,
528
+ "model_type": "llama",
529
+ "num_attention_heads": 32,
530
+ "num_key_value_heads": 8,
531
+ "num_layers": 48,
532
+ "torch_dtype": null,
533
+ "total_b_params": 10.7
534
+ },
535
+ "upstage/solar-pro-preview-instruct": {
536
+ "architectures": [
537
+ "SolarForCausalLM"
538
+ ],
539
+ "hidden_size": 5120,
540
+ "max_position_embeddings": 4096,
541
+ "model_type": "solar",
542
+ "num_attention_heads": 40,
543
+ "num_key_value_heads": 10,
544
+ "num_layers": 64,
545
+ "torch_dtype": null,
546
+ "total_b_params": 22.1
547
+ }
548
+ }
pytest.ini ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ [pytest]
2
+ testpaths = tests
3
+ markers =
4
+ network: reaches the Hugging Face Hub
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Runtime
2
+ gradio==5.49.1
3
+ numpy==2.2.6
4
+ pandas==2.3.1
5
+ scikit-learn==1.7.1
6
+ scipy==1.16.2
7
+ xgboost==3.0.4
8
+ joblib==1.5.2
9
+
10
+ # Build-time only: scripts/build_gpu_db.py regenerates data/gpu_database.csv.
11
+ # The app itself reads the generated CSV and never imports dbgpu.
12
+ dbgpu==2025.12
scripts/build_gpu_db.py ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build the demo's GPU database from dbgpu (TechPowerUp specs).
2
+
3
+ The WattGPU models need a handful of GPU specifications per prediction:
4
+ memory bandwidth, memory size, memory type, clocks, transistor count,
5
+ release year, TDP, and peak dense FP16 tensor throughput.
6
+
7
+ Everything except the tensor throughput comes straight out of `dbgpu`, which
8
+ is the same source the paper's `data/gpu_features.csv` was generated from.
9
+ TechPowerUp does not publish tensor-core throughput, so this script fills
10
+ `tensor_tflops_16b` from, in order of preference:
11
+
12
+ 1. the curated values already in the paper's `data/gpu_features.csv`,
13
+ 2. a curated table of manufacturer-reported figures for common
14
+ accelerators (`CURATED_TENSOR_TFLOPS`),
15
+ 3. an architecture-based estimate,
16
+ tensor_cores * boost_clock * FLOPs-per-tensor-core-per-cycle,
17
+ which reproduces the manufacturer figures for the GPUs in (1)-(2) to
18
+ within ~15%.
19
+
20
+ Rows for which no throughput can be established at all are still kept: the
21
+ power model does not use it, and the ITL model reports the gap to the user.
22
+
23
+ Usage: python scripts/build_gpu_db.py [--out data/gpu_database.csv]
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+ import os
30
+ import sys
31
+
32
+ import pandas as pd
33
+
34
+ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
35
+ PAPER_DATA = os.path.join(os.path.dirname(REPO_ROOT), "data")
36
+ PAPER_GPU_FEATURES = os.path.join(PAPER_DATA, "gpu_features.csv")
37
+ PAPER_MEASUREMENTS = os.path.join(PAPER_DATA, "watt_counts_subset.csv")
38
+
39
+ # Columns the demo keeps from dbgpu. A superset of what the two models use, so
40
+ # the UI can show a spec sheet alongside the prediction.
41
+ KEPT_COLUMNS = [
42
+ "manufacturer",
43
+ "name",
44
+ "gpu_name",
45
+ "generation",
46
+ "architecture",
47
+ "base_clock_mhz",
48
+ "boost_clock_mhz",
49
+ "process_size_nm",
50
+ "transistor_count_m",
51
+ "release_date",
52
+ "memory_clock_mhz",
53
+ "memory_size_gb",
54
+ "memory_bus_bits",
55
+ "memory_bandwidth_gb_s",
56
+ "memory_type",
57
+ "shading_units",
58
+ "streaming_multiprocessors",
59
+ "tensor_cores",
60
+ "l2_cache_mb",
61
+ "thermal_design_power_w",
62
+ "half_float_performance_gflop_s",
63
+ "single_float_performance_gflop_s",
64
+ "tpu_url",
65
+ ]
66
+
67
+ # Manufacturer-reported peak dense FP16 tensor throughput (TFLOP/s, no
68
+ # sparsity). Sources: NVIDIA datasheets and AMD Instinct product briefs.
69
+ CURATED_TENSOR_TFLOPS = {
70
+ # NVIDIA data centre
71
+ "Tesla V100 PCIe 16 GB": 112,
72
+ "Tesla V100 SXM2 16 GB": 125,
73
+ "Tesla V100 SXM2 32 GB": 125,
74
+ "Tesla V100S PCIe 32 GB": 130,
75
+ "Tesla T4": 65,
76
+ "A2 PCIe": 36,
77
+ "A10 PCIe": 125,
78
+ "A10G": 70,
79
+ "A16 PCIe": 71,
80
+ "A30 PCIe": 165,
81
+ "A40 PCIe": 150,
82
+ "A100 PCIe 40 GB": 312,
83
+ "A100 PCIe 80 GB": 312,
84
+ "A100 SXM4 40 GB": 312,
85
+ "A100 SXM4 80 GB": 312,
86
+ "L4": 121,
87
+ "L40": 181,
88
+ "L40S": 362,
89
+ "H100 PCIe 80 GB": 756,
90
+ "H100 SXM5 80 GB": 989,
91
+ "H100 SXM5 96 GB": 989,
92
+ "H100 NVL 94 GB": 835,
93
+ "H200 SXM 141 GB": 989,
94
+ "H200 NVL": 835,
95
+ "B200 SXM 180 GB": 2250,
96
+ "RTX 6000 Ada Generation": 364,
97
+ "RTX 5000 Ada Generation": 262,
98
+ "RTX A6000": 155,
99
+ "RTX A5000": 111,
100
+ }
101
+
102
+ # Only NVIDIA parts are kept. Every measurement behind WattGPU ran on NVIDIA
103
+ # hardware under vLLM with CUDA, and the two strongest hardware features the
104
+ # models use -- memory bandwidth and FP16 tensor throughput -- mean different
105
+ # things on other vendors' matrix engines. Estimating for AMD or Intel would be
106
+ # extrapolating across an architectural boundary the training data never crosses.
107
+ KEPT_MANUFACTURERS = ("NVIDIA",)
108
+
109
+ # Dense FP16 tensor FLOPs per tensor core per clock cycle, by architecture.
110
+ # Consumer parts use the FP16-with-FP16-accumulate rate, matching how the
111
+ # paper's `gpu_features.csv` reports RTX cards.
112
+ FLOPS_PER_TENSOR_CORE_PER_CYCLE = {
113
+ "Volta": 128,
114
+ "Turing": 128,
115
+ "Ampere": 256,
116
+ "Ada Lovelace": 256,
117
+ "Hopper": 1024,
118
+ "Blackwell": 256,
119
+ "Blackwell 2.0": 256,
120
+ }
121
+
122
+ # GA100 (A100/A30) doubles the per-core rate of consumer Ampere.
123
+ DATACENTRE_AMPERE_CHIPS = {"GA100"}
124
+
125
+
126
+ def _estimate_tensor_tflops(row: pd.Series) -> float | None:
127
+ """Architecture-based estimate of dense FP16 tensor throughput."""
128
+ cores = row.get("tensor_cores")
129
+ clock = row.get("boost_clock_mhz")
130
+ arch = row.get("architecture")
131
+
132
+ if not cores or pd.isna(cores) or float(cores) <= 0:
133
+ return None
134
+ if not clock or pd.isna(clock):
135
+ return None
136
+
137
+ per_cycle = FLOPS_PER_TENSOR_CORE_PER_CYCLE.get(arch)
138
+ if per_cycle is None:
139
+ return None
140
+ if arch == "Ampere" and str(row.get("gpu_name")) in DATACENTRE_AMPERE_CHIPS:
141
+ per_cycle = 512
142
+
143
+ return round(float(cores) * float(clock) * 1e6 * per_cycle / 1e12, 1)
144
+
145
+
146
+ def _paper_tensor_tflops() -> dict[str, float]:
147
+ """Tensor throughput for the GPUs the models were actually trained on.
148
+
149
+ Restricted to the profiled GPUs so the rest of the database stays on a
150
+ single convention (dense FP16 with FP16 accumulate). The paper's file also
151
+ lists consumer cards, but with the FP32-accumulate rate, which would be
152
+ inconsistent with the estimate used for every other consumer part.
153
+ """
154
+ if not (os.path.exists(PAPER_GPU_FEATURES) and os.path.exists(PAPER_MEASUREMENTS)):
155
+ print("note: paper data not found, skipping profiled-GPU overrides")
156
+ return {}
157
+
158
+ profiled = set(pd.read_csv(PAPER_MEASUREMENTS, usecols=["gpu_type"])["gpu_type"])
159
+ paper = pd.read_csv(PAPER_GPU_FEATURES, sep=";")
160
+ paper = paper[paper["gpu_type"].isin(profiled)]
161
+ paper = paper.dropna(subset=["gpu_db_name", "tensor_tflops"])
162
+ return dict(zip(paper["gpu_db_name"], paper["tensor_tflops"].astype(float)))
163
+
164
+
165
+ def build(min_memory_gb: float = 6.0) -> pd.DataFrame:
166
+ from dbgpu import GPUDatabase
167
+
168
+ df = GPUDatabase.default().dataframe
169
+ print(f"dbgpu: {len(df)} GPU specifications")
170
+
171
+ df = df[[c for c in KEPT_COLUMNS if c in df.columns]].copy()
172
+
173
+ # Only GPUs that could plausibly serve an LLM: enough memory to hold
174
+ # weights, and a known memory bandwidth (the single strongest feature in
175
+ # both models).
176
+ df = df[df["manufacturer"].isin(KEPT_MANUFACTURERS)]
177
+ print(f"after restricting to {', '.join(KEPT_MANUFACTURERS)}: {len(df)}")
178
+
179
+ df = df[df["memory_bandwidth_gb_s"].notna()]
180
+ df = df[df["memory_size_gb"].fillna(0) >= min_memory_gb]
181
+ df = df[df["thermal_design_power_w"].notna()]
182
+ print(f"after filtering to LLM-capable parts: {len(df)}")
183
+
184
+ df["release_date"] = pd.to_datetime(df["release_date"], errors="coerce")
185
+ df["release_year"] = df["release_date"].dt.year
186
+
187
+ # `gpu_db_name` is the join key used by the paper's data files.
188
+ df = df.rename(columns={"name": "gpu_db_name"})
189
+
190
+ # Profiled GPUs take precedence: their values are the ones the models were
191
+ # trained against.
192
+ overrides = {**CURATED_TENSOR_TFLOPS, **_paper_tensor_tflops()}
193
+ df["tensor_tflops_16b"] = df["gpu_db_name"].map(overrides)
194
+ estimated = df.apply(_estimate_tensor_tflops, axis=1)
195
+ df["tensor_tflops_source"] = "unknown"
196
+ df.loc[estimated.notna(), "tensor_tflops_source"] = "estimated"
197
+ df.loc[df["tensor_tflops_16b"].notna(), "tensor_tflops_source"] = "reported"
198
+ df["tensor_tflops_16b"] = df["tensor_tflops_16b"].fillna(estimated)
199
+
200
+ df["boost_percentage"] = df["boost_clock_mhz"] / df["base_clock_mhz"]
201
+
202
+ df = df.sort_values(["manufacturer", "gpu_db_name"]).reset_index(drop=True)
203
+ print(df["tensor_tflops_source"].value_counts().to_string())
204
+ return df
205
+
206
+
207
+ def main() -> int:
208
+ parser = argparse.ArgumentParser(description=__doc__)
209
+ parser.add_argument("--out", default=os.path.join(REPO_ROOT, "data", "gpu_database.csv"))
210
+ parser.add_argument("--min-memory-gb", type=float, default=6.0)
211
+ args = parser.parse_args()
212
+
213
+ df = build(min_memory_gb=args.min_memory_gb)
214
+ os.makedirs(os.path.dirname(args.out), exist_ok=True)
215
+ df.to_csv(args.out, index=False)
216
+ print(f"wrote {len(df)} GPUs to {args.out}")
217
+ return 0
218
+
219
+
220
+ if __name__ == "__main__":
221
+ sys.exit(main())
scripts/cache_gated_models.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cache the architecture of licence-gated models the demo cannot read.
2
+
3
+ Some of the best-known LLMs -- Llama and Gemma -- are licence-gated on the
4
+ Hugging Face Hub, so an anonymous demo cannot read their `config.json` and has
5
+ to refuse them. Serving them with a maintainer's own token would work, but it
6
+ would mean using one person's licence acceptance on behalf of every visitor.
7
+
8
+ Instead this script reads the architecture from a public, unquantised mirror of
9
+ the same weights and caches the handful of integers WattGPU actually needs.
10
+ Those numbers -- layer count, hidden size, head counts, parameter count -- are
11
+ published in the model cards and papers, so caching them redistributes nothing.
12
+
13
+ Each entry records the mirror it came from, so any value can be traced and
14
+ re-checked. Run this again to refresh:
15
+
16
+ python scripts/cache_gated_models.py
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import json
23
+ import os
24
+ import sys
25
+
26
+ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
27
+ sys.path.insert(0, REPO_ROOT)
28
+
29
+ from wattgpu_demo.hf_models import ( # noqa: E402
30
+ HF_ENDPOINT,
31
+ _get_json,
32
+ detect_quantisation,
33
+ llm_info_from_config,
34
+ )
35
+
36
+ # Gated model -> a public repository holding the same architecture at full
37
+ # precision. Mirrors are checked for quantisation before anything is cached.
38
+ MIRRORS = {
39
+ "meta-llama/Llama-3.2-1B": "unsloth/Llama-3.2-1B",
40
+ "meta-llama/Llama-3.2-1B-Instruct": "unsloth/Llama-3.2-1B-Instruct",
41
+ "meta-llama/Meta-Llama-3-8B-Instruct": "NousResearch/Meta-Llama-3-8B-Instruct",
42
+ "google/gemma-3-1b-it": "unsloth/gemma-3-1b-it",
43
+ "google/gemma-3-270m": "unsloth/gemma-3-270m",
44
+ }
45
+
46
+ DEFAULT_OUT = os.path.join(REPO_ROOT, "data", "gated_llms.json")
47
+
48
+
49
+ def fetch_entry(canonical: str, mirror: str) -> dict:
50
+ config = _get_json(f"{HF_ENDPOINT}/{mirror}/raw/main/config.json", mirror)
51
+
52
+ quantisation = detect_quantisation(mirror, config)
53
+ if quantisation:
54
+ raise ValueError(
55
+ f"{mirror} is quantised ({quantisation}); it cannot stand in for {canonical}")
56
+
57
+ info = _get_json(f"{HF_ENDPOINT}/api/models/{mirror}", mirror)
58
+ total = ((info or {}).get("safetensors") or {}).get("total")
59
+ if not isinstance(total, (int, float)) or total <= 0:
60
+ raise ValueError(f"{mirror} publishes no safetensors index")
61
+
62
+ # Reuse the same adapter the live path uses, so a cached model and a
63
+ # fetched one go through identical validation.
64
+ llm = llm_info_from_config(canonical, config, float(total) / 1e9, "published config")
65
+ return {
66
+ "model_type": llm.model_type,
67
+ "num_layers": llm.num_layers,
68
+ "hidden_size": llm.hidden_size,
69
+ "num_attention_heads": llm.num_attention_heads,
70
+ "num_key_value_heads": llm.num_key_value_heads,
71
+ "total_b_params": round(llm.total_b_params, 6),
72
+ "architectures": llm.architectures,
73
+ "max_position_embeddings": llm.max_position_embeddings,
74
+ "torch_dtype": llm.torch_dtype,
75
+ "source_mirror": mirror,
76
+ }
77
+
78
+
79
+ def main() -> int:
80
+ parser = argparse.ArgumentParser(description=__doc__)
81
+ parser.add_argument("--out", default=DEFAULT_OUT)
82
+ args = parser.parse_args()
83
+
84
+ entries: dict[str, dict] = {}
85
+ for canonical, mirror in MIRRORS.items():
86
+ try:
87
+ entries[canonical] = fetch_entry(canonical, mirror)
88
+ e = entries[canonical]
89
+ print(f" {canonical}: {e['total_b_params']:.3f} B, "
90
+ f"{e['num_layers']} layers, via {mirror}")
91
+ except Exception as exc: # noqa: BLE001 - report and keep going
92
+ print(f" SKIPPED {canonical}: {exc}")
93
+
94
+ os.makedirs(os.path.dirname(args.out), exist_ok=True)
95
+ with open(args.out, "w") as fh:
96
+ json.dump(entries, fh, indent=2, sort_keys=True)
97
+ print(f"\nwrote {len(entries)} gated-model architectures to {args.out}")
98
+ return 0 if entries else 1
99
+
100
+
101
+ if __name__ == "__main__":
102
+ sys.exit(main())
scripts/train_models.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fit the WattGPU power and ITL models and save them for the demo.
2
+
3
+ Two things are produced:
4
+
5
+ 1. `power.joblib` / `itl.joblib` — the pipelines from the paper, refitted on
6
+ the full Watt Counts subset.
7
+ 2. `meta.json` — the sets of profiled LLMs and GPUs (which decide the demo's
8
+ certainty tier), plus the accuracy each tier can be expected to deliver.
9
+
10
+ The tier accuracies come from the paper's own validation protocols, so the
11
+ number shown next to a prediction is measured under exactly the conditions
12
+ that prediction is made in:
13
+
14
+ green (seen LLM, seen GPU) -> 5-fold grouped CV
15
+ yellow (unseen LLM, seen GPU) -> leave-one-LLM-out
16
+ orange (seen LLM, unseen GPU) -> leave-one-GPU-out
17
+
18
+ Usage: python scripts/train_models.py
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import ast
25
+ import json
26
+ import os
27
+ import sys
28
+
29
+ import joblib
30
+ import numpy as np
31
+ import pandas as pd
32
+ from sklearn.compose import ColumnTransformer
33
+ from sklearn.impute import SimpleImputer
34
+ from sklearn.model_selection import GroupKFold, LeaveOneGroupOut
35
+ from sklearn.pipeline import Pipeline
36
+ from sklearn.preprocessing import OrdinalEncoder, StandardScaler
37
+ from xgboost import XGBRegressor
38
+
39
+ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
40
+ sys.path.insert(0, REPO_ROOT)
41
+
42
+ from wattgpu_demo.features import ( # noqa: E402
43
+ ID_COLUMNS,
44
+ ITL_FEATURES,
45
+ ITL_TARGET,
46
+ POWER_FEATURES,
47
+ POWER_TARGET,
48
+ build_training_frame,
49
+ clean_frame,
50
+ )
51
+
52
+ PAPER_DATA_DIR = os.path.join(os.path.dirname(REPO_ROOT), "data")
53
+
54
+ # Hyperparameters as reported in the paper.
55
+ POWER_REGRESSOR = dict(max_depth=6, reg_lambda=150, n_estimators=200)
56
+ ITL_REGRESSOR = dict(max_depth=5, reg_lambda=100, n_estimators=100)
57
+
58
+
59
+ def build_pipeline(X: pd.DataFrame, regressor) -> Pipeline:
60
+ """Preprocessing + regressor, identical to the notebook's `_build_pipeline`."""
61
+ numeric = X.select_dtypes(include=[np.number]).columns.tolist()
62
+ categorical = X.select_dtypes(include=["object", "category"]).columns.tolist()
63
+
64
+ preprocessor = ColumnTransformer([
65
+ ("num", Pipeline([
66
+ ("imputer", SimpleImputer(strategy="mean")),
67
+ ("scaler", StandardScaler()),
68
+ ]), numeric),
69
+ ("cat", Pipeline([
70
+ ("imputer", SimpleImputer(strategy="most_frequent")),
71
+ # Unseen categories (a new `model_type` or `memory_type`) encode to
72
+ # -1 rather than raising, which is what lets the demo predict for
73
+ # architectures that were never profiled.
74
+ ("encoder", OrdinalEncoder(handle_unknown="use_encoded_value", unknown_value=-1)),
75
+ ]), categorical),
76
+ ])
77
+ return Pipeline([("preprocessor", preprocessor), ("regressor", regressor)])
78
+
79
+
80
+ def mdape(y_true: np.ndarray, y_pred: np.ndarray) -> float:
81
+ """Median absolute percentage error, the paper's headline metric."""
82
+ return float(np.median(np.abs((y_true - y_pred) / y_true)) * 100)
83
+
84
+
85
+ def cross_validate(df: pd.DataFrame, group_col: str | None, target_col: str,
86
+ regressor_kwargs: dict, log_transform_y: bool) -> np.ndarray:
87
+ """Out-of-fold predictions under CV, LOGO or LOLO."""
88
+ X = df.drop(columns=[c for c in [target_col, *ID_COLUMNS] if c in df.columns])
89
+ y = df[target_col]
90
+ predictions = np.full(len(df), np.nan)
91
+
92
+ if group_col is not None:
93
+ splits = LeaveOneGroupOut().split(X, y, df[group_col])
94
+ else:
95
+ # Group on the configuration so replicates of one (LLM, GPU) pair never
96
+ # straddle the train/test boundary.
97
+ config_id = df[ID_COLUMNS].astype(str).agg("|".join, axis=1)
98
+ splits = GroupKFold(n_splits=5).split(X, y, groups=config_id)
99
+
100
+ for train_idx, test_idx in splits:
101
+ pipeline = build_pipeline(X, XGBRegressor(**regressor_kwargs))
102
+ y_train = np.log(y.iloc[train_idx]) if log_transform_y else y.iloc[train_idx]
103
+ pipeline.fit(X.iloc[train_idx], y_train)
104
+ y_pred = pipeline.predict(X.iloc[test_idx])
105
+ predictions[test_idx] = np.exp(y_pred) if log_transform_y else y_pred
106
+
107
+ return predictions
108
+
109
+
110
+ def double_holdout_predictions(df: pd.DataFrame, target_col: str,
111
+ regressor_kwargs: dict, log_transform_y: bool) -> np.ndarray:
112
+ """Predictions for pairs whose LLM *and* GPU are both held out.
113
+
114
+ The paper validates generalisation one axis at a time (LOGO and LOLO). This
115
+ is the natural extension: for every measured (LLM, GPU) pair, train on the
116
+ data with that GPU and that LLM both removed entirely, then predict the
117
+ pair. It is the only honest way to attach an error to an estimate where
118
+ neither side was measured -- without it, such an estimate would carry no
119
+ validated accuracy at all.
120
+ """
121
+ X = df.drop(columns=[c for c in [target_col, *ID_COLUMNS] if c in df.columns])
122
+ y = df[target_col]
123
+ predictions = np.full(len(df), np.nan)
124
+
125
+ models = df["model"].to_numpy()
126
+ gpus = df["gpu_db_name"].to_numpy()
127
+ pairs = df[["model", "gpu_db_name"]].drop_duplicates().itertuples(index=False)
128
+
129
+ for n, (model, gpu) in enumerate(pairs, start=1):
130
+ test = (models == model) & (gpus == gpu)
131
+ train = (models != model) & (gpus != gpu)
132
+ if not train.any() or not test.any():
133
+ continue
134
+
135
+ pipeline = build_pipeline(X, XGBRegressor(**regressor_kwargs))
136
+ y_train = np.log(y[train]) if log_transform_y else y[train]
137
+ pipeline.fit(X[train], y_train)
138
+ y_pred = pipeline.predict(X[test])
139
+ predictions[test] = np.exp(y_pred) if log_transform_y else y_pred
140
+ if n % 40 == 0:
141
+ print(f" double holdout: {n} pairs")
142
+
143
+ return predictions
144
+
145
+
146
+ def tier_accuracy(df: pd.DataFrame, target_col: str, regressor_kwargs: dict,
147
+ log_transform_y: bool, to_watts: bool) -> dict[str, dict[str, float]]:
148
+ """MdAPE per certainty tier and scenario, each under its own protocol.
149
+
150
+ Reported separately for offline and server operation because, as in the
151
+ paper's Tables 2 and 3, the two regimes differ substantially -- especially
152
+ for ITL, where offline throughput depends on batching effects that the
153
+ features capture only partly.
154
+ """
155
+ scale = df["thermal_design_power_w"].to_numpy() if to_watts else 1.0
156
+ y_true = df[target_col].to_numpy() * scale
157
+ # The paper reports the two server load levels together.
158
+ regime = np.where(df["scenario"].to_numpy() == "offline", "offline", "server")
159
+
160
+ results: dict[str, dict[str, float]] = {}
161
+ tiers = (("green", None), ("yellow", "model"), ("orange", "gpu_db_name"), ("red", "both"))
162
+ for tier, group_col in tiers:
163
+ if group_col == "both":
164
+ y_pred = double_holdout_predictions(
165
+ df, target_col, regressor_kwargs, log_transform_y) * scale
166
+ else:
167
+ y_pred = cross_validate(
168
+ df, group_col, target_col, regressor_kwargs, log_transform_y) * scale
169
+ valid = ~np.isnan(y_pred)
170
+ results[tier] = {
171
+ scenario: round(mdape(y_true[valid & (regime == scenario)],
172
+ y_pred[valid & (regime == scenario)]), 1)
173
+ for scenario in ("offline", "server")
174
+ }
175
+ print(f" {tier:<7} MdAPE offline {results[tier]['offline']:5.1f}% "
176
+ f"server {results[tier]['server']:5.1f}%")
177
+ return results
178
+
179
+
180
+ def fit_final(df: pd.DataFrame, target_col: str, regressor_kwargs: dict,
181
+ log_transform_y: bool) -> Pipeline:
182
+ """Refit on every row, which is what the demo serves predictions from."""
183
+ X = df.drop(columns=[c for c in [target_col, *ID_COLUMNS] if c in df.columns])
184
+ y = np.log(df[target_col]) if log_transform_y else df[target_col]
185
+ pipeline = build_pipeline(X, XGBRegressor(**regressor_kwargs))
186
+ pipeline.fit(X, y)
187
+ return pipeline
188
+
189
+
190
+ def _parse_architectures(value) -> list[str]:
191
+ """`model_features.csv` stores the architecture list as a Python literal."""
192
+ if value is None or (isinstance(value, float) and pd.isna(value)):
193
+ return []
194
+ if isinstance(value, list):
195
+ return [str(v) for v in value]
196
+ try:
197
+ parsed = ast.literal_eval(str(value))
198
+ except (ValueError, SyntaxError):
199
+ return [str(value)]
200
+ return [str(v) for v in parsed] if isinstance(parsed, (list, tuple)) else [str(parsed)]
201
+
202
+
203
+ def main() -> int:
204
+ parser = argparse.ArgumentParser(description=__doc__)
205
+ parser.add_argument("--data-dir", default=PAPER_DATA_DIR)
206
+ parser.add_argument("--out-dir", default=os.path.join(REPO_ROOT, "data", "models"))
207
+ parser.add_argument("--skip-validation", action="store_true",
208
+ help="fit only; keep the tier accuracies from a previous run")
209
+ args = parser.parse_args()
210
+
211
+ os.makedirs(args.out_dir, exist_ok=True)
212
+
213
+ print(f"loading measurements from {args.data_dir}")
214
+ df_all = build_training_frame(args.data_dir)
215
+ print(f" {len(df_all)} runs, {df_all.model.nunique()} LLMs, {df_all.gpu_db_name.nunique()} GPUs")
216
+
217
+ meta: dict = {}
218
+ accuracies: dict = {}
219
+
220
+ # --- power draw ---------------------------------------------------------
221
+ df_power = clean_frame(df_all, [*ID_COLUMNS, POWER_TARGET, *POWER_FEATURES])
222
+ tdp = df_all.set_index("gpu_db_name")["thermal_design_power_w"].drop_duplicates()
223
+ df_power["thermal_design_power_w"] = df_power["gpu_db_name"].map(tdp)
224
+ print(f"\npower model: {len(df_power)} rows, {len(POWER_FEATURES)} features")
225
+ if not args.skip_validation:
226
+ accuracies["power"] = tier_accuracy(
227
+ df_power.drop(columns=["thermal_design_power_w"]).assign(
228
+ thermal_design_power_w=df_power["thermal_design_power_w"]),
229
+ POWER_TARGET, POWER_REGRESSOR, log_transform_y=False, to_watts=True)
230
+
231
+ power_pipeline = fit_final(
232
+ df_power.drop(columns=["thermal_design_power_w"]),
233
+ POWER_TARGET, POWER_REGRESSOR, log_transform_y=False)
234
+ joblib.dump(power_pipeline, os.path.join(args.out_dir, "power.joblib"))
235
+
236
+ # --- inter-token latency ------------------------------------------------
237
+ df_itl = clean_frame(df_all, [*ID_COLUMNS, ITL_TARGET, *ITL_FEATURES])
238
+ print(f"\nITL model: {len(df_itl)} rows, {len(ITL_FEATURES)} features")
239
+ if not args.skip_validation:
240
+ accuracies["itl"] = tier_accuracy(
241
+ df_itl, ITL_TARGET, ITL_REGRESSOR, log_transform_y=True, to_watts=False)
242
+
243
+ itl_pipeline = fit_final(df_itl, ITL_TARGET, ITL_REGRESSOR, log_transform_y=True)
244
+ joblib.dump(itl_pipeline, os.path.join(args.out_dir, "itl.joblib"))
245
+
246
+ # --- metadata -----------------------------------------------------------
247
+ meta_path = os.path.join(args.out_dir, "meta.json")
248
+ if args.skip_validation and os.path.exists(meta_path):
249
+ with open(meta_path) as fh:
250
+ accuracies = json.load(fh).get("tier_accuracy_mdape", accuracies)
251
+
252
+ # A model or GPU counts as "seen" only if it survived into a training frame.
253
+ seen_models = sorted(set(df_power["model"]) | set(df_itl["model"]))
254
+ seen_gpus = sorted(set(df_power["gpu_db_name"]) | set(df_itl["gpu_db_name"]))
255
+
256
+ meta = {
257
+ "seen_models": seen_models,
258
+ "seen_gpus": seen_gpus,
259
+ "power_features": POWER_FEATURES,
260
+ "itl_features": ITL_FEATURES,
261
+ "tier_accuracy_mdape": accuracies,
262
+ # The ITL model is fitted on log(itl); predictions must be exponentiated.
263
+ "log_transformed_targets": ["itl"],
264
+ "n_training_runs": {"power": len(df_power), "itl": len(df_itl)},
265
+ "gpu_tdp": {k: float(v) for k, v in tdp.items()},
266
+ }
267
+ with open(meta_path, "w") as fh:
268
+ json.dump(meta, fh, indent=2)
269
+
270
+ # Cache the architecture of every profiled LLM. These features are already
271
+ # in the paper's data, so a profiled model needs no Hub round-trip -- which
272
+ # also makes licence-gated models (Llama, Gemma) work without a token.
273
+ cache = {}
274
+ for model_id, group in df_all.groupby("model"):
275
+ if model_id not in seen_models:
276
+ continue
277
+ row = group.iloc[0]
278
+ cache[model_id] = {
279
+ "model_type": str(row["model_type"]),
280
+ "num_layers": int(row["num_layers"]),
281
+ "hidden_size": int(row["hidden_size"]),
282
+ "num_attention_heads": int(row["num_attention_heads"]),
283
+ "num_key_value_heads": int(row["num_key_value_heads"]),
284
+ "total_b_params": float(row["total_b_params"]),
285
+ "architectures": _parse_architectures(row.get("architectures")),
286
+ "max_position_embeddings": (
287
+ None if pd.isna(row.get("max_position_embeddings"))
288
+ else int(row["max_position_embeddings"])
289
+ ),
290
+ "torch_dtype": (
291
+ None if pd.isna(row.get("torch_dtype")) or row.get("torch_dtype") == "N/A"
292
+ else str(row["torch_dtype"])
293
+ ),
294
+ }
295
+ cache_path = os.path.join(args.out_dir, "profiled_llms.json")
296
+ with open(cache_path, "w") as fh:
297
+ json.dump(cache, fh, indent=2, sort_keys=True)
298
+ print(f" cached architecture for {len(cache)} profiled LLMs")
299
+
300
+ print(f"\nsaved models and metadata to {args.out_dir}")
301
+ print(f" {len(seen_models)} profiled LLMs, {len(seen_gpus)} profiled GPUs")
302
+ return 0
303
+
304
+
305
+ if __name__ == "__main__":
306
+ sys.exit(main())
tests/test_demo.py ADDED
@@ -0,0 +1,1176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the WattGPU demo.
2
+
3
+ Covers the four combinations the certainty tier distinguishes -- known and
4
+ unknown LLMs against known and unknown GPUs -- plus the error paths.
5
+
6
+ Tests marked `network` reach the Hugging Face Hub; run
7
+ `pytest -m "not network"` to skip them.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import html
13
+ import json
14
+ import os
15
+ import re
16
+ import sys
17
+ import urllib.parse
18
+
19
+ import pytest
20
+
21
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
22
+
23
+ import app as demo_app # noqa: E402
24
+ from wattgpu_demo import demand_log, emissions # noqa: E402
25
+ from wattgpu_demo.errors import ( # noqa: E402
26
+ GPUNotFoundError,
27
+ InsufficientMemoryError,
28
+ ModelConfigError,
29
+ ModelNotFoundError,
30
+ NetworkError,
31
+ UnsupportedModelError,
32
+ WattGPUError,
33
+ )
34
+ from wattgpu_demo.features import ITL_FEATURES, POWER_FEATURES # noqa: E402
35
+ from wattgpu_demo.gpu_db import GPUDatabase # noqa: E402
36
+ from wattgpu_demo.hf_models import ( # noqa: E402
37
+ fetch_llm_info,
38
+ llm_info_from_config,
39
+ search_models,
40
+ )
41
+ from wattgpu_demo.predictor import WattGPUPredictor # noqa: E402
42
+
43
+ network = pytest.mark.network
44
+
45
+ # A profiled LLM and a profiled GPU, from the paper's measurements.
46
+ KNOWN_LLM = "meta-llama/Llama-3.1-8B-Instruct"
47
+ KNOWN_GPU = "H200 NVL"
48
+ # Released after the study, and never profiled.
49
+ UNKNOWN_LLM = "Qwen/Qwen3-8B"
50
+ UNKNOWN_GPU = "A100 SXM4 80 GB"
51
+
52
+
53
+ @pytest.fixture(scope="session")
54
+ def predictor() -> WattGPUPredictor:
55
+ return WattGPUPredictor()
56
+
57
+
58
+ @pytest.fixture(scope="session")
59
+ def gpu_db() -> GPUDatabase:
60
+ return GPUDatabase()
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Fixture assumptions
65
+ # ---------------------------------------------------------------------------
66
+
67
+ def test_fixtures_are_what_the_tests_assume(predictor):
68
+ assert KNOWN_LLM in predictor.profiled_models
69
+ assert KNOWN_GPU in predictor.profiled_gpus
70
+ assert UNKNOWN_LLM not in predictor.profiled_models
71
+ assert UNKNOWN_GPU not in predictor.profiled_gpus
72
+ assert UNKNOWN_GPU in predictor.gpu_db.names
73
+
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # The four certainty tiers
77
+ # ---------------------------------------------------------------------------
78
+
79
+ def test_known_llm_known_gpu_is_green(predictor):
80
+ result = predictor.predict(KNOWN_LLM, KNOWN_GPU, "server_low")
81
+
82
+ assert result.certainty.tier == "green"
83
+ assert "High confidence" in result.certainty.label
84
+ # A profiled model is read from the local cache, not the Hub.
85
+ assert result.llm.params_source == "profiled dataset"
86
+ assert 0 < result.power_w <= result.gpu.thermal_design_power_w * 1.2
87
+ assert result.itl_s is not None and result.itl_s > 0
88
+ assert result.power_mdape is not None and result.itl_mdape is not None
89
+
90
+
91
+ @network
92
+ def test_unknown_llm_known_gpu_is_yellow(predictor):
93
+ result = predictor.predict(UNKNOWN_LLM, KNOWN_GPU, "server_low")
94
+
95
+ assert result.certainty.tier == "yellow"
96
+ assert result.llm.params_source == "safetensors index"
97
+ assert 7 < result.llm.total_b_params < 9
98
+ assert 0 < result.power_w <= result.gpu.thermal_design_power_w * 1.2
99
+ assert result.itl_s is not None and result.itl_s > 0
100
+
101
+
102
+ def test_known_llm_unknown_gpu_is_orange(predictor):
103
+ result = predictor.predict(KNOWN_LLM, UNKNOWN_GPU, "offline")
104
+
105
+ assert result.certainty.tier == "orange"
106
+ assert result.gpu.gpu_db_name == UNKNOWN_GPU
107
+ assert 0 < result.power_w <= result.gpu.thermal_design_power_w * 1.2
108
+ assert result.itl_s is not None and result.itl_s > 0
109
+
110
+
111
+ @network
112
+ @pytest.mark.parametrize("scenario,expected_band,expected_label", [
113
+ ("server_low", "orange", "Lower confidence"),
114
+ ("server_high", "orange", "Lower confidence"),
115
+ ("offline", "red", "Low confidence"),
116
+ ])
117
+ def test_unknown_llm_unknown_gpu_band_depends_on_scenario(
118
+ predictor, scenario, expected_band, expected_label):
119
+ """Extrapolating on both axes costs little under server load, a lot offline."""
120
+ result = predictor.predict(UNKNOWN_LLM, UNKNOWN_GPU, scenario)
121
+
122
+ assert result.certainty.tier == expected_band
123
+ assert expected_label in result.certainty.label
124
+ assert "neither in training data" in result.certainty.label
125
+ assert 0 < result.power_w <= result.gpu.thermal_design_power_w * 1.2
126
+ assert result.itl_s is not None and result.itl_s > 0
127
+ # The error must be measured, not left blank.
128
+ assert result.power_mdape is not None and result.itl_mdape is not None
129
+ note = " ".join(result.notes)
130
+ assert "both axes at once" in note and "mailto" in note
131
+
132
+
133
+ @network
134
+ def test_error_always_comes_from_the_double_holdout(predictor):
135
+ """A friendlier band must not borrow a friendlier number.
136
+
137
+ Under server load a both-unseen pair is shown in the orange band, but the
138
+ error reported has to stay the one measured by the double holdout.
139
+ """
140
+ both_unseen = predictor.predict(UNKNOWN_LLM, UNKNOWN_GPU, "server_low")
141
+ gpu_unseen = predictor.predict(KNOWN_LLM, UNKNOWN_GPU, "server_low")
142
+
143
+ assert both_unseen.certainty.tier == gpu_unseen.certainty.tier == "orange"
144
+ assert both_unseen.certainty.accuracy_key == "red"
145
+ assert gpu_unseen.certainty.accuracy_key == "orange"
146
+
147
+ accuracy = predictor.meta["tier_accuracy_mdape"]
148
+ assert both_unseen.power_mdape == accuracy["power"]["red"]["server"]
149
+ assert gpu_unseen.power_mdape == accuracy["power"]["orange"]["server"]
150
+ assert both_unseen.power_mdape > gpu_unseen.power_mdape
151
+
152
+
153
+ def test_the_band_rule_is_justified_by_the_measurements(predictor):
154
+ """Pins the reason both-unseen shares the orange band under server load.
155
+
156
+ If retraining ever made the double holdout materially worse than
157
+ leave-one-GPU-out on the server side, the shared band would stop being
158
+ defensible and this test would catch it.
159
+ """
160
+ accuracy = predictor.meta["tier_accuracy_mdape"]
161
+ for target in ("power", "itl"):
162
+ orange = accuracy[target]["orange"]["server"]
163
+ red = accuracy[target]["red"]["server"]
164
+ assert red - orange <= 3.0, (
165
+ f"{target}: double holdout is {red}% vs {orange}% under server load, "
166
+ "too far apart to share the orange band")
167
+
168
+ # Offline latency is the case that does degrade, hence its own band.
169
+ assert (accuracy["itl"]["red"]["offline"]
170
+ - accuracy["itl"]["orange"]["offline"]) > 5.0
171
+
172
+
173
+ def test_tier_accuracy_is_ordered_by_difficulty(predictor):
174
+ """Generalising to new hardware is harder than to new architectures."""
175
+ for target in ("power", "itl"):
176
+ for regime in ("offline", "server"):
177
+ accuracy = predictor.meta["tier_accuracy_mdape"][target]
178
+ assert accuracy["green"][regime] <= accuracy["yellow"][regime]
179
+ assert accuracy["yellow"][regime] <= accuracy["orange"][regime]
180
+ # Extrapolating on both axes is never easier than on one.
181
+ assert accuracy["orange"][regime] <= accuracy["red"][regime]
182
+
183
+
184
+ def test_scenarios_are_labelled_by_concurrent_users(predictor):
185
+ """The UI asks for concurrent users, which is estimable before deployment."""
186
+ from wattgpu_demo.features import SCENARIO_LABELS, SCENARIOS
187
+
188
+ assert SCENARIOS == ["server_low", "server_high", "offline"]
189
+ assert set(SCENARIO_LABELS) == set(SCENARIOS)
190
+ assert "concurrent user" in SCENARIO_LABELS["server_low"]
191
+ assert "concurrent users" in SCENARIO_LABELS["server_high"]
192
+ assert "batch" in SCENARIO_LABELS["offline"]
193
+ # No label should still be phrased as a request rate.
194
+ assert not any("requests/s" in label for label in SCENARIO_LABELS.values())
195
+
196
+ # Every scenario must still be predictable end to end.
197
+ for scenario in SCENARIOS:
198
+ assert predictor.predict(KNOWN_LLM, KNOWN_GPU, scenario).power_w > 0
199
+
200
+
201
+ def test_scenario_labels_match_the_measured_request_rates():
202
+ """The user-facing user counts must follow from the rates actually run.
203
+
204
+ Only two arrival rates appear in the dataset, so these settings are three
205
+ measured points rather than ranges. At ~1 message per minute per active
206
+ user they correspond to about 1 and about 20 concurrent users.
207
+ """
208
+ from wattgpu_demo.features import SCENARIO_LABELS, SCENARIO_RATES_QPS
209
+
210
+ assert SCENARIO_RATES_QPS["server_low"] == pytest.approx(0.017, abs=1e-3)
211
+ assert SCENARIO_RATES_QPS["server_high"] == pytest.approx(0.330, abs=1e-3)
212
+ assert SCENARIO_RATES_QPS["offline"] is None
213
+
214
+ per_user_rate = 1 / 60 # one message per minute
215
+ assert round(SCENARIO_RATES_QPS["server_low"] / per_user_rate) == 1
216
+ assert round(SCENARIO_RATES_QPS["server_high"] / per_user_rate) == 20
217
+
218
+ assert "single" in SCENARIO_LABELS["server_low"]
219
+ assert "20" in SCENARIO_LABELS["server_high"]
220
+
221
+
222
+ def test_only_saturated_load_reports_batch_amortised_latency(predictor):
223
+ """The two latency definitions must never be presented as the same thing."""
224
+ for scenario in ("server_low", "server_high"):
225
+ result = predictor.predict(KNOWN_LLM, KNOWN_GPU, scenario)
226
+ assert result.latency_is_per_user
227
+ assert result.energy_per_token_j is None
228
+
229
+ saturated = predictor.predict(KNOWN_LLM, KNOWN_GPU, "offline")
230
+ assert not saturated.latency_is_per_user
231
+ assert saturated.energy_per_token_j is not None
232
+
233
+ # The whole-batch figure is orders of magnitude smaller than a user's wait,
234
+ # which is exactly why it must not be labelled a latency.
235
+ per_user = predictor.predict(KNOWN_LLM, KNOWN_GPU, "server_low")
236
+ assert saturated.itl_s < per_user.itl_s / 10
237
+
238
+
239
+ def test_offline_reports_throughput_and_explains_where_it_comes_from():
240
+ output = demo_app.predict(KNOWN_LLM, KNOWN_GPU, "offline")
241
+ assert "Throughput" in output and "tok/s" in output
242
+ assert "Inter-token latency" not in output
243
+ # It must say how the number was derived, and why the batch-wide latency it
244
+ # came from cannot stand in for a server's per-user latency.
245
+ assert "inverse of the inter-token latency" in output
246
+ assert "whole batch" in output
247
+
248
+
249
+ def test_per_user_result_calls_it_inter_token_latency():
250
+ output = demo_app.predict(KNOWN_LLM, KNOWN_GPU, "server_low")
251
+ assert "Inter-token latency" in output
252
+ assert "Throughput" not in output
253
+
254
+
255
+ # ---------------------------------------------------------------------------
256
+ # Error handling
257
+ # ---------------------------------------------------------------------------
258
+
259
+ @network
260
+ def test_nonexistent_model_is_reported_clearly():
261
+ with pytest.raises(ModelNotFoundError) as excinfo:
262
+ fetch_llm_info("definitely-not-a-real-owner/definitely-not-a-real-model")
263
+ assert "not-a-real-model" in excinfo.value.display()
264
+
265
+
266
+ @pytest.mark.parametrize("bad_id", ["", " ", "justaname", "a/b/c"])
267
+ def test_malformed_model_ids_are_rejected_without_a_request(bad_id):
268
+ with pytest.raises(ModelNotFoundError):
269
+ fetch_llm_info(bad_id)
270
+
271
+
272
+ @network
273
+ def test_repository_without_usable_config_is_reported():
274
+ """A GGUF-only repository has a config.json that lacks the needed fields."""
275
+ with pytest.raises((ModelConfigError, ModelNotFoundError)):
276
+ fetch_llm_info("TheBloke/Llama-2-7B-GGUF")
277
+
278
+
279
+ def test_unknown_gpu_suggests_close_matches(gpu_db):
280
+ with pytest.raises(GPUNotFoundError) as excinfo:
281
+ gpu_db.get("A100 SXM 80")
282
+ assert "A100 SXM4 80 GB" in excinfo.value.display()
283
+
284
+
285
+ def test_database_is_nvidia_only(gpu_db):
286
+ """Every profiled run used NVIDIA hardware under vLLM, so other vendors'
287
+ matrix engines are outside what the features describe."""
288
+ assert set(gpu_db.frame["manufacturer"]) == {"NVIDIA"}
289
+ for absent in ("Radeon Instinct MI300X", "Data Center GPU Max 1550"):
290
+ with pytest.raises(GPUNotFoundError):
291
+ gpu_db.get(absent)
292
+
293
+
294
+ def test_empty_gpu_is_rejected(gpu_db):
295
+ with pytest.raises(GPUNotFoundError):
296
+ gpu_db.get("")
297
+
298
+
299
+ def test_unknown_scenario_is_rejected(predictor):
300
+ with pytest.raises(WattGPUError):
301
+ predictor.predict(KNOWN_LLM, KNOWN_GPU, "not-a-scenario")
302
+
303
+
304
+ @network
305
+ @pytest.mark.parametrize("model_id,described_as", [
306
+ ("Qwen/Qwen3-Embedding-0.6B", "an embedding model"), # declares ForCausalLM
307
+ ("Qwen/Qwen3-Reranker-4B", "a reranker"), # declares ForCausalLM
308
+ ("sentence-transformers/all-MiniLM-L6-v2", "an embedding model"),
309
+ ("BAAI/bge-reranker-v2-m3", "a classifier"),
310
+ ])
311
+ def test_embedding_and_reranking_models_are_refused(predictor, model_id, described_as):
312
+ """These run one forward pass rather than generating token by token, so
313
+ inter-token latency does not apply. Two of them declare `ForCausalLM`, so
314
+ the Hub's task tag is what distinguishes them."""
315
+ with pytest.raises(UnsupportedModelError) as excinfo:
316
+ predictor.predict(model_id, KNOWN_GPU, "offline")
317
+ assert described_as in excinfo.value.display()
318
+
319
+
320
+ def test_non_generative_architecture_is_refused_without_the_network():
321
+ with pytest.raises(UnsupportedModelError):
322
+ llm_info_from_config("acme/encoder",
323
+ {**LLAMA_8B_CONFIG, "architectures": ["BertModel"]})
324
+ with pytest.raises(UnsupportedModelError):
325
+ llm_info_from_config("acme/reranker",
326
+ {**LLAMA_8B_CONFIG,
327
+ "architectures": ["XLMRobertaForSequenceClassification"]})
328
+
329
+
330
+ def test_model_too_large_for_the_gpu_is_refused(predictor):
331
+ """A model whose weights cannot fit gets no estimate at all."""
332
+ with pytest.raises(InsufficientMemoryError) as excinfo:
333
+ predictor.predict("microsoft/phi-4", "Tesla T4", "offline")
334
+
335
+ message = excinfo.value.display()
336
+ assert "Does not fit" in message
337
+ assert "29.4 GB" in message # 14.7 B parameters at FP16
338
+ assert "14.4 GB" in message # 90% of the T4's 16 GB
339
+ assert "at least 33 GB" in message
340
+
341
+
342
+ def test_borderline_model_is_refused_rather_than_estimated(predictor):
343
+ """8.03 B at FP16 is 16.06 GB, which does not fit a 16 GB card once vLLM
344
+ has taken its 10%. The old code estimated anyway and rounded the warning to
345
+ a self-contradictory "16 GB, more than the 16 GB"."""
346
+ with pytest.raises(InsufficientMemoryError):
347
+ predictor.predict(KNOWN_LLM, "Tesla T4", "offline")
348
+
349
+
350
+ @network
351
+ def test_memory_check_runs_before_the_coverage_check(predictor):
352
+ """Not fitting is a physical fact, and the more useful thing to report."""
353
+ with pytest.raises(InsufficientMemoryError):
354
+ # Neither side measured, and also far too large: memory wins over the
355
+ # "we have no data for either" refusal.
356
+ predictor.predict(UNKNOWN_LLM, "GeForce RTX 3060 12 GB", "offline")
357
+
358
+
359
+ def test_no_measured_pair_is_ever_refused_for_memory(predictor):
360
+ """Every pair in the dataset actually ran, so none may be called too large.
361
+
362
+ This is what pins the 90% budget: a stricter threshold would start
363
+ contradicting the measurements the models were fitted on.
364
+ """
365
+ from wattgpu_demo.features import build_training_frame
366
+ from wattgpu_demo.predictor import BYTES_PER_PARAM, VLLM_MEMORY_UTILISATION
367
+
368
+ paper_data = os.path.join(
369
+ os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
370
+ "data")
371
+ if not os.path.exists(paper_data):
372
+ pytest.skip("the paper's data directory is not available")
373
+
374
+ df = build_training_frame(paper_data)
375
+ pairs = df[["model", "gpu_db_name", "total_b_params", "memory_size_gb"]].drop_duplicates()
376
+ spare = (pairs.memory_size_gb * VLLM_MEMORY_UTILISATION
377
+ - pairs.total_b_params * BYTES_PER_PARAM)
378
+
379
+ too_large = pairs[spare <= 0]
380
+ assert too_large.empty, f"would refuse measured pairs:\n{too_large}"
381
+
382
+
383
+ def test_tight_fit_warns_without_refusing(predictor):
384
+ """SOLAR-10.7B on an A30 was measured, but leaves ~0.2 GB for the KV cache."""
385
+ result = predictor.predict("upstage/SOLAR-10.7B-Instruct-v1.0", "A30 PCIe", "offline")
386
+ assert result.power_w > 0
387
+ assert any("KV cache" in w for w in result.warnings)
388
+
389
+
390
+ # ---------------------------------------------------------------------------
391
+ # config.json -> WattGPU features
392
+ # ---------------------------------------------------------------------------
393
+
394
+ LLAMA_8B_CONFIG = {
395
+ "model_type": "llama",
396
+ "architectures": ["LlamaForCausalLM"],
397
+ "num_hidden_layers": 32,
398
+ "hidden_size": 4096,
399
+ "num_attention_heads": 32,
400
+ "num_key_value_heads": 8,
401
+ "intermediate_size": 14336,
402
+ "vocab_size": 128256,
403
+ "max_position_embeddings": 131072,
404
+ "torch_dtype": "bfloat16",
405
+ }
406
+
407
+
408
+ def test_converter_maps_a_config_without_touching_the_network():
409
+ llm = llm_info_from_config("acme/llama-like", LLAMA_8B_CONFIG)
410
+
411
+ assert llm.model_type == "llama"
412
+ assert (llm.num_layers, llm.hidden_size) == (32, 4096)
413
+ assert (llm.num_attention_heads, llm.num_key_value_heads) == (32, 8)
414
+ # Estimated from the transformer's shape, so within a few percent of 8.03 B.
415
+ assert llm.total_b_params == pytest.approx(8.0, abs=0.4)
416
+ assert llm.params_source == "estimated from config"
417
+
418
+
419
+ def test_converter_uses_a_known_parameter_count_when_given_one():
420
+ llm = llm_info_from_config("acme/llama-like", LLAMA_8B_CONFIG, 8.03, "safetensors index")
421
+ assert llm.total_b_params == 8.03
422
+ assert llm.params_source == "safetensors index"
423
+
424
+
425
+ @pytest.mark.parametrize("aliases", [
426
+ {"n_layer": 12, "n_embd": 768, "n_head": 12}, # GPT-2 naming
427
+ {"num_layers": 12, "d_model": 768, "num_heads": 12}, # T5-ish naming
428
+ ])
429
+ def test_converter_understands_alternative_field_names(aliases):
430
+ config = {"model_type": "x", "vocab_size": 50257, **aliases}
431
+ llm = llm_info_from_config("acme/aliased", config)
432
+ assert llm.num_layers == 12 and llm.hidden_size == 768
433
+ # No GQA declared, so every head keeps its own K/V.
434
+ assert llm.num_key_value_heads == llm.num_attention_heads
435
+
436
+
437
+ def test_converter_unwraps_a_multimodal_text_config():
438
+ config = {
439
+ "model_type": "gemma3",
440
+ "architectures": ["Gemma3ForConditionalGeneration"],
441
+ "vision_config": {"hidden_size": 1152},
442
+ "text_config": {"num_hidden_layers": 48, "hidden_size": 3840,
443
+ "num_attention_heads": 16, "num_key_value_heads": 8,
444
+ "intermediate_size": 15360, "vocab_size": 262208},
445
+ }
446
+ llm = llm_info_from_config("acme/multimodal", config)
447
+ assert llm.num_layers == 48 and llm.hidden_size == 3840
448
+ assert any("multimodal" in w for w in llm.warnings)
449
+
450
+
451
+ def test_converter_rejects_a_config_missing_required_fields():
452
+ with pytest.raises(ModelConfigError):
453
+ llm_info_from_config("acme/incomplete", {"model_type": "mystery"})
454
+
455
+
456
+ # --- unsupported model families -----------------------------------------
457
+
458
+ @pytest.mark.parametrize("moe_config", [
459
+ {"num_local_experts": 8}, # Mixtral
460
+ {"num_experts": 64, "num_experts_per_tok": 8}, # Qwen3-MoE, OLMoE
461
+ {"n_routed_experts": 160}, # DeepSeek
462
+ {"moe_intermediate_size": 1408},
463
+ {"architectures": ["Qwen3MoeForCausalLM"]},
464
+ ])
465
+ def test_converter_refuses_mixture_of_experts(moe_config):
466
+ with pytest.raises(UnsupportedModelError) as excinfo:
467
+ llm_info_from_config("acme/moe", {**LLAMA_8B_CONFIG, **moe_config})
468
+ message = excinfo.value.display()
469
+ assert "Not supported yet" in message and "mixture-of-experts" in message
470
+ assert "get in touch" in message.lower()
471
+
472
+
473
+ @pytest.mark.parametrize("quantised_config", [
474
+ {"quantization_config": {"quant_method": "gptq", "bits": 4}},
475
+ {"quantization_config": {"quant_method": "awq"}},
476
+ {"quantization_config": {"quant_method": "compressed-tensors"}},
477
+ {"torch_dtype": "int8"},
478
+ {"dtype": "float8_e4m3fn"},
479
+ ])
480
+ def test_converter_refuses_quantised_models(quantised_config):
481
+ with pytest.raises(UnsupportedModelError) as excinfo:
482
+ llm_info_from_config("acme/quant", {**LLAMA_8B_CONFIG, **quantised_config})
483
+ assert "quantised" in excinfo.value.display()
484
+
485
+
486
+ @pytest.mark.parametrize("model_id", [
487
+ "TheBloke/Llama-2-7B-AWQ",
488
+ "TheBloke/Llama-2-7B-GPTQ",
489
+ "unsloth/Llama-3.1-8B-bnb-4bit",
490
+ "RedHatAI/Llama-3.1-8B-Instruct-FP8",
491
+ ])
492
+ def test_converter_refuses_quantised_repository_names(model_id):
493
+ """Caught even when the config does not declare the quantisation."""
494
+ with pytest.raises(UnsupportedModelError):
495
+ llm_info_from_config(model_id, LLAMA_8B_CONFIG)
496
+
497
+
498
+ @pytest.mark.parametrize("model_id", [
499
+ "meta-llama/Llama-3.1-8B-Instruct",
500
+ "Qwen/Qwen3-8B",
501
+ "mistralai/Mistral-7B-Instruct-v0.3",
502
+ "microsoft/phi-4",
503
+ "allenai/OLMo-2-1124-7B-Instruct",
504
+ ])
505
+ def test_ordinary_model_names_are_not_mistaken_for_quantised(model_id):
506
+ """The name-based fallback must not fire on plain model names."""
507
+ assert llm_info_from_config(model_id, LLAMA_8B_CONFIG).total_b_params > 0
508
+
509
+
510
+ @network
511
+ @pytest.mark.parametrize("model_id", [
512
+ "mistralai/Mixtral-8x7B-Instruct-v0.1", # MoE
513
+ "Qwen/Qwen3-30B-A3B", # MoE
514
+ "openai/gpt-oss-20b", # MoE
515
+ "TheBloke/Llama-2-7B-AWQ", # quantised
516
+ "RedHatAI/Meta-Llama-3.1-8B-Instruct-FP8", # quantised
517
+ ])
518
+ def test_real_unsupported_models_are_refused(model_id):
519
+ with pytest.raises(UnsupportedModelError):
520
+ fetch_llm_info(model_id)
521
+
522
+
523
+ def test_unmeasured_model_carries_a_detection_caveat(predictor):
524
+ """Detection is good but not infallible, and the UI must say so."""
525
+ result = predictor.predict(UNKNOWN_LLM, KNOWN_GPU, "server_low")
526
+ assert result.notes, "an unmeasured model should carry a note"
527
+ note = " ".join(result.notes)
528
+ assert "config.json" in note
529
+ assert "mixture-of-experts" in note and "quantised" in note
530
+
531
+
532
+ def test_measured_model_carries_no_detection_caveat(predictor):
533
+ assert predictor.predict(KNOWN_LLM, KNOWN_GPU, "server_low").notes == []
534
+
535
+
536
+ @network
537
+ def test_hub_search_finds_models():
538
+ results = search_models("qwen3")
539
+ assert results and all("/" in r for r in results)
540
+ assert any("Qwen" in r for r in results)
541
+
542
+
543
+ @pytest.mark.parametrize("query", ["", " ", "q"])
544
+ def test_hub_search_ignores_queries_that_are_too_short(query):
545
+ assert search_models(query) == []
546
+
547
+
548
+ # ---------------------------------------------------------------------------
549
+ # GPU database
550
+ # ---------------------------------------------------------------------------
551
+
552
+ def test_every_profiled_gpu_is_in_the_database(predictor, gpu_db):
553
+ missing = [g for g in predictor.profiled_gpus if g not in gpu_db.names]
554
+ assert missing == []
555
+
556
+
557
+ def test_profiled_gpus_have_reported_throughput(predictor, gpu_db):
558
+ for name in predictor.profiled_gpus:
559
+ gpu = gpu_db.get(name)
560
+ assert gpu.tflops_source == "reported", name
561
+ assert gpu.tflops_16b and gpu.tflops_16b > 0
562
+
563
+
564
+ def test_gpu_dropdown_lists_only_measured_gpus():
565
+ """Unmeasured GPUs are reachable by search, not by scrolling the list."""
566
+ assert demo_app.gpu_input.choices == [(g, g) for g in demo_app.predictor.profiled_gpus]
567
+
568
+
569
+ @pytest.mark.parametrize("query,expected", [
570
+ ("a100", "A100 SXM4 80 GB"),
571
+ ("h100", "H100 SXM5 80 GB"),
572
+ ("4090", "GeForce RTX 4090"),
573
+ ])
574
+ def test_gpu_search_reaches_the_full_catalogue(gpu_db, query, expected):
575
+ assert expected in gpu_db.search(query)
576
+
577
+
578
+ def test_gpu_search_ranks_prefix_matches_first(gpu_db):
579
+ results = gpu_db.search("a100")
580
+ assert results[0].lower().startswith("a100")
581
+
582
+
583
+ @pytest.mark.parametrize("query", ["", " ", "h"])
584
+ def test_gpu_search_ignores_queries_that_are_too_short(gpu_db, query):
585
+ assert gpu_db.search(query) == []
586
+
587
+
588
+ def test_gpu_search_surfaces_measured_gpus_first():
589
+ class KeyUp:
590
+ input_value = "h200"
591
+
592
+ choices = demo_app.search_gpus(KeyUp()).constructor_args["choices"]
593
+ assert choices[0] == "H200 NVL" # the measured one, ahead of other H200 boards
594
+
595
+
596
+ def test_gpu_lookup_is_case_insensitive(gpu_db):
597
+ assert gpu_db.get("h200 nvl").gpu_db_name == "H200 NVL"
598
+
599
+
600
+ def test_database_covers_a_broad_range_of_nvidia_hardware(gpu_db):
601
+ assert len(gpu_db.names) > 200
602
+ for expected in ("H100 SXM5 80 GB", "A100 SXM4 80 GB", "GeForce RTX 4090",
603
+ "RTX A6000", "L40"):
604
+ assert expected in gpu_db.names
605
+
606
+
607
+ # ---------------------------------------------------------------------------
608
+ # Feature-space consistency between training and inference
609
+ # ---------------------------------------------------------------------------
610
+
611
+ def test_inference_row_has_exactly_the_trained_features(predictor):
612
+ llm = predictor._resolve_llm(KNOWN_LLM)
613
+ gpu = predictor.gpu_db.get(KNOWN_GPU)
614
+ row = predictor._feature_row(llm, gpu, "offline")
615
+
616
+ for feature_set in (POWER_FEATURES, ITL_FEATURES):
617
+ assert set(feature_set) <= set(row.columns)
618
+ assert not row[feature_set].isna().any().any()
619
+
620
+ assert predictor.meta["power_features"] == POWER_FEATURES
621
+ assert predictor.meta["itl_features"] == ITL_FEATURES
622
+
623
+
624
+ def test_derived_features_match_their_definitions(predictor):
625
+ llm = predictor._resolve_llm(KNOWN_LLM)
626
+ gpu = predictor.gpu_db.get(KNOWN_GPU)
627
+ row = predictor._feature_row(llm, gpu, "offline").iloc[0]
628
+
629
+ assert row["bandwidth_latency"] == pytest.approx(
630
+ llm.total_b_params * 2 / gpu.memory_bandwidth_gb_s)
631
+ assert row["compute_latency_s"] == pytest.approx(
632
+ 2 * llm.total_b_params * 1e9 / (gpu.tflops_16b * 1e12))
633
+ assert row["boost_percentage"] == pytest.approx(
634
+ gpu.boost_clock_mhz / gpu.base_clock_mhz)
635
+
636
+
637
+ # ---------------------------------------------------------------------------
638
+ # Physical sanity
639
+ # ---------------------------------------------------------------------------
640
+
641
+ def test_faster_gpu_gives_lower_latency(predictor):
642
+ # A model small enough to fit both cards, so the comparison is about speed.
643
+ small = "Qwen/Qwen2.5-1.5B-Instruct"
644
+ fast = predictor.predict(small, "H200 NVL", "server_low")
645
+ slow = predictor.predict(small, "Tesla T4", "server_low")
646
+ assert fast.itl_s < slow.itl_s
647
+
648
+
649
+ def test_larger_model_is_slower_on_the_same_gpu(predictor):
650
+ small = predictor.predict("Qwen/Qwen2.5-1.5B-Instruct", KNOWN_GPU, "server_low")
651
+ large = predictor.predict("Qwen/Qwen2.5-7B-Instruct", KNOWN_GPU, "server_low")
652
+ assert small.itl_s < large.itl_s
653
+
654
+
655
+ def test_offline_reports_energy_per_token_and_server_does_not(predictor):
656
+ offline = predictor.predict(KNOWN_LLM, KNOWN_GPU, "offline")
657
+ server = predictor.predict(KNOWN_LLM, KNOWN_GPU, "server_high")
658
+
659
+ assert offline.energy_per_token_j is not None and offline.energy_per_token_j > 0
660
+ assert offline.energy_per_token_j == pytest.approx(offline.power_w * offline.itl_s)
661
+ assert server.energy_per_token_j is None
662
+
663
+
664
+ def test_offline_draws_more_power_than_a_lightly_loaded_server(predictor):
665
+ """A saturated GPU should draw more than an idle-ish one."""
666
+ offline = predictor.predict(KNOWN_LLM, KNOWN_GPU, "offline")
667
+ server = predictor.predict(KNOWN_LLM, KNOWN_GPU, "server_low")
668
+ assert offline.power_w > server.power_w
669
+
670
+
671
+ # ---------------------------------------------------------------------------
672
+ # The Gradio callback itself
673
+ # ---------------------------------------------------------------------------
674
+
675
+ @pytest.mark.parametrize("model_id,gpu_name,expected", [
676
+ (KNOWN_LLM, KNOWN_GPU, "High confidence"),
677
+ (KNOWN_LLM, UNKNOWN_GPU, "GPU not in training data"),
678
+ ("not-a-real/model-at-all", KNOWN_GPU, "Cannot make this estimate"),
679
+ (KNOWN_LLM, "No Such GPU 9000", "not in the GPU database"),
680
+ ])
681
+ def test_callback_always_renders_html(model_id, gpu_name, expected):
682
+ output = demo_app.predict(model_id, gpu_name, "offline")
683
+ assert output.startswith("<div")
684
+ assert expected in output
685
+
686
+
687
+ @network
688
+ def test_callback_renders_the_lowest_tier_rather_than_refusing():
689
+ output = demo_app.predict(UNKNOWN_LLM, UNKNOWN_GPU, "offline")
690
+ assert "wg-red" in output
691
+ assert "Low confidence" in output
692
+ assert "Cannot make this estimate" not in output
693
+
694
+
695
+ def test_refusals_are_styled_apart_from_the_confidence_tiers():
696
+ """Being out of scope is not a low confidence level."""
697
+ output = demo_app.predict("not-a-real/model-at-all", KNOWN_GPU, "offline")
698
+ assert "wg-error" in output
699
+ assert "wg-red" not in output
700
+
701
+
702
+ # ---------------------------------------------------------------------------
703
+ # Energy and emissions per million tokens
704
+ # ---------------------------------------------------------------------------
705
+
706
+ def test_energy_per_million_tokens_follows_from_the_prediction(predictor):
707
+ result = predictor.predict(KNOWN_LLM, KNOWN_GPU, "offline")
708
+
709
+ # Board joules per token, grossed up to the facility, then to Wh per 1M.
710
+ facility_j = result.energy_per_token_j / 0.7 * 1.2
711
+ assert result.energy_per_million_tokens_wh == pytest.approx(facility_j * 1e6 / 3600)
712
+ # And equivalently, power divided by throughput at the same boundary.
713
+ assert result.energy_per_million_tokens_wh == pytest.approx(
714
+ result.power_w / result.tokens_per_second / 0.7 * 1.2 * 1e6 / 3600)
715
+
716
+
717
+ def test_both_scenarios_use_the_same_system_boundary(predictor):
718
+ """Offline per-token and server per-hour must be comparable, so both
719
+ gross the board power up by the same host and PUE factors."""
720
+ offline = predictor.predict(KNOWN_LLM, KNOWN_GPU, "offline")
721
+ server = predictor.predict(KNOWN_LLM, KNOWN_GPU, "server_low")
722
+
723
+ board_only_wh = offline.energy_per_token_j * 1e6 / 3600
724
+ assert offline.energy_per_million_tokens_wh / board_only_wh == pytest.approx(1.2 / 0.7)
725
+ assert server.energy_per_hour_wh / server.power_w == pytest.approx(1.2 / 0.7)
726
+
727
+
728
+ def test_emissions_scale_with_the_grid(predictor):
729
+ clean = predictor.predict(KNOWN_LLM, KNOWN_GPU, "offline", carbon_intensity=30)
730
+ dirty = predictor.predict(KNOWN_LLM, KNOWN_GPU, "offline", carbon_intensity=600)
731
+
732
+ # Energy is a property of the hardware, not the grid.
733
+ assert clean.energy_per_million_tokens_wh == dirty.energy_per_million_tokens_wh
734
+ assert dirty.co2_per_million_tokens_g == pytest.approx(
735
+ clean.co2_per_million_tokens_g * 20)
736
+ assert clean.co2_per_million_tokens_g == pytest.approx(
737
+ clean.energy_per_million_tokens_wh / 1000 * 30)
738
+
739
+
740
+ def test_default_grid_is_the_eu_average(predictor):
741
+ result = predictor.predict(KNOWN_LLM, KNOWN_GPU, "offline")
742
+ assert result.carbon_intensity_g_per_kwh == 240.0
743
+
744
+
745
+ @pytest.mark.parametrize("bad", [None, "", "abc", -1, 99999])
746
+ def test_unusable_carbon_intensity_falls_back_to_the_default(predictor, bad):
747
+ result = predictor.predict(KNOWN_LLM, KNOWN_GPU, "offline", carbon_intensity=bad)
748
+ assert result.carbon_intensity_g_per_kwh == 240.0
749
+
750
+
751
+ @pytest.mark.parametrize("scenario", ["server_low", "server_high"])
752
+ def test_per_token_energy_is_offline_only(predictor, scenario):
753
+ """Per-token energy derives from batch-wide timing, which server runs do
754
+ not measure; there the useful figure is per hour instead."""
755
+ result = predictor.predict(KNOWN_LLM, KNOWN_GPU, scenario)
756
+ assert result.energy_per_million_tokens_wh is None
757
+ assert result.co2_per_million_tokens_g is None
758
+ assert result.energy_per_hour_wh is not None
759
+
760
+
761
+ def test_hourly_energy_is_server_only(predictor):
762
+ result = predictor.predict(KNOWN_LLM, KNOWN_GPU, "offline")
763
+ assert result.energy_per_hour_wh is None
764
+ assert result.co2_per_hour_g is None
765
+ assert result.energy_per_million_tokens_wh is not None
766
+
767
+
768
+ @pytest.mark.parametrize("scenario", ["server_low", "server_high"])
769
+ def test_hourly_energy_grosses_up_to_the_whole_facility(predictor, scenario):
770
+ """GPU board -> whole host (GPU is 70% of it) -> facility (PUE 1.2)."""
771
+ result = predictor.predict(KNOWN_LLM, KNOWN_GPU, scenario)
772
+
773
+ assert result.energy_per_hour_wh == pytest.approx(result.power_w / 0.7 * 1.2)
774
+ # Always more than the board alone, by that fixed factor.
775
+ assert result.energy_per_hour_wh > result.power_w
776
+ assert result.energy_per_hour_wh / result.power_w == pytest.approx(1.2 / 0.7)
777
+
778
+ assert result.co2_per_hour_g == pytest.approx(
779
+ result.energy_per_hour_wh / 1000 * result.carbon_intensity_g_per_kwh)
780
+
781
+
782
+ def test_hourly_result_shows_the_figures():
783
+ output = demo_app.predict(KNOWN_LLM, KNOWN_GPU, "server_low")
784
+ assert "Host energy per hour" in output and "CO₂e per hour" in output
785
+ assert "Energy per 1M tokens" not in output
786
+ assert "whole server" in output
787
+
788
+
789
+ # ---------------------------------------------------------------------------
790
+ # Per-request Hugging Face token
791
+ # ---------------------------------------------------------------------------
792
+
793
+ def test_token_is_passed_to_the_hub_and_not_kept(predictor, monkeypatch):
794
+ """A visitor's token must reach the Hub call and go no further."""
795
+ seen = {}
796
+
797
+ def capture(model_id, token=None):
798
+ seen["token"] = token
799
+ return predictor._resolve_llm(KNOWN_LLM)
800
+
801
+ monkeypatch.setattr("wattgpu_demo.predictor.fetch_llm_info", capture)
802
+ result = predictor.predict("some/unmeasured", KNOWN_GPU, "server_low",
803
+ hf_token="hf_secret_value")
804
+
805
+ assert seen["token"] == "hf_secret_value"
806
+ # It must not survive anywhere on the prediction.
807
+ assert "hf_secret_value" not in repr(result)
808
+
809
+
810
+ def test_token_never_reaches_the_rendered_output():
811
+ output = demo_app.predict(KNOWN_LLM, KNOWN_GPU, "offline", 240, "hf_secret_value")
812
+ assert "hf_secret_value" not in output
813
+ # Nor the share link.
814
+ assert "token" not in _share_link(output)
815
+
816
+
817
+ def test_token_is_not_echoed_back_by_the_submit_handler():
818
+ html_out, echoed = demo_app.on_submit(KNOWN_LLM, KNOWN_GPU, "offline", 240,
819
+ "hf_secret_value")
820
+ assert "hf_secret_value" not in html_out
821
+ assert echoed == 240 # only the intensity is written back
822
+
823
+
824
+ def test_token_is_never_written_to_the_demand_log(demand_log_path):
825
+ demo_app.predict("microsoft/phi-4", "Tesla T4", "offline", 240, "hf_secret_value")
826
+ contents = demand_log_path.read_text()
827
+ assert "hf_secret_value" not in contents
828
+ assert set(_entries(demand_log_path)[0]) == {"at", "model", "gpu", "scenario", "reason"}
829
+
830
+
831
+ def test_token_field_is_masked_and_empty_by_default():
832
+ assert demo_app.token_input.type == "password"
833
+ assert not demo_app.token_input.value
834
+
835
+
836
+ def test_supplied_token_wins_over_the_deployment_token(monkeypatch):
837
+ from wattgpu_demo.hf_models import _auth_headers, has_hf_token
838
+
839
+ monkeypatch.setenv("HF_TOKEN", "hf_deployment")
840
+ assert _auth_headers("hf_visitor")["Authorization"] == "Bearer hf_visitor"
841
+ assert _auth_headers()["Authorization"] == "Bearer hf_deployment"
842
+ assert _auth_headers(" ")["Authorization"] == "Bearer hf_deployment"
843
+
844
+ monkeypatch.delenv("HF_TOKEN")
845
+ assert _auth_headers() == {}
846
+ assert _auth_headers("hf_visitor") == {"Authorization": "Bearer hf_visitor"}
847
+ # has_hf_token describes the deployment, not a per-request token.
848
+ assert has_hf_token() is False
849
+
850
+
851
+ def test_gated_and_rate_limit_messages_point_at_the_token_field(monkeypatch):
852
+ import urllib.error
853
+
854
+ import wattgpu_demo.hf_models as hf
855
+
856
+ monkeypatch.delenv("HF_TOKEN", raising=False)
857
+ monkeypatch.delenv("HUGGING_FACE_HUB_TOKEN", raising=False)
858
+
859
+ for code, expected in ((401, "paste a read-only access token"),
860
+ (429, "pasting a read-only access token")):
861
+ def fail(request, *a, _code=code, **kw):
862
+ raise urllib.error.HTTPError(request.full_url, _code, "nope", {}, None)
863
+
864
+ monkeypatch.setattr(hf.urllib.request, "urlopen", fail)
865
+ with pytest.raises(WattGPUError) as excinfo:
866
+ hf.fetch_llm_info("gated/model")
867
+ message = excinfo.value.display()
868
+ assert expected in message
869
+ assert "Use your own Hugging Face token" in message
870
+
871
+
872
+ def test_analytics_are_disabled_because_the_form_carries_a_secret():
873
+ assert demo_app.demo.analytics_enabled is False
874
+
875
+
876
+ def test_clearing_the_intensity_restores_the_default():
877
+ """An empty box must not silently disagree with the number in the result."""
878
+ html, echoed = demo_app.on_submit(KNOWN_LLM, KNOWN_GPU, "offline", None)
879
+ assert echoed == emissions.DEFAULT_CARBON_INTENSITY_G_PER_KWH
880
+ assert "240 gCO₂e/kWh" in html
881
+
882
+
883
+ def test_submitting_a_valid_intensity_echoes_it_back():
884
+ html, echoed = demo_app.on_submit(KNOWN_LLM, KNOWN_GPU, "offline", 35)
885
+ assert echoed == 35
886
+ assert "35 gCO₂e/kWh" in html
887
+
888
+
889
+ def test_intensity_help_offers_electricity_maps_without_overstating():
890
+ info = demo_app.carbon_input.info
891
+ assert emissions.ELECTRICITY_MAPS_URL in info
892
+ assert "might be different" in info
893
+ assert "probably very different" not in info
894
+
895
+
896
+ def test_efficient_gpu_uses_less_energy_per_token(predictor):
897
+ small = "Qwen/Qwen2.5-1.5B-Instruct"
898
+ modern = predictor.predict(small, "H200 NVL", "offline")
899
+ older = predictor.predict(small, "Tesla T4", "offline")
900
+ assert modern.energy_per_million_tokens_wh < older.energy_per_million_tokens_wh
901
+
902
+
903
+ def test_power_card_names_the_gpu_explicitly():
904
+ """The power figure is the board's, unlike the energy figures around it."""
905
+ output = demo_app.predict(KNOWN_LLM, KNOWN_GPU, "offline")
906
+ assert "GPU mean power draw" in output
907
+
908
+
909
+ def test_share_button_matches_the_primary_button_in_both_themes():
910
+ """White on sky-500, with no theme-specific override to darken it."""
911
+ assert "color: #fff; background: #0ea5e9;" in demo_app.CSS
912
+ assert "#06202e" not in demo_app.CSS # the old dark-mode text colour
913
+
914
+
915
+ def test_watt_counts_dataset_is_cited():
916
+ assert demo_app.WATT_COUNTS_URL == "https://arxiv.org/abs/2604.09048"
917
+
918
+
919
+ def test_offline_result_shows_energy_and_emissions():
920
+ output = demo_app.predict(KNOWN_LLM, KNOWN_GPU, "offline")
921
+ assert "Energy per 1M tokens" in output
922
+ assert "CO₂e per 1M tokens" in output
923
+ assert "240 gCO₂e/kWh" in output
924
+ assert "whole server" in output
925
+
926
+
927
+ def test_the_boundary_is_explained_once_in_assumptions_not_on_every_result():
928
+ """The result stays uncluttered; the reasoning lives in the panel."""
929
+ for scenario in ("offline", "server_low"):
930
+ output = demo_app.predict(KNOWN_LLM, KNOWN_GPU, scenario)
931
+ assert "PUE" not in output
932
+ assert "70%" not in output
933
+
934
+ assert "PUE" in demo_app.ENERGY_ASSUMPTIONS
935
+ assert "70%" in demo_app.ENERGY_ASSUMPTIONS
936
+ # And why the two scenarios report different quantities.
937
+ assert "per hour" in demo_app.ENERGY_ASSUMPTIONS
938
+ assert "saturated" in demo_app.ENERGY_ASSUMPTIONS
939
+
940
+
941
+ def test_electricity_maps_is_offered_as_the_source():
942
+ """Users cannot be expected to know their grid's intensity."""
943
+ assert "app.electricitymaps.com" in emissions.ELECTRICITY_MAPS_URL
944
+
945
+
946
+ # ---------------------------------------------------------------------------
947
+ # Licence-gated models
948
+ # ---------------------------------------------------------------------------
949
+
950
+ GATED_MODELS = [
951
+ "meta-llama/Llama-3.2-1B-Instruct",
952
+ "meta-llama/Meta-Llama-3-8B-Instruct",
953
+ "google/gemma-3-1b-it",
954
+ "google/gemma-3-270m",
955
+ ]
956
+
957
+
958
+ @pytest.mark.parametrize("model_id", GATED_MODELS)
959
+ def test_gated_models_resolve_without_a_token(predictor, model_id, monkeypatch):
960
+ """Llama and Gemma work with no HF_TOKEN and no Hub call at all."""
961
+ monkeypatch.delenv("HF_TOKEN", raising=False)
962
+ monkeypatch.delenv("HUGGING_FACE_HUB_TOKEN", raising=False)
963
+ # Any Hub lookup would be a bug: these must come from the local cache.
964
+ monkeypatch.setattr("wattgpu_demo.predictor.fetch_llm_info",
965
+ lambda _: pytest.fail("hit the Hub for a cached gated model"))
966
+
967
+ result = predictor.predict(model_id, KNOWN_GPU, "server_low")
968
+ assert result.llm.params_source == "published config"
969
+ assert result.llm.total_b_params > 0
970
+ assert result.power_w > 0
971
+
972
+
973
+ @pytest.mark.parametrize("model_id", GATED_MODELS)
974
+ def test_gated_models_are_not_treated_as_measured(predictor, model_id):
975
+ """Knowing the architecture is not the same as having measured the model."""
976
+ assert model_id not in predictor.profiled_models
977
+ assert model_id in predictor.known_models
978
+ result = predictor.predict(model_id, KNOWN_GPU, "server_low")
979
+ assert result.certainty.tier == "yellow"
980
+ # And the estimate still carries the config-derived caveat.
981
+ assert any("config.json" in n for n in result.notes)
982
+
983
+
984
+ def test_rate_limiting_is_explained_and_does_not_break_cached_models(predictor, monkeypatch):
985
+ """The Hub rate-limits anonymous traffic per IP, which a busy public Space
986
+ shares across all its visitors. Cached models must keep working."""
987
+ import urllib.error
988
+
989
+ import wattgpu_demo.hf_models as hf
990
+
991
+ def rate_limited(request, *args, **kwargs):
992
+ raise urllib.error.HTTPError(request.full_url, 429, "Too Many Requests", {}, None)
993
+
994
+ monkeypatch.setattr(hf.urllib.request, "urlopen", rate_limited)
995
+
996
+ with pytest.raises(NetworkError) as excinfo:
997
+ predictor.predict("some/unmeasured-model", KNOWN_GPU, "server_low")
998
+ assert "rate-limiting" in excinfo.value.display()
999
+
1000
+ # Measured and gated-cached models need no Hub call, so they still answer.
1001
+ assert predictor.predict(KNOWN_LLM, KNOWN_GPU, "server_low").power_w > 0
1002
+ assert predictor.predict("google/gemma-3-1b-it", KNOWN_GPU, "server_low").power_w > 0
1003
+
1004
+
1005
+ def test_gated_cache_records_its_source(predictor):
1006
+ """Every cached value must be traceable to the mirror it came from."""
1007
+ for entry in predictor._gated_llms.values():
1008
+ assert entry["source_mirror"].count("/") == 1
1009
+ assert 0 < entry["total_b_params"] < 1000
1010
+ assert entry["num_layers"] > 0 and entry["hidden_size"] > 0
1011
+
1012
+
1013
+ def test_known_models_are_offered_before_typing():
1014
+ assert demo_app.model_input.choices == [
1015
+ (m, m) for m in demo_app.predictor.known_models]
1016
+
1017
+
1018
+ # ---------------------------------------------------------------------------
1019
+ # Permalinks
1020
+ # ---------------------------------------------------------------------------
1021
+
1022
+ class _FakeRequest:
1023
+ def __init__(self, params):
1024
+ self.query_params = params
1025
+
1026
+
1027
+ def _share_link(output: str) -> str:
1028
+ """The permalink the share button carries.
1029
+
1030
+ Read the way a browser would: `&` is written `&amp;` inside an attribute,
1031
+ and the DOM decodes it before any script sees it.
1032
+ """
1033
+ raw = re.search(r'class="wg-share" data-href="([^"]+)"', output).group(1)
1034
+ return html.unescape(raw)
1035
+
1036
+
1037
+ def _value(component):
1038
+ """The value a Gradio update carries, or the raw value if it is not one."""
1039
+ args = getattr(component, "constructor_args", None)
1040
+ return args.get("value") if isinstance(args, dict) else component
1041
+
1042
+
1043
+ def _is_untouched(component) -> bool:
1044
+ """True for `gr.skip()`, which leaves a component exactly as it was."""
1045
+ return component == {"__type__": "update"}
1046
+
1047
+
1048
+ def test_result_carries_a_relative_permalink():
1049
+ output = demo_app.predict(KNOWN_LLM, KNOWN_GPU, "server_high")
1050
+ href = _share_link(output)
1051
+
1052
+ assert href.startswith("?") # relative, so it works at any deployed URL
1053
+ params = dict(urllib.parse.parse_qsl(href[1:]))
1054
+ assert params == {"model": KNOWN_LLM, "gpu": KNOWN_GPU, "scenario": "server_high"}
1055
+
1056
+
1057
+ def test_permalink_round_trips_into_the_form():
1058
+ output = demo_app.predict(KNOWN_LLM, KNOWN_GPU, "offline")
1059
+ href = _share_link(output)
1060
+ params = dict(urllib.parse.parse_qsl(href[1:]))
1061
+
1062
+ model, gpu, scenario, carbon, result = demo_app.restore_from_link(_FakeRequest(params))
1063
+ assert _value(model) == KNOWN_LLM
1064
+ assert _value(gpu) == KNOWN_GPU
1065
+ assert _value(scenario) == "offline"
1066
+ assert _value(carbon) == emissions.DEFAULT_CARBON_INTENSITY_G_PER_KWH
1067
+ assert "wg-chip" in result
1068
+
1069
+
1070
+ def test_permalink_ignores_an_unknown_gpu_rather_than_opening_on_an_error():
1071
+ updates = demo_app.restore_from_link(
1072
+ _FakeRequest({"model": KNOWN_LLM, "gpu": "No Such GPU", "scenario": "offline"}))
1073
+ assert all(_is_untouched(component) for component in updates)
1074
+
1075
+
1076
+ def test_permalink_falls_back_on_invalid_values():
1077
+ *_, scenario, carbon, result = demo_app.restore_from_link(
1078
+ _FakeRequest({"model": KNOWN_LLM, "gpu": KNOWN_GPU,
1079
+ "scenario": "'; drop", "carbon": "not-a-number"}))
1080
+ assert _value(scenario) == "server_low"
1081
+ assert _value(carbon) == emissions.DEFAULT_CARBON_INTENSITY_G_PER_KWH
1082
+ assert "wg-chip" in result
1083
+
1084
+
1085
+ def test_permalink_carries_a_non_default_carbon_intensity():
1086
+ output = demo_app.predict(KNOWN_LLM, KNOWN_GPU, "offline", 35)
1087
+ href = _share_link(output)
1088
+ params = dict(urllib.parse.parse_qsl(href[1:]))
1089
+ assert params["carbon"] == "35"
1090
+
1091
+ *_, carbon, result = demo_app.restore_from_link(_FakeRequest(params))
1092
+ assert _value(carbon) == 35
1093
+ assert "35 gCO₂e/kWh" in result
1094
+
1095
+
1096
+ def test_permalink_omits_the_default_carbon_intensity():
1097
+ """Ordinary links stay short."""
1098
+ output = demo_app.predict(KNOWN_LLM, KNOWN_GPU, "offline")
1099
+ href = _share_link(output)
1100
+ assert "carbon" not in href
1101
+
1102
+
1103
+ def test_page_without_parameters_is_left_alone():
1104
+ assert all(_is_untouched(c) for c in demo_app.restore_from_link(_FakeRequest({})))
1105
+
1106
+
1107
+ # ---------------------------------------------------------------------------
1108
+ # Demand log
1109
+ # ---------------------------------------------------------------------------
1110
+
1111
+ @pytest.fixture
1112
+ def demand_log_path(tmp_path, monkeypatch):
1113
+ path = tmp_path / "demand.jsonl"
1114
+ monkeypatch.setenv("WATTGPU_DEMAND_LOG", str(path))
1115
+ monkeypatch.delenv("WATTGPU_DEMAND_LOG_DISABLED", raising=False)
1116
+ return path
1117
+
1118
+
1119
+ def _entries(path):
1120
+ return [json.loads(line) for line in path.read_text().splitlines()]
1121
+
1122
+
1123
+ @pytest.mark.parametrize("model_id,gpu_name,expected_reason", [
1124
+ ("microsoft/phi-4", "Tesla T4", "does-not-fit"),
1125
+ ])
1126
+ def test_refusals_are_logged_as_demand(demand_log_path, model_id, gpu_name, expected_reason):
1127
+ demo_app.predict(model_id, gpu_name, "offline")
1128
+
1129
+ entries = _entries(demand_log_path)
1130
+ assert len(entries) == 1
1131
+ assert entries[0]["model"] == model_id
1132
+ assert entries[0]["gpu"] == gpu_name
1133
+ assert entries[0]["reason"] == expected_reason
1134
+ assert entries[0]["scenario"] == "offline"
1135
+ # Nothing about who asked.
1136
+ assert set(entries[0]) == {"at", "model", "gpu", "scenario", "reason"}
1137
+
1138
+
1139
+ def test_typos_are_not_logged_as_demand(demand_log_path):
1140
+ """A misspelled name is not evidence anybody wants that pair covered."""
1141
+ demo_app.predict("nonexistent/typo-xyz", KNOWN_GPU, "offline")
1142
+ demo_app.predict(KNOWN_LLM, "No Such GPU 9000", "offline")
1143
+ assert not demand_log_path.exists()
1144
+
1145
+
1146
+ def test_successful_estimates_are_not_logged(demand_log_path):
1147
+ demo_app.predict(KNOWN_LLM, KNOWN_GPU, "offline")
1148
+ assert not demand_log_path.exists()
1149
+
1150
+
1151
+ def test_demand_log_can_be_disabled(demand_log_path, monkeypatch):
1152
+ monkeypatch.setenv("WATTGPU_DEMAND_LOG_DISABLED", "1")
1153
+ demo_app.predict("microsoft/phi-4", "Tesla T4", "offline")
1154
+ assert not demand_log_path.exists()
1155
+
1156
+
1157
+ def test_demand_log_survives_an_unwritable_path(monkeypatch):
1158
+ """Logging must never take down a response."""
1159
+ monkeypatch.setenv("WATTGPU_DEMAND_LOG", "/proc/nope/cannot-write.jsonl")
1160
+ output = demo_app.predict("microsoft/phi-4", "Tesla T4", "offline")
1161
+ assert "Does not fit" in output
1162
+
1163
+
1164
+ def test_demand_summary_ranks_by_frequency(demand_log_path):
1165
+ for _ in range(3):
1166
+ demand_log.record_refusal("a/moe", "H200 NVL", "offline", "moe")
1167
+ demand_log.record_refusal("b/quant", "L4", "server_low", "quantised")
1168
+
1169
+ assert demand_log.summarise(str(demand_log_path))[0] == (
1170
+ "moe: a/moe @ H200 NVL", 3)
1171
+
1172
+
1173
+ def test_callback_escapes_html_in_user_input():
1174
+ output = demo_app.predict("<script>alert(1)</script>/x", "L40S", "offline")
1175
+ assert "<script>" not in output
1176
+ assert "&lt;script&gt;" in output
wattgpu_demo/__init__.py ADDED
File without changes
wattgpu_demo/demand_log.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Record which (model, GPU) pairs the demo cannot answer.
2
+
3
+ Every refusal is a request someone wanted and did not get, so the log doubles
4
+ as a demand-ranked coverage roadmap: it says which models and which GPUs are
5
+ worth measuring or supporting next.
6
+
7
+ Only the query itself is written -- the model id, the GPU name, the scenario
8
+ and why it was refused. No identifiers, no addresses, nothing about who asked.
9
+ The UI discloses that this happens.
10
+
11
+ On Hugging Face Spaces the container filesystem is ephemeral, so the log is
12
+ lost on restart unless `WATTGPU_DEMAND_LOG` points at persistent storage.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import collections
18
+ import json
19
+ import os
20
+ import threading
21
+ from datetime import datetime, timezone
22
+
23
+ DEFAULT_LOG_PATH = os.path.join(
24
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
25
+ "data", "demand_log.jsonl",
26
+ )
27
+
28
+ # Appends are short, but several viewers can hit the app at once.
29
+ _LOCK = threading.Lock()
30
+
31
+ # Truncated so a pathological input cannot bloat the log.
32
+ MAX_FIELD_CHARS = 200
33
+
34
+
35
+ def log_path() -> str:
36
+ return os.environ.get("WATTGPU_DEMAND_LOG", DEFAULT_LOG_PATH)
37
+
38
+
39
+ def is_enabled() -> bool:
40
+ """Logging is on unless explicitly disabled."""
41
+ return os.environ.get("WATTGPU_DEMAND_LOG_DISABLED", "").lower() not in ("1", "true", "yes")
42
+
43
+
44
+ def record_refusal(model_id: str, gpu_name: str, scenario: str, reason: str) -> None:
45
+ """Append one refused query. Never raises: logging must not break a response."""
46
+ if not is_enabled():
47
+ return
48
+
49
+ entry = {
50
+ "at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
51
+ "model": str(model_id or "")[:MAX_FIELD_CHARS],
52
+ "gpu": str(gpu_name or "")[:MAX_FIELD_CHARS],
53
+ "scenario": str(scenario or "")[:MAX_FIELD_CHARS],
54
+ "reason": reason,
55
+ }
56
+ try:
57
+ path = log_path()
58
+ directory = os.path.dirname(path)
59
+ if directory:
60
+ os.makedirs(directory, exist_ok=True)
61
+ with _LOCK, open(path, "a", encoding="utf-8") as fh:
62
+ fh.write(json.dumps(entry) + "\n")
63
+ except OSError:
64
+ pass # a read-only or full filesystem must not take the demo down
65
+
66
+
67
+ def summarise(path: str | None = None, limit: int = 20) -> list[tuple[str, int]]:
68
+ """Most-requested refused queries, for deciding what to cover next."""
69
+ counts: collections.Counter[str] = collections.Counter()
70
+ try:
71
+ with open(path or log_path(), encoding="utf-8") as fh:
72
+ for line in fh:
73
+ try:
74
+ entry = json.loads(line)
75
+ except json.JSONDecodeError:
76
+ continue
77
+ counts[f"{entry.get('reason')}: {entry.get('model')} @ {entry.get('gpu')}"] += 1
78
+ except OSError:
79
+ return []
80
+ return counts.most_common(limit)
wattgpu_demo/emissions.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Turn predicted energy into per-million-token figures and CO2e.
2
+
3
+ Energy per million tokens is the honest core here: it comes straight from the
4
+ predicted power and latency, needs no assumption about who is running the GPU
5
+ or where, and does not go stale. Emissions are a single visible multiplier on
6
+ top of it -- the grid's carbon intensity, which the user can change.
7
+
8
+ Every figure here is at the **facility level**: the predicted GPU board power is
9
+ grossed up to the whole host, then multiplied by the data centre's PUE, so what
10
+ is reported is roughly what the wall socket sees rather than what the card
11
+ draws. The same scaling is applied in both scenarios, so the offline per-token
12
+ and the server per-hour numbers describe the same system boundary.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ JOULES_PER_WH = 3600.0
18
+ TOKENS_PER_MILLION = 1_000_000
19
+
20
+ # EU-27 average grid carbon intensity, approximately, in gCO2e per kWh. It is a
21
+ # yearly average across a whole continent, so it is a reference point rather
22
+ # than a figure for any particular deployment: real intensity swings by a factor
23
+ # of ten between regions, and by the hour within one.
24
+ DEFAULT_CARBON_INTENSITY_G_PER_KWH = 240.0
25
+ DEFAULT_CARBON_INTENSITY_LABEL = "EU average"
26
+
27
+ # Live and historical intensity by region.
28
+ ELECTRICITY_MAPS_URL = "https://app.electricitymaps.com/map/live/fifteen_minutes"
29
+
30
+ # Guards against nonsense typed into the input. The low end is below the
31
+ # cleanest grids (Iceland, Norway), the high end above the dirtiest.
32
+ MIN_CARBON_INTENSITY = 0.0
33
+ MAX_CARBON_INTENSITY = 2000.0
34
+
35
+ # Scaling the GPU board's draw up to what the wall socket sees.
36
+ #
37
+ # The GPU dominates an inference server but is not all of it: CPU, memory,
38
+ # storage, NICs and fans draw the rest. Taking the GPU as ~70% of the host's
39
+ # total is a common rule of thumb for a single-accelerator inference node.
40
+ GPU_SHARE_OF_HOST_POWER = 0.7
41
+
42
+ # Power usage effectiveness: facility draw divided by IT draw, covering cooling
43
+ # and power distribution. 1.2 is typical of a modern hyperscale data centre;
44
+ # older or smaller sites are worse.
45
+ DEFAULT_PUE = 1.2
46
+
47
+
48
+ def facility_power_w(gpu_power_w: float,
49
+ gpu_share: float = GPU_SHARE_OF_HOST_POWER,
50
+ pue: float = DEFAULT_PUE) -> float:
51
+ """Wall-socket power for one GPU's share of a server.
52
+
53
+ Grosses the predicted board power up to the whole host, then applies the
54
+ data centre's overhead on top.
55
+ """
56
+ return gpu_power_w / gpu_share * pue
57
+
58
+
59
+ def energy_per_hour_wh(gpu_power_w: float) -> float:
60
+ """Facility-level watt-hours for one hour of serving. Numerically the
61
+ facility power, since one hour at W watts is W watt-hours."""
62
+ return facility_power_w(gpu_power_w)
63
+
64
+
65
+ def co2_per_hour_g(energy_wh: float, carbon_intensity_g_per_kwh: float) -> float:
66
+ """Grams of CO2e for one hour at a given grid intensity."""
67
+ return (energy_wh / 1000.0) * carbon_intensity_g_per_kwh
68
+
69
+
70
+ def energy_per_million_tokens_wh(gpu_energy_per_token_j: float) -> float:
71
+ """Facility watt-hours to generate a million tokens.
72
+
73
+ Takes the GPU board's energy per token, grosses it up to the whole server
74
+ and applies the data-centre overhead, so it is comparable with the hourly
75
+ figure reported under server load.
76
+ """
77
+ facility_j = facility_power_w(gpu_energy_per_token_j)
78
+ return facility_j * TOKENS_PER_MILLION / JOULES_PER_WH
79
+
80
+
81
+ def co2_per_million_tokens_g(energy_wh: float, carbon_intensity_g_per_kwh: float) -> float:
82
+ """Grams of CO2e for a million tokens at a given grid intensity."""
83
+ return (energy_wh / 1000.0) * carbon_intensity_g_per_kwh
84
+
85
+
86
+ def clean_carbon_intensity(value: float | int | str | None) -> float:
87
+ """Coerce user input to a usable intensity, falling back to the default."""
88
+ try:
89
+ intensity = float(value) # type: ignore[arg-type]
90
+ except (TypeError, ValueError):
91
+ return DEFAULT_CARBON_INTENSITY_G_PER_KWH
92
+ if not MIN_CARBON_INTENSITY <= intensity <= MAX_CARBON_INTENSITY:
93
+ return DEFAULT_CARBON_INTENSITY_G_PER_KWH
94
+ return intensity
95
+
96
+
97
+ def format_energy(watt_hours: float) -> str:
98
+ if watt_hours >= 1000:
99
+ return f"{watt_hours / 1000:,.3g} kWh"
100
+ if watt_hours >= 1:
101
+ return f"{watt_hours:.3g} Wh"
102
+ return f"{watt_hours * 1000:.3g} mWh"
103
+
104
+
105
+ def format_co2(grams: float) -> str:
106
+ if grams >= 1000:
107
+ return f"{grams / 1000:,.3g} kg"
108
+ if grams >= 1:
109
+ return f"{grams:.3g} g"
110
+ return f"{grams * 1000:.3g} mg"
wattgpu_demo/errors.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """User-facing errors.
2
+
3
+ Every failure path in the demo raises a `WattGPUError` carrying a message
4
+ that is safe and useful to show directly in the UI: what went wrong, and
5
+ what the user can do about it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+
11
+ class WattGPUError(Exception):
12
+ """An error with a message meant to be displayed to the user."""
13
+
14
+ def __init__(self, message: str, hint: str | None = None) -> None:
15
+ super().__init__(message)
16
+ self.message = message
17
+ self.hint = hint
18
+
19
+ def display(self) -> str:
20
+ return f"{self.message}\n\n{self.hint}" if self.hint else self.message
21
+
22
+
23
+ class ModelNotFoundError(WattGPUError):
24
+ """The Hugging Face repository does not exist, or is gated/private."""
25
+
26
+
27
+ class ModelConfigError(WattGPUError):
28
+ """The repository exists but does not look like a supported LLM."""
29
+
30
+
31
+ class GPUNotFoundError(WattGPUError):
32
+ """The GPU is not in the specification database."""
33
+
34
+
35
+ class NetworkError(WattGPUError):
36
+ """The Hugging Face Hub could not be reached."""
37
+
38
+
39
+ class UnsupportedModelError(WattGPUError):
40
+ """The model is of a kind WattGPU does not cover: MoE or quantised."""
41
+
42
+
43
+ class InsufficientMemoryError(WattGPUError):
44
+ """The model's weights do not fit in the GPU's usable memory."""
wattgpu_demo/features.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Feature engineering shared by training and inference.
2
+
3
+ The transformations here mirror `WattGPU.ipynb` exactly. Keeping one
4
+ implementation is what guarantees that a row built at inference time from a
5
+ `config.json` and a dbgpu specification lands in the same feature space as the
6
+ rows the models were fitted on.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+
13
+ import numpy as np
14
+ import pandas as pd
15
+
16
+ # Operational scenarios, derived in the paper from the Poisson arrival rate.
17
+ # These are three *measured operating points*, not ranges. Every server run in
18
+ # the dataset used one of exactly two Poisson arrival rates, and the offline
19
+ # runs kept the GPU permanently saturated:
20
+ #
21
+ # server_low 0.017 requests/s
22
+ # server_high 0.330 requests/s
23
+ # offline saturated, requests always queued
24
+ #
25
+ # Converted at a typical one message per minute per active user
26
+ # (0.0167 requests/s each), those rates are about 1 and about 20 concurrent
27
+ # users. The model takes the scenario as a category, so it can only answer at
28
+ # these three points -- there is no interpolation to an arbitrary request rate.
29
+ SCENARIOS = ["server_low", "server_high", "offline"]
30
+ SCENARIO_RATES_QPS = {"server_low": 0.017, "server_high": 0.330, "offline": None}
31
+ SCENARIO_LABELS = {
32
+ "server_low": "Low load — a single concurrent user",
33
+ "server_high": "Medium load — about 20 concurrent users",
34
+ "offline": "Offline / batch",
35
+ }
36
+
37
+ # Offline runs record only whole-batch timing, so the latency target means
38
+ # something different there: wall-clock time per token across every request in
39
+ # flight, rather than the gap one user sees between tokens. Its inverse is
40
+ # therefore the batch's aggregate throughput, which is how the UI reports it.
41
+ # Under server load the target is a genuine per-user latency, and system
42
+ # throughput cannot be recovered from it without a model of batching.
43
+ SCENARIOS_WITH_PER_USER_LATENCY = ("server_low", "server_high")
44
+
45
+ # Identifier columns kept for grouping but never used as features.
46
+ ID_COLUMNS = ["gpu_db_name", "model"]
47
+
48
+ # §4.1 feature set for mean power draw (target: normalized_power_draw).
49
+ POWER_FEATURES = [
50
+ "scenario",
51
+ "log_total_b_params",
52
+ "model_type",
53
+ "num_key_value_heads",
54
+ "num_layers",
55
+ "memory_bandwidth_gb_s",
56
+ "boost_percentage",
57
+ "transistor_count_m",
58
+ "base_clock_mhz",
59
+ "release_year",
60
+ "bandwidth_latency",
61
+ ]
62
+
63
+ # §4.2 feature set for inter-token latency (target: itl).
64
+ ITL_FEATURES = [
65
+ "scenario",
66
+ "model_type",
67
+ "total_b_params",
68
+ "hidden_size",
69
+ "num_attention_heads",
70
+ "num_key_value_heads",
71
+ "num_layers",
72
+ "memory_bandwidth_gb_s",
73
+ "memory_size_gb",
74
+ "memory_type",
75
+ "tflops_16b",
76
+ "release_year",
77
+ "bandwidth_latency",
78
+ "compute_latency_s",
79
+ ]
80
+
81
+ POWER_TARGET = "normalized_power_draw"
82
+ ITL_TARGET = "itl"
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Derived features (single source of truth for training and inference)
87
+ # ---------------------------------------------------------------------------
88
+
89
+ def add_derived_features(df: pd.DataFrame) -> pd.DataFrame:
90
+ """Add the engineered GPU, LLM and pair features used by both models."""
91
+ df = df.copy()
92
+
93
+ df["boost_percentage"] = df["boost_clock_mhz"] / df["base_clock_mhz"]
94
+ df["log_total_b_params"] = np.log(df["total_b_params"])
95
+
96
+ # Bandwidth latency: time to read all FP16 weights from GPU memory once.
97
+ df["bandwidth_latency"] = (df["total_b_params"] * 2) / df["memory_bandwidth_gb_s"]
98
+
99
+ # Compute latency: lower bound on the time to compute one token at peak FLOPS.
100
+ df["compute_latency_s"] = (2 * df["total_b_params"] * 1e9) / (df["tflops_16b"] * 1e12)
101
+
102
+ return df
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # Training frame (replicates the notebook's preprocessing)
107
+ # ---------------------------------------------------------------------------
108
+
109
+ def _convert_column_to_list(df: pd.DataFrame, column: str) -> None:
110
+ df[column] = df[column].apply(eval)
111
+
112
+
113
+ def _compute_itl_per_request(row: pd.Series) -> float:
114
+ """Mean ITL for a server-scenario row, averaged over its requests.
115
+
116
+ ITL is undefined for requests generating fewer than two tokens, so those
117
+ are dropped.
118
+ """
119
+ e2e = np.array(row["e2e_request_latency_seconds_events"])
120
+ ttft = np.array(row["time_to_first_token_seconds_events"])
121
+ tokens = np.array(row["request_generation_tokens"])
122
+
123
+ valid = tokens >= 2
124
+ if not valid.any():
125
+ return np.nan
126
+ return ((e2e[valid] - ttft[valid]) / (tokens[valid] - 1)).mean()
127
+
128
+
129
+ def build_training_frame(data_dir: str) -> pd.DataFrame:
130
+ """Load the Watt Counts subset and produce the modelling frame.
131
+
132
+ Follows `WattGPU.ipynb`: expand the per-request event lists, merge LLM and
133
+ GPU metadata, derive the operational scenario, restrict to dense models,
134
+ and engineer the features.
135
+ """
136
+ df = pd.read_csv(os.path.join(data_dir, "watt_counts_subset.csv"), index_col=0)
137
+
138
+ for column in (
139
+ "time_to_first_token_seconds_events",
140
+ "e2e_request_latency_seconds_events",
141
+ "request_generation_tokens",
142
+ ):
143
+ _convert_column_to_list(df, column)
144
+
145
+ # LLM metadata from Hugging Face; drop columns that are mostly empty.
146
+ df_models = pd.read_csv(os.path.join(data_dir, "model_features.csv"), sep=";", index_col=0)
147
+ df_models = df_models[df_models.columns[df_models.isna().sum() / len(df_models) < 0.1]]
148
+ df = df.merge(df_models, left_on="model", right_index=True, how="left")
149
+
150
+ # GPU metadata from manufacturer specifications.
151
+ df_gpus = pd.read_csv(os.path.join(data_dir, "gpu_features.csv"), index_col=0, sep=";")
152
+ df = df.merge(df_gpus, left_on="gpu_type", right_index=True, how="left")
153
+
154
+ # Scenario comes from the Poisson arrival rate; rows without one are offline.
155
+ df.loc[df.lambda_qps < 0.2, "scenario"] = "server_low"
156
+ df.loc[df.lambda_qps > 0.2, "scenario"] = "server_high"
157
+ df.loc[df.lambda_qps.isna(), "scenario"] = "offline"
158
+
159
+ df = df[df.generation_tokens_total != 0]
160
+
161
+ # Peak dense FP16 tensor throughput, which TechPowerUp does not publish.
162
+ df["tflops_16b"] = df["tensor_tflops"]
163
+
164
+ # MoE models are excluded: their inference pattern needs separate modelling.
165
+ df = df[df.total_b_params == df.active_b_params].copy()
166
+
167
+ df["normalized_power_draw"] = df["gpu_0_power_draw_watts_mean"] / df["thermal_design_power_w"]
168
+ df = add_derived_features(df)
169
+
170
+ # ITL: wall time per token offline, mean per-request decode latency on server.
171
+ offline = df.scenario == "offline"
172
+ df.loc[offline, "itl"] = (
173
+ df.loc[offline, "measurement_duration"] / df.loc[offline, "generation_tokens_total"]
174
+ )
175
+ df.loc[~offline, "itl"] = df.loc[~offline].apply(_compute_itl_per_request, axis=1)
176
+
177
+ return df
178
+
179
+
180
+ def clean_frame(df: pd.DataFrame, columns: list[str]) -> pd.DataFrame:
181
+ """Select `columns`, drop uninformative ones, and drop rows with NaNs."""
182
+ out = df.loc[:, columns]
183
+ out = out.dropna(axis=1, how="all")
184
+ out = out.loc[:, out.nunique(dropna=True) > 1]
185
+ return out.dropna(axis=0, how="any")
wattgpu_demo/gpu_db.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Look-ups over the GPU specification database built by `scripts/build_gpu_db.py`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import difflib
6
+ import os
7
+ from dataclasses import dataclass, field
8
+ from typing import Any
9
+
10
+ import pandas as pd
11
+
12
+ from .errors import GPUNotFoundError
13
+
14
+ DEFAULT_DB_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
15
+ "data", "gpu_database.csv")
16
+
17
+
18
+ @dataclass
19
+ class GPUInfo:
20
+ """One GPU's specifications, as WattGPU consumes them."""
21
+
22
+ gpu_db_name: str
23
+ manufacturer: str
24
+ architecture: str | None
25
+ memory_size_gb: float
26
+ memory_bandwidth_gb_s: float
27
+ memory_type: str | None
28
+ base_clock_mhz: float | None
29
+ boost_clock_mhz: float | None
30
+ transistor_count_m: float | None
31
+ thermal_design_power_w: float
32
+ release_year: int | None
33
+ tflops_16b: float | None
34
+ tflops_source: str
35
+ tpu_url: str | None = None
36
+ warnings: list[str] = field(default_factory=list)
37
+
38
+ def spec_rows(self) -> list[tuple[str, str]]:
39
+ tflops = (f"{self.tflops_16b:.0f} ({self.tflops_source})"
40
+ if self.tflops_16b is not None else "unknown")
41
+ return [
42
+ ("GPU", self.gpu_db_name),
43
+ ("Manufacturer", self.manufacturer),
44
+ ("Architecture", self.architecture or "—"),
45
+ ("Released", str(self.release_year or "—")),
46
+ ("Memory", f"{self.memory_size_gb:.0f} GB {self.memory_type or ''}".strip()),
47
+ ("Memory bandwidth", f"{self.memory_bandwidth_gb_s:,.0f} GB/s"),
48
+ ("Clocks", f"{self.base_clock_mhz or '—'} / {self.boost_clock_mhz or '—'} MHz"),
49
+ ("TDP", f"{self.thermal_design_power_w:.0f} W"),
50
+ ("FP16 tensor", f"{tflops} TFLOP/s"),
51
+ ]
52
+
53
+
54
+ def _to_float(value: Any) -> float | None:
55
+ return None if value is None or pd.isna(value) else float(value)
56
+
57
+
58
+ class GPUDatabase:
59
+ """The demo's GPU specifications, keyed by TechPowerUp product name."""
60
+
61
+ def __init__(self, path: str = DEFAULT_DB_PATH) -> None:
62
+ if not os.path.exists(path):
63
+ raise GPUNotFoundError(
64
+ "The GPU specification database is missing.",
65
+ f"Expected it at `{path}`. Run `python scripts/build_gpu_db.py` to create it.",
66
+ )
67
+ self.frame = pd.read_csv(path)
68
+ self._by_name = {name.lower(): name for name in self.frame["gpu_db_name"]}
69
+
70
+ @property
71
+ def names(self) -> list[str]:
72
+ return self.frame["gpu_db_name"].tolist()
73
+
74
+ def search(self, query: str, limit: int = 25) -> list[str]:
75
+ """Substring search over the catalogue, best matches first.
76
+
77
+ Names that start with the query rank above names that merely contain
78
+ it, so typing "a100" surfaces the A100 variants before the boards that
79
+ happen to mention it.
80
+ """
81
+ query = (query or "").strip().lower()
82
+ if len(query) < 2:
83
+ return []
84
+
85
+ starts, contains = [], []
86
+ for name in self.names:
87
+ lowered = name.lower()
88
+ if lowered.startswith(query):
89
+ starts.append(name)
90
+ elif query in lowered:
91
+ contains.append(name)
92
+ return (starts + contains)[:limit]
93
+
94
+ def get(self, name: str) -> GPUInfo:
95
+ """Resolve a GPU by name, case-insensitively.
96
+
97
+ Raises `GPUNotFoundError` with close-match suggestions when the name
98
+ is not in the database.
99
+ """
100
+ name = (name or "").strip()
101
+ if not name:
102
+ raise GPUNotFoundError(
103
+ "No GPU selected.",
104
+ "Pick one from the list, or type a product name such as `A100 SXM4 80 GB`.",
105
+ )
106
+
107
+ resolved = self._by_name.get(name.lower())
108
+ if resolved is None:
109
+ suggestions = difflib.get_close_matches(name, self.names, n=5, cutoff=0.5)
110
+ hint = (f"Did you mean: {', '.join(f'`{s}`' for s in suggestions)}?"
111
+ if suggestions else
112
+ "Use the TechPowerUp product name, for example `A100 SXM4 80 GB`. "
113
+ "The list only covers accelerators with at least 6 GB of memory.")
114
+ raise GPUNotFoundError(f"**{name}** is not in the GPU database.", hint)
115
+
116
+ row = self.frame[self.frame["gpu_db_name"] == resolved].iloc[0]
117
+ warnings: list[str] = []
118
+
119
+ tflops = _to_float(row.get("tensor_tflops_16b"))
120
+ source = str(row.get("tensor_tflops_source") or "unknown")
121
+ if tflops is None:
122
+ warnings.append(
123
+ f"No FP16 tensor throughput known for {resolved}. Power is still "
124
+ "estimated; latency is not."
125
+ )
126
+ elif source == "estimated":
127
+ warnings.append(
128
+ f"FP16 throughput for {resolved} is estimated from its tensor-core "
129
+ "count and clock, not taken from a datasheet."
130
+ )
131
+
132
+ return GPUInfo(
133
+ gpu_db_name=resolved,
134
+ manufacturer=str(row.get("manufacturer") or "—"),
135
+ architecture=None if pd.isna(row.get("architecture")) else str(row["architecture"]),
136
+ memory_size_gb=float(row["memory_size_gb"]),
137
+ memory_bandwidth_gb_s=float(row["memory_bandwidth_gb_s"]),
138
+ memory_type=None if pd.isna(row.get("memory_type")) else str(row["memory_type"]),
139
+ base_clock_mhz=_to_float(row.get("base_clock_mhz")),
140
+ boost_clock_mhz=_to_float(row.get("boost_clock_mhz")),
141
+ transistor_count_m=_to_float(row.get("transistor_count_m")),
142
+ thermal_design_power_w=float(row["thermal_design_power_w"]),
143
+ release_year=None if pd.isna(row.get("release_year")) else int(row["release_year"]),
144
+ tflops_16b=tflops,
145
+ tflops_source=source,
146
+ tpu_url=None if pd.isna(row.get("tpu_url")) else str(row["tpu_url"]),
147
+ warnings=warnings,
148
+ )
wattgpu_demo/hf_models.py ADDED
@@ -0,0 +1,526 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fetch the LLM metadata WattGPU needs from the Hugging Face Hub.
2
+
3
+ The models consume six architecture fields (`model_type`, `num_layers`,
4
+ `hidden_size`, `num_attention_heads`, `num_key_value_heads`,
5
+ `total_b_params`). Five come from `config.json`; the parameter count comes
6
+ from the Hub's safetensors index, with a config-derived estimate as fallback
7
+ for repositories that do not publish one.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ import urllib.error
15
+ import urllib.parse
16
+ import urllib.request
17
+ from dataclasses import dataclass, field
18
+ from typing import Any
19
+
20
+ from .errors import (
21
+ ModelConfigError,
22
+ ModelNotFoundError,
23
+ NetworkError,
24
+ UnsupportedModelError,
25
+ WattGPUError,
26
+ )
27
+
28
+ # Direct contact for coverage requests and collaboration.
29
+ CONTACT_EMAIL = "mfa@leaner-ai.com"
30
+ CONTACT_URL = f"mailto:{CONTACT_EMAIL}"
31
+
32
+ HF_ENDPOINT = "https://huggingface.co"
33
+ USER_AGENT = "wattgpu-demo/1.0"
34
+ TIMEOUT_S = 20
35
+
36
+ # `config.json` keys that mean the same thing across architecture families.
37
+ FIELD_ALIASES: dict[str, tuple[str, ...]] = {
38
+ "num_layers": ("num_hidden_layers", "n_layer", "num_layers", "n_layers"),
39
+ "hidden_size": ("hidden_size", "n_embd", "d_model", "hidden_dim"),
40
+ "num_attention_heads": ("num_attention_heads", "n_head", "n_heads", "num_heads"),
41
+ "num_key_value_heads": ("num_key_value_heads", "num_kv_heads", "n_head_kv"),
42
+ "intermediate_size": ("intermediate_size", "n_inner", "ffn_dim", "d_ff"),
43
+ "vocab_size": ("vocab_size",),
44
+ }
45
+
46
+ # Config keys that indicate a mixture-of-experts model. MoE inference activates
47
+ # only a subset of the weights per token, so its power and latency behave
48
+ # differently from the dense models WattGPU was trained on.
49
+ MOE_KEYS = (
50
+ "num_experts",
51
+ "num_local_experts",
52
+ "n_routed_experts",
53
+ "moe_num_experts",
54
+ "num_experts_per_tok",
55
+ "moe_intermediate_size",
56
+ "n_shared_experts",
57
+ "moe_layer_freq",
58
+ "shared_expert_intermediate_size",
59
+ )
60
+
61
+ # Quantised weights change both the memory traffic and the arithmetic a GPU
62
+ # does per token. Every profiled run used FP16, so quantised models are out of
63
+ # scope. `quantization_config` is written by transformers for GPTQ, AWQ,
64
+ # bitsandbytes, compressed-tensors, FP8 and friends, and is the reliable signal.
65
+ QUANTIZATION_CONFIG_KEYS = ("quantization_config", "quantization")
66
+
67
+ # Dtypes that are not a 16-bit float.
68
+ QUANTISED_DTYPES = ("int8", "int4", "uint8", "uint4", "fp8", "float8", "fp4", "nf4")
69
+
70
+ # Fallback for repositories that quantise without recording it in the config.
71
+ # Matched against the repository id, lower-cased, so keep these specific enough
72
+ # not to fire on ordinary model names.
73
+ QUANTISATION_ID_MARKERS = (
74
+ "gptq", "awq", "gguf", "-4bit", "-8bit", "int4", "int8", "fp8", "fp4",
75
+ "nf4", "-bnb", "w4a16", "w8a8", "mxfp4", "autoround", "quantized",
76
+ "quantised", "-qat",
77
+ )
78
+
79
+ # WattGPU was profiled on decoder-only generative models served by vLLM.
80
+ DECODER_ARCHITECTURE_SUFFIXES = ("ForCausalLM", "LMHeadModel", "ForConditionalGeneration")
81
+
82
+ # Architectures that are definitely not autoregressive generation: encoders used
83
+ # for embeddings, and classification heads used for reranking. These do not
84
+ # generate tokens at all, so inter-token latency is meaningless for them.
85
+ NON_GENERATIVE_ARCHITECTURE_SUFFIXES = (
86
+ "ForSequenceClassification",
87
+ "ForMaskedLM",
88
+ "ForTokenClassification",
89
+ "ForQuestionAnswering",
90
+ "ForMultipleChoice",
91
+ "EmbeddingModel",
92
+ )
93
+
94
+ # Hub task tags that mean the repository is not served as a generative LLM.
95
+ # This is the load-bearing check: an embedding or reranker model built on a
96
+ # causal LM (Qwen3-Embedding, Qwen3-Reranker) still declares `Qwen3ForCausalLM`
97
+ # in its config, so the architecture alone cannot tell them apart.
98
+ NON_GENERATIVE_PIPELINE_TAGS = {
99
+ "feature-extraction": "an embedding model",
100
+ "sentence-similarity": "an embedding model",
101
+ "text-ranking": "a reranker",
102
+ "text-classification": "a classifier",
103
+ "token-classification": "a token classifier",
104
+ "fill-mask": "a masked language model",
105
+ "question-answering": "an extractive question-answering model",
106
+ "zero-shot-classification": "a classifier",
107
+ }
108
+
109
+
110
+ @dataclass
111
+ class LLMInfo:
112
+ """Everything the demo knows about one Hugging Face model."""
113
+
114
+ model_id: str
115
+ model_type: str
116
+ num_layers: int
117
+ hidden_size: int
118
+ num_attention_heads: int
119
+ num_key_value_heads: int
120
+ total_b_params: float
121
+ params_source: str
122
+ architectures: list[str] = field(default_factory=list)
123
+ max_position_embeddings: int | None = None
124
+ torch_dtype: str | None = None
125
+ is_moe: bool = False
126
+ warnings: list[str] = field(default_factory=list)
127
+
128
+ def spec_rows(self) -> list[tuple[str, str]]:
129
+ """Human-readable spec sheet for the UI."""
130
+ return [
131
+ ("Model", self.model_id),
132
+ ("Architecture", ", ".join(self.architectures) or "—"),
133
+ ("Model type", self.model_type),
134
+ ("Parameters (B)", f"{self.total_b_params:.3g}"
135
+ + (" (estimated)" if "estimated" in self.params_source else "")),
136
+ ("Layers", str(self.num_layers)),
137
+ ("Hidden size", str(self.hidden_size)),
138
+ ("Attention heads", str(self.num_attention_heads)),
139
+ ("KV heads", str(self.num_key_value_heads)),
140
+ ("Max context", str(self.max_position_embeddings or "—")),
141
+ ("Weight dtype", self.torch_dtype or "—"),
142
+ ]
143
+
144
+
145
+ def _auth_headers(token: str | None = None) -> dict[str, str]:
146
+ """Bearer token for the Hub, if one is available.
147
+
148
+ A token passed in by the caller wins over the deployment's own
149
+ `HF_TOKEN` / `HUGGING_FACE_HUB_TOKEN`, so a visitor can supply their own for
150
+ a single request. Caller-supplied tokens are used and discarded -- never
151
+ stored, logged, or written into a shareable link.
152
+ """
153
+ supplied = (token or "").strip()
154
+ if supplied:
155
+ return {"Authorization": f"Bearer {supplied}"}
156
+ configured = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
157
+ return {"Authorization": f"Bearer {configured.strip()}"} if configured else {}
158
+
159
+
160
+ def has_hf_token() -> bool:
161
+ """Whether the deployment itself has a token, ignoring per-request ones."""
162
+ return bool(os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN"))
163
+
164
+
165
+ def _get_json(url: str, what: str, token: str | None = None) -> Any:
166
+ headers = {"User-Agent": USER_AGENT, **_auth_headers(token)}
167
+ request = urllib.request.Request(url, headers=headers)
168
+ try:
169
+ with urllib.request.urlopen(request, timeout=TIMEOUT_S) as response:
170
+ return json.loads(response.read().decode("utf-8"))
171
+ except urllib.error.HTTPError as exc:
172
+ if exc.code in (401, 403):
173
+ # The Hub answers 401 both for gated repositories and for ones that
174
+ # do not exist, so it is not possible to tell the two apart here.
175
+ hint = (
176
+ "Either the repository does not exist, or it is private or "
177
+ "licence-gated. Check the spelling first."
178
+ )
179
+ if _auth_headers(token):
180
+ hint += (
181
+ " The token in use does not grant access to it — accept the "
182
+ "model's licence on huggingface.co with that account."
183
+ )
184
+ else:
185
+ hint += (
186
+ " For a gated model such as Llama or Gemma, accept its licence "
187
+ "on huggingface.co and paste a read-only access token into "
188
+ "**Use your own Hugging Face token** below."
189
+ )
190
+ raise ModelNotFoundError(f"Cannot read **{what}** from the Hugging Face Hub.", hint) from exc
191
+ if exc.code == 404:
192
+ raise ModelNotFoundError(
193
+ f"**{what}** has no `config.json`.",
194
+ "The repository exists but does not look like a transformers "
195
+ "model. GGUF-only and adapter repositories are not supported; "
196
+ "use the original model repository instead.",
197
+ ) from exc
198
+ if exc.code == 429:
199
+ # The Hub rate-limits anonymous requests per IP. A busy public
200
+ # deployment shares one IP across all its visitors, so this is
201
+ # reached in practice, not just under abuse.
202
+ hint = "The Hugging Face Hub is rate-limiting requests. Please try again shortly."
203
+ if not _auth_headers(token):
204
+ hint += (
205
+ " The limit is per IP address and shared by everyone using this "
206
+ "demo, so pasting a read-only access token into **Use your own "
207
+ "Hugging Face token** below will lift it for you. Measured and "
208
+ "cached models are unaffected — they need no Hub lookup at all."
209
+ )
210
+ raise NetworkError(f"Too many requests to the Hub while reading **{what}**.",
211
+ hint) from exc
212
+ raise NetworkError(
213
+ f"Hugging Face returned HTTP {exc.code} for **{what}**.",
214
+ "This is usually temporary. Please try again in a moment.",
215
+ ) from exc
216
+ except urllib.error.URLError as exc:
217
+ raise NetworkError(
218
+ "Could not reach the Hugging Face Hub.",
219
+ f"The network request failed ({exc.reason}). Check connectivity and retry.",
220
+ ) from exc
221
+ except json.JSONDecodeError as exc:
222
+ raise ModelConfigError(
223
+ f"The configuration file for **{what}** is not valid JSON.",
224
+ "This repository does not look like a standard transformers model.",
225
+ ) from exc
226
+
227
+
228
+ def _pick(config: dict, logical_name: str) -> Any:
229
+ for key in FIELD_ALIASES[logical_name]:
230
+ value = config.get(key)
231
+ if value is not None:
232
+ return value
233
+ return None
234
+
235
+
236
+ def _unwrap_text_config(config: dict) -> tuple[dict, list[str]]:
237
+ """Multimodal repos nest the language model under `text_config`."""
238
+ text_config = config.get("text_config")
239
+ if isinstance(text_config, dict) and _pick(text_config, "num_layers") is not None:
240
+ merged = {**config, **text_config}
241
+ return merged, [
242
+ "This is a multimodal repository; predictions use its text decoder "
243
+ "(`text_config`) and ignore the vision tower."
244
+ ]
245
+ return config, []
246
+
247
+
248
+ def _estimate_b_params(config: dict) -> float | None:
249
+ """Estimate parameter count from the transformer's shape.
250
+
251
+ Embeddings + per-layer attention and MLP weights. Accurate to a few percent
252
+ for standard decoder-only models, and only used when the Hub publishes no
253
+ safetensors index.
254
+ """
255
+ layers = _pick(config, "num_layers")
256
+ hidden = _pick(config, "hidden_size")
257
+ heads = _pick(config, "num_attention_heads")
258
+ vocab = _pick(config, "vocab_size")
259
+ intermediate = _pick(config, "intermediate_size")
260
+ if not all(isinstance(v, int) and v > 0 for v in (layers, hidden, heads, vocab)):
261
+ return None
262
+ if not isinstance(intermediate, int) or intermediate <= 0:
263
+ intermediate = 4 * hidden
264
+
265
+ kv_heads = _pick(config, "num_key_value_heads") or heads
266
+ head_dim = config.get("head_dim") or hidden // heads
267
+
268
+ attention = hidden * (heads * head_dim) + 2 * hidden * (kv_heads * head_dim) + (heads * head_dim) * hidden
269
+ mlp = 3 * hidden * intermediate # gated MLP; ~1.5x the cost of a plain one
270
+ embeddings = vocab * hidden * (1 if config.get("tie_word_embeddings") else 2)
271
+
272
+ return (layers * (attention + mlp) + embeddings) / 1e9
273
+
274
+
275
+ def detect_moe(config: dict) -> str | None:
276
+ """Return the config key that marks this as MoE, or None."""
277
+ for key in MOE_KEYS:
278
+ if config.get(key):
279
+ return key
280
+ for architecture in config.get("architectures") or []:
281
+ if "moe" in str(architecture).lower():
282
+ return "architectures"
283
+ return None
284
+
285
+
286
+ def detect_quantisation(model_id: str, config: dict) -> str | None:
287
+ """Return a short description of the quantisation found, or None."""
288
+ for key in QUANTIZATION_CONFIG_KEYS:
289
+ value = config.get(key)
290
+ if isinstance(value, dict) and value:
291
+ method = value.get("quant_method") or value.get("method") or "unknown method"
292
+ return str(method)
293
+ if isinstance(value, str) and value:
294
+ return value
295
+
296
+ # Both spellings are checked independently: recent transformers renamed
297
+ # `torch_dtype` to `dtype`, and a config may carry both.
298
+ for key in ("torch_dtype", "dtype"):
299
+ dtype = str(config.get(key) or "").lower()
300
+ if any(marker in dtype for marker in QUANTISED_DTYPES):
301
+ return dtype
302
+
303
+ lowered = model_id.lower()
304
+ for marker in QUANTISATION_ID_MARKERS:
305
+ if marker in lowered:
306
+ return f"the repository name contains `{marker.strip('-')}`"
307
+ return None
308
+
309
+
310
+ def _reject_if_unsupported(model_id: str, config: dict) -> None:
311
+ """Refuse model families WattGPU was never trained on.
312
+
313
+ Every profiled run served a dense model with FP16 weights. Mixture-of-experts
314
+ routing and quantised weights both change the memory traffic and arithmetic
315
+ per token enough that an estimate would be misleading rather than merely
316
+ uncertain, so these are refused rather than flagged.
317
+ """
318
+ moe_key = detect_moe(config)
319
+ if moe_key:
320
+ raise UnsupportedModelError(
321
+ f"Not supported yet: **{model_id}** is a mixture-of-experts model.",
322
+ "WattGPU was trained on dense models only. MoE models activate a "
323
+ "fraction of their weights per token, so their power and latency "
324
+ "follow different rules.\n\n"
325
+ "Support for mixture-of-experts models is in progress — "
326
+ f"[get in touch]({CONTACT_URL}) to hear when it lands, or to tell us "
327
+ "which models you need covered.",
328
+ )
329
+
330
+ quantisation = detect_quantisation(model_id, config)
331
+ if quantisation:
332
+ raise UnsupportedModelError(
333
+ f"Not supported yet: **{model_id}** looks quantised ({quantisation}).",
334
+ "Every measurement behind WattGPU used FP16 weights. Quantisation "
335
+ "changes both memory traffic and arithmetic per token, so an estimate "
336
+ "would not carry over.\n\n"
337
+ "Support for quantised weights is in progress — "
338
+ f"[get in touch]({CONTACT_URL}) to hear when it lands, or to tell us "
339
+ "which formats you need covered.",
340
+ )
341
+
342
+
343
+ def llm_info_from_config(model_id: str, config: dict,
344
+ total_b_params: float | None = None,
345
+ params_source: str = "") -> LLMInfo:
346
+ """Convert a raw `config.json` into the features WattGPU consumes.
347
+
348
+ This is the adapter between the Hugging Face world -- where every
349
+ architecture family names its fields differently -- and the fixed schema
350
+ the trained models expect. It is pure: give it a parsed config and it
351
+ returns an `LLMInfo`, no network involved. `fetch_llm_info` is the thin
352
+ transport layer on top of it.
353
+
354
+ Pass `total_b_params` when the parameter count is known from elsewhere
355
+ (the Hub's safetensors index); otherwise it is estimated from the config.
356
+ """
357
+ if not isinstance(config, dict):
358
+ raise ModelConfigError(
359
+ f"**{model_id}** has an unexpected `config.json`.",
360
+ "Only standard transformers models are supported.",
361
+ )
362
+
363
+ config, warnings = _unwrap_text_config(config)
364
+
365
+ num_layers = _pick(config, "num_layers")
366
+ hidden_size = _pick(config, "hidden_size")
367
+ num_heads = _pick(config, "num_attention_heads")
368
+ missing = [
369
+ name
370
+ for name, value in (
371
+ ("number of layers", num_layers),
372
+ ("hidden size", hidden_size),
373
+ ("attention heads", num_heads),
374
+ )
375
+ if not isinstance(value, int) or value <= 0
376
+ ]
377
+ if missing:
378
+ raise ModelConfigError(
379
+ f"**{model_id}** does not expose the fields WattGPU needs "
380
+ f"({', '.join(missing)}).",
381
+ "This is usually an embedding model, an adapter, a quantised "
382
+ "re-upload, or a non-transformer architecture. Try the original "
383
+ "decoder-only base repository instead.",
384
+ )
385
+
386
+ kv_heads = _pick(config, "num_key_value_heads")
387
+ if not isinstance(kv_heads, int) or kv_heads <= 0:
388
+ kv_heads = num_heads # no GQA/MQA: every head has its own K/V
389
+
390
+ if total_b_params is None:
391
+ estimate = _estimate_b_params(config)
392
+ if estimate is None:
393
+ raise ModelConfigError(
394
+ f"Could not determine the parameter count for **{model_id}**.",
395
+ "The repository publishes neither a safetensors index nor enough "
396
+ "of `config.json` to estimate it.",
397
+ )
398
+ total_b_params = estimate
399
+ params_source = params_source or "estimated from config"
400
+
401
+ architectures = list(config.get("architectures") or [])
402
+ if architectures and not any(
403
+ a.endswith(DECODER_ARCHITECTURE_SUFFIXES) for a in architectures
404
+ ):
405
+ # A bare `*Model` is an encoder without a language-modelling head.
406
+ definitely_not_generative = any(
407
+ a.endswith(NON_GENERATIVE_ARCHITECTURE_SUFFIXES) or a.endswith("Model")
408
+ for a in architectures
409
+ )
410
+ if definitely_not_generative:
411
+ raise UnsupportedModelError(
412
+ f"Not supported: **{model_id}** is not a generative model "
413
+ f"(`{', '.join(architectures)}`).",
414
+ "WattGPU estimates the cost of generating tokens one at a time. "
415
+ "Embedding, reranking and classification models run a single "
416
+ "forward pass instead, so inter-token latency does not apply and "
417
+ "their power profile is different.",
418
+ )
419
+ warnings.append(
420
+ f"`{', '.join(architectures)}` is not a recognised decoder-only "
421
+ "architecture. WattGPU was profiled on autoregressive LLMs, so treat "
422
+ "this estimate as indicative only."
423
+ )
424
+
425
+ _reject_if_unsupported(model_id, config)
426
+ is_moe = False
427
+
428
+ return LLMInfo(
429
+ model_id=model_id,
430
+ model_type=str(config.get("model_type") or "unknown"),
431
+ num_layers=int(num_layers),
432
+ hidden_size=int(hidden_size),
433
+ num_attention_heads=int(num_heads),
434
+ num_key_value_heads=int(kv_heads),
435
+ total_b_params=float(total_b_params),
436
+ params_source=params_source,
437
+ architectures=architectures,
438
+ max_position_embeddings=config.get("max_position_embeddings"),
439
+ torch_dtype=config.get("torch_dtype") or config.get("dtype"),
440
+ is_moe=is_moe,
441
+ warnings=warnings,
442
+ )
443
+
444
+
445
+ def fetch_llm_info(model_id: str, token: str | None = None) -> LLMInfo:
446
+ """Look up a model on the Hugging Face Hub and extract WattGPU's features.
447
+
448
+ `token` is an optional per-request Hugging Face token, used for this call
449
+ and then dropped. Raises a `WattGPUError` subclass with a displayable
450
+ message on any failure.
451
+ """
452
+ model_id = (model_id or "").strip().strip("/")
453
+ if not model_id:
454
+ raise ModelNotFoundError(
455
+ "No model identifier given.",
456
+ "Enter a Hugging Face model id, for example `Qwen/Qwen3-4B`.",
457
+ )
458
+ if model_id.startswith(("http://", "https://")):
459
+ model_id = model_id.split("huggingface.co/", 1)[-1].split("/tree/", 1)[0]
460
+ if model_id.count("/") != 1:
461
+ raise ModelNotFoundError(
462
+ f"**{model_id}** is not a valid model identifier.",
463
+ "Use the `owner/name` form, for example `mistralai/Mistral-7B-Instruct-v0.3`.",
464
+ )
465
+
466
+ config = _get_json(f"{HF_ENDPOINT}/{model_id}/raw/main/config.json", model_id, token)
467
+
468
+ # Parameter count: prefer the Hub's safetensors index over an estimate.
469
+ total_b_params: float | None = None
470
+ params_source = ""
471
+ extra_warnings: list[str] = []
472
+ try:
473
+ info = _get_json(f"{HF_ENDPOINT}/api/models/{model_id}", model_id, token)
474
+
475
+ # The Hub's declared task is the only reliable way to spot an embedding
476
+ # or reranking model that is built on a causal-LM backbone.
477
+ pipeline_tag = (info or {}).get("pipeline_tag")
478
+ described_as = NON_GENERATIVE_PIPELINE_TAGS.get(pipeline_tag)
479
+ if described_as:
480
+ raise UnsupportedModelError(
481
+ f"Not supported: **{model_id}** is {described_as}, not a "
482
+ "text-generation model.",
483
+ "WattGPU estimates the cost of generating tokens one at a time. "
484
+ "Embedding, reranking and classification models run a single "
485
+ "forward pass instead, so inter-token latency does not apply and "
486
+ "their power profile is different.\n\n"
487
+ f"The Hub lists this repository under `{pipeline_tag}`.",
488
+ )
489
+
490
+ total = ((info or {}).get("safetensors") or {}).get("total")
491
+ if isinstance(total, (int, float)) and total > 0:
492
+ total_b_params = float(total) / 1e9
493
+ params_source = "safetensors index"
494
+ except NetworkError:
495
+ extra_warnings.append(
496
+ "Could not read the safetensors index; the parameter count is estimated."
497
+ )
498
+
499
+ llm = llm_info_from_config(model_id, config, total_b_params, params_source)
500
+ llm.warnings = [*extra_warnings, *llm.warnings]
501
+ return llm
502
+
503
+
504
+ def search_models(query: str, limit: int = 20, token: str | None = None) -> list[str]:
505
+ """Search the Hub for text-generation models matching `query`.
506
+
507
+ Used to drive the live search box. Returns an empty list rather than
508
+ raising: a failed autocomplete should never interrupt typing.
509
+ """
510
+ query = (query or "").strip()
511
+ if len(query) < 2:
512
+ return []
513
+
514
+ url = (
515
+ f"{HF_ENDPOINT}/api/models"
516
+ f"?search={urllib.parse.quote(query)}"
517
+ "&filter=text-generation&sort=downloads&direction=-1"
518
+ f"&limit={int(limit)}"
519
+ )
520
+ try:
521
+ results = _get_json(url, query, token)
522
+ except WattGPUError:
523
+ return []
524
+ if not isinstance(results, list):
525
+ return []
526
+ return [m["id"] for m in results if isinstance(m, dict) and m.get("id")]
wattgpu_demo/predictor.py ADDED
@@ -0,0 +1,450 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Serve WattGPU predictions, with a certainty tier attached to each one.
2
+
3
+ The tier says how far the query sits from the models' training data, which is
4
+ the single most important caveat on any prediction here:
5
+
6
+ green — the LLM and the GPU were both profiled
7
+ yellow — an unprofiled LLM on a profiled GPU
8
+ orange — a profiled LLM on an unprofiled GPU
9
+ red — neither was profiled; not supported
10
+
11
+ The red case is refused rather than guessed at. The paper validates
12
+ generalisation along one axis at a time (leave-one-LLM-out and
13
+ leave-one-GPU-out), so there is no measured basis for extrapolating along both
14
+ at once.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import math
21
+ import os
22
+ from dataclasses import dataclass, field
23
+
24
+ import joblib
25
+ import numpy as np
26
+ import pandas as pd
27
+
28
+ from .emissions import (
29
+ DEFAULT_CARBON_INTENSITY_G_PER_KWH,
30
+ clean_carbon_intensity,
31
+ co2_per_hour_g,
32
+ co2_per_million_tokens_g,
33
+ energy_per_hour_wh,
34
+ energy_per_million_tokens_wh,
35
+ )
36
+ from .errors import InsufficientMemoryError, WattGPUError
37
+ from .features import (
38
+ ITL_FEATURES,
39
+ POWER_FEATURES,
40
+ SCENARIOS,
41
+ SCENARIOS_WITH_PER_USER_LATENCY,
42
+ add_derived_features,
43
+ )
44
+ from .gpu_db import GPUDatabase, GPUInfo
45
+ from .hf_models import CONTACT_URL, LLMInfo, fetch_llm_info
46
+
47
+ DEFAULT_MODEL_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
48
+ "data", "models")
49
+
50
+ # Bytes per parameter at FP16, the precision every profiled run used.
51
+ BYTES_PER_PARAM = 2
52
+
53
+ # vLLM's default `--gpu-memory-utilization`. The rest of the card is left to the
54
+ # CUDA context and fragmentation, so it is not available for weights or KV cache.
55
+ # The paper's own feature engineering uses the same 0.9 factor.
56
+ VLLM_MEMORY_UTILISATION = 0.9
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class Certainty:
61
+ """A certainty tier. What each one means is explained once in the UI, so
62
+ only the short label travels with a prediction.
63
+
64
+ `tier` is the band shown to the user. `accuracy_key` is the validation
65
+ protocol whose measured error backs the estimate. The two usually match,
66
+ but not always: an estimate can sit in a better band than its coverage
67
+ alone suggests when the measurements say the extra extrapolation costs
68
+ almost nothing. The error reported always follows `accuracy_key`, so the
69
+ number stays the one that was actually measured for the situation.
70
+ """
71
+
72
+ tier: str
73
+ label: str
74
+ accuracy_key: str
75
+
76
+
77
+ CERTAINTY_TIERS = {
78
+ "green": Certainty("green", "High confidence", "green"),
79
+ "yellow": Certainty("yellow", "Moderate confidence — LLM not in training data",
80
+ "yellow"),
81
+ "orange": Certainty("orange", "Lower confidence — GPU not in training data",
82
+ "orange"),
83
+ # Neither side measured. Under server load the double holdout lands within a
84
+ # couple of points of leave-one-GPU-out (power 15.1% vs 13.0%, latency 8.6%
85
+ # vs 8.5%), so it earns the same band. Offline it does not: latency degrades
86
+ # from 24.9% to 36.2%, which is a genuinely weaker estimate.
87
+ "neither_server": Certainty("orange", "Lower confidence — neither in training data",
88
+ "red"),
89
+ "neither_offline": Certainty("red", "Low confidence — neither in training data",
90
+ "red"),
91
+ }
92
+
93
+
94
+ @dataclass
95
+ class Prediction:
96
+ """A power and latency prediction with its provenance and caveats."""
97
+
98
+ llm: LLMInfo
99
+ gpu: GPUInfo
100
+ scenario: str
101
+ certainty: Certainty
102
+ power_w: float
103
+ power_fraction_of_tdp: float
104
+ power_mdape: float | None
105
+ itl_s: float | None
106
+ itl_mdape: float | None
107
+ itl_unavailable_reason: str | None = None
108
+ carbon_intensity_g_per_kwh: float = DEFAULT_CARBON_INTENSITY_G_PER_KWH
109
+ warnings: list[str] = field(default_factory=list)
110
+ notes: list[str] = field(default_factory=list)
111
+
112
+ @property
113
+ def tokens_per_second(self) -> float | None:
114
+ """Aggregate generation throughput, defined only for offline batches.
115
+
116
+ Offline the model's target is wall-clock time per generated token over
117
+ the whole batch, so its inverse is the batch's token throughput exactly
118
+ -- no assumption about batching required.
119
+
120
+ Under server load the target is one user's inter-token latency, and its
121
+ inverse would be that user's token rate rather than the system's. There
122
+ is no way to recover system throughput from it without knowing how many
123
+ requests the server runs at once, which is what WattGPU+ adds.
124
+ """
125
+ if self.latency_is_per_user or not self.itl_s:
126
+ return None
127
+ return 1.0 / self.itl_s
128
+
129
+ @property
130
+ def latency_is_per_user(self) -> bool:
131
+ """Whether `itl_s` is the gap one user sees between tokens.
132
+
133
+ True under light and moderate load, where the dataset timed individual
134
+ requests. False under saturation, where it timed only the batch as a
135
+ whole, so `itl_s` is wall-clock time per token amortised over every
136
+ request in flight -- a system capacity figure, not a user-facing one.
137
+ """
138
+ return self.scenario in SCENARIOS_WITH_PER_USER_LATENCY
139
+
140
+ @property
141
+ def energy_per_token_j(self) -> float | None:
142
+ """Energy per generated token, defined only under saturation.
143
+
144
+ There `itl_s` is wall time per token across the whole batch, so power
145
+ times that time is the energy the GPU spent per token. Under light or
146
+ moderate load `itl_s` is one user's latency while other requests share
147
+ the GPU, and the product would not be energy attributable to a token.
148
+ """
149
+ if self.latency_is_per_user or not self.itl_s:
150
+ return None
151
+ return self.power_w * self.itl_s
152
+
153
+ @property
154
+ def energy_per_million_tokens_wh(self) -> float | None:
155
+ """Watt-hours per million generated tokens.
156
+
157
+ Provider- and region-independent, and derived only from the two
158
+ quantities the models predict, so it needs no further assumptions.
159
+ """
160
+ energy = self.energy_per_token_j
161
+ return None if energy is None else energy_per_million_tokens_wh(energy)
162
+
163
+ @property
164
+ def co2_per_million_tokens_g(self) -> float | None:
165
+ """Grams of CO2e per million tokens at the chosen grid intensity."""
166
+ energy = self.energy_per_million_tokens_wh
167
+ if energy is None:
168
+ return None
169
+ return co2_per_million_tokens_g(energy, self.carbon_intensity_g_per_kwh)
170
+
171
+ @property
172
+ def energy_per_hour_wh(self) -> float | None:
173
+ """Wall-socket watt-hours for an hour of serving.
174
+
175
+ Reported under server load, where the useful question is what the
176
+ deployment costs to keep running rather than what a token costs: the
177
+ GPU is idle much of the time, so per-token figures would be dominated
178
+ by how busy it happens to be. Unlike the per-token figure this is a
179
+ facility-level number -- it grosses the board power up to the whole
180
+ host and applies the data centre's overhead.
181
+ """
182
+ if not self.latency_is_per_user:
183
+ return None
184
+ return energy_per_hour_wh(self.power_w)
185
+
186
+ @property
187
+ def co2_per_hour_g(self) -> float | None:
188
+ """Grams of CO2e for an hour of serving at the chosen grid intensity."""
189
+ energy = self.energy_per_hour_wh
190
+ if energy is None:
191
+ return None
192
+ return co2_per_hour_g(energy, self.carbon_intensity_g_per_kwh)
193
+
194
+
195
+ class WattGPUPredictor:
196
+ """Loads the trained pipelines and answers prediction queries."""
197
+
198
+ def __init__(self, model_dir: str = DEFAULT_MODEL_DIR, gpu_db: GPUDatabase | None = None) -> None:
199
+ meta_path = os.path.join(model_dir, "meta.json")
200
+ if not os.path.exists(meta_path):
201
+ raise WattGPUError(
202
+ "The trained WattGPU models are missing.",
203
+ f"Expected them in `{model_dir}`. Run `python scripts/train_models.py` first.",
204
+ )
205
+ with open(meta_path) as fh:
206
+ self.meta = json.load(fh)
207
+
208
+ # Architectures of the profiled LLMs, taken from the paper's data. Using
209
+ # these instead of the Hub keeps green-tier predictions network-free and
210
+ # makes licence-gated models work without a token.
211
+ cache_path = os.path.join(model_dir, "profiled_llms.json")
212
+ self._profiled_llms: dict[str, dict] = {}
213
+ if os.path.exists(cache_path):
214
+ with open(cache_path) as fh:
215
+ self._profiled_llms = json.load(fh)
216
+
217
+ # Architectures of licence-gated models, read from public mirrors by
218
+ # `scripts/cache_gated_models.py`. Lets the demo answer for Llama and
219
+ # Gemma without a maintainer's token standing in for every visitor.
220
+ self._gated_llms: dict[str, dict] = {}
221
+ gated_path = os.path.join(
222
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
223
+ "data", "gated_llms.json")
224
+ if os.path.exists(gated_path):
225
+ with open(gated_path) as fh:
226
+ self._gated_llms = json.load(fh)
227
+
228
+ self.power_model = joblib.load(os.path.join(model_dir, "power.joblib"))
229
+ self.itl_model = joblib.load(os.path.join(model_dir, "itl.joblib"))
230
+ self.gpu_db = gpu_db or GPUDatabase()
231
+
232
+ self.seen_models = {m.lower() for m in self.meta["seen_models"]}
233
+ self.seen_gpus = set(self.meta["seen_gpus"])
234
+ self._accuracy = self.meta.get("tier_accuracy_mdape", {})
235
+ self._itl_is_log = "itl" in self.meta.get("log_transformed_targets", [])
236
+
237
+ # -- catalogue ---------------------------------------------------------
238
+
239
+ @property
240
+ def profiled_models(self) -> list[str]:
241
+ """Models the training data actually covers."""
242
+ return sorted(self.meta["seen_models"])
243
+
244
+ @property
245
+ def known_models(self) -> list[str]:
246
+ """Models resolvable without a Hub call: measured, plus gated mirrors.
247
+
248
+ These are what the dropdown offers before the user types. Being here
249
+ says only that the architecture is known offline, not that the model
250
+ was measured -- the certainty tier still follows coverage.
251
+ """
252
+ return sorted({*self.meta["seen_models"], *self._gated_llms})
253
+
254
+ @property
255
+ def profiled_gpus(self) -> list[str]:
256
+ return sorted(self.meta["seen_gpus"])
257
+
258
+ @property
259
+ def all_gpus(self) -> list[str]:
260
+ """Profiled GPUs first, then every other GPU in the database."""
261
+ profiled = self.profiled_gpus
262
+ rest = sorted(set(self.gpu_db.names) - set(profiled))
263
+ return profiled + rest
264
+
265
+ # -- certainty ---------------------------------------------------------
266
+
267
+ def classify(self, llm: LLMInfo, gpu: GPUInfo, scenario: str) -> Certainty:
268
+ """Pick the certainty tier for a query.
269
+
270
+ The scenario matters only for the hardest case: extrapolating on both
271
+ axes at once costs little under server load but a lot offline, so the
272
+ band follows the measurements rather than the coverage alone.
273
+ """
274
+ seen_llm = llm.model_id.lower() in self.seen_models
275
+ seen_gpu = gpu.gpu_db_name in self.seen_gpus
276
+
277
+ if seen_llm and seen_gpu:
278
+ return CERTAINTY_TIERS["green"]
279
+ if seen_gpu:
280
+ return CERTAINTY_TIERS["yellow"]
281
+ if seen_llm:
282
+ return CERTAINTY_TIERS["orange"]
283
+ # Neither measured, validated by the double holdout in train_models.py.
284
+ return CERTAINTY_TIERS[
285
+ "neither_offline" if scenario == "offline" else "neither_server"]
286
+
287
+ def _mdape(self, target: str, accuracy_key: str, scenario: str) -> float | None:
288
+ """The measured error for this situation, whatever band it is shown in."""
289
+ regime = "offline" if scenario == "offline" else "server"
290
+ return self._accuracy.get(target, {}).get(accuracy_key, {}).get(regime)
291
+
292
+ # -- prediction --------------------------------------------------------
293
+
294
+ def _feature_row(self, llm: LLMInfo, gpu: GPUInfo, scenario: str) -> pd.DataFrame:
295
+ row = pd.DataFrame([{
296
+ "scenario": scenario,
297
+ "model_type": llm.model_type,
298
+ "total_b_params": llm.total_b_params,
299
+ "hidden_size": llm.hidden_size,
300
+ "num_attention_heads": llm.num_attention_heads,
301
+ "num_key_value_heads": llm.num_key_value_heads,
302
+ "num_layers": llm.num_layers,
303
+ "memory_bandwidth_gb_s": gpu.memory_bandwidth_gb_s,
304
+ "memory_size_gb": gpu.memory_size_gb,
305
+ "memory_type": gpu.memory_type,
306
+ "base_clock_mhz": gpu.base_clock_mhz,
307
+ "boost_clock_mhz": gpu.boost_clock_mhz,
308
+ "transistor_count_m": gpu.transistor_count_m,
309
+ "release_year": gpu.release_year,
310
+ "tflops_16b": gpu.tflops_16b,
311
+ }])
312
+ return add_derived_features(row)
313
+
314
+ @staticmethod
315
+ def _lookup(cache: dict[str, dict], key: str) -> dict | None:
316
+ return cache.get(key) or next(
317
+ (v for k, v in cache.items() if k.lower() == key.lower()), None)
318
+
319
+ def _resolve_llm(self, model_id: str, token: str | None = None) -> LLMInfo:
320
+ """Resolve an LLM from the local caches, falling back to the Hub.
321
+
322
+ Measured models come from the profiled cache, licence-gated ones from
323
+ the mirrored cache, and everything else from a live Hub lookup. Only
324
+ the profiled cache implies the model is in the training data; the
325
+ gated cache is an architecture lookup and nothing more, so those models
326
+ still land in the tier their coverage earns.
327
+ """
328
+ key = (model_id or "").strip().strip("/")
329
+
330
+ profiled = self._lookup(self._profiled_llms, key)
331
+ if profiled is not None:
332
+ return LLMInfo(model_id=key, params_source="profiled dataset", **profiled)
333
+
334
+ gated = self._lookup(self._gated_llms, key)
335
+ if gated is not None:
336
+ entry = {k: v for k, v in gated.items() if k != "source_mirror"}
337
+ return LLMInfo(model_id=key, params_source="published config", **entry)
338
+
339
+ return fetch_llm_info(key, token)
340
+
341
+ @staticmethod
342
+ def _check_fits_in_memory(llm: LLMInfo, gpu: GPUInfo) -> float:
343
+ """Refuse pairs whose FP16 weights cannot fit, and return spare memory.
344
+
345
+ Returns the GB left for the KV cache and activations once the weights
346
+ are loaded, following the paper's own budget: vLLM claims 90% of the
347
+ card, and the weights come out of that.
348
+ """
349
+ weights_gb = llm.total_b_params * BYTES_PER_PARAM
350
+ usable_gb = gpu.memory_size_gb * VLLM_MEMORY_UTILISATION
351
+ spare_gb = usable_gb - weights_gb
352
+ if spare_gb > 0:
353
+ return spare_gb
354
+
355
+ # Smallest card that could hold the weights inside vLLM's 90% budget.
356
+ needed_gb = math.ceil(weights_gb / VLLM_MEMORY_UTILISATION)
357
+ raise InsufficientMemoryError(
358
+ f"Does not fit: **{llm.model_id}** needs more memory than "
359
+ f"**{gpu.gpu_db_name}** has.",
360
+ f"Its {llm.total_b_params:.1f} B parameters take about "
361
+ f"{weights_gb:.1f} GB at FP16, but only about {usable_gb:.1f} GB of the "
362
+ f"card's {gpu.memory_size_gb:.0f} GB is usable for a model — vLLM leaves "
363
+ "roughly 10% to the CUDA context — and the KV cache still has to come "
364
+ "out of what is left.\n\n"
365
+ "Running this would need quantisation or several GPUs, neither of which "
366
+ "WattGPU covers, so no estimate is given. Try a smaller model, or a GPU "
367
+ f"with at least {needed_gb} GB.",
368
+ )
369
+
370
+ def predict(self, model_id: str, gpu_name: str, scenario: str,
371
+ carbon_intensity: float | str | None = None,
372
+ hf_token: str | None = None) -> Prediction:
373
+ """Predict mean power draw and inter-token latency for one deployment.
374
+
375
+ Raises a `WattGPUError` subclass, with a message meant for display, if
376
+ the LLM or GPU cannot be resolved or the combination is unsupported.
377
+ """
378
+ if scenario not in SCENARIOS:
379
+ raise WattGPUError(
380
+ f"Unknown scenario **{scenario}**.",
381
+ f"Choose one of: {', '.join(SCENARIOS)}.",
382
+ )
383
+
384
+ llm = self._resolve_llm(model_id, hf_token)
385
+ gpu = self.gpu_db.get(gpu_name)
386
+ spare_memory_gb = self._check_fits_in_memory(llm, gpu)
387
+ certainty = self.classify(llm, gpu, scenario)
388
+
389
+ features = self._feature_row(llm, gpu, scenario)
390
+ warnings = [*llm.warnings, *gpu.warnings]
391
+ notes: list[str] = []
392
+
393
+ # For a model we never measured, everything WattGPU knows about it came
394
+ # from parsing `config.json`. Mixture-of-experts and quantised models are
395
+ # rejected on that basis, and the detection is good but not infallible --
396
+ # a repository can quantise without recording it. Say so, once.
397
+ if certainty.accuracy_key == "red":
398
+ notes.append(
399
+ "Neither side is in the training data, so this extrapolates on both "
400
+ "axes at once. The error shown is measured for exactly that case, by "
401
+ "retraining with this GPU and this model both removed. "
402
+ f"[Ask us]({CONTACT_URL}) if you need a measured figure for this pair."
403
+ )
404
+
405
+ if llm.params_source != "profiled dataset":
406
+ notes.append(
407
+ "Architecture read from `config.json` on the Hub. WattGPU covers "
408
+ "dense FP16 models only — mixture-of-experts and quantised models "
409
+ "are detected and refused, but one that declares neither in its "
410
+ "config could slip through."
411
+ )
412
+
413
+ # Power draw is predicted as a fraction of TDP, then denormalised.
414
+ fraction = float(self.power_model.predict(features[POWER_FEATURES])[0])
415
+ power_w = fraction * gpu.thermal_design_power_w
416
+
417
+ itl_s: float | None = None
418
+ itl_reason: str | None = None
419
+ if gpu.tflops_16b is None:
420
+ itl_reason = (
421
+ f"Latency needs the FP16 tensor throughput of {gpu.gpu_db_name}, "
422
+ "which is unknown."
423
+ )
424
+ else:
425
+ raw = float(self.itl_model.predict(features[ITL_FEATURES])[0])
426
+ itl_s = float(np.exp(raw)) if self._itl_is_log else raw
427
+
428
+ if spare_memory_gb < 2.0:
429
+ warnings.append(
430
+ f"The weights fit, but leave only about {spare_memory_gb:.1f} GB for the "
431
+ "KV cache, so the server would handle very few requests at once. Under "
432
+ "offline / batch that means far less batching than the measurements had, "
433
+ "and the estimate will be optimistic."
434
+ )
435
+
436
+ return Prediction(
437
+ llm=llm,
438
+ gpu=gpu,
439
+ scenario=scenario,
440
+ certainty=certainty,
441
+ power_w=power_w,
442
+ power_fraction_of_tdp=fraction,
443
+ power_mdape=self._mdape("power", certainty.accuracy_key, scenario),
444
+ itl_s=itl_s,
445
+ itl_mdape=self._mdape("itl", certainty.accuracy_key, scenario),
446
+ itl_unavailable_reason=itl_reason,
447
+ carbon_intensity_g_per_kwh=clean_carbon_intensity(carbon_intensity),
448
+ warnings=warnings,
449
+ notes=notes,
450
+ )