Skip to content

Split the four monoliths along their real dependency layers, and delete the code that was repeated - #35

Merged
AndreSlavescu merged 1 commit into
mainfrom
cleanup/structure-and-duplication
Aug 30, 2026
Merged

Split the four monoliths along their real dependency layers, and delete the code that was repeated#35
AndreSlavescu merged 1 commit into
mainfrom
cleanup/structure-and-duplication

Conversation

@AndreSlavescu

Copy link
Copy Markdown
Owner

What this is

A structural cleanup. No behaviour changes: 738 passed, 57 skipped — the same counts as before the first commit in this branch — and make check / make code-qual are clean.

The splits

Was Now Largest
test_mlx_backend.py 4,963 8 files by area 1,294
integrations/mlx_lm.py 4,715 8-module package 1,604
codegen/msl_emitter.py 2,717 6 modules 935
compiler/lowering.py 2,429 4 modules 1,115
backends/mlx_quantized.py 1,870 5 modules 825
kernels/ (top-level) metile/kernels/

Import paths are unchanged. Each package re-exports what its module used to expose, including the private names tests reach for.

Why they aren't split by concern

The first attempt at mlx_lm grouped by concern and produced 11 import cycles. The symbol graph says that shouldn't happen — 107 symbols in 100 strongly-connected components, largest of size 2, so it is nearly a DAG. The partition was cutting across its layering.

The thing that was backwards: _mlx_lm_plan_key calls apply_metile_to_mlx_lm, because tuning works by applying a config and measuring it. So apply is a mid-layer primitive, not the top. Placed there, the assignment comes out with zero ordering violations.

All four splits were solved the same way — build the dependency graph, find the SCCs that must stay together, run a fixpoint pushing callers above callees — rather than chosen by name.

The duplication

Mostly one shape: 18 _read_config/_write_config pairs across 7 backends differing only in a cache path. Their real bodies collapse to two serialisation schemes, so they became 6 helpers on metile.runtime.cache, verified for round-trip, cache miss, changed config shape, and the METILE_DISABLE_DISK_CACHE path.

Also: 8 compressed-calibration functions, two 91-line group tuners (merged only after normalising both and confirming they were identical), 8 _print_table copies (byte-identical output verified), a duplicated matplotlib helper, and _decode_logits copied from the library into a test.

Clone groups 29 → 25. Dead symbols 0.

Two groups deliberately left

_persistent_key and the *_dispatches family are cache-identity and reporting code. Sharing them would add injection points that silently restore another kernel's tuned config — that failure mode costs more than the duplication.

The test-suite accommodation — worth a look

Splitting the packages broke 30 tests that did monkeypatch.setattr(pkg, "_x", ...), which worked when caller and callee shared one namespace.

The patch targets were not rewritten by hand: 23 of the 44 names are now bound in more than one module, and patching the wrong one leaves a test that passes without exercising anything. tests/module_patching.py replaces every binding instead — also the only correct option for shared mutable state whose writers and readers now sit in different modules. It is mutation-tested: degrading it to package-only patching reproduces exactly the failures it fixes.

Also in here

  • validate_pass_order is wired into the pass pipeline against the passes that actually ran. Doing so surfaced that one recorded constraint was backwardsdouble_buffer_k_loop always runs and split_k_loop is its fallback, so any f16 GEMM over the 30KB threadgroup budget violated it. Corrected, with tests/test_pass_order.py covering it and a source-level guard against a pass silently bypassing the recorder.
  • backfill_precision_labels.py deleted — a one-time migration, verified complete (--check reports nothing to write, all 8 result files carry labels, nothing imports it).

One pre-existing issue, not from this branch

test_agx_isa.py::test_nopping_an_instruction_removes_exactly_its_effect is flaky — it failed twice during this work and passed on re-run and on a clean checkout of main. Worth chasing separately.

Deviation from plan

No compatibility shim for the old top-level import kernels. A shim would reinstall the generic name and defeat the reason for moving it; at version 0.0.1 there are no external users. import kernels now fails cleanly.

🤖 Generated with Claude Code

…te the code that was repeated

The largest file was 4,963 lines and the integration module had 98 top-level functions with no
section markers. Splitting those by concern is the obvious move and it is also the wrong one: the
first attempt at mlx_lm produced eleven import cycles. The symbol graph says why it should have
worked -- 107 symbols in 100 strongly-connected components, the largest of size two, so the module
is very nearly a DAG and a clean split has to exist. The partition was cutting across its layering.

What the layering actually is: _mlx_lm_plan_key calls apply_metile_to_mlx_lm, because tuning works
by applying a configuration and measuring it. apply is therefore a mid-layer primitive rather than
the entry point everything else sits above, and once it is placed there the assignment comes out
with zero ordering violations. Each of the four splits was solved the same way -- build the graph,
find the strongly-connected components that must stay together, then run a fixpoint that pushes
callers above callees -- rather than chosen by name.

Import paths are unchanged. Every split module re-exports what it used to expose, including the
private names the tests reach for.

The duplication that came out was mostly one shape: eighteen _read_config/_write_config pairs
across seven backends, differing only in a cache path. Their real bodies collapse to two
serialisation schemes, so they became six helpers on metile.runtime.cache, verified for round trip,
cache miss, changed config shape, and the disable-cache environment variable. Eight compressed
calibration functions and two ninety-one line group tuners went the same way, the tuners only after
normalising both and confirming they were identical.

Two clone groups were left alone. The _persistent_key functions and the *_dispatches family are
cache identity and reporting code, where sharing would add injection points that silently restore
another kernel's tuned configuration. That failure mode costs more than the duplication does.

Splitting the packages broke thirty tests that patched a name on the module and relied on caller
and callee sharing one namespace. The patch targets were not rewritten by hand: twenty-three of the
forty-four names are bound in more than one module now, and patching the wrong one leaves a test
that passes without exercising anything. tests/module_patching.py replaces every binding instead,
which is also the only correct thing to do for shared mutable state whose writers and readers now
sit in different modules.

Also: kernels/ moves under metile/ so installing the package no longer claims the top-level
kernels name; validate_pass_order is wired into the pass pipeline and its stale constraint
corrected; the spent precision-label backfill is deleted.

Largest file 4,963 -> 1,604. Clone groups 29 -> 25. Dead symbols 0. 738 passed, 57 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@AndreSlavescu
AndreSlavescu merged commit 18671c7 into main Aug 30, 2026
4 checks passed
@AndreSlavescu
AndreSlavescu deleted the cleanup/structure-and-duplication branch August 30, 2026 16:18
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