Repository navigation
Conversation
SDMClient.in_process() answers the Universal TFM contract with structured-data-models in the caller's own Python process. The client's adapters send what they would send a NIM and read back what a NIM answers, so predictions, sessions, rankings and forecasts work unchanged: only the transport differs, as it already does for Databricks Model Serving. The request is read the way the SDM NIM reads it. Wire tables are decoded by their declared dtypes, each relational neighbourhood is scoped by its instance_id, and the context is fitted on a copy of the model with the NIM's seed, one estimator and, on a GPU, its autocast precision. Against the current NIM code served on the CPU with the same published weights, every prediction, probability and quantile matched exactly, on synthetic graphs and on Adult, Wine, Titanic, Bank, Students and Northwind, and every refusal carried the same status and code. qa/probe_in_process.py repeats that comparison. A model passed in is only used: each fit is on a shallow copy that shares its weights, so a context the caller fitted on it survives. With none, each model is loaded from its published weights on first use. structured-data-models is public, so the hygiene rule that treated its name as an internal build now covers only the one that still is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Manush Murali <manushm@nvidia.com>
PyArrow comes with the extra, not with the client, and the table decoder imported it at module scope. Opening an in-process client on a base install therefore raised ModuleNotFoundError from inside the package, before the check that names the extra. It is imported where it is used now. The test that covered this blocked only `sdm`, in a process that had already imported the package, so it passed either way. It runs in a fresh interpreter with everything the extra brings blocked, and fails on the old code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Manush Murali <manushm@nvidia.com>
A request whose table the model cannot represent, such as a single-table context holding only its key and target, raised inside the model and came back as a 500 INTERNAL_ERROR. The NIM refuses the same request with 422 VALIDATION_FAILED, "table cannot be represented by SDM", and labels the model cannot hold with 422 TASK_SCHEMA_MISMATCH. Both are answered that way here now, for every table rather than for the one shape that surfaced it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Manush Murali <manushm@nvidia.com>
uv.lock is what the open source scan inventories, and it resolves every extra, so structured-data-models and what it brings belong in it. It could not be relocked: the workspace admits Python 3.10 and structured-data-models needs 3.11. The extra now carries that marker, so on 3.10 it installs nothing, and in_process() says it needs Python 3.11 rather than asking for an extra that was already installed. The lock gains structured-data-models, PyTorch and PyTorch's CUDA wheels for Linux, and nothing else in it moves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Manush Murali <manushm@nvidia.com>
A relational request is gathered by the relational engine whichever way it is answered, so asking KumoRelational in process needs `[relational]` as well as `[in-process]`. The quickstart and the installation guide said only the second. A fractional numeric key is refused, as the NIM refuses it, because keys are joined as integers. The message said such a key joins to no row, which is not why; it now reads as the NIM's does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Manush Murali <manushm@nvidia.com>
A NIM reports a request it refuses while creating a session as SESSION_CREATE_VALIDATION_FAILED, and one refused while scoring against a session as SESSION_PREDICT_SCHEMA_MISMATCH, keeping the message, for every validation failure rather than one. In process they kept their original codes, so the same session refusal arrived under a different code depending on where the model ran. The relational NIM also refuses a context with no rows, which a holdout of every entity produces after the client's own check has passed. In process that session was fitted on nothing and answered. It is refused now, as the NIM refuses it, in one call and in a session. A single-table context left empty the same way still answers its session's first prediction with a 500, as the NIM does. A model loaded for one task alone, which a NIM never is, is refused for the other with UNSUPPORTED_TASK before anything is fitted, rather than failing inside the model as an internal error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Manush Murali <manushm@nvidia.com>
The client counts a context's classes before it is pinned, and a holdout then takes rows out of it. A binary context left with one class, or a multiclass one that lost a class it declares, was fitted and answered in process, while the NIM refuses both. The NIM's three class checks now run on the context as it arrives: exactly two classes for a binary task, at least two for a multiclass one, and declared classes equal to those present. A run_mode the client does not know is not refused: the NIM accepts any short string there, and answers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Manush Murali <manushm@nvidia.com>
The installed-wheel, sbom and build jobs installed the client set with `--find-links dist` and `==$VERSION`. Once 1.0.0 was on PyPI, pip resolved the published release as readily as the wheel built here, and the installed-wheel job began testing it: a pull request adding to the client failed there because the release lacked the addition. Each distribution is now named by its wheel file, and the installed-wheel job checks that every one was installed from dist/ before running the suite. The release workflow keeps `--find-links`: the version it builds is not on PyPI yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Manush Murali <manushm@nvidia.com>
…es them A NIM validates the inference block against a typed model before any model sees it. A value of the wrong kind, such as a run_mode that is not a string or an inference_config that is not an object, is 422 VALIDATION_FAILED there; in process it was ignored and answered. A boolean is read as the boolean it spells, so use_prediction_time 'no' asks for nothing on the NIM and was read here as asking for prediction time. The options are now typed and coerced the same way first. Each model then applies its own NIM's checks in its NIM's order: the tabular one refuses use_prediction_time with its own 400 and message, which in process had borrowed the relational one's 422. Every combination of value, model and route measured against the NIM's code now returns the same status, code and message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Manush Murali <manushm@nvidia.com>
The limits this client checks before sending hold in process as they do against a NIM. A NIM also sets limits of its own, per deployment, to protect a shared server: the longest string a cell may hold, the most distinct values a column may have, the largest body it accepts. Those are not applied when the model runs in the caller's own process, and the quickstart and the limits reference now say so rather than leaving it to be discovered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Manush Murali <manushm@nvidia.com>
A NIM validates every request against the contract before anything reads it: each name and label it carries is at most 1,024 characters, and a table names each column once. In process neither was checked, so a request the NIM refuses was answered. The one a caller reaches without trying is a predictive query longer than that limit, whose text travels as metadata.source_query; the NIM refuses it with 422 VALIDATION_FAILED. Both rules are checked first now, and the refusal names each field the way the NIM names it. The quickstart also says where the answers differ by design: on a CPU the model computes at full precision, while a GPU-hosted NIM computes at reduced precision. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Manush Murali <manushm@nvidia.com>
Before anything else, the NIM requires each table to supply exactly the columns its schema declares, the rows to score excepted only for the target. In process that was checked piecemeal and later, so a missing or extra column came back under a different code and message, and a related table the context never carried was refused for the wrong reason. The column check now runs first, as the NIM's does, and a split is then held to the tables the relationships name and to those the context was fitted on. The checks it makes redundant are removed rather than left unreachable. Three smaller differences, each measured against the NIM: - A tabular request carrying relationships or related tables is refused naming whichever the NIM names first, including one on the predict side only, which was let through. - Quantile levels are judged only when quantiles are asked for, and are refused in the NIM's words. - On Python 3.10, in_process() raises a StructuredDataError with the code MISSING_EXTRA rather than MissingExtraError; its docstring says so, and the prerequisites no longer say the client never uses a GPU. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Manush Murali <manushm@nvidia.com>
An unsupported output or task was refused with the NIM's code and a message that differed by its closing period, and an unknown session with a different sentence. Each now reads exactly as the NIM's does. The module docstring says that a payload written by hand, rather than built by the client, is not held to every rule a NIM applies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Manush Murali <manushm@nvidia.com>
On an L4, in process gave the NIM's answers on the same GPU exactly, in one call and in a session. A CPU run against a GPU-hosted NIM differs by more than rounding, because the two compute at different precision and draw different random numbers: a class probability moved by 0.16. The quickstart said "the last digits", and the parity probe said a GPU NIM always needs a tolerance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Manush Murali <manushm@nvidia.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The client could only reach a model over HTTP: a NIM, or a Databricks serving endpoint. Someone who already has structured-data-models loaded in a notebook had to call its tensor API directly, and then use a different API again once the model was hosted.
SDMClient.in_process()runs the model in the caller's own Python process behind the same calls.With no model passed, each one is loaded from its published weights on Hugging Face the first time it is asked for, on CUDA when available, else the CPU.
How it works
It is a third transport beside the HTTP one and the Databricks one. The adapters send exactly the payload they would send a NIM and read back what a NIM answers, so predictions,
fit()sessions, rankings and forecasts work unchanged: nothing above the transport knows where the model runs.The transport reads that payload the way the SDM NIM does:
instance_id, the way the model's own sampler scopes each example.NimRequestError, with the NIM's status and code.Verified against the NIM itself
I served the current SDM NIM code locally on the CPU, with the same published weights and structured-data-models 0.2.1, and sent each request both ways. Every prediction, probability and quantile was identical (largest difference 0.0):
kumo-tabularkumo-relationaluse_prediction_time, custom quantile levels, mean statistic, float foreign keys with nulls, int64 keys past 2**53, string keysRANK TOP 3,FORECAST 3 TIMEFRAMES, sessions scored twicekumo-tabular); Northwind counts, sums, binary and integer regression (kumo-relational), one-shot and in a sessionEvery refusal matched on status, code and message:
scores,embeddings,explanation, orprobabilitieson a regression422 UNSUPPORTED_OUTPUTLIST_DISTINCT,ranking422 UNSUPPORTED_TASKinference_config, unknown inference option,use_prediction_timeonkumo-tabular400 INVALID_REQUESTrun_modethat is not a string422 VALIDATION_FAILED422 VALIDATION_FAILED422 UNSUPPORTED_DTYPE422 VALIDATION_FAILED422 VALIDATION_FAILED422 TASK_SCHEMA_MISMATCH422 SESSION_CREATE_VALIDATION_FAILED,422 SESSION_PREDICT_SCHEMA_MISMATCH404 SESSION_NOT_FOUND422 VALIDATION_FAILEDInference options are typed and coerced as the NIM's request model does them, so
use_prediction_time: 'no'asks for nothing on both. A sweep of 32 values across the four inference fields, on both models, in one call and in a session, matched the NIM on status, code and message.And on a GPU. On an NVIDIA L4, I served the same NIM code with its GPU profile (CUDA, bf16 autocast) and ran in-process on the same GPU, with no model passed and so the default device. In-process loaded onto
cuda:0under bf16 autocast, gave identical answers when asked twice, and matched the NIM with a largest difference of 0.0 across all 37 compared answers: tabular and relational, sessions, and every real dataset above. Every refusal probe also matched on status, code and message.qa/probe_in_process.pyrepeats the real-data comparison against any NIM. When this process runs on the same device as the NIM, CPU or GPU, it needs no tolerance. With this process on a CPU and the NIM on a GPU, setSDM_IN_PROCESS_TOLERANCE.Tests
test_a_model_runs_in_this_process.pyuses small random-weight models, so it needs no network and no GPU:503 MODEL_NOT_READY, with the original error as its cause.UNSUPPORTED_TASKbefore fitting.in_process()raisesMissingExtraErrornamingin-process. This is checked in a fresh interpreter with structured-data-models, PyTorch and PyArrow all absent, and also by hand on a clean install of the built wheel.Two deliberate breaks were each caught: changing the seed, and fitting on the caller's model instead of a copy. CI runs the module in the engine job, which now installs CPU PyTorch and the
in-processextra. The other jobs skip it.Locally: 2707 passed across the four pure-Python packages and the root suite, and 2568 passed in the engine suite. ruff, mypy over all five packages, hygiene and the notices check are clean. I also installed the built wheel with
[in-process]into a clean venv and ran the quickstart from outside the checkout, using the publishedkumo-tabularweights.Worth a look
structured-data-models-client1.0.0 reached PyPI while this was in review. After that,--find-links distwith==1.0.0resolved the published release instead of the wheel CI had just built, soinstalled-wheelstarted testing the release and failed on the new method.installed-wheel,sbomand thebuilddry-run now name each wheel file, andinstalled-wheelasserts that all four distributions came fromdist/. This affects every pull request against the client from now on, not only this one.structured-data-modelsas an internal build a reader cannot look up. That package is now public on GitHub and PyPI, and it is what the new extra installs, so the rule now covers only the internal build name it was written for.materialize()still says those columns are dropped. It is now wrong against both a current NIM and this transport, and is left for a follow-up.python_version >= '3.11'marker, because without one the workspace lock cannot resolve. As a result, on 3.10 the extra installs nothing, andin_process()says it needs Python 3.11 rather than asking for an extra that was already installed. This was checked on a real 3.10 interpreter.uv.lockgrows. It is what the open source scan inventories, and it resolves every extra, so it gains structured-data-models, PyTorch, and PyTorch's CUDA wheels for Linux. Nothing already in it moves.TFM_MAX_*, such as string-cell bytes, distinct values per column and body size. They protect a shared server, and two NIMs may set them differently. The limits this client checks before sending still apply.docs/reference/limits.mdsays so.kind, can be answered differently, for example with a 500 where a NIM answers 422.Follow-ups, not in this change:
metadata.source_query, and the contract caps that field at 1,024 characters, so the NIM answers422 VALIDATION_FAILED. In-process now refuses it the same way. The fix belongs in what the client sends, for example by not sending the text past the cap.instance_table. The client lets a user name a tableinstance_table, which is the wire's own name for the scored rows. The NIM refuses such a request with422 INVALID_SCHEMA, and so does in-process, so it should be refused when the request is gathered.CPU against GPU. On a CPU, in-process computes at full precision and draws its random numbers on the CPU. A GPU-hosted NIM does neither, so the two can differ by more than rounding: on the L4, a class probability moved by 0.16 and a regression by 0.41. The quickstart said "the last digits", which understated it, and now gives the measured figure. On the same device, the two are identical.
🤖 Generated with Claude Code