Specializer M-boundary: generated row marshaller + SpecializedTransform (TASK-45) - #29
Merged
Merged
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…onstruct output, reused state + span-buffer Str columns (TASK-45 stretch 1) The boundary work knowable at prepare time now happens at prepare time: attribute names interned once, model_construct resolved once, input buffers and RunState owned by the fn and cleared (not dropped) per call. Rows cross as dicts or pydantic models. ColData::Str holds one flat buffer + spans instead of per-cell Strings, which also removes a hidden per-load String clone in the cranelift h_load_str helper. SPECIALIZER_GENERIC_BOUNDARY pins the old path as the bench baseline; .boundary getter mirrors .backend. infer_rows() is the direct hot entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d by the specializer (TASK-45 stretch 2) SQLTransform minus transformer refs: fit() reuses the state-extraction pipeline (window aggregates freeze into per-partition static tables, template refs inline), then prepares through DuckDBInferFn — global and partitioned window aggregates land on cranelift via the equi-join rewrite and agree with SQLTransform row for row. infer/infer_batch take dicts or pydantic models straight through the marshaller, no SimpleNamespace hop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(TASK-45 stretch 4) Counting-allocator tests now pin zero heap allocation per warm run for the interpreter AND cranelift, over the probe/arith fixture and a string-heavy program (case, trim, substr, concat, itos/ftos, parse, compare). What had to change: substr and trim become pure sub-span arithmetic (byte ranges into the input span — no copy at all), case mapping streams char-at-a-time into the arena (Arena::case_map), number->text formats straight into the arena (Arena::push_fmt) with DuckF64 rendering to a stack buffer, string compare/parse read the arena immutably, and h_probe emits values without the per-call Vec + ScalarVal clones. All shared between backends; 500-seed differential and the full 612-test pytest suite stay green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eline (TASK-45 stretch 3) Measurement killed an assumption: pydantic v2's model_construct is pure-Python and SLOWER than model_validate (1432ns vs 882ns per row, pydantic 2.13). The marshaller now builds output rows the way the design doc meant — object.__new__ + direct slot writes (__dict__, __pydantic_fields_set__, extra, private), 491ns measured — and the bench gained a 'generic' engine (SPECIALIZER_GENERIC_BOUNDARY) as the AC #2 baseline. Results: marshaller beats the generic boundary 1.9-2.5x on the no-op f; the specializer beats the shipping native engine 3.7-4.2x end-to-end at n=1024 and 1.5-2.8x at n=1; the JIT-vs- interp compute gap is finally visible through Python. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ion notes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…semantics, dict rows on the generic baseline, reentrancy fallback (TASK-45) A 17-agent adversarial fleet confirmed 9 findings reducing to three root causes, all fixed with regression tests: 1. The slot-fill output silently broke user-SUPPLIED output models (validators skipped, defaults dropped to AttributeError, extra=allow and private attrs crashed). Slot fill now applies only to the synthesized plain model; a supplied model goes through model_validate per row — master semantics exactly. 2. SPECIALIZER_GENERIC_BOUNDARY dropped dict-row support, making the baseline reject documented inputs with a misleading missing- attribute error. The generic fill now takes the same dict path. 3. infer's move to &mut self regressed same-object reentrancy (a row property calling infer mid-marshal raised 'Already borrowed'). infer is &self again with the marshaller in a RefCell; a reentrant call finds it borrowed and completes via the per-call generic path. Bench spot-check after the fixes: unchanged (noop 1.1µs at n=1, ~2.6x vs generic at n=1024). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ahrzb
approved these changes
Jul 26, 2026
…e harness, three-way parity gate (titanic + fraud so far) Wide-table Kaggle-style inference paths as repo assets: titanic (10->20, string/CASE-heavy) and IEEE-CIS-style fraud (32->38, five join tables, NULL-indicator flags) — each with a handcrafted-Python twin and exact three-way parity (specializer == DuckDB multiset == handcrafted) gated in pytest before any timing. Engines: spec / interp / generic / native / codegen / duckdb-per-call / python(+dict floor). First numbers: spec beats the handcrafted twin 1.8-1.9x at n=1024; duckdb-per-call is ms- scale; native and codegen cannot build either scenario (IS NULL projections unsupported there). house_prices + store_sales to follow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… harness-aware titanic/fraud rewrites Four famous-problem inference paths, all passing exact three-way parity (specializer == DuckDB multiset == handcrafted twin) on 300 seeded rows: titanic 10->20 (strings/CASE), house_prices ~40->30+ (wide arithmetic, quality ordinals, neighborhood encoding), fraud_txn 32->38 (five probe tables, NULL flags, time arithmetic), store_sales ~20+dim (store-dim join, competition/promo2 arithmetic, seasonal factors). Each module records which real winning-solution features the v0 surface could NOT express — the workload-driven support ladder (log/log1p in all four; instr/position; true floor; IN-list; fractional pow; trig). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…trim semantics (reviewer note) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ahrzb
approved these changes
Jul 26, 2026
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.
M-boundary — the milestone where the compute win becomes end-to-end visible
Design doc milestone 6 (flag 1): everything about the Python boundary that is knowable at prepare time now happens at prepare time.
What landed
src/duckdb/mod.rs): interned attribute names in fixed field order, input buffers +RunStateowned and cleared-not-dropped per call, dict or pydantic rows, output rows by direct pydantic-v2 slot fill for the synthesized model (measured: construct 1432ns > validate 882ns > slot fill 491ns/row).SPECIALIZER_GENERIC_BOUNDARYpins the old path as the bench baseline;.boundarygetter mirrors.backend;infer_rows()is the direct hot entry.SpecializedTransform(sql_transform/_specialized.py): SQLTransform surface minus transformer refs, reusing the whole fit pipeline — window aggregates freeze into static tables and ride the equi-join rewrite onto cranelift; parity with SQLTransform asserted row-for-row.ColData::Strbecame flat buffer + spans; substr/trim are pure sub-span arithmetic; case map and number→text stream into the arena;h_probeemits without clones. Counting-allocator tests pin zero heap allocations per warm run on both backends.output_models keep fullmodel_validatesemantics; the generic baseline accepts dict rows; reentrantinferfalls back to the generic path instead of raising "Already borrowed").Measured (§10 discipline, p50 ns/call)
Acceptance criteria (TASK-45) — all checked
mise gate-specializergreen — cargo 127 + pytest 615, corpus 53/625/0 of 678🤖 Generated with Claude Code