feat(forge): event-driven native training job (mlx_job) + relocate genome types to protocol (task #52) - #1843
Merged
Conversation
…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
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>
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.
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
ForgeCustodiantraitsdiverge — the native one does only health+gguf-export; the full train lifecycle lived on the dying Unsloth
client; (b)
genome/fine_tuning/job_actoris event-driven but trains a SYNTHETIC random tensor ("stand-inmodule 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:
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).
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