Files
LocalAI/backend/go/vllm-cpp/govllmcpp.go
Ettore Di Giacinto 5945b0eb11 feat(vllm-cpp): wire the full engine config surface through engine_args
The vllm-cpp backend could configure four of the engine's knobs - block size,
KV block count, max sequence length and max concurrent sequences - out of a
config surface that is considerably larger. Speculative decoding, prefix
caching, the chunked-prefill token budget, the scheduling policy and the
external KV connector were all reachable from vllm.cpp's own HTTP server and
from nothing LocalAI could write in a model config.

Part of that gap was the C ABI itself, which carried strictly less than
EngineParams does; that is fixed upstream in vllm.cpp ABI v9 (this bumps the pin
to it). The rest was here: the backend parsed a flat `options:` list with five
recognised keys and had no way to express a nested JSON document at all.

Configuration now goes through `engine_args:`, the same map the vLLM and SGLang
backends already take, with keys spelled as vLLM's own CLI flags - so a
`speculative_config` or `kv_transfer_config` block written for vLLM works
verbatim:

  engine_args:
    max_num_batched_tokens: 8192
    enable_prefix_caching: true
    scheduling_policy: lpm
    speculative_config:
      method: dflash
      model: z-lab/Qwen3.6-27B-DFlash
      num_speculative_tokens: 4
    kv_transfer_config:
      kv_connector: LMCacheConnector
      kv_role: kv_both
      kv_connector_extra_config: {host: 127.0.0.1, port: 65432}

The `options:` list keeps working, and now reads every key too, so no existing
config breaks; engine_args wins where both set the same key.

Two details worth calling out. `enable_prefix_caching: false` maps to the ABI's
force-OFF state (2), not the 0 that means "let the model capability decide" -
collapsing them would silently turn the cache ON for the dense architectures
that default it on. And cSamplingParams grows the ABI v8 logits-processor tail:
LocalAI installs no processor, but the C side reads those fields off the pointer
we hand it, so a Go struct that stopped short would have had the engine read 16
bytes past our allocation and call whatever sat there.

The importer gets the safetensors counterpart of the llama-cpp MTP hook: a
`vllm-cpp` import of a HuggingFace repo probes config.json and, on a checkpoint
that declares an MTP head, writes speculative_config {method: mtp} into the
generated engine_args. DFlash draft repositories are detected and refused with a
warning rather than configured as standalone models, since a drafter cannot
serve alone. The llama-cpp importer stops applying its own `spec_type:draft-mtp`
options when the chosen backend is vllm-cpp: those are llama.cpp option keys
vllm-cpp does not read, and vllm.cpp rejects MTP over a GGUF source anyway
because the `mtp.*` draft tensors only exist in the safetensors checkpoint.

docs/content/features/text-generation.md gains a vllm.cpp section - the backend
had no documentation page at all - covering the engine_args table, all three
speculative methods, LMCache, and the legacy options list.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [ClaudeCode]
2026-07-29 09:06:36 +00:00

218 lines
7.3 KiB
Go

package main
// purego bindings for the vllm.cpp stable C ABI (include/vllm.h, ABI v9).
//
// 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 = 9
// Prefix-caching tri-state (vllm_model_params.enable_prefix_caching, ABI v7).
// 0 is the model-capability default, which is what a config that says nothing
// about prefix caching must produce.
const (
prefixCachingModelDefault int32 = 0
prefixCachingOn int32 = 1
prefixCachingOff int32 = 2
)
// prefixCachingName renders the tri-state for the load log line, where "0"
// would otherwise read as "off" rather than "whatever the model defaults to".
func prefixCachingName(state int32) string {
switch state {
case prefixCachingOn:
return "on"
case prefixCachingOff:
return "off"
default:
return "model-default"
}
}
// vllm_status (vllm.h).
const (
vllmOK = 0
)
// cModelParams mirrors vllm_model_params. Every field is naturally aligned on
// LP64 (the int32 pairs sit together), so the Go layout needs no explicit
// padding; the offsets are asserted in vllmcpp_test.go.
type cModelParams struct {
ModelPath uintptr // const char*
TokenizerConfigPath uintptr // const char*; NULL = <model_dir>/... (ABI v9)
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)
SpeculativeConfig uintptr // const char* JSON; NULL = no speculation (ABI v6)
EnablePrefixCaching int32 // tri-state 0/1/2 (ABI v7)
MaxNumBatchedTokens int32 // <= 0 = per-arch default (ABI v9)
SchedulingPolicy uintptr // const char*; NULL = "fcfs" (ABI v9)
KVTransferConfig uintptr // const char* JSON; NULL = no connector (ABI v9)
}
// 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
// ABI v8 tail. LocalAI installs no custom logits processor, but the fields
// MUST be mirrored: the C side reads them off the pointer we hand it, so a
// Go struct that stopped at StructuredJSONObject would have the engine read
// 16 bytes past our allocation and call whatever garbage sat there.
LogitsProcessor uintptr // vllm_logits_processor; NULL = none
LogitsProcessorUserData uintptr // void*
}
// 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))
}