logit-classifier 0.4.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.
Files changed (74) hide show
  1. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/FINDINGS.md +134 -55
  2. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/PKG-INFO +29 -15
  3. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/README.md +28 -14
  4. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/examples/quickstart.py +6 -2
  5. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/pyproject.toml +6 -2
  6. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/_version.py +1 -1
  7. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/backends/_torch_window.py +2 -2
  8. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/backends/base.py +2 -1
  9. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/backends/comfy_clip.py +34 -18
  10. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/backends/hf.py +121 -27
  11. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/calibrate.py +13 -1
  12. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/classifier.py +9 -12
  13. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/config.py +14 -3
  14. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/deps.py +7 -4
  15. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/labels.py +0 -5
  16. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/prompt.py +2 -2
  17. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/schema.py +1 -1
  18. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/scoring.py +2 -2
  19. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/service.py +0 -4
  20. logit_classifier-0.5.0/src/logit_classifier/toolkit/__init__.py +7 -0
  21. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/toolkit/comfyui/__init__.py +1 -1
  22. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/toolkit/comfyui/generate.py +2 -18
  23. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/toolkit/comfyui/scopes.py +7 -20
  24. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/toolkit/comfyui/tagger.py +1 -1
  25. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/toolkit/tags.py +6 -4
  26. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/vision.py +19 -14
  27. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/web/index.html +0 -2
  28. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests/test_toolkit.py +13 -41
  29. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests/test_unit.py +428 -39
  30. logit_classifier-0.5.0/tests-AB/ab_attention_kernel.py +145 -0
  31. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests-AB/ab_branch_packing.py +6 -6
  32. logit_classifier-0.5.0/tests-AB/ab_frozen_prefix_cache.py +125 -0
  33. logit_classifier-0.5.0/tests-AB/ab_prior_traffic.py +139 -0
  34. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests-AB/ab_temperature.py +6 -1
  35. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests-AB/benchmark.py +9 -3
  36. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests-AB/comfy_env.py +3 -1
  37. logit_classifier-0.4.0/src/logit_classifier/tags.py +0 -36
  38. logit_classifier-0.4.0/src/logit_classifier/toolkit/__init__.py +0 -5
  39. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/.gitignore +0 -0
  40. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/LICENSE +0 -0
  41. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/examples/compare_models.py +0 -0
  42. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/examples/http_client.py +0 -0
  43. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/examples/images.py +0 -0
  44. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/examples/own_backend.py +0 -0
  45. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/examples/question_types.py +0 -0
  46. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/__init__.py +0 -0
  47. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/__main__.py +0 -0
  48. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/backends/__init__.py +0 -0
  49. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/cli.py +0 -0
  50. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/errors.py +0 -0
  51. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/py.typed +0 -0
  52. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/toolkit/comfyui/classifier.py +0 -0
  53. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/src/logit_classifier/toolkit/comfyui/progress.py +0 -0
  54. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests/fixtures/banking77_test.json +0 -0
  55. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests/fixtures/eval_set.json +0 -0
  56. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests/fixtures/many_options_request.json +0 -0
  57. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests/fixtures/quickstart_request.json +0 -0
  58. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests/test_model.py +0 -0
  59. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests-AB/ab_determinism_scope.py +0 -0
  60. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests-AB/ab_env.py +0 -0
  61. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests-AB/ab_math_sdp_reduction.py +0 -0
  62. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests-AB/ab_multi_label.py +0 -0
  63. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests-AB/ab_noul_wording.py +0 -0
  64. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests-AB/banking77.py +0 -0
  65. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests-AB/evaluate.py +0 -0
  66. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests-AB/inputs/_full.png +0 -0
  67. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests-AB/inputs/_sheet.png +0 -0
  68. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests-AB/inputs/low-L.png +0 -0
  69. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests-AB/inputs/low-R.png +0 -0
  70. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests-AB/inputs/mid-L.png +0 -0
  71. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests-AB/inputs/mid-R.png +0 -0
  72. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests-AB/inputs/top-L.png +0 -0
  73. {logit_classifier-0.4.0 → logit_classifier-0.5.0}/tests-AB/inputs/top-R.png +0 -0
  74. {logit_classifier-0.4.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
- Two positions carry a fixed meaning instead. The escape label always means none of these,
60
- and a score's letters always name the same rungs. Their mean mass follows the traffic, so
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.5** | **0.75** |
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. `sdpa_kernel` turns the math backend **off** while it holds the CUDA
179
- backends, so this setting is live only when `_usable_attention_backends` returns nothing and
180
- no `sdpa_kernel` block is entered. ComfyUI turns it **on** at import, at
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 GB | suffix tokens | chunks | |
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 GB. Nothing
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 GB, 5 chunks | 2015.7 ms, 5 chunks |
245
- | 1024 | 608.3 ms, 10.47 GB, 3 chunks | 2014.9 ms, 5 chunks |
246
- | **2048** | **626.0 ms, 10.66 GB, 3 chunks** | **2015.8 ms, 5 chunks** |
247
- | 4096 | 793.5 ms, 11.03 GB, 2 chunks | 2286.5 ms, 4 chunks |
248
- | 8192 | 1119.4 ms, 11.84 GB, 2 chunks | 2289.5 ms, 4 chunks |
249
- | none | 2062.2 ms, 13.66 GB, 1 chunk | 2299.1 ms, 4 chunks |
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,91 @@ 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. Adding questions to a record is close to free up to about 8,
260
- because the fixed cost of two forward passes dominates a short record.
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 | 138 | 138 | 26,100 |
265
- | 4 | 136 | 34 | 26,500 |
266
- | 8 | 140 | 17.5 | 25,800 |
267
- | 32 | 278 | 8.7 | 13,000 |
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 | 140 | 17.5 | 25,700 |
274
- | 556 | 185 | 23 | 19,500 |
275
- | 2,092 | 451 | 56 | 8,000 |
276
- | 8,236 | 1,575 | 197 | 2,300 |
277
- | 32,812 | 41,515 | 5,189 | 87 |
278
-
279
- Prefill runs at 46 to 48 TFLOP/s from about 2,000 tokens up, near this card's practical
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`.
283
-
284
- ## The attention backend has to be pinned on Windows
285
-
286
- Torch 2.11 ships no FlashAttention kernel for Windows. Left to choose for itself the
287
- dispatcher reaches the math backend, which builds the full attention matrix. Measured on an
288
- 8,192 token prefill.
289
-
290
- | Backend selection | Time | Peak memory |
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`.
322
+
323
+ ## The memory efficient kernel, not cuDNN
324
+
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.
331
+
332
+ Both arms ran on one loaded model.
333
+
334
+ | | cuDNN, grouped heads | memory efficient, expanded heads |
291
335
  |---|---|---|
292
- | left to the dispatcher | 147,221 ms | 27.6 GB |
293
- | pinned to cudnn and efficient | 1,291 ms | 9.2 GB |
294
-
295
- `HFBackend` probes the backends at startup, at the model's own dtype, and pins the working
296
- ones. `benchmark.py`.
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.
297
381
 
298
382
  ## One presence wording, two threshold pairs
299
383
 
@@ -326,12 +410,14 @@ wording.
326
410
  |---|---|---|
327
411
  | score used the choice confidence formula | wrong confidence on every score answer | ported both official formulas |
328
412
  | attention probe ran in bfloat16 whatever the model dtype | any other dtype crashed on the first forward | probe at the model's dtype |
329
- | attention probe used one call shape | approved a backend that then failed | probe causal and masked, with grouped query attention |
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 |
330
415
  | abstain combined groups by product | shrinks with group count, never fires at scale | weakest decline |
331
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 |
332
417
  | M-RoPE offset dropped on branch rows | Qwen3-VL raises rather than misplacing tokens | read `rope_deltas` after the prefix pass |
333
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 |
334
- | 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 | score has its own bucket |
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 |
335
421
 
336
422
  ## Known weaknesses
337
423
 
@@ -345,13 +431,6 @@ now saturate near 0 and 1, so the best threshold sits at 0.9999 rather than near
345
431
 
346
432
  **Noul regressed 8 points** moving to the vision model, and image tagging leans on noul.
347
433
 
348
- **Very long states fall off the prefill ceiling.** At 33,068 prefilled tokens the card runs
349
- at 7.1 TFLOP/s against 47.9 at 8,492. The prefix key value cache is about 4.9 GB at that
350
- length and every chunk copies it, which leaves a 24 GB card no room. Expanding the cache
351
- once and cropping between chunks was measured and is worse, at 12,732 ms against 1,710 ms
352
- and 23.66 GB against 15.12 GB, because a cropped cache is a non-contiguous view. Unsolved.
353
- `benchmark.py`.
354
-
355
434
  ## Layout
356
435
 
357
436
  `tests/` holds the pytest suite, the unit tests and 14 tests that need the GPU. `tests-AB/` holds
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: logit-classifier
3
- Version: 0.4.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 52 options or scores each level alone.
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 with at least 10 GB of free VRAM. The weights are
82
- about 8 GB per model and download on first use.
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.4.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 the service stores the running prior |
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 stays unused until a bucket has 32 observations, so the first requests
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
- The library holds the prior in memory and writes no file. The service stores it in
273
- `calibration.json`. Pass `Config(calibration_path=...)` to keep it across runs from the
274
- library. A stored prior carries a fingerprint of the model and the prompt shape, and
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. A
296
- question with more options is split into groups, and each group carries an escape label so
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 140 ms whatever it asks, up to about 8
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. Keep a record under about 8,000 tokens, since a longer state falls off the
305
- prefill ceiling.
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
 
@@ -19,7 +19,8 @@ A `choice` holds up to 255 options. A `score` holds 2 to 10 levels. One request
19
19
  to 256 questions, and every question is answered against the same state in one pass.
20
20
 
21
21
  A branch is one prompt whose last token position carries a distribution. A question needs
22
- one branch, or several when it holds more than 52 options or scores each level alone.
22
+ one branch, or several when it holds more than 51 options (52 with `abstain` off) or scores
23
+ each level alone.
23
24
 
24
25
  `abstain` is how strongly the model wants none of your options. Every choice question is
25
26
  offered an extra label named `none of these`, and that label's mass comes back as
@@ -35,6 +36,15 @@ over its options, so one winner takes nearly all the mass.
35
36
 
36
37
  The package needs Python 3.12 or newer.
37
38
 
39
+ The `hf` extra needs a CUDA build of torch. The torch wheels on PyPI for Windows have no CUDA,
40
+ so on Windows install torch from the PyTorch index first.
41
+
42
+ ```
43
+ pip install torch torchvision --index-url https://download.pytorch.org/whl/cu128
44
+ ```
45
+
46
+ Then install the package.
47
+
38
48
  ```
39
49
  pip install "logit-classifier[hf,service]"
40
50
  ```
@@ -45,8 +55,10 @@ pip install "logit-classifier[hf,service]"
45
55
  | `hf` | torch, torchvision, transformers, accelerate, safetensors, huggingface-hub, pillow | loading the weights in this process |
46
56
  | `service` | fastapi, uvicorn, pillow | the HTTP endpoint and the browser page |
47
57
 
48
- The `hf` extra needs an NVIDIA GPU with at least 10 GB of free VRAM. The weights are
49
- about 8 GB per model and download on first use.
58
+ The `hf` extra needs an NVIDIA GPU. A short request peaks under 9 GiB of VRAM, and a state
59
+ near 8,000 tokens peaks at about 10.2 GiB. Every VRAM figure in this README is an allocated
60
+ peak, and the process needs more for the CUDA context and the allocator's cache. The weights
61
+ take 8 to 9 GB of disk per model and download on first use.
50
62
 
51
63
  They go to the Hugging Face cache by default. Point `models_dir` at a folder to keep them
52
64
  beside your project instead, which is what the examples do.
@@ -145,7 +157,7 @@ the model.
145
157
 
146
158
  ```json
147
159
  {
148
- "model": "logit-classifier-0.4.0",
160
+ "model": "logit-classifier-0.5.0",
149
161
  "answers": {
150
162
  "department": {
151
163
  "type": "choice",
@@ -221,7 +233,7 @@ reads none of them. `logit-classifier config` prints what they produce.
221
233
  | `LOGIT_SCORE_METHOD` | `joint` | set to `independent` to judge each level alone |
222
234
  | `LOGIT_PERMUTATIONS` | `1` | letterings averaged per question, each one adds branches |
223
235
  | `LOGIT_ABSTAIN` | `1` | set to `0` to drop the `none of these` label below 52 options |
224
- | `LOGIT_CALIBRATION_PATH` | `calibration.json` | where the service stores the running prior |
236
+ | `LOGIT_CALIBRATION_PATH` | `calibration.json` | where `Config.from_env()` stores the running prior |
225
237
 
226
238
  ## Calibration
227
239
 
@@ -229,16 +241,17 @@ Two corrections are applied before the softmax.
229
241
 
230
242
  The label prior is the model's standing preference for the token `A` over `B`. It is
231
243
  learned as a running mean over served requests, so it needs no labelled data or extra
232
- forward passes. It stays unused until a bucket has 32 observations, so the first requests
244
+ forward passes. It corrects choice questions only, since the letters of a noul and a score
245
+ always name the same answers. It stays unused until a bucket has 32 observations, so the first requests
233
246
  after a cold start can answer a little differently from later ones.
234
247
 
235
248
  The temperature is fitted per model and divides the logits. Both shipped values were
236
249
  fitted on Banking77 rather than on your data, so refit yours when the absolute numbers
237
250
  matter.
238
251
 
239
- The library holds the prior in memory and writes no file. The service stores it in
240
- `calibration.json`. Pass `Config(calibration_path=...)` to keep it across runs from the
241
- library. A stored prior carries a fingerprint of the model and the prompt shape, and
252
+ `Config()` holds the prior in memory and writes no file. `Config.from_env()` stores it at
253
+ `LOGIT_CALIBRATION_PATH` after every request, so the service keeps it across restarts. Pass
254
+ `Config(calibration_path=...)` to pick the file yourself. A stored prior carries a fingerprint of the model and the prompt shape, and
242
255
  changing either one starts the prior over.
243
256
 
244
257
  ## Determinism
@@ -259,17 +272,18 @@ own settings everywhere outside a pass.
259
272
 
260
273
  ## Options Above 52
261
274
 
262
- Options are labeled `A` to `Z` then `a` to `z`, which gives 52 single token labels. A
263
- question with more options is split into groups, and each group carries an escape label so
275
+ Options are labeled `A` to `Z` then `a` to `z`, which gives 52 single token labels. While
276
+ `abstain` is on, the `none of these` label takes one of them. A question with more options
277
+ than the free labels is split into groups, and each group carries an escape label so
264
278
  the groups can be weighed against each other. Accuracy is lower on that path and `abstain`
265
279
  is much weaker, so keep a question under 52 options where you can.
266
280
 
267
281
  ## Speed
268
282
 
269
- On a warm RTX 3090 Ti a short record takes about 140 ms whatever it asks, up to about 8
283
+ On a warm RTX 3090 Ti a short record takes about 135 ms whatever it asks, up to about 8
270
284
  questions, because two forward passes dominate. So send every question for a record in one
271
- request. Keep a record under about 8,000 tokens, since a longer state falls off the
272
- prefill ceiling.
285
+ request. A state near 8,000 tokens takes about 1.8 seconds. A state near 33,000 tokens takes
286
+ about 11.5 seconds and peaks at about 15.2 GiB of VRAM.
273
287
 
274
288
  ## ComfyUI Node Packs
275
289
 
@@ -65,7 +65,7 @@ def report(answers: dict) -> None:
65
65
 
66
66
  def main() -> None:
67
67
  parser = argparse.ArgumentParser(description="Ask one state a few questions.")
68
- parser.add_argument("--text", default=SAMPLE_TEXT, help="the text to classify")
68
+ parser.add_argument("--text", default=None, help="the text to classify, or a caption beside --image")
69
69
  parser.add_argument("--image", default=None, help="a picture to classify instead of the text")
70
70
  parser.add_argument("--model", default=None, help="a model id, else the configured default")
71
71
  args = parser.parse_args()
@@ -73,7 +73,11 @@ def main() -> None:
73
73
  config = Config(models_dir=MODELS_DIR)
74
74
  backend = load_model(args.model, config)
75
75
  classifier = Classifier(config, backend=backend)
76
- state = {"image": args.image, "caption": args.text} if args.image else args.text
76
+ state: object = args.text if args.text is not None else SAMPLE_TEXT
77
+ if args.image:
78
+ state = {"image": args.image}
79
+ if args.text is not None:
80
+ state = {"image": args.image, "caption": args.text}
77
81
  questions = IMAGE_QUESTIONS if args.image else QUESTIONS
78
82
 
79
83
  print(f"model {backend.model_id}, reads images: {backend.sees_images}\n")
@@ -143,11 +143,15 @@ ignore_missing_imports = true
143
143
  [[tool.mypy.overrides]]
144
144
  # transformers annotates from_pretrained and eval loosely, which strict mode reads
145
145
  # as untyped calls. torch resolves to Any where the hf extra is absent, which makes
146
- # @torch.inference_mode() an untyped decorator. Every other strict check still
147
- # applies to this module.
146
+ # @torch.inference_mode() an untyped decorator. CI installs no hf extra, so DynamicCache
147
+ # resolves to Any there, which makes the prefix cache a subclass of Any and its update
148
+ # return Any. An inline ignore would go unused locally, where transformers is typed.
149
+ # Every other strict check still applies to this module.
148
150
  module = ["logit_classifier.backends.hf"]
149
151
  disallow_untyped_calls = false
150
152
  disallow_untyped_decorators = false
153
+ disallow_subclassing_any = false
154
+ warn_return_any = false
151
155
 
152
156
  [[tool.mypy.overrides]]
153
157
  # torch leaves fp16_bf16_reduction_math_sdp_allowed unannotated, and a line ignore
@@ -1,3 +1,3 @@
1
1
  """The package version, apart from __init__ so config.py can read it without a cycle."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.5.0"
@@ -51,8 +51,8 @@ def _determinism() -> Iterator[None]:
51
51
  saved_matmul = None if has_slots else torch.get_float32_matmul_precision()
52
52
  saved_cuda = torch.backends.cuda.matmul.fp32_precision if has_slots else None
53
53
  saved_mkldnn = mkldnn.fp32_precision if has_slots else None
54
- # ComfyUI turns this on at import, at comfy/model_management.py:569. It governs
55
- # the math attention backend, which runs only when attention_backends is empty.
54
+ # ComfyUI turns this on at import, at comfy/model_management.py:569. It governs the math
55
+ # attention backend, last in HFBackend's FALLBACK_ATTENTION and in core's own SDPA priority.
56
56
  has_sdp = hasattr(torch.backends.cuda, "allow_fp16_bf16_reduction_math_sdp") and hasattr(
57
57
  torch.backends.cuda, "fp16_bf16_reduction_math_sdp_allowed"
58
58
  )
@@ -1,7 +1,8 @@
1
1
  """The readout port, the one boundary between the arithmetic and a host's forward pass.
2
2
 
3
3
  Everything above this port is stdlib and numpy. Everything below it is one host's
4
- way of turning token ids into a logit row: transformers here, a ComfyUI CLIP next.
4
+ way of turning token ids into a logit row: transformers in hf.py, a ComfyUI CLIP in
5
+ comfy_clip.py.
5
6
  """
6
7
 
7
8
  from __future__ import annotations