Skip to content

release: sync fork and add signed notarized macOS DMG pipeline - #2

Merged
jonathan308 merged 345 commits into
mainfrom
release/signed-dmg
Aug 25, 2026
Merged

jonathan308 merged 345 commits into
mainfrom
release/signed-dmg

Conversation

@jonathan308

Copy link
Copy Markdown
Owner

Summary

  • sync the stale fork default branch from its 0.5.6 base to current upstream 0.6.3rc3
  • add the two-stage signed macOS DMG workflow (secretless build, protected signing/notarization job)
  • pin the updater to the fork release repository and Developer ID team
  • verify hardened runtime, secure timestamps, app + DMG notarization/stapling, Gatekeeper policy, mounted payload, and SHA-256 output
  • create or update only a draft GitHub Release

Validation

  • bash -n scripts/release_macos_dmg.sh
  • plutil -lint on Info.plist and release entitlements
  • 18 focused Swift updater/release tests passed (ReleasesCheckerTests, UpdateInstallerTests)
  • workflow actions are SHA-pinned

Required operator setup after merge

Create the protected macos-release environment and add the five Apple secrets plus APPLE_TEAM_ID documented in docs/release-public-checklist.md. The workflow will remain safely blocked until those values exist.

jundot and others added 30 commits August 13, 2026 15:36
mlx-lm's sharded_load maps every constructed parameter name through model.safetensors.index.json before downloading, but an architecture whose sanitize() fuses weights at load time (glm_moe_dsa's DSA indexer fusing wk and weights_proj, deepseek_v4 stacking expert biases) constructs names the index cannot contain, so distributed loading of a local checkpoint raises 'Pipeline loading is only supported for MLX converted models' even though the normal loader handles it fine. The gate only exists to pick which files to download; when it rejects a local directory the wrapped sharded_load now repeats the load-and-shard tail on the local path, where load_model performs the sanitize fusion exactly as it does for a single process. Installed by the glm_moe_dsa and deepseek_v4 patches, idempotent, and rebinds the server's import-time alias too. Verified that both ranks of a 2-rank GLM-5.2 launch get through the gate and into weight loading; the full serve on one 512 GB Mac then dies to jetsam because two 200 GB shards plus double page cache exceed the machine, which is a placement the production admission planner rejects, so end-to-end engaged-regime serving stays covered by the planned two-Mac validation.
…e copies

A boundary snapshot used to hold the whole prompt cache, so a long conversation settled into dozens of near-duplicate full copies (the last 64 boundaries of a 128k chat differ by 2048 tokens each) and one 128k prefill wrote a triangular sum of bytes. The store now mirrors the local paged SSD policy: a boundary file holds what that boundary added. Plain KVCache members in step with the token stream store only their newest step-sized slab through a KVCacheSegment stand-in, positionally immutable and byte-exact under concatenation, with a layout entry carrying MLA-style zero-width halves that safetensors cannot hold. Non-sliceable members keep their proven full-state serialisation, which for rotating windows and recurrent slots is constant-sized and for the pooling cache remains its only representation. Restoring boundary B assembles the chain of files at step..B; a hole simply hides the deeper boundaries from that rank's vote, and prompts sharing a prefix share their early files like local blocks do. Eviction stays a deterministic LRU, now with an optional byte budget, and orphaned deeper files age out on their own. The GLM-5.2 engaged-regime chain drops from about 6.5 GB cumulative to 1.17 GB linear at 12k tokens and the gap grows quadratically with context. Re-verified everything on the new format: greedy parity on gemma-4-26B, Qwen3.6-27B, DeepSeek-V4-Flash and GLM-5.2 (short and engaged regime), the real-server end-to-end run, and the three 2-rank loopback profiles.
…snapshot-cache

feat(cluster): add SSD boundary snapshot prompt cache for distributed ranks
* feat(scheduler): decode fairness for concurrent prefill (jundot#2031, jundot#2622)

* fix(scheduler): share the prefill hold deadline across engines

---------

Co-authored-by: jundot <jundot@users.noreply.github.com>
Claude Code sends periodic role=system reminders after tool results, which the anthropic adapter surfaces as tool -> system -> assistant runs. The V4 relocator only accepted user-adjacent runs, so these conversations fell back to front-consolidation and every new reminder rewrote the rendered prompt near the system section, invalidating the whole prefix and forcing 100k+ token re-prefills. Relocate such runs as a latest_reminder before the contiguous tool run so the rendered prefix stays byte-stable across requests.
…ocks

The supersede-on-extend lineage assumed the block before the first new block was the previous store's tip, but on a store that diverged from an existing chain it is a shared-prefix interior block; stripping it two stores later permanently broke partial-match walk-back restores. Track real store tips and record lineage only when the predecessor was one. Also, dedup'd blocks were never rewritten, so blocks first stored without snapshot coverage kept placeholder payloads forever; backfill them from the current store's boundary snapshots so walk-back can restore at those boundaries again.
Quantizing a model directly from the Hugging Face cache used the route-safe cache ID as the output name, which produced invalid default repository IDs during upload. Derive the output base from the bare canonical repository name while preserving the full source repository ID for display.
…jundot#2644)

* feat(cache): add GDN sidecar state codecs

Persisted GDN sidecars are dominated by the fp32 recurrent state — the
largest single payload in a full checkpoint. Encode it only at the
serialization boundary (bf16, row-wise int8, and a randomized-Hadamard
int8 variant) and restore to fp32, so live inference never sees reduced
precision. Unknown codecs, malformed metadata and non-finite values fail
closed: a corrupt sidecar becomes a cache miss, not silent garbage.

* feat(cache): add RHT-INT16 GDN sidecar codec

Row-wise int8 keeps the codec cheap but its reconstruction error is still
visible at the per-row scale. Spreading the same quantization across an
int16 payload after a randomized Hadamard rotation drops relative L2 to
~0.0025% — near-lossless while still ~2x smaller than fp32 — which makes it
a safe default. The fixed sign diagonal is deterministic and never touches
the generation RNG.

* feat(cache): add GDN snapshot storage policy

Replace the split boolean with auto|ssd_sidecar|embedded. Reduced sidecars
are namespaced apart from fp32 but still fall back to a legacy fp32 sidecar
at the same endpoint, so an experiment's warm cache never silently reuses
another arm's checkpoint. Admin changes update the scheduler template and
unload models so the selection applies on the next load.
…jundot#2643)

Sync the vendored Muse Glimmer implementation with upstream mlx-vlm numerics and reasoning configuration while preserving oMLX-specific compatibility hooks.
Add an Enhanced Readability preference for web admin pages.

Co-authored-by: LXD-8 <308763019+LXD-8@users.noreply.github.com>
…calls (jundot#2617)

Snapshot/restore the budget processor around speculative token calls in the DSpark verify chain and draft generation, so only emitted tokens count toward the thinking budget.
…ot#2623)

DeepSeek V4-style chat templates can end the rendered prompt with <think>, so initialize Responses streaming from the rendered prompt state. Reuse the existing detection to separate reasoning deltas and restore reasoning token accounting.

Fixes jundot#2584
jundot and others added 28 commits August 25, 2026 01:11
Hoist the memory helpers' imports to the top of the module, fold the headroom check into the snapshot helper, and return directly on a memory skip so the ladder no longer logs a compile failure that never ran.
…ndot#3105)

The single-instance (AneHybridQ4Primitive) and dual (DualAneHybridPrimitive)
ANE dispatch paths retained the producer command buffer and called
model_->begin() outside any try. An exception thrown after begin() but before
the detached evaluation thread was spawned — most realistically from the qmm
device.get_kernel() calls that sit in that window — orphaned the ticket:
submitted_ stayed ahead of completed_ forever, so the next begin() threw
"overlapping evaluations" and the program stayed wedged until the process
restarted (finding C1). The retained command buffer also leaked.

Introduce a small AneDispatchGuard RAII helper that, on unwind between begin()
and the spawn, releases the producer buffer (unless the pack step already
retired it via producer_released()) and cancels the outstanding ticket(s) to
rebalance the counters. begin() itself is wrapped in a narrow try that only
releases the buffer — begin() throws before it increments the counter, so
there is no ticket to cancel there — mirroring the existing fused-path
pattern; the guard then covers the get_kernel window and is disarmed once the
evaluation thread(s) own the ticket. The dual path disarms before the first
spawn so it never double-counts a ticket a thread already owns.

The fused path (AneHybridQ4SwiGLUDownPrimitive) already guards retain+begin
but shares the same residual get_kernel-after-begin gap; it is left unchanged
here and called out for a focused follow-up.


Claude-Session: https://claude.ai/code/session_01GENz3t3tZVc8En54DxbZ9w

Co-authored-by: Alyta Phoenix <alyta@phoenixes.net>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Extend the C1 dispatch guard from jundot#3105 to the fused SwiGLU/down path: construct it right after begin, fold the manual pack-failure rollback into it, and disarm before the evaluation threads take ticket ownership. Closes the same orphan-ticket wedge the single and dual paths already guard.
_prepare_fused_down_for_bank dequantized down_proj's ENTIRE packed
weight to a dense fp32 array (dense_down), but only columns
[0:gpu_start] were ever consumed from it -- down0/down1 and, when
cpu_hidden>0, cpu_down_weight. The GPU portion (columns
[gpu_start:hidden]) reads down.weight directly, still quantized, a
few lines later for state.down_weight/down_scales/down_biases. The
[gpu_start:hidden] suffix of dense_down was therefore computed and
immediately discarded on every fused-down MLP compile -- a pure-waste
transient of roughly (hidden - gpu_start) * out_features * 4 bytes
(~0.5GB/layer at typical Qwen3.5/3.6 MLP dimensions).

Fix: slice down.weight/scales/biases to [:gpu_start] (in packed-axis
terms: gpu_start // 8 for the 4-bit-packed weight, gpu_start // 128
for the group_size=128 scales/biases) before calling mx.dequantize,
instead of after. gpu_start is always a multiple of 128 (per_ane and
cpu_hidden are both rounded down to 128 via the existing `// 128 *
128` pattern), so the packed-axis slice boundaries land exactly on
quantization group boundaries -- the sliced-then-dequantized result is
bit-identical to the old dequantize-then-sliced one, verified directly
against a full-dequant reference in the new correctness test.

Co-authored-by: Alyta Phoenix <alyta@phoenixes.net>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…pressure (jundot#3103)

The compiled ANE procedure banks keep their packed weight blobs mapped for
the model's whole residency (~13 GB for a 27B at mlp 0.35 / gdn 0.45). A
config tuned at a short calibration length therefore competes with the KV
cache at long context: once usage crosses the prefill guard's sizing
target, chunks collapse 2048 -> 512 and a 57K prefill runs ~40-45% slower
than with ANE disabled outright.

The existing headroom ladder cannot recover this memory: idle-model
eviction skips the requesting model, and the pooled-buffer reclaim keeps
'succeeding' marginally on every pass (prefill continuously refills MLX's
buffer cache) while never touching the banks. Three changes:

- release_qwen35_ane_prefill(model): latch every sliced module through the
  existing per-module failure flags (the dispatch sites fall back to stock
  GPU compute and never lazily recompile), drop the state references, zero
  the status counters. The native programs free their mapped blobs when the
  last reference dies; the next load rebuilds them from settings.
- EnginePool ladder: a new last rung sheds the requesting model's banks on
  its own MLX thread (step loop parked, nothing mid-dispatch), and a
  request whose earlier pass already got a reclaim escalates straight to
  the release instead of reclaiming again first.
- Scheduler: allow a second eviction pause per request, so recurring
  pressure can reach the deeper rung after a marginal first-pass reclaim.

Verified on an M5 Pro 64 GB (57K prompt, banks resident 13.95 GB by
footprint delta): under an enforced ceiling the second pause logs the
release, the prefill finishes at full 2048-token chunks, /api/status
reports the shed state, and subsequent requests serve GPU-only. With no
memory pressure the rungs never run and behavior is unchanged.
… terms (jundot#3082)

Chunked-prefill rejections logged one aggregate cap comparison with no way to tell which contributor actually bound. Add a second warning line on every rejection that breaks the admission bound into its separate terms: resident usage, the predicted per-chunk transient, the session observed-max ratchet, and the ANE prefill transient reservation.
After jundot#3103 sheds the banks, drop the load-time ANE I/O surface reservation from the memory monitor so admission stops pausing for memory that no longer exists, promote the shed log to a warning, and report an explicit shed flag in /api/status so it does not read like a load-time compile failure. A fresh enable clears the flag.
…st prefill-memory rejection (jundot#2992)

_with_json_keepalive wraps non-streaming completions in a StreamingResponse
so long prefills don't hit client read-timeouts, but it yielded its first
keepalive byte -- and therefore committed the ASGI response to whatever
status StreamingResponse was built with (always 200, none of the 6 call
sites passed an explicit status_code) -- before knowing whether the
wrapped request would succeed or fail. A memory-guard rejection that
happens quickly (the common case for most prompt sizes) still surfaced a
well-formed error body, but with a 200 status silently telling any client
that only checks the status code that the request succeeded.

Adds _json_response_or_keepalive, which races the wrapped coroutine
against a short grace period. A fast outcome (success or
PrefillMemoryExceededError) returns a plain Response/JSONResponse with
the real status code, bypassing the streaming wrapper entirely -- reusing
the same body-construction helper the synchronous preflight-rejection
path already uses. Only requests still running past the grace period fall
back to the existing keepalive-streaming behavior, which still commits to
200 on any later failure once streaming has begun -- an HTTP/1.1
constraint once headers have shipped, not something this fix can close,
but that only affects the far less common case of a failure minutes into
a long-running request rather than the ordinary fast-reject case this
fixes.

Applies to all 6 previously-affected endpoints: markitdown completions,
embeddings, completions, chat completions, Anthropic messages, and the
Responses API -- preserving engine-lease release semantics for the four
that hold one.


Claude-Session: https://claude.ai/code/session_01GENz3t3tZVc8En54DxbZ9w

Co-authored-by: Alyta Phoenix <alyta@phoenixes.net>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…reaming paths (jundot#3060)

The streaming generators' blanket except is the innermost handler, so a
PrefillMemoryExceededError raised during prefill was flattened to a
generic server_error before the correct handler in _with_sse_keepalive
could run (jundot#3036) - the structured body (root type, omlx_code,
estimated_bytes, limit_bytes) was built by unreachable code.

Route the classification through the generators themselves: the OpenAI
chat and completions generators share a _streaming_error_payload()
helper that keeps the structured body for guard rejections and the
exact previous behavior for everything else. The same shadowing existed
on the two paths the issue left unchecked: /v1/messages now emits an
Anthropic-native invalid_request_error event with the guard's detailed
message, and /v1/responses now carries {code, message} in the
response.failed error object instead of failing with no error at all.

Regression tests pin the exceeded/aborted/generic payloads.
…iler is unavailable (jundot#3067)

* fix(ane-tuner): return a completed GPU-only verdict when the ANE compiler is unavailable

On a machine without the private ANE runtime (or a build without the
procedure-bank compiler), the tuner previously discovered the gap only
deep inside the bank-split ladder: every width failed with the same
'compiler is unavailable' error, the run ended in status error, and the
model was left unloaded (jundot#3044, M2 Ultra). The serve path has always
handled the same condition gracefully by skipping ANE.

Probe availability at the top of run_tuning, before anything is pinned
or unloaded, and return a completed run recommending GPU-only: on such
a machine that IS the tuning answer, not an error. The probe is shared
with qwen35_ane_compile_linear_bank's own gate so the two cannot
drift.

* test(ane-tuner): stub the compiler preflight in mocked pipeline tests

The new run_tuning preflight consults the real probes, which return False on runners without the extension and short-circuit every mocked pipeline test to a GPU-only verdict. Pretend the compiler exists in the shared fixture; the unavailable-path tests re-stub the probes and the parity test pins the real implementation explicitly.

---------

Co-authored-by: jundot <jundot@users.noreply.github.com>
The jundot#3067 preflight verdict ships processing_tps/speedup_percent as null for the first time; the mac app DTO declared them non-optional, so the status decode threw on exactly the machines the preflight serves. Make both fields optional and have the mac and web recommendation cards drop the tok/s segment when nothing was measured.
…ble-specprefill

fix(ane): disable SpecPrefill during ANE split tuning
* perf(ane): reuse compiled Qwen programs across loads

Opt-in with OMLX_QWEN35_ANE_COMPILE_CACHE=1. Uses stable per-OS model URLs and compiledModelExists to reuse Apple's AOT programs, with cross-process locking, fail-open temp compilation, and one corrupt-hit recompile fallback. Covers individual linear, fused SwiGLU/down, and procedure-bank programs.\n\nMeasured fresh-process load reductions: M1 Ultra 65.17s -> 22.42s, M2 Max 55.74s -> 20.08s, M1 Max 54.37s -> 25.55s. Output parity was byte-identical; oMLX persistent data footprint is zero bytes (lock files only).

* perf(ane): bound compile-cache lock acquisition and harden staging delete

flock(LOCK_EX) blocked forever when a suspended process held the entry
lock, hanging every later load of that identifier. Take the lock
non-blocking with a 30s deadline and fail open to the temp path on
timeout. Resolve symlinks before the cache-root prefix check so a symlink
under the cache root cannot redirect the staging delete outside it.

* test(ane): assert bounded lock acquisition and symlink-resolved staging delete

---------

Co-authored-by: jundot <jundot@users.noreply.github.com>
The reuse landed in jundot#2975 behind OMLX_QWEN35_ANE_COMPILE_CACHE, which nobody discovers. Add a toggle in the advanced cache group that exports the env var before the first ANE compile, so it applies on the next restart, and keep it off by default. An explicit env var still wins on the serve path.
A staging path under the cache root that resolves outside it fell through to the unlocked delete, which followed the symlink out of the cache. Skip those instead, and drop the entry's 0-byte lock file along with the entry it guards so one file per compiled procedure does not accumulate.
…hing

A gdn_fraction under the bank floor makes _prepare_gdn_for_bank reject every layer, and the only symptom was gdn_layers=0, which reads the same as a compile failure. Warn at enable time with the model's actual floor, and move the floor rule next to the bank code so the tuner's grid clamp and this warning share one implementation.

Based on jundot#2905 by finaltv971.
Bring README.ko.md up to date with README.md. Sections and assets now
match 1:1 (32 headings, 11 images, 20 code blocks, 22 table rows) and
every shell command is identical to the English version.

Added (missing entirely):
- "실험적 멀티 Mac 추론" section
- Native custom kernel note blockquote (from jundot#2209)
- --hf-endpoint example under CLI 설정
- MTPLX, mlx-serve, SiliconScope acknowledgments

Corrected (stale vs English):
- CLI memory flags: --max-model-memory / --max-process-memory are no
  longer registered in omlx/cli.py; replaced with --memory-guard and
  --memory-guard-gb
- Requirements: Python 3.10+ -> 3.11-3.13, added macOS 15.0+ (Sequoia)
- brew install omlx -> brew install jundot/omlx/omlx (both occurrences)
- Tool calling streaming described the opposite behavior (buffer-then-
  send); now incremental emission with tool-call markup suppressed
- Sparkle-based updater -> built-in auto-update
- Custom kernels marked optional -> strongly recommended, added Qwen3.5
- Admin dashboard language list extended to match English
- Fixed a stray space in the built-in chat paragraph

Note: the python badge line duplicates a change in the stalled draft
jundot#2574; this version also carries macOS 15.0+ and keeps M5 from jundot#2925.

Other translations are intentionally untouched and left for contributor
review.
@jonathan308
jonathan308 merged commit cde134f into main Aug 25, 2026
3 checks passed
@jonathan308
jonathan308 deleted the release/signed-dmg branch September 13, 2026 00:25
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.