mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-31 10:28:43 -04:00
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]
118 lines
4.7 KiB
Go
118 lines
4.7 KiB
Go
package config
|
|
|
|
// Speculative-decoding auto-defaults for the vllm-cpp backend, the safetensors
|
|
// counterpart of the GGUF/llama.cpp hook in mtp.go.
|
|
//
|
|
// The two engines detect and spell the same feature differently. llama.cpp
|
|
// reads `<arch>.nextn_predict_layers` out of the GGUF header and takes
|
|
// `spec_type:draft-mtp` in `options:`; vllm.cpp reads `mtp_num_hidden_layers`
|
|
// out of the checkpoint's config.json and takes vLLM's own
|
|
// `--speculative-config` JSON, which LocalAI carries in `engine_args`. The
|
|
// engine resolves the draft depth and the default k itself, so the config only
|
|
// has to name the method.
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"github.com/mudler/xlog"
|
|
)
|
|
|
|
// hfSpecConfig is the subset of a HuggingFace config.json that decides whether
|
|
// speculative decoding can be auto-enabled.
|
|
type hfSpecConfig struct {
|
|
ModelType string `json:"model_type"`
|
|
// MtpNumHiddenLayers is the MTP head depth (upstream speculative.py reads
|
|
// it as n_predict for the qwen3_5 / qwen3_5_moe families).
|
|
MtpNumHiddenLayers uint32 `json:"mtp_num_hidden_layers"`
|
|
// DFlashConfig marks a z-lab DFlash DRAFT checkpoint (mask_token_id +
|
|
// target_layer_ids). Its presence means this repo is a draft, not a
|
|
// servable target.
|
|
DFlashConfig json.RawMessage `json:"dflash_config"`
|
|
// TextConfig is where multimodal checkpoints nest the language-model
|
|
// config, and therefore the MTP depth.
|
|
TextConfig *hfSpecConfig `json:"text_config"`
|
|
}
|
|
|
|
// parseHFSpecConfig decodes the speculative-relevant subset of a config.json.
|
|
// A document that does not parse yields nothing rather than an error: detection
|
|
// is best-effort and must never break an import.
|
|
func parseHFSpecConfig(configJSON []byte) (hfSpecConfig, bool) {
|
|
if len(configJSON) == 0 {
|
|
return hfSpecConfig{}, false
|
|
}
|
|
var c hfSpecConfig
|
|
if err := json.Unmarshal(configJSON, &c); err != nil {
|
|
xlog.Debug("[vllm-spec] config.json did not parse; skipping detection", "error", err)
|
|
return hfSpecConfig{}, false
|
|
}
|
|
return c, true
|
|
}
|
|
|
|
// IsDFlashDraftConfig reports whether a HuggingFace config.json describes a
|
|
// DFlash DRAFT checkpoint. Unlike MTP - whose head ships inside the target
|
|
// checkpoint's `mtp.*` tensors - a DFlash draft is its own repo that can only
|
|
// run paired with a target it verifies against, so it must never be configured
|
|
// as a standalone model.
|
|
func IsDFlashDraftConfig(configJSON []byte) bool {
|
|
c, ok := parseHFSpecConfig(configJSON)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return len(c.DFlashConfig) > 0 ||
|
|
(c.TextConfig != nil && len(c.TextConfig.DFlashConfig) > 0)
|
|
}
|
|
|
|
// HasSafetensorsMTPHead reports whether a HuggingFace config.json declares a
|
|
// self-speculating Multi-Token Prediction head, returning its depth. The depth
|
|
// is informational: vllm.cpp resolves n_predict and the default
|
|
// num_speculative_tokens from the checkpoint itself.
|
|
//
|
|
// DFlash drafts are excluded for the same reason `gemma4-assistant` GGUFs are
|
|
// excluded from the llama.cpp hook: they carry head metadata but cannot
|
|
// self-speculate.
|
|
//
|
|
// NOTE this is a safetensors-only signal. vllm.cpp rejects an MTP config over a
|
|
// GGUF source, because the `mtp.*` draft tensors only exist in the safetensors
|
|
// checkpoint - so the GGUF import path must not use this.
|
|
func HasSafetensorsMTPHead(configJSON []byte) (uint32, bool) {
|
|
c, ok := parseHFSpecConfig(configJSON)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
if IsDFlashDraftConfig(configJSON) {
|
|
return 0, false
|
|
}
|
|
n := c.MtpNumHiddenLayers
|
|
if n == 0 && c.TextConfig != nil {
|
|
n = c.TextConfig.MtpNumHiddenLayers
|
|
}
|
|
return n, n > 0
|
|
}
|
|
|
|
// ApplyVLLMSpeculativeDefaults enables MTP speculative decoding in cfg's
|
|
// engine_args when nothing is configured there yet. It is a no-op when the user
|
|
// already set a speculative_config, so an explicit choice (a different method,
|
|
// an explicit k, a DFlash draft) is never clobbered.
|
|
//
|
|
// `layers` is the detected head depth and is only used for the diagnostic log
|
|
// line - the engine derives the real k from the checkpoint.
|
|
func ApplyVLLMSpeculativeDefaults(cfg *ModelConfig, layers uint32) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
if _, set := cfg.EngineArgs["speculative_config"]; set {
|
|
xlog.Debug("[vllm-spec] MTP head detected but speculative_config already configured; leaving user choice intact",
|
|
"name", cfg.Name, "mtp_num_hidden_layers", layers)
|
|
return
|
|
}
|
|
if cfg.EngineArgs == nil {
|
|
cfg.EngineArgs = map[string]any{}
|
|
}
|
|
// Only the method: vllm.cpp defaults num_speculative_tokens to the
|
|
// checkpoint's own n_predict (speculative.py:865-875), which is the right
|
|
// value far more reliably than anything guessable here.
|
|
cfg.EngineArgs["speculative_config"] = map[string]any{"method": "mtp"}
|
|
xlog.Info("[vllm-spec] MTP head detected; enabling mtp speculative decoding",
|
|
"name", cfg.Name, "mtp_num_hidden_layers", layers)
|
|
}
|