Repository navigation
feat(cluster): add qualification, staging, planning, and deployment - #4
Closed
jonathan308 wants to merge 138 commits into
Closed
jonathan308 wants to merge 138 commits into
jonathan308 wants to merge 138 commits into
Conversation
7 tasks done
Make the audio upload limit configurable through settings, environment variables, CLI, and the admin API while preserving the existing 100MB default.
Adds first-class Qwen3.8 Flash Next and Qwen4-Exp support, including multimodal preprocessing, QSA cache handling, and external PLE SSD mmap loading.
…t#3174) Adds production Qwen3.8 Flash Next text/image support with prefix-cache parity, Lightning MTP, oQ conversion, and PLE SSD mmap handling.
…t#3186) Co-authored-by: jundot <[email protected]>
…xt (jundot#3200) Qwen3.8 Flash Next uses a dedicated VLM Lightning MTP path that intentionally bypasses the generic mlx-lm compatibility whitelist. Allow qwen4_exp checkpoints with embedded MTP weights through the admin compatibility and save gates.
Bumps [ddgs](https://github.com/deedy5/ddgs) from 9.15.0 to 9.16.0. - [Release notes](https://github.com/deedy5/ddgs/releases) - [Commits](deedy5/ddgs@v9.15.0...v9.16.0) --- updated-dependencies: - dependency-name: ddgs dependency-version: 9.16.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
_extract_numeric_answer falls back to the last number in the response
when the model does not emit the "#### N" marker. Its fallback pattern
-?[\d,]+(?:\.\d+)? also matches a bare comma, because [\d,]+ needs only
one character from the class. Any comma after the final digit therefore
became the last "number" in the text, and .replace(",", "") turned it
into an empty string.
"The total is 72 clips, altogether." extracted "" instead of "72", so
check_answer returned False on a correct response and the question was
scored wrong (parse_error on the external-API path).
Anchor the pattern on a digit at both ends: -?\d(?:[\d,]*\d)?(?:\.\d+)?.
Thousands separators still match, since they sit between digits. The
"####" branch uses the same pattern.
All 1,319 gold answers in omlx/eval/data/gsm8k_test.jsonl extract
identically before and after, so scoring keys are untouched.
…e benchmark (jundot#3434) `OMLX_QWEN35_QMM_NAX_VARIANT=6` selected a 64x64x64 tile with wm=1, wn=4, i.e. a 16-column per-simdgroup N tile. On M5 Max / mlx 0.32.2 it returns wrong output: max abs error 5-8 against stock `quantized_matmul` at every tested shape, bit width (4/8) and group size, while variants 0-5 agree with stock to within a bf16 ulp. Remove the instantiation and the C++ table entry, validate the env knob in Python (out-of-range or non-numeric values warn once and fall back to variant 0 instead of raising from inside a prefill), and add a hardware test that checks every bundled tile against stock for q4/q8 x gs64/gs128. benchmarks/bench_qwen35_nax_qmm_variants.py times stock vs each bundled tile at Qwen4-Exp GDN/attention projection shapes. Results on M5 Max (mlx 0.32.2, ms per call, lower is better): q8/gs64 T stock v0 v2 v2/stock gdn.in_proj_qkv 8192 18.97 18.14 15.01 0.79 gdn.in_proj_z 8192 10.18 10.25 9.18 0.90 attn.q_proj 8192 19.57 19.38 17.16 0.88 attn.o_proj 8192 10.06 10.17 9.76 0.97 q4/gs64 gdn.in_proj_qkv 8192 8.14 7.99 8.96 1.10 attn.q_proj 8192 17.24 17.11 15.96 0.93 Variant 2 (bm=128) wins 5-20% at 8-bit but is mixed at 4-bit, so the default stays at variant 0 (the stock tile); the knob and the benchmark are there for per-model tuning. Claude-Session: https://claude.ai/code/session_01FUwVt6F1YHbTLvNvmAGEvU Co-authored-by: Claude Fable 5.1 <[email protected]>
jundot#3469) * test: pin MLX_ENABLE_TF32=0 under pytest so fp32 parity tests hold on M5 MLX 0.32.2 runs fp32 GPU matmuls through the M5 tensor unit at TF32 precision by default (128x128 fp32 GPU-vs-CPU max abs error 3.5e-2 with TF32, 1.9e-5 without). Four existing gathered-vs-dense QSA parity tests assert 2e-5 in fp32 and fail on an untouched tree on M5 hardware. tests/conftest.py now pins it off for the test session only; production servers still run with TF32 enabled. * perf(qwen4): route Qwen4ExpRMSNorm through mx.fast.rms_norm Qwen4ExpRMSNorm spelled RMS norm out in MLX ops -- fp32 upcast, optional grouping, y * rsqrt(mean(y^2) + eps) * (1 + w) -- where mx.fast.rms_norm exists. The class is built at 11 sites: the two hyper-connection norms per layer, the attention query/key layernorms, the indexer norms and the PLE layer, so the hand-rolled chain ran a few hundred times per forward. rms_norm takes a 1-D weight, so the grouped case (hc_norm: 10240 in four groups of 2560) cannot hand it the per-group scale; it normalises over the group axis and applies the scale after. The scale stays fp32 either way -- rounding (1 + w) to bf16 costs the full half-ULP bound, ~3.9e-3 relative. Not bitwise: the kernel sums the squares in a different order than mx.mean, so a value occasionally lands on the far side of a bf16 rounding boundary. Measured over 3.1M elements per weight magnitude: 1-8 ppm of elements move, never more than one bf16 ULP, and the rate does not track weight scale. For comparison, running the same row through a 1-row versus a 4-row forward already moves 2.4% of elements by up to 64 ULP, so this sits far below variation the model exhibits between one request and the next -- a bar the new tests assert directly rather than assume. * perf(qwen4): dispatch each decoder layer to the GPU as it is built With one async_eval after the whole forward, the GPU sits idle while Python builds the next ~8.5k-node decode graph and MLX encodes it. Calling mx.async_eval on each decoder layer's output during the layer loop (decode and verify rows only, <= 64 rows) lets the GPU execute layer i while the host builds layer i+1. Scheduling only: outputs are bit-identical, which the ABBA confirms (identical cycle counts and acceptance in every arm). Qwen3.8-Flash-Next-oQ4e-mtp on an M5 Max, interleaved ABBA: serial decode (MTP off): +18..22% tok/s (code 5k 44.6 -> 53.5) Lightning MTP depth 3: -11.5% ms/cycle (44.1 -> 39.1), 65.8 -> 75.9 tok/s at code 5k Disable with OMLX_QWEN4_EAGER_DISPATCH=0. * perf(qwen4): fused hyper-connection kernels for decode and verify rows Qwen4ExpGatedResidual._forward builds ~30 MLX nodes per call and runs 97 times per forward; under Lightning MTP the host-side kernel encoding of that graph, not its GPU time, dominates a verify cycle. Three row-batched Metal kernels replace it for batch*seq <= 16: a per-stream RMS norm (bit-identical to mx.fast.rms_norm), the down and inject projections split over four K-slices with the silu / 2*sigmoid epilogues folded in, and the up projection with the sigmoid gate, stream mix and mean. The affine unpack helpers come from hc_projection; 4/5/6/8-bit gs64 weights are supported, with separate bit widths for the down and inject projections. Results stay within a few bf16 ULP of the canonical path and at least as close to fp32; the path fails closed. Qwen3.8-Flash-Next-oQ4e-mtp on an M5 Max, interleaved pinned-depth ABBA on top of eager dispatch: MTP depth 3 -6.5% ms/cycle (39.2 -> 36.6), 68.7 -> 75.3 tok/s mean over four prompts. Serial decode is GPU-bound after eager dispatch and does not change. Disable with OMLX_QWEN4_HC_FUSED=0. * fix(qwen4_exp): require hidden % 512 for the fused hyper-connection path The fused down projection walks each K-slice (= hidden) in blocks of 32 lanes x 2 packs x 8 values = 512 elements for 4/5-bit weights with no tail handling, but compatible() only asked for a multiple of 256. A hidden size such as 768 (4-bit) or 1280 (5-bit) passed the check and the last block read past the slice: 77 and 88 bf16 ULP off the canonical path on an M5 Max. 6/8-bit weights (block 256) and the shipped checkpoint (hidden 2560) were unaffected. Require hidden % 512 == 0 for every bit width and drop the redundant (hc_count * hidden) % 512 check. Tests: compatible() rejects 768/1280, and the parity sweep now also covers hidden 512 and 1536. * perf(qwen4_exp): handle partial final blocks in the fused hyper-connection kernels Instead of gating the fused path on hidden % 512, let it take any hidden_size that is a multiple of 64 (the quantisation group and the up kernel's grid unit): * norm kernel: PER = ceil(H / 256) with a k < H guard on the loads and the store; * down and inject loops: the main loop stops at SLICE - SLICE % BLOCK and a lane-guarded extra iteration covers the remainder. The unpack helpers read exactly VPT values and VPT * BITS / 8 weight bytes, so no lane touches memory past its slice. The inject loop's separate "safe" last block goes away: hc_qdot_safe is byte-for-byte hc_qdot in the shared header, so the loop shape is now the same as the down loop's. For the shipped geometry (hidden 2560) every tail is zero and the guards fold away: old and new kernels are bit-identical for 4/5/6/8-bit weights at 1, 4 and 16 rows. compatible() also logs once, at INFO, when a Qwen4-Exp model stays on the canonical path because of its layout, so a future checkpoint that misses the fused kernels is visible in the server log rather than just slower. Tests: parity sweep at hidden 512/768/1152/1344/1536 x 4/5/6/8-bit (tails of 64, 128, 256 and 320 elements across the three kernels), norm bit-identity at 768/1152/1344, rejection below 64, and the one-time log. * fix(qwen4_exp): do not log the exact-hybrid projection as a fused-path gap With Lightning MTP off, qwen4_exp.load_weights installs upstream's exact hybrid projection on every eligible hyper-connection (91 of 97 in the shipped checkpoint), and those modules keep their compiled single-token path. compatible() must still step aside for them, but that is a runtime choice, not a checkpoint layout, so the one-time "kernels not used for this model" line no longer fires for it. It still fires for a real combined input_inject_weight layout. * perf(qwen4_exp): run the fused hyper-connection kernels ahead of the exact hybrid projection With Lightning MTP off, qwen4_exp.load_weights flags 91 of the 97 hyper-connections in the shipped checkpoint with upstream's exact hybrid projection and compiles their single-token _forward. compatible() used to step aside for those modules, so in serial decode the fused kernels ran on 6 modules and the other 91 paid a compiled call plus its own kernels each token. Let the fused path take precedence for the rows it covers. _forward is untouched and keeps the hybrid projection for everything else. Serial decode on the M5 Max, interleaved env A/B on this branch (High Power, MTP off, 512 tokens): code 5k 55.2 -> 62.8 tok/s (+13.8%) code 41k 50.6 -> 56.8 tok/s (+12.2%) prose 5.8k 54.9 -> 62.4 tok/s (+13.6%) prose 34k 51.1 -> 57.6 tok/s (+12.8%) MTP verify is unchanged (the hybrid is never installed with MTP on). Serial outputs now match the canonical path to a few bf16 ULP, as the MTP path already did, instead of bit-exactly.
) * feat(cluster): add v2 node identity module NodeIdentity (uuid4 node_id, scutil-derived friendly_name, schema_version 1) persisted atomically at 0600 to ~/.omlx/cluster/identity.json, with friendly -name collision repair (-2 suffix), rename(), and corruption handling that never silently rotates node_id. Offline unit tests cover persistence, collision, rename, and corrupt-file behavior. * feat(cluster): add v2 DeviceRegistry for paired/discovered devices DeviceRegistry persists paired devices atomically (0600, schema_version 1) to devices.json while discovered-but-unpaired devices stay memory-only. Merge API dedupes on node_id, never downgrades paired trust, and is shaped to accept discovery PeerRecords directly. Fails closed on corruption like ClusterRegistry. Existing ClusterRegistry untouched. * feat(cluster): add v2 PeerRecord + DiscoveryService (mDNS/multicast/manual/tailscale) Layered peer discovery appended to omlx/cluster/discovery.py without touching the existing dns-sd Bonjour code: _omlx._tcp.local. via optional python-zeroconf (absence disables mDNS only), exo-style IPv6 link-local HELLO/WASSUP on ff12::6f6d:6c78/53413 with per-interface joins, nonce-echo self-drop, higher-node_id-initiates-contact, blake2s cluster-hash filter (log-once on mismatch), verification probes against /api/cluster/node_id with node_id-mismatch address drops, RTT-based link classification, and discovered/suspect(6s)/dead(30s) liveness with on_change callbacks. multicast_ok reports whether a foreign HELLO arrived within 5s so the UI can warn about macOS Local Network permission. 30 offline tests; sockets, prober, clock, interfaces, tailscale, and zeroconf all injected. * feat(cluster): wire v2 discovery endpoints and service into server New omlx/cluster/discovery_routes.py with GET /api/cluster/node_id (public, per-IP token-bucket rate limited, 0600 identity only) and GET /api/cluster/devices (admin-gated; {paired, discovered, self} plus multicast_ok/mdns_available signals so the UI can warn about macOS Local Network denial instead of showing silent zeros). discovery_router registered in _register_cluster_routes behind the distributed-inference exposure flag only. Identity + device registry configured at startup; DiscoveryService started/stopped in the server lifespan when the feature is enabled (OMLX_DISCOVERY=0 opts out). zeroconf added as the 'cluster' extra. * test(cluster): cover mDNS announce/failure paths; whitelist v2 test hooks in seams lint * cluster v2: code-based pairing + enrollment bridge (Module B) PairingManager drives the existing fail-closed SSH TOFU primitives (ssh_keys install_authorized_key / add_verified_peer_host_key) instead of the 3-step copy-paste relay: 6-digit code shown on the joiner, 10-minute validity, 3 attempts then 10-minute lockout, blake2s(code+node_id) on the wire, 32-byte cluster_key wrapped under PBKDF2-HMAC-SHA256 (100k iters), JSON-lines audit log, deny + unpair with SSH trust revocation. Endpoints under /api/cluster: pair/request + pair/status (public, pinned by the out-of-band code) and pair/approve + pair/deny + DELETE devices/{node_id} (admin). Wired in server.py alongside the existing cluster routers; legacy relay endpoints kept working and marked advanced/legacy. Module A registry/identity are feature-detected with schema-compatible local fallbacks until that branch lands. * cluster v2: offline pairing tests (loopback, lockout, expiry, unpair, legacy) Two-manager loopback happy path via injected transport; wrong-code 3-strike lockout (correct code refused inside window, code expired after it, attempts reset when a lockout ends inside validity); expiry; deny; unpair revocation (device store + key store + legacy enrolled-nodes + SSH material); key/device store persistence, 0600, fail-closed corruption; Module A registry bridge feature-detection; endpoint error mapping (400/403/404/410/422/423/502/503); legacy 3-step relay route registration + HMAC token non-regression. * cluster: add GET /api/cluster/discovery/health (seam a) Pinned shape from Agent C's fixture: multicast_rx_within_5s, last_multicast_rx_at, mdns_active, transport. DiscoveryService now records a wall-clock timestamp alongside the monotonic hello marker and exposes last_multicast_rx_at / mdns_active / transport_summary(). Admin- gated like /devices; fixture shape matched exactly, no UI changes needed. * cluster: wire pairing onto Module A DeviceRegistry (seam b) - server.py startup: configure identity + device registry BEFORE the pairing manager and pass get_device_registry() as registry=, so pairing approvals persist into the real registry instead of the fallback store. - DeviceRegistryBridge.put_paired: detect mark_paired first and call it with keyword fields. The real DeviceRegistry.mark_paired takes kwargs, and its merge() persists only already-paired devices — routing an approval through merge would have left new peers memory-only. - load_node_identity: Module A ships load_or_create as a module-level function (not a NodeIdentity classmethod); call it with the base_path identity.json so pairing and discovery share one identity. - Tests: bridge driven against the real DeviceRegistry (persistence, unpair); full pairing suite green. * cluster: surface pending pair requests in /api/cluster/devices (seam c) PairingManager.pending_requests() rows are merged into the discovered list with state 'awaiting_approval' (pending state wins for a node that is both discovered and awaiting approval), matching the wizard fixture devices_pending_approval.json. Module A keeps endpoint ownership; the pairing manager is looked up lazily and its absence changes nothing. * cluster: settings-level cluster_name via persisted cluster.json (seam e) ~/.omlx/cluster/cluster.json ({schema_version, cluster_name}, atomic 0600 write) is the settings-level cluster name: server startup feeds it into DiscoveryConfig, and the node_id probe reports it even when the discovery service is disabled. Missing/malformed file falls back to 'omlx' — pre-config behavior unchanged. * cluster-v2 discovery: macOS resilience overhaul + manual peer add Field-diagnosed on the Studio/M5 test pair (macOS 26.5.2, TB5 direct link): - _sync_interfaces now drops vanished interfaces and detects interface renumbering (TB hotplug), re-joining the group under the new ifindex. Previously a stale ifindex silently wedged sends with EHOSTUNREACH forever (observed: 7,085 failures in 9 min, then total silence). - HELLO sends now carry the egress interface as the destination scope id (4-tuple) instead of mutating IPV6_MULTICAST_IF on the shared socket. - WASSUP unicast replies preserve the HELLO source's scope id; without it replies to link-local senders have no route and the handshake could never complete even when multicast flowed. - Background loops run under a supervisor that logs CRITICAL and restarts them on crash, and warns + restarts on unexpected clean exit; the datagram handler is exception-guarded; the loop rebuilds the socket after 3 consecutive all-failed HELLO rounds and clears its join state on exit so a supervised restart starts clean. - Per-interface/per-peer rate-limited failure logging (60s) ends the one-line-per-second-per-interface log spam. - New GET /api/cluster/discovery/health/detail (loop liveness, socket state, joined interfaces, TX error/counters) so 'no peers' is distinguishable from 'discovery dead'. The pinned wizard fixture shape on /discovery/health is untouched. - New POST /api/cluster/devices/manual {ip, port?} — deterministic peer add over the Thunderbolt 10.0.0.x pair (or any IP), verified through the standard HTTP probe path, admin-gated + rate-limited. - /api/cluster/devices self row now reports real local addresses instead of a hardcoded empty list. Tests: 17 new (scoped 4-tuple sends, scope-preserving replies, renumber heal, vanished-if drop, TX rate limiting, fail-round socket reset, socket rebuild liveness, supervised crash restart, health detail, manual-add route incl. 400/503 paths). 62/62 discovery tests pass; 421/421 across discovery+wizard+pairing+exposure scope. * cluster-v2 discovery: backoff socket rebuilds, flag suspected Local Network blocks Follow-up to 5ab14c1 from live field observation on the M5 node: when the wedge is process-level (macOS Local Network permission denial — fresh processes with the same interpreter send/receive fine while the server gets EHOSTUNREACH on every interface), per-second socket rebuilds cannot help and only spam the log (1 warning/sec indefinitely). - Socket rebuilds now back off exponentially (2^n seconds, capped at 60s); the streak warning logs once per streak instead of every round. - After 6 fruitless rebuilds the service logs CRITICAL with actionable guidance (Local Network settings / restart / manual-add fallback) and sets local_network_blocked_suspected, exposed via /api/cluster/discovery/health/detail. Any successful TX round or foreign HELLO RX clears the flag and the backoff schedule. - 88/88 discovery + wizard tests pass. * cluster v2: carry coordinator caps through pair/status; detect TB RDMA in local caps - join_status() (the poll path the joiner UI actually drives) now returns the same coordinator block approve() returns — caps + ssh key — so complete_join persists real capabilities instead of empty caps - local_caps() detects the Thunderbolt RDMA fabric via rdma_ctl status + ibv_devices (fail-safe, macOS-only, injectable runner): thunderbolt flag when rdma_* devices exist, jaccl when RDMA is also enabled — the wizard's RDMA check row and jaccl-vs-ring selection key off these Tests: +3 (join_status caps/key, rdma fabric detection on/off/failing); cluster slice 1561 pass 0 fail * cluster v2: fix rdma device parse (ibv_devices output is whitespace-indented) * cluster v2: non-downgrading device-registry merge A discovery HELLO carries structurally complete but value-empty caps ({chip:'', ram_gb:0.0,...}); DeviceRegistry.merge truthiness-replaced the pairing-exchanged record, so a few seconds after pairing every paired device silently lost its chip/ram/fabric flags and its routable addrs — the wizard then planned with ram_gb=0 and dropped the node. Merge is now field-wise: real values win, backends union, fabric flags sticky-true, addrs union (capped), and records without flags keep their shape. * fix(cluster): paired device rows carry paired=true in /api/cluster/devices Registry rows never carried the flag (only the self record set it), so wizard consumers keyed on device.paired -- the per-Mac memory-contributes role list and the device card's Pair/Paired state -- treated an already paired peer as unpaired: the peer's role row was missing and the card kept offering Pair after a successful pairing. List membership is now reflected on the row itself; UI fixtures already expected this contract. * Rehydrate trusted peer endpoints after reboot * Reconnect paired peers through system Python * Expose and force paired peer probes * Discover Tailscale peers from macOS app * Keep paired discovery heartbeats responsive * Heartbeat verified discovery candidates * fix(cluster): make pairing and SSH enrollment fail closed * test(cluster): keep injected RDMA probes host-independent * fix(cluster): serialize pairing completion and roll back partial trust * fix(cluster): rate-limit public pairing endpoints --------- Co-authored-by: jonathan308 <[email protected]>
…fest endpoint
- deployment.py: path_map field, model_path_for(), schema v2 (v1 decodes to
empty map), path_map rides the worker contract, decode_worker_path_map()
- registry.py: deployments.json schema v1->v2 in-memory migration, persisted
atomically on load, migrated_from reported
- planner.py: ShardPlan.path_map passthrough (excluded from plan_hash)
- routes.py: path_map on plan/deployment requests, placement signature folds
path_map when present, staging honors per-node destinations
- launch.py: preflight probes each node at its own path_map path
- inference_worker.py: rank resolves its model path from the signed contract
- modelsync.py: ModelSyncManager (status/sync, auto|rsync|download), manifest
builder + GET /api/cluster/models/{model_id}/manifest, progress events
- tests: 32 new offline tests (manifest fixture, path_map, v1 migration,
sync decisions with mocked transports)
…oder in worker test harness - modelsync.build_rsync_argv now builds its -e command from ssh_policy.cluster_ssh_options() (seam test compliance: no literal ssh argv outside the shared policy) - test_cluster_inference_worker harness fakes decode_worker_path_map next to decode_worker_contract; the fake plan string is not a real contract
allocate_layers_proportional (exact Fraction arithmetic, min one layer per
node, deterministic tie-breaks) plus plan_proportional_pipeline, sharing the
assignment/fit/hash tail with plan_unequal_pipeline via _finish_pipeline_plan.
The balanced planner's hash payload is byte-identical to before. Routes gain
an additive allocation field ('balanced' default) on plan/deployment/replan
requests; proportional is pipeline-only.
…V fit, route wiring
…n→reload) Extract the activation pipeline into _activate_and_report so the replan endpoint and the explicit /deployments approval share one code path. The replan endpoint derives nodes/hosts/backend from the current deployment when not posted, renders a signed preview with a placement diff, and on approval runs the quiescence-gated unload (engine-pool prepare_cluster_ reload) whose distributed supervisor teardown is verified before the registry swap and eager reload.
…, N-node membership change
Agent E flagged the seam: ClusterReplanRequest had no path_map and the replan derivation dropped the current deployment's per-node paths, silently reverting a heterogeneous-path cluster to same-path resolution. Add path_map to ClusterReplanRequest, derive it from the current deployment when not posted (tracked in the 'derived' map), and pass it into the effective ClusterDeploymentRequest. Explicit override (including an empty map) wins. Tests updated + carry-forward coverage added.
The catalogue tie-break preferred pipeline at equal width because it survives slow links. When the paired registry shows every requested peer reachable over JACCL/Thunderbolt RDMA, tensor parallelism is the better recommendation — it splits per-token compute across the group. - _splits/assess_model/catalogue_for_cluster gain prefer_tensor (default False: API behavior unchanged for unknown links) - catalogue endpoint derives it from the paired device registry: every non-self requested node must be paired with jaccl/thunderbolt caps; unknown or unpaired peers fail closed to pipeline - 4 new tests; 33 catalogue + 1702 cluster/model tests green
macOS announces Thunderbolt interfaces as fe80::…%en10; mlx's address parser cannot read the zone id and ranks died at startup with 'Can't parse address fe80::…%en10:56758'. _validate_ip now strips the zone (machine-local by definition, never valid on the wire) and the hostfile orders routable addresses ahead of link-local fallbacks, so a Mac with configured 10.x Thunderbolt IPs never advertises the ambiguous one first. 3 tests pin the exact failure.
jonathan308
force-pushed
the
feat/cluster-v2-planning
branch
from
September 8, 2026 03:33
059ec4a to
3218bf1
Compare
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.
Stack position
2/4 of the backend-first Cluster v2 split requested in jundot#3118.
Depends on the trust foundation in
feat/cluster-v2-trust/ upstream jundot#3155. This draft exists on the fork so its incremental diff and CI can be validated before the dependency merges upstream.Scope
openrsyncDeliberately excluded
Validation before draft
git diff --check: passed