BigMoeOnEdge/docs/backends.md
Raffaele 2b808d5215
build: llama.cpp to upstream master of 2026-10-18 (#278)
* build: llama.cpp to upstream master of 2026-10-18

The fork branch bmoe/expert-ready-hook-1810-dl is upstream master 5aeac37 plus the same single
expert-ready-hook commit, in the variant that also answers the CPU registry's get_proc_address,
so a build that loads the CPU backend as a module reaches it too. 523 upstream commits; the new
base loads architectures the old one did not (Kolibri-1 among them).

Upstream reworked common's chat and speculative APIs. The engine follows with the smallest change
that keeps its behaviour, each one the way upstream's own callers now do it:

- the rendered prompt arrives as parts; it is joined back with .str(), as upstream's server does,
  and tokenized exactly as before;
- the parser arena's JSON serialization is gone; build_parse_params copies the arena instead, as
  upstream's chat session does, and reasoning_format already lives on the template inputs;
- common_speculative_process reads a common_batch; the batch the engine decodes is mirrored into
  one at the three MTP call sites. The decode itself stays on the public llama_batch API.

Same backends and build options, so docs/backends.md holds. All 23 host gates pass, a tiny
Mixtral-style and a tiny OLMoE model pass all 90 byte-identity gates, and on a real model the
greedy output is unchanged, MTP drafts and accepts, and the Metal prefill still runs.

* test(gates): G18c compares decide and perplexity to within the kernels' batch-shape rounding

The two paths put the same choice tokens through different batch shapes, a
wide prefill and one row at a time, and the ARM CPU backend from the
2026-10 base accumulates them in another order: the three log-probabilities
differ by the same 1e-12 relative, the log-softmax normaliser one ulp apart.
Upstream never promised that comparison bitwise; the gates that prove
streamed == resident stay bit-exact. A bound of 1e-9 relative keeps the
check nine orders of magnitude below a wrong answer.
2026-10-18 13:28:31 +02:00

9.6 KiB

Compute backends: what llama.cpp carries, and what this engine has run on

The engine does not implement a compute backend. Every device it can use is one llama.cpp registers, reached through the public ggml_backend_dev_* registry, so the list of what is possible is llama.cpp's and the list of what is known to work is much shorter. This page keeps the two apart.

The same facts cut by operating system, next to the rest of what each platform has, are in platforms.md.

What the pinned llama.cpp carries

Read from ggml/src/ and ggml/CMakeLists.txt of the pinned submodule (efaae9929, read again at that bump: same backends, same columns). A bump can add or remove rows; redo the reading rather than trusting this table.

The last three columns come from reading the sources, not from running them: the type each backend registers its devices as, whether its sources name GGML_OP_MUL_MAT_ID (the op every routed expert goes through, so a backend without it sends the experts back to the CPU), and whether it can wrap memory the caller already owns (buffer_from_host_ptr: the capability it advertises, and whether the function is actually wired in). A backend naming an op is not proof it accepts every weight type for it.

backend build flag hardware registers as names the expert op wraps host memory (advertised / wired)
CPU GGML_CPU (on) every machine CPU yes yes / yes
BLAS GGML_BLAS a host BLAS library (Accelerate, OpenBLAS, ...) accelerator no yes / yes
Metal GGML_METAL Apple GPUs GPU yes yes / yes
CUDA GGML_CUDA NVIDIA GPUs GPU, or integrated GPU yes no / no
HIP GGML_HIP AMD GPUs through ROCm; compiles the CUDA sources as CUDA as CUDA as CUDA
MUSA GGML_MUSA Moore Threads GPUs; compiles the CUDA sources as CUDA as CUDA as CUDA
Vulkan GGML_VULKAN any GPU with a Vulkan driver GPU, or integrated GPU yes no / yes
OpenCL GGML_OPENCL Adreno first, other OpenCL GPUs GPU yes no / a function that declines
SYCL GGML_SYCL Intel GPUs through oneAPI GPU, or integrated GPU yes no / yes
WebGPU GGML_WEBGPU browsers and native WebGPU GPU yes no / no
Hexagon GGML_HEXAGON Qualcomm NPU (HTP) GPU yes no / no
CANN GGML_CANN Huawei Ascend NPU GPU yes no / no
OpenVINO GGML_OPENVINO Intel CPU, GPU and NPU through OpenVINO GPU yes no / no
ET GGML_ET Esperanto ET-SoC GPU yes no / no
zDNN GGML_ZDNN IBM Z accelerator no no / no
ZenDNN GGML_ZENDNN AMD CPUs through ZenDNN accelerator yes yes / yes
VirtGPU GGML_VIRTGPU a host GPU remoted into a virtual machine (the remoted device's) (the remoted device's) no / yes
RPC GGML_RPC another machine, or a loopback server GPU (the remote's) no / no

Three things in that table decide what this engine can do with a backend.

  • The registered type is not a description of the hardware. NPUs register as GPU, and Metal registers as GPU on unified memory. Nothing here reads the type as "has memory of its own"; that is measured (hardware-planning.md).
  • The advertised capability and the wired function disagree on three backends. Vulkan, SYCL and VirtGPU report buffer_from_host_ptr false and implement it. OpenCL reports it false and wires a function that returns nothing, so there the false holds in effect, and the call is safe to make. The planner therefore records true as yes and anything else as unknown.
  • There is no backend for the Apple Neural Engine, for MediaTek or Samsung NPUs, or for an Intel NPU outside OpenVINO. On those the engine has the CPU and whatever GPU backend the platform has.

What this engine has run on

backend where state what it does today
CPU Android, Windows, macOS, Linux measured on phones and desktops; gates run in CI on Linux decode, prefill, expert streaming
Hexagon Android, Snapdragon with Hexagon v73 or later measured on a phone; in the release APK (scripts/build-hexagon-android.sh) prefill on the NPU, decode on the CPU (npu-prefill.md)
Metal macOS measured on one 16 GB Apple-silicon laptop; on in every macOS build prefill on the GPU, decode on the CPU (npu-prefill.md)
RPC host test builds a test fixture only: a loopback server fronting the CPU, so the prefill-device gates have a second device nothing a front-end can select
BLAS macOS, where llama.cpp turns it on by default linked, never given a weight nothing placed on it
OpenCL Android built, in the release APK beside Hexagon (scripts/build-hexagon-android.sh), importing the Khronos ICD loader so a phone without OpenCL still starts the engine, and the vendor's own library where there is one; in July the dense set on a phone's integrated GPU was measured and lost 27 % (RunConfig::dense_on_device) run on a Snapdragon-class phone with an Adreno GPU: 344 GiB/s at prefill width against the cores' 36, and it did not reproduce the host's result, so the plan refuses it on correctness and the prefill stays where the decode is; named by hand it is refused by the arena, whose per-expert placement its padded allocations do not fit (npu-prefill.md)
Vulkan Android, Linux, Windows built, in the release APK and in the Linux and Windows archives, as a module that loads only where a Vulkan driver is present. One 2020 phone ran it and its driver ended the probe, which is what the isolated probe contains run on the same phone: 44 GiB/s one token wide against the cores' 36, and 36 at prefill width, so no prefill win; its fp16 path did not reproduce the host's result and its fp32 one did. Forced by hand, a 1727-token prompt prefilled in 85 s against 65 on the cores (npu-prefill.md)
CUDA, HIP, SYCL Linux, Windows not in any archive: a module built where the toolchain is (-DGGML_CUDA=ON, ...) and dropped beside the binary is loaded like the shipped ones; none has been run here unknown
MUSA, WebGPU, CANN, OpenVINO, ET, zDNN, ZenDNN, VirtGPU - never built here unknown

Three backends, then, with a measurement behind them, out of eighteen.

Two decisions behind the archives, written down so they are not re-made by accident. CUDA and HIP stay out of the default archives: their runtime libraries weigh more than the rest of the archive, and Vulkan reaches the same GPUs; a module built where the toolchain is loads from beside the binary like the shipped ones. And a module that is present and never given a weight is not known to be free: on one laptop with an integrated GPU, a build with Vulkan linked in loaded the model five times slower than one without, cause not found. The modules build owes that measurement, on the same machine, before the load time with an idle GPU module is called a non-event.

What each role asks of a backend

The engine uses a device in one of four ways, and each asks for something different.

role what the backend must do where it stands
decode on the host be the CPU backend, with the expert-ready hook for the overlap every platform
prefill device (--prefill-device) run wide graphs, accept weights through ggml_backend_tensor_set, and take the model's weight types or a type the engine can convert to Hexagon and Metal measured; the mechanism names no backend, so the others are untested, not excluded
layers on a device (--gpu-layers, from llama.cpp's capacity fitter) have memory of its own, or the placement frees nothing applied by the planner where a device has its own memory; never run on such a machine here. On unified memory the fitter's answer is set aside, and forcing it on the 16 GB laptop ended in the GPU's out-of-memory error
streamed experts computed on a device wrap memory the streamer already reserved (buffer_from_host_ptr), which is what the fourth column above is about not built. Metal offers it; Vulkan, OpenCL and SYCL implement it without advertising it; CUDA has no route through the public API

How a backend reaches the planner

No rule names a backend. A device that registers is enumerated, asked for its memory, and measured: the same matmul on the model's own quantized format as the host, checked against the host's result, and the cost of one host/device crossing. A device that does not reproduce the host's result is excluded on correctness, whatever its speed.

So a backend llama.cpp adds needs no code here to be seen. It does need a module beside the executable, and a machine to run it on. Every release is built that way (BMOE_BACKEND_MODULES, the top-level CMakeLists): the engine knows no backend at compile time, each backend is a library ggml loads from the executable's directory at start-up, the CPU one comes in one library per instruction-set tier with the best picked on the machine, and a module whose driver library is missing fails to load and is simply not there, instead of keeping the binary from starting. The plan then reports what registered, and keeps "this machine has no accelerator" apart from "this build carries none". The Metal backend, the first one tried on a desktop, surfaced three faults no test had (one memory pool counted twice, an idle device taking graph nodes, a cache the kernel compressed). The honest reading of "unknown" above is that the same should be expected of each of the others.

scripts/rented-box.sh is there for that: one session on a machine nobody here owns, which builds, plans, measures, and writes down what the plan predicted next to what the engine did.