Skip to content

fix(distributed): backend discovery hid GPU-only backends behind the controller's capability#10947

Merged
mudler merged 2 commits into
masterfrom
fix/backends-available-node-capability
Jul 19, 2026
Merged

fix(distributed): backend discovery hid GPU-only backends behind the controller's capability#10947
mudler merged 2 commits into
masterfrom
fix/backends-available-node-capability

Conversation

@localai-bot

Copy link
Copy Markdown
Collaborator

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 a default (or cpu) key was dropped entirely. On a live cluster, GET /backends/available returned 275 entries with zero longcat/l4t entries: 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 works fine — DistributedBackendManager.InstallBackend ships 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 a default key.

  • PRESENT: llama-cpp, whisper, vibevoice, moss-tts-cpp, rfdetr, depth-anything, mlx, vllm (all have default)
  • ABSENT: longcat-video (nvidia keys only), vllm-omni (nvidia + amd only)

Root cause

ListAvailableBackendsEndpoint (core/http/endpoints/localai/backend.go:331) calls gallery.AvailableBackends(...)availableBackendsWithFilter(..., filterByCapability=true) (core/gallery/gallery.go:397), which filters on backend.IsCompatibleWith(systemState) using the controller's system state. For a meta backend that resolves to CapabilitiesMap[systemState.Capability(...)]; on a GPU-less controller getSystemCapabilities() returns default and Capability() falls back defaultcpu, 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 (capability column, 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's gpu_vendor is 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-pinned SystemState, 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/available
  • GET /api/fine-tuning/backends (finetune.go:279)
  • GET /api/quantization/backends (quantization.go:198)
  • /api/operations backend-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 op

The fine-tune and quantization endpoints were initially assumed to be job-placement rather than discovery; they are not — both are List...BackendsEndpoint returning "installed backends tagged X" for UI lists, i.e. identical semantics to /backends/available.

Deliberately not done

No compatible_nodes field on the response — GalleryBackend stays 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 stash during the first commit's pre-commit gate reverted the staged source out from under it, so 1d9b7fe19 captured 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

ok  core/gallery                  16.498s
ok  pkg/system                     0.010s
ok  core/http/routes               0.322s
ok  core/http/endpoints/localai   31.539s
ok  core/services/worker           0.268s
ok  core/services/nodes          186.939s   (7 new registry specs)

make lint → 0 issues. Full pre-commit gate green.


🤖 Generated with Claude Code

mudler added 2 commits July 18, 2026 22:39
…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
mudler enabled auto-merge (squash) July 19, 2026 07:01
@mudler
mudler merged commit b19afb1 into master Jul 19, 2026
67 checks passed
@mudler
mudler deleted the fix/backends-available-node-capability branch July 19, 2026 07:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants