Skip to content

Commit b92ad2b

Browse files
mstrathmanclaude
andcommitted
feat(onnx): ONNX runtime serving path for predict() (vector layout)
Adds the opt-in foundation-model serving path (make loadable-onnx, -DSQLITE_PREDICT_ONNX). predict-onnx.c is the only file that links onnxruntime; the core `loadable` stays pure C99, zero-dependency (0 onnx symbols, 0 onnxruntime linkage, verified). This pass implements the 'vector' io_spec layout: a self-contained, pre-trained model (a distilled student or any exported tabular classifier/regressor) mapping features -> prediction, dispatched through the existing predict() model-backend seam. The 'in_context' layout (a teacher such as TabFM that ingests the training rows as context) and the GPU execution providers are the next pass, validated on the gated GPU runner; requesting cuda/tensorrt here fails loud rather than silently dropping to CPU. Performance shape (per RFC 0005): sessions are cached process-global by (weights, device, precision) and reused; query rows run in batches, not one at a time; execution-provider selection is explicit. Registry/plumbing: - _predict_models gains weights_uri (external teacher weights) + io_spec (tensor mapping); the weight-source CHECK now allows an inline BLOB (small students) XOR a URI (large teachers). Idempotent column adds. - predict_register(model_id, config_json) records a model and pins its weights by content hash so the receipt/replay path can detect changes. - New error codes: MODEL_EXISTS, RUNTIME_UNAVAILABLE, IO_SPEC, INFERENCE. - Receipts record the execution provider + precision; the CPU-fp32 path is deterministic, so replay is bit-exact (proven by the tests). - License gate: a non-permissive model (TabFM is non-commercial) needs accept_license to match before it will run. Tests (self-skip when the extension has no onnx runtime, so `make test` ignores them): hand-built ONNX fixtures with reference outputs computed independently in pure Python; classifier + regressor correctness, exact replay, mutation detection, 2500-row multi-batch, non-numeric handling, license gate, fail-loud device/precision, schema mismatches, and an RSS leak soak. Plus an ASan/UBSan/LSan C soak (make test-asan-onnx) and a CI onnx job on ubuntu. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: mstrathman <matthew.strathman@gmail.com>
1 parent 0e4887e commit b92ad2b

15 files changed

Lines changed: 1997 additions & 9 deletions

‎.github/workflows/ci.yml‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,24 @@ jobs:
106106
make soak-wasm
107107
- name: Run under node
108108
run: node dist/soak.js
109+
110+
onnx:
111+
name: onnx (cpu)
112+
runs-on: ubuntu-latest
113+
env:
114+
ORT_VERSION: "1.27.1"
115+
steps:
116+
- uses: actions/checkout@v4
117+
- name: Install uv
118+
uses: astral-sh/setup-uv@v5
119+
- name: Fetch onnxruntime
120+
run: |
121+
curl -fsSL -o ort.tgz \
122+
"https://github.com/microsoft/onnxruntime/releases/download/v${ORT_VERSION}/onnxruntime-linux-x64-${ORT_VERSION}.tgz"
123+
tar xzf ort.tgz
124+
echo "ONNXRUNTIME_PREFIX=$PWD/onnxruntime-linux-x64-${ORT_VERSION}" \
125+
>> "$GITHUB_ENV"
126+
- name: Build the onnx variant + run its tests
127+
run: make test-onnx
128+
- name: ASan + LSan soak of the onnx backend
129+
run: make test-asan-onnx

‎Makefile‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ prefix?=dist
3838
TARGET_LOADABLE=$(prefix)/predict0.$(LOADABLE_EXTENSION)
3939

4040
OBJS=sqlite-predict.c predict-forecast.c predict-receipts.c predict-tabular.c vendor/sha256.c
41+
ONNX_OBJS=$(OBJS) predict-onnx.c
42+
43+
# onnxruntime is a build+runtime dependency of the loadable-onnx variant
44+
# only. Point ONNXRUNTIME_PREFIX at an install (dir with include/ + lib/),
45+
# or install it (macOS: `brew install onnxruntime`; the prefix is auto-
46+
# detected there). The include lives at include/ for tarball installs and
47+
# include/onnxruntime/ for brew, so both are on the search path.
48+
ONNXRUNTIME_PREFIX ?= $(shell brew --prefix onnxruntime 2>/dev/null)
49+
ONNX_CFLAGS=-DSQLITE_PREDICT_ONNX -I$(ONNXRUNTIME_PREFIX)/include \
50+
-I$(ONNXRUNTIME_PREFIX)/include/onnxruntime
51+
ONNX_LDFLAGS=-L$(ONNXRUNTIME_PREFIX)/lib -lonnxruntime \
52+
-Wl,-rpath,$(ONNXRUNTIME_PREFIX)/lib
4153

4254
$(prefix):
4355
mkdir -p $(prefix)
@@ -58,6 +70,14 @@ sqlite-predict.h: sqlite-predict.h.tmpl VERSION
5870
loadable: $(prefix) vendor/sqlite3ext.h sqlite-predict.h $(OBJS)
5971
$(CC) -fPIC -shared -std=c99 -Wall -Wextra -Ivendor/ -I./ -O3 $(CFLAGS) $(OBJS) -o $(TARGET_LOADABLE) $(LDFLAGS)
6072

73+
# opt-in ONNX build: same loadable, plus predict-onnx.c linked against
74+
# onnxruntime. Serves onnx-runtime models (distilled students, exported
75+
# classifiers) through predict(). The core `loadable` stays zero-dependency.
76+
loadable-onnx: $(prefix) vendor/sqlite3ext.h sqlite-predict.h $(ONNX_OBJS)
77+
$(CC) -fPIC -shared -std=c99 -Wall -Wextra -Ivendor/ -I./ -O3 \
78+
$(ONNX_CFLAGS) $(CFLAGS) $(ONNX_OBJS) -o $(TARGET_LOADABLE) \
79+
$(LDFLAGS) $(ONNX_LDFLAGS)
80+
6181
debug: $(prefix) vendor/sqlite3ext.h sqlite-predict.h $(OBJS)
6282
$(CC) -fPIC -shared -std=c99 -Wall -Wextra -Ivendor/ -I./ -g -O0 -DSQLITE_PREDICT_DEBUG $(CFLAGS) $(OBJS) -o $(TARGET_LOADABLE) $(LDFLAGS)
6383

@@ -66,6 +86,12 @@ test-loadable: loadable
6686

6787
test: test-loadable
6888

89+
# ONNX build + its tests. The onnx-marked tests build a fixture model and
90+
# exercise the runtime path; they self-skip when the loaded extension has
91+
# no onnx runtime, so the core `make test` above ignores them.
92+
test-onnx: loadable-onnx
93+
cd tests && uv run pytest -q
94+
6995
# ASan+UBSan on the C soak driver (standalone executable: no DYLD
7096
# injection needed, macOS SIP strips it for system binaries anyway).
7197
# Covers every operation, receipts, replay, and the error paths.
@@ -78,6 +104,21 @@ test-asan: vendor/sqlite3ext.h sqlite-predict.h
78104
tests/soak.c $(OBJS) vendor/sqlite3.c -o $(prefix)/soak-asan
79105
UBSAN_OPTIONS=print_stacktrace=1 ./$(prefix)/soak-asan
80106

107+
# ASan/UBSan soak for the onnx backend (leak-checked on Linux via LSan;
108+
# onnxruntime's own still-reachable allocations are filtered by onnx.supp).
109+
# Needs onnxruntime (ONNXRUNTIME_PREFIX) and the committed fixture model.
110+
test-asan-onnx: vendor/sqlite3ext.h sqlite-predict.h
111+
mkdir -p $(prefix)
112+
clang -std=c99 -g -O1 -fsanitize=address,undefined \
113+
-fno-omit-frame-pointer -fno-sanitize-recover=undefined \
114+
-DSQLITE_CORE -DSQLITE_PREDICT_STATIC -DSQLITE_STRICT_SUBTYPE=1 \
115+
$(ONNX_CFLAGS) -Ivendor/ -I./ \
116+
tests/soak_onnx.c $(ONNX_OBJS) vendor/sqlite3.c \
117+
-o $(prefix)/soak-onnx-asan $(ONNX_LDFLAGS)
118+
LSAN_OPTIONS=suppressions=tests/onnx.supp \
119+
UBSAN_OPTIONS=print_stacktrace=1 \
120+
./$(prefix)/soak-onnx-asan tests/fixtures/logreg.onnx
121+
81122
# libFuzzer harness (statically links sqlite3.c; SQLITE_CORE build)
82123
fuzz-build: vendor/sqlite3ext.h sqlite-predict.h
83124
mkdir -p $(prefix)
@@ -134,4 +175,5 @@ clean:
134175
format:
135176
clang-format -i sqlite-predict.c predict-*.c
136177

137-
.PHONY: loadable debug test test-loadable clean format
178+
.PHONY: loadable loadable-onnx debug test test-loadable test-onnx \
179+
test-asan-onnx clean format

‎predict-internal.h‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ typedef size_t usize;
6161
#define PREDICT_ERR_PROBE "PREDICT_ERR_PROBE"
6262
#define PREDICT_ERR_LICENSE "PREDICT_ERR_LICENSE"
6363
#define PREDICT_ERR_MODEL_NOT_FOUND "PREDICT_ERR_MODEL_NOT_FOUND"
64+
#define PREDICT_ERR_MODEL_EXISTS "PREDICT_ERR_MODEL_EXISTS"
6465
#define PREDICT_ERR_MODEL_HASH "PREDICT_ERR_MODEL_HASH"
66+
#define PREDICT_ERR_RUNTIME_UNAVAILABLE "PREDICT_ERR_RUNTIME_UNAVAILABLE"
67+
#define PREDICT_ERR_IO_SPEC "PREDICT_ERR_IO_SPEC"
68+
#define PREDICT_ERR_INFERENCE "PREDICT_ERR_INFERENCE"
6569
#define PREDICT_ERR_STUDENT_EXISTS "PREDICT_ERR_STUDENT_EXISTS"
6670
#define PREDICT_ERR_RESOURCE "PREDICT_ERR_RESOURCE"
6771
#define PREDICT_ERR_RECEIPT_NOT_FOUND "PREDICT_ERR_RECEIPT_NOT_FOUND"
@@ -127,6 +131,73 @@ int predict0_receipts_ensure(sqlite3 *db, char **errmsg);
127131
/* content_hash of a registered model; sqlite3_malloc'd. NULL = absent. */
128132
char *predict0_registry_model_hash(sqlite3 *db, const char *model_id);
129133

134+
/* A registry row, as the dispatcher and runtime backends see it. All
135+
* strings are sqlite3_malloc'd; weights is a malloc'd copy of the inline
136+
* BLOB (NULL when the model is URI-referenced or has no local weights). */
137+
typedef struct {
138+
char *runtime; /* 'onnx' | 'ggml' | 'tree' | 'remote' | 'bundled' */
139+
char *kind; /* 'tabular-fm' | 'student' | ... */
140+
char *weights_uri; /* external path to weights; NULL if inline/none */
141+
char *io_spec; /* JSON tensor mapping; NULL if not set */
142+
char *license; /* SPDX id */
143+
char *content_hash; /* hex sha-256 pinning the exact weights */
144+
void *weights; /* inline weight bytes; NULL if URI/none */
145+
int weights_len;
146+
} predict0_model_row;
147+
148+
/* Look up a model. Returns 0 and fills *out on hit, 1 if absent, or an
149+
* SQLITE_ error. Free *out with predict0_model_row_free on a hit. */
150+
int predict0_registry_lookup(sqlite3 *db, const char *model_id,
151+
predict0_model_row *out);
152+
void predict0_model_row_free(predict0_model_row *m);
153+
154+
/* SHA-256 of a file's bytes, streamed. hex into out[65]. Returns 0 on
155+
* success; on failure sets *errmsg (sqlite3_malloc'd, PREDICT_ERR_* lead). */
156+
int predict0_hash_file(const char *path, char out[PREDICT_HEX_BUFSIZE],
157+
char **errmsg);
158+
159+
/* ---- runtime backends (predict-onnx.c, opt-in build) ---- */
160+
161+
/* Backend-relevant options, parsed from the predict() JSON. Borrowed
162+
* pointers into the caller's parsed options; valid for the call only. */
163+
typedef struct {
164+
const char *device; /* 'cpu'|'coreml'|'cuda'|'tensorrt'; NULL=cpu */
165+
const char *precision; /* 'fp32'|'fp16'|'int8'; NULL=fp32 */
166+
const char *accept_license; /* SPDX the caller accepts; NULL=none */
167+
int receipt; /* emit a receipt? */
168+
} predict0_backend_opts;
169+
170+
/* One prediction a runtime backend hands back, in apply-query order. */
171+
typedef struct {
172+
int ref_type; /* SQLITE_INTEGER/FLOAT/TEXT/NULL of the row_ref */
173+
i64 ref_i;
174+
f64 ref_f;
175+
char *ref_t; /* sqlite3_malloc'd, for a TEXT ref */
176+
char *prediction; /* sqlite3_malloc'd; NULL on status/error rows */
177+
f64 confidence;
178+
int has_conf;
179+
const char *status; /* static string ('ok','non_numeric',...) */
180+
} predict0_result;
181+
182+
void predict0_results_free(predict0_result *rows, int n);
183+
184+
#ifdef SQLITE_PREDICT_ONNX
185+
/* Vector-layout ONNX inference: the model is self-contained (features ->
186+
* prediction), so train_sql is not consulted. Reads row_ref + named
187+
* features from apply_sql, maps them to the model's declared feature order,
188+
* runs batched inference on the requested execution provider, and fills
189+
* rows/n (apply order; free with predict0_results_free). When
190+
* opts->receipt, emits a receipt and writes receipt_id_out. Returns
191+
* SQLITE_OK, or an SQLITE_ code with *errmsg set (PREDICT_ERR_* lead). */
192+
int predict0_onnx_predict_vector(sqlite3 *db, const char *model_id,
193+
const char *apply_sql,
194+
const predict0_model_row *model,
195+
const predict0_backend_opts *opts,
196+
predict0_result **rows, int *n,
197+
char receipt_id_out[PREDICT_ULID_BUFSIZE],
198+
char **errmsg);
199+
#endif
200+
130201
/* Deterministic logical digest of all user tables (schema + rows,
131202
* excluding _predict_% and sqlite_%), hex into out[65]. */
132203
int predict0_logical_digest(sqlite3 *db, char out[PREDICT_HEX_BUFSIZE], char **errmsg);

0 commit comments

Comments
 (0)