Set up hal0 — the local AI inference appliance
hal0 runs open models on your own hardware: every inference slot is its own podman container supervised by systemd, with a control-plane dashboard on :8080, OmniRouter, Hermes agents, and Hindsight memory. This guide covers a fresh install for new operators and an in-place upgrade for existing boxes — plus the full R5 / v1.0.0-alpha.0 changelog.
v1.0.0-alpha.0's installer was live-validated on two substrates — podman 4.9.3 · privileged and podman 5.7 · unprivileged — both a clean fresh install and an in-place upgrade, exit 0, non-destructive.
PrerequisitesRequirements
hal0 runs on any systemd Linux on x86_64. It self-detects your package manager and GPU/NPU backend.
Host
- systemd Linux, x86_64
- Python 3.11 – 3.14 on
$PATH - podman (container runtime for slots)
- Disk for models (point at a big volume with
--models-dir) - Free ports:
8080(API) + the slot pool
Distributions
Package-manager steps route through lib/distro.sh:
- Debian / Ubuntu · Fedora / RHEL
- Arch · openSUSE · Alpine
NPU (FastFlowLM) ships an Ubuntu .deb only — on non-apt distros the host NPU probe waits on a manual install; GPU/CPU paths are fully supported everywhere.
Inference backends
Vulkan recommended on Strix Halo
RADV/Vulkan benchmarked +40% prefill · +16% gen · −30% TTFT vs ROCm on the same weights (unified-memory APUs). v1.0.0-alpha.0 standardizes shared APU models on Vulkan.
ROCm · NPU · CPU
ROCm/HIP for discrete AMD, AMDXDNA NPU via FastFlowLM, and a fully-supported CPU path. hal0 picks per-model at launch; you can pin a device.
The installer runs as root (re-execs under sudo). hal0-api runs as root so it can apply updates + manage units. A dedicated hal0 system user runs the non-root services (agents, hermes-gateway, hindsight-api). Model containers (podman) are the sandbox boundary.
New usersInstall hal0
The one-liner
# Fetches the signed release tarball, cosign-verifies it against the # workflow OIDC identity, then hands off to install.sh. curl -fsSL https://hal0.dev/install.sh | bash
From a clone (development / air-gapped)
# Standard install at /opt/hal0 sudo bash installer/install.sh # Point model pulls at a larger disk sudo bash installer/install.sh --models-dir=/mnt/ai-models # Local-only dev install under $PWD/.hal0ai (editable, no systemd) bash installer/install.sh --dev # Set everything up but don't start the units yet sudo bash installer/install.sh --no-start
Environment overrides
| Variable | Default | Purpose |
|---|---|---|
HAL0_PREFIX | /opt/hal0 | Install root |
HAL0_PORT | 8080 | API + dashboard port |
HAL0_PYTHON | python3 | Interpreter |
HAL0_NO_PROBE | 0 | =1 skips the end-of-install hardware probe |
HAL0_TOOLBOX_IMAGE_{VULKAN,ROCM,…} | — | Override per-backend container image refs |
What the installer does
- Pre-flight — Python 3.11–3.14, systemd, x86_64, disk, free ports, container-runtime smoke test. Accurate diagnostics on failure (see troubleshooting).
- Layout — code at
/usr/lib/hal0/hal0-<version>behind acurrentsymlink + shared venv; config in/etc/hal0/; state in/var/lib/hal0/{models,registry,slots,cache}.--devlands everything under$PWD/.hal0ai/. - Install — creates the venv, installs the release tree, links
/usr/local/bin/hal0+hal0-agent. - Dashboard UI — builds
ui/distif missing andnpmis present (self-hosted fonts — renders identically offline). - Config defaults — writes
hal0.toml,api.env,upstreams.toml.capabilities.tomlships empty by design → the first-run dashboard renders the bundle picker. Existing files are never clobbered. - systemd — installs + enables
hal0-api+hal0-openwebuiand starts them (unless--no-start). Per-slothal0-slot@<name>.serviceunits are managed by hal0 when slots load. - Hardware probe — sanity-checks GPU/Vulkan/ROCm/NPU device access.
New usersFirst run
- Open the dashboard — browse to
http://<host>:8080. On first load,capabilities.tomlis empty, so the dashboard renders the bundle picker / setup. - Pick a tier — choose a hardware-anchored bundle (a curated model collection sized to your RAM) or start empty and add models yourself.
- Verify health —
hal0 doctorshould report all-green;GET :8080/api/healthreturns200. Services show active in the dashboard.
# Sanity from the CLI hal0 doctor --json hal0 slot list curl -fsS http://127.0.0.1:8080/api/health
First model & first chat
# Pull a model into the catalog, create a slot, chat hal0 model pull <model> hal0 slot create my-slot --model <model> --provider llama-server hal0 chat --slot my-slot
Models own logical behavior such as chat templates. Physical launch facts — device, NGL, threads, binary, profile, and image pin — belong to the slot.
Existing usersUpgrade in place — no reinstall
Upgrading from an earlier hal0 is an idempotent, non-destructive re-run — not a fresh install. It converges units/services/config through the new code + one daemon-reload, heals prior failed slot units, and never clobbers your config.
# In place — re-run the installer (or `hal0 update`) curl -fsSL https://hal0.dev/install.sh | bash # hal0 update swaps the /current symlink atomically sudo hal0 update
The upgrade path was exercised live on both boxes — a cached in-place upgrade completed in ~22 s and even healed a previously-failed slot unit. No data wiped.
The code swap is safe on its own, but R5 ships one-shot data migrations that are operator-run at a downtime window, not automatic. Your box keeps working on the old config after the code swap (see back-compat) — run the migrations deliberately.
Migrations (run deliberately, at a window)
The current CLI ships several one-shot migrations. None run automatically on boot or update. Preview each plan, apply it deliberately, then verify the result.
The migrate CLI at a glance
| Command | Purpose | Default | Key flags |
|---|---|---|---|
hal0 migrate model-layout |
Create the canonical <recipe>/<capability>/ symlink tree for a v0.1.x model store. Model files stay in place. |
dry-run | --apply · --force/-f · --registry · --mount-root · --canonical-root |
hal0 slot migrate-hw |
Restore slot-owned hardware values: NGL, runner binary, and deliberate image pins. | dry-run | --apply · --yes/-y · --stop-services |
hal0 slot migrate-id-keying |
Flip every slot artefact from name-keyed (<name>.toml, hal0-slot@<name>) to id-keyed (<id>.toml, hal0-slot@<id>). |
apply after confirmation | --dry-run · --yes/-y · --stop-services |
hal0 memory migrate --from honcho --to hindsight |
One-shot memory-engine migration; only needed if you ran Honcho. | apply; use --dry-run to preview |
--dry-run · --agent · --dataset · --since · --resume |
hal0 migrate model-layout — end-to-end
Use this for a v0.1.x model store under /mnt/ai-models/. It builds the canonical symlink tree under /var/lib/hal0/models/.
Model files are never moved or copied. The command is dry-run by default and idempotent after a successful apply.
Standard usage — plan, apply, verify:
# 1. Plan the move. No files written. Read the action table. sudo hal0 migrate model-layout # → table of: action / link / target / source / reason # → "planned by leaf" rollup # → "--dry-run — would create N symlink(s); pass --apply to write." # 2. Apply. sudo hal0 migrate model-layout --apply # → "applied N symlink(s) under /var/lib/hal0/models." # Idempotent — re-running --apply after success is a no-op. # 3. Verify. Exit 0 means current; exit 1 means links are still pending. sudo hal0 doctor migrations sudo hal0 doctor models find /var/lib/hal0/models -type l
The command writes only symlinks under --canonical-root. It does not modify the source files under --mount-root.
To roll back, remove only the links listed by the dry-run/apply table. Do not delete model source files.
--force overwrites a differing symlink, but never a non-symlink file. Review every would-overwrite row before using it.
Use --registry, --mount-root, and --canonical-root only when your paths differ from the FHS defaults.
hal0 slot migrate-hw — end-to-end
This migration restores physical settings to slots: NGL, runner binary, and deliberate image pins. It also clears the folded model hardware columns.
The bare command is a dry-run. --apply rewrites slot TOMLs and the registry DB after creating a timestamped backup.
# 1. Preview the fold (no files written, no columns nulled). sudo hal0 slot migrate-hw # → per-slot list of: set_ngl · set_binary · set_image_pin · drops # 2. Apply. Let the CLI stop active hal0 units. sudo hal0 slot migrate-hw --apply --yes --stop-services # → "backup written to /var/lib/hal0/backups/<timestamped>.tar.gz" # → "Applied hardware-ownership fold: ..." # → "Restart hal0-api (and daemon-reload) to pick up the slot HW grid." sudo systemctl daemon-reload sudo systemctl start hal0-api hal0 doctor # expect all-green for slots
--apply refuses while hal0-api or any hal0-slot@* unit is active. Stop them first, or pass --stop-services.
The bare command is the dry-run; this subcommand has no separate --dry-run flag.
hal0 slot migrate-id-keying — end-to-end
This optional migration changes slot artefacts from mutable-name keys to stable IDs. It renames the TOML, state directory, systemd unit, and podman container.
Run it only after upgrading to the bilingual id-keying runtime. See the full runbook before applying.
# 1. Stop hal0. Even the preview refuses while these units are active. sudo systemctl stop 'hal0-slot@*' hal0-api # 2. Preview — prints the computed name→id plan, no file moves. sudo hal0 slot migrate-id-keying --dry-run # 3. Apply. The command backs up slots/ + hal0.db, then flips artefacts. sudo hal0 slot migrate-id-keying --yes sudo systemctl daemon-reload sudo systemctl start hal0-api hal0 slot list # confirm every slot appears once systemctl list-units 'hal0-slot@*'
The id-keying command has no undo. It writes a timestamped backup under /var/lib/hal0/backups/ before moving anything.
If verification fails, stop hal0 and restore that backup. Use --stop-services instead of the manual stop command when desired.
Honcho → Hindsight
Only boxes that previously ran Honcho need the memory migration. Run it before upgrading; Hindsight-only boxes are already current.
hal0 memory migrate --from honcho --to hindsight
Why your box keeps working before you migrate
Tolerant config
Legacy slot and model keys load without a boot crash. Dry-run the relevant migrator to see what will change.
Nothing automatic
Update swaps code, but one-shot migrations remain operator-controlled. Existing layouts continue working until you schedule the migration.
model-layout adds symlinks without moving source data. The slot migrations rewrite state and therefore create backups first.
ReleaseWhat's new — R5 / v1.0.0-alpha.0
R5 hardens install and runtime behavior. The current alpha adds workload-oriented profiles, slot-owned hardware, stable slot IDs, a first-class brain route, and clearer slot/model drawers.
Current Workload profiles & slot-owned hardware
- Seeded profiles are workload-oriented templates rather than hardware identity. Applying one stamps defaults; later model edits do not mutate the profile.
- Physical launch facts now live on each slot: device, NGL, threads, resolved runner binary, profile, and optional
image_pin. - Models remain logical and device-agnostic. The slot-purity update moves
chat_templateto the model and sunsets the old slot tier. hal0 slot migrate-hwfolds prior model/profile hardware values back onto slots. It is dry-run-first, backup-first, and maintenance-window gated.- Slot artefacts can be converted from names to stable IDs with
hal0 slot migrate-id-keying; name-keyed boxes remain supported.
Dashboard drawers
- The slot drawer presents a typed hardware grid: Device, Image, Profile, Threads, and NGL.
- It shows the resolved runner binary, profile-default image placeholder, and actual running image reference with slot ID.
- Parallel and Extra Args now sit in the slot's Model section. Reasoning and MTP remain model-owned.
- The model drawer adds the typed Thinking capability (
auto,on, oroff) before MTP.
Memory, brain & Hermes
- Memory tools use the Hindsight surface:
hindsight_recall,hindsight_retain, andhindsight_reflect. Honcho is removed. - The first-class
hal0-brainmodule now serves the primaryPOST /api/brain/chatsteward route. - Hermes persona and brain-profile registration run in the API boot lifespan, so fresh installs, updates, and dev starts share one path.
hal0 agent install hermes --reset-personasrestores canonical personas;--repairremains non-destructive.- Hermes uninstall now stops its service before removal, and provisioning accepts both string and
Pathdatabase paths.
Install & runtime hardening
- Container-runtime preflight now surfaces the real cause on failure (e.g. kernel keyring-quota exhaustion) instead of a misleading nesting/keyctl message.
- GPU preflight + renderer now derive the render group from the device node's owner GID (not the group name) — fixes silent GPU-access loss on hosts where
renderD128's group is misnamed. - Slot units emit
StartLimit*in[Unit]so restart rate-limiting actually applies. - Hermes gateway install runs as the
hal0user. Uninstall now stops the Hermes service before removing it. hal0 agent status --jsonemits real JSON;reconcile_listenerswired into/api/ports.- Shared APU models standardized on Vulkan (benchmark-backed).
Housekeeping (SWEEP)
- Deprecated surfaces remain machine-stamped
HAL0-SUNSET: v1.0.0until their scheduled removal. - Python and UI package manifests now report
1.0.0-alpha.0consistently. - Boot is split into 14 named, observable phases with a typed
BootState. - The bilingual slot runtime reads both name-keyed and id-keyed layouts; the operator chooses when to flip.
OperateTroubleshooting
unprivileged LXC Install refuses at the container-runtime preflight
Symptom: podman run fails with crun: create keyring: Disk quota exceeded. Cause: the host kernel keyring byte-quota (kernel.keys.maxbytes) is exhausted — usually leaked keyrings from repeated failed healthchecks. hal0 correctly refuses (a real slot couldn't start either).
# Clear it, then re-run install # reboot the container — or — keyctl clear @s # or raise the quota on the Proxmox / host kernel sysctl -w kernel.keys.maxbytes=2000000
GPU Slot runs but on CPU / GPU access denied
If /dev/dri/renderD128 is owned by a group that isn't your host's render group, older builds group-added the wrong GID. v1.0.0-alpha.0 derives the GID from the device node itself. Confirm your hal0 user is in the device's owner group:
stat -c '%G %g' /dev/dri/renderD128 id -nG hal0
Hardened host: permissions look wrong after install
A CIS/STIG umask (0027/0077) leaking into the install can leave the tree 0700. The installer normalizes to 022 for its own body; if you see PermissionError reading /usr/lib/hal0 or /etc/hal0/slots, run hal0 doctor perms --fix.
hal0 doctor --json reports health across slots, memory banks, Hermes, services, and perms. It's the fastest first look for any issue.
ReferencePaths & commands
Filesystem
/usr/lib/hal0/current | active code (symlink) |
/etc/hal0/ | config (hal0.toml, slots/, api.env) |
/mnt/ai-models | default source model store |
/var/lib/hal0/models | canonical recipe/capability symlink tree |
/var/lib/hal0/hal0.db | model registry + slot identity |
/var/lib/hal0/slots | per-slot runtime state |
Everyday commands
hal0 doctor | health across the platform |
hal0 slot list / create | manage inference slots |
hal0 model pull / default | manage the catalog + tuning |
hal0 doctor migrations | detect pending model-layout links |
hal0 migrate model-layout | preview/apply canonical model links |
hal0 update | atomic in-place upgrade |
hal0 chat --slot <s> | talk to a slot |
hal0-api.service serves the control plane on :8080. Slot units use hal0-slot@<name> before id-keying and hal0-slot@<id> after it.
Agents, Hermes gateway, and Hindsight run as the non-root hal0 user.