Upstream releases¶
gmlx carries mlx-vlm 0.7.3 and mlx-lm 0.31.3 whole, in
gmlx/_vendor/mlx_vlm and gmlx/_vendor/mlx_lm, and imports those copies,
never an installed mlx-vlm or mlx-lm. The copy is gmlx code. A new upstream
release changes nothing in gmlx until a port takes one of its changes, by
decision and with a test. mlx stays an installed dependency, held to a
version floor.
The copy¶
Each copied file is upstream's, with only its module references rewritten
from mlx_vlm and mlx_lm to gmlx._vendor.mlx_vlm and
gmlx._vendor.mlx_lm. scripts/vendor_upstream.py made the copy and does
the rewrite. gmlx/_vendor/manifest.json lists every file that upstream
shipped, with its sha256 as shipped and a status:
| Status | Meaning |
|---|---|
verbatim |
The file is upstream's bytes. |
codemod |
The file is upstream's bytes after the rewrite. |
edited |
The file was changed by hand, with a # gmlx: comment at each change. |
alias |
The file is replaced by a module that aliases an owned gmlx module. |
excluded |
The file is left out of the tree. |
license |
The file is the package's LICENSE, as upstream shipped it. |
A bug in the copy is fixed in place, like any gmlx bug. The fix carries a
# gmlx: comment and a test that fails without it, and
python scripts/vendor_upstream.py manifest records the new file. Code
that gmlx owns outright replaces its copy. The KV cache classes live in
gmlx/cache/kv_cache.py, and the attention mask and SDPA helpers in
gmlx/cache/attention.py. The copied mlx-vlm cache module is an alias of
gmlx/cache/kv_cache.py. The copied mlx-lm cache module and both copied
models/base.py files are edited to import the owned classes and helpers
in place of their own. An owned model has a row in gmlx/models/owned.py,
which the copied loaders read before their own model packages.
A line that must name a stock module, such as a reader of the module names
an earlier release recorded, keeps its names with a # gmlx: stock-name
comment. Only the files that STOCK_NAME_FILES in the tool lists may carry
it.
Checks¶
| Check | Where | What it does |
|---|---|---|
| Copy check | vendor_upstream.py rewrite --check and verify in the lint job, tests/test_vendor_manifest.py |
Every module reference names the copy, and the tree matches the manifest. |
| Copy lint | vendor_upstream.py lint in the lint job, tests/test_vendor_upstream.py |
Ruff finds no undefined or twice-defined name in the copy beyond scripts/vendor_ruff_baseline.json, which is empty. |
| Stock imports | tests/stock_blocker.py, tests/test_no_stock_imports.py |
The tests refuse a stock import, and a child with poisoned stock packages runs gmlx without reaching one. |
| Stock installs | The last step of the macOS CI job | gmlx with all its extras installs no mlx-vlm, mlx-lm or mlx-embeddings, and the copy's version files name the releases the manifest records. |
| Claims | gmlx/upstream/claims.py and tests/upstream/test_claims.py |
Each name an installer rebinds exists in the copy. After install, its wrap chain holds a wrapper from each module its row lists. |
| mlx floor | check_mlx_version, at CLI entry, and gmlx doctor |
Both refuse an mlx below the floor that the copied mlx-vlm declares. |
| Static calls | pyright, in the macOS CI job |
It type-checks the files in [tool.pyright].include, including their calls into the copy. |
The copy check also fails a copied file whose sha256 differs from the
manifest, a file under the copy that the manifest does not list, and an
edit without its marker. The copy lint leaves line numbers out of its
findings, so an edit above one moves nothing. The poisoned child runs the
CLI, loads, generates and serves. python -m gmlx.upstream.claims
--installed runs the claims check, and gmlx doctor also prints the
installed mlx and the copied versions.
Reading a new release¶
scripts/upstream_canary.sh downloads the wheels of the newest mlx-vlm and
mlx-lm, or of the versions it is given, together with the copied releases,
and runs scripts/drift_report.py on them. Nothing is installed. The
weekly drift job in CI runs it and puts the report in the run summary, and
it never fails the run.
scripts/upstream_canary.sh # the newest releases
scripts/upstream_canary.sh 0.7.4 # an mlx-vlm version, the newest mlx-lm
The report starts with a file summary for each package: the files that
upstream changed, removed or added, and the files that both upstream and
gmlx changed. The summary names every file that gmlx edits, aliases, takes
units from or mirrors, however many other files changed. Unit rows follow
for the files that gmlx edits or imports from, for owned code taken from
an upstream file, and for gmlx code that mirrors upstream units beside the
copy: the Qwen3.5 text models in gmlx/models/qwen35, the Kimi-K3 short
convolution and the APC manager builder. A unit is a function, a method,
the rest of a class or a module assignment. Each row says whether upstream
changed the unit, gmlx did, or both did, from a three-way compare of the
copy, the copied release and the new release. A unit that an edited file
imports from an owned module keeps its row when upstream changes it, and
the row names the owned file. The import rewrite is applied to both
releases first, so it never shows as a change.
scripts/drift_report.py also runs on its own. --new mlx_vlm=DIR and
--old mlx_vlm=DIR take package directories, and --json writes the same
data as JSON.
Porting a change¶
The report is a list to triage, not a gate. Each change goes through three steps:
- Decide. Port an upstream correctness fix when the bug applies to the copy. List performance and structural changes for review, and leave the rest.
- Port. Edit the copied file in place, or the owned module that replaced
it, with a
# gmlx:comment at the change. Add a test that fails without the port. - Record. Run
python scripts/vendor_upstream.py manifestand thenverify, and commit the port with its test and the manifest.
A port never moves the copy to a new release wholesale. The manifest keeps the sha256 of the release each file was copied from, so the next report still compares against it, and a ported unit that matches the new release drops out of the rows.
Upgrading mlx¶
mlx is the one upstream package that gmlx installs. mlx-kquant pins the
exact mlx release that its kernels were built against, so pyproject.toml
gives mlx no version of its own. MLX_FLOOR in gmlx/upstream/versions.py
is the floor that the copied mlx-vlm declares, and
tests/upstream/test_versions.py holds the two equal. These steps move
mlx:
- Take the mlx-kquant release built for the new mlx.
- Run the full test suite and the tests marked
integrationin a scratch venv, as Testing describes. - Run live smoke tests with models on disk. Cover a served MTP model, a
warm prompt-cache hit, an MXFP4 model, a DeepSeek-V4 model and one
gmlx talkturn. - Commit the version bump on its own.
Adding a rebind¶
New behavior in the copy goes in as an in-place edit, marked # gmlx:,
which needs no installer. An installer that rebinds a name in the copy
gets a row in CLAIMS in gmlx/upstream/claims.py in the same change.
The row names the copied module and attribute, the gmlx site that uses it,
and how the name is claimed: as a wrap, an HTTP route, the app or a
subclass. The check fails when the name is missing from the copy, or when
the installers leave the copy's own object bound to it. An installer is
idempotent, and it raises rather than doing nothing when the name it
expects is missing.