Prefer tract over CoreML on macOS by default (+ engine kill-switch and per-model override) - #20
Conversation
anketpratapsingh
left a comment
There was a problem hiding this comment.
Review: read in full, built and exercised on Windows
The resolve_with(is_macos) seam works exactly as advertised: all 10 new policy tests run deterministically on this non-macOS host. Full suite 68 passed / 0 failed in release mode, rustfmt and clippy -D warnings clean (toolchain 1.97 here, so the 1.98 lint motivation is taken from CI, same as your review of #22 noted in the other direction).
Design reads right to me: the layering (env kill-switch > catalog override > capability filter > macOS flip) puts the most explicit signal on top; filtering at registry() so capabilities() reports the restricted set keeps the CLI handshake honest; macos_engine = "tract" on an incapable family erroring loudly is the correct fail-closed choice, and the truth-table test pins the whole policy. The kill-switch only ever removing ORT (typos warn and change nothing) is a good property.
Needs action before merge
1. Rebase onto main — trial merge conflicts in src/model.rs. Main (via #22, merged 08:58Z today) already carries the blob_to_f32 as_chunks migration, with an explanatory comment; this branch carries a comment-less copy of the same change (83694cb), so both sides edited the same region and git cannot auto-merge. This is the third branch to carry that hunk — on rebase your copy should drop out, leaving the two real commits. (Verified by trial merge against origin/main; the branch is currently 7 commits behind.)
2. Missing CHANGELOG entry and upgrader note. This flips the macOS default that 1.4.0's own "Notes for upgraders" announced in the opposite direction. Every Apple Silicon install that reindexed under 1.4.0 CoreML will now resolve to tract, trip the backend-provenance warning, and should be told to travsr embed reindex --rebuild — the exact mirror of the note 1.4.0 shipped for the tract-to-CoreML flip. As it stands the diff touches no CHANGELOG at all.
Cross-repo sharp edge worth documenting (non-blocking here, fix belongs in travsr)
A hand-set macos_engine is silently wiped by the next travsr embed init. The CLI's write_model_descriptor (travsr repo, embed_catalog.rs) serializes a closed field set — dim/pooling/query_prefix/n_inputs/truncate_dim/family — and install_models calls it unconditionally at the end, even when every model file was "already present". So the documented operator flow ("set macos_engine = tract for a misbehaving model") survives only until anyone re-runs embed init, which reverts the model to auto with no warning. The env kill-switch is unaffected, and auto is the right default for today's catalog, so nothing breaks now — but the field's doc comment should say the CLI rewrites the file, and the travsr-side fix (carry macos_engine through the writer, or promote it to catalog data as your out-of-scope note suggests) deserves an issue so it does not get lost.
Observation, no action
Interplay with #22 (now on main): flipping macOS from CoreML to tract also moves those installs from the single-submitter regime (one inference loop, whole token budget) to multi-worker --parallel with the divided budget. That is strictly favourable — your measured 743 MB tract RSS predates #22's flattening — but it means macOS perf after rebase will look different (better) than this PR's own benchmark table, which is worth a one-line note when you re-verify post-rebase.
Verdict
The measurement is convincing, the mechanism explains it, the escape hatches are all present, and the tests pin the policy tightly. No objection to the substance — happy to see this merge once it is rebased and carries the upgrader note.
Adds a no-rebuild engine override: TRAVSR_EMBED_ENGINE=tract drops every ORT factory (accelerated + CPU) from the registry so the resolver can only select the pure-Rust tract CPU engine; auto or unset keeps the normal preference cascade; anything else is ignored with a warning. Applied in registry() so capabilities() reports the same restricted set the resolver will use, and the pre-flight handshake never advertises acceleration the user has switched off. The filter is split into apply_engine_override(factories, env) so it is unit-tested without mutating process-wide env under a parallel test run. Motivation: on macOS the CoreML execution provider is measurably slower and far heavier than tract for the BERT-family models we ship, so operators need a way to force tract without shipping a new binary. The engine-selection default itself is tracked in #19.
On macOS the accelerated ORT path is CoreML, which for the BERT-family
models shipped here is roughly 2x slower and 5-6x heavier than the
pure-Rust tract engine (it fragments the graph into many CoreML/CPU
partitions and pays a dynamic-shape recompile tax). The resolver ranked
CoreML (pref 100) above tract (pref 50), so it won whenever it warmed up.
Flip the default on macOS: tract outranks the accelerated engine for
families tract can run (it is lifted just above PREF_ACCELERATED via
effective_preference). ORT is still reached when tract cannot run the
family (ModernBERT, nomic-bert). Off macOS "accelerated" is CUDA/etc.
and keeps its top preference, so the flip is gated to is_macos.
Add a per-model catalog override, model.toml `macos_engine`:
auto (default) - the flip above
tract - drop every ORT factory (per-model kill-switch)
ort - keep accelerated-first (for a model benchmarked
faster on CoreML, e.g. a future large model)
resolve() is split into a testable resolve_with(is_macos) so the
macOS-only ordering is deterministic on any CI host. Precedence, most
explicit first: TRAVSR_EMBED_ENGINE env, then model.toml macos_engine,
then capability, then the default flip.
Verified E2E on a real CoreML build: auto -> tract, macos_engine=ort ->
ort/CoreML, and TRAVSR_EMBED_ENGINE=tract overrides the ort catalog
back to tract. macos_engine defaults to auto so existing model.toml
files need no migration; the CLI populating it per model is tracked in
#19, as is the CoreML EP static-shape/MLProgram tuning lead.
A wrapped doc line began with '+ CPU)', which clippy parses as a markdown list bullet under -D warnings (doc_lazy_continuation). Reword so no line starts with '+'. Doc comment only, no behaviour change.
The Unreleased section carried no entry for this branch at all, and the flip reverses the macOS default that 1.4.0's own "Notes for upgraders" announced in the other direction. Every Apple Silicon install that reindexed under 1.4.0 on CoreML now resolves to tract, trips the backend-provenance warning, and should run `travsr embed reindex --rebuild`, so the upgrader note is the mirror of the one 1.4.0 shipped for the tract-to-CoreML flip, and points at `macos_engine = "ort"` for anyone who wants to stay on CoreML. Also records, on the `macos_engine` field and in the changelog, that the value is not durable: the CLI's `write_model_descriptor` serializes a closed field set (dim/pooling/query_prefix/n_inputs/truncate_dim/family) that does not include `macos_engine`, and `install_models` calls it unconditionally at the end of the run even when every model file was "already present". A hand-set value therefore reverts to `auto` on the next `embed init` with no warning. The env kill-switch is unaffected, and `auto` is correct for today's catalog, so nothing breaks now; the durable fix belongs in travsr.
83694cb to
4ca61e9
Compare
anketpratapsingh
left a comment
There was a problem hiding this comment.
Re-review after the rebase, focused on memory and performance impact
Verified the update first: rebased onto main (now MERGEABLE, redundant as_chunks commit gone), changelog added with an explicit Notes-for-upgraders section, and the macos_engine-wipe sharp edge from my first review is now documented both in the changelog and the field's doc comment, with the env switch named as the durable alternative. The only src delta since my first pass is that expanded doc comment — logic is byte-identical, so my earlier functional review stands. Re-ran locally on Windows against the rebased base: 78 passed / 0 failed in release mode, clippy -D warnings clean; all 8 CI checks green.
Memory: no new risk, and a large improvement where it applies
- The change itself cannot leak or accumulate.
MacosEngineis aCopyenum on the existing descriptor; the kill-switch is aretain()over a two-to-three element factory Vec at registry construction. No new threads, caches, statics, or retained buffers anywhere in the diff — nothing in the class of problem #735 was about. - macOS steady state improves dramatically by this PR's own A/B: 743 MB peak vs ~4.0 GB on CoreML for the same corpus. And post-rebase it compounds with the token-budget change now in the base: tract is the multi-worker path, so the divided budget bounds activations as
-jscales, where CoreML was single-submitter holding the whole budget. Real-world macOS peaks should land below the PR's own pre-rebase 743 MB figure. - Off macOS: zero change by default. The flip is
is_macos-gated and the env switch is opt-in;bert_with_confirmed_accelerator_chooses_accelerated_ort(run withis_macos=false) pins accelerated-first ordering structurally, and it passes.
Performance: improvement on macOS, unmeasurable cost elsewhere, one bounded regression window
- macOS BERT-family: ~2x throughput per the 30k-doc measurement, and CoreML never finished that corpus at all. Non-BERT families keep ORT (tested), so nothing loses acceleration it needs.
- Runtime cost of the new code: one env read per
registry()call and one enum compare inresolve— both run once per model load, never per query or per batch. Unmeasurable. - One-time upgrade cost, correctly documented: the backend flip trips the provenance warning and recommends a full
--rebuildon macOS installs that reindexed under CoreML. Worth knowing that the rebuild itself runs ~2x faster on tract than it would have on CoreML, so the toll is smaller than it sounds. - The one residual perf gap is the case where CoreML genuinely wins for some future BERT-family model on some Mac: the default costs it acceleration, and the durable escape (
macos_engine = "ort") is undermined by the CLI rewrite until the travsr-side fix lands. Note the asymmetry: the env switch can force tract but there is deliberately no env value to force ort. That is defensible (the switch is a kill-switch, not a selector), but if the CLI fix is slow to land, anortenv value would be the cheap interim escape. Per the current catalog no shipped model is in that set, so this is a forward-looking note, not a present regression.
Verdict
Approving. Memory: strictly improved on macOS, untouched elsewhere, nothing that can grow or leak. Performance: improved where it changes anything, with the single documented one-time rebuild cost at upgrade.
What
Makes tract the default embedding engine on macOS and adds two overrides, so CoreML stops being picked for models where it loses. Fixes the selection half of #19.
Why
On macOS the accelerated ORT path is CoreML. Measured on Apple Silicon with the shipped bge-small model over an identical 30k-document corpus (all release builds):
CoreML loses because ORT fragments the BERT graph into ~97 CoreML/CPU partitions (a copy at every seam) and pays a dynamic-shape recompile tax. The resolver ranked CoreML (pref 100) above tract (pref 50), so it won whenever it warmed up.
Note: an earlier autorelease-leak hypothesis turned out to be a red herring. That ORT bug (onnxruntime#14455) is already fixed upstream in
model.mm, and an A/B soak confirmed a Rust-side autorelease wrap changed nothing, so it is not included here.Change
Layered engine resolution, most explicit first:
TRAVSR_EMBED_ENGINEenv kill-switch:tractdrops every ORT factory;auto/unset keeps the cascade.model.tomlmacos_engine:auto(default) |tract(per-model kill-switch) |ort(opt back into accelerated-first, for a model benchmarked faster on CoreML).The flip is gated to
is_macos, so Linux/CUDA keeps accelerated-first.resolve()is split into a testableresolve_with(is_macos)so the macOS-only ordering is deterministic on any CI host.macos_enginedefaults toauto, so existingmodel.tomlfiles need no migration.Verification
Unit tests: default and
--features ort-coremlsuites green (flip, both catalog values, incapable-family error, precedence truth table). fmt + clippy-D warningsclean on both feature sets.E2E on a real ort-coreml release build (bge-small):
tractmacos_engine = "ort"ort/CoreMLTRAVSR_EMBED_ENGINE=tractover the ort catalogtract(env wins)Out of scope (tracked in #19)
ortcrate exposes no partition/node-assignment API. The better lead is CoreML EP tuning (with_static_input_shapes,with_model_format(MLProgram),with_model_cache_dir), which needs its own benchmark.macos_engineper model from the catalog as models are benchmarked. Until then everything defaults toauto(the flip), which is correct for the current catalog. tract cannot run ModernBERT/nomic-bert, where ORT is still used.