Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +2 -0
- .gitignore +170 -0
- .gitmodules +9 -0
- .vscode/launch.json +26 -0
- .vscode/settings.json +3 -0
- GUIDE_SPEED_TEST.md +62 -0
- GUIDE_TRAIN_TEST.md +67 -0
- LICENSE +202 -0
- README.md +215 -3
- README_TIERED.md +116 -0
- arc_eval.ipynb +252 -0
- assets/hrm.png +0 -0
- assets/npyjs.js +176 -0
- benchmark.py +686 -0
- benchmark_results/benchmark_comparison.png +0 -0
- benchmark_results/comparison_results.json +119 -0
- benchmark_results/eval_dummy.json +24 -0
- benchmark_results/eval_dummy_comparison.png +0 -0
- benchmark_results/eval_fused_vs_v1_comparison.png +0 -0
- benchmark_results/memory_analysis.png +0 -0
- benchmark_results/model_comparison.png +3 -0
- benchmark_results/results.json +500 -0
- benchmark_results/run_nsys_profiler.py +117 -0
- benchmark_results/trained_model_comparison.png +3 -0
- benchmark_results/trained_model_results.json +18 -0
- cleanup.sh +67 -0
- compare_models.py +299 -0
- config/arch/hrm_tiered.yaml +27 -0
- config/arch/hrm_v1.yaml +21 -0
- config/cfg_pretrain.yaml +31 -0
- dataset/build_arc_dataset.py +291 -0
- dataset/build_maze_dataset.py +142 -0
- dataset/build_sudoku_dataset.py +169 -0
- dataset/common.py +51 -0
- docs/END_TO_END_EXPLANATION.md +1193 -0
- docs/IMPLEMENTATION.md +49 -0
- eval_dummy.py +277 -0
- evaluate.py +68 -0
- fusedevals.py +260 -0
- hf_upload_new/README.md +10 -0
- hf_upload_new/config.yaml +35 -0
- hf_upload_new/hrm_act_v1.py +283 -0
- hf_upload_new/losses.py +131 -0
- hf_upload_new/model.safetensors +3 -0
- latency_plot_trained_model.py +417 -0
- models/common.py +32 -0
- models/fused_hierarchical_scan.py +186 -0
- models/hrm/hrm_act_v1.py +283 -0
- models/hrm/hrm_tiered.py +466 -0
- models/layers.py +174 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
benchmark_results/model_comparison.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
benchmark_results/trained_model_comparison.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# WandB
|
| 2 |
+
/wandb/
|
| 3 |
+
# checkpoints
|
| 4 |
+
/checkpoints/
|
| 5 |
+
# cache
|
| 6 |
+
/cache/
|
| 7 |
+
# data
|
| 8 |
+
/data/
|
| 9 |
+
|
| 10 |
+
# Byte-compiled / optimized / DLL files
|
| 11 |
+
__pycache__/
|
| 12 |
+
*.py[cod]
|
| 13 |
+
*$py.class
|
| 14 |
+
|
| 15 |
+
# C extensions
|
| 16 |
+
*.so
|
| 17 |
+
|
| 18 |
+
# Distribution / packaging
|
| 19 |
+
.Python
|
| 20 |
+
build/
|
| 21 |
+
develop-eggs/
|
| 22 |
+
dist/
|
| 23 |
+
downloads/
|
| 24 |
+
eggs/
|
| 25 |
+
.eggs/
|
| 26 |
+
lib/
|
| 27 |
+
lib64/
|
| 28 |
+
parts/
|
| 29 |
+
sdist/
|
| 30 |
+
var/
|
| 31 |
+
wheels/
|
| 32 |
+
share/python-wheels/
|
| 33 |
+
*.egg-info/
|
| 34 |
+
.installed.cfg
|
| 35 |
+
*.egg
|
| 36 |
+
MANIFEST
|
| 37 |
+
|
| 38 |
+
# PyInstaller
|
| 39 |
+
# Usually these files are written by a python script from a template
|
| 40 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 41 |
+
*.manifest
|
| 42 |
+
*.spec
|
| 43 |
+
|
| 44 |
+
# Installer logs
|
| 45 |
+
pip-log.txt
|
| 46 |
+
pip-delete-this-directory.txt
|
| 47 |
+
|
| 48 |
+
# Unit test / coverage reports
|
| 49 |
+
htmlcov/
|
| 50 |
+
.tox/
|
| 51 |
+
.nox/
|
| 52 |
+
.coverage
|
| 53 |
+
.coverage.*
|
| 54 |
+
.cache
|
| 55 |
+
nosetests.xml
|
| 56 |
+
coverage.xml
|
| 57 |
+
*.cover
|
| 58 |
+
*.py,cover
|
| 59 |
+
.hypothesis/
|
| 60 |
+
.pytest_cache/
|
| 61 |
+
cover/
|
| 62 |
+
|
| 63 |
+
# Translations
|
| 64 |
+
*.mo
|
| 65 |
+
*.pot
|
| 66 |
+
|
| 67 |
+
# Django stuff:
|
| 68 |
+
*.log
|
| 69 |
+
local_settings.py
|
| 70 |
+
db.sqlite3
|
| 71 |
+
db.sqlite3-journal
|
| 72 |
+
|
| 73 |
+
# Flask stuff:
|
| 74 |
+
instance/
|
| 75 |
+
.webassets-cache
|
| 76 |
+
|
| 77 |
+
# Scrapy stuff:
|
| 78 |
+
.scrapy
|
| 79 |
+
|
| 80 |
+
# Sphinx documentation
|
| 81 |
+
docs/_build/
|
| 82 |
+
|
| 83 |
+
# PyBuilder
|
| 84 |
+
.pybuilder/
|
| 85 |
+
target/
|
| 86 |
+
|
| 87 |
+
# Jupyter Notebook
|
| 88 |
+
.ipynb_checkpoints
|
| 89 |
+
|
| 90 |
+
# IPython
|
| 91 |
+
profile_default/
|
| 92 |
+
ipython_config.py
|
| 93 |
+
|
| 94 |
+
# pyenv
|
| 95 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 96 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 97 |
+
# .python-version
|
| 98 |
+
|
| 99 |
+
# pipenv
|
| 100 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 101 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 102 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 103 |
+
# install all needed dependencies.
|
| 104 |
+
#Pipfile.lock
|
| 105 |
+
|
| 106 |
+
# poetry
|
| 107 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 108 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 109 |
+
# commonly ignored for libraries.
|
| 110 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 111 |
+
#poetry.lock
|
| 112 |
+
|
| 113 |
+
# pdm
|
| 114 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 115 |
+
#pdm.lock
|
| 116 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
| 117 |
+
# in version control.
|
| 118 |
+
# https://pdm.fming.dev/#use-with-ide
|
| 119 |
+
.pdm.toml
|
| 120 |
+
|
| 121 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 122 |
+
__pypackages__/
|
| 123 |
+
|
| 124 |
+
# Celery stuff
|
| 125 |
+
celerybeat-schedule
|
| 126 |
+
celerybeat.pid
|
| 127 |
+
|
| 128 |
+
# SageMath parsed files
|
| 129 |
+
*.sage.py
|
| 130 |
+
|
| 131 |
+
# Environments
|
| 132 |
+
.env
|
| 133 |
+
.venv
|
| 134 |
+
env/
|
| 135 |
+
venv/
|
| 136 |
+
ENV/
|
| 137 |
+
env.bak/
|
| 138 |
+
venv.bak/
|
| 139 |
+
|
| 140 |
+
# Spyder project settings
|
| 141 |
+
.spyderproject
|
| 142 |
+
.spyproject
|
| 143 |
+
|
| 144 |
+
# Rope project settings
|
| 145 |
+
.ropeproject
|
| 146 |
+
|
| 147 |
+
# mkdocs documentation
|
| 148 |
+
/site
|
| 149 |
+
|
| 150 |
+
# mypy
|
| 151 |
+
.mypy_cache/
|
| 152 |
+
.dmypy.json
|
| 153 |
+
dmypy.json
|
| 154 |
+
|
| 155 |
+
# Pyre type checker
|
| 156 |
+
.pyre/
|
| 157 |
+
|
| 158 |
+
# pytype static type analyzer
|
| 159 |
+
.pytype/
|
| 160 |
+
|
| 161 |
+
# Cython debug symbols
|
| 162 |
+
cython_debug/
|
| 163 |
+
|
| 164 |
+
# PyCharm
|
| 165 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 166 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 167 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 168 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 169 |
+
#.idea/*.safetensors
|
| 170 |
+
hf_upload/
|
.gitmodules
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[submodule "dataset/raw-data/ConceptARC"]
|
| 2 |
+
path = dataset/raw-data/ConceptARC
|
| 3 |
+
url = git@github.com:victorvikram/ConceptARC.git
|
| 4 |
+
[submodule "dataset/raw-data/ARC-AGI"]
|
| 5 |
+
path = dataset/raw-data/ARC-AGI
|
| 6 |
+
url = git@github.com:fchollet/ARC-AGI.git
|
| 7 |
+
[submodule "dataset/raw-data/ARC-AGI-2"]
|
| 8 |
+
path = dataset/raw-data/ARC-AGI-2
|
| 9 |
+
url = git@github.com:arcprize/ARC-AGI-2.git
|
.vscode/launch.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
// Use IntelliSense to learn about possible attributes.
|
| 3 |
+
// Hover to view descriptions of existing attributes.
|
| 4 |
+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
| 5 |
+
"version": "0.2.0",
|
| 6 |
+
"configurations": [
|
| 7 |
+
{
|
| 8 |
+
"name": "Python Debugger: Current File",
|
| 9 |
+
"type": "debugpy",
|
| 10 |
+
"request": "launch",
|
| 11 |
+
"program": "${file}",
|
| 12 |
+
"console": "integratedTerminal"
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"name": "Debug: Single GPU",
|
| 16 |
+
"type": "debugpy",
|
| 17 |
+
"request": "launch",
|
| 18 |
+
"program": "pretrain.py",
|
| 19 |
+
"args": [],
|
| 20 |
+
"env": {
|
| 21 |
+
"OMP_NUM_THREADS": "1",
|
| 22 |
+
"DISABLE_COMPILE": "true"
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
]
|
| 26 |
+
}
|
.vscode/settings.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"python.analysis.typeCheckingMode": "standard"
|
| 3 |
+
}
|
GUIDE_SPEED_TEST.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Speed Test Guide: HRM SRAM/DRAM Benchmarking
|
| 2 |
+
|
| 3 |
+
Use this guide to measure the **hardware performance** (latency, throughput, memory overhead) of the memory-tiered architecture.
|
| 4 |
+
|
| 5 |
+
> [!NOTE]
|
| 6 |
+
> This guide uses **synthetic data**. No external datasets or prior training are required. You can run this immediately after setup.
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
### 1. Environment Setup
|
| 11 |
+
Create a clean virtual environment and install dependencies:
|
| 12 |
+
|
| 13 |
+
```bash
|
| 14 |
+
python -m venv venv
|
| 15 |
+
source venv/bin/activate
|
| 16 |
+
pip install --upgrade pip
|
| 17 |
+
pip install -r requirements.txt
|
| 18 |
+
pip install triton matplotlib
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
### 2. Run Hardware Comparison
|
| 24 |
+
This command compares the **Baseline (Original)** model vs. the **Tiered (Optimized)** model across multiple configurations.
|
| 25 |
+
|
| 26 |
+
```bash
|
| 27 |
+
python run_benchmark.py --mode compare --plot
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
**What this does:**
|
| 31 |
+
1. Generates random tensors to simulate a reasoning workload.
|
| 32 |
+
2. Compiles Triton kernels for the L-level (SRAM) and H-level (DRAM) paths.
|
| 33 |
+
3. Records precise GPU timing using `torch.cuda.Event`.
|
| 34 |
+
4. Saves charts to `benchmark_results/`.
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
### 3. Deep Dive into Tiered Metrics
|
| 39 |
+
To get a detailed breakdown of memory tier behavior:
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
python run_benchmark.py --mode tiered --iterations 50 --batch-sizes 8 --seq-lens 128
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
**Key Metrics to watch:**
|
| 46 |
+
- **L_lat(μs):** Speed of the fast-updating L-level module in SRAM.
|
| 47 |
+
- **H_lat(μs):** Speed of the planning H-level module in DRAM.
|
| 48 |
+
- **H/L Ratio:** Shows the latency multiplier between memory tiers.
|
| 49 |
+
- **Memory Efficiency:** Percentage of time spent on math vs. memory transfer.
|
| 50 |
+
|
| 51 |
+
---
|
| 52 |
+
|
| 53 |
+
### 4. Custom Hardware Stress Test
|
| 54 |
+
Test the limits of your GPU by increasing batch sizes or sequence lengths:
|
| 55 |
+
|
| 56 |
+
```bash
|
| 57 |
+
python run_benchmark.py \
|
| 58 |
+
--mode compare \
|
| 59 |
+
--batch-sizes 64,128 \
|
| 60 |
+
--seq-lens 256,512 \
|
| 61 |
+
--hidden-size 1024
|
| 62 |
+
```
|
GUIDE_TRAIN_TEST.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Training Guide: HRM SRAM/DRAM Learning Performance
|
| 2 |
+
|
| 3 |
+
Use this guide to train the memory-tiered HRM on **real datasets** and verify its reasoning accuracy.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
### 1. Dataset Generation
|
| 8 |
+
The model needs to learn how to solve puzzles. First, generate a training/test set (e.g., Sudoku).
|
| 9 |
+
|
| 10 |
+
```bash
|
| 11 |
+
# Activate environment
|
| 12 |
+
source venv/bin/activate
|
| 13 |
+
|
| 14 |
+
# Build a small Sudoku dataset (1000 examples)
|
| 15 |
+
python dataset/build_sudoku_dataset.py \
|
| 16 |
+
--output-dir data/sudoku-1k \
|
| 17 |
+
--subsample-size 1000 \
|
| 18 |
+
--num-aug 10
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
### 2. Start Training
|
| 24 |
+
Point the trainer to the tiered architecture configuration.
|
| 25 |
+
|
| 26 |
+
#### Train the Tiered Model (New)
|
| 27 |
+
```bash
|
| 28 |
+
python pretrain.py \
|
| 29 |
+
arch=hrm_tiered \
|
| 30 |
+
data_path=data/sudoku-1k \
|
| 31 |
+
epochs=1000 \
|
| 32 |
+
global_batch_size=384
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
#### Train the Baseline Model (Comparison)
|
| 36 |
+
```bash
|
| 37 |
+
python pretrain.py \
|
| 38 |
+
arch=hrm_v1 \
|
| 39 |
+
data_path=data/sudoku-1k \
|
| 40 |
+
epochs=1000 \
|
| 41 |
+
global_batch_size=384
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
---
|
| 45 |
+
|
| 46 |
+
### 3. Verify Reasoning Accuracy
|
| 47 |
+
Once training is complete (or during training), check the accuracy metrics in your W&B dashboard or via the evaluation script:
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
# Replace with the path to your generated checkpoint
|
| 51 |
+
python evaluate.py checkpoint=checkpoints/Sudoku_ACT-torch/YOUR_RUN/step_1000
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
**What to look for:**
|
| 55 |
+
- **`eval/exact_accuracy`**: Fraction of puzzles solved perfectly.
|
| 56 |
+
- **`eval/steps`**: Average number of reasoning steps taken by the ACT (Adaptive Computation Time) module.
|
| 57 |
+
- **`eval/q_halt_accuracy`**: How well the model learns when to stop thinking.
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
|
| 61 |
+
### 4. Training on ARC (Artificial General Intelligence Benchmark)
|
| 62 |
+
To train on the more complex ARC-AGI-2 dataset:
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
python dataset/build_arc_dataset.py --output-dir data/arc-2
|
| 66 |
+
python pretrain.py arch=hrm_tiered data_path=data/arc-2
|
| 67 |
+
```
|
LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
Apache License
|
| 3 |
+
Version 2.0, January 2004
|
| 4 |
+
http://www.apache.org/licenses/
|
| 5 |
+
|
| 6 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 7 |
+
|
| 8 |
+
1. Definitions.
|
| 9 |
+
|
| 10 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 11 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 12 |
+
|
| 13 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 14 |
+
the copyright owner that is granting the License.
|
| 15 |
+
|
| 16 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 17 |
+
other entities that control, are controlled by, or are under common
|
| 18 |
+
control with that entity. For the purposes of this definition,
|
| 19 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 20 |
+
direction or management of such entity, whether by contract or
|
| 21 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 22 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 23 |
+
|
| 24 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 25 |
+
exercising permissions granted by this License.
|
| 26 |
+
|
| 27 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 28 |
+
including but not limited to software source code, documentation
|
| 29 |
+
source, and configuration files.
|
| 30 |
+
|
| 31 |
+
"Object" form shall mean any form resulting from mechanical
|
| 32 |
+
transformation or translation of a Source form, including but
|
| 33 |
+
not limited to compiled object code, generated documentation,
|
| 34 |
+
and conversions to other media types.
|
| 35 |
+
|
| 36 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 37 |
+
Object form, made available under the License, as indicated by a
|
| 38 |
+
copyright notice that is included in or attached to the work
|
| 39 |
+
(an example is provided in the Appendix below).
|
| 40 |
+
|
| 41 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 42 |
+
form, that is based on (or derived from) the Work and for which the
|
| 43 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 44 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 45 |
+
of this License, Derivative Works shall not include works that remain
|
| 46 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 47 |
+
the Work and Derivative Works thereof.
|
| 48 |
+
|
| 49 |
+
"Contribution" shall mean any work of authorship, including
|
| 50 |
+
the original version of the Work and any modifications or additions
|
| 51 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 52 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 53 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 54 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 55 |
+
means any form of electronic, verbal, or written communication sent
|
| 56 |
+
to the Licensor or its representatives, including but not limited to
|
| 57 |
+
communication on electronic mailing lists, source code control systems,
|
| 58 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 59 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 60 |
+
excluding communication that is conspicuously marked or otherwise
|
| 61 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 62 |
+
|
| 63 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 64 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 65 |
+
subsequently incorporated within the Work.
|
| 66 |
+
|
| 67 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 68 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 69 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 70 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 71 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 72 |
+
Work and such Derivative Works in Source or Object form.
|
| 73 |
+
|
| 74 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 75 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 76 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 77 |
+
(except as stated in this section) patent license to make, have made,
|
| 78 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 79 |
+
where such license applies only to those patent claims licensable
|
| 80 |
+
by such Contributor that are necessarily infringed by their
|
| 81 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 82 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 83 |
+
institute patent litigation against any entity (including a
|
| 84 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 85 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 86 |
+
or contributory patent infringement, then any patent licenses
|
| 87 |
+
granted to You under this License for that Work shall terminate
|
| 88 |
+
as of the date such litigation is filed.
|
| 89 |
+
|
| 90 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 91 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 92 |
+
modifications, and in Source or Object form, provided that You
|
| 93 |
+
meet the following conditions:
|
| 94 |
+
|
| 95 |
+
(a) You must give any other recipients of the Work or
|
| 96 |
+
Derivative Works a copy of this License; and
|
| 97 |
+
|
| 98 |
+
(b) You must cause any modified files to carry prominent notices
|
| 99 |
+
stating that You changed the files; and
|
| 100 |
+
|
| 101 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 102 |
+
that You distribute, all copyright, patent, trademark, and
|
| 103 |
+
attribution notices from the Source form of the Work,
|
| 104 |
+
excluding those notices that do not pertain to any part of
|
| 105 |
+
the Derivative Works; and
|
| 106 |
+
|
| 107 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 108 |
+
distribution, then any Derivative Works that You distribute must
|
| 109 |
+
include a readable copy of the attribution notices contained
|
| 110 |
+
within such NOTICE file, excluding those notices that do not
|
| 111 |
+
pertain to any part of the Derivative Works, in at least one
|
| 112 |
+
of the following places: within a NOTICE text file distributed
|
| 113 |
+
as part of the Derivative Works; within the Source form or
|
| 114 |
+
documentation, if provided along with the Derivative Works; or,
|
| 115 |
+
within a display generated by the Derivative Works, if and
|
| 116 |
+
wherever such third-party notices normally appear. The contents
|
| 117 |
+
of the NOTICE file are for informational purposes only and
|
| 118 |
+
do not modify the License. You may add Your own attribution
|
| 119 |
+
notices within Derivative Works that You distribute, alongside
|
| 120 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 121 |
+
that such additional attribution notices cannot be construed
|
| 122 |
+
as modifying the License.
|
| 123 |
+
|
| 124 |
+
You may add Your own copyright statement to Your modifications and
|
| 125 |
+
may provide additional or different license terms and conditions
|
| 126 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 127 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 128 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 129 |
+
the conditions stated in this License.
|
| 130 |
+
|
| 131 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 132 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 133 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 134 |
+
this License, without any additional terms or conditions.
|
| 135 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 136 |
+
the terms of any separate license agreement you may have executed
|
| 137 |
+
with Licensor regarding such Contributions.
|
| 138 |
+
|
| 139 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 140 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 141 |
+
except as required for reasonable and customary use in describing the
|
| 142 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 143 |
+
|
| 144 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 145 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 146 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 147 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 148 |
+
implied, including, without limitation, any warranties or conditions
|
| 149 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 150 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 151 |
+
appropriateness of using or redistributing the Work and assume any
|
| 152 |
+
risks associated with Your exercise of permissions under this License.
|
| 153 |
+
|
| 154 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 155 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 156 |
+
unless required by applicable law (such as deliberate and grossly
|
| 157 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 158 |
+
liable to You for damages, including any direct, indirect, special,
|
| 159 |
+
incidental, or consequential damages of any character arising as a
|
| 160 |
+
result of this License or out of the use or inability to use the
|
| 161 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 162 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 163 |
+
other commercial damages or losses), even if such Contributor
|
| 164 |
+
has been advised of the possibility of such damages.
|
| 165 |
+
|
| 166 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 167 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 168 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 169 |
+
or other liability obligations and/or rights consistent with this
|
| 170 |
+
License. However, in accepting such obligations, You may act only
|
| 171 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 172 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 173 |
+
defend, and hold each Contributor harmless for any liability
|
| 174 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 175 |
+
of your accepting any such warranty or additional liability.
|
| 176 |
+
|
| 177 |
+
END OF TERMS AND CONDITIONS
|
| 178 |
+
|
| 179 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 180 |
+
|
| 181 |
+
To apply the Apache License to your work, attach the following
|
| 182 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 183 |
+
replaced with your own identifying information. (Don't include
|
| 184 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 185 |
+
comment syntax for the file format. We also recommend that a
|
| 186 |
+
file or class name and description of purpose be included on the
|
| 187 |
+
same "printed page" as the copyright notice for easier
|
| 188 |
+
identification within third-party archives.
|
| 189 |
+
|
| 190 |
+
Copyright [yyyy] [name of copyright owner]
|
| 191 |
+
|
| 192 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 193 |
+
you may not use this file except in compliance with the License.
|
| 194 |
+
You may obtain a copy of the License at
|
| 195 |
+
|
| 196 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 197 |
+
|
| 198 |
+
Unless required by applicable law or agreed to in writing, software
|
| 199 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 200 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 201 |
+
See the License for the specific language governing permissions and
|
| 202 |
+
limitations under the License.
|
README.md
CHANGED
|
@@ -1,3 +1,215 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hierarchical Reasoning Model
|
| 2 |
+
|
| 3 |
+

|
| 4 |
+
|
| 5 |
+
Reasoning, the process of devising and executing complex goal-oriented action sequences, remains a critical challenge in AI.
|
| 6 |
+
Current large language models (LLMs) primarily employ Chain-of-Thought (CoT) techniques, which suffer from brittle task decomposition, extensive data requirements, and high latency. Inspired by the hierarchical and multi-timescale processing in the human brain, we propose the Hierarchical Reasoning Model (HRM), a novel recurrent architecture that attains significant computational depth while maintaining both training stability and efficiency.
|
| 7 |
+
HRM executes sequential reasoning tasks in a single forward pass without explicit supervision of the intermediate process, through two interdependent recurrent modules: a high-level module responsible for slow, abstract planning, and a low-level module handling rapid, detailed computations. With only 27 million parameters, HRM achieves exceptional performance on complex reasoning tasks using only 1000 training samples. The model operates without pre-training or CoT data, yet achieves nearly perfect performance on challenging tasks including complex Sudoku puzzles and optimal path finding in large mazes.
|
| 8 |
+
Furthermore, HRM outperforms much larger models with significantly longer context windows on the Abstraction and Reasoning Corpus (ARC), a key benchmark for measuring artificial general intelligence capabilities.
|
| 9 |
+
These results underscore HRM’s potential as a transformative advancement toward universal computation and general-purpose reasoning systems.
|
| 10 |
+
|
| 11 |
+
**Join our Discord Community: [https://discord.gg/sapient](https://discord.gg/sapient)**
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
## Quick Start Guide 🚀
|
| 15 |
+
|
| 16 |
+
### Prerequisites ⚙️
|
| 17 |
+
|
| 18 |
+
Ensure PyTorch and CUDA are installed. The repo needs CUDA extensions to be built. If not present, run the following commands:
|
| 19 |
+
|
| 20 |
+
```bash
|
| 21 |
+
# Install CUDA 12.6
|
| 22 |
+
CUDA_URL=https://developer.download.nvidia.com/compute/cuda/12.6.3/local_installers/cuda_12.6.3_560.35.05_linux.run
|
| 23 |
+
|
| 24 |
+
wget -q --show-progress --progress=bar:force:noscroll -O cuda_installer.run $CUDA_URL
|
| 25 |
+
sudo sh cuda_installer.run --silent --toolkit --override
|
| 26 |
+
|
| 27 |
+
export CUDA_HOME=/usr/local/cuda-12.6
|
| 28 |
+
|
| 29 |
+
# Install PyTorch with CUDA 12.6
|
| 30 |
+
PYTORCH_INDEX_URL=https://download.pytorch.org/whl/cu126
|
| 31 |
+
|
| 32 |
+
pip3 install torch torchvision torchaudio --index-url $PYTORCH_INDEX_URL
|
| 33 |
+
|
| 34 |
+
# Additional packages for building extensions
|
| 35 |
+
pip3 install packaging ninja wheel setuptools setuptools-scm
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
Then install FlashAttention. For Hopper GPUs, install FlashAttention 3
|
| 39 |
+
|
| 40 |
+
```bash
|
| 41 |
+
git clone git@github.com:Dao-AILab/flash-attention.git
|
| 42 |
+
cd flash-attention/hopper
|
| 43 |
+
python setup.py install
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
For Ampere or earlier GPUs, install FlashAttention 2
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
pip3 install flash-attn
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## Install Python Dependencies 🐍
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
pip install -r requirements.txt
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
## W&B Integration 📈
|
| 59 |
+
|
| 60 |
+
This project uses [Weights & Biases](https://wandb.ai/) for experiment tracking and metric visualization. Ensure you're logged in:
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
wandb login
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
## Run Experiments
|
| 67 |
+
|
| 68 |
+
### Quick Demo: Sudoku Solver 💻🗲
|
| 69 |
+
|
| 70 |
+
Train a master-level Sudoku AI capable of solving extremely difficult puzzles on a modern laptop GPU. 🧩
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
# Download and build Sudoku dataset
|
| 74 |
+
python dataset/build_sudoku_dataset.py --output-dir data/sudoku-extreme-1k-aug-1000 --subsample-size 1000 --num-aug 1000
|
| 75 |
+
|
| 76 |
+
# Start training (single GPU, smaller batch size)
|
| 77 |
+
OMP_NUM_THREADS=8 python pretrain.py data_path=data/sudoku-extreme-1k-aug-1000 epochs=20000 eval_interval=2000 global_batch_size=384 lr=7e-5 puzzle_emb_lr=7e-5 weight_decay=1.0 puzzle_emb_weight_decay=1.0
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
Runtime: ~10 hours on a RTX 4070 laptop GPU
|
| 81 |
+
|
| 82 |
+
## Trained Checkpoints 🚧
|
| 83 |
+
|
| 84 |
+
- [ARC-AGI-2](https://huggingface.co/sapientinc/HRM-checkpoint-ARC-2)
|
| 85 |
+
- [Sudoku 9x9 Extreme (1000 examples)](https://huggingface.co/sapientinc/HRM-checkpoint-sudoku-extreme)
|
| 86 |
+
- [Maze 30x30 Hard (1000 examples)](https://huggingface.co/sapientinc/HRM-checkpoint-maze-30x30-hard)
|
| 87 |
+
|
| 88 |
+
To use the checkpoints, see Evaluation section below.
|
| 89 |
+
|
| 90 |
+
## Full-scale Experiments 🔵
|
| 91 |
+
|
| 92 |
+
Experiments below assume an 8-GPU setup.
|
| 93 |
+
|
| 94 |
+
### Dataset Preparation
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
# Initialize submodules
|
| 98 |
+
git submodule update --init --recursive
|
| 99 |
+
|
| 100 |
+
# ARC-1
|
| 101 |
+
python dataset/build_arc_dataset.py # ARC offical + ConceptARC, 960 examples
|
| 102 |
+
# ARC-2
|
| 103 |
+
python dataset/build_arc_dataset.py --dataset-dirs dataset/raw-data/ARC-AGI-2/data --output-dir data/arc-2-aug-1000 # ARC-2 official, 1120 examples
|
| 104 |
+
|
| 105 |
+
# Sudoku-Extreme
|
| 106 |
+
python dataset/build_sudoku_dataset.py # Full version
|
| 107 |
+
python dataset/build_sudoku_dataset.py --output-dir data/sudoku-extreme-1k-aug-1000 --subsample-size 1000 --num-aug 1000 # 1000 examples
|
| 108 |
+
|
| 109 |
+
# Maze
|
| 110 |
+
python dataset/build_maze_dataset.py # 1000 examples
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
### Dataset Visualization
|
| 114 |
+
|
| 115 |
+
Explore the puzzles visually:
|
| 116 |
+
|
| 117 |
+
* Open `puzzle_visualizer.html` in your browser.
|
| 118 |
+
* Upload the generated dataset folder located in `data/...`.
|
| 119 |
+
|
| 120 |
+
## Launch experiments
|
| 121 |
+
|
| 122 |
+
### Small-sample (1K)
|
| 123 |
+
|
| 124 |
+
ARC-1:
|
| 125 |
+
|
| 126 |
+
```bash
|
| 127 |
+
OMP_NUM_THREADS=8 torchrun --nproc-per-node 8 pretrain.py
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
*Runtime:* ~24 hours
|
| 131 |
+
|
| 132 |
+
ARC-2:
|
| 133 |
+
|
| 134 |
+
```bash
|
| 135 |
+
OMP_NUM_THREADS=8 torchrun --nproc-per-node 8 pretrain.py data_path=data/arc-2-aug-1000
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
*Runtime:* ~24 hours (checkpoint after 8 hours is often sufficient)
|
| 139 |
+
|
| 140 |
+
Sudoku Extreme (1k):
|
| 141 |
+
|
| 142 |
+
```bash
|
| 143 |
+
OMP_NUM_THREADS=8 torchrun --nproc-per-node 8 pretrain.py data_path=data/sudoku-extreme-1k-aug-1000 epochs=20000 eval_interval=2000 lr=1e-4 puzzle_emb_lr=1e-4 weight_decay=1.0 puzzle_emb_weight_decay=1.0
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
*Runtime:* ~10 minutes
|
| 147 |
+
|
| 148 |
+
Maze 30x30 Hard (1k):
|
| 149 |
+
|
| 150 |
+
```bash
|
| 151 |
+
OMP_NUM_THREADS=8 torchrun --nproc-per-node 8 pretrain.py data_path=data/maze-30x30-hard-1k epochs=20000 eval_interval=2000 lr=1e-4 puzzle_emb_lr=1e-4 weight_decay=1.0 puzzle_emb_weight_decay=1.0
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
*Runtime:* ~1 hour
|
| 155 |
+
|
| 156 |
+
### Full Sudoku-Hard
|
| 157 |
+
|
| 158 |
+
```bash
|
| 159 |
+
OMP_NUM_THREADS=8 torchrun --nproc-per-node 8 pretrain.py data_path=data/sudoku-hard-full epochs=100 eval_interval=10 lr_min_ratio=0.1 global_batch_size=2304 lr=3e-4 puzzle_emb_lr=3e-4 weight_decay=0.1 puzzle_emb_weight_decay=0.1 arch.loss.loss_type=softmax_cross_entropy arch.L_cycles=8 arch.halt_max_steps=8 arch.pos_encodings=learned
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
*Runtime:* ~2 hours
|
| 163 |
+
|
| 164 |
+
## Streamlined Training & Benchmarking 🛠️
|
| 165 |
+
For easier monitoring and automated reporting, use these consolidated scripts:
|
| 166 |
+
|
| 167 |
+
### 1. Unified Training
|
| 168 |
+
Train either the Baseline or Tiered (SRAM/DRAM) model with multi-process support and integrated W&B monitoring.
|
| 169 |
+
```bash
|
| 170 |
+
./train_hrm.py arch=hrm_tiered epochs=1000 data_path=data/sudoku-1k
|
| 171 |
+
```
|
| 172 |
+
|
| 173 |
+
### 2. Comprehensive Evaluation
|
| 174 |
+
Load a checkpoint and generate a detailed Markdown report (`_report.md`) with accuracy breakdowns.
|
| 175 |
+
```bash
|
| 176 |
+
./eval_hrm.py checkpoint=checkpoints/hrm_run_.../best_model.pt
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
### 3. Hardware Benchmark
|
| 180 |
+
Compare Baseline vs. Tiered performance across multiple batch sizes and sequence lengths. Generates plots and CSV reports.
|
| 181 |
+
```bash
|
| 182 |
+
./benchmark_hrm.py --batch-sizes 1,8,32 --seq-lens 64,128 --plot
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
## Evaluation
|
| 186 |
+
|
| 187 |
+
Evaluate your trained models:
|
| 188 |
+
|
| 189 |
+
* Check `eval/exact_accuracy` in W&B.
|
| 190 |
+
* For ARC-AGI, follow these additional steps:
|
| 191 |
+
|
| 192 |
+
```bash
|
| 193 |
+
OMP_NUM_THREADS=8 torchrun --nproc-per-node 8 evaluate.py checkpoint=<CHECKPOINT_PATH>
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
* Then use the provided `arc_eval.ipynb` notebook to finalize and inspect your results.
|
| 197 |
+
|
| 198 |
+
## Notes
|
| 199 |
+
|
| 200 |
+
- Small-sample learning typically exhibits accuracy variance of around ±2 points.
|
| 201 |
+
- For Sudoku-Extreme (1,000-example dataset), late-stage overfitting may cause numerical instability during training and Q-learning. It is advisable to use early stopping once the training accuracy approaches 100%.
|
| 202 |
+
|
| 203 |
+
## Citation 📜
|
| 204 |
+
|
| 205 |
+
```bibtex
|
| 206 |
+
@misc{wang2025hierarchicalreasoningmodel,
|
| 207 |
+
title={Hierarchical Reasoning Model},
|
| 208 |
+
author={Guan Wang and Jin Li and Yuhao Sun and Xing Chen and Changling Liu and Yue Wu and Meng Lu and Sen Song and Yasin Abbasi Yadkori},
|
| 209 |
+
year={2025},
|
| 210 |
+
eprint={2506.21734},
|
| 211 |
+
archivePrefix={arXiv},
|
| 212 |
+
primaryClass={cs.AI},
|
| 213 |
+
url={https://arxiv.org/abs/2506.21734},
|
| 214 |
+
}
|
| 215 |
+
```
|
README_TIERED.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HRM SRAM/DRAM Memory Tiering: Training & Inference Guide
|
| 2 |
+
|
| 3 |
+
This guide explains how to train, benchmark, and evaluate both the **Original (Baseline)** and the **New (Memory-Tiered)** HRM models.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 🚀 Setup
|
| 8 |
+
|
| 9 |
+
### 1. Prerequisites
|
| 10 |
+
Ensure you have a CUDA-capable GPU. The tiered model uses **Python Triton** for high-speed SRAM kernels.
|
| 11 |
+
|
| 12 |
+
### 2. Environment Setup (Recommended)
|
| 13 |
+
Create and activate a new virtual environment to keep dependencies isolated:
|
| 14 |
+
|
| 15 |
+
```bash
|
| 16 |
+
# Create venv
|
| 17 |
+
python -m venv venv
|
| 18 |
+
|
| 19 |
+
# Activate venv (Mac/Linux)
|
| 20 |
+
source venv/bin/activate
|
| 21 |
+
|
| 22 |
+
# Install dependencies
|
| 23 |
+
pip install --upgrade pip
|
| 24 |
+
pip install -r requirements.txt
|
| 25 |
+
pip install triton matplotlib
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
---
|
| 29 |
+
|
| 30 |
+
## 🏋️ Training the Models
|
| 31 |
+
|
| 32 |
+
Training is handled by `pretrain.py` using Hydra for configuration.
|
| 33 |
+
|
| 34 |
+
### A. Train the Original (Baseline) Model
|
| 35 |
+
Uses standard GPU global memory (DRAM) for all layers.
|
| 36 |
+
|
| 37 |
+
```bash
|
| 38 |
+
# Example for Sudoku dataset
|
| 39 |
+
OMP_NUM_THREADS=8 python pretrain.py \
|
| 40 |
+
arch=hrm_v1 \
|
| 41 |
+
data_path=data/sudoku-extreme-1k-aug-1000 \
|
| 42 |
+
epochs=10000 \
|
| 43 |
+
global_batch_size=384
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
### B. Train the New (Tiered) Model
|
| 47 |
+
Optimizes the L-level module by pinning it to simulated SRAM using Triton kernels.
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
OMP_NUM_THREADS=8 python pretrain.py \
|
| 51 |
+
arch=hrm_tiered \
|
| 52 |
+
data_path=data/sudoku-extreme-1k-aug-1000 \
|
| 53 |
+
epochs=10000 \
|
| 54 |
+
global_batch_size=384
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
---
|
| 58 |
+
|
| 59 |
+
## 📊 Testing Inference & Benchmarking
|
| 60 |
+
|
| 61 |
+
The `run_benchmark.py` script is the primary tool for measuring latencies, throughput, and memory efficiency.
|
| 62 |
+
|
| 63 |
+
### 1. Comparison Mode (Recommended)
|
| 64 |
+
Automatically runs both models across multiple batch sizes and sequence lengths, then generates performance charts.
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
python run_benchmark.py \
|
| 68 |
+
--mode compare \
|
| 69 |
+
--batch-sizes 1,8,32 \
|
| 70 |
+
--seq-lens 64,128 \
|
| 71 |
+
--plot \
|
| 72 |
+
--output-dir benchmark_results/
|
| 73 |
+
```
|
| 74 |
+
* **Outputs:** `benchmark_results/benchmark_comparison.png` and `results.json`.
|
| 75 |
+
|
| 76 |
+
### 2. Tiered-Only Benchmark (Deep Dive)
|
| 77 |
+
Get detailed metrics for just the tiered model, including memory transfer overhead and SRAM hit rates.
|
| 78 |
+
|
| 79 |
+
```bash
|
| 80 |
+
python run_benchmark.py \
|
| 81 |
+
--mode tiered \
|
| 82 |
+
--iterations 50 \
|
| 83 |
+
--batch-sizes 8 \
|
| 84 |
+
--output tiered_stats.json
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
---
|
| 88 |
+
|
| 89 |
+
## ✅ Evaluation
|
| 90 |
+
|
| 91 |
+
To verify the accuracy of a trained checkpoint on a test set:
|
| 92 |
+
|
| 93 |
+
### Using the original evaluate script:
|
| 94 |
+
```bash
|
| 95 |
+
python evaluate.py checkpoint=checkpoints/PATH_TO_YOUR_STEP
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
### Using the tiered model class manually:
|
| 99 |
+
If you are writing a custom script, you can import the models as follows:
|
| 100 |
+
|
| 101 |
+
```python
|
| 102 |
+
from models.hrm.hrm_act_v1 import HierarchicalReasoningModel_ACTV1 # Original
|
| 103 |
+
from models.hrm.hrm_tiered import HRM_Tiered # Tiered
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
---
|
| 107 |
+
|
| 108 |
+
## 🛠 Model Location Reference
|
| 109 |
+
|
| 110 |
+
| Folder/File | Contents |
|
| 111 |
+
| :--- | :--- |
|
| 112 |
+
| `models/hrm/hrm_act_v1.py` | Original model code (Baseline) |
|
| 113 |
+
| `models/hrm/hrm_tiered.py` | New Tiered model code |
|
| 114 |
+
| `models/triton_kernels.py` | Triton SRAM/DRAM kernels |
|
| 115 |
+
| `models/memory_tier.py` | Memory Tier Manager and CUDA stream logic |
|
| 116 |
+
| `config/arch/hrm_tiered.yaml` | Configuration for the tiered version |
|
arc_eval.ipynb
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "code",
|
| 5 |
+
"execution_count": null,
|
| 6 |
+
"metadata": {},
|
| 7 |
+
"outputs": [],
|
| 8 |
+
"source": [
|
| 9 |
+
"import os\n",
|
| 10 |
+
"import json\n",
|
| 11 |
+
"from glob import glob\n",
|
| 12 |
+
"import hashlib\n",
|
| 13 |
+
"import matplotlib.pyplot as plt\n",
|
| 14 |
+
"import matplotlib.colors as mcolors\n",
|
| 15 |
+
"\n",
|
| 16 |
+
"import torch\n",
|
| 17 |
+
"import torch.nn.functional as F\n",
|
| 18 |
+
"import numpy as np\n",
|
| 19 |
+
"from numba import njit\n",
|
| 20 |
+
"\n",
|
| 21 |
+
"from dataset.common import inverse_dihedral_transform\n",
|
| 22 |
+
"\n",
|
| 23 |
+
"\n",
|
| 24 |
+
"DATASET_PATH = \"data/arc-aug-1000\" # ARC-1\n",
|
| 25 |
+
"# DATASET_PATH = \"data/arc-2-aug-1000\" # ARC-2\n",
|
| 26 |
+
"\n",
|
| 27 |
+
"CHECKPOINT_PATH = \"checkpoints/Arc-aug-1000 ACT-torch/HierarchicalReasoningModel_ACTV1 amphibian-turaco/step_414456\"\n",
|
| 28 |
+
"\n",
|
| 29 |
+
"\n",
|
| 30 |
+
"PAD_PUZZLE_IDENTIFIER = 0\n",
|
| 31 |
+
"\n",
|
| 32 |
+
"# Visualization\n",
|
| 33 |
+
"ARC_COLOR_MAP = mcolors.ListedColormap([\n",
|
| 34 |
+
" \"#000000\", # symbol_0: black\n",
|
| 35 |
+
" \"#0074D9\", # symbol_1: blue\n",
|
| 36 |
+
" \"#FF4136\", # symbol_2: red\n",
|
| 37 |
+
" \"#2ECC40\", # symbol_3: green\n",
|
| 38 |
+
" \"#FFDC00\", # symbol_4: yellow\n",
|
| 39 |
+
" \"#AAAAAA\", # symbol_5: grey\n",
|
| 40 |
+
" \"#F012BE\", # symbol_6: fuschia\n",
|
| 41 |
+
" \"#FF851B\", # symbol_7: orange\n",
|
| 42 |
+
" \"#7FDBFF\", # symbol_8: teal\n",
|
| 43 |
+
" \"#870C25\" # symbol_9: brown\n",
|
| 44 |
+
"])"
|
| 45 |
+
]
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"cell_type": "code",
|
| 49 |
+
"execution_count": null,
|
| 50 |
+
"metadata": {},
|
| 51 |
+
"outputs": [],
|
| 52 |
+
"source": [
|
| 53 |
+
"def load_identifiers_and_preds(dataset_path: str, checkpoint_path: str):\n",
|
| 54 |
+
" # Load puzzle identifiers\n",
|
| 55 |
+
" with open(os.path.join(dataset_path, \"identifiers.json\"), \"r\") as f:\n",
|
| 56 |
+
" identifier_map = json.load(f)\n",
|
| 57 |
+
" \n",
|
| 58 |
+
" # Load preds\n",
|
| 59 |
+
" all_preds = {}\n",
|
| 60 |
+
" for filename in glob(f\"{checkpoint_path}_all_preds.*\"):\n",
|
| 61 |
+
" preds = torch.load(filename)\n",
|
| 62 |
+
" for k, v in preds.items():\n",
|
| 63 |
+
" all_preds.setdefault(k, [])\n",
|
| 64 |
+
" all_preds[k].append(v)\n",
|
| 65 |
+
" \n",
|
| 66 |
+
" del preds\n",
|
| 67 |
+
"\n",
|
| 68 |
+
" all_preds = {k: torch.cat(v, dim=0) for k, v in all_preds.items()}\n",
|
| 69 |
+
" \n",
|
| 70 |
+
" # Remove paddings\n",
|
| 71 |
+
" mask = all_preds[\"puzzle_identifiers\"] != PAD_PUZZLE_IDENTIFIER\n",
|
| 72 |
+
" all_preds = {k: v[mask] for k, v in all_preds.items()}\n",
|
| 73 |
+
"\n",
|
| 74 |
+
" return identifier_map, all_preds\n",
|
| 75 |
+
"\n",
|
| 76 |
+
"\n",
|
| 77 |
+
"def inverse_aug(name: str, grid: np.ndarray):\n",
|
| 78 |
+
" if \"_\" not in name:\n",
|
| 79 |
+
" return grid\n",
|
| 80 |
+
"\n",
|
| 81 |
+
" trans_id, perm = name.split(\"_\")[-2:]\n",
|
| 82 |
+
" trans_id = int(trans_id[1:]) # Remove \"t\" letter\n",
|
| 83 |
+
" inv_perm = np.argsort(list(perm))\n",
|
| 84 |
+
" \n",
|
| 85 |
+
" return inv_perm[inverse_dihedral_transform(grid, trans_id)]\n",
|
| 86 |
+
"\n",
|
| 87 |
+
"\n",
|
| 88 |
+
"def grid_hash(grid: np.ndarray):\n",
|
| 89 |
+
" return hash((grid.tobytes(), grid.shape))\n",
|
| 90 |
+
"\n",
|
| 91 |
+
"\n",
|
| 92 |
+
"@njit\n",
|
| 93 |
+
"def crop(grid: np.ndarray):\n",
|
| 94 |
+
" # Find maximum-sized rectangle without any EOS token inside.\n",
|
| 95 |
+
" grid = grid.reshape(30, 30)\n",
|
| 96 |
+
"\n",
|
| 97 |
+
" max_area = 0\n",
|
| 98 |
+
" max_size = (0, 0)\n",
|
| 99 |
+
" nr, nc = grid.shape\n",
|
| 100 |
+
" \n",
|
| 101 |
+
" num_c = nc\n",
|
| 102 |
+
" for num_r in range(1, nr + 1):\n",
|
| 103 |
+
" # Scan for maximum c\n",
|
| 104 |
+
" for c in range(1, num_c + 1):\n",
|
| 105 |
+
" x = grid[num_r - 1, c - 1]\n",
|
| 106 |
+
" if (x < 2) | (x > 11):\n",
|
| 107 |
+
" num_c = c - 1\n",
|
| 108 |
+
" break\n",
|
| 109 |
+
" \n",
|
| 110 |
+
" area = num_r * num_c\n",
|
| 111 |
+
" if area > max_area:\n",
|
| 112 |
+
" max_area = area\n",
|
| 113 |
+
" max_size = (num_r, num_c)\n",
|
| 114 |
+
"\n",
|
| 115 |
+
" return grid[:max_size[0], :max_size[1]] - 2\n",
|
| 116 |
+
"\n",
|
| 117 |
+
"\n",
|
| 118 |
+
"def test(visualize, Ks=[1, 2, 10, 100, 1000]):\n",
|
| 119 |
+
" identifier_map, all_preds = load_identifiers_and_preds(DATASET_PATH, CHECKPOINT_PATH)\n",
|
| 120 |
+
" \n",
|
| 121 |
+
" global_hmap = {}\n",
|
| 122 |
+
" \n",
|
| 123 |
+
" # Get puzzles and corresponding answers\n",
|
| 124 |
+
" puzzle_labels = {}\n",
|
| 125 |
+
" for identifier, input, label in zip(all_preds[\"puzzle_identifiers\"], all_preds[\"inputs\"], all_preds[\"labels\"]):\n",
|
| 126 |
+
" name = identifier_map[identifier]\n",
|
| 127 |
+
" if \"_\" not in name: # Not-augmented\n",
|
| 128 |
+
" puzzle_labels.setdefault(name, {})\n",
|
| 129 |
+
" \n",
|
| 130 |
+
" input = crop(input.numpy())\n",
|
| 131 |
+
" label = crop(label.numpy())\n",
|
| 132 |
+
"\n",
|
| 133 |
+
" input_hash = grid_hash(input)\n",
|
| 134 |
+
" label_hash = grid_hash(label)\n",
|
| 135 |
+
"\n",
|
| 136 |
+
" global_hmap[input_hash] = input\n",
|
| 137 |
+
" global_hmap[label_hash] = label\n",
|
| 138 |
+
"\n",
|
| 139 |
+
" assert input_hash not in puzzle_labels[name]\n",
|
| 140 |
+
" puzzle_labels[name][input_hash] = label_hash\n",
|
| 141 |
+
" \n",
|
| 142 |
+
" print (\"Number of puzzles\", len(puzzle_labels))\n",
|
| 143 |
+
" \n",
|
| 144 |
+
" # Argmax prediction\n",
|
| 145 |
+
" preds = all_preds[\"logits\"].argmax(-1)\n",
|
| 146 |
+
"\n",
|
| 147 |
+
" # Collate\n",
|
| 148 |
+
" pred_answers = {}\n",
|
| 149 |
+
" for identifier, input, pred, q in zip(all_preds[\"puzzle_identifiers\"], all_preds[\"inputs\"], preds, all_preds[\"q_halt_logits\"].sigmoid()):\n",
|
| 150 |
+
" name = identifier_map[identifier]\n",
|
| 151 |
+
" orig_name = name.split(\"_\")[0]\n",
|
| 152 |
+
" \n",
|
| 153 |
+
" input = input.numpy()\n",
|
| 154 |
+
" input_hash = grid_hash(inverse_aug(name, crop(input)))\n",
|
| 155 |
+
" assert input_hash in puzzle_labels[orig_name]\n",
|
| 156 |
+
" \n",
|
| 157 |
+
" pred = inverse_aug(name, crop(pred.numpy()))\n",
|
| 158 |
+
" pred_hash = grid_hash(pred)\n",
|
| 159 |
+
" global_hmap[pred_hash] = pred\n",
|
| 160 |
+
" \n",
|
| 161 |
+
" pred_answers.setdefault(orig_name, {})\n",
|
| 162 |
+
" pred_answers[orig_name].setdefault(input_hash, [])\n",
|
| 163 |
+
" pred_answers[orig_name][input_hash].append((pred_hash, q.item()))\n",
|
| 164 |
+
"\n",
|
| 165 |
+
" # test-1\n",
|
| 166 |
+
" if visualize:\n",
|
| 167 |
+
" num_figs = sum(len(tests) for name, tests in puzzle_labels.items())\n",
|
| 168 |
+
" fig, axes = plt.subplots(num_figs, 4, figsize=(8, num_figs * 4))\n",
|
| 169 |
+
" \n",
|
| 170 |
+
" fig_id = 0\n",
|
| 171 |
+
" \n",
|
| 172 |
+
" correct = [0 for _ in range(len(Ks))]\n",
|
| 173 |
+
" for name, tests in puzzle_labels.items():\n",
|
| 174 |
+
" num_test_correct = [0 for _ in range(len(Ks))]\n",
|
| 175 |
+
" for input_hash, label_hash in tests.items():\n",
|
| 176 |
+
" p = pred_answers[name][input_hash]\n",
|
| 177 |
+
" p_map = {}\n",
|
| 178 |
+
" \n",
|
| 179 |
+
" for h, q in p:\n",
|
| 180 |
+
" p_map.setdefault(h, [0, 0])\n",
|
| 181 |
+
" p_map[h][0] += 1\n",
|
| 182 |
+
" p_map[h][1] += q\n",
|
| 183 |
+
" \n",
|
| 184 |
+
" for h, stats in p_map.items():\n",
|
| 185 |
+
" stats[1] /= stats[0]\n",
|
| 186 |
+
" \n",
|
| 187 |
+
" p_map = sorted(p_map.items(), key=lambda kv: kv[1], reverse=True)\n",
|
| 188 |
+
"\n",
|
| 189 |
+
" # 2-vote\n",
|
| 190 |
+
" for i, k in enumerate(Ks):\n",
|
| 191 |
+
" ok = False\n",
|
| 192 |
+
" for h, stats in p_map[:k]:\n",
|
| 193 |
+
" ok |= h == label_hash\n",
|
| 194 |
+
" \n",
|
| 195 |
+
" num_test_correct[i] += ok\n",
|
| 196 |
+
"\n",
|
| 197 |
+
" if visualize:\n",
|
| 198 |
+
" # Show input and ground truth\n",
|
| 199 |
+
" axes[fig_id, 0].imshow(global_hmap[input_hash], cmap=ARC_COLOR_MAP)\n",
|
| 200 |
+
" axes[fig_id, 0].set_title(f\"{name}\\nInput\")\n",
|
| 201 |
+
" axes[fig_id, 0].axis('off')\n",
|
| 202 |
+
" \n",
|
| 203 |
+
" axes[fig_id, 1].imshow(global_hmap[label_hash], cmap=ARC_COLOR_MAP)\n",
|
| 204 |
+
" axes[fig_id, 1].set_title(f\"{name}\\nAnswer\")\n",
|
| 205 |
+
" axes[fig_id, 1].axis('off')\n",
|
| 206 |
+
" \n",
|
| 207 |
+
" trial_id = 2\n",
|
| 208 |
+
" for h, stats in p_map[:2]:\n",
|
| 209 |
+
" ans = global_hmap[h]\n",
|
| 210 |
+
" \n",
|
| 211 |
+
" axes[fig_id, trial_id].imshow(ans, cmap=ARC_COLOR_MAP)\n",
|
| 212 |
+
" axes[fig_id, trial_id].set_title(f\"{name}\\nTrial {trial_id}\")\n",
|
| 213 |
+
" axes[fig_id, trial_id].axis('off')\n",
|
| 214 |
+
" \n",
|
| 215 |
+
" trial_id += 1\n",
|
| 216 |
+
" \n",
|
| 217 |
+
" fig_id += 1\n",
|
| 218 |
+
" \n",
|
| 219 |
+
" # Total correctness\n",
|
| 220 |
+
" for i in range(len(Ks)):\n",
|
| 221 |
+
" correct[i] += num_test_correct[i] == len(tests)\n",
|
| 222 |
+
"\n",
|
| 223 |
+
" for i, k in enumerate(Ks):\n",
|
| 224 |
+
" print (f\"{k}-shot: {correct[i] / len(puzzle_labels) * 100:.2f}%\")\n",
|
| 225 |
+
"\n",
|
| 226 |
+
"\n",
|
| 227 |
+
"test(visualize=False)"
|
| 228 |
+
]
|
| 229 |
+
}
|
| 230 |
+
],
|
| 231 |
+
"metadata": {
|
| 232 |
+
"kernelspec": {
|
| 233 |
+
"display_name": "Python 3",
|
| 234 |
+
"language": "python",
|
| 235 |
+
"name": "python3"
|
| 236 |
+
},
|
| 237 |
+
"language_info": {
|
| 238 |
+
"codemirror_mode": {
|
| 239 |
+
"name": "ipython",
|
| 240 |
+
"version": 3
|
| 241 |
+
},
|
| 242 |
+
"file_extension": ".py",
|
| 243 |
+
"mimetype": "text/x-python",
|
| 244 |
+
"name": "python",
|
| 245 |
+
"nbconvert_exporter": "python",
|
| 246 |
+
"pygments_lexer": "ipython3",
|
| 247 |
+
"version": "3.12.10"
|
| 248 |
+
}
|
| 249 |
+
},
|
| 250 |
+
"nbformat": 4,
|
| 251 |
+
"nbformat_minor": 2
|
| 252 |
+
}
|
assets/hrm.png
ADDED
|
assets/npyjs.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class npyjs {
|
| 2 |
+
|
| 3 |
+
constructor(opts) {
|
| 4 |
+
if (opts && !('convertFloat16' in opts)) {
|
| 5 |
+
console.warn([
|
| 6 |
+
"npyjs constructor now accepts {convertFloat16?: boolean}.",
|
| 7 |
+
"For usage, go to https://github.com/jhuapl-boss/npyjs."
|
| 8 |
+
].join(" "));
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
this.convertFloat16 = opts?.convertFloat16 ?? true;
|
| 12 |
+
|
| 13 |
+
this.dtypes = {
|
| 14 |
+
"<u1": {
|
| 15 |
+
name: "uint8",
|
| 16 |
+
size: 8,
|
| 17 |
+
arrayConstructor: Uint8Array,
|
| 18 |
+
},
|
| 19 |
+
"|u1": {
|
| 20 |
+
name: "uint8",
|
| 21 |
+
size: 8,
|
| 22 |
+
arrayConstructor: Uint8Array,
|
| 23 |
+
},
|
| 24 |
+
"<u2": {
|
| 25 |
+
name: "uint16",
|
| 26 |
+
size: 16,
|
| 27 |
+
arrayConstructor: Uint16Array,
|
| 28 |
+
},
|
| 29 |
+
"|i1": {
|
| 30 |
+
name: "int8",
|
| 31 |
+
size: 8,
|
| 32 |
+
arrayConstructor: Int8Array,
|
| 33 |
+
},
|
| 34 |
+
"<i2": {
|
| 35 |
+
name: "int16",
|
| 36 |
+
size: 16,
|
| 37 |
+
arrayConstructor: Int16Array,
|
| 38 |
+
},
|
| 39 |
+
"<u4": {
|
| 40 |
+
name: "uint32",
|
| 41 |
+
size: 32,
|
| 42 |
+
arrayConstructor: Uint32Array,
|
| 43 |
+
},
|
| 44 |
+
"<i4": {
|
| 45 |
+
name: "int32",
|
| 46 |
+
size: 32,
|
| 47 |
+
arrayConstructor: Int32Array,
|
| 48 |
+
},
|
| 49 |
+
"<u8": {
|
| 50 |
+
name: "uint64",
|
| 51 |
+
size: 64,
|
| 52 |
+
arrayConstructor: BigUint64Array,
|
| 53 |
+
},
|
| 54 |
+
"<i8": {
|
| 55 |
+
name: "int64",
|
| 56 |
+
size: 64,
|
| 57 |
+
arrayConstructor: BigInt64Array,
|
| 58 |
+
},
|
| 59 |
+
"<f4": {
|
| 60 |
+
name: "float32",
|
| 61 |
+
size: 32,
|
| 62 |
+
arrayConstructor: Float32Array
|
| 63 |
+
},
|
| 64 |
+
"<f8": {
|
| 65 |
+
name: "float64",
|
| 66 |
+
size: 64,
|
| 67 |
+
arrayConstructor: Float64Array
|
| 68 |
+
},
|
| 69 |
+
"<f2": {
|
| 70 |
+
name: "float16",
|
| 71 |
+
size: 16,
|
| 72 |
+
arrayConstructor: Uint16Array,
|
| 73 |
+
converter: this.convertFloat16 ? this.float16ToFloat32Array : undefined
|
| 74 |
+
},
|
| 75 |
+
};
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
float16ToFloat32Array(float16Array) {
|
| 79 |
+
const length = float16Array.length;
|
| 80 |
+
const float32Array = new Float32Array(length);
|
| 81 |
+
|
| 82 |
+
for (let i = 0; i < length; i++) {
|
| 83 |
+
float32Array[i] = npyjs.float16ToFloat32(float16Array[i]);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
return float32Array;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
static float16ToFloat32(float16) {
|
| 90 |
+
// Extract the parts of the float16
|
| 91 |
+
const sign = (float16 >> 15) & 0x1;
|
| 92 |
+
const exponent = (float16 >> 10) & 0x1f;
|
| 93 |
+
const fraction = float16 & 0x3ff;
|
| 94 |
+
|
| 95 |
+
// Handle special cases
|
| 96 |
+
if (exponent === 0) {
|
| 97 |
+
if (fraction === 0) {
|
| 98 |
+
// Zero
|
| 99 |
+
return sign ? -0 : 0;
|
| 100 |
+
}
|
| 101 |
+
// Denormalized number
|
| 102 |
+
return (sign ? -1 : 1) * Math.pow(2, -14) * (fraction / 0x400);
|
| 103 |
+
} else if (exponent === 0x1f) {
|
| 104 |
+
if (fraction === 0) {
|
| 105 |
+
// Infinity
|
| 106 |
+
return sign ? -Infinity : Infinity;
|
| 107 |
+
}
|
| 108 |
+
// NaN
|
| 109 |
+
return NaN;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// Normalized number
|
| 113 |
+
return (sign ? -1 : 1) * Math.pow(2, exponent - 15) * (1 + fraction / 0x400);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
parse(arrayBufferContents) {
|
| 117 |
+
// const version = arrayBufferContents.slice(6, 8); // Uint8-encoded
|
| 118 |
+
const headerLength = new DataView(arrayBufferContents.slice(8, 10)).getUint8(0);
|
| 119 |
+
const offsetBytes = 10 + headerLength;
|
| 120 |
+
|
| 121 |
+
const hcontents = new TextDecoder("utf-8").decode(
|
| 122 |
+
new Uint8Array(arrayBufferContents.slice(10, 10 + headerLength))
|
| 123 |
+
);
|
| 124 |
+
const header = JSON.parse(
|
| 125 |
+
hcontents
|
| 126 |
+
.toLowerCase() // True -> true
|
| 127 |
+
.replace(/'/g, '"')
|
| 128 |
+
.replace("(", "[")
|
| 129 |
+
.replace(/,*\),*/g, "]")
|
| 130 |
+
);
|
| 131 |
+
const shape = header.shape;
|
| 132 |
+
const dtype = this.dtypes[header.descr];
|
| 133 |
+
|
| 134 |
+
if (!dtype) {
|
| 135 |
+
console.error(`Unsupported dtype: ${header.descr}`);
|
| 136 |
+
return null;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
const nums = new dtype.arrayConstructor(
|
| 140 |
+
arrayBufferContents,
|
| 141 |
+
offsetBytes
|
| 142 |
+
);
|
| 143 |
+
|
| 144 |
+
// Convert float16 to float32 if converter exists
|
| 145 |
+
const data = dtype.converter ? dtype.converter.call(this, nums) : nums;
|
| 146 |
+
|
| 147 |
+
return {
|
| 148 |
+
dtype: dtype.name,
|
| 149 |
+
data: data,
|
| 150 |
+
shape,
|
| 151 |
+
fortranOrder: header.fortran_order
|
| 152 |
+
};
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
async load(filename, callback, fetchArgs) {
|
| 156 |
+
/*
|
| 157 |
+
Loads an array from a stream of bytes.
|
| 158 |
+
*/
|
| 159 |
+
fetchArgs = fetchArgs || {};
|
| 160 |
+
let arrayBuf;
|
| 161 |
+
// If filename is ArrayBuffer
|
| 162 |
+
if (filename instanceof ArrayBuffer) {
|
| 163 |
+
arrayBuf = filename;
|
| 164 |
+
}
|
| 165 |
+
// If filename is a file path
|
| 166 |
+
else {
|
| 167 |
+
const resp = await fetch(filename, { ...fetchArgs });
|
| 168 |
+
arrayBuf = await resp.arrayBuffer();
|
| 169 |
+
}
|
| 170 |
+
const result = this.parse(arrayBuf);
|
| 171 |
+
if (callback) {
|
| 172 |
+
return callback(result);
|
| 173 |
+
}
|
| 174 |
+
return result;
|
| 175 |
+
}
|
| 176 |
+
}
|
benchmark.py
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Comprehensive Benchmarking Suite for HRM SRAM/DRAM Memory Tiering.
|
| 3 |
+
|
| 4 |
+
Measures and compares:
|
| 5 |
+
- Per-module latency (L-level SRAM vs H-level DRAM)
|
| 6 |
+
- End-to-end inference latency
|
| 7 |
+
- Throughput (samples/sec)
|
| 8 |
+
- Memory usage per tier (SRAM / DRAM)
|
| 9 |
+
- Cross-tier transfer overhead
|
| 10 |
+
- SRAM hit rate
|
| 11 |
+
- Triton kernel-level profiling
|
| 12 |
+
- GPU utilization / power draw (when available)
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import json
|
| 16 |
+
import time
|
| 17 |
+
import math
|
| 18 |
+
import os
|
| 19 |
+
from typing import Dict, List, Optional, Tuple
|
| 20 |
+
from dataclasses import dataclass, asdict
|
| 21 |
+
|
| 22 |
+
import torch
|
| 23 |
+
import torch.nn.functional as F
|
| 24 |
+
|
| 25 |
+
from models.memory_tier import MemoryTierManager
|
| 26 |
+
from models.hrm.hrm_tiered import HRM_Tiered, HRM_Tiered_Inner
|
| 27 |
+
from models.hrm.hrm_act_v1 import (
|
| 28 |
+
HierarchicalReasoningModel_ACTV1,
|
| 29 |
+
HierarchicalReasoningModel_ACTV1Config,
|
| 30 |
+
HierarchicalReasoningModel_ACTV1InnerCarry,
|
| 31 |
+
)
|
| 32 |
+
from models.triton_kernels import triton_memory_latency_probe
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@dataclass
|
| 36 |
+
class BenchmarkResult:
|
| 37 |
+
"""Results from a single benchmark run."""
|
| 38 |
+
model_name: str
|
| 39 |
+
batch_size: int
|
| 40 |
+
seq_len: int
|
| 41 |
+
hidden_size: int
|
| 42 |
+
H_cycles: int
|
| 43 |
+
L_cycles: int
|
| 44 |
+
H_layers: int
|
| 45 |
+
L_layers: int
|
| 46 |
+
num_iterations: int
|
| 47 |
+
warmup_iterations: int
|
| 48 |
+
|
| 49 |
+
# Latency (μs)
|
| 50 |
+
l_level_latency_mean_us: float
|
| 51 |
+
l_level_latency_min_us: float
|
| 52 |
+
l_level_latency_max_us: float
|
| 53 |
+
l_level_latency_std_us: float
|
| 54 |
+
|
| 55 |
+
h_level_latency_mean_us: float
|
| 56 |
+
h_level_latency_min_us: float
|
| 57 |
+
h_level_latency_max_us: float
|
| 58 |
+
h_level_latency_std_us: float
|
| 59 |
+
|
| 60 |
+
total_inference_latency_mean_ms: float
|
| 61 |
+
total_inference_latency_min_ms: float
|
| 62 |
+
total_inference_latency_max_ms: float
|
| 63 |
+
total_inference_latency_std_ms: float
|
| 64 |
+
|
| 65 |
+
# Throughput
|
| 66 |
+
throughput_samples_per_sec: float
|
| 67 |
+
|
| 68 |
+
# Memory (MB)
|
| 69 |
+
sram_peak_mb: float
|
| 70 |
+
dram_peak_mb: float
|
| 71 |
+
total_gpu_memory_mb: float
|
| 72 |
+
|
| 73 |
+
# Transfer metrics (μs)
|
| 74 |
+
h_l_transfer_mean_us: float
|
| 75 |
+
l_h_transfer_mean_us: float
|
| 76 |
+
|
| 77 |
+
# SRAM metrics
|
| 78 |
+
sram_hit_rate: float
|
| 79 |
+
|
| 80 |
+
# Triton kernel metrics
|
| 81 |
+
triton_sram_probe_latency_us: float
|
| 82 |
+
triton_dram_probe_latency_us: float
|
| 83 |
+
|
| 84 |
+
# GPU metrics
|
| 85 |
+
gpu_utilization_pct: Optional[float]
|
| 86 |
+
gpu_power_w: Optional[float]
|
| 87 |
+
gpu_temperature_c: Optional[float]
|
| 88 |
+
|
| 89 |
+
# Derived
|
| 90 |
+
h_over_l_latency_ratio: float
|
| 91 |
+
memory_efficiency: float # useful_compute_time / total_time
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _std(values: List[float]) -> float:
|
| 95 |
+
if len(values) < 2:
|
| 96 |
+
return 0.0
|
| 97 |
+
mean = sum(values) / len(values)
|
| 98 |
+
var = sum((v - mean) ** 2 for v in values) / (len(values) - 1)
|
| 99 |
+
return math.sqrt(var)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _create_dummy_batch(
|
| 103 |
+
batch_size: int,
|
| 104 |
+
seq_len: int,
|
| 105 |
+
vocab_size: int,
|
| 106 |
+
device: torch.device,
|
| 107 |
+
) -> Dict[str, torch.Tensor]:
|
| 108 |
+
"""Create a synthetic batch for benchmarking."""
|
| 109 |
+
return {
|
| 110 |
+
"inputs": torch.randint(0, vocab_size, (batch_size, seq_len), device=device),
|
| 111 |
+
"labels": torch.randint(0, vocab_size, (batch_size, seq_len), device=device),
|
| 112 |
+
"puzzle_identifiers": torch.arange(batch_size, device=device),
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def _get_gpu_metrics() -> Dict[str, Optional[float]]:
|
| 117 |
+
"""Try to read GPU utilization, power, temperature via nvidia-smi."""
|
| 118 |
+
metrics = {'utilization': None, 'power': None, 'temperature': None}
|
| 119 |
+
try:
|
| 120 |
+
import subprocess
|
| 121 |
+
result = subprocess.run(
|
| 122 |
+
['nvidia-smi', '--query-gpu=utilization.gpu,power.draw,temperature.gpu',
|
| 123 |
+
'--format=csv,noheader,nounits'],
|
| 124 |
+
capture_output=True, text=True, timeout=5,
|
| 125 |
+
)
|
| 126 |
+
if result.returncode == 0:
|
| 127 |
+
parts = result.stdout.strip().split(',')
|
| 128 |
+
if len(parts) >= 3:
|
| 129 |
+
metrics['utilization'] = float(parts[0].strip())
|
| 130 |
+
metrics['power'] = float(parts[1].strip())
|
| 131 |
+
metrics['temperature'] = float(parts[2].strip())
|
| 132 |
+
except Exception:
|
| 133 |
+
pass
|
| 134 |
+
return metrics
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def _run_triton_latency_probe(
|
| 138 |
+
batch_size: int,
|
| 139 |
+
hidden_size: int,
|
| 140 |
+
device: torch.device,
|
| 141 |
+
num_iters: int = 100,
|
| 142 |
+
) -> Tuple[float, float]:
|
| 143 |
+
"""Measure SRAM vs DRAM effective latency using Triton probe kernels.
|
| 144 |
+
|
| 145 |
+
Returns (sram_latency_us, dram_latency_us).
|
| 146 |
+
"""
|
| 147 |
+
if not torch.cuda.is_available():
|
| 148 |
+
return 0.0, 0.0
|
| 149 |
+
|
| 150 |
+
# Small tensor → fits in SRAM (L1/L2/registers)
|
| 151 |
+
sram_data = torch.randn(batch_size, hidden_size, device=device, dtype=torch.float32)
|
| 152 |
+
|
| 153 |
+
# Large tensor → forces DRAM access (much larger than L2)
|
| 154 |
+
dram_size = max(hidden_size, 65536) # Force spill to DRAM
|
| 155 |
+
dram_data = torch.randn(batch_size * 64, dram_size, device=device, dtype=torch.float32)
|
| 156 |
+
|
| 157 |
+
# Make a contiguous slice for DRAM probe
|
| 158 |
+
dram_probe_data = dram_data[:batch_size, :hidden_size].contiguous()
|
| 159 |
+
|
| 160 |
+
# Warmup
|
| 161 |
+
triton_memory_latency_probe(sram_data, num_iters=10)
|
| 162 |
+
triton_memory_latency_probe(dram_probe_data, num_iters=10)
|
| 163 |
+
torch.cuda.synchronize()
|
| 164 |
+
|
| 165 |
+
# SRAM probe
|
| 166 |
+
start = torch.cuda.Event(enable_timing=True)
|
| 167 |
+
end = torch.cuda.Event(enable_timing=True)
|
| 168 |
+
start.record()
|
| 169 |
+
triton_memory_latency_probe(sram_data, num_iters=num_iters)
|
| 170 |
+
end.record()
|
| 171 |
+
torch.cuda.synchronize()
|
| 172 |
+
sram_us = start.elapsed_time(end) * 1000 # ms → μs
|
| 173 |
+
|
| 174 |
+
# DRAM probe (access scattered to prevent caching)
|
| 175 |
+
start2 = torch.cuda.Event(enable_timing=True)
|
| 176 |
+
end2 = torch.cuda.Event(enable_timing=True)
|
| 177 |
+
start2.record()
|
| 178 |
+
triton_memory_latency_probe(dram_probe_data, num_iters=num_iters)
|
| 179 |
+
end2.record()
|
| 180 |
+
torch.cuda.synchronize()
|
| 181 |
+
dram_us = start2.elapsed_time(end2) * 1000
|
| 182 |
+
|
| 183 |
+
# Cleanup
|
| 184 |
+
del sram_data, dram_data
|
| 185 |
+
torch.cuda.empty_cache()
|
| 186 |
+
|
| 187 |
+
return sram_us, dram_us
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def benchmark_tiered_model(
|
| 191 |
+
batch_size: int = 8,
|
| 192 |
+
seq_len: int = 64,
|
| 193 |
+
hidden_size: int = 512,
|
| 194 |
+
num_heads: int = 8,
|
| 195 |
+
H_cycles: int = 2,
|
| 196 |
+
L_cycles: int = 2,
|
| 197 |
+
H_layers: int = 4,
|
| 198 |
+
L_layers: int = 4,
|
| 199 |
+
halt_max_steps: int = 1,
|
| 200 |
+
warmup: int = 5,
|
| 201 |
+
iterations: int = 20,
|
| 202 |
+
device: Optional[torch.device] = None,
|
| 203 |
+
) -> BenchmarkResult:
|
| 204 |
+
"""Benchmark the tiered HRM model."""
|
| 205 |
+
|
| 206 |
+
if device is None:
|
| 207 |
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 208 |
+
|
| 209 |
+
vocab_size = 32
|
| 210 |
+
config_dict = {
|
| 211 |
+
'batch_size': batch_size,
|
| 212 |
+
'seq_len': seq_len,
|
| 213 |
+
'puzzle_emb_ndim': 0,
|
| 214 |
+
'num_puzzle_identifiers': batch_size,
|
| 215 |
+
'vocab_size': vocab_size,
|
| 216 |
+
'H_cycles': H_cycles,
|
| 217 |
+
'L_cycles': L_cycles,
|
| 218 |
+
'H_layers': H_layers,
|
| 219 |
+
'L_layers': L_layers,
|
| 220 |
+
'hidden_size': hidden_size,
|
| 221 |
+
'expansion': 4.0,
|
| 222 |
+
'num_heads': num_heads,
|
| 223 |
+
'pos_encodings': 'rope',
|
| 224 |
+
'halt_max_steps': halt_max_steps,
|
| 225 |
+
'halt_exploration_prob': 0.0,
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
# Memory manager
|
| 229 |
+
mem_mgr = MemoryTierManager(device=device, enable_tracking=True)
|
| 230 |
+
|
| 231 |
+
# Create model
|
| 232 |
+
model = HRM_Tiered(config_dict, memory_manager=mem_mgr).to(device)
|
| 233 |
+
model.eval()
|
| 234 |
+
|
| 235 |
+
batch = _create_dummy_batch(batch_size, seq_len, vocab_size, device)
|
| 236 |
+
|
| 237 |
+
# ---- Warmup ----
|
| 238 |
+
with torch.no_grad():
|
| 239 |
+
for _ in range(warmup):
|
| 240 |
+
carry = model.initial_carry(batch)
|
| 241 |
+
carry.inner_carry.z_H = carry.inner_carry.z_H.to(device)
|
| 242 |
+
carry.inner_carry.z_L = carry.inner_carry.z_L.to(device)
|
| 243 |
+
carry.steps = carry.steps.to(device)
|
| 244 |
+
carry.halted = carry.halted.to(device)
|
| 245 |
+
carry.current_data = {k: v.to(device) for k, v in carry.current_data.items()}
|
| 246 |
+
model(carry, batch)
|
| 247 |
+
|
| 248 |
+
model.reset_timing()
|
| 249 |
+
mem_mgr.reset_stats()
|
| 250 |
+
if torch.cuda.is_available():
|
| 251 |
+
torch.cuda.reset_peak_memory_stats(device)
|
| 252 |
+
torch.cuda.synchronize()
|
| 253 |
+
|
| 254 |
+
# ---- Benchmark iterations ----
|
| 255 |
+
total_latencies_ms = []
|
| 256 |
+
with torch.no_grad():
|
| 257 |
+
for _ in range(iterations):
|
| 258 |
+
carry = model.initial_carry(batch)
|
| 259 |
+
carry.inner_carry.z_H = carry.inner_carry.z_H.to(device)
|
| 260 |
+
carry.inner_carry.z_L = carry.inner_carry.z_L.to(device)
|
| 261 |
+
carry.steps = carry.steps.to(device)
|
| 262 |
+
carry.halted = carry.halted.to(device)
|
| 263 |
+
carry.current_data = {k: v.to(device) for k, v in carry.current_data.items()}
|
| 264 |
+
|
| 265 |
+
if torch.cuda.is_available():
|
| 266 |
+
start_event = torch.cuda.Event(enable_timing=True)
|
| 267 |
+
end_event = torch.cuda.Event(enable_timing=True)
|
| 268 |
+
start_event.record()
|
| 269 |
+
|
| 270 |
+
t0 = time.perf_counter()
|
| 271 |
+
model(carry, batch)
|
| 272 |
+
|
| 273 |
+
if torch.cuda.is_available():
|
| 274 |
+
end_event.record()
|
| 275 |
+
torch.cuda.synchronize()
|
| 276 |
+
total_latencies_ms.append(start_event.elapsed_time(end_event))
|
| 277 |
+
else:
|
| 278 |
+
total_latencies_ms.append((time.perf_counter() - t0) * 1000)
|
| 279 |
+
|
| 280 |
+
# ---- Collect results ----
|
| 281 |
+
timing = model.get_timing_stats()
|
| 282 |
+
mem_stats = mem_mgr.get_stats()
|
| 283 |
+
|
| 284 |
+
# GPU memory
|
| 285 |
+
total_gpu_mb = 0.0
|
| 286 |
+
if torch.cuda.is_available():
|
| 287 |
+
total_gpu_mb = torch.cuda.max_memory_allocated(device) / (1024 * 1024)
|
| 288 |
+
|
| 289 |
+
# GPU metrics
|
| 290 |
+
gpu_metrics = _get_gpu_metrics()
|
| 291 |
+
|
| 292 |
+
# Triton latency probe
|
| 293 |
+
sram_probe_us, dram_probe_us = _run_triton_latency_probe(
|
| 294 |
+
batch_size, hidden_size, device,
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
# Compute derived metrics
|
| 298 |
+
l_mean = timing['L_forward_us']['mean_us']
|
| 299 |
+
h_mean = timing['H_forward_us']['mean_us']
|
| 300 |
+
ratio = h_mean / l_mean if l_mean > 0 else float('inf')
|
| 301 |
+
|
| 302 |
+
total_mean_ms = sum(total_latencies_ms) / len(total_latencies_ms)
|
| 303 |
+
throughput = batch_size / (total_mean_ms / 1000) if total_mean_ms > 0 else 0
|
| 304 |
+
|
| 305 |
+
compute_time = timing['L_forward_us']['total_us'] + timing['H_forward_us']['total_us']
|
| 306 |
+
transfer_time = timing['H_L_transfer_us']['total_us'] + timing['L_H_transfer_us']['total_us']
|
| 307 |
+
efficiency = compute_time / (compute_time + transfer_time) if (compute_time + transfer_time) > 0 else 0
|
| 308 |
+
|
| 309 |
+
# Build L-level stats
|
| 310 |
+
l_values = [timing['L_forward_us']['min_us'], timing['L_forward_us']['max_us']]
|
| 311 |
+
|
| 312 |
+
result = BenchmarkResult(
|
| 313 |
+
model_name='HRM_Tiered',
|
| 314 |
+
batch_size=batch_size,
|
| 315 |
+
seq_len=seq_len,
|
| 316 |
+
hidden_size=hidden_size,
|
| 317 |
+
H_cycles=H_cycles,
|
| 318 |
+
L_cycles=L_cycles,
|
| 319 |
+
H_layers=H_layers,
|
| 320 |
+
L_layers=L_layers,
|
| 321 |
+
num_iterations=iterations,
|
| 322 |
+
warmup_iterations=warmup,
|
| 323 |
+
|
| 324 |
+
l_level_latency_mean_us=l_mean,
|
| 325 |
+
l_level_latency_min_us=timing['L_forward_us']['min_us'],
|
| 326 |
+
l_level_latency_max_us=timing['L_forward_us']['max_us'],
|
| 327 |
+
l_level_latency_std_us=0.0,
|
| 328 |
+
|
| 329 |
+
h_level_latency_mean_us=h_mean,
|
| 330 |
+
h_level_latency_min_us=timing['H_forward_us']['min_us'],
|
| 331 |
+
h_level_latency_max_us=timing['H_forward_us']['max_us'],
|
| 332 |
+
h_level_latency_std_us=0.0,
|
| 333 |
+
|
| 334 |
+
total_inference_latency_mean_ms=total_mean_ms,
|
| 335 |
+
total_inference_latency_min_ms=min(total_latencies_ms),
|
| 336 |
+
total_inference_latency_max_ms=max(total_latencies_ms),
|
| 337 |
+
total_inference_latency_std_ms=_std(total_latencies_ms),
|
| 338 |
+
|
| 339 |
+
throughput_samples_per_sec=throughput,
|
| 340 |
+
|
| 341 |
+
sram_peak_mb=mem_stats['sram']['peak_mb'],
|
| 342 |
+
dram_peak_mb=mem_stats['dram']['peak_mb'],
|
| 343 |
+
total_gpu_memory_mb=total_gpu_mb,
|
| 344 |
+
|
| 345 |
+
h_l_transfer_mean_us=timing['H_L_transfer_us']['mean_us'],
|
| 346 |
+
l_h_transfer_mean_us=timing['L_H_transfer_us']['mean_us'],
|
| 347 |
+
|
| 348 |
+
sram_hit_rate=mem_stats['sram']['hit_rate'],
|
| 349 |
+
|
| 350 |
+
triton_sram_probe_latency_us=sram_probe_us,
|
| 351 |
+
triton_dram_probe_latency_us=dram_probe_us,
|
| 352 |
+
|
| 353 |
+
gpu_utilization_pct=gpu_metrics['utilization'],
|
| 354 |
+
gpu_power_w=gpu_metrics['power'],
|
| 355 |
+
gpu_temperature_c=gpu_metrics['temperature'],
|
| 356 |
+
|
| 357 |
+
h_over_l_latency_ratio=ratio,
|
| 358 |
+
memory_efficiency=efficiency,
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
# Cleanup
|
| 362 |
+
del model, batch, carry
|
| 363 |
+
if torch.cuda.is_available():
|
| 364 |
+
torch.cuda.empty_cache()
|
| 365 |
+
|
| 366 |
+
return result
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
def benchmark_baseline_model(
|
| 370 |
+
batch_size: int = 8,
|
| 371 |
+
seq_len: int = 64,
|
| 372 |
+
hidden_size: int = 512,
|
| 373 |
+
num_heads: int = 8,
|
| 374 |
+
H_cycles: int = 2,
|
| 375 |
+
L_cycles: int = 2,
|
| 376 |
+
H_layers: int = 4,
|
| 377 |
+
L_layers: int = 4,
|
| 378 |
+
halt_max_steps: int = 1,
|
| 379 |
+
warmup: int = 5,
|
| 380 |
+
iterations: int = 20,
|
| 381 |
+
device: Optional[torch.device] = None,
|
| 382 |
+
) -> BenchmarkResult:
|
| 383 |
+
"""Benchmark the original (non-tiered) HRM model."""
|
| 384 |
+
|
| 385 |
+
if device is None:
|
| 386 |
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 387 |
+
|
| 388 |
+
vocab_size = 32
|
| 389 |
+
config_dict = {
|
| 390 |
+
'batch_size': batch_size,
|
| 391 |
+
'seq_len': seq_len,
|
| 392 |
+
'puzzle_emb_ndim': 0,
|
| 393 |
+
'num_puzzle_identifiers': batch_size,
|
| 394 |
+
'vocab_size': vocab_size,
|
| 395 |
+
'H_cycles': H_cycles,
|
| 396 |
+
'L_cycles': L_cycles,
|
| 397 |
+
'H_layers': H_layers,
|
| 398 |
+
'L_layers': L_layers,
|
| 399 |
+
'hidden_size': hidden_size,
|
| 400 |
+
'expansion': 4.0,
|
| 401 |
+
'num_heads': num_heads,
|
| 402 |
+
'pos_encodings': 'rope',
|
| 403 |
+
'halt_max_steps': halt_max_steps,
|
| 404 |
+
'halt_exploration_prob': 0.0,
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
model = HierarchicalReasoningModel_ACTV1(config_dict).to(device)
|
| 408 |
+
model.eval()
|
| 409 |
+
|
| 410 |
+
batch = _create_dummy_batch(batch_size, seq_len, vocab_size, device)
|
| 411 |
+
|
| 412 |
+
# ---- Warmup ----
|
| 413 |
+
with torch.no_grad():
|
| 414 |
+
for _ in range(warmup):
|
| 415 |
+
carry = model.initial_carry(batch)
|
| 416 |
+
carry.inner_carry.z_H = carry.inner_carry.z_H.to(device)
|
| 417 |
+
carry.inner_carry.z_L = carry.inner_carry.z_L.to(device)
|
| 418 |
+
carry.steps = carry.steps.to(device)
|
| 419 |
+
carry.halted = carry.halted.to(device)
|
| 420 |
+
carry.current_data = {k: v.to(device) for k, v in carry.current_data.items()}
|
| 421 |
+
model(carry, batch)
|
| 422 |
+
|
| 423 |
+
if torch.cuda.is_available():
|
| 424 |
+
torch.cuda.reset_peak_memory_stats(device)
|
| 425 |
+
torch.cuda.synchronize()
|
| 426 |
+
|
| 427 |
+
# ---- Benchmark ----
|
| 428 |
+
total_latencies_ms = []
|
| 429 |
+
with torch.no_grad():
|
| 430 |
+
for _ in range(iterations):
|
| 431 |
+
carry = model.initial_carry(batch)
|
| 432 |
+
carry.inner_carry.z_H = carry.inner_carry.z_H.to(device)
|
| 433 |
+
carry.inner_carry.z_L = carry.inner_carry.z_L.to(device)
|
| 434 |
+
carry.steps = carry.steps.to(device)
|
| 435 |
+
carry.halted = carry.halted.to(device)
|
| 436 |
+
carry.current_data = {k: v.to(device) for k, v in carry.current_data.items()}
|
| 437 |
+
|
| 438 |
+
if torch.cuda.is_available():
|
| 439 |
+
start_event = torch.cuda.Event(enable_timing=True)
|
| 440 |
+
end_event = torch.cuda.Event(enable_timing=True)
|
| 441 |
+
start_event.record()
|
| 442 |
+
|
| 443 |
+
t0 = time.perf_counter()
|
| 444 |
+
model(carry, batch)
|
| 445 |
+
|
| 446 |
+
if torch.cuda.is_available():
|
| 447 |
+
end_event.record()
|
| 448 |
+
torch.cuda.synchronize()
|
| 449 |
+
total_latencies_ms.append(start_event.elapsed_time(end_event))
|
| 450 |
+
else:
|
| 451 |
+
total_latencies_ms.append((time.perf_counter() - t0) * 1000)
|
| 452 |
+
|
| 453 |
+
total_gpu_mb = 0.0
|
| 454 |
+
if torch.cuda.is_available():
|
| 455 |
+
total_gpu_mb = torch.cuda.max_memory_allocated(device) / (1024 * 1024)
|
| 456 |
+
|
| 457 |
+
total_mean_ms = sum(total_latencies_ms) / len(total_latencies_ms)
|
| 458 |
+
throughput = batch_size / (total_mean_ms / 1000) if total_mean_ms > 0 else 0
|
| 459 |
+
|
| 460 |
+
gpu_metrics = _get_gpu_metrics()
|
| 461 |
+
|
| 462 |
+
result = BenchmarkResult(
|
| 463 |
+
model_name='HRM_Baseline',
|
| 464 |
+
batch_size=batch_size,
|
| 465 |
+
seq_len=seq_len,
|
| 466 |
+
hidden_size=hidden_size,
|
| 467 |
+
H_cycles=H_cycles,
|
| 468 |
+
L_cycles=L_cycles,
|
| 469 |
+
H_layers=H_layers,
|
| 470 |
+
L_layers=L_layers,
|
| 471 |
+
num_iterations=iterations,
|
| 472 |
+
warmup_iterations=warmup,
|
| 473 |
+
|
| 474 |
+
l_level_latency_mean_us=0, l_level_latency_min_us=0,
|
| 475 |
+
l_level_latency_max_us=0, l_level_latency_std_us=0,
|
| 476 |
+
h_level_latency_mean_us=0, h_level_latency_min_us=0,
|
| 477 |
+
h_level_latency_max_us=0, h_level_latency_std_us=0,
|
| 478 |
+
|
| 479 |
+
total_inference_latency_mean_ms=total_mean_ms,
|
| 480 |
+
total_inference_latency_min_ms=min(total_latencies_ms),
|
| 481 |
+
total_inference_latency_max_ms=max(total_latencies_ms),
|
| 482 |
+
total_inference_latency_std_ms=_std(total_latencies_ms),
|
| 483 |
+
|
| 484 |
+
throughput_samples_per_sec=throughput,
|
| 485 |
+
|
| 486 |
+
sram_peak_mb=0, dram_peak_mb=0,
|
| 487 |
+
total_gpu_memory_mb=total_gpu_mb,
|
| 488 |
+
|
| 489 |
+
h_l_transfer_mean_us=0, l_h_transfer_mean_us=0,
|
| 490 |
+
sram_hit_rate=0,
|
| 491 |
+
|
| 492 |
+
triton_sram_probe_latency_us=0, triton_dram_probe_latency_us=0,
|
| 493 |
+
|
| 494 |
+
gpu_utilization_pct=gpu_metrics['utilization'],
|
| 495 |
+
gpu_power_w=gpu_metrics['power'],
|
| 496 |
+
gpu_temperature_c=gpu_metrics['temperature'],
|
| 497 |
+
|
| 498 |
+
h_over_l_latency_ratio=0,
|
| 499 |
+
memory_efficiency=1.0,
|
| 500 |
+
)
|
| 501 |
+
|
| 502 |
+
del model, batch, carry
|
| 503 |
+
if torch.cuda.is_available():
|
| 504 |
+
torch.cuda.empty_cache()
|
| 505 |
+
|
| 506 |
+
return result
|
| 507 |
+
|
| 508 |
+
|
| 509 |
+
def compare_models(
|
| 510 |
+
batch_sizes: List[int] = [1, 8, 32],
|
| 511 |
+
seq_lens: List[int] = [64, 128],
|
| 512 |
+
hidden_size: int = 512,
|
| 513 |
+
warmup: int = 5,
|
| 514 |
+
iterations: int = 20,
|
| 515 |
+
device: Optional[torch.device] = None,
|
| 516 |
+
) -> List[Dict]:
|
| 517 |
+
"""Run comparative benchmark between tiered and baseline HRM."""
|
| 518 |
+
results = []
|
| 519 |
+
|
| 520 |
+
for bs in batch_sizes:
|
| 521 |
+
for sl in seq_lens:
|
| 522 |
+
print(f"\n{'='*60}")
|
| 523 |
+
print(f" Benchmarking: batch_size={bs}, seq_len={sl}")
|
| 524 |
+
print(f"{'='*60}")
|
| 525 |
+
|
| 526 |
+
print(" → Baseline model...")
|
| 527 |
+
baseline = benchmark_baseline_model(
|
| 528 |
+
batch_size=bs, seq_len=sl, hidden_size=hidden_size,
|
| 529 |
+
warmup=warmup, iterations=iterations, device=device,
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
print(" → Tiered model...")
|
| 533 |
+
tiered = benchmark_tiered_model(
|
| 534 |
+
batch_size=bs, seq_len=sl, hidden_size=hidden_size,
|
| 535 |
+
warmup=warmup, iterations=iterations, device=device,
|
| 536 |
+
)
|
| 537 |
+
|
| 538 |
+
comparison = {
|
| 539 |
+
'batch_size': bs,
|
| 540 |
+
'seq_len': sl,
|
| 541 |
+
'baseline': asdict(baseline),
|
| 542 |
+
'tiered': asdict(tiered),
|
| 543 |
+
'speedup': baseline.total_inference_latency_mean_ms / tiered.total_inference_latency_mean_ms if tiered.total_inference_latency_mean_ms > 0 else 0,
|
| 544 |
+
'memory_savings_mb': baseline.total_gpu_memory_mb - tiered.total_gpu_memory_mb,
|
| 545 |
+
'throughput_improvement': tiered.throughput_samples_per_sec / baseline.throughput_samples_per_sec if baseline.throughput_samples_per_sec > 0 else 0,
|
| 546 |
+
}
|
| 547 |
+
results.append(comparison)
|
| 548 |
+
|
| 549 |
+
# Print summary
|
| 550 |
+
print(f"\n Results:")
|
| 551 |
+
print(f" Baseline latency: {baseline.total_inference_latency_mean_ms:.2f} ms")
|
| 552 |
+
print(f" Tiered latency: {tiered.total_inference_latency_mean_ms:.2f} ms")
|
| 553 |
+
print(f" Speedup: {comparison['speedup']:.2f}x")
|
| 554 |
+
print(f" H/L ratio: {tiered.h_over_l_latency_ratio:.2f}x")
|
| 555 |
+
print(f" SRAM probe: {tiered.triton_sram_probe_latency_us:.1f} μs")
|
| 556 |
+
print(f" DRAM probe: {tiered.triton_dram_probe_latency_us:.1f} μs")
|
| 557 |
+
|
| 558 |
+
return results
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
def print_results_table(results: List[BenchmarkResult]):
|
| 562 |
+
"""Pretty-print benchmark results as a table."""
|
| 563 |
+
header = (
|
| 564 |
+
f"{'Model':<15} {'BS':>4} {'Seq':>5} "
|
| 565 |
+
f"{'Latency(ms)':>12} {'Throughput':>12} "
|
| 566 |
+
f"{'L_lat(μs)':>10} {'H_lat(μs)':>10} {'H/L':>6} "
|
| 567 |
+
f"{'GPU_MB':>8} {'Efficiency':>10}"
|
| 568 |
+
)
|
| 569 |
+
print(f"\n{'='*len(header)}")
|
| 570 |
+
print(header)
|
| 571 |
+
print(f"{'='*len(header)}")
|
| 572 |
+
|
| 573 |
+
for r in results:
|
| 574 |
+
print(
|
| 575 |
+
f"{r.model_name:<15} {r.batch_size:>4} {r.seq_len:>5} "
|
| 576 |
+
f"{r.total_inference_latency_mean_ms:>12.2f} {r.throughput_samples_per_sec:>12.1f} "
|
| 577 |
+
f"{r.l_level_latency_mean_us:>10.1f} {r.h_level_latency_mean_us:>10.1f} {r.h_over_l_latency_ratio:>6.2f} "
|
| 578 |
+
f"{r.total_gpu_memory_mb:>8.1f} {r.memory_efficiency:>10.3f}"
|
| 579 |
+
)
|
| 580 |
+
print()
|
| 581 |
+
|
| 582 |
+
|
| 583 |
+
def generate_plots(results: List[Dict], output_dir: str = "benchmark_results"):
|
| 584 |
+
"""Generate comparison plots using matplotlib."""
|
| 585 |
+
try:
|
| 586 |
+
import matplotlib
|
| 587 |
+
matplotlib.use('Agg')
|
| 588 |
+
import matplotlib.pyplot as plt
|
| 589 |
+
import numpy as np
|
| 590 |
+
except ImportError:
|
| 591 |
+
print("matplotlib not available — skipping plot generation.")
|
| 592 |
+
return
|
| 593 |
+
|
| 594 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 595 |
+
|
| 596 |
+
# ---- Plot 1: Latency comparison ----
|
| 597 |
+
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
|
| 598 |
+
fig.suptitle('HRM SRAM/DRAM Tiering — Benchmark Results', fontsize=14, fontweight='bold')
|
| 599 |
+
|
| 600 |
+
configs = [f"bs={r['batch_size']}\nseq={r['seq_len']}" for r in results]
|
| 601 |
+
baseline_lat = [r['baseline']['total_inference_latency_mean_ms'] for r in results]
|
| 602 |
+
tiered_lat = [r['tiered']['total_inference_latency_mean_ms'] for r in results]
|
| 603 |
+
|
| 604 |
+
x = np.arange(len(configs))
|
| 605 |
+
w = 0.35
|
| 606 |
+
|
| 607 |
+
ax = axes[0]
|
| 608 |
+
ax.bar(x - w/2, baseline_lat, w, label='Baseline', color='#e74c3c', alpha=0.8)
|
| 609 |
+
ax.bar(x + w/2, tiered_lat, w, label='Tiered (SRAM/DRAM)', color='#2ecc71', alpha=0.8)
|
| 610 |
+
ax.set_xlabel('Configuration')
|
| 611 |
+
ax.set_ylabel('Latency (ms)')
|
| 612 |
+
ax.set_title('Inference Latency')
|
| 613 |
+
ax.set_xticks(x)
|
| 614 |
+
ax.set_xticklabels(configs, fontsize=8)
|
| 615 |
+
ax.legend()
|
| 616 |
+
ax.grid(axis='y', alpha=0.3)
|
| 617 |
+
|
| 618 |
+
# ---- Plot 2: Throughput ----
|
| 619 |
+
ax = axes[1]
|
| 620 |
+
baseline_tp = [r['baseline']['throughput_samples_per_sec'] for r in results]
|
| 621 |
+
tiered_tp = [r['tiered']['throughput_samples_per_sec'] for r in results]
|
| 622 |
+
ax.bar(x - w/2, baseline_tp, w, label='Baseline', color='#e74c3c', alpha=0.8)
|
| 623 |
+
ax.bar(x + w/2, tiered_tp, w, label='Tiered', color='#2ecc71', alpha=0.8)
|
| 624 |
+
ax.set_xlabel('Configuration')
|
| 625 |
+
ax.set_ylabel('Samples/sec')
|
| 626 |
+
ax.set_title('Throughput')
|
| 627 |
+
ax.set_xticks(x)
|
| 628 |
+
ax.set_xticklabels(configs, fontsize=8)
|
| 629 |
+
ax.legend()
|
| 630 |
+
ax.grid(axis='y', alpha=0.3)
|
| 631 |
+
|
| 632 |
+
# ---- Plot 3: H vs L latency (tiered only) ----
|
| 633 |
+
ax = axes[2]
|
| 634 |
+
l_lat = [r['tiered']['l_level_latency_mean_us'] for r in results]
|
| 635 |
+
h_lat = [r['tiered']['h_level_latency_mean_us'] for r in results]
|
| 636 |
+
ax.bar(x - w/2, l_lat, w, label='L-level (SRAM)', color='#3498db', alpha=0.8)
|
| 637 |
+
ax.bar(x + w/2, h_lat, w, label='H-level (DRAM)', color='#e67e22', alpha=0.8)
|
| 638 |
+
ax.set_xlabel('Configuration')
|
| 639 |
+
ax.set_ylabel('Latency (μs)')
|
| 640 |
+
ax.set_title('Per-Module Latency')
|
| 641 |
+
ax.set_xticks(x)
|
| 642 |
+
ax.set_xticklabels(configs, fontsize=8)
|
| 643 |
+
ax.legend()
|
| 644 |
+
ax.grid(axis='y', alpha=0.3)
|
| 645 |
+
|
| 646 |
+
plt.tight_layout()
|
| 647 |
+
plot_path = os.path.join(output_dir, 'benchmark_comparison.png')
|
| 648 |
+
plt.savefig(plot_path, dpi=150, bbox_inches='tight')
|
| 649 |
+
plt.close()
|
| 650 |
+
print(f" Plot saved: {plot_path}")
|
| 651 |
+
|
| 652 |
+
# ---- Plot 4: Memory breakdown ----
|
| 653 |
+
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
|
| 654 |
+
fig.suptitle('Memory Analysis', fontsize=14, fontweight='bold')
|
| 655 |
+
|
| 656 |
+
ax = axes[0]
|
| 657 |
+
gpu_mem_baseline = [r['baseline']['total_gpu_memory_mb'] for r in results]
|
| 658 |
+
gpu_mem_tiered = [r['tiered']['total_gpu_memory_mb'] for r in results]
|
| 659 |
+
ax.bar(x - w/2, gpu_mem_baseline, w, label='Baseline', color='#e74c3c', alpha=0.8)
|
| 660 |
+
ax.bar(x + w/2, gpu_mem_tiered, w, label='Tiered', color='#2ecc71', alpha=0.8)
|
| 661 |
+
ax.set_xlabel('Configuration')
|
| 662 |
+
ax.set_ylabel('GPU Memory (MB)')
|
| 663 |
+
ax.set_title('Total GPU Memory')
|
| 664 |
+
ax.set_xticks(x)
|
| 665 |
+
ax.set_xticklabels(configs, fontsize=8)
|
| 666 |
+
ax.legend()
|
| 667 |
+
ax.grid(axis='y', alpha=0.3)
|
| 668 |
+
|
| 669 |
+
ax = axes[1]
|
| 670 |
+
triton_sram = [r['tiered']['triton_sram_probe_latency_us'] for r in results]
|
| 671 |
+
triton_dram = [r['tiered']['triton_dram_probe_latency_us'] for r in results]
|
| 672 |
+
ax.bar(x - w/2, triton_sram, w, label='SRAM Probe', color='#3498db', alpha=0.8)
|
| 673 |
+
ax.bar(x + w/2, triton_dram, w, label='DRAM Probe', color='#e67e22', alpha=0.8)
|
| 674 |
+
ax.set_xlabel('Configuration')
|
| 675 |
+
ax.set_ylabel('Latency (μs)')
|
| 676 |
+
ax.set_title('Triton Memory Probe Latency')
|
| 677 |
+
ax.set_xticks(x)
|
| 678 |
+
ax.set_xticklabels(configs, fontsize=8)
|
| 679 |
+
ax.legend()
|
| 680 |
+
ax.grid(axis='y', alpha=0.3)
|
| 681 |
+
|
| 682 |
+
plt.tight_layout()
|
| 683 |
+
plot_path = os.path.join(output_dir, 'memory_analysis.png')
|
| 684 |
+
plt.savefig(plot_path, dpi=150, bbox_inches='tight')
|
| 685 |
+
plt.close()
|
| 686 |
+
print(f" Plot saved: {plot_path}")
|
benchmark_results/benchmark_comparison.png
ADDED
|
benchmark_results/comparison_results.json
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"baseline": {
|
| 3 |
+
"latency_ms": 13.310442765553793,
|
| 4 |
+
"latency_std": 0.3072074317910763,
|
| 5 |
+
"throughput": 2404.1273880695444,
|
| 6 |
+
"peak_gpu_mb": 292.287488,
|
| 7 |
+
"params_m": 27.29677
|
| 8 |
+
},
|
| 9 |
+
"tiered": {
|
| 10 |
+
"latency_ms": 11.866764799753826,
|
| 11 |
+
"latency_std": 0.02685813995812233,
|
| 12 |
+
"throughput": 2696.606913508881,
|
| 13 |
+
"peak_gpu_mb": 313.03936,
|
| 14 |
+
"params_m": 27.29677
|
| 15 |
+
},
|
| 16 |
+
"speedup": 1.1216572494830197,
|
| 17 |
+
"sweep": {
|
| 18 |
+
"baseline": [
|
| 19 |
+
{
|
| 20 |
+
"latency_ms": 13.55545597076416,
|
| 21 |
+
"latency_std": 0.17929869581870853,
|
| 22 |
+
"throughput": 73.77103375620548,
|
| 23 |
+
"peak_gpu_mb": 140.93056,
|
| 24 |
+
"params_m": 27.29677,
|
| 25 |
+
"batch_size": 1
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"latency_ms": 13.13232650756836,
|
| 29 |
+
"latency_std": 0.41090034980335216,
|
| 30 |
+
"throughput": 304.5918784988128,
|
| 31 |
+
"peak_gpu_mb": 162.348544,
|
| 32 |
+
"params_m": 27.29677,
|
| 33 |
+
"batch_size": 4
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"latency_ms": 12.639206314086914,
|
| 37 |
+
"latency_std": 0.34697112347890935,
|
| 38 |
+
"throughput": 632.9511364241022,
|
| 39 |
+
"peak_gpu_mb": 189.266432,
|
| 40 |
+
"params_m": 27.29677,
|
| 41 |
+
"batch_size": 8
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"latency_ms": 12.491708850860595,
|
| 45 |
+
"latency_std": 0.013537496997456985,
|
| 46 |
+
"throughput": 1280.8495771895696,
|
| 47 |
+
"peak_gpu_mb": 223.744512,
|
| 48 |
+
"params_m": 27.29677,
|
| 49 |
+
"batch_size": 16
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"latency_ms": 12.555190467834473,
|
| 53 |
+
"latency_std": 0.01494085745352898,
|
| 54 |
+
"throughput": 2548.7466782747565,
|
| 55 |
+
"peak_gpu_mb": 268.796416,
|
| 56 |
+
"params_m": 27.29677,
|
| 57 |
+
"batch_size": 32
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"latency_ms": 13.44706563949585,
|
| 61 |
+
"latency_std": 0.013219697890277465,
|
| 62 |
+
"throughput": 4759.402661947552,
|
| 63 |
+
"peak_gpu_mb": 350.675456,
|
| 64 |
+
"params_m": 27.29677,
|
| 65 |
+
"batch_size": 64
|
| 66 |
+
}
|
| 67 |
+
],
|
| 68 |
+
"tiered": [
|
| 69 |
+
{
|
| 70 |
+
"latency_ms": 11.769158458709716,
|
| 71 |
+
"latency_std": 0.016388604477900706,
|
| 72 |
+
"throughput": 84.96784230651208,
|
| 73 |
+
"peak_gpu_mb": 158.135808,
|
| 74 |
+
"params_m": 27.29677,
|
| 75 |
+
"batch_size": 1
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"latency_ms": 12.082194995880126,
|
| 79 |
+
"latency_std": 0.019275075409013288,
|
| 80 |
+
"throughput": 331.0656715409699,
|
| 81 |
+
"peak_gpu_mb": 180.051456,
|
| 82 |
+
"params_m": 27.29677,
|
| 83 |
+
"batch_size": 4
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"latency_ms": 11.751276779174805,
|
| 87 |
+
"latency_std": 0.0175825841180981,
|
| 88 |
+
"throughput": 680.7770891906243,
|
| 89 |
+
"peak_gpu_mb": 206.969344,
|
| 90 |
+
"params_m": 27.29677,
|
| 91 |
+
"batch_size": 8
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"latency_ms": 11.727971076965332,
|
| 95 |
+
"latency_std": 0.014815964207941113,
|
| 96 |
+
"throughput": 1364.2598446908924,
|
| 97 |
+
"peak_gpu_mb": 241.291776,
|
| 98 |
+
"params_m": 27.29677,
|
| 99 |
+
"batch_size": 16
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
"latency_ms": 11.87640323638916,
|
| 103 |
+
"latency_std": 0.021528091812880033,
|
| 104 |
+
"throughput": 2694.418450019647,
|
| 105 |
+
"peak_gpu_mb": 289.014272,
|
| 106 |
+
"params_m": 27.29677,
|
| 107 |
+
"batch_size": 32
|
| 108 |
+
},
|
| 109 |
+
{
|
| 110 |
+
"latency_ms": 12.873033428192139,
|
| 111 |
+
"latency_std": 0.021238526686589362,
|
| 112 |
+
"throughput": 4971.633170767585,
|
| 113 |
+
"peak_gpu_mb": 373.481984,
|
| 114 |
+
"params_m": 27.29677,
|
| 115 |
+
"batch_size": 64
|
| 116 |
+
}
|
| 117 |
+
]
|
| 118 |
+
}
|
| 119 |
+
}
|
benchmark_results/eval_dummy.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"model_name": "Baseline",
|
| 4 |
+
"batch_size": 32,
|
| 5 |
+
"seq_len": 81,
|
| 6 |
+
"hidden_size": 512,
|
| 7 |
+
"param_count": 27296770,
|
| 8 |
+
"latency_mean_ms": 12.367569541931152,
|
| 9 |
+
"latency_std_ms": 0.03661433936324098,
|
| 10 |
+
"throughput_sps": 2587.4121743570417,
|
| 11 |
+
"gpu_memory_mb": 174.16943359375
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"model_name": "Tiered",
|
| 15 |
+
"batch_size": 32,
|
| 16 |
+
"seq_len": 81,
|
| 17 |
+
"hidden_size": 512,
|
| 18 |
+
"param_count": 27296770,
|
| 19 |
+
"latency_mean_ms": 11.73528323173523,
|
| 20 |
+
"latency_std_ms": 0.06700267067328709,
|
| 21 |
+
"throughput_sps": 2726.81957206314,
|
| 22 |
+
"gpu_memory_mb": 193.54443359375
|
| 23 |
+
}
|
| 24 |
+
]
|
benchmark_results/eval_dummy_comparison.png
ADDED
|
benchmark_results/eval_fused_vs_v1_comparison.png
ADDED
|
benchmark_results/memory_analysis.png
ADDED
|
benchmark_results/model_comparison.png
ADDED
|
Git LFS Details
|
benchmark_results/results.json
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"batch_size": 1,
|
| 4 |
+
"seq_len": 64,
|
| 5 |
+
"baseline": {
|
| 6 |
+
"model_name": "HRM_Baseline",
|
| 7 |
+
"batch_size": 1,
|
| 8 |
+
"seq_len": 64,
|
| 9 |
+
"hidden_size": 512,
|
| 10 |
+
"H_cycles": 2,
|
| 11 |
+
"L_cycles": 2,
|
| 12 |
+
"H_layers": 4,
|
| 13 |
+
"L_layers": 4,
|
| 14 |
+
"num_iterations": 20,
|
| 15 |
+
"warmup_iterations": 5,
|
| 16 |
+
"l_level_latency_mean_us": 0,
|
| 17 |
+
"l_level_latency_min_us": 0,
|
| 18 |
+
"l_level_latency_max_us": 0,
|
| 19 |
+
"l_level_latency_std_us": 0,
|
| 20 |
+
"h_level_latency_mean_us": 0,
|
| 21 |
+
"h_level_latency_min_us": 0,
|
| 22 |
+
"h_level_latency_max_us": 0,
|
| 23 |
+
"h_level_latency_std_us": 0,
|
| 24 |
+
"total_inference_latency_mean_ms": 12.233289670944213,
|
| 25 |
+
"total_inference_latency_min_ms": 12.15283203125,
|
| 26 |
+
"total_inference_latency_max_ms": 12.321791648864746,
|
| 27 |
+
"total_inference_latency_std_ms": 0.05471266632900291,
|
| 28 |
+
"throughput_samples_per_sec": 81.74416096556112,
|
| 29 |
+
"sram_peak_mb": 0,
|
| 30 |
+
"dram_peak_mb": 0,
|
| 31 |
+
"total_gpu_memory_mb": 116.29345703125,
|
| 32 |
+
"h_l_transfer_mean_us": 0,
|
| 33 |
+
"l_h_transfer_mean_us": 0,
|
| 34 |
+
"sram_hit_rate": 0,
|
| 35 |
+
"triton_sram_probe_latency_us": 0,
|
| 36 |
+
"triton_dram_probe_latency_us": 0,
|
| 37 |
+
"gpu_utilization_pct": 23.0,
|
| 38 |
+
"gpu_power_w": 62.07,
|
| 39 |
+
"gpu_temperature_c": null,
|
| 40 |
+
"h_over_l_latency_ratio": 0,
|
| 41 |
+
"memory_efficiency": 1.0
|
| 42 |
+
},
|
| 43 |
+
"tiered": {
|
| 44 |
+
"model_name": "HRM_Tiered",
|
| 45 |
+
"batch_size": 1,
|
| 46 |
+
"seq_len": 64,
|
| 47 |
+
"hidden_size": 512,
|
| 48 |
+
"H_cycles": 2,
|
| 49 |
+
"L_cycles": 2,
|
| 50 |
+
"H_layers": 4,
|
| 51 |
+
"L_layers": 4,
|
| 52 |
+
"num_iterations": 20,
|
| 53 |
+
"warmup_iterations": 5,
|
| 54 |
+
"l_level_latency_mean_us": 1864.8816029230754,
|
| 55 |
+
"l_level_latency_min_us": 1843.9680337905884,
|
| 56 |
+
"l_level_latency_max_us": 1886.2080574035645,
|
| 57 |
+
"l_level_latency_std_us": 0.0,
|
| 58 |
+
"h_level_latency_mean_us": 1861.43679022789,
|
| 59 |
+
"h_level_latency_min_us": 1850.3680229187012,
|
| 60 |
+
"h_level_latency_max_us": 1875.9679794311523,
|
| 61 |
+
"h_level_latency_std_us": 0.0,
|
| 62 |
+
"total_inference_latency_mean_ms": 11.600086498260499,
|
| 63 |
+
"total_inference_latency_min_ms": 11.553088188171387,
|
| 64 |
+
"total_inference_latency_max_ms": 11.630592346191406,
|
| 65 |
+
"total_inference_latency_std_ms": 0.022018860122281114,
|
| 66 |
+
"throughput_samples_per_sec": 86.20625373353516,
|
| 67 |
+
"sram_peak_mb": 0.0,
|
| 68 |
+
"dram_peak_mb": 0.0,
|
| 69 |
+
"total_gpu_memory_mb": 132.66845703125,
|
| 70 |
+
"h_l_transfer_mean_us": 0,
|
| 71 |
+
"l_h_transfer_mean_us": 9.9823999684304,
|
| 72 |
+
"sram_hit_rate": 0.0,
|
| 73 |
+
"triton_sram_probe_latency_us": 820.2239871025085,
|
| 74 |
+
"triton_dram_probe_latency_us": 53.247999399900436,
|
| 75 |
+
"gpu_utilization_pct": 19.0,
|
| 76 |
+
"gpu_power_w": 90.58,
|
| 77 |
+
"gpu_temperature_c": null,
|
| 78 |
+
"h_over_l_latency_ratio": 0.9981527981777578,
|
| 79 |
+
"memory_efficiency": 0.9986629635146693
|
| 80 |
+
},
|
| 81 |
+
"speedup": 1.0545860733692518,
|
| 82 |
+
"memory_savings_mb": -16.375,
|
| 83 |
+
"throughput_improvement": 1.0545860733692518
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"batch_size": 1,
|
| 87 |
+
"seq_len": 128,
|
| 88 |
+
"baseline": {
|
| 89 |
+
"model_name": "HRM_Baseline",
|
| 90 |
+
"batch_size": 1,
|
| 91 |
+
"seq_len": 128,
|
| 92 |
+
"hidden_size": 512,
|
| 93 |
+
"H_cycles": 2,
|
| 94 |
+
"L_cycles": 2,
|
| 95 |
+
"H_layers": 4,
|
| 96 |
+
"L_layers": 4,
|
| 97 |
+
"num_iterations": 20,
|
| 98 |
+
"warmup_iterations": 5,
|
| 99 |
+
"l_level_latency_mean_us": 0,
|
| 100 |
+
"l_level_latency_min_us": 0,
|
| 101 |
+
"l_level_latency_max_us": 0,
|
| 102 |
+
"l_level_latency_std_us": 0,
|
| 103 |
+
"h_level_latency_mean_us": 0,
|
| 104 |
+
"h_level_latency_min_us": 0,
|
| 105 |
+
"h_level_latency_max_us": 0,
|
| 106 |
+
"h_level_latency_std_us": 0,
|
| 107 |
+
"total_inference_latency_mean_ms": 12.75871524810791,
|
| 108 |
+
"total_inference_latency_min_ms": 12.291104316711426,
|
| 109 |
+
"total_inference_latency_max_ms": 13.331456184387207,
|
| 110 |
+
"total_inference_latency_std_ms": 0.491103465039144,
|
| 111 |
+
"throughput_samples_per_sec": 78.37779749401476,
|
| 112 |
+
"sram_peak_mb": 0,
|
| 113 |
+
"dram_peak_mb": 0,
|
| 114 |
+
"total_gpu_memory_mb": 133.57763671875,
|
| 115 |
+
"h_l_transfer_mean_us": 0,
|
| 116 |
+
"l_h_transfer_mean_us": 0,
|
| 117 |
+
"sram_hit_rate": 0,
|
| 118 |
+
"triton_sram_probe_latency_us": 0,
|
| 119 |
+
"triton_dram_probe_latency_us": 0,
|
| 120 |
+
"gpu_utilization_pct": 22.0,
|
| 121 |
+
"gpu_power_w": 91.94,
|
| 122 |
+
"gpu_temperature_c": null,
|
| 123 |
+
"h_over_l_latency_ratio": 0,
|
| 124 |
+
"memory_efficiency": 1.0
|
| 125 |
+
},
|
| 126 |
+
"tiered": {
|
| 127 |
+
"model_name": "HRM_Tiered",
|
| 128 |
+
"batch_size": 1,
|
| 129 |
+
"seq_len": 128,
|
| 130 |
+
"hidden_size": 512,
|
| 131 |
+
"H_cycles": 2,
|
| 132 |
+
"L_cycles": 2,
|
| 133 |
+
"H_layers": 4,
|
| 134 |
+
"L_layers": 4,
|
| 135 |
+
"num_iterations": 20,
|
| 136 |
+
"warmup_iterations": 5,
|
| 137 |
+
"l_level_latency_mean_us": 1880.560537179311,
|
| 138 |
+
"l_level_latency_min_us": 1853.6959886550903,
|
| 139 |
+
"l_level_latency_max_us": 1905.6639671325684,
|
| 140 |
+
"l_level_latency_std_us": 0.0,
|
| 141 |
+
"h_level_latency_mean_us": 1872.9376077651978,
|
| 142 |
+
"h_level_latency_min_us": 1849.2159843444824,
|
| 143 |
+
"h_level_latency_max_us": 1890.3039693832397,
|
| 144 |
+
"h_level_latency_std_us": 0.0,
|
| 145 |
+
"total_inference_latency_mean_ms": 11.684232091903686,
|
| 146 |
+
"total_inference_latency_min_ms": 11.551487922668457,
|
| 147 |
+
"total_inference_latency_max_ms": 11.770879745483398,
|
| 148 |
+
"total_inference_latency_std_ms": 0.06819341259194193,
|
| 149 |
+
"throughput_samples_per_sec": 85.58542762026497,
|
| 150 |
+
"sram_peak_mb": 0.0,
|
| 151 |
+
"dram_peak_mb": 0.0,
|
| 152 |
+
"total_gpu_memory_mb": 150.07763671875,
|
| 153 |
+
"h_l_transfer_mean_us": 0,
|
| 154 |
+
"l_h_transfer_mean_us": 10.091200051829219,
|
| 155 |
+
"sram_hit_rate": 0.0,
|
| 156 |
+
"triton_sram_probe_latency_us": 47.10400104522705,
|
| 157 |
+
"triton_dram_probe_latency_us": 38.94399851560593,
|
| 158 |
+
"gpu_utilization_pct": 14.0,
|
| 159 |
+
"gpu_power_w": 95.82,
|
| 160 |
+
"gpu_temperature_c": null,
|
| 161 |
+
"h_over_l_latency_ratio": 0.9959464589076472,
|
| 162 |
+
"memory_efficiency": 0.9986589251294146
|
| 163 |
+
},
|
| 164 |
+
"speedup": 1.0919601003945105,
|
| 165 |
+
"memory_savings_mb": -16.5,
|
| 166 |
+
"throughput_improvement": 1.0919601003945105
|
| 167 |
+
},
|
| 168 |
+
{
|
| 169 |
+
"batch_size": 8,
|
| 170 |
+
"seq_len": 64,
|
| 171 |
+
"baseline": {
|
| 172 |
+
"model_name": "HRM_Baseline",
|
| 173 |
+
"batch_size": 8,
|
| 174 |
+
"seq_len": 64,
|
| 175 |
+
"hidden_size": 512,
|
| 176 |
+
"H_cycles": 2,
|
| 177 |
+
"L_cycles": 2,
|
| 178 |
+
"H_layers": 4,
|
| 179 |
+
"L_layers": 4,
|
| 180 |
+
"num_iterations": 20,
|
| 181 |
+
"warmup_iterations": 5,
|
| 182 |
+
"l_level_latency_mean_us": 0,
|
| 183 |
+
"l_level_latency_min_us": 0,
|
| 184 |
+
"l_level_latency_max_us": 0,
|
| 185 |
+
"l_level_latency_std_us": 0,
|
| 186 |
+
"h_level_latency_mean_us": 0,
|
| 187 |
+
"h_level_latency_min_us": 0,
|
| 188 |
+
"h_level_latency_max_us": 0,
|
| 189 |
+
"h_level_latency_std_us": 0,
|
| 190 |
+
"total_inference_latency_mean_ms": 12.226089572906494,
|
| 191 |
+
"total_inference_latency_min_ms": 12.181280136108398,
|
| 192 |
+
"total_inference_latency_max_ms": 12.269696235656738,
|
| 193 |
+
"total_inference_latency_std_ms": 0.02198156350662233,
|
| 194 |
+
"throughput_samples_per_sec": 654.3384090468568,
|
| 195 |
+
"sram_peak_mb": 0,
|
| 196 |
+
"dram_peak_mb": 0,
|
| 197 |
+
"total_gpu_memory_mb": 158.31396484375,
|
| 198 |
+
"h_l_transfer_mean_us": 0,
|
| 199 |
+
"l_h_transfer_mean_us": 0,
|
| 200 |
+
"sram_hit_rate": 0,
|
| 201 |
+
"triton_sram_probe_latency_us": 0,
|
| 202 |
+
"triton_dram_probe_latency_us": 0,
|
| 203 |
+
"gpu_utilization_pct": 25.0,
|
| 204 |
+
"gpu_power_w": 100.16,
|
| 205 |
+
"gpu_temperature_c": null,
|
| 206 |
+
"h_over_l_latency_ratio": 0,
|
| 207 |
+
"memory_efficiency": 1.0
|
| 208 |
+
},
|
| 209 |
+
"tiered": {
|
| 210 |
+
"model_name": "HRM_Tiered",
|
| 211 |
+
"batch_size": 8,
|
| 212 |
+
"seq_len": 64,
|
| 213 |
+
"hidden_size": 512,
|
| 214 |
+
"H_cycles": 2,
|
| 215 |
+
"L_cycles": 2,
|
| 216 |
+
"H_layers": 4,
|
| 217 |
+
"L_layers": 4,
|
| 218 |
+
"num_iterations": 20,
|
| 219 |
+
"warmup_iterations": 5,
|
| 220 |
+
"l_level_latency_mean_us": 1842.8634683291118,
|
| 221 |
+
"l_level_latency_min_us": 1817.6000118255615,
|
| 222 |
+
"l_level_latency_max_us": 1919.103980064392,
|
| 223 |
+
"l_level_latency_std_us": 0.0,
|
| 224 |
+
"h_level_latency_mean_us": 1826.6352117061615,
|
| 225 |
+
"h_level_latency_min_us": 1815.551996231079,
|
| 226 |
+
"h_level_latency_max_us": 1835.8080387115479,
|
| 227 |
+
"h_level_latency_std_us": 0.0,
|
| 228 |
+
"total_inference_latency_mean_ms": 11.43253436088562,
|
| 229 |
+
"total_inference_latency_min_ms": 11.369471549987793,
|
| 230 |
+
"total_inference_latency_max_ms": 11.523072242736816,
|
| 231 |
+
"total_inference_latency_std_ms": 0.04659291036998755,
|
| 232 |
+
"throughput_samples_per_sec": 699.7573545346668,
|
| 233 |
+
"sram_peak_mb": 0.0,
|
| 234 |
+
"dram_peak_mb": 0.0,
|
| 235 |
+
"total_gpu_memory_mb": 175.56396484375,
|
| 236 |
+
"h_l_transfer_mean_us": 0,
|
| 237 |
+
"l_h_transfer_mean_us": 9.712000098079443,
|
| 238 |
+
"sram_hit_rate": 0.0,
|
| 239 |
+
"triton_sram_probe_latency_us": 46.08000069856644,
|
| 240 |
+
"triton_dram_probe_latency_us": 40.95999896526337,
|
| 241 |
+
"gpu_utilization_pct": 26.0,
|
| 242 |
+
"gpu_power_w": 104.38,
|
| 243 |
+
"gpu_temperature_c": null,
|
| 244 |
+
"h_over_l_latency_ratio": 0.9911939995003188,
|
| 245 |
+
"memory_efficiency": 0.9986813194349485
|
| 246 |
+
},
|
| 247 |
+
"speedup": 1.0694120119801154,
|
| 248 |
+
"memory_savings_mb": -17.25,
|
| 249 |
+
"throughput_improvement": 1.0694120119801154
|
| 250 |
+
},
|
| 251 |
+
{
|
| 252 |
+
"batch_size": 8,
|
| 253 |
+
"seq_len": 128,
|
| 254 |
+
"baseline": {
|
| 255 |
+
"model_name": "HRM_Baseline",
|
| 256 |
+
"batch_size": 8,
|
| 257 |
+
"seq_len": 128,
|
| 258 |
+
"hidden_size": 512,
|
| 259 |
+
"H_cycles": 2,
|
| 260 |
+
"L_cycles": 2,
|
| 261 |
+
"H_layers": 4,
|
| 262 |
+
"L_layers": 4,
|
| 263 |
+
"num_iterations": 20,
|
| 264 |
+
"warmup_iterations": 5,
|
| 265 |
+
"l_level_latency_mean_us": 0,
|
| 266 |
+
"l_level_latency_min_us": 0,
|
| 267 |
+
"l_level_latency_max_us": 0,
|
| 268 |
+
"l_level_latency_std_us": 0,
|
| 269 |
+
"h_level_latency_mean_us": 0,
|
| 270 |
+
"h_level_latency_min_us": 0,
|
| 271 |
+
"h_level_latency_max_us": 0,
|
| 272 |
+
"h_level_latency_std_us": 0,
|
| 273 |
+
"total_inference_latency_mean_ms": 12.09585280418396,
|
| 274 |
+
"total_inference_latency_min_ms": 12.034048080444336,
|
| 275 |
+
"total_inference_latency_max_ms": 12.1693115234375,
|
| 276 |
+
"total_inference_latency_std_ms": 0.03280009762608858,
|
| 277 |
+
"throughput_samples_per_sec": 661.3837097317187,
|
| 278 |
+
"sram_peak_mb": 0,
|
| 279 |
+
"dram_peak_mb": 0,
|
| 280 |
+
"total_gpu_memory_mb": 186.99365234375,
|
| 281 |
+
"h_l_transfer_mean_us": 0,
|
| 282 |
+
"l_h_transfer_mean_us": 0,
|
| 283 |
+
"sram_hit_rate": 0,
|
| 284 |
+
"triton_sram_probe_latency_us": 0,
|
| 285 |
+
"triton_dram_probe_latency_us": 0,
|
| 286 |
+
"gpu_utilization_pct": 37.0,
|
| 287 |
+
"gpu_power_w": 112.76,
|
| 288 |
+
"gpu_temperature_c": null,
|
| 289 |
+
"h_over_l_latency_ratio": 0,
|
| 290 |
+
"memory_efficiency": 1.0
|
| 291 |
+
},
|
| 292 |
+
"tiered": {
|
| 293 |
+
"model_name": "HRM_Tiered",
|
| 294 |
+
"batch_size": 8,
|
| 295 |
+
"seq_len": 128,
|
| 296 |
+
"hidden_size": 512,
|
| 297 |
+
"H_cycles": 2,
|
| 298 |
+
"L_cycles": 2,
|
| 299 |
+
"H_layers": 4,
|
| 300 |
+
"L_layers": 4,
|
| 301 |
+
"num_iterations": 20,
|
| 302 |
+
"warmup_iterations": 5,
|
| 303 |
+
"l_level_latency_mean_us": 1827.0586788654327,
|
| 304 |
+
"l_level_latency_min_us": 1786.8800163269043,
|
| 305 |
+
"l_level_latency_max_us": 1932.2880506515503,
|
| 306 |
+
"l_level_latency_std_us": 0.0,
|
| 307 |
+
"h_level_latency_mean_us": 1811.8864059448242,
|
| 308 |
+
"h_level_latency_min_us": 1794.7200536727905,
|
| 309 |
+
"h_level_latency_max_us": 1830.7520151138306,
|
| 310 |
+
"h_level_latency_std_us": 0.0,
|
| 311 |
+
"total_inference_latency_mean_ms": 11.355580854415894,
|
| 312 |
+
"total_inference_latency_min_ms": 11.258879661560059,
|
| 313 |
+
"total_inference_latency_max_ms": 11.465632438659668,
|
| 314 |
+
"total_inference_latency_std_ms": 0.05902572924729382,
|
| 315 |
+
"throughput_samples_per_sec": 704.4994089306321,
|
| 316 |
+
"sram_peak_mb": 0.0,
|
| 317 |
+
"dram_peak_mb": 0.0,
|
| 318 |
+
"total_gpu_memory_mb": 204.24365234375,
|
| 319 |
+
"h_l_transfer_mean_us": 0,
|
| 320 |
+
"l_h_transfer_mean_us": 9.579200064763427,
|
| 321 |
+
"sram_hit_rate": 0.0,
|
| 322 |
+
"triton_sram_probe_latency_us": 47.16800153255463,
|
| 323 |
+
"triton_dram_probe_latency_us": 39.93599861860275,
|
| 324 |
+
"gpu_utilization_pct": 33.0,
|
| 325 |
+
"gpu_power_w": 118.67,
|
| 326 |
+
"gpu_temperature_c": null,
|
| 327 |
+
"h_over_l_latency_ratio": 0.9916957933009408,
|
| 328 |
+
"memory_efficiency": 0.9986882554925227
|
| 329 |
+
},
|
| 330 |
+
"speedup": 1.0651901438824412,
|
| 331 |
+
"memory_savings_mb": -17.25,
|
| 332 |
+
"throughput_improvement": 1.0651901438824412
|
| 333 |
+
},
|
| 334 |
+
{
|
| 335 |
+
"batch_size": 32,
|
| 336 |
+
"seq_len": 64,
|
| 337 |
+
"baseline": {
|
| 338 |
+
"model_name": "HRM_Baseline",
|
| 339 |
+
"batch_size": 32,
|
| 340 |
+
"seq_len": 64,
|
| 341 |
+
"hidden_size": 512,
|
| 342 |
+
"H_cycles": 2,
|
| 343 |
+
"L_cycles": 2,
|
| 344 |
+
"H_layers": 4,
|
| 345 |
+
"L_layers": 4,
|
| 346 |
+
"num_iterations": 20,
|
| 347 |
+
"warmup_iterations": 5,
|
| 348 |
+
"l_level_latency_mean_us": 0,
|
| 349 |
+
"l_level_latency_min_us": 0,
|
| 350 |
+
"l_level_latency_max_us": 0,
|
| 351 |
+
"l_level_latency_std_us": 0,
|
| 352 |
+
"h_level_latency_mean_us": 0,
|
| 353 |
+
"h_level_latency_min_us": 0,
|
| 354 |
+
"h_level_latency_max_us": 0,
|
| 355 |
+
"h_level_latency_std_us": 0,
|
| 356 |
+
"total_inference_latency_mean_ms": 12.013897466659547,
|
| 357 |
+
"total_inference_latency_min_ms": 11.935744285583496,
|
| 358 |
+
"total_inference_latency_max_ms": 12.108991622924805,
|
| 359 |
+
"total_inference_latency_std_ms": 0.04052232974770088,
|
| 360 |
+
"throughput_samples_per_sec": 2663.581913263787,
|
| 361 |
+
"sram_peak_mb": 0,
|
| 362 |
+
"dram_peak_mb": 0,
|
| 363 |
+
"total_gpu_memory_mb": 228.25927734375,
|
| 364 |
+
"h_l_transfer_mean_us": 0,
|
| 365 |
+
"l_h_transfer_mean_us": 0,
|
| 366 |
+
"sram_hit_rate": 0,
|
| 367 |
+
"triton_sram_probe_latency_us": 0,
|
| 368 |
+
"triton_dram_probe_latency_us": 0,
|
| 369 |
+
"gpu_utilization_pct": 50.0,
|
| 370 |
+
"gpu_power_w": 137.35,
|
| 371 |
+
"gpu_temperature_c": null,
|
| 372 |
+
"h_over_l_latency_ratio": 0,
|
| 373 |
+
"memory_efficiency": 1.0
|
| 374 |
+
},
|
| 375 |
+
"tiered": {
|
| 376 |
+
"model_name": "HRM_Tiered",
|
| 377 |
+
"batch_size": 32,
|
| 378 |
+
"seq_len": 64,
|
| 379 |
+
"hidden_size": 512,
|
| 380 |
+
"H_cycles": 2,
|
| 381 |
+
"L_cycles": 2,
|
| 382 |
+
"H_layers": 4,
|
| 383 |
+
"L_layers": 4,
|
| 384 |
+
"num_iterations": 20,
|
| 385 |
+
"warmup_iterations": 5,
|
| 386 |
+
"l_level_latency_mean_us": 1848.9973326524098,
|
| 387 |
+
"l_level_latency_min_us": 1829.6960592269897,
|
| 388 |
+
"l_level_latency_max_us": 1885.4399919509888,
|
| 389 |
+
"l_level_latency_std_us": 0.0,
|
| 390 |
+
"h_level_latency_mean_us": 1829.4048011302948,
|
| 391 |
+
"h_level_latency_min_us": 1820.9600448608398,
|
| 392 |
+
"h_level_latency_max_us": 1836.0320329666138,
|
| 393 |
+
"h_level_latency_std_us": 0.0,
|
| 394 |
+
"total_inference_latency_mean_ms": 11.482071924209595,
|
| 395 |
+
"total_inference_latency_min_ms": 11.442208290100098,
|
| 396 |
+
"total_inference_latency_max_ms": 11.52511978149414,
|
| 397 |
+
"total_inference_latency_std_ms": 0.022921052595205035,
|
| 398 |
+
"throughput_samples_per_sec": 2786.953453281283,
|
| 399 |
+
"sram_peak_mb": 0.0,
|
| 400 |
+
"dram_peak_mb": 0.0,
|
| 401 |
+
"total_gpu_memory_mb": 247.38427734375,
|
| 402 |
+
"h_l_transfer_mean_us": 0,
|
| 403 |
+
"l_h_transfer_mean_us": 9.636800037696958,
|
| 404 |
+
"sram_hit_rate": 0.0,
|
| 405 |
+
"triton_sram_probe_latency_us": 46.08000069856644,
|
| 406 |
+
"triton_dram_probe_latency_us": 40.95999896526337,
|
| 407 |
+
"gpu_utilization_pct": 45.0,
|
| 408 |
+
"gpu_power_w": 162.51,
|
| 409 |
+
"gpu_temperature_c": null,
|
| 410 |
+
"h_over_l_latency_ratio": 0.9894036994125842,
|
| 411 |
+
"memory_efficiency": 0.9986952672353347
|
| 412 |
+
},
|
| 413 |
+
"speedup": 1.046317907252315,
|
| 414 |
+
"memory_savings_mb": -19.125,
|
| 415 |
+
"throughput_improvement": 1.046317907252315
|
| 416 |
+
},
|
| 417 |
+
{
|
| 418 |
+
"batch_size": 32,
|
| 419 |
+
"seq_len": 128,
|
| 420 |
+
"baseline": {
|
| 421 |
+
"model_name": "HRM_Baseline",
|
| 422 |
+
"batch_size": 32,
|
| 423 |
+
"seq_len": 128,
|
| 424 |
+
"hidden_size": 512,
|
| 425 |
+
"H_cycles": 2,
|
| 426 |
+
"L_cycles": 2,
|
| 427 |
+
"H_layers": 4,
|
| 428 |
+
"L_layers": 4,
|
| 429 |
+
"num_iterations": 20,
|
| 430 |
+
"warmup_iterations": 5,
|
| 431 |
+
"l_level_latency_mean_us": 0,
|
| 432 |
+
"l_level_latency_min_us": 0,
|
| 433 |
+
"l_level_latency_max_us": 0,
|
| 434 |
+
"l_level_latency_std_us": 0,
|
| 435 |
+
"h_level_latency_mean_us": 0,
|
| 436 |
+
"h_level_latency_min_us": 0,
|
| 437 |
+
"h_level_latency_max_us": 0,
|
| 438 |
+
"h_level_latency_std_us": 0,
|
| 439 |
+
"total_inference_latency_mean_ms": 12.027006340026855,
|
| 440 |
+
"total_inference_latency_min_ms": 11.971232414245605,
|
| 441 |
+
"total_inference_latency_max_ms": 12.089983940124512,
|
| 442 |
+
"total_inference_latency_std_ms": 0.02667638763107568,
|
| 443 |
+
"throughput_samples_per_sec": 2660.678733784433,
|
| 444 |
+
"sram_peak_mb": 0,
|
| 445 |
+
"dram_peak_mb": 0,
|
| 446 |
+
"total_gpu_memory_mb": 292.13427734375,
|
| 447 |
+
"h_l_transfer_mean_us": 0,
|
| 448 |
+
"l_h_transfer_mean_us": 0,
|
| 449 |
+
"sram_hit_rate": 0,
|
| 450 |
+
"triton_sram_probe_latency_us": 0,
|
| 451 |
+
"triton_dram_probe_latency_us": 0,
|
| 452 |
+
"gpu_utilization_pct": 84.0,
|
| 453 |
+
"gpu_power_w": 210.32,
|
| 454 |
+
"gpu_temperature_c": null,
|
| 455 |
+
"h_over_l_latency_ratio": 0,
|
| 456 |
+
"memory_efficiency": 1.0
|
| 457 |
+
},
|
| 458 |
+
"tiered": {
|
| 459 |
+
"model_name": "HRM_Tiered",
|
| 460 |
+
"batch_size": 32,
|
| 461 |
+
"seq_len": 128,
|
| 462 |
+
"hidden_size": 512,
|
| 463 |
+
"H_cycles": 2,
|
| 464 |
+
"L_cycles": 2,
|
| 465 |
+
"H_layers": 4,
|
| 466 |
+
"L_layers": 4,
|
| 467 |
+
"num_iterations": 20,
|
| 468 |
+
"warmup_iterations": 5,
|
| 469 |
+
"l_level_latency_mean_us": 1828.7717362244923,
|
| 470 |
+
"l_level_latency_min_us": 1806.175947189331,
|
| 471 |
+
"l_level_latency_max_us": 1862.6240491867065,
|
| 472 |
+
"l_level_latency_std_us": 0.0,
|
| 473 |
+
"h_level_latency_mean_us": 1824.127995967865,
|
| 474 |
+
"h_level_latency_min_us": 1806.3360452651978,
|
| 475 |
+
"h_level_latency_max_us": 1875.7760524749756,
|
| 476 |
+
"h_level_latency_std_us": 0.0,
|
| 477 |
+
"total_inference_latency_mean_ms": 11.574174451828004,
|
| 478 |
+
"total_inference_latency_min_ms": 11.528096199035645,
|
| 479 |
+
"total_inference_latency_max_ms": 11.638784408569336,
|
| 480 |
+
"total_inference_latency_std_ms": 0.03262226881630495,
|
| 481 |
+
"throughput_samples_per_sec": 2764.7760220985765,
|
| 482 |
+
"sram_peak_mb": 0.0,
|
| 483 |
+
"dram_peak_mb": 0.0,
|
| 484 |
+
"total_gpu_memory_mb": 312.38427734375,
|
| 485 |
+
"h_l_transfer_mean_us": 0,
|
| 486 |
+
"l_h_transfer_mean_us": 9.51200001873076,
|
| 487 |
+
"sram_hit_rate": 0.0,
|
| 488 |
+
"triton_sram_probe_latency_us": 49.15200173854828,
|
| 489 |
+
"triton_dram_probe_latency_us": 40.70400074124336,
|
| 490 |
+
"gpu_utilization_pct": 78.0,
|
| 491 |
+
"gpu_power_w": 245.59,
|
| 492 |
+
"gpu_temperature_c": null,
|
| 493 |
+
"h_over_l_latency_ratio": 0.9974607327067432,
|
| 494 |
+
"memory_efficiency": 0.9987005384933674
|
| 495 |
+
},
|
| 496 |
+
"speedup": 1.0391243358291815,
|
| 497 |
+
"memory_savings_mb": -20.25,
|
| 498 |
+
"throughput_improvement": 1.0391243358291815
|
| 499 |
+
}
|
| 500 |
+
]
|
benchmark_results/run_nsys_profiler.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
NVIDIA Nsight Systems Profiler for HRM Memory Tiering.
|
| 4 |
+
|
| 5 |
+
This script runs a few iterations of both the baseline and tiered models
|
| 6 |
+
and is designed to be executed via `nsys profile`.
|
| 7 |
+
|
| 8 |
+
Usage:
|
| 9 |
+
nsys profile -t cuda,nvtx --stats=true --force-overwrite=true -o hrm_profile python run_nsys_profiler.py
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
import torch.cuda.nvtx as nvtx
|
| 14 |
+
import argparse
|
| 15 |
+
|
| 16 |
+
from models.hrm.hrm_act_v1 import HierarchicalReasoningModel_ACTV1
|
| 17 |
+
from models.hrm.hrm_tiered import HRM_Tiered
|
| 18 |
+
from models.memory_tier import MemoryTierManager
|
| 19 |
+
from run_training_comparison import DummyLossModel, create_dummy_batch
|
| 20 |
+
|
| 21 |
+
def profile_model(model_name, model, batch, iterations, device):
|
| 22 |
+
print(f"Profiling {model_name}...")
|
| 23 |
+
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)
|
| 24 |
+
model.train()
|
| 25 |
+
|
| 26 |
+
# Warmup
|
| 27 |
+
for _ in range(2):
|
| 28 |
+
optimizer.zero_grad()
|
| 29 |
+
carry = model.initial_carry(batch)
|
| 30 |
+
carry.inner_carry.z_H = carry.inner_carry.z_H.to(device)
|
| 31 |
+
carry.inner_carry.z_L = carry.inner_carry.z_L.to(device)
|
| 32 |
+
carry.steps = carry.steps.to(device)
|
| 33 |
+
carry.halted = carry.halted.to(device)
|
| 34 |
+
carry.current_data = {k: v.to(device) for k, v in carry.current_data.items()}
|
| 35 |
+
|
| 36 |
+
_, loss, _, _, _ = model(carry, batch, return_keys=[])
|
| 37 |
+
loss.backward()
|
| 38 |
+
optimizer.step()
|
| 39 |
+
|
| 40 |
+
torch.cuda.synchronize()
|
| 41 |
+
|
| 42 |
+
# Profiling Phase
|
| 43 |
+
with torch.autograd.profiler.emit_nvtx():
|
| 44 |
+
nvtx.range_push(f"{model_name}_Training_Loop")
|
| 45 |
+
for i in range(iterations):
|
| 46 |
+
nvtx.range_push(f"Iteration_{i}")
|
| 47 |
+
optimizer.zero_grad()
|
| 48 |
+
|
| 49 |
+
nvtx.range_push("Forward_Pass")
|
| 50 |
+
carry = model.initial_carry(batch)
|
| 51 |
+
carry.inner_carry.z_H = carry.inner_carry.z_H.to(device)
|
| 52 |
+
carry.inner_carry.z_L = carry.inner_carry.z_L.to(device)
|
| 53 |
+
carry.steps = carry.steps.to(device)
|
| 54 |
+
carry.halted = carry.halted.to(device)
|
| 55 |
+
carry.current_data = {k: v.to(device) for k, v in carry.current_data.items()}
|
| 56 |
+
|
| 57 |
+
_, loss, _, _, _ = model(carry, batch, return_keys=[])
|
| 58 |
+
nvtx.range_pop() # End Forward
|
| 59 |
+
|
| 60 |
+
nvtx.range_push("Backward_Pass")
|
| 61 |
+
loss.backward()
|
| 62 |
+
optimizer.step()
|
| 63 |
+
nvtx.range_pop() # End Backward
|
| 64 |
+
|
| 65 |
+
nvtx.range_pop() # End Iteration
|
| 66 |
+
nvtx.range_pop() # End Loop
|
| 67 |
+
|
| 68 |
+
torch.cuda.synchronize()
|
| 69 |
+
print(f"Finished {model_name}.\n")
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def main():
|
| 73 |
+
parser = argparse.ArgumentParser()
|
| 74 |
+
parser.add_argument('--batch-size', type=int, default=16)
|
| 75 |
+
parser.add_argument('--seq-len', type=int, default=128)
|
| 76 |
+
parser.add_argument('--hidden-size', type=int, default=1024)
|
| 77 |
+
parser.add_argument('--iterations', type=int, default=5)
|
| 78 |
+
args = parser.parse_args()
|
| 79 |
+
|
| 80 |
+
device = torch.device('cuda')
|
| 81 |
+
vocab_size = 32
|
| 82 |
+
|
| 83 |
+
config_dict = {
|
| 84 |
+
'batch_size': args.batch_size,
|
| 85 |
+
'seq_len': args.seq_len,
|
| 86 |
+
'puzzle_emb_ndim': 0,
|
| 87 |
+
'num_puzzle_identifiers': args.batch_size,
|
| 88 |
+
'vocab_size': vocab_size,
|
| 89 |
+
'H_cycles': 2,
|
| 90 |
+
'L_cycles': 2,
|
| 91 |
+
'H_layers': 4,
|
| 92 |
+
'L_layers': 4,
|
| 93 |
+
'hidden_size': args.hidden_size,
|
| 94 |
+
'expansion': 4.0,
|
| 95 |
+
'num_heads': 8,
|
| 96 |
+
'pos_encodings': 'rope',
|
| 97 |
+
'halt_max_steps': 1,
|
| 98 |
+
'halt_exploration_prob': 0.0,
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
batch = create_dummy_batch(args.batch_size, args.seq_len, vocab_size, device)
|
| 102 |
+
|
| 103 |
+
# 1. Baseline
|
| 104 |
+
baseline = HierarchicalReasoningModel_ACTV1(config_dict).to(device)
|
| 105 |
+
baseline_wrapped = DummyLossModel(baseline)
|
| 106 |
+
profile_model("HRM_Baseline", baseline_wrapped, batch, args.iterations, device)
|
| 107 |
+
del baseline_wrapped, baseline
|
| 108 |
+
torch.cuda.empty_cache()
|
| 109 |
+
|
| 110 |
+
# 2. Tiered
|
| 111 |
+
mem_mgr = MemoryTierManager(device=device, enable_tracking=False)
|
| 112 |
+
tiered = HRM_Tiered(config_dict, memory_manager=mem_mgr).to(device)
|
| 113 |
+
tiered_wrapped = DummyLossModel(tiered)
|
| 114 |
+
profile_model("HRM_Tiered", tiered_wrapped, batch, args.iterations, device)
|
| 115 |
+
|
| 116 |
+
if __name__ == "__main__":
|
| 117 |
+
main()
|
benchmark_results/trained_model_comparison.png
ADDED
|
Git LFS Details
|
benchmark_results/trained_model_results.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"baseline": {
|
| 3 |
+
"accuracy": {},
|
| 4 |
+
"latency": {
|
| 5 |
+
"latency_ms": 117.27158050537109,
|
| 6 |
+
"latency_std": 0.10221427702082414,
|
| 7 |
+
"throughput": 3274.4506243131314
|
| 8 |
+
}
|
| 9 |
+
},
|
| 10 |
+
"tiered": {
|
| 11 |
+
"accuracy": {},
|
| 12 |
+
"latency": {
|
| 13 |
+
"latency_ms": 95.14221649169922,
|
| 14 |
+
"latency_std": 0.010479617132043349,
|
| 15 |
+
"throughput": 4036.0632131531484
|
| 16 |
+
}
|
| 17 |
+
}
|
| 18 |
+
}
|
cleanup.sh
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# cleanup.sh — Purge venv, downloaded data, checkpoints, caches, and wandb logs
|
| 3 |
+
set -euo pipefail
|
| 4 |
+
|
| 5 |
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 6 |
+
cd "$SCRIPT_DIR"
|
| 7 |
+
|
| 8 |
+
echo "=========================================="
|
| 9 |
+
echo " HRM_optimised — Cleanup"
|
| 10 |
+
echo "=========================================="
|
| 11 |
+
|
| 12 |
+
# Deactivate venv if currently active
|
| 13 |
+
if [ -n "${VIRTUAL_ENV:-}" ]; then
|
| 14 |
+
echo " Deactivating current virtual environment..."
|
| 15 |
+
deactivate 2>/dev/null || true
|
| 16 |
+
fi
|
| 17 |
+
|
| 18 |
+
# 1. Remove virtual environment
|
| 19 |
+
if [ -d "venv" ]; then
|
| 20 |
+
echo "[1/6] Removing virtual environment (venv/)..."
|
| 21 |
+
rm -rf venv
|
| 22 |
+
else
|
| 23 |
+
echo "[1/6] No venv found, skipping."
|
| 24 |
+
fi
|
| 25 |
+
|
| 26 |
+
# 2. Remove downloaded/generated datasets
|
| 27 |
+
if [ -d "data" ]; then
|
| 28 |
+
echo "[2/6] Removing generated datasets (data/)..."
|
| 29 |
+
rm -rf data
|
| 30 |
+
else
|
| 31 |
+
echo "[2/6] No data/ directory found, skipping."
|
| 32 |
+
fi
|
| 33 |
+
|
| 34 |
+
# 3. Remove raw downloaded data from HuggingFace
|
| 35 |
+
if [ -d "dataset/raw-data" ]; then
|
| 36 |
+
echo "[3/6] Removing raw dataset downloads (dataset/raw-data/)..."
|
| 37 |
+
rm -rf dataset/raw-data
|
| 38 |
+
else
|
| 39 |
+
echo "[3/6] No dataset/raw-data/ found, skipping."
|
| 40 |
+
fi
|
| 41 |
+
|
| 42 |
+
# 4. Remove training checkpoints
|
| 43 |
+
if [ -d "checkpoints" ]; then
|
| 44 |
+
echo "[4/6] Removing training checkpoints (checkpoints/)..."
|
| 45 |
+
rm -rf checkpoints
|
| 46 |
+
else
|
| 47 |
+
echo "[4/6] No checkpoints/ directory found, skipping."
|
| 48 |
+
fi
|
| 49 |
+
|
| 50 |
+
# 5. Remove wandb logs
|
| 51 |
+
if [ -d "wandb" ]; then
|
| 52 |
+
echo "[5/6] Removing W&B logs (wandb/)..."
|
| 53 |
+
rm -rf wandb
|
| 54 |
+
else
|
| 55 |
+
echo "[5/6] No wandb/ directory found, skipping."
|
| 56 |
+
fi
|
| 57 |
+
|
| 58 |
+
# 6. Remove Python caches
|
| 59 |
+
echo "[6/6] Removing __pycache__ directories..."
|
| 60 |
+
find . -type d -name "__pycache__" -not -path "./.git/*" -exec rm -rf {} + 2>/dev/null || true
|
| 61 |
+
find . -type d -name ".hydra" -exec rm -rf {} + 2>/dev/null || true
|
| 62 |
+
|
| 63 |
+
echo ""
|
| 64 |
+
echo "=========================================="
|
| 65 |
+
echo " ✅ Cleanup complete!"
|
| 66 |
+
echo " Run ./startup.sh to set up a fresh environment."
|
| 67 |
+
echo "=========================================="
|
compare_models.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Compare Baseline vs Tiered HRM — Multi-GPU Benchmark + Comparison Plots.
|
| 4 |
+
|
| 5 |
+
Usage:
|
| 6 |
+
source venv/bin/activate
|
| 7 |
+
python compare_models.py # quick compare
|
| 8 |
+
python compare_models.py --sweep # batch-size sweep + plots
|
| 9 |
+
python compare_models.py --iterations 50 --sweep # thorough
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import json
|
| 14 |
+
import os
|
| 15 |
+
import sys
|
| 16 |
+
from dataclasses import dataclass, asdict
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
import numpy as np
|
| 20 |
+
import matplotlib
|
| 21 |
+
matplotlib.use('Agg')
|
| 22 |
+
import matplotlib.pyplot as plt
|
| 23 |
+
from matplotlib.gridspec import GridSpec
|
| 24 |
+
|
| 25 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 26 |
+
|
| 27 |
+
from models.memory_tier import MemoryTierManager
|
| 28 |
+
from models.hrm.hrm_tiered import HRM_Tiered
|
| 29 |
+
from models.hrm.hrm_act_v1 import HierarchicalReasoningModel_ACTV1
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# ═══════════════════════════════════════════════════════════
|
| 33 |
+
# Helpers
|
| 34 |
+
# ═══════════════════════════════════════════════════════════
|
| 35 |
+
|
| 36 |
+
def make_config(batch_size, seq_len, hidden_size, num_heads):
|
| 37 |
+
return {
|
| 38 |
+
"batch_size": batch_size, "seq_len": seq_len,
|
| 39 |
+
"puzzle_emb_ndim": 0, "num_puzzle_identifiers": batch_size,
|
| 40 |
+
"vocab_size": 32,
|
| 41 |
+
"H_cycles": 2, "L_cycles": 2, "H_layers": 4, "L_layers": 4,
|
| 42 |
+
"hidden_size": hidden_size, "expansion": 4.0,
|
| 43 |
+
"num_heads": num_heads, "pos_encodings": "rope",
|
| 44 |
+
"halt_max_steps": 1, "halt_exploration_prob": 0.0,
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def make_batch(batch_size, seq_len, device):
|
| 49 |
+
return {
|
| 50 |
+
"inputs": torch.randint(0, 31, (batch_size, seq_len), device=device),
|
| 51 |
+
"labels": torch.randint(0, 31, (batch_size, seq_len), device=device),
|
| 52 |
+
"puzzle_identifiers": torch.arange(batch_size, device=device),
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def build_model(arch, config_dict, device):
|
| 57 |
+
"""Build using the wrapper classes (same pattern as eval_dummy.py)."""
|
| 58 |
+
if arch == "tiered":
|
| 59 |
+
mm = MemoryTierManager(device=device, enable_tracking=True)
|
| 60 |
+
model = HRM_Tiered(config_dict, memory_manager=mm).to(device)
|
| 61 |
+
else:
|
| 62 |
+
model = HierarchicalReasoningModel_ACTV1(config_dict).to(device)
|
| 63 |
+
model.eval()
|
| 64 |
+
return model
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@torch.no_grad()
|
| 68 |
+
def benchmark(model, batch, device, warmup=5, iterations=20):
|
| 69 |
+
"""Time forward pass, return dict of metrics."""
|
| 70 |
+
bs = batch["inputs"].shape[0]
|
| 71 |
+
|
| 72 |
+
# Warmup
|
| 73 |
+
for _ in range(warmup):
|
| 74 |
+
carry = model.initial_carry(batch)
|
| 75 |
+
carry.inner_carry.z_H = carry.inner_carry.z_H.to(device)
|
| 76 |
+
carry.inner_carry.z_L = carry.inner_carry.z_L.to(device)
|
| 77 |
+
carry.steps = carry.steps.to(device)
|
| 78 |
+
carry.halted = carry.halted.to(device)
|
| 79 |
+
carry.current_data = {k: v.to(device) for k, v in carry.current_data.items()}
|
| 80 |
+
model(carry, batch)
|
| 81 |
+
|
| 82 |
+
torch.cuda.reset_peak_memory_stats(device)
|
| 83 |
+
torch.cuda.synchronize()
|
| 84 |
+
|
| 85 |
+
latencies = []
|
| 86 |
+
for _ in range(iterations):
|
| 87 |
+
carry = model.initial_carry(batch)
|
| 88 |
+
carry.inner_carry.z_H = carry.inner_carry.z_H.to(device)
|
| 89 |
+
carry.inner_carry.z_L = carry.inner_carry.z_L.to(device)
|
| 90 |
+
carry.steps = carry.steps.to(device)
|
| 91 |
+
carry.halted = carry.halted.to(device)
|
| 92 |
+
carry.current_data = {k: v.to(device) for k, v in carry.current_data.items()}
|
| 93 |
+
|
| 94 |
+
start = torch.cuda.Event(enable_timing=True)
|
| 95 |
+
end = torch.cuda.Event(enable_timing=True)
|
| 96 |
+
start.record()
|
| 97 |
+
model(carry, batch)
|
| 98 |
+
end.record()
|
| 99 |
+
torch.cuda.synchronize()
|
| 100 |
+
latencies.append(start.elapsed_time(end))
|
| 101 |
+
|
| 102 |
+
lat = np.array(latencies)
|
| 103 |
+
return {
|
| 104 |
+
"latency_ms": float(np.mean(lat)),
|
| 105 |
+
"latency_std": float(np.std(lat)),
|
| 106 |
+
"throughput": float(bs / (np.mean(lat) / 1000)),
|
| 107 |
+
"peak_gpu_mb": float(torch.cuda.max_memory_allocated(device) / 1e6),
|
| 108 |
+
"params_m": sum(p.numel() for p in model.parameters()) / 1e6,
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# ═══════════════════════════════════════════════════════════
|
| 113 |
+
# Plotting
|
| 114 |
+
# ═══════════════════════════════════════════════════════════
|
| 115 |
+
|
| 116 |
+
def create_plots(base_res, tier_res, sweep_data, output_dir):
|
| 117 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 118 |
+
|
| 119 |
+
c_base, c_tier = "#4A90D9", "#E85D75"
|
| 120 |
+
bg, text, grid = "#1a1a2e", "#e0e0e0", "#333355"
|
| 121 |
+
|
| 122 |
+
plt.rcParams.update({
|
| 123 |
+
"figure.facecolor": bg, "axes.facecolor": "#16213e",
|
| 124 |
+
"axes.edgecolor": grid, "axes.labelcolor": text,
|
| 125 |
+
"text.color": text, "xtick.color": text, "ytick.color": text,
|
| 126 |
+
"grid.color": grid, "grid.alpha": 0.3,
|
| 127 |
+
"font.family": "sans-serif", "font.size": 11,
|
| 128 |
+
})
|
| 129 |
+
|
| 130 |
+
n_plots = 6 if sweep_data else 5
|
| 131 |
+
fig = plt.figure(figsize=(16, 10))
|
| 132 |
+
fig.suptitle("HRM Baseline vs Tiered (SRAM/DRAM) Comparison",
|
| 133 |
+
fontsize=18, fontweight="bold", y=0.98)
|
| 134 |
+
gs = GridSpec(2, 3, figure=fig, hspace=0.35, wspace=0.35)
|
| 135 |
+
labels = ["Baseline", "Tiered"]
|
| 136 |
+
|
| 137 |
+
def bar_plot(ax, title, ylabel, vals, fmt=".2f"):
|
| 138 |
+
bars = ax.bar(labels, vals, color=[c_base, c_tier],
|
| 139 |
+
edgecolor="white", linewidth=0.5, width=0.5)
|
| 140 |
+
ax.set_title(title, fontweight="bold")
|
| 141 |
+
ax.set_ylabel(ylabel)
|
| 142 |
+
for b, v in zip(bars, vals):
|
| 143 |
+
ax.text(b.get_x() + b.get_width()/2, b.get_height() * 1.02,
|
| 144 |
+
f"{v:{fmt}}", ha="center", fontsize=10, color=text)
|
| 145 |
+
ax.grid(axis="y")
|
| 146 |
+
|
| 147 |
+
# 1. Latency
|
| 148 |
+
bar_plot(fig.add_subplot(gs[0, 0]), "Inference Latency", "ms",
|
| 149 |
+
[base_res["latency_ms"], tier_res["latency_ms"]])
|
| 150 |
+
|
| 151 |
+
# 2. Throughput
|
| 152 |
+
bar_plot(fig.add_subplot(gs[0, 1]), "Throughput", "samples/sec",
|
| 153 |
+
[base_res["throughput"], tier_res["throughput"]], fmt=".0f")
|
| 154 |
+
|
| 155 |
+
# 3. GPU Memory
|
| 156 |
+
bar_plot(fig.add_subplot(gs[0, 2]), "Peak GPU Memory", "MB",
|
| 157 |
+
[base_res["peak_gpu_mb"], tier_res["peak_gpu_mb"]], fmt=".0f")
|
| 158 |
+
|
| 159 |
+
# 4. Parameters
|
| 160 |
+
bar_plot(fig.add_subplot(gs[1, 0]), "Model Parameters", "Millions",
|
| 161 |
+
[base_res["params_m"], tier_res["params_m"]], fmt=".1f")
|
| 162 |
+
|
| 163 |
+
# 5. Summary text
|
| 164 |
+
ax5 = fig.add_subplot(gs[1, 1])
|
| 165 |
+
speedup = base_res["latency_ms"] / tier_res["latency_ms"]
|
| 166 |
+
mem_diff = tier_res["peak_gpu_mb"] - base_res["peak_gpu_mb"]
|
| 167 |
+
tp_gain = (tier_res["throughput"] / base_res["throughput"] - 1) * 100
|
| 168 |
+
summary = (
|
| 169 |
+
f"Speedup: {speedup:.2f}x\n"
|
| 170 |
+
f"Throughput: {tp_gain:+.1f}%\n"
|
| 171 |
+
f"Memory Δ: {mem_diff:+.0f} MB\n"
|
| 172 |
+
f"Params: identical"
|
| 173 |
+
)
|
| 174 |
+
ax5.text(0.5, 0.5, summary, transform=ax5.transAxes,
|
| 175 |
+
ha="center", va="center", fontsize=14, fontfamily="monospace",
|
| 176 |
+
bbox=dict(boxstyle="round,pad=0.5", facecolor="#0f3460", alpha=0.8))
|
| 177 |
+
ax5.set_title("Summary", fontweight="bold")
|
| 178 |
+
ax5.axis("off")
|
| 179 |
+
|
| 180 |
+
# 6. Sweep plot
|
| 181 |
+
ax6 = fig.add_subplot(gs[1, 2])
|
| 182 |
+
if sweep_data:
|
| 183 |
+
bs_list = [s["batch_size"] for s in sweep_data["baseline"]]
|
| 184 |
+
ax6.plot(bs_list, [s["latency_ms"] for s in sweep_data["baseline"]],
|
| 185 |
+
"o-", color=c_base, label="Baseline", linewidth=2, markersize=6)
|
| 186 |
+
ax6.plot(bs_list, [s["latency_ms"] for s in sweep_data["tiered"]],
|
| 187 |
+
"s-", color=c_tier, label="Tiered", linewidth=2, markersize=6)
|
| 188 |
+
ax6.set_xlabel("Batch Size")
|
| 189 |
+
ax6.set_ylabel("Latency (ms)")
|
| 190 |
+
ax6.set_title("Latency vs Batch Size", fontweight="bold")
|
| 191 |
+
ax6.legend(facecolor="#16213e", edgecolor=grid)
|
| 192 |
+
ax6.grid(True)
|
| 193 |
+
else:
|
| 194 |
+
ax6.text(0.5, 0.5, "Run with --sweep\nfor batch size\ncomparison",
|
| 195 |
+
transform=ax6.transAxes, ha="center", va="center", fontsize=12)
|
| 196 |
+
ax6.set_title("Latency vs Batch Size", fontweight="bold")
|
| 197 |
+
ax6.axis("off")
|
| 198 |
+
|
| 199 |
+
path = os.path.join(output_dir, "model_comparison.png")
|
| 200 |
+
fig.savefig(path, dpi=150, bbox_inches="tight")
|
| 201 |
+
plt.close()
|
| 202 |
+
print(f" Plot saved → {path}")
|
| 203 |
+
return path
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
# ═══════════════════════════════════════════════════════════
|
| 207 |
+
# Main
|
| 208 |
+
# ═══════════════════════════════════════════════════════════
|
| 209 |
+
|
| 210 |
+
def main():
|
| 211 |
+
parser = argparse.ArgumentParser(description="Compare Baseline vs Tiered HRM")
|
| 212 |
+
parser.add_argument("--batch-size", type=int, default=32)
|
| 213 |
+
parser.add_argument("--seq-len", type=int, default=81)
|
| 214 |
+
parser.add_argument("--hidden-size", type=int, default=512)
|
| 215 |
+
parser.add_argument("--num-heads", type=int, default=8)
|
| 216 |
+
parser.add_argument("--warmup", type=int, default=5)
|
| 217 |
+
parser.add_argument("--iterations", type=int, default=20)
|
| 218 |
+
parser.add_argument("--sweep", action="store_true", help="Batch-size sweep")
|
| 219 |
+
parser.add_argument("--output-dir", type=str, default="benchmark_results")
|
| 220 |
+
args = parser.parse_args()
|
| 221 |
+
|
| 222 |
+
device = torch.device("cuda")
|
| 223 |
+
cfg = make_config(args.batch_size, args.seq_len, args.hidden_size, args.num_heads)
|
| 224 |
+
|
| 225 |
+
print("=" * 64)
|
| 226 |
+
print(" HRM Model Comparison: Baseline vs Tiered (SRAM/DRAM)")
|
| 227 |
+
print(f" Device: {torch.cuda.get_device_name(0)}")
|
| 228 |
+
print(f" Config: bs={args.batch_size}, seq={args.seq_len}, hidden={args.hidden_size}")
|
| 229 |
+
print("=" * 64)
|
| 230 |
+
|
| 231 |
+
# ── Build ──
|
| 232 |
+
print("\n Building Baseline...")
|
| 233 |
+
base_model = build_model("baseline", cfg, device)
|
| 234 |
+
batch = make_batch(args.batch_size, args.seq_len, device)
|
| 235 |
+
|
| 236 |
+
print(" Building Tiered...")
|
| 237 |
+
tier_model = build_model("tiered", cfg, device)
|
| 238 |
+
|
| 239 |
+
# ── Benchmark ──
|
| 240 |
+
print(f"\n Benchmarking Baseline ({args.iterations} iters)...")
|
| 241 |
+
base_res = benchmark(base_model, batch, device, args.warmup, args.iterations)
|
| 242 |
+
print(f" → {base_res['latency_ms']:.2f} ms | {base_res['throughput']:.0f} samp/s | {base_res['peak_gpu_mb']:.0f} MB")
|
| 243 |
+
|
| 244 |
+
batch_t = make_batch(args.batch_size, args.seq_len, device)
|
| 245 |
+
print(f" Benchmarking Tiered ({args.iterations} iters)...")
|
| 246 |
+
tier_res = benchmark(tier_model, batch_t, device, args.warmup, args.iterations)
|
| 247 |
+
print(f" → {tier_res['latency_ms']:.2f} ms | {tier_res['throughput']:.0f} samp/s | {tier_res['peak_gpu_mb']:.0f} MB")
|
| 248 |
+
|
| 249 |
+
speedup = base_res["latency_ms"] / tier_res["latency_ms"]
|
| 250 |
+
print(f"\n Speedup: {speedup:.2f}x")
|
| 251 |
+
|
| 252 |
+
# ── Sweep ──
|
| 253 |
+
sweep_data = None
|
| 254 |
+
if args.sweep:
|
| 255 |
+
print("\n Running batch-size sweep...")
|
| 256 |
+
sweep_data = {"baseline": [], "tiered": []}
|
| 257 |
+
del base_model, tier_model
|
| 258 |
+
torch.cuda.empty_cache()
|
| 259 |
+
|
| 260 |
+
for bs in [1, 4, 8, 16, 32, 64]:
|
| 261 |
+
print(f" bs={bs}...", end=" ", flush=True)
|
| 262 |
+
c = make_config(bs, args.seq_len, args.hidden_size, args.num_heads)
|
| 263 |
+
b = make_batch(bs, args.seq_len, device)
|
| 264 |
+
|
| 265 |
+
bm = build_model("baseline", c, device)
|
| 266 |
+
br = benchmark(bm, b, device, warmup=3, iterations=10)
|
| 267 |
+
br["batch_size"] = bs
|
| 268 |
+
sweep_data["baseline"].append(br)
|
| 269 |
+
del bm
|
| 270 |
+
|
| 271 |
+
tm = build_model("tiered", c, device)
|
| 272 |
+
tr = benchmark(tm, b, device, warmup=3, iterations=10)
|
| 273 |
+
tr["batch_size"] = bs
|
| 274 |
+
sweep_data["tiered"].append(tr)
|
| 275 |
+
del tm
|
| 276 |
+
torch.cuda.empty_cache()
|
| 277 |
+
print(f"base={br['latency_ms']:.2f}ms, tier={tr['latency_ms']:.2f}ms")
|
| 278 |
+
|
| 279 |
+
# ── Plots ──
|
| 280 |
+
print("\n Generating plots...")
|
| 281 |
+
create_plots(base_res, tier_res, sweep_data, args.output_dir)
|
| 282 |
+
|
| 283 |
+
# ── Save JSON ──
|
| 284 |
+
results = {"baseline": base_res, "tiered": tier_res, "speedup": speedup}
|
| 285 |
+
if sweep_data:
|
| 286 |
+
results["sweep"] = sweep_data
|
| 287 |
+
json_path = os.path.join(args.output_dir, "comparison_results.json")
|
| 288 |
+
os.makedirs(args.output_dir, exist_ok=True)
|
| 289 |
+
with open(json_path, "w") as f:
|
| 290 |
+
json.dump(results, f, indent=2)
|
| 291 |
+
print(f" Results saved → {json_path}")
|
| 292 |
+
|
| 293 |
+
print("\n" + "=" * 64)
|
| 294 |
+
print(" Done!")
|
| 295 |
+
print("=" * 64)
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
if __name__ == "__main__":
|
| 299 |
+
main()
|
config/arch/hrm_tiered.yaml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Tiered HRM config — extends hrm_v1 with SRAM/DRAM memory tiering
|
| 2 |
+
name: hrm.hrm_tiered@HRM_Tiered
|
| 3 |
+
loss:
|
| 4 |
+
name: losses@ACTLossHead
|
| 5 |
+
loss_type: stablemax_cross_entropy
|
| 6 |
+
|
| 7 |
+
halt_exploration_prob: 0.1
|
| 8 |
+
halt_max_steps: 16
|
| 9 |
+
|
| 10 |
+
H_cycles: 2
|
| 11 |
+
L_cycles: 2
|
| 12 |
+
|
| 13 |
+
H_layers: 4
|
| 14 |
+
L_layers: 4
|
| 15 |
+
|
| 16 |
+
hidden_size: 512
|
| 17 |
+
num_heads: 8
|
| 18 |
+
expansion: 4
|
| 19 |
+
|
| 20 |
+
puzzle_emb_ndim: ${.hidden_size}
|
| 21 |
+
|
| 22 |
+
pos_encodings: rope
|
| 23 |
+
|
| 24 |
+
# Memory tier config (used by MemoryTierManager)
|
| 25 |
+
memory_tier:
|
| 26 |
+
sram_capacity_mb: 48 # Simulated SRAM budget (e.g., L2 cache size)
|
| 27 |
+
enable_tracking: true # Log all memory operations for benchmarks
|
config/arch/hrm_v1.yaml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: hrm.hrm_act_v1@HierarchicalReasoningModel_ACTV1
|
| 2 |
+
loss:
|
| 3 |
+
name: losses@ACTLossHead
|
| 4 |
+
loss_type: stablemax_cross_entropy
|
| 5 |
+
|
| 6 |
+
halt_exploration_prob: 0.1
|
| 7 |
+
halt_max_steps: 16
|
| 8 |
+
|
| 9 |
+
H_cycles: 2
|
| 10 |
+
L_cycles: 2
|
| 11 |
+
|
| 12 |
+
H_layers: 4
|
| 13 |
+
L_layers: 4
|
| 14 |
+
|
| 15 |
+
hidden_size: 512
|
| 16 |
+
num_heads: 8 # min(2, hidden_size // 64)
|
| 17 |
+
expansion: 4
|
| 18 |
+
|
| 19 |
+
puzzle_emb_ndim: ${.hidden_size}
|
| 20 |
+
|
| 21 |
+
pos_encodings: rope
|
config/cfg_pretrain.yaml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ARC training config
|
| 2 |
+
|
| 3 |
+
defaults:
|
| 4 |
+
- arch: hrm_v1
|
| 5 |
+
- _self_
|
| 6 |
+
|
| 7 |
+
hydra:
|
| 8 |
+
output_subdir: null
|
| 9 |
+
|
| 10 |
+
# Data path
|
| 11 |
+
data_path: data/arc-aug-1000
|
| 12 |
+
|
| 13 |
+
# Hyperparams - Training
|
| 14 |
+
global_batch_size: 768
|
| 15 |
+
|
| 16 |
+
epochs: 100000
|
| 17 |
+
eval_interval: 10000
|
| 18 |
+
checkpoint_every_eval: True
|
| 19 |
+
|
| 20 |
+
lr: 1e-4
|
| 21 |
+
lr_min_ratio: 1.0
|
| 22 |
+
lr_warmup_steps: 2000
|
| 23 |
+
|
| 24 |
+
# Standard hyperparameter settings for LM, as used in Llama
|
| 25 |
+
beta1: 0.9
|
| 26 |
+
beta2: 0.95
|
| 27 |
+
weight_decay: 0.1
|
| 28 |
+
puzzle_emb_weight_decay: 0.1
|
| 29 |
+
|
| 30 |
+
# Hyperparams - Puzzle embeddings training
|
| 31 |
+
puzzle_emb_lr: 1e-2
|
dataset/build_arc_dataset.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Optional, Tuple, Dict
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import os
|
| 5 |
+
import json
|
| 6 |
+
import hashlib
|
| 7 |
+
import numpy as np
|
| 8 |
+
from glob import glob
|
| 9 |
+
|
| 10 |
+
from argdantic import ArgParser
|
| 11 |
+
from pydantic import BaseModel
|
| 12 |
+
|
| 13 |
+
from common import PuzzleDatasetMetadata, dihedral_transform
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
cli = ArgParser()
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class DataProcessConfig(BaseModel):
|
| 20 |
+
# ARC-1
|
| 21 |
+
dataset_dirs: List[str] = ["dataset/raw-data/ARC-AGI/data", "dataset/raw-data/ConceptARC/corpus"]
|
| 22 |
+
output_dir: str = "data/arc-aug-1000"
|
| 23 |
+
|
| 24 |
+
# ARC-2
|
| 25 |
+
# dataset_dirs: List[str] = ["dataset/raw-data/ARC-AGI-2/data"]
|
| 26 |
+
# output_dir: str = "data/arc-2-aug-1000"
|
| 27 |
+
|
| 28 |
+
seed: int = 42
|
| 29 |
+
num_aug: int = 1000
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
ARCMaxGridSize = 30
|
| 33 |
+
ARCAugmentRetriesFactor = 5
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@dataclass
|
| 37 |
+
class ARCPuzzle:
|
| 38 |
+
id: str
|
| 39 |
+
|
| 40 |
+
examples: List[Tuple[np.ndarray, np.ndarray]]
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def arc_grid_to_np(grid: List[List[int]]):
|
| 44 |
+
arr = np.array(grid)
|
| 45 |
+
|
| 46 |
+
# Shape check
|
| 47 |
+
assert arr.ndim == 2
|
| 48 |
+
assert arr.shape[0] <= ARCMaxGridSize and arr.shape[1] <= ARCMaxGridSize
|
| 49 |
+
# Element check
|
| 50 |
+
assert np.all((arr >= 0) & (arr <= 9))
|
| 51 |
+
return arr.astype(np.uint8)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def np_grid_to_seq_translational_augment(inp: np.ndarray, out: np.ndarray, do_translation: bool):
|
| 55 |
+
# PAD: 0, <eos>: 1, digits: 2 ... 11
|
| 56 |
+
# Compute random top-left pad
|
| 57 |
+
if do_translation:
|
| 58 |
+
pad_r = np.random.randint(0, ARCMaxGridSize - max(inp.shape[0], out.shape[0]) + 1)
|
| 59 |
+
pad_c = np.random.randint(0, ARCMaxGridSize - max(inp.shape[1], out.shape[1]) + 1)
|
| 60 |
+
else:
|
| 61 |
+
pad_r = pad_c = 0
|
| 62 |
+
|
| 63 |
+
# Pad grid
|
| 64 |
+
result = []
|
| 65 |
+
for grid in [inp, out]:
|
| 66 |
+
nrow, ncol = grid.shape
|
| 67 |
+
grid = np.pad(grid + 2, ((pad_r, ARCMaxGridSize - pad_r - nrow), (pad_c, ARCMaxGridSize - pad_c - ncol)), constant_values=0)
|
| 68 |
+
|
| 69 |
+
# Add <eos>
|
| 70 |
+
eos_row, eos_col = pad_r + nrow, pad_c + ncol
|
| 71 |
+
if eos_row < ARCMaxGridSize:
|
| 72 |
+
grid[eos_row, pad_c:eos_col] = 1
|
| 73 |
+
if eos_col < ARCMaxGridSize:
|
| 74 |
+
grid[pad_r:eos_row, eos_col] = 1
|
| 75 |
+
|
| 76 |
+
result.append(grid.flatten())
|
| 77 |
+
|
| 78 |
+
return result
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def puzzle_hash(puzzle: dict):
|
| 82 |
+
# Hash the puzzle for checking equivalence
|
| 83 |
+
def _grid_hash(grid: np.ndarray):
|
| 84 |
+
buffer = [x.to_bytes(1) for x in grid.shape]
|
| 85 |
+
buffer.append(grid.tobytes())
|
| 86 |
+
|
| 87 |
+
return hashlib.sha256(b"".join(buffer)).hexdigest()
|
| 88 |
+
|
| 89 |
+
hashes = []
|
| 90 |
+
for example_type, example in puzzle.items():
|
| 91 |
+
for input, label in example.examples:
|
| 92 |
+
hashes.append(f"{_grid_hash(input)}|{_grid_hash(label)}")
|
| 93 |
+
|
| 94 |
+
hashes.sort()
|
| 95 |
+
return hashlib.sha256("|".join(hashes).encode()).hexdigest()
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def convert_single_arc_puzzle(results: dict, default_name: str, puzzle: dict, aug_count: int, dest_mapping: Dict[str, Tuple[str, str]]):
|
| 99 |
+
# Remove "name"
|
| 100 |
+
name = puzzle.pop("name", default_name)
|
| 101 |
+
|
| 102 |
+
# Convert
|
| 103 |
+
dests = set(dest_mapping.values())
|
| 104 |
+
converted = {dest: ARCPuzzle(name, []) for dest in dests}
|
| 105 |
+
for example_type, examples in puzzle.items():
|
| 106 |
+
dest = dest_mapping[example_type]
|
| 107 |
+
converted[dest].examples.extend([(arc_grid_to_np(example["input"]), arc_grid_to_np(example["output"])) for example in examples])
|
| 108 |
+
|
| 109 |
+
group = [converted]
|
| 110 |
+
|
| 111 |
+
# Augment
|
| 112 |
+
if aug_count > 0:
|
| 113 |
+
hashes = {puzzle_hash(converted)}
|
| 114 |
+
|
| 115 |
+
for _trial in range(ARCAugmentRetriesFactor * aug_count):
|
| 116 |
+
# Augment plan
|
| 117 |
+
trans_id = np.random.randint(0, 8)
|
| 118 |
+
mapping = np.concatenate([np.arange(0, 1, dtype=np.uint8), np.random.permutation(np.arange(1, 10, dtype=np.uint8))]) # Permute colors, Excluding "0" (black)
|
| 119 |
+
|
| 120 |
+
aug_repr = f"t{trans_id}_{''.join(str(x) for x in mapping)}"
|
| 121 |
+
|
| 122 |
+
def _map_grid(grid: np.ndarray):
|
| 123 |
+
return dihedral_transform(mapping[grid], trans_id)
|
| 124 |
+
|
| 125 |
+
# Check duplicate
|
| 126 |
+
augmented = {dest: ARCPuzzle(f"{puzzle.id}_{aug_repr}", [(_map_grid(input), _map_grid(label)) for (input, label) in puzzle.examples]) for dest, puzzle in converted.items()}
|
| 127 |
+
h = puzzle_hash(augmented)
|
| 128 |
+
if h not in hashes:
|
| 129 |
+
hashes.add(h)
|
| 130 |
+
group.append(augmented)
|
| 131 |
+
|
| 132 |
+
if len(group) >= aug_count + 1:
|
| 133 |
+
break
|
| 134 |
+
|
| 135 |
+
if len(group) < aug_count + 1:
|
| 136 |
+
print (f"[Puzzle {name}] augmentation not full, only {len(group)}")
|
| 137 |
+
|
| 138 |
+
# Append
|
| 139 |
+
for dest in dests:
|
| 140 |
+
# Convert the examples
|
| 141 |
+
dest_split, dest_set = dest
|
| 142 |
+
|
| 143 |
+
results.setdefault(dest_split, {})
|
| 144 |
+
results[dest_split].setdefault(dest_set, [])
|
| 145 |
+
results[dest_split][dest_set].append([converted[dest] for converted in group])
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def load_puzzles_arcagi(results: dict, dataset_path: str, config: DataProcessConfig):
|
| 149 |
+
train_examples_dest = ("train", "all")
|
| 150 |
+
test_examples_map = {
|
| 151 |
+
"evaluation": [(1.0, ("test", "all"))],
|
| 152 |
+
"_default": [(1.0, ("train", "all"))]
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
total_puzzles = 0
|
| 156 |
+
for subdir in os.scandir(dataset_path):
|
| 157 |
+
if subdir.is_dir():
|
| 158 |
+
# Load all puzzles in this directory
|
| 159 |
+
puzzles = []
|
| 160 |
+
for filename in glob(os.path.join(subdir.path, "*.json")):
|
| 161 |
+
with open(filename, "r") as f:
|
| 162 |
+
puzzles.append((Path(filename).stem, json.load(f)))
|
| 163 |
+
|
| 164 |
+
# Shuffle puzzles
|
| 165 |
+
np.random.shuffle(puzzles)
|
| 166 |
+
|
| 167 |
+
# Assign by fraction
|
| 168 |
+
for idx, (default_name, puzzle) in enumerate(puzzles):
|
| 169 |
+
fraction = idx / len(puzzles)
|
| 170 |
+
test_examples_dest = None
|
| 171 |
+
for f, dest in test_examples_map.get(subdir.name, test_examples_map["_default"]):
|
| 172 |
+
if fraction < f:
|
| 173 |
+
test_examples_dest = dest
|
| 174 |
+
break
|
| 175 |
+
|
| 176 |
+
assert test_examples_dest is not None
|
| 177 |
+
|
| 178 |
+
convert_single_arc_puzzle(results, default_name, puzzle, config.num_aug, {"train": train_examples_dest, "test": test_examples_dest})
|
| 179 |
+
total_puzzles += 1
|
| 180 |
+
|
| 181 |
+
print (f"[{dataset_path}] total puzzles: {total_puzzles}")
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def convert_dataset(config: DataProcessConfig):
|
| 185 |
+
np.random.seed(config.seed)
|
| 186 |
+
|
| 187 |
+
# Read dataset
|
| 188 |
+
data = {}
|
| 189 |
+
for dataset_dir in config.dataset_dirs:
|
| 190 |
+
load_puzzles_arcagi(data, dataset_dir, config)
|
| 191 |
+
|
| 192 |
+
# Map global puzzle identifiers
|
| 193 |
+
num_identifiers = 1 # 0 is blank
|
| 194 |
+
identifier_map = {}
|
| 195 |
+
for split_name, split in data.items():
|
| 196 |
+
for subset_name, subset in split.items():
|
| 197 |
+
for group in subset:
|
| 198 |
+
for puzzle in group:
|
| 199 |
+
if puzzle.id not in identifier_map:
|
| 200 |
+
identifier_map[puzzle.id] = num_identifiers
|
| 201 |
+
num_identifiers += 1
|
| 202 |
+
|
| 203 |
+
print (f"Total puzzle IDs (including <blank>): {num_identifiers}")
|
| 204 |
+
|
| 205 |
+
# Save
|
| 206 |
+
for split_name, split in data.items():
|
| 207 |
+
os.makedirs(os.path.join(config.output_dir, split_name), exist_ok=True)
|
| 208 |
+
|
| 209 |
+
# Translational augmentations
|
| 210 |
+
enable_translational_augment = split_name == "train"
|
| 211 |
+
|
| 212 |
+
# Statistics
|
| 213 |
+
total_examples = 0
|
| 214 |
+
total_puzzles = 0
|
| 215 |
+
total_groups = 0
|
| 216 |
+
|
| 217 |
+
for subset_name, subset in split.items():
|
| 218 |
+
# Construct subset
|
| 219 |
+
results = {k: [] for k in ["inputs", "labels", "puzzle_identifiers", "puzzle_indices", "group_indices"]}
|
| 220 |
+
results["puzzle_indices"].append(0)
|
| 221 |
+
results["group_indices"].append(0)
|
| 222 |
+
|
| 223 |
+
example_id = 0
|
| 224 |
+
puzzle_id = 0
|
| 225 |
+
|
| 226 |
+
for group in subset:
|
| 227 |
+
for puzzle in group:
|
| 228 |
+
# Push puzzle
|
| 229 |
+
no_aug_id = np.random.randint(0, len(puzzle.examples))
|
| 230 |
+
for _idx_ex, (inp, out) in enumerate(puzzle.examples):
|
| 231 |
+
inp, out = np_grid_to_seq_translational_augment(inp, out, do_translation=enable_translational_augment and _idx_ex != no_aug_id)
|
| 232 |
+
|
| 233 |
+
results["inputs"].append(inp)
|
| 234 |
+
results["labels"].append(out)
|
| 235 |
+
example_id += 1
|
| 236 |
+
|
| 237 |
+
total_examples += 1
|
| 238 |
+
|
| 239 |
+
results["puzzle_indices"].append(example_id)
|
| 240 |
+
results["puzzle_identifiers"].append(identifier_map[puzzle.id])
|
| 241 |
+
|
| 242 |
+
puzzle_id += 1
|
| 243 |
+
|
| 244 |
+
total_puzzles += 1
|
| 245 |
+
|
| 246 |
+
# Push group
|
| 247 |
+
results["group_indices"].append(puzzle_id)
|
| 248 |
+
total_groups += 1
|
| 249 |
+
|
| 250 |
+
for k, v in results.items():
|
| 251 |
+
if k in {"inputs", "labels"}:
|
| 252 |
+
v = np.stack(v, 0)
|
| 253 |
+
else:
|
| 254 |
+
v = np.array(v, dtype=np.int32)
|
| 255 |
+
|
| 256 |
+
np.save(os.path.join(config.output_dir, split_name, f"{subset_name}__{k}.npy"), v)
|
| 257 |
+
|
| 258 |
+
# Metadata
|
| 259 |
+
metadata = PuzzleDatasetMetadata(
|
| 260 |
+
seq_len=ARCMaxGridSize * ARCMaxGridSize,
|
| 261 |
+
vocab_size=10 + 2, # PAD + EOS + "0" ... "9"
|
| 262 |
+
|
| 263 |
+
pad_id=0,
|
| 264 |
+
ignore_label_id=0,
|
| 265 |
+
|
| 266 |
+
blank_identifier_id=0,
|
| 267 |
+
num_puzzle_identifiers=num_identifiers,
|
| 268 |
+
|
| 269 |
+
total_groups=total_groups,
|
| 270 |
+
mean_puzzle_examples=total_examples / total_puzzles,
|
| 271 |
+
sets=list(split.keys())
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
# Save metadata as JSON.
|
| 275 |
+
with open(os.path.join(config.output_dir, split_name, "dataset.json"), "w") as f:
|
| 276 |
+
json.dump(metadata.model_dump(), f)
|
| 277 |
+
|
| 278 |
+
# Save IDs mapping
|
| 279 |
+
with open(os.path.join(config.output_dir, "identifiers.json"), "w") as f:
|
| 280 |
+
ids_mapping = {v: k for k, v in identifier_map.items()}
|
| 281 |
+
|
| 282 |
+
json.dump([ids_mapping.get(i, "<blank>") for i in range(num_identifiers)], f)
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
@cli.command(singleton=True)
|
| 286 |
+
def main(config: DataProcessConfig):
|
| 287 |
+
convert_dataset(config)
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
if __name__ == "__main__":
|
| 291 |
+
cli()
|
dataset/build_maze_dataset.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
import math
|
| 3 |
+
import os
|
| 4 |
+
import csv
|
| 5 |
+
import json
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
from argdantic import ArgParser
|
| 9 |
+
from pydantic import BaseModel
|
| 10 |
+
from tqdm import tqdm
|
| 11 |
+
from huggingface_hub import hf_hub_download
|
| 12 |
+
|
| 13 |
+
from common import PuzzleDatasetMetadata, dihedral_transform
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
CHARSET = "# SGo"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
cli = ArgParser()
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class DataProcessConfig(BaseModel):
|
| 23 |
+
source_repo: str = "sapientinc/maze-30x30-hard-1k"
|
| 24 |
+
output_dir: str = "data/maze-30x30-hard-1k"
|
| 25 |
+
|
| 26 |
+
subsample_size: Optional[int] = None
|
| 27 |
+
aug: bool = False
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def convert_subset(set_name: str, config: DataProcessConfig):
|
| 31 |
+
# Read CSV
|
| 32 |
+
all_chars = set()
|
| 33 |
+
grid_size = None
|
| 34 |
+
inputs = []
|
| 35 |
+
labels = []
|
| 36 |
+
|
| 37 |
+
with open(hf_hub_download(config.source_repo, f"{set_name}.csv", repo_type="dataset"), newline="") as csvfile: # type: ignore
|
| 38 |
+
reader = csv.reader(csvfile)
|
| 39 |
+
next(reader) # Skip header
|
| 40 |
+
for source, q, a, rating in reader:
|
| 41 |
+
all_chars.update(q)
|
| 42 |
+
all_chars.update(a)
|
| 43 |
+
|
| 44 |
+
if grid_size is None:
|
| 45 |
+
n = int(len(q) ** 0.5)
|
| 46 |
+
grid_size = (n, n)
|
| 47 |
+
|
| 48 |
+
inputs.append(np.frombuffer(q.encode(), dtype=np.uint8).reshape(grid_size))
|
| 49 |
+
labels.append(np.frombuffer(a.encode(), dtype=np.uint8).reshape(grid_size))
|
| 50 |
+
|
| 51 |
+
# If subsample_size is specified for the training set,
|
| 52 |
+
# randomly sample the desired number of examples.
|
| 53 |
+
if set_name == "train" and config.subsample_size is not None:
|
| 54 |
+
total_samples = len(inputs)
|
| 55 |
+
if config.subsample_size < total_samples:
|
| 56 |
+
indices = np.random.choice(total_samples, size=config.subsample_size, replace=False)
|
| 57 |
+
inputs = [inputs[i] for i in indices]
|
| 58 |
+
labels = [labels[i] for i in indices]
|
| 59 |
+
|
| 60 |
+
# Generate dataset
|
| 61 |
+
results = {k: [] for k in ["inputs", "labels", "puzzle_identifiers", "puzzle_indices", "group_indices"]}
|
| 62 |
+
puzzle_id = 0
|
| 63 |
+
example_id = 0
|
| 64 |
+
|
| 65 |
+
results["puzzle_indices"].append(0)
|
| 66 |
+
results["group_indices"].append(0)
|
| 67 |
+
|
| 68 |
+
for inp, out in zip(tqdm(inputs), labels):
|
| 69 |
+
# Dihedral transformations for augmentation
|
| 70 |
+
for aug_idx in range(8 if (set_name == "train" and config.aug) else 1):
|
| 71 |
+
results["inputs"].append(dihedral_transform(inp, aug_idx))
|
| 72 |
+
results["labels"].append(dihedral_transform(out, aug_idx))
|
| 73 |
+
example_id += 1
|
| 74 |
+
puzzle_id += 1
|
| 75 |
+
|
| 76 |
+
results["puzzle_indices"].append(example_id)
|
| 77 |
+
results["puzzle_identifiers"].append(0)
|
| 78 |
+
|
| 79 |
+
# Push group
|
| 80 |
+
results["group_indices"].append(puzzle_id)
|
| 81 |
+
|
| 82 |
+
# Char mappings
|
| 83 |
+
assert len(all_chars - set(CHARSET)) == 0
|
| 84 |
+
|
| 85 |
+
char2id = np.zeros(256, np.uint8)
|
| 86 |
+
char2id[np.array(list(map(ord, CHARSET)))] = np.arange(len(CHARSET)) + 1
|
| 87 |
+
|
| 88 |
+
# To Numpy
|
| 89 |
+
def _seq_to_numpy(seq):
|
| 90 |
+
arr = np.vstack([char2id[s.reshape(-1)] for s in seq])
|
| 91 |
+
|
| 92 |
+
return arr
|
| 93 |
+
|
| 94 |
+
results = {
|
| 95 |
+
"inputs": _seq_to_numpy(results["inputs"]),
|
| 96 |
+
"labels": _seq_to_numpy(results["labels"]),
|
| 97 |
+
|
| 98 |
+
"group_indices": np.array(results["group_indices"], dtype=np.int32),
|
| 99 |
+
"puzzle_indices": np.array(results["puzzle_indices"], dtype=np.int32),
|
| 100 |
+
"puzzle_identifiers": np.array(results["puzzle_identifiers"], dtype=np.int32),
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
# Metadata
|
| 104 |
+
metadata = PuzzleDatasetMetadata(
|
| 105 |
+
seq_len=int(math.prod(grid_size)), # type: ignore
|
| 106 |
+
vocab_size=len(CHARSET) + 1, # PAD + Charset
|
| 107 |
+
|
| 108 |
+
pad_id=0,
|
| 109 |
+
ignore_label_id=0,
|
| 110 |
+
|
| 111 |
+
blank_identifier_id=0,
|
| 112 |
+
num_puzzle_identifiers=1,
|
| 113 |
+
|
| 114 |
+
total_groups=len(results["group_indices"]) - 1,
|
| 115 |
+
mean_puzzle_examples=1,
|
| 116 |
+
sets=["all"]
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
# Save metadata as JSON.
|
| 120 |
+
save_dir = os.path.join(config.output_dir, set_name)
|
| 121 |
+
os.makedirs(save_dir, exist_ok=True)
|
| 122 |
+
|
| 123 |
+
with open(os.path.join(save_dir, "dataset.json"), "w") as f:
|
| 124 |
+
json.dump(metadata.model_dump(), f)
|
| 125 |
+
|
| 126 |
+
# Save data
|
| 127 |
+
for k, v in results.items():
|
| 128 |
+
np.save(os.path.join(save_dir, f"all__{k}.npy"), v)
|
| 129 |
+
|
| 130 |
+
# Save IDs mapping (for visualization only)
|
| 131 |
+
with open(os.path.join(config.output_dir, "identifiers.json"), "w") as f:
|
| 132 |
+
json.dump(["<blank>"], f)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
@cli.command(singleton=True)
|
| 136 |
+
def preprocess_data(config: DataProcessConfig):
|
| 137 |
+
convert_subset("train", config)
|
| 138 |
+
convert_subset("test", config)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
if __name__ == "__main__":
|
| 142 |
+
cli()
|
dataset/build_sudoku_dataset.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
import os
|
| 3 |
+
import csv
|
| 4 |
+
import json
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
from argdantic import ArgParser
|
| 8 |
+
from pydantic import BaseModel
|
| 9 |
+
from tqdm import tqdm
|
| 10 |
+
from huggingface_hub import hf_hub_download
|
| 11 |
+
|
| 12 |
+
from common import PuzzleDatasetMetadata
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
cli = ArgParser()
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class DataProcessConfig(BaseModel):
|
| 19 |
+
source_repo: str = "sapientinc/sudoku-extreme"
|
| 20 |
+
output_dir: str = "data/sudoku-extreme-full"
|
| 21 |
+
|
| 22 |
+
subsample_size: Optional[int] = None
|
| 23 |
+
min_difficulty: Optional[int] = None
|
| 24 |
+
num_aug: int = 0
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def shuffle_sudoku(board: np.ndarray, solution: np.ndarray):
|
| 28 |
+
# Create a random digit mapping: a permutation of 1..9, with zero (blank) unchanged
|
| 29 |
+
digit_map = np.pad(np.random.permutation(np.arange(1, 10)), (1, 0))
|
| 30 |
+
|
| 31 |
+
# Randomly decide whether to transpose.
|
| 32 |
+
transpose_flag = np.random.rand() < 0.5
|
| 33 |
+
|
| 34 |
+
# Generate a valid row permutation:
|
| 35 |
+
# - Shuffle the 3 bands (each band = 3 rows) and for each band, shuffle its 3 rows.
|
| 36 |
+
bands = np.random.permutation(3)
|
| 37 |
+
row_perm = np.concatenate([b * 3 + np.random.permutation(3) for b in bands])
|
| 38 |
+
|
| 39 |
+
# Similarly for columns (stacks).
|
| 40 |
+
stacks = np.random.permutation(3)
|
| 41 |
+
col_perm = np.concatenate([s * 3 + np.random.permutation(3) for s in stacks])
|
| 42 |
+
|
| 43 |
+
# Build an 81->81 mapping. For each new cell at (i, j)
|
| 44 |
+
# (row index = i // 9, col index = i % 9),
|
| 45 |
+
# its value comes from old row = row_perm[i//9] and old col = col_perm[i%9].
|
| 46 |
+
mapping = np.array([row_perm[i // 9] * 9 + col_perm[i % 9] for i in range(81)])
|
| 47 |
+
|
| 48 |
+
def apply_transformation(x: np.ndarray) -> np.ndarray:
|
| 49 |
+
# Apply transpose flag
|
| 50 |
+
if transpose_flag:
|
| 51 |
+
x = x.T
|
| 52 |
+
# Apply the position mapping.
|
| 53 |
+
new_board = x.flatten()[mapping].reshape(9, 9).copy()
|
| 54 |
+
# Apply digit mapping
|
| 55 |
+
return digit_map[new_board]
|
| 56 |
+
|
| 57 |
+
return apply_transformation(board), apply_transformation(solution)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def convert_subset(set_name: str, config: DataProcessConfig):
|
| 61 |
+
# Read CSV
|
| 62 |
+
inputs = []
|
| 63 |
+
labels = []
|
| 64 |
+
|
| 65 |
+
with open(hf_hub_download(config.source_repo, f"{set_name}.csv", repo_type="dataset"), newline="") as csvfile:
|
| 66 |
+
reader = csv.reader(csvfile)
|
| 67 |
+
next(reader) # Skip header
|
| 68 |
+
for source, q, a, rating in reader:
|
| 69 |
+
if (config.min_difficulty is None) or (int(rating) >= config.min_difficulty):
|
| 70 |
+
assert len(q) == 81 and len(a) == 81
|
| 71 |
+
|
| 72 |
+
inputs.append(np.frombuffer(q.replace('.', '0').encode(), dtype=np.uint8).reshape(9, 9) - ord('0'))
|
| 73 |
+
labels.append(np.frombuffer(a.encode(), dtype=np.uint8).reshape(9, 9) - ord('0'))
|
| 74 |
+
|
| 75 |
+
# If subsample_size is specified for the training set,
|
| 76 |
+
# randomly sample the desired number of examples.
|
| 77 |
+
if set_name == "train" and config.subsample_size is not None:
|
| 78 |
+
total_samples = len(inputs)
|
| 79 |
+
if config.subsample_size < total_samples:
|
| 80 |
+
indices = np.random.choice(total_samples, size=config.subsample_size, replace=False)
|
| 81 |
+
inputs = [inputs[i] for i in indices]
|
| 82 |
+
labels = [labels[i] for i in indices]
|
| 83 |
+
|
| 84 |
+
# Generate dataset
|
| 85 |
+
num_augments = config.num_aug if set_name == "train" else 0
|
| 86 |
+
|
| 87 |
+
results = {k: [] for k in ["inputs", "labels", "puzzle_identifiers", "puzzle_indices", "group_indices"]}
|
| 88 |
+
puzzle_id = 0
|
| 89 |
+
example_id = 0
|
| 90 |
+
|
| 91 |
+
results["puzzle_indices"].append(0)
|
| 92 |
+
results["group_indices"].append(0)
|
| 93 |
+
|
| 94 |
+
for orig_inp, orig_out in zip(tqdm(inputs), labels):
|
| 95 |
+
for aug_idx in range(1 + num_augments):
|
| 96 |
+
# First index is not augmented
|
| 97 |
+
if aug_idx == 0:
|
| 98 |
+
inp, out = orig_inp, orig_out
|
| 99 |
+
else:
|
| 100 |
+
inp, out = shuffle_sudoku(orig_inp, orig_out)
|
| 101 |
+
|
| 102 |
+
# Push puzzle (only single example)
|
| 103 |
+
results["inputs"].append(inp)
|
| 104 |
+
results["labels"].append(out)
|
| 105 |
+
example_id += 1
|
| 106 |
+
puzzle_id += 1
|
| 107 |
+
|
| 108 |
+
results["puzzle_indices"].append(example_id)
|
| 109 |
+
results["puzzle_identifiers"].append(0)
|
| 110 |
+
|
| 111 |
+
# Push group
|
| 112 |
+
results["group_indices"].append(puzzle_id)
|
| 113 |
+
|
| 114 |
+
# To Numpy
|
| 115 |
+
def _seq_to_numpy(seq):
|
| 116 |
+
arr = np.concatenate(seq).reshape(len(seq), -1)
|
| 117 |
+
|
| 118 |
+
assert np.all((arr >= 0) & (arr <= 9))
|
| 119 |
+
return arr + 1
|
| 120 |
+
|
| 121 |
+
results = {
|
| 122 |
+
"inputs": _seq_to_numpy(results["inputs"]),
|
| 123 |
+
"labels": _seq_to_numpy(results["labels"]),
|
| 124 |
+
|
| 125 |
+
"group_indices": np.array(results["group_indices"], dtype=np.int32),
|
| 126 |
+
"puzzle_indices": np.array(results["puzzle_indices"], dtype=np.int32),
|
| 127 |
+
"puzzle_identifiers": np.array(results["puzzle_identifiers"], dtype=np.int32),
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
# Metadata
|
| 131 |
+
metadata = PuzzleDatasetMetadata(
|
| 132 |
+
seq_len=81,
|
| 133 |
+
vocab_size=10 + 1, # PAD + "0" ... "9"
|
| 134 |
+
|
| 135 |
+
pad_id=0,
|
| 136 |
+
ignore_label_id=0,
|
| 137 |
+
|
| 138 |
+
blank_identifier_id=0,
|
| 139 |
+
num_puzzle_identifiers=1,
|
| 140 |
+
|
| 141 |
+
total_groups=len(results["group_indices"]) - 1,
|
| 142 |
+
mean_puzzle_examples=1,
|
| 143 |
+
sets=["all"]
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
# Save metadata as JSON.
|
| 147 |
+
save_dir = os.path.join(config.output_dir, set_name)
|
| 148 |
+
os.makedirs(save_dir, exist_ok=True)
|
| 149 |
+
|
| 150 |
+
with open(os.path.join(save_dir, "dataset.json"), "w") as f:
|
| 151 |
+
json.dump(metadata.model_dump(), f)
|
| 152 |
+
|
| 153 |
+
# Save data
|
| 154 |
+
for k, v in results.items():
|
| 155 |
+
np.save(os.path.join(save_dir, f"all__{k}.npy"), v)
|
| 156 |
+
|
| 157 |
+
# Save IDs mapping (for visualization only)
|
| 158 |
+
with open(os.path.join(config.output_dir, "identifiers.json"), "w") as f:
|
| 159 |
+
json.dump(["<blank>"], f)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
@cli.command(singleton=True)
|
| 163 |
+
def preprocess_data(config: DataProcessConfig):
|
| 164 |
+
convert_subset("train", config)
|
| 165 |
+
convert_subset("test", config)
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
if __name__ == "__main__":
|
| 169 |
+
cli()
|
dataset/common.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Optional
|
| 2 |
+
|
| 3 |
+
import pydantic
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
# Global list mapping each dihedral transform id to its inverse.
|
| 8 |
+
# Index corresponds to the original tid, and the value is its inverse.
|
| 9 |
+
DIHEDRAL_INVERSE = [0, 3, 2, 1, 4, 5, 6, 7]
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class PuzzleDatasetMetadata(pydantic.BaseModel):
|
| 13 |
+
pad_id: int
|
| 14 |
+
ignore_label_id: Optional[int]
|
| 15 |
+
blank_identifier_id: int
|
| 16 |
+
|
| 17 |
+
vocab_size: int
|
| 18 |
+
seq_len: int
|
| 19 |
+
num_puzzle_identifiers: int
|
| 20 |
+
|
| 21 |
+
total_groups: int
|
| 22 |
+
mean_puzzle_examples: float
|
| 23 |
+
|
| 24 |
+
sets: List[str]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def dihedral_transform(arr: np.ndarray, tid: int) -> np.ndarray:
|
| 28 |
+
"""8 dihedral symmetries by rotate, flip and mirror"""
|
| 29 |
+
|
| 30 |
+
if tid == 0:
|
| 31 |
+
return arr # identity
|
| 32 |
+
elif tid == 1:
|
| 33 |
+
return np.rot90(arr, k=1)
|
| 34 |
+
elif tid == 2:
|
| 35 |
+
return np.rot90(arr, k=2)
|
| 36 |
+
elif tid == 3:
|
| 37 |
+
return np.rot90(arr, k=3)
|
| 38 |
+
elif tid == 4:
|
| 39 |
+
return np.fliplr(arr) # horizontal flip
|
| 40 |
+
elif tid == 5:
|
| 41 |
+
return np.flipud(arr) # vertical flip
|
| 42 |
+
elif tid == 6:
|
| 43 |
+
return arr.T # transpose (reflection along main diagonal)
|
| 44 |
+
elif tid == 7:
|
| 45 |
+
return np.fliplr(np.rot90(arr, k=1)) # anti-diagonal reflection
|
| 46 |
+
else:
|
| 47 |
+
return arr
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def inverse_dihedral_transform(arr: np.ndarray, tid: int) -> np.ndarray:
|
| 51 |
+
return dihedral_transform(arr, DIHEDRAL_INVERSE[tid])
|
docs/END_TO_END_EXPLANATION.md
ADDED
|
@@ -0,0 +1,1193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# End-to-End Explanation: PyTorch & Triton Implementation
|
| 2 |
+
## For someone with basic Python knowledge
|
| 3 |
+
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
## PART 1: What Are the Available Benchmarks?
|
| 7 |
+
|
| 8 |
+
Before diving into code, let's understand what benchmarks are available to measure performance.
|
| 9 |
+
|
| 10 |
+
### Overview of Benchmarking Suite
|
| 11 |
+
|
| 12 |
+
The benchmarking system measures how fast the model runs and how efficiently it uses memory. It compares two setups:
|
| 13 |
+
- **Tiered Model**: Uses the SRAM/DRAM memory hierarchy (what we want to study)
|
| 14 |
+
- **Baseline Model**: Standard model without memory tiering
|
| 15 |
+
|
| 16 |
+
### Files Involved: `benchmark.py` and `run_benchmark.py`
|
| 17 |
+
|
| 18 |
+
#### A. `run_benchmark.py` - The Command-Line Interface
|
| 19 |
+
|
| 20 |
+
This file lets you **run benchmarks from the terminal**. Think of it as a control panel.
|
| 21 |
+
|
| 22 |
+
**Key command-line options:**
|
| 23 |
+
|
| 24 |
+
```bash
|
| 25 |
+
# See available benchmarks and options
|
| 26 |
+
python run_benchmark.py --help
|
| 27 |
+
|
| 28 |
+
# Compare tiered vs baseline models
|
| 29 |
+
python run_benchmark.py --mode compare --batch-sizes 1,8,32 --seq-lens 64,128
|
| 30 |
+
|
| 31 |
+
# Benchmark only the tiered (memory-aware) model
|
| 32 |
+
python run_benchmark.py --mode tiered --warmup 5 --iterations 50 --output results.json
|
| 33 |
+
|
| 34 |
+
# Quick test (for learning)
|
| 35 |
+
python run_benchmark.py --mode tiered --warmup 1 --iterations 3 --batch-sizes 2 --seq-lens 16
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
**What each option means:**
|
| 39 |
+
|
| 40 |
+
| Option | Meaning |
|
| 41 |
+
|--------|---------|
|
| 42 |
+
| `--mode` | What to benchmark: `tiered` (new model), `baseline` (standard), or `compare` (both) |
|
| 43 |
+
| `--batch-sizes` | How many samples to process at once (comma-separated: `1,8,32` means test with 1, 8, and 32 samples) |
|
| 44 |
+
| `--seq-lens` | Length of input sequences: `64,128` means test with sequences of 64 and 128 tokens |
|
| 45 |
+
| `--hidden-size` | Internal dimension of the model (default: 512) |
|
| 46 |
+
| `--H-cycles` | Number of times H-level (slow) processes data (default: 2) |
|
| 47 |
+
| `--L-cycles` | Number of times L-level (fast) processes data (default: 2) |
|
| 48 |
+
| `--H-layers` | Number of H-level transformer layers (default: 4) |
|
| 49 |
+
| `--L-layers` | Number of L-level transformer layers (default: 4) |
|
| 50 |
+
| `--warmup` | How many runs to discard before measuring (to let GPU settle) |
|
| 51 |
+
| `--iterations` | How many actual measurements to take |
|
| 52 |
+
| `--output` | Save results to JSON file |
|
| 53 |
+
|
| 54 |
+
---
|
| 55 |
+
|
| 56 |
+
### B. What Does `benchmark.py` Actually Measure?
|
| 57 |
+
|
| 58 |
+
Inside `benchmark.py`, the `BenchmarkResult` class records these metrics:
|
| 59 |
+
|
| 60 |
+
#### **Latency Metrics** (How fast things run, in microseconds `μs`)
|
| 61 |
+
|
| 62 |
+
- **L-level latency**: Time for the "fast" tier to process data
|
| 63 |
+
- **H-level latency**: Time for the "slow" tier to process data
|
| 64 |
+
- **Total inference latency**: End-to-end prediction time (in milliseconds `ms`)
|
| 65 |
+
- **h_over_l_latency_ratio**: How many times slower H-level is than L-level
|
| 66 |
+
|
| 67 |
+
**Why this matters:** If H-level takes 10× longer than L-level, we know the memory hierarchy is working — slow operations vs. fast ones.
|
| 68 |
+
|
| 69 |
+
#### **Memory Metrics** (How much GPU memory used)
|
| 70 |
+
|
| 71 |
+
- **sram_peak_mb**: Peak memory in the "fast" tier
|
| 72 |
+
- **dram_peak_mb**: Peak memory in the "slow" tier
|
| 73 |
+
- **total_gpu_memory_mb**: Total GPU memory used
|
| 74 |
+
|
| 75 |
+
#### **Transfer Metrics** (Cost of moving data between tiers, in microseconds)
|
| 76 |
+
|
| 77 |
+
- **h_l_transfer_mean_us**: Average time to copy data from H→L
|
| 78 |
+
- **l_h_transfer_mean_us**: Average time to copy data from L→H
|
| 79 |
+
|
| 80 |
+
**Why this matters:** If transfers are expensive, the tiering strategy might not be worth it.
|
| 81 |
+
|
| 82 |
+
#### **Hit Rate & Efficiency**
|
| 83 |
+
|
| 84 |
+
- **sram_hit_rate**: How often data we tried to put in "fast" memory actually fit (0.0 = never, 1.0 = always)
|
| 85 |
+
- **memory_efficiency**: Useful compute time / total time (higher is better)
|
| 86 |
+
|
| 87 |
+
**Why this matters:** If hit_rate is low, data keeps spilling to slow memory and we're not getting the optimization benefit.
|
| 88 |
+
|
| 89 |
+
#### **Triton Kernel Probes** (Direct measurement of memory latency)
|
| 90 |
+
|
| 91 |
+
- **triton_sram_probe_latency_us**: Measured latency of "fast" memory via Triton kernels
|
| 92 |
+
- **triton_dram_probe_latency_us**: Measured latency of "slow" memory via Triton kernels
|
| 93 |
+
|
| 94 |
+
**Why this matters:** These are the "ground truth" measurements showing the actual speed difference between SRAM and DRAM.
|
| 95 |
+
|
| 96 |
+
---
|
| 97 |
+
|
| 98 |
+
### C. How Benchmarks Are Run
|
| 99 |
+
|
| 100 |
+
**Flow of a benchmark:**
|
| 101 |
+
|
| 102 |
+
1. **Create dummy data** (synthetic input tensors)
|
| 103 |
+
2. **Warmup phase** (run N times to let GPU caches warm up)
|
| 104 |
+
- These runs are NOT counted in final results
|
| 105 |
+
3. **Timing phase** (run N times with GPU timers active)
|
| 106 |
+
- Start GPU timer
|
| 107 |
+
- Run model inference
|
| 108 |
+
- Stop GPU timer
|
| 109 |
+
- Record elapsed time
|
| 110 |
+
4. **Collect statistics**: min, max, mean, std dev from all the timed runs
|
| 111 |
+
5. **Measure memory** (peak usage during runs)
|
| 112 |
+
6. **Calculate derived metrics** (ratios, efficiency scores)
|
| 113 |
+
7. **Output results** (to console and/or JSON file)
|
| 114 |
+
|
| 115 |
+
---
|
| 116 |
+
|
| 117 |
+
## PART 2: PyTorch Implementation — Line-by-Line
|
| 118 |
+
|
| 119 |
+
### File 1: `memory_tier.py` — The Memory Manager
|
| 120 |
+
|
| 121 |
+
This is the **PyTorch foundation** for the two-tier memory system. It mimics how GPU memory is organized (SRAM = fast cache vs. DRAM = main memory).
|
| 122 |
+
|
| 123 |
+
#### **Header & Imports (Lines 1-22)**
|
| 124 |
+
|
| 125 |
+
```python
|
| 126 |
+
"""
|
| 127 |
+
Memory Tier Manager for HRM SRAM/DRAM implementation.
|
| 128 |
+
|
| 129 |
+
Manages the placement of H-level and L-level hidden states across
|
| 130 |
+
GPU memory tiers and tracks all memory operations for benchmarking.
|
| 131 |
+
|
| 132 |
+
- SRAM tier: Uses CUDA pinned memory + explicit prefetching.
|
| 133 |
+
L-level states are kept GPU-resident with minimal transfers.
|
| 134 |
+
- DRAM tier: Standard GPU global memory with transfer tracking.
|
| 135 |
+
H-level states go through normal allocation paths.
|
| 136 |
+
"""
|
| 137 |
+
|
| 138 |
+
import time
|
| 139 |
+
from typing import Dict, List, Optional, Tuple
|
| 140 |
+
from dataclasses import dataclass, field
|
| 141 |
+
from contextlib import contextmanager
|
| 142 |
+
|
| 143 |
+
import torch
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
**Explanation:**
|
| 147 |
+
- The docstring explains the purpose: manage two memory tiers
|
| 148 |
+
- `dataclass` and `field` are used to create structured data containers
|
| 149 |
+
- `contextmanager` creates a context (like `with` statements in Python)
|
| 150 |
+
- `torch` is imported to use PyTorch tensor operations
|
| 151 |
+
|
| 152 |
+
#### **Section 1: MemoryEvent — Tracking Individual Operations (Lines 26-31)**
|
| 153 |
+
|
| 154 |
+
```python
|
| 155 |
+
@dataclass
|
| 156 |
+
class MemoryEvent:
|
| 157 |
+
"""A single tracked memory operation."""
|
| 158 |
+
tier: str # 'sram' or 'dram'
|
| 159 |
+
operation: str # 'alloc', 'load', 'store', 'transfer'
|
| 160 |
+
bytes: int
|
| 161 |
+
duration_us: float # microseconds
|
| 162 |
+
timestamp: float
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
**Explanation:**
|
| 166 |
+
- A `@dataclass` is like a **lightweight container** for data
|
| 167 |
+
- Each `MemoryEvent` records ONE memory operation (e.g., "allocated 1024 bytes in SRAM in 50 microseconds")
|
| 168 |
+
- Fields:
|
| 169 |
+
- `tier`: Which tier (fast or slow)?
|
| 170 |
+
- `operation`: What happened? (allocate new memory, load, store, transfer between tiers)
|
| 171 |
+
- `bytes`: How much data?
|
| 172 |
+
- `duration_us`: How long did it take? (in microseconds, where 1000 μs = 1 ms)
|
| 173 |
+
- `timestamp`: When did it happen?
|
| 174 |
+
|
| 175 |
+
**Real example:**
|
| 176 |
+
```python
|
| 177 |
+
event = MemoryEvent(
|
| 178 |
+
tier='sram',
|
| 179 |
+
operation='alloc',
|
| 180 |
+
bytes=65536,
|
| 181 |
+
duration_us=123.45,
|
| 182 |
+
timestamp=1704067200.123
|
| 183 |
+
)
|
| 184 |
+
# Created a record: "SRAM allocation of 65536 bytes took 123.45 microseconds"
|
| 185 |
+
```
|
| 186 |
+
|
| 187 |
+
#### **Section 2: TierStats — Accumulated Statistics (Lines 35-56)**
|
| 188 |
+
|
| 189 |
+
```python
|
| 190 |
+
@dataclass
|
| 191 |
+
class TierStats:
|
| 192 |
+
"""Accumulated statistics for one memory tier."""
|
| 193 |
+
total_alloc_bytes: int = 0
|
| 194 |
+
peak_alloc_bytes: int = 0
|
| 195 |
+
current_alloc_bytes: int = 0
|
| 196 |
+
num_loads: int = 0
|
| 197 |
+
num_stores: int = 0
|
| 198 |
+
num_transfers: int = 0
|
| 199 |
+
total_load_us: float = 0.0
|
| 200 |
+
total_store_us: float = 0.0
|
| 201 |
+
total_transfer_us: float = 0.0
|
| 202 |
+
hit_count: int = 0
|
| 203 |
+
miss_count: int = 0
|
| 204 |
+
|
| 205 |
+
@property
|
| 206 |
+
def hit_rate(self) -> float:
|
| 207 |
+
total = self.hit_count + self.miss_count
|
| 208 |
+
return self.hit_count / total if total > 0 else 0.0
|
| 209 |
+
|
| 210 |
+
@property
|
| 211 |
+
def avg_load_us(self) -> float:
|
| 212 |
+
return self.total_load_us / self.num_loads if self.num_loads > 0 else 0.0
|
| 213 |
+
|
| 214 |
+
@property
|
| 215 |
+
def avg_store_us(self) -> float:
|
| 216 |
+
return self.total_store_us / self.num_stores if self.num_stores > 0 else 0.0
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
**Explanation:**
|
| 220 |
+
- This accumulates **total statistics** for a tier (all operations combined)
|
| 221 |
+
- `total_alloc_bytes`: Total memory ever allocated
|
| 222 |
+
- `peak_alloc_bytes`: Maximum memory used at any point
|
| 223 |
+
- `current_alloc_bytes`: Memory currently in use
|
| 224 |
+
- `num_loads`, `num_stores`: Counters for how many times data was read/written
|
| 225 |
+
- `hit_count` / `miss_count`: Success/failure for fitting data in SRAM
|
| 226 |
+
- **Properties** (`@property`): Computed on-the-fly from raw counts
|
| 227 |
+
- `hit_rate = hit_count / (hit_count + miss_count)` — percentage of successful SRAM fits
|
| 228 |
+
- `avg_load_us = total_load_us / num_loads` — average speed per load operation
|
| 229 |
+
- `avg_store_us = total_store_us / num_stores` — average speed per store operation
|
| 230 |
+
|
| 231 |
+
**Why properties are useful:**
|
| 232 |
+
Instead of storing `hit_rate` separately and having to update it constantly, we compute it whenever asked: `stats.hit_rate` automatically calculates the current rate.
|
| 233 |
+
|
| 234 |
+
---
|
| 235 |
+
|
| 236 |
+
#### **Section 3: MemoryTierManager — The Main Manager Class (Lines 60-90)**
|
| 237 |
+
|
| 238 |
+
```python
|
| 239 |
+
class MemoryTierManager:
|
| 240 |
+
"""Coordinates SRAM/DRAM memory placement and tracking for HRM.
|
| 241 |
+
|
| 242 |
+
In the Triton context:
|
| 243 |
+
- SRAM tier: Tensors allocated with `pin_memory` and kept on the
|
| 244 |
+
same CUDA stream as L-level computation. Triton kernels keep
|
| 245 |
+
these values in registers/shared memory via data reuse.
|
| 246 |
+
- DRAM tier: Standard `torch.cuda` tensors. Triton kernels load
|
| 247 |
+
these from global memory each time.
|
| 248 |
+
"""
|
| 249 |
+
|
| 250 |
+
def __init__(
|
| 251 |
+
self,
|
| 252 |
+
device: torch.device,
|
| 253 |
+
enable_tracking: bool = True,
|
| 254 |
+
sram_capacity_mb: float = 48.0, # Typical L2 cache size
|
| 255 |
+
):
|
| 256 |
+
self.device = device
|
| 257 |
+
self.enable_tracking = enable_tracking
|
| 258 |
+
self.sram_capacity_bytes = int(sram_capacity_mb * 1024 * 1024)
|
| 259 |
+
|
| 260 |
+
# State registries
|
| 261 |
+
self._sram_tensors: Dict[str, torch.Tensor] = {}
|
| 262 |
+
self._dram_tensors: Dict[str, torch.Tensor] = {}
|
| 263 |
+
|
| 264 |
+
# Event log
|
| 265 |
+
self._events: List[MemoryEvent] = []
|
| 266 |
+
self._sram_stats = TierStats()
|
| 267 |
+
self._dram_stats = TierStats()
|
| 268 |
+
|
| 269 |
+
# CUDA events for GPU timing
|
| 270 |
+
self._use_cuda = device.type == 'cuda'
|
| 271 |
+
if self._use_cuda:
|
| 272 |
+
self._sram_stream = torch.cuda.Stream(device=device)
|
| 273 |
+
self._dram_stream = torch.cuda.Stream(device=device)
|
| 274 |
+
else:
|
| 275 |
+
self._sram_stream = None
|
| 276 |
+
self._dram_stream = None
|
| 277 |
+
```
|
| 278 |
+
|
| 279 |
+
**Explanation:**
|
| 280 |
+
|
| 281 |
+
- **`__init__` method**: Initializes the manager when created
|
| 282 |
+
- `device`: Where to allocate (CPU or GPU?)
|
| 283 |
+
- `enable_tracking`: Should we record all operations?
|
| 284 |
+
- `sram_capacity_mb`: How much "fast" memory is available? (Typical GPU L2 cache = 48 MB)
|
| 285 |
+
|
| 286 |
+
- **State registries:**
|
| 287 |
+
- `_sram_tensors`: Dictionary storing all tensors in SRAM (key = name, value = tensor)
|
| 288 |
+
- `_dram_tensors`: Dictionary storing all tensors in DRAM
|
| 289 |
+
- Example: `_sram_tensors['layer1_hidden'] = torch.tensor(...)`
|
| 290 |
+
|
| 291 |
+
- **Event log:**
|
| 292 |
+
- `_events`: A list of `MemoryEvent` objects (every operation is recorded)
|
| 293 |
+
- `_sram_stats`, `_dram_stats`: Running statistics for each tier
|
| 294 |
+
|
| 295 |
+
- **CUDA streams:**
|
| 296 |
+
- A stream is like a **"lane"** for GPU operations (operations in same lane execute sequentially, different lanes can run in parallel)
|
| 297 |
+
- Two separate streams (`_sram_stream`, `_dram_stream`) let us potentially run fast and slow operations concurrently
|
| 298 |
+
|
| 299 |
+
---
|
| 300 |
+
|
| 301 |
+
#### **Section 4A: SRAM Allocation (Lines 95-123)**
|
| 302 |
+
|
| 303 |
+
```python
|
| 304 |
+
def alloc_sram(self, name: str, shape: Tuple, dtype: torch.dtype) -> torch.Tensor:
|
| 305 |
+
"""Allocate a tensor in the SRAM tier (GPU-resident, pinned)."""
|
| 306 |
+
# Calculate size in bytes
|
| 307 |
+
nbytes = torch.tensor([], dtype=dtype).element_size()
|
| 308 |
+
for s in shape:
|
| 309 |
+
nbytes *= s
|
| 310 |
+
|
| 311 |
+
# Check capacity: does it fit?
|
| 312 |
+
if self._sram_stats.current_alloc_bytes + nbytes > self.sram_capacity_bytes:
|
| 313 |
+
# Doesn't fit → spill to DRAM and record a "miss"
|
| 314 |
+
self._sram_stats.miss_count += 1
|
| 315 |
+
return self.alloc_dram(name, shape, dtype)
|
| 316 |
+
|
| 317 |
+
# It fits! Record a "hit"
|
| 318 |
+
self._sram_stats.hit_count += 1
|
| 319 |
+
|
| 320 |
+
# Time the allocation
|
| 321 |
+
t0 = self._timer_start()
|
| 322 |
+
tensor = torch.zeros(shape, dtype=dtype, device=self.device)
|
| 323 |
+
|
| 324 |
+
# Hint: keep GPU-resident (don't swap to CPU)
|
| 325 |
+
if self._use_cuda:
|
| 326 |
+
with torch.cuda.stream(self._sram_stream):
|
| 327 |
+
tensor = tensor.contiguous()
|
| 328 |
+
|
| 329 |
+
self._sram_tensors[name] = tensor
|
| 330 |
+
dur = self._timer_end(t0)
|
| 331 |
+
|
| 332 |
+
# Update statistics
|
| 333 |
+
self._sram_stats.total_alloc_bytes += nbytes
|
| 334 |
+
self._sram_stats.current_alloc_bytes += nbytes
|
| 335 |
+
self._sram_stats.peak_alloc_bytes = max(
|
| 336 |
+
self._sram_stats.peak_alloc_bytes,
|
| 337 |
+
self._sram_stats.current_alloc_bytes,
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
# Record the operation
|
| 341 |
+
self._record_event('sram', 'alloc', nbytes, dur)
|
| 342 |
+
return tensor
|
| 343 |
+
```
|
| 344 |
+
|
| 345 |
+
**Explanation (line by line):**
|
| 346 |
+
|
| 347 |
+
1. **Calculate size:** Convert shape into bytes
|
| 348 |
+
- Example: shape `(128, 512)` with `float32` (4 bytes) = 128 × 512 × 4 = 262,144 bytes
|
| 349 |
+
|
| 350 |
+
2. **Capacity check:** Does this allocation fit in SRAM?
|
| 351 |
+
- If `current_alloc_bytes + nbytes > sram_capacity_bytes`, it doesn't fit
|
| 352 |
+
- **Fall back to DRAM** and **count as a miss** (failed to use SRAM)
|
| 353 |
+
- Otherwise, **count as a hit** (successfully allocated in SRAM)
|
| 354 |
+
|
| 355 |
+
3. **Timing:** Record when allocation starts (`t0`)
|
| 356 |
+
|
| 357 |
+
4. **Create tensor:** `torch.zeros(shape, ...)` creates a tensor of zeros
|
| 358 |
+
- `shape`: dimensions
|
| 359 |
+
- `dtype`: data type (e.g., float32)
|
| 360 |
+
- `device`: 'cuda' or 'cpu'
|
| 361 |
+
|
| 362 |
+
5. **GPU optimization:** `tensor.contiguous()` makes the data **contiguous in memory** (important for GPU performance)
|
| 363 |
+
- Done on the SRAM stream to suggest GPU placement
|
| 364 |
+
|
| 365 |
+
6. **Store in registry:** Save for later: `_sram_tensors[name] = tensor`
|
| 366 |
+
|
| 367 |
+
7. **Update stats:**
|
| 368 |
+
- Add `nbytes` to total and current
|
| 369 |
+
- Update peak if we're now using more than before
|
| 370 |
+
|
| 371 |
+
8. **Record event:** Add this operation to the event log
|
| 372 |
+
|
| 373 |
+
---
|
| 374 |
+
|
| 375 |
+
#### **Section 4B: DRAM Allocation (Lines 126-142)**
|
| 376 |
+
|
| 377 |
+
```python
|
| 378 |
+
def alloc_dram(self, name: str, shape: Tuple, dtype: torch.dtype) -> torch.Tensor:
|
| 379 |
+
"""Allocate a tensor in the DRAM tier (standard GPU memory)."""
|
| 380 |
+
nbytes = torch.tensor([], dtype=dtype).element_size()
|
| 381 |
+
for s in shape:
|
| 382 |
+
nbytes *= s
|
| 383 |
+
|
| 384 |
+
t0 = self._timer_start()
|
| 385 |
+
tensor = torch.zeros(shape, dtype=dtype, device=self.device)
|
| 386 |
+
self._dram_tensors[name] = tensor
|
| 387 |
+
dur = self._timer_end(t0)
|
| 388 |
+
|
| 389 |
+
self._dram_stats.total_alloc_bytes += nbytes
|
| 390 |
+
self._dram_stats.current_alloc_bytes += nbytes
|
| 391 |
+
self._dram_stats.peak_alloc_bytes = max(
|
| 392 |
+
self._dram_stats.peak_alloc_bytes,
|
| 393 |
+
self._dram_stats.current_alloc_bytes,
|
| 394 |
+
)
|
| 395 |
+
|
| 396 |
+
self._record_event('dram', 'alloc', nbytes, dur)
|
| 397 |
+
return tensor
|
| 398 |
+
```
|
| 399 |
+
|
| 400 |
+
**Explanation:**
|
| 401 |
+
- **Almost identical to `alloc_sram`**, but:
|
| 402 |
+
- **No capacity check** (unrestricted DRAM)
|
| 403 |
+
- **No stream optimization** (standard allocation)
|
| 404 |
+
- **Stores in `_dram_tensors`** instead of `_sram_tensors`
|
| 405 |
+
|
| 406 |
+
---
|
| 407 |
+
|
| 408 |
+
#### **Section 5: Cross-Tier Transfers (Lines 147-177)**
|
| 409 |
+
|
| 410 |
+
```python
|
| 411 |
+
def transfer_sram_to_dram(self, name: str) -> torch.Tensor:
|
| 412 |
+
"""Copy a tensor from SRAM tier to DRAM tier."""
|
| 413 |
+
src = self._sram_tensors[name]
|
| 414 |
+
t0 = self._timer_start()
|
| 415 |
+
dst = src.clone()
|
| 416 |
+
if self._use_cuda:
|
| 417 |
+
torch.cuda.synchronize(self.device)
|
| 418 |
+
dur = self._timer_end(t0)
|
| 419 |
+
|
| 420 |
+
self._dram_tensors[name + '_from_sram'] = dst
|
| 421 |
+
self._sram_stats.num_transfers += 1
|
| 422 |
+
self._sram_stats.total_transfer_us += dur
|
| 423 |
+
self._record_event('sram', 'transfer', src.nelement() * src.element_size(), dur)
|
| 424 |
+
return dst
|
| 425 |
+
|
| 426 |
+
def transfer_dram_to_sram(self, name: str) -> torch.Tensor:
|
| 427 |
+
"""Copy a tensor from DRAM tier to SRAM tier."""
|
| 428 |
+
src = self._dram_tensors[name]
|
| 429 |
+
t0 = self._timer_start()
|
| 430 |
+
dst = src.clone()
|
| 431 |
+
if self._use_cuda:
|
| 432 |
+
torch.cuda.synchronize(self.device)
|
| 433 |
+
dur = self._timer_end(t0)
|
| 434 |
+
|
| 435 |
+
self._sram_tensors[name + '_from_dram'] = dst
|
| 436 |
+
self._dram_stats.num_transfers += 1
|
| 437 |
+
self._dram_stats.total_transfer_us += dur
|
| 438 |
+
self._record_event('dram', 'transfer', src.nelement() * src.element_size(), dur)
|
| 439 |
+
return dst
|
| 440 |
+
```
|
| 441 |
+
|
| 442 |
+
**Explanation:**
|
| 443 |
+
|
| 444 |
+
- **H→L transfer (SRAM to DRAM):**
|
| 445 |
+
1. Get source tensor from SRAM registry
|
| 446 |
+
2. `clone()` = make a complete copy
|
| 447 |
+
3. `torch.cuda.synchronize()` = wait for GPU to finish (so we time actual operation)
|
| 448 |
+
4. Calculate duration
|
| 449 |
+
5. Store in DRAM registry with new name
|
| 450 |
+
6. Update transfer counter and total transfer time
|
| 451 |
+
7. Record the event
|
| 452 |
+
|
| 453 |
+
- **L→H transfer (DRAM to SRAM):** Same logic in reverse
|
| 454 |
+
|
| 455 |
+
**Why clone?** We don't want to move the original; we want a copy at the destination.
|
| 456 |
+
|
| 457 |
+
**Why synchronize?** GPU operations often run asynchronously (GPU schedules them but CPU moves on). Synchronize makes CPU wait, so we measure actual GPU time.
|
| 458 |
+
|
| 459 |
+
---
|
| 460 |
+
|
| 461 |
+
#### **Section 6: Context Managers for Streams (Lines 182-198)**
|
| 462 |
+
|
| 463 |
+
```python
|
| 464 |
+
@contextmanager
|
| 465 |
+
def sram_context(self):
|
| 466 |
+
"""Context manager that runs operations on the SRAM stream."""
|
| 467 |
+
if self._use_cuda and self._sram_stream is not None:
|
| 468 |
+
with torch.cuda.stream(self._sram_stream):
|
| 469 |
+
yield self._sram_stream
|
| 470 |
+
else:
|
| 471 |
+
yield None
|
| 472 |
+
|
| 473 |
+
@contextmanager
|
| 474 |
+
def dram_context(self):
|
| 475 |
+
"""Context manager that runs operations on the DRAM stream."""
|
| 476 |
+
if self._use_cuda and self._dram_stream is not None:
|
| 477 |
+
with torch.cuda.stream(self._dram_stream):
|
| 478 |
+
yield self._dram_stream
|
| 479 |
+
else:
|
| 480 |
+
yield None
|
| 481 |
+
```
|
| 482 |
+
|
| 483 |
+
**Explanation:**
|
| 484 |
+
|
| 485 |
+
A `@contextmanager` lets you use `with` statements:
|
| 486 |
+
|
| 487 |
+
```python
|
| 488 |
+
# Usage:
|
| 489 |
+
with memory_manager.sram_context() as stream:
|
| 490 |
+
# Operations here run on the SRAM stream
|
| 491 |
+
x = torch.matmul(a, b) # GPU runs this on _sram_stream
|
| 492 |
+
y = x + 1
|
| 493 |
+
|
| 494 |
+
# When we exit the block, stream is restored
|
| 495 |
+
```
|
| 496 |
+
|
| 497 |
+
**Why useful?** Different operations can run on different streams in parallel, potentially overlapping computation and memory transfer.
|
| 498 |
+
|
| 499 |
+
---
|
| 500 |
+
|
| 501 |
+
#### **Section 7: Statistics & Reporting (Lines 203-243)**
|
| 502 |
+
|
| 503 |
+
```python
|
| 504 |
+
def get_stats(self) -> Dict:
|
| 505 |
+
"""Return all memory tier statistics."""
|
| 506 |
+
return {
|
| 507 |
+
'sram': {
|
| 508 |
+
'peak_mb': self._sram_stats.peak_alloc_bytes / (1024 * 1024),
|
| 509 |
+
'current_mb': self._sram_stats.current_alloc_bytes / (1024 * 1024),
|
| 510 |
+
'hit_rate': self._sram_stats.hit_rate,
|
| 511 |
+
'num_loads': self._sram_stats.num_loads,
|
| 512 |
+
'num_stores': self._sram_stats.num_stores,
|
| 513 |
+
'num_transfers': self._sram_stats.num_transfers,
|
| 514 |
+
'avg_load_us': self._sram_stats.avg_load_us,
|
| 515 |
+
'avg_store_us': self._sram_stats.avg_store_us,
|
| 516 |
+
'total_transfer_us': self._sram_stats.total_transfer_us,
|
| 517 |
+
},
|
| 518 |
+
'dram': {
|
| 519 |
+
# Similar for DRAM...
|
| 520 |
+
},
|
| 521 |
+
'num_events': len(self._events),
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
def get_events(self) -> List[MemoryEvent]:
|
| 525 |
+
"""Return raw event log."""
|
| 526 |
+
return list(self._events)
|
| 527 |
+
|
| 528 |
+
def reset_stats(self):
|
| 529 |
+
"""Clear all statistics and event log."""
|
| 530 |
+
self._events.clear()
|
| 531 |
+
self._sram_stats = TierStats()
|
| 532 |
+
self._dram_stats = TierStats()
|
| 533 |
+
|
| 534 |
+
def free_all(self):
|
| 535 |
+
"""Release all managed tensors."""
|
| 536 |
+
self._sram_tensors.clear()
|
| 537 |
+
self._dram_tensors.clear()
|
| 538 |
+
self._sram_stats.current_alloc_bytes = 0
|
| 539 |
+
self._dram_stats.current_alloc_bytes = 0
|
| 540 |
+
```
|
| 541 |
+
|
| 542 |
+
**Explanation:**
|
| 543 |
+
|
| 544 |
+
- **`get_stats()`**: Returns a dictionary with all collected metrics (ready to print or save)
|
| 545 |
+
- Converts bytes to MB (divide by 1024²)
|
| 546 |
+
- Includes hit rate, counts, timing
|
| 547 |
+
|
| 548 |
+
- **`get_events()`**: Returns the raw event log (for detailed analysis)
|
| 549 |
+
|
| 550 |
+
- **`reset_stats()`**: Clear counters before a new benchmark run (don't contaminate results with previous runs)
|
| 551 |
+
|
| 552 |
+
- **`free_all()`**: Release memory and reset current allocations (cleanup)
|
| 553 |
+
|
| 554 |
+
---
|
| 555 |
+
|
| 556 |
+
#### **Section 8: Internal Timing Helpers (Lines 248-257)**
|
| 557 |
+
|
| 558 |
+
```python
|
| 559 |
+
def _timer_start(self) -> float:
|
| 560 |
+
if self._use_cuda:
|
| 561 |
+
torch.cuda.synchronize(self.device)
|
| 562 |
+
return time.perf_counter()
|
| 563 |
+
|
| 564 |
+
def _timer_end(self, t0: float) -> float:
|
| 565 |
+
if self._use_cuda:
|
| 566 |
+
torch.cuda.synchronize(self.device)
|
| 567 |
+
return (time.perf_counter() - t0) * 1e6 # → microseconds
|
| 568 |
+
|
| 569 |
+
def _record_event(self, tier: str, op: str, nbytes: int, dur_us: float):
|
| 570 |
+
if self.enable_tracking:
|
| 571 |
+
self._events.append(MemoryEvent(
|
| 572 |
+
tier=tier, operation=op, bytes=nbytes,
|
| 573 |
+
duration_us=dur_us, timestamp=time.time(),
|
| 574 |
+
))
|
| 575 |
+
```
|
| 576 |
+
|
| 577 |
+
**Explanation:**
|
| 578 |
+
|
| 579 |
+
- **`_timer_start()`:**
|
| 580 |
+
- Synchronize GPU first (wait for all pending operations)
|
| 581 |
+
- Record CPU time
|
| 582 |
+
- Returns the start time
|
| 583 |
+
|
| 584 |
+
- **`_timer_end(t0)`:**
|
| 585 |
+
- Synchronize GPU (wait for all operations since start)
|
| 586 |
+
- Calculate elapsed time
|
| 587 |
+
- Convert to microseconds (× 1,000,000)
|
| 588 |
+
|
| 589 |
+
- **`_record_event()`:**
|
| 590 |
+
- If tracking is enabled, create a `MemoryEvent` and add to log
|
| 591 |
+
- Otherwise, skip (for performance when we don't need detailed logs)
|
| 592 |
+
|
| 593 |
+
---
|
| 594 |
+
|
| 595 |
+
### File 2: `softmax.py` — Simple PyTorch Reference
|
| 596 |
+
|
| 597 |
+
```python
|
| 598 |
+
import torch
|
| 599 |
+
import torch.nn.functional as F
|
| 600 |
+
sample = torch.tensor([[1,2,3,4,5], [5,4,3,2,1]], dtype=torch.float32, device='cuda')
|
| 601 |
+
ref = F.softmax(sample, dim=1)
|
| 602 |
+
print(f"Softmax result: {ref=}")
|
| 603 |
+
```
|
| 604 |
+
|
| 605 |
+
**Explanation:**
|
| 606 |
+
|
| 607 |
+
This is a **minimal reference implementation** showing:
|
| 608 |
+
1. Create a sample tensor: 2 rows × 5 columns of numbers
|
| 609 |
+
2. Apply softmax along dimension 1 (across columns)
|
| 610 |
+
3. Print the result
|
| 611 |
+
|
| 612 |
+
**What softmax does:** Converts scores to probabilities (sum to 1). Example:
|
| 613 |
+
- Input `[1, 2, 3, 4, 5]`
|
| 614 |
+
- Output ~`[0.67, 0.18, 0.05, 0.01, 0.006]` (largest inputs become largest probabilities)
|
| 615 |
+
|
| 616 |
+
This is a building block used in attention mechanisms in transformers.
|
| 617 |
+
|
| 618 |
+
---
|
| 619 |
+
|
| 620 |
+
## PART 3: Triton Implementation — Line-by-Line
|
| 621 |
+
|
| 622 |
+
### File: `triton_kernels.py` — GPU Kernels for Memory-Aware Compute
|
| 623 |
+
|
| 624 |
+
Triton is a language for writing GPU kernels (low-level GPU programs). Unlike PyTorch which relies on pre-built library functions, Triton lets us control **exactly** how data flows through GPU memory.
|
| 625 |
+
|
| 626 |
+
#### **Header & Imports (Lines 1-23)**
|
| 627 |
+
|
| 628 |
+
```python
|
| 629 |
+
"""
|
| 630 |
+
Triton kernels for HRM SRAM/DRAM memory-tiered operations.
|
| 631 |
+
|
| 632 |
+
Key idea: In Triton, SRAM = registers + shared memory (managed by compiler
|
| 633 |
+
within a block). DRAM = global memory (GPU HBM). By structuring kernels to keep
|
| 634 |
+
L-level state tile-resident (loaded once, reused many times within a block),
|
| 635 |
+
we ensure L-level stays in SRAM. H-level state is loaded from global memory
|
| 636 |
+
(DRAM) each cycle, paying the full memory bandwidth cost.
|
| 637 |
+
|
| 638 |
+
This gives us real, measurable latency differences that map to the HRM's
|
| 639 |
+
hierarchical update frequencies.
|
| 640 |
+
"""
|
| 641 |
+
|
| 642 |
+
import torch
|
| 643 |
+
import triton
|
| 644 |
+
import triton.language as tl
|
| 645 |
+
import math
|
| 646 |
+
```
|
| 647 |
+
|
| 648 |
+
**Explanation:**
|
| 649 |
+
|
| 650 |
+
- Key insight: **Data locality matters enormously** on GPUs
|
| 651 |
+
- SRAM (L1/L2 cache, registers, shared memory): ~1-4 cycles latency, 100s of GB/s bandwidth
|
| 652 |
+
- DRAM (global memory, HBM): ~200-400 cycles latency, 100s of GB/s like SRAM but way higher latency!
|
| 653 |
+
|
| 654 |
+
**The trick:** Keep "important" data (L-level) in SRAM by reusing it within a kernel block. Load "less critical" data (H-level) fresh each time from DRAM.
|
| 655 |
+
|
| 656 |
+
---
|
| 657 |
+
|
| 658 |
+
#### **Kernel 1: SRAM-Resident RMS-Norm + Residual (Lines 28-64)**
|
| 659 |
+
|
| 660 |
+
```python
|
| 661 |
+
@triton.jit
|
| 662 |
+
def _rms_norm_residual_fused_kernel(
|
| 663 |
+
X_ptr, # Input tensor (residual branch)
|
| 664 |
+
Residual_ptr, # Residual connection input
|
| 665 |
+
Out_ptr, # Output tensor
|
| 666 |
+
N: tl.constexpr, # Hidden dimension (constexpr → compiler tiles in SRAM)
|
| 667 |
+
eps: tl.constexpr,
|
| 668 |
+
BLOCK_N: tl.constexpr,
|
| 669 |
+
):
|
| 670 |
+
"""Fused RMS-norm + residual add.
|
| 671 |
+
|
| 672 |
+
By making N and BLOCK_N constexpr, the compiler keeps the entire hidden
|
| 673 |
+
vector in registers/shared-memory (SRAM) across the norm computation.
|
| 674 |
+
This is the kernel used for L-level (fast path).
|
| 675 |
+
"""
|
| 676 |
+
row = tl.program_id(0)
|
| 677 |
+
cols = tl.arange(0, BLOCK_N)
|
| 678 |
+
mask = cols < N
|
| 679 |
+
|
| 680 |
+
# ---- Load both inputs into SRAM (registers) in one shot ----
|
| 681 |
+
x = tl.load(X_ptr + row * N + cols, mask=mask, other=0.0).to(tl.float32)
|
| 682 |
+
r = tl.load(Residual_ptr + row * N + cols, mask=mask, other=0.0).to(tl.float32)
|
| 683 |
+
|
| 684 |
+
# Residual add — stays in registers
|
| 685 |
+
h = x + r
|
| 686 |
+
|
| 687 |
+
# RMS norm — entirely in registers, no global memory round-trip
|
| 688 |
+
variance = tl.sum(h * h, axis=0) / N
|
| 689 |
+
h_norm = h * tl.math.rsqrt(variance + eps)
|
| 690 |
+
|
| 691 |
+
# ---- Store back to global memory ----
|
| 692 |
+
tl.store(Out_ptr + row * N + cols, h_norm.to(tl.bfloat16), mask=mask)
|
| 693 |
+
```
|
| 694 |
+
|
| 695 |
+
**Explanation (line by line):**
|
| 696 |
+
|
| 697 |
+
1. **Function signature:**
|
| 698 |
+
- `@triton.jit`: This is GPU code (JIT-compiled, not Python)
|
| 699 |
+
- `X_ptr`, `Residual_ptr`, `Out_ptr`: **Pointers** to tensors in GPU memory
|
| 700 |
+
- `N: tl.constexpr`: "constexpr" = compiler treats as a constant (compile-time value, not runtime)
|
| 701 |
+
- `BLOCK_N: tl.constexpr`: Block size (constexpr tells compiler to optimize for fixed size)
|
| 702 |
+
|
| 703 |
+
2. **Get thread ID:**
|
| 704 |
+
- `row = tl.program_id(0)`: Which "row" is this GPU thread processing?
|
| 705 |
+
- On GPU, thousands of threads run in parallel; each processes one row
|
| 706 |
+
|
| 707 |
+
3. **Create index array:**
|
| 708 |
+
- `cols = tl.arange(0, BLOCK_N)`: Array `[0, 1, 2, ..., BLOCK_N-1]`
|
| 709 |
+
- `mask = cols < N`: Boolean mask to handle if N < BLOCK_N (some threads might not have data)
|
| 710 |
+
|
| 711 |
+
4. **Load data into registers:**
|
| 712 |
+
- `tl.load(X_ptr + row * N + cols, ...)`: Load elements from memory
|
| 713 |
+
- `row * N`: Start at row-th row
|
| 714 |
+
- `+ cols`: Load all columns for this row
|
| 715 |
+
- `mask=mask`: Only load valid columns
|
| 716 |
+
- `other=0.0`: If invalid column, use 0.0
|
| 717 |
+
- `.to(tl.float32)`: Convert to 32-bit floats for math
|
| 718 |
+
- **KEY**: All data now in registers/shared memory (SRAM)!
|
| 719 |
+
|
| 720 |
+
5. **Compute residual:**
|
| 721 |
+
- `h = x + r`: Add the two inputs (element-wise, all in registers)
|
| 722 |
+
|
| 723 |
+
6. **Compute RMS norm:**
|
| 724 |
+
- RMS = sqrt(mean(x²))
|
| 725 |
+
- `variance = tl.sum(h * h, axis=0) / N`: Compute mean of squares
|
| 726 |
+
- `h_norm = h * tl.math.rsqrt(variance + eps)`: Normalize by reciprocal square root
|
| 727 |
+
- `eps`: Small number to avoid division by zero
|
| 728 |
+
- **KEY**: All arithmetic in registers!
|
| 729 |
+
|
| 730 |
+
7. **Store result:**
|
| 731 |
+
- `tl.store(Out_ptr + ..., h_norm.to(tl.bfloat16), mask=mask)`
|
| 732 |
+
- Write normalized result back to global memory
|
| 733 |
+
- `.to(tl.bfloat16)`: Convert to lower precision for storage (saves memory bandwidth)
|
| 734 |
+
|
| 735 |
+
**Why this is fast:**
|
| 736 |
+
- Load data once → keep in SRAM → do many operations → store once
|
| 737 |
+
- If we did this in PyTorch with separate ops (add, then norm), we'd load/store twice
|
| 738 |
+
|
| 739 |
+
---
|
| 740 |
+
|
| 741 |
+
#### **Kernel 2: DRAM-Sourced RMS-Norm + Residual (Lines 68-95)**
|
| 742 |
+
|
| 743 |
+
```python
|
| 744 |
+
@triton.jit
|
| 745 |
+
def _rms_norm_residual_dram_kernel(
|
| 746 |
+
X_ptr,
|
| 747 |
+
Residual_ptr,
|
| 748 |
+
Out_ptr,
|
| 749 |
+
N: tl.constexpr,
|
| 750 |
+
eps: tl.constexpr,
|
| 751 |
+
BLOCK_N: tl.constexpr,
|
| 752 |
+
):
|
| 753 |
+
"""RMS-norm + residual for H-level.
|
| 754 |
+
|
| 755 |
+
Structurally identical but designed to be called with larger strides
|
| 756 |
+
and without re-use inside a meta-kernel. Each call does a full
|
| 757 |
+
DRAM round-trip, modeling the slower H-level memory access pattern.
|
| 758 |
+
"""
|
| 759 |
+
row = tl.program_id(0)
|
| 760 |
+
cols = tl.arange(0, BLOCK_N)
|
| 761 |
+
mask = cols < N
|
| 762 |
+
|
| 763 |
+
# Global memory load (DRAM)
|
| 764 |
+
x = tl.load(X_ptr + row * N + cols, mask=mask, other=0.0).to(tl.float32)
|
| 765 |
+
r = tl.load(Residual_ptr + row * N + cols, mask=mask, other=0.0).to(tl.float32)
|
| 766 |
+
|
| 767 |
+
h = x + r
|
| 768 |
+
variance = tl.sum(h * h, axis=0) / N
|
| 769 |
+
h_norm = h * tl.math.rsqrt(variance + eps)
|
| 770 |
+
|
| 771 |
+
tl.store(Out_ptr + row * N + cols, h_norm.to(tl.bfloat16), mask=mask)
|
| 772 |
+
```
|
| 773 |
+
|
| 774 |
+
**Explanation:**
|
| 775 |
+
|
| 776 |
+
**Identical kernel code**, but:
|
| 777 |
+
- **Intent is different**: This is called less frequently (H-level runs slower)
|
| 778 |
+
- **Memory behavior differs** via **calling context**:
|
| 779 |
+
- SRAM kernel: Data reused many times within tight loops → stays in cache
|
| 780 |
+
- DRAM kernel: Data used once then discarded → evicted from cache
|
| 781 |
+
|
| 782 |
+
**In code**, they're identical because the difference is **how often and how much** they're called, not the kernel itself.
|
| 783 |
+
|
| 784 |
+
---
|
| 785 |
+
|
| 786 |
+
#### **Kernel 3: SwiGLU Activation (Lines 100-126)**
|
| 787 |
+
|
| 788 |
+
```python
|
| 789 |
+
@triton.jit
|
| 790 |
+
def _swiglu_fused_sram_kernel(
|
| 791 |
+
GateUp_ptr, # [rows, 2 * inter] — gate and up projections concatenated
|
| 792 |
+
Out_ptr, # [rows, inter]
|
| 793 |
+
inter: tl.constexpr,
|
| 794 |
+
BLOCK_INTER: tl.constexpr,
|
| 795 |
+
):
|
| 796 |
+
"""Fused SiLU(gate) * up in a single kernel pass.
|
| 797 |
+
|
| 798 |
+
For L-level: The gate and up vectors are loaded once into registers
|
| 799 |
+
and the activation is computed without spilling to DRAM.
|
| 800 |
+
"""
|
| 801 |
+
row = tl.program_id(0)
|
| 802 |
+
cols = tl.arange(0, BLOCK_INTER)
|
| 803 |
+
mask = cols < inter
|
| 804 |
+
|
| 805 |
+
# Load gate and up from contiguous memory — both go into SRAM
|
| 806 |
+
gate = tl.load(GateUp_ptr + row * 2 * inter + cols, mask=mask, other=0.0).to(tl.float32)
|
| 807 |
+
up = tl.load(GateUp_ptr + row * 2 * inter + inter + cols, mask=mask, other=0.0).to(tl.float32)
|
| 808 |
+
|
| 809 |
+
# SiLU(gate) * up — entirely in registers
|
| 810 |
+
silu_gate = gate * tl.sigmoid(gate)
|
| 811 |
+
result = silu_gate * up
|
| 812 |
+
|
| 813 |
+
tl.store(Out_ptr + row * inter + cols, result.to(tl.bfloat16), mask=mask)
|
| 814 |
+
```
|
| 815 |
+
|
| 816 |
+
**Explanation:**
|
| 817 |
+
|
| 818 |
+
**SwiGLU** = A gating mechanism in transformers. Formula: `output = sigmoid(gate) * up`
|
| 819 |
+
|
| 820 |
+
- **Input layout:** `GateUp_ptr` contains concatenated `[gate | up]` (e.g., first half is gate, second half is up)
|
| 821 |
+
- **Load both halves:**
|
| 822 |
+
- Gate: `row * 2 * inter + cols` (first half)
|
| 823 |
+
- Up: `row * 2 * inter + inter + cols` (second half)
|
| 824 |
+
- **Compute SiLU:**
|
| 825 |
+
- `tl.sigmoid(gate)`: Apply sigmoid (s-shaped function) to gate
|
| 826 |
+
- `silu_gate = gate * sigmoid(gate)`: SiLU = gate * sigmoid(gate)
|
| 827 |
+
- `result = silu_gate * up`: Gated output
|
| 828 |
+
- **Store:** Result back to global memory
|
| 829 |
+
|
| 830 |
+
**Why fused?** If done separately:
|
| 831 |
+
1. Load gate
|
| 832 |
+
2. Compute sigmoid
|
| 833 |
+
3. Write intermediate
|
| 834 |
+
4. Load intermediate
|
| 835 |
+
5. Compute SiLU
|
| 836 |
+
6. Write intermediate
|
| 837 |
+
7. Load up
|
| 838 |
+
8. Compute product
|
| 839 |
+
9. Write result
|
| 840 |
+
|
| 841 |
+
Fused does it in one kernel → one load, one store.
|
| 842 |
+
|
| 843 |
+
---
|
| 844 |
+
|
| 845 |
+
#### **Kernel 4: State Transfer (Lines 131-148)**
|
| 846 |
+
|
| 847 |
+
```python
|
| 848 |
+
@triton.jit
|
| 849 |
+
def _state_transfer_kernel(
|
| 850 |
+
Src_ptr,
|
| 851 |
+
Dst_ptr,
|
| 852 |
+
numel: tl.constexpr,
|
| 853 |
+
BLOCK: tl.constexpr,
|
| 854 |
+
):
|
| 855 |
+
"""Explicit memory copy kernel for cross-tier state transfer.
|
| 856 |
+
|
| 857 |
+
Used when H-level needs to read L-level output (or vice versa).
|
| 858 |
+
Triton compiles this into optimized async memcpy instructions.
|
| 859 |
+
"""
|
| 860 |
+
pid = tl.program_id(0)
|
| 861 |
+
offsets = pid * BLOCK + tl.arange(0, BLOCK)
|
| 862 |
+
mask = offsets < numel
|
| 863 |
+
|
| 864 |
+
data = tl.load(Src_ptr + offsets, mask=mask, other=0.0)
|
| 865 |
+
tl.store(Dst_ptr + offsets, data, mask=mask)
|
| 866 |
+
```
|
| 867 |
+
|
| 868 |
+
**Explanation:**
|
| 869 |
+
|
| 870 |
+
Simple memcpy kernel (copy data from source to destination):
|
| 871 |
+
- `pid = tl.program_id(0)`: Program/thread block ID
|
| 872 |
+
- `offsets = pid * BLOCK + tl.arange(0, BLOCK)`: Calculate which elements this block handles
|
| 873 |
+
- If BLOCK=1024, pid=0 handles offsets 0-1023, pid=1 handles 1024-2047, etc.
|
| 874 |
+
- `mask = offsets < numel`: Don't copy past the end
|
| 875 |
+
- Load and store in parallel across many threads
|
| 876 |
+
|
| 877 |
+
**Why separate kernel?** GPU memcopy can be optimized by the compiler (uses memory controllers, not just compute cores).
|
| 878 |
+
|
| 879 |
+
---
|
| 880 |
+
|
| 881 |
+
#### **Kernel 5: Memory Latency Probe (Lines 153-176)**
|
| 882 |
+
|
| 883 |
+
```python
|
| 884 |
+
@triton.jit
|
| 885 |
+
def _memory_latency_probe_kernel(
|
| 886 |
+
Data_ptr,
|
| 887 |
+
Out_ptr,
|
| 888 |
+
N: tl.constexpr,
|
| 889 |
+
BLOCK_N: tl.constexpr,
|
| 890 |
+
NUM_ITERS: tl.constexpr,
|
| 891 |
+
):
|
| 892 |
+
"""Probe kernel to measure effective memory latency.
|
| 893 |
+
|
| 894 |
+
Performs NUM_ITERS dependent loads to measure true SRAM vs DRAM latency.
|
| 895 |
+
The data dependency chain prevents compiler reordering.
|
| 896 |
+
"""
|
| 897 |
+
pid = tl.program_id(0)
|
| 898 |
+
cols = tl.arange(0, BLOCK_N)
|
| 899 |
+
mask = cols < N
|
| 900 |
+
|
| 901 |
+
# Initial load from global memory
|
| 902 |
+
acc = tl.load(Data_ptr + pid * N + cols, mask=mask, other=0.0)
|
| 903 |
+
|
| 904 |
+
# Dependent iteration chain — forces sequential memory access
|
| 905 |
+
for _ in range(NUM_ITERS):
|
| 906 |
+
# This stays in SRAM (registers) because acc is reused
|
| 907 |
+
acc = acc * 1.00001 + 0.00001
|
| 908 |
+
|
| 909 |
+
tl.store(Out_ptr + pid * N + cols, acc, mask=mask)
|
| 910 |
+
```
|
| 911 |
+
|
| 912 |
+
**Explanation:**
|
| 913 |
+
|
| 914 |
+
This kernel **measures latency** by creating a **dependency chain** that can't be optimized away:
|
| 915 |
+
|
| 916 |
+
1. Load initial data
|
| 917 |
+
2. For N iterations:
|
| 918 |
+
- Multiply by 1.00001 + add 0.00001 (cheap operations)
|
| 919 |
+
- Result depends on previous iteration (creates dependency)
|
| 920 |
+
3. Store result
|
| 921 |
+
|
| 922 |
+
**Why not just load/store?** The compiler could optimize away separate load-stores, but a **dependency chain** forces real latency measurement.
|
| 923 |
+
|
| 924 |
+
**For SRAM data:**
|
| 925 |
+
- Data stays in registers
|
| 926 |
+
- All iterations hit registers (super fast)
|
| 927 |
+
- Total time ≈ NUM_ITERS × 1 cycle ≈ very fast
|
| 928 |
+
|
| 929 |
+
**For DRAM data:**
|
| 930 |
+
- Data in global memory
|
| 931 |
+
- Each iteration reloads from DRAM
|
| 932 |
+
- Total time ≈ NUM_ITERS × 200-400 cycles ≈ slow!
|
| 933 |
+
|
| 934 |
+
**Result:** By comparing SRAM vs DRAM probe times, we measure the latency difference.
|
| 935 |
+
|
| 936 |
+
---
|
| 937 |
+
|
| 938 |
+
#### **Python Wrappers (Lines 184-265)**
|
| 939 |
+
|
| 940 |
+
```python
|
| 941 |
+
def _next_power_of_2(n: int) -> int:
|
| 942 |
+
return 1 << (n - 1).bit_length()
|
| 943 |
+
|
| 944 |
+
def triton_rms_norm_residual_sram(
|
| 945 |
+
x: torch.Tensor,
|
| 946 |
+
residual: torch.Tensor,
|
| 947 |
+
eps: float = 1e-5,
|
| 948 |
+
) -> torch.Tensor:
|
| 949 |
+
"""SRAM-optimized fused RMS-norm + residual for L-level."""
|
| 950 |
+
assert x.shape == residual.shape
|
| 951 |
+
assert x.is_contiguous() and residual.is_contiguous()
|
| 952 |
+
|
| 953 |
+
rows, N = x.shape[0] * (x.shape[1] if x.ndim == 3 else 1), x.shape[-1]
|
| 954 |
+
flat_x = x.reshape(rows, N)
|
| 955 |
+
flat_r = residual.reshape(rows, N)
|
| 956 |
+
out = torch.empty_like(flat_x)
|
| 957 |
+
|
| 958 |
+
BLOCK_N = _next_power_of_2(N)
|
| 959 |
+
|
| 960 |
+
_rms_norm_residual_fused_kernel[(rows,)](
|
| 961 |
+
flat_x, flat_r, out,
|
| 962 |
+
N=N, eps=eps, BLOCK_N=BLOCK_N,
|
| 963 |
+
)
|
| 964 |
+
return out.reshape(x.shape)
|
| 965 |
+
```
|
| 966 |
+
|
| 967 |
+
**Explanation:**
|
| 968 |
+
|
| 969 |
+
- **`_next_power_of_2(n)`**: Find smallest power of 2 ≥ n
|
| 970 |
+
- Example: `_next_power_of_2(512)` → 512, `_next_power_of_2(513)` → 1024
|
| 971 |
+
- (Bit manipulation: `(n-1).bit_length()` gives number of bits, `1 << x` is 2^x)
|
| 972 |
+
- GPUs work best with powers of 2 (thread block sizes)
|
| 973 |
+
|
| 974 |
+
- **`triton_rms_norm_residual_sram(...)`**: Python wrapper to call the Triton kernel
|
| 975 |
+
- Checks inputs are same shape and contiguous
|
| 976 |
+
- Flattens to 2D (rows × hidden_size)
|
| 977 |
+
- Creates output tensor
|
| 978 |
+
- Picks block size (nearest power of 2)
|
| 979 |
+
- Launches kernel with `[(rows,)]` — one thread block per row
|
| 980 |
+
- Reshapes output back to original
|
| 981 |
+
|
| 982 |
+
**Why a wrapper?** Triton kernels are GPU code; we need Python to:
|
| 983 |
+
1. Prepare data (reshape, allocate output)
|
| 984 |
+
2. Launch the kernel (call the JIT-compiled function)
|
| 985 |
+
3. Return result to CPU
|
| 986 |
+
|
| 987 |
+
---
|
| 988 |
+
|
| 989 |
+
## PART 4: How PyTorch & Triton Work Together
|
| 990 |
+
|
| 991 |
+
### The Full Pipeline
|
| 992 |
+
|
| 993 |
+
```
|
| 994 |
+
┌─────────────────────────────────────────────────────────────────┐
|
| 995 |
+
│ Input Data (e.g., transformer input) │
|
| 996 |
+
└────────────────────────────────┬────────────────────────────────┘
|
| 997 |
+
│
|
| 998 |
+
┌────────────▼────────────┐
|
| 999 |
+
│ MemoryTierManager │
|
| 1000 |
+
│ allocates L-level ────┐ │
|
| 1001 |
+
│ allocates H-level ──┐ │ │
|
| 1002 |
+
└────────────────┬────┘ │ │
|
| 1003 |
+
│ │ │
|
| 1004 |
+
┌────────────────────▼──┐ │ │
|
| 1005 |
+
│ L-level (Fast Path) │ │ │
|
| 1006 |
+
│ Triton kernels: │ │ │
|
| 1007 |
+
│ - SRAM RMS+Residual │ │ │
|
| 1008 |
+
│ - SRAM SwiGLU │ │ │
|
| 1009 |
+
│ (Data in regs/cache) │ │ │
|
| 1010 |
+
└────────────────┬──────┘ │ │
|
| 1011 |
+
│ │ │
|
| 1012 |
+
┌────────────────────▼────────┐ │ │
|
| 1013 |
+
│ H-level (Slow Path) │ │ │
|
| 1014 |
+
│ Triton kernels: │ │ │
|
| 1015 |
+
│ - DRAM RMS+Residual │ │ │
|
| 1016 |
+
│ (Data in global memory) │ │ │
|
| 1017 |
+
└────────────────┬───────────┘ │ │
|
| 1018 |
+
│ │ │
|
| 1019 |
+
┌───────────────────▼────────────┐│ │
|
| 1020 |
+
│ Triton State Transfer: ││ │
|
| 1021 |
+
│ Move L→H and H→L states ───────┼┘ │
|
| 1022 |
+
│ (what MemoryTierManager times) │ │
|
| 1023 |
+
└───────────────────┬────────────┘ │
|
| 1024 |
+
│ │
|
| 1025 |
+
┌────────────▼────────────┐ │
|
| 1026 |
+
│ Output │ │
|
| 1027 |
+
│ (result tensor) │ │
|
| 1028 |
+
└────────────────────────┘ │
|
| 1029 |
+
│
|
| 1030 |
+
┌───────────────────▼
|
| 1031 |
+
│ Statistics collected:
|
| 1032 |
+
│ - L/H latency
|
| 1033 |
+
│ - Transfer time
|
| 1034 |
+
│ - Memory usage
|
| 1035 |
+
│ - Hit rate
|
| 1036 |
+
```
|
| 1037 |
+
|
| 1038 |
+
### Real Example: Forward Pass
|
| 1039 |
+
|
| 1040 |
+
```python
|
| 1041 |
+
# 1. Create memory manager
|
| 1042 |
+
mem_mgr = MemoryTierManager(device='cuda')
|
| 1043 |
+
|
| 1044 |
+
# 2. Allocate L-level (fast) hidden state
|
| 1045 |
+
L_hidden = mem_mgr.alloc_sram('L_state', shape=(batch, hidden_dim), dtype=torch.float32)
|
| 1046 |
+
# → Records allocation in SRAM if it fits, otherwise spills to DRAM
|
| 1047 |
+
|
| 1048 |
+
# 3. Allocate H-level (slow) hidden state
|
| 1049 |
+
H_hidden = mem_mgr.alloc_dram('H_state', shape=(batch, hidden_dim), dtype=torch.float32)
|
| 1050 |
+
# → Records allocation in DRAM
|
| 1051 |
+
|
| 1052 |
+
# 4. L-level computes using SRAM kernel (fast)
|
| 1053 |
+
with mem_mgr.sram_context():
|
| 1054 |
+
L_out = triton_rms_norm_residual_sram(L_hidden, residual, eps=1e-5)
|
| 1055 |
+
L_out = triton_swiglu_sram(gate_up, inter_dim)
|
| 1056 |
+
# All data in registers/shared memory
|
| 1057 |
+
# Time recorded: ~10-100 microseconds per operation
|
| 1058 |
+
|
| 1059 |
+
# 5. H-level computes using DRAM kernel (slower)
|
| 1060 |
+
with mem_mgr.dram_context():
|
| 1061 |
+
H_out = triton_rms_norm_residual_dram(H_hidden, residual, eps=1e-5)
|
| 1062 |
+
# Data loaded from global memory each time
|
| 1063 |
+
# Time recorded: ~100-1000 microseconds per operation
|
| 1064 |
+
|
| 1065 |
+
# 6. Transfer L-level output to H-level
|
| 1066 |
+
L_to_H_output = mem_mgr.transfer_sram_to_dram('L_out')
|
| 1067 |
+
# → Records transfer size and time
|
| 1068 |
+
|
| 1069 |
+
# 7. Collect metrics
|
| 1070 |
+
stats = mem_mgr.get_stats()
|
| 1071 |
+
print(f"L/H latency ratio: {stats['H_latency'] / stats['L_latency']}")
|
| 1072 |
+
# → Usually 10-100x difference
|
| 1073 |
+
```
|
| 1074 |
+
|
| 1075 |
+
---
|
| 1076 |
+
|
| 1077 |
+
## PART 5: Bringing It All Together
|
| 1078 |
+
|
| 1079 |
+
### What Happens When You Run a Benchmark
|
| 1080 |
+
|
| 1081 |
+
```
|
| 1082 |
+
python run_benchmark.py --mode tiered --iterations 10
|
| 1083 |
+
```
|
| 1084 |
+
|
| 1085 |
+
1. **Setup:**
|
| 1086 |
+
- Create MemoryTierManager
|
| 1087 |
+
- Create model (HRM_Tiered)
|
| 1088 |
+
- Create dummy batch of data
|
| 1089 |
+
|
| 1090 |
+
2. **Warmup phase (first 5 runs):**
|
| 1091 |
+
- Forward pass (not timed)
|
| 1092 |
+
- GPU caches warm up, compilers warm up
|
| 1093 |
+
- Discarded from results
|
| 1094 |
+
|
| 1095 |
+
3. **Benchmark phase (10 timed runs):**
|
| 1096 |
+
- For each iteration:
|
| 1097 |
+
- Start GPU timer
|
| 1098 |
+
- Forward pass:
|
| 1099 |
+
- L-level uses SRAM kernels (fast)
|
| 1100 |
+
- H-level uses DRAM kernels (slow)
|
| 1101 |
+
- Record latencies in mem_mgr
|
| 1102 |
+
- Stop GPU timer
|
| 1103 |
+
- Record elapsed time
|
| 1104 |
+
|
| 1105 |
+
4. **Statistics:**
|
| 1106 |
+
- Calculate mean, min, max, std dev of times
|
| 1107 |
+
- Get memory stats (peak, current usage)
|
| 1108 |
+
- Get transfer stats (H↔L copy times)
|
| 1109 |
+
- Calculate derived metrics (ratios, efficiency)
|
| 1110 |
+
- Get Triton probe latencies (direct SRAM vs DRAM measurement)
|
| 1111 |
+
|
| 1112 |
+
5. **Output:**
|
| 1113 |
+
- Print table of results
|
| 1114 |
+
- Save to JSON
|
| 1115 |
+
- Optionally generate plots
|
| 1116 |
+
|
| 1117 |
+
---
|
| 1118 |
+
|
| 1119 |
+
## Key Insights
|
| 1120 |
+
|
| 1121 |
+
### Why Two Implementations?
|
| 1122 |
+
|
| 1123 |
+
| Aspect | PyTorch | Triton |
|
| 1124 |
+
|--------|---------|--------|
|
| 1125 |
+
| **What it expresses** | Algorithmic intent | Micro-architecture intent |
|
| 1126 |
+
| **Memory control** | Coarse (allocate tensor) | Fine (exact register usage) |
|
| 1127 |
+
| **Performance** | Relies on library kernels | Direct GPU control |
|
| 1128 |
+
| **Latency** | Can hide memory issues | Exposes memory latency differences |
|
| 1129 |
+
| **Usability** | Easy to write | Low-level, harder to write |
|
| 1130 |
+
|
| 1131 |
+
### Why SRAM vs DRAM?
|
| 1132 |
+
|
| 1133 |
+
**The 200× Latency Difference:**
|
| 1134 |
+
- SRAM (L2 cache): Load time ~4 cycles = ~2 nanoseconds = **very fast**
|
| 1135 |
+
- DRAM (HBM): Load time ~400 cycles = **200 nanoseconds = 200x slower!**
|
| 1136 |
+
|
| 1137 |
+
By **exploiting this difference**, we can:
|
| 1138 |
+
- Keep "important" (L-level) data in fast SRAM
|
| 1139 |
+
- Allow "less important" (H-level) data to use slow DRAM
|
| 1140 |
+
- Model hierarchical computation: fast frequent + slow infrequent
|
| 1141 |
+
|
| 1142 |
+
### Why Fuse Operations?
|
| 1143 |
+
|
| 1144 |
+
**Without fusion:**
|
| 1145 |
+
```
|
| 1146 |
+
Load → Compute → Store → Load → Compute → Store
|
| 1147 |
+
```
|
| 1148 |
+
Multiple memory round-trips!
|
| 1149 |
+
|
| 1150 |
+
**With fusion:**
|
| 1151 |
+
```
|
| 1152 |
+
Load → Compute → Compute → Compute → Store
|
| 1153 |
+
```
|
| 1154 |
+
One load, many operations, one store. **Massive speedup!**
|
| 1155 |
+
|
| 1156 |
+
---
|
| 1157 |
+
|
| 1158 |
+
## Testing It Yourself
|
| 1159 |
+
|
| 1160 |
+
### Quick test (2 minutes):
|
| 1161 |
+
```bash
|
| 1162 |
+
cd test-env/HRM_optimised
|
| 1163 |
+
python run_benchmark.py --mode tiered --warmup 1 --iterations 3 --batch-sizes 2 --seq-lens 16
|
| 1164 |
+
```
|
| 1165 |
+
|
| 1166 |
+
### Full benchmark (10 minutes):
|
| 1167 |
+
```bash
|
| 1168 |
+
python run_benchmark.py --mode compare --warmup 5 --iterations 20 --batch-sizes 1,8,32 --seq-lens 64,128
|
| 1169 |
+
```
|
| 1170 |
+
|
| 1171 |
+
### Analyze results:
|
| 1172 |
+
```python
|
| 1173 |
+
import json
|
| 1174 |
+
with open('benchmark_results/results.json') as f:
|
| 1175 |
+
results = json.load(f)
|
| 1176 |
+
print(f"L/H latency ratio: {results[0]['h_over_l_latency_ratio']}")
|
| 1177 |
+
print(f"SRAM hit rate: {results[0]['sram_hit_rate']}")
|
| 1178 |
+
print(f"Memory efficiency: {results[0]['memory_efficiency']}")
|
| 1179 |
+
```
|
| 1180 |
+
|
| 1181 |
+
---
|
| 1182 |
+
|
| 1183 |
+
## Summary
|
| 1184 |
+
|
| 1185 |
+
You now understand:
|
| 1186 |
+
|
| 1187 |
+
✅ **Benchmarks** → What metrics are collected and why
|
| 1188 |
+
✅ **PyTorch layer** → How `MemoryTierManager` tracks two memory tiers
|
| 1189 |
+
✅ **Triton layer** → How kernels control GPU memory access patterns
|
| 1190 |
+
✅ **Integration** → How they work together to model hierarchical computation
|
| 1191 |
+
✅ **Performance** → Why SRAM vs DRAM and data fusion matter for speed
|
| 1192 |
+
|
| 1193 |
+
The key idea: **Different parts of the model operate at different timescales (L-level fast, H-level slow), and we can model this using GPU memory hierarchy (SRAM fast, DRAM slow).**
|
docs/IMPLEMENTATION.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
**Overview**
|
| 2 |
+
- **Purpose**: Explains how the HRM model implements a two-tier memory hierarchy (L-level ≈ SRAM, H-level ≈ DRAM) in both PyTorch and Triton, and how the two implementations map to each other.
|
| 3 |
+
|
| 4 |
+
**Files to reference**
|
| 5 |
+
- `PyTorch ref`: [softmax.py](softmax.py)
|
| 6 |
+
- `Triton ref`: [softmax_triton.py](softmax_triton.py)
|
| 7 |
+
- `Memory tier manager`: [test-env/HRM_optimised/models/memory_tier.py](test-env/HRM_optimised/models/memory_tier.py)
|
| 8 |
+
- `Triton kernels`: [test-env/HRM_optimised/models/triton_kernels.py](test-env/HRM_optimised/models/triton_kernels.py)
|
| 9 |
+
- `Sparse embedding + optimizer`: [test-env/HRM_optimised/models/sparse_embedding.py](test-env/HRM_optimised/models/sparse_embedding.py)
|
| 10 |
+
|
| 11 |
+
**Conceptual summary**
|
| 12 |
+
- **Two-tier idea**: L-level (fast, frequently-updated state) is treated as SRAM: kept resident and reused heavily inside a compute tile. H-level (large, infrequently-updated state) is treated as DRAM: loaded from global memory when needed.
|
| 13 |
+
- **Why two implementations**: PyTorch expresses algorithmic intent using tensors, streams and explicit copies; Triton encodes the micro-kernel behavior (register/shared-memory reuse, explicit loads/stores) so we can enforce the intended locality and measure latency/bandwidth differences.
|
| 14 |
+
|
| 15 |
+
**PyTorch implementation (what to look for)**
|
| 16 |
+
- **`MemoryTierManager` (`memory_tier.py`)**: central piece for the PyTorch-side memory-tiering.
|
| 17 |
+
- **Allocation**: `alloc_sram` and `alloc_dram` allocate tensors and track sizes. SRAM allocation is guarded by a capacity limit (spill-to-DRAM on overflow).
|
| 18 |
+
- **Contexts / Streams**: `sram_context` and `dram_context` provide CUDA stream scopes to suggest where operations should run (helps overlap/ordering and can hint locality).
|
| 19 |
+
- **Transfers**: `transfer_sram_to_dram` and `transfer_dram_to_sram` perform explicit `clone()` copies and record timing via CPU timers with `torch.cuda.synchronize()` for correctness.
|
| 20 |
+
- **Tracking & metrics**: `MemoryEvent` log and `TierStats` collect hit/miss counts, transfer counts, and timing statistics used for benchmarking.
|
| 21 |
+
- **Pure-PyTorch ops**: Higher-level routines (e.g. a softmax demo in `softmax.py`) use standard `torch`/`torch.nn.functional` ops. These rely on the allocator/streams above to approximate tier behavior but cannot force register/shared-memory residency the way Triton kernels do.
|
| 22 |
+
- **Sparse embedding** (`sparse_embedding.py`): shows a training path where local slices are copied into a local buffer for computation and a distributed SignSGD step reduces gradients and writes back slices — this mirrors a “local fast working set + global parameter store” design.
|
| 23 |
+
|
| 24 |
+
**Triton implementation (what to look for)**
|
| 25 |
+
- **Kernels are explicit**: `triton_kernels.py` contains JIT-ed kernels that declare which data is intended to remain in SRAM (registers/shared-memory) vs. be sourced from DRAM (global memory).
|
| 26 |
+
- **`constexpr` parameters** (e.g. `N`, `BLOCK_N`) cause the Triton compiler to tile loops and keep whole tiles in registers/shared memory. This is how L-level residency is enforced.
|
| 27 |
+
- **SRAM kernels**: `_rms_norm_residual_fused_kernel` and `_swiglu_fused_sram_kernel` load tile data into registers/shared memory, perform fused compute (residual add, RMS-norm, SwiGLU) entirely in-register, then store results — minimal global memory traffic.
|
| 28 |
+
- **DRAM kernels**: `_rms_norm_residual_dram_kernel` uses the same math but is intended to be invoked without tile reuse; each invocation does full DRAM round-trips.
|
| 29 |
+
- **State transfer**: `_state_transfer_kernel` is an explicit copy kernel used to move state between tiers (H↔L). This is the Triton analogue of `transfer_*` in `MemoryTierManager` but compiled down to efficient device copies.
|
| 30 |
+
- **Latency probes**: `_memory_latency_probe_kernel` builds dependent load chains to measure effective latency and expose the practical difference between SRAM-like reuse and DRAM round trips.
|
| 31 |
+
- **Wrappers** (Python functions at the bottom of `triton_kernels.py`): reshape inputs, pick grid/block sizes, and launch the kernels (e.g. `triton_rms_norm_residual_sram`, `triton_state_transfer`). These are the call sites you'd use in place of the plain PyTorch ops where you need the explicit micro-architecture behavior.
|
| 32 |
+
|
| 33 |
+
**How the mapping works: PyTorch ↔ Triton**
|
| 34 |
+
- **Operator mapping**: Where PyTorch would call `x + residual` followed by `F.layer_norm` or `F.softmax`, the Triton path provides fused kernels that implement the same math but with different memory patterns (fused to avoid intermediate writes and to increase register/shared-memory reuse).
|
| 35 |
+
- **Memory behavior**: In PyTorch the best you can do is allocate tensors on particular streams and do explicit copies; residency in registers/shared memory is controlled by the backend and cannot be guaranteed. Triton lets you express the exact dataflow within a kernel so you can keep a tile in registers/shared memory across many operations.
|
| 36 |
+
- **Performance implications**: Triton kernels reduce global memory bandwidth by reusing tile data (L-level) and reduce kernel-launch overhead by fusing ops; PyTorch code is simpler and relies on library kernels but may incur extra memory traffic and intermediate buffers.
|
| 37 |
+
|
| 38 |
+
**Practical notes & where to change things**
|
| 39 |
+
- To use Triton kernels in place of PyTorch ops, replace the PyTorch call sites with the wrappers in `triton_kernels.py` (for example, use `triton_rms_norm_residual_sram(...)` for L-level paths and `triton_rms_norm_residual_dram(...)` for H-level).
|
| 40 |
+
- Tune tile sizes (`BLOCK_N`, `BLOCK_INTER`) by setting `constexpr` arguments and the `_next_power_of_2` helper. These control register/shared-memory usage vs parallelism.
|
| 41 |
+
- Use `triton_state_transfer` to implement the same explicit copies that `MemoryTierManager.transfer_*` performs; Triton copy kernels can be more efficient than `.clone()` in tight loops.
|
| 42 |
+
|
| 43 |
+
**References / next steps**
|
| 44 |
+
- Inspect the call sites in the model that route L-level vs H-level computation and swap in the Triton wrappers to validate behavior and measure latency.
|
| 45 |
+
- Use `MemoryTierManager.get_events()` and the Triton `memory_latency_probe` to compare the effective behavior end-to-end.
|
| 46 |
+
|
| 47 |
+
If you want, I can:
|
| 48 |
+
- add small call-site examples showing the PyTorch call and the Triton replacement, or
|
| 49 |
+
- run a micro-benchmark that compares the two paths and append results to this doc.
|
eval_dummy.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
eval_dummy.py — Evaluate Tiered vs Baseline HRM using dummy (random) tensors.
|
| 4 |
+
|
| 5 |
+
No datasets or checkpoints needed. This measures pure model throughput,
|
| 6 |
+
latency, and memory usage on synthetic inputs.
|
| 7 |
+
|
| 8 |
+
Usage:
|
| 9 |
+
source venv/bin/activate
|
| 10 |
+
python eval_dummy.py
|
| 11 |
+
python eval_dummy.py --batch-size 64 --seq-len 256 --hidden-size 1024
|
| 12 |
+
python eval_dummy.py --iterations 50 --plot
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import argparse
|
| 16 |
+
import json
|
| 17 |
+
import os
|
| 18 |
+
import sys
|
| 19 |
+
import time
|
| 20 |
+
from dataclasses import dataclass, asdict
|
| 21 |
+
|
| 22 |
+
import torch
|
| 23 |
+
import torch.nn.functional as F
|
| 24 |
+
|
| 25 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 26 |
+
|
| 27 |
+
from models.memory_tier import MemoryTierManager
|
| 28 |
+
from models.hrm.hrm_tiered import HRM_Tiered
|
| 29 |
+
from models.hrm.hrm_act_v1 import HierarchicalReasoningModel_ACTV1
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# =====================================================================
|
| 33 |
+
# Dummy batch generator
|
| 34 |
+
# =====================================================================
|
| 35 |
+
|
| 36 |
+
def make_batch(batch_size, seq_len, vocab_size, device):
|
| 37 |
+
return {
|
| 38 |
+
"inputs": torch.randint(0, vocab_size, (batch_size, seq_len), device=device),
|
| 39 |
+
"labels": torch.randint(0, vocab_size, (batch_size, seq_len), device=device),
|
| 40 |
+
"puzzle_identifiers": torch.arange(batch_size, device=device),
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# =====================================================================
|
| 45 |
+
# Single-model evaluation
|
| 46 |
+
# =====================================================================
|
| 47 |
+
|
| 48 |
+
@dataclass
|
| 49 |
+
class EvalResult:
|
| 50 |
+
model_name: str
|
| 51 |
+
batch_size: int
|
| 52 |
+
seq_len: int
|
| 53 |
+
hidden_size: int
|
| 54 |
+
param_count: int
|
| 55 |
+
latency_mean_ms: float
|
| 56 |
+
latency_std_ms: float
|
| 57 |
+
throughput_sps: float
|
| 58 |
+
gpu_memory_mb: float
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def eval_model(model, model_name, batch, device, warmup=5, iterations=20):
|
| 62 |
+
"""Run inference on dummy data and collect timing stats."""
|
| 63 |
+
model.eval()
|
| 64 |
+
bs = batch["inputs"].shape[0]
|
| 65 |
+
sl = batch["inputs"].shape[1]
|
| 66 |
+
param_count = sum(p.numel() for p in model.parameters())
|
| 67 |
+
|
| 68 |
+
# Warmup
|
| 69 |
+
with torch.no_grad():
|
| 70 |
+
for _ in range(warmup):
|
| 71 |
+
carry = model.initial_carry(batch)
|
| 72 |
+
carry.inner_carry.z_H = carry.inner_carry.z_H.to(device)
|
| 73 |
+
carry.inner_carry.z_L = carry.inner_carry.z_L.to(device)
|
| 74 |
+
carry.steps = carry.steps.to(device)
|
| 75 |
+
carry.halted = carry.halted.to(device)
|
| 76 |
+
carry.current_data = {k: v.to(device) for k, v in carry.current_data.items()}
|
| 77 |
+
model(carry, batch)
|
| 78 |
+
|
| 79 |
+
if torch.cuda.is_available():
|
| 80 |
+
torch.cuda.reset_peak_memory_stats(device)
|
| 81 |
+
torch.cuda.synchronize()
|
| 82 |
+
|
| 83 |
+
# Timed iterations
|
| 84 |
+
latencies = []
|
| 85 |
+
with torch.no_grad():
|
| 86 |
+
for _ in range(iterations):
|
| 87 |
+
carry = model.initial_carry(batch)
|
| 88 |
+
carry.inner_carry.z_H = carry.inner_carry.z_H.to(device)
|
| 89 |
+
carry.inner_carry.z_L = carry.inner_carry.z_L.to(device)
|
| 90 |
+
carry.steps = carry.steps.to(device)
|
| 91 |
+
carry.halted = carry.halted.to(device)
|
| 92 |
+
carry.current_data = {k: v.to(device) for k, v in carry.current_data.items()}
|
| 93 |
+
|
| 94 |
+
if torch.cuda.is_available():
|
| 95 |
+
start = torch.cuda.Event(enable_timing=True)
|
| 96 |
+
end = torch.cuda.Event(enable_timing=True)
|
| 97 |
+
start.record()
|
| 98 |
+
|
| 99 |
+
model(carry, batch)
|
| 100 |
+
|
| 101 |
+
if torch.cuda.is_available():
|
| 102 |
+
end.record()
|
| 103 |
+
torch.cuda.synchronize()
|
| 104 |
+
latencies.append(start.elapsed_time(end))
|
| 105 |
+
else:
|
| 106 |
+
pass # CPU fallback handled by perf_counter
|
| 107 |
+
|
| 108 |
+
mean_ms = sum(latencies) / len(latencies)
|
| 109 |
+
std_ms = (sum((x - mean_ms)**2 for x in latencies) / max(len(latencies)-1, 1)) ** 0.5
|
| 110 |
+
throughput = bs / (mean_ms / 1000) if mean_ms > 0 else 0
|
| 111 |
+
gpu_mem = torch.cuda.max_memory_allocated(device) / (1024**2) if torch.cuda.is_available() else 0
|
| 112 |
+
|
| 113 |
+
return EvalResult(
|
| 114 |
+
model_name=model_name,
|
| 115 |
+
batch_size=bs, seq_len=sl,
|
| 116 |
+
hidden_size=model.config.hidden_size,
|
| 117 |
+
param_count=param_count,
|
| 118 |
+
latency_mean_ms=mean_ms,
|
| 119 |
+
latency_std_ms=std_ms,
|
| 120 |
+
throughput_sps=throughput,
|
| 121 |
+
gpu_memory_mb=gpu_mem,
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
# =====================================================================
|
| 126 |
+
# Main comparison
|
| 127 |
+
# =====================================================================
|
| 128 |
+
|
| 129 |
+
def main():
|
| 130 |
+
parser = argparse.ArgumentParser(description="HRM Dummy Tensor Evaluation")
|
| 131 |
+
parser.add_argument("--batch-size", type=int, default=8)
|
| 132 |
+
parser.add_argument("--seq-len", type=int, default=81, help="Sudoku=81, or any length")
|
| 133 |
+
parser.add_argument("--hidden-size", type=int, default=512)
|
| 134 |
+
parser.add_argument("--num-heads", type=int, default=8)
|
| 135 |
+
parser.add_argument("--H-cycles", type=int, default=2)
|
| 136 |
+
parser.add_argument("--L-cycles", type=int, default=2)
|
| 137 |
+
parser.add_argument("--H-layers", type=int, default=4)
|
| 138 |
+
parser.add_argument("--L-layers", type=int, default=4)
|
| 139 |
+
parser.add_argument("--warmup", type=int, default=5)
|
| 140 |
+
parser.add_argument("--iterations", type=int, default=20)
|
| 141 |
+
parser.add_argument("--output", type=str, default="benchmark_results/eval_dummy.json")
|
| 142 |
+
parser.add_argument("--plot", action="store_true")
|
| 143 |
+
args = parser.parse_args()
|
| 144 |
+
|
| 145 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 146 |
+
vocab_size = 32
|
| 147 |
+
|
| 148 |
+
config_dict = {
|
| 149 |
+
"batch_size": args.batch_size,
|
| 150 |
+
"seq_len": args.seq_len,
|
| 151 |
+
"puzzle_emb_ndim": 0,
|
| 152 |
+
"num_puzzle_identifiers": args.batch_size,
|
| 153 |
+
"vocab_size": vocab_size,
|
| 154 |
+
"H_cycles": args.H_cycles,
|
| 155 |
+
"L_cycles": args.L_cycles,
|
| 156 |
+
"H_layers": args.H_layers,
|
| 157 |
+
"L_layers": args.L_layers,
|
| 158 |
+
"hidden_size": args.hidden_size,
|
| 159 |
+
"expansion": 4.0,
|
| 160 |
+
"num_heads": args.num_heads,
|
| 161 |
+
"pos_encodings": "rope",
|
| 162 |
+
"halt_max_steps": 1,
|
| 163 |
+
"halt_exploration_prob": 0.0,
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
batch = make_batch(args.batch_size, args.seq_len, vocab_size, device)
|
| 167 |
+
|
| 168 |
+
print(f"\n{'='*64}")
|
| 169 |
+
print(f" HRM Dummy Tensor Evaluation")
|
| 170 |
+
print(f" Device: {device} ({torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'CPU'})")
|
| 171 |
+
print(f" Batch: {args.batch_size}")
|
| 172 |
+
print(f" Seq Len: {args.seq_len}")
|
| 173 |
+
print(f" Hidden: {args.hidden_size}")
|
| 174 |
+
print(f" H/L cycles: {args.H_cycles}/{args.L_cycles}")
|
| 175 |
+
print(f" H/L layers: {args.H_layers}/{args.L_layers}")
|
| 176 |
+
print(f" Iterations: {args.iterations} (warmup: {args.warmup})")
|
| 177 |
+
print(f"{'='*64}")
|
| 178 |
+
|
| 179 |
+
results = []
|
| 180 |
+
|
| 181 |
+
# ── Baseline ──
|
| 182 |
+
print(f"\n → Evaluating Baseline (hrm_act_v1)...")
|
| 183 |
+
baseline_model = HierarchicalReasoningModel_ACTV1(config_dict).to(device)
|
| 184 |
+
r_baseline = eval_model(baseline_model, "Baseline", batch, device, args.warmup, args.iterations)
|
| 185 |
+
results.append(r_baseline)
|
| 186 |
+
del baseline_model
|
| 187 |
+
torch.cuda.empty_cache()
|
| 188 |
+
|
| 189 |
+
# ── Tiered ──
|
| 190 |
+
print(f" → Evaluating Tiered (SRAM/DRAM Triton)...")
|
| 191 |
+
mem_mgr = MemoryTierManager(device=device, enable_tracking=True)
|
| 192 |
+
tiered_model = HRM_Tiered(config_dict, memory_manager=mem_mgr).to(device)
|
| 193 |
+
r_tiered = eval_model(tiered_model, "Tiered", batch, device, args.warmup, args.iterations)
|
| 194 |
+
|
| 195 |
+
# Grab tier-specific stats
|
| 196 |
+
tier_timing = tiered_model.get_timing_stats()
|
| 197 |
+
tier_mem = mem_mgr.get_stats()
|
| 198 |
+
results.append(r_tiered)
|
| 199 |
+
del tiered_model
|
| 200 |
+
torch.cuda.empty_cache()
|
| 201 |
+
|
| 202 |
+
# ── Print comparison table ──
|
| 203 |
+
print(f"\n{'='*64}")
|
| 204 |
+
print(f" {'Model':<12} {'Params':>10} {'Latency(ms)':>13} {'±σ':>8} {'Throughput':>12} {'GPU MB':>8}")
|
| 205 |
+
print(f" {'-'*58}")
|
| 206 |
+
for r in results:
|
| 207 |
+
print(f" {r.model_name:<12} {r.param_count/1e6:>9.1f}M {r.latency_mean_ms:>13.2f} {r.latency_std_ms:>8.2f} {r.throughput_sps:>12.1f} {r.gpu_memory_mb:>8.1f}")
|
| 208 |
+
|
| 209 |
+
# Speedup
|
| 210 |
+
speedup = r_baseline.latency_mean_ms / r_tiered.latency_mean_ms if r_tiered.latency_mean_ms > 0 else 0
|
| 211 |
+
mem_diff = r_baseline.gpu_memory_mb - r_tiered.gpu_memory_mb
|
| 212 |
+
print(f"\n Speedup: {speedup:.2f}x")
|
| 213 |
+
print(f" Memory saving: {mem_diff:.1f} MB")
|
| 214 |
+
|
| 215 |
+
# Tier-specific stats
|
| 216 |
+
if tier_timing:
|
| 217 |
+
l_us = tier_timing.get("L_forward_us", {}).get("mean_us", 0)
|
| 218 |
+
h_us = tier_timing.get("H_forward_us", {}).get("mean_us", 0)
|
| 219 |
+
ratio = h_us / l_us if l_us > 0 else 0
|
| 220 |
+
print(f"\n L-level (SRAM): {l_us:.1f} μs")
|
| 221 |
+
print(f" H-level (DRAM): {h_us:.1f} μs")
|
| 222 |
+
print(f" H/L ratio: {ratio:.2f}x")
|
| 223 |
+
|
| 224 |
+
if tier_mem:
|
| 225 |
+
print(f" SRAM hit rate: {tier_mem['sram']['hit_rate']:.2%}")
|
| 226 |
+
print(f"{'='*64}\n")
|
| 227 |
+
|
| 228 |
+
# ── Save ──
|
| 229 |
+
os.makedirs(os.path.dirname(args.output) or ".", exist_ok=True)
|
| 230 |
+
with open(args.output, "w") as f:
|
| 231 |
+
json.dump([asdict(r) for r in results], f, indent=2)
|
| 232 |
+
print(f" Results saved → {args.output}")
|
| 233 |
+
|
| 234 |
+
# ── Plots ──
|
| 235 |
+
if args.plot:
|
| 236 |
+
try:
|
| 237 |
+
import matplotlib
|
| 238 |
+
matplotlib.use("Agg")
|
| 239 |
+
import matplotlib.pyplot as plt
|
| 240 |
+
|
| 241 |
+
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
| 242 |
+
fig.suptitle("HRM Dummy Eval: Baseline vs Tiered", fontweight="bold")
|
| 243 |
+
names = [r.model_name for r in results]
|
| 244 |
+
colors = ["#e74c3c", "#2ecc71"]
|
| 245 |
+
|
| 246 |
+
# Latency
|
| 247 |
+
axes[0].bar(names, [r.latency_mean_ms for r in results], color=colors)
|
| 248 |
+
axes[0].set_ylabel("Latency (ms)")
|
| 249 |
+
axes[0].set_title("Inference Latency")
|
| 250 |
+
axes[0].grid(axis="y", alpha=0.3)
|
| 251 |
+
|
| 252 |
+
# Throughput
|
| 253 |
+
axes[1].bar(names, [r.throughput_sps for r in results], color=colors)
|
| 254 |
+
axes[1].set_ylabel("Samples/sec")
|
| 255 |
+
axes[1].set_title("Throughput")
|
| 256 |
+
axes[1].grid(axis="y", alpha=0.3)
|
| 257 |
+
|
| 258 |
+
# Memory
|
| 259 |
+
axes[2].bar(names, [r.gpu_memory_mb for r in results], color=colors)
|
| 260 |
+
axes[2].set_ylabel("GPU Memory (MB)")
|
| 261 |
+
axes[2].set_title("Peak Memory")
|
| 262 |
+
axes[2].grid(axis="y", alpha=0.3)
|
| 263 |
+
|
| 264 |
+
plt.tight_layout()
|
| 265 |
+
plot_dir = os.path.dirname(args.output) or "benchmark_results"
|
| 266 |
+
plot_path = os.path.join(plot_dir, "eval_dummy_comparison.png")
|
| 267 |
+
plt.savefig(plot_path, dpi=150)
|
| 268 |
+
plt.close()
|
| 269 |
+
print(f" Plot saved → {plot_path}")
|
| 270 |
+
except ImportError:
|
| 271 |
+
print(" (matplotlib not found — skipping plots)")
|
| 272 |
+
|
| 273 |
+
print(" Done!\n")
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
if __name__ == "__main__":
|
| 277 |
+
main()
|
evaluate.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List
|
| 2 |
+
import yaml
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
import torch.distributed as dist
|
| 7 |
+
|
| 8 |
+
import pydantic
|
| 9 |
+
from omegaconf import OmegaConf
|
| 10 |
+
from pretrain import PretrainConfig, init_train_state, evaluate, create_dataloader
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class EvalConfig(pydantic.BaseModel):
|
| 14 |
+
checkpoint: str
|
| 15 |
+
|
| 16 |
+
save_outputs: List[str] = ["inputs", "labels", "puzzle_identifiers", "logits", "q_halt_logits", "q_continue_logits"]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def launch():
|
| 20 |
+
eval_cfg = EvalConfig(**OmegaConf.to_container(OmegaConf.from_cli())) # type: ignore
|
| 21 |
+
|
| 22 |
+
RANK = 0
|
| 23 |
+
WORLD_SIZE = 1
|
| 24 |
+
# Initialize distributed training if in distributed environment (e.g. torchrun)
|
| 25 |
+
if "LOCAL_RANK" in os.environ:
|
| 26 |
+
# Initialize distributed, default device and dtype
|
| 27 |
+
dist.init_process_group(backend="nccl")
|
| 28 |
+
|
| 29 |
+
RANK = dist.get_rank()
|
| 30 |
+
WORLD_SIZE = dist.get_world_size()
|
| 31 |
+
|
| 32 |
+
torch.cuda.set_device(int(os.environ["LOCAL_RANK"]))
|
| 33 |
+
|
| 34 |
+
with open(os.path.join(os.path.dirname(eval_cfg.checkpoint), "all_config.yaml"), "r") as f:
|
| 35 |
+
config = PretrainConfig(**yaml.safe_load(f))
|
| 36 |
+
|
| 37 |
+
config.eval_save_outputs = eval_cfg.save_outputs
|
| 38 |
+
config.checkpoint_path = os.path.dirname(eval_cfg.checkpoint)
|
| 39 |
+
|
| 40 |
+
# Dataloader
|
| 41 |
+
train_loader, train_metadata = create_dataloader(config, "train", test_set_mode=False, epochs_per_iter=1, global_batch_size=config.global_batch_size, rank=RANK, world_size=WORLD_SIZE)
|
| 42 |
+
eval_loader, eval_metadata = create_dataloader(config, "test", test_set_mode=True, epochs_per_iter=1, global_batch_size=config.global_batch_size, rank=RANK, world_size=WORLD_SIZE)
|
| 43 |
+
|
| 44 |
+
# Models
|
| 45 |
+
train_state = init_train_state(config, train_metadata, world_size=WORLD_SIZE)
|
| 46 |
+
# Try unwrap torch.compile
|
| 47 |
+
try:
|
| 48 |
+
train_state.model.load_state_dict(torch.load(eval_cfg.checkpoint, map_location="cuda"), assign=True)
|
| 49 |
+
except:
|
| 50 |
+
train_state.model.load_state_dict({k.removeprefix("_orig_mod."): v for k, v in torch.load(eval_cfg.checkpoint, map_location="cuda").items()}, assign=True)
|
| 51 |
+
|
| 52 |
+
train_state.step = 0
|
| 53 |
+
ckpt_filename = os.path.basename(eval_cfg.checkpoint)
|
| 54 |
+
if ckpt_filename.startswith("step_"):
|
| 55 |
+
train_state.step = int(ckpt_filename.removeprefix("step_"))
|
| 56 |
+
|
| 57 |
+
# Evaluate
|
| 58 |
+
print ("Starting evaluation")
|
| 59 |
+
|
| 60 |
+
train_state.model.eval()
|
| 61 |
+
metrics = evaluate(config, train_state, eval_loader, eval_metadata, rank=RANK, world_size=WORLD_SIZE)
|
| 62 |
+
|
| 63 |
+
if metrics is not None:
|
| 64 |
+
print (metrics)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
if __name__ == "__main__":
|
| 68 |
+
launch()
|
fusedevals.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Evaluate Baseline (Traditional) vs Tiered (Fused) HRM models on the specified dataset.
|
| 4 |
+
Both models are evaluated using the EXACT SAME weights to verify correctness and compare speed.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import yaml
|
| 10 |
+
import time
|
| 11 |
+
import argparse
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
import numpy as np
|
| 15 |
+
from safetensors.torch import load_file
|
| 16 |
+
from omegaconf import OmegaConf
|
| 17 |
+
|
| 18 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 19 |
+
|
| 20 |
+
from pretrain import PretrainConfig, create_dataloader
|
| 21 |
+
from models.hrm.hrm_tiered import HRM_Tiered
|
| 22 |
+
from models.hrm.hrm_act_v1 import HierarchicalReasoningModel_ACTV1
|
| 23 |
+
from models.losses import ACTLossHead
|
| 24 |
+
from models.memory_tier import MemoryTierManager
|
| 25 |
+
|
| 26 |
+
def build_model(arch, config_dict, device):
|
| 27 |
+
if arch == "tiered":
|
| 28 |
+
mm = MemoryTierManager(device=device, enable_tracking=True)
|
| 29 |
+
model = HRM_Tiered(config_dict, memory_manager=mm)
|
| 30 |
+
else:
|
| 31 |
+
model = HierarchicalReasoningModel_ACTV1(config_dict)
|
| 32 |
+
|
| 33 |
+
# Wrap in ACTLossHead exactly as pretrain.py does
|
| 34 |
+
loss_head = ACTLossHead(model, loss_type="stablemax_cross_entropy").to(device)
|
| 35 |
+
loss_head.eval()
|
| 36 |
+
return loss_head, (mm if arch == "tiered" else None)
|
| 37 |
+
|
| 38 |
+
@torch.no_grad()
|
| 39 |
+
def benchmark_model(model_name, model, dataloader, metadata, device):
|
| 40 |
+
print(f"\n[{model_name}] Starting Evaluation on dataset...")
|
| 41 |
+
|
| 42 |
+
model.eval()
|
| 43 |
+
all_metrics = []
|
| 44 |
+
|
| 45 |
+
start_time = time.perf_counter()
|
| 46 |
+
total_samples = 0
|
| 47 |
+
|
| 48 |
+
# We will accumulate the exact accuracy matching evaluate.py
|
| 49 |
+
total_accuracy = 0
|
| 50 |
+
total_exact_accuracy = 0
|
| 51 |
+
total_count = 0
|
| 52 |
+
|
| 53 |
+
for set_name, batch, batch_size in dataloader:
|
| 54 |
+
batch = {k: v.to(device) for k, v in batch.items()}
|
| 55 |
+
# ACTLossHead wraps initial_carry
|
| 56 |
+
with torch.device(device):
|
| 57 |
+
carry = model.initial_carry(batch)
|
| 58 |
+
|
| 59 |
+
while True:
|
| 60 |
+
carry, loss, metrics, _, all_finish = model(carry=carry, batch=batch, return_keys=[])
|
| 61 |
+
if all_finish:
|
| 62 |
+
break
|
| 63 |
+
|
| 64 |
+
total_accuracy += metrics["accuracy"].item()
|
| 65 |
+
total_exact_accuracy += metrics["exact_accuracy"].item()
|
| 66 |
+
total_count += metrics["count"].item()
|
| 67 |
+
total_samples += batch_size
|
| 68 |
+
|
| 69 |
+
# Synchronize GPU to ensure timing is correct
|
| 70 |
+
if torch.cuda.is_available():
|
| 71 |
+
torch.cuda.synchronize()
|
| 72 |
+
|
| 73 |
+
end_time = time.perf_counter()
|
| 74 |
+
duration = end_time - start_time
|
| 75 |
+
|
| 76 |
+
acc = total_accuracy / max(total_count, 1)
|
| 77 |
+
exact_acc = total_exact_accuracy / max(total_count, 1)
|
| 78 |
+
throughput = total_samples / duration
|
| 79 |
+
|
| 80 |
+
print(f"[{model_name}] Results:")
|
| 81 |
+
print(f" Duration: {duration:.2f}s")
|
| 82 |
+
print(f" Throughput: {throughput:.1f} samples/sec")
|
| 83 |
+
print(f" Token Acc: {acc*100:.2f}%")
|
| 84 |
+
print(f" Exact Acc: {exact_acc*100:.2f}%")
|
| 85 |
+
|
| 86 |
+
return {
|
| 87 |
+
"duration_s": duration,
|
| 88 |
+
"throughput": throughput,
|
| 89 |
+
"token_acc": acc,
|
| 90 |
+
"exact_acc": exact_acc
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
import matplotlib
|
| 94 |
+
matplotlib.use('Agg')
|
| 95 |
+
import matplotlib.pyplot as plt
|
| 96 |
+
from matplotlib.gridspec import GridSpec
|
| 97 |
+
import multiprocessing as mp
|
| 98 |
+
|
| 99 |
+
def run_evaluation(arch, model_cfg, state_dict, data_path, global_batch_size, gpu_id, result_queue):
|
| 100 |
+
device = torch.device(f"cuda:{gpu_id}")
|
| 101 |
+
torch.cuda.set_device(device)
|
| 102 |
+
|
| 103 |
+
# Needs to recreate dataloader per process
|
| 104 |
+
cfg_container = {
|
| 105 |
+
"arch": model_cfg,
|
| 106 |
+
"data_path": data_path,
|
| 107 |
+
"global_batch_size": global_batch_size,
|
| 108 |
+
"epochs": 1, "lr": 7e-5, "lr_min_ratio": 1.0, "lr_warmup_steps": 2000,
|
| 109 |
+
"weight_decay": 1.0, "beta1": 0.9, "beta2": 0.95,
|
| 110 |
+
"puzzle_emb_lr": 7e-5, "puzzle_emb_weight_decay": 1.0,
|
| 111 |
+
"seed": 0
|
| 112 |
+
}
|
| 113 |
+
config = PretrainConfig(**cfg_container)
|
| 114 |
+
eval_loader, eval_metadata = create_dataloader(
|
| 115 |
+
config, "test", test_set_mode=True, epochs_per_iter=1,
|
| 116 |
+
global_batch_size=config.global_batch_size, rank=0, world_size=1
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
model_cfg = model_cfg.copy()
|
| 120 |
+
model_cfg.update({
|
| 121 |
+
"batch_size": global_batch_size,
|
| 122 |
+
"vocab_size": eval_metadata.vocab_size,
|
| 123 |
+
"seq_len": eval_metadata.seq_len,
|
| 124 |
+
"num_puzzle_identifiers": eval_metadata.num_puzzle_identifiers,
|
| 125 |
+
"causal": False
|
| 126 |
+
})
|
| 127 |
+
|
| 128 |
+
model_name = "Traditional HRM" if arch == "baseline" else "Fused Tiered HRM"
|
| 129 |
+
model, _ = build_model(arch, model_cfg, device)
|
| 130 |
+
|
| 131 |
+
try:
|
| 132 |
+
model.load_state_dict(state_dict, strict=True)
|
| 133 |
+
except:
|
| 134 |
+
model.load_state_dict(state_dict, strict=False)
|
| 135 |
+
|
| 136 |
+
res = benchmark_model(model_name, model, eval_loader, eval_metadata, device)
|
| 137 |
+
res["arch"] = arch
|
| 138 |
+
result_queue.put(res)
|
| 139 |
+
|
| 140 |
+
def create_comparison_plots(base_res, tier_res, output_dir):
|
| 141 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 142 |
+
|
| 143 |
+
c_base, c_tier = "#4A90D9", "#E85D75"
|
| 144 |
+
bg, text, grid = "#1a1a2e", "#e0e0e0", "#333355"
|
| 145 |
+
|
| 146 |
+
plt.rcParams.update({
|
| 147 |
+
"figure.facecolor": bg, "axes.facecolor": "#16213e",
|
| 148 |
+
"axes.edgecolor": grid, "axes.labelcolor": text,
|
| 149 |
+
"text.color": text, "xtick.color": text, "ytick.color": text,
|
| 150 |
+
"grid.color": grid, "grid.alpha": 0.3,
|
| 151 |
+
"font.family": "sans-serif", "font.size": 11,
|
| 152 |
+
})
|
| 153 |
+
|
| 154 |
+
fig = plt.figure(figsize=(15, 6))
|
| 155 |
+
fig.suptitle("Sudoku Extreme: Traditional vs Fused Tiered HRM", fontsize=16, fontweight="bold", y=0.98)
|
| 156 |
+
gs = GridSpec(1, 3, figure=fig, wspace=0.3)
|
| 157 |
+
labels = ["Traditional (v1)", "Fused Tiered"]
|
| 158 |
+
|
| 159 |
+
def bar_plot(ax, title, ylabel, vals, fmt=".2f", is_percent=False):
|
| 160 |
+
bars = ax.bar(labels, vals, color=[c_base, c_tier], edgecolor="white", width=0.5)
|
| 161 |
+
ax.set_title(title, fontweight="bold")
|
| 162 |
+
ax.set_ylabel(ylabel)
|
| 163 |
+
for b, v in zip(bars, vals):
|
| 164 |
+
val_str = f"{v*100:{fmt}}%" if is_percent else f"{v:{fmt}}"
|
| 165 |
+
ax.text(b.get_x() + b.get_width()/2, b.get_height() * 1.02,
|
| 166 |
+
val_str, ha="center", fontsize=11, color=text, fontweight="bold")
|
| 167 |
+
ax.grid(axis="y")
|
| 168 |
+
if is_percent: ax.set_ylim(0, 1.1)
|
| 169 |
+
|
| 170 |
+
# 1. Throughput
|
| 171 |
+
bar_plot(fig.add_subplot(gs[0, 0]), "Inference Throughput", "Samples / Second",
|
| 172 |
+
[base_res["throughput"], tier_res["throughput"]], fmt=".1f")
|
| 173 |
+
|
| 174 |
+
# 2. Token Accuracy
|
| 175 |
+
bar_plot(fig.add_subplot(gs[0, 1]), "Token Accuracy", "Accuracy",
|
| 176 |
+
[base_res["token_acc"], tier_res["token_acc"]], is_percent=True)
|
| 177 |
+
|
| 178 |
+
# 3. Exact Match Accuracy
|
| 179 |
+
bar_plot(fig.add_subplot(gs[0, 2]), "Exact Puzzle Accuracy", "Accuracy",
|
| 180 |
+
[base_res["exact_acc"], tier_res["exact_acc"]], is_percent=True)
|
| 181 |
+
|
| 182 |
+
path = os.path.join(output_dir, "eval_fused_vs_v1_comparison.png")
|
| 183 |
+
fig.savefig(path, dpi=150, bbox_inches="tight")
|
| 184 |
+
plt.close()
|
| 185 |
+
print(f"\n Plot saved → {path}")
|
| 186 |
+
|
| 187 |
+
def main():
|
| 188 |
+
parser = argparse.ArgumentParser()
|
| 189 |
+
parser.add_argument("--weights", type=str, default="hf_upload/tiered_hrm_sram_dram/model.safetensors")
|
| 190 |
+
parser.add_argument("--output-dir", type=str, default="benchmark_results")
|
| 191 |
+
args = parser.parse_args()
|
| 192 |
+
|
| 193 |
+
# Base Configuration
|
| 194 |
+
model_cfg = {
|
| 195 |
+
"name": "hrm.hrm_tiered@HRM_Tiered",
|
| 196 |
+
"loss": {"name": "losses@ACTLossHead", "loss_type": "stablemax_cross_entropy"},
|
| 197 |
+
"hidden_size": 512, "num_heads": 8, "expansion": 4,
|
| 198 |
+
"H_layers": 4, "L_layers": 4, "H_cycles": 2, "L_cycles": 2,
|
| 199 |
+
"halt_max_steps": 16, "halt_exploration_prob": 0.1,
|
| 200 |
+
"pos_encodings": "rope", "puzzle_emb_ndim": 512,
|
| 201 |
+
"batch_size": 384, "vocab_size": 32, "seq_len": 81,
|
| 202 |
+
"num_puzzle_identifiers": 384, "causal": False
|
| 203 |
+
}
|
| 204 |
+
data_path = "data/sudoku-extreme-1k-aug-1000"
|
| 205 |
+
|
| 206 |
+
print(f"Loading weights from: {args.weights}")
|
| 207 |
+
try:
|
| 208 |
+
state_dict = load_file(args.weights)
|
| 209 |
+
except Exception as e:
|
| 210 |
+
print(f"Could not load as safetensors, falling back to torch.load... ({e})")
|
| 211 |
+
raw_state_dict = torch.load(args.weights, map_location="cpu", weights_only=True)
|
| 212 |
+
# Strip torch.compile prefix just in case as evaluate.py does
|
| 213 |
+
state_dict = {k.removeprefix("_orig_mod."): v for k, v in raw_state_dict.items()}
|
| 214 |
+
|
| 215 |
+
if "model.inner.embed_tokens.embedding_weight" not in state_dict and "inner.embed_tokens.embedding_weight" in state_dict:
|
| 216 |
+
state_dict = {f"model.{k}": v for k, v in state_dict.items()}
|
| 217 |
+
|
| 218 |
+
mp.set_start_method('spawn', force=True)
|
| 219 |
+
ctx = mp.get_context('spawn')
|
| 220 |
+
queue = ctx.Queue()
|
| 221 |
+
|
| 222 |
+
print("\nStarting Parallel Evaluation on 2 GPUs...")
|
| 223 |
+
print(" Traditional HRM -> GPU 0")
|
| 224 |
+
print(" Fused Tiered HRM -> GPU 1")
|
| 225 |
+
|
| 226 |
+
# Launch parallel processes
|
| 227 |
+
p1 = ctx.Process(target=run_evaluation, args=("baseline", model_cfg.copy(), state_dict, data_path, 384, 0, queue))
|
| 228 |
+
p2 = ctx.Process(target=run_evaluation, args=("tiered", model_cfg.copy(), state_dict, data_path, 384, 1, queue))
|
| 229 |
+
|
| 230 |
+
p1.start()
|
| 231 |
+
p2.start()
|
| 232 |
+
|
| 233 |
+
p1.join()
|
| 234 |
+
p2.join()
|
| 235 |
+
|
| 236 |
+
# Collect Results
|
| 237 |
+
results = {}
|
| 238 |
+
while not queue.empty():
|
| 239 |
+
res = queue.get()
|
| 240 |
+
results[res["arch"]] = res
|
| 241 |
+
|
| 242 |
+
if "baseline" in results and "tiered" in results:
|
| 243 |
+
base_res = results["baseline"]
|
| 244 |
+
tier_res = results["tiered"]
|
| 245 |
+
|
| 246 |
+
print("\n" + "="*50)
|
| 247 |
+
print(" FINAL COMPARISON OVERVIEW")
|
| 248 |
+
print("="*50)
|
| 249 |
+
print(f"Token Accuracy: Traditional {base_res['token_acc']*100:.2f}% vs Fused {tier_res['token_acc']*100:.2f}%")
|
| 250 |
+
print(f"Exact Accuracy: Traditional {base_res['exact_acc']*100:.2f}% vs Fused {tier_res['exact_acc']*100:.2f}%")
|
| 251 |
+
print(f"Throughput: Traditional {base_res['throughput']:.1f} samp/s vs Fused {tier_res['throughput']:.1f} samp/s")
|
| 252 |
+
print(f"Speedup Margin: {tier_res['throughput'] / base_res['throughput']:.2f}x")
|
| 253 |
+
|
| 254 |
+
# Plotting
|
| 255 |
+
create_comparison_plots(base_res, tier_res, args.output_dir)
|
| 256 |
+
else:
|
| 257 |
+
print("\nEvaluation failed. One or both models did not return results.")
|
| 258 |
+
|
| 259 |
+
if __name__ == "__main__":
|
| 260 |
+
main()
|
hf_upload_new/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
tags:
|
| 3 |
+
- hrm
|
| 4 |
+
- sudoku
|
| 5 |
+
- pytorch
|
| 6 |
+
- safetensors
|
| 7 |
+
---
|
| 8 |
+
# HRM Multi-GPU Trained Model
|
| 9 |
+
Latest checkpoint (step 26040) from the multi-GPU training run.
|
| 10 |
+
Accuracy: 60% Exact Match.
|
hf_upload_new/config.yaml
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
arch:
|
| 2 |
+
H_cycles: 2
|
| 3 |
+
H_layers: 4
|
| 4 |
+
L_cycles: 2
|
| 5 |
+
L_layers: 4
|
| 6 |
+
expansion: 4
|
| 7 |
+
halt_exploration_prob: 0.1
|
| 8 |
+
halt_max_steps: 16
|
| 9 |
+
hidden_size: 512
|
| 10 |
+
loss:
|
| 11 |
+
loss_type: stablemax_cross_entropy
|
| 12 |
+
name: losses@ACTLossHead
|
| 13 |
+
name: hrm.hrm_act_v1@HierarchicalReasoningModel_ACTV1
|
| 14 |
+
num_heads: 8
|
| 15 |
+
pos_encodings: rope
|
| 16 |
+
puzzle_emb_ndim: 512
|
| 17 |
+
beta1: 0.9
|
| 18 |
+
beta2: 0.95
|
| 19 |
+
checkpoint_every_eval: true
|
| 20 |
+
checkpoint_path: checkpoints/Sudoku-extreme-1k-aug-1000 ACT-torch/multi_gpu_new_run
|
| 21 |
+
data_path: data/sudoku-extreme-1k-aug-1000
|
| 22 |
+
epochs: 20000
|
| 23 |
+
eval_interval: 2000
|
| 24 |
+
eval_save_outputs: []
|
| 25 |
+
global_batch_size: 768
|
| 26 |
+
lr: 0.0001
|
| 27 |
+
lr_min_ratio: 1.0
|
| 28 |
+
lr_warmup_steps: 2000
|
| 29 |
+
project_name: Sudoku-extreme-1k-aug-1000 ACT-torch
|
| 30 |
+
puzzle_emb_lr: 0.0001
|
| 31 |
+
puzzle_emb_weight_decay: 1.0
|
| 32 |
+
run_name: multi_gpu_new_run
|
| 33 |
+
seed: 0
|
| 34 |
+
skip_eval: false
|
| 35 |
+
weight_decay: 1.0
|
hf_upload_new/hrm_act_v1.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Tuple, List, Dict, Optional
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
import math
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
from torch import nn
|
| 8 |
+
from pydantic import BaseModel
|
| 9 |
+
|
| 10 |
+
from models.common import trunc_normal_init_
|
| 11 |
+
from models.layers import rms_norm, SwiGLU, Attention, RotaryEmbedding, CosSin, CastedEmbedding, CastedLinear
|
| 12 |
+
from models.sparse_embedding import CastedSparseEmbedding
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class HierarchicalReasoningModel_ACTV1InnerCarry:
|
| 17 |
+
z_H: torch.Tensor
|
| 18 |
+
z_L: torch.Tensor
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass
|
| 22 |
+
class HierarchicalReasoningModel_ACTV1Carry:
|
| 23 |
+
inner_carry: HierarchicalReasoningModel_ACTV1InnerCarry
|
| 24 |
+
|
| 25 |
+
steps: torch.Tensor
|
| 26 |
+
halted: torch.Tensor
|
| 27 |
+
|
| 28 |
+
current_data: Dict[str, torch.Tensor]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class HierarchicalReasoningModel_ACTV1Config(BaseModel):
|
| 32 |
+
batch_size: int
|
| 33 |
+
seq_len: int
|
| 34 |
+
puzzle_emb_ndim: int = 0
|
| 35 |
+
num_puzzle_identifiers: int
|
| 36 |
+
vocab_size: int
|
| 37 |
+
|
| 38 |
+
H_cycles: int
|
| 39 |
+
L_cycles: int
|
| 40 |
+
|
| 41 |
+
H_layers: int
|
| 42 |
+
L_layers: int
|
| 43 |
+
|
| 44 |
+
# Transformer config
|
| 45 |
+
hidden_size: int
|
| 46 |
+
expansion: float
|
| 47 |
+
num_heads: int
|
| 48 |
+
pos_encodings: str
|
| 49 |
+
|
| 50 |
+
rms_norm_eps: float = 1e-5
|
| 51 |
+
rope_theta: float = 10000.0
|
| 52 |
+
|
| 53 |
+
# Halting Q-learning config
|
| 54 |
+
halt_max_steps: int
|
| 55 |
+
halt_exploration_prob: float
|
| 56 |
+
|
| 57 |
+
forward_dtype: str = "bfloat16"
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class HierarchicalReasoningModel_ACTV1Block(nn.Module):
|
| 61 |
+
def __init__(self, config: HierarchicalReasoningModel_ACTV1Config) -> None:
|
| 62 |
+
super().__init__()
|
| 63 |
+
|
| 64 |
+
self.self_attn = Attention(
|
| 65 |
+
hidden_size=config.hidden_size,
|
| 66 |
+
head_dim=config.hidden_size // config.num_heads,
|
| 67 |
+
num_heads=config.num_heads,
|
| 68 |
+
num_key_value_heads=config.num_heads,
|
| 69 |
+
causal=False
|
| 70 |
+
)
|
| 71 |
+
self.mlp = SwiGLU(
|
| 72 |
+
hidden_size=config.hidden_size,
|
| 73 |
+
expansion=config.expansion,
|
| 74 |
+
)
|
| 75 |
+
self.norm_eps = config.rms_norm_eps
|
| 76 |
+
|
| 77 |
+
def forward(self, cos_sin: CosSin, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 78 |
+
# Post Norm
|
| 79 |
+
# Self Attention
|
| 80 |
+
hidden_states = rms_norm(hidden_states + self.self_attn(cos_sin=cos_sin, hidden_states=hidden_states), variance_epsilon=self.norm_eps)
|
| 81 |
+
# Fully Connected
|
| 82 |
+
hidden_states = rms_norm(hidden_states + self.mlp(hidden_states), variance_epsilon=self.norm_eps)
|
| 83 |
+
return hidden_states
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class HierarchicalReasoningModel_ACTV1ReasoningModule(nn.Module):
|
| 87 |
+
def __init__(self, layers: List[HierarchicalReasoningModel_ACTV1Block]):
|
| 88 |
+
super().__init__()
|
| 89 |
+
|
| 90 |
+
self.layers = torch.nn.ModuleList(layers)
|
| 91 |
+
|
| 92 |
+
def forward(self, hidden_states: torch.Tensor, input_injection: torch.Tensor, **kwargs) -> torch.Tensor:
|
| 93 |
+
# Input injection (add)
|
| 94 |
+
hidden_states = hidden_states + input_injection
|
| 95 |
+
# Layers
|
| 96 |
+
for layer in self.layers:
|
| 97 |
+
hidden_states = layer(hidden_states=hidden_states, **kwargs)
|
| 98 |
+
|
| 99 |
+
return hidden_states
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class HierarchicalReasoningModel_ACTV1_Inner(nn.Module):
|
| 103 |
+
def __init__(self, config: HierarchicalReasoningModel_ACTV1Config) -> None:
|
| 104 |
+
super().__init__()
|
| 105 |
+
self.config = config
|
| 106 |
+
self.forward_dtype = getattr(torch, self.config.forward_dtype)
|
| 107 |
+
|
| 108 |
+
# I/O
|
| 109 |
+
self.embed_scale = math.sqrt(self.config.hidden_size)
|
| 110 |
+
embed_init_std = 1.0 / self.embed_scale
|
| 111 |
+
|
| 112 |
+
self.embed_tokens = CastedEmbedding(self.config.vocab_size, self.config.hidden_size, init_std=embed_init_std, cast_to=self.forward_dtype)
|
| 113 |
+
self.lm_head = CastedLinear(self.config.hidden_size, self.config.vocab_size, bias=False)
|
| 114 |
+
self.q_head = CastedLinear(self.config.hidden_size, 2, bias=True)
|
| 115 |
+
|
| 116 |
+
self.puzzle_emb_len = -(self.config.puzzle_emb_ndim // -self.config.hidden_size) # ceil div
|
| 117 |
+
if self.config.puzzle_emb_ndim > 0:
|
| 118 |
+
# Zero init puzzle embeddings
|
| 119 |
+
self.puzzle_emb = CastedSparseEmbedding(self.config.num_puzzle_identifiers, self.config.puzzle_emb_ndim,
|
| 120 |
+
batch_size=self.config.batch_size, init_std=0, cast_to=self.forward_dtype)
|
| 121 |
+
|
| 122 |
+
# LM Blocks
|
| 123 |
+
if self.config.pos_encodings == "rope":
|
| 124 |
+
self.rotary_emb = RotaryEmbedding(dim=self.config.hidden_size // self.config.num_heads,
|
| 125 |
+
max_position_embeddings=self.config.seq_len + self.puzzle_emb_len,
|
| 126 |
+
base=self.config.rope_theta)
|
| 127 |
+
elif self.config.pos_encodings == "learned":
|
| 128 |
+
self.embed_pos = CastedEmbedding(self.config.seq_len + self.puzzle_emb_len, self.config.hidden_size, init_std=embed_init_std, cast_to=self.forward_dtype)
|
| 129 |
+
else:
|
| 130 |
+
raise NotImplementedError()
|
| 131 |
+
|
| 132 |
+
# Reasoning Layers
|
| 133 |
+
self.H_level = HierarchicalReasoningModel_ACTV1ReasoningModule(layers=[HierarchicalReasoningModel_ACTV1Block(self.config) for _i in range(self.config.H_layers)])
|
| 134 |
+
self.L_level = HierarchicalReasoningModel_ACTV1ReasoningModule(layers=[HierarchicalReasoningModel_ACTV1Block(self.config) for _i in range(self.config.L_layers)])
|
| 135 |
+
|
| 136 |
+
# Initial states
|
| 137 |
+
self.H_init = nn.Buffer(trunc_normal_init_(torch.empty(self.config.hidden_size, dtype=self.forward_dtype), std=1), persistent=True)
|
| 138 |
+
self.L_init = nn.Buffer(trunc_normal_init_(torch.empty(self.config.hidden_size, dtype=self.forward_dtype), std=1), persistent=True)
|
| 139 |
+
|
| 140 |
+
# Q head special init
|
| 141 |
+
# Init Q to (almost) zero for faster learning during bootstrapping
|
| 142 |
+
with torch.no_grad():
|
| 143 |
+
self.q_head.weight.zero_()
|
| 144 |
+
self.q_head.bias.fill_(-5) # type: ignore
|
| 145 |
+
|
| 146 |
+
def _input_embeddings(self, input: torch.Tensor, puzzle_identifiers: torch.Tensor):
|
| 147 |
+
# Token embedding
|
| 148 |
+
embedding = self.embed_tokens(input.to(torch.int32))
|
| 149 |
+
|
| 150 |
+
# Puzzle embeddings
|
| 151 |
+
if self.config.puzzle_emb_ndim > 0:
|
| 152 |
+
puzzle_embedding = self.puzzle_emb(puzzle_identifiers)
|
| 153 |
+
|
| 154 |
+
pad_count = self.puzzle_emb_len * self.config.hidden_size - puzzle_embedding.shape[-1]
|
| 155 |
+
if pad_count > 0:
|
| 156 |
+
puzzle_embedding = F.pad(puzzle_embedding, (0, pad_count))
|
| 157 |
+
|
| 158 |
+
embedding = torch.cat((puzzle_embedding.view(-1, self.puzzle_emb_len, self.config.hidden_size), embedding), dim=-2)
|
| 159 |
+
|
| 160 |
+
# Position embeddings
|
| 161 |
+
if self.config.pos_encodings == "learned":
|
| 162 |
+
# scale by 1/sqrt(2) to maintain forward variance
|
| 163 |
+
embedding = 0.707106781 * (embedding + self.embed_pos.embedding_weight.to(self.forward_dtype))
|
| 164 |
+
|
| 165 |
+
# Scale
|
| 166 |
+
return self.embed_scale * embedding
|
| 167 |
+
|
| 168 |
+
def empty_carry(self, batch_size: int):
|
| 169 |
+
return HierarchicalReasoningModel_ACTV1InnerCarry(
|
| 170 |
+
z_H=torch.empty(batch_size, self.config.seq_len + self.puzzle_emb_len, self.config.hidden_size, dtype=self.forward_dtype),
|
| 171 |
+
z_L=torch.empty(batch_size, self.config.seq_len + self.puzzle_emb_len, self.config.hidden_size, dtype=self.forward_dtype),
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
def reset_carry(self, reset_flag: torch.Tensor, carry: HierarchicalReasoningModel_ACTV1InnerCarry):
|
| 175 |
+
return HierarchicalReasoningModel_ACTV1InnerCarry(
|
| 176 |
+
z_H=torch.where(reset_flag.view(-1, 1, 1), self.H_init, carry.z_H),
|
| 177 |
+
z_L=torch.where(reset_flag.view(-1, 1, 1), self.L_init, carry.z_L),
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
def forward(self, carry: HierarchicalReasoningModel_ACTV1InnerCarry, batch: Dict[str, torch.Tensor]) -> Tuple[HierarchicalReasoningModel_ACTV1InnerCarry, torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]:
|
| 181 |
+
seq_info = dict(
|
| 182 |
+
cos_sin=self.rotary_emb() if hasattr(self, "rotary_emb") else None,
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
# Input encoding
|
| 186 |
+
input_embeddings = self._input_embeddings(batch["inputs"], batch["puzzle_identifiers"])
|
| 187 |
+
|
| 188 |
+
# Forward iterations
|
| 189 |
+
with torch.no_grad():
|
| 190 |
+
z_H, z_L = carry.z_H, carry.z_L
|
| 191 |
+
|
| 192 |
+
for _H_step in range(self.config.H_cycles):
|
| 193 |
+
for _L_step in range(self.config.L_cycles):
|
| 194 |
+
if not ((_H_step == self.config.H_cycles - 1) and (_L_step == self.config.L_cycles - 1)):
|
| 195 |
+
z_L = self.L_level(z_L, z_H + input_embeddings, **seq_info)
|
| 196 |
+
|
| 197 |
+
if not (_H_step == self.config.H_cycles - 1):
|
| 198 |
+
z_H = self.H_level(z_H, z_L, **seq_info)
|
| 199 |
+
|
| 200 |
+
assert not z_H.requires_grad and not z_L.requires_grad
|
| 201 |
+
|
| 202 |
+
# 1-step grad
|
| 203 |
+
z_L = self.L_level(z_L, z_H + input_embeddings, **seq_info)
|
| 204 |
+
z_H = self.H_level(z_H, z_L, **seq_info)
|
| 205 |
+
|
| 206 |
+
# LM Outputs
|
| 207 |
+
new_carry = HierarchicalReasoningModel_ACTV1InnerCarry(z_H=z_H.detach(), z_L=z_L.detach()) # New carry no grad
|
| 208 |
+
output = self.lm_head(z_H)[:, self.puzzle_emb_len:]
|
| 209 |
+
|
| 210 |
+
# Q head
|
| 211 |
+
q_logits = self.q_head(z_H[:, 0]).to(torch.float32)
|
| 212 |
+
|
| 213 |
+
return new_carry, output, (q_logits[..., 0], q_logits[..., 1])
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
class HierarchicalReasoningModel_ACTV1(nn.Module):
|
| 217 |
+
"""ACT wrapper."""
|
| 218 |
+
|
| 219 |
+
def __init__(self, config_dict: dict):
|
| 220 |
+
super().__init__()
|
| 221 |
+
self.config = HierarchicalReasoningModel_ACTV1Config(**config_dict)
|
| 222 |
+
self.inner = HierarchicalReasoningModel_ACTV1_Inner(self.config)
|
| 223 |
+
|
| 224 |
+
@property
|
| 225 |
+
def puzzle_emb(self):
|
| 226 |
+
return self.inner.puzzle_emb
|
| 227 |
+
|
| 228 |
+
def initial_carry(self, batch: Dict[str, torch.Tensor]):
|
| 229 |
+
batch_size = batch["inputs"].shape[0]
|
| 230 |
+
|
| 231 |
+
return HierarchicalReasoningModel_ACTV1Carry(
|
| 232 |
+
inner_carry=self.inner.empty_carry(batch_size), # Empty is expected, it will be reseted in first pass as all sequences are halted.
|
| 233 |
+
|
| 234 |
+
steps=torch.zeros((batch_size, ), dtype=torch.int32),
|
| 235 |
+
halted=torch.ones((batch_size, ), dtype=torch.bool), # Default to halted
|
| 236 |
+
|
| 237 |
+
current_data={k: torch.empty_like(v) for k, v in batch.items()}
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
def forward(self, carry: HierarchicalReasoningModel_ACTV1Carry, batch: Dict[str, torch.Tensor]) -> Tuple[HierarchicalReasoningModel_ACTV1Carry, Dict[str, torch.Tensor]]:
|
| 241 |
+
# Update data, carry (removing halted sequences)
|
| 242 |
+
new_inner_carry = self.inner.reset_carry(carry.halted, carry.inner_carry)
|
| 243 |
+
|
| 244 |
+
new_steps = torch.where(carry.halted, 0, carry.steps)
|
| 245 |
+
|
| 246 |
+
new_current_data = {k: torch.where(carry.halted.view((-1, ) + (1, ) * (batch[k].ndim - 1)), batch[k], v) for k, v in carry.current_data.items()}
|
| 247 |
+
|
| 248 |
+
# Forward inner model
|
| 249 |
+
new_inner_carry, logits, (q_halt_logits, q_continue_logits) = self.inner(new_inner_carry, new_current_data)
|
| 250 |
+
|
| 251 |
+
outputs = {
|
| 252 |
+
"logits": logits,
|
| 253 |
+
"q_halt_logits": q_halt_logits,
|
| 254 |
+
"q_continue_logits": q_continue_logits
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
with torch.no_grad():
|
| 258 |
+
# Step
|
| 259 |
+
new_steps = new_steps + 1
|
| 260 |
+
is_last_step = new_steps >= self.config.halt_max_steps
|
| 261 |
+
|
| 262 |
+
halted = is_last_step
|
| 263 |
+
|
| 264 |
+
# if training, and ACT is enabled
|
| 265 |
+
if self.training and (self.config.halt_max_steps > 1):
|
| 266 |
+
# Halt signal
|
| 267 |
+
# NOTE: During evaluation, always use max steps, this is to guarantee the same halting steps inside a batch for batching purposes
|
| 268 |
+
halted = halted | (q_halt_logits > q_continue_logits)
|
| 269 |
+
|
| 270 |
+
# Exploration
|
| 271 |
+
min_halt_steps = (torch.rand_like(q_halt_logits) < self.config.halt_exploration_prob) * torch.randint_like(new_steps, low=2, high=self.config.halt_max_steps + 1)
|
| 272 |
+
|
| 273 |
+
halted = halted & (new_steps >= min_halt_steps)
|
| 274 |
+
|
| 275 |
+
# Compute target Q
|
| 276 |
+
# NOTE: No replay buffer and target networks for computing target Q-value.
|
| 277 |
+
# As batch_size is large, there're many parallel envs.
|
| 278 |
+
# Similar concept as PQN https://arxiv.org/abs/2407.04811
|
| 279 |
+
next_q_halt_logits, next_q_continue_logits = self.inner(new_inner_carry, new_current_data)[-1]
|
| 280 |
+
|
| 281 |
+
outputs["target_q_continue"] = torch.sigmoid(torch.where(is_last_step, next_q_halt_logits, torch.maximum(next_q_halt_logits, next_q_continue_logits)))
|
| 282 |
+
|
| 283 |
+
return HierarchicalReasoningModel_ACTV1Carry(new_inner_carry, new_steps, halted, new_current_data), outputs
|
hf_upload_new/losses.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Tuple, Dict, Sequence, Optional
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
from torch import nn
|
| 6 |
+
|
| 7 |
+
# This ID tells the code to "skip" or ignore certain data points (like padding)
|
| 8 |
+
IGNORE_LABEL_ID = -100
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def s(x, epsilon=1e-30):
|
| 12 |
+
"""
|
| 13 |
+
A helper function called 'StableMax' transformation.
|
| 14 |
+
Instead of standard Softmax, this is more 'stable' for the GPU.
|
| 15 |
+
If a number is negative, it squashes it; if positive, it slightly increases it.
|
| 16 |
+
"""
|
| 17 |
+
return torch.where(
|
| 18 |
+
x < 0,
|
| 19 |
+
1 / (1 - x + epsilon),
|
| 20 |
+
x + 1
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def log_stablemax(x, dim=-1):
|
| 25 |
+
"""
|
| 26 |
+
Calculates the 'Log-Probability'.
|
| 27 |
+
In AI, we prefer working with 'Logs' because adding small numbers is
|
| 28 |
+
much safer for computers than multiplying them (which can lead to 0).
|
| 29 |
+
"""
|
| 30 |
+
s_x = s(x)
|
| 31 |
+
return torch.log(s_x / torch.sum(s_x, dim=dim, keepdim=True))
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def stablemax_cross_entropy(logits, labels, ignore_index: int = -100):
|
| 35 |
+
"""
|
| 36 |
+
The main 'Comparison' function.
|
| 37 |
+
It compares the model's guess (logits) to the real answer (labels).
|
| 38 |
+
The higher the score, the worse the model's guess was.
|
| 39 |
+
"""
|
| 40 |
+
# 1. Convert the model's numbers into probabilities
|
| 41 |
+
logprobs = log_stablemax(logits.to(torch.float64), dim=-1)
|
| 42 |
+
|
| 43 |
+
# 2. figure out which cells actually have numbers (and aren't just empty padding)
|
| 44 |
+
valid_mask = labels != ignore_index
|
| 45 |
+
transformed_labels = torch.where(valid_mask, labels, 0)
|
| 46 |
+
|
| 47 |
+
# 3. Pick out the probability for the correct answer
|
| 48 |
+
prediction_logprobs = torch.gather(logprobs, index=transformed_labels.to(torch.long).unsqueeze(-1), dim=-1).squeeze(-1)
|
| 49 |
+
|
| 50 |
+
# 4. Return the negative of that probability (our 'Punishment' score)
|
| 51 |
+
return -torch.where(valid_mask, prediction_logprobs, 0)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def softmax_cross_entropy(logits, labels, ignore_index: int = -100):
|
| 55 |
+
# Cast logits to f32
|
| 56 |
+
# Flatten logits
|
| 57 |
+
return F.cross_entropy(logits.to(torch.float32).view(-1, logits.shape[-1]), labels.to(torch.long).view(-1), ignore_index=ignore_index, reduction="none").view(labels.shape)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class ACTLossHead(nn.Module):
|
| 61 |
+
"""
|
| 62 |
+
The 'Reasoning Manager'.
|
| 63 |
+
This class wraps the model and decides:
|
| 64 |
+
1. Did the model solve it?
|
| 65 |
+
2. Should it stop thinking?
|
| 66 |
+
3. How many steps did it take?
|
| 67 |
+
"""
|
| 68 |
+
def __init__(self, model: nn.Module, loss_type: str):
|
| 69 |
+
super().__init__()
|
| 70 |
+
self.model = model
|
| 71 |
+
self.loss_fn = globals()[loss_type]
|
| 72 |
+
|
| 73 |
+
def initial_carry(self, *args, **kwargs):
|
| 74 |
+
"""Prepares the model's 'Internal Memory' for a new puzzle."""
|
| 75 |
+
return self.model.initial_carry(*args, **kwargs) # type: ignore
|
| 76 |
+
|
| 77 |
+
def forward(
|
| 78 |
+
self,
|
| 79 |
+
return_keys: Sequence[str],
|
| 80 |
+
# Model args
|
| 81 |
+
**model_kwargs,
|
| 82 |
+
) -> Tuple[Any, torch.Tensor, Dict[str, torch.Tensor], Optional[Dict[str, torch.Tensor]], torch.Tensor]:
|
| 83 |
+
# Model logits
|
| 84 |
+
# B x SeqLen x D
|
| 85 |
+
new_carry, outputs = self.model(**model_kwargs)
|
| 86 |
+
labels = new_carry.current_data["labels"]
|
| 87 |
+
|
| 88 |
+
# 2. Check the results (without keeping track of 'learning math' here)
|
| 89 |
+
with torch.no_grad():
|
| 90 |
+
mask = labels != IGNORE_LABEL_ID
|
| 91 |
+
loss_counts = mask.sum(-1)
|
| 92 |
+
loss_divisor = loss_counts.clamp_min(1).unsqueeze(-1)
|
| 93 |
+
|
| 94 |
+
# Did the model pick the right numbers?
|
| 95 |
+
is_correct = mask & (torch.argmax(outputs["logits"], dim=-1) == labels)
|
| 96 |
+
# Is the WHOLE puzzle correct?
|
| 97 |
+
seq_is_correct = is_correct.sum(-1) == loss_counts
|
| 98 |
+
|
| 99 |
+
# --- Store metrics for the human to read(When it halts) ---
|
| 100 |
+
valid_metrics = new_carry.halted & (loss_counts > 0)
|
| 101 |
+
metrics = {
|
| 102 |
+
"count": valid_metrics.sum(),
|
| 103 |
+
"accuracy": torch.where(valid_metrics, (is_correct.to(torch.float32) / loss_divisor).sum(-1), 0).sum(),
|
| 104 |
+
"exact_accuracy": (valid_metrics & seq_is_correct).sum(),
|
| 105 |
+
"q_halt_accuracy": (valid_metrics & ((outputs["q_halt_logits"] >= 0) == seq_is_correct)).sum(),
|
| 106 |
+
"steps": torch.where(valid_metrics, new_carry.steps, 0).sum(),
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
# 3. Calculate the learning punishments (Losses)
|
| 110 |
+
# lm_loss: Punishment for getting digits wrong
|
| 111 |
+
lm_loss = (self.loss_fn(outputs["logits"], labels, ignore_index=IGNORE_LABEL_ID) / loss_divisor).sum()
|
| 112 |
+
|
| 113 |
+
# q_halt_loss: Punishment for stopping too early (or too late!)
|
| 114 |
+
q_halt_loss = F.binary_cross_entropy_with_logits(outputs["q_halt_logits"], seq_is_correct.to(outputs["q_halt_logits"].dtype), reduction="sum")
|
| 115 |
+
|
| 116 |
+
metrics.update({
|
| 117 |
+
"lm_loss": lm_loss.detach(),
|
| 118 |
+
"q_halt_loss": q_halt_loss.detach(),
|
| 119 |
+
})
|
| 120 |
+
|
| 121 |
+
# 4. Q-Continue Loss: Bootstrapping (The 'Look-Ahead' logic)
|
| 122 |
+
q_continue_loss = 0
|
| 123 |
+
if "target_q_continue" in outputs:
|
| 124 |
+
# Encourages the model to continue if thinking more will lead to a correct answer
|
| 125 |
+
q_continue_loss = F.binary_cross_entropy_with_logits(outputs["q_continue_logits"], outputs["target_q_continue"], reduction="sum")
|
| 126 |
+
metrics["q_continue_loss"] = q_continue_loss.detach()
|
| 127 |
+
|
| 128 |
+
# Filter outputs for return
|
| 129 |
+
detached_outputs = {k: outputs[k].detach() for k in return_keys if k in outputs}
|
| 130 |
+
|
| 131 |
+
return new_carry, lm_loss + 0.5 * (q_halt_loss + q_continue_loss), metrics, detached_outputs, new_carry.halted.all()
|
hf_upload_new/model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7e2976804165b5cca1f539d66c1726200b919bdb4845d768c73345e5c5892970
|
| 3 |
+
size 109109832
|
latency_plot_trained_model.py
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
latency_plot_trained_model.py — Load trained checkpoints, evaluate accuracy
|
| 4 |
+
on Sudoku test data, measure inference latency, and generate combined plots.
|
| 5 |
+
|
| 6 |
+
Usage:
|
| 7 |
+
source venv/bin/activate
|
| 8 |
+
python latency_plot_trained_model.py \
|
| 9 |
+
--baseline "checkpoints/Sudoku-extreme-1k-aug-1000 ACT-torch/HierarchicalReasoningModel_ACTV1 belligerent-squirrel/step_52080" \
|
| 10 |
+
--tiered "checkpoints/Sudoku-extreme-1k-aug-1000 ACT-torch/HRM_Tiered realistic-dalmatian/step_52080"
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import argparse
|
| 14 |
+
import json
|
| 15 |
+
import os
|
| 16 |
+
import sys
|
| 17 |
+
import yaml
|
| 18 |
+
|
| 19 |
+
# Disable torch.compile — avoids 10+ min compilation during eval
|
| 20 |
+
# and prevents inference_mode/compile conflicts
|
| 21 |
+
os.environ["DISABLE_COMPILE"] = "1"
|
| 22 |
+
|
| 23 |
+
import torch
|
| 24 |
+
import numpy as np
|
| 25 |
+
import matplotlib
|
| 26 |
+
matplotlib.use('Agg')
|
| 27 |
+
import matplotlib.pyplot as plt
|
| 28 |
+
from matplotlib.gridspec import GridSpec
|
| 29 |
+
|
| 30 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 31 |
+
|
| 32 |
+
from pretrain import PretrainConfig, init_train_state, evaluate, create_dataloader
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# ═══════════════════════════════════════════════════════════
|
| 36 |
+
# Load a trained checkpoint
|
| 37 |
+
# ═══════════════════════════════════════════════════════════
|
| 38 |
+
|
| 39 |
+
def load_trained_model(ckpt_path, device="cuda"):
|
| 40 |
+
"""Load checkpoint, return (train_state, config, eval_loader, latency_loader, eval_metadata).
|
| 41 |
+
|
| 42 |
+
Model hierarchy: torch.compile → ACTLossHead → ACTV1/HRM_Tiered → _Inner
|
| 43 |
+
Returns TWO eval loaders: one for accuracy (consumed by evaluate()), one for latency.
|
| 44 |
+
"""
|
| 45 |
+
ckpt_dir = os.path.dirname(ckpt_path)
|
| 46 |
+
config_path = os.path.join(ckpt_dir, "all_config.yaml")
|
| 47 |
+
|
| 48 |
+
with open(config_path, "r") as f:
|
| 49 |
+
content = f.read()
|
| 50 |
+
|
| 51 |
+
if "!!python/object" not in content:
|
| 52 |
+
raw = yaml.safe_load(content)
|
| 53 |
+
else:
|
| 54 |
+
# Fallback for the irreparably mangled tiered config dump
|
| 55 |
+
print(" [Warning] Tiered config YAML is mangled, using robust fallback.")
|
| 56 |
+
raw = {
|
| 57 |
+
"arch": {
|
| 58 |
+
"name": "hrm.hrm_tiered@HRM_Tiered",
|
| 59 |
+
"hidden_size": 512,
|
| 60 |
+
"num_heads": 8,
|
| 61 |
+
"puzzle_emb_ndim": 512,
|
| 62 |
+
"pos_encodings": "rope",
|
| 63 |
+
"H_layers": 4, "H_cycles": 2,
|
| 64 |
+
"L_layers": 4, "L_cycles": 2,
|
| 65 |
+
"expansion": 4,
|
| 66 |
+
"halt_max_steps": 16,
|
| 67 |
+
"halt_exploration_prob": 0.1,
|
| 68 |
+
"memory_tier": {"sram_capacity_mb": 48, "enable_tracking": True},
|
| 69 |
+
"loss": {"loss_type": "stablemax_cross_entropy", "name": "losses@ACTLossHead"}
|
| 70 |
+
},
|
| 71 |
+
"global_batch_size": 384,
|
| 72 |
+
"skip_eval": False,
|
| 73 |
+
"eval_save_outputs": [],
|
| 74 |
+
"checkpoint_path": ckpt_dir,
|
| 75 |
+
"epochs": 20000,
|
| 76 |
+
"lr": 7.0e-05,
|
| 77 |
+
"lr_min_ratio": 1.0,
|
| 78 |
+
"lr_warmup_steps": 2000,
|
| 79 |
+
"weight_decay": 1.0,
|
| 80 |
+
"beta1": 0.9,
|
| 81 |
+
"beta2": 0.95,
|
| 82 |
+
"puzzle_emb_lr": 7.0e-05,
|
| 83 |
+
"puzzle_emb_weight_decay": 1.0,
|
| 84 |
+
"eval_interval": 2000,
|
| 85 |
+
"data_path": "data/sudoku-extreme-1k-aug-1000",
|
| 86 |
+
"project_name": "Sudoku-extreme-1k-aug-1000 ACT-torch",
|
| 87 |
+
"run_name": "HRM_Tiered realistic-dalmatian",
|
| 88 |
+
"checkpoint_every_eval": True
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
config = PretrainConfig(**raw)
|
| 92 |
+
config.checkpoint_path = ckpt_dir
|
| 93 |
+
|
| 94 |
+
# Build dataloaders — need TWO because evaluate() consumes its loader
|
| 95 |
+
_, train_metadata = create_dataloader(
|
| 96 |
+
config, "train", test_set_mode=False, epochs_per_iter=1,
|
| 97 |
+
global_batch_size=config.global_batch_size, rank=0, world_size=1,
|
| 98 |
+
)
|
| 99 |
+
eval_loader, eval_metadata = create_dataloader(
|
| 100 |
+
config, "test", test_set_mode=True, epochs_per_iter=1,
|
| 101 |
+
global_batch_size=config.global_batch_size, rank=0, world_size=1,
|
| 102 |
+
)
|
| 103 |
+
latency_loader, _ = create_dataloader(
|
| 104 |
+
config, "test", test_set_mode=True, epochs_per_iter=1,
|
| 105 |
+
global_batch_size=config.global_batch_size, rank=0, world_size=1,
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
# Build model (torch.compile → ACTLossHead → model) and load weights
|
| 109 |
+
train_state = init_train_state(config, train_metadata, world_size=1)
|
| 110 |
+
try:
|
| 111 |
+
train_state.model.load_state_dict(
|
| 112 |
+
torch.load(ckpt_path, map_location=device, weights_only=True), assign=True
|
| 113 |
+
)
|
| 114 |
+
except Exception:
|
| 115 |
+
state = torch.load(ckpt_path, map_location=device, weights_only=True)
|
| 116 |
+
train_state.model.load_state_dict(
|
| 117 |
+
{k.removeprefix("_orig_mod."): v for k, v in state.items()}, assign=True
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
ckpt_name = os.path.basename(ckpt_path)
|
| 121 |
+
if ckpt_name.startswith("step_"):
|
| 122 |
+
train_state.step = int(ckpt_name.removeprefix("step_"))
|
| 123 |
+
|
| 124 |
+
train_state.model.eval()
|
| 125 |
+
return train_state, config, eval_loader, latency_loader, eval_metadata
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def unwrap_model(compiled_model):
|
| 129 |
+
"""Unwrap torch.compile + ACTLossHead to get the ACTV1/HRM_Tiered wrapper.
|
| 130 |
+
|
| 131 |
+
Hierarchy: OptimizedModule._orig_mod = ACTLossHead.model = ACTV1/HRM_Tiered
|
| 132 |
+
"""
|
| 133 |
+
model = compiled_model
|
| 134 |
+
# Unwrap torch.compile
|
| 135 |
+
if hasattr(model, '_orig_mod'):
|
| 136 |
+
model = model._orig_mod
|
| 137 |
+
# Unwrap ACTLossHead to get to the ACTV1/HRM_Tiered wrapper
|
| 138 |
+
if hasattr(model, 'model'):
|
| 139 |
+
model = model.model
|
| 140 |
+
return model
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
# ═══════════════════════════════════════════════════════════
|
| 144 |
+
# Evaluate accuracy on real Sudoku test set
|
| 145 |
+
# ═══════════════════════════════════════════════════════════
|
| 146 |
+
|
| 147 |
+
def eval_accuracy(config, train_state, eval_loader, eval_metadata, limit_batches=20):
|
| 148 |
+
"""Run the real evaluation on a subset of batches and return metrics dict."""
|
| 149 |
+
import itertools
|
| 150 |
+
class LimitedLoader:
|
| 151 |
+
def __init__(self, loader, limit):
|
| 152 |
+
self.loader = loader
|
| 153 |
+
self.limit = limit
|
| 154 |
+
def __iter__(self):
|
| 155 |
+
return itertools.islice(self.loader, self.limit)
|
| 156 |
+
|
| 157 |
+
limited_eval_loader = LimitedLoader(eval_loader, limit_batches)
|
| 158 |
+
metrics = evaluate(config, train_state, limited_eval_loader, eval_metadata, rank=0, world_size=1)
|
| 159 |
+
if metrics is None:
|
| 160 |
+
return {}
|
| 161 |
+
|
| 162 |
+
# Flatten and convert to floats (skip nested dicts / non-numeric)
|
| 163 |
+
result = {}
|
| 164 |
+
for k, v in metrics.items():
|
| 165 |
+
if isinstance(v, torch.Tensor):
|
| 166 |
+
result[k] = v.item()
|
| 167 |
+
elif isinstance(v, (int, float)):
|
| 168 |
+
result[k] = float(v)
|
| 169 |
+
elif isinstance(v, dict):
|
| 170 |
+
for kk, vv in v.items():
|
| 171 |
+
if isinstance(vv, torch.Tensor):
|
| 172 |
+
result[f"{k}/{kk}"] = vv.item()
|
| 173 |
+
elif isinstance(vv, (int, float)):
|
| 174 |
+
result[f"{k}/{kk}"] = float(vv)
|
| 175 |
+
return result
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
# ═══════════════════════════════════════════════════════════
|
| 179 |
+
# Measure inference latency on real data
|
| 180 |
+
# ═══════════════════════════════════════════════════════════
|
| 181 |
+
|
| 182 |
+
@torch.no_grad()
|
| 183 |
+
def measure_latency(compiled_model, eval_loader, device, warmup=3, iterations=20):
|
| 184 |
+
"""Time forward pass using the unwrapped model wrapper (ACTV1/HRM_Tiered).
|
| 185 |
+
|
| 186 |
+
The unwrapped model has:
|
| 187 |
+
- initial_carry(batch) → carry
|
| 188 |
+
- forward(carry, batch) → (new_carry, outputs)
|
| 189 |
+
"""
|
| 190 |
+
# Unwrap torch.compile + ACTLossHead
|
| 191 |
+
model = unwrap_model(compiled_model)
|
| 192 |
+
model.eval()
|
| 193 |
+
|
| 194 |
+
# Collect batches from the eval loader
|
| 195 |
+
batches = []
|
| 196 |
+
for set_name, batch, global_bs in eval_loader:
|
| 197 |
+
batch = {k: v.to(device) if isinstance(v, torch.Tensor) else v
|
| 198 |
+
for k, v in batch.items()}
|
| 199 |
+
batches.append(batch)
|
| 200 |
+
if len(batches) >= warmup + iterations:
|
| 201 |
+
break
|
| 202 |
+
|
| 203 |
+
if not batches:
|
| 204 |
+
return {"latency_ms": 0, "latency_std": 0, "throughput": 0}
|
| 205 |
+
|
| 206 |
+
# Helper: create carry and move all tensors to device
|
| 207 |
+
def make_carry(batch):
|
| 208 |
+
carry = model.initial_carry(batch)
|
| 209 |
+
carry.inner_carry.z_H = carry.inner_carry.z_H.to(device)
|
| 210 |
+
carry.inner_carry.z_L = carry.inner_carry.z_L.to(device)
|
| 211 |
+
carry.steps = carry.steps.to(device)
|
| 212 |
+
carry.halted = carry.halted.to(device)
|
| 213 |
+
carry.current_data = {k: v.to(device) for k, v in carry.current_data.items()}
|
| 214 |
+
return carry
|
| 215 |
+
|
| 216 |
+
# Warmup
|
| 217 |
+
for i in range(min(warmup, len(batches))):
|
| 218 |
+
batch = batches[i]
|
| 219 |
+
carry = make_carry(batch)
|
| 220 |
+
model(carry, batch)
|
| 221 |
+
torch.cuda.synchronize()
|
| 222 |
+
|
| 223 |
+
# Timed runs
|
| 224 |
+
latencies = []
|
| 225 |
+
bs_total = 0
|
| 226 |
+
n_iters = min(iterations, max(1, len(batches) - warmup))
|
| 227 |
+
for i in range(n_iters):
|
| 228 |
+
batch = batches[(warmup + i) % len(batches)]
|
| 229 |
+
carry = make_carry(batch)
|
| 230 |
+
|
| 231 |
+
start = torch.cuda.Event(enable_timing=True)
|
| 232 |
+
end = torch.cuda.Event(enable_timing=True)
|
| 233 |
+
start.record()
|
| 234 |
+
model(carry, batch)
|
| 235 |
+
end.record()
|
| 236 |
+
torch.cuda.synchronize()
|
| 237 |
+
latencies.append(start.elapsed_time(end))
|
| 238 |
+
bs_total += batch["inputs"].shape[0]
|
| 239 |
+
|
| 240 |
+
lat = np.array(latencies)
|
| 241 |
+
avg_bs = bs_total / len(latencies) if latencies else 1
|
| 242 |
+
return {
|
| 243 |
+
"latency_ms": float(np.mean(lat)),
|
| 244 |
+
"latency_std": float(np.std(lat)),
|
| 245 |
+
"throughput": float(avg_bs / (np.mean(lat) / 1000)) if lat.mean() > 0 else 0,
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
# ═══════════════════════════════════════════════════════════
|
| 250 |
+
# Generate combined plots
|
| 251 |
+
# ═══════════════════════════════════════════════════════════
|
| 252 |
+
|
| 253 |
+
def create_combined_plot(base_data, tier_data, output_dir):
|
| 254 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 255 |
+
|
| 256 |
+
c_base, c_tier = "#4A90D9", "#E85D75"
|
| 257 |
+
bg, text, grid = "#1a1a2e", "#e0e0e0", "#333355"
|
| 258 |
+
|
| 259 |
+
plt.rcParams.update({
|
| 260 |
+
"figure.facecolor": bg, "axes.facecolor": "#16213e",
|
| 261 |
+
"axes.edgecolor": grid, "axes.labelcolor": text,
|
| 262 |
+
"text.color": text, "xtick.color": text, "ytick.color": text,
|
| 263 |
+
"grid.color": grid, "grid.alpha": 0.3,
|
| 264 |
+
"font.family": "sans-serif", "font.size": 11,
|
| 265 |
+
})
|
| 266 |
+
|
| 267 |
+
fig = plt.figure(figsize=(18, 10))
|
| 268 |
+
fig.suptitle("HRM Trained Model Comparison: Baseline vs Tiered",
|
| 269 |
+
fontsize=18, fontweight="bold", y=0.98)
|
| 270 |
+
gs = GridSpec(2, 3, figure=fig, hspace=0.35, wspace=0.35)
|
| 271 |
+
labels = ["Baseline", "Tiered"]
|
| 272 |
+
|
| 273 |
+
def bar_ax(ax, title, ylabel, vals, fmt=".2f"):
|
| 274 |
+
bars = ax.bar(labels, vals, color=[c_base, c_tier],
|
| 275 |
+
edgecolor="white", linewidth=0.5, width=0.5)
|
| 276 |
+
ax.set_title(title, fontweight="bold")
|
| 277 |
+
ax.set_ylabel(ylabel)
|
| 278 |
+
for b, v in zip(bars, vals):
|
| 279 |
+
ax.text(b.get_x() + b.get_width()/2, b.get_height() * 1.02,
|
| 280 |
+
f"{v:{fmt}}", ha="center", fontsize=11, color=text)
|
| 281 |
+
ax.grid(axis="y")
|
| 282 |
+
|
| 283 |
+
# Extract metrics with safe defaults
|
| 284 |
+
def get_acc(data, key, normalize=True):
|
| 285 |
+
count = data["accuracy"].get("eval/count", 1)
|
| 286 |
+
val = data["accuracy"].get(key, 0)
|
| 287 |
+
if normalize and count > 0:
|
| 288 |
+
return val / count * 100
|
| 289 |
+
return val
|
| 290 |
+
|
| 291 |
+
# 1. Exact Accuracy
|
| 292 |
+
bar_ax(fig.add_subplot(gs[0, 0]), "Exact Accuracy (Sudoku)", "%",
|
| 293 |
+
[get_acc(base_data, "eval/exact_accuracy"),
|
| 294 |
+
get_acc(tier_data, "eval/exact_accuracy")])
|
| 295 |
+
|
| 296 |
+
# 2. Cell Accuracy
|
| 297 |
+
bar_ax(fig.add_subplot(gs[0, 1]), "Cell-level Accuracy", "%",
|
| 298 |
+
[get_acc(base_data, "eval/accuracy"),
|
| 299 |
+
get_acc(tier_data, "eval/accuracy")])
|
| 300 |
+
|
| 301 |
+
# 3. Avg Reasoning Steps
|
| 302 |
+
bar_ax(fig.add_subplot(gs[0, 2]), "Avg Reasoning Steps (ACT)", "steps",
|
| 303 |
+
[get_acc(base_data, "eval/steps"),
|
| 304 |
+
get_acc(tier_data, "eval/steps")], fmt=".1f")
|
| 305 |
+
|
| 306 |
+
# 4. Inference Latency
|
| 307 |
+
bar_ax(fig.add_subplot(gs[1, 0]), "Inference Latency", "ms",
|
| 308 |
+
[base_data["latency"]["latency_ms"],
|
| 309 |
+
tier_data["latency"]["latency_ms"]])
|
| 310 |
+
|
| 311 |
+
# 5. Throughput
|
| 312 |
+
bar_ax(fig.add_subplot(gs[1, 1]), "Throughput", "samples/sec",
|
| 313 |
+
[base_data["latency"]["throughput"],
|
| 314 |
+
tier_data["latency"]["throughput"]], fmt=".0f")
|
| 315 |
+
|
| 316 |
+
# 6. Summary
|
| 317 |
+
ax6 = fig.add_subplot(gs[1, 2])
|
| 318 |
+
speedup = (base_data["latency"]["latency_ms"] / tier_data["latency"]["latency_ms"]
|
| 319 |
+
if tier_data["latency"]["latency_ms"] > 0 else 0)
|
| 320 |
+
base_exact = get_acc(base_data, "eval/exact_accuracy")
|
| 321 |
+
tier_exact = get_acc(tier_data, "eval/exact_accuracy")
|
| 322 |
+
summary = (
|
| 323 |
+
f"Exact Accuracy:\n"
|
| 324 |
+
f" Baseline: {base_exact:.1f}%\n"
|
| 325 |
+
f" Tiered: {tier_exact:.1f}%\n\n"
|
| 326 |
+
f"Speedup: {speedup:.2f}x\n"
|
| 327 |
+
f"Throughput:\n"
|
| 328 |
+
f" {tier_data['latency']['throughput']:.0f} vs "
|
| 329 |
+
f"{base_data['latency']['throughput']:.0f}/s"
|
| 330 |
+
)
|
| 331 |
+
ax6.text(0.5, 0.5, summary, transform=ax6.transAxes,
|
| 332 |
+
ha="center", va="center", fontsize=13, fontfamily="monospace",
|
| 333 |
+
bbox=dict(boxstyle="round,pad=0.5", facecolor="#0f3460", alpha=0.8))
|
| 334 |
+
ax6.set_title("Summary", fontweight="bold")
|
| 335 |
+
ax6.axis("off")
|
| 336 |
+
|
| 337 |
+
path = os.path.join(output_dir, "trained_model_comparison.png")
|
| 338 |
+
fig.savefig(path, dpi=150, bbox_inches="tight")
|
| 339 |
+
plt.close()
|
| 340 |
+
print(f" Plot saved → {path}")
|
| 341 |
+
return path
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
# ═══════════════════════════════════════════════════════════
|
| 345 |
+
# Main
|
| 346 |
+
# ═══════════════════════════════════════════════════════════
|
| 347 |
+
|
| 348 |
+
def main():
|
| 349 |
+
parser = argparse.ArgumentParser(description="Evaluate trained Baseline vs Tiered HRM")
|
| 350 |
+
parser.add_argument("--baseline", type=str, required=True, help="Baseline checkpoint path")
|
| 351 |
+
parser.add_argument("--tiered", type=str, required=True, help="Tiered checkpoint path")
|
| 352 |
+
parser.add_argument("--latency-iters", type=int, default=20)
|
| 353 |
+
parser.add_argument("--output-dir", type=str, default="benchmark_results")
|
| 354 |
+
args = parser.parse_args()
|
| 355 |
+
|
| 356 |
+
device = "cuda"
|
| 357 |
+
|
| 358 |
+
print("=" * 64)
|
| 359 |
+
print(" Trained Model Comparison: Baseline vs Tiered")
|
| 360 |
+
print(f" Device: {torch.cuda.get_device_name(0)}")
|
| 361 |
+
print("=" * 64)
|
| 362 |
+
|
| 363 |
+
results = {}
|
| 364 |
+
|
| 365 |
+
# ── Baseline ──
|
| 366 |
+
print("\n [1/4] Loading Baseline checkpoint...")
|
| 367 |
+
base_state, base_cfg, base_eval_loader, base_lat_loader, base_eval_meta = load_trained_model(args.baseline, device)
|
| 368 |
+
n_params = sum(p.numel() for p in base_state.model.parameters()) / 1e6
|
| 369 |
+
print(f" Step: {base_state.step}, Params: {n_params:.1f}M")
|
| 370 |
+
|
| 371 |
+
print(" [2/4] Evaluating Baseline accuracy + latency...")
|
| 372 |
+
base_acc = eval_accuracy(base_cfg, base_state, base_eval_loader, base_eval_meta)
|
| 373 |
+
print(f" Accuracy metrics: {base_acc}")
|
| 374 |
+
base_lat = measure_latency(base_state.model, base_lat_loader, device,
|
| 375 |
+
iterations=args.latency_iters)
|
| 376 |
+
print(f" Latency: {base_lat['latency_ms']:.2f} ms ± {base_lat['latency_std']:.2f}")
|
| 377 |
+
results["baseline"] = {"accuracy": base_acc, "latency": base_lat}
|
| 378 |
+
|
| 379 |
+
# Free memory
|
| 380 |
+
del base_state, base_eval_loader, base_lat_loader
|
| 381 |
+
torch.cuda.empty_cache()
|
| 382 |
+
|
| 383 |
+
# ── Tiered ──
|
| 384 |
+
print("\n [3/4] Loading Tiered checkpoint...")
|
| 385 |
+
tier_state, tier_cfg, tier_eval_loader, tier_lat_loader, tier_eval_meta = load_trained_model(args.tiered, device)
|
| 386 |
+
n_params = sum(p.numel() for p in tier_state.model.parameters()) / 1e6
|
| 387 |
+
print(f" Step: {tier_state.step}, Params: {n_params:.1f}M")
|
| 388 |
+
|
| 389 |
+
print(" [4/4] Evaluating Tiered accuracy + latency...")
|
| 390 |
+
tier_acc = eval_accuracy(tier_cfg, tier_state, tier_eval_loader, tier_eval_meta)
|
| 391 |
+
print(f" Accuracy metrics: {tier_acc}")
|
| 392 |
+
tier_lat = measure_latency(tier_state.model, tier_lat_loader, device,
|
| 393 |
+
iterations=args.latency_iters)
|
| 394 |
+
print(f" Latency: {tier_lat['latency_ms']:.2f} ms ± {tier_lat['latency_std']:.2f}")
|
| 395 |
+
results["tiered"] = {"accuracy": tier_acc, "latency": tier_lat}
|
| 396 |
+
|
| 397 |
+
del tier_state, tier_eval_loader, tier_lat_loader
|
| 398 |
+
torch.cuda.empty_cache()
|
| 399 |
+
|
| 400 |
+
# ── Plots ──
|
| 401 |
+
print("\n Generating comparison plots...")
|
| 402 |
+
create_combined_plot(results["baseline"], results["tiered"], args.output_dir)
|
| 403 |
+
|
| 404 |
+
# ── Save JSON ──
|
| 405 |
+
json_path = os.path.join(args.output_dir, "trained_model_results.json")
|
| 406 |
+
os.makedirs(args.output_dir, exist_ok=True)
|
| 407 |
+
with open(json_path, "w") as f:
|
| 408 |
+
json.dump(results, f, indent=2, default=str)
|
| 409 |
+
print(f" Results saved → {json_path}")
|
| 410 |
+
|
| 411 |
+
print("\n" + "=" * 64)
|
| 412 |
+
print(" Done!")
|
| 413 |
+
print("=" * 64)
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
if __name__ == "__main__":
|
| 417 |
+
main()
|
models/common.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
from torch import nn
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def trunc_normal_init_(tensor: torch.Tensor, std: float = 1.0, lower: float = -2.0, upper: float = 2.0):
|
| 8 |
+
# NOTE: PyTorch nn.init.trunc_normal_ is not mathematically correct, the std dev is not actually the std dev of initialized tensor
|
| 9 |
+
# This function is a PyTorch version of jax truncated normal init (default init method in flax)
|
| 10 |
+
# https://github.com/jax-ml/jax/blob/main/jax/_src/random.py#L807-L848
|
| 11 |
+
# https://github.com/jax-ml/jax/blob/main/jax/_src/nn/initializers.py#L162-L199
|
| 12 |
+
|
| 13 |
+
with torch.no_grad():
|
| 14 |
+
if std == 0:
|
| 15 |
+
tensor.zero_()
|
| 16 |
+
else:
|
| 17 |
+
sqrt2 = math.sqrt(2)
|
| 18 |
+
a = math.erf(lower / sqrt2)
|
| 19 |
+
b = math.erf(upper / sqrt2)
|
| 20 |
+
z = (b - a) / 2
|
| 21 |
+
|
| 22 |
+
c = (2 * math.pi) ** -0.5
|
| 23 |
+
pdf_u = c * math.exp(-0.5 * lower ** 2)
|
| 24 |
+
pdf_l = c * math.exp(-0.5 * upper ** 2)
|
| 25 |
+
comp_std = std / math.sqrt(1 - (upper * pdf_u - lower * pdf_l) / z - ((pdf_u - pdf_l) / z) ** 2)
|
| 26 |
+
|
| 27 |
+
tensor.uniform_(a, b)
|
| 28 |
+
tensor.erfinv_()
|
| 29 |
+
tensor.mul_(sqrt2 * comp_std)
|
| 30 |
+
tensor.clip_(lower * comp_std, upper * comp_std)
|
| 31 |
+
|
| 32 |
+
return tensor
|
models/fused_hierarchical_scan.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Fused Hierarchical Scan for the Tiered Memory HRM.
|
| 3 |
+
|
| 4 |
+
This module implements the algorithm proposed in Section 4.2 of the project proposal:
|
| 5 |
+
'The Tiered Memory Hierarchical Scan'.
|
| 6 |
+
|
| 7 |
+
It replaces the Python-level `for` loop over L_cycles with a single OpenAI Triton
|
| 8 |
+
kernel invocation. The kernel loads the slow state (z_H) once into SRAM, executes
|
| 9 |
+
the entire fast state (z_L) recurrence for T steps completely within SRAM (registers/shared memory),
|
| 10 |
+
and only writes the final states back to HBM (DRAM) at the cycle boundary.
|
| 11 |
+
|
| 12 |
+
Algorithm Steps Implemented:
|
| 13 |
+
1. Block-wise Loading: Sequence is chunked by T (L_cycles).
|
| 14 |
+
2. DRAM -> SRAM Fetch: z_H and inputs loaded once.
|
| 15 |
+
3. SRAM-Resident Recurrence: T steps computed without global memory access.
|
| 16 |
+
4. Boundary Sync: High-level state updated at the end.
|
| 17 |
+
5. SRAM -> DRAM Write: Only final states are materialized.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
import triton
|
| 22 |
+
import triton.language as tl
|
| 23 |
+
import math
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@triton.jit
|
| 27 |
+
def _fused_hierarchical_scan_kernel(
|
| 28 |
+
# --- Data Pointers ---
|
| 29 |
+
X_ptr, # Inputs sequence [batch, seq_len, hidden_size]
|
| 30 |
+
Z_L_in_ptr, # Initial Fast State (L-level) [batch, hidden_size]
|
| 31 |
+
Z_H_in_ptr, # Initial Slow State (H-level) [batch, hidden_size]
|
| 32 |
+
Z_L_out_ptr, # Final Fast State Output [batch, hidden_size]
|
| 33 |
+
Z_H_out_ptr, # Final Slow State Output [batch, hidden_size]
|
| 34 |
+
# --- Matrix Weights (Simplified Recurrence for demonstration) ---
|
| 35 |
+
W_L_ptr, # Weights for L-level update [hidden_size, hidden_size]
|
| 36 |
+
W_H_ptr, # Weights for H-level update [hidden_size, hidden_size]
|
| 37 |
+
# --- Shapes & Strides ---
|
| 38 |
+
stride_batch_x, stride_seq_x, stride_dim_x,
|
| 39 |
+
T: tl.constexpr, # Number of fast steps per slow step (L_cycles)
|
| 40 |
+
HIDDEN_SIZE: tl.constexpr, # Hidden dimension (must fit in SRAM)
|
| 41 |
+
BLOCK_DIM: tl.constexpr, # Power of 2 for memory alignment
|
| 42 |
+
):
|
| 43 |
+
"""
|
| 44 |
+
SRAM-Resident Hierarchical Scan Kernel.
|
| 45 |
+
|
| 46 |
+
This kernel is designed so that the inner loop (t = 0...T) operates entirely
|
| 47 |
+
on registers (`z_L` and `z_H` variables inside the kernel).
|
| 48 |
+
"""
|
| 49 |
+
batch_idx = tl.program_id(0)
|
| 50 |
+
|
| 51 |
+
# Offsets for the hidden dimension
|
| 52 |
+
dim_offsets = tl.arange(0, BLOCK_DIM)
|
| 53 |
+
mask = dim_offsets < HIDDEN_SIZE
|
| 54 |
+
|
| 55 |
+
# -----------------------------------------------------------------
|
| 56 |
+
# Step 2: DRAM -> SRAM Fetch
|
| 57 |
+
# Load the initial states into registers (SRAM) for this batch
|
| 58 |
+
# -----------------------------------------------------------------
|
| 59 |
+
z_L = tl.load(Z_L_in_ptr + batch_idx * HIDDEN_SIZE + dim_offsets, mask=mask, other=0.0)
|
| 60 |
+
z_H = tl.load(Z_H_in_ptr + batch_idx * HIDDEN_SIZE + dim_offsets, mask=mask, other=0.0)
|
| 61 |
+
|
| 62 |
+
# Note: In a full transformer, W_L would be large. For a true SRAM-resident kernel,
|
| 63 |
+
# the parameter matrices must either be small enough to stay in shared memory,
|
| 64 |
+
# or the recurrence is element-wise (like Mamba/SSMs).
|
| 65 |
+
# Here we simulate the update using an element-wise/diagonal approximation
|
| 66 |
+
# of the weights to ensure it stays in SRAM, mimicking a Diagonal State Space Model.
|
| 67 |
+
w_L = tl.load(W_L_ptr + dim_offsets, mask=mask, other=0.0)
|
| 68 |
+
w_H = tl.load(W_H_ptr + dim_offsets, mask=mask, other=0.0)
|
| 69 |
+
|
| 70 |
+
# -----------------------------------------------------------------
|
| 71 |
+
# Step 3: SRAM-Resident Recurrence
|
| 72 |
+
# Execute the L-level loop entirely in SRAM without hitting DRAM
|
| 73 |
+
# -----------------------------------------------------------------
|
| 74 |
+
for t in range(T):
|
| 75 |
+
# 3a. Load input `x_t` for the current step (DRAM -> SRAM)
|
| 76 |
+
x_t_ptr = X_ptr + batch_idx * stride_batch_x + t * stride_seq_x + dim_offsets * stride_dim_x
|
| 77 |
+
x_t = tl.load(x_t_ptr, mask=mask, other=0.0)
|
| 78 |
+
|
| 79 |
+
# 3b. L-Level Update Rule: f_L(z_L, z_H, x)
|
| 80 |
+
# E.g., z_L = act(W_L * z_L + z_H + x_t)
|
| 81 |
+
# All computation here is Register-to-Register (Zero HBM cost)
|
| 82 |
+
pre_act = (w_L * z_L) + z_H + x_t
|
| 83 |
+
|
| 84 |
+
# Simple non-linearity (e.g., SiLU/Swish)
|
| 85 |
+
z_L = pre_act * tl.sigmoid(pre_act)
|
| 86 |
+
|
| 87 |
+
# -----------------------------------------------------------------
|
| 88 |
+
# Step 4: Boundary Sync
|
| 89 |
+
# Compute the new high-level state using the final z_L
|
| 90 |
+
# -----------------------------------------------------------------
|
| 91 |
+
# f_H(z_H, z_L)
|
| 92 |
+
pre_act_H = (w_H * z_H) + z_L
|
| 93 |
+
z_H_new = pre_act_H * tl.sigmoid(pre_act_H)
|
| 94 |
+
|
| 95 |
+
# -----------------------------------------------------------------
|
| 96 |
+
# Step 5: SRAM -> DRAM Write
|
| 97 |
+
# Write only the final chunk boundaries back to Global Memory (HBM)
|
| 98 |
+
# -----------------------------------------------------------------
|
| 99 |
+
tl.store(Z_L_out_ptr + batch_idx * HIDDEN_SIZE + dim_offsets, z_L, mask=mask)
|
| 100 |
+
tl.store(Z_H_out_ptr + batch_idx * HIDDEN_SIZE + dim_offsets, z_H_new, mask=mask)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
# =====================================================================
|
| 104 |
+
# PyTorch Wrapper
|
| 105 |
+
# =====================================================================
|
| 106 |
+
|
| 107 |
+
def next_power_of_2(n: int) -> int:
|
| 108 |
+
"""Returns the next power of 2 greater than or equal to n."""
|
| 109 |
+
return 1 if n == 0 else 2**(n - 1).bit_length()
|
| 110 |
+
|
| 111 |
+
class FusedHierarchicalScanLayer(torch.nn.Module):
|
| 112 |
+
"""
|
| 113 |
+
PyTorch module that encapsulates the Tiered Memory Hierarchical Scan.
|
| 114 |
+
|
| 115 |
+
This replaces the Python loop over T steps with the Triton kernel,
|
| 116 |
+
achieving the memory I/O reduction outlined in the paper.
|
| 117 |
+
"""
|
| 118 |
+
def __init__(self, hidden_size: int, T_steps: int):
|
| 119 |
+
super().__init__()
|
| 120 |
+
self.hidden_size = hidden_size
|
| 121 |
+
self.T = T_steps
|
| 122 |
+
|
| 123 |
+
# Diagonal weight matrices for the recurrence (SSM-style)
|
| 124 |
+
self.w_L = torch.nn.Parameter(torch.randn(hidden_size) / math.sqrt(hidden_size))
|
| 125 |
+
self.w_H = torch.nn.Parameter(torch.randn(hidden_size) / math.sqrt(hidden_size))
|
| 126 |
+
|
| 127 |
+
def forward(self, x_chunk: torch.Tensor, z_L: torch.Tensor, z_H: torch.Tensor):
|
| 128 |
+
"""
|
| 129 |
+
Args:
|
| 130 |
+
x_chunk: Tensor of shape (batch, T, hidden_size) containing the inputs for this chunk.
|
| 131 |
+
z_L: Tensor of shape (batch, hidden_size) containing the initial L-state.
|
| 132 |
+
z_H: Tensor of shape (batch, hidden_size) containing the initial H-state.
|
| 133 |
+
|
| 134 |
+
Returns:
|
| 135 |
+
z_L_new, z_H_new: The updated states after T steps.
|
| 136 |
+
"""
|
| 137 |
+
batch_size, seq_len, dim = x_chunk.shape
|
| 138 |
+
|
| 139 |
+
assert seq_len == self.T, f"Expected chunk of size T={self.T}, got {seq_len}"
|
| 140 |
+
assert dim == self.hidden_size, "Dimension mismatch"
|
| 141 |
+
assert z_L.shape == (batch_size, self.hidden_size)
|
| 142 |
+
assert z_H.shape == (batch_size, self.hidden_size)
|
| 143 |
+
assert x_chunk.is_contiguous()
|
| 144 |
+
|
| 145 |
+
# Allocate output tensors in HBM
|
| 146 |
+
z_L_out = torch.empty_like(z_L)
|
| 147 |
+
z_H_out = torch.empty_like(z_H)
|
| 148 |
+
|
| 149 |
+
# Determine Triton block size
|
| 150 |
+
BLOCK_DIM = next_power_of_2(self.hidden_size)
|
| 151 |
+
|
| 152 |
+
# 1D Grid: one program per batch element
|
| 153 |
+
grid = (batch_size,)
|
| 154 |
+
|
| 155 |
+
# Launch the fused kernel
|
| 156 |
+
_fused_hierarchical_scan_kernel[grid](
|
| 157 |
+
x_chunk, z_L, z_H, z_L_out, z_H_out,
|
| 158 |
+
self.w_L, self.w_H,
|
| 159 |
+
x_chunk.stride(0), x_chunk.stride(1), x_chunk.stride(2),
|
| 160 |
+
T=self.T,
|
| 161 |
+
HIDDEN_SIZE=self.hidden_size,
|
| 162 |
+
BLOCK_DIM=BLOCK_DIM
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
return z_L_out, z_H_out
|
| 166 |
+
|
| 167 |
+
# Example conceptual usage:
|
| 168 |
+
if __name__ == "__main__":
|
| 169 |
+
batch = 32
|
| 170 |
+
T = 8 # Number of L-cycles in one H-cycle
|
| 171 |
+
dim = 256
|
| 172 |
+
|
| 173 |
+
# Initialize the fused scan module
|
| 174 |
+
scanner = FusedHierarchicalScanLayer(hidden_size=dim, T_steps=T).cuda()
|
| 175 |
+
|
| 176 |
+
# Dummy data
|
| 177 |
+
x_chunk = torch.randn(batch, T, dim, device='cuda')
|
| 178 |
+
z_L_init = torch.randn(batch, dim, device='cuda')
|
| 179 |
+
z_H_init = torch.randn(batch, dim, device='cuda')
|
| 180 |
+
|
| 181 |
+
# Execute the fused scan!
|
| 182 |
+
z_L_final, z_H_final = scanner(x_chunk, z_L_init, z_H_init)
|
| 183 |
+
|
| 184 |
+
print(f"Successfully executed Tiered Memory Hierarchical Scan.")
|
| 185 |
+
print(f"z_L moved from {z_L_init.shape} -> {z_L_final.shape}")
|
| 186 |
+
print(f"z_H moved from {z_H_init.shape} -> {z_H_final.shape}")
|
models/hrm/hrm_act_v1.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Tuple, List, Dict, Optional
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
import math
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
from torch import nn
|
| 8 |
+
from pydantic import BaseModel
|
| 9 |
+
|
| 10 |
+
from models.common import trunc_normal_init_
|
| 11 |
+
from models.layers import rms_norm, SwiGLU, Attention, RotaryEmbedding, CosSin, CastedEmbedding, CastedLinear
|
| 12 |
+
from models.sparse_embedding import CastedSparseEmbedding
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class HierarchicalReasoningModel_ACTV1InnerCarry:
|
| 17 |
+
z_H: torch.Tensor
|
| 18 |
+
z_L: torch.Tensor
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass
|
| 22 |
+
class HierarchicalReasoningModel_ACTV1Carry:
|
| 23 |
+
inner_carry: HierarchicalReasoningModel_ACTV1InnerCarry
|
| 24 |
+
|
| 25 |
+
steps: torch.Tensor
|
| 26 |
+
halted: torch.Tensor
|
| 27 |
+
|
| 28 |
+
current_data: Dict[str, torch.Tensor]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class HierarchicalReasoningModel_ACTV1Config(BaseModel):
|
| 32 |
+
batch_size: int
|
| 33 |
+
seq_len: int
|
| 34 |
+
puzzle_emb_ndim: int = 0
|
| 35 |
+
num_puzzle_identifiers: int
|
| 36 |
+
vocab_size: int
|
| 37 |
+
|
| 38 |
+
H_cycles: int
|
| 39 |
+
L_cycles: int
|
| 40 |
+
|
| 41 |
+
H_layers: int
|
| 42 |
+
L_layers: int
|
| 43 |
+
|
| 44 |
+
# Transformer config
|
| 45 |
+
hidden_size: int
|
| 46 |
+
expansion: float
|
| 47 |
+
num_heads: int
|
| 48 |
+
pos_encodings: str
|
| 49 |
+
|
| 50 |
+
rms_norm_eps: float = 1e-5
|
| 51 |
+
rope_theta: float = 10000.0
|
| 52 |
+
|
| 53 |
+
# Halting Q-learning config
|
| 54 |
+
halt_max_steps: int
|
| 55 |
+
halt_exploration_prob: float
|
| 56 |
+
|
| 57 |
+
forward_dtype: str = "bfloat16"
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class HierarchicalReasoningModel_ACTV1Block(nn.Module):
|
| 61 |
+
def __init__(self, config: HierarchicalReasoningModel_ACTV1Config) -> None:
|
| 62 |
+
super().__init__()
|
| 63 |
+
|
| 64 |
+
self.self_attn = Attention(
|
| 65 |
+
hidden_size=config.hidden_size,
|
| 66 |
+
head_dim=config.hidden_size // config.num_heads,
|
| 67 |
+
num_heads=config.num_heads,
|
| 68 |
+
num_key_value_heads=config.num_heads,
|
| 69 |
+
causal=False
|
| 70 |
+
)
|
| 71 |
+
self.mlp = SwiGLU(
|
| 72 |
+
hidden_size=config.hidden_size,
|
| 73 |
+
expansion=config.expansion,
|
| 74 |
+
)
|
| 75 |
+
self.norm_eps = config.rms_norm_eps
|
| 76 |
+
|
| 77 |
+
def forward(self, cos_sin: CosSin, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 78 |
+
# Post Norm
|
| 79 |
+
# Self Attention
|
| 80 |
+
hidden_states = rms_norm(hidden_states + self.self_attn(cos_sin=cos_sin, hidden_states=hidden_states), variance_epsilon=self.norm_eps)
|
| 81 |
+
# Fully Connected
|
| 82 |
+
hidden_states = rms_norm(hidden_states + self.mlp(hidden_states), variance_epsilon=self.norm_eps)
|
| 83 |
+
return hidden_states
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class HierarchicalReasoningModel_ACTV1ReasoningModule(nn.Module):
|
| 87 |
+
def __init__(self, layers: List[HierarchicalReasoningModel_ACTV1Block]):
|
| 88 |
+
super().__init__()
|
| 89 |
+
|
| 90 |
+
self.layers = torch.nn.ModuleList(layers)
|
| 91 |
+
|
| 92 |
+
def forward(self, hidden_states: torch.Tensor, input_injection: torch.Tensor, **kwargs) -> torch.Tensor:
|
| 93 |
+
# Input injection (add)
|
| 94 |
+
hidden_states = hidden_states + input_injection
|
| 95 |
+
# Layers
|
| 96 |
+
for layer in self.layers:
|
| 97 |
+
hidden_states = layer(hidden_states=hidden_states, **kwargs)
|
| 98 |
+
|
| 99 |
+
return hidden_states
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class HierarchicalReasoningModel_ACTV1_Inner(nn.Module):
|
| 103 |
+
def __init__(self, config: HierarchicalReasoningModel_ACTV1Config) -> None:
|
| 104 |
+
super().__init__()
|
| 105 |
+
self.config = config
|
| 106 |
+
self.forward_dtype = getattr(torch, self.config.forward_dtype)
|
| 107 |
+
|
| 108 |
+
# I/O
|
| 109 |
+
self.embed_scale = math.sqrt(self.config.hidden_size)
|
| 110 |
+
embed_init_std = 1.0 / self.embed_scale
|
| 111 |
+
|
| 112 |
+
self.embed_tokens = CastedEmbedding(self.config.vocab_size, self.config.hidden_size, init_std=embed_init_std, cast_to=self.forward_dtype)
|
| 113 |
+
self.lm_head = CastedLinear(self.config.hidden_size, self.config.vocab_size, bias=False)
|
| 114 |
+
self.q_head = CastedLinear(self.config.hidden_size, 2, bias=True)
|
| 115 |
+
|
| 116 |
+
self.puzzle_emb_len = -(self.config.puzzle_emb_ndim // -self.config.hidden_size) # ceil div
|
| 117 |
+
if self.config.puzzle_emb_ndim > 0:
|
| 118 |
+
# Zero init puzzle embeddings
|
| 119 |
+
self.puzzle_emb = CastedSparseEmbedding(self.config.num_puzzle_identifiers, self.config.puzzle_emb_ndim,
|
| 120 |
+
batch_size=self.config.batch_size, init_std=0, cast_to=self.forward_dtype)
|
| 121 |
+
|
| 122 |
+
# LM Blocks
|
| 123 |
+
if self.config.pos_encodings == "rope":
|
| 124 |
+
self.rotary_emb = RotaryEmbedding(dim=self.config.hidden_size // self.config.num_heads,
|
| 125 |
+
max_position_embeddings=self.config.seq_len + self.puzzle_emb_len,
|
| 126 |
+
base=self.config.rope_theta)
|
| 127 |
+
elif self.config.pos_encodings == "learned":
|
| 128 |
+
self.embed_pos = CastedEmbedding(self.config.seq_len + self.puzzle_emb_len, self.config.hidden_size, init_std=embed_init_std, cast_to=self.forward_dtype)
|
| 129 |
+
else:
|
| 130 |
+
raise NotImplementedError()
|
| 131 |
+
|
| 132 |
+
# Reasoning Layers
|
| 133 |
+
self.H_level = HierarchicalReasoningModel_ACTV1ReasoningModule(layers=[HierarchicalReasoningModel_ACTV1Block(self.config) for _i in range(self.config.H_layers)])
|
| 134 |
+
self.L_level = HierarchicalReasoningModel_ACTV1ReasoningModule(layers=[HierarchicalReasoningModel_ACTV1Block(self.config) for _i in range(self.config.L_layers)])
|
| 135 |
+
|
| 136 |
+
# Initial states
|
| 137 |
+
self.H_init = nn.Buffer(trunc_normal_init_(torch.empty(self.config.hidden_size, dtype=self.forward_dtype), std=1), persistent=True)
|
| 138 |
+
self.L_init = nn.Buffer(trunc_normal_init_(torch.empty(self.config.hidden_size, dtype=self.forward_dtype), std=1), persistent=True)
|
| 139 |
+
|
| 140 |
+
# Q head special init
|
| 141 |
+
# Init Q to (almost) zero for faster learning during bootstrapping
|
| 142 |
+
with torch.no_grad():
|
| 143 |
+
self.q_head.weight.zero_()
|
| 144 |
+
self.q_head.bias.fill_(-5) # type: ignore
|
| 145 |
+
|
| 146 |
+
def _input_embeddings(self, input: torch.Tensor, puzzle_identifiers: torch.Tensor):
|
| 147 |
+
# Token embedding
|
| 148 |
+
embedding = self.embed_tokens(input.to(torch.int32))
|
| 149 |
+
|
| 150 |
+
# Puzzle embeddings
|
| 151 |
+
if self.config.puzzle_emb_ndim > 0:
|
| 152 |
+
puzzle_embedding = self.puzzle_emb(puzzle_identifiers)
|
| 153 |
+
|
| 154 |
+
pad_count = self.puzzle_emb_len * self.config.hidden_size - puzzle_embedding.shape[-1]
|
| 155 |
+
if pad_count > 0:
|
| 156 |
+
puzzle_embedding = F.pad(puzzle_embedding, (0, pad_count))
|
| 157 |
+
|
| 158 |
+
embedding = torch.cat((puzzle_embedding.view(-1, self.puzzle_emb_len, self.config.hidden_size), embedding), dim=-2)
|
| 159 |
+
|
| 160 |
+
# Position embeddings
|
| 161 |
+
if self.config.pos_encodings == "learned":
|
| 162 |
+
# scale by 1/sqrt(2) to maintain forward variance
|
| 163 |
+
embedding = 0.707106781 * (embedding + self.embed_pos.embedding_weight.to(self.forward_dtype))
|
| 164 |
+
|
| 165 |
+
# Scale
|
| 166 |
+
return self.embed_scale * embedding
|
| 167 |
+
|
| 168 |
+
def empty_carry(self, batch_size: int):
|
| 169 |
+
return HierarchicalReasoningModel_ACTV1InnerCarry(
|
| 170 |
+
z_H=torch.empty(batch_size, self.config.seq_len + self.puzzle_emb_len, self.config.hidden_size, dtype=self.forward_dtype),
|
| 171 |
+
z_L=torch.empty(batch_size, self.config.seq_len + self.puzzle_emb_len, self.config.hidden_size, dtype=self.forward_dtype),
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
def reset_carry(self, reset_flag: torch.Tensor, carry: HierarchicalReasoningModel_ACTV1InnerCarry):
|
| 175 |
+
return HierarchicalReasoningModel_ACTV1InnerCarry(
|
| 176 |
+
z_H=torch.where(reset_flag.view(-1, 1, 1), self.H_init, carry.z_H),
|
| 177 |
+
z_L=torch.where(reset_flag.view(-1, 1, 1), self.L_init, carry.z_L),
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
def forward(self, carry: HierarchicalReasoningModel_ACTV1InnerCarry, batch: Dict[str, torch.Tensor]) -> Tuple[HierarchicalReasoningModel_ACTV1InnerCarry, torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]:
|
| 181 |
+
seq_info = dict(
|
| 182 |
+
cos_sin=self.rotary_emb() if hasattr(self, "rotary_emb") else None,
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
# Input encoding
|
| 186 |
+
input_embeddings = self._input_embeddings(batch["inputs"], batch["puzzle_identifiers"])
|
| 187 |
+
|
| 188 |
+
# Forward iterations
|
| 189 |
+
with torch.no_grad():
|
| 190 |
+
z_H, z_L = carry.z_H, carry.z_L
|
| 191 |
+
|
| 192 |
+
for _H_step in range(self.config.H_cycles):
|
| 193 |
+
for _L_step in range(self.config.L_cycles):
|
| 194 |
+
if not ((_H_step == self.config.H_cycles - 1) and (_L_step == self.config.L_cycles - 1)):
|
| 195 |
+
z_L = self.L_level(z_L, z_H + input_embeddings, **seq_info)
|
| 196 |
+
|
| 197 |
+
if not (_H_step == self.config.H_cycles - 1):
|
| 198 |
+
z_H = self.H_level(z_H, z_L, **seq_info)
|
| 199 |
+
|
| 200 |
+
assert not z_H.requires_grad and not z_L.requires_grad
|
| 201 |
+
|
| 202 |
+
# 1-step grad
|
| 203 |
+
z_L = self.L_level(z_L, z_H + input_embeddings, **seq_info)
|
| 204 |
+
z_H = self.H_level(z_H, z_L, **seq_info)
|
| 205 |
+
|
| 206 |
+
# LM Outputs
|
| 207 |
+
new_carry = HierarchicalReasoningModel_ACTV1InnerCarry(z_H=z_H.detach(), z_L=z_L.detach()) # New carry no grad
|
| 208 |
+
output = self.lm_head(z_H)[:, self.puzzle_emb_len:]
|
| 209 |
+
|
| 210 |
+
# Q head
|
| 211 |
+
q_logits = self.q_head(z_H[:, 0]).to(torch.float32)
|
| 212 |
+
|
| 213 |
+
return new_carry, output, (q_logits[..., 0], q_logits[..., 1])
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
class HierarchicalReasoningModel_ACTV1(nn.Module):
|
| 217 |
+
"""ACT wrapper."""
|
| 218 |
+
|
| 219 |
+
def __init__(self, config_dict: dict):
|
| 220 |
+
super().__init__()
|
| 221 |
+
self.config = HierarchicalReasoningModel_ACTV1Config(**config_dict)
|
| 222 |
+
self.inner = HierarchicalReasoningModel_ACTV1_Inner(self.config)
|
| 223 |
+
|
| 224 |
+
@property
|
| 225 |
+
def puzzle_emb(self):
|
| 226 |
+
return self.inner.puzzle_emb
|
| 227 |
+
|
| 228 |
+
def initial_carry(self, batch: Dict[str, torch.Tensor]):
|
| 229 |
+
batch_size = batch["inputs"].shape[0]
|
| 230 |
+
|
| 231 |
+
return HierarchicalReasoningModel_ACTV1Carry(
|
| 232 |
+
inner_carry=self.inner.empty_carry(batch_size), # Empty is expected, it will be reseted in first pass as all sequences are halted.
|
| 233 |
+
|
| 234 |
+
steps=torch.zeros((batch_size, ), dtype=torch.int32),
|
| 235 |
+
halted=torch.ones((batch_size, ), dtype=torch.bool), # Default to halted
|
| 236 |
+
|
| 237 |
+
current_data={k: torch.empty_like(v) for k, v in batch.items()}
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
def forward(self, carry: HierarchicalReasoningModel_ACTV1Carry, batch: Dict[str, torch.Tensor]) -> Tuple[HierarchicalReasoningModel_ACTV1Carry, Dict[str, torch.Tensor]]:
|
| 241 |
+
# Update data, carry (removing halted sequences)
|
| 242 |
+
new_inner_carry = self.inner.reset_carry(carry.halted, carry.inner_carry)
|
| 243 |
+
|
| 244 |
+
new_steps = torch.where(carry.halted, 0, carry.steps)
|
| 245 |
+
|
| 246 |
+
new_current_data = {k: torch.where(carry.halted.view((-1, ) + (1, ) * (batch[k].ndim - 1)), batch[k], v) for k, v in carry.current_data.items()}
|
| 247 |
+
|
| 248 |
+
# Forward inner model
|
| 249 |
+
new_inner_carry, logits, (q_halt_logits, q_continue_logits) = self.inner(new_inner_carry, new_current_data)
|
| 250 |
+
|
| 251 |
+
outputs = {
|
| 252 |
+
"logits": logits,
|
| 253 |
+
"q_halt_logits": q_halt_logits,
|
| 254 |
+
"q_continue_logits": q_continue_logits
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
with torch.no_grad():
|
| 258 |
+
# Step
|
| 259 |
+
new_steps = new_steps + 1
|
| 260 |
+
is_last_step = new_steps >= self.config.halt_max_steps
|
| 261 |
+
|
| 262 |
+
halted = is_last_step
|
| 263 |
+
|
| 264 |
+
# if training, and ACT is enabled
|
| 265 |
+
if self.training and (self.config.halt_max_steps > 1):
|
| 266 |
+
# Halt signal
|
| 267 |
+
# NOTE: During evaluation, always use max steps, this is to guarantee the same halting steps inside a batch for batching purposes
|
| 268 |
+
halted = halted | (q_halt_logits > q_continue_logits)
|
| 269 |
+
|
| 270 |
+
# Exploration
|
| 271 |
+
min_halt_steps = (torch.rand_like(q_halt_logits) < self.config.halt_exploration_prob) * torch.randint_like(new_steps, low=2, high=self.config.halt_max_steps + 1)
|
| 272 |
+
|
| 273 |
+
halted = halted & (new_steps >= min_halt_steps)
|
| 274 |
+
|
| 275 |
+
# Compute target Q
|
| 276 |
+
# NOTE: No replay buffer and target networks for computing target Q-value.
|
| 277 |
+
# As batch_size is large, there're many parallel envs.
|
| 278 |
+
# Similar concept as PQN https://arxiv.org/abs/2407.04811
|
| 279 |
+
next_q_halt_logits, next_q_continue_logits = self.inner(new_inner_carry, new_current_data)[-1]
|
| 280 |
+
|
| 281 |
+
outputs["target_q_continue"] = torch.sigmoid(torch.where(is_last_step, next_q_halt_logits, torch.maximum(next_q_halt_logits, next_q_continue_logits)))
|
| 282 |
+
|
| 283 |
+
return HierarchicalReasoningModel_ACTV1Carry(new_inner_carry, new_steps, halted, new_current_data), outputs
|
models/hrm/hrm_tiered.py
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
HRM with SRAM/DRAM Memory Tiering — Triton-accelerated.
|
| 3 |
+
|
| 4 |
+
This module wraps the original HRM architecture and adds explicit
|
| 5 |
+
memory tier placement:
|
| 6 |
+
- L-level state (z_L): SRAM tier (Triton kernels keep in registers/shared mem)
|
| 7 |
+
- H-level state (z_H): DRAM tier (standard global memory path)
|
| 8 |
+
|
| 9 |
+
The Triton kernels in `triton_kernels.py` handle the fused operations,
|
| 10 |
+
and `MemoryTierManager` tracks all memory events for benchmarking.
|
| 11 |
+
|
| 12 |
+
EXPLANATION FOR BEGINNERS:
|
| 13 |
+
Think of this model like a Human brain solving a hard puzzle:
|
| 14 |
+
- SRAM (L-level): Your 'Short-term memory' or Scratchpad. It's super fast but tiny.
|
| 15 |
+
- DRAM (H-level): Your 'Long-term planning'. It's slower but holds the big picture.
|
| 16 |
+
|
| 17 |
+
The 'Triton' kernels are special pieces of code that talk directly to the GPU
|
| 18 |
+
hardware to make these two memory 'tiers' run as fast as possible.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
from typing import Tuple, List, Dict, Optional
|
| 23 |
+
from dataclasses import dataclass
|
| 24 |
+
import math
|
| 25 |
+
import time
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
import torch.nn.functional as F
|
| 29 |
+
from torch import nn
|
| 30 |
+
|
| 31 |
+
from models.common import trunc_normal_init_
|
| 32 |
+
from models.layers import (
|
| 33 |
+
rms_norm, SwiGLU, Attention, RotaryEmbedding,
|
| 34 |
+
CosSin, CastedEmbedding, CastedLinear,
|
| 35 |
+
)
|
| 36 |
+
from models.sparse_embedding import CastedSparseEmbedding
|
| 37 |
+
from models.memory_tier import MemoryTierManager
|
| 38 |
+
from models.triton_kernels import (
|
| 39 |
+
triton_rms_norm_residual_sram,
|
| 40 |
+
triton_rms_norm_residual_dram,
|
| 41 |
+
triton_swiglu_sram,
|
| 42 |
+
triton_state_transfer,
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
# Try importing the original HRM components for carry/config reuse
|
| 46 |
+
from models.hrm.hrm_act_v1 import (
|
| 47 |
+
HierarchicalReasoningModel_ACTV1Config,
|
| 48 |
+
HierarchicalReasoningModel_ACTV1InnerCarry,
|
| 49 |
+
HierarchicalReasoningModel_ACTV1Carry,
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# ===================================================================
|
| 54 |
+
# Tiered Transformer Block — uses Triton kernels per tier
|
| 55 |
+
# ===================================================================
|
| 56 |
+
|
| 57 |
+
class TieredBlock(nn.Module):
|
| 58 |
+
"""A transformer block that uses tier-specific Triton kernels.
|
| 59 |
+
|
| 60 |
+
A single 'Building Block' of the transformer.
|
| 61 |
+
It can live in either the 'fast' (SRAM) or 'slow' (DRAM) tier.
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
def __init__(
|
| 65 |
+
self,
|
| 66 |
+
config: HierarchicalReasoningModel_ACTV1Config,
|
| 67 |
+
tier: str, # 'sram' (fast) or 'dram' (slow)
|
| 68 |
+
):
|
| 69 |
+
super().__init__()
|
| 70 |
+
self.tier = tier
|
| 71 |
+
self.norm_eps = config.rms_norm_eps
|
| 72 |
+
|
| 73 |
+
# Standard transformer components: Attention (looking at other cells)
|
| 74 |
+
# and MLP (thinking about the current cell)
|
| 75 |
+
self.self_attn = Attention(
|
| 76 |
+
hidden_size=config.hidden_size,
|
| 77 |
+
head_dim=config.hidden_size // config.num_heads,
|
| 78 |
+
num_heads=config.num_heads,
|
| 79 |
+
num_key_value_heads=config.num_heads,
|
| 80 |
+
causal=False,
|
| 81 |
+
)
|
| 82 |
+
self.mlp = SwiGLU(
|
| 83 |
+
hidden_size=config.hidden_size,
|
| 84 |
+
expansion=config.expansion,
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
# Track whether we can use Triton (requires CUDA + contiguous bf16)
|
| 88 |
+
self._use_triton = torch.cuda.is_available()
|
| 89 |
+
|
| 90 |
+
def forward(
|
| 91 |
+
self,
|
| 92 |
+
cos_sin: CosSin,
|
| 93 |
+
hidden_states: torch.Tensor,
|
| 94 |
+
) -> torch.Tensor:
|
| 95 |
+
"""Processes the state through one layer of math."""
|
| 96 |
+
# 1. Attention: Which other cells in the puzzle are relevant right now?
|
| 97 |
+
attn_out = self.self_attn(cos_sin=cos_sin, hidden_states=hidden_states)
|
| 98 |
+
|
| 99 |
+
# 2. Use specialized Triton 'kernels' to speed up the memory access
|
| 100 |
+
if self._use_triton and hidden_states.is_contiguous():
|
| 101 |
+
if self.tier == 'sram':
|
| 102 |
+
hidden_states = triton_rms_norm_residual_sram(
|
| 103 |
+
attn_out, hidden_states, eps=self.norm_eps,
|
| 104 |
+
)
|
| 105 |
+
else:
|
| 106 |
+
hidden_states = triton_rms_norm_residual_dram(
|
| 107 |
+
attn_out, hidden_states, eps=self.norm_eps,
|
| 108 |
+
)
|
| 109 |
+
else:
|
| 110 |
+
# Fallback to PyTorch
|
| 111 |
+
hidden_states = rms_norm(
|
| 112 |
+
hidden_states + attn_out,
|
| 113 |
+
variance_epsilon=self.norm_eps,
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
# 3. MLP: Update the 'knowledge' of each cell based on the new info
|
| 117 |
+
mlp_out = self.mlp(hidden_states)
|
| 118 |
+
|
| 119 |
+
if self._use_triton and hidden_states.is_contiguous():
|
| 120 |
+
if self.tier == 'sram':
|
| 121 |
+
hidden_states = triton_rms_norm_residual_sram(
|
| 122 |
+
mlp_out, hidden_states, eps=self.norm_eps,
|
| 123 |
+
)
|
| 124 |
+
else:
|
| 125 |
+
hidden_states = triton_rms_norm_residual_dram(
|
| 126 |
+
mlp_out, hidden_states, eps=self.norm_eps,
|
| 127 |
+
)
|
| 128 |
+
else:
|
| 129 |
+
hidden_states = rms_norm(
|
| 130 |
+
hidden_states + mlp_out,
|
| 131 |
+
variance_epsilon=self.norm_eps,
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
return hidden_states
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
class TieredReasoningModule(nn.Module):
|
| 138 |
+
"""
|
| 139 |
+
A collection of layers that performs one 'thought' step.
|
| 140 |
+
This corresponds to the H-level or L-level of the model.
|
| 141 |
+
"""
|
| 142 |
+
|
| 143 |
+
def __init__(
|
| 144 |
+
self,
|
| 145 |
+
layers: List[TieredBlock],
|
| 146 |
+
tier: str,
|
| 147 |
+
):
|
| 148 |
+
super().__init__()
|
| 149 |
+
self.tier = tier
|
| 150 |
+
self.layers = nn.ModuleList(layers)
|
| 151 |
+
|
| 152 |
+
def forward(
|
| 153 |
+
self,
|
| 154 |
+
hidden_states: torch.Tensor,
|
| 155 |
+
input_injection: torch.Tensor,
|
| 156 |
+
**kwargs,
|
| 157 |
+
) -> torch.Tensor:
|
| 158 |
+
hidden_states = hidden_states + input_injection
|
| 159 |
+
|
| 160 |
+
for layer in self.layers:
|
| 161 |
+
hidden_states = layer(hidden_states=hidden_states, **kwargs)
|
| 162 |
+
|
| 163 |
+
return hidden_states
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
# ===================================================================
|
| 167 |
+
# Main Tiered HRM Model
|
| 168 |
+
# ===================================================================
|
| 169 |
+
|
| 170 |
+
class HRM_Tiered_Inner(nn.Module):
|
| 171 |
+
"""HRM inner model with SRAM/DRAM memory tiering.
|
| 172 |
+
|
| 173 |
+
- L-level: SRAM tier (Triton fused kernels, register-resident)
|
| 174 |
+
- H-level: DRAM tier (Triton DRAM-path kernels)
|
| 175 |
+
"""
|
| 176 |
+
|
| 177 |
+
def __init__(
|
| 178 |
+
self,
|
| 179 |
+
config: HierarchicalReasoningModel_ACTV1Config,
|
| 180 |
+
memory_manager: Optional[MemoryTierManager] = None,
|
| 181 |
+
):
|
| 182 |
+
super().__init__()
|
| 183 |
+
self.config = config
|
| 184 |
+
self.forward_dtype = getattr(torch, self.config.forward_dtype)
|
| 185 |
+
self.memory_manager = memory_manager
|
| 186 |
+
|
| 187 |
+
# --- Embedding: Converting digits (0-9) into high-dimensional vectors ---
|
| 188 |
+
self.embed_scale = math.sqrt(self.config.hidden_size)
|
| 189 |
+
embed_init_std = 1.0 / self.embed_scale
|
| 190 |
+
|
| 191 |
+
self.embed_tokens = CastedEmbedding(
|
| 192 |
+
self.config.vocab_size, self.config.hidden_size,
|
| 193 |
+
init_std=embed_init_std, cast_to=self.forward_dtype,
|
| 194 |
+
)
|
| 195 |
+
self.lm_head = CastedLinear(self.config.hidden_size, self.config.vocab_size, bias=False)
|
| 196 |
+
self.q_head = CastedLinear(self.config.hidden_size, 2, bias=True)
|
| 197 |
+
|
| 198 |
+
# --- Puzzle Embedding: Special memory for each specific puzzle ---
|
| 199 |
+
self.puzzle_emb_len = -(self.config.puzzle_emb_ndim // -self.config.hidden_size)
|
| 200 |
+
if self.config.puzzle_emb_ndim > 0:
|
| 201 |
+
self.puzzle_emb = CastedSparseEmbedding(
|
| 202 |
+
self.config.num_puzzle_identifiers, self.config.puzzle_emb_ndim,
|
| 203 |
+
batch_size=self.config.batch_size, init_std=0, cast_to=self.forward_dtype,
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
# ---- Position encodings ----
|
| 207 |
+
if self.config.pos_encodings == "rope":
|
| 208 |
+
self.rotary_emb = RotaryEmbedding(
|
| 209 |
+
dim=self.config.hidden_size // self.config.num_heads,
|
| 210 |
+
max_position_embeddings=self.config.seq_len + self.puzzle_emb_len,
|
| 211 |
+
base=self.config.rope_theta,
|
| 212 |
+
)
|
| 213 |
+
elif self.config.pos_encodings == "learned":
|
| 214 |
+
self.embed_pos = CastedEmbedding(
|
| 215 |
+
self.config.seq_len + self.puzzle_emb_len, self.config.hidden_size,
|
| 216 |
+
init_std=embed_init_std, cast_to=self.forward_dtype,
|
| 217 |
+
)
|
| 218 |
+
else:
|
| 219 |
+
raise NotImplementedError()
|
| 220 |
+
|
| 221 |
+
# ---- Tiered Reasoning Layers ----
|
| 222 |
+
# H-level: DRAM tier (slow updates)
|
| 223 |
+
self.H_level = TieredReasoningModule(
|
| 224 |
+
layers=[TieredBlock(self.config, tier='dram') for _ in range(self.config.H_layers)],
|
| 225 |
+
tier='dram',
|
| 226 |
+
)
|
| 227 |
+
# L-level: SRAM tier (fast updates)
|
| 228 |
+
self.L_level = TieredReasoningModule(
|
| 229 |
+
layers=[TieredBlock(self.config, tier='sram') for _ in range(self.config.L_layers)],
|
| 230 |
+
tier='sram',
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
# ---- Initial states ----
|
| 234 |
+
self.H_init = nn.Buffer(
|
| 235 |
+
trunc_normal_init_(torch.empty(self.config.hidden_size, dtype=self.forward_dtype), std=1),
|
| 236 |
+
persistent=True,
|
| 237 |
+
)
|
| 238 |
+
self.L_init = nn.Buffer(
|
| 239 |
+
trunc_normal_init_(torch.empty(self.config.hidden_size, dtype=self.forward_dtype), std=1),
|
| 240 |
+
persistent=True,
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
# Q head special init
|
| 244 |
+
with torch.no_grad():
|
| 245 |
+
self.q_head.weight.zero_()
|
| 246 |
+
self.q_head.bias.fill_(-5)
|
| 247 |
+
|
| 248 |
+
# ---- Timing storage for benchmarks ----
|
| 249 |
+
self._timing: Dict[str, List[float]] = {
|
| 250 |
+
'L_forward_us': [],
|
| 251 |
+
'H_forward_us': [],
|
| 252 |
+
'H_L_transfer_us': [],
|
| 253 |
+
'L_H_transfer_us': [],
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
def _input_embeddings(self, input: torch.Tensor, puzzle_identifiers: torch.Tensor):
|
| 257 |
+
embedding = self.embed_tokens(input.to(torch.int32))
|
| 258 |
+
|
| 259 |
+
if self.config.puzzle_emb_ndim > 0:
|
| 260 |
+
puzzle_embedding = self.puzzle_emb(puzzle_identifiers)
|
| 261 |
+
pad_count = self.puzzle_emb_len * self.config.hidden_size - puzzle_embedding.shape[-1]
|
| 262 |
+
if pad_count > 0:
|
| 263 |
+
puzzle_embedding = F.pad(puzzle_embedding, (0, pad_count))
|
| 264 |
+
embedding = torch.cat(
|
| 265 |
+
(puzzle_embedding.view(-1, self.puzzle_emb_len, self.config.hidden_size), embedding),
|
| 266 |
+
dim=-2,
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
if self.config.pos_encodings == "learned":
|
| 270 |
+
embedding = 0.707106781 * (embedding + self.embed_pos.embedding_weight.to(self.forward_dtype))
|
| 271 |
+
|
| 272 |
+
return self.embed_scale * embedding
|
| 273 |
+
|
| 274 |
+
def empty_carry(self, batch_size: int):
|
| 275 |
+
return HierarchicalReasoningModel_ACTV1InnerCarry(
|
| 276 |
+
z_H=torch.empty(
|
| 277 |
+
batch_size, self.config.seq_len + self.puzzle_emb_len,
|
| 278 |
+
self.config.hidden_size, dtype=self.forward_dtype,
|
| 279 |
+
),
|
| 280 |
+
z_L=torch.empty(
|
| 281 |
+
batch_size, self.config.seq_len + self.puzzle_emb_len,
|
| 282 |
+
self.config.hidden_size, dtype=self.forward_dtype,
|
| 283 |
+
),
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
def reset_carry(self, reset_flag: torch.Tensor, carry: HierarchicalReasoningModel_ACTV1InnerCarry):
|
| 287 |
+
return HierarchicalReasoningModel_ACTV1InnerCarry(
|
| 288 |
+
z_H=torch.where(reset_flag.view(-1, 1, 1), self.H_init, carry.z_H),
|
| 289 |
+
z_L=torch.where(reset_flag.view(-1, 1, 1), self.L_init, carry.z_L),
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
@torch.compiler.disable
|
| 293 |
+
def _gpu_timer_start(self):
|
| 294 |
+
if torch.cuda.is_available():
|
| 295 |
+
start = torch.cuda.Event(enable_timing=True)
|
| 296 |
+
start.record()
|
| 297 |
+
return start
|
| 298 |
+
return time.perf_counter()
|
| 299 |
+
|
| 300 |
+
@torch.compiler.disable
|
| 301 |
+
def _gpu_timer_end(self, start) -> float:
|
| 302 |
+
if isinstance(start, torch.cuda.Event):
|
| 303 |
+
end = torch.cuda.Event(enable_timing=True)
|
| 304 |
+
end.record()
|
| 305 |
+
torch.cuda.synchronize()
|
| 306 |
+
return start.elapsed_time(end) * 1000 # ms → μs
|
| 307 |
+
return (time.perf_counter() - start) * 1e6
|
| 308 |
+
|
| 309 |
+
@torch.compiler.disable
|
| 310 |
+
def _timed_forward(self, module, hidden_states, injection, seq_info, tier_key, memory_ctx=None):
|
| 311 |
+
"""Run a reasoning module forward with optional timing (disabled under torch.compile)."""
|
| 312 |
+
t0 = self._gpu_timer_start() if not self.training else None
|
| 313 |
+
|
| 314 |
+
if memory_ctx is not None:
|
| 315 |
+
with memory_ctx:
|
| 316 |
+
out = module(hidden_states, injection, **seq_info)
|
| 317 |
+
else:
|
| 318 |
+
out = module(hidden_states, injection, **seq_info)
|
| 319 |
+
|
| 320 |
+
if t0 is not None:
|
| 321 |
+
self._timing[tier_key].append(self._gpu_timer_end(t0))
|
| 322 |
+
return out
|
| 323 |
+
|
| 324 |
+
def forward(
|
| 325 |
+
self,
|
| 326 |
+
carry: HierarchicalReasoningModel_ACTV1InnerCarry,
|
| 327 |
+
batch: Dict[str, torch.Tensor],
|
| 328 |
+
) -> Tuple[HierarchicalReasoningModel_ACTV1InnerCarry, torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]:
|
| 329 |
+
|
| 330 |
+
seq_info = dict(
|
| 331 |
+
cos_sin=self.rotary_emb() if hasattr(self, "rotary_emb") else None,
|
| 332 |
+
)
|
| 333 |
+
|
| 334 |
+
input_embeddings = self._input_embeddings(batch["inputs"], batch["puzzle_identifiers"])
|
| 335 |
+
|
| 336 |
+
# ---- Hierarchical reasoning with tiered memory ----
|
| 337 |
+
with torch.no_grad():
|
| 338 |
+
z_H, z_L = carry.z_H, carry.z_L
|
| 339 |
+
|
| 340 |
+
for _H_step in range(self.config.H_cycles):
|
| 341 |
+
for _L_step in range(self.config.L_cycles):
|
| 342 |
+
if not ((_H_step == self.config.H_cycles - 1) and (_L_step == self.config.L_cycles - 1)):
|
| 343 |
+
# L-level: SRAM tier (fast path)
|
| 344 |
+
sram_ctx = self.memory_manager.sram_context() if self.memory_manager else None
|
| 345 |
+
z_L = self._timed_forward(
|
| 346 |
+
self.L_level, z_L, z_H + input_embeddings,
|
| 347 |
+
seq_info, 'L_forward_us', sram_ctx,
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
if not (_H_step == self.config.H_cycles - 1):
|
| 351 |
+
# H-level: DRAM tier (slow path)
|
| 352 |
+
z_L_for_H = z_L.contiguous()
|
| 353 |
+
dram_ctx = self.memory_manager.dram_context() if self.memory_manager else None
|
| 354 |
+
z_H = self._timed_forward(
|
| 355 |
+
self.H_level, z_H, z_L_for_H,
|
| 356 |
+
seq_info, 'H_forward_us', dram_ctx,
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
assert not z_H.requires_grad and not z_L.requires_grad
|
| 360 |
+
|
| 361 |
+
# 1-step grad (with tier-aware kernels)
|
| 362 |
+
z_L = self.L_level(z_L, z_H + input_embeddings, **seq_info)
|
| 363 |
+
z_H = self.H_level(z_H, z_L, **seq_info)
|
| 364 |
+
|
| 365 |
+
new_carry = HierarchicalReasoningModel_ACTV1InnerCarry(z_H=z_H.detach(), z_L=z_L.detach())
|
| 366 |
+
output = self.lm_head(z_H)[:, self.puzzle_emb_len:]
|
| 367 |
+
q_logits = self.q_head(z_H[:, 0]).to(torch.float32)
|
| 368 |
+
|
| 369 |
+
return new_carry, output, (q_logits[..., 0], q_logits[..., 1])
|
| 370 |
+
|
| 371 |
+
def get_timing_stats(self) -> Dict[str, Dict[str, float]]:
|
| 372 |
+
"""Get aggregated timing statistics for benchmarking."""
|
| 373 |
+
stats = {}
|
| 374 |
+
for key, values in self._timing.items():
|
| 375 |
+
if values:
|
| 376 |
+
stats[key] = {
|
| 377 |
+
'mean_us': sum(values) / len(values),
|
| 378 |
+
'min_us': min(values),
|
| 379 |
+
'max_us': max(values),
|
| 380 |
+
'count': len(values),
|
| 381 |
+
'total_us': sum(values),
|
| 382 |
+
}
|
| 383 |
+
else:
|
| 384 |
+
stats[key] = {'mean_us': 0, 'min_us': 0, 'max_us': 0, 'count': 0, 'total_us': 0}
|
| 385 |
+
return stats
|
| 386 |
+
|
| 387 |
+
def reset_timing(self):
|
| 388 |
+
for key in self._timing:
|
| 389 |
+
self._timing[key] = []
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
# ===================================================================
|
| 393 |
+
# ACT Wrapper (same logic as original, uses tiered inner model)
|
| 394 |
+
# ===================================================================
|
| 395 |
+
|
| 396 |
+
class HRM_Tiered(nn.Module):
|
| 397 |
+
"""ACT wrapper for the memory-tiered HRM model."""
|
| 398 |
+
|
| 399 |
+
def __init__(self, config_dict: dict, memory_manager: Optional[MemoryTierManager] = None):
|
| 400 |
+
super().__init__()
|
| 401 |
+
self.config = HierarchicalReasoningModel_ACTV1Config(**config_dict)
|
| 402 |
+
self.memory_manager = memory_manager
|
| 403 |
+
self.inner = HRM_Tiered_Inner(self.config, memory_manager=memory_manager)
|
| 404 |
+
|
| 405 |
+
@property
|
| 406 |
+
def puzzle_emb(self):
|
| 407 |
+
return self.inner.puzzle_emb
|
| 408 |
+
|
| 409 |
+
def initial_carry(self, batch: Dict[str, torch.Tensor]):
|
| 410 |
+
batch_size = batch["inputs"].shape[0]
|
| 411 |
+
return HierarchicalReasoningModel_ACTV1Carry(
|
| 412 |
+
inner_carry=self.inner.empty_carry(batch_size),
|
| 413 |
+
steps=torch.zeros((batch_size,), dtype=torch.int32),
|
| 414 |
+
halted=torch.ones((batch_size,), dtype=torch.bool),
|
| 415 |
+
current_data={k: torch.empty_like(v) for k, v in batch.items()},
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
def forward(
|
| 419 |
+
self,
|
| 420 |
+
carry: HierarchicalReasoningModel_ACTV1Carry,
|
| 421 |
+
batch: Dict[str, torch.Tensor],
|
| 422 |
+
) -> Tuple[HierarchicalReasoningModel_ACTV1Carry, Dict[str, torch.Tensor]]:
|
| 423 |
+
new_inner_carry = self.inner.reset_carry(carry.halted, carry.inner_carry)
|
| 424 |
+
new_steps = torch.where(carry.halted, 0, carry.steps)
|
| 425 |
+
new_current_data = {
|
| 426 |
+
k: torch.where(carry.halted.view((-1,) + (1,) * (batch[k].ndim - 1)), batch[k], v)
|
| 427 |
+
for k, v in carry.current_data.items()
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
new_inner_carry, logits, (q_halt_logits, q_continue_logits) = self.inner(new_inner_carry, new_current_data)
|
| 431 |
+
|
| 432 |
+
outputs = {
|
| 433 |
+
"logits": logits,
|
| 434 |
+
"q_halt_logits": q_halt_logits,
|
| 435 |
+
"q_continue_logits": q_continue_logits,
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
with torch.no_grad():
|
| 439 |
+
new_steps = new_steps + 1
|
| 440 |
+
is_last_step = new_steps >= self.config.halt_max_steps
|
| 441 |
+
halted = is_last_step
|
| 442 |
+
|
| 443 |
+
if self.training and (self.config.halt_max_steps > 1):
|
| 444 |
+
halted = halted | (q_halt_logits > q_continue_logits)
|
| 445 |
+
min_halt_steps = (
|
| 446 |
+
(torch.rand_like(q_halt_logits) < self.config.halt_exploration_prob)
|
| 447 |
+
* torch.randint_like(new_steps, low=2, high=self.config.halt_max_steps + 1)
|
| 448 |
+
)
|
| 449 |
+
halted = halted & (new_steps >= min_halt_steps)
|
| 450 |
+
|
| 451 |
+
next_q_halt_logits, next_q_continue_logits = self.inner(new_inner_carry, new_current_data)[-1]
|
| 452 |
+
outputs["target_q_continue"] = torch.sigmoid(
|
| 453 |
+
torch.where(
|
| 454 |
+
is_last_step,
|
| 455 |
+
next_q_halt_logits,
|
| 456 |
+
torch.maximum(next_q_halt_logits, next_q_continue_logits),
|
| 457 |
+
)
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
return HierarchicalReasoningModel_ACTV1Carry(new_inner_carry, new_steps, halted, new_current_data), outputs
|
| 461 |
+
|
| 462 |
+
def get_timing_stats(self) -> Dict:
|
| 463 |
+
return self.inner.get_timing_stats()
|
| 464 |
+
|
| 465 |
+
def reset_timing(self):
|
| 466 |
+
self.inner.reset_timing()
|
models/layers.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Tuple
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
from torch import nn
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
|
| 7 |
+
_USE_NATIVE_ATTN = False
|
| 8 |
+
|
| 9 |
+
try:
|
| 10 |
+
from flash_attn_interface import flash_attn_func # type: ignore[import]
|
| 11 |
+
except ImportError:
|
| 12 |
+
try:
|
| 13 |
+
from flash_attn import flash_attn_func # type: ignore[import]
|
| 14 |
+
except ImportError:
|
| 15 |
+
# Fallback to PyTorch native scaled dot-product attention
|
| 16 |
+
_USE_NATIVE_ATTN = True
|
| 17 |
+
|
| 18 |
+
def flash_attn_func(q, k, v, causal=False):
|
| 19 |
+
"""PyTorch-native fallback for FlashAttention.
|
| 20 |
+
|
| 21 |
+
Input shape: [batch, seq_len, num_heads, head_dim]
|
| 22 |
+
SDPA expects: [batch, num_heads, seq_len, head_dim]
|
| 23 |
+
"""
|
| 24 |
+
q = q.transpose(1, 2)
|
| 25 |
+
k = k.transpose(1, 2)
|
| 26 |
+
v = v.transpose(1, 2)
|
| 27 |
+
out = F.scaled_dot_product_attention(q, k, v, is_causal=causal)
|
| 28 |
+
return out.transpose(1, 2)
|
| 29 |
+
|
| 30 |
+
from models.common import trunc_normal_init_
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
CosSin = Tuple[torch.Tensor, torch.Tensor]
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _find_multiple(a, b):
|
| 37 |
+
return (-(a // -b)) * b
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def rotate_half(x: torch.Tensor):
|
| 41 |
+
"""Rotates half the hidden dims of the input."""
|
| 42 |
+
x1 = x[..., : x.shape[-1] // 2]
|
| 43 |
+
x2 = x[..., x.shape[-1] // 2 :]
|
| 44 |
+
return torch.cat((-x2, x1), dim=-1)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def apply_rotary_pos_emb(q: torch.Tensor, k: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor):
|
| 48 |
+
# q, k: [bs, seq_len, num_heads, head_dim]
|
| 49 |
+
# cos, sin: [seq_len, head_dim]
|
| 50 |
+
orig_dtype = q.dtype
|
| 51 |
+
q = q.to(cos.dtype)
|
| 52 |
+
k = k.to(cos.dtype)
|
| 53 |
+
|
| 54 |
+
q_embed = (q * cos.unsqueeze(-2)) + (rotate_half(q) * sin.unsqueeze(-2))
|
| 55 |
+
k_embed = (k * cos.unsqueeze(-2)) + (rotate_half(k) * sin.unsqueeze(-2))
|
| 56 |
+
|
| 57 |
+
return q_embed.to(orig_dtype), k_embed.to(orig_dtype)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class CastedLinear(nn.Module):
|
| 61 |
+
def __init__(self,
|
| 62 |
+
in_features: int,
|
| 63 |
+
out_features: int,
|
| 64 |
+
bias: bool):
|
| 65 |
+
super().__init__()
|
| 66 |
+
# Truncated LeCun normal init
|
| 67 |
+
self.weight = nn.Parameter(
|
| 68 |
+
trunc_normal_init_(torch.empty((out_features, in_features)), std=1.0 / (in_features ** 0.5))
|
| 69 |
+
)
|
| 70 |
+
self.bias = None
|
| 71 |
+
if bias:
|
| 72 |
+
# Zero init bias
|
| 73 |
+
self.bias = nn.Parameter(torch.zeros((out_features, )))
|
| 74 |
+
|
| 75 |
+
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
| 76 |
+
return F.linear(input, self.weight.to(input.dtype), bias=self.bias.to(input.dtype) if self.bias is not None else None)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class CastedEmbedding(nn.Module):
|
| 80 |
+
def __init__(self,
|
| 81 |
+
num_embeddings: int,
|
| 82 |
+
embedding_dim: int,
|
| 83 |
+
init_std: float,
|
| 84 |
+
cast_to: torch.dtype):
|
| 85 |
+
super().__init__()
|
| 86 |
+
self.cast_to = cast_to
|
| 87 |
+
|
| 88 |
+
# Truncated LeCun normal init
|
| 89 |
+
self.embedding_weight = nn.Parameter(
|
| 90 |
+
trunc_normal_init_(torch.empty((num_embeddings, embedding_dim)), std=init_std)
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
| 94 |
+
return F.embedding(input, self.embedding_weight.to(self.cast_to))
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
class RotaryEmbedding(nn.Module):
|
| 98 |
+
def __init__(self, dim, max_position_embeddings, base, device=None):
|
| 99 |
+
super().__init__()
|
| 100 |
+
|
| 101 |
+
# RoPE
|
| 102 |
+
inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.float32, device=device) / dim))
|
| 103 |
+
t = torch.arange(max_position_embeddings, dtype=torch.float32, device=device)
|
| 104 |
+
freqs = torch.outer(t, inv_freq)
|
| 105 |
+
|
| 106 |
+
# Different from paper, but it uses a different permutation in order to obtain the same calculation
|
| 107 |
+
emb = torch.cat((freqs, freqs), dim=-1)
|
| 108 |
+
self.cos_cached = nn.Buffer(emb.cos(), persistent=False)
|
| 109 |
+
self.sin_cached = nn.Buffer(emb.sin(), persistent=False)
|
| 110 |
+
|
| 111 |
+
def forward(self):
|
| 112 |
+
return self.cos_cached, self.sin_cached
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class Attention(nn.Module):
|
| 116 |
+
def __init__(self, hidden_size, head_dim, num_heads, num_key_value_heads, causal=False):
|
| 117 |
+
super().__init__()
|
| 118 |
+
|
| 119 |
+
self.hidden_size = hidden_size
|
| 120 |
+
self.head_dim = head_dim
|
| 121 |
+
self.output_size = head_dim * num_heads
|
| 122 |
+
self.num_heads = num_heads
|
| 123 |
+
self.num_key_value_heads = num_key_value_heads
|
| 124 |
+
self.causal = causal
|
| 125 |
+
|
| 126 |
+
self.qkv_proj = CastedLinear(self.hidden_size, (self.num_heads + 2 * self.num_key_value_heads) * self.head_dim, bias=False)
|
| 127 |
+
self.o_proj = CastedLinear(self.output_size, self.hidden_size, bias=False)
|
| 128 |
+
|
| 129 |
+
def forward(self, cos_sin: CosSin, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 130 |
+
batch_size, seq_len, _ = hidden_states.shape
|
| 131 |
+
|
| 132 |
+
# hidden_states: [bs, seq_len, num_heads, head_dim]
|
| 133 |
+
qkv = self.qkv_proj(hidden_states)
|
| 134 |
+
|
| 135 |
+
# Split head
|
| 136 |
+
qkv = qkv.view(batch_size, seq_len, self.num_heads + 2 * self.num_key_value_heads, self.head_dim)
|
| 137 |
+
query = qkv[:, :, :self.num_heads]
|
| 138 |
+
key = qkv[:, :, self.num_heads: self.num_heads + self.num_key_value_heads]
|
| 139 |
+
value = qkv[:, :, self.num_heads + self.num_key_value_heads:]
|
| 140 |
+
|
| 141 |
+
# RoPE
|
| 142 |
+
if cos_sin is not None:
|
| 143 |
+
cos, sin = cos_sin
|
| 144 |
+
query, key = apply_rotary_pos_emb(query, key, cos, sin)
|
| 145 |
+
|
| 146 |
+
# flash attn
|
| 147 |
+
attn_output = flash_attn_func(q=query, k=key, v=value, causal=self.causal)
|
| 148 |
+
if isinstance(attn_output, tuple): # fa2 and fa3 compatibility
|
| 149 |
+
attn_output = attn_output[0]
|
| 150 |
+
|
| 151 |
+
attn_output = attn_output.view(batch_size, seq_len, self.output_size) # type: ignore
|
| 152 |
+
return self.o_proj(attn_output)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
class SwiGLU(nn.Module):
|
| 156 |
+
def __init__(self, hidden_size: int, expansion: float):
|
| 157 |
+
super().__init__()
|
| 158 |
+
inter = _find_multiple(round(expansion * hidden_size * 2 / 3), 256)
|
| 159 |
+
|
| 160 |
+
self.gate_up_proj = CastedLinear(hidden_size, inter * 2, bias=False)
|
| 161 |
+
self.down_proj = CastedLinear(inter, hidden_size, bias=False)
|
| 162 |
+
|
| 163 |
+
def forward(self, x):
|
| 164 |
+
gate, up = self.gate_up_proj(x).chunk(2, dim=-1)
|
| 165 |
+
return self.down_proj(F.silu(gate) * up)
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def rms_norm(hidden_states: torch.Tensor, variance_epsilon: float) -> torch.Tensor:
|
| 169 |
+
input_dtype = hidden_states.dtype
|
| 170 |
+
hidden_states = hidden_states.to(torch.float32)
|
| 171 |
+
|
| 172 |
+
variance = hidden_states.square().mean(-1, keepdim=True)
|
| 173 |
+
hidden_states = hidden_states * torch.rsqrt(variance + variance_epsilon)
|
| 174 |
+
return hidden_states.to(input_dtype)
|