mirror of
https://github.com/mudler/LocalAI.git
synced 2026-08-06 13:26:11 -04:00
Compare commits
1 Commits
bot/issue-
...
bot/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc8113a8d6 |
@@ -125,7 +125,7 @@ The per-backend prefix match only sees files under a backend's own directory, so
|
||||
| `backend/backend.proto` | nothing if the edit is additive-only, otherwise everything (see below) |
|
||||
| `backend/Dockerfile.<x>` | the Linux entries whose `dockerfile:` names it |
|
||||
| `backend/python/common/` | Python, Linux + Darwin |
|
||||
| `scripts/build/package-gpu-libs.sh` | every Linux entry (Python, Go and C++ all run it) |
|
||||
| `scripts/build/package-gpu-libs.sh` | Python, Linux only |
|
||||
| `scripts/build/<lang>-darwin.sh` | the Darwin entries that build target routes to |
|
||||
| `.github/workflows/backend_build[_darwin].yml` | everything on that OS |
|
||||
| anything else under `scripts/build/` (except `*_test.sh`) | everything — conservative default for unclassified packaging inputs |
|
||||
|
||||
@@ -113,54 +113,6 @@ if [ "${BUILD_TYPE:-}" = "vulkan" ] && [ "${SKIP_DRIVERS:-false}" = "false" ]; t
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
fi
|
||||
|
||||
# --- 2b. Intel graphics driver (BUILD_TYPE=sycl*) ---
|
||||
# The Intel oneAPI base image brings the compilers and the oneAPI libraries, but
|
||||
# not the driver that talks to the graphics card. The packaging step copies that
|
||||
# driver into the backend, so that the backend works on a machine which has no
|
||||
# Intel graphics packages of its own, for the same reason the Vulkan section
|
||||
# above installs the Mesa drivers. Install it here so there is something to copy.
|
||||
#
|
||||
# Only the sycl builds are covered, because those are the ones whose packaging
|
||||
# copies the driver. See package_intel_libs in scripts/build/package-gpu-libs.sh.
|
||||
#
|
||||
# The driver comes from Intel's own package repository, not from the Ubuntu
|
||||
# archive. The archive has 23.43 from late 2023, which does not know any card
|
||||
# released since, so a machine with a recent Intel GPU would end up carrying a
|
||||
# driver that cannot drive it. Intel's repository has 25.18 for the same Ubuntu
|
||||
# release.
|
||||
#
|
||||
# Anything that goes wrong here fails the build, on purpose. An unreachable
|
||||
# repository is a passing problem that a retry fixes, whereas carrying a
|
||||
# different driver than intended, or none, is a difference nobody would notice
|
||||
# until a user reports an idle GPU.
|
||||
if case "${BUILD_TYPE:-}" in sycl*) true;; *) false;; esac \
|
||||
&& [ "${SKIP_DRIVERS:-false}" = "false" ]; then
|
||||
# Ubuntu release name, which is what the repository is indexed by.
|
||||
ubuntu_codename=$(. /etc/os-release && echo "${VERSION_CODENAME:-}")
|
||||
if [ -z "$ubuntu_codename" ]; then
|
||||
echo "ERROR: cannot tell which Ubuntu release this image is, so cannot pick the Intel driver repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The key is armored text, which apt reads directly from a .asc file, so
|
||||
# there is no need for gnupg here. "unified" is the component Intel ships
|
||||
# its current driver in.
|
||||
mkdir -p /usr/share/keyrings
|
||||
curl -fsSL https://repositories.intel.com/gpu/intel-graphics.key \
|
||||
-o /usr/share/keyrings/intel-graphics.asc
|
||||
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/intel-graphics.asc] https://repositories.intel.com/gpu/ubuntu ${ubuntu_codename} unified" \
|
||||
> /etc/apt/sources.list.d/intel-graphics.list
|
||||
apt-get update
|
||||
# The first package holds the driver OpenCL talks to, the second the driver
|
||||
# Level Zero talks to. Between them they pull in the compiler and the memory
|
||||
# manager that both need.
|
||||
apt-get install -y --no-install-recommends \
|
||||
intel-opencl-icd \
|
||||
libze-intel-gpu1
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
fi
|
||||
|
||||
# --- 3. CUDA toolkit (BUILD_TYPE=cublas|l4t) ---
|
||||
if { [ "${BUILD_TYPE:-}" = "cublas" ] || [ "${BUILD_TYPE:-}" = "l4t" ]; } && [ "${SKIP_DRIVERS:-false}" = "false" ]; then
|
||||
apt-get update
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
arch=${1:?target architecture is required}
|
||||
build_type=${2-}
|
||||
|
||||
# GPU arm64 base images do not consistently provide the gcc-14 toolchain needed
|
||||
# to compile ggml's armv9.2 CPU variants. Keep their portable fallback until the
|
||||
# builder images can supply that compiler.
|
||||
if [ "$arch" = "arm64" ] && [ -n "$build_type" ]; then
|
||||
echo llama-cpp-fallback
|
||||
else
|
||||
echo llama-cpp-cpu-all
|
||||
fi
|
||||
@@ -18,12 +18,10 @@ if [[ -n "${CUDA_DOCKER_ARCH:-}" ]]; then
|
||||
fi
|
||||
|
||||
cd /LocalAI/backend/cpp/llama-cpp
|
||||
BUILD_TARGET=$(/LocalAI/.docker/llama-cpp-build-target.sh "${TARGETARCH}" "${BUILD_TYPE:-}")
|
||||
if [ "$BUILD_TARGET" = "llama-cpp-cpu-all" ]; then
|
||||
# One build with ggml CPU_ALL_VARIANTS replaces the per-microarch binaries (x86:
|
||||
# avx/avx2/avx512/fallback; arm64: armv8.x/armv9.x). BUILD_TYPE remains in the
|
||||
# environment, so GPU builds retain their accelerator backend while ggml dlopens the
|
||||
# best CPU library when work is offloaded to the host.
|
||||
if [ -z "${BUILD_TYPE:-}" ]; then
|
||||
# Pure CPU image (BUILD_TYPE empty): one build with ggml CPU_ALL_VARIANTS replaces the
|
||||
# per-microarch binaries (x86: avx/avx2/avx512/fallback; arm64: armv8.x/armv9.x). ggml
|
||||
# dlopens the best libggml-cpu-*.so at runtime by probing host CPU features.
|
||||
#
|
||||
# arm64: the CPU_ALL_VARIANTS table includes armv9.2 SME variants whose -march=...+sme is
|
||||
# rejected by the Ubuntu 24.04 default gcc-13. gcc-14 accepts it, so build the arm64
|
||||
@@ -37,8 +35,14 @@ if [ "$BUILD_TARGET" = "llama-cpp-cpu-all" ]; then
|
||||
apt-get update -qq && apt-get install -y -qq gcc-14 g++-14
|
||||
export CC=gcc-14 CXX=g++-14
|
||||
fi
|
||||
make llama-cpp-cpu-all
|
||||
else
|
||||
# GPU build (cublas/hipblas/sycl/vulkan/...): the accelerator does the compute, so a
|
||||
# single fallback CPU build is enough - no per-microarch CPU variants needed. (This also
|
||||
# keeps the heavy GPU backend compile from also building the whole CPU variant matrix,
|
||||
# and avoids the gcc-14 apt step on GPU base images such as nvidia l4t.)
|
||||
make llama-cpp-fallback
|
||||
fi
|
||||
make "$BUILD_TARGET"
|
||||
make llama-cpp-grpc
|
||||
make llama-cpp-rpc-server
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
arch=${1:?target architecture is required}
|
||||
build_type=${2-}
|
||||
|
||||
# GPU arm64 base images do not consistently provide the gcc-14 toolchain needed
|
||||
# to compile ggml's armv9.2 CPU variants. Keep their portable fallback until the
|
||||
# builder images can supply that compiler.
|
||||
if [ "$arch" = "arm64" ] && [ -n "$build_type" ]; then
|
||||
echo turboquant-fallback
|
||||
else
|
||||
echo turboquant-cpu-all
|
||||
fi
|
||||
@@ -19,18 +19,20 @@ fi
|
||||
|
||||
cd /LocalAI/backend/cpp/turboquant
|
||||
|
||||
BUILD_TARGET=$(/LocalAI/.docker/turboquant-build-target.sh "${TARGETARCH}" "${BUILD_TYPE:-}")
|
||||
if [ "$BUILD_TARGET" = "turboquant-cpu-all" ]; then
|
||||
# BUILD_TYPE remains in the environment, so GPU builds retain their accelerator while
|
||||
# ggml selects the best CPU library when model work is offloaded to the host.
|
||||
if [ -z "${BUILD_TYPE:-}" ]; then
|
||||
# Pure CPU image: one ggml CPU_ALL_VARIANTS build replaces the per-microarch binaries.
|
||||
# arm64: the armv9.2 SME variants need gcc-14 (gcc-13 rejects +sme).
|
||||
if [ "${TARGETARCH}" = "arm64" ]; then
|
||||
sh /LocalAI/.docker/apt-mirror.sh || true
|
||||
apt-get update -qq && apt-get install -y -qq gcc-14 g++-14
|
||||
export CC=gcc-14 CXX=g++-14
|
||||
fi
|
||||
make turboquant-cpu-all
|
||||
else
|
||||
# GPU build (cublas/hipblas/sycl/vulkan/...): single fallback CPU build, the accelerator
|
||||
# does the compute. Keeps the GPU compile from also building the CPU variant matrix and
|
||||
# avoids the gcc-14 apt step on GPU base images such as nvidia l4t.
|
||||
make turboquant-fallback
|
||||
fi
|
||||
make "$BUILD_TARGET"
|
||||
make turboquant-grpc
|
||||
make turboquant-rpc-server
|
||||
|
||||
|
||||
64
.github/ci/refresh-site-counters.sh
vendored
64
.github/ci/refresh-site-counters.sh
vendored
@@ -1,64 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Refreshes the counters shown on the landing page from the GitHub API.
|
||||
#
|
||||
# The numbers used to be typed into the templates by hand, which meant they
|
||||
# only moved when somebody remembered, and a stale star count on the front
|
||||
# page is worse than no star count. Everything the API can answer for lives
|
||||
# in website/data/stats.yaml and is rewritten wholesale by this script.
|
||||
#
|
||||
# Anything the API cannot answer for (the Discord member count) is read back
|
||||
# out of the existing file and carried through untouched.
|
||||
set -euo pipefail
|
||||
|
||||
REPO="${REPO:-mudler/LocalAI}"
|
||||
OUT="${OUT:-website/data/stats.yaml}"
|
||||
|
||||
# The contributors and releases endpoints are paginated and never report a
|
||||
# total. Asking for one item per page makes the last page number equal to the
|
||||
# item count, which the Link header hands over.
|
||||
count_via_link_header() {
|
||||
local path="$1" link last
|
||||
link=$(gh api -i "${path}?per_page=1" 2>/dev/null | tr -d '\r' | grep -i '^link:' || true)
|
||||
if [ -z "$link" ]; then
|
||||
# No Link header means a single page, so count that page directly.
|
||||
gh api "${path}?per_page=100" --jq 'length'
|
||||
return
|
||||
fi
|
||||
last=$(sed -n 's/.*[?&]page=\([0-9]*\)>; rel="last".*/\1/p' <<<"$link")
|
||||
[ -n "$last" ] || { gh api "${path}?per_page=100" --jq 'length'; return; }
|
||||
printf '%s\n' "$last"
|
||||
}
|
||||
|
||||
read -r stars forks < <(gh api "repos/${REPO}" --jq '"\(.stargazers_count) \(.forks_count)"')
|
||||
contributors=$(count_via_link_header "repos/${REPO}/contributors")
|
||||
releases=$(count_via_link_header "repos/${REPO}/releases")
|
||||
|
||||
# Not derivable from the GitHub API, so keep whatever is already on disk.
|
||||
discord=$(sed -n 's/^discord: *\([0-9]*\).*/\1/p' "$OUT" 2>/dev/null | head -1)
|
||||
discord="${discord:-0}"
|
||||
|
||||
for n in stars forks contributors releases; do
|
||||
v="${!n}"
|
||||
[[ "$v" =~ ^[0-9]+$ ]] && [ "$v" -gt 0 ] || {
|
||||
echo "refusing to write: ${n} came back as '${v}'" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
cat > "$OUT" <<YAML
|
||||
# Counters shown on the landing page.
|
||||
#
|
||||
# The four GitHub fields are rewritten by .github/ci/refresh-site-counters.sh,
|
||||
# which runs weekly from .github/workflows/refresh-site-counters.yml. Editing
|
||||
# them by hand works but will be overwritten on the next run.
|
||||
stars: ${stars}
|
||||
forks: ${forks}
|
||||
contributors: ${contributors}
|
||||
releases: ${releases}
|
||||
|
||||
# The GitHub API cannot answer for this one, so it is maintained by hand and
|
||||
# the refresh script carries it through untouched.
|
||||
discord: ${discord}
|
||||
YAML
|
||||
|
||||
echo "stars=${stars} forks=${forks} contributors=${contributors} releases=${releases} discord=${discord}"
|
||||
44
.github/workflows/refresh-site-counters.yml
vendored
44
.github/workflows/refresh-site-counters.yml
vendored
@@ -1,44 +0,0 @@
|
||||
name: Refresh site counters
|
||||
|
||||
# The landing page shows a star count, a contributor count and a release
|
||||
# count. They were typed in by hand, so they drifted the moment somebody
|
||||
# forgot. This pulls the real numbers once a week and commits them only when
|
||||
# they have actually moved, which in turn triggers the usual Pages deploy.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Mondays, 06:17 UTC. Off the hour on purpose, since the scheduler queues
|
||||
# everything that asks for :00 and drops what it cannot run.
|
||||
- cron: '17 6 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: refresh-site-counters
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
refresh:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Read the counts off the GitHub API
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./.github/ci/refresh-site-counters.sh
|
||||
|
||||
- name: Commit only if something moved
|
||||
run: |
|
||||
if git diff --quiet -- website/data/stats.yaml; then
|
||||
echo "counters unchanged, nothing to commit"
|
||||
exit 0
|
||||
fi
|
||||
git diff --unified=0 -- website/data/stats.yaml
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add website/data/stats.yaml
|
||||
git commit -m "chore(website): refresh the counters"
|
||||
git push
|
||||
@@ -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?=f78227c52736a4792a50aa3f82ead7e7385c891b
|
||||
AUDIO_CPP_VERSION?=f32876cfb45732dd4f43264e9104d229e95b0bc3
|
||||
AUDIO_CPP_REPO?=https://github.com/0xShug0/audio.cpp
|
||||
|
||||
CURRENT_MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
# Pinned to the HEAD of the `prism` branch on https://github.com/PrismML-Eng/llama.cpp.
|
||||
# Auto-bumped nightly by .github/workflows/bump_deps.yaml.
|
||||
BONSAI_VERSION?=4dd165625bb6c020285eec8b342af25cf60233dd
|
||||
BONSAI_VERSION?=7529fdaaf99ffdc5ca71ace9c7409a56b27ad92f
|
||||
LLAMA_REPO?=https://github.com/PrismML-Eng/llama.cpp
|
||||
|
||||
CMAKE_ARGS?=
|
||||
|
||||
@@ -40,27 +40,6 @@ else
|
||||
if [ -d "$CURDIR/lib/hipblaslt/library" ]; then
|
||||
export HIPBLASLT_TENSILE_LIBPATH="$CURDIR"/lib/hipblaslt/library
|
||||
fi
|
||||
# Backends built for Intel GPUs carry a copy of the Intel graphics driver,
|
||||
# and libze_loader is only there in those builds. Level Zero looks for a
|
||||
# driver on its own, so point it at the copy that came with this backend: it
|
||||
# was built against the same C library, while the machine's own driver may
|
||||
# not have been, and loading that one can crash on start.
|
||||
#
|
||||
# Anything the user set is left alone, so a machine with a graphics card
|
||||
# newer than the driver carried here can still be told to use its own.
|
||||
# Nothing is said about OpenCL: no OpenCL driver is carried, so anything we
|
||||
# set there would leave OpenCL worse off than the machine's own setup.
|
||||
if [ -e "$CURDIR/lib/libze_loader.so.1" ]; then
|
||||
if [ -e "$CURDIR/lib/libze_intel_gpu.so.1" ] && [ -z "${ZE_ENABLE_ALT_DRIVERS:-}" ]; then
|
||||
export ZE_ENABLE_ALT_DRIVERS="$CURDIR"/lib/libze_intel_gpu.so.1
|
||||
fi
|
||||
# Ask the driver how much graphics memory is free. Without this, the
|
||||
# backend reads zero on an integrated graphics chip, because such a chip
|
||||
# shares the system memory instead of having its own.
|
||||
if [ -z "${ZES_ENABLE_SYSMAN:-}" ]; then
|
||||
export ZES_ENABLE_SYSMAN=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# If there is a lib/ld.so, use it
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
IK_LLAMA_VERSION?=3f53a059024039358e9fef75b5dc0c99dbcb40f9
|
||||
IK_LLAMA_VERSION?=9992f6b515ee63c7d6f7beee6b8414b0a6d1dd43
|
||||
LLAMA_REPO?=https://github.com/ikawrakow/ik_llama.cpp
|
||||
|
||||
CMAKE_ARGS?=
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
LLAMA_VERSION?=876a4321163249c43ca4e986818fab5ab081f282
|
||||
LLAMA_VERSION?=1cbfd1988311775425d36c0ce066590f7d3049cf
|
||||
LLAMA_REPO?=https://github.com/ggerganov/llama.cpp
|
||||
|
||||
CMAKE_ARGS?=
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
# MiniMax-M3 chat-template parser, vendored from upstream llama.cpp PR #24523.
|
||||
#
|
||||
# Upstream has since merged the *model* half of #24523 (LLM_ARCH_MINIMAX_M3,
|
||||
# src/models/minimax-m3.cpp, the gguf-py constants and conversion/minimax.py), so
|
||||
# only the chat half is carried here: M3's namespace token "]<]minimax[>[" collides
|
||||
# with the autoparser's markup delimiters, so common/chat.cpp needs a dedicated
|
||||
# template detection + PEG parser that upstream does not have yet.
|
||||
#
|
||||
# Rebased against LLAMA_VERSION 0d47ea7427463093e69128bf2c2f9cd06b3ee5b3, which also
|
||||
# renamed common_chat_params::thinking_end_tag to thinking_end_tags (a vector).
|
||||
# LLAMA_VERSION is auto-bumped nightly; if a bump rejects this patch, re-vendor from
|
||||
# #24523 — or, once the chat half merges upstream, delete this file.
|
||||
# See https://github.com/mudler/LocalAI/issues/10820 and PR #10837.
|
||||
diff --git a/common/chat.cpp b/common/chat.cpp
|
||||
index 7a6e7238c..2dd015a2e 100644
|
||||
--- a/common/chat.cpp
|
||||
+++ b/common/chat.cpp
|
||||
@@ -2121,6 +2121,191 @@ static common_chat_params common_chat_params_init_deepseek_v3_2(const common_cha
|
||||
return data;
|
||||
}
|
||||
|
||||
+static common_chat_params common_chat_params_init_minimax_m3(const common_chat_template & tmpl,
|
||||
+ const autoparser::generation_params & inputs) {
|
||||
+ common_chat_params data;
|
||||
+
|
||||
+ data.prompt = common_chat_template_direct_apply_impl(tmpl, inputs);
|
||||
+ data.generation_prompt = common_chat_template_generation_prompt_impl(tmpl, inputs);
|
||||
+ data.format = COMMON_CHAT_FORMAT_PEG_NATIVE;
|
||||
+ data.supports_thinking = true;
|
||||
+ data.thinking_start_tag = "<mm:think>";
|
||||
+ data.thinking_end_tags = {"</mm:think>"};
|
||||
+
|
||||
+ // M3 prefixes every tool tag with the namespace token "]<]minimax[>[";
|
||||
+ // params use the parameter name as the tag (<file_path>...</file_path>).
|
||||
+ const std::string NS = "]<]minimax[>[";
|
||||
+ const std::string THINK_START = "<mm:think>";
|
||||
+ const std::string THINK_END = "</mm:think>";
|
||||
+ const std::string FC_START = NS + "<tool_call>";
|
||||
+ const std::string FC_END = NS + "</tool_call>";
|
||||
+ const std::string INVOKE_END = NS + "</invoke>";
|
||||
+
|
||||
+ data.preserved_tokens = {
|
||||
+ NS,
|
||||
+ "<tool_call>",
|
||||
+ "</tool_call>",
|
||||
+ THINK_START,
|
||||
+ THINK_END,
|
||||
+ };
|
||||
+
|
||||
+ auto has_tools = inputs.tools.is_array() && !inputs.tools.empty();
|
||||
+ auto has_response_format = !inputs.json_schema.is_null() && inputs.json_schema.is_object();
|
||||
+ auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
|
||||
+ auto include_grammar = has_response_format || (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE);
|
||||
+
|
||||
+ const std::string GEN_PROMPT = data.generation_prompt;
|
||||
+
|
||||
+ if (inputs.has_continuation()) {
|
||||
+ const auto & msg = inputs.continue_msg;
|
||||
+
|
||||
+ data.generation_prompt = GEN_PROMPT + THINK_START + msg.reasoning_content;
|
||||
+ if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT) {
|
||||
+ data.generation_prompt += THINK_END + msg.render_content();
|
||||
+ }
|
||||
+
|
||||
+ data.prompt += data.generation_prompt;
|
||||
+ }
|
||||
+
|
||||
+ auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) {
|
||||
+ auto generation_prompt = p.literal(GEN_PROMPT);
|
||||
+ auto end = p.end();
|
||||
+
|
||||
+ auto reasoning = p.eps();
|
||||
+ // M3 can emit a bare </mm:think> (no opener) after tool results; keep the opener optional.
|
||||
+ if (extract_reasoning && inputs.enable_thinking) {
|
||||
+ reasoning = p.optional(p.optional(p.literal(THINK_START)) + p.reasoning(p.until(THINK_END)) + THINK_END);
|
||||
+ } else if (extract_reasoning) {
|
||||
+ reasoning = p.optional(p.optional(p.literal(THINK_START)) + p.until(THINK_END) + p.literal(THINK_END));
|
||||
+ }
|
||||
+
|
||||
+ if (has_response_format) {
|
||||
+ auto response_format = p.rule("response-format",
|
||||
+ p.literal("```json") + p.space() +
|
||||
+ p.content(p.schema(p.json(), "response-format-schema", inputs.json_schema)) +
|
||||
+ p.space() + p.literal("```"));
|
||||
+ return generation_prompt + reasoning + response_format + end;
|
||||
+ }
|
||||
+
|
||||
+ if (!has_tools || inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE) {
|
||||
+ return generation_prompt + reasoning + p.content(p.rest()) + end;
|
||||
+ }
|
||||
+
|
||||
+ auto tool_choice = p.choice();
|
||||
+ foreach_function(inputs.tools, [&](const json & tool) {
|
||||
+ const auto & function = tool.at("function");
|
||||
+ std::string name = function.at("name");
|
||||
+ auto params = function.contains("parameters") ? function.at("parameters") : json::object();
|
||||
+ const auto & props = params.contains("properties") ? params.at("properties") : json::object();
|
||||
+
|
||||
+ std::set<std::string> required;
|
||||
+ if (params.contains("required")) {
|
||||
+ params.at("required").get_to(required);
|
||||
+ }
|
||||
+
|
||||
+ auto schema_info = common_schema_info();
|
||||
+ schema_info.resolve_refs(params);
|
||||
+
|
||||
+ std::vector<common_peg_parser> required_parsers;
|
||||
+ std::vector<common_peg_parser> optional_parsers;
|
||||
+ for (const auto & [param_name, param_schema] : props.items()) {
|
||||
+ bool is_required = required.find(param_name) != required.end();
|
||||
+ bool is_string = schema_info.resolves_to_string(param_schema);
|
||||
+
|
||||
+ const std::string p_close = NS + "</" + param_name + ">";
|
||||
+
|
||||
+ auto arg = p.tool_arg(
|
||||
+ p.tool_arg_open(
|
||||
+ p.literal(NS + "<") +
|
||||
+ p.tool_arg_name(p.literal(param_name)) +
|
||||
+ p.literal(">")) +
|
||||
+ (is_string
|
||||
+ ? p.ac(p.tool_arg_string_value(p.until(p_close)) +
|
||||
+ p.tool_arg_close(p.literal(p_close)), p_close)
|
||||
+ : p.tool_arg_json_value(p.schema(p.json(),
|
||||
+ "tool-" + name + "-arg-" + param_name + "-schema",
|
||||
+ param_schema, false)) +
|
||||
+ p.tool_arg_close(p.literal(p_close))));
|
||||
+
|
||||
+ auto named_arg = p.rule("tool-" + name + "-arg-" + param_name, arg);
|
||||
+ if (is_required) {
|
||||
+ required_parsers.push_back(named_arg);
|
||||
+ } else {
|
||||
+ optional_parsers.push_back(named_arg);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ common_peg_parser args_seq = p.eps();
|
||||
+ for (size_t i = 0; i < required_parsers.size(); i++) {
|
||||
+ if (i > 0) {
|
||||
+ args_seq = args_seq + p.space();
|
||||
+ }
|
||||
+ args_seq = args_seq + required_parsers[i];
|
||||
+ }
|
||||
+
|
||||
+ if (!optional_parsers.empty()) {
|
||||
+ common_peg_parser any_opt = p.choice();
|
||||
+ for (const auto & opt : optional_parsers) {
|
||||
+ any_opt |= opt;
|
||||
+ }
|
||||
+ args_seq = args_seq + p.repeat(p.space() + any_opt, 0, -1);
|
||||
+ }
|
||||
+
|
||||
+ common_peg_parser invoke_body = args_seq;
|
||||
+ auto func_parser = p.tool(
|
||||
+ p.tool_open(p.literal(NS + "<invoke name=\"") +
|
||||
+ p.tool_name(p.literal(name)) + p.literal("\">")) +
|
||||
+ p.space() + invoke_body + p.space() +
|
||||
+ p.tool_close(p.literal(INVOKE_END)));
|
||||
+
|
||||
+ tool_choice |= p.rule("tool-" + name, func_parser);
|
||||
+ });
|
||||
+
|
||||
+ auto require_tools = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED;
|
||||
+
|
||||
+ common_peg_parser tool_calls = p.eps();
|
||||
+ if (inputs.parallel_tool_calls) {
|
||||
+ tool_calls = p.trigger_rule("tool-call",
|
||||
+ p.literal(FC_START) + p.space() + tool_choice +
|
||||
+ p.zero_or_more(p.space() + tool_choice) + p.space() + p.literal(FC_END));
|
||||
+ } else {
|
||||
+ tool_calls = p.trigger_rule("tool-call",
|
||||
+ p.literal(FC_START) + p.space() + tool_choice + p.space() + p.literal(FC_END));
|
||||
+ }
|
||||
+
|
||||
+ if (!require_tools) {
|
||||
+ tool_calls = p.optional(tool_calls);
|
||||
+ }
|
||||
+
|
||||
+ auto content_before_tools = p.content(p.until(FC_START));
|
||||
+ return generation_prompt + reasoning + content_before_tools + tool_calls + end;
|
||||
+ });
|
||||
+
|
||||
+ data.parser = parser.save();
|
||||
+
|
||||
+ if (include_grammar) {
|
||||
+ data.grammar_lazy = !(has_response_format || (has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED));
|
||||
+ data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
+ foreach_function(inputs.tools, [&](const json & tool) {
|
||||
+ const auto & function = tool.at("function");
|
||||
+ auto schema = function.contains("parameters") ? function.at("parameters") : json::object();
|
||||
+ builder.resolve_refs(schema);
|
||||
+ });
|
||||
+ if (has_response_format) {
|
||||
+ auto schema = inputs.json_schema;
|
||||
+ builder.resolve_refs(schema);
|
||||
+ }
|
||||
+ parser.build_grammar(builder, data.grammar_lazy);
|
||||
+ });
|
||||
+
|
||||
+ data.grammar_triggers = {
|
||||
+ { COMMON_GRAMMAR_TRIGGER_TYPE_WORD, FC_START },
|
||||
+ };
|
||||
+ }
|
||||
+
|
||||
+ return data;
|
||||
+}
|
||||
+
|
||||
// Cohere2 MoE (a.k.a. "North Code") parser.
|
||||
//
|
||||
// The assistant turn is fully marker-wrapped:
|
||||
@@ -2707,6 +2892,15 @@ std::optional<common_chat_params> common_chat_try_specialized_template(
|
||||
return common_chat_params_init_gigachat_v3(tmpl, params);
|
||||
}
|
||||
|
||||
+ // MiniMax-M3: the namespace token "]<]minimax[>[" collides with the autoparser's
|
||||
+ // markup delimiters, so detect the template and use a dedicated parser.
|
||||
+ if (src.find("]<]minimax[>[") != std::string::npos &&
|
||||
+ src.find("<tool_call>") != std::string::npos &&
|
||||
+ src.find("<invoke name=") != std::string::npos) {
|
||||
+ LOG_DBG("Using specialized template: MiniMax-M3\n");
|
||||
+ return common_chat_params_init_minimax_m3(tmpl, params);
|
||||
+ }
|
||||
+
|
||||
// DeepSeek V3.2/V4 format detection: template defines dsml_token and uses it for tool calls.
|
||||
// The template source contains the token as a variable assignment, not as a literal in markup.
|
||||
// V3.2 names the tool call block "function_calls", V4 names it "tool_calls".
|
||||
@@ -12,10 +12,10 @@ grep -e "flags" /proc/cpuinfo | head -1
|
||||
|
||||
BINARY=llama-cpp-fallback
|
||||
|
||||
# CPU images and x86 GPU images ship a single llama-cpp-cpu-all built with ggml
|
||||
# CPU images (x86, arm64, darwin) ship a single llama-cpp-cpu-all built with ggml
|
||||
# CPU_ALL_VARIANTS: ggml's backend registry dlopens the best libggml-cpu-*.so for this
|
||||
# host, so no shell-side AVX probing. GPU arm64 images still ship llama-cpp-fallback
|
||||
# until their builder toolchains support ggml's complete arm variant matrix.
|
||||
# host, so no shell-side AVX probing. GPU images (cublas/sycl/vulkan/hipblas) ship only
|
||||
# llama-cpp-fallback (the accelerator does the compute), so fall back to it when absent.
|
||||
if [ -e "$CURDIR"/llama-cpp-cpu-all ]; then
|
||||
BINARY=llama-cpp-cpu-all
|
||||
fi
|
||||
@@ -42,27 +42,6 @@ else
|
||||
if [ -d "$CURDIR/lib/hipblaslt/library" ]; then
|
||||
export HIPBLASLT_TENSILE_LIBPATH="$CURDIR"/lib/hipblaslt/library
|
||||
fi
|
||||
# Backends built for Intel GPUs carry a copy of the Intel graphics driver,
|
||||
# and libze_loader is only there in those builds. Level Zero looks for a
|
||||
# driver on its own, so point it at the copy that came with this backend: it
|
||||
# was built against the same C library, while the machine's own driver may
|
||||
# not have been, and loading that one can crash on start.
|
||||
#
|
||||
# Anything the user set is left alone, so a machine with a graphics card
|
||||
# newer than the driver carried here can still be told to use its own.
|
||||
# Nothing is said about OpenCL: no OpenCL driver is carried, so anything we
|
||||
# set there would leave OpenCL worse off than the machine's own setup.
|
||||
if [ -e "$CURDIR/lib/libze_loader.so.1" ]; then
|
||||
if [ -e "$CURDIR/lib/libze_intel_gpu.so.1" ] && [ -z "${ZE_ENABLE_ALT_DRIVERS:-}" ]; then
|
||||
export ZE_ENABLE_ALT_DRIVERS="$CURDIR"/lib/libze_intel_gpu.so.1
|
||||
fi
|
||||
# Ask the driver how much graphics memory is free. Without this,
|
||||
# llama.cpp reads zero on an integrated graphics chip, because such a
|
||||
# chip shares the system memory instead of having its own.
|
||||
if [ -z "${ZES_ENABLE_SYSMAN:-}" ]; then
|
||||
export ZES_ENABLE_SYSMAN=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# If there is a lib/ld.so, use it
|
||||
@@ -76,4 +55,4 @@ echo "Using binary: $BINARY"
|
||||
exec "$CURDIR"/$BINARY "$@"
|
||||
|
||||
# We should never reach this point, however just in case we do, run fallback
|
||||
exec "$CURDIR"/llama-cpp-fallback "$@"
|
||||
exec "$CURDIR"/llama-cpp-fallback "$@"
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
# Pinned to the HEAD of feature/turboquant-kv-cache on https://github.com/TheTom/llama-cpp-turboquant.
|
||||
# Auto-bumped nightly by .github/workflows/bump_deps.yaml.
|
||||
TURBOQUANT_VERSION?=8a891f4b566efdbd3cea92fafee3227a0a267683
|
||||
TURBOQUANT_VERSION?=c26cbdffcf6fc9b7430cd6b117757e9a3f70b7ea
|
||||
LLAMA_REPO?=https://github.com/TheTom/llama-cpp-turboquant
|
||||
|
||||
CMAKE_ARGS?=
|
||||
|
||||
@@ -12,11 +12,9 @@ grep -e "flags" /proc/cpuinfo | head -1
|
||||
|
||||
BINARY=turboquant-fallback
|
||||
|
||||
# CPU images and x86 GPU images ship a single turboquant-cpu-all built with ggml
|
||||
# CPU_ALL_VARIANTS: ggml's
|
||||
# x86/arm64 ship a single turboquant-cpu-all built with ggml CPU_ALL_VARIANTS: ggml's
|
||||
# backend registry dlopens the best libggml-cpu-*.so for this host, so no shell-side
|
||||
# probing. GPU arm64 images still ship turboquant-fallback until their builder toolchains
|
||||
# support ggml's complete arm variant matrix.
|
||||
# probing. ROCm ships only turboquant-fallback, so fall back to it when cpu-all is absent.
|
||||
if [ -e "$CURDIR"/turboquant-cpu-all ]; then
|
||||
BINARY=turboquant-cpu-all
|
||||
fi
|
||||
@@ -42,27 +40,6 @@ else
|
||||
if [ -d "$CURDIR/lib/hipblaslt/library" ]; then
|
||||
export HIPBLASLT_TENSILE_LIBPATH="$CURDIR"/lib/hipblaslt/library
|
||||
fi
|
||||
# Backends built for Intel GPUs carry a copy of the Intel graphics driver,
|
||||
# and libze_loader is only there in those builds. Level Zero looks for a
|
||||
# driver on its own, so point it at the copy that came with this backend: it
|
||||
# was built against the same C library, while the machine's own driver may
|
||||
# not have been, and loading that one can crash on start.
|
||||
#
|
||||
# Anything the user set is left alone, so a machine with a graphics card
|
||||
# newer than the driver carried here can still be told to use its own.
|
||||
# Nothing is said about OpenCL: no OpenCL driver is carried, so anything we
|
||||
# set there would leave OpenCL worse off than the machine's own setup.
|
||||
if [ -e "$CURDIR/lib/libze_loader.so.1" ]; then
|
||||
if [ -e "$CURDIR/lib/libze_intel_gpu.so.1" ] && [ -z "${ZE_ENABLE_ALT_DRIVERS:-}" ]; then
|
||||
export ZE_ENABLE_ALT_DRIVERS="$CURDIR"/lib/libze_intel_gpu.so.1
|
||||
fi
|
||||
# Ask the driver how much graphics memory is free. Without this, the
|
||||
# backend reads zero on an integrated graphics chip, because such a chip
|
||||
# shares the system memory instead of having its own.
|
||||
if [ -z "${ZES_ENABLE_SYSMAN:-}" ]; then
|
||||
export ZES_ENABLE_SYSMAN=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# If there is a lib/ld.so, use it
|
||||
|
||||
@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
|
||||
|
||||
# CrispASR version (release tag)
|
||||
CRISPASR_REPO?=https://github.com/CrispStrobe/CrispASR
|
||||
CRISPASR_VERSION?=b5211ac635489049ee8ce86a82d69faa18e8d8da
|
||||
CRISPASR_VERSION?=677e95d0e60010f10636c3a0b1ba215b38a4a943
|
||||
SO_TARGET?=libgocrispasr.so
|
||||
|
||||
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF
|
||||
|
||||
@@ -11,7 +11,7 @@ JOBS?=$(shell nproc --ignore=1)
|
||||
# build; leaving this on `master` always picks up the latest C-API surface
|
||||
# (incl. the per-detection accessor functions used by gorfdetrcpp.go).
|
||||
RFDETR_REPO?=https://github.com/localai-org/rf-detr.cpp.git
|
||||
RFDETR_VERSION?=98d0f381b832ef08a608b65c7dd78db066ed8b9a
|
||||
RFDETR_VERSION?=65c0ffcc9a9bc9dae38252f63d0417c9845a6cf7
|
||||
|
||||
ifeq ($(NATIVE),false)
|
||||
CMAKE_ARGS+=-DGGML_NATIVE=OFF
|
||||
|
||||
@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
|
||||
|
||||
# whisper.cpp version
|
||||
WHISPER_REPO?=https://github.com/ggml-org/whisper.cpp
|
||||
WHISPER_CPP_VERSION?=2ca53bb45e38748d07b310eeb36245a7157ac882
|
||||
WHISPER_CPP_VERSION?=4523d0ce373ee4b2176b3251fff29fd4864fcf38
|
||||
SO_TARGET?=libgowhisper.so
|
||||
|
||||
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF
|
||||
|
||||
@@ -236,8 +236,8 @@ var _ = Describe("InstallModelFromGallery with an empty base config", func() {
|
||||
Expect(install(e.Name, gallery.GalleryModel{})).To(Succeed())
|
||||
cfg := installedConfig(e.Name)
|
||||
Expect(cfg["name"]).To(Equal(e.Name))
|
||||
// The catalog's own overrides, verbatim, laid over the empty base.
|
||||
Expect(cfg["parameters"]).To(Equal(e.Overrides["parameters"]))
|
||||
// The catalog's model override survives inference-default enrichment.
|
||||
Expect(cfg["parameters"]).To(HaveKeyWithValue("model", "LiquidAI_LFM2-1.2B-RAG-Q4_K_M.gguf"))
|
||||
Expect(cfg["known_usecases"]).To(Equal(e.Overrides["known_usecases"]))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,6 @@ package importers
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
@@ -32,7 +31,7 @@ func (i *MLXImporter) Match(details Details) bool {
|
||||
}
|
||||
|
||||
b, ok := preferencesMap["backend"].(string)
|
||||
if ok && slices.Contains([]string{"mlx", "mlx-vlm", "mlx-audio"}, b) {
|
||||
if ok && b == "mlx" || b == "mlx-vlm" {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -72,32 +71,19 @@ func (i *MLXImporter) Import(details Details) (gallery.ModelConfig, error) {
|
||||
// (issue #10269). Send them to the mlx-vlm backend, which applies the
|
||||
// processor-aware chat template.
|
||||
backend := "mlx"
|
||||
usecases := []string{config.UsecaseChat}
|
||||
useTokenizerTemplate := true
|
||||
if details.HuggingFace != nil {
|
||||
switch details.HuggingFace.PipelineTag {
|
||||
case "image-text-to-text":
|
||||
backend = "mlx-vlm"
|
||||
case "text-to-speech":
|
||||
backend = "mlx-audio"
|
||||
usecases = []string{config.UsecaseTTS}
|
||||
useTokenizerTemplate = false
|
||||
}
|
||||
if details.HuggingFace != nil && details.HuggingFace.PipelineTag == "image-text-to-text" {
|
||||
backend = "mlx-vlm"
|
||||
}
|
||||
// An explicit backend preference always wins.
|
||||
b, ok := preferencesMap["backend"].(string)
|
||||
if ok {
|
||||
backend = b
|
||||
if backend == "mlx-audio" {
|
||||
usecases = []string{config.UsecaseTTS}
|
||||
useTokenizerTemplate = false
|
||||
}
|
||||
}
|
||||
|
||||
modelConfig := config.ModelConfig{
|
||||
Name: name,
|
||||
Description: description,
|
||||
KnownUsecaseStrings: usecases,
|
||||
KnownUsecaseStrings: []string{config.UsecaseChat},
|
||||
Backend: backend,
|
||||
PredictionOptions: schema.PredictionOptions{
|
||||
BasicModelRequest: schema.BasicModelRequest{
|
||||
@@ -105,7 +91,7 @@ func (i *MLXImporter) Import(details Details) (gallery.ModelConfig, error) {
|
||||
},
|
||||
},
|
||||
TemplateConfig: config.TemplateConfig{
|
||||
UseTokenizerTemplate: useTokenizerTemplate,
|
||||
UseTokenizerTemplate: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -48,16 +48,6 @@ var _ = Describe("MLXImporter", func() {
|
||||
Expect(result).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should match when backend preference is mlx-audio", func() {
|
||||
preferences := json.RawMessage(`{"backend": "mlx-audio"}`)
|
||||
details := importers.Details{
|
||||
URI: "https://example.com/model",
|
||||
Preferences: preferences,
|
||||
}
|
||||
|
||||
Expect(importer.Match(details)).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should not match when URI does not contain mlx-community/ and no backend preference", func() {
|
||||
details := importers.Details{
|
||||
URI: "https://huggingface.co/other-org/test-model",
|
||||
@@ -133,21 +123,6 @@ var _ = Describe("MLXImporter", func() {
|
||||
Expect(modelConfig.ConfigFile).To(ContainSubstring("backend: mlx-vlm"))
|
||||
})
|
||||
|
||||
It("should configure explicit mlx-audio imports for text-to-speech", func() {
|
||||
preferences := json.RawMessage(`{"backend": "mlx-audio"}`)
|
||||
details := importers.Details{
|
||||
URI: "https://huggingface.co/mlx-community/Kokoro-82M-4bit",
|
||||
Preferences: preferences,
|
||||
}
|
||||
|
||||
modelConfig, err := importer.Import(details)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(modelConfig.ConfigFile).To(ContainSubstring("backend: mlx-audio"))
|
||||
Expect(modelConfig.ConfigFile).To(ContainSubstring("- tts"))
|
||||
Expect(modelConfig.ConfigFile).ToNot(ContainSubstring("use_tokenizer_template: true"))
|
||||
})
|
||||
|
||||
It("should auto-route vision-language models to the mlx-vlm backend", func() {
|
||||
// gemma-4 E4B and similar VLMs declare pipeline_tag
|
||||
// "image-text-to-text" on HuggingFace. The text-only mlx-lm
|
||||
@@ -168,23 +143,6 @@ var _ = Describe("MLXImporter", func() {
|
||||
Expect(modelConfig.ConfigFile).To(ContainSubstring("backend: mlx-vlm"))
|
||||
})
|
||||
|
||||
It("should auto-route text-to-speech models to the mlx-audio backend", func() {
|
||||
details := importers.Details{
|
||||
URI: "https://huggingface.co/mlx-community/Kokoro-82M-4bit",
|
||||
HuggingFace: &hfapi.ModelDetails{
|
||||
ModelID: "mlx-community/Kokoro-82M-4bit",
|
||||
PipelineTag: "text-to-speech",
|
||||
},
|
||||
}
|
||||
|
||||
modelConfig, err := importer.Import(details)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(modelConfig.ConfigFile).To(ContainSubstring("backend: mlx-audio"))
|
||||
Expect(modelConfig.ConfigFile).To(ContainSubstring("- tts"))
|
||||
Expect(modelConfig.ConfigFile).ToNot(ContainSubstring("use_tokenizer_template: true"))
|
||||
})
|
||||
|
||||
It("should keep text-only models on the plain mlx backend", func() {
|
||||
details := importers.Details{
|
||||
URI: "https://huggingface.co/mlx-community/Llama-3.2-1B-Instruct-4bit",
|
||||
|
||||
90
core/gallery/inference_defaults_install_test.go
Normal file
90
core/gallery/inference_defaults_install_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package gallery_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/mudler/LocalAI/core/gallery"
|
||||
"github.com/mudler/LocalAI/pkg/modelartifacts"
|
||||
"github.com/mudler/LocalAI/pkg/system"
|
||||
)
|
||||
|
||||
var _ = Describe("gallery inference defaults", func() {
|
||||
readPersistedConfig := func(modelsPath, name string) map[string]any {
|
||||
data, err := os.ReadFile(filepath.Join(modelsPath, name+".yaml"))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
persisted := map[string]any{}
|
||||
Expect(yaml.Unmarshal(data, &persisted)).To(Succeed())
|
||||
return persisted
|
||||
}
|
||||
|
||||
expectNestedDefaults := func(persisted map[string]any) {
|
||||
Expect(persisted).NotTo(HaveKey("temperature"))
|
||||
Expect(persisted).NotTo(HaveKey("top_p"))
|
||||
parameters, ok := persisted["parameters"].(map[string]any)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(parameters).To(HaveKeyWithValue("temperature", 0.7))
|
||||
Expect(parameters).To(HaveKeyWithValue("top_p", 0.42))
|
||||
Expect(parameters).To(HaveKeyWithValue("top_k", 20))
|
||||
Expect(parameters).To(HaveKeyWithValue("min_p", 0))
|
||||
Expect(parameters).To(HaveKeyWithValue("repeat_penalty", 1))
|
||||
Expect(parameters).To(HaveKeyWithValue("presence_penalty", 1.5))
|
||||
}
|
||||
|
||||
It("persists defaults under parameters after artifact binding", func() {
|
||||
modelsPath := GinkgoT().TempDir()
|
||||
state, err := system.GetSystemState(system.WithModelPath(modelsPath))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
resolved := modelartifacts.Spec{
|
||||
Name: "model", Target: "model",
|
||||
Source: modelartifacts.Source{Type: "huggingface", Repo: "owner/qwen3.5-model", Revision: "main"},
|
||||
Resolved: &modelartifacts.Resolved{
|
||||
Endpoint: "https://huggingface.co",
|
||||
Revision: "0123456789abcdef0123456789abcdef01234567",
|
||||
CacheKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
},
|
||||
}
|
||||
fake := &fakeArtifactMaterializer{result: modelartifacts.Result{Spec: resolved}}
|
||||
definition := &gallery.ModelConfig{Name: "qwen3.5-artifact", ConfigFile: `
|
||||
backend: transformers
|
||||
artifacts:
|
||||
- name: model
|
||||
target: model
|
||||
source: {type: huggingface, repo: owner/qwen3.5-model}
|
||||
parameters:
|
||||
model: owner/qwen3.5-model
|
||||
top_p: 0.42
|
||||
`}
|
||||
|
||||
_, err = gallery.InstallModel(context.Background(), state, "", definition, nil, nil, false,
|
||||
gallery.WithArtifactMaterializer(fake))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
expectNestedDefaults(readPersistedConfig(modelsPath, definition.Name))
|
||||
})
|
||||
|
||||
It("persists defaults under parameters when the entry declares files", func() {
|
||||
modelsPath := GinkgoT().TempDir()
|
||||
state, err := system.GetSystemState(system.WithModelPath(modelsPath))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(os.WriteFile(filepath.Join(modelsPath, "weights.gguf"), []byte("weights"), 0644)).To(Succeed())
|
||||
definition := &gallery.ModelConfig{
|
||||
Name: "qwen3.5-files",
|
||||
ConfigFile: `
|
||||
backend: llama-cpp
|
||||
parameters:
|
||||
model: weights.gguf
|
||||
top_p: 0.42
|
||||
`,
|
||||
Files: []gallery.File{{Filename: "weights.gguf", URI: "https://example.com/weights.gguf"}},
|
||||
}
|
||||
|
||||
_, err = gallery.InstallModel(context.Background(), state, "", definition, nil, nil, false)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
expectNestedDefaults(readPersistedConfig(modelsPath, definition.Name))
|
||||
})
|
||||
})
|
||||
@@ -622,35 +622,41 @@ func InstallModel(ctx context.Context, systemState *system.SystemState, nameOver
|
||||
lconfig.ApplyInferenceDefaults(&modelConfig, name, modelConfig.Model)
|
||||
|
||||
// Merge inference defaults into configMap so they are persisted without losing unknown fields.
|
||||
defaults := make(map[string]any)
|
||||
if modelConfig.Temperature != nil {
|
||||
if _, exists := configMap["temperature"]; !exists {
|
||||
configMap["temperature"] = *modelConfig.Temperature
|
||||
}
|
||||
defaults["temperature"] = *modelConfig.Temperature
|
||||
}
|
||||
if modelConfig.TopP != nil {
|
||||
if _, exists := configMap["top_p"]; !exists {
|
||||
configMap["top_p"] = *modelConfig.TopP
|
||||
}
|
||||
defaults["top_p"] = *modelConfig.TopP
|
||||
}
|
||||
if modelConfig.TopK != nil {
|
||||
if _, exists := configMap["top_k"]; !exists {
|
||||
configMap["top_k"] = *modelConfig.TopK
|
||||
}
|
||||
defaults["top_k"] = *modelConfig.TopK
|
||||
}
|
||||
if modelConfig.MinP != nil {
|
||||
if _, exists := configMap["min_p"]; !exists {
|
||||
configMap["min_p"] = *modelConfig.MinP
|
||||
}
|
||||
defaults["min_p"] = *modelConfig.MinP
|
||||
}
|
||||
if modelConfig.RepeatPenalty != 0 {
|
||||
if _, exists := configMap["repeat_penalty"]; !exists {
|
||||
configMap["repeat_penalty"] = modelConfig.RepeatPenalty
|
||||
}
|
||||
defaults["repeat_penalty"] = modelConfig.RepeatPenalty
|
||||
}
|
||||
if modelConfig.PresencePenalty != 0 {
|
||||
if _, exists := configMap["presence_penalty"]; !exists {
|
||||
configMap["presence_penalty"] = modelConfig.PresencePenalty
|
||||
defaults["presence_penalty"] = modelConfig.PresencePenalty
|
||||
}
|
||||
if len(defaults) > 0 {
|
||||
parameters, ok := configMap["parameters"].(map[string]any)
|
||||
if !ok {
|
||||
parameters = make(map[string]any)
|
||||
configMap["parameters"] = parameters
|
||||
}
|
||||
for key, value := range defaults {
|
||||
if _, exists := parameters[key]; !exists {
|
||||
parameters[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updatedConfigYAML, err = yaml.Marshal(configMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal config with inference defaults: %v", err)
|
||||
}
|
||||
|
||||
if valid, err := modelConfig.Validate(); !valid {
|
||||
|
||||
@@ -38,7 +38,6 @@ var knownPrefOnlyBackends = []schema.KnownBackend{
|
||||
{Name: "whisperx", Modality: "asr", AutoDetect: false, Description: "WhisperX transcription (preference-only)"},
|
||||
{Name: "crispasr", Modality: "asr", AutoDetect: false, Description: "CrispASR multi-architecture transcription (preference-only)"},
|
||||
// TTS
|
||||
{Name: "mlx-audio", Modality: "tts", AutoDetect: false, Description: "MLX-Audio text-to-speech models (auto-detected; pref-only fallback)"},
|
||||
{Name: "kokoros", Modality: "tts", AutoDetect: false, Description: "Kokoros TTS (preference-only)"},
|
||||
{Name: "qwen-tts", Modality: "tts", AutoDetect: false, Description: "Qwen TTS (preference-only)"},
|
||||
{Name: "qwen3-tts-cpp", Modality: "tts", AutoDetect: false, Description: "Qwen3 TTS C++ (preference-only)"},
|
||||
|
||||
@@ -152,7 +152,6 @@ var _ = Describe("Backend Endpoints", func() {
|
||||
expectPrefOnly("tinygrad", "text")
|
||||
expectPrefOnly("trl", "text")
|
||||
expectPrefOnly("mlx-vlm", "text")
|
||||
expectPrefOnly("mlx-audio", "tts")
|
||||
expectPrefOnly("whisperx", "asr")
|
||||
expectPrefOnly("crispasr", "asr")
|
||||
expectPrefOnly("kokoros", "tts")
|
||||
|
||||
@@ -3,7 +3,6 @@ import { test, expect } from './coverage-fixtures.js'
|
||||
// Seeds two-message chat into localStorage so we don't need a live model.
|
||||
async function seedChat(page, history) {
|
||||
await page.addInitScript((h) => {
|
||||
if (localStorage.getItem('localai_chats_data')) return
|
||||
const chat = {
|
||||
id: 'seed1', name: 'Seeded Chat', model: 'test-model',
|
||||
history: h, systemPrompt: '', mcpMode: false, mcpServers: [],
|
||||
@@ -34,56 +33,6 @@ const TWO_TURNS = [
|
||||
{ role: 'assistant', content: 'second answer' },
|
||||
]
|
||||
|
||||
test('saved message edits persist without sending a completion request', async ({ page }) => {
|
||||
await mockModels(page)
|
||||
let completionRequests = 0
|
||||
await page.route('**/v1/chat/completions', (route) => {
|
||||
completionRequests++
|
||||
route.abort()
|
||||
})
|
||||
await seedChat(page, TWO_TURNS)
|
||||
await page.goto('/app/chat')
|
||||
|
||||
const firstUser = page.locator('.chat-message-user').first()
|
||||
await firstUser.hover()
|
||||
await firstUser.getByTitle('Edit').click()
|
||||
await firstUser.getByRole('textbox').fill('edited first question')
|
||||
await firstUser.getByRole('button', { name: 'Save' }).click()
|
||||
|
||||
const firstAssistant = page.locator('.chat-message-assistant').first()
|
||||
await firstAssistant.hover()
|
||||
await firstAssistant.getByTitle('Edit').click()
|
||||
await firstAssistant.getByRole('textbox').fill('edited first answer')
|
||||
await firstAssistant.getByRole('button', { name: 'Save' }).click()
|
||||
|
||||
await expect(firstUser).toContainText('edited first question')
|
||||
await expect(firstAssistant).toContainText('edited first answer')
|
||||
await expect.poll(() => page.evaluate(() => {
|
||||
const data = JSON.parse(localStorage.getItem('localai_chats_data'))
|
||||
return data.chats[0].history.slice(0, 2).map(message => message.content)
|
||||
})).toEqual(['edited first question', 'edited first answer'])
|
||||
|
||||
await page.reload()
|
||||
await expect(page.locator('.chat-message-user').first()).toContainText('edited first question')
|
||||
await expect(page.locator('.chat-message-assistant').first()).toContainText('edited first answer')
|
||||
expect(completionRequests).toBe(0)
|
||||
})
|
||||
|
||||
test('cancelling a message edit leaves the original content unchanged', async ({ page }) => {
|
||||
await mockModels(page)
|
||||
await seedChat(page, TWO_TURNS)
|
||||
await page.goto('/app/chat')
|
||||
|
||||
const firstUser = page.locator('.chat-message-user').first()
|
||||
await firstUser.hover()
|
||||
await firstUser.getByTitle('Edit').click()
|
||||
await firstUser.getByRole('textbox').fill('discard this draft')
|
||||
await firstUser.getByRole('button', { name: 'Cancel' }).click()
|
||||
|
||||
await expect(firstUser).toContainText('first question')
|
||||
await expect(firstUser).not.toContainText('discard this draft')
|
||||
})
|
||||
|
||||
test('duplicate creates an independent copy and switches to it', async ({ page }) => {
|
||||
await mockModels(page)
|
||||
await seedChat(page, TWO_TURNS)
|
||||
@@ -163,29 +112,6 @@ const FILE_TURNS = [
|
||||
{ role: 'assistant', content: 'nope, that is it' },
|
||||
]
|
||||
|
||||
test('editing a file prompt preserves its content blocks and attachment metadata', async ({ page }) => {
|
||||
await mockModels(page)
|
||||
await seedChat(page, FILE_TURNS)
|
||||
await page.goto('/app/chat')
|
||||
|
||||
const firstUser = page.locator('.chat-message-user').first()
|
||||
await firstUser.hover()
|
||||
await firstUser.getByTitle('Edit').click()
|
||||
await firstUser.getByRole('textbox').fill('edited file question')
|
||||
await firstUser.getByRole('button', { name: 'Save' }).click()
|
||||
|
||||
await expect.poll(() => page.evaluate(() => {
|
||||
const data = JSON.parse(localStorage.getItem('localai_chats_data'))
|
||||
return data.chats[0].history[0]
|
||||
})).toEqual({
|
||||
...FILE_TURNS[0],
|
||||
content: [
|
||||
{ type: 'text', text: 'edited file question' },
|
||||
FILE_TURNS[0].content[1],
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
test('regenerating a non-last answer in a fork still sends the uploaded file content', async ({ page }) => {
|
||||
await mockModels(page)
|
||||
let sentMessages = null
|
||||
|
||||
@@ -79,30 +79,4 @@ test.describe('Traces - bounded list and on-demand detail', () => {
|
||||
await expect(page.locator('text=hello from the response body')).toBeVisible()
|
||||
await expect(page.locator('text=203.0.113.9').first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('keeps the expanded trace open when a refresh prepends a new row', async ({ page }) => {
|
||||
await page.locator('tr', { hasText: '/v1/chat/completions' }).first().click()
|
||||
await expect(page.locator('text=hello from the request body')).toBeVisible()
|
||||
|
||||
await page.route('**/api/traces?*', (route) => {
|
||||
route.fulfill({
|
||||
contentType: 'application/json',
|
||||
headers: { 'X-Total-Count': '843' },
|
||||
body: JSON.stringify([
|
||||
{
|
||||
id: '8',
|
||||
request: { method: 'GET', path: '/v1/models', body: null },
|
||||
response: { status: 200, body: null },
|
||||
},
|
||||
...LIST_BODY,
|
||||
]),
|
||||
})
|
||||
})
|
||||
|
||||
await page.getByRole('button', { name: 'Refresh' }).click()
|
||||
|
||||
await expect(page.locator('text=hello from the request body')).toBeVisible()
|
||||
const originalRow = page.locator('tr', { hasText: '/v1/chat/completions' }).first()
|
||||
await expect(originalRow.locator('i.fa-chevron-down')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -71,10 +71,6 @@
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Kopieren",
|
||||
"edit": "Bearbeiten",
|
||||
"editMessage": "Nachricht bearbeiten",
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen",
|
||||
"regenerate": "Neu generieren",
|
||||
"jumpToLatest": "Jump to latest"
|
||||
},
|
||||
|
||||
@@ -71,10 +71,6 @@
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Copy",
|
||||
"edit": "Edit",
|
||||
"editMessage": "Edit message",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"regenerate": "Regenerate",
|
||||
"branch": "Branch from here",
|
||||
"jumpToLatest": "Jump to latest"
|
||||
|
||||
@@ -71,10 +71,6 @@
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Copiar",
|
||||
"edit": "Editar",
|
||||
"editMessage": "Editar mensaje",
|
||||
"save": "Guardar",
|
||||
"cancel": "Cancelar",
|
||||
"regenerate": "Regenerar",
|
||||
"jumpToLatest": "Jump to latest"
|
||||
},
|
||||
|
||||
@@ -71,10 +71,6 @@
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Salin",
|
||||
"edit": "Edit",
|
||||
"editMessage": "Edit pesan",
|
||||
"save": "Simpan",
|
||||
"cancel": "Batal",
|
||||
"regenerate": "Hasilkan ulang",
|
||||
"jumpToLatest": "Lompat ke terbaru"
|
||||
},
|
||||
|
||||
@@ -71,10 +71,6 @@
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Copia",
|
||||
"edit": "Modifica",
|
||||
"editMessage": "Modifica messaggio",
|
||||
"save": "Salva",
|
||||
"cancel": "Annulla",
|
||||
"regenerate": "Rigenera",
|
||||
"jumpToLatest": "Torna in fondo"
|
||||
},
|
||||
|
||||
@@ -71,10 +71,6 @@
|
||||
},
|
||||
"actions": {
|
||||
"copy": "복사",
|
||||
"edit": "편집",
|
||||
"editMessage": "메시지 편집",
|
||||
"save": "저장",
|
||||
"cancel": "취소",
|
||||
"regenerate": "다시 생성",
|
||||
"jumpToLatest": "Jump to latest"
|
||||
},
|
||||
|
||||
@@ -71,10 +71,6 @@
|
||||
},
|
||||
"actions": {
|
||||
"copy": "复制",
|
||||
"edit": "编辑",
|
||||
"editMessage": "编辑消息",
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"regenerate": "重新生成",
|
||||
"jumpToLatest": "Jump to latest"
|
||||
},
|
||||
|
||||
@@ -3540,37 +3540,6 @@ button.collapsible-header:focus-visible {
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
|
||||
.chat-message-edit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
min-width: min(32rem, 60vw);
|
||||
}
|
||||
|
||||
.chat-message-edit-input {
|
||||
width: 100%;
|
||||
min-height: 6rem;
|
||||
resize: vertical;
|
||||
border: 1px solid var(--color-primary-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-bg-primary);
|
||||
color: var(--color-text-primary);
|
||||
font: inherit;
|
||||
line-height: 1.5;
|
||||
padding: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.chat-message-edit-input:focus {
|
||||
outline: 2px solid var(--color-primary-light);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.chat-message-edit-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.chat-message-system {
|
||||
align-self: center;
|
||||
max-width: 90%;
|
||||
|
||||
@@ -287,24 +287,6 @@ function UserMessageContent({ content, files }) {
|
||||
)
|
||||
}
|
||||
|
||||
function editableMessageText(message) {
|
||||
if (typeof message.content === 'string') return message.content
|
||||
if (!Array.isArray(message.content)) return null
|
||||
const textBlock = message.content.find(block => block?.type === 'text')
|
||||
return typeof textBlock?.text === 'string' ? textBlock.text : null
|
||||
}
|
||||
|
||||
function withEditedMessageText(message, text) {
|
||||
if (typeof message.content === 'string') return { ...message, content: text }
|
||||
const textIndex = message.content.findIndex(block => block?.type === 'text')
|
||||
return {
|
||||
...message,
|
||||
content: message.content.map((block, index) =>
|
||||
index === textIndex ? { ...block, text } : block
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export default function Chat() {
|
||||
const { model: urlModel } = useParams()
|
||||
const { addToast } = useOutletContext()
|
||||
@@ -347,8 +329,6 @@ export default function Chat() {
|
||||
const [clientMCPServers, setClientMCPServers] = useState(() => loadClientMCPServers())
|
||||
const [confirmDialog, setConfirmDialog] = useState(null)
|
||||
const [completionGlowIdx, setCompletionGlowIdx] = useState(-1)
|
||||
const [editingMessageIndex, setEditingMessageIndex] = useState(null)
|
||||
const [messageEditDraft, setMessageEditDraft] = useState('')
|
||||
const prevStreamingRef = useRef(false)
|
||||
const {
|
||||
connect: mcpConnect, disconnect: mcpDisconnect, disconnectAll: mcpDisconnectAll,
|
||||
@@ -565,33 +545,6 @@ export default function Chat() {
|
||||
updateChatSettings(activeChat.id, { clientMCPServers: next })
|
||||
}, [activeChat, updateChatSettings])
|
||||
|
||||
const startMessageEdit = useCallback((index, message) => {
|
||||
const text = editableMessageText(message)
|
||||
if (text === null) return
|
||||
setEditingMessageIndex(index)
|
||||
setMessageEditDraft(text)
|
||||
}, [])
|
||||
|
||||
const cancelMessageEdit = useCallback(() => {
|
||||
setEditingMessageIndex(null)
|
||||
setMessageEditDraft('')
|
||||
}, [])
|
||||
|
||||
const saveMessageEdit = useCallback(() => {
|
||||
if (!activeChat || isStreaming || editingMessageIndex === null || !messageEditDraft.trim()) return
|
||||
const message = activeChat.history[editingMessageIndex]
|
||||
if (!message || editableMessageText(message) === null) return
|
||||
const history = activeChat.history.map((item, index) =>
|
||||
index === editingMessageIndex ? withEditedMessageText(item, messageEditDraft) : item
|
||||
)
|
||||
updateChatSettings(activeChat.id, { history })
|
||||
cancelMessageEdit()
|
||||
}, [activeChat, isStreaming, editingMessageIndex, messageEditDraft, updateChatSettings, cancelMessageEdit])
|
||||
|
||||
useEffect(() => {
|
||||
cancelMessageEdit()
|
||||
}, [activeChat?.id, isStreaming, cancelMessageEdit])
|
||||
|
||||
// Load initial message from home page
|
||||
const homeDataProcessed = useRef(false)
|
||||
useEffect(() => {
|
||||
@@ -1217,80 +1170,40 @@ export default function Chat() {
|
||||
{msg.role === 'assistant' && activeChat.model && (
|
||||
<span className="chat-message-model">{activeChat.model}</span>
|
||||
)}
|
||||
{editingMessageIndex === i ? (
|
||||
<div className="chat-message-edit">
|
||||
<textarea
|
||||
autoFocus
|
||||
className="chat-message-edit-input"
|
||||
value={messageEditDraft}
|
||||
onChange={(event) => setMessageEditDraft(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') cancelMessageEdit()
|
||||
}}
|
||||
aria-label={t('actions.editMessage')}
|
||||
/>
|
||||
<div className="chat-message-edit-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={saveMessageEdit}
|
||||
disabled={!messageEditDraft.trim()}
|
||||
>
|
||||
{t('actions.save')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={cancelMessageEdit}
|
||||
>
|
||||
{t('actions.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="chat-message-content">
|
||||
{msg.role === 'user' ? (
|
||||
<UserMessageContent content={msg.content} files={msg.files} />
|
||||
) : (
|
||||
<div dangerouslySetInnerHTML={{
|
||||
__html: canvasMode
|
||||
? renderMarkdownWithArtifacts(typeof msg.content === 'string' ? msg.content : '', i)
|
||||
: renderMarkdown(typeof msg.content === 'string' ? msg.content : '')
|
||||
}} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="chat-message-content">
|
||||
{msg.role === 'user' ? (
|
||||
<UserMessageContent content={msg.content} files={msg.files} />
|
||||
) : (
|
||||
<div dangerouslySetInnerHTML={{
|
||||
__html: canvasMode
|
||||
? renderMarkdownWithArtifacts(typeof msg.content === 'string' ? msg.content : '', i)
|
||||
: renderMarkdown(typeof msg.content === 'string' ? msg.content : '')
|
||||
}} />
|
||||
)}
|
||||
</div>
|
||||
{msg.role === 'assistant' && typeof msg.content === 'string' && msg.content.includes('Error:') && (
|
||||
<a href="/app/traces?tab=backend" className="chat-error-trace-link">
|
||||
<i className="fas fa-wave-square" /> {t('errors.viewTraces')}
|
||||
</a>
|
||||
)}
|
||||
{editingMessageIndex !== i && (
|
||||
<div className="chat-message-actions">
|
||||
<button onClick={() => copyMessage(msg.content)} title={t('actions.copy')}>
|
||||
<i className="fas fa-copy" />
|
||||
<div className="chat-message-actions">
|
||||
<button onClick={() => copyMessage(msg.content)} title={t('actions.copy')}>
|
||||
<i className="fas fa-copy" />
|
||||
</button>
|
||||
{msg.role === 'assistant' && !isStreaming && (
|
||||
<button onClick={() => handleRegenerate(i)} title={t('actions.regenerate')}>
|
||||
<i className="fas fa-rotate" />
|
||||
</button>
|
||||
{(msg.role === 'user' || msg.role === 'assistant') &&
|
||||
editableMessageText(msg) !== null && !isStreaming && (
|
||||
<button onClick={() => startMessageEdit(i, msg)} title={t('actions.edit')}>
|
||||
<i className="fas fa-pen" />
|
||||
</button>
|
||||
)}
|
||||
{msg.role === 'assistant' && !isStreaming && (
|
||||
<button onClick={() => handleRegenerate(i)} title={t('actions.regenerate')}>
|
||||
<i className="fas fa-rotate" />
|
||||
</button>
|
||||
)}
|
||||
{msg.role === 'assistant' && !isStreaming && (
|
||||
<button
|
||||
onClick={() => { forkChat(activeChat.id, i + 1); addToast(t('toasts.forked'), 'success', 2000) }}
|
||||
title={t('actions.branch')}
|
||||
>
|
||||
<i className="fas fa-code-branch" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
{msg.role === 'assistant' && !isStreaming && (
|
||||
<button
|
||||
onClick={() => { forkChat(activeChat.id, i + 1); addToast(t('toasts.forked'), 'success', 2000) }}
|
||||
title={t('actions.branch')}
|
||||
>
|
||||
<i className="fas fa-code-branch" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -342,7 +342,7 @@ export default function Traces() {
|
||||
const [apiCount, setApiCount] = useState(0)
|
||||
const [backendCount, setBackendCount] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [expandedTraceId, setExpandedTraceId] = useState(null)
|
||||
const [expandedRow, setExpandedRow] = useState(null)
|
||||
// detail holds the full record for the currently expanded row, fetched on
|
||||
// demand from /api/traces/:id (the list response omits the bodies).
|
||||
const [detail, setDetail] = useState(null)
|
||||
@@ -360,8 +360,7 @@ export default function Traces() {
|
||||
duration: (a, b) => (a.duration || 0) - (b.duration || 0),
|
||||
}
|
||||
const toggleSort = (key) => {
|
||||
setExpandedTraceId(null)
|
||||
setDetail(null)
|
||||
setExpandedRow(null)
|
||||
setSort(s => s.key === key ? { key, dir: s.dir === 'asc' ? 'desc' : 'asc' } : { key, dir: 'asc' })
|
||||
}
|
||||
const sortableTh = (key, label, props = {}) => (
|
||||
@@ -434,21 +433,20 @@ export default function Traces() {
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
setExpandedTraceId(null)
|
||||
setExpandedRow(null)
|
||||
setDetail(null)
|
||||
fetchTraces()
|
||||
}, [fetchTraces])
|
||||
|
||||
// Expanding a row pulls the full record (bodies, data fields, audio
|
||||
// snippets) that the list response deliberately omits.
|
||||
const toggleRow = useCallback(async (row, index) => {
|
||||
const traceKey = row?.id ?? index
|
||||
if (expandedTraceId === traceKey) {
|
||||
setExpandedTraceId(null)
|
||||
const toggleRow = useCallback(async (index, row) => {
|
||||
if (expandedRow === index) {
|
||||
setExpandedRow(null)
|
||||
setDetail(null)
|
||||
return
|
||||
}
|
||||
setExpandedTraceId(traceKey)
|
||||
setExpandedRow(index)
|
||||
setDetail(null)
|
||||
if (!row?.id) return
|
||||
try {
|
||||
@@ -459,7 +457,7 @@ export default function Traces() {
|
||||
} catch {
|
||||
// Fall back to the summary view; the row still renders what it has.
|
||||
}
|
||||
}, [expandedTraceId, activeTab])
|
||||
}, [expandedRow, activeTab])
|
||||
|
||||
// Auto-refresh every 5 seconds
|
||||
useEffect(() => {
|
||||
@@ -472,7 +470,7 @@ export default function Traces() {
|
||||
if (activeTab === 'api') await tracesApi.clear()
|
||||
else await tracesApi.clearBackend()
|
||||
setTraces([])
|
||||
setExpandedTraceId(null)
|
||||
setExpandedRow(null)
|
||||
setDetail(null)
|
||||
addToast('Traces cleared', 'success')
|
||||
} catch (err) {
|
||||
@@ -502,7 +500,7 @@ export default function Traces() {
|
||||
}
|
||||
|
||||
// Reset sort + expansion when switching trace tabs (columns differ).
|
||||
useEffect(() => { setSort({ key: null, dir: 'asc' }); setExpandedTraceId(null); setDetail(null) }, [activeTab])
|
||||
useEffect(() => { setSort({ key: null, dir: 'asc' }); setExpandedRow(null); setDetail(null) }, [activeTab])
|
||||
|
||||
const sortedTraces = sort.key && TRACE_SORT[sort.key]
|
||||
? [...traces].sort((a, b) => sort.dir === 'asc' ? TRACE_SORT[sort.key](a, b) : TRACE_SORT[sort.key](b, a))
|
||||
@@ -637,9 +635,9 @@ export default function Traces() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedTraces.map((trace, i) => (
|
||||
<React.Fragment key={trace.id ?? i}>
|
||||
<tr onClick={() => toggleRow(trace, i)} className="clickable">
|
||||
<td><i className={`fas fa-chevron-${expandedTraceId === (trace.id ?? i) ? 'down' : 'right'} text-xs`} /></td>
|
||||
<React.Fragment key={i}>
|
||||
<tr onClick={() => toggleRow(i, trace)} className="clickable">
|
||||
<td><i className={`fas fa-chevron-${expandedRow === i ? 'down' : 'right'} text-xs`} /></td>
|
||||
<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>
|
||||
@@ -650,7 +648,7 @@ export default function Traces() {
|
||||
: <i className="fas fa-check-circle text-success" />}
|
||||
</td>
|
||||
</tr>
|
||||
{expandedTraceId === (trace.id ?? i) && (
|
||||
{expandedRow === i && (
|
||||
<tr>
|
||||
<td colSpan="6" className="p-0">
|
||||
<ApiTraceDetail trace={detail && detail.id === trace.id ? detail : trace} />
|
||||
@@ -676,9 +674,9 @@ export default function Traces() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedTraces.map((trace, i) => (
|
||||
<React.Fragment key={trace.id ?? i}>
|
||||
<tr onClick={() => toggleRow(trace, i)} className="clickable">
|
||||
<td><i className={`fas fa-chevron-${expandedTraceId === (trace.id ?? i) ? 'down' : 'right'} text-xs`} /></td>
|
||||
<React.Fragment key={i}>
|
||||
<tr onClick={() => toggleRow(i, trace)} className="clickable">
|
||||
<td><i className={`fas fa-chevron-${expandedRow === i ? 'down' : 'right'} text-xs`} /></td>
|
||||
<td><span style={typeBadgeStyle(trace.type)}>{trace.type || '-'}</span></td>
|
||||
<td className="text-sub nowrap">{formatDateTime(trace.timestamp)}</td>
|
||||
<td className="text-mono text-sm">{trace.model_name || '-'}</td>
|
||||
@@ -692,7 +690,7 @@ export default function Traces() {
|
||||
: <i className="fas fa-check-circle text-success" />}
|
||||
</td>
|
||||
</tr>
|
||||
{expandedTraceId === (trace.id ?? i) && (
|
||||
{expandedRow === i && (
|
||||
<tr>
|
||||
<td colSpan="7" className="p-0">
|
||||
<BackendTraceDetail trace={detail && detail.id === trace.id ? detail : trace} />
|
||||
|
||||
@@ -27,17 +27,9 @@ Building and running the site locally requires a recent `extended` version of [H
|
||||
You can find out more about how to install Hugo for your environment in our
|
||||
[Getting started](https://www.docsy.dev/docs/getting-started/#prerequisites-and-installation) guide.
|
||||
|
||||
From the LocalAI repository root, run:
|
||||
Once you've made your working copy of the site repo, from the repo root folder, run:
|
||||
|
||||
```bash
|
||||
make docs
|
||||
```
|
||||
|
||||
The Hugo configuration lives in the `docs` directory. To invoke Hugo
|
||||
directly instead, run:
|
||||
|
||||
```bash
|
||||
cd docs
|
||||
hugo server
|
||||
```
|
||||
|
||||
|
||||
@@ -329,23 +329,7 @@ This configuration has been tested on a 'custom' cluster managed by SUSE Rancher
|
||||
|
||||
### Requirements
|
||||
|
||||
You need a machine with an Intel GPU and a kernel that drives it, which every current Linux kernel does. You do not need to install any Intel graphics packages: the backends carry their own copy of the Intel graphics driver, so they work on a machine that has none installed, and on a machine whose own driver was built against a newer C library than the backend.
|
||||
|
||||
If you build from source instead of using the images, you need the [Intel oneAPI Base Toolkit](https://software.intel.com/content/www/us/en/develop/tools/oneapi/base-toolkit/download.html).
|
||||
|
||||
#### Using your own Intel driver instead
|
||||
|
||||
The carried driver comes from Intel's own package repository, so it knows the cards released up to the point the image was built. If your GPU is newer than that, or you would rather use the driver your distribution ships, point the backend at it:
|
||||
|
||||
```bash
|
||||
docker run --rm -ti --device /dev/dri -p 8080:8080 \
|
||||
-e ZE_ENABLE_ALT_DRIVERS=/usr/lib/x86_64-linux-gnu/libze_intel_gpu.so.1 \
|
||||
-v $PWD/models:/models quay.io/go-skynet/local-ai:{{< version >}}-gpu-intel
|
||||
```
|
||||
|
||||
Set the path to wherever your distribution keeps that file. Whatever you set is used as is, and the carried driver is left alone.
|
||||
|
||||
The backends carry only the driver Level Zero uses, which is how llama.cpp reaches an Intel GPU. They do not carry an OpenCL driver, so OpenCL inside a container continues to use whatever the image itself provides.
|
||||
If building from source, you need to install [Intel oneAPI Base Toolkit](https://software.intel.com/content/www/us/en/develop/tools/oneapi/base-toolkit/download.html) and have the Intel drivers available in the system.
|
||||
|
||||
### Container images
|
||||
|
||||
@@ -371,8 +355,6 @@ docker run --rm -ti --device /dev/dri -p 8080:8080 -e DEBUG=true -e MODELS_PATH=
|
||||
|
||||
Note also that sycl does have a known issue to hang with `mmap: true`. You have to disable it in the model configuration if explicitly enabled.
|
||||
|
||||
On an integrated Intel GPU, the amount of free graphics memory can only be read if the driver is asked to report it. The backends do that for you by setting `ZES_ENABLE_SYSMAN=1`. If you set that variable yourself, your value is kept, and setting it to `0` makes the backend read zero free memory, because an integrated GPU has no memory of its own and shares the system's.
|
||||
|
||||
## Vulkan acceleration
|
||||
|
||||
### Requirements
|
||||
@@ -474,7 +456,7 @@ sycl-ls
|
||||
|
||||
- **NVIDIA**: Ensure `nvidia-container-toolkit` is installed and the Docker runtime is configured. Test with `docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi`.
|
||||
- **AMD**: Ensure `/dev/dri` and `/dev/kfd` are passed to the container and that `amdgpu-dkms` is installed on the host.
|
||||
- **Intel**: Ensure `/dev/dri` is passed to the container. No Intel graphics packages are needed on the host, since the backends bring their own driver. If the GPU is a recent model that the carried driver does not know, point the backend at the host's own driver as shown in [Intel acceleration](#intel-acceleration-sycl).
|
||||
- **Intel**: Ensure `/dev/dri` is passed to the container and Intel GPU drivers are installed on the host.
|
||||
|
||||
### Model loads on CPU instead of GPU
|
||||
|
||||
|
||||
@@ -80,12 +80,6 @@ The WebUI provides a powerful model import interface that supports both simple a
|
||||
- Custom preferences
|
||||
5. Click "Import Model" to start the import process
|
||||
|
||||
Repositories under `mlx-community` are imported with the native MLX backend.
|
||||
LocalAI uses Hugging Face's pipeline metadata to select `mlx-vlm` for
|
||||
vision-language models and `mlx-audio` for text-to-speech models; other MLX
|
||||
repositories use `mlx`. An explicit backend selection in the import form always
|
||||
overrides this automatic routing.
|
||||
|
||||
### Advanced Import Mode
|
||||
|
||||
For full control over model configuration:
|
||||
|
||||
@@ -63,10 +63,6 @@ To get your first chat working:
|
||||
1. Open the **Models** page and search for `qwen3-4b`. Click **Install** on the `qwen3-4b` entry and wait for the download to finish. (`qwen3-4b` is a small, CPU-friendly Qwen3 model that also supports tool calling, so you can reuse it later in the [Build your first agent]({{% relref "getting-started/first-agent" %}}) walkthrough.)
|
||||
2. Open the **Chat** page, select `qwen3-4b` from the model dropdown, type a message, and send it. You should get a reply within a few seconds.
|
||||
|
||||
To correct an earlier prompt or response without running the model again, hover
|
||||
over the saved message and select **Edit**. **Save** updates that conversation's
|
||||
local history; **Cancel** discards the draft.
|
||||
|
||||
### Downloading models from the CLI
|
||||
|
||||
When starting LocalAI (either via Docker or via CLI) you can specify as argument a list of models to install automatically before starting the API, for example:
|
||||
|
||||
@@ -1,48 +1,4 @@
|
||||
---
|
||||
- name: "deepseek-v4-flash-0731"
|
||||
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
||||
urls:
|
||||
- https://huggingface.co/unsloth/DeepSeek-V4-Flash-0731-GGUF
|
||||
description: |
|
||||
# DeepSeek-V4-Flash-0731
|
||||
|
||||
Technical Report👁️
|
||||
|
||||
## Introduction
|
||||
|
||||
**DeepSeek-V4-Flash-0731** is the official release of **DeepSeek-V4-Flash**, superseding the preview version, with substantially enhanced agentic capabilities. It has the same model structure as DeepSeek-V4-Flash-DSpark, i.e. it comes with a speculative decoding module attached.
|
||||
|
||||
DeepSeek-V4-Flash-0731 outperforms DeepSeek-V4-Pro (Preview) on benchmarks listed below despite its far smaller activated parameter count, and is broadly competitive with the strongest proprietary models available.
|
||||
|
||||
Notes:
|
||||
|
||||
1. For the Code Agent tasks among the public benchmarks above, DeepSeek-V4-Flash-0731 is evaluated with the minimal mode of DeepSeek Harness (to be released) as the agent framework, using the `max` reasoning effort level with `temperature = 1.0, top_p = 0.95`.
|
||||
2. † DSBench-FullStack is an internal full-stack development test set; DSBench-Hard is an internal test set of difficult coding-agent problems.
|
||||
|
||||
## Chat Template
|
||||
|
||||
...
|
||||
license: "mit"
|
||||
tags:
|
||||
- llm
|
||||
- gguf
|
||||
- deepseek
|
||||
icon: https://github.com/deepseek-ai/DeepSeek-V2/blob/main/figures/logo.svg
|
||||
overrides:
|
||||
backend: ds4
|
||||
function:
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
parameters:
|
||||
model: ds4flash.gguf
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: ds4flash.gguf
|
||||
sha256: ""
|
||||
uri: https://huggingface.co/unsloth/DeepSeek-V4-Flash-0731-GGUF
|
||||
- name: "parable-granite-4.1-3b-claude-fable-5"
|
||||
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
||||
urls:
|
||||
@@ -114,7 +70,7 @@
|
||||
files:
|
||||
- filename: llama-cpp/models/Parable-Qwen3-4B-Claude-Fable-5-Q4_K_M/Parable-Qwen3-4B-Claude-Fable-5-GGUF-Q4_K_M.gguf
|
||||
uri: https://huggingface.co/AnkitAI/Parable-Qwen3-4B-Claude-Fable-5-GGUF/resolve/main/Parable-Qwen3-4B-Claude-Fable-5-GGUF-Q4_K_M.gguf
|
||||
sha256: c94b06a912aa901f3da5689754577ad534415efafc50dcee3f389594a153bf38
|
||||
sha256: c6f991bd243fd1449d50a58a8de7d26bcad35d908bc0193e5a2c2c56ac6b8d5f
|
||||
- name: "parable-granite-4.1-8b-claude-fable-5"
|
||||
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
||||
urls:
|
||||
@@ -941,90 +897,6 @@
|
||||
- filename: llama-cpp/mmproj/inkling-UD-Q4_K_XL/mmproj-BF16.gguf
|
||||
sha256: 662c925e1df293cfba16ffd6bd53dac31d3c73160ba65dff7270d7a70f351e91
|
||||
uri: https://huggingface.co/unsloth/inkling-GGUF/resolve/main/mmproj-BF16.gguf
|
||||
- &inkling-small
|
||||
name: "inkling-small"
|
||||
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
||||
urls:
|
||||
- https://huggingface.co/thinkingmachines/Inkling-Small
|
||||
- https://huggingface.co/unsloth/Inkling-Small-GGUF
|
||||
description: |
|
||||
Inkling Small is a 276B-parameter mixture-of-experts multimodal model with 12B active parameters for text, image, and audio understanding, instruction following, coding, and tool use. This entry uses the Q4_K_M GGUF quantization, whose five language-model shards total approximately 162.5 GB.
|
||||
license: "apache-2.0"
|
||||
tags:
|
||||
- llm
|
||||
- gguf
|
||||
- vision
|
||||
- audio
|
||||
- multimodal
|
||||
overrides:
|
||||
backend: llama-cpp
|
||||
function:
|
||||
automatic_tool_parsing_fallback: true
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
mmproj: llama-cpp/mmproj/Inkling-Small-UD-Q4_K_M/mmproj-BF16.gguf
|
||||
options:
|
||||
- use_jinja:true
|
||||
parameters:
|
||||
model: llama-cpp/models/Inkling-Small-UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00001-of-00005.gguf
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: llama-cpp/models/Inkling-Small-UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00001-of-00005.gguf
|
||||
sha256: a51ac3f439198f2817219edd582be4b600c273be24e78cbd58ebff982d9f007e
|
||||
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00001-of-00005.gguf
|
||||
- filename: llama-cpp/models/Inkling-Small-UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00002-of-00005.gguf
|
||||
sha256: 3dccdd473cc3a191e6028f6105b01831ed3a8dc30ec4e02679f9e0c2ccb59671
|
||||
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00002-of-00005.gguf
|
||||
- filename: llama-cpp/models/Inkling-Small-UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00003-of-00005.gguf
|
||||
sha256: 1a7edf29bda1d278b4668e1a082d7db634b845a53ea58c918a8cea1f9006c21c
|
||||
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00003-of-00005.gguf
|
||||
- filename: llama-cpp/models/Inkling-Small-UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00004-of-00005.gguf
|
||||
sha256: 376f67568438da96b10566730e8a9e17e3f665ab1b9d82eba48ec33708b172f7
|
||||
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00004-of-00005.gguf
|
||||
- filename: llama-cpp/models/Inkling-Small-UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00005-of-00005.gguf
|
||||
sha256: e34364af0d04d2d295bc374f1a4fa80002e4277db1bcf4b56240dbd443ac21d3
|
||||
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/UD-Q4_K_M/Inkling-Small-UD-Q4_K_M-00005-of-00005.gguf
|
||||
- filename: llama-cpp/mmproj/Inkling-Small-UD-Q4_K_M/mmproj-BF16.gguf
|
||||
sha256: 05d4475a956030be87b099865d6552a541a476db8cc3e266fcfa7c5a24846248
|
||||
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/mmproj-BF16.gguf
|
||||
variants:
|
||||
- model: inkling-small-iq2-m
|
||||
- !!merge <<: *inkling-small
|
||||
name: "inkling-small-iq2-m"
|
||||
description: |
|
||||
Inkling Small is a 276B-parameter mixture-of-experts multimodal model with 12B active parameters for text, image, and audio understanding, instruction following, coding, and tool use. This entry uses the IQ2_M GGUF quantization, whose three language-model shards total approximately 82.4 GB.
|
||||
overrides:
|
||||
backend: llama-cpp
|
||||
function:
|
||||
automatic_tool_parsing_fallback: true
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
mmproj: llama-cpp/mmproj/Inkling-Small-UD-IQ2_M/mmproj-BF16.gguf
|
||||
options:
|
||||
- use_jinja:true
|
||||
parameters:
|
||||
model: llama-cpp/models/Inkling-Small-UD-IQ2_M/Inkling-Small-UD-IQ2_M-00001-of-00003.gguf
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: llama-cpp/models/Inkling-Small-UD-IQ2_M/Inkling-Small-UD-IQ2_M-00001-of-00003.gguf
|
||||
sha256: 3b6ace30e488ad26e816cdba4e42714f40110a3142a210bd5c2e48f69e27cb31
|
||||
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/UD-IQ2_M/Inkling-Small-UD-IQ2_M-00001-of-00003.gguf
|
||||
- filename: llama-cpp/models/Inkling-Small-UD-IQ2_M/Inkling-Small-UD-IQ2_M-00002-of-00003.gguf
|
||||
sha256: 5ca94e858ae116eb513a2af1facd35844d42ef4a209e1d85cc4ecc73cd21b894
|
||||
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/UD-IQ2_M/Inkling-Small-UD-IQ2_M-00002-of-00003.gguf
|
||||
- filename: llama-cpp/models/Inkling-Small-UD-IQ2_M/Inkling-Small-UD-IQ2_M-00003-of-00003.gguf
|
||||
sha256: 8a84e00d4625d52491969f88f24f0999bc77527b7fa1d15cbe44ef88235bb377
|
||||
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/UD-IQ2_M/Inkling-Small-UD-IQ2_M-00003-of-00003.gguf
|
||||
- filename: llama-cpp/mmproj/Inkling-Small-UD-IQ2_M/mmproj-BF16.gguf
|
||||
sha256: 05d4475a956030be87b099865d6552a541a476db8cc3e266fcfa7c5a24846248
|
||||
uri: https://huggingface.co/unsloth/Inkling-Small-GGUF/resolve/main/mmproj-BF16.gguf
|
||||
variants: []
|
||||
- name: "qwythos-9b-v2"
|
||||
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
||||
urls:
|
||||
@@ -1124,74 +996,6 @@
|
||||
- filename: llama-cpp/mmproj/Qwen3.6-27B-Fable-Fus-711-UnHeretic-NM-DAU-NEO-MAX-NEO-MTP-Q4_K_M/mmproj-F32.gguf
|
||||
sha256: fdc443e974cad1f61c45af1cfd5580855855ddce0d6c14cc500a5714c486ac1d
|
||||
uri: https://huggingface.co/DavidAU/Qwen3.6-27B-Fable-Fusion-711-Uncensored-Heretic-NM-DAU-NEO-MAX-MTP-GGUF/resolve/main/mmproj-F32.gguf
|
||||
- &qwopus3-6-27b-fusion
|
||||
name: "qwopus3.6-27b-fusion"
|
||||
variants:
|
||||
- model: qwopus3.6-27b-fusion-q8
|
||||
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
||||
urls:
|
||||
- https://huggingface.co/Qwen/Qwen3.6-27B
|
||||
- https://huggingface.co/KyleHessling1/Qwopus3.6-27B-Fusion-GGUF
|
||||
description: |
|
||||
Qwopus3.6-27B Fusion is an experimental 27B Qwen3.6 merge that combines the
|
||||
Qwopus reasoning and coding fine-tunes. It targets agentic coding and
|
||||
general reasoning, supports the Qwen3.6 262K context window, and retains
|
||||
the base model's MTP head. This default entry uses the 16.8 GB Q4_K_M GGUF.
|
||||
license: qwen
|
||||
tags:
|
||||
- llm
|
||||
- gguf
|
||||
- reasoning
|
||||
- thinking
|
||||
- mtp
|
||||
overrides:
|
||||
backend: llama-cpp
|
||||
function:
|
||||
automatic_tool_parsing_fallback: true
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
options:
|
||||
- use_jinja:true
|
||||
- spec_type:draft-mtp
|
||||
- spec_n_max:6
|
||||
- spec_p_min:0.75
|
||||
parameters:
|
||||
model: llama-cpp/models/Qwopus3.6-27B-Fusion-Q4_K_M/Qwopus3.6-27B-Fusion-Q4_K_M.gguf
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: llama-cpp/models/Qwopus3.6-27B-Fusion-Q4_K_M/Qwopus3.6-27B-Fusion-Q4_K_M.gguf
|
||||
sha256: 6d4e6e9ffba84ffe5166969303d4918f3a31682d75862416827412cee8f20d5d
|
||||
uri: huggingface://KyleHessling1/Qwopus3.6-27B-Fusion-GGUF/Qwopus3.6-27B-Fusion-Q4_K_M.gguf
|
||||
- !!merge <<: *qwopus3-6-27b-fusion
|
||||
name: "qwopus3.6-27b-fusion-q8"
|
||||
variants: null
|
||||
description: |
|
||||
Qwopus3.6-27B Fusion in the higher-fidelity 29.0 GB Q8_0 GGUF format.
|
||||
It retains the base model's MTP head for speculative decoding.
|
||||
overrides:
|
||||
backend: llama-cpp
|
||||
function:
|
||||
automatic_tool_parsing_fallback: true
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
options:
|
||||
- use_jinja:true
|
||||
- spec_type:draft-mtp
|
||||
- spec_n_max:6
|
||||
- spec_p_min:0.75
|
||||
parameters:
|
||||
model: llama-cpp/models/Qwopus3.6-27B-Fusion-Q8_0/Qwopus3.6-27B-Fusion-Q8_0.gguf
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: llama-cpp/models/Qwopus3.6-27B-Fusion-Q8_0/Qwopus3.6-27B-Fusion-Q8_0.gguf
|
||||
sha256: 5594e1776b75beedf4a54b933bba386dc83a0883417e3bcd9ef53fdfd120d5b6
|
||||
uri: huggingface://KyleHessling1/Qwopus3.6-27B-Fusion-GGUF/Qwopus3.6-27B-Fusion-Q8_0.gguf
|
||||
- name: "minicpm5-1b-claude-opus-fable5-v2-thinking"
|
||||
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
||||
urls:
|
||||
@@ -1605,7 +1409,7 @@
|
||||
files:
|
||||
- filename: ds4flash.gguf
|
||||
uri: https://huggingface.co/unsloth/DeepSeek-V4-Flash-GGUF
|
||||
sha256: 7d6d1691bc2d02c5a8194afb8bd9b57519343afebf020231c61d9236504b4a5c
|
||||
sha256: ea2be54e4e989cb8cc2a88c0791b15eaa50eb97fb46a89843f6415d7e1a73e33
|
||||
- name: "qwopus3.6-35b-a3b-coder-mtp"
|
||||
url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
|
||||
urls:
|
||||
@@ -5390,82 +5194,6 @@
|
||||
- filename: llama-cpp/models/Qwen3.5-35B-A3B-APEX-GGUF/Qwen3.5-35B-A3B-APEX-Quality.gguf
|
||||
sha256: 50887b60c77ee5c95bc3657814ae993abcab7b2d71868b9af1e84d6badd09a57
|
||||
uri: https://huggingface.co/mudler/Qwen3.5-35B-A3B-APEX-GGUF/resolve/main/Qwen3.5-35B-A3B-APEX-Quality.gguf
|
||||
- &fara1-5-9b
|
||||
name: fara1.5-9b
|
||||
url: github:mudler/LocalAI/gallery/virtual.yaml@master
|
||||
variants:
|
||||
- model: fara1.5-9b-q8
|
||||
urls:
|
||||
- https://huggingface.co/microsoft/Fara1.5-9B
|
||||
- https://huggingface.co/bartowski/Fara1.5-9B-GGUF
|
||||
description: |
|
||||
Fara1.5-9B is Microsoft's 9B-parameter multimodal computer-use agent for web browsers, fine-tuned from Qwen3.5-9B. It accepts screenshots and text, emits structured browser actions, supports a 262K-token context, and should be deployed with appropriate sandboxing and user-confirmation controls. This entry uses the recommended Q4_K_M GGUF quantization.
|
||||
license: mit
|
||||
tags:
|
||||
- fara
|
||||
- qwen
|
||||
- qwen3.5
|
||||
- 9b
|
||||
- llm
|
||||
- gguf
|
||||
- quantized
|
||||
- chat
|
||||
- vision
|
||||
- multimodal
|
||||
- agent
|
||||
- computer-use
|
||||
- gpu
|
||||
- cpu
|
||||
last_checked: "2026-08-01"
|
||||
overrides:
|
||||
backend: llama-cpp
|
||||
function:
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
- vision
|
||||
mmproj: llama-cpp/mmproj/Fara1.5-9B-GGUF/mmproj-Fara1.5-9B-f16.gguf
|
||||
options:
|
||||
- use_jinja:true
|
||||
parameters:
|
||||
model: llama-cpp/models/Fara1.5-9B-GGUF/Fara1.5-9B-Q4_K_M.gguf
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: llama-cpp/models/Fara1.5-9B-GGUF/Fara1.5-9B-Q4_K_M.gguf
|
||||
sha256: a02e7220337b87290bca7ef7225ba4afa7104efa05c0851de6c29ec9c5d04c7d
|
||||
uri: huggingface://bartowski/Fara1.5-9B-GGUF/Fara1.5-9B-Q4_K_M.gguf
|
||||
- filename: llama-cpp/mmproj/Fara1.5-9B-GGUF/mmproj-Fara1.5-9B-f16.gguf
|
||||
sha256: 97b423c81719ffc367124a9739d6feb6f62d62f60869a6d385a701b963ce1906
|
||||
uri: huggingface://bartowski/Fara1.5-9B-GGUF/mmproj-Fara1.5-9B-f16.gguf
|
||||
- !!merge <<: *fara1-5-9b
|
||||
name: fara1.5-9b-q8
|
||||
variants: []
|
||||
description: |
|
||||
Fara1.5-9B is Microsoft's 9B-parameter multimodal computer-use agent for web browsers, fine-tuned from Qwen3.5-9B. It accepts screenshots and text, emits structured browser actions, supports a 262K-token context, and should be deployed with appropriate sandboxing and user-confirmation controls. This entry uses the higher-quality Q8_0 GGUF quantization.
|
||||
overrides:
|
||||
backend: llama-cpp
|
||||
function:
|
||||
grammar:
|
||||
disable: true
|
||||
known_usecases:
|
||||
- chat
|
||||
- vision
|
||||
mmproj: llama-cpp/mmproj/Fara1.5-9B-GGUF/mmproj-Fara1.5-9B-f16.gguf
|
||||
options:
|
||||
- use_jinja:true
|
||||
parameters:
|
||||
model: llama-cpp/models/Fara1.5-9B-GGUF/Fara1.5-9B-Q8_0.gguf
|
||||
template:
|
||||
use_tokenizer_template: true
|
||||
files:
|
||||
- filename: llama-cpp/models/Fara1.5-9B-GGUF/Fara1.5-9B-Q8_0.gguf
|
||||
sha256: a2e30cca7aec006266308153ae781347505af16baa514bbd4e0e3f4a79ea3a22
|
||||
uri: huggingface://bartowski/Fara1.5-9B-GGUF/Fara1.5-9B-Q8_0.gguf
|
||||
- filename: llama-cpp/mmproj/Fara1.5-9B-GGUF/mmproj-Fara1.5-9B-f16.gguf
|
||||
sha256: 97b423c81719ffc367124a9739d6feb6f62d62f60869a6d385a701b963ce1906
|
||||
uri: huggingface://bartowski/Fara1.5-9B-GGUF/mmproj-Fara1.5-9B-f16.gguf
|
||||
- name: fara1.5-27b
|
||||
url: github:mudler/LocalAI/gallery/virtual.yaml@master
|
||||
variants:
|
||||
@@ -6214,14 +5942,14 @@
|
||||
- instruction-tuned
|
||||
- code
|
||||
- math
|
||||
last_checked: "2026-08-01"
|
||||
last_checked: "2026-07-28"
|
||||
overrides:
|
||||
parameters:
|
||||
model: Nanbeige4.2-3B-Q8_0.gguf
|
||||
model: nanbeige4.2-3b-Q8_0.gguf
|
||||
files:
|
||||
- filename: Nanbeige4.2-3B-Q8_0.gguf
|
||||
sha256: 4f8bd17cdf58bea2a94aef03457e0b8f019c26fe4daee7ae49b61bfa935a9126
|
||||
uri: huggingface://owao/Nanbeige4.2-3B-GGUF/Nanbeige4.2-3B-Q8_0.gguf
|
||||
- filename: nanbeige4.2-3b-Q8_0.gguf
|
||||
sha256: 44707bb25e7ba3f2b0b5f3c2311da95ee3676986dd2014ce2aaeb14113590e33
|
||||
uri: huggingface://owao/Nanbeige4.2-3B-GGUF/nanbeige4.2-3b-Q8_0.gguf
|
||||
- name: nanbeige4.2-3b
|
||||
url: github:mudler/LocalAI/gallery/nanbeige4.2.yaml@master
|
||||
urls:
|
||||
@@ -6244,16 +5972,16 @@
|
||||
- instruction-tuned
|
||||
- code
|
||||
- math
|
||||
last_checked: "2026-08-01"
|
||||
last_checked: "2026-07-28"
|
||||
variants:
|
||||
- model: nanbeige4.2-3b-q8
|
||||
overrides:
|
||||
parameters:
|
||||
model: Nanbeige4.2-3B-Q4_K_M.gguf
|
||||
model: nanbeige4.2-3b-Q4_K_M.gguf
|
||||
files:
|
||||
- filename: Nanbeige4.2-3B-Q4_K_M.gguf
|
||||
sha256: ffe1b9b8ee95ec4b962c379905aa8be6f72ae9c4645c6c70e3b6ff7b197e6ef4
|
||||
uri: huggingface://owao/Nanbeige4.2-3B-GGUF/Nanbeige4.2-3B-Q4_K_M.gguf
|
||||
- filename: nanbeige4.2-3b-Q4_K_M.gguf
|
||||
sha256: 9ffd17d14472ff208409b3f51a6d87a5e5ec1b878b9a6f4dfe15c2a883366104
|
||||
uri: huggingface://owao/Nanbeige4.2-3B-GGUF/nanbeige4.2-3b-Q4_K_M.gguf
|
||||
- name: nemo-parakeet-tdt-0.6b
|
||||
url: github:mudler/LocalAI/gallery/virtual.yaml@master
|
||||
urls:
|
||||
@@ -13368,8 +13096,8 @@
|
||||
model: rfdetr-seg-medium-f16.gguf
|
||||
files:
|
||||
- filename: rfdetr-seg-medium-f16.gguf
|
||||
sha256: 885d85ed6935495fc50ff464e06b6ea3bd8e8386865852d68a8be0f649d65afe
|
||||
uri: huggingface://mudler/rfdetr-cpp-seg-medium/rfdetr-seg-medium-f16.gguf
|
||||
sha256: dd7c8da7cf0a2e64a1002f5ff66d7fede45b00e612457f249bcd9d4a0c122566
|
||||
- name: rfdetr-cpp-seg-large
|
||||
url: github:mudler/LocalAI/gallery/virtual.yaml@master
|
||||
urls:
|
||||
@@ -13397,8 +13125,8 @@
|
||||
model: rfdetr-seg-large-f16.gguf
|
||||
files:
|
||||
- filename: rfdetr-seg-large-f16.gguf
|
||||
sha256: 90423066d0791b4ae249f3986cce1f095a1e4090bf46800bf7f9e371ea80d559
|
||||
uri: huggingface://mudler/rfdetr-cpp-seg-large/rfdetr-seg-large-f16.gguf
|
||||
sha256: ffc631b8e6115b11bdbb8e876c77aaa3e3e5d8c41c00ce8417ebbf183b1e6404
|
||||
- name: rfdetr-cpp-seg-xlarge
|
||||
url: github:mudler/LocalAI/gallery/virtual.yaml@master
|
||||
urls:
|
||||
@@ -13426,8 +13154,8 @@
|
||||
model: rfdetr-seg-xlarge-f16.gguf
|
||||
files:
|
||||
- filename: rfdetr-seg-xlarge-f16.gguf
|
||||
sha256: 0b82de4a6e65a40bc930979a1a4281cb24de35203d30eeefd797c858101a7bec
|
||||
uri: huggingface://mudler/rfdetr-cpp-seg-xlarge/rfdetr-seg-xlarge-f16.gguf
|
||||
sha256: 72b6210e255ebb89a8d471c06a6f4b5307205b1a34b9e4292de90d1488da4e26
|
||||
- name: rfdetr-cpp-seg-2xlarge
|
||||
url: github:mudler/LocalAI/gallery/virtual.yaml@master
|
||||
urls:
|
||||
@@ -13456,8 +13184,8 @@
|
||||
model: rfdetr-seg-2xlarge-f16.gguf
|
||||
files:
|
||||
- filename: rfdetr-seg-2xlarge-f16.gguf
|
||||
sha256: 7f957997db23e844194ea8266a95b4adc3deb6d0b71c0924922b20fbdeafa299
|
||||
uri: huggingface://mudler/rfdetr-cpp-seg-2xlarge/rfdetr-seg-2xlarge-f16.gguf
|
||||
sha256: 00f3988bdf9a382b06610c200b3938b65a7731d14eafa84b73f6c3b5be4af8d9
|
||||
- name: edgetam
|
||||
url: github:mudler/LocalAI/gallery/virtual.yaml@master
|
||||
urls:
|
||||
|
||||
@@ -70,11 +70,6 @@ func ExtractArchive(archive, dst string) error {
|
||||
if f.FileInfo.Mode()&os.ModeSymlink != 0 {
|
||||
return fmt.Errorf("archive contains a symlink")
|
||||
}
|
||||
if linkname, ok := archiveMemberLinkname(f); ok {
|
||||
if err := validateArchiveMemberPath(extractRoot, linkname); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -100,18 +95,6 @@ func archiveMemberName(f archiver.File) string {
|
||||
}
|
||||
}
|
||||
|
||||
// archiveMemberLinkname reports the target of a tar hardlink member, which carries a regular file mode and so is not caught by the symlink check.
|
||||
func archiveMemberLinkname(f archiver.File) (string, bool) {
|
||||
switch h := f.Header.(type) {
|
||||
case tar.Header:
|
||||
return h.Linkname, h.Typeflag == tar.TypeLink
|
||||
case *tar.Header:
|
||||
return h.Linkname, h.Typeflag == tar.TypeLink
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func validateArchiveMemberPath(root, name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("archive contains an empty path")
|
||||
|
||||
@@ -3,7 +3,6 @@ package utils_test
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"compress/gzip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@@ -60,53 +59,6 @@ var _ = Describe("utils/archive tests", func() {
|
||||
Expect(err.Error()).To(ContainSubstring("unsafe path"))
|
||||
Expect(filepath.Join(tmpDir, "escaped.txt")).ToNot(BeAnExistingFile())
|
||||
})
|
||||
|
||||
It("rejects tar hardlinks that overwrite a file outside the destination", func() {
|
||||
tmpDir := GinkgoT().TempDir()
|
||||
archivePath := filepath.Join(tmpDir, "model.tar.gz")
|
||||
extractPath := filepath.Join(tmpDir, "models")
|
||||
outsidePath := filepath.Join(tmpDir, "outside.txt")
|
||||
|
||||
Expect(os.WriteFile(outsidePath, []byte("original"), 0o600)).To(Succeed())
|
||||
Expect(writeTarGzArchiveWithHardlinkedFile(archivePath, "payload.bin", "../outside.txt", "overwritten")).To(Succeed())
|
||||
|
||||
err := ExtractArchive(archivePath, extractPath)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("unsafe path"))
|
||||
|
||||
contents, readErr := os.ReadFile(outsidePath)
|
||||
Expect(readErr).ToNot(HaveOccurred())
|
||||
Expect(string(contents)).To(Equal("original"))
|
||||
})
|
||||
|
||||
It("extracts tar hardlinks that stay inside the destination", func() {
|
||||
tmpDir := GinkgoT().TempDir()
|
||||
archivePath := filepath.Join(tmpDir, "model.tar.gz")
|
||||
extractPath := filepath.Join(tmpDir, "models")
|
||||
|
||||
Expect(writeTarGzArchiveWithInternalHardlink(archivePath, "model.bin", "alias.bin", "weights")).To(Succeed())
|
||||
|
||||
Expect(ExtractArchive(archivePath, extractPath)).To(Succeed())
|
||||
|
||||
extracted, err := os.ReadFile(filepath.Join(extractPath, "alias.bin"))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(string(extracted)).To(Equal("weights"))
|
||||
})
|
||||
|
||||
It("rejects tar hardlinks that point outside the destination", func() {
|
||||
tmpDir := GinkgoT().TempDir()
|
||||
archivePath := filepath.Join(tmpDir, "model.tar")
|
||||
extractPath := filepath.Join(tmpDir, "models")
|
||||
|
||||
Expect(writeTarArchiveWithHardlink(archivePath, "payload.bin", "../escaped.txt")).To(Succeed())
|
||||
|
||||
err := ExtractArchive(archivePath, extractPath)
|
||||
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("unsafe path"))
|
||||
Expect(filepath.Join(tmpDir, "escaped.txt")).ToNot(BeAnExistingFile())
|
||||
})
|
||||
})
|
||||
|
||||
func writeZipArchive(path string, files map[string]string) (err error) {
|
||||
@@ -174,121 +126,3 @@ func writeTarArchive(path string, files map[string]string) (err error) {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeTarArchiveWithHardlink(path, name, linkname string) (err error) {
|
||||
out, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if closeErr := out.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}()
|
||||
|
||||
writer := tar.NewWriter(out)
|
||||
defer func() {
|
||||
if closeErr := writer.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}()
|
||||
|
||||
return writer.WriteHeader(&tar.Header{
|
||||
Name: name,
|
||||
Linkname: linkname,
|
||||
Typeflag: tar.TypeLink,
|
||||
Mode: 0o600,
|
||||
})
|
||||
}
|
||||
|
||||
func writeTarGzArchiveWithHardlinkedFile(path, name, linkname, contents string) (err error) {
|
||||
out, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if closeErr := out.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}()
|
||||
|
||||
compressor := gzip.NewWriter(out)
|
||||
defer func() {
|
||||
if closeErr := compressor.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}()
|
||||
|
||||
writer := tar.NewWriter(compressor)
|
||||
defer func() {
|
||||
if closeErr := writer.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}()
|
||||
|
||||
if err := writer.WriteHeader(&tar.Header{
|
||||
Name: name,
|
||||
Linkname: linkname,
|
||||
Typeflag: tar.TypeLink,
|
||||
Mode: 0o600,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := []byte(contents)
|
||||
if err := writer.WriteHeader(&tar.Header{
|
||||
Name: name,
|
||||
Mode: 0o600,
|
||||
Size: int64(len(data)),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = writer.Write(data)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func writeTarGzArchiveWithInternalHardlink(path, targetName, linkName, contents string) (err error) {
|
||||
out, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if closeErr := out.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}()
|
||||
|
||||
compressor := gzip.NewWriter(out)
|
||||
defer func() {
|
||||
if closeErr := compressor.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}()
|
||||
|
||||
writer := tar.NewWriter(compressor)
|
||||
defer func() {
|
||||
if closeErr := writer.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}()
|
||||
|
||||
data := []byte(contents)
|
||||
if err := writer.WriteHeader(&tar.Header{
|
||||
Name: targetName,
|
||||
Mode: 0o600,
|
||||
Size: int64(len(data)),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := writer.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return writer.WriteHeader(&tar.Header{
|
||||
Name: linkName,
|
||||
Linkname: targetName,
|
||||
Typeflag: tar.TypeLink,
|
||||
Mode: 0o600,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Checks how the run.sh of each C++ backend sets up the Intel graphics driver.
|
||||
#
|
||||
# A backend built for Intel GPUs carries its own copy of the Intel graphics
|
||||
# driver. run.sh has to tell Level Zero, which is how llama.cpp reaches the
|
||||
# card, to use that copy. Three things must hold, and all three have broken in
|
||||
# the past:
|
||||
#
|
||||
# 1. If the user already chose a driver, keep the user's choice. Otherwise a
|
||||
# machine with a graphics card too new for the carried driver stops
|
||||
# working, with no way to get back to the driver that did work.
|
||||
# 2. Say nothing about OpenCL. No OpenCL driver is carried, so pointing
|
||||
# OpenCL at the backend's own directory would leave it with no driver at
|
||||
# all, where saying nothing leaves it the machine's own.
|
||||
# 3. Ask the driver for the amount of free memory. Without this, llama.cpp
|
||||
# reads zero free memory on an integrated graphics chip, because such a
|
||||
# chip has no memory of its own and shares the system's.
|
||||
#
|
||||
# The test builds a fake backend directory for each run.sh, runs it, and reads
|
||||
# back the variables it exported.
|
||||
set -euo pipefail
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
REPO_ROOT=$(dirname "$(dirname "$(dirname "$(realpath "$0")")")")
|
||||
|
||||
RUN_SCRIPTS=(
|
||||
"backend/cpp/llama-cpp/run.sh llama-cpp"
|
||||
"backend/cpp/turboquant/run.sh turboquant"
|
||||
"backend/cpp/bonsai/run.sh bonsai"
|
||||
)
|
||||
|
||||
failures=0
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*"
|
||||
failures=$((failures + 1))
|
||||
}
|
||||
|
||||
# Builds a fake backend directory: the real run.sh, a stand-in for the backend
|
||||
# program that prints the variables we care about, and whichever libraries the
|
||||
# caller asked for.
|
||||
#
|
||||
# Usage: make_backend <dir> <program-prefix> [library ...]
|
||||
make_backend() {
|
||||
local dir="$1" prefix="$2"
|
||||
shift 2
|
||||
|
||||
mkdir -p "$dir/lib"
|
||||
cp "$RUN_SH" "$dir/run.sh"
|
||||
chmod +x "$dir/run.sh"
|
||||
|
||||
local lib
|
||||
for lib in "$@"; do
|
||||
: > "$dir/lib/$lib"
|
||||
done
|
||||
|
||||
cat > "$dir/${prefix}-fallback" <<'PROGRAM'
|
||||
#!/bin/bash
|
||||
echo "level_zero_driver=${ZE_ENABLE_ALT_DRIVERS:-}"
|
||||
echo "opencl_driver_list=${OCL_ICD_VENDORS:-}"
|
||||
echo "report_free_memory=${ZES_ENABLE_SYSMAN:-}"
|
||||
PROGRAM
|
||||
chmod +x "$dir/${prefix}-fallback"
|
||||
}
|
||||
|
||||
# Runs a fake backend and prints the one variable asked for.
|
||||
# Usage: read_variable <dir> <name>
|
||||
read_variable() {
|
||||
local dir="$1" name="$2"
|
||||
bash "$dir/run.sh" 2>/dev/null | sed -n "s/^${name}=//p"
|
||||
}
|
||||
|
||||
for entry in "${RUN_SCRIPTS[@]}"; do
|
||||
read -r script prefix <<< "$entry"
|
||||
RUN_SH="$REPO_ROOT/$script"
|
||||
|
||||
if [ ! -f "$RUN_SH" ]; then
|
||||
fail "$script does not exist"
|
||||
continue
|
||||
fi
|
||||
|
||||
# An Intel build with its own graphics driver: point Level Zero and OpenCL
|
||||
# at the bundled copies and ask for the free memory reading.
|
||||
bundled="$WORK/$prefix-bundled"
|
||||
make_backend "$bundled" "$prefix" \
|
||||
libze_loader.so.1 libze_intel_gpu.so.1 libigdrcl.so
|
||||
mkdir -p "$bundled/etc/OpenCL/vendors"
|
||||
echo "libigdrcl.so" > "$bundled/etc/OpenCL/vendors/intel.icd"
|
||||
|
||||
got=$(read_variable "$bundled" level_zero_driver)
|
||||
if [ "$got" != "$bundled/lib/libze_intel_gpu.so.1" ]; then
|
||||
fail "$script: expected Level Zero to use the bundled driver, got '$got'"
|
||||
fi
|
||||
|
||||
# Even with an OpenCL driver and a driver list sitting in the backend, which
|
||||
# is what an older packaging left behind, OpenCL must be left alone.
|
||||
got=$(read_variable "$bundled" opencl_driver_list)
|
||||
if [ -n "$got" ]; then
|
||||
fail "$script: OpenCL was pointed at the backend's own directory ('$got')"
|
||||
fi
|
||||
|
||||
got=$(read_variable "$bundled" report_free_memory)
|
||||
if [ "$got" != "1" ]; then
|
||||
fail "$script: expected the free memory reading to be turned on, got '$got'"
|
||||
fi
|
||||
|
||||
# The user picked a driver already. Both choices must survive.
|
||||
got=$(ZE_ENABLE_ALT_DRIVERS=/usr/lib/host-driver.so \
|
||||
read_variable "$bundled" level_zero_driver)
|
||||
if [ "$got" != "/usr/lib/host-driver.so" ]; then
|
||||
fail "$script: the user's Level Zero driver was overwritten with '$got'"
|
||||
fi
|
||||
|
||||
got=$(ZES_ENABLE_SYSMAN=0 read_variable "$bundled" report_free_memory)
|
||||
if [ "$got" != "0" ]; then
|
||||
fail "$script: the user's free memory setting was overwritten with '$got'"
|
||||
fi
|
||||
|
||||
# A build for some other kind of graphics card. None of the Intel
|
||||
# variables belong here.
|
||||
other="$WORK/$prefix-other"
|
||||
make_backend "$other" "$prefix" libcublas.so.12
|
||||
|
||||
for name in level_zero_driver opencl_driver_list report_free_memory; do
|
||||
got=$(read_variable "$other" "$name")
|
||||
if [ -n "$got" ]; then
|
||||
fail "$script: $name was set on a build with no Intel libraries ('$got')"
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if [ "$failures" -gt 0 ]; then
|
||||
echo "$failures check(s) failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: every run.sh sets up the Intel graphics driver correctly"
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CURDIR=$(dirname "$(realpath "$0")")
|
||||
SELECTOR="$CURDIR/../../.docker/llama-cpp-build-target.sh"
|
||||
|
||||
assert_target() {
|
||||
local arch=$1
|
||||
local build_type=$2
|
||||
local expected=$3
|
||||
local actual
|
||||
|
||||
actual=$("$SELECTOR" "$arch" "$build_type")
|
||||
if [ "$actual" != "$expected" ]; then
|
||||
echo "FAIL: $arch/$build_type selected $actual, expected $expected"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_target amd64 cublas llama-cpp-cpu-all
|
||||
assert_target amd64 vulkan llama-cpp-cpu-all
|
||||
assert_target amd64 "" llama-cpp-cpu-all
|
||||
assert_target arm64 cublas llama-cpp-fallback
|
||||
assert_target arm64 "" llama-cpp-cpu-all
|
||||
|
||||
echo "PASS: llama.cpp build target preserves CPU variants where supported"
|
||||
@@ -1,171 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Checks what package_intel_libs puts in a backend built for Intel GPUs.
|
||||
#
|
||||
# The packager copies the libraries a backend needs next to the backend itself,
|
||||
# so it can run on a machine that has none of them installed. Four things have
|
||||
# to happen, and each one has been missing at some point:
|
||||
#
|
||||
# 1. Copy the libraries the backend program is linked against. Some of them
|
||||
# are only reachable from the program, not from any other copied library,
|
||||
# so looking at the copied libraries alone is not enough.
|
||||
# 2. Copy the libraries that are opened by name while the program runs. Those
|
||||
# are invisible to any tool that reads the list of libraries a file is
|
||||
# linked against, so they have to be named one by one.
|
||||
# 3. Copy the Intel graphics driver, which is also opened by name at run
|
||||
# time.
|
||||
# 4. Copy only the driver Level Zero talks to, and leave the OpenCL one out.
|
||||
# llama.cpp reaches an Intel GPU through Level Zero; the OpenCL driver
|
||||
# brings a second copy of the graphics compiler with it, which is about
|
||||
# 139 MB for a path nothing here uses.
|
||||
#
|
||||
# The test builds a stand-in for an oneAPI installation, a stand-in for a
|
||||
# driver installation and two fake backend programs, runs the real packager and
|
||||
# checks the result.
|
||||
set -euo pipefail
|
||||
|
||||
CURDIR=$(dirname "$(realpath "$0")")
|
||||
SCRIPT="$CURDIR/package-gpu-libs.sh"
|
||||
|
||||
if ! command -v gcc >/dev/null 2>&1 || ! command -v ldd >/dev/null 2>&1; then
|
||||
echo "SKIP: gcc/ldd not available"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
# Stand-in for /opt/intel/oneapi/*/lib.
|
||||
ONEAPI="$WORK/oneapi/lib"
|
||||
mkdir -p "$ONEAPI"
|
||||
|
||||
# Two libraries the backend programs are linked against, one each. Nothing else
|
||||
# refers to them, so they can only be found by looking at the programs.
|
||||
echo 'int first_fn(void){return 1;}' > "$WORK/first.c"
|
||||
gcc -shared -fPIC -o "$ONEAPI/libfakeoneapifirst.so.2" "$WORK/first.c"
|
||||
echo 'int second_fn(void){return 2;}' > "$WORK/second.c"
|
||||
gcc -shared -fPIC -o "$ONEAPI/libfakeoneapisecond.so.2" "$WORK/second.c"
|
||||
|
||||
# A library that is opened by name while the program runs. Nothing is linked
|
||||
# against it, so only the list of names in the packager can find it.
|
||||
echo 'int adapter_fn(void){return 3;}' > "$WORK/adapter.c"
|
||||
gcc -shared -fPIC -o "$ONEAPI/libur_adapter_level_zero.so.0" "$WORK/adapter.c"
|
||||
|
||||
# Two fake backend programs, in the directory the real packaging script uses:
|
||||
# package/, one level above package/lib. One is named after llama.cpp, the
|
||||
# other is not, because the same packager serves several backends.
|
||||
PKG="$WORK/package"
|
||||
TARGET="$PKG/lib"
|
||||
mkdir -p "$TARGET"
|
||||
echo 'int first_fn(void); int main(void){return first_fn();}' > "$WORK/main1.c"
|
||||
gcc -o "$PKG/llama-cpp-grpc" "$WORK/main1.c" \
|
||||
-L"$ONEAPI" -l:libfakeoneapifirst.so.2 -Wl,-rpath,"$ONEAPI"
|
||||
echo 'int second_fn(void); int main(void){return second_fn();}' > "$WORK/main2.c"
|
||||
gcc -o "$PKG/bonsai-grpc" "$WORK/main2.c" \
|
||||
-L"$ONEAPI" -l:libfakeoneapisecond.so.2 -Wl,-rpath,"$ONEAPI"
|
||||
|
||||
# The real directory also holds the script that starts the backend. Looking at a
|
||||
# shell script for libraries has to be harmless.
|
||||
printf '#!/bin/bash\necho started\n' > "$PKG/run.sh"
|
||||
chmod +x "$PKG/run.sh"
|
||||
|
||||
# Stand-in for the Intel graphics driver installation. These files are opened by
|
||||
# name at run time rather than linked, so the packager has to name the ones it
|
||||
# wants.
|
||||
DRV="$WORK/driver"
|
||||
mkdir -p "$DRV/intel-opencl"
|
||||
echo 'int ze_drv(void){return 4;}' > "$WORK/zedrv.c"
|
||||
gcc -shared -fPIC -o "$DRV/libze_intel_gpu.so.1" "$WORK/zedrv.c"
|
||||
echo 'int cl_drv(void){return 5;}' > "$WORK/cldrv.c"
|
||||
gcc -shared -fPIC -o "$DRV/intel-opencl/libigdrcl.so" "$WORK/cldrv.c"
|
||||
|
||||
# The compiler front end the OpenCL driver needs, and the large library it is
|
||||
# linked against. The link is what makes the big one arrive on its own if the
|
||||
# front end is ever copied again, so the fake mirrors it.
|
||||
echo 'int clang_fn(void){return 6;}' > "$WORK/clang.c"
|
||||
gcc -shared -fPIC -o "$DRV/libopencl-clang.so.15" "$WORK/clang.c"
|
||||
echo 'int clang_fn(void); int fcl_fn(void){return clang_fn();}' > "$WORK/fcl.c"
|
||||
gcc -shared -fPIC -o "$DRV/libigdfcl.so.2" "$WORK/fcl.c" \
|
||||
-L"$DRV" -l:libopencl-clang.so.15 -Wl,-rpath,"$DRV"
|
||||
|
||||
# Let the fake oneAPI libraries be found the way the real ones are on the build
|
||||
# machine.
|
||||
export LD_LIBRARY_PATH="$ONEAPI:${LD_LIBRARY_PATH:-}"
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source "$SCRIPT" "$TARGET"
|
||||
|
||||
export BUILD_TYPE=sycl_f16
|
||||
export INTEL_ONEAPI_LIB_DIRS="$ONEAPI"
|
||||
export INTEL_DRIVER_LIB_DIRS="$DRV $DRV/intel-opencl"
|
||||
package_intel_libs
|
||||
|
||||
fail=false
|
||||
|
||||
for lib in libfakeoneapifirst.so.2 libfakeoneapisecond.so.2; do
|
||||
if [ ! -e "$TARGET/$lib" ]; then
|
||||
echo "FAIL: $lib is missing; the backend programs' own libraries were not copied"
|
||||
fail=true
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ! -e "$TARGET/libur_adapter_level_zero.so.0" ]; then
|
||||
echo "FAIL: the Level Zero adapter, which is opened by name, was not copied"
|
||||
fail=true
|
||||
fi
|
||||
|
||||
if [ ! -e "$TARGET/libze_intel_gpu.so.1" ]; then
|
||||
echo "FAIL: the Level Zero graphics driver was not copied"
|
||||
fail=true
|
||||
fi
|
||||
|
||||
# The OpenCL driver and the compiler front end that hangs off it are left out,
|
||||
# and so is the driver list that would name them.
|
||||
for lib in libigdrcl.so libigdfcl.so.2 libopencl-clang.so.15; do
|
||||
if [ -e "$TARGET/$lib" ]; then
|
||||
echo "FAIL: $lib was copied, but nothing here uses the OpenCL path"
|
||||
fail=true
|
||||
fi
|
||||
done
|
||||
if [ -e "$TARGET/../etc/OpenCL" ]; then
|
||||
echo "FAIL: an OpenCL driver list was created for a path nothing uses"
|
||||
fail=true
|
||||
fi
|
||||
|
||||
# The Python backends for Intel GPUs, built as BUILD_TYPE=intel, start without
|
||||
# run.sh and so never load a copied driver. Copying one for them would add
|
||||
# several hundred megabytes that nothing reads.
|
||||
PYTHON_STYLE="$WORK/python-backend/lib"
|
||||
mkdir -p "$PYTHON_STYLE"
|
||||
(
|
||||
BUILD_TYPE=intel \
|
||||
INTEL_ONEAPI_LIB_DIRS="$ONEAPI" \
|
||||
INTEL_DRIVER_LIB_DIRS="$DRV $DRV/intel-opencl" \
|
||||
bash -c 'source "$0" "$1"; package_intel_libs' "$SCRIPT" "$PYTHON_STYLE"
|
||||
) >/dev/null 2>&1
|
||||
if [ -e "$PYTHON_STYLE/libze_intel_gpu.so.1" ]; then
|
||||
echo "FAIL: the graphics driver was copied into a backend that cannot load it"
|
||||
fail=true
|
||||
fi
|
||||
|
||||
# A build for Intel GPUs that ends up with no driver still works, but only on a
|
||||
# machine that has its own. That is easy to cause by accident and impossible to
|
||||
# see afterwards, so the packager has to say so.
|
||||
warning=$(
|
||||
BUILD_TYPE=sycl_f16 \
|
||||
INTEL_ONEAPI_LIB_DIRS="$ONEAPI" \
|
||||
INTEL_DRIVER_LIB_DIRS="$WORK/empty" \
|
||||
bash -c 'source "$0" "$1"; package_intel_libs' \
|
||||
"$SCRIPT" "$WORK/nodriver/lib" 2>&1 >/dev/null || true
|
||||
)
|
||||
if ! grep -qi "no intel graphics driver" <<< "$warning"; then
|
||||
echo "FAIL: no warning when the graphics driver could not be copied"
|
||||
fail=true
|
||||
fi
|
||||
|
||||
if [ "$fail" = true ]; then
|
||||
ls -la "$TARGET" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: the oneAPI libraries, the adapter and the Level Zero graphics driver were all handled"
|
||||
exit 0
|
||||
@@ -675,43 +675,21 @@ package_rocm_libs() {
|
||||
package_intel_libs() {
|
||||
echo "Packaging Intel oneAPI/SYCL libraries for BUILD_TYPE=${BUILD_TYPE}..."
|
||||
|
||||
# Where to look for the oneAPI libraries. The default is the standard install
|
||||
# layout. The list can be overridden with a space-separated one, which lets
|
||||
# the tests run without a real oneAPI install, the same way ROCM_BASE_DIRS
|
||||
# works. Both the current and the older math library layouts are listed, and
|
||||
# the check below skips whichever of them is absent.
|
||||
local intel_lib_paths
|
||||
if [ -n "${INTEL_ONEAPI_LIB_DIRS:-}" ]; then
|
||||
# shellcheck disable=SC2206 # intentional word-split of the override
|
||||
intel_lib_paths=(${INTEL_ONEAPI_LIB_DIRS})
|
||||
else
|
||||
intel_lib_paths=(
|
||||
"/opt/intel/oneapi/compiler/latest/lib"
|
||||
"/opt/intel/oneapi/mkl/latest/lib"
|
||||
"/opt/intel/oneapi/mkl/latest/lib/intel64"
|
||||
"/opt/intel/oneapi/dnnl/latest/lib"
|
||||
"/opt/intel/oneapi/tbb/latest/lib/intel64/gcc4.8"
|
||||
)
|
||||
fi
|
||||
local intel_lib_paths=(
|
||||
"/opt/intel/oneapi/compiler/latest/lib"
|
||||
"/opt/intel/oneapi/mkl/latest/lib/intel64"
|
||||
"/opt/intel/oneapi/tbb/latest/lib/intel64/gcc4.8"
|
||||
)
|
||||
|
||||
# The oneAPI libraries a backend needs at run time. The math library entries
|
||||
# cover both of its number formats and both of its threading layers, because
|
||||
# the llama.cpp build for Intel GPUs uses a different combination than the
|
||||
# rest. The libur_adapter_* entries have to be named here even though nothing
|
||||
# is linked against them: oneAPI opens them by name while the program runs,
|
||||
# so the dependency scan later in this function cannot see them.
|
||||
# Core Intel oneAPI runtime libraries
|
||||
local intel_libs=(
|
||||
"libsycl.so*"
|
||||
"libOpenCL.so*"
|
||||
"libmkl_core.so*"
|
||||
"libmkl_intel_lp64.so*"
|
||||
"libmkl_intel_ilp64.so*"
|
||||
"libmkl_intel_thread.so*"
|
||||
"libmkl_tbb_thread.so*"
|
||||
"libmkl_sequential.so*"
|
||||
"libmkl_sycl.so*"
|
||||
"libmkl_sycl_blas.so*"
|
||||
"libdnnl.so*"
|
||||
"libiomp5.so*"
|
||||
"libsvml.so*"
|
||||
"libirng.so*"
|
||||
@@ -719,10 +697,6 @@ package_intel_libs() {
|
||||
"libintlc.so*"
|
||||
"libtbb.so*"
|
||||
"libtbbmalloc.so*"
|
||||
"libur_loader.so*"
|
||||
"libur_adapter_level_zero.so*"
|
||||
"libur_adapter_level_zero_v2.so*"
|
||||
"libur_adapter_opencl.so*"
|
||||
"libpi_level_zero.so*"
|
||||
"libpi_opencl.so*"
|
||||
"libze_loader.so*"
|
||||
@@ -736,92 +710,10 @@ package_intel_libs() {
|
||||
fi
|
||||
done
|
||||
|
||||
# Copy the libraries the backend programs themselves are linked against. The
|
||||
# list above is not enough on its own: the programs are linked directly
|
||||
# against several oneAPI libraries that no copied library refers to, so
|
||||
# without this step the backend only ran inside the build image, where oneAPI
|
||||
# happens to be on the library path.
|
||||
#
|
||||
# The programs sit one level above the target directory, in package/, next to
|
||||
# the run.sh that starts them. Every backend that builds for Intel GPUs is
|
||||
# covered by looking at all of them, rather than at one set of names, because
|
||||
# llama.cpp, turboquant and bonsai all come through here.
|
||||
local pkg_dir="$TARGET_LIB_DIR/.."
|
||||
local bin
|
||||
for bin in "$pkg_dir"/*; do
|
||||
if [ -f "$bin" ] && [ -x "$bin" ]; then
|
||||
copy_elf_deps "$bin"
|
||||
fi
|
||||
done
|
||||
|
||||
# Copy the Intel graphics driver itself, the way the Vulkan packaging copies
|
||||
# the Mesa driver. Level Zero opens the driver by name while the program
|
||||
# runs, so no dependency scan can find it and it has to be named here.
|
||||
#
|
||||
# This is what lets the backend run on a machine with no Intel graphics
|
||||
# packages installed, and also on a machine whose own driver was built
|
||||
# against a newer C library than the one this backend carries, where loading
|
||||
# the host's driver crashes. Carrying the driver is safe across kernel
|
||||
# versions because it reaches the graphics hardware through an interface the
|
||||
# kernel keeps stable. The NVIDIA driver has no such interface, which is why
|
||||
# that one is never copied.
|
||||
#
|
||||
# Only the builds that start through run.sh get a driver: run.sh is what
|
||||
# tells Level Zero to use it. The Python backends built for Intel GPUs start
|
||||
# differently and keep using the host's driver, so copying one for them would
|
||||
# add several hundred megabytes that nothing would ever load.
|
||||
#
|
||||
# Only the Level Zero side is copied. llama.cpp reaches an Intel GPU through
|
||||
# Level Zero, which hands the driver ready-compiled programs and so needs
|
||||
# only the compiler's back end. The OpenCL driver can be handed source code
|
||||
# instead, so it also needs the compiler's front end, and that pulls in a
|
||||
# copy of clang: around 139 MB for a path nothing here takes. A user who
|
||||
# wants OpenCL has their machine's own.
|
||||
case "${BUILD_TYPE:-}" in
|
||||
sycl*)
|
||||
local intel_driver_lib_dirs
|
||||
if [ -n "${INTEL_DRIVER_LIB_DIRS:-}" ]; then
|
||||
# shellcheck disable=SC2206 # split the override into words on purpose
|
||||
intel_driver_lib_dirs=(${INTEL_DRIVER_LIB_DIRS})
|
||||
else
|
||||
intel_driver_lib_dirs=(
|
||||
"/usr/lib/x86_64-linux-gnu"
|
||||
"/usr/lib"
|
||||
)
|
||||
fi
|
||||
local driver_libs=(
|
||||
"libze_intel_gpu.so*" # the driver Level Zero talks to
|
||||
"libigc.so*" # turns compute programs into instructions for the card
|
||||
"libigdgmm.so*" # manages graphics memory
|
||||
)
|
||||
local drv_dir pat
|
||||
for drv_dir in "${intel_driver_lib_dirs[@]}"; do
|
||||
[ -d "$drv_dir" ] || continue
|
||||
for pat in "${driver_libs[@]}"; do
|
||||
copy_libs_glob "${drv_dir}/${pat}"
|
||||
done
|
||||
done
|
||||
;;
|
||||
esac
|
||||
|
||||
# Copy whatever the steps above still missed. Each library copied so far can
|
||||
# need further libraries of its own, and a missing one stops the backend from
|
||||
# starting at all (issue #10537).
|
||||
# Pull in transitive deps the allowlist misses so the backend is
|
||||
# self-contained (same class of failure as #10537).
|
||||
sweep_transitive_deps "$TARGET_LIB_DIR"
|
||||
|
||||
# Say so when a build meant for Intel GPUs ends up without a driver. It still
|
||||
# works on a machine that has its own, so nothing fails here, and the only
|
||||
# other way to notice is a user reporting that their GPU is not used. The
|
||||
# usual cause is a build image that predates the driver being installed in
|
||||
# .docker/install-base-deps.sh.
|
||||
case "${BUILD_TYPE:-}" in
|
||||
sycl*)
|
||||
if [ ! -e "$TARGET_LIB_DIR/libze_intel_gpu.so.1" ]; then
|
||||
echo "WARNING: no Intel graphics driver was found to copy. This backend will only use a GPU on a machine that has its own Intel driver installed." >&2
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Intel oneAPI libraries packaged successfully"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CURDIR=$(dirname "$(realpath "$0")")
|
||||
SELECTOR="$CURDIR/../../.docker/turboquant-build-target.sh"
|
||||
|
||||
assert_target() {
|
||||
local arch=$1
|
||||
local build_type=$2
|
||||
local expected=$3
|
||||
local actual
|
||||
|
||||
actual=$("$SELECTOR" "$arch" "$build_type")
|
||||
if [ "$actual" != "$expected" ]; then
|
||||
echo "FAIL: $arch/$build_type selected $actual, expected $expected"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_target amd64 cublas turboquant-cpu-all
|
||||
assert_target amd64 vulkan turboquant-cpu-all
|
||||
assert_target amd64 "" turboquant-cpu-all
|
||||
assert_target arm64 cublas turboquant-fallback
|
||||
assert_target arm64 "" turboquant-cpu-all
|
||||
|
||||
echo "PASS: turboquant build target preserves CPU variants where supported"
|
||||
@@ -389,14 +389,10 @@ export const SHARED_BUILD_INPUTS = [
|
||||
darwin: always,
|
||||
},
|
||||
{
|
||||
// Decides which GPU libraries end up inside an image. Every Linux image
|
||||
// runs it: Dockerfile.python calls it directly, and the Go and C++ backends
|
||||
// call it from their own package.sh. Naming only the Python images here is
|
||||
// how a packaging fix for the Intel llama.cpp backend could merge and reach
|
||||
// no image, which is the #10946 case all over again. The Darwin builds have
|
||||
// their own packaging scripts and never call this one.
|
||||
// Stages the CUDA/ROCm runtime libraries into every Python image's lib/.
|
||||
// COPY'd and run by Dockerfile.python only. This is the #10946 case.
|
||||
matches: file => file === "scripts/build/package-gpu-libs.sh",
|
||||
linux: always,
|
||||
linux: isLinuxPython,
|
||||
darwin: never,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -86,25 +86,20 @@ const run = (changedFiles, previousMatrix) =>
|
||||
|
||||
const names = entries => entries.map(e => e.backend).sort();
|
||||
|
||||
test("a change to only package-gpu-libs.sh rebuilds every Linux image", () => {
|
||||
// The PR #10946 regression: this script decides which GPU libraries end up
|
||||
// inside an image, but lives under scripts/, so the per-backend prefix match
|
||||
// produced an empty matrix and the packaging fix shipped to nothing.
|
||||
//
|
||||
// Every Linux image runs it, not only the Python ones: Dockerfile.python
|
||||
// calls it directly, and the Go and C++ backends call it from their own
|
||||
// package.sh (see backend/cpp/llama-cpp/package.sh and backend/go/*/
|
||||
// package.sh). Leaving those out is how a fix aimed at the Intel llama.cpp
|
||||
// backend could merge and reach no image.
|
||||
test("a change to only package-gpu-libs.sh rebuilds every Python image", () => {
|
||||
// The PR #10946 regression: this script is COPY'd and run by
|
||||
// Dockerfile.python for every Python backend, but lives under scripts/, so
|
||||
// the per-backend prefix match produced an empty matrix and the cuDNN
|
||||
// packaging fix shipped to nothing.
|
||||
const { filtered, filteredDarwin, changedBackends } = run([
|
||||
"scripts/build/package-gpu-libs.sh",
|
||||
]);
|
||||
|
||||
assert.equal(filtered.length, includes.length);
|
||||
assert.notEqual(filtered.length, 0, "expected a non-empty Linux matrix");
|
||||
assert.deepEqual(names(filtered), ["diffusers", "vllm"]);
|
||||
assert.ok(changedBackends.has("vllm"));
|
||||
assert.ok(changedBackends.has("llama-cpp"));
|
||||
|
||||
// The Darwin builds have their own packaging scripts and never call this one.
|
||||
// Darwin Python builds never invoke it (see scripts/build/python-darwin.sh).
|
||||
assert.deepEqual(filteredDarwin, []);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# Counters shown on the landing page.
|
||||
#
|
||||
# The four GitHub fields are rewritten by .github/ci/refresh-site-counters.sh,
|
||||
# which runs weekly from .github/workflows/refresh-site-counters.yml. Editing
|
||||
# them by hand works but will be overwritten on the next run.
|
||||
stars: 48067
|
||||
forks: 4320
|
||||
contributors: 225
|
||||
releases: 133
|
||||
|
||||
# The GitHub API cannot answer for this one, so it is maintained by hand and
|
||||
# the refresh script carries it through untouched.
|
||||
discord: 3187
|
||||
@@ -16,8 +16,9 @@ enableEmoji = true
|
||||
discord = 'https://discord.gg/uJAeKSAGDy'
|
||||
x = 'https://twitter.com/LocalAI_API'
|
||||
huggingface = 'https://huggingface.co/mudler'
|
||||
# Counters live in data/stats.yaml, which .github/ci/refresh-site-counters.sh
|
||||
# rewrites weekly. They used to sit here as hand-typed strings and drifted.
|
||||
# Refreshed by hand or by CI; shown in the nav and the traction band.
|
||||
stars = '48,042'
|
||||
contributors = '224'
|
||||
|
||||
[markup.goldmark.renderer]
|
||||
unsafe = true
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
<a class="btn btn--o" href="{{ .Site.Params.github }}">★ Star on GitHub</a>
|
||||
</div>
|
||||
<div class="figures fd">
|
||||
<div><b class="tnum" data-count="{{ .Site.Data.stats.stars }}">0</b><span>GitHub stars</span></div>
|
||||
<div><b class="tnum" data-count="48042">0</b><span>GitHub stars</span></div>
|
||||
<div><b class="tnum" data-count="73">0</b><span>Backends</span></div>
|
||||
<div><b class="tnum" data-count="{{ len .Site.Data.engines.engines }}">0</b><span>Engines we wrote</span></div>
|
||||
<div><b class="tnum" data-count="18">0</b><span>Engines we wrote</span></div>
|
||||
<div><b class="tnum" data-count="1585">0</b><span>Models, one click</span></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,11 +164,8 @@
|
||||
<div class="shell">
|
||||
<div class="bars rv" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="kicker rv">Engines we build</p>
|
||||
<h2 class="rv mt1" style="max-width:22ch">{{ len .Site.Data.engines.engines }} engines, written from scratch.</h2>
|
||||
{{/* Names the link back to the runtime section explicitly. Readers were
|
||||
arriving here and taking these for unrelated side projects, because
|
||||
nothing on the page said they are the backends the core loads. */}}
|
||||
<p class="lede rv mt2">Those backends the core pulls in on demand are mostly ours. Most projects wrap somebody else's engine. We wrote ours, because the thing we needed came as a 9 GB Python install, or was closed, or did not exist. Each one is a binary and a GGUF file, checked against the reference implementation in CI.</p>
|
||||
<h2 class="rv mt1" style="max-width:22ch">Eighteen engines, written from scratch.</h2>
|
||||
<p class="lede rv mt2">Most backends wrap somebody else's engine. These do not. They exist because the thing we needed was a 9 GB Python install, or closed, or nobody had built it yet. Each one is a binary and a GGUF file, checked against the reference implementation in CI.</p>
|
||||
|
||||
<div class="spot rv">
|
||||
<div>
|
||||
@@ -250,7 +247,7 @@
|
||||
<video src="/media/vllm-race.mp4" muted loop playsinline preload="none" data-lazy aria-label="vllm.cpp ahead of vLLM at every concurrency level"></video>
|
||||
</div>
|
||||
|
||||
<p class="rv mt2"><a class="btn btn--o" href="/engines/">All {{ len .Site.Data.engines.engines }} engines →</a></p>
|
||||
<p class="rv mt2"><a class="btn btn--o" href="/engines/">All eighteen engines →</a></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -260,10 +257,7 @@
|
||||
<div class="bars rv" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
|
||||
<p class="kicker rv">APEX quantization</p>
|
||||
<h2 class="rv mt1" style="max-width:19ch">The model you could not fit, on the card you already own.</h2>
|
||||
{{/* APEX was being used as a known term on first appearance, in a section
|
||||
that opened straight onto a benchmark table. The first two sentences
|
||||
say what it is and why it follows the engines. */}}
|
||||
<p class="lede rv mt2">The engine decides how fast a model runs. The weights decide whether it runs at all, so we build those too. APEX assigns a different precision to every tensor and every layer: a 35B mixture-of-experts model goes from 64.6 GB, out of reach of any consumer GPU, to 12.2 GB at 74 tokens a second. That is more than twice the speed of the original, and quality barely moves. The file is an ordinary GGUF, so stock llama.cpp opens it with no patches, and 201 of them are already sitting in the LocalAI gallery.</p>
|
||||
<p class="lede rv mt2">A 35B mixture-of-experts model is 64.6 GB at full precision, which puts it out of reach of every consumer GPU. APEX gets it to 12.2 GB, and it runs at 74 tokens a second, more than twice the speed of the original. Quality barely moves. The file is an ordinary GGUF, so stock llama.cpp opens it with no patches, and 201 of them are already sitting in the LocalAI gallery.</p>
|
||||
<div class="sizes rv">
|
||||
<div class="sz"><span>F16 · 64.6 GB</span><i style="width:100%"></i><u>30.4 t/s</u></div>
|
||||
<div class="sz"><span>Q8_0 · 34.4 GB</span><i style="width:53%"></i><u>52.5 t/s</u></div>
|
||||
@@ -369,14 +363,14 @@
|
||||
<h2 class="rv mt1" style="max-width:22ch">Forty-eight thousand stars, and still shipping every week.</h2>
|
||||
<div class="trend rv">
|
||||
<img src="/img/trendshift.svg" alt="LocalAI on Trendshift">
|
||||
<p>LocalAI has been <b>trending on GitHub</b> repeatedly since it launched, and it is one of the most starred self-hosted AI projects there is. <b>{{ lang.FormatNumberCustom 0 .Site.Data.stats.contributors }} people</b> have contributed code, <b>{{ lang.FormatNumberCustom 0 .Site.Data.stats.discord }}</b> are in the Discord, and the README is kept translated into <b>eight languages</b> because the users are everywhere.</p>
|
||||
<p>LocalAI has been <b>trending on GitHub</b> repeatedly since it launched, and it is one of the most starred self-hosted AI projects there is. <b>224 people</b> have contributed code, <b>3,187</b> are in the Discord, and the README is kept translated into <b>eight languages</b> because the users are everywhere.</p>
|
||||
</div>
|
||||
<div class="big rv">
|
||||
<div><b class="tnum" data-count="{{ .Site.Data.stats.stars }}">0</b><span>Stars</span></div>
|
||||
<div><b class="tnum" data-count="{{ .Site.Data.stats.forks }}">0</b><span>Forks</span></div>
|
||||
<div><b class="tnum" data-count="{{ .Site.Data.stats.contributors }}">0</b><span>Contributors</span></div>
|
||||
<div><b class="tnum" data-count="{{ .Site.Data.stats.releases }}">0</b><span>Releases</span></div>
|
||||
<div><b class="tnum" data-count="{{ .Site.Data.stats.discord }}">0</b><span>In Discord</span></div>
|
||||
<div><b class="tnum" data-count="48042">0</b><span>Stars</span></div>
|
||||
<div><b class="tnum" data-count="4314">0</b><span>Forks</span></div>
|
||||
<div><b class="tnum" data-count="224">0</b><span>Contributors</span></div>
|
||||
<div><b class="tnum" data-count="133">0</b><span>Releases</span></div>
|
||||
<div><b class="tnum" data-count="3187">0</b><span>In Discord</span></div>
|
||||
<div><b class="tnum" data-count="0" data-text="3 yrs">0</b><span>Shipping since</span></div>
|
||||
</div>
|
||||
<div class="tl rv">
|
||||
@@ -427,7 +421,7 @@
|
||||
employers reads as a customer logo wall, which is a claim we are not
|
||||
making; a sentence keeps it about the people, which is the true one. */}}
|
||||
<p class="kicker rv mt3">Who shows up</p>
|
||||
<h3 class="eco__h rv">{{ lang.FormatNumberCustom 0 .Site.Data.stats.contributors }} people have put code in this repository.</h3>
|
||||
<h3 class="eco__h rv">{{ .Site.Params.contributors }} people have put code in this repository.</h3>
|
||||
{{- $co := slice }}
|
||||
{{- range .Site.Data.ecosystem.contributors.companies }}{{ $co = $co | append (printf "<b>%s</b>" .name) }}{{ end }}
|
||||
{{- $ac := slice }}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<a href="{{ .Site.Params.docsURL }}">Docs</a>
|
||||
</nav>
|
||||
<div class="topright">
|
||||
<a class="pill" href="{{ .Site.Params.github }}">★ {{ lang.FormatNumberCustom 0 .Site.Data.stats.stars }}</a>
|
||||
<a class="pill" href="{{ .Site.Params.github }}">★ {{ .Site.Params.stars }}</a>
|
||||
<a class="go-btn" href="{{ $home }}#start">Install</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user