neuralk-oss 0.1.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 (57) hide show
  1. neuralk_oss-0.1.0/.github/workflows/release.yml +75 -0
  2. neuralk_oss-0.1.0/.gitignore +18 -0
  3. neuralk_oss-0.1.0/.python-version +1 -0
  4. neuralk_oss-0.1.0/LICENSE +21 -0
  5. neuralk_oss-0.1.0/PKG-INFO +121 -0
  6. neuralk_oss-0.1.0/README.md +101 -0
  7. neuralk_oss-0.1.0/pyproject.toml +51 -0
  8. neuralk_oss-0.1.0/src/neuralk_oss/__init__.py +7 -0
  9. neuralk_oss-0.1.0/src/neuralk_oss/inference/__init__.py +7 -0
  10. neuralk_oss-0.1.0/src/neuralk_oss/inference/classifier.py +165 -0
  11. neuralk_oss-0.1.0/src/neuralk_oss/inference/estimator.py +139 -0
  12. neuralk_oss-0.1.0/src/neuralk_oss/inference/recipe.py +117 -0
  13. neuralk_oss-0.1.0/src/neuralk_oss/model/__init__.py +6 -0
  14. neuralk_oss-0.1.0/src/neuralk_oss/model/attention.py +340 -0
  15. neuralk_oss-0.1.0/src/neuralk_oss/model/col.py +141 -0
  16. neuralk_oss-0.1.0/src/neuralk_oss/model/config.py +182 -0
  17. neuralk_oss-0.1.0/src/neuralk_oss/model/icl.py +85 -0
  18. neuralk_oss-0.1.0/src/neuralk_oss/model/kernels.py +62 -0
  19. neuralk_oss-0.1.0/src/neuralk_oss/model/row.py +77 -0
  20. neuralk_oss-0.1.0/src/neuralk_oss/model/seldon.py +289 -0
  21. neuralk_oss-0.1.0/src/neuralk_oss/py.typed +0 -0
  22. neuralk_oss-0.1.0/tests/__init__.py +0 -0
  23. neuralk_oss-0.1.0/tests/conftest.py +34 -0
  24. neuralk_oss-0.1.0/tests/golden/expected/classifier-binary_numerical.npy +0 -0
  25. neuralk_oss-0.1.0/tests/golden/expected/classifier-degenerate_columns.npy +0 -0
  26. neuralk_oss-0.1.0/tests/golden/expected/classifier-high_cardinality_categorical.npy +0 -0
  27. neuralk_oss-0.1.0/tests/golden/expected/classifier-many_classes.npy +0 -0
  28. neuralk_oss-0.1.0/tests/golden/expected/classifier-multiclass_mixed.npy +0 -0
  29. neuralk_oss-0.1.0/tests/golden/expected/classifier-numpy_input.npy +0 -0
  30. neuralk_oss-0.1.0/tests/golden/expected/classifier-single_query_row.npy +0 -0
  31. neuralk_oss-0.1.0/tests/golden/expected/classifier-subsampled_context.npy +0 -0
  32. neuralk_oss-0.1.0/tests/golden/expected/classifier-unseen_category.npy +0 -0
  33. neuralk_oss-0.1.0/tests/golden/expected/classifier-variant_checkpoint.npy +0 -0
  34. neuralk_oss-0.1.0/tests/golden/expected/cuda/classifier-binary_numerical.npy +0 -0
  35. neuralk_oss-0.1.0/tests/golden/expected/cuda/classifier-degenerate_columns.npy +0 -0
  36. neuralk_oss-0.1.0/tests/golden/expected/cuda/classifier-high_cardinality_categorical.npy +0 -0
  37. neuralk_oss-0.1.0/tests/golden/expected/cuda/classifier-many_classes.npy +0 -0
  38. neuralk_oss-0.1.0/tests/golden/expected/cuda/classifier-multiclass_mixed.npy +0 -0
  39. neuralk_oss-0.1.0/tests/golden/expected/cuda/classifier-numpy_input.npy +0 -0
  40. neuralk_oss-0.1.0/tests/golden/expected/cuda/classifier-single_query_row.npy +0 -0
  41. neuralk_oss-0.1.0/tests/golden/expected/cuda/classifier-subsampled_context.npy +0 -0
  42. neuralk_oss-0.1.0/tests/golden/expected/cuda/classifier-unseen_category.npy +0 -0
  43. neuralk_oss-0.1.0/tests/golden/expected/cuda/classifier-variant_checkpoint.npy +0 -0
  44. neuralk_oss-0.1.0/tests/golden/expected/model-seldon-tiny-classifier-variant.npy +0 -0
  45. neuralk_oss-0.1.0/tests/golden/expected/model-seldon-tiny-classifier.npy +0 -0
  46. neuralk_oss-0.1.0/tests/golden/expected/model-seldon-tiny-regressor.npy +0 -0
  47. neuralk_oss-0.1.0/tests/golden/make_checkpoints.py +47 -0
  48. neuralk_oss-0.1.0/tests/golden/seldon-tiny-classifier/config.json +32 -0
  49. neuralk_oss-0.1.0/tests/golden/seldon-tiny-classifier/model.safetensors +0 -0
  50. neuralk_oss-0.1.0/tests/golden/seldon-tiny-classifier-variant/config.json +32 -0
  51. neuralk_oss-0.1.0/tests/golden/seldon-tiny-classifier-variant/model.safetensors +0 -0
  52. neuralk_oss-0.1.0/tests/golden/seldon-tiny-regressor/config.json +32 -0
  53. neuralk_oss-0.1.0/tests/golden/seldon-tiny-regressor/model.safetensors +0 -0
  54. neuralk_oss-0.1.0/tests/test_classifier.py +61 -0
  55. neuralk_oss-0.1.0/tests/test_golden.py +186 -0
  56. neuralk_oss-0.1.0/tests/test_model.py +103 -0
  57. neuralk_oss-0.1.0/uv.lock +1393 -0
@@ -0,0 +1,75 @@
1
+ # Builds the sdist and wheel and publishes them: to TestPyPI when run by hand
2
+ # from the Actions tab, to PyPI when a `v*` tag is pushed (the tag must match
3
+ # the version in pyproject.toml). Trusted Publishing: each index trusts this
4
+ # workflow in its environment (`testpypi`, `pypi`), no token is stored. A
5
+ # version is uploaded once per index; bump it in pyproject.toml before another
6
+ # run.
7
+ name: Release
8
+
9
+ on:
10
+ workflow_dispatch:
11
+ push:
12
+ tags: ["v*"]
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ build:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
22
+ with:
23
+ persist-credentials: false
24
+ - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
25
+ - name: Check the tag matches the version
26
+ if: github.ref_type == 'tag'
27
+ env:
28
+ TAG: ${{ github.ref_name }}
29
+ run: |
30
+ version=$(uv version --short)
31
+ if [ "$TAG" != "v$version" ]; then
32
+ echo "::error::tag $TAG does not match version $version in pyproject.toml"
33
+ exit 1
34
+ fi
35
+ - run: uv build
36
+ - run: uvx twine check --strict dist/*
37
+ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
38
+ with:
39
+ name: dist
40
+ path: dist/
41
+ if-no-files-found: error
42
+
43
+ testpypi:
44
+ if: github.event_name == 'workflow_dispatch'
45
+ needs: build
46
+ runs-on: ubuntu-latest
47
+ environment:
48
+ name: testpypi
49
+ url: https://test.pypi.org/p/neuralk-oss
50
+ permissions:
51
+ id-token: write # Trusted Publishing
52
+ steps:
53
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
54
+ with:
55
+ name: dist
56
+ path: dist/
57
+ - uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
58
+ with:
59
+ repository-url: https://test.pypi.org/legacy/
60
+
61
+ pypi:
62
+ if: github.event_name == 'push' && github.ref_type == 'tag'
63
+ needs: build
64
+ runs-on: ubuntu-latest
65
+ environment:
66
+ name: pypi
67
+ url: https://pypi.org/p/neuralk-oss
68
+ permissions:
69
+ id-token: write # Trusted Publishing
70
+ steps:
71
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
72
+ with:
73
+ name: dist
74
+ path: dist/
75
+ - uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
@@ -0,0 +1,18 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .coverage
10
+ htmlcov/
11
+ .DS_Store
12
+
13
+ # Model weights
14
+ *.pt
15
+ *.pth
16
+ *.ckpt
17
+ *.safetensors
18
+ !tests/golden/**/*.safetensors
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Neuralk-AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.5
2
+ Name: neuralk-oss
3
+ Version: 0.1.0
4
+ Summary: Neuralk-AI open-source models: loading and inference
5
+ Project-URL: Repository, https://github.com/Neuralk-AI/neuralk_oss
6
+ Author: Neuralk-AI
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
11
+ Requires-Python: >=3.11
12
+ Requires-Dist: huggingface-hub
13
+ Requires-Dist: numpy
14
+ Requires-Dist: pandas<3,>=2.2
15
+ Requires-Dist: pydantic>=2
16
+ Requires-Dist: safetensors
17
+ Requires-Dist: structured-data-models==0.2.1
18
+ Requires-Dist: torch>=2.7
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Neuralk OSS
22
+
23
+ Neuralk-AI open-source models.
24
+
25
+ ## Seldon
26
+
27
+ Seldon is a tabular in-context-learning foundation model: `fit` keeps the
28
+ training table as context, `predict_proba` classifies new rows against it in
29
+ one forward pass, with no gradient step.
30
+
31
+ ```python
32
+ from neuralk_oss import SeldonClassifier
33
+
34
+ clf = SeldonClassifier.from_pretrained("seldon-small-classifier")
35
+ clf.fit(X_train, y_train) # pandas DataFrame (or array) and labels
36
+ proba = clf.predict_proba(X_test) # (n_rows, n_classes), columns in clf.classes_ order
37
+ ```
38
+
39
+ ### Checkpoints
40
+
41
+ | name | task | parameters |
42
+ | --- | --- | --- |
43
+ | `seldon-small-classifier` | classification, up to 10 classes per model call | 27.4M |
44
+
45
+ A checkpoint is a directory holding `config.json` and `model.safetensors`.
46
+ `from_pretrained` takes:
47
+
48
+ - a checkpoint name, such as `"seldon-small-classifier"`: downloaded from the
49
+ Hugging Face repository [`Neuralk-AI/seldon`](https://huggingface.co/Neuralk-AI/seldon),
50
+ where each checkpoint is a directory, on the first call, then read from the
51
+ Hugging Face cache (`~/.cache/huggingface`, or `HF_HOME`);
52
+ - a local directory, read in place: `from_pretrained("path/to/seldon-small-classifier")`;
53
+ - any Hugging Face repository, with the checkpoint's directory and a revision:
54
+ `from_pretrained("org/repo", subfolder="seldon-small-classifier", revision="v1")`.
55
+
56
+ The repository is private for now: log in once with an account that can read
57
+ it (`hf auth login`), or pass `token=...` to `from_pretrained`.
58
+
59
+ Column types are inferred (categorical columns are embedded as such, NaNs are
60
+ kept for the model's own imputation), each of the `n_estimators` ensemble
61
+ members (8 by default) sees its own preprocessing, and their logits are
62
+ averaged. Inference runs in float16 autocast on CUDA by default
63
+ (`amp=False` for float32). Tables with more classes than the model's head
64
+ (10) go through error-correcting output codes.
65
+
66
+ ### Preprocessing
67
+
68
+ The preprocessing and ensembling come from structured-data-models (`sdm`,
69
+ Apache-2.0), pinned in `pyproject.toml`. `SeldonRecipe` builds its
70
+ recipe one overridable method per step:
71
+
72
+ ```python
73
+ import sdm.processing as sp
74
+ from neuralk_oss import SeldonClassifier, SeldonRecipe
75
+
76
+
77
+ class MyRecipe(SeldonRecipe):
78
+ max_features = 200
79
+
80
+ def numerical_transforms(self):
81
+ return [sp.Identity(), sp.RankGaussian(max_knots=self.rank_gaussian_knots)]
82
+
83
+
84
+ clf = SeldonClassifier.from_pretrained("seldon-small-classifier", recipe=MyRecipe())
85
+ ```
86
+
87
+ ### The model alone
88
+
89
+ ```python
90
+ from neuralk_oss import Seldon
91
+
92
+ model = Seldon.from_pretrained("seldon-small-classifier")
93
+ context = model.encode_context(X_context, y_context, categorical_mask) # (B, T, H), (B, T), (B, H)
94
+ logits = model.predict_query(X_query, context, categorical_mask) # (B, T_query, 10)
95
+ ```
96
+
97
+ ## Layout
98
+
99
+ ```
100
+ src/neuralk_oss/
101
+ ├── model/ # Seldon's architecture, config and checkpoint format
102
+ └── inference/ # sdm estimator, preprocessing recipe, scikit-learn style classifier
103
+ tests/
104
+ ```
105
+
106
+ ## Development
107
+
108
+ With [uv](https://docs.astral.sh/uv/):
109
+
110
+ ```bash
111
+ uv sync
112
+ uv run pytest
113
+ uv run ruff check . && uv run ruff format --check .
114
+ ```
115
+
116
+ `tests/test_golden.py` compares the outputs of tiny random-weight checkpoints
117
+ (`tests/golden/`) to saved references. After an intended change of outputs,
118
+ regenerate them with `UPDATE_GOLDEN=1 uv run pytest tests/test_golden.py` and
119
+ commit the new `tests/golden/expected/`. With CUDA, the same cases also run on
120
+ the GPU in float16 autocast; the classifier's CUDA references
121
+ (`tests/golden/expected/cuda/`) are only regenerated on a CUDA machine.
@@ -0,0 +1,101 @@
1
+ # Neuralk OSS
2
+
3
+ Neuralk-AI open-source models.
4
+
5
+ ## Seldon
6
+
7
+ Seldon is a tabular in-context-learning foundation model: `fit` keeps the
8
+ training table as context, `predict_proba` classifies new rows against it in
9
+ one forward pass, with no gradient step.
10
+
11
+ ```python
12
+ from neuralk_oss import SeldonClassifier
13
+
14
+ clf = SeldonClassifier.from_pretrained("seldon-small-classifier")
15
+ clf.fit(X_train, y_train) # pandas DataFrame (or array) and labels
16
+ proba = clf.predict_proba(X_test) # (n_rows, n_classes), columns in clf.classes_ order
17
+ ```
18
+
19
+ ### Checkpoints
20
+
21
+ | name | task | parameters |
22
+ | --- | --- | --- |
23
+ | `seldon-small-classifier` | classification, up to 10 classes per model call | 27.4M |
24
+
25
+ A checkpoint is a directory holding `config.json` and `model.safetensors`.
26
+ `from_pretrained` takes:
27
+
28
+ - a checkpoint name, such as `"seldon-small-classifier"`: downloaded from the
29
+ Hugging Face repository [`Neuralk-AI/seldon`](https://huggingface.co/Neuralk-AI/seldon),
30
+ where each checkpoint is a directory, on the first call, then read from the
31
+ Hugging Face cache (`~/.cache/huggingface`, or `HF_HOME`);
32
+ - a local directory, read in place: `from_pretrained("path/to/seldon-small-classifier")`;
33
+ - any Hugging Face repository, with the checkpoint's directory and a revision:
34
+ `from_pretrained("org/repo", subfolder="seldon-small-classifier", revision="v1")`.
35
+
36
+ The repository is private for now: log in once with an account that can read
37
+ it (`hf auth login`), or pass `token=...` to `from_pretrained`.
38
+
39
+ Column types are inferred (categorical columns are embedded as such, NaNs are
40
+ kept for the model's own imputation), each of the `n_estimators` ensemble
41
+ members (8 by default) sees its own preprocessing, and their logits are
42
+ averaged. Inference runs in float16 autocast on CUDA by default
43
+ (`amp=False` for float32). Tables with more classes than the model's head
44
+ (10) go through error-correcting output codes.
45
+
46
+ ### Preprocessing
47
+
48
+ The preprocessing and ensembling come from structured-data-models (`sdm`,
49
+ Apache-2.0), pinned in `pyproject.toml`. `SeldonRecipe` builds its
50
+ recipe one overridable method per step:
51
+
52
+ ```python
53
+ import sdm.processing as sp
54
+ from neuralk_oss import SeldonClassifier, SeldonRecipe
55
+
56
+
57
+ class MyRecipe(SeldonRecipe):
58
+ max_features = 200
59
+
60
+ def numerical_transforms(self):
61
+ return [sp.Identity(), sp.RankGaussian(max_knots=self.rank_gaussian_knots)]
62
+
63
+
64
+ clf = SeldonClassifier.from_pretrained("seldon-small-classifier", recipe=MyRecipe())
65
+ ```
66
+
67
+ ### The model alone
68
+
69
+ ```python
70
+ from neuralk_oss import Seldon
71
+
72
+ model = Seldon.from_pretrained("seldon-small-classifier")
73
+ context = model.encode_context(X_context, y_context, categorical_mask) # (B, T, H), (B, T), (B, H)
74
+ logits = model.predict_query(X_query, context, categorical_mask) # (B, T_query, 10)
75
+ ```
76
+
77
+ ## Layout
78
+
79
+ ```
80
+ src/neuralk_oss/
81
+ ├── model/ # Seldon's architecture, config and checkpoint format
82
+ └── inference/ # sdm estimator, preprocessing recipe, scikit-learn style classifier
83
+ tests/
84
+ ```
85
+
86
+ ## Development
87
+
88
+ With [uv](https://docs.astral.sh/uv/):
89
+
90
+ ```bash
91
+ uv sync
92
+ uv run pytest
93
+ uv run ruff check . && uv run ruff format --check .
94
+ ```
95
+
96
+ `tests/test_golden.py` compares the outputs of tiny random-weight checkpoints
97
+ (`tests/golden/`) to saved references. After an intended change of outputs,
98
+ regenerate them with `UPDATE_GOLDEN=1 uv run pytest tests/test_golden.py` and
99
+ commit the new `tests/golden/expected/`. With CUDA, the same cases also run on
100
+ the GPU in float16 autocast; the classifier's CUDA references
101
+ (`tests/golden/expected/cuda/`) are only regenerated on a CUDA machine.
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "neuralk-oss"
7
+ version = "0.1.0"
8
+ description = "Neuralk-AI open-source models: loading and inference"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Neuralk-AI" }]
13
+ requires-python = ">=3.11"
14
+ dependencies = [
15
+ "huggingface_hub",
16
+ "numpy",
17
+ "pandas>=2.2,<3",
18
+ "pydantic>=2",
19
+ "safetensors",
20
+ # Pinned: the preprocessing and ensembling Seldon was evaluated with.
21
+ "structured-data-models==0.2.1",
22
+ "torch>=2.7",
23
+ ]
24
+ classifiers = [
25
+ "Programming Language :: Python :: 3",
26
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
27
+ ]
28
+
29
+ [project.urls]
30
+ Repository = "https://github.com/Neuralk-AI/neuralk_oss"
31
+
32
+ [dependency-groups]
33
+ dev = [
34
+ "pytest>=8",
35
+ "ruff>=0.6",
36
+ ]
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["src/neuralk_oss"]
40
+
41
+ [tool.pytest.ini_options]
42
+ testpaths = ["tests"]
43
+
44
+ [tool.ruff]
45
+ line-length = 100
46
+ target-version = "py311"
47
+
48
+ [tool.ruff.lint]
49
+ select = ["E", "F", "I", "UP", "B"]
50
+ # The formatter wraps code; docstring summaries stay on one line.
51
+ ignore = ["E501"]
@@ -0,0 +1,7 @@
1
+ """Neuralk-AI open-source models."""
2
+
3
+ from neuralk_oss.inference import SeldonClassifier, SeldonICL, SeldonRecipe
4
+ from neuralk_oss.model import Seldon, SeldonConfig
5
+
6
+ __all__ = ["Seldon", "SeldonClassifier", "SeldonConfig", "SeldonICL", "SeldonRecipe"]
7
+ __version__ = "0.1.0"
@@ -0,0 +1,7 @@
1
+ """Inference: Seldon behind sdm's preprocessing and ensembling, as a classifier."""
2
+
3
+ from neuralk_oss.inference.classifier import SeldonClassifier
4
+ from neuralk_oss.inference.estimator import SeldonICL
5
+ from neuralk_oss.inference.recipe import SeldonRecipe
6
+
7
+ __all__ = ["SeldonClassifier", "SeldonICL", "SeldonRecipe"]
@@ -0,0 +1,165 @@
1
+ """Seldon as a scikit-learn style classifier over pandas tables."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from collections.abc import Iterator
7
+ from contextlib import contextmanager, nullcontext
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+ import pandas as pd
12
+ import sdm
13
+ import torch
14
+ from torch.nn.attention import SDPBackend, sdpa_kernel
15
+
16
+ from neuralk_oss.inference.estimator import SeldonICL
17
+ from neuralk_oss.inference.recipe import SeldonRecipe
18
+ from neuralk_oss.model import Seldon
19
+
20
+ _TARGET = "target"
21
+ # Every attention kernel PyTorch has but cuDNN's (see ``cudnn_attention``).
22
+ _NON_CUDNN_ATTENTION = [
23
+ SDPBackend.FLASH_ATTENTION,
24
+ SDPBackend.EFFICIENT_ATTENTION,
25
+ SDPBackend.MATH,
26
+ ]
27
+
28
+
29
+ class SeldonClassifier:
30
+ """A Seldon classifier: ``fit`` keeps the training table as context, ``predict_proba`` reads the test rows against it.
31
+
32
+ sdm infers each column's type (categorical columns are embedded as such,
33
+ NaNs kept for the model's imputation and missingness term), preprocesses
34
+ each ensemble member with ``recipe``, and averages the members' logits
35
+ before the softmax.
36
+
37
+ Parameters
38
+ ----------
39
+ model : Seldon
40
+ A classifier, e.g. ``Seldon.from_pretrained(path)``.
41
+ n_estimators : int, default=8
42
+ Ensemble members.
43
+ device : str or torch.device, optional
44
+ Device of the model and the tables; CUDA when available by default.
45
+ max_context_size : int, default=200_000
46
+ Training rows one member reads in context; a larger table gives each
47
+ member its own random subset.
48
+ amp : bool, default=True
49
+ float16 autocast on CUDA; False runs in float32.
50
+ cudnn_attention : bool, default=False
51
+ Let PyTorch's attention use cuDNN's kernel. It is faster once a table
52
+ shape has run, but builds a plan per new shape, seconds each, which a
53
+ table fitted once pays in full; by default the FlashAttention and
54
+ memory-efficient kernels run instead.
55
+ random_state : int, default=0
56
+ Seed of the context subsets and of sdm's per-member preprocessing.
57
+ recipe : SeldonRecipe, optional
58
+ The preprocessing; ``SeldonRecipe()`` by default.
59
+ query_chunk_size : int, default=8192
60
+ Test rows run through the model together, per member.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ model: Seldon,
66
+ *,
67
+ n_estimators: int = 8,
68
+ device: str | torch.device | None = None,
69
+ max_context_size: int = 200_000,
70
+ amp: bool = True,
71
+ random_state: int = 0,
72
+ cudnn_attention: bool = False,
73
+ recipe: SeldonRecipe | None = None,
74
+ query_chunk_size: int = 8192,
75
+ ) -> None:
76
+ if device is None:
77
+ device = "cuda" if torch.cuda.is_available() else "cpu"
78
+ self.device = torch.device(device)
79
+ self.n_estimators = n_estimators
80
+ self.max_context_size = max_context_size
81
+ self.amp = amp
82
+ self.cudnn_attention = cudnn_attention
83
+ self.random_state = random_state
84
+ self.estimator = SeldonICL(model, recipe, query_chunk_size).to(self.device)
85
+
86
+ @classmethod
87
+ def from_pretrained(
88
+ cls,
89
+ name_or_path: str | os.PathLike,
90
+ *,
91
+ subfolder: str | None = None,
92
+ revision: str | None = None,
93
+ token: str | None = None,
94
+ **kwargs: Any,
95
+ ) -> SeldonClassifier:
96
+ """The classifier of a saved model: a local directory, a checkpoint name such as ``"seldon-small-classifier"``, or a Hub repository (see ``Seldon.from_pretrained``); ``kwargs`` are the constructor's."""
97
+ model = Seldon.from_pretrained(
98
+ name_or_path, subfolder=subfolder, revision=revision, token=token
99
+ )
100
+ return cls(model, **kwargs)
101
+
102
+ def fit(self, X: pd.DataFrame | np.ndarray, y: pd.Series | np.ndarray) -> SeldonClassifier:
103
+ """Keep ``X`` and ``y`` as the context of every member."""
104
+ X = _frame(X)
105
+ self.classes_, codes = np.unique(np.asarray(y), return_inverse=True)
106
+ self._stypes = sdm.infer_stypes(X, _low_cardinality="infer")
107
+ x_context = sdm.TableTensor.from_pandas(df=X, stypes=self._stypes)
108
+ y_context = sdm.TableTensor.from_pandas(
109
+ df=pd.DataFrame({_TARGET: codes}), stypes={_TARGET: "categorical"}
110
+ )
111
+ num_estimators: int | None = self.n_estimators
112
+ self._subsampled = len(X) > self.max_context_size
113
+ if self._subsampled:
114
+ generator = torch.Generator().manual_seed(self.random_state)
115
+ repeats = -(-self.n_estimators * self.max_context_size // len(X))
116
+ permutations = [torch.randperm(len(X), generator=generator) for _ in range(repeats)]
117
+ shape = (self.n_estimators, self.max_context_size)
118
+ index = torch.cat(permutations)[: shape[0] * shape[1]]
119
+ x_context = x_context[index].unflatten(0, shape)
120
+ y_context = y_context[index].unflatten(0, shape)
121
+ num_estimators = None
122
+ with self._inference():
123
+ self.estimator.fit(
124
+ x=x_context.to(self.device),
125
+ y=y_context.to(self.device),
126
+ num_estimators=num_estimators,
127
+ generator=torch.Generator(self.device).manual_seed(self.random_state),
128
+ )
129
+ return self
130
+
131
+ def predict_proba(self, X: pd.DataFrame | np.ndarray) -> np.ndarray:
132
+ """Class probabilities of the rows of ``X``, ``(n_rows, n_classes)`` in the order of ``classes_``."""
133
+ X = _frame(X)
134
+ query = sdm.TableTensor.from_pandas(df=X, stypes=self._stypes, device=self.device)
135
+ if self._subsampled:
136
+ query = query.expand(self.n_estimators, *query.size())
137
+ with self._inference():
138
+ out = self.estimator.predict(query)
139
+ codes = [int(code) for code in out.columns[sdm.Stype.numerical]]
140
+ proba = np.zeros((len(X), len(self.classes_)), dtype=np.float32)
141
+ proba[:, codes] = out.numerical.float().cpu().numpy()
142
+ return proba
143
+
144
+ def predict(self, X: pd.DataFrame | np.ndarray) -> np.ndarray:
145
+ """The most probable class of each row of ``X``."""
146
+ return self.classes_[self.predict_proba(X).argmax(axis=1)]
147
+
148
+ @contextmanager
149
+ def _inference(self) -> Iterator[None]:
150
+ amp = self.amp and self.device.type == "cuda"
151
+ attention = nullcontext() if self.cudnn_attention else sdpa_kernel(_NON_CUDNN_ATTENTION)
152
+ with (
153
+ torch.inference_mode(),
154
+ torch.autocast(self.device.type, dtype=torch.float16, enabled=amp),
155
+ attention,
156
+ ):
157
+ yield
158
+
159
+
160
+ def _frame(X: pd.DataFrame | np.ndarray) -> pd.DataFrame:
161
+ """``X`` as a DataFrame with string column names, which sdm requires."""
162
+ if isinstance(X, pd.DataFrame):
163
+ return X.rename(columns=str)
164
+ X = np.asarray(X)
165
+ return pd.DataFrame(X, columns=[f"x{i}" for i in range(X.shape[1])])