Repository navigation
Two-Mac cluster serving: real JACCL worker, guided link setup, coordinator and provider fixes - #1407
Draft
jonathan308 wants to merge 945 commits into
Draft
jonathan308 wants to merge 945 commits into
jonathan308 wants to merge 945 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
jonathan308
requested a deployment
to
benchmarks
October 9, 2026 03:38 — with
GitHub Actions
Waiting
jonathan308
requested a deployment
to
benchmarks
October 9, 2026 04:08 — with
GitHub Actions
Waiting
jonathan308
requested a deployment
to
benchmarks
October 9, 2026 05:20 — with
GitHub Actions
Waiting
jonathan308
requested a deployment
to
benchmarks
October 9, 2026 06:18 — with
GitHub Actions
Waiting
…epts Round 6f hardware (27B, fresh homes through `darkbloom cluster`): both ranks ended at startup, five times, with "Pair waits must be between one nanosecond and one minute". owner-serve's derived budgets size the clean-stop wait at twice the predicted longest request within 10...120 s, but ClusterWorkerPair refuses any wait above one minute before it looks at its workers. Installed-session check runner and ClusterDerivedTimeBudgetTests: for planned deadlines from 1 s to an hour the derived pairTiming must pass the pair's own limit, and a 330 s deadline's clean-stop wait is the pair's minute. Fails at this commit (the fix follows). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-minute limit owner-serve's DistributedInstalledTimeBudgets sized the clean-stop wait at twice the predicted longest request with a 120 s ceiling; ClusterWorkerPair refuses any wait above ClusterWorkerPairTiming.longestWaitNanoseconds (one minute) before it starts. On a setup whose planned request deadline is above 150 s (the 27B on M3 Ultra + M5 Ultra in round 6f) every start ended with "Pair waits must be between one nanosecond and one minute" and restarted. The limit is now public and the policy's ceiling (and a clamp in derived()) is that limit: a request still running a minute into a stop is cancelled. Red: checks-6-red (installed-session runner); green: checks-6-green (installed-session + ProcessChecks). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 15, 2026
…iter
The macOS 27.0 SDK adds a nonisolated(nonsending) overload of
withTaskCancellationHandler. Both Swift 6.4 toolchains on that SDK pick it,
and the call into the non-Sendable writer's helpers then fails to compile
("sending 'writer' risks causing data races"). Passing isolation: #isolation
selects the overload the code was written for on every SDK; behaviour is
unchanged (the operation runs on the caller's isolation, as before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dinator `darkbloom cluster local [model]` (also `darkbloom cluster --local`) runs the same onboarding as `darkbloom cluster` and serves the pair on the leading Mac's local API only. No Mac of the cluster contacts a coordinator: the leader part starts with the new `darkbloom start --distributed --no-coordinator`, which never builds the control-only member connection, so nothing can refuse it (a refusal used to fail the start). - Scope (ClusterServingScope) saved in the leader's setup record until changed (owner 2026-10-15): restarts and reboots serve the same way. The cluster service keys rank 0's part by the scope, so a change restarts it; `l` / `p` in `darkbloom cluster` switch; a serving cluster restarts its leading part and the screen waits for it. - The API listens on every network of the leading Mac by default (owner: LAN/tailnet), always with the local API key; `--bind` names an address and takes only localhost or a numeric IPv4/IPv6 address. A saved address the API does not take falls back to loopback. `darkbloom local` prints a network URL. - The member records the scope from the leader's start command (and the 15 s sync re-sends it), so it holds if control moves; an older peer's messages carry none and change nothing. - Catalog models only, as before; qualification records never gate it. - Shorter screens: blocked models grouped by reason, different builds on the two Macs named plainly, "not supported on a cluster yet", and the development-build service line without the login-agent explanation (now in the service log). Checks: onboarding 319/0 (local flows, scope values, grouping), console 74,686 / 37 groups (scope in the record), installed session (start arguments, part key, log lines, wire), control 219/0, diagnostics pass. Failing first on 69ba75b: the new checks do not compile there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…locally The owner asked for a single command: `darkbloom cluster` no longer takes --local or --bind; `darkbloom cluster local [model] [--bind <address>]` is the only spelling. The onboarding request and checks are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er port
Each Mac's cluster service started its two link listeners (setup exchange
and control lease) once, bound to the cable port and address active at that
moment, and never again. Moving the cable to another Thunderbolt port (the
owner's pair went en3 -> en1 -> en2) left both Macs listening on dead ports:
`darkbloom cluster` sat at "Reading both Macs over the cable" until the
services were restarted by hand.
Every 5 s each listener now reads the active cable port; when the port or
its address changed it is stopped and started again on the new one
(`ClusterSetupLinkExchange.cableMoved`). A reading with no cable keeps it.
Also: the console's developer wizard shortens the new plain blockers
("not supported on a cluster yet", "different Darkbloom builds"), and its
four checks follow that wording (they still expected the old one at
b95bb9e); the onboarding logs why the service does not start at login
once per service start instead of on every run.
Checks: console 74,690 / 38 groups (new: the cable moved), onboarding
319/0, installed session, diagnostics, control 219/0. Failing first on
06fe959: `cableMoved` and `Responder.selection` do not exist there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`darkbloom` run from the PATH is the installer's link (~/.darkbloom/bin/darkbloom -> Darkbloom.app/Contents/MacOS/darkbloom). The setup recorded that link as each Mac's owner executable, and the owner's start refuses a symlink by design (lstat in ClusterLocalOwnerConfiguration), so every cluster set up from the plain command failed to start rank 0 with "Installed local owner is unsafe or not executable" (2x M4 Max / M5 Max pair, 2026-10-15). The bench never saw it: it ran the app by its path. The setup now names the file the link points at (ClusterSetupMemberFacts.installedOwnerPath); the owner check is unchanged. Checks: console 74,693 / 39 groups (new: a real file plus the installer's link: the setup names the regular file). Failing first on b34d135. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n, words not cases
Owner feedback on the 2x Mac run (2026-10-15): the model list was a wall of
"needs:" lines, "(this Mac)" named the other Mac on a member's screen, the
first pairing failed with developer options, and a failing part repeated two
progress lines every 2 s until Ctrl-C.
- The list is one line per model (`N. Title - size · how it is split`, or
how it gets onto the Macs); the planner's reason, what moves and (serving
through Darkbloom only) the public-traffic line are said once chosen
(`ClusterOnboardingModelChoice.publicTraffic`, nil from older leaders).
Two models with the same name show their ids.
- Copy and download lines name the leading Mac "(this Mac)" only on its own
screen, never in an answer a member shows.
- `darkbloom cluster local` on the Mac that joins goes on to the choice
(forwarded to the leader) instead of sending you to the other Mac.
- Pairing waits up to 20 s for the cable port's address, then says to try
again in plain words.
- The start says each progress line once, and after three identical part
ends says so (with each Mac's last error and `d`) instead of repeating;
a part's last error reads as words, not `invalid("...")`.
Checks: onboarding 327/0 (one line per model, ids for twins, no public line
when local, the joiner carries on), console 74,697 / 39 groups (error words),
installed session, diagnostics, control 219/0; changelog 25 valid.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stage memory gate's two fixed margins, the admissible floor (6 GiB) and
the loading headroom (4 GiB), were measured on 256 GiB Macs and applied to
every Mac. On a 36 GB M4 Max paired with an M5 Max the planner put one Gemma
layer there (about 1 GB of weights), and the part was refused at 5.6 GB
admissible: the floor alone asked for 6 GiB (owner, 2026-10-15: "1 layer can
absolutely fit"; universality: no chip or memory constant decides).
- The two margins keep their measured values on Macs with 96 GiB or more;
below that they are the same share of the observed Mac's memory, never
under 2 and 1.5 GiB (QwenDenseStageLoadPolicy.minimumAdmissibleBytes /
loadingHeadroomBytes(physicalMemoryBytes:)). A decision on an observation
uses that observation's Mac, so the planner's record of the other Mac and
the worker's own load gate agree. The allocator headroom (a bound against
MLX's limit, not free memory) is unchanged. Every load and request is still
checked against the Mac's memory at that moment.
- "Restart <Mac> to free memory" only when nothing else frees enough; memory
macOS would give but the gate does not count yet says "Quit apps".
- A model chosen on the member carries its id: a leader that restarted since
it made the list plans again and adopts it ("Nothing to adopt: plan again"
on the M5 Max after the M4 Max restarted).
- The model list keeps the last catalog it read when the catalog does not
answer in 10 s (models and names vanished right after a restart).
Checks: placement 717+ pass (new: margins at 8/36/64/96/256 GiB, a 36 GiB
record carries its own; the 8 + 512 GiB pair now divides with the 8 GiB Mac
holding the smaller share, and a nearly full 8 GiB Mac still does not);
provider placement 211, TP plan, prefill schedule, prefix policy, stage
transfer, onboarding 327/0, control 219/0, installed session pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, deployment line
Found serving Gemma 4 26B QAT across an M4 Max 36 GB + M5 Max 128 GB pair
with `darkbloom cluster local` (2026-10-15 night):
- Port: oMLX held port 8000 on the M5 Max. Once the planner made that Mac
rank 0, every start loaded both Macs and then failed its bind, ending as
a bare CancellationError every ~14 s. The leading part now gets a port no
other program listens on (8000, else the next free one, kept while free:
ClusterLocalServing.servingPort / portIsFree), and `darkbloom start
--distributed` refuses a taken port before anything loads, in words.
- Reasoning markers: the local API (cluster and `start --local`) built its
OpenAI service with no default reasoning parser, so Gemma's
`<|channel>thought ... <channel|>` reached `content` (tool-result turn).
The cluster's API now parses by the model's type
(ProviderLoop.inferReasoningParser), as the coordinator path does.
- Cache sync: a member reported the leader's cache settings as applied only
when its onboarding role was "member"; a Mac that drove the onboarding and
then followed got them again every 15 s (and its ranks ignored them). Both
now follow control (ClusterService.followedLeaderPin).
- Deployment line: when the adopt ran on one Mac and control moved to the
rank 0 Mac before the start, the ready screen showed a line recorded for an
earlier start ("M5 Max 29 layers / M4 Max 1 layers" while M4 held 0-4).
It is now built from the saved setup's own Plan.
- The start's "keeps ending the same way" counted polls, not ends: it now
counts a line only when the screen turns to it.
Hardware (M4 Max 36 GB + M5 Max 128 GB, TB5 RDMA, local only, Gemma 4 26B
QAT, pipeline 0-5|5-30): models, 401 without key, chat, stream (first token
61-64 ms, decode 71 tok/s), tools + streamed tool call + tool result (clean),
JSON mode, stop, 5,515-token prompt (first token 1.47 s), 4 concurrent,
seeded twice identical; reached from the other Mac over the cable and LAN.
Images refused (known cluster limit).
Checks: onboarding 327/0, console 74,697 / 39 groups, control 219/0,
installed session (new: ports), diagnostics pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`darkbloom cluster local EigenLabs/Qwen3.8-27B-4bit-mtp` on an M4 Max leader stopped at the download: "permanently ineligible on this provider: missing runtime capabilities [apple_m5, mlx_nax]". That requirement is single-Mac serving's (one Mac serving the build alone); the cluster served this 27B on an M3 Ultra + M5 Ultra pair. The cluster's download (downloadOnThisMac) now grants the catalog's single-Mac requirements for a model whose digest this build's cluster runtime serves (ClusterModelCopyTrust.servableArtifacts): its adapters, grants and memory gates decide; the single-Mac provider keeps its own eligibility check when it loads (owner: chips never gate). Hardware: the same command now downloads on the M4 Max (in progress). Also (unproven on hardware): a cluster copy of a single-Mac model whose manifest names the same files as the catalog's but is not the catalog's exact bytes is checked once per process against the catalog and replaced (ClusterProductModelStore.repairManifest). Qwen 3.5 9B's copy differs from the catalog only in `created_at` and is refused by the worker's manifest pin; the repair has not yet taken effect on the pair (under investigation). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Mac's store Every model the cluster cloned in from single-Mac Darkbloom's store (Qwen 3.5 9B, and Qwen 3.8 27B right after its download) was "not supported on a cluster yet", and the other Mac refused the copy: the worker pins the catalog manifest's exact bytes, and the cluster copy held the coordinator's re-encoding instead. catalogManifest fetched the CDN's exact file and then refused it, because it compared the file list in order and the CDN lists the files in another order than the receipt. The lists are now compared as sets (ClusterCatalogArtifact.sameFiles, checked), and a cluster copy that already holds another encoding of the same files is replaced once per process by the catalog's bytes (repairManifest). Hardware (M4 Max 36 GB + M5 Max 128 GB): both copies' manifests became the registered ones (d1239a5b... for the 27B), the 27B copied to the M5 Max over the cable in 8.0 s (2.03 GB/s) and served. Checks: provisioning 139/0 (new: order). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… worker log Found serving Qwen 3.8 27B (pipeline + MTP) on an M4 Max 36 GB + M5 Max 128 GB pair with `darkbloom cluster local`: - Prefix cache: the 36 GB Mac admitted the largest context that fit (29,072 tokens) and left the prompt-prefix cache 0.4 MB, so its budget was 0, and one rank without a cache turns reuse off for both: no request ever reused a prefix. A memory-bound context now keeps one granule of budget (512 MiB of room) when that costs at most a quarter of the context (QwenResidentRuntime.admittedContext). Hardware: the M4 Max admitted the full 32,768 tokens and a 256 MiB budget; a repeated 5,554-token prompt's first token 7.10 s -> 0.28 s, a repeated 2,654-token one 3.6 s -> 0.36 s. - Streamed reasoning: the cluster's local API resolved the reasoning parser only in the OpenAI service, not in the request the engine sees, so the prompt's open think block did not seed the streaming parser and Qwen's whole reasoning arrived in one frame when it closed (1.7 s of keep-alives). The engine now takes the same default (cluster API only; the single-Mac local server is unchanged). First token 1.7 s -> 0.22 s (0.105 s warm). - Worker log: a worker's own diagnostics were kept only as a tail of a worker that failed. The owner now appends them to ~/.darkbloom/cluster-setup/worker.log (owner-only, 4 MiB, one rotation); this is how the prefix budget above was found. Hardware (Qwen 3.8 27B, M5 Max rank 0 with 60 layers on port 8001 because oMLX holds 8000, M4 Max 4 layers): models, chat, streamed reasoning, tools (+ streamed, + tool result), JSON, stop, 5,554-token prompt, 4 concurrent (serialized), seeded; decode ~47 tok/s, MTP acceptance ~83%. Images refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…models, manifests the worker takes
Found serving Qwen 3.8 27B and GPT-OSS 20B on an M4 Max 36 GB + M5 Max
128 GB pair with `darkbloom cluster local` (2026-10-16 night):
- One address: which Mac is rank 0 follows the plan and changed between
starts (M4 Max on one, M5 Max on the next), and the API moved with it.
The Mac that is not rank 0 now passes its own port (8000, else the next
free one) over the cable to rank 0's API, byte for byte
(ClusterLocalRelay, run by its cluster service while local-only serving
runs; stopped before that Mac becomes rank 0). The ready line says so.
- One key: each Mac served with its own local token, so the key changed
too. `cluster local` now serves with the pair's key
(~/.darkbloom/cluster_local_token), sent with each local-only serve inside
the keyed link control session; `darkbloom local` shows it on either Mac.
The start's test request uses the key of the server it asks.
- Member provisioning: a model chosen on a member (the list came from the
control leader) failed with "Nothing to copy: plan again.". The member now
plans the same models itself and fetches the one the list showed (by id);
the adopt then runs on the leader.
- Manifests: the catalog serves two encodings of a manifest (the CDN's
file and the coordinator's re-encoding) and the worker pins one per model
(Qwen 3.8 27B the CDN's, GPT-OSS 20B the coordinator's). The cluster copy
now takes the encoding this Mac's own worker accepts (its runtime
description, no load), and repairs a folder only with that answer.
- Planning: only rank 0 records served speeds, and the plan runs on the
control leader, often the other Mac. The setup report now carries the
Mac's newest served-speed records and the plan reads both Macs'. With an
index-only basis the start screen no longer prints figures as seconds
("decode 0.7 vs 1.6 tok/s, first token 278 s" for a 27B at 47 tok/s).
- The test request counts a thinking model's reasoning for its first token
and says when its tokens ran out while thinking (GPT-OSS).
- The worker-log installer moved out of DistributedInstalledOwner.swift,
which the model-free runners compile without ClusterSetupPaths
(1f1baf0 broke their build).
Hardware: GPT-OSS 20B (pipeline, M5 Max rank 0 on 8001 because oMLX holds
8000) reached through the M4 Max's port 8000 relay with the shared key:
chat, streaming (first token 124 ms p50, decode ~75 tok/s), tools +
streamed + tool result, JSON, 5K prompt, 4 concurrent; soak 30 sequential
and 20 at 4 parallel, 0 failures. Manifest repaired to the registered
a66dc823... and copied; Qwen 3.8 27B's stays d1239a5b....
Checks: onboarding 327/0, console 74,699 / 39 groups, control 219/0,
installed session (new: relay), diagnostics, process, placement pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…i 2 27B) `darkbloom cluster local ternary-bonsai-2-27b` on the M4 Max 36 GB + M5 Max 128 GB pair loaded, then every request failed after exactly 10 s with "Inference generation failed." (engine_v2 submit: ClusterWorkerOwnerError .deadline). The worker sat at 70-95% CPU while idle: its serving-context refresh (QwenResidentRuntime.refreshServingContext, holding the operation lock) binary-searches the admissible context, and each step derived the whole request allowance, for a Prism stage walking every tensor name through the plan (QwenPrismHadamardConstants.widenedBytes); with the prefix-cache reserve search of 1f1baf0 it took longer than the 10 s a request waits for its reservation. - widenedBytes is kept per plan and rank (it does not depend on the context), and admittedContext keeps each context's charge per loaded stage (QwenResidentChargeMemo, bounded). Idle worker CPU: 0-0.4%. - A failing local API request now writes one line to the Mac's log with the closed failure vocabulary (code, error case name, bounded reason; no error text). DARKBLOOM_DEBUG_LOCAL_ERRORS=1 adds the error's own text for local debugging (this is how the cause above was found). Hardware: Bonsai 2 27B (pipeline; M4 Max rank 0 with 12 layers, then M5 Max rank 0 with 60): chat, streaming (first token 146 ms warm, ~30 tok/s), tools + streamed + tool result, JSON, stop, 5,554-token prompt (10.2 s cold, 0.39 s repeated), 4 concurrent, seeded; soak 20/20 through the other Mac's relay port. Worker c0e66071. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings #1407 up to date with master (8 commits: dev/staging release lanes and the reusable release-provider.yml, darkbloom-dev builds, 0.9.21 startup calibration, 8 concurrent paged slots on M5, native Bonsai 9B with rectangular MTP, model registration split). Conflicts resolved: - libs/mlx-swift-lm: pinned to 167ca6bd, a merge of master's pin (14661d9b, mlx-swift-lm#311) into darkbloom/tp-engine (mlx-swift-lm#308), merged there without conflicts. - Engine (EngineV2Bridge, +Submission, MultiModelBatchSchedulerEngine): both sides' new parameters kept (the distributed deadline/origin and master's measurementPurpose / calibration posture). - ProviderLoop: master's BuildEnvironment log subsystem with the debug Secure Enclave key file kept. LocalCommand: the cluster key hint, named with BuildEnvironment.cliName. - Release: master moved the release body into the reusable release-provider.yml; the cluster-helper build, staging, Local Network usage string, signing and final-artifact checks were ported there (with $APP_NAME for dev app names), and the new dev-branch lane stages and ad-hoc signs the helpers. provider-signing-validation.py takes master's recorded packaging names and keeps the cluster-helper checks; its tests cover both (20/20). - Docs: dates, and the direct-mode table keeps both rows. Checks on the merge: darkbloom builds; onboarding 327/0, console 74,699 / 39 groups, control 219/0, installed session, diagnostics, provisioning, placement pass; signing validation tests 20/20; changelog 31 valid; docs-check 190 OK. Worker and plan helper build (native, JACCL). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plan is made on the memory each Mac has at that moment. On the 36 GB M4
Max with the owner's apps open, that memory moves (the file cache refills,
apps grow): Nemotron 3.5 Lightning was planned with 17 layers on it, its
load gate then refused that share ("needs 7.40 GiB, has 6.1"), and both
Macs retried the same split every 30 s indefinitely.
The cluster service on the Mac that leads the session now counts starts of
the same setup that ended with a `memory_gate_refused` incident; after two
(and at most once per 10 minutes) the control leader plans the same model
again on the memory now and adopts that split, as the update path does
(ClusterService.noteMemoryRefusal). A start that ends for any other reason
resets the count.
Hardware: built and deployed on the M4 Max 36 GB + M5 Max 128 GB pair; the
re-plan itself has not been triggered there yet (the next Nemotron start
fit). Nemotron 3.5 Lightning serves (M4 Max 9 layers, M5 Max 43): first
token 48 ms warm, ~67 tok/s, tools and JSON mode with room to think,
5.5K prompt 1.86 s cold / 0.23 s repeated, soak 20/20.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…local APIs The full provider suite found the cluster's local API no longer answering byte for byte as the single-Mac local API (ClusterAPIParityTests, owner rule 2026-10-14): f8de477 gave only the cluster's API a default reasoning parser (by its model's type), so a Qwen answer's `</think>` was split into reasoning_content on the cluster and left in content on a single Mac; and with the think-tag parser on, a stream whose prompt tail opens no think block held the whole answer until it ended (DistributedHTTPCleanStopTests). The parser default now lives in the shared local HTTP layer (LocalChatUploadResponder, from makeLocalInferenceApplication's modelTypeProvider), so both APIs resolve it the same way, and only for the channel formats whose markers are never meant to be shown and whose streaming parsers pass text on as it comes: Gemma 4 (`<|channel>thought`) and GPT-OSS (Harmony). Think-tag models keep the local API's raw stream on both, as before. The request carries the resolved parser, so the engine and the service agree. The cluster-only engine/service default is removed. Checks: ClusterAPIParityTests (7 families), DistributedHTTPCleanStopTests, StandaloneServer, LocalMode, ReasoningPromptProbe, ThinkStreamingLatency, DistributedLocalServer: 113 tests in 11 suites pass (metallib staged). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Right after Qwen3.5 35B A3B was downloaded and copied, the 36 GB M4 Max
showed 23 GB of file-backed memory, nearly all active, of which its load
gate counts only what the kernel can take before applications' memory
(0.09 GB): even a 4-layer share was refused ("needs 2.69 GiB, has 2.28")
until the pages aged. Most of that cache was the model's own files.
ClusterModelStore.dropCachedPages(folder:) invalidates every regular
file's pages in a folder through a shared mapping and msync(MS_INVALIDATE)
(no privileges, the bytes untouched; measured on this Mac: a 3.5 GB weight
file read into the cache, then dropped, file-backed 17.05 -> 14.46 GB). The
cluster service calls it for its part's model folder right before it
starts the part; the load then reads the weights into its own memory.
Checks: provisioning 142/0 (new: drop pages, bytes unchanged, missing
folder).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d before planning 87993a1 dropped them only before a part starts. The plan and the list's "fits now" read each Mac's load gate first: right after Qwen 3.6 35B was downloaded (written and verified with the file cache off), the 36 GB M4 Max's gate admitted 1.7 GB and every model said "Quit apps on Apple M4 Max first"; dropping that model's pages took it to 14.7 GB (file-backed 25.0 -> 10.7 GB). The pages are now dropped after the download (product store folder), after a received copy is in place, and for every cluster and product-store model folder before the leader plans. This was the cause of most of the night's memory refusals on the 36 GB Mac (the test harness had been freeing that cache by hand). Hardware: after re-reading Qwen 3.6 35B's 21 GB into the cache, `darkbloom cluster local qwen3.6-35b-a3b-vl-mtp-mxfp8` planned, started and answered in 43 s with no help (M5 Max 36 layers / M4 Max 4). Qwen 3.6 35B A3B: first token 78 ms warm, ~62 tok/s, 5.5K prompt 1.69 s cold / 0.25 s repeated, soak 15/15. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the master sync The first CI run on the synced #1407 found two release checks the merge left behind: - provider-release-cache.yml (new on master) stages the unsigned app without --cluster-helpers, which stage requires ("the following arguments are required: --cluster-helpers"). It now passes the action's cluster-helpers-dir output, like the release lanes. - release-swift.yml's push path filter must list every script a release runs (test-provider-release-pipeline.py): scripts/build-cluster-helpers.sh and scripts/sign-cluster-helpers.sh added. test-provider-signing-validation.py now also requires every workflow that stages an unsigned app to hand it the cluster helpers (checked to fail without the fix). Release Integrity's script suite passes locally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ssue The first CI run on the synced #1407 failed one test of 4,963: aCancelRightAfterTheLaunchProbe, a probe the suite itself calls "not a requirement of this slice" whose released-receipt check is already a known, intermittent issue (an early cancel can quarantine the member). On every run of 2026-10-16 (CI, an M4 Max at f17d6b9, and at 201b91f before the master merge) the session ended before the member sent any native_pair_cancel ("owner invalidated before the prelude completed"), the same issue one step earlier. The cancel wait now joins the known issue (intermittent), and the probe stops there when no cancel came. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…memo and error line A read-only review of the night's commits found: - Relay (c67bd1d): stop() closed descriptors its copy threads still used, so a number reused by the next relay or a link session could be written to or half-closed; an upstream opened during stop leaked. Each descriptor is now closed only by the thread that owns it; stop() shuts down and waits (at most 2 s) until the accept thread has closed the listener, so the port is free when this Mac becomes rank 0 (polled every 200 ms: a listener is not woken by shutdown on macOS). EMFILE/ENFILE/ENOBUFS/ENOMEM no longer end the accept loop with the port held; any other end marks the relay stopped so the next tick replaces it. At most 64 connections, 600 s read idle bound. It runs only when the API listens on every address (scope bind 0.0.0.0), and writes local.json only when no other live server owns it. The rank 0 port poll backs off to 120 s while the other Mac does not answer. - Re-plan (963b099): after 3 re-plans in a row that return the same split it stops re-planning (memory short for every split). - dropCachedPages: opened non-blocking and judged by fstat of what was opened (a FIFO swapped in cannot hang the service). - Charge memo (201b91f): keyed also by MTP depth, the profile's context, prompt and chunk limits and the prefill schedule. - Local API error line: the engine's own error enum names its case; any other error only its type (no description text). Not changed: the memory margins scaled below 96 GiB (0d9057c) are the owner's request; the review asks for measurements on small Macs. Hardware: relay serving Gemma (10 at 4 parallel through the M4 Max's port, tools), then Nemotron with the M4 Max as rank 0: the relay gave 8000 back and rank 0 took it. Checks: installed session (relay), provisioning pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he kernel can drop) Morning, with the owner using the 36 GB M4 Max (about 14 GB of applications, 14 GB of mostly active file cache), the pair stopped answering: every request got 503 "Provider capacity is temporarily unavailable" while both parts stayed loaded, and a restart then said "Quit apps on Apple M4 Max" for every model. - Requests on a loaded part: each reservation asked for the load-time entry floor (2.25 GiB on that Mac) on top of its own needs, and had 1.61 GiB. A reservation on a loaded stage now asks for what the request needs plus the loading headroom (QwenResidentRequestAllowance.requireLive onLoadedStage, with the floor lowered to the headroom); pressure, swap and the compression harm checks still refuse. - Cache the kernel can drop: the gate counts only the file cache taken before applications' memory (1.3 of 14.5 GB here), though touching 4-6 GB dropped that much cache with the compressor unchanged. When a decision is short only for that reason, the gate lets the kernel drop it (QwenDenseStageLoadReclaim: anonymous memory touched 128 MiB at a time and released, stopped at more than 64 MiB of compressions or swap-outs or pressure above normal), then judges a fresh sample. - The screens no longer say "Quit apps" for a share the gate counts less than macOS would give (gateCountsLess): the load makes that room itself. Hardware, with the owner's apps open and no help: `darkbloom cluster local gemma-4-26b-qat-4bit` planned, started and answered in 37 s (M5 Max 29 layers / M4 Max 1); models, 401, chat, stream (first token 62-68 ms, ~68 tok/s), tools + streamed + result, JSON, stop, 5.5K prompt, 4 concurrent, seeded all pass; the M4 Max's compressor stayed at 1.88 GB throughout. Checks: onboarding 327/0, console 74,699 / 39 groups, stage transfer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing separate on the local API - Cable on a port never set up: the owner moved the cable to Thunderbolt 1 on the M4 Max. Only Thunderbolt 2 and 3 had Darkbloom's network service with a fixed address, so the port had a link-local address only and the Macs could not find each other; `darkbloom cluster` said only "the other Mac is gone". The resume screen now says "The cable is on Thunderbolt 1 (en1), which is not set up for the cluster" and `f` runs the link setup (one macOS approval); the service's log says the same when the cable moves (OnboardingLive.unsetCablePort). - Reasoning: the local API (single Mac and cluster alike, so the parity holds) resolves every model's parser by type, think-tag formats included, on chat completions and now on the Responses API (where Gemma's `<|channel>` reached output_text). The local engine seeds content mode for a streamed think-tag request whose prompt tail opens no think block (seedsContentForUnknownPromptTail), so the answer streams at once instead of waiting for a close marker. Checks: onboarding 327/0, console 74,699 / 39 groups, control 219/0; 119 swift tests in 17 suites (parity, clean stop, standalone server, local mode, reasoning probe, think streaming latency, local endpoint) pass; the MiMo fixture tests need fixtures not present here. Hardware: the screen names the unset port on the M4 Max (cable on Thunderbolt 1); the reasoning change is not yet run on the pair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
997c1fb's room-making touched only the shortfall plus 512 MiB. The kernel drops inactive file cache first, which is exactly the part the gate already counts, so the admissible figure did not move: the M4 Max's 1-layer Gemma share stayed refused at load ("needs 2.25 GiB, has 2.14") and the pair re-planned and retried. The touch now covers the counted cache plus the shortfall plus 1 GiB (bounded by the cache above the reserve), so active cache is reached; it still stops at any compression, swap or pressure. Each attempt writes one darkbloom-reclaim-v1 line to the worker log (asked, touched, admitted, admissible). Hardware: Gemma 4 26B started from the M5 Max's screen (M4 Max rank 0 with 1 layer, API on its port 8000, the M5 Max's relay on 8001); the full API set passes (first token 56-59 ms, ~74 tok/s, tools, JSON, stop, 5.5K prompt, 4 concurrent, seeded). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch is waiting to be deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Experimental two-Mac ("cluster") serving: one model split across two Macs as a two-stage layer pipeline over MLX's JACCL backend (RDMA over Thunderbolt 5). This PR carries the provider and runtime side of the foundation extracted from the research drafts (#1226, #1227), completes the parts that extraction left out, and adds what was needed to run it on real hardware. Ordinary single-Mac serving is unchanged; everything here is opt-in (
darkbloom start --local --distributed,darkbloom cluster …).Status: draft. As of 2026-10-16 (
4d0a2f475, addsdarkbloom cluster local; see the first update below) and 2026-10-14 evening (round 6f,69ba75be7) two Macs pair and serve withdarkbloom clusteralone: the planner shards each model (pipeline with speculative decoding by default; tensor parallel when predicted faster), a missing model is copied over the cable, failover is automatic, the cluster API matches the single-Mac endpoint for tool calling and the common request types, and with the production coordinator the pair serves locally. See the 2026-10-14 updates.Update 2026-10-16 (overnight hardware run, M4 Max 36 GB + M5 Max 128 GB)
Head
4d0a2f475(fast-forward; all commits signed and Verified).a0a06c49f,4d0a2f475: a cable moved to a never-set-up Thunderbolt port is named on thedarkbloom clusterscreen and set up with one key (f); the local API (single Mac and cluster) puts every model's reasoning in its own field, think-tag models included, on chat completions and the Responses API, while a stream whose prompt opens no think block still streams at once; the room-making covers the cache the gate already counts. Hardware: Gemma 4 26B restarted from the M5 Max's screen and passed the full API set; the Responses API (plain, streamed, tools) passes on the pair. Not yet run on the pair: the think-tag reasoning change with a Qwen model.997c1fb70: a 36 GB Mac in everyday use keeps serving. With the owner's apps open the pair answered every request with 503 (each reservation on the loaded part asked for the 2.25 GiB load floor and had 1.6 GiB), and a restart said "Quit apps" for every model. A reservation on a loaded stage now asks for its own needs plus headroom, and a gate decision short only because it does not count droppable file cache lets the kernel drop that cache first (stopped at any compression, swap or pressure). Confirmed with the apps open and no help: Gemma 4 26B planned, started and answered in 37 s; the full API set passes; the M4 Max's compressor did not move.8159469a4: fixes from a review of the night's commits. Relay: descriptors are closed only by the thread that owns them,stopwaits until the port is free, it survives running out of descriptors, at most 64 connections with a 600 s idle bound, it runs only when the API listens on every address, and it leaves another local server's discovery file alone. Re-plan gives up after 3 same-split results. Page drop opens non-blocking. Charge memo keyed by MTP depth, limits and schedule. Error line logs the case or type only. Confirmed on hardware: relay under 4 parallel requests, then a switch that made the M4 Max rank 0 (the relay gave port 8000 back to it). Still open from the review: the memory margins that scale below 96 GiB (0d9057c86, the owner's request) need measurements on a small Mac.CI is green at
8159469a4(and was at0b480e3cc) (CI and SDK 27 release preparation pass) now that the PR merges cleanly. The first run (0ddc13e73) failed Release Integrity and the SDK 27 release lane on two release-workflow gaps from the master merge, and the unit lanes on the early-cancel probe (1 of 4,963 tests, failing before tonight too). Fixed inf17d6b9fc(provider-release-cache.yml passes the cluster helpers; release-swift.yml path filter lists the helper scripts; a check that every staging workflow passes them) and0b480e3cc(the probe's missing cancel joins its existing known issue).All eight catalog models a cluster serves were brought up on this pair through
darkbloom cluster local(Gemma 4 26B QAT, Qwen 3.8 27B, GPT-OSS 20B, Bonsai 2 27B, Nemotron 3.5 Lightning, Qwen3.5 35B A3B, Qwen 3.6 35B A3B, Qwen 3.5 9B; the two 28 GB Gemma builds were not downloaded for lack of disk on the 36 GB Mac). Qwen 3.5 9B (offered again after the manifest fix): copied in 3.1 s, first token 69 ms warm, ~65 tok/s with MTP. One key on every path: the other Mac reached the API over the LAN through the M4 Max's relay (first token 231 ms), over the cable (96 ms) and directly (88 ms).87993a1ce,0ddc13e73: a model's own pages are dropped from the file cache after a download, after a received copy, before planning and before a part starts (shared mapping +msync(MS_INVALIDATE), no privileges). Right after a download those pages sat in the active cache, which the load gate does not count: the 36 GB M4 Max admitted 1.7 GB and every model said "Quit apps"; dropped, 14.7 GB. This caused most of the night's refusals on that Mac. Qwen 3.6 35B A3B now serves with no help: planned, started and answering in 43 s; first token 78 ms warm, ~62 tok/s, soak 15/15. Synced with master (4a324c841mergesab423c1c0; no conflicts left, so CI runs again).Full provider swift-testing suite on the merged sources (run on the M5 Max, matched metallib staged): 4,156 tests in 569 suites; it found the cluster API no longer answering exactly as the single-Mac API (my f8de477 gave only the cluster a default reasoning parser).
f4dc0dd8amoves that default into the shared local HTTP layer for both APIs, for the channel formats only (Gemma 4, GPT-OSS Harmony); think-tag models keep the raw local stream on both. Parity (7 families), clean-stop and standalone-server suites pass (113 tests). Remaining failures: numerical-tolerance tests that fail on the M5 Max's GPU and pass on an M4 Max (GPT-OSS prefill output, CBv2 logits, sliding-window chunking), MiMo scanner tests that need their fixtures, and theaCancelRightAfterTheLaunchProbeprobe (marked "not a requirement").Master sync (
4a324c841): 14 conflicts resolved.libs/mlx-swift-lmnow pins167ca6bd, a merge of master's pin (mlx-swift-lm#311) intodarkbloom/tp-engine(mlx-swift-lm#308, pushed there, Verified). The cluster-helper release steps moved into master's reusablerelease-provider.yml(with$APP_NAME) and the new dev-branch lane stages and ad-hoc signs the helpers; signing-validation tests 20/20. Every model-free runner passes on the merge, plus targeted swift tests (30/30). Hardware on the merged build: Qwen3.5 35B A3B (soak 15/15) and Gemma 4 26B QAT (stream, tools, tool result, JSON) throughcluster local.Qwen3.5 35B A3B and Nemotron 3.5 Lightning serve. Qwen3.5 35B: first token 71 ms warm, ~61 tok/s, 5.5K prompt 1.7 s cold / 0.25 s repeated. Nemotron: first token 48 ms warm, ~67 tok/s, soak 20/20.
963b09925: the pair plans again when a Mac's load gate keeps refusing its share (proven on hardware: Qwen3.5 35B was planned with 24 of 40 layers on the 36 GB Mac, refused at load twice, then re-planned by itself to 36/4). Submodule pins unchanged; the cluster worker changed (prefix-cache budget), so both Macs need this build.GPT-OSS 20B serves through
darkbloom cluster local(pipeline, M5 Max rank 0): first token 124 ms p50, decode ~75 tok/s, tools/JSON/5K prompt pass; soak of 30 sequential + 20 at 4 parallel with 0 failures, all reached through the other Mac's port.PrismML Bonsai 2 27B serves (copied M5 Max → M4 Max over the cable in 4.1 s at 2.1 GB/s): first token 146 ms warm, ~30 tok/s, prefix reuse (5.5K prompt 10.2 s → 0.39 s), soak 20/20.
201b91f5b: the worker's idle context refresh recomputed the Prism stage constants at every step of its search and starved requests (each failed after 10 s); now kept per plan/rank, idle worker CPU ~0%.c67bd1ddf: one address and one key forcluster local(the Mac that is not rank 0 passes its port to rank 0's API over the cable; the pair shares one API key, synced inside the keyed link channel;darkbloom localshows it on either Mac); a model chosen on a member is fetched there; the manifest encoding is the one this Mac's worker accepts (GPT-OSS pins the coordinator's, Qwen 3.8 the CDN's); plans use the other Mac's served speeds; no index estimates shown as seconds; fixes the runner build broken by1f1baf0fb.Qwen 3.8 27B serves through
darkbloom cluster local(pipeline + MTP, M5 Max 60 layers / M4 Max 4, no coordinator): downloaded on the M4 Max from the catalog (16.3 GB, verified), copied to the M5 Max over the cable in 8.0 s (2.03 GB/s), then served. Chat, streamed reasoning, tools (+ streamed, + tool result), JSON mode, stop, 5,554-token prompt, 4 concurrent (serialized), seeded. Decode ~47 tok/s, MTP acceptance ~83%, warm first token 105 ms; a repeated 5,554-token prompt starts in 0.28 s (7.10 s cold).1e7948d5f: the leader downloads a model the cluster serves whatever its chip (the 27B's single-Mac "M5 only" requirement stopped an M4 Max leader).646e5be9c: models taken from this Mac's own store were "not supported on a cluster yet" and copies were refused: the CDN's exact manifest was rejected by an order-sensitive file comparison and the coordinator's re-encoding written instead.1f1baf0fb: (1) a memory-bound Mac's context now leaves the prefix cache a budget (it had 0 on the 36 GB Mac, which turned reuse off for both ranks); (2) a thinking model's reasoning streamed from the first token on the cluster API (superseded byf4dc0dd8a: think-tag models now keep the raw stream, as on a single Mac); (3) each Mac keeps its worker's own log,~/.darkbloom/cluster-setup/worker.log.Observed, not fixed yet: stop sequences also match inside a thinking model's reasoning (GPT-OSS
stop: ["7"]ended in its analysis); concurrent requests are served one at a time; Gemma has no prefix reuse or MTP on the cluster; a truncated non-streamed thinking answer returns its reasoning ascontent(same rule as single-Mac non-streaming);Update 2026-10-15:
darkbloom cluster local(serve the pair with no coordinator)Head
f8de47733(fast-forward from69ba75be7; 8 commits, all signed and Verified). Submodule pins unchanged.Hardware:
darkbloom cluster localserves Gemma 4 26B QAT across an M4 Max 36 GB + M5 Max 128 GB pair (TB5 RDMA, pipeline, M4 Max layers 0-4 / M5 Max 5-29, no coordinator): models list, 401 without the key, chat, streaming (first token 61-64 ms, decode ~71 tok/s), tool call + streamed tool call + tool result, JSON mode, stop sequences, a 5,515-token prompt (first token 1.47 s, 3,741 tok/s prefill), 4 concurrent streams, seeded sampling identical twice, reached from the other Mac over the cable and over the LAN. Images are refused (known cluster limit); Gemma has no prefix reuse or MTP on the cluster yet.0d9057c86: a smaller Mac takes the share it can hold. The stage gate's 6 GiB floor and 4 GiB loading headroom (measured on 256 GiB Macs) now scale with the Mac below 96 GiB (36 GB: 2.25 / 1.5 GiB); the 36 GB Mac's one-layer part was refused before. Also: a choice made on the member survives a leader restart, "Restart" only when nothing else frees memory, the last catalog is kept when it does not answer.f8de47733: fixes found on that pair: another server on port 8000 (oMLX) made every start end after both Macs loaded (now: a free port, checked before loading); Gemma's thought-channel markers reachedcontenton the local API (now parsed by model type); the leader re-sent cache settings every 15 s to a member that drove the onboarding; the ready line could name an earlier split.Not proven yet: a 36 GB Mac with ~13 GB of apps open admits only ~3 GB for a part by the gate's file-cache rule; the run freed droppable file cache first (test harness, no settings changed). More models next.
e7c0860d0: a cluster set up from the plaindarkbloomcommand never started its leading part. The setup recorded the installer's PATH link (~/.darkbloom/bin/darkbloom) as each Mac's owner, and the owner start refuses symlinks by design ("Installed local owner is unsafe or not executable"); found on the 2x M4 Max / M5 Max pair, never seen on the bench (it ran the app by path). The setup now names the file; the check is unchanged.4f763ab12: plainer onboarding (owner feedback from the same run): one line per model, details once chosen, no public-traffic line when serving locally only, ids for same-named models, "(this Mac)" never on the other Mac's screen, the Mac that joins carriescluster localon to the choice, pairing waits for the cable address, errors as words, and a part that keeps ending the same way is said after three tries instead of repeating.Hardware status (2x Mac, M4 Max 36 GB + M5 Max 128 GB): pairing, link setup, model choice, setup sync and the local-only start all ran through
darkbloom cluster local; the leading part then hit the owner-path bug above (fixed). Serving not yet proven: the M4 Max's memory gate admits 1.2 GB now (file cache 15 GB after the model copy) against the 6 GB per-part minimum.b34d13506(21:45): the cable can move to another port. Each Mac's cluster service bound its two link listeners (setup exchange, control lease) to the cable port active at start and never again; moving the cable to another Thunderbolt port (the 2x M4 Max pair went en3 → en1 → en2) left both Macs listening on dead ports anddarkbloom clusterwaiting at "Reading both Macs over the cable" until the services were restarted by hand. Every 5 s each listener now re-reads the active port and is started again there when the port or its address changed (ClusterSetupLinkExchange.cableMoved); a reading with no cable keeps it. Correction: atb95bb9e65four console wizard checks still expected the old "planner cannot read" wording (my console run had compiled just before the wording change);b34d13506updates them, console now 74,690 expectations / 38 groups pass.What changed
darkbloom cluster local [model] [--bind <address>]: the same onboarding asdarkbloom cluster(pairing, model choice, planner, copies, MTP, caches, failover, stop,v), but no Mac of the cluster contacts a coordinator. The leader part starts with the newdarkbloom start --local --distributed --no-coordinator, which never builds the control-only member connection, so a coordinator can no longer refuse the start (with production today, a refusal failed it).ClusterOnboardingLeadership.localOnly/localBind) and survives restarts and reboots;l/pindarkbloom clusterswitch. The cluster service keys rank 0's part by the scope, so a switch restarts that part and the screen waits until it serves. The member records the leader's scope from the start command (re-sent by the 15 s sync), so it holds if control moves; older peers carry none.--bindtakes onlylocalhostor a numeric IPv4/IPv6 address, and a saved address the API does not take falls back to loopback.darkbloom localprints anetwork:URL. The local API has no TLS (documented).31ea55820): that SDK adds anonisolated(nonsending)withTaskCancellationHandleroverload, which madeDistributedHTTPEventStreamfail to compile; the call now namesisolation: #isolation(no behaviour change).flowchart LR subgraph Before A1[darkbloom cluster] --> B1["leader: start --local --distributed"] B1 --> C1{coordinator answers the member role?} C1 -- registers --> D1[public serving] C1 -- unreachable / no role --> E1[local API fallback] C1 -- refuses --> F1[start fails, restarts] end subgraph After A2[darkbloom cluster local] --> S2[(scope saved in setup record)] S2 --> B2["leader: start --local --distributed --no-coordinator --bind addr"] B2 --> E2[local API only: no member loop, nothing to refuse] A3[darkbloom cluster: l / p] --> S2 endValidation (model-free, M4 Max, macOS 27.2): onboarding checks 319/0 (local from scratch, switch while serving,
l/p, named model, grouped reasons, scope values; failing first on69ba75be7, where they do not compile), console 74,686 expectations / 37 groups (scope in the record, old records read as public, bad address → loopback), installed-session checks (start arguments, part key, log lines, wire round-trips), control 219/0, diagnostics pass;changelog check25 valid;docs-check189 OK. Thedarkbloomproduct builds (debug,--build-system native) with Xcode 27.0 beta's SwiftPM and the 27.0 CLT compiler; every new refusal checked on the binary.Not yet (said plainly): no hardware run of
cluster localyet (a test build is installed on a 2x M4 Max pair; result to follow); full provider swift-testing suite and the remaining check runners not re-run on this head; the AGENTS.md refactor-pass subagent is still to run; this branch still conflicts with master (no Swift CI until the master sync, next).Build from this PR (both Macs must run the identical build):
gh pr checkout 1407 -R Layr-Labs/d-inference && git submodule update --init --recursive, thencd provider-swift && swift build --build-system native --product darkbloom. Debug builds keep the pairing key inDARKBLOOM_DEBUG_SE_KEY_FILE.Update 2026-10-14 (evening): rounds 6e and 6f: tensor parallel in the product, MTP on the sharded default, endpoint parity
Head
69ba75be7(fast-forward from30c084f88via 6ec0fc4857d). All commits signed. Submodules from published Layr fork branches, each pin the head of a draft PR:libs/mlx-swift37304a4d= Layr-Labs/mlx-swift#56, nestedmlx535d4e4e= Layr-Labs/mlx#35 andmlx-c2415568= Layr-Labs/mlx-c#15 (darkbloom/sealed-all-sum: the sealed all-sum),libs/mlx535d4e4e= Layr-Labs/mlx#35,libs/mlx-swift-lmba67a67a= Layr-Labs/mlx-swift-lm#308 (darkbloom/tp-engine, stacked on Layr-Labs/mlx-swift-lm#307). The fork PRs are stacked on Layr-Labs/mlx#33, Layr-Labs/mlx-c#14, Layr-Labs/mlx-swift#54 and Layr-Labs/mlx-swift-lm#307; merge each base first.What changed
DARKBLOOM_CLUSTER_FORCE_MODEexists for pilots (release builds refuse it). Fixed in 6e: a normal end of request was read as a client stop and ended the session.pipeline_mtp_v1records for the 27B, 9B and MiMo; the planner plans it by default from the records' measured speed-up (27B 35.6 → 55.6 tok/s on the bench pair, tokens equal to the plain pipeline).Hardware (M3 Ultra + M5 Ultra,
darkbloom clusterfrom fresh homes, coordinator without the member role → local serving)Tests: focused provider suites 558 tests / 72 suites (incl. the API parity tests), library and worker tests, all 35 model-free check runners.
Not in this head yet (on local branches, next merge): the tensor-parallel hang fix (rank lockstep + GPU-fence poisoning; 37 requests, 0 hangs), ~4x cheaper sealed all-sums, tensor parallel + MTP (27B 62–63 tok/s vs 39.5 plain TP), generic TP sharding for every family; A3B / VL / Bonsai parity runs; the remaining endpoint gaps above; coordinator Layr-Labs/darkbloom-platform#54 must list the new plan modes for a cluster-aware coordinator.
Update 2026-10-14: integration round 6 (6 → 6d): master sync, one-command setup, sharded deployments, no manual model downloads, failover, MiMo, chip-agnostic approvals
Head
30c084f88, on2a6b186ae(round 5) via 6 (33da00709), 6c (34d7711fe) and 6d. All commits signed.libs/mlx-swift-lmpins65af9799(draft Layr-Labs/mlx-swift-lm#307; merge it with a merge commit).What changed
darkbloom cluster→ setup → pick a model → serving. One admin approval per Mac,yfor the pairing code. Noplan/configure/flags/SSH for users; the setup screen states the deployment in one line (e.g. "Serving Qwen 3.5 9B across both Macs: pipeline, … layers / … layers"). 33 one-command findings fixed (setup entry points, offered set, two cable ports, updates, wording, FileVault/auto-login restart line).pipeline_mtp_v1) and tensor parallel modes in the protocol; phase split, prefill/decode hand-off hybrids and the per-request "alone" route stay in the code but are OFF by default (one developer opt-in); setups saved with a hand-off mode are refused and re-planned.darkbloom clustercommands (forwarded to the leader over the paired link); the leader is rank 0 and fails over at lease expiry; a Mac serves alone only after 10 minutes of absence and only if it can run the model; the pair re-forms by itself.provider.tomlnow list the catalog.pipeline_mtp_v1).Hardware (M3 Ultra + M5 Ultra over Thunderbolt 5 RDMA, through
darkbloom clusterfrom fresh homes)provider.toml: the catalog listed from production, the 9B copied and served across both Macs.Tests: provider suite (only the 6 baseline names), every cluster check runner, library + worker package tests at 6b; focused suites + 11/11 check runners at 6c; placement/onboarding/console/control/configuration runners at 6d.
Still open: tensor parallel serving in the product (in progress),
pipeline_mtp_v1records so MTP is planned by default, rank-0 memory growth under load (~20 MiB/min), records never blocking owner-only serving (built on a branch), Developer ID signed build + firewall-on reboot test (owner), coordinator draft Layr-Labs/darkbloom-platform#54 deploy (owner).Earlier rounds (condensed; full text in this description's edit history)
Linked issue
None.
Before and after
Before — the extracted branch could not start a collective, could not finish a distributed start, and gave the operator no way to see why.
flowchart LR classDef bad fill:#fde2e1,stroke:#b42318,color:#3b0d0a classDef ok fill:#e3f1e5,stroke:#1e7a34,color:#0b2e14 classDef neutral fill:#eef1f5,stroke:#5b6676,color:#1a2230 OP["Operator"]:::neutral --> START["start --local --distributed"]:::neutral START --> REG["Register as cluster member"]:::ok REG --> ACK["Coordinator never acknowledges"]:::bad ACK --> KILL["Leader tears down after 10 s"]:::bad START --> WRK["Launch rank worker"]:::neutral WRK --> NOSRC["Worker has no source in the tree"]:::bad WRK --> STUB["macOS 14 target compiles the JACCL stub"]:::bad OP --> LINK["Thunderbolt port without its own IPv4 address"]:::neutral LINK --> SILENT["Nothing reports it; JACCL refuses at init"]:::badAfter — a 26.2 worker package builds real JACCL, the coordinator (its side is in darkbloom-platform) acknowledges and guards members, and the link is inspected before anything is launched.
flowchart LR classDef ok fill:#e3f1e5,stroke:#1e7a34,color:#0b2e14 classDef neutral fill:#eef1f5,stroke:#5b6676,color:#1a2230 classDef pending fill:#fff3d6,stroke:#9a6700,color:#3d2a00 OP["Operator"]:::neutral --> CL["darkbloom cluster link"]:::ok CL --> VERDICT["Per-port verdict and one sentence of guidance"]:::ok OP --> STAGE["darkbloom-cluster-stage-check"]:::ok STAGE --> LOADED["Each Mac loads and releases its stage of the real model"]:::ok OP --> START["start --local --distributed"]:::neutral START --> REG["Register as cluster member"]:::ok REG --> ACK["cluster_member_accepted"]:::ok START --> WRK["darkbloom-cluster-worker, macOS 26.2 build"]:::ok WRK --> JACCL["Real JACCL group"]:::ok JACCL --> XPORT["Collectives over RDMA: exact on two Macs"]:::ok XPORT --> RUN["Two-rank generation of the real model"]:::ok RUN --> SERVE["Served by start --distributed and the coordinator"]:::pending OP --> SETUP["darkbloom cluster: detect, one approval, ready"]:::okGreen is exercised by a test or a run recorded below; amber is built but not yet run on two Macs.
What changed
Native worker package (
libs/darkbloom-cluster-worker, new). The provider and the shared cluster modules build for macOS 14, where the pinnedmlx-swiftcompiles the JACCL stub. A sibling package with a macOS 26.2 minimum builds the real backend with no submodule pin change.build-native-worker.shrefuses a binary that holds the stub or whose Mach-O minimum is not exactly 26.2. Products:darkbloom-cluster-worker— the rank worker the installed plan launches (from Preserve shared distributed runtime and provider integration #1226).darkbloom-cluster-collective-check— both ranks run it; verifies reductions, point-to-point transfers and ordering byte for byte. No model.darkbloom-cluster-stage-check— one Mac, one rank, the real artifact, no collective: verified stage load, release, and memory before and after.darkbloom-cluster-reference,darkbloom-cluster-pair-check— a single-Mac staged reference, a two-Mac driver (rank 0 local, rank 1 over SSH, never signals a worker) and a comparator with a closed set of verdicts (exact,tokensEqualLogitsDiffer,divergedAtNearTie,diverged,incomparable).A pair could not decode before this PR: rank 0 handed over its residual while Metal completion handlers still held the buffer, and the sender refuses storage it does not own alone. One GPU stream synchronization on rank 0's path fixes it (
fix(cluster): drain the GPU stream before rank 0 hands over its residual). That change is on the serving path and wants a careful read.A rank's last resort against a native call that never returns is now a deadline thread (
ProcessDeadline), not onlyalarm: on real hardware a rank spinning in the RDMA completion poll ran more than nine minutes past its alarm.Coordinator. Moved to
Layr-Labs/darkbloom-platform(draft pull request 6 there): the member acknowledgement, the pair lifecycle and quarantine release, pair formation, routing an owner's requests to its pair's leader, and the owner views. Nothing undercoordinator/differs from master here.Provider
darkbloom cluster(no subcommand): guided link setup. It checks RDMA, waits for a Thunderbolt connection, and if the connected port has no IPv4 address of its own it gives it a link-local one through one macOS approval prompt and installs a small root launchd job (one file under/Library/LaunchDaemons, two system tools, no installed program) that re-adds the address whenever the port has none; then verifies. A one-time address proved not durable on a Mac whose port is a bridge member: it was gone about two hours later with no restart or replug.--temporarykeeps the address-only path,--dry-runprints the exact commands and changes nothing, andcluster link --removeremoves the job, its file and the address.cluster link --fix,--removeand--watchare the pieces. Darkbloom never calls sudo. The root job has not run behind a real approval yet.--cluster-memberstart mode; 21 research test files ported.cluster_membershipin the member registration, the pair approval saved bycluster configure, the member control installed before connecting and attached at acceptance. This build declines every pair preparation on purpose: a coordinator-formed pair needs an owner mode, a worker bootstrap and a rank-1 request channel that are not in the tree, so a member answers 503 instead of half-serving.darkbloom cluster console(and baredarkbloom clusteron a terminal that can draw): an interactive screen over the operations above. Readiness comes from the link probe and doctor, pairing and trust from the saved setup with an explicit approval that counts only once the whole question has been on screen for 500 ms, the admitted models from the runtime's own list; it can fix the link, start and stop the local session, recover a stranded journal and write a redacted diagnostic export.handoff/TUI-wiring.mdlists 53 screen items: 31 wired to a real operation, 10 whose operation exists but is not connected, 12 with no operation yet, and a check fails if the screen shows anything outside the wired set.--plainand--jsonprint the same state without a terminal.darkbloom cluster link [--json], also run bycluster doctor: read-only inspection of RDMA state, port state, interface address, bridge membership and the IPv4-mapped GID, with a closed set of verdicts. No address, MAC or GID is kept from tool output.provider.tomlowner-only and atomically; a file first created by an ordinary save was 0644 and the cluster readers refused it.start --local --distributed):cluster statusandcluster doctorsay that nothing vouches for the peer that answers.darkbloom cluster recoverclears a journal whose owner died, and refuses while any process still holds the device or carries the recorded epoch.cluster doctorrefuse a worker binary that does not carry the progress guard.Runtime
A second registered model. The resident adapter selects its model from a closed catalog (
QwenResidentModelDefinition), and admission, byte ceilings, capability and the worker's model and cut validation are per registered model. The registered Qwen3.8 27B is admitted with every structural cut; the 9B's receipts, capability bytes and tests are unchanged.Phase split, a second generation mode beside the pipeline, for both registered models: the pair prefills with lookahead, rank 0 hands its committed request state to rank 1 after the first token (one checked transfer per state tensor, verified by rank 1 before it adopts, a 20 s limit bound into the agreement), and rank 1, which holds the whole model, decodes alone and relays tokens to rank 0 in batches of 1, 2, 4, 8 then 16: 41 transfers after the first token for 128 outputs against 1,397 in the pipeline. The worker takes
--generation-modeand advertises the modes it supports; the pipeline stays the default and its agreement bytes are unchanged and pinned by a test. A correctness-only local-socket transport lets two ranks run on one Mac for tests; it and the fault-injection switch are refused unless a worker is started with--qualification-switches yes.Benchmark tooling under
scripts/benchmarks/cluster/(a streamed load generator with a self-test, an isolated serve harness, artifact verification against manifests) and the night's report,docs/reports/2026-10-09-two-mac-baseline-and-model-matrix.md: the single-Mac "before" through the product's serving path on both Macs, and the state of every catalog model on each.Six of twelve slices of delivering a follower's stage over the link (design and measurements in
handoff/DESIGN-stage-transfer.md): a pinned per-tensor content inventory with a generator and an independent Python cross-check, the pin for the registered 9B, the transfer plan and state machines, a loader seam that separates stage metadata from where the bytes come from (all 14 existing load receipts unchanged), stage metadata derived from the pin with no weight file, and the MLX-side intake with astage-check transfermode. In one process on the M3 Ultra a follower's stage (3.98 GB) is received and verified in 1.65 s against 4.72 s for a local load of the same stage, and corrupted, swapped and truncated inputs are refused; the follower is ready about when a local load would be, so the transfer buys a follower with no weights on disk, not time. Nothing has crossed the link or run between two processes, and the load path does not use it yet.Host gate: swap left from earlier is admitted while memory pressure is normal and refused under warning pressure. The previous rule refused any Mac with swap in use. Free-page requirements are unchanged.
Resident admission tests: a real positive path on both ranks from the registered metadata, and one property per refusal. The previous suite could not fail for the reason each case named.
Test plan
Run on an M3 Ultra (256 GB, macOS 27.2) and an M5 Max (128 GB, macOS 27.0), Xcode 27.0, one Thunderbolt 5 cable.
ClusterLinkCheckswith 1,353 expectations)swift test --package-path libs/darkbloom-cluster— 22 tests in 5 suites;DarkbloomClusterWorkerTestsbuilt for 26.2 — 9 testsmake provider-testafter merging master — the same 75 tests fail as on unmodified master on this machine (same 148 issues, same 56 XCTest errors; mostly a missingpagedattention.metaltest resource and MiMo fixtures). The branch adds 142 tests to those targets and none of them faildarkbloom-cluster-stage-checkon the registered Qwen3.5 9B artifact: every supported cut and both ranks on the M3 Ultra, cut 4 and both ranks on the M5 Max. Each loads in about 3 seconds, releases to a few KB active and 0 cached, and both Macs report the same storage commitmentdarkbloom clusteron both Macs: one reported ready; on the other the approval prompt was accepted, the address added and verifieddarkbloom-cluster-collective-checkacross the two Macs: 5.36 GiB of reductions and transfers, 0 mismatches on both ranks, IP byte counters at 0.0003% of the payload63fd73706(after the 27B and stage-transfer commits): cluster library 37 tests in 8 suites; worker package 48 XCTest cases; all 18 check runners (11 library, 3 worker package includingStageTransferChecks, 4 provider; the link runner has 2,072 expectations in 32 groups); the provider buildsdarkbloom cluster --dry-runon both Macs with the new build: the M3 Ultra needs no prompt; the M5 Max prints the fourteen commands the approval would run and changes nothing. The job definition, built by the same command chain, was loaded as a harmless stand-in in a user launchd domain: runs at load and every 10 s, survives six immediate reinstalls, unloads cleanly4e5f70793, built with--build-system nativeas the release tooling builds it: 4,539 tests in 621 suites, 5 failing, the same five that fail on unmodified master on this machine (three MiMo tests that need CI's fixtures, two that depend on the home directory). With the toolchain's default build system 75 fail on master too, because nested resource bundles are not found657a22d8f(phase split and console): library tests 72 in 10 suites; worker package tests (16 and 41); 19 self-contained check runners, the console's with 11,689 expectations in 23 groups on pseudo-terminals; the full provider suite, native build system, 4,544 tests in 622 suites, failing only the five environment tests that fail on masterq, Ctrl-C, SIGTERM and SIGHUP restore the terminal; a second instance is refused; fifteen existing read-only commands print the same output as before895aece69and the test-only12250136a(memory gate second version, stage-transfer slices 3, 4 and 6, installed-path mode selection): library tests 102 in 14 suites; worker package tests; 20 check runners (the worker startup checks 15 of 15 against a built worker); the full provider suite, native build system, 4,548 tests in 623 suites, failing only the five environment tests that fail on masterdarkbloom start --local --distributedon the two Macs, then the lifecycle faultsmake docs-checkreports one broken link, indocs/reports/2026-10-03-all-model-prefix-qualification.md; it is on master (from #1326) and unchanged here.make docs-impact-checkpasses.Components touched
Layr-Labs/darkbloom-platformProtocol / interface changes
No protocol/interface changes
Yes — described above and matching side updated
The provider registers
cluster_membershipin the member role and expectscluster_member_accepted; the coordinator side of both is in darkbloom-platform draft 6. A coordinator without it ignores the fields and never sends the acknowledgement, and the provider's negotiation timer then ends a distributed start.New CLI:
darkbloom cluster(guided setup),darkbloom cluster link,darkbloom cluster recover.cluster doctornow also fails when the local link is not ready or the worker binary has no progress guard.cluster statusreports the bootstrap selection and the progress limit.New optional worker argument
--startup-deadline-uptime-nanoseconds; exit status 123 when it expires. New optional worker arguments--generation-modeand--qualification-switches;supportedGenerationModesin the capability record, omitted for a pipeline-only runtime so existing records keep their bytes. An owner and a worker must come from one tree: an owner without this change refuses a new worker's capability record.New CLI:
darkbloom cluster console [--json|--plain] [--dry-run] [--temporary].New decision-receipt policy identifier
registered_dense_selected_stage_load_resources_v2.New runtime model ID
registered_qwen38_27b; the worker validates--model-idand--stage-cutagainst the closed catalog. New optional fieldcontentInventorySHA256on a registered specification, in no existing fingerprint.darkbloom clustergains--dry-runand--temporary;cluster linkreports gainaddressKept, andguidancecan be non-null while the state isready.No release artifact or version change.
Documentation impact
docs/reference/cluster-control-protocol.md,protocol-messages.md,configuration.md,docs/provider/installation.md(uninstall removes the link job), and the two package READMEs. Coordinator-only pages are back at master's text; citations of coordinator files that moved are marked as paths in the platform repository.make docs-impact-checkpasses;make docs-checkreports only the broken link that is on master.Not shown by this PR
handoff/DESIGN-pair-prefill-engine-decode.md; by its arithmetic the gain for this model is a few percent at the one measured pair configuration. A layer pipeline helps prefill and models that do not fit one Mac; tensor parallel is not in this runtime.libs/mlxorlibs/mlx-swift. Until it does, the guarded JACCL exists only in builds that check out the mlx branch by hand, and the installed path refuses to serve without it.handoff/DESIGN-resource-gate-v3.md). The review's concern about the first version was confirmed on hardware: with the cache held mapped and read by another process, a load on the M5 Max compressed 18.4 GiB while the pressure level still read normal; that is what the growth check is for. Run on both Macs for both models: the after-download state, cache made of the model being loaded, cache held by another process, two loads started together, and held loads that must still be refused. Not done: each of its commits built and tested on its own, and the comparison of the final binaries with the ones the hardware runs used. Small Macs are still refused from the after-download state (a 32 GB Mac never admits a 10 GiB stage). This is a resource-admission policy change and wants a decision before it is relied on anywhere else.Notes for reviewers
Refactor pass. The link work had its own refactor pass and a second read-only review. The coordinator half had both before it moved. The library changes for the 27B and the stage-transfer slices have had correctness reviews by their authors' reviewers but not yet the dedicated refactor pass this repository requires; it will run over the library once the branches still in flight (phase split, memory gate) have landed, and this pull request stays a draft until then.
A wait that could hang. Two provider unit tests in the suite "Owned native member invocation" failed intermittently in CI (12 of 30 isolated runs locally). The cause was a product defect, not the tests:
Process.waitUntilExit()for a process started from a dispatch thread returned late and was sometimes never reported, so a member never learned its owner had exited, stayed quarantined and never released its pair.ClusterProcessExitnow takes the exit from the termination handler and latches it, at the remote endpoint, the worker process pump and the capability probe; no bound was loosened and no signal added or moved. A check runs 300 sessions from a dispatch thread and fails at about session 47 on the old files. After: 0 of 30 isolated suite runs. Worth a reviewer's eye: the one SIGKILL that can reach a worker is in the worker process pump, after the worker's own hard deadline, SIGTERM and a further margin (the documented exception inClusterWorkerSignalPolicy); and the endpoint can SIGKILL an owner past its ceiling, which would leave a still-live worker with a closed command stream and nobody to clear the journal.AGENT-HANDOFF.mdandhandoff/are working documents (status by gate, gap map, evidence ledger, runbook). They are here so the state is reviewable; drop them before merge if they do not belong in the tree.The swap-gate change is a resource-admission policy change. The previous rule came from the research measurements; I found no measurement behind it, but it wants a second opinion.
🤖 Generated with Claude Code