Skip to content

fix(regressor): make output_type="full" match a local prediction - #369

Merged
ggprior merged 1 commit into
mainfrom
georg/full_output_chunking
Aug 31, 2026
Merged

fix(regressor): make output_type="full" match a local prediction#369
ggprior merged 1 commit into
mainfrom
georg/full_output_chunking

Conversation

@ggprior

@ggprior ggprior commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

AI;DR: TabPFN output_type=full logits are -inf if bucket probability is zero. Over the wire (JSON) this is transmitted as null. This breaks several extensions including unsupervised and synthetic (whenever logits are turned back into probas via softmax op)

Claude generated description

Two things made TabPFNRegressor.predict(output_type="full") through the client
differ from the same call against the local tabpfn package.

Masked bars came back as NaN

Bars a row's distribution puts no mass on carry a logit of -inf locally. The
API serializes non-finite floats as null (the pydantic default, which keeps
the payload spec-conforming), and the client turned null into NaN. Unlike
-inf, a single NaN propagates: exp(NaN) makes the normalising sum NaN,
so one bar takes out all 5000 and the row's distribution is gone. Anything that
samples from the returned criterion then yields NaN.

On breast-cancer with three features, 529 of 569 rows (93%) had at least one
such bar, so almost every sampled value was NaN.

Restoring null -> -inf on unpack reproduces the API's own mean field to
float32 precision, which is what pins the interpretation:

row 0 row 1 row 2
recomputed from restored logits 21.13436167 16.24415825 18.31675832
mean as returned 21.13440704 16.24416733 18.31677628

null is genuinely lossy — -inf, +inf and a real NaN all collapse to it —
so this is a recovery, not a decode. It is right for every value the model
actually produces here, and becomes a no-op if the payload ever encodes
non-finite floats losslessly.

The full-output row cap raised instead of splitting

A full-output response carries one logit per bar per test row, so the API caps
the rows one response may cover. Callers above the cap got a ValueError telling
them to split by hand, where the same call locally just works. predict now
splits the request itself and concatenates the parts; borders is
row-independent so it is taken once, and an unrecognised field fails loudly
rather than silently returning the first chunk.

Verified against the live API: the merged arrays are identical to a manual
split.

Scope

Only output_type="full" is affected. Other output types keep the existing
"split it yourself" behaviour, since their caps are compute limits rather than a
payload-size artefact.

6 new unit tests, all offline.

@ggprior
ggprior force-pushed the georg/full_output_chunking branch from fb32c68 to 1ec43ed Compare August 27, 2026 14:20
@ggprior
ggprior requested a review from simo-prior August 27, 2026 15:31
@ggprior
ggprior marked this pull request as ready for review August 27, 2026 15:31
@ggprior
ggprior requested a review from a team as a code owner August 27, 2026 15:31
Two things made a full regression prediction through the client differ
from the same call against the local tabpfn package.

Bars outside a row's support are -inf, which the response encoding has no
representation for, so they arrived as NaN. Softmaxing them yields NaN
for the whole row, which propagates into anything that samples from the
returned criterion. Restoring them to -inf reproduces the server's own
`mean` field to float32 precision.

The full-output payload also caps how many test rows one response may
cover, so callers above the cap got a ValueError telling them to split
by hand. `predict` now splits the request itself and concatenates the
parts, which is what the arrays from one unrestricted call would hold.
@ggprior
ggprior force-pushed the georg/full_output_chunking branch from 1ec43ed to ea47711 Compare August 27, 2026 15:33
@ggprior
ggprior added this pull request to the merge queue Aug 31, 2026
Merged via the queue into main with commit 43a6b6b Aug 31, 2026
11 checks passed
@ggprior
ggprior deleted the georg/full_output_chunking branch August 31, 2026 09:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants