Skip to content

Merge upstream main (#603 model cache extraction) - #68

Merged
solderzzc merged 4 commits into
mainfrom
sync/upstream-603
Sep 28, 2026
Merged

solderzzc merged 4 commits into
mainfrom
sync/upstream-603

Conversation

@solderzzc

Copy link
Copy Markdown
Member

Proposed changes

Merges ml-explore/mlx-swift-lm main (ee673d6) into our main with a real merge commit, so later upstream merges start from a shared base.

Upstream brings two commits:

The merge had no conflicts. MLXLanguageModel.swift is the only file both sides changed, and our case .chunk(let text, _): (for Generation.chunk(String, tokenId:)) is kept. SwiftLM does not use MLXFoundationModels.

Testing

  • Full xcodebuild test (serial): 1,702 tests, 11 failures. All 11 also fail on our main (7cc37a0), so none come from this merge. Most are numeric tolerances. One is a WiredMemory "Ticket not active" assertion that looks like the bug upstream mlx-swift#471 fixes.
  • SwiftLM swift build -c release and --build-tests succeed with this commit as the submodule.
  • swift-format 6.2.3 lint on the 14 changed files is clean.
  • Gap: all of MLXFoundationModels is behind canImport(FoundationModels, _version: 2), which needs the macOS 27 SDK. So the Extract the model cache and stop whole-cache eviction abandoning an in-flight load ml-explore/mlx-swift-lm#603 code and its new tests (ModelCacheEvictionTests, MLXDownloadProgressTests) did not compile or run here. ModelCache.swift and MLXDownloadProgress.swift were type-checked separately against our modules, with the gate removed.

Found during review (not fixed here)

  • Pre-existing, ours: several IntegrationTesting files still bind case .chunk(let text):, for example GuidedGenerationBenchmarkTests.swift:427, Gemma4EseriesLoadIntegrationTests.swift:60, StaleWeightIndexIntegrationTests.swift:52 and MTPIteratorEndToEndDiagnosticTests.swift. That does not compile against our two-value Generation.chunk. CI never builds that project. Separate fix.
  • Upstream Extract the model cache and stop whole-cache eviction abandoning an in-flight load ml-explore/mlx-swift-lm#603: a cancelled load's late .started can overwrite a newer load's progress entry for the same model, which hides the newer load's download progress. To be reported upstream.

Checklist

  • I have read the CONTRIBUTING document
  • I have run pre-commit run --all-files to format my code / installed pre-commit prior to committing changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the necessary documentation (if needed)

AI usage

  • I have read this PR description in full and approve it as my own, and it
    accurately describes the code changes.
  • AI usage disclosure: Claude Code (Opus 5.5) did the merge, ran the tests and the review, and wrote this description.

🤖 Generated with Claude Code

negativetime and others added 4 commits September 15, 2026 15:26
TokenIterator.next() cleared the buffer cache when tokenCount % 256 == 0,
but checked after incrementing tokenCount, so the first clear came at
token 256. mlx-lm checks before incrementing and also clears on the first
token, right after prefill. A generation shorter than 256 tokens never
cleared, so repeated short requests grew the cache without bound.

Co-authored-by: Joshua Langberg <negativetime@me.com>
…n-flight load (ml-explore#603)

* Move the model cache into its own file

- Moves the constraint kind, the tokenizer bias, and the cache actor out of the model file, changing only the access the cross-file use requires: `private actor ModelCache` becomes `actor ModelCache`, because a `private` top-level type is visible only inside its own file. No other content changes.

The model cache grew large enough inside MLXLanguageModel.swift to warrant its own file, separate from the model's own responsibilities.

* Catch only the fork-unavailable error when a constraint clone fails

- Narrows the freshly-compiled constraint's clone attempt from a blanket `try?` to a catch on `GrammarError.forkFailed`, matching the cached-template clone site above it, which already narrowed this way. Every other clone failure now reaches the caller instead of being silently treated as the expected one.

A blanket `try?` treated every clone failure as the one expected case, xgrammar lacking `Fork()`. `GrammarConstraint.clone()` has exactly one throw site today, so this narrowing changes no observable behavior yet; it only stops a future second failure mode from being swallowed the same way.

* Cancel every registered load from evictAll()

- Cancels every registered load task, which remove(modelID:) already did for one model.
- Corrects the eviction documentation, which claimed an in-flight load completes.
- Adds a test that evictAll() cancels an in-flight load, and the same probe against evict().
- Bounds both new tests with a time limit, because each parks its loader and a regression would hang.

evictAll() dropped every load registration without cancelling, so nothing in the process could reach a load that was still running. A caller awaiting preload() or respond() now fails, and callers coalesced onto one load fail together. Cancellation is cooperative. A load already inside loadWeights runs to completion and allocates, and that limit applies to evict() too.

* Rename the grammar tokenizer members away from an xgrammar abbreviation

- Renames the grammar tokenizer members and their storage away from an abbreviation of the library name: `xgTokenizers` becomes `grammarTokenizers`, `makeXGTokenizer` becomes `makeGrammarTokenizer`, and `hasCachedXGTokenizer` becomes `hasCachedGrammarTokenizer`, at every call site.
- Renames the same local variables at every call site: `xgTokenizer` and `xgTok` become `grammarTokenizer`, and the `vocab` local inside `makeGrammarTokenizer` becomes `vocabulary` to avoid shadowing the `vocab:` parameter label once its neighbor is renamed.

The cache's tokenizer members were named after xgrammar's own abbreviation rather than the type they store, which the style guide's initialism rule flags.

* Name the download progress throughput property in full

- Renames the observable throughput property to throughputBytesPerSecond, dropping the abbreviation.
- Drops public from the two reporting entry points, leaving the observable state and the shared instance public.

Every caller of the two entry points is in this module, and the tests reach them through @testable import, so neither needs to be public. No release contains this type: the newest tag is 3.31.4 from June, and the whole MLXFoundationModels library arrived in July, so no consumer can depend on either name.

* Track download progress for each model

- Keys the progress state on the model ID, with one published value per model and accessors that add every entry.
- Reports the end of a load from a defer, so a failed or cancelled load clears its own entry.
- Sends every report through one stream and one main-actor consumer, which applies them in the order the loader sent them.
- Gives every load its own identifier, so a report from a cancelled load cannot change the load that replaced it.
- Removes isActive and modelName, and adds activeDownloads and download(forModelID:).

One fraction and one identifier cannot describe two loads, and the cache loads models concurrently, so two downloads overwrote each other's state. reportCompleted() ran only after a successful load, so a cancelled load left the state active for the life of the process. Each report also created its own main-actor task, and those tasks run in no fixed order, so a progress report could apply after the end and leave a finished download on screen. Eviction cancels a load and forgets it at once, so a replacement can start while the cancelled load still transfers bytes, and both shared one entry.

No release contains this type, so none of these changes can break a consumer.
@solderzzc
solderzzc merged commit eb586d2 into main Sep 28, 2026
7 checks passed
solderzzc added a commit to SharpAI/SwiftLM that referenced this pull request Sep 29, 2026
Picks up SharpAI/mlx-swift-lm#72: rejected MTP drafts are rolled back
layer by layer, including sliding-window layers after the ring wraps
(#184). The range also has SharpAI/mlx-swift-lm#68 (upstream #603 sync,
not used by SwiftLM).

README: replace the "avoid --mtp" warning with the measured result.
Rollback is exact; the remaining differences from plain decoding are
batch-shape bf16 numerics and the text-only vs vision model path.

Fixes #184

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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.

4 participants