Skip to content

Specializer M-boundary: generated row marshaller + SpecializedTransform (TASK-45) - #29

Merged
claude-agent-ahrzb[bot] merged 11 commits into
masterfrom
claude/specializer-m-boundary
Jul 26, 2026
Merged

Specializer M-boundary: generated row marshaller + SpecializedTransform (TASK-45)#29
claude-agent-ahrzb[bot] merged 11 commits into
masterfrom
claude/specializer-m-boundary

Conversation

@claude-agent-ahrzb

Copy link
Copy Markdown

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

  • Generated row marshaller (src/duckdb/mod.rs): interned attribute names in fixed field order, input buffers + RunState owned 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_BOUNDARY pins the old path as the bench baseline; .boundary getter 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.
  • Allocation-free steady state (AC feat(codegen): struct/list containers — TASK-29 Phase B (Tasks 1–2 of 4) #4): ColData::Str became flat buffer + spans; substr/trim are pure sub-span arithmetic; case map and number→text stream into the arena; h_probe emits without clones. Counting-allocator tests pin zero heap allocations per warm run on both backends.
  • Adversarial fleet (17 agents) ran before merge: 9 confirmed findings → 3 root causes, all fixed with regression tests (supplied output_models keep full model_validate semantics; the generic baseline accepts dict rows; reentrant infer falls back to the generic path instead of raising "Already borrowed").

Measured (§10 discipline, p50 ns/call)

n=1 n=1024
marshaller vs generic boundary (no-op f) 1.9× 2.5×
specializer vs shipping native (worst/best case) 1.5–2.8× 3.7–4.2×
cranelift vs interp control (arith, n=1024) 339µs vs 391µs

Acceptance criteria (TASK-45) — all checked

  1. ✅ SpecializedTransform fit/infer/infer_batch on the v0 subset, dict + model rows
  2. ✅ No-op boundary baseline: generic vs marshaller at n ∈ {1, 8, 64, 1024}
  3. ✅ End-to-end p50/p99 vs native + codegen
  4. ✅ Steady-state hot path allocates nothing beyond the output objects (counting-allocator proof)
  5. mise gate-specializer green — cargo 127 + pytest 615, corpus 53/625/0 of 678

🤖 Generated with Claude Code

ahrzb and others added 8 commits July 26, 2026 09:05
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 and others added 3 commits July 26, 2026 11:09
…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>
@claude-agent-ahrzb
claude-agent-ahrzb Bot merged commit b6e39d7 into master Jul 26, 2026
1 check passed
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.

1 participant