Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c09e78e
Run the models in this process, through the same calls as a NIM
Manushpm8 Oct 16, 2026
8379145
Name the in-process extra on a client installed without it
Manushpm8 Oct 16, 2026
2497a8d
Refuse a table the model cannot hold as the NIM does
Manushpm8 Oct 16, 2026
51b78f1
Lock the in-process extra, and say which Python it needs
Manushpm8 Oct 16, 2026
3df0536
Say what a relational model needs in process, and why a key was refused
Manushpm8 Oct 16, 2026
5467c4d
File a refusal inside a session as the NIM files it
Manushpm8 Oct 18, 2026
a7606f1
Check the classes of the context that arrives, as the NIM does
Manushpm8 Oct 18, 2026
586882c
Install the wheels CI built, not a release of the same version
Manushpm8 Oct 18, 2026
c26a29f
Read inference options as the NIM types them, then as each model judg…
Manushpm8 Oct 18, 2026
7f283d4
Say which limits hold in process, and which belong to a deployment
Manushpm8 Oct 18, 2026
01fe2ae
Hold a request to the contract's own limits before reading it
Manushpm8 Oct 18, 2026
4d13340
Read a request's tables in the order the NIM reads them
Manushpm8 Oct 18, 2026
b801a2c
Word task, output and session refusals as the NIM words them
Manushpm8 Oct 18, 2026
22583a4
Say what a GPU run of a model in process gives, measured
Manushpm8 Oct 18, 2026
e38b57a
Refuse a hand-written request where the NIM refuses it
Manushpm8 Oct 19, 2026
077e9f6
List every validation problem in one refusal, as the NIM does
Manushpm8 Oct 19, 2026
06da019
Answer a composite key under its values as sent
Manushpm8 Oct 19, 2026
a087506
Merge main into run-models-in-process
Manushpm8 Oct 19, 2026
cbc052d
Answer a request through a backend, not an imitation of a NIM
Manushpm8 Oct 20, 2026
7a2ec95
Cut a logged query to the length the contract allows
Manushpm8 Oct 20, 2026
6024344
Write inference options in their contract types before sending
Manushpm8 Oct 20, 2026
ea10315
Answer a key of several columns under its values in process
Manushpm8 Oct 20, 2026
81ff26f
Test the logged query where it is cut, not only where it is answered
Manushpm8 Oct 20, 2026
a6f35af
Type NumPy inference values, and refuse rows scored without their key
Manushpm8 Oct 20, 2026
c2f086a
Refuse a context without a column it declares, and copy what a sessio…
Manushpm8 Oct 20, 2026
5a0f819
Hold each table to exactly its declared columns, and copy the pinned …
Manushpm8 Oct 20, 2026
8e70c01
Say where a model runs, and pin what the copy tests rely on
Manushpm8 Oct 20, 2026
f274321
Say what fit() and models() do for a model in this process
Manushpm8 Oct 20, 2026
85240c7
Test each column check on the half it guards
Manushpm8 Oct 20, 2026
74108d7
Say which contract rules a model in this process does not re-check
Manushpm8 Oct 20, 2026
302d085
Say what models() lists for a model in this process
Manushpm8 Oct 20, 2026
ea3587c
Merge main into run-models-in-process
Manushpm8 Oct 20, 2026
638f15f
Refuse what cannot be answered before the model runs
Manushpm8 Oct 20, 2026
f8ec557
Let closing win, and stop reading a scalar that holds none
Manushpm8 Oct 20, 2026
b2cc7c7
Read the closed flag and the rows under one lock
Manushpm8 Oct 20, 2026
a760dfe
Say what is wrong with the rows, not that there are none
Manushpm8 Oct 20, 2026
26d0907
Refuse a table without instance_id, and a model with no reader, up front
Manushpm8 Oct 21, 2026
eaf7635
Hold DuckDB below 2 and pandas below 3.1 (#47)
Manushpm8 Oct 22, 2026
9efa467
Refuse an unreadable cell and a ragged row with a NIM's codes
Manushpm8 Oct 22, 2026
f6dd0da
Skip the in-process parity cases where structured-data-models is absent
Manushpm8 Oct 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 30 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,13 @@ jobs:
- run: |
cd packages/structured-data-models-relational-engine
python -m pytest tests -q -m 'not live_nim and not live_warehouse'
# The client's suite again, with an engine present. The test job installs
# none, so every client test that needs one skips there and runs nowhere.
# The client's suite again, with an engine present and the models able to
# run in process. The test job installs neither, so every client test
# that needs one skips there and runs nowhere. PyTorch comes from its CPU
# index, since the default one resolves to a far larger CUDA build.
- run: |
pip install -e './packages/structured-data-models-client[test]'
pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install -e './packages/structured-data-models-client[test,in-process]'
python -m pytest packages/structured-data-models-client/tests -q \
-m 'not live_nim and not live_warehouse'
# And the root suite, which holds the relational wire snapshots. They
Expand Down Expand Up @@ -205,11 +208,14 @@ jobs:
EOF
# Building each one proves nothing about installing them together. A pin
# only one distribution carries makes the set unsatisfiable, and the
# resolver is the only thing that says so.
# resolver is the only thing that says so. Every one is named by file:
# once a version is on PyPI, `--find-links` with `==$VERSION` resolves
# the published release as readily as the wheel built here.
- run: |
VERSION=$(cat dist/VERSION)
pip install --dry-run --find-links "$GITHUB_WORKSPACE/dist" \
"structured-data-models-client[relational,sqlite,duckdb]==$VERSION"
pip install --dry-run \
"$(ls dist/structured_data_models_client-$VERSION-*.whl)[relational,sqlite,duckdb]" \
dist/structured_data_models_{api,pql,connectors,relational_engine}-$VERSION-*.whl
# The wheels only: dist/ also carries VERSION for the jobs downstream,
# and twine refuses anything that is not a distribution.
- run: pip install --upgrade twine && twine check dist/*.whl
Expand Down Expand Up @@ -325,15 +331,27 @@ jobs:
- run: |
pip install --upgrade pip pytest requests-mock
VERSION=$(cat dist/VERSION)
pip install --find-links "$GITHUB_WORKSPACE/dist" \
"structured-data-models-client[sqlite,duckdb]==$VERSION"
pip install \
"$(ls dist/structured_data_models_client-$VERSION-*.whl)[sqlite,duckdb]" \
dist/structured_data_models_{api,pql,connectors}-$VERSION-*.whl
- name: The installed distribution is what gets imported, and it is typed
working-directory: /tmp
run: |
python - <<'EOF'
import pathlib
import json, pathlib
from importlib.metadata import distribution
import predictive_query, sdm_api, sdm_client, sdm_connectors

# The same version on PyPI resolves as readily as the wheel built
# here, and then this job tests the published release instead.
for name in ('client', 'api', 'pql', 'connectors'):
recorded = distribution(f'structured-data-models-{name}').read_text('direct_url.json')
source = json.loads(recorded or '{}').get('url', 'an index')
assert '/dist/' in source, (
f'structured-data-models-{name} was installed from {source}, '
f'not from the wheel built here'
)

for module in (sdm_api, sdm_client, sdm_connectors, predictive_query):
location = pathlib.Path(module.__file__).resolve()
assert 'site-packages' in location.parts, (
Expand Down Expand Up @@ -370,8 +388,9 @@ jobs:
- run: |
pip install --upgrade pip cyclonedx-bom
VERSION=$(cat dist/VERSION)
pip install --find-links "$GITHUB_WORKSPACE/dist" \
"structured-data-models-client[sqlite,duckdb,postgres,databricks,snowflake,s3]==$VERSION"
pip install \
"$(ls dist/structured_data_models_client-$VERSION-*.whl)[sqlite,duckdb,postgres,databricks,snowflake,s3]" \
dist/structured_data_models_{api,pql,connectors}-$VERSION-*.whl
cyclonedx-py environment --of JSON --validate -o dist/sbom.cyclonedx.json
python -c "import json,sys; d=json.load(open('dist/sbom.cyclonedx.json')); names={c['name'] for c in d.get('components', [])}; missing={'structured-data-models-client','structured-data-models-api','structured-data-models-pql','structured-data-models-connectors'} - names; print(f'{len(names)} components'); sys.exit(f'SBOM missing {sorted(missing)}' if missing else 0)"
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
Expand Down
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ these packages. All five distributions share one version and one entry.

### Added

- `SDMClient.in_process()` runs `kumo-tabular` and `kumo-relational` in this
Python process with structured-data-models, through the same calls as a NIM:
predictions, fitted sessions, rankings and forecasts. It loads each model
from its published weights on first use, or uses one passed in. The request
is prepared as it is for a NIM and the model run on it directly, the context
fitted as the NIM fits it. A request the model cannot answer is refused with
a `StructuredDataError` whose code says why. Install it with the
`in-process` extra.
- The relational engine publishes wheels for macOS (Apple silicon and Intel)
and 64-bit Windows, and for CPython 3.14, alongside the `manylinux_2_28`
x86-64 and aarch64 wheels it already built, so `structured-data-models-client[relational]`
Expand All @@ -22,6 +30,26 @@ these packages. All five distributions share one version and one entry.
emulated, and each is smoke-tested on the architecture it targets. The
release no longer builds aarch64 under QEMU, which also takes a privileged
container image out of the release path.
- A request's inference options are written in the types the contract
gives them before it is sent: `use_prediction_time` spelled `'no'` is sent
as `false`, a NumPy boolean or integer is sent as the value it holds, and a
value that cannot be read as its type, such as a `run_mode` that is not
text, is refused with `INVALID_REQUEST` before sending, where a NIM
answered `422 VALIDATION_FAILED`.
- The `duckdb` extra holds `duckdb` below 2. The 2.0 release warns on the
Arrow fetch the relational engine reads DuckDB tables through, and this
release is tested against 1.5.
- `structured-data-models-client` and the relational engine hold `pandas`
below 3.1. The 3.1 release raises taking a date offset from the earliest
timestamp, which the engine does for every graph without a time column,
and this release is tested against 3.0.

### Fixed

- A predictive query longer than 1,024 characters is answered. A relational
request carries the query in `metadata.source_query` for logging, which the
contract caps at 1,024 characters, so a NIM refused a longer one with
`VALIDATION_FAILED`. The logged copy is now cut to fit.

## 1.0.0 - 2026-10-15

Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ manage.
This client talks to those models. It builds the request, sends it to a NIM
inference endpoint, and gives you back a `PredictionResult`, which
`to_pandas()` turns into a DataFrame. Inference runs in the NIM, so this
package downloads no weights and needs no GPU.
package downloads no weights and needs no GPU. To run the model in your own
Python process instead, with the same calls, open the client with
`SDMClient.in_process()`.

Two models are available through the client:

Expand Down Expand Up @@ -84,6 +86,7 @@ The [quickstart](docs/get-started/quickstart.md) runs this end to end, and
| `pip install 'structured-data-models-client[relational]'` | Adds the relational model and its native engine |
| `pip install 'structured-data-models-client[snowflake]'` | Reads tables from a warehouse. Also `[sqlite]`, `[duckdb]`, `[postgres]`, `[databricks]`, `[lakebase]`, `[s3]` |
| `pip install 'structured-data-models-client[all]'` | The relational model and every warehouse backend |
| `pip install 'structured-data-models-client[in-process]'` | Runs the models in this Python process, through `SDMClient.in_process()` |

[Installation](docs/get-started/installation.md) has the rest, including what
differs on Databricks.
Expand Down
3 changes: 2 additions & 1 deletion docs/about/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ pip install structured-data-models-client # tabular, PQL, tran
pip install 'structured-data-models-client[relational]' # adds the native relational engine
pip install 'structured-data-models-client[snowflake]' # one backend (also duckdb/sqlite/postgres/databricks/s3)
pip install 'structured-data-models-client[all]' # relational + every backend
pip install 'structured-data-models-client[in-process]' # runs the models in this process
```

`[explain]` and `[relbench]` are the engine's own extras, not the client's, so
Expand Down Expand Up @@ -673,7 +674,7 @@ serve.

| Call | Answer |
|---|---|
| `client.models()` | Read from the endpoint's `/v1/models`. Endpoint truth. |
| `client.models()` | Read from the endpoint's `/v1/models`. Endpoint truth. In process, the models the client can run. |
| `client.capabilities(model)` | Local: what this client can build and send. Tagged `source="sdk"`. |

`/v1/models` is a NIM runtime route and is **not** one of the contract's four
Expand Down
11 changes: 7 additions & 4 deletions docs/about/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ labelled rows and the rows you want scored, and it answers in one pass. There
is no training loop, no hyperparameter search, and no per-dataset artifact.

This client builds the request, sends it to a NIM, and gives you back a
`PredictionResult`, which `to_pandas()` turns into a DataFrame. Inference runs in the NIM, so the client downloads no weights and
needs no GPU.
`PredictionResult`, which `to_pandas()` turns into a DataFrame. Inference runs
in the NIM, so the client downloads no weights and needs no GPU. A client
opened with `SDMClient.in_process()` runs the model in your own process
instead, with the same calls; that one loads the weights, and uses a GPU when
there is one.

## The one idea

Expand Down Expand Up @@ -35,5 +38,5 @@ declared as data instead of discovered from a 422.
- **You want to train a model.** These models do not train. If you need a model
fitted to your data, this is the wrong tool.
- **Your data cannot leave your environment.** Run a NIM in your environment
and point the client at it.
- **You want inference in-process.** Everything here is an HTTP call.
and point the client at it, or run the model in your own process with
`SDMClient.in_process()`.
1 change: 1 addition & 0 deletions docs/get-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
| `pip install 'structured-data-models-client[snowflake]'` | Reads tables from a warehouse. Also `[sqlite]`, `[duckdb]`, `[postgres]`, `[databricks]`, `[lakebase]`, `[s3]`. |
| `pip install 'structured-data-models-client[all]'` | The relational model and every warehouse backend. |
| `pip install 'structured-data-models-client[databricks-serving]'` | Reaches a model served on Databricks Model Serving, through `SDMClient.for_databricks_serving`. Not in `[all]`: it is a way to reach a model, not a backend. |
| `pip install 'structured-data-models-client[in-process]'` | Runs the models in this Python process with structured-data-models, through `SDMClient.in_process`. Brings PyTorch. The relational model also needs `[relational]`, which gathers its requests. **Python 3.11 or later**: on 3.10 the extra installs nothing, and `in_process()` says why. Not in `[all]`, for the same reason as `[databricks-serving]`. |

**What you install is not what you import.** The five distributions are
`structured-data-models-client`, `-api`, `-pql`, `-connectors` and
Expand Down
7 changes: 5 additions & 2 deletions docs/get-started/prerequisites.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
distribution found for structured-data-models-relational-engine`, which
names the package and not the reason.
- **A reachable NIM** serving `kumo-tabular` or `kumo-relational`. The client
runs no model and downloads no weights.
- **No GPU** on the client. The NIM owns that.
runs no model and downloads no weights. The one exception is a client
opened with `SDMClient.in_process()`, which runs the model itself; see the
[quickstart](quickstart.md#a-model-in-this-process).
- **No GPU** on the client. The NIM owns that. A client opened with
`SDMClient.in_process()` uses one when there is one, and the CPU otherwise.

## Platforms

Expand Down
55 changes: 55 additions & 0 deletions docs/get-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,61 @@ This needs `pip install 'structured-data-models-client[databricks-serving]'`. A
answers one request at a time, so `fit()` is refused there; `predict()` sends
the context with every call.

## A model in this process

The model can also run in your own Python process, with no NIM at all. The
calls are the same; only how the client is opened differs:

```python
from sdm_client import KumoRelational, SDMClient

with SDMClient.in_process() as client:
result = client.predict(request, model=KumoRelational())
```

Each model is loaded from its published weights on Hugging Face the first time
it is asked for, on a GPU when there is one. A model you already loaded with
structured-data-models is used as it is:

```python
import sdm

relational = sdm.models.KumoRelational(device='cuda')
with SDMClient.in_process(relational) as client:
result = client.predict(request, model=KumoRelational())
```

It answers as the NIM does. The request is prepared exactly as it is for a NIM,
and the model is run on it directly, with no HTTP in between. The context is
fitted with the NIM's seed and settings, and `fit()` holds its session in
memory. On a GPU it computes as the NIM does, and with the published weights on
the same GPU it gives the NIM's answers exactly.

A request the model cannot answer is refused with a `StructuredDataError` whose
`code` says why, from the codes a NIM uses, such as `UNSUPPORTED_OUTPUT`. It is
not a `NimRequestError` and has no HTTP status, since nothing was sent. Inside
a session the code is the reason's own, where a NIM files the refusal under
`SESSION_CREATE_VALIDATION_FAILED` or `SESSION_PREDICT_SCHEMA_MISMATCH`. A
request is checked for what the model needs to read it, not against every rule
of the request contract: a name longer than the contract's 1,024 characters, or
a key the contract does not name in a request built by hand and passed to
`Request.materialized`, is answered here where a NIM refuses it with
`VALIDATION_FAILED`.

Two things differ by design. On a CPU it computes at full precision and draws
its random numbers there, so its answers can differ from a GPU-hosted NIM's by
more than rounding: in one comparison a class probability moved by 0.16. And
the limits a deployment sets to protect a shared server are not applied, since
none is shared here; see [limits](../reference/limits.md). That includes a
session's expiry: what `fit()` returns holds its fitted context, in GPU memory
when the model is on a GPU, until it or the client is closed, so open it in a
`with` block, as above, or call its `close()`.

This needs `pip install 'structured-data-models-client[in-process]'`, which
brings PyTorch, on Python 3.11 or later. A relational request is still
gathered by the relational engine, so for `KumoRelational` install both:
`pip install 'structured-data-models-client[relational,in-process]'`.

## Which rows get scored

A query says *what* to predict. It never says *which rows*.
Expand Down
9 changes: 8 additions & 1 deletion docs/reference/limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ Both endpoints enforce sizes, and most of these are the relational one's.
| hops | relational | 6 | at `materialize()` |
| tables | relational | 15 | against the graph the request carries, not the schema it was built from |

A client opened with `SDMClient.in_process()` is held to this table as well,
since this client checks it. A NIM also has limits of its own, which each
deployment sets, such as the longest string a cell may hold or the most
distinct values a column may have. Those protect a shared server, and are
not applied when the model runs in your own process.

A relational request for more entities than one call scores is not refused.
`materialize()` gathers it in batches of at most that many, each with a
context of its own, sampled around that batch's rows rather than copied from
Expand Down Expand Up @@ -181,7 +187,8 @@ not read from the endpoint.
`models()` *is* read from the endpoint, so it is the endpoint's own answer. The
route it uses is supplied by the NIM runtime rather than by the Universal TFM
contract, so an endpoint that does not serve it gives a clear error rather than
a wrong answer.
a wrong answer. A client opened with `SDMClient.in_process()` has no endpoint,
and lists the models it can run.

There is more than one build of `kumo-relational`. The client declares the
broader build with six task kinds and seven output fields. A narrower build
Expand Down
11 changes: 10 additions & 1 deletion packages/structured-data-models-client/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ classifiers = [
dependencies = [
"structured-data-models-api==1.0.0",
"structured-data-models-pql==1.0.0",
"pandas>=2.2.2",
# 3.1 builds a frame around an integer past the float range where earlier
# releases refused one, and a request's description relies on that
# refusal to leave out the table carrying it.
"pandas>=2.2.2,<3.1",
"numpy>=1.23",
"requests>=2.28",
"urllib3>=1.26",
Expand Down Expand Up @@ -78,6 +81,12 @@ s3 = ["structured-data-models-connectors[s3]==1.0.0"]
# model rather than a backend to read, so `all` does not carry it; ask for it by
# name.
databricks-serving = ["databricks-sdk>=0.68.0,<1.0"]
# Running the models in this process rather than reaching a NIM. Like
# `databricks-serving` it is a way to reach a model, and it brings PyTorch, so
# `all` does not carry it. structured-data-models needs Python 3.11; on 3.10
# the extra installs nothing, and `SDMClient.in_process()` says why. The marker
# is also what lets the workspace lock resolve.
in-process = ["structured-data-models>=0.2.1,<0.3; python_version >= '3.11'"]
all = [
"structured-data-models-client[relational,sqlite,duckdb,postgres,snowflake,databricks,lakebase,s3]",
]
Expand Down
Loading
Loading