Skip to content

Quick start — Proxmox LXC

A Proxmox LXC is the primary deployment shape for hal0 on AMD Strix Halo. An LXC shares the host kernel, so the iGPU and XDNA NPU are passed through as ordinary device nodes — no VM, no vGPU shim, near-native throughput. hal0’s own FLM provider treats “Strix Halo under Proxmox LXC” as its main target.

This guide assumes a working Proxmox VE host with a Strix Halo (Ryzen AI Max+) APU and walks the full path: prepare the host kernel, create a privileged Ubuntu 24.04 container, pass through the GPU + NPU, install hal0, and wire up the NPU trio.

Everything the accelerators need lives on the host kernel, because the LXC shares it. Do this once on the PVE host.

The iGPU uses the in-tree amdgpu driver. The NPU needs the amdxdna kernel module loaded and recent NPU firmware:

  • Kernel ≥ 6.11 with amdxdna available (modprobe amdxdna; confirm /dev/accel/accel0 appears on the host).
  • NPU firmware ≥ 1.1.0.0 (validated in the field at 1.1.2.65).

hal0 supplies the userspace runtime (XRT + the FastFlowLM binary) inside its container image, but it cannot load a kernel driver from inside a container — the driver and firmware must already be present on the host.

On Strix Halo the GPU’s usable memory is the amdgpu GTT pool, which is carved from system RAM (unified memory). A model can only allocate up to the GTT cap, not the full RAM pool — so if GTT is left at its small default, large models won’t fit even on a 128 GB box.

hal0 measures the live GTT cap and sizes its model recommendations against it, but it does not configure GTT for you. Set it on the host kernel command line. On a 128 GB Strix Halo, the reference values from hal0’s profile-tuning work are:

Terminal window
# /etc/default/grub — append to GRUB_CMDLINE_LINUX_DEFAULT, then update-grub && reboot
amd_iommu=off amdgpu.gttsize=126976 ttm.pages_limit=32505856
ParameterValueMeaning
amdgpu.gttsize126976GTT window in MiB (≈ 124 GiB)
ttm.pages_limit32505856TTM page cap in 4 KiB pages (≈ 124 GiB — keep it equal to gttsize)
amd_iommu=offDisables the IOMMU; measured worth ~5–12% on this workload

Create a privileged Ubuntu 24.04 LXC with nesting enabled (containers run inside it) and enough disk for models:

Terminal window
# on the Proxmox host, as root
pct create 105 local:vztmpl/ubuntu-24.04-standard_24.04-2_amd64.tar.zst \
--hostname hal0 --cores 8 --memory 8192 --swap 2048 \
--rootfs local-zfs:64 --net0 name=eth0,bridge=vmbr0,ip=dhcp \
--ostype ubuntu --features nesting=1,fuse=1,keyctl=1,mknod=1 \
--unprivileged 0 --onboot 1

Append the passthrough block to /etc/pve/lxc/105.conf on the host. This exposes four device nodes and grants the container cgroup permission to open them:

/etc/pve/lxc/105.conf
dev0: /dev/dri/renderD128
dev1: /dev/dri/card0
dev2: /dev/kfd
dev3: /dev/accel/accel0
lxc.cgroup2.devices.allow: c 226:* rwm
lxc.cgroup2.devices.allow: c 234:* rwm
lxc.cgroup2.devices.allow: c 261:* rwm
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.prlimit.memlock: unlimited
lxc.apparmor.profile: unconfined
DeviceNodeRole
iGPU render/dev/dri/renderD128Vulkan / ROCm render node
iGPU card/dev/dri/card0 (amdgpu)DRM card device
ROCm compute/dev/kfdROCm compute queues
XDNA NPU/dev/accel/accel0FastFlowLM NPU device

The cgroup majors correspond to the DRM (226), the dynamically-allocated kfd/accel majors (234, 261), and a misc device (10:200) on this reference host. lxc.prlimit.memlock: unlimited lets the NPU DMA-lock model weights; lxc.apparmor.profile: unconfined is required for the container runtime to launch slots inside the LXC.

Start (or restart) the container so the passthrough takes effect:

Terminal window
pct start 105

Open a shell in the container (pct enter 105) and install the one prerequisite the Ubuntu base image lacks, then run the installer:

Terminal window
apt-get update && apt-get install -y python3-venv python3-pip
curl -fsSL https://hal0.dev/install.sh | bash

The installer auto-installs podman, builds the Python venv, writes the systemd units, probes the hardware (it should now see the iGPU and NPU via the passed-through nodes), seeds a hardware-recommended slot, and starts hal0-api on port 8080. See Install hal0 for the full breakdown of what the bootstrap does.

On an apt host with the NPU present, the installer already handles the FastFlowLM (FLM) userspace: it verifies and installs the pinned FastFlowLM .deb (matched to your Ubuntu release), pulls the NPU runtime libraries, and installs libxrt-npu2 from the lemonade PPA. A single flm serve process backs the NPU trio — one chat model plus optional embedding and speech-to-text “passengers” sharing the NPU.

Confirm the NPU runtime is reachable from inside the container:

Terminal window
flm validate # → "flm validate ok — NPU runtime reachable"
hal0 doctor # re-runs preflight; hardware probe records the NPU

The seeded npu slot serves a chat model on port 8088. The trio’s embedding and STT passengers are opt-in — enable them per slot (the asr / embed keys on the slot’s [npu] table) once the chat slot is loaded. See Voice (STT/TTS) and Slots for the trio routing details.

6. Surface true host memory pressure (optional)

Section titled “6. Surface true host memory pressure (optional)”

Inside an LXC, hal0 only sees its own cgroup memory share — but the GPU’s GTT pool is drawn from the same physical DIMMs every other tenant and the host kernel use. Give hal0 a read-only Proxmox API token so the dashboard’s unified-memory bar shows real host pressure instead of pretending only this container’s bytes exist.

Create a read-only token in the Proxmox UI (Datacenter → Permissions → API Tokens, with PVEAuditor on /), then configure it via the dashboard Settings → Proxmox card, or write /etc/hal0/proxmox.json directly:

{
"proxmox": { "host": "192.0.2.10", "port": 8006, "verify_ssl": false, "service": "PVE" },
"auth": { "user": "hal0@pve", "token_name": "readonly", "token_value": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }
}

hal0 polls GET /cluster/resources (cached 30 s) and surfaces host RAM total/used plus every running tenant’s allocation. Non-Proxmox installs leave the file absent and the feature stays silent.

Terminal window
hal0 status # system + slot status
systemctl status hal0-api # control plane
journalctl -fu hal0-api # live logs

The API and dashboard listen on http://<container-ip>:8080; OpenWebUI on :3001.