Grio43 commited on
Commit
d7b2aae
·
verified ·
1 Parent(s): 83e4e1d

Release experimental-X1.1: completed 448px ONNX and FP32 safetensors

Browse files
.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
+ *.onnx filter=lfs diff=lfs merge=lfs -text
2
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
LICENSE ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Educational Use Only License
2
+
3
+ Copyright (c) 2025
4
+
5
+ ## Grant of License
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, and distribute the Software solely for **educational purposes**, subject to the following conditions:
8
+
9
+ ## Educational Purposes Definition
10
+
11
+ "Educational purposes" means use of the Software for:
12
+ - Personal learning and skill development
13
+ - Academic research and study
14
+ - Teaching in educational institutions (schools, universities, colleges)
15
+ - Non-commercial educational workshops and tutorials
16
+ - Student projects and assignments
17
+ - Open-source educational content creation
18
+
19
+ ## Restrictions
20
+
21
+ The Software may **NOT** be used for:
22
+ - Commercial purposes of any kind
23
+ - Revenue-generating activities
24
+ - Production environments in for-profit organizations
25
+ - Consulting or professional services
26
+ - Any purpose that directly or indirectly generates monetary compensation
27
+
28
+ ## Attribution
29
+
30
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
31
+
32
+ Any public use or distribution of work derived from this Software must include clear attribution to the original author and a link to the original repository.
33
+
34
+ ## Disclaimer
35
+
36
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
37
+
38
+ ## Additional Terms
39
+
40
+ 1. **Derivative Works**: Any modifications or derivative works must also be licensed under these same terms and clearly marked as modified versions.
41
+
42
+ 2. **No Endorsement**: Use of this Software does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor.
43
+
44
+ 3. **Termination**: This license automatically terminates if you violate any of these restrictions and may be terminated by the copyright holder at any time.
45
+
46
+ ## Contact
47
+
48
+ For commercial licensing inquiries or permissions beyond the scope of this license, please contact: on telegram @grio43
README.md ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ pipeline_tag: image-classification
3
+ license: other
4
+ license_name: educational-use-only
5
+ license_link: LICENSE
6
+ language:
7
+ - en
8
+ base_model: Grio43/OppaiOracle-experimental-X1
9
+ base_model_relation: finetune
10
+ tags:
11
+ - pytorch
12
+ - onnx
13
+ - safetensors
14
+ - anime
15
+ - image-tagging
16
+ - multi-label
17
+ - vision-transformer
18
+ - experimental
19
+ ---
20
+
21
+ # OppaiOracle experimental-X1.1
22
+
23
+ **experimental-X1.1 is the 448×448 continuation of
24
+ [experimental-X1](https://huggingface.co/Grio43/OppaiOracle-experimental-X1).**
25
+ It is a multi-label anime and illustration tagger, released in **FP32 ONNX and
26
+ FP32 safetensors** formats. Both contain the selected **448px epoch 9, update
27
+ 55,719** weights. This continuation completed its nine-epoch schedule and WSD
28
+ cooldown to zero learning rate on September 17, 2026.
29
+
30
+ This remains an experimental release. The shared-tag comparison below shows higher
31
+ micro-F1 than V1.1 at development-tuned thresholds, with slightly lower overall
32
+ exact mAP. Performance varies by tag and frequency; there is no blanket claim that
33
+ X1.1 replaces V1.1 for every use.
34
+
35
+ ## Download and run
36
+
37
+ The complete download includes both weight formats, about **1.18 GB** in total.
38
+ Use a fresh Python environment, download the repository, then install the
39
+ dependencies for your chosen format:
40
+
41
+ ```bash
42
+ python -m pip install huggingface_hub
43
+ python -c "from huggingface_hub import snapshot_download; snapshot_download('Grio43/OppaiOracle-experimental-X1.1', local_dir='OppaiOracle-experimental-X1.1')"
44
+ cd OppaiOracle-experimental-X1.1
45
+ ```
46
+
47
+ For ONNX Runtime on CPU:
48
+
49
+ ```bash
50
+ python -m pip install -r requirements.txt
51
+ python infer.py image.jpg --format onnx --device cpu --output predictions.json
52
+ ```
53
+
54
+ For safetensors with PyTorch:
55
+
56
+ ```bash
57
+ python -m pip install -r requirements-torch.txt
58
+ python infer.py image.jpg --format safetensors --device cpu --output predictions.json
59
+ ```
60
+
61
+ For a smaller download, obtain just [download.py](download.py), install
62
+ `huggingface_hub`, and run one of the following from the directory containing it:
63
+
64
+ ```bash
65
+ python download.py OppaiOracle-experimental-X1.1 --format onnx
66
+ python download.py OppaiOracle-experimental-X1.1 --format safetensors
67
+ ```
68
+
69
+ Each command downloads the selected weights and required support files. The
70
+ helper resolves `--revision` (default `main`) to one commit before downloading,
71
+ keeping the configuration, vocabulary and weights aligned. Use `--format all`
72
+ to obtain both formats. The resulting directory supports the same install and
73
+ inference commands above.
74
+
75
+ The supplied PyTorch model code loads `model.safetensors` and the real architecture
76
+ configuration in `config.json`. These are inference weights; the safetensors file
77
+ does not contain a pickled training checkpoint or optimizer state. The release
78
+ preserves FP32 values without quantization or conversion to BF16/FP16.
79
+
80
+ Both inference paths apply the same preprocessing and return tag names with sigmoid
81
+ scores, excluding PAD and UNK. The default threshold is **0.7085427045822144**, the
82
+ best micro-F1 threshold on the shared-tag development comparison's 200-point grid.
83
+ It is a useful starting point, **not final deployment calibration**, and was not
84
+ optimized independently for every tag or for the complete vocabulary. Set
85
+ `--threshold` for your application. Scores are not calibrated probabilities that a
86
+ label is correct.
87
+
88
+ To inspect the 30 highest scores regardless of the default threshold:
89
+
90
+ ```bash
91
+ python infer.py image.jpg --format onnx --threshold 0 --top-k 30
92
+ ```
93
+
94
+ Multiple image paths and `--batch-size` are supported. `--device cuda` requires
95
+ the corresponding CUDA-capable PyTorch or ONNX Runtime environment. Local
96
+ inference does not make network requests.
97
+
98
+ ## Training
99
+
100
+ The model lineage was trained **from scratch**, without an externally pretrained
101
+ backbone. X1.1 starts from the released 320px X1 weights and adapts **all model
102
+ weights**, rather than freezing the backbone.
103
+
104
+ | Stage | Recorded recipe |
105
+ |---|---|
106
+ | 320px source | V2 Phase 1, epoch 21, update 127,974; the checkpoint released as experimental-X1 |
107
+ | 320px optimization | Effective batch 1,024; AdamW8bit with FP32 head/position optimizer states; BF16 AMP; weight decay 0.05; 10,000-update warmup; stable LR 5.4e-4 |
108
+ | Source loss history | ASL negative gamma 4 initially, explicitly changed to 3 at update 41,223; positive gamma 0, probability clip 0.05, alpha 1, no label smoothing, detached focal weights |
109
+ | 448px continuation | Nine phase-local epochs, 55,719 optimizer updates, September 13–17, 2026 |
110
+ | Initialization | One-time bicubic positional-grid interpolation from 20×20 to 28×28; fresh optimizer, scheduler and phase-local history; learned weights and frozen vocabulary retained |
111
+ | 448px batch and precision | Microbatch 48 × 21 accumulation = 1,008; BF16 AMP; no activation checkpointing |
112
+ | 448px optimizer | AdamW8bit; FP32 head/position optimizer states; weight decay 0.05; gradient norm clipping 1.0 |
113
+ | 448px schedule | Base LR 1.5e-5, square-root scaling from batch 256 gives peak LR 2.97647022495e-5; one warmup epoch; budget-driven WSD cooldown begins at update 48,451, lasts 7,268 updates and ends at zero LR |
114
+ | 448px loss | Fixed ASL negative/positive gamma 3/0, probability clip 0.05, alpha 1, no label smoothing, detached focal weights; PAD/UNK excluded |
115
+ | Selection | Highest recorded cooldown validation binned mAP; epoch 9 is both the selected best and final checkpoint |
116
+
117
+ The 320px source did not complete its original planned cooldown. The separate
118
+ 448px experiment did complete its own cooldown; it does not imply completion of a
119
+ later 512px phase. Plateau-triggered cooldown was disabled for the nine-epoch
120
+ continuation; its total budget included warmup and cooldown.
121
+
122
+ At 448px, drop-path was 0.15 and ordinary dropout was zero. Augmentation used
123
+ brightness/contrast/saturation jitter of 0.22/0.15/0.06, each at probability 0.5;
124
+ 2–5-degree rotation at probability 0.30; Gaussian blur sigma 0.1–1.0 at probability
125
+ 0.15; and horizontal flipping at probability 0.5. Images carrying any of
126
+ `gen:left-handed`, `gen:left-to-right_manga`, `gen:right-over-left_kimono` or
127
+ `gen:right-to-left_comic` were excluded from horizontal flips. Random erasing was
128
+ disabled.
129
+
130
+ ## Dataset and tag updates
131
+
132
+ The frozen prepared corpus contains **6,269,823 image/metadata pairs**, split into
133
+ **6,239,823 TRAIN** and **30,000 EVAL** images. The 448px continuation retained the
134
+ same split, labels and vocabulary as X1.
135
+
136
+ | Source | Pairs |
137
+ |---|---:|
138
+ | Danbooru | 6,142,955 |
139
+ | Real-life photos without people | 123,335 |
140
+ | Requested images and tags | 3,533 |
141
+
142
+ **Dataset-maintainer provenance:** the author reports that the Danbooru metadata
143
+ was synchronized in **August 2026**, deprecated tags were removed, and renamed
144
+ tags were updated to their correct canonical names. This describes the metadata
145
+ preparation, not an assertion that every image was uploaded in August. These
146
+ cleanup statements are supplied by the dataset maintainer; the release checks
147
+ verify the frozen artifacts and model alignment.
148
+
149
+ General tags have a **450-image minimum**, down from V1.1's **500**. Counts are
150
+ distinct images in the combined prepared corpus. Character, copyright, artist and
151
+ metadata tags use a 550-image minimum; the four rating tags are mandatory.
152
+ Despite the lower general-tag floor, the cleaned vocabulary has **14,994 tags**
153
+ versus **19,292 in V1.1**. The overall reduction accompanies deprecated-tag removal,
154
+ canonical-name updates and the changed vocabulary policy; it is not a count of
155
+ deprecated tags alone.
156
+
157
+ | Category | Tags |
158
+ |---|---:|
159
+ | General | 9,920 |
160
+ | Character | 3,069 |
161
+ | Copyright | 1,139 |
162
+ | Artist | 745 |
163
+ | Metadata | 117 |
164
+ | Rating | 4 |
165
+ | **Total real tags** | **14,994** |
166
+
167
+ The model has **14,996 output positions**, including PAD at index 0 and UNK at
168
+ index 1. Keep the supplied vocabulary ordering: V1.1 indices and tag counts are
169
+ not compatible with this model.
170
+
171
+ ## Evaluation against V1.1
172
+
173
+ Both models were evaluated at **448px in FP32 ONNX**, on the same frozen **30,000
174
+ EVAL images** and recorded labels, with identical preprocessing and TF32 disabled.
175
+ All source-sidecar hashes were verified. One-to-one shared names yielded **13,569
176
+ tags**, with **13,394 supported tags** contributing to macro AP. V1.1 results were
177
+ reused from a verified archived evaluation of the same images and support.
178
+
179
+ | Shared-tag measurement | experimental-X1.1 | V1.1 |
180
+ |---|---:|---:|
181
+ | Exact mAP | 58.7513% | 59.2716% |
182
+ | 200-bin mAP | 58.5193% | 58.8611% |
183
+ | Best grid micro-F1 | 68.6410% | 66.5864% |
184
+ | Threshold for that micro-F1 | 0.708543 | 0.798995 |
185
+ | General-tag exact mAP, 8,676 supported shared tags | 43.7549% | 43.9700% |
186
+
187
+ X1.1's overall shared-tag exact mAP is **0.5203 percentage points lower**; its
188
+ micro-F1 is **2.0546 points higher** at independently selected grid thresholds.
189
+ Across supported shared tags, X1.1 has higher AP on 5,425, V1.1 on 6,271, and 1,698
190
+ tie. For general tags with at least 50 EVAL positives, X1.1's exact mAP is 54.893%
191
+ versus 53.134%; V1.1 retains an advantage among the sparsest general tags.
192
+
193
+ These are **development-set measurements**, not an independent clean-label test.
194
+ This EVAL set has informed training decisions and threshold selection. V1.1's
195
+ training overlap with these images is unverified. Recorded labels may be missing
196
+ or incorrect, and aggregate gains do not establish a gain for every tag.
197
+ See [comparison details](comparison_v1_1.json) and
198
+ [shared-tag AP results](comparison_v1_1_per_tag.csv).
199
+
200
+ For the complete X1.1 supported vocabulary, the FP32 export's exact mAP was
201
+ **57.7464%** over **14,767 tags**. The final BF16 training validation recorded exact
202
+ mAP **57.7166%**, binned mAP **57.5063%**, and micro-F1 **66.2121%** at the inherited
203
+ training diagnostic threshold 0.7927. These differ in numerical precision and/or
204
+ tag scope from the shared-tag table; they should not be substituted for one
205
+ another.
206
+ The archived full FP32 measurement is in [validation.json](validation.json);
207
+ [training_metadata.json](training_metadata.json) records the training provenance.
208
+
209
+ ## Architecture and preprocessing
210
+
211
+ The **147,706,932-parameter** ViT uses patch size 16, width 896, 18 transformer
212
+ layers, 14 attention heads, MLP width 2,320, QK normalization and LayerScale
213
+ initialization 0.1. Its positional grid is 28×28 at 448px.
214
+
215
+ | ONNX input/output | Type | Shape |
216
+ |---|---|---|
217
+ | `pixel_values` | float32 | `[batch, 3, 448, 448]` |
218
+ | `padding_mask` | bool | `[batch, 448, 448]` |
219
+ | `probabilities` | float32 | `[batch, 14996]` |
220
+
221
+ Batch size is dynamic; spatial dimensions are fixed. Apply EXIF orientation,
222
+ convert to RGB, composite transparency onto `[114,114,114]`, and downscale with
223
+ LANCZOS while preserving aspect ratio, **without upscaling**. Center the result on
224
+ a 448×448 canvas with the same pad color. Padding-mask values are **True for
225
+ padding**. Divide pixels by 255, normalize using mean and standard deviation
226
+ `[0.5,0.5,0.5]`, and arrange as NCHW. The supplied script implements this contract.
227
+
228
+ The ONNX output already includes sigmoid. The safetensors inference helper
229
+ applies sigmoid to the PyTorch logits. Four rating scores are independent sigmoid
230
+ outputs, not a softmax. A frontend must support the two inputs and this
231
+ preprocessing; a raw-image, single-input tagger interface needs an adapter.
232
+
233
+ ## Files, integrity and download statistics
234
+
235
+ | File | Purpose |
236
+ |---|---|
237
+ | `model.onnx` | FP32 inference graph, about 591 MB, with embedded vocabulary/preprocessing metadata |
238
+ | `model.safetensors` | FP32 tensor-only weights, about 591 MB |
239
+ | `config.json` | Real architecture and release configuration consumed by the supplied loader |
240
+ | `preprocessing.json` | Image transform and input contract |
241
+ | `vocabulary.json`, `selected_tags.csv` | Exact output ordering and tag categories |
242
+ | `infer.py`, `download.py` | Standalone inference and selective release download helpers |
243
+ | `requirements.txt`, `requirements-torch.txt` | Dependencies for CPU ONNX and PyTorch/safetensors inference |
244
+ | `training_metadata.json`, `validation.json` | Training provenance and archived full FP32 evaluation |
245
+ | `inference_checks.json` | Release-package checks using a bounded set of fixtures; not another full evaluation |
246
+ | `release_manifest.json`, `SHA256SUMS` | Provenance and file-integrity records |
247
+
248
+ The normal repository download includes `config.json`, which the supplied loader
249
+ actually uses. [Hugging Face download statistics](https://huggingface.co/docs/hub/models-download-stats)
250
+ count requests to supported query files such as `config.json`. The public counter
251
+ is not a unique-user count; direct downloads of weights alone may not be reflected.
252
+ There are no extra telemetry calls or artificial counting requests in the
253
+ inference helper.
254
+
255
+ The selected source checkpoint SHA256 is:
256
+
257
+ ```text
258
+ d400c2e460e7287357f86083f84c187790a45b1d47985e908c52825373f888c3
259
+ ```
260
+
261
+ This identifies the preserved local training source. The release manifest and
262
+ checksums identify the distributed files; optimizer state and the pickled training
263
+ checkpoint are not included. The release ONNX has refreshed descriptive metadata;
264
+ its inference graph and learned weights are unchanged from the evaluated artifact.
265
+ The manifest records both artifact identities.
266
+
267
+ ## Scope and license
268
+
269
+ X1.1 is intended for experimental tagging, comparison and feedback within the
270
+ license terms. Training annotations are noisy and incomplete; rare concepts and
271
+ visually similar tags can be unreliable. Review suggestions before applying bulk
272
+ dataset edits. The vocabulary includes sexual-content labels and independent
273
+ rating tags; this model has not been validated as a safety classifier.
274
+
275
+ The release retains the **[Educational Use Only License](LICENSE)**. It permits
276
+ the educational uses described in that file and prohibits commercial use. Read
277
+ the full license for attribution, redistribution and other conditions.
SHA256SUMS ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 5a4c917343b3f76df936dbd7f23ad4693aea7f96ad4c1029cdfd70ac3c78cb9b .gitattributes
2
+ 322447c97c7fc1b266286efd31b7a9aefd21a111fdbde60090ebfa0601cca497 LICENSE
3
+ 4023de4f067e0cf475dfc80d0d038477bb1d4bbea4c5edf9f23d24913e575206 README.md
4
+ dfc682758528177f0d96a0a1230add733f537dd40932239095893c6c585eef5e checkpoint_evidence.json
5
+ 565667f7bd86c3b61c4fda46cec38e8838ad0719401b991513fedaba9961e3f8 comparison_groups.csv
6
+ ad522ba09cd16227f88215a62c4f71f64f0af9fe930ea39f93a0c406f6d9a279 comparison_v1_1.json
7
+ bf6ce722e843f4246d42fa86bb17a665eb0de514a378bd7aed9ba36b1f9c47e3 comparison_v1_1_per_tag.csv
8
+ 39e218e1674f5fb4e00023aa4bcea778c09704d856f9f833ef01bb251d1415fd config.json
9
+ c83f832a32b6a8b8802dd9207f2f9be2ebef2a6d169d6daf1cdb2530cfe287ab custom_drop_path.py
10
+ ab4c1c04c03f5cefe3153bede72878a0d2df9e8378724922ed1571338b38d3c6 download.py
11
+ 23f6dd10da65f031e5aae31fe88aefa35c6aa34e33d31986d45c9f9d57c705b8 frozen_evaluation_contract.json
12
+ 79b15ad63de33354bc0a39176cd9eddd915361bd48b6c27b885009aac77185ea infer.py
13
+ f828ef9205af946e5633e416044fd84d6568292bfc1e02cb0852f484368426da inference_checks.json
14
+ 373cc75ed9975e58864e4ed73a34316a696b6091b83971207677a6dae924fcd2 mask_utils.py
15
+ 49ba0f66f83ba88a101b6edac54ec39f0a12eab66743e13ae42efc3570745bf7 model.onnx
16
+ ec07d0034f48ba3ceff8624993baf3ea30fa3a2e3cc8908682eb3e4f84eda766 model.safetensors
17
+ bce3773157a4c4dfc153c633d489c1e7c18f544b7fac6f51bd318ebe0fb7c6aa model_architecture.py
18
+ 1c9af089a8c150b5270bc933fe4053a913f284a4242b53cdb94d108c529b2ef9 preprocessing.json
19
+ b058043b832bbb02c4139d3766dc8f1aac1fb781f90c897cdcf9230694cc2126 release_manifest.json
20
+ d604ef2e5271edac5d5b4bba2eeccf11102e69dd82667f40ca0be417b806ea62 requirements-gpu.txt
21
+ 9d4beb51ff7c74d2ff68981496e6c1b37f1ab9cc85a53e692cb22c345c1ee3d7 requirements-torch.txt
22
+ abfcf5a28c6c88fa86822c1d7bcbf7afca92a893b8a6a4a65d3267a8786c3b28 requirements.txt
23
+ ec2b028ae5c8355bc7ccbab51ef967ef4bec940b686fe3e564e0177a865c9799 selected_tags.csv
24
+ 3b20a011b5acf7732c1546dab99d6a56f48730eb54cadf30434f1dba62053436 thresholds.json
25
+ 1a9ba147bc89a2eef54f2e80ae406f8652332fb91c9f2e3cf7fb05556388ad44 training_buckets.csv
26
+ 54ef172f7c21c8cb414bef220e6ca45a33871134c99a4b3a1532c425f8ebc989 training_epochs.csv
27
+ 6d4af05a7ee8d54fd09aae488b0fd470a39c2cb16430e0f31597a3b9a867800a training_metadata.json
28
+ fd2475144e7583842f4c733ad77d770749964b147a41e732a58c868516fc4e6f validation.json
29
+ e3e6ccc137d9152d8a17fbd19e0c1ae595b2317a6c55893fd25779620b4b8087 vocabulary.json
checkpoint_evidence.json ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "checkpoint": "checkpoint_epoch_9_step_55719.pt",
3
+ "checkpoint_sha256": "d400c2e460e7287357f86083f84c187790a45b1d47985e908c52825373f888c3",
4
+ "epoch": 9,
5
+ "step": 55719,
6
+ "scheduler": {
7
+ "warmup_steps": 6191,
8
+ "total_steps": 55719,
9
+ "cooldown_fraction": 0.15,
10
+ "budget_cooldown_start": 48451,
11
+ "min_lr": 0.0,
12
+ "base_max_lr": 2.9764702249476646e-05,
13
+ "cooldown_start": 48451,
14
+ "cooldown_steps": 7268,
15
+ "cooldown_reason": "budget",
16
+ "cooldown_best_metric": 0.5750626921653748,
17
+ "base_lrs": [
18
+ 2.9764702249476646e-05,
19
+ 2.9764702249476646e-05
20
+ ],
21
+ "last_epoch": 55719,
22
+ "_step_count": 55720,
23
+ "_is_initial": false,
24
+ "_get_lr_called_within_step": false,
25
+ "_last_lr": [
26
+ 0.0,
27
+ 0.0
28
+ ]
29
+ },
30
+ "preprocessing": {
31
+ "normalize_mean": [
32
+ 0.5,
33
+ 0.5,
34
+ 0.5
35
+ ],
36
+ "normalize_std": [
37
+ 0.5,
38
+ 0.5,
39
+ 0.5
40
+ ],
41
+ "image_size": 448,
42
+ "patch_size": 16,
43
+ "color_order": "RGB"
44
+ }
45
+ }
comparison_groups.csv ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ group,tags,x1_ap,v11_ap,delta_points,x1_wins,v11_wins,ties
2
+ all shared supported,13394,58.75134140920024,59.27161263163755,-0.5202712200966514,5425,6271,1698
3
+ shared with >=50 EVAL positives,2028,57.690053587344046,55.98307259190696,1.7069809948860188,1161,867,0
4
+ category gen,8676,43.75492858232571,43.96996116217886,-0.21503257453254906,4069,4420,187
5
+ category char,2870,91.87811929600326,91.97710045881874,-0.0989811631562091,815,965,1090
6
+ category copyright,1050,78.31961239122653,79.32927887092089,-1.0096664829623132,378,524,148
7
+ category artist,695,83.73359510134034,89.3767281228395,-5.643133035251241,109,314,272
8
+ category meta,99,28.44267269873385,27.431288556721427,1.0113841334870555,52,46,1
9
+ category rating,4,90.30946791172028,74.51295033097267,15.796517580747604,2,2,0
10
+ frequency 300-499,63,45.1914965982608,51.18916831404295,-5.997671779348618,14,36,13
11
+ frequency 500-999,3962,63.82945302063262,66.1829312171874,-2.3534781905738833,1093,1742,1127
12
+ frequency 1000-4999,6049,57.39406408068191,57.68059412367721,-0.286530040883122,2509,2986,554
13
+ frequency 5000-9999,1249,52.204597668254,51.39728918997086,0.8073084776866152,638,607,4
14
+ frequency 10000-plus,2071,57.361599985368,55.69153087854903,1.6700691066390936,1171,900,0
comparison_v1_1.json ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "x1": {
3
+ "model": "X1 448 final epoch 9",
4
+ "checkpoint": {
5
+ "checkpoint": "checkpoint_epoch_9_step_55719.pt",
6
+ "checkpoint_sha256": "d400c2e460e7287357f86083f84c187790a45b1d47985e908c52825373f888c3",
7
+ "epoch": 9,
8
+ "step": 55719,
9
+ "scheduler": {
10
+ "warmup_steps": 6191,
11
+ "total_steps": 55719,
12
+ "cooldown_fraction": 0.15,
13
+ "budget_cooldown_start": 48451,
14
+ "min_lr": 0.0,
15
+ "base_max_lr": 2.9764702249476646e-05,
16
+ "cooldown_start": 48451,
17
+ "cooldown_steps": 7268,
18
+ "cooldown_reason": "budget",
19
+ "cooldown_best_metric": 0.5750626921653748,
20
+ "base_lrs": [
21
+ 2.9764702249476646e-05,
22
+ 2.9764702249476646e-05
23
+ ],
24
+ "last_epoch": 55719,
25
+ "_step_count": 55720,
26
+ "_is_initial": false,
27
+ "_get_lr_called_within_step": false,
28
+ "_last_lr": [
29
+ 0.0,
30
+ 0.0
31
+ ]
32
+ },
33
+ "preprocessing": {
34
+ "normalize_mean": [
35
+ 0.5,
36
+ 0.5,
37
+ 0.5
38
+ ],
39
+ "normalize_std": [
40
+ 0.5,
41
+ 0.5,
42
+ 0.5
43
+ ],
44
+ "image_size": 448,
45
+ "patch_size": 16,
46
+ "color_order": "RGB"
47
+ }
48
+ },
49
+ "model_sha256": "c5fcab0fb53e07aee437f3ef29e19a1e60b3dc18380048fa6d99baf35fa6c8f3",
50
+ "native_image_size": 448,
51
+ "samples": 30000,
52
+ "sample_sha256": "20a60e7a747cdf9a698a464c9d324612aad2ca6b43a541db0813554eb9af8694",
53
+ "shared_tags": 13569,
54
+ "supported_shared_tags": 13394,
55
+ "binned_mAP": 0.5851927682386426,
56
+ "exact_mAP": 0.5875134140920024,
57
+ "best_grid_micro_f1": 0.6864101262842809,
58
+ "best_grid_threshold": 0.7085427045822144,
59
+ "precision_at_best_grid": 0.7065020665350971,
60
+ "recall_at_best_grid": 0.6674293596992867,
61
+ "all_supported_tags": 14767,
62
+ "all_tag_exact_mAP": 0.5774638913821051,
63
+ "all_tag_binned_mAP": 0.5750950549967043,
64
+ "inference_seconds": 347.91008079855237,
65
+ "total_seconds": 487.7315649986267,
66
+ "preprocessing": "FP32 RGB, native resolution, downscale-only centered LANCZOS letterbox, mean/std 0.5, pad 114, True=padding",
67
+ "verified_sidecars": 30000
68
+ },
69
+ "v11": {
70
+ "model": "V1.1",
71
+ "model_sha256": "8567852deb135eccfe4b8445d48e4476ee8846436486679adc0642cfeda07d13",
72
+ "native_image_size": 448,
73
+ "samples": 30000,
74
+ "sample_sha256": "20a60e7a747cdf9a698a464c9d324612aad2ca6b43a541db0813554eb9af8694",
75
+ "shared_tags": 13569,
76
+ "supported_shared_tags": 13394,
77
+ "binned_mAP": 0.5886105397423468,
78
+ "exact_mAP": 0.5927161263163755,
79
+ "best_grid_micro_f1": 0.6658638534826367,
80
+ "best_grid_threshold": 0.7989949584007263,
81
+ "precision_at_best_grid": 0.6852139862873703,
82
+ "recall_at_best_grid": 0.6475765838848551,
83
+ "inference_seconds": 416.6925043994561,
84
+ "total_seconds": 522.6292808055878,
85
+ "preprocessing": "FP32 RGB, native resolution, downscale-only centered LANCZOS letterbox, mean/std 0.5, pad 114, True=padding"
86
+ },
87
+ "shared_tag_results": [
88
+ {
89
+ "group": "all shared supported",
90
+ "tags": 13394,
91
+ "x1_ap": 58.75134140920024,
92
+ "v11_ap": 59.27161263163755,
93
+ "delta_points": -0.5202712200966514,
94
+ "x1_wins": 5425,
95
+ "v11_wins": 6271,
96
+ "ties": 1698
97
+ },
98
+ {
99
+ "group": "shared with >=50 EVAL positives",
100
+ "tags": 2028,
101
+ "x1_ap": 57.690053587344046,
102
+ "v11_ap": 55.98307259190696,
103
+ "delta_points": 1.7069809948860188,
104
+ "x1_wins": 1161,
105
+ "v11_wins": 867,
106
+ "ties": 0
107
+ },
108
+ {
109
+ "group": "category gen",
110
+ "tags": 8676,
111
+ "x1_ap": 43.75492858232571,
112
+ "v11_ap": 43.96996116217886,
113
+ "delta_points": -0.21503257453254906,
114
+ "x1_wins": 4069,
115
+ "v11_wins": 4420,
116
+ "ties": 187
117
+ },
118
+ {
119
+ "group": "category char",
120
+ "tags": 2870,
121
+ "x1_ap": 91.87811929600326,
122
+ "v11_ap": 91.97710045881874,
123
+ "delta_points": -0.0989811631562091,
124
+ "x1_wins": 815,
125
+ "v11_wins": 965,
126
+ "ties": 1090
127
+ },
128
+ {
129
+ "group": "category copyright",
130
+ "tags": 1050,
131
+ "x1_ap": 78.31961239122653,
132
+ "v11_ap": 79.32927887092089,
133
+ "delta_points": -1.0096664829623132,
134
+ "x1_wins": 378,
135
+ "v11_wins": 524,
136
+ "ties": 148
137
+ },
138
+ {
139
+ "group": "category artist",
140
+ "tags": 695,
141
+ "x1_ap": 83.73359510134034,
142
+ "v11_ap": 89.3767281228395,
143
+ "delta_points": -5.643133035251241,
144
+ "x1_wins": 109,
145
+ "v11_wins": 314,
146
+ "ties": 272
147
+ },
148
+ {
149
+ "group": "category meta",
150
+ "tags": 99,
151
+ "x1_ap": 28.44267269873385,
152
+ "v11_ap": 27.431288556721427,
153
+ "delta_points": 1.0113841334870555,
154
+ "x1_wins": 52,
155
+ "v11_wins": 46,
156
+ "ties": 1
157
+ },
158
+ {
159
+ "group": "category rating",
160
+ "tags": 4,
161
+ "x1_ap": 90.30946791172028,
162
+ "v11_ap": 74.51295033097267,
163
+ "delta_points": 15.796517580747604,
164
+ "x1_wins": 2,
165
+ "v11_wins": 2,
166
+ "ties": 0
167
+ },
168
+ {
169
+ "group": "frequency 300-499",
170
+ "tags": 63,
171
+ "x1_ap": 45.1914965982608,
172
+ "v11_ap": 51.18916831404295,
173
+ "delta_points": -5.997671779348618,
174
+ "x1_wins": 14,
175
+ "v11_wins": 36,
176
+ "ties": 13
177
+ },
178
+ {
179
+ "group": "frequency 500-999",
180
+ "tags": 3962,
181
+ "x1_ap": 63.82945302063262,
182
+ "v11_ap": 66.1829312171874,
183
+ "delta_points": -2.3534781905738833,
184
+ "x1_wins": 1093,
185
+ "v11_wins": 1742,
186
+ "ties": 1127
187
+ },
188
+ {
189
+ "group": "frequency 1000-4999",
190
+ "tags": 6049,
191
+ "x1_ap": 57.39406408068191,
192
+ "v11_ap": 57.68059412367721,
193
+ "delta_points": -0.286530040883122,
194
+ "x1_wins": 2509,
195
+ "v11_wins": 2986,
196
+ "ties": 554
197
+ },
198
+ {
199
+ "group": "frequency 5000-9999",
200
+ "tags": 1249,
201
+ "x1_ap": 52.204597668254,
202
+ "v11_ap": 51.39728918997086,
203
+ "delta_points": 0.8073084776866152,
204
+ "x1_wins": 638,
205
+ "v11_wins": 607,
206
+ "ties": 4
207
+ },
208
+ {
209
+ "group": "frequency 10000-plus",
210
+ "tags": 2071,
211
+ "x1_ap": 57.361599985368,
212
+ "v11_ap": 55.69153087854903,
213
+ "delta_points": 1.6700691066390936,
214
+ "x1_wins": 1171,
215
+ "v11_wins": 900,
216
+ "ties": 0
217
+ }
218
+ ],
219
+ "exact_map_delta_points": -0.5202712224373074,
220
+ "micro_f1_delta_points": 2.0546272801644228,
221
+ "comparability": "Same frozen 30,000 EVAL identities and labels; every source sidecar hash verified; identical one-to-one shared tags and support; both 448 FP32 ONNX CUDA with TF32 disabled and identical preprocessing. V1.1 metrics reused from verified archived evaluation. Development-set comparison; V1.1 training overlap is unverified. F1 thresholds independently optimized on the same 200-point grid; not deployment calibration."
222
+ }
comparison_v1_1_per_tag.csv ADDED
The diff for this file is too large to render. See raw diff
 
config.json ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "oppaioracle_vit",
3
+ "architectures": [
4
+ "SimplifiedTagger"
5
+ ],
6
+ "release": "experimental-X1.1",
7
+ "repo_id": "Grio43/OppaiOracle-experimental-X1.1",
8
+ "image_size": 448,
9
+ "num_labels": 14996,
10
+ "parameter_count": 147706932,
11
+ "torch_dtype": "float32",
12
+ "architecture": {
13
+ "image_size": 448,
14
+ "patch_size": 16,
15
+ "num_channels": 3,
16
+ "hidden_size": 896,
17
+ "num_hidden_layers": 18,
18
+ "num_attention_heads": 14,
19
+ "intermediate_size": 2320,
20
+ "num_tags": 14996,
21
+ "dropout": 0.0,
22
+ "pos_dropout": 0.0,
23
+ "attention_dropout": 0.0,
24
+ "qk_norm": true,
25
+ "layer_scale_init": 0.1,
26
+ "layer_norm_eps": 1e-06,
27
+ "use_flex_attention": false,
28
+ "flex_block_size": 128,
29
+ "attention_bias": true,
30
+ "token_ignore_threshold": 0.9,
31
+ "gradient_checkpointing": false,
32
+ "checkpoint_every_n_layers": 1,
33
+ "drop_path_rate": 0.15,
34
+ "check_numerical_stability": false,
35
+ "logit_clamp_value": 15.0,
36
+ "use_fp32_layernorm": false
37
+ },
38
+ "preprocessing": {
39
+ "normalize_mean": [
40
+ 0.5,
41
+ 0.5,
42
+ 0.5
43
+ ],
44
+ "normalize_std": [
45
+ 0.5,
46
+ 0.5,
47
+ 0.5
48
+ ],
49
+ "image_size": 448,
50
+ "patch_size": 16,
51
+ "color_order": "RGB",
52
+ "pad_color": [
53
+ 114,
54
+ 114,
55
+ 114
56
+ ],
57
+ "resize_filter": "LANCZOS",
58
+ "resize_mode": "letterbox_downscale_only",
59
+ "upscale": false,
60
+ "exif_transpose": true,
61
+ "alpha_composite": true,
62
+ "input_layout": "NCHW",
63
+ "padding_mask_true_means": "padding",
64
+ "output_activation": "sigmoid",
65
+ "skip_indices": [
66
+ 0,
67
+ 1
68
+ ]
69
+ },
70
+ "vocab_file": "vocabulary.json",
71
+ "vocab_sha256": "e3e6ccc137d9152d8a17fbd19e0c1ae595b2317a6c55893fd25779620b4b8087",
72
+ "model_file_onnx": "model.onnx",
73
+ "model_file_safetensors": "model.safetensors",
74
+ "threshold": 0.7085427045822144,
75
+ "threshold_calibrated": false,
76
+ "threshold_source": "200-point micro-F1 grid on 13,569 shared development tags; not final deployment calibration"
77
+ }
custom_drop_path.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+ class SafeDropPath(nn.Module):
5
+ def __init__(self, drop_prob: float = 0.0):
6
+ super().__init__()
7
+ drop_prob = float(drop_prob)
8
+ if not (0.0 <= drop_prob < 1.0):
9
+ raise ValueError(f"drop_prob must be in [0, 1), got {drop_prob}")
10
+ self.drop_prob = drop_prob
11
+
12
+ def forward(self, x):
13
+ if not self.training or self.drop_prob == 0.0:
14
+ return x
15
+ keep_prob = 1.0 - self.drop_prob
16
+ # keep_prob is always > 0 due to __init__ validation
17
+ shape = (x.shape[0],) + (1,) * (x.ndim - 1)
18
+ # Draw in float32: drawing in x.dtype (e.g. bf16) quantizes keep_prob
19
+ # and skews the realized keep rate. The 0/1 mask casts back exactly.
20
+ random_tensor = keep_prob + torch.rand(shape, dtype=torch.float32, device=x.device)
21
+ random_tensor.floor_()
22
+ return x.div(keep_prob) * random_tensor.to(x.dtype)
download.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Download an immutable OppaiOracle release snapshot using Hugging Face Hub."""
3
+ import argparse
4
+ import json
5
+ from pathlib import Path, PurePosixPath
6
+
7
+
8
+ DEFAULT_REPO_ID = 'Grio43/OppaiOracle-experimental-X1.1'
9
+ SUPPORT_FILES = [
10
+ 'README.md', 'LICENSE', 'config.json', 'selected_tags.csv', 'thresholds.json',
11
+ 'infer.py', 'download.py', 'model_architecture.py', 'custom_drop_path.py',
12
+ 'mask_utils.py', 'requirements*.txt', 'release_manifest.json',
13
+ 'preprocessing.json', 'training_metadata.json', 'validation.json',
14
+ 'comparison_v1_1.json', 'comparison_v1_1_per_tag.csv', 'comparison_groups.csv',
15
+ 'checkpoint_evidence.json', 'frozen_evaluation_contract.json',
16
+ 'training_epochs.csv', 'training_buckets.csv', 'inference_checks.json', 'SHA256SUMS',
17
+ ]
18
+
19
+
20
+ def release_filename(name):
21
+ """Require ordinary relative Hub filenames, without glob expansion."""
22
+ path = PurePosixPath(name)
23
+ if (not name or path.is_absolute() or '..' in path.parts
24
+ or any(char in name for char in ('\\', ':', '*', '?', '[', ']'))):
25
+ raise ValueError(f'Invalid release filename: {name!r}')
26
+ return name
27
+
28
+
29
+ def download_release(destination, repo_id=DEFAULT_REPO_ID, model_format='onnx', revision='main'):
30
+ from huggingface_hub import HfApi, hf_hub_download, snapshot_download
31
+
32
+ # Resolve once, so config and model files cannot straddle a moving main.
33
+ commit = HfApi().model_info(repo_id=repo_id, revision=revision).sha
34
+ if not commit:
35
+ raise RuntimeError('Hugging Face did not return an immutable commit SHA')
36
+ destination = Path(destination)
37
+ config_path = hf_hub_download(repo_id=repo_id, filename='config.json',
38
+ revision=commit, local_dir=destination)
39
+ config = json.loads(Path(config_path).read_text(encoding='utf-8-sig'))
40
+ formats = ('onnx', 'safetensors') if model_format == 'all' else (model_format,)
41
+ models = [release_filename(config[f'model_file_{kind}']) for kind in formats]
42
+ vocab = release_filename(config.get('vocab_file', 'vocabulary.json'))
43
+ patterns = SUPPORT_FILES + [vocab] + models
44
+ snapshot_download(repo_id=repo_id, revision=commit, local_dir=destination,
45
+ allow_patterns=patterns)
46
+ record = {'repo_id': repo_id, 'commit': commit, 'format': model_format,
47
+ 'model_files': models}
48
+ (destination / 'download_info.json').write_text(
49
+ json.dumps(record, indent=2) + '\n', encoding='utf-8')
50
+ return record
51
+
52
+
53
+ def main():
54
+ parser = argparse.ArgumentParser(description=__doc__)
55
+ parser.add_argument('destination', nargs='?', type=Path,
56
+ default=Path('OppaiOracle-experimental-X1.1'))
57
+ parser.add_argument('--repo-id', default=DEFAULT_REPO_ID)
58
+ parser.add_argument('--revision', default='main', help='Branch, tag, or commit; resolved to a pinned commit')
59
+ parser.add_argument('--format', choices=('onnx', 'safetensors', 'all'), default='onnx')
60
+ args = parser.parse_args()
61
+ record = download_release(args.destination, args.repo_id, args.format, args.revision)
62
+ print(json.dumps({'directory': str(args.destination.resolve()), **record}, indent=2))
63
+
64
+
65
+ if __name__ == '__main__':
66
+ main()
frozen_evaluation_contract.json ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "manifest": "./logs/v2_phase1/dataset_manifest.json",
3
+ "frozen_ignore_hash": "2194183cb84a03b1701e2aa1833176633223e906055a8e6d031ba13a64c1b021",
4
+ "current_ignore_hash": "b8fa4041e90e598c8ffcde983500b849606e93d349d5810bac1762f4a812bd60",
5
+ "frozen_vocab_file_sha256": "a168f4cb343e75fdbacea13de0c3ec2d970829ee0df68a193b4b18cea6a1ed81",
6
+ "newly_ignored_existing_tags": [
7
+ "meta:alt_text",
8
+ "meta:alternate_language",
9
+ "meta:annotated",
10
+ "meta:archived_source",
11
+ "meta:bad_bcy_id",
12
+ "meta:bad_blogspot_link",
13
+ "meta:bad_discord_link",
14
+ "meta:bad_facebook_link",
15
+ "meta:bad_fc2_link",
16
+ "meta:bad_hentai_foundry_id",
17
+ "meta:bad_imgur_id",
18
+ "meta:bad_instagram_id",
19
+ "meta:bad_lofter_id",
20
+ "meta:bad_nico_seiga_link",
21
+ "meta:bad_nijie_id",
22
+ "meta:bad_poipiku_id",
23
+ "meta:bad_source",
24
+ "meta:bad_tumblr_link",
25
+ "meta:bad_twitter_link",
26
+ "meta:bad_weibo_id",
27
+ "meta:check_artist",
28
+ "meta:check_character",
29
+ "meta:cropped",
30
+ "meta:derivative_work",
31
+ "meta:detexted",
32
+ "meta:fancy_embedded_notes",
33
+ "meta:hard-translated",
34
+ "meta:has_bad_revision",
35
+ "meta:has_censored_revision",
36
+ "meta:has_downscaled_revision",
37
+ "meta:imageboard_desourced",
38
+ "meta:inactive_account",
39
+ "meta:large_variant_set",
40
+ "meta:pixel-perfect_duplicate",
41
+ "meta:protected_link",
42
+ "meta:redrawn",
43
+ "meta:second-party_source",
44
+ "meta:source_larger",
45
+ "meta:source_mismatch",
46
+ "meta:source_smaller",
47
+ "meta:third-party_source",
48
+ "meta:variant_set"
49
+ ],
50
+ "verified_sidecars": 30000,
51
+ "method": "Direct frozen-EVAL sidecars and original prepared vocabulary. Original ignored tags were excluded when this vocabulary was built; all original output labels remain evaluated. No preparation or training startup is performed."
52
+ }
infer.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Offline ONNX or safetensors inference for OppaiOracle experimental-X1.1."""
3
+ import argparse
4
+ import base64
5
+ import gzip
6
+ import hashlib
7
+ import json
8
+ from pathlib import Path
9
+
10
+ import numpy as np
11
+ from PIL import Image, ImageOps
12
+
13
+
14
+ def read_json(path):
15
+ return json.loads(Path(path).read_text(encoding='utf-8-sig'))
16
+
17
+
18
+ def local_file(directory, name):
19
+ """Resolve a release-relative file without escaping the package directory."""
20
+ root = Path(directory).resolve()
21
+ path = (root / name).resolve()
22
+ if not path.is_relative_to(root):
23
+ raise ValueError(f'Release file must be inside {root}: {name}')
24
+ return path
25
+
26
+
27
+ def vocabulary_sha256(vocabulary):
28
+ canonical = json.dumps(vocabulary, ensure_ascii=False, sort_keys=True,
29
+ separators=(',', ':')).encode('utf-8')
30
+ return hashlib.sha256(canonical).hexdigest()
31
+
32
+
33
+ def load_config(config_path):
34
+ config = read_json(config_path)
35
+ pre = config['preprocessing']
36
+ if (int(pre['image_size']) != 448 or pre.get('color_order', 'RGB') != 'RGB'
37
+ or list(pre['pad_color']) != [114, 114, 114]
38
+ or list(pre['normalize_mean']) != [0.5, 0.5, 0.5]
39
+ or list(pre['normalize_std']) != [0.5, 0.5, 0.5]
40
+ or pre.get('upscale', False)):
41
+ raise ValueError('Expected the X1.1 448px RGB/downscale-only preprocessing contract')
42
+ if pre.get('resize_filter', 'LANCZOS').upper() != 'LANCZOS':
43
+ raise ValueError('X1.1 requires LANCZOS resampling')
44
+ if int(config['architecture']['image_size']) != int(pre['image_size']):
45
+ raise ValueError('Architecture and preprocessing image sizes differ')
46
+ directory = Path(config_path).resolve().parent
47
+ vocab = read_json(local_file(directory, config.get('vocab_file', 'vocabulary.json')))
48
+ digest = vocabulary_sha256(vocab)
49
+ if digest != config['vocab_sha256']:
50
+ raise ValueError('Vocabulary checksum does not match config.json')
51
+ names = vocab['index_to_tag']
52
+ num_tags = int(config['architecture']['num_tags'])
53
+ if set(names) != {str(i) for i in range(num_tags)}:
54
+ raise ValueError('Vocabulary indices must cover every model output exactly')
55
+ if names['0'] != '<PAD>' or names['1'] != '<UNK>':
56
+ raise ValueError('Expected PAD=0 and UNK=1')
57
+ if vocab['tag_to_index'] != {tag: int(index) for index, tag in names.items()}:
58
+ raise ValueError('Vocabulary index maps disagree')
59
+ return config, vocab
60
+
61
+
62
+ def preprocess(path, metadata):
63
+ """EXIF transpose, alpha composite, downscale-only LANCZOS, centered pad."""
64
+ size = int(metadata['image_size'])
65
+ pad = tuple(metadata['pad_color'])
66
+ with Image.open(path) as source:
67
+ img = ImageOps.exif_transpose(source)
68
+ if img.mode in ('RGBA', 'LA') or 'transparency' in img.info:
69
+ rgba = img.convert('RGBA')
70
+ background = Image.new('RGB', img.size, pad)
71
+ background.paste(rgba.convert('RGB'), mask=rgba.getchannel('A'))
72
+ img = background
73
+ else:
74
+ img = img.convert('RGB')
75
+ scale = min(1.0, size / img.width, size / img.height)
76
+ width = max(1, round(img.width * scale))
77
+ height = max(1, round(img.height * scale))
78
+ resized = img.resize((width, height), Image.Resampling.LANCZOS)
79
+ canvas = Image.new('RGB', (size, size), pad)
80
+ left, top = (size - width) // 2, (size - height) // 2
81
+ canvas.paste(resized, (left, top))
82
+ array = np.asarray(canvas, dtype=np.float32)
83
+ mean = np.asarray(metadata['normalize_mean'], dtype=np.float32)
84
+ std = np.asarray(metadata['normalize_std'], dtype=np.float32)
85
+ array = (array / 255.0 - mean) / std
86
+ mask = np.ones((size, size), dtype=bool)
87
+ mask[top:top + height, left:left + width] = False
88
+ return np.ascontiguousarray(array.transpose(2, 0, 1)), mask
89
+
90
+
91
+ class ONNXRuntime:
92
+ def __init__(self, model_path, config, device):
93
+ import onnxruntime as ort
94
+
95
+ if device == 'cuda':
96
+ # Reuse CUDA DLLs from PyTorch when it is installed.
97
+ try:
98
+ import torch # noqa: F401
99
+ except ImportError:
100
+ pass
101
+ if hasattr(ort, 'preload_dlls'):
102
+ ort.preload_dlls()
103
+ if 'CUDAExecutionProvider' not in ort.get_available_providers():
104
+ raise RuntimeError('Install compatible onnxruntime-gpu or use --device cpu')
105
+ providers = [('CUDAExecutionProvider', {'use_tf32': False}), 'CPUExecutionProvider']
106
+ else:
107
+ providers = ['CPUExecutionProvider']
108
+ options = ort.SessionOptions()
109
+ options.intra_op_num_threads = 8
110
+ self.session = ort.InferenceSession(str(model_path), sess_options=options,
111
+ providers=providers)
112
+ if device == 'cuda' and 'CUDAExecutionProvider' not in self.session.get_providers():
113
+ raise RuntimeError('CUDA provider initialization failed')
114
+ metadata = self.session.get_modelmeta().custom_metadata_map
115
+ if metadata.get('preprocessing') != 'external':
116
+ raise ValueError('Expected the release ONNX model with external preprocessing')
117
+ if metadata.get('vocab_sha256') != config['vocab_sha256']:
118
+ raise ValueError('ONNX vocabulary checksum does not match config.json')
119
+ embedded = json.loads(gzip.decompress(base64.b64decode(metadata['vocab_b64_gzip'])))
120
+ if vocabulary_sha256(embedded) != config['vocab_sha256']:
121
+ raise ValueError('ONNX embedded vocabulary checksum mismatch')
122
+ for key in ('image_size', 'pad_color', 'normalize_mean', 'normalize_std'):
123
+ if json.loads(metadata[key]) != config['preprocessing'][key]:
124
+ raise ValueError(f'ONNX preprocessing differs from config.json: {key}')
125
+ if metadata.get('color_order', 'RGB') != config['preprocessing'].get('color_order', 'RGB'):
126
+ raise ValueError('ONNX color order differs from config.json')
127
+ inputs = {item.name: item.type for item in self.session.get_inputs()}
128
+ if set(inputs) != {'pixel_values', 'padding_mask'} or inputs['padding_mask'] != 'tensor(bool)':
129
+ raise ValueError('Unexpected ONNX input contract')
130
+ if inputs['pixel_values'] not in ('tensor(float)', 'tensor(float16)'):
131
+ raise ValueError('Unsupported ONNX input precision')
132
+ self.dtype = np.float16 if inputs['pixel_values'] == 'tensor(float16)' else np.float32
133
+
134
+ def predict(self, pixels, masks):
135
+ # The exported ONNX graph already includes sigmoid.
136
+ return self.session.run(['probabilities'], {
137
+ 'pixel_values': pixels.astype(self.dtype, copy=False),
138
+ 'padding_mask': masks,
139
+ })[0]
140
+
141
+
142
+ class SafetensorsRuntime:
143
+ def __init__(self, model_path, config, device):
144
+ import torch
145
+ from safetensors import safe_open
146
+ from safetensors.torch import load_file
147
+ from model_architecture import SimplifiedTagger, VisionTransformerConfig
148
+
149
+ if device == 'cuda' and not torch.cuda.is_available():
150
+ raise RuntimeError('CUDA is unavailable; use --device cpu')
151
+ torch.set_num_threads(8)
152
+ if device == 'cuda':
153
+ torch.backends.cuda.matmul.allow_tf32 = False
154
+ torch.backends.cudnn.allow_tf32 = False
155
+ architecture = dict(config['architecture'])
156
+ architecture.update(use_flex_attention=False, gradient_checkpointing=False)
157
+ self.model = SimplifiedTagger(VisionTransformerConfig(**architecture))
158
+ # This architecture selects eager SDPA through set_onnx_mode; changing
159
+ # use_flex_attention alone does not switch its forward implementation.
160
+ self.model.set_onnx_mode(True)
161
+ with safe_open(str(model_path), framework='pt', device='cpu') as handle:
162
+ metadata = handle.metadata() or {}
163
+ if metadata.get('vocab_sha256') != config['vocab_sha256']:
164
+ raise ValueError('Safetensors vocabulary checksum does not match config.json')
165
+ # No pickle loading or remote code downloads. Strict loading preserves
166
+ # every output index and catches incompatible architectures.
167
+ state = load_file(str(model_path), device='cpu')
168
+ self.model.load_state_dict(state, strict=True)
169
+ del state
170
+ self.model = self.model.float().to(device).eval()
171
+ self.device = device
172
+
173
+ def predict(self, pixels, masks):
174
+ import torch
175
+
176
+ with torch.inference_mode():
177
+ logits = self.model(torch.from_numpy(pixels).to(self.device),
178
+ padding_mask=torch.from_numpy(masks).to(self.device))['tag_logits']
179
+ return torch.sigmoid(logits.float()).cpu().numpy()
180
+
181
+
182
+ def select_tags(scores, names, threshold, top_k):
183
+ indices = np.flatnonzero(scores >= threshold)
184
+ indices = indices[indices >= 2] # PAD and UNK are never predictions.
185
+ indices = indices[np.argsort(-scores[indices], kind='stable')]
186
+ if top_k:
187
+ indices = indices[:top_k]
188
+ return [{'name': names[str(int(i))], 'score': float(scores[i])} for i in indices]
189
+
190
+
191
+ def main():
192
+ parser = argparse.ArgumentParser(description=__doc__)
193
+ parser.add_argument('images', nargs='+', type=Path)
194
+ parser.add_argument('--format', choices=('onnx', 'safetensors'), default='onnx')
195
+ parser.add_argument('--model', type=Path, help='Override the local model file')
196
+ parser.add_argument('--config', type=Path, default=Path(__file__).resolve().parent / 'config.json')
197
+ parser.add_argument('--device', choices=('cpu', 'cuda'), default='cpu')
198
+ parser.add_argument('--threshold', type=float, help='Override the decision threshold in config.json')
199
+ parser.add_argument('--top-k', type=int, default=0, help='0 returns every tag above threshold')
200
+ parser.add_argument('--batch-size', type=int, default=1)
201
+ parser.add_argument('--output', type=Path)
202
+ args = parser.parse_args()
203
+ if not 1 <= args.batch_size <= 128:
204
+ parser.error('--batch-size must be between 1 and 128')
205
+ if args.top_k < 0 or (args.threshold is not None and not 0 <= args.threshold <= 1):
206
+ parser.error('Use a nonnegative --top-k and a threshold between 0 and 1')
207
+ config, vocab = load_config(args.config)
208
+ model_path = args.model or local_file(args.config.parent, config[f'model_file_{args.format}'])
209
+ threshold = args.threshold if args.threshold is not None else float(config['threshold'])
210
+ if not 0 <= threshold <= 1:
211
+ raise ValueError('Config threshold must be between 0 and 1')
212
+ runtime_type = ONNXRuntime if args.format == 'onnx' else SafetensorsRuntime
213
+ runtime = runtime_type(model_path, config, args.device)
214
+ results = []
215
+ for offset in range(0, len(args.images), args.batch_size):
216
+ paths = args.images[offset:offset + args.batch_size]
217
+ batch = [preprocess(path, config['preprocessing']) for path in paths]
218
+ pixels = np.stack([item[0] for item in batch])
219
+ masks = np.stack([item[1] for item in batch])
220
+ probabilities = runtime.predict(pixels, masks)
221
+ if probabilities.shape != (len(paths), int(config['architecture']['num_tags'])):
222
+ raise RuntimeError('Model output shape does not match vocabulary')
223
+ if not np.isfinite(probabilities).all() or ((probabilities < 0) | (probabilities > 1)).any():
224
+ raise RuntimeError('Model returned invalid probabilities')
225
+ for path, scores in zip(paths, probabilities):
226
+ results.append({'image': str(path), 'tags': select_tags(
227
+ scores, vocab['index_to_tag'], threshold, args.top_k)})
228
+ output = {
229
+ 'model': model_path.name,
230
+ 'format': args.format,
231
+ 'threshold': threshold,
232
+ 'threshold_source': 'user override' if args.threshold is not None else config.get('threshold_source', 'config.json'),
233
+ 'vocab_sha256': config['vocab_sha256'],
234
+ 'predictions': results,
235
+ }
236
+ rendered = json.dumps(output, indent=2, ensure_ascii=False)
237
+ if args.output:
238
+ args.output.write_text(rendered + '\n', encoding='utf-8')
239
+ else:
240
+ print(rendered)
241
+
242
+
243
+ if __name__ == '__main__':
244
+ main()
inference_checks.json ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "passed": true,
3
+ "scope": "Four deterministic synthetic image fixtures; full development AP is archived in validation.json",
4
+ "atol": 0.0005,
5
+ "rtol": 0.001,
6
+ "checks": [
7
+ {
8
+ "format": "onnx",
9
+ "device": "cpu",
10
+ "batch": 1,
11
+ "max_absolute_error_vs_cpu_safetensors": 1.1324882507324219e-06,
12
+ "mean_absolute_error": 6.287191922638158e-08
13
+ },
14
+ {
15
+ "format": "onnx",
16
+ "device": "cpu",
17
+ "batch": 2,
18
+ "max_absolute_error_vs_cpu_safetensors": 1.3113021850585938e-06,
19
+ "mean_absolute_error": 5.217425069758974e-08
20
+ },
21
+ {
22
+ "format": "onnx",
23
+ "device": "cpu",
24
+ "batch": 4,
25
+ "max_absolute_error_vs_cpu_safetensors": 1.3113021850585938e-06,
26
+ "mean_absolute_error": 6.336669855500077e-08
27
+ },
28
+ {
29
+ "device": "cpu",
30
+ "padding_mask_max_effect": 0.6925351023674011
31
+ },
32
+ {
33
+ "format": "onnx",
34
+ "device": "cuda",
35
+ "batch": 1,
36
+ "max_absolute_error_vs_cpu_safetensors": 7.748603820800781e-07,
37
+ "mean_absolute_error": 6.658750351107301e-08
38
+ },
39
+ {
40
+ "format": "onnx",
41
+ "device": "cuda",
42
+ "batch": 2,
43
+ "max_absolute_error_vs_cpu_safetensors": 1.043081283569336e-06,
44
+ "mean_absolute_error": 5.9538443508699856e-08
45
+ },
46
+ {
47
+ "format": "onnx",
48
+ "device": "cuda",
49
+ "batch": 4,
50
+ "max_absolute_error_vs_cpu_safetensors": 9.238719940185547e-07,
51
+ "mean_absolute_error": 6.518111206332833e-08
52
+ },
53
+ {
54
+ "device": "cuda",
55
+ "padding_mask_max_effect": 0.6925351023674011
56
+ },
57
+ {
58
+ "format": "safetensors",
59
+ "device": "cuda",
60
+ "batch": 2,
61
+ "max_absolute_error_vs_cpu_safetensors": 1.1920928955078125e-06
62
+ }
63
+ ],
64
+ "versions": {
65
+ "torch": "2.14.0+cu130",
66
+ "numpy": "2.2.6"
67
+ },
68
+ "model_sha256": "49ba0f66f83ba88a101b6edac54ec39f0a12eab66743e13ae42efc3570745bf7",
69
+ "safetensors_sha256": "ec07d0034f48ba3ceff8624993baf3ea30fa3a2e3cc8908682eb3e4f84eda766"
70
+ }
mask_utils.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Mask migration shims.
4
+ Standardize pixel padding masks to (B,1,H,W) bool with True=PAD,
5
+ and pool to token-level ignore masks (B,L) with True=IGNORE.
6
+ """
7
+ from typing import Optional
8
+ import torch
9
+ import torch.nn.functional as F
10
+
11
+ @torch.no_grad()
12
+ def ensure_pixel_padding_mask(
13
+ mask: torch.Tensor,
14
+ mask_semantics: str = 'auto'
15
+ ) -> Optional[torch.Tensor]:
16
+ """
17
+ (mask) -> (B,1,H,W,bool, True=PAD)
18
+ Accepts (B,H,W) or (B,1,H,W) of {bool,uint8,int,float}.
19
+
20
+ Args:
21
+ mask: Input mask tensor
22
+ mask_semantics: Interpretation of True values:
23
+ - 'auto': Heuristic detection (legacy, unreliable)
24
+ - 'pad': True means padding (no inversion needed)
25
+ - 'valid': True means valid pixels (will be inverted)
26
+
27
+ Returns:
28
+ Standardized mask with True=PAD semantics
29
+ """
30
+ if mask is None:
31
+ return None
32
+
33
+ # Use tensor-based assertions that work during tracing
34
+ original_dims = mask.dim()
35
+ if mask.dim() == 3:
36
+ mask = mask.unsqueeze(1) # (B,1,H,W)
37
+
38
+ # Trace-friendly validation
39
+ if not torch.jit.is_tracing():
40
+ assert mask.dim() == 4, \
41
+ f"padding_mask must be (B,H,W) or (B,1,H,W) after expansion; got {original_dims}D -> {mask.dim()}D with shape {tuple(mask.shape)}"
42
+
43
+ # Binarize + cast
44
+ if mask.dtype != torch.bool:
45
+ mask = (mask > 0.5)
46
+
47
+ # Apply semantics
48
+ if mask_semantics == 'valid':
49
+ # Invert: True=valid -> True=pad
50
+ mask = ~mask
51
+ elif mask_semantics == 'pad':
52
+ # Already correct semantics
53
+ pass
54
+ elif mask_semantics == 'auto':
55
+ # Legacy heuristic (unreliable, deprecated) - per-sample decision
56
+ frac_true = mask.flatten(2).to(torch.float16).mean(dim=2) # (B, 1)
57
+ should_invert = frac_true > 0.5 # (B, 1)
58
+ mask = torch.where(should_invert.unsqueeze(-1).unsqueeze(-1), ~mask, mask)
59
+ # Log warning in non-tracing mode
60
+ if not torch.jit.is_tracing():
61
+ import logging
62
+ logging.getLogger(__name__).warning(
63
+ "Using 'auto' mask semantics detection is unreliable. "
64
+ "Please specify mask_semantics='pad' or 'valid' explicitly."
65
+ )
66
+ else:
67
+ raise ValueError(f"Invalid mask_semantics: {mask_semantics}. Use 'auto', 'pad', or 'valid'.")
68
+
69
+ return mask
70
+
71
+ @torch.no_grad()
72
+ def pixel_to_token_ignore(mask_b1hw: torch.Tensor, patch: int, threshold: float = 0.9) -> torch.Tensor:
73
+ """
74
+ (B,1,H,W,bool True=PAD) -> (B,L,bool True=IGNORE)
75
+ Pools pad-fraction per patch via avg_pool2d(kernel=stride=patch) and marks
76
+ tokens IGNORE when pad_fraction >= threshold.
77
+ """
78
+ B, C, H, W = mask_b1hw.shape
79
+
80
+ # Trace-friendly validation
81
+ if not torch.jit.is_tracing():
82
+ assert mask_b1hw.dim() == 4, \
83
+ f"Expected 4D tensor (B,1,H,W), got {mask_b1hw.dim()}D with shape {tuple(mask_b1hw.shape)}"
84
+ assert C == 1, \
85
+ f"Expected channel dimension of 1, got {C} with shape {tuple(mask_b1hw.shape)}"
86
+ assert mask_b1hw.dtype == torch.bool, \
87
+ f"mask_b1hw must be bool (True=PAD), got dtype {mask_b1hw.dtype}"
88
+ assert H % patch == 0, \
89
+ f"Image height {H} must be divisible by patch size {patch}"
90
+ assert W % patch == 0, \
91
+ f"Image width {W} must be divisible by patch size {patch}"
92
+
93
+ # pad_fraction per patch
94
+ pad_frac = F.avg_pool2d(mask_b1hw.to(torch.float16), kernel_size=patch, stride=patch) # (B,1,GH,GW)
95
+ ignore = (pad_frac.squeeze(1) >= threshold) # (B,GH,GW)
96
+ return ignore.flatten(1) # (B,L)
model.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:49ba0f66f83ba88a101b6edac54ec39f0a12eab66743e13ae42efc3570745bf7
3
+ size 592413547
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ec07d0034f48ba3ceff8624993baf3ea30fa3a2e3cc8908682eb3e4f84eda766
3
+ size 590858968
model_architecture.py ADDED
@@ -0,0 +1,860 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Model Architecture for Anime Image Tagger - Direct Training (modified)
4
+ Vision Transformer for anime image tagging
5
+ """
6
+
7
+ from abc import ABC, abstractmethod
8
+ import logging
9
+ import math
10
+ import warnings
11
+ from dataclasses import dataclass, fields
12
+ from typing import Any, Dict, Optional, Tuple
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ import torch
17
+ import torch.nn as nn
18
+ import torch.nn.functional as F
19
+ from mask_utils import ensure_pixel_padding_mask, pixel_to_token_ignore
20
+ from custom_drop_path import SafeDropPath
21
+
22
+ # Import Flex Attention (PyTorch 2.5+)
23
+ from torch.nn.attention.flex_attention import flex_attention, create_block_mask, BlockMask
24
+
25
+ logger.info("Using PyTorch Flex Attention")
26
+
27
+
28
+ def _check_triton_available() -> bool:
29
+ """Check if Triton is available for compiled block mask creation."""
30
+ try:
31
+ import triton
32
+ return True
33
+ except ImportError:
34
+ return False
35
+
36
+
37
+ # Cache the Triton availability check at module load time
38
+ _TRITON_AVAILABLE = _check_triton_available()
39
+ # PyTorch 2.14 deprecates create_block_mask(_compile=True). Reuse the public
40
+ # compiled callable; compilation remains lazy until the first forward pass.
41
+ _CREATE_BLOCK_MASK = torch.compile(create_block_mask) if _TRITON_AVAILABLE else create_block_mask
42
+
43
+
44
+ class BaseTagger(ABC, nn.Module):
45
+ """Abstract base class for image tagging models."""
46
+
47
+ @abstractmethod
48
+ def forward(
49
+ self,
50
+ pixel_values: torch.Tensor,
51
+ padding_mask: Optional[torch.Tensor] = None,
52
+ ) -> Dict[str, torch.Tensor]:
53
+ """Forward pass returning tag_logits dict."""
54
+ pass
55
+
56
+ @abstractmethod
57
+ def set_onnx_mode(self, enabled: bool = True) -> None:
58
+ """Enable/disable ONNX-compatible attention mode."""
59
+ pass
60
+
61
+ @property
62
+ @abstractmethod
63
+ def config(self):
64
+ """Return model configuration."""
65
+ pass
66
+
67
+
68
+ def initialize_tag_head_bias(
69
+ model: BaseTagger,
70
+ index_to_tag: Dict[int, str],
71
+ tag_frequencies: Dict[str, int],
72
+ total_samples: int,
73
+ min_prior: float = 1e-5,
74
+ max_prior: float = 0.99,
75
+ rated_samples: Optional[int] = None,
76
+ ) -> None:
77
+ """Initialize tag_head bias with log-prior for focal loss (RetinaNet technique).
78
+
79
+ Sets each tag's bias to log(prior / (1 - prior)) based on empirical tag frequency.
80
+ This makes the model start by predicting "mostly negative" for sparse labels,
81
+ providing meaningful gradients from the first step instead of random 0.5 predictions.
82
+
83
+ Must be called AFTER model creation (which zeros the bias) and BEFORE checkpoint
84
+ loading (which will overwrite the bias if resuming).
85
+ """
86
+ with torch.no_grad():
87
+ bias = model.tag_head.bias
88
+ num_tags = bias.shape[0]
89
+ for idx in range(num_tags):
90
+ tag = index_to_tag.get(idx, "")
91
+ freq = tag_frequencies.get(tag, 0)
92
+ denominator = (rated_samples if tag.startswith('rating:') and rated_samples is not None
93
+ else total_samples)
94
+ # An entirely unrated corpus provides no rating prior evidence.
95
+ empirical = 0.25 if denominator == 0 and tag.startswith('rating:') else freq / max(1, denominator)
96
+ prior = max(min_prior, min(max_prior, empirical))
97
+ bias[idx] = math.log(prior / (1 - prior))
98
+
99
+ bias_vals = bias.tolist()
100
+ logger.info(
101
+ "Tag head bias initialized with log-prior: min=%.2f, max=%.2f, mean=%.2f",
102
+ min(bias_vals), max(bias_vals), sum(bias_vals) / len(bias_vals),
103
+ )
104
+
105
+
106
+ class LayerNormFp32(nn.LayerNorm):
107
+ """
108
+ LayerNorm that optionally casts to float32 before calling the original LayerNorm.
109
+ This is to improve stability when using mixed precision training.
110
+
111
+ Args:
112
+ normalized_shape: Input shape from an expected input of size
113
+ eps: A value added to the denominator for numerical stability
114
+ elementwise_affine: A boolean value that when set to True, gives learnable parameters
115
+ use_fp32: If True, cast to float32 before LayerNorm (better stability).
116
+ If False, use native dtype (faster but potentially less stable).
117
+ """
118
+ def __init__(self, normalized_shape, eps=1e-5, elementwise_affine=True, use_fp32=True):
119
+ super().__init__(normalized_shape, eps=eps, elementwise_affine=elementwise_affine)
120
+ self.use_fp32 = use_fp32
121
+
122
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
123
+ if self.use_fp32:
124
+ return super().forward(x.float()).type_as(x)
125
+ else:
126
+ return super().forward(x)
127
+
128
+
129
+ @dataclass
130
+ class VisionTransformerConfig:
131
+ """Configuration for the Vision Transformer used in direct training."""
132
+ image_size: int = 448
133
+ # Patch size must divide image_size evenly to avoid losing border information (validated in __post_init__)
134
+ patch_size: int = 16
135
+ num_channels: int = 3
136
+ hidden_size: int = 1024
137
+ num_hidden_layers: int = 18
138
+ num_attention_heads: int = 16
139
+ intermediate_size: int = 4096
140
+ num_tags: int = 100000 # This should be overridden with actual vocab size (includes rating tags)
141
+ dropout: float = 0.1
142
+ pos_dropout: float = 0.0 # Position embedding dropout (0.0 = modern standard; drop_path handles regularization)
143
+ attention_dropout: float = 0.1
144
+ qk_norm: bool = False # Legacy checkpoints omit this; V2 explicitly enables it.
145
+ layer_scale_init: float = 0.0 # Zero disables LayerScale for legacy checkpoints.
146
+ layer_norm_eps: float = 1e-6
147
+ use_flex_attention: bool = True # Use Flex Attention (PyTorch 2.5+)
148
+ flex_block_size: int = 128 # Block size for Flex Attention sparse computation
149
+ attention_bias: bool = True # Use bias in attention QKV and projection layers
150
+ # Token ignore threshold: a token is ignored if >= this fraction of its pixels are PAD
151
+ token_ignore_threshold: float = 0.9
152
+ # Enable gradient checkpointing by default to reduce memory usage
153
+ gradient_checkpointing: bool = True
154
+ # Checkpoint every N-th layer (1=all layers, 2=every 2nd, 4=every 4th, etc.)
155
+ checkpoint_every_n_layers: int = 1
156
+ drop_path_rate: float = 0.0
157
+ # Enable numerical stability checking (for debugging only)
158
+ check_numerical_stability: bool = False
159
+ # Logit clamping for inference/export outputs (None to disable)
160
+ # exp(15) ~ 3.3M which is safe for softmax in float16/bfloat16
161
+ # Applied only in eval mode: training logits are left unclamped because
162
+ # clamp has zero gradient outside the bounds (see forward()).
163
+ logit_clamp_value: Optional[float] = 15.0
164
+ # Precision configuration
165
+ use_fp32_layernorm: bool = False # Use FP32 for LayerNorm (better stability, slight speed cost). Set to False for full bfloat16.
166
+
167
+ def __post_init__(self):
168
+ # Validate drop_path_rate
169
+ assert 0.0 <= self.drop_path_rate < 1.0, (
170
+ f"drop_path_rate must be in [0, 1), got {self.drop_path_rate}"
171
+ )
172
+ if self.drop_path_rate > 0.5:
173
+ warnings.warn(
174
+ f"drop_path_rate={self.drop_path_rate} is unusually high; "
175
+ "values between 0.08 and 0.3 are typical for ViT."
176
+ )
177
+
178
+ # Validate image_size and patch_size
179
+ if self.image_size <= 0:
180
+ raise ValueError(
181
+ f"image_size must be positive, got {self.image_size}"
182
+ )
183
+
184
+ if self.patch_size <= 0:
185
+ raise ValueError(
186
+ f"patch_size must be positive, got {self.patch_size}"
187
+ )
188
+
189
+ if self.image_size % self.patch_size != 0:
190
+ # Calculate valid alternatives
191
+ valid_sizes = [
192
+ s for s in [224, 256, 384, 448, 512, 576, 640, 768, 896, 1024]
193
+ if s % self.patch_size == 0 and abs(s - self.image_size) < 200
194
+ ]
195
+
196
+ raise ValueError(
197
+ f"image_size ({self.image_size}) must be evenly divisible by "
198
+ f"patch_size ({self.patch_size}). "
199
+ f"Current: {self.image_size} % {self.patch_size} = {self.image_size % self.patch_size}. "
200
+ f"\n\nSuggested fixes:"
201
+ f"\n 1. Use a standard image size: {valid_sizes if valid_sizes else 'N/A'}"
202
+ f"\n 2. Change patch_size to a factor of {self.image_size}: "
203
+ f"{[d for d in [8, 14, 16, 20, 32] if self.image_size % d == 0]}"
204
+ f"\n 3. Adjust image_size to nearest multiple of {self.patch_size}: "
205
+ f"{(self.image_size // self.patch_size) * self.patch_size} or "
206
+ f"{((self.image_size // self.patch_size) + 1) * self.patch_size}"
207
+ )
208
+
209
+ # Validate computed values make sense
210
+ num_patches = (self.image_size // self.patch_size) ** 2
211
+ if num_patches < 4:
212
+ warnings.warn(
213
+ f"Very few patches ({num_patches}) with image_size={self.image_size} "
214
+ f"and patch_size={self.patch_size}. Model may underperform."
215
+ )
216
+
217
+ if num_patches > 10000:
218
+ warnings.warn(
219
+ f"Very many patches ({num_patches}) with image_size={self.image_size} "
220
+ f"and patch_size={self.patch_size}. May cause memory issues."
221
+ )
222
+
223
+ # Validate hidden_size is divisible by num_attention_heads
224
+ if self.hidden_size % self.num_attention_heads != 0:
225
+ raise ValueError(
226
+ f"hidden_size ({self.hidden_size}) must be divisible by "
227
+ f"num_attention_heads ({self.num_attention_heads}). "
228
+ f"Current remainder: {self.hidden_size % self.num_attention_heads}. "
229
+ f"head_dim would be {self.hidden_size / self.num_attention_heads:.2f} (must be integer)."
230
+ )
231
+
232
+
233
+ class TransformerBlock(nn.Module):
234
+ """Single transformer block using Flex Attention."""
235
+
236
+ def __init__(self, config: VisionTransformerConfig, drop_path: float = 0.):
237
+ super().__init__()
238
+ self.config = config
239
+ self.norm1 = LayerNormFp32(config.hidden_size, eps=config.layer_norm_eps, use_fp32=config.use_fp32_layernorm)
240
+
241
+ # Flex Attention setup
242
+ self.num_heads = config.num_attention_heads
243
+ self.head_dim = config.hidden_size // config.num_attention_heads
244
+ self.scale = self.head_dim ** -0.5
245
+ self.flex_block_size = config.flex_block_size
246
+
247
+ # QKV projection (fused linear for efficiency)
248
+ self.qkv = nn.Linear(config.hidden_size, 3 * config.hidden_size, bias=config.attention_bias)
249
+ self.proj = nn.Linear(config.hidden_size, config.hidden_size, bias=config.attention_bias)
250
+ self.attn_dropout = config.attention_dropout
251
+ self.q_norm = LayerNormFp32(self.head_dim, eps=config.layer_norm_eps, use_fp32=True) if config.qk_norm else nn.Identity()
252
+ self.k_norm = LayerNormFp32(self.head_dim, eps=config.layer_norm_eps, use_fp32=True) if config.qk_norm else nn.Identity()
253
+ self.ls1 = nn.Parameter(torch.full((config.hidden_size,), config.layer_scale_init)) if config.layer_scale_init > 0 else None
254
+ self.ls2 = nn.Parameter(torch.full((config.hidden_size,), config.layer_scale_init)) if config.layer_scale_init > 0 else None
255
+ self.capture_attention_stats = False
256
+ self.max_attention_logit = None
257
+
258
+ # Drop path and MLP
259
+ self.drop_path = SafeDropPath(drop_path) if drop_path > 0.0 else nn.Identity()
260
+ self.norm2 = LayerNormFp32(config.hidden_size, eps=config.layer_norm_eps, use_fp32=config.use_fp32_layernorm)
261
+ self.mlp = nn.Sequential(
262
+ nn.Linear(config.hidden_size, config.intermediate_size),
263
+ nn.GELU(),
264
+ nn.Dropout(config.dropout),
265
+ nn.Linear(config.intermediate_size, config.hidden_size),
266
+ nn.Dropout(config.dropout)
267
+ )
268
+
269
+ def _normalize_qk(self, q, k):
270
+ q, k = self.q_norm(q), self.k_norm(k)
271
+ if self.capture_attention_stats:
272
+ # Chunked exact maximum: bounded scratch rather than retaining L x L
273
+ # scores for every layer. Only enabled for the diagnostic forward.
274
+ with torch.no_grad(), torch.autocast(device_type=q.device.type, enabled=False):
275
+ maximum = q.new_zeros((), dtype=torch.float32)
276
+ for start in range(0, q.shape[-2], 64):
277
+ scores = q[:, :, start:start + 64].float() @ k.float().transpose(-2, -1)
278
+ maximum = torch.maximum(maximum, scores.abs().amax() * self.scale)
279
+ self.max_attention_logit = maximum
280
+ return q, k
281
+
282
+ def _residual(self, x, attn_out):
283
+ attention = self.proj(attn_out)
284
+ x = x + self.drop_path(attention if self.ls1 is None else attention * self.ls1.to(attention.dtype))
285
+ mlp = self.mlp(self.norm2(x))
286
+ return x + self.drop_path(mlp if self.ls2 is None else mlp * self.ls2.to(mlp.dtype))
287
+
288
+ def forward(self, x: torch.Tensor, block_mask: Optional[BlockMask] = None) -> torch.Tensor:
289
+ """Forward pass using Flex Attention.
290
+
291
+ Args:
292
+ x: Input tensor of shape (batch_size, seq_len, hidden_size)
293
+ block_mask: Optional pre-computed BlockMask for padding-aware attention.
294
+ Created once by SimplifiedTagger and shared across all layers.
295
+
296
+ Returns:
297
+ torch.Tensor: Output tensor of shape (batch_size, seq_len, hidden_size)
298
+
299
+ Note:
300
+ Uses pre-layer normalization (LayerNorm -> Attention -> Residual).
301
+ """
302
+ normed_x = self.norm1(x)
303
+ B, L, D = normed_x.shape
304
+
305
+ # QKV projection and reshape
306
+ qkv = self.qkv(normed_x).reshape(B, L, 3, self.num_heads, self.head_dim)
307
+ q, k, v = qkv.unbind(2) # Each: (B, L, num_heads, head_dim)
308
+
309
+ # Transpose to (B, num_heads, L, head_dim) for flex_attention
310
+ # IMPORTANT: Make tensors contiguous after transpose to ensure proper memory layout.
311
+ # Non-contiguous tensors from reshape->unbind->transpose cause stride assertion
312
+ # failures when torch.compile traces the graph (inductor expects contiguous strides).
313
+ q = q.transpose(1, 2).contiguous()
314
+ k = k.transpose(1, 2).contiguous()
315
+ v = v.transpose(1, 2).contiguous()
316
+ q, k = self._normalize_qk(q, k)
317
+
318
+ # flex_attention has no native attention-weight dropout, so apply V-dropout
319
+ # before the call. Zeroing random value tokens removes their contribution to
320
+ # all queries — a documented approximation of attention dropout used in T5
321
+ # and several BERT variants. Replaces the previous post-attention output
322
+ # dropout, which did not regularize attention weights as the config name
323
+ # suggested.
324
+ if self.training and self.attn_dropout > 0:
325
+ v = F.dropout(v, p=self.attn_dropout, training=True)
326
+
327
+ # Flex Attention computation (block_mask is pre-computed and shared across layers)
328
+ attn_out = flex_attention(
329
+ q, k, v,
330
+ block_mask=block_mask,
331
+ scale=self.scale,
332
+ )
333
+
334
+ # Reshape back to (B, L, D) - ensure contiguous for torch.compile compatibility
335
+ attn_out = attn_out.transpose(1, 2).reshape(B, L, D).contiguous()
336
+
337
+ # Residual connections
338
+ x = self._residual(x, attn_out)
339
+
340
+ return x
341
+
342
+ def forward_sdpa(self, x: torch.Tensor, attn_mask: Optional[torch.Tensor] = None) -> torch.Tensor:
343
+ """Forward pass using scaled_dot_product_attention (ONNX-compatible).
344
+
345
+ This method provides an ONNX-exportable alternative to the flex_attention path.
346
+ It uses F.scaled_dot_product_attention which is supported by ONNX opset 14+.
347
+
348
+ Args:
349
+ x: Input tensor of shape (batch_size, seq_len, hidden_size)
350
+ attn_mask: Optional boolean attention mask (B, L) with True=ATTEND, False=IGNORE.
351
+ Passed directly to SDPA as a boolean key-padding mask — avoids the
352
+ additive float mask path that forces PyTorch off the Flash/efficient
353
+ backends.
354
+
355
+ Returns:
356
+ torch.Tensor: Output tensor of shape (batch_size, seq_len, hidden_size)
357
+
358
+ Note:
359
+ Uses pre-layer normalization (LayerNorm -> Attention -> Residual).
360
+ For ONNX export, this path is used instead of flex_attention.
361
+ """
362
+ normed_x = self.norm1(x)
363
+ B, L, D = normed_x.shape
364
+
365
+ # QKV projection and reshape
366
+ qkv = self.qkv(normed_x).reshape(B, L, 3, self.num_heads, self.head_dim)
367
+ q, k, v = qkv.unbind(2) # Each: (B, L, num_heads, head_dim)
368
+
369
+ # Transpose to (B, num_heads, L, head_dim) for scaled_dot_product_attention
370
+ q = q.transpose(1, 2).contiguous()
371
+ k = k.transpose(1, 2).contiguous()
372
+ v = v.transpose(1, 2).contiguous()
373
+ q, k = self._normalize_qk(q, k)
374
+
375
+ # Pass the boolean mask straight through. SDPA accepts a bool key-padding mask
376
+ # broadcastable to (B, num_heads, L_q, L_k); we broadcast (B, L) -> (B, 1, 1, L).
377
+ # Bool semantics for SDPA: True=ATTEND, False=IGNORE — already matches `attn_mask`.
378
+ sdpa_mask: Optional[torch.Tensor] = None
379
+ if attn_mask is not None:
380
+ if attn_mask.ndim != 2 or attn_mask.shape[0] != B or attn_mask.shape[1] != L:
381
+ raise ValueError(
382
+ f"attn_mask shape {attn_mask.shape} mismatch. "
383
+ f"Expected ({B}, {L}) for input shape ({B}, {L}, {D})"
384
+ )
385
+ sdpa_mask = attn_mask.unsqueeze(1).unsqueeze(2) # (B, 1, 1, L) bool
386
+
387
+ # Scaled dot-product attention (ONNX-compatible)
388
+ attn_out = F.scaled_dot_product_attention(
389
+ q, k, v,
390
+ attn_mask=sdpa_mask,
391
+ dropout_p=self.attn_dropout if self.training else 0.0,
392
+ scale=self.scale,
393
+ )
394
+
395
+ # Reshape back to (B, L, D)
396
+ attn_out = attn_out.transpose(1, 2).reshape(B, L, D).contiguous()
397
+
398
+ # Residual connections
399
+ x = self._residual(x, attn_out)
400
+
401
+ return x
402
+
403
+
404
+ @torch.no_grad()
405
+ def sample_attention_logits(model, images, padding_mask=None):
406
+ """Exact absolute score maximum on a small diagnostic batch, without RNG drift."""
407
+ model = getattr(model, '_orig_mod', model)
408
+ old_mode = model._onnx_mode
409
+ devices = [images.device.index] if images.is_cuda else []
410
+ try:
411
+ model._onnx_mode = True # Eager SDPA avoids compiling a diagnostic graph.
412
+ for block in model.blocks:
413
+ block.capture_attention_stats = True
414
+ with torch.random.fork_rng(devices=devices):
415
+ model(images, padding_mask=padding_mask)
416
+ return torch.stack([block.max_attention_logit for block in model.blocks]).amax().item()
417
+ finally:
418
+ model._onnx_mode = old_mode
419
+ for block in model.blocks:
420
+ block.capture_attention_stats = False
421
+ block.max_attention_logit = None
422
+
423
+
424
+ class SimplifiedTagger(BaseTagger):
425
+ """Vision Transformer based tagger for anime images."""
426
+ def __init__(self, config: VisionTransformerConfig):
427
+ super().__init__()
428
+ self._config = config
429
+ self._onnx_mode = False # When True, use SDPA instead of flex_attention
430
+ # Patch embedding layer
431
+ self.patch_embed = nn.Conv2d(
432
+ config.num_channels,
433
+ config.hidden_size,
434
+ kernel_size=config.patch_size,
435
+ stride=config.patch_size
436
+ )
437
+ # Position embeddings
438
+ num_patches = (config.image_size // config.patch_size) ** 2
439
+ self.pos_embed = nn.Parameter(
440
+ torch.zeros(1, num_patches + 1, config.hidden_size)
441
+ )
442
+ self.cls_token = nn.Parameter(
443
+ torch.zeros(1, 1, config.hidden_size)
444
+ )
445
+ # Initialize pos_embed and cls_token with truncated normal (std=0.02)
446
+ # These are nn.Parameter objects, not nn.Module, so _init_weights() doesn't handle them.
447
+ # Standard ViT initialization follows timm/HuggingFace convention.
448
+ nn.init.trunc_normal_(self.pos_embed, std=0.02)
449
+ nn.init.trunc_normal_(self.cls_token, std=0.02)
450
+ self.pos_drop = nn.Dropout(p=config.pos_dropout)
451
+ # Transformer blocks
452
+ rate = getattr(config, 'drop_path_rate', 0.0)
453
+ dpr = torch.linspace(0.0, rate, config.num_hidden_layers)
454
+ dpr = dpr.clamp_max(1.0 - 1e-6).tolist()
455
+ self.blocks = nn.ModuleList([
456
+ TransformerBlock(config, drop_path=float(p))
457
+ for p in dpr
458
+ ])
459
+ # Final layer norm
460
+ self.norm = LayerNormFp32(config.hidden_size, eps=config.layer_norm_eps, use_fp32=config.use_fp32_layernorm)
461
+ # Classification head (ratings are now part of the tag vocabulary)
462
+ self.tag_head = nn.Linear(config.hidden_size, config.num_tags)
463
+ # Weight initialization
464
+ self.apply(self._init_weights)
465
+ # Override patch_embed init: standard ViT uses trunc_normal(std=0.02).
466
+ # The Conv2d branch in _init_weights sets std = sqrt(2/(k*k*out_channels))
467
+ # ≈ 0.0028 for our k=16, out_channels=1024 — ~7x smaller than ViT convention,
468
+ # which produces vanishing patch-embed activations early in from-scratch
469
+ # training. Re-init here so the apply() pass above doesn't overwrite us.
470
+ nn.init.trunc_normal_(self.patch_embed.weight, std=0.02)
471
+ if self.patch_embed.bias is not None:
472
+ nn.init.constant_(self.patch_embed.bias, 0)
473
+
474
+ def _init_weights(self, module):
475
+ if isinstance(module, nn.Linear):
476
+ torch.nn.init.trunc_normal_(module.weight, std=0.02)
477
+ if module.bias is not None:
478
+ nn.init.constant_(module.bias, 0)
479
+ elif isinstance(module, (nn.LayerNorm, LayerNormFp32)):
480
+ nn.init.constant_(module.bias, 0)
481
+ nn.init.constant_(module.weight, 1.0)
482
+ elif isinstance(module, nn.Conv2d):
483
+ fan_out = module.kernel_size[0] * module.kernel_size[1] * module.out_channels
484
+ fan_out //= module.groups
485
+ module.weight.data.normal_(0, math.sqrt(2.0 / fan_out))
486
+ if module.bias is not None:
487
+ module.bias.data.zero_()
488
+
489
+ def set_onnx_mode(self, enabled: bool = True) -> None:
490
+ """Enable or disable ONNX-compatible mode.
491
+
492
+ When enabled, the model uses F.scaled_dot_product_attention instead of
493
+ flex_attention. This is required for ONNX export since flex_attention
494
+ is a PyTorch 2.5+ CUDA kernel that cannot be traced by ONNX.
495
+
496
+ Args:
497
+ enabled: If True, use SDPA (ONNX-compatible). If False, use flex_attention.
498
+
499
+ Note:
500
+ This only affects the attention computation path. Model weights are unchanged.
501
+ The SDPA path produces numerically equivalent results to flex_attention.
502
+ """
503
+ self._onnx_mode = enabled
504
+ logger.info(f"ONNX mode {'enabled' if enabled else 'disabled'} - using {'SDPA' if enabled else 'flex_attention'}")
505
+
506
+ @property
507
+ def config(self):
508
+ """Return model configuration."""
509
+ return self._config
510
+
511
+ def _get_pos_embed(self, grid_h: int, grid_w: int) -> torch.Tensor:
512
+ """Return position embeddings matched to the runtime token grid.
513
+
514
+ Fast path (the common case, and the ONNX-export resolution): the runtime
515
+ grid equals the stored square grid, so the stored parameter is returned
516
+ unchanged. Otherwise the 2D patch position embeddings are bicubically
517
+ interpolated to (grid_h, grid_w) — same approach as the checkpoint-resume
518
+ interpolation in training_utils.py — keeping the CLS embedding as-is.
519
+ Without this, inputs at a different resolution would silently add the
520
+ raster-order *prefix* of the stored grid.
521
+ """
522
+ stored_len = self.pos_embed.shape[1] # 1 (CLS) + stored_grid**2
523
+ stored_grid = int(math.isqrt(stored_len - 1))
524
+ if grid_h == stored_grid and grid_w == stored_grid:
525
+ return self.pos_embed
526
+ cls_pos = self.pos_embed[:, :1, :]
527
+ patch_pos = self.pos_embed[:, 1:, :]
528
+ hidden_size = patch_pos.shape[-1]
529
+ # Reshape to 2D spatial grid, interpolate, flatten back
530
+ patch_pos = patch_pos.reshape(1, stored_grid, stored_grid, hidden_size).permute(0, 3, 1, 2).float()
531
+ patch_pos = F.interpolate(
532
+ patch_pos, size=(grid_h, grid_w), mode='bicubic', align_corners=False
533
+ )
534
+ patch_pos = patch_pos.permute(0, 2, 3, 1).reshape(1, grid_h * grid_w, hidden_size)
535
+ patch_pos = patch_pos.to(self.pos_embed.dtype)
536
+ return torch.cat([cls_pos, patch_pos], dim=1)
537
+
538
+ def _check_numerical_stability(
539
+ self,
540
+ tag_logits: torch.Tensor,
541
+ ) -> None:
542
+ """Check for NaN/Inf in logits and log statistics.
543
+
544
+ This method is only called when config.check_numerical_stability=True.
545
+ It helps diagnose numerical instability issues during training/inference.
546
+ """
547
+ tag_has_nan = torch.isnan(tag_logits).any().item()
548
+ tag_has_inf = torch.isinf(tag_logits).any().item()
549
+
550
+ if tag_has_nan or tag_has_inf:
551
+ warnings.warn(
552
+ f"Numerical instability in tag_logits: "
553
+ f"NaN={tag_has_nan}, Inf={tag_has_inf}. "
554
+ f"Stats: min={tag_logits.min():.2f}, max={tag_logits.max():.2f}, "
555
+ f"mean={tag_logits.mean():.2f}, std={tag_logits.std():.2f}"
556
+ )
557
+
558
+ # Diagnostic only: report how many logits exceed the *configured* clamp
559
+ # value (the real clamp happens later in forward() at logit_clamp_value,
560
+ # eval mode only — training logits are left unclamped).
561
+ clamp_threshold = self._config.logit_clamp_value
562
+ if clamp_threshold is not None:
563
+ tag_over_clamp = (tag_logits.abs() > clamp_threshold).any().item()
564
+ if tag_over_clamp:
565
+ num_over = (tag_logits.abs() > clamp_threshold).sum().item()
566
+ warnings.warn(
567
+ f"{num_over} tag logits exceed logit_clamp_value={clamp_threshold} "
568
+ f"(clamped downstream in eval mode only; max abs value: {tag_logits.abs().max():.2f})"
569
+ )
570
+
571
+ @torch.compiler.disable(recursive=False)
572
+ def _create_block_mask(self, key_padding_mask: torch.Tensor, seq_len: int) -> BlockMask:
573
+ """Create BlockMask for padding-aware attention.
574
+
575
+ This method creates the mask once per forward pass, which is then shared
576
+ across all transformer layers for efficiency (instead of creating per-layer).
577
+ Keep its host dispatch outside the model graph: PyTorch 2.14 can raise
578
+ Inductor CantSplit when block-mask reductions are fused with dynamic image
579
+ shapes. recursive=False lets the inner mask builder remain compiled.
580
+
581
+ Args:
582
+ key_padding_mask: (B, L) bool, True=IGNORE (padding tokens)
583
+ seq_len: Sequence length
584
+
585
+ Returns:
586
+ BlockMask for use with flex_attention
587
+ """
588
+ B = key_padding_mask.shape[0]
589
+ attend_mask = ~key_padding_mask # (B, L) True=ATTEND
590
+
591
+ def mask_mod(b, h, q_idx, kv_idx):
592
+ # Both query and key must be non-padding for attention
593
+ # This prevents padding tokens from attending (wasteful) and being attended to
594
+ # Use bitwise & instead of logical 'and' for Triton 3.4+ compatibility
595
+ return attend_mask[b, q_idx] & attend_mask[b, kv_idx]
596
+
597
+ # Use first block's config for flex_block_size
598
+ flex_block_size = self._config.flex_block_size
599
+
600
+ return _CREATE_BLOCK_MASK(
601
+ mask_mod,
602
+ B=B,
603
+ H=None, # Broadcast across heads - mask is head-independent (saves memory)
604
+ Q_LEN=seq_len,
605
+ KV_LEN=seq_len,
606
+ device=key_padding_mask.device,
607
+ BLOCK_SIZE=min(flex_block_size, seq_len),
608
+ )
609
+
610
+ def forward(
611
+ self,
612
+ pixel_values: torch.Tensor,
613
+ padding_mask: Optional[torch.Tensor] = None, # (B,H,W) or (B,1,H,W), auto-detected semantics
614
+ ) -> Dict[str, torch.Tensor]:
615
+ B = pixel_values.shape[0]
616
+ # Patch embedding (optionally force fp32 for numerical stability under AMP)
617
+ if self._config.use_fp32_layernorm and pixel_values.dtype in (torch.float16, torch.bfloat16):
618
+ # Detect device type dynamically to support cuda/cpu/mps
619
+ device_type = pixel_values.device.type
620
+ # Autocast is only supported on certain device types
621
+ supported_devices = {'cuda', 'cpu', 'mps', 'xpu'}
622
+
623
+ if device_type in supported_devices:
624
+ # Use autocast to disable AMP for this operation
625
+ with torch.autocast(device_type=device_type, enabled=False):
626
+ x = self.patch_embed(pixel_values.float())
627
+ else:
628
+ # Fallback: Just convert to float32 without autocast context
629
+ # This works on all devices but doesn't interact with AMP
630
+ warnings.warn(
631
+ f"Device type '{device_type}' doesn't support autocast. "
632
+ f"Using fallback path for patch embedding."
633
+ )
634
+ x = self.patch_embed(pixel_values.float())
635
+
636
+ x = x.to(pixel_values.dtype)
637
+ else:
638
+ x = self.patch_embed(pixel_values)
639
+ # Capture the runtime token grid before flattening (for pos-embed matching)
640
+ grid_h, grid_w = x.shape[-2], x.shape[-1]
641
+ x = x.flatten(2).transpose(1, 2).contiguous()
642
+ # Add CLS token
643
+ cls_tokens = self.cls_token.expand(B, -1, -1)
644
+ x = torch.cat([cls_tokens, x], dim=1)
645
+ # Add position embeddings (bicubically interpolated to the runtime grid
646
+ # if it differs from the stored grid; no-op at the trained resolution)
647
+ x = x + self._get_pos_embed(grid_h, grid_w)
648
+ x = self.pos_drop(x)
649
+ # =======================================================================
650
+ # MASK SEMANTICS DOCUMENTATION
651
+ # =======================================================================
652
+ # This section handles padding masks with multiple transformations.
653
+ # Understanding the semantics at each step is crucial for correctness.
654
+ #
655
+ # STEP 1: Input padding_mask (pixel-level)
656
+ # - Shape: (B, H, W) or (B, 1, H, W)
657
+ # - Semantics: True = PADDING pixel (should be ignored)
658
+ #
659
+ # STEP 2: attn_kpm (token-level key padding mask)
660
+ # - Shape: (B, 1+Lp) where Lp = number of patch tokens
661
+ # - Semantics: True = IGNORE this token (it's mostly padding)
662
+ # - CLS token is always False (never ignored)
663
+ #
664
+ # STEP 3a: For ONNX/SDPA mode -> sdpa_attn_mask
665
+ # - We invert: sdpa_attn_mask = ~attn_kpm
666
+ # - Semantics: True = ATTEND to this token
667
+ # - forward_sdpa() passes this directly to F.scaled_dot_product_attention as
668
+ # a boolean key-padding mask (broadcast to (B, 1, 1, L)). SDPA's bool-mask
669
+ # contract matches: True=attend, False=ignore — so no additive conversion
670
+ # is needed, and PyTorch can keep using the Flash/efficient backends.
671
+ #
672
+ # STEP 3b: For flex_attention mode -> block_mask
673
+ # - _create_block_mask inverts internally: attend_mask = ~key_padding_mask
674
+ # - BlockMask uses: True = ATTEND (allow attention between these positions)
675
+ # - flex_attention natively handles this without further inversion
676
+ # =======================================================================
677
+
678
+ # Build key-padding mask (CLS + patch tokens) from pixel-level mask.
679
+ # Output: attn_kpm with semantics True=IGNORE (padding tokens to exclude)
680
+ attn_kpm: Optional[torch.Tensor] = None
681
+ if padding_mask is not None:
682
+ # Convert pixel mask to standard format: (B,1,H,W) bool, True=PAD
683
+ pm = ensure_pixel_padding_mask(padding_mask, mask_semantics='pad')
684
+ thr = getattr(self._config, "token_ignore_threshold", 0.9)
685
+ # Pool pixel mask to token-level: token is IGNORE if >= threshold fraction is padding
686
+ token_ignore = pixel_to_token_ignore(pm, patch=self._config.patch_size, threshold=thr) # (B,Lp)
687
+ # Prepend CLS token (always attend, never ignored)
688
+ cls_keep = torch.zeros(B, 1, dtype=torch.bool, device=token_ignore.device) # False = don't ignore
689
+ attn_kpm = torch.cat([cls_keep, token_ignore], dim=1) # (B, 1+Lp), True=IGNORE
690
+
691
+ if attn_kpm is not None:
692
+ # Only validate in debug mode to avoid expensive GPU-CPU synchronization
693
+ # (skip inside tracing to avoid control flow issues)
694
+ if self._config.check_numerical_stability and not torch.jit.is_tracing() and not self._onnx_mode:
695
+ if attn_kpm.all(dim=1).any().item():
696
+ raise RuntimeError("attn_kpm masks all keys for at least one sample.")
697
+
698
+ # Create attention mask based on mode (ONNX vs flex_attention)
699
+ # Each path converts attn_kpm (True=IGNORE) to the format expected by the attention mechanism
700
+ block_mask: Optional[BlockMask] = None
701
+ sdpa_attn_mask: Optional[torch.Tensor] = None
702
+
703
+ if self._onnx_mode:
704
+ # ONNX mode: use SDPA with simple boolean mask
705
+ # For ONNX export, images are padded to square by InferenceWrapper,
706
+ # so typically no masking is needed. But support it for completeness.
707
+ if attn_kpm is not None:
708
+ # Invert semantics: attn_kpm True=IGNORE -> sdpa_attn_mask True=ATTEND
709
+ # forward_sdpa() passes this boolean mask straight to SDPA (True=ATTEND,
710
+ # False=IGNORE); no additive -inf conversion, so the Flash/efficient backend stays in use.
711
+ # Always apply (no .any() guard) so the mask branch is captured during export.
712
+ # An all-False attn_kpm produces an all-True sdpa_attn_mask (all-attend),
713
+ # which is mathematically identical to no mask.
714
+ sdpa_attn_mask = ~attn_kpm
715
+ else:
716
+ # Normal mode: use flex_attention with BlockMask
717
+ # _create_block_mask handles the inversion internally:
718
+ # attend_mask = ~key_padding_mask (True=ATTEND)
719
+ # BlockMask allows attention where attend_mask[q] & attend_mask[kv] is True
720
+ # No .any() guard: it forces a GPU->CPU sync every forward and with
721
+ # letterboxed data the mask is almost always non-empty anyway. An
722
+ # all-False attn_kpm yields an all-attend BlockMask (same result).
723
+ if attn_kpm is not None:
724
+ block_mask = self._create_block_mask(attn_kpm, x.size(1))
725
+
726
+ # Transformer blocks with optional selective gradient checkpointing
727
+ # checkpoint_every_n_layers controls granularity: 1=all, 2=every 2nd, 4=every 4th
728
+ checkpoint_interval = getattr(self._config, 'checkpoint_every_n_layers', 1)
729
+ for idx, block in enumerate(self.blocks):
730
+ should_checkpoint = (
731
+ self._config.gradient_checkpointing
732
+ and self.training
733
+ and (idx % checkpoint_interval == 0)
734
+ )
735
+
736
+ if self._onnx_mode:
737
+ # ONNX mode: use SDPA path (no checkpointing during export)
738
+ x = block.forward_sdpa(x, attn_mask=sdpa_attn_mask)
739
+ elif should_checkpoint:
740
+ # use_reentrant=False is recommended for PyTorch 2.x and works better with
741
+ # complex inputs like attention masks. It's also more memory efficient.
742
+ # See: https://pytorch.org/docs/stable/checkpoint.html
743
+
744
+ def create_block_forward(b, mask):
745
+ """Create a closure that captures block and mask for checkpointing."""
746
+ def block_forward(hidden_states):
747
+ return b(hidden_states, block_mask=mask)
748
+ return block_forward
749
+
750
+ x = torch.utils.checkpoint.checkpoint(
751
+ create_block_forward(block, block_mask),
752
+ x,
753
+ use_reentrant=False
754
+ )
755
+ else:
756
+ x = block(x, block_mask=block_mask)
757
+ # Final norm
758
+ x = self.norm(x)
759
+ # Use CLS token for classification
760
+ cls_output = x[:, 0]
761
+ # Predictions (ratings are included as tags in the vocabulary)
762
+ tag_logits = self.tag_head(cls_output)
763
+
764
+ # Monitor for numerical issues (optional, controlled by config)
765
+ if self._config.check_numerical_stability:
766
+ self._check_numerical_stability(tag_logits)
767
+
768
+ # Clamp logits for inference/export outputs only. During training the
769
+ # clamp is skipped: torch.clamp has zero gradient outside the bounds,
770
+ # which would kill the learning signal exactly for confidently-wrong
771
+ # predictions. The loss (loss_functions.py) computes in fp32 with its
772
+ # own log/exp clamps, so unclamped training logits are numerically safe.
773
+ if self._config.logit_clamp_value is not None and not self.training:
774
+ clamp_val = self._config.logit_clamp_value
775
+ tag_logits = torch.clamp(tag_logits, min=-clamp_val, max=clamp_val)
776
+
777
+ return {
778
+ 'tag_logits': tag_logits,
779
+ 'logits': tag_logits
780
+ }
781
+
782
+ def cleanup(self):
783
+ """Explicitly release GPU memory and clear cached tensors."""
784
+ import gc
785
+
786
+ # Move all parameters to CPU to release GPU memory
787
+ self.cpu()
788
+
789
+ # Clear any cached gradients
790
+ for param in self.parameters():
791
+ if param.grad is not None:
792
+ param.grad = None
793
+
794
+ # Force garbage collection
795
+ gc.collect()
796
+
797
+ # Clear CUDA cache if available
798
+ if torch.cuda.is_available():
799
+ # Synchronize before clearing cache to ensure all GPU operations complete
800
+ torch.cuda.synchronize()
801
+ torch.cuda.empty_cache()
802
+
803
+ def __del__(self):
804
+ """Cleanup on deletion."""
805
+ try:
806
+ self.cleanup()
807
+ except Exception:
808
+ pass
809
+
810
+ def __enter__(self):
811
+ """Context manager support."""
812
+ return self
813
+
814
+ def __exit__(self, exc_type, exc_val, exc_tb):
815
+ """Ensure cleanup on exit."""
816
+ self.cleanup()
817
+ return False
818
+
819
+
820
+ def create_model(config: Optional[VisionTransformerConfig] = None, **kwargs) -> BaseTagger:
821
+ """Create the ViT tagger model (SimplifiedTagger) from configuration.
822
+
823
+ Args:
824
+ config: VisionTransformerConfig instance. If provided, config fields in kwargs are ignored.
825
+ **kwargs: Alternative to config - specify config fields as keyword arguments.
826
+ See VisionTransformerConfig for details on each parameter.
827
+
828
+ Returns:
829
+ SimplifiedTagger instance with the specified configuration.
830
+
831
+ Examples:
832
+ # Using config object
833
+ config = VisionTransformerConfig(image_size=512, num_tags=10000)
834
+ model = create_model(config=config)
835
+
836
+ # Using kwargs
837
+ model = create_model(image_size=512, num_tags=10000)
838
+ """
839
+ # Pop the legacy architecture selector so it doesn't reach the config-field filter
840
+ # below. Only the ViT path (SimplifiedTagger) is supported now; the value is ignored.
841
+ kwargs.pop('architecture_type', None)
842
+
843
+ if config is None:
844
+ # Get the names of the fields in the VisionTransformerConfig dataclass
845
+ config_fields = {f.name for f in fields(VisionTransformerConfig)}
846
+
847
+ # Filter kwargs to only include keys that are in the config_fields
848
+ filtered_kwargs = {k: v for k, v in kwargs.items() if k in config_fields}
849
+
850
+ # Warn if kwargs contains invalid keys (excluding already-extracted keys)
851
+ invalid_keys = set(kwargs.keys()) - config_fields
852
+ if invalid_keys:
853
+ warnings.warn(
854
+ f"Ignoring unknown configuration parameters: {sorted(invalid_keys)}. "
855
+ f"Valid parameters: {sorted(config_fields)}"
856
+ )
857
+
858
+ config = VisionTransformerConfig(**filtered_kwargs)
859
+
860
+ return SimplifiedTagger(config)
preprocessing.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "normalize_mean": [
3
+ 0.5,
4
+ 0.5,
5
+ 0.5
6
+ ],
7
+ "normalize_std": [
8
+ 0.5,
9
+ 0.5,
10
+ 0.5
11
+ ],
12
+ "image_size": 448,
13
+ "patch_size": 16,
14
+ "color_order": "RGB",
15
+ "pad_color": [
16
+ 114,
17
+ 114,
18
+ 114
19
+ ],
20
+ "resize_filter": "LANCZOS",
21
+ "resize_mode": "letterbox_downscale_only",
22
+ "upscale": false,
23
+ "exif_transpose": true,
24
+ "alpha_composite": true,
25
+ "input_layout": "NCHW",
26
+ "padding_mask_true_means": "padding",
27
+ "output_activation": "sigmoid",
28
+ "skip_indices": [
29
+ 0,
30
+ 1
31
+ ]
32
+ }
release_manifest.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "release": "OppaiOracle experimental-X1.1",
3
+ "repo_id": "Grio43/OppaiOracle-experimental-X1.1",
4
+ "created_at": "2026-10-12T09:59:58.688810+00:00",
5
+ "image_size": 448,
6
+ "epoch": 9,
7
+ "step": 55719,
8
+ "source_checkpoint_sha256": "d400c2e460e7287357f86083f84c187790a45b1d47985e908c52825373f888c3",
9
+ "validated_onnx_source_sha256": "c5fcab0fb53e07aee437f3ef29e19a1e60b3dc18380048fa6d99baf35fa6c8f3",
10
+ "onnx_graph_sha256": "bbb2fd38ed9fdfe5977b0fd79ca2c7820a1eab5839b416aab5579109e6c0fa74",
11
+ "onnx_change": "descriptive metadata only; graph identical to fully evaluated artifact",
12
+ "safetensors": "all 332 FP32 tensors exact round-trip; no optimizer or pickle",
13
+ "model_sha256": "49ba0f66f83ba88a101b6edac54ec39f0a12eab66743e13ae42efc3570745bf7",
14
+ "safetensors_sha256": "ec07d0034f48ba3ceff8624993baf3ea30fa3a2e3cc8908682eb3e4f84eda766",
15
+ "vocab_sha256": "e3e6ccc137d9152d8a17fbd19e0c1ae595b2317a6c55893fd25779620b4b8087",
16
+ "storage": "Hugging Face Hub / Xet",
17
+ "license": "educational-use-only",
18
+ "download_count_query_file": "config.json",
19
+ "validation": "validation.json"
20
+ }
requirements-gpu.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ numpy>=1.26
2
+ Pillow>=10.0
3
+ onnxruntime-gpu>=1.20
4
+ huggingface_hub>=1.0
requirements-torch.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ numpy>=1.26
2
+ Pillow>=10.0
3
+ torch>=2.5
4
+ safetensors>=0.4.5
5
+ huggingface_hub>=1.0
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ numpy>=1.26
2
+ Pillow>=10.0
3
+ onnxruntime>=1.20
4
+ huggingface_hub>=1.0
selected_tags.csv ADDED
The diff for this file is too large to render. See raw diff
 
thresholds.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "default": 0.7085427045822144,
3
+ "source": "200-point micro-F1 grid on 13,569 shared development tags; not final deployment calibration",
4
+ "score_calibration": false,
5
+ "training_diagnostic_threshold": 0.7927
6
+ }
training_buckets.csv ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ bucket,epoch7,epoch8,epoch9,epoch7_to_9_points
2
+ 300-499,37.72737681865692,37.53387928009033,37.342289090156555,-0.3850877285003662
3
+ 500-999,61.54675483703613,61.733537912368774,61.75047755241394,0.20372271537780762
4
+ 1000-4999,57.105010747909546,57.1607232093811,57.215815782547,0.11080503463745117
5
+ 5000-9999,52.05754041671753,52.18653082847595,52.18939185142517,0.1318514347076416
6
+ 10000+,57.25409388542175,57.3200523853302,57.348477840423584,0.09438395500183105
training_epochs.csv ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ epoch,validation_time,exact_map,binned_map,macro_f1,micro_f1,val_loss,train_loss,samples,skipped
2
+ 1,2026-09-13T18:19:02.022533-04:00,55.92384934425354,55.73147535324097,43.22007596492767,64.86003398895264,0.001663603587076068,0.0016142689855769277,30000,0
3
+ 2,2026-09-14T04:16:38.378663-04:00,56.59424662590027,56.39519691467285,44.07239556312561,65.32300114631653,0.0016429137904196978,0.0015640060883015394,30000,0
4
+ 3,2026-09-14T13:52:19.915251-04:00,56.98235034942627,56.77585005760193,44.495999813079834,65.55598378181458,0.001634233514778316,0.0015406454913318157,30000,0
5
+ 4,2026-09-14T23:42:07.983341-04:00,57.2227418422699,57.01723098754883,44.791001081466675,65.70025682449341,0.0016256555682048202,0.0015247202245518565,30000,0
6
+ 5,2026-09-15T09:26:10.020244-04:00,57.34878182411194,57.14470148086548,44.962278008461,65.80770015716553,0.0016207325970754027,0.0015117722796276212,30000,0
7
+ 6,2026-09-15T19:23:48.602481-04:00,57.48898983001709,57.290494441986084,45.18154859542847,65.89643359184265,0.0016172741306945682,0.0015005783643573523,30000,0
8
+ 7,2026-09-16T05:10:51.323995-04:00,57.589566707611084,57.38818645477295,45.37670016288757,66.06296300888062,0.0016201940597966313,0.00149048981256783,30000,0
9
+ 8,2026-09-16T14:55:16.420660-04:00,57.68805742263794,57.48524069786072,45.54440975189209,66.12292528152466,0.0016188551671802998,0.0014811164001002908,30000,0
10
+ 9,2026-09-17T01:43:38.368238-04:00,57.71656036376953,57.506269216537476,45.742398500442505,66.21212959289551,0.0016158618964254856,0.0014653843827545643,30000,0
training_metadata.json ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "source_checkpoint_sha256": "d400c2e460e7287357f86083f84c187790a45b1d47985e908c52825373f888c3",
3
+ "parent_release": "Grio43/OppaiOracle-experimental-X1",
4
+ "parent_epoch": 21,
5
+ "parent_step": 127974,
6
+ "phase": 2,
7
+ "epoch": 9,
8
+ "step": 55719,
9
+ "completed_at": "2026-09-17T01:43:41.793971",
10
+ "model": {
11
+ "architecture_type": "vit",
12
+ "hidden_size": 896,
13
+ "num_hidden_layers": 18,
14
+ "num_attention_heads": 14,
15
+ "intermediate_size": 2320,
16
+ "image_size": 448,
17
+ "patch_size": 16,
18
+ "num_channels": 3,
19
+ "hidden_dropout_prob": 0.0,
20
+ "pos_dropout": 0.0,
21
+ "attention_dropout": 0.0,
22
+ "qk_norm": true,
23
+ "layer_scale_init": 0.1,
24
+ "drop_path_rate": 0.15,
25
+ "initializer_range": 0.02,
26
+ "layer_norm_eps": 1e-06,
27
+ "use_fp32_layernorm": false,
28
+ "use_flex_attention": true,
29
+ "attention_bias": true,
30
+ "flex_block_size": 128,
31
+ "token_ignore_threshold": 0.9,
32
+ "num_labels": 14996,
33
+ "num_groups": 20,
34
+ "tags_per_group": 10000,
35
+ "gradient_checkpointing": false,
36
+ "checkpoint_every_n_layers": 1
37
+ },
38
+ "training": {
39
+ "memory_format": "contiguous",
40
+ "num_epochs": 9,
41
+ "learning_rate": 1.5e-05,
42
+ "lr_scaling_mode": "sqrt",
43
+ "lr_base_batch_size": 256,
44
+ "weight_decay": 0.05,
45
+ "gradient_accumulation_steps": 21,
46
+ "world_size": 1,
47
+ "optimizer": "adamw8bit",
48
+ "adam_beta1": 0.9,
49
+ "adam_beta2": 0.999,
50
+ "adan_beta3": 0.99,
51
+ "adam_epsilon": 1e-07,
52
+ "scheduler": "wsd",
53
+ "warmup_epochs": 1,
54
+ "warmup_steps": null,
55
+ "wsd_cooldown_fraction": 0.15,
56
+ "wsd_plateau_enabled": false,
57
+ "fp32_head_optimizer": true,
58
+ "num_cycles": 1,
59
+ "cycle_decay": 0.9,
60
+ "lr_end": 0.0,
61
+ "use_amp": true,
62
+ "amp_dtype": "bfloat16",
63
+ "enable_anomaly_detection": false,
64
+ "max_grad_norm": 1.0,
65
+ "gradient_clipping": {
66
+ "enabled": true,
67
+ "max_norm": 1.0
68
+ },
69
+ "save_steps": 1500,
70
+ "save_total_limit": 3,
71
+ "save_best_only": false,
72
+ "resume_from": "latest",
73
+ "eval_steps": 0,
74
+ "logging_steps": 50,
75
+ "resume_reseed": true,
76
+ "allow_unverified_vocab_resume": false,
77
+ "on_lr_schedule_change": "error",
78
+ "tag_loss": {
79
+ "alpha": 1.0,
80
+ "gamma_neg": 3.0,
81
+ "gamma_neg_override": null,
82
+ "gamma_pos": 0.0,
83
+ "detach_focal_weight": true,
84
+ "label_smoothing": 0.0,
85
+ "clip": 0.05,
86
+ "class_weights": null,
87
+ "class_weight_strategy": null,
88
+ "class_weight_clip_min": 0.05,
89
+ "class_weight_clip_max": 5.0,
90
+ "class_weight_beta": 0.9999
91
+ },
92
+ "phase": 2,
93
+ "asl_schedule": {
94
+ "enabled": false,
95
+ "min_dwell_epochs": 3,
96
+ "phase1": {
97
+ "gamma_neg_min": 3.0,
98
+ "gamma_neg_max": 3.0,
99
+ "hold_epochs": 8
100
+ },
101
+ "phase2": {
102
+ "gamma_neg_min": 3.0,
103
+ "gamma_neg_max": 3.0,
104
+ "hold_epochs": 2
105
+ },
106
+ "phase3": {
107
+ "gamma_neg_min": 3.0,
108
+ "gamma_neg_max": 3.0,
109
+ "hold_epochs": 0
110
+ }
111
+ },
112
+ "asl_telemetry": {
113
+ "enabled": true,
114
+ "interval_updates": 100,
115
+ "log_every_updates": 500,
116
+ "ema_beta": 0.98,
117
+ "topk_hard": 10,
118
+ "num_deciles": 10,
119
+ "hist_min": 0.05,
120
+ "hist_max": 0.95,
121
+ "hist_bins": 18,
122
+ "watch_band_low": 0.2,
123
+ "watch_band_high": 0.5,
124
+ "epr_alarm_rel_drop": 0.05,
125
+ "epr_alarm_window_epochs": 2,
126
+ "exclude_rating_tags": true,
127
+ "sibling_groups_path": "./configs/confusable_groups.json",
128
+ "shadow_controller_enabled": true,
129
+ "shadow_lambda": 0.05,
130
+ "shadow_delta_p_target": 0.2
131
+ },
132
+ "device": "cuda",
133
+ "use_compile": true,
134
+ "compile_mode": "max-autotune-no-cudagraphs",
135
+ "compile_fullgraph": false,
136
+ "compile_dynamic": true,
137
+ "use_tensorboard": true,
138
+ "seed": 42,
139
+ "deterministic": false,
140
+ "benchmark": true,
141
+ "selection_metric": "val_mAP",
142
+ "early_stopping_patience": 4,
143
+ "early_stopping_threshold": 5e-07,
144
+ "early_stopping_burn_in_epochs": 2,
145
+ "early_stopping_burn_in_strategy": "median",
146
+ "early_stopping_mode": "advise",
147
+ "early_stopping_min_delta": 0.003,
148
+ "early_stopping_plateau_slope_threshold": 0.0001,
149
+ "early_stopping_plateau_noise_multiplier": 2.0,
150
+ "early_stopping_window": 5,
151
+ "early_stopping_confirm": 2,
152
+ "early_stopping_min_epochs": 10,
153
+ "early_stopping_regression_delta": 0.006,
154
+ "early_stopping_regression_confirm": 2,
155
+ "early_stopping_overfit_confirm": 3,
156
+ "early_stopping_overfit_window": 8,
157
+ "early_stopping_overfit_min_relative_change": 0.01,
158
+ "early_stopping_overfit_noise_multiplier": 2.0,
159
+ "early_stopping_overfit_ranking_tolerance": 0.0001,
160
+ "wsd_deterioration_actions": {
161
+ "peak_decline": "preserve",
162
+ "overfit_loss": "warn"
163
+ },
164
+ "early_stopping_phases": {
165
+ "phase1": {
166
+ "mode": null,
167
+ "window": 5,
168
+ "confirm": 2,
169
+ "min_epochs": 10,
170
+ "plateau_slope_threshold": null,
171
+ "plateau_noise_multiplier": null
172
+ },
173
+ "phase2": {
174
+ "mode": "advise",
175
+ "window": 5,
176
+ "confirm": 2,
177
+ "min_epochs": 10,
178
+ "plateau_slope_threshold": null,
179
+ "plateau_noise_multiplier": null
180
+ },
181
+ "phase3": {
182
+ "mode": "off",
183
+ "window": null,
184
+ "confirm": null,
185
+ "min_epochs": null,
186
+ "plateau_slope_threshold": null,
187
+ "plateau_noise_multiplier": null
188
+ }
189
+ },
190
+ "use_distillation": false,
191
+ "distillation_alpha": 0.7,
192
+ "distillation_temperature": 3.0
193
+ },
194
+ "data_recipe": {
195
+ "vocab_min_frequency": 550,
196
+ "vocab_min_frequency_by_category": {
197
+ "artist": 550,
198
+ "char": 550,
199
+ "copyright": 550,
200
+ "gen": 450,
201
+ "meta": 550
202
+ },
203
+ "random_flip_prob": 0.5,
204
+ "horizontal_flip_exclude_tags": [
205
+ "gen:left-handed",
206
+ "gen:left-to-right_manga",
207
+ "gen:right-over-left_kimono",
208
+ "gen:right-to-left_comic"
209
+ ],
210
+ "color_jitter_enabled": true,
211
+ "color_jitter_brightness": 0.22,
212
+ "color_jitter_brightness_p": 0.5,
213
+ "color_jitter_contrast": 0.15,
214
+ "color_jitter_contrast_p": 0.5,
215
+ "color_jitter_saturation": 0.06,
216
+ "color_jitter_saturation_p": 0.5,
217
+ "random_erasing_enabled": false,
218
+ "random_erasing_p": 0.35,
219
+ "random_erasing_scale_min": 0.007,
220
+ "random_erasing_scale_max": 0.03,
221
+ "random_erasing_ratio_min": 0.3,
222
+ "random_erasing_ratio_max": 2.0,
223
+ "random_rotation_enabled": true,
224
+ "random_rotation_p": 0.3,
225
+ "random_rotation_min_degrees": 2.0,
226
+ "random_rotation_max_degrees": 5.0,
227
+ "gaussian_blur_enabled": true,
228
+ "gaussian_blur_p": 0.15,
229
+ "gaussian_blur_kernel_size": 3,
230
+ "gaussian_blur_sigma_min": 0.1,
231
+ "gaussian_blur_sigma_max": 1.0
232
+ },
233
+ "train_images": 6239823,
234
+ "eval_images": 30000,
235
+ "dataset_maintenance": {
236
+ "danbooru_metadata_sync": "2026-08",
237
+ "deprecated_tags_removed": true,
238
+ "renamed_tags_updated_to_current_names": true,
239
+ "provenance": "model author"
240
+ },
241
+ "metrics": {
242
+ "train_loss": 0.0014653843827545643,
243
+ "val_loss": 0.0016158618964254856,
244
+ "val_f1_macro": 0.45742398500442505,
245
+ "val_f1_micro": 0.6621212959289551,
246
+ "val_mAP": 0.5750626921653748,
247
+ "selection_metric": "val_mAP"
248
+ },
249
+ "scheduler": {
250
+ "warmup_steps": 6191,
251
+ "total_steps": 55719,
252
+ "cooldown_fraction": 0.15,
253
+ "budget_cooldown_start": 48451,
254
+ "min_lr": 0.0,
255
+ "base_max_lr": 2.9764702249476646e-05,
256
+ "cooldown_start": 48451,
257
+ "cooldown_steps": 7268,
258
+ "cooldown_reason": "budget",
259
+ "cooldown_best_metric": 0.5750626921653748,
260
+ "base_lrs": [
261
+ 2.9764702249476646e-05,
262
+ 2.9764702249476646e-05
263
+ ],
264
+ "last_epoch": 55719,
265
+ "_step_count": 55720,
266
+ "_is_initial": false,
267
+ "_get_lr_called_within_step": false,
268
+ "_last_lr": [
269
+ 0.0,
270
+ 0.0
271
+ ]
272
+ }
273
+ }
validation.json ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "X1 448 final epoch 9",
3
+ "checkpoint": {
4
+ "checkpoint": "checkpoint_epoch_9_step_55719.pt",
5
+ "checkpoint_sha256": "d400c2e460e7287357f86083f84c187790a45b1d47985e908c52825373f888c3",
6
+ "epoch": 9,
7
+ "step": 55719,
8
+ "scheduler": {
9
+ "warmup_steps": 6191,
10
+ "total_steps": 55719,
11
+ "cooldown_fraction": 0.15,
12
+ "budget_cooldown_start": 48451,
13
+ "min_lr": 0.0,
14
+ "base_max_lr": 2.9764702249476646e-05,
15
+ "cooldown_start": 48451,
16
+ "cooldown_steps": 7268,
17
+ "cooldown_reason": "budget",
18
+ "cooldown_best_metric": 0.5750626921653748,
19
+ "base_lrs": [
20
+ 2.9764702249476646e-05,
21
+ 2.9764702249476646e-05
22
+ ],
23
+ "last_epoch": 55719,
24
+ "_step_count": 55720,
25
+ "_is_initial": false,
26
+ "_get_lr_called_within_step": false,
27
+ "_last_lr": [
28
+ 0.0,
29
+ 0.0
30
+ ]
31
+ },
32
+ "preprocessing": {
33
+ "normalize_mean": [
34
+ 0.5,
35
+ 0.5,
36
+ 0.5
37
+ ],
38
+ "normalize_std": [
39
+ 0.5,
40
+ 0.5,
41
+ 0.5
42
+ ],
43
+ "image_size": 448,
44
+ "patch_size": 16,
45
+ "color_order": "RGB"
46
+ }
47
+ },
48
+ "model_sha256": "c5fcab0fb53e07aee437f3ef29e19a1e60b3dc18380048fa6d99baf35fa6c8f3",
49
+ "native_image_size": 448,
50
+ "samples": 30000,
51
+ "sample_sha256": "20a60e7a747cdf9a698a464c9d324612aad2ca6b43a541db0813554eb9af8694",
52
+ "shared_tags": 13569,
53
+ "supported_shared_tags": 13394,
54
+ "binned_mAP": 0.5851927682386426,
55
+ "exact_mAP": 0.5875134140920024,
56
+ "best_grid_micro_f1": 0.6864101262842809,
57
+ "best_grid_threshold": 0.7085427045822144,
58
+ "precision_at_best_grid": 0.7065020665350971,
59
+ "recall_at_best_grid": 0.6674293596992867,
60
+ "all_supported_tags": 14767,
61
+ "all_tag_exact_mAP": 0.5774638913821051,
62
+ "all_tag_binned_mAP": 0.5750950549967043,
63
+ "inference_seconds": 347.91008079855237,
64
+ "total_seconds": 487.7315649986267,
65
+ "preprocessing": "FP32 RGB, native resolution, downscale-only centered LANCZOS letterbox, mean/std 0.5, pad 114, True=padding",
66
+ "verified_sidecars": 30000
67
+ }
vocabulary.json ADDED
The diff for this file is too large to render. See raw diff