Skip to content

fix(updater): migrate gpu-vulkan slot TOMLs to gpu-rocm/cpu - #1934

Open
thinmintdev wants to merge 5 commits into
mainfrom
fix/update-migrate-gpu-vulkan-slots
Open

fix(updater): migrate gpu-vulkan slot TOMLs to gpu-rocm/cpu#1934
thinmintdev wants to merge 5 commits into
mainfrom
fix/update-migrate-gpu-vulkan-slots

Conversation

@thinmintdev

@thinmintdev thinmintdev commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary

PR #1923 retired the Vulkan LLM lane (#1888) and made container.py's load_sync refuse to load a GPU slot whose device is gpu-vulkan or gpu-rocm when /dev/kfd is not visible. It deliberately relabeled only the installer's seed slot TOMLs (installer/etc-hal0/slots/*.toml) from gpu-vulkan to gpu-rocm, and explicitly left slot TOMLs already materialised on an existing install untouched — so any pre-#1923 install with device = "gpu-vulkan" slots refuses to load them after updating, until an operator manually relabels the TOML (raised as open question 3 in #1923's report, filed as #1924).

This PR adds an updater migration that does that relabel automatically.

Design

relabel_stale_vulkan_slots is a new sixth pass in run_post_activation_migrations (src/hal0/updater/updater.py) — the single sequence both Updater.commit() (self-update) and install.sh's repair/upgrade-in-place path call, so both upgrade routes converge on the same on-disk state.

  • Trigger for gpu-rocm: /dev/kfd present and usable (hal0.providers._gpu.kfd_present(), added in fix(gpu): require /dev/kfd and retire the Vulkan LLM lane #1923) — matches PR fix(gpu): require /dev/kfd and retire the Vulkan LLM lane #1923's own seed relabel exactly.
  • Trigger for cpu: /dev/kfd not present. This is a genuine operator-visible behavior change (the slot drops off the GPU and gets much slower), so it is logged as its own distinct WARNING-level breadcrumb (updater.slot_vulkan_relabeled_cpu_fallback, note prefixed BEHAVIOR CHANGE:) — never folded silently into the routine relabel log line (updater.slot_vulkan_relabeled_rocm).
  • AMD-host scope: only fires when the amdgpu kernel driver is bound on this host (hal0.providers._gpu.host_is_amd_gpu()), exactly matching require_kfd_for_gpu_slot's own scope. A non-AMD host (Intel iGPU, NVIDIA without CDI) has no /dev/kfd by design, was never gated by fix(gpu): require /dev/kfd and retire the Vulkan LLM lane #1923's guard, and is a full no-op here — no relabel, no log.
  • Runtime scope (llama.cpp-backed slots only): device = "gpu-vulkan" is not exclusively an llama.cpp label — capabilities/catalog.py deliberately keeps it for the non-llama runtimes (Kokoro TTS, whisper.cpp/Moonshine STT, ComfyUI), which run genuinely-Vulkan images. Every candidate slot is resolved through hal0.providers.container._spec_provider_for — the same discriminator load_sync itself uses — and is skipped ENTIRELY (no relabel, no warning, no log, on either kfd axis) unless it resolves to None (the default llama-server GPU provider). The kfd-absent non-llama case (require_kfd_for_gpu_slot itself over-firing for non-llama runtimes) is a separate, already-filed bug tracked as gpu: #1923's /dev/kfd guard over-fires on non-llama Vulkan runtimes — kokoro/whisper.cpp/ComfyUI slots refused on kfd-less AMD boxes #1941 and is deliberately NOT addressed here.
  • Narrow scope (hermes: an upgraded box keeps an old slot ceiling below Hermes' floor, and nothing preflights the anchor window #1867 rails): raw tomllib load / write_toml_atomic dump, same convention as the existing retag_stale_slot_images / clear_stale_mtp_overrides passes in this module. Only the device key's value is ever changed (top-level or nested under [slot], matching the two TOML shapes those passes already handle); every other field, and every slot whose device is not literally "gpu-vulkan" or that isn't llama.cpp-backed, is left untouched. Note this is narrow-scope-by-key, not byte-for-byte file preservation — like every other pass in this module, a touched file's comments are dropped on rewrite (write_toml_atomic reserializes the whole parsed document via tomli_w).
  • Idempotency: once a slot's device reads "gpu-rocm" or "cpu" it no longer matches the "gpu-vulkan" guard, so a second run touches nothing and emits no new log lines (covered by test_relabel_is_idempotent, test_relabel_is_idempotent_on_cpu_fallback, test_idempotent_second_run_logs_nothing_new).
  • Journal breadcrumb: every mutation logs slot, old ("gpu-vulkan"), new ("gpu-rocm" or "cpu"), and job_id, via structlog log.warning — same shape as the other post-activation passes.
  • Non-fatal in the aggregate sequence, like the other four data-cleanup passes: a failure here logs and does not block the other migrations or the caller's activation.

This should ship in the same release as #1923 — otherwise any pre-#1923 install with gpu-vulkan LLM slots hits the new load-time refusal with no automatic recovery path.

Test plan

  • tests/updater/test_vulkan_slot_migration.py (new file, 22 tests) — TDD throughout, including the runtime-scope fix (RED first against the pre-fix code, confirmed the non-llama tests failed, then implementation). Covers: kfd-present → gpu-rocm relabel (flat + nested [slot] shape), kfd-absent → cpu relabel with a loud distinct breadcrumb, AMD-host scoping (non-AMD host is a full no-op, real-probe default), runtime scoping (Kokoro/ComfyUI/transcription-shaped slots survive completely untouched on both kfd axes, alongside a genuine llama.cpp slot that still relabels), narrow scope (other keys/slots untouched), unreadable-TOML is skipped not fatal, idempotent re-run (both branches, including "logs nothing new"), and the default (no override) probes the real kfd_present()/host_is_amd_gpu().
  • tests/updater/test_post_activation_migrations.py (4 existing tests edited, stub signature updated for amd_host) to stub and assert the new sixth pass runs in order, alongside the others, and is skipped when the schema migration fails first.
  • installer/install.sh's post-activation-migrations transcript/log updated to name the vulkan-slot pass alongside the other four.
  • env -u FORCE_COLOR HAL0_HOME=$(mktemp -d) uv run --extra dev pytest tests/updater tests/install -q → 538 passed
  • uv run ruff format --check src tests → all formatted
  • make lint → all checks passed
  • python scripts/check_sunset.py → scar markers at baseline

Refs #1924 #1888. Related: #1941 (tracks require_kfd_for_gpu_slot itself over-firing for non-llama Vulkan runtimes on a kfd-absent AMD host — out of scope for this migration).

PR #1923 retired the Vulkan LLM lane and made load_sync refuse a GPU
slot without /dev/kfd, but deliberately left existing on-disk slot
TOMLs unmigrated — any install with device="gpu-vulkan" slots refuses
to load them after updating (#1924).

Add relabel_stale_vulkan_slots as a sixth post-activation migration
pass, following the same raw-TOML-surgery convention as
retag_stale_slot_images/clear_stale_mtp_overrides: relabel device from
gpu-vulkan to gpu-rocm when /dev/kfd is present, or to cpu (with a
loud WARNING breadcrumb, since this is a real behavior change) when it
is not. Only the device key is touched, and the pass is idempotent
since a relabeled slot no longer matches the gpu-vulkan guard.

Refs #1924 #1888
The migration's docstring used "legacy", which scripts/check_sunset.py's
scar-marker regex flags — reword to "retired" (no behavior change).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5169a161a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

holder = raw["slot"]
if holder is None:
continue
new_device = "gpu-rocm" if have_kfd else "cpu"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restrict relabeling to affected AMD llama.cpp slots

This host-wide choice rewrites every gpu-vulkan slot based solely on /dev/kfd, although Vulkan remains valid for other configurations. Intel/NVIDIA hosts have no KFD by design, so working Vulkan LLM slots are permanently downgraded to CPU; additionally, Vulkan-only provider slots such as Whisper.cpp, Kokoro, VibeVoice, and ComfyUI are changed to either CPU or unsupported ROCm depending on the probe. Filter by the affected AMD llama.cpp runtime/host instead of migrating all Vulkan slots.

Useful? React with 👍 / 👎.

# operator flips MTP to Auto/Off in the drawer.

try:
relabel_stale_vulkan_slots(job_id=job_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Run the new migration from the activated release

In the checked Updater.commit() self-update flow, run_post_activation_migrations() executes before seam.activate() installs the target release into the shared venv. Consequently, an upgrade into the release containing this new call runs the old process's version of this function, which has no Vulkan migration; after activation, the new KFD guard is present but the legacy slot TOMLs remain unchanged. Invoke this newly shipped migration through the staged/activated code so it runs on the update that introduces it.

Useful? React with 👍 / 👎.

from hal0.config.paths import slots_config_dir
from hal0.providers._gpu import kfd_present as _probe_kfd_present

have_kfd = _probe_kfd_present() if kfd_present is None else kfd_present

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Probe KFD access as the service account

On the checked install.sh repair/upgrade path, this probe runs as root, while kfd_present() explicitly uses os.access() to determine whether the current process can read and write /dev/kfd; the service later loads slots as User=hal0. If KFD exists but its group mapping or permissions do not admit hal0—the LXC failure mode the probe is meant to catch—root can select gpu-rocm, then the restarted service fails the same KFD preflight and cannot load the slot. Run the usability probe with the service user's credentials rather than the installer's.

Useful? React with 👍 / 👎.

Codex review on #1934 (P1): the migration was relabeling ANY device =
"gpu-vulkan" slot based solely on kfd_present(), but require_kfd_for_gpu_slot
(added in #1923) only ever gates a gpu-vulkan slot on an AMD host
(host_is_amd_gpu()). On Intel/NVIDIA hardware /dev/kfd never exists by
design, so a working non-AMD Vulkan slot was never broken by #1923 — and
the unscoped migration would have silently downgraded it to cpu on its
next update, since kfd_present() is also (correctly) False there.

Add the same amd_host gate require_kfd_for_gpu_slot uses: on a non-AMD
host the migration is now a full no-op (no writes, no log lines).

Refs #1924 #1888
@thinmintdev

Copy link
Copy Markdown
Contributor Author

Triaged the Codex review comments:

  1. P1 "Restrict relabeling to affected AMD llama.cpp slots" — valid, fixed in b864084. The migration was relabeling any device = "gpu-vulkan" slot based solely on kfd_present(), but require_kfd_for_gpu_slot (added in fix(gpu): require /dev/kfd and retire the Vulkan LLM lane #1923) only ever gates a gpu-vulkan slot on an AMD host (host_is_amd_gpu()). On Intel/NVIDIA hardware /dev/kfd never exists by design, so a working non-AMD Vulkan slot was never broken by fix(gpu): require /dev/kfd and retire the Vulkan LLM lane #1923, and the unscoped migration would have silently downgraded it to cpu. Added the same amd_host gate require_kfd_for_gpu_slot uses — on a non-AMD host the migration is now a full no-op. New tests cover this (test_non_amd_host_is_a_full_noop, test_non_amd_host_logs_nothing, test_default_probes_real_amd_host).

    On the narrower point about Kokoro/whisper.cpp/ComfyUI slots specifically: those providers don't branch their container spec on the gpu-rocm vs gpu-vulkan string (they resolve GPU device paths generically via resolve_gpu_device_paths()/resolve_gpu_group_ids()), and require_kfd_for_gpu_slot itself gates any device="gpu-vulkan" slot on an AMD host regardless of provider type — so on an AMD box without /dev/kfd, those slots are equally blocked by fix(gpu): require /dev/kfd and retire the Vulkan LLM lane #1923's own guard and equally need the relabel. This mirrors PR fix(gpu): require /dev/kfd and retire the Vulkan LLM lane #1923's own precedent, which relabeled the rerank/embed seed slots (non-LLM types) the same way.

  2. P1 "Run the new migration from the activated release" — accurate, but this is a pre-existing architectural property of run_post_activation_migrations shared by all four existing passes (ensure_seed_profiles, clear_stale_mtp_overrides, retag_stale_slot_images, sanitize_model_extra_args), not something introduced by this PR: Updater.commit()'s step 7 (migrations) runs before step 8 (seam.activate(), the symlink swap + venv re-pip), using the already-imported old process's code. So on the specific hal0 update self-update transaction that installs the release containing this fix, it runs one cycle late — the same known limitation every prior post-activation migration has always had. install.sh's repair/upgrade-in-place path is unaffected (it always runs a fresh python -c invocation against the already-pip-installed target code). Restructuring Updater.commit()'s activation ordering to close this gap for all five passes at once is a materially larger, separately-scoped change (the code has an explicit comment on why migrations currently run before the profile-catalog-reset gate check) — out of scope for this narrowly-scoped update: migrate existing device="gpu-vulkan" slot TOMLs to gpu-rocm — post-#1923 they refuse to load #1924 fix. Noting it here rather than silently dropping it; happy to file a tracking issue if the team wants it addressed as its own PR.

  3. P2 "Probe KFD access as the service account" — real edge case, skipping for this PR. The migration's kfd_present() probe runs as whatever user executes the migration (root during install.sh, or the hal0 update caller's process). The actual enforcement (require_kfd_for_gpu_slot in container.py's load_sync) runs later, in the real hal0-api service process, as the real User=hal0 account, and re-probes /dev/kfd access independently at slot-load time. So a root/service-account permission mismatch (the exact LXC gid-mapping failure mode fix(gpu): require /dev/kfd and retire the Vulkan LLM lane #1923's own preflight gid-check exists to catch) can't produce a silently-broken slot: if the migration picks gpu-rocm optimistically and the service account genuinely can't reach /dev/kfd, the slot still fails to load with the same clear GpuPreflightError message it would have hit as gpu-vulkan — just one hop later, and diagnosable by the same remedy (fix the render/kfd gid mapping). Probing as the service account from inside an install-time/root migration would need a privilege-drop or subprocess-as-user, which is a meaningfully bigger and more invasive change than this narrowly-scoped migration warrants.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b86408439f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2954 to +2957
if raw.get("device") == "gpu-vulkan":
holder = raw
elif isinstance(raw.get("slot"), dict) and raw["slot"].get("device") == "gpu-vulkan":
holder = raw["slot"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Read the authoritative [slot] device first

When a legacy nested TOML also contains a duplicated top-level device = "gpu-vulkan"—a drift shape explicitly handled by slot_scalar_table()—this branch selects the root dictionary. If both copies are Vulkan, only the ignored root value is relabeled; _flatten_slot_toml() treats [slot] as authoritative, so the next load still sees gpu-vulkan and the slot remains broken despite the migration logging success. Select the holder through slot_scalar_table(raw), which gives any [slot] table precedence.

Useful? React with 👍 / 👎.

Comment on lines +2961 to +2963
holder["device"] = new_device
try:
write_toml_atomic(toml_path, raw)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Lock the slot read-modify-write migration

When an update runs in the background while an operator or API request edits an affected slot, this migration reads and replaces the TOML without the coarse slot_write_lock() used by the normal slot writers. A PATCH that lands after this read but before write_toml_atomic() is overwritten by the stale raw dictionary, silently losing the concurrent configuration change. Hold slot_write_lock() across the read-through-write operation and use the shared slot writer seam.

Useful? React with 👍 / 👎.

Comment on lines +2954 to +2957
if raw.get("device") == "gpu-vulkan":
holder = raw
elif isinstance(raw.get("slot"), dict) and raw["slot"].get("device") == "gpu-vulkan":
holder = raw["slot"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Migrate legacy backend-only Vulkan slots

On older supported slot TOMLs that contain backend = "vulkan" but no device, this guard never selects a holder, even though SlotConfig._promote_backend_to_device() explicitly treats that shape as gpu-vulkan. The manager's raw slot load then reaches the container preflight without a device, so the KFD guard also skips it and an AMD host without KFD can continue launching the invalid Vulkan lane after this migration. Normalize the authoritative scalar table's legacy backend value and persist the corresponding device during this pass.

Useful? React with 👍 / 👎.

Comment on lines +2961 to +2963
holder["device"] = new_device
try:
write_toml_atomic(toml_path, raw)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reload already-running slots after relabeling

When an affected Vulkan slot is active during an installer upgrade—or once the self-update invocation is corrected—this write changes only its TOML. The checked post-swap and installer unit-rerender paths deliberately do not restart slot services, so the existing container keeps its old Vulkan devices and argv; on the CPU branch the warning even claims the slot no longer uses the GPU while the live process still does. For the #1888 failure mode, that process can continue returning invalid tokens until an unrelated reload or reboot, so changed active LLM slots need to be stopped/reloaded or surfaced as requiring immediate operator action.

Useful? React with 👍 / 👎.

@thinmintdev

Copy link
Copy Markdown
Contributor Author

Independent review — verdict: request changes (1 blocking, 3 non-blocking)

Reviewed at b8640843 in a detached worktree. Migration-grade scrutiny per the #1867 rails.

What holds up

  • Issue update: migrate existing device="gpu-vulkan" slot TOMLs to gpu-rocm — post-#1923 they refuse to load #1924's requirement is met for the case it describes: AMD-scoped (host_is_amd_gpu), gpu-rocm when kfd_present() else cpu, distinct per-mutation journal breadcrumbs, idempotent, wired as a best-effort pass in run_post_activation_migrations so both Updater.commit() and install.sh's repair path converge.
  • Only device is ever written. Verified by reading: holder is bound only from a literal device == "gpu-vulkan" (top-level or [slot]-nested), and the sole assignment is holder["device"]. Same two-shape convention as retag_stale_slot_images / clear_stale_mtp_overrides.
  • write_toml_atomic semantics — yes, it fully reserializes through tomli_w, so key values survive but comments/formatting do not. That is identical to the tradeoff retag_stale_slot_images and config/migrations/hw_slot_ownership.py already accept on the same files, so it is precedent, not a new regression (but see finding 2). Ownership is not a hazard here: the pass runs in-process as the service account under commit(), and install.sh's root-run path has the P3-perms backstop after the migration block.
  • Triage item 2 (pre-swap gap) is genuinely pre-existing. Updater.commit() step 7 calls run_post_activation_migrations before seam.activate(), and all five prior passes live inside that same function — there is no other call site except install.sh's python -c block (which runs against already-pip-installed target code). Documented-not-fixed is the right call here; it should get its own issue.
  • Triage item 3 (kfd probe as caller vs service user) — reasoning checks out. require_kfd_for_gpu_slot re-probes independently in container.py:load_sync as User=hal0 at slot-load time, so an optimistic gpu-rocm can only ever produce the same loud GpuPreflightError, never a silent bad lane.
  • Interaction with fix(gpu): require /dev/kfd and retire the Vulkan LLM lane #1923's guard after a cpu relabel: verified safe. require_kfd_for_gpu_slot returns immediately for device="cpu" (not gpu-vulkan, not gpu-rocm), and _effective_backend_and_device_class treats device as sole truth, so the slot renders as a CPU lane and loads on a kfd-less box.
  • Idempotency test proves both halves: return 0 and len(calls) unchanged on the re-run (test_idempotent_second_run_logs_nothing_new). "No writes" is structural rather than asserted (the holder is None: continue precedes the write) — acceptable.

Revert-and-confirm-red

Six mutations against the merge head, running tests/updater/test_vulkan_slot_migration.py + tests/updater/test_post_activation_migrations.py (19 collected — note the PR description's "24 new tests" is 13 new + edits to 3 existing):

Mutation Result
is_amd = True (drop AMD scope) 3 failed / 16 passed
kfd-absent branch → gpu-rocm instead of cpu 3 failed / 16 passed
drop the nested [slot] shape handler 1 failed / 18 passed
match any device (break idempotency) 4 failed / 15 passed
cpu fallback logs the routine rocm breadcrumb (silent behavior change) 1 failed (test_cpu_fallback_logs_a_loud_warning)
remove the pass from run_post_activation_migrations 2 failed / 17 passed

Baseline restored → 19 passed. Full suite env -u FORCE_COLOR HAL0_HOME=$(mktemp -d) uv run --extra dev pytest tests/updater tests/install -q531 passed. The tests are load-bearing, not decorative.


1. BLOCKING — the migration relabels non-llama.cpp gpu-vulkan slots, which are a live, currently-advertised configuration

gpu-vulkan was retired for the llama.cpp lane only. capabilities/catalog.py says so explicitly, in the comment right above the suppression this PR builds on (L429-431):

gpu-vulkan survives for the non-llama runtimes below (kokoro / whisper.cpp / ComfyUI), which run genuinely-Vulkan images, and for non-AMD GPUs.

and _RUNTIME_TO_HOST_BACKENDS (L73-79) still fans whisper.cpp / kokoro / vibevoice / ComfyUI out to gpu-vulkan on an AMD host today. So a whisper.cpp or Kokoro-GPU slot created by the picker this week on a Strix box carries device = "gpu-vulkan" legitimately. This migration rewrites all of them:

  • AMD host, kfd present (the ordinary Strix Halo box): that slot loads fine today — require_kfd_for_gpu_slot returns early because kfd is present. fix(gpu): require /dev/kfd and retire the Vulkan LLM lane #1923 never broke it. The migration rewrites it to gpu-rocm anyway: a label its runtime image cannot honour, outside the device list its own picker offers, and it silently swaps gpu_visibility_env's GGML_VK_VISIBLE_DEVICES for HIP_VISIBLE_DEVICES on any slot with a gpu_index pin (providers/_gpu.py:365-368), so the multi-GPU pin is quietly lost on a Vulkan runtime.
  • AMD host, kfd absent: those slots are currently refused (the guard is provider-agnostic — load_sync passes only the device string), so the author's triage is right that they need something. But cpu is the wrong something: whisper.cpp's and ComfyUI's Vulkan images work fine without /dev/kfd, and because _effective_backend_and_device_class makes device the sole hardware truth, cpu strips /dev/dri passthrough entirely. A working GPU image-gen / STT slot becomes a CPU one. The BEHAVIOR CHANGE breadcrumb makes it loud, but the hermes: an upgraded box keeps an old slot ceiling below Hermes' floor, and nothing preflights the anchor window #1867 rails are about not making the mutation in the first place, not only about logging it.

The triage on Codex's P1 answered the AMD/non-AMD axis but not this one: "those slots are equally blocked by #1923's own guard" is true only in the kfd-absent branch, and the PR applies the relabel in both.

Suggested resolution — either:

Either way this needs a test asserting a whisper.cpp/ComfyUI-shaped gpu-vulkan slot's outcome, since today's suite has none.

2. Non-blocking — the docstring overstates what survives the rewrite

Only the device key is ever written … (no other field, comment, or slot is touched…)

write_toml_atomic dumps the whole tomllib-parsed dict through tomli_w: every comment and all formatting in the file are destroyed, and the shipped seed slot TOMLs (installer/etc-hal0/slots/*.toml) are comment-heavy — including the #1888 rationale comments an operator would want to still be there after this exact migration. It matches the existing convention and I am not asking to change the mechanism, but a migration docstring claiming comments are untouched is the kind of claim the #1867 rails exist to keep honest. Please reword (and the PR body's "narrow scope" bullet with it).

3. Non-blocking — install.sh's operator-facing text was not updated

installer/install.sh:2448 and :2462 still enumerate "seed-profile / stale-MTP / runner-image / extra-args", and the block comment at 2426-2435 still describes four passes. An operator running the documented repair/upgrade path gets a transcript that never mentions the vulkan pass — the one pass in the sequence that can make a slot slower.

4. Nit — stub signature drift

tests/updater/test_post_activation_migrations.py's _vulkan_migration(*, job_id=None, kfd_present=None) omits amd_host. Harmless today (production passes only job_id), but the stub no longer mirrors the real signature.


CI

python (3.12), ui, sunset, CodeQL/Analyze all green. γ-suite (chromium) pending — known #1926 flake, a rerun is in flight; nothing in this diff touches the UI surface.

Changes-requested review on #1934 (blocking): the migration matched on
the bare device = "gpu-vulkan" string, so it also rewrote gpu-vulkan
slots belonging to non-llama runtimes (Kokoro TTS, whisper.cpp/Moonshine
STT, ComfyUI) that legitimately run Vulkan images — capabilities/
catalog.py deliberately keeps gpu-vulkan for exactly those runtimes.
On a kfd-present box this relabeled a working Vulkan slot to gpu-rocm,
a label its image can't honor (gpu_visibility_env silently swaps
GGML_VK_VISIBLE_DEVICES -> HIP_VISIBLE_DEVICES, dropping any gpu_index
pin); on a kfd-absent box it demoted a working Vulkan STT/image-gen
slot to cpu, stripping /dev/dri.

Gate on hal0.providers.container._spec_provider_for — the same
runtime-family discriminator load_sync itself uses — and skip a
candidate slot entirely (no relabel, no warning, no log, on either kfd
axis) unless it resolves to None (the default llama-server GPU
provider). The kfd-absent non-llama case (require_kfd_for_gpu_slot
itself over-firing for non-llama runtimes) is tracked separately as
#1941 and is deliberately not addressed here.

Also:
- correct the docstring's false "comments survive" implication —
  write_toml_atomic reserializes via tomli_w and drops them, same as
  every other pass in this module (non-blocking review finding)
- add the vulkan-slot pass to install.sh's post-activation-migrations
  transcript, which still only named four passes (non-blocking)
- update the run_post_activation_migrations test stub to accept
  amd_host, matching the real signature (non-blocking)

TDD: the seven new runtime-scope tests were confirmed RED against the
pre-fix code (kokoro/comfyui/transcription slots were being relabeled)
before this commit made them green.

Refs #1924 #1888
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@thinmintdev

Copy link
Copy Markdown
Contributor Author

Addressed all four findings from the changes-requested review, pushed in f515543.

Blocking — runtime scoping

Fixed. The migration now resolves every device = "gpu-vulkan" candidate slot through hal0.providers.container._spec_provider_for(holder) — the same authoritative runtime-family discriminator load_sync itself uses to pick a provider (None = the default llama-server GPU provider; non-None = Kokoro/Moonshine/ComfyUI/FLM/Qwen3TTS). A slot only gets relabeled when _spec_provider_for returns None; otherwise it's skipped entirely — no relabel, no warning, no log line, on either kfd axis. If _spec_provider_for itself raises (an unrecognized runtime_family), the slot is also left untouched and a distinct updater.vulkan_migration_slot_runtime_unresolved warning is logged (conservative: we can't safely conclude it's llama.cpp-backed, so we don't touch it).

Per your note, the kfd-absent non-llama case (i.e. require_kfd_for_gpu_slot itself over-firing and blocking a genuinely-Vulkan Kokoro/ComfyUI/whisper.cpp slot on an AMD box with no /dev/kfd) is explicitly not touched by this migration — that's #1941's problem, not this one's.

New TDD coverage in tests/updater/test_vulkan_slot_migration.py: test_kokoro_slot_survives_untouched_kfd_present/absent, test_comfyui_slot_survives_untouched_kfd_present/absent, test_transcription_slot_survives_untouched, test_non_llama_slots_log_nothing_on_either_kfd_axis, and test_llama_slot_still_relabels_alongside_untouched_non_llama_slot (a genuine llama.cpp slot in the same directory as a Kokoro slot — the llama slot relabels, its neighbor doesn't). Confirmed these 7 tests were RED against the pre-fix code (kokoro/comfyui/transcription-shaped slots were being wrongly relabeled) before implementing the _spec_provider_for gate.

Non-blocking

  1. Comment-preservation claim — fixed. The docstring's "no other field, comment, or slot is touched" line implied byte-level preservation, which is false (write_toml_atomic reserializes the whole document via tomli_w, same as every other migration in this module). Reworded to "narrow scope by key, not byte-for-byte file preservation," with an explicit note that comments are dropped on any touched file's rewrite — matching retag_stale_slot_images/clear_stale_mtp_overrides's actual (undocumented but real) behavior. Also corrected the equivalent claim in the PR body.

  2. install.sh transcript — fixed. installer/install.sh's post-activation-migrations block (comment header, info line, and the printed summary line) now names the vulkan-slot pass alongside seed-profile/mtp/image-pin/extra-args, in the same order run_post_activation_migrations actually runs them.

  3. Test stub signature — fixed. test_post_activation_migrations.py's _vulkan_migration stub now accepts amd_host (added *, job_id=None, kfd_present=None, amd_host=None), matching the real function's signature after the earlier AMD-scoping fix (b864084).

  4. PR body test-count claim — corrected. The PR body now states the actual counts: tests/updater/test_vulkan_slot_migration.py is a new file with 22 tests (all new relative to main, including the 7 runtime-scope tests added in this round); tests/updater/test_post_activation_migrations.py has 4 existing tests edited (one renamed test_runs_all_five_passestest_runs_all_six_passes), no new test functions there.

Verification

  • env -u FORCE_COLOR HAL0_HOME=$(mktemp -d) uv run --extra dev pytest tests/updater tests/install -q538 passed
  • uv run ruff format --check src tests → all formatted
  • make lint → all checks passed
  • python scripts/check_sunset.py → scar markers at baseline (192 ≤ 192)
  • bash -n installer/install.sh → syntax OK

Not merging or enabling automerge — awaiting review.

@thinmintdev

Copy link
Copy Markdown
Contributor Author

Re-review of f5155430 — verdict: approve, 0 blocking (3 nits, none gating)

Re-reviewed in a fresh detached worktree at f5155430. The blocking finding is genuinely fixed, not papered over.

Blocking finding — resolved

_spec_provider_for is the right discriminator: it is literally the function load_sync calls to pick a provider, so the migration's "is this llama.cpp-backed" question is answered by the same authority that decides what actually launches. Verified by reading container.py:1123-1177:

  • RuntimeFamily is a closed literal (llama-server, flm, kokoro, qwen3tts, moonshine, comfyui); every non-llama member has a dispatch branch returning a provider instance, so None really does mean "the default llama-server GPU provider".
  • The gate catches the profile-family path, not only the type string — I probed a type = "llm" slot carrying profile = "qwen3-tts": skipped, device unchanged. That is the case the new tests don't cover (they exercise type=tts|image|transcription), and it works.
  • whisper.cpp has no runtime family of its own; a whisper-shaped slot resolves through family == "moonshine" or slot_type == "transcription" → Moonshine provider → skipped. Covered.
  • No false-skip for real llama slots: llm / embedding / reranking types with no special profile fall through to None and still relabel (test_llama_slot_still_relabels_alongside_untouched_non_llama_slot proves it with a Kokoro neighbor in the same directory).

Exception branch — verified conservative, as documented. I probed it directly by making _spec_provider_for raise: the slot file is byte-identical afterwards on both kfd axes, relabel_stale_vulkan_slots returns 0, and exactly one updater.vulkan_migration_slot_runtime_unresolved warning fires per pass, carrying the slot name. Not a silent skip — logged, and at the same key granularity as the sibling updater.vulkan_migration_slot_unreadable breadcrumb (both omit job_id, consistent with the existing per-slot warnings in this function). Correct call: an unresolvable runtime family cannot be concluded to be llama.cpp-backed, so not touching it is the safe default.

Deferring the kfd-absent non-llama case to #1941 is the right split — that is require_kfd_for_gpu_slot over-firing, not a migration bug.

Revert-and-confirm-red on the new gate

Four mutations at f5155430, each run against tests/updater/test_vulkan_slot_migration.py (22 collected):

Mutation Result
remove the _spec_provider_for block entirely (pre-fix behavior) 7 failed / 15 passed
keep the call, drop only the skip (if False:) 7 failed / 15 passed
invert the gate (if provider is None: — skip llama, relabel non-llama) 18 failed / 4 passed
exception branch falls through to relabel instead of skipping 22 passed ← see nit 1

Exactly 7 red on revert, matching the claim. Baseline restored → 22 passed; full env -u FORCE_COLOR HAL0_HOME=$(mktemp -d) uv run --extra dev pytest tests/updater tests/install -q538 passed.

Re-verified post-revision

  • Device-key-only still holds, now proven by value rather than by reading: I round-tripped a slot carrying runtime, port, n_gpu_layers, gpu_index, [model] and [server].env, deleted device from both sides, and asserted the parsed documents are equal. Only device changed.
  • Idempotency still holds and is now stronger than the suite asserts: the second run leaves the file byte-identical (not merely "device unchanged" / "no new log lines") — the continue precedes the write, so no rewrite occurs.
  • Non-AMD no-op unchanged: still returns before the directory is globbed.

Non-blocking follow-ups (2, 3, 4 from the first pass — all landed)

  • Docstring comment claim: fixed, and better than asked — it now names write_toml_atomic's tomli_w reserialization and states that comments are dropped, plus notes the same is true of retag_stale_slot_images / clear_stale_mtp_overrides.
  • install.sh transcript: fixed in all three places (block comment, info line, printed summary), in the order the passes actually run.
  • Stub signature: fixed (amd_host added).
  • PR body test-count claim: corrected (22 new + 4 edited).

Nits — not gating

  1. The exception branch is untested. Mutating it to relabel-instead-of-skip leaves all 22 tests green. It behaves correctly (I verified out-of-band), but nothing guards it against a future edit. A ~10-line test monkeypatching hal0.providers.container._spec_provider_for to raise, asserting file-unchanged + the runtime_unresolved event, would close it. Same for the profile-family path (type=llm + a non-llama profile), which is the discriminator's more interesting half and is currently only covered by type-shaped slots.
  2. Private cross-module import. from hal0.providers.container import _spec_provider_for couples the updater to a private name. Justified here (parity with load_sync is the whole point, and a public alias would be its own churn), but worth a one-line note at the import so a future rename doesn't silently change this migration's scope.
  3. Docstring formatting artifact"…logs nothing\n new." in relabel_stale_vulkan_slots wrapped mid-phrase. Cosmetic.

CI

All green on this head, including γ-suite (chromium) (11m58s pass — the #1926 flake did not recur).

No blocking objections. Approving.

…gate

Approved-with-nits follow-up on #1934: the except branch of the
_spec_provider_for gate (added for the runtime-scoping blocking fix)
was untested — a mutation that turned its `continue` into an
optimistic relabel would have passed all 22 existing tests.

Add test_runtime_resolution_error_leaves_slot_untouched_and_logs,
which monkeypatches _spec_provider_for to raise and asserts: the slot
TOML is byte-identical after the run, the pass returns 0 (no crash),
and updater.vulkan_migration_slot_runtime_unresolved is logged with
the slot name and error. Confirmed this test fails when the except
branch is mutated to fall through as llama.cpp instead of skipping
(verified locally, reverted before commit — not left in the tree).

Also add test_qwen3tts_profile_slot_survives_untouched, pinning the
profile-family-shaped case (a slot dispatched to a non-llama runtime
via its `profile` field rather than device/type) as a regression
guard — reviewer manually verified this path but nothing pinned it.

And a one-line comment on the _spec_provider_for import noting the
parity-with-load_sync is intentional, so it isn't "cleaned up" into a
public wrapper or re-derived locally.

Refs #1924 #1888
@thinmintdev

Copy link
Copy Markdown
Contributor Author

Pushed the two nit tests + import comment in b7600d7.

  • test_runtime_resolution_error_leaves_slot_untouched_and_logs — monkeypatches _spec_provider_for to raise, asserts the slot TOML is byte-identical after the run, the pass returns 0 (no crash), and updater.vulkan_migration_slot_runtime_unresolved is logged with slot/error. Confirmed this test catches a mutation: temporarily changed the except branch to fall through as provider = None (i.e. treat an unresolvable runtime as llama.cpp and relabel it) — the new test failed (assert result == 0 → got 1), reverted, re-ran to confirm green again.
  • test_qwen3tts_profile_slot_survives_untouched — pins the profile-family-shaped case (type="llm", profile="qwen3-tts", device="gpu-vulkan" — the seed profile name resolves to the qwen3tts runtime_family via ProfileCatalog, matched before the generic type="tts" → Kokoro fallback in _spec_provider_for) as a regression guard on both kfd axes.
  • Added a one-line comment where _spec_provider_for is imported from container.py, noting the parity-with-load_sync is intentional.

Verification: env -u FORCE_COLOR HAL0_HOME=$(mktemp -d) uv run --extra dev pytest tests/updater tests/install -q540 passed; ruff format --check clean; make lint clean; check_sunset.py at baseline.

Not merging — leaving for operator merge.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

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