mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-30 18:09:05 -04:00
* feat(backend): add vllm-cpp text-generation backend (vllm.cpp) Wrap https://github.com/mudler/vllm.cpp - the LocalAI-team from-scratch C++20 port of vLLM (paged KV cache, continuous batching, prefix caching, safetensors + GGUF loading, no Python at inference) - as a Go gRPC backend over its stable C ABI (ABI v2) via purego. Backend (backend/go/vllm-cpp): - Load -> vllm_engine_load: accepts a .gguf file or a config.json model dir (anything else is refused, satisfying the greedy-probe rule); context_size maps to max_model_len, options block_size/num_blocks/max_num_seqs size the KV cache and scheduler admission. - Predict -> vllm_complete (blocking); PredictStream -> vllm_complete_stream with the per-delta C callback bridged into the gRPC stream. The backend embeds base.Base (not SingleThread): concurrent requests batch continuously in the engine's shared AsyncLLM scheduler. - PredictOptions.Grammar -> the ABI's structured_grammar (GBNF), giving grammar-constrained tool calling at parity with llama-cpp; the ABI also exposes JSON-schema/regex/choice constraints. - Hand-mirrored POD structs with layout locked by unit tests (unsafe.Offsetof vs the C offsets) and a runtime vllm_abi_version gate. - One portable library per platform (vllm.cpp uses per-file SIMD tiers with runtime dispatch), so no avx/avx2/avx512 variant builds. Wiring: - backend-matrix: CPU amd64+arm64 (per-arch + manifest merge), CUDA 12/13 amd64 (120a;121a Blackwell fat binary), L4T arm64 (121a, GB10/DGX Spark - the runtime-proven GPU target), Vulkan amd64, and Darwin arm64 Metal. - backend/index.yaml meta + 12 image entries (latest/development x cpu, cuda12, cuda13, l4t, vulkan, metal); bump_deps registration for the VLLM_CPP_VERSION pin; root Makefile registration; test-extra runs the unit specs (pure Go, no engine build). - Importers: preference-only swaps - llama-cpp (GGUF) and vllm (safetensors) advertise vllm-cpp via AdditionalBackends and emit backend: vllm-cpp without tokenizer templating (the C ABI takes the FINAL prompt; templating and tool parsing stay LocalAI-side). No auto-detect importer. - Docs: backends list, top-level README maintained-engines table, compatibility table. Verified: 20/20 Ginkgo specs against the real pinned engine and Qwen3.5-2B-UD-Q8_K_XL.gguf on CPU - blocking + streaming parity, greedy determinism, stop words, GBNF-constrained generation, and 4 concurrent streams; plus a dlopen/ABI-gate smoke of the built gRPC server binary. Upstream ABI v2 + production structured-output wiring landed as mudler/vllm.cpp@86013f3. Assisted-by: Claude Code:claude-fable-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(vllm-cpp): ride the autoparser code path - engine-side chat templating and tool engagement (ABI v3) The backend now implements AIModelRich (PredictRich / PredictStreamRich) over vllm.cpp's ABI v3 chat entry points, so chat and tool calling ride the SAME code path as the llama.cpp autoparser: the ENGINE renders the model's chat template, decides when a tool call engages, and parses it - LocalAI receives pre-parsed ChatDelta / ToolCallDelta protos exactly as it does from llama-cpp. - With use_tokenizer_template + structured Messages, PredictOptions lowers to ONE OpenAI chat request JSON (messages, tools, tool_choice, sampling, stream_options.include_usage) for vllm_chat / vllm_chat_stream. tool_choice auto lowers engine-side to a LAZY structural-tag decode constraint - free text until the model emits the tool trigger, then the call is grammar-constrained; required/named force a call. Tool output is parsed by the engine's streaming Hermes-style parser; each chat.completion.chunk maps onto ChatDeltas (content / reasoning_content / tool_calls) which the host already prefers over Go-side tag extraction. Without structured messages the plain path (LocalAI templating + optional GBNF grammar) applies unchanged. - The engine resolves the chat template from the GGUF tokenizer.chat_template metadata (or tokenizer_config.json); templates beyond its minja subset - e.g. the full Qwen3.5 namespace()/macro template - degrade engine-side to a Hermes-aware fallback prompt (tools schemas + <tool_call> instruction) with a stderr witness, so structural-tag engagement keeps working. - Importers now emit the same config shape as llama-cpp for vllm-cpp (use_tokenizer_template: true, no-grammar autoparser flow); only the llama-cpp-specific use_jinja option and the vllm-python parser options are dropped. - Pin bumped to mudler/vllm.cpp@aaed7ec (ABI v3 + chat-prompt resolution). Verified against the real engine and Qwen3.5-2B-UD-Q8_K_XL.gguf on CPU: full suite green - blocking chat, streaming deltas concatenating byte-equal to the blocking answer, a REQUIRED tool call returning schema-valid arguments JSON, and an AUTO run where the engine itself engages get_weather and streams parsed tool deltas; plus unit specs for the request lowering, chunk->ChatDelta mapping, and the C struct mirrors (ABI gate now v3). Assisted-by: Claude Code:claude-fable-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(vllm-cpp): ABI v5 - engine-side parser selection for 30 tool dialects + reasoning Bump the vllm.cpp pin to the autoparser-parity engine: 30 tool-call dialects (every pure-text parser in the pinned vLLM registry, each ported 1:1 with its upstream tests), 7 reasoning parsers, google/minja as the template renderer (the full Qwen3.5 template now renders engine-side), per-family structural tags (tool_choice required/named compiles the model's NATIVE syntax where expressible), and template auto-detection for both parser axes. Backend changes: - cModelParams mirrors ABI v5 (tool_parser + reasoning_parser fields, layout-locked by the offset tests; ABI gate now v5). - New model options tool_parser:<name> / reasoning_parser:<name> pass through to the engine; unset means template auto-detection (18-row tool marker table; [THINK]->mistral, <think>->think_auto for reasoning); "none" disables the reasoning split; unknown names fail the first chat call. - Chat chunks parse the `reasoning` field (the pin renamed reasoning_content), flowing into ChatDelta.ReasoningContent which the host already prefers. Live e2e against Qwen3.5-2B-UD-Q8_K_XL.gguf on CPU, full suite green: the real chat template renders (no more fallback), reasoning auto-detection picks think_auto so markerless answers stay pure content (the live run caught the deepseek_r1 content-swallow upstream and drove the think_auto fix), required tool_choice returns schema-valid arguments, auto tool_choice engages engine-side and streams parsed deltas, and blocking/streaming stay byte-identical. Turn latency also dropped (proper template EOS behavior). Upstream program landed as mudler/vllm.cpp 86013f3..5fffe7e (ABI v2-v5, minja, parser waves B1/B2/B4, reasoning seam, structural-tag registry, think_auto). Assisted-by: Claude Code:claude-fable-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore(vllm-cpp): bump the engine pin to the ENG-wave close-out mudler/vllm.cpp@df8909b: the six engine-backed vLLM tool-parser families (qwen3-coder/xml/mimo, kimi_k2, glm45/47, minimax_m2, gemma4, seed_oss) text-reimplemented from their wire formats and held to the upstream test suites - 39 registered dialects; the pinned vLLM registry is now covered except the three Rust/Harmony-backed families, descoped by decision. kimi_k2 also gains a full native structural-tag builder; four new template auto-detection rows land with test-pinned ordering. Full backend e2e re-run green against Qwen3.5-2B-UD-Q8_K_XL.gguf on CPU. Assisted-by: Claude Code:claude-fable-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(vllm-cpp): add the vllm-cpp-development gallery meta The gallery grew the twelve latest/development image entries but was missing the separate vllm-cpp-development meta (own capabilities map targeting the -development image names), which every backend ships so the development gallery resolves per-platform. Validated: all capability targets in both metas resolve to existing entries, and every image URI's tag suffix matches a backend-matrix build. Also full-stack verified in this change's context (single-node local-ai from this branch, locally-built backend under --backends-path, Qwen3.5-2B GGUF): /v1/chat/completions non-stream (clean content + usage), streaming (SSE deltas), tool_choice auto engaging get_weather engine-side with schema-valid arguments and finish_reason=tool_calls, and streamed tool-call deltas in the standard name-first cadence. Assisted-by: Claude Code:claude-fable-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(vllm-cpp): repair the CI backend builds - gcc-14 -Werror + fat-arch Triton Two distinct failures took down all five vllm-cpp backend builds on the PR: 1. gcc-14 (ubuntu:24.04 CI images; the local toolchain is gcc-13) fails the engine build with -Werror=maybe-uninitialized in InputBatch::condense - a false positive through a staging std::optional's raw storage. Fixed upstream (mudler/vllm.cpp@61f3e85) by moving slot-to-slot directly; verified BOTH ways under dockerized g++-14.2 (unfixed reproduces CI's two diagnostics exactly, fixed compiles clean) with the engine's behavior suites green. Pin bumped to that sha. 2. The amd64 CUDA builds died at CMake configure: the vendored Triton-AOT cubin trees are per-arch and the engine refuses -DVLLM_CPP_TRITON=ON on a multi-arch (120a;121a) fat build unless pinned to one tree, which would be unsound for the other arch. Triton is now enabled only on the single-arch arm64/GB10 build (where the cubins matter); the fat amd64 binary uses the engine's non-AOT GDN path. Backend e2e re-run green at the new pin (Qwen3.5-2B on CPU, full suite). Assisted-by: Claude Code:claude-fable-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(vllm-cpp): cuda-12 images cannot compile compute_121a - target 120a only The second CI round surfaced a CUDA-version constraint: the cuda-12 (12.8) image's nvcc rejects 'compute_121a' (GB10 arch support landed with CUDA 13), killing the amd64 cuda-12 build at nvcc. Gate the architecture list on CUDA_MAJOR_VERSION (exported by Dockerfile.golang): cuda-12 builds consumer Blackwell 120a only, cuda-13 keeps the 120a;121a fat binary, arm64/l4t (cuda-13) keeps single-arch 121a with the Triton cubins. GB10 is arm64, so the amd64 cuda-12 image never served it - no capability change. Verified by Makefile dry-run variable dumps for all three combinations (cuda12 -> 120a; cuda13 -> 120a;121a; cpu -> CUDA off). Assisted-by: Claude Code:claude-fable-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(vllm-cpp): drop the cuda-12 variant - the engine needs the CUDA 13 toolchain Third CI round, third layer: with the arch list already narrowed to 120a, the cuda-12 (12.8) build still dies in ptxas compiling the sm_120a NVFP4 MMA kernels ("Vector type too large, exceeds 128 bit limit") - the Blackwell fp4 path genuinely requires the CUDA 13 toolchain, and vllm.cpp supports Blackwell-family GPUs only. Shipping a cuda-12 image without the fp4 kernels would be a crippled build of an engine whose whole GPU story is fp4, so the variant is dropped instead: - backend-matrix: cuda-12 vllm-cpp entry removed (cuda-13 amd64, l4t arm64, cpu, vulkan, metal remain). - gallery: cuda12 image entries removed; the nvidia capability now resolves to the cuda13 image in both metas; the nvidia-cuda-12 key is dropped so older-driver hosts fall back to the CPU image instead of an unrunnable one. - backend Makefile: BUILD_TYPE=cublas under CUDA_MAJOR_VERSION=12 now fails fast with a clear message; cuda-13 keeps the 120a;121a fat binary and arm64/l4t keeps 121a with the Triton cubins. Verified: Makefile branch dumps for all four combinations (cuda12 loud error, cuda13 fat, arm64 121a+Triton, cpu off), YAML parses, matrix filter tests green, gallery capability targets all resolve. Assisted-by: Claude Code:claude-fable-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(vllm-cpp): forward multi-turn tool identity and reasoning to the engine chatRequestJSON dropped Message.ToolCallId and Message.Name on role="tool" replies and Message.ReasoningContent on assistant history, so a second turn after tool execution reached the engine's chat template without the fields that bind a tool result to the call it answers. Forward all three (present-only, matching the OpenAI wire shape) and pin vllm.cpp to 6a0bd3e7, where ChatMessage parses/round-trips tool_calls, tool_call_id, name and reasoning and the minja adapter exposes them to the template context. Adds the round-trip request-lowering spec (user -> assistant tool_call -> tool reply -> lowered request) and re-ran the gated e2e suite against the new engine pin with a real Qwen3.5 GGUF: chat, reasoning split, streaming parity, required-tool and auto-tool cases all green. Assisted-by: Claude Code:claude-fable-5 [Bash] [Edit] [Read] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(vllm-cpp): bump vllm.cpp for the darwin arm64 i8mm build fix The darwin-metal CI job was the first build to compile the engine's arm CPU-quant files on macOS and hit their Linux-only <asm/hwcap.h> / <sys/auxv.h> includes. vllm.cpp 9e1c9025 detects i8mm per-OS (auxv on Linux, sysctl on Apple Silicon) with kernels untouched. Gated e2e suite re-run green against the new pin with a real Qwen3.5 GGUF. Assisted-by: Claude Code:claude-fable-5 [Bash] [Read] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(vllm-cpp): darwin build - bound cmake parallelism when nproc is absent The macOS runners have no nproc, so JOBS evaluated empty and `cmake --build -j$(JOBS)` became bare `-j`: unlimited clang jobs on a 3-core/7GB Mac, which swap-thrashed until the 6h GHA timeout (the log shows "nproc: Command not found" and 7+ concurrent clang processes being reaped at the cutoff). Use the same portable fallback chain as the other darwin backends: nproc, then sysctl hw.ncpu, then 4. Assisted-by: Claude Code:claude-fable-5 [Bash] [Edit] [Read] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
183 lines
5.7 KiB
Go
183 lines
5.7 KiB
Go
package main
|
|
|
|
// purego bindings for the vllm.cpp stable C ABI (include/vllm.h, ABI v2).
|
|
//
|
|
// The structs below are hand-mirrored PODs of the C declarations, with
|
|
// explicit padding so the Go layout matches the C layout on linux/darwin
|
|
// amd64+arm64. Struct-by-value entry points (the *_default helpers) are NOT
|
|
// bound - purego's struct-return support is platform-dependent - so the
|
|
// defaults are replicated here and guarded by the vllm_abi_version check at
|
|
// startup: a library whose ABI differs from what these mirrors were written
|
|
// against is refused before any request runs.
|
|
|
|
import (
|
|
"fmt"
|
|
"unsafe"
|
|
|
|
"github.com/ebitengine/purego"
|
|
)
|
|
|
|
// abiVersion is the VLLM_ABI_VERSION this file mirrors (vllm.h).
|
|
const abiVersion = 5
|
|
|
|
// vllm_status (vllm.h).
|
|
const (
|
|
vllmOK = 0
|
|
)
|
|
|
|
// cModelParams mirrors vllm_model_params.
|
|
type cModelParams struct {
|
|
ModelPath uintptr // const char*
|
|
TokenizerConfigPath uintptr // const char*
|
|
BlockSize int32
|
|
NumBlocks int32
|
|
MaxModelLen int32
|
|
MaxNumSeqs int32
|
|
ToolParser uintptr // const char*; NULL = auto-detect (ABI v4)
|
|
ReasoningParser uintptr // const char*; NULL = auto-detect (ABI v5)
|
|
}
|
|
|
|
// cSamplingParams mirrors vllm_sampling_params (ABI v2, structured fields
|
|
// included). Padding matches the C compiler's: the uint64 seed is 8-aligned,
|
|
// and each pointer following an int32 is 8-aligned.
|
|
type cSamplingParams struct {
|
|
Temperature float32
|
|
TopP float32
|
|
TopK int32
|
|
MinP float32
|
|
MaxTokens int32
|
|
_ [4]byte
|
|
Seed uint64
|
|
HasSeed int32
|
|
PresencePenalty float32
|
|
FrequencyPenalty float32
|
|
RepetitionPenalty float32
|
|
MinTokens int32
|
|
IgnoreEOS int32
|
|
Stop uintptr // const char* const*
|
|
NStop int32
|
|
_ [4]byte
|
|
StructuredJSON uintptr // const char*
|
|
StructuredRegex uintptr // const char*
|
|
StructuredChoice uintptr // const char* const*
|
|
NStructuredChoice int32
|
|
_ [4]byte
|
|
StructuredGrammar uintptr // const char*
|
|
StructuredJSONObject int32
|
|
_ [4]byte
|
|
}
|
|
|
|
// cCompletion mirrors vllm_completion.
|
|
type cCompletion struct {
|
|
Text uintptr // char*, caller-owned
|
|
FinishReason uintptr // const char*, library-owned
|
|
PromptTokens int32
|
|
CompletionTokens int32
|
|
}
|
|
|
|
// defaultSamplingParams mirrors vllm_sampling_params_default().
|
|
func defaultSamplingParams() cSamplingParams {
|
|
return cSamplingParams{
|
|
Temperature: 1.0,
|
|
TopP: 1.0,
|
|
MaxTokens: 16,
|
|
RepetitionPenalty: 1.0,
|
|
}
|
|
}
|
|
|
|
// defaultModelParams mirrors vllm_model_params_default().
|
|
func defaultModelParams() cModelParams {
|
|
return cModelParams{
|
|
BlockSize: 32,
|
|
NumBlocks: 256,
|
|
MaxNumSeqs: 8,
|
|
}
|
|
}
|
|
|
|
var (
|
|
vllmEngineLoad func(params, out unsafe.Pointer) int32
|
|
vllmEngineFree func(engine uintptr)
|
|
vllmComplete func(engine uintptr, prompt string, params, out unsafe.Pointer) int32
|
|
vllmCompleteStream func(engine uintptr, prompt string, params unsafe.Pointer, cb uintptr, userData uintptr) int32
|
|
vllmChat func(engine uintptr, requestJSON string, out unsafe.Pointer) int32
|
|
vllmChatStream func(engine uintptr, requestJSON string, cb uintptr, userData uintptr) int32
|
|
vllmStringFree func(s uintptr)
|
|
vllmCompletionFree func(out unsafe.Pointer)
|
|
vllmLastError func() string
|
|
vllmVersion func() string
|
|
vllmABIVersion func() int32
|
|
)
|
|
|
|
type libFunc struct {
|
|
ptr any
|
|
name string
|
|
}
|
|
|
|
// registerLib dlopens libvllm and binds the C ABI, refusing an ABI-version
|
|
// mismatch (the struct mirrors above would be undefined behavior against a
|
|
// different layout).
|
|
func registerLib(libName string) error {
|
|
lib, err := purego.Dlopen(libName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
|
|
if err != nil {
|
|
return fmt.Errorf("vllm-cpp: dlopen %s: %w", libName, err)
|
|
}
|
|
for _, lf := range []libFunc{
|
|
{&vllmEngineLoad, "vllm_engine_load"},
|
|
{&vllmEngineFree, "vllm_engine_free"},
|
|
{&vllmComplete, "vllm_complete"},
|
|
{&vllmCompleteStream, "vllm_complete_stream"},
|
|
{&vllmChat, "vllm_chat"},
|
|
{&vllmChatStream, "vllm_chat_stream"},
|
|
{&vllmStringFree, "vllm_string_free"},
|
|
{&vllmCompletionFree, "vllm_completion_free"},
|
|
{&vllmLastError, "vllm_last_error"},
|
|
{&vllmVersion, "vllm_version"},
|
|
{&vllmABIVersion, "vllm_abi_version"},
|
|
} {
|
|
purego.RegisterLibFunc(lf.ptr, lib, lf.name)
|
|
}
|
|
if v := vllmABIVersion(); v != abiVersion {
|
|
return fmt.Errorf("vllm-cpp: ABI mismatch: library reports v%d, backend built against v%d", v, abiVersion)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// cString returns a NUL-terminated byte slice for s. The backing array may be
|
|
// passed to C for the duration of a call (the ABI borrows and copies); keep it
|
|
// alive across the call with runtime.KeepAlive.
|
|
func cString(s string) []byte {
|
|
b := make([]byte, len(s)+1)
|
|
copy(b, s)
|
|
return b
|
|
}
|
|
|
|
// cStringArray builds a NULL-free array of C-string pointers plus the backing
|
|
// buffers that must stay alive for the duration of the C call.
|
|
func cStringArray(ss []string) (ptrs []uintptr, backing [][]byte) {
|
|
backing = make([][]byte, 0, len(ss))
|
|
ptrs = make([]uintptr, 0, len(ss))
|
|
for _, s := range ss {
|
|
b := cString(s)
|
|
backing = append(backing, b)
|
|
ptrs = append(ptrs, uintptr(unsafe.Pointer(&b[0]))) // #nosec G103 -- borrowed by C for the call only
|
|
}
|
|
return ptrs, backing
|
|
}
|
|
|
|
// goString copies a NUL-terminated C string.
|
|
func goString(p uintptr) string {
|
|
if p == 0 {
|
|
return ""
|
|
}
|
|
//nolint:govet // C-owned pointer handed over by purego, valid for this call
|
|
base := unsafe.Pointer(p) // #nosec G103 -- C-owned, copied out immediately
|
|
n := 0
|
|
for *(*byte)(unsafe.Add(base, n)) != 0 {
|
|
n++
|
|
}
|
|
if n == 0 {
|
|
return ""
|
|
}
|
|
return string(unsafe.Slice((*byte)(base), n))
|
|
}
|