Skip to content

Commit 0de1814

Browse files
Merge branch 'master' into fix/9287-backend-autodetect
2 parents ad54b54 + 84972cb commit 0de1814

609 files changed

Lines changed: 55796 additions & 5045 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/adding-backends.md

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ The build matrix is data-only YAML at `.github/backend-matrix.yml` (not inside `
3434

3535
**Without an entry here no image is ever built or pushed, and the gallery entry in `backend/index.yaml` will point at a tag that does not exist.** The `dockerfile:` field must point at `./backend/Dockerfile.<lang>` matching the language bucket from step 1 (e.g. `Dockerfile.python`, `Dockerfile.golang`, `Dockerfile.rust`). The `tag-suffix` must match the `uri:` in the corresponding `backend/index.yaml` image entry exactly.
3636

37-
**`scripts/changed-backends.js` registration — REQUIRED for any new dockerfile suffix.** This is the single most common omission, because it has no effect on the PR that adds the backend (when no prior path filter could catch it anyway) — it only breaks the *next* PR that touches your backend's directory, which then gets zero CI jobs and looks broken for unrelated reasons. Edit `scripts/changed-backends.js:inferBackendPath` and add a branch BEFORE the more-generic suffixes:
37+
**Path-filter registration — REQUIRED for any new dockerfile suffix.** This is the single most common omission, because it has no effect on the PR that adds the backend (when no prior path filter could catch it anyway) — it only breaks the *next* PR that touches your backend's directory, which then gets zero CI jobs and looks broken for unrelated reasons. Edit `scripts/lib/backend-filter.mjs:inferBackendPath` and add a branch BEFORE the more-generic suffixes:
3838

3939
```js
4040
if (item.dockerfile.endsWith("<your-dockerfile-suffix>")) {
@@ -54,7 +54,9 @@ for (const e of m.include.filter(e => e.backend === '<your-backend>')) {
5454
}"
5555
```
5656

57-
A quick way to find the right insertion point: `grep -n 'item.dockerfile.endsWith' scripts/changed-backends.js`.
57+
A quick way to find the right insertion point: `grep -n 'item.dockerfile.endsWith' scripts/lib/backend-filter.mjs`.
58+
59+
If your backend consumes a *shared* build input that lives outside its own directory (a new script under `scripts/build/`, a new file copied into every image), add a rule to `SHARED_BUILD_INPUTS` in the same file — the per-backend prefix match cannot see those, and a miss ships your change to no image at all. See `scripts/lib/backend-filter_test.mjs` for the pattern; `make test-ci-scripts` runs it.
5860

5961
**`bump_deps.yaml` registration — REQUIRED for any backend pinning an upstream commit.** If your backend's Makefile has a `*_VERSION?=<sha>` pin to a third-party repo, the daily auto-bump bot at `.github/workflows/bump_deps.yaml` won't notice it unless you register the backend in its matrix. The bot runs `.github/bump_deps.sh` which `grep`s for `^$VAR?=` in the Makefile you list — so the pin MUST live in the Makefile (not in a separate shell script). The bump for ds4 (#9761) had to walk this back because the original landed the pin in `prepare.sh`, which the bot can't see. Pattern (for `antirez/ds4`):
6062

@@ -115,7 +117,7 @@ Wiring a backend into `includeDarwin:` is more than the matrix entry:
115117

116118
1. **`includeDarwin:` entry** — `tag-suffix: "-metal-darwin-arm64-<backend>"`, `build-type: "metal"`, `lang: "go"` for go+ggml backends; omit `build-type` for the bespoke C++ ones (llama-cpp / ds4 / privacy-filter). Match an existing entry of the same shape.
117119
2. **`backend/index.yaml`** — add `metal:` to the backend's `capabilities` map (main and `-development`) and concrete `metal-<backend>` / `metal-<backend>-development` image entries pointing at the `-metal-darwin-arm64-<backend>` images.
118-
3. **C/C++ backends only** — add an `inferBackendPathDarwin` case in `scripts/changed-backends.js` returning `backend/cpp/<backend>/` (the generic fallthrough assumes `backend/<lang>/`, which is wrong for a C++ source tree driven with `lang: go`), and give `run.sh` a Darwin branch that exports `DYLD_LIBRARY_PATH` instead of `LD_LIBRARY_PATH`. If the build is bespoke (single `grpc-server` + dylib bundling), model it on `scripts/build/ds4-darwin.sh` and add a `backends/<backend>-darwin` make target plus a gated step in `.github/workflows/backend_build_darwin.yml`.
120+
3. **C/C++ backends only** — add an `inferBackendPathDarwin` case in `scripts/lib/backend-filter.mjs` returning `backend/cpp/<backend>/` (the generic fallthrough assumes `backend/<lang>/`, which is wrong for a C++ source tree driven with `lang: go`), and give `run.sh` a Darwin branch that exports `DYLD_LIBRARY_PATH` instead of `LD_LIBRARY_PATH`. If the build is bespoke (single `grpc-server` + dylib bundling), model it on `scripts/build/ds4-darwin.sh` and add a `backends/<backend>-darwin` make target plus a gated step in `.github/workflows/backend_build_darwin.yml`.
119121
4. **C++ proto gotcha** — if the backend compiles the generated gRPC/protobuf in a separate CMake target (e.g. `hw_grpc_proto`), that target must link `protobuf::libprotobuf` + `gRPC::grpc++` so the Homebrew include dirs propagate; otherwise macOS fails with `google/protobuf/runtime_version.h not found` (Linux hides this because apt headers sit in `/usr/include`).
120122

121123
The CI path filter only builds a backend on a PR when a file under its directory changes, so a darwin-only YAML edit builds nothing — touch a file under `backend/<lang>/<backend>/` (a one-line comment is enough) in the same PR.
@@ -216,6 +218,69 @@ docker-build-backends: ... docker-build-<backend-name>
216218
- If the backend is in `backend/python/<backend-name>/` but uses `.` as context in the workflow file, use `.` context
217219
- Check similar backends to determine the correct context
218220

221+
## Engine preference for gallery model variants
222+
223+
A gallery entry can declare `variants`, alternative builds of the same weights,
224+
and LocalAI picks one per host: it drops builds whose backend cannot run here or
225+
that do not fit memory, then ranks the survivors by **engine preference
226+
first, serving feature second, size third** (`SelectVariant` in
227+
`core/gallery/resolve_variant.go`).
228+
229+
Ask whether your backend should outrank another one on some hardware. If it
230+
should, add it to `engineNamePreferenceRules` in `pkg/system/capabilities.go`,
231+
best engine first for that capability:
232+
233+
```go
234+
{Nvidia, []string{engineVLLM, engineSGLang, engineLlamaCpp}},
235+
+ {Nvidia, []string{engineVLLM, engineSGLang, engineMyEngine, engineLlamaCpp}},
236+
```
237+
238+
That is the ENGINE NAME table, matched as a substring of a gallery entry's
239+
`backend:` value. Two sibling tables in the same file speak different
240+
vocabularies and are matched against different things:
241+
242+
| Table | Vocabulary | Matched against | Consumer |
243+
|-------|-----------|-----------------|----------|
244+
| `backendBuildTagPreferenceRules` | build tags (`cuda`, `rocm`, `metal`) | installed build directory names, as a substring | alias resolution in `ListSystemBackends` |
245+
| `engineNamePreferenceRules` | engine names (`vllm`, `llama-cpp`, `mlx`) | a gallery entry's `backend:`, as a substring | gallery variant ranking |
246+
| `servingFeaturePreferenceTokens` | serving features (`dflash`, `mtp`) | a gallery entry's `tags:`, compared whole and case-insensitively, and nothing else | gallery variant ranking, one rank below the engine |
247+
248+
**Putting a token in the wrong table matches nothing and does not error**: every
249+
candidate scores equal and the next sort key decides, so the preference silently
250+
stops existing. The block comment above all three tables spells the contract out.
251+
252+
The serving feature table is the odd one: it is not keyed by capability, because
253+
no hardware prefers a plain build over an equivalent faster build of the same
254+
weights. It reads a declared tag and nothing else. The entry name was the
255+
original signal and is gone: a naming convention is not a contract, and names
256+
are author-supplied free text where a short marker like `mtp` turns up inside
257+
unrelated words or on weights whose entry enables nothing.
258+
`overrides.options` was rejected for the mirror-image reason: `spec_type:` is
259+
llama.cpp's config vocabulary, whereas a cross-backend ranking decision must
260+
work the same for `ds4`'s `mtp_path:` and `sglang`'s `speculative_algorithm:`.
261+
262+
**If your backend can serve the same weights faster** (speculative decoding,
263+
multi-token prediction), say so in the docs for its gallery entries so curators
264+
tag them: the tagging rule and the per-backend evidence table live in
265+
[adding-gallery-models.md](adding-gallery-models.md). A backend never needs to
266+
appear in the token table itself; it ranks builds, not engines.
267+
268+
Leaving your backend out is a valid choice when no ordering can be justified for
269+
it. It then ranks below every known engine and selection falls back to size,
270+
which is the behaviour that predates preference.
271+
272+
**Leaving a whole capability out is not.** A missing row gives that host an
273+
empty preference list, so size alone decides among everything that survives the
274+
filters, and the filter will not save you: `IsBackendCompatible` derives hardware
275+
support from the engine NAME, so `vllm` and `sglang` carry no darwin, cuda, rocm
276+
or sycl token and are never dropped on a host with no GPU. That is why `default`
277+
(no usable accelerator, including a GPU under the 4 GiB VRAM floor) and
278+
`darwin-x86` both have rows putting `llama-cpp` first. Every capability
279+
`getSystemCapabilities()` can return needs a row unless every engine really is
280+
equally at home there. When you add one, enumerate the engines you are demoting
281+
rather than relying on them falling through unmatched: unmatched engines all tie
282+
with each other, so size decides among them.
283+
219284
## Documenting the backend (README + docs)
220285

221286
A backend is not "added" until it is discoverable. Update the user-facing docs:
@@ -243,7 +308,7 @@ After adding a new backend, verify:
243308

244309
- [ ] Backend directory structure is complete with all necessary files
245310
- [ ] Build configurations added to `.github/backend-matrix.yml` for all desired platforms (per-arch entries with `platform-tag` for multi-arch; `builder-base-image` for llama-cpp / ik-llama-cpp / turboquant)
246-
- [ ] **OS coverage considered**: added to `includeDarwin:` (macOS/Apple Silicon) if the backend can build there — with the `backend/index.yaml` `metal:` capability + `metal-<backend>` image entries, a `run.sh` Darwin/DYLD branch and `inferBackendPathDarwin` case for C++ backends — or the PR explains why an OS is unsupported. Do not ship Linux-only by default.
311+
- [ ] **OS coverage considered**: added to `includeDarwin:` (macOS/Apple Silicon) if the backend can build there — with the `backend/index.yaml` `metal:` capability + `metal-<backend>` image entries, a `run.sh` Darwin/DYLD branch and `inferBackendPathDarwin` case (in `scripts/lib/backend-filter.mjs`) for C++ backends — or the PR explains why an OS is unsupported. Do not ship Linux-only by default.
247312
- [ ] Meta definition added to `backend/index.yaml` in the `## metas` section
248313
- [ ] Image entries added to `backend/index.yaml` for all build variants (latest + development)
249314
- [ ] Tag suffixes match between workflow file and index.yaml
@@ -252,6 +317,7 @@ After adding a new backend, verify:
252317
- [ ] No Makefile syntax errors (check with linter)
253318
- [ ] Follows the same pattern as similar backends (e.g., if it's a transcription backend, follow `faster-whisper` pattern)
254319
- [ ] **`Load` validates its input and refuses models it can't serve.** When a model config has no explicit `backend:`, the model loader greedily probes *every* installed backend with the model's name and binds to the first `Load` that succeeds — an accept-anything `Load` will capture arbitrary LLMs (issue #9287). Backends that load a real artefact get this for free (the load fails); backends with no artefact must gate on the name: `opus` accepts only its own name (or none), `local-store` requires the `store.NamespacePrefix` namespace marker sent by `core/backend/stores.go`.
320+
- [ ] **Gallery variant ranking considered**: if this backend should be preferred over another on some hardware, it is listed in `engineNamePreferenceRules` (NOT `backendBuildTagPreferenceRules`, NOT `servingFeaturePreferenceTokens`) in `pkg/system/capabilities.go`. A missing entry silently ranks it last and lets the next sort key decide.
255321
- [ ] Documented: added to the category list in `docs/content/features/backends.md` (and any new endpoint/realtime capability documented under `docs/content/`)
256322
- [ ] If it is an in-house native C/C++/GGML engine, added to the maintained-engines table in the top-level `README.md`
257323

.agents/adding-gallery-models.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,108 @@ To add a variant (e.g., different quantization), use YAML merge:
9191
uri: huggingface://<gguf-org>/<gguf-repo>/<filename>-Q8_0.gguf
9292
```
9393

94+
## Offering several builds of one model (`variants`)
95+
96+
When the same model is published in more than one quantization, or is also
97+
servable by another engine, add each build as its own ordinary gallery entry and
98+
then point one of them at the others with `variants`:
99+
100+
```yaml
101+
- !!merge <<: *chatml
102+
name: "nanbeige4.1-3b-q4"
103+
# ... the usual urls / overrides / files for the Q4 build ...
104+
variants:
105+
- model: nanbeige4.1-3b-q8
106+
```
107+
108+
Rules:
109+
110+
- The declaring entry is a **complete, normal entry**. It keeps its own
111+
`files`/`overrides` and stays installable on every host and by every older
112+
LocalAI release, which simply ignore `variants`.
113+
- A variant references another gallery entry **by name**. That entry must exist
114+
and must not declare `variants` of its own.
115+
- **A referenced entry keeps its own gallery row by default.** It is hidden only
116+
in the collapsed listing (`collapse_variants=true`, which the web UI requests
117+
by default), where the declaring entry stands in for it. Searching there still
118+
matches the referenced entry and answers with the entry declaring it, so
119+
referencing an entry never makes it unfindable; turning the collapse off
120+
returns it under its own name.
121+
- **Order carries no meaning.** Do not try to encode a preference; write the
122+
list in whatever order reads best.
123+
- **A variant may be smaller than the declaring entry.** Offering a downgrade
124+
for small hosts is a normal shape: the declaring entry's own build competes
125+
like every other candidate, so a large host keeps the large build.
126+
- **Do not describe hardware.** At install time LocalAI drops variants whose
127+
backend cannot run on the host, then drops those that do not fit available
128+
memory. The declaring entry's own build is exempt from both filters, so
129+
selection always terminates on something installable. Sizes are measured live
130+
from the weights and cached, so nothing has to be written down.
131+
- **Engine preference outranks size.** Among the builds that survive the
132+
filters, the host's preferred engine wins first and only then does the larger
133+
footprint win. On NVIDIA a vLLM build beats a larger llama.cpp one; on Apple
134+
silicon an MLX build beats a larger GGUF one; on a host with no preference for
135+
either engine the larger build wins, since a bigger footprint is a higher
136+
quality quantization of the same weights. Predict what a user gets by asking
137+
which engine the host prefers before asking which build is biggest. The
138+
per-capability order lives in `engineNamePreferenceRules`
139+
(`pkg/system/capabilities.go`); see
140+
[adding-backends.md](adding-backends.md) for how a backend gets into it.
141+
- **Serving feature preference sits between engine and size.** Among builds on
142+
an equally preferred engine, one that speculates or predicts several tokens
143+
per step beats the plain build of the same weights, because it answers faster
144+
for the same output: a `dflash` build beats an `mtp` one, and either beats a
145+
plain build. The order lives in `servingFeaturePreferenceTokens`
146+
(`pkg/system/capabilities.go`) and is matched against the entry's `tags:` and
147+
**nothing else**: not the entry name, not `overrides.options`. See
148+
[the tagging rule](#the-dflash--mtp-tagging-rule) below. Engine deliberately
149+
outranks it: a serving feature makes the right engine faster, it does not make
150+
a wrong engine right. Fit still outranks both, so a drafter pairing (strictly
151+
larger than the plain build, since it ships a drafter alongside it) is dropped
152+
on a host too small for it before this order is ever consulted.
153+
- A variant is nothing but a name; there is no per-variant memory field. When
154+
the measured size for a build is wrong, correct it on the referenced entry by
155+
setting that entry's own `size:` (e.g. `size: "20GiB"`). The estimator prefers
156+
a declared size over its own guesswork, so the fix applies everywhere the size
157+
is shown or compared rather than only to variant selection.
158+
159+
Users can override the automatic choice with `variant` on `POST /models/apply`,
160+
`local-ai models install --variant`, or the `install_model` MCP tool. See
161+
`docs/content/features/model-gallery.md`.
162+
163+
The gallery lint specs live in `core/gallery`, so run that suite after adding a
164+
`variants` list.
165+
166+
### The `dflash` / `mtp` tagging rule
167+
168+
**Tag an entry `dflash` or `mtp` when the entry actually configures that
169+
feature. Variant ranking reads the tag and nothing else.**
170+
171+
Decide by looking at what the entry configures, in whatever vocabulary its
172+
backend uses:
173+
174+
| Backend | Configures the feature when it declares |
175+
|---------|------------------------------------------|
176+
| `llama-cpp` | `overrides.options` contains `spec_type:draft-dflash` or `spec_type:draft-mtp` |
177+
| `ds4` | `overrides.options` contains `mtp_path:` / `mtp_draft:` |
178+
| `sglang` | the referenced `gallery/*.yaml` sets `speculative_algorithm:` |
179+
180+
That check is curation-time only. `spec_type` is llama.cpp's config vocabulary,
181+
and a cross-backend ranking decision must not depend on one backend's option
182+
syntax, which is precisely why the ranker reads the tag instead of the options.
183+
184+
Two mistakes the rule exists to prevent:
185+
186+
- **Weights that carry the heads are not an entry that enables them.** The
187+
NVFP4 GGUF entries ship MTP-bearing weights but set only `use_jinja:true`, so
188+
they enable no speculative decoding and must NOT be tagged. Tagging them wins
189+
them the feature axis without being any faster.
190+
- **A name is not a declaration.** An entry whose name spells `-mtp` while
191+
configuring nothing gets no tag, and an entry that configures the feature is
192+
tagged even when its name says nothing (`hy3`, `glm-5.2`). Ranking never reads
193+
the name, so an untagged build that does enable the feature is simply ranked
194+
as plain rather than promoted on a marker nobody meant.
195+
94196
## Available template configs
95197

96198
Look at existing `.yaml` files in `gallery/` to find the right prompt template for your model architecture:

0 commit comments

Comments
 (0)