Skip to content

Commit ea438cd

Browse files
localai-botmudler
andauthored
feat(vllm-cpp): wire the full engine config surface through engine_args (#11159)
The backend could configure four of the engine's knobs (block size, KV block count, max sequence length, 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 reachable from vllm.cpp's own HTTP server and from nothing LocalAI could write in a model config. Config now goes through `engine_args:`, the same map the vLLM and SGLang backends take, with keys spelled as vLLM's own CLI flags so a speculative_config or kv_transfer_config block written for vLLM works verbatim. The legacy `options:` list keeps working and reads every key too; engine_args wins where both set one. Unknown keys are logged and ignored rather than fatal: the field is shared with the other engines, so a config carrying their knobs must not take the model down. Two details worth knowing: `enable_prefix_caching: false` maps to the ABI tri-state force-OFF (2), not 0. 0 means "let the model capability decide" and dense architectures default the cache on, so collapsing the two would silently enable it against an explicit false. enable_jump_forward (ABI v10) shares the encoding, deferring to VT_ENABLE_JUMP_FORWARD instead of to the model. The importer probes config.json on a vllm-cpp import and writes speculative_config: {method: mtp} when the checkpoint declares an MTP head, the safetensors analogue of the llama-cpp importer's GGUF probe. DFlash draft repos are refused with a warning instead, since a drafter cannot serve alone and the pairing is not derivable from either repo. The draft path is resolved against LocalAI's model directory, because the engine only looks in a directory holding config.json or in the HF cache and never downloads: the repo-id spelling the vLLM docs teach used to die deep in the load with "draft checkpoint not found". docs/content/features/text-generation.md gains a vllm.cpp section covering the engine_args table, all three speculative methods, LMCache and the legacy list. The backend had no documentation page before. This replaces a branch that had gone stale behind master and carried its own route to ABI v10, which #11386 has since landed in minimal form. Rebased onto that as a single commit rather than replaying the intermediate steps, whose ABI v9 mirrors no longer make sense against master's pin. The Darwin build fixes for Apple Clang's gnu-folding-constant diagnostic on C++, Objective-C and Objective-C++, originally authored by localai-org-maint-bot, are folded in here. Verified: `make abi-check` agrees at v10; unit specs, core/config and core/gallery/importers green; and the full e2e passes in 1330s against a CPU libvllm.so reporting ABI v10 with Qwen_Qwen3.5-0.8B-Q4_K_M.gguf (load, blocking completion, streaming, chat and tool calls). Assisted-by: Claude:claude-fable-5 golangci-lint Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 32023f3 commit ea438cd

11 files changed

Lines changed: 1231 additions & 33 deletions

File tree

backend/go/vllm-cpp/Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ endif
9696
UNAME_S := $(shell uname -s)
9797
ifeq ($(UNAME_S),Darwin)
9898
LIB=libvllm.dylib
99+
# Apple Clang diagnoses a pair of constant-folded array bounds in the Metal
100+
# build as a GNU extension. Disable that diagnostic for both Objective-C and
101+
# C++ because vllm.cpp appends target-local -Werror after these global flags.
102+
CMAKE_ARGS+=-DCMAKE_CXX_FLAGS=-Wno-gnu-folding-constant
103+
CMAKE_ARGS+=-DCMAKE_OBJC_FLAGS=-Wno-gnu-folding-constant
104+
CMAKE_ARGS+=-DCMAKE_OBJCXX_FLAGS=-Wno-gnu-folding-constant
99105
else
100106
LIB=libvllm.so
101107
endif

backend/go/vllm-cpp/backend.go

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -109,41 +109,79 @@ func (v *VllmCpp) Load(opts *pb.ModelOptions) error {
109109

110110
v.opts = parseOptions(opts)
111111

112+
// A DFlash draft is a second checkpoint the engine opens by path, and the
113+
// engine never downloads one. Resolve it against LocalAI's models directory
114+
// now so a repo-id spelling works, and so a missing draft fails here with an
115+
// actionable message rather than as an HF-cache miss inside the load.
116+
resolvedSpec, err := resolveDraftModelPath(v.opts.speculativeConfig, opts.ModelPath)
117+
if err != nil {
118+
return err
119+
}
120+
v.opts.speculativeConfig = resolvedSpec
121+
112122
mp := defaultModelParams()
113123
if v.opts.blockSize > 0 {
114124
mp.BlockSize = v.opts.blockSize
115125
}
116126
if v.opts.numBlocks > 0 {
117127
mp.NumBlocks = v.opts.numBlocks
118128
}
129+
// Sequence-length precedence, narrowest source last: context_size is the
130+
// generic LocalAI knob every backend honours, max_model_len is the
131+
// vLLM-specific one, and engine_args.max_model_len is the explicit
132+
// vllm-cpp override.
119133
if opts.ContextSize > 0 {
120134
mp.MaxModelLen = opts.ContextSize
121135
}
136+
if opts.MaxModelLen > 0 {
137+
mp.MaxModelLen = opts.MaxModelLen
138+
}
139+
if v.opts.maxModelLen > 0 {
140+
mp.MaxModelLen = v.opts.maxModelLen
141+
}
122142
if v.opts.maxNumSeqs > 0 {
123143
mp.MaxNumSeqs = v.opts.maxNumSeqs
124144
}
145+
if v.opts.maxNumBatchedTokens > 0 {
146+
mp.MaxNumBatchedTokens = v.opts.maxNumBatchedTokens
147+
}
148+
mp.EnablePrefixCaching = v.opts.enablePrefixCaching
149+
mp.EnableJumpForward = v.opts.enableJumpForward
125150

151+
// Every string below is borrowed by C for the duration of the load call
152+
// only (the library copies what it keeps), so the backing slices just have
153+
// to outlive vllmEngineLoad - hence the single KeepAlive after it.
126154
modelC := cString(model)
127155
mp.ModelPath = uintptr(unsafe.Pointer(&modelC[0])) // #nosec G103 -- borrowed by C for the load call only
128-
var toolParserC, reasoningParserC []byte
129-
if v.opts.toolParser != "" {
130-
toolParserC = cString(v.opts.toolParser)
131-
mp.ToolParser = uintptr(unsafe.Pointer(&toolParserC[0])) // #nosec G103 -- borrowed by C for the load call only
132-
}
133-
if v.opts.reasoningParser != "" {
134-
reasoningParserC = cString(v.opts.reasoningParser)
135-
mp.ReasoningParser = uintptr(unsafe.Pointer(&reasoningParserC[0])) // #nosec G103 -- borrowed by C for the load call only
156+
keep := [][]byte{modelC}
157+
setStr := func(dst *uintptr, s string) {
158+
if s == "" {
159+
return
160+
}
161+
b := cString(s)
162+
keep = append(keep, b)
163+
*dst = uintptr(unsafe.Pointer(&b[0])) // #nosec G103 -- borrowed by C for the load call only
136164
}
165+
setStr(&mp.ToolParser, v.opts.toolParser)
166+
setStr(&mp.ReasoningParser, v.opts.reasoningParser)
167+
setStr(&mp.SpeculativeConfig, v.opts.speculativeConfig)
168+
setStr(&mp.KVTransferConfig, v.opts.kvTransferConfig)
169+
setStr(&mp.SchedulingPolicy, v.opts.schedulingPolicy)
170+
setStr(&mp.TokenizerConfigPath, v.opts.tokenizerConfigPath)
137171

138172
xlog.Info("[vllm-cpp] Load", "model", model, "engine", vllmVersion(),
139173
"blockSize", mp.BlockSize, "numBlocks", mp.NumBlocks,
140-
"maxModelLen", mp.MaxModelLen, "maxNumSeqs", mp.MaxNumSeqs)
174+
"maxModelLen", mp.MaxModelLen, "maxNumSeqs", mp.MaxNumSeqs,
175+
"maxNumBatchedTokens", mp.MaxNumBatchedTokens,
176+
"prefixCaching", triStateName(mp.EnablePrefixCaching),
177+
"jumpForward", triStateName(mp.EnableJumpForward),
178+
"schedulingPolicy", v.opts.schedulingPolicy,
179+
"speculativeConfig", v.opts.speculativeConfig,
180+
"kvTransferConfig", v.opts.kvTransferConfig)
141181

142182
var engine uintptr
143183
rc := vllmEngineLoad(unsafe.Pointer(&mp), unsafe.Pointer(&engine)) // #nosec G103 -- POD out-params
144-
runtime.KeepAlive(modelC)
145-
runtime.KeepAlive(toolParserC)
146-
runtime.KeepAlive(reasoningParserC)
184+
runtime.KeepAlive(keep)
147185
if rc != vllmOK {
148186
return fmt.Errorf("vllm-cpp: engine load failed: %s", vllmLastError())
149187
}

backend/go/vllm-cpp/govllmcpp.go

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,31 +23,54 @@ import (
2323
// registerLib, where it takes the backend down on every load (issue #11379).
2424
const abiVersion = 10
2525

26+
// The ABI's tri-state toggles (enable_prefix_caching ABI v7,
27+
// enable_jump_forward ABI v10) share one encoding: 0 is NOT "off", it is
28+
// "defer" - to the model capability for prefix caching, to the environment for
29+
// jump forward. Only 2 is an explicit off.
30+
const (
31+
triStateDefer int32 = 0
32+
triStateOn int32 = 1
33+
triStateOff int32 = 2
34+
)
35+
36+
// triStateName renders a tri-state for the load log line, where "0" would
37+
// otherwise read as "off" rather than "whatever the default resolves to".
38+
func triStateName(state int32) string {
39+
switch state {
40+
case triStateOn:
41+
return "on"
42+
case triStateOff:
43+
return "off"
44+
default:
45+
return "model-default"
46+
}
47+
}
48+
2649
// vllm_status (vllm.h).
2750
const (
2851
vllmOK = 0
2952
)
3053

31-
// cModelParams mirrors vllm_model_params. The fields the backend does not set
32-
// are still mirrored: the engine reads the whole struct, so the Go value must
33-
// be the same size as the C one. Every one of them is inert when zeroed, which
34-
// is what keeps the engine byte-identical to the pre-v6 behavior.
54+
// cModelParams mirrors vllm_model_params. The int32 fields sit in pairs so the
55+
// interior needs no padding on LP64, but the struct is 8-aligned (it holds
56+
// pointers) and ends on a lone int32, so the trailing pad is explicit. Offsets
57+
// and total size are asserted in vllmcpp_test.go.
3558
type cModelParams struct {
3659
ModelPath uintptr // const char*
37-
TokenizerConfigPath uintptr // const char*
60+
TokenizerConfigPath uintptr // const char*; NULL = <model_dir>/... (ABI v9)
3861
BlockSize int32
3962
NumBlocks int32
4063
MaxModelLen int32
4164
MaxNumSeqs int32
4265
ToolParser uintptr // const char*; NULL = auto-detect (ABI v4)
4366
ReasoningParser uintptr // const char*; NULL = auto-detect (ABI v5)
44-
SpeculativeConfig uintptr // const char*; NULL = no speculation (ABI v6)
45-
EnablePrefixCaching int32 // 0 = model default, 1 = on, 2 = off (ABI v7)
67+
SpeculativeConfig uintptr // const char* JSON; NULL = no speculation (ABI v6)
68+
EnablePrefixCaching int32 // tri-state 0/1/2 (ABI v7)
4669
MaxNumBatchedTokens int32 // <= 0 = per-arch default (ABI v9)
4770
SchedulingPolicy uintptr // const char*; NULL = "fcfs" (ABI v9)
48-
KVTransferConfig uintptr // const char*; NULL = no connector (ABI v9)
49-
EnableJumpForward int32 // 0 = env-resolved (off), 1 = on, 2 = off (ABI v10)
50-
_ [4]byte
71+
KVTransferConfig uintptr // const char* JSON; NULL = no connector (ABI v9)
72+
EnableJumpForward int32 // tri-state 0/1/2 (ABI v10)
73+
_ [4]byte // trailing pad to the struct's 8-byte alignment
5174
}
5275

5376
// cSamplingParams mirrors vllm_sampling_params (structured fields included).
@@ -78,10 +101,12 @@ type cSamplingParams struct {
78101
StructuredGrammar uintptr // const char*
79102
StructuredJSONObject int32
80103
_ [4]byte
81-
// Per-request custom logits processor (ABI v8). Left NULL: a Go callback
82-
// would have to run inside the sampler's decode step for every token.
104+
// ABI v8 tail. LocalAI installs no custom logits processor, but the fields
105+
// MUST be mirrored: the C side reads them off the pointer we hand it, so a
106+
// Go struct that stopped at StructuredJSONObject would have the engine read
107+
// 16 bytes past our allocation and call whatever garbage sat there.
83108
LogitsProcessor uintptr // vllm_logits_processor; NULL = none
84-
LogitsProcessorUserData uintptr // void*, passed back to the callback
109+
LogitsProcessorUserData uintptr // void*
85110
}
86111

87112
// cCompletion mirrors vllm_completion.

0 commit comments

Comments
 (0)