mp-juuuns commited on
Commit
9fc8c19
·
verified ·
1 Parent(s): e086ff0

Publish task-agnostic 4L base, separate SemEval model, and 24L-to-4L distillation platform

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -34
  2. LICENSE +203 -0
  3. README.md +140 -0
  4. SHA256SUMS +66 -0
  5. benchmark/reports/structural_control-seed41.json +0 -0
  6. benchmark/reports/structural_control-seed42.json +0 -0
  7. benchmark/reports/structural_control-seed43.json +0 -0
  8. benchmark/reports/task_agnostic-seed41.json +0 -0
  9. benchmark/reports/task_agnostic-seed42.json +0 -0
  10. benchmark/reports/task_agnostic-seed43.json +0 -0
  11. benchmark/semeval-transfer-summary.json +47 -0
  12. chat_template.jinja +120 -0
  13. config.json +80 -0
  14. distillation/README.md +108 -0
  15. distillation/configs/task_agnostic_base.yaml +19 -0
  16. distillation/examples/labels.json +5 -0
  17. distillation/examples/multilabel_train.jsonl +2 -0
  18. distillation/examples/single_label_train.jsonl +2 -0
  19. distillation/examples/unlabeled.jsonl +2 -0
  20. distillation/pyproject.toml +22 -0
  21. distillation/qwen35_distill/__init__.py +6 -0
  22. distillation/qwen35_distill/checkpoint.py +324 -0
  23. distillation/qwen35_distill/classification.py +386 -0
  24. distillation/qwen35_distill/cli.py +197 -0
  25. distillation/qwen35_distill/layer_maps.py +87 -0
  26. distillation/qwen35_distill/losses.py +98 -0
  27. distillation/qwen35_distill/schema.py +149 -0
  28. distillation/qwen35_distill/training.py +275 -0
  29. docs/RELEASE_CONTRACT.md +73 -0
  30. merges.txt +0 -0
  31. model.safetensors +3 -0
  32. models/semeval-propaganda/LICENSE +203 -0
  33. models/semeval-propaganda/README.md +27 -0
  34. models/semeval-propaganda/benchmark/final4l_aggregate.json +289 -0
  35. models/semeval-propaganda/benchmark/seed41.json +0 -0
  36. models/semeval-propaganda/benchmark/seed42.json +0 -0
  37. models/semeval-propaganda/benchmark/seed43.json +0 -0
  38. models/semeval-propaganda/chat_template.jinja +120 -0
  39. models/semeval-propaganda/classify.py +217 -0
  40. models/semeval-propaganda/config.json +122 -0
  41. models/semeval-propaganda/labels.json +34 -0
  42. models/semeval-propaganda/model.safetensors +3 -0
  43. models/semeval-propaganda/release_manifest.json +637 -0
  44. models/semeval-propaganda/requirements.txt +3 -0
  45. models/semeval-propaganda/seeds/seed42/chat_template.jinja +120 -0
  46. models/semeval-propaganda/seeds/seed42/config.json +122 -0
  47. models/semeval-propaganda/seeds/seed42/model.safetensors +3 -0
  48. models/semeval-propaganda/seeds/seed42/tokenizer.json +0 -0
  49. models/semeval-propaganda/seeds/seed42/tokenizer_config.json +39 -0
  50. models/semeval-propaganda/seeds/seed43/chat_template.jinja +120 -0
.gitattributes CHANGED
@@ -1,35 +1,2 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
  *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  *.safetensors filter=lfs diff=lfs merge=lfs -text
2
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
LICENSE ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright 2018- The Hugging Face team. All rights reserved.
2
+
3
+ Apache License
4
+ Version 2.0, January 2004
5
+ http://www.apache.org/licenses/
6
+
7
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
8
+
9
+ 1. Definitions.
10
+
11
+ "License" shall mean the terms and conditions for use, reproduction,
12
+ and distribution as defined by Sections 1 through 9 of this document.
13
+
14
+ "Licensor" shall mean the copyright owner or entity authorized by
15
+ the copyright owner that is granting the License.
16
+
17
+ "Legal Entity" shall mean the union of the acting entity and all
18
+ other entities that control, are controlled by, or are under common
19
+ control with that entity. For the purposes of this definition,
20
+ "control" means (i) the power, direct or indirect, to cause the
21
+ direction or management of such entity, whether by contract or
22
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
23
+ outstanding shares, or (iii) beneficial ownership of such entity.
24
+
25
+ "You" (or "Your") shall mean an individual or Legal Entity
26
+ exercising permissions granted by this License.
27
+
28
+ "Source" form shall mean the preferred form for making modifications,
29
+ including but not limited to software source code, documentation
30
+ source, and configuration files.
31
+
32
+ "Object" form shall mean any form resulting from mechanical
33
+ transformation or translation of a Source form, including but
34
+ not limited to compiled object code, generated documentation,
35
+ and conversions to other media types.
36
+
37
+ "Work" shall mean the work of authorship, whether in Source or
38
+ Object form, made available under the License, as indicated by a
39
+ copyright notice that is included in or attached to the work
40
+ (an example is provided in the Appendix below).
41
+
42
+ "Derivative Works" shall mean any work, whether in Source or Object
43
+ form, that is based on (or derived from) the Work and for which the
44
+ editorial revisions, annotations, elaborations, or other modifications
45
+ represent, as a whole, an original work of authorship. For the purposes
46
+ of this License, Derivative Works shall not include works that remain
47
+ separable from, or merely link (or bind by name) to the interfaces of,
48
+ the Work and Derivative Works thereof.
49
+
50
+ "Contribution" shall mean any work of authorship, including
51
+ the original version of the Work and any modifications or additions
52
+ to that Work or Derivative Works thereof, that is intentionally
53
+ submitted to Licensor for inclusion in the Work by the copyright owner
54
+ or by an individual or Legal Entity authorized to submit on behalf of
55
+ the copyright owner. For the purposes of this definition, "submitted"
56
+ means any form of electronic, verbal, or written communication sent
57
+ to the Licensor or its representatives, including but not limited to
58
+ communication on electronic mailing lists, source code control systems,
59
+ and issue tracking systems that are managed by, or on behalf of, the
60
+ Licensor for the purpose of discussing and improving the Work, but
61
+ excluding communication that is conspicuously marked or otherwise
62
+ designated in writing by the copyright owner as "Not a Contribution."
63
+
64
+ "Contributor" shall mean Licensor and any individual or Legal Entity
65
+ on behalf of whom a Contribution has been received by Licensor and
66
+ subsequently incorporated within the Work.
67
+
68
+ 2. Grant of Copyright License. Subject to the terms and conditions of
69
+ this License, each Contributor hereby grants to You a perpetual,
70
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
71
+ copyright license to reproduce, prepare Derivative Works of,
72
+ publicly display, publicly perform, sublicense, and distribute the
73
+ Work and such Derivative Works in Source or Object form.
74
+
75
+ 3. Grant of Patent License. Subject to the terms and conditions of
76
+ this License, each Contributor hereby grants to You a perpetual,
77
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
78
+ (except as stated in this section) patent license to make, have made,
79
+ use, offer to sell, sell, import, and otherwise transfer the Work,
80
+ where such license applies only to those patent claims licensable
81
+ by such Contributor that are necessarily infringed by their
82
+ Contribution(s) alone or by combination of their Contribution(s)
83
+ with the Work to which such Contribution(s) was submitted. If You
84
+ institute patent litigation against any entity (including a
85
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
86
+ or a Contribution incorporated within the Work constitutes direct
87
+ or contributory patent infringement, then any patent licenses
88
+ granted to You under this License for that Work shall terminate
89
+ as of the date such litigation is filed.
90
+
91
+ 4. Redistribution. You may reproduce and distribute copies of the
92
+ Work or Derivative Works thereof in any medium, with or without
93
+ modifications, and in Source or Object form, provided that You
94
+ meet the following conditions:
95
+
96
+ (a) You must give any other recipients of the Work or
97
+ Derivative Works a copy of this License; and
98
+
99
+ (b) You must cause any modified files to carry prominent notices
100
+ stating that You changed the files; and
101
+
102
+ (c) You must retain, in the Source form of any Derivative Works
103
+ that You distribute, all copyright, patent, trademark, and
104
+ attribution notices from the Source form of the Work,
105
+ excluding those notices that do not pertain to any part of
106
+ the Derivative Works; and
107
+
108
+ (d) If the Work includes a "NOTICE" text file as part of its
109
+ distribution, then any Derivative Works that You distribute must
110
+ include a readable copy of the attribution notices contained
111
+ within such NOTICE file, excluding those notices that do not
112
+ pertain to any part of the Derivative Works, in at least one
113
+ of the following places: within a NOTICE text file distributed
114
+ as part of the Derivative Works; within the Source form or
115
+ documentation, if provided along with the Derivative Works; or,
116
+ within a display generated by the Derivative Works, if and
117
+ wherever such third-party notices normally appear. The contents
118
+ of the NOTICE file are for informational purposes only and
119
+ do not modify the License. You may add Your own attribution
120
+ notices within Derivative Works that You distribute, alongside
121
+ or as an addendum to the NOTICE text from the Work, provided
122
+ that such additional attribution notices cannot be construed
123
+ as modifying the License.
124
+
125
+ You may add Your own copyright statement to Your modifications and
126
+ may provide additional or different license terms and conditions
127
+ for use, reproduction, or distribution of Your modifications, or
128
+ for any such Derivative Works as a whole, provided Your use,
129
+ reproduction, and distribution of the Work otherwise complies with
130
+ the conditions stated in this License.
131
+
132
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
133
+ any Contribution intentionally submitted for inclusion in the Work
134
+ by You to the Licensor shall be under the terms and conditions of
135
+ this License, without any additional terms or conditions.
136
+ Notwithstanding the above, nothing herein shall supersede or modify
137
+ the terms of any separate license agreement you may have executed
138
+ with Licensor regarding such Contributions.
139
+
140
+ 6. Trademarks. This License does not grant permission to use the trade
141
+ names, trademarks, service marks, or product names of the Licensor,
142
+ except as required for reasonable and customary use in describing the
143
+ origin of the Work and reproducing the content of the NOTICE file.
144
+
145
+ 7. Disclaimer of Warranty. Unless required by applicable law or
146
+ agreed to in writing, Licensor provides the Work (and each
147
+ Contributor provides its Contributions) on an "AS IS" BASIS,
148
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
149
+ implied, including, without limitation, any warranties or conditions
150
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
151
+ PARTICULAR PURPOSE. You are solely responsible for determining the
152
+ appropriateness of using or redistributing the Work and assume any
153
+ risks associated with Your exercise of permissions under this License.
154
+
155
+ 8. Limitation of Liability. In no event and under no legal theory,
156
+ whether in tort (including negligence), contract, or otherwise,
157
+ unless required by applicable law (such as deliberate and grossly
158
+ negligent acts) or agreed to in writing, shall any Contributor be
159
+ liable to You for damages, including any direct, indirect, special,
160
+ incidental, or consequential damages of any character arising as a
161
+ result of this License or out of the use or inability to use the
162
+ Work (including but not limited to damages for loss of goodwill,
163
+ work stoppage, computer failure or malfunction, or any and all
164
+ other commercial damages or losses), even if such Contributor
165
+ has been advised of the possibility of such damages.
166
+
167
+ 9. Accepting Warranty or Additional Liability. While redistributing
168
+ the Work or Derivative Works thereof, You may choose to offer,
169
+ and charge a fee for, acceptance of support, warranty, indemnity,
170
+ or other liability obligations and/or rights consistent with this
171
+ License. However, in accepting such obligations, You may act only
172
+ on Your own behalf and on Your sole responsibility, not on behalf
173
+ of any other Contributor, and only if You agree to indemnify,
174
+ defend, and hold each Contributor harmless for any liability
175
+ incurred by, or claims asserted against, such Contributor by reason
176
+ of your accepting any such warranty or additional liability.
177
+
178
+ END OF TERMS AND CONDITIONS
179
+
180
+ APPENDIX: How to apply the Apache License to your work.
181
+
182
+ To apply the Apache License to your work, attach the following
183
+ boilerplate notice, with the fields enclosed by brackets "[]"
184
+ replaced with your own identifying information. (Don't include
185
+ the brackets!) The text should be enclosed in the appropriate
186
+ comment syntax for the file format. We also recommend that a
187
+ file or class name and description of purpose be included on the
188
+ same "printed page" as the copyright notice for easier
189
+ identification within third-party archives.
190
+
191
+ Copyright [yyyy] [name of copyright owner]
192
+
193
+ Licensed under the Apache License, Version 2.0 (the "License");
194
+ you may not use this file except in compliance with the License.
195
+ You may obtain a copy of the License at
196
+
197
+ http://www.apache.org/licenses/LICENSE-2.0
198
+
199
+ Unless required by applicable law or agreed to in writing, software
200
+ distributed under the License is distributed on an "AS IS" BASIS,
201
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
202
+ See the License for the specific language governing permissions and
203
+ limitations under the License.
README.md ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model: Qwen/Qwen3.5-0.8B
4
+ pipeline_tag: feature-extraction
5
+ library_name: transformers
6
+ language:
7
+ - en
8
+ tags:
9
+ - qwen3.5
10
+ - classification-backbone
11
+ - knowledge-distillation
12
+ - model-compression
13
+ - edge-ai
14
+ ---
15
+
16
+ # Qwen3.5 Standalone 4L Classification Base
17
+
18
+ This repository publishes a **headless, classification-ready four-layer
19
+ Qwen3.5 text backbone**. The root model has no task labels and no classification
20
+ head. It is intended as a compact starting point for single-label or multilabel
21
+ classifiers, not as a chat model or a ready-made universal classifier.
22
+
23
+ The repository follows one integrated 1-1-1 layout:
24
+
25
+ - **Root model:** task-agnostic four-layer base trained on unlabeled general
26
+ text.
27
+ - **`models/semeval-propaganda/`:** the existing SemEval-derived propaganda
28
+ classifier as a separate task-specific lineage.
29
+ - **`distillation/`:** a reusable 24L→8L→6L→4L platform for your own data.
30
+
31
+ ## Load the headless base
32
+
33
+ ```python
34
+ from transformers import AutoModel, AutoTokenizer
35
+
36
+ model_id = "mp-juuuns/qwen35-standalone4l-classification-base"
37
+ tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=False)
38
+ backbone = AutoModel.from_pretrained(model_id, trust_remote_code=False)
39
+ ```
40
+
41
+ The root uses `Qwen3_5TextModel` in Transformers 5.13.0. It has four text
42
+ layers in the sequence `linear → full → linear → full`, hidden size 1,024, and
43
+ the full upstream Qwen3.5 tokenizer. The published weight file contains no
44
+ `score.weight`.
45
+
46
+ ## Make your own classifier
47
+
48
+ ```bash
49
+ git clone https://huggingface.co/mp-juuuns/qwen35-standalone4l-classification-base
50
+ cd qwen35-standalone4l-classification-base/distillation
51
+ python -m pip install -e .
52
+
53
+ qwen35-distill finetune \
54
+ --checkpoint .. \
55
+ --train examples/multilabel_train.jsonl \
56
+ --labels examples/labels.json \
57
+ --mode multilabel \
58
+ --output my-classifier-4l
59
+ ```
60
+
61
+ For a full task-specific compression run, first fine-tune a 24-layer teacher,
62
+ then repeat `materialize-classifier` and `distill-classifier` for `24to8`,
63
+ `8to6`, and `6to4`. Exact JSONL formats and commands are in
64
+ [`distillation/README.md`](distillation/README.md).
65
+
66
+ ## How the root weights were made
67
+
68
+ The text backbone and full tokenizer were extracted from
69
+ [`Qwen/Qwen3.5-0.8B`](https://huggingface.co/Qwen/Qwen3.5-0.8B). Students were
70
+ trained sequentially through 24→8→6→4 using hidden-boundary and final
71
+ representation matching on unlabeled text.
72
+
73
+ The task-free training set was exactly 4,096 deterministically shuffled,
74
+ non-empty rows from the first locally cached WikiText-103 raw training shard.
75
+ No SemEval examples, labels, logits, thresholds, or evidence/span objectives
76
+ were read while training these root weights.
77
+
78
+ WikiText is not redistributed here. Its current Hugging Face page has a license
79
+ wording discrepancy: metadata lists CC BY-SA 3.0 and GFDL, while the prose says
80
+ CC BY-SA 4.0. Users should review the
81
+ [`Salesforce/wikitext`](https://huggingface.co/datasets/Salesforce/wikitext)
82
+ terms directly.
83
+
84
+ ## One-task transfer check
85
+
86
+ We attached a fresh 14-label head and used the frozen SemEval-derived
87
+ article-level protocol only as a transfer probe.
88
+
89
+ | Initialization | Opened-test Macro-F1, mean ± sample SD |
90
+ |---|---:|
91
+ | Task-agnostic 4L base | **0.58748 ± 0.01579** |
92
+ | Same 4L structural copy without task-free KD | 0.51809 ± 0.01631 |
93
+ | Existing separate specialized 4L lineage | 0.58757 ± 0.00717 |
94
+
95
+ Results use seeds 41/42/43 and a previously opened 55-article public test split.
96
+ They are exploratory, not untouched confirmatory evidence. The derived task
97
+ measures article-level presence of 14 techniques and is not identical to the
98
+ official SemEval span- or fragment-level task. Three seed values are not a
99
+ confidence interval.
100
+
101
+ This check supports transfer on one English classification task only. It does
102
+ not establish universal classification quality, multilingual generalization,
103
+ calibration, safety, or production suitability.
104
+
105
+ ## Separate SemEval model
106
+
107
+ The ready-to-use propaganda model is under
108
+ [`models/semeval-propaganda/`](models/semeval-propaganda/). It preserves the
109
+ currently published seed-41/42/43 checkpoints and links to the original
110
+ repository at immutable revision
111
+ [`e7e0ff1`](https://huggingface.co/mp-juuuns/qwen35-standalone4l-propaganda-classifier/tree/e7e0ff16828052687d2e8dd7849e7a521629cf38).
112
+
113
+ That specialized model is **not** claimed to descend from this new task-free
114
+ base. It has its own SemEval task-specific shrink/distillation and fine-tuning
115
+ history and a reduced 128k vocabulary, while the root base retains the full
116
+ upstream tokenizer.
117
+
118
+ ## Limitations
119
+
120
+ - A classification head and labeled training are required before root-model
121
+ predictions have task meaning.
122
+ - The task-free corpus is small: 4,096 rows from one WikiText shard.
123
+ - Only one downstream transfer task was measured.
124
+ - Default layer maps are documented structural choices, not universal optima.
125
+ - Long-context, multilingual, calibration, robustness, fairness, and device
126
+ performance of the new root base have not been established.
127
+ - Do not use either model as a fact checker, safety oracle, or autonomous
128
+ decision maker.
129
+
130
+ ## License and attribution
131
+
132
+ The released code and model artifacts use Apache-2.0, subject to upstream model
133
+ and data terms. Qwen3.5 is attributed to Qwen. Transformers, PyTorch,
134
+ Hugging Face Hub, WikiText, and SemEval remain the work of their respective
135
+ authors. No WikiText or SemEval source records are redistributed.
136
+
137
+ Machine-readable training manifests, transfer summaries, hashes, and the
138
+ release inventory are included under `provenance/`, `benchmark/`, and
139
+ `release_manifest.json`.
140
+
SHA256SUMS ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ee05350c7fda0af4da116020dd7b976d731de3beed4f5e717d5bcc95ead24550 .gitattributes
2
+ 77fd4710def9ec3c0f6225800e0235f15a425abd4a8b03559127fcd782612049 LICENSE
3
+ 4f5af67b76f19fca88e242298fa501aa6aa811b4f1f51b481d17abb46e0d39cf README.md
4
+ 816c3249d3ec281ef73d34dc3ab7576a48415d250c32977eb94ca37f73b0b810 benchmark/reports/structural_control-seed41.json
5
+ 0b19eabe1e31d0012c754db3b31173f8a6fcdee30973f1a1bebd3a370d11064c benchmark/reports/structural_control-seed42.json
6
+ 6dde34173462eac50141ff082c18194f3233f8c06853532d9b12756562e69e2e benchmark/reports/structural_control-seed43.json
7
+ 6b3a2e0372a83e1e71c2eca9596efc8cd1a3e39c28e25e3373f195431b5858a4 benchmark/reports/task_agnostic-seed41.json
8
+ 42dc788983716be0e9847aad87ff79c7e7297884f2635a8d098639be77326639 benchmark/reports/task_agnostic-seed42.json
9
+ 7ee52dc94a152a2f767dcff171f3c9cc1072fabd3ebc7b14db53617b7b1d33d5 benchmark/reports/task_agnostic-seed43.json
10
+ 18c8d840dd2290ca25c517d52cbcf1ad6c6ae8d9c32c545236e96bd5ada7dab2 benchmark/semeval-transfer-summary.json
11
+ 04b007131663760bf3e581e5a953be77044014e87efe1d2a6ca4b72ec0eac978 chat_template.jinja
12
+ bb33f3a52d787e15db7d516c6de36a7ddd2c675e7964d5b9309131c4e5d365f0 config.json
13
+ f5b7248f7781412e8b5d8f2fb3b0c309568e4e75baf55029b26a6fa985d9d608 distillation/README.md
14
+ adf8b6e3f0f5a5dfe042dcc09a7a8e7ef8cd1146462b089c5f6c9393e9a3131f distillation/configs/task_agnostic_base.yaml
15
+ 8b528ab38eb2e9f53f9ec2c50d4a0c860d87594067dc9b064b4bb47ef51f8f50 distillation/examples/labels.json
16
+ daf5859bb93c474d766ecb0681a2585a6a8ed3cb5efcdd476c845727a0c3583c distillation/examples/multilabel_train.jsonl
17
+ 918490e8db364c6e601cfdcb09e28c272eafc8359ffe190645a4cbecdf06aed3 distillation/examples/single_label_train.jsonl
18
+ 7a1db673674a003ef749b1fa90a99eff6db70c3189411b9cee574cc23d084860 distillation/examples/unlabeled.jsonl
19
+ 3c5a0f9b48849381307a6b3c258729d4079bf5d42bb6ab5d28edbdda4a1a3592 distillation/pyproject.toml
20
+ 2965e662310bf4197d1cbd4f9b3fcf3f383950dbfc4f033729a286d026709f3a distillation/qwen35_distill/__init__.py
21
+ d67bf7ed3bfb6cb622d6d35a99a4292f1191395270264e81a7d19283d206e5dc distillation/qwen35_distill/checkpoint.py
22
+ ea06bec682622addc426def4f7e315b3d12f7f595af44f714cc811588cf64782 distillation/qwen35_distill/classification.py
23
+ d8fb1584484596f9c35db9c2c296dab08b2dd9bb1b7910a115710feee7562771 distillation/qwen35_distill/cli.py
24
+ a9c49af0ee14b307a08c096bcd1ae55fa9bc67bdb807fa9383fc313246e7edd7 distillation/qwen35_distill/layer_maps.py
25
+ d9b135cf5b0adf4bbfc321200395fbd6112e49fd1a027b30262bcff1c4c0b654 distillation/qwen35_distill/losses.py
26
+ 00731ae7838317105f9fff960e3da210b2a55954feb37b3ac793bcfc81fdab2d distillation/qwen35_distill/schema.py
27
+ 4dc432acb70e03848d2e5964e8550065490fda3857aac32fe3b79895ef72026f distillation/qwen35_distill/training.py
28
+ 5a1d9e939cd0f350770bcc2a116bc427bb9971bc4fbbbda17ce568e9127185ef docs/RELEASE_CONTRACT.md
29
+ a9d356d7bdf1ef4949e3e748e95b8e10ad9d4e2e838eddc38a0a7b6b94d1db8d merges.txt
30
+ 2732c616772fe320cdea228ab4554981418b1b2bf615c4183fb1ac8e6e2168d3 model.safetensors
31
+ 77fd4710def9ec3c0f6225800e0235f15a425abd4a8b03559127fcd782612049 models/semeval-propaganda/LICENSE
32
+ 8dbc2e641280c471b78064808ced7fffcb81c639784a5936b57175975a718f6f models/semeval-propaganda/README.md
33
+ 0bae347bb54f8089a5fa5450f8366b5c56eb3e598beaf9a87f4385363a402bcb models/semeval-propaganda/benchmark/final4l_aggregate.json
34
+ c1bd379680abcf50b47b7d23e4aafbc8b5e7f40a0b0e9700bde21a8ce49af803 models/semeval-propaganda/benchmark/seed41.json
35
+ a30e38433d008db4aa7e5fbfd477ab80a0daf50e970f8e7b04397efc1a47843a models/semeval-propaganda/benchmark/seed42.json
36
+ 96466290d09d27b8bc044a9f9a95765ce12b4d7b02dfb0123e436e7535623353 models/semeval-propaganda/benchmark/seed43.json
37
+ 04b007131663760bf3e581e5a953be77044014e87efe1d2a6ca4b72ec0eac978 models/semeval-propaganda/chat_template.jinja
38
+ 1c09956cc5d3553eed0b5f64c20c81064df4373ccfaf4fec26929b8f4819773a models/semeval-propaganda/classify.py
39
+ 9f04dfbd7e1696cd3df2ccbe194f5f155efe5baefd1a0b615851f51bf46691a0 models/semeval-propaganda/config.json
40
+ 946fed4fe4c3d320fd113ec8e5776b10f2707b936f857341bb5443d7a9038248 models/semeval-propaganda/labels.json
41
+ 0352da6e4717a2e6f3a9d643d034be2cb1c39632d1bce0ced339ddb62acedb37 models/semeval-propaganda/model.safetensors
42
+ ef64dc628fe9bb4efd2a0b6a739f0c842c6fd4586b1803707e7a071e439cdf69 models/semeval-propaganda/release_manifest.json
43
+ 2994886af208a0120c051745f9c14036e6f2caae8c4af863e5965d5a5bd68fdf models/semeval-propaganda/requirements.txt
44
+ 04b007131663760bf3e581e5a953be77044014e87efe1d2a6ca4b72ec0eac978 models/semeval-propaganda/seeds/seed42/chat_template.jinja
45
+ 9f04dfbd7e1696cd3df2ccbe194f5f155efe5baefd1a0b615851f51bf46691a0 models/semeval-propaganda/seeds/seed42/config.json
46
+ 2db5f4be391d9dee64458cb5865530f39747019e7a219443b5f7850befbce697 models/semeval-propaganda/seeds/seed42/model.safetensors
47
+ dd2b27bbd4f5f650c7200300013ef03ee01efe620b128343163521bf04806fc8 models/semeval-propaganda/seeds/seed42/tokenizer.json
48
+ 2215318fe27878ec67e772931bb5f40a98658348d33f2672a575f766a2190d35 models/semeval-propaganda/seeds/seed42/tokenizer_config.json
49
+ 04b007131663760bf3e581e5a953be77044014e87efe1d2a6ca4b72ec0eac978 models/semeval-propaganda/seeds/seed43/chat_template.jinja
50
+ 9f04dfbd7e1696cd3df2ccbe194f5f155efe5baefd1a0b615851f51bf46691a0 models/semeval-propaganda/seeds/seed43/config.json
51
+ 81a56d497df758e8a292eea528438cfc4d5a847cd37d3a7e1107fefa7c500640 models/semeval-propaganda/seeds/seed43/model.safetensors
52
+ dd2b27bbd4f5f650c7200300013ef03ee01efe620b128343163521bf04806fc8 models/semeval-propaganda/seeds/seed43/tokenizer.json
53
+ 2215318fe27878ec67e772931bb5f40a98658348d33f2672a575f766a2190d35 models/semeval-propaganda/seeds/seed43/tokenizer_config.json
54
+ 737c4e145a5b51660c4c45a8acea4c32219cbbbc0706abe235e36b2ad430f17d models/semeval-propaganda/thresholds.json
55
+ dd2b27bbd4f5f650c7200300013ef03ee01efe620b128343163521bf04806fc8 models/semeval-propaganda/tokenizer.json
56
+ 2215318fe27878ec67e772931bb5f40a98658348d33f2672a575f766a2190d35 models/semeval-propaganda/tokenizer_config.json
57
+ e831b8a121fe896bb9f033287a5b82a0bffad1037a45bbb1ec913fed261add92 provenance/base/finalization_manifest.json
58
+ ec734855870758cbfef99031dac2ded85b54c496532e5c514a9a4f63287c3493 provenance/base/training_manifest.json
59
+ b9ce99b993c4f4563bc2521fc731bfd82f5e8149c141133599fbd8f982ba43d9 provenance/task_agnostic_stages/24to8.json
60
+ ec734855870758cbfef99031dac2ded85b54c496532e5c514a9a4f63287c3493 provenance/task_agnostic_stages/6to4.json
61
+ 65e5383afbbcfe0b7dd78a5411c319ed6bf2586659fb1892aca9ecbd5dfd4993 provenance/task_agnostic_stages/8to6.json
62
+ 143945a19235351e00048a99c74799e362eefec3b5e3f3a2ad4c8b64611034a4 release_manifest.json
63
+ 7b48e7811e56e552fc4aef44189e1d7c9b1352c85d2b3f10ff0bdd1ea5cdd824 requirements.txt
64
+ 06b9509352d2af50381ab2247e083b80d32d5c0aba91c272ca9ff729b6a0e523 tokenizer.json
65
+ 5ab9bed0a4d27949672f65ba1141d6dd5b0514fb9091d3426506ecebb5d5e294 tokenizer_config.json
66
+ ce99b4cb2983d118806ce0a8b777a35b093e2000a503ebde25853284c9dfa003 vocab.json
benchmark/reports/structural_control-seed41.json ADDED
The diff for this file is too large to render. See raw diff
 
benchmark/reports/structural_control-seed42.json ADDED
The diff for this file is too large to render. See raw diff
 
benchmark/reports/structural_control-seed43.json ADDED
The diff for this file is too large to render. See raw diff
 
benchmark/reports/task_agnostic-seed41.json ADDED
The diff for this file is too large to render. See raw diff
 
benchmark/reports/task_agnostic-seed42.json ADDED
The diff for this file is too large to render. See raw diff
 
benchmark/reports/task_agnostic-seed43.json ADDED
The diff for this file is too large to render. See raw diff
 
benchmark/semeval-transfer-summary.json ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema": "standalone4l-transfer-summary-v1",
3
+ "claim_scope": "Exploratory one-task transfer evidence on a previously opened 55-article SemEval-derived test split; not confirmatory.",
4
+ "metric": "article-level multilabel Macro-F1",
5
+ "seeds": [
6
+ 41,
7
+ 42,
8
+ 43
9
+ ],
10
+ "task_agnostic_base": {
11
+ "values": [
12
+ 0.5883093293517536,
13
+ 0.6028379036218096,
14
+ 0.5712816197954101
15
+ ],
16
+ "mean": 0.5874762842563245,
17
+ "sample_sd": 0.01579462678670608
18
+ },
19
+ "structural_copy_control": {
20
+ "values": [
21
+ 0.5169424579262757,
22
+ 0.5349405779540175,
23
+ 0.5023778364496152
24
+ ],
25
+ "mean": 0.5180869574433028,
26
+ "sample_sd": 0.016311512588628545
27
+ },
28
+ "same_seed_deltas": [
29
+ 0.07136687142547793,
30
+ 0.06789732566779205,
31
+ 0.06890378334579494
32
+ ],
33
+ "mean_delta": 0.06938932681302164,
34
+ "publication_gate_passed": true,
35
+ "existing_specialized_separate_lineage": {
36
+ "values": [
37
+ 0.5832848452288824,
38
+ 0.5958405181160762,
39
+ 0.5835785045218137
40
+ ],
41
+ "mean": 0.5875679559555907,
42
+ "sample_sd": 0.007165593191426643
43
+ },
44
+ "test_articles": 55,
45
+ "test_previously_opened": true,
46
+ "three_seed_sd_is_not_confidence_interval": true
47
+ }
chat_template.jinja ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- set image_count = namespace(value=0) %}
2
+ {%- set video_count = namespace(value=0) %}
3
+ {%- macro render_content(content, do_vision_count) %}
4
+ {%- if content is string %}
5
+ {{- content }}
6
+ {%- else %}
7
+ {%- for item in content %}
8
+ {%- if 'image' in item or 'image_url' in item or item.type == 'image' %}
9
+ {%- if do_vision_count %}
10
+ {%- set image_count.value = image_count.value + 1 %}
11
+ {%- endif %}
12
+ {%- if add_vision_id %}Picture {{ image_count.value }}: {% endif -%}
13
+ <|vision_start|><|image_pad|><|vision_end|>
14
+ {%- elif 'video' in item or item.type == 'video' %}
15
+ {%- if do_vision_count %}
16
+ {%- set video_count.value = video_count.value + 1 %}
17
+ {%- endif %}
18
+ {%- if add_vision_id %}Video {{ video_count.value }}: {% endif -%}
19
+ <|vision_start|><|video_pad|><|vision_end|>
20
+ {%- elif 'text' in item %}
21
+ {{- item.text }}
22
+ {%- endif %}
23
+ {%- endfor %}
24
+ {%- endif %}
25
+ {%- endmacro %}
26
+ {%- if tools is defined and tools is iterable and tools | length > 0 %}
27
+ {{- '<|im_start|>system\n' }}
28
+ {{- "# Tools\n\nYou have access to the following functions:\n\n<tools>" }}
29
+ {%- for tool in tools %}
30
+ {{- "\n" }}
31
+ {{- tool | tojson }}
32
+ {%- endfor %}
33
+ {{- "\n</tools>" }}
34
+ {{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
35
+ {%- if messages[0].role == 'system' and messages[0].content | trim %}
36
+ {%- set content = render_content(messages[0].content, false) %}
37
+ {{- '\n\n' + content.lstrip('\n') }}
38
+ {%- endif %}
39
+ {{- '<|im_end|>\n' }}
40
+ {%- else %}
41
+ {%- if messages[0].role == 'system' %}
42
+ {{- '<|im_start|>system\n' + render_content(messages[0].content, false) + '<|im_end|>\n' }}
43
+ {%- endif %}
44
+ {%- endif %}
45
+ {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
46
+ {%- for message in messages[::-1] %}
47
+ {%- set index = (messages|length - 1) - loop.index0 %}
48
+ {%- if ns.multi_step_tool and message.role == "user" %}
49
+ {%- set content = render_content(message.content, False) %}
50
+ {%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
51
+ {%- set ns.multi_step_tool = false %}
52
+ {%- set ns.last_query_index = index %}
53
+ {%- endif %}
54
+ {%- endif %}
55
+ {%- endfor %}
56
+ {%- for message in messages %}
57
+ {%- set content = render_content(message.content, true) %}
58
+ {%- if (message.role == "user") or (message.role == "system" and not loop.first) %}
59
+ {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
60
+ {%- elif message.role == "assistant" %}
61
+ {%- set reasoning_content = '' %}
62
+ {%- if message.reasoning_content is defined and message.reasoning_content is not none %}
63
+ {%- set reasoning_content = message.reasoning_content %}
64
+ {%- else %}
65
+ {%- if '</think>' in content %}
66
+ {%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
67
+ {%- set content = content.split('</think>')[-1].lstrip('\n') %}
68
+ {%- endif %}
69
+ {%- endif %}
70
+ {%- if message.tool_calls is defined and message.tool_calls is not none and message.tool_calls is iterable and message.tool_calls | length > 0 %}
71
+ {%- set content = content.rstrip('\n') %}
72
+ {%- endif %}
73
+ {%- if loop.index0 > ns.last_query_index %}
74
+ {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content.strip('\n') + '\n</think>\n\n' + content.lstrip('\n') }}
75
+ {%- else %}
76
+ {{- '<|im_start|>' + message.role + '\n' + content }}
77
+ {%- endif %}
78
+ {%- if message.tool_calls is defined and message.tool_calls is not none and message.tool_calls is iterable and message.tool_calls | length > 0 %}
79
+ {%- for tool_call in message.tool_calls %}
80
+ {%- if tool_call.function is defined %}
81
+ {%- set tool_call = tool_call.function %}
82
+ {%- endif %}
83
+ {%- if loop.first %}
84
+ {%- if content|trim %}
85
+ {{- '\n\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
86
+ {%- else %}
87
+ {{- '<tool_call>\n<function=' + tool_call.name + '>\n' }}
88
+ {%- endif %}
89
+ {%- else %}
90
+ {{- '\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
91
+ {%- endif %}
92
+ {%- if tool_call.arguments is defined %}
93
+ {%- for args_name, args_value in tool_call.arguments|items %}
94
+ {{- '<parameter=' + args_name + '>\n' }}
95
+ {%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
96
+ {{- args_value }}
97
+ {{- '\n</parameter>\n' }}
98
+ {%- endfor %}
99
+ {%- endif %}
100
+ {{- '</function>\n</tool_call>' }}
101
+ {%- endfor %}
102
+ {%- endif %}
103
+ {{- '<|im_end|>\n' }}
104
+ {%- elif message.role == "tool" %}
105
+ {%- if loop.previtem and loop.previtem.role != "tool" %}
106
+ {{- '<|im_start|>user' }}
107
+ {%- endif %}
108
+ {{- '\n<tool_response>\n' }}
109
+ {{- content }}
110
+ {{- '\n</tool_response>' }}
111
+ {%- if not loop.last and loop.nextitem.role != "tool" %}
112
+ {{- '<|im_end|>\n' }}
113
+ {%- elif loop.last %}
114
+ {{- '<|im_end|>\n' }}
115
+ {%- endif %}
116
+ {%- endif %}
117
+ {%- endfor %}
118
+ {%- if add_generation_prompt %}
119
+ {{- '<|im_start|>assistant\n<think>\n' }}
120
+ {%- endif %}
config.json ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "Qwen3_5TextModel"
4
+ ],
5
+ "attention_bias": false,
6
+ "attention_dropout": 0.0,
7
+ "attn_output_gate": true,
8
+ "bos_token_id": null,
9
+ "dtype": "bfloat16",
10
+ "eos_token_id": 248046,
11
+ "full_attention_interval": 4,
12
+ "head_dim": 256,
13
+ "hidden_act": "silu",
14
+ "hidden_size": 1024,
15
+ "initializer_range": 0.02,
16
+ "intermediate_size": 3584,
17
+ "layer_types": [
18
+ "linear_attention",
19
+ "full_attention",
20
+ "linear_attention",
21
+ "full_attention"
22
+ ],
23
+ "linear_conv_kernel_dim": 4,
24
+ "linear_key_head_dim": 128,
25
+ "linear_num_key_heads": 16,
26
+ "linear_num_value_heads": 16,
27
+ "linear_value_head_dim": 128,
28
+ "mamba_ssm_dtype": "float32",
29
+ "max_position_embeddings": 262144,
30
+ "mlp_only_layers": [],
31
+ "model_type": "qwen3_5_text",
32
+ "mtp_num_hidden_layers": 1,
33
+ "mtp_use_dedicated_embeddings": false,
34
+ "num_attention_heads": 8,
35
+ "num_hidden_layers": 4,
36
+ "num_key_value_heads": 2,
37
+ "pad_token_id": 248044,
38
+ "partial_rotary_factor": 0.25,
39
+ "rms_norm_eps": 1e-06,
40
+ "rope_parameters": {
41
+ "mrope_interleaved": true,
42
+ "mrope_section": [
43
+ 11,
44
+ 11,
45
+ 10
46
+ ],
47
+ "partial_rotary_factor": 0.25,
48
+ "rope_theta": 10000000,
49
+ "rope_type": "default"
50
+ },
51
+ "standalone4l_distillation": {
52
+ "schema_version": "qwen35-stage-map-v1",
53
+ "selected_layers": [
54
+ 0,
55
+ 2,
56
+ 3,
57
+ 5
58
+ ],
59
+ "selection_basis": "predeclared_architecture_only_default",
60
+ "source_depth": 6,
61
+ "stage": "6to4",
62
+ "target_depth": 4,
63
+ "task_labels_used_for_weight_materialization": false
64
+ },
65
+ "standalone4l_role": "task_agnostic_classification_base_4l",
66
+ "tie_word_embeddings": true,
67
+ "transformers_version": "5.13.0",
68
+ "use_cache": false,
69
+ "vocab_size": 248320,
70
+ "standalone4l_base": {
71
+ "schema_version": "standalone4l-task-agnostic-base-v1",
72
+ "role": "classification_ready_headless_text_backbone",
73
+ "task_labels_used_in_base_training": false,
74
+ "semeval_used_in_base_training": false,
75
+ "tokenizer": "upstream_full_qwen35",
76
+ "training_corpus_rows": 4096,
77
+ "training_objective": "hidden_and_interface_representation_distillation",
78
+ "published_classification_head": false
79
+ }
80
+ }
distillation/README.md ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Qwen3.5 Classification Distillation
2
+
3
+ This package turns the text backbone of `Qwen/Qwen3.5-0.8B` into smaller
4
+ classification-ready backbones and task-specific classifiers.
5
+
6
+ Two workflows are intentionally separate:
7
+
8
+ 1. **Task-agnostic base:** hidden/interface distillation on unlabeled text.
9
+ It produces a headless text backbone and never reads task labels.
10
+ 2. **Task-specific classifier:** attach a single-label or multilabel head,
11
+ train a 24-layer teacher on a user dataset, then transfer hard labels,
12
+ teacher logits, and hidden/interface representations through
13
+ `24L → 8L → 6L → 4L`.
14
+
15
+ The published default maps are structural defaults, not a claim that they are
16
+ universally optimal:
17
+
18
+ | Stage | Source layers retained | Target layer types |
19
+ |---|---|---|
20
+ | 24L → 8L | `0,4,6,11,13,16,20,23` | `L,L,L,F,L,L,L,F` |
21
+ | 8L → 6L | `0,1,3,4,6,7` | `L,L,F,L,L,F` |
22
+ | 6L → 4L | `0,2,3,5` | `L,F,L,F` |
23
+
24
+ The final 6L→4L numeric map overlaps the earlier SemEval experiment, but the
25
+ task-agnostic base uses the fixed structural contract above and no SemEval
26
+ labels, examples, logits, thresholds, or span objectives. Architecture choice
27
+ and task-free weight training are reported as separate provenance claims.
28
+
29
+ ## Dataset formats
30
+
31
+ Unlabeled:
32
+
33
+ ```json
34
+ {"id":"u-1","text":"Unlabeled text used only for representation distillation."}
35
+ ```
36
+
37
+ Single-label:
38
+
39
+ ```json
40
+ {"id":"s-1","text":"Example text.","label":"class_a"}
41
+ ```
42
+
43
+ Multilabel:
44
+
45
+ ```json
46
+ {"id":"m-1","text":"Example text.","labels":["class_a","class_c"]}
47
+ ```
48
+
49
+ Each split is a separate JSONL file. `id` must be unique across all splits.
50
+ An optional `group_id` is checked for cross-split leakage.
51
+
52
+ ## Initial commands
53
+
54
+ ```bash
55
+ python -m qwen35_distill.cli validate-dataset \
56
+ --mode unlabeled --train examples/unlabeled.jsonl
57
+
58
+ python -m qwen35_distill.cli extract-text \
59
+ --source /path/to/Qwen3.5-0.8B \
60
+ --output /path/to/text-24l
61
+
62
+ python -m qwen35_distill.cli materialize \
63
+ --source /path/to/text-24l \
64
+ --output /path/to/text-8l \
65
+ --stage 24to8
66
+
67
+ python -m qwen35_distill.cli distill \
68
+ --teacher /path/to/text-24l \
69
+ --student /path/to/text-8l \
70
+ --data /path/to/unlabeled.parquet \
71
+ --output /path/to/text-8l-kd \
72
+ --stage 24to8 --device cuda --dtype bfloat16
73
+ ```
74
+
75
+ Every output is exclusive: existing directories are never overwritten.
76
+
77
+ ## Build a classifier from your own labels
78
+
79
+ Put the exact label order in `labels.json`, then train a teacher or attach a
80
+ head directly to the published 4-layer base:
81
+
82
+ ```bash
83
+ qwen35-distill finetune \
84
+ --checkpoint /path/to/text-24l \
85
+ --train train.jsonl --validation validation.jsonl \
86
+ --labels labels.json --mode multilabel \
87
+ --output classifier-24l
88
+ ```
89
+
90
+ To preserve the classifier head while following the full compression chain,
91
+ repeat these two commands for `24to8`, `8to6`, and `6to4`:
92
+
93
+ ```bash
94
+ qwen35-distill materialize-classifier \
95
+ --source classifier-24l --stage 24to8 \
96
+ --output classifier-8l-init
97
+
98
+ qwen35-distill distill-classifier \
99
+ --teacher classifier-24l --checkpoint classifier-8l-init \
100
+ --stage 24to8 --train train.jsonl --validation validation.jsonl \
101
+ --labels labels.json --mode multilabel \
102
+ --output classifier-8l
103
+ ```
104
+
105
+ The task-specific distillation objective combines the user labels, teacher
106
+ logits, and aligned hidden boundaries. The teacher hashes, dataset hashes,
107
+ label order, environment, loss history, and fresh-reload check are written to
108
+ each output manifest.
distillation/configs/task_agnostic_base.yaml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ schema_version: qwen35-distill-config-v1
2
+ mode: task_agnostic
3
+ tokenizer: upstream_full
4
+ stages:
5
+ - 24to8
6
+ - 8to6
7
+ - 6to4
8
+ training:
9
+ seed: 41
10
+ epochs: 1
11
+ max_rows: 50000
12
+ max_length: 128
13
+ batch_size: 1
14
+ gradient_accumulation: 8
15
+ learning_rate: 0.00002
16
+ claims:
17
+ task_labels_used: false
18
+ semeval_used: false
19
+ universal_generalization_claimed: false
distillation/examples/labels.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ [
2
+ "tag_a",
3
+ "tag_b",
4
+ "tag_c"
5
+ ]
distillation/examples/multilabel_train.jsonl ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ {"id":"m-1","text":"A short example with two tags.","labels":["tag_a","tag_b"],"group_id":"g-1"}
2
+ {"id":"m-2","text":"A second example with one tag.","labels":["tag_c"],"group_id":"g-2"}
distillation/examples/single_label_train.jsonl ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ {"id":"s-1","text":"The package arrived early.","label":"positive","group_id":"g-1"}
2
+ {"id":"s-2","text":"The package did not arrive.","label":"negative","group_id":"g-2"}
distillation/examples/unlabeled.jsonl ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ {"id":"u-1","text":"A small public example for task-agnostic representation distillation."}
2
+ {"id":"u-2","text":"Users should replace these rows with their own licensed text corpus."}
distillation/pyproject.toml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "qwen35-classification-distill"
7
+ version = "0.1.0"
8
+ description = "Auditable Qwen3.5 24L→8L→6L→4L classification distillation"
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "torch>=2.4",
12
+ "transformers==5.13.0",
13
+ "safetensors>=0.4",
14
+ "PyYAML>=6",
15
+ "pyarrow>=16",
16
+ ]
17
+
18
+ [project.scripts]
19
+ qwen35-distill = "qwen35_distill.cli:main"
20
+
21
+ [tool.setuptools]
22
+ packages = ["qwen35_distill"]
distillation/qwen35_distill/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """Auditable Qwen3.5 classification distillation."""
2
+
3
+ from .layer_maps import DEFAULT_STAGE_MAPS, StageMap
4
+
5
+ __all__ = ["DEFAULT_STAGE_MAPS", "StageMap"]
6
+ __version__ = "0.1.0"
distillation/qwen35_distill/checkpoint.py ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import hashlib
5
+ import json
6
+ import shutil
7
+ from pathlib import Path
8
+ from typing import Any, Iterable
9
+
10
+ import torch
11
+ from safetensors import safe_open
12
+
13
+ from .layer_maps import StageMap
14
+ from .schema import sha256_file
15
+
16
+
17
+ def _weight_files(path: Path) -> list[Path]:
18
+ index = path / "model.safetensors.index.json"
19
+ if index.is_file():
20
+ document = json.loads(index.read_text(encoding="utf-8"))
21
+ names = sorted(set(document["weight_map"].values()))
22
+ files = [path / name for name in names]
23
+ else:
24
+ files = sorted(path.glob("*.safetensors"))
25
+ if not files:
26
+ raise FileNotFoundError(f"no safetensors weights in {path}")
27
+ for file in files:
28
+ if not file.is_file():
29
+ raise FileNotFoundError(file)
30
+ return files
31
+
32
+
33
+ def checkpoint_hashes(path: Path) -> dict[str, str]:
34
+ names = [
35
+ "config.json",
36
+ "tokenizer.json",
37
+ "tokenizer_config.json",
38
+ "vocab.json",
39
+ "merges.txt",
40
+ ]
41
+ files = _weight_files(path) + [path / name for name in names if (path / name).is_file()]
42
+ return {file.name: sha256_file(file) for file in files}
43
+
44
+
45
+ def _load_source_tensors(
46
+ files: Iterable[Path], *, prefix: str
47
+ ) -> dict[str, torch.Tensor]:
48
+ state: dict[str, torch.Tensor] = {}
49
+ for file in files:
50
+ with safe_open(file, framework="pt", device="cpu") as handle:
51
+ for name in handle.keys():
52
+ if prefix and not name.startswith(prefix):
53
+ continue
54
+ target_name = name.removeprefix(prefix)
55
+ if target_name in state:
56
+ raise ValueError(f"duplicate tensor {target_name}")
57
+ state[target_name] = handle.get_tensor(name)
58
+ return state
59
+
60
+
61
+ def _copy_tokenizer_files(source: Path, output: Path) -> None:
62
+ for name in (
63
+ "tokenizer.json",
64
+ "tokenizer_config.json",
65
+ "vocab.json",
66
+ "merges.txt",
67
+ "chat_template.jinja",
68
+ "special_tokens_map.json",
69
+ ):
70
+ path = source / name
71
+ if path.is_file():
72
+ shutil.copy2(path, output / name)
73
+
74
+
75
+ def extract_text_checkpoint(source: Path, output: Path) -> dict[str, Any]:
76
+ """Extract the 24-layer text backbone from the official multimodal checkpoint."""
77
+ from transformers.models.qwen3_5.configuration_qwen3_5 import Qwen3_5TextConfig
78
+ from transformers.models.qwen3_5.modeling_qwen3_5 import (
79
+ Qwen3_5TextModel,
80
+ Qwen3_5TextRotaryEmbedding,
81
+ )
82
+
83
+ source = source.resolve()
84
+ output = output.resolve()
85
+ if output.exists():
86
+ raise FileExistsError(output)
87
+ document = json.loads((source / "config.json").read_text(encoding="utf-8"))
88
+ if "text_config" not in document:
89
+ raise ValueError("source is not the official multimodal Qwen3.5 layout")
90
+ config_document = copy.deepcopy(document["text_config"])
91
+ if int(config_document["num_hidden_layers"]) != 24:
92
+ raise ValueError("expected a 24-layer text backbone")
93
+ config_document["architectures"] = ["Qwen3_5TextModel"]
94
+ config_document["use_cache"] = False
95
+ config_document["standalone4l_role"] = "task_agnostic_teacher_24l"
96
+ config = Qwen3_5TextConfig(**config_document)
97
+ with torch.device("meta"):
98
+ model = Qwen3_5TextModel(config)
99
+ mapped = _load_source_tensors(
100
+ _weight_files(source), prefix="model.language_model."
101
+ )
102
+ expected = set(model.state_dict())
103
+ if set(mapped) != expected:
104
+ raise ValueError(
105
+ f"text extraction mismatch: missing={sorted(expected-set(mapped))[:10]}, "
106
+ f"unexpected={sorted(set(mapped)-expected)[:10]}"
107
+ )
108
+ model.load_state_dict(mapped, strict=True, assign=True)
109
+ fresh_rotary = Qwen3_5TextRotaryEmbedding(config, device="cpu")
110
+ model.rotary_emb.inv_freq = fresh_rotary.inv_freq
111
+ model.rotary_emb.original_inv_freq = fresh_rotary.original_inv_freq
112
+ output.mkdir(parents=True, exist_ok=False)
113
+ model.eval().save_pretrained(output, safe_serialization=True)
114
+ _copy_tokenizer_files(source, output)
115
+ report = {
116
+ "schema_version": "qwen35-text-extraction-v1",
117
+ "source": str(source),
118
+ "output": str(output),
119
+ "source_hashes": checkpoint_hashes(source),
120
+ "output_hashes": checkpoint_hashes(output),
121
+ "tensor_count": len(mapped),
122
+ "semeval_or_task_labels_used": False,
123
+ }
124
+ (output / "extraction_manifest.json").write_text(
125
+ json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
126
+ )
127
+ return report
128
+
129
+
130
+ def map_layer_tensor_name(name: str, selected_layers: tuple[int, ...]) -> str | None:
131
+ prefix = "layers."
132
+ if not name.startswith(prefix):
133
+ return name
134
+ suffix = name.removeprefix(prefix)
135
+ parts = suffix.split(".", 1)
136
+ if len(parts) != 2:
137
+ raise ValueError(f"invalid layer tensor name: {name}")
138
+ source_index = int(parts[0])
139
+ try:
140
+ target_index = selected_layers.index(source_index)
141
+ except ValueError:
142
+ return None
143
+ return f"layers.{target_index}.{parts[1]}"
144
+
145
+
146
+ def materialize_student(
147
+ source: Path, output: Path, stage: StageMap
148
+ ) -> dict[str, Any]:
149
+ from transformers import AutoConfig
150
+ from transformers.models.qwen3_5.modeling_qwen3_5 import (
151
+ Qwen3_5TextModel,
152
+ Qwen3_5TextRotaryEmbedding,
153
+ )
154
+
155
+ source = source.resolve()
156
+ output = output.resolve()
157
+ if output.exists():
158
+ raise FileExistsError(output)
159
+ source_config = AutoConfig.from_pretrained(source, local_files_only=True)
160
+ target_types = stage.validate(source_config.layer_types)
161
+ target_config = copy.deepcopy(source_config)
162
+ target_config.num_hidden_layers = stage.target_depth
163
+ target_config.layer_types = list(target_types)
164
+ target_config.architectures = ["Qwen3_5TextModel"]
165
+ target_config.use_cache = False
166
+ target_config.standalone4l_distillation = {
167
+ "schema_version": "qwen35-stage-map-v1",
168
+ "stage": stage.name,
169
+ "source_depth": stage.source_depth,
170
+ "target_depth": stage.target_depth,
171
+ "selected_layers": list(stage.selected_layers),
172
+ "selection_basis": "predeclared_architecture_only_default",
173
+ "task_labels_used_for_weight_materialization": False,
174
+ }
175
+ with torch.device("meta"):
176
+ model = Qwen3_5TextModel(target_config)
177
+ source_state = _load_source_tensors(_weight_files(source), prefix="")
178
+ mapped: dict[str, torch.Tensor] = {}
179
+ for source_name, tensor in source_state.items():
180
+ target_name = map_layer_tensor_name(source_name, stage.selected_layers)
181
+ if target_name is None:
182
+ continue
183
+ if target_name in mapped:
184
+ raise ValueError(f"duplicate mapped tensor {target_name}")
185
+ mapped[target_name] = tensor
186
+ expected = set(model.state_dict())
187
+ if set(mapped) != expected:
188
+ raise ValueError(
189
+ f"student mapping mismatch: missing={sorted(expected-set(mapped))[:10]}, "
190
+ f"unexpected={sorted(set(mapped)-expected)[:10]}"
191
+ )
192
+ model.load_state_dict(mapped, strict=True, assign=True)
193
+ fresh_rotary = Qwen3_5TextRotaryEmbedding(target_config, device="cpu")
194
+ model.rotary_emb.inv_freq = fresh_rotary.inv_freq
195
+ model.rotary_emb.original_inv_freq = fresh_rotary.original_inv_freq
196
+ output.mkdir(parents=True, exist_ok=False)
197
+ model.eval().save_pretrained(output, safe_serialization=True)
198
+ _copy_tokenizer_files(source, output)
199
+ report = {
200
+ "schema_version": "qwen35-student-materialization-v1",
201
+ "stage": stage.name,
202
+ "source": str(source),
203
+ "output": str(output),
204
+ "selected_layers": list(stage.selected_layers),
205
+ "source_hashes": checkpoint_hashes(source),
206
+ "output_hashes": checkpoint_hashes(output),
207
+ "mapped_tensor_count": len(mapped),
208
+ "task_labels_used": False,
209
+ }
210
+ (output / "materialization_manifest.json").write_text(
211
+ json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
212
+ )
213
+ return report
214
+
215
+
216
+ def finalize_base_checkpoint(
217
+ source: Path,
218
+ output: Path,
219
+ *,
220
+ training_corpus_rows: int = 4096,
221
+ training_objective: str = "hidden_and_interface_representation_distillation",
222
+ ) -> dict[str, Any]:
223
+ """Create a classification-ready, headless canonical base without changing weights."""
224
+ from transformers import AutoModel, AutoModelForSequenceClassification, AutoTokenizer
225
+
226
+ if training_corpus_rows < 0:
227
+ raise ValueError("training_corpus_rows must be non-negative")
228
+ if not training_objective.strip():
229
+ raise ValueError("training_objective must be non-empty")
230
+ source = source.resolve()
231
+ output = output.resolve()
232
+ if output.exists():
233
+ raise FileExistsError(output)
234
+ source_hashes = checkpoint_hashes(source)
235
+ config_path = source / "config.json"
236
+ document = json.loads(config_path.read_text(encoding="utf-8"))
237
+ tokenizer = AutoTokenizer.from_pretrained(
238
+ source, local_files_only=True, trust_remote_code=False
239
+ )
240
+ if tokenizer.eos_token_id is None:
241
+ raise ValueError("tokenizer has no EOS token for classification padding")
242
+ if tokenizer.pad_token_id is None:
243
+ tokenizer.pad_token = tokenizer.eos_token
244
+ document["architectures"] = ["Qwen3_5TextModel"]
245
+ document["pad_token_id"] = int(tokenizer.pad_token_id)
246
+ document["eos_token_id"] = int(tokenizer.eos_token_id)
247
+ document["use_cache"] = False
248
+ document["standalone4l_role"] = "task_agnostic_classification_base_4l"
249
+ document.pop("id2label", None)
250
+ document.pop("label2id", None)
251
+ document.pop("num_labels", None)
252
+ document.pop("problem_type", None)
253
+ document["standalone4l_base"] = {
254
+ "schema_version": "standalone4l-task-agnostic-base-v1",
255
+ "role": "classification_ready_headless_text_backbone",
256
+ "task_labels_used_in_base_training": False,
257
+ "semeval_used_in_base_training": False,
258
+ "tokenizer": "upstream_full_qwen35",
259
+ "training_corpus_rows": training_corpus_rows,
260
+ "training_objective": training_objective,
261
+ "published_classification_head": False,
262
+ }
263
+ shutil.copytree(source, output)
264
+ (output / "config.json").write_text(
265
+ json.dumps(document, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
266
+ )
267
+ tokenizer.save_pretrained(output)
268
+ weight_hashes_before = {
269
+ name: value
270
+ for name, value in source_hashes.items()
271
+ if name.endswith(".safetensors")
272
+ }
273
+ weight_hashes_after = {
274
+ name: value
275
+ for name, value in checkpoint_hashes(output).items()
276
+ if name.endswith(".safetensors")
277
+ }
278
+ if weight_hashes_before != weight_hashes_after:
279
+ raise RuntimeError("canonical finalization changed model weight bytes")
280
+ base = AutoModel.from_pretrained(
281
+ output, local_files_only=True, trust_remote_code=False, dtype=torch.bfloat16
282
+ ).eval()
283
+ batch = tokenizer(
284
+ ["A short classification input.", "A second input."],
285
+ return_tensors="pt",
286
+ padding=True,
287
+ )
288
+ with torch.inference_mode():
289
+ hidden = base(**batch, use_cache=False).last_hidden_state
290
+ if not bool(torch.isfinite(hidden).all()):
291
+ raise RuntimeError("canonical base reload produced non-finite hidden states")
292
+ classifier = AutoModelForSequenceClassification.from_pretrained(
293
+ output,
294
+ local_files_only=True,
295
+ trust_remote_code=False,
296
+ num_labels=3,
297
+ ignore_mismatched_sizes=True,
298
+ dtype=torch.bfloat16,
299
+ ).eval()
300
+ with torch.inference_mode():
301
+ logits = classifier(**batch).logits
302
+ if logits.shape != (2, 3) or not bool(torch.isfinite(logits).all()):
303
+ raise RuntimeError("classification-head attachment validation failed")
304
+ report = {
305
+ "schema_version": "standalone4l-base-finalization-v1",
306
+ "source": str(source),
307
+ "output": str(output),
308
+ "source_hashes": source_hashes,
309
+ "output_hashes": checkpoint_hashes(output),
310
+ "weight_bytes_preserved": True,
311
+ "pad_token_id": int(tokenizer.pad_token_id),
312
+ "eos_token_id": int(tokenizer.eos_token_id),
313
+ "base_reload_finite": True,
314
+ "classification_head_attachment": {
315
+ "validated": True,
316
+ "test_num_labels": 3,
317
+ "test_batch_size": 2,
318
+ "published_head": False,
319
+ },
320
+ }
321
+ (output / "finalization_manifest.json").write_text(
322
+ json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
323
+ )
324
+ return report
distillation/qwen35_distill/classification.py ADDED
@@ -0,0 +1,386 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import json
5
+ import math
6
+ import platform
7
+ import random
8
+ import shutil
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Any
12
+ from zoneinfo import ZoneInfo
13
+
14
+ import torch
15
+ from torch.optim import AdamW
16
+ from transformers import AutoConfig, AutoModelForSequenceClassification, AutoTokenizer
17
+ from transformers.models.qwen3_5.modeling_qwen3_5 import (
18
+ Qwen3_5TextForSequenceClassification,
19
+ Qwen3_5TextRotaryEmbedding,
20
+ )
21
+
22
+ from .checkpoint import (
23
+ _copy_tokenizer_files,
24
+ _load_source_tensors,
25
+ _weight_files,
26
+ checkpoint_hashes,
27
+ map_layer_tensor_name,
28
+ )
29
+ from .layer_maps import StageMap
30
+ from .losses import classification_distillation_loss, task_agnostic_boundary_loss
31
+ from .schema import TaskMode, read_jsonl, validate_dataset
32
+ from .training import _dtype
33
+
34
+
35
+ def labels_from_file(path: Path) -> list[str]:
36
+ labels = json.loads(path.read_text(encoding="utf-8"))
37
+ if not isinstance(labels, list) or any(
38
+ not isinstance(label, str) or not label for label in labels
39
+ ):
40
+ raise ValueError("labels file must be a JSON string list")
41
+ if len(set(labels)) != len(labels) or len(labels) < 2:
42
+ raise ValueError("labels must contain at least two unique values")
43
+ return labels
44
+
45
+
46
+ def encode_targets(
47
+ rows: list[dict[str, Any]], *, mode: TaskMode, labels: list[str]
48
+ ) -> torch.Tensor:
49
+ label_to_id = {label: index for index, label in enumerate(labels)}
50
+ if mode == "single_label":
51
+ return torch.tensor([label_to_id[row["label"]] for row in rows], dtype=torch.long)
52
+ if mode == "multilabel":
53
+ target = torch.zeros((len(rows), len(labels)), dtype=torch.float32)
54
+ for row_index, row in enumerate(rows):
55
+ for label in row["labels"]:
56
+ target[row_index, label_to_id[label]] = 1.0
57
+ return target
58
+ raise ValueError("classification requires single_label or multilabel mode")
59
+
60
+
61
+ def materialize_classifier(
62
+ source: Path, output: Path, stage: StageMap
63
+ ) -> dict[str, Any]:
64
+ """Copy a trained classifier head and the selected backbone layers."""
65
+ source = source.resolve()
66
+ output = output.resolve()
67
+ if output.exists():
68
+ raise FileExistsError(output)
69
+ config = AutoConfig.from_pretrained(source, local_files_only=True)
70
+ target_types = stage.validate(config.layer_types)
71
+ target_config = copy.deepcopy(config)
72
+ target_config.num_hidden_layers = stage.target_depth
73
+ target_config.layer_types = list(target_types)
74
+ target_config.architectures = ["Qwen3_5TextForSequenceClassification"]
75
+ target_config.use_cache = False
76
+ target_config.standalone4l_distillation = {
77
+ "schema_version": "qwen35-classifier-stage-map-v1",
78
+ "stage": stage.name,
79
+ "selected_layers": list(stage.selected_layers),
80
+ "classification_head_copied": True,
81
+ }
82
+ with torch.device("meta"):
83
+ model = Qwen3_5TextForSequenceClassification(target_config)
84
+ source_state = _load_source_tensors(_weight_files(source), prefix="")
85
+ mapped: dict[str, torch.Tensor] = {}
86
+ for source_name, tensor in source_state.items():
87
+ if source_name.startswith("model.layers."):
88
+ target_name = "model." + (
89
+ map_layer_tensor_name(
90
+ source_name.removeprefix("model."), stage.selected_layers
91
+ )
92
+ or ""
93
+ )
94
+ if target_name == "model.":
95
+ continue
96
+ else:
97
+ target_name = source_name
98
+ if target_name in mapped:
99
+ raise ValueError(f"duplicate mapped tensor {target_name}")
100
+ mapped[target_name] = tensor
101
+ expected = set(model.state_dict())
102
+ if set(mapped) != expected:
103
+ raise ValueError(
104
+ f"classifier mapping mismatch: missing={sorted(expected-set(mapped))[:10]}, "
105
+ f"unexpected={sorted(set(mapped)-expected)[:10]}"
106
+ )
107
+ model.load_state_dict(mapped, strict=True, assign=True)
108
+ fresh_rotary = Qwen3_5TextRotaryEmbedding(target_config, device="cpu")
109
+ model.model.rotary_emb.inv_freq = fresh_rotary.inv_freq
110
+ model.model.rotary_emb.original_inv_freq = fresh_rotary.original_inv_freq
111
+ output.mkdir(parents=True, exist_ok=False)
112
+ model.eval().save_pretrained(output, safe_serialization=True)
113
+ _copy_tokenizer_files(source, output)
114
+ report = {
115
+ "schema_version": "qwen35-classifier-materialization-v1",
116
+ "stage": stage.name,
117
+ "source": str(source),
118
+ "output": str(output),
119
+ "selected_layers": list(stage.selected_layers),
120
+ "head_copied": True,
121
+ "source_hashes": checkpoint_hashes(source),
122
+ "output_hashes": checkpoint_hashes(output),
123
+ }
124
+ (output / "materialization_manifest.json").write_text(
125
+ json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
126
+ )
127
+ return report
128
+
129
+
130
+ def _batch_rows(
131
+ rows: list[dict[str, Any]], batch_size: int, seed: int
132
+ ) -> list[list[dict[str, Any]]]:
133
+ shuffled = list(rows)
134
+ random.Random(seed).shuffle(shuffled)
135
+ return [shuffled[i : i + batch_size] for i in range(0, len(shuffled), batch_size)]
136
+
137
+
138
+ def _prepare_model(
139
+ checkpoint: Path,
140
+ *,
141
+ labels: list[str],
142
+ mode: TaskMode,
143
+ dtype: torch.dtype,
144
+ attach_head: bool,
145
+ ) -> Qwen3_5TextForSequenceClassification:
146
+ kwargs = {
147
+ "local_files_only": True,
148
+ "dtype": dtype,
149
+ "num_labels": len(labels),
150
+ "id2label": {index: label for index, label in enumerate(labels)},
151
+ "label2id": {label: index for index, label in enumerate(labels)},
152
+ "problem_type": (
153
+ "single_label_classification"
154
+ if mode == "single_label"
155
+ else "multi_label_classification"
156
+ ),
157
+ }
158
+ if attach_head:
159
+ kwargs["ignore_mismatched_sizes"] = True
160
+ model = AutoModelForSequenceClassification.from_pretrained(checkpoint, **kwargs)
161
+ if not isinstance(model, Qwen3_5TextForSequenceClassification):
162
+ raise TypeError("checkpoint did not load as Qwen3_5TextForSequenceClassification")
163
+ return model
164
+
165
+
166
+ def _train_classifier(
167
+ *,
168
+ model: Qwen3_5TextForSequenceClassification,
169
+ tokenizer: Any,
170
+ train_rows: list[dict[str, Any]],
171
+ labels: list[str],
172
+ mode: TaskMode,
173
+ device: torch.device,
174
+ max_length: int,
175
+ epochs: int,
176
+ batch_size: int,
177
+ gradient_accumulation: int,
178
+ learning_rate: float,
179
+ seed: int,
180
+ teacher: Qwen3_5TextForSequenceClassification | None = None,
181
+ stage: StageMap | None = None,
182
+ temperature: float = 2.0,
183
+ hard_weight: float = 1.0,
184
+ logit_weight: float = 1.0,
185
+ hidden_weight: float = 1.0,
186
+ ) -> list[dict[str, Any]]:
187
+ model.train()
188
+ model.gradient_checkpointing_enable()
189
+ optimizer = AdamW(model.parameters(), lr=learning_rate, weight_decay=0.01)
190
+ history: list[dict[str, Any]] = []
191
+ optimizer.zero_grad(set_to_none=True)
192
+ for epoch in range(1, epochs + 1):
193
+ total = 0.0
194
+ batches = _batch_rows(train_rows, batch_size, seed + epoch)
195
+ for batch_index, rows in enumerate(batches, start=1):
196
+ encoded = tokenizer(
197
+ [row["text"] for row in rows],
198
+ return_tensors="pt",
199
+ padding=True,
200
+ truncation=True,
201
+ max_length=max_length,
202
+ )
203
+ encoded = {key: value.to(device) for key, value in encoded.items()}
204
+ targets = encode_targets(rows, mode=mode, labels=labels).to(device)
205
+ output = model(**encoded, output_hidden_states=True, use_cache=False)
206
+ if teacher is None:
207
+ if mode == "single_label":
208
+ loss = torch.nn.functional.cross_entropy(
209
+ output.logits.float(), targets
210
+ )
211
+ else:
212
+ loss = torch.nn.functional.binary_cross_entropy_with_logits(
213
+ output.logits.float(), targets.float()
214
+ )
215
+ else:
216
+ assert stage is not None
217
+ with torch.inference_mode():
218
+ teacher_output = teacher(
219
+ **encoded, output_hidden_states=True, use_cache=False
220
+ )
221
+ classification_loss, _ = classification_distillation_loss(
222
+ output.logits,
223
+ teacher_output.logits,
224
+ targets,
225
+ mode=mode,
226
+ temperature=temperature,
227
+ hard_weight=hard_weight,
228
+ logit_weight=logit_weight,
229
+ )
230
+ hidden_loss, _ = task_agnostic_boundary_loss(
231
+ output.hidden_states,
232
+ teacher_output.hidden_states,
233
+ stage.teacher_boundary_indices(),
234
+ encoded["attention_mask"],
235
+ )
236
+ loss = classification_loss + hidden_weight * hidden_loss
237
+ if not bool(torch.isfinite(loss)):
238
+ raise FloatingPointError(f"non-finite loss at epoch {epoch} batch {batch_index}")
239
+ (loss / gradient_accumulation).backward()
240
+ total += float(loss.detach())
241
+ if (
242
+ batch_index % gradient_accumulation == 0
243
+ or batch_index == len(batches)
244
+ ):
245
+ norm = torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
246
+ if not math.isfinite(float(norm)):
247
+ raise FloatingPointError("non-finite gradient norm")
248
+ optimizer.step()
249
+ optimizer.zero_grad(set_to_none=True)
250
+ value = total / len(batches)
251
+ history.append({"epoch": epoch, "batches": len(batches), "mean_loss": value})
252
+ print(
253
+ json.dumps(
254
+ {"event": "classifier_epoch", "epoch": epoch, "mean_loss": value},
255
+ ensure_ascii=False,
256
+ ),
257
+ flush=True,
258
+ )
259
+ return history
260
+
261
+
262
+ def train_classifier(
263
+ *,
264
+ checkpoint: Path,
265
+ train_path: Path,
266
+ validation_path: Path | None,
267
+ labels_path: Path,
268
+ output_path: Path,
269
+ mode: TaskMode,
270
+ device_name: str,
271
+ dtype_name: str,
272
+ max_length: int,
273
+ epochs: int,
274
+ batch_size: int,
275
+ gradient_accumulation: int,
276
+ learning_rate: float,
277
+ seed: int,
278
+ teacher_path: Path | None = None,
279
+ stage: StageMap | None = None,
280
+ ) -> dict[str, Any]:
281
+ if output_path.exists():
282
+ raise FileExistsError(output_path)
283
+ if mode not in ("single_label", "multilabel"):
284
+ raise ValueError("mode must be single_label or multilabel")
285
+ labels = labels_from_file(labels_path)
286
+ split_paths = {"train": train_path}
287
+ if validation_path:
288
+ split_paths["validation"] = validation_path
289
+ audit = validate_dataset(split_paths, mode=mode, labels=labels)
290
+ rows = read_jsonl(train_path)
291
+ device = torch.device(device_name)
292
+ if device.type == "cuda" and not torch.cuda.is_available():
293
+ raise RuntimeError("CUDA requested but unavailable")
294
+ dtype = _dtype(dtype_name)
295
+ tokenizer = AutoTokenizer.from_pretrained(
296
+ checkpoint, local_files_only=True, trust_remote_code=False
297
+ )
298
+ if tokenizer.pad_token_id is None:
299
+ tokenizer.pad_token = tokenizer.eos_token
300
+ model = _prepare_model(
301
+ checkpoint,
302
+ labels=labels,
303
+ mode=mode,
304
+ dtype=dtype,
305
+ attach_head=teacher_path is None,
306
+ ).to(device)
307
+ model.config.pad_token_id = tokenizer.pad_token_id
308
+ teacher = None
309
+ teacher_hashes = None
310
+ if teacher_path is not None:
311
+ if stage is None:
312
+ raise ValueError("distillation requires a stage")
313
+ teacher_hashes = checkpoint_hashes(teacher_path)
314
+ teacher = _prepare_model(
315
+ teacher_path,
316
+ labels=labels,
317
+ mode=mode,
318
+ dtype=dtype,
319
+ attach_head=False,
320
+ ).to(device).eval()
321
+ stage.validate(teacher.config.layer_types)
322
+ for parameter in teacher.parameters():
323
+ parameter.requires_grad_(False)
324
+ history = _train_classifier(
325
+ model=model,
326
+ tokenizer=tokenizer,
327
+ train_rows=rows,
328
+ labels=labels,
329
+ mode=mode,
330
+ device=device,
331
+ max_length=max_length,
332
+ epochs=epochs,
333
+ batch_size=batch_size,
334
+ gradient_accumulation=gradient_accumulation,
335
+ learning_rate=learning_rate,
336
+ seed=seed,
337
+ teacher=teacher,
338
+ stage=stage,
339
+ )
340
+ if teacher_path is not None and checkpoint_hashes(teacher_path) != teacher_hashes:
341
+ raise RuntimeError("teacher checkpoint changed during classifier distillation")
342
+ output_path.mkdir(parents=True, exist_ok=False)
343
+ model.eval().save_pretrained(output_path, safe_serialization=True)
344
+ tokenizer.save_pretrained(output_path)
345
+ reloaded = AutoModelForSequenceClassification.from_pretrained(
346
+ output_path, local_files_only=True, dtype=dtype
347
+ ).to(device).eval()
348
+ sample = tokenizer(rows[0]["text"], return_tensors="pt", truncation=True, max_length=max_length)
349
+ sample = {key: value.to(device) for key, value in sample.items()}
350
+ with torch.inference_mode():
351
+ logits = reloaded(**sample).logits
352
+ if logits.shape != (1, len(labels)) or not bool(torch.isfinite(logits).all()):
353
+ raise RuntimeError("fresh classifier reload failed")
354
+ report = {
355
+ "schema_version": "qwen35-classifier-training-v1",
356
+ "status": "completed",
357
+ "timestamp_kst": datetime.now(ZoneInfo("Asia/Seoul")).isoformat(),
358
+ "workflow": "task_specific_distillation" if teacher_path else "supervised_finetune",
359
+ "mode": mode,
360
+ "labels": labels,
361
+ "checkpoint": str(checkpoint.resolve()),
362
+ "teacher": str(teacher_path.resolve()) if teacher_path else None,
363
+ "stage": stage.name if stage else None,
364
+ "dataset_audit": audit,
365
+ "training": {
366
+ "seed": seed,
367
+ "epochs": epochs,
368
+ "batch_size": batch_size,
369
+ "gradient_accumulation": gradient_accumulation,
370
+ "max_length": max_length,
371
+ "learning_rate": learning_rate,
372
+ "history": history,
373
+ },
374
+ "environment": {
375
+ "python": platform.python_version(),
376
+ "torch": torch.__version__,
377
+ "device": str(device),
378
+ "dtype": dtype_name,
379
+ },
380
+ "fresh_reload_finite": True,
381
+ "output_hashes": checkpoint_hashes(output_path),
382
+ }
383
+ (output_path / "training_manifest.json").write_text(
384
+ json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
385
+ )
386
+ return report
distillation/qwen35_distill/cli.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+
7
+ from .checkpoint import (
8
+ extract_text_checkpoint,
9
+ finalize_base_checkpoint,
10
+ materialize_student,
11
+ )
12
+ from .classification import materialize_classifier, train_classifier
13
+ from .layer_maps import get_stage
14
+ from .schema import validate_dataset
15
+ from .training import task_agnostic_distill
16
+
17
+
18
+ def _print(value: object) -> None:
19
+ print(json.dumps(value, ensure_ascii=False, indent=2), flush=True)
20
+
21
+
22
+ def build_parser() -> argparse.ArgumentParser:
23
+ parser = argparse.ArgumentParser(prog="qwen35-distill")
24
+ subparsers = parser.add_subparsers(dest="command", required=True)
25
+
26
+ validate = subparsers.add_parser("validate-dataset")
27
+ validate.add_argument(
28
+ "--mode", choices=("unlabeled", "single_label", "multilabel"), required=True
29
+ )
30
+ validate.add_argument("--train", type=Path, required=True)
31
+ validate.add_argument("--validation", type=Path)
32
+ validate.add_argument("--test", type=Path)
33
+ validate.add_argument("--labels", type=Path)
34
+
35
+ extract = subparsers.add_parser("extract-text")
36
+ extract.add_argument("--source", type=Path, required=True)
37
+ extract.add_argument("--output", type=Path, required=True)
38
+
39
+ materialize = subparsers.add_parser("materialize")
40
+ materialize.add_argument("--source", type=Path, required=True)
41
+ materialize.add_argument("--output", type=Path, required=True)
42
+ materialize.add_argument(
43
+ "--stage", choices=("24to8", "8to6", "6to4"), required=True
44
+ )
45
+
46
+ materialize_head = subparsers.add_parser("materialize-classifier")
47
+ materialize_head.add_argument("--source", type=Path, required=True)
48
+ materialize_head.add_argument("--output", type=Path, required=True)
49
+ materialize_head.add_argument(
50
+ "--stage", choices=("24to8", "8to6", "6to4"), required=True
51
+ )
52
+
53
+ finalize = subparsers.add_parser("finalize-base")
54
+ finalize.add_argument("--source", type=Path, required=True)
55
+ finalize.add_argument("--output", type=Path, required=True)
56
+ finalize.add_argument("--training-corpus-rows", type=int, default=4096)
57
+ finalize.add_argument(
58
+ "--training-objective",
59
+ default="hidden_and_interface_representation_distillation",
60
+ )
61
+
62
+ distill = subparsers.add_parser("distill")
63
+ distill.add_argument("--teacher", type=Path, required=True)
64
+ distill.add_argument("--student", type=Path, required=True)
65
+ distill.add_argument("--data", type=Path, required=True)
66
+ distill.add_argument("--output", type=Path, required=True)
67
+ distill.add_argument(
68
+ "--stage", choices=("24to8", "8to6", "6to4"), required=True
69
+ )
70
+ distill.add_argument("--device", default="cuda")
71
+ distill.add_argument(
72
+ "--dtype", choices=("float32", "float16", "bfloat16"), default="bfloat16"
73
+ )
74
+ distill.add_argument("--max-rows", type=int, default=50000)
75
+ distill.add_argument("--max-length", type=int, default=128)
76
+ distill.add_argument("--epochs", type=int, default=1)
77
+ distill.add_argument("--batch-size", type=int, default=1)
78
+ distill.add_argument("--gradient-accumulation", type=int, default=8)
79
+ distill.add_argument("--learning-rate", type=float, default=2e-5)
80
+ distill.add_argument("--seed", type=int, default=41)
81
+ distill.add_argument("--progress-every", type=int, default=100)
82
+
83
+ for name in ("finetune", "distill-classifier"):
84
+ command = subparsers.add_parser(name)
85
+ command.add_argument("--checkpoint", type=Path, required=True)
86
+ command.add_argument("--train", type=Path, required=True)
87
+ command.add_argument("--validation", type=Path)
88
+ command.add_argument("--labels", type=Path, required=True)
89
+ command.add_argument("--output", type=Path, required=True)
90
+ command.add_argument(
91
+ "--mode", choices=("single_label", "multilabel"), required=True
92
+ )
93
+ command.add_argument("--device", default="cuda")
94
+ command.add_argument(
95
+ "--dtype",
96
+ choices=("float32", "float16", "bfloat16"),
97
+ default="bfloat16",
98
+ )
99
+ command.add_argument("--max-length", type=int, default=256)
100
+ command.add_argument("--epochs", type=int, default=3)
101
+ command.add_argument("--batch-size", type=int, default=1)
102
+ command.add_argument("--gradient-accumulation", type=int, default=8)
103
+ command.add_argument("--learning-rate", type=float, default=2e-5)
104
+ command.add_argument("--seed", type=int, default=41)
105
+ if name == "distill-classifier":
106
+ command.add_argument("--teacher", type=Path, required=True)
107
+ command.add_argument(
108
+ "--stage", choices=("24to8", "8to6", "6to4"), required=True
109
+ )
110
+ return parser
111
+
112
+
113
+ def main() -> None:
114
+ args = build_parser().parse_args()
115
+ if args.command == "validate-dataset":
116
+ paths = {"train": args.train}
117
+ if args.validation:
118
+ paths["validation"] = args.validation
119
+ if args.test:
120
+ paths["test"] = args.test
121
+ labels = None
122
+ if args.labels:
123
+ value = json.loads(args.labels.read_text(encoding="utf-8"))
124
+ if not isinstance(value, list) or any(
125
+ not isinstance(label, str) for label in value
126
+ ):
127
+ raise ValueError("--labels must contain a JSON string list")
128
+ labels = value
129
+ _print(validate_dataset(paths, mode=args.mode, labels=labels))
130
+ elif args.command == "extract-text":
131
+ _print(extract_text_checkpoint(args.source, args.output))
132
+ elif args.command == "materialize":
133
+ _print(materialize_student(args.source, args.output, get_stage(args.stage)))
134
+ elif args.command == "materialize-classifier":
135
+ _print(materialize_classifier(args.source, args.output, get_stage(args.stage)))
136
+ elif args.command == "finalize-base":
137
+ _print(
138
+ finalize_base_checkpoint(
139
+ args.source,
140
+ args.output,
141
+ training_corpus_rows=args.training_corpus_rows,
142
+ training_objective=args.training_objective,
143
+ )
144
+ )
145
+ elif args.command == "distill":
146
+ _print(
147
+ task_agnostic_distill(
148
+ teacher_path=args.teacher,
149
+ student_path=args.student,
150
+ data_path=args.data,
151
+ output_path=args.output,
152
+ stage=get_stage(args.stage),
153
+ device_name=args.device,
154
+ dtype_name=args.dtype,
155
+ max_rows=args.max_rows,
156
+ max_length=args.max_length,
157
+ epochs=args.epochs,
158
+ batch_size=args.batch_size,
159
+ gradient_accumulation=args.gradient_accumulation,
160
+ learning_rate=args.learning_rate,
161
+ seed=args.seed,
162
+ progress_every=args.progress_every,
163
+ )
164
+ )
165
+ elif args.command in ("finetune", "distill-classifier"):
166
+ _print(
167
+ train_classifier(
168
+ checkpoint=args.checkpoint,
169
+ train_path=args.train,
170
+ validation_path=args.validation,
171
+ labels_path=args.labels,
172
+ output_path=args.output,
173
+ mode=args.mode,
174
+ device_name=args.device,
175
+ dtype_name=args.dtype,
176
+ max_length=args.max_length,
177
+ epochs=args.epochs,
178
+ batch_size=args.batch_size,
179
+ gradient_accumulation=args.gradient_accumulation,
180
+ learning_rate=args.learning_rate,
181
+ seed=args.seed,
182
+ teacher_path=(
183
+ args.teacher if args.command == "distill-classifier" else None
184
+ ),
185
+ stage=(
186
+ get_stage(args.stage)
187
+ if args.command == "distill-classifier"
188
+ else None
189
+ ),
190
+ )
191
+ )
192
+ else:
193
+ raise AssertionError(args.command)
194
+
195
+
196
+ if __name__ == "__main__":
197
+ main()
distillation/qwen35_distill/layer_maps.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Sequence
5
+
6
+
7
+ LINEAR = "linear_attention"
8
+ FULL = "full_attention"
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class StageMap:
13
+ name: str
14
+ source_depth: int
15
+ target_depth: int
16
+ selected_layers: tuple[int, ...]
17
+ expected_source_types: tuple[str, ...]
18
+ expected_target_types: tuple[str, ...]
19
+
20
+ def validate(self, source_types: Sequence[str]) -> tuple[str, ...]:
21
+ observed = tuple(str(value) for value in source_types)
22
+ if len(observed) != self.source_depth:
23
+ raise ValueError(
24
+ f"{self.name}: source depth {len(observed)} != {self.source_depth}"
25
+ )
26
+ if observed != self.expected_source_types:
27
+ raise ValueError(f"{self.name}: source layer-type contract mismatch")
28
+ if len(self.selected_layers) != self.target_depth:
29
+ raise ValueError(f"{self.name}: selected-layer count mismatch")
30
+ if self.selected_layers[0] != 0:
31
+ raise ValueError(f"{self.name}: first source layer must be retained")
32
+ if self.selected_layers[-1] != self.source_depth - 1:
33
+ raise ValueError(f"{self.name}: final source layer must be retained")
34
+ if tuple(sorted(set(self.selected_layers))) != self.selected_layers:
35
+ raise ValueError(f"{self.name}: selected layers must be sorted and unique")
36
+ target = tuple(observed[index] for index in self.selected_layers)
37
+ if target != self.expected_target_types:
38
+ raise ValueError(
39
+ f"{self.name}: target types {target} != {self.expected_target_types}"
40
+ )
41
+ return target
42
+
43
+ def teacher_boundary_indices(self) -> tuple[int, ...]:
44
+ """Teacher hidden-state indices aligned to every student boundary."""
45
+ return self.selected_layers + (self.source_depth,)
46
+
47
+
48
+ QWEN35_24_TYPES = (LINEAR, LINEAR, LINEAR, FULL) * 6
49
+ QWEN35_8_TYPES = (LINEAR, LINEAR, LINEAR, FULL, LINEAR, LINEAR, LINEAR, FULL)
50
+ QWEN35_6_TYPES = (LINEAR, LINEAR, FULL, LINEAR, LINEAR, FULL)
51
+ QWEN35_4_TYPES = (LINEAR, FULL, LINEAR, FULL)
52
+
53
+ DEFAULT_STAGE_MAPS: dict[str, StageMap] = {
54
+ "24to8": StageMap(
55
+ name="24to8",
56
+ source_depth=24,
57
+ target_depth=8,
58
+ selected_layers=(0, 4, 6, 11, 13, 16, 20, 23),
59
+ expected_source_types=QWEN35_24_TYPES,
60
+ expected_target_types=QWEN35_8_TYPES,
61
+ ),
62
+ "8to6": StageMap(
63
+ name="8to6",
64
+ source_depth=8,
65
+ target_depth=6,
66
+ selected_layers=(0, 1, 3, 4, 6, 7),
67
+ expected_source_types=QWEN35_8_TYPES,
68
+ expected_target_types=QWEN35_6_TYPES,
69
+ ),
70
+ "6to4": StageMap(
71
+ name="6to4",
72
+ source_depth=6,
73
+ target_depth=4,
74
+ selected_layers=(0, 2, 3, 5),
75
+ expected_source_types=QWEN35_6_TYPES,
76
+ expected_target_types=QWEN35_4_TYPES,
77
+ ),
78
+ }
79
+
80
+
81
+ def get_stage(name: str) -> StageMap:
82
+ try:
83
+ return DEFAULT_STAGE_MAPS[name]
84
+ except KeyError as exc:
85
+ raise ValueError(
86
+ f"unknown stage {name!r}; expected one of {sorted(DEFAULT_STAGE_MAPS)}"
87
+ ) from exc
distillation/qwen35_distill/losses.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+
5
+ import torch
6
+ from torch import Tensor
7
+ from torch.nn import functional as F
8
+
9
+
10
+ def masked_normalized_hidden_loss(
11
+ student: Tensor, teacher: Tensor, attention_mask: Tensor
12
+ ) -> Tensor:
13
+ if student.shape != teacher.shape:
14
+ raise ValueError(f"hidden shape mismatch: {student.shape} vs {teacher.shape}")
15
+ if student.ndim not in (3, 4):
16
+ raise ValueError("hidden tensors must be [B,S,H] or [B,K,S,H]")
17
+ if attention_mask.ndim != 2 or attention_mask.shape != student.shape[:1] + student.shape[-2:-1]:
18
+ raise ValueError("attention mask shape mismatch")
19
+ student_norm = F.normalize(student.float(), dim=-1)
20
+ teacher_norm = F.normalize(teacher.float(), dim=-1)
21
+ token_loss = 1.0 - (student_norm * teacher_norm).sum(dim=-1)
22
+ mask = attention_mask.to(dtype=token_loss.dtype)
23
+ if token_loss.ndim == 3:
24
+ mask = mask[:, None, :]
25
+ denominator = mask.sum() * (token_loss.shape[1] if token_loss.ndim == 3 else 1)
26
+ if float(denominator) <= 0:
27
+ raise ValueError("attention mask has no valid tokens")
28
+ return (token_loss * mask).sum() / denominator
29
+
30
+
31
+ def gather_boundaries(
32
+ hidden_states: Sequence[Tensor], indices: Sequence[int]
33
+ ) -> Tensor:
34
+ if not indices:
35
+ raise ValueError("boundary indices are empty")
36
+ if max(indices) >= len(hidden_states):
37
+ raise ValueError("boundary index outside hidden-state tuple")
38
+ return torch.stack([hidden_states[index] for index in indices], dim=1)
39
+
40
+
41
+ def task_agnostic_boundary_loss(
42
+ student_hidden_states: Sequence[Tensor],
43
+ teacher_hidden_states: Sequence[Tensor],
44
+ teacher_indices: Sequence[int],
45
+ attention_mask: Tensor,
46
+ ) -> tuple[Tensor, dict[str, Tensor]]:
47
+ student_indices = tuple(range(len(student_hidden_states)))
48
+ student = gather_boundaries(student_hidden_states, student_indices)
49
+ teacher = gather_boundaries(teacher_hidden_states, teacher_indices)
50
+ if student.shape != teacher.shape:
51
+ raise ValueError(
52
+ f"student/teacher boundary shape mismatch: {student.shape} vs {teacher.shape}"
53
+ )
54
+ all_loss = masked_normalized_hidden_loss(student, teacher, attention_mask)
55
+ final_loss = masked_normalized_hidden_loss(
56
+ student[:, -1], teacher[:, -1], attention_mask
57
+ )
58
+ interface_loss = masked_normalized_hidden_loss(
59
+ student[:, 1:-1], teacher[:, 1:-1], attention_mask
60
+ )
61
+ return 0.5 * (all_loss + final_loss), {
62
+ "all_boundaries": all_loss,
63
+ "interfaces": interface_loss,
64
+ "final": final_loss,
65
+ }
66
+
67
+
68
+ def classification_distillation_loss(
69
+ student_logits: Tensor,
70
+ teacher_logits: Tensor,
71
+ labels: Tensor,
72
+ *,
73
+ mode: str,
74
+ temperature: float,
75
+ hard_weight: float,
76
+ logit_weight: float,
77
+ ) -> tuple[Tensor, dict[str, Tensor]]:
78
+ if temperature <= 0:
79
+ raise ValueError("temperature must be positive")
80
+ if mode == "single_label":
81
+ hard = F.cross_entropy(student_logits.float(), labels.long())
82
+ soft = F.kl_div(
83
+ F.log_softmax(student_logits.float() / temperature, dim=-1),
84
+ F.softmax(teacher_logits.float() / temperature, dim=-1),
85
+ reduction="batchmean",
86
+ ) * temperature**2
87
+ elif mode == "multilabel":
88
+ hard = F.binary_cross_entropy_with_logits(
89
+ student_logits.float(), labels.float()
90
+ )
91
+ soft_targets = torch.sigmoid(teacher_logits.float() / temperature)
92
+ soft = F.binary_cross_entropy_with_logits(
93
+ student_logits.float() / temperature, soft_targets
94
+ ) * temperature**2
95
+ else:
96
+ raise ValueError(f"unsupported classification mode: {mode}")
97
+ total = hard_weight * hard + logit_weight * soft
98
+ return total, {"hard": hard, "logit": soft}
distillation/qwen35_distill/schema.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any, Iterable, Literal
7
+
8
+
9
+ TaskMode = Literal["unlabeled", "single_label", "multilabel"]
10
+
11
+
12
+ def sha256_file(path: Path) -> str:
13
+ digest = hashlib.sha256()
14
+ with path.open("rb") as handle:
15
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
16
+ digest.update(chunk)
17
+ return digest.hexdigest()
18
+
19
+
20
+ def read_jsonl(path: Path) -> list[dict[str, Any]]:
21
+ rows: list[dict[str, Any]] = []
22
+ with path.open("r", encoding="utf-8") as handle:
23
+ for line_number, line in enumerate(handle, start=1):
24
+ if not line.strip():
25
+ continue
26
+ value = json.loads(line)
27
+ if not isinstance(value, dict):
28
+ raise ValueError(f"{path}:{line_number}: row must be an object")
29
+ rows.append(value)
30
+ if not rows:
31
+ raise ValueError(f"{path}: dataset is empty")
32
+ return rows
33
+
34
+
35
+ def validate_rows(
36
+ rows: Iterable[dict[str, Any]],
37
+ *,
38
+ mode: TaskMode,
39
+ split: str,
40
+ known_labels: set[str] | None = None,
41
+ ) -> dict[str, Any]:
42
+ ids: set[str] = set()
43
+ groups: set[str] = set()
44
+ observed_labels: set[str] = set()
45
+ count = 0
46
+ for index, row in enumerate(rows, start=1):
47
+ count += 1
48
+ sample_id = row.get("id")
49
+ text = row.get("text")
50
+ if not isinstance(sample_id, str) or not sample_id.strip():
51
+ raise ValueError(f"{split} row {index}: id must be a non-empty string")
52
+ if sample_id in ids:
53
+ raise ValueError(f"{split}: duplicate id {sample_id!r}")
54
+ ids.add(sample_id)
55
+ if not isinstance(text, str) or not text.strip():
56
+ raise ValueError(f"{split} row {index}: text must be a non-empty string")
57
+ group = row.get("group_id")
58
+ if group is not None:
59
+ if not isinstance(group, str) or not group.strip():
60
+ raise ValueError(f"{split} row {index}: invalid group_id")
61
+ groups.add(group)
62
+ if mode == "unlabeled":
63
+ if "label" in row or "labels" in row:
64
+ raise ValueError(f"{split} row {index}: unlabeled data contains labels")
65
+ elif mode == "single_label":
66
+ label = row.get("label")
67
+ if not isinstance(label, str) or not label:
68
+ raise ValueError(f"{split} row {index}: missing single label")
69
+ if "labels" in row:
70
+ raise ValueError(f"{split} row {index}: both label and labels present")
71
+ observed_labels.add(label)
72
+ elif mode == "multilabel":
73
+ labels = row.get("labels")
74
+ if not isinstance(labels, list) or not labels:
75
+ raise ValueError(f"{split} row {index}: labels must be a non-empty list")
76
+ if "label" in row:
77
+ raise ValueError(f"{split} row {index}: both label and labels present")
78
+ if any(not isinstance(label, str) or not label for label in labels):
79
+ raise ValueError(f"{split} row {index}: invalid multilabel value")
80
+ if len(set(labels)) != len(labels):
81
+ raise ValueError(f"{split} row {index}: duplicate label")
82
+ observed_labels.update(labels)
83
+ else:
84
+ raise ValueError(f"unsupported mode: {mode}")
85
+ if known_labels is not None:
86
+ unknown = observed_labels - known_labels
87
+ if unknown:
88
+ raise ValueError(f"{split}: unknown labels {sorted(unknown)}")
89
+ return {
90
+ "split": split,
91
+ "rows": count,
92
+ "ids": ids,
93
+ "groups": groups,
94
+ "observed_labels": observed_labels,
95
+ }
96
+
97
+
98
+ def validate_dataset(
99
+ split_paths: dict[str, Path],
100
+ *,
101
+ mode: TaskMode,
102
+ labels: list[str] | None = None,
103
+ ) -> dict[str, Any]:
104
+ if not split_paths or "train" not in split_paths:
105
+ raise ValueError("a train split is required")
106
+ if labels is not None:
107
+ if mode == "unlabeled":
108
+ raise ValueError("unlabeled mode must not define labels")
109
+ if not labels or len(set(labels)) != len(labels):
110
+ raise ValueError("labels must be non-empty and unique")
111
+ known_labels = set(labels)
112
+ else:
113
+ known_labels = None
114
+ reports: dict[str, dict[str, Any]] = {}
115
+ all_ids: set[str] = set()
116
+ split_groups: dict[str, set[str]] = {}
117
+ for split, path in split_paths.items():
118
+ path = path.resolve()
119
+ rows = read_jsonl(path)
120
+ report = validate_rows(
121
+ rows, mode=mode, split=split, known_labels=known_labels
122
+ )
123
+ overlap = all_ids & report["ids"]
124
+ if overlap:
125
+ raise ValueError(f"cross-split duplicate IDs: {sorted(overlap)[:10]}")
126
+ all_ids.update(report["ids"])
127
+ split_groups[split] = report["groups"]
128
+ reports[split] = {
129
+ "path": str(path),
130
+ "sha256": sha256_file(path),
131
+ "rows": report["rows"],
132
+ "observed_labels": sorted(report["observed_labels"]),
133
+ }
134
+ split_names = list(split_groups)
135
+ for left_index, left in enumerate(split_names):
136
+ for right in split_names[left_index + 1 :]:
137
+ overlap = split_groups[left] & split_groups[right]
138
+ if overlap:
139
+ raise ValueError(
140
+ f"group leakage between {left} and {right}: {sorted(overlap)[:10]}"
141
+ )
142
+ return {
143
+ "schema_version": "qwen35-classification-dataset-audit-v1",
144
+ "mode": mode,
145
+ "label_order": labels,
146
+ "splits": reports,
147
+ "cross_split_id_overlap": 0,
148
+ "cross_split_group_overlap": 0,
149
+ }
distillation/qwen35_distill/training.py ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import math
5
+ import platform
6
+ import random
7
+ import shutil
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Any
11
+ from zoneinfo import ZoneInfo
12
+
13
+ import pyarrow.parquet as pq
14
+ import torch
15
+ from torch.optim import AdamW
16
+ from transformers import AutoTokenizer
17
+ from transformers.models.qwen3_5.modeling_qwen3_5 import Qwen3_5TextModel
18
+
19
+ from .checkpoint import checkpoint_hashes
20
+ from .layer_maps import StageMap
21
+ from .losses import task_agnostic_boundary_loss
22
+ from .schema import read_jsonl, sha256_file
23
+
24
+
25
+ def _read_unlabeled_texts(path: Path) -> list[str]:
26
+ if path.suffix == ".jsonl":
27
+ rows = read_jsonl(path)
28
+ texts = []
29
+ for index, row in enumerate(rows, start=1):
30
+ if "label" in row or "labels" in row:
31
+ raise ValueError(f"unlabeled input contains labels at row {index}")
32
+ text = row.get("text")
33
+ if isinstance(text, str) and text.strip():
34
+ texts.append(text)
35
+ return texts
36
+ if path.suffix == ".parquet":
37
+ table = pq.read_table(path, columns=["text"])
38
+ return [
39
+ text
40
+ for text in table.column("text").to_pylist()
41
+ if isinstance(text, str) and text.strip()
42
+ ]
43
+ raise ValueError("unlabeled data must be .jsonl or .parquet")
44
+
45
+
46
+ def _dtype(name: str) -> torch.dtype:
47
+ choices = {
48
+ "float32": torch.float32,
49
+ "float16": torch.float16,
50
+ "bfloat16": torch.bfloat16,
51
+ }
52
+ try:
53
+ return choices[name]
54
+ except KeyError as exc:
55
+ raise ValueError(f"unsupported dtype {name}") from exc
56
+
57
+
58
+ def _copy_non_weight_files(source: Path, output: Path) -> None:
59
+ for name in (
60
+ "tokenizer.json",
61
+ "tokenizer_config.json",
62
+ "vocab.json",
63
+ "merges.txt",
64
+ "chat_template.jinja",
65
+ "special_tokens_map.json",
66
+ ):
67
+ path = source / name
68
+ if path.is_file():
69
+ shutil.copy2(path, output / name)
70
+
71
+
72
+ def task_agnostic_distill(
73
+ *,
74
+ teacher_path: Path,
75
+ student_path: Path,
76
+ data_path: Path,
77
+ output_path: Path,
78
+ stage: StageMap,
79
+ device_name: str,
80
+ dtype_name: str,
81
+ max_rows: int,
82
+ max_length: int,
83
+ epochs: int,
84
+ batch_size: int,
85
+ gradient_accumulation: int,
86
+ learning_rate: float,
87
+ seed: int,
88
+ progress_every: int,
89
+ ) -> dict[str, Any]:
90
+ if output_path.exists():
91
+ raise FileExistsError(output_path)
92
+ if min(max_rows, max_length, epochs, batch_size, gradient_accumulation) < 1:
93
+ raise ValueError("row/length/epoch/batch/accumulation values must be positive")
94
+ if progress_every < 1:
95
+ raise ValueError("progress_every must be positive")
96
+ random.seed(seed)
97
+ torch.manual_seed(seed)
98
+ device = torch.device(device_name)
99
+ if device.type == "cuda" and not torch.cuda.is_available():
100
+ raise RuntimeError("CUDA requested but unavailable")
101
+ dtype = _dtype(dtype_name)
102
+ teacher_hashes_before = checkpoint_hashes(teacher_path)
103
+ student_hashes_before = checkpoint_hashes(student_path)
104
+ texts = _read_unlabeled_texts(data_path)
105
+ random.Random(seed).shuffle(texts)
106
+ texts = texts[:max_rows]
107
+ if not texts:
108
+ raise ValueError("no non-empty training texts")
109
+
110
+ tokenizer = AutoTokenizer.from_pretrained(
111
+ student_path, local_files_only=True, trust_remote_code=False
112
+ )
113
+ if tokenizer.pad_token_id is None:
114
+ tokenizer.pad_token = tokenizer.eos_token
115
+ teacher = Qwen3_5TextModel.from_pretrained(
116
+ teacher_path, local_files_only=True, torch_dtype=dtype
117
+ ).to(device)
118
+ student = Qwen3_5TextModel.from_pretrained(
119
+ student_path, local_files_only=True, torch_dtype=dtype
120
+ ).to(device)
121
+ stage.validate(teacher.config.layer_types)
122
+ if int(student.config.num_hidden_layers) != stage.target_depth:
123
+ raise ValueError("student depth does not match stage")
124
+ for parameter in teacher.parameters():
125
+ parameter.requires_grad_(False)
126
+ teacher.eval()
127
+ student.train()
128
+ student.config.pad_token_id = tokenizer.pad_token_id
129
+ student.gradient_checkpointing_enable()
130
+ optimizer = AdamW(student.parameters(), lr=learning_rate, weight_decay=0.01)
131
+ history: list[dict[str, Any]] = []
132
+ optimizer.zero_grad(set_to_none=True)
133
+ global_step = 0
134
+ optimizer_steps = 0
135
+
136
+ for epoch in range(1, epochs + 1):
137
+ epoch_total = 0.0
138
+ epoch_interface = 0.0
139
+ epoch_final = 0.0
140
+ epoch_batches = 0
141
+ for offset in range(0, len(texts), batch_size):
142
+ batch_texts = texts[offset : offset + batch_size]
143
+ encoded = tokenizer(
144
+ batch_texts,
145
+ return_tensors="pt",
146
+ padding=True,
147
+ truncation=True,
148
+ max_length=max_length,
149
+ )
150
+ encoded = {key: value.to(device) for key, value in encoded.items()}
151
+ with torch.inference_mode():
152
+ teacher_output = teacher(
153
+ **encoded, output_hidden_states=True, use_cache=False
154
+ )
155
+ student_output = student(
156
+ **encoded, output_hidden_states=True, use_cache=False
157
+ )
158
+ loss, components = task_agnostic_boundary_loss(
159
+ student_output.hidden_states,
160
+ teacher_output.hidden_states,
161
+ stage.teacher_boundary_indices(),
162
+ encoded["attention_mask"],
163
+ )
164
+ if not bool(torch.isfinite(loss)):
165
+ raise FloatingPointError(f"non-finite loss at step {global_step}")
166
+ (loss / gradient_accumulation).backward()
167
+ global_step += 1
168
+ epoch_batches += 1
169
+ epoch_total += float(loss.detach())
170
+ epoch_interface += float(components["interfaces"].detach())
171
+ epoch_final += float(components["final"].detach())
172
+ if global_step % gradient_accumulation == 0 or offset + batch_size >= len(texts):
173
+ grad_norm = torch.nn.utils.clip_grad_norm_(student.parameters(), 1.0)
174
+ if not math.isfinite(float(grad_norm)):
175
+ raise FloatingPointError("non-finite gradient norm")
176
+ optimizer.step()
177
+ optimizer.zero_grad(set_to_none=True)
178
+ optimizer_steps += 1
179
+ if global_step == 1 or global_step % progress_every == 0:
180
+ print(
181
+ json.dumps(
182
+ {
183
+ "event": "distill_progress",
184
+ "stage": stage.name,
185
+ "epoch": epoch,
186
+ "batch": epoch_batches,
187
+ "batches_per_epoch": math.ceil(len(texts) / batch_size),
188
+ "global_step": global_step,
189
+ "optimizer_steps": optimizer_steps,
190
+ "mean_loss_so_far": epoch_total / epoch_batches,
191
+ },
192
+ ensure_ascii=False,
193
+ ),
194
+ flush=True,
195
+ )
196
+ history.append(
197
+ {
198
+ "epoch": epoch,
199
+ "batches": epoch_batches,
200
+ "mean_total_loss": epoch_total / epoch_batches,
201
+ "mean_interface_loss": epoch_interface / epoch_batches,
202
+ "mean_final_loss": epoch_final / epoch_batches,
203
+ "optimizer_steps_total": optimizer_steps,
204
+ }
205
+ )
206
+
207
+ teacher_hashes_after = checkpoint_hashes(teacher_path)
208
+ if teacher_hashes_after != teacher_hashes_before:
209
+ raise RuntimeError("teacher checkpoint changed during distillation")
210
+ student.eval()
211
+ output_path.mkdir(parents=True, exist_ok=False)
212
+ student.save_pretrained(output_path, safe_serialization=True)
213
+ _copy_non_weight_files(student_path, output_path)
214
+ sample = tokenizer(
215
+ texts[0], return_tensors="pt", truncation=True, max_length=max_length
216
+ )
217
+ sample = {key: value.to(device) for key, value in sample.items()}
218
+ with torch.inference_mode():
219
+ forward = student(**sample, use_cache=False).last_hidden_state
220
+ if not bool(torch.isfinite(forward).all()):
221
+ raise RuntimeError("saved student produced non-finite forward before reload")
222
+ del student
223
+ if device.type == "cuda":
224
+ torch.cuda.empty_cache()
225
+ reloaded = Qwen3_5TextModel.from_pretrained(
226
+ output_path, local_files_only=True, torch_dtype=dtype
227
+ ).to(device).eval()
228
+ with torch.inference_mode():
229
+ reload_forward = reloaded(**sample, use_cache=False).last_hidden_state
230
+ if not bool(torch.isfinite(reload_forward).all()):
231
+ raise RuntimeError("fresh-reloaded student produced non-finite forward")
232
+ max_abs_reload = float((forward.float() - reload_forward.float()).abs().max())
233
+ report = {
234
+ "schema_version": "qwen35-task-agnostic-kd-v1",
235
+ "status": "completed",
236
+ "timestamp_kst": datetime.now(ZoneInfo("Asia/Seoul")).isoformat(),
237
+ "stage": stage.name,
238
+ "teacher": str(teacher_path.resolve()),
239
+ "student_initial": str(student_path.resolve()),
240
+ "output": str(output_path.resolve()),
241
+ "data": {
242
+ "path": str(data_path.resolve()),
243
+ "sha256": sha256_file(data_path),
244
+ "rows_used": len(texts),
245
+ "labels_read": False,
246
+ "semeval_used": False,
247
+ },
248
+ "training": {
249
+ "seed": seed,
250
+ "epochs": epochs,
251
+ "batch_size": batch_size,
252
+ "gradient_accumulation": gradient_accumulation,
253
+ "max_length": max_length,
254
+ "learning_rate": learning_rate,
255
+ "progress_every": progress_every,
256
+ "history": history,
257
+ },
258
+ "environment": {
259
+ "python": platform.python_version(),
260
+ "torch": torch.__version__,
261
+ "device": str(device),
262
+ "dtype": dtype_name,
263
+ },
264
+ "teacher_hashes_before": teacher_hashes_before,
265
+ "teacher_hashes_after": teacher_hashes_after,
266
+ "student_initial_hashes": student_hashes_before,
267
+ "output_hashes": checkpoint_hashes(output_path),
268
+ "teacher_immutable": True,
269
+ "fresh_reload_finite": True,
270
+ "fresh_reload_max_abs": max_abs_reload,
271
+ }
272
+ (output_path / "training_manifest.json").write_text(
273
+ json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
274
+ )
275
+ return report
docs/RELEASE_CONTRACT.md ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Unified 1-1-1 Hugging Face release contract
2
+
3
+ ## Repository identity
4
+
5
+ - New repository: `mp-juuuns/qwen35-standalone4l-classification-base`
6
+ - Existing repository preserved without mutation:
7
+ `mp-juuuns/qwen35-standalone4l-propaganda-classifier`
8
+ - Existing specialized fixed revision:
9
+ `e7e0ff16828052687d2e8dd7849e7a521629cf38`
10
+
11
+ ## The three public surfaces
12
+
13
+ 1. Repository root: a headless four-layer Qwen3.5 text backbone trained without
14
+ SemEval examples, labels, logits, thresholds, or evidence objectives.
15
+ 2. `models/semeval-propaganda/`: the existing three-seed task-specific
16
+ classifier copied byte-for-byte as a separate historical lineage. It is not
17
+ described as a fine-tuned child of the new root base.
18
+ 3. `distillation/`: the reusable 24L→8L→6L→4L platform for unlabeled
19
+ representation distillation and user-owned single-label or multilabel
20
+ classification data.
21
+
22
+ ## Base training contract
23
+
24
+ - Source: the text backbone and full tokenizer of `Qwen/Qwen3.5-0.8B`.
25
+ - Structural defaults:
26
+ - 24→8: `0,4,6,11,13,16,20,23`
27
+ - 8→6: `0,1,3,4,6,7`
28
+ - 6→4: `0,2,3,5`
29
+ - Task-free data: 4,096 deterministically shuffled non-empty rows from the
30
+ first locally cached WikiText-103 raw training shard.
31
+ - Objective: aligned hidden-boundary and final-representation distillation.
32
+ - One epoch per stage, seed 41, maximum length 128, batch size 1, gradient
33
+ accumulation 8, learning rate 2e-5, BF16 CUDA.
34
+ - No classification head is published at the root.
35
+
36
+ The WikiText dataset page currently has inconsistent license wording: metadata
37
+ lists CC BY-SA 3.0 and GFDL, while the prose says CC BY-SA 4.0. The release
38
+ redistributes no WikiText text and discloses both statements rather than
39
+ silently selecting one.
40
+
41
+ ## Transfer gate
42
+
43
+ The only downstream validation task is an article-level multilabel task derived
44
+ from SemEval-2020 Task 11. It is not the official span- or fragment-level task.
45
+ The public test split contains 55 previously opened articles, so results are
46
+ exploratory rather than confirmatory.
47
+
48
+ | Initialization | Seed 41 | Seed 42 | Seed 43 | Mean ± sample SD |
49
+ |---|---:|---:|---:|---:|
50
+ | Task-agnostic KD base | 0.58831 | 0.60284 | 0.57128 | 0.58748 ± 0.01579 |
51
+ | Structural 4L copy, no task-free KD | 0.51694 | 0.53494 | 0.50238 | 0.51809 ± 0.01631 |
52
+
53
+ The preregistered publication gate was that the task-agnostic base must not be
54
+ worse than the structural-copy control by more than 0.01 mean Macro-F1. The
55
+ observed mean difference was +0.06939, so the gate passed. Three training seeds
56
+ are not a confidence interval and no significance claim is made.
57
+
58
+ ## Release gates
59
+
60
+ - Root `model.safetensors` contains no `score.weight`.
61
+ - Root loads with `AutoModel`, and a temporary classification head can be
62
+ attached with `AutoModelForSequenceClassification`.
63
+ - Root weight SHA-256 is
64
+ `2732c616772fe320cdea228ab4554981418b1b2bf615c4183fb1ac8e6e2168d3`.
65
+ - Base manifests say `labels_read=false` and `semeval_used=false`.
66
+ - Nested specialized checkpoint hashes equal the already published release.
67
+ - The platform validates label order and ID/group split leakage, never
68
+ overwrites output directories, hashes inputs/outputs, verifies immutable
69
+ teachers, and reloads fresh checkpoints.
70
+ - The release contains no SemEval or WikiText source data.
71
+ - The old specialized repository revision and inventory are checked before and
72
+ after creation of the new repository.
73
+
merges.txt ADDED
The diff for this file is too large to render. See raw diff
 
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2732c616772fe320cdea228ab4554981418b1b2bf615c4183fb1ac8e6e2168d3
3
+ size 668198976
models/semeval-propaganda/LICENSE ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright 2018- The Hugging Face team. All rights reserved.
2
+
3
+ Apache License
4
+ Version 2.0, January 2004
5
+ http://www.apache.org/licenses/
6
+
7
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
8
+
9
+ 1. Definitions.
10
+
11
+ "License" shall mean the terms and conditions for use, reproduction,
12
+ and distribution as defined by Sections 1 through 9 of this document.
13
+
14
+ "Licensor" shall mean the copyright owner or entity authorized by
15
+ the copyright owner that is granting the License.
16
+
17
+ "Legal Entity" shall mean the union of the acting entity and all
18
+ other entities that control, are controlled by, or are under common
19
+ control with that entity. For the purposes of this definition,
20
+ "control" means (i) the power, direct or indirect, to cause the
21
+ direction or management of such entity, whether by contract or
22
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
23
+ outstanding shares, or (iii) beneficial ownership of such entity.
24
+
25
+ "You" (or "Your") shall mean an individual or Legal Entity
26
+ exercising permissions granted by this License.
27
+
28
+ "Source" form shall mean the preferred form for making modifications,
29
+ including but not limited to software source code, documentation
30
+ source, and configuration files.
31
+
32
+ "Object" form shall mean any form resulting from mechanical
33
+ transformation or translation of a Source form, including but
34
+ not limited to compiled object code, generated documentation,
35
+ and conversions to other media types.
36
+
37
+ "Work" shall mean the work of authorship, whether in Source or
38
+ Object form, made available under the License, as indicated by a
39
+ copyright notice that is included in or attached to the work
40
+ (an example is provided in the Appendix below).
41
+
42
+ "Derivative Works" shall mean any work, whether in Source or Object
43
+ form, that is based on (or derived from) the Work and for which the
44
+ editorial revisions, annotations, elaborations, or other modifications
45
+ represent, as a whole, an original work of authorship. For the purposes
46
+ of this License, Derivative Works shall not include works that remain
47
+ separable from, or merely link (or bind by name) to the interfaces of,
48
+ the Work and Derivative Works thereof.
49
+
50
+ "Contribution" shall mean any work of authorship, including
51
+ the original version of the Work and any modifications or additions
52
+ to that Work or Derivative Works thereof, that is intentionally
53
+ submitted to Licensor for inclusion in the Work by the copyright owner
54
+ or by an individual or Legal Entity authorized to submit on behalf of
55
+ the copyright owner. For the purposes of this definition, "submitted"
56
+ means any form of electronic, verbal, or written communication sent
57
+ to the Licensor or its representatives, including but not limited to
58
+ communication on electronic mailing lists, source code control systems,
59
+ and issue tracking systems that are managed by, or on behalf of, the
60
+ Licensor for the purpose of discussing and improving the Work, but
61
+ excluding communication that is conspicuously marked or otherwise
62
+ designated in writing by the copyright owner as "Not a Contribution."
63
+
64
+ "Contributor" shall mean Licensor and any individual or Legal Entity
65
+ on behalf of whom a Contribution has been received by Licensor and
66
+ subsequently incorporated within the Work.
67
+
68
+ 2. Grant of Copyright License. Subject to the terms and conditions of
69
+ this License, each Contributor hereby grants to You a perpetual,
70
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
71
+ copyright license to reproduce, prepare Derivative Works of,
72
+ publicly display, publicly perform, sublicense, and distribute the
73
+ Work and such Derivative Works in Source or Object form.
74
+
75
+ 3. Grant of Patent License. Subject to the terms and conditions of
76
+ this License, each Contributor hereby grants to You a perpetual,
77
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
78
+ (except as stated in this section) patent license to make, have made,
79
+ use, offer to sell, sell, import, and otherwise transfer the Work,
80
+ where such license applies only to those patent claims licensable
81
+ by such Contributor that are necessarily infringed by their
82
+ Contribution(s) alone or by combination of their Contribution(s)
83
+ with the Work to which such Contribution(s) was submitted. If You
84
+ institute patent litigation against any entity (including a
85
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
86
+ or a Contribution incorporated within the Work constitutes direct
87
+ or contributory patent infringement, then any patent licenses
88
+ granted to You under this License for that Work shall terminate
89
+ as of the date such litigation is filed.
90
+
91
+ 4. Redistribution. You may reproduce and distribute copies of the
92
+ Work or Derivative Works thereof in any medium, with or without
93
+ modifications, and in Source or Object form, provided that You
94
+ meet the following conditions:
95
+
96
+ (a) You must give any other recipients of the Work or
97
+ Derivative Works a copy of this License; and
98
+
99
+ (b) You must cause any modified files to carry prominent notices
100
+ stating that You changed the files; and
101
+
102
+ (c) You must retain, in the Source form of any Derivative Works
103
+ that You distribute, all copyright, patent, trademark, and
104
+ attribution notices from the Source form of the Work,
105
+ excluding those notices that do not pertain to any part of
106
+ the Derivative Works; and
107
+
108
+ (d) If the Work includes a "NOTICE" text file as part of its
109
+ distribution, then any Derivative Works that You distribute must
110
+ include a readable copy of the attribution notices contained
111
+ within such NOTICE file, excluding those notices that do not
112
+ pertain to any part of the Derivative Works, in at least one
113
+ of the following places: within a NOTICE text file distributed
114
+ as part of the Derivative Works; within the Source form or
115
+ documentation, if provided along with the Derivative Works; or,
116
+ within a display generated by the Derivative Works, if and
117
+ wherever such third-party notices normally appear. The contents
118
+ of the NOTICE file are for informational purposes only and
119
+ do not modify the License. You may add Your own attribution
120
+ notices within Derivative Works that You distribute, alongside
121
+ or as an addendum to the NOTICE text from the Work, provided
122
+ that such additional attribution notices cannot be construed
123
+ as modifying the License.
124
+
125
+ You may add Your own copyright statement to Your modifications and
126
+ may provide additional or different license terms and conditions
127
+ for use, reproduction, or distribution of Your modifications, or
128
+ for any such Derivative Works as a whole, provided Your use,
129
+ reproduction, and distribution of the Work otherwise complies with
130
+ the conditions stated in this License.
131
+
132
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
133
+ any Contribution intentionally submitted for inclusion in the Work
134
+ by You to the Licensor shall be under the terms and conditions of
135
+ this License, without any additional terms or conditions.
136
+ Notwithstanding the above, nothing herein shall supersede or modify
137
+ the terms of any separate license agreement you may have executed
138
+ with Licensor regarding such Contributions.
139
+
140
+ 6. Trademarks. This License does not grant permission to use the trade
141
+ names, trademarks, service marks, or product names of the Licensor,
142
+ except as required for reasonable and customary use in describing the
143
+ origin of the Work and reproducing the content of the NOTICE file.
144
+
145
+ 7. Disclaimer of Warranty. Unless required by applicable law or
146
+ agreed to in writing, Licensor provides the Work (and each
147
+ Contributor provides its Contributions) on an "AS IS" BASIS,
148
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
149
+ implied, including, without limitation, any warranties or conditions
150
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
151
+ PARTICULAR PURPOSE. You are solely responsible for determining the
152
+ appropriateness of using or redistributing the Work and assume any
153
+ risks associated with Your exercise of permissions under this License.
154
+
155
+ 8. Limitation of Liability. In no event and under no legal theory,
156
+ whether in tort (including negligence), contract, or otherwise,
157
+ unless required by applicable law (such as deliberate and grossly
158
+ negligent acts) or agreed to in writing, shall any Contributor be
159
+ liable to You for damages, including any direct, indirect, special,
160
+ incidental, or consequential damages of any character arising as a
161
+ result of this License or out of the use or inability to use the
162
+ Work (including but not limited to damages for loss of goodwill,
163
+ work stoppage, computer failure or malfunction, or any and all
164
+ other commercial damages or losses), even if such Contributor
165
+ has been advised of the possibility of such damages.
166
+
167
+ 9. Accepting Warranty or Additional Liability. While redistributing
168
+ the Work or Derivative Works thereof, You may choose to offer,
169
+ and charge a fee for, acceptance of support, warranty, indemnity,
170
+ or other liability obligations and/or rights consistent with this
171
+ License. However, in accepting such obligations, You may act only
172
+ on Your own behalf and on Your sole responsibility, not on behalf
173
+ of any other Contributor, and only if You agree to indemnify,
174
+ defend, and hold each Contributor harmless for any liability
175
+ incurred by, or claims asserted against, such Contributor by reason
176
+ of your accepting any such warranty or additional liability.
177
+
178
+ END OF TERMS AND CONDITIONS
179
+
180
+ APPENDIX: How to apply the Apache License to your work.
181
+
182
+ To apply the Apache License to your work, attach the following
183
+ boilerplate notice, with the fields enclosed by brackets "[]"
184
+ replaced with your own identifying information. (Don't include
185
+ the brackets!) The text should be enclosed in the appropriate
186
+ comment syntax for the file format. We also recommend that a
187
+ file or class name and description of purpose be included on the
188
+ same "printed page" as the copyright notice for easier
189
+ identification within third-party archives.
190
+
191
+ Copyright [yyyy] [name of copyright owner]
192
+
193
+ Licensed under the Apache License, Version 2.0 (the "License");
194
+ you may not use this file except in compliance with the License.
195
+ You may obtain a copy of the License at
196
+
197
+ http://www.apache.org/licenses/LICENSE-2.0
198
+
199
+ Unless required by applicable law or agreed to in writing, software
200
+ distributed under the License is distributed on an "AS IS" BASIS,
201
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
202
+ See the License for the specific language governing permissions and
203
+ limitations under the License.
models/semeval-propaganda/README.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SemEval-derived propaganda classifier
2
+
3
+ This subfolder preserves the already published three-seed task-specific model.
4
+ It is a separate lineage and is **not** a fine-tuned child of the headless base
5
+ at this repository root.
6
+
7
+ - Original repository: https://huggingface.co/mp-juuuns/qwen35-standalone4l-propaganda-classifier
8
+ - Preserved revision: `e7e0ff16828052687d2e8dd7849e7a521629cf38`
9
+ - Task: article-level multilabel presence of 14 propaganda techniques derived
10
+ from SemEval-2020 Task 11 annotations.
11
+ - Scope: not the official span- or fragment-level task; not a fact checker.
12
+ - Root of this subfolder: seed 41. Seeds 42 and 43 are under `seeds/`.
13
+
14
+ ```python
15
+ from transformers import AutoModelForSequenceClassification, AutoTokenizer
16
+
17
+ repo = "mp-juuuns/qwen35-standalone4l-classification-base"
18
+ subfolder = "models/semeval-propaganda"
19
+ tokenizer = AutoTokenizer.from_pretrained(repo, subfolder=subfolder)
20
+ model = AutoModelForSequenceClassification.from_pretrained(
21
+ repo, subfolder=subfolder
22
+ ).eval()
23
+ ```
24
+
25
+ The files below are byte-identical copies of the current release bundle. The
26
+ original release manifest and locked thresholds are included for provenance.
27
+ No SemEval source article or annotation file is redistributed.
models/semeval-propaganda/benchmark/final4l_aggregate.json ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema_version": "final-broad-arm-aggregate-v1",
3
+ "status": "completed_three_seed_audited",
4
+ "created_at": "2026-07-30T12:49:56+09:00",
5
+ "arm_id": "qwen35-standalone4l-commonhead",
6
+ "experiment_id": "S-20260729-final-broad-benchmark-v1",
7
+ "model": {
8
+ "description": "Qwen3.5 independently distilled standalone 4-layer L-F-L-F sequence classifier with shared 14-class head",
9
+ "source_checkpoint": "results/layer_shrink_v6/20260728-v6-r23-standalone4l-kd-seed41-v1/student-4l-final-bf16",
10
+ "source_audit_sha256": "6c53ed491d5c1d8d18610c7f68105691a12d2a999eafd0b471f2b373f92b1298",
11
+ "source_config_sha256": "f2e2b9547696963a08a1b9403b4ea7973b8c9626c6d0a119dfdee2713e3dfdd9",
12
+ "source_model_sha256": "8b3959f6326c75f557b781caa3930478d0b112faf7db8ff59ec5c6c7c444edcf",
13
+ "excluded_historical_joint_heads_sha256": "db886bfab8b827a17062c1fa4057641d2c77aa265e83f5fe07aa731f0de06fd1",
14
+ "tokenizer_sha256": "2106c29a0c7de1308e536b9f8a01833b9ff9bd0b34a526019f478bc53b6da81d",
15
+ "tokenizer_config_sha256": "2215318fe27878ec67e772931bb5f40a98658348d33f2672a575f766a2190d35",
16
+ "teacher_depth": 6,
17
+ "student_depth": 4,
18
+ "selected_teacher_layers": [
19
+ 0,
20
+ 2,
21
+ 3,
22
+ 5
23
+ ],
24
+ "layer_types": [
25
+ "linear_attention",
26
+ "full_attention",
27
+ "linear_attention",
28
+ "full_attention"
29
+ ],
30
+ "hidden_size": 1024,
31
+ "vocabulary_size": 128000,
32
+ "parameters": 210903360,
33
+ "head_policy": "Historical 3-class score weight replaced with a freshly initialized shared 14-class score weight; separate historical joint_heads.safetensors not loaded."
34
+ },
35
+ "dataset": {
36
+ "name": "SemEval-2020 Task 11 public professional human gold v2",
37
+ "train_sha256": "d5e3307374565a4bffba47c02da850c73f5616d3bace99b08f7b689bf8ce2bdf",
38
+ "calibration_sha256": "80911ff094cf300644e43a2baf57029cc0527d34af7881c787f0b898c2e83acd",
39
+ "test_sha256": "ed224269dc0a386dfefff2a8cb8cafb8b9bb0a2d8495c616f8365892b4709463",
40
+ "task_spec_sha256": "8ce5df61b17c1f7819b161f84f7c1e21415b0c6014abc9ce3c5c2fa4120715ff",
41
+ "test_articles": 55,
42
+ "test_previously_opened_public_gold": true
43
+ },
44
+ "protocol": {
45
+ "seeds": [
46
+ 41,
47
+ 42,
48
+ 43
49
+ ],
50
+ "epochs": 5,
51
+ "train_windows_per_epoch": 2428,
52
+ "early_stopping": false,
53
+ "max_length": 256,
54
+ "stride": 128,
55
+ "micro_batch_size": 1,
56
+ "eval_batch_size": 2,
57
+ "gradient_accumulation": 32,
58
+ "effective_batch_size": 32,
59
+ "learning_rate": 2e-05,
60
+ "weight_decay": 0.01,
61
+ "gradient_clip": 1.0,
62
+ "dtype": "bfloat16",
63
+ "test_access": "once_per_seed_after_epoch5_calibration_lock"
64
+ },
65
+ "seed_artifacts": [
66
+ {
67
+ "seed": 41,
68
+ "report": {
69
+ "path": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed41/report.json",
70
+ "sha256": "ec5314140fff0bdd46e87ee00ad67bee7e61d82c7cfa34add1510dd7d5bdb3c0"
71
+ },
72
+ "manifest": {
73
+ "path": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed41.run-manifest.json",
74
+ "sha256": "9c9de086a0679611f9ffa99a8e3b01b30889d80a859fad1b80fffb7fa3d08ea9"
75
+ },
76
+ "predictions": {
77
+ "path": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed41/test_predictions.jsonl",
78
+ "sha256": "6564a34da352ee3808b00c24e69010144fef5947af8b4913656a13547c693f1e"
79
+ },
80
+ "checkpoint": {
81
+ "path": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed41/checkpoint/model.safetensors",
82
+ "sha256": "0352da6e4717a2e6f3a9d643d034be2cb1c39632d1bce0ced339ddb62acedb37"
83
+ },
84
+ "reload_exact": true,
85
+ "max_probability_delta": 0.0
86
+ },
87
+ {
88
+ "seed": 42,
89
+ "report": {
90
+ "path": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed42/report.json",
91
+ "sha256": "e5c45a7dc41bebf4550da7c5763d0759c6d0922da9efda4048564c11c6d2f3af"
92
+ },
93
+ "manifest": {
94
+ "path": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed42.run-manifest.json",
95
+ "sha256": "6e61bb7ab1731c138301acf3d9877c8b18cc3750b856fecba4042099f14fa538"
96
+ },
97
+ "predictions": {
98
+ "path": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed42/test_predictions.jsonl",
99
+ "sha256": "2a1a22583b6a32860b744f7c8022379816417f09ebe4a71f34de3462e5c5e597"
100
+ },
101
+ "checkpoint": {
102
+ "path": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed42/checkpoint/model.safetensors",
103
+ "sha256": "2db5f4be391d9dee64458cb5865530f39747019e7a219443b5f7850befbce697"
104
+ },
105
+ "reload_exact": true,
106
+ "max_probability_delta": 0.0
107
+ },
108
+ {
109
+ "seed": 43,
110
+ "report": {
111
+ "path": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed43/report.json",
112
+ "sha256": "e05bc2146d720706780fc45a1fb05911d5c2926caa0bcb12ffead946028eef0d"
113
+ },
114
+ "manifest": {
115
+ "path": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed43.run-manifest.json",
116
+ "sha256": "092244cf229081b20eee52cbb10889f98889f486366c440a409fe1845a888905"
117
+ },
118
+ "predictions": {
119
+ "path": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed43/test_predictions.jsonl",
120
+ "sha256": "d6bb001f9cff810810ba17dce1e93edfea7c155a7b18fbf2e31c05a5eb1f16d9"
121
+ },
122
+ "checkpoint": {
123
+ "path": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed43/checkpoint/model.safetensors",
124
+ "sha256": "81a56d497df758e8a292eea528438cfc4d5a847cd37d3a7e1107fefa7c500640"
125
+ },
126
+ "reload_exact": true,
127
+ "max_probability_delta": 0.0
128
+ }
129
+ ],
130
+ "quality": {
131
+ "test_macro_f1": {
132
+ "values": [
133
+ 0.5832848452288824,
134
+ 0.5958405181160762,
135
+ 0.5835785045218137
136
+ ],
137
+ "mean": 0.5875679559555907,
138
+ "sample_sd": 0.007165753447433348
139
+ },
140
+ "test_micro_f1": {
141
+ "values": [
142
+ 0.6494252873563218,
143
+ 0.6458616010854816,
144
+ 0.6486486486486486
145
+ ],
146
+ "mean": 0.647978512363484,
147
+ "sample_sd": 0.0018739735696546695
148
+ },
149
+ "final_calibration_macro_f1": {
150
+ "values": [
151
+ 0.5555148573005716,
152
+ 0.5351509002776785,
153
+ 0.5298118328152787
154
+ ],
155
+ "mean": 0.540159196797843,
156
+ "sample_sd": 0.01356368837920977
157
+ },
158
+ "locked_thresholds": [
159
+ 0.1,
160
+ 0.05,
161
+ 0.05
162
+ ],
163
+ "stability_warning": "All seeds showed late-epoch calibration decline and large threshold movement; two of three final thresholds hit the lower edge of the preregistered grid even though test F1 remained high.",
164
+ "small_test_warning": "The shared test split contains only 55 previously opened public-gold articles; high F1 and paired-seed consistency require confirmation on distinct public human-gold datasets."
165
+ },
166
+ "resources": {
167
+ "wall_seconds": {
168
+ "values": [
169
+ 1634.488557530014,
170
+ 1652.9397718419787,
171
+ 1643.488202479988
172
+ ],
173
+ "mean": 1643.6388439506602,
174
+ "sample_sd": 9.226529522850198
175
+ },
176
+ "peak_cuda_allocated_bytes": {
177
+ "values": [
178
+ 2143655936,
179
+ 2143655936,
180
+ 2143655936
181
+ ],
182
+ "mean": 2143655936.0,
183
+ "sample_sd": 0.0
184
+ },
185
+ "peak_cuda_reserved_bytes": {
186
+ "values": [
187
+ 2281701376,
188
+ 2281701376,
189
+ 2273312768
190
+ ],
191
+ "mean": 2278905173.3333335,
192
+ "sample_sd": 4843165.086926248
193
+ },
194
+ "ambient_gpu_context": {
195
+ "nvml_pid_at_preflight": 197568,
196
+ "visible_in_wsl_process_namespace": false,
197
+ "baseline_used_mib_range": [
198
+ 3707,
199
+ 3729
200
+ ],
201
+ "baseline_free_mib_range": [
202
+ 12267,
203
+ 12289
204
+ ],
205
+ "baseline_utilization_percent": 1,
206
+ "stopped_for_benchmark": false,
207
+ "runner_peak_metrics_process_local": true
208
+ }
209
+ },
210
+ "raw_comparisons": {
211
+ "qwen35_full24l": {
212
+ "other_macro_f1_mean": 0.5642638295960175,
213
+ "macro_mean_delta_this_minus_other": 0.02330412635957324,
214
+ "paired_seed_macro_deltas": [
215
+ 0.011832078826701897,
216
+ 0.03452762812104826,
217
+ 0.023552672130969676
218
+ ],
219
+ "wall_reduction_fraction": 0.8720113142644486,
220
+ "peak_cuda_allocated_reduction_fraction": 0.7192454897050662,
221
+ "parameter_reduction_fraction": 0.7196952459369882
222
+ },
223
+ "qwen35_c8q8l": {
224
+ "other_macro_f1_mean": 0.5366873350531403,
225
+ "macro_mean_delta_this_minus_other": 0.05088062090245049,
226
+ "paired_seed_macro_deltas": [
227
+ 0.06653285611641446,
228
+ 0.04916191142573212,
229
+ 0.03694709516520478
230
+ ],
231
+ "wall_reduction_fraction": 0.6170087121951836,
232
+ "peak_cuda_allocated_reduction_fraction": 0.4965455376253577,
233
+ "parameter_reduction_fraction": 0.4982463406111276
234
+ },
235
+ "xlmr_base": {
236
+ "other_macro_f1_mean": 0.5483113137371987,
237
+ "macro_mean_delta_this_minus_other": 0.03925664221839209,
238
+ "paired_seed_macro_deltas": [
239
+ 0.04717846633760603,
240
+ 0.050893521484219884,
241
+ 0.01969793883335047
242
+ ]
243
+ },
244
+ "qwen25_fixed8l": {
245
+ "other_macro_f1_mean": 0.513131621013363,
246
+ "macro_mean_delta_this_minus_other": 0.07443633494222779,
247
+ "paired_seed_macro_deltas": [
248
+ 0.056130719634202686,
249
+ 0.09312943034491972,
250
+ 0.07404885484756096
251
+ ]
252
+ },
253
+ "mbert": {
254
+ "other_macro_f1_mean": 0.5319538028157794,
255
+ "macro_mean_delta_this_minus_other": 0.05561415313981133,
256
+ "paired_seed_macro_deltas": [
257
+ 0.03516462711549406,
258
+ 0.07017577990032031,
259
+ 0.06150205240361961
260
+ ]
261
+ },
262
+ "minilm_multilingual": {
263
+ "other_macro_f1_mean": 0.5078871917240945,
264
+ "macro_mean_delta_this_minus_other": 0.07968076423149628,
265
+ "paired_seed_macro_deltas": [
266
+ 0.09316945437553353,
267
+ 0.08396188337177446,
268
+ 0.061910954947180796
269
+ ]
270
+ },
271
+ "claim_boundary": "Raw descriptive shared-protocol comparisons only. Three paired seeds and one 55-article opened public-gold test split do not establish population-level superiority, deployment superiority, or an overall systems winner."
272
+ },
273
+ "environment": {
274
+ "optional_fast_linear_attention_installed": false,
275
+ "optional_causal_convolution_installed": false,
276
+ "execution_path": "Transformers PyTorch fallback",
277
+ "tensorboard_added_mid_matrix": false
278
+ },
279
+ "exclusions": {
280
+ "historical_opened_calibration_diagnostic_used_as_result": false,
281
+ "historical_joint_heads_loaded": false,
282
+ "semeval2023_task3_private_or_final_data_used": false,
283
+ "mac_used": false,
284
+ "new_human_gold_created": false,
285
+ "uno_service_disrupted": false
286
+ },
287
+ "independent_audit": "passed",
288
+ "derived_research_indexes_rebuilt": false
289
+ }
models/semeval-propaganda/benchmark/seed41.json ADDED
The diff for this file is too large to render. See raw diff
 
models/semeval-propaganda/benchmark/seed42.json ADDED
The diff for this file is too large to render. See raw diff
 
models/semeval-propaganda/benchmark/seed43.json ADDED
The diff for this file is too large to render. See raw diff
 
models/semeval-propaganda/chat_template.jinja ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- set image_count = namespace(value=0) %}
2
+ {%- set video_count = namespace(value=0) %}
3
+ {%- macro render_content(content, do_vision_count) %}
4
+ {%- if content is string %}
5
+ {{- content }}
6
+ {%- else %}
7
+ {%- for item in content %}
8
+ {%- if 'image' in item or 'image_url' in item or item.type == 'image' %}
9
+ {%- if do_vision_count %}
10
+ {%- set image_count.value = image_count.value + 1 %}
11
+ {%- endif %}
12
+ {%- if add_vision_id %}Picture {{ image_count.value }}: {% endif -%}
13
+ <|vision_start|><|image_pad|><|vision_end|>
14
+ {%- elif 'video' in item or item.type == 'video' %}
15
+ {%- if do_vision_count %}
16
+ {%- set video_count.value = video_count.value + 1 %}
17
+ {%- endif %}
18
+ {%- if add_vision_id %}Video {{ video_count.value }}: {% endif -%}
19
+ <|vision_start|><|video_pad|><|vision_end|>
20
+ {%- elif 'text' in item %}
21
+ {{- item.text }}
22
+ {%- endif %}
23
+ {%- endfor %}
24
+ {%- endif %}
25
+ {%- endmacro %}
26
+ {%- if tools is defined and tools is iterable and tools | length > 0 %}
27
+ {{- '<|im_start|>system\n' }}
28
+ {{- "# Tools\n\nYou have access to the following functions:\n\n<tools>" }}
29
+ {%- for tool in tools %}
30
+ {{- "\n" }}
31
+ {{- tool | tojson }}
32
+ {%- endfor %}
33
+ {{- "\n</tools>" }}
34
+ {{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
35
+ {%- if messages[0].role == 'system' and messages[0].content | trim %}
36
+ {%- set content = render_content(messages[0].content, false) %}
37
+ {{- '\n\n' + content.lstrip('\n') }}
38
+ {%- endif %}
39
+ {{- '<|im_end|>\n' }}
40
+ {%- else %}
41
+ {%- if messages[0].role == 'system' %}
42
+ {{- '<|im_start|>system\n' + render_content(messages[0].content, false) + '<|im_end|>\n' }}
43
+ {%- endif %}
44
+ {%- endif %}
45
+ {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
46
+ {%- for message in messages[::-1] %}
47
+ {%- set index = (messages|length - 1) - loop.index0 %}
48
+ {%- if ns.multi_step_tool and message.role == "user" %}
49
+ {%- set content = render_content(message.content, False) %}
50
+ {%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
51
+ {%- set ns.multi_step_tool = false %}
52
+ {%- set ns.last_query_index = index %}
53
+ {%- endif %}
54
+ {%- endif %}
55
+ {%- endfor %}
56
+ {%- for message in messages %}
57
+ {%- set content = render_content(message.content, true) %}
58
+ {%- if (message.role == "user") or (message.role == "system" and not loop.first) %}
59
+ {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
60
+ {%- elif message.role == "assistant" %}
61
+ {%- set reasoning_content = '' %}
62
+ {%- if message.reasoning_content is defined and message.reasoning_content is not none %}
63
+ {%- set reasoning_content = message.reasoning_content %}
64
+ {%- else %}
65
+ {%- if '</think>' in content %}
66
+ {%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
67
+ {%- set content = content.split('</think>')[-1].lstrip('\n') %}
68
+ {%- endif %}
69
+ {%- endif %}
70
+ {%- if message.tool_calls is defined and message.tool_calls is not none and message.tool_calls is iterable and message.tool_calls | length > 0 %}
71
+ {%- set content = content.rstrip('\n') %}
72
+ {%- endif %}
73
+ {%- if loop.index0 > ns.last_query_index %}
74
+ {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content.strip('\n') + '\n</think>\n\n' + content.lstrip('\n') }}
75
+ {%- else %}
76
+ {{- '<|im_start|>' + message.role + '\n' + content }}
77
+ {%- endif %}
78
+ {%- if message.tool_calls is defined and message.tool_calls is not none and message.tool_calls is iterable and message.tool_calls | length > 0 %}
79
+ {%- for tool_call in message.tool_calls %}
80
+ {%- if tool_call.function is defined %}
81
+ {%- set tool_call = tool_call.function %}
82
+ {%- endif %}
83
+ {%- if loop.first %}
84
+ {%- if content|trim %}
85
+ {{- '\n\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
86
+ {%- else %}
87
+ {{- '<tool_call>\n<function=' + tool_call.name + '>\n' }}
88
+ {%- endif %}
89
+ {%- else %}
90
+ {{- '\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
91
+ {%- endif %}
92
+ {%- if tool_call.arguments is defined %}
93
+ {%- for args_name, args_value in tool_call.arguments|items %}
94
+ {{- '<parameter=' + args_name + '>\n' }}
95
+ {%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
96
+ {{- args_value }}
97
+ {{- '\n</parameter>\n' }}
98
+ {%- endfor %}
99
+ {%- endif %}
100
+ {{- '</function>\n</tool_call>' }}
101
+ {%- endfor %}
102
+ {%- endif %}
103
+ {{- '<|im_end|>\n' }}
104
+ {%- elif message.role == "tool" %}
105
+ {%- if loop.previtem and loop.previtem.role != "tool" %}
106
+ {{- '<|im_start|>user' }}
107
+ {%- endif %}
108
+ {{- '\n<tool_response>\n' }}
109
+ {{- content }}
110
+ {{- '\n</tool_response>' }}
111
+ {%- if not loop.last and loop.nextitem.role != "tool" %}
112
+ {{- '<|im_end|>\n' }}
113
+ {%- elif loop.last %}
114
+ {{- '<|im_end|>\n' }}
115
+ {%- endif %}
116
+ {%- endif %}
117
+ {%- endfor %}
118
+ {%- if add_generation_prompt %}
119
+ {{- '<|im_start|>assistant\n<think>\n' }}
120
+ {%- endif %}
models/semeval-propaganda/classify.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Run the public Qwen3.5 standalone-4L multilabel classifier.
3
+
4
+ The script is intentionally self-contained so the same file can be uploaded as
5
+ ``classify.py`` to Hugging Face. Long articles are split into the frozen
6
+ 256-token / 128-token-overlap windows and combined with a label-wise maximum.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import math
14
+ from pathlib import Path
15
+ from typing import Any, Sequence
16
+
17
+ import torch
18
+ from transformers import AutoModelForSequenceClassification, AutoTokenizer
19
+
20
+
21
+ DEFAULT_MODEL = "mp-juuuns/qwen35-standalone4l-propaganda-classifier"
22
+ LOCKED_THRESHOLDS = {41: 0.10, 42: 0.05, 43: 0.05}
23
+ MAX_LENGTH = 256
24
+ STRIDE = 128
25
+
26
+
27
+ def seed_subfolder(model_id: str, seed: int) -> str | None:
28
+ """Resolve the public repo layout while allowing direct local checkpoints."""
29
+
30
+ if seed == 41:
31
+ return None
32
+ local = Path(model_id)
33
+ if local.exists():
34
+ candidate = local / "seeds" / f"seed{seed}"
35
+ return f"seeds/seed{seed}" if candidate.is_dir() else None
36
+ return f"seeds/seed{seed}"
37
+
38
+
39
+ def resolve_device(requested: str) -> torch.device:
40
+ if requested == "auto":
41
+ return torch.device("cuda" if torch.cuda.is_available() else "cpu")
42
+ device = torch.device(requested)
43
+ if device.type == "cuda" and not torch.cuda.is_available():
44
+ raise RuntimeError("CUDA was requested but is not available")
45
+ return device
46
+
47
+
48
+ def load_runtime(
49
+ model_id: str,
50
+ *,
51
+ seed: int,
52
+ device: torch.device,
53
+ revision: str | None = None,
54
+ local_files_only: bool = False,
55
+ ) -> tuple[Any, Any, list[str]]:
56
+ subfolder = seed_subfolder(model_id, seed)
57
+ shared: dict[str, Any] = {
58
+ "revision": revision,
59
+ "local_files_only": local_files_only,
60
+ "trust_remote_code": False,
61
+ }
62
+ if subfolder:
63
+ shared["subfolder"] = subfolder
64
+ tokenizer = AutoTokenizer.from_pretrained(model_id, **shared)
65
+ if tokenizer.pad_token_id is None:
66
+ if tokenizer.eos_token_id is None:
67
+ raise RuntimeError("tokenizer has neither pad_token_id nor eos_token_id")
68
+ tokenizer.pad_token = tokenizer.eos_token
69
+ dtype = torch.bfloat16 if device.type == "cuda" and torch.cuda.is_bf16_supported() else (
70
+ torch.float16 if device.type == "cuda" else torch.float32
71
+ )
72
+ model = AutoModelForSequenceClassification.from_pretrained(
73
+ model_id,
74
+ dtype=dtype,
75
+ **shared,
76
+ ).to(device)
77
+ model.eval()
78
+ if hasattr(model.config, "use_cache"):
79
+ model.config.use_cache = False
80
+ id2label = getattr(model.config, "id2label", {})
81
+ labels = [str(id2label.get(index, id2label.get(str(index), f"LABEL_{index}"))) for index in range(14)]
82
+ if len(labels) != 14 or any(label.startswith("LABEL_") for label in labels):
83
+ raise RuntimeError("checkpoint does not expose the fixed 14-label order")
84
+ return tokenizer, model, labels
85
+
86
+
87
+ def classify_texts(
88
+ texts: Sequence[str],
89
+ *,
90
+ tokenizer: Any,
91
+ model: Any,
92
+ labels: Sequence[str],
93
+ threshold: float,
94
+ device: torch.device,
95
+ batch_size: int = 8,
96
+ ) -> list[dict[str, Any]]:
97
+ if not texts or any(not isinstance(text, str) or not text.strip() for text in texts):
98
+ raise ValueError("at least one non-empty text is required")
99
+ if len(labels) != 14:
100
+ raise ValueError("expected the fixed 14-label order")
101
+ if not 0 < threshold < 1:
102
+ raise ValueError("threshold must be in (0, 1)")
103
+ encoded = tokenizer(
104
+ list(texts),
105
+ truncation=True,
106
+ max_length=MAX_LENGTH,
107
+ stride=STRIDE,
108
+ return_overflowing_tokens=True,
109
+ padding=True,
110
+ return_tensors="pt",
111
+ )
112
+ mapping = encoded.pop("overflow_to_sample_mapping")
113
+ model_inputs = {
114
+ key: value
115
+ for key, value in encoded.items()
116
+ if key in {"input_ids", "attention_mask", "token_type_ids"}
117
+ }
118
+ window_probabilities: list[torch.Tensor] = []
119
+ with torch.inference_mode():
120
+ for start in range(0, int(mapping.numel()), batch_size):
121
+ batch = {
122
+ key: value[start : start + batch_size].to(device)
123
+ for key, value in model_inputs.items()
124
+ }
125
+ logits = model(**batch).logits
126
+ if logits.ndim != 2 or logits.shape[1] != 14:
127
+ raise RuntimeError(f"unexpected classifier output shape: {tuple(logits.shape)}")
128
+ window_probabilities.append(torch.sigmoid(logits.float()).cpu())
129
+ probabilities = torch.cat(window_probabilities, dim=0)
130
+ if probabilities.shape != (mapping.numel(), 14) or not torch.isfinite(probabilities).all():
131
+ raise RuntimeError("non-finite or malformed probability matrix")
132
+ outputs: list[dict[str, Any]] = []
133
+ mapping_list = mapping.tolist()
134
+ for article_index, _ in enumerate(texts):
135
+ indices = [index for index, mapped in enumerate(mapping_list) if mapped == article_index]
136
+ if not indices:
137
+ raise RuntimeError(f"tokenizer produced no window for input {article_index}")
138
+ article_scores = probabilities[indices].amax(dim=0).tolist()
139
+ if not all(math.isfinite(value) for value in article_scores):
140
+ raise RuntimeError("non-finite article score")
141
+ predicted_ids = [index for index, score in enumerate(article_scores) if score >= threshold]
142
+ outputs.append(
143
+ {
144
+ "input_index": article_index,
145
+ "windows": len(indices),
146
+ "predicted_ids": predicted_ids,
147
+ "predicted_labels": [labels[index] for index in predicted_ids],
148
+ "threshold": threshold,
149
+ "scores": [
150
+ {"id": index, "label": label, "probability": article_scores[index]}
151
+ for index, label in enumerate(labels)
152
+ ],
153
+ }
154
+ )
155
+ return outputs
156
+
157
+
158
+ def parse_args() -> argparse.Namespace:
159
+ parser = argparse.ArgumentParser(description=__doc__)
160
+ parser.add_argument("--model", default=DEFAULT_MODEL)
161
+ parser.add_argument("--revision")
162
+ parser.add_argument("--seed", type=int, choices=sorted(LOCKED_THRESHOLDS), default=41)
163
+ parser.add_argument("--text", action="append", default=[])
164
+ parser.add_argument("--text-file", type=Path, action="append", default=[])
165
+ parser.add_argument("--threshold", type=float)
166
+ parser.add_argument("--device", choices=("auto", "cpu", "cuda"), default="auto")
167
+ parser.add_argument("--batch-size", type=int, default=8)
168
+ parser.add_argument("--local-files-only", action="store_true")
169
+ return parser.parse_args()
170
+
171
+
172
+ def main() -> int:
173
+ args = parse_args()
174
+ texts = list(args.text)
175
+ texts.extend(path.read_text(encoding="utf-8") for path in args.text_file)
176
+ if not texts:
177
+ raise SystemExit("provide --text or --text-file")
178
+ if args.batch_size < 1:
179
+ raise SystemExit("--batch-size must be positive")
180
+ threshold = LOCKED_THRESHOLDS[args.seed] if args.threshold is None else args.threshold
181
+ device = resolve_device(args.device)
182
+ tokenizer, model, labels = load_runtime(
183
+ args.model,
184
+ seed=args.seed,
185
+ device=device,
186
+ revision=args.revision,
187
+ local_files_only=args.local_files_only,
188
+ )
189
+ outputs = classify_texts(
190
+ texts,
191
+ tokenizer=tokenizer,
192
+ model=model,
193
+ labels=labels,
194
+ threshold=threshold,
195
+ device=device,
196
+ batch_size=args.batch_size,
197
+ )
198
+ payload = {
199
+ "model": args.model,
200
+ "seed": args.seed,
201
+ "threshold": threshold,
202
+ "device": str(device),
203
+ "contract": {
204
+ "task": "14-label multilabel propaganda-technique classification",
205
+ "max_length": MAX_LENGTH,
206
+ "stride": STRIDE,
207
+ "article_aggregation": "labelwise maximum probability across windows",
208
+ "trust_remote_code": False,
209
+ },
210
+ "outputs": outputs,
211
+ }
212
+ print(json.dumps(payload, ensure_ascii=False, indent=2, allow_nan=False))
213
+ return 0
214
+
215
+
216
+ if __name__ == "__main__":
217
+ raise SystemExit(main())
models/semeval-propaganda/config.json ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "Qwen3_5TextForSequenceClassification"
4
+ ],
5
+ "attention_bias": false,
6
+ "attention_dropout": 0.0,
7
+ "attn_output_gate": true,
8
+ "bos_token_id": null,
9
+ "dtype": "bfloat16",
10
+ "eos_token_id": 127967,
11
+ "full_attention_interval": 4,
12
+ "head_dim": 256,
13
+ "hidden_act": "silu",
14
+ "hidden_size": 1024,
15
+ "id2label": {
16
+ "0": "Appeal_to_Authority",
17
+ "1": "Appeal_to_fear-prejudice",
18
+ "2": "Bandwagon,Reductio_ad_hitlerum",
19
+ "3": "Black-and-White_Fallacy",
20
+ "4": "Causal_Oversimplification",
21
+ "5": "Doubt",
22
+ "6": "Exaggeration,Minimisation",
23
+ "7": "Flag-Waving",
24
+ "8": "Loaded_Language",
25
+ "9": "Name_Calling,Labeling",
26
+ "10": "Repetition",
27
+ "11": "Slogans",
28
+ "12": "Thought-terminating_Cliches",
29
+ "13": "Whataboutism,Straw_Men,Red_Herring"
30
+ },
31
+ "initializer_range": 0.02,
32
+ "intermediate_size": 3584,
33
+ "label2id": {
34
+ "Appeal_to_Authority": 0,
35
+ "Appeal_to_fear-prejudice": 1,
36
+ "Bandwagon,Reductio_ad_hitlerum": 2,
37
+ "Black-and-White_Fallacy": 3,
38
+ "Causal_Oversimplification": 4,
39
+ "Doubt": 5,
40
+ "Exaggeration,Minimisation": 6,
41
+ "Flag-Waving": 7,
42
+ "Loaded_Language": 8,
43
+ "Name_Calling,Labeling": 9,
44
+ "Repetition": 10,
45
+ "Slogans": 11,
46
+ "Thought-terminating_Cliches": 12,
47
+ "Whataboutism,Straw_Men,Red_Herring": 13
48
+ },
49
+ "layer_types": [
50
+ "linear_attention",
51
+ "full_attention",
52
+ "linear_attention",
53
+ "full_attention"
54
+ ],
55
+ "linear_conv_kernel_dim": 4,
56
+ "linear_key_head_dim": 128,
57
+ "linear_num_key_heads": 16,
58
+ "linear_num_value_heads": 16,
59
+ "linear_value_head_dim": 128,
60
+ "mamba_ssm_dtype": "float32",
61
+ "max_position_embeddings": 262144,
62
+ "mlp_only_layers": [],
63
+ "model_type": "qwen3_5_text",
64
+ "mtp_num_hidden_layers": 1,
65
+ "mtp_use_dedicated_embeddings": false,
66
+ "num_attention_heads": 8,
67
+ "num_hidden_layers": 4,
68
+ "num_key_value_heads": 2,
69
+ "pad_token_id": 127967,
70
+ "partial_rotary_factor": 0.25,
71
+ "phase_v6_map_selection_rank": 1,
72
+ "phase_v6_map_selection_split": "public_gold_train",
73
+ "phase_v6_materialized_role": "student6l-r3-final-teacher",
74
+ "phase_v6_materialized_trainable_state": "results/layer_shrink_v6/20260726-v6-r3-student6l-map013457-kd-seed41-pilot-v2/student-6l-kd-trainable.safetensors",
75
+ "phase_v6_selected_original_8l_layers": [
76
+ 0,
77
+ 3,
78
+ 4,
79
+ 7
80
+ ],
81
+ "phase_v6_selected_source_layers": [
82
+ 0,
83
+ 1,
84
+ 3,
85
+ 4,
86
+ 5,
87
+ 7
88
+ ],
89
+ "phase_v6_selected_source_layers_6l": [
90
+ 0,
91
+ 2,
92
+ 3,
93
+ 5
94
+ ],
95
+ "phase_v6_source_teacher": "student6l-r3-final",
96
+ "phase_v6_standalone4l_kd": {
97
+ "canonical_materialization_dtype": "bfloat16",
98
+ "canonical_training_state_preservation": true,
99
+ "epochs": 3,
100
+ "legacy_float16_checkpoint": "results/layer_shrink_v6/20260728-v6-r23-standalone4l-kd-seed41-v1/student-4l-final",
101
+ "opened_calibration_used_for_model_selection": false,
102
+ "seed": 41,
103
+ "teacher": "results/layer_shrink_v6/20260728-v6-r22-standalone4l-map-probe-seed41-v1/teacher-6l-r3-final"
104
+ },
105
+ "problem_type": "multi_label_classification",
106
+ "rms_norm_eps": 1e-06,
107
+ "rope_parameters": {
108
+ "mrope_interleaved": true,
109
+ "mrope_section": [
110
+ 11,
111
+ 11,
112
+ 10
113
+ ],
114
+ "partial_rotary_factor": 0.25,
115
+ "rope_theta": 10000000,
116
+ "rope_type": "default"
117
+ },
118
+ "tie_word_embeddings": true,
119
+ "transformers_version": "5.13.0",
120
+ "use_cache": false,
121
+ "vocab_size": 128000
122
+ }
models/semeval-propaganda/labels.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "labels": [
3
+ "Appeal_to_Authority",
4
+ "Appeal_to_fear-prejudice",
5
+ "Bandwagon,Reductio_ad_hitlerum",
6
+ "Black-and-White_Fallacy",
7
+ "Causal_Oversimplification",
8
+ "Doubt",
9
+ "Exaggeration,Minimisation",
10
+ "Flag-Waving",
11
+ "Loaded_Language",
12
+ "Name_Calling,Labeling",
13
+ "Repetition",
14
+ "Slogans",
15
+ "Thought-terminating_Cliches",
16
+ "Whataboutism,Straw_Men,Red_Herring"
17
+ ],
18
+ "id2label": {
19
+ "0": "Appeal_to_Authority",
20
+ "1": "Appeal_to_fear-prejudice",
21
+ "2": "Bandwagon,Reductio_ad_hitlerum",
22
+ "3": "Black-and-White_Fallacy",
23
+ "4": "Causal_Oversimplification",
24
+ "5": "Doubt",
25
+ "6": "Exaggeration,Minimisation",
26
+ "7": "Flag-Waving",
27
+ "8": "Loaded_Language",
28
+ "9": "Name_Calling,Labeling",
29
+ "10": "Repetition",
30
+ "11": "Slogans",
31
+ "12": "Thought-terminating_Cliches",
32
+ "13": "Whataboutism,Straw_Men,Red_Herring"
33
+ }
34
+ }
models/semeval-propaganda/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0352da6e4717a2e6f3a9d643d034be2cb1c39632d1bce0ced339ddb62acedb37
3
+ size 421812688
models/semeval-propaganda/release_manifest.json ADDED
@@ -0,0 +1,637 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema": "qwen35-standalone4l-huggingface-release-manifest-v2",
3
+ "status": "complete",
4
+ "repo_id": "mp-juuuns/qwen35-standalone4l-propaganda-classifier",
5
+ "root_checkpoint_seed": 41,
6
+ "seed_metadata": [
7
+ {
8
+ "seed": 41,
9
+ "threshold": 0.1,
10
+ "macro_f1": 0.5832848452288824,
11
+ "micro_f1": 0.6494252873563218,
12
+ "report_sha256": "ec5314140fff0bdd46e87ee00ad67bee7e61d82c7cfa34add1510dd7d5bdb3c0",
13
+ "checkpoint_hashes": {
14
+ "model.safetensors": "0352da6e4717a2e6f3a9d643d034be2cb1c39632d1bce0ced339ddb62acedb37",
15
+ "config.json": "9f04dfbd7e1696cd3df2ccbe194f5f155efe5baefd1a0b615851f51bf46691a0",
16
+ "tokenizer.json": "dd2b27bbd4f5f650c7200300013ef03ee01efe620b128343163521bf04806fc8",
17
+ "tokenizer_config.json": "2215318fe27878ec67e772931bb5f40a98658348d33f2672a575f766a2190d35",
18
+ "chat_template.jinja": "04b007131663760bf3e581e5a953be77044014e87efe1d2a6ca4b72ec0eac978"
19
+ }
20
+ },
21
+ {
22
+ "seed": 42,
23
+ "threshold": 0.05,
24
+ "macro_f1": 0.5958405181160762,
25
+ "micro_f1": 0.6458616010854816,
26
+ "report_sha256": "e5c45a7dc41bebf4550da7c5763d0759c6d0922da9efda4048564c11c6d2f3af",
27
+ "checkpoint_hashes": {
28
+ "model.safetensors": "2db5f4be391d9dee64458cb5865530f39747019e7a219443b5f7850befbce697",
29
+ "config.json": "9f04dfbd7e1696cd3df2ccbe194f5f155efe5baefd1a0b615851f51bf46691a0",
30
+ "tokenizer.json": "dd2b27bbd4f5f650c7200300013ef03ee01efe620b128343163521bf04806fc8",
31
+ "tokenizer_config.json": "2215318fe27878ec67e772931bb5f40a98658348d33f2672a575f766a2190d35",
32
+ "chat_template.jinja": "04b007131663760bf3e581e5a953be77044014e87efe1d2a6ca4b72ec0eac978"
33
+ }
34
+ },
35
+ {
36
+ "seed": 43,
37
+ "threshold": 0.05,
38
+ "macro_f1": 0.5835785045218137,
39
+ "micro_f1": 0.6486486486486486,
40
+ "report_sha256": "e05bc2146d720706780fc45a1fb05911d5c2926caa0bcb12ffead946028eef0d",
41
+ "checkpoint_hashes": {
42
+ "model.safetensors": "81a56d497df758e8a292eea528438cfc4d5a847cd37d3a7e1107fefa7c500640",
43
+ "config.json": "9f04dfbd7e1696cd3df2ccbe194f5f155efe5baefd1a0b615851f51bf46691a0",
44
+ "tokenizer.json": "dd2b27bbd4f5f650c7200300013ef03ee01efe620b128343163521bf04806fc8",
45
+ "tokenizer_config.json": "2215318fe27878ec67e772931bb5f40a98658348d33f2672a575f766a2190d35",
46
+ "chat_template.jinja": "04b007131663760bf3e581e5a953be77044014e87efe1d2a6ca4b72ec0eac978"
47
+ }
48
+ }
49
+ ],
50
+ "files_before_manifest": [
51
+ {
52
+ "path": ".gitattributes",
53
+ "role": "Hugging Face large-file policy",
54
+ "bytes": 1243,
55
+ "sha256": "cb9f787a7d5053ba7b8a4c7d6938a85ad902321fafd923cfbbab160eaf6b33fd",
56
+ "source": "generated",
57
+ "source_sha256": null
58
+ },
59
+ {
60
+ "path": "LICENSE",
61
+ "role": "Apache-2.0 license text",
62
+ "bytes": 11418,
63
+ "sha256": "77fd4710def9ec3c0f6225800e0235f15a425abd4a8b03559127fcd782612049",
64
+ "source": "references/software/transformers-5.13.0/LICENSE",
65
+ "source_sha256": "77fd4710def9ec3c0f6225800e0235f15a425abd4a8b03559127fcd782612049"
66
+ },
67
+ {
68
+ "path": "README.md",
69
+ "role": "Hugging Face model card",
70
+ "bytes": 26722,
71
+ "sha256": "951d304189735d79403abde007b88992a0c7c6f60b8a01cb066596f703d6ed89",
72
+ "source": "generated",
73
+ "source_sha256": null
74
+ },
75
+ {
76
+ "path": "benchmark/MODEL_CARD_EVIDENCE.md",
77
+ "role": "claim-by-claim model-card evidence ledger",
78
+ "bytes": 14498,
79
+ "sha256": "80ae7e7fc6ef834188d2a02bcaa5dcc657d88b8b007b9904182fec8b5d555e61",
80
+ "source": "docs/huggingface/STANDALONE4L_MODEL_CARD_EVIDENCE.md",
81
+ "source_sha256": "80ae7e7fc6ef834188d2a02bcaa5dcc657d88b8b007b9904182fec8b5d555e61"
82
+ },
83
+ {
84
+ "path": "benchmark/SEALED_REPORT.md",
85
+ "role": "sealed benchmark narrative",
86
+ "bytes": 7049,
87
+ "sha256": "f74f85f43c3d18c836a74cb325d9cd1d6174a7932a2bf6f3664b5849eededc69",
88
+ "source": "results/final_broad_benchmark/20260729-v1/report/REPORT.md",
89
+ "source_sha256": "f74f85f43c3d18c836a74cb325d9cd1d6174a7932a2bf6f3664b5849eededc69"
90
+ },
91
+ {
92
+ "path": "benchmark/SEALED_REPORT_SHA256SUMS.txt",
93
+ "role": "sealed benchmark evidence",
94
+ "bytes": 2399,
95
+ "sha256": "f2c05574d9c89a5e6fbc2cd94ab313e533a1315cd813739fa80fd0f38df8274a",
96
+ "source": "docs/benchmarks/final_broad_20260730/SEALED_REPORT_SHA256SUMS.txt",
97
+ "source_sha256": "f2c05574d9c89a5e6fbc2cd94ab313e533a1315cd813739fa80fd0f38df8274a"
98
+ },
99
+ {
100
+ "path": "benchmark/availability.csv",
101
+ "role": "sealed benchmark evidence",
102
+ "bytes": 1081,
103
+ "sha256": "6dc0633d6c3512638754c73dabfa49603abdc1d51de36a3cc9f529332d1497de",
104
+ "source": "docs/benchmarks/final_broad_20260730/availability.csv",
105
+ "source_sha256": "6dc0633d6c3512638754c73dabfa49603abdc1d51de36a3cc9f529332d1497de"
106
+ },
107
+ {
108
+ "path": "benchmark/edge_systems.csv",
109
+ "role": "sealed benchmark evidence",
110
+ "bytes": 679,
111
+ "sha256": "8ff11a3d57e0af3ae4836a85a56cf6bcafd62801d5c851b8178918bc4e67912f",
112
+ "source": "docs/benchmarks/final_broad_20260730/edge_systems.csv",
113
+ "source_sha256": "8ff11a3d57e0af3ae4836a85a56cf6bcafd62801d5c851b8178918bc4e67912f"
114
+ },
115
+ {
116
+ "path": "benchmark/examples/final_release_training_examples.audit.json",
117
+ "role": "independent release-example audit",
118
+ "bytes": 7534,
119
+ "sha256": "9f2869c905733a125a42128726acbb55f6f0aeeb7d8a9f40ea254f6d49e9f79c",
120
+ "source": "docs/benchmarks/examples/final_release_training_examples.audit.json",
121
+ "source_sha256": "9f2869c905733a125a42128726acbb55f6f0aeeb7d8a9f40ea254f6d49e9f79c",
122
+ "normalization": "none"
123
+ },
124
+ {
125
+ "path": "benchmark/examples/final_release_training_examples.json",
126
+ "role": "bounded training examples and per-model outputs",
127
+ "bytes": 101271,
128
+ "sha256": "97d292125709277a5023afa012a6bdaa61d1effc3ea58633d95188f07d69831a",
129
+ "source": "docs/benchmarks/examples/final_release_training_examples.json",
130
+ "source_sha256": "97d292125709277a5023afa012a6bdaa61d1effc3ea58633d95188f07d69831a",
131
+ "normalization": "none"
132
+ },
133
+ {
134
+ "path": "benchmark/figures/00_report_overview.png",
135
+ "role": "benchmark figure",
136
+ "bytes": 295438,
137
+ "sha256": "10c458787ad0cc19745bde47a3c2b94e229cff00d92a24c6a128d05e0a72b4e3",
138
+ "source": "docs/assets/final_broad_benchmark/00_report_overview.png",
139
+ "source_sha256": "10c458787ad0cc19745bde47a3c2b94e229cff00d92a24c6a128d05e0a72b4e3"
140
+ },
141
+ {
142
+ "path": "benchmark/figures/01_quality_macro_micro.png",
143
+ "role": "benchmark figure",
144
+ "bytes": 159292,
145
+ "sha256": "c7c8c42a69dd9eaa42c5805656c051e3fa3fd2e9f1dc3a737c463f77dd0010ce",
146
+ "source": "docs/assets/final_broad_benchmark/01_quality_macro_micro.png",
147
+ "source_sha256": "c7c8c42a69dd9eaa42c5805656c051e3fa3fd2e9f1dc3a737c463f77dd0010ce"
148
+ },
149
+ {
150
+ "path": "benchmark/figures/02_cuda_latency_cold.png",
151
+ "role": "benchmark figure",
152
+ "bytes": 192134,
153
+ "sha256": "ef7cf15fe1b980261f046c6b810f7bac715415407ec259ee1d0f32728e400409",
154
+ "source": "docs/assets/final_broad_benchmark/02_cuda_latency_cold.png",
155
+ "source_sha256": "ef7cf15fe1b980261f046c6b810f7bac715415407ec259ee1d0f32728e400409"
156
+ },
157
+ {
158
+ "path": "benchmark/figures/03_cuda_memory_telemetry.png",
159
+ "role": "benchmark figure",
160
+ "bytes": 264045,
161
+ "sha256": "5ecbfd18aa67b453bd77c6eadf84abe3bdfcf5cba1469111a1c6053605305538",
162
+ "source": "docs/assets/final_broad_benchmark/03_cuda_memory_telemetry.png",
163
+ "source_sha256": "5ecbfd18aa67b453bd77c6eadf84abe3bdfcf5cba1469111a1c6053605305538"
164
+ },
165
+ {
166
+ "path": "benchmark/figures/04_edge_x86_unoq.png",
167
+ "role": "benchmark figure",
168
+ "bytes": 171643,
169
+ "sha256": "520518e678b7e7f8e35680d84e254c3a630ede66b9eede5ed72efc73b5c0cc5a",
170
+ "source": "docs/assets/final_broad_benchmark/04_edge_x86_unoq.png",
171
+ "source_sha256": "520518e678b7e7f8e35680d84e254c3a630ede66b9eede5ed72efc73b5c0cc5a"
172
+ },
173
+ {
174
+ "path": "benchmark/figures/05_pareto_quality_latency_memory.png",
175
+ "role": "benchmark figure",
176
+ "bytes": 182234,
177
+ "sha256": "52884ce31ecf4a309e2af8401fc9a8ec498601aee38f0ff040dacb74fd7d7442",
178
+ "source": "docs/assets/final_broad_benchmark/05_pareto_quality_latency_memory.png",
179
+ "source_sha256": "52884ce31ecf4a309e2af8401fc9a8ec498601aee38f0ff040dacb74fd7d7442"
180
+ },
181
+ {
182
+ "path": "benchmark/figures/06_prompt_generation_diagnostic.png",
183
+ "role": "benchmark figure",
184
+ "bytes": 180996,
185
+ "sha256": "0bbd9f72b151d88b0913f72a36845042bb018930fc51d08a941e88acbd1dc500",
186
+ "source": "docs/assets/final_broad_benchmark/06_prompt_generation_diagnostic.png",
187
+ "source_sha256": "0bbd9f72b151d88b0913f72a36845042bb018930fc51d08a941e88acbd1dc500"
188
+ },
189
+ {
190
+ "path": "benchmark/figures/07_availability_missingness.png",
191
+ "role": "benchmark figure",
192
+ "bytes": 246078,
193
+ "sha256": "f6ac419ac13e29a59ebf93fb1cce7c3cd783ba26f72b9387b84aa3110ec2d77e",
194
+ "source": "docs/assets/final_broad_benchmark/07_availability_missingness.png",
195
+ "source_sha256": "f6ac419ac13e29a59ebf93fb1cce7c3cd783ba26f72b9387b84aa3110ec2d77e"
196
+ },
197
+ {
198
+ "path": "benchmark/figures/08_depth_quality_latency_scaling.json",
199
+ "role": "depth chart machine-readable analysis",
200
+ "bytes": 2819,
201
+ "sha256": "9f05bccc9d8e0b6a62ab2f2d89bdc6e9007c4abaaac464dd5e8b1a492ef204de",
202
+ "source": "docs/assets/final_broad_benchmark/08_depth_quality_latency_scaling.json",
203
+ "source_sha256": "9f05bccc9d8e0b6a62ab2f2d89bdc6e9007c4abaaac464dd5e8b1a492ef204de",
204
+ "normalization": "none"
205
+ },
206
+ {
207
+ "path": "benchmark/figures/08_depth_quality_latency_scaling.png",
208
+ "role": "benchmark figure",
209
+ "bytes": 256039,
210
+ "sha256": "164c6269fe1aed00733366d0f88b31494e6c1f4d9d7c975f6d36cc1254336e24",
211
+ "source": "docs/assets/final_broad_benchmark/08_depth_quality_latency_scaling.png",
212
+ "source_sha256": "164c6269fe1aed00733366d0f88b31494e6c1f4d9d7c975f6d36cc1254336e24"
213
+ },
214
+ {
215
+ "path": "benchmark/final4l_aggregate.json",
216
+ "role": "three-seed aggregate",
217
+ "bytes": 10882,
218
+ "sha256": "0bae347bb54f8089a5fa5450f8366b5c56eb3e598beaf9a87f4385363a402bcb",
219
+ "source": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/aggregate.json",
220
+ "source_sha256": "02d66036227403c9713e7c1f364f3444dfe0f17ad7b68785a8bee4adc482e2eb",
221
+ "normalization": "none"
222
+ },
223
+ {
224
+ "path": "benchmark/independent_audit.json",
225
+ "role": "sealed benchmark evidence",
226
+ "bytes": 2838,
227
+ "sha256": "613517f2bea2313592a3ce8955afd9d5f365784f3a875a8f7057b6697fee8da9",
228
+ "source": "docs/benchmarks/final_broad_20260730/independent_audit.json",
229
+ "source_sha256": "613517f2bea2313592a3ce8955afd9d5f365784f3a875a8f7057b6697fee8da9",
230
+ "normalization": "none"
231
+ },
232
+ {
233
+ "path": "benchmark/quality_matrix.csv",
234
+ "role": "sealed benchmark evidence",
235
+ "bytes": 3144,
236
+ "sha256": "c6e316f7081e2a6dcda1ec4e95b57cdcc357ae49ed8b3f5a876c5b51e0df722f",
237
+ "source": "docs/benchmarks/final_broad_20260730/quality_matrix.csv",
238
+ "source_sha256": "c6e316f7081e2a6dcda1ec4e95b57cdcc357ae49ed8b3f5a876c5b51e0df722f"
239
+ },
240
+ {
241
+ "path": "benchmark/seed_reports/seed41.json",
242
+ "role": "seed-41 frozen report",
243
+ "bytes": 115793,
244
+ "sha256": "c1bd379680abcf50b47b7d23e4aafbc8b5e7f40a0b0e9700bde21a8ce49af803",
245
+ "source": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed41/report.json",
246
+ "source_sha256": "ec5314140fff0bdd46e87ee00ad67bee7e61d82c7cfa34add1510dd7d5bdb3c0",
247
+ "normalization": "project-root paths made repository-relative"
248
+ },
249
+ {
250
+ "path": "benchmark/seed_reports/seed42.json",
251
+ "role": "seed-42 frozen report",
252
+ "bytes": 115780,
253
+ "sha256": "a30e38433d008db4aa7e5fbfd477ab80a0daf50e970f8e7b04397efc1a47843a",
254
+ "source": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed42/report.json",
255
+ "source_sha256": "e5c45a7dc41bebf4550da7c5763d0759c6d0922da9efda4048564c11c6d2f3af",
256
+ "normalization": "project-root paths made repository-relative"
257
+ },
258
+ {
259
+ "path": "benchmark/seed_reports/seed43.json",
260
+ "role": "seed-43 frozen report",
261
+ "bytes": 115985,
262
+ "sha256": "96466290d09d27b8bc044a9f9a95765ce12b4d7b02dfb0123e436e7535623353",
263
+ "source": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed43/report.json",
264
+ "source_sha256": "e05bc2146d720706780fc45a1fb05911d5c2926caa0bcb12ffead946028eef0d",
265
+ "normalization": "project-root paths made repository-relative"
266
+ },
267
+ {
268
+ "path": "benchmark/summary.json",
269
+ "role": "sealed benchmark evidence",
270
+ "bytes": 5765,
271
+ "sha256": "0f7002222e3757052ad9f19f95b5e7fc86f119bd7e7e16c60a018d3560f76e09",
272
+ "source": "docs/benchmarks/final_broad_20260730/summary.json",
273
+ "source_sha256": "0f7002222e3757052ad9f19f95b5e7fc86f119bd7e7e16c60a018d3560f76e09",
274
+ "normalization": "none"
275
+ },
276
+ {
277
+ "path": "benchmark/unoq_lineage/01_all_models_latency_memory.png",
278
+ "role": "physical UNO Q lineage figure",
279
+ "bytes": 285503,
280
+ "sha256": "33a50de3de92c958978a979ceba165f491a11b9c3509aa903f794c4baf71ee97",
281
+ "source": "docs/assets/unoq_lineage_matrix/01_all_models_latency_memory.png",
282
+ "source_sha256": "33a50de3de92c958978a979ceba165f491a11b9c3509aa903f794c4baf71ee97"
283
+ },
284
+ {
285
+ "path": "benchmark/unoq_lineage/02_layer_reduction_efficiency.png",
286
+ "role": "physical UNO Q lineage figure",
287
+ "bytes": 211761,
288
+ "sha256": "38c54566de25cad0b8c34389736f7f42f73e2d96757901ecd570804238598067",
289
+ "source": "docs/assets/unoq_lineage_matrix/02_layer_reduction_efficiency.png",
290
+ "source_sha256": "38c54566de25cad0b8c34389736f7f42f73e2d96757901ecd570804238598067"
291
+ },
292
+ {
293
+ "path": "benchmark/unoq_lineage/03_vocab_embedding_reduction.png",
294
+ "role": "physical UNO Q lineage figure",
295
+ "bytes": 201019,
296
+ "sha256": "9265bfc1e41b0777022aee7bb862979908d5e722f7e01baf40fe87a1fd8f00f4",
297
+ "source": "docs/assets/unoq_lineage_matrix/03_vocab_embedding_reduction.png",
298
+ "source_sha256": "9265bfc1e41b0777022aee7bb862979908d5e722f7e01baf40fe87a1fd8f00f4"
299
+ },
300
+ {
301
+ "path": "benchmark/unoq_lineage/REPORT.md",
302
+ "role": "physical UNO Q lineage narrative",
303
+ "bytes": 6664,
304
+ "sha256": "dc09c9c12c0fd846619c024bb4a71007241d031699288f9311d4ea34fa18249a",
305
+ "source": "generated",
306
+ "source_sha256": null
307
+ },
308
+ {
309
+ "path": "benchmark/unoq_lineage/all_models.csv",
310
+ "role": "physical UNO Q all-model table",
311
+ "bytes": 4178,
312
+ "sha256": "d01079b046942aab1dfe6fccf973b43a4b054781ee5b47ee0033f4e6b5eb024c",
313
+ "source": "results/unoq_lineage_matrix/20260731-v1/analysis/all_models.csv",
314
+ "source_sha256": "d01079b046942aab1dfe6fccf973b43a4b054781ee5b47ee0033f4e6b5eb024c"
315
+ },
316
+ {
317
+ "path": "benchmark/unoq_lineage/analysis.json",
318
+ "role": "physical UNO Q lineage analysis",
319
+ "bytes": 13963,
320
+ "sha256": "c899a19e4433e3a40547a11cfcfc3b0a7b6f00c436aa8c1d742a5dc0399bf649",
321
+ "source": "results/unoq_lineage_matrix/20260731-v1/analysis/analysis.json",
322
+ "source_sha256": "c899a19e4433e3a40547a11cfcfc3b0a7b6f00c436aa8c1d742a5dc0399bf649",
323
+ "normalization": "none"
324
+ },
325
+ {
326
+ "path": "benchmark/unoq_lineage/chart_inputs.json",
327
+ "role": "physical UNO Q chart inputs",
328
+ "bytes": 11443,
329
+ "sha256": "3f204fcc159824a43a3b41e7b1c08ea661e6337f4277646095cca75000972050",
330
+ "source": "docs/assets/unoq_lineage_matrix/chart_inputs.json",
331
+ "source_sha256": "3f204fcc159824a43a3b41e7b1c08ea661e6337f4277646095cca75000972050",
332
+ "normalization": "none"
333
+ },
334
+ {
335
+ "path": "benchmark/unoq_lineage/manifest.json",
336
+ "role": "physical UNO Q model/runtime manifest",
337
+ "bytes": 6853,
338
+ "sha256": "4c74bf715c38d4577e939bf987f5e9f3ce93bd10a64f1999657cb3e93302378c",
339
+ "source": "results/unoq_lineage_matrix/20260731-v1/manifest.json",
340
+ "source_sha256": "4c74bf715c38d4577e939bf987f5e9f3ce93bd10a64f1999657cb3e93302378c",
341
+ "normalization": "none"
342
+ },
343
+ {
344
+ "path": "benchmark/x86_cuda_systems.csv",
345
+ "role": "sealed benchmark evidence",
346
+ "bytes": 2939,
347
+ "sha256": "11321452290bc53da5ba371d2f2524590d4abd91d981ad0f89c8708752e2a47e",
348
+ "source": "docs/benchmarks/final_broad_20260730/x86_cuda_systems.csv",
349
+ "source_sha256": "11321452290bc53da5ba371d2f2524590d4abd91d981ad0f89c8708752e2a47e"
350
+ },
351
+ {
352
+ "path": "chat_template.jinja",
353
+ "role": "seed-41 Transformers checkpoint",
354
+ "bytes": 6669,
355
+ "sha256": "04b007131663760bf3e581e5a953be77044014e87efe1d2a6ca4b72ec0eac978",
356
+ "source": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed41/checkpoint/chat_template.jinja",
357
+ "source_sha256": "04b007131663760bf3e581e5a953be77044014e87efe1d2a6ca4b72ec0eac978"
358
+ },
359
+ {
360
+ "path": "classify.py",
361
+ "role": "self-contained Transformers CLI",
362
+ "bytes": 8070,
363
+ "sha256": "1c09956cc5d3553eed0b5f64c20c81064df4373ccfaf4fec26929b8f4819773a",
364
+ "source": "scripts/hf_standalone4l_classify.py",
365
+ "source_sha256": "1c09956cc5d3553eed0b5f64c20c81064df4373ccfaf4fec26929b8f4819773a"
366
+ },
367
+ {
368
+ "path": "config.json",
369
+ "role": "seed-41 Transformers checkpoint",
370
+ "bytes": 3380,
371
+ "sha256": "9f04dfbd7e1696cd3df2ccbe194f5f155efe5baefd1a0b615851f51bf46691a0",
372
+ "source": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed41/checkpoint/config.json",
373
+ "source_sha256": "9f04dfbd7e1696cd3df2ccbe194f5f155efe5baefd1a0b615851f51bf46691a0"
374
+ },
375
+ {
376
+ "path": "edge/README.md",
377
+ "role": "UNO Q deployment guide",
378
+ "bytes": 9079,
379
+ "sha256": "fb03e2e2f2ae1452f634f70330ead9d9d17ce0a643473081c880bcfa6fac4668",
380
+ "source": "generated",
381
+ "source_sha256": null
382
+ },
383
+ {
384
+ "path": "edge/benchmark/A53_SELECTION_DECISION.md",
385
+ "role": "generic-vs-A53 runtime selection decision",
386
+ "bytes": 3765,
387
+ "sha256": "b3bd314d6207b4e4d6fe9e3ec348a8a3e2caa3510296b0e0651382ca5714e034",
388
+ "source": "results/standalone4l_edge/20260730-phase4b-a53-paired-unoq-v1/DECISION.md",
389
+ "source_sha256": "b3bd314d6207b4e4d6fe9e3ec348a8a3e2caa3510296b0e0651382ca5714e034"
390
+ },
391
+ {
392
+ "path": "edge/benchmark/a53_analysis.json",
393
+ "role": "preregistered generic-vs-A53 gate analysis",
394
+ "bytes": 5571,
395
+ "sha256": "d5023e573e344885b643e45fa9b6de52180c2ee09f2e857c420c41b74fda37d6",
396
+ "source": "results/standalone4l_edge/20260730-phase4b-a53-paired-unoq-v1/analysis/analysis.json",
397
+ "source_sha256": "f619bd86f14fc1706fa0c0d107e521b5517264a53d769b67e35999fa6422b6fb",
398
+ "normalization": "project-root paths made repository-relative"
399
+ },
400
+ {
401
+ "path": "edge/benchmark/a53_audit.json",
402
+ "role": "independent generic-vs-A53 audit",
403
+ "bytes": 3223,
404
+ "sha256": "82bef36a79a6348428ad04a2fbc816e2ede3d8c97bfb56dcb649cf3310c83a4f",
405
+ "source": "results/standalone4l_edge/20260730-phase4b-a53-paired-unoq-v1/analysis/AUDIT.json",
406
+ "source_sha256": "82bef36a79a6348428ad04a2fbc816e2ede3d8c97bfb56dcb649cf3310c83a4f",
407
+ "normalization": "none"
408
+ },
409
+ {
410
+ "path": "edge/benchmark/execution_context.json",
411
+ "role": "runtime selection execution context",
412
+ "bytes": 1418,
413
+ "sha256": "ab647cb7e10c63c97a7e9ad45edd4f05996463ef84b09d1835ed445f46f80283",
414
+ "source": "results/standalone4l_edge/20260730-phase4b-a53-paired-unoq-v1/EXECUTION_CONTEXT.json",
415
+ "source_sha256": "ab647cb7e10c63c97a7e9ad45edd4f05996463ef84b09d1835ed445f46f80283",
416
+ "normalization": "none"
417
+ },
418
+ {
419
+ "path": "edge/benchmark/generic_vs_a53_unoq.png",
420
+ "role": "raw-driven generic-vs-A53 comparison graph",
421
+ "bytes": 253732,
422
+ "sha256": "d06a0858634fab01cc43f29c8774e161f4943f8282fb512719942737e4e8c22c",
423
+ "source": "results/standalone4l_edge/20260730-phase4b-a53-paired-unoq-v1/analysis/generic_vs_a53_unoq.png",
424
+ "source_sha256": "d06a0858634fab01cc43f29c8774e161f4943f8282fb512719942737e4e8c22c"
425
+ },
426
+ {
427
+ "path": "edge/classify_short.py",
428
+ "role": "short-text Q8 inference helper",
429
+ "bytes": 5149,
430
+ "sha256": "61f11a4b64586763424a918c0cbccecfeecf27e632d0a2c50e209d8fe893d38b",
431
+ "source": "scripts/qwen35_standalone4l_q8_short_inference_v1.py",
432
+ "source_sha256": "61f11a4b64586763424a918c0cbccecfeecf27e632d0a2c50e209d8fe893d38b"
433
+ },
434
+ {
435
+ "path": "edge/qwen3.5-0.8b-standalone4l-joint-q8_0.gguf",
436
+ "role": "sealed Q8 GGUF",
437
+ "bytes": 228999360,
438
+ "sha256": "acc07b4fdd8260147b3c6cb7c7104513176bcb72c88f5277feaa25fc9080f0b3",
439
+ "source": "results/standalone4l_edge/20260728-phase2c-quant-ladder-v1/candidates/qwen3.5-0.8b-standalone4l-joint-q8_0.gguf",
440
+ "source_sha256": "acc07b4fdd8260147b3c6cb7c7104513176bcb72c88f5277feaa25fc9080f0b3"
441
+ },
442
+ {
443
+ "path": "edge/qwen3.5-0.8b-standalone4l-q8-arm64-runtime-v3.tar.gz",
444
+ "role": "sealed ARM64 runtime package",
445
+ "bytes": 222663144,
446
+ "sha256": "daf5d051c816a3aef94713e83fb532f4bd23340d3e8714818696695f9ccd26cc",
447
+ "source": "results/standalone4l_edge/20260729-phase3a-aarch64-q8-runtime-v3/qwen3.5-0.8b-standalone4l-q8-arm64-runtime-v3.tar.gz",
448
+ "source_sha256": "daf5d051c816a3aef94713e83fb532f4bd23340d3e8714818696695f9ccd26cc"
449
+ },
450
+ {
451
+ "path": "edge/runtime_manifest.json",
452
+ "role": "ARM64 package manifest",
453
+ "bytes": 8363,
454
+ "sha256": "2f91f9359efbfef94721beb8fe29b075fa04d923dd9ba829e6e75d057f7619a2",
455
+ "source": "results/standalone4l_edge/20260729-phase3a-aarch64-q8-runtime-v3/manifest.json",
456
+ "source_sha256": "2f91f9359efbfef94721beb8fe29b075fa04d923dd9ba829e6e75d057f7619a2"
457
+ },
458
+ {
459
+ "path": "edge/technique_bias.json",
460
+ "role": "required external classifier bias",
461
+ "bytes": 1307,
462
+ "sha256": "e499e4ae081129d04bcd8e9790f4aa26014436b8f0d916805798f2d638bcf1bf",
463
+ "source": "results/standalone4l_edge/20260728-phase2a-x86-f16-no-ui-v3/packed/technique_bias.json",
464
+ "source_sha256": "e499e4ae081129d04bcd8e9790f4aa26014436b8f0d916805798f2d638bcf1bf"
465
+ },
466
+ {
467
+ "path": "edge/tokenizer/tokenizer.json",
468
+ "role": "Q8 window tokenizer",
469
+ "bytes": 9224067,
470
+ "sha256": "2106c29a0c7de1308e536b9f8a01833b9ff9bd0b34a526019f478bc53b6da81d",
471
+ "source": "results/layer_shrink_v6/20260728-v6-r23-standalone4l-kd-seed41-v1/student-4l-final/tokenizer.json",
472
+ "source_sha256": "2106c29a0c7de1308e536b9f8a01833b9ff9bd0b34a526019f478bc53b6da81d"
473
+ },
474
+ {
475
+ "path": "edge/tokenizer/tokenizer_config.json",
476
+ "role": "Q8 window tokenizer",
477
+ "bytes": 1314,
478
+ "sha256": "2215318fe27878ec67e772931bb5f40a98658348d33f2672a575f766a2190d35",
479
+ "source": "results/layer_shrink_v6/20260728-v6-r23-standalone4l-kd-seed41-v1/student-4l-final/tokenizer_config.json",
480
+ "source_sha256": "2215318fe27878ec67e772931bb5f40a98658348d33f2672a575f766a2190d35"
481
+ },
482
+ {
483
+ "path": "edge/unoq_acceptance_audit.json",
484
+ "role": "UNO Q acceptance audit",
485
+ "bytes": 3461,
486
+ "sha256": "bc5c910c838f715cbea18d736832e2eafb4a43fd99cf2d69ab490b9083b3608c",
487
+ "source": "results/standalone4l_edge/20260729-phase3b-unoq-device-acceptance-v1/audit.json",
488
+ "source_sha256": "bc5c910c838f715cbea18d736832e2eafb4a43fd99cf2d69ab490b9083b3608c"
489
+ },
490
+ {
491
+ "path": "edge/unoq_acceptance_report.json",
492
+ "role": "UNO Q acceptance report",
493
+ "bytes": 7467,
494
+ "sha256": "ee0c9c2996ea65f5b18d602b66bdc9ac2e0b160aeb52faa54d255c24446217b1",
495
+ "source": "results/standalone4l_edge/20260729-phase3b-unoq-device-acceptance-v1/acceptance_report.json",
496
+ "source_sha256": "ee0c9c2996ea65f5b18d602b66bdc9ac2e0b160aeb52faa54d255c24446217b1"
497
+ },
498
+ {
499
+ "path": "labels.json",
500
+ "role": "fixed label order",
501
+ "bytes": 891,
502
+ "sha256": "946fed4fe4c3d320fd113ec8e5776b10f2707b936f857341bb5443d7a9038248",
503
+ "source": "generated",
504
+ "source_sha256": null
505
+ },
506
+ {
507
+ "path": "model.safetensors",
508
+ "role": "seed-41 Transformers checkpoint",
509
+ "bytes": 421812688,
510
+ "sha256": "0352da6e4717a2e6f3a9d643d034be2cb1c39632d1bce0ced339ddb62acedb37",
511
+ "source": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed41/checkpoint/model.safetensors",
512
+ "source_sha256": "0352da6e4717a2e6f3a9d643d034be2cb1c39632d1bce0ced339ddb62acedb37"
513
+ },
514
+ {
515
+ "path": "requirements.txt",
516
+ "role": "inference dependency lock",
517
+ "bytes": 54,
518
+ "sha256": "2994886af208a0120c051745f9c14036e6f2caae8c4af863e5965d5a5bd68fdf",
519
+ "source": "generated",
520
+ "source_sha256": null
521
+ },
522
+ {
523
+ "path": "seeds/seed42/chat_template.jinja",
524
+ "role": "seed-42 Transformers checkpoint",
525
+ "bytes": 6669,
526
+ "sha256": "04b007131663760bf3e581e5a953be77044014e87efe1d2a6ca4b72ec0eac978",
527
+ "source": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed42/checkpoint/chat_template.jinja",
528
+ "source_sha256": "04b007131663760bf3e581e5a953be77044014e87efe1d2a6ca4b72ec0eac978"
529
+ },
530
+ {
531
+ "path": "seeds/seed42/config.json",
532
+ "role": "seed-42 Transformers checkpoint",
533
+ "bytes": 3380,
534
+ "sha256": "9f04dfbd7e1696cd3df2ccbe194f5f155efe5baefd1a0b615851f51bf46691a0",
535
+ "source": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed42/checkpoint/config.json",
536
+ "source_sha256": "9f04dfbd7e1696cd3df2ccbe194f5f155efe5baefd1a0b615851f51bf46691a0"
537
+ },
538
+ {
539
+ "path": "seeds/seed42/model.safetensors",
540
+ "role": "seed-42 Transformers checkpoint",
541
+ "bytes": 421812688,
542
+ "sha256": "2db5f4be391d9dee64458cb5865530f39747019e7a219443b5f7850befbce697",
543
+ "source": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed42/checkpoint/model.safetensors",
544
+ "source_sha256": "2db5f4be391d9dee64458cb5865530f39747019e7a219443b5f7850befbce697"
545
+ },
546
+ {
547
+ "path": "seeds/seed42/tokenizer.json",
548
+ "role": "seed-42 Transformers checkpoint",
549
+ "bytes": 9223900,
550
+ "sha256": "dd2b27bbd4f5f650c7200300013ef03ee01efe620b128343163521bf04806fc8",
551
+ "source": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed42/checkpoint/tokenizer.json",
552
+ "source_sha256": "dd2b27bbd4f5f650c7200300013ef03ee01efe620b128343163521bf04806fc8"
553
+ },
554
+ {
555
+ "path": "seeds/seed42/tokenizer_config.json",
556
+ "role": "seed-42 Transformers checkpoint",
557
+ "bytes": 1314,
558
+ "sha256": "2215318fe27878ec67e772931bb5f40a98658348d33f2672a575f766a2190d35",
559
+ "source": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed42/checkpoint/tokenizer_config.json",
560
+ "source_sha256": "2215318fe27878ec67e772931bb5f40a98658348d33f2672a575f766a2190d35"
561
+ },
562
+ {
563
+ "path": "seeds/seed43/chat_template.jinja",
564
+ "role": "seed-43 Transformers checkpoint",
565
+ "bytes": 6669,
566
+ "sha256": "04b007131663760bf3e581e5a953be77044014e87efe1d2a6ca4b72ec0eac978",
567
+ "source": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed43/checkpoint/chat_template.jinja",
568
+ "source_sha256": "04b007131663760bf3e581e5a953be77044014e87efe1d2a6ca4b72ec0eac978"
569
+ },
570
+ {
571
+ "path": "seeds/seed43/config.json",
572
+ "role": "seed-43 Transformers checkpoint",
573
+ "bytes": 3380,
574
+ "sha256": "9f04dfbd7e1696cd3df2ccbe194f5f155efe5baefd1a0b615851f51bf46691a0",
575
+ "source": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed43/checkpoint/config.json",
576
+ "source_sha256": "9f04dfbd7e1696cd3df2ccbe194f5f155efe5baefd1a0b615851f51bf46691a0"
577
+ },
578
+ {
579
+ "path": "seeds/seed43/model.safetensors",
580
+ "role": "seed-43 Transformers checkpoint",
581
+ "bytes": 421812688,
582
+ "sha256": "81a56d497df758e8a292eea528438cfc4d5a847cd37d3a7e1107fefa7c500640",
583
+ "source": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed43/checkpoint/model.safetensors",
584
+ "source_sha256": "81a56d497df758e8a292eea528438cfc4d5a847cd37d3a7e1107fefa7c500640"
585
+ },
586
+ {
587
+ "path": "seeds/seed43/tokenizer.json",
588
+ "role": "seed-43 Transformers checkpoint",
589
+ "bytes": 9223900,
590
+ "sha256": "dd2b27bbd4f5f650c7200300013ef03ee01efe620b128343163521bf04806fc8",
591
+ "source": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed43/checkpoint/tokenizer.json",
592
+ "source_sha256": "dd2b27bbd4f5f650c7200300013ef03ee01efe620b128343163521bf04806fc8"
593
+ },
594
+ {
595
+ "path": "seeds/seed43/tokenizer_config.json",
596
+ "role": "seed-43 Transformers checkpoint",
597
+ "bytes": 1314,
598
+ "sha256": "2215318fe27878ec67e772931bb5f40a98658348d33f2672a575f766a2190d35",
599
+ "source": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed43/checkpoint/tokenizer_config.json",
600
+ "source_sha256": "2215318fe27878ec67e772931bb5f40a98658348d33f2672a575f766a2190d35"
601
+ },
602
+ {
603
+ "path": "thresholds.json",
604
+ "role": "seed thresholds and aggregate metrics",
605
+ "bytes": 2631,
606
+ "sha256": "737c4e145a5b51660c4c45a8acea4c32219cbbbc0706abe235e36b2ad430f17d",
607
+ "source": "generated",
608
+ "source_sha256": null
609
+ },
610
+ {
611
+ "path": "tokenizer.json",
612
+ "role": "seed-41 Transformers checkpoint",
613
+ "bytes": 9223900,
614
+ "sha256": "dd2b27bbd4f5f650c7200300013ef03ee01efe620b128343163521bf04806fc8",
615
+ "source": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed41/checkpoint/tokenizer.json",
616
+ "source_sha256": "dd2b27bbd4f5f650c7200300013ef03ee01efe620b128343163521bf04806fc8"
617
+ },
618
+ {
619
+ "path": "tokenizer_config.json",
620
+ "role": "seed-41 Transformers checkpoint",
621
+ "bytes": 1314,
622
+ "sha256": "2215318fe27878ec67e772931bb5f40a98658348d33f2672a575f766a2190d35",
623
+ "source": "results/final_broad_benchmark/20260729-v1/trainable/qwen35-standalone4l-commonhead/seed41/checkpoint/tokenizer_config.json",
624
+ "source_sha256": "2215318fe27878ec67e772931bb5f40a98658348d33f2672a575f766a2190d35"
625
+ }
626
+ ],
627
+ "claim_boundary": {
628
+ "opened_public_human_gold": true,
629
+ "test_articles": 55,
630
+ "three_seed_sample_sd_not_ci": true,
631
+ "q8_different_head_lineage": true,
632
+ "task3_private_or_final_used": false,
633
+ "mac_used": false,
634
+ "recommended_edge_runtime": "generic-armv8a-v3",
635
+ "a53_candidate_promoted": false
636
+ }
637
+ }
models/semeval-propaganda/requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ torch>=2.4
2
+ transformers==5.13.0
3
+ huggingface_hub>=0.34
models/semeval-propaganda/seeds/seed42/chat_template.jinja ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- set image_count = namespace(value=0) %}
2
+ {%- set video_count = namespace(value=0) %}
3
+ {%- macro render_content(content, do_vision_count) %}
4
+ {%- if content is string %}
5
+ {{- content }}
6
+ {%- else %}
7
+ {%- for item in content %}
8
+ {%- if 'image' in item or 'image_url' in item or item.type == 'image' %}
9
+ {%- if do_vision_count %}
10
+ {%- set image_count.value = image_count.value + 1 %}
11
+ {%- endif %}
12
+ {%- if add_vision_id %}Picture {{ image_count.value }}: {% endif -%}
13
+ <|vision_start|><|image_pad|><|vision_end|>
14
+ {%- elif 'video' in item or item.type == 'video' %}
15
+ {%- if do_vision_count %}
16
+ {%- set video_count.value = video_count.value + 1 %}
17
+ {%- endif %}
18
+ {%- if add_vision_id %}Video {{ video_count.value }}: {% endif -%}
19
+ <|vision_start|><|video_pad|><|vision_end|>
20
+ {%- elif 'text' in item %}
21
+ {{- item.text }}
22
+ {%- endif %}
23
+ {%- endfor %}
24
+ {%- endif %}
25
+ {%- endmacro %}
26
+ {%- if tools is defined and tools is iterable and tools | length > 0 %}
27
+ {{- '<|im_start|>system\n' }}
28
+ {{- "# Tools\n\nYou have access to the following functions:\n\n<tools>" }}
29
+ {%- for tool in tools %}
30
+ {{- "\n" }}
31
+ {{- tool | tojson }}
32
+ {%- endfor %}
33
+ {{- "\n</tools>" }}
34
+ {{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
35
+ {%- if messages[0].role == 'system' and messages[0].content | trim %}
36
+ {%- set content = render_content(messages[0].content, false) %}
37
+ {{- '\n\n' + content.lstrip('\n') }}
38
+ {%- endif %}
39
+ {{- '<|im_end|>\n' }}
40
+ {%- else %}
41
+ {%- if messages[0].role == 'system' %}
42
+ {{- '<|im_start|>system\n' + render_content(messages[0].content, false) + '<|im_end|>\n' }}
43
+ {%- endif %}
44
+ {%- endif %}
45
+ {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
46
+ {%- for message in messages[::-1] %}
47
+ {%- set index = (messages|length - 1) - loop.index0 %}
48
+ {%- if ns.multi_step_tool and message.role == "user" %}
49
+ {%- set content = render_content(message.content, False) %}
50
+ {%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
51
+ {%- set ns.multi_step_tool = false %}
52
+ {%- set ns.last_query_index = index %}
53
+ {%- endif %}
54
+ {%- endif %}
55
+ {%- endfor %}
56
+ {%- for message in messages %}
57
+ {%- set content = render_content(message.content, true) %}
58
+ {%- if (message.role == "user") or (message.role == "system" and not loop.first) %}
59
+ {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
60
+ {%- elif message.role == "assistant" %}
61
+ {%- set reasoning_content = '' %}
62
+ {%- if message.reasoning_content is defined and message.reasoning_content is not none %}
63
+ {%- set reasoning_content = message.reasoning_content %}
64
+ {%- else %}
65
+ {%- if '</think>' in content %}
66
+ {%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
67
+ {%- set content = content.split('</think>')[-1].lstrip('\n') %}
68
+ {%- endif %}
69
+ {%- endif %}
70
+ {%- if message.tool_calls is defined and message.tool_calls is not none and message.tool_calls is iterable and message.tool_calls | length > 0 %}
71
+ {%- set content = content.rstrip('\n') %}
72
+ {%- endif %}
73
+ {%- if loop.index0 > ns.last_query_index %}
74
+ {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content.strip('\n') + '\n</think>\n\n' + content.lstrip('\n') }}
75
+ {%- else %}
76
+ {{- '<|im_start|>' + message.role + '\n' + content }}
77
+ {%- endif %}
78
+ {%- if message.tool_calls is defined and message.tool_calls is not none and message.tool_calls is iterable and message.tool_calls | length > 0 %}
79
+ {%- for tool_call in message.tool_calls %}
80
+ {%- if tool_call.function is defined %}
81
+ {%- set tool_call = tool_call.function %}
82
+ {%- endif %}
83
+ {%- if loop.first %}
84
+ {%- if content|trim %}
85
+ {{- '\n\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
86
+ {%- else %}
87
+ {{- '<tool_call>\n<function=' + tool_call.name + '>\n' }}
88
+ {%- endif %}
89
+ {%- else %}
90
+ {{- '\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
91
+ {%- endif %}
92
+ {%- if tool_call.arguments is defined %}
93
+ {%- for args_name, args_value in tool_call.arguments|items %}
94
+ {{- '<parameter=' + args_name + '>\n' }}
95
+ {%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
96
+ {{- args_value }}
97
+ {{- '\n</parameter>\n' }}
98
+ {%- endfor %}
99
+ {%- endif %}
100
+ {{- '</function>\n</tool_call>' }}
101
+ {%- endfor %}
102
+ {%- endif %}
103
+ {{- '<|im_end|>\n' }}
104
+ {%- elif message.role == "tool" %}
105
+ {%- if loop.previtem and loop.previtem.role != "tool" %}
106
+ {{- '<|im_start|>user' }}
107
+ {%- endif %}
108
+ {{- '\n<tool_response>\n' }}
109
+ {{- content }}
110
+ {{- '\n</tool_response>' }}
111
+ {%- if not loop.last and loop.nextitem.role != "tool" %}
112
+ {{- '<|im_end|>\n' }}
113
+ {%- elif loop.last %}
114
+ {{- '<|im_end|>\n' }}
115
+ {%- endif %}
116
+ {%- endif %}
117
+ {%- endfor %}
118
+ {%- if add_generation_prompt %}
119
+ {{- '<|im_start|>assistant\n<think>\n' }}
120
+ {%- endif %}
models/semeval-propaganda/seeds/seed42/config.json ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "Qwen3_5TextForSequenceClassification"
4
+ ],
5
+ "attention_bias": false,
6
+ "attention_dropout": 0.0,
7
+ "attn_output_gate": true,
8
+ "bos_token_id": null,
9
+ "dtype": "bfloat16",
10
+ "eos_token_id": 127967,
11
+ "full_attention_interval": 4,
12
+ "head_dim": 256,
13
+ "hidden_act": "silu",
14
+ "hidden_size": 1024,
15
+ "id2label": {
16
+ "0": "Appeal_to_Authority",
17
+ "1": "Appeal_to_fear-prejudice",
18
+ "2": "Bandwagon,Reductio_ad_hitlerum",
19
+ "3": "Black-and-White_Fallacy",
20
+ "4": "Causal_Oversimplification",
21
+ "5": "Doubt",
22
+ "6": "Exaggeration,Minimisation",
23
+ "7": "Flag-Waving",
24
+ "8": "Loaded_Language",
25
+ "9": "Name_Calling,Labeling",
26
+ "10": "Repetition",
27
+ "11": "Slogans",
28
+ "12": "Thought-terminating_Cliches",
29
+ "13": "Whataboutism,Straw_Men,Red_Herring"
30
+ },
31
+ "initializer_range": 0.02,
32
+ "intermediate_size": 3584,
33
+ "label2id": {
34
+ "Appeal_to_Authority": 0,
35
+ "Appeal_to_fear-prejudice": 1,
36
+ "Bandwagon,Reductio_ad_hitlerum": 2,
37
+ "Black-and-White_Fallacy": 3,
38
+ "Causal_Oversimplification": 4,
39
+ "Doubt": 5,
40
+ "Exaggeration,Minimisation": 6,
41
+ "Flag-Waving": 7,
42
+ "Loaded_Language": 8,
43
+ "Name_Calling,Labeling": 9,
44
+ "Repetition": 10,
45
+ "Slogans": 11,
46
+ "Thought-terminating_Cliches": 12,
47
+ "Whataboutism,Straw_Men,Red_Herring": 13
48
+ },
49
+ "layer_types": [
50
+ "linear_attention",
51
+ "full_attention",
52
+ "linear_attention",
53
+ "full_attention"
54
+ ],
55
+ "linear_conv_kernel_dim": 4,
56
+ "linear_key_head_dim": 128,
57
+ "linear_num_key_heads": 16,
58
+ "linear_num_value_heads": 16,
59
+ "linear_value_head_dim": 128,
60
+ "mamba_ssm_dtype": "float32",
61
+ "max_position_embeddings": 262144,
62
+ "mlp_only_layers": [],
63
+ "model_type": "qwen3_5_text",
64
+ "mtp_num_hidden_layers": 1,
65
+ "mtp_use_dedicated_embeddings": false,
66
+ "num_attention_heads": 8,
67
+ "num_hidden_layers": 4,
68
+ "num_key_value_heads": 2,
69
+ "pad_token_id": 127967,
70
+ "partial_rotary_factor": 0.25,
71
+ "phase_v6_map_selection_rank": 1,
72
+ "phase_v6_map_selection_split": "public_gold_train",
73
+ "phase_v6_materialized_role": "student6l-r3-final-teacher",
74
+ "phase_v6_materialized_trainable_state": "results/layer_shrink_v6/20260726-v6-r3-student6l-map013457-kd-seed41-pilot-v2/student-6l-kd-trainable.safetensors",
75
+ "phase_v6_selected_original_8l_layers": [
76
+ 0,
77
+ 3,
78
+ 4,
79
+ 7
80
+ ],
81
+ "phase_v6_selected_source_layers": [
82
+ 0,
83
+ 1,
84
+ 3,
85
+ 4,
86
+ 5,
87
+ 7
88
+ ],
89
+ "phase_v6_selected_source_layers_6l": [
90
+ 0,
91
+ 2,
92
+ 3,
93
+ 5
94
+ ],
95
+ "phase_v6_source_teacher": "student6l-r3-final",
96
+ "phase_v6_standalone4l_kd": {
97
+ "canonical_materialization_dtype": "bfloat16",
98
+ "canonical_training_state_preservation": true,
99
+ "epochs": 3,
100
+ "legacy_float16_checkpoint": "results/layer_shrink_v6/20260728-v6-r23-standalone4l-kd-seed41-v1/student-4l-final",
101
+ "opened_calibration_used_for_model_selection": false,
102
+ "seed": 41,
103
+ "teacher": "results/layer_shrink_v6/20260728-v6-r22-standalone4l-map-probe-seed41-v1/teacher-6l-r3-final"
104
+ },
105
+ "problem_type": "multi_label_classification",
106
+ "rms_norm_eps": 1e-06,
107
+ "rope_parameters": {
108
+ "mrope_interleaved": true,
109
+ "mrope_section": [
110
+ 11,
111
+ 11,
112
+ 10
113
+ ],
114
+ "partial_rotary_factor": 0.25,
115
+ "rope_theta": 10000000,
116
+ "rope_type": "default"
117
+ },
118
+ "tie_word_embeddings": true,
119
+ "transformers_version": "5.13.0",
120
+ "use_cache": false,
121
+ "vocab_size": 128000
122
+ }
models/semeval-propaganda/seeds/seed42/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2db5f4be391d9dee64458cb5865530f39747019e7a219443b5f7850befbce697
3
+ size 421812688
models/semeval-propaganda/seeds/seed42/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
models/semeval-propaganda/seeds/seed42/tokenizer_config.json ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "audio_bos_token": "<|audio_start|>",
4
+ "audio_eos_token": "<|audio_end|>",
5
+ "audio_token": "<|audio_pad|>",
6
+ "backend": "tokenizers",
7
+ "bos_token": null,
8
+ "clean_up_tokenization_spaces": false,
9
+ "eos_token": "<|im_end|>",
10
+ "errors": "replace",
11
+ "image_token": "<|image_pad|>",
12
+ "is_local": true,
13
+ "local_files_only": true,
14
+ "max_length": 256,
15
+ "model_max_length": 131072,
16
+ "model_specific_special_tokens": {
17
+ "audio_bos_token": "<|audio_start|>",
18
+ "audio_eos_token": "<|audio_end|>",
19
+ "audio_token": "<|audio_pad|>",
20
+ "image_token": "<|image_pad|>",
21
+ "video_token": "<|video_pad|>",
22
+ "vision_bos_token": "<|vision_start|>",
23
+ "vision_eos_token": "<|vision_end|>"
24
+ },
25
+ "pad_to_multiple_of": null,
26
+ "pad_token": "<|endoftext|>",
27
+ "pad_token_type_id": 0,
28
+ "padding_side": "right",
29
+ "pretokenize_regex": "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?[\\p{L}\\p{M}]+|\\p{N}| ?[^\\s\\p{L}\\p{M}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
30
+ "split_special_tokens": false,
31
+ "stride": 0,
32
+ "tokenizer_class": "Qwen2Tokenizer",
33
+ "truncation_side": "right",
34
+ "truncation_strategy": "longest_first",
35
+ "unk_token": null,
36
+ "video_token": "<|video_pad|>",
37
+ "vision_bos_token": "<|vision_start|>",
38
+ "vision_eos_token": "<|vision_end|>"
39
+ }
models/semeval-propaganda/seeds/seed43/chat_template.jinja ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- set image_count = namespace(value=0) %}
2
+ {%- set video_count = namespace(value=0) %}
3
+ {%- macro render_content(content, do_vision_count) %}
4
+ {%- if content is string %}
5
+ {{- content }}
6
+ {%- else %}
7
+ {%- for item in content %}
8
+ {%- if 'image' in item or 'image_url' in item or item.type == 'image' %}
9
+ {%- if do_vision_count %}
10
+ {%- set image_count.value = image_count.value + 1 %}
11
+ {%- endif %}
12
+ {%- if add_vision_id %}Picture {{ image_count.value }}: {% endif -%}
13
+ <|vision_start|><|image_pad|><|vision_end|>
14
+ {%- elif 'video' in item or item.type == 'video' %}
15
+ {%- if do_vision_count %}
16
+ {%- set video_count.value = video_count.value + 1 %}
17
+ {%- endif %}
18
+ {%- if add_vision_id %}Video {{ video_count.value }}: {% endif -%}
19
+ <|vision_start|><|video_pad|><|vision_end|>
20
+ {%- elif 'text' in item %}
21
+ {{- item.text }}
22
+ {%- endif %}
23
+ {%- endfor %}
24
+ {%- endif %}
25
+ {%- endmacro %}
26
+ {%- if tools is defined and tools is iterable and tools | length > 0 %}
27
+ {{- '<|im_start|>system\n' }}
28
+ {{- "# Tools\n\nYou have access to the following functions:\n\n<tools>" }}
29
+ {%- for tool in tools %}
30
+ {{- "\n" }}
31
+ {{- tool | tojson }}
32
+ {%- endfor %}
33
+ {{- "\n</tools>" }}
34
+ {{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
35
+ {%- if messages[0].role == 'system' and messages[0].content | trim %}
36
+ {%- set content = render_content(messages[0].content, false) %}
37
+ {{- '\n\n' + content.lstrip('\n') }}
38
+ {%- endif %}
39
+ {{- '<|im_end|>\n' }}
40
+ {%- else %}
41
+ {%- if messages[0].role == 'system' %}
42
+ {{- '<|im_start|>system\n' + render_content(messages[0].content, false) + '<|im_end|>\n' }}
43
+ {%- endif %}
44
+ {%- endif %}
45
+ {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
46
+ {%- for message in messages[::-1] %}
47
+ {%- set index = (messages|length - 1) - loop.index0 %}
48
+ {%- if ns.multi_step_tool and message.role == "user" %}
49
+ {%- set content = render_content(message.content, False) %}
50
+ {%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
51
+ {%- set ns.multi_step_tool = false %}
52
+ {%- set ns.last_query_index = index %}
53
+ {%- endif %}
54
+ {%- endif %}
55
+ {%- endfor %}
56
+ {%- for message in messages %}
57
+ {%- set content = render_content(message.content, true) %}
58
+ {%- if (message.role == "user") or (message.role == "system" and not loop.first) %}
59
+ {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
60
+ {%- elif message.role == "assistant" %}
61
+ {%- set reasoning_content = '' %}
62
+ {%- if message.reasoning_content is defined and message.reasoning_content is not none %}
63
+ {%- set reasoning_content = message.reasoning_content %}
64
+ {%- else %}
65
+ {%- if '</think>' in content %}
66
+ {%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
67
+ {%- set content = content.split('</think>')[-1].lstrip('\n') %}
68
+ {%- endif %}
69
+ {%- endif %}
70
+ {%- if message.tool_calls is defined and message.tool_calls is not none and message.tool_calls is iterable and message.tool_calls | length > 0 %}
71
+ {%- set content = content.rstrip('\n') %}
72
+ {%- endif %}
73
+ {%- if loop.index0 > ns.last_query_index %}
74
+ {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content.strip('\n') + '\n</think>\n\n' + content.lstrip('\n') }}
75
+ {%- else %}
76
+ {{- '<|im_start|>' + message.role + '\n' + content }}
77
+ {%- endif %}
78
+ {%- if message.tool_calls is defined and message.tool_calls is not none and message.tool_calls is iterable and message.tool_calls | length > 0 %}
79
+ {%- for tool_call in message.tool_calls %}
80
+ {%- if tool_call.function is defined %}
81
+ {%- set tool_call = tool_call.function %}
82
+ {%- endif %}
83
+ {%- if loop.first %}
84
+ {%- if content|trim %}
85
+ {{- '\n\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
86
+ {%- else %}
87
+ {{- '<tool_call>\n<function=' + tool_call.name + '>\n' }}
88
+ {%- endif %}
89
+ {%- else %}
90
+ {{- '\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
91
+ {%- endif %}
92
+ {%- if tool_call.arguments is defined %}
93
+ {%- for args_name, args_value in tool_call.arguments|items %}
94
+ {{- '<parameter=' + args_name + '>\n' }}
95
+ {%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
96
+ {{- args_value }}
97
+ {{- '\n</parameter>\n' }}
98
+ {%- endfor %}
99
+ {%- endif %}
100
+ {{- '</function>\n</tool_call>' }}
101
+ {%- endfor %}
102
+ {%- endif %}
103
+ {{- '<|im_end|>\n' }}
104
+ {%- elif message.role == "tool" %}
105
+ {%- if loop.previtem and loop.previtem.role != "tool" %}
106
+ {{- '<|im_start|>user' }}
107
+ {%- endif %}
108
+ {{- '\n<tool_response>\n' }}
109
+ {{- content }}
110
+ {{- '\n</tool_response>' }}
111
+ {%- if not loop.last and loop.nextitem.role != "tool" %}
112
+ {{- '<|im_end|>\n' }}
113
+ {%- elif loop.last %}
114
+ {{- '<|im_end|>\n' }}
115
+ {%- endif %}
116
+ {%- endif %}
117
+ {%- endfor %}
118
+ {%- if add_generation_prompt %}
119
+ {{- '<|im_start|>assistant\n<think>\n' }}
120
+ {%- endif %}