Skip to content

Commit 066abf8

Browse files
localai-botmudler
andauthored
feat(llama-cpp): cpu_moe/n_cpu_moe options + generic upstream-flag passthrough (#10490)
* feat(llama-cpp): add main-model cpu_moe/n_cpu_moe options Mirror the existing draft_cpu_moe/draft_n_cpu_moe siblings for the main model, matching upstream --cpu-moe / --n-cpu-moe (common/arg.cpp). Lets users keep MoE expert weights on CPU to manage VRAM on large MoE models. Closes part of #10483 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(llama-cpp): forward unknown '-' options to upstream arg parser Any options: entry starting with '-' is collected and passed verbatim to llama.cpp's own common_params_parse (LLAMA_EXAMPLE_SERVER) at the end of params_parse, so every upstream llama-server flag works without a new hand-wired branch. Passthrough runs last and wins on overlap; n_parallel is snapshotted to survive parser_init's SERVER reset, and help/usage/completion flags are skipped to avoid exiting the backend. Closes #10483 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * docs(llama-cpp): document cpu_moe/n_cpu_moe and option passthrough Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(llama-cpp): terminate tensor/kv override vectors after passthrough The tensor_buft_overrides padding and the kv/draft override terminators ran before the generic option passthrough, so a passthrough flag (--cpu-moe, --override-tensor, --override-kv, ...) appended a real entry after the null sentinel - tripping the model loader's back().pattern == nullptr assertion (crash) or being silently dropped. Move all three termination/padding blocks to the end of params_parse, after both the named-option loop and common_params_parse have pushed their real entries. Also widen the exit()-flag skip list so --version, --license, --list-devices and --cache-list cannot terminate the backend. 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>
1 parent a7fec9a commit 066abf8

2 files changed

Lines changed: 150 additions & 21 deletions

File tree

backend/cpp/llama-cpp/grpc-server.cpp

Lines changed: 117 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
#include "backend.pb.h"
3838
#include "backend.grpc.pb.h"
3939
#include "common.h"
40+
#include "arg.h"
4041
#include "chat-auto-parser.h"
4142
#include <getopt.h>
4243
#include <grpcpp/ext/proto_server_reflection_plugin.h>
@@ -592,6 +593,10 @@ static void params_parse(server_context& /*ctx_server*/, const backend::ModelOpt
592593
params.checkpoint_min_step = 256;
593594
#endif
594595

596+
// Raw upstream llama-server flags collected from any option entry that
597+
// starts with '-'. Applied once after the loop via common_params_parse.
598+
std::vector<std::string> extra_argv;
599+
595600
// decode options. Options are in form optname:optvale, or if booleans only optname.
596601
for (int i = 0; i < request->options_size(); i++) {
597602
std::string opt = request->options(i);
@@ -1080,6 +1085,31 @@ static void params_parse(server_context& /*ctx_server*/, const backend::ModelOpt
10801085
} catch (...) {}
10811086
}
10821087

1088+
// --- main model MoE on CPU (upstream --cpu-moe / --n-cpu-moe) ---
1089+
} else if (!strcmp(optname, "cpu_moe")) {
1090+
// Bool-style flag: keep all MoE expert weights on CPU.
1091+
const bool enable = (optval == NULL) ||
1092+
optval_str == "true" || optval_str == "1" || optval_str == "yes" ||
1093+
optval_str == "on" || optval_str == "enabled";
1094+
if (enable) {
1095+
params.tensor_buft_overrides.push_back(llm_ffn_exps_cpu_override());
1096+
}
1097+
} else if (!strcmp(optname, "n_cpu_moe")) {
1098+
if (optval != NULL) {
1099+
try {
1100+
int n = std::stoi(optval_str);
1101+
if (n < 0) n = 0;
1102+
// Keep override-name storage alive for the lifetime of the
1103+
// params struct (mirrors upstream arg.cpp's function-local static).
1104+
static std::list<std::string> buft_overrides_main;
1105+
for (int i = 0; i < n; ++i) {
1106+
buft_overrides_main.push_back(llm_ffn_exps_block_regex(i));
1107+
params.tensor_buft_overrides.push_back(
1108+
{buft_overrides_main.back().c_str(), ggml_backend_cpu_buffer_type()});
1109+
}
1110+
} catch (...) {}
1111+
}
1112+
10831113
// --- draft model tensor buffer overrides (upstream --spec-draft-override-tensor) ---
10841114
} else if (!strcmp(optname, "draft_override_tensor") || !strcmp(optname, "spec_draft_override_tensor")) {
10851115
// Format: <tensor regex>=<buffer type>,<tensor regex>=<buffer type>,...
@@ -1111,6 +1141,30 @@ static void params_parse(server_context& /*ctx_server*/, const backend::ModelOpt
11111141
else { cur.push_back(c); }
11121142
}
11131143
if (!cur.empty()) flush(cur);
1144+
1145+
// --- generic passthrough: any entry starting with '-' is a raw
1146+
// upstream llama-server flag, forwarded verbatim to the parser. ---
1147+
} else if (optname[0] == '-') {
1148+
std::string flag = optname;
1149+
// These flags make upstream's parser exit() (printing usage /
1150+
// completion), which would kill the backend process. Skip them.
1151+
if (flag == "-h" || flag == "--help" || flag == "--usage" ||
1152+
flag == "--version" || flag == "--license" ||
1153+
flag == "--list-devices" || flag == "-cl" ||
1154+
flag == "--cache-list" ||
1155+
flag.rfind("--completion", 0) == 0) {
1156+
fprintf(stderr,
1157+
"[llama-cpp] ignoring passthrough flag that would exit: %s\n",
1158+
flag.c_str());
1159+
} else {
1160+
extra_argv.push_back(flag);
1161+
// Preserve the whole value after the first ':' so embedded
1162+
// colons (e.g. host:port) survive strtok's truncation of optval.
1163+
auto colon = opt.find(':');
1164+
if (colon != std::string::npos) {
1165+
extra_argv.push_back(opt.substr(colon + 1));
1166+
}
1167+
}
11141168
}
11151169
}
11161170

@@ -1146,27 +1200,6 @@ static void params_parse(server_context& /*ctx_server*/, const backend::ModelOpt
11461200
}
11471201
}
11481202

1149-
if (!params.kv_overrides.empty()) {
1150-
params.kv_overrides.emplace_back();
1151-
params.kv_overrides.back().key[0] = 0;
1152-
}
1153-
1154-
// tensor_buft_overrides sentinel termination (mirrors upstream common/arg.cpp).
1155-
// Real entries are pushed during option parsing; here we pad/terminate so the
1156-
// model loader sees back().pattern == nullptr (GGML_ASSERT at common.cpp:1543)
1157-
// and so llama_params_fit has the placeholder slots it requires.
1158-
{
1159-
const size_t ntbo = llama_max_tensor_buft_overrides();
1160-
while (params.tensor_buft_overrides.size() < ntbo) {
1161-
params.tensor_buft_overrides.push_back({nullptr, nullptr});
1162-
}
1163-
}
1164-
// Terminate the draft tensor_buft_overrides list with a sentinel, mirroring
1165-
// the main-model handling above.
1166-
if (!params.speculative.draft.tensor_buft_overrides.empty()) {
1167-
params.speculative.draft.tensor_buft_overrides.push_back({nullptr, nullptr});
1168-
}
1169-
11701203
// TODO: Add yarn
11711204

11721205
if (!request->tensorsplit().empty()) {
@@ -1259,6 +1292,69 @@ static void params_parse(server_context& /*ctx_server*/, const backend::ModelOpt
12591292
params.sampling.grammar_triggers.push_back(std::move(trigger));
12601293
}
12611294
}
1295+
1296+
// Apply any raw upstream flags last so an explicit passthrough flag wins
1297+
// over the LocalAI-resolved field it maps to (e.g. --ctx-size beats
1298+
// context_size). This is the same parser llama-server itself uses.
1299+
if (!extra_argv.empty()) {
1300+
// common_params_parser_init resets a few fields for the SERVER example
1301+
// (n_parallel -> -1, use_color). Snapshot n_parallel so an unrelated
1302+
// passthrough flag can't silently clobber LocalAI's resolved value.
1303+
const int saved_n_parallel = params.n_parallel;
1304+
1305+
std::vector<char *> argv;
1306+
std::string prog = "llama-server";
1307+
argv.push_back(prog.data());
1308+
for (auto & a : extra_argv) {
1309+
argv.push_back(a.data());
1310+
}
1311+
1312+
// ctx_arg.params is a reference, so this overlays the given flags onto
1313+
// `params` in place. Returns false on a recoverable parse error (and
1314+
// self-restores params); may exit() on a hard error, exactly as
1315+
// passing the same bad flag to llama-server would.
1316+
if (!common_params_parse((int)argv.size(), argv.data(), params,
1317+
LLAMA_EXAMPLE_SERVER)) {
1318+
fprintf(stderr,
1319+
"[llama-cpp] failed to parse passthrough options; ignoring them\n");
1320+
}
1321+
1322+
// Restore n_parallel unless a passthrough flag explicitly set it
1323+
// (parser_init's reset sentinel for SERVER is -1).
1324+
if (params.n_parallel == -1) {
1325+
params.n_parallel = saved_n_parallel;
1326+
}
1327+
}
1328+
1329+
// Terminate/pad the override vectors only after BOTH the named-option loop
1330+
// and the generic passthrough (common_params_parse above) have pushed their
1331+
// real entries, so back() is the null sentinel the model loader asserts on.
1332+
// Running these before the passthrough let a passthrough flag (--cpu-moe,
1333+
// --override-tensor, --override-kv, ...) append a real entry after the
1334+
// sentinel: a GGML_ASSERT crash for tensor_buft_overrides, a silent drop for
1335+
// kv_overrides. Double-termination is harmless (the while is a no-op if the
1336+
// passthrough parse already padded; an extra trailing null is ignored).
1337+
1338+
if (!params.kv_overrides.empty()) {
1339+
params.kv_overrides.emplace_back();
1340+
params.kv_overrides.back().key[0] = 0;
1341+
}
1342+
1343+
// tensor_buft_overrides sentinel termination (mirrors upstream common/arg.cpp).
1344+
// Real entries are pushed during option parsing; here we pad/terminate so the
1345+
// model loader sees back().pattern == nullptr (GGML_ASSERT at common.cpp:1543)
1346+
// and so llama_params_fit has the placeholder slots it requires.
1347+
{
1348+
const size_t ntbo = llama_max_tensor_buft_overrides();
1349+
while (params.tensor_buft_overrides.size() < ntbo) {
1350+
params.tensor_buft_overrides.push_back({nullptr, nullptr});
1351+
}
1352+
}
1353+
// Terminate the draft tensor_buft_overrides list with a sentinel, mirroring
1354+
// the main-model handling above.
1355+
if (!params.speculative.draft.tensor_buft_overrides.empty()) {
1356+
params.speculative.draft.tensor_buft_overrides.push_back({nullptr, nullptr});
1357+
}
12621358
}
12631359

12641360

docs/content/advanced/model-configuration.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,39 @@ These llama.cpp options are passed through the `options:` array.
494494
| `direct_io` / `use_direct_io` | bool | `false` | Open the model with `O_DIRECT` (faster cold loads on NVMe; ignored if not supported). |
495495
| `verbosity` | int | `3` | llama.cpp internal log verbosity threshold. Higher = more verbose. |
496496
| `override_tensor` / `tensor_buft_overrides` | string | "" | Per-tensor buffer-type overrides for the main model. Format: `<tensor regex>=<buffer type>,<tensor regex>=<buffer type>,...`. Mirrors the existing `draft_override_tensor` syntax for the draft model. |
497+
| `cpu_moe` | bool | false | Keep all MoE expert weights of the main model on CPU (upstream `--cpu-moe`). Frees VRAM on large MoE models (DeepSeek, Qwen3 `*-A3B`). |
498+
| `n_cpu_moe` | int | 0 | Keep MoE expert weights of the first N main-model layers on CPU (upstream `--n-cpu-moe`). |
499+
500+
#### Generic option passthrough
501+
502+
Any `options:` entry whose name starts with `-` is forwarded **verbatim** to
503+
upstream llama.cpp's own `llama-server` argument parser. This means any flag the
504+
bundled llama.cpp supports works without LocalAI needing a dedicated option,
505+
even ones added after your LocalAI version was built. See the upstream
506+
[server flags reference](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md).
507+
508+
Format mirrors the rest of the array - `--flag` for a boolean, or `--flag:value`
509+
for a flag that takes a value. Everything after the first `:` is the value, so
510+
embedded colons (e.g. `host:port`) are preserved:
511+
512+
```yaml
513+
options:
514+
- "--cpu-moe" # boolean flag
515+
- "--n-cpu-moe:4" # flag with a value
516+
- "--override-tensor:exps=CPU"
517+
```
518+
519+
Notes:
520+
521+
- **Precedence:** passthrough flags are applied last, so an explicit flag
522+
overrides the LocalAI option it maps to (e.g. `--ctx-size:8192` overrides
523+
`context_size`).
524+
- **Power-user territory:** an invalid flag or value is rejected by the upstream
525+
parser exactly as it would be by `llama-server`, which can fail model loading.
526+
Prefer the named options above when one exists.
527+
- Flags that would terminate the process (such as `--help`, `--usage`,
528+
`--version`, `--license`, `--list-devices`, `--cache-list`, and
529+
`--completion*`) are ignored.
497530

498531
### Prompt Caching
499532

0 commit comments

Comments
 (0)