logit-classifier 0.1.0__tar.gz → 0.2.1__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 (60) hide show
  1. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/PKG-INFO +36 -14
  2. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/README.md +35 -13
  3. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/examples/compare_models.py +2 -1
  4. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/examples/images.py +2 -1
  5. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/examples/question_types.py +2 -1
  6. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/examples/quickstart.py +2 -1
  7. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/pyproject.toml +9 -2
  8. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/src/logit_classifier/__init__.py +3 -2
  9. logit_classifier-0.2.1/src/logit_classifier/_version.py +3 -0
  10. logit_classifier-0.2.1/src/logit_classifier/backends/_torch_window.py +96 -0
  11. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/src/logit_classifier/backends/base.py +4 -0
  12. logit_classifier-0.2.1/src/logit_classifier/backends/comfy_clip.py +414 -0
  13. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/src/logit_classifier/backends/hf.py +1 -83
  14. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/src/logit_classifier/classifier.py +34 -5
  15. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/src/logit_classifier/config.py +5 -4
  16. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/src/logit_classifier/prompt.py +8 -2
  17. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/src/logit_classifier/schema.py +14 -2
  18. logit_classifier-0.2.1/src/logit_classifier/tags.py +171 -0
  19. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/src/logit_classifier/vision.py +55 -18
  20. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/src/logit_classifier/web/index.html +2 -2
  21. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests/test_unit.py +1191 -7
  22. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/.gitignore +0 -0
  23. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/FINDINGS.md +0 -0
  24. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/LICENSE +0 -0
  25. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/examples/http_client.py +0 -0
  26. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/examples/own_backend.py +0 -0
  27. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/src/logit_classifier/__main__.py +0 -0
  28. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/src/logit_classifier/backends/__init__.py +0 -0
  29. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/src/logit_classifier/calibrate.py +0 -0
  30. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/src/logit_classifier/cli.py +0 -0
  31. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/src/logit_classifier/deps.py +0 -0
  32. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/src/logit_classifier/errors.py +0 -0
  33. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/src/logit_classifier/labels.py +0 -0
  34. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/src/logit_classifier/py.typed +0 -0
  35. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/src/logit_classifier/scoring.py +0 -0
  36. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/src/logit_classifier/service.py +0 -0
  37. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests/fixtures/banking77_test.json +0 -0
  38. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests/fixtures/eval_set.json +0 -0
  39. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests/fixtures/many_options_request.json +0 -0
  40. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests/fixtures/quickstart_request.json +0 -0
  41. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests/test_model.py +0 -0
  42. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests-AB/ab_branch_packing.py +0 -0
  43. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests-AB/ab_determinism_scope.py +0 -0
  44. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests-AB/ab_env.py +0 -0
  45. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests-AB/ab_math_sdp_reduction.py +0 -0
  46. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests-AB/ab_multi_label.py +0 -0
  47. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests-AB/ab_noul_wording.py +0 -0
  48. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests-AB/ab_temperature.py +0 -0
  49. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests-AB/banking77.py +0 -0
  50. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests-AB/benchmark.py +0 -0
  51. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests-AB/evaluate.py +0 -0
  52. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests-AB/inputs/_full.png +0 -0
  53. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests-AB/inputs/_sheet.png +0 -0
  54. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests-AB/inputs/low-L.png +0 -0
  55. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests-AB/inputs/low-R.png +0 -0
  56. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests-AB/inputs/mid-L.png +0 -0
  57. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests-AB/inputs/mid-R.png +0 -0
  58. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests-AB/inputs/top-L.png +0 -0
  59. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests-AB/inputs/top-R.png +0 -0
  60. {logit_classifier-0.1.0 → logit_classifier-0.2.1}/tests-AB/tune_groups.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: logit-classifier
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: Local zero-shot classifier for text and images. Declares options, returns a calibrated probability for each, generates no text. Accepts the TypeSafe System One request format.
5
5
  Project-URL: Repository, https://github.com/Blakeem/logit-classifier
6
6
  Project-URL: Bug Tracker, https://github.com/Blakeem/logit-classifier/issues
@@ -88,8 +88,8 @@ beside your project instead, which is what the examples do.
88
88
  Config(models_dir=Path("models"))
89
89
  ```
90
90
 
91
- `LOGIT_MODELS_DIR` sets the same folder for every process, and `HF_HOME` moves the
92
- Hugging Face cache itself.
91
+ `LOGIT_MODELS_DIR` sets the same folder for the service and for `Config.from_env()`.
92
+ `HF_HOME` moves the Hugging Face cache itself.
93
93
 
94
94
  ## Models
95
95
 
@@ -98,8 +98,9 @@ Hugging Face cache itself.
98
98
  | reads images | yes | no |
99
99
  | better at | choice and score | noul |
100
100
 
101
- `Qwen3-VL-4B-Instruct` is the default. Set `LOGIT_MODEL_ID` to use the other one. Any Qwen
102
- chat model loads, and a model with no fitted temperature gets 2.5.
101
+ `Qwen3-VL-4B-Instruct` is the default. Set `LOGIT_MODEL_ID` to use the other one in the
102
+ service or through `Config.from_env()`. Any Qwen chat model loads, and a model with no
103
+ fitted temperature gets 2.5.
103
104
 
104
105
  ## Python API
105
106
 
@@ -169,7 +170,7 @@ the model.
169
170
 
170
171
  ```json
171
172
  {
172
- "model": "logit-classifier-0.1.0",
173
+ "model": "logit-classifier-0.2.1",
173
174
  "answers": {
174
175
  "department": {
175
176
  "type": "choice",
@@ -210,7 +211,7 @@ only in process.
210
211
  "questions": { "sky": { "type": "noul", "instructions": "This crop shows the night sky" } } }
211
212
  ```
212
213
 
213
- The image is encoded once for the entire request, so asking several questions about one
214
+ The image is encoded once for the entire request. So asking several questions about one
214
215
  picture costs little more than asking one.
215
216
 
216
217
  A model with no vision tower rejects the request rather than ignoring the picture.
@@ -230,8 +231,9 @@ Each script runs on its own.
230
231
 
231
232
  ## Configuration
232
233
 
233
- Every setting reads from the environment at startup. `logit-classifier config` prints what
234
- they produce.
234
+ The service and `logit-classifier config` read these variables at startup through
235
+ `Config.from_env()`. Library code gets them by calling `Config.from_env()`, since `Config()`
236
+ reads none of them. `logit-classifier config` prints what they produce.
235
237
 
236
238
  | Variable | Default | Effect |
237
239
  |---|---|---|
@@ -242,6 +244,7 @@ they produce.
242
244
  | `LOGIT_PRIOR_DEBIAS` | `1` | set to `0` to skip the label prior |
243
245
  | `LOGIT_BATCH_BRANCHES` | `1` | set to `0` for one forward pass per branch |
244
246
  | `LOGIT_SCORE_METHOD` | `joint` | set to `independent` to judge each level alone |
247
+ | `LOGIT_PERMUTATIONS` | `1` | letterings averaged per question, each one adds branches |
245
248
  | `LOGIT_ABSTAIN` | `1` | set to `0` to drop the `none of these` label below 52 options |
246
249
  | `LOGIT_CALIBRATION_PATH` | `calibration.json` | where the service stores the running prior |
247
250
 
@@ -269,9 +272,10 @@ Nothing is sampled, so the same request returns bitwise identical logits.
269
272
 
270
273
  Batch composition and padding length both change the low bits of a bfloat16 forward pass,
271
274
  and both are pure functions of the request. So the same question asked inside two
272
- different requests can differ slightly. Set `LOGIT_BATCH_BRANCHES=0` to score each branch
273
- alone, which makes a question independent of the questions sent with it and costs one
274
- forward pass per branch.
275
+ different requests can differ slightly. Scoring each branch alone makes a question
276
+ independent of the questions sent with it and costs one forward pass per branch. The service
277
+ turns it on with `LOGIT_BATCH_BRANCHES=0`. Library code passes `Config(batch_branches=False)`,
278
+ and `ComfyClipBackend` takes `batch_branches=False` as a keyword.
275
279
 
276
280
  A forward pass needs several process-global torch settings held at known values. Torch
277
281
  exposes none of them as a call argument, so the backend sets them around each pass and
@@ -320,6 +324,24 @@ the render and label contracts and returns one token id per label.
320
324
  `logit_classifier.backends.hf.HFBackend` is the transformers implementation to read
321
325
  against.
322
326
 
327
+ `ComfyClipBackend` is the backend over a Qwen3-VL text encoder that the workflow already
328
+ loaded, such as the one Krea 2 uses. So no second model is loaded into VRAM.
329
+
330
+ ```python
331
+ from logit_classifier import Classifier, Config, NoulQuestion, SystemOneRequest
332
+ from logit_classifier.backends.comfy_clip import ComfyClipBackend
333
+
334
+ classifier = Classifier(Config(), backend=ComfyClipBackend(clip))
335
+ question = NoulQuestion(instructions="This image visibly contains a dragon")
336
+ request = SystemOneRequest(state="", questions={"dragon": question})
337
+ response, _ = classifier.classify(request, image=image)
338
+ ```
339
+
340
+ The `image` keyword takes an image the host already decoded, such as a ComfyUI IMAGE of
341
+ shape `[1, H, W, 3]`. An empty state asks about the image alone. Questions share one forward
342
+ pass of up to 4,096 tokens, and the image counts toward that limit. Any text encoder other
343
+ than Qwen3-VL raises `UnsupportedModelError`.
344
+
323
345
  ## Differences From Jev
324
346
 
325
347
  This project matches the System One request and response format and the documented limits.
@@ -329,8 +351,8 @@ Jev is trained for calibrated probabilities. This project reads them from a gene
329
351
  so the choices agree more often than the confidences do.
330
352
 
331
353
  Jev judges a score level without its number or its neighbours. This project judges all
332
- levels together by default. Set `LOGIT_SCORE_METHOD=independent` for the documented
333
- behavior.
354
+ levels together by default. The service follows the documented behavior with
355
+ `LOGIT_SCORE_METHOD=independent`, and library code with `Config(score_method="independent")`.
334
356
 
335
357
  Jev publishes status codes but no error body. The error shape here is our own.
336
358
 
@@ -55,8 +55,8 @@ beside your project instead, which is what the examples do.
55
55
  Config(models_dir=Path("models"))
56
56
  ```
57
57
 
58
- `LOGIT_MODELS_DIR` sets the same folder for every process, and `HF_HOME` moves the
59
- Hugging Face cache itself.
58
+ `LOGIT_MODELS_DIR` sets the same folder for the service and for `Config.from_env()`.
59
+ `HF_HOME` moves the Hugging Face cache itself.
60
60
 
61
61
  ## Models
62
62
 
@@ -65,8 +65,9 @@ Hugging Face cache itself.
65
65
  | reads images | yes | no |
66
66
  | better at | choice and score | noul |
67
67
 
68
- `Qwen3-VL-4B-Instruct` is the default. Set `LOGIT_MODEL_ID` to use the other one. Any Qwen
69
- chat model loads, and a model with no fitted temperature gets 2.5.
68
+ `Qwen3-VL-4B-Instruct` is the default. Set `LOGIT_MODEL_ID` to use the other one in the
69
+ service or through `Config.from_env()`. Any Qwen chat model loads, and a model with no
70
+ fitted temperature gets 2.5.
70
71
 
71
72
  ## Python API
72
73
 
@@ -136,7 +137,7 @@ the model.
136
137
 
137
138
  ```json
138
139
  {
139
- "model": "logit-classifier-0.1.0",
140
+ "model": "logit-classifier-0.2.1",
140
141
  "answers": {
141
142
  "department": {
142
143
  "type": "choice",
@@ -177,7 +178,7 @@ only in process.
177
178
  "questions": { "sky": { "type": "noul", "instructions": "This crop shows the night sky" } } }
178
179
  ```
179
180
 
180
- The image is encoded once for the entire request, so asking several questions about one
181
+ The image is encoded once for the entire request. So asking several questions about one
181
182
  picture costs little more than asking one.
182
183
 
183
184
  A model with no vision tower rejects the request rather than ignoring the picture.
@@ -197,8 +198,9 @@ Each script runs on its own.
197
198
 
198
199
  ## Configuration
199
200
 
200
- Every setting reads from the environment at startup. `logit-classifier config` prints what
201
- they produce.
201
+ The service and `logit-classifier config` read these variables at startup through
202
+ `Config.from_env()`. Library code gets them by calling `Config.from_env()`, since `Config()`
203
+ reads none of them. `logit-classifier config` prints what they produce.
202
204
 
203
205
  | Variable | Default | Effect |
204
206
  |---|---|---|
@@ -209,6 +211,7 @@ they produce.
209
211
  | `LOGIT_PRIOR_DEBIAS` | `1` | set to `0` to skip the label prior |
210
212
  | `LOGIT_BATCH_BRANCHES` | `1` | set to `0` for one forward pass per branch |
211
213
  | `LOGIT_SCORE_METHOD` | `joint` | set to `independent` to judge each level alone |
214
+ | `LOGIT_PERMUTATIONS` | `1` | letterings averaged per question, each one adds branches |
212
215
  | `LOGIT_ABSTAIN` | `1` | set to `0` to drop the `none of these` label below 52 options |
213
216
  | `LOGIT_CALIBRATION_PATH` | `calibration.json` | where the service stores the running prior |
214
217
 
@@ -236,9 +239,10 @@ Nothing is sampled, so the same request returns bitwise identical logits.
236
239
 
237
240
  Batch composition and padding length both change the low bits of a bfloat16 forward pass,
238
241
  and both are pure functions of the request. So the same question asked inside two
239
- different requests can differ slightly. Set `LOGIT_BATCH_BRANCHES=0` to score each branch
240
- alone, which makes a question independent of the questions sent with it and costs one
241
- forward pass per branch.
242
+ different requests can differ slightly. Scoring each branch alone makes a question
243
+ independent of the questions sent with it and costs one forward pass per branch. The service
244
+ turns it on with `LOGIT_BATCH_BRANCHES=0`. Library code passes `Config(batch_branches=False)`,
245
+ and `ComfyClipBackend` takes `batch_branches=False` as a keyword.
242
246
 
243
247
  A forward pass needs several process-global torch settings held at known values. Torch
244
248
  exposes none of them as a call argument, so the backend sets them around each pass and
@@ -287,6 +291,24 @@ the render and label contracts and returns one token id per label.
287
291
  `logit_classifier.backends.hf.HFBackend` is the transformers implementation to read
288
292
  against.
289
293
 
294
+ `ComfyClipBackend` is the backend over a Qwen3-VL text encoder that the workflow already
295
+ loaded, such as the one Krea 2 uses. So no second model is loaded into VRAM.
296
+
297
+ ```python
298
+ from logit_classifier import Classifier, Config, NoulQuestion, SystemOneRequest
299
+ from logit_classifier.backends.comfy_clip import ComfyClipBackend
300
+
301
+ classifier = Classifier(Config(), backend=ComfyClipBackend(clip))
302
+ question = NoulQuestion(instructions="This image visibly contains a dragon")
303
+ request = SystemOneRequest(state="", questions={"dragon": question})
304
+ response, _ = classifier.classify(request, image=image)
305
+ ```
306
+
307
+ The `image` keyword takes an image the host already decoded, such as a ComfyUI IMAGE of
308
+ shape `[1, H, W, 3]`. An empty state asks about the image alone. Questions share one forward
309
+ pass of up to 4,096 tokens, and the image counts toward that limit. Any text encoder other
310
+ than Qwen3-VL raises `UnsupportedModelError`.
311
+
290
312
  ## Differences From Jev
291
313
 
292
314
  This project matches the System One request and response format and the documented limits.
@@ -296,8 +318,8 @@ Jev is trained for calibrated probabilities. This project reads them from a gene
296
318
  so the choices agree more often than the confidences do.
297
319
 
298
320
  Jev judges a score level without its number or its neighbours. This project judges all
299
- levels together by default. Set `LOGIT_SCORE_METHOD=independent` for the documented
300
- behavior.
321
+ levels together by default. The service follows the documented behavior with
322
+ `LOGIT_SCORE_METHOD=independent`, and library code with `Config(score_method="independent")`.
301
323
 
302
324
  Jev publishes status codes but no error body. The error shape here is our own.
303
325
 
@@ -19,7 +19,8 @@ from pathlib import Path
19
19
  from logit_classifier import Classifier, Config, load_model, parse_request
20
20
 
21
21
  # Weights land beside the project instead of in the global Hugging Face cache.
22
- # Set LOGIT_MODELS_DIR, or HF_HOME, to keep them somewhere shared across projects.
22
+ # Edit MODELS_DIR to share one folder across projects.
23
+ # Neither LOGIT_MODELS_DIR nor HF_HOME reaches this script.
23
24
  MODELS_DIR = Path(__file__).resolve().parent.parent / "models"
24
25
 
25
26
  MODELS = ["Qwen/Qwen3-VL-4B-Instruct", "Qwen/Qwen3-4B-Instruct-2507"]
@@ -23,7 +23,8 @@ from logit_classifier import (
23
23
  )
24
24
 
25
25
  # Weights land beside the project instead of in the global Hugging Face cache.
26
- # Set LOGIT_MODELS_DIR, or HF_HOME, to keep them somewhere shared across projects.
26
+ # Edit MODELS_DIR to share one folder across projects.
27
+ # Neither LOGIT_MODELS_DIR nor HF_HOME reaches this script.
27
28
  MODELS_DIR = Path(__file__).resolve().parent.parent / "models"
28
29
  QUESTIONS = {
29
30
  "subject": {
@@ -14,7 +14,8 @@ from pathlib import Path
14
14
  from logit_classifier import Classifier, Config, load_model, parse_request
15
15
 
16
16
  # Weights land beside the project instead of in the global Hugging Face cache.
17
- # Set LOGIT_MODELS_DIR, or HF_HOME, to keep them somewhere shared across projects.
17
+ # Edit MODELS_DIR to share one folder across projects.
18
+ # Neither LOGIT_MODELS_DIR nor HF_HOME reaches this script.
18
19
  MODELS_DIR = Path(__file__).resolve().parent.parent / "models"
19
20
 
20
21
  REVIEW = "Shipped two days late and the box was crushed, but the product itself works fine."
@@ -16,7 +16,8 @@ from pathlib import Path
16
16
  from logit_classifier import Classifier, Config, load_model, parse_request
17
17
 
18
18
  # Weights land beside the project instead of in the global Hugging Face cache.
19
- # Set LOGIT_MODELS_DIR, or HF_HOME, to keep them somewhere shared across projects.
19
+ # Edit MODELS_DIR to share one folder across projects.
20
+ # Neither LOGIT_MODELS_DIR nor HF_HOME reaches this script.
20
21
  MODELS_DIR = Path(__file__).resolve().parent.parent / "models"
21
22
 
22
23
  SAMPLE_TEXT = "I have been trying to connect my Stripe account for 3 days and it keeps failing."
@@ -136,7 +136,8 @@ show_error_codes = true
136
136
  [[tool.mypy.overrides]]
137
137
  # transformers publishes no complete stubs, and accelerate none at all. torch is
138
138
  # listed so the type gate still runs where the hf extra is absent, such as CI.
139
- module = ["transformers.*", "accelerate.*", "torch.*"]
139
+ # The ComfyUI host supplies comfy, and neither this venv nor CI has it.
140
+ module = ["transformers.*", "accelerate.*", "torch.*", "comfy.*"]
140
141
  ignore_missing_imports = true
141
142
 
142
143
  [[tool.mypy.overrides]]
@@ -148,6 +149,12 @@ module = ["logit_classifier.backends.hf"]
148
149
  disallow_untyped_calls = false
149
150
  disallow_untyped_decorators = false
150
151
 
152
+ [[tool.mypy.overrides]]
153
+ # torch leaves fp16_bf16_reduction_math_sdp_allowed unannotated, and a line ignore
154
+ # would go unused, and so fail, where the hf extra is absent and torch is Any.
155
+ module = ["logit_classifier.backends._torch_window"]
156
+ disallow_untyped_calls = false
157
+
151
158
  [tool.pytest.ini_options]
152
159
  pythonpath = ["src"]
153
160
  testpaths = ["tests"]
@@ -159,7 +166,7 @@ requires = ["hatchling"]
159
166
  build-backend = "hatchling.build"
160
167
 
161
168
  [tool.hatch.version]
162
- path = "src/logit_classifier/__init__.py"
169
+ path = "src/logit_classifier/_version.py"
163
170
 
164
171
  [tool.hatch.build.targets.wheel]
165
172
  packages = ["src/logit_classifier"]
@@ -9,10 +9,12 @@ with the `[hf]` extra and the HTTP service with `[service]`.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ from ._version import __version__
12
13
  from .backends.base import (
13
14
  Backend,
14
15
  BackendContractError,
15
16
  BranchLogits,
17
+ UnsupportedModelError,
16
18
  VisionUnsupportedError,
17
19
  verify_backend,
18
20
  )
@@ -45,8 +47,6 @@ from .schema import (
45
47
  )
46
48
  from .vision import ImageError
47
49
 
48
- __version__ = "0.1.0"
49
-
50
50
  # The ComfyUI socket type a node pack declares for a loaded classifier. It lives
51
51
  # here so the library and the packs cannot drift apart on the spelling.
52
52
  COMFY_SOCKET_TYPE = "LOGIT_CLASSIFIER"
@@ -79,6 +79,7 @@ __all__ = [
79
79
  "ScoreQuestion",
80
80
  "SystemOneRequest",
81
81
  "SystemOneResponse",
82
+ "UnsupportedModelError",
82
83
  "Usage",
83
84
  "VisionUnsupportedError",
84
85
  "__version__",
@@ -0,0 +1,3 @@
1
+ """The package version, apart from __init__ so config.py can read it without a cycle."""
2
+
3
+ __version__ = "0.2.1"
@@ -0,0 +1,96 @@
1
+ """The determinism window every backend's forward pass runs inside.
2
+
3
+ torch is imported inside the window rather than at module scope, so a backend over a
4
+ host's own model can import this without pulling torch into a core-only process.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import threading
10
+ from collections.abc import Iterator
11
+ from contextlib import contextmanager
12
+ from typing import Any
13
+
14
+ # Two windows open on two threads would interleave their saves, and the second to
15
+ # exit would restore the pinned values rather than the host's. The service already
16
+ # serialises on its own GPU lock, which is always taken before this one.
17
+ _WINDOW = threading.RLock()
18
+
19
+
20
+ @contextmanager
21
+ def _determinism() -> Iterator[None]:
22
+ """Hold the torch globals that decide bit-exact reductions, for one forward pass.
23
+
24
+ Every alternative value buys speed by giving up bit-exactness, so none is tunable.
25
+ torch exposes none of them as a call argument, so scoping them means setting them
26
+ here and putting the host's values back after. A ComfyUI host sharing this process
27
+ keeps its own settings everywhere outside the block.
28
+
29
+ On the CUDA attention path only allow_bf16_reduced_precision_reduction moves a logit
30
+ on either shipped model. The rest are kept because cudnn picks convolution algorithms
31
+ by timing, which is specific to the card, and `ab_determinism_scope.py` measured one.
32
+
33
+ The sdp and fp16 accumulation settings are held for a third reason, that ComfyUI turns
34
+ each of them on and neither is reachable by that sweep. `ab_math_sdp_reduction.py`
35
+ measures the sdp one on the math backend.
36
+
37
+ Where torch has per-backend matmul slots, the coarse getter raises once a host has
38
+ set a slot directly, so only the raw slots are saved, pinned and put back there.
39
+ """
40
+ import torch
41
+
42
+ with _WINDOW:
43
+ saved_benchmark = torch.backends.cudnn.benchmark
44
+ saved_deterministic = torch.backends.cudnn.deterministic
45
+ saved_bf16 = torch.backends.cuda.matmul.allow_bf16_reduced_precision_reduction
46
+ # torch ships no stub for the mkldnn matmul slot, so it is reached by name.
47
+ mkldnn: Any = getattr(torch.backends.mkldnn, "matmul", None)
48
+ has_slots = hasattr(torch.backends.cuda.matmul, "fp32_precision") and hasattr(
49
+ mkldnn, "fp32_precision"
50
+ )
51
+ saved_matmul = None if has_slots else torch.get_float32_matmul_precision()
52
+ saved_cuda = torch.backends.cuda.matmul.fp32_precision if has_slots else None
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.
56
+ has_sdp = hasattr(torch.backends.cuda, "allow_fp16_bf16_reduction_math_sdp") and hasattr(
57
+ torch.backends.cuda, "fp16_bf16_reduction_math_sdp_allowed"
58
+ )
59
+ saved_sdp = bool(
60
+ has_sdp and torch.backends.cuda.fp16_bf16_reduction_math_sdp_allowed()
61
+ )
62
+ # A bare --fast turns this on, since comfy/cli_args.py then enables every
63
+ # PerformanceFeature. It is the fp16 sibling of the bf16 reduction above.
64
+ has_fp16_acc = hasattr(torch.backends.cuda.matmul, "allow_fp16_accumulation")
65
+ saved_fp16_acc = bool(
66
+ has_fp16_acc and torch.backends.cuda.matmul.allow_fp16_accumulation
67
+ )
68
+
69
+ # Pinning sits inside the try, so a setter that raises part way still restores the host.
70
+ try:
71
+ torch.backends.cudnn.benchmark = False
72
+ torch.backends.cudnn.deterministic = True
73
+ torch.backends.cuda.matmul.allow_bf16_reduced_precision_reduction = False
74
+ if has_slots:
75
+ torch.backends.cuda.matmul.fp32_precision = "ieee"
76
+ mkldnn.fp32_precision = "ieee"
77
+ else:
78
+ torch.set_float32_matmul_precision("highest")
79
+ if has_sdp:
80
+ torch.backends.cuda.allow_fp16_bf16_reduction_math_sdp(False)
81
+ if has_fp16_acc:
82
+ torch.backends.cuda.matmul.allow_fp16_accumulation = False
83
+ yield
84
+ finally:
85
+ torch.backends.cudnn.benchmark = saved_benchmark
86
+ torch.backends.cudnn.deterministic = saved_deterministic
87
+ torch.backends.cuda.matmul.allow_bf16_reduced_precision_reduction = saved_bf16
88
+ if has_slots:
89
+ torch.backends.cuda.matmul.fp32_precision = saved_cuda
90
+ mkldnn.fp32_precision = saved_mkldnn
91
+ elif saved_matmul is not None:
92
+ torch.set_float32_matmul_precision(saved_matmul)
93
+ if has_sdp:
94
+ torch.backends.cuda.allow_fp16_bf16_reduction_math_sdp(saved_sdp)
95
+ if has_fp16_acc:
96
+ torch.backends.cuda.matmul.allow_fp16_accumulation = saved_fp16_acc
@@ -32,6 +32,10 @@ class BackendContractError(LogitClassifierError, RuntimeError):
32
32
  """A backend returned rows the port does not allow."""
33
33
 
34
34
 
35
+ class UnsupportedModelError(LogitClassifierError, ValueError):
36
+ """A host passed a loaded model this backend cannot read a logit row from."""
37
+
38
+
35
39
  @dataclass(frozen=True)
36
40
  class BranchLogits:
37
41
  """Raw label logits for one branch, before any calibration."""