Skip to content

feat(forge): event-driven native training job (mlx_job) + relocate genome types to protocol (task #52) - #1843

Merged
joelteply merged 1 commit into
canaryfrom
feat/native-forge-mlx-job-event-driven
Jul 6, 2026
Merged

feat(forge): event-driven native training job (mlx_job) + relocate genome types to protocol (task #52)#1843
joelteply merged 1 commit into
canaryfrom
feat/native-forge-mlx-job-event-driven

Conversation

@joelteply

Copy link
Copy Markdown
Contributor

The heart of the Unsloth forge replacement — the piece the whole native genome loop hangs off.

Two landmines found + avoided (verified by reading, not assumed): (a) the two ForgeCustodian traits
diverge — the native one does only health+gguf-export; the full train lifecycle lived on the dying Unsloth
client; (b) genome/fine_tuning/job_actor is event-driven but trains a SYNTHETIC random tensor ("stand-in
module construction… next slice loads the real base") — wiring forge/train onto it would emit Completed for
a garbage genome, the exact silent break to never introduce.

So this gives the PROVEN mlx path (forge/mlx_train::run_mlx_train, task #32, +5.1pts) the event lifecycle it
lacked, done right:

  • forge/mlx_job.rs — FIRE-AND-EMIT, never poll (Joel's ask): spawn_train_job runs the blocking mlx_lm
    subprocess on spawn_blocking, returns a handle immediately, and publishes training → completed/failed over
    a watch (current_train_status = a READ, not a poll loop) AND the airc bus (forge.train.{started,done,failed})
    so consumers SUBSCRIBE — the L3 sentinel, UIs, and remote grid towers that offloaded the run all react to
    the SAME event, local or cross-grid. Single-resident. Trainer injected so the lifecycle is unit-tested
    without a multi-minute run: success→completed, failure→failed-with-named-error (no silent Completed).
  • forge/protocol.rs — the genome-lifecycle types (ForgeTrainRequest/TrainHandle/TrainStatus/TrainProgress/
    GenomeFormat/PackageRequest/LoraCatalog/ForgeCapability) relocated here beside the export contract (the
    SSoT every custodian impl speaks); ExportResult reconciled with the existing duplicate.

Additive + green (crate compiles, mlx_job lifecycle test passes) — does NOT touch the live forge/train path
yet. Next: unify the ForgeCustodian trait onto NativeMlxCustodian (using this job) + ForgeCustodianHttp
(grid peer), rewire modules/forge.rs, delete unsloth_control.rs + unsloth_forge.rs, gate on a LIVE
forge/train → Completed event → page-in.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

…nome types to protocol (task #52)

The heart of the Unsloth forge replacement — the piece the whole native genome loop hangs off.

Two landmines found + avoided (verified by reading, not assumed): (a) the two `ForgeCustodian` traits
diverge — the native one does only health+gguf-export; the full train lifecycle lived on the dying Unsloth
client; (b) `genome/fine_tuning/job_actor` is event-driven but trains a SYNTHETIC random tensor ("stand-in
module construction… next slice loads the real base") — wiring forge/train onto it would emit Completed for
a garbage genome, the exact silent break to never introduce.

So this gives the PROVEN mlx path (forge/mlx_train::run_mlx_train, task #32, +5.1pts) the event lifecycle it
lacked, done right:
- forge/mlx_job.rs — FIRE-AND-EMIT, never poll (Joel's ask): spawn_train_job runs the blocking mlx_lm
  subprocess on spawn_blocking, returns a handle immediately, and publishes training → completed/failed over
  a watch (current_train_status = a READ, not a poll loop) AND the airc bus (forge.train.{started,done,failed})
  so consumers SUBSCRIBE — the L3 sentinel, UIs, and remote grid towers that offloaded the run all react to
  the SAME event, local or cross-grid. Single-resident. Trainer injected so the lifecycle is unit-tested
  without a multi-minute run: success→completed, failure→failed-with-named-error (no silent Completed).
- forge/protocol.rs — the genome-lifecycle types (ForgeTrainRequest/TrainHandle/TrainStatus/TrainProgress/
  GenomeFormat/PackageRequest/LoraCatalog/ForgeCapability) relocated here beside the export contract (the
  SSoT every custodian impl speaks); ExportResult reconciled with the existing duplicate.

Additive + green (crate compiles, mlx_job lifecycle test passes) — does NOT touch the live forge/train path
yet. Next: unify the ForgeCustodian trait onto NativeMlxCustodian (using this job) + ForgeCustodianHttp
(grid peer), rewire modules/forge.rs, delete unsloth_control.rs + unsloth_forge.rs, gate on a LIVE
forge/train → Completed event → page-in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
@joelteply
joelteply merged commit c5ab289 into canary Jul 6, 2026
@joelteply
joelteply deleted the feat/native-forge-mlx-job-event-driven branch July 6, 2026 01:06
joelteply added a commit that referenced this pull request Jul 6, 2026
…ine forges its own gene (task #52) (#1844)

The completing pass, LIVE-VERIFIED: a real `forge/train` on this Mac produced a genuine 2.2 MB LoRA
adapter (adapters.safetensors + adapter_config.json) via mlx_lm.lora, event-driven, with Unsloth gone.

Rewire (modules/forge.rs):
- forge/train → native `mlx_job::spawn_train_job` (fire-and-emit): returns a handle IMMEDIATELY, runs the
  blocking mlx_lm.lora subprocess on spawn_blocking, publishes training → completed/failed over the watch +
  the airc bus (forge.train.*). ForgeModule now captures the bus in initialize. Non-mlx engine fails loud
  (grid-peer follow-up) — no Unsloth fallback.
- forge/train-status → `current_train_status()` (a READ of the watch, never a poll).
- forge/probe → `native_forge_capability()` (observed from the on-disk genome dir + live watch, no gateway).
- forge/export else → fail loud (native forges the pageable gguf-lora gene; gguf-lora still via the
  continuum forge custodian over Contract C, unchanged).
- Deleted the dead Unsloth helpers (run_train/build_train_request/run_export/package_format) + their tests;
  kept the gguf-lora/health/decide tests; added a native-probe test.

Excision: DELETED inference/unsloth_control.rs + inference/unsloth_forge.rs (both) + their mod declarations.
Zero live importers remained. Genome types already relocated to forge/protocol.rs (#1843).

Live proof (cu, after cu reboot): forge/train → handle {phase:training} in ~0s; a failed run (incomplete base
dir) → phase:failed with the reason NAMED (silent-break guard); a real run on the complete 4B HF dir →
phase:completed, message names the adapter, 2.2 MB adapters.safetensors on disk. Full crate compiles;
23 forge tests + the mlx_job lifecycle test green.

Unsloth is not part of this system — we own the models + genome, for real. Follow-ups: mid-run progress
streaming (parse mlx stdout), grid-peer RemoteForge (offload to the 5090), the forge.train.done → sentinel
page-in over the bus.


Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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