Initial commit;
Browse files- .gitignore +207 -0
- README.md +339 -7
- app.py +1105 -0
- assets/leaner-ai-logo.svg +26 -0
- data/demand_log.jsonl +1 -0
- data/gated_llms.json +72 -0
- data/gpu_database.csv +355 -0
- data/models/itl.joblib +3 -0
- data/models/meta.json +139 -0
- data/models/power.joblib +3 -0
- data/models/profiled_llms.json +548 -0
- pytest.ini +4 -0
- requirements.txt +12 -0
- scripts/build_gpu_db.py +221 -0
- scripts/cache_gated_models.py +102 -0
- scripts/train_models.py +306 -0
- tests/test_demo.py +1176 -0
- wattgpu_demo/__init__.py +0 -0
- wattgpu_demo/demand_log.py +80 -0
- wattgpu_demo/emissions.py +110 -0
- wattgpu_demo/errors.py +44 -0
- wattgpu_demo/features.py +185 -0
- wattgpu_demo/gpu_db.py +148 -0
- wattgpu_demo/hf_models.py +526 -0
- wattgpu_demo/predictor.py +450 -0
.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:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: green
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: apache-2.0
|
| 12 |
-
short_description:
|
| 13 |
---
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 `&` 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 "<script>" 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 |
+
)
|