Skip to content

Feat/model run profile - #489

Merged
toasterbook88 merged 11 commits into
mainfrom
feat/model-run-profile
Oct 5, 2026
Merged

toasterbook88 merged 11 commits into
mainfrom
feat/model-run-profile

Conversation

@toasterbook88

Copy link
Copy Markdown
Owner

No description provided.

AXIS Contributor added 4 commits October 3, 2026 15:36
Plan and start now use one axis.model-run/v1 profile. With no new flags the
argv stays llama-server, -m, the weights path, --port, the port, and --host
127.0.0.1. A plan-default port refuses until --port is passed. --n-gpu-layers
is a string and requires measured VRAM on one discrete device.
The three nvidia-smi memory queries now start with index. A four-column row
records Index and IndexSource nvidia-smi only when every numeric cell parses.
Legacy three-column and two-column rows, Metal, and lspci leave Index nil.
--main-gpu N is emitted only when that N matches an observed nvidia-smi index.
The receipt quotes the split-mode none and row help. An omitted pin does not
become 0.
…ening

--ollama-model replaces --weights and is mutually exclusive with it. Load
and unload curl 127.0.0.1:11434 /api/generate, then GET /api/ps. Axis does
not exec ollama serve, does not send num_gpu or main_gpu, and does not kill
comm=ollama. A generation whose engine is ollama unloads on that path and
never reaches the llama-server process kill.
axis model start builds a profile and calls PlanStartProfile. PlanStart had
no production caller, so the deadcode gate rejected the branch. Tests build
that same default profile themselves.
Copilot AI balanced review requested due to automatic review settings October 3, 2026 20:50

@toasterbook88 toasterbook88 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AXIS Grok gate

Verdict: PASS-WITH-RISK
PR: #489 Feat/model run profile
Head: d169143
Base: main @ cb202b8
URL: #489

Files (20): cmd/axis/model.go, model_ollama_test.go, model_run_profile.go, model_run_profile_test.go; internal/facts/local_gpu.go, remote.go, remote_bundle.go, gpu_index_test.go, gpu_query_test.go; internal/modellife/plan.go, plan_test.go, plan_profile_test.go, ollama.go, ollama_test.go; internal/modelplan/single_node.go, profile_test.go; internal/models/run_profile.go, run_profile_test.go, model_operation.go, types.go.

Invariant check:

  • Advisory does not override the fact plane. axis.model-plan/v1 Selected is labeled advisory. Start rebuilds device fields from the snapshot, refuses a plan-default port, refuses n-gpu-layers without measured free VRAM on one discrete device, and emits --main-gpu only when that index is an observed nvidia-smi index. Omitted pin stays nil, not 0. Metal/lspci/legacy three-column rows leave Index nil.
  • Cache stays explicit. Existing loadModelCommandSnapshot path; --live unchanged; receipts carry snapshot source and publication id. Load fact for Ollama is GET /api/ps after the curl, not OllamaInfo.Listening.
  • HITL / dispatch lock not touched. No secret_read, ssh_bypass, fleet_exec, spawn_subagent, or self-authorize. Remote curl uses the existing node SSH seam, loopback 127.0.0.1:11434 only, shell-single-quoted JSON. Does not exec ollama serve, does not send num_gpu/main_gpu, does not kill comm=ollama.
  • Agent self-model untouched.

Residuals:

  • CI not green yet. Test & Build, govulncheck, Analyze (actions), and copilot-pull-request-reviewer are in progress. auto-merge skipped. Combined status success is Devin Review skip (trial expired), not the test suite.
  • Ollama place and --ollama-model stop do not require StatusComplete or OllamaInfo.Listening before the SSH curl. Fail-closed on curl or missing /api/ps listing, but an incomplete node can still be targeted if resolve returns it. POST /api/generate can run tokens; it is not a pure load API.
  • Receipt VRAMFreeMeasured comes from ObserveLaunchDevice (best discrete / unified), not from the pinned nvidia-smi index. A --main-gpu pin can name a different device than the VRAM figure on the receipt.
  • --write-profile is 0644 on an operator path. Plan text does not print Selected.Refusals; JSON does. Start still refuses those profiles.

Operator action: do not merge on this comment. Wait for Test & Build / govulncheck / Analyze on d169143. No APPROVE from this gate.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

GPU fact framing, pinned-device validation, Ollama verification, receipt handling, and profile contracts contain correctness issues.

Review effort: Balanced
Findings: 7 Medium severity · 2 Low severity

Open (9)
What changed in this PR

Adds truth-backed model run profiles connecting placement plans to model lifecycle execution, including GPU pinning and Ollama placement.

Changes:

  • Adds validated run profiles and llama-server flag projection.
  • Collects NVIDIA GPU indices for explicit pinning.
  • Adds Ollama load/unload lifecycle support and tests.
File Description
internal/​models/​types.go Adds GPU index facts.
internal/​models/​run_profile.go Defines run profiles and validation.
internal/​models/​run_profile_test.go Tests profile behavior.
internal/​models/​model_operation.go Extends operation receipts.
internal/​modelplan/​single_node.go Adds selected launch profiles.
internal/​modelplan/​profile_test.go Tests planned profiles.
internal/​modellife/​plan.go Projects profiles into llama-server argv.
internal/​modellife/​plan_test.go Adapts start-plan tests.
internal/​modellife/​plan_profile_test.go Tests profile execution safeguards.
internal/​modellife/​ollama.go Implements Ollama load/unload scripts.
internal/​modellife/​ollama_test.go Tests Ollama scripts.
internal/​facts/​remote.go Reuses GPU collection commands.
internal/​facts/​remote_bundle.go Collects remote GPU indices.
internal/​facts/​local_gpu.go Parses indexed NVIDIA facts.
internal/​facts/​gpu_query_test.go Tests GPU query consistency.
internal/​facts/​gpu_index_test.go Tests GPU index parsing.
cmd/​axis/​model.go Integrates profiles and Ollama lifecycle.
cmd/​axis/​model_run_profile.go Handles profile CLI input/output.
cmd/​axis/​model_run_profile_test.go Tests profile CLI flows.
cmd/​axis/​model_ollama_test.go Tests Ollama CLI lifecycle.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread cmd/axis/model.go
Comment thread cmd/axis/model.go
Comment thread internal/facts/remote_bundle.go Outdated
Comment thread internal/modellife/ollama.go
Comment thread internal/modellife/plan.go
Comment thread internal/models/run_profile.go Outdated
Comment thread internal/models/types.go
Comment thread cmd/axis/model.go Outdated
Comment thread cmd/axis/model.go
AXIS Contributor added 4 commits October 3, 2026 17:43
mlx_lm.server is the observed launch tool. The stop guard matches that
basename or the argv sequence -m then mlx_lm.server. It does not kill
comm=python or comm=mlx_lm. Port-only stops stay on the llama-server check.
A device hold stores one GPU index and that device's MiB. Index 0
round-trips. Load drops an expired hold without adding it to the VRAM
sums, and entry writes keep the hold slice.
… time

After the llama-server probe succeeds, sample that node's listener RSS
and store one ExecutionObservation. A failed sample warns and leaves the
process up. Model plan drops a candidate whose fresh peak exceeds
allocatable RAM.
Encode every remote nvidia-smi GPU row before base64.
Match an untagged Ollama name only to the :latest tag.
Refuse an Ollama profile with refusals before the load POST.
Keep ModelRunProfile YAML keys in snake_case.
Print the placed Ollama model on a successful text receipt.
Refresh the daemon cache after a successful Ollama unload.
Deep-copy GPUInfo.Index when cloning a snapshot.
Gate n-gpu-layers on the pinned GPU's measured free VRAM.
Name Ollama and MLX in the model help and two stale sentences.
Wait briefly for SIGKILL before the MLX stop script asserts death.
@toasterbook88

Copy link
Copy Markdown
Owner Author

Hold. axis model start grows a mode board (--from-plan, --n-gpu-layers, --ctx-size, --ollama-model, --mlx-model, and the rest). The daily path should stay one line: pick the node that already has the runtime and say so. This does not do that. Copilot's nine threads look addressed in fec781b. Not merging from this comment.

AXIS Contributor added 3 commits October 4, 2026 17:44
axis model start <model> reads the snapshot and names one complete
node whose runtime already has that model. A resident model is
reported and left running. An Ollama server that is listening and
lists the model is loaded on loopback, and that load is accepted
only when the server returns done_reason load. Ollama SSH is
refused unless the node is complete and the server is listening.
@toasterbook88
toasterbook88 merged commit 7d7bd2b into main Oct 5, 2026
6 checks passed
@toasterbook88
toasterbook88 deleted the feat/model-run-profile branch October 5, 2026 01:08
@toasterbook88

toasterbook88 commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner Author

Hardware Validation red on main since this PR: new fixture helpers hard-code PATH and drop the Nix-profile awk

What fails — the Hardware Validation runs on the pushes from this PR's merge and the next one (runs 37250216353, 37250828528; jobs 111576154161, 111577904135). The scheduled hardware run one day earlier (37187361294) was green because these fixture files did not exist yet.

Exactly three tests fail, and only these:

  • TestShellStopMLXGuardMatchesServerNotPythonOrConsole (cmd/axis)
  • TestLlamaServerSampleScriptMaxRSSAndDeviceRow (cmd/axis)
  • TestRemoteBundleKeepsEveryNvidiaSMIRow (internal/facts)

with sh: line 1: awk: command not found inside the spawned fixture process (model_mlx_test.go:262, model_observation_test.go:384).

Root cause (introduced by this PR's merge 7d7bd2b). Three new fixture helpers run their stubbed scripts with a hard-coded environment:

  • cmd/axis/model_mlx_test.go:307
    cmd.Env = []string{"PATH=" + dir + ":/usr/bin:/bin"}
  • cmd/axis/model_observation_test.go:497
    cmd.Env = []string{"PATH=" + dir + ":/usr/bin:/bin", "HOME=" + dir}
  • internal/facts/remote_bundle_gpu_test.go:27
    cmd.Env = append(os.Environ(), "PATH="+dir+":/usr/bin:/bin")

Two problems with that on the self-hosted NixOS runner:

  1. There is no awk at /usr/bin or /bin on that host. The workflow's Install runner dependencies step already documents this ("NixOS keeps awk/sed/stat in this profile, not /usr/bin") and works around it by prepending /run/current-system/sw/bin to PATH — but a fixture that replaces PATH throws that lookup away, and the scripts under test call real awk (nvidia-smi parsing, ps supervision checks, RSS/device-row sampling).
  2. For the bundle helper, POSIX exec semantics make the appended PATH= (after os.Environ()) the winning value, so even the inherited profile path is masked, not just missing.

The fixtures pass on conventional Ubuntu runners and macOS dev hosts (where /usr/bin/awk exists), so this is an environment-dependent test-portability bug rather than product-code rot.

Proposed fix — three small edits, using the repo's existing idiom. Keep stub-dir precedence first, then append the inherited PATH (separator built once, the way internal/facts/resident_models_test.go:352-356 already does):

// cmd/axis/model_mlx_test.go (runMLXStopScript) — test helpers can't cross packages,
// so mirror the withSandboxedPATH idiom inline:
sep := string(os.PathListSeparator)
cmd.Env = []string{"PATH=" + dir + sep + os.Getenv("PATH")}

// cmd/axis/model_observation_test.go (runSampleShell)
sep := string(os.PathListSeparator)
cmd.Env = []string{"PATH=" + dir + sep + os.Getenv("PATH"), "HOME=" + dir}

// internal/facts/remote_bundle_gpu_test.go — same package as the existing helper,
// so just reuse it instead of hand-rolling:
cmd.Env = withSandboxedPATH(dir)

Rationale: the stub dir stays first, so fixture stubs keep shadowing real binaries and the assertions' isolation semantics are unchanged; the inherited tail supplies host-level tools (awk, and base64/tr for the bundle path) on hosts where they live only in the profile. withSandboxedPATH (added with the resident-model tests) already carries the Nix-profile + /usr/bin + /bin fallbacks, so the internal/facts side needs no new code at all — the two cmd/axis fixtures get the minimal sep idiom inline. No workflow change is needed — the Install runner dependencies step already puts the Nix profile on PATH, and hack/hermetic-go-test.sh leaves PATH alone.

Validation plan for a small fix PR (three files × a few lines):

  1. Local: go test ./cmd/axis -run 'TestShellStopMLX|TestLlamaServerSample' and go test ./internal/facts -run 'TestRemoteBundleKeeps' stay green on a conventional PATH.
  2. Re-run Hardware Validation on the fix branch: the two currently-red packages (cmd/axis, internal/facts) flip green on the NixOS runner.
  3. Watch the same three tests by name — no other test should change behavior (stubs still shadow).

Scope note. hardware-validation is currently not a merge gate: main's branch protection requires only Test & Build + govulncheck (both green at tip), and release.yml gates live in tag-time jobs on hosted runners, so this does not block the next tag mechanically — but it keeps hardware-validation red on every main push until fixed, which is noise the next release-prep PR shouldn't inherit.

Happy to open the small fix PR if that's the wanted next step.

toasterbook88 added a commit that referenced this pull request Oct 6, 2026
## Keep fixture stubs ahead of the host PATH (Hardware Validation is red
on main)

Three test fixtures added in #489 replace their child-process `PATH`
with
`stubdir:/usr/bin:/bin`. The scripts under test call real tools (`awk`
for
the nvidia-smi bundle parse and the classifier/observation checks), and
on
profile-based Linux runners those tools live only in the system profile
—
not under `/usr/bin`/`/bin`. Result: the last `Hardware Validation`
pushes
are red with `sh: line 1: awk: command not found` while the same tests
pass on conventional hosts and locally (run/job refs: 37250216353,
37250828528, jobs 111576154161, 111577904135). Full root-cause comment
is
on the source PR (#489).

### Change

- `cmd/axis/model_mlx_test.go` and `cmd/axis/model_observation_test.go`
keep
the stub directory **first** and append the inherited PATH after it,
built
with a single `sep := string(os.PathListSeparator)`; `HOME=` handling is
  unchanged. Fixture stubs keep shadowing real binaries.
- `internal/facts/remote_bundle_gpu_test.go` replaces the manual
  `append(os.Environ(), "PATH=…")` with the package's existing
  `withSandboxedPATH(dir)` helper, which already orders stubs first and
  appends profile fallbacks. No new PATH implementation is introduced.

### Validation run

- `go vet ./cmd/axis ./internal/facts` clean.
- Focused suites green:
  `go test ./cmd/axis -run 'TestShellStopMLX|TestLlamaServerSample'` and
`go test ./internal/facts -run
'TestRemoteBundleKeeps|TestResidentModels'`.
- A batched `TestModelEvict*` run
(`TestModelEvictDefaultsToLiveSnapshot`,
  `TestModelEvictLiveFalseUsesDaemonCache`, `TestModelEvictByGPUIndex`,
  `TestModelResumeByReceiptID`,
  `TestModelResumeUnitNameUsesSnapshotSupervisorAndPort`) first hit
  event-flush 5-second timeouts during a heavy-load window (workstation
  load average 116–160). A later batch re-run of the same five tests
  passed on this tree, and the same batched set passed on a pristine
  main-tip tree. No controlled load A/B was performed, so the timeouts
  are load-correlated with causality unproven; no test code was changed.
- No workflow file is touched: the workflow's `Install runner
dependencies`
step already places the profile directory on PATH, and the hermetic-test
  harness leaves PATH to the caller.
- This branch needs one `Hardware Validation` run on itself to confirm
the
self-hosted lane goes green with the fixture fix in place; local focused
  runs alone do not satisfy that.

### Notes

- Test-only change; no product code touched (3 files, +14/−3).
- Merging this before the next release tag keeps the hardware lane
quiet.

---------

Co-authored-by: cranium-agent <agent@local>
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.

2 participants