logit-classifier 0.3.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/FINDINGS.md +155 -51
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/PKG-INFO +59 -35
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/README.md +58 -34
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/examples/quickstart.py +6 -2
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/pyproject.toml +6 -2
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/_version.py +1 -1
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/backends/_torch_window.py +2 -2
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/backends/base.py +2 -1
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/backends/comfy_clip.py +34 -18
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/backends/hf.py +121 -27
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/calibrate.py +13 -1
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/classifier.py +9 -12
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/config.py +14 -3
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/deps.py +7 -4
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/labels.py +0 -5
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/prompt.py +2 -2
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/schema.py +1 -1
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/scoring.py +2 -2
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/service.py +0 -4
- logit_classifier-0.5.0/src/logit_classifier/toolkit/__init__.py +7 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/toolkit/comfyui/__init__.py +13 -13
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/toolkit/comfyui/generate.py +2 -18
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/toolkit/comfyui/scopes.py +8 -21
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/toolkit/comfyui/tagger.py +72 -67
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/toolkit/tags.py +60 -41
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/vision.py +19 -14
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/web/index.html +0 -2
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests/test_toolkit.py +156 -152
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests/test_unit.py +428 -39
- logit_classifier-0.5.0/tests-AB/ab_attention_kernel.py +145 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/ab_branch_packing.py +6 -6
- logit_classifier-0.5.0/tests-AB/ab_frozen_prefix_cache.py +125 -0
- logit_classifier-0.5.0/tests-AB/ab_prior_traffic.py +139 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/ab_temperature.py +6 -1
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/benchmark.py +9 -3
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/comfy_env.py +3 -1
- logit_classifier-0.3.0/src/logit_classifier/tags.py +0 -36
- logit_classifier-0.3.0/src/logit_classifier/toolkit/__init__.py +0 -5
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/.gitignore +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/LICENSE +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/examples/compare_models.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/examples/http_client.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/examples/images.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/examples/own_backend.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/examples/question_types.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/__init__.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/__main__.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/backends/__init__.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/cli.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/errors.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/py.typed +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/toolkit/comfyui/classifier.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/src/logit_classifier/toolkit/comfyui/progress.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests/fixtures/banking77_test.json +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests/fixtures/eval_set.json +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests/fixtures/many_options_request.json +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests/fixtures/quickstart_request.json +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests/test_model.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/ab_determinism_scope.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/ab_env.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/ab_math_sdp_reduction.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/ab_multi_label.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/ab_noul_wording.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/banking77.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/evaluate.py +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/inputs/_full.png +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/inputs/_sheet.png +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/inputs/low-L.png +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/inputs/low-R.png +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/inputs/mid-L.png +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/inputs/mid-R.png +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/inputs/top-L.png +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/inputs/top-R.png +0 -0
- {logit_classifier-0.3.0 → logit_classifier-0.5.0}/tests-AB/tune_groups.py +0 -0
|
@@ -56,9 +56,10 @@ options, but that is the split path defect below, not a gain. `ab_temperature.py
|
|
|
56
56
|
## The prior corrects letters, not fixed positions
|
|
57
57
|
|
|
58
58
|
The running prior subtracts the model's standing preference for one letter over another.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
correcting them moves one request's answer with unrelated requests.
|
|
59
|
+
Some positions carry a fixed meaning instead. The escape label always means none of these,
|
|
60
|
+
a score's letters always name the same rungs, and a noul's `A` is always yes. Their mean mass
|
|
61
|
+
follows the traffic, so correcting them moves one request's answer with unrelated requests.
|
|
62
|
+
The prior therefore covers the lettered options of choice and member branches only.
|
|
62
63
|
|
|
63
64
|
Banking77, 462 held out rows, Qwen3-VL-4B-Instruct at its fitted temperature.
|
|
64
65
|
|
|
@@ -72,6 +73,41 @@ The fix does not regress calibration. The accuracy gain is four rows in 462, nea
|
|
|
72
73
|
of this sample. The prior still costs calibration against no prior, because the shipped
|
|
73
74
|
temperature was fitted with the prior off. `banking77.py`, with and without `--no-prior`.
|
|
74
75
|
|
|
76
|
+
A noul under the prior shows the same coupling. Banking77 statements ("This message is about
|
|
77
|
+
<intent>", true for the row's own intent) ran as request streams at four true rates, the first
|
|
78
|
+
32 requests of each stream unscored.
|
|
79
|
+
|
|
80
|
+
| Stream | Prior | AUC | Recall at 0.5 | False positives at 0.5 | Recall at 0.9999 |
|
|
81
|
+
|---|---|---|---|---|---|
|
|
82
|
+
| 50% true | off | 0.9538 | 0.891 | 0.065 | 0.050 |
|
|
83
|
+
| 50% true | on | 0.9531 | 0.891 | 0.065 | 0.059 |
|
|
84
|
+
| 80% true | off | 0.9419 | 0.872 | 0.091 | 0.055 |
|
|
85
|
+
| 80% true | on | 0.9410 | 0.848 | 0.068 | 0.006 |
|
|
86
|
+
| 95% true | off | 0.9428 | 0.878 | 0.083 | 0.056 |
|
|
87
|
+
| 95% true | on | 0.9409 | 0.821 | 0.000 | 0.005 |
|
|
88
|
+
| 20% true | off | 0.9439 | 0.878 | 0.078 | 0.000 |
|
|
89
|
+
| 20% true | on | 0.9435 | 0.878 | 0.102 | 0.122 |
|
|
90
|
+
|
|
91
|
+
The AUC does not move, since the correction is one shift of the yes/no gap. The operating point
|
|
92
|
+
moves with the traffic instead. A request took 126.6 ms with the prior off and 126.4 ms with it
|
|
93
|
+
on, so the prior has no speed cost or gain. `ab_prior_traffic.py`.
|
|
94
|
+
|
|
95
|
+
ContextAnchoredTileRefine's tags bench kept 8.2 correct tags per tile with the prior off and
|
|
96
|
+
5.4 with it on, over 45 judged tiles on 15 pictures. The bench ran on 00518c6, before 3ac0294
|
|
97
|
+
took the prior off nouls, so its prior also corrected the verify and locate nouls. The loss grew
|
|
98
|
+
over the later tiles, which the earlier tiles' mostly true verify answers had trained.
|
|
99
|
+
|
|
100
|
+
| Tile in run order | Correct tags, prior off | Correct tags, prior on |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| first | 114 | 111 |
|
|
103
|
+
| second | 110 | 80 |
|
|
104
|
+
| third | 146 | 52 |
|
|
105
|
+
|
|
106
|
+
The first tile kept the same count on 13 of 15 pictures. It lost 2 tags on dragon4k and 1 on
|
|
107
|
+
market4k, both under the long prompt. `ab_tags_bench.py` in
|
|
108
|
+
ContextAnchoredTileRefine's `tests-AB/`, arms `prior-off` and `prior-on`, added in its commit
|
|
109
|
+
bf6c7ba.
|
|
110
|
+
|
|
75
111
|
## Option order helps above 52 options and nowhere else
|
|
76
112
|
|
|
77
113
|
| Options | Letterings | Accuracy | ECE |
|
|
@@ -160,7 +196,7 @@ the CUDA attention path, only one of them changes a logit on either shipped mode
|
|
|
160
196
|
|---|---|---|
|
|
161
197
|
| `cudnn.benchmark` | 0.0 | 0.0 |
|
|
162
198
|
| `cudnn.deterministic` | 0.0 | 0.0 |
|
|
163
|
-
| `allow_bf16_reduced_precision_reduction` | **0.
|
|
199
|
+
| `allow_bf16_reduced_precision_reduction` | **0.375** | **1.125** |
|
|
164
200
|
| float32 matmul precision | 0.0 | 0.0 |
|
|
165
201
|
|
|
166
202
|
Max absolute logit shift against the fully pinned run, window removed so the process
|
|
@@ -175,9 +211,10 @@ logits, and a run under a hostile host state matches a run under a pinned one, b
|
|
|
175
211
|
gap of 0.0 on both models.
|
|
176
212
|
|
|
177
213
|
`allow_fp16_bf16_reduction_math_sdp` is held for a different reason and the sweep above
|
|
178
|
-
could not reach it.
|
|
179
|
-
|
|
180
|
-
|
|
214
|
+
could not reach it. The math backend is **off** while `sdpa_kernel` holds the backends the
|
|
215
|
+
probe found. When `_usable_attention_backends` returns nothing, the `FALLBACK_ATTENTION` block
|
|
216
|
+
puts math last. So this setting is live only on a pass that flash, cuDNN and memory efficient
|
|
217
|
+
attention all reject. ComfyUI turns it **on** at import, at
|
|
181
218
|
`comfy/model_management.py:569`, so inside ComfyUI that path would run with reduced
|
|
182
219
|
precision reductions.
|
|
183
220
|
|
|
@@ -214,7 +251,7 @@ Every row in a chunk is left-padded to that chunk's longest suffix, and each pad
|
|
|
214
251
|
token costs a full forward plus attention over the whole prefix. Taking branches in the
|
|
215
252
|
order the caller sent them drags short branches to the longest width.
|
|
216
253
|
|
|
217
|
-
| Request shape | ms | peak
|
|
254
|
+
| Request shape | ms | peak GiB | suffix tokens | chunks | |
|
|
218
255
|
|---|---|---|---|---|---|
|
|
219
256
|
| mixed, four choices plus 12 nouls plus 4 scores | 2055.6 | 13.66 | 14600 | 1 | request order |
|
|
220
257
|
| | **623.9** | **10.66** | **2614** | 3 | packed |
|
|
@@ -226,7 +263,7 @@ order the caller sent them drags short branches to the longest width.
|
|
|
226
263
|
| 30 three option choices | 396 | 8.99 | 2160 | 1 | both, bitwise identical |
|
|
227
264
|
| 12 twenty option choices | 586 | 9.24 | 3960 | 1 | both, bitwise identical |
|
|
228
265
|
|
|
229
|
-
3.29x and 5.06x on the two shapes that mix widths, each giving back about 3
|
|
266
|
+
3.29x and 5.06x on the two shapes that mix widths, each giving back about 3 GiB. Nothing
|
|
230
267
|
at all on the five that do not, which is the point. The sort is stable, so equal lengths
|
|
231
268
|
keep the caller's order and the grouping stays a pure function of the request.
|
|
232
269
|
|
|
@@ -241,12 +278,12 @@ width, which is every choice question, and moved its logits for no gain.
|
|
|
241
278
|
|
|
242
279
|
| ceiling | mixed shape, 800 token prefix | mixed widths, long prefix |
|
|
243
280
|
|---|---|---|
|
|
244
|
-
| 512 | 610.9 ms, 9.91
|
|
245
|
-
| 1024 | 608.3 ms, 10.47
|
|
246
|
-
| **2048** | **626.0 ms, 10.66
|
|
247
|
-
| 4096 | 793.5 ms, 11.03
|
|
248
|
-
| 8192 | 1119.4 ms, 11.84
|
|
249
|
-
| none | 2062.2 ms, 13.66
|
|
281
|
+
| 512 | 610.9 ms, 9.91 GiB, 5 chunks | 2015.7 ms, 5 chunks |
|
|
282
|
+
| 1024 | 608.3 ms, 10.47 GiB, 3 chunks | 2014.9 ms, 5 chunks |
|
|
283
|
+
| **2048** | **626.0 ms, 10.66 GiB, 3 chunks** | **2015.8 ms, 5 chunks** |
|
|
284
|
+
| 4096 | 793.5 ms, 11.03 GiB, 2 chunks | 2286.5 ms, 4 chunks |
|
|
285
|
+
| 8192 | 1119.4 ms, 11.84 GiB, 2 chunks | 2289.5 ms, 4 chunks |
|
|
286
|
+
| none | 2062.2 ms, 13.66 GiB, 1 chunk | 2299.1 ms, 4 chunks |
|
|
250
287
|
|
|
251
288
|
Anything from 512 to 2048 measured the same on both, and 4096 upward is clearly worse.
|
|
252
289
|
The value is not a knife edge. 2048 sits in the middle of the flat band and leaves the
|
|
@@ -256,44 +293,116 @@ most room before two near-equal wide branches are split apart.
|
|
|
256
293
|
|
|
257
294
|
## Throughput and where it falls off
|
|
258
295
|
|
|
259
|
-
Warm model, RTX 3090 Ti.
|
|
260
|
-
|
|
296
|
+
Warm model, RTX 3090 Ti. Each row repeats one request, so its sequence lengths are already
|
|
297
|
+
seen. Adding questions to a record is close to free up to about 8, because the fixed cost of
|
|
298
|
+
two forward passes dominates a short record.
|
|
261
299
|
|
|
262
300
|
| Questions per record | Total ms | ms per tag | Records per hour |
|
|
263
301
|
|---|---|---|---|
|
|
264
|
-
| 1 |
|
|
265
|
-
| 4 |
|
|
266
|
-
| 8 |
|
|
267
|
-
| 32 |
|
|
302
|
+
| 1 | 135 | 135 | 26,700 |
|
|
303
|
+
| 4 | 132 | 33 | 27,300 |
|
|
304
|
+
| 8 | 135 | 16.9 | 26,600 |
|
|
305
|
+
| 32 | 295 | 9.2 | 12,200 |
|
|
268
306
|
|
|
269
307
|
The state is prefilled once per record, so a longer record costs more.
|
|
270
308
|
|
|
271
|
-
| State tokens | Total ms, 8 questions | ms per tag | Records per hour |
|
|
272
|
-
|
|
273
|
-
| 172 |
|
|
274
|
-
| 556 |
|
|
275
|
-
| 2,092 |
|
|
276
|
-
| 8,236 | 1,
|
|
277
|
-
| 32,812 |
|
|
309
|
+
| State tokens | Total ms, 8 questions | ms per tag | Records per hour | Peak GiB |
|
|
310
|
+
|---|---|---|---|---|
|
|
311
|
+
| 172 | 144 | 18.0 | 24,900 | 8.36 |
|
|
312
|
+
| 556 | 199 | 24.9 | 18,100 | 8.48 |
|
|
313
|
+
| 2,092 | 494 | 62 | 7,300 | 8.92 |
|
|
314
|
+
| 8,236 | 1,805 | 226 | 2,000 | 10.23 |
|
|
315
|
+
| 32,812 | 11,453 | 1,432 | 314 | 15.23 |
|
|
316
|
+
|
|
317
|
+
The TFLOP/s count covers the weights' FLOPs only. Prefill runs at about 42 TFLOP/s from 2,348
|
|
318
|
+
to 8,492 prefilled tokens. A 428 token prefill runs at 26.3 TFLOP/s and a 33,068 token prefill
|
|
319
|
+
at 25.6. Attention work grows with the square of the state length, so the count leaves out the
|
|
320
|
+
most work on the 33,068 token row. The peak is `torch.cuda.max_memory_allocated` in GiB, weights
|
|
321
|
+
included. `benchmark.py`.
|
|
278
322
|
|
|
279
|
-
|
|
280
|
-
BF16 ceiling. Below that the time is almost all kernel launch rather than arithmetic, so a
|
|
281
|
-
428 token request spends 0.14 ms on the math. The last row falls off the ceiling for the
|
|
282
|
-
reason under Known weaknesses. `benchmark.py`.
|
|
323
|
+
## The memory efficient kernel, not cuDNN
|
|
283
324
|
|
|
284
|
-
|
|
325
|
+
Torch 2.11 ships no FlashAttention kernel for Windows, so `HFBackend` probes the backends at
|
|
326
|
+
startup, at the model's own dtype, and pins one. cuDNN builds an execution plan the first time it sees a sequence length, and served requests
|
|
327
|
+
rarely repeat a length. The memory efficient kernel has no such cost, but it rejects grouped
|
|
328
|
+
key and value heads. transformers passes grouped heads whenever a pass has no mask, which is
|
|
329
|
+
the prefix pass. So the model loads under its own attention name, which expands the heads
|
|
330
|
+
before the kernel sees them.
|
|
285
331
|
|
|
286
|
-
|
|
287
|
-
dispatcher reaches the math backend, which builds the full attention matrix. Measured on an
|
|
288
|
-
8,192 token prefill.
|
|
332
|
+
Both arms ran on one loaded model.
|
|
289
333
|
|
|
290
|
-
|
|
|
334
|
+
| | cuDNN, grouped heads | memory efficient, expanded heads |
|
|
291
335
|
|---|---|---|
|
|
292
|
-
|
|
|
293
|
-
|
|
|
336
|
+
| 40 requests of unseen length, 96 to 2,865 tokens, p50 | 1,501 ms | 335 ms |
|
|
337
|
+
| the same 40 requests repeated, p50 | 335 ms | 327 ms |
|
|
338
|
+
| image request of unseen length, 132 image tokens | 2,257 ms | 214 ms |
|
|
339
|
+
| 8,192 token state and 8 questions, unseen | 3,664 ms | 1,927 ms |
|
|
340
|
+
| 8,192 token state and 8 questions, repeated | 1,672 ms | 1,742 ms |
|
|
341
|
+
| peak memory at 8,192 tokens | 15.76 GiB | 15.76 GiB |
|
|
342
|
+
| Banking77, 462 held out rows, accuracy | 0.6255 | 0.6212 |
|
|
343
|
+
| Banking77, 462 held out rows, ECE at T=1.25 | 0.0882 | 0.0834 |
|
|
344
|
+
|
|
345
|
+
The two kernels round differently in bfloat16. Logits moved by a median of 0.25 and at most
|
|
346
|
+
1.5, and the top letter changed on 36 of 924 rows. Accuracy moved by 2 of 462 held out rows,
|
|
347
|
+
which is inside the noise of that sample, so the fitted temperature stands. The same request
|
|
348
|
+
still returns bitwise equal logits on 924 of 924 rows. `ab_attention_kernel.py`.
|
|
349
|
+
|
|
350
|
+
## One frozen prefix cache, not a copy per chunk
|
|
351
|
+
|
|
352
|
+
Each chunk used to deep copy the prefix cache and broadcast it across its rows, which stored
|
|
353
|
+
rows times the prefix in every layer for the whole forward. The prefix cache is now frozen
|
|
354
|
+
after the prefix pass, and each layer joins the broadcast prefix to the chunk's own keys and
|
|
355
|
+
values only for that layer's attention.
|
|
356
|
+
|
|
357
|
+
Both arms ran on one idle GPU, the commit before the change against the commit after.
|
|
358
|
+
|
|
359
|
+
| Request | peak before | peak after | logits |
|
|
360
|
+
|---|---|---|---|
|
|
361
|
+
| mixed, four choices plus nouls plus scores | 10.67 GiB | 8.75 GiB | bitwise identical |
|
|
362
|
+
| skewed, one wide choice beside twenty nouls | 10.95 GiB | 8.76 GiB | bitwise identical |
|
|
363
|
+
| uniform, 256 nouls at the Jev cap | 8.74 GiB | 8.39 GiB | bitwise identical |
|
|
364
|
+
| split, one 77 option choice | 8.61 GiB | 8.40 GiB | bitwise identical |
|
|
365
|
+
| tile fragments, 24 nouls | 8.63 GiB | 8.36 GiB | bitwise identical |
|
|
366
|
+
| uniform, 30 three option choices | 8.99 GiB | 8.47 GiB | bitwise identical |
|
|
367
|
+
| uniform, 12 twenty option choices | 9.24 GiB | 8.60 GiB | bitwise identical |
|
|
368
|
+
| 8,192 token state and 8 questions | 15.76 GiB | 10.24 GiB | bitwise identical |
|
|
369
|
+
| image state, top-L.png | 8.37 GiB | 8.34 GiB | bitwise identical |
|
|
370
|
+
|
|
371
|
+
The peaks in the packing and kernel sections above were measured with the copy.
|
|
372
|
+
`ab_frozen_prefix_cache.py`.
|
|
373
|
+
|
|
374
|
+
A request of 33,068 prefilled tokens took 155,903 ms at 1.9 TFLOP/s on the memory efficient
|
|
375
|
+
kernel and 52,813 ms on cuDNN while each chunk copied the prefix cache. The frozen cache brings
|
|
376
|
+
it to 11,453 ms. `benchmark.py`.
|
|
377
|
+
|
|
378
|
+
That request's prefix cache holds about 4.5 GiB. The figure is derived from the shipped config
|
|
379
|
+
rather than measured, as 147,456 bytes per token (36 layers × 8 key value heads × 128 head_dim
|
|
380
|
+
× 2 tensors × 2 bytes) times the 32,812 prefix tokens.
|
|
381
|
+
|
|
382
|
+
## One presence wording, two threshold pairs
|
|
383
|
+
|
|
384
|
+
Correct tags per tile at a pooled precision, over 905 judged tile candidates on 23 tiles,
|
|
385
|
+
from the Logit Tagger's `tests-AB/ab_presence_wording.py` (round 4).
|
|
386
|
+
|
|
387
|
+
| Wording | P 0.95 | P 0.97 | P 0.98 | P 0.99 |
|
|
388
|
+
|---|---|---|---|---|
|
|
389
|
+
| "This image visibly contains {TAG}" | 17.43 | 12.78 | 9.87 | 6.04 |
|
|
390
|
+
| "Is there a {TAG} in this image?" (0.3.0, article by rule) | 17.35 | 11.30 | 2.39 | 2.09 |
|
|
391
|
+
| "Present in this image: {TAG}" | 17.61 | 8.74 | 8.13 | 6.74 |
|
|
392
|
+
|
|
393
|
+
The shipped fp8 tile pass cut offline, per tile and prompt over 69 runs, from
|
|
394
|
+
ContextAnchoredTileRefine's `tests-AB/ab_tags_paired.py`.
|
|
395
|
+
|
|
396
|
+
| Thresholds | Correct | Wrong or vague | Precision |
|
|
397
|
+
|---|---|---|---|
|
|
398
|
+
| broad (0.999, 0.9999) | 12.32 | 0.54 | 0.958 |
|
|
399
|
+
| strict (0.99998, 0.99998) | 7.33 | 0.17 | 0.977 |
|
|
294
400
|
|
|
295
|
-
|
|
296
|
-
|
|
401
|
+
Every paired interval against "This image visibly contains {TAG}" at 0.95 and 0.98 spans zero.
|
|
402
|
+
It needs no grammar, and the tile pass already asked it, so its thresholds carry over. On
|
|
403
|
+
declared COCO tag lists "Is there a {TAG} in this image?" (0.3.0, article by rule) kept more
|
|
404
|
+
correct tags at precision 0.98 (3.79 against 1.90 per image), which is the cost of one
|
|
405
|
+
wording.
|
|
297
406
|
|
|
298
407
|
## Bugs found and fixed
|
|
299
408
|
|
|
@@ -301,12 +410,14 @@ ones. `benchmark.py`.
|
|
|
301
410
|
|---|---|---|
|
|
302
411
|
| score used the choice confidence formula | wrong confidence on every score answer | ported both official formulas |
|
|
303
412
|
| attention probe ran in bfloat16 whatever the model dtype | any other dtype crashed on the first forward | probe at the model's dtype |
|
|
304
|
-
| attention probe used one call shape | approved a backend that then failed | probe causal and masked
|
|
413
|
+
| attention probe used one call shape | approved a backend that then failed | probe the causal and masked forms the forward sends |
|
|
414
|
+
| cuDNN attention built an execution plan for every new sequence length | about a second on almost every served request | memory efficient kernel, with grouped heads expanded before it |
|
|
305
415
|
| abstain combined groups by product | shrinks with group count, never fires at scale | weakest decline |
|
|
306
416
|
| image source detected by string length | an 8x8 PNG encodes shorter than the threshold and was read as a path | check whether the file exists |
|
|
307
417
|
| M-RoPE offset dropped on branch rows | Qwen3-VL raises rather than misplacing tokens | read `rope_deltas` after the prefix pass |
|
|
308
418
|
| prior learned and corrected the escape label | traffic that often declines taught the prior to undo a decline | prior over the lettered options only |
|
|
309
|
-
| a joint score shared a prior bucket with a choice of the same width | a score's top rung learned the choice traffic's escape mass |
|
|
419
|
+
| a joint score shared a prior bucket with a choice of the same width | a score's top rung learned the choice traffic's escape mass | no prior on a score |
|
|
420
|
+
| the yes/no and score layouts learned the traffic's answer rate | mostly true traffic cut recall at 0.9999 from 0.055 to 0.006 | prior over choice and member branches only |
|
|
310
421
|
|
|
311
422
|
## Known weaknesses
|
|
312
423
|
|
|
@@ -320,15 +431,8 @@ now saturate near 0 and 1, so the best threshold sits at 0.9999 rather than near
|
|
|
320
431
|
|
|
321
432
|
**Noul regressed 8 points** moving to the vision model, and image tagging leans on noul.
|
|
322
433
|
|
|
323
|
-
**Very long states fall off the prefill ceiling.** At 33,068 prefilled tokens the card runs
|
|
324
|
-
at 7.1 TFLOP/s against 47.9 at 8,492. The prefix key value cache is about 4.9 GB at that
|
|
325
|
-
length and every chunk copies it, which leaves a 24 GB card no room. Expanding the cache
|
|
326
|
-
once and cropping between chunks was measured and is worse, at 12,732 ms against 1,710 ms
|
|
327
|
-
and 23.66 GB against 15.12 GB, because a cropped cache is a non-contiguous view. Unsolved.
|
|
328
|
-
`benchmark.py`.
|
|
329
|
-
|
|
330
434
|
## Layout
|
|
331
435
|
|
|
332
|
-
`tests/` holds the pytest suite,
|
|
436
|
+
`tests/` holds the pytest suite, the unit tests and 14 tests that need the GPU. `tests-AB/` holds
|
|
333
437
|
the measurement harnesses, matching ComfyUI-ContextAnchoredTileRefine. Fixtures live in
|
|
334
438
|
`tests/fixtures/` and `tests-AB/ab_env.py` points at them.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: logit-classifier
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Local zero-shot classifier for text and images. Declares options and reads a calibrated probability for each from the logits. Accepts the TypeSafe System One request format, and ships a toolkit of image tag tools and ComfyUI node tools.
|
|
5
5
|
Project-URL: Repository, https://github.com/Blakeem/logit-classifier
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/Blakeem/logit-classifier/issues
|
|
@@ -52,7 +52,8 @@ A `choice` holds up to 255 options. A `score` holds 2 to 10 levels. One request
|
|
|
52
52
|
to 256 questions, and every question is answered against the same state in one pass.
|
|
53
53
|
|
|
54
54
|
A branch is one prompt whose last token position carries a distribution. A question needs
|
|
55
|
-
one branch, or several when it holds more than
|
|
55
|
+
one branch, or several when it holds more than 51 options (52 with `abstain` off) or scores
|
|
56
|
+
each level alone.
|
|
56
57
|
|
|
57
58
|
`abstain` is how strongly the model wants none of your options. Every choice question is
|
|
58
59
|
offered an extra label named `none of these`, and that label's mass comes back as
|
|
@@ -68,6 +69,15 @@ over its options, so one winner takes nearly all the mass.
|
|
|
68
69
|
|
|
69
70
|
The package needs Python 3.12 or newer.
|
|
70
71
|
|
|
72
|
+
The `hf` extra needs a CUDA build of torch. The torch wheels on PyPI for Windows have no CUDA,
|
|
73
|
+
so on Windows install torch from the PyTorch index first.
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu128
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Then install the package.
|
|
80
|
+
|
|
71
81
|
```
|
|
72
82
|
pip install "logit-classifier[hf,service]"
|
|
73
83
|
```
|
|
@@ -78,8 +88,10 @@ pip install "logit-classifier[hf,service]"
|
|
|
78
88
|
| `hf` | torch, torchvision, transformers, accelerate, safetensors, huggingface-hub, pillow | loading the weights in this process |
|
|
79
89
|
| `service` | fastapi, uvicorn, pillow | the HTTP endpoint and the browser page |
|
|
80
90
|
|
|
81
|
-
The `hf` extra needs an NVIDIA GPU
|
|
82
|
-
|
|
91
|
+
The `hf` extra needs an NVIDIA GPU. A short request peaks under 9 GiB of VRAM, and a state
|
|
92
|
+
near 8,000 tokens peaks at about 10.2 GiB. Every VRAM figure in this README is an allocated
|
|
93
|
+
peak, and the process needs more for the CUDA context and the allocator's cache. The weights
|
|
94
|
+
take 8 to 9 GB of disk per model and download on first use.
|
|
83
95
|
|
|
84
96
|
They go to the Hugging Face cache by default. Point `models_dir` at a folder to keep them
|
|
85
97
|
beside your project instead, which is what the examples do.
|
|
@@ -178,7 +190,7 @@ the model.
|
|
|
178
190
|
|
|
179
191
|
```json
|
|
180
192
|
{
|
|
181
|
-
"model": "logit-classifier-0.
|
|
193
|
+
"model": "logit-classifier-0.5.0",
|
|
182
194
|
"answers": {
|
|
183
195
|
"department": {
|
|
184
196
|
"type": "choice",
|
|
@@ -254,7 +266,7 @@ reads none of them. `logit-classifier config` prints what they produce.
|
|
|
254
266
|
| `LOGIT_SCORE_METHOD` | `joint` | set to `independent` to judge each level alone |
|
|
255
267
|
| `LOGIT_PERMUTATIONS` | `1` | letterings averaged per question, each one adds branches |
|
|
256
268
|
| `LOGIT_ABSTAIN` | `1` | set to `0` to drop the `none of these` label below 52 options |
|
|
257
|
-
| `LOGIT_CALIBRATION_PATH` | `calibration.json` | where
|
|
269
|
+
| `LOGIT_CALIBRATION_PATH` | `calibration.json` | where `Config.from_env()` stores the running prior |
|
|
258
270
|
|
|
259
271
|
## Calibration
|
|
260
272
|
|
|
@@ -262,16 +274,17 @@ Two corrections are applied before the softmax.
|
|
|
262
274
|
|
|
263
275
|
The label prior is the model's standing preference for the token `A` over `B`. It is
|
|
264
276
|
learned as a running mean over served requests, so it needs no labelled data or extra
|
|
265
|
-
forward passes. It
|
|
277
|
+
forward passes. It corrects choice questions only, since the letters of a noul and a score
|
|
278
|
+
always name the same answers. It stays unused until a bucket has 32 observations, so the first requests
|
|
266
279
|
after a cold start can answer a little differently from later ones.
|
|
267
280
|
|
|
268
281
|
The temperature is fitted per model and divides the logits. Both shipped values were
|
|
269
282
|
fitted on Banking77 rather than on your data, so refit yours when the absolute numbers
|
|
270
283
|
matter.
|
|
271
284
|
|
|
272
|
-
|
|
273
|
-
`
|
|
274
|
-
|
|
285
|
+
`Config()` holds the prior in memory and writes no file. `Config.from_env()` stores it at
|
|
286
|
+
`LOGIT_CALIBRATION_PATH` after every request, so the service keeps it across restarts. Pass
|
|
287
|
+
`Config(calibration_path=...)` to pick the file yourself. A stored prior carries a fingerprint of the model and the prompt shape, and
|
|
275
288
|
changing either one starts the prior over.
|
|
276
289
|
|
|
277
290
|
## Determinism
|
|
@@ -292,17 +305,18 @@ own settings everywhere outside a pass.
|
|
|
292
305
|
|
|
293
306
|
## Options Above 52
|
|
294
307
|
|
|
295
|
-
Options are labeled `A` to `Z` then `a` to `z`, which gives 52 single token labels.
|
|
296
|
-
|
|
308
|
+
Options are labeled `A` to `Z` then `a` to `z`, which gives 52 single token labels. While
|
|
309
|
+
`abstain` is on, the `none of these` label takes one of them. A question with more options
|
|
310
|
+
than the free labels is split into groups, and each group carries an escape label so
|
|
297
311
|
the groups can be weighed against each other. Accuracy is lower on that path and `abstain`
|
|
298
312
|
is much weaker, so keep a question under 52 options where you can.
|
|
299
313
|
|
|
300
314
|
## Speed
|
|
301
315
|
|
|
302
|
-
On a warm RTX 3090 Ti a short record takes about
|
|
316
|
+
On a warm RTX 3090 Ti a short record takes about 135 ms whatever it asks, up to about 8
|
|
303
317
|
questions, because two forward passes dominate. So send every question for a record in one
|
|
304
|
-
request.
|
|
305
|
-
|
|
318
|
+
request. A state near 8,000 tokens takes about 1.8 seconds. A state near 33,000 tokens takes
|
|
319
|
+
about 11.5 seconds and peaks at about 15.2 GiB of VRAM.
|
|
306
320
|
|
|
307
321
|
## ComfyUI Node Packs
|
|
308
322
|
|
|
@@ -371,7 +385,8 @@ and the classifier never imports the toolkit.
|
|
|
371
385
|
| `drop_subsets` | drops a tag whose words all appear in a longer tag |
|
|
372
386
|
| `prompt_word_forms`, `in_prompt` | test whether a tag's words appear in a prompt, singular or plural |
|
|
373
387
|
| `merge_candidates` | merges proposed tags with a prompt's tags under a cap |
|
|
374
|
-
| `
|
|
388
|
+
| `presence_statement` | writes "This image visibly contains car" with the tag as given |
|
|
389
|
+
| `BROAD_THRESHOLDS`, `STRICT_THRESHOLDS` | the scores a tag must reach, broad for more tags and strict for fewer wrong ones |
|
|
375
390
|
|
|
376
391
|
```python
|
|
377
392
|
from logit_classifier.toolkit.tags import drop_subsets, parse_candidates
|
|
@@ -387,35 +402,44 @@ stays beside "cat ear", and "signage" is dropped beside "signage in chinese".
|
|
|
387
402
|
### ComfyUI Tools
|
|
388
403
|
|
|
389
404
|
These run inside a ComfyUI node over the CLIP the workflow loaded. Each one imports comfy and
|
|
390
|
-
torch only when it is called.
|
|
405
|
+
torch only when it is called. `logit_classifier.toolkit.comfyui` exports every tool. The table
|
|
406
|
+
names the module of each tool it lists.
|
|
391
407
|
|
|
392
|
-
| Tool | Does |
|
|
393
|
-
|
|
394
|
-
| `comfy_classifier` | builds a classifier over the CLIP, with errors that name the node and the Load CLIP fix |
|
|
395
|
-
| `generate_text` | generates one greedy reply with the Qwen chat template, and stops early on a condition |
|
|
396
|
-
| `
|
|
397
|
-
| `
|
|
398
|
-
| `
|
|
399
|
-
| `
|
|
400
|
-
| `
|
|
401
|
-
| `
|
|
402
|
-
| `
|
|
408
|
+
| Module | Tool | Does |
|
|
409
|
+
|---|---|---|
|
|
410
|
+
| `classifier` | `comfy_classifier` | builds a classifier over the CLIP, with errors that name the node and the Load CLIP fix |
|
|
411
|
+
| `generate` | `generate_text` | generates one greedy reply with the Qwen chat template, and stops early on a condition |
|
|
412
|
+
| `generate` | `clip_generate` | runs core's generate, then clears the CUDA graphs it captured |
|
|
413
|
+
| `scopes` | `shared_vision_encode` | runs the vision tower once per image for every generate and classify inside the block |
|
|
414
|
+
| `scopes` | `skip_resident_loads` | skips core's model load while the CLIP is already on the GPU |
|
|
415
|
+
| `progress` | `routed_progress_bars`, `send_status`, `run_outcome` | show one progress bar per run and a status line under the node |
|
|
416
|
+
| `tagger` | `fit_image` | downscales an image to a pixel cap |
|
|
417
|
+
| `tagger` | `tag_prompt` | lists the physical things a prompt names |
|
|
418
|
+
| `tagger` | `tag_image` | tags an image, with a prompt's things added as candidates |
|
|
419
|
+
| `tagger` | `TagSettings` | holds every wording, budget, cap and threshold of the tagger |
|
|
403
420
|
|
|
404
421
|
The tagger generates candidate tags over the CLIP and verifies each one with the classifier.
|
|
405
422
|
|
|
406
423
|
```python
|
|
407
|
-
from logit_classifier.toolkit.comfyui import TagSettings, comfy_classifier,
|
|
424
|
+
from logit_classifier.toolkit.comfyui import TagSettings, comfy_classifier, fit_image, tag_image
|
|
408
425
|
|
|
409
426
|
classifier = comfy_classifier(clip, node="My Tagger")
|
|
410
|
-
|
|
411
|
-
trace = tag_picture(clip, classifier, picture, settings=TagSettings(), known={})
|
|
427
|
+
trace = tag_image(clip, classifier, fit_image(image, 1024 * 1024), settings=TagSettings(), thing_cache={})
|
|
412
428
|
tags = trace.kept
|
|
413
429
|
```
|
|
414
430
|
|
|
415
|
-
`
|
|
431
|
+
`thing_cache` holds the thing check's answers across a run, so each tag is asked once. The
|
|
416
432
|
`TagSettings` defaults are the current measured values, and a later release may change them.
|
|
417
433
|
Pass every value your output depends on to keep it fixed.
|
|
418
434
|
|
|
435
|
+
For fewer wrong tags, pass `STRICT_THRESHOLDS` as `verify_thresholds`.
|
|
436
|
+
|
|
437
|
+
```python
|
|
438
|
+
from logit_classifier.toolkit.tags import STRICT_THRESHOLDS
|
|
439
|
+
|
|
440
|
+
settings = TagSettings(verify_thresholds=STRICT_THRESHOLDS)
|
|
441
|
+
```
|
|
442
|
+
|
|
419
443
|
## Differences From Jev
|
|
420
444
|
|
|
421
445
|
This project matches the System One request and response format and the documented limits.
|
|
@@ -434,13 +458,13 @@ Jev publishes status codes but no error body. The error shape here is our own.
|
|
|
434
458
|
|
|
435
459
|
```
|
|
436
460
|
uv sync --all-extras
|
|
437
|
-
uv run pytest tests
|
|
438
|
-
uv run pytest tests/test_model.py -m model
|
|
461
|
+
uv run python -m pytest tests -m "not model"
|
|
462
|
+
uv run python -m pytest tests/test_model.py -m model
|
|
439
463
|
uv run ruff check .
|
|
440
464
|
uv run mypy
|
|
441
465
|
```
|
|
442
466
|
|
|
443
|
-
The unit tests need no weights. The model tests load the GPU and take
|
|
467
|
+
The unit tests need no weights. The model tests load the GPU and take 24 to 126 seconds, the longer run being the first of a session.
|
|
444
468
|
|
|
445
469
|
`FINDINGS.md` is the measurement record. Every number there names the script in
|
|
446
470
|
`tests-AB/` that produced it.
|