v0.6.1 — MLX supervisor hardening, speculative decoding, vision embedding honesty
Reliability + observability hardening on top of 0.6.0's multi-MLX foundation. The big wins: an MLX supervisor that can no longer get stuck restarting an orphan-port forever (orphan reap + crash-window quarantine), vision embedding chips that stop lying when the backend isn't installed, a brew install ollama-herd path that actually works, a tunable FLEET_MLX_MAX_INFLIGHT_PER_MODEL for operators willing to trade memory for batched throughput, dashboard color semantics that finally agree with the product's "idle hardware is waste" thesis, and speculative decoding live on the dedicated 30B compactor for an immediate Claude-Code-summarization-pass speedup. Source-read research in docs/research/mlx-lm-stability-and-concurrency.md confirmed v0.31.3 is the right pin; v0.31.2 would have traded our quarantine-able bug for two uncontainable ones.
Added
- Speculative decoding enabled on the dedicated context compactor (port 11441,
Qwen3-Coder-30B-A3B-Instruct-4bit). Re-tested on our pinnedmlx-lm==0.31.3and confirmed it works on standard transformer MoEs with aQwen3-1.7B-4bitdraft (≈94 tok/s on M3 Ultra, noArraysCacheerror). Live config in~/.fleet-manager/envadds"draft_model":"mlx-community/Qwen3-1.7B-4bit","num_draft_tokens":4to the 30B-A3B server entry only — every Claude Code request's pre-summarization pass benefits, which is the hot path. Not enabled on port 11440 (Qwen3-Coder-Next-4bit, the main coding model): the Qwen3-Next architecture uses Mamba/SSM-style linear attention layers that mlx-lm represents with a non-trimmableArraysCache, so spec decoding still hits ml-explore/mlx-lm#1081 atspeculative_generate_step:531. The earlier "always blocked onArraysCache" framing was wrong — the bug is architecture-specific, not version-specific. Updated post-mortem indocs/issues/mlx-speculative-decoding-blocked.md. Per-specdraft_model+num_draft_tokenswere already plumbed throughMlxServerSpec.from_dictin 0.6.0 — this release is config-only on the production fleet.
Fixed
-
FLEET_MLX_MAX_INFLIGHT_PER_MODELenv var — tunable per-model concurrent-request cap on the MLX proxy. Default1(strict serialization, matching historical behavior). Bump to2or3to letmlx_lm.server'sBatchGeneratorprocess multiple requests in one inference pass — empirically validated 2026-04-27 on the local fleet (3 concurrent requests took 0.55s wall vs 1.10s sum, confirming real parallelism). The default stays conservative because each in-flight request carries its own KV cache state, so 2× concurrent 100K-token prefills is 2× memory pressure; concurrent paths inmlx_lm.serverhave historically been bug magnets (#965, #1166 — both fixed in v0.31.3, but the pattern is real). Operator opt-in only. Full source-read + live test indocs/research/mlx-lm-stability-and-concurrency.md. 5 new tests cover defaults, clamping (negative/zero values floor to 1), the 2-concurrent-acquires-then-third-blocks behavior, and per-model semaphore independence. Confirmed in the same research that mlx-lm v0.31.3 is the right pin even though it's where we hit #1208 — downgrading to v0.31.2 would trade our quarantine-able bug for two uncontainable ones (#1166 Qwen3-Next concurrent crash + #1181 thread-local stream crash). -
MLX supervisor now detects and SIGKILLs orphan
mlx_lm.serverprocesses on the configured port before spawning its own. A previous herd-node session killed viapkill -9 -f "bin/herd-node"(without also killingmlx_lm.server) leaves the MLX subprocesses alive —Popen(start_new_session=True)makes them survive their parent's death; they get reparented to launchd and keep holding ports 11440 / 11441. The next supervisor startup couldn't bind, exited rc=1 in ~2 seconds, and the quarantine guard kicked in against a process that didn't exist while the orphan kept serving requests. Result was 17 hours ofmlx_server_quarantinedwarnings against a fleet that was actually serving traffic, just not under any supervisor's control. Newfind_orphan_mlx_pids_on_port()(psutil-based, identity-strict — only kills processes whose cmdline mentionsmlx_lm.serverAND whose net_connections show binding to our port).MlxSupervisor.start()calls it before its ownPopenand SIGKILLs anything found, with a loud WARNING explaining what was reaped. 8 new tests for the strict identity check (right cmdline + right port = kill; wrong port = leave alone; non-mlx process on right port = leave alone; permission errors handled). Operator restart recipe inCLAUDE.mdupdated topkill -9 -f "bin/herd|mlx_lm.server"so the manual path doesn't reproduce the trap. See observation indocs/observations.md2026-04-27. -
MLX supervisor now quarantines a crash-looping subprocess instead of restarting it forever. On 2026-04-26 a stuck-state in
mlx_lm.server v0.31.3(latest) triggered a 420-restart, 2.5-hour crash loop on the local fleet — the supervisor's_monitorcorrectly detected each crash and restarted at the (capped) 60s exponential-backoff cadence, but had no upper bound on how many times it would keep doing that. Now: ≥5 crashes within a 5-minute rolling window switches the supervisor into "quarantined" state with a 10-minute restart interval, and surfaces a CRITICALmlx_server_quarantinedhealth-check recommendation pointing at logs + likely causes. Quarantine clears automatically once a restart stays up for the full window — so transient bursts still recover gracefully; only persistent failures get throttled. Tunable via_QUARANTINE_FAILURE_COUNT/_QUARANTINE_WINDOW_S/_QUARANTINE_RESTART_INTERVALinmlx_supervisor.py. 8 new tests for the threshold + windowing logic. Underlying mlx-lm bug filed upstream as ml-explore/mlx-lm#1208. See observation indocs/observations.md2026-04-26. -
Vision embedding chips no longer lie about availability when
onnxruntimeis missing. The dashboard previously rendered DINOv2 / SigLIP / CLIP chips on a node card whenever the model weights were cached on disk, regardless of whether the embedding backend (onnxruntime) could actually load them. Operators saw "available" chips and assumed the service worked; the first real/embedcall returned HTTP 500. The collector now probes foronnxruntimeimport on every heartbeat and refuses to advertise vision-embedding models when the backend isn't loadable, so the chips disappear (honest) instead of misleading. Paired with a newvision_backend_missinghealth check (WARNING) that fires when weights ARE cached but the backend is missing — operators see "Vision embedding backend not installed on<node>. Runuv sync --extra embedding..." in the Recommendations panel instead of silently-disappearing chips. Also closes the recurring root cause: the local-deploy snippet inCLAUDE.mdwasuv sync(without--extra embedding), which is destructive — every restart stripped the embedding deps. Updated touv sync --all-extrasso optional capabilities stay resident across restarts. Newvision_embedding_status: dictfield on heartbeat +NodeStatecarries{backend_available, cached_model_count}so future health checks have a clean signal to read. 9 new tests intests/test_server/test_health_vision_backend.pycovering both fires-on-missing and silent-when-fine paths plus the collector probe directly. -
brew install ollama-herdnow actually works. The Homebrew formula atgeeks-accelerator/homebrew-ollama-herdhad been broken throughout 0.5.x — Homebrew'spip install --no-binary :all:policy forced source builds forpydantic-core, which required Rust to bootstrapmaturin, which the formula didn't depend on; sixpyproject.tomldeps (cryptography,cffi,pycparser,tiktoken,regex,websockets) were also missing from the formula'sresourceblocks; andpydantic-corewas version-mismatched against the bundledpydantic(2.45.0 vs the required 2.41.5). Fix shipped to the tap asgeeks-accelerator/homebrew-ollama-herd@71856f3(no PyPI republish needed). Verified end-to-end on macOS Apple Silicon: clean fresh-user install in ~5 minutes, all critical imports clean, bothherdandherd-nodeCLIs functional. The release checklist inCLAUDE.mdwas updated to make the brew end-to-end install test a non-negotiable gate so this class of failure can't recur. Background and post-mortem indocs/observations.md(entry: 2026-04-25).
Added
- Platform-aware thermal signal — new
ThermalMetricson the heartbeat withstate(nominal/warning/unknown),temperature_c, andsourcefields. Linux nodes now report real peak temps frompsutil.sensors_temperatures()(scanningcoretemp/k10temp/zenpower/cpu_thermaldrivers) and flagwarningabove 85°C. macOS and Windows honestly reportunknown— Apple Silicon'smachdep.xcpmis Intel-only,powermetricsrequires sudo, andpmset -g thermonly reports past events;psutil.sensors_temperatures()isn't implemented on macOS at all. The dashboard's.bar-thermaloverlay now uses the reported signal when available and falls back to the CPU≥95% proxy only when state isunknown, so Linux operators get first-class thermal detection and macOS operators keep the existing behavior with a clean seam for future upgrades. Seesrc/fleet_manager/common/system_metrics.py::get_thermal_metrics. /dashboard/color-statesdev route — renders the utilization bar in every Axis B warning state side-by-side (normal / memory warning / memory critical / CPU thermal) plus gradient sweeps for CPU (cyan→purple) and memory (soft-blue→deep-purple). Uses the live dashboard CSS + JS so any change to production styling reflects here automatically. Linked fromdocs/guides/dashboard-color-reference.mdand used by the marketing-site agent for consistent screenshot captures.docs/handoffs/ollamaherd-com-color-refresh.md— self-contained brief for the agent maintaining the marketing site. Documents the palette change, explains why the old landing-page screenshots now contradict the product's own messaging, and provides the live reference URL for capturing replacements.
Changed
- Dashboard color semantics: utilization is no longer a warning. CPU, memory, and per-node RAM bars now render in a blue→purple gradient (
utilizationColor(pct, metric)) instead of the old green-to-red busy-is-bad scale. A Mac Studio at 95% memory is the product working as designed, not a problem — the previous coloring contradicted the product's own thesis that idle hardware is waste. Warning state moved to a separate visual axis:.bar-warning/.bar-criticaloutlines fire on OS-reported memory pressure (psutil.virtual_memory().pressure), and.bar-thermalfires on sustained ≥95% CPU as a throttling proxy. Preserved: disk bar still uses the busy-is-bad scale (disk full genuinely breaks things), and the capacity-score bar keeps its green-at-high-availability semantic. Also addeddocs/guides/dashboard-color-reference.mdas a one-page cheat sheet for future UI additions so the semantic doesn't drift back into server-ops defaults. Seedocs/plans/dashboard-color-semantics.mdfor the full rationale and audit.