From f699408640c0a62b161feafedb066e83707271d6 Mon Sep 17 00:00:00 2001 From: Krish Garg Date: Mon, 5 Oct 2026 16:46:37 -0700 Subject: [PATCH 01/11] Add the release bundle build script packaging/build.sh macos14|macos26 builds sys1rust against the pinned MLX wheel for that macOS target, points its rpath at lib/ next to bin/, signs it ad hoc, checks linkage, minimum macOS, signature and --version, and tars it with libmlx, libjaccl, the metallib and the license files. Co-Authored-By: Claude Opus 5.5 (1M context) --- .gitignore | 4 ++ packaging/build.sh | 83 +++++++++++++++++++++++++++++++++++++++ packaging/mlx-wheels.lock | 3 ++ packaging/third_party.py | 57 +++++++++++++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100755 packaging/build.sh create mode 100644 packaging/mlx-wheels.lock create mode 100755 packaging/third_party.py diff --git a/.gitignore b/.gitignore index eb6e747..d90aff8 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ bench/contenders/*/src/ # Raw measurement data stays local; results/*.md summarize it results/raw/ + +# Release bundle work dirs and output (packaging/build.sh) +packaging/.work/ +dist/ diff --git a/packaging/build.sh b/packaging/build.sh new file mode 100755 index 0000000..9609d62 --- /dev/null +++ b/packaging/build.sh @@ -0,0 +1,83 @@ +#!/bin/bash +# Build one release bundle: packaging/build.sh macos14|macos26 OUT_DIR +# Downloads the pinned MLX wheel (packaging/mlx-wheels.lock), builds sys1rust against it for +# the flavor's macOS target, makes the binary find lib/ next to bin/ (@executable_path/../lib), +# signs it ad hoc, checks the result and writes OUT_DIR/sys1rust---arm64.tar.gz. +set -euo pipefail + +fail() { echo "build.sh: $*" >&2; exit 1; } + +FLAVOR=${1:-} +OUT_ARG=${2:-} +case "$FLAVOR" in + macos14) TARGET=14.0 ;; + macos26) TARGET=26.2 ;; + *) echo "usage: $0 macos14|macos26 OUT_DIR" >&2; exit 2 ;; +esac +[ -n "$OUT_ARG" ] || { echo "usage: $0 macos14|macos26 OUT_DIR" >&2; exit 2; } +mkdir -p "$OUT_ARG" +OUT=$(cd "$OUT_ARG" && pwd) +ROOT=$(cd "$(dirname "$0")/.." && pwd) + +# read exits 1 when the lock has no line for the flavor, so let the check below report it. +read -r URL SHA < <(awk -v f="$FLAVOR" '$1 == f {print $2, $3}' "$ROOT/packaging/mlx-wheels.lock") || true +[ -n "${URL:-}" ] || fail "no wheel for $FLAVOR in packaging/mlx-wheels.lock" +VERSION=$(sed -n 's/^version = "\(.*\)"$/\1/p' "$ROOT/runtime/Cargo.toml") +[ -n "$VERSION" ] || fail "no workspace version in runtime/Cargo.toml" +NAME="sys1rust-$VERSION-$FLAVOR-arm64" +WORK="$ROOT/packaging/.work/$FLAVOR" +mkdir -p "$WORK" + +# 1. The pinned wheel. +WHEEL="$WORK/$(basename "$URL")" +if [ ! -f "$WHEEL" ]; then + curl -fsSL --retry 3 -o "$WHEEL.part" "$URL" + mv "$WHEEL.part" "$WHEEL" +fi +echo "$SHA $WHEEL" | shasum -a 256 -c - >/dev/null || fail "sha256 mismatch for $WHEEL" +rm -rf "$WORK/wheel" +unzip -q "$WHEEL" -d "$WORK/wheel" +export MLX_SYS_PREBUILT_DIR="$WORK/wheel/mlx" + +# 2. The binary, for the flavor's oldest macOS. +export MACOSX_DEPLOYMENT_TARGET="$TARGET" +export SYS1_BUILD_FLAVOR="$FLAVOR" +export CARGO_TARGET_DIR="$WORK/target" +cargo build --release --locked --manifest-path "$ROOT/runtime/Cargo.toml" -p sys1rust + +# 3. The bundle. +STAGE="$WORK/$NAME" +rm -rf "$STAGE" +mkdir -p "$STAGE/bin" "$STAGE/lib" "$STAGE/licenses" +BIN="$STAGE/bin/sys1rust" +cp "$CARGO_TARGET_DIR/release/sys1rust" "$BIN" +cp "$MLX_SYS_PREBUILT_DIR"/lib/{libmlx.dylib,libjaccl.dylib,mlx.metallib} "$STAGE/lib/" +cp "$ROOT/LICENSE" "$ROOT/NOTICE" "$STAGE/" +cp "$WORK"/wheel/mlx_metal-*.dist-info/licenses/LICENSE "$STAGE/licenses/MLX-LICENSE" +python3 "$ROOT/packaging/third_party.py" > "$STAGE/licenses/THIRD_PARTY.md" + +# 4. Find lib/ next to bin/, not the build machine's wheel directory. +otool -l "$BIN" | awk '/cmd LC_RPATH/ {getline; getline; print $2}' | while read -r rpath; do + install_name_tool -delete_rpath "$rpath" "$BIN" +done +install_name_tool -add_rpath @executable_path/../lib "$BIN" +codesign --force -s - "$BIN" + +# 5. Checks. +bad=$(otool -L "$BIN" | tail -n +2 | awk '{print $1}' | grep -Ev '^(@rpath/|/usr/lib/|/System/)' || true) +[ -z "$bad" ] || fail "the binary links outside the bundle and the OS: $bad" +rpaths=$(otool -l "$BIN" | awk '/cmd LC_RPATH/ {getline; getline; print $2}') +[ "$rpaths" = "@executable_path/../lib" ] || fail "rpaths are: $rpaths" +for f in "$BIN" "$STAGE/lib/libmlx.dylib" "$STAGE/lib/libjaccl.dylib"; do + minos=$(vtool -show-build "$f" | awk '/minos/ {print $2; exit}') + python3 -c 'import sys; a, b = (tuple(map(int, v.split("."))) for v in sys.argv[1:]); sys.exit(a > b)' \ + "$minos" "$TARGET" || fail "$f needs macOS $minos, above the $FLAVOR target $TARGET" +done +codesign --verify --strict "$BIN" || fail "codesign --verify failed" +want="sys1rust $VERSION (MLX 0.32.2, $FLAVOR build)" +got=$("$BIN" --version) +[ "$got" = "$want" ] || fail "--version printed '$got', expected '$want'" + +# 6. The tarball. macOS tar would also store this Mac's xattrs and AppleDouble ._ files. +tar -C "$WORK" --no-mac-metadata --no-xattrs -czf "$OUT/$NAME.tar.gz" "$NAME" +echo "$OUT/$NAME.tar.gz $(du -h "$OUT/$NAME.tar.gz" | cut -f1)" diff --git a/packaging/mlx-wheels.lock b/packaging/mlx-wheels.lock new file mode 100644 index 0000000..26d5555 --- /dev/null +++ b/packaging/mlx-wheels.lock @@ -0,0 +1,3 @@ +# flavor wheel URL sha256 (from https://pypi.org/pypi/mlx-metal/0.32.2/json) +macos14 https://files.pythonhosted.org/packages/f7/ab/ba1952908c5d2a5070cf1cfbfea0161c4751ea62299e2776819810917483/mlx_metal-0.32.2-py3-none-macosx_14_0_arm64.whl 3825fff379dbc107dd3413e564a06caeaa24819910ec49c0439e454c06a1b9b8 +macos26 https://files.pythonhosted.org/packages/dd/cd/4e50bf325100e7165e13d025f264362bf0009196269f9eaf87f2c6e738a2/mlx_metal-0.32.2-py3-none-macosx_26_0_arm64.whl e6abeac9ac5265830c9c1541b6f96e9be37a85c2446763a46ad466c63a3837ab diff --git a/packaging/third_party.py b/packaging/third_party.py new file mode 100755 index 0000000..5e1a938 --- /dev/null +++ b/packaging/third_party.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Print THIRD_PARTY.md for a release bundle: every crate compiled into the sys1rust binary +(normal dependencies on aarch64-apple-darwin) with its license expression and the license +files in its source, then mlx-c, which mlx-sys builds and links in statically. + +Usage: packaging/third_party.py > THIRD_PARTY.md +""" +import json +import os +import subprocess + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PREFIXES = ("LICENSE", "LICENCE", "COPYING", "NOTICE", "UNLICENSE") + + +def license_files(d): + names = sorted(n for n in os.listdir(d) + if n.upper().startswith(PREFIXES) and os.path.isfile(os.path.join(d, n))) + if not names: + return ["The crate's source has no license file.", ""] + out = [] + for n in names: + with open(os.path.join(d, n), encoding="utf-8", errors="replace") as f: + out += [f"### {n}", "", "```", f.read().rstrip(), "```", ""] + return out + + +def main(): + meta = json.loads(subprocess.check_output([ + "cargo", "metadata", "--format-version", "1", "--locked", + "--manifest-path", os.path.join(ROOT, "runtime", "Cargo.toml"), + "--filter-platform", "aarch64-apple-darwin"])) + pkgs = {p["id"]: p for p in meta["packages"]} + nodes = {n["id"]: n for n in meta["resolve"]["nodes"]} + members = set(meta["workspace_members"]) + root = next(i for i in members if pkgs[i]["name"] == "sys1rust") + found, stack = set(), [root] + while stack: + for dep in nodes[stack.pop()]["deps"]: + normal = any(k["kind"] is None for k in dep["dep_kinds"]) + if normal and dep["pkg"] not in found: + found.add(dep["pkg"]) + stack.append(dep["pkg"]) + out = ["# Third-party licenses", "", + "The sys1rust binary includes these Rust crates and mlx-c. Each section gives the " + "license expression and the license files in the source.", ""] + for pid in sorted(found - members, key=lambda i: (pkgs[i]["name"], pkgs[i]["version"])): + p = pkgs[pid] + out += [f"## {p['name']} {p['version']}", "", f"License: {p.get('license') or 'see below'}", ""] + out += license_files(os.path.dirname(p["manifest_path"])) + out += ["## mlx-c, built by mlx-sys and linked in", "", "License: MIT", ""] + out += license_files(os.path.join(ROOT, "runtime", "vendor", "mlx-sys", "src", "mlx-c")) + print("\n".join(out)) + + +if __name__ == "__main__": + main() From 220ab3b51cc5ba09706a01a1b4e581ca3b8fdcc7 Mon Sep 17 00:00:00 2001 From: Krish Garg Date: Mon, 5 Oct 2026 16:48:17 -0700 Subject: [PATCH 02/11] Add the release smoke test Starts a bundle's sys1rust serve, sends the 24 smoke requests and checks the answers against the fp32 CPU reference with compare.py's rules, then requires a clean SIGINT shutdown. Co-Authored-By: Claude Opus 5.5 (1M context) --- packaging/smoke.py | 92 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100755 packaging/smoke.py diff --git a/packaging/smoke.py b/packaging/smoke.py new file mode 100755 index 0000000..c4005e7 --- /dev/null +++ b/packaging/smoke.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Release smoke test. Starts `SYS1RUST serve --offline --port 0` with typed-decisions, sends the +24 requests in bench/workloads/smoke.jsonl, compares the answers with the fp32 CPU reference in +bench/reference/typed-decisions/smoke.jsonl by the rules of bench/harness/compare.py (each +question's answer agrees unless the reference is a near tie, and probabilities are within +MAX_DRIFT), then stops the server with SIGINT and requires exit code 0. + +Run `SYS1RUST pull` first. Usage: packaging/smoke.py PATH_TO_SYS1RUST +""" +import http.client +import json +import os +import queue +import signal +import subprocess +import sys +import threading + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(ROOT, "bench", "harness")) +from compare import NEAR_TIE, agree, drift, margin, read_jsonl # noqa: E402 + +MODEL = "typed-decisions" +REQUESTS = 24 +MAX_DRIFT = 0.05 +READY_TIMEOUT_S = 300 + + +def main(): + exe = sys.argv[1] + reqs = read_jsonl(os.path.join(ROOT, "bench", "workloads", "smoke.jsonl")) + ref = {r["id"]: r["answers"] + for r in read_jsonl(os.path.join(ROOT, "bench", "reference", MODEL, "smoke.jsonl")) + if r.get("type") == "result"} + assert len(reqs) == REQUESTS and len(ref) == REQUESTS, (len(reqs), len(ref)) + + env = {k: v for k, v in os.environ.items() + if not k.startswith(("LAYA_", "SYS1_")) and k != "HF_HUB_OFFLINE"} + proc = subprocess.Popen([exe, "serve", "--offline", "--model", MODEL, "--port", "0"], + stdout=subprocess.PIPE, text=True, env=env) + lines = queue.Queue() + threading.Thread(target=lambda: [lines.put(line) for line in proc.stdout], daemon=True).start() + failures, questions, worst = [], 0, 0.0 + try: + try: + ready = json.loads(lines.get(timeout=READY_TIMEOUT_S)) + except queue.Empty: + sys.exit(f"no ready line within {READY_TIMEOUT_S} s") + host, port = ready["addr"].rsplit(":", 1) + print(f"ready: {ready['model']} @ {ready['revision']}, engine {ready['engine']}, " + f"load {ready['load_ms']} ms", flush=True) + conn = http.client.HTTPConnection(host, int(port), timeout=120) + for r in reqs: + conn.request("POST", "/v1/systemone", body=json.dumps(dict(r["body"], model=MODEL)), + headers={"content-type": "application/json"}) + resp = conn.getresponse() + data = resp.read() + if resp.status != 200: + failures.append(f"{r['id']}: HTTP {resp.status} {data[:200]!r}") + continue + answers = json.loads(data)["answers"] + for q, want in ref[r["id"]].items(): + questions += 1 + got = answers.get(q) + if got is None: + failures.append(f"{r['id']} {q}: missing") + continue + if not agree(got, want) and margin(want) >= NEAR_TIE: + failures.append(f"{r['id']} {q}: {got} vs reference {want}") + d = drift(got, want) + if d is not None: + worst = max(worst, d) + if d > MAX_DRIFT: + failures.append(f"{r['id']} {q}: drift {d:.4f} > {MAX_DRIFT}") + conn.close() + finally: + proc.send_signal(signal.SIGINT) + try: + code = proc.wait(timeout=30) + except subprocess.TimeoutExpired: + proc.kill() + code = "killed after 30 s" + print(f"{REQUESTS} requests, {questions} questions, largest drift {worst:.4f}, " + f"{len(failures)} failures, server exit {code}") + for f in failures: + print("FAIL", f) + if failures or code != 0: + sys.exit(1) + + +if __name__ == "__main__": + main() From eae9e54a9d511c5c8be6c1352d6194a247da807f Mon Sep 17 00:00:00 2001 From: Krish Garg Date: Mon, 5 Oct 2026 16:53:24 -0700 Subject: [PATCH 03/11] Add the Homebrew formula template and its render script The formula picks the macOS 26 bundle on 26.2 and later and the macOS 14 bundle otherwise, installs into libexec with a bin symlink, and declares a brew services entry for sys1rust serve. Co-Authored-By: Claude Opus 5.5 (1M context) --- packaging/render_formula.sh | 17 +++++++++++++ packaging/sys1rust.rb.in | 51 +++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100755 packaging/render_formula.sh create mode 100644 packaging/sys1rust.rb.in diff --git a/packaging/render_formula.sh b/packaging/render_formula.sh new file mode 100755 index 0000000..2a418b2 --- /dev/null +++ b/packaging/render_formula.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Print the Homebrew formula for a release: +# packaging/render_formula.sh TAG SHA256_MACOS26 SHA256_MACOS14 [BASE_URL] +# BASE_URL defaults to the GitHub release of TAG. A file:// URL tests a local build. +set -euo pipefail +TAG=$1 +S26=$2 +S14=$3 +BASE=${4:-https://github.com/krishhgg/sys1rust/releases/download/$TAG} +VERSION=${TAG#v} +VERSION=${VERSION%%-rc*} +for s in "$S26" "$S14"; do + [[ $s =~ ^[0-9a-f]{64}$ ]] || { echo "render_formula.sh: not a sha256: '$s'" >&2; exit 1; } +done +sed -e "s|@VERSION@|$VERSION|g" -e "s|@BASE_URL@|$BASE|g" \ + -e "s|@SHA256_MACOS26@|$S26|g" -e "s|@SHA256_MACOS14@|$S14|g" \ + "$(dirname "$0")/sys1rust.rb.in" diff --git a/packaging/sys1rust.rb.in b/packaging/sys1rust.rb.in new file mode 100644 index 0000000..f6f5c5e --- /dev/null +++ b/packaging/sys1rust.rb.in @@ -0,0 +1,51 @@ +# Generated by packaging/render_formula.sh in krishhgg/sys1rust for each release. +# The next release overwrites edits here. +class Sys1rust < Formula + desc "Laya System 1 decision server for Apple silicon, in Rust and Metal" + homepage "https://github.com/krishhgg/sys1rust" + version "@VERSION@" + license "Apache-2.0" + + depends_on arch: :arm64 + depends_on macos: :sonoma + + # MLX's build for macOS 26.2 and later is about 3x faster on the M5 than its macOS 14 build. + # on_macos keeps the MacOS call off Linux, where brew style rejects it. + on_macos do + if MacOS.full_version >= "26.2" + url "@BASE_URL@/sys1rust-@VERSION@-macos26-arm64.tar.gz" + sha256 "@SHA256_MACOS26@" + else + url "@BASE_URL@/sys1rust-@VERSION@-macos14-arm64.tar.gz" + sha256 "@SHA256_MACOS14@" + end + end + + def install + libexec.install Dir["*"] + bin.install_symlink libexec/"bin/sys1rust" + end + + def caveats + <<~EOS + The first `sys1rust serve` downloads the typed-decisions model (846 MB) into the + Hugging Face cache (~/.cache/huggingface/hub). `sys1rust pull` downloads it ahead of time. + + The first start after an install or upgrade compiles Metal shaders for about 1.7 s. + + The `brew services start` command below runs the server on 127.0.0.1:8000. + EOS + end + + service do + run [opt_bin/"sys1rust", "serve"] + keep_alive true + log_path var/"log/sys1rust.log" + error_log_path var/"log/sys1rust.log" + end + + test do + assert_match version.to_s, shell_output("#{bin}/sys1rust --version") + assert_match "typed-decisions", shell_output("#{bin}/sys1rust models") + end +end From 4908aba9642db7945e1ee7e8a758e4c5a1513505 Mon Sep 17 00:00:00 2001 From: Krish Garg Date: Mon, 5 Oct 2026 16:57:03 -0700 Subject: [PATCH 04/11] Make the smoke test fail on malformed answers and always clean up The smoke test now checks each answer against the reference's type and fields before it allows a near-tie disagreement, and it requires probabilities wherever the reference has them so the drift check always runs. Each request runs in a worker thread with a 120 s deadline, and a transport error, timeout or bad JSON records a FAIL line with its stage. SIGINT and SIGTERM handlers route through the cleanup, which waits up to 10 s after SIGKILL and reports a server that is still alive. Co-Authored-By: Claude Opus 5.5 (1M context) --- packaging/smoke.py | 213 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 170 insertions(+), 43 deletions(-) diff --git a/packaging/smoke.py b/packaging/smoke.py index c4005e7..09fdcc1 100755 --- a/packaging/smoke.py +++ b/packaging/smoke.py @@ -5,10 +5,16 @@ question's answer agrees unless the reference is a near tie, and probabilities are within MAX_DRIFT), then stops the server with SIGINT and requires exit code 0. +Every answer must also be well formed: the reference's type and every field the reference has, +so a near tie or a missing probability map can't hide a broken answer. A request that fails in +transport, runs past REQUEST_TIMEOUT_S or returns bad JSON fails the run and stops sending. The +server always gets stopped, also when this script gets SIGINT or SIGTERM. + Run `SYS1RUST pull` first. Usage: packaging/smoke.py PATH_TO_SYS1RUST """ import http.client import json +import math import os import queue import signal @@ -24,6 +30,155 @@ REQUESTS = 24 MAX_DRIFT = 0.05 READY_TIMEOUT_S = 300 +# Covers connect, send, headers and body of one request. CI runners have a slow paravirtual GPU. +REQUEST_TIMEOUT_S = 120 +STOP_TIMEOUT_S = 30 +KILL_TIMEOUT_S = 10 + + +class Interrupted(Exception): + """Raised by the SIGINT and SIGTERM handlers so the server cleanup in `finally` runs.""" + + +def on_signal(signum, _frame): + raise Interrupted(signal.Signals(signum).name) + + +def is_number(v): + return isinstance(v, (int, float)) and not isinstance(v, bool) and math.isfinite(v) + + +def malformed(got, want): + """Says why `got` can't stand in for the reference answer `want`, or returns None if it can.""" + if not isinstance(got, dict): + return f"answer is {type(got).__name__}, not an object" + if got.get("type") != want["type"]: + return f"type {got.get('type')!r}, reference type {want['type']!r}" + labels = want.get("probabilities") or {} + choice = got.get("choice") + if "choice" in want and not (isinstance(choice, str) and choice in labels): + return f"choice {choice!r} is not a reference label" + for field in ("score", "noul"): + if field in want and not is_number(got.get(field)): + return f"{field} {got.get(field)!r} is not a number" + if "probabilities" in want: + probs = got.get("probabilities") + if not isinstance(probs, dict) or not probs: + return "no probabilities" + if not all(is_number(p) for p in probs.values()): + return "a probability is not a number" + return None + + +def post(host, port, body, out): + """Sends one request on a fresh connection. Keeps the current stage in out["stage"] so a + timeout can name it, and leaves out["error"] or out["status"], out["data"] and, after a 200, + out["answers"].""" + try: + out["stage"] = "connect" + conn = http.client.HTTPConnection(host, port, timeout=REQUEST_TIMEOUT_S) + conn.connect() + out["stage"] = "send" + conn.request("POST", "/v1/systemone", body=body, headers={"content-type": "application/json"}) + out["stage"] = "status" + resp = conn.getresponse() + out["status"] = resp.status + out["stage"] = "read" + out["data"] = resp.read() + conn.close() + if resp.status == 200: + out["stage"] = "parse" + answers = json.loads(out["data"])["answers"] + if not isinstance(answers, dict): + raise ValueError(f"answers is {type(answers).__name__}, not an object") + out["answers"] = answers + except Exception as e: + out["error"] = e + + +def check(rid, answers, want_all, failures, stats): + for q, want in want_all.items(): + stats["questions"] += 1 + where = f"{rid} {q}" + if q not in answers: + failures.append(f"{where}: missing") + continue + got = answers[q] + bad = malformed(got, want) + if bad: + failures.append(f"{where}: malformed answer, {bad}: {got}") + continue + try: + ok, d = agree(got, want), drift(got, want) + except (TypeError, ValueError) as e: + failures.append(f"{where}: cannot compare {got}: {e!r}") + continue + if not ok and margin(want) >= NEAR_TIE: + failures.append(f"{where}: {got} vs reference {want}") + if d is None: + failures.append(f"{where}: no probability drift for {got}") + continue + stats["worst"] = max(stats["worst"], d) + if d > MAX_DRIFT: + failures.append(f"{where}: drift {d:.4f} > {MAX_DRIFT}") + + +def run(proc, reqs, ref, failures, stats): + lines = queue.Queue() + + def pump(): + for line in proc.stdout: + lines.put(line) + lines.put(None) + + threading.Thread(target=pump, daemon=True).start() + try: + line = lines.get(timeout=READY_TIMEOUT_S) + except queue.Empty: + failures.append(f"no ready line within {READY_TIMEOUT_S} s") + return + if line is None: + failures.append("server exited before printing the ready line") + return + ready = json.loads(line) + host, port = ready["addr"].rsplit(":", 1) + print(f"ready: {ready['model']} @ {ready['revision']}, engine {ready['engine']}, " + f"load {ready['load_ms']} ms", flush=True) + for r in reqs: + out = {} + body = json.dumps(dict(r["body"], model=MODEL)) + worker = threading.Thread(target=post, args=(host, int(port), body, out), daemon=True) + worker.start() + worker.join(REQUEST_TIMEOUT_S) + if worker.is_alive(): + failures.append(f"{r['id']}: no full reply within {REQUEST_TIMEOUT_S} s, " + f"stuck at stage {out['stage']}") + return + if "error" in out: + failures.append(f"{r['id']}: {out['stage']} failed: {out['error']!r}") + return + if out["status"] != 200: + stats["requests"] += 1 + failures.append(f"{r['id']}: HTTP {out['status']} {out['data'][:200]!r}") + continue + stats["requests"] += 1 + check(r["id"], out["answers"], ref[r["id"]], failures, stats) + + +def stop(proc): + """Stops the server with SIGINT and returns its exit status, or a note if SIGKILL was needed.""" + if proc.poll() is not None: + return proc.returncode + proc.send_signal(signal.SIGINT) + try: + return proc.wait(timeout=STOP_TIMEOUT_S) + except subprocess.TimeoutExpired: + proc.kill() + try: + proc.wait(timeout=KILL_TIMEOUT_S) + return f"killed after {STOP_TIMEOUT_S} s" + except subprocess.TimeoutExpired: + return f"pid {proc.pid} still running {KILL_TIMEOUT_S} s after SIGKILL" def main(): @@ -36,51 +191,23 @@ def main(): env = {k: v for k, v in os.environ.items() if not k.startswith(("LAYA_", "SYS1_")) and k != "HF_HUB_OFFLINE"} - proc = subprocess.Popen([exe, "serve", "--offline", "--model", MODEL, "--port", "0"], - stdout=subprocess.PIPE, text=True, env=env) - lines = queue.Queue() - threading.Thread(target=lambda: [lines.put(line) for line in proc.stdout], daemon=True).start() - failures, questions, worst = [], 0, 0.0 + failures, stats = [], {"requests": 0, "questions": 0, "worst": 0.0} + signal.signal(signal.SIGINT, on_signal) + signal.signal(signal.SIGTERM, on_signal) + proc, code = None, "not started" try: - try: - ready = json.loads(lines.get(timeout=READY_TIMEOUT_S)) - except queue.Empty: - sys.exit(f"no ready line within {READY_TIMEOUT_S} s") - host, port = ready["addr"].rsplit(":", 1) - print(f"ready: {ready['model']} @ {ready['revision']}, engine {ready['engine']}, " - f"load {ready['load_ms']} ms", flush=True) - conn = http.client.HTTPConnection(host, int(port), timeout=120) - for r in reqs: - conn.request("POST", "/v1/systemone", body=json.dumps(dict(r["body"], model=MODEL)), - headers={"content-type": "application/json"}) - resp = conn.getresponse() - data = resp.read() - if resp.status != 200: - failures.append(f"{r['id']}: HTTP {resp.status} {data[:200]!r}") - continue - answers = json.loads(data)["answers"] - for q, want in ref[r["id"]].items(): - questions += 1 - got = answers.get(q) - if got is None: - failures.append(f"{r['id']} {q}: missing") - continue - if not agree(got, want) and margin(want) >= NEAR_TIE: - failures.append(f"{r['id']} {q}: {got} vs reference {want}") - d = drift(got, want) - if d is not None: - worst = max(worst, d) - if d > MAX_DRIFT: - failures.append(f"{r['id']} {q}: drift {d:.4f} > {MAX_DRIFT}") - conn.close() + proc = subprocess.Popen([exe, "serve", "--offline", "--model", MODEL, "--port", "0"], + stdout=subprocess.PIPE, text=True, env=env) + run(proc, reqs, ref, failures, stats) + except Exception as e: + failures.append(f"stopped early: {e!r}") finally: - proc.send_signal(signal.SIGINT) - try: - code = proc.wait(timeout=30) - except subprocess.TimeoutExpired: - proc.kill() - code = "killed after 30 s" - print(f"{REQUESTS} requests, {questions} questions, largest drift {worst:.4f}, " + # A second signal must not cut the cleanup short. stop() is bounded. + signal.signal(signal.SIGINT, signal.SIG_IGN) + signal.signal(signal.SIGTERM, signal.SIG_IGN) + if proc is not None: + code = stop(proc) + print(f"{stats['requests']} requests, {stats['questions']} questions, largest drift {stats['worst']:.4f}, " f"{len(failures)} failures, server exit {code}") for f in failures: print("FAIL", f) From 05c8a0392367a60f65e146e54fd895db8228be5e Mon Sep 17 00:00:00 2001 From: Krish Garg Date: Mon, 5 Oct 2026 18:07:06 -0700 Subject: [PATCH 05/11] Ship third-party notices for lib/ and fix release script findings packaging/licenses holds pinned license texts for the third-party code that MLX 0.32.2 compiles into libmlx.dylib, libjaccl.dylib and mlx.metallib. build.sh copies them into the bundle's licenses/, appends packaging/licenses/NOTICE to the bundle NOTICE and fails if a file is missing. build.sh also reads the whole LC_RPATH path, so a checkout path with spaces builds, requires the Rust version in packaging/rust-toolchain-version and prints tarball sizes in decimal MB. third_party.py runs cargo metadata only with that Rust version. render_formula.sh escapes \, & and | so BASE_URL and the version land in the formula literally. Co-Authored-By: Claude Opus 5.5 (1M context) --- packaging/build.sh | 41 +++- packaging/licenses/NOTICE | 35 +++ packaging/licenses/expm1f-mlx-0.32.2.txt | 29 +++ packaging/licenses/fp16-ba1d31f.txt | 15 ++ packaging/licenses/gguf-tools-8fa6eb6.txt | 25 +++ packaging/licenses/metal-cpp-26.txt | 207 ++++++++++++++++++ packaging/licenses/nlohmann-json-3.11.3.txt | 33 +++ packaging/licenses/pocketfft-mlx-0.32.2.txt | 38 ++++ .../licenses/thrust-cexpf-mlx-0.32.2.txt | 23 ++ .../licenses/v8-small-vector-mlx-0.32.2.txt | 30 +++ packaging/render_formula.sh | 4 +- packaging/rust-toolchain-version | 1 + packaging/third_party.py | 11 + 13 files changed, 484 insertions(+), 8 deletions(-) create mode 100644 packaging/licenses/NOTICE create mode 100644 packaging/licenses/expm1f-mlx-0.32.2.txt create mode 100644 packaging/licenses/fp16-ba1d31f.txt create mode 100644 packaging/licenses/gguf-tools-8fa6eb6.txt create mode 100644 packaging/licenses/metal-cpp-26.txt create mode 100644 packaging/licenses/nlohmann-json-3.11.3.txt create mode 100644 packaging/licenses/pocketfft-mlx-0.32.2.txt create mode 100644 packaging/licenses/thrust-cexpf-mlx-0.32.2.txt create mode 100644 packaging/licenses/v8-small-vector-mlx-0.32.2.txt create mode 100644 packaging/rust-toolchain-version diff --git a/packaging/build.sh b/packaging/build.sh index 9609d62..477b15c 100755 --- a/packaging/build.sh +++ b/packaging/build.sh @@ -1,12 +1,25 @@ #!/bin/bash # Build one release bundle: packaging/build.sh macos14|macos26 OUT_DIR -# Downloads the pinned MLX wheel (packaging/mlx-wheels.lock), builds sys1rust against it for -# the flavor's macOS target, makes the binary find lib/ next to bin/ (@executable_path/../lib), -# signs it ad hoc, checks the result and writes OUT_DIR/sys1rust---arm64.tar.gz. +# Downloads the pinned MLX wheel (packaging/mlx-wheels.lock), builds sys1rust against it with the +# pinned Rust (packaging/rust-toolchain-version) for the flavor's macOS target, makes the binary +# find lib/ next to bin/ (@executable_path/../lib), signs it ad hoc, adds the license files, +# checks the result and writes OUT_DIR/sys1rust---arm64.tar.gz. set -euo pipefail fail() { echo "build.sh: $*" >&2; exit 1; } +# Prints each LC_RPATH of a Mach-O file on its own line. otool prints "path (offset N)". +rpaths_of() { + otool -l "$1" | awk '/cmd LC_RPATH/ {getline; getline; sub(/^ *path /, ""); sub(/ \(offset [0-9]+\)$/, ""); print}' +} + +# Third-party code that MLX 0.32.2 compiles into lib/ and whose license asks for its notice in +# binary copies. packaging/licenses/NOTICE describes each one. fmt 12.1.0 (MIT with an exception +# for object code) and ThreadPool (zlib, notice for source copies only) need none. +MLX_THIRD_PARTY=(nlohmann-json-3.11.3.txt gguf-tools-8fa6eb6.txt fp16-ba1d31f.txt metal-cpp-26.txt + pocketfft-mlx-0.32.2.txt v8-small-vector-mlx-0.32.2.txt expm1f-mlx-0.32.2.txt + thrust-cexpf-mlx-0.32.2.txt) + FLAVOR=${1:-} OUT_ARG=${2:-} case "$FLAVOR" in @@ -28,6 +41,14 @@ NAME="sys1rust-$VERSION-$FLAVOR-arm64" WORK="$ROOT/packaging/.work/$FLAVOR" mkdir -p "$WORK" +# The pinned Rust. The release workflow installs the version in the same file. +RUST=$(cat "$ROOT/packaging/rust-toolchain-version") +for tool in rustc cargo; do + got=$("$tool" --version | awk '{print $2}') + [ "$got" = "$RUST" ] || fail "$tool is $got, but the release build needs $RUST" \ + "(packaging/rust-toolchain-version). Run 'rustup toolchain install $RUST' and build with RUSTUP_TOOLCHAIN=$RUST." +done + # 1. The pinned wheel. WHEEL="$WORK/$(basename "$URL")" if [ ! -f "$WHEEL" ]; then @@ -52,12 +73,18 @@ mkdir -p "$STAGE/bin" "$STAGE/lib" "$STAGE/licenses" BIN="$STAGE/bin/sys1rust" cp "$CARGO_TARGET_DIR/release/sys1rust" "$BIN" cp "$MLX_SYS_PREBUILT_DIR"/lib/{libmlx.dylib,libjaccl.dylib,mlx.metallib} "$STAGE/lib/" -cp "$ROOT/LICENSE" "$ROOT/NOTICE" "$STAGE/" +cp "$ROOT/LICENSE" "$STAGE/" cp "$WORK"/wheel/mlx_metal-*.dist-info/licenses/LICENSE "$STAGE/licenses/MLX-LICENSE" +for f in "${MLX_THIRD_PARTY[@]}"; do + [ -f "$ROOT/packaging/licenses/$f" ] || fail "packaging/licenses/$f is missing" + grep -qF "licenses/$f" "$ROOT/packaging/licenses/NOTICE" || fail "packaging/licenses/NOTICE does not name $f" + cp "$ROOT/packaging/licenses/$f" "$STAGE/licenses/" +done +{ cat "$ROOT/NOTICE"; echo; cat "$ROOT/packaging/licenses/NOTICE"; } > "$STAGE/NOTICE" python3 "$ROOT/packaging/third_party.py" > "$STAGE/licenses/THIRD_PARTY.md" # 4. Find lib/ next to bin/, not the build machine's wheel directory. -otool -l "$BIN" | awk '/cmd LC_RPATH/ {getline; getline; print $2}' | while read -r rpath; do +rpaths_of "$BIN" | while IFS= read -r rpath; do install_name_tool -delete_rpath "$rpath" "$BIN" done install_name_tool -add_rpath @executable_path/../lib "$BIN" @@ -66,7 +93,7 @@ codesign --force -s - "$BIN" # 5. Checks. bad=$(otool -L "$BIN" | tail -n +2 | awk '{print $1}' | grep -Ev '^(@rpath/|/usr/lib/|/System/)' || true) [ -z "$bad" ] || fail "the binary links outside the bundle and the OS: $bad" -rpaths=$(otool -l "$BIN" | awk '/cmd LC_RPATH/ {getline; getline; print $2}') +rpaths=$(rpaths_of "$BIN") [ "$rpaths" = "@executable_path/../lib" ] || fail "rpaths are: $rpaths" for f in "$BIN" "$STAGE/lib/libmlx.dylib" "$STAGE/lib/libjaccl.dylib"; do minos=$(vtool -show-build "$f" | awk '/minos/ {print $2; exit}') @@ -80,4 +107,4 @@ got=$("$BIN" --version) # 6. The tarball. macOS tar would also store this Mac's xattrs and AppleDouble ._ files. tar -C "$WORK" --no-mac-metadata --no-xattrs -czf "$OUT/$NAME.tar.gz" "$NAME" -echo "$OUT/$NAME.tar.gz $(du -h "$OUT/$NAME.tar.gz" | cut -f1)" +echo "$OUT/$NAME.tar.gz $(stat -f %z "$OUT/$NAME.tar.gz" | awk '{printf "%.1f MB", $1 / 1e6}')" diff --git a/packaging/licenses/NOTICE b/packaging/licenses/NOTICE new file mode 100644 index 0000000..68ae7fa --- /dev/null +++ b/packaging/licenses/NOTICE @@ -0,0 +1,35 @@ +lib/ holds libmlx.dylib, libjaccl.dylib and mlx.metallib from the mlx-metal 0.32.2 wheel on +PyPI, built from MLX 0.32.2 (https://github.com/ml-explore/mlx at +1f8e74e3f12f31365464a6867c6579f0e9b29d85), Copyright 2023 Apple Inc., licensed under the MIT +License (see licenses/MLX-LICENSE). MLX compiles the third-party code below into them. Each file +in licenses/ names its source and commit at the top. + + nlohmann/json 3.11.3, in libmlx.dylib and libjaccl.dylib. Copyright 2013-2022 Niels Lohmann, + MIT License, with parts by Björn Hoehrmann, Florian Loitsch and Evan Nemerson (MIT) and the + Abseil Authors (Apache License 2.0, see LICENSE). See licenses/nlohmann-json-3.11.3.txt. + + gguflib from gguf-tools (https://github.com/antirez/gguf-tools), in libmlx.dylib. + Copyright 2022 Georgi Gerganov, MIT License. See licenses/gguf-tools-8fa6eb6.txt. + + FP16 (https://github.com/Maratyszcza/FP16), the source of gguflib's fp16.c, in libmlx.dylib. + Copyright 2017 Facebook Inc., 2017 Georgia Institute of Technology and 2019 Google LLC, + MIT License. See licenses/fp16-ba1d31f.txt. + + metal-cpp 26 (https://developer.apple.com/metal/cpp/), in libmlx.dylib. Copyright 2024 Apple + Inc., Apache License 2.0. See licenses/metal-cpp-26.txt. + + PocketFFT, MLX's mlx/3rdparty/pocketfft.h, in libmlx.dylib. Copyright 2010-2022 + Max-Planck-Society, 2019-2020 Peter Bell, and for the DCT-IV code 2003 and 2007-2014 Matteo + Frigo and the Massachusetts Institute of Technology, BSD 3-Clause License. See + licenses/pocketfft-mlx-0.32.2.txt. + + SmallVector from V8, MLX's mlx/small_vector.h, in libmlx.dylib. Copyright 2018 the V8 project + authors, BSD 3-Clause License. See licenses/v8-small-vector-mlx-0.32.2.txt. + + expm1f by Norbert Juffa, MLX's mlx/backend/metal/kernels/expm1f.h, in mlx.metallib and as + kernel source in libmlx.dylib. Copyright 2015-2023 Norbert Juffa, BSD 2-Clause License. See + licenses/expm1f-mlx-0.32.2.txt. + + cexpf from NVIDIA Thrust, MLX's mlx/backend/metal/kernels/cexpf.h, in mlx.metallib and as + kernel source in libmlx.dylib. Copyright 2008-2013 NVIDIA Corporation and 2013 Filipe RNC Maia, + Apache License 2.0 (see LICENSE). See licenses/thrust-cexpf-mlx-0.32.2.txt. diff --git a/packaging/licenses/expm1f-mlx-0.32.2.txt b/packaging/licenses/expm1f-mlx-0.32.2.txt new file mode 100644 index 0000000..49e921e --- /dev/null +++ b/packaging/licenses/expm1f-mlx-0.32.2.txt @@ -0,0 +1,29 @@ +expm1f by Norbert Juffa, in MLX 0.32.2 as mlx/backend/metal/kernels/expm1f.h, compiled into +lib/mlx.metallib and kept as kernel source in lib/libmlx.dylib +Source: https://github.com/ml-explore/mlx/blob/1f8e74e3f12f31365464a6867c6579f0e9b29d85/mlx/backend/metal/kernels/expm1f.h, lines 8 to 32 + +Copyright (c) 2015-2023 Norbert Juffa +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packaging/licenses/fp16-ba1d31f.txt b/packaging/licenses/fp16-ba1d31f.txt new file mode 100644 index 0000000..a79710d --- /dev/null +++ b/packaging/licenses/fp16-ba1d31f.txt @@ -0,0 +1,15 @@ +FP16, the source of gguf-tools' fp16.c, in lib/libmlx.dylib +Source: https://github.com/Maratyszcza/FP16/blob/ba1d31f5eed2eb4a69e4dea3870a68c7c95f998f/LICENSE +Commit ba1d31f5eed2eb4a69e4dea3870a68c7c95f998f, the last change to LICENSE + +The MIT License (MIT) + +Copyright (c) 2017 Facebook Inc. +Copyright (c) 2017 Georgia Institute of Technology +Copyright 2019 Google LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packaging/licenses/gguf-tools-8fa6eb6.txt b/packaging/licenses/gguf-tools-8fa6eb6.txt new file mode 100644 index 0000000..57d1ab8 --- /dev/null +++ b/packaging/licenses/gguf-tools-8fa6eb6.txt @@ -0,0 +1,25 @@ +gguflib (gguflib.c and fp16.c from gguf-tools), in lib/libmlx.dylib +Source: https://github.com/antirez/gguf-tools/blob/8fa6eb65236618e28fd7710a0fba565f7faa1848/LICENSE +Commit 8fa6eb65236618e28fd7710a0fba565f7faa1848, pinned by MLX 0.32.2 mlx/io/CMakeLists.txt + +MIT License + +Copyright (c) 2022 Georgi Gerganov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packaging/licenses/metal-cpp-26.txt b/packaging/licenses/metal-cpp-26.txt new file mode 100644 index 0000000..f518ae2 --- /dev/null +++ b/packaging/licenses/metal-cpp-26.txt @@ -0,0 +1,207 @@ +metal-cpp 26, in lib/libmlx.dylib +Source: https://developer.apple.com/metal/cpp/files/metal-cpp_26.zip +(sha256 4df3c078b9aadcb516212e9cb03004cbc5ce9a3e9c068fa3144d021db585a3a4), file metal-cpp/LICENSE.txt +Pinned by MLX 0.32.2 CMakeLists.txt. The zip has no NOTICE file. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright © 2024 Apple Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packaging/licenses/nlohmann-json-3.11.3.txt b/packaging/licenses/nlohmann-json-3.11.3.txt new file mode 100644 index 0000000..062fa9d --- /dev/null +++ b/packaging/licenses/nlohmann-json-3.11.3.txt @@ -0,0 +1,33 @@ +nlohmann/json 3.11.3, in lib/libmlx.dylib and lib/libjaccl.dylib +Source: https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz +(sha256 d6c65aca6b1ed68e7a182f4757257b107ae403032760ed6ef121c9d55e81757d), file json/LICENSE.MIT +Tag v3.11.3, commit 9cca280a4d0ccf0c08f47a99aa71d1b0e52f8d03 + +MIT License + +Copyright (c) 2013-2022 Niels Lohmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +single_include/nlohmann/json.hpp also names these copyright holders for parts of it: + + 2008-2009 Björn Hoehrmann (UTF-8 decoder), MIT License as above + 2009 Florian Loitsch (Grisu2), MIT License as above + 2016-2021 Evan Nemerson (Hedley), MIT License as above + 2018 The Abseil Authors (absl/utility/utility.h), Apache License 2.0, in LICENSE at the top of this bundle diff --git a/packaging/licenses/pocketfft-mlx-0.32.2.txt b/packaging/licenses/pocketfft-mlx-0.32.2.txt new file mode 100644 index 0000000..8f97bec --- /dev/null +++ b/packaging/licenses/pocketfft-mlx-0.32.2.txt @@ -0,0 +1,38 @@ +PocketFFT, vendored in MLX 0.32.2 as mlx/3rdparty/pocketfft.h, in lib/libmlx.dylib +Source: https://github.com/ml-explore/mlx/blob/1f8e74e3f12f31365464a6867c6579f0e9b29d85/mlx/3rdparty/pocketfft.h, lines 2 to 36 + +This file is part of pocketfft. + +Copyright (C) 2010-2022 Max-Planck-Society +Copyright (C) 2019-2020 Peter Bell + +For the odd-sized DCT-IV transforms: + Copyright (C) 2003, 2007-14 Matteo Frigo + Copyright (C) 2003, 2007-14 Massachusetts Institute of Technology + +Authors: Martin Reinecke, Peter Bell + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. +* Neither the name of the copyright holder nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packaging/licenses/thrust-cexpf-mlx-0.32.2.txt b/packaging/licenses/thrust-cexpf-mlx-0.32.2.txt new file mode 100644 index 0000000..1422c9c --- /dev/null +++ b/packaging/licenses/thrust-cexpf-mlx-0.32.2.txt @@ -0,0 +1,23 @@ +cexpf from NVIDIA Thrust, in MLX 0.32.2 as mlx/backend/metal/kernels/cexpf.h, compiled into +lib/mlx.metallib and kept as kernel source in lib/libmlx.dylib +Source: https://github.com/ml-explore/mlx/blob/1f8e74e3f12f31365464a6867c6579f0e9b29d85/mlx/backend/metal/kernels/cexpf.h, lines 2 to 18 +Neither NVIDIA/cccl nor NVIDIA/thrust has a NOTICE file. The Apache License 2.0 text is in +LICENSE at the top of this bundle. + +Copyright © 2008-2013 NVIDIA Corporation +Copyright © 2013 Filipe RNC Maia + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Forked from +https://github.com/NVIDIA/cccl/blob/main/thrust/thrust/detail/complex/cexpf.h diff --git a/packaging/licenses/v8-small-vector-mlx-0.32.2.txt b/packaging/licenses/v8-small-vector-mlx-0.32.2.txt new file mode 100644 index 0000000..6ba0243 --- /dev/null +++ b/packaging/licenses/v8-small-vector-mlx-0.32.2.txt @@ -0,0 +1,30 @@ +SmallVector from V8, adapted in MLX 0.32.2 as mlx/small_vector.h, in lib/libmlx.dylib +Source: https://github.com/ml-explore/mlx/blob/1f8e74e3f12f31365464a6867c6579f0e9b29d85/mlx/small_vector.h, lines 2 to 28 + +Copyright © 2018 the V8 project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packaging/render_formula.sh b/packaging/render_formula.sh index 2a418b2..013fe88 100755 --- a/packaging/render_formula.sh +++ b/packaging/render_formula.sh @@ -12,6 +12,8 @@ VERSION=${VERSION%%-rc*} for s in "$S26" "$S14"; do [[ $s =~ ^[0-9a-f]{64}$ ]] || { echo "render_formula.sh: not a sha256: '$s'" >&2; exit 1; } done -sed -e "s|@VERSION@|$VERSION|g" -e "s|@BASE_URL@|$BASE|g" \ +# Escapes \, & and the | delimiter so sed puts the value in literally. +lit() { printf '%s' "$1" | sed -e 's/[\\&|]/\\&/g'; } +sed -e "s|@VERSION@|$(lit "$VERSION")|g" -e "s|@BASE_URL@|$(lit "$BASE")|g" \ -e "s|@SHA256_MACOS26@|$S26|g" -e "s|@SHA256_MACOS14@|$S14|g" \ "$(dirname "$0")/sys1rust.rb.in" diff --git a/packaging/rust-toolchain-version b/packaging/rust-toolchain-version new file mode 100644 index 0000000..55f6ae9 --- /dev/null +++ b/packaging/rust-toolchain-version @@ -0,0 +1 @@ +1.95.0 diff --git a/packaging/third_party.py b/packaging/third_party.py index 5e1a938..4443d0a 100755 --- a/packaging/third_party.py +++ b/packaging/third_party.py @@ -3,11 +3,15 @@ (normal dependencies on aarch64-apple-darwin) with its license expression and the license files in its source, then mlx-c, which mlx-sys builds and links in statically. +It runs `cargo metadata` only with the Rust version in packaging/rust-toolchain-version, the +one build.sh checks before it builds. + Usage: packaging/third_party.py > THIRD_PARTY.md """ import json import os import subprocess +import sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PREFIXES = ("LICENSE", "LICENCE", "COPYING", "NOTICE", "UNLICENSE") @@ -26,6 +30,13 @@ def license_files(d): def main(): + with open(os.path.join(ROOT, "packaging", "rust-toolchain-version")) as f: + rust = f.read().strip() + got = subprocess.check_output(["cargo", "--version"], text=True).split()[1] + if got != rust: + sys.exit(f"third_party.py: cargo is {got}, but the release build needs {rust} " + f"(packaging/rust-toolchain-version). Run 'rustup toolchain install {rust}' " + f"and build with RUSTUP_TOOLCHAIN={rust}.") meta = json.loads(subprocess.check_output([ "cargo", "metadata", "--format-version", "1", "--locked", "--manifest-path", os.path.join(ROOT, "runtime", "Cargo.toml"), From 48cad33da66806183770e32a5e79d67711b7ea78 Mon Sep 17 00:00:00 2001 From: Krish Garg Date: Mon, 5 Oct 2026 18:09:13 -0700 Subject: [PATCH 06/11] Require every reference label and guard startup signals in the smoke test malformed() now fails an answer whose probability map lacks a label from the reference's map. compare.py counts a missing label as 0, so dropping a low-probability label stayed under the drift limit. Extra labels still pass, as in compare.py. A SIGINT or SIGTERM that arrives while Popen starts the server now waits until `proc` holds the child, so the cleanup in `finally` stops it. The docstrings now name the actor. Co-Authored-By: Claude Opus 5.5 (1M context) --- packaging/smoke.py | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/packaging/smoke.py b/packaging/smoke.py index 09fdcc1..d6e728f 100755 --- a/packaging/smoke.py +++ b/packaging/smoke.py @@ -5,10 +5,11 @@ question's answer agrees unless the reference is a near tie, and probabilities are within MAX_DRIFT), then stops the server with SIGINT and requires exit code 0. -Every answer must also be well formed: the reference's type and every field the reference has, -so a near tie or a missing probability map can't hide a broken answer. A request that fails in -transport, runs past REQUEST_TIMEOUT_S or returns bad JSON fails the run and stops sending. The -server always gets stopped, also when this script gets SIGINT or SIGTERM. +Every answer must also be well formed: the reference's type, every field the reference has and +a probability for every label in the reference's map, so a near tie or a missing probability +can't hide a broken answer. A request that fails in transport, runs past REQUEST_TIMEOUT_S or +returns bad JSON fails the run and stops sending. The script always stops the server, also when +it gets SIGINT or SIGTERM. Run `SYS1RUST pull` first. Usage: packaging/smoke.py PATH_TO_SYS1RUST """ @@ -37,11 +38,20 @@ class Interrupted(Exception): - """Raised by the SIGINT and SIGTERM handlers so the server cleanup in `finally` runs.""" + """The SIGINT and SIGTERM handler raises this so the `finally` in main() stops the server.""" + + +# While Popen starts the server, on_signal only records the signal. main() raises it once `proc` +# holds the server, so the cleanup in `finally` can't miss a child that Popen already started. +launch = {"active": False, "pending": None} def on_signal(signum, _frame): - raise Interrupted(signal.Signals(signum).name) + name = signal.Signals(signum).name + if launch["active"]: + launch["pending"] = launch["pending"] or name + return + raise Interrupted(name) def is_number(v): @@ -67,6 +77,10 @@ def malformed(got, want): return "no probabilities" if not all(is_number(p) for p in probs.values()): return "a probability is not a number" + # compare.py counts a missing label as 0, which can keep a broken answer under MAX_DRIFT. + missing = sorted(set(labels) - set(probs)) + if missing: + return f"no probability for reference labels {missing}" return None @@ -166,7 +180,7 @@ def pump(): def stop(proc): - """Stops the server with SIGINT and returns its exit status, or a note if SIGKILL was needed.""" + """Stops the server with SIGINT and returns its exit status, or a note if it needed SIGKILL.""" if proc.poll() is not None: return proc.returncode proc.send_signal(signal.SIGINT) @@ -196,8 +210,14 @@ def main(): signal.signal(signal.SIGTERM, on_signal) proc, code = None, "not started" try: - proc = subprocess.Popen([exe, "serve", "--offline", "--model", MODEL, "--port", "0"], - stdout=subprocess.PIPE, text=True, env=env) + launch["active"] = True + try: + proc = subprocess.Popen([exe, "serve", "--offline", "--model", MODEL, "--port", "0"], + stdout=subprocess.PIPE, text=True, env=env) + finally: + launch["active"] = False + if launch["pending"]: + raise Interrupted(launch["pending"]) run(proc, reqs, ref, failures, stats) except Exception as e: failures.append(f"stopped early: {e!r}") From 8f4c01d340e1be626f7d41af2ed0a6fa3a5b48b3 Mon Sep 17 00:00:00 2001 From: Krish Garg Date: Mon, 5 Oct 2026 18:27:33 -0700 Subject: [PATCH 07/11] Add the release workflow and checklist A v* tag builds both bundles on macos-26 with the Rust in packaging/rust-toolchain-version, pulls typed-decisions and smoke-tests them on macOS 14, 15 and 26 runners, and publishes the release with SHA256SUMS. A final tag also pushes the rendered formula to krishhgg/homebrew-tap with TAP_TOKEN. An -rc tag publishes a prerelease only. Pull requests that touch packaging run the build and smoke jobs. The publish job clones the tap before it creates the release, so an empty or expired token stops it before anything goes public. A rerun keeps a release that holds the same bundles and skips the tap commit when the formula is unchanged. packaging/RELEASE.md is the checklist, including the lock file refresh after a version bump. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/release.yml | 191 ++++++++++++++++++++++++++++++++++ packaging/RELEASE.md | 71 +++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 packaging/RELEASE.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6ce0647 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,191 @@ +# On a v* tag, build both bundles, smoke-test them on macOS 14, 15 and 26 runners, publish the +# GitHub release and update Formula/sys1rust.rb in krishhgg/homebrew-tap. A vX.Y.Z-rcN tag +# publishes a prerelease and leaves the tap alone. packaging/RELEASE.md is the checklist. +# A pull request that changes packaging/ or this file runs the build and smoke jobs only. +name: release + +on: + push: + tags: ["v*"] + pull_request: + paths: + - "packaging/**" + - ".github/workflows/release.yml" + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +defaults: + run: + # Runs bash with -eo pipefail, so a failed command inside a pipe fails the step. + shell: bash + +jobs: + build: + runs-on: macos-26 + timeout-minutes: 90 + steps: + - uses: actions/checkout@v7 + - name: Check the tag against the workspace version + if: startsWith(github.ref, 'refs/tags/v') + run: | + # build.sh reads the version with the same sed, which matches only the + # [workspace.package] line. vX.Y.Z publishes that version and vX.Y.Z-rcN a prerelease of it. + version=$(sed -n 's/^version = "\(.*\)"$/\1/p' runtime/Cargo.toml) + re='^v([0-9]+\.[0-9]+\.[0-9]+)(-rc[0-9]+)?$' + if ! [[ $GITHUB_REF_NAME =~ $re ]] || [ "${BASH_REMATCH[1]}" != "$version" ]; then + echo "::error::Tag $GITHUB_REF_NAME does not match the workspace version $version. Tag v$version or v$version-rcN." + exit 1 + fi + - name: Install the pinned Rust + run: | + rust=$(cat packaging/rust-toolchain-version) + rustup toolchain install "$rust" --profile minimal + RUSTUP_TOOLCHAIN=$rust rustc --version + { + echo "RUSTUP_TOOLCHAIN=$rust" + echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" + } >> "$GITHUB_ENV" + - name: Cache the cargo registry + uses: actions/cache@v6 + with: + path: | + ${{ env.CARGO_HOME }}/registry/index + ${{ env.CARGO_HOME }}/registry/cache + key: cargo-registry-${{ hashFiles('runtime/Cargo.lock') }} + restore-keys: cargo-registry- + - name: Build the macOS 26 bundle + run: packaging/build.sh macos26 dist + - name: Build the macOS 14 bundle + run: packaging/build.sh macos14 dist + - uses: actions/upload-artifact@v7 + with: + name: bundles + path: dist/*.tar.gz + if-no-files-found: error + # The tarballs are gzip files already. + compression-level: 0 + + smoke: + needs: build + strategy: + fail-fast: false + matrix: + include: + # GitHub retires the macos-14 image on 2026-11-02 (actions/runner-images#13518). + - { runner: macos-14, flavor: macos14 } + - { runner: macos-15, flavor: macos14 } + - { runner: macos-26, flavor: macos26 } + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 + env: + FLAVOR: ${{ matrix.flavor }} + HF_HUB_CACHE: ${{ github.workspace }}/.hf-cache + steps: + - uses: actions/checkout@v7 + - uses: actions/download-artifact@v8 + with: + name: bundles + path: dist + - name: Unpack and run through a symlink, as Homebrew does + run: | + mkdir -p "$RUNNER_TEMP/pkg" "$RUNNER_TEMP/bin" + tar -xzf dist/sys1rust-*-"$FLAVOR"-arm64.tar.gz -C "$RUNNER_TEMP/pkg" + ln -s "$RUNNER_TEMP"/pkg/sys1rust-*/bin/sys1rust "$RUNNER_TEMP/bin/sys1rust" + echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" + - name: Version and model list + run: sw_vers && sys1rust --version && sys1rust models + - name: Cache the typed-decisions snapshot + uses: actions/cache@v6 + with: + path: .hf-cache + # The typed-decisions revision in bench/models.lock.json, which sys1rust pins. + key: hf-typed-decisions-1a793eb568e6718f15941d08f85432581df534e3 + - name: Pull typed-decisions + run: sys1rust pull typed-decisions + - name: Smoke test + run: python3 packaging/smoke.py "$(command -v sys1rust)" + + publish: + if: startsWith(github.ref, 'refs/tags/v') + needs: smoke + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + env: + GH_REPO: ${{ github.repository }} + steps: + - uses: actions/checkout@v7 + with: + # gh reads GH_TOKEN and the tap clone has its own helper, so git keeps no token here. + persist-credentials: false + - uses: actions/download-artifact@v8 + with: + name: bundles + path: dist + - name: Checksums + run: cd dist && sha256sum sys1rust-*.tar.gz > SHA256SUMS && cat SHA256SUMS + - name: Clone the Homebrew tap + if: ${{ !contains(github.ref_name, '-rc') }} + env: + TAP_TOKEN: ${{ secrets.TAP_TOKEN }} + run: | + if [ -z "$TAP_TOKEN" ]; then + echo "::error::The TAP_TOKEN secret is empty. Set it to a token that can write to krishhgg/homebrew-tap." + exit 1 + fi + # Git asks this credential helper for the password, and the helper reads TAP_TOKEN from + # the environment. The token stays out of the clone URL, .git/config, every command line + # and the log. The empty helper first drops any helper the runner set up. The clone runs + # before the release exists, so an expired token stops the job before anything is published. + # shellcheck disable=SC2016 + git clone --depth 1 -c credential.helper= \ + -c 'credential.helper=!f() { echo username=x-access-token; echo "password=$TAP_TOKEN"; }; f' \ + https://github.com/krishhgg/homebrew-tap.git "$RUNNER_TEMP/tap" + - name: GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: | + tag=$GITHUB_REF_NAME + # A rerun of this job finds the release that the first attempt made. It goes on only if + # the release holds these same bundles, so the formula's hashes match what users download. + if gh release view "$tag" > /dev/null 2>&1; then + gh release download "$tag" --pattern SHA256SUMS --output "$RUNNER_TEMP/SHA256SUMS.published" + if ! cmp -s dist/SHA256SUMS "$RUNNER_TEMP/SHA256SUMS.published"; then + echo "::error::Release $tag already has other bundles. Rerun only the failed jobs, or delete the release first." + exit 1 + fi + echo "Release $tag already has these bundles" + exit 0 + fi + flags=() + case "$tag" in *-rc*) flags+=(--prerelease) ;; esac + gh release create "$tag" dist/sys1rust-*.tar.gz dist/SHA256SUMS --verify-tag \ + --title "sys1rust ${tag#v}" --generate-notes "${flags[@]}" + - name: Update the Homebrew formula + if: ${{ !contains(github.ref_name, '-rc') }} + env: + TAP_TOKEN: ${{ secrets.TAP_TOKEN }} + run: | + version=${GITHUB_REF_NAME#v} + sha() { awk -v f="sys1rust-$version-$1-arm64.tar.gz" '$2 == f {print $1}' dist/SHA256SUMS; } + mkdir -p "$RUNNER_TEMP/tap/Formula" + packaging/render_formula.sh "$GITHUB_REF_NAME" "$(sha macos26)" "$(sha macos14)" \ + > "$RUNNER_TEMP/tap/Formula/sys1rust.rb" + cd "$RUNNER_TEMP/tap" + git add Formula/sys1rust.rb + # A rerun after a successful push finds the formula unchanged. That is the wanted state, + # so the step succeeds without a commit. + if git diff --cached --quiet; then + echo "Formula/sys1rust.rb already describes $GITHUB_REF_NAME" + exit 0 + fi + git -c user.name="github-actions[bot]" \ + -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \ + commit -m "sys1rust $version" + git push origin HEAD diff --git a/packaging/RELEASE.md b/packaging/RELEASE.md new file mode 100644 index 0000000..dfc0cd6 --- /dev/null +++ b/packaging/RELEASE.md @@ -0,0 +1,71 @@ +# Releasing sys1rust + +Pushing a `v*` tag runs `.github/workflows/release.yml`. It builds both bundles on a macOS 26 runner, smoke-tests them on macOS 14, 15 and 26 runners and publishes the GitHub release with `SHA256SUMS`. A final `vX.Y.Z` tag also updates `Formula/sys1rust.rb` in krishhgg/homebrew-tap. A `vX.Y.Z-rcN` tag publishes a prerelease and leaves the tap alone. A pull request that changes `packaging/` or the workflow runs the build and smoke jobs only. + +Run every command below from the repository root on an Apple silicon Mac with macOS 26.2 or later. Use 1 shell for all of them, because later steps reuse `version` and `RUSTUP_TOOLCHAIN`. The tests, the smoke runs and the Homebrew service load a model on the GPU. Run each of them alone, with no other GPU work on the Mac. + +1. Install the pinned Rust and select it for this shell. `packaging/build.sh` and `packaging/third_party.py` stop when `rustc` or `cargo` reports another version. + ```sh + rust=$(cat packaging/rust-toolchain-version) + rustup toolchain install "$rust" + export RUSTUP_TOOLCHAIN=$rust + ``` +2. Set the version in the `[workspace.package]` table of `runtime/Cargo.toml` and update `runtime/Cargo.lock` to match. build.sh builds with `--locked`, so a stale lock file fails the release build. + ```sh + cargo update --workspace --manifest-path runtime/Cargo.toml + ``` + Merge both files to main. Then switch to main and read the version back. + ```sh + git switch main && git pull --ff-only + version=$(sed -n 's/^version = "\(.*\)"$/\1/p' runtime/Cargo.toml) + echo "$version" + ``` +3. Build both bundles, run the strict suite against the macOS 26 MLX build, and smoke-test both bundles. The tests read the pinned snapshots from the Hugging Face cache, so pull all 3 models first. Every command must pass. The laya-mlx `settings` tests take about 14 minutes. + ```sh + packaging/build.sh macos26 dist && packaging/build.sh macos14 dist + for m in typed-decisions multilingual english; do + packaging/.work/macos26/sys1rust-$version-macos26-arm64/bin/sys1rust pull $m + done + export MLX_SYS_PREBUILT_DIR=$PWD/packaging/.work/macos26/wheel/mlx + cargo test --release --manifest-path runtime/Cargo.toml --workspace + for t in settings loading reference; do + SYS1_TEST_ALL_CHECKPOINTS=1 cargo test --release --manifest-path runtime/Cargo.toml \ + -p laya-mlx --test $t -- --ignored --test-threads 1 + done + cargo test --release --manifest-path runtime/Cargo.toml -p sys1rust --test live -- --ignored + cargo test --release --manifest-path runtime/Cargo.toml -p sys1rust --test live_pull -- --ignored + for fl in macos26 macos14; do + python3 packaging/smoke.py packaging/.work/$fl/sys1rust-$version-$fl-arm64/bin/sys1rust + done + ``` +4. Tag a release candidate and push the tag. The workflow builds and smoke-tests the bundles and publishes a prerelease. It leaves the tap alone. Follow the run on the repository's Actions page. If a job fails, fix the cause on main and tag the next candidate, `v$version-rc2`. Until GitHub retires the macOS 14 image on 2026-11-02, it fails `macos-14` jobs during scheduled brownouts (actions/runner-images#13518). Rerun a job that failed that way. + ```sh + git tag "v$version-rc1" && git push origin "v$version-rc1" + ``` +5. Install the candidate through Homebrew from a throwaway local tap. `packaging/render_formula.sh` writes the formula for the prerelease with the hashes from its `SHA256SUMS`. The first service start downloads typed-decisions (846 MB) if `~/.cache/huggingface/hub` does not hold it yet. If `curl` never answers, read `$(brew --prefix)/var/log/sys1rust.log`. + ```sh + sums=$(gh release download "v$version-rc1" --repo krishhgg/sys1rust --pattern SHA256SUMS --output -) + sha() { printf '%s\n' "$sums" | awk -v f="sys1rust-$version-$1-arm64.tar.gz" '$2 == f {print $1}'; } + brew tap-new --no-git local/sys1rust-test + packaging/render_formula.sh "v$version-rc1" "$(sha macos26)" "$(sha macos14)" \ + > "$(brew --repository local/sys1rust-test)/Formula/sys1rust.rb" + HOMEBREW_NO_AUTO_UPDATE=1 brew install local/sys1rust-test/sys1rust + brew test local/sys1rust-test/sys1rust + brew services start local/sys1rust-test/sys1rust + until curl -fsS 127.0.0.1:8000/health; do sleep 2; done + brew services stop local/sys1rust-test/sys1rust + brew uninstall sys1rust && brew untap local/sys1rust-test + ``` +6. Tag the release and push the tag. The workflow publishes the release and pushes `Formula/sys1rust.rb` to krishhgg/homebrew-tap with the `TAP_TOKEN` secret, a fine-grained token with contents read and write on that repo only. The publish job clones the tap before it creates the release, so an empty or expired token stops the job before anything goes public. + ```sh + git tag "v$version" && git push origin "v$version" + ``` +7. On a clean account, or after `brew uninstall sys1rust`, check that `brew install krishhgg/tap/sys1rust`, `sys1rust serve` and `brew services start sys1rust` work. + +## If the tap update fails + +If the publish job fails at the tap because `TAP_TOKEN` expired or lost access, make a new fine-grained token with contents read and write on krishhgg/homebrew-tap only. Store it with the command below, which prompts for the value. Then open the failed run on the Actions page and choose "Re-run failed jobs". The rerun finds the release from the first attempt with the same bundles, keeps it and pushes the formula. If the formula is already up to date, the job finishes without a commit. "Re-run all jobs" builds new bundles, and the publish job then stops rather than replace the published files. + +```sh +gh secret set TAP_TOKEN --repo krishhgg/sys1rust +``` From 9bb594334a3f836b8a617f122a498611e6175f49 Mon Sep 17 00:00:00 2001 From: Krish Garg Date: Mon, 5 Oct 2026 18:37:07 -0700 Subject: [PATCH 08/11] Ship notices for the remaining code MLX adapts lib/ carries more adapted third-party code than the first inventory listed. packaging/licenses now holds pinned texts for 8 more sources, packaging/licenses/NOTICE names each one, and build.sh fails if any is missing. - erf and erfinv by njuffa on Stack Overflow, in Metal erf.h and the CPU erfinv. MLX's constants match answer 35148199 revision 4 (2022) and answer 49743348 revision 3 (2021), both CC BY-SA 4.0 with no separate license. - PyTorch, for the fp8 e4m3 conversion in Metal fp8.h and CPU unary_ops.h and for the CPU erf. - supervised-lda's NumpyFormat.hpp, for the .npy reader and writer in io/load.cpp. - Julien Pommier's neon_mathfun.h (zlib) with simd_utils' FMA change (BSD 2-Clause), for the CPU sincos, which libmlx.dylib also keeps as C++ source in its CPU JIT preamble. - fastermath, for the CPU exp. - NumPy, TensorFlow and JAX, whose einsum path search, cumulative logsumexp gradient and truncated normal sampler MLX translates. NOTICE ends with what needs no notice and why. fmt, ThreadPool and Cephes are listed there, along with the papers and algorithms MLX implements in its own code. Co-Authored-By: Claude Opus 5.5 (1M context) --- packaging/build.sh | 10 +- packaging/licenses/NOTICE | 49 +++++++++- packaging/licenses/fastermath-2ed806f.txt | 32 +++++++ packaging/licenses/jax-5942e15.txt | 19 ++++ packaging/licenses/numpy-1d49c7f.txt | 34 +++++++ packaging/licenses/pytorch-e3643e1.txt | 91 +++++++++++++++++++ packaging/licenses/simd-utils-3c1433a.txt | 55 +++++++++++ .../licenses/stackoverflow-erf-erfinv.txt | 25 +++++ packaging/licenses/supervised-lda-fe3a39b.txt | 26 ++++++ packaging/licenses/tensorflow-2a59109.txt | 19 ++++ 10 files changed, 354 insertions(+), 6 deletions(-) create mode 100644 packaging/licenses/fastermath-2ed806f.txt create mode 100644 packaging/licenses/jax-5942e15.txt create mode 100644 packaging/licenses/numpy-1d49c7f.txt create mode 100644 packaging/licenses/pytorch-e3643e1.txt create mode 100644 packaging/licenses/simd-utils-3c1433a.txt create mode 100644 packaging/licenses/stackoverflow-erf-erfinv.txt create mode 100644 packaging/licenses/supervised-lda-fe3a39b.txt create mode 100644 packaging/licenses/tensorflow-2a59109.txt diff --git a/packaging/build.sh b/packaging/build.sh index 477b15c..51f717a 100755 --- a/packaging/build.sh +++ b/packaging/build.sh @@ -13,12 +13,14 @@ rpaths_of() { otool -l "$1" | awk '/cmd LC_RPATH/ {getline; getline; sub(/^ *path /, ""); sub(/ \(offset [0-9]+\)$/, ""); print}' } -# Third-party code that MLX 0.32.2 compiles into lib/ and whose license asks for its notice in -# binary copies. packaging/licenses/NOTICE describes each one. fmt 12.1.0 (MIT with an exception -# for object code) and ThreadPool (zlib, notice for source copies only) need none. +# Third-party code that MLX 0.32.2 builds into lib/, as is or adapted, and whose license asks for +# its notice or attribution in lib/'s copies. packaging/licenses/NOTICE describes each one and, +# at its end, what needs none. MLX_THIRD_PARTY=(nlohmann-json-3.11.3.txt gguf-tools-8fa6eb6.txt fp16-ba1d31f.txt metal-cpp-26.txt pocketfft-mlx-0.32.2.txt v8-small-vector-mlx-0.32.2.txt expm1f-mlx-0.32.2.txt - thrust-cexpf-mlx-0.32.2.txt) + thrust-cexpf-mlx-0.32.2.txt stackoverflow-erf-erfinv.txt pytorch-e3643e1.txt + supervised-lda-fe3a39b.txt simd-utils-3c1433a.txt fastermath-2ed806f.txt numpy-1d49c7f.txt + tensorflow-2a59109.txt jax-5942e15.txt) FLAVOR=${1:-} OUT_ARG=${2:-} diff --git a/packaging/licenses/NOTICE b/packaging/licenses/NOTICE index 68ae7fa..431dbed 100644 --- a/packaging/licenses/NOTICE +++ b/packaging/licenses/NOTICE @@ -1,8 +1,8 @@ lib/ holds libmlx.dylib, libjaccl.dylib and mlx.metallib from the mlx-metal 0.32.2 wheel on PyPI, built from MLX 0.32.2 (https://github.com/ml-explore/mlx at 1f8e74e3f12f31365464a6867c6579f0e9b29d85), Copyright 2023 Apple Inc., licensed under the MIT -License (see licenses/MLX-LICENSE). MLX compiles the third-party code below into them. Each file -in licenses/ names its source and commit at the top. +License (see licenses/MLX-LICENSE). MLX builds the third-party code below, or its own adaptations +of it, into them. Each file in licenses/ names its source and commit at the top. nlohmann/json 3.11.3, in libmlx.dylib and libjaccl.dylib. Copyright 2013-2022 Niels Lohmann, MIT License, with parts by Björn Hoehrmann, Florian Loitsch and Evan Nemerson (MIT) and the @@ -33,3 +33,48 @@ in licenses/ names its source and commit at the top. cexpf from NVIDIA Thrust, MLX's mlx/backend/metal/kernels/cexpf.h, in mlx.metallib and as kernel source in libmlx.dylib. Copyright 2008-2013 NVIDIA Corporation and 2013 Filipe RNC Maia, Apache License 2.0 (see LICENSE). See licenses/thrust-cexpf-mlx-0.32.2.txt. + + erf and erfinv by njuffa (Norbert Juffa) on Stack Overflow, MLX's + mlx/backend/metal/kernels/erf.h and the erfinv in mlx/backend/cpu/simd/math.h, in mlx.metallib + and libmlx.dylib, which also keeps them as source. Creative Commons Attribution-ShareAlike 4.0 + International License (CC BY-SA 4.0). MLX modified both. See + licenses/stackoverflow-erf-erfinv.txt. + + PyTorch (https://github.com/pytorch/pytorch), the source of MLX's float to fp8 e4m3 conversion + in mlx/backend/metal/kernels/fp8.h and mlx/backend/cpu/unary_ops.h and of its CPU erf in + mlx/backend/cpu/simd/math.h, in mlx.metallib and libmlx.dylib, which also keeps them as source. + Copyright Facebook Inc. and the other holders it lists, BSD 3-Clause License. See + licenses/pytorch-e3643e1.txt. + + NumpyFormat.hpp from supervised-lda (https://github.com/angeloskath/supervised-lda), the source + of MLX's .npy reader and writer in mlx/io/load.cpp, in libmlx.dylib. Copyright 2016 Angelos + Katharopoulos and Despoina Paschalidou, MIT License. See licenses/supervised-lda-fe3a39b.txt. + + neon_mathfun.h by Julien Pommier, from simd_utils (https://github.com/JishinMaster/simd_utils), + the source of MLX's CPU sincos in mlx/backend/cpu/simd/math.h, in libmlx.dylib as object code + and as C++ source in its CPU JIT preamble. Copyright 2011 Julien Pommier, zlib License, and 2019 + JishinMaster, BSD 2-Clause License. MLX modified it. See licenses/simd-utils-3c1433a.txt. + + fastermath (https://github.com/akohlmey/fastermath), the source of MLX's CPU exp in + mlx/backend/cpu/simd/math.h, in libmlx.dylib as object code and as C++ source in its CPU JIT + preamble. Copyright 2012-2013 Axel Kohlmeyer, BSD 3-Clause License. See + licenses/fastermath-2ed806f.txt. + + NumPy (https://github.com/numpy/numpy), whose einsum path search MLX translates in + mlx/einsum.cpp, in libmlx.dylib. Copyright 2005-2024 NumPy Developers, BSD 3-Clause License. + See licenses/numpy-1d49c7f.txt. + + TensorFlow (https://github.com/tensorflow/tensorflow), whose CumulativeLogsumexp gradient MLX + translates in mlx/primitives.cpp, in libmlx.dylib. Copyright 2015 The TensorFlow Authors, + Apache License 2.0 (see LICENSE). See licenses/tensorflow-2a59109.txt. + + JAX (https://github.com/jax-ml/jax), whose truncated_normal MLX translates in mlx/random.cpp, + in libmlx.dylib. Copyright 2018 The JAX Authors, Apache License 2.0 (see LICENSE). See + licenses/jax-5942e15.txt. + +lib/ also holds code that needs no notice here. fmt 12.1.0 is MIT with an exception for object +code. ThreadPool (https://github.com/progschj/ThreadPool) is zlib, which asks for its notice in +source copies only, and lib/ holds it as object code only. Cephes reaches MLX only through +fastermath and Julien Pommier's code, and its terms ask for no notice. MLX also cites papers, +documentation, math results and algorithms that it implements in code of its own, such as +Threefry, CUB's merge sort, VkFFT's Stockham FFT and Neil Sloane's table of Hadamard matrices. diff --git a/packaging/licenses/fastermath-2ed806f.txt b/packaging/licenses/fastermath-2ed806f.txt new file mode 100644 index 0000000..0d8eac3 --- /dev/null +++ b/packaging/licenses/fastermath-2ed806f.txt @@ -0,0 +1,32 @@ +fastermath, the source of MLX 0.32.2's exp in mlx/backend/cpu/simd/math.h, in lib/libmlx.dylib as +object code and as C++ source in its CPU JIT preamble +Source: https://github.com/akohlmey/fastermath/blob/2ed806f45871129c9ccddc63422370641af9f14d/src/exp.c, lines 2 to 27 +Commit 2ed806f45871129c9ccddc63422370641af9f14d, the head of master, which MLX cites. The repository +has no LICENSE file. exp.c carries the notice below. + + Copyright (c) 2012,2013 Axel Kohlmeyer + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packaging/licenses/jax-5942e15.txt b/packaging/licenses/jax-5942e15.txt new file mode 100644 index 0000000..b363948 --- /dev/null +++ b/packaging/licenses/jax-5942e15.txt @@ -0,0 +1,19 @@ +JAX, whose truncated_normal MLX 0.32.2 translates in mlx/random.cpp, in lib/libmlx.dylib +Source: https://github.com/jax-ml/jax/blob/5942e1580e1a2f974492cc6024ce06448ee4b01c/jax/_src/random.py, lines 1 to 13 +Tag jax-v0.4.21 (2023-12-04), commit 5942e1580e1a2f974492cc6024ce06448ee4b01c. MLX cites JAX's +documentation without a commit. This release's _truncated_normal matches MLX's. JAX has no NOTICE +file at that commit. The Apache License 2.0 text is in LICENSE at the top of this bundle. + +Copyright 2018 The JAX Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packaging/licenses/numpy-1d49c7f.txt b/packaging/licenses/numpy-1d49c7f.txt new file mode 100644 index 0000000..21c5e35 --- /dev/null +++ b/packaging/licenses/numpy-1d49c7f.txt @@ -0,0 +1,34 @@ +NumPy, whose einsum path search MLX 0.32.2 translates in mlx/einsum.cpp, in lib/libmlx.dylib +Source: https://github.com/numpy/numpy/blob/1d49c7f7ff527c696fc26ab2278ad51632a66660/LICENSE.txt +Commit 1d49c7f7ff527c696fc26ab2278ad51632a66660, cited by MLX's einsum.cpp + +Copyright (c) 2005-2024, NumPy Developers. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the NumPy Developers nor the names of any + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packaging/licenses/pytorch-e3643e1.txt b/packaging/licenses/pytorch-e3643e1.txt new file mode 100644 index 0000000..63da153 --- /dev/null +++ b/packaging/licenses/pytorch-e3643e1.txt @@ -0,0 +1,91 @@ +PyTorch, the source of MLX 0.32.2's float to fp8 e4m3 conversion (mlx/backend/metal/kernels/fp8.h +and mlx/backend/cpu/unary_ops.h) and of its CPU erf (mlx/backend/cpu/simd/math.h), in +lib/mlx.metallib and lib/libmlx.dylib, which also keeps them as source +Source: https://github.com/pytorch/pytorch/blob/e3643e1e0e923f0fc063dfab6f45c956d568919d/LICENSE +Commit e3643e1e0e923f0fc063dfab6f45c956d568919d, cited by MLX's fp8.h. LICENSE is the same at +abf28982a8cb43342e7669d859de9543fd804cc9, the commit MLX's CPU erf cites. + +From PyTorch: + +Copyright (c) 2016- Facebook, Inc (Adam Paszke) +Copyright (c) 2014- Facebook, Inc (Soumith Chintala) +Copyright (c) 2011-2014 Idiap Research Institute (Ronan Collobert) +Copyright (c) 2012-2014 Deepmind Technologies (Koray Kavukcuoglu) +Copyright (c) 2011-2012 NEC Laboratories America (Koray Kavukcuoglu) +Copyright (c) 2011-2013 NYU (Clement Farabet) +Copyright (c) 2006-2010 NEC Laboratories America (Ronan Collobert, Leon Bottou, Iain Melvin, Jason Weston) +Copyright (c) 2006 Idiap Research Institute (Samy Bengio) +Copyright (c) 2001-2004 Idiap Research Institute (Ronan Collobert, Samy Bengio, Johnny Mariethoz) + +From Caffe2: + +Copyright (c) 2016-present, Facebook Inc. All rights reserved. + +All contributions by Facebook: +Copyright (c) 2016 Facebook Inc. + +All contributions by Google: +Copyright (c) 2015 Google Inc. +All rights reserved. + +All contributions by Yangqing Jia: +Copyright (c) 2015 Yangqing Jia +All rights reserved. + +All contributions by Kakao Brain: +Copyright 2019-2020 Kakao Brain + +All contributions by Cruise LLC: +Copyright (c) 2022 Cruise LLC. +All rights reserved. + +All contributions by Tri Dao: +Copyright (c) 2024 Tri Dao. +All rights reserved. + +All contributions by Arm: +Copyright (c) 2021, 2023-2024 Arm Limited and/or its affiliates + +All contributions from Caffe: +Copyright(c) 2013, 2014, 2015, the respective contributors +All rights reserved. + +All other contributions: +Copyright(c) 2015, 2016 the respective contributors +All rights reserved. + +Caffe2 uses a copyright model similar to Caffe: each contributor holds +copyright over their contributions to Caffe2. The project versioning records +all such contribution and copyright details. If a contributor wants to further +mark their specific copyright on a particular contribution, they should +indicate their copyright solely in the commit message of the change when it is +committed. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the names of Facebook, Deepmind Technologies, NYU, NEC Laboratories America + and IDIAP Research Institute nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/packaging/licenses/simd-utils-3c1433a.txt b/packaging/licenses/simd-utils-3c1433a.txt new file mode 100644 index 0000000..a849451 --- /dev/null +++ b/packaging/licenses/simd-utils-3c1433a.txt @@ -0,0 +1,55 @@ +neon_mathfun.h from simd_utils, the source of MLX 0.32.2's sincos in mlx/backend/cpu/simd/math.h, +in lib/libmlx.dylib as object code and as C++ source in its CPU JIT preamble. MLX modified it. +Source: https://github.com/JishinMaster/simd_utils/blob/3c1433a86fb38edcc9b02039f3c9a65b16640976/neon_mathfun.h, lines 7 to 26, +and https://github.com/JishinMaster/simd_utils/blob/3c1433a86fb38edcc9b02039f3c9a65b16640976/LICENSE +Commit 3c1433a86fb38edcc9b02039f3c9a65b16640976, pinned by MLX's simd/math.h. neon_mathfun.h carries +Julien Pommier's zlib notice. MLX took the FMA version that simd_utils added, so the simd_utils +BSD 2-Clause License follows the zlib notice. + +Copyright (C) 2011 Julien Pommier + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + (this is the zlib license) + +The simd_utils LICENSE. + +BSD 2-Clause License + +Copyright (c) 2019, JishinMaster +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packaging/licenses/stackoverflow-erf-erfinv.txt b/packaging/licenses/stackoverflow-erf-erfinv.txt new file mode 100644 index 0000000..ecd5a58 --- /dev/null +++ b/packaging/licenses/stackoverflow-erf-erfinv.txt @@ -0,0 +1,25 @@ +erf and erfinv by njuffa (Norbert Juffa) on Stack Overflow, adapted in MLX 0.32.2 as +mlx/backend/metal/kernels/erf.h and as the erfinv in mlx/backend/cpu/simd/math.h, in +lib/mlx.metallib and lib/libmlx.dylib, which also keeps them as source +Source: https://github.com/ml-explore/mlx/blob/1f8e74e3f12f31365464a6867c6579f0e9b29d85/mlx/backend/metal/kernels/erf.h +MLX cites the erf answer only. Its erfinv constants match the second answer below. + +erf is my_erff from the answer by njuffa (https://stackoverflow.com/users/780717/njuffa) to +"Efficient faithfully-rounded implementation of error function erff()", +https://stackoverflow.com/a/35148199. MLX's constants match revision 4 of 2022-06-18, +https://stackoverflow.com/revisions/35148199/4. + +erfinv is my_erfinvf from the answer by njuffa to "Inverse Error Function in C", +https://stackoverflow.com/a/49743348. MLX's constants match revision 3 of 2021-06-10, +https://stackoverflow.com/revisions/49743348/3. + +Stack Overflow licenses both revisions under the Creative Commons Attribution-ShareAlike 4.0 +International License (CC BY-SA 4.0), https://creativecommons.org/licenses/by-sa/4.0/legalcode. +Neither answer carries a license of its own. + +MLX modified both. It ported them from C to Metal and to its C++ SIMD types, computes erf's tail +as -expm1f(r) in place of the answer's expf or expm1f paths, calls the platform log in erfinv and +drops the answers' helper functions. + +The licensor offers this material as-is and makes no warranties, as Section 5 of CC BY-SA 4.0 +states. diff --git a/packaging/licenses/supervised-lda-fe3a39b.txt b/packaging/licenses/supervised-lda-fe3a39b.txt new file mode 100644 index 0000000..39f5aa5 --- /dev/null +++ b/packaging/licenses/supervised-lda-fe3a39b.txt @@ -0,0 +1,26 @@ +NumpyFormat.hpp from supervised-lda, the source of MLX 0.32.2's .npy reader and writer in +mlx/io/load.cpp, in lib/libmlx.dylib +Source: https://github.com/angeloskath/supervised-lda/blob/fe3a39bb0d6c7d0c2a33f069440869ad70774da8/LICENSE +Commit fe3a39bb0d6c7d0c2a33f069440869ad70774da8, the head of master, which MLX cites + +The MIT License (MIT) + +Copyright (c) 2016 Angelos Katharopoulos & Despoina Paschalidou + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packaging/licenses/tensorflow-2a59109.txt b/packaging/licenses/tensorflow-2a59109.txt new file mode 100644 index 0000000..e578cc2 --- /dev/null +++ b/packaging/licenses/tensorflow-2a59109.txt @@ -0,0 +1,19 @@ +TensorFlow, whose CumulativeLogsumexp gradient MLX 0.32.2 translates in mlx/primitives.cpp, in +lib/libmlx.dylib +Source: https://github.com/tensorflow/tensorflow/blob/2a5910906a0e0f3dbc186ff9db6386d81a63448c/tensorflow/python/ops/math_grad.py, lines 1 to 13 +Commit 2a5910906a0e0f3dbc186ff9db6386d81a63448c, cited by MLX's primitives.cpp. TensorFlow has no +NOTICE file at that commit. The Apache License 2.0 text is in LICENSE at the top of this bundle. + +Copyright 2015 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. From 4f66a55115272c6c218811a7c948468665a8c67d Mon Sep 17 00:00:00 2001 From: Krish Garg Date: Mon, 5 Oct 2026 21:12:25 -0700 Subject: [PATCH 09/11] Harden the release publish step and drop the macOS 14 runner Smoke-test the macOS 14 bundle on macos-15 only. GitHub retires the macos-14 image on 2026-11-02 and its brownouts already fail jobs. macos-15 runs that bundle on an OS older than 26.2, the case it exists for, and the macOS 26 bundle stays on macos-26. The publish job now: - deletes, by id, every draft release of the tag that a cancelled gh release create left, then creates the release again. A rerun accepts a published release only if both bundles and SHA256SUMS match this run's files. - runs git push --dry-run against the tap before it creates the release. The public tap clones without the token, so the clone never checked it. The dry run asks GitHub for git-receive-pack, which GitHub serves only to a token it accepts with push access to the tap. It sends no commits and does not check branch rules. - leaves the tap alone with a notice when its formula has a newer version (sort -V), so a retried older tag cannot downgrade it. Every tag's publish job shares the tap-update concurrency group. RELEASE.md pulls the 3 pinned models into a fresh HF_HUB_CACHE under packaging/.work before the strict suite. The laya-mlx tests load the snapshot that refs/main names, else the only cached one, and sys1rust pull writes no refs/main, so a shared cache could test another revision. The candidate tag lives in rc, so tagging, the SHA256SUMS download and the test formula all follow a bump to rc2. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/release.yml | 69 +++++++++++++++++++++++++++-------- packaging/RELEASE.md | 25 ++++++++----- 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6ce0647..baf7602 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -# On a v* tag, build both bundles, smoke-test them on macOS 14, 15 and 26 runners, publish the +# On a v* tag, build both bundles, smoke-test them on macOS 15 and 26 runners, publish the # GitHub release and update Formula/sys1rust.rb in krishhgg/homebrew-tap. A vX.Y.Z-rcN tag # publishes a prerelease and leaves the tap alone. packaging/RELEASE.md is the checklist. # A pull request that changes packaging/ or this file runs the build and smoke jobs only. @@ -76,8 +76,8 @@ jobs: fail-fast: false matrix: include: - # GitHub retires the macos-14 image on 2026-11-02 (actions/runner-images#13518). - - { runner: macos-14, flavor: macos14 } + # macos-15 runs the macOS 14 bundle on an OS older than 26.2, the case that bundle exists for. + # There is no macos-14 leg. GitHub retires that image on 2026-11-02 (actions/runner-images#13518). - { runner: macos-15, flavor: macos14 } - { runner: macos-26, flavor: macos26 } runs-on: ${{ matrix.runner }} @@ -115,6 +115,12 @@ jobs: needs: smoke runs-on: ubuntu-latest timeout-minutes: 15 + # Every tag's publish job joins this one group, so 2 tags never write the tap at the same + # time. GitHub runs 1 job of a group and holds 1 more. A third tag's job cancels the held + # one, and that tag's publish job then needs a rerun. + concurrency: + group: tap-update + cancel-in-progress: false permissions: contents: write env: @@ -130,7 +136,7 @@ jobs: path: dist - name: Checksums run: cd dist && sha256sum sys1rust-*.tar.gz > SHA256SUMS && cat SHA256SUMS - - name: Clone the Homebrew tap + - name: Clone the Homebrew tap and check that TAP_TOKEN can push to it if: ${{ !contains(github.ref_name, '-rc') }} env: TAP_TOKEN: ${{ secrets.TAP_TOKEN }} @@ -141,26 +147,45 @@ jobs: fi # Git asks this credential helper for the password, and the helper reads TAP_TOKEN from # the environment. The token stays out of the clone URL, .git/config, every command line - # and the log. The empty helper first drops any helper the runner set up. The clone runs - # before the release exists, so an expired token stops the job before anything is published. + # and the log. The empty helper first drops any helper the runner set up. # shellcheck disable=SC2016 git clone --depth 1 -c credential.helper= \ -c 'credential.helper=!f() { echo username=x-access-token; echo "password=$TAP_TOKEN"; }; f' \ https://github.com/krishhgg/homebrew-tap.git "$RUNNER_TEMP/tap" + # The tap is public, so the clone runs without the token. A dry-run push asks GitHub for + # git-receive-pack, the service behind every push, and GitHub serves it only to a token + # that it accepts and that has push access to the tap. An expired, revoked or read-only + # token therefore stops the job here, before the release exists. The dry run sends no + # commits, so it does not check branch rules on main. Unlike HEAD, --all also works on + # a tap with no commits. + git -C "$RUNNER_TEMP/tap" push --dry-run --all origin - name: GitHub release env: GH_TOKEN: ${{ github.token }} run: | tag=$GITHUB_REF_NAME - # A rerun of this job finds the release that the first attempt made. It goes on only if - # the release holds these same bundles, so the formula's hashes match what users download. - if gh release view "$tag" > /dev/null 2>&1; then - gh release download "$tag" --pattern SHA256SUMS --output "$RUNNER_TEMP/SHA256SUMS.published" - if ! cmp -s dist/SHA256SUMS "$RUNNER_TEMP/SHA256SUMS.published"; then - echo "::error::Release $tag already has other bundles. Rerun only the failed jobs, or delete the release first." + # gh release create makes a draft, uploads the files and then publishes the draft, so a + # cancelled attempt can leave a draft behind. No user can see a draft, and gh release + # create does not reuse one. Delete every draft of this tag and create the release again. + # The deletes go by id, so they never hit a published release. The git tag stays. + drafts=$(gh api "repos/$GH_REPO/releases" --paginate \ + --jq ".[] | select(.draft and .tag_name == \"$tag\") | .id") + for id in $drafts; do + echo "Deleting draft release $id of $tag, which an earlier attempt left" + gh api -X DELETE "repos/$GH_REPO/releases/$id" + done + # This endpoint finds only a published release. A rerun goes on with it only if it has + # both bundles and the SHA256SUMS of this run, so the formula's hashes match what users + # download. + if gh api "repos/$GH_REPO/releases/tags/$tag" --silent 2> /dev/null; then + pub=$RUNNER_TEMP/published + if ! gh release download "$tag" --dir "$pub" --pattern 'sys1rust-*.tar.gz' --pattern SHA256SUMS || + ! cmp -s dist/SHA256SUMS "$pub/SHA256SUMS" || + ! (cd "$pub" && sha256sum --quiet --check SHA256SUMS); then + echo "::error::Release $tag is published with other files than this run built. Rerun only the failed jobs, or delete the release first." exit 1 fi - echo "Release $tag already has these bundles" + echo "Release $tag is published with these bundles and SHA256SUMS" exit 0 fi flags=() @@ -173,10 +198,24 @@ jobs: TAP_TOKEN: ${{ secrets.TAP_TOKEN }} run: | version=${GITHUB_REF_NAME#v} + formula=$RUNNER_TEMP/tap/Formula/sys1rust.rb + # A rerun of an older tag's publish job must not put its formula over a newer one. The + # tap-update group keeps a second tag's job from changing the tap after the clone. + if [ -f "$formula" ]; then + current=$(sed -n 's/^ *version "\(.*\)"$/\1/p' "$formula") + if ! [[ $current =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::The tap's Formula/sys1rust.rb needs 1 version line with an X.Y.Z version, and it has '$current'." + exit 1 + fi + if [ "$current" != "$version" ] && + [ "$(printf '%s\n' "$current" "$version" | sort -V | tail -n 1)" = "$current" ]; then + echo "::notice::The tap has sys1rust $current, which is newer than $version, so the tap stays as it is. Release $GITHUB_REF_NAME is published." + exit 0 + fi + fi sha() { awk -v f="sys1rust-$version-$1-arm64.tar.gz" '$2 == f {print $1}' dist/SHA256SUMS; } mkdir -p "$RUNNER_TEMP/tap/Formula" - packaging/render_formula.sh "$GITHUB_REF_NAME" "$(sha macos26)" "$(sha macos14)" \ - > "$RUNNER_TEMP/tap/Formula/sys1rust.rb" + packaging/render_formula.sh "$GITHUB_REF_NAME" "$(sha macos26)" "$(sha macos14)" > "$formula" cd "$RUNNER_TEMP/tap" git add Formula/sys1rust.rb # A rerun after a successful push finds the formula unchanged. That is the wanted state, diff --git a/packaging/RELEASE.md b/packaging/RELEASE.md index dfc0cd6..9b7fd25 100644 --- a/packaging/RELEASE.md +++ b/packaging/RELEASE.md @@ -1,8 +1,8 @@ # Releasing sys1rust -Pushing a `v*` tag runs `.github/workflows/release.yml`. It builds both bundles on a macOS 26 runner, smoke-tests them on macOS 14, 15 and 26 runners and publishes the GitHub release with `SHA256SUMS`. A final `vX.Y.Z` tag also updates `Formula/sys1rust.rb` in krishhgg/homebrew-tap. A `vX.Y.Z-rcN` tag publishes a prerelease and leaves the tap alone. A pull request that changes `packaging/` or the workflow runs the build and smoke jobs only. +Pushing a `v*` tag runs `.github/workflows/release.yml`. It builds both bundles on a macOS 26 runner, smoke-tests the macOS 14 bundle on a macOS 15 runner and the macOS 26 bundle on a macOS 26 runner, and publishes the GitHub release with `SHA256SUMS`. A final `vX.Y.Z` tag also updates `Formula/sys1rust.rb` in krishhgg/homebrew-tap. A `vX.Y.Z-rcN` tag publishes a prerelease and leaves the tap alone. A pull request that changes `packaging/` or the workflow runs the build and smoke jobs only. -Run every command below from the repository root on an Apple silicon Mac with macOS 26.2 or later. Use 1 shell for all of them, because later steps reuse `version` and `RUSTUP_TOOLCHAIN`. The tests, the smoke runs and the Homebrew service load a model on the GPU. Run each of them alone, with no other GPU work on the Mac. +Run every command below from the repository root on an Apple silicon Mac with macOS 26.2 or later. Use 1 shell for all of them, because later steps reuse `version`, `RUSTUP_TOOLCHAIN`, `HF_HUB_CACHE` and `rc`. The tests, the smoke runs and the Homebrew service load a model on the GPU. Run each of them alone, with no other GPU work on the Mac. 1. Install the pinned Rust and select it for this shell. `packaging/build.sh` and `packaging/third_party.py` stop when `rustc` or `cargo` reports another version. ```sh @@ -20,9 +20,11 @@ Run every command below from the repository root on an Apple silicon Mac with ma version=$(sed -n 's/^version = "\(.*\)"$/\1/p' runtime/Cargo.toml) echo "$version" ``` -3. Build both bundles, run the strict suite against the macOS 26 MLX build, and smoke-test both bundles. The tests read the pinned snapshots from the Hugging Face cache, so pull all 3 models first. Every command must pass. The laya-mlx `settings` tests take about 14 minutes. +3. Build both bundles, run the strict suite against the macOS 26 MLX build, and smoke-test both bundles. The laya-mlx tests do not ask for the pinned revisions. They load the snapshot that a model's `refs/main` names, or else the model's only snapshot, and `sys1rust pull` writes no `refs/main`. So this step pulls the 3 pinned models (846 MB, 678 MB and 846 MB) into a fresh cache under `packaging/.work`, which then holds only the pinned snapshots. `HF_HUB_CACHE` points every later command in this shell at that cache. Every command must pass. The laya-mlx `settings` tests take about 14 minutes. ```sh packaging/build.sh macos26 dist && packaging/build.sh macos14 dist + export HF_HUB_CACHE=$PWD/packaging/.work/hf-cache + rm -rf "$HF_HUB_CACHE" for m in typed-decisions multilingual english; do packaging/.work/macos26/sys1rust-$version-macos26-arm64/bin/sys1rust pull $m done @@ -38,16 +40,17 @@ Run every command below from the repository root on an Apple silicon Mac with ma python3 packaging/smoke.py packaging/.work/$fl/sys1rust-$version-$fl-arm64/bin/sys1rust done ``` -4. Tag a release candidate and push the tag. The workflow builds and smoke-tests the bundles and publishes a prerelease. It leaves the tap alone. Follow the run on the repository's Actions page. If a job fails, fix the cause on main and tag the next candidate, `v$version-rc2`. Until GitHub retires the macOS 14 image on 2026-11-02, it fails `macos-14` jobs during scheduled brownouts (actions/runner-images#13518). Rerun a job that failed that way. +4. Tag a release candidate and push the tag. `rc` holds the candidate's tag for this step and the next. The workflow builds and smoke-tests the bundles and publishes a prerelease. It leaves the tap alone. Follow the run on the repository's Actions page. If a job fails, merge the fix to main, run `git pull --ff-only`, set `rc` to the next candidate, such as `v$version-rc2`, and run the tag line again. ```sh - git tag "v$version-rc1" && git push origin "v$version-rc1" + rc=v$version-rc1 + git tag "$rc" && git push origin "$rc" ``` -5. Install the candidate through Homebrew from a throwaway local tap. `packaging/render_formula.sh` writes the formula for the prerelease with the hashes from its `SHA256SUMS`. The first service start downloads typed-decisions (846 MB) if `~/.cache/huggingface/hub` does not hold it yet. If `curl` never answers, read `$(brew --prefix)/var/log/sys1rust.log`. +5. Install the candidate through Homebrew from a throwaway local tap. `packaging/render_formula.sh` writes the formula for the prerelease with the hashes from its `SHA256SUMS`. The service does not see this shell's `HF_HUB_CACHE`, so its first start downloads typed-decisions (846 MB) if `~/.cache/huggingface/hub` does not hold it yet. If `curl` never answers, read `$(brew --prefix)/var/log/sys1rust.log`. ```sh - sums=$(gh release download "v$version-rc1" --repo krishhgg/sys1rust --pattern SHA256SUMS --output -) + sums=$(gh release download "$rc" --repo krishhgg/sys1rust --pattern SHA256SUMS --output -) sha() { printf '%s\n' "$sums" | awk -v f="sys1rust-$version-$1-arm64.tar.gz" '$2 == f {print $1}'; } brew tap-new --no-git local/sys1rust-test - packaging/render_formula.sh "v$version-rc1" "$(sha macos26)" "$(sha macos14)" \ + packaging/render_formula.sh "$rc" "$(sha macos26)" "$(sha macos14)" \ > "$(brew --repository local/sys1rust-test)/Formula/sys1rust.rb" HOMEBREW_NO_AUTO_UPDATE=1 brew install local/sys1rust-test/sys1rust brew test local/sys1rust-test/sys1rust @@ -56,7 +59,7 @@ Run every command below from the repository root on an Apple silicon Mac with ma brew services stop local/sys1rust-test/sys1rust brew uninstall sys1rust && brew untap local/sys1rust-test ``` -6. Tag the release and push the tag. The workflow publishes the release and pushes `Formula/sys1rust.rb` to krishhgg/homebrew-tap with the `TAP_TOKEN` secret, a fine-grained token with contents read and write on that repo only. The publish job clones the tap before it creates the release, so an empty or expired token stops the job before anything goes public. +6. Tag the release and push the tag. The workflow publishes the release and pushes `Formula/sys1rust.rb` to krishhgg/homebrew-tap with the `TAP_TOKEN` secret, a fine-grained token with contents read and write on that repo only. The publish job stops on an empty secret. Before it creates the release, it also runs `git push --dry-run` against the tap with the token. GitHub answers that only for a token that it accepts and that has push access to the tap, so an expired, revoked or read-only token also stops the job before anything goes public. The dry run sends no commits, so it does not check branch rules on the tap's main. ```sh git tag "v$version" && git push origin "v$version" ``` @@ -64,8 +67,10 @@ Run every command below from the repository root on an Apple silicon Mac with ma ## If the tap update fails -If the publish job fails at the tap because `TAP_TOKEN` expired or lost access, make a new fine-grained token with contents read and write on krishhgg/homebrew-tap only. Store it with the command below, which prompts for the value. Then open the failed run on the Actions page and choose "Re-run failed jobs". The rerun finds the release from the first attempt with the same bundles, keeps it and pushes the formula. If the formula is already up to date, the job finishes without a commit. "Re-run all jobs" builds new bundles, and the publish job then stops rather than replace the published files. +If the publish job fails at the token check or at the push because `TAP_TOKEN` expired or lost access, make a new fine-grained token with contents read and write on krishhgg/homebrew-tap only. Store it with the command below, which prompts for the value. Then open the failed run on the Actions page and choose "Re-run failed jobs". The rerun deletes any draft release that a cancelled attempt left. It keeps a published release only if that release's 2 bundles and `SHA256SUMS` match the run's files, and otherwise stops. Then it pushes the formula. If the formula is already up to date, the job finishes without a commit. If the tap already has a newer version, the job leaves the tap alone and says so in a notice. "Re-run all jobs" builds new bundles, and the publish job then stops rather than replace the published files. ```sh gh secret set TAP_TOKEN --repo krishhgg/sys1rust ``` + +Publish jobs of all tags run one at a time, and GitHub holds at most 1 more waiting. A third tag pushed while 1 publish job runs and another waits cancels the waiting job. Rerun it. From b99118d230a5def2f5b8e0fc2f2a485d4bcf4c2b Mon Sep 17 00:00:00 2001 From: Krish Garg Date: Mon, 5 Oct 2026 21:22:40 -0700 Subject: [PATCH 10/11] Stop on a leftover draft release instead of deleting it The publish job deleted, by id, every draft release of the tag that a cancelled gh release create left. Publishing a draft keeps its id, so a maintainer who published the draft between the list and the delete would lose a live release, and the concurrency group cannot coordinate with a manual publish. The job now stops on such a draft, prints its URL and asks for a manual delete before a rerun. The published-release guard stays as it was. Also drop the last comment that mentioned the macos-14 runner. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/release.yml | 18 +++++++++--------- packaging/RELEASE.md | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index baf7602..6e5bdb3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,7 +77,6 @@ jobs: matrix: include: # macos-15 runs the macOS 14 bundle on an OS older than 26.2, the case that bundle exists for. - # There is no macos-14 leg. GitHub retires that image on 2026-11-02 (actions/runner-images#13518). - { runner: macos-15, flavor: macos14 } - { runner: macos-26, flavor: macos26 } runs-on: ${{ matrix.runner }} @@ -165,15 +164,16 @@ jobs: run: | tag=$GITHUB_REF_NAME # gh release create makes a draft, uploads the files and then publishes the draft, so a - # cancelled attempt can leave a draft behind. No user can see a draft, and gh release - # create does not reuse one. Delete every draft of this tag and create the release again. - # The deletes go by id, so they never hit a published release. The git tag stays. + # cancelled attempt can leave a draft behind, and gh release create would add a second + # one. The job stops on a draft of this tag and leaves it to a maintainer. Deleting it + # here could remove a live release if someone publishes the draft in between. drafts=$(gh api "repos/$GH_REPO/releases" --paginate \ - --jq ".[] | select(.draft and .tag_name == \"$tag\") | .id") - for id in $drafts; do - echo "Deleting draft release $id of $tag, which an earlier attempt left" - gh api -X DELETE "repos/$GH_REPO/releases/$id" - done + --jq ".[] | select(.draft and .tag_name == \"$tag\") | .html_url" | + paste -sd ' ' -) + if [ -n "$drafts" ]; then + echo "::error::Release $tag has a draft that an earlier attempt left: $drafts. Delete it on the Releases page (or \`gh release delete $tag\` after checking it is still a draft), then rerun the publish job." + exit 1 + fi # This endpoint finds only a published release. A rerun goes on with it only if it has # both bundles and the SHA256SUMS of this run, so the formula's hashes match what users # download. diff --git a/packaging/RELEASE.md b/packaging/RELEASE.md index 9b7fd25..47a2d7b 100644 --- a/packaging/RELEASE.md +++ b/packaging/RELEASE.md @@ -67,7 +67,7 @@ Run every command below from the repository root on an Apple silicon Mac with ma ## If the tap update fails -If the publish job fails at the token check or at the push because `TAP_TOKEN` expired or lost access, make a new fine-grained token with contents read and write on krishhgg/homebrew-tap only. Store it with the command below, which prompts for the value. Then open the failed run on the Actions page and choose "Re-run failed jobs". The rerun deletes any draft release that a cancelled attempt left. It keeps a published release only if that release's 2 bundles and `SHA256SUMS` match the run's files, and otherwise stops. Then it pushes the formula. If the formula is already up to date, the job finishes without a commit. If the tap already has a newer version, the job leaves the tap alone and says so in a notice. "Re-run all jobs" builds new bundles, and the publish job then stops rather than replace the published files. +If the publish job fails at the token check or at the push because `TAP_TOKEN` expired or lost access, make a new fine-grained token with contents read and write on krishhgg/homebrew-tap only. Store it with the command below, which prompts for the value. Then open the failed run on the Actions page and choose "Re-run failed jobs". If a cancelled attempt left a draft release of the tag, the rerun stops and names it. Delete it on the Releases page, or with `gh release delete ` after checking that it is still a draft, then rerun the publish job. The rerun keeps a published release only if that release's 2 bundles and `SHA256SUMS` match the run's files, and otherwise stops. Then it pushes the formula. If the formula is already up to date, the job finishes without a commit. If the tap already has a newer version, the job leaves the tap alone and says so in a notice. "Re-run all jobs" builds new bundles, and the publish job then stops rather than replace the published files. ```sh gh secret set TAP_TOKEN --repo krishhgg/sys1rust From a1455bdf54b56c7b1b0f011ba62da07231bdd790 Mon Sep 17 00:00:00 2001 From: Krish Garg Date: Mon, 5 Oct 2026 21:34:51 -0700 Subject: [PATCH 11/11] Require every reference field in the smoke test and state the strict suite's GPU smoke.py now checks a reply's shape the way live.rs does. Question ids, answer fields and probability labels must match the reference's lists in order, and the legend must equal the reference's. noul, the confidences, act_probability and every probability must be numbers in [0, 1], and confidence, answer_confidence and act_probability must stay within MAX_DRIFT. A reply without confidence, answer_confidence, act_probability or legend used to pass. RELEASE.md says the strict suite needs an M5-class GPU on macOS 26.2 or later, because the laya-mlx settings tests need MLX's NAX gemms, and that the smoke tests run on any Apple silicon Mac. Co-Authored-By: Claude Opus 5.5 (1M context) --- packaging/RELEASE.md | 2 +- packaging/smoke.py | 76 ++++++++++++++++++++++++++++++-------------- 2 files changed, 53 insertions(+), 25 deletions(-) diff --git a/packaging/RELEASE.md b/packaging/RELEASE.md index 47a2d7b..a382e85 100644 --- a/packaging/RELEASE.md +++ b/packaging/RELEASE.md @@ -2,7 +2,7 @@ Pushing a `v*` tag runs `.github/workflows/release.yml`. It builds both bundles on a macOS 26 runner, smoke-tests the macOS 14 bundle on a macOS 15 runner and the macOS 26 bundle on a macOS 26 runner, and publishes the GitHub release with `SHA256SUMS`. A final `vX.Y.Z` tag also updates `Formula/sys1rust.rb` in krishhgg/homebrew-tap. A `vX.Y.Z-rcN` tag publishes a prerelease and leaves the tap alone. A pull request that changes `packaging/` or the workflow runs the build and smoke jobs only. -Run every command below from the repository root on an Apple silicon Mac with macOS 26.2 or later. Use 1 shell for all of them, because later steps reuse `version`, `RUSTUP_TOOLCHAIN`, `HF_HUB_CACHE` and `rc`. The tests, the smoke runs and the Homebrew service load a model on the GPU. Run each of them alone, with no other GPU work on the Mac. +Run every command below from the repository root on a Mac with an M5-class GPU (GPU generation 17 or later) and macOS 26.2 or later. The strict suite's laya-mlx `settings` tests need MLX's NAX gemms and fail on the fallback that older GPUs get, while the smoke tests run on any Apple silicon Mac. Use 1 shell for all the commands, because later steps reuse `version`, `RUSTUP_TOOLCHAIN`, `HF_HUB_CACHE` and `rc`. The tests, the smoke runs and the Homebrew service load a model on the GPU. Run each of them alone, with no other GPU work on the Mac. 1. Install the pinned Rust and select it for this shell. `packaging/build.sh` and `packaging/third_party.py` stop when `rustc` or `cargo` reports another version. ```sh diff --git a/packaging/smoke.py b/packaging/smoke.py index d6e728f..ea20ade 100755 --- a/packaging/smoke.py +++ b/packaging/smoke.py @@ -3,13 +3,16 @@ 24 requests in bench/workloads/smoke.jsonl, compares the answers with the fp32 CPU reference in bench/reference/typed-decisions/smoke.jsonl by the rules of bench/harness/compare.py (each question's answer agrees unless the reference is a near tie, and probabilities are within -MAX_DRIFT), then stops the server with SIGINT and requires exit code 0. +MAX_DRIFT), then stops the server with SIGINT and requires exit code 0. confidence, +answer_confidence and action.act_probability must be within MAX_DRIFT too. -Every answer must also be well formed: the reference's type, every field the reference has and -a probability for every label in the reference's map, so a near tie or a missing probability -can't hide a broken answer. A request that fails in transport, runs past REQUEST_TIMEOUT_S or -returns bad JSON fails the run and stops sending. The script always stops the server, also when -it gets SIGINT or SIGTERM. +Every reply must also have the reference's shape, as the strict suite checks it +(runtime/crates/sys1rust/tests/live.rs): the reference's question ids, each answer's fields in +the reference's order, the reference's probability labels in its order and the reference's +legend. Probabilities, noul, the confidences and act_probability must be numbers in [0, 1], and +score a finite number. So a near tie or a missing field can't hide a broken answer. A request +that fails in transport, runs past REQUEST_TIMEOUT_S or returns bad JSON fails the run and stops +sending. The script always stops the server, also when it gets SIGINT or SIGTERM. Run `SYS1RUST pull` first. Usage: packaging/smoke.py PATH_TO_SYS1RUST """ @@ -58,32 +61,54 @@ def is_number(v): return isinstance(v, (int, float)) and not isinstance(v, bool) and math.isfinite(v) +def is_probability(v): + return is_number(v) and 0 <= v <= 1 + + def malformed(got, want): - """Says why `got` can't stand in for the reference answer `want`, or returns None if it can.""" + """Says why `got` can't stand in for the reference answer `want`, or returns None if it can. + live.rs asserts the same key lists, so an extra field or label fails here too.""" if not isinstance(got, dict): return f"answer is {type(got).__name__}, not an object" - if got.get("type") != want["type"]: - return f"type {got.get('type')!r}, reference type {want['type']!r}" + if list(got) != list(want): + return f"fields {list(got)}, reference fields {list(want)}" + if got["type"] != want["type"]: + return f"type {got['type']!r}, reference type {want['type']!r}" labels = want.get("probabilities") or {} - choice = got.get("choice") - if "choice" in want and not (isinstance(choice, str) and choice in labels): - return f"choice {choice!r} is not a reference label" - for field in ("score", "noul"): - if field in want and not is_number(got.get(field)): - return f"{field} {got.get(field)!r} is not a number" + if "choice" in want and not (isinstance(got["choice"], str) and got["choice"] in labels): + return f"choice {got['choice']!r} is not a reference label" + if "score" in want and not is_number(got["score"]): + return f"score {got['score']!r} is not a number" + for field in ("noul", "confidence", "answer_confidence"): + if field in want and not is_probability(got[field]): + return f"{field} {got[field]!r} is not a number in [0, 1]" + if "legend" in want and got["legend"] != want["legend"]: + return f"legend {got['legend']!r}, reference legend {want['legend']!r}" + if "action" in want: + # The reference's action holds act_probability only. + action = got["action"] + if not isinstance(action, dict): + return f"action {action!r} is not an object" + for k in want["action"]: + if not is_probability(action.get(k)): + return f"action.{k} {action.get(k)!r} is not a number in [0, 1]" if "probabilities" in want: - probs = got.get("probabilities") - if not isinstance(probs, dict) or not probs: - return "no probabilities" - if not all(is_number(p) for p in probs.values()): - return "a probability is not a number" - # compare.py counts a missing label as 0, which can keep a broken answer under MAX_DRIFT. - missing = sorted(set(labels) - set(probs)) - if missing: - return f"no probability for reference labels {missing}" + probs = got["probabilities"] + if not isinstance(probs, dict) or list(probs) != list(labels): + return f"probabilities {probs!r}, reference labels {list(labels)}" + if not all(is_probability(p) for p in probs.values()): + return "a probability is not a number in [0, 1]" return None +def field_drift(got, want): + """The largest difference in confidence, answer_confidence and action.act_probability, + the probabilities that compare.py's drift leaves out. malformed() has checked them.""" + pairs = [(got[f], want[f]) for f in ("confidence", "answer_confidence") if f in want] + pairs += [(got["action"][k], v) for k, v in want.get("action", {}).items()] + return max((abs(a - b) for a, b in pairs), default=0.0) + + def post(host, port, body, out): """Sends one request on a fresh connection. Keeps the current stage in out["stage"] so a timeout can name it, and leaves out["error"] or out["status"], out["data"] and, after a 200, @@ -111,6 +136,8 @@ def post(host, port, body, out): def check(rid, answers, want_all, failures, stats): + if list(answers) != list(want_all): + failures.append(f"{rid}: question ids {list(answers)}, reference {list(want_all)}") for q, want in want_all.items(): stats["questions"] += 1 where = f"{rid} {q}" @@ -132,6 +159,7 @@ def check(rid, answers, want_all, failures, stats): if d is None: failures.append(f"{where}: no probability drift for {got}") continue + d = max(d, field_drift(got, want)) stats["worst"] = max(stats["worst"], d) if d > MAX_DRIFT: failures.append(f"{where}: drift {d:.4f} > {MAX_DRIFT}")