Skip to content

🔄 Auto-Sync: Apple Upstream Repository - #62

Open
solderzzc wants to merge 235 commits into
mainfrom
sync/upstream-latest
Open

🔄 Auto-Sync: Apple Upstream Repository#62
solderzzc wants to merge 235 commits into
mainfrom
sync/upstream-latest

Conversation

@solderzzc

Copy link
Copy Markdown
Member

Automated PR to synchronize SharpAI fork with Apple mlx-swift-lm upstream repository. Please review merge conflicts (if any) and verify CI pipelines.

Opened manually — the workflow's own gh pr create step has been failing silently every day since this branch started diverging (Resource not accessible by personal access token), so this backlog (229 commits) was never surfaced for review. SWIFTLM_PR_TOKEN needs its permissions fixed before the daily workflow can do this on its own again.

🤖 Generated with Claude Code

alankessler and others added 30 commits April 15, 2026 12:56
BERT models crash when input exceeds maxPositionEmbeddings because
the position embedding table is fixed-size. Truncate with a warning
rather than crashing.

Expose maxPositionEmbeddings on the EmbeddingModel protocol (default
nil, non-breaking) so callers can check the limit and pre-truncate
or chunk as needed.

Fixes #62.
* Bump swift-syntax dependency
* Update and fix usage examples
ml-explore#149)

Qwen35Language.LanguageModel.callAsFunction assumes inputs is always 2D
[batch, seq], but text-only callers like WiredMemoryUtils.tune and
TokenIterator can pass 1D [seq] token arrays. This causes
getRopeIndex() and subsequent dim(1) calls to crash with
"SmallVector out of range" when accessing a non-existent dimension.

Add an ndim check at the top of callAsFunction to expand 1D inputs
to 2D before any dimension-dependent logic runs.

Fixes ml-explore#148
* Add coherence integration tests

* Consolidate task registration
…xplore#168) (ml-explore#170)

`TokenRing.loadPrompt` used `prompt.dim(0)` to count tokens, which
returns 1 for VLM models that pass [1, n]-shaped prompts. This caused
the ring buffer to be incorrectly sized, leading to a broadcast shape
crash on the next `append` call during generation.

Flatten the prompt to 1D upfront so the token count and all downstream
slicing work correctly regardless of input shape.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lore#167)

The tools schema was passed to the chat template (so the model knew
about the tools) but never forwarded to the ToolCallProcessor that
parses the response. Without it, array and other non-string parameter
types in XML tool call formats (Qwen 3.5) were returned as raw strings
instead of being decoded to their proper types.

Fixes ml-explore#159.
…ml-explore#174)

GlmOcr never forwarded tools or additionalContext to applyChatTemplate
(added after ml-explore#140, missed the pattern). SmolVLM2's video path was also
missing them while its other two branches had them.
* fix: use preconcurrency CoreImage import
…ore#164)

feat: add ParoQuant model support
                                                                                                
Load PARO-quantized models (AutoAWQ format) with pairwise Givens rotation applied at runtime via a
Metal kernel.                                                                                    
                   
Key additions:                                                                                    
- RotateQuantizedLinear: Metal kernel for pairwise Givens rotation + quantized matmul. Rotation
state derived once at load time (thread-safe under concurrent inference).                         
- ParoQuantLoader: AutoAWQ→MLX weight conversion, rotation layer patching, fused in_proj_ba
splitting for Mamba projections.                                                                  
- maybeQuantizeKVCache fix: handle hybrid caches (attention + Mamba) where cache[0] isn't a KV    
attention cache.                                                                              
- Unit tests covering pair packing, AWQ conversion, quantization round-trip, and concurrent       
safety.
* Handle stringified JSON tool call arguments
…l-explore#229)

MLXArray(-Float.infinity) creates a float32 scalar. which() then promotes
the entire bf16 segsum output to fp32, doubling memory per SSM layer.

At L=2048 this wastes ~960MB on Qwen3.5-35B (30 GDN layers, 4 heads)
and ~24GB on Nemotron-30B (48 Mamba layers, 64 heads) per prefill.

Fix: match the -inf scalar to the accumulator dtype.
…-explore#193)

* feat: expose speculative decoding in ChatSession (ml-explore#181)
* feat: add generateTask overload for SpeculativeTokenIterator

Add SpeculativeDecodingConfig struct and wire SpeculativeTokenIterator
into ChatSession.streamMap, enabling ~2-3x generation speedup with
no API break for existing callers.

Exposes a public generateTask(iterator: consuming SpeculativeTokenIterator)
overload so that ChatSession can obtain the (stream, task) pair needed
for clean early-termination handling — matching the pattern used by
the existing TokenIterator overload.

---------

Co-authored-by: David Koski <46639364+davidkoski@users.noreply.github.com>
…ore#223)

Fix EmbeddingGemma weight loading (`sanitize(weights:)`)

Two bugs prevented loading any `mlx-community/embeddinggemma-*` checkpoint:

1. **Init-order crash** — `sanitize(weights:)` assigned the dense head via `self._dense.wrappedValue = ...`, which fatals when `module != nil`. Route through `update(modules:)` instead.

2. **Wrong hidden size** — the dense head was initialized with `config.intermediateSize` (the backbone MLP dim), but the actual checkpoint expands 4× to `hiddenSize * 4`. Read the dim directly from `dense.0.weight` shape instead.

---
Co-authored-by: Anatoly Samoilenko anatoly.samoilenko@gmail.com
Co-authored-by: David Koski dkoski@apple.com
…ustion (ml-explore#226)

CIContext() default options cache IOSurface-backed GPU textures for intermediate
filter results. Each frame in the VLM image pipeline (tone-curve → bicubic
resample → color-matrix normalize) allocates multiple IOSurfaces that are held
in the context cache across calls.

Processing a large library (hundreds of videos, each yielding several frames)
accumulates thousands of cached surfaces and hits the macOS per-process
IOSurface kernel limit of 16384, causing render failures:

  IOSurface creation failed: e00002be (likely per client IOSurface limit of
  16384 reached)
  -[CIContext _startTaskToRender:...] Render failed because of failure to
  allocate intermediate.

Fix: pass `.cacheIntermediates: false` when constructing the shared context.
Batch inference never re-renders the same CIImage twice, so the intermediate
cache provides no benefit. The change eliminates the surface accumulation while
leaving all functional behavior intact.

Co-authored-by: Vladimir <vladimir@sinitcin.com>
…plore#228)

Softmax was applied to all 128 expert scores before top-k selection, so
selected weights came from the full distribution rather than just the
top-k. Moved softmax to after selection so it operates only on the chosen
experts. Drops the renormalization step since softmax on top-k already
produces a valid distribution.

Also fuses norm + scale from 3 dispatches into one MLXFast.rmsNorm call.
…l-explore#227)

SwitchGLU variant for models that ship a single fused gate_up_proj weight
of shape [numExperts, 2*hiddenDims, inputDims] instead of separate
gate_proj / up_proj. Gemma 4 26B MoE uses this layout.

One gatherMM dispatch for the combined projection, split, then activate.
Same gatherSort optimization as SwitchGLU.
) (ml-explore#253)

Fix UserInput init not populating self.images/self.videos (ml-explore#182)

Property observers don't fire during initialization, so prompt.didSet — which keeps self.images/self.videos in sync — was never called by two of the inits:

init(prompt: String, images:videos:tools:additionalContext:) built a .chat prompt from its parameters but never copied images/videos into self.
init(prompt: Prompt, images:videos:...) had an explicit case .chat: break, silently dropping the parameters.
Both inits now mirror init(chat:processing:tools:additionalContext:): extract images and videos from the chat messages, or use the explicit parameters for non-chat prompts.

Add regression tests for UserInput init image/video sync (ml-explore#182)

Three tests pinning the contract that self.images/self.videos are populated at construction, not deferred to the next prompt assignment:

testInitFromPromptStringPopulatesImages — string-based init reflects the images parameter immediately.
testInitFromPromptEnumPopulatesImagesForChat — the .chat branch of the Prompt-based init derives images from chat messages rather than dropping them.
testInitFromPromptStringPopulatesVideos — symmetric coverage for the videos parameter.
…re#225)

* pipeline prefill chunks with asyncEval -- 10x on GDN models

eval(cache) between prefill chunks was a blocking sync -- CPU idle while
GPU worked, then built next chunk serially. Python mlx-lm avoids this
because eval is deferred until a value is read.

asyncEval on cache state per chunk lets CPU build ahead while GPU
pipelines through. One terminal eval(cache) after the loop.

Prefill throughput (tok/s), Qwen3.6-35B-A3B-4bit, M5 Max 128GB:

  ctx  | before | after  | speedup
  128  | 260    | 696    | 2.7x
  512  | 235    | 2201   | 9.4x
  1k   | 244    | 3130   | 12.8x
  2k   | 270    | 3937   | 14.6x

No regression on dense models (Gemma 4 E2B, Llama 3.2 3B).
Decode unchanged -- different code path.
…lore#224)

* fix gated delta state precision -- fp32 state to match Python

State was typed as q.dtype (bf16), losing precision across T-step
recurrence. Python mlx-lm keeps state in fp32. Aligns Swift with that.

- kernel source: write o_state as StT not InT
- dispatch: add StT template, use state.dtype for output
- gatedDeltaUpdate: create state in fp32, upcast if needed

Before: kernel showed ~0.25 max diff vs ops fallback at T>1.
After: matches ops path.

Tested on M5 Max 128GB, Qwen3.6-35B-A3B-4bit.

Co-authored-by: tturney <tturney@psyguard.ai>
Removed the command to show SDK build version during the build process. This was informative, but ultimately causing problems with installations and not actually guarding anything.
* models should not mutate state during eval

- fixes ml-explore#157

* sync gptoss with mlx-lm -- no missing sinks
)

Fix Gemma4TextBackbone.callAsFunction crash on 1D token input

When callers construct LMInput directly (e.g. for manual KV-cache reuse), tokens arrive as 1D (L,) instead of 2D (1, L). This causes processedPerLayerInputs to be 3D, and the 4D subscript on finalPerLayerInputs crashes in mlx_array_dim. The first request succeeds because autoregressive step adds .newAxis; the crash hits on the initial prefill of continuation requests.

Fix: expand 1D inputs / 2D inputsEmbeds to add a leading batch dim at the top of callAsFunction. Zero-copy reshape; no behavior change for callers already passing the canonical 2D shape.
Fix Gemma4VisionPooler kernel derivation to use padded sequence length

Previously, the kernel was derived from the real patch count, which could yield kernel=2 at the 280-token budget instead of the expected 3, causing real patches to map to zero rows in the einsum output.

Extract gemma4VisionPoolingKernel(paddedPatchCount:outputLength:) and derive the kernel from pooledHiddenStates.dim(1) to consistently return pool=3 across all supported budgets. Add regression tests for all five budgets {70, 140, 280, 560, 1120}.
aleroot and others added 15 commits August 27, 2026 17:10
* Optimize Qwen MoE expert reduction

* Enable Qwen direct expert reduction by default
* Fuse Qwen GDN input projections
* Move inference preparation to LanguageModel lifecycle
* Use prepare overload for inference lifecycle
…l-explore#594)

* Stop calling deprecated MLX memory APIs from the integration tests

MLX moved its memory inspection and cache control onto a Memory type and deprecated the older GPU spellings. The integration tests still used the old names, so every build of the IntegrationTesting scheme printed a dozen deprecation warnings that buried the warnings actually worth reading.

Switch the call sites over to Memory.snapshot() and Memory.clearCache(), and fix a doc comment that pointed at the old name. The deprecated calls just forward to the new ones, so behavior is unchanged.

* Move Gemma's video frame handling off the deprecated VideoFrame API

Gemma was the last video model still building frames through the old VideoFrame spellings, which take and hand back a CoreImage image directly. Every other vision model already goes through the newer form that wraps the image in the general-purpose image case, so this was the only source of these deprecation warnings left in the library.

Reading the incoming frame through the supported accessor also removes a force-try: the deprecated property traps when the frame is not backed by a CoreImage image, whereas the surrounding frame-processing closure is already allowed to throw, so the failure now surfaces to the caller instead of killing the process.

* Remove a leftover unused local from Gemma's text backbone

An earlier fix to Gemma's cross-layer key/value sharing stopped consulting whether the caller supplied a cache when deciding which layers reuse an earlier layer's keys and values. That fix removed both readers of the local variable holding that answer but left the variable itself behind, so the compiler has been reporting it as an unused value ever since.

Drop the declaration. Its initializer only tested an optional for nil, so nothing observable changes.

* Clear the remaining compiler warnings from the package

With the deprecation warnings fixed, the build still reported a handful of style diagnostics. Most were `var` bindings that are only mutated through subscript assignment or a copying update, which `let` already permits, and `try`/`await` markers on expressions whose callees turned out to be synchronous. Breakpoints of note: the optional-key-path mock in the chat-session tests calls a synchronous prepare overload, and the reranker test used a conditional cast to an existential to check a protocol conformance that is statically guaranteed; the coercion form still fails the build if that conformance is ever removed, but no longer warns.

The one warning needing care was ParoQuant's concurrency test, which captures a non-Sendable layer in the @sendable closure of DispatchQueue.concurrentPerform. Unsynchronized access is the whole point of that test, so the layer now crosses the boundary in a small unchecked-Sendable wrapper, alongside the existing thread-safe accumulator in the same file.
* Add the CI state label module with the run-start rule

A pull request whose CI run has just started carries ci-running, and loses
the labels that described the previous run. An approval by a maintainer
survives the start, because the completion rule needs to see it.

* Turn a finished CI run into one pull request label

A green run asks for review, or reports the pull request ready to merge when
a maintainer has already approved it. A red run names what the author must
fix, and only calls for formatting work when formatting is the single thing
that failed. A run that failed on the build image, or that was cancelled,
asks for another run instead of blaming the author.

* Put a pull request back in the CI queue when a new commit arrives

Every CI result and every approval describes the commit that has just been
replaced, so all of them go. The pull request is only queued again once a
scan has read it, which its category label proves.

* Read the run and write the labels through an injected client

The three functions that talk to GitHub take the client as an argument, so
the tests inject a fake and none of them reaches the network. Finding the
pull request from the head commit is what makes this work for a fork, whose
run payload may carry no pull request at all.

* Choose the pull request the run actually tested, and test the requests

A commit can belong to more than one open pull request when branches are
stacked, so the head commit now decides which one the label describes.
The fake client also records what each read asks GitHub for, so a wrong
run number can no longer pass the tests and fail only in a live run.

* Mark the build-image steps and run the label classifier's tests

Three steps check the state of the build machine rather than the change, so
their names now say so. A failure in one of them asks for another run instead
of asking the author for a fix. A new job runs the classifier's own tests, so
a change to it cannot merge untested.

* Label a pull request while CI runs and when it finishes

A pull request shows ci-running from the moment its run starts, and gets one
label describing the result when the run ends. Both handlers ignore a run
whose commit the pull request has already left, so nobody reviews code that
has been replaced and no newer commit loses its place in the queue.

* Requeue a pull request for CI when a new commit arrives

Every label describing the previous commit goes, and the pull request waits
for CI again. A pull request no scan has read is cleared but not queued, so
new code never reaches the build machine before it has been read.

* Ignore a CI run that never started, and stop pinning a stale base commit

A run held for approval reports a conclusion without having run, so the
completion handler now ignores it rather than clearing the labels a
maintainer set. The reset workflow also takes the default branch instead
of the pull request's base commit, because an older base does not yet
contain the script the job loads.

* Say what the reset workflow's checkout actually takes

The comment still described a pinned base commit after the checkout changed
to the default branch. It describes the security property of a job that
holds a write token, so a stale version of it is worse than none.

* Accept the old build-machine step names, and correct four comments

A pull request from a fork runs its own copy of the CI workflow, so one
branched before the step rename still reports the old names. The classifier
now treats both spellings as a build-machine failure, rather than asking a
contributor to fix a machine they cannot reach. Four comments also said more
than was true: about which files a workflow loads, about what a checkout
takes, about why one clause exists, and about what never cancelling
guarantees.

* Say when the legacy step names can go, without naming another repository

The note pointed at a file a reader of this repository cannot see. It now
states the condition on its own terms: the list can go once no open pull
request predates the rename.

* Copy edits

* Read the CI workflow's contract number from a step name

* Ask for another run when the rules do not match the branch

* Let the contract number replace the list of old step names

* Name the contract number in the CI workflow

* Log the branch's contract number next to the expected one

* Fix two tests that passed for the wrong reason

Both tests check that a failure the author cannot fix gets the needs-ci label. Their sample job data left out the contract step. One test should reach the no-failed-step check, and the other should reach the build-machine check. Both reached the newer contract check instead. With the build-machine check deleted, every test still passed, so nothing guarded that check.

This change adds the contract step to both samples. Each test then reaches needs-ci through its own check again. If you delete the build-machine check now, its test fails.
…ensors (ml-explore#598)

An MLX-converted checkpoint may keep its mtp.* tensors for speculative
decoding. Treating their presence as proof of a raw upstream checkpoint
applies the zero-centered RMSNorm +1 shift a second time, leaving every
layernorm near 2.0 instead of 1.0 and degrading generation to garbage
tokens.

The conv1d layout identifies raw checkpoints on its own: transformers
exports store conv1d as [C, 1, K], and a converter that transposes conv1d
shifts the norms in the same sanitize pass, so the two signals cannot
diverge in a valid checkpoint. MTP-only drafter checkpoints are unaffected
because the mtp. filter removes all their tensors before the shift loop.
…tchGLU (ml-explore#511)

* Allow downstream specialization of Qwen3.5 GDN/MoE blocks and SwitchGLU

`Qwen35Language` has no access modifier, so everything nested in it is
internal regardless of its own modifier, and `GatedDeltaNet`,
`SparseMoeBlock`, `DecoderLayer` and `Model` are additionally `final`.
`SwitchGLU` and `SwitchLinear` are `public` rather than `open`. Together this
makes the Qwen3.5 language stack impossible to specialize from another module.

Serving this model with routed experts streamed from SSD instead of resident
needs exactly one substitution — our own expert MLP in place of
`SparseMoeBlock`'s — while keeping the rest of the vendored model and its
weight keys intact. Without subclassing that becomes a reimplementation:
~445 lines re-deriving the `@ModuleInfo(key:)` bindings, the gather/scatter
sort and the quantized-vs-dense dispatch that `SwitchGLU` already gets right,
and every upstream fix has to be hand-ported.

Widens access only:
  Qwen35Language                                          -> public
  GatedDeltaNet, SparseMoeBlock, DecoderLayer, Model       -> open
  SwitchGLU, SwitchLinear                                  -> open
  SwitchGLU/SwitchLinear/SparseMoeBlock callAsFunction      -> open

`Module` is already `open`, so nothing changes in mlx-swift.

`SparseMoeBlock.callAsFunction` had to widen as a consequence, not a choice:
it satisfies a requirement in the public `UnaryLayer` protocol, so an `open`
class cannot leave it internal. Made `open` rather than `public` since
overriding it is the point.

Leaves FusedGateUpSwitchGLU alone and omits the GDN conv-fold port, which is
a behaviour change and belongs in its own PR.

Full suite: 296 tests, 0 failures.

* public/open updates

---------

Co-authored-by: Fred Jura <digital@afya.ai>
Co-authored-by: David Koski <dkoski@apple.com>
…e#596)

Mirrors the Gemma 3/4 encoder exposures (ml-explore#387, and the Gemma 4 follow-up) for
Falcon-H1, so client code can run the decoder stack over an embedding it built
itself rather than over a single token id.

The motivating case is a DualAR TTS model (Audio8 TTS Preview 0.1b, model_type
`arktts`, `slow_backbone: falcon_h1`) whose slow-stack input is a text embedding
summed with ten codec-codebook embeddings. Such a client never has one token id
to look up, so `FalconH1ModelInner.callAsFunction(_:mask:cache:)` is unreachable
for it, and today the only options are to fork the file or re-implement the mask
setup and layer loop against internal details.

Exposed at @_spi(FalconH1Encoder) scope, keeping this off the advertised public
API of MLXLLM while making it usable outside the module:
  * FalconH1Configuration.hiddenSize / .numHiddenLayers / .rmsNormEps /
    .embeddingMultiplier
  * FalconH1DecoderLayer and its callAsFunction
  * FalconH1ModelInner.init, .embedTokens, .layers, .finalLayerNorm
  * FalconH1ModelInner.callAsFunction(inputsEmbeds:cache:) — new
  * FalconH1ModelInner.callAsFunction(_:mask:cache:) — was internal

`callAsFunction(inputsEmbeds:cache:)` is the seam; the token-id overload now
delegates to it, so the two paths cannot drift. No behaviour changes for
existing callers: FalconH1Model still embeds, runs the stack and applies the
head exactly as before.

`FalconH1ModelInner.init` is exposed because constructing `FalconH1Model` is not
an equivalent way in: the wrapper adds an `lm_head` whenever
`tie_word_embeddings` is false, which is the decoded default. A client bringing
its own head does not want that module and would otherwise have to account for
it when matching module keys against a checkpoint.

`FalconH1Model.model` and the mask helpers (`createSSMMask`,
`createAttentionMask`) were already public, and none of the layer's stored
property types are `private`, so unlike the Gemma 4 change nothing else had to
widen.

FalconH1EncoderAccessTests proves sufficiency the way the Gemma access tests do:
it drives the stack using ONLY the exposed surface, importing MLXLLM without
@testable and opting in via @_spi(FalconH1Encoder). It checks shape,
determinism, that the seam agrees with the token path, that the layer stack can
be driven a layer at a time to tap every hidden state, and that the stack
actually transforms its input — the last so the suite cannot pass on a seam that
returns its argument or runs zero layers.

One test earns its place beyond access checking. `sanitize()` folds
`embedding_multiplier` into `embed_tokens.weight`, which is right when the token
lookup is the only thing entering the stack but is a live trap for a composite-
embedding client: fold it and add unscaled contributions, and the lookup half is
scaled while the rest is not. Measured against the PyTorch reference for the
model above, that is 77% relative error on the embedding and 38% on the final
hidden state — while still producing plausible audio of the right length, so
listening does not catch it. testCompositeEmbeddingMultiplierEquivalence pins
`(raw + other) * m == folded + other * m` and asserts the unscaled-remainder
variant is observably different, both at the embedding and after the stack, so
the wrong form cannot quietly become the expected one.

Runs on a tiny randomly-initialized model; no weights are downloaded.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
- Restore the per-response usage update the adapter sends to FoundationModels. A July fix removed it because the beta SDK declared a call the shipping system library did not provide, and every request crashed the moment generation finished.
- Confirm the mismatch is gone on Xcode 27 beta 6 with the macOS 27 and iOS 27 SDKs. The system library now provides the call, and a real request completes normally.
- Before this change a caller read zero prompt tokens. Completion tokens were already counted from the streamed text, so totals looked plausible and the loss was easy to miss.
- Add integration tests for the usage a caller actually reads: a new session starts at zero, one response reports both prompt and completion tokens, a session sums usage across turns, two sessions stay independent, and the streaming path agrees with its final snapshot.
- Confirm the new tests fail without the restored update and pass with it.
- Note the new tests need a macOS 27 host, so CI does not run them today.
…l-explore#579)

* Suspend instead of blocking cooperative threads while weights load

The Load API is synchronous and blocks its thread on file I/O and
DispatchQueue.concurrentPerform, but every model factory calls it from
an async context, parking a Swift concurrency cooperative thread for
the whole load. Add an async loadWeights overload that hops to a global
queue and suspends the caller, and use it from the factories. The
synchronous overload remains for synchronous callers such as convert.

* Disambiguate loadWeights doc links now that an async overload exists

The new async loadWeights overload made three DocC symbol links
ambiguous, failing the docs verification in CI. Point each link at the
intended overload: the async one (-8b333) from porting.md, which
describes the inference path, and the synchronous one (-6eqw7) from the
model-conversion docs and the async overload's own doc comment.
- In `GenerationEventObserverTests`, two tasks drained the generation channel with an empty `catch {}` block. A read failure there would pass the test silently. Extracts one drain helper. Each test now awaits its result and accepts only a cancellation error.
- Asserts the expected error from every load and warmup task instead of discarding it with `try?`.
- The downloader and tokenizer-loader test stubs were classes with no stored state, marked `@unchecked Sendable`. That annotation tells the compiler to trust the type is safe to share across threads, without the compiler checking it. Converts the stubs to structs, which are `Sendable` by construction, so the compiler proves the guarantee instead of the annotation asserting it.
- `EventBox` and `SeedRecorder` hold mutable state a closure captures across a task boundary, so a struct will not work for them. They used `NSLock` with the same `@unchecked Sendable` escape hatch as the stubs above. Replaces the lock with the standard library's `Mutex`, so these types are provably `Sendable` too, instead of asserting it.
- In `GenerationEventObserverTests`, `withValue` took two closures, one passed as an argument and one as a trailing closure. swift-format's `OnlyOneTrailingClosureArgument` rule flags that form because it is easy to misread which parameter each closure binds to. Spells out the second closure with its argument label instead.
- `EvictBiasStubTokenizerLoader.load()` returns a `CountingTokenizer`, which requires OS 27. The old code checked that at runtime with a `guard` and a `preconditionFailure` that could never actually fire. Marks the stub itself with the OS 27 requirement, so the compiler enforces it and the dead runtime check is gone.

An empty catch and a discarded task result both hid a real loader failure, so a broken load could not fail a test. The fixtures also claimed thread safety by annotation where value semantics or a mutex states it, and asserted an availability requirement at runtime that the type system can enforce at compile time.
…follow-up) (ml-explore#471)

feat(paroquant): MoE architecture support, prepared checkpoint caching, and rotation kernel optimizations

### MoE Support & Architecture
- **PairwiseRotation & RotateSwitchGLU:** Extracted standalone `PairwiseRotation` module and implemented `RotateSwitchGLU` to support MoE PARO models (e.g., Qwen3.6-35B-A3B).
- **MoE Loader Pipeline:** Added passes to stack per-expert AWQ weights, remap shared rotations, and patch switch GLU layers without mutating dense paths.
- **Conversion Robustness:** Dropped theta-filter to convert un-partnered expert weights, match scales/biases to checkpoint float dtype, and optimized prefix matching from $O(N \cdot M)$ to $O(1)$.

### Performance
- **Pre-Gather MoE Rotation:** Moved `gate_up` rotation before expert gather/sort, reducing row-rotation overhead by $1/\text{topK}$.
- **Simdgroup Rotation Kernel:** Rewrote the Metal rotation kernel for `groupSize == 128` to execute in a single simdgroup with `simdgroup_barrier` (with generic fallback for other sizes), delivering up to 2x speedup.
- **GatedDelta Fusion:** Compile-fused the 6-kernel elementwise decay gate chain into a single dispatch.
- **Prepared Checkpoints:** Added background caching of converted weights to `prepared_checkpoint.safetensors` with self-healing validation manifests, cutting warm-load times significantly.

### Fixes & Cleanups
- Unified `RotationDerivedState` and batched GPU evaluation across all rotation modules at load time.
- Froze `PairwiseRotation` parameters to allow fused weighted reduction in `SwitchGLU`.
- Fixed tool-call format and chat convention resolution in `loadParoQuantModel`.
- Fixed volume free-space detection for disk persistence on CI runners.

Refs ml-explore#208, ml-explore#209, ml-explore#220, ml-explore#424, ml-explore#471
…e#605)

- Adds `await` to the two `loadWeights` calls in `Gemma4AssistantDraftModelIntegrationTests.swift`, both inside `async throws` test functions.
- Adds `await` to the `loadWeights` call in `loadTargetAndDrafter` in `MTPIteratorEndToEndDiagnosticTests.swift`.
- Adds `await` to the two `loadWeights` calls in `loadRung4Drafter` in `MTPRung4TokenParityTests.swift`.

ml-explore#579 added an async `loadWeights` overload next to the existing synchronous one. Swift prefers the async overload inside an async function, so a plain `try loadWeights(...)` in an async function now needs `await`. These five calls sit inside async functions but never awaited. None compiled. The package's CI never builds IntegrationTesting. The only job that builds it, `integration_build_xcode27`, is disabled with `if: false`, so ml-explore#579 merged despite breaking this project.
…l-explore#602)

loraLayers returned only selfAttn, so adapters targeting mlp.* failed
LoRAContainer.load with unhandled keys. Gemma3Text/Qwen35/Llama (and the
Gemma4 VLM wrapper) return the decoder layers; match them. Adds a regression
test that loads a synthetic self_attn + mlp adapter onto a tiny Gemma4TextModel.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
  Support differentiation and activation checkpointing in gated-delta recurrence

    • Enable training via ops fallback: The fused Metal CustomKernel lacks a VJP, causing fatal aborts during backward passes (e.g., in Qwen3.5/Qwen3-Next). Added a useKernel flag to gatedDeltaUpdate (defaults to true), passing !training from calling models to fall back to the differentiable ops path, matching Python mlx-lm. Added gradient sanity and numerical parity tests.
    • Chunked activation recomputation: The step-by-step ops graph retains all intermediate states, consuming unviable amounts of memory at scale (~4 MB/step for Qwen3.5-27B). Wrapped recurrence in 16-step chunks via CustomFunction to recompute states during backward passes, mimicking mx.checkpoint.
    • Inference unchanged: Continues using the fused kernel by default.
@github-actions
github-actions Bot force-pushed the sync/upstream-latest branch from ca43b53 to 240b7ea Compare September 10, 2026 09:04
spokvulcan and others added 7 commits September 10, 2026 08:33
…ml-explore#613)

NaiveStreamingDetokenizer.next() measured the common prefix between the
previous decode and the new one in Characters, so a token that appended a
combining scalar (U+FE0F, a zero-width joiner, an accent) to the previous
character re-emitted the whole merged cluster: a ZWJ flag sequence
streamed as its base flag repeated, a quote followed by a variation
selector as two quotes. The prefix is now measured in Unicode scalars,
which compares exactly and keeps the fallback for tokenizers whose decode
is not append-only.

Four tests in StreamingDetokenizerTests stream a ZWJ flag sequence, a
variation selector after an ASCII quote, a combining accent, and a
variation selector after a multi-byte character split across two tokens,
and compare the scalar sequence with the full decode.
* Add configuration-based LoRA metadata discovery
* Share LoRA metadata discovery across LLM and VLM factories
…l-explore#584)

RotatingKVCache: make trim wrap-aware instead of corrupting the ring

Speculative decoding rewinds rejected drafts by trimming, but trimming a wrapped ring previously left dead rows in the logical timeline. Trimming now linearizes the ring to temporal order and discards the newest rows, clamped to the non-pinned span.

• Direct callers (speculative iterators): Accept documented, bounded window-edge loss rather than silent corruption when overwrites have occurred.
• Exact callers (isTrimmable): Retain strict offset + positions < maxSize semantics for staged rounds, restore points, and prompt cache reuse.
• State & layout: Decouple offset == fill by tracking live rows via idx and an explicit wrapped flag (serialized as a 7th metaState value; backward-compatible with 5/6-value legacy states).
• Includes a fix for rotating cache trim following legacy restore.
@github-actions
github-actions Bot force-pushed the sync/upstream-latest branch from 240b7ea to 557f0ca Compare September 11, 2026 09:02
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.