Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -274,15 +280,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 @@ -319,8 +337,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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ versioned by [semantic versioning](https://semver.org) as
[`docs/reference/stability.md`](docs/reference/stability.md) defines it for
these packages. All five distributions share one version and one entry.

## Unreleased

### 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 context
is fitted as the NIM fits it, and a request the NIM refuses is refused with
the same `NimRequestError`. Install it with the `in-process` extra.

## 1.0.0 - 2026-10-15

First public release. The five distributions are versioned and published
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,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 @@ -68,6 +70,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
1 change: 1 addition & 0 deletions docs/about/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,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
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 @@ -7,8 +7,11 @@
reported as `No matching 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
39 changes: 39 additions & 0 deletions docs/get-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,45 @@ 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 context is fitted with the NIM's seed and
settings, `fit()` holds its session in memory, and a request the NIM refuses
is refused with the same `NimRequestError`. On a GPU it computes as the NIM
does, and on the same GPU it gives the NIM's answers exactly. 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).

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
6 changes: 6 additions & 0 deletions 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
6 changes: 6 additions & 0 deletions packages/structured-data-models-client/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,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
46 changes: 44 additions & 2 deletions packages/structured-data-models-client/src/sdm_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,12 +369,54 @@ def for_databricks_serving(
)
return client

@classmethod
def in_process(cls, *models: Any, device: Any = None) -> SDMClient:
r"""Open a client that runs the models in this Python process.

The same calls, answered by structured-data-models here instead of by
a NIM. Pass models already loaded with it, or none to load each one
from its published weights the first time it is asked for:

>>> import sdm
>>> relational = sdm.models.KumoRelational()
>>> with SDMClient.in_process(relational) as client:
... result = client.predict(request, model=KumoRelational())

Each model is fitted as the NIM fits it, at the same seed and, on a
GPU, the same precision. A refusal is the
:class:`~sdm_client.errors.NimRequestError` a NIM answers with.

Args:
*models: ``sdm.models.KumoTabular`` or ``sdm.models.KumoRelational``
models, at most one of each. A model passed is only used:
each context is fitted on a copy that shares its weights.
device: Where to load the models this client loads itself, such as
``'cuda'``. Defaults to CUDA when it is available, else the
CPU. Refused with ``models``, which stay where they are.

Raises:
MissingExtraError: If structured-data-models is not installed;
install ``structured-data-models-client[in-process]``. On
Python 3.10, which it does not support, the error is a
:class:`StructuredDataError` with the same code
``MISSING_EXTRA`` that names the Python it needs.
StructuredDataError: With code ``INVALID_CONFIGURATION``, for a
model other than those two, two of one kind, or ``device``
given with ``models``.
"""
from sdm_client.in_process import InProcessTransport

client = cls.__new__(cls)
client._start(InProcessTransport(models, device=device))
return client

@property
def url(self) -> str:
r"""Where this client sends requests.

A NIM's base URL, or ``'databricks-serving:<name>'`` for a client
opened with :meth:`for_databricks_serving`.
A NIM's base URL, ``'databricks-serving:<name>'`` for a client opened
with :meth:`for_databricks_serving`, or ``'in-process'`` for one
opened with :meth:`in_process`.
"""
return self._transport.url

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES.
# All rights reserved.
# SPDX-License-Identifier: Apache-2.0

r"""The models served in this Python process rather than by a NIM.

Behind :meth:`SDMClient.in_process <sdm_client.SDMClient.in_process>`, and not
public API itself. Needs the ``in-process`` extra, which brings
structured-data-models and PyTorch.
"""

from sdm_client.in_process.transport import InProcessTransport

__all__ = ['InProcessTransport']
Loading
Loading