@meridiona/meridian-darwin-arm64 1.66.0 → 1.66.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.66.
|
|
1
|
+
1.66.2
|
package/bin/meridian
CHANGED
|
Binary file
|
package/bin/meridian-tray
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meridiona/meridian-darwin-arm64",
|
|
3
|
-
"version": "1.66.
|
|
3
|
+
"version": "1.66.2",
|
|
4
4
|
"description": "Prebuilt Meridian app for macOS arm64 (daemon binary + dashboard + Python services). Installed via @meridiona/meridian.",
|
|
5
5
|
"homepage": "https://github.com/Meridiona/meridian",
|
|
6
6
|
"repository": {
|
|
@@ -13,7 +13,7 @@ app_state: dict[str, Any] = {}
|
|
|
13
13
|
# `received`/`total` are AGGREGATE byte counts summed across every pipeline model
|
|
14
14
|
# (llm + reranker + embedder), so the wizard's progress bar has a single honest
|
|
15
15
|
# denominator. The wire contract the Rust tray decodes is exactly
|
|
16
|
-
# state/received/total/error (tray/src-tauri/src/mlx_server.rs::PrefetchStatus);
|
|
16
|
+
# state/received/total/error/speed (tray/src-tauri/src/mlx_server.rs::PrefetchStatus);
|
|
17
17
|
# `models` is an additive per-model breakdown the tray ignores.
|
|
18
18
|
prefetch_state: dict[str, Any] = {
|
|
19
19
|
"state": "idle",
|
|
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
import asyncio
|
|
18
18
|
import logging
|
|
19
|
+
import os
|
|
19
20
|
import threading
|
|
20
21
|
import time
|
|
21
22
|
from pathlib import Path
|
|
@@ -31,12 +32,12 @@ log = logging.getLogger("agents.server")
|
|
|
31
32
|
router = APIRouter()
|
|
32
33
|
|
|
33
34
|
# Speed is derived from the growth of on-disk `received` between /prefetch_status
|
|
34
|
-
# polls — NOT from a tqdm hook
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
# polls — NOT from a tqdm hook (a tqdm_class override never fires reliably across
|
|
36
|
+
# backends). The byte-delta approach is backend-agnostic; with Xet disabled for the
|
|
37
|
+
# prefetch (see `_download_spec`) the classic path grows the blob linearly, so this
|
|
38
|
+
# now reads as a smooth rate rather than the 0-then-burst pattern hf_xet produced.
|
|
39
|
+
# Stored as a dict so both functions can mutate it in-place without `global`s.
|
|
40
|
+
_speed_state: dict[str, float | int] = {"recv": 0, "ts": 0.0}
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
def _hf_cache_dir_for(model_id: str) -> Path:
|
|
@@ -82,17 +83,84 @@ def _spec_total_bytes(spec: model_registry.ModelSpec) -> int:
|
|
|
82
83
|
return total
|
|
83
84
|
|
|
84
85
|
|
|
86
|
+
def _env_positive_int(name: str, default: int) -> int:
|
|
87
|
+
"""Read a positive-int env override, falling back to `default` on bad input.
|
|
88
|
+
|
|
89
|
+
Parsed at import, so a non-integer value (operator typo) must degrade to the
|
|
90
|
+
default rather than raise ValueError and brick MLX-server startup.
|
|
91
|
+
"""
|
|
92
|
+
raw = os.environ.get(name)
|
|
93
|
+
if raw is None:
|
|
94
|
+
return default
|
|
95
|
+
try:
|
|
96
|
+
return max(1, int(raw))
|
|
97
|
+
except ValueError:
|
|
98
|
+
log.warning(
|
|
99
|
+
"server: ignoring non-integer env override; using default",
|
|
100
|
+
extra={"env_var": name, "raw": raw, "default": default},
|
|
101
|
+
)
|
|
102
|
+
return default
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# How many times to (re)enter snapshot_download for one model before giving up.
|
|
106
|
+
# Each retry resumes from the on-disk ``.incomplete`` partial (HF Range request),
|
|
107
|
+
# so bytes already pulled are never re-downloaded. Env-overridable for ops.
|
|
108
|
+
_PREFETCH_MAX_ATTEMPTS = _env_positive_int("MERIDIAN_PREFETCH_MAX_ATTEMPTS", 5)
|
|
109
|
+
|
|
110
|
+
|
|
85
111
|
def _download_spec(spec: model_registry.ModelSpec) -> None:
|
|
86
112
|
"""Download one model's weights to the HF cache (no load into memory).
|
|
87
113
|
|
|
88
|
-
|
|
89
|
-
|
|
114
|
+
Forces the classic (non-Xet) transfer path and retries with resume so a
|
|
115
|
+
stalled connection can never wedge the download indefinitely:
|
|
116
|
+
|
|
117
|
+
* **Xet is disabled for the pull.** ``hf_xet`` does its transfer in Rust with
|
|
118
|
+
no per-read timeout and can't be interrupted from Python, so a half-open
|
|
119
|
+
connection to the Xet CAS endpoint hangs forever (observed: a prefetch stuck
|
|
120
|
+
at 57% for 4h on a healthy 16 Mbps link — bytes never resumed). The classic
|
|
121
|
+
path caps every read at ``HF_HUB_DOWNLOAD_TIMEOUT`` (10s) and resumes from the
|
|
122
|
+
``.incomplete`` partial via a Range request, so a stall raises and self-heals
|
|
123
|
+
instead of hanging. On a cold first-run pull of distinct model weights Xet's
|
|
124
|
+
chunk-dedup buys little, so this trades an unused fast-path for a bounded,
|
|
125
|
+
resumable one. (Bonus: the classic path grows the on-disk blob linearly, so
|
|
126
|
+
``/prefetch_status``'s byte-delta speed reads smoothly instead of sitting at 0
|
|
127
|
+
then jumping in bursts as hf_xet flushes reconstructed chunks.)
|
|
128
|
+
* **Bounded retries** re-enter ``snapshot_download`` on any error; each attempt
|
|
129
|
+
resumes from the partial, so transient drops that exhaust the library's own
|
|
130
|
+
per-read retries still converge without re-pulling what's on disk.
|
|
131
|
+
|
|
90
132
|
``mlx_lm.load`` / ``mlx_embeddings.load`` resolve the files straight from the
|
|
91
133
|
cache afterwards. Only disk is touched — the single-slot runtime loads each
|
|
92
|
-
model lazily on first use.
|
|
134
|
+
model lazily on first use. Disabling Xet here is process-global (it flips a
|
|
135
|
+
huggingface_hub module constant), so later in-process model loads inherit the
|
|
136
|
+
same bounded, resumable path — which is what we want on a network where Xet
|
|
137
|
+
stalls.
|
|
93
138
|
"""
|
|
139
|
+
from huggingface_hub import constants as hf_constants
|
|
94
140
|
from huggingface_hub import snapshot_download
|
|
95
|
-
|
|
141
|
+
|
|
142
|
+
# Flip the *module attribute* (re-read per download at file_download.py call
|
|
143
|
+
# sites), NOT the env var: the env is parsed into this constant once at import,
|
|
144
|
+
# so ``os.environ`` here would be a no-op for an already-imported library.
|
|
145
|
+
hf_constants.HF_HUB_DISABLE_XET = True
|
|
146
|
+
|
|
147
|
+
for attempt in range(1, _PREFETCH_MAX_ATTEMPTS + 1):
|
|
148
|
+
try:
|
|
149
|
+
snapshot_download(spec.model_id, allow_patterns=spec.allow_patterns)
|
|
150
|
+
return
|
|
151
|
+
except Exception as exc: # noqa: BLE001 — retry-with-resume, re-raise if exhausted
|
|
152
|
+
log.warning(
|
|
153
|
+
"server: prefetch download attempt failed; will resume from partial",
|
|
154
|
+
extra={
|
|
155
|
+
"model_id": spec.model_id,
|
|
156
|
+
"attempt": attempt,
|
|
157
|
+
"max_attempts": _PREFETCH_MAX_ATTEMPTS,
|
|
158
|
+
"error": str(exc),
|
|
159
|
+
},
|
|
160
|
+
)
|
|
161
|
+
if attempt >= _PREFETCH_MAX_ATTEMPTS:
|
|
162
|
+
raise # all attempts exhausted — surface the active error to _run_prefetch
|
|
163
|
+
time.sleep(min(2**attempt, 30)) # 2s, 4s, 8s, 16s … capped at 30s
|
|
96
164
|
|
|
97
165
|
|
|
98
166
|
def _run_prefetch(specs: list[model_registry.ModelSpec]) -> None:
|
|
@@ -140,7 +208,7 @@ def _run_prefetch(specs: list[model_registry.ModelSpec]) -> None:
|
|
|
140
208
|
prefetch_state["error"] = str(exc)
|
|
141
209
|
prefetch_state["speed"] = 0.0
|
|
142
210
|
root.set_status(trace.Status(trace.StatusCode.ERROR, str(exc)))
|
|
143
|
-
log.
|
|
211
|
+
log.exception("server: model prefetch failed", extra={"error": str(exc)})
|
|
144
212
|
|
|
145
213
|
|
|
146
214
|
@router.post("/prefetch_model")
|
|
@@ -160,13 +228,13 @@ async def prefetch_model() -> dict:
|
|
|
160
228
|
|
|
161
229
|
from fastapi.concurrency import run_in_threadpool
|
|
162
230
|
|
|
163
|
-
global _last_recv, _last_recv_ts
|
|
164
231
|
specs = list(model_registry.ALL_SPECS)
|
|
165
232
|
|
|
166
233
|
with prefetch_lock:
|
|
167
234
|
if prefetch_state["state"] in ("downloading", "done"):
|
|
168
235
|
return dict(prefetch_state) # idempotent — no duplicate downloads
|
|
169
|
-
|
|
236
|
+
_speed_state["recv"] = 0
|
|
237
|
+
_speed_state["ts"] = 0.0
|
|
170
238
|
prefetch_state.update(
|
|
171
239
|
state="downloading",
|
|
172
240
|
received=0,
|
|
@@ -189,7 +257,7 @@ async def prefetch_model() -> dict:
|
|
|
189
257
|
return_exceptions=True,
|
|
190
258
|
)
|
|
191
259
|
totals: list[int] = []
|
|
192
|
-
for spec, r in zip(specs, probe_results):
|
|
260
|
+
for spec, r in zip(specs, probe_results, strict=True):
|
|
193
261
|
if isinstance(r, int):
|
|
194
262
|
totals.append(r)
|
|
195
263
|
else:
|
|
@@ -223,7 +291,6 @@ async def prefetch_status() -> dict:
|
|
|
223
291
|
"""
|
|
224
292
|
from fastapi.concurrency import run_in_threadpool
|
|
225
293
|
|
|
226
|
-
global _last_recv, _last_recv_ts
|
|
227
294
|
with prefetch_lock:
|
|
228
295
|
st = dict(prefetch_state)
|
|
229
296
|
models = list(st.get("models", []))
|
|
@@ -252,12 +319,13 @@ async def prefetch_status() -> dict:
|
|
|
252
319
|
# EMA-smoothed; decays to 0 when bytes stop flowing (delta 0) or the run ends.
|
|
253
320
|
now = time.monotonic()
|
|
254
321
|
with prefetch_lock:
|
|
255
|
-
if st["state"] == "downloading" and
|
|
256
|
-
inst = max(0, st["received"] -
|
|
322
|
+
if st["state"] == "downloading" and _speed_state["ts"] and now > _speed_state["ts"]:
|
|
323
|
+
inst = max(0, st["received"] - _speed_state["recv"]) / (now - _speed_state["ts"])
|
|
257
324
|
prev = prefetch_state.get("speed", 0.0) or 0.0
|
|
258
325
|
prefetch_state["speed"] = inst if prev == 0.0 else prev * 0.5 + inst * 0.5
|
|
259
326
|
elif st["state"] != "downloading":
|
|
260
327
|
prefetch_state["speed"] = 0.0
|
|
261
|
-
|
|
328
|
+
_speed_state["recv"] = st["received"]
|
|
329
|
+
_speed_state["ts"] = now
|
|
262
330
|
st["speed"] = prefetch_state["speed"]
|
|
263
331
|
return st
|
|
@@ -96,7 +96,7 @@ _ENTITY_RESCUE_CAP = 4
|
|
|
96
96
|
_embedder = None # cached (model, tokenizer) tuple
|
|
97
97
|
|
|
98
98
|
|
|
99
|
-
def _get_embedder():
|
|
99
|
+
def _get_embedder() -> tuple:
|
|
100
100
|
global _embedder
|
|
101
101
|
if _embedder is None:
|
|
102
102
|
import mlx_embeddings
|
|
@@ -135,7 +135,8 @@ def evict_embedder() -> None:
|
|
|
135
135
|
_embedder = None
|
|
136
136
|
gc.collect()
|
|
137
137
|
try:
|
|
138
|
-
import mlx.core as mx
|
|
138
|
+
import mlx.core as mx
|
|
139
|
+
mx.clear_cache()
|
|
139
140
|
except Exception: # noqa: BLE001 — cache flush is best-effort; non-fatal if mlx absent
|
|
140
141
|
pass
|
|
141
142
|
log.info("session_distiller: embedding model evicted from memory")
|
package/services/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "meridian-agents"
|
|
7
|
-
version = "1.66.
|
|
7
|
+
version = "1.66.2"
|
|
8
8
|
description = "Meridian agents — MLX classifier server and Jira worklog synthesis for meridian.db"
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
authors = [{ name = "Meridiona" }]
|