AMD AI inference stack for NixOS — packages XRT, XDNA driver plugin, FastFlowLM, and Lemonade with a NixOS module for NPU + ROCm GPU support.
On Apple Silicon (aarch64-darwin) the same flake also serves the cross-platform Lemonade server (llama.cpp Metal backend) via a nix-darwin module — see macOS (nix-darwin). The AMD/NPU/ROCm stack is Linux-only.
| Package | Description | Source |
|---|---|---|
xrt |
Xilinx Runtime for AMD NPU | Built from Xilinx/XRT |
xrt-plugin-amdxdna |
XDNA userspace driver plugin | Built from amd/xdna-driver branch 1.7 |
fastflowlm |
NPU-optimized LLM runtime | Built from FastFlowLM |
lemonade |
OpenAI-compatible local AI server (lemond + CLI + web UI + Tauri desktop app) |
Built from lemonade-sdk/lemonade |
llama-cpp-rocm |
ROCm-accelerated llama.cpp backend | Built from ggerganov/llama.cpp |
llama-cpp-vulkan |
Vulkan-accelerated llama.cpp backend | Built from ggerganov/llama.cpp |
whisper-cpp-vulkan |
Vulkan-accelerated whisper.cpp backend | pkgs.whisper-cpp.override { vulkanSupport = true; } |
stable-diffusion-cpp-rocm |
ROCm-accelerated stable-diffusion.cpp backend | pkgs.stable-diffusion-cpp.override { rocmSupport = true; } |
ds4 |
DeepSeek V4 inference engine, Strix Halo (gfx1151) ROCm backend (ds4, ds4-server, ds4-bench, ds4-eval, ds4-agent) |
Built from antirez/ds4 |
gaia |
AMD GAIA agent framework launcher (gaia, gaia-cli, gaia-mcp, gaia-emr, gaia-code) |
uvx wrapper around amd/gaia |
benchmark |
Multi-backend benchmark harness | nix run .#benchmark |
CPU backends for llamacpp / whispercpp / sd-cpp use vanilla nixpkgs packages (pkgs.llama-cpp, pkgs.whisper-cpp, pkgs.stable-diffusion-cpp) and are wired automatically when enableLemonade = true. The GPU backends track nixpkgs too; the mtp recipe — built-in MTP support added by lemonade #1944, backed by llama.cpp #22673 — fires on any nixpkgs llama.cpp past b9175.
The lemonade package composes three derivations:
lemonade.passthru.web-app— React web UI (buildNpmPackage, served bylemondat/)lemonade.passthru.tauri-frontend— desktop-shell renderer bundle (buildNpmPackage)lemonade.passthru.tauri-app— Tauri desktop binary (rustPlatform.buildRustPackageagainst webkit2gtk-4.1)
Both UIs are built by default. Headless / server-only consumers can opt out:
nix-amd-ai.overlays.default = final: prev: {
lemonade = (prev.lemonade.override {
withWebApp = true; # default — web UI served by lemond
withDesktopApp = false; # skip Rust + webkit2gtk closure
});
};# flake.nix
inputs.nix-amd-ai.url = "github:noamsto/nix-amd-ai";
# host configuration
{inputs, ...}: {
imports = [inputs.nix-amd-ai.nixosModules.default];
hardware.amd-npu = {
enable = true;
enableNPU = true; # default; set false for GPU-only hosts (see "Other hardware")
enableFastFlowLM = true; # LLM inference on NPU (requires enableNPU)
enableLemonade = true; # OpenAI-compatible API server
enableROCm = true; # ROCm GPU backends (llamacpp + sd-cpp)
enableVulkan = true; # Vulkan GPU backends (llamacpp + whispercpp)
enableImageGen = true; # default true; set false to drop sd-cpp from closure
lemonade.user = "youruser";
};
users.users.youruser.extraGroups = ["video" "render"];
}On Apple Silicon the flake ships a darwinModules.default exposing services.lemonade. It installs the Lemonade server and runs it as a per-user LaunchAgent (the llama.cpp Metal backend needs a GUI login session, so it cannot run as a root daemon). The Metal/sd.cpp backends are fetched into ~/.cache/lemonade on first run, exactly as the upstream .pkg does — there is no NPU/ROCm wiring on macOS.
# flake.nix
inputs.nix-amd-ai.url = "github:noamsto/nix-amd-ai";
# darwin configuration
{inputs, ...}: {
imports = [inputs.nix-amd-ai.darwinModules.default];
services.lemonade = {
enable = true;
port = 13305; # default
host = "localhost"; # default
};
}The package alone (no service) is also available: nix build github:noamsto/nix-amd-ai#lemonade on aarch64-darwin produces bin/lemond + bin/lemonade serving the OpenAI-compatible API at http://localhost:13305/api/v1. It wraps upstream's prebuilt, server-only lemonade-embeddable-*-macos-arm64 release (no web UI / Tauri app — those ship only in the .pkg).
Pre-built packages are available via Cachix:
# nix.settings in your NixOS config (see caveat below for flake nixConfig)
substituters = ["https://nix-amd-ai.cachix.org"];
trusted-public-keys = ["nix-amd-ai.cachix.org-1:F4OU4vw/lV2oiG6SBHZ+nqjl4EFJuqI4X9A7pvaBmhQ="];Important
Put this in nix.settings (NixOS) or your daemon's nix.conf. A substituter added only via flake nixConfig takes effect only for trusted users — otherwise Nix silently ignores it and rebuilds everything from source, including the Tauri app's crates.io cargo-vendor fetch (the failure in #28).
Warning
Do not .follows our nixpkgs input. The overlay is intentionally built against this flake's pinned nixpkgs (see flake.nix pinned) so the input closure hash matches both cache.nixos.org (Hydra-cached pkgs.llama-cpp.override, etc.) and our Cachix. If you add inputs.nix-amd-ai.inputs.nixpkgs.follows = "nixpkgs", the overrides re-hash against your nixpkgs and every backend rebuilds from source. Just leave this input pinned:
# good — let nix-amd-ai keep its own pinned nixpkgs
inputs.nix-amd-ai.url = "github:noamsto/nix-amd-ai";
# bad — forces rebuilds of llama-cpp / whisper-cpp / stable-diffusion-cpp
# inputs.nix-amd-ai.inputs.nixpkgs.follows = "nixpkgs";- NixOS with kernel >= 6.14 (has
amdxdnadriver built-in) — only required whenenableNPU = true - AMD Ryzen AI processor with XDNA 2 NPU (Strix Point / Strix Halo) for the NPU path; the GPU backends run on any supported AMD GPU with
enableNPU = false(see "Other hardware") - User in
videoandrendergroups
The module splits into an NPU half and a GPU half. The NPU half (XRT + amdxdna + FastFlowLM) is built and tested for XDNA 2 (Strix Point / Strix Halo) — that's what FastFlowLM targets. The GPU backends are independent and run on other AMD GPUs.
Set enableNPU = false to drop the XRT/amdxdna closure (kernel module, IOMMU param, udev rules, memlock limits) and run GPU-only. Example for a Hawk Point APU (Ryzen 9 8945HS, Radeon 780M / gfx1103):
hardware.amd-npu = {
enable = true;
enableNPU = false; # no XDNA-2 NPU on Hawk Point
enableVulkan = true; # 780M via RADV — works, and fastest on these iGPUs
enableLemonade = true;
lemonade.user = "youruser";
};-
Vulkan is the recommended path: RADV is arch-agnostic, so llama.cpp / whisper.cpp run on any RDNA3 iGPU including the Radeon 780M (Phoenix / Hawk Point).
-
NPU (
enableFastFlowLM) is XDNA-2 only; the assertion blocks it unlessenableNPU = true. -
ROCm (
enableROCm): the shippedllama-cpp-rocmis compiled withgfx1103in itsCMAKE_HIP_ARCHITECTURESlist, so it carries native 780M kernels — noHSA_OVERRIDE_GFX_VERSIONworkaround should be needed. This is untested on actual Hawk Point hardware, and rocBLAS coverage forgfx1103APUs can be uneven, so Vulkan remains the recommended path. If ROCm misbehaves, the usual fallback is to alias the arch togfx1100:systemd.services.lemond.environment.HSA_OVERRIDE_GFX_VERSION = "11.0.0";
- Kernel modules (
amdxdna) - Udev rules for NPU device access
- PAM limits (unlimited memlock for NPU buffer allocation)
- XRT + plugin merged tree for runtime plugin discovery
- Lemonade systemd service with XRT/FLM/ROCm/Vulkan environment
- Environment variables (
XILINX_XRT,XRT_PATH) - Declarative backend wiring (both the
lemondservice and direct CLI usage receive the ROCm/Vulkan backend paths automatically)
The lemonade source build deliberately doesn't bundle backend llama-server / whisper-server / sd-server binaries — it expects host-provided paths. The module exports the matching env vars from the lemond service Environment and the user session, then lemonade migrates them into ~/.cache/lemonade/config.json:
| Flag | What gets wired |
|---|---|
enableLemonade |
CPU recipes always-on: llamacpp:cpu, whispercpp:cpu, sd-cpp:cpu (when enableImageGen) |
enableROCm |
llamacpp:rocm, llamacpp:system (via LEMONADE_GGML_HIP_PATH), sd-cpp:rocm (when enableImageGen) |
enableVulkan |
llamacpp:vulkan, whispercpp:vulkan, sd-cpp:vulkan (when enableImageGen) |
enableVllm (default false) |
vllm:rocm from the lemonade-sdk/vllm-rocm prebuilt (requires enableROCm); pick the GPU target with vllmGpuTarget (gfx1150/gfx1151). Experimental, ~7.6 GB closure — see below |
enableImageGen (default true) |
Gates all sd-cpp:* packages; turn off for ~150 MB CPU / ~1.5 GB ROCm savings on headless LLM-only hosts |
Omni models (e.g. LMX-Omni-*) pull in two backends that need extra host plumbing the module wires automatically with enableLemonade (#33): whispercpp resolves its writable runtime dir from the unit's RuntimeDirectory, and the runtime-downloaded kokoro TTS binary is a foreign prebuilt ELF, so the module enables nix-ld (its default libraries already cover koko's openssl + gcc-libs) and re-exports NIX_LD* into the lemond service. nix-ld is set via mkDefault, so hosts managing it themselves can opt out.
enableVllm wires the experimental vllm:rocm backend, repackaged from the
upstream lemonade-sdk/vllm-rocm prebuilt (#63).
Building vLLM + ROCm from source isn't viable here — nixpkgs rocmPackages
trails ROCm 7.15 and lacks the Strix gfx targets — so we relocate their portable
Python + torch + TheRock-ROCm bundle instead (interpreter-patched, not
autoPatchelf'd, which would inject mismatched nixpkgs libs and segfault torch).
It's off by default and adds a ~7.6 GB closure with no binary-cache substituter.
Validated on gfx1150 (standalone and through lemonade's OpenAI API). The
gfx1151 (Strix Halo) target builds and its vllm-server launches — torch and
vLLM import cleanly, so the packaging carries over — but no gfx1151 kernel has
run, because there is no Halo host to run it on. On gfx1150 our benchmarks
still put Vulkan ahead of ROCm and vLLM's batching doesn't help single-user
workloads, so enableVllm mainly matters on gfx1151 (where the
Vulkan-fills-VRAM-first freeze on X11 makes the ROCm path worthwhile) or for
vLLM-specific features.
Vanilla v10.5.0 ignores these env vars on NixOS for several reasons that this flake patches in-tree (see pkgs/lemonade/default.nix:postPatch, issue #5, upstream lemonade-sdk/lemonade#1791):
install_backendshort-circuits onfind_external_backend_binarybefore theno_fetch_executablesthrow and the rocm-stable / TheRock runtime fetches, so user-supplied*_binpaths actually skip the entire download flow.- The Linux ROCm
LD_LIBRARY_PATHblock is gated on the same check, so a nix-storellama-serverkeeps its RPATH-resolved libs instead of being shadowed by~/.cache/lemonade/bin/.../lib. is_ggml_hip_plugin_available()honorsLEMONADE_GGML_HIP_PATHso thesystemllamacpp recipe stops being permanentlyunsupportedon NixOS.LEMONADE_WHISPERCPP_VULKAN_BINis added to the env-var migration table (upstream only mapped CPU/NPU for whispercpp).ConfigFile::get_defaultshonorsLEMONADE_DEFAULTS_PATH, so the module can seed backend bin paths from a store path instead of the hardcoded/usr/share/lemonade/defaults.jsonthat NixOS can't populate (v10.7.0 dropped the env→config migration this replaced).- The download SSE handler treats
sink.writefailure as a transient client disconnect rather than a cancel signal, so a backgrounded Tauri window doesn't kill an in-flight multi-GB download.
If lemonade backends reports a backend as installed but benchmarks report <5 t/s decode on a small model, you're on CPU — check that the matching enable* option is set and the host has been rebuilt.
LEMONADE_DEFAULTS_PATH only seeds ~/.cache/lemonade/config.json on lemond's
first run — afterwards ConfigFile::load merges the packaged defaults
under the persisted file, so every key the module declares goes inert. Backend
bin paths survived that because they point at stable /etc/lemonade/backends/*
symlinks, but scalars did not: a host that first started lemond before enabling
enableVllm kept global_timeout = 0, which vLLM reads as its startup-readiness
budget and turns into zero poll attempts (#68).
The lemond unit therefore re-applies the module-declared keys on every start,
leaving everything else to whatever the web UI persisted. lemonade.settings
rides the same path for keys the module has no dedicated option for:
hardware.amd-npu.lemonade.settings = {
max_loaded_models = -1; # keep a small NPU model and a big GPU model resident together
auto_evict = true; # then let lemond reclaim on idle / VRAM pressure
};max_loaded_models (default 1) is what makes models take turns — raise or
unset it (-1) to keep several resident. auto_evict is a separate,
opt-in background reclaimer (default off) that unloads idle models and
sheds them once VRAM crosses auto_evict_threshold_pct (default 0.90); it
pairs naturally with an unlimited max_loaded_models. Anything lemond's
RuntimeConfig validates is accepted. Values merge recursively over the
module's computed defaults, so overriding llamacpp.args does not drop the
sibling llamacpp.*_bin paths.
Per-model eviction knobs (pinned, evict_idle_timeout,
downsize_idle_timeout, evict_weight_factor, and a per-recipe auto_evict
override) live in lemond's separate recipe_options.json and are set through
lemonade itself, not through this option.
Reconciliation only ever writes keys, never deletes them: dropping a key from
settings stops it being re-applied but leaves the last value in the persisted
config. Set it back to the value you want rather than removing the line.
Two keys are not reachable this way. lemond persists --port and --host
into config.json itself on every start, after the reconcile hook has run, so
settings.host / settings.port are silently overwritten — use the dedicated
lemonade.host and lemonade.port options instead. That same write also
rewrites the file through a fresh ofstream + rename, which resets its mode to
0644 on every start; the hook preserves whatever mode it finds, but it cannot
hold the file tighter than lemond leaves it.
Lemonade >=11.5.0 stopped sending Access-Control-Allow-Origin: * by
default, so non-loopback browsers get a 403 Origin not allowed unless their
origin is listed in LEMONADE_ALLOWED_ORIGINS. Like host/port, this is env-only
— there is no config.json key, so lemonade.settings cannot reach it; set
lemonade.allowedOrigins instead. Loopback origins and non-http(s) desktop
schemes are always allowed, so this only matters once lemonade.host is
bound to something a LAN or remote browser can reach; the module warns if
you set the former without the latter.
WebKitGTK suspends the network process for windows that are minimized, hidden, or moved to another workspace. That kills the SSE progress stream lemond uses for downloads at ~60–90 s. Without our patch, that nuked the whole download mid-flight. With the patch, the download keeps running server-side and finishes regardless — but the UI stops seeing progress until you refocus the window (and may need a refresh to pick up the result). For very large pulls, prefer the regular browser at http://localhost:13305 or lemonade pull <model> from the CLI; both survive backgrounding cleanly.
The desktop app is the only part of lemonade that pulls a Rust + npm build (and a crates.io cargo-vendor fetch). Headless/server hosts that only need the lemond API + CLI can skip it entirely with lemonade.desktopApp.enable = false; — this drops the Tauri build path from the closure. (The pre-built app is also on the binary cache, so configuring the substituter avoids building it from source in the first place.)
The iGPU draws GPU memory from the GTT pool. By default the kernel exposes ~27 GB addressable, which covers the 17–22 GB models this flake targets on a 64 GB Strix Point host — so leave these options unset there; they're a no-op.
On a 128 GB Strix Halo host you need to raise the ceiling to expose the
large unified pool for big models. The module takes sizes in GiB and
computes the ttm page counts for you (pages = GiB × 262144):
hardware.amd-npu.gpuMemory = {
ttmSizeGiB = 120; # GTT pool ceiling → ttm pages_limit
pagePoolSizeGiB = 60; # pre-cached pool → ttm page_pool_size
};This emits options ttm pages_limit=31457280 page_pool_size=15728640 via
boot.extraModprobeConfig. Recommended starting point for 128 GB:
| Option | Value (128 GB) | Meaning |
|---|---|---|
ttmSizeGiB |
120 | Hard ceiling on the GTT pool; leaves ~8 GiB for the OS/CPU. |
pagePoolSizeGiB |
60 | Pre-cached pool inside that ceiling. |
Note: these Halo values are guidance from the Strix Halo wiki, not measured on a Halo host by this flake (the development target is a 64 GB Strix Point P14s). Treat them as a starting point, not a validated tune.
Leave RAM headroom — don't set ttmSizeGiB to your full physical RAM; the
CPU and OS still need their share (the 120/128 example keeps a margin).
The Strix Halo wiki suggests amd_iommu=off for a small memory-read speedup.
Do not do this on a host that uses the NPU. amdxdna needs the IOMMU present
for PASID; with amd_iommu=off there is no IOMMU at all and the NPU dies.
amd_iommu=off is only viable on a GPU-only host that has given up XDNA.
The IOMMU default-domain mode is a separate knob. amdxdna historically
required Translated mode (SVA/PASID), so the module used to pin
iommu.passthrough=0. Since the June 2026 amdxdna fix (upstream 5b96159,
"skip PASID tag in non-SVA mode") the driver no longer tags DMA with an invalid
PASID under an identity default domain, so the NPU works with iommu=pt too.
The module no longer forces the mode — it leaves the kernel default (Translated
on NixOS), and hosts that want passthrough for a memory-read win can set
iommu=pt themselves.
The wiki recommends biasing the CPU to performance (governor + HWP boost) for
+3% memory bandwidth / +5–8% pp512. We don't wire this, because on a
shared-TDP APU the tradeoff is murky:
- There's no direct CPU-governor → GPU-clock link — the iGPU has its own
clock domain. Pinning CPU cores to
performancedoesn't raise GPU clocks. - On shared package power, forcing the CPU to max frequency steals TDP from the iGPU during bandwidth-bound decode — a bounded, possibly net-negative lever.
- The knob actually aimed at decode is the C-state latency floor
(
/dev/cpu_dma_latency), which keeps the fabric/memory subsystem clocked; the governor is not. - Prefill (
pp512) does have a CPU component, so the wiki's prefill claim is plausible — for prefill, not decode.
It's left out until an A/B on an idle/AC host (governor pinned performance)
confirms whether the wiki's numbers reproduce on Strix Point. Tracked in
#19.
Lemonade v10.10.0 stopped auto-discovering a flm on PATH; it now only looks
there when flm.prefer_system is set in config.json. Without it, lemond
ignores the nix-provided flm, marks the NPU backend installable/"not
installed", and lists no FLM models even after flm pull. The module now seeds
flm.prefer_system = true (with enableFastFlowLM), so fresh installs work.
A cached ~/.cache/lemonade/config.json wins over that seed (lemonade merges
user config over defaults), so hosts that ran an older lemonade keep the stale
prefer_system: false. Fix an existing host once, after rebuilding, by deleting
the cached config so the module's defaults reseed it:
rm ~/.cache/lemonade/config.json
sudo systemctl restart lemondSee #62.
Harmless. aie2_get_info handles the NPU's GET_INFO ioctl, and the mainline amdxdna driver implements only a subset of query types (AIE status/version/metadata, clock, hw-contexts). When userspace (xrt-smi, a system monitor, or the lemonade/FastFlowLM init path) probes a power/sensor/telemetry param the driver doesn't implement yet, it returns -EOPNOTSUPP and logs that *ERROR* line — often on a timer, so it repeats. NPU inference is unaffected. Upstream is filling in the missing queries (power reporting ~Linux 7.1, hwmon exposure tracked in xdna-driver#323); a newer kernel makes the line disappear.
AMD GAIA is a Python agent framework that uses lemond as its inference backend (Email Triage / Code / Jira / Blender / RAG / MCP agents, plus a built-in web UI). Upstream targets pip / electron installers, neither of which fits a NixOS host cleanly, and the Python dependency tree is large and fast-moving (weekly-ish releases, torch + transformers + ~60 transitive deps). The flake therefore ships a thin uvx wrapper rather than a from-source Nix build:
nix run .#gaia # interactive CLI; falls back to printing help
nix run .#gaia -- ui # launch the web UI (FastAPI + bundled SPA)
nix shell .#gaia -c gaia-mcp # MCP bridge server
nix shell .#gaia -c gaia-code # code-agent CLIThe wrapper pre-sets LEMONADE_BASE_URL=http://localhost:13305/api/v1 (matching the module's default lemonade.port); override the env var to point at a different host. Behind the scenes it runs uvx --from "amd-gaia[ui]==<version>" <entry> — so the first invocation downloads the wheel and ~60 transitive deps into ~/.cache/uv (~30 s, with progress visible) and subsequent runs reuse it.
Bump the pinned version in pkgs/gaia/default.nix when a new GAIA release lands and you want it. CI doesn't auto-bump GAIA today (only lemonade / fastflowlm / xdna are wired into scripts/check-updates.sh).
ds4 is antirez's self-contained native inference engine for DeepSeek V4. It is deliberately narrow — not a generic GGUF runner — and its ROCm backend targets Strix Halo (gfx1151) only, so the package is x86_64-linux + AMD-hardware specific and pins gfx1151 via the gpuTarget argument.
nix run .#ds4 -- -m /path/to/DeepSeek-V4-Flash.gguf # interactive chat
nix shell .#ds4 -c ds4-server --ctx 100000 # OpenAI-compatible serverThe engine only; bring your own GGUF (see upstream STRIXHALO.md for the recommended DeepSeek-V4-Flash quant and the host GTT/ttm.pages_limit kernel tuning). Upstream ships no releases, so pkgs/ds4/default.nix pins a commit and is bumped manually — CI builds it but scripts/check-updates.sh doesn't track it.
To run ds4-server as a managed systemd unit, enable it via the module:
hardware.amd-npu.ds4 = {
enable = true;
user = "youruser"; # must be in render + video
model = "/var/lib/ds4/DeepSeek-V4-Flash.gguf"; # runtime path, not store-copied
ctx = 100000;
extraArgs = ["--ssd-streaming" "--kv-disk-dir" "/var/lib/ds4/server-kv"];
};Binds 127.0.0.1:8000 by default (host/port); the unit runs with render/video GPU access, LimitMEMLOCK=infinity, and a writable /var/lib/ds4 (StateDirectory) for the optional SSD-streaming KV cache. Retarget another AMD GPU with package = pkgs.ds4.override { gpuTarget = "gfx1103"; } (experimental — upstream only validates gfx1151).
All numbers measured on Strix Point (gfx1150, Radeon 890M iGPU, 64 GiB DDR5-5600). Prompt 256 tokens, generation 128 tokens, 3 iterations after 1 warmup.
| Metric | ROCm | Vulkan | Winner |
|---|---|---|---|
| Prefill (pp512) | 360 ± 18 t/s | 370 ± 3 t/s | Vulkan (+3%, within noise) |
| Decode (tg128) | 13.86 ± 0.18 t/s | 17.52 ± 0.33 t/s | Vulkan (+26%) |
| Backend | Model | TTFT (s) | Decode (t/s) |
|---|---|---|---|
| Vulkan (llamacpp:vulkan) | Qwen3.5-9B-GGUF (UD-Q4_K_XL) |
1.36 | 12.9 +/- 0.1 |
| ROCm (llamacpp:rocm) | Qwen3.5-9B-GGUF (UD-Q4_K_XL) |
1.69 | 10.8 +/- 0.1 |
| FLM (flm:npu) | qwen3.5-9b-FLM |
4.17 | 11.9 +/- 4.5 |
Notes: FLM's TTFT is dominated by a one-off NPU compile-to-cache; steady-state decode is the useful number. FLM's GGUF-vs-proprietary format means quantization isn't bit-identical to the llamacpp row, so treat these as same-family, not same-weights.
Recommendation:
- General LLM inference (7B–26B Q4): use Vulkan. On Strix Point 890M with llama.cpp b8770, Vulkan wins decode at every size tested and ties or wins prefill. The previous "ROCm for prefill-heavy" advice no longer holds now that ROCm targets gfx1150 natively (the gfx1102 Tensile arch-logic was apparently more tuned than gfx1150's is today).
- Power-budget / idle-GPU scenarios: use FLM/NPU — decode is competitive with Vulkan and offloads the GPU, but the compile-on-first-load TTFT is noticeable.
- ROCm is kept installed as a fallback and for ecosystem tooling (
rocminfo, profiling, HIP apps); re-evaluate when newer rocBLAS/Tensile logic for gfx1150 lands.
Enable all three and let lemonade pick the recipe per model.
Coding agents (Claude Code, opencode) ship large system prompts — 10k+ tokens once MCP servers, skills, and tool schemas are loaded. On a Strix Point iGPU, prompt processing runs at ~350 t/s, so the agent's first turn spends 25–35 s before the first token is emitted. Neither lemonade nor the agents send SSE keep-alive events during that silent window, and most clients close the socket after ~30 s, yielding:
[Info] (Process) srv log_server_r: done request: POST /v1/chat/completions 127.0.0.1 200
[Error] (HttpClient) CURL error: Failed writing received data to disk/application
[Error] (WrappedServer) Streaming request failed: ...
Tracked upstream as lemonade-sdk/lemonade#1364. Until that lands, this module sets LEMONADE_GLOBAL_TIMEOUT=0 on the lemond service to disable its own 300 s upstream cap, which covers the variant where lemonade gives up on llama-server. The downstream client timeout remains a separate problem — best addressed by shortening the prompt or choosing a leaner agent.
Practical guidance:
- Vulkan for short-prompt workloads. Decode is ~26 % faster than ROCm; safe for chat UIs and ad-hoc prompts that stay roughly under 10k tokens, where prompt processing finishes well before the ~30 s client cutoff.
- ROCm for large-prompt workloads. Its ~15 % faster prefill shaves 10k-token prompts from ~33 s (Vulkan) to ~28 s — just enough to land under most clients' silence timeout. Coding agents like Claude Code and opencode fall in this bucket.
- pi (Hugging Face's recommended local coding agent — see the official docs) is the best fit for this hardware. Its prompt is a fraction of Claude Code's and it's designed around llama.cpp-served local models.
- Claude Code / opencode are usable — strip down MCP servers, skills, and plugins to shrink the startup prompt, and prefer ROCm while #1364 is unresolved.
You can verify that backends are correctly wired by running:
lemonade backendsAll AMD-applicable recipes should report installed (kokoro is intentionally skipped — Rust port, narrower use case):
Recipe Backend Status Message/Version
flm npu installed v0.9.40
llamacpp cpu installed b8983
rocm installed b8770
system installed -
vulkan installed b8770
sd-cpp cpu installed master-558-8afbeb6
rocm installed master-558-8afbeb6
whispercpp cpu installed v1.8.4
vulkan installed v1.8.4
Quick image-gen smoke test:
lemonade pull SD-Turbo
curl -s -X POST http://localhost:13305/api/v1/images/generations \
-H 'Content-Type: application/json' \
-d '{"model":"SD-Turbo","prompt":"a red apple on a wooden table","size":"512x512"}' \
| jq -r '.data[0].b64_json' | base64 -d > out.pngWith both enableROCm and enableVulkan set, lemond logs should show Starting server on port 8001 (backend: vulkan) and no Installing sd-server line — sd-server is invoked directly from the nix store. sd-cpp's auto backend selection prefers Vulkan whenever both variants are already installed: 11.5.1 made that the case for every engine (llamacpp, sd-cpp, whispercpp), matching llamacpp's pre-existing Vulkan-first preference order. To exercise the ROCm path specifically, pin it with hardware.amd-npu.lemonade.settings.sdcpp.backend = "rocm"; (the runtime config section is sdcpp, not sd-cpp).
The .#benchmark harness measures real decode throughput through a running lemond,
compares it against a hardware-derived ceiling, and gates against silent CPU fallback:
nix run .#benchmark # interactive TUI
nix run .#benchmark -- --no-tui --backend rocm Gemma-4-26B-A4B-it-GGUF # headless / CIThe TUI walks Hardware → Preflight → Mode → Model → Params → Run → Results, with a live
status rail (gfx arch · GTT budget · GPU% · power · preflight) above every screen.
--no-tui prints markdown and exits non-zero when a model falls below --min-decode-tps
(default 5 t/s), reliably signalling CPU fallback.
See pkgs/benchmark-go/README.md for the full
reference: wizard flow, modes (HTTP / MTP A/B / backend), the model picker
(search, fit glyphs, markers), the results columns (Decode, Predicted, % ceil), the
status rail, preflight fixers, and every headless flag.
Authoritative MTP A/B numbers (idle GPU + AC + performance power profile) are pending — the methodology is stable but no clean reference run has been committed yet.
- Build: All packages built and cached on every push to
main - Update: Weekly check for upstream releases, auto-creates PR with version bumps