Code2aum commited on
Commit
5dc80b3
·
verified ·
1 Parent(s): d4387aa

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +2 -0
  2. .gitignore +170 -0
  3. .gitmodules +9 -0
  4. .vscode/launch.json +26 -0
  5. .vscode/settings.json +3 -0
  6. GUIDE_SPEED_TEST.md +62 -0
  7. GUIDE_TRAIN_TEST.md +67 -0
  8. LICENSE +202 -0
  9. README.md +215 -3
  10. README_TIERED.md +116 -0
  11. arc_eval.ipynb +252 -0
  12. assets/hrm.png +0 -0
  13. assets/npyjs.js +176 -0
  14. benchmark.py +686 -0
  15. benchmark_results/benchmark_comparison.png +0 -0
  16. benchmark_results/comparison_results.json +119 -0
  17. benchmark_results/eval_dummy.json +24 -0
  18. benchmark_results/eval_dummy_comparison.png +0 -0
  19. benchmark_results/eval_fused_vs_v1_comparison.png +0 -0
  20. benchmark_results/memory_analysis.png +0 -0
  21. benchmark_results/model_comparison.png +3 -0
  22. benchmark_results/results.json +500 -0
  23. benchmark_results/run_nsys_profiler.py +117 -0
  24. benchmark_results/trained_model_comparison.png +3 -0
  25. benchmark_results/trained_model_results.json +18 -0
  26. cleanup.sh +67 -0
  27. compare_models.py +299 -0
  28. config/arch/hrm_tiered.yaml +27 -0
  29. config/arch/hrm_v1.yaml +21 -0
  30. config/cfg_pretrain.yaml +31 -0
  31. dataset/build_arc_dataset.py +291 -0
  32. dataset/build_maze_dataset.py +142 -0
  33. dataset/build_sudoku_dataset.py +169 -0
  34. dataset/common.py +51 -0
  35. docs/END_TO_END_EXPLANATION.md +1193 -0
  36. docs/IMPLEMENTATION.md +49 -0
  37. eval_dummy.py +277 -0
  38. evaluate.py +68 -0
  39. fusedevals.py +260 -0
  40. hf_upload_new/README.md +10 -0
  41. hf_upload_new/config.yaml +35 -0
  42. hf_upload_new/hrm_act_v1.py +283 -0
  43. hf_upload_new/losses.py +131 -0
  44. hf_upload_new/model.safetensors +3 -0
  45. latency_plot_trained_model.py +417 -0
  46. models/common.py +32 -0
  47. models/fused_hierarchical_scan.py +186 -0
  48. models/hrm/hrm_act_v1.py +283 -0
  49. models/hrm/hrm_tiered.py +466 -0
  50. 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
- license: mit
3
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hierarchical Reasoning Model
2
+
3
+ ![](./assets/hrm.png)
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

  • SHA256: 81a9239f0f371cd0c6519215d1bf6d1469aa03aad9afa5ba8765a5d295d68879
  • Pointer size: 131 Bytes
  • Size of remote file: 177 kB
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

  • SHA256: 5ed56bbb985e5ad6106a09c8f2b41bd63c3f1b0fa4074d7b34b5ffeafcc731a8
  • Pointer size: 131 Bytes
  • Size of remote file: 142 kB
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)