mirror of
https://github.com/mudler/LocalAI.git
synced 2026-08-05 04:44:02 -04:00
Compare commits
17 Commits
bot/issue-
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb444f917f | ||
|
|
a05a790021 | ||
|
|
9f62401fca | ||
|
|
4a5c5e51b7 | ||
|
|
c61b6f2286 | ||
|
|
0332e9729f | ||
|
|
a8d310573e | ||
|
|
144baaa809 | ||
|
|
86c2e9a273 | ||
|
|
89995d7535 | ||
|
|
1f4ec3bdf8 | ||
|
|
1466aaa9f7 | ||
|
|
e6712844ee | ||
|
|
b1d964ef7b | ||
|
|
0d342c61d8 | ||
|
|
4fec33966a | ||
|
|
8f52437c81 |
@@ -16,8 +16,7 @@ side (`pkg/oci/cosignverify` plus the gallery YAML).
|
||||
per-arch manifest before checking signatures.
|
||||
- **Storage:** Signatures are written as OCI 1.1 referrers
|
||||
(`--registry-referrers-mode=oci-1-1`) in the new Sigstore bundle format
|
||||
(current cosign releases do this by default; no `--new-bundle-format`
|
||||
flag). No `:sha256-<hex>.sig` tag clutter.
|
||||
(`--new-bundle-format`). No `:sha256-<hex>.sig` tag clutter.
|
||||
- **Consumer:** `pkg/oci/cosignverify` discovers the bundle via the
|
||||
referrers API, hands it to `sigstore-go`, and verifies it against the
|
||||
policy declared in the gallery YAML (`Gallery.Verification`).
|
||||
@@ -34,14 +33,15 @@ to sign. The job needs:
|
||||
|
||||
- `permissions: { id-token: write, contents: read }` at the job level so
|
||||
the runner can exchange its GitHub OIDC token for a Fulcio cert.
|
||||
- `sigstore/cosign-installer@v3` step (current cosign releases already
|
||||
default to the new bundle format).
|
||||
- `sigstore/cosign-installer@v3` step (the pinned cosign v2 release needs
|
||||
`--new-bundle-format` explicitly).
|
||||
- After each `docker buildx imagetools create`, resolve the resulting
|
||||
list digest with `docker buildx imagetools inspect <tag> --format
|
||||
'{{.Manifest.Digest}}'` and sign:
|
||||
|
||||
```sh
|
||||
cosign sign --yes --recursive \
|
||||
--new-bundle-format \
|
||||
--registry-referrers-mode=oci-1-1 \
|
||||
"${REGISTRY_REPO}@${DIGEST}"
|
||||
```
|
||||
@@ -70,7 +70,7 @@ entry (`backend/index.yaml`):
|
||||
url: github:mudler/LocalAI/backend/index.yaml@master
|
||||
verification:
|
||||
issuer: "https://token.actions.githubusercontent.com"
|
||||
identity_regex: "^https://github\\.com/mudler/LocalAI/\\.github/workflows/backend_merge\\.yml@refs/heads/master$"
|
||||
identity_regex: "^https://github\\.com/mudler/LocalAI/\\.github/workflows/backend_merge\\.yml@refs/(heads/master|tags/.+)$"
|
||||
# Optional revocation cutoff; advance during incident response.
|
||||
# not_before: "2026-06-01T00:00:00Z"
|
||||
```
|
||||
|
||||
6
.github/workflows/backend_merge.yml
vendored
6
.github/workflows/backend_merge.yml
vendored
@@ -71,8 +71,8 @@ jobs:
|
||||
|
||||
# cosign signs each pushed manifest list with --recursive so the
|
||||
# index and every per-arch entry get an attached Sigstore bundle.
|
||||
# Recent cosign releases always emit the new bundle format, so
|
||||
# there's no extra CLI flag to opt into it.
|
||||
# The pinned cosign v2 release needs --new-bundle-format explicitly;
|
||||
# the verifier only consumes OCI 1.1 Sigstore bundle referrers.
|
||||
- name: Install cosign
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: sigstore/cosign-installer@v3
|
||||
@@ -159,6 +159,7 @@ jobs:
|
||||
# manifest before checking signatures need the per-arch
|
||||
# signatures, not just the list-level one.
|
||||
cosign sign --yes --recursive \
|
||||
--new-bundle-format \
|
||||
--registry-referrers-mode=oci-1-1 \
|
||||
"quay.io/go-skynet/local-ai-backends@${digest}"
|
||||
|
||||
@@ -185,6 +186,7 @@ jobs:
|
||||
' <<< "$DOCKER_METADATA_OUTPUT_JSON")
|
||||
digest=$(docker buildx imagetools inspect "$first_tag" --format '{{.Manifest.Digest}}')
|
||||
cosign sign --yes --recursive \
|
||||
--new-bundle-format \
|
||||
--registry-referrers-mode=oci-1-1 \
|
||||
"localai/localai-backends@${digest}"
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
# recipe is a make target (not a prepare.sh) so 'make purge && make' is a clean
|
||||
# rebuild and so the bump bot can see the pin.
|
||||
|
||||
AUDIO_CPP_VERSION?=4e3aea2fd99aeaa5924e71c51eb2793846045332
|
||||
AUDIO_CPP_VERSION?=238ab6a9e321c17de8e120559f57efeedaeb1345
|
||||
AUDIO_CPP_REPO?=https://github.com/0xShug0/audio.cpp
|
||||
|
||||
CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# ds4 backend Makefile.
|
||||
#
|
||||
# Upstream pin lives below as DS4_VERSION?=b7e9f0091139999b6c070a57590c447c5741da5c
|
||||
# Upstream pin lives below as DS4_VERSION?=6747e7718dd08f00b680d0c16231f2d59ec3747e
|
||||
# (.github/bump_deps.sh) can find and update it - matches the
|
||||
# llama-cpp / ik-llama-cpp / turboquant convention.
|
||||
|
||||
DS4_VERSION?=b7e9f0091139999b6c070a57590c447c5741da5c
|
||||
DS4_VERSION?=6747e7718dd08f00b680d0c16231f2d59ec3747e
|
||||
DS4_REPO?=https://github.com/antirez/ds4
|
||||
|
||||
CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
IK_LLAMA_VERSION?=60389410a1ff01f9d37dcc6261db33b3183bdea2
|
||||
IK_LLAMA_VERSION?=6b55d2c7504f482e7c8ec6cbf22a19f3778c522b
|
||||
LLAMA_REPO?=https://github.com/ikawrakow/ik_llama.cpp
|
||||
|
||||
CMAKE_ARGS?=
|
||||
|
||||
@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
|
||||
|
||||
# CrispASR version (release tag)
|
||||
CRISPASR_REPO?=https://github.com/CrispStrobe/CrispASR
|
||||
CRISPASR_VERSION?=fe3caf8e363b27572dbdd1a9d37083f25e6decda
|
||||
CRISPASR_VERSION?=ec730908a418b6032f9e69ded6186d3f042a7747
|
||||
SO_TARGET?=libgocrispasr.so
|
||||
|
||||
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF
|
||||
|
||||
@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
|
||||
|
||||
# stablediffusion.cpp (ggml)
|
||||
STABLEDIFFUSION_GGML_REPO?=https://github.com/leejet/stable-diffusion.cpp
|
||||
STABLEDIFFUSION_GGML_VERSION?=db99efdd6d2a43c7937fd55b3359206c680a75b0
|
||||
STABLEDIFFUSION_GGML_VERSION?=ea7f0c87cfe4c673263b4c201c596c7f1cbe2528
|
||||
|
||||
CMAKE_ARGS+=-DGGML_MAX_NAME=128
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ JOBS?=$(shell nproc --ignore=1 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || e
|
||||
|
||||
# vllm.cpp version
|
||||
VLLM_CPP_REPO?=https://github.com/mudler/vllm.cpp
|
||||
VLLM_CPP_VERSION?=9d1fad3cde0acb95eb0bb0a1025f40a0eb614147
|
||||
VLLM_CPP_VERSION?=0757cac231ecd571a83c4fd2f50805c9251fc225
|
||||
|
||||
# MLX GEMM provider (darwin/metal only; see the metal branch below for why).
|
||||
# Consumed as the prebuilt pip wheel: building MLX from source needs `xcrun
|
||||
|
||||
@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
|
||||
|
||||
# whisper.cpp version
|
||||
WHISPER_REPO?=https://github.com/ggml-org/whisper.cpp
|
||||
WHISPER_CPP_VERSION?=64d57d3df5c8dacee098577257edcaa154bf5ef3
|
||||
WHISPER_CPP_VERSION?=306c88f4d1286aec1bf96e544632897886af5501
|
||||
SO_TARGET?=libgowhisper.so
|
||||
|
||||
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF
|
||||
|
||||
@@ -193,12 +193,22 @@
|
||||
alias: "vllm-cpp"
|
||||
license: apache-2.0
|
||||
description: |
|
||||
vllm.cpp is a from-scratch C++20 port of vLLM created and maintained by the LocalAI team.
|
||||
It mirrors vLLM's V1 architecture (paged KV cache, continuous batching, prefix caching,
|
||||
scheduler, sampler) on a portable tensor runtime with no Python, PyTorch or ggml at
|
||||
inference time. It loads Hugging Face safetensors and GGUF checkpoints, supports
|
||||
structured output (JSON schema / regex / choice / GBNF grammar) enforced in-engine,
|
||||
and runs on CPU, NVIDIA CUDA (Blackwell-family), Apple Metal and Vulkan.
|
||||
ALPHA development builds. Try it, but llama-cpp stays the recommendation for
|
||||
production use.
|
||||
|
||||
vllm.cpp is an Apache-2.0 C++20 inference engine maintained by the LocalAI team,
|
||||
developed in its own repository and usable without LocalAI. It began as a port of
|
||||
vLLM and keeps vLLM as its reference implementation, checking output against it and
|
||||
benchmarking against it, while growing a featureset of its own. It implements vLLM's
|
||||
V1 architecture (paged KV cache, continuous batching, prefix caching, scheduler,
|
||||
sampler) on a portable tensor runtime with no Python, PyTorch or ggml at inference
|
||||
time. It loads GGUF as well as Hugging Face safetensors, supports structured output
|
||||
(JSON schema / regex / choice / GBNF grammar) enforced in-engine, ships speculative
|
||||
decoding and KV offload, and runs on CPU, NVIDIA CUDA (Blackwell-family), Apple
|
||||
Metal and Vulkan.
|
||||
|
||||
The project is expected to be renamed as it diverges further from vLLM; the new
|
||||
name is still to be decided.
|
||||
urls:
|
||||
- https://github.com/mudler/vllm.cpp
|
||||
tags:
|
||||
|
||||
@@ -8,13 +8,8 @@ run: fish-speech
|
||||
bash run.sh
|
||||
@echo "fish-speech run."
|
||||
|
||||
.PHONY: test-unit
|
||||
test-unit:
|
||||
python3 -m unittest -v prepare_upstream_test.py
|
||||
bash run_test.sh
|
||||
|
||||
.PHONY: test
|
||||
test: fish-speech test-unit
|
||||
test: fish-speech
|
||||
@echo "Testing fish-speech..."
|
||||
bash test.sh
|
||||
@echo "fish-speech tested."
|
||||
|
||||
@@ -44,13 +44,6 @@ fi
|
||||
# It requires native portaudio libs which aren't available on all build environments.
|
||||
sed -i.bak '/"pyaudio"/d' "${FISH_SPEECH_DIR}/pyproject.toml"
|
||||
|
||||
# CUDA 13 has no torch 2.8 wheels, so fish-speech's exact upstream pin would
|
||||
# make pip select the CPU-only aarch64 wheel from PyPI. Prepare the cloned tree
|
||||
# before resolving it, and use soundfile for reference audio because torchcodec
|
||||
# does not publish Linux aarch64 wheels.
|
||||
python3 "${backend_dir}/prepare_upstream.py" "${FISH_SPEECH_DIR}" \
|
||||
--cuda-major "${CUDA_MAJOR_VERSION:-}"
|
||||
|
||||
# Install fish-speech deps from source (without the package itself since we use PYTHONPATH)
|
||||
ensureVenv
|
||||
if [ "x${USE_PIP}" == "xtrue" ]; then
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
TORCH_28 = '"torch==2.8.0"'
|
||||
TORCH_29 = '"torch==2.9.1"'
|
||||
TORCHAUDIO_28 = '"torchaudio==2.8.0"'
|
||||
TORCHAUDIO_29 = '"torchaudio==2.9.1"'
|
||||
TORCHAUDIO_LOAD = (
|
||||
" waveform, original_sr = "
|
||||
"torchaudio.load(reference_audio, backend=self.backend)"
|
||||
)
|
||||
SOUNDFILE_LOAD = "\n".join(
|
||||
(
|
||||
" import soundfile as _sf",
|
||||
" import torch as _torch",
|
||||
"",
|
||||
" data, original_sr = _sf.read(",
|
||||
' reference_audio, dtype="float32", always_2d=True',
|
||||
" )",
|
||||
" waveform = _torch.from_numpy(data.T.copy())",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def patch_cuda13_dependencies(pyproject: Path) -> None:
|
||||
content = pyproject.read_text()
|
||||
if (
|
||||
TORCH_28 not in content
|
||||
and TORCHAUDIO_28 not in content
|
||||
and TORCH_29 in content
|
||||
and TORCHAUDIO_29 in content
|
||||
):
|
||||
return
|
||||
if TORCH_28 not in content or TORCHAUDIO_28 not in content:
|
||||
raise RuntimeError("fish-speech's torch 2.8 dependency pins have changed")
|
||||
|
||||
content = content.replace(TORCH_28, TORCH_29)
|
||||
content = content.replace(TORCHAUDIO_28, TORCHAUDIO_29)
|
||||
pyproject.write_text(content)
|
||||
|
||||
|
||||
def patch_reference_loader(loader: Path) -> None:
|
||||
content = loader.read_text()
|
||||
if TORCHAUDIO_LOAD not in content and content.count(SOUNDFILE_LOAD) == 1:
|
||||
return
|
||||
if content.count(TORCHAUDIO_LOAD) != 1:
|
||||
raise RuntimeError("fish-speech's torchaudio.load call has changed")
|
||||
|
||||
loader.write_text(content.replace(TORCHAUDIO_LOAD, SOUNDFILE_LOAD))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("source", type=Path)
|
||||
parser.add_argument("--cuda-major")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.cuda_major == "13":
|
||||
patch_cuda13_dependencies(args.source / "pyproject.toml")
|
||||
patch_reference_loader(
|
||||
args.source / "fish_speech/inference_engine/reference_loader.py"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,136 +0,0 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MODULE_PATH = Path(__file__).with_name("prepare_upstream.py")
|
||||
|
||||
|
||||
def load_prepare_upstream():
|
||||
if not MODULE_PATH.exists():
|
||||
raise AssertionError("prepare_upstream.py is missing")
|
||||
spec = importlib.util.spec_from_file_location("prepare_upstream", MODULE_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class FakeAudioData:
|
||||
@property
|
||||
def T(self):
|
||||
return self
|
||||
|
||||
def copy(self):
|
||||
return "channels-first"
|
||||
|
||||
|
||||
class PrepareUpstreamTests(unittest.TestCase):
|
||||
def test_cuda13_dependencies_follow_available_pytorch_wheels(self):
|
||||
prepare_upstream = load_prepare_upstream()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
pyproject = Path(tmp) / "pyproject.toml"
|
||||
pyproject.write_text(
|
||||
'dependencies = [\n "torch==2.8.0",\n "torchaudio==2.8.0",\n]\n'
|
||||
'stable = [\n "torch==2.8.0",\n "torchaudio",\n]\n'
|
||||
)
|
||||
|
||||
prepare_upstream.patch_cuda13_dependencies(pyproject)
|
||||
|
||||
self.assertEqual(
|
||||
pyproject.read_text(),
|
||||
'dependencies = [\n "torch==2.9.1",\n "torchaudio==2.9.1",\n]\n'
|
||||
'stable = [\n "torch==2.9.1",\n "torchaudio",\n]\n',
|
||||
)
|
||||
|
||||
def test_reference_audio_uses_soundfile_without_torchcodec(self):
|
||||
prepare_upstream = load_prepare_upstream()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
loader = Path(tmp) / "reference_loader.py"
|
||||
loader.write_text(
|
||||
"class ReferenceLoader:\n"
|
||||
" def load_audio(self, reference_audio):\n"
|
||||
" waveform, original_sr = torchaudio.load(reference_audio, backend=self.backend)\n"
|
||||
" return waveform, original_sr\n"
|
||||
)
|
||||
prepare_upstream.patch_reference_loader(loader)
|
||||
|
||||
calls = []
|
||||
fake_soundfile = types.SimpleNamespace(
|
||||
read=lambda source, **kwargs: (
|
||||
calls.append((source, kwargs)) or FakeAudioData(),
|
||||
24000,
|
||||
)
|
||||
)
|
||||
fake_torch = types.SimpleNamespace(
|
||||
from_numpy=lambda data: ("tensor", data),
|
||||
)
|
||||
previous_soundfile = sys.modules.get("soundfile")
|
||||
previous_torch = sys.modules.get("torch")
|
||||
sys.modules["soundfile"] = fake_soundfile
|
||||
sys.modules["torch"] = fake_torch
|
||||
try:
|
||||
namespace = {"torchaudio": None}
|
||||
exec(compile(loader.read_text(), str(loader), "exec"), namespace)
|
||||
instance = namespace["ReferenceLoader"]()
|
||||
instance.backend = "soundfile"
|
||||
|
||||
waveform, sample_rate = instance.load_audio("voice.wav")
|
||||
finally:
|
||||
if previous_soundfile is None:
|
||||
del sys.modules["soundfile"]
|
||||
else:
|
||||
sys.modules["soundfile"] = previous_soundfile
|
||||
if previous_torch is None:
|
||||
del sys.modules["torch"]
|
||||
else:
|
||||
sys.modules["torch"] = previous_torch
|
||||
|
||||
self.assertEqual(waveform, ("tensor", "channels-first"))
|
||||
self.assertEqual(sample_rate, 24000)
|
||||
self.assertEqual(
|
||||
calls,
|
||||
[("voice.wav", {"dtype": "float32", "always_2d": True})],
|
||||
)
|
||||
|
||||
def test_reference_loader_drift_fails_the_build(self):
|
||||
prepare_upstream = load_prepare_upstream()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
loader = Path(tmp) / "reference_loader.py"
|
||||
loader.write_text("def load_audio():\n pass\n")
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "torchaudio.load call"):
|
||||
prepare_upstream.patch_reference_loader(loader)
|
||||
|
||||
def test_preparation_can_be_repeated(self):
|
||||
prepare_upstream = load_prepare_upstream()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
pyproject = Path(tmp) / "pyproject.toml"
|
||||
pyproject.write_text(
|
||||
'dependencies = ["torch==2.8.0", "torchaudio==2.8.0"]\n'
|
||||
)
|
||||
loader = Path(tmp) / "reference_loader.py"
|
||||
loader.write_text(
|
||||
"def load_audio(reference_audio):\n"
|
||||
" waveform, original_sr = torchaudio.load(reference_audio, backend=self.backend)\n"
|
||||
)
|
||||
|
||||
prepare_upstream.patch_cuda13_dependencies(pyproject)
|
||||
prepare_upstream.patch_reference_loader(loader)
|
||||
try:
|
||||
prepare_upstream.patch_cuda13_dependencies(pyproject)
|
||||
prepare_upstream.patch_reference_loader(loader)
|
||||
except RuntimeError as err:
|
||||
self.fail(f"preparation is not idempotent: {err}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,3 +1,3 @@
|
||||
--extra-index-url https://download.pytorch.org/whl/cu130
|
||||
torch==2.9.1+cu130
|
||||
torchaudio==2.9.1
|
||||
torch
|
||||
torchaudio
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
--extra-index-url https://download.pytorch.org/whl/cu130
|
||||
torch==2.9.1+cu130
|
||||
torchaudio==2.9.1
|
||||
torch
|
||||
torchaudio
|
||||
|
||||
@@ -6,8 +6,4 @@ else
|
||||
source $backend_dir/../common/libbackend.sh
|
||||
fi
|
||||
|
||||
# Editable installs record their build-time absolute source path, which becomes
|
||||
# stale when the backend is relocated under /backends at install time.
|
||||
export PYTHONPATH="${EDIR}/fish-speech-src${PYTHONPATH:+:${PYTHONPATH}}"
|
||||
|
||||
startBackend "$@"
|
||||
startBackend $@
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/bin/bash
|
||||
# SPDX-License-Identifier: MIT
|
||||
set -euo pipefail
|
||||
|
||||
backend_dir=$(cd "$(dirname "$0")" && pwd)
|
||||
work=$(mktemp -d)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
mkdir -p "$work/backend/common" "$work/backend/fish-speech-src"
|
||||
cp "$backend_dir/run.sh" "$work/backend/run.sh"
|
||||
|
||||
cat > "$work/backend/common/libbackend.sh" <<'EOF'
|
||||
EDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
startBackend() {
|
||||
printf '%s\n' "$PYTHONPATH"
|
||||
}
|
||||
EOF
|
||||
|
||||
actual=$(PYTHONPATH=/existing/path bash "$work/backend/run.sh")
|
||||
expected="$work/backend/fish-speech-src:/existing/path"
|
||||
|
||||
if [ "$actual" != "$expected" ]; then
|
||||
printf 'expected PYTHONPATH %s, got %s\n' "$expected" "$actual" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: relocated fish-speech source is importable"
|
||||
@@ -60,6 +60,7 @@ type APIExchange struct {
|
||||
}
|
||||
|
||||
var traceBuffer *circularbuffer.Queue[APIExchange]
|
||||
var inFlightTraces = make(map[string]APIExchange)
|
||||
var mu sync.Mutex
|
||||
var logChan = make(chan traceCommand, 100)
|
||||
var traceIDSeq atomic.Uint64
|
||||
@@ -126,16 +127,17 @@ func initializeTracing(dataPath string, maxItems int) {
|
||||
continue
|
||||
}
|
||||
exchange := *command.exchange
|
||||
mu.Lock()
|
||||
delete(inFlightTraces, exchange.ID)
|
||||
if traceBuffer != nil {
|
||||
traceBuffer.Enqueue(exchange)
|
||||
}
|
||||
mu.Unlock()
|
||||
if command.store != nil {
|
||||
if err := command.store.Append(exchange.ID, exchange); err != nil {
|
||||
xlog.Warn("Failed to persist API trace", "error", err)
|
||||
}
|
||||
}
|
||||
mu.Lock()
|
||||
if traceBuffer != nil {
|
||||
traceBuffer.Enqueue(exchange)
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
})
|
||||
@@ -261,6 +263,38 @@ func TraceMiddleware(app *application.Application) echo.MiddlewareFunc {
|
||||
// tens of MB, which then locks the admin Traces UI fetching the
|
||||
// JSON dump faster than the 5s auto-refresh.
|
||||
maxBodyBytes := app.ApplicationConfig().TracingMaxBodyBytes
|
||||
requestHeaders := redactSensitiveHeaders(c.Request().Header)
|
||||
requestBody, requestTruncated := truncateForTrace(body, maxBodyBytes)
|
||||
exchange := APIExchange{
|
||||
ID: nextTraceID(),
|
||||
Timestamp: startTime,
|
||||
ClientIP: c.RealIP(),
|
||||
UserAgent: c.Request().UserAgent(),
|
||||
Request: APIExchangeRequest{
|
||||
Method: c.Request().Method,
|
||||
Path: c.Path(),
|
||||
Headers: &requestHeaders,
|
||||
Body: &requestBody,
|
||||
BodyTruncated: requestTruncated,
|
||||
BodyBytes: len(body),
|
||||
},
|
||||
}
|
||||
if user := auth.GetUser(c); user != nil {
|
||||
exchange.UserID = user.ID
|
||||
exchange.UserName = user.Name
|
||||
}
|
||||
mu.Lock()
|
||||
inFlightTraces[exchange.ID] = exchange
|
||||
mu.Unlock()
|
||||
queued := false
|
||||
defer func() {
|
||||
if queued {
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
delete(inFlightTraces, exchange.ID)
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
// Wrap response writer to capture body
|
||||
resBody := new(bytes.Buffer)
|
||||
@@ -287,47 +321,27 @@ func TraceMiddleware(app *application.Application) echo.MiddlewareFunc {
|
||||
// the trace endpoint is admin-only but the buffer is also reachable
|
||||
// via any heap-dump-style introspection, and tokens shouldn't
|
||||
// outlive the request that carried them.
|
||||
requestHeaders := redactSensitiveHeaders(c.Request().Header)
|
||||
requestBody, requestTruncated := truncateForTrace(body, maxBodyBytes)
|
||||
responseHeaders := redactSensitiveHeaders(c.Response().Header())
|
||||
responseBody := make([]byte, resBody.Len())
|
||||
copy(responseBody, resBody.Bytes())
|
||||
exchange := APIExchange{
|
||||
ID: nextTraceID(),
|
||||
Timestamp: startTime,
|
||||
Duration: time.Since(startTime),
|
||||
ClientIP: c.RealIP(),
|
||||
UserAgent: c.Request().UserAgent(),
|
||||
Request: APIExchangeRequest{
|
||||
Method: c.Request().Method,
|
||||
Path: c.Path(),
|
||||
Headers: &requestHeaders,
|
||||
Body: &requestBody,
|
||||
BodyTruncated: requestTruncated,
|
||||
BodyBytes: len(body),
|
||||
},
|
||||
Response: APIExchangeResponse{
|
||||
Status: status,
|
||||
Headers: &responseHeaders,
|
||||
Body: &responseBody,
|
||||
BodyTruncated: mw.truncated,
|
||||
BodyBytes: mw.totalBytes,
|
||||
},
|
||||
exchange.Duration = time.Since(startTime)
|
||||
exchange.Response = APIExchangeResponse{
|
||||
Status: status,
|
||||
Headers: &responseHeaders,
|
||||
Body: &responseBody,
|
||||
BodyTruncated: mw.truncated,
|
||||
BodyBytes: mw.totalBytes,
|
||||
}
|
||||
if handlerErr != nil {
|
||||
exchange.Error = handlerErr.Error()
|
||||
}
|
||||
|
||||
if user := auth.GetUser(c); user != nil {
|
||||
exchange.UserID = user.ID
|
||||
exchange.UserName = user.Name
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
store := traceStore
|
||||
mu.Unlock()
|
||||
select {
|
||||
case logChan <- traceCommand{exchange: &exchange, store: store}:
|
||||
queued = true
|
||||
default:
|
||||
xlog.Warn("Trace channel full, dropping trace")
|
||||
}
|
||||
@@ -345,6 +359,10 @@ func GetTraces() []APIExchange {
|
||||
return []APIExchange{}
|
||||
}
|
||||
traces := traceBuffer.Values()
|
||||
for _, exchange := range inFlightTraces {
|
||||
exchange.Duration = time.Since(exchange.Timestamp)
|
||||
traces = append(traces, exchange)
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
slices.SortFunc(traces, func(a, b APIExchange) int {
|
||||
|
||||
108
core/http/middleware/trace_live_test.go
Normal file
108
core/http/middleware/trace_live_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mudler/LocalAI/core/application"
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
"github.com/mudler/LocalAI/pkg/system"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("live API traces", func() {
|
||||
newApp := func(root string) *application.Application {
|
||||
app, err := application.New(
|
||||
config.EnableTracing,
|
||||
config.WithDataPath(root),
|
||||
config.WithDisableLocalAIAssistant(true),
|
||||
config.WithDisableStats(true),
|
||||
config.WithSystemState(&system.SystemState{
|
||||
Model: system.Model{ModelsPath: root},
|
||||
Backend: system.Backend{BackendsPath: root},
|
||||
}),
|
||||
)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
DeferCleanup(func() { Expect(app.Shutdown()).To(Succeed()) })
|
||||
ClearTraces()
|
||||
return app
|
||||
}
|
||||
|
||||
It("lists a request while its handler is still running", func() {
|
||||
root := GinkgoT().TempDir()
|
||||
app := newApp(root)
|
||||
|
||||
started := make(chan struct{})
|
||||
release := make(chan struct{})
|
||||
DeferCleanup(func() {
|
||||
select {
|
||||
case <-release:
|
||||
default:
|
||||
close(release)
|
||||
}
|
||||
})
|
||||
handler := TraceMiddleware(app)(func(c echo.Context) error {
|
||||
close(started)
|
||||
<-release
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
})
|
||||
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(http.MethodPost, "/slow", http.NoBody)
|
||||
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
||||
rec := httptest.NewRecorder()
|
||||
ctx := e.NewContext(req, rec)
|
||||
ctx.SetPath("/slow")
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- handler(ctx)
|
||||
}()
|
||||
<-started
|
||||
|
||||
var running APIExchange
|
||||
Eventually(func() bool {
|
||||
traces := GetTraces()
|
||||
if len(traces) != 1 {
|
||||
return false
|
||||
}
|
||||
running = traces[0]
|
||||
return running.Request.Path == "/slow"
|
||||
}).Should(BeTrue())
|
||||
Expect(running.Response.Status).To(Equal(0))
|
||||
Expect(running.Duration).To(BeNumerically(">", 0))
|
||||
|
||||
close(release)
|
||||
Expect(<-done).To(Succeed())
|
||||
Eventually(func() []APIExchange { return GetTraces() }).Should(ConsistOf(
|
||||
And(
|
||||
HaveField("ID", running.ID),
|
||||
HaveField("Response.Status", http.StatusNoContent),
|
||||
HaveField("Duration", BeNumerically(">", time.Duration(0))),
|
||||
),
|
||||
))
|
||||
})
|
||||
|
||||
It("removes an in-flight trace when the handler panics", func() {
|
||||
app := newApp(GinkgoT().TempDir())
|
||||
handler := TraceMiddleware(app)(func(echo.Context) error {
|
||||
panic("handler panic")
|
||||
})
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(http.MethodPost, "/panic", http.NoBody)
|
||||
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
||||
ctx := e.NewContext(req, httptest.NewRecorder())
|
||||
ctx.SetPath("/panic")
|
||||
|
||||
func() {
|
||||
defer func() { _ = recover() }()
|
||||
_ = handler(ctx)
|
||||
}()
|
||||
|
||||
Expect(GetTraces()).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
22
core/http/react-ui/e2e/traces-live.spec.js
Normal file
22
core/http/react-ui/e2e/traces-live.spec.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { test, expect } from './coverage-fixtures.js'
|
||||
|
||||
test('marks an API trace with no response status as in progress', async ({ page }) => {
|
||||
await page.route('**/api/traces?*', route => route.fulfill({
|
||||
json: [{
|
||||
id: 'running-1',
|
||||
timestamp: '2026-08-05T02:00:00Z',
|
||||
duration: 2_000_000_000,
|
||||
request: { method: 'POST', path: '/v1/chat/completions' },
|
||||
response: { status: 0 },
|
||||
}],
|
||||
headers: { 'X-Total-Count': '1' },
|
||||
}))
|
||||
await page.route('**/api/backend-traces?*', route => route.fulfill({ json: [] }))
|
||||
|
||||
await page.goto('/app/traces')
|
||||
|
||||
const row = page.locator('tbody tr').filter({ hasText: '/v1/chat/completions' })
|
||||
await expect(row.getByText('Running', { exact: true })).toBeVisible()
|
||||
await expect(row.locator('[title="In progress"]')).toBeVisible()
|
||||
await expect(row.locator('.fa-check-circle')).toHaveCount(0)
|
||||
})
|
||||
@@ -664,10 +664,16 @@ export default function Traces() {
|
||||
<td><span className="badge badge-info">{trace.request?.method || '-'}</span></td>
|
||||
<td className="text-mono text-sm">{trace.request?.path || '-'}</td>
|
||||
<td className="text-sub cell-clip" title={trace.user_name || trace.user_id || ''}>{trace.user_name || trace.user_id || '-'}</td>
|
||||
<td><span className={`badge ${(trace.response?.status || 0) < 400 ? 'badge-success' : 'badge-error'}`}>{trace.response?.status || '-'}</span></td>
|
||||
<td>
|
||||
{trace.response?.status === 0
|
||||
? <span className="badge badge-info">Running</span>
|
||||
: <span className={`badge ${trace.response.status < 400 ? 'badge-success' : 'badge-error'}`}>{trace.response.status}</span>}
|
||||
</td>
|
||||
<td><LatencyCell ns={trace.duration} max={slowestTrace} /></td>
|
||||
<td className="text-center">
|
||||
{trace.error
|
||||
{trace.response?.status === 0
|
||||
? <i className="fas fa-spinner fa-spin text-primary" title="In progress" />
|
||||
: trace.error
|
||||
? <i className="fas fa-times-circle text-error" title={trace.error} />
|
||||
: <i className="fas fa-check-circle text-success" />}
|
||||
</td>
|
||||
|
||||
@@ -72,6 +72,44 @@ tags:
|
||||
- "text-generation"
|
||||
```
|
||||
|
||||
### Verifying OCI Backends
|
||||
|
||||
Backend galleries can require keyless Sigstore signatures for every OCI image
|
||||
they provide. Add a `verification` policy to the gallery configuration, then
|
||||
enable strict integrity mode:
|
||||
|
||||
```bash
|
||||
export LOCALAI_BACKEND_GALLERIES='[{"name":"localai","url":"github:mudler/LocalAI/backend/index.yaml@master","verification":{"issuer":"https://token.actions.githubusercontent.com","identity_regex":"^https://github\\.com/mudler/LocalAI/\\.github/workflows/backend_merge\\.yml@refs/(heads/master|tags/.+)$"}}]'
|
||||
export LOCALAI_REQUIRE_BACKEND_INTEGRITY=1
|
||||
local-ai run
|
||||
```
|
||||
|
||||
The policy pins the Fulcio issuer and the GitHub Actions workflow identity that
|
||||
signed the image. The identity expression covers development images produced
|
||||
from `master` and release images produced from tags. Use a narrower expression
|
||||
if your deployment only accepts one release channel.
|
||||
|
||||
Without strict mode, an OCI gallery without a verification policy installs
|
||||
with a warning. With strict mode, LocalAI refuses galleries without a policy,
|
||||
images without a compatible Sigstore bundle, and signatures that do not match
|
||||
the configured identity. Existing images published before bundle signing was
|
||||
enabled must be rebuilt or re-signed before strict deployments can install
|
||||
them.
|
||||
|
||||
An optional `not_before` RFC3339 value revokes signatures logged before that
|
||||
time. Advance it after a signing-workflow compromise, then rebuild or re-sign
|
||||
the trusted images:
|
||||
|
||||
```json
|
||||
{
|
||||
"verification": {
|
||||
"issuer": "https://token.actions.githubusercontent.com",
|
||||
"identity_regex": "^https://github\\.com/mudler/LocalAI/\\.github/workflows/backend_merge\\.yml@refs/(heads/master|tags/.+)$",
|
||||
"not_before": "2026-08-05T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pre-installing Backends
|
||||
|
||||
You can pre-install backends when starting LocalAI using the `LOCALAI_EXTERNAL_BACKENDS` environment variable:
|
||||
|
||||
@@ -9,6 +9,11 @@ LocalAI can retain recent API exchanges and backend operations for inspection
|
||||
on the **Traces** page in the management interface. Enable tracing in runtime
|
||||
settings or with the existing tracing configuration.
|
||||
|
||||
API requests appear while they are still running. Their elapsed duration
|
||||
updates when the page refreshes, and the result column marks them as in
|
||||
progress until the response completes. In-flight requests live only in memory;
|
||||
the completed exchange is what LocalAI adds to the bounded, persistent history.
|
||||
|
||||
API and backend trace histories are persisted in separate directories below
|
||||
the configured data path. They are restored after a clean service restart,
|
||||
whether or not authentication is enabled.
|
||||
|
||||
@@ -785,35 +785,18 @@
|
||||
- name: "qwen3.6-35b-a3b-uncensored-genesis-hermes-v6"
|
||||
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
||||
urls:
|
||||
- https://huggingface.co/HauhauCS/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive
|
||||
- https://huggingface.co/LuffyTheFox/Qwen3.6-35B-A3B-Uncensored-Genesis-Hermes-V6-GGUF
|
||||
description: |
|
||||
# Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive
|
||||
Qwen3.6-35B-A3B Uncensored Genesis Hermes V6 is LuffyTheFox's multimodal,
|
||||
agentic derivative of HauhauCS's uncensored Qwen3.6-35B-A3B model. It
|
||||
combines Genesis tensor calibration with Hermes function-calling data while
|
||||
retaining the 35B mixture-of-experts architecture, roughly 3B active
|
||||
parameters per token, and the native 262K-token context window.
|
||||
|
||||
> **Join the Discord** for updates, roadmaps, projects, or just to chat.
|
||||
|
||||
Qwen3.6-35B-A3B uncensored by HauhauCS. **0/465 Refusals.**
|
||||
|
||||
> **HuggingFace's "Hardware Compatibility" widget doesn't recognize K_P quants** — it may show fewer files than actually exist. Click **"View +X variants"** or go to **Files and versions** to see all available downloads.
|
||||
|
||||
## About
|
||||
|
||||
No changes to datasets or capabilities. Fully functional, 100% of what the original authors intended - just without the refusals.
|
||||
|
||||
These are meant to be the best lossless uncensored models out there.
|
||||
|
||||
## Aggressive Variant
|
||||
|
||||
Stronger uncensoring — model is fully unlocked and won't refuse prompts. May occasionally append short disclaimers (baked into base model training, not refusals) but full content is always generated.
|
||||
|
||||
For a more conservative uncensor that keeps some safety guardrails, check the Balanced variant when it's available.
|
||||
|
||||
## Downloads
|
||||
|
||||
All quants generated with importance matrix (imatrix) for optimal quality preservation on abliterated weights.
|
||||
|
||||
## What are K_P quants?
|
||||
|
||||
...
|
||||
This entry installs the Q8_0 GGUF together with its F16 multimodal projector
|
||||
for llama.cpp. The model card recommends Jinja chat templates and at least a
|
||||
128K context for its thinking behavior. License: Apache-2.0.
|
||||
license: "apache-2.0"
|
||||
tags:
|
||||
- llm
|
||||
@@ -2009,7 +1992,7 @@
|
||||
files:
|
||||
- filename: ds4flash.gguf
|
||||
uri: https://huggingface.co/unsloth/DeepSeek-V4-Flash-GGUF
|
||||
sha256: 1bfdafd1c288eb1b2bcb629ee9e1b7567dcf0abbe4d20995905a3c3465e9bd1e
|
||||
sha256: ba1d64ad8d77038124839956b614db2e889daa1a4ddc83060bb06ccb5a1d7461
|
||||
- name: "qwopus3.6-35b-a3b-coder-mtp"
|
||||
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
||||
urls:
|
||||
@@ -2108,6 +2091,83 @@
|
||||
- filename: llama-cpp/models/Qwen-AgentWorld-35B-A3B-GGUF/Qwen-AgentWorld-35B-A3B-UD-Q4_K_M.gguf
|
||||
sha256: e7a8eafdd8013443b6bcc4b6fb47b2d2025f772d359650b9ceb7d75971e22cad
|
||||
uri: https://huggingface.co/unsloth/Qwen-AgentWorld-35B-A3B-GGUF/resolve/main/Qwen-AgentWorld-35B-A3B-UD-Q4_K_M.gguf
|
||||
- &agents-a1-4b
|
||||
name: "agents-a1-4b"
|
||||
variants:
|
||||
- model: agents-a1-4b-q8
|
||||
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
||||
urls:
|
||||
- https://huggingface.co/InternScience/Agents-A1-4B
|
||||
- https://huggingface.co/InternScience/Agents-A1-4B-Q4_K_M-GGUF
|
||||
description: |
|
||||
Agents-A1-4B is InternScience's Apache-2.0 dense 4B agentic model, based on
|
||||
Qwen3.5. It is trained for long-horizon search, engineering and scientific
|
||||
research, instruction following, tool use, and multimodal tasks. This entry
|
||||
uses the official Q4_K_M GGUF quantization and vision projector.
|
||||
license: "apache-2.0"
|
||||
tags:
|
||||
- llm
|
||||
- gguf
|
||||
- vision
|
||||
- multimodal
|
||||
- gpu
|
||||
- cpu
|
||||
icon: https://huggingface.co/InternScience/Agents-A1-4B/resolve/main/figures/logo_nobg.png
|
||||
overrides:
|
||||
backend: llama-cpp
|
||||
function:
|
||||
automatic_tool_parsing_fallback: true
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
mmproj: llama-cpp/mmproj/Agents-A1-4B-Q4_K_M/Agents-A1-4B-mmproj.gguf
|
||||
options:
|
||||
- use_jinja:true
|
||||
parameters:
|
||||
model: llama-cpp/models/Agents-A1-4B-Q4_K_M/Agents-A1-4B-Q4_K_M.gguf
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: llama-cpp/models/Agents-A1-4B-Q4_K_M/Agents-A1-4B-Q4_K_M.gguf
|
||||
sha256: d93c393a9bd5139a4b5cfe24d31ef553c5a497bfb8afec178a354ecbf508f062
|
||||
uri: huggingface://InternScience/Agents-A1-4B-Q4_K_M-GGUF/Agents-A1-4B-Q4_K_M.gguf
|
||||
- filename: llama-cpp/mmproj/Agents-A1-4B-Q4_K_M/Agents-A1-4B-mmproj.gguf
|
||||
sha256: 254145e7e03e9e8d3120813fac8033ffa04e411eb6d70a198833504935681084
|
||||
uri: huggingface://InternScience/Agents-A1-4B-Q4_K_M-GGUF/Agents-A1-4B-mmproj.gguf
|
||||
- !!merge <<: *agents-a1-4b
|
||||
name: "agents-a1-4b-q8"
|
||||
variants: []
|
||||
urls:
|
||||
- https://huggingface.co/InternScience/Agents-A1-4B
|
||||
- https://huggingface.co/InternScience/Agents-A1-4B-Q8_0-GGUF
|
||||
description: |
|
||||
Agents-A1-4B is InternScience's Apache-2.0 dense 4B agentic model, based on
|
||||
Qwen3.5. It is trained for long-horizon search, engineering and scientific
|
||||
research, instruction following, tool use, and multimodal tasks. This entry
|
||||
uses the official Q8_0 GGUF quantization and vision projector.
|
||||
overrides:
|
||||
backend: llama-cpp
|
||||
function:
|
||||
automatic_tool_parsing_fallback: true
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
mmproj: llama-cpp/mmproj/Agents-A1-4B-Q8_0/Agents-A1-4B-mmproj.gguf
|
||||
options:
|
||||
- use_jinja:true
|
||||
parameters:
|
||||
model: llama-cpp/models/Agents-A1-4B-Q8_0/Agents-A1-4B-Q8_0.gguf
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: llama-cpp/models/Agents-A1-4B-Q8_0/Agents-A1-4B-Q8_0.gguf
|
||||
sha256: c327f66e820dae550bd230394595071c79f48c88d411b452d013ee4b5999fcea
|
||||
uri: huggingface://InternScience/Agents-A1-4B-Q8_0-GGUF/Agents-A1-4B-Q8_0.gguf
|
||||
- filename: llama-cpp/mmproj/Agents-A1-4B-Q8_0/Agents-A1-4B-mmproj.gguf
|
||||
sha256: 254145e7e03e9e8d3120813fac8033ffa04e411eb6d70a198833504935681084
|
||||
uri: huggingface://InternScience/Agents-A1-4B-Q8_0-GGUF/Agents-A1-4B-mmproj.gguf
|
||||
- name: "ornith-1.0-9b"
|
||||
variants:
|
||||
- model: ornith-1.0-9b-mtp
|
||||
@@ -2631,6 +2691,83 @@
|
||||
- filename: llama-cpp/models/LFM2.5-1.2B-Instruct-GGUF/LFM2.5-1.2B-Instruct-Q4_K_M.gguf
|
||||
sha256: b1b3de114215d9507409a662a501a631095a479a419584e8a2ded6304b19b4f5
|
||||
uri: https://huggingface.co/LiquidAI/LFM2.5-1.2B-Instruct-GGUF/resolve/main/LFM2.5-1.2B-Instruct-Q4_K_M.gguf
|
||||
- &lfm2-5-2-6b
|
||||
name: "lfm2.5-2.6b"
|
||||
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
||||
urls:
|
||||
- https://huggingface.co/LiquidAI/LFM2.5-2.6B
|
||||
- https://huggingface.co/LiquidAI/LFM2.5-2.6B-GGUF
|
||||
description: |
|
||||
LFM2.5-2.6B is LiquidAI's compact, text-only reasoning model for on-device
|
||||
agentic workloads. It has 2.69B parameters, a 128K-token context window,
|
||||
multilingual support, and post-training for tool use, instruction following,
|
||||
data extraction, RAG, and multi-step agents. This entry uses the recommended
|
||||
Q4_K_M GGUF quantization from LiquidAI's official repository.
|
||||
license: "other"
|
||||
tags:
|
||||
- llm
|
||||
- gguf
|
||||
- reasoning
|
||||
- cpu
|
||||
- gpu
|
||||
icon: https://cdn-uploads.huggingface.co/production/uploads/61b8e2ba285851687028d395/2b08LKpev0DNEk6DlnWkY.png
|
||||
variants:
|
||||
- model: lfm2.5-2.6b-q8
|
||||
overrides:
|
||||
backend: llama-cpp
|
||||
context_size: 131072
|
||||
function:
|
||||
automatic_tool_parsing_fallback: true
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
- completion
|
||||
options:
|
||||
- use_jinja:true
|
||||
parameters:
|
||||
model: llama-cpp/models/LFM2.5-2.6B-GGUF/LFM2.5-2.6B-Q4_K_M.gguf
|
||||
repeat_penalty: 1.1
|
||||
temperature: 0.1
|
||||
top_k: 50
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: llama-cpp/models/LFM2.5-2.6B-GGUF/LFM2.5-2.6B-Q4_K_M.gguf
|
||||
sha256: 79fdf00351b46cf26f020aead28d01889886be87c55fa0eb907e6f9b00bfee14
|
||||
uri: https://huggingface.co/LiquidAI/LFM2.5-2.6B-GGUF/resolve/main/LFM2.5-2.6B-Q4_K_M.gguf
|
||||
- !!merge <<: *lfm2-5-2-6b
|
||||
name: "lfm2.5-2.6b-q8"
|
||||
description: |
|
||||
LFM2.5-2.6B is LiquidAI's compact, text-only reasoning model for on-device
|
||||
agentic workloads. It has 2.69B parameters, a 128K-token context window,
|
||||
multilingual support, and post-training for tool use, instruction following,
|
||||
data extraction, RAG, and multi-step agents. This entry uses the higher-quality
|
||||
Q8_0 GGUF quantization from LiquidAI's official repository.
|
||||
variants: null
|
||||
overrides:
|
||||
backend: llama-cpp
|
||||
context_size: 131072
|
||||
function:
|
||||
automatic_tool_parsing_fallback: true
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
- completion
|
||||
options:
|
||||
- use_jinja:true
|
||||
parameters:
|
||||
model: llama-cpp/models/LFM2.5-2.6B-GGUF/LFM2.5-2.6B-Q8_0.gguf
|
||||
repeat_penalty: 1.1
|
||||
temperature: 0.1
|
||||
top_k: 50
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: llama-cpp/models/LFM2.5-2.6B-GGUF/LFM2.5-2.6B-Q8_0.gguf
|
||||
sha256: 36587fdf27bdfc69caf2637273679a0870ec155162161bde6fd16e8c70bdb757
|
||||
uri: https://huggingface.co/LiquidAI/LFM2.5-2.6B-GGUF/resolve/main/LFM2.5-2.6B-Q8_0.gguf
|
||||
- name: "qwopus3.6-27b-coder-compat-mtp"
|
||||
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
||||
urls:
|
||||
|
||||
13
scripts/build/backend-signing_test.sh
Executable file
13
scripts/build/backend-signing_test.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
WORKFLOW="$(dirname "$(realpath "$0")")/../../.github/workflows/backend_merge.yml"
|
||||
|
||||
sign_commands=$(grep -Ec -- '^[[:space:]]+cosign sign([[:space:]]|$)' "$WORKFLOW" || true)
|
||||
bundle_flags=$(grep -Ec -- '^[[:space:]]+--new-bundle-format([[:space:]]|$)' "$WORKFLOW" || true)
|
||||
if [ "$sign_commands" -ne 2 ] || [ "$bundle_flags" -ne "$sign_commands" ]; then
|
||||
echo "FAIL: every backend signing command must request the new bundle format (commands=$sign_commands flags=$bundle_flags)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: backend signing emits Sigstore bundles for both registries"
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
title: "What landed in LocalAI 4.8"
|
||||
date: 2026-08-01
|
||||
date: 2026-08-04
|
||||
author: "Ettore Di Giacinto"
|
||||
category: "Release"
|
||||
tags: ["release", "vllm.cpp", "audio.cpp", "3d", "gallery", "distributed", "performance"]
|
||||
summary: "A new inference engine, 3D generation, one backend that serves six audio endpoints, and a web interface 3.48x lighter. 321 pull requests in eighteen days."
|
||||
summary: "A new inference engine, 3D generation, one backend that serves six audio endpoints, and a web interface 3.48x lighter. 374 pull requests in twenty-one days."
|
||||
extracss: ["blog.css"]
|
||||
---
|
||||
|
||||
LocalAI 4.8.0 is out. It took eighteen days and 321 merged pull requests, and it pulls in two directions at once: three new things LocalAI can do that it could not do before, and a long list of places where it now does the old things without lying to you.
|
||||
LocalAI 4.8.0 is out, after twenty-one days and 374 merged pull requests. There are three new things LocalAI can do, and a lot of repair work on things it already did.
|
||||
|
||||
The full notes list everything. This post covers the parts that change what you do day to day, with the pull request numbers so you can read the diffs.
|
||||
|
||||
@@ -36,6 +36,11 @@ The third one was `/api/traces` returning a 21 MB unpaginated blob that the UI p
|
||||
|
||||
## One gallery entry, several builds
|
||||
|
||||
<figure>
|
||||
<img src="/media/v4-8-0-ui-model-variants.png" alt="The model detail pane listing every variant">
|
||||
<figcaption>One entry, four builds. LocalAI picks the largest that fits and marks it auto-selected.</figcaption>
|
||||
</figure>
|
||||
|
||||
Installing a model no longer means reading a list of quantizations and guessing which one your card will hold. A gallery entry can now declare `variants:`, a list of references to other entries that are alternative builds of the same weights:
|
||||
|
||||
```yaml
|
||||
@@ -55,12 +60,43 @@ Every surface can override the choice: `variant` on `POST /models/apply`, `local
|
||||
|
||||
One gap worth knowing about: in distributed mode `InstallModel` resolves against the frontend rather than the worker that will serve the model, so a cluster with a small frontend and large workers selects conservatively. PRs [#10943](https://github.com/mudler/LocalAI/pull/10943), [#10983](https://github.com/mudler/LocalAI/pull/10983), [#10992](https://github.com/mudler/LocalAI/pull/10992), [#11027](https://github.com/mudler/LocalAI/pull/11027) and [#11139](https://github.com/mudler/LocalAI/pull/11139).
|
||||
|
||||
## A new engine: vllm.cpp
|
||||
## A new engine: vllm.cpp (alpha)
|
||||
|
||||
[vllm.cpp](https://github.com/mudler/vllm.cpp) is a from-scratch C++20 port of vLLM, written and maintained by the LocalAI team under Apache-2.0, and it ships here as the `vllm-cpp` backend ([#11100](https://github.com/mudler/LocalAI/pull/11100)). It mirrors vLLM's V1 architecture, so paged KV cache, continuous batching, prefix caching, scheduler and sampler, on a portable tensor runtime with no Python, no PyTorch and no ggml at inference. It loads Hugging Face safetensors and GGUF, enforces structured output inside the engine (JSON schema, regex, choice, GBNF), and builds for CPU amd64 and arm64, CUDA 12 and 13 including Blackwell, L4T for GB10, Vulkan and Darwin Metal.
|
||||
[vllm.cpp](https://github.com/mudler/vllm.cpp) is Apache-2.0 and maintained by the LocalAI team. We want it community-first rather than a LocalAI-only engine, so it lives in its own repository with its own docs, benchmark record and issue tracker, and it runs without LocalAI anywhere in the picture. It began as a C++20 port of vLLM. It ships here as the `vllm-cpp` backend ([#11100](https://github.com/mudler/LocalAI/pull/11100)). It implements vLLM's V1 architecture, so paged KV cache, continuous batching, prefix caching, scheduler and sampler, on a portable tensor runtime with no Python, no PyTorch and no ggml at inference. vLLM stays its reference implementation: correctness is checked by comparing output against it, and the benchmark scoreboard is kept against it.
|
||||
|
||||
It has grown features vLLM does not have, which is most of the reason the port exists. It loads GGUF as well as safetensors, runs on CPU, Apple Metal and Vulkan alongside CUDA 12 and 13 and L4T for GB10, and ships speculative decoding and KV offload. Its benchmark page now measures against llama.cpp, MLX-LM and DwarfStar as well as vLLM, because on that hardware those are the engines it competes with. The project is expected to be renamed, with the new name still to be decided; it is drifting far enough that vllm.cpp will eventually mislead.
|
||||
|
||||
Tool calling is at llama.cpp parity by construction, because chat deliberately reuses the same autoparser path: full minja chat templates, `tool_choice: auto` lowered to a lazy structural-tag decode constraint, 30 tool dialects, 7 reasoning parsers, and streamed `ChatDelta` and `ToolCallDelta`.
|
||||
|
||||
<figure>
|
||||
<img src="/media/v4-8-0-vllm-cpp-scoreboard.png" alt="Throughput of vllm.cpp relative to each reference engine, drawn as deviation from parity">
|
||||
<figcaption>llama.cpp is left out because its 1.18x is a prefill ratio, and putting that on the same axis as throughput would compare two different measurements.</figcaption>
|
||||
</figure>
|
||||
|
||||
Numbers from the project's own [scoreboard](https://github.com/mudler/vllm.cpp/blob/master/docs/BENCHMARKS.md), which calls ties ties and losses losses. Above 1.0 means vllm.cpp is ahead:
|
||||
|
||||
<div class="tw">
|
||||
<table>
|
||||
<thead><tr><th>Reference</th><th>Workload</th><th>Result</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>vLLM</td><td>Qwen3.6-27B NVFP4, GB10</td><td>1.045x at concurrency 1, 1.007x to 1.017x from c2 to c32, output token-for-token identical</td></tr>
|
||||
<tr><td>vLLM</td><td>Qwen3.6-35B-A3B NVFP4, GB10</td><td>1.010x at c16 and 1.013x at c32, behind from c1 to c8 (0.817x at c1)</td></tr>
|
||||
<tr><td>llama.cpp</td><td>Qwen3.5-2B GGUF, CPU aarch64</td><td>prefill 1.18x, decode a tie, memory parity</td></tr>
|
||||
<tr><td>MLX-LM</td><td>Qwen3-0.6B, Apple M4</td><td>97.6% of warm total, prefill ahead</td></tr>
|
||||
<tr><td>DwarfStar (ds4)</td><td>DeepSeek-V4-Flash IQ2_XXS, one DGX Spark</td><td>18.69 vs 16.33 tok/s decode, <b>1.144x</b>, same output</td></tr>
|
||||
<tr><td>vLLM</td><td>Laguna-XS-2.1 NVFP4, GB10</td><td>44.46 vs 43.10 tok/s, <b>1.03x</b>, same output</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
The upstream page is careful about its own noise: on the 27B grid the run-to-run spread is 0.5% and c2 through c32 land between 0.7% and 1.7%, so it calls those five ties rather than wins. The concurrency-1 result is the one it stands behind.
|
||||
|
||||
The DeepSeek-V4-Flash row is the one that shows how far this has moved from being a vLLM port. It runs DeepSeek-V4-Flash at roughly 2-bit (IQ2_XXS mixed, about 80 GB) on a single DGX Spark, decoding at 18.69 tok/s against DwarfStar's 16.33. At 300B+ total parameters even a 4-bit checkpoint is 156 GB or more, so a 2-bit GGUF is what fits inside the Spark's 119 GiB unified pool, and reading GGUF is what makes that possible.
|
||||
|
||||
That number moved twice in a week, and the second move came from one lever. The dense Q8_0 projection tower was being read from the GGUF mmap over unified memory, which the GB10 reads about 20% slower per-GEMV than device memory. Staging that 6 GiB tower device-resident once at load, same bytes and same kernels, took decode from 16.23 to 18.69, generating the same tokens and using no more peak memory. The same change took Laguna-XS-2.1 from 87% of vLLM to 1.03x ahead of it.
|
||||
|
||||
Speculative decoding is in similar shape: MTP on Qwen3.6-27B NVFP4 generates the same tokens as vLLM's MTP and runs about 4% faster at concurrency 1.
|
||||
|
||||
Configuration is a normal backend install:
|
||||
|
||||
```yaml
|
||||
@@ -73,9 +109,24 @@ options:
|
||||
- max_num_seqs:16 # also: block_size:<n>, num_blocks:<n>
|
||||
```
|
||||
|
||||
The CPU path is verified end to end against `Qwen3.5-2B-UD-Q8_K_XL.gguf` with the full Ginkgo suite, covering blocking and streaming byte-parity, greedy determinism, stop words, GBNF-constrained generation, concurrent streams, reasoning split and both `required` and `auto` tool calls. The maturity statement from the release notes is worth repeating in full:
|
||||
**Treat these as alpha development builds, not a released backend.** vllm.cpp is early, and shipping it in 4.8 is about getting it in front of people who want to try it, not about recommending it for anything you care about. `llama-cpp` stays the default for real use.
|
||||
|
||||
> The GPU images build and ship, but their runtime behavior has not been through the same e2e gate yet. This is a first release of a young engine: no throughput comparison against upstream vLLM is claimed here, and `llama-cpp` remains the default recommendation for general use. Try it, and please report what breaks.
|
||||
The CPU path is verified end to end against `Qwen3.5-2B-UD-Q8_K_XL.gguf` with the full Ginkgo suite, covering blocking and streaming byte-parity, greedy determinism, stop words, GBNF-constrained generation, concurrent streams, reasoning split and both `required` and `auto` tool calls. The GPU images build and ship, but their runtime behavior has not been through that gate. No throughput comparison against upstream vLLM is claimed. Expect rough edges, and please report what breaks.
|
||||
|
||||
On Apple Silicon the image now ships vllm.cpp's MLX GEMM provider ([#11137](https://github.com/mudler/LocalAI/pull/11137)). Upstream keeps it off by default because it adds about 124 MB, so we measured before turning it on. Qwen3-1.7B-bf16 on an M4, p=512 g=128, both arms toggled on one binary so a build difference cannot explain the gap:
|
||||
|
||||
<div class="tw">
|
||||
<table>
|
||||
<thead><tr><th>Batch</th><th>MLX tok/s</th><th>native tok/s</th><th>speedup</th><th>MLX TTFT</th><th>native TTFT</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>1</td><td>5.79</td><td>3.08</td><td><b>1.88x</b></td><td>3.32 s</td><td>7.68 s</td></tr>
|
||||
<tr><td>4</td><td>15.75</td><td>10.24</td><td><b>1.54x</b></td><td>9.63 s</td><td>18.77 s</td></tr>
|
||||
<tr><td>16</td><td>38.65</td><td>17.69</td><td><b>2.19x</b></td><td>18.33 s</td><td>54.48 s</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Two reps, with rep spread reaching 9.4%, so treat the multipliers as +/-10%. Time to first token roughly halves across the range.
|
||||
|
||||
<figure>
|
||||
<video src="/media/vllm-race.mp4" muted loop playsinline preload="none" data-lazy aria-label="vllm.cpp generating tokens"></video>
|
||||
@@ -84,7 +135,7 @@ The CPU path is verified end to end against `Qwen3.5-2B-UD-Q8_K_XL.gguf` with th
|
||||
|
||||
## LocalAI generates 3D models now
|
||||
|
||||
This is a new modality rather than a new backend under an existing one, so it goes through the whole stack: a `Generate3D` RPC in `backend.proto`, a `FLAG_3D` capability so the loader knows which backends can serve it, and `POST /v1/3d/generations`.
|
||||
3D generation is a new modality, so it had to be wired through the whole stack: a `Generate3D` RPC in `backend.proto`, a `FLAG_3D` capability so the loader knows which backends can serve it, and `POST /v1/3d/generations`.
|
||||
|
||||
The first engine behind it is `trellis2cpp`, an image-to-3D backend over TRELLIS.2. You give it an image, you get a GLB back. The web UI has a page for it with a native GLB viewer, so you can turn the result around in the browser instead of downloading it to find out whether it worked, history kept in IndexedDB so a reload does not lose your generations, and previewable print remeshing for output you actually intend to send to a printer ([#10979](https://github.com/mudler/LocalAI/pull/10979)).
|
||||
|
||||
@@ -95,7 +146,7 @@ The first engine behind it is `trellis2cpp`, an image-to-3D backend over TRELLIS
|
||||
|
||||
## One backend, six audio endpoints
|
||||
|
||||
The usual shape for audio is one backend per model family, which means a process per capability and a config file for each. `audio-cpp` wraps [audio.cpp](https://github.com/0xShug0/audio.cpp), a multi-family ggml audio engine, and inverts that: one backend process serves several unrelated families through a single runtime vocabulary, and works out which family a checkpoint belongs to from the GGUF's own `audiocpp.model_spec.family` metadata key. There is nothing backend-specific to write in the model config.
|
||||
The usual shape for audio is one backend per model family, which means a process per capability and a config file for each. `audio-cpp` wraps [audio.cpp](https://github.com/0xShug0/audio.cpp), a multi-family ggml audio engine. One backend process serves several unrelated families through a single runtime vocabulary, and works out which family a checkpoint belongs to from the GGUF's own `audiocpp.model_spec.family` metadata key. There is nothing backend-specific to write in the model config.
|
||||
|
||||
<div class="tw">
|
||||
<table>
|
||||
@@ -130,7 +181,12 @@ The `bonsai` backend serves the 1-bit (Q1_0) and ternary (Q2_0) Bonsai quantizat
|
||||
|
||||
## The operations bar became a page
|
||||
|
||||
The old operations bar rendered one row per in-flight operation above every page. Queue four model installs and a backend and it took most of the viewport, on every route, until the last one finished. Two things were conflated there: a global "something is happening" signal, which needs one line, and the detail of what is happening, which needs somewhere to put it.
|
||||
<figure>
|
||||
<img src="/media/v4-8-0-ui-activity.png" alt="The Activity page with four installs running">
|
||||
<figcaption>Four backend installs in flight, and the record of what already finished.</figcaption>
|
||||
</figure>
|
||||
|
||||
The old operations bar rendered one row per in-flight operation above every page. Queue four model installs and a backend and it took most of the viewport, on every route, until the last one finished. It was doing two jobs at once. A global "something is happening" signal only needs one line, and the detail of what is happening needs a page of its own.
|
||||
|
||||
The strip is now one line, permanently, showing a failure first and otherwise the least-advanced running operation, with a `+N more` pill. Its `✕` hides the strip and no longer cancels anything. That is a deliberate behavior change worth knowing about before you click it out of habit: the same glyph used to cancel a 17 GB download in one row and dismiss a message in the next. Cancelling moved to the new page, behind a button that says so.
|
||||
|
||||
@@ -179,6 +235,6 @@ Valkey Search joins the vector store options as the `valkey-store` backend ([#11
|
||||
|
||||
This is also the release where localai.io split in two: the project site at the root, and the documentation under `/docs/`. Every URL that was published before still resolves, through 214 generated redirect stubs, because GitHub Pages has no server-side rewrites to do it properly ([#11243](https://github.com/mudler/LocalAI/pull/11243)).
|
||||
|
||||
Twenty-four people contributed to this release, eleven of them for the first time. The gallery went from 1,221 entries to 1,505.
|
||||
Twenty-five people contributed to this release, eleven of them for the first time. The gallery went from 1,221 entries to 1,515.
|
||||
|
||||
To upgrade, pull `localai/localai:latest` or re-run the install script. The [full changelog](https://github.com/mudler/LocalAI/compare/v4.7.1...v4.8.0) has everything this post left out.
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
<p class="kicker rv">The runtime</p>
|
||||
<h2 class="rv mt1" style="max-width:21ch">Everything else plugs into LocalAI.</h2>
|
||||
<p class="lede rv mt2">One binary with an OpenAI-compatible API in front of it. Point an existing client at it and the calls keep working, except now the model is on your machine. It also speaks the Anthropic, Ollama and ElevenLabs APIs, so most tools need a URL change and nothing else.</p>
|
||||
<p class="lede rv mt2">Underneath, a small core pulls each engine in as a separate backend, only when a model asks for it. That is why one install covers this much ground without becoming a 9 GB download.</p>
|
||||
<p class="lede rv mt2">The engine behind that API is swappable. One model can run on llama.cpp while the next loads on vLLM, SGLang or MLX, and the client never notices: same endpoint, same request, different engine underneath. Switching is one line in the model's config.</p>
|
||||
<p class="lede rv mt2">A small core pulls each engine in as a separate backend, only when a model asks for it. That is why one install covers this much ground without becoming a 9 GB download.</p>
|
||||
<div class="apis rv">
|
||||
<span>OpenAI API</span><span>Anthropic API</span><span>Ollama API</span><span>ElevenLabs API</span><span>Realtime over WebRTC</span>
|
||||
</div>
|
||||
|
||||
BIN
website/static/media/3d-generation.gif
Normal file
BIN
website/static/media/3d-generation.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 646 KiB |
BIN
website/static/media/v4-8-0-ui-activity.png
Normal file
BIN
website/static/media/v4-8-0-ui-activity.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 263 KiB |
BIN
website/static/media/v4-8-0-ui-home.png
Normal file
BIN
website/static/media/v4-8-0-ui-home.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 197 KiB |
BIN
website/static/media/v4-8-0-ui-model-variants.png
Normal file
BIN
website/static/media/v4-8-0-ui-model-variants.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 316 KiB |
100
website/static/media/v4-8-0-vllm-cpp-scoreboard.html
Normal file
100
website/static/media/v4-8-0-vllm-cpp-scoreboard.html
Normal file
@@ -0,0 +1,100 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
/* palette lifted from the two logos:
|
||||
LocalAI #0E2632 navy, #385360 slate, #469AAF teal, #90A8AE haze
|
||||
vllm.cpp #3AB4CA teal, #95C4D1 light */
|
||||
:root{
|
||||
--bg:#0b1c25; --ink:#e8f1f4; --dim:#90a8ae; --faint:#5d757f;
|
||||
--teal:#3ab4ca; --teal-hi:#7fd4e2; --amber:#e0a944; --rule:#1d3440;
|
||||
}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
html,body{width:1600px;height:900px}
|
||||
body{
|
||||
background:radial-gradient(1250px 720px at 80% -12%, #143140 0%, var(--bg) 62%);
|
||||
color:var(--ink);
|
||||
font-family:-apple-system,"SF Pro Display","Segoe UI",Helvetica,Arial,sans-serif;
|
||||
-webkit-font-smoothing:antialiased; padding:58px 84px; position:relative;
|
||||
}
|
||||
.eyebrow{display:flex;align-items:center;gap:14px;color:var(--teal);
|
||||
font-weight:600;font-size:23px;letter-spacing:.14em;text-transform:uppercase}
|
||||
.eyebrow .dot{width:11px;height:11px;border-radius:50%;background:var(--teal);
|
||||
box-shadow:0 0 16px 2px var(--teal)}
|
||||
h1{font-size:56px;line-height:1.06;font-weight:760;margin:16px 0 6px;letter-spacing:-.02em}
|
||||
h1 .grad{background:linear-gradient(92deg,var(--teal),var(--teal-hi));
|
||||
-webkit-background-clip:text;background-clip:text;color:transparent}
|
||||
.sub{color:var(--dim);font-size:23px;margin-bottom:14px}
|
||||
svg{width:100%;height:auto;display:block}
|
||||
.foot{position:absolute;left:84px;right:84px;bottom:40px;display:flex;
|
||||
justify-content:space-between;align-items:center;color:var(--faint);
|
||||
font-size:21px;border-top:1px solid var(--rule);padding-top:16px}
|
||||
.foot .link{color:var(--ink);font-weight:600}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="eyebrow"><span class="dot"></span>vllm.cpp · throughput vs the reference engine</div>
|
||||
<h1>Measured against <span class="grad">what each workload actually runs on</span></h1>
|
||||
<div class="sub">Throughput relative to the reference. 1.00 is parity, bars run from it. Higher is faster.</div>
|
||||
|
||||
<svg id="c" viewBox="0 0 1432 585"></svg>
|
||||
|
||||
<div class="foot">
|
||||
<span class="link">github.com/mudler/vllm.cpp</span>
|
||||
<span>GB10 unless noted · greedy, reference in its own production config · docs/BENCHMARKS.md</span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const rows = [
|
||||
{ref:'DwarfStar (ds4)', work:'DeepSeek-V4-Flash IQ2_XXS', v:1.144, note:'18.69 vs 16.33 tok/s'},
|
||||
{ref:'vLLM', work:'Qwen3.6-27B NVFP4, c1', v:1.045, note:'86.05 vs 82.32 tok/s'},
|
||||
{ref:'vLLM', work:'Laguna-XS-2.1 NVFP4', v:1.030, note:'44.46 vs 43.10 tok/s'},
|
||||
{ref:'vLLM', work:'Qwen3.6-35B-A3B, c32', v:1.013, note:'3030.5 vs 2993.0 tok/s'},
|
||||
{ref:'MLX-LM', work:'Qwen3-0.6B, Apple M4', v:0.976, note:'97.6% of warm total'},
|
||||
];
|
||||
|
||||
const W=1432, H=585;
|
||||
const AX=64; // axis strip reserved at the bottom
|
||||
const LBL=470; // left label gutter
|
||||
const R=150; // right gutter for the value
|
||||
const lo=-0.055, hi=0.165; // deviation domain around parity
|
||||
const pw=W-LBL-R;
|
||||
const x = d => LBL + pw*((d-lo)/(hi-lo));
|
||||
const zero = x(0);
|
||||
const rowH = (H-AX)/rows.length;
|
||||
const barH = 46;
|
||||
|
||||
let g='';
|
||||
// faint engineering grid at 2% steps
|
||||
for(let d=-0.04; d<=0.16001; d+=0.02){
|
||||
const gx=x(d), on0=Math.abs(d)<1e-9;
|
||||
g+=`<line x1="${gx}" y1="4" x2="${gx}" y2="${H-AX+10}" stroke="${on0?'#4a6b78':'#16303c'}" stroke-width="${on0?2:1}"/>`;
|
||||
g+=`<text x="${gx}" y="${H-22}" fill="${on0?'#90a8ae':'#4d6570'}" font-size="17" text-anchor="middle"
|
||||
font-weight="${on0?'700':'400'}">${(1+d).toFixed(2)}</text>`;
|
||||
}
|
||||
|
||||
rows.forEach((r,i)=>{
|
||||
const cy = i*rowH + rowH/2;
|
||||
const d = r.v-1;
|
||||
const ahead = d>=0;
|
||||
const col = ahead ? '#3ab4ca' : '#e0a944';
|
||||
const x0 = ahead ? zero : x(d);
|
||||
const w = Math.abs(x(d)-zero);
|
||||
|
||||
// reference + workload, two weights on one line
|
||||
g+=`<text x="${LBL-26}" y="${cy-4}" fill="#e8f1f4" font-size="25" font-weight="670" text-anchor="end">${r.ref}</text>`;
|
||||
g+=`<text x="${LBL-26}" y="${cy+22}" fill="#5d757f" font-size="19" text-anchor="end">${r.work}</text>`;
|
||||
|
||||
g+=`<rect x="${x0}" y="${cy-barH/2}" width="${Math.max(w,2)}" height="${barH}" rx="4" fill="${col}" opacity="0.92"/>`;
|
||||
|
||||
// value, then the raw measurement under it
|
||||
const vx = ahead ? x(d)+18 : zero+18;
|
||||
g+=`<text x="${vx}" y="${cy+1}" fill="${col}" font-size="27" font-weight="700"
|
||||
font-variant-numeric="tabular-nums">${r.v.toFixed(3)}×</text>`;
|
||||
g+=`<text x="${vx}" y="${cy+23}" fill="#5d757f" font-size="17">${r.note}</text>`;
|
||||
});
|
||||
document.getElementById('c').innerHTML=g;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
website/static/media/v4-8-0-vllm-cpp-scoreboard.png
Normal file
BIN
website/static/media/v4-8-0-vllm-cpp-scoreboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 689 KiB |
Reference in New Issue
Block a user