diff --git a/.github/workflows/release-macos-dmg.yml b/.github/workflows/release-macos-dmg.yml new file mode 100644 index 0000000000..8ef115ec73 --- /dev/null +++ b/.github/workflows/release-macos-dmg.yml @@ -0,0 +1,240 @@ +--- +name: Signed macOS DMG + +on: + workflow_dispatch: + inputs: + tag: + description: Existing version tag to build (for example, v0.6.3) + required: true + type: string + +permissions: + contents: read + +concurrency: + group: signed-macos-release + cancel-in-progress: false + +jobs: + build-unsigned-app: + name: Build unsigned arm64 app + runs-on: macos-26 + timeout-minutes: 120 + steps: + - name: Check out the release tag + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ inputs.tag }} + fetch-depth: 0 + + - name: Validate tag and source version + env: + RELEASE_TAG: ${{ inputs.tag }} + run: | + set -euo pipefail + [[ "$RELEASE_TAG" =~ ^v[0-9][0-9A-Za-z.+-]*$ ]] || { + echo "invalid release tag: $RELEASE_TAG" >&2 + exit 1 + } + git show-ref --verify --quiet "refs/tags/$RELEASE_TAG" || { + echo "tag does not exist: $RELEASE_TAG" >&2 + exit 1 + } + version=$(sed -n 's/^__version__ = "\([^"]*\)"$/\1/p' omlx/_version.py) + [[ -n "$version" && "v$version" == "$RELEASE_TAG" ]] || { + echo "tag $RELEASE_TAG does not match omlx version $version" >&2 + exit 1 + } + + - name: Set up Python 3.11 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: "3.11" + + - name: Select release Xcode + run: | + sudo xcode-select --switch /Applications/Xcode_26.6.app/Contents/Developer + xcodebuild -version + + - name: Install host-side packaging tools + run: | + python -m pip install "pip==26.1.2" + python -m pip install \ + "venvstacks==0.7.0" "cmake==4.4.2" "nanobind==2.13.0" \ + "setuptools==80.10.2" "wheel==0.47.0" + + - name: Build staged app with native kernels + env: + OMLX_RELEASE_REPOSITORY: ${{ github.repository }} + run: apps/omlx-mac/Scripts/build.sh release --with-custom-kernel --rebuild-donor + + - name: Confirm arm64 application output + run: | + set -euo pipefail + app=apps/omlx-mac/build/Stage/oMLX.app + test -d "$app" + test "$(lipo -archs "$app/Contents/MacOS/oMLX")" = arm64 + actual_repo=$(/usr/libexec/PlistBuddy -c 'Print :OMLXReleaseRepository' "$app/Contents/Info.plist") + test "$actual_repo" = "${{ github.repository }}" + + - name: Archive app without losing symlinks or extended attributes + run: | + mkdir -p release-input + ditto -c -k --keepParent --sequesterRsrc \ + apps/omlx-mac/build/Stage/oMLX.app release-input/oMLX-unsigned.zip + + - name: Upload unsigned app for the gated signing job + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: unsigned-macos-app + path: release-input/oMLX-unsigned.zip + if-no-files-found: error + retention-days: 1 + + sign-notarize-release: + name: Sign, notarize, and draft release + needs: build-unsigned-app + runs-on: macos-26 + timeout-minutes: 180 + environment: macos-release + permissions: + contents: write + steps: + # Release tooling and entitlements come from the protected default + # branch, while the untrusted/tagged app payload comes from the prior + # secretless job. + - name: Check out trusted release tooling + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.event.repository.default_branch }} + persist-credentials: false + + - name: Select release Xcode + run: | + sudo xcode-select --switch /Applications/Xcode_26.6.app/Contents/Developer + xcodebuild -version + + - name: Download unsigned app + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: unsigned-macos-app + path: release-input + + - name: Extract unsigned app + run: | + mkdir -p release-stage + ditto -x -k release-input/oMLX-unsigned.zip release-stage + test -d release-stage/oMLX.app + + - name: Import Developer ID identity and prepare notary key + env: + APPLE_DEVELOPER_ID_APPLICATION_P12_BASE64: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_P12_BASE64 }} + APPLE_DEVELOPER_ID_APPLICATION_P12_PASSWORD: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_P12_PASSWORD }} + APPLE_NOTARY_API_KEY_P8_BASE64: ${{ secrets.APPLE_NOTARY_API_KEY_P8_BASE64 }} + APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }} + APPLE_NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }} + APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} + run: | + set -euo pipefail + for name in \ + APPLE_DEVELOPER_ID_APPLICATION_P12_BASE64 \ + APPLE_DEVELOPER_ID_APPLICATION_P12_PASSWORD \ + APPLE_NOTARY_API_KEY_P8_BASE64 \ + APPLE_NOTARY_KEY_ID APPLE_NOTARY_ISSUER_ID APPLE_TEAM_ID; do + [[ -n "${!name:-}" ]] || { echo "missing release credential: $name" >&2; exit 1; } + done + [[ "$APPLE_TEAM_ID" =~ ^[A-Z0-9]{10}$ ]] || { echo "invalid APPLE_TEAM_ID" >&2; exit 1; } + + keychain="$RUNNER_TEMP/omlx-release.keychain-db" + p12="$RUNNER_TEMP/developer-id-application.p12" + notary_key="$RUNNER_TEMP/AuthKey_${APPLE_NOTARY_KEY_ID}.p8" + { + echo "RELEASE_KEYCHAIN=$keychain" + echo "RELEASE_NOTARY_KEY=$notary_key" + } >> "$GITHUB_ENV" + keychain_password=$(openssl rand -hex 32) + echo "::add-mask::$keychain_password" + + printf '%s' "$APPLE_DEVELOPER_ID_APPLICATION_P12_BASE64" | /usr/bin/base64 --decode >"$p12" + printf '%s' "$APPLE_NOTARY_API_KEY_P8_BASE64" | /usr/bin/base64 --decode >"$notary_key" + chmod 600 "$p12" "$notary_key" + + security create-keychain -p "$keychain_password" "$keychain" + security set-keychain-settings -lut 21600 "$keychain" + security unlock-keychain -p "$keychain_password" "$keychain" + security import "$p12" -k "$keychain" -f pkcs12 -x \ + -P "$APPLE_DEVELOPER_ID_APPLICATION_P12_PASSWORD" \ + -T /usr/bin/codesign >/dev/null + rm -f "$p12" + security set-key-partition-list \ + -S apple-tool:,apple:,codesign: -s -k "$keychain_password" \ + "$keychain" >/dev/null + security list-keychains -d user -s "$keychain" + + identities=$(security find-identity -v -p codesigning "$keychain") + identity=$(printf '%s\n' "$identities" | awk -v team="($APPLE_TEAM_ID)" \ + '/Developer ID Application:/ && index($0, team) {print $2}') + count=$(printf '%s\n' "$identity" | sed '/^$/d' | wc -l | tr -d ' ') + [[ "$count" = 1 ]] || { + echo "expected exactly one Developer ID Application identity for APPLE_TEAM_ID" >&2 + exit 1 + } + + echo "RELEASE_SIGNING_IDENTITY=$identity" >> "$GITHUB_ENV" + + - name: Sign, notarize, staple, and verify DMG + env: + APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }} + APPLE_NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }} + APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} + RELEASE_TAG: ${{ inputs.tag }} + run: | + chmod +x scripts/release_macos_dmg.sh + scripts/release_macos_dmg.sh \ + --app release-stage/oMLX.app \ + --output-dir dist \ + --identity "$RELEASE_SIGNING_IDENTITY" \ + --team-id "$APPLE_TEAM_ID" \ + --notary-key "$RELEASE_NOTARY_KEY" \ + --notary-key-id "$APPLE_NOTARY_KEY_ID" \ + --notary-issuer "$APPLE_NOTARY_ISSUER_ID" \ + --version "${RELEASE_TAG#v}" + + - name: Upload verified workflow artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: signed-notarized-macos-dmg + path: | + dist/*.dmg + dist/*.sha256 + if-no-files-found: error + + - name: Create or update draft GitHub Release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ inputs.tag }} + run: | + set -euo pipefail + release_json=$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG" 2>/dev/null || true) + if [[ -n "$release_json" ]]; then + [[ "$(jq -r .draft <<<"$release_json")" = true ]] || { + echo "refusing to modify an already-published release" >&2 + exit 1 + } + else + prerelease=() + [[ "$RELEASE_TAG" =~ (a|b|rc|dev)[0-9]+$ ]] && prerelease=(--prerelease) + gh release create "$RELEASE_TAG" --verify-tag --draft \ + --generate-notes --title "$RELEASE_TAG" "${prerelease[@]}" + fi + gh release upload "$RELEASE_TAG" dist/*.dmg dist/*.sha256 --clobber + + - name: Remove ephemeral signing material + if: always() + run: | + if [[ -n "${RELEASE_KEYCHAIN:-}" ]]; then + security delete-keychain "$RELEASE_KEYCHAIN" >/dev/null 2>&1 || true + fi + rm -f "${RELEASE_NOTARY_KEY:-}" \ + "$RUNNER_TEMP/developer-id-application.p12" diff --git a/.gitignore b/.gitignore index 8c5217bbcb..e322bd5444 100644 --- a/.gitignore +++ b/.gitignore @@ -104,6 +104,23 @@ packaging/_export/ packaging/_build/ *.dmg +# Apple signing/notarization credentials and temporary keychains +*.p12 +*.pfx +*.p8 +*.cer +*.crt +*.pem +*.jks +*.keystore +*.provisionprofile +*.mobileprovision +*.keychain +*.keychain-db +AuthKey_*.p8 +.env.* +!.env.example + # Generated engine commit metadata (build-time artifact) omlx/_engine_commits.json @@ -123,3 +140,9 @@ docs/native_app_architecture.md uv.lock # generated TurboQuant memory report (machine-specific) tq_batch_memory.md + +# Local working notes and session tooling — never part of a PR +.audit/ +.claude/ +issues.md +clusterPlan.md diff --git a/Formula/omlx.rb b/Formula/omlx.rb index 54343779b2..d28365edb9 100644 --- a/Formula/omlx.rb +++ b/Formula/omlx.rb @@ -3,8 +3,8 @@ class Omlx < Formula desc "LLM inference server optimized for Apple Silicon" homepage "https://github.com/jundot/omlx" - url "https://github.com/jundot/omlx/archive/refs/tags/v0.5.5.tar.gz" - sha256 "d77b58c007b3f1d3b5463ac66ddcd9923db5839f82213d9b221e0f68b867ff3c" + url "https://github.com/jundot/omlx/archive/refs/tags/v0.6.3rc3.tar.gz" + sha256 "1d08ff4585eb796f34f56d266b69cae65b51a48d98a4b213caca094d7c82a839" license "Apache-2.0" head "https://github.com/jundot/omlx.git", branch: "main" @@ -15,7 +15,7 @@ class Omlx < Formula depends_on "rust" => :build depends_on arch: :arm64 - depends_on :macos + depends_on macos: :sequoia depends_on "python@3.11" # macOS 27 beta's `strip` corrupts dynamic offsets in Mach-O libraries diff --git a/README.fr.md b/README.fr.md index f5d1b43712..c4f162d0e8 100644 --- a/README.fr.md +++ b/README.fr.md @@ -91,7 +91,7 @@ pip install -e ".[mcp]" # Avec support MCP (Model Context Protocol) OMLX_WITH_CUSTOM_KERNEL=1 pip install -e . ``` -Nécessite macOS 15.0+ (Sequoia), Python 3.10+, et Apple Silicon (M1/M2/M3/M4). +Nécessite macOS 15.0+ (Sequoia), Python 3.10+, et Apple Silicon (M1/M2/M3/M4/M5). ## Démarrage rapide diff --git a/README.ja.md b/README.ja.md index 7bc665a668..23594a396b 100644 --- a/README.ja.md +++ b/README.ja.md @@ -90,7 +90,7 @@ pip install -e ".[mcp]" # MCP(Model Context Protocol)サポート付き OMLX_WITH_CUSTOM_KERNEL=1 pip install -e . ``` -Python 3.10+とApple Silicon(M1/M2/M3/M4)が必要です。 +Python 3.10+とApple Silicon(M1/M2/M3/M4/M5)が必要です。 ## クイックスタート diff --git a/README.ko.md b/README.ko.md index 8fb4aae410..d7ea74f0df 100644 --- a/README.ko.md +++ b/README.ko.md @@ -15,7 +15,7 @@
-
+
@@ -230,7 +257,7 @@ Claude Code에서 작은 컨텍스트 모델을 실행하기 위한 컨텍스트
### macOS 메뉴 바 앱
-네이티브 Swift / SwiftUI 메뉴 바 앱 (Electron이 아닙니다!). 터미널 없이 서버를 시작, 중지, 모니터링합니다. 서빙 통계 (재시작해도 유지됨), 크래시 시 자동 재시작, Sparkle 기반 자동 업데이트를 포함합니다.
+네이티브 Swift / SwiftUI 메뉴 바 앱 (Electron이 아닙니다!). 터미널 없이 서버를 시작, 중지, 모니터링합니다. 서빙 통계 (재시작해도 유지됨), 크래시 시 자동 재시작, 빌트인 자동 업데이트를 포함합니다.
@@ -264,7 +291,7 @@ mlx-lm에서 사용 가능한 모든 함수 호출 형식, JSON 스키마 검증
| Kimi K2 | `<\|tool_calls_section_begin\|>` |
| Longcat | `
@@ -421,4 +435,5 @@ Contributions are welcome! See [Contributing Guide](docs/CONTRIBUTING.md) for de
- [mlx-embeddings](https://github.com/Blaizzy/mlx-embeddings) - Embedding model support for Apple Silicon
- [dflash-mlx](https://github.com/bstnxbt/dflash-mlx) - Block diffusion speculative decoding on Apple Silicon
- [MTPLX](https://github.com/youssofal/mtplx) - Lightning MTP's verify-shape Metal kernels are powered by MTPLX by Youssof Altoukhi, which also inspired the depth-k pipeline
+- [mlx-serve](https://github.com/ddalcu/mlx-serve) - The fused GDN verify prework kernel is adapted from mlx-serve's port of the mlxfast-challenge qwen35_packed_gdn_prework kernel
- [SiliconScope](https://github.com/kennss/SiliconScope) - The menu bar statistics take their design and rendering approach from SiliconScope by Kennt Kim, which also inspired the energy-efficient re-render gating
diff --git a/README.zh.md b/README.zh.md
index a6469c1b8c..0cae007b27 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -90,7 +90,7 @@ pip install -e ".[mcp]" # 含 MCP(Model Context Protocol)支持
OMLX_WITH_CUSTOM_KERNEL=1 pip install -e .
```
-需要 macOS 15.0+ (Sequoia), Python 3.10+ 和 Apple Silicon(M1/M2/M3/M4)。
+需要 macOS 15.0+ (Sequoia), Python 3.10+ 和 Apple Silicon(M1/M2/M3/M4/M5)。
## 快速开始
diff --git a/apps/omlx-mac/Resources/Info.plist b/apps/omlx-mac/Resources/Info.plist
index f7c86bca33..d05a45a474 100644
--- a/apps/omlx-mac/Resources/Info.plist
+++ b/apps/omlx-mac/Resources/Info.plist
@@ -22,6 +22,8 @@
{{ t('bench.config.generation_hint') }}
+{{ t('acc_bench.config.external_extra_body_hint') }}
{{ t('acc_bench.config.external_max_tokens_hint') }}
++ oMLX finds Metal and CUDA workers, checks their connections and + usable memory, and chooses how to shard each model. +
++ Generate it first in Terminal, then run the command above. +
+
+
+
+
+
+
+
+ + Connection checks, model splitting, and retries are automatic. + macOS only asks when it needs permission. +
++ Generate one secure command per CUDA box. It installs the minimal + worker runtime, exchanges pinned SSH identities, and adds the box + to this pool automatically. +
++ The command is single-use and expires after 30 minutes. It may ask for + sudo on the CUDA box; after that, discovery, SSH setup, environment + creation, compatibility checks, and pool selection are automatic. + Generate a fresh command for the second CUDA worker. +
+ +Live hardware state from this oMLX instance
++ oMLX validates this coordinator, every worker, their memory, and every required link before launch. +
+ ++ Connect it and open oMLX. Bonjour discovery updates this view automatically. +
++ Starts an isolated child, verifies ready/ping/shutdown, then cleans it up. +
++ Runs two local MLX ranks and verifies a ring all-sum. Loopback only—not the remote link. +
++ Runs an unequal two-rank hybrid Mamba/attention graph and verifies both ranks agree. +
+Known-host SSH establishes identity; the probe checks oMLX, MLX, memory, RDMA, and the return route.
++ oMLX deliberately uses its own dedicated SSH key. A successful manual SSH login may be using a different key, so complete these steps on both Macs before checking the peer. +
++ {{ t('cluster.pairing.shared_secret_hint') }} The secret only authenticates this key exchange; it is not your SSH password. +
+This key belongs only to oMLX cluster connections. If either Mac says no key was found, generate one there first.
++ Generate and copy this Mac's token, then paste and pair it on the other Mac. Next generate the other Mac's token and bring it back here. Repeat in the other direction so each Mac trusts the other. +
+Copy this token and paste it into section B on the other Mac.
+This direction is paired. Repeat these steps on the other Mac so both directions are trusted.
+ ++ + +
++ First connection only: oMLX records a new peer address automatically and refuses changed host keys. + Password prompts are never shown by the GUI; configure an SSH key for unattended launches. +
+Contiguous pipeline stages in MLX rank order
++
+ This change requires macOS Recovery and cannot be made by a running app. +
+ + + Why this is needed + ++ Fast links carry tensor-parallel traffic (an all-reduce per layer). Slower hops are used for pipeline stage boundaries. +
++ No single command fixes this one — it has to be settled on every Mac. +
++ Review the plan below, then activate. Activation starts processes on every selected worker. +
++ Auto optimisation benchmarks every accelerator and selected link during activation. +
+ ++ Measured from the safe allocation cap, not installed memory. +
++ Automatic uses measured memory and speed. Move a device's target to put + more layers there; every override is re-checked before launch. +
++ Set by the Mac with the least room left — a request passes through every stage. +
+Advanced fallback for reviewing and activating a hand-tuned plan separately.
++ oMLX did not apply that candidate because it would no longer match the files and memory split you approved. +
+{{ t('modal.model_settings.thinking_budget_hint') }}
-{{ t('modal.model_settings.vlm_mtp_processor_locked') }}
{{ t('modal.model_settings.qwen_ane_hint') }}
+{{ t('modal.model_settings.qwen_ane_dual_hint') }}
+{{ t('modal.model_settings.qwen_ane_cpu_hint') }}
+{{ t('modal.model_settings.qwen_ane_cpu_scheduler_hint') }}
+{{ t('modal.model_settings.qwen_ane_gdn_hint') }}
+{{ t('modal.model_settings.qwen_ane_tune_hint') }}
+Attention-based sparse prefill for MoE/hybrid models. (Paper) (HuggingFace)
Block diffusion speculative decoding for faster generation. Supports Qwen (3, 3.5, 3.6), Gemma4, and Laguna model families. Requires a DFlash draft model checkpoint.
Single-stream only: requests run one at a time.
* MLX impl by bstnxbt(GitHub)
Block diffusion speculative decoding with compatible DFlash and DFlash2 checkpoints.
Single-stream only: requests run one at a time.
* MLX impl by bstnxbt(GitHub)
DFlash draft checkpoint (e.g. z-lab/Qwen3-4B-DFlash-b16, z-lab/gemma-4-26B-A4B-it-DFlash, poolside/Laguna-S-2.1-DFlash-NVFP4). Note: -DFlash suffix only; -assistant variants are for MTP.
+DFlash draft checkpoint (e.g. z-lab/Qwen3-4B-DFlash-b16, z-lab/gemma-4-26B-A4B-it-DFlash, z-lab/Qwen3.8-27B-DFlash2, poolside/Laguna-S-2.1-DFlash-NVFP4). Note: -DFlash / -DFlash2 suffix; -assistant variants are for MTP.
Draft model sliding-attention window. Helps stabilise acceptance on long contexts. Leave empty for dflash default (1024).
+Draft model sliding-attention window. Helps stabilise acceptance on long contexts. Leave empty for dflash default (2048).
Attention-sink tokens always kept regardless of window. Leave empty for dflash default (64).
+Attention-sink tokens always kept regardless of window. Leave empty for dflash default (0).
Maximum draft and verify tokens per cycle. Leave empty to use the checkpoint block size.
+{{ t('settings.resource.decode_fairness_description') }}
+{{ t('settings.mcp.expose_tools_hint') }}
+{{ t('settings.advanced.distributed_inference_hint') }}
+{{ t('settings.advanced.hot_cache_write_through_hint') }}
+{{ t('settings.advanced.ane_compile_cache_hint') }}
+{{ t('settings.advanced.gdn_cache_policy_hint') }}
+{{ t('settings.advanced.gdn_snapshot_storage_hint') }}
+{{ t('settings.advanced.gdn_pending_write_limit_hint') }}
+{{ t('settings.advanced.gdn_sidecar_state_precision_hint') }}
++ + {{ t('settings.advanced.gdn_sidecar_state_precision_warning') }} +
+{{ t('settings.integrations.websearch.provider_hint') }}
+{{ t('settings.integrations.websearch.ddgs_backends_hint') }}
+{{ t('settings.integrations.websearch.brave_api_key_hint') }}
+{{ t('settings.integrations.websearch.searxng_url_hint') }}
+{{ t('settings.integrations.websearch.max_results_hint') }}
+{{ t('settings.integrations.websearch.content_mode_hint') }}
+{{ t('settings.integrations.websearch.content_truncate_hint') }}
+{{ t('settings.integrations.websearch.content_max_chars_hint') }}
+
Prefix Hit Rate
+{{ t('status.runtime_cache.prefix_hit_rate') }}
Memory Hit Rate
+{{ t('status.runtime_cache.memory_hit_rate') }}
Prefix Evictions
+{{ t('status.runtime_cache.prefix_evictions') }}
Memory Evictions
+{{ t('status.runtime_cache.memory_evictions') }}
| Model | -Block Size | -Indexed Blocks | -Sub-block Cache | -SSD Files | -SSD Size | -Memory Entries | -Memory Size | +{{ t('status.runtime_cache.col_model') }} | +{{ t('status.runtime_cache.col_block_size') }} | +{{ t('status.runtime_cache.col_indexed_blocks') }} | +{{ t('status.runtime_cache.col_subblock_cache') }} | +{{ t('status.runtime_cache.col_ssd_files') }} | +{{ t('status.runtime_cache.col_ssd_size') }} | +{{ t('status.runtime_cache.col_memory_entries') }} | +{{ t('status.runtime_cache.col_memory_size') }} |
|---|