Skip to content

Pool: reconciler, controller, membership, fleet simulator and placement planner - #74

Open
data-angel wants to merge 11 commits into
mainfrom
pool-integration
Open

data-angel wants to merge 11 commits into
mainfrom
pool-integration

Conversation

@data-angel

Copy link
Copy Markdown
Contributor

Summary

Integrates the pool management feature set (branches pool-head-sim and pool-planner-phase1) into current main:

  • Pool reconciler & controller (src/pool-reconciler.mjs, src/pool-controller.mjs): converge pool state, with controller preemption and admission kept below the memory guard (guard aborts are learned from).
  • Pool membership (src/pool-membership.mjs, src/pool-membership-store.mjs, src/pool-node.mjs): enrollment tokens, node leases with node-id takeover protection, and a planner bridge.
  • Fleet simulator (src/pool-sim.mjs, src/pool-sim-cli.mjs): deterministic scenario-driven simulator (lloom pool simulate) with membership leases; scenarios in test/fixtures/pool/scenarios/.
  • Dry-run placement planner (src/pool-planner.mjs, lloom pool plan): plans pool placement against the pool catalog/inventory without mutating state.
  • Supporting docs (docs/pool-planning.md, updates to docs/clusters.md, docs/architecture.md, docs/runtime-policy.md, README.md, CHANGELOG.md) and 9 new test files (109 pool tests, all passing).

Integration notes

  • package.json and CHANGELOG.md were resolved against current main (the branches forked ~30 commits back); pool sources/tests were added to check:js and test:unit.
  • npm run check:js passes; full test:unit passes (one unrelated flaky research-workers timing test failed once and passed on 4 subsequent runs, including on clean main).

Sanitization

Reviewed the full diff for credentials, tokens, local paths, and private hostnames: none present. All content is product code, docs, and test fixtures.

data-angel and others added 11 commits October 11, 2026 11:37
Treat every cluster node as one pool and propose which model lanes run
where, with which residency, and how each alias routes, plus the diff
from what runs now. Phase 1 is read-only: nothing is started, stopped or
rerouted, and per-node admission stays the safety layer.

- Node inventory (/gateway/node?inventory=1, cached): structured compute
  archs, memory domains, model-root disk, cached recipe weights, commands
  and Docker images, and NVIDIA Sync fabric links.
- Model-centric catalog from managed runtimes, federated targets,
  unmanaged lanes declared with a model `pool` block, external models and
  (opt-in) bundled recipes, with measured footprint and live-metric or
  benchmark performance.
- Pure planner: memory per domain with headroom, TP only across directly
  linked nodes, arch/platform/accelerator matching, pins and cordons as
  hard constraints; lexicographic objective (coverage, external fallback,
  SLOs, demand-weighted latency plus load-time churn); greedy placement
  plus single-workload local search. Models sharing a runtime share one
  placement.
- Per-alias demand: /gateway/metrics adds `requested` buckets.
- Recipes may declare requirements.computeArchs, requirements.tensorParallel
  and settings.maxSequences (schema, validator, example).
- Admission and pool planning share one memory-budget helper.

Fixture tests reproduce the three-Spark fleet: all nodes give the
sparkqwen-pair-rc16 profile; cordoning the pair gives standalone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- pool-reconciler: orders a plan into make-before-break steps (prefetch,
  start, route, drain+stop), swaps through interim members, residency
  downgrades before stops; dependency-aware executor skips dependents of a
  failed step so a lane that never starts never strands its aliases.
- pool-controller: off/advise/auto loop with urgency (coverage restored),
  stable-plan confirmation, cooldowns, an hourly load budget, slower
  hysteresis for residency-only changes, and failure backoff.
- pool-sim: discrete-event fleet model (requests, admission + LRU eviction,
  keep-warm restore, memory guard aborts, node hangs/failures, operator
  actions) driving the real planner/reconciler/controller; scenarios for
  pair taken, pair crash, a Mac joining, a low memory estimate and noisy
  demand, compared against today's behaviour (off).
- planner: budgets capped at the runtime memory guard ceiling (0.9 by
  default) so plans never rely on loads the guard would abort; variants can
  exclude nodes after failed starts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nodes can join a head with a one-time enrollment token and keep a lease
with heartbeats from `serve`, so the pool planner knows who is in the
pool. Membership is inert until a token is issued; static cluster.nodes
config and `cluster add-node` are unchanged.

- src/pool-membership.mjs: pure membership core (tokens, enroll,
  heartbeat, cordon/drain/leave, ready/suspect/lost/left leases, planner
  node shape). Tokens and node secrets are stored only as SHA-256 hashes.
- src/pool-membership-store.mjs: pool-membership.json next to the config,
  loaded on first use, written atomically (0600) only on changes.
- Head routes: POST /gateway/pool/enroll (token only, rate limited) and
  /gateway/pool/heartbeat (node secret only) authenticate in their
  handlers; GET /gateway/pool/members is admin-read; tokens and
  cordon/uncordon/drain are admin writes.
- src/pool-node.mjs: `lloom pool join` and the serve heartbeat loop with
  backoff; the secret lives in a private file or an env var.
- CLI: lloom pool token create | members | cordon | uncordon | drain | join.
- Planner: members merge with configured nodes (config wins on endpoint
  and auth; membership adds liveness, cordon and inventory). Enrolled-only
  nodes are planned but flagged as needing `cluster add-node` to route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fleet simulator showed admission could approve a load the runtime
memory guard was certain to abort: admission kept only reserveMemoryGb
(8 GB) free, while the guard trips at 90% host use (12.8 GB free on a
128 GB Spark). After an abort the runtime is latched until a manual
start, so the lane stayed down.

- runtime-policy: the host check keeps max(reserve, guard margin) free and
  cluster node budgets are capped at the guard ceiling.
- runtime-memory-safety: memorySafetyCeiling() shared by the guard,
  admission and pool planning.
- runtime-manager: a guard abort records usedGb and consumedGb (the load's
  lower-bound footprint) and status reports memorySafetyLatched.
- pool-catalog: raises a variant's memory to the abort lower bound and
  excludes a latched lane until it is started by hand.
- reconciler: per-step timeouts so a stuck step cannot wedge the
  controller; simulator found and now covers a drain that never settled.
- simulator: models the latch and fixed admission (legacyAdmission keeps
  the old behaviour for comparison) and logs controller hold reasons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The simulator can run nodes through pool membership (enroll, heartbeat,
  lease loss) so node loss is detected the way a head detects it; new
  lease-loss scenario (crash plus a harmless 60 s partition).
- The scenario found the controller blocked for 30 minutes on a manual
  restart of a lane whose node had just died. Stability now tracks the
  moves, not just the plan; an urgent plan for a different target cancels
  a change in progress (executeReconcile takes an AbortSignal); the same
  target never preempts itself.
- Reconciler re-issues an idempotent start for lanes still loading so
  route flips wait for them.
- Register membership tests in check:js and test:unit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- lloom pool simulate <scenario> [--mode off|advise|auto|compare]: runs a
  scenario through the planner, reconciler and controller; exits non-zero
  when expectations fail.
- docs/pool-planning.md: reconciling, the controller, simulation and the
  scenario results; runtime-policy.md: the guard margin in admission.
- Membership tests sliced the token body with split('_'), but base64url
  bodies can contain '_', so a short fragment sometimes matched the state
  JSON; slice the fixed prefix instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security review findings:
- Medium: any enrollment token could re-enroll any node id, replacing a
  live node's secret, endpoint and reported capacity (and flipping a
  configured peer unavailable in plans). Tokens can now be bound to a node
  id (lloom pool token create --node-id); claiming a current member or a
  configured cluster.nodes id needs a token bound to it (409 otherwise).
  lloom pool leave / POST /gateway/pool/nodes/:id/leave frees an id.
- Heartbeats are refused before the body is read unless the bearer has the
  node-secret shape, and are rate limited per client.
- Rate limiters evict least recently used clients instead of clearing the
  whole map when full.
- Node-reported text with control characters is dropped (no escape
  sequences in lloom pool members output).
- join reports the saved secret path and the pool.head to set when the
  config update fails after enrolling, and warns about plain HTTP heads
  off loopback and tailnets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugs (each with a test):
- A model that moved between nodes left its old lane running: an unplanned
  lane is now retired once every planned lane serving its models is up.
- Lanes already stopping were treated as freed memory; they now hold it and
  get an idempotent stop that starts on that node wait for.
- The planner could plan past the guard when only memorySafety was set; the
  catalog now passes memorySafetyCeiling() itself.
- A cluster node with unknown capacity got a negative admission budget,
  blocking no-request plans; the clamp is floored at zero, and the guard
  margin rounds up.
- Memory-safety abort evidence never cleared; a healthy start clears it and
  a measured footprint supersedes it.
- An alias moved to an interim member during a swap was never moved back;
  the flip back is queued and ordered after the interim flip.

Hardening: ticks never overlap; throwing observers cannot wedge the
executor or controller; concurrency is at least 1; a lane already loading
is not charged to the load budget again; one move larger than the budget
may go in an empty hour; only route changes preempt; vacating a cordoned
or draining node is urgent. The simulator reports draining lanes as
stopping and the controller's last plan, and memory-estimate-low now
asserts the abort, the latch and video planned as uncovered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…anner

Brings in origin/pool-head-sim (which includes pool-planner-phase1):
pool reconciler/controller, membership enrollment tokens and node
leases, deterministic fleet simulator, and the dry-run placement
planner. package.json and CHANGELOG resolved against current main,
with pool sources/tests added to check:js and test:unit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant