fix(distributed): backend discovery hid GPU-only backends behind the controller's capability#10947
Merged
Merged
Conversation
…mode GET /backends/available filtered the gallery against the system state of the host serving the request. In a distributed deployment that host is the controller, which typically has no GPU, while the GPUs live on worker nodes. Any meta backend whose capabilities map lacks a "default" (or "cpu") key was therefore dropped from the listing entirely — longcat-video, vllm-omni, ltx-video, parakeet, edgetam and qwentts were invisible in the UI even though installing them by name on a GPU worker worked fine. Workers now report their own meta-backend capability at registration and the controller persists it on the node row. The controller cannot derive it: OS-dependent capabilities (metal, darwin-x86, nvidia-l4t) and the CUDA runtime refinements are only observable on the worker. Nodes registered before this field existed fall back to a coarse capability derived from their GPU vendor and VRAM. Backend discovery then evaluates compatibility as the union over healthy backend nodes, so a backend runnable on any node is offered while one no node can run stays hidden. Each remote capability is evaluated through a capability-pinned system state, otherwise a forced capability on the controller image (LOCALAI_FORCE_META_BACKEND_CAPABILITY or /run/localai/capability) would silently override every worker's verdict. With no registered nodes the listing is byte-for-byte what it was, so single-node deployments are unaffected. Also fixes the same-root-cause misclassification in /api/operations, which used the capability-filtered listing to decide whether an operation was a backend or a model install. A GPU-only backend installing on a worker is still a backend operation on the controller, so that lookup is now unfiltered. Assisted-by: Claude:claude-opus-4-8 golangci-lint Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Implementation for the specs added in the previous commit, plus the two
remaining discovery endpoints.
Capability-filtered backend discovery evaluated compatibility against the
system state of the host serving the request. In a distributed deployment
that host is the controller, which typically has no GPU, while the GPUs
live on worker nodes. Any meta backend whose capabilities map lacks a
"default" (or "cpu") key was dropped entirely — longcat-video, vllm-omni,
ltx-video, parakeet, edgetam and qwentts were invisible in the UI even
though installing them by name on a GPU worker worked fine.
Workers now report their own meta-backend capability at registration and
the controller persists it on the node row. The controller cannot derive
it: OS-dependent capabilities (metal, darwin-x86, nvidia-l4t) and the CUDA
runtime refinements are only observable on the worker. Nodes registered
before this field existed fall back to a coarse capability derived from
their GPU vendor and VRAM.
Discovery then evaluates compatibility as the union over healthy backend
nodes, so a backend runnable on any node is offered while one no node can
run stays hidden. Each remote capability is evaluated through a
capability-pinned system state, otherwise a forced capability on the
controller image (LOCALAI_FORCE_META_BACKEND_CAPABILITY or
/run/localai/capability) would silently override every worker's verdict.
With no registered nodes the listing is byte-for-byte what it was, so
single-node deployments are unaffected.
Four surfaces shared this root cause and are all routed through the same
helper now:
- GET /backends/available
- GET /api/fine-tuning/backends
- GET /api/quantization/backends
- /api/operations backend-vs-model classification, which additionally
had no reason to filter by capability at all: a GPU-only backend
installing on a worker is still a backend operation on the
controller, so that lookup is now unfiltered.
Assisted-by: Claude:claude-opus-4-8 golangci-lint
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
mudler
approved these changes
Jul 19, 2026
mudler
enabled auto-merge (squash)
July 19, 2026 07:01
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Capability-filtered backend discovery evaluated compatibility against the host serving the request. In a distributed deployment that host is the controller — typically a GPU-less pod — while the GPUs live on worker nodes.
Any meta backend whose
capabilities:map lacks adefault(orcpu) key was dropped entirely. On a live cluster,GET /backends/availablereturned 275 entries with zero longcat/l4t entries:longcat-video,vllm-omni,ltx-video,parakeet,edgetamandqwenttswere invisible in the UI, even though installing them by name on a GPU worker works fine —DistributedBackendManager.InstallBackendships only the backend name, which the worker resolves against its own hardware.Confirmed against
backend/index.yaml: the present/absent split falls exactly along whether a meta declares adefaultkey.llama-cpp,whisper,vibevoice,moss-tts-cpp,rfdetr,depth-anything,mlx,vllm(all havedefault)longcat-video(nvidia keys only),vllm-omni(nvidia + amd only)Root cause
ListAvailableBackendsEndpoint(core/http/endpoints/localai/backend.go:331) callsgallery.AvailableBackends(...)→availableBackendsWithFilter(..., filterByCapability=true)(core/gallery/gallery.go:397), which filters onbackend.IsCompatibleWith(systemState)using the controller's system state. For a meta backend that resolves toCapabilitiesMap[systemState.Capability(...)]; on a GPU-less controllergetSystemCapabilities()returnsdefaultandCapability()falls backdefault→cpu, so a meta with neither key is dropped.Fix
Workers report their own meta-backend capability at registration; the controller persists it on the node row (
capabilitycolumn, AutoMigrate). The controller cannot derive it — OS-dependent capabilities (metal, darwin-x86, nvidia-l4t) and CUDA-runtime refinements are only observable on the worker, and a node'sgpu_vendoris empty on Apple Silicon. Nodes registered before this field fall back to a coarse capability from GPU vendor + VRAM.Discovery then unions capabilities over healthy backend nodes (
status = healthy AND node_type = backend, matching the scheduler's own predicate). Each remote capability is evaluated through a capability-pinnedSystemState, so a forced capability on the controller image (LOCALAI_FORCE_META_BACKEND_CAPABILITY//run/localai/capability) cannot override a worker's verdict.An empty node list reproduces the previous listing exactly — single-node behavior is untouched. A registry error degrades to the local-only listing rather than blanking the catalog or 500-ing.
Four surfaces fixed
GET /backends/availableGET /api/fine-tuning/backends(finetune.go:279)GET /api/quantization/backends(quantization.go:198)/api/operationsbackend-vs-model classification (ui_api.go:216) — now unfiltered: a GPU-only backend installing on a worker is still a backend op on the controller, and was being misclassified as a model opThe fine-tune and quantization endpoints were initially assumed to be job-placement rather than discovery; they are not — both are
List...BackendsEndpointreturning "installed backends tagged X" for UI lists, i.e. identical semantics to/backends/available.Deliberately not done
No
compatible_nodesfield on the response —GalleryBackendstays the gallery YAML type, and per-node install already exists via/api/nodes/:id/backends/install.Known remaining gap (not addressed)
All three list endpoints also filter on
b.Installed, which on a distributed controller comes from the controller's own empty filesystem. Fine-tune and quantization lists may still be short in distributed mode until installed-state aggregation is cluster-aware. Worth a follow-up issue.Note for the merger
Please squash-merge. The two commits split accidentally: a
git stashduring the first commit's pre-commit gate reverted the staged source out from under it, so1d9b7fe19captured only the spec files and is red in isolation. The branch tip is correct and fully green. History was not rewritten to repair it, per this repo's rule requiring explicit approval for that.Testing
make lint→ 0 issues. Full pre-commit gate green.🤖 Generated with Claude Code