nima1 commited on
Commit
4be6a52
·
verified ·
1 Parent(s): d57e242

Publish verified checkpoint and losslessly compressed study evidence

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. LICENSE +202 -0
  2. NOTICE +16 -0
  3. README.md +159 -0
  4. checkpoint/adapter/README.md +24 -0
  5. checkpoint/adapter/adapter_config.json +295 -0
  6. checkpoint/adapter/adapter_model.safetensors +3 -0
  7. checkpoint/joint_head.safetensors +3 -0
  8. checkpoint/reference.json +124 -0
  9. checkpoint/training_config.json +719 -0
  10. code/.dockerignore +14 -0
  11. code/.python-version +1 -0
  12. code/Dockerfile +23 -0
  13. code/LICENSE +202 -0
  14. code/NOTICE +16 -0
  15. code/README.md +142 -0
  16. code/docs/clef.md +121 -0
  17. code/docs/demo-hosting.md +156 -0
  18. code/docs/evaluation-protocol.md +163 -0
  19. code/docs/interfaces.md +15 -0
  20. code/docs/rules.md +87 -0
  21. code/docs/tutorials/00-foundation.md +57 -0
  22. code/docs/tutorials/01-game.md +153 -0
  23. code/docs/tutorials/02-baselines.md +140 -0
  24. code/docs/tutorials/03-data.md +189 -0
  25. code/docs/tutorials/04-gpu-feasibility.md +300 -0
  26. code/docs/tutorials/05-evaluation.md +281 -0
  27. code/docs/tutorials/06-release.md +234 -0
  28. code/pyproject.toml +59 -0
  29. code/release/dataset-card.md +84 -0
  30. code/release/model-card.md +125 -0
  31. code/reports/clef-development.json +66 -0
  32. code/reports/dataset-manifest.json +539 -0
  33. code/reports/evaluation-hardware.json +42 -0
  34. code/reports/final-audit-source.md +149 -0
  35. code/reports/final-audit.json +0 -0
  36. code/reports/final-study.md +229 -0
  37. code/reports/final-summary.json +1297 -0
  38. code/reports/gpu-feasibility.json +210 -0
  39. code/reports/selected-checkpoint-reload.json +21 -0
  40. code/reports/study-preregistration.md +25 -0
  41. code/reports/study-training.json +696 -0
  42. code/reports/training-launch-source.json +0 -0
  43. code/reports/training-source-supplement.json +11 -0
  44. code/reports/validation-summary.json +158 -0
  45. code/scripts/benchmark_clef.py +51 -0
  46. code/scripts/build_release.py +640 -0
  47. code/scripts/evaluate_clef.py +616 -0
  48. code/scripts/export_demo.py +213 -0
  49. code/scripts/gpu_session.py +123 -0
  50. code/scripts/prepare_checkpoint.py +168 -0
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 2026 Alibaba Cloud
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.
NOTICE ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Stackcraft
2
+ Copyright 2026 Stackcraft contributors.
3
+
4
+ Stackcraft is an independent educational falling-block game and fine-tuning study.
5
+ It is not affiliated with or endorsed by Tetris, Cloudflare, or the Qwen project.
6
+
7
+ The optional trained model derives from Cloudflare/Clef-Flash (Apache-2.0),
8
+ revision 17f0b0ad64efb65d273590632833508766b2aae6, which derives from
9
+ Qwen/Qwen3.5-9B. Original backbone weights and native joint-schema implementation
10
+ remain upstream dependencies; this repository does not redistribute the backbone.
11
+ Stackcraft adds game-specific LoRA matrices and trains a separate native decision
12
+ head. The trained head and LoRA weights must be loaded together.
13
+
14
+ Upstream model: https://huggingface.co/Cloudflare/clef-flash
15
+ Base architecture: https://huggingface.co/Qwen/Qwen3.5-9B
16
+ Python package dependency licenses remain with their respective authors.
README.md ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model: Cloudflare/clef-flash
4
+ base_model_relation: adapter
5
+ library_name: peft
6
+ language:
7
+ - en
8
+ tags:
9
+ - clef
10
+ - decision-model
11
+ - lora
12
+ - imitation-learning
13
+ - stackcraft
14
+ ---
15
+
16
+ # Stackcraft Clef-Flash
17
+
18
+ Selected checkpoint: **epoch-02**, chosen on validation before final tests.
19
+ This is a small, synthetic imitation-learning study of a falling-block placement
20
+ policy. It is not a generally capable game agent, reinforcement-learning result,
21
+ or competitive Tetris implementation.
22
+
23
+ The artifact contains a rank-4 LoRA adapter **and a separately trained FP32 native
24
+ joint decision head** for `Cloudflare/clef-flash`, pinned revision
25
+ `17f0b0ad64efb65d273590632833508766b2aae6`. Load both. The frozen BF16 backbone
26
+ is downloaded separately; these files are not a standalone model. Clef-flash is
27
+ based on Qwen3.5-9B. The native schema head scores every supplied legal placement
28
+ in one forward pass, with no generated text or command parsing.
29
+
30
+ Related releases: [source and tutorials](https://github.com/kkarimi/stackcraft),
31
+ [model](https://huggingface.co/nima1/stackcraft-clef-flash-lora),
32
+ [dataset](https://huggingface.co/datasets/nima1/stackcraft-data), and
33
+ [planned CPU demo](https://huggingface.co/spaces/nima1/stackcraft).
34
+ The hosted demo awaits Hugging Face's Docker-account subscription requirement;
35
+ [local Docker instructions](https://github.com/kkarimi/stackcraft/blob/main/docs/demo-hosting.md)
36
+ work without that subscription.
37
+
38
+ ## Intended use
39
+
40
+ Learn how to build deterministic environments, generate search-teacher labels,
41
+ train a custom decision model, preserve its native head, and evaluate complete
42
+ games with paired uncertainty. The playable demo uses recorded model replays;
43
+ it does not provide public live GPU inference.
44
+
45
+ The observation is a 10×20 occupied-cell grid, current tetromino, exactly one next
46
+ piece, rules and every legal placement with its coordinates. The action selects
47
+ an orientation and column, then drops vertically. There is no hold, wall kick,
48
+ tuck, gravity timer, T-spin bonus or changing speed. Hidden future pieces and
49
+ sequence seeds are excluded from neural input. Probabilities reflect the model's
50
+ choice distribution; they are not calibrated chances of clearing a line or winning.
51
+
52
+ ## Training and data
53
+
54
+ The study uses 827 training and 215 validation positions generated from fixed
55
+ seven-bag streams. Random, heuristic and search policies collect states. Every
56
+ label comes from a fixed two-placement search using the current piece and one
57
+ preview, with a board-height/holes/bumpiness/line-clear heuristic. These are
58
+ synthetic teacher preferences, not human demonstrations or globally optimal moves.
59
+ Exact duplicate observations are removed across splits. Six source hashes and
60
+ exact file hashes accompany the dataset manifest.
61
+
62
+ Preregistered training: seed 42, two epochs, batch 1, accumulation 8, rank 4/alpha 8,
63
+ no LoRA dropout, AdamW lr 1e-5/weight_decay 0.01, gradient norm cap 1.0, label smoothing
64
+ 0.05 and Brier sum weight0.1. BF16 backbone, FP32 head, text-layer LoRA including
65
+ both hybrid attention types and MLPs. Vision, output and other original parameters
66
+ remain frozen. There are 132,582,404 trainable parameters: 121,762,820 in the
67
+ joint head and 10,819,584 in LoRA. This is why the head is a substantial part of
68
+ the saved artifact, even though most backbone weights stay frozen. Select only between epoch 01/02 using lowest finite target NLL on
69
+ all 215 validation positions; exact ties choose the earlier epoch.
70
+
71
+ ## Evaluation and limitations
72
+
73
+ Final preregistered comparison: 200 untouched seeds 30000–30199, 200-piece cap,
74
+ selected model versus unchanged native BF16-head Clef, unchanged FP32-head
75
+ ablation, random legal placement and a cheap heuristic. Primary metric is lines
76
+ cleared. Report score, survival, cap hits, failures and latency. Paired 95% bootstrap
77
+ intervals use 10000 episode resamples with seed 2026. Failed episodes remain in the
78
+ analysis, assigned zero primary outcomes with partial outcomes reported separately.
79
+
80
+ Verified final outcomes on all 200 paired seeds, with a 200-piece cap. Lines, score and placed-piece summaries use the preregistered zero-on-error policy. Cap hits indicate censored survival.
81
+
82
+ | Player | Lines mean (median) | Score mean (median) | Placed mean (median) | Cap hits | Errors |
83
+ | --- | ---: | ---: | ---: | ---: | ---: |
84
+ | base | 0.070 (0.0) | 7.000 (0.0) | 26.020 (26.0) | 0.0% | 0.0% |
85
+ | base-fp32 | 0.105 (0.0) | 10.500 (0.0) | 26.000 (26.0) | 0.0% | 0.0% |
86
+ | trained | 16.810 (16.0) | 1745.500 (1650.0) | 83.565 (82.0) | 0.0% | 0.0% |
87
+ | random | 0.140 (0.0) | 14.000 (0.0) | 26.645 (27.0) | 0.0% | 0.0% |
88
+ | heuristic | 76.530 (77.0) | 8306.500 (8300.0) | 200.000 (200.0) | 100.0% | 0.0% |
89
+
90
+ Each paired 95% bootstrap interval resamples complete episode differences. Trained versus native base is the primary comparison; the heuristic and FP32-head comparisons are separate checks. Intervals are not adjusted for multiple comparisons.
91
+
92
+ | Comparison (first minus second) | Mean lines difference | Paired 95% interval |
93
+ | --- | ---: | ---: |
94
+ | trained − base | 16.740 | [15.740, 17.770] |
95
+ | trained − heuristic | -59.720 | [-60.805, -58.620] |
96
+ | trained − base-fp32 | 16.705 | [15.705, 17.740] |
97
+ | base-fp32 − base | 0.035 | [-0.020, 0.090] |
98
+
99
+ Decision latency includes recorded first-call effects, tokenization and policy overhead, but excludes game rendering and replay playback.
100
+
101
+ | Player | Decision mean ms | Median ms | p95 ms | Invalid decisions |
102
+ | --- | ---: | ---: | ---: | ---: |
103
+ | base | 149.110 | 139.381 | 244.916 | 0 |
104
+ | base-fp32 | 150.188 | 140.968 | 247.880 | 0 |
105
+ | trained | 206.814 | 171.844 | 309.000 | 0 |
106
+ | random | 0.001 | 0.001 | 0.003 | 0 |
107
+ | heuristic | 0.199 | 0.139 | 0.281 | 0 |
108
+
109
+ The full reports retain raw outcomes, all failed seeds and probability logs.
110
+
111
+ Fine-tuning improved complete-game play over both unchanged Clef references in
112
+ this study. The fixed heuristic still cleared substantially more lines and used
113
+ about one thousandth of the trained model's decision time on this workstation.
114
+ All five policies had zero errors and invalid decisions. Every heuristic game hit
115
+ the 200-piece cap; no neural or random game did. This is evidence of task adaptation,
116
+ not a reason to prefer this 9B model over the heuristic for the simplified game.
117
+ See [the readable study report](code/reports/final-study.md) and
118
+ [the hardware/runtime snapshot](code/reports/evaluation-hardware.json).
119
+
120
+
121
+ Only one training seed, one small synthetic dataset, one simplified game ruleset
122
+ and one hardware/software configuration are studied. The finite cap censors
123
+ survival. Validation teacher agreement does not establish game quality. Test
124
+ uncertainty does not capture variation across training seeds.
125
+
126
+ ## Loading
127
+
128
+ Use the released Stackcraft source and locked ML dependencies. Generic
129
+ `AutoModel` or adapter-only loading omits the custom decision head and is incorrect.
130
+ From the downloaded model repository's `code/` directory, install the locked ML
131
+ environment with `uv sync --locked --extra ml` and cache the pinned upstream
132
+ snapshot as described in [Tutorial 02](code/docs/tutorials/02-baselines.md).
133
+ Run this example with `uv run --locked --extra ml python`; the sibling
134
+ `../checkpoint` directory contains the released adapter and head:
135
+
136
+ ```python
137
+ from stackcraft.clef import ClefPlayer
138
+ from stackcraft.training import load_checkpoint
139
+ from stackcraft.engine import new_game
140
+ from stackcraft.players import observe
141
+
142
+ player = ClefPlayer.from_pretrained(trust_pinned_code=True, local_files_only=True)
143
+ load_checkpoint(player.model, "../checkpoint")
144
+ print(player.choose(observe(new_game(42))))
145
+ ```
146
+
147
+ `trust_pinned_code=True` executes reviewed, hash-checked upstream Python. It is
148
+ not a sandbox. The measured RTX 5090 inference baseline allocated 19.7 GB; bounded
149
+ LoRA training allocated 23.2 GB. Load only with sufficient free VRAM. Context is
150
+ limited to 4096 tokens, with explicit rejection before any state truncation.
151
+
152
+ See the released tutorials, training configuration, artifact hashes and evaluation
153
+ report for exact reproduction. Apache-2.0; see LICENSE and NOTICE for upstream
154
+ attribution. This project is independent of Cloudflare, Qwen and Tetris.
155
+
156
+
157
+ ## Bundle layout
158
+
159
+ `checkpoint/` preserves the selected checkpoint bytes. `code/` contains the source, locked environment, tests, scripts and milestone tutorials. `evidence.zip` losslessly compresses the frozen selection, both validation candidates and complete final evaluation. From the model repository root, run `python -m zipfile -e evidence.zip .` after download verification to restore `evidence/` and all original relative paths. `evidence-files.json` records each raw file's byte count and SHA256; extraction preserves the original evidence hashes. From `code/`, run `uv sync --locked --extra ml`; load `../checkpoint` using the native Stackcraft loader. No access to a private GitHub repository is required.
checkpoint/adapter/README.md ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ base_model: Cloudflare/clef-flash
3
+ library_name: peft
4
+ tags:
5
+ - lora
6
+ - stackcraft
7
+ ---
8
+
9
+ # Stackcraft LoRA adapter component
10
+
11
+ This Stackcraft adapter adapts [Cloudflare/clef-flash](https://huggingface.co/Cloudflare/clef-flash),
12
+ released by Cloudflare and pinned at revision `17f0b0ad64efb65d273590632833508766b2aae6`.
13
+ The original model card and license are provided in that upstream repository.
14
+
15
+ This directory is only one component of the Stackcraft checkpoint. Load the
16
+ parent checkpoint's `joint_head.safetensors` and this adapter together through
17
+ Stackcraft's `load_checkpoint` function. The native decision head contains learned
18
+ parameters; loading only the LoRA adapter does not reproduce the trained model.
19
+ Use the exact pinned base and the parent `training_config.json` contract.
20
+
21
+ This component card does not claim improved performance or publication status.
22
+ See the release's full model card and evaluation report for measured results,
23
+ limitations, training provenance and reproduction commands. A prepared candidate
24
+ has not necessarily been selected by validation.
checkpoint/adapter/adapter_config.json ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "alora_invocation_tokens": null,
3
+ "alpha_pattern": {},
4
+ "arrow_config": null,
5
+ "auto_mapping": {
6
+ "base_model_class": "Qwen3_5ForConditionalGeneration",
7
+ "parent_library": "transformers.models.qwen3_5.modeling_qwen3_5"
8
+ },
9
+ "base_model_name_or_path": "Cloudflare/clef-flash",
10
+ "bias": "none",
11
+ "corda_config": null,
12
+ "ensure_weight_tying": false,
13
+ "eva_config": null,
14
+ "exclude_modules": null,
15
+ "fan_in_fan_out": false,
16
+ "inference_mode": true,
17
+ "init_lora_weights": true,
18
+ "kasa_config": null,
19
+ "layer_replication": null,
20
+ "layers_pattern": null,
21
+ "layers_to_transform": null,
22
+ "loftq_config": {},
23
+ "lora_alpha": 8,
24
+ "lora_bias": false,
25
+ "lora_dropout": 0.0,
26
+ "lora_ga_config": null,
27
+ "megatron_config": null,
28
+ "megatron_core": "megatron.core",
29
+ "modules_to_save": null,
30
+ "monteclora_config": null,
31
+ "peft_type": "LORA",
32
+ "peft_version": "0.21.2",
33
+ "qalora_group_size": 16,
34
+ "r": 4,
35
+ "rank_pattern": {},
36
+ "revision": "17f0b0ad64efb65d273590632833508766b2aae6",
37
+ "target_modules": [
38
+ "model.language_model.layers.0.linear_attn.in_proj_a",
39
+ "model.language_model.layers.0.linear_attn.in_proj_b",
40
+ "model.language_model.layers.0.linear_attn.in_proj_qkv",
41
+ "model.language_model.layers.0.linear_attn.in_proj_z",
42
+ "model.language_model.layers.0.linear_attn.out_proj",
43
+ "model.language_model.layers.0.mlp.down_proj",
44
+ "model.language_model.layers.0.mlp.gate_proj",
45
+ "model.language_model.layers.0.mlp.up_proj",
46
+ "model.language_model.layers.1.linear_attn.in_proj_a",
47
+ "model.language_model.layers.1.linear_attn.in_proj_b",
48
+ "model.language_model.layers.1.linear_attn.in_proj_qkv",
49
+ "model.language_model.layers.1.linear_attn.in_proj_z",
50
+ "model.language_model.layers.1.linear_attn.out_proj",
51
+ "model.language_model.layers.1.mlp.down_proj",
52
+ "model.language_model.layers.1.mlp.gate_proj",
53
+ "model.language_model.layers.1.mlp.up_proj",
54
+ "model.language_model.layers.10.linear_attn.in_proj_a",
55
+ "model.language_model.layers.10.linear_attn.in_proj_b",
56
+ "model.language_model.layers.10.linear_attn.in_proj_qkv",
57
+ "model.language_model.layers.10.linear_attn.in_proj_z",
58
+ "model.language_model.layers.10.linear_attn.out_proj",
59
+ "model.language_model.layers.10.mlp.down_proj",
60
+ "model.language_model.layers.10.mlp.gate_proj",
61
+ "model.language_model.layers.10.mlp.up_proj",
62
+ "model.language_model.layers.11.mlp.down_proj",
63
+ "model.language_model.layers.11.mlp.gate_proj",
64
+ "model.language_model.layers.11.mlp.up_proj",
65
+ "model.language_model.layers.11.self_attn.k_proj",
66
+ "model.language_model.layers.11.self_attn.o_proj",
67
+ "model.language_model.layers.11.self_attn.q_proj",
68
+ "model.language_model.layers.11.self_attn.v_proj",
69
+ "model.language_model.layers.12.linear_attn.in_proj_a",
70
+ "model.language_model.layers.12.linear_attn.in_proj_b",
71
+ "model.language_model.layers.12.linear_attn.in_proj_qkv",
72
+ "model.language_model.layers.12.linear_attn.in_proj_z",
73
+ "model.language_model.layers.12.linear_attn.out_proj",
74
+ "model.language_model.layers.12.mlp.down_proj",
75
+ "model.language_model.layers.12.mlp.gate_proj",
76
+ "model.language_model.layers.12.mlp.up_proj",
77
+ "model.language_model.layers.13.linear_attn.in_proj_a",
78
+ "model.language_model.layers.13.linear_attn.in_proj_b",
79
+ "model.language_model.layers.13.linear_attn.in_proj_qkv",
80
+ "model.language_model.layers.13.linear_attn.in_proj_z",
81
+ "model.language_model.layers.13.linear_attn.out_proj",
82
+ "model.language_model.layers.13.mlp.down_proj",
83
+ "model.language_model.layers.13.mlp.gate_proj",
84
+ "model.language_model.layers.13.mlp.up_proj",
85
+ "model.language_model.layers.14.linear_attn.in_proj_a",
86
+ "model.language_model.layers.14.linear_attn.in_proj_b",
87
+ "model.language_model.layers.14.linear_attn.in_proj_qkv",
88
+ "model.language_model.layers.14.linear_attn.in_proj_z",
89
+ "model.language_model.layers.14.linear_attn.out_proj",
90
+ "model.language_model.layers.14.mlp.down_proj",
91
+ "model.language_model.layers.14.mlp.gate_proj",
92
+ "model.language_model.layers.14.mlp.up_proj",
93
+ "model.language_model.layers.15.mlp.down_proj",
94
+ "model.language_model.layers.15.mlp.gate_proj",
95
+ "model.language_model.layers.15.mlp.up_proj",
96
+ "model.language_model.layers.15.self_attn.k_proj",
97
+ "model.language_model.layers.15.self_attn.o_proj",
98
+ "model.language_model.layers.15.self_attn.q_proj",
99
+ "model.language_model.layers.15.self_attn.v_proj",
100
+ "model.language_model.layers.16.linear_attn.in_proj_a",
101
+ "model.language_model.layers.16.linear_attn.in_proj_b",
102
+ "model.language_model.layers.16.linear_attn.in_proj_qkv",
103
+ "model.language_model.layers.16.linear_attn.in_proj_z",
104
+ "model.language_model.layers.16.linear_attn.out_proj",
105
+ "model.language_model.layers.16.mlp.down_proj",
106
+ "model.language_model.layers.16.mlp.gate_proj",
107
+ "model.language_model.layers.16.mlp.up_proj",
108
+ "model.language_model.layers.17.linear_attn.in_proj_a",
109
+ "model.language_model.layers.17.linear_attn.in_proj_b",
110
+ "model.language_model.layers.17.linear_attn.in_proj_qkv",
111
+ "model.language_model.layers.17.linear_attn.in_proj_z",
112
+ "model.language_model.layers.17.linear_attn.out_proj",
113
+ "model.language_model.layers.17.mlp.down_proj",
114
+ "model.language_model.layers.17.mlp.gate_proj",
115
+ "model.language_model.layers.17.mlp.up_proj",
116
+ "model.language_model.layers.18.linear_attn.in_proj_a",
117
+ "model.language_model.layers.18.linear_attn.in_proj_b",
118
+ "model.language_model.layers.18.linear_attn.in_proj_qkv",
119
+ "model.language_model.layers.18.linear_attn.in_proj_z",
120
+ "model.language_model.layers.18.linear_attn.out_proj",
121
+ "model.language_model.layers.18.mlp.down_proj",
122
+ "model.language_model.layers.18.mlp.gate_proj",
123
+ "model.language_model.layers.18.mlp.up_proj",
124
+ "model.language_model.layers.19.mlp.down_proj",
125
+ "model.language_model.layers.19.mlp.gate_proj",
126
+ "model.language_model.layers.19.mlp.up_proj",
127
+ "model.language_model.layers.19.self_attn.k_proj",
128
+ "model.language_model.layers.19.self_attn.o_proj",
129
+ "model.language_model.layers.19.self_attn.q_proj",
130
+ "model.language_model.layers.19.self_attn.v_proj",
131
+ "model.language_model.layers.2.linear_attn.in_proj_a",
132
+ "model.language_model.layers.2.linear_attn.in_proj_b",
133
+ "model.language_model.layers.2.linear_attn.in_proj_qkv",
134
+ "model.language_model.layers.2.linear_attn.in_proj_z",
135
+ "model.language_model.layers.2.linear_attn.out_proj",
136
+ "model.language_model.layers.2.mlp.down_proj",
137
+ "model.language_model.layers.2.mlp.gate_proj",
138
+ "model.language_model.layers.2.mlp.up_proj",
139
+ "model.language_model.layers.20.linear_attn.in_proj_a",
140
+ "model.language_model.layers.20.linear_attn.in_proj_b",
141
+ "model.language_model.layers.20.linear_attn.in_proj_qkv",
142
+ "model.language_model.layers.20.linear_attn.in_proj_z",
143
+ "model.language_model.layers.20.linear_attn.out_proj",
144
+ "model.language_model.layers.20.mlp.down_proj",
145
+ "model.language_model.layers.20.mlp.gate_proj",
146
+ "model.language_model.layers.20.mlp.up_proj",
147
+ "model.language_model.layers.21.linear_attn.in_proj_a",
148
+ "model.language_model.layers.21.linear_attn.in_proj_b",
149
+ "model.language_model.layers.21.linear_attn.in_proj_qkv",
150
+ "model.language_model.layers.21.linear_attn.in_proj_z",
151
+ "model.language_model.layers.21.linear_attn.out_proj",
152
+ "model.language_model.layers.21.mlp.down_proj",
153
+ "model.language_model.layers.21.mlp.gate_proj",
154
+ "model.language_model.layers.21.mlp.up_proj",
155
+ "model.language_model.layers.22.linear_attn.in_proj_a",
156
+ "model.language_model.layers.22.linear_attn.in_proj_b",
157
+ "model.language_model.layers.22.linear_attn.in_proj_qkv",
158
+ "model.language_model.layers.22.linear_attn.in_proj_z",
159
+ "model.language_model.layers.22.linear_attn.out_proj",
160
+ "model.language_model.layers.22.mlp.down_proj",
161
+ "model.language_model.layers.22.mlp.gate_proj",
162
+ "model.language_model.layers.22.mlp.up_proj",
163
+ "model.language_model.layers.23.mlp.down_proj",
164
+ "model.language_model.layers.23.mlp.gate_proj",
165
+ "model.language_model.layers.23.mlp.up_proj",
166
+ "model.language_model.layers.23.self_attn.k_proj",
167
+ "model.language_model.layers.23.self_attn.o_proj",
168
+ "model.language_model.layers.23.self_attn.q_proj",
169
+ "model.language_model.layers.23.self_attn.v_proj",
170
+ "model.language_model.layers.24.linear_attn.in_proj_a",
171
+ "model.language_model.layers.24.linear_attn.in_proj_b",
172
+ "model.language_model.layers.24.linear_attn.in_proj_qkv",
173
+ "model.language_model.layers.24.linear_attn.in_proj_z",
174
+ "model.language_model.layers.24.linear_attn.out_proj",
175
+ "model.language_model.layers.24.mlp.down_proj",
176
+ "model.language_model.layers.24.mlp.gate_proj",
177
+ "model.language_model.layers.24.mlp.up_proj",
178
+ "model.language_model.layers.25.linear_attn.in_proj_a",
179
+ "model.language_model.layers.25.linear_attn.in_proj_b",
180
+ "model.language_model.layers.25.linear_attn.in_proj_qkv",
181
+ "model.language_model.layers.25.linear_attn.in_proj_z",
182
+ "model.language_model.layers.25.linear_attn.out_proj",
183
+ "model.language_model.layers.25.mlp.down_proj",
184
+ "model.language_model.layers.25.mlp.gate_proj",
185
+ "model.language_model.layers.25.mlp.up_proj",
186
+ "model.language_model.layers.26.linear_attn.in_proj_a",
187
+ "model.language_model.layers.26.linear_attn.in_proj_b",
188
+ "model.language_model.layers.26.linear_attn.in_proj_qkv",
189
+ "model.language_model.layers.26.linear_attn.in_proj_z",
190
+ "model.language_model.layers.26.linear_attn.out_proj",
191
+ "model.language_model.layers.26.mlp.down_proj",
192
+ "model.language_model.layers.26.mlp.gate_proj",
193
+ "model.language_model.layers.26.mlp.up_proj",
194
+ "model.language_model.layers.27.mlp.down_proj",
195
+ "model.language_model.layers.27.mlp.gate_proj",
196
+ "model.language_model.layers.27.mlp.up_proj",
197
+ "model.language_model.layers.27.self_attn.k_proj",
198
+ "model.language_model.layers.27.self_attn.o_proj",
199
+ "model.language_model.layers.27.self_attn.q_proj",
200
+ "model.language_model.layers.27.self_attn.v_proj",
201
+ "model.language_model.layers.28.linear_attn.in_proj_a",
202
+ "model.language_model.layers.28.linear_attn.in_proj_b",
203
+ "model.language_model.layers.28.linear_attn.in_proj_qkv",
204
+ "model.language_model.layers.28.linear_attn.in_proj_z",
205
+ "model.language_model.layers.28.linear_attn.out_proj",
206
+ "model.language_model.layers.28.mlp.down_proj",
207
+ "model.language_model.layers.28.mlp.gate_proj",
208
+ "model.language_model.layers.28.mlp.up_proj",
209
+ "model.language_model.layers.29.linear_attn.in_proj_a",
210
+ "model.language_model.layers.29.linear_attn.in_proj_b",
211
+ "model.language_model.layers.29.linear_attn.in_proj_qkv",
212
+ "model.language_model.layers.29.linear_attn.in_proj_z",
213
+ "model.language_model.layers.29.linear_attn.out_proj",
214
+ "model.language_model.layers.29.mlp.down_proj",
215
+ "model.language_model.layers.29.mlp.gate_proj",
216
+ "model.language_model.layers.29.mlp.up_proj",
217
+ "model.language_model.layers.3.mlp.down_proj",
218
+ "model.language_model.layers.3.mlp.gate_proj",
219
+ "model.language_model.layers.3.mlp.up_proj",
220
+ "model.language_model.layers.3.self_attn.k_proj",
221
+ "model.language_model.layers.3.self_attn.o_proj",
222
+ "model.language_model.layers.3.self_attn.q_proj",
223
+ "model.language_model.layers.3.self_attn.v_proj",
224
+ "model.language_model.layers.30.linear_attn.in_proj_a",
225
+ "model.language_model.layers.30.linear_attn.in_proj_b",
226
+ "model.language_model.layers.30.linear_attn.in_proj_qkv",
227
+ "model.language_model.layers.30.linear_attn.in_proj_z",
228
+ "model.language_model.layers.30.linear_attn.out_proj",
229
+ "model.language_model.layers.30.mlp.down_proj",
230
+ "model.language_model.layers.30.mlp.gate_proj",
231
+ "model.language_model.layers.30.mlp.up_proj",
232
+ "model.language_model.layers.31.mlp.down_proj",
233
+ "model.language_model.layers.31.mlp.gate_proj",
234
+ "model.language_model.layers.31.mlp.up_proj",
235
+ "model.language_model.layers.31.self_attn.k_proj",
236
+ "model.language_model.layers.31.self_attn.o_proj",
237
+ "model.language_model.layers.31.self_attn.q_proj",
238
+ "model.language_model.layers.31.self_attn.v_proj",
239
+ "model.language_model.layers.4.linear_attn.in_proj_a",
240
+ "model.language_model.layers.4.linear_attn.in_proj_b",
241
+ "model.language_model.layers.4.linear_attn.in_proj_qkv",
242
+ "model.language_model.layers.4.linear_attn.in_proj_z",
243
+ "model.language_model.layers.4.linear_attn.out_proj",
244
+ "model.language_model.layers.4.mlp.down_proj",
245
+ "model.language_model.layers.4.mlp.gate_proj",
246
+ "model.language_model.layers.4.mlp.up_proj",
247
+ "model.language_model.layers.5.linear_attn.in_proj_a",
248
+ "model.language_model.layers.5.linear_attn.in_proj_b",
249
+ "model.language_model.layers.5.linear_attn.in_proj_qkv",
250
+ "model.language_model.layers.5.linear_attn.in_proj_z",
251
+ "model.language_model.layers.5.linear_attn.out_proj",
252
+ "model.language_model.layers.5.mlp.down_proj",
253
+ "model.language_model.layers.5.mlp.gate_proj",
254
+ "model.language_model.layers.5.mlp.up_proj",
255
+ "model.language_model.layers.6.linear_attn.in_proj_a",
256
+ "model.language_model.layers.6.linear_attn.in_proj_b",
257
+ "model.language_model.layers.6.linear_attn.in_proj_qkv",
258
+ "model.language_model.layers.6.linear_attn.in_proj_z",
259
+ "model.language_model.layers.6.linear_attn.out_proj",
260
+ "model.language_model.layers.6.mlp.down_proj",
261
+ "model.language_model.layers.6.mlp.gate_proj",
262
+ "model.language_model.layers.6.mlp.up_proj",
263
+ "model.language_model.layers.7.mlp.down_proj",
264
+ "model.language_model.layers.7.mlp.gate_proj",
265
+ "model.language_model.layers.7.mlp.up_proj",
266
+ "model.language_model.layers.7.self_attn.k_proj",
267
+ "model.language_model.layers.7.self_attn.o_proj",
268
+ "model.language_model.layers.7.self_attn.q_proj",
269
+ "model.language_model.layers.7.self_attn.v_proj",
270
+ "model.language_model.layers.8.linear_attn.in_proj_a",
271
+ "model.language_model.layers.8.linear_attn.in_proj_b",
272
+ "model.language_model.layers.8.linear_attn.in_proj_qkv",
273
+ "model.language_model.layers.8.linear_attn.in_proj_z",
274
+ "model.language_model.layers.8.linear_attn.out_proj",
275
+ "model.language_model.layers.8.mlp.down_proj",
276
+ "model.language_model.layers.8.mlp.gate_proj",
277
+ "model.language_model.layers.8.mlp.up_proj",
278
+ "model.language_model.layers.9.linear_attn.in_proj_a",
279
+ "model.language_model.layers.9.linear_attn.in_proj_b",
280
+ "model.language_model.layers.9.linear_attn.in_proj_qkv",
281
+ "model.language_model.layers.9.linear_attn.in_proj_z",
282
+ "model.language_model.layers.9.linear_attn.out_proj",
283
+ "model.language_model.layers.9.mlp.down_proj",
284
+ "model.language_model.layers.9.mlp.gate_proj",
285
+ "model.language_model.layers.9.mlp.up_proj"
286
+ ],
287
+ "target_parameters": null,
288
+ "task_type": null,
289
+ "trainable_token_indices": null,
290
+ "use_bdlora": null,
291
+ "use_dora": false,
292
+ "use_qalora": false,
293
+ "use_rslora": false,
294
+ "velora_config": null
295
+ }
checkpoint/adapter/adapter_model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3ed169fe68b20a370bb856f9d91842543374ff3c98fd3bbe139ef2d0b97877a3
3
+ size 43352408
checkpoint/joint_head.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4f21c0f315cf1bcf852a9887d158928855eaa9bdcab6b9770d518abf3727fa1b
3
+ size 487063576
checkpoint/reference.json ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "absolute_tolerance": 0.0001,
3
+ "dataset_manifest_sha256": "aeddcfc1ea5390f12122d2e786c1e2030dcc97d3790b6d7523432548198b1f94",
4
+ "dataset_train_sha256": "edd682761db95a4f25bb30a284489c54d9336a36da0a9b19d2cda860b428baa8",
5
+ "max_length": 4096,
6
+ "probabilities": [
7
+ {
8
+ "r0x0": 0.2526208162307739,
9
+ "r0x1": 0.12492077797651291,
10
+ "r0x2": 0.08399814367294312,
11
+ "r0x3": 0.05339020863175392,
12
+ "r0x4": 0.04619518667459488,
13
+ "r0x5": 0.044043805450201035,
14
+ "r0x6": 0.0331004299223423,
15
+ "r0x7": 0.07307567447423935,
16
+ "r1x0": 0.06716147065162659,
17
+ "r1x1": 0.008080358617007732,
18
+ "r1x2": 0.004807046614587307,
19
+ "r1x3": 0.004841183312237263,
20
+ "r1x4": 0.005607179831713438,
21
+ "r1x5": 0.004920979030430317,
22
+ "r1x6": 0.004632336087524891,
23
+ "r1x7": 0.004383244551718235,
24
+ "r1x8": 0.01221535075455904,
25
+ "r2x0": 0.02925245091319084,
26
+ "r2x1": 0.009518387727439404,
27
+ "r2x2": 0.006364461965858936,
28
+ "r2x3": 0.007643744349479675,
29
+ "r2x4": 0.0082625737413764,
30
+ "r2x5": 0.00851234421133995,
31
+ "r2x6": 0.007145871873944998,
32
+ "r2x7": 0.012282103300094604,
33
+ "r3x0": 0.03042912669479847,
34
+ "r3x1": 0.006548034492880106,
35
+ "r3x2": 0.004051720257848501,
36
+ "r3x3": 0.003932017832994461,
37
+ "r3x4": 0.004734409507364035,
38
+ "r3x5": 0.0048585254698991776,
39
+ "r3x6": 0.0044471994042396545,
40
+ "r3x7": 0.00464443676173687,
41
+ "r3x8": 0.019378365948796272
42
+ },
43
+ {
44
+ "r0x0": 0.05243745446205139,
45
+ "r0x1": 0.0021382851991802454,
46
+ "r0x2": 0.0015760728856548667,
47
+ "r0x3": 0.002449487801641226,
48
+ "r0x4": 0.032596979290246964,
49
+ "r0x5": 0.37996670603752136,
50
+ "r0x6": 0.13670691847801208,
51
+ "r0x7": 0.12666313350200653,
52
+ "r1x0": 0.0826447382569313,
53
+ "r1x1": 0.00209549767896533,
54
+ "r1x2": 0.0008976972894743085,
55
+ "r1x3": 0.0008652249234728515,
56
+ "r1x4": 0.0014294564025476575,
57
+ "r1x5": 0.028665941208600998,
58
+ "r1x6": 0.008725162595510483,
59
+ "r1x7": 0.005233264062553644,
60
+ "r1x8": 0.009060735814273357,
61
+ "r2x0": 0.03716142103075981,
62
+ "r2x1": 0.002115321345627308,
63
+ "r2x2": 0.005341287236660719,
64
+ "r2x3": 0.001745575456880033,
65
+ "r2x4": 0.0019583813846111298,
66
+ "r2x5": 0.019835354760289192,
67
+ "r2x6": 0.007319271564483643,
68
+ "r2x7": 0.009388658218085766,
69
+ "r3x0": 0.005738606210798025,
70
+ "r3x1": 0.0012146560475230217,
71
+ "r3x2": 0.0012203187216073275,
72
+ "r3x3": 0.0019278834806755185,
73
+ "r3x4": 0.012783590704202652,
74
+ "r3x5": 0.0051281689666211605,
75
+ "r3x6": 0.0024644453078508377,
76
+ "r3x7": 0.0036620264872908592,
77
+ "r3x8": 0.0068422844633460045
78
+ },
79
+ {
80
+ "r0x0": 0.44664159417152405,
81
+ "r0x1": 0.00948373880237341,
82
+ "r0x2": 0.003061962779611349,
83
+ "r0x3": 0.0024329586885869503,
84
+ "r0x4": 0.0033077853731811047,
85
+ "r0x5": 0.0038714406546205282,
86
+ "r0x6": 0.004481742158532143,
87
+ "r0x7": 0.4447571933269501,
88
+ "r1x0": 0.02148980088531971,
89
+ "r1x1": 0.003787056542932987,
90
+ "r1x2": 0.0029788941610604525,
91
+ "r1x3": 0.002205177675932646,
92
+ "r1x4": 0.002177440095692873,
93
+ "r1x5": 0.002208774909377098,
94
+ "r1x6": 0.0032660418655723333,
95
+ "r1x7": 0.02149323932826519,
96
+ "r1x8": 0.02235523797571659
97
+ },
98
+ {
99
+ "r0x0": 0.07569620758295059,
100
+ "r0x1": 0.004194475244730711,
101
+ "r0x2": 0.002426150254905224,
102
+ "r0x3": 0.002500610426068306,
103
+ "r0x4": 0.002711081877350807,
104
+ "r0x5": 0.0028261507395654917,
105
+ "r0x6": 0.0035828864201903343,
106
+ "r0x7": 0.006198260933160782,
107
+ "r1x0": 0.5267710089683533,
108
+ "r1x1": 0.2791306972503662,
109
+ "r1x2": 0.05139099806547165,
110
+ "r1x3": 0.012985441833734512,
111
+ "r1x4": 0.005060354247689247,
112
+ "r1x5": 0.0031180710066109896,
113
+ "r1x6": 0.002313777571544051,
114
+ "r1x7": 0.014574174769222736,
115
+ "r1x8": 0.004519620444625616
116
+ }
117
+ ],
118
+ "row_ids": [
119
+ "seed-10000-turn-0",
120
+ "seed-10000-turn-1",
121
+ "seed-10000-turn-2",
122
+ "seed-10000-turn-3"
123
+ ]
124
+ }
checkpoint/training_config.json ADDED
@@ -0,0 +1,719 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "base_model": "Cloudflare/clef-flash",
3
+ "base_revision": "17f0b0ad64efb65d273590632833508766b2aae6",
4
+ "encoding_version": "stackcraft-clef-v1",
5
+ "extra": {
6
+ "config": {
7
+ "batch_size": 1,
8
+ "brier_weight": 0.1,
9
+ "clip_gradient_norm": 1.0,
10
+ "epochs": 2,
11
+ "gradient_accumulation": 8,
12
+ "label_smoothing": 0.05,
13
+ "learning_rate": 1e-05,
14
+ "max_length": 4096,
15
+ "mode": "lora",
16
+ "optimizer": "AdamW",
17
+ "rank": 4,
18
+ "seed": 42,
19
+ "selection": "external validation only; this script does not choose a checkpoint",
20
+ "weight_decay": 0.01
21
+ },
22
+ "dataset_config_sha256": "65b72544c80cf19f4ac3b12ca109d0ebda18fb70eadb4b170a55b1e8bb3b4f6d",
23
+ "dataset_counts": {
24
+ "train": 827,
25
+ "validation": 215
26
+ },
27
+ "dataset_manifest_sha256": "aeddcfc1ea5390f12122d2e786c1e2030dcc97d3790b6d7523432548198b1f94",
28
+ "dataset_source_commit": "70d84bd8dc5d4a60f3b96455a57d9e6f9416d109",
29
+ "dataset_split_sha256": {
30
+ "train": "edd682761db95a4f25bb30a284489c54d9336a36da0a9b19d2cda860b428baa8",
31
+ "validation": "eff9cdc5932e935959ac4d26dce6470d335090f7428a91930001954266d133bc"
32
+ },
33
+ "epoch": 2,
34
+ "examples": 827,
35
+ "mean_training_loss": 1.9667175294878694,
36
+ "optimizer_steps": 104,
37
+ "peak_allocated_bytes": 23226543104,
38
+ "peak_reserved_bytes": 24178065408,
39
+ "seconds": 744.6554337799898,
40
+ "shuffle_order_sha256": "b9d20efe77959592c99e9247a58d844be69df50db595bd87c6d7e595f5d2e0c4",
41
+ "source_commit": "d9fcd05c02e9327d4d791f2338725092cede8bdd",
42
+ "source_dirty": true,
43
+ "source_hashes": {
44
+ "scripts/train_clef.py": "e83586cf0fd0d7bb8c4aa15f9b858b9f9ffdb915622e355deb9513788d5054aa",
45
+ "src/stackcraft/clef.py": "c505f35d27121ba9ec9db3c663927ac8b28304f4ed864765be61ce96c8c86e13",
46
+ "src/stackcraft/data.py": "74b68a1f391a3128d2e499c7e86b6c94a1f2a2bef33ffe75c8631207065bd991",
47
+ "src/stackcraft/training.py": "f7e48b041b331cf2b68674ec2abb1059dd1da47cb261697b46260b4f0169dba8"
48
+ },
49
+ "test_trajectories_used": false,
50
+ "validation_used_for_training": false
51
+ },
52
+ "format_version": 1,
53
+ "head_shapes": {
54
+ "evidence_layers.0.attention.in_proj_bias": [
55
+ 3072
56
+ ],
57
+ "evidence_layers.0.attention.in_proj_weight": [
58
+ 3072,
59
+ 1024
60
+ ],
61
+ "evidence_layers.0.attention.out_proj.bias": [
62
+ 1024
63
+ ],
64
+ "evidence_layers.0.attention.out_proj.weight": [
65
+ 1024,
66
+ 1024
67
+ ],
68
+ "evidence_layers.0.feedforward.0.bias": [
69
+ 4096
70
+ ],
71
+ "evidence_layers.0.feedforward.0.weight": [
72
+ 4096,
73
+ 1024
74
+ ],
75
+ "evidence_layers.0.feedforward.3.bias": [
76
+ 1024
77
+ ],
78
+ "evidence_layers.0.feedforward.3.weight": [
79
+ 1024,
80
+ 4096
81
+ ],
82
+ "evidence_layers.0.feedforward_norm.bias": [
83
+ 1024
84
+ ],
85
+ "evidence_layers.0.feedforward_norm.weight": [
86
+ 1024
87
+ ],
88
+ "evidence_layers.0.memory_norm.bias": [
89
+ 1024
90
+ ],
91
+ "evidence_layers.0.memory_norm.weight": [
92
+ 1024
93
+ ],
94
+ "evidence_layers.0.query_norm.bias": [
95
+ 1024
96
+ ],
97
+ "evidence_layers.0.query_norm.weight": [
98
+ 1024
99
+ ],
100
+ "evidence_layers.1.attention.in_proj_bias": [
101
+ 3072
102
+ ],
103
+ "evidence_layers.1.attention.in_proj_weight": [
104
+ 3072,
105
+ 1024
106
+ ],
107
+ "evidence_layers.1.attention.out_proj.bias": [
108
+ 1024
109
+ ],
110
+ "evidence_layers.1.attention.out_proj.weight": [
111
+ 1024,
112
+ 1024
113
+ ],
114
+ "evidence_layers.1.feedforward.0.bias": [
115
+ 4096
116
+ ],
117
+ "evidence_layers.1.feedforward.0.weight": [
118
+ 4096,
119
+ 1024
120
+ ],
121
+ "evidence_layers.1.feedforward.3.bias": [
122
+ 1024
123
+ ],
124
+ "evidence_layers.1.feedforward.3.weight": [
125
+ 1024,
126
+ 4096
127
+ ],
128
+ "evidence_layers.1.feedforward_norm.bias": [
129
+ 1024
130
+ ],
131
+ "evidence_layers.1.feedforward_norm.weight": [
132
+ 1024
133
+ ],
134
+ "evidence_layers.1.memory_norm.bias": [
135
+ 1024
136
+ ],
137
+ "evidence_layers.1.memory_norm.weight": [
138
+ 1024
139
+ ],
140
+ "evidence_layers.1.query_norm.bias": [
141
+ 1024
142
+ ],
143
+ "evidence_layers.1.query_norm.weight": [
144
+ 1024
145
+ ],
146
+ "field_norm.bias": [
147
+ 1024
148
+ ],
149
+ "field_norm.weight": [
150
+ 1024
151
+ ],
152
+ "global_projection.weight": [
153
+ 1024,
154
+ 4096
155
+ ],
156
+ "hidden_norm.bias": [
157
+ 4096
158
+ ],
159
+ "hidden_norm.weight": [
160
+ 4096
161
+ ],
162
+ "joint_logit_scale": [],
163
+ "layers.0.linear1.bias": [
164
+ 4096
165
+ ],
166
+ "layers.0.linear1.weight": [
167
+ 4096,
168
+ 1024
169
+ ],
170
+ "layers.0.linear2.bias": [
171
+ 1024
172
+ ],
173
+ "layers.0.linear2.weight": [
174
+ 1024,
175
+ 4096
176
+ ],
177
+ "layers.0.multihead_attn.in_proj_bias": [
178
+ 3072
179
+ ],
180
+ "layers.0.multihead_attn.in_proj_weight": [
181
+ 3072,
182
+ 1024
183
+ ],
184
+ "layers.0.multihead_attn.out_proj.bias": [
185
+ 1024
186
+ ],
187
+ "layers.0.multihead_attn.out_proj.weight": [
188
+ 1024,
189
+ 1024
190
+ ],
191
+ "layers.0.norm1.bias": [
192
+ 1024
193
+ ],
194
+ "layers.0.norm1.weight": [
195
+ 1024
196
+ ],
197
+ "layers.0.norm2.bias": [
198
+ 1024
199
+ ],
200
+ "layers.0.norm2.weight": [
201
+ 1024
202
+ ],
203
+ "layers.0.norm3.bias": [
204
+ 1024
205
+ ],
206
+ "layers.0.norm3.weight": [
207
+ 1024
208
+ ],
209
+ "layers.0.self_attn.in_proj_bias": [
210
+ 3072
211
+ ],
212
+ "layers.0.self_attn.in_proj_weight": [
213
+ 3072,
214
+ 1024
215
+ ],
216
+ "layers.0.self_attn.out_proj.bias": [
217
+ 1024
218
+ ],
219
+ "layers.0.self_attn.out_proj.weight": [
220
+ 1024,
221
+ 1024
222
+ ],
223
+ "layers.1.linear1.bias": [
224
+ 4096
225
+ ],
226
+ "layers.1.linear1.weight": [
227
+ 4096,
228
+ 1024
229
+ ],
230
+ "layers.1.linear2.bias": [
231
+ 1024
232
+ ],
233
+ "layers.1.linear2.weight": [
234
+ 1024,
235
+ 4096
236
+ ],
237
+ "layers.1.multihead_attn.in_proj_bias": [
238
+ 3072
239
+ ],
240
+ "layers.1.multihead_attn.in_proj_weight": [
241
+ 3072,
242
+ 1024
243
+ ],
244
+ "layers.1.multihead_attn.out_proj.bias": [
245
+ 1024
246
+ ],
247
+ "layers.1.multihead_attn.out_proj.weight": [
248
+ 1024,
249
+ 1024
250
+ ],
251
+ "layers.1.norm1.bias": [
252
+ 1024
253
+ ],
254
+ "layers.1.norm1.weight": [
255
+ 1024
256
+ ],
257
+ "layers.1.norm2.bias": [
258
+ 1024
259
+ ],
260
+ "layers.1.norm2.weight": [
261
+ 1024
262
+ ],
263
+ "layers.1.norm3.bias": [
264
+ 1024
265
+ ],
266
+ "layers.1.norm3.weight": [
267
+ 1024
268
+ ],
269
+ "layers.1.self_attn.in_proj_bias": [
270
+ 3072
271
+ ],
272
+ "layers.1.self_attn.in_proj_weight": [
273
+ 3072,
274
+ 1024
275
+ ],
276
+ "layers.1.self_attn.out_proj.bias": [
277
+ 1024
278
+ ],
279
+ "layers.1.self_attn.out_proj.weight": [
280
+ 1024,
281
+ 1024
282
+ ],
283
+ "layers.2.linear1.bias": [
284
+ 4096
285
+ ],
286
+ "layers.2.linear1.weight": [
287
+ 4096,
288
+ 1024
289
+ ],
290
+ "layers.2.linear2.bias": [
291
+ 1024
292
+ ],
293
+ "layers.2.linear2.weight": [
294
+ 1024,
295
+ 4096
296
+ ],
297
+ "layers.2.multihead_attn.in_proj_bias": [
298
+ 3072
299
+ ],
300
+ "layers.2.multihead_attn.in_proj_weight": [
301
+ 3072,
302
+ 1024
303
+ ],
304
+ "layers.2.multihead_attn.out_proj.bias": [
305
+ 1024
306
+ ],
307
+ "layers.2.multihead_attn.out_proj.weight": [
308
+ 1024,
309
+ 1024
310
+ ],
311
+ "layers.2.norm1.bias": [
312
+ 1024
313
+ ],
314
+ "layers.2.norm1.weight": [
315
+ 1024
316
+ ],
317
+ "layers.2.norm2.bias": [
318
+ 1024
319
+ ],
320
+ "layers.2.norm2.weight": [
321
+ 1024
322
+ ],
323
+ "layers.2.norm3.bias": [
324
+ 1024
325
+ ],
326
+ "layers.2.norm3.weight": [
327
+ 1024
328
+ ],
329
+ "layers.2.self_attn.in_proj_bias": [
330
+ 3072
331
+ ],
332
+ "layers.2.self_attn.in_proj_weight": [
333
+ 3072,
334
+ 1024
335
+ ],
336
+ "layers.2.self_attn.out_proj.bias": [
337
+ 1024
338
+ ],
339
+ "layers.2.self_attn.out_proj.weight": [
340
+ 1024,
341
+ 1024
342
+ ],
343
+ "layers.3.linear1.bias": [
344
+ 4096
345
+ ],
346
+ "layers.3.linear1.weight": [
347
+ 4096,
348
+ 1024
349
+ ],
350
+ "layers.3.linear2.bias": [
351
+ 1024
352
+ ],
353
+ "layers.3.linear2.weight": [
354
+ 1024,
355
+ 4096
356
+ ],
357
+ "layers.3.multihead_attn.in_proj_bias": [
358
+ 3072
359
+ ],
360
+ "layers.3.multihead_attn.in_proj_weight": [
361
+ 3072,
362
+ 1024
363
+ ],
364
+ "layers.3.multihead_attn.out_proj.bias": [
365
+ 1024
366
+ ],
367
+ "layers.3.multihead_attn.out_proj.weight": [
368
+ 1024,
369
+ 1024
370
+ ],
371
+ "layers.3.norm1.bias": [
372
+ 1024
373
+ ],
374
+ "layers.3.norm1.weight": [
375
+ 1024
376
+ ],
377
+ "layers.3.norm2.bias": [
378
+ 1024
379
+ ],
380
+ "layers.3.norm2.weight": [
381
+ 1024
382
+ ],
383
+ "layers.3.norm3.bias": [
384
+ 1024
385
+ ],
386
+ "layers.3.norm3.weight": [
387
+ 1024
388
+ ],
389
+ "layers.3.self_attn.in_proj_bias": [
390
+ 3072
391
+ ],
392
+ "layers.3.self_attn.in_proj_weight": [
393
+ 3072,
394
+ 1024
395
+ ],
396
+ "layers.3.self_attn.out_proj.bias": [
397
+ 1024
398
+ ],
399
+ "layers.3.self_attn.out_proj.weight": [
400
+ 1024,
401
+ 1024
402
+ ],
403
+ "memory_projection.weight": [
404
+ 1024,
405
+ 4096
406
+ ],
407
+ "option_context_projection.weight": [
408
+ 1024,
409
+ 4096
410
+ ],
411
+ "option_lexical_projection.weight": [
412
+ 1024,
413
+ 4096
414
+ ],
415
+ "option_norm.bias": [
416
+ 1024
417
+ ],
418
+ "option_norm.weight": [
419
+ 1024
420
+ ],
421
+ "option_question_projection.weight": [
422
+ 1024,
423
+ 4096
424
+ ],
425
+ "option_summary_norm.bias": [
426
+ 1024
427
+ ],
428
+ "option_summary_norm.weight": [
429
+ 1024
430
+ ],
431
+ "prior_logit_scale": [],
432
+ "question_projection.weight": [
433
+ 1024,
434
+ 4096
435
+ ],
436
+ "residual_gate": [],
437
+ "residual_scorer.0.bias": [
438
+ 1024
439
+ ],
440
+ "residual_scorer.0.weight": [
441
+ 1024,
442
+ 4096
443
+ ],
444
+ "residual_scorer.3.bias": [
445
+ 1
446
+ ],
447
+ "residual_scorer.3.weight": [
448
+ 1,
449
+ 1024
450
+ ],
451
+ "type_embedding.weight": [
452
+ 3,
453
+ 1024
454
+ ]
455
+ },
456
+ "head_type": "native-joint-schema-fp32-gathered-rows-v1",
457
+ "lora": {
458
+ "alpha": 8,
459
+ "dropout": 0.0,
460
+ "rank": 4,
461
+ "target_modules": [
462
+ "model.language_model.layers.0.linear_attn.in_proj_a",
463
+ "model.language_model.layers.0.linear_attn.in_proj_b",
464
+ "model.language_model.layers.0.linear_attn.in_proj_qkv",
465
+ "model.language_model.layers.0.linear_attn.in_proj_z",
466
+ "model.language_model.layers.0.linear_attn.out_proj",
467
+ "model.language_model.layers.0.mlp.down_proj",
468
+ "model.language_model.layers.0.mlp.gate_proj",
469
+ "model.language_model.layers.0.mlp.up_proj",
470
+ "model.language_model.layers.1.linear_attn.in_proj_a",
471
+ "model.language_model.layers.1.linear_attn.in_proj_b",
472
+ "model.language_model.layers.1.linear_attn.in_proj_qkv",
473
+ "model.language_model.layers.1.linear_attn.in_proj_z",
474
+ "model.language_model.layers.1.linear_attn.out_proj",
475
+ "model.language_model.layers.1.mlp.down_proj",
476
+ "model.language_model.layers.1.mlp.gate_proj",
477
+ "model.language_model.layers.1.mlp.up_proj",
478
+ "model.language_model.layers.10.linear_attn.in_proj_a",
479
+ "model.language_model.layers.10.linear_attn.in_proj_b",
480
+ "model.language_model.layers.10.linear_attn.in_proj_qkv",
481
+ "model.language_model.layers.10.linear_attn.in_proj_z",
482
+ "model.language_model.layers.10.linear_attn.out_proj",
483
+ "model.language_model.layers.10.mlp.down_proj",
484
+ "model.language_model.layers.10.mlp.gate_proj",
485
+ "model.language_model.layers.10.mlp.up_proj",
486
+ "model.language_model.layers.11.mlp.down_proj",
487
+ "model.language_model.layers.11.mlp.gate_proj",
488
+ "model.language_model.layers.11.mlp.up_proj",
489
+ "model.language_model.layers.11.self_attn.k_proj",
490
+ "model.language_model.layers.11.self_attn.o_proj",
491
+ "model.language_model.layers.11.self_attn.q_proj",
492
+ "model.language_model.layers.11.self_attn.v_proj",
493
+ "model.language_model.layers.12.linear_attn.in_proj_a",
494
+ "model.language_model.layers.12.linear_attn.in_proj_b",
495
+ "model.language_model.layers.12.linear_attn.in_proj_qkv",
496
+ "model.language_model.layers.12.linear_attn.in_proj_z",
497
+ "model.language_model.layers.12.linear_attn.out_proj",
498
+ "model.language_model.layers.12.mlp.down_proj",
499
+ "model.language_model.layers.12.mlp.gate_proj",
500
+ "model.language_model.layers.12.mlp.up_proj",
501
+ "model.language_model.layers.13.linear_attn.in_proj_a",
502
+ "model.language_model.layers.13.linear_attn.in_proj_b",
503
+ "model.language_model.layers.13.linear_attn.in_proj_qkv",
504
+ "model.language_model.layers.13.linear_attn.in_proj_z",
505
+ "model.language_model.layers.13.linear_attn.out_proj",
506
+ "model.language_model.layers.13.mlp.down_proj",
507
+ "model.language_model.layers.13.mlp.gate_proj",
508
+ "model.language_model.layers.13.mlp.up_proj",
509
+ "model.language_model.layers.14.linear_attn.in_proj_a",
510
+ "model.language_model.layers.14.linear_attn.in_proj_b",
511
+ "model.language_model.layers.14.linear_attn.in_proj_qkv",
512
+ "model.language_model.layers.14.linear_attn.in_proj_z",
513
+ "model.language_model.layers.14.linear_attn.out_proj",
514
+ "model.language_model.layers.14.mlp.down_proj",
515
+ "model.language_model.layers.14.mlp.gate_proj",
516
+ "model.language_model.layers.14.mlp.up_proj",
517
+ "model.language_model.layers.15.mlp.down_proj",
518
+ "model.language_model.layers.15.mlp.gate_proj",
519
+ "model.language_model.layers.15.mlp.up_proj",
520
+ "model.language_model.layers.15.self_attn.k_proj",
521
+ "model.language_model.layers.15.self_attn.o_proj",
522
+ "model.language_model.layers.15.self_attn.q_proj",
523
+ "model.language_model.layers.15.self_attn.v_proj",
524
+ "model.language_model.layers.16.linear_attn.in_proj_a",
525
+ "model.language_model.layers.16.linear_attn.in_proj_b",
526
+ "model.language_model.layers.16.linear_attn.in_proj_qkv",
527
+ "model.language_model.layers.16.linear_attn.in_proj_z",
528
+ "model.language_model.layers.16.linear_attn.out_proj",
529
+ "model.language_model.layers.16.mlp.down_proj",
530
+ "model.language_model.layers.16.mlp.gate_proj",
531
+ "model.language_model.layers.16.mlp.up_proj",
532
+ "model.language_model.layers.17.linear_attn.in_proj_a",
533
+ "model.language_model.layers.17.linear_attn.in_proj_b",
534
+ "model.language_model.layers.17.linear_attn.in_proj_qkv",
535
+ "model.language_model.layers.17.linear_attn.in_proj_z",
536
+ "model.language_model.layers.17.linear_attn.out_proj",
537
+ "model.language_model.layers.17.mlp.down_proj",
538
+ "model.language_model.layers.17.mlp.gate_proj",
539
+ "model.language_model.layers.17.mlp.up_proj",
540
+ "model.language_model.layers.18.linear_attn.in_proj_a",
541
+ "model.language_model.layers.18.linear_attn.in_proj_b",
542
+ "model.language_model.layers.18.linear_attn.in_proj_qkv",
543
+ "model.language_model.layers.18.linear_attn.in_proj_z",
544
+ "model.language_model.layers.18.linear_attn.out_proj",
545
+ "model.language_model.layers.18.mlp.down_proj",
546
+ "model.language_model.layers.18.mlp.gate_proj",
547
+ "model.language_model.layers.18.mlp.up_proj",
548
+ "model.language_model.layers.19.mlp.down_proj",
549
+ "model.language_model.layers.19.mlp.gate_proj",
550
+ "model.language_model.layers.19.mlp.up_proj",
551
+ "model.language_model.layers.19.self_attn.k_proj",
552
+ "model.language_model.layers.19.self_attn.o_proj",
553
+ "model.language_model.layers.19.self_attn.q_proj",
554
+ "model.language_model.layers.19.self_attn.v_proj",
555
+ "model.language_model.layers.2.linear_attn.in_proj_a",
556
+ "model.language_model.layers.2.linear_attn.in_proj_b",
557
+ "model.language_model.layers.2.linear_attn.in_proj_qkv",
558
+ "model.language_model.layers.2.linear_attn.in_proj_z",
559
+ "model.language_model.layers.2.linear_attn.out_proj",
560
+ "model.language_model.layers.2.mlp.down_proj",
561
+ "model.language_model.layers.2.mlp.gate_proj",
562
+ "model.language_model.layers.2.mlp.up_proj",
563
+ "model.language_model.layers.20.linear_attn.in_proj_a",
564
+ "model.language_model.layers.20.linear_attn.in_proj_b",
565
+ "model.language_model.layers.20.linear_attn.in_proj_qkv",
566
+ "model.language_model.layers.20.linear_attn.in_proj_z",
567
+ "model.language_model.layers.20.linear_attn.out_proj",
568
+ "model.language_model.layers.20.mlp.down_proj",
569
+ "model.language_model.layers.20.mlp.gate_proj",
570
+ "model.language_model.layers.20.mlp.up_proj",
571
+ "model.language_model.layers.21.linear_attn.in_proj_a",
572
+ "model.language_model.layers.21.linear_attn.in_proj_b",
573
+ "model.language_model.layers.21.linear_attn.in_proj_qkv",
574
+ "model.language_model.layers.21.linear_attn.in_proj_z",
575
+ "model.language_model.layers.21.linear_attn.out_proj",
576
+ "model.language_model.layers.21.mlp.down_proj",
577
+ "model.language_model.layers.21.mlp.gate_proj",
578
+ "model.language_model.layers.21.mlp.up_proj",
579
+ "model.language_model.layers.22.linear_attn.in_proj_a",
580
+ "model.language_model.layers.22.linear_attn.in_proj_b",
581
+ "model.language_model.layers.22.linear_attn.in_proj_qkv",
582
+ "model.language_model.layers.22.linear_attn.in_proj_z",
583
+ "model.language_model.layers.22.linear_attn.out_proj",
584
+ "model.language_model.layers.22.mlp.down_proj",
585
+ "model.language_model.layers.22.mlp.gate_proj",
586
+ "model.language_model.layers.22.mlp.up_proj",
587
+ "model.language_model.layers.23.mlp.down_proj",
588
+ "model.language_model.layers.23.mlp.gate_proj",
589
+ "model.language_model.layers.23.mlp.up_proj",
590
+ "model.language_model.layers.23.self_attn.k_proj",
591
+ "model.language_model.layers.23.self_attn.o_proj",
592
+ "model.language_model.layers.23.self_attn.q_proj",
593
+ "model.language_model.layers.23.self_attn.v_proj",
594
+ "model.language_model.layers.24.linear_attn.in_proj_a",
595
+ "model.language_model.layers.24.linear_attn.in_proj_b",
596
+ "model.language_model.layers.24.linear_attn.in_proj_qkv",
597
+ "model.language_model.layers.24.linear_attn.in_proj_z",
598
+ "model.language_model.layers.24.linear_attn.out_proj",
599
+ "model.language_model.layers.24.mlp.down_proj",
600
+ "model.language_model.layers.24.mlp.gate_proj",
601
+ "model.language_model.layers.24.mlp.up_proj",
602
+ "model.language_model.layers.25.linear_attn.in_proj_a",
603
+ "model.language_model.layers.25.linear_attn.in_proj_b",
604
+ "model.language_model.layers.25.linear_attn.in_proj_qkv",
605
+ "model.language_model.layers.25.linear_attn.in_proj_z",
606
+ "model.language_model.layers.25.linear_attn.out_proj",
607
+ "model.language_model.layers.25.mlp.down_proj",
608
+ "model.language_model.layers.25.mlp.gate_proj",
609
+ "model.language_model.layers.25.mlp.up_proj",
610
+ "model.language_model.layers.26.linear_attn.in_proj_a",
611
+ "model.language_model.layers.26.linear_attn.in_proj_b",
612
+ "model.language_model.layers.26.linear_attn.in_proj_qkv",
613
+ "model.language_model.layers.26.linear_attn.in_proj_z",
614
+ "model.language_model.layers.26.linear_attn.out_proj",
615
+ "model.language_model.layers.26.mlp.down_proj",
616
+ "model.language_model.layers.26.mlp.gate_proj",
617
+ "model.language_model.layers.26.mlp.up_proj",
618
+ "model.language_model.layers.27.mlp.down_proj",
619
+ "model.language_model.layers.27.mlp.gate_proj",
620
+ "model.language_model.layers.27.mlp.up_proj",
621
+ "model.language_model.layers.27.self_attn.k_proj",
622
+ "model.language_model.layers.27.self_attn.o_proj",
623
+ "model.language_model.layers.27.self_attn.q_proj",
624
+ "model.language_model.layers.27.self_attn.v_proj",
625
+ "model.language_model.layers.28.linear_attn.in_proj_a",
626
+ "model.language_model.layers.28.linear_attn.in_proj_b",
627
+ "model.language_model.layers.28.linear_attn.in_proj_qkv",
628
+ "model.language_model.layers.28.linear_attn.in_proj_z",
629
+ "model.language_model.layers.28.linear_attn.out_proj",
630
+ "model.language_model.layers.28.mlp.down_proj",
631
+ "model.language_model.layers.28.mlp.gate_proj",
632
+ "model.language_model.layers.28.mlp.up_proj",
633
+ "model.language_model.layers.29.linear_attn.in_proj_a",
634
+ "model.language_model.layers.29.linear_attn.in_proj_b",
635
+ "model.language_model.layers.29.linear_attn.in_proj_qkv",
636
+ "model.language_model.layers.29.linear_attn.in_proj_z",
637
+ "model.language_model.layers.29.linear_attn.out_proj",
638
+ "model.language_model.layers.29.mlp.down_proj",
639
+ "model.language_model.layers.29.mlp.gate_proj",
640
+ "model.language_model.layers.29.mlp.up_proj",
641
+ "model.language_model.layers.3.mlp.down_proj",
642
+ "model.language_model.layers.3.mlp.gate_proj",
643
+ "model.language_model.layers.3.mlp.up_proj",
644
+ "model.language_model.layers.3.self_attn.k_proj",
645
+ "model.language_model.layers.3.self_attn.o_proj",
646
+ "model.language_model.layers.3.self_attn.q_proj",
647
+ "model.language_model.layers.3.self_attn.v_proj",
648
+ "model.language_model.layers.30.linear_attn.in_proj_a",
649
+ "model.language_model.layers.30.linear_attn.in_proj_b",
650
+ "model.language_model.layers.30.linear_attn.in_proj_qkv",
651
+ "model.language_model.layers.30.linear_attn.in_proj_z",
652
+ "model.language_model.layers.30.linear_attn.out_proj",
653
+ "model.language_model.layers.30.mlp.down_proj",
654
+ "model.language_model.layers.30.mlp.gate_proj",
655
+ "model.language_model.layers.30.mlp.up_proj",
656
+ "model.language_model.layers.31.mlp.down_proj",
657
+ "model.language_model.layers.31.mlp.gate_proj",
658
+ "model.language_model.layers.31.mlp.up_proj",
659
+ "model.language_model.layers.31.self_attn.k_proj",
660
+ "model.language_model.layers.31.self_attn.o_proj",
661
+ "model.language_model.layers.31.self_attn.q_proj",
662
+ "model.language_model.layers.31.self_attn.v_proj",
663
+ "model.language_model.layers.4.linear_attn.in_proj_a",
664
+ "model.language_model.layers.4.linear_attn.in_proj_b",
665
+ "model.language_model.layers.4.linear_attn.in_proj_qkv",
666
+ "model.language_model.layers.4.linear_attn.in_proj_z",
667
+ "model.language_model.layers.4.linear_attn.out_proj",
668
+ "model.language_model.layers.4.mlp.down_proj",
669
+ "model.language_model.layers.4.mlp.gate_proj",
670
+ "model.language_model.layers.4.mlp.up_proj",
671
+ "model.language_model.layers.5.linear_attn.in_proj_a",
672
+ "model.language_model.layers.5.linear_attn.in_proj_b",
673
+ "model.language_model.layers.5.linear_attn.in_proj_qkv",
674
+ "model.language_model.layers.5.linear_attn.in_proj_z",
675
+ "model.language_model.layers.5.linear_attn.out_proj",
676
+ "model.language_model.layers.5.mlp.down_proj",
677
+ "model.language_model.layers.5.mlp.gate_proj",
678
+ "model.language_model.layers.5.mlp.up_proj",
679
+ "model.language_model.layers.6.linear_attn.in_proj_a",
680
+ "model.language_model.layers.6.linear_attn.in_proj_b",
681
+ "model.language_model.layers.6.linear_attn.in_proj_qkv",
682
+ "model.language_model.layers.6.linear_attn.in_proj_z",
683
+ "model.language_model.layers.6.linear_attn.out_proj",
684
+ "model.language_model.layers.6.mlp.down_proj",
685
+ "model.language_model.layers.6.mlp.gate_proj",
686
+ "model.language_model.layers.6.mlp.up_proj",
687
+ "model.language_model.layers.7.mlp.down_proj",
688
+ "model.language_model.layers.7.mlp.gate_proj",
689
+ "model.language_model.layers.7.mlp.up_proj",
690
+ "model.language_model.layers.7.self_attn.k_proj",
691
+ "model.language_model.layers.7.self_attn.o_proj",
692
+ "model.language_model.layers.7.self_attn.q_proj",
693
+ "model.language_model.layers.7.self_attn.v_proj",
694
+ "model.language_model.layers.8.linear_attn.in_proj_a",
695
+ "model.language_model.layers.8.linear_attn.in_proj_b",
696
+ "model.language_model.layers.8.linear_attn.in_proj_qkv",
697
+ "model.language_model.layers.8.linear_attn.in_proj_z",
698
+ "model.language_model.layers.8.linear_attn.out_proj",
699
+ "model.language_model.layers.8.mlp.down_proj",
700
+ "model.language_model.layers.8.mlp.gate_proj",
701
+ "model.language_model.layers.8.mlp.up_proj",
702
+ "model.language_model.layers.9.linear_attn.in_proj_a",
703
+ "model.language_model.layers.9.linear_attn.in_proj_b",
704
+ "model.language_model.layers.9.linear_attn.in_proj_qkv",
705
+ "model.language_model.layers.9.linear_attn.in_proj_z",
706
+ "model.language_model.layers.9.linear_attn.out_proj",
707
+ "model.language_model.layers.9.mlp.down_proj",
708
+ "model.language_model.layers.9.mlp.gate_proj",
709
+ "model.language_model.layers.9.mlp.up_proj"
710
+ ]
711
+ },
712
+ "loss": {
713
+ "brier_reduction": "sum_options",
714
+ "brier_weight": 0.1,
715
+ "label_smoothing": 0.05
716
+ },
717
+ "mode": "lora",
718
+ "native_source_sha256": "0e304cf7c6500e8bb59bef7e2afd2c6373f82596dfb3b57d1aa93c175e2dc3a3"
719
+ }
code/.dockerignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Send only game source and locked build inputs to Docker.
2
+ **
3
+ !Dockerfile
4
+ !.dockerignore
5
+ !pyproject.toml
6
+ !uv.lock
7
+ !README.md
8
+ !LICENSE
9
+ !NOTICE
10
+ !src/
11
+ !src/stackcraft/
12
+ !src/stackcraft/**
13
+ **/__pycache__/
14
+ **/*.pyc
code/.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.13
code/Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.13-slim-bookworm@sha256:5024f48ba9441d4b13a95d3945abc6365538e3a31109833367a1923523c6efed AS builder
2
+ COPY --from=ghcr.io/astral-sh/uv:0.12.23@sha256:61d393e44e249f2e4b526b6c7ddcecce245946826e608e11c93ad4f5bba55b21 /uv /usr/local/bin/uv
3
+ ENV UV_PROJECT_ENVIRONMENT=/opt/venv \
4
+ UV_PYTHON_DOWNLOADS=never \
5
+ UV_LINK_MODE=copy
6
+ WORKDIR /opt/build
7
+ COPY pyproject.toml uv.lock README.md LICENSE NOTICE ./
8
+ COPY src/ ./src/
9
+ RUN uv sync --locked --no-dev --no-editable --python /usr/local/bin/python
10
+
11
+ FROM python:3.13-slim-bookworm@sha256:5024f48ba9441d4b13a95d3945abc6365538e3a31109833367a1923523c6efed AS runtime
12
+ RUN useradd --create-home --uid 1000 stackcraft
13
+ COPY --from=builder --chown=1000:1000 /opt/venv /opt/venv
14
+ COPY --chown=1000:1000 LICENSE NOTICE /home/stackcraft/
15
+ USER 1000
16
+ WORKDIR /home/stackcraft
17
+ ENV PORT=7860 \
18
+ PYTHONUNBUFFERED=1 \
19
+ PYTHONDONTWRITEBYTECODE=1
20
+ EXPOSE 7860
21
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
22
+ CMD /opt/venv/bin/python -c "import os, urllib.request; urllib.request.urlopen('http://127.0.0.1:' + os.environ.get('PORT', '7860') + '/health', timeout=3)"
23
+ CMD ["/bin/sh", "-c", "exec /opt/venv/bin/stackcraft serve --host 0.0.0.0 --port \"${PORT:-7860}\""]
code/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 2026 Alibaba Cloud
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.
code/NOTICE ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Stackcraft
2
+ Copyright 2026 Stackcraft contributors.
3
+
4
+ Stackcraft is an independent educational falling-block game and fine-tuning study.
5
+ It is not affiliated with or endorsed by Tetris, Cloudflare, or the Qwen project.
6
+
7
+ The optional trained model derives from Cloudflare/Clef-Flash (Apache-2.0),
8
+ revision 17f0b0ad64efb65d273590632833508766b2aae6, which derives from
9
+ Qwen/Qwen3.5-9B. Original backbone weights and native joint-schema implementation
10
+ remain upstream dependencies; this repository does not redistribute the backbone.
11
+ Stackcraft adds game-specific LoRA matrices and trains a separate native decision
12
+ head. The trained head and LoRA weights must be loaded together.
13
+
14
+ Upstream model: https://huggingface.co/Cloudflare/clef-flash
15
+ Base architecture: https://huggingface.co/Qwen/Qwen3.5-9B
16
+ Python package dependency licenses remain with their respective authors.
code/README.md ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stackcraft
2
+
3
+ A playable, deterministic falling-block game for studying decision models. The goal is a fair comparison of a heuristic player, unchanged Clef-flash, and a fine-tuned model on identical piece sequences.
4
+
5
+ **Status:** training, validation selection, fresh-process reload and the 1,000-game
6
+ held-out evaluation are complete. The final report and recorded demo passed review. See [the release tutorial](docs/tutorials/06-release.md)
7
+ for milestones and acceptance criteria.
8
+
9
+ ## Releases
10
+
11
+ - [Planned CPU demo](https://huggingface.co/spaces/nima1/stackcraft) — hosting awaits the account requirement below
12
+ - [Model adapter and decision head](https://huggingface.co/nima1/stackcraft-clef-flash-lora)
13
+ - [Synthetic dataset](https://huggingface.co/datasets/nima1/stackcraft-data)
14
+ - [Source and release verification](https://github.com/kkarimi/stackcraft)
15
+
16
+ The demo replays actual model decisions alongside live human play. It needs no
17
+ GPU hosting. All four destinations are approved for public release. Hugging Face
18
+ currently requires a PRO subscription to create the Docker CPU Space; the local
19
+ demo works, and hosted publication awaits that account requirement.
20
+
21
+ ## What the experiment found
22
+
23
+ On 200 paired sequences per player, with a 200-piece cap:
24
+
25
+ | Player | Mean lines | Mean pieces placed | Mean decision time |
26
+ | --- | ---: | ---: | ---: |
27
+ | Unchanged Clef-flash | 0.070 | 26.020 | 149.11 ms |
28
+ | Unchanged FP32-head control | 0.105 | 26.000 | 150.19 ms |
29
+ | Fine-tuned Clef-flash | 16.810 | 83.565 | 206.81 ms |
30
+ | Random legal placement | 0.140 | 26.645 | 0.0015 ms |
31
+ | Fixed heuristic | 76.530 | 200.000 | 0.1985 ms |
32
+
33
+ Fine-tuning improved mean lines over native Clef by **16.74**, with a paired 95%
34
+ bootstrap interval of **[15.74, 17.77]**. The same improvement remains against the
35
+ unchanged FP32-head control. The heuristic cleared substantially more lines than
36
+ the trained model and was about 1,042 times faster per decision on this machine.
37
+ All players had zero errors or invalid decisions. The heuristic reached the cap
38
+ in every game; its uncapped survival was not measured.
39
+
40
+ This is a successful adaptation experiment and a clear example of why a cheap
41
+ baseline matters. It does not establish that a large model is the best tool for
42
+ this game. See [the full study report](reports/final-study.md) for medians,
43
+ uncertainty, validation results, limitations and reproducible evidence.
44
+
45
+
46
+ ## Run locally
47
+
48
+ Install [uv](https://docs.astral.sh/uv/), then from this directory:
49
+
50
+ ```bash
51
+ uv sync --locked
52
+ uv run --locked stackcraft serve --port 8087
53
+ ```
54
+
55
+ Open http://127.0.0.1:8087. The game needs no GPU or model credential. Sessions are local, in memory, and expire when the server restarts; download a replay to retain a game. The local service retains 128 sessions and limits each to 2,000 placements. Use a single server worker. The game does not require an account.
56
+
57
+ ## Development
58
+
59
+ ```bash
60
+ uv run --locked ruff check .
61
+ uv run --locked ruff format --check .
62
+ uv run --locked ty check src/stackcraft --exclude src/stackcraft/training.py
63
+ uv run --locked pytest
64
+ ```
65
+
66
+ These commands check the game without installing ML packages. Full-repository
67
+ type checking also covers training scripts and requires the ML extra:
68
+
69
+ ```bash
70
+ uv run --locked --extra ml ty check
71
+ ```
72
+
73
+ Installing that extra does not run GPU inference. When keeping an ML environment,
74
+ use `--extra ml` on uv commands to prevent synchronization from removing those
75
+ optional packages.
76
+
77
+ The committed lockfile fixes dependency versions; `uv sync --locked` refuses to silently update it. Python 3.13 is selected by `.python-version`. No pip activation or global dependency installation is needed.
78
+
79
+ Read [tutorial 00](docs/tutorials/00-foundation.md) for the setup rationale, [the rules](docs/rules.md) for the exact game, and [the API contract](docs/interfaces.md) for integration. This is a simplified turn-based placement game, with no gravity timer, hold or wall kicks. It is not affiliated with Tetris.
80
+
81
+ ## Offline baseline tournament
82
+
83
+ ```bash
84
+ uv run --locked stackcraft tournament --seed-start 1000 --episodes 20 --max-pieces 100 --output runs/development.json
85
+ ```
86
+
87
+ This runs random and fixed heuristic players on identical seeds, records every decision and replay, and refuses to overwrite an existing result. See [tutorial02](docs/tutorials/02-baselines.md). Development scores are not held-out model results.
88
+
89
+ ## Expert-labelled data
90
+
91
+ ```bash
92
+ uv run --locked stackcraft generate-data --output data/study-v1
93
+ ```
94
+
95
+ This generates training/validation episodes from fixed disjoint seed pools, labels moves with bounded search, removes duplicate observations and writes a hashed manifest. Final test seeds remain reserved. See [tutorial03](docs/tutorials/03-data.md).
96
+
97
+ Optional Clef tooling uses `uv sync --locked --extra ml`. Its native encoder, real GPU inference, finite head/LoRA gradients and exact fresh-process checkpoint reload have been verified. Full-study quality is a separate evaluation. See [native adapter documentation](docs/clef.md).
98
+
99
+ ## The model used here
100
+
101
+ The base is **Cloudflare/clef-flash**, built on Qwen3.5-9B, pinned at
102
+ `17f0b0ad64efb65d273590632833508766b2aae6`. It produces probabilities over every
103
+ supplied legal move instead of generating text. Training updates rank-4 text-layer
104
+ LoRA matrices and the native joint decision head; the original backbone is frozen.
105
+ The checkpoint must save and load both the adapter and head.
106
+
107
+ On the RTX 5090, the short native inference pilot allocated 19.7 GB and averaged
108
+ 144 ms per decision. The training probe allocated 23.2 GB; both head-only and LoRA-plus-head
109
+ checkpoints reproduced their reference probabilities exactly in fresh processes.
110
+ These are engineering measurements, not evidence of better game play. See
111
+ [the measured feasibility report](reports/gpu-feasibility.json).
112
+
113
+ ## Follow the milestones
114
+
115
+ 1. [Foundation and modern Python tools](docs/tutorials/00-foundation.md)
116
+ 2. [Deterministic game and replay](docs/tutorials/01-game.md)
117
+ 3. [Cheap and unchanged-model baselines](docs/tutorials/02-baselines.md)
118
+ 4. [Search-teacher data and leakage checks](docs/tutorials/03-data.md)
119
+ 5. [Real GPU training and checkpoint fidelity](docs/tutorials/04-gpu-feasibility.md)
120
+ 6. [Validation selection and paired game evaluation](docs/tutorials/05-evaluation.md)
121
+ 7. [Release, public reproduction and fresh-download checks](docs/tutorials/06-release.md)
122
+
123
+ Each tutorial explains the commands, evidence, design choices and alternatives.
124
+ The [evaluation protocol](docs/evaluation-protocol.md) freezes 200 test sequences
125
+ and a 200-piece cap before results. It compares the selected model with the native
126
+ base, an unchanged FP32-head ablation, random moves and a fast heuristic.
127
+
128
+ The training runner intentionally accepts only the exact frozen study dataset.
129
+ A newly generated dataset can have different provenance hashes even if its labels
130
+ match. For exact reproduction use the released dataset or Tutorial03's source-hash
131
+ checked reconstruction. A new dataset or hyperparameter study should be recorded
132
+ as a new experiment, not quietly substituted into this study.
133
+
134
+ ## Demo hosting
135
+
136
+ The [Docker deployment guide](docs/demo-hosting.md) builds a CPU-only image with
137
+ no model credentials or Torch. Human gameplay is live; model comparisons replay
138
+ recorded decisions and probabilities. Playback speed is separate from measured
139
+ inference latency. Local packaging has been verified. Release verification is recorded in the GitHub repository.
140
+
141
+ Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE). Stackcraft is independent
142
+ of Cloudflare, Qwen and Tetris.
code/docs/clef.md ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Native Clef adapter
2
+
3
+ Status: native encoding, real-weight BF16 inference and bounded head/LoRA GPU
4
+ training have passed. Fresh-process checkpoint parity and full held-out quality
5
+ are separate gates; see the plan and reports for their current status. Fake
6
+ components in unit tests exercise mapping and failure handling only.
7
+
8
+ ## Pinned upstream contract
9
+
10
+ - Model: `Cloudflare/clef-flash`.
11
+ - Revision: `17f0b0ad64efb65d273590632833508766b2aae6`.
12
+ - Native file: `joint_schema_model.py`.
13
+ - Source SHA256: `0e304cf7c6500e8bb59bef7e2afd2c6373f82596dfb3b57d1aa93c175e2dc3a3`.
14
+ - Observation encoding: `stackcraft-clef-v1`.
15
+
16
+ Inspected source: [pinned native implementation](https://huggingface.co/Cloudflare/clef-flash/blob/17f0b0ad64efb65d273590632833508766b2aae6/joint_schema_model.py).
17
+ The loader pins a complete snapshot and supplies its local directory to upstream
18
+ `load_release_model`; passing only the repository name to upstream would download
19
+ an unpinned snapshot. `import_pinned_source` checks exact file bytes before
20
+ executing them. Callers must explicitly set `trust_pinned_code=True`: this executes
21
+ reviewed upstream Python with the process's permissions, not in a sandbox.
22
+
23
+ The unchanged loader constructs `Qwen3_5ForConditionalGeneration`, loads a separate
24
+ `JointSchemaHead`, then constructs `AutoProcessor`. Its processor configuration is
25
+ multimodal even though this application passes only text. Optional ML dependencies
26
+ must therefore support that native construction; importing `stackcraft.clef` or
27
+ playing the game itself requires none of them.
28
+
29
+ ## Input and outputs
30
+
31
+ `observation_record(observation)` includes the 20 board rows, current piece, exactly
32
+ one preview piece, rules and coordinate definitions. Occupied cells are `#` and
33
+ empty cells are `.`; colors carry no decision-relevant information in these rules.
34
+ The native choice question contains every legal action ID with its rotation,
35
+ column, landing row and four absolute cell coordinates. It does not include a seed,
36
+ hidden stream, score-derived helper features, or heuristic candidate pruning.
37
+
38
+ Native choice encoding sorts option IDs lexicographically. Probabilities must be
39
+ mapped using `EncodedQuestion.option_ids`, rather than the input dictionary order.
40
+ The adapter reads `model(collate_records([encoded], pad_id, device))[0][0]` and takes
41
+ FP32 softmax. It retains unrounded float probabilities. Upstream `systemone()`
42
+ rounds probabilities to four decimals, which is unsuitable for precise validation
43
+ and probability metrics. On exact probability ties, Stackcraft uses the shared
44
+ engine action order.
45
+
46
+ ## Context integrity
47
+
48
+ The native encoder reserves schema tokens and silently truncates state tokens to
49
+ fit `max_length`. Losing some board rows would change the game state presented to
50
+ the model. Before encoding, Stackcraft counts every separately tokenized segment
51
+ used by the pinned encoder. It rejects the entire decision if complete input does
52
+ not fit the configured limit, then checks the resulting length and option IDs.
53
+ It never removes choices or board rows to make a record fit.
54
+
55
+ The default limit is 4,096 tokens, a memory-conscious initial setting, not a claim
56
+ about the backbone's advertised context. If observed positions exceed it, measure
57
+ the required size and preregister a consistently larger limit for both base and
58
+ trained models. Any different text representation needs a new encoding version.
59
+
60
+ ## Integration example
61
+
62
+ After ML dependencies and the pinned snapshot are available, and available GPU
63
+ memory has been checked:
64
+
65
+ ```python
66
+ from stackcraft.clef import ClefPlayer
67
+ from stackcraft.engine import new_game
68
+ from stackcraft.players import observe
69
+
70
+ player = ClefPlayer.from_pretrained(
71
+ trust_pinned_code=True,
72
+ local_files_only=True,
73
+ device="cuda",
74
+ max_length=4096,
75
+ )
76
+ decision = player.choose(observe(new_game(42)))
77
+ print(decision.action_id, decision.probabilities, player.last_input_tokens)
78
+ ```
79
+
80
+ This example loads real 9B weights. Real inference passed on an RTX 5090.
81
+ The loader does not stop other workloads, allocate rented compute, or implicitly
82
+ download missing weights by default. A caller can deliberately set
83
+ `local_files_only=False` to obtain this pinned snapshot. Model identity, source
84
+ hash and encoding version must accompany measurements.
85
+
86
+ `player.runtime_config` records model ID, player/checkpoint revision, pinned base
87
+ revision, encoding version, native source SHA256 and configured context limit.
88
+ After each successful decision it also records the actual first model parameter's
89
+ dtype and device. These are initially null until inference succeeds; construction
90
+ does not consume model parameters or import Torch for metadata. The native release
91
+ uses one device and dtype; this field is not a full precision inventory for future
92
+ mixed-precision training models. `last_input_tokens` records the successful
93
+ decision's complete encoded length. Tournament artifacts should snapshot runtime
94
+ metadata and retain token counts per decision, rather than infer them from defaults.
95
+
96
+ Run the offline adapter checks with:
97
+
98
+ ```sh
99
+ uv run --locked pytest tests/test_clef.py
100
+ ```
101
+
102
+ CPU verification used the actual `Qwen3VLProcessor` and native `encode_record`,
103
+ without constructing a backbone or loading weights. Fourteen fixtures covered
104
+ all seven pieces on an empty board and on a dense 15-row patterned stack, including
105
+ 34-choice positions. Exact token counts matched in every case: 848–2,248 tokens,
106
+ all within 4,096. Rejecting a limit one token below each required size also passes
107
+ the dedicated integration check. These fixtures do not establish a universal
108
+ maximum; every inference request still runs preflight.
109
+
110
+ Reproduce that optional check after installing the ML extra and caching pinned
111
+ tokenizer/processor/source files:
112
+
113
+ ```sh
114
+ STACKCRAFT_TEST_NATIVE_ENCODING=1 uv run --locked --extra ml pytest tests/test_clef.py
115
+ ```
116
+
117
+ The two-seed real-weight development baseline used19.69GB peak allocated memory
118
+ and averaged144ms per decision; reports/clef-development.json records the result.
119
+ These development measurements do not establish trained-model quality. Training must call native
120
+ `ClefModel.forward` with gradients enabled; `choose()` intentionally uses inference
121
+ mode and must not be used as a training path.
code/docs/demo-hosting.md ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Host the playable game and recorded comparison
2
+
3
+ The demo serves the actual Python game and validated recorded model decisions. It
4
+ requires no GPU, Hugging Face token, Torch installation or model download. Human
5
+ moves are live simulator calls; comparison boards replay decisions measured during
6
+ the evaluation. Playback timing is deliberately separate from inference latency.
7
+
8
+ This is a deployment package, not evidence of publication. Create or upload a new
9
+ Space only after the project owner confirms its destination and visibility.
10
+
11
+ ## Export the fixed example after evaluation
12
+
13
+ The demonstration seed is **30000**, chosen before inspecting results. Do not pick
14
+ a better-looking seed after the evaluation. A single game illustrates behavior;
15
+ the aggregate evaluation report establishes the quantitative result.
16
+
17
+ After the final tournament has completed, export to a new file:
18
+
19
+ ```bash
20
+ uv run --locked python scripts/export_demo.py \
21
+ --input runs/final-evaluation/report.json \
22
+ --players base trained heuristic \
23
+ --seed 30000 \
24
+ --output runs/release-demo/baseline-demo.json
25
+ ```
26
+
27
+ The input path above is an example; use the actual complete evaluation report.
28
+ `--input` can be repeated for separate tournament or individual episode artifacts.
29
+ The exporter requires two to four distinct players, an identical piece cap, and
30
+ exactly one recording per selected player at the fixed seed. The optional
31
+ `base-fp32` player is a precision ablation, not the primary unchanged model.
32
+
33
+ The exporter replays every selected action with the authoritative engine and
34
+ checks final outcomes, stop reasons, observations, probability distributions and
35
+ decision/action alignment. It preserves errors as errors. It keeps complete
36
+ replays, compact decision probabilities, measured decision times, actual player
37
+ revisions, runtime metadata, source filenames and SHA-256 hashes. It strips the
38
+ large repeated observation payloads because the game can reconstruct them.
39
+ Neither missing players nor invalid records are replaced with simulated model
40
+ results. Existing outputs are never overwritten.
41
+
42
+ After the results page's publication destination is confirmed, optionally add
43
+ `--report-url` with its absolute HTTPS URL. This adds a "Full study results" link
44
+ to the race without changing source hashes or recorded decisions. Omit it until
45
+ the actual destination is known; the UI hides the link when absent or invalid.
46
+
47
+ The JSON is compatible with the race manifest loaded from
48
+ `src/stackcraft/web/baseline-demo.json`. Before a release, review the exported
49
+ example and replace that packaged asset explicitly. The checked-in development
50
+ asset must remain labelled as development until actual evaluation replays replace
51
+ it. Do not rename a heuristic recording to make a missing model board appear.
52
+
53
+ ## Build the game-only container
54
+
55
+ ```bash
56
+ docker build -t stackcraft-demo:local .
57
+ docker run --rm --name stackcraft-demo-local \
58
+ -p 127.0.0.1:7860:7860 stackcraft-demo:local
59
+ ```
60
+
61
+ Open `http://127.0.0.1:7860`. The container runs as UID 1000 and listens on all
62
+ interfaces inside the container; this local command exposes it only on loopback.
63
+ The Dockerfile uses Python 3.13 and a versioned uv executable. Its build stage runs
64
+ `uv sync --locked --no-dev --no-editable` with no ML extra. Installing the project
65
+ rather than an editable source link checks that the wheel contains HTML, CSS,
66
+ JavaScript and the replay JSON. The runtime stage contains that environment and
67
+ uses the CLI directly, with no dependency resolution at startup. This follows the
68
+ [uv container installation guidance](https://docs.astral.sh/uv/guides/integration/docker/).
69
+
70
+ `.dockerignore` allows only the source and locked build inputs into the build
71
+ context. Local credentials, model weights, training runs, `.venv`, raw datasets
72
+ and Git history are excluded. The image does not need them to reconstruct games.
73
+ The dependency lock freezes Python packages. Both the Python base and uv image
74
+ are pinned by digest; the verified Python base supplies 3.13.16. Update these
75
+ explicitly when upgrading the deployment, and record the final application image
76
+ identity for the published build.
77
+
78
+ The container defaults to port 7860. To change it locally:
79
+
80
+ ```bash
81
+ docker run --rm --name stackcraft-demo-local \
82
+ -e PORT=8090 -p 127.0.0.1:8090:8090 stackcraft-demo:local
83
+ ```
84
+
85
+ This container setting does not change the normal CLI's default local port 8080.
86
+ Use a free host port; do not stop another application merely to reclaim 8080.
87
+
88
+ ## Configure a Hugging Face Docker Space
89
+
90
+ As checked on 2026-10-06, Hugging Face requires a PRO subscription for a personal
91
+ account to create a Docker Space, including `cpu-basic`. That hardware has no
92
+ hourly charge, but it is not subscription-free hosting. Static Spaces have different
93
+ requirements and cannot run this Python HTTP service unchanged. See the
94
+ [official Spaces overview](https://huggingface.co/docs/hub/spaces-overview).
95
+ The initial `nima1/stackcraft` creation returned HTTP 402; no subscription or paid
96
+ hardware was purchased. Resolve the account requirement or choose another CPU host
97
+ before following the deployment steps below.
98
+
99
+ Once publication is authorized, the Space repository needs the Dockerfile,
100
+ `.dockerignore`, `pyproject.toml`, `uv.lock`, `src/` including the reviewed replay
101
+ asset, and a README. Put this configuration at the top of the Space README:
102
+
103
+ ```yaml
104
+ ---
105
+ title: Stackcraft
106
+ emoji: 🧩
107
+ colorFrom: green
108
+ colorTo: purple
109
+ sdk: docker
110
+ app_port: 7860
111
+ ---
112
+ ```
113
+
114
+ The README body should link the model, dataset, code, aggregate report and
115
+ reproduction tutorials, and explain which activity is live. The Space configuration
116
+ uses `sdk: docker` and the same exposed port as the application. UID 1000 matches
117
+ Hugging Face's documented container permissions. No GPU hardware upgrade or model
118
+ credential is needed for this recorded demonstration. See the official
119
+ [Docker Spaces documentation](https://huggingface.co/docs/hub/spaces-sdks-docker).
120
+
121
+ Hugging Face builds from the Space repository. Upload only the explicit release
122
+ files, not the entire working directory. The source repository's visibility and
123
+ the Space's visibility are separate publication choices.
124
+
125
+ ## Check the actual deployment
126
+
127
+ Run these checks against the built image and repeat them against the published
128
+ Space URL after release:
129
+
130
+ 1. `/health` returns `{"status":"ok"}` and `/` loads its packaged assets.
131
+ 2. A human can rotate, place a piece, restart and download a replay.
132
+ 3. Opening that replay reproduces the same score, lines and board.
133
+ 4. The race clearly says recorded; scrub to both the first and last frame.
134
+ 5. The shorter run stays at its actual final state. An error is labelled as an
135
+ error, and reaching the episode cap is distinct from top-out.
136
+ 6. The selected action and any probability alternatives come from the recorded
137
+ decision at that position. They are not an explanation generated after the run.
138
+ 7. "Start live board · seed N" explicitly starts a new human game beside the
139
+ recorded players using the exact recording seed. Check the same episode cap,
140
+ keyboard controls, per-move recording synchronization and the live/recorded labels.
141
+ Scrubbing recorded moves must leave the human board unchanged. "Match my move"
142
+ restores the shared piece index. Switching Play/race preserves the human session;
143
+ "Leave comparison" returns it to uncapped ordinary play without a reset. On narrow
144
+ screens boards stack so controls remain readable; check for horizontal overflow.
145
+ 8. The running image imports the game without Torch. No requests download model
146
+ weights, and no inference service is contacted.
147
+
148
+ Game sessions live in process memory. A container restart discards them, and the
149
+ service retains a bounded number of sessions. Save a replay for durable evidence.
150
+ Use one application worker with the current in-memory session store; independent
151
+ worker processes would not share game IDs. A larger deployment would need a
152
+ shared session store and request limits, rather than silently adding workers.
153
+
154
+ Keep the final image identity, released manifest hash and successful URL checks in
155
+ `record.md`. A local Dockerfile or a successful upload alone does not establish
156
+ that the public comparison works.
code/docs/evaluation-protocol.md ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stackcraft evaluation protocol — completed study, frozen procedure
2
+
3
+ The final test used all **200 episode seeds, 30000–30199**, at the frozen 200-piece
4
+ cap, with all five players and no errors. Training, validation selection, reload
5
+ verification, final evaluation and the independent CPU audit are complete. The
6
+ [final study](../reports/final-study.md) reports the measured outcomes and
7
+ [compact JSON](../reports/final-summary.json) preserves exact statistics and hashes.
8
+ Release verification is tracked separately in the source repository.
9
+
10
+ The procedure below records decisions frozen before test results were opened;
11
+ completion does not authorize tuning against these now-disclosed sequences.
12
+ `evaluation.py` consumes existing episode artifacts and position predictions;
13
+ it does not invoke models, select checkpoints, or start games.
14
+
15
+ ## Decisions that must be frozen before opening the test results
16
+
17
+ The final episode cap is now **frozen at 200 pieces**, using all 200 reserved seeds.
18
+ This was chosen before inspecting any test results. The unchanged-model development
19
+ pilot measured approximately 144 ms per decision. If all three neural players reach
20
+ every cap, 120,000 decisions would take roughly 4.8 hours at that rate, excluding
21
+ loading and reporting. Earlier base development games died much sooner; runtime
22
+ must still be measured and reported rather than promised. Each episode is saved
23
+ atomically so interrupted evaluation can resume without discarding difficult games.
24
+
25
+ Freeze the selected trained checkpoint and original pinned Clef revision, input
26
+ encoding, precision, hardware/runtime configuration, heuristic revision, all
27
+ seeds, cap, inference failure behavior, bootstrap parameters and compute budget.
28
+ The same legal placements and one-piece preview must reach every player. Primary
29
+ base is the pinned native model with its original BF16 head. Training uses an FP32
30
+ head; therefore an explicitly named `base-fp32` ablation keeps the original weights
31
+ but uses the same FP32 wrapper as training. Report trained-versus-native-base as
32
+ the primary comparison and trained-versus-base-fp32 to separate training from head
33
+ precision effects. Backbone BF16, input encoding, context limit, device and source
34
+ revision must agree. A changed base quantization would be a separate comparison.
35
+
36
+ Checkpoint selection uses validation only. Teacher-action agreement and target
37
+ cross-entropy on validation positions are diagnostic selection metrics, separate
38
+ from complete-game outcomes. A model that imitates the expert more closely may
39
+ still play worse. Preregister any training configuration search and tie rule;
40
+ `paired_report` never makes those choices from test results.
41
+
42
+ ## Paired episode outcomes
43
+
44
+ Primary outcome is lines cleared. Secondary outcomes are game score and pieces
45
+ placed. Retain every planned seed for each player, including failed episodes.
46
+ Duplicate player/seed pairs, mismatched seed sets, caps or rules, and changing
47
+ player identity/runtime within a comparison are rejected by the report function.
48
+ Its schema checks are not a replacement for verifying replay integrity when
49
+ episodes are collected.
50
+
51
+ An inference exception or illegal decision ends that player's episode. For the
52
+ primary analysis, assign **zero lines, score and pieces to any errored episode**.
53
+ Keep its original partial achievements in `observed_before_error`, retain its seed,
54
+ and report its error. This explicit reliability penalty prevents a model from
55
+ benefiting statistically from dropping difficult failures. The same policy applies
56
+ to base, trained and heuristic. Never describe a failed episode as a successful
57
+ zero-line game; status and error counts remain separate.
58
+
59
+ Report mean and median metrics, cap-hit rate, failed episodes, error rate and
60
+ invalid-decision count. Survival is censored at the cap: a cap hit means at least
61
+ that many pieces, not a measured death at that turn. This protocol cannot establish
62
+ uncapped longevity or full competitive Tetris performance.
63
+
64
+ For each seed, subtract base outcome from trained outcome. Resample these paired
65
+ episode differences with replacement, **10,000 bootstrap replicates, RNG seed
66
+ 2026**, and report their mean plus the percentile 95% interval. Resampling turns
67
+ would falsely treat correlated moves within a game as independent observations.
68
+ Pieces and score get descriptive intervals; lines are the single primary endpoint.
69
+ The intervals are conditional on the tested checkpoint and episode distribution;
70
+ one trained seed does not characterize training-seed variability.
71
+
72
+ `positive_mean_lines_signal` is true only if there are at least two pairs, the
73
+ primary interval's lower bound exceeds zero, and both base and trained episode
74
+ error rates are at or below the declared threshold (**default 0%**). This flag is
75
+ evidence for that measured comparison, not an automatic claim of general
76
+ superiority, approval to publish, or a way to select a checkpoint on the test set.
77
+ The heuristic remains a separate required comparator. Run the same paired analysis
78
+ against it and disclose whether the model actually beats that cheap baseline.
79
+
80
+ ## Timing and input lengths
81
+
82
+ Summarize decision latency mean, median and nearest-rank p95 from all attempts,
83
+ including failed attempts. These are timings recorded around `Player.choose`;
84
+ they include tokenization and policy overhead, exclude game rendering/replay, and
85
+ should not be advertised as pure GPU kernel time. CUDA probability transfer must
86
+ complete before `choose` returns. Record cold loading separately and identify any
87
+ warmup excluded before measured episodes. Never mix cached-replay speed with live
88
+ model latency. Input-token summaries use only events containing `input_tokens`
89
+ and report their count, so missing token metadata remains visible.
90
+
91
+ ## Position predictions
92
+
93
+ `summarize_positions` reports agreement with the fixed teacher label, target NLL
94
+ and multiclass Brier score (sum over choices, averaged over positions). The fixed
95
+ label uses engine-order tie-breaking; agreement therefore penalizes choosing a
96
+ different equally valued action. Consider a separately named optimal-value tie
97
+ agreement metric later if preregistered; do not silently change the definition.
98
+
99
+ Missing or invalid predictions stay in the agreement denominator as failures.
100
+ Aggregate NLL/Brier are available only with a complete legal probability
101
+ distribution for every position. Partial diagnostic means have explicit subset
102
+ denominators and cannot be presented as full-set results. A zero probability on
103
+ the teacher label means infinite NLL; JSON stores a null mean plus an explicit
104
+ `nll_is_infinite` flag and zero-target count, rather than silently clipping it.
105
+
106
+ ```python
107
+ from stackcraft.evaluation import paired_report, summarize_positions
108
+
109
+ # Load complete, previously collected artifacts; this runs no model or game.
110
+ report = paired_report(episodes, trained_id="trained", base_id="base")
111
+ position_report = summarize_positions(validation_rows, predictions_by_record_id)
112
+ ```
113
+
114
+ Toy tests verify known constant paired improvements, mismatched-pair rejection,
115
+ failure retention, metadata consistency and proper probability-score arithmetic.
116
+ Passing them verifies report mechanics, not any model's game performance.
117
+
118
+ ## Execute and resume a frozen evaluation
119
+
120
+ `scripts/evaluate_clef.py positions --dataset data/study-v1 --checkpoint PATH
121
+ --output NEWDIR` evaluates the entire validation split and writes unrounded
122
+ predictions plus NLL/agreement/Brier evidence. Its default players are native base,
123
+ base-fp32 and trained. Model loading is sequential to fit one GPU; this is real
124
+ inference and must follow memory admission. `--players trained` evaluates another
125
+ candidate without rerunning the unchanged baselines.
126
+
127
+ After validation, run `scripts/select_checkpoint.py` with both explicit epoch
128
+ checkpoint/report pairs and a new output directory, as shown in Tutorial 05.
129
+ It recomputes metrics from the raw predictions, verifies both checkpoint trees,
130
+ selects by the preregistered rule, and copies self-contained validation evidence.
131
+ Its `selection.json` contains schema_version 1,
132
+ selection_split `validation`, selected_key, decision_rule, checkpoint_sha256 mapping,
133
+ validation_metrics, validation_evidence `{path,sha256}`, test_seeds, their canonical
134
+ JSON SHA256 as test_seeds_sha256, max_pieces 200, encoding_version, max_length,
135
+ players, head_dtypes, bootstrap_samples 10000, bootstrap_seed 2026, max_error_rate
136
+ 0.0. Validation evidence must be the script's position report for the same checkpoint;
137
+ its path is relative to selection.json. Checkpoint hashes include every checkpoint
138
+ file. Set head_dtypes to `{base: bfloat16, base-fp32: float32, trained: float32}`
139
+ when all three neural players are included.
140
+
141
+ Both epoch candidates and their eligibility reasons are mandatory in the selection
142
+ artifact. Before opening test seeds, the evaluator checks all 215 positions,
143
+ complete finite probability metrics, zero inference errors, checkpoint/dataset
144
+ binding, and the lowest-NLL winner with an earlier-epoch tie rule. All five players,
145
+ including the FP32-head ablation, are required by this study.
146
+
147
+ ```sh
148
+ uv run --locked --extra ml python scripts/evaluate_clef.py tournament \
149
+ --checkpoint PATH --seeds 30000:30200 --max-pieces 200 \
150
+ --final-test --selection-file selection.json --output runs/final-evaluation
151
+ ```
152
+
153
+ The default includes all five players. The final-test switch is an explicit
154
+ acknowledgment that these held-out seeds will now be consumed. Without it, the
155
+ script rejects any overlap with the reserved pool before loading a player.
156
+
157
+ Every completed episode is stored as `PLAYER/seed-N.json`; `progress.json` records
158
+ progress, and the final `report.json` includes all episodes and paired comparisons.
159
+ Repeat exactly the same command with `--resume` after an interruption. The script
160
+ checks request, source hashes, selected checkpoint, frozen protocol and saved replay
161
+ outcomes, skips completed players without loading them, and resumes missing seeds.
162
+ It never retries or deletes a recorded error episode. Loading is timed separately;
163
+ there are no silently excluded warmup decisions.
code/docs/interfaces.md ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Initial implementation contract
2
+
3
+ The Python engine is authoritative. These names are shared across independently implemented engine, service, and UI. All board coordinates have x increasing right and y increasing down; rows are top to bottom. Board cells are 0 for empty or 1 through 7 for piece colors. Board width is 10, height is 20. Four normalized orientations are enumerated and duplicate shapes removed.
4
+
5
+ `schema.py` defines frozen dataclasses: `Placement(id: str, rotation: int, x: int, y: int, cells: tuple[tuple[int,int], ...])`; `GameState(board: tuple[tuple[int,...],...], seed: int, piece_index: int, current: str, next_piece: str, score: int=0, lines: int=0, terminal: bool=False)`; `Transition(state: GameState, action: Placement, cleared: int)`.
6
+
7
+ `pieces.py`: `piece_at(seed: int, index: int) -> str`, `rotations(piece: str) -> tuple[tuple[tuple[int,int],...],...]`. Piece colors map in order `I,O,T,S,Z,J,L` to 1..7. Deterministic sequence is versioned, uses a local random generator, and is identical regardless of player pace.
8
+
9
+ `engine.py`: `new_game(seed: int) -> GameState`, `legal_actions(state: GameState) -> tuple[Placement,...]`, `step(state: GameState, action_id: str) -> Transition`. Action IDs are `r{rotation}x{x}`. Placements are straight vertical hard drops starting fully within the board. No legal placement means terminal. An illegal or terminal-state action raises `ValueError` without mutating state. `legal_actions` includes landing y and absolute cell positions so clients can display a ghost without computing game rules. Clear rows simultaneously, score 100/300/500/800 for 1/2/3/4 lines, advance piece index, and detect next-piece top-out.
10
+
11
+ `replay.py`: `make_replay(seed: int, actions: list[str]) -> dict` computes a JSON-serializable artifact containing `schema_version: 1`, `rules_version: "stackcraft-v1"`, seed, action IDs, and final score/lines/pieces/terminal. `replay_states(artifact: dict) -> list[GameState]` validates versions and actions and returns initial plus successive states; validate final summary if provided.
12
+
13
+ Service HTTP endpoints (all JSON unless noted): `GET /` serves the game; `POST /api/games` with `{seed: int}` returns a session snapshot; `POST /api/games/{id}/moves` with `{action_id: str, expected_pieces: int}` returns its next snapshot; `GET /api/games/{id}` snapshot; `GET /api/games/{id}/replay` returns replay artifact; `POST /api/replays` with an artifact returns `{frames: [snapshot_without_id,...]}`. `GET /health` returns `{status:"ok"}`. Snapshots contain `id`, `board`, `current`, `next_piece`, `score`, `lines`, `pieces` (piece index), `terminal`, `legal_actions` (serialized Placement). Error responses have readable `detail`; unknown session 404, invalid action/replay 422, stale expected_pieces 409. On uncertain move response the client refreshes state before allowing another move; replay retries must not advance a second piece. API never reveals future piece stream to model adapters (seed exists only in server storage/replay).
14
+
15
+ UI uses these routes directly with fetch; no second simulator. Arrow left/right selects legal placements at current rotation, up rotates selection, space/Enter drops. Buttons allow all actions. Restart requests the seed input again. Download replay and load replay use the above endpoints, with frame stepping. UI source belongs in `src/stackcraft/web/` and uses vanilla JS/CSS/HTML to avoid a second package manager. Label the game as turn-based placement; no hold, tucks, kicks, T-spin bonuses or gravity timer. Comparison boards will be added after baselines.
code/docs/rules.md ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stackcraft v1 rules
2
+
3
+ Rules version: `stackcraft-v1`. Replay schema version: `1`.
4
+
5
+ Stackcraft is a turn-based tetromino placement game. Its research rules deliberately
6
+ exclude real-time gravity, hold, wall kicks, tucks, lock delay, T-spins, combos,
7
+ back-to-back bonuses, and level multipliers. It is not a full competitive Tetris
8
+ implementation. Humans and every bot use the same legal-placement enumerator.
9
+
10
+ ## Board and pieces
11
+
12
+ The board has 10 columns and 20 rows. Coordinates increase rightward (`x`) and
13
+ downward (`y`); `(0, 0)` is the top-left cell. Empty cells are `0`. Piece colors
14
+ are integer IDs `1..7` corresponding to `I,O,T,S,Z,J,L`.
15
+
16
+ Each piece has four cells. Orientation zero is defined in `pieces.py`. Other
17
+ orientations are successive clockwise rotations, normalized so their minimum x
18
+ and y are zero; duplicate orientations are removed. `I,S,Z` have two orientations,
19
+ `O` has one, and `T,J,L` have four. Rotation numbers refer to this unique list,
20
+ not to a wall-kick rotation system.
21
+
22
+ ## Piece stream and information
23
+
24
+ Each seven-piece bag contains every shape once. Bag number `b` uses a private
25
+ Python `Random` instance seeded with the string `stackcraft-v1:{seed}:{b}` and
26
+ an explicitly defined descending Fisher-Yates shuffle: at position `p` swap with
27
+ `int(random() * (p + 1))`. This avoids dependence on global random state, request
28
+ order, and higher-level shuffle implementation changes. The locked Python
29
+ environment and stream fixture tests provide an additional reproduction check.
30
+
31
+ Players observe the board, current piece, and exactly one next piece. Seeds,
32
+ bag state, and later pieces must not appear in model observations. Replays contain
33
+ a seed for reconstruction, so replay metadata is not a valid model observation.
34
+ Piece index is the count of successfully placed pieces, initially zero.
35
+
36
+ ## Legal moves and top-out
37
+
38
+ For each unique orientation and fitting column, place the normalized shape at
39
+ `y=0`, fully within the board. If any of those cells is occupied, this placement
40
+ is unavailable, even if there is space below. Otherwise move the shape down one
41
+ row at a time until the next step would collide or leave the board. This is the
42
+ only landing for that orientation and column. A piece cannot pass through a
43
+ block or slide underneath an overhang.
44
+
45
+ The action ID is `r{rotation}x{x}`, for example `r1x4`. Legal actions include the
46
+ landing y-coordinate and all four absolute cell coordinates. They are ordered
47
+ by rotation, then x, making deterministic tie-breaking possible. If there are no
48
+ legal moves, the game is terminal. Top-out is evaluated for the next current
49
+ piece after the preceding move's row clears. There are no hidden spawn rows.
50
+
51
+ ## Clearing and scoring
52
+
53
+ After placing a piece, remove every full row simultaneously. Remaining rows keep
54
+ their relative order and empty rows are added at the top. Award these points:
55
+
56
+ | Rows cleared by this move | Points |
57
+ | --- | --- |
58
+ | 0 | 0 |
59
+ | 1 | 100 |
60
+ | 2 | 300 |
61
+ | 3 | 500 |
62
+ | 4 | 800 |
63
+
64
+ Advance to the preview piece and reveal one new preview. Lines and score are
65
+ cumulative; there are no movement or hard-drop points. Lines cleared are the
66
+ primary research outcome. Score, placed pieces, and top-out are additional
67
+ outcomes. Evaluation episode caps are separate from these rules and must be
68
+ reported; reaching a cap does not imply top-out.
69
+
70
+ ## State, errors, and replay
71
+
72
+ `GameState`, `Placement`, and `Transition` are frozen dataclasses. Boards and
73
+ cell collections are tuples. `step(state, action_id)` validates an ID against
74
+ the current legal moves and returns a new state. Invalid moves, including any
75
+ move after top-out, raise `ValueError`; the input state remains unchanged.
76
+
77
+ `place(board, piece, action)` is a lower-level helper for expert and heuristic
78
+ afterboard evaluation. It reads no seed or future piece. Its action must already
79
+ come from `legal_actions` for that board and piece; it checks cell bounds and
80
+ occupation, but does not repeat path validation. External move IDs use `step`.
81
+
82
+ A replay stores schema/rules versions, seed, action IDs, and final score, lines,
83
+ placed pieces and terminal status. Import re-simulates every move. Invalid moves,
84
+ incompatible versions and inconsistent final summaries are rejected. Partial
85
+ games are valid. A consistent replay is reproducible evidence, not a signed
86
+ attestation of who played: changing actions and recomputing their summary creates
87
+ another valid game.
code/docs/tutorials/00-foundation.md ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Milestone 00: a reproducible experiment starts with a reproducible game
2
+
3
+ The eventual question is whether training helps a decision model play better. Before loading a model, establish a game that behaves identically for two players given the same pieces. Otherwise a score difference could reflect easier pieces or different rules.
4
+
5
+ ## 1. Create an isolated Python project
6
+
7
+ This repository uses Python 3.13, a `src/stackcraft` package, and uv. Run from the repository root:
8
+
9
+ ```bash
10
+ uv sync --locked
11
+ uv run --locked stackcraft --help
12
+ uv run --locked stackcraft --version
13
+ ```
14
+
15
+ The help lists `serve`, `tournament` and `generate-data`; the version is `stackcraft 0.1.0`. uv creates `.venv` and installs the package locally. `pyproject.toml` describes dependencies while `uv.lock` records exact resolved versions. Committing both lets another person recreate the environment. `--locked` detects a stale lock instead of silently selecting new versions.
16
+
17
+ Development dependencies are a dependency group, installed by default, rather than an optional product feature. This keeps pytest and linting separate from game requirements. The ML extra isolates Torch and large model dependencies so someone can play the game without a GPU. The code does not import a model from the CLI startup path.
18
+
19
+ ## 2. Write the rules before comparing players
20
+
21
+ Read `docs/rules.md` and `docs/interfaces.md`. All players receive the same board, current piece, and one-piece preview. They select an orientation and column; Python computes the vertical landing. A fixed seven-bag seed makes piece order reproducible. A seed is stored for replay, but never shown to a model: exposing it or later pieces would let one player gain information unavailable to another.
22
+
23
+ The simplified rules omit real-time gravity and wall kicks. A full Tetris implementation would mix planning ability with timing and control. We first isolate the decision we want to learn: which legal placement is best? The UI must disclose those rules.
24
+
25
+ ## 3. Keep one authoritative engine
26
+
27
+ Python simulates placements and emits legal actions with landing cells. The browser draws these cells and sends an action ID back. It does not recalculate collisions. Two engines in different languages could diverge and make replays unreliable. Frozen state objects and pure transition functions make mistakes easier to reproduce and test.
28
+
29
+ FastAPI serves the small JSON API and static HTML/CSS/JavaScript together. This first version needs no separate Node package manager or frontend framework. That reduces setup friction while leaving room to add one when the interface genuinely needs it.
30
+
31
+ ## 4. Verify behavior and source quality
32
+
33
+ ```bash
34
+ uv run --locked ruff check .
35
+ uv run --locked ruff format --check .
36
+ uv run --locked ty check src/stackcraft --exclude src/stackcraft/training.py
37
+ uv run --locked pytest
38
+ ```
39
+
40
+ These commands check the game without installing ML packages. Full-repository
41
+ type checking also covers training scripts and requires the ML extra:
42
+
43
+ ```bash
44
+ uv run --locked --extra ml ty check
45
+ ```
46
+
47
+ Installing that extra does not run GPU inference. When keeping an ML environment,
48
+ use `--extra ml` on uv commands to prevent synchronization from removing those
49
+ optional packages.
50
+
51
+ Ruff catches common source mistakes and applies one formatting style. ty checks Python types. pytest verifies outcomes such as a cleared row, an unchanged state after an illegal move, and a replay that reaches the original final board. Passing checks does not prove model quality; that requires a later held-out tournament.
52
+
53
+ ## 5. Preserve decisions and evidence
54
+
55
+ `plan.md` tracks milestones; `record.md` stores concise results and experiment intentions. Code and tutorials use local Git commits. Environments, weights, raw datasets, credentials and checkpoints are ignored. Later reports will refer to their hashes and release revisions rather than placing gigabytes in Git.
56
+
57
+ The next milestone adds the engine, playable board and replay. Model training starts only after those behaviors can be verified independently.
code/docs/tutorials/01-game.md ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Milestone 1: build a game whose decisions can be replayed
2
+
3
+ The deliverable is a playable turn-based game backed by a deterministic Python
4
+ engine. Before interpreting a model's performance, we need confidence that every
5
+ player follows identical rules and that saved decisions reconstruct the game.
6
+ Read [the complete rules](../rules.md) before extending the simulator.
7
+
8
+ ## 1. Install and start the game
9
+
10
+ From the repository root, install the committed environment and start the server:
11
+
12
+ ```sh
13
+ uv sync --locked
14
+ uv run --locked stackcraft serve --host 127.0.0.1 --port 8080
15
+ ```
16
+
17
+ Open `http://127.0.0.1:8080`. Choose a seed and start a game. Left/right select a
18
+ column, up changes orientation, and Space or Enter drops the selected placement.
19
+ Buttons provide the same controls. There is no gravity timer: take as long as
20
+ you need. The preview shows exactly one future piece.
21
+
22
+ Use restart with the same seed to receive the same piece stream. Download the
23
+ replay, load it again, and step through its frames. The imported game is simulated
24
+ again by Python; the browser does not trust a claimed score in the file.
25
+
26
+ Why this design: real-time input would mix decision quality with timing and
27
+ keyboard control. Placement decisions are an interpretable first experiment.
28
+ The browser displays authoritative legal moves rather than maintaining a second
29
+ simulator that could drift away from training rules.
30
+
31
+ ## 2. Inspect the pure engine
32
+
33
+ ```python
34
+ from stackcraft.engine import legal_actions, new_game, step
35
+
36
+ before = new_game(seed=42)
37
+ move = legal_actions(before)[0]
38
+ transition = step(before, move.id)
39
+ assert before.piece_index == 0
40
+ assert transition.state.piece_index == 1
41
+ print(move.id, move.cells, transition.cleared)
42
+ ```
43
+
44
+ Every state is immutable. `step` returns a new state and the resolved placement;
45
+ it does not modify `before`. This makes it possible to examine multiple candidate
46
+ moves from exactly the same position without accidentally advancing the real game.
47
+ Actions are ordered by orientation then column so ties can be resolved reproducibly.
48
+
49
+ The engine starts each candidate shape at row zero and drops it vertically. It
50
+ checks every step rather than picking the lowest empty space: the latter would
51
+ incorrectly let pieces pass through overhangs. Row clears happen simultaneously,
52
+ then the next piece is checked for top-out.
53
+
54
+ Why not use a mutable board everywhere: shared mutable state makes search branches,
55
+ HTTP sessions, and replay easier to contaminate accidentally. Frozen dataclasses
56
+ and tuple boards make those mistakes visible. For game-scale boards, clarity is
57
+ more valuable than prematurely optimizing away small copies.
58
+
59
+ ## 3. Separate hidden state from observations
60
+
61
+ The seed determines independently shuffled seven-piece bags. A bag includes every
62
+ shape once; it is not seven independent random draws. `piece_at(seed, index)` is
63
+ random-access and uses its own PRNG, so opening another game cannot alter a stream.
64
+
65
+ Only the current piece and one preview are legitimate player inputs. The engine
66
+ and replay know the seed to reconstruct games, but a model or search expert must
67
+ not exploit it to look further ahead. The lower-level `place` helper returns an
68
+ afterboard and row-clear count without reading any piece stream. Later baselines
69
+ and the search expert can therefore evaluate a candidate safely.
70
+
71
+ ## 4. Reconstruct and validate a replay
72
+
73
+ ```python
74
+ import json
75
+
76
+ from stackcraft.replay import make_replay, replay_states
77
+
78
+ artifact = make_replay(42, [move.id])
79
+ frames = replay_states(json.loads(json.dumps(artifact)))
80
+ assert frames == [before, transition.state]
81
+ ```
82
+
83
+ The replay contains actions rather than a pile of unverified board screenshots.
84
+ Its rules version is necessary: changing rotations or scoring must not silently
85
+ reinterpret an old experiment. Import also checks the final summary against the
86
+ recomputed outcome and rejects moves after terminal state.
87
+
88
+ This detects inconsistent files, not malicious authorship. Someone can construct
89
+ a different valid action sequence and summary; publication provenance and artifact
90
+ hashes will be separate experiment records.
91
+
92
+ ## 5. Check the behavioural contract
93
+
94
+ ```sh
95
+ uv run --locked pytest tests/test_pieces.py tests/test_engine.py tests/test_replay.py
96
+ ```
97
+
98
+ These tests cover shape orientations, all empty-board placements, boundaries,
99
+ collisions, blocked entry paths, one through four simultaneous row clears,
100
+ score accumulation, top-out, deterministic piece streams, state immutability,
101
+ replay round-trips and tampering. Tests for the service cover request handling
102
+ separately. Browser inspection must additionally check readable ghost pieces,
103
+ keyboard/button actions, restart and replay controls; engine tests cannot prove
104
+ that an interface is usable.
105
+
106
+ Next, measure random and heuristic players before adding a model. A playable
107
+ game establishes the experimental environment; it does not yet demonstrate that
108
+ fine-tuning helps.
109
+
110
+ ## Watch recorded players side by side
111
+
112
+ Choose **Watch a race** to load the packaged recordings for unchanged Clef-flash,
113
+ the Stackcraft fine-tune and the heuristic. The release's `baseline-demo.json`
114
+ contains their actual action traces for **seed 30000**, fixed before inspecting
115
+ results, with the same **200-piece cap**. The service reconstructs every frame
116
+ through the authoritative replay engine. This replaces the earlier development
117
+ Random/Heuristic example; it does not rerun model inference in your browser.
118
+
119
+ At the final frame of this illustrative seed, unchanged Clef has topped out after
120
+ 23 pieces without clearing a line, the fine-tune has topped out after 82 pieces
121
+ with 16 lines, and the heuristic has reached the 200-piece cap with 78 lines. These
122
+ are one game's outcomes, not the aggregate result across 200 held-out seeds.
123
+ Consult the full study report to assess overall performance and uncertainty.
124
+
125
+ The boards are synchronized by placements, not decision time. Playback is
126
+ explicitly recorded and does not imply live inference or model speed. A player
127
+ that tops out keeps its final board visible while other recordings continue.
128
+ Expand a recorded choice to inspect its actual selected-action probability and
129
+ up to three alternatives; these are model outputs, not generated explanations.
130
+ The provenance section lists the source artifact hashes. Returning to Play
131
+ preserves the human game.
132
+
133
+ ## Play alongside recorded opponents
134
+
135
+ The race view offers an explicit **Start live board · seed N** action. This starts
136
+ a new human game with the recording's seed and episode cap; merely opening or
137
+ leaving the race tab does not reset the current game. The human board is labelled
138
+ **LIVE HUMAN**, while opponents remain labelled as recorded. Both use the Python
139
+ engine's legal placements and the same deterministic piece stream.
140
+
141
+ Each human placement advances the recorded timeline to the same piece count. A
142
+ recording that ended earlier stays on its final frame. You can scrub the recorded
143
+ timeline independently without changing your board; **Match my move** resynchronizes
144
+ it. The original board DOM and session controller move between views, avoiding a
145
+ second simulator or conflicting copies of live state.
146
+
147
+ At the recording's episode cap, human placement controls stop and show **Cap
148
+ reached**, distinct from top-out. Switching to Play retains that comparison cap
149
+ and the same session. **Leave comparison** removes comparison mode and returns
150
+ the existing game to ordinary play; only starting/restarting a game resets it.
151
+ Pending moves temporarily disable view changes so a late response cannot update
152
+ a different session. Desktop boards align side by side; narrow screens stack them
153
+ without shrinking the human controls.
code/docs/tutorials/02-baselines.md ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Tutorial 02 — Build a comparison before training
2
+
3
+ This milestone establishes offline opponents and a replayable tournament. **Unchanged Clef now has a small real-GPU development benchmark.** These results cannot establish an advantage from fine-tuning.
4
+
5
+ ## Give every player the same information
6
+
7
+ `players.observe(state)` creates a frozen `Observation`: board, current piece, exactly
8
+ one next piece, legal placements, and the rules version. It omits the episode seed,
9
+ piece index, and random generator. A seed would reveal the entire future sequence;
10
+ giving it to a search player would invalidate the intended experiment. The tournament
11
+ retains the seed separately so another person can replay an episode.
12
+
13
+ Players implement `choose(observation) -> Decision`. A decision identifies one legal
14
+ placement and can provide a complete probability distribution. The runner rejects
15
+ illegal moves, missing probabilities, nonfinite probabilities, and incorrect totals.
16
+ An exception ends that player's episode as an explicit error. A random fallback would
17
+ hide deployment failures and turn the measured player into a different policy.
18
+
19
+ ## Start with two cheap opponents
20
+
21
+ `RandomPlayer(seed=0)` samples uniformly from legal placements using its own random
22
+ generator. Its seed is unrelated to the game's sequence seed. The tournament creates
23
+ a fresh instance for each episode, so earlier game length cannot alter later choices.
24
+
25
+ `HeuristicPlayer` scores every legal afterboard with these fixed, untuned weights:
26
+
27
+ value = -aggregate_height - 4*holes - bumpiness + 8*cleared_lines
28
+
29
+ Aggregate height sums the ten column heights. A hole is an empty cell below a filled
30
+ cell in its column. Bumpiness sums differences between adjacent column heights. The
31
+ line bonus uses lines cleared by this move. All features describe the board after
32
+ simultaneous row removal. Ties follow the engine's rotation-then-column ordering.
33
+ These intentionally simple integer weights are a declared baseline, not an optimized
34
+ or competitive Tetris claim. The heuristic receives the preview but does not use it;
35
+ the later search expert will be able to search one additional placement.
36
+
37
+ We call the authoritative engine's `place` helper to obtain afterboards. Calling
38
+ `step` on the full episode state would expose the hidden future sequence, and a
39
+ separate simulator could disagree with the game's actual rules.
40
+
41
+ ## Preregister the development pilot
42
+
43
+ Before running any tournament, the pilot is fixed as: development seeds **0–19**,
44
+ 100-piece cap, random RNG seed 0, the weights above, and two players (random and
45
+ heuristic). Question: does the runner produce repeatable paired games, and does the
46
+ simple board heuristic behave plausibly? This is engineering development, not a
47
+ held-out model study. Do not put these seeds into the final test pool or use this
48
+ pilot to claim model improvement. Stop after 40 episodes or a runner failure.
49
+
50
+ Expected artifact: `runs/baselines-development.json`. Record mean/median lines,
51
+ score, pieces placed, cap-hit and error rates, and decision latency. Acceptance is
52
+ valid replays and no unexplained errors; poor heuristic play is reportable, not a
53
+ reason to secretly select nicer seeds. Starting commit is
54
+ `53db6bd0e7f42723bc63db7ad56c4ac139a04031`, plus the uncommitted baseline
55
+ implementation recorded in Git at integration.
56
+
57
+ ## Reproduce the pilot
58
+
59
+ From the repository, after `uv sync --locked --group dev`:
60
+
61
+ ```bash
62
+ uv run --locked python - <<'PY'
63
+ import json
64
+ from pathlib import Path
65
+ from stackcraft.players import HeuristicPlayer, RandomPlayer
66
+ from stackcraft.tournament import run_tournament
67
+
68
+ result = run_tournament({'random': RandomPlayer, 'heuristic': HeuristicPlayer}, range(20), 100)
69
+ output = Path('runs/baselines-development.json')
70
+ output.parent.mkdir(parents=True, exist_ok=True)
71
+ output.write_text(json.dumps(result, indent=2) + '\n')
72
+ print(json.dumps(result['summary'], indent=2))
73
+ PY
74
+ ```
75
+
76
+ Each episode records visible observations, chosen action IDs, optional probabilities,
77
+ the complete action replay, final outcomes, explicit errors, and decision times.
78
+ `replay_states(episode['replay'])` reconstructs its final board independently. All
79
+ players receive identical piece sequences for each seed, even when one dies earlier.
80
+
81
+ The cap censors survival: 100 pieces means "survived at least 100", not "would die
82
+ at 100". Report cap-hit rate alongside survival. Latency times only `choose`, not
83
+ rendering, observation construction, engine transition, or replay validation. It
84
+ includes Python overhead and is machine dependent. Game content is deterministic;
85
+ latency is deliberately excluded when checking repeatability.
86
+
87
+ ## Development pilot result
88
+
89
+ The preregistered pilot ran on 2026-10-05. All 40 episode replays matched their
90
+ recorded final outcomes; neither player produced an error.
91
+
92
+ | Player | Mean lines | Median lines | Mean score | Mean pieces | Cap-hit rate | Mean decision time |
93
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: |
94
+ | Random | 0.10 | 0 | 10 | 26.9 | 0% | 0.0016 ms |
95
+ | Heuristic | 35.95 | 36 | 3,900 | 100 | 100% | 0.179 ms |
96
+
97
+ Every heuristic episode reached the cap. This supports using a longer development
98
+ cap to estimate the cost of final evaluation; it does not measure unlimited survival.
99
+ The pilot ran in approximately 3.37 seconds including independent replay checks
100
+ and writing JSON. Timing is illustrative local CPU execution, with no GPU model.
101
+ Artifact SHA-256:
102
+ `ed4973c69496cdc0ebb5720e3beaae621406988a6d298233c10ab962e1c499af`.
103
+ Regeneration preserves game outcomes but changes timing fields and therefore the
104
+ full artifact hash.
105
+
106
+ ## Actual unchanged Clef development run
107
+
108
+ The pinned native Clef-flash backbone and joint head ran on RTX 5090 in BF16.
109
+ First cache the exact upstream snapshot from the repository root:
110
+
111
+ ```bash
112
+ uv run --locked --extra ml hf download Cloudflare/clef-flash \
113
+ --revision 17f0b0ad64efb65d273590632833508766b2aae6
114
+ ```
115
+
116
+ This installs the optional ML dependencies and downloads files; it does not load
117
+ the model onto the GPU. The benchmark loader uses local files only and will fail
118
+ on a fresh machine without this step. Once at least25GiB GPU memory is free, run:
119
+
120
+ ```bash
121
+ uv run --locked --extra ml python scripts/benchmark_clef.py \
122
+ --output runs/clef-development-repeat.json --episodes 2 --max-pieces 30
123
+ ```
124
+
125
+ Choose a new output path if it already exists. Do not stop other workloads without
126
+ authorization. The loader explicitly trusts the pinned, hash-checked upstream
127
+ Python implementation; see Tutorial04 for that trust boundary and memory checks.
128
+
129
+ On development seeds0–1 (30-piece cap), Clef averaged0lines and27.5pieces;
130
+ the heuristic averaged9lines and reached30pieces both times. No player errored.
131
+ Clef's55decisions averaged144ms (p95 241ms), after a separate warmup.
132
+ These two episodes establish feasibility, not statistical performance. Raw probabilities,
133
+ input-token counts, pinned revisions and timing are retained in
134
+ `runs/clef-development-v1.json`. Reference PyTorch attention kernels were used;
135
+ faster optional kernels were not installed, so latency is implementation-specific.
136
+ The GLM service was temporarily stopped under user authorization and restored;
137
+ its in-container health endpoint returned `{"status":"ok"}`.
138
+
139
+ Use fresh held-out episode seeds for the final study. A large neural model can lose
140
+ to a few lines of board arithmetic; the report must make that outcome visible.
code/docs/tutorials/03-data.md ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Tutorial 03 — Teach from search without leaking future pieces
2
+
3
+ The dataset records recommendations from a bounded search expert. These are
4
+ synthetic labels from a specified algorithm, not human labels or globally optimal
5
+ Tetris moves. The expert sees the current piece and one preview, exactly as other
6
+ players do.
7
+
8
+ ## Write the experiment before collecting examples
9
+
10
+ Starting commit: `412be98cf90bc7e3e60cd39d8bf9d479bcf65d27`, plus the expert/data
11
+ implementation committed at integration. Preregistered pilot: training seeds
12
+ 10000–10023, validation seeds 20000–20005, at most 40 pieces per episode. Cycle
13
+ behavior policies random, heuristic, expert by episode index within each split.
14
+ Label every visited nonterminal state with the expert. Generate the pilot twice
15
+ and require identical records, manifest and JSONL hashes. Stop each job at 120
16
+ seconds or the first audit failure. Output the first bundle under `data/pilot-v1`.
17
+
18
+ Reserve seeds 30000–30199 for final evaluation; generate no trajectories from
19
+ them during dataset work. Development seeds 0–19 and 1000 are excluded from all
20
+ dataset pools. A separate development comparison uses only seeds 0–4 and a
21
+ 100-piece cap, with heuristic and expert. Its purpose is to inspect search behavior
22
+ and cost, not measure neural-model improvement. Do not tune weights using these
23
+ results. Expected comparison artifact: `runs/expert-development.json`.
24
+
25
+ ## Search only information available to the player
26
+
27
+ For each legal current placement, the expert clears completed rows and enumerates
28
+ all legal placements of the visible preview. It scores:
29
+
30
+ 8 * current_lines + max(preview_afterboard_value)
31
+
32
+ The afterboard value is the same untuned `-height -4*holes -bumpiness +8*lines`
33
+ heuristic from Tutorial 02. If the preview has no legal placement, its value is
34
+ -10000. This is a two-placement search with no pruning. Equal values select the
35
+ first engine action in rotation/column order.
36
+
37
+ The search constructs a synthetic state with dummy seed/index only to enumerate
38
+ geometry. It calls `place`, never `step`, so it cannot query the piece after the
39
+ preview. Tests make sequence lookup fail during search to enforce that boundary.
40
+ Searching more known future pieces would give the teacher information unavailable
41
+ to the model; it would solve a different task. Long-horizon search is also more
42
+ expensive and is unnecessary for this first imitation-learning experiment.
43
+
44
+ ## Collect recovery states as well as expert play
45
+
46
+ Only collecting expert trajectories produces many tidy boards and few recovery
47
+ situations. Cycling random, greedy heuristic, and expert behavior visits different
48
+ board conditions. Every behavior's state receives the same expert label. The
49
+ random policy has its own RNG, seeded with episode seed plus one million; this
50
+ controls collection reproducibility without exposing any seed to the teacher.
51
+
52
+ Each row retains the raw visible observation, action values for every legal move,
53
+ chosen move, behavior policy, episode/turn, rules, teacher revision and source
54
+ commit. Provenance fields are for auditing; training inputs must use only
55
+ `observation`. In particular, never serialize the whole dataset row as model state.
56
+
57
+ ## Split episodes first, then remove duplicate observations
58
+
59
+ Seed pools are disjoint before generation. All retained states from one episode
60
+ remain in one split. Exact visible observations can still repeat across episodes,
61
+ especially empty boards. Deduplication hashes the full observation after replacing
62
+ board colors with occupied/empty cells, because colors do not affect game dynamics.
63
+ Current piece, preview, legal geometry, and rules remain in that hash.
64
+
65
+ Process training before validation and keep the first unique observation. Remove
66
+ later duplicates within either split and across splits, recording both counts.
67
+ Require duplicate teacher labels and action values to agree; a conflict stops the
68
+ run. This deterministic policy sacrifices some validation rows rather than letting
69
+ identical positions inflate reported generalization. Full episode IDs and seed
70
+ reservations are checked separately from observation hashes.
71
+
72
+ ## Reproduce and audit
73
+
74
+ ```python
75
+ from pathlib import Path
76
+ from stackcraft.data import DatasetConfig, audit_dataset, generate_dataset, write_dataset
77
+ from stackcraft.provenance import source_identity
78
+
79
+ config = DatasetConfig()
80
+ identity = source_identity(Path.cwd())
81
+ commit = identity["source_commit"] + ("+working-tree" if identity["source_dirty"] else "")
82
+ output = Path("data/pilot-v1")
83
+ assert not output.exists(), "Choose a new output directory"
84
+ bundle = generate_dataset(config, source_commit=commit)
85
+ assert bundle == generate_dataset(config, source_commit=bundle.manifest["source_commit"])
86
+ print(audit_dataset(bundle.records, bundle.manifest))
87
+ write_dataset(bundle, output)
88
+ ```
89
+
90
+ Run with `uv run --locked python` from the repository root or the public bundle's
91
+ `code/` directory. Source identity uses that exact Git root or verifies the bundled
92
+ source manifest, so private Git access is not required. This creates a new pilot
93
+ with the current source identity, not necessarily the original study's bytes.
94
+ The writer refuses existing artifact files so a new experiment cannot silently
95
+ replace recorded evidence.
96
+ The manifest stores the complete configuration, seed reservations, teacher formula
97
+ revision, collection and exclusion counts, and SHA-256 hashes of canonical JSONL.
98
+ Auditing reconstructs legal placements and verifies label argmax with the tie rule,
99
+ finite values, provenance, duplicate exclusion, and absence of hidden observation
100
+ fields. It does not prove that the teacher is optimal. Evaluation must still test
101
+ complete games, not just how accurately a model copies these labels.
102
+
103
+ The manifest also fingerprints `engine.py`, `pieces.py`, `schema.py`,
104
+ `players/__init__.py`, `expert.py`, and `data.py`. During development, append
105
+ `+working-tree` to the commit identifier when source edits are not committed.
106
+ Before study training, freeze the dataset against its committed generator source.
107
+ Preserve those exact bytes through publication; later documentation or packaging
108
+ commits are not reasons to regenerate the training dataset. A source hash records
109
+ what ran; it must not imply that an earlier commit includes later code.
110
+
111
+ ## Pilot observations
112
+
113
+ The preregistered 2026-10-05 pilot collected 833 training and 217 validation
114
+ positions. Six repeated training positions and two validation positions already
115
+ present in training were removed, leaving **827 training / 215 validation** rows.
116
+ Both complete generation runs produced identical bundles. Each took about 9.05
117
+ seconds; a separate structural audit took 0.49 seconds on the development machine.
118
+
119
+ The historical pilot's JSONL hashes are:
120
+
121
+ - Train: `bccf62673fb3160ac957650d400edcd6efb28e56b5e212940aba3281fb4bb82d`.
122
+ - Validation: `e6e87cad6aea503ef41894d7aa53cd80d635d540c7d2f2942f79a033692cce76`.
123
+
124
+ These artifacts used the original pilot source recorded in their manifest. A later
125
+ change allowed the explicit `+working-tree` commit suffix; that change does not
126
+ retroactively change which code produced this pilot. A final committed regeneration
127
+ will have different provenance hashes even when game examples remain the same.
128
+
129
+ For an additional label check, an independently written nested loop recomputed
130
+ every action value on 16 positions: first and highest-stack examples from each of
131
+ the six split/behavior groups, then further highest-stack positions to reach 16.
132
+ The sample included height-20 recovery boards. All values and chosen labels
133
+ matched. Evidence is `runs/data-pilot-independent-audit.json`. This checks the
134
+ implementation against the declared search formula, not optimality of that formula.
135
+
136
+ The five-seed development tournament produced mean lines 37.8 for the heuristic
137
+ and 38.6 for search; both reached the 100-piece cap in all five games with no
138
+ errors. Mean search decision time was 8.33 ms versus 0.183 ms for the heuristic.
139
+ The collection and comparison jobs ran concurrently on CPU, so these times are
140
+ illustrative, not isolated performance benchmarks. Five capped games are insufficient
141
+ to establish broad superiority. No neural model was evaluated.
142
+
143
+ ## Reproduce from the public source bundle without private Git access
144
+
145
+ The model release includes `code/` with source, a lockfile and a source manifest.
146
+ From that directory, `uv sync --locked` installs the game/data tools. The regular
147
+ `stackcraft generate-data` command records the bundle's source identity and flags
148
+ modified source files. Its provenance will therefore differ from the original
149
+ M3 commit even when generator logic is unchanged.
150
+
151
+ The probe and full-training commands in later tutorials use `data/study-v1`.
152
+ For a fresh reproduction, the command below creates that directory with the exact
153
+ original study bytes. Alternatively, place the released dataset's `train.jsonl`,
154
+ `validation.jsonl` and `manifest.json` in that directory and verify them using the
155
+ release audit. Do not substitute the newly generated pilot above for the study.
156
+
157
+ To reproduce the original study bytes exactly, first verify that every generator
158
+ source file matches the original dataset manifest, then use its recorded commit
159
+ in the generator API. This avoids falsely stamping changed code with an old commit:
160
+
161
+ ```bash
162
+ uv run --locked python - <<'PY'
163
+ import hashlib
164
+ import json
165
+ from pathlib import Path
166
+ from stackcraft.data import DatasetConfig, generate_dataset, write_dataset
167
+
168
+ manifest = json.loads(Path('reports/dataset-manifest.json').read_text())
169
+ for name, expected in manifest['source_hashes'].items():
170
+ path = Path('src/stackcraft') / name
171
+ assert hashlib.sha256(path.read_bytes()).hexdigest() == expected, name
172
+ output = Path('data/study-v1')
173
+ assert not output.exists(), 'Choose a new output directory'
174
+ bundle = generate_dataset(DatasetConfig(), source_commit=manifest['source_commit'])
175
+ assert bundle.manifest == manifest, 'Generated manifest differs from the frozen study'
176
+ write_dataset(bundle, output)
177
+ for split in ('train', 'validation'):
178
+ actual = hashlib.sha256((output / f'{split}.jsonl').read_bytes()).hexdigest()
179
+ assert actual == manifest['splits'][split]['sha256'], split
180
+ print('Both original split hashes match')
181
+ PY
182
+ ```
183
+
184
+ If `data/study-v1` already exists, retain and audit it rather than overwrite it.
185
+ For a separate regeneration, choose a new output directory and supply that path
186
+ with `--dataset` in subsequent probe, training and evaluation commands. Use only
187
+ the audited downloaded dataset or the exact hash-matching reconstruction to
188
+ reproduce study training. If any source hash fails, investigate or generate a
189
+ clearly new dataset; do not suppress the check or reuse the original study's identity.
code/docs/tutorials/04-gpu-feasibility.md ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Tutorial 04 — Prove training and reload before running an experiment
2
+
3
+ This milestone asks a narrow engineering question: can the real pinned Clef-flash
4
+ model take gradient steps on this GPU, save all learned parameters, and reproduce
5
+ its probabilities in a fresh process? It does not ask whether the model plays
6
+ better. The latter requires complete games on the held-out sequences in Milestone 5.
7
+
8
+ **The bounded real-weight training and fresh-process reload gate passed.** Both
9
+ head-only and LoRA-plus-head artifacts reproduced their reference probabilities
10
+ exactly. The first LoRA reload exposed a saved-configuration compatibility issue;
11
+ the corrected loader and successful retry are documented below. This establishes
12
+ local training feasibility, not improved game performance.
13
+
14
+ ## Start from the native decision model
15
+
16
+ The base is `Cloudflare/clef-flash`, revision
17
+ `17f0b0ad64efb65d273590632833508766b2aae6`. The local loader verifies the pinned
18
+ `joint_schema_model.py` source hash and loads an explicitly resolved snapshot.
19
+ It requires `trust_pinned_code=True`: pinning and inspecting Python source controls
20
+ which code executes; it does not turn that code into a sandbox.
21
+
22
+ An observation becomes one native choice question containing every legal
23
+ placement. The answer is a distribution over action IDs, not a generated command
24
+ or natural-language explanation. `encode_observation` checks the complete token
25
+ budget before native encoding, because silently truncating a board would change
26
+ the task. Training uses only the visible board, current piece, one preview and
27
+ legal placements. Dataset seed and episode metadata remain outside the input.
28
+
29
+ The native encoder sorts option IDs lexicographically. `decision_loss` locates the
30
+ teacher's action in `encoded.questions[0].option_ids`; it must not reuse the
31
+ engine's rotation/column index as a target index. Both orders contain the same
32
+ legal moves, but their order can differ.
33
+
34
+ Do not train through `systemone()` or `ClefPlayer.choose()`: those are inference
35
+ paths. The probe calls `native.collate_records` and then `model(batch)` with
36
+ normal autograd enabled. `choose()` is appropriate for the before/after reference
37
+ probabilities, where gradients are unnecessary.
38
+
39
+ ## Keep the backbone small in memory and the head stable
40
+
41
+ `src/stackcraft/training.py` implements two bounded configurations:
42
+
43
+ | Configuration | Updated parameters | Purpose |
44
+ | --- | --- | --- |
45
+ | Head only | Native joint decision head | Isolate head training and checkpoint handling before a backbone backward pass. |
46
+ | Rank-4 LoRA plus head | Small added text-layer matrices and the native head | Check whether the task can adapt both text representations and decision scoring. |
47
+
48
+ The backbone remains BF16. The native decision head is converted to FP32, and
49
+ `FP32DecisionHead` disables autocast inside the head. Its floating inputs are
50
+ converted to FP32 while token IDs and masks retain their original types. This
51
+ keeps the trainable scoring operations in full precision without converting the
52
+ whole multi-billion-parameter backbone.
53
+
54
+ The head reads selected rows of the output vocabulary embedding. Converting the
55
+ entire vocabulary matrix to FP32 first creates a large temporary allocation
56
+ (roughly 3.79 GiB for this release). `GatheredFloat32Embedding` instead implements
57
+ `weight[indices].float()`: select the needed rows first, then cast. This preserves
58
+ the actual native head computation and autograd connections. A tiny actual-native-
59
+ head test compares outputs and gradients with the full-cast reference exactly.
60
+ That test checks the optimization, not full-model training feasibility.
61
+
62
+ LoRA targets are full module paths under the text transformer layers. They include
63
+ both ordinary attention projections and Qwen3.5's linear-attention projections,
64
+ as well as the MLP projections. Matching only suffixes such as `q_proj` could
65
+ accidentally select the vision encoder; matching only ordinary attention would
66
+ miss the hybrid backbone. The vision tower, vocabulary output layer and unrelated
67
+ modules are excluded. Original backbone parameters are frozen. The probe uses
68
+ rank 4, alpha 8, no LoRA dropout and no trained backbone biases.
69
+
70
+ Non-reentrant gradient checkpointing recomputes intermediate activations during
71
+ backward instead of retaining all of them. This trades time for memory while
72
+ preserving gradients through frozen parts of the backbone into LoRA matrices.
73
+ A tiny actual Qwen3.5 hybrid-backbone test checks nonzero LoRA gradients in ordinary
74
+ attention, linear attention and MLP layers. These are stronger plumbing checks
75
+ than testing a stand-in linear model alone, but still do not establish the real
76
+ 9B GPU memory requirement.
77
+
78
+ We start with BF16 rather than immediately adding 4-bit quantized training. The
79
+ native model has a custom head and loader; another quantization layer would add
80
+ an unverified compatibility dependency. If BF16 fails, record the failure and
81
+ make a bounded next decision rather than silently changing the experiment.
82
+
83
+ ## Prepare before borrowing the GPU
84
+
85
+ Run these commands from the repository root or the public bundle's `code/`
86
+ directory. Install the ML tools and cache the pinned release before testing:
87
+
88
+ ```bash
89
+ uv sync --locked --extra ml --group dev
90
+ uv run --locked --extra ml hf download Cloudflare/clef-flash \
91
+ --revision 17f0b0ad64efb65d273590632833508766b2aae6
92
+ ```
93
+
94
+ Downloading only populates the cache; it does not load GPU weights. It can run
95
+ while other GPU services remain available. The actual native-head tests need
96
+ this cached source. Explicitly enable the native tokenizer/encoder check too:
97
+
98
+ ```bash
99
+ STACKCRAFT_TEST_NATIVE_ENCODING=1 uv run --locked --extra ml \
100
+ pytest tests/test_training.py tests/test_clef.py
101
+ ```
102
+
103
+ These checks use CPU fixtures and the real tokenizer/source; they do not load
104
+ the full backbone or establish GPU feasibility. Inspect the test summary: a
105
+ skipped test is not a passed native-model test.
106
+
107
+ The training probe expects the audited `data/study-v1` bundle established by
108
+ Tutorial03's exact-study reconstruction or a verified dataset download. If you
109
+ used a different directory, pass that same path with `--dataset` below.
110
+ It validates the train/validation bundle, then uses only the first four training
111
+ positions. It does not sample held-out test seeds. Record the current source
112
+ commit and any uncommitted probe changes in `record.md` before running.
113
+
114
+ This workstation normally runs a local GLM Docker service. The user explicitly
115
+ authorized temporarily stopping that service and restoring it after these
116
+ Stackcraft experiments. `scripts/gpu_session.py` is specific to that approved
117
+ service; it is not a generic instruction to stop another machine's workloads.
118
+ It first requires the configured container to be running, then stops it, runs a
119
+ bounded child job, and attempts restoration during cleanup. The training script
120
+ itself never stops services. It refuses to load unless CUDA is available and at
121
+ least **25 GiB is free**. Free-memory admission is a precondition, not a guarantee
122
+ that backward will fit.
123
+
124
+ ## Run the bounded real-weight probe
125
+
126
+ The initial budget is two configurations and at most 30 minutes of child-job
127
+ time. The commands below allocate 20 minutes to the training probe and 10 minutes
128
+ to the separate reload check. Choose fresh output and session-record paths on a
129
+ retry; neither existing evidence nor checkpoints should be overwritten.
130
+
131
+ ```bash
132
+ uv run --locked --extra ml python scripts/gpu_session.py \
133
+ --timeout 1200 --record runs/gpu-training-probe-v1.json -- \
134
+ uv run --locked --extra ml python scripts/probe_clef_training.py \
135
+ --dataset data/study-v1 --output runs/clef-training-probe-v1
136
+ ```
137
+
138
+ The probe first records unchanged native probabilities. It measures the initial
139
+ probability drift introduced by moving the head to FP32, before training; a
140
+ precision change must not be mistaken for a learned improvement. It then runs
141
+ three head-only steps. After saving that checkpoint, it releases the model and
142
+ loads a fresh unchanged base for five LoRA-plus-head steps. The LoRA experiment
143
+ therefore does not inherit the head-only warm-up.
144
+
145
+ Both configurations use batch size one, AdamW with learning rate `1e-5`, norm
146
+ clipping at 1.0, and this loss:
147
+
148
+ smoothed_cross_entropy(label_smoothing=0.05)
149
+ + 0.1 * sum_over_options((probability - one_hot_teacher_label)**2)
150
+
151
+ The cross-entropy term learns the expert action. Smoothing avoids assigning the
152
+ entire training target to one action. The Brier term also penalizes the predicted
153
+ probability distribution's distance from the teacher target. These fixed choices
154
+ are a probe configuration, not evidence of calibrated confidence or optimal
155
+ hyperparameters.
156
+
157
+ For every step, the probe checks finite loss and gradients and records nonzero
158
+ head/LoRA gradient sums, tokens, elapsed time, and CUDA peak allocated/reserved
159
+ bytes. Before/after parameter hashes check that intended parameters changed and
160
+ frozen parameters did not. Hashing the entire frozen backbone costs time but
161
+ checks something that `requires_grad=False` alone cannot prove: what bytes actually
162
+ changed during this run.
163
+
164
+ Inspect `runs/clef-training-probe-v1/report.json` and the session record. A timeout,
165
+ CUDA error, absent gradient or failed hash check is a failed gate. A report left at
166
+ `running` after forced termination is incomplete evidence, even if some checkpoint
167
+ files exist. Do not start long training merely because loss fell on four examples.
168
+
169
+ ## Save the head as well as the adapter
170
+
171
+ A LoRA checkpoint has this structure:
172
+
173
+ ```text
174
+ checkpoint/
175
+ joint_head.safetensors
176
+ adapter/
177
+ adapter_config.json
178
+ adapter_model.safetensors
179
+ training_config.json
180
+ reference.json
181
+ ```
182
+
183
+ The head is an independently trained part of Clef; saving only the PEFT adapter
184
+ would omit learned parameters. `joint_head.safetensors` stores FP32 native head
185
+ keys without wrapper-specific prefixes. The adapter stores only the added LoRA
186
+ weights, not another copy of the frozen base. Metadata identifies the base
187
+ revision, native source hash, input encoding, head format, LoRA settings and
188
+ teacher rows. The loader checks this contract, head shapes and dtypes, and actual
189
+ saved adapter configuration before restoring weights.
190
+
191
+ The head-only probe writes `head-checkpoint/` without an adapter. It is diagnostic
192
+ output, not a selected release model.
193
+
194
+ ## Require a separate fresh-process reload
195
+
196
+ Only after the first probe succeeds, run:
197
+
198
+ ```bash
199
+ uv run --locked --extra ml python scripts/gpu_session.py \
200
+ --timeout 600 --record runs/gpu-training-reload-v1.json -- \
201
+ uv run --locked --extra ml python scripts/probe_clef_training.py \
202
+ --dataset data/study-v1 --output runs/clef-training-reload-v1 \
203
+ --reload runs/clef-training-probe-v1/checkpoint
204
+ ```
205
+
206
+ This starts another Python process, loads the unchanged pinned base, restores both
207
+ adapter and head, and evaluates the same four training observations. Require the
208
+ same option IDs and a **maximum absolute probability difference of at most
209
+ `1e-4`** across all recorded options. The tolerance was declared before the run.
210
+ Compare unrounded probabilities; rounded display values can conceal a mismatch.
211
+
212
+ A same-process test could accidentally reuse trained parameters that were never
213
+ saved. The fresh process tests the actual inference artifact. A passing reload
214
+ establishes serialization fidelity on these references, not general game skill
215
+ or correctness on every possible input.
216
+
217
+ ## Restore service and record the limits
218
+
219
+ Inspect both `restored_running` and `restored_healthy` after success and failure.
220
+ The wrapper checks `/health` inside the restored container and allows up to 120
221
+ seconds for readiness, retrying transient timeouts and malformed startup responses.
222
+ It does not query the unrelated service on host port 8080. A running process alone
223
+ would not establish readiness. Nested cleanup ensures a child-termination error
224
+ still reaches restoration; repeated ordinary interrupts are ignored during that
225
+ cleanup. Preserve the wrapper record, probe reports and console logs, including
226
+ errors. Mocked tests exercise these recovery paths without touching Docker.
227
+
228
+ Process cleanup cannot guarantee recovery after `SIGKILL`, host power loss or a
229
+ Docker daemon failure. If the wrapper was forcibly terminated, inspect the exact
230
+ approved container and any remaining job before acting. Restore the existing
231
+ service only after the experiment has released GPU memory; do not start another
232
+ probe simply because the last terminal stopped printing output.
233
+
234
+ The earlier M2 real-weight development pilot is useful context: on this RTX 5090,
235
+ it recorded 55 unchanged-Clef decisions at a mean of about **0.144 seconds per
236
+ decision**, with **19,686,539,776 bytes** peak CUDA allocation (about 19.7 GB,
237
+ 18.3 GiB). The benchmark script took 12.60 seconds; the stop/job/restore wrapper
238
+ interval was about 14.20 seconds and recorded successful restoration. These are
239
+ inference measurements on two short development games, not training memory or
240
+ held-out quality results. Sources are `runs/clef-development-v1.json` and
241
+ `runs/gpu-baseline-session.json`.
242
+
243
+ ## Measured training probe and the first reload failure
244
+
245
+ The real-weight probe passed on 2026-10-05 using the RTX 5090 and Torch
246
+ `2.14.1+cu130`. Evidence is `runs/probe-v1/report.json`; the actual run directory
247
+ differs from the fresh reproduction paths shown above. It used training positions
248
+ `seed-10000-turn-0` through `seed-10000-turn-3`, with 1,302–2,252 encoded tokens.
249
+
250
+ | Measurement | Head-only probe | LoRA-plus-head probe |
251
+ | --- | ---: | ---: |
252
+ | Optimization steps | 3 | 5 |
253
+ | Observed step times | 0.154–0.328 s | 0.670–1.322 s |
254
+ | Peak allocated CUDA bytes | 21,323,760,640 | 23,180,389,888 |
255
+ | Peak reserved CUDA bytes | 21,541,945,344 | 23,595,057,152 |
256
+ | Intended parameter changes | Verified | Verified |
257
+ | Frozen parameter hashes | Unchanged | Unchanged |
258
+
259
+ All reported losses and gradients were finite, with nonzero gradients in the
260
+ intended groups. Total probe time was **51.81 seconds**, including model reload,
261
+ parameter hashing and checkpoint work. This is not an estimate of a full epoch:
262
+ only four short training positions were exercised. Peak reserved memory is the
263
+ PyTorch allocator's reservation, not the same quantity as all memory reported by
264
+ `nvidia-smi`.
265
+
266
+ The FP32 head changed initial probabilities by at most **0.00074412** before any
267
+ training. That is a precision effect relative to the original BF16 head, not a
268
+ failed serialization comparison. The `1e-4` reload threshold compares the saved
269
+ trained FP32-head model with its own fresh reload.
270
+
271
+ The first fresh reload, `runs/probe-reload-v1/report.json`, failed after 4.15
272
+ seconds because the saved adapter configuration differed from our metadata. PEFT
273
+ automatically shortened **248 full target paths to 12 module suffixes**. The strict
274
+ loader correctly refused a mismatch; the failure does not by itself mean the
275
+ weight tensors are corrupt. Accepting suffixes without checking what they match
276
+ could attach an adapter to unintended modules. The correction must expand saved
277
+ target selectors against the actual pinned backbone and require exactly the
278
+ intended full target set, rejecting any extra destinations. This is why fresh
279
+ reload is a milestone gate rather than a final packaging chore.
280
+
281
+ The corrected loader resolves saved selectors against the pinned backbone and
282
+ requires the exact intended destinations. The retry preserved the original
283
+ checkpoint and failed report; it changed validation of equivalent target selectors,
284
+ not the trained weights. A regression test covers target shortening and rejects
285
+ selectors that also match unintended modules.
286
+
287
+ Both independent fresh-process checks then passed:
288
+
289
+ | Artifact | Report | Maximum probability difference | Process time |
290
+ | --- | --- | ---: | ---: |
291
+ | Head only | `runs/probe-head-reload-v1/report.json` | 0.0 | 5.44 s |
292
+ | LoRA plus head | `runs/probe-reload-v2/report.json` | 0.0 | 5.65 s |
293
+
294
+ The LoRA retry used source commit
295
+ `f7be7ceee9c38566dfebad0ebf5f1c04958faee3`. The dataset manifest SHA-256 was
296
+ `aeddcfc1ea5390f12122d2e786c1e2030dcc97d3790b6d7523432548198b1f94` throughout.
297
+ The approved GLM service was restored and passed its health check after these
298
+ probes. **M4 is complete.** Milestone 5 can now train a selected checkpoint and
299
+ measure complete games; the four-position probe does not demonstrate that the
300
+ trained policy wins or generalizes.
code/docs/tutorials/05-evaluation.md ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Tutorial 05 — Select on validation, then measure complete games
2
+
3
+ Training, validation selection, selected-checkpoint reload and the held-out game
4
+ evaluation are complete. Epoch 02 cleared **16.81 lines per game**, versus **0.07**
5
+ for native Clef and **76.53** for the cheap heuristic on all 200 reserved seeds.
6
+ This tutorial gives the frozen procedure and explains the measured outcome. See
7
+ [the final study](../../reports/final-study.md) for full results and limitations,
8
+ and [the compact JSON](../../reports/final-summary.json) for exact metrics and hashes.
9
+ The experiment results are complete; release verification is tracked separately.
10
+
11
+ ## Separate learning, selection and final testing
12
+
13
+ The two candidate checkpoints are epochs 01 and 02 of the same seed-42 LoRA run.
14
+ Their training configuration and dataset are fixed before comparison. Evaluate
15
+ both on **all 215 validation positions** and select the lowest finite mean target
16
+ negative log likelihood (NLL). An exact tie chooses the earlier epoch. A candidate
17
+ with inference errors, missing probability distributions, or nonfinite NLL is
18
+ ineligible. If both are ineligible, stop before opening the test set.
19
+
20
+ NLL measures how much probability the model assigns to the teacher's recommended
21
+ placement. Selecting on a smooth probability score can distinguish checkpoints
22
+ even when their top choices tie. Teacher agreement and Brier score remain useful
23
+ diagnostics, but cannot override the preregistered rule after results are visible.
24
+
25
+ Copying the teacher is not the same as playing well. Each small mistake changes
26
+ the next board; the model eventually encounters states different from its training
27
+ examples. The teacher itself uses a limited search horizon and a heuristic value
28
+ function. Final evaluation must therefore play complete games on unseen sequences.
29
+
30
+ ## Train exactly the preregistered candidates
31
+
32
+ Pass the real GPU feasibility and fresh-process reload gates in Tutorial 04 first.
33
+ Use the exact 827/215-row dataset from Tutorial 03. The training runner rejects a
34
+ substituted dataset even when its labels look similar: hashes bind the experiment
35
+ to specific bytes and provenance. Use a fresh run directory when reproducing.
36
+
37
+ ```bash
38
+ uv sync --locked --extra ml --group dev
39
+ uv run --locked --extra ml python scripts/train_clef.py \
40
+ --dataset data/study-v1 --output runs/study-v1 \
41
+ --mode lora --epochs 2 --learning-rate 1e-5 \
42
+ --accumulation 8 --max-length 4096
43
+ ```
44
+
45
+ This command needs a free GPU. On the original workstation only, run it through
46
+ the authorized stop/restore wrapper from Tutorial 04 with a 3,600-second timeout.
47
+ The script uses seed 42, batch size one, rank-4/alpha-8 LoRA with zero dropout,
48
+ AdamW weight decay 0.01, gradient norm clipping at 1.0, and the fixed smoothed
49
+ cross-entropy plus Brier loss from Tutorial 04. The backbone stays BF16 and frozen;
50
+ LoRA matrices and the native FP32 head learn. Each epoch visits all training rows
51
+ in a recorded deterministic shuffled order. Validation rows never train the model.
52
+
53
+ Accumulating eight single-example gradients fits the memory budget while averaging
54
+ over several boards per optimizer step. The final group has only three rows and
55
+ is divided by three, not eight. Two complete epochs give two declared candidates
56
+ without an open-ended search. Neither falling loss nor a promising demo authorizes
57
+ extra epochs after the test set has been opened. The script defaults to one epoch,
58
+ so the explicit `--epochs 2` above is essential for reproducing this study.
59
+
60
+ The completed run processed 827 examples and 104 optimizer steps in each epoch:
61
+
62
+ | Epoch | Mean training loss | Time |
63
+ | --- | ---: | ---: |
64
+ | 01 | 2.577967 | 742.48 s |
65
+ | 02 | 1.966718 | 744.66 s |
66
+
67
+ Total training time including hashes and checkpoint work was 1,511.25 seconds
68
+ (about 25.2 minutes). Peak CUDA allocation was 23.23 GB, or about 21.64 GiB.
69
+ The head and LoRA parameters changed, while all frozen parameter hashes remained
70
+ unchanged. These are training measurements, not held-out performance. See
71
+ [the training report](../../reports/study-training.json),
72
+ [the original launch source](../../reports/training-launch-source.json), and
73
+ [the archived preregistration](../../reports/study-preregistration.md).
74
+
75
+ ## Prepare checkpoint metadata before freezing it
76
+
77
+ PEFT can write a generic adapter README with placeholders and a local cache path.
78
+ Keep raw training outputs immutable. Prepare candidate copies before validation:
79
+
80
+ ```bash
81
+ uv run --locked --extra ml python scripts/prepare_checkpoint.py \
82
+ --source runs/study-v1/epoch-01 --output checkpoints/candidates/epoch-01
83
+ uv run --locked --extra ml python scripts/prepare_checkpoint.py \
84
+ --source runs/study-v1/epoch-02 --output checkpoints/candidates/epoch-02
85
+ ```
86
+
87
+ The script replaces only `adapter/README.md`, compares every other file byte for
88
+ byte, and saves a sibling preparation record. It does not alter learned weights
89
+ or training settings. Validate and select these exact copies. Once selected,
90
+ even a README edit would break the frozen checkpoint file mapping. See Tutorial 06
91
+ for how release documentation stays outside the checkpoint directory.
92
+
93
+ ## Generate validation evidence
94
+
95
+ These commands load real GPU models. Use the memory admission and service-restoration
96
+ procedure from Tutorial 04. The evaluator never downloads weights implicitly or
97
+ stops another workload. Replace paths if your run directory differs.
98
+
99
+ ```sh
100
+ uv run --locked --extra ml python scripts/evaluate_clef.py positions \
101
+ --dataset data/study-v1 --checkpoint checkpoints/candidates/epoch-01 \
102
+ --players base base-fp32 trained --output runs/validation-epoch-01
103
+
104
+ uv run --locked --extra ml python scripts/evaluate_clef.py positions \
105
+ --dataset data/study-v1 --checkpoint checkpoints/candidates/epoch-02 \
106
+ --players trained --output runs/validation-epoch-02
107
+ ```
108
+
109
+ Every validation row gets an unrounded probability record or an explicit error.
110
+ The position report binds those records to checkpoint file hashes and the dataset
111
+ manifest. Aggregate NLL and Brier are unavailable if any probability distribution
112
+ is missing; a successful subset cannot masquerade as a complete evaluation.
113
+
114
+ The first command also measures two unchanged-model references. `base` retains
115
+ the original native BF16 head. `base-fp32` keeps those same learned weights but
116
+ uses our FP32 head wrapper. Training uses that wrapper, so the extra comparison
117
+ shows whether a change comes from numerical precision rather than learning.
118
+ The primary comparison remains trained versus native base. All three share the
119
+ same complete observation encoding and BF16 backbone.
120
+
121
+ ## Freeze the choice before running test episodes
122
+
123
+ ```sh
124
+ uv run --locked --extra ml python scripts/select_checkpoint.py \
125
+ --dataset data/study-v1 \
126
+ --candidate01 checkpoints/candidates/epoch-01 runs/validation-epoch-01/report.json \
127
+ --candidate02 checkpoints/candidates/epoch-02 runs/validation-epoch-02/report.json \
128
+ --output runs/selection-v1
129
+ ```
130
+
131
+ The selector independently recomputes metrics from every raw prediction against
132
+ the audited validation rows. It verifies actual checkpoint bytes, training seed
133
+ and configuration, epoch identity, common dataset and evaluation provenance. It
134
+ accepts exactly the two preregistered candidates. It then copies both validation
135
+ reports, prediction files and checkpoint metadata into a self-contained evidence
136
+ directory. The large model weights remain in their checkpoint directories and
137
+ are referenced by hashes.
138
+
139
+ `selection.json` records the selected epoch, decision rule, both candidates,
140
+ their eligibility, validation metrics, and the frozen test protocol. The runner
141
+ rechecks that the selected epoch actually wins the declared rule. Manual editing
142
+ of a label such as "best model" is not sufficient to unlock final evaluation.
143
+ Use a new output directory for a new selection attempt; existing evidence is not
144
+ silently overwritten.
145
+
146
+ ## Inspect selection and reload the selected artifact
147
+
148
+ The measured validation results used all 215 positions for every policy, with
149
+ zero errors and complete probability distributions:
150
+
151
+ | Policy | Teacher agreement | Mean target NLL | Mean Brier score |
152
+ | --- | ---: | ---: | ---: |
153
+ | Native base | 21/215 (9.77%) | 2.952702 | 0.923173 |
154
+ | Unchanged FP32 head | 22/215 (10.23%) | 2.952393 | 0.923102 |
155
+ | Epoch 01 | 89/215 (41.40%) | 1.860870 | 0.722190 |
156
+ | Epoch 02 | 92/215 (42.79%) | 1.775923 | 0.708980 |
157
+
158
+ The declared NLL rule selected epoch 02. The saved
159
+ [validation summary](../../reports/validation-summary.json) identifies its complete
160
+ file hashes and the selection hash. Teacher agreement improved on these positions;
161
+ that still does not establish better complete-game play.
162
+
163
+ Before consuming test seeds, reload the selected artifact in another process:
164
+
165
+ ```bash
166
+ uv run --locked --extra ml python scripts/probe_clef_training.py \
167
+ --dataset data/study-v1 --reload checkpoints/candidates/epoch-02 \
168
+ --output runs/selected-reload-v1
169
+ ```
170
+
171
+ This command also needs the GPU admission/restoration procedure. In this study it
172
+ reproduced all four training-position reference distributions with **0.0 maximum
173
+ absolute difference**, below the predeclared `1e-4` tolerance. See
174
+ [the reload report](../../reports/selected-checkpoint-reload.json).
175
+ The references use training positions, so this check does not consume test games.
176
+
177
+ ## Run the paired test once
178
+
179
+ Use the selected checkpoint path from the evidence, not whichever epoch you hoped
180
+ would win. The reserved pool is all **200 seeds, 30000–30199**, with a **200-piece
181
+ cap**. Each player gets the same piece sequence for a given seed. There are five
182
+ players: native base, FP32-head base ablation, selected trained model, random and
183
+ the fixed cheap heuristic.
184
+
185
+ ```sh
186
+ uv run --locked --extra ml python scripts/evaluate_clef.py tournament \
187
+ --checkpoint SELECTED_CHECKPOINT \
188
+ --seeds 30000:30200 --max-pieces 200 --final-test \
189
+ --selection-file runs/selection-v1/selection.json \
190
+ --output runs/final-evaluation
191
+ ```
192
+
193
+ `--final-test` explicitly acknowledges that these held-out seeds are being consumed.
194
+ Without it, any overlap with the reserved pool is rejected before model loading.
195
+ The cap and full seed pool cannot silently shrink to finish sooner. The initial
196
+ development timing estimate permits hours in the worst case; short early deaths
197
+ can make actual evaluation much faster.
198
+
199
+ Each completed episode is saved atomically, with actions, probabilities, runtime
200
+ configuration, errors and a replay. To continue an interrupted run, repeat the same
201
+ command with `--resume`. The runner checks source/dependency hashes, checkpoint
202
+ identity, selection, and saved replay outcomes. It skips completed players without
203
+ loading their models again. Recorded errors remain part of the experiment, rather
204
+ than being retried until a nicer result appears.
205
+
206
+ ## Read the report without hiding failures
207
+
208
+ The primary outcome is lines cleared. Score and pieces survived are secondary.
209
+ The report includes means, medians, cap-hit rate, failure counts, latency and input
210
+ lengths. The [hardware snapshot](../../reports/evaluation-hardware.json) records
211
+ the RTX 5090, driver, CPU, Python and inference settings observed during this run.
212
+ Compare latency only with that environment in mind; the snapshot does not measure
213
+ energy consumption or continuous GPU clock behavior. A game reaching 200 pieces survived **at least** 200; its eventual death
214
+ was not observed. Always report cap-hit rates with survival statistics.
215
+
216
+ For the primary reliability-adjusted analysis, any errored episode receives zero
217
+ lines, score and pieces. Its measured partial outcome remains visible separately.
218
+ Dropping failures would bias the result toward easier successful games. Both base
219
+ and trained must have zero episode errors for the default positive-signal flag.
220
+
221
+ For each seed, subtract base lines from trained lines, then bootstrap these paired
222
+ differences 10,000 times with RNG seed 2026. Report the mean and percentile 95%
223
+ interval. Resampling complete episode pairs respects the fact that moves within
224
+ one game are correlated. Treating every move as independent would exaggerate the
225
+ amount of evidence.
226
+
227
+ A lower interval bound above zero supports improved mean lines for this checkpoint
228
+ and seed distribution. It does not establish competitive Tetris ability, training
229
+ seed stability, or superiority over the heuristic. Report the trained-versus-
230
+ heuristic comparison even if it is embarrassing. A reproducible negative result
231
+ with clear limitations is a stronger portfolio artifact than a selective win.
232
+
233
+ After test results are opened, do not use them to choose between epochs or change
234
+ the observation format. A subsequent experiment needs a new preregistered study
235
+ and fresh held-out sequences. The current report should preserve failures and
236
+ limitations alongside the playable recorded comparison.
237
+
238
+
239
+ ## Completed outcome and what it teaches
240
+
241
+ All five players completed the 200 paired seeds at the declared 200-piece cap:
242
+
243
+ | Player | Mean lines | Median lines | Mean pieces | Cap hits |
244
+ | --- | ---: | ---: | ---: | ---: |
245
+ | Native base | 0.070 | 0 | 26.020 | 0/200 |
246
+ | Unchanged FP32 head | 0.105 | 0 | 26.000 | 0/200 |
247
+ | Selected epoch 02 | 16.810 | 16 | 83.565 | 0/200 |
248
+ | Random | 0.140 | 0 | 26.645 | 0/200 |
249
+ | Fixed heuristic | 76.530 | 77 | 200.000 | 200/200 |
250
+
251
+ There were zero inference errors and zero invalid decisions. The paired mean line
252
+ gain over native base was **16.74 [15.74, 17.77]** (95% percentile bootstrap,
253
+ 10,000 replicates, seed 2026). Against the unchanged FP32 head, the gain was
254
+ **16.705 [15.705, 17.74]**, so the observed improvement cannot be attributed to head
255
+ precision alone. Against the heuristic the difference was
256
+ **−59.72 [−60.805, −58.619875]**. Both the improvement and this large shortfall belong
257
+ in the result. The heuristic's eventual survival remains unknown because every
258
+ one of its games reached the cap.
259
+
260
+ The selected model's mean decision latency was **206.8 ms**, compared with the
261
+ heuristic's **0.1985 ms**, about 1,042 times slower on these measured workloads.
262
+ Policies visited different states, and neural inference used the GPU while the
263
+ heuristic used the CPU. This is an operational comparison, not an isolated
264
+ same-input speed benchmark. The final wrapper took 85.1 minutes, including model
265
+ loading, report writing and service restoration. Energy and monetary costs were
266
+ not measured. The simple heuristic remains the practical choice for this game.
267
+
268
+ An independent CPU audit replayed all 1,000 saved episodes and independently
269
+ recomputed every paired interval. See [its results](../../reports/final-audit.json).
270
+ The learned model passes the narrow improvement test over its unchanged base;
271
+ that does not turn its 42.79% teacher agreement into a general measure of game
272
+ quality. The final report includes score/survival intervals, all latency summaries,
273
+ hardware, source identities and portable release evidence paths.
274
+
275
+ To replicate the analysis, first verify the report/checkpoint/dataset hashes in
276
+ [final-summary.json](../../reports/final-summary.json), then use the frozen source
277
+ and commands above in fresh output directories. Reproducing inference needs the
278
+ GPU runtime; inspecting metrics and replaying saved actions do not. Reproduction
279
+ uses the already disclosed sequences, so it is not a new blind test. Any follow-up
280
+ training or design change needs a separate declared experiment and fresh test
281
+ seeds. No post-test tuning was used to produce the results reported here.
code/docs/tutorials/06-release.md ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Tutorial 06 — Ship an experiment another person can inspect
2
+
3
+ Publication is the last milestone, after training, validation selection and held-out
4
+ evaluation. A model upload alone is insufficient: the artifact must load, its claims
5
+ must match measured games, and the demo must distinguish recorded model decisions
6
+ from live human play. This tutorial describes the release process; publication links
7
+ and verified results are recorded in the final release report when available.
8
+
9
+ ## Separate the four deliverables
10
+
11
+ The source repository contains the simulator, native model adapter, training and
12
+ evaluation scripts, tests, locked environment and tutorials. The model repository
13
+ contains the selected checkpoint's LoRA weights and separately trained decision
14
+ head, plus source and losslessly compressed evidence needed to reproduce the study. The dataset repository
15
+ contains exact train/validation JSONL files and the generator manifest. The demo
16
+ repository runs the game and replays cached model decisions without downloading a
17
+ 9B model or renting GPU hardware.
18
+
19
+ The source bundle also contains `source-manifest.json` with its source commit and
20
+ file hashes. Scripts use an exact Git root when available, otherwise they verify
21
+ that manifest. Changed, missing or newly added source files mark the run as modified;
22
+ an unrelated parent repository is never mistaken for Stackcraft's source identity.
23
+ See Tutorial03 for hash-checked reproduction of the original dataset bytes without
24
+ access to the original Git repository.
25
+
26
+ These are different visibility decisions. A private GitHub repository does not
27
+ make files inside a public model repository or public Space private. In particular,
28
+ the reproducibility source in the model release and the Space's game source are
29
+ public if those repositories are public. Review the concrete local bundle, proposed
30
+ owners and visibility before creating external repositories.
31
+
32
+ The owner approved all four destinations as public on 2026-10-06: `kkarimi/stackcraft` on GitHub and
33
+ `nima1/stackcraft-clef-flash-lora`, `nima1/stackcraft-data`, and Space
34
+ `nima1/stackcraft` on Hugging Face. This approval explicitly includes the source
35
+ inside the public model and Space repositories. The final release report in GitHub
36
+ records immutable revisions and actual download/hosting verification.
37
+
38
+ ## Preserve evidence while preparing checkpoint metadata
39
+
40
+ Raw epoch checkpoints remain under the training run directory. PEFT can generate
41
+ an `adapter/README.md` with placeholders and a local cache path. Before validation,
42
+ copy each epoch checkpoint into a separate candidate directory and replace only
43
+ that documentation with accurate upstream attribution and loading instructions.
44
+ Compare every other file's SHA256 with the raw checkpoint and record the mapping.
45
+ The preparation script performs these checks and writes a sibling provenance file:
46
+
47
+ ```bash
48
+ uv run --locked --extra ml python scripts/prepare_checkpoint.py \
49
+ --source runs/study-v1/epoch-01 --output checkpoints/candidates/epoch-01
50
+ uv run --locked --extra ml python scripts/prepare_checkpoint.py \
51
+ --source runs/study-v1/epoch-02 --output checkpoints/candidates/epoch-02
52
+ ```
53
+ This edits no learned weights or training configuration.
54
+
55
+ Validate those exact candidate directories, then freeze the selected directory's
56
+ complete file/hash mapping. Do not revise it after selection. Model cards, reports
57
+ and release links belong outside `model/checkpoint/`, so adding release documentation
58
+ does not silently change the checkpoint used in validation and test evaluation.
59
+
60
+ A final model repository must contain both `joint_head.safetensors` and
61
+ `adapter/adapter_model.safetensors`. Loading only the adapter discards the learned
62
+ joint head. Loading the selected checkpoint requires the pinned upstream
63
+ `Cloudflare/clef-flash` snapshot and Stackcraft's strict loader, not a generic
64
+ text-generation pipeline. See Tutorial04 for the reason and serialization checks.
65
+
66
+ ## Assemble and audit a local release
67
+
68
+ Wait for all five players to finish all 200 games, then export the demonstration
69
+ from the complete final report. Seed 30000 was fixed before test results; do not
70
+ choose a more flattering game after seeing outcomes.
71
+
72
+ ```bash
73
+ uv run --locked --extra ml python scripts/export_demo.py \
74
+ --input runs/final-evaluation/report.json \
75
+ --players base trained heuristic --seed 30000 \
76
+ --output runs/demo-final.json
77
+ ```
78
+
79
+ The exporter validates the stored replays, observations and decisions without
80
+ loading a model. Inspect the output, then deliberately replace the packaged
81
+ historical development recording and review that Git diff:
82
+
83
+ ```bash
84
+ cp runs/demo-final.json src/stackcraft/web/baseline-demo.json
85
+ ```
86
+
87
+ Run the local game and check the final three-player comparison plus the optional
88
+ live human board. Then stage a new release directory:
89
+
90
+ ```bash
91
+ uv run --locked --extra ml python scripts/build_release.py \
92
+ --checkpoint checkpoints/candidates/epoch-02 \
93
+ --selection runs/selection-v1/selection.json \
94
+ --evaluation runs/final-evaluation --dataset data/study-v1 \
95
+ --output runs/release-v1
96
+ ```
97
+
98
+ Epoch 02 is the measured selected checkpoint for this study; use your frozen
99
+ selection's actual path when reproducing. The builder rejects incomplete final
100
+ evaluation, mismatched hashes or a missing selected-checkpoint reference. It also
101
+ requires the packaged demo to match the exact fixed-seed export from that report.
102
+
103
+ After a public report destination is confirmed, optionally pass the same
104
+ `--report-url https://APPROVED_REPORT_LOCATION` to **both** exporter and builder.
105
+ Use new output directories and deliberately update the packaged demo again.
106
+ Omitting the URL from only one command causes the exact-manifest check to fail.
107
+ These commands prepare local files only; neither creates a remote repository nor
108
+ uploads anything.
109
+
110
+ The builder stages separate model, dataset and demo directories and a release
111
+ manifest. Raw evaluation and selection files are packed in `model/evidence.zip`
112
+ with an `evidence/` path prefix. `evidence-files.json` records every uncompressed
113
+ file's SHA256 and size; the builder rechecks archive entry bytes before dropping
114
+ its temporary uncompressed copy. Compression changes transport size, not any
115
+ record, checkpoint or frozen evidence hash.
116
+
117
+ After verifying the downloaded release manifest, extract from the model repository
118
+ root when you need the raw reports or per-episode files:
119
+
120
+ ```bash
121
+ python -m zipfile -e evidence.zip .
122
+ ```
123
+
124
+ This restores the `evidence/evaluation/` and `evidence/selection/` paths used by
125
+ the study report. Model inference itself does not require extracting the logs.
126
+ It includes only explicit source/build inputs. Environments, credentials,
127
+ Git history, local service data, raw upstream backbone weights and arbitrary files
128
+ from the working directory are excluded. This is safer and more reproducible than
129
+ uploading the repository directory recursively and hoping ignore patterns cover it.
130
+
131
+ Audit the concrete staged files:
132
+
133
+ - Model and dataset cards describe synthetic data, teacher bias, simplified rules,
134
+ the selected training configuration and actual complete-game outcomes.
135
+ - Negative results, model errors, the FP32-head ablation and the cheap heuristic
136
+ remain visible. There are no unmeasured claims of superiority or calibrated confidence.
137
+ - Every planned test seed is represented for every preregistered player, including
138
+ failures; aggregate uncertainty agrees with the stored episode outcomes.
139
+ - The fixed demo seed is30000, chosen before results, with actual action probabilities.
140
+ - Dataset bytes and checkpoint bytes match their frozen hashes. The trained
141
+ checkpoint's probability references identify the same four training positions.
142
+ - LICENSE and NOTICE retain the Apache-2.0 attribution for Clef-flash/Qwen and
143
+ identify Stackcraft as an independent project.
144
+ - Cards have no placeholder metrics or paths that require the author's machine.
145
+
146
+ A release manifest identifies bytes, not scientific correctness. Independent replay
147
+ checks, selection review and honest documentation are still needed. Keep the
148
+ manifest hash and source commit with the final release report.
149
+
150
+ ## Validate the game-only deployment
151
+
152
+ Follow [the deployment guide](../demo-hosting.md) to build the pinned Docker image.
153
+ It installs through uv's lockfile with no ML extra, runs as UID1000 and listens on
154
+ port7860 for a Docker Space. Use the same image configuration locally first.
155
+
156
+ Test a human game, a downloaded/reloaded replay, the recorded comparison, probability
157
+ displays, scrubbing to final outcomes and returning to the preserved human session.
158
+ Check desktop and phone layouts. The recorded race must not imply that changing
159
+ playback speed changes inference speed. It illustrates one sequence; the report
160
+ summarizes all200 paired sequences.
161
+
162
+ The public demonstration needs CPU hosting because neural inference already happened
163
+ during evaluation. This removes ongoing GPU expense and makes the portfolio usable
164
+ when the training machine is offline. An interactive live-model endpoint would be a
165
+ separate deployment with admission, latency and concurrency requirements.
166
+
167
+ ## Publish only the approved bundle
168
+
169
+ Confirm concrete destinations and visibility after the local bundle is reviewable.
170
+ Use existing CLI authentication; do not put access tokens in code, Git, model cards,
171
+ commands or chat. Create the approved GitHub repository and push the reviewed local
172
+ commits. Create the approved model, dataset and Docker Space repositories, uploading
173
+ the corresponding staged directories only. The Space uses `cpu-basic`, with no hourly hardware charge. Creating a personal
174
+ Docker Space nevertheless requires a Hugging Face PRO subscription under the
175
+ current policy; see [the deployment guide](../demo-hosting.md). No subscription
176
+ purchase or paid GPU upgrade is authorized by this study.
177
+
178
+ Save immutable Hugging Face commit revisions returned by each upload, together with
179
+ the Git commit and model/dataset/demo URLs. Repository names alone are mutable;
180
+ revisions identify which bytes were checked. A successful upload does not establish
181
+ that the Space builds or the model loads.
182
+
183
+ ## Verify the actual release, not the local staging copy
184
+
185
+ Download the uploaded model and dataset into a new directory using those exact
186
+ revisions. Compare every released file against the release manifest. Run the dataset
187
+ audit on downloaded JSONL files and its manifest. Use a fresh Python process to load
188
+ the downloaded checkpoint onto the pinned upstream backbone and compare its four
189
+ raw reference probability distributions at maximum absolute tolerance1e-4.
190
+
191
+ Use `huggingface_hub.snapshot_download` with each recorded immutable revision and
192
+ an empty `local_dir` for each repository. Use `repo_type="dataset"` for the dataset
193
+ and `repo_type="space"` for the demo. `local_dir` materializes regular files; passing
194
+ a shared cache snapshot directory directly can expose symlinks, which the verifier
195
+ rejects. Keep the original local `release-manifest.json` separately as the trusted
196
+ comparison, rather than trusting a manifest fetched with the same download.
197
+
198
+ From the reviewed source checkout, verify all three downloaded repositories:
199
+
200
+ ```bash
201
+ uv run --locked --extra ml python scripts/verify_release.py \
202
+ --trusted-manifest LOCAL_RELEASE/release-manifest.json \
203
+ --model DOWNLOADED_MODEL \
204
+ --dataset DOWNLOADED_DATASET \
205
+ --demo DOWNLOADED_SPACE \
206
+ --output runs/released-files-verification.json
207
+ ```
208
+
209
+ This verifies file sizes and SHA256 hashes, checks each uploaded per-repository
210
+ manifest, and reruns the dataset schema/split audit. It rejects missing or extra
211
+ payloads and links, except narrowly defined Hugging Face download bookkeeping,
212
+ which it lists in the report. It does not establish publisher authenticity,
213
+ prove that a download was fresh, or load the neural model. Save the download
214
+ revisions and command separately, then perform the GPU parity check below.
215
+
216
+ The existing probe script can perform that final serialization check:
217
+
218
+ ```bash
219
+ uv run --locked --extra ml python scripts/probe_clef_training.py \
220
+ --dataset DOWNLOADED_DATASET \
221
+ --reload DOWNLOADED_MODEL/checkpoint \
222
+ --output runs/released-model-reload
223
+ ```
224
+
225
+ Run this only with sufficient free GPU memory. On the original workstation, use the
226
+ approved GLM stop/restore wrapper and verify restored health afterward. Another
227
+ machine must arrange its own workloads; the workstation-specific wrapper is not a
228
+ portable permission to stop services.
229
+
230
+ Check the published Space's `/health`, static assets, human moves and recorded race
231
+ in a browser. Verify model/dataset links and any private GitHub access expectations.
232
+ Record failures and correct the actual published bundle before declaring release
233
+ complete. Then update the plan's M6 status and final outcomes with verified links,
234
+ revisions, hashes, measured limitations and fresh-download evidence.
code/pyproject.toml ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "stackcraft"
7
+ version = "0.1.0"
8
+ description = "A reproducible falling-block playground for decision models"
9
+ readme = "README.md"
10
+ requires-python = ">=3.13,<3.14"
11
+ dependencies = ["fastapi", "uvicorn"]
12
+
13
+ [project.optional-dependencies]
14
+ ml = [
15
+ "torch==2.14.1",
16
+ "torchvision",
17
+ "transformers==5.18.0",
18
+ "peft==0.21.2",
19
+ "safetensors>=0.8,<1",
20
+ "huggingface-hub>=1.33,<2",
21
+ "pillow>=12,<13",
22
+ ]
23
+
24
+ [project.scripts]
25
+ stackcraft = "stackcraft.cli:main"
26
+
27
+ [dependency-groups]
28
+ dev = [
29
+ "pytest",
30
+ "ruff",
31
+ "ty",
32
+ "httpx2>=2.13.1",
33
+ ]
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/stackcraft"]
37
+
38
+ [tool.ruff]
39
+ target-version = "py313"
40
+ line-length = 100
41
+
42
+ [tool.ruff.lint]
43
+ select = ["E", "F", "I", "UP", "B"]
44
+
45
+ [tool.pytest.ini_options]
46
+ testpaths = ["tests"]
47
+ addopts = "-ra"
48
+
49
+ [tool.uv]
50
+ required-version = ">=0.12.23,<0.13"
51
+
52
+ [tool.uv.sources]
53
+ torch = [{ index = "pytorch-cu130", marker = "sys_platform == 'linux'" }]
54
+ torchvision = [{ index = "pytorch-cu130", marker = "sys_platform == 'linux'" }]
55
+
56
+ [[tool.uv.index]]
57
+ name = "pytorch-cu130"
58
+ url = "https://download.pytorch.org/whl/cu130"
59
+ explicit = true
code/release/dataset-card.md ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ tags:
6
+ - synthetic
7
+ - imitation-learning
8
+ - games
9
+ - stackcraft
10
+ size_categories:
11
+ - 1K<n<10K
12
+ configs:
13
+ - config_name: default
14
+ data_files:
15
+ - split: train
16
+ path: train.jsonl
17
+ - split: validation
18
+ path: validation.jsonl
19
+ ---
20
+
21
+ # Stackcraft search-teacher positions
22
+
23
+ A synthetic dataset for a deterministic, simplified falling-block placement game.
24
+ **827 training positions and 215 validation positions**, generated entirely by
25
+ code. No human demonstrations, personal data, web scraping or human row-by-row
26
+ label review are claimed. Independent agent review recomputed 16 pilot labels;
27
+ that sample audit is not a review of every row.
28
+
29
+ Each row contains a visible board, current piece, one next piece, all legal actions,
30
+ a teacher action, teacher action values and provenance. The board uses integer
31
+ piece colors; the model encoding reduces them to occupancy because color does not
32
+ affect these rules. Metadata includes seed and turn for reproduction; **do not
33
+ feed seed or hidden sequence metadata to the policy**. Stackcraft's encoder feeds
34
+ only the visible observation. Read the dataset schema and tutorial03 before use.
35
+
36
+ Related releases: [source and tutorials](https://github.com/kkarimi/stackcraft),
37
+ [model](https://huggingface.co/nima1/stackcraft-clef-flash-lora),
38
+ [dataset](https://huggingface.co/datasets/nima1/stackcraft-data), and
39
+ [planned CPU demo](https://huggingface.co/spaces/nima1/stackcraft).
40
+ The hosted demo awaits Hugging Face's Docker-account subscription requirement;
41
+ [local Docker instructions](https://github.com/kkarimi/stackcraft/blob/main/docs/demo-hosting.md)
42
+ work without that subscription.
43
+
44
+ ## Generation
45
+
46
+ The teacher searches the current placement and exactly one preview placement.
47
+ It rewards current line clears and the best next afterboard, using fixed weights
48
+ for aggregate height, holes, bumpiness and cleared lines. No later pieces are
49
+ available. Labels are heuristic recommendations, not proofs of optimal play.
50
+
51
+ Collection cycles random, heuristic and search-expert policies, at a 40-piece cap,
52
+ to expose different board conditions. Training seeds 10000–10023 and validation
53
+ seeds 20000–20005 are disjoint. Six within-training and two cross-split duplicate
54
+ observations were excluded. Final test seeds 30000–30199 are reserved for complete
55
+ game evaluation; there is no test-position split for training or selection.
56
+
57
+ Source commit: `70d84bd8dc5d4a60f3b96455a57d9e6f9416d109`.
58
+ The bundle includes `manifest.json` with source/configuration and split hashes.
59
+
60
+ - train SHA256: `edd682761db95a4f25bb30a284489c54d9336a36da0a9b19d2cda860b428baa8`
61
+ - validation SHA256: `eff9cdc5932e935959ac4d26dce6470d335090f7428a91930001954266d133bc`
62
+
63
+ The model release includes public source under `code/` and the exact regeneration
64
+ recipe in `code/docs/tutorials/03-data.md`. Use its **Reproduce from the public
65
+ source bundle without private Git access** procedure: first verify all six generator
66
+ source hashes against this dataset's `manifest.json`, then generate with the
67
+ original source-commit provenance. Access to the original Git repository is not
68
+ required. The procedure reproduced both JSONL hashes exactly on the study machine.
69
+ Ordinary `stackcraft generate-data --output NEWDIR` records the current source
70
+ identity; a new source commit deliberately changes provenance fields and therefore
71
+ file hashes even if labels are identical. A regeneration and an independent
72
+ sample-label audit preceded study training.
73
+
74
+ ## Limits
75
+
76
+ The study is small and entirely synthetic. Teacher bias, narrow collection seeds,
77
+ short collection episodes and one-step preview limit generalization. Duplicate
78
+ removal prevents exact overlap, not all structural similarity. It is not full
79
+ real-time Tetris: no hold, kicks, tucks or gravity timing. Colors and piece IDs
80
+ are program-generated; Stackcraft uses original game visuals and branding.
81
+
82
+ Use teacher agreement as a diagnostic and complete held-out games for outcome
83
+ measurement. Do not call a model better solely because it copies these labels.
84
+ Apache-2.0; see LICENSE and accompanying generation/reproduction tutorials.
code/release/model-card.md ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model: Cloudflare/clef-flash
4
+ base_model_relation: adapter
5
+ library_name: peft
6
+ language:
7
+ - en
8
+ tags:
9
+ - clef
10
+ - decision-model
11
+ - lora
12
+ - imitation-learning
13
+ - stackcraft
14
+ ---
15
+
16
+ # Stackcraft Clef-Flash
17
+
18
+ **Release preparation: full-study results and selected checkpoint are pending.**
19
+ This is a small, synthetic imitation-learning study of a falling-block placement
20
+ policy. It is not a generally capable game agent, reinforcement-learning result,
21
+ or competitive Tetris implementation.
22
+
23
+ The artifact contains a rank-4 LoRA adapter **and a separately trained FP32 native
24
+ joint decision head** for `Cloudflare/clef-flash`, pinned revision
25
+ `17f0b0ad64efb65d273590632833508766b2aae6`. Load both. The frozen BF16 backbone
26
+ is downloaded separately; these files are not a standalone model. Clef-flash is
27
+ based on Qwen3.5-9B. The native schema head scores every supplied legal placement
28
+ in one forward pass, with no generated text or command parsing.
29
+
30
+ Related releases: [source and tutorials](https://github.com/kkarimi/stackcraft),
31
+ [model](https://huggingface.co/nima1/stackcraft-clef-flash-lora),
32
+ [dataset](https://huggingface.co/datasets/nima1/stackcraft-data), and
33
+ [planned CPU demo](https://huggingface.co/spaces/nima1/stackcraft).
34
+ The hosted demo awaits Hugging Face's Docker-account subscription requirement;
35
+ [local Docker instructions](https://github.com/kkarimi/stackcraft/blob/main/docs/demo-hosting.md)
36
+ work without that subscription.
37
+
38
+ ## Intended use
39
+
40
+ Learn how to build deterministic environments, generate search-teacher labels,
41
+ train a custom decision model, preserve its native head, and evaluate complete
42
+ games with paired uncertainty. The playable demo uses recorded model replays;
43
+ it does not provide public live GPU inference.
44
+
45
+ The observation is a 10×20 occupied-cell grid, current tetromino, exactly one next
46
+ piece, rules and every legal placement with its coordinates. The action selects
47
+ an orientation and column, then drops vertically. There is no hold, wall kick,
48
+ tuck, gravity timer, T-spin bonus or changing speed. Hidden future pieces and
49
+ sequence seeds are excluded from neural input. Probabilities reflect the model's
50
+ choice distribution; they are not calibrated chances of clearing a line or winning.
51
+
52
+ ## Training and data
53
+
54
+ The study uses 827 training and 215 validation positions generated from fixed
55
+ seven-bag streams. Random, heuristic and search policies collect states. Every
56
+ label comes from a fixed two-placement search using the current piece and one
57
+ preview, with a board-height/holes/bumpiness/line-clear heuristic. These are
58
+ synthetic teacher preferences, not human demonstrations or globally optimal moves.
59
+ Exact duplicate observations are removed across splits. Six source hashes and
60
+ exact file hashes accompany the dataset manifest.
61
+
62
+ Preregistered training: seed 42, two epochs, batch 1, accumulation 8, rank 4/alpha 8,
63
+ no LoRA dropout, AdamW lr 1e-5/weight_decay 0.01, gradient norm cap 1.0, label smoothing
64
+ 0.05 and Brier sum weight0.1. BF16 backbone, FP32 head, text-layer LoRA including
65
+ both hybrid attention types and MLPs. Vision, output and other original parameters
66
+ remain frozen. There are 132,582,404 trainable parameters: 121,762,820 in the
67
+ joint head and 10,819,584 in LoRA. This is why the head is a substantial part of
68
+ the saved artifact, even though most backbone weights stay frozen. Select only between epoch 01/02 using lowest finite target NLL on
69
+ all 215 validation positions; exact ties choose the earlier epoch.
70
+
71
+ ## Evaluation and limitations
72
+
73
+ Final preregistered comparison: 200 untouched seeds 30000–30199, 200-piece cap,
74
+ selected model versus unchanged native BF16-head Clef, unchanged FP32-head
75
+ ablation, random legal placement and a cheap heuristic. Primary metric is lines
76
+ cleared. Report score, survival, cap hits, failures and latency. Paired 95% bootstrap
77
+ intervals use 10000 episode resamples with seed 2026. Failed episodes remain in the
78
+ analysis, assigned zero primary outcomes with partial outcomes reported separately.
79
+
80
+ **Insert verified held-out results here before publication.**
81
+
82
+ Fine-tuning improved complete-game play over both unchanged Clef references in
83
+ this study. The fixed heuristic still cleared substantially more lines and used
84
+ about one thousandth of the trained model's decision time on this workstation.
85
+ All five policies had zero errors and invalid decisions. Every heuristic game hit
86
+ the 200-piece cap; no neural or random game did. This is evidence of task adaptation,
87
+ not a reason to prefer this 9B model over the heuristic for the simplified game.
88
+ See [the readable study report](code/reports/final-study.md) and
89
+ [the hardware/runtime snapshot](code/reports/evaluation-hardware.json).
90
+
91
+
92
+ Only one training seed, one small synthetic dataset, one simplified game ruleset
93
+ and one hardware/software configuration are studied. The finite cap censors
94
+ survival. Validation teacher agreement does not establish game quality. Test
95
+ uncertainty does not capture variation across training seeds.
96
+
97
+ ## Loading
98
+
99
+ Use the released Stackcraft source and locked ML dependencies. Generic
100
+ `AutoModel` or adapter-only loading omits the custom decision head and is incorrect.
101
+ From the downloaded model repository's `code/` directory, install the locked ML
102
+ environment with `uv sync --locked --extra ml` and cache the pinned upstream
103
+ snapshot as described in [Tutorial 02](code/docs/tutorials/02-baselines.md).
104
+ Run this example with `uv run --locked --extra ml python`; the sibling
105
+ `../checkpoint` directory contains the released adapter and head:
106
+
107
+ ```python
108
+ from stackcraft.clef import ClefPlayer
109
+ from stackcraft.training import load_checkpoint
110
+ from stackcraft.engine import new_game
111
+ from stackcraft.players import observe
112
+
113
+ player = ClefPlayer.from_pretrained(trust_pinned_code=True, local_files_only=True)
114
+ load_checkpoint(player.model, "../checkpoint")
115
+ print(player.choose(observe(new_game(42))))
116
+ ```
117
+
118
+ `trust_pinned_code=True` executes reviewed, hash-checked upstream Python. It is
119
+ not a sandbox. The measured RTX 5090 inference baseline allocated 19.7 GB; bounded
120
+ LoRA training allocated 23.2 GB. Load only with sufficient free VRAM. Context is
121
+ limited to 4096 tokens, with explicit rejection before any state truncation.
122
+
123
+ See the released tutorials, training configuration, artifact hashes and evaluation
124
+ report for exact reproduction. Apache-2.0; see LICENSE and NOTICE for upstream
125
+ attribution. This project is independent of Cloudflare, Qwen and Tetris.
code/reports/clef-development.json ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema_version": 1,
3
+ "rules_version": "stackcraft-v1",
4
+ "seeds": [
5
+ 0,
6
+ 1
7
+ ],
8
+ "max_pieces": 30,
9
+ "summary": {
10
+ "random": {
11
+ "episodes": 2,
12
+ "mean_lines": 0,
13
+ "median_lines": 0.0,
14
+ "mean_score": 0,
15
+ "mean_pieces": 27,
16
+ "cap_hit_rate": 0,
17
+ "error_rate": 0,
18
+ "timing": {
19
+ "count": 54,
20
+ "total_seconds": 9.454792598262429e-05,
21
+ "mean_seconds": 1.7508875181967461e-06,
22
+ "median_seconds": 1.580017851665616e-06,
23
+ "p95_seconds": 2.9500224627554417e-06
24
+ }
25
+ },
26
+ "heuristic": {
27
+ "episodes": 2,
28
+ "mean_lines": 9,
29
+ "median_lines": 9.0,
30
+ "mean_score": 1200,
31
+ "mean_pieces": 30,
32
+ "cap_hit_rate": 1,
33
+ "error_rate": 0,
34
+ "timing": {
35
+ "count": 60,
36
+ "total_seconds": 0.010729207948315889,
37
+ "mean_seconds": 0.00017882013247193148,
38
+ "median_seconds": 0.00013837897859048098,
39
+ "p95_seconds": 0.00027111798408441246
40
+ }
41
+ },
42
+ "clef-base": {
43
+ "episodes": 2,
44
+ "mean_lines": 0,
45
+ "median_lines": 0.0,
46
+ "mean_score": 0,
47
+ "mean_pieces": 27.5,
48
+ "cap_hit_rate": 0,
49
+ "error_rate": 0,
50
+ "timing": {
51
+ "count": 55,
52
+ "total_seconds": 7.934351755975513,
53
+ "mean_seconds": 0.1442609410177366,
54
+ "median_seconds": 0.1331863310188055,
55
+ "p95_seconds": 0.24066273399512284
56
+ }
57
+ }
58
+ },
59
+ "development_only": true,
60
+ "gpu": "NVIDIA GeForce RTX 5090",
61
+ "peak_allocated_bytes": 19686539776,
62
+ "peak_reserved_bytes": 20002635776,
63
+ "elapsed_seconds": 12.598100575996796,
64
+ "artifact_sha256": "3c56622dc2a95eb89df4cae87ecce4bb2a4afb26e47c43553a9e7f826e971e93",
65
+ "replays_verified": 6
66
+ }
code/reports/dataset-manifest.json ADDED
@@ -0,0 +1,539 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "config": {
3
+ "behavior_cycle": [
4
+ "random",
5
+ "heuristic",
6
+ "expert"
7
+ ],
8
+ "max_pieces": 40,
9
+ "reserved_test_seeds": [
10
+ 30000,
11
+ 30001,
12
+ 30002,
13
+ 30003,
14
+ 30004,
15
+ 30005,
16
+ 30006,
17
+ 30007,
18
+ 30008,
19
+ 30009,
20
+ 30010,
21
+ 30011,
22
+ 30012,
23
+ 30013,
24
+ 30014,
25
+ 30015,
26
+ 30016,
27
+ 30017,
28
+ 30018,
29
+ 30019,
30
+ 30020,
31
+ 30021,
32
+ 30022,
33
+ 30023,
34
+ 30024,
35
+ 30025,
36
+ 30026,
37
+ 30027,
38
+ 30028,
39
+ 30029,
40
+ 30030,
41
+ 30031,
42
+ 30032,
43
+ 30033,
44
+ 30034,
45
+ 30035,
46
+ 30036,
47
+ 30037,
48
+ 30038,
49
+ 30039,
50
+ 30040,
51
+ 30041,
52
+ 30042,
53
+ 30043,
54
+ 30044,
55
+ 30045,
56
+ 30046,
57
+ 30047,
58
+ 30048,
59
+ 30049,
60
+ 30050,
61
+ 30051,
62
+ 30052,
63
+ 30053,
64
+ 30054,
65
+ 30055,
66
+ 30056,
67
+ 30057,
68
+ 30058,
69
+ 30059,
70
+ 30060,
71
+ 30061,
72
+ 30062,
73
+ 30063,
74
+ 30064,
75
+ 30065,
76
+ 30066,
77
+ 30067,
78
+ 30068,
79
+ 30069,
80
+ 30070,
81
+ 30071,
82
+ 30072,
83
+ 30073,
84
+ 30074,
85
+ 30075,
86
+ 30076,
87
+ 30077,
88
+ 30078,
89
+ 30079,
90
+ 30080,
91
+ 30081,
92
+ 30082,
93
+ 30083,
94
+ 30084,
95
+ 30085,
96
+ 30086,
97
+ 30087,
98
+ 30088,
99
+ 30089,
100
+ 30090,
101
+ 30091,
102
+ 30092,
103
+ 30093,
104
+ 30094,
105
+ 30095,
106
+ 30096,
107
+ 30097,
108
+ 30098,
109
+ 30099,
110
+ 30100,
111
+ 30101,
112
+ 30102,
113
+ 30103,
114
+ 30104,
115
+ 30105,
116
+ 30106,
117
+ 30107,
118
+ 30108,
119
+ 30109,
120
+ 30110,
121
+ 30111,
122
+ 30112,
123
+ 30113,
124
+ 30114,
125
+ 30115,
126
+ 30116,
127
+ 30117,
128
+ 30118,
129
+ 30119,
130
+ 30120,
131
+ 30121,
132
+ 30122,
133
+ 30123,
134
+ 30124,
135
+ 30125,
136
+ 30126,
137
+ 30127,
138
+ 30128,
139
+ 30129,
140
+ 30130,
141
+ 30131,
142
+ 30132,
143
+ 30133,
144
+ 30134,
145
+ 30135,
146
+ 30136,
147
+ 30137,
148
+ 30138,
149
+ 30139,
150
+ 30140,
151
+ 30141,
152
+ 30142,
153
+ 30143,
154
+ 30144,
155
+ 30145,
156
+ 30146,
157
+ 30147,
158
+ 30148,
159
+ 30149,
160
+ 30150,
161
+ 30151,
162
+ 30152,
163
+ 30153,
164
+ 30154,
165
+ 30155,
166
+ 30156,
167
+ 30157,
168
+ 30158,
169
+ 30159,
170
+ 30160,
171
+ 30161,
172
+ 30162,
173
+ 30163,
174
+ 30164,
175
+ 30165,
176
+ 30166,
177
+ 30167,
178
+ 30168,
179
+ 30169,
180
+ 30170,
181
+ 30171,
182
+ 30172,
183
+ 30173,
184
+ 30174,
185
+ 30175,
186
+ 30176,
187
+ 30177,
188
+ 30178,
189
+ 30179,
190
+ 30180,
191
+ 30181,
192
+ 30182,
193
+ 30183,
194
+ 30184,
195
+ 30185,
196
+ 30186,
197
+ 30187,
198
+ 30188,
199
+ 30189,
200
+ 30190,
201
+ 30191,
202
+ 30192,
203
+ 30193,
204
+ 30194,
205
+ 30195,
206
+ 30196,
207
+ 30197,
208
+ 30198,
209
+ 30199
210
+ ],
211
+ "train_seeds": [
212
+ 10000,
213
+ 10001,
214
+ 10002,
215
+ 10003,
216
+ 10004,
217
+ 10005,
218
+ 10006,
219
+ 10007,
220
+ 10008,
221
+ 10009,
222
+ 10010,
223
+ 10011,
224
+ 10012,
225
+ 10013,
226
+ 10014,
227
+ 10015,
228
+ 10016,
229
+ 10017,
230
+ 10018,
231
+ 10019,
232
+ 10020,
233
+ 10021,
234
+ 10022,
235
+ 10023
236
+ ],
237
+ "validation_seeds": [
238
+ 20000,
239
+ 20001,
240
+ 20002,
241
+ 20003,
242
+ 20004,
243
+ 20005
244
+ ]
245
+ },
246
+ "config_sha256": "65b72544c80cf19f4ac3b12ca109d0ebda18fb70eadb4b170a55b1e8bb3b4f6d",
247
+ "deduplication": {
248
+ "exclusions": {
249
+ "train": {
250
+ "cross_split": 0,
251
+ "within_split": 6
252
+ },
253
+ "validation": {
254
+ "cross_split": 2,
255
+ "within_split": 0
256
+ }
257
+ },
258
+ "normalization": "board occupancy"
259
+ },
260
+ "rules_version": "stackcraft-v1",
261
+ "schema_version": 1,
262
+ "seed_pools": {
263
+ "reserved_test": [
264
+ 30000,
265
+ 30001,
266
+ 30002,
267
+ 30003,
268
+ 30004,
269
+ 30005,
270
+ 30006,
271
+ 30007,
272
+ 30008,
273
+ 30009,
274
+ 30010,
275
+ 30011,
276
+ 30012,
277
+ 30013,
278
+ 30014,
279
+ 30015,
280
+ 30016,
281
+ 30017,
282
+ 30018,
283
+ 30019,
284
+ 30020,
285
+ 30021,
286
+ 30022,
287
+ 30023,
288
+ 30024,
289
+ 30025,
290
+ 30026,
291
+ 30027,
292
+ 30028,
293
+ 30029,
294
+ 30030,
295
+ 30031,
296
+ 30032,
297
+ 30033,
298
+ 30034,
299
+ 30035,
300
+ 30036,
301
+ 30037,
302
+ 30038,
303
+ 30039,
304
+ 30040,
305
+ 30041,
306
+ 30042,
307
+ 30043,
308
+ 30044,
309
+ 30045,
310
+ 30046,
311
+ 30047,
312
+ 30048,
313
+ 30049,
314
+ 30050,
315
+ 30051,
316
+ 30052,
317
+ 30053,
318
+ 30054,
319
+ 30055,
320
+ 30056,
321
+ 30057,
322
+ 30058,
323
+ 30059,
324
+ 30060,
325
+ 30061,
326
+ 30062,
327
+ 30063,
328
+ 30064,
329
+ 30065,
330
+ 30066,
331
+ 30067,
332
+ 30068,
333
+ 30069,
334
+ 30070,
335
+ 30071,
336
+ 30072,
337
+ 30073,
338
+ 30074,
339
+ 30075,
340
+ 30076,
341
+ 30077,
342
+ 30078,
343
+ 30079,
344
+ 30080,
345
+ 30081,
346
+ 30082,
347
+ 30083,
348
+ 30084,
349
+ 30085,
350
+ 30086,
351
+ 30087,
352
+ 30088,
353
+ 30089,
354
+ 30090,
355
+ 30091,
356
+ 30092,
357
+ 30093,
358
+ 30094,
359
+ 30095,
360
+ 30096,
361
+ 30097,
362
+ 30098,
363
+ 30099,
364
+ 30100,
365
+ 30101,
366
+ 30102,
367
+ 30103,
368
+ 30104,
369
+ 30105,
370
+ 30106,
371
+ 30107,
372
+ 30108,
373
+ 30109,
374
+ 30110,
375
+ 30111,
376
+ 30112,
377
+ 30113,
378
+ 30114,
379
+ 30115,
380
+ 30116,
381
+ 30117,
382
+ 30118,
383
+ 30119,
384
+ 30120,
385
+ 30121,
386
+ 30122,
387
+ 30123,
388
+ 30124,
389
+ 30125,
390
+ 30126,
391
+ 30127,
392
+ 30128,
393
+ 30129,
394
+ 30130,
395
+ 30131,
396
+ 30132,
397
+ 30133,
398
+ 30134,
399
+ 30135,
400
+ 30136,
401
+ 30137,
402
+ 30138,
403
+ 30139,
404
+ 30140,
405
+ 30141,
406
+ 30142,
407
+ 30143,
408
+ 30144,
409
+ 30145,
410
+ 30146,
411
+ 30147,
412
+ 30148,
413
+ 30149,
414
+ 30150,
415
+ 30151,
416
+ 30152,
417
+ 30153,
418
+ 30154,
419
+ 30155,
420
+ 30156,
421
+ 30157,
422
+ 30158,
423
+ 30159,
424
+ 30160,
425
+ 30161,
426
+ 30162,
427
+ 30163,
428
+ 30164,
429
+ 30165,
430
+ 30166,
431
+ 30167,
432
+ 30168,
433
+ 30169,
434
+ 30170,
435
+ 30171,
436
+ 30172,
437
+ 30173,
438
+ 30174,
439
+ 30175,
440
+ 30176,
441
+ 30177,
442
+ 30178,
443
+ 30179,
444
+ 30180,
445
+ 30181,
446
+ 30182,
447
+ 30183,
448
+ 30184,
449
+ 30185,
450
+ 30186,
451
+ 30187,
452
+ 30188,
453
+ 30189,
454
+ 30190,
455
+ 30191,
456
+ 30192,
457
+ 30193,
458
+ 30194,
459
+ 30195,
460
+ 30196,
461
+ 30197,
462
+ 30198,
463
+ 30199
464
+ ],
465
+ "train": [
466
+ 10000,
467
+ 10001,
468
+ 10002,
469
+ 10003,
470
+ 10004,
471
+ 10005,
472
+ 10006,
473
+ 10007,
474
+ 10008,
475
+ 10009,
476
+ 10010,
477
+ 10011,
478
+ 10012,
479
+ 10013,
480
+ 10014,
481
+ 10015,
482
+ 10016,
483
+ 10017,
484
+ 10018,
485
+ 10019,
486
+ 10020,
487
+ 10021,
488
+ 10022,
489
+ 10023
490
+ ],
491
+ "validation": [
492
+ 20000,
493
+ 20001,
494
+ 20002,
495
+ 20003,
496
+ 20004,
497
+ 20005
498
+ ]
499
+ },
500
+ "source_commit": "70d84bd8dc5d4a60f3b96455a57d9e6f9416d109",
501
+ "source_hashes": {
502
+ "data.py": "74b68a1f391a3128d2e499c7e86b6c94a1f2a2bef33ffe75c8631207065bd991",
503
+ "engine.py": "0805be38ee968d569fa1c12230730f491490f7d27cfcc570c400ac3b4a546453",
504
+ "expert.py": "608ea7c1516083807d58dbd9bec82d505c1d4bca61e14579f4821e51607e08b5",
505
+ "pieces.py": "c5bee0f8767aeeab279cfbadb4c7b4c0bbf1f31c1d29b08dda197f0627654b29",
506
+ "players/__init__.py": "550bed9237303a845cdcc101542b0b688173a8f5436dd1d08c5f85a5a19fa2b2",
507
+ "schema.py": "35c70d96e8a49e434dc0a3607412a0ce58141306df59e09b1170511694cb6eb5"
508
+ },
509
+ "source_note": "File hashes identify source; commit may precede working-tree edits.",
510
+ "splits": {
511
+ "train": {
512
+ "behavior_records": {
513
+ "heuristic": 318,
514
+ "random": 190,
515
+ "search-expert": 319
516
+ },
517
+ "collected": 833,
518
+ "records": 827,
519
+ "sha256": "edd682761db95a4f25bb30a284489c54d9336a36da0a9b19d2cda860b428baa8"
520
+ },
521
+ "validation": {
522
+ "behavior_records": {
523
+ "heuristic": 80,
524
+ "random": 56,
525
+ "search-expert": 79
526
+ },
527
+ "collected": 217,
528
+ "records": 215,
529
+ "sha256": "eff9cdc5932e935959ac4d26dce6470d335090f7428a91930001954266d133bc"
530
+ }
531
+ },
532
+ "teacher": {
533
+ "optimal": false,
534
+ "preview_pieces": 1,
535
+ "search": "exhaustive legal placements"
536
+ },
537
+ "teacher_revision": "search-v1-preview1-height1-holes4-bump1-lines8-topout10000",
538
+ "test_trajectories_generated": false
539
+ }
code/reports/evaluation-hardware.json ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cpu_model": "AMD Ryzen 9 9950X3D 16-Core Processor",
3
+ "evaluation_request_sha256": "a507e3b947d603b02f56b6df12b160e9e182ee5706d2ac64d8e9f93358f79e62",
4
+ "gpus": [
5
+ {
6
+ "configured_power_limit_watts": 575.0,
7
+ "driver_version": "610.57.04",
8
+ "name": "NVIDIA GeForce RTX 5090",
9
+ "reported_total_memory_mib": 32607
10
+ }
11
+ ],
12
+ "host_reported_memory_kib": 129391464,
13
+ "kernel": "7.2.5-3-omarchy",
14
+ "logical_cpu_count": 32,
15
+ "observed_at_utc": "2026-10-05T21:20:14.982799+00:00",
16
+ "python_version": "3.13.16",
17
+ "schema_version": 1,
18
+ "scope": "Read-only hardware snapshot during final evaluation; no energy usage or continuous clock/temperature measurement",
19
+ "trained_runtime_config": {
20
+ "base_revision": "17f0b0ad64efb65d273590632833508766b2aae6",
21
+ "cpu_threads": 8,
22
+ "cuda_matmul_allow_tf32": false,
23
+ "cuda_version": "13.0",
24
+ "device": "cuda:0",
25
+ "dtype": "torch.bfloat16",
26
+ "encoding_version": "stackcraft-clef-v1",
27
+ "evaluation_head": "fp32-gathered-rows",
28
+ "float32_matmul_precision": "highest",
29
+ "head_dtype": "float32",
30
+ "max_length": 4096,
31
+ "model_id": "Cloudflare/clef-flash",
32
+ "revision": "Cloudflare/clef-flash@17f0b0ad64efb65d273590632833508766b2aae6:checkpoint-sha256-b8133e9775a82c6b952fc1a1b1c805c5bc993fbcd919e81f663ad35e122ab7c7:stackcraft-clef-v1",
33
+ "source_sha256": "0e304cf7c6500e8bb59bef7e2afd2c6373f82596dfb3b57d1aa93c175e2dc3a3",
34
+ "versions": {
35
+ "huggingface-hub": "1.33.0",
36
+ "peft": "0.21.2",
37
+ "safetensors": "0.8.0",
38
+ "torch": "2.14.1+cu130",
39
+ "transformers": "5.18.0"
40
+ }
41
+ }
42
+ }
code/reports/final-audit-source.md ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Independent final evidence audit
2
+
3
+ This archives the exact CPU audit script run after evaluation. Its SHA256 is
4
+ recorded in [the audit result](final-audit.json). It assumes the original local
5
+ `runs/`, dataset and checkpoint layout; it is an execution record, not a portable
6
+ release-verification CLI. Use `scripts/verify_release.py` for release downloads.
7
+ No new games or model inference were run by this audit.
8
+
9
+ ```text
10
+ """Independent CPU-only audit of frozen final evidence; reconstructs saved actions only."""
11
+ import hashlib
12
+ import importlib.util
13
+ import json
14
+ import math
15
+ import random
16
+ import statistics
17
+ import time
18
+ from pathlib import Path
19
+ from types import SimpleNamespace
20
+
21
+ ROOT = Path.cwd()
22
+ OUT = ROOT / 'runs/final-review/audit.json'
23
+ assert not OUT.exists(), 'Refuse to overwrite audit'
24
+ start = time.monotonic()
25
+ def read(p):
26
+ return json.loads(Path(p).read_text())
27
+ def sha(p):
28
+ h = hashlib.sha256()
29
+ with Path(p).open('rb') as f:
30
+ while chunk := f.read(1048576):
31
+ h.update(chunk)
32
+ return h.hexdigest()
33
+ def module(name):
34
+ spec = importlib.util.spec_from_file_location(name, ROOT / 'scripts' / name)
35
+ obj = importlib.util.module_from_spec(spec)
36
+ spec.loader.exec_module(obj)
37
+ return obj
38
+
39
+ evaluator = module('evaluate_clef.py')
40
+ exporter = module('export_demo.py')
41
+ request = read('runs/final-evaluation/request.json')
42
+ report_path = Path('runs/final-evaluation/report.json')
43
+ report = read(report_path)
44
+ print('Loaded final report; validating frozen identities.', flush=True)
45
+ players = ['base', 'base-fp32', 'trained', 'random', 'heuristic']
46
+ seeds = list(range(30000, 30200))
47
+ assert request['players'] == players
48
+ assert set(report['players']) == set(players)
49
+ for key, expected in [('mode', 'tournament'), ('final_test', True), ('max_pieces', 200), ('seeds', seeds)]:
50
+ assert report[key] == request[key] == expected, key
51
+ for file, expected in request['source_hashes'].items():
52
+ assert sha(file) == expected, file
53
+ assert report['provenance']['source_hashes'] == request['source_hashes']
54
+ assert report['provenance']['installed_versions'] == request['installed_versions']
55
+ checkpoint = Path(request['checkpoint'])
56
+ checkpoint_hashes = evaluator.checkpoint_hashes(checkpoint)
57
+ assert checkpoint_hashes == request['checkpoint_sha256'] == report['checkpoint_sha256']
58
+ selection_path = Path(request['selection_file'])
59
+ assert sha(selection_path) == request['selection_sha256']
60
+ assert read(selection_path) == report['selection']
61
+ args = SimpleNamespace(seeds=tuple(seeds), final_test=True, max_pieces=200, players=players,
62
+ selection_file=selection_path, checkpoint=checkpoint, max_length=4096)
63
+ evaluator.validate_selection(args, checkpoint_hashes)
64
+ evaluator.validate_neural_runtimes(report['players'])
65
+ episodes = report['episodes']
66
+ assert len(episodes) == 1000
67
+ indexed = {(e['player_id'], e['seed']): e for e in episodes}
68
+ assert len(indexed) == 1000
69
+ assert set(indexed) == {(p, s) for p in players for s in seeds}
70
+ metrics = ('lines', 'score', 'pieces')
71
+ summaries = {}
72
+ episode_hashes = {}
73
+ def summary(values):
74
+ if not values:
75
+ return dict(count=0, mean=None, median=None, p95=None)
76
+ return dict(count=len(values), mean=statistics.mean(values), median=statistics.median(values),
77
+ p95=sorted(values)[math.ceil(.95 * len(values)) - 1])
78
+ for player in players:
79
+ metadata = read(Path('runs/final-evaluation') / player / 'player.json')
80
+ assert metadata == report['players'][player]
81
+ rows = []
82
+ for seed in seeds:
83
+ path = Path('runs/final-evaluation') / player / f'seed-{seed}.json'
84
+ episode = read(path)
85
+ assert episode == indexed[player, seed], str(path)
86
+ assert episode['player']['revision'] == metadata['revision']
87
+ assert episode['player']['runtime_config'] == metadata['runtime_config']
88
+ assert episode['max_pieces'] == 200
89
+ exporter.validated_player(episode, 0)
90
+ if player in ('base', 'base-fp32', 'trained'):
91
+ assert all(d['probabilities'] and 0 < d['input_tokens'] <= 4096 for d in episode['decisions'])
92
+ episode_hashes[str(path)] = sha(path)
93
+ rows.append(episode)
94
+ primary = {m: summary([0 if e['errors'] else e['outcome'][m] for e in rows]) for m in metrics}
95
+ observed = {m: summary([e['outcome'][m] for e in rows]) for m in metrics}
96
+ failed = [e['seed'] for e in rows if e['errors']]
97
+ cap_count = sum(e['outcome']['cap_hit'] for e in rows)
98
+ events = [d for e in rows for d in e['decisions']]
99
+ computed = dict(episodes=200, failed_episodes=len(failed), error_rate=len(failed)/200,
100
+ invalid_decisions=sum(er['kind']=='invalid_decision' for e in rows for er in e['errors']),
101
+ cap_hit_rate=cap_count/200, failure_adjusted=primary, observed_before_error=observed,
102
+ latency_seconds=summary([d['decision_seconds'] for d in events]),
103
+ input_tokens=summary([d['input_tokens'] for d in events if d.get('input_tokens') is not None]),
104
+ failed_seeds=failed)
105
+ for comparison in ('trained_vs_base', 'trained_vs_heuristic', 'trained_vs_base_fp32', 'base_fp32_vs_base'):
106
+ stored = report[comparison]['players'][player]
107
+ for key, value in computed.items():
108
+ assert stored[key] == value, (comparison, player, key)
109
+ summaries[player] = {**computed, 'cap_count': cap_count}
110
+ print(f'{player}: all 200 saved replays/observations/decisions verified; mean lines={primary["lines"]["mean"]}', flush=True)
111
+ comparisons = {}
112
+ for key, left, right in [('trained_vs_base','trained','base'), ('trained_vs_heuristic','trained','heuristic'),
113
+ ('trained_vs_base_fp32','trained','base-fp32'), ('base_fp32_vs_base','base-fp32','base')]:
114
+ stored = report[key]
115
+ assert stored['trained_id'] == left and stored['base_id'] == right
116
+ assert stored['seeds'] == seeds and stored['matches_reserved_test_pool'] is True
117
+ assert stored['max_pieces'] == 200 and stored['selection_performed'] is False
118
+ assert stored['max_error_rate'] == 0
119
+ comparisons[key] = {}
120
+ for metric in metrics:
121
+ def score(p, seed):
122
+ e = indexed[p, seed]
123
+ return 0 if e['errors'] else e['outcome'][metric]
124
+ deltas = [score(left,s)-score(right,s) for s in seeds]
125
+ rng = random.Random(2026)
126
+ means = sorted(sum(rng.choice(deltas) for _ in seeds)/200 for _ in range(10000))
127
+ def percentile(q):
128
+ n = 9999*q
129
+ low = int(n)
130
+ return means[low] + (means[math.ceil(n)]-means[low])*(n-low)
131
+ expected = dict(episodes=200, mean_difference=statistics.mean(deltas), ci95_lower=percentile(.025),
132
+ ci95_upper=percentile(.975), method='paired episode percentile bootstrap',
133
+ bootstrap_samples=10000, bootstrap_seed=2026)
134
+ assert expected == stored['paired_trained_minus_base'][metric], (key, metric)
135
+ comparisons[key][metric] = expected
136
+ acceptable = not summaries[left]['failed_seeds'] and not summaries[right]['failed_seeds']
137
+ assert stored['errors_acceptable'] == acceptable
138
+ assert stored['positive_mean_lines_signal'] == (comparisons[key]['lines']['ci95_lower'] > 0 and acceptable)
139
+ print(f'{key}: all three paired metrics and bootstrap intervals independently match.', flush=True)
140
+ result = dict(status='passed', audit_script_sha256=sha(__file__), elapsed_seconds=time.monotonic()-start,
141
+ report_sha256=sha(report_path), request_sha256=sha('runs/final-evaluation/request.json'),
142
+ selection_sha256=sha(selection_path), checkpoint_sha256=checkpoint_hashes,
143
+ source_hashes=request['source_hashes'], episode_file_sha256=episode_hashes,
144
+ episodes_verified=1000, summaries=summaries, comparisons=comparisons,
145
+ method='Saved replay reconstruction; independent statistics and random.choice paired bootstrap. No new policy decisions/model loads.')
146
+ OUT.write_text(json.dumps(result, indent=2, allow_nan=False)+'\n')
147
+ print(json.dumps({k: result[k] for k in ('status','elapsed_seconds','report_sha256','selection_sha256','episodes_verified')}), flush=True)
148
+
149
+ ```
code/reports/final-audit.json ADDED
The diff for this file is too large to render. See raw diff
 
code/reports/final-study.md ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stackcraft: completed Clef fine-tuning study
2
+
3
+ Fine-tuning improved Clef's play in this small, turn-based block game, but a fixed
4
+ arithmetic heuristic remained much stronger and cheaper. The selected model
5
+ cleared **16.81 lines per game**, compared with **0.07** for the unchanged native
6
+ model and **76.53** for the heuristic. The paired trained-minus-native improvement
7
+ was **16.74 lines, 95% bootstrap interval [15.74, 17.77]**.
8
+
9
+ These are results from all **200 held-out piece sequences**, seeds 30000–30199,
10
+ with a 200-piece cap for each of five players: 1,000 completed episodes. There
11
+ were **zero inference errors and zero invalid decisions**. Training, selection,
12
+ reload verification, final evaluation and an independent computational audit are
13
+ complete. Publication and fresh-download verification are tracked separately in
14
+ the [source repository](https://github.com/kkarimi/stackcraft).
15
+
16
+ The [compact machine-readable report](final-summary.json) contains full-precision
17
+ summaries, all paired intervals, runtime configuration and evidence hashes.
18
+
19
+ ## What was tested
20
+
21
+ Stackcraft uses a 10×20 board, seven tetrominoes and deterministic seven-bag piece
22
+ sequences. A turn selects a legal rotation-and-column placement that drops
23
+ vertically. There is no hold, wall kick, tuck, T-spin scoring or real-time movement
24
+ deadline. Scores are 100/300/500/800 for clearing one/two/three/four lines in a move.
25
+ Each player receives the board, current piece, one next-piece preview and the same
26
+ legal options. Seed, sequence index and future random state are excluded from the
27
+ policy observation. This tests decisions over structured state, not visual play.
28
+
29
+ The neural players use `Cloudflare/clef-flash`, pinned to revision
30
+ `17f0b0ad64efb65d273590632833508766b2aae6`, with the same full observation encoding,
31
+ option order, BF16 backbone and 4,096-token limit. The three neural conditions are:
32
+
33
+ - **Native base:** unchanged weights and original BF16 decision head.
34
+ - **FP32 base:** unchanged weights, using the FP32 head wrapper also used in training.
35
+ - **Trained:** selected rank-4 LoRA adapters plus the learned FP32 native decision head.
36
+
37
+ **Random** chooses uniformly among legal options using its own recorded RNG.
38
+ **Heuristic** greedily scores each resulting board with weights frozen before final evaluation:
39
+ `−aggregate height − 4 × holes − bumpiness + 8 × cleared lines`. It does not use
40
+ the available preview. The lookahead teacher used for labeling is distinct from
41
+ this greedy heuristic and was not one of the five final tournament players.
42
+
43
+ ## Held-out game outcomes
44
+
45
+ Entries show **mean / median** across all 200 episodes for each player. Survival
46
+ means pieces placed. A cap hit means survival of *at least* 200 pieces, not death
47
+ at piece 200.
48
+
49
+ | Player | Lines | Score | Pieces survived | Cap hits | Errors / invalid decisions |
50
+ | --- | ---: | ---: | ---: | ---: | ---: |
51
+ | Native base | 0.070 / 0 | 7.0 / 0 | 26.020 / 26 | 0/200 | 0 / 0 |
52
+ | FP32 base | 0.105 / 0 | 10.5 / 0 | 26.000 / 26 | 0/200 | 0 / 0 |
53
+ | Trained | 16.810 / 16 | 1745.5 / 1650 | 83.565 / 82 | 0/200 | 0 / 0 |
54
+ | Random | 0.140 / 0 | 14.0 / 0 | 26.645 / 27 | 0/200 | 0 / 0 |
55
+ | Heuristic | 76.530 / 77 | 8306.5 / 8300 | 200.000 / 200 | 200/200 | 0 / 0 |
56
+
57
+ No failures were dropped. The frozen analysis assigns zero lines, score and pieces
58
+ to any errored episode and preserves the observed partial outcome separately.
59
+ Because none failed, failure-adjusted and observed summaries coincide here.
60
+
61
+ For each comparison, subtract the second player's outcome from the first on the
62
+ same seed. Resample the 200 paired differences with replacement 10,000 times,
63
+ using bootstrap seed 2026, and take the percentile 95% interval. Resampling whole
64
+ episodes preserves dependence among moves within a game.
65
+
66
+ | Comparison | Mean line difference | Paired 95% interval |
67
+ | --- | ---: | ---: |
68
+ | Trained − native base (primary) | +16.740 | [15.740, 17.770] |
69
+ | Trained − FP32 base | +16.705 | [15.705, 17.740] |
70
+ | Trained − heuristic | −59.720 | [−60.805, −58.619875] |
71
+ | FP32 base − native base | +0.035 | [−0.020, 0.090] |
72
+
73
+ The trained gain persists against the unchanged FP32 head. Head precision alone
74
+ does not explain that gain in this experiment. The FP32-versus-native interval
75
+ includes zero. This is not an ablation separating the effects of learned LoRA
76
+ from learned head weights: both were trained together.
77
+
78
+ The primary trained-minus-native score difference was +1738.5 [1633.0, 1848.5],
79
+ and survival difference was +57.545 pieces [54.930, 60.210]. All twelve intervals
80
+ (lines, score and pieces for all four comparisons) are preserved in
81
+ [final-summary.json](final-summary.json). Lines are the primary endpoint; score
82
+ and survival intervals are descriptive secondary results, without a multiplicity
83
+ correction. These intervals describe episode variability for one selected
84
+ checkpoint, not variability across training runs.
85
+
86
+ ## Data, training and checkpoint selection
87
+
88
+ The disclosed synthetic dataset contains **827 training positions and 215
89
+ validation positions**. Separate episode seed pools supplied mixed random,
90
+ heuristic and lookahead-expert trajectories, sampled for at most 40 moves per
91
+ episode. Occupancy-normalized deduplication removed six within-training duplicates
92
+ and two cross-split duplicates. The teacher enumerates placements of the current
93
+ piece and the single visible preview, scoring line clears and the resulting board;
94
+ it cannot inspect unseen pieces. Its labels are bounded-search recommendations,
95
+ not proven optimal moves. See the [dataset manifest](dataset-manifest.json) and
96
+ [Tutorial 03](../docs/tutorials/03-data.md).
97
+
98
+ One seed-42 training run produced exactly two preregistered epoch candidates.
99
+ Training used BF16 frozen backbone weights without quantization, rank-4/alpha-8
100
+ LoRA with zero dropout, and the full FP32 native joint head: 132,582,404 trainable
101
+ parameters. The fixed loss was cross-entropy with 0.05 label smoothing plus Brier
102
+ loss weighted 0.1. AdamW used learning rate 1e-5 and weight decay 0.01, with batch
103
+ size one, accumulation eight and gradient clipping at 1.0. The final accumulation
104
+ group was normalized by its actual three examples. Each epoch processed all 827
105
+ training positions in 104 optimizer steps. Frozen parameter hashes stayed unchanged.
106
+
107
+ Selection used the lowest finite mean target negative log likelihood (NLL) on
108
+ **all 215 validation positions**, with exact ties assigned to the earlier epoch.
109
+ Candidates with errors or incomplete probabilities were ineligible. Both were
110
+ eligible; epoch 02 won. Test games did not choose the checkpoint.
111
+
112
+ | Validation condition | Teacher agreement | Mean target NLL | Mean Brier |
113
+ | --- | ---: | ---: | ---: |
114
+ | Native base | 21/215 (9.77%) | 2.952702 | 0.923173 |
115
+ | FP32 base | 22/215 (10.23%) | 2.952393 | 0.923102 |
116
+ | Epoch 01 | 89/215 (41.40%) | 1.860870 | 0.722190 |
117
+ | Epoch 02, selected | 92/215 (42.79%) | 1.775923 | 0.708980 |
118
+
119
+ **42.79% teacher agreement is not 42.79% game quality.** It measures agreement
120
+ with one chosen teacher action, including its deterministic tie rule. Small errors
121
+ change subsequent states, and imitation can fail on boards absent from training.
122
+ Complete games provide a separate behavioral measurement.
123
+
124
+ The selected checkpoint reloaded in a fresh process with **0.0 maximum absolute
125
+ probability difference** on four fixed training-position references, below the
126
+ 1e-4 tolerance. This checks those reference distributions, not all possible boards
127
+ or downloaded publication artifacts. Full details are in the
128
+ [training](study-training.json), [validation](validation-summary.json) and
129
+ [reload](selected-checkpoint-reload.json) reports. The training report's status
130
+ reflects its creation before external validation; later evidence completes that
131
+ stage. The [archived preregistration](study-preregistration.md) preserves the
132
+ pre-test choices; it is a local source archive, not a signed external registration.
133
+
134
+ ## Runtime and practical cost
135
+
136
+ The workstation used an NVIDIA RTX 5090 (reported 32,607 MiB), AMD Ryzen 9 9950X3D,
137
+ eight Torch CPU threads and about 123 GiB host RAM. Runtime was Python 3.13.16,
138
+ Torch 2.14.1+cu130, CUDA 13.0, Transformers 5.18.0 and PEFT 0.21.2. TF32 was disabled.
139
+ The [hardware snapshot](evaluation-hardware.json) records the remaining details.
140
+
141
+ | Player | Mean decision ms | Median ms | p95 ms | Decisions | Local model initialization s |
142
+ | --- | ---: | ---: | ---: | ---: | ---: |
143
+ | Native base | 149.110 | 139.381 | 244.916 | 5,204 | 3.866 |
144
+ | FP32 base | 150.188 | 140.968 | 247.880 | 5,200 | 2.176 |
145
+ | Trained | 206.814 | 171.844 | 309.000 | 16,713 | 2.528 |
146
+ | Random | 0.001497 | 0.001250 | 0.002879 | 5,329 | 0.000035 |
147
+ | Heuristic | 0.198510 | 0.139489 | 0.280528 | 40,000 | 0.000002 |
148
+
149
+ Latencies cover all `Player.choose` calls, including encoding and probability
150
+ transfer, with no excluded warmup. They exclude rendering, replay serialization
151
+ and model loading. Initialization was measured separately with weights already
152
+ cached locally; these are not download or reliably disk-cold timings. The neural
153
+ players' mean input lengths were 1,354.30, 1,349.73 and 1,532.11 tokens respectively
154
+ (native, FP32, trained); the trained p95 was 2,259 tokens.
155
+
156
+ The trained model took about **1,042 times** the heuristic's mean decision time
157
+ while clearing far fewer lines. This is an observed workload comparison: neural
158
+ players use the GPU, the heuristic uses the CPU, and policies visit different
159
+ boards. It is not a controlled same-state kernel benchmark. For a practical game
160
+ bot under these rules, the heuristic is the clear choice. The fine-tuning result
161
+ is useful as a reproducible learning experiment, not a reason to replace the
162
+ heuristic in production.
163
+
164
+ Training took **1,511.25 seconds (25.2 minutes)** including checkpoint and hash
165
+ work; peak CUDA allocation was **23.23 GB (21.64 GiB)** and reservation 24.18 GB.
166
+ The final evaluation wrapper took **5,103.11 seconds (85.1 minutes)** including
167
+ loading, reporting and the local service stop/restore health checks. This was
168
+ within the pre-test multi-hour budget. These durations exclude earlier development,
169
+ validation and dataset generation. No energy consumption or monetary cost was
170
+ measured; the GPU's configured 575 W ceiling is not an energy-use measurement.
171
+
172
+ ## A fixed replay illustration
173
+
174
+ Seed **30000**, the first reserved seed and the declared demo sequence, provides
175
+ an exploratory illustration. These values come from saved replays; no additional
176
+ games were generated for this description.
177
+
178
+ | Player | Lines | Pieces | Final maximum height | Final holes |
179
+ | --- | ---: | ---: | ---: | ---: |
180
+ | Native base | 0 | 23 | 20 | 86 |
181
+ | FP32 base | 0 | 26 | 20 | 79 |
182
+ | Trained | 16 | 82 | 20 | 28 |
183
+ | Random | 0 | 24 | 20 | 88 |
184
+ | Heuristic | 78 | 200 (cap) | 3 | 1 |
185
+
186
+ A hole is an empty cell below an occupied cell in its column. These are different
187
+ endpoints: the heuristic is still alive at the cap, whereas the other players
188
+ have topped out. The trained player clears lines but eventually reaches the top.
189
+ The table describes this replay; it does not establish why the model fails or
190
+ prove a causal relationship between final holes and the aggregate result.
191
+
192
+ ## Reproducibility and remaining limits
193
+
194
+ The independently implemented CPU audit checked all 1,000 persisted episodes:
195
+ replayed states, observations, choices, outcomes, source/checkpoint/selection
196
+ bindings and aggregate statistics. It independently recomputed all twelve paired
197
+ intervals and matched the report. The [audit results](final-audit.json) and
198
+ [archived audit source](final-audit-source.md) preserve that check; its script hash
199
+ and compact outcome are also included in the summary. This is an independent code-path check by another agent,
200
+ not an external scientific replication.
201
+
202
+ The full evaluation JSON is identified by SHA-256
203
+ `5585574b77e81c8d7dd44464c4dfb09f9511ccec0d9150c3ea44793760416857`.
204
+ The frozen selection SHA-256 is
205
+ `025b62587a26bd8cc78c80f362524b8c178190df5188b317299121afa6b1203b`.
206
+ Dataset manifest, checkpoint files, evaluation source files and supporting report
207
+ hashes are all included in [final-summary.json](final-summary.json).
208
+
209
+ The prepared model-release layout contains `code/` with these reports, source,
210
+ locked environment, tests and tutorials; `evidence.zip` losslessly preserves
211
+ `evidence/evaluation/` with the full
212
+ report, request and per-player episode records; and `evidence/selection/` with
213
+ the frozen choice and both candidates' validation evidence. Extract the ZIP from
214
+ the model repository root to restore these exact paths; `evidence-files.json`
215
+ records their unchanged uncompressed hashes and sizes. These portable paths
216
+ are relative to the model repository root and do not require access to private Git
217
+ history. See the [model release](https://huggingface.co/nima1/stackcraft-clef-flash-lora),
218
+ [dataset](https://huggingface.co/datasets/nima1/stackcraft-data), and
219
+ [CPU demo](https://huggingface.co/spaces/nima1/stackcraft).
220
+ [Tutorial 05](../docs/tutorials/05-evaluation.md) gives the reproduction procedure.
221
+
222
+ The principal limitations are one training seed, a small synthetic dataset, an
223
+ approximate teacher, a single structured encoding and simplified rules. Training
224
+ positions cover shorter trajectories than the 200-piece test horizon. All 200
225
+ heuristic games reached the cap, so their uncapped longevity is unknown. The weak
226
+ unchanged-model results characterize this pinned model and encoding, not every
227
+ possible way to use Clef. No model, prompt, dataset, policy or checkpoint was tuned
228
+ after opening this final test. Future improvements require a new declared study
229
+ and fresh held-out sequences; these results remain the record of this experiment.
code/reports/final-summary.json ADDED
@@ -0,0 +1,1297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "checkpoint_sha256": {
3
+ "adapter/README.md": "bf1fa63446e8ee38c5ac154ade9138a0e2354e3215201f59473102eeb5206fa0",
4
+ "adapter/adapter_config.json": "bbbce7f19e186b4a79c39d03786bb190d6084e7990a601e8e28672cf1c969741",
5
+ "adapter/adapter_model.safetensors": "3ed169fe68b20a370bb856f9d91842543374ff3c98fd3bbe139ef2d0b97877a3",
6
+ "joint_head.safetensors": "4f21c0f315cf1bcf852a9887d158928855eaa9bdcab6b9770d518abf3727fa1b",
7
+ "reference.json": "adacc63684d28598f8016602bab790cd3967317986b6934dd94a5e14fabe571f",
8
+ "training_config.json": "3ead31d0a3ccb6a62914c3853ee26332f6412382c5196e40b461182e64d8b329"
9
+ },
10
+ "comparisons": {
11
+ "base_fp32_vs_base": {
12
+ "base_id": "base",
13
+ "errors_acceptable": true,
14
+ "paired_trained_minus_base": {
15
+ "lines": {
16
+ "bootstrap_samples": 10000,
17
+ "bootstrap_seed": 2026,
18
+ "ci95_lower": -0.02,
19
+ "ci95_upper": 0.09,
20
+ "episodes": 200,
21
+ "mean_difference": 0.035,
22
+ "method": "paired episode percentile bootstrap"
23
+ },
24
+ "pieces": {
25
+ "bootstrap_samples": 10000,
26
+ "bootstrap_seed": 2026,
27
+ "ci95_lower": -0.405,
28
+ "ci95_upper": 0.36,
29
+ "episodes": 200,
30
+ "mean_difference": -0.02,
31
+ "method": "paired episode percentile bootstrap"
32
+ },
33
+ "score": {
34
+ "bootstrap_samples": 10000,
35
+ "bootstrap_seed": 2026,
36
+ "ci95_lower": -2.0,
37
+ "ci95_upper": 9.0,
38
+ "episodes": 200,
39
+ "mean_difference": 3.5,
40
+ "method": "paired episode percentile bootstrap"
41
+ }
42
+ },
43
+ "positive_mean_lines_signal": false,
44
+ "trained_id": "base-fp32"
45
+ },
46
+ "trained_vs_base": {
47
+ "base_id": "base",
48
+ "errors_acceptable": true,
49
+ "paired_trained_minus_base": {
50
+ "lines": {
51
+ "bootstrap_samples": 10000,
52
+ "bootstrap_seed": 2026,
53
+ "ci95_lower": 15.74,
54
+ "ci95_upper": 17.77,
55
+ "episodes": 200,
56
+ "mean_difference": 16.74,
57
+ "method": "paired episode percentile bootstrap"
58
+ },
59
+ "pieces": {
60
+ "bootstrap_samples": 10000,
61
+ "bootstrap_seed": 2026,
62
+ "ci95_lower": 54.93,
63
+ "ci95_upper": 60.21,
64
+ "episodes": 200,
65
+ "mean_difference": 57.545,
66
+ "method": "paired episode percentile bootstrap"
67
+ },
68
+ "score": {
69
+ "bootstrap_samples": 10000,
70
+ "bootstrap_seed": 2026,
71
+ "ci95_lower": 1633.0,
72
+ "ci95_upper": 1848.5,
73
+ "episodes": 200,
74
+ "mean_difference": 1738.5,
75
+ "method": "paired episode percentile bootstrap"
76
+ }
77
+ },
78
+ "positive_mean_lines_signal": true,
79
+ "trained_id": "trained"
80
+ },
81
+ "trained_vs_base_fp32": {
82
+ "base_id": "base-fp32",
83
+ "errors_acceptable": true,
84
+ "paired_trained_minus_base": {
85
+ "lines": {
86
+ "bootstrap_samples": 10000,
87
+ "bootstrap_seed": 2026,
88
+ "ci95_lower": 15.705,
89
+ "ci95_upper": 17.74,
90
+ "episodes": 200,
91
+ "mean_difference": 16.705,
92
+ "method": "paired episode percentile bootstrap"
93
+ },
94
+ "pieces": {
95
+ "bootstrap_samples": 10000,
96
+ "bootstrap_seed": 2026,
97
+ "ci95_lower": 55.01,
98
+ "ci95_upper": 60.185,
99
+ "episodes": 200,
100
+ "mean_difference": 57.565,
101
+ "method": "paired episode percentile bootstrap"
102
+ },
103
+ "score": {
104
+ "bootstrap_samples": 10000,
105
+ "bootstrap_seed": 2026,
106
+ "ci95_lower": 1629.4875,
107
+ "ci95_upper": 1845.0,
108
+ "episodes": 200,
109
+ "mean_difference": 1735,
110
+ "method": "paired episode percentile bootstrap"
111
+ }
112
+ },
113
+ "positive_mean_lines_signal": true,
114
+ "trained_id": "trained"
115
+ },
116
+ "trained_vs_heuristic": {
117
+ "base_id": "heuristic",
118
+ "errors_acceptable": true,
119
+ "paired_trained_minus_base": {
120
+ "lines": {
121
+ "bootstrap_samples": 10000,
122
+ "bootstrap_seed": 2026,
123
+ "ci95_lower": -60.805,
124
+ "ci95_upper": -58.619875,
125
+ "episodes": 200,
126
+ "mean_difference": -59.72,
127
+ "method": "paired episode percentile bootstrap"
128
+ },
129
+ "pieces": {
130
+ "bootstrap_samples": 10000,
131
+ "bootstrap_seed": 2026,
132
+ "ci95_lower": -118.925125,
133
+ "ci95_upper": -113.86987500000001,
134
+ "episodes": 200,
135
+ "mean_difference": -116.435,
136
+ "method": "paired episode percentile bootstrap"
137
+ },
138
+ "score": {
139
+ "bootstrap_samples": 10000,
140
+ "bootstrap_seed": 2026,
141
+ "ci95_lower": -6681.5125,
142
+ "ci95_upper": -6436.0,
143
+ "episodes": 200,
144
+ "mean_difference": -6561,
145
+ "method": "paired episode percentile bootstrap"
146
+ }
147
+ },
148
+ "positive_mean_lines_signal": false,
149
+ "trained_id": "trained"
150
+ }
151
+ },
152
+ "conclusion": "Selected training checkpoint improves mean lines over both unchanged Clef references; fixed heuristic is much stronger and cheaper.",
153
+ "evidence": {
154
+ "audit_source": {
155
+ "release_path": "code/reports/final-audit-source.md",
156
+ "sha256": "61db8b121130ffd2eeb0a9c667f53f0c21647e865e93d44fbb9967e71d870020",
157
+ "size_bytes": 8166
158
+ },
159
+ "dataset": {
160
+ "release_path": "code/reports/dataset-manifest.json",
161
+ "sha256": "f591ec4c4b853942bd0a89331280b8c3491b3eb87f0b2a69ae4821b9623bd936",
162
+ "size_bytes": 8175
163
+ },
164
+ "evaluation": {
165
+ "release_path": "evidence/evaluation/report.json",
166
+ "sha256": "5585574b77e81c8d7dd44464c4dfb09f9511ccec0d9150c3ea44793760416857",
167
+ "size_bytes": 1225235921
168
+ },
169
+ "hardware": {
170
+ "release_path": "code/reports/evaluation-hardware.json",
171
+ "sha256": "cea05c0d6f208dbeae498be743d131f645cebe88a5054a8af5a884714582085b",
172
+ "size_bytes": 1606
173
+ },
174
+ "independent_audit": {
175
+ "release_path": "code/reports/final-audit.json",
176
+ "sha256": "4766d0224a8bddc8c6e6fe0e86e75937f65abdb261667d5d59bd3d482c8adb89",
177
+ "size_bytes": 133571
178
+ },
179
+ "preregistration": {
180
+ "release_path": "code/reports/study-preregistration.md",
181
+ "sha256": "bd6d34b4e71a2e38f4dcdc4a0b0a1aca7f7572e5d063af6b3e8491c6c6382d96",
182
+ "size_bytes": 4183
183
+ },
184
+ "reload": {
185
+ "release_path": "code/reports/selected-checkpoint-reload.json",
186
+ "sha256": "57489d34b22627c0fd0e3dc42f7f11231a901c059db7a4ea8cbab0aba2c37cdd",
187
+ "size_bytes": 630
188
+ },
189
+ "request": {
190
+ "release_path": "evidence/evaluation/request.json",
191
+ "sha256": "a507e3b947d603b02f56b6df12b160e9e182ee5706d2ac64d8e9f93358f79e62",
192
+ "size_bytes": 4699
193
+ },
194
+ "selection": {
195
+ "release_path": "evidence/selection/selection.json",
196
+ "sha256": "025b62587a26bd8cc78c80f362524b8c178190df5188b317299121afa6b1203b",
197
+ "size_bytes": 8037
198
+ },
199
+ "training": {
200
+ "release_path": "code/reports/study-training.json",
201
+ "sha256": "c272f9e1dedfedf285c0d7d1a46aa77a89d48019bf621e31a11921f38fc05600",
202
+ "size_bytes": 63791
203
+ },
204
+ "validation": {
205
+ "release_path": "code/reports/validation-summary.json",
206
+ "sha256": "2fd17daeeb12dfecf739c2dcd885e80ca3dc4547acaa3d22722ac9a4d747122a",
207
+ "size_bytes": 7990
208
+ }
209
+ },
210
+ "evidence_path_convention": "Paths are relative to the model release root; source-only copies retain compact reports under reports/. Publication is pending at report creation.",
211
+ "hardware": {
212
+ "cpu_model": "AMD Ryzen 9 9950X3D 16-Core Processor",
213
+ "evaluation_request_sha256": "a507e3b947d603b02f56b6df12b160e9e182ee5706d2ac64d8e9f93358f79e62",
214
+ "gpus": [
215
+ {
216
+ "configured_power_limit_watts": 575.0,
217
+ "driver_version": "610.57.04",
218
+ "name": "NVIDIA GeForce RTX 5090",
219
+ "reported_total_memory_mib": 32607
220
+ }
221
+ ],
222
+ "host_reported_memory_kib": 129391464,
223
+ "kernel": "7.2.5-3-omarchy",
224
+ "logical_cpu_count": 32,
225
+ "observed_at_utc": "2026-10-05T21:20:14.982799+00:00",
226
+ "python_version": "3.13.16",
227
+ "schema_version": 1,
228
+ "scope": "Read-only hardware snapshot during final evaluation; no energy usage or continuous clock/temperature measurement",
229
+ "trained_runtime_config": {
230
+ "base_revision": "17f0b0ad64efb65d273590632833508766b2aae6",
231
+ "cpu_threads": 8,
232
+ "cuda_matmul_allow_tf32": false,
233
+ "cuda_version": "13.0",
234
+ "device": "cuda:0",
235
+ "dtype": "torch.bfloat16",
236
+ "encoding_version": "stackcraft-clef-v1",
237
+ "evaluation_head": "fp32-gathered-rows",
238
+ "float32_matmul_precision": "highest",
239
+ "head_dtype": "float32",
240
+ "max_length": 4096,
241
+ "model_id": "Cloudflare/clef-flash",
242
+ "revision": "Cloudflare/clef-flash@17f0b0ad64efb65d273590632833508766b2aae6:checkpoint-sha256-b8133e9775a82c6b952fc1a1b1c805c5bc993fbcd919e81f663ad35e122ab7c7:stackcraft-clef-v1",
243
+ "source_sha256": "0e304cf7c6500e8bb59bef7e2afd2c6373f82596dfb3b57d1aa93c175e2dc3a3",
244
+ "versions": {
245
+ "huggingface-hub": "1.33.0",
246
+ "peft": "0.21.2",
247
+ "safetensors": "0.8.0",
248
+ "torch": "2.14.1+cu130",
249
+ "transformers": "5.18.0"
250
+ }
251
+ }
252
+ },
253
+ "illustration": {
254
+ "players": {
255
+ "base": {
256
+ "evidence": {
257
+ "release_path": "evidence/evaluation/base/seed-30000.json",
258
+ "sha256": "429e52aa67bbdae6cd35f469af876b9b7872d61e4b862cb2ea47f40e7ef918d4",
259
+ "size_bytes": 325003
260
+ },
261
+ "final_board": {
262
+ "aggregate_height": 178,
263
+ "holes": 86,
264
+ "max_height": 20
265
+ },
266
+ "outcome": {
267
+ "cap_hit": false,
268
+ "lines": 0,
269
+ "pieces": 23,
270
+ "score": 0,
271
+ "status": "top_out",
272
+ "terminal": true
273
+ }
274
+ },
275
+ "base-fp32": {
276
+ "evidence": {
277
+ "release_path": "evidence/evaluation/base-fp32/seed-30000.json",
278
+ "sha256": "87744a7184c3ffbe1a4e7b8e5b526f628993de4135a551117de2c2283c105c34",
279
+ "size_bytes": 335159
280
+ },
281
+ "final_board": {
282
+ "aggregate_height": 183,
283
+ "holes": 79,
284
+ "max_height": 20
285
+ },
286
+ "outcome": {
287
+ "cap_hit": false,
288
+ "lines": 0,
289
+ "pieces": 26,
290
+ "score": 0,
291
+ "status": "top_out",
292
+ "terminal": true
293
+ }
294
+ },
295
+ "heuristic": {
296
+ "evidence": {
297
+ "release_path": "evidence/evaluation/heuristic/seed-30000.json",
298
+ "sha256": "5b7508b7a345998a7c677a5153779ca9d46bfd65ada64371c7117f54c6537c09",
299
+ "size_bytes": 2800853
300
+ },
301
+ "final_board": {
302
+ "aggregate_height": 21,
303
+ "holes": 1,
304
+ "max_height": 3
305
+ },
306
+ "outcome": {
307
+ "cap_hit": true,
308
+ "lines": 78,
309
+ "pieces": 200,
310
+ "score": 8800,
311
+ "status": "cap",
312
+ "terminal": false
313
+ }
314
+ },
315
+ "random": {
316
+ "evidence": {
317
+ "release_path": "evidence/evaluation/random/seed-30000.json",
318
+ "sha256": "f39dc524807bb0008a9bbe2cd004e152fdb939405d1e50879c81da2942c7a49e",
319
+ "size_bytes": 318599
320
+ },
321
+ "final_board": {
322
+ "aggregate_height": 184,
323
+ "holes": 88,
324
+ "max_height": 20
325
+ },
326
+ "outcome": {
327
+ "cap_hit": false,
328
+ "lines": 0,
329
+ "pieces": 24,
330
+ "score": 0,
331
+ "status": "top_out",
332
+ "terminal": true
333
+ }
334
+ },
335
+ "trained": {
336
+ "evidence": {
337
+ "release_path": "evidence/evaluation/trained/seed-30000.json",
338
+ "sha256": "d18bd7010da5c2d98ef02a73585a3cac0a47764a43a597c1d1b2d1280949890c",
339
+ "size_bytes": 1156045
340
+ },
341
+ "final_board": {
342
+ "aggregate_height": 196,
343
+ "holes": 28,
344
+ "max_height": 20
345
+ },
346
+ "outcome": {
347
+ "cap_hit": false,
348
+ "lines": 16,
349
+ "pieces": 82,
350
+ "score": 1600,
351
+ "status": "top_out",
352
+ "terminal": true
353
+ }
354
+ }
355
+ },
356
+ "scope": "Exploratory description of the preselected replay seed, not a causal explanation or extra experiment. Endpoints differ in turn and terminal status. Holes are empty cells below an occupied cell in their column.",
357
+ "seed": 30000
358
+ },
359
+ "independent_audit": {
360
+ "audit_script_sha256": "867eb41c8319c7c7b428a037b9c8a4f5a5e9c235a15631fae6716aed565626d1",
361
+ "elapsed_seconds": 63.74646008000127,
362
+ "episodes_verified": 1000,
363
+ "method": "Saved replay reconstruction; independent statistics and random.choice paired bootstrap. No new policy decisions/model loads.",
364
+ "report_sha256": "5585574b77e81c8d7dd44464c4dfb09f9511ccec0d9150c3ea44793760416857",
365
+ "request_sha256": "a507e3b947d603b02f56b6df12b160e9e182ee5706d2ac64d8e9f93358f79e62",
366
+ "selection_sha256": "025b62587a26bd8cc78c80f362524b8c178190df5188b317299121afa6b1203b",
367
+ "status": "passed"
368
+ },
369
+ "limitations": [
370
+ "One training seed; confidence intervals do not include training-seed variability.",
371
+ "827 synthetic training positions and 215 validation positions; teacher search is bounded and not guaranteed optimal.",
372
+ "Simplified deterministic turn-based rules, structured board input and one-piece preview, not real-time Tetris.",
373
+ "All heuristic episodes reach the cap; uncapped survival is unknown.",
374
+ "Teacher agreement is not a game-quality metric.",
375
+ "Different policy states and GPU/CPU execution make latency a workload measurement rather than a controlled same-input kernel benchmark.",
376
+ "No checkpoint, prompt, dataset or policy tuning after opening this final test. A future experiment needs new held-out seeds."
377
+ ],
378
+ "players": {
379
+ "base": {
380
+ "cap_hit_rate": 0,
381
+ "cap_hits": 0,
382
+ "episodes": 200,
383
+ "error_rate": 0.0,
384
+ "failed_episodes": 0,
385
+ "failed_seeds": [],
386
+ "failure_adjusted": {
387
+ "lines": {
388
+ "count": 200,
389
+ "mean": 0.07,
390
+ "median": 0.0,
391
+ "p95": 1
392
+ },
393
+ "pieces": {
394
+ "count": 200,
395
+ "mean": 26.02,
396
+ "median": 26.0,
397
+ "p95": 30
398
+ },
399
+ "score": {
400
+ "count": 200,
401
+ "mean": 7,
402
+ "median": 0.0,
403
+ "p95": 100
404
+ }
405
+ },
406
+ "identity": {
407
+ "name": "base",
408
+ "revision": "Cloudflare/clef-flash@17f0b0ad64efb65d273590632833508766b2aae6:stackcraft-clef-v1",
409
+ "runtime_config": {
410
+ "base_revision": "17f0b0ad64efb65d273590632833508766b2aae6",
411
+ "cpu_threads": 8,
412
+ "cuda_matmul_allow_tf32": false,
413
+ "cuda_version": "13.0",
414
+ "device": "cuda:0",
415
+ "dtype": "torch.bfloat16",
416
+ "encoding_version": "stackcraft-clef-v1",
417
+ "evaluation_head": "native",
418
+ "float32_matmul_precision": "highest",
419
+ "head_dtype": "bfloat16",
420
+ "max_length": 4096,
421
+ "model_id": "Cloudflare/clef-flash",
422
+ "revision": "Cloudflare/clef-flash@17f0b0ad64efb65d273590632833508766b2aae6:stackcraft-clef-v1",
423
+ "source_sha256": "0e304cf7c6500e8bb59bef7e2afd2c6373f82596dfb3b57d1aa93c175e2dc3a3",
424
+ "versions": {
425
+ "huggingface-hub": "1.33.0",
426
+ "peft": "0.21.2",
427
+ "safetensors": "0.8.0",
428
+ "torch": "2.14.1+cu130",
429
+ "transformers": "5.18.0"
430
+ }
431
+ }
432
+ },
433
+ "input_tokens": {
434
+ "count": 5204,
435
+ "mean": 1354.2999615680246,
436
+ "median": 1286.0,
437
+ "p95": 2258
438
+ },
439
+ "invalid_decisions": 0,
440
+ "latency_seconds": {
441
+ "count": 5204,
442
+ "mean": 0.149109957484823,
443
+ "median": 0.1393812735186657,
444
+ "p95": 0.2449162660050206
445
+ },
446
+ "load_seconds": [
447
+ 3.866232068015961
448
+ ],
449
+ "observed_before_error": {
450
+ "lines": {
451
+ "count": 200,
452
+ "mean": 0.07,
453
+ "median": 0.0,
454
+ "p95": 1
455
+ },
456
+ "pieces": {
457
+ "count": 200,
458
+ "mean": 26.02,
459
+ "median": 26.0,
460
+ "p95": 30
461
+ },
462
+ "score": {
463
+ "count": 200,
464
+ "mean": 7,
465
+ "median": 0.0,
466
+ "p95": 100
467
+ }
468
+ }
469
+ },
470
+ "base-fp32": {
471
+ "cap_hit_rate": 0,
472
+ "cap_hits": 0,
473
+ "episodes": 200,
474
+ "error_rate": 0.0,
475
+ "failed_episodes": 0,
476
+ "failed_seeds": [],
477
+ "failure_adjusted": {
478
+ "lines": {
479
+ "count": 200,
480
+ "mean": 0.105,
481
+ "median": 0.0,
482
+ "p95": 1
483
+ },
484
+ "pieces": {
485
+ "count": 200,
486
+ "mean": 26,
487
+ "median": 26.0,
488
+ "p95": 31
489
+ },
490
+ "score": {
491
+ "count": 200,
492
+ "mean": 10.5,
493
+ "median": 0.0,
494
+ "p95": 100
495
+ }
496
+ },
497
+ "identity": {
498
+ "name": "base-fp32",
499
+ "revision": "Cloudflare/clef-flash@17f0b0ad64efb65d273590632833508766b2aae6:stackcraft-clef-v1:head-fp32-ablation",
500
+ "runtime_config": {
501
+ "base_revision": "17f0b0ad64efb65d273590632833508766b2aae6",
502
+ "cpu_threads": 8,
503
+ "cuda_matmul_allow_tf32": false,
504
+ "cuda_version": "13.0",
505
+ "device": "cuda:0",
506
+ "dtype": "torch.bfloat16",
507
+ "encoding_version": "stackcraft-clef-v1",
508
+ "evaluation_head": "fp32-gathered-rows",
509
+ "float32_matmul_precision": "highest",
510
+ "head_dtype": "float32",
511
+ "max_length": 4096,
512
+ "model_id": "Cloudflare/clef-flash",
513
+ "revision": "Cloudflare/clef-flash@17f0b0ad64efb65d273590632833508766b2aae6:stackcraft-clef-v1:head-fp32-ablation",
514
+ "source_sha256": "0e304cf7c6500e8bb59bef7e2afd2c6373f82596dfb3b57d1aa93c175e2dc3a3",
515
+ "versions": {
516
+ "huggingface-hub": "1.33.0",
517
+ "peft": "0.21.2",
518
+ "safetensors": "0.8.0",
519
+ "torch": "2.14.1+cu130",
520
+ "transformers": "5.18.0"
521
+ }
522
+ }
523
+ },
524
+ "input_tokens": {
525
+ "count": 5200,
526
+ "mean": 1349.7273076923077,
527
+ "median": 1286.0,
528
+ "p95": 2258
529
+ },
530
+ "invalid_decisions": 0,
531
+ "latency_seconds": {
532
+ "count": 5200,
533
+ "mean": 0.1501883656640264,
534
+ "median": 0.14096801349660382,
535
+ "p95": 0.24787959101377055
536
+ },
537
+ "load_seconds": [
538
+ 2.1760330779943615
539
+ ],
540
+ "observed_before_error": {
541
+ "lines": {
542
+ "count": 200,
543
+ "mean": 0.105,
544
+ "median": 0.0,
545
+ "p95": 1
546
+ },
547
+ "pieces": {
548
+ "count": 200,
549
+ "mean": 26,
550
+ "median": 26.0,
551
+ "p95": 31
552
+ },
553
+ "score": {
554
+ "count": 200,
555
+ "mean": 10.5,
556
+ "median": 0.0,
557
+ "p95": 100
558
+ }
559
+ }
560
+ },
561
+ "heuristic": {
562
+ "cap_hit_rate": 1,
563
+ "cap_hits": 200,
564
+ "episodes": 200,
565
+ "error_rate": 0.0,
566
+ "failed_episodes": 0,
567
+ "failed_seeds": [],
568
+ "failure_adjusted": {
569
+ "lines": {
570
+ "count": 200,
571
+ "mean": 76.53,
572
+ "median": 77.0,
573
+ "p95": 79
574
+ },
575
+ "pieces": {
576
+ "count": 200,
577
+ "mean": 200,
578
+ "median": 200.0,
579
+ "p95": 200
580
+ },
581
+ "score": {
582
+ "count": 200,
583
+ "mean": 8306.5,
584
+ "median": 8300.0,
585
+ "p95": 8800
586
+ }
587
+ },
588
+ "identity": {
589
+ "name": "heuristic",
590
+ "revision": "heuristic-v1-height1-holes4-bump1-lines8",
591
+ "runtime_config": {}
592
+ },
593
+ "input_tokens": {
594
+ "count": 0,
595
+ "mean": null,
596
+ "median": null,
597
+ "p95": null
598
+ },
599
+ "invalid_decisions": 0,
600
+ "latency_seconds": {
601
+ "count": 40000,
602
+ "mean": 0.00019850957853268482,
603
+ "median": 0.00013948898413218558,
604
+ "p95": 0.00028052800917066634
605
+ },
606
+ "load_seconds": [
607
+ 2.1799933165311813e-06
608
+ ],
609
+ "observed_before_error": {
610
+ "lines": {
611
+ "count": 200,
612
+ "mean": 76.53,
613
+ "median": 77.0,
614
+ "p95": 79
615
+ },
616
+ "pieces": {
617
+ "count": 200,
618
+ "mean": 200,
619
+ "median": 200.0,
620
+ "p95": 200
621
+ },
622
+ "score": {
623
+ "count": 200,
624
+ "mean": 8306.5,
625
+ "median": 8300.0,
626
+ "p95": 8800
627
+ }
628
+ }
629
+ },
630
+ "random": {
631
+ "cap_hit_rate": 0,
632
+ "cap_hits": 0,
633
+ "episodes": 200,
634
+ "error_rate": 0.0,
635
+ "failed_episodes": 0,
636
+ "failed_seeds": [],
637
+ "failure_adjusted": {
638
+ "lines": {
639
+ "count": 200,
640
+ "mean": 0.14,
641
+ "median": 0.0,
642
+ "p95": 1
643
+ },
644
+ "pieces": {
645
+ "count": 200,
646
+ "mean": 26.645,
647
+ "median": 27.0,
648
+ "p95": 31
649
+ },
650
+ "score": {
651
+ "count": 200,
652
+ "mean": 14,
653
+ "median": 0.0,
654
+ "p95": 100
655
+ }
656
+ },
657
+ "identity": {
658
+ "name": "random",
659
+ "revision": "random-v1-rng-0",
660
+ "runtime_config": {}
661
+ },
662
+ "input_tokens": {
663
+ "count": 0,
664
+ "mean": null,
665
+ "median": null,
666
+ "p95": null
667
+ },
668
+ "invalid_decisions": 0,
669
+ "latency_seconds": {
670
+ "count": 5329,
671
+ "mean": 1.4965126650845871e-06,
672
+ "median": 1.2499804142862558e-06,
673
+ "p95": 2.8790091164410114e-06
674
+ },
675
+ "load_seconds": [
676
+ 3.4689990570768714e-05
677
+ ],
678
+ "observed_before_error": {
679
+ "lines": {
680
+ "count": 200,
681
+ "mean": 0.14,
682
+ "median": 0.0,
683
+ "p95": 1
684
+ },
685
+ "pieces": {
686
+ "count": 200,
687
+ "mean": 26.645,
688
+ "median": 27.0,
689
+ "p95": 31
690
+ },
691
+ "score": {
692
+ "count": 200,
693
+ "mean": 14,
694
+ "median": 0.0,
695
+ "p95": 100
696
+ }
697
+ }
698
+ },
699
+ "trained": {
700
+ "cap_hit_rate": 0,
701
+ "cap_hits": 0,
702
+ "episodes": 200,
703
+ "error_rate": 0.0,
704
+ "failed_episodes": 0,
705
+ "failed_seeds": [],
706
+ "failure_adjusted": {
707
+ "lines": {
708
+ "count": 200,
709
+ "mean": 16.81,
710
+ "median": 16.0,
711
+ "p95": 29
712
+ },
713
+ "pieces": {
714
+ "count": 200,
715
+ "mean": 83.565,
716
+ "median": 82.0,
717
+ "p95": 114
718
+ },
719
+ "score": {
720
+ "count": 200,
721
+ "mean": 1745.5,
722
+ "median": 1650.0,
723
+ "p95": 3100
724
+ }
725
+ },
726
+ "identity": {
727
+ "name": "trained",
728
+ "revision": "Cloudflare/clef-flash@17f0b0ad64efb65d273590632833508766b2aae6:checkpoint-sha256-b8133e9775a82c6b952fc1a1b1c805c5bc993fbcd919e81f663ad35e122ab7c7:stackcraft-clef-v1",
729
+ "runtime_config": {
730
+ "base_revision": "17f0b0ad64efb65d273590632833508766b2aae6",
731
+ "cpu_threads": 8,
732
+ "cuda_matmul_allow_tf32": false,
733
+ "cuda_version": "13.0",
734
+ "device": "cuda:0",
735
+ "dtype": "torch.bfloat16",
736
+ "encoding_version": "stackcraft-clef-v1",
737
+ "evaluation_head": "fp32-gathered-rows",
738
+ "float32_matmul_precision": "highest",
739
+ "head_dtype": "float32",
740
+ "max_length": 4096,
741
+ "model_id": "Cloudflare/clef-flash",
742
+ "revision": "Cloudflare/clef-flash@17f0b0ad64efb65d273590632833508766b2aae6:checkpoint-sha256-b8133e9775a82c6b952fc1a1b1c805c5bc993fbcd919e81f663ad35e122ab7c7:stackcraft-clef-v1",
743
+ "source_sha256": "0e304cf7c6500e8bb59bef7e2afd2c6373f82596dfb3b57d1aa93c175e2dc3a3",
744
+ "versions": {
745
+ "huggingface-hub": "1.33.0",
746
+ "peft": "0.21.2",
747
+ "safetensors": "0.8.0",
748
+ "torch": "2.14.1+cu130",
749
+ "transformers": "5.18.0"
750
+ }
751
+ }
752
+ },
753
+ "input_tokens": {
754
+ "count": 16713,
755
+ "mean": 1532.1127864536588,
756
+ "median": 1304,
757
+ "p95": 2259
758
+ },
759
+ "invalid_decisions": 0,
760
+ "latency_seconds": {
761
+ "count": 16713,
762
+ "mean": 0.20681440101212237,
763
+ "median": 0.17184428698965348,
764
+ "p95": 0.3090000880183652
765
+ },
766
+ "load_seconds": [
767
+ 2.5281899149995297
768
+ ],
769
+ "observed_before_error": {
770
+ "lines": {
771
+ "count": 200,
772
+ "mean": 16.81,
773
+ "median": 16.0,
774
+ "p95": 29
775
+ },
776
+ "pieces": {
777
+ "count": 200,
778
+ "mean": 83.565,
779
+ "median": 82.0,
780
+ "p95": 114
781
+ },
782
+ "score": {
783
+ "count": 200,
784
+ "mean": 1745.5,
785
+ "median": 1650.0,
786
+ "p95": 3100
787
+ }
788
+ }
789
+ }
790
+ },
791
+ "protocol": {
792
+ "bootstrap_samples": 10000,
793
+ "bootstrap_seed": 2026,
794
+ "checkpoint_selection": "lowest finite validation NLL over all 215 positions; exact tie selects earlier of epoch-01 and epoch-02",
795
+ "ci_method": "paired episode percentile bootstrap",
796
+ "episodes_per_player": 200,
797
+ "failure_policy": "zero lines/score/pieces on errors; retain all pairs",
798
+ "final_test": true,
799
+ "max_error_rate": 0.0,
800
+ "max_pieces": 200,
801
+ "primary_metric": "lines",
802
+ "rules_version": "stackcraft-v1",
803
+ "secondary_metrics": [
804
+ "score",
805
+ "pieces"
806
+ ],
807
+ "seeds": [
808
+ 30000,
809
+ 30001,
810
+ 30002,
811
+ 30003,
812
+ 30004,
813
+ 30005,
814
+ 30006,
815
+ 30007,
816
+ 30008,
817
+ 30009,
818
+ 30010,
819
+ 30011,
820
+ 30012,
821
+ 30013,
822
+ 30014,
823
+ 30015,
824
+ 30016,
825
+ 30017,
826
+ 30018,
827
+ 30019,
828
+ 30020,
829
+ 30021,
830
+ 30022,
831
+ 30023,
832
+ 30024,
833
+ 30025,
834
+ 30026,
835
+ 30027,
836
+ 30028,
837
+ 30029,
838
+ 30030,
839
+ 30031,
840
+ 30032,
841
+ 30033,
842
+ 30034,
843
+ 30035,
844
+ 30036,
845
+ 30037,
846
+ 30038,
847
+ 30039,
848
+ 30040,
849
+ 30041,
850
+ 30042,
851
+ 30043,
852
+ 30044,
853
+ 30045,
854
+ 30046,
855
+ 30047,
856
+ 30048,
857
+ 30049,
858
+ 30050,
859
+ 30051,
860
+ 30052,
861
+ 30053,
862
+ 30054,
863
+ 30055,
864
+ 30056,
865
+ 30057,
866
+ 30058,
867
+ 30059,
868
+ 30060,
869
+ 30061,
870
+ 30062,
871
+ 30063,
872
+ 30064,
873
+ 30065,
874
+ 30066,
875
+ 30067,
876
+ 30068,
877
+ 30069,
878
+ 30070,
879
+ 30071,
880
+ 30072,
881
+ 30073,
882
+ 30074,
883
+ 30075,
884
+ 30076,
885
+ 30077,
886
+ 30078,
887
+ 30079,
888
+ 30080,
889
+ 30081,
890
+ 30082,
891
+ 30083,
892
+ 30084,
893
+ 30085,
894
+ 30086,
895
+ 30087,
896
+ 30088,
897
+ 30089,
898
+ 30090,
899
+ 30091,
900
+ 30092,
901
+ 30093,
902
+ 30094,
903
+ 30095,
904
+ 30096,
905
+ 30097,
906
+ 30098,
907
+ 30099,
908
+ 30100,
909
+ 30101,
910
+ 30102,
911
+ 30103,
912
+ 30104,
913
+ 30105,
914
+ 30106,
915
+ 30107,
916
+ 30108,
917
+ 30109,
918
+ 30110,
919
+ 30111,
920
+ 30112,
921
+ 30113,
922
+ 30114,
923
+ 30115,
924
+ 30116,
925
+ 30117,
926
+ 30118,
927
+ 30119,
928
+ 30120,
929
+ 30121,
930
+ 30122,
931
+ 30123,
932
+ 30124,
933
+ 30125,
934
+ 30126,
935
+ 30127,
936
+ 30128,
937
+ 30129,
938
+ 30130,
939
+ 30131,
940
+ 30132,
941
+ 30133,
942
+ 30134,
943
+ 30135,
944
+ 30136,
945
+ 30137,
946
+ 30138,
947
+ 30139,
948
+ 30140,
949
+ 30141,
950
+ 30142,
951
+ 30143,
952
+ 30144,
953
+ 30145,
954
+ 30146,
955
+ 30147,
956
+ 30148,
957
+ 30149,
958
+ 30150,
959
+ 30151,
960
+ 30152,
961
+ 30153,
962
+ 30154,
963
+ 30155,
964
+ 30156,
965
+ 30157,
966
+ 30158,
967
+ 30159,
968
+ 30160,
969
+ 30161,
970
+ 30162,
971
+ 30163,
972
+ 30164,
973
+ 30165,
974
+ 30166,
975
+ 30167,
976
+ 30168,
977
+ 30169,
978
+ 30170,
979
+ 30171,
980
+ 30172,
981
+ 30173,
982
+ 30174,
983
+ 30175,
984
+ 30176,
985
+ 30177,
986
+ 30178,
987
+ 30179,
988
+ 30180,
989
+ 30181,
990
+ 30182,
991
+ 30183,
992
+ 30184,
993
+ 30185,
994
+ 30186,
995
+ 30187,
996
+ 30188,
997
+ 30189,
998
+ 30190,
999
+ 30191,
1000
+ 30192,
1001
+ 30193,
1002
+ 30194,
1003
+ 30195,
1004
+ 30196,
1005
+ 30197,
1006
+ 30198,
1007
+ 30199
1008
+ ],
1009
+ "total_episodes": 1000,
1010
+ "training_seeds": [
1011
+ 42
1012
+ ]
1013
+ },
1014
+ "provenance": {
1015
+ "base_revision": "17f0b0ad64efb65d273590632833508766b2aae6",
1016
+ "encoding_version": "stackcraft-clef-v1",
1017
+ "installed_versions": {
1018
+ "huggingface-hub": "1.33.0",
1019
+ "peft": "0.21.2",
1020
+ "safetensors": "0.8.0",
1021
+ "torch": "2.14.1+cu130",
1022
+ "transformers": "5.18.0"
1023
+ },
1024
+ "precision_policy": "native BF16 base; FP32 trained head; optional base-fp32 ablation",
1025
+ "rules_version": "stackcraft-v1",
1026
+ "source_commit": "e524f0c15c2dd60ebfdc1374d66836c79e1e6dd6",
1027
+ "source_dirty": false,
1028
+ "source_hashes": {
1029
+ "scripts/evaluate_clef.py": "70a47e9314ec9d7351a939bae32f07f14bd08f6106b60ce6155687c0540b92dd",
1030
+ "src/stackcraft/clef.py": "c505f35d27121ba9ec9db3c663927ac8b28304f4ed864765be61ce96c8c86e13",
1031
+ "src/stackcraft/data.py": "74b68a1f391a3128d2e499c7e86b6c94a1f2a2bef33ffe75c8631207065bd991",
1032
+ "src/stackcraft/engine.py": "0805be38ee968d569fa1c12230730f491490f7d27cfcc570c400ac3b4a546453",
1033
+ "src/stackcraft/evaluation.py": "df56f9d36655e79573a014a5c55c2571bc5e04e2c20f1b37e2d939b871e889e6",
1034
+ "src/stackcraft/pieces.py": "c5bee0f8767aeeab279cfbadb4c7b4c0bbf1f31c1d29b08dda197f0627654b29",
1035
+ "src/stackcraft/players/__init__.py": "550bed9237303a845cdcc101542b0b688173a8f5436dd1d08c5f85a5a19fa2b2",
1036
+ "src/stackcraft/provenance.py": "0728104f22361ad2a8b4471e6eef0467852d94f650d0259d7f5f674c0086f13b",
1037
+ "src/stackcraft/schema.py": "35c70d96e8a49e434dc0a3607412a0ce58141306df59e09b1170511694cb6eb5",
1038
+ "src/stackcraft/tournament.py": "288ed447a32e0845f52097372a644fc211b1632790655f620801d3ce1639d559",
1039
+ "src/stackcraft/training.py": "f7e48b041b331cf2b68674ec2abb1059dd1da47cb261697b46260b4f0169dba8",
1040
+ "uv.lock": "bc7fae476529578be003856b5f19e09c8cc96e0020ab999e20e1f8210696f9c8"
1041
+ },
1042
+ "source_identity_method": "exact-git-root"
1043
+ },
1044
+ "reload": {
1045
+ "dataset_manifest_sha256": "aeddcfc1ea5390f12122d2e786c1e2030dcc97d3790b6d7523432548198b1f94",
1046
+ "elapsed_seconds": 5.802506145002553,
1047
+ "gpu": "NVIDIA GeForce RTX 5090",
1048
+ "initial_free_vram": 33179500544,
1049
+ "max_absolute_probability_difference": 0.0,
1050
+ "reload": true,
1051
+ "row_ids": [
1052
+ "seed-10000-turn-0",
1053
+ "seed-10000-turn-1",
1054
+ "seed-10000-turn-2",
1055
+ "seed-10000-turn-3"
1056
+ ],
1057
+ "source_commit": "7a21f6491bcaf2afacc9aa5b5e2733a35c41a7a8",
1058
+ "source_dirty": false,
1059
+ "source_identity_method": "exact-git-root",
1060
+ "status": "passed",
1061
+ "tolerance": 0.0001,
1062
+ "torch": "2.14.1+cu130",
1063
+ "total_vram": 33711521792
1064
+ },
1065
+ "runtime": {
1066
+ "decision_latency_scope": "All Player.choose calls, including encoding and probability transfer; no excluded warmup; cached local model initialization recorded separately. Policies visit different states.",
1067
+ "energy_measured": false,
1068
+ "final_evaluation_wrapper_seconds": 5103.112404823303,
1069
+ "monetary_cost_measured": false,
1070
+ "trained_to_heuristic_mean_decision_latency_ratio": 1041.8358778494417,
1071
+ "wrapper_scope": "Includes service stop/restore and health checks, evaluation, model loading and report writing; not pure inference time."
1072
+ },
1073
+ "schema_version": 1,
1074
+ "selection": {
1075
+ "checkpoint_sha256": {
1076
+ "adapter/README.md": "bf1fa63446e8ee38c5ac154ade9138a0e2354e3215201f59473102eeb5206fa0",
1077
+ "adapter/adapter_config.json": "bbbce7f19e186b4a79c39d03786bb190d6084e7990a601e8e28672cf1c969741",
1078
+ "adapter/adapter_model.safetensors": "3ed169fe68b20a370bb856f9d91842543374ff3c98fd3bbe139ef2d0b97877a3",
1079
+ "joint_head.safetensors": "4f21c0f315cf1bcf852a9887d158928855eaa9bdcab6b9770d518abf3727fa1b",
1080
+ "reference.json": "adacc63684d28598f8016602bab790cd3967317986b6934dd94a5e14fabe571f",
1081
+ "training_config.json": "3ead31d0a3ccb6a62914c3853ee26332f6412382c5196e40b461182e64d8b329"
1082
+ },
1083
+ "dataset_manifest_sha256": "aeddcfc1ea5390f12122d2e786c1e2030dcc97d3790b6d7523432548198b1f94",
1084
+ "decision_rule": "lowest finite mean target NLL over all 215 validation positions; exact tie chooses earlier epoch",
1085
+ "selected_key": "epoch-02",
1086
+ "selection_sha256": "025b62587a26bd8cc78c80f362524b8c178190df5188b317299121afa6b1203b",
1087
+ "validation": {
1088
+ "epoch-01": {
1089
+ "players": {
1090
+ "base": {
1091
+ "available_subset_mean_brier": 0.9231725575578064,
1092
+ "complete_probability_coverage": true,
1093
+ "correct": 21,
1094
+ "error_rate": 0.0,
1095
+ "failed_predictions": [],
1096
+ "finite_subset_mean_nll": 2.9527020509302693,
1097
+ "finite_subset_nll_positions": 215,
1098
+ "mean_brier": 0.9231725575578064,
1099
+ "mean_nll": 2.9527020509302693,
1100
+ "missing_probabilities": [],
1101
+ "nll_is_infinite": false,
1102
+ "positions": 215,
1103
+ "probability_positions": 215,
1104
+ "selection_performed": false,
1105
+ "teacher_agreement": 0.09767441860465116,
1106
+ "zero_target_probability_count": 0
1107
+ },
1108
+ "base-fp32": {
1109
+ "available_subset_mean_brier": 0.9231017245023939,
1110
+ "complete_probability_coverage": true,
1111
+ "correct": 22,
1112
+ "error_rate": 0.0,
1113
+ "failed_predictions": [],
1114
+ "finite_subset_mean_nll": 2.9523931327950934,
1115
+ "finite_subset_nll_positions": 215,
1116
+ "mean_brier": 0.9231017245023939,
1117
+ "mean_nll": 2.9523931327950934,
1118
+ "missing_probabilities": [],
1119
+ "nll_is_infinite": false,
1120
+ "positions": 215,
1121
+ "probability_positions": 215,
1122
+ "selection_performed": false,
1123
+ "teacher_agreement": 0.10232558139534884,
1124
+ "zero_target_probability_count": 0
1125
+ },
1126
+ "trained": {
1127
+ "available_subset_mean_brier": 0.7221895727255984,
1128
+ "complete_probability_coverage": true,
1129
+ "correct": 89,
1130
+ "error_rate": 0.0,
1131
+ "failed_predictions": [],
1132
+ "finite_subset_mean_nll": 1.8608697515370076,
1133
+ "finite_subset_nll_positions": 215,
1134
+ "mean_brier": 0.7221895727255984,
1135
+ "mean_nll": 1.8608697515370076,
1136
+ "missing_probabilities": [],
1137
+ "nll_is_infinite": false,
1138
+ "positions": 215,
1139
+ "probability_positions": 215,
1140
+ "selection_performed": false,
1141
+ "teacher_agreement": 0.413953488372093,
1142
+ "zero_target_probability_count": 0
1143
+ }
1144
+ },
1145
+ "provenance": {
1146
+ "base_revision": "17f0b0ad64efb65d273590632833508766b2aae6",
1147
+ "encoding_version": "stackcraft-clef-v1",
1148
+ "installed_versions": {
1149
+ "huggingface-hub": "1.33.0",
1150
+ "peft": "0.21.2",
1151
+ "safetensors": "0.8.0",
1152
+ "torch": "2.14.1+cu130",
1153
+ "transformers": "5.18.0"
1154
+ },
1155
+ "precision_policy": "native BF16 base; FP32 trained head; optional base-fp32 ablation",
1156
+ "rules_version": "stackcraft-v1",
1157
+ "source_commit": "ada38885b55ae3e4b6705a18dec98d194d37447e",
1158
+ "source_dirty": false,
1159
+ "source_hashes": {
1160
+ "scripts/evaluate_clef.py": "70a47e9314ec9d7351a939bae32f07f14bd08f6106b60ce6155687c0540b92dd",
1161
+ "src/stackcraft/clef.py": "c505f35d27121ba9ec9db3c663927ac8b28304f4ed864765be61ce96c8c86e13",
1162
+ "src/stackcraft/data.py": "74b68a1f391a3128d2e499c7e86b6c94a1f2a2bef33ffe75c8631207065bd991",
1163
+ "src/stackcraft/engine.py": "0805be38ee968d569fa1c12230730f491490f7d27cfcc570c400ac3b4a546453",
1164
+ "src/stackcraft/evaluation.py": "df56f9d36655e79573a014a5c55c2571bc5e04e2c20f1b37e2d939b871e889e6",
1165
+ "src/stackcraft/pieces.py": "c5bee0f8767aeeab279cfbadb4c7b4c0bbf1f31c1d29b08dda197f0627654b29",
1166
+ "src/stackcraft/players/__init__.py": "550bed9237303a845cdcc101542b0b688173a8f5436dd1d08c5f85a5a19fa2b2",
1167
+ "src/stackcraft/provenance.py": "0728104f22361ad2a8b4471e6eef0467852d94f650d0259d7f5f674c0086f13b",
1168
+ "src/stackcraft/schema.py": "35c70d96e8a49e434dc0a3607412a0ce58141306df59e09b1170511694cb6eb5",
1169
+ "src/stackcraft/tournament.py": "288ed447a32e0845f52097372a644fc211b1632790655f620801d3ce1639d559",
1170
+ "src/stackcraft/training.py": "f7e48b041b331cf2b68674ec2abb1059dd1da47cb261697b46260b4f0169dba8",
1171
+ "uv.lock": "bc7fae476529578be003856b5f19e09c8cc96e0020ab999e20e1f8210696f9c8"
1172
+ },
1173
+ "source_identity_method": "exact-git-root"
1174
+ },
1175
+ "report_sha256": "ce401ef9fe1d25c11c7a0e3845def347967d8617223307ad823e23a90c3f0033"
1176
+ },
1177
+ "epoch-02": {
1178
+ "players": {
1179
+ "trained": {
1180
+ "available_subset_mean_brier": 0.7089798657211578,
1181
+ "complete_probability_coverage": true,
1182
+ "correct": 92,
1183
+ "error_rate": 0.0,
1184
+ "failed_predictions": [],
1185
+ "finite_subset_mean_nll": 1.7759231168523646,
1186
+ "finite_subset_nll_positions": 215,
1187
+ "mean_brier": 0.7089798657211578,
1188
+ "mean_nll": 1.7759231168523646,
1189
+ "missing_probabilities": [],
1190
+ "nll_is_infinite": false,
1191
+ "positions": 215,
1192
+ "probability_positions": 215,
1193
+ "selection_performed": false,
1194
+ "teacher_agreement": 0.42790697674418604,
1195
+ "zero_target_probability_count": 0
1196
+ }
1197
+ },
1198
+ "provenance": {
1199
+ "base_revision": "17f0b0ad64efb65d273590632833508766b2aae6",
1200
+ "encoding_version": "stackcraft-clef-v1",
1201
+ "installed_versions": {
1202
+ "huggingface-hub": "1.33.0",
1203
+ "peft": "0.21.2",
1204
+ "safetensors": "0.8.0",
1205
+ "torch": "2.14.1+cu130",
1206
+ "transformers": "5.18.0"
1207
+ },
1208
+ "precision_policy": "native BF16 base; FP32 trained head; optional base-fp32 ablation",
1209
+ "rules_version": "stackcraft-v1",
1210
+ "source_commit": "7a21f6491bcaf2afacc9aa5b5e2733a35c41a7a8",
1211
+ "source_dirty": false,
1212
+ "source_hashes": {
1213
+ "scripts/evaluate_clef.py": "70a47e9314ec9d7351a939bae32f07f14bd08f6106b60ce6155687c0540b92dd",
1214
+ "src/stackcraft/clef.py": "c505f35d27121ba9ec9db3c663927ac8b28304f4ed864765be61ce96c8c86e13",
1215
+ "src/stackcraft/data.py": "74b68a1f391a3128d2e499c7e86b6c94a1f2a2bef33ffe75c8631207065bd991",
1216
+ "src/stackcraft/engine.py": "0805be38ee968d569fa1c12230730f491490f7d27cfcc570c400ac3b4a546453",
1217
+ "src/stackcraft/evaluation.py": "df56f9d36655e79573a014a5c55c2571bc5e04e2c20f1b37e2d939b871e889e6",
1218
+ "src/stackcraft/pieces.py": "c5bee0f8767aeeab279cfbadb4c7b4c0bbf1f31c1d29b08dda197f0627654b29",
1219
+ "src/stackcraft/players/__init__.py": "550bed9237303a845cdcc101542b0b688173a8f5436dd1d08c5f85a5a19fa2b2",
1220
+ "src/stackcraft/provenance.py": "0728104f22361ad2a8b4471e6eef0467852d94f650d0259d7f5f674c0086f13b",
1221
+ "src/stackcraft/schema.py": "35c70d96e8a49e434dc0a3607412a0ce58141306df59e09b1170511694cb6eb5",
1222
+ "src/stackcraft/tournament.py": "288ed447a32e0845f52097372a644fc211b1632790655f620801d3ce1639d559",
1223
+ "src/stackcraft/training.py": "f7e48b041b331cf2b68674ec2abb1059dd1da47cb261697b46260b4f0169dba8",
1224
+ "uv.lock": "bc7fae476529578be003856b5f19e09c8cc96e0020ab999e20e1f8210696f9c8"
1225
+ },
1226
+ "source_identity_method": "exact-git-root"
1227
+ },
1228
+ "report_sha256": "9482be5ac08b85a7bafc18d8f70a35ba277f476d9a96dd0684553e937abc4d2a"
1229
+ }
1230
+ }
1231
+ },
1232
+ "status": "final evaluation complete; publication pending",
1233
+ "training": {
1234
+ "config": {
1235
+ "batch_size": 1,
1236
+ "brier_weight": 0.1,
1237
+ "clip_gradient_norm": 1.0,
1238
+ "epochs": 2,
1239
+ "gradient_accumulation": 8,
1240
+ "label_smoothing": 0.05,
1241
+ "learning_rate": 1e-05,
1242
+ "max_length": 4096,
1243
+ "mode": "lora",
1244
+ "optimizer": "AdamW",
1245
+ "rank": 4,
1246
+ "seed": 42,
1247
+ "selection": "external validation only; this script does not choose a checkpoint",
1248
+ "weight_decay": 0.01
1249
+ },
1250
+ "dataset_counts": {
1251
+ "train": 827,
1252
+ "validation": 215
1253
+ },
1254
+ "dataset_manifest_sha256": "aeddcfc1ea5390f12122d2e786c1e2030dcc97d3790b6d7523432548198b1f94",
1255
+ "dataset_split_sha256": {
1256
+ "train": "edd682761db95a4f25bb30a284489c54d9336a36da0a9b19d2cda860b428baa8",
1257
+ "validation": "eff9cdc5932e935959ac4d26dce6470d335090f7428a91930001954266d133bc"
1258
+ },
1259
+ "elapsed_seconds": 1511.2463698700012,
1260
+ "epochs": [
1261
+ {
1262
+ "checkpoint": "runs/study-v1/epoch-01",
1263
+ "epoch": 1,
1264
+ "examples": 827,
1265
+ "mean_training_loss": 2.5779671531346287,
1266
+ "optimizer_steps": 104,
1267
+ "peak_allocated_bytes": 23230341120,
1268
+ "peak_reserved_bytes": 24178065408,
1269
+ "seconds": 742.4755487579969,
1270
+ "shuffle_order_sha256": "b6e0d7189b1d965472f1132dda2e9be3a35093dae838baf5d8e0c0d3471b6198"
1271
+ },
1272
+ {
1273
+ "checkpoint": "runs/study-v1/epoch-02",
1274
+ "epoch": 2,
1275
+ "examples": 827,
1276
+ "mean_training_loss": 1.9667175294878694,
1277
+ "optimizer_steps": 104,
1278
+ "peak_allocated_bytes": 23226543104,
1279
+ "peak_reserved_bytes": 24178065408,
1280
+ "seconds": 744.6554337799898,
1281
+ "shuffle_order_sha256": "b9d20efe77959592c99e9247a58d844be69df50db595bd87c6d7e595f5d2e0c4"
1282
+ }
1283
+ ],
1284
+ "frozen_parameters_unchanged": true,
1285
+ "source_commit": "d9fcd05c02e9327d4d791f2338725092cede8bdd",
1286
+ "source_dirty": true,
1287
+ "source_hashes": {
1288
+ "scripts/train_clef.py": "e83586cf0fd0d7bb8c4aa15f9b858b9f9ffdb915622e355deb9513788d5054aa",
1289
+ "src/stackcraft/clef.py": "c505f35d27121ba9ec9db3c663927ac8b28304f4ed864765be61ce96c8c86e13",
1290
+ "src/stackcraft/data.py": "74b68a1f391a3128d2e499c7e86b6c94a1f2a2bef33ffe75c8631207065bd991",
1291
+ "src/stackcraft/training.py": "f7e48b041b331cf2b68674ec2abb1059dd1da47cb261697b46260b4f0169dba8"
1292
+ },
1293
+ "test_trajectories_used": false,
1294
+ "trainable_parameters": 132582404,
1295
+ "validation_used_for_training": false
1296
+ }
1297
+ }
code/reports/gpu-feasibility.json ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "runs/probe-v1/report.json": {
3
+ "sha256": "b66d2c01a43d08234229f3d7d3980a7d2d161fb21aa26965acb3bfe243351807",
4
+ "report": {
5
+ "status": "passed",
6
+ "reload": false,
7
+ "gpu": "NVIDIA GeForce RTX 5090",
8
+ "total_vram": 33711521792,
9
+ "initial_free_vram": 33179500544,
10
+ "torch": "2.14.1+cu130",
11
+ "dataset_manifest_sha256": "aeddcfc1ea5390f12122d2e786c1e2030dcc97d3790b6d7523432548198b1f94",
12
+ "source_commit": "e38ac536d0a6af444b48fddd6d8789ace089a51c",
13
+ "row_ids": [
14
+ "seed-10000-turn-0",
15
+ "seed-10000-turn-1",
16
+ "seed-10000-turn-2",
17
+ "seed-10000-turn-3"
18
+ ],
19
+ "runtime_config": {
20
+ "model_id": "Cloudflare/clef-flash",
21
+ "revision": "Cloudflare/clef-flash@17f0b0ad64efb65d273590632833508766b2aae6:stackcraft-clef-v1",
22
+ "base_revision": "17f0b0ad64efb65d273590632833508766b2aae6",
23
+ "encoding_version": "stackcraft-clef-v1",
24
+ "source_sha256": "0e304cf7c6500e8bb59bef7e2afd2c6373f82596dfb3b57d1aa93c175e2dc3a3",
25
+ "max_length": 4096,
26
+ "dtype": "torch.bfloat16",
27
+ "device": "cuda:0"
28
+ },
29
+ "load_and_native_seconds": 5.267827647010563,
30
+ "fp32_head_initial_max_probability_drift": 0.0007441192865371704,
31
+ "head_steps": [
32
+ {
33
+ "step": 1,
34
+ "row_id": "seed-10000-turn-0",
35
+ "tokens": 2248,
36
+ "loss": 3.5077385902404785,
37
+ "gradient_abs_sums": {
38
+ "head": 1406.742018819321,
39
+ "lora": 0.0
40
+ },
41
+ "seconds": 0.3278578449971974,
42
+ "peak_allocated_bytes": 21323760640,
43
+ "peak_reserved_bytes": 21369978880
44
+ },
45
+ {
46
+ "step": 2,
47
+ "row_id": "seed-10000-turn-1",
48
+ "tokens": 2252,
49
+ "loss": 3.839761734008789,
50
+ "gradient_abs_sums": {
51
+ "head": 3064.3000336452387,
52
+ "lora": 0.0
53
+ },
54
+ "seconds": 0.258370580006158,
55
+ "peak_allocated_bytes": 21323760640,
56
+ "peak_reserved_bytes": 21541945344
57
+ },
58
+ {
59
+ "step": 3,
60
+ "row_id": "seed-10000-turn-2",
61
+ "tokens": 1302,
62
+ "loss": 2.7408740520477295,
63
+ "gradient_abs_sums": {
64
+ "head": 2409.824099964928,
65
+ "lora": 0.0
66
+ },
67
+ "seconds": 0.1538237390050199,
68
+ "peak_allocated_bytes": 21323760640,
69
+ "peak_reserved_bytes": 21541945344
70
+ }
71
+ ],
72
+ "head_frozen_parameters_unchanged": true,
73
+ "lora_steps": [
74
+ {
75
+ "step": 1,
76
+ "row_id": "seed-10000-turn-0",
77
+ "tokens": 2248,
78
+ "loss": 3.5077385902404785,
79
+ "gradient_abs_sums": {
80
+ "head": 1406.742226867238,
81
+ "lora": 4314.483247246593
82
+ },
83
+ "seconds": 1.3170126390177757,
84
+ "peak_allocated_bytes": 22128918016,
85
+ "peak_reserved_bytes": 22531801088
86
+ },
87
+ {
88
+ "step": 2,
89
+ "row_id": "seed-10000-turn-1",
90
+ "tokens": 2252,
91
+ "loss": 3.845935106277466,
92
+ "gradient_abs_sums": {
93
+ "head": 3134.688414009288,
94
+ "lora": 2980.2572337791935
95
+ },
96
+ "seconds": 1.3217443070025183,
97
+ "peak_allocated_bytes": 23176526848,
98
+ "peak_reserved_bytes": 23483908096
99
+ },
100
+ {
101
+ "step": 3,
102
+ "row_id": "seed-10000-turn-2",
103
+ "tokens": 1302,
104
+ "loss": 2.744056463241577,
105
+ "gradient_abs_sums": {
106
+ "head": 2369.929164870642,
107
+ "lora": 3249.260241425407
108
+ },
109
+ "seconds": 0.672728523000842,
110
+ "peak_allocated_bytes": 23176526848,
111
+ "peak_reserved_bytes": 23483908096
112
+ },
113
+ {
114
+ "step": 4,
115
+ "row_id": "seed-10000-turn-3",
116
+ "tokens": 1304,
117
+ "loss": 2.775989294052124,
118
+ "gradient_abs_sums": {
119
+ "head": 2491.2841364948545,
120
+ "lora": 3332.906228476757
121
+ },
122
+ "seconds": 0.6700605849910062,
123
+ "peak_allocated_bytes": 23176526848,
124
+ "peak_reserved_bytes": 23483908096
125
+ },
126
+ {
127
+ "step": 5,
128
+ "row_id": "seed-10000-turn-0",
129
+ "tokens": 2248,
130
+ "loss": 3.4597291946411133,
131
+ "gradient_abs_sums": {
132
+ "head": 1691.1293117979076,
133
+ "lora": 4719.155243932968
134
+ },
135
+ "seconds": 1.3178317880083341,
136
+ "peak_allocated_bytes": 23180389888,
137
+ "peak_reserved_bytes": 23595057152
138
+ }
139
+ ],
140
+ "checkpoint": "runs/probe-v1/checkpoint",
141
+ "frozen_parameters_unchanged": true,
142
+ "elapsed_seconds": 51.810954032989684
143
+ }
144
+ },
145
+ "runs/probe-head-reload-v1/report.json": {
146
+ "sha256": "ed7afb912569ce4738d506962ca98c33cbbaee2d922ab4bff7b8adcbbae8c521",
147
+ "report": {
148
+ "status": "passed",
149
+ "reload": true,
150
+ "gpu": "NVIDIA GeForce RTX 5090",
151
+ "total_vram": 33711521792,
152
+ "initial_free_vram": 33179500544,
153
+ "torch": "2.14.1+cu130",
154
+ "dataset_manifest_sha256": "aeddcfc1ea5390f12122d2e786c1e2030dcc97d3790b6d7523432548198b1f94",
155
+ "source_commit": "e38ac536d0a6af444b48fddd6d8789ace089a51c",
156
+ "row_ids": [
157
+ "seed-10000-turn-0",
158
+ "seed-10000-turn-1",
159
+ "seed-10000-turn-2",
160
+ "seed-10000-turn-3"
161
+ ],
162
+ "max_absolute_probability_difference": 0.0,
163
+ "tolerance": 0.0001,
164
+ "elapsed_seconds": 5.435347482009092
165
+ }
166
+ },
167
+ "runs/probe-reload-v1/report.json": {
168
+ "sha256": "174d5267b1825bbd66bcf4456fdc97b505e7a3565557462358ab99c66bd11539",
169
+ "report": {
170
+ "status": "failed",
171
+ "reload": true,
172
+ "gpu": "NVIDIA GeForce RTX 5090",
173
+ "total_vram": 33711521792,
174
+ "initial_free_vram": 33179500544,
175
+ "torch": "2.14.1+cu130",
176
+ "dataset_manifest_sha256": "aeddcfc1ea5390f12122d2e786c1e2030dcc97d3790b6d7523432548198b1f94",
177
+ "source_commit": "e38ac536d0a6af444b48fddd6d8789ace089a51c",
178
+ "row_ids": [
179
+ "seed-10000-turn-0",
180
+ "seed-10000-turn-1",
181
+ "seed-10000-turn-2",
182
+ "seed-10000-turn-3"
183
+ ],
184
+ "error": "ValueError: saved adapter configuration differs from checkpoint metadata",
185
+ "elapsed_seconds": 4.1464992630062625
186
+ }
187
+ },
188
+ "runs/probe-reload-v2/report.json": {
189
+ "sha256": "1687e90ab5518785bc3c6a1701978828f410624bab6e297e5a4a2deb8bdcf2e3",
190
+ "report": {
191
+ "status": "passed",
192
+ "reload": true,
193
+ "gpu": "NVIDIA GeForce RTX 5090",
194
+ "total_vram": 33711521792,
195
+ "initial_free_vram": 33179500544,
196
+ "torch": "2.14.1+cu130",
197
+ "dataset_manifest_sha256": "aeddcfc1ea5390f12122d2e786c1e2030dcc97d3790b6d7523432548198b1f94",
198
+ "source_commit": "f7be7ceee9c38566dfebad0ebf5f1c04958faee3",
199
+ "row_ids": [
200
+ "seed-10000-turn-0",
201
+ "seed-10000-turn-1",
202
+ "seed-10000-turn-2",
203
+ "seed-10000-turn-3"
204
+ ],
205
+ "max_absolute_probability_difference": 0.0,
206
+ "tolerance": 0.0001,
207
+ "elapsed_seconds": 5.648779143986758
208
+ }
209
+ }
210
+ }
code/reports/selected-checkpoint-reload.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "status": "passed",
3
+ "reload": true,
4
+ "gpu": "NVIDIA GeForce RTX 5090",
5
+ "total_vram": 33711521792,
6
+ "initial_free_vram": 33179500544,
7
+ "torch": "2.14.1+cu130",
8
+ "dataset_manifest_sha256": "aeddcfc1ea5390f12122d2e786c1e2030dcc97d3790b6d7523432548198b1f94",
9
+ "row_ids": [
10
+ "seed-10000-turn-0",
11
+ "seed-10000-turn-1",
12
+ "seed-10000-turn-2",
13
+ "seed-10000-turn-3"
14
+ ],
15
+ "source_commit": "7a21f6491bcaf2afacc9aa5b5e2733a35c41a7a8",
16
+ "source_dirty": false,
17
+ "source_identity_method": "exact-git-root",
18
+ "max_absolute_probability_difference": 0.0,
19
+ "tolerance": 0.0001,
20
+ "elapsed_seconds": 5.802506145002553
21
+ }
code/reports/study-preregistration.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Archived training and final-test preregistration
2
+
3
+ The excerpt below is preserved verbatim from `record.md` at training launch commit
4
+ `d9fcd05c02e9327d4d791f2338725092cede8bdd`. The original complete record's SHA256 is
5
+ `bb18a6fc7c21d3d186afcf682e724015263340f3c269500951f8f159054453e1`. This archive lets readers inspect the
6
+ recorded decisions without access to the original Git repository. It documents
7
+ what was recorded; it is not a signed timestamp or independent registration service.
8
+
9
+ The final checkpoint selection and evaluation request provide separate hashes of
10
+ the actual artifacts used. This archive is historical: later outcomes belong in
11
+ the training, validation, reload and final evaluation reports.
12
+
13
+ ---
14
+
15
+ ## 2026-10-05 — M4 training and M5 preregistration
16
+
17
+ Real-weight probe from e38ac53 passed3head-only and5rank4LoRA steps. Both trainable groups changed; full frozen parameter hashes were unchanged. All recorded losses and gradients finite, head and LoRA gradients nonzero. LoRA peak allocated23,180,389,888bytes/reserved23,595,057,152bytes; step time0.67–1.32s on1302–2252tokens. Wrapper restored GLM and verified its in-container health. Fresh-process parity is the remaining M4 gate; artifacts runs/probe-v1 and runs/probe-reload-v1.
18
+
19
+ Conditional on M4 reload passing, preregister M5: one seed42, BF16 native backbone, FP32 native head, rank4/alpha8/dropout0 text-only LoRA, no quantization, max4096tokens, AdamW lr1e-5/weight_decay0.01, batch1, gradient accumulation8 (last partial group normalized by actual size), norm clip1, CE label smoothing0.05 plus Brier sum weight0.1. Train exactly two epochs over the827 fixed training positions shuffled deterministically per epoch; save both epoch checkpoints. Bounded search is these two checkpoints only; exclude feasibility-probe checkpoints. Use all215 validation positions to select the epoch with lowest finite mean target NLL; exact tie chooses earlier epoch. Report teacher agreement and Brier as diagnostics, not selection overrides. If either candidate has inference errors or nonfinite NLL it is ineligible. If both fail, preserve evidence and reconsider before tests. Initial fulltraining timeout3600s; no test access during training/selection.
20
+
21
+ Freeze final evaluation at200paired seeds30000–30199 and200piececap, bootstrap10000replicates/seed2026, error threshold0. Primary comparison selectedtrained versus unchanged nativeBF16headClef. Include an unchangedFP32head ablation to separate precision change from learning, plus random and heuristic. Same rules, complete observation encoding, pinnedbackbone, legalchoices and preview. Three neural policies at200×200moves and144ms imply4.8h worst-case, but unchangedbase dies around28pieces in development; expect much less. Allow bounded per-policy execution with incremental artifacts; never drop failedseeds or quietly reducepool. Record exact checkpoint and validation evidence hashes before any testepisode. Publish negative outcomes honestly; no tuning after testresults.
22
+
23
+ Fresh LoRA reload v1 correctly rejected PEFT's shortened target list. Saved496adapter tensors independently map exactly248intended text layers; weights were sound. Loader now expands saved targets over every backbone module and requires exact target equality, rejecting accidental vision/MTP/output matches. A32-layer tiny-backbone regression reproduces PEFT shortening. New saves use explicit full targets and canonical pinned model ID. Original failed artifact and probe weights preserved; retry uses same checkpoint, not a new training configuration. Head-only fresh-process reload passed independently; GLM restored healthy on both success and rejectedreload.
24
+
25
+ M4 complete: head-only fresh reload maxabsoluteprobabilitydifference0.0 (tolerance1e-4); unchanged saved LoRA checkpoint retry after semantic-target validation also0.0. Both fresh processes loaded pinned weights; no retraining needed for reload fix. Compact reports/gpu-feasibility.json retains success and original rejectedreload with sourcehashes. Fulltraining preregistration above is now enabled; output runs/study-v1 with two epoch checkpoints,3600second wrapper timeout and restored GLM health.
code/reports/study-training.json ADDED
@@ -0,0 +1,696 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "changed_trainable_parameter_names": [
3
+ "language_model.base_model.model.model.language_model.layers.0.linear_attn.out_proj.lora_A.default.weight",
4
+ "language_model.base_model.model.model.language_model.layers.0.linear_attn.out_proj.lora_B.default.weight",
5
+ "language_model.base_model.model.model.language_model.layers.0.linear_attn.in_proj_qkv.lora_A.default.weight",
6
+ "language_model.base_model.model.model.language_model.layers.0.linear_attn.in_proj_qkv.lora_B.default.weight",
7
+ "language_model.base_model.model.model.language_model.layers.0.linear_attn.in_proj_z.lora_A.default.weight",
8
+ "language_model.base_model.model.model.language_model.layers.0.linear_attn.in_proj_z.lora_B.default.weight",
9
+ "language_model.base_model.model.model.language_model.layers.0.linear_attn.in_proj_b.lora_A.default.weight",
10
+ "language_model.base_model.model.model.language_model.layers.0.linear_attn.in_proj_b.lora_B.default.weight",
11
+ "language_model.base_model.model.model.language_model.layers.0.linear_attn.in_proj_a.lora_A.default.weight",
12
+ "language_model.base_model.model.model.language_model.layers.0.linear_attn.in_proj_a.lora_B.default.weight",
13
+ "language_model.base_model.model.model.language_model.layers.0.mlp.gate_proj.lora_A.default.weight",
14
+ "language_model.base_model.model.model.language_model.layers.0.mlp.gate_proj.lora_B.default.weight",
15
+ "language_model.base_model.model.model.language_model.layers.0.mlp.up_proj.lora_A.default.weight",
16
+ "language_model.base_model.model.model.language_model.layers.0.mlp.up_proj.lora_B.default.weight",
17
+ "language_model.base_model.model.model.language_model.layers.0.mlp.down_proj.lora_A.default.weight",
18
+ "language_model.base_model.model.model.language_model.layers.0.mlp.down_proj.lora_B.default.weight",
19
+ "language_model.base_model.model.model.language_model.layers.1.linear_attn.out_proj.lora_A.default.weight",
20
+ "language_model.base_model.model.model.language_model.layers.1.linear_attn.out_proj.lora_B.default.weight",
21
+ "language_model.base_model.model.model.language_model.layers.1.linear_attn.in_proj_qkv.lora_A.default.weight",
22
+ "language_model.base_model.model.model.language_model.layers.1.linear_attn.in_proj_qkv.lora_B.default.weight",
23
+ "language_model.base_model.model.model.language_model.layers.1.linear_attn.in_proj_z.lora_A.default.weight",
24
+ "language_model.base_model.model.model.language_model.layers.1.linear_attn.in_proj_z.lora_B.default.weight",
25
+ "language_model.base_model.model.model.language_model.layers.1.linear_attn.in_proj_b.lora_A.default.weight",
26
+ "language_model.base_model.model.model.language_model.layers.1.linear_attn.in_proj_b.lora_B.default.weight",
27
+ "language_model.base_model.model.model.language_model.layers.1.linear_attn.in_proj_a.lora_A.default.weight",
28
+ "language_model.base_model.model.model.language_model.layers.1.linear_attn.in_proj_a.lora_B.default.weight",
29
+ "language_model.base_model.model.model.language_model.layers.1.mlp.gate_proj.lora_A.default.weight",
30
+ "language_model.base_model.model.model.language_model.layers.1.mlp.gate_proj.lora_B.default.weight",
31
+ "language_model.base_model.model.model.language_model.layers.1.mlp.up_proj.lora_A.default.weight",
32
+ "language_model.base_model.model.model.language_model.layers.1.mlp.up_proj.lora_B.default.weight",
33
+ "language_model.base_model.model.model.language_model.layers.1.mlp.down_proj.lora_A.default.weight",
34
+ "language_model.base_model.model.model.language_model.layers.1.mlp.down_proj.lora_B.default.weight",
35
+ "language_model.base_model.model.model.language_model.layers.2.linear_attn.out_proj.lora_A.default.weight",
36
+ "language_model.base_model.model.model.language_model.layers.2.linear_attn.out_proj.lora_B.default.weight",
37
+ "language_model.base_model.model.model.language_model.layers.2.linear_attn.in_proj_qkv.lora_A.default.weight",
38
+ "language_model.base_model.model.model.language_model.layers.2.linear_attn.in_proj_qkv.lora_B.default.weight",
39
+ "language_model.base_model.model.model.language_model.layers.2.linear_attn.in_proj_z.lora_A.default.weight",
40
+ "language_model.base_model.model.model.language_model.layers.2.linear_attn.in_proj_z.lora_B.default.weight",
41
+ "language_model.base_model.model.model.language_model.layers.2.linear_attn.in_proj_b.lora_A.default.weight",
42
+ "language_model.base_model.model.model.language_model.layers.2.linear_attn.in_proj_b.lora_B.default.weight",
43
+ "language_model.base_model.model.model.language_model.layers.2.linear_attn.in_proj_a.lora_A.default.weight",
44
+ "language_model.base_model.model.model.language_model.layers.2.linear_attn.in_proj_a.lora_B.default.weight",
45
+ "language_model.base_model.model.model.language_model.layers.2.mlp.gate_proj.lora_A.default.weight",
46
+ "language_model.base_model.model.model.language_model.layers.2.mlp.gate_proj.lora_B.default.weight",
47
+ "language_model.base_model.model.model.language_model.layers.2.mlp.up_proj.lora_A.default.weight",
48
+ "language_model.base_model.model.model.language_model.layers.2.mlp.up_proj.lora_B.default.weight",
49
+ "language_model.base_model.model.model.language_model.layers.2.mlp.down_proj.lora_A.default.weight",
50
+ "language_model.base_model.model.model.language_model.layers.2.mlp.down_proj.lora_B.default.weight",
51
+ "language_model.base_model.model.model.language_model.layers.3.self_attn.q_proj.lora_A.default.weight",
52
+ "language_model.base_model.model.model.language_model.layers.3.self_attn.q_proj.lora_B.default.weight",
53
+ "language_model.base_model.model.model.language_model.layers.3.self_attn.k_proj.lora_A.default.weight",
54
+ "language_model.base_model.model.model.language_model.layers.3.self_attn.k_proj.lora_B.default.weight",
55
+ "language_model.base_model.model.model.language_model.layers.3.self_attn.v_proj.lora_A.default.weight",
56
+ "language_model.base_model.model.model.language_model.layers.3.self_attn.v_proj.lora_B.default.weight",
57
+ "language_model.base_model.model.model.language_model.layers.3.self_attn.o_proj.lora_A.default.weight",
58
+ "language_model.base_model.model.model.language_model.layers.3.self_attn.o_proj.lora_B.default.weight",
59
+ "language_model.base_model.model.model.language_model.layers.3.mlp.gate_proj.lora_A.default.weight",
60
+ "language_model.base_model.model.model.language_model.layers.3.mlp.gate_proj.lora_B.default.weight",
61
+ "language_model.base_model.model.model.language_model.layers.3.mlp.up_proj.lora_A.default.weight",
62
+ "language_model.base_model.model.model.language_model.layers.3.mlp.up_proj.lora_B.default.weight",
63
+ "language_model.base_model.model.model.language_model.layers.3.mlp.down_proj.lora_A.default.weight",
64
+ "language_model.base_model.model.model.language_model.layers.3.mlp.down_proj.lora_B.default.weight",
65
+ "language_model.base_model.model.model.language_model.layers.4.linear_attn.out_proj.lora_A.default.weight",
66
+ "language_model.base_model.model.model.language_model.layers.4.linear_attn.out_proj.lora_B.default.weight",
67
+ "language_model.base_model.model.model.language_model.layers.4.linear_attn.in_proj_qkv.lora_A.default.weight",
68
+ "language_model.base_model.model.model.language_model.layers.4.linear_attn.in_proj_qkv.lora_B.default.weight",
69
+ "language_model.base_model.model.model.language_model.layers.4.linear_attn.in_proj_z.lora_A.default.weight",
70
+ "language_model.base_model.model.model.language_model.layers.4.linear_attn.in_proj_z.lora_B.default.weight",
71
+ "language_model.base_model.model.model.language_model.layers.4.linear_attn.in_proj_b.lora_A.default.weight",
72
+ "language_model.base_model.model.model.language_model.layers.4.linear_attn.in_proj_b.lora_B.default.weight",
73
+ "language_model.base_model.model.model.language_model.layers.4.linear_attn.in_proj_a.lora_A.default.weight",
74
+ "language_model.base_model.model.model.language_model.layers.4.linear_attn.in_proj_a.lora_B.default.weight",
75
+ "language_model.base_model.model.model.language_model.layers.4.mlp.gate_proj.lora_A.default.weight",
76
+ "language_model.base_model.model.model.language_model.layers.4.mlp.gate_proj.lora_B.default.weight",
77
+ "language_model.base_model.model.model.language_model.layers.4.mlp.up_proj.lora_A.default.weight",
78
+ "language_model.base_model.model.model.language_model.layers.4.mlp.up_proj.lora_B.default.weight",
79
+ "language_model.base_model.model.model.language_model.layers.4.mlp.down_proj.lora_A.default.weight",
80
+ "language_model.base_model.model.model.language_model.layers.4.mlp.down_proj.lora_B.default.weight",
81
+ "language_model.base_model.model.model.language_model.layers.5.linear_attn.out_proj.lora_A.default.weight",
82
+ "language_model.base_model.model.model.language_model.layers.5.linear_attn.out_proj.lora_B.default.weight",
83
+ "language_model.base_model.model.model.language_model.layers.5.linear_attn.in_proj_qkv.lora_A.default.weight",
84
+ "language_model.base_model.model.model.language_model.layers.5.linear_attn.in_proj_qkv.lora_B.default.weight",
85
+ "language_model.base_model.model.model.language_model.layers.5.linear_attn.in_proj_z.lora_A.default.weight",
86
+ "language_model.base_model.model.model.language_model.layers.5.linear_attn.in_proj_z.lora_B.default.weight",
87
+ "language_model.base_model.model.model.language_model.layers.5.linear_attn.in_proj_b.lora_A.default.weight",
88
+ "language_model.base_model.model.model.language_model.layers.5.linear_attn.in_proj_b.lora_B.default.weight",
89
+ "language_model.base_model.model.model.language_model.layers.5.linear_attn.in_proj_a.lora_A.default.weight",
90
+ "language_model.base_model.model.model.language_model.layers.5.linear_attn.in_proj_a.lora_B.default.weight",
91
+ "language_model.base_model.model.model.language_model.layers.5.mlp.gate_proj.lora_A.default.weight",
92
+ "language_model.base_model.model.model.language_model.layers.5.mlp.gate_proj.lora_B.default.weight",
93
+ "language_model.base_model.model.model.language_model.layers.5.mlp.up_proj.lora_A.default.weight",
94
+ "language_model.base_model.model.model.language_model.layers.5.mlp.up_proj.lora_B.default.weight",
95
+ "language_model.base_model.model.model.language_model.layers.5.mlp.down_proj.lora_A.default.weight",
96
+ "language_model.base_model.model.model.language_model.layers.5.mlp.down_proj.lora_B.default.weight",
97
+ "language_model.base_model.model.model.language_model.layers.6.linear_attn.out_proj.lora_A.default.weight",
98
+ "language_model.base_model.model.model.language_model.layers.6.linear_attn.out_proj.lora_B.default.weight",
99
+ "language_model.base_model.model.model.language_model.layers.6.linear_attn.in_proj_qkv.lora_A.default.weight",
100
+ "language_model.base_model.model.model.language_model.layers.6.linear_attn.in_proj_qkv.lora_B.default.weight",
101
+ "language_model.base_model.model.model.language_model.layers.6.linear_attn.in_proj_z.lora_A.default.weight",
102
+ "language_model.base_model.model.model.language_model.layers.6.linear_attn.in_proj_z.lora_B.default.weight",
103
+ "language_model.base_model.model.model.language_model.layers.6.linear_attn.in_proj_b.lora_A.default.weight",
104
+ "language_model.base_model.model.model.language_model.layers.6.linear_attn.in_proj_b.lora_B.default.weight",
105
+ "language_model.base_model.model.model.language_model.layers.6.linear_attn.in_proj_a.lora_A.default.weight",
106
+ "language_model.base_model.model.model.language_model.layers.6.linear_attn.in_proj_a.lora_B.default.weight",
107
+ "language_model.base_model.model.model.language_model.layers.6.mlp.gate_proj.lora_A.default.weight",
108
+ "language_model.base_model.model.model.language_model.layers.6.mlp.gate_proj.lora_B.default.weight",
109
+ "language_model.base_model.model.model.language_model.layers.6.mlp.up_proj.lora_A.default.weight",
110
+ "language_model.base_model.model.model.language_model.layers.6.mlp.up_proj.lora_B.default.weight",
111
+ "language_model.base_model.model.model.language_model.layers.6.mlp.down_proj.lora_A.default.weight",
112
+ "language_model.base_model.model.model.language_model.layers.6.mlp.down_proj.lora_B.default.weight",
113
+ "language_model.base_model.model.model.language_model.layers.7.self_attn.q_proj.lora_A.default.weight",
114
+ "language_model.base_model.model.model.language_model.layers.7.self_attn.q_proj.lora_B.default.weight",
115
+ "language_model.base_model.model.model.language_model.layers.7.self_attn.k_proj.lora_A.default.weight",
116
+ "language_model.base_model.model.model.language_model.layers.7.self_attn.k_proj.lora_B.default.weight",
117
+ "language_model.base_model.model.model.language_model.layers.7.self_attn.v_proj.lora_A.default.weight",
118
+ "language_model.base_model.model.model.language_model.layers.7.self_attn.v_proj.lora_B.default.weight",
119
+ "language_model.base_model.model.model.language_model.layers.7.self_attn.o_proj.lora_A.default.weight",
120
+ "language_model.base_model.model.model.language_model.layers.7.self_attn.o_proj.lora_B.default.weight",
121
+ "language_model.base_model.model.model.language_model.layers.7.mlp.gate_proj.lora_A.default.weight",
122
+ "language_model.base_model.model.model.language_model.layers.7.mlp.gate_proj.lora_B.default.weight",
123
+ "language_model.base_model.model.model.language_model.layers.7.mlp.up_proj.lora_A.default.weight",
124
+ "language_model.base_model.model.model.language_model.layers.7.mlp.up_proj.lora_B.default.weight",
125
+ "language_model.base_model.model.model.language_model.layers.7.mlp.down_proj.lora_A.default.weight",
126
+ "language_model.base_model.model.model.language_model.layers.7.mlp.down_proj.lora_B.default.weight",
127
+ "language_model.base_model.model.model.language_model.layers.8.linear_attn.out_proj.lora_A.default.weight",
128
+ "language_model.base_model.model.model.language_model.layers.8.linear_attn.out_proj.lora_B.default.weight",
129
+ "language_model.base_model.model.model.language_model.layers.8.linear_attn.in_proj_qkv.lora_A.default.weight",
130
+ "language_model.base_model.model.model.language_model.layers.8.linear_attn.in_proj_qkv.lora_B.default.weight",
131
+ "language_model.base_model.model.model.language_model.layers.8.linear_attn.in_proj_z.lora_A.default.weight",
132
+ "language_model.base_model.model.model.language_model.layers.8.linear_attn.in_proj_z.lora_B.default.weight",
133
+ "language_model.base_model.model.model.language_model.layers.8.linear_attn.in_proj_b.lora_A.default.weight",
134
+ "language_model.base_model.model.model.language_model.layers.8.linear_attn.in_proj_b.lora_B.default.weight",
135
+ "language_model.base_model.model.model.language_model.layers.8.linear_attn.in_proj_a.lora_A.default.weight",
136
+ "language_model.base_model.model.model.language_model.layers.8.linear_attn.in_proj_a.lora_B.default.weight",
137
+ "language_model.base_model.model.model.language_model.layers.8.mlp.gate_proj.lora_A.default.weight",
138
+ "language_model.base_model.model.model.language_model.layers.8.mlp.gate_proj.lora_B.default.weight",
139
+ "language_model.base_model.model.model.language_model.layers.8.mlp.up_proj.lora_A.default.weight",
140
+ "language_model.base_model.model.model.language_model.layers.8.mlp.up_proj.lora_B.default.weight",
141
+ "language_model.base_model.model.model.language_model.layers.8.mlp.down_proj.lora_A.default.weight",
142
+ "language_model.base_model.model.model.language_model.layers.8.mlp.down_proj.lora_B.default.weight",
143
+ "language_model.base_model.model.model.language_model.layers.9.linear_attn.out_proj.lora_A.default.weight",
144
+ "language_model.base_model.model.model.language_model.layers.9.linear_attn.out_proj.lora_B.default.weight",
145
+ "language_model.base_model.model.model.language_model.layers.9.linear_attn.in_proj_qkv.lora_A.default.weight",
146
+ "language_model.base_model.model.model.language_model.layers.9.linear_attn.in_proj_qkv.lora_B.default.weight",
147
+ "language_model.base_model.model.model.language_model.layers.9.linear_attn.in_proj_z.lora_A.default.weight",
148
+ "language_model.base_model.model.model.language_model.layers.9.linear_attn.in_proj_z.lora_B.default.weight",
149
+ "language_model.base_model.model.model.language_model.layers.9.linear_attn.in_proj_b.lora_A.default.weight",
150
+ "language_model.base_model.model.model.language_model.layers.9.linear_attn.in_proj_b.lora_B.default.weight",
151
+ "language_model.base_model.model.model.language_model.layers.9.linear_attn.in_proj_a.lora_A.default.weight",
152
+ "language_model.base_model.model.model.language_model.layers.9.linear_attn.in_proj_a.lora_B.default.weight",
153
+ "language_model.base_model.model.model.language_model.layers.9.mlp.gate_proj.lora_A.default.weight",
154
+ "language_model.base_model.model.model.language_model.layers.9.mlp.gate_proj.lora_B.default.weight",
155
+ "language_model.base_model.model.model.language_model.layers.9.mlp.up_proj.lora_A.default.weight",
156
+ "language_model.base_model.model.model.language_model.layers.9.mlp.up_proj.lora_B.default.weight",
157
+ "language_model.base_model.model.model.language_model.layers.9.mlp.down_proj.lora_A.default.weight",
158
+ "language_model.base_model.model.model.language_model.layers.9.mlp.down_proj.lora_B.default.weight",
159
+ "language_model.base_model.model.model.language_model.layers.10.linear_attn.out_proj.lora_A.default.weight",
160
+ "language_model.base_model.model.model.language_model.layers.10.linear_attn.out_proj.lora_B.default.weight",
161
+ "language_model.base_model.model.model.language_model.layers.10.linear_attn.in_proj_qkv.lora_A.default.weight",
162
+ "language_model.base_model.model.model.language_model.layers.10.linear_attn.in_proj_qkv.lora_B.default.weight",
163
+ "language_model.base_model.model.model.language_model.layers.10.linear_attn.in_proj_z.lora_A.default.weight",
164
+ "language_model.base_model.model.model.language_model.layers.10.linear_attn.in_proj_z.lora_B.default.weight",
165
+ "language_model.base_model.model.model.language_model.layers.10.linear_attn.in_proj_b.lora_A.default.weight",
166
+ "language_model.base_model.model.model.language_model.layers.10.linear_attn.in_proj_b.lora_B.default.weight",
167
+ "language_model.base_model.model.model.language_model.layers.10.linear_attn.in_proj_a.lora_A.default.weight",
168
+ "language_model.base_model.model.model.language_model.layers.10.linear_attn.in_proj_a.lora_B.default.weight",
169
+ "language_model.base_model.model.model.language_model.layers.10.mlp.gate_proj.lora_A.default.weight",
170
+ "language_model.base_model.model.model.language_model.layers.10.mlp.gate_proj.lora_B.default.weight",
171
+ "language_model.base_model.model.model.language_model.layers.10.mlp.up_proj.lora_A.default.weight",
172
+ "language_model.base_model.model.model.language_model.layers.10.mlp.up_proj.lora_B.default.weight",
173
+ "language_model.base_model.model.model.language_model.layers.10.mlp.down_proj.lora_A.default.weight",
174
+ "language_model.base_model.model.model.language_model.layers.10.mlp.down_proj.lora_B.default.weight",
175
+ "language_model.base_model.model.model.language_model.layers.11.self_attn.q_proj.lora_A.default.weight",
176
+ "language_model.base_model.model.model.language_model.layers.11.self_attn.q_proj.lora_B.default.weight",
177
+ "language_model.base_model.model.model.language_model.layers.11.self_attn.k_proj.lora_A.default.weight",
178
+ "language_model.base_model.model.model.language_model.layers.11.self_attn.k_proj.lora_B.default.weight",
179
+ "language_model.base_model.model.model.language_model.layers.11.self_attn.v_proj.lora_A.default.weight",
180
+ "language_model.base_model.model.model.language_model.layers.11.self_attn.v_proj.lora_B.default.weight",
181
+ "language_model.base_model.model.model.language_model.layers.11.self_attn.o_proj.lora_A.default.weight",
182
+ "language_model.base_model.model.model.language_model.layers.11.self_attn.o_proj.lora_B.default.weight",
183
+ "language_model.base_model.model.model.language_model.layers.11.mlp.gate_proj.lora_A.default.weight",
184
+ "language_model.base_model.model.model.language_model.layers.11.mlp.gate_proj.lora_B.default.weight",
185
+ "language_model.base_model.model.model.language_model.layers.11.mlp.up_proj.lora_A.default.weight",
186
+ "language_model.base_model.model.model.language_model.layers.11.mlp.up_proj.lora_B.default.weight",
187
+ "language_model.base_model.model.model.language_model.layers.11.mlp.down_proj.lora_A.default.weight",
188
+ "language_model.base_model.model.model.language_model.layers.11.mlp.down_proj.lora_B.default.weight",
189
+ "language_model.base_model.model.model.language_model.layers.12.linear_attn.out_proj.lora_A.default.weight",
190
+ "language_model.base_model.model.model.language_model.layers.12.linear_attn.out_proj.lora_B.default.weight",
191
+ "language_model.base_model.model.model.language_model.layers.12.linear_attn.in_proj_qkv.lora_A.default.weight",
192
+ "language_model.base_model.model.model.language_model.layers.12.linear_attn.in_proj_qkv.lora_B.default.weight",
193
+ "language_model.base_model.model.model.language_model.layers.12.linear_attn.in_proj_z.lora_A.default.weight",
194
+ "language_model.base_model.model.model.language_model.layers.12.linear_attn.in_proj_z.lora_B.default.weight",
195
+ "language_model.base_model.model.model.language_model.layers.12.linear_attn.in_proj_b.lora_A.default.weight",
196
+ "language_model.base_model.model.model.language_model.layers.12.linear_attn.in_proj_b.lora_B.default.weight",
197
+ "language_model.base_model.model.model.language_model.layers.12.linear_attn.in_proj_a.lora_A.default.weight",
198
+ "language_model.base_model.model.model.language_model.layers.12.linear_attn.in_proj_a.lora_B.default.weight",
199
+ "language_model.base_model.model.model.language_model.layers.12.mlp.gate_proj.lora_A.default.weight",
200
+ "language_model.base_model.model.model.language_model.layers.12.mlp.gate_proj.lora_B.default.weight",
201
+ "language_model.base_model.model.model.language_model.layers.12.mlp.up_proj.lora_A.default.weight",
202
+ "language_model.base_model.model.model.language_model.layers.12.mlp.up_proj.lora_B.default.weight",
203
+ "language_model.base_model.model.model.language_model.layers.12.mlp.down_proj.lora_A.default.weight",
204
+ "language_model.base_model.model.model.language_model.layers.12.mlp.down_proj.lora_B.default.weight",
205
+ "language_model.base_model.model.model.language_model.layers.13.linear_attn.out_proj.lora_A.default.weight",
206
+ "language_model.base_model.model.model.language_model.layers.13.linear_attn.out_proj.lora_B.default.weight",
207
+ "language_model.base_model.model.model.language_model.layers.13.linear_attn.in_proj_qkv.lora_A.default.weight",
208
+ "language_model.base_model.model.model.language_model.layers.13.linear_attn.in_proj_qkv.lora_B.default.weight",
209
+ "language_model.base_model.model.model.language_model.layers.13.linear_attn.in_proj_z.lora_A.default.weight",
210
+ "language_model.base_model.model.model.language_model.layers.13.linear_attn.in_proj_z.lora_B.default.weight",
211
+ "language_model.base_model.model.model.language_model.layers.13.linear_attn.in_proj_b.lora_A.default.weight",
212
+ "language_model.base_model.model.model.language_model.layers.13.linear_attn.in_proj_b.lora_B.default.weight",
213
+ "language_model.base_model.model.model.language_model.layers.13.linear_attn.in_proj_a.lora_A.default.weight",
214
+ "language_model.base_model.model.model.language_model.layers.13.linear_attn.in_proj_a.lora_B.default.weight",
215
+ "language_model.base_model.model.model.language_model.layers.13.mlp.gate_proj.lora_A.default.weight",
216
+ "language_model.base_model.model.model.language_model.layers.13.mlp.gate_proj.lora_B.default.weight",
217
+ "language_model.base_model.model.model.language_model.layers.13.mlp.up_proj.lora_A.default.weight",
218
+ "language_model.base_model.model.model.language_model.layers.13.mlp.up_proj.lora_B.default.weight",
219
+ "language_model.base_model.model.model.language_model.layers.13.mlp.down_proj.lora_A.default.weight",
220
+ "language_model.base_model.model.model.language_model.layers.13.mlp.down_proj.lora_B.default.weight",
221
+ "language_model.base_model.model.model.language_model.layers.14.linear_attn.out_proj.lora_A.default.weight",
222
+ "language_model.base_model.model.model.language_model.layers.14.linear_attn.out_proj.lora_B.default.weight",
223
+ "language_model.base_model.model.model.language_model.layers.14.linear_attn.in_proj_qkv.lora_A.default.weight",
224
+ "language_model.base_model.model.model.language_model.layers.14.linear_attn.in_proj_qkv.lora_B.default.weight",
225
+ "language_model.base_model.model.model.language_model.layers.14.linear_attn.in_proj_z.lora_A.default.weight",
226
+ "language_model.base_model.model.model.language_model.layers.14.linear_attn.in_proj_z.lora_B.default.weight",
227
+ "language_model.base_model.model.model.language_model.layers.14.linear_attn.in_proj_b.lora_A.default.weight",
228
+ "language_model.base_model.model.model.language_model.layers.14.linear_attn.in_proj_b.lora_B.default.weight",
229
+ "language_model.base_model.model.model.language_model.layers.14.linear_attn.in_proj_a.lora_A.default.weight",
230
+ "language_model.base_model.model.model.language_model.layers.14.linear_attn.in_proj_a.lora_B.default.weight",
231
+ "language_model.base_model.model.model.language_model.layers.14.mlp.gate_proj.lora_A.default.weight",
232
+ "language_model.base_model.model.model.language_model.layers.14.mlp.gate_proj.lora_B.default.weight",
233
+ "language_model.base_model.model.model.language_model.layers.14.mlp.up_proj.lora_A.default.weight",
234
+ "language_model.base_model.model.model.language_model.layers.14.mlp.up_proj.lora_B.default.weight",
235
+ "language_model.base_model.model.model.language_model.layers.14.mlp.down_proj.lora_A.default.weight",
236
+ "language_model.base_model.model.model.language_model.layers.14.mlp.down_proj.lora_B.default.weight",
237
+ "language_model.base_model.model.model.language_model.layers.15.self_attn.q_proj.lora_A.default.weight",
238
+ "language_model.base_model.model.model.language_model.layers.15.self_attn.q_proj.lora_B.default.weight",
239
+ "language_model.base_model.model.model.language_model.layers.15.self_attn.k_proj.lora_A.default.weight",
240
+ "language_model.base_model.model.model.language_model.layers.15.self_attn.k_proj.lora_B.default.weight",
241
+ "language_model.base_model.model.model.language_model.layers.15.self_attn.v_proj.lora_A.default.weight",
242
+ "language_model.base_model.model.model.language_model.layers.15.self_attn.v_proj.lora_B.default.weight",
243
+ "language_model.base_model.model.model.language_model.layers.15.self_attn.o_proj.lora_A.default.weight",
244
+ "language_model.base_model.model.model.language_model.layers.15.self_attn.o_proj.lora_B.default.weight",
245
+ "language_model.base_model.model.model.language_model.layers.15.mlp.gate_proj.lora_A.default.weight",
246
+ "language_model.base_model.model.model.language_model.layers.15.mlp.gate_proj.lora_B.default.weight",
247
+ "language_model.base_model.model.model.language_model.layers.15.mlp.up_proj.lora_A.default.weight",
248
+ "language_model.base_model.model.model.language_model.layers.15.mlp.up_proj.lora_B.default.weight",
249
+ "language_model.base_model.model.model.language_model.layers.15.mlp.down_proj.lora_A.default.weight",
250
+ "language_model.base_model.model.model.language_model.layers.15.mlp.down_proj.lora_B.default.weight",
251
+ "language_model.base_model.model.model.language_model.layers.16.linear_attn.out_proj.lora_A.default.weight",
252
+ "language_model.base_model.model.model.language_model.layers.16.linear_attn.out_proj.lora_B.default.weight",
253
+ "language_model.base_model.model.model.language_model.layers.16.linear_attn.in_proj_qkv.lora_A.default.weight",
254
+ "language_model.base_model.model.model.language_model.layers.16.linear_attn.in_proj_qkv.lora_B.default.weight",
255
+ "language_model.base_model.model.model.language_model.layers.16.linear_attn.in_proj_z.lora_A.default.weight",
256
+ "language_model.base_model.model.model.language_model.layers.16.linear_attn.in_proj_z.lora_B.default.weight",
257
+ "language_model.base_model.model.model.language_model.layers.16.linear_attn.in_proj_b.lora_A.default.weight",
258
+ "language_model.base_model.model.model.language_model.layers.16.linear_attn.in_proj_b.lora_B.default.weight",
259
+ "language_model.base_model.model.model.language_model.layers.16.linear_attn.in_proj_a.lora_A.default.weight",
260
+ "language_model.base_model.model.model.language_model.layers.16.linear_attn.in_proj_a.lora_B.default.weight",
261
+ "language_model.base_model.model.model.language_model.layers.16.mlp.gate_proj.lora_A.default.weight",
262
+ "language_model.base_model.model.model.language_model.layers.16.mlp.gate_proj.lora_B.default.weight",
263
+ "language_model.base_model.model.model.language_model.layers.16.mlp.up_proj.lora_A.default.weight",
264
+ "language_model.base_model.model.model.language_model.layers.16.mlp.up_proj.lora_B.default.weight",
265
+ "language_model.base_model.model.model.language_model.layers.16.mlp.down_proj.lora_A.default.weight",
266
+ "language_model.base_model.model.model.language_model.layers.16.mlp.down_proj.lora_B.default.weight",
267
+ "language_model.base_model.model.model.language_model.layers.17.linear_attn.out_proj.lora_A.default.weight",
268
+ "language_model.base_model.model.model.language_model.layers.17.linear_attn.out_proj.lora_B.default.weight",
269
+ "language_model.base_model.model.model.language_model.layers.17.linear_attn.in_proj_qkv.lora_A.default.weight",
270
+ "language_model.base_model.model.model.language_model.layers.17.linear_attn.in_proj_qkv.lora_B.default.weight",
271
+ "language_model.base_model.model.model.language_model.layers.17.linear_attn.in_proj_z.lora_A.default.weight",
272
+ "language_model.base_model.model.model.language_model.layers.17.linear_attn.in_proj_z.lora_B.default.weight",
273
+ "language_model.base_model.model.model.language_model.layers.17.linear_attn.in_proj_b.lora_A.default.weight",
274
+ "language_model.base_model.model.model.language_model.layers.17.linear_attn.in_proj_b.lora_B.default.weight",
275
+ "language_model.base_model.model.model.language_model.layers.17.linear_attn.in_proj_a.lora_A.default.weight",
276
+ "language_model.base_model.model.model.language_model.layers.17.linear_attn.in_proj_a.lora_B.default.weight",
277
+ "language_model.base_model.model.model.language_model.layers.17.mlp.gate_proj.lora_A.default.weight",
278
+ "language_model.base_model.model.model.language_model.layers.17.mlp.gate_proj.lora_B.default.weight",
279
+ "language_model.base_model.model.model.language_model.layers.17.mlp.up_proj.lora_A.default.weight",
280
+ "language_model.base_model.model.model.language_model.layers.17.mlp.up_proj.lora_B.default.weight",
281
+ "language_model.base_model.model.model.language_model.layers.17.mlp.down_proj.lora_A.default.weight",
282
+ "language_model.base_model.model.model.language_model.layers.17.mlp.down_proj.lora_B.default.weight",
283
+ "language_model.base_model.model.model.language_model.layers.18.linear_attn.out_proj.lora_A.default.weight",
284
+ "language_model.base_model.model.model.language_model.layers.18.linear_attn.out_proj.lora_B.default.weight",
285
+ "language_model.base_model.model.model.language_model.layers.18.linear_attn.in_proj_qkv.lora_A.default.weight",
286
+ "language_model.base_model.model.model.language_model.layers.18.linear_attn.in_proj_qkv.lora_B.default.weight",
287
+ "language_model.base_model.model.model.language_model.layers.18.linear_attn.in_proj_z.lora_A.default.weight",
288
+ "language_model.base_model.model.model.language_model.layers.18.linear_attn.in_proj_z.lora_B.default.weight",
289
+ "language_model.base_model.model.model.language_model.layers.18.linear_attn.in_proj_b.lora_A.default.weight",
290
+ "language_model.base_model.model.model.language_model.layers.18.linear_attn.in_proj_b.lora_B.default.weight",
291
+ "language_model.base_model.model.model.language_model.layers.18.linear_attn.in_proj_a.lora_A.default.weight",
292
+ "language_model.base_model.model.model.language_model.layers.18.linear_attn.in_proj_a.lora_B.default.weight",
293
+ "language_model.base_model.model.model.language_model.layers.18.mlp.gate_proj.lora_A.default.weight",
294
+ "language_model.base_model.model.model.language_model.layers.18.mlp.gate_proj.lora_B.default.weight",
295
+ "language_model.base_model.model.model.language_model.layers.18.mlp.up_proj.lora_A.default.weight",
296
+ "language_model.base_model.model.model.language_model.layers.18.mlp.up_proj.lora_B.default.weight",
297
+ "language_model.base_model.model.model.language_model.layers.18.mlp.down_proj.lora_A.default.weight",
298
+ "language_model.base_model.model.model.language_model.layers.18.mlp.down_proj.lora_B.default.weight",
299
+ "language_model.base_model.model.model.language_model.layers.19.self_attn.q_proj.lora_A.default.weight",
300
+ "language_model.base_model.model.model.language_model.layers.19.self_attn.q_proj.lora_B.default.weight",
301
+ "language_model.base_model.model.model.language_model.layers.19.self_attn.k_proj.lora_A.default.weight",
302
+ "language_model.base_model.model.model.language_model.layers.19.self_attn.k_proj.lora_B.default.weight",
303
+ "language_model.base_model.model.model.language_model.layers.19.self_attn.v_proj.lora_A.default.weight",
304
+ "language_model.base_model.model.model.language_model.layers.19.self_attn.v_proj.lora_B.default.weight",
305
+ "language_model.base_model.model.model.language_model.layers.19.self_attn.o_proj.lora_A.default.weight",
306
+ "language_model.base_model.model.model.language_model.layers.19.self_attn.o_proj.lora_B.default.weight",
307
+ "language_model.base_model.model.model.language_model.layers.19.mlp.gate_proj.lora_A.default.weight",
308
+ "language_model.base_model.model.model.language_model.layers.19.mlp.gate_proj.lora_B.default.weight",
309
+ "language_model.base_model.model.model.language_model.layers.19.mlp.up_proj.lora_A.default.weight",
310
+ "language_model.base_model.model.model.language_model.layers.19.mlp.up_proj.lora_B.default.weight",
311
+ "language_model.base_model.model.model.language_model.layers.19.mlp.down_proj.lora_A.default.weight",
312
+ "language_model.base_model.model.model.language_model.layers.19.mlp.down_proj.lora_B.default.weight",
313
+ "language_model.base_model.model.model.language_model.layers.20.linear_attn.out_proj.lora_A.default.weight",
314
+ "language_model.base_model.model.model.language_model.layers.20.linear_attn.out_proj.lora_B.default.weight",
315
+ "language_model.base_model.model.model.language_model.layers.20.linear_attn.in_proj_qkv.lora_A.default.weight",
316
+ "language_model.base_model.model.model.language_model.layers.20.linear_attn.in_proj_qkv.lora_B.default.weight",
317
+ "language_model.base_model.model.model.language_model.layers.20.linear_attn.in_proj_z.lora_A.default.weight",
318
+ "language_model.base_model.model.model.language_model.layers.20.linear_attn.in_proj_z.lora_B.default.weight",
319
+ "language_model.base_model.model.model.language_model.layers.20.linear_attn.in_proj_b.lora_A.default.weight",
320
+ "language_model.base_model.model.model.language_model.layers.20.linear_attn.in_proj_b.lora_B.default.weight",
321
+ "language_model.base_model.model.model.language_model.layers.20.linear_attn.in_proj_a.lora_A.default.weight",
322
+ "language_model.base_model.model.model.language_model.layers.20.linear_attn.in_proj_a.lora_B.default.weight",
323
+ "language_model.base_model.model.model.language_model.layers.20.mlp.gate_proj.lora_A.default.weight",
324
+ "language_model.base_model.model.model.language_model.layers.20.mlp.gate_proj.lora_B.default.weight",
325
+ "language_model.base_model.model.model.language_model.layers.20.mlp.up_proj.lora_A.default.weight",
326
+ "language_model.base_model.model.model.language_model.layers.20.mlp.up_proj.lora_B.default.weight",
327
+ "language_model.base_model.model.model.language_model.layers.20.mlp.down_proj.lora_A.default.weight",
328
+ "language_model.base_model.model.model.language_model.layers.20.mlp.down_proj.lora_B.default.weight",
329
+ "language_model.base_model.model.model.language_model.layers.21.linear_attn.out_proj.lora_A.default.weight",
330
+ "language_model.base_model.model.model.language_model.layers.21.linear_attn.out_proj.lora_B.default.weight",
331
+ "language_model.base_model.model.model.language_model.layers.21.linear_attn.in_proj_qkv.lora_A.default.weight",
332
+ "language_model.base_model.model.model.language_model.layers.21.linear_attn.in_proj_qkv.lora_B.default.weight",
333
+ "language_model.base_model.model.model.language_model.layers.21.linear_attn.in_proj_z.lora_A.default.weight",
334
+ "language_model.base_model.model.model.language_model.layers.21.linear_attn.in_proj_z.lora_B.default.weight",
335
+ "language_model.base_model.model.model.language_model.layers.21.linear_attn.in_proj_b.lora_A.default.weight",
336
+ "language_model.base_model.model.model.language_model.layers.21.linear_attn.in_proj_b.lora_B.default.weight",
337
+ "language_model.base_model.model.model.language_model.layers.21.linear_attn.in_proj_a.lora_A.default.weight",
338
+ "language_model.base_model.model.model.language_model.layers.21.linear_attn.in_proj_a.lora_B.default.weight",
339
+ "language_model.base_model.model.model.language_model.layers.21.mlp.gate_proj.lora_A.default.weight",
340
+ "language_model.base_model.model.model.language_model.layers.21.mlp.gate_proj.lora_B.default.weight",
341
+ "language_model.base_model.model.model.language_model.layers.21.mlp.up_proj.lora_A.default.weight",
342
+ "language_model.base_model.model.model.language_model.layers.21.mlp.up_proj.lora_B.default.weight",
343
+ "language_model.base_model.model.model.language_model.layers.21.mlp.down_proj.lora_A.default.weight",
344
+ "language_model.base_model.model.model.language_model.layers.21.mlp.down_proj.lora_B.default.weight",
345
+ "language_model.base_model.model.model.language_model.layers.22.linear_attn.out_proj.lora_A.default.weight",
346
+ "language_model.base_model.model.model.language_model.layers.22.linear_attn.out_proj.lora_B.default.weight",
347
+ "language_model.base_model.model.model.language_model.layers.22.linear_attn.in_proj_qkv.lora_A.default.weight",
348
+ "language_model.base_model.model.model.language_model.layers.22.linear_attn.in_proj_qkv.lora_B.default.weight",
349
+ "language_model.base_model.model.model.language_model.layers.22.linear_attn.in_proj_z.lora_A.default.weight",
350
+ "language_model.base_model.model.model.language_model.layers.22.linear_attn.in_proj_z.lora_B.default.weight",
351
+ "language_model.base_model.model.model.language_model.layers.22.linear_attn.in_proj_b.lora_A.default.weight",
352
+ "language_model.base_model.model.model.language_model.layers.22.linear_attn.in_proj_b.lora_B.default.weight",
353
+ "language_model.base_model.model.model.language_model.layers.22.linear_attn.in_proj_a.lora_A.default.weight",
354
+ "language_model.base_model.model.model.language_model.layers.22.linear_attn.in_proj_a.lora_B.default.weight",
355
+ "language_model.base_model.model.model.language_model.layers.22.mlp.gate_proj.lora_A.default.weight",
356
+ "language_model.base_model.model.model.language_model.layers.22.mlp.gate_proj.lora_B.default.weight",
357
+ "language_model.base_model.model.model.language_model.layers.22.mlp.up_proj.lora_A.default.weight",
358
+ "language_model.base_model.model.model.language_model.layers.22.mlp.up_proj.lora_B.default.weight",
359
+ "language_model.base_model.model.model.language_model.layers.22.mlp.down_proj.lora_A.default.weight",
360
+ "language_model.base_model.model.model.language_model.layers.22.mlp.down_proj.lora_B.default.weight",
361
+ "language_model.base_model.model.model.language_model.layers.23.self_attn.q_proj.lora_A.default.weight",
362
+ "language_model.base_model.model.model.language_model.layers.23.self_attn.q_proj.lora_B.default.weight",
363
+ "language_model.base_model.model.model.language_model.layers.23.self_attn.k_proj.lora_A.default.weight",
364
+ "language_model.base_model.model.model.language_model.layers.23.self_attn.k_proj.lora_B.default.weight",
365
+ "language_model.base_model.model.model.language_model.layers.23.self_attn.v_proj.lora_A.default.weight",
366
+ "language_model.base_model.model.model.language_model.layers.23.self_attn.v_proj.lora_B.default.weight",
367
+ "language_model.base_model.model.model.language_model.layers.23.self_attn.o_proj.lora_A.default.weight",
368
+ "language_model.base_model.model.model.language_model.layers.23.self_attn.o_proj.lora_B.default.weight",
369
+ "language_model.base_model.model.model.language_model.layers.23.mlp.gate_proj.lora_A.default.weight",
370
+ "language_model.base_model.model.model.language_model.layers.23.mlp.gate_proj.lora_B.default.weight",
371
+ "language_model.base_model.model.model.language_model.layers.23.mlp.up_proj.lora_A.default.weight",
372
+ "language_model.base_model.model.model.language_model.layers.23.mlp.up_proj.lora_B.default.weight",
373
+ "language_model.base_model.model.model.language_model.layers.23.mlp.down_proj.lora_A.default.weight",
374
+ "language_model.base_model.model.model.language_model.layers.23.mlp.down_proj.lora_B.default.weight",
375
+ "language_model.base_model.model.model.language_model.layers.24.linear_attn.out_proj.lora_A.default.weight",
376
+ "language_model.base_model.model.model.language_model.layers.24.linear_attn.out_proj.lora_B.default.weight",
377
+ "language_model.base_model.model.model.language_model.layers.24.linear_attn.in_proj_qkv.lora_A.default.weight",
378
+ "language_model.base_model.model.model.language_model.layers.24.linear_attn.in_proj_qkv.lora_B.default.weight",
379
+ "language_model.base_model.model.model.language_model.layers.24.linear_attn.in_proj_z.lora_A.default.weight",
380
+ "language_model.base_model.model.model.language_model.layers.24.linear_attn.in_proj_z.lora_B.default.weight",
381
+ "language_model.base_model.model.model.language_model.layers.24.linear_attn.in_proj_b.lora_A.default.weight",
382
+ "language_model.base_model.model.model.language_model.layers.24.linear_attn.in_proj_b.lora_B.default.weight",
383
+ "language_model.base_model.model.model.language_model.layers.24.linear_attn.in_proj_a.lora_A.default.weight",
384
+ "language_model.base_model.model.model.language_model.layers.24.linear_attn.in_proj_a.lora_B.default.weight",
385
+ "language_model.base_model.model.model.language_model.layers.24.mlp.gate_proj.lora_A.default.weight",
386
+ "language_model.base_model.model.model.language_model.layers.24.mlp.gate_proj.lora_B.default.weight",
387
+ "language_model.base_model.model.model.language_model.layers.24.mlp.up_proj.lora_A.default.weight",
388
+ "language_model.base_model.model.model.language_model.layers.24.mlp.up_proj.lora_B.default.weight",
389
+ "language_model.base_model.model.model.language_model.layers.24.mlp.down_proj.lora_A.default.weight",
390
+ "language_model.base_model.model.model.language_model.layers.24.mlp.down_proj.lora_B.default.weight",
391
+ "language_model.base_model.model.model.language_model.layers.25.linear_attn.out_proj.lora_A.default.weight",
392
+ "language_model.base_model.model.model.language_model.layers.25.linear_attn.out_proj.lora_B.default.weight",
393
+ "language_model.base_model.model.model.language_model.layers.25.linear_attn.in_proj_qkv.lora_A.default.weight",
394
+ "language_model.base_model.model.model.language_model.layers.25.linear_attn.in_proj_qkv.lora_B.default.weight",
395
+ "language_model.base_model.model.model.language_model.layers.25.linear_attn.in_proj_z.lora_A.default.weight",
396
+ "language_model.base_model.model.model.language_model.layers.25.linear_attn.in_proj_z.lora_B.default.weight",
397
+ "language_model.base_model.model.model.language_model.layers.25.linear_attn.in_proj_b.lora_A.default.weight",
398
+ "language_model.base_model.model.model.language_model.layers.25.linear_attn.in_proj_b.lora_B.default.weight",
399
+ "language_model.base_model.model.model.language_model.layers.25.linear_attn.in_proj_a.lora_A.default.weight",
400
+ "language_model.base_model.model.model.language_model.layers.25.linear_attn.in_proj_a.lora_B.default.weight",
401
+ "language_model.base_model.model.model.language_model.layers.25.mlp.gate_proj.lora_A.default.weight",
402
+ "language_model.base_model.model.model.language_model.layers.25.mlp.gate_proj.lora_B.default.weight",
403
+ "language_model.base_model.model.model.language_model.layers.25.mlp.up_proj.lora_A.default.weight",
404
+ "language_model.base_model.model.model.language_model.layers.25.mlp.up_proj.lora_B.default.weight",
405
+ "language_model.base_model.model.model.language_model.layers.25.mlp.down_proj.lora_A.default.weight",
406
+ "language_model.base_model.model.model.language_model.layers.25.mlp.down_proj.lora_B.default.weight",
407
+ "language_model.base_model.model.model.language_model.layers.26.linear_attn.out_proj.lora_A.default.weight",
408
+ "language_model.base_model.model.model.language_model.layers.26.linear_attn.out_proj.lora_B.default.weight",
409
+ "language_model.base_model.model.model.language_model.layers.26.linear_attn.in_proj_qkv.lora_A.default.weight",
410
+ "language_model.base_model.model.model.language_model.layers.26.linear_attn.in_proj_qkv.lora_B.default.weight",
411
+ "language_model.base_model.model.model.language_model.layers.26.linear_attn.in_proj_z.lora_A.default.weight",
412
+ "language_model.base_model.model.model.language_model.layers.26.linear_attn.in_proj_z.lora_B.default.weight",
413
+ "language_model.base_model.model.model.language_model.layers.26.linear_attn.in_proj_b.lora_A.default.weight",
414
+ "language_model.base_model.model.model.language_model.layers.26.linear_attn.in_proj_b.lora_B.default.weight",
415
+ "language_model.base_model.model.model.language_model.layers.26.linear_attn.in_proj_a.lora_A.default.weight",
416
+ "language_model.base_model.model.model.language_model.layers.26.linear_attn.in_proj_a.lora_B.default.weight",
417
+ "language_model.base_model.model.model.language_model.layers.26.mlp.gate_proj.lora_A.default.weight",
418
+ "language_model.base_model.model.model.language_model.layers.26.mlp.gate_proj.lora_B.default.weight",
419
+ "language_model.base_model.model.model.language_model.layers.26.mlp.up_proj.lora_A.default.weight",
420
+ "language_model.base_model.model.model.language_model.layers.26.mlp.up_proj.lora_B.default.weight",
421
+ "language_model.base_model.model.model.language_model.layers.26.mlp.down_proj.lora_A.default.weight",
422
+ "language_model.base_model.model.model.language_model.layers.26.mlp.down_proj.lora_B.default.weight",
423
+ "language_model.base_model.model.model.language_model.layers.27.self_attn.q_proj.lora_A.default.weight",
424
+ "language_model.base_model.model.model.language_model.layers.27.self_attn.q_proj.lora_B.default.weight",
425
+ "language_model.base_model.model.model.language_model.layers.27.self_attn.k_proj.lora_A.default.weight",
426
+ "language_model.base_model.model.model.language_model.layers.27.self_attn.k_proj.lora_B.default.weight",
427
+ "language_model.base_model.model.model.language_model.layers.27.self_attn.v_proj.lora_A.default.weight",
428
+ "language_model.base_model.model.model.language_model.layers.27.self_attn.v_proj.lora_B.default.weight",
429
+ "language_model.base_model.model.model.language_model.layers.27.self_attn.o_proj.lora_A.default.weight",
430
+ "language_model.base_model.model.model.language_model.layers.27.self_attn.o_proj.lora_B.default.weight",
431
+ "language_model.base_model.model.model.language_model.layers.27.mlp.gate_proj.lora_A.default.weight",
432
+ "language_model.base_model.model.model.language_model.layers.27.mlp.gate_proj.lora_B.default.weight",
433
+ "language_model.base_model.model.model.language_model.layers.27.mlp.up_proj.lora_A.default.weight",
434
+ "language_model.base_model.model.model.language_model.layers.27.mlp.up_proj.lora_B.default.weight",
435
+ "language_model.base_model.model.model.language_model.layers.27.mlp.down_proj.lora_A.default.weight",
436
+ "language_model.base_model.model.model.language_model.layers.27.mlp.down_proj.lora_B.default.weight",
437
+ "language_model.base_model.model.model.language_model.layers.28.linear_attn.out_proj.lora_A.default.weight",
438
+ "language_model.base_model.model.model.language_model.layers.28.linear_attn.out_proj.lora_B.default.weight",
439
+ "language_model.base_model.model.model.language_model.layers.28.linear_attn.in_proj_qkv.lora_A.default.weight",
440
+ "language_model.base_model.model.model.language_model.layers.28.linear_attn.in_proj_qkv.lora_B.default.weight",
441
+ "language_model.base_model.model.model.language_model.layers.28.linear_attn.in_proj_z.lora_A.default.weight",
442
+ "language_model.base_model.model.model.language_model.layers.28.linear_attn.in_proj_z.lora_B.default.weight",
443
+ "language_model.base_model.model.model.language_model.layers.28.linear_attn.in_proj_b.lora_A.default.weight",
444
+ "language_model.base_model.model.model.language_model.layers.28.linear_attn.in_proj_b.lora_B.default.weight",
445
+ "language_model.base_model.model.model.language_model.layers.28.linear_attn.in_proj_a.lora_A.default.weight",
446
+ "language_model.base_model.model.model.language_model.layers.28.linear_attn.in_proj_a.lora_B.default.weight",
447
+ "language_model.base_model.model.model.language_model.layers.28.mlp.gate_proj.lora_A.default.weight",
448
+ "language_model.base_model.model.model.language_model.layers.28.mlp.gate_proj.lora_B.default.weight",
449
+ "language_model.base_model.model.model.language_model.layers.28.mlp.up_proj.lora_A.default.weight",
450
+ "language_model.base_model.model.model.language_model.layers.28.mlp.up_proj.lora_B.default.weight",
451
+ "language_model.base_model.model.model.language_model.layers.28.mlp.down_proj.lora_A.default.weight",
452
+ "language_model.base_model.model.model.language_model.layers.28.mlp.down_proj.lora_B.default.weight",
453
+ "language_model.base_model.model.model.language_model.layers.29.linear_attn.out_proj.lora_A.default.weight",
454
+ "language_model.base_model.model.model.language_model.layers.29.linear_attn.out_proj.lora_B.default.weight",
455
+ "language_model.base_model.model.model.language_model.layers.29.linear_attn.in_proj_qkv.lora_A.default.weight",
456
+ "language_model.base_model.model.model.language_model.layers.29.linear_attn.in_proj_qkv.lora_B.default.weight",
457
+ "language_model.base_model.model.model.language_model.layers.29.linear_attn.in_proj_z.lora_A.default.weight",
458
+ "language_model.base_model.model.model.language_model.layers.29.linear_attn.in_proj_z.lora_B.default.weight",
459
+ "language_model.base_model.model.model.language_model.layers.29.linear_attn.in_proj_b.lora_A.default.weight",
460
+ "language_model.base_model.model.model.language_model.layers.29.linear_attn.in_proj_b.lora_B.default.weight",
461
+ "language_model.base_model.model.model.language_model.layers.29.linear_attn.in_proj_a.lora_A.default.weight",
462
+ "language_model.base_model.model.model.language_model.layers.29.linear_attn.in_proj_a.lora_B.default.weight",
463
+ "language_model.base_model.model.model.language_model.layers.29.mlp.gate_proj.lora_A.default.weight",
464
+ "language_model.base_model.model.model.language_model.layers.29.mlp.gate_proj.lora_B.default.weight",
465
+ "language_model.base_model.model.model.language_model.layers.29.mlp.up_proj.lora_A.default.weight",
466
+ "language_model.base_model.model.model.language_model.layers.29.mlp.up_proj.lora_B.default.weight",
467
+ "language_model.base_model.model.model.language_model.layers.29.mlp.down_proj.lora_A.default.weight",
468
+ "language_model.base_model.model.model.language_model.layers.29.mlp.down_proj.lora_B.default.weight",
469
+ "language_model.base_model.model.model.language_model.layers.30.linear_attn.out_proj.lora_A.default.weight",
470
+ "language_model.base_model.model.model.language_model.layers.30.linear_attn.out_proj.lora_B.default.weight",
471
+ "language_model.base_model.model.model.language_model.layers.30.linear_attn.in_proj_qkv.lora_A.default.weight",
472
+ "language_model.base_model.model.model.language_model.layers.30.linear_attn.in_proj_qkv.lora_B.default.weight",
473
+ "language_model.base_model.model.model.language_model.layers.30.linear_attn.in_proj_z.lora_A.default.weight",
474
+ "language_model.base_model.model.model.language_model.layers.30.linear_attn.in_proj_z.lora_B.default.weight",
475
+ "language_model.base_model.model.model.language_model.layers.30.linear_attn.in_proj_b.lora_A.default.weight",
476
+ "language_model.base_model.model.model.language_model.layers.30.linear_attn.in_proj_b.lora_B.default.weight",
477
+ "language_model.base_model.model.model.language_model.layers.30.linear_attn.in_proj_a.lora_A.default.weight",
478
+ "language_model.base_model.model.model.language_model.layers.30.linear_attn.in_proj_a.lora_B.default.weight",
479
+ "language_model.base_model.model.model.language_model.layers.30.mlp.gate_proj.lora_A.default.weight",
480
+ "language_model.base_model.model.model.language_model.layers.30.mlp.gate_proj.lora_B.default.weight",
481
+ "language_model.base_model.model.model.language_model.layers.30.mlp.up_proj.lora_A.default.weight",
482
+ "language_model.base_model.model.model.language_model.layers.30.mlp.up_proj.lora_B.default.weight",
483
+ "language_model.base_model.model.model.language_model.layers.30.mlp.down_proj.lora_A.default.weight",
484
+ "language_model.base_model.model.model.language_model.layers.30.mlp.down_proj.lora_B.default.weight",
485
+ "language_model.base_model.model.model.language_model.layers.31.self_attn.q_proj.lora_A.default.weight",
486
+ "language_model.base_model.model.model.language_model.layers.31.self_attn.q_proj.lora_B.default.weight",
487
+ "language_model.base_model.model.model.language_model.layers.31.self_attn.k_proj.lora_A.default.weight",
488
+ "language_model.base_model.model.model.language_model.layers.31.self_attn.k_proj.lora_B.default.weight",
489
+ "language_model.base_model.model.model.language_model.layers.31.self_attn.v_proj.lora_A.default.weight",
490
+ "language_model.base_model.model.model.language_model.layers.31.self_attn.v_proj.lora_B.default.weight",
491
+ "language_model.base_model.model.model.language_model.layers.31.self_attn.o_proj.lora_A.default.weight",
492
+ "language_model.base_model.model.model.language_model.layers.31.self_attn.o_proj.lora_B.default.weight",
493
+ "language_model.base_model.model.model.language_model.layers.31.mlp.gate_proj.lora_A.default.weight",
494
+ "language_model.base_model.model.model.language_model.layers.31.mlp.gate_proj.lora_B.default.weight",
495
+ "language_model.base_model.model.model.language_model.layers.31.mlp.up_proj.lora_A.default.weight",
496
+ "language_model.base_model.model.model.language_model.layers.31.mlp.up_proj.lora_B.default.weight",
497
+ "language_model.base_model.model.model.language_model.layers.31.mlp.down_proj.lora_A.default.weight",
498
+ "language_model.base_model.model.model.language_model.layers.31.mlp.down_proj.lora_B.default.weight",
499
+ "head.native_head.prior_logit_scale",
500
+ "head.native_head.joint_logit_scale",
501
+ "head.native_head.residual_gate",
502
+ "head.native_head.hidden_norm.weight",
503
+ "head.native_head.hidden_norm.bias",
504
+ "head.native_head.memory_projection.weight",
505
+ "head.native_head.question_projection.weight",
506
+ "head.native_head.option_question_projection.weight",
507
+ "head.native_head.global_projection.weight",
508
+ "head.native_head.option_context_projection.weight",
509
+ "head.native_head.option_lexical_projection.weight",
510
+ "head.native_head.type_embedding.weight",
511
+ "head.native_head.evidence_layers.0.query_norm.weight",
512
+ "head.native_head.evidence_layers.0.query_norm.bias",
513
+ "head.native_head.evidence_layers.0.memory_norm.weight",
514
+ "head.native_head.evidence_layers.0.memory_norm.bias",
515
+ "head.native_head.evidence_layers.0.attention.in_proj_weight",
516
+ "head.native_head.evidence_layers.0.attention.in_proj_bias",
517
+ "head.native_head.evidence_layers.0.attention.out_proj.weight",
518
+ "head.native_head.evidence_layers.0.attention.out_proj.bias",
519
+ "head.native_head.evidence_layers.0.feedforward_norm.weight",
520
+ "head.native_head.evidence_layers.0.feedforward_norm.bias",
521
+ "head.native_head.evidence_layers.0.feedforward.0.weight",
522
+ "head.native_head.evidence_layers.0.feedforward.0.bias",
523
+ "head.native_head.evidence_layers.0.feedforward.3.weight",
524
+ "head.native_head.evidence_layers.0.feedforward.3.bias",
525
+ "head.native_head.evidence_layers.1.query_norm.weight",
526
+ "head.native_head.evidence_layers.1.query_norm.bias",
527
+ "head.native_head.evidence_layers.1.memory_norm.weight",
528
+ "head.native_head.evidence_layers.1.memory_norm.bias",
529
+ "head.native_head.evidence_layers.1.attention.in_proj_weight",
530
+ "head.native_head.evidence_layers.1.attention.in_proj_bias",
531
+ "head.native_head.evidence_layers.1.attention.out_proj.weight",
532
+ "head.native_head.evidence_layers.1.attention.out_proj.bias",
533
+ "head.native_head.evidence_layers.1.feedforward_norm.weight",
534
+ "head.native_head.evidence_layers.1.feedforward_norm.bias",
535
+ "head.native_head.evidence_layers.1.feedforward.0.weight",
536
+ "head.native_head.evidence_layers.1.feedforward.0.bias",
537
+ "head.native_head.evidence_layers.1.feedforward.3.weight",
538
+ "head.native_head.evidence_layers.1.feedforward.3.bias",
539
+ "head.native_head.option_summary_norm.weight",
540
+ "head.native_head.option_summary_norm.bias",
541
+ "head.native_head.layers.0.self_attn.in_proj_weight",
542
+ "head.native_head.layers.0.self_attn.in_proj_bias",
543
+ "head.native_head.layers.0.self_attn.out_proj.weight",
544
+ "head.native_head.layers.0.self_attn.out_proj.bias",
545
+ "head.native_head.layers.0.multihead_attn.in_proj_weight",
546
+ "head.native_head.layers.0.multihead_attn.in_proj_bias",
547
+ "head.native_head.layers.0.multihead_attn.out_proj.weight",
548
+ "head.native_head.layers.0.multihead_attn.out_proj.bias",
549
+ "head.native_head.layers.0.linear1.weight",
550
+ "head.native_head.layers.0.linear1.bias",
551
+ "head.native_head.layers.0.linear2.weight",
552
+ "head.native_head.layers.0.linear2.bias",
553
+ "head.native_head.layers.0.norm1.weight",
554
+ "head.native_head.layers.0.norm1.bias",
555
+ "head.native_head.layers.0.norm2.weight",
556
+ "head.native_head.layers.0.norm2.bias",
557
+ "head.native_head.layers.0.norm3.weight",
558
+ "head.native_head.layers.0.norm3.bias",
559
+ "head.native_head.layers.1.self_attn.in_proj_weight",
560
+ "head.native_head.layers.1.self_attn.in_proj_bias",
561
+ "head.native_head.layers.1.self_attn.out_proj.weight",
562
+ "head.native_head.layers.1.self_attn.out_proj.bias",
563
+ "head.native_head.layers.1.multihead_attn.in_proj_weight",
564
+ "head.native_head.layers.1.multihead_attn.in_proj_bias",
565
+ "head.native_head.layers.1.multihead_attn.out_proj.weight",
566
+ "head.native_head.layers.1.multihead_attn.out_proj.bias",
567
+ "head.native_head.layers.1.linear1.weight",
568
+ "head.native_head.layers.1.linear1.bias",
569
+ "head.native_head.layers.1.linear2.weight",
570
+ "head.native_head.layers.1.linear2.bias",
571
+ "head.native_head.layers.1.norm1.weight",
572
+ "head.native_head.layers.1.norm1.bias",
573
+ "head.native_head.layers.1.norm2.weight",
574
+ "head.native_head.layers.1.norm2.bias",
575
+ "head.native_head.layers.1.norm3.weight",
576
+ "head.native_head.layers.1.norm3.bias",
577
+ "head.native_head.layers.2.self_attn.in_proj_weight",
578
+ "head.native_head.layers.2.self_attn.in_proj_bias",
579
+ "head.native_head.layers.2.self_attn.out_proj.weight",
580
+ "head.native_head.layers.2.self_attn.out_proj.bias",
581
+ "head.native_head.layers.2.multihead_attn.in_proj_weight",
582
+ "head.native_head.layers.2.multihead_attn.in_proj_bias",
583
+ "head.native_head.layers.2.multihead_attn.out_proj.weight",
584
+ "head.native_head.layers.2.multihead_attn.out_proj.bias",
585
+ "head.native_head.layers.2.linear1.weight",
586
+ "head.native_head.layers.2.linear1.bias",
587
+ "head.native_head.layers.2.linear2.weight",
588
+ "head.native_head.layers.2.linear2.bias",
589
+ "head.native_head.layers.2.norm1.weight",
590
+ "head.native_head.layers.2.norm1.bias",
591
+ "head.native_head.layers.2.norm2.weight",
592
+ "head.native_head.layers.2.norm2.bias",
593
+ "head.native_head.layers.2.norm3.weight",
594
+ "head.native_head.layers.2.norm3.bias",
595
+ "head.native_head.layers.3.self_attn.in_proj_weight",
596
+ "head.native_head.layers.3.self_attn.in_proj_bias",
597
+ "head.native_head.layers.3.self_attn.out_proj.weight",
598
+ "head.native_head.layers.3.self_attn.out_proj.bias",
599
+ "head.native_head.layers.3.multihead_attn.in_proj_weight",
600
+ "head.native_head.layers.3.multihead_attn.in_proj_bias",
601
+ "head.native_head.layers.3.multihead_attn.out_proj.weight",
602
+ "head.native_head.layers.3.multihead_attn.out_proj.bias",
603
+ "head.native_head.layers.3.linear1.weight",
604
+ "head.native_head.layers.3.linear1.bias",
605
+ "head.native_head.layers.3.linear2.weight",
606
+ "head.native_head.layers.3.linear2.bias",
607
+ "head.native_head.layers.3.norm1.weight",
608
+ "head.native_head.layers.3.norm1.bias",
609
+ "head.native_head.layers.3.norm2.weight",
610
+ "head.native_head.layers.3.norm2.bias",
611
+ "head.native_head.layers.3.norm3.weight",
612
+ "head.native_head.layers.3.norm3.bias",
613
+ "head.native_head.field_norm.weight",
614
+ "head.native_head.field_norm.bias",
615
+ "head.native_head.option_norm.weight",
616
+ "head.native_head.option_norm.bias",
617
+ "head.native_head.residual_scorer.0.weight",
618
+ "head.native_head.residual_scorer.0.bias",
619
+ "head.native_head.residual_scorer.3.weight",
620
+ "head.native_head.residual_scorer.3.bias"
621
+ ],
622
+ "config": {
623
+ "batch_size": 1,
624
+ "brier_weight": 0.1,
625
+ "clip_gradient_norm": 1.0,
626
+ "epochs": 2,
627
+ "gradient_accumulation": 8,
628
+ "label_smoothing": 0.05,
629
+ "learning_rate": 1e-05,
630
+ "max_length": 4096,
631
+ "mode": "lora",
632
+ "optimizer": "AdamW",
633
+ "rank": 4,
634
+ "seed": 42,
635
+ "selection": "external validation only; this script does not choose a checkpoint",
636
+ "weight_decay": 0.01
637
+ },
638
+ "dataset_config_sha256": "65b72544c80cf19f4ac3b12ca109d0ebda18fb70eadb4b170a55b1e8bb3b4f6d",
639
+ "dataset_counts": {
640
+ "train": 827,
641
+ "validation": 215
642
+ },
643
+ "dataset_manifest_sha256": "aeddcfc1ea5390f12122d2e786c1e2030dcc97d3790b6d7523432548198b1f94",
644
+ "dataset_source_commit": "70d84bd8dc5d4a60f3b96455a57d9e6f9416d109",
645
+ "dataset_split_sha256": {
646
+ "train": "edd682761db95a4f25bb30a284489c54d9336a36da0a9b19d2cda860b428baa8",
647
+ "validation": "eff9cdc5932e935959ac4d26dce6470d335090f7428a91930001954266d133bc"
648
+ },
649
+ "elapsed_seconds": 1511.2463698700012,
650
+ "epochs": [
651
+ {
652
+ "checkpoint": "runs/study-v1/epoch-01",
653
+ "epoch": 1,
654
+ "examples": 827,
655
+ "mean_training_loss": 2.5779671531346287,
656
+ "optimizer_steps": 104,
657
+ "peak_allocated_bytes": 23230341120,
658
+ "peak_reserved_bytes": 24178065408,
659
+ "seconds": 742.4755487579969,
660
+ "shuffle_order_sha256": "b6e0d7189b1d965472f1132dda2e9be3a35093dae838baf5d8e0c0d3471b6198"
661
+ },
662
+ {
663
+ "checkpoint": "runs/study-v1/epoch-02",
664
+ "epoch": 2,
665
+ "examples": 827,
666
+ "mean_training_loss": 1.9667175294878694,
667
+ "optimizer_steps": 104,
668
+ "peak_allocated_bytes": 23226543104,
669
+ "peak_reserved_bytes": 24178065408,
670
+ "seconds": 744.6554337799898,
671
+ "shuffle_order_sha256": "b9d20efe77959592c99e9247a58d844be69df50db595bd87c6d7e595f5d2e0c4"
672
+ }
673
+ ],
674
+ "frozen_parameters_unchanged": true,
675
+ "gpu": "NVIDIA GeForce RTX 5090",
676
+ "initial_free_vram": 33179500544,
677
+ "package_versions": {
678
+ "peft": "0.21.2",
679
+ "safetensors": "0.8.0",
680
+ "torch": "2.14.1+cu130",
681
+ "transformers": "5.18.0"
682
+ },
683
+ "source_commit": "d9fcd05c02e9327d4d791f2338725092cede8bdd",
684
+ "source_dirty": true,
685
+ "source_hashes": {
686
+ "scripts/train_clef.py": "e83586cf0fd0d7bb8c4aa15f9b858b9f9ffdb915622e355deb9513788d5054aa",
687
+ "src/stackcraft/clef.py": "c505f35d27121ba9ec9db3c663927ac8b28304f4ed864765be61ce96c8c86e13",
688
+ "src/stackcraft/data.py": "74b68a1f391a3128d2e499c7e86b6c94a1f2a2bef33ffe75c8631207065bd991",
689
+ "src/stackcraft/training.py": "f7e48b041b331cf2b68674ec2abb1059dd1da47cb261697b46260b4f0169dba8"
690
+ },
691
+ "status": "trained-awaiting-external-validation",
692
+ "test_trajectories_used": false,
693
+ "total_vram": 33711521792,
694
+ "trainable_parameters": 132582404,
695
+ "validation_used_for_training": false
696
+ }
code/reports/training-launch-source.json ADDED
The diff for this file is too large to render. See raw diff
 
code/reports/training-source-supplement.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "source_commit": "d9fcd05",
3
+ "source_hashes": {
4
+ "src/stackcraft/engine.py": "0805be38ee968d569fa1c12230730f491490f7d27cfcc570c400ac3b4a546453",
5
+ "src/stackcraft/pieces.py": "c5bee0f8767aeeab279cfbadb4c7b4c0bbf1f31c1d29b08dda197f0627654b29",
6
+ "src/stackcraft/schema.py": "35c70d96e8a49e434dc0a3607412a0ce58141306df59e09b1170511694cb6eb5",
7
+ "src/stackcraft/players/__init__.py": "550bed9237303a845cdcc101542b0b688173a8f5436dd1d08c5f85a5a19fa2b2",
8
+ "uv.lock": "bc7fae476529578be003856b5f19e09c8cc96e0020ab999e20e1f8210696f9c8"
9
+ },
10
+ "verified_equal_to_current_at_training_start": true
11
+ }
code/reports/validation-summary.json ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "selected_key": "epoch-02",
3
+ "decision_rule": "lowest finite mean target NLL over all 215 validation positions; exact tie chooses earlier epoch",
4
+ "selection_sha256": "025b62587a26bd8cc78c80f362524b8c178190df5188b317299121afa6b1203b",
5
+ "checkpoint_sha256": {
6
+ "adapter/README.md": "bf1fa63446e8ee38c5ac154ade9138a0e2354e3215201f59473102eeb5206fa0",
7
+ "adapter/adapter_config.json": "bbbce7f19e186b4a79c39d03786bb190d6084e7990a601e8e28672cf1c969741",
8
+ "adapter/adapter_model.safetensors": "3ed169fe68b20a370bb856f9d91842543374ff3c98fd3bbe139ef2d0b97877a3",
9
+ "joint_head.safetensors": "4f21c0f315cf1bcf852a9887d158928855eaa9bdcab6b9770d518abf3727fa1b",
10
+ "reference.json": "adacc63684d28598f8016602bab790cd3967317986b6934dd94a5e14fabe571f",
11
+ "training_config.json": "3ead31d0a3ccb6a62914c3853ee26332f6412382c5196e40b461182e64d8b329"
12
+ },
13
+ "dataset_manifest_sha256": "aeddcfc1ea5390f12122d2e786c1e2030dcc97d3790b6d7523432548198b1f94",
14
+ "validation": {
15
+ "epoch-01": {
16
+ "report_sha256": "ce401ef9fe1d25c11c7a0e3845def347967d8617223307ad823e23a90c3f0033",
17
+ "players": {
18
+ "base": {
19
+ "available_subset_mean_brier": 0.9231725575578064,
20
+ "complete_probability_coverage": true,
21
+ "correct": 21,
22
+ "error_rate": 0.0,
23
+ "failed_predictions": [],
24
+ "finite_subset_mean_nll": 2.9527020509302693,
25
+ "finite_subset_nll_positions": 215,
26
+ "mean_brier": 0.9231725575578064,
27
+ "mean_nll": 2.9527020509302693,
28
+ "missing_probabilities": [],
29
+ "nll_is_infinite": false,
30
+ "positions": 215,
31
+ "probability_positions": 215,
32
+ "selection_performed": false,
33
+ "teacher_agreement": 0.09767441860465116,
34
+ "zero_target_probability_count": 0
35
+ },
36
+ "base-fp32": {
37
+ "available_subset_mean_brier": 0.9231017245023939,
38
+ "complete_probability_coverage": true,
39
+ "correct": 22,
40
+ "error_rate": 0.0,
41
+ "failed_predictions": [],
42
+ "finite_subset_mean_nll": 2.9523931327950934,
43
+ "finite_subset_nll_positions": 215,
44
+ "mean_brier": 0.9231017245023939,
45
+ "mean_nll": 2.9523931327950934,
46
+ "missing_probabilities": [],
47
+ "nll_is_infinite": false,
48
+ "positions": 215,
49
+ "probability_positions": 215,
50
+ "selection_performed": false,
51
+ "teacher_agreement": 0.10232558139534884,
52
+ "zero_target_probability_count": 0
53
+ },
54
+ "trained": {
55
+ "available_subset_mean_brier": 0.7221895727255984,
56
+ "complete_probability_coverage": true,
57
+ "correct": 89,
58
+ "error_rate": 0.0,
59
+ "failed_predictions": [],
60
+ "finite_subset_mean_nll": 1.8608697515370076,
61
+ "finite_subset_nll_positions": 215,
62
+ "mean_brier": 0.7221895727255984,
63
+ "mean_nll": 1.8608697515370076,
64
+ "missing_probabilities": [],
65
+ "nll_is_infinite": false,
66
+ "positions": 215,
67
+ "probability_positions": 215,
68
+ "selection_performed": false,
69
+ "teacher_agreement": 0.413953488372093,
70
+ "zero_target_probability_count": 0
71
+ }
72
+ },
73
+ "provenance": {
74
+ "base_revision": "17f0b0ad64efb65d273590632833508766b2aae6",
75
+ "encoding_version": "stackcraft-clef-v1",
76
+ "installed_versions": {
77
+ "huggingface-hub": "1.33.0",
78
+ "peft": "0.21.2",
79
+ "safetensors": "0.8.0",
80
+ "torch": "2.14.1+cu130",
81
+ "transformers": "5.18.0"
82
+ },
83
+ "precision_policy": "native BF16 base; FP32 trained head; optional base-fp32 ablation",
84
+ "rules_version": "stackcraft-v1",
85
+ "source_commit": "ada38885b55ae3e4b6705a18dec98d194d37447e",
86
+ "source_dirty": false,
87
+ "source_hashes": {
88
+ "scripts/evaluate_clef.py": "70a47e9314ec9d7351a939bae32f07f14bd08f6106b60ce6155687c0540b92dd",
89
+ "src/stackcraft/clef.py": "c505f35d27121ba9ec9db3c663927ac8b28304f4ed864765be61ce96c8c86e13",
90
+ "src/stackcraft/data.py": "74b68a1f391a3128d2e499c7e86b6c94a1f2a2bef33ffe75c8631207065bd991",
91
+ "src/stackcraft/engine.py": "0805be38ee968d569fa1c12230730f491490f7d27cfcc570c400ac3b4a546453",
92
+ "src/stackcraft/evaluation.py": "df56f9d36655e79573a014a5c55c2571bc5e04e2c20f1b37e2d939b871e889e6",
93
+ "src/stackcraft/pieces.py": "c5bee0f8767aeeab279cfbadb4c7b4c0bbf1f31c1d29b08dda197f0627654b29",
94
+ "src/stackcraft/players/__init__.py": "550bed9237303a845cdcc101542b0b688173a8f5436dd1d08c5f85a5a19fa2b2",
95
+ "src/stackcraft/provenance.py": "0728104f22361ad2a8b4471e6eef0467852d94f650d0259d7f5f674c0086f13b",
96
+ "src/stackcraft/schema.py": "35c70d96e8a49e434dc0a3607412a0ce58141306df59e09b1170511694cb6eb5",
97
+ "src/stackcraft/tournament.py": "288ed447a32e0845f52097372a644fc211b1632790655f620801d3ce1639d559",
98
+ "src/stackcraft/training.py": "f7e48b041b331cf2b68674ec2abb1059dd1da47cb261697b46260b4f0169dba8",
99
+ "uv.lock": "bc7fae476529578be003856b5f19e09c8cc96e0020ab999e20e1f8210696f9c8"
100
+ },
101
+ "source_identity_method": "exact-git-root"
102
+ }
103
+ },
104
+ "epoch-02": {
105
+ "report_sha256": "9482be5ac08b85a7bafc18d8f70a35ba277f476d9a96dd0684553e937abc4d2a",
106
+ "players": {
107
+ "trained": {
108
+ "available_subset_mean_brier": 0.7089798657211578,
109
+ "complete_probability_coverage": true,
110
+ "correct": 92,
111
+ "error_rate": 0.0,
112
+ "failed_predictions": [],
113
+ "finite_subset_mean_nll": 1.7759231168523646,
114
+ "finite_subset_nll_positions": 215,
115
+ "mean_brier": 0.7089798657211578,
116
+ "mean_nll": 1.7759231168523646,
117
+ "missing_probabilities": [],
118
+ "nll_is_infinite": false,
119
+ "positions": 215,
120
+ "probability_positions": 215,
121
+ "selection_performed": false,
122
+ "teacher_agreement": 0.42790697674418604,
123
+ "zero_target_probability_count": 0
124
+ }
125
+ },
126
+ "provenance": {
127
+ "base_revision": "17f0b0ad64efb65d273590632833508766b2aae6",
128
+ "encoding_version": "stackcraft-clef-v1",
129
+ "installed_versions": {
130
+ "huggingface-hub": "1.33.0",
131
+ "peft": "0.21.2",
132
+ "safetensors": "0.8.0",
133
+ "torch": "2.14.1+cu130",
134
+ "transformers": "5.18.0"
135
+ },
136
+ "precision_policy": "native BF16 base; FP32 trained head; optional base-fp32 ablation",
137
+ "rules_version": "stackcraft-v1",
138
+ "source_commit": "7a21f6491bcaf2afacc9aa5b5e2733a35c41a7a8",
139
+ "source_dirty": false,
140
+ "source_hashes": {
141
+ "scripts/evaluate_clef.py": "70a47e9314ec9d7351a939bae32f07f14bd08f6106b60ce6155687c0540b92dd",
142
+ "src/stackcraft/clef.py": "c505f35d27121ba9ec9db3c663927ac8b28304f4ed864765be61ce96c8c86e13",
143
+ "src/stackcraft/data.py": "74b68a1f391a3128d2e499c7e86b6c94a1f2a2bef33ffe75c8631207065bd991",
144
+ "src/stackcraft/engine.py": "0805be38ee968d569fa1c12230730f491490f7d27cfcc570c400ac3b4a546453",
145
+ "src/stackcraft/evaluation.py": "df56f9d36655e79573a014a5c55c2571bc5e04e2c20f1b37e2d939b871e889e6",
146
+ "src/stackcraft/pieces.py": "c5bee0f8767aeeab279cfbadb4c7b4c0bbf1f31c1d29b08dda197f0627654b29",
147
+ "src/stackcraft/players/__init__.py": "550bed9237303a845cdcc101542b0b688173a8f5436dd1d08c5f85a5a19fa2b2",
148
+ "src/stackcraft/provenance.py": "0728104f22361ad2a8b4471e6eef0467852d94f650d0259d7f5f674c0086f13b",
149
+ "src/stackcraft/schema.py": "35c70d96e8a49e434dc0a3607412a0ce58141306df59e09b1170511694cb6eb5",
150
+ "src/stackcraft/tournament.py": "288ed447a32e0845f52097372a644fc211b1632790655f620801d3ce1639d559",
151
+ "src/stackcraft/training.py": "f7e48b041b331cf2b68674ec2abb1059dd1da47cb261697b46260b4f0169dba8",
152
+ "uv.lock": "bc7fae476529578be003856b5f19e09c8cc96e0020ab999e20e1f8210696f9c8"
153
+ },
154
+ "source_identity_method": "exact-git-root"
155
+ }
156
+ }
157
+ }
158
+ }
code/scripts/benchmark_clef.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Actual pinned unchanged Clef development tournament; no held-out test seeds."""
2
+
3
+ import argparse
4
+ import json
5
+ import time
6
+ from pathlib import Path
7
+
8
+ import torch
9
+
10
+ from stackcraft.clef import ClefPlayer
11
+ from stackcraft.engine import new_game
12
+ from stackcraft.players import HeuristicPlayer, RandomPlayer, observe
13
+ from stackcraft.tournament import run_tournament
14
+
15
+
16
+ def main():
17
+ parser = argparse.ArgumentParser(description=__doc__)
18
+ parser.add_argument("--output", type=Path, required=True)
19
+ parser.add_argument("--episodes", type=int, default=2)
20
+ parser.add_argument("--max-pieces", type=int, default=30)
21
+ args = parser.parse_args()
22
+ if args.output.exists() or not 1 <= args.episodes <= 20:
23
+ parser.error("use a new output and1–20 development episodes")
24
+ args.output.parent.mkdir(parents=True, exist_ok=True)
25
+ torch.set_num_threads(8)
26
+ free, total = torch.cuda.mem_get_info()
27
+ if free < 25 * 1024**3:
28
+ raise RuntimeError(f"insufficient free GPU memory: {free}")
29
+ started = time.monotonic()
30
+ player = ClefPlayer.from_pretrained(trust_pinned_code=True)
31
+ warmup = player.choose(observe(new_game(42)))
32
+ print(
33
+ json.dumps({"warmup": warmup.action_id, "load_seconds": time.monotonic() - started}),
34
+ flush=True,
35
+ )
36
+ result = run_tournament(
37
+ {"random": RandomPlayer, "heuristic": HeuristicPlayer, "clef-base": lambda: player},
38
+ list(range(args.episodes)),
39
+ args.max_pieces,
40
+ )
41
+ result["development_only"] = True
42
+ result["gpu"] = torch.cuda.get_device_name()
43
+ result["peak_allocated_bytes"] = torch.cuda.max_memory_allocated()
44
+ result["peak_reserved_bytes"] = torch.cuda.max_memory_reserved()
45
+ result["elapsed_seconds"] = time.monotonic() - started
46
+ args.output.write_text(json.dumps(result, indent=2, allow_nan=False) + "\n")
47
+ print(json.dumps(result["summary"]), flush=True)
48
+
49
+
50
+ if __name__ == "__main__":
51
+ main()
code/scripts/build_release.py ADDED
@@ -0,0 +1,640 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Validate completed evidence and stage local release bundles; never upload anything."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import hashlib
7
+ import importlib.util
8
+ import json
9
+ import shutil
10
+ import stat
11
+ import zipfile
12
+ from pathlib import Path, PurePosixPath
13
+ from types import ModuleType, SimpleNamespace
14
+ from typing import Any
15
+
16
+ from stackcraft.data import audit_dataset
17
+ from stackcraft.evaluation import FINAL_TEST_SEEDS, paired_report
18
+ from stackcraft.players import Decision, observe, validate_decision
19
+ from stackcraft.provenance import source_identity
20
+ from stackcraft.schema import GameState
21
+
22
+ ROOT = Path(__file__).resolve().parents[1]
23
+ PLAYERS = ("base", "base-fp32", "trained", "random", "heuristic")
24
+ COMPARISONS = {
25
+ "trained_vs_base": ("trained", "base"),
26
+ "trained_vs_heuristic": ("trained", "heuristic"),
27
+ "trained_vs_base_fp32": ("trained", "base-fp32"),
28
+ "base_fp32_vs_base": ("base-fp32", "base"),
29
+ }
30
+ CHECKPOINT_FILES = {
31
+ "training_config.json",
32
+ "joint_head.safetensors",
33
+ "reference.json",
34
+ "adapter/adapter_config.json",
35
+ "adapter/adapter_model.safetensors",
36
+ "adapter/README.md",
37
+ }
38
+ REPRO_SCRIPTS = (
39
+ "benchmark_clef.py",
40
+ "probe_clef_training.py",
41
+ "train_clef.py",
42
+ "evaluate_clef.py",
43
+ "select_checkpoint.py",
44
+ "export_demo.py",
45
+ "build_release.py",
46
+ "verify_release.py",
47
+ "prepare_checkpoint.py",
48
+ "gpu_session.py",
49
+ )
50
+
51
+
52
+ def sha256(path: Path) -> str:
53
+ digest = hashlib.sha256()
54
+ with path.open("rb") as stream:
55
+ while chunk := stream.read(1_048_576):
56
+ digest.update(chunk)
57
+ return digest.hexdigest()
58
+
59
+
60
+ def json_file(path: Path) -> Any:
61
+ return json.loads(path.read_text())
62
+
63
+
64
+ def write_json(path: Path, value: Any) -> None:
65
+ path.parent.mkdir(parents=True, exist_ok=True)
66
+ path.write_text(json.dumps(value, indent=2, sort_keys=True, allow_nan=False) + "\n")
67
+
68
+
69
+ def load_helper(name: str) -> ModuleType:
70
+ spec = importlib.util.spec_from_file_location(
71
+ f"stackcraft_release_{name}", ROOT / "scripts" / name
72
+ )
73
+ if spec is None or spec.loader is None:
74
+ raise ValueError(f"cannot load reviewed release helper {name}")
75
+ module = importlib.util.module_from_spec(spec)
76
+ spec.loader.exec_module(module)
77
+ return module
78
+
79
+
80
+ def copy_file(source: Path, destination: Path) -> None:
81
+ if source.is_symlink() or not source.is_file():
82
+ raise ValueError(f"release inputs must be regular files, not links: {source}")
83
+ destination.parent.mkdir(parents=True, exist_ok=True)
84
+ shutil.copyfile(source, destination)
85
+
86
+
87
+ def safe_relative(path: str) -> Path:
88
+ relative = Path(path)
89
+ if relative.is_absolute() or ".." in relative.parts or not relative.parts:
90
+ raise ValueError("release evidence paths must stay within their evidence directory")
91
+ return relative
92
+
93
+
94
+ def validate_reference(checkpoint: Path, dataset: Path, records: dict[str, Any]) -> None:
95
+ reference = json_file(checkpoint / "reference.json")
96
+ rows = records["train"][:4]
97
+ manifest_hash = sha256(dataset / "manifest.json")
98
+ if (
99
+ reference.get("row_ids") != [row["id"] for row in rows]
100
+ or reference.get("dataset_manifest_sha256") != manifest_hash
101
+ or reference.get("dataset_train_sha256") != sha256(dataset / "train.jsonl")
102
+ or reference.get("absolute_tolerance") != 1e-4
103
+ ):
104
+ raise ValueError("checkpoint parity reference is not bound to the fixed training dataset")
105
+ metadata = json_file(checkpoint / "training_config.json")
106
+ if metadata.get("extra", {}).get("dataset_manifest_sha256") != manifest_hash:
107
+ raise ValueError("checkpoint was trained with a different dataset manifest")
108
+ config = metadata.get("extra", {}).get("config", {})
109
+ if reference.get("max_length") != config.get("max_length"):
110
+ raise ValueError("checkpoint reference context limit differs from training configuration")
111
+ probabilities = reference.get("probabilities")
112
+ if not isinstance(probabilities, list) or len(probabilities) != len(rows):
113
+ raise ValueError("checkpoint needs all four fixed-row probability references")
114
+ for row, distribution in zip(rows, probabilities, strict=True):
115
+ raw = row["observation"]
116
+ state = GameState(
117
+ tuple(tuple(r) for r in raw["board"]), 0, 0, raw["current"], raw["next_piece"]
118
+ )
119
+ observation = observe(state)
120
+ if not isinstance(distribution, dict) or not distribution:
121
+ raise ValueError("checkpoint reference must contain complete probabilities")
122
+ validate_decision(
123
+ Decision(max(distribution, key=distribution.__getitem__), distribution), observation
124
+ )
125
+
126
+
127
+ def validate_evaluation(
128
+ evaluation: Path, selection_path: Path, checkpoint: Path, helpers: dict[str, ModuleType]
129
+ ) -> tuple[dict[str, Any], dict[str, str], list[Path]]:
130
+ evaluator, exporter = helpers["evaluate"], helpers["export"]
131
+ hashes = evaluator.checkpoint_hashes(checkpoint)
132
+ if not set(hashes).issubset(CHECKPOINT_FILES):
133
+ raise ValueError("checkpoint contains unexpected files outside the release allowlist")
134
+ if not {"training_config.json", "joint_head.safetensors", "reference.json"}.issubset(hashes):
135
+ raise ValueError("checkpoint lacks trained head, metadata or reload reference")
136
+ metadata = json_file(checkpoint / "training_config.json")
137
+ if metadata.get("mode") == "lora" and not {
138
+ "adapter/adapter_config.json",
139
+ "adapter/adapter_model.safetensors",
140
+ "adapter/README.md",
141
+ }.issubset(hashes):
142
+ raise ValueError("LoRA checkpoint is missing adapter weights, configuration or attribution")
143
+ selection = json_file(selection_path)
144
+ report = json_file(evaluation / "report.json")
145
+ request = json_file(evaluation / "request.json")
146
+ if (
147
+ report.get("mode") != "tournament"
148
+ or report.get("final_test") is not True
149
+ or report.get("seeds") != list(FINAL_TEST_SEEDS)
150
+ or report.get("max_pieces") != 200
151
+ or set(report.get("players", {})) != set(PLAYERS)
152
+ or report.get("checkpoint_sha256") != hashes
153
+ or report.get("selection") != selection
154
+ ):
155
+ raise ValueError("evaluation must be the complete frozen five-player, 200-seed final test")
156
+ if (
157
+ request.get("final_test") is not True
158
+ or request.get("mode") != "tournament"
159
+ or request.get("seeds") != list(FINAL_TEST_SEEDS)
160
+ or request.get("max_pieces") != 200
161
+ or set(request.get("players", [])) != set(PLAYERS)
162
+ or request.get("checkpoint_sha256") != hashes
163
+ or request.get("selection_sha256") != sha256(selection_path)
164
+ ):
165
+ raise ValueError("evaluation request is not bound to the checkpoint and frozen selection")
166
+ args = SimpleNamespace(
167
+ seeds=tuple(FINAL_TEST_SEEDS),
168
+ final_test=True,
169
+ max_pieces=200,
170
+ players=request["players"],
171
+ selection_file=selection_path,
172
+ checkpoint=checkpoint,
173
+ max_length=request["max_length"],
174
+ )
175
+ evaluator.validate_selection(args, hashes)
176
+ episodes = report.get("episodes", [])
177
+ expected_pairs = {(player, seed) for player in PLAYERS for seed in FINAL_TEST_SEEDS}
178
+ by_pair = {(episode.get("player_id"), episode.get("seed")): episode for episode in episodes}
179
+ if len(episodes) != len(expected_pairs) or set(by_pair) != expected_pairs:
180
+ raise ValueError("final report must retain every player/seed pair exactly once")
181
+ paths = [Path("report.json"), Path("request.json")]
182
+ for player in PLAYERS:
183
+ metadata_path = Path(player) / "player.json"
184
+ metadata = json_file(evaluation / metadata_path)
185
+ if metadata != report["players"][player]:
186
+ raise ValueError("evaluation player metadata differs from its final report")
187
+ paths.append(metadata_path)
188
+ for seed in FINAL_TEST_SEEDS:
189
+ relative = Path(player) / f"seed-{seed}.json"
190
+ episode = json_file(evaluation / relative)
191
+ if episode != by_pair[(player, seed)]:
192
+ raise ValueError("episode file differs from final report")
193
+ if episode["player"]["revision"] != metadata["revision"] or episode["player"].get(
194
+ "runtime_config", {}
195
+ ) != metadata.get("runtime_config", {}):
196
+ raise ValueError("episode player identity differs from recorded metadata")
197
+ # Recompute every board, visible observation, action and probability check.
198
+ exporter.validated_player(episode, 0)
199
+ paths.append(relative)
200
+ evaluator.validate_neural_runtimes(report["players"])
201
+ for key, (trained, base) in COMPARISONS.items():
202
+ computed = paired_report(episodes, trained_id=trained, base_id=base)
203
+ if computed != report.get(key):
204
+ raise ValueError(f"reported {key} does not match recomputed paired outcomes")
205
+ return report, hashes, paths
206
+
207
+
208
+ def selection_evidence_files(selection_path: Path) -> list[Path]:
209
+ """Preserve self-contained selection evidence with original relative links."""
210
+ selection = json_file(selection_path)
211
+ files = {Path(selection_path.name)}
212
+ pointers = [selection["validation_evidence"]]
213
+ for candidate in selection["candidates"]:
214
+ pointers.extend((candidate["validation_evidence"], candidate["checkpoint_metadata"]))
215
+ for pointer in pointers:
216
+ relative = safe_relative(pointer["path"])
217
+ source = selection_path.parent / relative
218
+ if sha256(source) != pointer["sha256"]:
219
+ raise ValueError("selection evidence changed after its hash was frozen")
220
+ files.add(relative)
221
+ value = json_file(source)
222
+ if value.get("mode") == "positions":
223
+ for player in value["players"].values():
224
+ predictions = relative.parent / safe_relative(player["predictions_file"])
225
+ if sha256(selection_path.parent / predictions) != player["predictions_sha256"]:
226
+ raise ValueError("validation predictions do not match their recorded hash")
227
+ files.add(predictions)
228
+ if (selection_path.parent / "selection-audit.json").is_file():
229
+ files.add(Path("selection-audit.json"))
230
+ return sorted(files)
231
+
232
+
233
+ def _evidence_name(name: str) -> None:
234
+ path = PurePosixPath(name)
235
+ if (
236
+ "\\" in name
237
+ or ":" in name
238
+ or "\x00" in name
239
+ or path.is_absolute()
240
+ or ".." in path.parts
241
+ or len(path.parts) < 2
242
+ or path.parts[0] != "evidence"
243
+ or path.as_posix() != name
244
+ ):
245
+ raise ValueError("archive entries must be canonical files below evidence/")
246
+
247
+
248
+ def verify_evidence_archive(archive: Path, expected: dict[str, dict[str, Any]]) -> None:
249
+ """Read every entry back, checking exact names, lengths, CRC and raw SHA256."""
250
+ for name in expected:
251
+ _evidence_name(name)
252
+ with zipfile.ZipFile(archive) as source:
253
+ infos = source.infolist()
254
+ if len(infos) != len(expected) or {info.filename for info in infos} != set(expected):
255
+ raise ValueError("archive entries differ from the exact raw evidence inventory")
256
+ for info in infos:
257
+ _evidence_name(info.filename)
258
+ entry = expected[info.filename]
259
+ if (
260
+ info.is_dir()
261
+ or info.compress_type != zipfile.ZIP_DEFLATED
262
+ or info.flag_bits & 1
263
+ or stat.S_IFMT(info.external_attr >> 16) != stat.S_IFREG
264
+ or info.file_size != entry["bytes"]
265
+ ):
266
+ raise ValueError("archive entry is not the declared regular evidence file")
267
+ digest = hashlib.sha256()
268
+ size = 0
269
+ with source.open(info) as stream:
270
+ while chunk := stream.read(1_048_576):
271
+ size += len(chunk)
272
+ digest.update(chunk)
273
+ if size != entry["bytes"] or digest.hexdigest() != entry["sha256"]:
274
+ raise ValueError("archive entry bytes differ from the raw evidence SHA256")
275
+
276
+
277
+ def pack_evidence(model: Path) -> dict[str, Any]:
278
+ """Pack staged copies only; leave originals in place until the caller removes them."""
279
+ root = model / "evidence"
280
+ archive = model / "evidence.zip"
281
+ index = model / "evidence-files.json"
282
+ if archive.exists() or index.exists():
283
+ raise ValueError("evidence archive or inventory already exists")
284
+ paths = sorted(root.rglob("*"))
285
+ if root.is_symlink() or any(path.is_symlink() for path in paths):
286
+ raise ValueError("evidence archive inputs cannot contain symlinks")
287
+ files = [path for path in paths if path.is_file()]
288
+ if not files or any(not path.is_file() and not path.is_dir() for path in paths):
289
+ raise ValueError("evidence archive needs regular staged files")
290
+ expected = {}
291
+ with zipfile.ZipFile(archive, "x", compression=zipfile.ZIP_DEFLATED, compresslevel=6) as target:
292
+ for path in files:
293
+ name = path.relative_to(model).as_posix()
294
+ _evidence_name(name)
295
+ info = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
296
+ info.compress_type = zipfile.ZIP_DEFLATED
297
+ info.compress_level = 6
298
+ info.create_system = 3
299
+ info.external_attr = (stat.S_IFREG | 0o644) << 16
300
+ digest = hashlib.sha256()
301
+ size = 0
302
+ with path.open("rb") as source, target.open(info, "w", force_zip64=True) as stream:
303
+ while chunk := source.read(1_048_576):
304
+ digest.update(chunk)
305
+ size += len(chunk)
306
+ stream.write(chunk)
307
+ expected[name] = {"sha256": digest.hexdigest(), "bytes": size}
308
+ verify_evidence_archive(archive, expected)
309
+ # A concurrent edit must not be hidden by deleting a changed staged source.
310
+ for name, entry in expected.items():
311
+ path = model / name
312
+ if path.stat().st_size != entry["bytes"] or sha256(path) != entry["sha256"]:
313
+ raise ValueError("staged evidence changed while packing the archive")
314
+ manifest = {
315
+ "schema_version": 1,
316
+ "archive": "evidence.zip",
317
+ "compression": "ZIP_DEFLATED",
318
+ "compression_level": 6,
319
+ "archive_sha256": sha256(archive),
320
+ "archive_bytes": archive.stat().st_size,
321
+ "files": expected,
322
+ "extraction_root": "model repository root",
323
+ }
324
+ write_json(index, manifest)
325
+ return manifest
326
+
327
+
328
+ def render_model_card(template: str, report: dict[str, Any], selection: dict[str, Any]) -> str:
329
+ summary = report["trained_vs_base"]
330
+ rows = [
331
+ "| Player | Lines mean (median) | Score mean (median) | "
332
+ "Placed mean (median) | Cap hits | Errors |",
333
+ "| --- | ---: | ---: | ---: | ---: | ---: |",
334
+ ]
335
+ timing = [
336
+ "| Player | Decision mean ms | Median ms | p95 ms | Invalid decisions |",
337
+ "| --- | ---: | ---: | ---: | ---: |",
338
+ ]
339
+ for name in PLAYERS:
340
+ player = summary["players"][name]
341
+ primary = player["failure_adjusted"]
342
+ outcomes = " | ".join(
343
+ f"{primary[metric]['mean']:.3f} ({primary[metric]['median']:.1f})"
344
+ for metric in ("lines", "score", "pieces")
345
+ )
346
+ rows.append(
347
+ f"| {name} | {outcomes} | {player['cap_hit_rate']:.1%} | {player['error_rate']:.1%} |"
348
+ )
349
+ latency = player["latency_seconds"]
350
+ values = " | ".join(
351
+ f"{latency[key] * 1000:.3f}" if latency[key] is not None else "unavailable"
352
+ for key in ("mean", "median", "p95")
353
+ )
354
+ timing.append(f"| {name} | {values} | {player['invalid_decisions']} |")
355
+ comparisons = [
356
+ "| Comparison (first minus second) | Mean lines difference | Paired 95% interval |",
357
+ "| --- | ---: | ---: |",
358
+ ]
359
+ for key, (first, second) in COMPARISONS.items():
360
+ delta = report[key]["paired_trained_minus_base"]["lines"]
361
+ comparisons.append(
362
+ f"| {first} − {second} | {delta['mean_difference']:.3f} | "
363
+ f"[{delta['ci95_lower']:.3f}, {delta['ci95_upper']:.3f}] |"
364
+ )
365
+ results = (
366
+ "Verified final outcomes on all 200 paired seeds, with a 200-piece cap. "
367
+ "Lines, score and placed-piece summaries use the preregistered zero-on-error policy. "
368
+ "Cap hits indicate censored survival.\n\n"
369
+ + "\n".join(rows)
370
+ + "\n\nEach paired 95% bootstrap interval resamples complete episode differences. "
371
+ "Trained versus native base is the primary comparison; the heuristic and FP32-head "
372
+ "comparisons are separate checks. Intervals are not adjusted for multiple comparisons.\n\n"
373
+ + "\n".join(comparisons)
374
+ + "\n\nDecision latency includes recorded first-call effects, tokenization and policy "
375
+ "overhead, but excludes game rendering and replay playback.\n\n"
376
+ + "\n".join(timing)
377
+ + "\n\nThe full reports retain raw outcomes, all failed seeds and probability logs."
378
+ )
379
+ card = template.replace(
380
+ "**Release preparation: full-study results and selected checkpoint are pending.**",
381
+ f"Selected checkpoint: **{selection['selected_key']}**, "
382
+ "chosen on validation before final tests.",
383
+ ).replace("**Insert verified held-out results here before publication.**", results)
384
+ if "pending" in card.lower() or "insert verified" in card.lower():
385
+ raise ValueError("model card still contains pending release text")
386
+ return card + (
387
+ "\n\n## Bundle layout\n\n"
388
+ "`checkpoint/` preserves the selected checkpoint bytes. `code/` contains the source, "
389
+ "locked environment, tests, scripts and milestone tutorials. `evidence.zip` losslessly "
390
+ "compresses the frozen selection, both validation candidates and complete final "
391
+ "evaluation. From the model repository root, run `python -m zipfile -e evidence.zip .` after download verification to restore "
392
+ "`evidence/` and all original relative paths. `evidence-files.json` records each raw "
393
+ "file's byte count and SHA256; extraction preserves the original evidence hashes. "
394
+ "From `code/`, run `uv sync --locked --extra ml`; load `../checkpoint` using the "
395
+ "native Stackcraft loader. No access to a private GitHub repository is required.\n"
396
+ )
397
+
398
+
399
+ def code_files(root: Path, *, demo: bool) -> list[Path]:
400
+ """Explicit file classes; no planning, environments, arbitrary data, runs or credentials."""
401
+ files = [
402
+ Path(name)
403
+ for name in (
404
+ "pyproject.toml",
405
+ "uv.lock",
406
+ ".python-version",
407
+ "README.md",
408
+ "LICENSE",
409
+ "NOTICE",
410
+ "Dockerfile",
411
+ ".dockerignore",
412
+ )
413
+ ]
414
+ suffixes = {".py", ".html", ".js", ".css"}
415
+ for path in (root / "src/stackcraft").rglob("*"):
416
+ if path.is_file() and path.suffix in suffixes and "__pycache__" not in path.parts:
417
+ files.append(path.relative_to(root))
418
+ files.append(Path("src/stackcraft/web/baseline-demo.json"))
419
+ if not demo:
420
+ files.extend(Path("scripts") / name for name in REPRO_SCRIPTS)
421
+ files.extend((Path("release/model-card.md"), Path("release/dataset-card.md")))
422
+ for folder, suffix in (("tests", ".py"), ("docs", ".md")):
423
+ files.extend(path.relative_to(root) for path in (root / folder).rglob(f"*{suffix}"))
424
+ for suffix in (".json", ".md"):
425
+ files.extend(path.relative_to(root) for path in (root / "reports").glob(f"*{suffix}"))
426
+ for milestone, name in enumerate(
427
+ ("foundation", "game", "baselines", "data", "gpu-feasibility", "evaluation", "release")
428
+ ):
429
+ expected = Path("docs/tutorials") / f"{milestone:02d}-{name}.md"
430
+ if expected not in files:
431
+ raise ValueError(f"missing milestone tutorial: {expected}")
432
+ return sorted(set(files))
433
+
434
+
435
+ def bundled_source_readme(readme: str) -> str:
436
+ """Replace only the known private planning link with the shipped release tutorial."""
437
+ return readme.replace(
438
+ "[the project plan](plan.md)",
439
+ "[the release tutorial](docs/tutorials/06-release.md)",
440
+ )
441
+
442
+
443
+ def space_readme(report_url: str | None = None) -> str:
444
+ """Describe the CPU demo without links to files excluded from its bundle."""
445
+ readme = """---
446
+ title: Stackcraft
447
+ sdk: docker
448
+ app_port: 7860
449
+ license: apache-2.0
450
+ ---
451
+
452
+ # Stackcraft
453
+
454
+ Play a simplified falling-block game and watch a recorded comparison of an
455
+ unchanged Clef model, a trained model and a heuristic on the same piece sequence.
456
+
457
+ **Human play is live. Bot comparisons are recorded.** The comparison replays saved
458
+ placements and their decision probabilities; it does not call a model while you
459
+ watch. Playback speed changes the animation, not measured inference latency.
460
+ The fixed demonstration sequence is seed30000. One game illustrates behavior;
461
+ it does not establish which player performs better across the complete study.
462
+
463
+ Choose an orientation and column, then drop the current piece vertically onto a
464
+ 10×20 board. One next piece is visible. Completed rows clear simultaneously.
465
+ There is no timed gravity, hold, wall kick, tuck or T-spin bonus. This is a small
466
+ placement game, not a competitive Tetris implementation. Probabilities express
467
+ model preferences over legal placements, not calibrated chances of winning.
468
+
469
+ The Docker Space runs on CPU and needs no model weights, GPU or model credentials.
470
+ Human sessions are kept in memory; download a replay before a restart to retain
471
+ your game. The service runs as UID1000 on port7860 by default.
472
+
473
+ To run the same image locally:
474
+
475
+ ```bash
476
+ docker build -t stackcraft-demo .
477
+ docker run --rm -p 7860:7860 stackcraft-demo
478
+ ```
479
+
480
+ Open http://localhost:7860. The [Dockerfile](Dockerfile) and
481
+ [game source](src/stackcraft/) are included here. The
482
+ [recorded comparison](src/stackcraft/web/baseline-demo.json) contains the displayed
483
+ actions, probabilities, outcomes and source report hashes.
484
+
485
+ Apache-2.0: see [LICENSE](LICENSE) and [NOTICE](NOTICE). Stackcraft is independent
486
+ of Cloudflare, Qwen and Tetris.
487
+ """
488
+ if report_url:
489
+ readme += f"\n[Complete study report](<{report_url}>).\n"
490
+ return readme
491
+
492
+
493
+ def build_release(args: argparse.Namespace) -> dict[str, Any]:
494
+ if args.output.exists():
495
+ raise ValueError("release output already exists; use a new directory")
496
+ helpers = {"evaluate": load_helper("evaluate_clef.py"), "export": load_helper("export_demo.py")}
497
+ manifest = json_file(args.dataset / "manifest.json")
498
+ records = {
499
+ split: [
500
+ json.loads(line) for line in (args.dataset / f"{split}.jsonl").read_text().splitlines()
501
+ ]
502
+ for split in ("train", "validation")
503
+ }
504
+ audit_dataset(records, manifest)
505
+ for split in ("train", "validation"):
506
+ if sha256(args.dataset / f"{split}.jsonl") != manifest["splits"][split]["sha256"]:
507
+ raise ValueError("dataset file bytes differ from the manifest SHA256")
508
+ if {split: len(rows) for split, rows in records.items()} != {"train": 827, "validation": 215}:
509
+ raise ValueError("release dataset differs from the fixed 827/215-position study")
510
+ validate_reference(args.checkpoint, args.dataset, records)
511
+ report, checkpoint_hashes, evaluation_paths = validate_evaluation(
512
+ args.evaluation, args.selection, args.checkpoint, helpers
513
+ )
514
+ selection = json_file(args.selection)
515
+ evidence_paths = selection_evidence_files(args.selection)
516
+ expected_demo = helpers["export"].export_manifest(
517
+ [args.evaluation / "report.json"],
518
+ ["base", "trained", "heuristic"],
519
+ report_url=args.report_url,
520
+ )
521
+ if json_file(ROOT / "src/stackcraft/web/baseline-demo.json") != expected_demo:
522
+ raise ValueError(
523
+ "web demo must first be exported from this exact final report at fixed seed 30000"
524
+ )
525
+ card = render_model_card((ROOT / "release/model-card.md").read_text(), report, selection)
526
+ dataset_card = (ROOT / "release/dataset-card.md").read_text()
527
+ if "pending" in dataset_card.lower():
528
+ raise ValueError("dataset card still contains pending release text")
529
+ code_paths = code_files(ROOT, demo=False)
530
+ demo_paths = code_files(ROOT, demo=True)
531
+ identity = source_identity(ROOT)
532
+ args.output.mkdir(parents=True, exist_ok=False)
533
+ for relative in checkpoint_hashes:
534
+ copy_file(args.checkpoint / relative, args.output / "model/checkpoint" / relative)
535
+ for relative in code_paths:
536
+ copy_file(ROOT / relative, args.output / "model/code" / relative)
537
+ source_readme = args.output / "model/code/README.md"
538
+ source_readme.write_text(bundled_source_readme(source_readme.read_text()))
539
+ for relative in demo_paths:
540
+ copy_file(ROOT / relative, args.output / "demo" / relative)
541
+ for relative in evaluation_paths:
542
+ copy_file(args.evaluation / relative, args.output / "model/evidence/evaluation" / relative)
543
+ for relative in evidence_paths:
544
+ copy_file(
545
+ args.selection.parent / relative, args.output / "model/evidence/selection" / relative
546
+ )
547
+ pack_evidence(args.output / "model")
548
+ # Only remove the copies created in this new staging directory, after read-back
549
+ # verification. Original run/selection artifacts and checkpoints are untouched.
550
+ shutil.rmtree(args.output / "model/evidence")
551
+ for name in ("train.jsonl", "validation.jsonl", "manifest.json"):
552
+ copy_file(args.dataset / name, args.output / "dataset" / name)
553
+ for bundle in ("model", "dataset"):
554
+ for name in ("LICENSE", "NOTICE"):
555
+ copy_file(ROOT / name, args.output / bundle / name)
556
+ (args.output / "model/README.md").write_text(card)
557
+ (args.output / "dataset/README.md").write_text(dataset_card)
558
+ (args.output / "demo/README.md").write_text(space_readme(args.report_url))
559
+ copied_hashes = {
560
+ relative: sha256(args.output / "model/checkpoint" / relative)
561
+ for relative in checkpoint_hashes
562
+ }
563
+ if copied_hashes != checkpoint_hashes:
564
+ raise ValueError("checkpoint bytes changed during staging")
565
+ code = args.output / "model/code"
566
+ write_json(
567
+ code / "source-manifest.json",
568
+ {
569
+ "schema_version": 1,
570
+ "source_commit": identity["source_commit"],
571
+ "source_dirty": identity["source_dirty"],
572
+ "source_hashes": {
573
+ str(path.relative_to(code)): sha256(path)
574
+ for path in sorted(code.rglob("*"))
575
+ if path.is_file()
576
+ },
577
+ },
578
+ )
579
+ # Each independently uploaded repository carries its own byte manifest.
580
+ # The root manifest additionally covers these manifests and all three bundles.
581
+ for bundle in ("model", "dataset", "demo"):
582
+ directory = args.output / bundle
583
+ write_json(
584
+ directory / "release-files.json",
585
+ {
586
+ "schema_version": 1,
587
+ "files": {
588
+ str(path.relative_to(directory)): {
589
+ "sha256": sha256(path),
590
+ "bytes": path.stat().st_size,
591
+ }
592
+ for path in sorted(directory.rglob("*"))
593
+ if path.is_file()
594
+ },
595
+ "manifest_excludes_itself": True,
596
+ "checkpoint_sha256": checkpoint_hashes if bundle == "model" else None,
597
+ },
598
+ )
599
+ files = {
600
+ str(path.relative_to(args.output)): {"sha256": sha256(path), "bytes": path.stat().st_size}
601
+ for path in sorted(args.output.rglob("*"))
602
+ if path.is_file()
603
+ }
604
+ result = {
605
+ "schema_version": 1,
606
+ "kind": "local-release-bundle",
607
+ "published": False,
608
+ "checkpoint_sha256": checkpoint_hashes,
609
+ "selection_sha256": sha256(args.selection),
610
+ "evaluation_report_sha256": sha256(args.evaluation / "report.json"),
611
+ "dataset_manifest_sha256": sha256(args.dataset / "manifest.json"),
612
+ **identity,
613
+ "files": files,
614
+ "manifest_excludes_itself": True,
615
+ }
616
+ write_json(args.output / "release-manifest.json", result)
617
+ return result
618
+
619
+
620
+ def main(argv: list[str] | None = None) -> int:
621
+ parser = argparse.ArgumentParser(description=__doc__)
622
+ parser.add_argument("--checkpoint", required=True, type=Path)
623
+ parser.add_argument("--selection", required=True, type=Path)
624
+ parser.add_argument("--evaluation", required=True, type=Path)
625
+ parser.add_argument("--dataset", type=Path, default=Path("data/study-v1"))
626
+ parser.add_argument("--output", required=True, type=Path)
627
+ parser.add_argument(
628
+ "--report-url", help="Optional approved public report URL in the demo manifest"
629
+ )
630
+ args = parser.parse_args(argv)
631
+ try:
632
+ manifest = build_release(args)
633
+ except (OSError, ValueError, KeyError, TypeError) as error:
634
+ parser.error(str(error))
635
+ print(f"Prepared {len(manifest['files'])} files locally in {args.output}; nothing published.")
636
+ return 0
637
+
638
+
639
+ if __name__ == "__main__":
640
+ raise SystemExit(main())
code/scripts/evaluate_clef.py ADDED
@@ -0,0 +1,616 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Evaluate fixed Clef checkpoints without selecting from test results.
2
+
3
+ Examples (real GPU loads; run only after memory admission):
4
+ uv run --locked --extra ml python scripts/evaluate_clef.py positions \
5
+ --dataset data/study-v1 --checkpoint checkpoints/selected --output runs/validation
6
+ uv run --locked --extra ml python scripts/evaluate_clef.py tournament \
7
+ --checkpoint checkpoints/selected --seeds 0,1 --max-pieces 30 --output runs/dev-game
8
+
9
+ Final evaluation additionally requires --final-test --selection-file frozen.json.
10
+ The final selection schema is documented by validate_selection below.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import gc
17
+ import hashlib
18
+ import importlib.metadata
19
+ import json
20
+ import math
21
+ import time
22
+ from dataclasses import asdict
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ from stackcraft.clef import ENCODING_VERSION, MODEL_ID, MODEL_REVISION, ClefPlayer
27
+ from stackcraft.data import audit_dataset, canonical_json
28
+ from stackcraft.evaluation import FINAL_TEST_SEEDS, paired_report, summarize_positions
29
+ from stackcraft.players import (
30
+ Decision,
31
+ HeuristicPlayer,
32
+ Player,
33
+ RandomPlayer,
34
+ observe,
35
+ validate_decision,
36
+ )
37
+ from stackcraft.provenance import source_identity
38
+ from stackcraft.replay import replay_states
39
+ from stackcraft.schema import RULES_VERSION, GameState
40
+ from stackcraft.tournament import run_episode
41
+
42
+
43
+ def file_hash(path: Path) -> str:
44
+ digest = hashlib.sha256()
45
+ with path.open("rb") as stream:
46
+ while chunk := stream.read(1_048_576):
47
+ digest.update(chunk)
48
+ return digest.hexdigest()
49
+
50
+
51
+ def checkpoint_hashes(path: Path) -> dict[str, str]:
52
+ if (
53
+ not (path / "training_config.json").is_file()
54
+ or not (path / "joint_head.safetensors").is_file()
55
+ ):
56
+ raise ValueError("checkpoint must contain training_config.json and joint_head.safetensors")
57
+ return {
58
+ str(file.relative_to(path)): file_hash(file)
59
+ for file in sorted(path.rglob("*"))
60
+ if file.is_file()
61
+ }
62
+
63
+
64
+ def parse_seeds(value: str) -> tuple[int, ...]:
65
+ """Comma-separated integers or a Python-style start:stop range."""
66
+ try:
67
+ if ":" in value:
68
+ start, stop = (int(part) for part in value.split(":"))
69
+ seeds = tuple(range(start, stop))
70
+ else:
71
+ seeds = tuple(int(part) for part in value.split(","))
72
+ except ValueError as error:
73
+ raise argparse.ArgumentTypeError(
74
+ "seeds must be comma-separated integers or start:stop"
75
+ ) from error
76
+ if not seeds or len(seeds) != len(set(seeds)):
77
+ raise argparse.ArgumentTypeError("seeds must be nonempty and unique")
78
+ return seeds
79
+
80
+
81
+ def validation_ineligibility(report: dict[str, Any], metadata: dict[str, Any]) -> list[str]:
82
+ """The preregistered rule: all 215 rows, no errors, complete finite target NLL."""
83
+ reasons = []
84
+ metrics = report.get("players", {}).get("trained", {}).get("metrics", {})
85
+ if report.get("mode") != "positions" or report.get("split") != "validation":
86
+ reasons.append("not validation position evidence")
87
+ if report.get("positions") != 215 or metrics.get("positions") != 215:
88
+ reasons.append("not all 215 validation positions")
89
+ if metrics.get("error_rate") != 0 or metrics.get("failed_predictions") != []:
90
+ reasons.append("validation inference errors")
91
+ if (
92
+ metrics.get("complete_probability_coverage") is not True
93
+ or metrics.get("probability_positions") != 215
94
+ or metrics.get("missing_probabilities") != []
95
+ ):
96
+ reasons.append("incomplete validation probability coverage")
97
+ nll = metrics.get("mean_nll")
98
+ if (
99
+ not isinstance(nll, (float, int))
100
+ or isinstance(nll, bool)
101
+ or not math.isfinite(nll)
102
+ or metrics.get("nll_is_infinite") is not False
103
+ ):
104
+ reasons.append("nonfinite validation mean target NLL")
105
+ expected_manifest = metadata.get("extra", {}).get("dataset_manifest_sha256")
106
+ if not expected_manifest or report.get("dataset_manifest_sha256") != expected_manifest:
107
+ reasons.append("checkpoint/evidence dataset manifest mismatch")
108
+ return reasons
109
+
110
+
111
+ def validate_selection(args: argparse.Namespace, hashes: dict[str, str]) -> dict[str, Any] | None:
112
+ """Require a concrete frozen protocol before any reserved test seed can run.
113
+
114
+ JSON fields: schema_version=1, selection_split='validation', checkpoint_sha256
115
+ (full relative file/hash mapping), validation_evidence={path,sha256},
116
+ test_seeds (all 200), max_pieces, encoding_version, max_length,
117
+ players, head_dtypes, bootstrap_samples=10000, bootstrap_seed=2026,
118
+ max_error_rate=0.0, selected_key, decision_rule. Evidence paths are relative
119
+ to the selection file. Native base head remains BF16; base-fp32 is an ablation.
120
+ """
121
+ overlap = set(args.seeds) & set(FINAL_TEST_SEEDS)
122
+ if not args.final_test:
123
+ if overlap:
124
+ raise ValueError(
125
+ "reserved test seeds require explicit --final-test and frozen selection"
126
+ )
127
+ if args.selection_file is not None:
128
+ raise ValueError("--selection-file is reserved for --final-test")
129
+ return None
130
+ if tuple(args.seeds) != FINAL_TEST_SEEDS:
131
+ raise ValueError("final evaluation must use exactly --seeds 30000:30200")
132
+ if args.max_pieces != 200:
133
+ raise ValueError("final evaluation is frozen at a 200-piece cap")
134
+ if set(args.players) != {"base", "base-fp32", "trained", "random", "heuristic"}:
135
+ raise ValueError("final evaluation requires all five preregistered players")
136
+ if args.selection_file is None:
137
+ raise ValueError("--final-test requires --selection-file")
138
+ selection = json.loads(args.selection_file.read_text())
139
+ required = {
140
+ "schema_version": 1,
141
+ "selection_split": "validation",
142
+ "checkpoint_sha256": hashes,
143
+ "test_seeds": list(FINAL_TEST_SEEDS),
144
+ "test_seeds_sha256": hashlib.sha256(
145
+ canonical_json(list(FINAL_TEST_SEEDS)).encode()
146
+ ).hexdigest(),
147
+ "max_pieces": args.max_pieces,
148
+ "encoding_version": ENCODING_VERSION,
149
+ "max_length": args.max_length,
150
+ "players": args.players,
151
+ "head_dtypes": {
152
+ player: "bfloat16" if player == "base" else "float32"
153
+ for player in args.players
154
+ if player in ("base", "base-fp32", "trained")
155
+ },
156
+ "bootstrap_samples": 10000,
157
+ "bootstrap_seed": 2026,
158
+ "max_error_rate": 0.0,
159
+ }
160
+ for key, value in required.items():
161
+ if type(selection.get(key)) is not type(value) or selection[key] != value:
162
+ raise ValueError(f"frozen selection {key} differs from requested evaluation")
163
+ if any(
164
+ not isinstance(selection.get(key), str) or not selection[key]
165
+ for key in ("selected_key", "decision_rule")
166
+ ):
167
+ raise ValueError("frozen selection needs selected_key and decision_rule")
168
+ evidence = selection.get("validation_evidence", {})
169
+ if not isinstance(evidence.get("path"), str) or not evidence["path"]:
170
+ raise ValueError("frozen selection must identify validation evidence")
171
+ path = args.selection_file.parent / evidence["path"]
172
+ if not path.is_file() or file_hash(path) != evidence.get("sha256"):
173
+ raise ValueError("validation evidence file/hash mismatch")
174
+ validation = json.loads(path.read_text())
175
+ if validation.get("mode") != "positions" or validation.get("split") != "validation":
176
+ raise ValueError("selection evidence must be a validation-position evaluation")
177
+ if validation.get("checkpoint_sha256") != hashes:
178
+ raise ValueError("validation evidence evaluated a different checkpoint")
179
+ if "trained" not in validation.get("players", {}):
180
+ raise ValueError("validation evidence must include the trained checkpoint")
181
+ if selection.get("validation_metrics") != validation["players"]["trained"].get("metrics"):
182
+ raise ValueError("frozen validation metrics differ from evidence")
183
+ metadata = json.loads((args.checkpoint / "training_config.json").read_text())
184
+ if reasons := validation_ineligibility(validation, metadata):
185
+ raise ValueError("selected checkpoint is ineligible: " + "; ".join(reasons))
186
+ candidates = selection.get("candidates", [])
187
+ if len(candidates) != 2 or [candidate.get("epoch") for candidate in candidates] != [1, 2]:
188
+ raise ValueError("selection must preserve both preregistered epoch candidates")
189
+ eligible = []
190
+ for candidate in candidates:
191
+ candidate_evidence = args.selection_file.parent / candidate["validation_evidence"]["path"]
192
+ candidate_metadata = args.selection_file.parent / candidate["checkpoint_metadata"]["path"]
193
+ if (
194
+ file_hash(candidate_evidence) != candidate["validation_evidence"]["sha256"]
195
+ or file_hash(candidate_metadata)
196
+ != candidate["checkpoint_sha256"]["training_config.json"]
197
+ ):
198
+ raise ValueError("candidate evidence/metadata hash mismatch")
199
+ report = json.loads(candidate_evidence.read_text())
200
+ config = json.loads(candidate_metadata.read_text())
201
+ if (
202
+ report.get("checkpoint_sha256") != candidate["checkpoint_sha256"]
203
+ or config.get("extra", {}).get("epoch") != candidate["epoch"]
204
+ ):
205
+ raise ValueError("candidate report/checkpoint identity mismatch")
206
+ reasons = validation_ineligibility(report, config)
207
+ if candidate.get("ineligibility_reasons") != reasons:
208
+ raise ValueError("candidate eligibility differs from preserved evidence")
209
+ if not reasons:
210
+ eligible.append(
211
+ (report["players"]["trained"]["metrics"]["mean_nll"], candidate["epoch"], candidate)
212
+ )
213
+ if not eligible:
214
+ raise ValueError("no eligible validation checkpoint")
215
+ winner = min(eligible, key=lambda item: (item[0], item[1]))[2]
216
+ if winner["key"] != selection["selected_key"] or winner["checkpoint_sha256"] != hashes:
217
+ raise ValueError("selection violates lowest validation NLL with earlier-epoch tie rule")
218
+ return selection
219
+
220
+
221
+ def _write(path: Path, value: Any) -> None:
222
+ temporary = path.with_suffix(path.suffix + ".tmp")
223
+ temporary.write_text(json.dumps(value, indent=2, sort_keys=True, allow_nan=False) + "\n")
224
+ temporary.replace(path)
225
+
226
+
227
+ def _load_player(name: str, args: argparse.Namespace, hashes: dict[str, str]) -> Player:
228
+ if name == "random":
229
+ return RandomPlayer()
230
+ if name == "heuristic":
231
+ return HeuristicPlayer()
232
+ import torch
233
+
234
+ from stackcraft.training import FP32DecisionHead, load_checkpoint
235
+
236
+ torch.set_num_threads(8)
237
+ torch.backends.cuda.matmul.allow_tf32 = False
238
+
239
+ if args.device.startswith("cuda"):
240
+ free, _ = torch.cuda.mem_get_info(args.device)
241
+ if free < args.min_free_gib * 1024**3:
242
+ raise RuntimeError(f"GPU has {free / 1024**3:.2f} GiB free; need {args.min_free_gib}")
243
+ player = ClefPlayer.from_pretrained(
244
+ trust_pinned_code=True,
245
+ local_files_only=True,
246
+ device=args.device,
247
+ max_length=args.max_length,
248
+ )
249
+ if name == "trained":
250
+ load_checkpoint(player.model, args.checkpoint, trainable=False)
251
+ identity = hashlib.sha256(canonical_json(hashes).encode()).hexdigest()
252
+ player.revision = (
253
+ f"{MODEL_ID}@{MODEL_REVISION}:checkpoint-sha256-{identity}:{ENCODING_VERSION}"
254
+ )
255
+ elif name == "base-fp32":
256
+ # Explicit precision ablation; the primary base keeps the native BF16 head.
257
+ player.model.head = FP32DecisionHead(player.model.head)
258
+ player.revision += ":head-fp32-ablation"
259
+ player.model.eval()
260
+ player.name = name
261
+ player.runtime_config.update(
262
+ {
263
+ "revision": player.revision,
264
+ "dtype": str(next(player.model.parameters()).dtype),
265
+ "device": str(next(player.model.parameters()).device),
266
+ "head_dtype": "bfloat16" if name == "base" else "float32",
267
+ "evaluation_head": "native" if name == "base" else "fp32-gathered-rows",
268
+ "versions": installed_versions(),
269
+ "cuda_version": torch.version.cuda,
270
+ "cpu_threads": torch.get_num_threads(),
271
+ "float32_matmul_precision": torch.get_float32_matmul_precision(),
272
+ "cuda_matmul_allow_tf32": torch.backends.cuda.matmul.allow_tf32,
273
+ }
274
+ )
275
+ return player
276
+
277
+
278
+ def installed_versions() -> dict[str, str | None]:
279
+ versions = {}
280
+ for package in ("torch", "transformers", "peft", "safetensors", "huggingface-hub"):
281
+ try:
282
+ versions[package] = importlib.metadata.version(package)
283
+ except importlib.metadata.PackageNotFoundError:
284
+ versions[package] = None
285
+ return versions
286
+
287
+
288
+ def validate_neural_runtimes(identities: dict[str, Any]) -> None:
289
+ """Head precision may differ explicitly; shared backbone/input settings may not."""
290
+ common = (
291
+ "model_id",
292
+ "base_revision",
293
+ "encoding_version",
294
+ "source_sha256",
295
+ "max_length",
296
+ "dtype",
297
+ "device",
298
+ "versions",
299
+ "cuda_version",
300
+ "cpu_threads",
301
+ "float32_matmul_precision",
302
+ "cuda_matmul_allow_tf32",
303
+ )
304
+ selected = [
305
+ value["runtime_config"]
306
+ for key, value in identities.items()
307
+ if key in ("base", "base-fp32", "trained")
308
+ ]
309
+ if selected and any(
310
+ {k: runtime.get(k) for k in common} != {k: selected[0].get(k) for k in common}
311
+ for runtime in selected[1:]
312
+ ):
313
+ raise ValueError("neural players differ in shared backbone, encoding, precision or device")
314
+
315
+
316
+ def _release() -> None:
317
+ gc.collect()
318
+ import sys
319
+
320
+ if "torch" in sys.modules:
321
+ torch = sys.modules["torch"]
322
+ if torch.cuda.is_available():
323
+ torch.cuda.empty_cache()
324
+
325
+
326
+ def _provenance() -> dict[str, Any]:
327
+ root = Path(__file__).resolve().parents[1]
328
+ identity = source_identity(root)
329
+ paths = [Path(__file__).resolve(), root / "uv.lock"] + [
330
+ root / "src/stackcraft" / name
331
+ for name in (
332
+ "clef.py",
333
+ "training.py",
334
+ "evaluation.py",
335
+ "tournament.py",
336
+ "engine.py",
337
+ "pieces.py",
338
+ "players/__init__.py",
339
+ "schema.py",
340
+ "data.py",
341
+ "provenance.py",
342
+ )
343
+ ]
344
+ return {
345
+ **identity,
346
+ "source_commit": identity["source_commit"]
347
+ + ("+working-tree" if identity["source_dirty"] else ""),
348
+ "source_hashes": {str(path.relative_to(root)): file_hash(path) for path in paths},
349
+ "rules_version": RULES_VERSION,
350
+ "base_revision": MODEL_REVISION,
351
+ "encoding_version": ENCODING_VERSION,
352
+ "precision_policy": "native BF16 base; FP32 trained head; optional base-fp32 ablation",
353
+ "installed_versions": installed_versions(),
354
+ }
355
+
356
+
357
+ def evaluate_positions(args: argparse.Namespace, hashes: dict[str, str]) -> dict[str, Any]:
358
+ manifest = json.loads((args.dataset / "manifest.json").read_text())
359
+ splits = {
360
+ split: [
361
+ json.loads(line) for line in (args.dataset / f"{split}.jsonl").read_text().splitlines()
362
+ ]
363
+ for split in ("train", "validation")
364
+ }
365
+ audit_dataset(splits, manifest)
366
+ records = splits["validation"]
367
+ if not records:
368
+ raise ValueError("validation split is empty")
369
+ result: dict[str, Any] = {
370
+ "mode": "positions",
371
+ "split": "validation",
372
+ "positions": len(records),
373
+ "dataset_manifest_sha256": file_hash(args.dataset / "manifest.json"),
374
+ "checkpoint_sha256": hashes,
375
+ "provenance": _provenance(),
376
+ "players": {},
377
+ }
378
+ for name in args.players:
379
+ loading = time.perf_counter()
380
+ player = _load_player(name, args, hashes)
381
+ load_seconds = time.perf_counter() - loading
382
+ predictions: dict[str, Decision | None] = {}
383
+ with (args.output / f"{name}-positions.jsonl").open("x") as stream:
384
+ for row in records:
385
+ raw = row["observation"]
386
+ state = GameState(
387
+ tuple(tuple(r) for r in raw["board"]), 0, 0, raw["current"], raw["next_piece"]
388
+ )
389
+ observation = observe(state)
390
+ event: dict[str, Any] = {"id": row["id"], "target_action_id": row["action_id"]}
391
+ started = time.perf_counter()
392
+ try:
393
+ decision = player.choose(observation)
394
+ validate_decision(decision, observation)
395
+ except Exception as error:
396
+ predictions[row["id"]] = None
397
+ event["error"] = {"type": type(error).__name__, "message": str(error)}
398
+ else:
399
+ predictions[row["id"]] = decision
400
+ event["decision"] = asdict(decision)
401
+ event["input_tokens"] = getattr(player, "last_input_tokens", None)
402
+ event["decision_seconds"] = time.perf_counter() - started
403
+ stream.write(canonical_json(event) + "\n")
404
+ stream.flush()
405
+ result["players"][name] = {
406
+ "revision": player.revision,
407
+ "runtime_config": getattr(player, "runtime_config", {}),
408
+ "load_seconds": load_seconds,
409
+ "metrics": summarize_positions(records, predictions),
410
+ "predictions_file": f"{name}-positions.jsonl",
411
+ "predictions_sha256": file_hash(args.output / f"{name}-positions.jsonl"),
412
+ }
413
+ validate_neural_runtimes(result["players"])
414
+ _write(args.output / "report.json", result)
415
+ del player
416
+ _release()
417
+ return result
418
+
419
+
420
+ def evaluate_tournament(
421
+ args: argparse.Namespace, hashes: dict[str, str], selection: Any
422
+ ) -> dict[str, Any]:
423
+ episodes = []
424
+ result: dict[str, Any] = {
425
+ "mode": "tournament",
426
+ "final_test": args.final_test,
427
+ "seeds": list(args.seeds),
428
+ "max_pieces": args.max_pieces,
429
+ "checkpoint_sha256": hashes,
430
+ "selection": selection,
431
+ "provenance": _provenance(),
432
+ "players": {},
433
+ "episodes": episodes,
434
+ }
435
+ for name in args.players:
436
+ directory = args.output / name
437
+ directory.mkdir(exist_ok=True)
438
+ saved = {}
439
+ for seed in args.seeds:
440
+ path = directory / f"seed-{seed}.json"
441
+ if path.exists():
442
+ episode = json.loads(path.read_text())
443
+ if (
444
+ episode.get("player_id") != name
445
+ or episode.get("seed") != seed
446
+ or episode.get("max_pieces") != args.max_pieces
447
+ ):
448
+ raise ValueError("saved episode identity differs from frozen request")
449
+ final = replay_states(episode["replay"])[-1]
450
+ if (
451
+ any(
452
+ episode["outcome"][key] != getattr(final, key)
453
+ for key in ("score", "lines", "terminal")
454
+ )
455
+ or episode["outcome"]["pieces"] != final.piece_index
456
+ ):
457
+ raise ValueError("saved episode outcome differs from replay")
458
+ saved[seed] = episode
459
+ metadata_path = directory / "player.json"
460
+ if len(saved) == len(args.seeds):
461
+ if not metadata_path.exists():
462
+ raise ValueError("completed player episodes lack player metadata")
463
+ metadata = json.loads(metadata_path.read_text())
464
+ if any(
465
+ episode["player"]["revision"] != metadata["revision"]
466
+ or episode["player"].get("runtime_config", {}) != metadata["runtime_config"]
467
+ for episode in saved.values()
468
+ ):
469
+ raise ValueError("completed episode identity differs from saved player metadata")
470
+ result["players"][name] = metadata
471
+ episodes.extend(saved[seed] for seed in args.seeds)
472
+ continue
473
+ loading = time.perf_counter()
474
+ player = _load_player(name, args, hashes)
475
+ load_seconds = time.perf_counter() - loading
476
+ identity = {
477
+ "revision": player.revision,
478
+ "runtime_config": getattr(player, "runtime_config", {}),
479
+ }
480
+ if any(
481
+ episode["player"]["revision"] != identity["revision"]
482
+ or episode["player"].get("runtime_config", {}) != identity["runtime_config"]
483
+ for episode in saved.values()
484
+ ):
485
+ raise ValueError("loaded player differs from saved episode identity")
486
+ if metadata_path.exists():
487
+ metadata = json.loads(metadata_path.read_text())
488
+ if any(metadata.get(key) != value for key, value in identity.items()):
489
+ raise ValueError("loaded player differs from saved player metadata")
490
+ metadata["load_seconds"].append(load_seconds)
491
+ else:
492
+ metadata = {**identity, "load_seconds": [load_seconds]}
493
+ result["players"][name] = metadata
494
+ validate_neural_runtimes(result["players"])
495
+ _write(metadata_path, metadata)
496
+ # No excluded model calls. First-call effects remain in recorded latency.
497
+ for seed in args.seeds:
498
+ if seed in saved:
499
+ episodes.append(saved[seed])
500
+ continue
501
+ if name == "random":
502
+ player = RandomPlayer() # episode-independent behavior RNG
503
+ episode = run_episode(player, seed, args.max_pieces)
504
+ episode["player_id"] = name
505
+ _write(directory / f"seed-{seed}.json", episode)
506
+ episodes.append(episode)
507
+ _write(
508
+ args.output / "progress.json",
509
+ {"player": name, "seed": seed, "episodes": len(episodes)},
510
+ )
511
+ del player
512
+ _release()
513
+ validate_neural_runtimes(result["players"])
514
+ if "trained" in args.players and "base" in args.players:
515
+ result["trained_vs_base"] = paired_report(episodes, trained_id="trained", base_id="base")
516
+ if "trained" in args.players and "heuristic" in args.players:
517
+ result["trained_vs_heuristic"] = paired_report(
518
+ episodes, trained_id="trained", base_id="heuristic"
519
+ )
520
+ if "trained" in args.players and "base-fp32" in args.players:
521
+ result["trained_vs_base_fp32"] = paired_report(
522
+ episodes, trained_id="trained", base_id="base-fp32"
523
+ )
524
+ if "base-fp32" in args.players and "base" in args.players:
525
+ result["base_fp32_vs_base"] = paired_report(
526
+ episodes, trained_id="base-fp32", base_id="base"
527
+ )
528
+ _write(args.output / "report.json", result)
529
+ return result
530
+
531
+
532
+ def parser() -> argparse.ArgumentParser:
533
+ root = argparse.ArgumentParser(description=__doc__)
534
+ sub = root.add_subparsers(dest="mode", required=True)
535
+ for name in ("positions", "tournament"):
536
+ command = sub.add_parser(name)
537
+ command.add_argument("--checkpoint", type=Path)
538
+ command.add_argument("--output", type=Path, required=True)
539
+ command.add_argument(
540
+ "--players",
541
+ nargs="+",
542
+ choices=("base", "base-fp32", "trained", "random", "heuristic"),
543
+ default=["base", "base-fp32", "trained"]
544
+ if name == "positions"
545
+ else ["base", "base-fp32", "trained", "random", "heuristic"],
546
+ )
547
+ command.add_argument("--device", default="cuda")
548
+ command.add_argument("--min-free-gib", type=float, default=22.0)
549
+ command.add_argument("--max-length", type=int, default=4096)
550
+ if name == "positions":
551
+ command.add_argument("--dataset", type=Path, required=True)
552
+ else:
553
+ command.add_argument(
554
+ "--seeds",
555
+ type=parse_seeds,
556
+ required=True,
557
+ help="comma integers or start:stop (stop exclusive)",
558
+ )
559
+ command.add_argument("--max-pieces", type=int, required=True)
560
+ command.add_argument("--final-test", action="store_true")
561
+ command.add_argument("--selection-file", type=Path)
562
+ command.add_argument(
563
+ "--resume", action="store_true", help="resume matching atomic episode artifacts"
564
+ )
565
+ return root
566
+
567
+
568
+ def main(argv: list[str] | None = None) -> int:
569
+ cli = parser()
570
+ args = cli.parse_args(argv)
571
+ if len(set(args.players)) != len(args.players):
572
+ cli.error("players must be unique")
573
+ if args.max_length < 1 or not 0 < args.min_free_gib <= 1000:
574
+ cli.error("max-length and min-free-gib must be positive finite values")
575
+ if "trained" in args.players and args.checkpoint is None:
576
+ cli.error("trained evaluation requires --checkpoint")
577
+ if args.mode == "tournament" and args.max_pieces < 1:
578
+ cli.error("max-pieces must be positive")
579
+ try:
580
+ hashes = checkpoint_hashes(args.checkpoint) if args.checkpoint is not None else {}
581
+ selection = validate_selection(args, hashes) if args.mode == "tournament" else None
582
+ request = {
583
+ key: str(value) if isinstance(value, Path) else value
584
+ for key, value in vars(args).items()
585
+ if key != "resume"
586
+ }
587
+ provenance = _provenance()
588
+ request.update(
589
+ checkpoint_sha256=hashes,
590
+ source_hashes=provenance["source_hashes"],
591
+ installed_versions=provenance.get("installed_versions"),
592
+ )
593
+ request["selection_sha256"] = (
594
+ file_hash(args.selection_file)
595
+ if args.mode == "tournament" and args.selection_file
596
+ else None
597
+ )
598
+ request = json.loads(canonical_json(request))
599
+ if getattr(args, "resume", False):
600
+ if json.loads((args.output / "request.json").read_text()) != request:
601
+ raise ValueError("resume request/source/checkpoint differs from original run")
602
+ else:
603
+ args.output.mkdir(parents=True, exist_ok=False)
604
+ _write(args.output / "request.json", request)
605
+ if args.mode == "positions":
606
+ result = evaluate_positions(args, hashes)
607
+ else:
608
+ result = evaluate_tournament(args, hashes, selection)
609
+ except (ValueError, OSError) as error:
610
+ cli.error(str(error))
611
+ print(json.dumps({"mode": result["mode"], "output": str(args.output), "status": "complete"}))
612
+ return 0
613
+
614
+
615
+ if __name__ == "__main__":
616
+ raise SystemExit(main())
code/scripts/export_demo.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Export a fixed-seed, validated recorded comparison without loading any model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import hashlib
7
+ import json
8
+ import math
9
+ from dataclasses import asdict
10
+ from pathlib import Path
11
+ from typing import Any
12
+ from urllib.parse import urlsplit
13
+
14
+ from stackcraft.players import Decision, observe, validate_decision
15
+ from stackcraft.replay import replay_states
16
+ from stackcraft.schema import RULES_VERSION
17
+
18
+ DISPLAY_NAMES = {
19
+ "base": "Clef-flash base",
20
+ "base-fp32": "Base · FP32 head",
21
+ "trained": "Stackcraft fine-tune",
22
+ "heuristic": "Heuristic",
23
+ "random": "Random",
24
+ "clef-base": "Clef-flash base",
25
+ }
26
+ DEMO_SEED = 30000
27
+
28
+
29
+ def _json_value(value: Any) -> Any:
30
+ return json.loads(json.dumps(value, sort_keys=True, allow_nan=False))
31
+
32
+
33
+ def validated_player(episode: dict[str, Any], source_index: int) -> dict[str, Any]:
34
+ """Re-simulate moves and check probabilities against their exact observations."""
35
+ if (
36
+ type(episode.get("schema_version")) is not int
37
+ or episode["schema_version"] != 1
38
+ or episode.get("rules_version") != RULES_VERSION
39
+ ):
40
+ raise ValueError("episode uses an incompatible schema or rules version")
41
+ cap = episode.get("max_pieces")
42
+ if type(cap) is not int or not 1 <= cap <= 2000:
43
+ raise ValueError("demo episode cap must be between 1 and 2000")
44
+ replay = episode["replay"]
45
+ if replay.get("seed") != episode.get("seed"):
46
+ raise ValueError("episode seed differs from replay seed")
47
+ states = replay_states(replay)
48
+ final = states[-1]
49
+ if final.piece_index > cap:
50
+ raise ValueError("episode exceeds its declared cap")
51
+ outcome = episode["outcome"]
52
+ expected = {
53
+ "score": final.score,
54
+ "lines": final.lines,
55
+ "pieces": final.piece_index,
56
+ "terminal": final.terminal,
57
+ }
58
+ if any(type(outcome.get(k)) is not type(v) or outcome[k] != v for k, v in expected.items()):
59
+ raise ValueError("episode outcome differs from replay")
60
+ errors = episode.get("errors")
61
+ if not isinstance(errors, list):
62
+ raise ValueError("episode errors must be an explicit list")
63
+ status = "error" if errors else "top_out" if final.terminal else "cap"
64
+ if outcome.get("status") != status or outcome.get("cap_hit") is not (
65
+ not errors and not final.terminal and final.piece_index == cap
66
+ ):
67
+ raise ValueError("episode stop reason or cap flag is inconsistent")
68
+ if not errors and not final.terminal and final.piece_index != cap:
69
+ raise ValueError("successful recording ended before its declared cap")
70
+ events = episode["decisions"]
71
+ if not isinstance(events, list) or len(events) != final.piece_index + bool(errors):
72
+ raise ValueError("decision count differs from replay and error outcome")
73
+ compact = []
74
+ for index, event in enumerate(events):
75
+ if type(event.get("turn")) is not int or event["turn"] != index:
76
+ raise ValueError("recorded decision indices must be consecutive")
77
+ observation = observe(states[index])
78
+ if _json_value(event.get("observation")) != _json_value(asdict(observation)):
79
+ raise ValueError("recorded decision observation differs from replay")
80
+ seconds = event.get("decision_seconds")
81
+ if type(seconds) not in (float, int) or not math.isfinite(seconds) or seconds < 0:
82
+ raise ValueError("recorded decision time must be finite and nonnegative")
83
+ item = {"turn": index, "decision_seconds": seconds}
84
+ if index == final.piece_index:
85
+ if event.get("error") not in errors or "action_id" in event:
86
+ raise ValueError("terminal error event differs from episode errors")
87
+ item["error"] = event["error"]
88
+ else:
89
+ action = event.get("action_id")
90
+ if action != replay["actions"][index]:
91
+ raise ValueError("recorded decision differs from replay action")
92
+ distribution = event.get("probabilities")
93
+ validate_decision(Decision(action, distribution), observation)
94
+ item.update(action_id=action, probabilities=distribution)
95
+ if "input_tokens" in event:
96
+ if type(event["input_tokens"]) is not int or event["input_tokens"] < 1:
97
+ raise ValueError("input token counts must be positive integers")
98
+ item["input_tokens"] = event["input_tokens"]
99
+ compact.append(item)
100
+ player = episode["player"]
101
+ player_id = episode["player_id"]
102
+ if not isinstance(player.get("revision"), str) or not player["revision"]:
103
+ raise ValueError("player revision must identify the actual recorded player")
104
+ return {
105
+ "id": player_id,
106
+ "name": DISPLAY_NAMES.get(player_id, player.get("name", player_id)),
107
+ "revision": player["revision"],
108
+ "runtime_config": player.get("runtime_config", {}),
109
+ "replay": replay,
110
+ "outcome": outcome,
111
+ "errors": errors,
112
+ "decisions": compact,
113
+ "source_index": source_index,
114
+ }
115
+
116
+
117
+ def export_manifest(
118
+ inputs: list[Path], players: list[str], seed: int = DEMO_SEED, *, report_url: str | None = None
119
+ ) -> dict[str, Any]:
120
+ if report_url is not None:
121
+ parsed = urlsplit(report_url)
122
+ if (
123
+ parsed.scheme != "https"
124
+ or not parsed.hostname
125
+ or parsed.username is not None
126
+ or parsed.password is not None
127
+ or any(char.isspace() or ord(char) < 32 for char in report_url)
128
+ or "\\" in report_url
129
+ ):
130
+ raise ValueError("report URL must be an absolute HTTPS URL without credentials")
131
+ _ = parsed.port # Also reject malformed or out-of-range explicit ports.
132
+ if seed != DEMO_SEED or type(seed) is not int:
133
+ raise ValueError("the release demo seed is fixed before results at 30000")
134
+ if len(players) != len(set(players)) or not 2 <= len(players) <= 4:
135
+ raise ValueError("choose two to four distinct players")
136
+ sources = []
137
+ selected = {}
138
+ caps = set()
139
+ for path in inputs:
140
+ raw = path.read_bytes()
141
+ source = json.loads(raw)
142
+ if not isinstance(source, dict):
143
+ raise ValueError("source artifact must be a JSON object")
144
+ episodes = source.get("episodes", [source] if "replay" in source else None)
145
+ if not isinstance(episodes, list):
146
+ raise ValueError(f"{path.name} is not a tournament or episode artifact")
147
+ source_index = len(sources)
148
+ provenance = {
149
+ key: source[key]
150
+ for key in ("provenance", "checkpoint_sha256", "final_test", "development_only")
151
+ if key in source
152
+ }
153
+ sources.append({"file": path.name, "sha256": hashlib.sha256(raw).hexdigest(), **provenance})
154
+ for episode in episodes:
155
+ if not isinstance(episode, dict):
156
+ raise ValueError("episodes must be JSON objects")
157
+ identity = episode.get("player_id")
158
+ if episode.get("seed") != seed or identity not in players:
159
+ continue
160
+ if identity in selected:
161
+ raise ValueError(f"duplicate recording for {identity} at seed {seed}")
162
+ selected[identity] = validated_player(episode, source_index)
163
+ caps.add(episode["max_pieces"])
164
+ missing = set(players) - set(selected)
165
+ if missing:
166
+ raise ValueError(f"missing fixed-seed recordings for: {', '.join(sorted(missing))}")
167
+ if len(caps) != 1:
168
+ raise ValueError("all recorded players must have the same episode cap")
169
+ return {
170
+ "schema_version": 1,
171
+ "rules_version": RULES_VERSION,
172
+ "kind": "recorded-evaluation",
173
+ "label": "Recorded player comparison",
174
+ "seed": seed,
175
+ "max_pieces": caps.pop(),
176
+ "selection": {"seed": seed, "method": "fixed-before-results"},
177
+ "disclaimer": (
178
+ "Recorded decisions, no live model inference. Seed 30000 was fixed before results. "
179
+ "One replay illustrates behavior; it is not the aggregate evaluation. "
180
+ "Playback speed does not represent decision latency."
181
+ ),
182
+ "sources": sources,
183
+ **({"report_url": report_url} if report_url is not None else {}),
184
+ "players": [selected[name] for name in players],
185
+ }
186
+
187
+
188
+ def main(argv: list[str] | None = None) -> int:
189
+ parser = argparse.ArgumentParser(description=__doc__)
190
+ parser.add_argument("--input", action="append", type=Path, required=True)
191
+ parser.add_argument("--players", nargs="+", default=["base", "trained", "heuristic"])
192
+ parser.add_argument("--seed", type=int, default=DEMO_SEED)
193
+ parser.add_argument("--output", type=Path, required=True)
194
+ parser.add_argument(
195
+ "--report-url", help="Confirmed public HTTPS location of full study results"
196
+ )
197
+ args = parser.parse_args(argv)
198
+ if args.output.exists():
199
+ parser.error("output exists; choose a new path to preserve earlier evidence")
200
+ try:
201
+ manifest = export_manifest(args.input, args.players, args.seed, report_url=args.report_url)
202
+ serialized = json.dumps(manifest, indent=2, allow_nan=False) + "\n"
203
+ args.output.parent.mkdir(parents=True, exist_ok=True)
204
+ with args.output.open("x") as stream:
205
+ stream.write(serialized)
206
+ except (OSError, ValueError, KeyError, TypeError) as error:
207
+ parser.error(str(error))
208
+ print(f"Exported recorded seed {args.seed} to {args.output}")
209
+ return 0
210
+
211
+
212
+ if __name__ == "__main__":
213
+ raise SystemExit(main())
code/scripts/gpu_session.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run a bounded Stackcraft job while temporarily borrowing the GLM GPU.
2
+
3
+ Use only under the user's recorded authorization to stop and restore GLM.
4
+ """
5
+
6
+ import argparse
7
+ import contextlib
8
+ import json
9
+ import os
10
+ import signal
11
+ import subprocess
12
+ import time
13
+ from pathlib import Path
14
+
15
+ CONTAINER = "omarchy-local-ai-glm-4.7-flash.llamacpp.q5xl.32k.rtx-5090-engine"
16
+
17
+
18
+ def main():
19
+ parser = argparse.ArgumentParser(description=__doc__)
20
+ parser.add_argument("--timeout", type=int, default=1800)
21
+ parser.add_argument("--record", type=Path, required=True)
22
+ parser.add_argument("command", nargs=argparse.REMAINDER)
23
+ args = parser.parse_args()
24
+ command = args.command[1:] if args.command[:1] == ["--"] else args.command
25
+ if not command or args.record.exists():
26
+ parser.error("supply a command and a new record path")
27
+ state = subprocess.check_output(
28
+ ["docker", "inspect", "--format", "{{.State.Running}}", CONTAINER], text=True
29
+ ).strip()
30
+ if state != "true":
31
+ parser.error("GLM is not currently running; do not alter an unexpected service state")
32
+ args.record.parent.mkdir(parents=True, exist_ok=True)
33
+ report = {"container": CONTAINER, "command": command, "original_running": True}
34
+ child = None
35
+ stopped = False
36
+
37
+ def interrupt(signum, frame):
38
+ raise KeyboardInterrupt(f"received signal{signum}")
39
+
40
+ signal.signal(signal.SIGTERM, interrupt)
41
+ signal.signal(signal.SIGINT, interrupt)
42
+ try:
43
+ # Set before stop so a partial/interrupted Docker call still attempts restoration.
44
+ stopped = True
45
+ subprocess.run(["docker", "stop", "--timeout", "30", CONTAINER], check=True, timeout=45)
46
+ report["stopped_at"] = time.time()
47
+ args.record.write_text(json.dumps(report, indent=2) + "\n")
48
+ child = subprocess.Popen(command, start_new_session=True)
49
+ report["job_pid"] = child.pid
50
+ args.record.write_text(json.dumps(report, indent=2) + "\n")
51
+ code = child.wait(timeout=args.timeout)
52
+ report["exit_code"] = code
53
+ if code:
54
+ raise RuntimeError(f"Stackcraft GPU job exited with code{code}")
55
+ finally:
56
+ # A second ordinary interrupt must not interrupt service restoration.
57
+ signal.signal(signal.SIGTERM, signal.SIG_IGN)
58
+ signal.signal(signal.SIGINT, signal.SIG_IGN)
59
+ try:
60
+ try:
61
+ if child is not None and child.poll() is None:
62
+ with contextlib.suppress(ProcessLookupError):
63
+ os.killpg(child.pid, signal.SIGTERM)
64
+ try:
65
+ child.wait(timeout=10)
66
+ except subprocess.TimeoutExpired:
67
+ with contextlib.suppress(ProcessLookupError):
68
+ os.killpg(child.pid, signal.SIGKILL)
69
+ child.wait(timeout=10)
70
+ finally:
71
+ if stopped:
72
+ subprocess.run(["docker", "start", CONTAINER], check=True, timeout=60)
73
+ restored = subprocess.check_output(
74
+ ["docker", "inspect", "--format", "{{.State.Running}}", CONTAINER],
75
+ text=True,
76
+ ).strip()
77
+ report["restored_running"] = restored == "true"
78
+ if not report["restored_running"]:
79
+ raise RuntimeError("GLM did not restart")
80
+ # Probe inside the container: host port8080 belongs to another service.
81
+ deadline = time.monotonic() + 120
82
+ while True:
83
+ healthy = False
84
+ try:
85
+ health = subprocess.run(
86
+ [
87
+ "docker",
88
+ "exec",
89
+ CONTAINER,
90
+ "curl",
91
+ "-fsS",
92
+ "--max-time",
93
+ "5",
94
+ "http://localhost:8080/health",
95
+ ],
96
+ capture_output=True,
97
+ text=True,
98
+ timeout=10,
99
+ )
100
+ healthy = health.returncode == 0 and json.loads(health.stdout) == {
101
+ "status": "ok"
102
+ }
103
+ except (subprocess.TimeoutExpired, json.JSONDecodeError):
104
+ pass
105
+ if healthy:
106
+ report["restored_healthy"] = True
107
+ break
108
+ if time.monotonic() >= deadline:
109
+ raise RuntimeError(
110
+ "GLM restarted but health did not recover within120s"
111
+ )
112
+ time.sleep(2)
113
+ report["restored_at"] = time.time()
114
+ except BaseException as error:
115
+ report["restoration_error"] = f"{type(error).__name__}: {error}"
116
+ raise
117
+ finally:
118
+ args.record.write_text(json.dumps(report, indent=2) + "\n")
119
+ print(json.dumps(report), flush=True)
120
+
121
+
122
+ if __name__ == "__main__":
123
+ main()
code/scripts/prepare_checkpoint.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Copy an immutable trained checkpoint and replace only its generated adapter card."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import hashlib
7
+ import json
8
+ import shutil
9
+ import stat
10
+ from datetime import UTC, datetime
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from stackcraft.clef import MODEL_ID, MODEL_REVISION
15
+
16
+ CARD_PATH = "adapter/README.md"
17
+ REQUIRED_FILES = {
18
+ "training_config.json",
19
+ "joint_head.safetensors",
20
+ "adapter/adapter_config.json",
21
+ "adapter/adapter_model.safetensors",
22
+ "reference.json",
23
+ CARD_PATH,
24
+ }
25
+ ADAPTER_CARD = f"""---
26
+ base_model: {MODEL_ID}
27
+ library_name: peft
28
+ tags:
29
+ - lora
30
+ - stackcraft
31
+ ---
32
+
33
+ # Stackcraft LoRA adapter component
34
+
35
+ This Stackcraft adapter adapts [{MODEL_ID}](https://huggingface.co/{MODEL_ID}),
36
+ released by Cloudflare and pinned at revision `{MODEL_REVISION}`.
37
+ The original model card and license are provided in that upstream repository.
38
+
39
+ This directory is only one component of the Stackcraft checkpoint. Load the
40
+ parent checkpoint's `joint_head.safetensors` and this adapter together through
41
+ Stackcraft's `load_checkpoint` function. The native decision head contains learned
42
+ parameters; loading only the LoRA adapter does not reproduce the trained model.
43
+ Use the exact pinned base and the parent `training_config.json` contract.
44
+
45
+ This component card does not claim improved performance or publication status.
46
+ See the release's full model card and evaluation report for measured results,
47
+ limitations, training provenance and reproduction commands. A prepared candidate
48
+ has not necessarily been selected by validation.
49
+ """
50
+
51
+
52
+ def _reject_symlink_components(path: Path) -> None:
53
+ for component in (path, *path.parents):
54
+ if component.is_symlink():
55
+ raise ValueError(f"symlinks are not accepted: {component}")
56
+
57
+
58
+ def file_hash(path: Path) -> str:
59
+ digest = hashlib.sha256()
60
+ with path.open("rb") as stream:
61
+ while chunk := stream.read(1_048_576):
62
+ digest.update(chunk)
63
+ return digest.hexdigest()
64
+
65
+
66
+ def inventory(directory: Path) -> dict[str, str]:
67
+ """Reject symlinks and special files; hash every regular file by relative path."""
68
+ result = {}
69
+ for path in sorted(directory.rglob("*")):
70
+ mode = path.lstat().st_mode
71
+ if stat.S_ISLNK(mode) or not (stat.S_ISREG(mode) or stat.S_ISDIR(mode)):
72
+ raise ValueError(f"checkpoint contains a symlink or special file: {path}")
73
+ if stat.S_ISREG(mode):
74
+ result[path.relative_to(directory).as_posix()] = file_hash(path)
75
+ return result
76
+
77
+
78
+ def prepare(source: Path, output: Path) -> dict[str, Any]:
79
+ _reject_symlink_components(source.absolute())
80
+ _reject_symlink_components(output.absolute())
81
+ source = source.resolve()
82
+ output = output.resolve()
83
+ provenance = output.parent / f"{output.name}-preparation.json"
84
+ _reject_symlink_components(provenance)
85
+ if not source.is_dir():
86
+ raise ValueError("source must be an existing checkpoint directory")
87
+ if output.is_relative_to(source) or source.is_relative_to(output):
88
+ raise ValueError("source and output directories must be separate, not nested")
89
+ if output.exists() or provenance.exists():
90
+ raise FileExistsError("output checkpoint or preparation record already exists")
91
+ before = inventory(source)
92
+ if not REQUIRED_FILES.issubset(before):
93
+ missing = ", ".join(sorted(REQUIRED_FILES - before.keys()))
94
+ raise ValueError(f"checkpoint is incomplete; missing: {missing}")
95
+ if any((source / name).stat().st_size == 0 for name in REQUIRED_FILES):
96
+ raise ValueError("required checkpoint files must not be empty")
97
+ metadata = json.loads((source / "training_config.json").read_text())
98
+ if (
99
+ not isinstance(metadata, dict)
100
+ or metadata.get("base_model") != MODEL_ID
101
+ or metadata.get("base_revision") != MODEL_REVISION
102
+ or metadata.get("mode") != "lora"
103
+ ):
104
+ raise ValueError("checkpoint does not identify the pinned Stackcraft LoRA base")
105
+ for name in ("adapter/adapter_config.json", "reference.json"):
106
+ if not isinstance(json.loads((source / name).read_text()), dict):
107
+ raise ValueError(f"{name} must be a JSON object")
108
+ output.parent.mkdir(parents=True, exist_ok=True)
109
+ output.mkdir(exist_ok=False)
110
+ try:
111
+ for name in before:
112
+ destination = output / name
113
+ destination.parent.mkdir(parents=True, exist_ok=True)
114
+ shutil.copy2(source / name, destination, follow_symlinks=False)
115
+ # Recheck copied entries before writing the card, including a link introduced
116
+ # by a source changing during copy. Never follow a copied symlink to write.
117
+ inventory(output)
118
+ (output / CARD_PATH).unlink()
119
+ with (output / CARD_PATH).open("x") as stream:
120
+ stream.write(ADAPTER_CARD)
121
+ after = inventory(output)
122
+ if before.keys() != after.keys():
123
+ raise RuntimeError("prepared checkpoint changed the file inventory")
124
+ changed = [name for name in before if before[name] != after[name]]
125
+ if any(name != CARD_PATH for name in changed):
126
+ raise RuntimeError("preparation altered a file other than adapter/README.md")
127
+ if inventory(source) != before:
128
+ raise RuntimeError("source checkpoint changed during preparation")
129
+ report = {
130
+ "schema_version": 1,
131
+ "operation": "replace-generated-adapter-documentation-only",
132
+ "created_at": datetime.now(UTC).isoformat(),
133
+ "source": str(source),
134
+ "output": str(output),
135
+ "source_sha256": before,
136
+ "output_sha256": after,
137
+ "changed_files": changed,
138
+ "unchanged_non_documentation": True,
139
+ "source_unchanged": True,
140
+ "base_model": MODEL_ID,
141
+ "base_revision": MODEL_REVISION,
142
+ "preparation_script_sha256": file_hash(Path(__file__)),
143
+ }
144
+ with provenance.open("x") as stream:
145
+ json.dump(report, stream, indent=2, sort_keys=True, allow_nan=False)
146
+ stream.write("\n")
147
+ except BaseException:
148
+ # This call created the output exclusively; leave the raw source untouched.
149
+ shutil.rmtree(output)
150
+ raise
151
+ return report
152
+
153
+
154
+ def main(argv: list[str] | None = None) -> int:
155
+ parser = argparse.ArgumentParser(description=__doc__)
156
+ parser.add_argument("--source", type=Path, required=True)
157
+ parser.add_argument("--output", type=Path, required=True)
158
+ args = parser.parse_args(argv)
159
+ try:
160
+ report = prepare(args.source, args.output)
161
+ except (OSError, ValueError, RuntimeError) as error:
162
+ parser.error(str(error))
163
+ print(json.dumps({"output": report["output"], "changed_files": report["changed_files"]}))
164
+ return 0
165
+
166
+
167
+ if __name__ == "__main__":
168
+ raise SystemExit(main())