Skip to content

Commit 496f9f6

Browse files
ahrzbclaude
authored andcommitted
docs: DRAFT-23 (native UDF families) + DRAFT-22 addendum
Typed confit.udfs.* entries with extracted state supersede desugar-into-SQL for known families: one SQL text across tiers, swap the entry, gate the swap. Keyless transformers inline the literal id; serving SQL keeps author names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 6325292 commit 496f9f6

2 files changed

Lines changed: 115 additions & 0 deletions

backlog/drafts/draft-22 - UDF-protocol-and-Confit-externs-serving-transformers-without-special-support.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,28 @@ unpinnable) — written down per kind, not discovered per bug.
188188
3. The params-join wiring Confit already needs regardless
189189
(`IS NOT DISTINCT FROM`, `ON ((1 = 1))`) — previously queued.
190190

191+
## Addendum (2026-07-29, follow-up dialogue — supersedes two points above)
192+
193+
AmirHossein's shape for the PCA/tree cases, adopted:
194+
195+
1. **Optimization = a typed udf entry, never a SQL rewrite.** "Desugaring
196+
becomes a column-width decision" above is superseded: fitted state does
197+
NOT move into wide params columns, and the call is NOT inlined into SQL
198+
arithmetic. Instead the udfs list carries typed entries —
199+
`confit.udfs.PCA("pca", n_components=2, instances={id: {"mean": [...],
200+
"components": [[...]]}})` — hyperparams on the entry, extracted state
201+
(plain arrays / tree tables, no pickle) per instance id, executed by a
202+
native kernel behind the same extern slot. The SQL text is identical
203+
across tiers, so the oracle gate is "swap the entry, rerun, compare" and
204+
a disagreement is always the kernel's fault. Full design: DRAFT-23.
205+
2. **Keyless transformers inline the literal id** (`pca(0, feats...)`) —
206+
no one-row params table, no `ON ((1 = 1))` join for the global case.
207+
(Keyless *aggregate* params still join; that wiring stays needed.)
208+
3. **Serving SQL keeps the author's function name** (`pca`, not
209+
`__cf_tf0`) — safe because the catalog guard proved no DuckDB collision;
210+
suffix (`pca_1`) when the same name is applied twice with different
211+
partitions.
212+
191213
## Rejected along the way
192214

193215
- **Compile-out as the primary path** (desugar/ONNX): a fallback is
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
---
2+
id: DRAFT-23
3+
title: "Native UDF families: typed confit.udfs.* entries with extracted state"
4+
status: Draft
5+
type: spike
6+
created: 2026-07-29
7+
---
8+
9+
## Where this came from
10+
11+
Design dialogue with AmirHossein, 2026-07-29, right after DRAFT-22 step 1
12+
landed (PR #62) and while the Confit extern substrate was being built. The
13+
shape is his: for known estimator families the *udf entry itself* goes
14+
native — the SQL never changes. This is also the design half of the parked
15+
"optimized sklearn transforms" milestone (milestone creation still gated on
16+
his go).
17+
18+
## The design
19+
20+
The `udfs=` list is a **typed registry**. Same SQL text under every entry:
21+
22+
```python
23+
DuckDBInferFn(
24+
'SELECT pca(__cf_p0.__cf_est, __cf_t.age, __cf_t.fare) AS e, ...'
25+
' FROM __THIS__ AS __cf_t LEFT JOIN __CF_PARAMS_0__ AS __cf_p0 ON ((...))',
26+
static_tables={"__CF_PARAMS_0__": "<country, __cf_est>"},
27+
udfs=[confit.udfs.PCA(
28+
"pca", n_components=2,
29+
instances={0: {"mean": [37.2, 8.1], "components": [[...], [...]]},
30+
1: {...}}, # plain arrays, extracted at fit — no pickle
31+
)],
32+
)
33+
34+
# trees: fit step is an importer; keyless => literal id, no join at all
35+
DuckDBInferFn(
36+
'SELECT score(0, __cf_t.age, __cf_t.fare) AS s, ...',
37+
udfs=[confit.udfs.TreeEnsemble("score", instances={0: "<trees as data>"})],
38+
)
39+
```
40+
41+
| entry | implementation | state | parity tier |
42+
|---|---|---|---|
43+
| `PythonTransform` | GIL trampoline -> `est.transform` | pickle | the oracle, always available |
44+
| `confit.udfs.StandardScaler` | native affine kernel | arrays | bit-exact target |
45+
| `confit.udfs.PCA` | native matvec kernel | arrays | toleranced (BLAS order unpinnable) |
46+
| `confit.udfs.TreeEnsemble` | native tree walk | tree tables | bit-exact reachable (compares + fixed-order sums) |
47+
48+
Rules:
49+
50+
- **Hyperparams live on the entry** (`n_components=2`); fitted state per
51+
instance id. The entry is self-describing: `takes`/`returns` are
52+
*derived* by the class (PCA with n_components=2 knows returns is 2 wide),
53+
not hand-declared.
54+
- **One SQL text across tiers.** Fallback -> optimized is swapping the list
55+
entry. The gate is "same query, same statics, swap the entry, compare"
56+
— no query rewrite to account for, so a disagreement is always the
57+
kernel's fault. Tolerance declared per family (table above), never
58+
discovered per bug.
59+
- **Family recognition in `SQLProjection._prepare`, conservative:** if the
60+
fitted estimator (or every step of a fitted Pipeline) maps to a native
61+
family, emit the typed entry with extracted state; anything unrecognized
62+
falls through to `PythonTransform`. Recognition is per-estimator-class;
63+
the fallback existing is what lets recognition stay conservative.
64+
- **No pickle for known families** — instances are arrays/tables,
65+
JSON/Arrow-serializable. This is what unlocks non-Python hosts (the
66+
wazero/chicory WASM path): a Go/Java service can serve per-country PCA
67+
because the entry is data plus a kernel that ships inside the engine.
68+
`PythonTransform` can never cross that boundary; native entries can.
69+
- **Engine side:** each family is a new `ExternImpl` variant behind the
70+
same extern slot the DRAFT-22 substrate provides (Cx slot table:
71+
PyTrampoline | NativeKernel). Frontend/type-checking unchanged.
72+
- **DuckDB batch side:** each `confit.udfs.*` class also registers a plain
73+
Python implementation of the same math (`create_function`), so the batch
74+
path and the differential gate run without the Rust engine in the loop.
75+
76+
## Sequencing
77+
78+
1. DRAFT-22 substrate lands (extern slots + PythonTransform trampoline +
79+
params-join wiring — in flight).
80+
2. `confit.udfs.StandardScaler` first (affine, bit-exact target — proves
81+
the swap-the-entry gate), then `PCA` (tolerance story), then
82+
`TreeEnsemble` (importer fit step, `reads = ()`).
83+
3. Interpretability follow-up, later: named per-component output columns.
84+
85+
## Rejected along the way
86+
87+
- **Desugar-into-SQL-expressions** for known families (DRAFT-22's original
88+
"column-width decision"): a query rewrite between tiers means the gate
89+
compares two different texts — weaker isolation, and wide params columns
90+
can't carry matrices/trees anyway. Superseded by typed entries.
91+
- **Pickled sklearn as the serving artifact for known families**: keeps
92+
Python in the serve path and locks out non-Python hosts; it remains the
93+
catch-all tier only.

0 commit comments

Comments
 (0)