Skip to content

server: one shadow per mind on the serving context: each shadow keeps its own optimizer, /learn trains any loaded shadow, and unload releases it - #85

Open
joelteply wants to merge 1 commit into
889580fc/seq-state-across-stream-countsfrom
cormac/per-mind-shadow
Open

joelteply wants to merge 1 commit into
889580fc/seq-state-across-stream-countsfrom
cormac/per-mind-shadow

Conversation

@joelteply

@joelteply joelteply commented Nov 1, 2026 •

Copy link
Copy Markdown

Continuum card eaeb2b87, S4 (a), the in-place dream stage (ONE-RESIDENT-MODEL §8.1 / §9). Today a serving context learns for exactly ONE mind per server run: a second resident's /learn is refused with "one shadow per server run". This lets every resident on a lane learn in place, each with her own shadow and optimizer.

Change

  • One optimizer per shadow. llama_context keeps one optimizer per shadow (opt_minds), and opt_ctx/opt_shadow are the active pair, so every existing training site is unchanged.
    • llama_opt_init with another adapter on a configured context calls opt_select. It creates that shadow's optimizer the first time and keeps each shadow's moments and accumulators across switches.
    • Only the active shadow's tensors carry PARAM, so a graph never trains a parameter the active optimizer lacks.
  • opt_configured() replaces opt_ctx != nullptr where the question is whether the context is sized for training (reserve skip, graph sizing). Releasing the active shadow would otherwise have rebuilt a scheduler that other shadows' optimizers still hold.
  • Unload releases instead of refusing. llama_opt_release drops one shadow's optimizer. The server's unload now releases a shadow that learned, instead of refusing it forever. Its moments are lost; the snapshot in (d) will be the durable form.
  • The server's single-shadow learn_lora_id is deleted.

Measured (IntelMac CPU, Release, Qwen2.5-0.5B Q4_K_M, -np 1, 2026-11-01)

  • learn-every-format.py --two-minds, fresh server: shadow-05 and a byte copy, shadow-06, learn alternately. Losses: 4.3179, 4.3179, 3.5043, 3.5043.
    • B's first step is untouched by A's step, and each second step continues its own optimizer.
    • Matching trajectories hold only if the weights and optimizers are independent; the test asserts it.
  • --shadow-retirement: a bound shadow unloads, and a reloaded one learns (loss 4.3179, 974 ms).
  • Not built: Metal/CUDA. This is host-side state only; no kernel changes.

Contract for the core: a shadow IS its file

POST /lora-adapters/load deduplicates by path on purpose: a client that dropped after posting and retries must get back the id it never received. So loading the same file twice returns the SAME adapter, with its trained weights.

  • Measured on a fresh server: load shadow-05 as A, take one step, load shadow-05 again. It returns id 0 again, and its next loss is 3.5043, A's second step.
  • Two minds learn independently only from distinct files. The core gives each mind her own shadow file, a copy of her stable gene, before her first learn.
  • The two-minds test uses a byte copy for this reason.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FRQWzgSfo79JtnwZywHKrE

… its own optimizer, /learn trains any loaded shadow, and unload releases it

A serving context trained exactly one shadow per server run (learn_lora_id):
the second mind's /learn was refused, so only one resident could ever
learn in place (continuum S4 (a), card eaeb2b87).

- llama_context keeps an optimizer per shadow (opt_minds); opt_ctx and
  opt_shadow point at the active one, so every training site reads the
  active pair as before. llama_opt_init with another adapter on a
  configured context selects it (opt_select), creating its optimizer the
  first time and keeping each shadow's moments and accumulators across
  switches. Only the active shadow's tensors carry the PARAM flag, so a
  graph never carries a parameter the active optimizer was not built
  with.
- opt_configured() is "this context is sized for training" (scheduler and
  graph): it was read as opt_ctx != nullptr, which a released active
  shadow would have falsified while other shadows' optimizers still held
  the scheduler.
- llama_opt_release forgets one shadow's optimizer; the server's unload
  releases a shadow that learned (its moments go; a snapshot is the
  durable form) instead of refusing it forever.
- learn-every-format.py --two-minds: two shadows from the same file learn
  alternately; their losses match step for step (4.3179, 4.3179, 3.5043,
  3.5043 on Qwen2.5-0.5B, IntelMac CPU), which holds only if each keeps
  its own weights and optimizer. --shadow-retirement now unloads a bound
  shadow and learns on a reloaded one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRQWzgSfo79JtnwZywHKrE
@joelteply
joelteply changed the base branch from master to 889580fc/seq-state-across-stream-counts November 1, 2026 07:14
@github-actions github-actions Bot added the server label Nov 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant